@hyperframes/studio 0.7.90 → 0.7.93
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-RS9ROX_S.js → hyperframes-player-NSdcoOcK.js} +1 -1
- package/dist/assets/{index-CquRJgm-.js → index-CCGnc_0M.js} +1 -1
- package/dist/assets/index-D78KEjgB.css +1 -0
- package/dist/assets/index-Dp1zVb9X.js +428 -0
- package/dist/assets/{index-Bi-CwQDX.js → index-wS6f7F5q.js} +1 -1
- package/dist/index.d.ts +40 -36
- package/dist/index.html +2 -2
- package/dist/index.js +9598 -6798
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/src/App.tsx +14 -14
- package/src/components/DesignPanelPromoteProvider.tsx +1 -1
- package/src/components/ExternalFileConflictBanner.test.tsx +142 -0
- package/src/components/ExternalFileConflictBanner.tsx +255 -0
- package/src/components/StudioRightPanel.tsx +1 -1
- package/src/components/editor/GsapAnimationList.tsx +84 -0
- package/src/components/editor/GsapAnimationSection.test.tsx +100 -11
- package/src/components/editor/GsapAnimationSection.tsx +11 -41
- package/src/components/editor/PropertyPanel.tsx +1 -1
- package/src/components/editor/PropertyPanelFlat.tsx +29 -26
- package/src/components/editor/gsapAnimationCallbacks.ts +0 -10
- package/src/components/editor/propertyPanel3dTransform.tsx +1 -1
- package/src/components/editor/propertyPanelCommitField.tsx +37 -2
- package/src/components/editor/propertyPanelFlatDescriptors.ts +20 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +56 -55
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +9 -37
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +69 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -2
- package/src/components/editor/propertyPanelPrimitives.tsx +2 -2
- package/src/components/editor/propertyPanelTransformCommit.test.ts +57 -0
- package/src/components/editor/propertyPanelTransformCommit.ts +14 -14
- package/src/components/editor/propertyPanelTypes.ts +10 -4
- package/src/components/editor/useInspectorGestureTransaction.test.tsx +65 -0
- package/src/components/editor/useInspectorGestureTransaction.ts +66 -10
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +78 -4
- package/src/components/nle/useTimelineEditCallbacks.ts +12 -7
- package/src/components/renders/useRenderQueue.ts +16 -3
- package/src/components/renders/useRenderQueueTelemetry.test.tsx +104 -0
- package/src/components/sidebar/AssetCard.tsx +4 -3
- package/src/components/sidebar/AudioRow.tsx +4 -3
- package/src/contexts/DomEditContext.tsx +1 -1
- package/src/contexts/FileManagerContext.tsx +12 -0
- package/src/hooks/externalFileChangeOwnership.test.ts +17 -0
- package/src/hooks/externalFileReloadBus.test.ts +34 -0
- package/src/hooks/externalFileReloadBus.ts +18 -0
- package/src/hooks/gsapDragCommit.test.ts +39 -1
- package/src/hooks/gsapDragCommit.ts +12 -40
- package/src/hooks/gsapEditOutcome.ts +72 -0
- package/src/hooks/gsapResizeIntercept.test.ts +90 -2
- package/src/hooks/gsapResizeIntercept.ts +44 -16
- package/src/hooks/gsapRuntimeBridge.test.ts +195 -27
- package/src/hooks/gsapRuntimeBridge.ts +93 -28
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +194 -24
- package/src/hooks/useAnimatedPropertyCommit.ts +101 -49
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +215 -0
- package/src/hooks/useDomEditSession.test.tsx +1 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useDomGeometryCommits.test.tsx +61 -0
- package/src/hooks/useDomGeometryCommits.ts +20 -0
- package/src/hooks/useEditorSave.test.tsx +131 -0
- package/src/hooks/useEditorSave.ts +113 -39
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +221 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +429 -0
- package/src/hooks/useFileManager.projectOwnership.test.tsx +139 -7
- package/src/hooks/useFileManager.ts +32 -3
- package/src/hooks/useGsapAnimationFetchFallback.ts +45 -24
- package/src/hooks/useGsapAwareEditing.test.tsx +125 -39
- package/src/hooks/useGsapAwareEditing.ts +88 -27
- package/src/hooks/useGsapInteractionFailureTelemetry.test.tsx +42 -0
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +5 -1
- package/src/hooks/useGsapKeyframeOps.test.tsx +83 -5
- package/src/hooks/useGsapKeyframeOps.ts +20 -9
- package/src/hooks/usePreviewPersistence.test.ts +23 -0
- package/src/hooks/usePreviewPersistence.ts +22 -58
- package/src/hooks/useSdkSession.ts +8 -52
- package/src/hooks/useStudioExternalFileChanges.ts +81 -0
- package/src/hooks/useStudioSdkSessions.ts +2 -7
- package/src/hooks/useStudioSessionStart.ts +15 -0
- package/src/hooks/useStudioTestHooks.test.tsx +11 -7
- package/src/player/components/BeatStrip.test.tsx +374 -0
- package/src/player/components/BeatStrip.tsx +295 -37
- package/src/player/components/KeyframeDiamondContextMenu.test.tsx +16 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +6 -5
- package/src/player/components/LayerDisclosureRow.tsx +2 -0
- package/src/player/components/Timeline.test.ts +26 -8
- package/src/player/components/Timeline.tsx +87 -88
- package/src/player/components/Timeline.virtualization.test.tsx +54 -11
- package/src/player/components/TimelineCanvas.tsx +14 -71
- package/src/player/components/TimelineClip.test.tsx +17 -2
- package/src/player/components/TimelineClip.tsx +20 -5
- package/src/player/components/TimelineClipDiamonds.test.tsx +536 -27
- package/src/player/components/TimelineClipDiamonds.tsx +118 -223
- package/src/player/components/TimelineCompactDiamonds.tsx +84 -0
- package/src/player/components/TimelineDiamondConnectors.tsx +31 -3
- package/src/player/components/TimelineGestureOverlay.test.tsx +90 -0
- package/src/player/components/TimelineGestureOverlay.tsx +96 -0
- package/src/player/components/TimelineLanes.test.tsx +64 -7
- package/src/player/components/TimelineLanes.tsx +84 -66
- package/src/player/components/TimelineOverlays.test.ts +161 -0
- package/src/player/components/TimelineOverlays.tsx +104 -11
- package/src/player/components/TimelinePropertyLanes.test.tsx +9 -5
- package/src/player/components/TimelinePropertyLanes.tsx +9 -2
- package/src/player/components/TimelineTrackHeader.test.tsx +1 -0
- package/src/player/components/TimelineTrackHeader.tsx +16 -0
- package/src/player/components/TimelineTrackRow.tsx +58 -7
- package/src/player/components/timelineCallbacks.ts +1 -1
- package/src/player/components/timelineClipDragGestureLifecycle.test.ts +18 -3
- package/src/player/components/timelineClipDragGestureLifecycle.ts +199 -51
- package/src/player/components/timelineClipDragPreview.test.ts +33 -0
- package/src/player/components/timelineClipDragPreview.ts +18 -12
- package/src/player/components/timelineClipDragTypes.ts +12 -0
- package/src/player/components/timelineDiamondTypes.ts +18 -21
- package/src/player/components/timelineDragDrop.test.tsx +206 -0
- package/src/player/components/timelineDragDrop.ts +152 -30
- package/src/player/components/timelineKeyboardNavigation.test.ts +303 -0
- package/src/player/components/timelineKeyboardNavigation.ts +337 -0
- package/src/player/components/timelineLaneProps.ts +3 -1
- package/src/player/components/timelineMarquee.test.ts +73 -25
- package/src/player/components/timelineMarquee.ts +44 -16
- package/src/player/components/timelineNavigationIdentity.ts +38 -0
- package/src/player/components/timelineTestViewport.ts +13 -0
- package/src/player/components/timelineViewModel.ts +4 -4
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +219 -17
- package/src/player/components/useTimelineClipDrag.ts +112 -70
- package/src/player/components/useTimelineClipRenderWindow.ts +8 -39
- package/src/player/components/useTimelineFocusCoordinator.test.tsx +174 -0
- package/src/player/components/useTimelineFocusCoordinator.ts +221 -0
- package/src/player/components/useTimelineKeyboardActor.test.tsx +267 -0
- package/src/player/components/useTimelineKeyboardActor.ts +147 -0
- package/src/player/components/useTimelineKeyframeHandlers.test.tsx +43 -7
- package/src/player/components/useTimelineKeyframeHandlers.ts +465 -2
- package/src/player/components/useTimelineLaneMoveRefresh.ts +7 -0
- package/src/player/components/useTimelineLogicalFocus.ts +80 -0
- package/src/player/components/useTimelineLogicalRows.test.tsx +57 -0
- package/src/player/components/useTimelineLogicalRows.ts +47 -0
- package/src/player/components/useTimelineRangeSelection.test.tsx +204 -0
- package/src/player/components/useTimelineRangeSelection.ts +202 -69
- package/src/player/components/useTimelineRangeSelectionScrub.test.tsx +3 -2
- package/src/player/components/useTimelineRowVirtualization.ts +26 -29
- package/src/player/components/useTrackGapMenu.test.tsx +64 -0
- package/src/player/components/useTrackGapMenu.ts +32 -9
- package/src/player/store/keyframeSlice.ts +59 -18
- package/src/player/store/liveTime.ts +11 -0
- package/src/player/store/playerStore.test.ts +106 -23
- package/src/player/store/playerStore.ts +43 -40
- package/src/player/store/timelineFocusState.ts +15 -0
- package/src/telemetry/canary.test.ts +325 -0
- package/src/telemetry/canary.ts +268 -0
- package/src/telemetry/client.test.ts +8 -3
- package/src/telemetry/client.ts +13 -43
- package/src/telemetry/config.ts +16 -2
- package/src/telemetry/distinctId.test.ts +35 -0
- package/src/telemetry/distinctId.ts +11 -2
- package/src/telemetry/policy.test.ts +81 -0
- package/src/telemetry/policy.ts +104 -0
- package/src/utils/domEditSaveQueue.test.ts +37 -1
- package/src/utils/domEditSaveQueue.ts +22 -4
- package/src/utils/externalConflictStorage.test.ts +95 -0
- package/src/utils/externalConflictStorage.ts +172 -0
- package/src/utils/studioFileVersion.test.ts +28 -1
- package/src/utils/studioFileVersion.ts +30 -0
- package/src/utils/studioPendingEdits.test.ts +108 -1
- package/src/utils/studioPendingEdits.ts +39 -2
- package/src/utils/studioSaveDiagnostics.ts +5 -0
- package/src/utils/studioTelemetry.test.ts +76 -0
- package/src/utils/studioTelemetry.ts +14 -6
- package/dist/assets/index-BXkzp_OU.css +0 -1
- package/dist/assets/index-Cql15Yur.js +0 -428
- package/src/player/components/useTimelineRevealClip.test.tsx +0 -139
- package/src/player/components/useTimelineRevealClip.ts +0 -180
|
@@ -5,8 +5,11 @@ import {
|
|
|
5
5
|
isTimelineRulerPress,
|
|
6
6
|
getMarqueeRect,
|
|
7
7
|
getTimelineClipRect,
|
|
8
|
+
getMarqueeClipCandidates,
|
|
8
9
|
computeMarqueeSelection,
|
|
9
10
|
} from "./timelineMarquee";
|
|
11
|
+
import { createTimelineClipIndex } from "../lib/timelineClipIndex";
|
|
12
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
10
13
|
import {
|
|
11
14
|
GUTTER,
|
|
12
15
|
LANE_H,
|
|
@@ -14,6 +17,7 @@ import {
|
|
|
14
17
|
RULER_H,
|
|
15
18
|
CLIP_Y,
|
|
16
19
|
TRACKS_LEFT_PAD,
|
|
20
|
+
createTimelineRowGeometry,
|
|
17
21
|
getTimelineRowTop,
|
|
18
22
|
} from "./timelineLayout";
|
|
19
23
|
|
|
@@ -95,9 +99,13 @@ describe("getMarqueeRect", () => {
|
|
|
95
99
|
|
|
96
100
|
describe("getTimelineClipRect", () => {
|
|
97
101
|
const trackOrder = [0, 2, 5];
|
|
102
|
+
const geometry = createTimelineRowGeometry(
|
|
103
|
+
trackOrder,
|
|
104
|
+
trackOrder.map(() => TRACK_H),
|
|
105
|
+
);
|
|
98
106
|
|
|
99
107
|
it("maps start/duration to x via pps and the track row to y via the shared row→y helper", () => {
|
|
100
|
-
const rect = getTimelineClipRect({ start: 2, duration: 3, track: 2 },
|
|
108
|
+
const rect = getTimelineClipRect({ start: 2, duration: 3, track: 2 }, geometry, 100, GUTTER);
|
|
101
109
|
expect(rect).toEqual({
|
|
102
110
|
left: GUTTER + 200,
|
|
103
111
|
top: getTimelineRowTop(1) + CLIP_Y,
|
|
@@ -107,54 +115,48 @@ describe("getTimelineClipRect", () => {
|
|
|
107
115
|
});
|
|
108
116
|
|
|
109
117
|
it("places the first visible track below the ruler + top breathing pad", () => {
|
|
110
|
-
const rect = getTimelineClipRect({ start: 0, duration: 1, track: 0 },
|
|
118
|
+
const rect = getTimelineClipRect({ start: 0, duration: 1, track: 0 }, geometry, 50, GUTTER);
|
|
111
119
|
expect(rect?.top).toBe(getTimelineRowTop(0) + CLIP_Y);
|
|
112
120
|
expect(rect?.left).toBe(GUTTER);
|
|
113
121
|
});
|
|
114
122
|
|
|
115
123
|
it("uses the row index in trackOrder, not the raw track number", () => {
|
|
116
|
-
const rect = getTimelineClipRect({ start: 0, duration: 1, track: 5 },
|
|
124
|
+
const rect = getTimelineClipRect({ start: 0, duration: 1, track: 5 }, geometry, 50, GUTTER);
|
|
117
125
|
expect(rect?.top).toBe(getTimelineRowTop(2) + CLIP_Y);
|
|
118
126
|
});
|
|
119
127
|
|
|
120
128
|
it("uses cumulative tops and the resolved height for an expanded row", () => {
|
|
121
129
|
const rowHeights = [TRACK_H + 2 * LANE_H, TRACK_H, TRACK_H];
|
|
130
|
+
const expandedGeometry = createTimelineRowGeometry(trackOrder, rowHeights);
|
|
122
131
|
const rect = getTimelineClipRect(
|
|
123
132
|
{ start: 0, duration: 1, track: 0 },
|
|
124
|
-
|
|
133
|
+
expandedGeometry,
|
|
125
134
|
50,
|
|
126
135
|
GUTTER,
|
|
127
|
-
rowHeights,
|
|
128
136
|
);
|
|
129
137
|
expect(rect).toMatchObject({
|
|
130
138
|
top: getTimelineRowTop(0, rowHeights) + CLIP_Y,
|
|
131
|
-
height:
|
|
139
|
+
height: TRACK_H - CLIP_Y * 2,
|
|
132
140
|
});
|
|
133
141
|
expect(
|
|
134
|
-
getTimelineClipRect({ start: 0, duration: 1, track: 2 },
|
|
135
|
-
?.top,
|
|
142
|
+
getTimelineClipRect({ start: 0, duration: 1, track: 2 }, expandedGeometry, 50, GUTTER)?.top,
|
|
136
143
|
).toBe(getTimelineRowTop(1, rowHeights) + CLIP_Y);
|
|
137
144
|
});
|
|
138
145
|
|
|
139
146
|
it("enforces the 4px minimum rendered width", () => {
|
|
140
|
-
const rect = getTimelineClipRect(
|
|
141
|
-
{ start: 0, duration: 0.01, track: 0 },
|
|
142
|
-
trackOrder,
|
|
143
|
-
10,
|
|
144
|
-
GUTTER,
|
|
145
|
-
);
|
|
147
|
+
const rect = getTimelineClipRect({ start: 0, duration: 0.01, track: 0 }, geometry, 10, GUTTER);
|
|
146
148
|
expect(rect?.width).toBe(4);
|
|
147
149
|
});
|
|
148
150
|
|
|
149
151
|
it("returns null for a track that is not displayed or an invalid pps", () => {
|
|
150
152
|
expect(
|
|
151
|
-
getTimelineClipRect({ start: 0, duration: 1, track: 9 },
|
|
153
|
+
getTimelineClipRect({ start: 0, duration: 1, track: 9 }, geometry, 100, GUTTER),
|
|
152
154
|
).toBeNull();
|
|
153
155
|
expect(
|
|
154
|
-
getTimelineClipRect({ start: 0, duration: 1, track: 0 },
|
|
156
|
+
getTimelineClipRect({ start: 0, duration: 1, track: 0 }, geometry, 0, GUTTER),
|
|
155
157
|
).toBeNull();
|
|
156
158
|
expect(
|
|
157
|
-
getTimelineClipRect({ start: 0, duration: 1, track: 0 },
|
|
159
|
+
getTimelineClipRect({ start: 0, duration: 1, track: 0 }, geometry, NaN, GUTTER),
|
|
158
160
|
).toBeNull();
|
|
159
161
|
});
|
|
160
162
|
});
|
|
@@ -162,6 +164,10 @@ describe("getTimelineClipRect", () => {
|
|
|
162
164
|
describe("computeMarqueeSelection", () => {
|
|
163
165
|
// Two visible tracks: row 0 = track 0, row 1 = track 1. pps 100.
|
|
164
166
|
const trackOrder = [0, 1];
|
|
167
|
+
const rowGeometry = createTimelineRowGeometry(
|
|
168
|
+
trackOrder,
|
|
169
|
+
trackOrder.map(() => TRACK_H),
|
|
170
|
+
);
|
|
165
171
|
const pps = 100;
|
|
166
172
|
const clips = [
|
|
167
173
|
{ id: "a", start: 0, duration: 1, track: 0 }, // x [32,132], row 0
|
|
@@ -175,7 +181,7 @@ describe("computeMarqueeSelection", () => {
|
|
|
175
181
|
const marquee = { left: ORIGIN, top: row0Top, width: 50, height: 10 };
|
|
176
182
|
const { ids, primaryId } = computeMarqueeSelection({
|
|
177
183
|
clips,
|
|
178
|
-
|
|
184
|
+
rowGeometry,
|
|
179
185
|
pps,
|
|
180
186
|
contentOrigin: ORIGIN,
|
|
181
187
|
marquee,
|
|
@@ -188,7 +194,7 @@ describe("computeMarqueeSelection", () => {
|
|
|
188
194
|
const marquee = { left: ORIGIN, top: row0Top, width: 60, height: row1Top - row0Top + 5 };
|
|
189
195
|
const { ids } = computeMarqueeSelection({
|
|
190
196
|
clips,
|
|
191
|
-
|
|
197
|
+
rowGeometry,
|
|
192
198
|
pps,
|
|
193
199
|
contentOrigin: ORIGIN,
|
|
194
200
|
marquee,
|
|
@@ -200,7 +206,7 @@ describe("computeMarqueeSelection", () => {
|
|
|
200
206
|
const marquee = { left: ORIGIN + 140, top: row0Top, width: 50, height: 10 };
|
|
201
207
|
const { ids } = computeMarqueeSelection({
|
|
202
208
|
clips,
|
|
203
|
-
|
|
209
|
+
rowGeometry,
|
|
204
210
|
pps,
|
|
205
211
|
contentOrigin: ORIGIN,
|
|
206
212
|
marquee,
|
|
@@ -212,7 +218,7 @@ describe("computeMarqueeSelection", () => {
|
|
|
212
218
|
const marquee = { left: GUTTER + 140, top: row0Top, width: 50, height: 10 };
|
|
213
219
|
const { ids, primaryId } = computeMarqueeSelection({
|
|
214
220
|
clips,
|
|
215
|
-
|
|
221
|
+
rowGeometry,
|
|
216
222
|
pps,
|
|
217
223
|
contentOrigin: GUTTER,
|
|
218
224
|
marquee,
|
|
@@ -226,7 +232,7 @@ describe("computeMarqueeSelection", () => {
|
|
|
226
232
|
const marquee = { left: GUTTER, top: row1Top, width: 100, height: 10 };
|
|
227
233
|
const { ids, primaryId } = computeMarqueeSelection({
|
|
228
234
|
clips,
|
|
229
|
-
|
|
235
|
+
rowGeometry,
|
|
230
236
|
pps,
|
|
231
237
|
contentOrigin: GUTTER,
|
|
232
238
|
marquee,
|
|
@@ -240,10 +246,11 @@ describe("computeMarqueeSelection", () => {
|
|
|
240
246
|
const wide = { left: ORIGIN, top: row0Top, width: 320, height: 10 };
|
|
241
247
|
const narrow = { left: ORIGIN, top: row0Top, width: 80, height: 10 };
|
|
242
248
|
expect(
|
|
243
|
-
computeMarqueeSelection({ clips,
|
|
249
|
+
computeMarqueeSelection({ clips, rowGeometry, pps, contentOrigin: ORIGIN, marquee: wide })
|
|
250
|
+
.ids,
|
|
244
251
|
).toEqual(new Set(["a", "b"]));
|
|
245
252
|
expect(
|
|
246
|
-
computeMarqueeSelection({ clips,
|
|
253
|
+
computeMarqueeSelection({ clips, rowGeometry, pps, contentOrigin: ORIGIN, marquee: narrow })
|
|
247
254
|
.ids,
|
|
248
255
|
).toEqual(new Set(["a"]));
|
|
249
256
|
});
|
|
@@ -252,7 +259,7 @@ describe("computeMarqueeSelection", () => {
|
|
|
252
259
|
const marquee = { left: 0, top: 0, width: 10000, height: 10000 };
|
|
253
260
|
const { ids } = computeMarqueeSelection({
|
|
254
261
|
clips: [{ id: "x", start: 0, duration: 1, track: 7 }],
|
|
255
|
-
|
|
262
|
+
rowGeometry,
|
|
256
263
|
pps,
|
|
257
264
|
contentOrigin: GUTTER,
|
|
258
265
|
marquee,
|
|
@@ -260,3 +267,44 @@ describe("computeMarqueeSelection", () => {
|
|
|
260
267
|
expect(ids).toEqual(new Set());
|
|
261
268
|
});
|
|
262
269
|
});
|
|
270
|
+
|
|
271
|
+
describe("getMarqueeClipCandidates", () => {
|
|
272
|
+
it("queries only the intersecting rows and time span", () => {
|
|
273
|
+
const rowGeometry = createTimelineRowGeometry([0, 1, 2], [TRACK_H, TRACK_H, TRACK_H]);
|
|
274
|
+
const near: TimelineElement = { id: "near", tag: "div", start: 1, duration: 1, track: 1 };
|
|
275
|
+
const wrongTime: TimelineElement = {
|
|
276
|
+
id: "wrong-time",
|
|
277
|
+
tag: "div",
|
|
278
|
+
start: 20,
|
|
279
|
+
duration: 1,
|
|
280
|
+
track: 1,
|
|
281
|
+
};
|
|
282
|
+
const wrongRow: TimelineElement = {
|
|
283
|
+
id: "wrong-row",
|
|
284
|
+
tag: "div",
|
|
285
|
+
start: 1,
|
|
286
|
+
duration: 1,
|
|
287
|
+
track: 2,
|
|
288
|
+
};
|
|
289
|
+
const clipIndex = createTimelineClipIndex([
|
|
290
|
+
[0, []],
|
|
291
|
+
[1, [near, wrongTime]],
|
|
292
|
+
[2, [wrongRow]],
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
expect(
|
|
296
|
+
getMarqueeClipCandidates({
|
|
297
|
+
clipIndex,
|
|
298
|
+
rowGeometry,
|
|
299
|
+
marquee: {
|
|
300
|
+
left: ORIGIN + 100,
|
|
301
|
+
top: getTimelineRowTop(1),
|
|
302
|
+
width: 100,
|
|
303
|
+
height: TRACK_H - 1,
|
|
304
|
+
},
|
|
305
|
+
pps: 100,
|
|
306
|
+
contentOrigin: ORIGIN,
|
|
307
|
+
}),
|
|
308
|
+
).toEqual([near]);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { RULER_H, CLIP_Y,
|
|
1
|
+
import { RULER_H, CLIP_Y, TRACK_H, type TimelineRowGeometry } from "./timelineLayout";
|
|
2
2
|
import { rectsOverlap, type Rect } from "../../utils/marqueeGeometry";
|
|
3
|
+
import { queryTimelineClipIndex, type TimelineClipIndex } from "../lib/timelineClipIndex";
|
|
4
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
3
5
|
|
|
4
6
|
/** Pointer must travel at least this far (either axis) before a pointerdown on
|
|
5
7
|
* the empty timeline body becomes a marquee drag instead of a plain click. */
|
|
@@ -62,23 +64,22 @@ export function getMarqueeRect(
|
|
|
62
64
|
/**
|
|
63
65
|
* A clip's rendered rect in canvas/content coordinates (the same space the
|
|
64
66
|
* marquee rect lives in): x from the shared content origin + start * pps, y from the clip's row
|
|
65
|
-
* index within the
|
|
67
|
+
* index within the canonical row geometry (cumulative row top + CLIP_Y).
|
|
66
68
|
* Returns null when the clip's track is not currently displayed.
|
|
67
69
|
*/
|
|
68
70
|
export function getTimelineClipRect(
|
|
69
71
|
clip: Pick<MarqueeClipInput, "start" | "duration" | "track">,
|
|
70
|
-
|
|
72
|
+
rowGeometry: TimelineRowGeometry,
|
|
71
73
|
pps: number,
|
|
72
74
|
contentOrigin: number,
|
|
73
|
-
rowHeights: readonly number[] = [],
|
|
74
75
|
): Rect | null {
|
|
75
|
-
const row =
|
|
76
|
+
const row = rowGeometry.getRowIndex(clip.track);
|
|
76
77
|
if (row < 0 || !Number.isFinite(pps) || pps <= 0) return null;
|
|
77
78
|
return {
|
|
78
79
|
left: contentOrigin + clip.start * pps,
|
|
79
|
-
top:
|
|
80
|
+
top: rowGeometry.getRowTop(row) + CLIP_Y,
|
|
80
81
|
width: Math.max(clip.duration * pps, MIN_CLIP_W),
|
|
81
|
-
height:
|
|
82
|
+
height: TRACK_H - CLIP_Y * 2,
|
|
82
83
|
};
|
|
83
84
|
}
|
|
84
85
|
|
|
@@ -90,29 +91,56 @@ export interface MarqueeSelectionResult {
|
|
|
90
91
|
primaryId: string | null;
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
/** Narrow a marquee hit test to the intersecting logical rows and time span. */
|
|
95
|
+
export function getMarqueeClipCandidates(input: {
|
|
96
|
+
clipIndex: TimelineClipIndex;
|
|
97
|
+
rowGeometry: TimelineRowGeometry;
|
|
98
|
+
marquee: Rect;
|
|
99
|
+
pps: number;
|
|
100
|
+
contentOrigin: number;
|
|
101
|
+
}): readonly TimelineElement[] {
|
|
102
|
+
if (!(input.pps > 0) || input.marquee.width <= 0 || input.marquee.height <= 0) return [];
|
|
103
|
+
const lastRow = input.rowGeometry.rowKeys.length - 1;
|
|
104
|
+
const first = Math.max(0, Math.floor(input.rowGeometry.getRowFromY(input.marquee.top)));
|
|
105
|
+
const last = Math.min(
|
|
106
|
+
lastRow,
|
|
107
|
+
Math.floor(input.rowGeometry.getRowFromY(input.marquee.top + input.marquee.height)),
|
|
108
|
+
);
|
|
109
|
+
if (first > last) return [];
|
|
110
|
+
const paddingSeconds = MIN_CLIP_W / input.pps;
|
|
111
|
+
const start = Math.max(
|
|
112
|
+
0,
|
|
113
|
+
(input.marquee.left - input.contentOrigin) / input.pps - paddingSeconds,
|
|
114
|
+
);
|
|
115
|
+
const end =
|
|
116
|
+
(input.marquee.left + input.marquee.width - input.contentOrigin) / input.pps + paddingSeconds;
|
|
117
|
+
if (end <= start) return [];
|
|
118
|
+
|
|
119
|
+
const candidates: TimelineElement[] = [];
|
|
120
|
+
for (let row = first; row <= last; row += 1) {
|
|
121
|
+
const rowKey = input.rowGeometry.rowKeys[row];
|
|
122
|
+
if (rowKey === undefined) continue;
|
|
123
|
+
candidates.push(...queryTimelineClipIndex(input.clipIndex, rowKey, { start, end }));
|
|
124
|
+
}
|
|
125
|
+
return candidates;
|
|
126
|
+
}
|
|
127
|
+
|
|
93
128
|
/**
|
|
94
129
|
* Live marquee selection: every clip whose rendered rect intersects the marquee.
|
|
95
130
|
* `baseSelection` (shift/cmd-additive) is unioned in but never affects primaryId.
|
|
96
131
|
*/
|
|
97
132
|
export function computeMarqueeSelection(input: {
|
|
98
133
|
clips: MarqueeClipInput[];
|
|
99
|
-
|
|
134
|
+
rowGeometry: TimelineRowGeometry;
|
|
100
135
|
pps: number;
|
|
101
136
|
contentOrigin: number;
|
|
102
137
|
marquee: Rect;
|
|
103
138
|
baseSelection?: Iterable<string>;
|
|
104
|
-
rowHeights?: readonly number[];
|
|
105
139
|
}): MarqueeSelectionResult {
|
|
106
140
|
const ids = new Set<string>(input.baseSelection ?? []);
|
|
107
141
|
let primaryId: string | null = null;
|
|
108
142
|
for (const clip of input.clips) {
|
|
109
|
-
const rect = getTimelineClipRect(
|
|
110
|
-
clip,
|
|
111
|
-
input.trackOrder,
|
|
112
|
-
input.pps,
|
|
113
|
-
input.contentOrigin,
|
|
114
|
-
input.rowHeights,
|
|
115
|
-
);
|
|
143
|
+
const rect = getTimelineClipRect(clip, input.rowGeometry, input.pps, input.contentOrigin);
|
|
116
144
|
if (rect && rectsOverlap(rect, input.marquee)) {
|
|
117
145
|
ids.add(clip.id);
|
|
118
146
|
primaryId = clip.id;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PropertyGroupName } from "@hyperframes/core/gsap-parser";
|
|
2
|
+
import {
|
|
3
|
+
timelineKeyframeSelectionKey,
|
|
4
|
+
type TimelineKeyframeTarget,
|
|
5
|
+
} from "./timelineKeyframeIdentity";
|
|
6
|
+
|
|
7
|
+
function stableId(kind: string, ...parts: Array<string | number>): string {
|
|
8
|
+
// Attribute-safe by construction; callers embedding it in CSS selectors must use CSS.escape.
|
|
9
|
+
return JSON.stringify(["timeline", kind, ...parts]);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function timelineTrackRowId(track: number): string {
|
|
13
|
+
return stableId("track", track);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function timelinePropertyRowId(elementId: string, group: PropertyGroupName): string {
|
|
17
|
+
return stableId("property", elementId, group);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function timelineLogicalRowCellId(
|
|
21
|
+
lanesId: string,
|
|
22
|
+
rowId: string,
|
|
23
|
+
cell: "header" | "content",
|
|
24
|
+
): string {
|
|
25
|
+
return `${lanesId}-${stableId("cell", rowId, cell)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function timelineClipFocusId(elementId: string): string {
|
|
29
|
+
return stableId("clip", elementId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function timelineKeyframeFocusId(elementId: string, target: TimelineKeyframeTarget): string {
|
|
33
|
+
return stableId("keyframe", timelineKeyframeSelectionKey(elementId, target));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function timelineEaseFocusId(elementId: string, target: TimelineKeyframeTarget): string {
|
|
37
|
+
return stableId("ease", timelineKeyframeSelectionKey(elementId, target));
|
|
38
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Configure the shared viewport geometry used by timeline gesture hook tests. */
|
|
2
|
+
export function configureTimelineTestViewport(scroll: HTMLElement, scrollHeight: number): void {
|
|
3
|
+
scroll.getBoundingClientRect = () =>
|
|
4
|
+
({ left: 0, top: 0, right: 800, bottom: 240, width: 800, height: 240 }) as DOMRect;
|
|
5
|
+
Object.defineProperties(scroll, {
|
|
6
|
+
scrollLeft: { configurable: true, writable: true, value: 0 },
|
|
7
|
+
scrollTop: { configurable: true, writable: true, value: 0 },
|
|
8
|
+
scrollWidth: { configurable: true, value: 10_000 },
|
|
9
|
+
scrollHeight: { configurable: true, value: scrollHeight },
|
|
10
|
+
clientWidth: { configurable: true, value: 800 },
|
|
11
|
+
clientHeight: { configurable: true, value: 240 },
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -29,10 +29,10 @@ export function getTimelinePreviewElement(
|
|
|
29
29
|
element: TimelineElement,
|
|
30
30
|
resizingClip: ResizingClipState | null,
|
|
31
31
|
): TimelineElement {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
) {
|
|
32
|
+
const elementIdentity = getTimelineElementIdentity(element);
|
|
33
|
+
const groupPreview = resizingClip?.groupPreview?.find((change) => change.key === elementIdentity);
|
|
34
|
+
if (groupPreview) return { ...element, ...groupPreview };
|
|
35
|
+
if (resizingClip && getTimelineElementIdentity(resizingClip.element) === elementIdentity) {
|
|
36
36
|
return {
|
|
37
37
|
...element,
|
|
38
38
|
start: resizingClip.previewStart,
|