@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
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
2
|
+
import { classifyZone } from "./timelineZones";
|
|
3
|
+
import { isLaneFree, timeRangesOverlap } from "./timelineCollision";
|
|
4
|
+
import { authoredTrackForLane, sameSourceFile } from "./timelineClipDragCommit";
|
|
5
|
+
import { samePaintScope } from "./timelineStackingSync";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Mirror a canvas z-order action (Bring to Front / Bring Forward / Send Backward /
|
|
9
|
+
* Send to Back) into a timeline LANE move — the pure resolver, no UI wiring.
|
|
10
|
+
*
|
|
11
|
+
* ── The model ────────────────────────────────────────────────────────────────
|
|
12
|
+
* Track order is the DEFAULT paint order; authored z is the ADVANCED override.
|
|
13
|
+
* Render truth stays z — the renderer never reads track index — and the studio
|
|
14
|
+
* maintains z ↔ track consistency at EDIT time: a deliberate vertical lane move
|
|
15
|
+
* syncs z (timelineStackingSync), and a z-order menu action calls THIS resolver
|
|
16
|
+
* to compute the accompanying lane move. When the user authors z that diverges
|
|
17
|
+
* from track order, the mirror never fights the authored override — it only
|
|
18
|
+
* keeps the default in step.
|
|
19
|
+
*
|
|
20
|
+
* ── Locked rules (agreed design — do not re-litigate here) ───────────────────
|
|
21
|
+
* - The mirror computes a lane move to ACCOMPANY a z action on a timeline clip;
|
|
22
|
+
* it never replaces the z patch.
|
|
23
|
+
* - ONE-ELEMENT STEP (bring-forward / send-backward): the z action stepped past
|
|
24
|
+
* exactly ONE element — the reference neighbor — so the lane move must too.
|
|
25
|
+
* Target = the free lane (whole-span, file-agnostic occupancy, same zone)
|
|
26
|
+
* closest to the reference, searched STRICTLY BETWEEN the reference's lane
|
|
27
|
+
* and the next temporally-overlapping same-file visual element's lane in the
|
|
28
|
+
* direction (exclusive bound). No free lane in that open interval (the common
|
|
29
|
+
* back-to-back case) → CREATE one at the boundary immediately beyond the
|
|
30
|
+
* reference neighbor (commitTrackInsert semantics) — never scan past the
|
|
31
|
+
* second element to a farther free lane, which would overshoot the paint
|
|
32
|
+
* order. With no second overlapping element
|
|
33
|
+
* beyond the reference, the bound is the zone edge: closest free lane beyond
|
|
34
|
+
* the neighbor, else insert immediately beyond it.
|
|
35
|
+
* - bring-to-front / send-to-back move past the WHOLE overlap set: closest free
|
|
36
|
+
* lane beyond the extreme overlap in the direction, else insert adjacent to
|
|
37
|
+
* the extreme.
|
|
38
|
+
* - Direction: bring-forward/front = toward LOWER display lanes (up = above);
|
|
39
|
+
* send-backward/back = toward HIGHER lanes, but only within the visual zone —
|
|
40
|
+
* the audio zone is untouched and never crossed (a bottom-of-zone insert lands
|
|
41
|
+
* AT the visual/audio boundary, i.e. still a visual lane).
|
|
42
|
+
* - Reference scope: same source file AND same stacking context (see
|
|
43
|
+
* samePaintScope — a file can contain several stacking contexts, and leaf z
|
|
44
|
+
* is only comparable within one). Lane FREENESS stays file-agnostic (any
|
|
45
|
+
* clip in the zone occupies its lane for everyone).
|
|
46
|
+
* - Non-clip decorations (no timeline presence) are out of scope — callers keep
|
|
47
|
+
* z-only behavior for them. Audio elements never mirror (z on audio is
|
|
48
|
+
* meaningless); the resolver returns null.
|
|
49
|
+
*
|
|
50
|
+
* ── OPEN product question ────────────────────────────────────────────────────
|
|
51
|
+
* send-to-back / bring-to-front scope: below/above EVERYTHING visual, or only
|
|
52
|
+
* the clips that temporally overlap the moved clip? The default implemented
|
|
53
|
+
* here is TEMPORAL-OVERLAP scope (the extreme is computed over same-file clips
|
|
54
|
+
* that overlap the moved clip in time), pending M/Bin sign-off. A clip with no
|
|
55
|
+
* temporal overlaps in the direction is "already at the extreme" → null.
|
|
56
|
+
*
|
|
57
|
+
* Deterministic: a pure function of its inputs — no Date, no randomness, no DOM.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
export type ZMirrorAction = "bring-to-front" | "bring-forward" | "send-backward" | "send-to-back";
|
|
61
|
+
|
|
62
|
+
export interface ZMirrorInput {
|
|
63
|
+
action: ZMirrorAction;
|
|
64
|
+
/** The clip acted on — store/display space (post-normalizeToZones lanes),
|
|
65
|
+
* carrying `authoredTrack` when the display lane diverges from the file. */
|
|
66
|
+
element: TimelineElement;
|
|
67
|
+
/** The expanded display element set (same set the drag commit reasons on). */
|
|
68
|
+
elements: TimelineElement[];
|
|
69
|
+
/** Timeline key of the neighbor the z action stepped over (forward/backward),
|
|
70
|
+
* when known — see resolveCrossedNeighbor in canvasContextMenuZOrder. */
|
|
71
|
+
crossedKey?: string | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type ZMirrorLaneMove =
|
|
75
|
+
| {
|
|
76
|
+
/** Land on an existing display lane. */
|
|
77
|
+
kind: "move";
|
|
78
|
+
/** Display lane to move to (store space). */
|
|
79
|
+
displayTrack: number;
|
|
80
|
+
/** Authored-space value to write (authoredTrackForLane translation). */
|
|
81
|
+
persistTrack: number;
|
|
82
|
+
}
|
|
83
|
+
| {
|
|
84
|
+
/** Create a new lane: boundary row compatible with commitTrackInsert's
|
|
85
|
+
* insertRow (index into the ascending display trackOrder; 0 = above the
|
|
86
|
+
* top lane, length = below the bottom). */
|
|
87
|
+
kind: "insert";
|
|
88
|
+
insertRow: number;
|
|
89
|
+
}
|
|
90
|
+
| null;
|
|
91
|
+
|
|
92
|
+
const keyOf = (el: TimelineElement): string => el.key ?? el.id;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Lane candidates for an EXPANDED sub-comp child: only its own siblings' lanes
|
|
96
|
+
* (same file). An expanded child's display row is synthetic host-space — landing
|
|
97
|
+
* it on an arbitrary host lane has no same-file occupant to translate the
|
|
98
|
+
* authored track from, and a track INSERT would renumber host space from a
|
|
99
|
+
* child origin. Ordinary top-level clips return null (no restriction).
|
|
100
|
+
*/
|
|
101
|
+
function expandedChildAllowedLanes(
|
|
102
|
+
element: TimelineElement,
|
|
103
|
+
elements: TimelineElement[],
|
|
104
|
+
): ReadonlySet<number> | null {
|
|
105
|
+
if (element.expandedParentStart == null) return null;
|
|
106
|
+
const selfKey = keyOf(element);
|
|
107
|
+
return new Set(
|
|
108
|
+
elements
|
|
109
|
+
.filter((el) => keyOf(el) !== selfKey && sameSourceFile(el, element))
|
|
110
|
+
.map((el) => el.track),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Ascending unique display lanes of `elements` — identical to how Timeline.tsx
|
|
115
|
+
* builds `trackOrder`, so `insertRow` indexes the same boundary space. Exported
|
|
116
|
+
* so the mirror wiring can hand commitZMirrorLaneMove the matching trackOrder. */
|
|
117
|
+
export function displayTrackOrder(elements: TimelineElement[]): number[] {
|
|
118
|
+
return [...new Set(elements.map((el) => el.track))].sort((a, b) => a - b);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Resolve the timeline lane move that mirrors a z-order action on `element`.
|
|
123
|
+
* Returns null when no timeline mirror applies: audio / zero-length clips, no
|
|
124
|
+
* reference neighbor in the action's direction (the menu action was likely
|
|
125
|
+
* disabled or a no-op), or the clip is already laned where the action puts it.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveZMirrorLaneMove(input: ZMirrorInput): ZMirrorLaneMove {
|
|
128
|
+
const { action, element, elements } = input;
|
|
129
|
+
if (classifyZone(element) === "audio") return null;
|
|
130
|
+
if (!(element.duration > 0)) return null;
|
|
131
|
+
|
|
132
|
+
const selfKey = keyOf(element);
|
|
133
|
+
const start = element.start;
|
|
134
|
+
const end = element.start + element.duration;
|
|
135
|
+
const up = action === "bring-forward" || action === "bring-to-front";
|
|
136
|
+
|
|
137
|
+
// Same paint scope (source file + stacking context), visual, temporally overlapping.
|
|
138
|
+
const overlapSet = elements.filter(
|
|
139
|
+
(el) =>
|
|
140
|
+
keyOf(el) !== selfKey &&
|
|
141
|
+
classifyZone(el) === "visual" &&
|
|
142
|
+
samePaintScope(el, element) &&
|
|
143
|
+
timeRangesOverlap(start, end, el.start, el.start + el.duration),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const referenceLane = resolveReferenceLane(input, overlapSet, up);
|
|
147
|
+
if (referenceLane == null) return null;
|
|
148
|
+
|
|
149
|
+
const order = displayTrackOrder(elements);
|
|
150
|
+
const visualLanes = displayTrackOrder(elements.filter((el) => classifyZone(el) === "visual"));
|
|
151
|
+
const refIdx = visualLanes.indexOf(referenceLane);
|
|
152
|
+
if (refIdx === -1) return null; // reference is not a visual lane — no mirror
|
|
153
|
+
|
|
154
|
+
const boundLane = stepBoundLane(action, overlapSet, referenceLane, up);
|
|
155
|
+
const allowedLanes = expandedChildAllowedLanes(element, elements);
|
|
156
|
+
const lane = closestFreeLane({
|
|
157
|
+
elements,
|
|
158
|
+
visualLanes,
|
|
159
|
+
refIdx,
|
|
160
|
+
up,
|
|
161
|
+
boundLane,
|
|
162
|
+
start,
|
|
163
|
+
end,
|
|
164
|
+
selfKey,
|
|
165
|
+
allowedLanes,
|
|
166
|
+
});
|
|
167
|
+
if (lane != null) {
|
|
168
|
+
// The closest free lane is the clip's OWN lane (possible only when z and
|
|
169
|
+
// track had diverged): the clip already sits where the action puts it.
|
|
170
|
+
if (lane === element.track) return null;
|
|
171
|
+
return {
|
|
172
|
+
kind: "move",
|
|
173
|
+
displayTrack: lane,
|
|
174
|
+
persistTrack: authoredTrackForLane(lane, elements, element),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Expanded children never INSERT: a new lane is a host-lane-space renumber,
|
|
179
|
+
// meaningless in the child's own file (buildTrackInsertEdits refuses too).
|
|
180
|
+
if (allowedLanes) return null;
|
|
181
|
+
// No free lane before the bound (or the zone edge) → create one adjacent to
|
|
182
|
+
// the reference: the boundary between its lane and the next in direction.
|
|
183
|
+
return { kind: "insert", insertRow: order.indexOf(referenceLane) + (up ? 0 : 1) };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface ZRepositionInput {
|
|
187
|
+
/** The clip the Layers-panel drag moved — store/display space. */
|
|
188
|
+
element: TimelineElement;
|
|
189
|
+
/** The expanded display element set (same set the drag commit reasons on). */
|
|
190
|
+
elements: TimelineElement[];
|
|
191
|
+
/**
|
|
192
|
+
* Timeline keys of the reordered sibling set in DESIRED render order,
|
|
193
|
+
* bottom→top, the moved element's own key included at its new slot. Siblings
|
|
194
|
+
* with no timeline presence carry null — they are skipped when resolving the
|
|
195
|
+
* nearest clip neighbors.
|
|
196
|
+
*/
|
|
197
|
+
desiredOrderKeys: ReadonlyArray<string | null>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Mirror an ARBITRARY z repositioning (a Layers-panel drag, which can jump
|
|
202
|
+
* several siblings in one drop) into a timeline lane move — the "equal jump"
|
|
203
|
+
* generalization of {@link resolveZMirrorLaneMove}'s one-step rule: the clip
|
|
204
|
+
* lands between its NEW paint neighbors' lanes.
|
|
205
|
+
*
|
|
206
|
+
* The reference lanes are the nearest siblings in the desired render order
|
|
207
|
+
* that are visual, same-file timeline clips: `above` = the first such sibling
|
|
208
|
+
* that now paints above the moved clip, `below` = the first that paints below.
|
|
209
|
+
* Target lane = the free lane (whole-span, same zone) strictly between the
|
|
210
|
+
* above-neighbor's lane and the below-neighbor's lane, closest to the above
|
|
211
|
+
* neighbor; when only one neighbor exists the zone edge bounds the search on
|
|
212
|
+
* the open side. No free lane in the interval → INSERT a new lane immediately
|
|
213
|
+
* beyond the above neighbor (below it), or immediately above the below
|
|
214
|
+
* neighbor when dropped on top — commitTrackInsert semantics, exactly like the
|
|
215
|
+
* menu mirror's insert fallback.
|
|
216
|
+
*
|
|
217
|
+
* Null when no mirror applies: audio / zero-length clips, no clip neighbor in
|
|
218
|
+
* the set (a z-only decoration shuffle), or the clip already sits where the
|
|
219
|
+
* drop puts it.
|
|
220
|
+
*/
|
|
221
|
+
// fallow-ignore-next-line complexity
|
|
222
|
+
export function resolveRepositionLaneMove(input: ZRepositionInput): ZMirrorLaneMove {
|
|
223
|
+
const { element, elements, desiredOrderKeys } = input;
|
|
224
|
+
if (classifyZone(element) === "audio") return null;
|
|
225
|
+
if (!(element.duration > 0)) return null;
|
|
226
|
+
|
|
227
|
+
const selfKey = keyOf(element);
|
|
228
|
+
const selfIdx = desiredOrderKeys.indexOf(selfKey);
|
|
229
|
+
if (selfIdx === -1) return null;
|
|
230
|
+
|
|
231
|
+
const clipLaneForKey = (key: string | null): number | null => {
|
|
232
|
+
if (key == null) return null;
|
|
233
|
+
const el = elements.find((candidate) => keyOf(candidate) === key);
|
|
234
|
+
return el &&
|
|
235
|
+
classifyZone(el) === "visual" &&
|
|
236
|
+
el.duration > 0 &&
|
|
237
|
+
samePaintScope(el, element) &&
|
|
238
|
+
keyOf(el) !== selfKey
|
|
239
|
+
? el.track
|
|
240
|
+
: null;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Nearest clip neighbor painting ABOVE (later in bottom→top order) / BELOW.
|
|
244
|
+
let aboveLane: number | null = null;
|
|
245
|
+
for (let i = selfIdx + 1; i < desiredOrderKeys.length && aboveLane == null; i++) {
|
|
246
|
+
aboveLane = clipLaneForKey(desiredOrderKeys[i]);
|
|
247
|
+
}
|
|
248
|
+
let belowLane: number | null = null;
|
|
249
|
+
for (let i = selfIdx - 1; i >= 0 && belowLane == null; i--) {
|
|
250
|
+
belowLane = clipLaneForKey(desiredOrderKeys[i]);
|
|
251
|
+
}
|
|
252
|
+
if (aboveLane == null && belowLane == null) return null;
|
|
253
|
+
|
|
254
|
+
const order = displayTrackOrder(elements);
|
|
255
|
+
const visualLanes = displayTrackOrder(elements.filter((el) => classifyZone(el) === "visual"));
|
|
256
|
+
const allowedLanes = expandedChildAllowedLanes(element, elements);
|
|
257
|
+
const args = {
|
|
258
|
+
elements,
|
|
259
|
+
visualLanes,
|
|
260
|
+
start: element.start,
|
|
261
|
+
end: element.start + element.duration,
|
|
262
|
+
selfKey,
|
|
263
|
+
allowedLanes,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
let lane: number | null;
|
|
267
|
+
let insertRow: number;
|
|
268
|
+
if (aboveLane != null) {
|
|
269
|
+
// Paints above = LOWER display lane: search DOWNWARD from the above
|
|
270
|
+
// neighbor (closest lane under it first), bounded by the below neighbor
|
|
271
|
+
// (exclusive) when one exists, else by the zone edge.
|
|
272
|
+
const refIdx = visualLanes.indexOf(aboveLane);
|
|
273
|
+
if (refIdx === -1) return null;
|
|
274
|
+
lane = closestFreeLane({ ...args, refIdx, up: false, boundLane: belowLane });
|
|
275
|
+
insertRow = order.indexOf(aboveLane) + 1;
|
|
276
|
+
} else {
|
|
277
|
+
// Dropped above everything that remains: search UPWARD from the below
|
|
278
|
+
// neighbor toward the zone top.
|
|
279
|
+
const refIdx = visualLanes.indexOf(belowLane!);
|
|
280
|
+
if (refIdx === -1) return null;
|
|
281
|
+
lane = closestFreeLane({ ...args, refIdx, up: true, boundLane: null });
|
|
282
|
+
insertRow = order.indexOf(belowLane!);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (lane != null) {
|
|
286
|
+
if (lane === element.track) return null;
|
|
287
|
+
return {
|
|
288
|
+
kind: "move",
|
|
289
|
+
displayTrack: lane,
|
|
290
|
+
persistTrack: authoredTrackForLane(lane, elements, element),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
// Expanded children never INSERT (host-lane-space renumber from a child
|
|
294
|
+
// origin) — see expandedChildAllowedLanes.
|
|
295
|
+
if (allowedLanes) return null;
|
|
296
|
+
return { kind: "insert", insertRow };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* ONE-ELEMENT-STEP bound (bring-forward / send-backward only): the lane of the
|
|
301
|
+
* NEXT temporally-overlapping same-file visual element strictly beyond the
|
|
302
|
+
* reference in the direction — the free-lane search may not reach it
|
|
303
|
+
* (exclusive). Front/back have no bound (they step past the whole overlap
|
|
304
|
+
* set), and neither does a step with no second overlapping element beyond the
|
|
305
|
+
* reference (the zone edge bounds instead).
|
|
306
|
+
*/
|
|
307
|
+
function stepBoundLane(
|
|
308
|
+
action: ZMirrorAction,
|
|
309
|
+
overlapSet: TimelineElement[],
|
|
310
|
+
referenceLane: number,
|
|
311
|
+
up: boolean,
|
|
312
|
+
): number | null {
|
|
313
|
+
if (action !== "bring-forward" && action !== "send-backward") return null;
|
|
314
|
+
const beyond = overlapSet
|
|
315
|
+
.map((el) => el.track)
|
|
316
|
+
.filter((lane) => (up ? lane < referenceLane : lane > referenceLane));
|
|
317
|
+
if (beyond.length === 0) return null;
|
|
318
|
+
return (up ? Math.max : Math.min)(...beyond);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Closest free lane strictly beyond the reference, lane-by-lane in direction,
|
|
323
|
+
* whole-span freeness, same zone (visual lanes only — never into audio),
|
|
324
|
+
* stopping at the exclusive bound when one applies. Null → no free lane in
|
|
325
|
+
* the open interval.
|
|
326
|
+
*/
|
|
327
|
+
function closestFreeLane(args: {
|
|
328
|
+
elements: TimelineElement[];
|
|
329
|
+
visualLanes: number[];
|
|
330
|
+
refIdx: number;
|
|
331
|
+
up: boolean;
|
|
332
|
+
boundLane: number | null;
|
|
333
|
+
start: number;
|
|
334
|
+
end: number;
|
|
335
|
+
selfKey: string;
|
|
336
|
+
/** When set, only these lanes are candidates (expanded-child scoping). */
|
|
337
|
+
allowedLanes?: ReadonlySet<number> | null;
|
|
338
|
+
}): number | null {
|
|
339
|
+
const { elements, visualLanes, refIdx, up, boundLane, start, end, selfKey, allowedLanes } = args;
|
|
340
|
+
const step = up ? -1 : 1;
|
|
341
|
+
for (let i = refIdx + step; i >= 0 && i < visualLanes.length; i += step) {
|
|
342
|
+
const lane = visualLanes[i];
|
|
343
|
+
if (pastSearchBound(lane, boundLane, up)) break;
|
|
344
|
+
if (allowedLanes && !allowedLanes.has(lane)) continue;
|
|
345
|
+
if (isLaneFree(elements, lane, start, end, selfKey)) return lane;
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** The exclusive one-element-step bound: stop at (never on/past) `boundLane`. */
|
|
351
|
+
function pastSearchBound(lane: number, boundLane: number | null, up: boolean): boolean {
|
|
352
|
+
if (boundLane == null) return false;
|
|
353
|
+
return up ? lane <= boundLane : lane >= boundLane;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* The lane the search starts from (the "reference neighbor"):
|
|
358
|
+
* - forward/backward: the crossed neighbor when provided and valid (a visual
|
|
359
|
+
* clip in the set); otherwise the closest temporally-overlapping same-file
|
|
360
|
+
* clip in the direction. None → null (the menu was probably disabled).
|
|
361
|
+
* - front/back: the extreme of the temporal-overlap set — topmost (lowest lane)
|
|
362
|
+
* for front, bottommost (highest lane) for back — restricted to overlaps
|
|
363
|
+
* strictly beyond the clip's own lane. None → already at the extreme → null.
|
|
364
|
+
*/
|
|
365
|
+
function resolveReferenceLane(
|
|
366
|
+
input: ZMirrorInput,
|
|
367
|
+
overlapSet: TimelineElement[],
|
|
368
|
+
up: boolean,
|
|
369
|
+
): number | null {
|
|
370
|
+
const stepAction = input.action === "bring-forward" || input.action === "send-backward";
|
|
371
|
+
if (stepAction) {
|
|
372
|
+
const crossedLane = crossedNeighborLane(input);
|
|
373
|
+
// Unknown / absent / non-visual crossed key → the temporal neighbor below.
|
|
374
|
+
if (crossedLane != null) return crossedLane;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Overlapping same-file lanes strictly beyond the moved clip's lane, in direction.
|
|
378
|
+
const lanes = overlapSet
|
|
379
|
+
.map((el) => el.track)
|
|
380
|
+
.filter((lane) => (up ? lane < input.element.track : lane > input.element.track));
|
|
381
|
+
if (lanes.length === 0) return null;
|
|
382
|
+
|
|
383
|
+
// Step actions want the CLOSEST lane in direction (max when up, min when
|
|
384
|
+
// down); front/back want the EXTREME of the set (min when up, max when down).
|
|
385
|
+
return (stepAction === up ? Math.max : Math.min)(...lanes);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** The crossed neighbor's display lane, when the key names a visual clip in the set. */
|
|
389
|
+
function crossedNeighborLane({ elements, crossedKey }: ZMirrorInput): number | null {
|
|
390
|
+
if (crossedKey == null) return null;
|
|
391
|
+
const crossed = elements.find((el) => keyOf(el) === crossedKey);
|
|
392
|
+
return crossed && classifyZone(crossed) === "visual" ? crossed.track : null;
|
|
393
|
+
}
|