fix(studio): make a timeline lane a z-band so a vertical restack moves the row

Clips share a lane only when they carry the same z-index and don't overlap
in time. Previously a non-overlapping clip packed into the first
time-compatible row regardless of z, so restacking changed the z-index but
the clip's row never followed.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-08 23:46:29 -04:00
parent db6ccb8082
commit eb6a311d82
2 changed files with 20 additions and 2 deletions
@@ -34,16 +34,29 @@ function rowIds(rows: readonly { elements: readonly TimelineElement[] }[]): stri
}
describe("buildStackingTimelineLayers", () => {
it("packs non-overlapping clips into the same lane even when their z-index differs", () => {
it("splits non-overlapping clips into separate lanes when their z-index differs", () => {
// A lane is a z-band: differing z must land on different rows even when the
// clips don't overlap in time, so a vertical (z) restack actually moves the
// clip's row. Rows are ordered by descending z.
const result = buildStackingTimelineLayers([
rowElement({ id: "back", zIndex: 1, start: 0, duration: 1 }),
rowElement({ id: "front", zIndex: 10, start: 1, duration: 1 }),
]);
expect(rowIds(result.visualLayers)).toEqual([["back", "front"]]);
expect(rowIds(result.visualLayers)).toEqual([["front"], ["back"]]);
expect(result.visualLayers[0]?.zIndex).toBe(10);
});
it("packs non-overlapping clips into one lane when they share a z-index", () => {
const result = buildStackingTimelineLayers([
rowElement({ id: "back", zIndex: 5, start: 0, duration: 1 }),
rowElement({ id: "front", zIndex: 5, start: 1, duration: 1 }),
]);
expect(rowIds(result.visualLayers)).toEqual([["back", "front"]]);
expect(result.visualLayers[0]?.zIndex).toBe(5);
});
it("splits clips into separate lanes when they overlap in time", () => {
const result = buildStackingTimelineLayers([
rowElement({ id: "front", zIndex: 10, start: 0, duration: 2 }),
@@ -78,8 +78,13 @@ function getOrderedContextKeys(items: readonly TimelineLayerOrderItem[]): string
}
function canJoinLayer(layer: BuildLayer, item: TimelineLayerOrderItem): boolean {
// A row IS a z-band: clips only share a lane when they carry the same z-index
// (and don't overlap in time). Without the z-index gate a non-overlapping clip
// greedily packs into the first time-compatible row — the topmost one — so a
// vertical restack changes z but the row never follows it.
return (
layer.contextKey === resolveStackingContextKey(item) &&
layer.zIndex === item.zIndex &&
layer.elements.every((element) => !timelineElementsOverlap(element, item.element))
);
}