@hyperframes/studio 0.8.138 → 0.8.140
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-B30apksz.js → hyperframes-player-BLuyxnK7.js} +1 -1
- package/dist/assets/{index-DXVvWfQG.js → index-CWfuDCoA.js} +1 -1
- package/dist/assets/{index-DIJuIBwI.js → index-Dr3Bo9uH.js} +1 -1
- package/dist/assets/{index-BdjA0CLD.js → index-VAHDM_zL.js} +210 -210
- package/dist/{chunk-4S3AYCOG.js → chunk-H2DUOXTF.js} +3 -2
- package/dist/{chunk-TDWGXBUT.js → chunk-TFLU2MGI.js} +1 -6
- package/dist/chunk-TFLU2MGI.js.map +1 -0
- package/dist/{chunk-7UQWVN52.js → chunk-UYWON5AX.js} +2 -2
- package/dist/{domEditingLayers-IEVLO54Z.js → domEditingLayers-KBTPQKGA.js} +3 -3
- package/dist/index.d.ts +20 -3
- package/dist/index.html +1 -1
- package/dist/index.js +7327 -6265
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/package.json +8 -8
- package/src/App.tsx +9 -4
- package/src/components/TimelineToolbar.test.tsx +79 -1
- package/src/components/TimelineToolbar.tsx +14 -29
- package/src/components/editor/DomEditOverlay.tsx +11 -7
- package/src/components/editor/PropertyPanel.test.tsx +600 -748
- package/src/components/editor/PropertyPanel.tsx +1 -0
- package/src/components/editor/PropertyPanelFlat.tsx +1 -0
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +2 -0
- package/src/components/editor/domEditOverlayGeometry.ts +7 -7
- package/src/components/editor/domEditOverlayGestures.ts +18 -0
- package/src/components/editor/domEditOverlayStartGesture.blocked.test.ts +1 -0
- package/src/components/editor/domEditOverlayStartGesture.test.ts +3 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +9 -1
- package/src/components/editor/dragUndoPaint.test.ts +2 -0
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +432 -15
- package/src/components/editor/groupDropKeepsSelection.test.ts +1 -0
- package/src/components/editor/manualEdits.ts +1 -1
- package/src/components/editor/manualEditsDom.ts +7 -7
- package/src/components/editor/manualEditsParsing.ts +14 -2
- package/src/components/editor/manualEditsTypes.ts +5 -0
- package/src/components/editor/manualOffsetDrag.test.ts +23 -0
- package/src/components/editor/manualOffsetDrag.ts +5 -0
- package/src/components/editor/previewGestureStarts.ts +284 -0
- package/src/components/editor/previewReadOnly.overlay.test.tsx +3 -2
- package/src/components/editor/propertyPanelFill.tsx +36 -13
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +4 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +13 -7
- package/src/components/editor/propertyPanelImageFill.test.tsx +164 -0
- package/src/components/editor/propertyPanelStyleSections.tsx +10 -7
- package/src/components/editor/rotateInlineSave.test.ts +1 -0
- package/src/components/editor/shiftDragAxisLock.test.tsx +1 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +67 -71
- package/src/components/nle/NLEContext.test.ts +26 -0
- package/src/components/nle/NLEContext.tsx +8 -3
- package/src/components/nle/NLEPreview.test.ts +44 -1
- package/src/components/nle/NLEPreview.tsx +7 -2
- package/src/hooks/draggedGsapPosition.ts +8 -0
- package/src/hooks/editMoment.ts +13 -0
- package/src/hooks/externalFileChangeOwnership.test.ts +25 -4
- package/src/hooks/gsapDragCommit.ts +2 -1
- package/src/hooks/gsapDragPositionCommit.ts +19 -7
- package/src/hooks/gsapPositionDetection.ts +5 -3
- package/src/hooks/gsapResizeIntercept.ts +11 -4
- package/src/hooks/gsapRuntimeBridge.ts +31 -11
- package/src/hooks/gsapValueAtPlayhead.browser.test.ts +3 -22
- package/src/hooks/gsapValueAtPlayhead.test.ts +15 -0
- package/src/hooks/gsapValueAtPlayhead.ts +12 -5
- package/src/hooks/timelineEditingHelpers.test.ts +22 -0
- package/src/hooks/timelineEditingHelpers.ts +18 -2
- package/src/hooks/useDomSelection.test.ts +21 -2
- package/src/hooks/useEditHistoryActions.test.tsx +20 -0
- package/src/hooks/useEditHistoryActions.ts +2 -0
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +536 -10
- package/src/hooks/useExternalFileChangeCoordinator.ts +72 -45
- package/src/hooks/useGsapAwareEditing.test.tsx +38 -0
- package/src/hooks/useGsapAwareEditing.ts +148 -136
- package/src/hooks/useGsapAwareGroupMove.ts +126 -113
- package/src/hooks/useGsapScriptCommits.ts +5 -4
- package/src/hooks/useMusicBeatAnalysis.ts +2 -1
- package/src/hooks/usePreviewPersistence.history.test.tsx +7 -2
- package/src/hooks/usePreviewPersistence.ts +1 -4
- package/src/hooks/useSdkSession.lifecycle.test.tsx +20 -0
- package/src/hooks/useSdkSession.ts +4 -1
- package/src/hooks/useThumbnailStripSize.test.tsx +65 -0
- package/src/hooks/useThumbnailStripSize.ts +23 -1
- package/src/hooks/useTimelineDeleteOps.test.tsx +10 -0
- package/src/hooks/useTimelineDeleteOps.ts +9 -10
- package/src/index.ts +2 -0
- package/src/player/components/AudioWaveform.test.tsx +153 -1
- package/src/player/components/AudioWaveform.tsx +93 -37
- package/src/player/components/Timeline.scrollRest.test.tsx +22 -0
- package/src/player/components/Timeline.test.ts +3 -1
- package/src/player/components/TimelineCanvas.tsx +4 -0
- package/src/player/components/TimelineClip.tsx +20 -6
- package/src/player/components/TimelineClipFades.test.tsx +11 -0
- package/src/player/components/TimelineClipFades.tsx +4 -4
- package/src/player/components/TimelineClipJoins.tsx +23 -18
- package/src/player/components/TimelineGestureOverlay.tsx +30 -27
- package/src/player/components/TimelineGroupRow.tsx +1 -0
- package/src/player/components/TimelineLaneClip.tsx +127 -0
- package/src/player/components/TimelineLanes.test.tsx +51 -6
- package/src/player/components/TimelineLanes.tsx +166 -178
- package/src/player/components/TimelineRuler.tsx +2 -0
- package/src/player/components/TimelineTimeLayer.tsx +25 -0
- package/src/player/components/VideoThumbnail.test.tsx +204 -12
- package/src/player/components/VideoThumbnail.tsx +37 -9
- package/src/player/components/thumbnailUtils.test.ts +17 -3
- package/src/player/components/thumbnailUtils.ts +17 -1
- package/src/player/components/timelineLayout.test.ts +3 -3
- package/src/player/components/timelineMotion.test.ts +27 -0
- package/src/player/components/timelineMotion.ts +62 -0
- package/src/player/components/timelineZoomInput.test.ts +438 -0
- package/src/player/components/timelineZoomInput.ts +456 -0
- package/src/player/components/useTimelineGeometry.ts +0 -3
- package/src/player/components/useTimelinePlayhead.test.tsx +97 -30
- package/src/player/components/useTimelinePlayhead.ts +53 -59
- package/src/player/components/useTimelineProviderState.tsx +16 -21
- package/src/player/components/useTimelineZoom.ts +7 -12
- package/src/player/hooks/useShadowPreviewReload.ts +6 -6
- package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +14 -0
- package/src/player/hooks/useTimelinePlayer.ts +8 -5
- package/src/player/lib/thumbnailScheduler.test.ts +20 -1
- package/src/player/lib/thumbnailScheduler.ts +1 -1
- package/src/player/lib/thumbnailVideoDecoder.test.ts +230 -30
- package/src/player/lib/thumbnailVideoDecoder.ts +197 -54
- package/src/player/lib/timelineElementHelpers.ts +0 -11
- package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
- package/src/player/lib/timelineViewportBudgets.ts +1 -1
- package/src/player/previewReloading.test.ts +48 -0
- package/src/player/previewReloading.ts +48 -0
- package/src/utils/gsapUndoRestore.browser.test.ts +4 -17
- package/src/utils/rootDuration.ts +5 -4
- package/src/utils/studioPendingEdits.test.ts +27 -0
- package/src/utils/studioPendingEdits.ts +9 -0
- package/src/utils/timelineAssetDrop.test.ts +13 -4
- package/src/utils/timelineAssetDrop.ts +6 -2
- package/dist/chunk-TDWGXBUT.js.map +0 -1
- /package/dist/{chunk-4S3AYCOG.js.map → chunk-H2DUOXTF.js.map} +0 -0
- /package/dist/{chunk-7UQWVN52.js.map → chunk-UYWON5AX.js.map} +0 -0
- /package/dist/{domEditingLayers-IEVLO54Z.js.map → domEditingLayers-KBTPQKGA.js.map} +0 -0
|
@@ -2,6 +2,17 @@ import { useRef, useCallback, useEffect, useLayoutEffect } from "react";
|
|
|
2
2
|
import { liveTime, usePlayerStore, type ZoomMode } from "../store/playerStore";
|
|
3
3
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
4
4
|
import { getPinchTimelineZoomPercent } from "./timelineZoom";
|
|
5
|
+
import {
|
|
6
|
+
currentTimelineZoomPercent,
|
|
7
|
+
requestTimelineZoom,
|
|
8
|
+
registerTimelineZoomViewport,
|
|
9
|
+
redrawTimelineZoomPreview,
|
|
10
|
+
settleTimelineZoom,
|
|
11
|
+
subscribeTimelineZoomPreview,
|
|
12
|
+
takeTimelineZoomAnchor,
|
|
13
|
+
timelineTimeAtX,
|
|
14
|
+
timelineZoomMapping,
|
|
15
|
+
} from "./timelineZoomInput";
|
|
5
16
|
import {
|
|
6
17
|
getTimelinePlaybackFollowScrollLeft,
|
|
7
18
|
getTimelineScrubTime,
|
|
@@ -36,17 +47,13 @@ interface UseTimelinePlayheadInput {
|
|
|
36
47
|
isDragging: React.RefObject<boolean>;
|
|
37
48
|
currentTime: number;
|
|
38
49
|
zoomMode: ZoomMode;
|
|
39
|
-
manualZoomPercent: number;
|
|
40
50
|
zoomModeRef: React.RefObject<ZoomMode>;
|
|
41
|
-
manualZoomPercentRef: React.RefObject<number>;
|
|
42
51
|
fitPps: number;
|
|
43
52
|
fitPpsRef: React.RefObject<number>;
|
|
44
53
|
effectiveDuration: number;
|
|
45
54
|
pps: number;
|
|
46
55
|
timelineReady: boolean;
|
|
47
56
|
elementsLength: number;
|
|
48
|
-
setZoomMode: (mode: ZoomMode) => void;
|
|
49
|
-
setManualZoomPercent: (percent: number) => void;
|
|
50
57
|
onSeek?: (time: number) => void;
|
|
51
58
|
contentOrigin: number;
|
|
52
59
|
}
|
|
@@ -60,28 +67,23 @@ export function useTimelinePlayhead({
|
|
|
60
67
|
currentTime,
|
|
61
68
|
zoomMode,
|
|
62
69
|
zoomModeRef,
|
|
63
|
-
manualZoomPercentRef,
|
|
64
70
|
fitPps: _fitPps,
|
|
65
71
|
fitPpsRef,
|
|
66
72
|
effectiveDuration,
|
|
67
73
|
pps,
|
|
68
74
|
timelineReady,
|
|
69
75
|
elementsLength,
|
|
70
|
-
setZoomMode,
|
|
71
|
-
setManualZoomPercent,
|
|
72
76
|
onSeek,
|
|
73
77
|
contentOrigin,
|
|
74
78
|
}: UseTimelinePlayheadInput) {
|
|
75
79
|
const dragScrollRaf = useRef(0);
|
|
76
80
|
const previousZoomModeRef = useRef<ZoomMode | null>(zoomMode);
|
|
77
|
-
// A
|
|
78
|
-
// The pinch handler anchors at the cursor instead, so it opts out via `skipCenterAnchorRef`.
|
|
81
|
+
// A zoom keeps its anchor (pinch: pointer, else playhead) in place; a resize keeps the centre.
|
|
79
82
|
const previousAnchorPpsRef = useRef(pps);
|
|
80
83
|
const userZoomCount = usePlayerStore((s) => s.userZoomCount);
|
|
81
84
|
const previousZoomCountRef = useRef(userZoomCount);
|
|
82
85
|
const lastLiveTimeRef = useRef(usePlayerStore.getState().currentTime);
|
|
83
86
|
const lastSeekCountRef = useRef(liveTime.seekCount());
|
|
84
|
-
const skipCenterAnchorRef = useRef(false);
|
|
85
87
|
const contentOriginRef = useRef(contentOrigin);
|
|
86
88
|
contentOriginRef.current = contentOrigin;
|
|
87
89
|
|
|
@@ -91,12 +93,15 @@ export function useTimelinePlayhead({
|
|
|
91
93
|
previousAnchorPpsRef.current = pps;
|
|
92
94
|
const prevZoomCount = previousZoomCountRef.current;
|
|
93
95
|
previousZoomCountRef.current = userZoomCount;
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
// Consumed even when pps didn't change (at the clamp), so it never lingers for a later zoom.
|
|
97
|
+
const anchor = takeTimelineZoomAnchor();
|
|
98
|
+
if (!scroll || pps === prevPps) return;
|
|
99
|
+
if (anchor) {
|
|
100
|
+
const maxScrollLeft = Math.max(0, scroll.scrollWidth - scroll.clientWidth);
|
|
101
|
+
const left = anchor.time * pps + contentOrigin - anchor.x;
|
|
102
|
+
scroll.scrollLeft = Math.max(0, Math.min(maxScrollLeft, left));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
100
105
|
const zoomed = userZoomCount !== prevZoomCount;
|
|
101
106
|
if (!zoomed && scroll.scrollLeft < 1) return;
|
|
102
107
|
const time = Math.max(0, lastLiveTimeRef.current);
|
|
@@ -121,10 +126,11 @@ export function useTimelinePlayhead({
|
|
|
121
126
|
const syncPlayheadPosition = useCallback(
|
|
122
127
|
(time: number) => {
|
|
123
128
|
if (!playheadRef.current || durationRef.current <= 0) return;
|
|
129
|
+
const at = timelineZoomMapping(ppsRef.current, contentOrigin);
|
|
124
130
|
playheadRef.current.style.transform = getTimelinePlayheadTransform(
|
|
125
131
|
time,
|
|
126
|
-
|
|
127
|
-
contentOrigin,
|
|
132
|
+
at.pps,
|
|
133
|
+
at.contentOrigin,
|
|
128
134
|
!usePlayerStore.getState().isPlaying,
|
|
129
135
|
);
|
|
130
136
|
},
|
|
@@ -158,14 +164,18 @@ export function useTimelinePlayhead({
|
|
|
158
164
|
useMountEffect(() => {
|
|
159
165
|
const place = (t: number, atRest: boolean) => {
|
|
160
166
|
if (!playheadRef.current || durationRef.current <= 0) return false;
|
|
167
|
+
const at = timelineZoomMapping(ppsRef.current, contentOriginRef.current);
|
|
161
168
|
playheadRef.current.style.transform = getTimelinePlayheadTransform(
|
|
162
169
|
t,
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
at.pps,
|
|
171
|
+
at.contentOrigin,
|
|
165
172
|
atRest,
|
|
166
173
|
);
|
|
167
174
|
return true;
|
|
168
175
|
};
|
|
176
|
+
const unsubPreview = subscribeTimelineZoomPreview(() =>
|
|
177
|
+
place(lastLiveTimeRef.current, !usePlayerStore.getState().isPlaying),
|
|
178
|
+
);
|
|
169
179
|
const dragging = () => isDragging.current || usePlayerStore.getState().beatDragging;
|
|
170
180
|
const unsubPlaying = usePlayerStore.subscribe((state, prev) => {
|
|
171
181
|
if (prev.isPlaying && !state.isPlaying) place(lastLiveTimeRef.current, true);
|
|
@@ -177,7 +187,8 @@ export function useTimelinePlayhead({
|
|
|
177
187
|
lastLiveTimeRef.current = t;
|
|
178
188
|
const playing = usePlayerStore.getState().isPlaying;
|
|
179
189
|
if (!place(t, !playing)) return;
|
|
180
|
-
const
|
|
190
|
+
const at = timelineZoomMapping(ppsRef.current, contentOriginRef.current);
|
|
191
|
+
const playheadX = at.contentOrigin + Math.max(0, t) * at.pps;
|
|
181
192
|
const scroll = scrollRef.current;
|
|
182
193
|
// Paused, only a seek scrolls: a reload's republish, frame-rounded, must not undo a person's scroll.
|
|
183
194
|
if (!scroll || dragging() || zoomModeRef.current === "fit" || (!playing && !sought)) return;
|
|
@@ -197,6 +208,7 @@ export function useTimelinePlayhead({
|
|
|
197
208
|
return () => {
|
|
198
209
|
unsub();
|
|
199
210
|
unsubPlaying();
|
|
211
|
+
unsubPreview();
|
|
200
212
|
};
|
|
201
213
|
});
|
|
202
214
|
|
|
@@ -237,62 +249,44 @@ export function useTimelinePlayhead({
|
|
|
237
249
|
[scrollRef, isDragging, zoomModeRef, seekFromX],
|
|
238
250
|
);
|
|
239
251
|
|
|
252
|
+
// Trackpad pinch arrives as ctrl+wheel; Cmd+wheel zooms too, as Mac editors do.
|
|
240
253
|
const handlePinchWheel = useCallback(
|
|
241
254
|
(e: WheelEvent) => {
|
|
242
|
-
if (!e.ctrlKey) return;
|
|
255
|
+
if (!e.ctrlKey && !e.metaKey) return;
|
|
243
256
|
const scroll = scrollRef.current;
|
|
244
257
|
if (!scroll || durationRef.current <= 0 || fitPpsRef.current <= 0 || ppsRef.current <= 0)
|
|
245
258
|
return;
|
|
246
259
|
e.preventDefault();
|
|
247
260
|
e.stopPropagation();
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
261
|
+
const x = e.clientX - scroll.getBoundingClientRect().left;
|
|
262
|
+
requestTimelineZoom(
|
|
263
|
+
getPinchTimelineZoomPercent(
|
|
264
|
+
e.deltaY,
|
|
265
|
+
"manual",
|
|
266
|
+
currentTimelineZoomPercent(),
|
|
267
|
+
fitPpsRef.current,
|
|
268
|
+
),
|
|
269
|
+
{ time: Math.max(0, timelineTimeAtX(x) ?? 0), x },
|
|
254
270
|
);
|
|
255
|
-
if (nextZoomPercent === manualZoomPercentRef.current && zoomModeRef.current === "manual")
|
|
256
|
-
return;
|
|
257
|
-
const nextPps = fitPpsRef.current * (nextZoomPercent / 100);
|
|
258
|
-
const nextScrollLeft = getTimelineScrollLeftForZoomAnchor({
|
|
259
|
-
pointerX: e.clientX - rect.left,
|
|
260
|
-
currentScrollLeft: scroll.scrollLeft,
|
|
261
|
-
contentOrigin,
|
|
262
|
-
currentPixelsPerSecond: ppsRef.current,
|
|
263
|
-
nextPixelsPerSecond: nextPps,
|
|
264
|
-
duration: durationRef.current,
|
|
265
|
-
});
|
|
266
|
-
// Pinch anchors at the cursor (below), so skip the center-anchor effect.
|
|
267
|
-
skipCenterAnchorRef.current = true;
|
|
268
|
-
setZoomMode("manual");
|
|
269
|
-
setManualZoomPercent(nextZoomPercent);
|
|
270
|
-
requestAnimationFrame(() => {
|
|
271
|
-
const maxScrollLeft = Math.max(0, scroll.scrollWidth - scroll.clientWidth);
|
|
272
|
-
scroll.scrollLeft = Math.min(maxScrollLeft, nextScrollLeft);
|
|
273
|
-
});
|
|
274
271
|
},
|
|
275
|
-
[
|
|
276
|
-
scrollRef,
|
|
277
|
-
durationRef,
|
|
278
|
-
fitPpsRef,
|
|
279
|
-
ppsRef,
|
|
280
|
-
zoomModeRef,
|
|
281
|
-
manualZoomPercentRef,
|
|
282
|
-
setManualZoomPercent,
|
|
283
|
-
setZoomMode,
|
|
284
|
-
contentOrigin,
|
|
285
|
-
],
|
|
272
|
+
[scrollRef, durationRef, fitPpsRef, ppsRef],
|
|
286
273
|
);
|
|
287
274
|
|
|
288
275
|
useEffect(() => {
|
|
289
276
|
const scroll = scrollRef.current;
|
|
290
277
|
if (!scroll) return;
|
|
291
278
|
scroll.addEventListener("wheel", handlePinchWheel, { passive: false, capture: true });
|
|
279
|
+
// A press meets the zoom it sees, not the one still waiting to be laid out.
|
|
280
|
+
scroll.addEventListener("pointerdown", settleTimelineZoom, { capture: true });
|
|
281
|
+
scroll.addEventListener("scroll", redrawTimelineZoomPreview, { passive: true });
|
|
282
|
+
const unregisterZoomViewport = registerTimelineZoomViewport({ scroll, contentOrigin });
|
|
292
283
|
return () => {
|
|
293
284
|
scroll.removeEventListener("wheel", handlePinchWheel, { capture: true });
|
|
285
|
+
scroll.removeEventListener("pointerdown", settleTimelineZoom, { capture: true });
|
|
286
|
+
scroll.removeEventListener("scroll", redrawTimelineZoomPreview);
|
|
287
|
+
unregisterZoomViewport();
|
|
294
288
|
};
|
|
295
|
-
}, [handlePinchWheel, scrollRef, timelineReady, elementsLength]);
|
|
289
|
+
}, [handlePinchWheel, scrollRef, timelineReady, elementsLength, contentOrigin]);
|
|
296
290
|
|
|
297
291
|
return { seekFromX, autoScrollDuringDrag, dragScrollRaf };
|
|
298
292
|
}
|
|
@@ -131,7 +131,7 @@ export function useTimelineProviderState({
|
|
|
131
131
|
const timelineSessionEpoch = usePlayerStore((s) => s.timelineSessionEpoch);
|
|
132
132
|
const sessionEpoch = sessionEpochProp ?? timelineSessionEpoch;
|
|
133
133
|
const setFocusedEaseSegment = usePlayerStore((s) => s.setFocusedEaseSegment);
|
|
134
|
-
const { zoomMode, manualZoomPercent
|
|
134
|
+
const { zoomMode, manualZoomPercent } = useTimelineZoom();
|
|
135
135
|
const playheadRef = useRef<HTMLDivElement>(null);
|
|
136
136
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
137
137
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
@@ -258,22 +258,21 @@ export function useTimelineProviderState({
|
|
|
258
258
|
timelineElements.length,
|
|
259
259
|
displayLayout.totalH,
|
|
260
260
|
]);
|
|
261
|
-
const { pps, fitPps, displayContentWidth, displayDuration, zoomModeRef
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
});
|
|
261
|
+
const { pps, fitPps, displayContentWidth, displayDuration, zoomModeRef } = useTimelineGeometry({
|
|
262
|
+
viewportWidth: viewport.clientWidth,
|
|
263
|
+
effectiveDuration,
|
|
264
|
+
zoomMode,
|
|
265
|
+
manualZoomPercent,
|
|
266
|
+
ppsRef,
|
|
267
|
+
fitPpsRef,
|
|
268
|
+
draggedClip,
|
|
269
|
+
resizingClip,
|
|
270
|
+
expandedElements: timelineElements,
|
|
271
|
+
isDragging,
|
|
272
|
+
scrollRef,
|
|
273
|
+
lastScrollLeftRef,
|
|
274
|
+
contentOrigin,
|
|
275
|
+
});
|
|
277
276
|
const timelineFocus = useTimelineLogicalFocus({
|
|
278
277
|
scrollRef,
|
|
279
278
|
tracks,
|
|
@@ -342,17 +341,13 @@ export function useTimelineProviderState({
|
|
|
342
341
|
isDragging,
|
|
343
342
|
currentTime,
|
|
344
343
|
zoomMode,
|
|
345
|
-
manualZoomPercent,
|
|
346
344
|
zoomModeRef,
|
|
347
|
-
manualZoomPercentRef,
|
|
348
345
|
fitPps,
|
|
349
346
|
fitPpsRef,
|
|
350
347
|
effectiveDuration,
|
|
351
348
|
pps,
|
|
352
349
|
timelineReady,
|
|
353
350
|
elementsLength: timelineElements.length,
|
|
354
|
-
setZoomMode,
|
|
355
|
-
setManualZoomPercent,
|
|
356
351
|
onSeek,
|
|
357
352
|
contentOrigin,
|
|
358
353
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// fallow-ignore-file dead-code
|
|
2
|
-
import { useCallback } from "react";
|
|
3
2
|
import { usePlayerStore, type ZoomMode } from "../store/playerStore";
|
|
3
|
+
import { cancelTimelineZoom, requestTimelineZoom } from "./timelineZoomInput";
|
|
4
4
|
|
|
5
5
|
export interface TimelineZoomState {
|
|
6
6
|
zoomMode: ZoomMode;
|
|
@@ -9,18 +9,13 @@ export interface TimelineZoomState {
|
|
|
9
9
|
setManualZoomPercent: (percent: number) => void;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/** Shared zoom-related store selectors used by Timeline and TimelineToolbar. */
|
|
13
12
|
export function useTimelineZoom(): TimelineZoomState {
|
|
14
13
|
const zoomMode = usePlayerStore((s) => s.zoomMode);
|
|
15
14
|
const manualZoomPercent = usePlayerStore((s) => s.manualZoomPercent);
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
},
|
|
23
|
-
[setStorePercent],
|
|
24
|
-
);
|
|
25
|
-
return { zoomMode, manualZoomPercent, setZoomMode, setManualZoomPercent };
|
|
15
|
+
const setStoreZoomMode = usePlayerStore((s) => s.setZoomMode);
|
|
16
|
+
const setZoomMode = (mode: ZoomMode) => {
|
|
17
|
+
cancelTimelineZoom();
|
|
18
|
+
setStoreZoomMode(mode);
|
|
19
|
+
};
|
|
20
|
+
return { zoomMode, manualZoomPercent, setZoomMode, setManualZoomPercent: requestTimelineZoom };
|
|
26
21
|
}
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type UseTimelineSyncCallbacksParams,
|
|
19
19
|
} from "./useTimelineSyncCallbacks";
|
|
20
20
|
import type { IframeWindow, PlaybackAdapter } from "../lib/playbackTypes";
|
|
21
|
-
import {
|
|
21
|
+
import { setPreviewReloading } from "../previewReloading";
|
|
22
22
|
import { announcePreviewPromoted } from "../sceneSwap";
|
|
23
23
|
import { afterStudioPendingEdits, isStudioEditSaving } from "../../utils/studioPendingEdits";
|
|
24
24
|
import { usePlayerStore } from "../store/playerStore";
|
|
@@ -110,7 +110,7 @@ export function useShadowPreviewReload({
|
|
|
110
110
|
isRefreshingRef.current = false;
|
|
111
111
|
pendingSeekRef.current = null;
|
|
112
112
|
setPreviewSlots(planShadowDiscard);
|
|
113
|
-
|
|
113
|
+
setPreviewReloading(false);
|
|
114
114
|
const message = `The preview did not reload (${cause}). The previous preview is still showing.`;
|
|
115
115
|
logReload("shadow-failed", { cause });
|
|
116
116
|
console.error(`[studio] ${message}`);
|
|
@@ -188,7 +188,7 @@ export function useShadowPreviewReload({
|
|
|
188
188
|
if (liveTime != null) handOverPlaybackRef.current(liveTime, playing);
|
|
189
189
|
announcePreviewPromoted({ retired, live: shadow });
|
|
190
190
|
onPromotedRef.current?.();
|
|
191
|
-
|
|
191
|
+
setPreviewReloading(false);
|
|
192
192
|
},
|
|
193
193
|
[
|
|
194
194
|
stopPendingShadow,
|
|
@@ -285,7 +285,7 @@ export function useShadowPreviewReload({
|
|
|
285
285
|
armReadyTimer(gen);
|
|
286
286
|
setPreviewSlots((prev) => planShadowReload(prev, gen, url));
|
|
287
287
|
// Thumbnails of the edit wait for the new preview instead of competing with it.
|
|
288
|
-
|
|
288
|
+
setPreviewReloading(true);
|
|
289
289
|
},
|
|
290
290
|
[stopPendingShadow, armReadyTimer, gestureSavesOnScreen],
|
|
291
291
|
);
|
|
@@ -300,7 +300,7 @@ export function useShadowPreviewReload({
|
|
|
300
300
|
pendingSeekRef.current = null;
|
|
301
301
|
usePlayerStore.getState().setTimelineReady(false);
|
|
302
302
|
setPreviewSlots(planShadowDiscard);
|
|
303
|
-
|
|
303
|
+
setPreviewReloading(false);
|
|
304
304
|
}, [stopPendingShadow, isRefreshingRef, pendingSeekRef]);
|
|
305
305
|
|
|
306
306
|
// Hiding the tab pauses the budget; showing it again restarts the full budget for a pending shadow.
|
|
@@ -317,7 +317,7 @@ export function useShadowPreviewReload({
|
|
|
317
317
|
return () => {
|
|
318
318
|
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
319
319
|
stopPendingShadow();
|
|
320
|
-
|
|
320
|
+
setPreviewReloading(false);
|
|
321
321
|
};
|
|
322
322
|
});
|
|
323
323
|
|
|
@@ -6,6 +6,7 @@ import { createRoot } from "react-dom/client";
|
|
|
6
6
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
7
|
import { SHADOW_READY_BUDGETS, SHADOW_READY_TIMEOUT_MS } from "./useShadowPreviewReload";
|
|
8
8
|
import { usePlayerStore } from "../store/playerStore";
|
|
9
|
+
import { isPreviewChanging } from "../previewReloading";
|
|
9
10
|
import { NLEProvider, useNLEContext, type NLEContextValue } from "../../components/nle/NLEContext";
|
|
10
11
|
import {
|
|
11
12
|
makeAdapterWindow,
|
|
@@ -752,6 +753,19 @@ describe("useTimelinePlayer scene swap", () => {
|
|
|
752
753
|
expect(roles(getApi())).toEqual(["live"]);
|
|
753
754
|
});
|
|
754
755
|
|
|
756
|
+
it("counts as a preview change until the swap lands, so a canvas press waits for the new scenes", async () => {
|
|
757
|
+
let reply: (r: Response) => void = () => {};
|
|
758
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(
|
|
759
|
+
() => new Promise<Response>((resolve) => (reply = resolve)),
|
|
760
|
+
);
|
|
761
|
+
const { getApi } = liveFilm(vi.fn(async () => {}));
|
|
762
|
+
act(() => getApi().refreshPlayer());
|
|
763
|
+
expect(isPreviewChanging()).toBe(true);
|
|
764
|
+
reply(new Response("<html>v2</html>"));
|
|
765
|
+
await settle();
|
|
766
|
+
expect(isPreviewChanging()).toBe(false);
|
|
767
|
+
});
|
|
768
|
+
|
|
755
769
|
it("falls back to the full reload when the preview refuses the swap", async () => {
|
|
756
770
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("<html>v2</html>"));
|
|
757
771
|
const { getApi } = liveFilm(async () => {
|
|
@@ -43,6 +43,7 @@ import { createPreviewMessageHandler } from "./previewMessageRouter";
|
|
|
43
43
|
import { timelineElementsChanged } from "./timelinePlayerSync";
|
|
44
44
|
import { safeContentDocument } from "./timelineSyncHydration";
|
|
45
45
|
import { sceneSwapFor } from "../sceneSwap";
|
|
46
|
+
import { whileScriptWrites } from "../previewReloading";
|
|
46
47
|
|
|
47
48
|
export interface UseTimelinePlayerOptions {
|
|
48
49
|
/** Runs right after a reloaded preview becomes the live iframe. */
|
|
@@ -504,11 +505,13 @@ export function useTimelinePlayer({
|
|
|
504
505
|
!!iframe.contentDocument && isStudioManualEditGestureLiveIn(iframe.contentDocument);
|
|
505
506
|
if (!swap || isRefreshingRef.current || swapWouldReplaceGestureNode)
|
|
506
507
|
return reloadWholeFilm(url.toString());
|
|
507
|
-
swap(url.toString(), isCurrent, cancel.signal).catch(
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
508
|
+
whileScriptWrites(() => swap(url.toString(), isCurrent, cancel.signal)).catch(
|
|
509
|
+
(error: unknown) => {
|
|
510
|
+
if (!isCurrent()) return;
|
|
511
|
+
logReload("scene-swap-refused", { reason: String(error) });
|
|
512
|
+
reloadWholeFilm(url.toString());
|
|
513
|
+
},
|
|
514
|
+
);
|
|
512
515
|
}, [reloadWholeFilm, previewGeneration]);
|
|
513
516
|
const pauseRef = useRef(pause);
|
|
514
517
|
pauseRef.current = pause;
|
|
@@ -219,6 +219,26 @@ describe("ThumbnailScheduler", () => {
|
|
|
219
219
|
expect(scheduler.getDiagnostics()).toMatchObject({ queued: 0, active: 0, leases: 0 });
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
+
it("starts nothing that a release later in the same batch drops", async () => {
|
|
223
|
+
const scheduler = new ThumbnailScheduler(
|
|
224
|
+
resolveTimelineViewportBudgets({ concurrentVideoDecodes: 1 }),
|
|
225
|
+
);
|
|
226
|
+
const video = (key: string, load: ThumbnailRequest["load"]) =>
|
|
227
|
+
scheduler.acquire(request(key, load, "visible", { kind: "video" }), vi.fn());
|
|
228
|
+
const activeLease = video("active", () => deferred<ThumbnailLoadedResult>().promise);
|
|
229
|
+
const droppedLoad = vi.fn(async () => result("dropped"));
|
|
230
|
+
const droppedLease = video("dropped", droppedLoad);
|
|
231
|
+
const keptLoad = vi.fn(async () => result("kept"));
|
|
232
|
+
video("kept", keptLoad);
|
|
233
|
+
|
|
234
|
+
// A timeline jump unmounts its clips in one commit, the loading one first.
|
|
235
|
+
activeLease.release();
|
|
236
|
+
droppedLease.release();
|
|
237
|
+
await flush();
|
|
238
|
+
expect(droppedLoad).not.toHaveBeenCalled();
|
|
239
|
+
expect(keptLoad).toHaveBeenCalledTimes(1);
|
|
240
|
+
});
|
|
241
|
+
|
|
222
242
|
it("frees an aborted job's slot at once, even when its loader ignores the abort", async () => {
|
|
223
243
|
const scheduler = new ThumbnailScheduler(
|
|
224
244
|
resolveTimelineViewportBudgets({ concurrentVideoDecodes: 1 }),
|
|
@@ -231,7 +251,6 @@ describe("ThumbnailScheduler", () => {
|
|
|
231
251
|
lease.release();
|
|
232
252
|
const nextLoad = vi.fn(async () => result("next"));
|
|
233
253
|
scheduler.acquire(request("next", nextLoad, "visible", { kind: "video" }), vi.fn());
|
|
234
|
-
await flush();
|
|
235
254
|
expect(nextLoad).toHaveBeenCalledTimes(1);
|
|
236
255
|
});
|
|
237
256
|
|
|
@@ -316,7 +316,7 @@ export class ThumbnailScheduler {
|
|
|
316
316
|
slotFree = true;
|
|
317
317
|
this.activeByBucket[bucket]--;
|
|
318
318
|
this.activeByKind[entry.request.kind]--;
|
|
319
|
-
this.pump();
|
|
319
|
+
queueMicrotask(() => this.pump());
|
|
320
320
|
};
|
|
321
321
|
controller.signal.addEventListener("abort", freeSlot, { once: true });
|
|
322
322
|
this.notify(entry);
|