mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 06:30:03 +00:00
Re-architect the timeline row model from data-track-index rows to stacking layers. Rows represent stacking layers per context: explicit-z clips merge onto one track when they share a z and don't overlap in time; auto-z clips stay one row each (DOM order); audio is pulled into its own bottom lanes. Rows keyed by a stable layer id, not data-track-index. Vertical drag always writes z-index, never track: drop onto a layer joins it (same z), between layers interpolates a new z, past the ends creates a new front/back layer. data-track-index is never rewritten; #958 holds. Adds hasExplicitZIndex capture (computed z != auto). L1: hasExplicitZIndex on the element model L2: buildStackingTimelineLayers (layer-based rows) L3: layer-aware vertical drag (join / interpolate / new-extreme)
103 lines
3.2 KiB
TypeScript
103 lines
3.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { TimelineElement } from "../store/playerStore";
|
|
import type { StackingTimelineLayer } from "./timelineTrackOrder";
|
|
import { resolveTimelineLayerZIndexChanges } from "./timelineLayerDrag";
|
|
|
|
function element(input: { id: string; zIndex: number; tag?: string }): TimelineElement {
|
|
return {
|
|
id: input.id,
|
|
tag: input.tag ?? "div",
|
|
start: 0,
|
|
duration: 1,
|
|
track: 0,
|
|
zIndex: input.zIndex,
|
|
hasExplicitZIndex: true,
|
|
stackingContextId: "root",
|
|
parentCompositionId: null,
|
|
compositionAncestors: ["root"],
|
|
};
|
|
}
|
|
|
|
function layer(id: string, zIndex: number): StackingTimelineLayer {
|
|
return {
|
|
id,
|
|
kind: "visual",
|
|
contextKey: "root",
|
|
zIndex,
|
|
placementTrack: 0,
|
|
elements: [element({ id, zIndex })],
|
|
};
|
|
}
|
|
|
|
describe("resolveTimelineLayerZIndexChanges", () => {
|
|
it("joins an existing layer by assigning the dragged clip that layer's z-index", () => {
|
|
const dragged = element({ id: "dragged", zIndex: 1 });
|
|
|
|
expect(
|
|
resolveTimelineLayerZIndexChanges({
|
|
element: dragged,
|
|
layers: [layer("front", 10), layer("back", 1)],
|
|
placement: { type: "onto", layerId: "front" },
|
|
})?.zIndexChanges,
|
|
).toEqual([{ key: "dragged", zIndex: 10 }]);
|
|
});
|
|
|
|
it("interpolates a new integer z-index strictly between neighboring layers", () => {
|
|
const dragged = element({ id: "dragged", zIndex: 1 });
|
|
|
|
expect(
|
|
resolveTimelineLayerZIndexChanges({
|
|
element: dragged,
|
|
layers: [layer("front", 10), layer("back", 4)],
|
|
placement: { type: "between", beforeLayerId: "front", afterLayerId: "back" },
|
|
})?.zIndexChanges,
|
|
).toEqual([{ key: "dragged", zIndex: 7 }]);
|
|
});
|
|
|
|
it("renumbers the minimum sibling set when adjacent layers leave no integer gap", () => {
|
|
const dragged = element({ id: "dragged", zIndex: 0 });
|
|
|
|
expect(
|
|
resolveTimelineLayerZIndexChanges({
|
|
element: dragged,
|
|
layers: [layer("front", 2), layer("back", 1), layer("lower", 0)],
|
|
placement: { type: "between", beforeLayerId: "front", afterLayerId: "back" },
|
|
})?.zIndexChanges,
|
|
).toEqual([
|
|
{ key: "dragged", zIndex: 2 },
|
|
{ key: "front", zIndex: 3 },
|
|
]);
|
|
});
|
|
|
|
it("assigns new extreme z-index values above the top and below the bottom layer", () => {
|
|
const dragged = element({ id: "dragged", zIndex: 0 });
|
|
const layers = [layer("front", 10), layer("back", -2)];
|
|
|
|
expect(
|
|
resolveTimelineLayerZIndexChanges({
|
|
element: dragged,
|
|
layers,
|
|
placement: { type: "above", layerId: "front" },
|
|
})?.zIndexChanges,
|
|
).toEqual([{ key: "dragged", zIndex: 11 }]);
|
|
|
|
expect(
|
|
resolveTimelineLayerZIndexChanges({
|
|
element: dragged,
|
|
layers,
|
|
placement: { type: "below", layerId: "back" },
|
|
})?.zIndexChanges,
|
|
).toEqual([{ key: "dragged", zIndex: -3 }]);
|
|
});
|
|
|
|
it("does not resolve stacking z-index changes for audio clips", () => {
|
|
expect(
|
|
resolveTimelineLayerZIndexChanges({
|
|
element: element({ id: "music", zIndex: 0, tag: "audio" }),
|
|
layers: [layer("front", 10)],
|
|
placement: { type: "onto", layerId: "front" },
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
});
|