@hyperframes/studio 0.7.90 → 0.7.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-RS9ROX_S.js → hyperframes-player-9qOa08Tv.js} +1 -1
- package/dist/assets/{index-Bi-CwQDX.js → index-Bw4lNdtO.js} +1 -1
- package/dist/assets/{index-CquRJgm-.js → index-CDTN2ZVa.js} +1 -1
- package/dist/assets/index-DIntkrQl.js +428 -0
- package/dist/index.d.ts +20 -16
- package/dist/index.html +1 -1
- package/dist/index.js +6495 -5429
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- 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/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/propertyPanelTypes.ts +1 -1
- 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/contexts/DomEditContext.tsx +1 -1
- package/src/hooks/useDomEditSession.test.tsx +1 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useGsapKeyframeOps.test.tsx +83 -5
- package/src/hooks/useGsapKeyframeOps.ts +20 -9
- 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/Timeline.test.ts +2 -3
- package/src/player/components/Timeline.tsx +47 -48
- package/src/player/components/TimelineCanvas.tsx +14 -71
- package/src/player/components/TimelineClip.tsx +6 -3
- package/src/player/components/TimelineClipDiamonds.test.tsx +442 -18
- package/src/player/components/TimelineClipDiamonds.tsx +66 -211
- package/src/player/components/TimelineGestureOverlay.test.tsx +90 -0
- package/src/player/components/TimelineGestureOverlay.tsx +96 -0
- package/src/player/components/TimelineLanes.test.tsx +0 -1
- package/src/player/components/TimelineLanes.tsx +3 -2
- package/src/player/components/TimelineOverlays.test.ts +161 -0
- package/src/player/components/TimelineOverlays.tsx +104 -11
- 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 +5 -15
- package/src/player/components/timelineDragDrop.test.tsx +206 -0
- package/src/player/components/timelineDragDrop.ts +152 -30
- package/src/player/components/timelineLaneProps.ts +0 -1
- package/src/player/components/timelineMarquee.test.ts +73 -25
- package/src/player/components/timelineMarquee.ts +44 -16
- 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 +4 -4
- 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/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 +11 -4
- 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/playerStore.test.ts +76 -0
- package/src/player/store/playerStore.ts +19 -11
- 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/studioTelemetry.test.ts +76 -0
- package/src/utils/studioTelemetry.ts +14 -6
- package/dist/assets/index-Cql15Yur.js +0 -428
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef, useState, useCallback, useMemo } from "react";
|
|
1
|
+
import { useRef, useState, useCallback, useMemo, useEffect } from "react";
|
|
2
2
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
3
3
|
import {
|
|
4
4
|
applyTimelineAutoScrollStep,
|
|
@@ -25,9 +25,13 @@ import type {
|
|
|
25
25
|
ResizingClipState,
|
|
26
26
|
BlockedClipState,
|
|
27
27
|
} from "./timelineClipDragTypes";
|
|
28
|
-
import { mountTimelineClipDragGestureLifecycle } from "./timelineClipDragGestureLifecycle";
|
|
29
28
|
import { getTimelineElementIndexes } from "../lib/timelineElementIndexes";
|
|
30
29
|
import type { TimelineRowGeometry } from "./timelineLayout";
|
|
30
|
+
import {
|
|
31
|
+
mountTimelineClipDragGestureLifecycle,
|
|
32
|
+
type TimelineGestureKind,
|
|
33
|
+
type TimelineGestureLifecycle,
|
|
34
|
+
} from "./timelineClipDragGestureLifecycle";
|
|
31
35
|
|
|
32
36
|
export type {
|
|
33
37
|
DraggedClipState,
|
|
@@ -74,6 +78,7 @@ interface UseTimelineClipDragInput {
|
|
|
74
78
|
readZIndex?: (element: TimelineElement) => number;
|
|
75
79
|
onStackingPatches?: (patches: StackingPatch[]) => Promise<unknown> | void;
|
|
76
80
|
refreshAfterLaneMove?: () => void;
|
|
81
|
+
sessionEpoch?: number;
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
export function useTimelineClipDrag({
|
|
@@ -92,6 +97,7 @@ export function useTimelineClipDrag({
|
|
|
92
97
|
readZIndex,
|
|
93
98
|
onStackingPatches,
|
|
94
99
|
refreshAfterLaneMove,
|
|
100
|
+
sessionEpoch = 0,
|
|
95
101
|
}: UseTimelineClipDragInput) {
|
|
96
102
|
const updateElement = usePlayerStore((s) => s.updateElement);
|
|
97
103
|
const rawBeatTimes = usePlayerStore((s) => s.beatAnalysis?.beatTimes ?? EMPTY_BEAT_TIMES);
|
|
@@ -162,41 +168,72 @@ export function useTimelineClipDrag({
|
|
|
162
168
|
[],
|
|
163
169
|
);
|
|
164
170
|
|
|
165
|
-
const [draggedClip,
|
|
171
|
+
const [draggedClip, setDraggedClipState] = useState<DraggedClipState | null>(null);
|
|
166
172
|
const draggedClipRef = useRef<DraggedClipState | null>(null);
|
|
167
|
-
|
|
173
|
+
const publishDraggedClip = useCallback((next: DraggedClipState | null) => {
|
|
174
|
+
draggedClipRef.current = next;
|
|
175
|
+
setDraggedClipState(next);
|
|
176
|
+
}, []);
|
|
168
177
|
|
|
169
|
-
const [resizingClip,
|
|
178
|
+
const [resizingClip, setResizingClipState] = useState<ResizingClipState | null>(null);
|
|
170
179
|
const resizingClipRef = useRef<ResizingClipState | null>(null);
|
|
171
|
-
|
|
180
|
+
const publishResizingClip = useCallback((next: ResizingClipState | null) => {
|
|
181
|
+
resizingClipRef.current = next;
|
|
182
|
+
setResizingClipState(next);
|
|
183
|
+
}, []);
|
|
184
|
+
|
|
185
|
+
const lifecycleRef = useRef<TimelineGestureLifecycle>({
|
|
186
|
+
kind: null,
|
|
187
|
+
phase: "complete",
|
|
188
|
+
pointerId: null,
|
|
189
|
+
sessionEpoch,
|
|
190
|
+
});
|
|
191
|
+
const sessionEpochRef = useRef(sessionEpoch);
|
|
192
|
+
sessionEpochRef.current = sessionEpoch;
|
|
193
|
+
const gestureSelectedKeysRef = useRef<ReadonlySet<string>>(new Set());
|
|
194
|
+
const cancelGestureRef = useRef<
|
|
195
|
+
(options?: { updateReact?: boolean; suppressClick?: boolean }) => boolean
|
|
196
|
+
>(() => false);
|
|
197
|
+
const beginGesture = useCallback((kind: TimelineGestureKind, pointerId: number) => {
|
|
198
|
+
if (lifecycleRef.current.phase === "active") cancelGestureRef.current();
|
|
199
|
+
lifecycleRef.current = {
|
|
200
|
+
kind,
|
|
201
|
+
phase: "active",
|
|
202
|
+
pointerId,
|
|
203
|
+
sessionEpoch: sessionEpochRef.current,
|
|
204
|
+
};
|
|
205
|
+
gestureSelectedKeysRef.current = new Set(usePlayerStore.getState().selectedElementIds);
|
|
206
|
+
}, []);
|
|
207
|
+
const setDraggedClip = useCallback(
|
|
208
|
+
(next: DraggedClipState | null) => {
|
|
209
|
+
if (!next) {
|
|
210
|
+
cancelGestureRef.current();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
beginGesture("drag", next.pointerId);
|
|
214
|
+
publishDraggedClip(next);
|
|
215
|
+
},
|
|
216
|
+
[beginGesture, publishDraggedClip],
|
|
217
|
+
);
|
|
218
|
+
const setResizingClip = useCallback(
|
|
219
|
+
(next: ResizingClipState | null) => {
|
|
220
|
+
if (!next) {
|
|
221
|
+
cancelGestureRef.current();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
beginGesture("resize", next.pointerId);
|
|
225
|
+
publishResizingClip(next);
|
|
226
|
+
},
|
|
227
|
+
[beginGesture, publishResizingClip],
|
|
228
|
+
);
|
|
172
229
|
|
|
173
230
|
const blockedClipRef = useRef<BlockedClipState | null>(null);
|
|
174
231
|
const suppressClickRef = useRef(false);
|
|
175
232
|
|
|
176
|
-
// Active multi-select group-resize session
|
|
177
|
-
//
|
|
178
|
-
// capability-clean multi-selection (null ⇒ single-clip resize). Holds the
|
|
179
|
-
// pre-gesture snapshot so the non-grabbed members (previewed through the store)
|
|
180
|
-
// roll back on escape / cancel / failed persist.
|
|
233
|
+
// Active multi-select group-resize session, created lazily on first movement.
|
|
234
|
+
// It owns a projection only; canonical store timing changes at commit.
|
|
181
235
|
const groupResizeRef = useRef<TimelineGroupResizeSession | null>(null);
|
|
182
236
|
|
|
183
|
-
// Restore the non-grabbed group members to their pre-gesture timing (the
|
|
184
|
-
// grabbed clip renders from resizingClip state, so it is never written during
|
|
185
|
-
// preview). `all` also restores the grabbed clip after a committed persist fails.
|
|
186
|
-
const restoreGroupResizeMembers = useCallback(
|
|
187
|
-
(session: TimelineGroupResizeSession, all = false) => {
|
|
188
|
-
for (const m of session.members) {
|
|
189
|
-
if (!all && m.key === session.grabbedKey) continue;
|
|
190
|
-
updateElement(m.key, {
|
|
191
|
-
start: m.start,
|
|
192
|
-
duration: m.duration,
|
|
193
|
-
playbackStart: m.playbackStart,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
[updateElement],
|
|
198
|
-
);
|
|
199
|
-
|
|
200
237
|
const onMoveElementRef = useRef(onMoveElement);
|
|
201
238
|
onMoveElementRef.current = onMoveElement;
|
|
202
239
|
const onMoveElementsRef = useRef(onMoveElements);
|
|
@@ -237,7 +274,7 @@ export function useTimelineClipDrag({
|
|
|
237
274
|
trackOrder: trackOrderRef.current,
|
|
238
275
|
rowHeights: rowGeometryRef?.current.rowHeights,
|
|
239
276
|
elements: elementsRef.current,
|
|
240
|
-
selectedKeys:
|
|
277
|
+
selectedKeys: gestureSelectedKeysRef.current,
|
|
241
278
|
buildSnapTargets,
|
|
242
279
|
audioTracks: dragAudioTracksRef.current,
|
|
243
280
|
});
|
|
@@ -256,18 +293,19 @@ export function useTimelineClipDrag({
|
|
|
256
293
|
buildSnapTargets,
|
|
257
294
|
});
|
|
258
295
|
const setResizeState = (v: ResizePreviewResult) =>
|
|
259
|
-
|
|
296
|
+
publishResizingClip(
|
|
297
|
+
resizingClipRef.current ? { ...resizingClipRef.current, started: true, ...v } : null,
|
|
298
|
+
);
|
|
260
299
|
|
|
261
300
|
// Group resize: a capability-clean multi-selection resizes rigidly by one
|
|
262
301
|
// shared, member-clamped delta (legacy main 36413da7f). The grabbed clip
|
|
263
|
-
// drives the raw delta
|
|
264
|
-
// members preview through the store (their store value stays pristine).
|
|
302
|
+
// drives the raw delta; every member renders from the coordinator projection.
|
|
265
303
|
const grabbedKey = resize.element.key ?? resize.element.id;
|
|
266
304
|
let session = groupResizeRef.current;
|
|
267
305
|
if (!session || session.grabbedKey !== grabbedKey || session.edge !== resize.edge) {
|
|
268
306
|
const members = buildTimelineGroupResizeMembers(
|
|
269
307
|
elementsRef.current,
|
|
270
|
-
|
|
308
|
+
gestureSelectedKeysRef.current,
|
|
271
309
|
grabbedKey,
|
|
272
310
|
resize.edge,
|
|
273
311
|
);
|
|
@@ -287,9 +325,9 @@ export function useTimelineClipDrag({
|
|
|
287
325
|
setResizeState(next);
|
|
288
326
|
return;
|
|
289
327
|
}
|
|
290
|
-
previewGroupResize(session, next,
|
|
328
|
+
previewGroupResize(session, next, setResizeState);
|
|
291
329
|
},
|
|
292
|
-
[scrollRef, ppsRef, buildSnapTargets,
|
|
330
|
+
[scrollRef, ppsRef, buildSnapTargets, publishResizingClip],
|
|
293
331
|
);
|
|
294
332
|
const applyResizePointerRef = useRef(applyResizePointer);
|
|
295
333
|
applyResizePointerRef.current = applyResizePointer;
|
|
@@ -300,9 +338,7 @@ export function useTimelineClipDrag({
|
|
|
300
338
|
cancelAnimationFrame(clipDragScrollRaf.current);
|
|
301
339
|
clipDragScrollRaf.current = 0;
|
|
302
340
|
}
|
|
303
|
-
// Gesture teardown: drop
|
|
304
|
-
// rebuilds them against fresh store state (see snapTargetsCacheRef). Does NOT
|
|
305
|
-
// touch groupResizeRef — commit reads it after this runs.
|
|
341
|
+
// Gesture teardown: drop frozen caches so the next gesture reads fresh state.
|
|
306
342
|
snapTargetsCacheRef.current.clear();
|
|
307
343
|
dragAudioTracksRef.current = null;
|
|
308
344
|
}, []);
|
|
@@ -317,16 +353,14 @@ export function useTimelineClipDrag({
|
|
|
317
353
|
if (!applyTimelineAutoScrollStep(scroll, pointer.clientX, pointer.clientY)) return;
|
|
318
354
|
|
|
319
355
|
if (drag) {
|
|
320
|
-
|
|
321
|
-
prev ? updateDraggedClipPreview(prev, pointer.clientX, pointer.clientY) : prev,
|
|
322
|
-
);
|
|
356
|
+
publishDraggedClip(updateDraggedClipPreview(drag, pointer.clientX, pointer.clientY));
|
|
323
357
|
} else if (resize) {
|
|
324
358
|
// Re-run the trim preview so the edge keeps tracking while the content
|
|
325
359
|
// scrolls under the stationary pointer (scroll-compensated pointer x).
|
|
326
360
|
applyResizePointerRef.current(resize, pointer.clientX);
|
|
327
361
|
}
|
|
328
362
|
clipDragScrollRaf.current = requestAnimationFrame(stepClipDragAutoScroll);
|
|
329
|
-
}, [scrollRef, updateDraggedClipPreview]);
|
|
363
|
+
}, [publishDraggedClip, scrollRef, updateDraggedClipPreview]);
|
|
330
364
|
|
|
331
365
|
const syncClipDragAutoScroll = useCallback(
|
|
332
366
|
(clientX: number, clientY: number) => {
|
|
@@ -354,35 +388,44 @@ export function useTimelineClipDrag({
|
|
|
354
388
|
const stopClipDragAutoScrollRef = useRef(stopClipDragAutoScroll);
|
|
355
389
|
stopClipDragAutoScrollRef.current = stopClipDragAutoScroll;
|
|
356
390
|
|
|
357
|
-
useMountEffect(() =>
|
|
358
|
-
|
|
359
|
-
draggedClipRef,
|
|
360
|
-
resizingClipRef,
|
|
361
|
-
blockedClipRef,
|
|
362
|
-
groupResizeRef,
|
|
363
|
-
suppressClickRef,
|
|
364
|
-
elementsRef,
|
|
365
|
-
trackOrderRef,
|
|
366
|
-
setDraggedClip,
|
|
367
|
-
setResizingClip,
|
|
368
|
-
setShowPopover,
|
|
369
|
-
setRangeSelectionRef,
|
|
370
|
-
applyResizePointerRef,
|
|
371
|
-
syncClipDragAutoScrollRef,
|
|
372
|
-
stopClipDragAutoScrollRef,
|
|
373
|
-
updateDraggedClipPreviewRef,
|
|
374
|
-
restoreGroupResizeMembers,
|
|
375
|
-
updateElement,
|
|
376
|
-
onMoveElementRef,
|
|
377
|
-
onMoveElementsRef,
|
|
378
|
-
onResizeElementRef,
|
|
379
|
-
onResizeElementsRef,
|
|
380
|
-
onBlockedEditAttemptRef,
|
|
381
|
-
readZIndexRef,
|
|
391
|
+
useMountEffect(() =>
|
|
392
|
+
mountTimelineClipDragGestureLifecycle({
|
|
382
393
|
onStackingPatchesRef,
|
|
383
394
|
refreshAfterLaneMoveRef,
|
|
384
|
-
|
|
385
|
-
|
|
395
|
+
readZIndexRef,
|
|
396
|
+
onBlockedEditAttemptRef,
|
|
397
|
+
onResizeElementsRef,
|
|
398
|
+
onResizeElementRef,
|
|
399
|
+
onMoveElementsRef,
|
|
400
|
+
onMoveElementRef,
|
|
401
|
+
updateElement,
|
|
402
|
+
publishDraggedClip,
|
|
403
|
+
updateDraggedClipPreviewRef,
|
|
404
|
+
stopClipDragAutoScrollRef,
|
|
405
|
+
syncClipDragAutoScrollRef,
|
|
406
|
+
applyResizePointerRef,
|
|
407
|
+
setRangeSelectionRef,
|
|
408
|
+
setShowPopover,
|
|
409
|
+
setResizingClipState,
|
|
410
|
+
setDraggedClipState,
|
|
411
|
+
trackOrderRef,
|
|
412
|
+
elementsRef,
|
|
413
|
+
gestureSelectedKeysRef,
|
|
414
|
+
suppressClickRef,
|
|
415
|
+
groupResizeRef,
|
|
416
|
+
blockedClipRef,
|
|
417
|
+
resizingClipRef,
|
|
418
|
+
draggedClipRef,
|
|
419
|
+
scrollRef,
|
|
420
|
+
cancelGestureRef,
|
|
421
|
+
sessionEpochRef,
|
|
422
|
+
lifecycleRef,
|
|
423
|
+
}),
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
cancelGestureRef.current();
|
|
428
|
+
}, [sessionEpoch]);
|
|
386
429
|
|
|
387
430
|
return {
|
|
388
431
|
draggedClip,
|
|
@@ -391,7 +434,6 @@ export function useTimelineClipDrag({
|
|
|
391
434
|
setResizingClip,
|
|
392
435
|
blockedClipRef,
|
|
393
436
|
suppressClickRef,
|
|
394
|
-
syncClipDragAutoScroll,
|
|
395
437
|
stopClipDragAutoScroll,
|
|
396
438
|
};
|
|
397
439
|
}
|
|
@@ -14,7 +14,7 @@ interface UseTimelineClipRenderWindowInput {
|
|
|
14
14
|
duration: number;
|
|
15
15
|
selectedElementId?: string;
|
|
16
16
|
draggedElementId?: string;
|
|
17
|
-
|
|
17
|
+
resizingElementIds?: readonly string[];
|
|
18
18
|
revealElementId?: string;
|
|
19
19
|
focusedEaseElementId?: string;
|
|
20
20
|
clipContextMenuElementId?: string;
|
|
@@ -36,7 +36,7 @@ export function useTimelineClipRenderWindow({
|
|
|
36
36
|
duration,
|
|
37
37
|
selectedElementId,
|
|
38
38
|
draggedElementId,
|
|
39
|
-
|
|
39
|
+
resizingElementIds,
|
|
40
40
|
revealElementId,
|
|
41
41
|
focusedEaseElementId,
|
|
42
42
|
clipContextMenuElementId,
|
|
@@ -60,7 +60,7 @@ export function useTimelineClipRenderWindow({
|
|
|
60
60
|
[
|
|
61
61
|
selectedElementId,
|
|
62
62
|
draggedElementId,
|
|
63
|
-
|
|
63
|
+
...(resizingElementIds ?? []),
|
|
64
64
|
revealElementId,
|
|
65
65
|
focusedEaseElementId,
|
|
66
66
|
clipContextMenuElementId,
|
|
@@ -74,7 +74,7 @@ export function useTimelineClipRenderWindow({
|
|
|
74
74
|
focusedEaseElementId,
|
|
75
75
|
focusedElementId,
|
|
76
76
|
keyframeContextMenuElementId,
|
|
77
|
-
|
|
77
|
+
resizingElementIds,
|
|
78
78
|
revealElementId,
|
|
79
79
|
selectedElementId,
|
|
80
80
|
],
|
|
@@ -5,14 +5,12 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
5
5
|
import { mountReactHarness } from "../../hooks/domSelectionTestHarness";
|
|
6
6
|
import type { TimelineElement } from "../store/playerStore";
|
|
7
7
|
import { usePlayerStore } from "../store/playerStore";
|
|
8
|
+
import * as telemetry from "../../telemetry/events";
|
|
8
9
|
import type { TimelineKeyframeTarget } from "./timelineKeyframeIdentity";
|
|
9
10
|
import { useTimelineKeyframeHandlers } from "./useTimelineKeyframeHandlers";
|
|
10
11
|
|
|
11
12
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
12
13
|
|
|
13
|
-
const trackStudioSegmentEaseEdit = vi.hoisted(() => vi.fn());
|
|
14
|
-
vi.mock("../../telemetry/events", () => ({ trackStudioSegmentEaseEdit }));
|
|
15
|
-
|
|
16
14
|
const ELEMENT: TimelineElement = {
|
|
17
15
|
id: "clip-1",
|
|
18
16
|
label: "Hero card",
|
|
@@ -46,8 +44,8 @@ const COLLIDING_TARGET: TimelineKeyframeTarget = {
|
|
|
46
44
|
|
|
47
45
|
afterEach(() => {
|
|
48
46
|
document.body.innerHTML = "";
|
|
49
|
-
|
|
50
|
-
usePlayerStore.setState({ focusedEaseSegment: null });
|
|
47
|
+
vi.restoreAllMocks();
|
|
48
|
+
usePlayerStore.setState({ focusedEaseSegment: null, timelineSessionEpoch: 0 });
|
|
51
49
|
});
|
|
52
50
|
|
|
53
51
|
/**
|
|
@@ -78,6 +76,9 @@ function mountHandlers(options: Partial<Parameters<typeof useTimelineKeyframeHan
|
|
|
78
76
|
|
|
79
77
|
describe("useTimelineKeyframeHandlers", () => {
|
|
80
78
|
it("tracks opening the segment ease editor when a timeline segment is selected", () => {
|
|
79
|
+
const trackStudioSegmentEaseEdit = vi
|
|
80
|
+
.spyOn(telemetry, "trackStudioSegmentEaseEdit")
|
|
81
|
+
.mockImplementation(() => {});
|
|
81
82
|
const { root, handlers } = mountHandlers();
|
|
82
83
|
act(() => handlers.onSelectSegment?.(ELEMENT.id, TARGET));
|
|
83
84
|
|
|
@@ -90,7 +91,7 @@ describe("useTimelineKeyframeHandlers", () => {
|
|
|
90
91
|
const { root, handlers } = mountHandlers();
|
|
91
92
|
act(() => handlers.onSelectSegment?.(ELEMENT.id, COLLIDING_TARGET));
|
|
92
93
|
|
|
93
|
-
expect(usePlayerStore.getState().focusedEaseSegment).
|
|
94
|
+
expect(usePlayerStore.getState().focusedEaseSegment).toMatchObject({
|
|
94
95
|
animationId: "position-tween",
|
|
95
96
|
collidingAnimationTargets: [
|
|
96
97
|
{ animationId: "position-tween", tweenPercentage: 100 },
|
|
@@ -111,7 +112,7 @@ describe("useTimelineKeyframeHandlers", () => {
|
|
|
111
112
|
// Selecting a segment must NOT move the playhead.
|
|
112
113
|
act(() => handlers.onSelectSegment?.(ELEMENT.id, FLAT_TWEEN_TARGET));
|
|
113
114
|
expect(onSeek).not.toHaveBeenCalled();
|
|
114
|
-
expect(usePlayerStore.getState().focusedEaseSegment).
|
|
115
|
+
expect(usePlayerStore.getState().focusedEaseSegment).toMatchObject({
|
|
115
116
|
animationId: "position-tween",
|
|
116
117
|
tweenPercentage: 100,
|
|
117
118
|
elementId: ELEMENT.id,
|
|
@@ -125,4 +126,39 @@ describe("useTimelineKeyframeHandlers", () => {
|
|
|
125
126
|
expect(onSeek).toHaveBeenCalledExactlyOnceWith(2);
|
|
126
127
|
act(() => root.unmount());
|
|
127
128
|
});
|
|
129
|
+
|
|
130
|
+
it("scopes a keyframe context target to the opening timeline session", () => {
|
|
131
|
+
const setKfContextMenu = vi.fn();
|
|
132
|
+
usePlayerStore.setState({ timelineSessionEpoch: 4 });
|
|
133
|
+
|
|
134
|
+
function Harness() {
|
|
135
|
+
const { onContextMenuKeyframe } = useTimelineKeyframeHandlers({
|
|
136
|
+
expandedElements: [ELEMENT],
|
|
137
|
+
keyframeCache: new Map(),
|
|
138
|
+
setSelectedElementId: vi.fn(),
|
|
139
|
+
setKfContextMenu,
|
|
140
|
+
toggleSelectedKeyframe: vi.fn(),
|
|
141
|
+
});
|
|
142
|
+
return (
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
onContextMenu={(event) => onContextMenuKeyframe(event, ELEMENT.id, TARGET)}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const root = mountReactHarness(<Harness />);
|
|
151
|
+
const button = document.querySelector("button");
|
|
152
|
+
expect(button).not.toBeNull();
|
|
153
|
+
act(() => {
|
|
154
|
+
button?.dispatchEvent(
|
|
155
|
+
new MouseEvent("contextmenu", { bubbles: true, clientX: 10, clientY: 20 }),
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(setKfContextMenu).toHaveBeenCalledWith(
|
|
160
|
+
expect.objectContaining({ elementId: ELEMENT.id, sessionEpoch: 4, x: 14, y: 22 }),
|
|
161
|
+
);
|
|
162
|
+
act(() => root.unmount());
|
|
163
|
+
});
|
|
128
164
|
});
|