@hyperframes/studio 0.7.56 → 0.7.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/hyperframes-player-CtTDO63S.js +459 -0
- package/dist/assets/{index-CmVCjZjp.js → index-B_UvTX3E.js} +205 -205
- package/dist/assets/{index-BWnOxAiH.js → index-C47jAC3Q.js} +1 -1
- package/dist/assets/{index-C4csZims.js → index-DeQPzqwH.js} +1 -1
- package/dist/assets/index-uahwWkgw.css +1 -0
- package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
- package/dist/chunk-OBAG3GWK.js.map +1 -0
- package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
- package/dist/index.d.ts +41 -4
- package/dist/index.html +2 -2
- package/dist/index.js +7436 -5423
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +3 -3
- package/src/components/StudioFeedbackBar.tsx +2 -2
- package/src/components/editor/CanvasContextMenu.test.tsx +166 -2
- package/src/components/editor/CanvasContextMenu.tsx +100 -20
- package/src/components/editor/DomEditOverlay.tsx +34 -62
- package/src/components/editor/LayersPanel.tsx +111 -91
- package/src/components/editor/canvasContextMenuZOrder.test.ts +243 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +184 -23
- package/src/components/editor/domEditingDom.ts +5 -6
- package/src/components/editor/domEditingElement.ts +15 -32
- package/src/components/editor/layersPanelSort.ts +80 -0
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/useCanvasContextMenuState.ts +92 -0
- package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
- package/src/components/editor/useLayerRevealOverride.ts +389 -0
- package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +61 -37
- package/src/components/nle/PreviewOverlays.tsx +61 -16
- package/src/components/nle/TimelinePane.test.ts +3 -0
- package/src/components/nle/TimelinePane.tsx +11 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
- package/src/components/nle/zLaneGesture.test.ts +176 -0
- package/src/components/nle/zLaneGesture.ts +77 -0
- package/src/components/sidebar/AssetCard.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +19 -2
- package/src/components/sidebar/AudioRow.tsx +19 -2
- package/src/hooks/domEditCommitTypes.ts +29 -2
- package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
- package/src/hooks/fetchStubTestUtils.ts +16 -0
- package/src/hooks/timelineEditingHelpers.test.ts +183 -1
- package/src/hooks/timelineEditingHelpers.ts +63 -259
- package/src/hooks/timelineMoveAdapter.test.ts +44 -40
- package/src/hooks/timelineMoveAdapter.ts +7 -4
- package/src/hooks/timelineTimingSync.test.ts +820 -0
- package/src/hooks/timelineTimingSync.ts +597 -0
- package/src/hooks/timelineTrackVisibility.test.ts +3 -1
- package/src/hooks/timelineTrackVisibility.ts +4 -3
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useClipboard.ts +19 -17
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useDomEditCommits.test.tsx +392 -23
- package/src/hooks/useDomEditCommits.ts +180 -53
- package/src/hooks/useDomEditSession.test.tsx +3 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useElementLifecycleOps.test.tsx +559 -28
- package/src/hooks/useElementLifecycleOps.ts +175 -69
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/usePreviewPersistence.ts +2 -2
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +591 -277
- package/src/hooks/useTimelineEditing.ts +202 -282
- package/src/hooks/useTimelineEditingTypes.ts +1 -1
- package/src/hooks/useTimelineGroupEditing.ts +187 -134
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +7 -2
- package/src/player/components/Timeline.test.ts +105 -18
- package/src/player/components/Timeline.tsx +69 -98
- package/src/player/components/TimelineCanvas.tsx +47 -7
- package/src/player/components/TimelineLanes.tsx +32 -10
- package/src/player/components/TimelineOverlays.tsx +36 -0
- package/src/player/components/TimelineRuler.tsx +70 -47
- package/src/player/components/TrackGapContextMenu.tsx +116 -0
- package/src/player/components/timelineCallbacks.ts +4 -1
- package/src/player/components/timelineClipDragCommit.test.ts +370 -11
- package/src/player/components/timelineClipDragCommit.ts +267 -68
- package/src/player/components/timelineGapCommit.test.ts +163 -0
- package/src/player/components/timelineGapCommit.ts +103 -0
- package/src/player/components/timelineGaps.test.ts +221 -0
- package/src/player/components/timelineGaps.ts +182 -0
- package/src/player/components/timelineLayout.test.ts +2 -1
- package/src/player/components/timelineLayout.ts +119 -30
- package/src/player/components/timelineMarquee.test.ts +20 -10
- package/src/player/components/timelineMarquee.ts +9 -2
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +102 -1
- package/src/player/components/timelineStackingSync.ts +45 -5
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZMirror.test.ts +667 -0
- package/src/player/components/timelineZMirror.ts +393 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/timelineZoom.test.ts +2 -2
- package/src/player/components/timelineZoom.ts +7 -3
- package/src/player/components/useTimelineClipDrag.ts +1 -1
- package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
- package/src/player/components/useTimelineGapHighlights.ts +110 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -3
- package/src/player/components/useTimelineRangeSelection.ts +6 -4
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineScrollViewport.ts +73 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +11 -2
- package/src/player/components/useTimelineStackingSync.ts +17 -4
- package/src/player/components/useTimelineTrackDerivations.ts +38 -0
- package/src/player/components/useTrackGapMenu.ts +155 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
- package/src/player/hooks/useExpandedTimelineElements.ts +19 -1
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
- package/src/player/hooks/useTimelinePlayer.ts +4 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +11 -1
- package/src/player/index.ts +3 -1
- package/src/player/lib/layerOrdering.test.ts +1 -15
- package/src/player/lib/layerOrdering.ts +5 -28
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/timelineDOM.test.ts +72 -0
- package/src/player/lib/timelineDOM.ts +12 -0
- package/src/player/lib/timelineElementHelpers.ts +54 -11
- package/src/player/lib/timelineIframeHelpers.ts +2 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +49 -1
- package/src/telemetry/events.test.ts +10 -0
- package/src/telemetry/events.ts +1 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/domEditSaveQueue.ts +4 -3
- package/src/utils/editHistory.test.ts +82 -0
- package/src/utils/editHistory.ts +14 -2
- package/src/utils/gsapSoftReload.test.ts +54 -115
- package/src/utils/gsapSoftReload.ts +42 -152
- package/src/utils/gsapUndoRestore.test.ts +269 -0
- package/src/utils/gsapUndoRestore.ts +269 -0
- package/src/utils/sourceScopedSelectorIndex.ts +29 -0
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +62 -86
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-D-GyYi2d.css +0 -1
- package/dist/chunk-5QSIMBEJ.js.map +0 -1
- /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import type { TimelineElement } from "../store/playerStore";
|
|
3
3
|
import { classifyZone, normalizeToZones } from "./timelineZones";
|
|
4
|
-
import { computeStackingPatches, type StackingElement } from "./timelineStackingSync";
|
|
5
4
|
|
|
6
5
|
function el(id: string, tag: string, track: number, duration = 2): TimelineElement {
|
|
7
6
|
return { id, tag, start: 0, duration, track };
|
|
@@ -29,16 +28,6 @@ function expectZoningIdempotent(input: TimelineElement[]): void {
|
|
|
29
28
|
for (const e of once) expect(trackOf(twice, e.id)).toBe(e.track);
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
/** The exact qa-clean live repro (array order = DOM order); fresh objects per call. */
|
|
33
|
-
function qaCleanRepro(): TimelineElement[] {
|
|
34
|
-
return [
|
|
35
|
-
zClip("blue-logo", 6.37, 3, 0, 3, "img"),
|
|
36
|
-
zClip("ralu", 6.37, 3, 0, 0, "img"),
|
|
37
|
-
zClip("black-logo", 11.92, 3, 1, 1, "img"),
|
|
38
|
-
zClip("video", 0.84, 20, 3, 2, "video"),
|
|
39
|
-
];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
31
|
describe("classifyZone", () => {
|
|
43
32
|
it("audio → audio; video / image / everything else → visual", () => {
|
|
44
33
|
expect(classifyZone(el("m", "audio", 3))).toBe("audio");
|
|
@@ -66,52 +55,77 @@ describe("classifyZone", () => {
|
|
|
66
55
|
});
|
|
67
56
|
});
|
|
68
57
|
|
|
69
|
-
describe("normalizeToZones", () => {
|
|
70
|
-
it("orders visual
|
|
71
|
-
// img
|
|
72
|
-
//
|
|
73
|
-
// `vid` must own the upper lane. (Was: pinned img=0/vid=1 to authored order,
|
|
74
|
-
// which contradicts the canvas — updated for per-clip DOM-order tie-break.)
|
|
58
|
+
describe("normalizeToZones — CapCut-stable lanes follow the track-index (never z)", () => {
|
|
59
|
+
it("orders visual lanes by authored track-index (ascending), audio at the bottom", () => {
|
|
60
|
+
// img (track 0), vid (track 2), mus (audio, track 5). Lanes follow the track
|
|
61
|
+
// index: the LOWER visual track owns the upper lane. z is irrelevant (absent).
|
|
75
62
|
const out = normalizeToZones([
|
|
76
63
|
el("img", "img", 0),
|
|
77
64
|
el("vid", "video", 2),
|
|
78
65
|
el("mus", "audio", 5),
|
|
79
66
|
]);
|
|
80
|
-
expect(trackOf(out, "
|
|
81
|
-
expect(trackOf(out, "
|
|
82
|
-
expect(trackOf(out, "mus")).toBe(2); // audio
|
|
67
|
+
expect(trackOf(out, "img")).toBe(0); // track 0 → top lane
|
|
68
|
+
expect(trackOf(out, "vid")).toBe(1); // track 2 → below it
|
|
69
|
+
expect(trackOf(out, "mus")).toBe(2); // audio → bottom
|
|
83
70
|
});
|
|
84
71
|
|
|
85
|
-
it("
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
expect(trackOf(out, "
|
|
91
|
-
|
|
92
|
-
|
|
72
|
+
it("compacts sparse visual track-indexes to contiguous lanes, preserving ascending order", () => {
|
|
73
|
+
// Distinct authored tracks 0, 3, 7 → three adjacent lanes in the same order.
|
|
74
|
+
const out = normalizeToZones([el("a", "video", 7), el("b", "img", 0), el("c", "video", 3)]);
|
|
75
|
+
expect(trackOf(out, "b")).toBe(0); // track 0
|
|
76
|
+
expect(trackOf(out, "c")).toBe(1); // track 3
|
|
77
|
+
expect(trackOf(out, "a")).toBe(2); // track 7
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("drops audio below the visual lanes even when it holds a LOWER authored index", () => {
|
|
81
|
+
// Audio authored at track 0, video at track 5 — audio must still sink below.
|
|
82
|
+
const out = normalizeToZones([zClip("a", 0, 10, 0, 0, "audio"), zClip("v", 0, 10, 5, 0)]);
|
|
83
|
+
expect(trackOf(out, "v")).toBe(0); // visual on top
|
|
84
|
+
expect(trackOf(out, "a")).toBe(1); // audio below, despite its lower track index
|
|
93
85
|
});
|
|
94
86
|
|
|
95
87
|
it("drops audio below the visual lanes even when sharing a track index", () => {
|
|
96
88
|
const out = normalizeToZones([el("v", "video", 0), el("a", "audio", 0)]);
|
|
97
|
-
expect(trackOf(out, "v")).toBe(0);
|
|
98
|
-
expect(trackOf(out, "a")).toBe(1);
|
|
89
|
+
expect(trackOf(out, "v")).toBe(0);
|
|
90
|
+
expect(trackOf(out, "a")).toBe(1);
|
|
99
91
|
});
|
|
100
92
|
|
|
101
|
-
it("groups multiple audio tracks at the bottom
|
|
102
|
-
const out = normalizeToZones([el("v", "video", 0), el("
|
|
93
|
+
it("groups multiple audio tracks at the bottom, ordered by their track index", () => {
|
|
94
|
+
const out = normalizeToZones([el("v", "video", 0), el("a2", "audio", 4), el("a1", "audio", 1)]);
|
|
103
95
|
expect(trackOf(out, "v")).toBe(0);
|
|
104
|
-
expect(trackOf(out, "a1")).toBe(1);
|
|
96
|
+
expect(trackOf(out, "a1")).toBe(1); // audio track 1 above audio track 4
|
|
105
97
|
expect(trackOf(out, "a2")).toBe(2);
|
|
106
98
|
});
|
|
107
99
|
|
|
100
|
+
it("ignores z-index entirely — a high-z clip does NOT jump above a lower-track clip", () => {
|
|
101
|
+
// lo on track 0 with z=1; hi on track 1 with z=99. They fully overlap in time.
|
|
102
|
+
// The old z-rank pack lifted hi above lo; the CapCut rule keeps lane = track.
|
|
103
|
+
const out = normalizeToZones([zClip("lo", 0, 10, 0, 1), zClip("hi", 0, 10, 1, 99)]);
|
|
104
|
+
expect(trackOf(out, "lo")).toBe(0); // track 0 stays on top
|
|
105
|
+
expect(trackOf(out, "hi")).toBe(1); // higher z does NOT lift it
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("ignores z-index across authored tracks (scattered z, lanes still by track)", () => {
|
|
109
|
+
const out = normalizeToZones([
|
|
110
|
+
zClip("t0", 0, 10, 0, 3),
|
|
111
|
+
zClip("t1", 0, 10, 1, 26),
|
|
112
|
+
zClip("t2", 0, 10, 2, 0),
|
|
113
|
+
]);
|
|
114
|
+
expect(trackOf(out, "t0")).toBe(0);
|
|
115
|
+
expect(trackOf(out, "t1")).toBe(1);
|
|
116
|
+
expect(trackOf(out, "t2")).toBe(2);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("sequential (non-overlapping) same-track clips share a lane", () => {
|
|
120
|
+
const out = normalizeToZones([zClip("a", 0, 5, 0, 1), zClip("c", 6, 3, 0, 9)]);
|
|
121
|
+
expect(trackOf(out, "a")).toBe(0);
|
|
122
|
+
expect(trackOf(out, "c")).toBe(0); // shares the lane regardless of z
|
|
123
|
+
});
|
|
124
|
+
|
|
108
125
|
it("returns the same array (identity) when already zoned", () => {
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
// which the new per-clip DOM-order pack would flip — so it wasn't a fixed
|
|
113
|
-
// point under the corrected canvas semantics; swapped to the stable order.)
|
|
114
|
-
const input = [el("v", "video", 1), el("i", "img", 0), el("a", "audio", 2)];
|
|
126
|
+
// i on track 0, v on track 1, a (audio) on track 2 — already contiguous, visual
|
|
127
|
+
// above audio, so re-zoning is a no-op and the SAME reference comes back.
|
|
128
|
+
const input = [el("i", "img", 0), el("v", "video", 1), el("a", "audio", 2)];
|
|
115
129
|
expect(normalizeToZones(input)).toBe(input);
|
|
116
130
|
});
|
|
117
131
|
|
|
@@ -125,301 +139,99 @@ describe("normalizeToZones", () => {
|
|
|
125
139
|
expectZoningIdempotent(input);
|
|
126
140
|
});
|
|
127
141
|
|
|
128
|
-
it("
|
|
142
|
+
it("re-derives identical lanes from fresh objects carrying the same tracks (reload-stable)", () => {
|
|
143
|
+
const build = (): TimelineElement[] => [
|
|
144
|
+
zClip("hi", 0, 10, 0, 9),
|
|
145
|
+
zClip("lo", 0, 10, 1, 1),
|
|
146
|
+
zClip("mid", 3, 5, 2, 5),
|
|
147
|
+
];
|
|
148
|
+
const first = normalizeToZones(build());
|
|
149
|
+
const second = normalizeToZones(build());
|
|
150
|
+
for (const e of first) expect(trackOf(second, e.id)).toBe(e.track);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("normalizeToZones — legacy overlap spill (display-only, deterministic)", () => {
|
|
155
|
+
it("splits time-overlapping SAME-track clips onto adjacent sub-lanes (no visible overlap)", () => {
|
|
156
|
+
// a [0,5), b [2,7) overlaps a, c [6,9) sequential — all authored on track 1.
|
|
157
|
+
// The editor forbids per-track overlap, but a legacy file can carry it; the
|
|
158
|
+
// spill orders by stable id (a, b, c) and first-fits: a→lane0, b overlaps a→
|
|
159
|
+
// lane1, c fits back on lane0 (no overlap with a).
|
|
129
160
|
const clip = (id: string, start: number, duration: number): TimelineElement => ({
|
|
130
161
|
id,
|
|
131
162
|
tag: "video",
|
|
132
163
|
start,
|
|
133
164
|
duration,
|
|
134
|
-
track: 1,
|
|
165
|
+
track: 1,
|
|
135
166
|
});
|
|
136
|
-
// a [0,5), b [2,7) overlaps a, c [6,9) fits after a. All equal-z (absent).
|
|
137
|
-
// Per-clip pack (DOM-order tie-break): c is last in DOM so it places on the
|
|
138
|
-
// top lane 0; b overlaps c and lands on lane 1; a overlaps b (and is earlier
|
|
139
|
-
// in DOM than b, so it must paint BELOW b) → lane 2. a can NOT drop onto c's
|
|
140
|
-
// lane 0 even though it doesn't overlap c, because that would place a ABOVE b
|
|
141
|
-
// in lane order while a paints below b — the canvas-correct constraint the old
|
|
142
|
-
// whole-track packer ignored (it shared a/c on lane 0, contradicting paint).
|
|
143
167
|
const out = normalizeToZones([clip("a", 0, 5), clip("b", 2, 5), clip("c", 6, 3)]);
|
|
144
|
-
expect(trackOf(out, "
|
|
145
|
-
expect(trackOf(out, "b")).toBe(1); // overlaps
|
|
146
|
-
expect(trackOf(out, "
|
|
147
|
-
|
|
148
|
-
// No two time-overlapping clips share a lane (the real NLE invariant).
|
|
168
|
+
expect(trackOf(out, "a")).toBe(0);
|
|
169
|
+
expect(trackOf(out, "b")).toBe(1); // overlaps a → adjacent sub-lane
|
|
170
|
+
expect(trackOf(out, "c")).toBe(0); // sequential to a → shares lane 0
|
|
171
|
+
// No two time-overlapping clips share a lane.
|
|
149
172
|
expect(trackOf(out, "a")).not.toBe(trackOf(out, "b"));
|
|
150
|
-
expect(trackOf(out, "b")).not.toBe(trackOf(out, "c"));
|
|
151
|
-
|
|
152
173
|
// Idempotent: re-laying the split result changes nothing.
|
|
153
174
|
const twice = normalizeToZones(out);
|
|
154
175
|
for (const e of out) expect(trackOf(twice, e.id)).toBe(e.track);
|
|
155
176
|
});
|
|
156
|
-
});
|
|
157
177
|
|
|
158
|
-
|
|
159
|
-
it("orders overlapping same-zone clips by z: higher z → higher (upper) lane", () => {
|
|
160
|
-
// lo (z=1) and hi (z=9) fully overlap in time on the same authored track.
|
|
161
|
-
const out = normalizeToZones([zClip("lo", 0, 10, 0, 1), zClip("hi", 0, 10, 0, 9)]);
|
|
162
|
-
expect(trackOf(out, "hi")).toBe(0); // higher z → upper lane (top)
|
|
163
|
-
expect(trackOf(out, "lo")).toBe(1); // lower z → below
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("orders three overlapping clips strictly by descending z", () => {
|
|
167
|
-
const out = normalizeToZones([
|
|
168
|
-
zClip("mid", 0, 10, 0, 5),
|
|
169
|
-
zClip("top", 0, 10, 0, 8),
|
|
170
|
-
zClip("bot", 0, 10, 0, 2),
|
|
171
|
-
]);
|
|
172
|
-
expect(trackOf(out, "top")).toBe(0);
|
|
173
|
-
expect(trackOf(out, "mid")).toBe(1);
|
|
174
|
-
expect(trackOf(out, "bot")).toBe(2);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("does NOT reorder non-overlapping (sequential) clips by z — they share a lane", () => {
|
|
178
|
-
// a [0,5) z=1 then c [6,9) z=9 — no time overlap, so z is irrelevant.
|
|
179
|
-
const out = normalizeToZones([zClip("a", 0, 5, 0, 1), zClip("c", 6, 3, 0, 9)]);
|
|
180
|
-
expect(trackOf(out, "a")).toBe(0);
|
|
181
|
-
expect(trackOf(out, "c")).toBe(0); // shares the lane regardless of higher z
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("leaves the audio zone unaffected by z", () => {
|
|
185
|
-
const out = normalizeToZones([
|
|
186
|
-
zClip("v", 0, 10, 0, 1),
|
|
187
|
-
zClip("m1", 0, 10, 1, 99, "audio"),
|
|
188
|
-
zClip("m2", 0, 10, 1, 0, "audio"),
|
|
189
|
-
]);
|
|
190
|
-
// Two overlapping audio clips split onto lanes below the visual clip; their
|
|
191
|
-
// relative z does not lift one above a visual clip.
|
|
192
|
-
expect(trackOf(out, "v")).toBe(0);
|
|
193
|
-
expect(trackOf(out, "m1")).toBeGreaterThan(trackOf(out, "v"));
|
|
194
|
-
expect(trackOf(out, "m2")).toBeGreaterThan(trackOf(out, "v"));
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("treats missing / auto z as 0 (undefined z clip sinks below a positive-z overlap)", () => {
|
|
198
|
-
const out = normalizeToZones([
|
|
199
|
-
{ id: "noz", tag: "video", start: 0, duration: 10, track: 0 }, // no zIndex
|
|
200
|
-
zClip("pos", 0, 10, 0, 3),
|
|
201
|
-
]);
|
|
202
|
-
expect(trackOf(out, "pos")).toBe(0); // z=3 → upper
|
|
203
|
-
expect(trackOf(out, "noz")).toBe(1); // absent z ⇒ 0 → below
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("tie-breaks equal-z overlapping clips on the STABLE id, not the mutated lane", () => {
|
|
207
|
-
// Equal z + full overlap: order must be deterministic (id asc) and survive
|
|
208
|
-
// re-normalization — the historical oscillation bug tie-broke on the track.
|
|
178
|
+
it("spills two fully-overlapping same-track clips by stable id (a above b)", () => {
|
|
209
179
|
const out = normalizeToZones([zClip("b", 0, 10, 0, 5), zClip("a", 0, 10, 0, 5)]);
|
|
210
180
|
expect(trackOf(out, "a")).toBe(0); // "a" < "b"
|
|
211
181
|
expect(trackOf(out, "b")).toBe(1);
|
|
182
|
+
// Survives re-normalization (stable id tie-break, never the mutated lane).
|
|
212
183
|
const twice = normalizeToZones(out);
|
|
213
184
|
for (const e of out) expect(trackOf(twice, e.id)).toBe(e.track);
|
|
214
185
|
});
|
|
215
|
-
|
|
216
|
-
it("FIXED POINT: normalizeToZones(normalizeToZones(x)) === normalizeToZones(x) with z present", () => {
|
|
217
|
-
const input = [
|
|
218
|
-
zClip("hi", 0, 10, 0, 9),
|
|
219
|
-
zClip("lo", 0, 10, 0, 1),
|
|
220
|
-
zClip("mid", 2, 6, 0, 5),
|
|
221
|
-
zClip("seq", 12, 4, 0, 7),
|
|
222
|
-
zClip("music", 0, 16, 1, 3, "audio"),
|
|
223
|
-
];
|
|
224
|
-
expectZoningIdempotent(input);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it("reload simulation: re-deriving lanes from the SAME z values yields identical lanes", () => {
|
|
228
|
-
// Simulate two independent discovery passes producing fresh element objects
|
|
229
|
-
// carrying the same z — lane assignment must be stable across reloads.
|
|
230
|
-
const build = (): TimelineElement[] => [
|
|
231
|
-
zClip("hi", 0, 10, 0, 9),
|
|
232
|
-
zClip("lo", 0, 10, 0, 1),
|
|
233
|
-
zClip("mid", 3, 5, 0, 5),
|
|
234
|
-
];
|
|
235
|
-
const first = normalizeToZones(build());
|
|
236
|
-
const second = normalizeToZones(build());
|
|
237
|
-
for (const e of first) expect(trackOf(second, e.id)).toBe(e.track);
|
|
238
|
-
});
|
|
239
186
|
});
|
|
240
187
|
|
|
241
|
-
describe("normalizeToZones —
|
|
242
|
-
//
|
|
243
|
-
// (
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
zClip("
|
|
249
|
-
zClip("
|
|
250
|
-
zClip("
|
|
251
|
-
zClip("
|
|
188
|
+
describe("normalizeToZones — legacy file with scattered z (requirement 6)", () => {
|
|
189
|
+
// Mirrors /tmp/hf-fixwave/userproj/index.html: many visual clips on contiguous
|
|
190
|
+
// authored tracks (0..17) each carrying an unrelated, scattered inline z-index,
|
|
191
|
+
// and audio on the highest tracks (18..). The display must follow the
|
|
192
|
+
// track-index, NOT the z, and a well-formed (contiguous, audio-last) legacy file
|
|
193
|
+
// must not be re-laned at all — normalize is the identity.
|
|
194
|
+
const legacy = (): TimelineElement[] => [
|
|
195
|
+
zClip("sub-0", 3, 1.15, 0, 0, "div"), // no explicit z (0)
|
|
196
|
+
zClip("cap-hit", 4.51, 1.73, 3, 26, "div"), // scattered z
|
|
197
|
+
zClip("cap-send", 5.85, 1.27, 4, 25, "div"),
|
|
198
|
+
zClip("avatar", 6.4, 1.148, 1, 26),
|
|
199
|
+
zClip("v-opener", 0, 3, 15, 12),
|
|
200
|
+
zClip("v-letters", 30.08, 4.39, 5, 25),
|
|
201
|
+
zClip("music", 3, 42.95, 18, 10, "audio"),
|
|
202
|
+
zClip("vo", 3.2, 10.3, 19, 4, "audio"),
|
|
252
203
|
];
|
|
253
204
|
|
|
254
|
-
it("
|
|
255
|
-
const out = normalizeToZones(
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
expect(trackOf(out, "
|
|
259
|
-
expect(trackOf(out, "
|
|
260
|
-
expect(trackOf(out, "
|
|
261
|
-
|
|
262
|
-
expect(trackOf(out, "
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
expect(trackOf(out, "
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it("all-equal-z overlapping clips stack by DOM order (later on top), lanes contiguous", () => {
|
|
284
|
-
// Was: "keeps ascending authored track order when all tracks share z" — that
|
|
285
|
-
// pinned t0=0/t1=1/t3=2 to the authored track index. But these three all
|
|
286
|
-
// start at 0 and OVERLAP, all z=0, so CSS paints them by DOM order: t3 (last)
|
|
287
|
-
// on top. The per-clip pack now reflects that (t3 lane 0, then t1, then t0),
|
|
288
|
-
// which is what the canvas actually shows. Lanes stay contiguous 0..2.
|
|
289
|
-
const out = normalizeToZones([
|
|
290
|
-
zClip("t0", 0, 2, 0, 0),
|
|
291
|
-
zClip("t1", 0, 2, 1, 0),
|
|
292
|
-
zClip("t3", 0, 2, 3, 0),
|
|
293
|
-
]);
|
|
294
|
-
expect(trackOf(out, "t3")).toBe(0); // last in DOM → paints on top
|
|
295
|
-
expect(trackOf(out, "t1")).toBe(1);
|
|
296
|
-
expect(trackOf(out, "t0")).toBe(2);
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
describe("normalizeToZones — EXACT qa-clean repro (per-clip constrained pack)", () => {
|
|
301
|
-
// The live repro from /tmp/hf-dnd-qa/qa-clean/index.html:
|
|
302
|
-
// blue-logo authored track 0, z=3, 6.37–9.37
|
|
303
|
-
// ralu image authored track 0, z=0, 6.37–9.37 (shares track 0 with blue-logo)
|
|
304
|
-
// black-logo authored track 1, z=1, 11.92–14.92
|
|
305
|
-
// video authored track 3, z=2, 0.84–20.84
|
|
306
|
-
// Canvas truth: video (z=2) covers ralu (z=0). The OLD whole-track packer
|
|
307
|
-
// ordered track 0 by its MAX z (3, from blue-logo), so ralu rode above the
|
|
308
|
-
// z=2 video — the timeline↔canvas contradiction. Array order below = DOM order.
|
|
309
|
-
|
|
310
|
-
it("ACCEPTANCE: lane order top→bottom is blue-logo, video, black-logo, ralu", () => {
|
|
311
|
-
const out = normalizeToZones(qaCleanRepro());
|
|
312
|
-
expect(trackOf(out, "blue-logo")).toBe(0); // z=3 → top
|
|
313
|
-
expect(trackOf(out, "video")).toBe(1); // z=2, overlaps blue-logo → below it
|
|
314
|
-
expect(trackOf(out, "black-logo")).toBe(2); // z=1, overlaps video (11.92–14.92 ∩ 0.84–20.84)
|
|
315
|
-
expect(trackOf(out, "ralu")).toBe(3); // z=0, overlaps blue-logo AND video → bottom
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it("REGRESSION: a low-z clip must not ride its authored trackmate's high z above a clip that covers it", () => {
|
|
319
|
-
const out = normalizeToZones(qaCleanRepro());
|
|
320
|
-
// ralu (z=0) shares authored track 0 with blue-logo (z=3) but must sink BELOW
|
|
321
|
-
// the video (z=2) that overlaps and paints over it — the whole-track bug.
|
|
322
|
-
expect(trackOf(out, "ralu")).toBeGreaterThan(trackOf(out, "video"));
|
|
323
|
-
// black-logo (z=1) below video (z=2) because they overlap in time.
|
|
324
|
-
expect(trackOf(out, "black-logo")).toBeGreaterThan(trackOf(out, "video"));
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it("FIXED POINT: running the NEW pack on its own output changes nothing", () => {
|
|
328
|
-
const once = normalizeToZones(qaCleanRepro());
|
|
329
|
-
const twice = normalizeToZones(once);
|
|
330
|
-
for (const e of once) expect(trackOf(twice, e.id)).toBe(e.track);
|
|
331
|
-
// And a third pass, to be sure convergence is genuine.
|
|
332
|
-
const thrice = normalizeToZones(twice);
|
|
333
|
-
for (const e of twice) expect(trackOf(thrice, e.id)).toBe(e.track);
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
describe("z ↔ lane round-trip convergence (both directions agree)", () => {
|
|
338
|
-
// Project a normalized TimelineElement onto the StackingElement view the
|
|
339
|
-
// forward (lane→z) mapping reasons over.
|
|
340
|
-
const toStacking = (els: TimelineElement[]): StackingElement[] =>
|
|
341
|
-
els.map((e, domIndex) => ({
|
|
342
|
-
key: e.key ?? e.id,
|
|
343
|
-
start: e.start,
|
|
344
|
-
duration: e.duration,
|
|
345
|
-
track: e.track,
|
|
346
|
-
zIndex: Number.isFinite(e.zIndex) ? (e.zIndex as number) : 0,
|
|
347
|
-
isAudio: classifyZone(e) === "audio",
|
|
348
|
-
domIndex,
|
|
349
|
-
}));
|
|
350
|
-
|
|
351
|
-
it("lane-move → z patch → re-discovery orders lanes by that same z → identical lanes (no oscillation)", () => {
|
|
352
|
-
// Two fully-overlapping visual clips. Authored: a below (z=1), b above (z=5).
|
|
353
|
-
const authored: TimelineElement[] = [zClip("a", 0, 10, 0, 1), zClip("b", 0, 10, 0, 5)];
|
|
354
|
-
const normalized = normalizeToZones(authored);
|
|
355
|
-
// z→lane placed b (z=5) on the upper lane 0, a on lane 1.
|
|
356
|
-
expect(trackOf(normalized, "b")).toBe(0);
|
|
357
|
-
expect(trackOf(normalized, "a")).toBe(1);
|
|
358
|
-
|
|
359
|
-
// USER lane-move: drag a to the TOP (lane 0) and push b down (lane 1).
|
|
360
|
-
const afterMove = normalized.map((e) =>
|
|
361
|
-
e.id === "a" ? { ...e, track: 0 } : e.id === "b" ? { ...e, track: 1 } : e,
|
|
362
|
-
);
|
|
363
|
-
|
|
364
|
-
// FORWARD: a lane-move writes the minimal z patch for the edited clip.
|
|
365
|
-
const patches = computeStackingPatches(toStacking(afterMove), ["a"]);
|
|
366
|
-
expect(patches).toEqual([{ key: "a", zIndex: 6 }]); // lifted above b (5)
|
|
367
|
-
|
|
368
|
-
// Apply the z patch back onto the elements (what handleDomZIndexReorderCommit
|
|
369
|
-
// persists; next discovery re-reads it as TimelineElement.zIndex).
|
|
370
|
-
const rediscovered = afterMove.map((e) => {
|
|
371
|
-
const p = patches.find((pp) => pp.key === (e.key ?? e.id));
|
|
372
|
-
return p ? { ...e, zIndex: p.zIndex } : e;
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
// REVERSE: re-normalize from the new z. a (z=6) must now own the upper lane —
|
|
376
|
-
// the same lane the user moved it to. Directions converge, they do not fight.
|
|
377
|
-
const renormalized = normalizeToZones(rediscovered);
|
|
378
|
-
expect(trackOf(renormalized, "a")).toBe(0);
|
|
379
|
-
expect(trackOf(renormalized, "b")).toBe(1);
|
|
380
|
-
|
|
381
|
-
// FIXED POINT: forward on the converged state produces NO further patch, and
|
|
382
|
-
// reverse is idempotent — the round-trip is stable.
|
|
383
|
-
expect(computeStackingPatches(toStacking(renormalized), ["a"])).toEqual([]);
|
|
384
|
-
const twice = normalizeToZones(renormalized);
|
|
385
|
-
for (const e of renormalized) expect(trackOf(twice, e.id)).toBe(e.track);
|
|
205
|
+
it("lanes follow the track-index, not the scattered z", () => {
|
|
206
|
+
const out = normalizeToZones(legacy());
|
|
207
|
+
// Visual tracks 0,1,3,4,5,15 compact to lanes 0..5 in ascending track order —
|
|
208
|
+
// z (0,26,25,26,12,25) is ignored.
|
|
209
|
+
expect(trackOf(out, "sub-0")).toBe(0); // track 0
|
|
210
|
+
expect(trackOf(out, "avatar")).toBe(1); // track 1
|
|
211
|
+
expect(trackOf(out, "cap-hit")).toBe(2); // track 3
|
|
212
|
+
expect(trackOf(out, "cap-send")).toBe(3); // track 4
|
|
213
|
+
expect(trackOf(out, "v-letters")).toBe(4); // track 5
|
|
214
|
+
expect(trackOf(out, "v-opener")).toBe(5); // track 15
|
|
215
|
+
// Audio stays below every visual lane.
|
|
216
|
+
expect(trackOf(out, "music")).toBe(6);
|
|
217
|
+
expect(trackOf(out, "vo")).toBe(7);
|
|
218
|
+
// The z=26 caption does NOT ride above the z=0 subtitle on track 0.
|
|
219
|
+
expect(trackOf(out, "cap-hit")).toBeGreaterThan(trackOf(out, "sub-0"));
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("does not rewrite a well-formed (contiguous, audio-last) legacy set — identity", () => {
|
|
223
|
+
// Same shape but authored tracks already contiguous 0..7 with audio last.
|
|
224
|
+
const input = [
|
|
225
|
+
zClip("a", 0, 3, 0, 12),
|
|
226
|
+
zClip("b", 0, 3, 1, 26),
|
|
227
|
+
zClip("c", 0, 3, 2, 3),
|
|
228
|
+
zClip("m", 0, 3, 3, 9, "audio"),
|
|
229
|
+
];
|
|
230
|
+
// Every clip already sits on its track-index lane, so normalize is a no-op.
|
|
231
|
+
expect(normalizeToZones(input)).toBe(input);
|
|
386
232
|
});
|
|
387
233
|
|
|
388
|
-
it("
|
|
389
|
-
|
|
390
|
-
const normalized = normalizeToZones(qaCleanRepro());
|
|
391
|
-
// Baseline lanes: blue-logo 0, video 1, black-logo 2, ralu 3.
|
|
392
|
-
expect(trackOf(normalized, "video")).toBe(1);
|
|
393
|
-
expect(trackOf(normalized, "ralu")).toBe(3);
|
|
394
|
-
|
|
395
|
-
// USER lane-move: drag video to the lane BELOW ralu (bottom). ralu is at
|
|
396
|
-
// lane 3, so video goes to a lane strictly greater — model it as lane 4.
|
|
397
|
-
const afterMove = normalized.map((e) => (e.id === "video" ? { ...e, track: 4 } : e));
|
|
398
|
-
|
|
399
|
-
// FORWARD: video (z=2) must drop below ralu (z=0). No integer z ≥ 0 fits
|
|
400
|
-
// strictly below 0, so the tie-aware sync cascades: video→0 and the clips that
|
|
401
|
-
// must stay above it (ralu z=0, black-logo z=1, blue-logo z=3) are bumped as
|
|
402
|
-
// needed so video paints below ralu with all z ≥ 0.
|
|
403
|
-
const patches = computeStackingPatches(toStacking(afterMove), ["video"]);
|
|
404
|
-
const patchByKey = new Map(patches.map((p) => [p.key, p.zIndex]));
|
|
405
|
-
// Video was moved; it must now be strictly below ralu in paint order.
|
|
406
|
-
const zAfter = (id: string): number =>
|
|
407
|
-
patchByKey.get(id) ?? (afterMove.find((e) => e.id === id)!.zIndex as number);
|
|
408
|
-
expect(zAfter("video")).toBeLessThan(zAfter("ralu"));
|
|
409
|
-
expect(zAfter("video")).toBeGreaterThanOrEqual(0);
|
|
410
|
-
expect(patchByKey.size).toBeGreaterThan(0);
|
|
411
|
-
|
|
412
|
-
// Apply patches and re-pack: video's lane must now be BELOW ralu's.
|
|
413
|
-
const rediscovered = afterMove.map((e) => {
|
|
414
|
-
const z = patchByKey.get(e.id);
|
|
415
|
-
return z != null ? { ...e, zIndex: z } : e;
|
|
416
|
-
});
|
|
417
|
-
const renormalized = normalizeToZones(rediscovered);
|
|
418
|
-
expect(trackOf(renormalized, "video")).toBeGreaterThan(trackOf(renormalized, "ralu"));
|
|
419
|
-
|
|
420
|
-
// FIXED POINT: re-running BOTH directions on the converged state is a no-op.
|
|
421
|
-
expect(computeStackingPatches(toStacking(renormalized), ["video"])).toEqual([]);
|
|
422
|
-
const twice = normalizeToZones(renormalized);
|
|
423
|
-
for (const e of renormalized) expect(trackOf(twice, e.id)).toBe(e.track);
|
|
234
|
+
it("is idempotent on the scattered-z legacy shape (no drift on re-discovery)", () => {
|
|
235
|
+
expectZoningIdempotent(legacy());
|
|
424
236
|
});
|
|
425
237
|
});
|