@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
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
7
|
+
import { createTimelineRowGeometry, getTimelineRowTop } from "./timelineLayout";
|
|
8
|
+
import { createTimelineClipIndex } from "../lib/timelineClipIndex";
|
|
9
|
+
import { useTimelineRangeSelection } from "./useTimelineRangeSelection";
|
|
10
|
+
import { configureTimelineTestViewport } from "./timelineTestViewport";
|
|
11
|
+
|
|
12
|
+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
13
|
+
|
|
14
|
+
const elements: TimelineElement[] = [
|
|
15
|
+
{ id: "first", tag: "div", start: 1, duration: 1, track: 0 },
|
|
16
|
+
{ id: "offscreen", tag: "div", start: 2, duration: 1, track: 50 },
|
|
17
|
+
{ id: "base", tag: "div", start: 8, duration: 1, track: 99 },
|
|
18
|
+
];
|
|
19
|
+
const tracks = Array.from({ length: 100 }, (_, index) => index);
|
|
20
|
+
const geometry = createTimelineRowGeometry(
|
|
21
|
+
tracks,
|
|
22
|
+
tracks.map(() => 48),
|
|
23
|
+
);
|
|
24
|
+
const clipIndex = createTimelineClipIndex(
|
|
25
|
+
tracks.map((track) => [track, elements.filter((element) => element.track === track)]),
|
|
26
|
+
);
|
|
27
|
+
const FIRST_ROW_Y = getTimelineRowTop(0) + 4;
|
|
28
|
+
const OFFSCREEN_ROW_Y = getTimelineRowTop(50) + 40;
|
|
29
|
+
|
|
30
|
+
function pointer(
|
|
31
|
+
currentTarget: HTMLElement,
|
|
32
|
+
pointerId: number,
|
|
33
|
+
clientX: number,
|
|
34
|
+
clientY: number,
|
|
35
|
+
init: Partial<React.PointerEvent> = {},
|
|
36
|
+
): React.PointerEvent {
|
|
37
|
+
return {
|
|
38
|
+
button: 0,
|
|
39
|
+
clientX,
|
|
40
|
+
clientY,
|
|
41
|
+
ctrlKey: false,
|
|
42
|
+
metaKey: false,
|
|
43
|
+
shiftKey: false,
|
|
44
|
+
pointerId,
|
|
45
|
+
currentTarget,
|
|
46
|
+
target: currentTarget,
|
|
47
|
+
...init,
|
|
48
|
+
} as React.PointerEvent;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderHarness(sessionEpoch = 1) {
|
|
52
|
+
usePlayerStore.setState({ timelineSessionEpoch: sessionEpoch });
|
|
53
|
+
const host = document.createElement("div");
|
|
54
|
+
const scroll = document.createElement("div");
|
|
55
|
+
scroll.setPointerCapture = vi.fn();
|
|
56
|
+
configureTimelineTestViewport(scroll, geometry.canvasHeight);
|
|
57
|
+
host.append(scroll);
|
|
58
|
+
document.body.append(host);
|
|
59
|
+
const root = createRoot(host);
|
|
60
|
+
let api: ReturnType<typeof useTimelineRangeSelection> | null = null;
|
|
61
|
+
const ppsRef = { current: 100 };
|
|
62
|
+
const dragScrollRaf = { current: 0 };
|
|
63
|
+
const isDragging = { current: false };
|
|
64
|
+
const elementsRef = { current: elements };
|
|
65
|
+
const rowGeometryRef = { current: geometry };
|
|
66
|
+
const seekFromX = vi.fn();
|
|
67
|
+
|
|
68
|
+
function Probe({ epoch }: { epoch: number }) {
|
|
69
|
+
api = useTimelineRangeSelection({
|
|
70
|
+
scrollRef: { current: scroll },
|
|
71
|
+
ppsRef,
|
|
72
|
+
effectiveDuration: 60,
|
|
73
|
+
pps: 100,
|
|
74
|
+
seekFromX,
|
|
75
|
+
autoScrollDuringDrag: vi.fn(),
|
|
76
|
+
dragScrollRaf,
|
|
77
|
+
isDragging,
|
|
78
|
+
setShowPopover: vi.fn(),
|
|
79
|
+
elementsRef,
|
|
80
|
+
clipIndex,
|
|
81
|
+
rowGeometryRef,
|
|
82
|
+
contentOrigin: 0,
|
|
83
|
+
sessionEpoch: epoch,
|
|
84
|
+
});
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
act(() => root.render(<Probe epoch={sessionEpoch} />));
|
|
89
|
+
return {
|
|
90
|
+
scroll,
|
|
91
|
+
root,
|
|
92
|
+
get api() {
|
|
93
|
+
if (!api) throw new Error("selection harness did not render");
|
|
94
|
+
return api;
|
|
95
|
+
},
|
|
96
|
+
rerender(epoch: number) {
|
|
97
|
+
usePlayerStore.setState({ timelineSessionEpoch: epoch });
|
|
98
|
+
act(() => root.render(<Probe epoch={epoch} />));
|
|
99
|
+
},
|
|
100
|
+
seekFromX,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function dragMarquee(
|
|
105
|
+
view: ReturnType<typeof renderHarness>,
|
|
106
|
+
options: { secondPointer?: boolean; release?: boolean } = {},
|
|
107
|
+
): void {
|
|
108
|
+
act(() => {
|
|
109
|
+
view.api.handlePointerDown(pointer(view.scroll, 7, 0, FIRST_ROW_Y));
|
|
110
|
+
if (options.secondPointer) {
|
|
111
|
+
view.api.handlePointerDown(pointer(view.scroll, 8, 500, FIRST_ROW_Y));
|
|
112
|
+
}
|
|
113
|
+
view.api.handlePointerMove(pointer(view.scroll, 7, 400, OFFSCREEN_ROW_Y));
|
|
114
|
+
if (options.release) {
|
|
115
|
+
view.api.handlePointerUp(pointer(view.scroll, 7, 400, OFFSCREEN_ROW_Y));
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function expectSelectedIds(...ids: string[]): void {
|
|
121
|
+
expect(usePlayerStore.getState().selectedElementIds).toEqual(new Set(ids));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function unmountHarness(view: ReturnType<typeof renderHarness>): void {
|
|
125
|
+
act(() => view.root.unmount());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
afterEach(() => {
|
|
129
|
+
usePlayerStore.getState().reset();
|
|
130
|
+
document.body.innerHTML = "";
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("useTimelineRangeSelection", () => {
|
|
134
|
+
it("marquee-selects model clips across unmounted virtual rows", () => {
|
|
135
|
+
const view = renderHarness();
|
|
136
|
+
dragMarquee(view);
|
|
137
|
+
|
|
138
|
+
expect(document.querySelectorAll("[data-clip]")).toHaveLength(0);
|
|
139
|
+
expectSelectedIds("first", "offscreen");
|
|
140
|
+
unmountHarness(view);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("ignores another pointer and restores the pre-drag selection on cancellation", () => {
|
|
144
|
+
usePlayerStore.getState().setSelectedElementId("base");
|
|
145
|
+
const view = renderHarness();
|
|
146
|
+
dragMarquee(view);
|
|
147
|
+
act(() => view.api.handlePointerUp(pointer(view.scroll, 8, 400, OFFSCREEN_ROW_Y)));
|
|
148
|
+
expectSelectedIds("first", "offscreen");
|
|
149
|
+
|
|
150
|
+
act(() => view.api.handlePointerCancel(pointer(view.scroll, 7, 400, OFFSCREEN_ROW_Y)));
|
|
151
|
+
expect(usePlayerStore.getState().selectedElementId).toBe("base");
|
|
152
|
+
expectSelectedIds("base");
|
|
153
|
+
unmountHarness(view);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("keeps the original pointer owner when a second pointer presses", () => {
|
|
157
|
+
const view = renderHarness();
|
|
158
|
+
dragMarquee(view, { secondPointer: true, release: true });
|
|
159
|
+
|
|
160
|
+
expectSelectedIds("first", "offscreen");
|
|
161
|
+
unmountHarness(view);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("commits a ruler click at its pointerdown position without requiring pointer movement", () => {
|
|
165
|
+
const view = renderHarness();
|
|
166
|
+
|
|
167
|
+
act(() => {
|
|
168
|
+
view.api.handlePointerDown(pointer(view.scroll, 7, 375, 5));
|
|
169
|
+
view.api.handlePointerUp(pointer(view.scroll, 7, 375, 5));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(view.seekFromX).toHaveBeenNthCalledWith(1, 375);
|
|
173
|
+
expect(view.seekFromX).toHaveBeenNthCalledWith(2, 375);
|
|
174
|
+
unmountHarness(view);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("does not clear a finalized range when capture is lost after pointerup", () => {
|
|
178
|
+
const view = renderHarness();
|
|
179
|
+
|
|
180
|
+
act(() => {
|
|
181
|
+
view.api.handlePointerDown(pointer(view.scroll, 7, 100, 80, { shiftKey: true }));
|
|
182
|
+
view.api.handlePointerMove(pointer(view.scroll, 7, 300, 80, { shiftKey: true }));
|
|
183
|
+
view.api.handlePointerUp(pointer(view.scroll, 7, 300, 80, { shiftKey: true }));
|
|
184
|
+
});
|
|
185
|
+
const finalized = view.api.rangeSelection;
|
|
186
|
+
expect(finalized).toMatchObject({ start: 1, end: 3 });
|
|
187
|
+
|
|
188
|
+
act(() => view.api.handlePointerCancel(pointer(view.scroll, 7, 300, 80)));
|
|
189
|
+
expect(view.api.rangeSelection).toEqual(finalized);
|
|
190
|
+
unmountHarness(view);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("cancels a live marquee when the project session changes", () => {
|
|
194
|
+
usePlayerStore.getState().setSelectedElementId("base");
|
|
195
|
+
const view = renderHarness(1);
|
|
196
|
+
dragMarquee(view);
|
|
197
|
+
usePlayerStore.getState().setSelectedElementId("base");
|
|
198
|
+
view.rerender(2);
|
|
199
|
+
|
|
200
|
+
expect(usePlayerStore.getState().selectedElementId).toBe("base");
|
|
201
|
+
expectSelectedIds("base");
|
|
202
|
+
unmountHarness(view);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -10,6 +10,7 @@ import { liveTime, usePlayerStore } from "../store/playerStore";
|
|
|
10
10
|
import { getTimelineScrubTime } from "./timelineLayout";
|
|
11
11
|
import {
|
|
12
12
|
computeMarqueeSelection,
|
|
13
|
+
getMarqueeClipCandidates,
|
|
13
14
|
getMarqueeRect,
|
|
14
15
|
isMarqueeDrag,
|
|
15
16
|
isTimelineRulerPress,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
} from "./timelineMarquee";
|
|
18
19
|
import type { Rect } from "../../utils/marqueeGeometry";
|
|
19
20
|
import type { TimelineRowGeometry } from "./timelineLayout";
|
|
21
|
+
import type { TimelineClipIndex } from "../lib/timelineClipIndex";
|
|
20
22
|
|
|
21
23
|
interface UseTimelineRangeSelectionInput {
|
|
22
24
|
scrollRef: React.RefObject<HTMLDivElement | null>;
|
|
@@ -30,10 +32,11 @@ interface UseTimelineRangeSelectionInput {
|
|
|
30
32
|
isDragging: React.RefObject<boolean>;
|
|
31
33
|
setShowPopover: (v: boolean) => void;
|
|
32
34
|
elementsRef: React.RefObject<TimelineElement[]>;
|
|
33
|
-
|
|
35
|
+
clipIndex: TimelineClipIndex;
|
|
34
36
|
rowGeometryRef: React.RefObject<TimelineRowGeometry>;
|
|
35
37
|
onSelectElement?: (element: TimelineElement | null) => void;
|
|
36
38
|
contentOrigin: number;
|
|
39
|
+
sessionEpoch: number;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
interface MarqueeDragState {
|
|
@@ -73,16 +76,21 @@ function commitMarqueeSelection(
|
|
|
73
76
|
rect: Rect,
|
|
74
77
|
additive: boolean,
|
|
75
78
|
marquee: MarqueeDragState,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
rowHeights: readonly number[],
|
|
79
|
+
clipIndex: TimelineClipIndex,
|
|
80
|
+
rowGeometry: TimelineRowGeometry,
|
|
79
81
|
pps: number,
|
|
80
82
|
contentOrigin: number,
|
|
81
83
|
): void {
|
|
84
|
+
const candidates = getMarqueeClipCandidates({
|
|
85
|
+
clipIndex,
|
|
86
|
+
rowGeometry,
|
|
87
|
+
marquee: rect,
|
|
88
|
+
pps,
|
|
89
|
+
contentOrigin,
|
|
90
|
+
});
|
|
82
91
|
const { ids, primaryId } = computeMarqueeSelection({
|
|
83
|
-
clips: toMarqueeClips(
|
|
84
|
-
|
|
85
|
-
rowHeights,
|
|
92
|
+
clips: toMarqueeClips([...candidates]),
|
|
93
|
+
rowGeometry,
|
|
86
94
|
pps,
|
|
87
95
|
contentOrigin,
|
|
88
96
|
marquee: rect,
|
|
@@ -95,6 +103,28 @@ function commitMarqueeSelection(
|
|
|
95
103
|
store.setSelectedElementIds(ids);
|
|
96
104
|
}
|
|
97
105
|
|
|
106
|
+
function canStartPointerGesture(
|
|
107
|
+
event: React.PointerEvent,
|
|
108
|
+
activePointerId: number | null,
|
|
109
|
+
sessionEpoch: number,
|
|
110
|
+
): boolean {
|
|
111
|
+
return (
|
|
112
|
+
event.button === 0 &&
|
|
113
|
+
activePointerId === null &&
|
|
114
|
+
sessionEpoch === usePlayerStore.getState().timelineSessionEpoch
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isMarqueePress(
|
|
119
|
+
point: { x: number; y: number } | null,
|
|
120
|
+
scrollRect: DOMRect | undefined,
|
|
121
|
+
clientY: number,
|
|
122
|
+
): point is { x: number; y: number } {
|
|
123
|
+
return (
|
|
124
|
+
point !== null && scrollRect !== undefined && !isTimelineRulerPress(clientY, scrollRect.top)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
98
128
|
export function useTimelineRangeSelection({
|
|
99
129
|
scrollRef,
|
|
100
130
|
ppsRef,
|
|
@@ -107,10 +137,11 @@ export function useTimelineRangeSelection({
|
|
|
107
137
|
isDragging,
|
|
108
138
|
setShowPopover,
|
|
109
139
|
elementsRef,
|
|
110
|
-
|
|
140
|
+
clipIndex,
|
|
111
141
|
rowGeometryRef,
|
|
112
142
|
onSelectElement,
|
|
113
143
|
contentOrigin,
|
|
144
|
+
sessionEpoch,
|
|
114
145
|
}: UseTimelineRangeSelectionInput) {
|
|
115
146
|
const isRangeSelecting = useRef(false);
|
|
116
147
|
const rangeAnchorTime = useRef(0);
|
|
@@ -126,6 +157,17 @@ export function useTimelineRangeSelection({
|
|
|
126
157
|
|
|
127
158
|
const seekRafRef = useRef(0);
|
|
128
159
|
const pendingClientXRef = useRef(0);
|
|
160
|
+
const activePointerIdRef = useRef<number | null>(null);
|
|
161
|
+
const gestureEpochRef = useRef<number | null>(null);
|
|
162
|
+
const sessionEpochRef = useRef(sessionEpoch);
|
|
163
|
+
sessionEpochRef.current = sessionEpoch;
|
|
164
|
+
|
|
165
|
+
const isGestureSessionCurrent = useCallback(
|
|
166
|
+
() =>
|
|
167
|
+
gestureEpochRef.current === sessionEpochRef.current &&
|
|
168
|
+
gestureEpochRef.current === usePlayerStore.getState().timelineSessionEpoch,
|
|
169
|
+
[],
|
|
170
|
+
);
|
|
129
171
|
|
|
130
172
|
// Marquee (rubber-band) multi-select on the empty timeline body.
|
|
131
173
|
const marqueeRef = useRef<MarqueeDragState | null>(null);
|
|
@@ -159,7 +201,7 @@ export function useTimelineRangeSelection({
|
|
|
159
201
|
const applyMarqueeAtClient = useCallback(
|
|
160
202
|
(clientX: number, clientY: number, shiftKey: boolean) => {
|
|
161
203
|
const marquee = marqueeRef.current;
|
|
162
|
-
if (!marquee) return;
|
|
204
|
+
if (!marquee || !isGestureSessionCurrent()) return;
|
|
163
205
|
const point = toContentPoint(clientX, clientY);
|
|
164
206
|
if (!point) return;
|
|
165
207
|
if (!marquee.active && !isMarqueeDrag(marquee.originX, marquee.originY, point.x, point.y)) {
|
|
@@ -175,14 +217,13 @@ export function useTimelineRangeSelection({
|
|
|
175
217
|
rect,
|
|
176
218
|
additive,
|
|
177
219
|
marquee,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
rowGeometryRef.current.rowHeights,
|
|
220
|
+
clipIndex,
|
|
221
|
+
rowGeometryRef.current,
|
|
181
222
|
ppsRef.current,
|
|
182
223
|
contentOrigin,
|
|
183
224
|
);
|
|
184
225
|
},
|
|
185
|
-
[toContentPoint,
|
|
226
|
+
[toContentPoint, isGestureSessionCurrent, clipIndex, rowGeometryRef, ppsRef, contentOrigin],
|
|
186
227
|
);
|
|
187
228
|
|
|
188
229
|
const stopMarqueeAutoScroll = useCallback(() => {
|
|
@@ -203,14 +244,16 @@ export function useTimelineRangeSelection({
|
|
|
203
244
|
const marquee = marqueeRef.current;
|
|
204
245
|
const pointer = marqueePointerRef.current;
|
|
205
246
|
const scroll = scrollRef.current;
|
|
206
|
-
if (!marquee || !pointer || !scroll)
|
|
247
|
+
if (!marquee || !pointer || !scroll || !isGestureSessionCurrent()) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
207
250
|
if (!applyTimelineAutoScrollStep(scroll, pointer.clientX, pointer.clientY)) return;
|
|
208
251
|
|
|
209
252
|
// Re-run at the SAME client point: toContentPoint folds in the new scroll, so
|
|
210
253
|
// the marquee's moving corner tracks the revealed content.
|
|
211
254
|
applyMarqueeAtClient(pointer.clientX, pointer.clientY, pointer.shiftKey);
|
|
212
255
|
marqueeScrollRaf.current = requestAnimationFrame(stepMarqueeAutoScroll);
|
|
213
|
-
}, [scrollRef, applyMarqueeAtClient]);
|
|
256
|
+
}, [scrollRef, applyMarqueeAtClient, isGestureSessionCurrent]);
|
|
214
257
|
|
|
215
258
|
const syncMarqueeAutoScroll = useCallback(
|
|
216
259
|
(clientX: number, clientY: number, shiftKey: boolean) => {
|
|
@@ -235,6 +278,8 @@ export function useTimelineRangeSelection({
|
|
|
235
278
|
const beginRangeSelection = useCallback(
|
|
236
279
|
(e: React.PointerEvent) => {
|
|
237
280
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
281
|
+
activePointerIdRef.current = e.pointerId;
|
|
282
|
+
gestureEpochRef.current = sessionEpochRef.current;
|
|
238
283
|
isRangeSelecting.current = true;
|
|
239
284
|
setShowPopover(false);
|
|
240
285
|
const rect = scrollRef.current?.getBoundingClientRect();
|
|
@@ -248,9 +293,31 @@ export function useTimelineRangeSelection({
|
|
|
248
293
|
[scrollRef, pps, setShowPopover, contentOrigin],
|
|
249
294
|
);
|
|
250
295
|
|
|
296
|
+
const beginScrub = useCallback(
|
|
297
|
+
(clientX: number) => {
|
|
298
|
+
isDragging.current = true;
|
|
299
|
+
setIsScrubbing(true);
|
|
300
|
+
pendingClientXRef.current = clientX;
|
|
301
|
+
seekFromX(clientX);
|
|
302
|
+
},
|
|
303
|
+
[isDragging, seekFromX],
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const beginMarquee = useCallback((point: { x: number; y: number }, additive: boolean) => {
|
|
307
|
+
const base = snapshotSelection();
|
|
308
|
+
marqueeRef.current = {
|
|
309
|
+
originX: point.x,
|
|
310
|
+
originY: point.y,
|
|
311
|
+
baseIds: base.ids,
|
|
312
|
+
basePrimary: base.primary,
|
|
313
|
+
additive,
|
|
314
|
+
active: false,
|
|
315
|
+
};
|
|
316
|
+
}, []);
|
|
317
|
+
|
|
251
318
|
const handlePointerDown = useCallback(
|
|
252
319
|
(e: React.PointerEvent) => {
|
|
253
|
-
if (e.
|
|
320
|
+
if (!canStartPointerGesture(e, activePointerIdRef.current, sessionEpochRef.current)) return;
|
|
254
321
|
if (e.shiftKey) {
|
|
255
322
|
beginRangeSelection(e);
|
|
256
323
|
return;
|
|
@@ -258,6 +325,8 @@ export function useTimelineRangeSelection({
|
|
|
258
325
|
shiftClickClipRef.current = null;
|
|
259
326
|
if ((e.target as HTMLElement).closest("[data-clip]")) return;
|
|
260
327
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
328
|
+
activePointerIdRef.current = e.pointerId;
|
|
329
|
+
gestureEpochRef.current = sessionEpochRef.current;
|
|
261
330
|
setRangeSelection(null);
|
|
262
331
|
setShowPopover(false);
|
|
263
332
|
const point = toContentPoint(e.clientX, e.clientY);
|
|
@@ -266,30 +335,15 @@ export function useTimelineRangeSelection({
|
|
|
266
335
|
// y (which folds in scrollTop) breaks once the body is scrolled down and
|
|
267
336
|
// the stuck ruler visually overlays scrolled-away track rows.
|
|
268
337
|
const scrollRect = scrollRef.current?.getBoundingClientRect();
|
|
269
|
-
if (!point
|
|
270
|
-
|
|
271
|
-
setIsScrubbing(true);
|
|
272
|
-
// Seed the pending coordinate so a press with no pointermove still
|
|
273
|
-
// replays THIS x on pointerup. `updateScrubDrag` is the only other
|
|
274
|
-
// writer, so without this a plain click settles on the ref's initial
|
|
275
|
-
// 0 and clamps the playhead back to t=0.
|
|
276
|
-
pendingClientXRef.current = e.clientX;
|
|
277
|
-
seekFromX(e.clientX);
|
|
338
|
+
if (!isMarqueePress(point, scrollRect, e.clientY)) {
|
|
339
|
+
beginScrub(e.clientX);
|
|
278
340
|
return;
|
|
279
341
|
}
|
|
280
342
|
// Empty body press → pending marquee. A plain click (no drag past the
|
|
281
343
|
// threshold) deselects on pointerup; a drag draws the marquee. Never scrubs.
|
|
282
|
-
|
|
283
|
-
marqueeRef.current = {
|
|
284
|
-
originX: point.x,
|
|
285
|
-
originY: point.y,
|
|
286
|
-
baseIds: base.ids,
|
|
287
|
-
basePrimary: base.primary,
|
|
288
|
-
additive: e.metaKey || e.ctrlKey,
|
|
289
|
-
active: false,
|
|
290
|
-
};
|
|
344
|
+
beginMarquee(point, e.metaKey || e.ctrlKey);
|
|
291
345
|
},
|
|
292
|
-
[beginRangeSelection,
|
|
346
|
+
[beginRangeSelection, beginScrub, beginMarquee, scrollRef, setShowPopover, toContentPoint],
|
|
293
347
|
);
|
|
294
348
|
|
|
295
349
|
// Scrub-drag update: live playhead feedback (liveTime) + RAF-throttled seek.
|
|
@@ -325,18 +379,27 @@ export function useTimelineRangeSelection({
|
|
|
325
379
|
[scrollRef, pps, seekFromX, autoScrollDuringDrag, isDragging, contentOrigin],
|
|
326
380
|
);
|
|
327
381
|
|
|
382
|
+
const updateRangeSelection = useCallback(
|
|
383
|
+
(e: React.PointerEvent) => {
|
|
384
|
+
const scroll = scrollRef.current;
|
|
385
|
+
const rect = scroll?.getBoundingClientRect();
|
|
386
|
+
if (!scroll || !rect) return;
|
|
387
|
+
const x = e.clientX - rect.left + scroll.scrollLeft - contentOrigin;
|
|
388
|
+
setRangeSelection((previous) =>
|
|
389
|
+
previous
|
|
390
|
+
? { ...previous, end: Math.max(0, x / pps), anchorX: e.clientX, anchorY: e.clientY }
|
|
391
|
+
: null,
|
|
392
|
+
);
|
|
393
|
+
},
|
|
394
|
+
[contentOrigin, pps, scrollRef],
|
|
395
|
+
);
|
|
396
|
+
|
|
328
397
|
const handlePointerMove = useCallback(
|
|
329
398
|
(e: React.PointerEvent) => {
|
|
399
|
+
if (!isGestureSessionCurrent()) return;
|
|
400
|
+
if (activePointerIdRef.current !== null && e.pointerId !== activePointerIdRef.current) return;
|
|
330
401
|
if (isRangeSelecting.current) {
|
|
331
|
-
|
|
332
|
-
if (rect) {
|
|
333
|
-
const x = e.clientX - rect.left + (scrollRef.current?.scrollLeft ?? 0) - contentOrigin;
|
|
334
|
-
setRangeSelection((prev) =>
|
|
335
|
-
prev
|
|
336
|
-
? { ...prev, end: Math.max(0, x / pps), anchorX: e.clientX, anchorY: e.clientY }
|
|
337
|
-
: null,
|
|
338
|
-
);
|
|
339
|
-
}
|
|
402
|
+
updateRangeSelection(e);
|
|
340
403
|
return;
|
|
341
404
|
}
|
|
342
405
|
const marquee = marqueeRef.current;
|
|
@@ -351,13 +414,12 @@ export function useTimelineRangeSelection({
|
|
|
351
414
|
updateScrubDrag(e.clientX);
|
|
352
415
|
},
|
|
353
416
|
[
|
|
354
|
-
pps,
|
|
355
|
-
scrollRef,
|
|
356
417
|
isDragging,
|
|
357
418
|
applyMarqueeAtClient,
|
|
358
419
|
syncMarqueeAutoScroll,
|
|
359
420
|
updateScrubDrag,
|
|
360
|
-
|
|
421
|
+
updateRangeSelection,
|
|
422
|
+
isGestureSessionCurrent,
|
|
361
423
|
],
|
|
362
424
|
);
|
|
363
425
|
|
|
@@ -403,17 +465,15 @@ export function useTimelineRangeSelection({
|
|
|
403
465
|
[stopMarqueeAutoScroll, elementsRef, onSelectElement],
|
|
404
466
|
);
|
|
405
467
|
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
if (!isDragging.current) return;
|
|
468
|
+
const canFinishPointerGesture = useCallback(
|
|
469
|
+
(e?: React.PointerEvent) => {
|
|
470
|
+
const pointerId = activePointerIdRef.current;
|
|
471
|
+
return pointerId !== null && (!e || e.pointerId === pointerId) && isGestureSessionCurrent();
|
|
472
|
+
},
|
|
473
|
+
[isGestureSessionCurrent],
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
const finishScrub = useCallback(() => {
|
|
417
477
|
if (seekRafRef.current) {
|
|
418
478
|
cancelAnimationFrame(seekRafRef.current);
|
|
419
479
|
seekRafRef.current = 0;
|
|
@@ -422,7 +482,69 @@ export function useTimelineRangeSelection({
|
|
|
422
482
|
isDragging.current = false;
|
|
423
483
|
setIsScrubbing(false);
|
|
424
484
|
cancelAnimationFrame(dragScrollRaf.current);
|
|
425
|
-
}, [
|
|
485
|
+
}, [dragScrollRaf, isDragging, seekFromX]);
|
|
486
|
+
|
|
487
|
+
const handlePointerUp = useCallback(
|
|
488
|
+
(e?: React.PointerEvent) => {
|
|
489
|
+
if (!canFinishPointerGesture(e)) return;
|
|
490
|
+
activePointerIdRef.current = null;
|
|
491
|
+
gestureEpochRef.current = null;
|
|
492
|
+
if (isRangeSelecting.current) {
|
|
493
|
+
finishRangeSelection();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const marquee = marqueeRef.current;
|
|
497
|
+
if (marquee) {
|
|
498
|
+
finishMarquee(marquee);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (isDragging.current) finishScrub();
|
|
502
|
+
},
|
|
503
|
+
[canFinishPointerGesture, finishRangeSelection, finishMarquee, finishScrub, isDragging],
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
const cancelActiveGesture = useCallback(
|
|
507
|
+
(updateUi: boolean, restoreSelection: boolean) => {
|
|
508
|
+
activePointerIdRef.current = null;
|
|
509
|
+
gestureEpochRef.current = null;
|
|
510
|
+
isRangeSelecting.current = false;
|
|
511
|
+
isDragging.current = false;
|
|
512
|
+
stopMarqueeAutoScroll();
|
|
513
|
+
if (seekRafRef.current) {
|
|
514
|
+
cancelAnimationFrame(seekRafRef.current);
|
|
515
|
+
seekRafRef.current = 0;
|
|
516
|
+
}
|
|
517
|
+
cancelAnimationFrame(dragScrollRaf.current);
|
|
518
|
+
dragScrollRaf.current = 0;
|
|
519
|
+
|
|
520
|
+
const marquee = marqueeRef.current;
|
|
521
|
+
marqueeRef.current = null;
|
|
522
|
+
if (restoreSelection && marquee?.active) {
|
|
523
|
+
const store = usePlayerStore.getState();
|
|
524
|
+
store.setSelectedElementId(marquee.basePrimary);
|
|
525
|
+
store.setSelectedElementIds(marquee.baseIds);
|
|
526
|
+
}
|
|
527
|
+
if (updateUi) {
|
|
528
|
+
setMarqueeRect(null);
|
|
529
|
+
setRangeSelection(null);
|
|
530
|
+
setIsScrubbing(false);
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
[dragScrollRaf, isDragging, stopMarqueeAutoScroll],
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
const handlePointerCancel = useCallback(
|
|
537
|
+
(e?: React.PointerEvent) => {
|
|
538
|
+
if (
|
|
539
|
+
activePointerIdRef.current === null ||
|
|
540
|
+
(e && activePointerIdRef.current !== null && e.pointerId !== activePointerIdRef.current)
|
|
541
|
+
) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
cancelActiveGesture(true, isGestureSessionCurrent());
|
|
545
|
+
},
|
|
546
|
+
[cancelActiveGesture, isGestureSessionCurrent],
|
|
547
|
+
);
|
|
426
548
|
|
|
427
549
|
// Escape: cancel an in-flight marquee (restores the pre-drag selection);
|
|
428
550
|
// otherwise clear any lingering multi-selection.
|
|
@@ -432,15 +554,11 @@ export function useTimelineRangeSelection({
|
|
|
432
554
|
const store = usePlayerStore.getState();
|
|
433
555
|
const marquee = marqueeRef.current;
|
|
434
556
|
if (marquee) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
// restore the pre-drag primary before repopulating the base ids.
|
|
441
|
-
store.setSelectedElementId(marquee.basePrimary);
|
|
442
|
-
store.setSelectedElementIds(marquee.baseIds);
|
|
443
|
-
}
|
|
557
|
+
cancelActiveGesture(true, true);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (isRangeSelecting.current || isDragging.current) {
|
|
561
|
+
cancelActiveGesture(true, true);
|
|
444
562
|
return;
|
|
445
563
|
}
|
|
446
564
|
// Escape with no marquee clears the whole selection — primary AND set.
|
|
@@ -451,7 +569,21 @@ export function useTimelineRangeSelection({
|
|
|
451
569
|
};
|
|
452
570
|
window.addEventListener("keydown", onKeyDown);
|
|
453
571
|
return () => window.removeEventListener("keydown", onKeyDown);
|
|
454
|
-
}, [
|
|
572
|
+
}, [cancelActiveGesture, isDragging]);
|
|
573
|
+
|
|
574
|
+
const previousSessionEpochRef = useRef(sessionEpoch);
|
|
575
|
+
useEffect(() => {
|
|
576
|
+
if (previousSessionEpochRef.current === sessionEpoch) return;
|
|
577
|
+
previousSessionEpochRef.current = sessionEpoch;
|
|
578
|
+
cancelActiveGesture(true, false);
|
|
579
|
+
}, [cancelActiveGesture, sessionEpoch]);
|
|
580
|
+
|
|
581
|
+
useEffect(
|
|
582
|
+
() => () => {
|
|
583
|
+
cancelActiveGesture(false, isGestureSessionCurrent());
|
|
584
|
+
},
|
|
585
|
+
[cancelActiveGesture, isGestureSessionCurrent],
|
|
586
|
+
);
|
|
455
587
|
|
|
456
588
|
return {
|
|
457
589
|
rangeSelection,
|
|
@@ -462,5 +594,6 @@ export function useTimelineRangeSelection({
|
|
|
462
594
|
handlePointerDown,
|
|
463
595
|
handlePointerMove,
|
|
464
596
|
handlePointerUp,
|
|
597
|
+
handlePointerCancel,
|
|
465
598
|
};
|
|
466
599
|
}
|
|
@@ -9,6 +9,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
9
9
|
import { mountReactHarness } from "../../hooks/domSelectionTestHarness";
|
|
10
10
|
import { useTimelineRangeSelection } from "./useTimelineRangeSelection";
|
|
11
11
|
import { getTimelineRowGeometry } from "./timelineLayout";
|
|
12
|
+
import { createTimelineClipIndex } from "../lib/timelineClipIndex";
|
|
12
13
|
|
|
13
14
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
14
15
|
|
|
@@ -60,7 +61,6 @@ function setup(): { handlers: () => Handlers; seekFromX: ReturnType<typeof vi.fn
|
|
|
60
61
|
const dragScrollRaf = { current: 0 };
|
|
61
62
|
const isDragging = { current: false };
|
|
62
63
|
const elementsRef = { current: [] };
|
|
63
|
-
const trackOrderRef = { current: [] };
|
|
64
64
|
const rowGeometryRef = { current: getTimelineRowGeometry([]) };
|
|
65
65
|
|
|
66
66
|
function Probe(): null {
|
|
@@ -75,7 +75,8 @@ function setup(): { handlers: () => Handlers; seekFromX: ReturnType<typeof vi.fn
|
|
|
75
75
|
isDragging,
|
|
76
76
|
setShowPopover: vi.fn(),
|
|
77
77
|
elementsRef,
|
|
78
|
-
|
|
78
|
+
clipIndex: createTimelineClipIndex([]),
|
|
79
|
+
sessionEpoch: 0,
|
|
79
80
|
rowGeometryRef,
|
|
80
81
|
contentOrigin: 0,
|
|
81
82
|
});
|