@hyperframes/studio 0.6.48 → 0.6.49
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/{index-B2QGnquo.js → index-B4Cr7MVx.js} +27 -27
- package/dist/index.html +1 -1
- package/package.json +4 -4
- package/src/components/editor/DomEditOverlay.test.ts +3 -2
- package/src/components/editor/DomEditOverlay.tsx +4 -7
- package/src/components/editor/LayersPanel.tsx +8 -7
- package/src/components/editor/domEditing.test.ts +58 -43
- package/src/components/editor/domEditingLayers.ts +56 -5
- package/src/components/editor/useDomEditOverlayGestures.ts +1 -1
- package/src/hooks/useDomEditCommits.ts +10 -1
- package/src/hooks/useDomEditSession.ts +4 -4
- package/src/hooks/useDomEditTextCommits.ts +3 -3
- package/src/hooks/useDomSelection.ts +28 -16
- package/src/hooks/usePreviewInteraction.ts +7 -6
- package/src/hooks/useStudioUrlState.ts +4 -3
- package/src/player/lib/playbackAdapter.test.ts +165 -2
- package/src/player/lib/playbackAdapter.ts +12 -4
- package/src/utils/studioUrlState.test.ts +6 -4
|
@@ -38,7 +38,7 @@ export interface UseDomEditTextCommitsParams {
|
|
|
38
38
|
buildDomSelectionFromTarget: (
|
|
39
39
|
target: HTMLElement,
|
|
40
40
|
options?: { preferClipAncestor?: boolean },
|
|
41
|
-
) => DomEditSelection | null
|
|
41
|
+
) => Promise<DomEditSelection | null>;
|
|
42
42
|
persistDomEditOperations: PersistDomEditOperations;
|
|
43
43
|
resolveImportedFontAsset: (fontFamilyValue: string) => ImportedFontAsset | null;
|
|
44
44
|
}
|
|
@@ -231,7 +231,7 @@ export function useDomEditTextCommits({
|
|
|
231
231
|
if (doc) {
|
|
232
232
|
const refreshed = findElementForSelection(doc, domEditSelection, activeCompPath);
|
|
233
233
|
if (refreshed) {
|
|
234
|
-
const nextSelection = buildDomSelectionFromTarget(refreshed);
|
|
234
|
+
const nextSelection = await buildDomSelectionFromTarget(refreshed);
|
|
235
235
|
if (nextSelection) {
|
|
236
236
|
applyDomSelection(nextSelection, { revealPanel: false, preserveGroup: true });
|
|
237
237
|
}
|
|
@@ -287,7 +287,7 @@ export function useDomEditTextCommits({
|
|
|
287
287
|
if (doc) {
|
|
288
288
|
const refreshed = findElementForSelection(doc, selection, activeCompPath);
|
|
289
289
|
if (refreshed) {
|
|
290
|
-
const nextSelection = buildDomSelectionFromTarget(refreshed);
|
|
290
|
+
const nextSelection = await buildDomSelectionFromTarget(refreshed);
|
|
291
291
|
if (nextSelection) {
|
|
292
292
|
applyDomSelection(nextSelection, { revealPanel: false, preserveGroup: true });
|
|
293
293
|
}
|
|
@@ -60,17 +60,19 @@ export interface UseDomSelectionReturn {
|
|
|
60
60
|
buildDomSelectionFromTarget: (
|
|
61
61
|
target: HTMLElement,
|
|
62
62
|
options?: { preferClipAncestor?: boolean },
|
|
63
|
-
) => DomEditSelection | null
|
|
63
|
+
) => Promise<DomEditSelection | null>;
|
|
64
64
|
resolveDomSelectionFromPreviewPoint: (
|
|
65
65
|
clientX: number,
|
|
66
66
|
clientY: number,
|
|
67
67
|
options?: { preferClipAncestor?: boolean },
|
|
68
|
-
) => DomEditSelection | null
|
|
68
|
+
) => Promise<DomEditSelection | null>;
|
|
69
69
|
updateDomEditHoverSelection: (selection: DomEditSelection | null) => void;
|
|
70
|
-
buildDomSelectionForTimelineElement: (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
buildDomSelectionForTimelineElement: (
|
|
71
|
+
element: TimelineElement,
|
|
72
|
+
) => Promise<DomEditSelection | null>;
|
|
73
|
+
handleTimelineElementSelect: (element: TimelineElement | null) => Promise<void>;
|
|
74
|
+
refreshDomEditSelectionFromPreview: (selection: DomEditSelection) => Promise<void>;
|
|
75
|
+
refreshDomEditGroupSelectionsFromPreview: (selections: DomEditSelection[]) => Promise<void>;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
// ── Hook ──
|
|
@@ -193,24 +195,34 @@ export function useDomSelection({
|
|
|
193
195
|
}, [applyDomSelection]);
|
|
194
196
|
|
|
195
197
|
const buildDomSelectionFromTarget = useCallback(
|
|
196
|
-
(
|
|
198
|
+
(
|
|
199
|
+
target: HTMLElement,
|
|
200
|
+
options?: { preferClipAncestor?: boolean; skipSourceProbe?: boolean },
|
|
201
|
+
) => {
|
|
197
202
|
return resolveDomEditSelection(target, {
|
|
198
203
|
activeCompositionPath: activeCompPath,
|
|
199
204
|
isMasterView,
|
|
200
205
|
preferClipAncestor: options?.preferClipAncestor,
|
|
206
|
+
skipSourceProbe: options?.skipSourceProbe,
|
|
207
|
+
projectId,
|
|
201
208
|
});
|
|
202
209
|
},
|
|
203
|
-
[activeCompPath, isMasterView],
|
|
210
|
+
[activeCompPath, isMasterView, projectId],
|
|
204
211
|
);
|
|
205
212
|
|
|
206
213
|
const resolveDomSelectionFromPreviewPoint = useCallback(
|
|
207
|
-
(
|
|
214
|
+
async (
|
|
215
|
+
clientX: number,
|
|
216
|
+
clientY: number,
|
|
217
|
+
options?: { preferClipAncestor?: boolean; skipSourceProbe?: boolean },
|
|
218
|
+
) => {
|
|
208
219
|
const iframe = previewIframeRef.current;
|
|
209
220
|
if (!iframe || captionEditMode) return null;
|
|
210
221
|
const target = getPreviewTargetFromPointer(iframe, clientX, clientY, activeCompPath);
|
|
211
222
|
if (!target) return null;
|
|
212
223
|
return buildDomSelectionFromTarget(target, {
|
|
213
224
|
preferClipAncestor: options?.preferClipAncestor,
|
|
225
|
+
skipSourceProbe: options?.skipSourceProbe,
|
|
214
226
|
});
|
|
215
227
|
},
|
|
216
228
|
[activeCompPath, buildDomSelectionFromTarget, captionEditMode, previewIframeRef],
|
|
@@ -223,7 +235,7 @@ export function useDomSelection({
|
|
|
223
235
|
}, []);
|
|
224
236
|
|
|
225
237
|
const buildDomSelectionForTimelineElement = useCallback(
|
|
226
|
-
(element: TimelineElement): DomEditSelection | null => {
|
|
238
|
+
async (element: TimelineElement): Promise<DomEditSelection | null> => {
|
|
227
239
|
const iframe = previewIframeRef.current;
|
|
228
240
|
let doc: Document | null = null;
|
|
229
241
|
try {
|
|
@@ -248,21 +260,21 @@ export function useDomSelection({
|
|
|
248
260
|
);
|
|
249
261
|
|
|
250
262
|
const handleTimelineElementSelect = useCallback(
|
|
251
|
-
(element: TimelineElement | null) => {
|
|
263
|
+
async (element: TimelineElement | null) => {
|
|
252
264
|
if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
|
|
253
265
|
if (!element) {
|
|
254
266
|
applyDomSelection(null, { revealPanel: false });
|
|
255
267
|
return;
|
|
256
268
|
}
|
|
257
269
|
|
|
258
|
-
const selection = buildDomSelectionForTimelineElement(element);
|
|
270
|
+
const selection = await buildDomSelectionForTimelineElement(element);
|
|
259
271
|
if (selection) applyDomSelection(selection);
|
|
260
272
|
},
|
|
261
273
|
[applyDomSelection, buildDomSelectionForTimelineElement],
|
|
262
274
|
);
|
|
263
275
|
|
|
264
276
|
const refreshDomEditSelectionFromPreview = useCallback(
|
|
265
|
-
(selection: DomEditSelection) => {
|
|
277
|
+
async (selection: DomEditSelection) => {
|
|
266
278
|
const iframe = previewIframeRef.current;
|
|
267
279
|
let doc: Document | null = null;
|
|
268
280
|
try {
|
|
@@ -275,7 +287,7 @@ export function useDomSelection({
|
|
|
275
287
|
const element = findElementForSelection(doc, selection, activeCompPath);
|
|
276
288
|
if (!element) return;
|
|
277
289
|
|
|
278
|
-
const nextSelection = buildDomSelectionFromTarget(element);
|
|
290
|
+
const nextSelection = await buildDomSelectionFromTarget(element);
|
|
279
291
|
if (nextSelection) {
|
|
280
292
|
applyDomSelection(nextSelection, {
|
|
281
293
|
revealPanel: false,
|
|
@@ -287,7 +299,7 @@ export function useDomSelection({
|
|
|
287
299
|
);
|
|
288
300
|
|
|
289
301
|
const refreshDomEditGroupSelectionsFromPreview = useCallback(
|
|
290
|
-
(selections: DomEditSelection[]) => {
|
|
302
|
+
async (selections: DomEditSelection[]) => {
|
|
291
303
|
const iframe = previewIframeRef.current;
|
|
292
304
|
let doc: Document | null = null;
|
|
293
305
|
try {
|
|
@@ -301,7 +313,7 @@ export function useDomSelection({
|
|
|
301
313
|
for (const selection of selections) {
|
|
302
314
|
const element = findElementForSelection(doc, selection, activeCompPath);
|
|
303
315
|
if (!element) continue;
|
|
304
|
-
const nextSelection = buildDomSelectionFromTarget(element);
|
|
316
|
+
const nextSelection = await buildDomSelectionFromTarget(element);
|
|
305
317
|
if (nextSelection) nextGroup.push(nextSelection);
|
|
306
318
|
}
|
|
307
319
|
if (nextGroup.length === 0) return;
|
|
@@ -20,8 +20,8 @@ export interface UsePreviewInteractionParams {
|
|
|
20
20
|
resolveDomSelectionFromPreviewPoint: (
|
|
21
21
|
clientX: number,
|
|
22
22
|
clientY: number,
|
|
23
|
-
options?: { preferClipAncestor?: boolean },
|
|
24
|
-
) => DomEditSelection | null
|
|
23
|
+
options?: { preferClipAncestor?: boolean; skipSourceProbe?: boolean },
|
|
24
|
+
) => Promise<DomEditSelection | null>;
|
|
25
25
|
updateDomEditHoverSelection: (selection: DomEditSelection | null) => void;
|
|
26
26
|
|
|
27
27
|
onClickToSource?: (selection: DomEditSelection) => void;
|
|
@@ -40,9 +40,9 @@ export function usePreviewInteraction({
|
|
|
40
40
|
onClickToSource,
|
|
41
41
|
}: UsePreviewInteractionParams) {
|
|
42
42
|
const handlePreviewCanvasMouseDown = useCallback(
|
|
43
|
-
(e: React.MouseEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
|
|
43
|
+
async (e: React.MouseEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
|
|
44
44
|
if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode || compositionLoading) return;
|
|
45
|
-
const nextSelection = resolveDomSelectionFromPreviewPoint(e.clientX, e.clientY, {
|
|
45
|
+
const nextSelection = await resolveDomSelectionFromPreviewPoint(e.clientX, e.clientY, {
|
|
46
46
|
preferClipAncestor: options?.preferClipAncestor ?? false,
|
|
47
47
|
});
|
|
48
48
|
if (!nextSelection) {
|
|
@@ -66,14 +66,15 @@ export function usePreviewInteraction({
|
|
|
66
66
|
);
|
|
67
67
|
|
|
68
68
|
const handlePreviewCanvasPointerMove = useCallback(
|
|
69
|
-
(e: React.PointerEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
|
|
69
|
+
async (e: React.PointerEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
|
|
70
70
|
if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode || compositionLoading) {
|
|
71
71
|
updateDomEditHoverSelection(null);
|
|
72
72
|
return null;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
const nextSelection = resolveDomSelectionFromPreviewPoint(e.clientX, e.clientY, {
|
|
75
|
+
const nextSelection = await resolveDomSelectionFromPreviewPoint(e.clientX, e.clientY, {
|
|
76
76
|
preferClipAncestor: options?.preferClipAncestor ?? false,
|
|
77
|
+
skipSourceProbe: true,
|
|
77
78
|
});
|
|
78
79
|
updateDomEditHoverSelection(nextSelection);
|
|
79
80
|
return nextSelection;
|
|
@@ -25,7 +25,7 @@ interface UseStudioUrlStateParams {
|
|
|
25
25
|
buildDomSelectionFromTarget: (
|
|
26
26
|
target: HTMLElement,
|
|
27
27
|
options?: { preferClipAncestor?: boolean },
|
|
28
|
-
) => DomEditSelection | null
|
|
28
|
+
) => Promise<DomEditSelection | null>;
|
|
29
29
|
applyDomSelection: (
|
|
30
30
|
selection: DomEditSelection | null,
|
|
31
31
|
options?: {
|
|
@@ -140,10 +140,11 @@ export function useStudioUrlState({
|
|
|
140
140
|
return;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
const selection = buildDomSelectionFromTarget(element, { preferClipAncestor: false });
|
|
144
|
-
applyDomSelection(selection, { revealPanel: false });
|
|
145
143
|
hydratedSelectionRef.current = true;
|
|
146
144
|
pendingSelectionRef.current = null;
|
|
145
|
+
void buildDomSelectionFromTarget(element, { preferClipAncestor: false }).then((selection) => {
|
|
146
|
+
applyDomSelection(selection, { revealPanel: false });
|
|
147
|
+
});
|
|
147
148
|
}, [
|
|
148
149
|
activeCompPath,
|
|
149
150
|
applyDomSelection,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { wrapTimeline } from "./playbackAdapter";
|
|
3
|
-
import type {
|
|
2
|
+
import { createStaticSeekPlaybackAdapter, wrapTimeline } from "./playbackAdapter";
|
|
3
|
+
import type {
|
|
4
|
+
RuntimePlaybackAdapter,
|
|
5
|
+
StaticSeekPlaybackClock,
|
|
6
|
+
TimelineLike,
|
|
7
|
+
} from "./playbackTypes";
|
|
4
8
|
|
|
5
9
|
describe("wrapTimeline seek keepPlaying option (#834)", () => {
|
|
6
10
|
function mockTimeline(): TimelineLike & {
|
|
@@ -48,3 +52,162 @@ describe("wrapTimeline seek keepPlaying option (#834)", () => {
|
|
|
48
52
|
expect(tl.seek).toHaveBeenCalledWith(5);
|
|
49
53
|
});
|
|
50
54
|
});
|
|
55
|
+
|
|
56
|
+
describe("createStaticSeekPlaybackAdapter seek keepPlaying option", () => {
|
|
57
|
+
type StaticSeekPlayer = Pick<RuntimePlaybackAdapter, "getTime"> &
|
|
58
|
+
Partial<Pick<RuntimePlaybackAdapter, "renderSeek" | "seek">>;
|
|
59
|
+
|
|
60
|
+
function makeFakeClock(): StaticSeekPlaybackClock & {
|
|
61
|
+
runNextFrame: () => boolean;
|
|
62
|
+
cancelled: number[];
|
|
63
|
+
scheduled: number;
|
|
64
|
+
setNow: (ms: number) => void;
|
|
65
|
+
} {
|
|
66
|
+
let now = 0;
|
|
67
|
+
let nextHandle = 0;
|
|
68
|
+
const pending = new Map<number, FrameRequestCallback>();
|
|
69
|
+
const cancelled: number[] = [];
|
|
70
|
+
let scheduled = 0;
|
|
71
|
+
return {
|
|
72
|
+
now: () => now,
|
|
73
|
+
requestAnimationFrame: (cb) => {
|
|
74
|
+
nextHandle += 1;
|
|
75
|
+
pending.set(nextHandle, cb);
|
|
76
|
+
scheduled += 1;
|
|
77
|
+
return nextHandle;
|
|
78
|
+
},
|
|
79
|
+
cancelAnimationFrame: (handle) => {
|
|
80
|
+
if (pending.delete(handle)) cancelled.push(handle);
|
|
81
|
+
},
|
|
82
|
+
runNextFrame: () => {
|
|
83
|
+
const next = pending.entries().next();
|
|
84
|
+
if (next.done) return false;
|
|
85
|
+
const [handle, cb] = next.value;
|
|
86
|
+
pending.delete(handle);
|
|
87
|
+
cb(now);
|
|
88
|
+
return true;
|
|
89
|
+
},
|
|
90
|
+
cancelled,
|
|
91
|
+
get scheduled() {
|
|
92
|
+
return scheduled;
|
|
93
|
+
},
|
|
94
|
+
setNow: (ms) => {
|
|
95
|
+
now = ms;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function makePlayer(): StaticSeekPlayer & {
|
|
101
|
+
renderSeek: ReturnType<typeof vi.fn>;
|
|
102
|
+
} {
|
|
103
|
+
return {
|
|
104
|
+
getTime: () => 0,
|
|
105
|
+
renderSeek: vi.fn(),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
it("default seek stops the RAF ticker so the adapter reports paused", () => {
|
|
110
|
+
const clock = makeFakeClock();
|
|
111
|
+
const player = makePlayer();
|
|
112
|
+
const adapter = createStaticSeekPlaybackAdapter(player, 10, clock);
|
|
113
|
+
|
|
114
|
+
adapter.play();
|
|
115
|
+
expect(adapter.isPlaying()).toBe(true);
|
|
116
|
+
|
|
117
|
+
adapter.seek(5);
|
|
118
|
+
|
|
119
|
+
expect(adapter.isPlaying()).toBe(false);
|
|
120
|
+
expect(adapter.getTime()).toBe(5);
|
|
121
|
+
expect(player.renderSeek).toHaveBeenLastCalledWith(5);
|
|
122
|
+
expect(clock.cancelled.length).toBeGreaterThan(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("default seek prevents the ticker from advancing further", () => {
|
|
126
|
+
const clock = makeFakeClock();
|
|
127
|
+
const player = makePlayer();
|
|
128
|
+
const adapter = createStaticSeekPlaybackAdapter(player, 10, clock);
|
|
129
|
+
|
|
130
|
+
adapter.play();
|
|
131
|
+
player.renderSeek.mockClear();
|
|
132
|
+
|
|
133
|
+
adapter.seek(5);
|
|
134
|
+
|
|
135
|
+
// Any frame the RAF callback already had queued before cancel should be a no-op.
|
|
136
|
+
clock.setNow(1000);
|
|
137
|
+
clock.runNextFrame();
|
|
138
|
+
expect(player.renderSeek).toHaveBeenCalledTimes(1); // only the seek itself
|
|
139
|
+
expect(player.renderSeek).toHaveBeenLastCalledWith(5);
|
|
140
|
+
expect(adapter.getTime()).toBe(5);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("seek with { keepPlaying: true } preserves playback and rebases the ticker", () => {
|
|
144
|
+
const clock = makeFakeClock();
|
|
145
|
+
const player = makePlayer();
|
|
146
|
+
const adapter = createStaticSeekPlaybackAdapter(player, 10, clock);
|
|
147
|
+
|
|
148
|
+
adapter.play();
|
|
149
|
+
clock.setNow(500);
|
|
150
|
+
expect(adapter.isPlaying()).toBe(true);
|
|
151
|
+
|
|
152
|
+
adapter.seek(3, { keepPlaying: true });
|
|
153
|
+
|
|
154
|
+
expect(adapter.isPlaying()).toBe(true);
|
|
155
|
+
expect(adapter.getTime()).toBe(3);
|
|
156
|
+
|
|
157
|
+
// Advance 1s of wall-clock time. With playStartTime rebased to 3 and
|
|
158
|
+
// playStartNow rebased to 500, the next tick should render around t=4.
|
|
159
|
+
clock.setNow(1500);
|
|
160
|
+
clock.runNextFrame();
|
|
161
|
+
expect(player.renderSeek).toHaveBeenLastCalledWith(4);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("seek with { keepPlaying: false } pauses (matches default)", () => {
|
|
165
|
+
const clock = makeFakeClock();
|
|
166
|
+
const player = makePlayer();
|
|
167
|
+
const adapter = createStaticSeekPlaybackAdapter(player, 10, clock);
|
|
168
|
+
|
|
169
|
+
adapter.play();
|
|
170
|
+
adapter.seek(5, { keepPlaying: false });
|
|
171
|
+
|
|
172
|
+
expect(adapter.isPlaying()).toBe(false);
|
|
173
|
+
expect(player.renderSeek).toHaveBeenLastCalledWith(5);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("seek with { keepPlaying: true } does not force playback when adapter is paused", () => {
|
|
177
|
+
const clock = makeFakeClock();
|
|
178
|
+
const player = makePlayer();
|
|
179
|
+
const adapter = createStaticSeekPlaybackAdapter(player, 10, clock);
|
|
180
|
+
|
|
181
|
+
adapter.seek(2, { keepPlaying: true });
|
|
182
|
+
|
|
183
|
+
expect(adapter.isPlaying()).toBe(false);
|
|
184
|
+
expect(adapter.getTime()).toBe(2);
|
|
185
|
+
expect(player.renderSeek).toHaveBeenLastCalledWith(2);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("seek without options stays back-compatible with the previous signature", () => {
|
|
189
|
+
const clock = makeFakeClock();
|
|
190
|
+
const player = makePlayer();
|
|
191
|
+
const adapter = createStaticSeekPlaybackAdapter(player, 10, clock);
|
|
192
|
+
|
|
193
|
+
// Caller written before the options parameter existed.
|
|
194
|
+
adapter.seek(4);
|
|
195
|
+
|
|
196
|
+
expect(player.renderSeek).toHaveBeenLastCalledWith(4);
|
|
197
|
+
expect(adapter.getTime()).toBe(4);
|
|
198
|
+
expect(adapter.isPlaying()).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("default seek clamps to duration and still pauses", () => {
|
|
202
|
+
const clock = makeFakeClock();
|
|
203
|
+
const player = makePlayer();
|
|
204
|
+
const adapter = createStaticSeekPlaybackAdapter(player, 10, clock);
|
|
205
|
+
|
|
206
|
+
adapter.play();
|
|
207
|
+
adapter.seek(99);
|
|
208
|
+
|
|
209
|
+
expect(adapter.getTime()).toBe(10);
|
|
210
|
+
expect(player.renderSeek).toHaveBeenLastCalledWith(10);
|
|
211
|
+
expect(adapter.isPlaying()).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -113,12 +113,20 @@ export function createStaticSeekPlaybackAdapter(
|
|
|
113
113
|
playing = false;
|
|
114
114
|
stopTicker();
|
|
115
115
|
},
|
|
116
|
-
seek: (time) => {
|
|
116
|
+
seek: (time, options) => {
|
|
117
117
|
renderSeek(time);
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
if (options?.keepPlaying) {
|
|
119
|
+
if (playing) {
|
|
120
|
+
playStartTime = currentTime;
|
|
121
|
+
playStartNow = clock.now();
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
121
124
|
}
|
|
125
|
+
// Default seek aligns with wrapTimeline: stop the RAF ticker so the
|
|
126
|
+
// adapter's `playing` flag matches the public seek contract instead of
|
|
127
|
+
// silently driving renderSeek in the background.
|
|
128
|
+
playing = false;
|
|
129
|
+
stopTicker();
|
|
122
130
|
},
|
|
123
131
|
getTime: () => currentTime,
|
|
124
132
|
getDuration: () => safeDuration,
|
|
@@ -53,7 +53,7 @@ function renderStudioUrlStateHarness(
|
|
|
53
53
|
timelineVisible: true,
|
|
54
54
|
activeCompPathHydrated: true,
|
|
55
55
|
domEditSelection: null,
|
|
56
|
-
buildDomSelectionFromTarget: () => null,
|
|
56
|
+
buildDomSelectionFromTarget: () => Promise.resolve(null),
|
|
57
57
|
applyDomSelection: () => {},
|
|
58
58
|
initialState: {
|
|
59
59
|
activeCompPath: null,
|
|
@@ -162,7 +162,7 @@ describe("studio url state", () => {
|
|
|
162
162
|
expect(normalizeStudioUrlPanelTab("motion", { motionPanelEnabled: false })).toBe("design");
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
it("hydrates seek first, preserves the initial url state, then restores selection", () => {
|
|
165
|
+
it("hydrates seek first, preserves the initial url state, then restores selection", async () => {
|
|
166
166
|
vi.useFakeTimers();
|
|
167
167
|
window.history.replaceState(null, "", "#project/demo?t=4.2&tab=design&selId=hero");
|
|
168
168
|
const requestSeek = vi.fn();
|
|
@@ -209,7 +209,7 @@ describe("studio url state", () => {
|
|
|
209
209
|
rightPanelTab: "design",
|
|
210
210
|
rightCollapsed: false,
|
|
211
211
|
applyDomSelection,
|
|
212
|
-
buildDomSelectionFromTarget: () => restoredSelection,
|
|
212
|
+
buildDomSelectionFromTarget: () => Promise.resolve(restoredSelection),
|
|
213
213
|
initialState: {
|
|
214
214
|
activeCompPath: null,
|
|
215
215
|
currentTime: 4.2,
|
|
@@ -232,8 +232,10 @@ describe("studio url state", () => {
|
|
|
232
232
|
expect(applyDomSelection).not.toHaveBeenCalled();
|
|
233
233
|
|
|
234
234
|
harness.rerender({ currentTime: 4.2 });
|
|
235
|
-
act(() => {
|
|
235
|
+
await act(async () => {
|
|
236
236
|
vi.advanceTimersByTime(250);
|
|
237
|
+
// Flush microtasks so the async buildDomSelectionFromTarget Promise resolves
|
|
238
|
+
await Promise.resolve();
|
|
237
239
|
});
|
|
238
240
|
expect(applyDomSelection).toHaveBeenCalledWith(restoredSelection, { revealPanel: false });
|
|
239
241
|
|