Skip to content

Commit ff8ad1c

Browse files
fix(studio): a row move restacks only the clip you moved, so nothing else goes behind (#5057)
* fix(studio): a row move restacks only the clip you moved, so nothing else goes behind Moved up, the clip rises above the clips it overlaps on the rows now below it; moved down, it sinks below the ones now above it, never under z-index 0. Neighbours keep their z, so a full-frame scene on the top row no longer jumps over a caption moved up a row beneath it. * refactor(studio): stacking sync comments match the moved-clip-only rule Drops the stale cascade wording, checks the time overlap before the paint-scope string compare, and every test case names its move direction. * fix(studio): a row move restacks only the dragged clip and a move down never hides it Selected clips that only shifted in time keep their z-index. A move down stops at the clips on lower rows it paints over, instead of dropping under them when z-index 0 cannot get it behind the clip above. * style(studio): drop a comment the drag commit's doc already covers
1 parent f01ae78 commit ff8ad1c

6 files changed

Lines changed: 185 additions & 390 deletions

File tree

‎packages/studio/src/player/components/timelineClipDragCommit.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,28 @@ describe("commitDraggedClipMove", () => {
588588
expectZLiftedToSix(onStackingPatches);
589589
});
590590

591+
it("a multi-selection lane move restacks only the dragged clip", async () => {
592+
// a moves up from row 2 to row 1 over c; the selected scene keeps its row, so it keeps its z.
593+
const elements = [
594+
el("scene", 0, 0, 10),
595+
el("b", 1, 5, 5),
596+
el("a", 2, 0, 4),
597+
el("c", 3, 0, 4),
598+
];
599+
const z: Record<string, number> = { scene: 0, b: 2, a: 1, c: 3 };
600+
const onStackingPatches = vi.fn();
601+
runClipMove(drag(elements[2], { previewStart: 0, previewTrack: 1 }), {
602+
elements,
603+
trackOrder: [0, 1, 2, 3],
604+
selectedKeys: new Set(["a", "scene"]),
605+
readZIndex: (e) => z[e.key ?? e.id] ?? 0,
606+
onStackingPatches,
607+
});
608+
await flushMicrotasks();
609+
expect(onStackingPatches).toHaveBeenCalledTimes(1);
610+
expect(onStackingPatches.mock.calls[0][0]).toEqual([{ key: "a", zIndex: 4 }]);
611+
});
612+
591613
it("partial z-sync deps (no readZIndex) → move persists but no stacking call", async () => {
592614
const elements = [el("a", 1, 0, 10), el("b", 0, 0, 10)];
593615
// onStackingPatches present but readZIndex absent → syncStackingForEdit needs

‎packages/studio/src/player/components/timelineClipDragCommit.ts‎

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,6 @@ export function commitDraggedClipMove(drag: DraggedClipState, deps: DragCommitDe
337337
if (multi?.keys.has(keyOf(e))) return { ...e, start: multi.movedStart(e) };
338338
return e;
339339
});
340-
const multiKeys = multi ? multi.keys : null;
341340
if (!isVertical || !deps.readZIndex || !deps.onStackingPatches) {
342341
void refreshAfterDurableLaneMove(
343342
persistMoveEdits(edits, deps, coalesceKey, "lane-reorder"),
@@ -354,7 +353,6 @@ export function commitDraggedClipMove(drag: DraggedClipState, deps: DragCommitDe
354353
dragKey,
355354
drag.element.track,
356355
drag.previewTrack,
357-
multiKeys,
358356
deps,
359357
coalesceKey,
360358
),
@@ -472,7 +470,6 @@ function commitTrackInsert(
472470
dragKey,
473471
drag.element.track,
474472
drag.insertRow!,
475-
multi ? multi.keys : null,
476473
deps,
477474
coalesceKey,
478475
),
@@ -532,8 +529,8 @@ export function commitZMirrorLaneMove(
532529
* vertical lane change. Projects the drop-intent element set (`candidate`: the
533530
* dragged clip at its new / fractional-insert lane, others at their current tracks)
534531
* onto StackingElement using the caller-supplied live z-index reader, then
535-
* delegates the minimal-z resolution to computeStackingPatches — a clip on the
536-
* upper lane paints above every clip it time-overlaps. No-op unless both z-sync
532+
* delegates to computeStackingPatches — the moved clip alone rises (moved up) or
533+
* sinks (moved down) past the clips it time-overlaps. No-op unless both z-sync
537534
* deps are present, and never when the gesture aimed at the clip's OWN current
538535
* lane (`aimedLane === currentLane` — not a relocation).
539536
*/
@@ -542,7 +539,6 @@ function syncStackingForEdit(
542539
dragKey: string,
543540
currentLane: number,
544541
aimedLane: number,
545-
multiKeys: ReadonlySet<string> | null,
546542
deps: DragCommitDeps,
547543
coalesceKey?: string,
548544
): Promise<void> {
@@ -567,10 +563,11 @@ function syncStackingForEdit(
567563
stackingContextId: el.stackingContextId ?? null,
568564
}));
569565

570-
const editedKeys = [dragKey];
571-
if (multiKeys) for (const k of multiKeys) if (k !== dragKey) editedKeys.push(k);
572-
573-
const patches = computeStackingPatches(stackingEls, editedKeys);
566+
const patches = computeStackingPatches(
567+
stackingEls,
568+
[dragKey],
569+
aimedLane < currentLane ? "up" : "down",
570+
);
574571
if (patches.length === 0) return Promise.resolve();
575572
return Promise.resolve(onStackingPatches(patches, coalesceKey)).then(() => undefined);
576573
}

0 commit comments

Comments
 (0)