@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
|
@@ -3,6 +3,7 @@ import type { TimelineElement } from "../store/playerStore";
|
|
|
3
3
|
import type { DraggedClipState } from "./useTimelineClipDrag";
|
|
4
4
|
import {
|
|
5
5
|
commitDraggedClipMove,
|
|
6
|
+
commitZMirrorLaneMove,
|
|
6
7
|
type DragCommitDeps,
|
|
7
8
|
type TimelineMoveEdit,
|
|
8
9
|
} from "./timelineClipDragCommit";
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
pushEditHistoryEntry,
|
|
13
14
|
} from "../../utils/editHistory";
|
|
14
15
|
import { normalizeToZones } from "./timelineZones";
|
|
16
|
+
import { resolveZMirrorLaneMove } from "./timelineZMirror";
|
|
15
17
|
import type { StackingPatch } from "./timelineStackingSync";
|
|
16
18
|
|
|
17
19
|
function el(
|
|
@@ -29,9 +31,7 @@ function el(
|
|
|
29
31
|
/** Flush the microtask chain: the z-sync now fires only after the move persist
|
|
30
32
|
* promise resolves (serialized), so tests asserting on it must await. */
|
|
31
33
|
async function flushMicrotasks(): Promise<void> {
|
|
32
|
-
await Promise.resolve();
|
|
33
|
-
await Promise.resolve();
|
|
34
|
-
await Promise.resolve();
|
|
34
|
+
for (let step = 0; step < 8; step += 1) await Promise.resolve();
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function drag(
|
|
@@ -219,6 +219,88 @@ describe("commitDraggedClipMove", () => {
|
|
|
219
219
|
expect(map.b).toBeUndefined(); // the other clip is NOT rewritten
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
+
it("persists a lane change in AUTHORED track space when the file is sparse", () => {
|
|
223
|
+
// Discovery normalized authored tracks {1, 2} to display lanes {0, 1}
|
|
224
|
+
// (authoredTrack records the file value). Moving 'a' onto b's lane must
|
|
225
|
+
// write b's AUTHORED track (2) — writing the display lane (1) would target
|
|
226
|
+
// a's own authored row and silently no-op in the file.
|
|
227
|
+
const elements = [
|
|
228
|
+
{ ...el("a", 0, 0, 3), authoredTrack: 1 },
|
|
229
|
+
{ ...el("b", 1, 10, 3), authoredTrack: 2 },
|
|
230
|
+
];
|
|
231
|
+
const { updateElement, onMoveElements } = runClipMove(
|
|
232
|
+
drag(elements[0], { previewStart: 20, previewTrack: 1, desiredTrack: 1 }),
|
|
233
|
+
{ elements, trackOrder: [0, 1] },
|
|
234
|
+
);
|
|
235
|
+
// Store stays in display-lane space, but authoredTrack is refreshed to the
|
|
236
|
+
// value just written to the file so a SECOND drag before any reload resolves
|
|
237
|
+
// authored tracks from current data, not the stale pre-edit value.
|
|
238
|
+
expect(updateElement).toHaveBeenCalledWith("a", { start: 20, track: 1, authoredTrack: 2 });
|
|
239
|
+
// ...while the persist is translated to the target lane's authored track.
|
|
240
|
+
const map = editMap(onMoveElements.mock.calls[0][0]);
|
|
241
|
+
expect(map.a).toEqual({ start: 20, track: 2 });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("resolves the authored track from occupants of the dragged clip's OWN source file", () => {
|
|
245
|
+
// Expanded sub-comp children live on synthetic display lanes next to host
|
|
246
|
+
// clips. Their authoredTrack is in THEIR file's coordinate space, so a lane
|
|
247
|
+
// occupied by a clip from a DIFFERENT file must never lend its authored
|
|
248
|
+
// value. Here lane 1 holds both a host-file clip (authored 12) and a
|
|
249
|
+
// same-file sibling (authored 7): the sibling answers.
|
|
250
|
+
const child3 = { ...el("c3", 0, 0, 3), authoredTrack: 3, sourceFile: "scene.html" };
|
|
251
|
+
const child7 = { ...el("c7", 1, 10, 3), authoredTrack: 7, sourceFile: "scene.html" };
|
|
252
|
+
const hostClip = { ...el("h", 1, 20, 3), authoredTrack: 12, sourceFile: "index.html" };
|
|
253
|
+
const elements = [child3, child7, hostClip];
|
|
254
|
+
const { onMoveElements } = runClipMove(
|
|
255
|
+
drag(child3, { previewStart: 0, previewTrack: 1, desiredTrack: 1 }),
|
|
256
|
+
{ elements, trackOrder: [0, 1] },
|
|
257
|
+
);
|
|
258
|
+
const map = editMap(onMoveElements.mock.calls[0][0]);
|
|
259
|
+
expect(map.c3).toEqual({ start: 0, track: 7 });
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("never persists the display-lane integer when the target lane has no same-file occupant", () => {
|
|
263
|
+
// Reviewer scenario: an expanded child of a SPARSE file (authored tracks 3
|
|
264
|
+
// and 7 → display lanes 4 and 5) dragged onto display lane 6, which holds
|
|
265
|
+
// only another file's clip. Persisting 6 (the display integer) or 12 (the
|
|
266
|
+
// foreign authored value) would corrupt the sparse file; the fallback
|
|
267
|
+
// offsets from the NEAREST same-file lane instead: authored 7 at lane 5,
|
|
268
|
+
// one lane further down → 8.
|
|
269
|
+
const child3 = { ...el("c3", 4, 0, 3), authoredTrack: 3, sourceFile: "scene.html" };
|
|
270
|
+
const child7 = { ...el("c7", 5, 10, 3), authoredTrack: 7, sourceFile: "scene.html" };
|
|
271
|
+
const foreign = { ...el("f", 6, 20, 3), authoredTrack: 12, sourceFile: "index.html" };
|
|
272
|
+
const elements = [child3, child7, foreign];
|
|
273
|
+
const { onMoveElements } = runClipMove(
|
|
274
|
+
drag(child3, { previewStart: 0, previewTrack: 6, desiredTrack: 6 }),
|
|
275
|
+
{ elements, trackOrder: [4, 5, 6] },
|
|
276
|
+
);
|
|
277
|
+
const map = editMap(onMoveElements.mock.calls[0][0]);
|
|
278
|
+
expect(map.c3.track).not.toBe(6); // not the display-lane integer
|
|
279
|
+
expect(map.c3.track).not.toBe(12); // not the foreign file's authored value
|
|
280
|
+
expect(map.c3).toEqual({ start: 0, track: 8 });
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("dropping onto an overlap spill sub-lane persists the base lane's shared authored track", () => {
|
|
284
|
+
// Authored track 2 holds two time-overlapping clips, which packTrackLanes
|
|
285
|
+
// spills onto display sub-lanes 1 and 2 (both authoredTrack 2). Dropping
|
|
286
|
+
// 'a' onto the spill sub-lane (2) is a legitimate same-track join: the
|
|
287
|
+
// persisted value is the shared authored track (2), even though the clip
|
|
288
|
+
// may re-pack onto a different sub-lane on the next normalize.
|
|
289
|
+
const elements = normalizeToZones([
|
|
290
|
+
{ ...el("a", 0, 30, 3), authoredTrack: 1 },
|
|
291
|
+
{ ...el("b1", 2, 0, 5), authoredTrack: 2 },
|
|
292
|
+
{ ...el("b2", 2, 3, 5), authoredTrack: 2 }, // overlaps b1 → spills
|
|
293
|
+
]);
|
|
294
|
+
expect(elements.map((e) => e.track)).toEqual([0, 1, 2]); // spill happened
|
|
295
|
+
const spillLane = elements.find((e) => e.id === "b2")!.track;
|
|
296
|
+
const { onMoveElements } = runClipMove(
|
|
297
|
+
drag(elements[0], { previewStart: 30, previewTrack: spillLane, desiredTrack: spillLane }),
|
|
298
|
+
{ elements, trackOrder: [0, 1, 2] },
|
|
299
|
+
);
|
|
300
|
+
const map = editMap(onMoveElements.mock.calls[0][0]);
|
|
301
|
+
expect(map.a).toEqual({ start: 30, track: 2 });
|
|
302
|
+
});
|
|
303
|
+
|
|
222
304
|
it("multi-selection time-move shifts EVERY selected clip by the drag delta (atomic)", () => {
|
|
223
305
|
const elements = [el("a", 0, 2, 3), el("b", 1, 10, 3), el("c", 2, 20, 3)];
|
|
224
306
|
// Drag 'a' +5s on its own lane while {a, b} are marquee-selected.
|
|
@@ -283,8 +365,76 @@ describe("commitDraggedClipMove", () => {
|
|
|
283
365
|
const map = editMap(onMoveElements.mock.calls[0][0]);
|
|
284
366
|
// Lanes are contiguous and distinct (no two overlapping clips share a lane).
|
|
285
367
|
expect(new Set([map.a.track, map.b.track, map.c.track])).toEqual(new Set([0, 1, 2]));
|
|
286
|
-
//
|
|
287
|
-
//
|
|
368
|
+
expect(map.a.track).toBe(0); // above the insert → unchanged
|
|
369
|
+
expect(map.c.track).toBe(1); // dragged clip lands on the new lane
|
|
370
|
+
expect(map.b.track).toBe(2); // at/below the insert → +1 shift
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("a selected audio passenger moves in time during a visual insert without being renumbered", () => {
|
|
374
|
+
const a = { ...el("a", 0, 0, 5), sourceFile: "scene.html" };
|
|
375
|
+
const b = { ...el("b", 1, 10, 5), sourceFile: "scene.html" };
|
|
376
|
+
const t = { ...el("t", 1, 0, 5), sourceFile: "scene.html" };
|
|
377
|
+
const audio = {
|
|
378
|
+
...el("audio", 2, 4, 20, "audio"),
|
|
379
|
+
sourceFile: "scene.html",
|
|
380
|
+
authoredTrack: 7,
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const { onMoveElements } = runClipMove(
|
|
384
|
+
drag(t, { previewStart: 5, previewTrack: 1, insertRow: 1 }),
|
|
385
|
+
{
|
|
386
|
+
elements: [a, b, t, audio],
|
|
387
|
+
trackOrder: [0, 1, 2],
|
|
388
|
+
selectedKeys: new Set(["t", "audio"]),
|
|
389
|
+
},
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const map = editMap(onMoveElements.mock.calls[0][0]);
|
|
393
|
+
expect(map.audio).toEqual({ start: 9, track: 7 });
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("an audio insert persists zone-local tracks without rewriting visual lanes", () => {
|
|
397
|
+
const v0 = el("v0", 0, 0, 5);
|
|
398
|
+
const v1 = el("v1", 1, 0, 5);
|
|
399
|
+
const a = { ...el("a", 2, 0, 5, "audio"), authoredTrack: 0 };
|
|
400
|
+
const b = { ...el("b", 3, 10, 5, "audio"), authoredTrack: 1 };
|
|
401
|
+
const t = { ...el("t", 3, 0, 5, "audio"), authoredTrack: 1 };
|
|
402
|
+
|
|
403
|
+
const { onMoveElements } = runClipMove(
|
|
404
|
+
drag(t, { previewStart: 0, previewTrack: 3, insertRow: 3 }),
|
|
405
|
+
{ elements: [v0, v1, a, b, t], trackOrder: [0, 1, 2, 3] },
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
expect(editMap(onMoveElements.mock.calls[0][0])).toEqual({
|
|
409
|
+
a: { start: 0, track: 0 },
|
|
410
|
+
t: { start: 0, track: 1 },
|
|
411
|
+
b: { start: 10, track: 2 },
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("refuses an audio insert when it would renumber a locked authored row", () => {
|
|
416
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
417
|
+
const visual = el("visual", 0, 0, 5);
|
|
418
|
+
const locked = {
|
|
419
|
+
...el("locked", 1, 0, 5, "audio"),
|
|
420
|
+
authoredTrack: 0,
|
|
421
|
+
timelineLocked: true,
|
|
422
|
+
};
|
|
423
|
+
const target = { ...el("target", 2, 0, 5, "audio"), authoredTrack: 1 };
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
const { onMoveElements } = runClipMove(
|
|
427
|
+
drag(target, { previewStart: 0, previewTrack: 2, insertRow: 1 }),
|
|
428
|
+
{ elements: [visual, locked, target], trackOrder: [0, 1, 2] },
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
expect(onMoveElements).not.toHaveBeenCalled();
|
|
432
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
433
|
+
expect.stringContaining("locked clip locked would need renumbering"),
|
|
434
|
+
);
|
|
435
|
+
} finally {
|
|
436
|
+
warnSpy.mockRestore();
|
|
437
|
+
}
|
|
288
438
|
});
|
|
289
439
|
|
|
290
440
|
describe("lane ↔ stacking sync", () => {
|
|
@@ -440,9 +590,9 @@ describe("commitDraggedClipMove", () => {
|
|
|
440
590
|
drag(elements[2], { previewStart: 30, previewTrack: 1, insertRow: 0 }),
|
|
441
591
|
[0, 1],
|
|
442
592
|
);
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
expect(lane.
|
|
593
|
+
expect(lane.dragged).toBe(0); // aimed at the very top
|
|
594
|
+
expect(lane.top).toBe(1);
|
|
595
|
+
expect(lane.mid).toBe(2);
|
|
446
596
|
});
|
|
447
597
|
|
|
448
598
|
it("BETWEEN-insert of a non-overlapping clip lands it between its neighbours", async () => {
|
|
@@ -453,8 +603,8 @@ describe("commitDraggedClipMove", () => {
|
|
|
453
603
|
[0, 1, 2],
|
|
454
604
|
);
|
|
455
605
|
expect(lane.a).toBe(0);
|
|
456
|
-
expect(lane.
|
|
457
|
-
expect(lane.
|
|
606
|
+
expect(lane.x).toBe(1); // between a and b, as aimed
|
|
607
|
+
expect(lane.b).toBe(2);
|
|
458
608
|
});
|
|
459
609
|
|
|
460
610
|
it("TOP-insert clears a NON-overlapping clip that currently tops the timeline", async () => {
|
|
@@ -466,7 +616,7 @@ describe("commitDraggedClipMove", () => {
|
|
|
466
616
|
drag(elements[2], { previewStart: 10, previewTrack: 2, insertRow: 0 }),
|
|
467
617
|
[0, 1, 2],
|
|
468
618
|
);
|
|
469
|
-
expect(lane.X).toBe(
|
|
619
|
+
expect(lane.X).toBe(0); // aimed top, cleared the non-overlapping T
|
|
470
620
|
});
|
|
471
621
|
|
|
472
622
|
it("dragging X among overlapping neighbours preserves the RELATIVE order of the others (symptom 2)", async () => {
|
|
@@ -941,3 +1091,212 @@ describe("commitDraggedClipMove", () => {
|
|
|
941
1091
|
});
|
|
942
1092
|
});
|
|
943
1093
|
});
|
|
1094
|
+
|
|
1095
|
+
describe("commitZMirrorLaneMove", () => {
|
|
1096
|
+
const mirrorDeps = (elements: TimelineElement[], trackOrder: number[]) => {
|
|
1097
|
+
const updateElement = vi.fn();
|
|
1098
|
+
const onMoveElements = vi.fn();
|
|
1099
|
+
return {
|
|
1100
|
+
updateElement,
|
|
1101
|
+
onMoveElements,
|
|
1102
|
+
deps: { elements, trackOrder, updateElement, onMoveElements } as DragCommitDeps,
|
|
1103
|
+
};
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
it("kind:move persists start + display lane with the persistTrack override (same shape as a lane drag)", async () => {
|
|
1107
|
+
// t sits at lane 2 in a sparse file (authored 7); the mirror lands it on
|
|
1108
|
+
// lane 0 whose authored track is 3.
|
|
1109
|
+
const t = { ...el("t", 2, 0, 10), authoredTrack: 7 };
|
|
1110
|
+
const elements = [{ ...el("a", 0, 20, 5), authoredTrack: 3 }, el("b", 1, 0, 10), t];
|
|
1111
|
+
const { updateElement, onMoveElements, deps } = mirrorDeps(elements, [0, 1, 2]);
|
|
1112
|
+
const moved = await commitZMirrorLaneMove(
|
|
1113
|
+
t,
|
|
1114
|
+
{ kind: "move", displayTrack: 0, persistTrack: 3 },
|
|
1115
|
+
deps,
|
|
1116
|
+
"z-reorder:bring-forward:t",
|
|
1117
|
+
);
|
|
1118
|
+
expect(moved).toBe(true);
|
|
1119
|
+
// Optimistic store update: DISPLAY lane + the written authoredTrack mirror.
|
|
1120
|
+
expect(updateElement).toHaveBeenCalledWith("t", {
|
|
1121
|
+
start: 0,
|
|
1122
|
+
track: 0,
|
|
1123
|
+
authoredTrack: 3,
|
|
1124
|
+
});
|
|
1125
|
+
// Persist: authored-space track, the z persist's coalesce key, lane-reorder op.
|
|
1126
|
+
expect(onMoveElements).toHaveBeenCalledTimes(1);
|
|
1127
|
+
const [persistEdits, coalesceKey, operation] = onMoveElements.mock.calls[0];
|
|
1128
|
+
expect(editMap(persistEdits)).toEqual({ t: { start: 0, track: 3 } });
|
|
1129
|
+
expect(coalesceKey).toBe("z-reorder:bring-forward:t");
|
|
1130
|
+
expect(operation).toBe("lane-reorder");
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
it("kind:insert reuses the track-insert renumber core (+1 shift below the new lane)", async () => {
|
|
1134
|
+
// a,b,c mutually overlapping on lanes 0/1/2. Mirror-insert c at row 1: c
|
|
1135
|
+
// lands on the new lane, b (at/below) shifts down — identical to the drag
|
|
1136
|
+
// insert test above, proving the shared core (no duplicated renumber logic).
|
|
1137
|
+
const elements = [el("a", 0, 0, 5), el("b", 1, 0, 5), el("c", 2, 0, 5)];
|
|
1138
|
+
const { onMoveElements, deps } = mirrorDeps(elements, [0, 1, 2]);
|
|
1139
|
+
const moved = await commitZMirrorLaneMove(
|
|
1140
|
+
elements[2],
|
|
1141
|
+
{ kind: "insert", insertRow: 1 },
|
|
1142
|
+
deps,
|
|
1143
|
+
"z-reorder:bring-forward:c",
|
|
1144
|
+
);
|
|
1145
|
+
expect(moved).toBe(true);
|
|
1146
|
+
expect(onMoveElements).toHaveBeenCalledTimes(1);
|
|
1147
|
+
expect(onMoveElements.mock.calls[0][1]).toBe("z-reorder:bring-forward:c");
|
|
1148
|
+
expect(onMoveElements.mock.calls[0][2]).toBe("track-insert");
|
|
1149
|
+
const map = editMap(onMoveElements.mock.calls[0][0]);
|
|
1150
|
+
expect(map.a.track).toBe(0);
|
|
1151
|
+
expect(map.c.track).toBe(1);
|
|
1152
|
+
expect(map.b.track).toBe(2);
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
it("visual mirror inserts never persist or renumber same-file audio", async () => {
|
|
1156
|
+
// b and t share a visual lane because they do not overlap. Inserting t between
|
|
1157
|
+
// a and b creates one extra visual lane, so whole-timeline normalization moves
|
|
1158
|
+
// audio from display lane 2 to 3. That display-only shift must not be written.
|
|
1159
|
+
const a = { ...el("a", 0, 0, 5), sourceFile: "scene.html" };
|
|
1160
|
+
const b = { ...el("b", 1, 10, 5), sourceFile: "scene.html" };
|
|
1161
|
+
const t = { ...el("t", 1, 0, 5), sourceFile: "scene.html" };
|
|
1162
|
+
const audio = { ...el("audio", 2, 0, 20, "audio"), sourceFile: "scene.html" };
|
|
1163
|
+
const { onMoveElements, deps } = mirrorDeps([a, b, t, audio], [0, 1, 2]);
|
|
1164
|
+
|
|
1165
|
+
const moved = await commitZMirrorLaneMove(
|
|
1166
|
+
t,
|
|
1167
|
+
{ kind: "insert", insertRow: 1 },
|
|
1168
|
+
deps,
|
|
1169
|
+
"z-reorder:bring-forward:t",
|
|
1170
|
+
);
|
|
1171
|
+
|
|
1172
|
+
expect(moved).toBe(true);
|
|
1173
|
+
const map = editMap(onMoveElements.mock.calls[0][0]);
|
|
1174
|
+
expect(map.audio).toBeUndefined();
|
|
1175
|
+
expect(map).toEqual({
|
|
1176
|
+
a: { start: 0, track: 0 },
|
|
1177
|
+
b: { start: 10, track: 2 },
|
|
1178
|
+
t: { start: 0, track: 1 },
|
|
1179
|
+
});
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
it("foreign expanded rows never distort the root-file insert topology", async () => {
|
|
1183
|
+
const host = { ...el("host", 0, 0, 5), sourceFile: "index.html" };
|
|
1184
|
+
const child1 = {
|
|
1185
|
+
...el("child-1", 0.25, 0, 5),
|
|
1186
|
+
sourceFile: "scene.html",
|
|
1187
|
+
expandedParentStart: 0,
|
|
1188
|
+
};
|
|
1189
|
+
const child2 = {
|
|
1190
|
+
...el("child-2", 0.5, 0, 5),
|
|
1191
|
+
sourceFile: "scene.html",
|
|
1192
|
+
expandedParentStart: 0,
|
|
1193
|
+
};
|
|
1194
|
+
const b = { ...el("b", 1, 0, 5), sourceFile: "index.html" };
|
|
1195
|
+
const t = { ...el("t", 2, 0, 5), sourceFile: "index.html" };
|
|
1196
|
+
const elements = [host, child1, child2, b, t];
|
|
1197
|
+
const { onMoveElements, deps } = mirrorDeps(elements, [0, 0.25, 0.5, 1, 2]);
|
|
1198
|
+
|
|
1199
|
+
const moved = await commitZMirrorLaneMove(
|
|
1200
|
+
t,
|
|
1201
|
+
{ kind: "insert", insertRow: 1 },
|
|
1202
|
+
deps,
|
|
1203
|
+
"z-reorder:bring-forward:t",
|
|
1204
|
+
);
|
|
1205
|
+
|
|
1206
|
+
expect(moved).toBe(true);
|
|
1207
|
+
expect(editMap(onMoveElements.mock.calls[0][0])).toEqual({
|
|
1208
|
+
host: { start: 0, track: 0 },
|
|
1209
|
+
t: { start: 0, track: 1 },
|
|
1210
|
+
b: { start: 0, track: 2 },
|
|
1211
|
+
});
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
it("never triggers the lane→z stacking sync (it would fight the just-set z values)", async () => {
|
|
1215
|
+
// Even with BOTH z-sync deps supplied (the drag paths would engage them for
|
|
1216
|
+
// a vertical move like this), the mirror commit must not emit stacking
|
|
1217
|
+
// patches — the z values were just written by the user's menu action.
|
|
1218
|
+
const t = el("t", 2, 0, 10);
|
|
1219
|
+
const elements = [el("a", 0, 20, 5), el("b", 1, 0, 10), t];
|
|
1220
|
+
const { deps } = mirrorDeps(elements, [0, 1, 2]);
|
|
1221
|
+
const onStackingPatches = vi.fn();
|
|
1222
|
+
const moved = await commitZMirrorLaneMove(
|
|
1223
|
+
t,
|
|
1224
|
+
{ kind: "move", displayTrack: 0, persistTrack: 0 },
|
|
1225
|
+
{ ...deps, readZIndex: () => 0, onStackingPatches },
|
|
1226
|
+
"z-reorder:bring-forward:t",
|
|
1227
|
+
);
|
|
1228
|
+
await flushMicrotasks();
|
|
1229
|
+
expect(moved).toBe(true);
|
|
1230
|
+
expect(onStackingPatches).not.toHaveBeenCalled();
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
it("resolves false and rolls the store back when the persist rejects", async () => {
|
|
1234
|
+
const t = el("t", 1, 0, 10);
|
|
1235
|
+
const elements = [el("a", 0, 20, 5), t];
|
|
1236
|
+
const updateElement = vi.fn();
|
|
1237
|
+
const onMoveElements = vi.fn().mockRejectedValue(new Error("boom"));
|
|
1238
|
+
const moved = await commitZMirrorLaneMove(
|
|
1239
|
+
t,
|
|
1240
|
+
{ kind: "move", displayTrack: 0, persistTrack: 0 },
|
|
1241
|
+
{ elements, trackOrder: [0, 1], updateElement, onMoveElements },
|
|
1242
|
+
"z-reorder:send-backward:t",
|
|
1243
|
+
);
|
|
1244
|
+
expect(moved).toBe(false);
|
|
1245
|
+
// Optimistic write then rollback to the original lane.
|
|
1246
|
+
expect(updateElement).toHaveBeenLastCalledWith("t", {
|
|
1247
|
+
start: 0,
|
|
1248
|
+
track: 1,
|
|
1249
|
+
authoredTrack: undefined,
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
it("END-TO-END one-element step: resolver insertRow renumbers the clip strictly between the two neighbors", async () => {
|
|
1254
|
+
// 3 stacked back-to-back clips + a free lane beyond the far one. Send t
|
|
1255
|
+
// (top) backward past b: the resolver must bound at c and produce the
|
|
1256
|
+
// insert row IMMEDIATELY below b, and commitZMirrorLaneMove's renumber must
|
|
1257
|
+
// land t strictly between b and c — never on the farther free lane 3.
|
|
1258
|
+
const t = el("t", 0, 0, 10);
|
|
1259
|
+
const b = el("b", 1, 0, 10);
|
|
1260
|
+
const c = el("c", 2, 0, 10);
|
|
1261
|
+
const far = el("far", 3, 20, 5); // free over t's span, beyond c
|
|
1262
|
+
const elements = [t, b, c, far];
|
|
1263
|
+
const move = resolveZMirrorLaneMove({
|
|
1264
|
+
action: "send-backward",
|
|
1265
|
+
element: t,
|
|
1266
|
+
elements,
|
|
1267
|
+
crossedKey: "b",
|
|
1268
|
+
});
|
|
1269
|
+
expect(move).toEqual({ kind: "insert", insertRow: 2 });
|
|
1270
|
+
const { onMoveElements, deps } = mirrorDeps(elements, [0, 1, 2, 3]);
|
|
1271
|
+
const moved = await commitZMirrorLaneMove(t, move!, deps, "z-reorder:send-backward:t");
|
|
1272
|
+
expect(moved).toBe(true);
|
|
1273
|
+
const map = editMap(onMoveElements.mock.calls[0][0]);
|
|
1274
|
+
// The renumber compacts t's vacated top lane, so the whole set shifts up by
|
|
1275
|
+
// one while t lands on the b/c boundary — strictly between the two.
|
|
1276
|
+
expect(map).toEqual({
|
|
1277
|
+
b: { start: 0, track: 0 },
|
|
1278
|
+
t: { start: 0, track: 1 },
|
|
1279
|
+
c: { start: 0, track: 2 },
|
|
1280
|
+
far: { start: 20, track: 3 },
|
|
1281
|
+
});
|
|
1282
|
+
expect(map.b.track).toBeLessThan(map.t.track);
|
|
1283
|
+
expect(map.t.track).toBeLessThan(map.c.track);
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
it("resolves false for a refused insert (locked clip would need renumbering)", async () => {
|
|
1287
|
+
// b is locked and sits at/below the insert row, so the whole-set renumber
|
|
1288
|
+
// is refused — no persist call.
|
|
1289
|
+
const a = el("a", 0, 0, 5);
|
|
1290
|
+
const b: TimelineElement = { ...el("b", 1, 0, 5), timelineLocked: true };
|
|
1291
|
+
const c = el("c", 2, 0, 5);
|
|
1292
|
+
const { onMoveElements, deps } = mirrorDeps([a, b, c], [0, 1, 2]);
|
|
1293
|
+
const moved = await commitZMirrorLaneMove(
|
|
1294
|
+
c,
|
|
1295
|
+
{ kind: "insert", insertRow: 1 },
|
|
1296
|
+
deps,
|
|
1297
|
+
"z-reorder:bring-forward:c",
|
|
1298
|
+
);
|
|
1299
|
+
expect(moved).toBe(false);
|
|
1300
|
+
expect(onMoveElements).not.toHaveBeenCalled();
|
|
1301
|
+
});
|
|
1302
|
+
});
|