@hyperframes/studio 0.8.131 → 0.8.133
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-BpOOAOcm.js → hyperframes-player-Cjcwveg0.js} +1 -1
- package/dist/assets/{index-DbSXe5_2.js → index-CwgWmfgx.js} +1 -1
- package/dist/assets/{index-BEcHKkGY.js → index-Dj21zXWA.js} +136 -136
- package/dist/assets/{index-DASBNWML.js → index-ME-Z5jh6.js} +1 -1
- package/dist/{chunk-DRVWEPO5.js → chunk-FOEID72M.js} +3 -1
- package/dist/{chunk-DRVWEPO5.js.map → chunk-FOEID72M.js.map} +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.html +1 -1
- package/dist/index.js +51 -108
- package/dist/index.js.map +1 -1
- package/dist/player.js +1 -1
- package/package.json +8 -8
- package/src/player/components/timelineClipDragCommit.test.ts +22 -0
- package/src/player/components/timelineClipDragCommit.ts +7 -10
- package/src/player/components/timelineStackingSync.test.ts +101 -142
- package/src/player/components/timelineStackingSync.ts +51 -235
- package/src/player/components/useTimelineStackingSync.ts +1 -1
- package/src/player/hooks/usePlaybackKeyboard.ts +1 -0
- package/src/player/hooks/useTimelinePlayer.playLock.test.ts +137 -0
- package/src/player/hooks/useTimelinePlayer.ts +15 -6
- package/src/player/hooks/useTimelinePlayerLoop.ts +1 -1
- package/src/player/lib/playbackSeek.ts +2 -0
- package/src/player/store/readinessSlice.ts +4 -0
- package/src/timelineStackingSyncExport.test.tsx +3 -2
package/dist/player.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.133",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -60,12 +60,12 @@
|
|
|
60
60
|
"dockview-react": "8.3.1",
|
|
61
61
|
"gsap": "^3.13.0",
|
|
62
62
|
"mediabunny": "^1.45.3",
|
|
63
|
-
"@hyperframes/core": "0.8.
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/
|
|
66
|
-
"@hyperframes/
|
|
67
|
-
"@hyperframes/
|
|
68
|
-
"@hyperframes/studio-server": "0.8.
|
|
63
|
+
"@hyperframes/core": "0.8.133",
|
|
64
|
+
"@hyperframes/sdk": "0.8.133",
|
|
65
|
+
"@hyperframes/parsers": "0.8.133",
|
|
66
|
+
"@hyperframes/player": "0.8.133",
|
|
67
|
+
"@hyperframes/shader-transitions": "0.8.133",
|
|
68
|
+
"@hyperframes/studio-server": "0.8.133"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"vite": "^6.4.2",
|
|
82
82
|
"vitest": "^4.1.11",
|
|
83
83
|
"zustand": "^5.0.0",
|
|
84
|
-
"@hyperframes/producer": "0.8.
|
|
84
|
+
"@hyperframes/producer": "0.8.133"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"react": "19",
|
|
@@ -588,6 +588,28 @@ describe("commitDraggedClipMove", () => {
|
|
|
588
588
|
expectZLiftedToSix(onStackingPatches);
|
|
589
589
|
});
|
|
590
590
|
|
|
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
|
+
|
|
591
613
|
it("partial z-sync deps (no readZIndex) → move persists but no stacking call", async () => {
|
|
592
614
|
const elements = [el("a", 1, 0, 10), el("b", 0, 0, 10)];
|
|
593
615
|
// onStackingPatches present but readZIndex absent → syncStackingForEdit needs
|
|
@@ -337,7 +337,6 @@ export function commitDraggedClipMove(drag: DraggedClipState, deps: DragCommitDe
|
|
|
337
337
|
if (multi?.keys.has(keyOf(e))) return { ...e, start: multi.movedStart(e) };
|
|
338
338
|
return e;
|
|
339
339
|
});
|
|
340
|
-
const multiKeys = multi ? multi.keys : null;
|
|
341
340
|
if (!isVertical || !deps.readZIndex || !deps.onStackingPatches) {
|
|
342
341
|
void refreshAfterDurableLaneMove(
|
|
343
342
|
persistMoveEdits(edits, deps, coalesceKey, "lane-reorder"),
|
|
@@ -354,7 +353,6 @@ export function commitDraggedClipMove(drag: DraggedClipState, deps: DragCommitDe
|
|
|
354
353
|
dragKey,
|
|
355
354
|
drag.element.track,
|
|
356
355
|
drag.previewTrack,
|
|
357
|
-
multiKeys,
|
|
358
356
|
deps,
|
|
359
357
|
coalesceKey,
|
|
360
358
|
),
|
|
@@ -472,7 +470,6 @@ function commitTrackInsert(
|
|
|
472
470
|
dragKey,
|
|
473
471
|
drag.element.track,
|
|
474
472
|
drag.insertRow!,
|
|
475
|
-
multi ? multi.keys : null,
|
|
476
473
|
deps,
|
|
477
474
|
coalesceKey,
|
|
478
475
|
),
|
|
@@ -532,8 +529,8 @@ export function commitZMirrorLaneMove(
|
|
|
532
529
|
* vertical lane change. Projects the drop-intent element set (`candidate`: the
|
|
533
530
|
* dragged clip at its new / fractional-insert lane, others at their current tracks)
|
|
534
531
|
* onto StackingElement using the caller-supplied live z-index reader, then
|
|
535
|
-
* delegates
|
|
536
|
-
*
|
|
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
|
|
537
534
|
* deps are present, and never when the gesture aimed at the clip's OWN current
|
|
538
535
|
* lane (`aimedLane === currentLane` — not a relocation).
|
|
539
536
|
*/
|
|
@@ -542,7 +539,6 @@ function syncStackingForEdit(
|
|
|
542
539
|
dragKey: string,
|
|
543
540
|
currentLane: number,
|
|
544
541
|
aimedLane: number,
|
|
545
|
-
multiKeys: ReadonlySet<string> | null,
|
|
546
542
|
deps: DragCommitDeps,
|
|
547
543
|
coalesceKey?: string,
|
|
548
544
|
): Promise<void> {
|
|
@@ -567,10 +563,11 @@ function syncStackingForEdit(
|
|
|
567
563
|
stackingContextId: el.stackingContextId ?? null,
|
|
568
564
|
}));
|
|
569
565
|
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
566
|
+
const patches = computeStackingPatches(
|
|
567
|
+
stackingEls,
|
|
568
|
+
[dragKey],
|
|
569
|
+
aimedLane < currentLane ? "up" : "down",
|
|
570
|
+
);
|
|
574
571
|
if (patches.length === 0) return Promise.resolve();
|
|
575
572
|
return Promise.resolve(onStackingPatches(patches, coalesceKey)).then(() => undefined);
|
|
576
573
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
computeStackingPatches,
|
|
4
4
|
laneIsAbove,
|
|
5
5
|
samePaintScope,
|
|
6
|
+
type StackingDirection,
|
|
6
7
|
type StackingElement,
|
|
7
8
|
} from "./timelineStackingSync";
|
|
8
9
|
|
|
@@ -18,9 +19,13 @@ function el(
|
|
|
18
19
|
return { key, track, start, duration, zIndex, isAudio, domIndex };
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
function patchMap(
|
|
22
|
+
function patchMap(
|
|
23
|
+
elements: StackingElement[],
|
|
24
|
+
edited: string[],
|
|
25
|
+
direction: StackingDirection,
|
|
26
|
+
): Record<string, number> {
|
|
22
27
|
const out: Record<string, number> = {};
|
|
23
|
-
for (const p of computeStackingPatches(elements, edited)) out[p.key] = p.zIndex;
|
|
28
|
+
for (const p of computeStackingPatches(elements, edited, direction)) out[p.key] = p.zIndex;
|
|
24
29
|
return out;
|
|
25
30
|
}
|
|
26
31
|
|
|
@@ -58,7 +63,7 @@ describe("stacking-context partitioning", () => {
|
|
|
58
63
|
stackingContextId: null,
|
|
59
64
|
};
|
|
60
65
|
|
|
61
|
-
expect(patchMap([root, scene], ["root"])).toEqual({});
|
|
66
|
+
expect(patchMap([root, scene], ["root"], "up")).toEqual({});
|
|
62
67
|
});
|
|
63
68
|
|
|
64
69
|
it("never compares or patches across stacking contexts", () => {
|
|
@@ -86,7 +91,7 @@ describe("stacking-context partitioning", () => {
|
|
|
86
91
|
};
|
|
87
92
|
// X edited: only same-context neighbours participate — none here, so X keeps
|
|
88
93
|
// its z (nothing to fix WITHIN its context) and Y is never touched.
|
|
89
|
-
expect(patchMap([x, y], ["x"])).toEqual({});
|
|
94
|
+
expect(patchMap([x, y], ["x"], "up")).toEqual({});
|
|
90
95
|
});
|
|
91
96
|
|
|
92
97
|
it("still resolves within the edited clip's own context", () => {
|
|
@@ -115,7 +120,7 @@ describe("stacking-context partitioning", () => {
|
|
|
115
120
|
// by flipping z so a MUST be lifted).
|
|
116
121
|
const aWrong = { ...a, track: 0, zIndex: 1 };
|
|
117
122
|
const bLow = { ...b, track: 1, zIndex: 5 };
|
|
118
|
-
const patches = patchMap([aWrong, bLow], ["a"]);
|
|
123
|
+
const patches = patchMap([aWrong, bLow], ["a"], "up");
|
|
119
124
|
expect(patches.a).toBeGreaterThan(5);
|
|
120
125
|
});
|
|
121
126
|
});
|
|
@@ -132,214 +137,168 @@ describe("computeStackingPatches", () => {
|
|
|
132
137
|
it("no overlapping clips → no patch", () => {
|
|
133
138
|
// a (0..5 on track 0) and b (10..15 on track 1) never overlap in time.
|
|
134
139
|
const elements = [el("a", 0, 0, 5, 10), el("b", 1, 10, 5, 5)];
|
|
135
|
-
expect(patchMap(elements, ["a"])).toEqual({});
|
|
140
|
+
expect(patchMap(elements, ["a"], "up")).toEqual({});
|
|
136
141
|
});
|
|
137
142
|
|
|
138
|
-
it("
|
|
139
|
-
// a on top lane (0) overlaps b on lane 1; a.z=1 is below b.z=5 → wrong.
|
|
143
|
+
it("moved up with z too low → raised above the clip it now sits above", () => {
|
|
140
144
|
const elements = [el("a", 0, 0, 10, 1), el("b", 1, 0, 10, 5)];
|
|
141
|
-
|
|
142
|
-
expect(patchMap(elements, ["a"])).toEqual({ a: 6 });
|
|
145
|
+
expect(patchMap(elements, ["a"], "up")).toEqual({ a: 6 });
|
|
143
146
|
});
|
|
144
147
|
|
|
145
|
-
it("
|
|
146
|
-
// a on lane 2 (bottom) overlaps b on lane 0 (top); a.z=9 above b.z=5 → wrong.
|
|
148
|
+
it("moved down with z too high → lowered below the clip now above it", () => {
|
|
147
149
|
const elements = [el("a", 2, 0, 10, 9), el("b", 0, 0, 10, 5)];
|
|
148
|
-
expect(patchMap(elements, ["a"])).toEqual({ a: 4 });
|
|
150
|
+
expect(patchMap(elements, ["a"], "down")).toEqual({ a: 4 });
|
|
149
151
|
});
|
|
150
152
|
|
|
151
|
-
it("
|
|
152
|
-
// a on top lane already has higher z than the lower-lane b it overlaps.
|
|
153
|
+
it("already in order → no patch (authored z preserved)", () => {
|
|
153
154
|
const elements = [el("a", 0, 0, 10, 8), el("b", 1, 0, 10, 3)];
|
|
154
|
-
expect(patchMap(elements, ["a"])).toEqual({});
|
|
155
|
+
expect(patchMap(elements, ["a"], "up")).toEqual({});
|
|
155
156
|
});
|
|
156
157
|
|
|
157
158
|
it("untouched clips never get a patch even when they overlap the edit", () => {
|
|
158
|
-
// b is out of order relative to a, but a is the only edited clip.
|
|
159
159
|
const elements = [el("a", 0, 0, 10, 1), el("b", 1, 0, 10, 5), el("c", 2, 0, 10, 9)];
|
|
160
|
-
const patches = computeStackingPatches(elements, ["a"]);
|
|
160
|
+
const patches = computeStackingPatches(elements, ["a"], "up");
|
|
161
161
|
expect(patches.map((p) => p.key)).toEqual(["a"]);
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
it("
|
|
165
|
-
// edited a on middle lane 1 between below-lane-2 (z=2) and above-lane-0 (z=10).
|
|
164
|
+
it("moved up rises one above the clips now below it and ignores the clips above it", () => {
|
|
166
165
|
const elements = [el("a", 1, 0, 10, 0), el("below", 2, 0, 10, 2), el("above", 0, 0, 10, 10)];
|
|
167
|
-
|
|
168
|
-
expect(patchMap(elements, ["a"])).toEqual({ a: 6 });
|
|
166
|
+
expect(patchMap(elements, ["a"], "up")).toEqual({ a: 3 });
|
|
169
167
|
});
|
|
170
168
|
|
|
171
|
-
it("
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
// order to break the tie, `above` no longer paints strictly above `a` (the
|
|
175
|
-
// under-patch bug). Tie-aware cascade: a→5 (above below's 4) AND above→6 so it
|
|
176
|
-
// stays strictly on top. Minimal: only the two overlapping neighbours move.
|
|
177
|
-
const elements = [el("a", 1, 0, 10, 0), el("below", 2, 0, 10, 4), el("above", 0, 0, 10, 5)];
|
|
178
|
-
expect(patchMap(elements, ["a"])).toEqual({ a: 5, above: 6 });
|
|
169
|
+
it("moved down sinks one below the clips now above it and ignores the clips below it", () => {
|
|
170
|
+
const elements = [el("a", 1, 0, 10, 20), el("below", 2, 0, 10, 2), el("above", 0, 0, 10, 10)];
|
|
171
|
+
expect(patchMap(elements, ["a"], "down")).toEqual({ a: 9 });
|
|
179
172
|
});
|
|
180
173
|
|
|
181
174
|
it("audio clips are excluded — an audio edit yields no patch", () => {
|
|
182
175
|
const elements = [el("music", 3, 0, 10, 0, true), el("v", 0, 0, 10, 5)];
|
|
183
|
-
expect(patchMap(elements, ["music"])).toEqual({});
|
|
176
|
+
expect(patchMap(elements, ["music"], "down")).toEqual({});
|
|
184
177
|
});
|
|
185
178
|
|
|
186
179
|
it("audio clips are excluded as neighbours — a visual edit ignores overlapping audio", () => {
|
|
187
|
-
// The only overlapping clip is audio → treated as no visual overlap → no patch.
|
|
188
180
|
const elements = [el("v", 0, 0, 10, 3), el("music", 3, 0, 10, 99, true)];
|
|
189
|
-
expect(patchMap(elements, ["v"])).toEqual({});
|
|
181
|
+
expect(patchMap(elements, ["v"], "up")).toEqual({});
|
|
190
182
|
});
|
|
191
183
|
|
|
192
|
-
it("
|
|
184
|
+
it("moved up over several clips → one above the highest of them", () => {
|
|
193
185
|
const elements = [el("a", 0, 0, 10, 0), el("b", 1, 0, 10, 3), el("c", 2, 0, 10, 7)];
|
|
194
|
-
|
|
195
|
-
expect(patchMap(elements, ["a"])).toEqual({ a: 8 });
|
|
186
|
+
expect(patchMap(elements, ["a"], "up")).toEqual({ a: 8 });
|
|
196
187
|
});
|
|
197
188
|
|
|
198
|
-
it("
|
|
189
|
+
it("moved down under several clips → one below the lowest of them, never under 0", () => {
|
|
199
190
|
const elements = [el("a", 2, 0, 10, 9), el("b", 0, 0, 10, 1), el("c", 1, 0, 10, 4)];
|
|
200
|
-
|
|
201
|
-
expect(patchMap(elements, ["a"])).toEqual({ a: 0 });
|
|
191
|
+
expect(patchMap(elements, ["a"], "down")).toEqual({ a: 0 });
|
|
202
192
|
});
|
|
203
193
|
|
|
204
194
|
it("partial time overlap still counts", () => {
|
|
205
|
-
// a: 0..6, b: 5..15 overlap in [5,6).
|
|
206
195
|
const elements = [el("a", 0, 0, 6, 1), el("b", 1, 5, 10, 5)];
|
|
207
|
-
expect(patchMap(elements, ["a"])).toEqual({ a: 6 });
|
|
196
|
+
expect(patchMap(elements, ["a"], "up")).toEqual({ a: 6 });
|
|
208
197
|
});
|
|
209
198
|
|
|
210
199
|
it("touching-but-not-overlapping intervals do NOT count", () => {
|
|
211
|
-
// a ends exactly where b starts (t=5) → half-open, no overlap.
|
|
212
200
|
const elements = [el("a", 0, 0, 5, 1), el("b", 1, 5, 5, 5)];
|
|
213
|
-
expect(patchMap(elements, ["a"])).toEqual({});
|
|
201
|
+
expect(patchMap(elements, ["a"], "up")).toEqual({});
|
|
214
202
|
});
|
|
215
203
|
|
|
216
|
-
it("multi-clip
|
|
217
|
-
// Drag a (lane 0) and b (lane 1) onto a region already holding c (lane 2, z=5).
|
|
218
|
-
// Both overlap c. Lower-lane b resolves first (above c → 6), then a (above b → 7).
|
|
204
|
+
it("multi-clip move up: the bottom member resolves first, the top one rises above it", () => {
|
|
219
205
|
const elements = [el("a", 0, 0, 10, 0), el("b", 1, 0, 10, 0), el("c", 2, 0, 10, 5)];
|
|
220
|
-
expect(patchMap(elements, ["a", "b"])).toEqual({ a: 7, b: 6 });
|
|
206
|
+
expect(patchMap(elements, ["a", "b"], "up")).toEqual({ a: 7, b: 6 });
|
|
221
207
|
});
|
|
222
208
|
|
|
223
|
-
it("multi-clip
|
|
209
|
+
it("multi-clip move down: the top member resolves first, the bottom one sinks below it", () => {
|
|
210
|
+
const elements = [el("a", 1, 0, 10, 9), el("b", 2, 0, 10, 9), el("c", 0, 0, 10, 5)];
|
|
211
|
+
expect(patchMap(elements, ["a", "b"], "down")).toEqual({ a: 4, b: 3 });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("multi-clip move skips a member that is already in order", () => {
|
|
224
215
|
const elements = [el("a", 0, 0, 10, 20), el("b", 1, 0, 10, 0), el("c", 2, 0, 10, 5)];
|
|
225
|
-
|
|
226
|
-
// below-neighbour c(5) and above-neighbour a(20) → floor((5+20)/2)=12.
|
|
227
|
-
expect(patchMap(elements, ["a", "b"])).toEqual({ b: 12 });
|
|
216
|
+
expect(patchMap(elements, ["a", "b"], "up")).toEqual({ b: 6 });
|
|
228
217
|
});
|
|
229
218
|
|
|
230
219
|
it("empty edited set → no patches", () => {
|
|
231
220
|
const elements = [el("a", 0, 0, 10, 1), el("b", 1, 0, 10, 5)];
|
|
232
|
-
expect(computeStackingPatches(elements, [])).toEqual([]);
|
|
221
|
+
expect(computeStackingPatches(elements, [], "up")).toEqual([]);
|
|
233
222
|
});
|
|
234
223
|
|
|
235
|
-
it("
|
|
236
|
-
// `ghost` is an overlapping upper-lane clip whose live node could not be
|
|
237
|
-
// resolved, so its z came back NaN. If it were fabricated to 0 it would be a
|
|
238
|
-
// real above-neighbour at the z-floor and drag the edited clip's cascade down.
|
|
239
|
-
// Excluded, the only real overlap is below-neighbour b(3) → a rises to 4.
|
|
224
|
+
it("an unresolved neighbour (non-finite z) is excluded, not treated as z=0", () => {
|
|
240
225
|
const elements = [
|
|
241
|
-
el("a",
|
|
242
|
-
el("b",
|
|
243
|
-
el("ghost",
|
|
226
|
+
el("a", 2, 0, 10, 9),
|
|
227
|
+
el("b", 0, 0, 10, 3),
|
|
228
|
+
el("ghost", 1, 0, 10, Number.NaN),
|
|
244
229
|
];
|
|
245
|
-
expect(patchMap(elements, ["a"])).toEqual({ a:
|
|
230
|
+
expect(patchMap(elements, ["a"], "down")).toEqual({ a: 2 });
|
|
246
231
|
});
|
|
247
232
|
|
|
248
|
-
it("
|
|
249
|
-
// The edited clip itself couldn't be resolved → it is dropped from the working
|
|
250
|
-
// set and produces nothing (no fabricated-0 self-patch).
|
|
233
|
+
it("an edited clip whose own z is unresolved (non-finite) yields no patch", () => {
|
|
251
234
|
const elements = [el("a", 0, 0, 10, Number.NaN), el("b", 1, 0, 10, 5)];
|
|
252
|
-
expect(patchMap(elements, ["a"])).toEqual({});
|
|
235
|
+
expect(patchMap(elements, ["a"], "up")).toEqual({});
|
|
253
236
|
});
|
|
254
237
|
});
|
|
255
238
|
|
|
256
|
-
describe("computeStackingPatches —
|
|
257
|
-
it("
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
//
|
|
239
|
+
describe("computeStackingPatches — only the moved clip changes", () => {
|
|
240
|
+
it("a full-frame scene on the top row stays behind a caption moved up a row under it", () => {
|
|
241
|
+
// Agent-made films often put the scene on track 0. The caption (z1, later in the
|
|
242
|
+
// file) moves from track 3 to 2, still under the scene's row: nothing is below
|
|
243
|
+
// it, so nothing changes. Lifting the scene over it would hide the caption.
|
|
244
|
+
const elements = [el("scene", 0, 0, 10, 0, false, 0), el("caption", 2, 0, 10, 1, false, 1)];
|
|
245
|
+
expect(patchMap(elements, ["caption"], "up")).toEqual({});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("a caption moved to the top row comes in front of the full-frame scene", () => {
|
|
249
|
+
const elements = [el("scene", 0, 0, 10, 5, false, 0), el("caption", -0.5, 0, 10, 1, false, 1)];
|
|
250
|
+
expect(patchMap(elements, ["caption"], "up")).toEqual({ caption: 6 });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("moved down under a z-0 clip earlier in the file: drops to 0 and the neighbour stays", () => {
|
|
254
|
+
// z never goes negative (it could paint behind the composition's background),
|
|
255
|
+
// and neighbours never move, so this order cannot be fully expressed.
|
|
256
|
+
const elements = [el("r", 0, 0, 10, 0, false, 0), el("v", 1, 0, 10, 2, false, 1)];
|
|
257
|
+
expect(patchMap(elements, ["v"], "down")).toEqual({ v: 0 });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("moved down when it cannot go lower → no patch", () => {
|
|
272
261
|
const elements = [el("r", 0, 0, 10, 0, false, 0), el("v", 1, 0, 10, 0, false, 1)];
|
|
273
|
-
expect(patchMap(elements, ["v"])).toEqual({
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it("equal-z without domIndex is ambiguous → conservatively bumps to guarantee order", () => {
|
|
277
|
-
// Same shape but NO domIndex: equal z is ambiguous, so the resolver cannot
|
|
278
|
-
// prove v is already below r and patches to make the order explicit (r above).
|
|
279
|
-
const elements = [el("r", 0, 0, 10, 0), el("v", 1, 0, 10, 0)];
|
|
280
|
-
const out = patchMap(elements, ["v"]);
|
|
281
|
-
// r must end up strictly above v (higher z) regardless of the exact numbers.
|
|
282
|
-
const vz = out.v ?? 0;
|
|
283
|
-
const rz = out.r ?? 0;
|
|
284
|
-
expect(rz).toBeGreaterThan(vz);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it("#2198 (Abhai repro): a lift cascades transitively so an UNTOUCHED pair never inverts", () => {
|
|
288
|
-
// m (z1, lane0, dom0), n (z0, lane1, dom1), e (z2, lane2, dom2, edited).
|
|
289
|
-
// e overlaps n [5,10); n overlaps m [12,15); e does NOT overlap m.
|
|
290
|
-
// Dragging e to the bottom lane forces n up to paint above e. A naive lift sets
|
|
291
|
-
// n→1, which TIES m (z1) and — n being later in the DOM — paints n above m,
|
|
292
|
-
// inverting the untouched (m,n) pair (which the next normalize would reshuffle).
|
|
293
|
-
// The transitive cascade lifts m too (→2) so m stays strictly above n.
|
|
294
|
-
const elements = [
|
|
295
|
-
el("m", 0, 12, 8, 1, false, 0),
|
|
296
|
-
el("n", 1, 5, 10, 0, false, 1),
|
|
297
|
-
el("e", 2, 0, 10, 2, false, 2),
|
|
298
|
-
];
|
|
299
|
-
expect(patchMap(elements, ["e"])).toEqual({ e: 0, n: 1, m: 2 });
|
|
262
|
+
expect(patchMap(elements, ["v"], "down")).toEqual({});
|
|
300
263
|
});
|
|
301
264
|
|
|
302
|
-
it("
|
|
303
|
-
// v
|
|
304
|
-
// non-overlapping clip x is never touched.
|
|
265
|
+
it("moved down never goes under a clip on a lower row it paints over now", () => {
|
|
266
|
+
// v cannot get under r (z0, earlier in the file); dropping it to 0 would hide it under w.
|
|
305
267
|
const elements = [
|
|
306
|
-
el("
|
|
307
|
-
el("
|
|
308
|
-
el("
|
|
309
|
-
el("x", 3, 50, 10, 0), // no time overlap → untouched
|
|
268
|
+
el("r", 0, 0, 10, 0, false, 0),
|
|
269
|
+
el("v", 1, 0, 10, 2, false, 1),
|
|
270
|
+
el("w", 2, 0, 10, 1, false, 2),
|
|
310
271
|
];
|
|
311
|
-
|
|
312
|
-
expect("x" in out).toBe(false);
|
|
313
|
-
// v below both r and s.
|
|
314
|
-
expect(out.v).toBeLessThan(out.r ?? 0);
|
|
315
|
-
expect(out.v).toBeLessThan(out.s ?? 0);
|
|
272
|
+
expect(patchMap(elements, ["v"], "down")).toEqual({});
|
|
316
273
|
});
|
|
317
|
-
});
|
|
318
274
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
// e must paint above b and below a. There is no integer strictly between 3 and
|
|
323
|
-
// 4, but a is LATER in the DOM, so e=4 ties a and a still paints on top by DOM
|
|
324
|
-
// order — a valid SINGLE patch. The old gap<2 rule cascaded and needlessly
|
|
325
|
-
// bumped a's authored z (the over-patch). Only e changes here.
|
|
326
|
-
const elements = [
|
|
327
|
-
el("b", 2, 0, 10, 3, false, 0),
|
|
328
|
-
el("e", 1, 0, 10, 0, false, 1),
|
|
329
|
-
el("a", 0, 0, 10, 4, false, 2),
|
|
330
|
-
];
|
|
331
|
-
expect(patchMap(elements, ["e"])).toEqual({ e: 4 });
|
|
275
|
+
it("moved down sinks only as far as the clips on lower rows allow", () => {
|
|
276
|
+
const elements = [el("s", 0, 0, 10, 5), el("v", 1, 0, 10, 9), el("w", 2, 0, 10, 7)];
|
|
277
|
+
expect(patchMap(elements, ["v"], "down")).toEqual({ v: 8 });
|
|
332
278
|
});
|
|
333
279
|
|
|
334
|
-
it("
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
280
|
+
it("moved down with equal z: the clip above, later in the file, already paints over it", () => {
|
|
281
|
+
const elements = [el("v", 1, 0, 10, 3, false, 0), el("r", 0, 0, 10, 3, false, 1)];
|
|
282
|
+
expect(patchMap(elements, ["v"], "down")).toEqual({});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("#2198: an untouched pair keeps its order because neighbours never move", () => {
|
|
286
|
+
// e overlaps n [5,10); n overlaps m [12,15); e does not overlap m.
|
|
338
287
|
const elements = [
|
|
339
|
-
el("
|
|
340
|
-
el("
|
|
341
|
-
el("e",
|
|
288
|
+
el("m", 0, 12, 8, 1, false, 0),
|
|
289
|
+
el("n", 1, 5, 10, 0, false, 1),
|
|
290
|
+
el("e", 2, 0, 10, 2, false, 2),
|
|
342
291
|
];
|
|
343
|
-
expect(patchMap(elements, ["e"])).toEqual({ e:
|
|
292
|
+
expect(patchMap(elements, ["e"], "down")).toEqual({ e: 0 });
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("equal z broken by file order: the clip later in the file already paints above", () => {
|
|
296
|
+
const elements = [el("b", 1, 0, 10, 3, false, 0), el("e", 0, 0, 10, 3, false, 1)];
|
|
297
|
+
expect(patchMap(elements, ["e"], "up")).toEqual({});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("equal z with no file order is ambiguous → the move still writes a z", () => {
|
|
301
|
+
const elements = [el("b", 1, 0, 10, 3), el("e", 0, 0, 10, 3)];
|
|
302
|
+
expect(patchMap(elements, ["e"], "up")).toEqual({ e: 4 });
|
|
344
303
|
});
|
|
345
304
|
});
|