diff --git a/packages/studio/src/components/nle/TimelinePane.test.ts b/packages/studio/src/components/nle/TimelinePane.test.ts deleted file mode 100644 index 1e4c0c082f..0000000000 --- a/packages/studio/src/components/nle/TimelinePane.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import type { TimelineElement } from "../../player"; -import { - forwardRebasedTimelineMoveElements, - forwardRebasedTimelineResizeElements, -} from "./TimelinePane"; - -describe("TimelinePane move wrapper", () => { - it("rebases expanded edits and forwards track-insert as the third argument", async () => { - const onMoveElements = vi.fn().mockResolvedValue(undefined); - const element: TimelineElement = { - id: "expanded-a", - domId: "a", - tag: "div", - start: 12, - duration: 2, - track: 0, - expandedParentStart: 10, - }; - await forwardRebasedTimelineMoveElements( - [{ element, updates: { start: 14, track: 2 } }], - "clip-lane-move:7", - "track-insert", - onMoveElements, - Number.POSITIVE_INFINITY, - ); - expect(onMoveElements).toHaveBeenCalledWith( - [ - { - element: expect.objectContaining({ id: "a", start: 2 }), - updates: { start: 4, track: 2 }, - }, - ], - "clip-lane-move:7", - "track-insert", - // The per-gesture coalesce window rides along with the shared key. - Number.POSITIVE_INFINITY, - ); - }); - - it("forwards one rebased resize batch with the shared gesture key", async () => { - const onResizeElements = vi.fn().mockResolvedValue(undefined); - const element: TimelineElement = { - id: "expanded-a", - domId: "a", - tag: "div", - start: 12, - duration: 2, - track: 0, - expandedParentStart: 10, - }; - await forwardRebasedTimelineResizeElements( - [{ element, start: 13, duration: 3 }], - { coalesceKey: "clip-group-resize:a:b" }, - onResizeElements, - ); - expect(onResizeElements).toHaveBeenCalledTimes(1); - expect(onResizeElements).toHaveBeenCalledWith( - [{ element: expect.objectContaining({ id: "a", start: 2 }), start: 3, duration: 3 }], - { coalesceKey: "clip-group-resize:a:b" }, - ); - }); -}); diff --git a/packages/studio/src/components/nle/TimelinePane.tsx b/packages/studio/src/components/nle/TimelinePane.tsx index 727fef32fb..f08c29615f 100644 --- a/packages/studio/src/components/nle/TimelinePane.tsx +++ b/packages/studio/src/components/nle/TimelinePane.tsx @@ -1,77 +1,10 @@ -import { useCallback, type ReactNode } from "react"; +import type { ReactNode } from "react"; import { Timeline } from "../../player"; import type { TimelineElement, TimelineTimeRange } from "../../player"; import type { BlockedTimelineEditIntent } from "../../player/components/timelineEditing"; import { AudioMeterStrip } from "./AudioMeterStrip"; import { useTimelineEditContext } from "../../contexts/TimelineEditContext"; -import { trackStudioExpandedClipEdit } from "../../telemetry/events"; import { useNLEContext } from "./NLEContext"; -import type { TimelineMoveOperation } from "../../hooks/timelineMoveAdapter"; - -type TimelineMoveEdit = { - element: TimelineElement; - updates: Pick; -}; - -export function forwardRebasedTimelineMoveElements( - edits: TimelineMoveEdit[], - coalesceKey: string | undefined, - operation: TimelineMoveOperation | undefined, - onMoveElements: ( - edits: TimelineMoveEdit[], - coalesceKey?: string, - operation?: TimelineMoveOperation, - coalesceMs?: number, - ) => Promise | void, - coalesceMs?: number, -) { - return onMoveElements( - edits.map(({ element, updates }) => { - const basis = element.expandedParentStart; - if (basis === undefined) return { element, updates }; - return { - element: { ...element, id: element.domId ?? element.id, start: element.start - basis }, - updates: { ...updates, start: Math.max(0, updates.start - basis) }, - }; - }), - coalesceKey, - operation, - coalesceMs, - ); -} - -type TimelineResizeChange = { - element: TimelineElement; - start: number; - duration: number; - playbackStart?: number; -}; - -export function forwardRebasedTimelineResizeElements( - changes: TimelineResizeChange[], - options: { coalesceKey?: string } | undefined, - onResizeElements: ( - changes: TimelineResizeChange[], - options?: { coalesceKey?: string }, - ) => Promise | void, -) { - return onResizeElements( - changes.map((change) => { - const basis = change.element.expandedParentStart; - if (basis === undefined) return change; - return { - ...change, - element: { - ...change.element, - id: change.element.domId ?? change.element.id, - start: change.element.start - basis, - }, - start: Math.max(0, change.start - basis), - }; - }), - options, - ); -} export interface TimelinePaneProps { /** Slot rendered above the timeline tracks (toolbar with split, delete, zoom) */ @@ -114,7 +47,6 @@ export interface TimelinePaneProps { canPasteClip?: () => boolean; } -// fallow-ignore-next-line complexity export function TimelinePane({ timelineToolbar, timelineFooter, @@ -142,119 +74,10 @@ export function TimelinePane({ timelineSessionEpoch, } = useNLEContext(); - // Move/resize/split come from the timeline edit context, not props — the - // wrappers below intercept expanded clips and must call the *real* handlers. - // (Delete is a direct prop; it stays that way.) + // Move/resize/split come from the timeline edit context, not props. const { onMoveElement, onMoveElements, onResizeElement, onResizeElements, onSplitElement } = useTimelineEditContext(); - // An expanded sub-comp child reaches the normal edit handlers in its own - // local coordinates: addressed by its real DOM id, with timeline time rebased - // onto the sub-comp it lives in. The handlers then save + reloadPreview exactly - // as they do for top-level clips — no separate live-DOM path. - const toLocalElement = useCallback( - (element: TimelineElement, basis: number): TimelineElement => ({ - ...element, - id: element.domId ?? element.id, - start: element.start - basis, - }), - [], - ); - - const handleMoveElement = useCallback( - (element: TimelineElement, updates: Pick) => { - const basis = element.expandedParentStart; - if (basis === undefined) return onMoveElement?.(element, updates); - trackStudioExpandedClipEdit({ action: "move" }); - onMoveElement?.(toLocalElement(element, basis), { - ...updates, - start: Math.max(0, updates.start - basis), - }); - }, - [onMoveElement, toLocalElement], - ); - - // Batched move (ripple / insert): rebase each expanded sub-comp child to its - // local coords, exactly as handleMoveElement does for a single clip. - const handleMoveElements = useCallback( - ( - edits: Array<{ element: TimelineElement; updates: Pick }>, - coalesceKey?: string, - operation?: TimelineMoveOperation, - coalesceMs?: number, - ) => { - // Match the sibling handlers: report the telemetry when the batch touches at - // least one expanded sub-comp child (the clips being rebased to local coords). - if (edits.some(({ element }) => element.expandedParentStart !== undefined)) { - trackStudioExpandedClipEdit({ action: "move" }); - } - if (!onMoveElements) return; - return forwardRebasedTimelineMoveElements( - edits, - coalesceKey, - operation, - onMoveElements, - coalesceMs, - ); - }, - [onMoveElements], - ); - - const handleResizeElement = useCallback( - ( - element: TimelineElement, - updates: Pick, - ) => { - const basis = element.expandedParentStart; - if (basis === undefined) return onResizeElement?.(element, updates); - trackStudioExpandedClipEdit({ action: "resize" }); - onResizeElement?.(toLocalElement(element, basis), { - ...updates, - start: Math.max(0, updates.start - basis), - }); - }, - [onResizeElement, toLocalElement], - ); - - const handleResizeElements = useCallback( - ( - changes: Array<{ - element: TimelineElement; - start: number; - duration: number; - playbackStart?: number; - }>, - options?: { coalesceKey?: string }, - ) => { - if (!onResizeElements) return; - if (changes.some(({ element }) => element.expandedParentStart !== undefined)) { - trackStudioExpandedClipEdit({ action: "resize" }); - } - return forwardRebasedTimelineResizeElements(changes, options, onResizeElements); - }, - [onResizeElements], - ); - - const handleDeleteElement = useCallback( - (element: TimelineElement) => { - const basis = element.expandedParentStart; - if (basis === undefined) return onDeleteElement?.(element); - trackStudioExpandedClipEdit({ action: "delete" }); - return onDeleteElement?.(toLocalElement(element, basis)); - }, - [onDeleteElement, toLocalElement], - ); - - const handleSplitElement = useCallback( - (element: TimelineElement, splitTime: number) => { - const basis = element.expandedParentStart; - if (basis === undefined) return onSplitElement?.(element, splitTime); - trackStudioExpandedClipEdit({ action: "split" }); - return onSplitElement?.(toLocalElement(element, basis), Math.max(0, splitTime - basis)); - }, - [onSplitElement, toLocalElement], - ); - return (
{ expect(history.moveCoalesceKeys).toEqual([]); }); - it("maps the crossed neighbor to its timeline key and rebases expanded sub-comp children", async () => { - // t is an expanded sub-comp child (expandedParentStart 5, absolute start 5): - // the mirror must forward its persist in LOCAL time (start 0), the same - // rebase a timeline lane drag applies (forwardRebasedTimelineMoveElements). - setStoreElements([ - { - ...storeEl("a", 0, 25, 5), - sourceFile: "sub.html", - key: "sub.html#a", - expandedParentStart: 5, - }, - { - ...storeEl("b", 1, 5, 10), - sourceFile: "sub.html", - key: "sub.html#b", - expandedParentStart: 5, - }, - { - ...storeEl("t", 2, 5, 10), - sourceFile: "sub.html", - key: "sub.html#t", - expandedParentStart: 5, - }, - ]); - const edits: Array<{ element: TimelineElement; updates: { start: number; track: number } }> = - []; - const onMoveElements: TimelineEditCallbacks["onMoveElements"] = (batch) => { - edits.push(...batch); - }; - const { mirror } = mountMirrorOnlyHarness(onMoveElements); - - const mirrored = await act(async () => - mirror({ - selectionKey: "sub.html#t", - action: "bring-forward", - // The crossed sibling maps to sub.html#b via its DOM id + sourceFile — - // the same derivation reorder entries use (deriveTimelineStoreKey). - crossed: domTarget("b"), - sourceFile: "sub.html", - coalesceKey: "z-reorder:bring-forward:t", - }), - ); - expect(mirrored).toBe(true); - expect(edits).toHaveLength(1); - // Rebased to sub-comp local coords: absolute 5 − parent start 5 = 0. - expect(edits[0].element.start).toBe(0); - expect(edits[0].updates).toMatchObject({ start: 0, track: 0 }); - }); - it("maps a cross-file duplicate selector to the source-scoped crossed occurrence", async () => { setStoreElements([ { diff --git a/packages/studio/src/components/nle/useCanvasZOrderTimelineMirror.ts b/packages/studio/src/components/nle/useCanvasZOrderTimelineMirror.ts index d4edd34559..c066293226 100644 --- a/packages/studio/src/components/nle/useCanvasZOrderTimelineMirror.ts +++ b/packages/studio/src/components/nle/useCanvasZOrderTimelineMirror.ts @@ -13,7 +13,6 @@ import { commitZMirrorLaneMove } from "../../player/components/timelineClipDragC import { deriveTimelineStoreKey } from "../../player/lib/timelineElementHelpers"; import { buildStableSelector, getSelectorIndex } from "../editor/domEditingDom"; import { useStudioShellContextOptional } from "../../contexts/StudioContext"; -import { forwardRebasedTimelineMoveElements } from "./TimelinePane"; export interface MirrorZOrderInput { /** Timeline store key of the element the menu acted on (entry.key), if any. */ @@ -44,10 +43,9 @@ export interface MirrorZOrderInput { * renders and the resolver expects. No alternate row expansion is built here. * * The mirror persists through the SAME machinery as a timeline lane drag - * (commitZMirrorLaneMove → persistMoveEdits → onMoveElements, with expanded - * children rebased to local coords via forwardRebasedTimelineMoveElements) — - * optimistic store update + rollback included, so the timeline reflects the - * lane change without a reload. The deps below deliberately OMIT + * (commitZMirrorLaneMove → persistMoveEdits → onMoveElements) — optimistic + * store update + rollback included, so the timeline reflects the lane change + * without a reload. The deps below deliberately OMIT * `readZIndex`/`onStackingPatches`: the lane→z stacking sync * (syncStackingForEdit) must not fire and recompute the z values the user just * set — commitZMirrorLaneMove never calls it, and without these deps it would @@ -160,16 +158,7 @@ function useMirrorLaneMoveCommit(): ( elements: els, trackOrder: displayTrackOrder(els), updateElement: (key, updates) => usePlayerStore.getState().updateElement(key, updates), - onMoveElements: onMoveElements - ? (edits, coalesceKey2, operation, coalesceMs) => - forwardRebasedTimelineMoveElements( - edits, - coalesceKey2, - operation, - onMoveElements, - coalesceMs, - ) - : undefined, + onMoveElements, // NO readZIndex / onStackingPatches: see the hook doc — the lane→z // stacking sync must not re-trigger and fight the just-set z values. }, diff --git a/packages/studio/src/player/components/timelineDragLanding.ts b/packages/studio/src/player/components/timelineDragLanding.ts index d463f5f4b9..880c003e7e 100644 --- a/packages/studio/src/player/components/timelineDragLanding.ts +++ b/packages/studio/src/player/components/timelineDragLanding.ts @@ -37,15 +37,11 @@ export function layoutAfterTrackInsert( } | null { const { elements, trackOrder } = deps; const editKey = keyOf(element); - // Expanded-child rows are synthetic host lanes, not source-file topology. - if (element.expandedParentStart != null) return null; const targetTrack = insertTrackValue(trackOrder, insertRow); // Foreign display rows and the opposite zone must not affect this topology. const writableZone = classifyZone(element); const writable = (src: TimelineElement): boolean => - sameSourceFile(src, element) && - classifyZone(src) === writableZone && - src.expandedParentStart == null; + sameSourceFile(src, element) && classifyZone(src) === writableZone; const topologyOrder = [...new Set(elements.filter(writable).map((e) => e.track))].sort( (a, b) => a - b, ); diff --git a/packages/studio/src/player/components/timelineGaps.test.ts b/packages/studio/src/player/components/timelineGaps.test.ts index 7dcf220d8f..4fd435414f 100644 --- a/packages/studio/src/player/components/timelineGaps.test.ts +++ b/packages/studio/src/player/components/timelineGaps.test.ts @@ -183,39 +183,9 @@ describe("trackHasGaps", () => { }); }); -describe("lane floor (expanded sub-comp children)", () => { - const child = (id: string, start: number, duration: number): TimelineElement => ({ - ...el(id, start, duration), - expandedParentStart: 16, - sourceFile: "scene.html", - }); - - it("laneGapFloor is 0 for ordinary lanes and the host window start for child lanes", () => { +describe("lane floor", () => { + it("laneGapFloor is always 0", () => { expect(laneGapFloor([el("a", 0, 2)])).toBe(0); - expect(laneGapFloor([child("c1", 16.5, 2), child("c2", 20, 2)])).toBe(16); - }); - - it("compaction lands the first child at the HOST window start, never absolute 0", () => { - const lane = [child("c1", 18, 2), child("c2", 22, 2)]; - expect(resolveAllTrackGaps(lane, undefined, laneGapFloor(lane))).toEqual([ - { key: "c1", newStart: 16 }, - { key: "c2", newStart: 18 }, - ]); - }); - - it("the leading gap starts at the floor for both close-one and the highlight intervals", () => { - const lane = [child("c1", 18, 2)]; - const floor = laneGapFloor(lane); - expect(resolveTrackGapAt(lane, 17, undefined, floor)).toEqual({ - gapStart: 16, - gapEnd: 18, - followingKeys: ["c1"], - }); - expect(resolveAllGapIntervals(lane, undefined, floor)).toEqual([{ start: 16, end: 18 }]); - }); - - it("a child lane contiguous from its host start has no gaps", () => { - const lane = [child("c1", 16, 2), child("c2", 18, 2)]; - expect(trackHasGaps(lane, undefined, laneGapFloor(lane))).toBe(false); + expect(laneGapFloor([])).toBe(0); }); }); diff --git a/packages/studio/src/player/components/timelineGaps.ts b/packages/studio/src/player/components/timelineGaps.ts index aef99c1671..6f51afd122 100644 --- a/packages/studio/src/player/components/timelineGaps.ts +++ b/packages/studio/src/player/components/timelineGaps.ts @@ -17,16 +17,9 @@ export const TRACK_GAP_EPSILON_S = 1e-3; const keyOf = (e: TimelineElement) => e.key ?? e.id; -/** - * The lane's time ORIGIN — the earliest start a clip on this lane may take. - * 0 for ordinary lanes; for a lane of expanded sub-comp children (post- - * collision-fix a lane is always single-origin) it is the children's host - * window start (`expandedParentStart`): display times are host-absolute, so - * compacting toward absolute 0 would drag a child BEFORE its host's window - * and persist a wrong (even negative) local time. - */ -export function laneGapFloor(elements: readonly TimelineElement[]): number { - return Math.max(0, ...elements.map((e) => e.expandedParentStart ?? 0)); +/** The lane's time ORIGIN — the earliest start a clip on this lane may take. */ +export function laneGapFloor(_elements: readonly TimelineElement[]): number { + return 0; } export const round3 = (v: number) => Math.round(v * 1000) / 1000; const endOf = (e: TimelineElement) => e.start + e.duration; diff --git a/packages/studio/src/player/components/timelineZones.test.ts b/packages/studio/src/player/components/timelineZones.test.ts index a4654088be..4b218f94a5 100644 --- a/packages/studio/src/player/components/timelineZones.test.ts +++ b/packages/studio/src/player/components/timelineZones.test.ts @@ -67,11 +67,6 @@ describe("isMainTrackElement", () => { it("is false for an audio clip even on track 0 (audio-only project has no main track)", () => { expect(isMainTrackElement(el("m", "audio", 0))).toBe(false); }); - - it("is false for an inline-expanded sub-composition child on track 0", () => { - const child: TimelineElement = { ...el("c", "video", 0), expandedParentStart: 4 }; - expect(isMainTrackElement(child)).toBe(false); - }); }); describe("normalizeToZones — CapCut-stable lanes follow the track-index (never z)", () => { diff --git a/packages/studio/src/player/components/timelineZones.ts b/packages/studio/src/player/components/timelineZones.ts index 01518f272b..9e8fc0d9e0 100644 --- a/packages/studio/src/player/components/timelineZones.ts +++ b/packages/studio/src/player/components/timelineZones.ts @@ -16,10 +16,9 @@ export function classifyZone(el: TimelineElement): TrackZone { } /** The "main track" is a convention, not a schema field: the first - * visual-zone display lane, matched only when it actually holds a visual - * clip and isn't an inline-expanded sub-composition child. */ + * visual-zone display lane, matched only when it actually holds a visual clip. */ export function isMainTrackElement(el: TimelineElement): boolean { - return el.track === 0 && classifyZone(el) === "visual" && el.expandedParentStart == null; + return el.track === 0 && classifyZone(el) === "visual"; } const keyOf = (el: TimelineElement) => el.key ?? el.id; diff --git a/packages/studio/src/telemetry/events.test.ts b/packages/studio/src/telemetry/events.test.ts index 6a281edc8d..fcb428ec45 100644 --- a/packages/studio/src/telemetry/events.test.ts +++ b/packages/studio/src/telemetry/events.test.ts @@ -12,7 +12,6 @@ const { trackPreviewFirstFrame, trackStudioRenderStart, trackStudioRazorSplit, - trackStudioExpandedClipEdit, trackStudioKeyframeLaneExpand, trackStudioSegmentEaseEdit, trackStudioFeedback, @@ -107,11 +106,6 @@ describe("studio telemetry events", () => { expect(trackEvent).toHaveBeenCalledWith("studio_razor_split", { mode: "all", count: 3 }); }); - it("trackStudioExpandedClipEdit emits 'studio_expanded_clip_edit' with action", () => { - trackStudioExpandedClipEdit({ action: "resize" }); - expect(trackEvent).toHaveBeenCalledWith("studio_expanded_clip_edit", { action: "resize" }); - }); - it("trackStudioKeyframeLaneExpand emits 'studio_keyframe_lane_expand' with expanded", () => { trackStudioKeyframeLaneExpand({ expanded: true }); expect(trackEvent).toHaveBeenCalledWith("studio_keyframe_lane_expand", { expanded: true }); diff --git a/packages/studio/src/telemetry/events.ts b/packages/studio/src/telemetry/events.ts index ac56e8849e..59994a46d8 100644 --- a/packages/studio/src/telemetry/events.ts +++ b/packages/studio/src/telemetry/events.ts @@ -84,14 +84,6 @@ export function trackStudioRazorSplit(props: { mode: "single" | "all"; count: nu }); } -// Adoption signal for the inline timeline-expansion surface: edits applied to a -// sub-composition child clip while its parent scene is expanded. -export function trackStudioExpandedClipEdit(props: { - action: "move" | "resize" | "delete" | "split"; -}): void { - trackEvent("studio_expanded_clip_edit", { action: props.action }); -} - // Adoption signal for the per-clip keyframe-lane caret toggle. export function trackStudioKeyframeLaneExpand(props: { expanded: boolean }): void { trackEvent("studio_keyframe_lane_expand", { expanded: props.expanded }); diff --git a/scripts/check-no-main-deletions.mjs b/scripts/check-no-main-deletions.mjs index 344c90190f..45c49dbf24 100644 --- a/scripts/check-no-main-deletions.mjs +++ b/scripts/check-no-main-deletions.mjs @@ -40,6 +40,10 @@ const STORYBOARD_VIEW_REASON = "owner-directed removal of the Studio storyboard view; its only readers were deleted with it"; export const ALLOWED_DELETIONS = new Map([ + [ + "packages/studio/src/components/nle/TimelinePane.test.ts", + "its only subject, the expandedParentStart rebase wrappers, is dead code now removed", + ], [ "scripts/test-reachability-baseline.json", "Reachability now requires zero orphans and rejects baseline files.",