@hyperframes/studio 0.8.90 → 0.8.92
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-DDjER_ii.js → hyperframes-player-DKJPDHUp.js} +1 -1
- package/dist/assets/{index-BKE3_N4v.js → index-C5_j5-3u.js} +178 -178
- package/dist/assets/{index-5SFeUXP8.js → index-CHZ4kFWm.js} +1 -1
- package/dist/assets/{index-CVsCxnlz.js → index-DpzpqJ_j.js} +1 -1
- package/dist/{chunk-KU4F7QZ3.js → chunk-QPSOTK6M.js} +8 -1
- package/dist/chunk-QPSOTK6M.js.map +1 -0
- package/dist/index.d.ts +18 -14
- package/dist/index.html +1 -1
- package/dist/index.js +9190 -8916
- package/dist/index.js.map +1 -1
- package/dist/player.js +1 -1
- package/package.json +8 -8
- package/src/components/TimelineToolbar.test.tsx +12 -0
- package/src/components/editor/MotionPathOverlay.tsx +5 -1
- package/src/components/editor/commitPreflightHandles.test.tsx +7 -20
- package/src/components/editor/elementPositionOffset.test.ts +62 -0
- package/src/components/editor/elementPositionOffset.ts +64 -0
- package/src/components/editor/motionPathCommit.test.ts +25 -5
- package/src/components/editor/motionPathSelection.ts +10 -3
- package/src/components/editor/useLayerRevealOverride.test.ts +8 -0
- package/src/components/editor/useLayerRevealOverride.ts +2 -1
- package/src/components/nle/NLEContext.tsx +1 -1
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +53 -0
- package/src/hooks/domEditPersistFailure.ts +8 -6
- package/src/hooks/elementOffsetStager.ts +65 -0
- package/src/hooks/gestureTransaction.ts +4 -2
- package/src/hooks/gsapDragCommit.ts +1 -0
- package/src/hooks/gsapEditOutcome.ts +2 -1
- package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +157 -0
- package/src/hooks/gsapRuntimeBridge.ts +50 -14
- package/src/hooks/gsapShared.test.ts +1 -1
- package/src/hooks/gsapShared.ts +16 -12
- package/src/hooks/gsapTweenReach.ts +30 -0
- package/src/hooks/timelineEditingHelpers.ts +2 -1
- package/src/hooks/useClipboard.source.test.ts +58 -0
- package/src/hooks/useClipboard.ts +12 -3
- package/src/hooks/useCommitPreflightCapabilities.ts +2 -2
- package/src/hooks/useDomEditCommits.ts +2 -0
- package/src/hooks/useDomEditPositionPatchCommit.ts +4 -1
- package/src/hooks/useDomEditSession.ts +2 -0
- package/src/hooks/useDomGeometryCommit.test.tsx +102 -0
- package/src/hooks/useDomGeometryCommit.ts +42 -1
- package/src/hooks/useDomGeometryCommits.test.tsx +27 -0
- package/src/hooks/useDomGeometryCommits.ts +14 -1
- package/src/hooks/useGsapAwareEditing.test.tsx +113 -5
- package/src/hooks/useGsapAwareEditing.ts +44 -7
- package/src/hooks/useGsapInteractionFailureTelemetry.test.tsx +2 -1
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +8 -4
- package/src/hooks/useRazorSplit.test.ts +5 -5
- package/src/hooks/useTimelineAssetDropOps.test.tsx +138 -0
- package/src/hooks/useTimelineAssetDropOps.ts +40 -18
- package/src/hooks/useTimelineEditing.test.tsx +384 -0
- package/src/hooks/useTimelineEditing.ts +12 -12
- package/src/hooks/useTimelineGroupEditing.ts +16 -7
- package/src/player/components/Timeline.rulerSeek.test.tsx +111 -0
- package/src/player/components/Timeline.showKeyframes.test.tsx +28 -2
- package/src/player/components/TimelinePropertyLanes.test.tsx +1 -1
- package/src/player/components/TimelineProvider.tsx +1 -0
- package/src/player/components/TimelineTrackHeader.test.tsx +1 -1
- package/src/player/components/TimelineTypes.ts +1 -1
- package/src/player/components/timelineClipDragCommit.test.ts +32 -66
- package/src/player/components/timelineClipDragCommit.ts +19 -37
- package/src/player/components/timelineClipDragPreview.test.ts +283 -24
- package/src/player/components/timelineClipDragPreview.ts +51 -83
- package/src/player/components/timelineClipDragTypes.ts +3 -8
- package/src/player/components/timelineCollision.test.ts +100 -302
- package/src/player/components/timelineCollision.ts +73 -160
- package/src/player/components/timelineDragDrop.test.tsx +19 -5
- package/src/player/components/timelineDragDrop.ts +7 -9
- package/src/player/components/timelineDragLanding.ts +0 -49
- package/src/player/components/timelineEditing.ts +2 -1
- package/src/player/components/timelineGroupEditing.ts +7 -2
- package/src/player/components/timelineLayout.test.ts +0 -25
- package/src/player/components/timelineLayout.ts +0 -16
- package/src/player/components/timelineMultiDragPreview.test.ts +34 -23
- package/src/player/components/timelineMultiDragPreview.ts +26 -25
- package/src/player/components/timelineProviderStateBuilders.test.ts +1 -0
- package/src/player/components/timelineProviderStateBuilders.ts +14 -6
- package/src/player/components/timelineShellMeta.tsx +99 -0
- package/src/player/components/timelineZMirror.test.ts +2 -2
- package/src/player/components/timelineZMirror.ts +1 -1
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +8 -5
- package/src/player/components/useTimelineClipDrag.ts +3 -2
- package/src/player/components/useTimelineDragPreviewState.ts +34 -0
- package/src/player/components/useTimelinePlayhead.test.tsx +215 -26
- package/src/player/components/useTimelinePlayhead.ts +56 -27
- package/src/player/components/useTimelineProviderState.tsx +29 -66
- package/src/player/components/useTimelineZoom.ts +9 -1
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +44 -0
- package/src/player/hooks/useTimelinePlayer.ts +7 -4
- package/src/player/lib/timelineDOM.test.ts +79 -0
- package/src/player/lib/timelineDOM.ts +16 -7
- package/src/player/lib/timelineIframeHelpers.test.ts +36 -0
- package/src/player/lib/timelineIframeHelpers.ts +12 -3
- package/src/player/store/liveTime.ts +6 -0
- package/src/player/store/playerStore.ts +2 -0
- package/src/player/store/timelineElement.ts +28 -3
- package/src/utils/razorSplitTransaction.test.ts +1 -1
- package/src/utils/razorSplitTransaction.ts +3 -3
- package/src/utils/timelineDropTrackInsert.test.ts +60 -1
- package/src/utils/timelineDropTrackInsert.ts +36 -5
- package/dist/chunk-KU4F7QZ3.js.map +0 -1
|
@@ -5,12 +5,10 @@ import {
|
|
|
5
5
|
isInsertAllowedForZone,
|
|
6
6
|
isLaneFree,
|
|
7
7
|
resolveInsertRow,
|
|
8
|
-
|
|
9
|
-
resolvePlacement,
|
|
8
|
+
resolveNearestFreeStart,
|
|
10
9
|
resolveZoneDropPlacement,
|
|
11
10
|
timeRangesOverlap,
|
|
12
11
|
} from "./timelineCollision";
|
|
13
|
-
import { INSERT_BOUNDARY_BAND } from "./timelineLayout";
|
|
14
12
|
|
|
15
13
|
function el(id: string, track: number, start: number, duration: number): TimelineElement {
|
|
16
14
|
return { id, tag: "video", start, duration, track };
|
|
@@ -41,196 +39,84 @@ describe("isLaneFree", () => {
|
|
|
41
39
|
});
|
|
42
40
|
});
|
|
43
41
|
|
|
44
|
-
describe("
|
|
45
|
-
const
|
|
42
|
+
describe("resolveNearestFreeStart", () => {
|
|
43
|
+
const row = [el("a", 0, 2, 3), el("b", 0, 8, 2)]; // busy [2,5) and [8,10)
|
|
46
44
|
|
|
47
|
-
it("keeps
|
|
48
|
-
|
|
49
|
-
expect(
|
|
50
|
-
resolvePlacement({
|
|
51
|
-
elements: els,
|
|
52
|
-
desiredTrack: 1,
|
|
53
|
-
start: 0,
|
|
54
|
-
duration: 4,
|
|
55
|
-
trackOrder,
|
|
56
|
-
excludeKey: null,
|
|
57
|
-
}),
|
|
58
|
-
).toEqual({
|
|
59
|
-
track: 1,
|
|
60
|
-
needsInsert: false,
|
|
61
|
-
});
|
|
45
|
+
it("keeps a start whose span is already free", () => {
|
|
46
|
+
expect(resolveNearestFreeStart(row, 0, 5, 3, null)).toBe(5);
|
|
47
|
+
expect(resolveNearestFreeStart(row, 1, 3, 4, null)).toBe(3); // another row
|
|
62
48
|
});
|
|
63
49
|
|
|
64
|
-
it("
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
expect(
|
|
68
|
-
|
|
69
|
-
elements: els,
|
|
70
|
-
desiredTrack: 2,
|
|
71
|
-
start: 1,
|
|
72
|
-
duration: 2,
|
|
73
|
-
trackOrder,
|
|
74
|
-
excludeKey: null,
|
|
75
|
-
}),
|
|
76
|
-
).toEqual({ track: 1, needsInsert: false });
|
|
50
|
+
it("keeps the clip's own start between edges no centisecond start fits", () => {
|
|
51
|
+
const tight = [el("a", 0, 0, 3.333), el("b", 0, 6.666, 4)];
|
|
52
|
+
expect(resolveNearestFreeStart(tight, 0, 3.333, 3.333, null)).toBe(10.67);
|
|
53
|
+
expect(resolveNearestFreeStart(tight, 0, 3.333, 3.333, null, 0, 3.333)).toBe(3.333);
|
|
54
|
+
expect(resolveNearestFreeStart(tight, 0, 3.4, 3.333, null, 0, 3.333)).toBe(3.333);
|
|
77
55
|
});
|
|
78
56
|
|
|
79
|
-
it("
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
elements: els,
|
|
85
|
-
desiredTrack: 2,
|
|
86
|
-
start: 0,
|
|
87
|
-
duration: 3,
|
|
88
|
-
trackOrder,
|
|
89
|
-
excludeKey: null,
|
|
90
|
-
}),
|
|
91
|
-
).toEqual({ track: 1, needsInsert: false });
|
|
57
|
+
it("keeps the clip's own start when float sums land a hair past a neighbour's edge", () => {
|
|
58
|
+
const after = [el("a", 0, 0, 0.333), el("b", 0, 2.066, 1)]; // 0.333 + 1.733 = 2.0660000000000003
|
|
59
|
+
expect(resolveNearestFreeStart(after, 0, 0.333, 1.733, null, 0, 0.333)).toBe(0.333);
|
|
60
|
+
const before = [el("a", 0, 0.1, 0.2), el("b", 0, 1.3, 1)]; // a ends at 0.30000000000000004
|
|
61
|
+
expect(resolveNearestFreeStart(before, 0, 0.3, 1, null, 0, 0.3)).toBe(0.3);
|
|
92
62
|
});
|
|
93
63
|
|
|
94
|
-
it("
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
expect(
|
|
98
|
-
|
|
99
|
-
elements: els,
|
|
100
|
-
desiredTrack: 1,
|
|
101
|
-
start: 1,
|
|
102
|
-
duration: 2,
|
|
103
|
-
trackOrder,
|
|
104
|
-
excludeKey: null,
|
|
105
|
-
}),
|
|
106
|
-
).toEqual({ track: 2, needsInsert: false });
|
|
64
|
+
it("takes the clip's own start only when it is free, not below minStart, and nearest", () => {
|
|
65
|
+
expect(resolveNearestFreeStart(row, 0, 4, 2, null, 0, 3.5)).toBe(5); // nearer, but its own spot overlaps a
|
|
66
|
+
expect(resolveNearestFreeStart(row, 0, 0, 1, null, 1, 0.5)).toBe(1); // below the floor
|
|
67
|
+
expect(resolveNearestFreeStart(row, 0, 6, 1, null, 0, 5.5)).toBe(6); // the release time is nearer
|
|
68
|
+
expect(resolveNearestFreeStart(row, 0, 6.5, 1, null, 0, 6)).toBe(6.5); // nearer again, not the origin
|
|
107
69
|
});
|
|
108
70
|
|
|
109
|
-
it("
|
|
110
|
-
|
|
111
|
-
expect(
|
|
112
|
-
resolvePlacement({
|
|
113
|
-
elements: els,
|
|
114
|
-
desiredTrack: 2,
|
|
115
|
-
start: 1,
|
|
116
|
-
duration: 2,
|
|
117
|
-
trackOrder,
|
|
118
|
-
excludeKey: null,
|
|
119
|
-
}),
|
|
120
|
-
).toEqual({ track: 2, needsInsert: true });
|
|
71
|
+
it("moves an overlapping span to the nearest time it fits", () => {
|
|
72
|
+
expect(resolveNearestFreeStart(row, 0, 4, 2, null)).toBe(5); // 1 s later beats 2 s earlier
|
|
73
|
+
expect(resolveNearestFreeStart(row, 0, 2.2, 1.5, null)).toBe(0.5); // 1.7 s earlier beats 2.8 s later
|
|
121
74
|
});
|
|
122
75
|
|
|
123
|
-
it("
|
|
124
|
-
|
|
125
|
-
// here). Landing on it would overlap, so the empty-zone branch must insert —
|
|
126
|
-
// not silently land on the occupied track (the placement hole).
|
|
127
|
-
const els = [el("blocker", 5, 0, 5)];
|
|
128
|
-
expect(
|
|
129
|
-
resolvePlacement({
|
|
130
|
-
elements: els,
|
|
131
|
-
desiredTrack: 5,
|
|
132
|
-
start: 1,
|
|
133
|
-
duration: 2,
|
|
134
|
-
trackOrder: [],
|
|
135
|
-
excludeKey: null,
|
|
136
|
-
}),
|
|
137
|
-
).toEqual({ track: 5, needsInsert: true });
|
|
76
|
+
it("sends a tie to the later time", () => {
|
|
77
|
+
expect(resolveNearestFreeStart([el("a", 0, 2, 2)], 0, 2, 2, null)).toBe(4); // 0 and 4 both 2 s away
|
|
138
78
|
});
|
|
139
79
|
|
|
140
|
-
it("
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// foreign-zone lane; the zone check must win and signal an insert.
|
|
144
|
-
const els = [el("elsewhere", 9, 0, 5)];
|
|
145
|
-
expect(
|
|
146
|
-
resolvePlacement({
|
|
147
|
-
elements: els,
|
|
148
|
-
desiredTrack: 5,
|
|
149
|
-
start: 1,
|
|
150
|
-
duration: 2,
|
|
151
|
-
trackOrder: [],
|
|
152
|
-
excludeKey: null,
|
|
153
|
-
}),
|
|
154
|
-
).toEqual({ track: 5, needsInsert: true });
|
|
80
|
+
it("skips a gap too short for the clip and lands after the row's last clip", () => {
|
|
81
|
+
expect(resolveNearestFreeStart(row, 0, 5, 4, null)).toBe(10); // the [5,8) gap is 3 s
|
|
82
|
+
expect(resolveNearestFreeStart([el("a", 0, 0, 30)], 0, 12, 5, null)).toBe(30);
|
|
155
83
|
});
|
|
156
84
|
|
|
157
|
-
it("
|
|
158
|
-
|
|
159
|
-
expect(
|
|
160
|
-
resolvePlacement({
|
|
161
|
-
elements: els,
|
|
162
|
-
desiredTrack: 1,
|
|
163
|
-
start: 0,
|
|
164
|
-
duration: 5,
|
|
165
|
-
trackOrder,
|
|
166
|
-
excludeKey: "self",
|
|
167
|
-
}),
|
|
168
|
-
).toEqual({ track: 1, needsInsert: false });
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
describe("resolveInsertRow", () => {
|
|
173
|
-
const n = 3; // three lanes: rows 0,1,2
|
|
174
|
-
|
|
175
|
-
it("targets the lane (null) when over its middle band", () => {
|
|
176
|
-
expect(resolveInsertRow(1.5, n, 0.22)).toBe(null); // dead center of lane 1
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("inserts at the top boundary of a lane when near its top edge", () => {
|
|
180
|
-
expect(resolveInsertRow(1.1, n, 0.22)).toBe(1); // just into lane 1 → boundary above it
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("inserts at the bottom boundary of a lane when near its bottom edge", () => {
|
|
184
|
-
expect(resolveInsertRow(1.9, n, 0.22)).toBe(2); // near bottom of lane 1 → boundary below
|
|
85
|
+
it("ignores the excluded (dragged) clip", () => {
|
|
86
|
+
expect(resolveNearestFreeStart(row, 0, 2, 3, "a")).toBe(2);
|
|
185
87
|
});
|
|
186
88
|
|
|
187
|
-
it("
|
|
188
|
-
|
|
89
|
+
it("never goes below the lowest allowed start", () => {
|
|
90
|
+
const blocked = [el("c", 0, 6.4, 3.6)]; // busy [6.4,10)
|
|
91
|
+
expect(resolveNearestFreeStart(blocked, 0, 6.5, 2, null)).toBe(4.4);
|
|
92
|
+
expect(resolveNearestFreeStart(blocked, 0, 6.5, 2, null, 6.5)).toBe(10);
|
|
93
|
+
expect(resolveNearestFreeStart([], 0, 6.667, 2, null, 6.667)).toBe(6.667); // not rounded up
|
|
189
94
|
});
|
|
190
95
|
|
|
191
|
-
it("
|
|
192
|
-
|
|
96
|
+
it("keeps a gap-bound start on centiseconds so a written start cannot overlap", () => {
|
|
97
|
+
const start = resolveNearestFreeStart([el("a", 0, 7.1, 2)], 0, 3, 5.045333, null);
|
|
98
|
+
expect(start).toBe(2.05);
|
|
99
|
+
expect(start + 5.045333).toBeLessThanOrEqual(7.1);
|
|
193
100
|
});
|
|
194
101
|
});
|
|
195
102
|
|
|
196
|
-
describe("resolveInsertRow
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const n = 3;
|
|
203
|
-
const b = INSERT_BOUNDARY_BAND;
|
|
204
|
-
|
|
205
|
-
it("the band is the clip inset (3/48), not the old feel-tuned 0.32", () => {
|
|
206
|
-
expect(b).toBeCloseTo(3 / 48, 10);
|
|
207
|
-
expect(b).toBeLessThan(0.1);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("returns null across the WHOLE clip body of every lane (no insert = move-to-lane)", () => {
|
|
211
|
-
// Sweep each lane's body [band+ε, 1−band−ε] in fine steps → always a move.
|
|
212
|
-
for (let lane = 0; lane < n; lane++) {
|
|
213
|
-
for (let frac = b + 0.01; frac <= 1 - b - 0.01 + 1e-9; frac += 0.02) {
|
|
214
|
-
expect(resolveInsertRow(lane + frac, n, b)).toBeNull();
|
|
215
|
-
}
|
|
103
|
+
describe("resolveInsertRow (only empty space outside the rows opens a track)", () => {
|
|
104
|
+
const n = 3; // three rows: 0, 1, 2
|
|
105
|
+
|
|
106
|
+
it("stays on the row everywhere over it, edges included", () => {
|
|
107
|
+
for (let rowFloat = 0; rowFloat < n; rowFloat += 0.01) {
|
|
108
|
+
expect(resolveInsertRow(rowFloat, n)).toBeNull();
|
|
216
109
|
}
|
|
217
110
|
});
|
|
218
111
|
|
|
219
|
-
it("
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
expect(resolveInsertRow(1 - b / 2, n, b)).toBe(1); // bottom gutter of lane 0
|
|
224
|
-
expect(resolveInsertRow(1 + b / 2, n, b)).toBe(1); // top gutter of lane 1
|
|
225
|
-
expect(resolveInsertRow(2 - b / 2, n, b)).toBe(2);
|
|
226
|
-
expect(resolveInsertRow(2 + b / 2, n, b)).toBe(2);
|
|
112
|
+
it("opens a track above the first row and below the last", () => {
|
|
113
|
+
expect(resolveInsertRow(-0.4, n)).toBe(0);
|
|
114
|
+
expect(resolveInsertRow(n, n)).toBe(n);
|
|
115
|
+
expect(resolveInsertRow(n + 0.4, n)).toBe(n);
|
|
227
116
|
});
|
|
228
117
|
|
|
229
|
-
it("
|
|
230
|
-
expect(resolveInsertRow(
|
|
231
|
-
expect(resolveInsertRow(-0.4, n, b)).toBe(0); // in the top breathing pad
|
|
232
|
-
expect(resolveInsertRow(n - b / 2, n, b)).toBe(n); // bottom gutter of last lane
|
|
233
|
-
expect(resolveInsertRow(n + 0.4, n, b)).toBe(n); // in the bottom breathing pad
|
|
118
|
+
it("opens the first track on an empty timeline", () => {
|
|
119
|
+
expect(resolveInsertRow(0.5, 0)).toBe(0);
|
|
234
120
|
});
|
|
235
121
|
});
|
|
236
122
|
|
|
@@ -273,7 +159,7 @@ describe("isInsertAllowedForZone", () => {
|
|
|
273
159
|
});
|
|
274
160
|
});
|
|
275
161
|
|
|
276
|
-
describe("resolveZoneDropPlacement (
|
|
162
|
+
describe("resolveZoneDropPlacement (a drop on a row stays on that row, no overlap)", () => {
|
|
277
163
|
// order [0,1,2] visual + [3] audio. audioRow = 3.
|
|
278
164
|
const order = [0, 1, 2, 3];
|
|
279
165
|
const audioTracks = new Set([3]);
|
|
@@ -286,90 +172,49 @@ describe("resolveZoneDropPlacement (the whole drop decision, no same-track overl
|
|
|
286
172
|
dragKey: "x",
|
|
287
173
|
isAudio: false,
|
|
288
174
|
};
|
|
175
|
+
const allVisualOccupied = () => [el("a", 0, 0, 10), el("b", 1, 0, 10), el("c", 2, 0, 10)];
|
|
289
176
|
|
|
290
177
|
it("lands on the aimed track when it is free at that time", () => {
|
|
291
178
|
expect(
|
|
292
179
|
resolveZoneDropPlacement({ ...base, elements: [el("a", 1, 10, 3)], desiredTrack: 1 }),
|
|
293
|
-
).toEqual({ track: 1, insertRow: null });
|
|
180
|
+
).toEqual({ track: 1, insertRow: null, start: 2 });
|
|
294
181
|
});
|
|
295
182
|
|
|
296
|
-
it("
|
|
183
|
+
it("stays on the aimed row at its nearest free time when the aimed span is occupied", () => {
|
|
297
184
|
expect(
|
|
298
185
|
resolveZoneDropPlacement({ ...base, elements: [el("a", 1, 0, 5)], desiredTrack: 1 }),
|
|
299
|
-
).toEqual({ track:
|
|
186
|
+
).toEqual({ track: 1, insertRow: null, start: 5 });
|
|
300
187
|
});
|
|
301
188
|
|
|
302
|
-
it("
|
|
189
|
+
it("never hops to another row or opens a track when every row is occupied", () => {
|
|
303
190
|
expect(
|
|
304
|
-
resolveZoneDropPlacement({
|
|
305
|
-
|
|
306
|
-
elements: [el("a", 0, 0, 5), el("b", 1, 0, 5)],
|
|
307
|
-
desiredTrack: 1,
|
|
308
|
-
}),
|
|
309
|
-
).toEqual({ track: 2, insertRow: null });
|
|
191
|
+
resolveZoneDropPlacement({ ...base, elements: allVisualOccupied(), desiredTrack: 1 }),
|
|
192
|
+
).toEqual({ track: 1, insertRow: null, start: 10 });
|
|
310
193
|
});
|
|
311
194
|
|
|
312
|
-
it("
|
|
195
|
+
it("does not fall back to the dragged clip's origin row", () => {
|
|
313
196
|
expect(
|
|
314
197
|
resolveZoneDropPlacement({
|
|
315
198
|
...base,
|
|
316
199
|
elements: [el("a", 0, 0, 5), el("b", 1, 0, 5), el("x", 2, 0, 5)],
|
|
317
200
|
desiredTrack: 1,
|
|
318
201
|
}),
|
|
319
|
-
).toEqual({ track: 1, insertRow:
|
|
320
|
-
|
|
321
|
-
expect(
|
|
322
|
-
resolveZoneDropPlacement({
|
|
323
|
-
...base,
|
|
324
|
-
elements: [el("x", 0, 0, 5), el("b", 1, 0, 5), el("a", 2, 0, 5)],
|
|
325
|
-
desiredTrack: 1,
|
|
326
|
-
}),
|
|
327
|
-
).toEqual({ track: 1, insertRow: 2 });
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it("auto-creates a new track when EVERY lane in the zone is occupied at that time", () => {
|
|
331
|
-
expect(
|
|
332
|
-
resolveZoneDropPlacement({
|
|
333
|
-
...base,
|
|
334
|
-
elements: [el("a", 0, 0, 5), el("b", 1, 0, 5), el("c", 2, 0, 5)],
|
|
335
|
-
desiredTrack: 1,
|
|
336
|
-
}),
|
|
337
|
-
).toEqual({ track: 1, insertRow: 2 });
|
|
202
|
+
).toEqual({ track: 1, insertRow: null, start: 5 });
|
|
338
203
|
});
|
|
339
204
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
205
|
+
it("Tag (2 s, 5 s long) onto the Subtitle row (0-6 s) lands at 6 s on that row", () => {
|
|
206
|
+
const elements = [el("title", 0, 0, 10), el("subtitle", 1, 0, 6), el("tag", 2, 2, 5)];
|
|
207
|
+
const drop = { ...base, elements, start: 2, duration: 5, dragKey: "tag" };
|
|
208
|
+
expect(resolveZoneDropPlacement({ ...drop, desiredTrack: 1 })).toEqual({
|
|
209
|
+
track: 1,
|
|
210
|
+
insertRow: null,
|
|
211
|
+
start: 6,
|
|
212
|
+
});
|
|
213
|
+
expect(resolveZoneDropPlacement({ ...drop, desiredTrack: 0 })).toEqual({
|
|
214
|
+
track: 0,
|
|
215
|
+
insertRow: null,
|
|
216
|
+
start: 10,
|
|
350
217
|
});
|
|
351
|
-
expect(result.insertRow).not.toBeNull(); // a track is created, not an origin snap-back
|
|
352
|
-
expect(result).toEqual({ track: 1, insertRow: 2 }); // adjacent, below the aimed row
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it("opens the new adjacent track ABOVE the aimed row when the pointer is in its upper half (UX rule 3)", () => {
|
|
356
|
-
expect(
|
|
357
|
-
resolveZoneDropPlacement({
|
|
358
|
-
...base,
|
|
359
|
-
elements: allVisualOccupied(),
|
|
360
|
-
desiredTrack: 1,
|
|
361
|
-
preferInsertAbove: true,
|
|
362
|
-
}),
|
|
363
|
-
).toEqual({ track: 1, insertRow: 1 }); // boundary ABOVE lane 1
|
|
364
|
-
// Same aim, pointer in the lower half → the track opens BELOW the aimed row.
|
|
365
|
-
expect(
|
|
366
|
-
resolveZoneDropPlacement({
|
|
367
|
-
...base,
|
|
368
|
-
elements: allVisualOccupied(),
|
|
369
|
-
desiredTrack: 1,
|
|
370
|
-
preferInsertAbove: false,
|
|
371
|
-
}),
|
|
372
|
-
).toEqual({ track: 1, insertRow: 2 });
|
|
373
218
|
});
|
|
374
219
|
|
|
375
220
|
it("shares a track for sequential (non-overlapping) clips", () => {
|
|
@@ -380,45 +225,37 @@ describe("resolveZoneDropPlacement (the whole drop decision, no same-track overl
|
|
|
380
225
|
desiredTrack: 1,
|
|
381
226
|
start: 2,
|
|
382
227
|
}),
|
|
383
|
-
).toEqual({ track: 1, insertRow: null });
|
|
228
|
+
).toEqual({ track: 1, insertRow: null, start: 2 });
|
|
384
229
|
});
|
|
385
230
|
|
|
386
231
|
it("clamps a visual clip OUT of the audio zone before placing", () => {
|
|
387
232
|
expect(resolveZoneDropPlacement({ ...base, elements: [], desiredTrack: 3 })).toEqual({
|
|
388
233
|
track: 2,
|
|
389
234
|
insertRow: null,
|
|
235
|
+
start: 2,
|
|
390
236
|
});
|
|
391
237
|
});
|
|
392
238
|
|
|
393
239
|
it("clamps an audio clip INTO the audio zone before placing", () => {
|
|
394
240
|
expect(
|
|
395
241
|
resolveZoneDropPlacement({ ...base, elements: [], desiredTrack: 0, isAudio: true }),
|
|
396
|
-
).toEqual({ track: 3, insertRow: null });
|
|
242
|
+
).toEqual({ track: 3, insertRow: null, start: 2 });
|
|
397
243
|
});
|
|
398
244
|
|
|
399
245
|
it("upward create-drag off the top lane inserts at the TOP of the visual zone, never below audio (reviewer repro)", () => {
|
|
400
|
-
//
|
|
401
|
-
// create threshold emits the sentinel desiredTrack = minTrack-1 = -1. The
|
|
402
|
-
// old hoist did order.indexOf(-1) = -1 and fell to below = order.length = 4,
|
|
403
|
-
// creating the new track BELOW the audio zone — repro {track:-1, insertRow:4}.
|
|
404
|
-
// It must now anchor to the top boundary of the visual zone.
|
|
246
|
+
// The sentinel desiredTrack = minTrack-1 = -1 must anchor to the visual zone's top boundary.
|
|
405
247
|
expect(
|
|
406
248
|
resolveZoneDropPlacement({ ...base, elements: allVisualOccupied(), desiredTrack: -1 }),
|
|
407
|
-
).toEqual({ track: -1, insertRow: 0 });
|
|
249
|
+
).toEqual({ track: -1, insertRow: 0, start: 2 });
|
|
408
250
|
});
|
|
409
251
|
|
|
410
252
|
it("downward create-drag off the bottom visual lane inserts at the audio boundary, never below it", () => {
|
|
411
|
-
// Sentinel desiredTrack = maxTrack+1 = 4 for a downward create-drag. A visual
|
|
412
|
-
// clip must land at the bottom of the visual zone (row 3 = just above audio),
|
|
413
|
-
// not past the audio lanes.
|
|
414
253
|
expect(
|
|
415
254
|
resolveZoneDropPlacement({ ...base, elements: allVisualOccupied(), desiredTrack: 4 }),
|
|
416
|
-
).toEqual({ track: 4, insertRow: 3 });
|
|
255
|
+
).toEqual({ track: 4, insertRow: 3, start: 2 });
|
|
417
256
|
});
|
|
418
257
|
|
|
419
258
|
it("audio create-drag with an out-of-range aim inserts inside the audio zone", () => {
|
|
420
|
-
// An audio clip aimed above its zone (sentinel below its own min lane) anchors
|
|
421
|
-
// to the audio zone's top boundary (row 3), not into the visual zone.
|
|
422
259
|
expect(
|
|
423
260
|
resolveZoneDropPlacement({
|
|
424
261
|
...base,
|
|
@@ -426,22 +263,22 @@ describe("resolveZoneDropPlacement (the whole drop decision, no same-track overl
|
|
|
426
263
|
desiredTrack: -1,
|
|
427
264
|
isAudio: true,
|
|
428
265
|
}),
|
|
429
|
-
).toEqual({ track: -1, insertRow: 3 });
|
|
266
|
+
).toEqual({ track: -1, insertRow: 3, start: 2 });
|
|
430
267
|
});
|
|
431
268
|
|
|
432
|
-
it("honors
|
|
269
|
+
it("honors an insert aimed at the space outside the rows in the clip's own zone", () => {
|
|
433
270
|
expect(
|
|
434
|
-
resolveZoneDropPlacement({ ...base, elements: [], desiredTrack:
|
|
435
|
-
).toEqual({ track:
|
|
271
|
+
resolveZoneDropPlacement({ ...base, elements: [], desiredTrack: 0, deliberateInsertRow: 0 }),
|
|
272
|
+
).toEqual({ track: 0, insertRow: 0, start: 2 });
|
|
436
273
|
});
|
|
437
274
|
|
|
438
|
-
it("ignores
|
|
275
|
+
it("ignores an insert that lands in the WRONG zone (visual into audio)", () => {
|
|
439
276
|
expect(
|
|
440
277
|
resolveZoneDropPlacement({ ...base, elements: [], desiredTrack: 1, deliberateInsertRow: 4 }),
|
|
441
|
-
).toEqual({ track: 1, insertRow: null });
|
|
278
|
+
).toEqual({ track: 1, insertRow: null, start: 2 });
|
|
442
279
|
});
|
|
443
280
|
|
|
444
|
-
it("lets an AUDIO clip
|
|
281
|
+
it("lets an AUDIO clip open a new audio track below the last row", () => {
|
|
445
282
|
expect(
|
|
446
283
|
resolveZoneDropPlacement({
|
|
447
284
|
...base,
|
|
@@ -450,69 +287,30 @@ describe("resolveZoneDropPlacement (the whole drop decision, no same-track overl
|
|
|
450
287
|
isAudio: true,
|
|
451
288
|
deliberateInsertRow: 4,
|
|
452
289
|
}),
|
|
453
|
-
).toEqual({ track: 3, insertRow: 4 });
|
|
290
|
+
).toEqual({ track: 3, insertRow: 4, start: 2 });
|
|
454
291
|
});
|
|
455
292
|
|
|
456
|
-
it("
|
|
457
|
-
|
|
458
|
-
// sole visual lane, which is occupied at the drop time. The zone-drop must
|
|
459
|
-
// create the audio zone's first lane (insertRow set) rather than stacking the
|
|
460
|
-
// audio clip onto the occupied visual track — the no-overlap invariant.
|
|
461
|
-
const result = resolveZoneDropPlacement({
|
|
293
|
+
it("audio dropped on a visual-only timeline opens its zone's first row, no overlap (#2195)", () => {
|
|
294
|
+
const drop = {
|
|
462
295
|
order: [0],
|
|
463
296
|
audioTracks: new Set<number>(),
|
|
464
297
|
elements: [el("v", 0, 0, 5)],
|
|
465
298
|
desiredTrack: 0,
|
|
466
299
|
deliberateInsertRow: null,
|
|
467
|
-
start: 1,
|
|
468
300
|
duration: 2,
|
|
469
301
|
dragKey: "audio",
|
|
470
302
|
isAudio: true,
|
|
303
|
+
};
|
|
304
|
+
// Occupied and free spans alike: the visual row is not a row an audio clip may land on.
|
|
305
|
+
expect(resolveZoneDropPlacement({ ...drop, start: 1 })).toEqual({
|
|
306
|
+
track: 0,
|
|
307
|
+
insertRow: 1,
|
|
308
|
+
start: 1,
|
|
471
309
|
});
|
|
472
|
-
expect(
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
it("free-span repro: audio dropped on a FREE stretch of a visual-only timeline → insert, not a visual lane (#2195)", () => {
|
|
477
|
-
// Same visual-only timeline, but the audio clip is aimed at a stretch the sole
|
|
478
|
-
// visual lane is FREE at. The zone check must still fire an insert (create the
|
|
479
|
-
// audio zone) rather than short-circuiting on isLaneFree and dropping the audio
|
|
480
|
-
// clip onto the visual lane — the kind-zone hole where the free-lane fast path
|
|
481
|
-
// beat the wrong-zone check.
|
|
482
|
-
const result = resolveZoneDropPlacement({
|
|
483
|
-
order: [0],
|
|
484
|
-
audioTracks: new Set<number>(),
|
|
485
|
-
elements: [el("v", 0, 0, 5)],
|
|
486
|
-
desiredTrack: 0,
|
|
487
|
-
deliberateInsertRow: null,
|
|
488
|
-
start: 10, // past the visual clip's end → the visual lane is FREE here
|
|
489
|
-
duration: 2,
|
|
490
|
-
dragKey: "audio",
|
|
491
|
-
isAudio: true,
|
|
310
|
+
expect(resolveZoneDropPlacement({ ...drop, start: 10 })).toEqual({
|
|
311
|
+
track: 0,
|
|
312
|
+
insertRow: 1,
|
|
313
|
+
start: 10,
|
|
492
314
|
});
|
|
493
|
-
expect(result.insertRow).not.toBeNull();
|
|
494
|
-
expect(result).toEqual({ track: 0, insertRow: 1 });
|
|
495
|
-
});
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
describe("resolveMainTrackDropStart (magnetic first clip on an empty main track)", () => {
|
|
499
|
-
it("snaps to 0 when landing on an empty main track", () => {
|
|
500
|
-
expect(resolveMainTrackDropStart([], 1, 0, false, 7)).toBe(0);
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
it("leaves the drop start alone once the main track already holds another clip", () => {
|
|
504
|
-
expect(resolveMainTrackDropStart([el("other", 0, 0, 3)], 1, 0, false, 7)).toBe(7);
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
it("leaves the drop start alone when landing off the main track", () => {
|
|
508
|
-
expect(resolveMainTrackDropStart([], 1, 2, false, 7)).toBe(7);
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
it("a clip already resident on the main track doing a horizontal move is a no-op", () => {
|
|
512
|
-
expect(resolveMainTrackDropStart([], 0, 0, false, 7)).toBe(7);
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
it("an audio clip landing on track 0 is not the main track (visual zone only)", () => {
|
|
516
|
-
expect(resolveMainTrackDropStart([], 1, 0, true, 7)).toBe(7);
|
|
517
315
|
});
|
|
518
316
|
});
|