@hyperframes/studio 0.7.57 → 0.7.58
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--Z69cEkE.js → hyperframes-player-CtTDO63S.js} +1 -1
- package/dist/assets/{index-DfmYkU44.js → index-B_UvTX3E.js} +204 -204
- package/dist/assets/{index-KsfE1bUu.js → index-C47jAC3Q.js} +1 -1
- package/dist/assets/{index-Bf1x1y8H.js → index-DeQPzqwH.js} +1 -1
- package/dist/assets/index-uahwWkgw.css +1 -0
- package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
- package/dist/chunk-OBAG3GWK.js.map +1 -0
- package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
- package/dist/index.d.ts +15 -4
- package/dist/index.html +2 -2
- package/dist/index.js +5954 -4474
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +3 -3
- package/src/components/StudioFeedbackBar.tsx +2 -2
- package/src/components/editor/CanvasContextMenu.test.tsx +27 -14
- package/src/components/editor/CanvasContextMenu.tsx +71 -6
- package/src/components/editor/DomEditOverlay.tsx +5 -2
- package/src/components/editor/LayersPanel.tsx +110 -99
- package/src/components/editor/canvasContextMenuZOrder.test.ts +57 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +40 -1
- package/src/components/editor/domEditingDom.ts +5 -6
- package/src/components/editor/domEditingElement.ts +15 -32
- package/src/components/editor/layersPanelSort.ts +80 -0
- package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
- package/src/components/editor/useLayerRevealOverride.ts +389 -0
- package/src/components/nle/PreviewOverlays.tsx +27 -2
- package/src/components/nle/TimelinePane.test.ts +3 -0
- package/src/components/nle/TimelinePane.tsx +11 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
- package/src/components/nle/zLaneGesture.test.ts +176 -0
- package/src/components/nle/zLaneGesture.ts +77 -0
- package/src/hooks/domEditCommitTypes.ts +17 -1
- package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
- package/src/hooks/fetchStubTestUtils.ts +16 -0
- package/src/hooks/timelineEditingHelpers.test.ts +92 -1
- package/src/hooks/timelineEditingHelpers.ts +32 -15
- package/src/hooks/timelineMoveAdapter.ts +2 -1
- package/src/hooks/timelineTimingSync.test.ts +703 -33
- package/src/hooks/timelineTimingSync.ts +282 -97
- package/src/hooks/timelineTrackVisibility.test.ts +3 -1
- package/src/hooks/timelineTrackVisibility.ts +4 -3
- package/src/hooks/useClipboard.ts +19 -17
- package/src/hooks/useDomEditCommits.test.tsx +316 -38
- package/src/hooks/useDomEditCommits.ts +143 -78
- package/src/hooks/useDomEditSession.test.tsx +3 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useElementLifecycleOps.test.tsx +426 -60
- package/src/hooks/useElementLifecycleOps.ts +174 -81
- package/src/hooks/usePreviewPersistence.ts +2 -2
- package/src/hooks/useTimelineEditing.test.tsx +302 -154
- package/src/hooks/useTimelineEditing.ts +97 -93
- package/src/hooks/useTimelineEditingTypes.ts +1 -1
- package/src/hooks/useTimelineGroupEditing.ts +35 -3
- package/src/player/components/Timeline.test.ts +31 -21
- package/src/player/components/Timeline.tsx +62 -91
- package/src/player/components/TimelineCanvas.tsx +36 -5
- package/src/player/components/TimelineLanes.tsx +32 -10
- package/src/player/components/TimelineOverlays.tsx +36 -0
- package/src/player/components/TimelineRuler.tsx +18 -5
- package/src/player/components/TrackGapContextMenu.tsx +116 -0
- package/src/player/components/timelineCallbacks.ts +4 -1
- package/src/player/components/timelineClipDragCommit.test.ts +279 -3
- package/src/player/components/timelineClipDragCommit.ts +173 -71
- package/src/player/components/timelineGapCommit.test.ts +163 -0
- package/src/player/components/timelineGapCommit.ts +103 -0
- package/src/player/components/timelineGaps.test.ts +221 -0
- package/src/player/components/timelineGaps.ts +182 -0
- package/src/player/components/timelineLayout.test.ts +2 -1
- package/src/player/components/timelineLayout.ts +22 -6
- package/src/player/components/timelineMarquee.test.ts +20 -10
- package/src/player/components/timelineMarquee.ts +9 -2
- package/src/player/components/timelineStackingSync.test.ts +42 -1
- package/src/player/components/timelineStackingSync.ts +31 -16
- package/src/player/components/timelineZMirror.test.ts +667 -0
- package/src/player/components/timelineZMirror.ts +393 -0
- package/src/player/components/timelineZoom.test.ts +2 -2
- package/src/player/components/timelineZoom.ts +7 -3
- package/src/player/components/useTimelineClipDrag.ts +1 -1
- package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
- package/src/player/components/useTimelineGapHighlights.ts +110 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -3
- package/src/player/components/useTimelineRangeSelection.ts +6 -4
- package/src/player/components/useTimelineScrollViewport.ts +73 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +0 -1
- package/src/player/components/useTimelineStackingSync.ts +5 -3
- package/src/player/components/useTimelineTrackDerivations.ts +38 -0
- package/src/player/components/useTrackGapMenu.ts +155 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
- package/src/player/hooks/useExpandedTimelineElements.ts +12 -1
- package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
- package/src/player/lib/layerOrdering.test.ts +1 -15
- package/src/player/lib/layerOrdering.ts +5 -28
- package/src/player/lib/timelineDOM.test.ts +72 -0
- package/src/player/lib/timelineDOM.ts +3 -0
- package/src/player/lib/timelineElementHelpers.ts +35 -11
- package/src/player/store/playerStore.ts +12 -1
- package/src/telemetry/events.test.ts +10 -0
- package/src/telemetry/events.ts +1 -0
- package/src/utils/domEditSaveQueue.ts +4 -3
- package/src/utils/editHistory.test.ts +82 -0
- package/src/utils/editHistory.ts +14 -2
- package/src/utils/gsapSoftReload.test.ts +54 -115
- package/src/utils/gsapSoftReload.ts +42 -152
- package/src/utils/gsapUndoRestore.test.ts +269 -0
- package/src/utils/gsapUndoRestore.ts +269 -0
- package/src/utils/sourceScopedSelectorIndex.ts +29 -0
- package/dist/assets/index-_pqzyxB1.css +0 -1
- package/dist/chunk-5QSIMBEJ.js.map +0 -1
- /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
useTimelineTrackVisibilityEditing,
|
|
32
32
|
} from "./timelineTrackVisibility";
|
|
33
33
|
import { useTimelineGroupEditing } from "./useTimelineGroupEditing";
|
|
34
|
+
import { serializeZLaneGesture } from "../components/nle/zLaneGesture";
|
|
34
35
|
import { sdkTimingPersist } from "../utils/sdkCutover";
|
|
35
36
|
import type { UseTimelineEditingOptions } from "./useTimelineEditingTypes";
|
|
36
37
|
|
|
@@ -127,105 +128,108 @@ export function useTimelineEditing({
|
|
|
127
128
|
const handleTimelineElementMove = useCallback(
|
|
128
129
|
// fallow-ignore-next-line complexity
|
|
129
130
|
(element: TimelineElement, updates: TimelineMoveUpdates) => {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
131
|
+
const commitMove = () => {
|
|
132
|
+
const targetPath = element.sourceFile || activeCompPath || "index.html";
|
|
133
|
+
const startChanged = updates.start !== element.start;
|
|
134
|
+
// A vertical-only lane move arrives with start unchanged but track changed
|
|
135
|
+
// (on this single-element path the drag commit has already folded the
|
|
136
|
+
// AUTHORED persist track into updates.track). It must persist like any
|
|
137
|
+
// other move — early-returning on !startChanged alone silently dropped
|
|
138
|
+
// the file write, so the lane snapped back on reload.
|
|
139
|
+
const trackChanged = updates.track !== element.track;
|
|
138
140
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
if (startChanged || trackChanged) {
|
|
142
|
+
const liveAttrs: Array<[string, string]> = [];
|
|
143
|
+
if (startChanged) {
|
|
144
|
+
liveAttrs.push(["data-start", formatTimelineAttributeNumber(updates.start)]);
|
|
145
|
+
}
|
|
146
|
+
if (trackChanged) {
|
|
147
|
+
liveAttrs.push(["data-track-index", formatTimelineAttributeNumber(updates.track)]);
|
|
148
|
+
}
|
|
149
|
+
patchIframeDomTiming(previewIframeRef.current, element, liveAttrs, activeCompPath);
|
|
146
150
|
}
|
|
147
|
-
patchIframeDomTiming(previewIframeRef.current, element, liveAttrs);
|
|
148
|
-
}
|
|
149
151
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
152
|
+
const reorderDone = applyTimelineStackingReorder({
|
|
153
|
+
element,
|
|
154
|
+
stackingReorder: updates.stackingReorder,
|
|
155
|
+
timelineElements,
|
|
156
|
+
iframe: previewIframeRef.current,
|
|
157
|
+
activeCompPath,
|
|
158
|
+
commit: handleDomZIndexReorderCommitRef?.current,
|
|
159
|
+
});
|
|
158
160
|
|
|
159
|
-
|
|
161
|
+
if (!startChanged && !trackChanged) return reorderDone;
|
|
160
162
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
163
|
+
// Snapshot the duration BEFORE the optimistic updates below so a failed
|
|
164
|
+
// persist can roll the readout + live root back (see captureDurationRollback).
|
|
165
|
+
const rollbackDuration = captureDurationRollback(previewIframeRef.current);
|
|
166
|
+
// needsExtension gates the SDK path (setTiming can't grow the root duration), so read the store BEFORE the readout sync below optimistically updates it.
|
|
167
|
+
const needsExtension = extendRootDurationIfNeeded(updates.start + element.duration);
|
|
168
|
+
// Optimistic duration readout: content-driven (grow AND shrink), from the just-patched live DOM. See syncPreviewContentDuration.
|
|
169
|
+
syncPreviewContentDuration(previewIframeRef.current);
|
|
168
170
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
targetPath,
|
|
190
|
-
domId: element.domId,
|
|
191
|
-
label: "Move timeline clip",
|
|
192
|
-
coalesceKey,
|
|
193
|
-
recordEdit,
|
|
194
|
-
edit: { kind: "shift", delta: updates.start - element.start },
|
|
195
|
-
}),
|
|
196
|
-
);
|
|
197
|
-
return reorderDone
|
|
198
|
-
.then(() => {
|
|
199
|
-
// The SDK setTiming path writes start only — a lane change must take
|
|
200
|
-
// the fallback, whose patch builder writes data-track-index too.
|
|
201
|
-
if (sdkSession && element.hfId && !needsExtension && !trackChanged) {
|
|
202
|
-
return sdkTimingPersist(
|
|
203
|
-
element.hfId,
|
|
171
|
+
const buildMovePatches: PersistTimelineEditInput["buildPatches"] = (original, target) => {
|
|
172
|
+
// Persist lane changes too — data-start-only writes let reload snap the lane back.
|
|
173
|
+
const track = trackChanged ? updates.track : undefined;
|
|
174
|
+
return buildTimelineMoveTimingPatch(
|
|
175
|
+
original,
|
|
176
|
+
target,
|
|
177
|
+
updates.start,
|
|
178
|
+
element.duration,
|
|
179
|
+
track,
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
const coalesceKey = `timeline-move:${element.hfId ?? element.id}`;
|
|
183
|
+
const moveFallback = () =>
|
|
184
|
+
enqueueEdit(element, "Move timeline clip", buildMovePatches, coalesceKey).then(() =>
|
|
185
|
+
// Soft-reload with the server's rewritten GSAP script — the timing-only move already patched
|
|
186
|
+
// DOM + store, so swapping the script avoids the all-clips flash; falls back to reloadPreview().
|
|
187
|
+
finishClipTimingFallback({
|
|
188
|
+
iframe: previewIframeRef.current,
|
|
189
|
+
reloadPreview,
|
|
190
|
+
projectId: projectIdRef.current,
|
|
204
191
|
targetPath,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
192
|
+
domId: element.domId,
|
|
193
|
+
label: "Move timeline clip",
|
|
194
|
+
coalesceKey,
|
|
195
|
+
recordEdit,
|
|
196
|
+
edit: { kind: "shift", delta: updates.start - element.start },
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
return reorderDone
|
|
200
|
+
.then(() => {
|
|
201
|
+
// The SDK setTiming path writes start only — a lane change must take
|
|
202
|
+
// the fallback, whose patch builder writes data-track-index too.
|
|
203
|
+
if (sdkSession && element.hfId && !needsExtension && !trackChanged) {
|
|
204
|
+
return sdkTimingPersist(
|
|
205
|
+
element.hfId,
|
|
206
|
+
targetPath,
|
|
207
|
+
{ start: updates.start },
|
|
208
|
+
sdkSession,
|
|
209
|
+
{
|
|
210
|
+
editHistory: { recordEdit },
|
|
211
|
+
writeProjectFile,
|
|
212
|
+
reloadPreview,
|
|
213
|
+
domEditSaveTimestampRef,
|
|
214
|
+
compositionPath: activeCompPath,
|
|
215
|
+
// Capture on-disk bytes as the undo `before` so undoing a timing move
|
|
216
|
+
// restores the file verbatim, not a normalized full-DOM re-emit.
|
|
217
|
+
readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
|
|
218
|
+
},
|
|
219
|
+
{ label: "Move timeline clip", coalesceKey },
|
|
220
|
+
).then((handled) => {
|
|
221
|
+
if (!handled) return moveFallback();
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return moveFallback();
|
|
225
|
+
})
|
|
226
|
+
.catch((error) => {
|
|
227
|
+
// Failed persist: revert the optimistic duration readout + live root.
|
|
228
|
+
rollbackDuration();
|
|
229
|
+
throw error;
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
return updates.stackingReorder ? serializeZLaneGesture(commitMove) : commitMove();
|
|
229
233
|
},
|
|
230
234
|
[
|
|
231
235
|
previewIframeRef,
|
|
@@ -261,7 +265,7 @@ export function useTimelineEditing({
|
|
|
261
265
|
: "data-media-start";
|
|
262
266
|
liveAttrs.push([liveAttr, formatTimelineAttributeNumber(updates.playbackStart)]);
|
|
263
267
|
}
|
|
264
|
-
patchIframeDomTiming(previewIframeRef.current, element, liveAttrs);
|
|
268
|
+
patchIframeDomTiming(previewIframeRef.current, element, liveAttrs, activeCompPath);
|
|
265
269
|
// Snapshot the duration BEFORE the optimistic updates below so a failed
|
|
266
270
|
// persist can roll the readout + live root back (see captureDurationRollback).
|
|
267
271
|
const rollbackDuration = captureDurationRollback(previewIframeRef.current);
|
|
@@ -23,7 +23,7 @@ export type TimelineZIndexReorderCommit = (
|
|
|
23
23
|
key?: string;
|
|
24
24
|
}>,
|
|
25
25
|
coalesceKey?: string,
|
|
26
|
-
) => Promise<void>;
|
|
26
|
+
) => Promise<import("./domEditCommitTypes").DomEditPatchBatchesResult | undefined | void>;
|
|
27
27
|
|
|
28
28
|
export interface UseTimelineEditingOptions {
|
|
29
29
|
projectId: string | null;
|
|
@@ -37,6 +37,8 @@ export interface TimelineGroupResizeChange {
|
|
|
37
37
|
export interface TimelineGroupCommitOptions {
|
|
38
38
|
beforeTiming?: Promise<void>;
|
|
39
39
|
coalesceKey?: string;
|
|
40
|
+
/** Per-entry undo coalesce window override (ms) — see EditHistoryEntry.coalesceMs. */
|
|
41
|
+
coalesceMs?: number;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
interface UseTimelineGroupEditingOptions {
|
|
@@ -137,6 +139,7 @@ export function useTimelineGroupEditing({
|
|
|
137
139
|
label: string,
|
|
138
140
|
batchChanges: PersistTimelineBatchChange[],
|
|
139
141
|
coalesceKey: string,
|
|
142
|
+
coalesceMs?: number,
|
|
140
143
|
) => {
|
|
141
144
|
await persistTimelineBatchEdit({
|
|
142
145
|
projectId,
|
|
@@ -148,6 +151,7 @@ export function useTimelineGroupEditing({
|
|
|
148
151
|
domEditSaveTimestampRef,
|
|
149
152
|
pendingTimelineEditPathRef,
|
|
150
153
|
coalesceKey,
|
|
154
|
+
coalesceMs,
|
|
151
155
|
});
|
|
152
156
|
forceReloadSdkSession?.();
|
|
153
157
|
},
|
|
@@ -176,6 +180,7 @@ export function useTimelineGroupEditing({
|
|
|
176
180
|
needsExtension: boolean;
|
|
177
181
|
label: string;
|
|
178
182
|
coalesceKey: string;
|
|
183
|
+
coalesceMs?: number;
|
|
179
184
|
}): Promise<boolean> => {
|
|
180
185
|
const sharedPath = allChangesSharePath(input.changes, activeCompPath);
|
|
181
186
|
const canUseSdk =
|
|
@@ -196,7 +201,7 @@ export function useTimelineGroupEditing({
|
|
|
196
201
|
compositionPath: activeCompPath,
|
|
197
202
|
readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
|
|
198
203
|
},
|
|
199
|
-
{ label: input.label, coalesceKey: input.coalesceKey },
|
|
204
|
+
{ label: input.label, coalesceKey: input.coalesceKey, coalesceMs: input.coalesceMs },
|
|
200
205
|
);
|
|
201
206
|
},
|
|
202
207
|
[
|
|
@@ -220,20 +225,38 @@ export function useTimelineGroupEditing({
|
|
|
220
225
|
if (change.track != null) {
|
|
221
226
|
attrs.push(["data-track-index", formatTimelineAttributeNumber(change.track)]);
|
|
222
227
|
}
|
|
223
|
-
patchIframeDomTiming(previewIframeRef.current, change.element, attrs);
|
|
228
|
+
patchIframeDomTiming(previewIframeRef.current, change.element, attrs, activeCompPath);
|
|
224
229
|
}
|
|
225
230
|
|
|
231
|
+
// TRACK-ONLY batch: every change keeps its start (moves never carry a
|
|
232
|
+
// duration change), so nothing timing-related changed — the batch only
|
|
233
|
+
// rewrites data-track-index, which the renderer never reads (documented
|
|
234
|
+
// in core runtime/timeline.ts; track is a studio lane concept). The live
|
|
235
|
+
// DOM patch above + the gesture owner's optimistic store update fully
|
|
236
|
+
// cover the UI, so after the persist there is nothing to GSAP-shift and
|
|
237
|
+
// nothing for the preview to recompute: skip the fallback below entirely.
|
|
238
|
+
// Running it anyway is what made the mirrored z-order lane move blink —
|
|
239
|
+
// a zero-delta batch yields no scriptText, and finishGroupTimingGsapFallback
|
|
240
|
+
// used to full-reload the iframe when there was no script to soft-swap
|
|
241
|
+
// (it now rebinds the runtime timing in place, but a track-only batch
|
|
242
|
+
// needs NO preview sync at all, so the skip stays).
|
|
243
|
+
const trackOnly = changes.every((change) => change.start === change.element.start);
|
|
244
|
+
|
|
226
245
|
const maxEnd = Math.max(...changes.map((change) => change.start + change.element.duration));
|
|
227
246
|
// Snapshot the duration BEFORE the optimistic updates below so a failed
|
|
228
247
|
// persist can roll the readout + live root back (see captureDurationRollback).
|
|
229
248
|
const rollbackDuration = captureDurationRollback(previewIframeRef.current);
|
|
230
249
|
// needsExtension gates the SDK path (setTiming can't grow the root duration),
|
|
231
250
|
// so read the store BEFORE the readout sync below optimistically updates it.
|
|
251
|
+
// Track-only batches leave every clip end unchanged, so both this and the
|
|
252
|
+
// readout sync below are provable no-ops there — kept unconditional so the
|
|
253
|
+
// duration machinery stays on one code path.
|
|
232
254
|
const needsExtension = extendRootDurationIfNeeded(maxEnd);
|
|
233
255
|
// Optimistic duration readout: content-driven (grow AND shrink), read from
|
|
234
256
|
// the just-patched live DOM. See syncPreviewContentDuration.
|
|
235
257
|
syncPreviewContentDuration(previewIframeRef.current);
|
|
236
258
|
const coalesceKey = options?.coalesceKey ?? moveCoalesceKey(changes);
|
|
259
|
+
const coalesceMs = options?.coalesceMs;
|
|
237
260
|
return enqueueGroupOperation("Move timeline clips", async (projectId) => {
|
|
238
261
|
await options?.beforeTiming;
|
|
239
262
|
const handledBySdk = await trySdkBatchPersist({
|
|
@@ -243,6 +266,7 @@ export function useTimelineGroupEditing({
|
|
|
243
266
|
needsExtension,
|
|
244
267
|
label: "Move timeline clips",
|
|
245
268
|
coalesceKey,
|
|
269
|
+
coalesceMs,
|
|
246
270
|
});
|
|
247
271
|
if (handledBySdk) return;
|
|
248
272
|
|
|
@@ -261,7 +285,12 @@ export function useTimelineGroupEditing({
|
|
|
261
285
|
),
|
|
262
286
|
})),
|
|
263
287
|
coalesceKey,
|
|
288
|
+
coalesceMs,
|
|
264
289
|
);
|
|
290
|
+
// Track-only: no timing delta → no GSAP positions to shift and no
|
|
291
|
+
// reload (see the trackOnly doc above). Mixed batches (any start
|
|
292
|
+
// change) keep the full fallback below.
|
|
293
|
+
if (trackOnly) return;
|
|
265
294
|
await finishGroupTimingGsapFallback({
|
|
266
295
|
projectId,
|
|
267
296
|
iframe: previewIframeRef.current,
|
|
@@ -313,7 +342,7 @@ export function useTimelineGroupEditing({
|
|
|
313
342
|
: "data-media-start";
|
|
314
343
|
liveAttrs.push([liveAttr, formatTimelineAttributeNumber(change.playbackStart)]);
|
|
315
344
|
}
|
|
316
|
-
patchIframeDomTiming(previewIframeRef.current, change.element, liveAttrs);
|
|
345
|
+
patchIframeDomTiming(previewIframeRef.current, change.element, liveAttrs, activeCompPath);
|
|
317
346
|
}
|
|
318
347
|
|
|
319
348
|
const maxEnd = Math.max(...changes.map((change) => change.start + change.duration));
|
|
@@ -327,6 +356,7 @@ export function useTimelineGroupEditing({
|
|
|
327
356
|
// the just-patched live DOM. See syncPreviewContentDuration.
|
|
328
357
|
syncPreviewContentDuration(previewIframeRef.current);
|
|
329
358
|
const coalesceKey = options?.coalesceKey ?? resizeCoalesceKey(changes);
|
|
359
|
+
const coalesceMs = options?.coalesceMs;
|
|
330
360
|
return enqueueGroupOperation("Resize timeline clips", async (projectId) => {
|
|
331
361
|
await options?.beforeTiming;
|
|
332
362
|
const handledBySdk = await trySdkBatchPersist({
|
|
@@ -339,6 +369,7 @@ export function useTimelineGroupEditing({
|
|
|
339
369
|
needsExtension,
|
|
340
370
|
label: "Resize timeline clips",
|
|
341
371
|
coalesceKey,
|
|
372
|
+
coalesceMs,
|
|
342
373
|
});
|
|
343
374
|
if (handledBySdk) return;
|
|
344
375
|
|
|
@@ -355,6 +386,7 @@ export function useTimelineGroupEditing({
|
|
|
355
386
|
}),
|
|
356
387
|
})),
|
|
357
388
|
coalesceKey,
|
|
389
|
+
coalesceMs,
|
|
358
390
|
);
|
|
359
391
|
await finishGroupTimingGsapFallback({
|
|
360
392
|
projectId,
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
PLAYHEAD_HEAD_W,
|
|
26
26
|
RULER_H,
|
|
27
27
|
TRACK_H,
|
|
28
|
+
TRACKS_LEFT_PAD,
|
|
28
29
|
getTimelineDisplayContentWidth,
|
|
29
30
|
getTimelineFitPps,
|
|
30
31
|
} from "./timelineLayout";
|
|
@@ -152,7 +153,8 @@ describe("Timeline provider boundary", () => {
|
|
|
152
153
|
});
|
|
153
154
|
|
|
154
155
|
const row = button.parentElement?.parentElement;
|
|
155
|
-
|
|
156
|
+
// Row children: [sticky gutter, TRACKS_LEFT_PAD spacer, time-mapped content].
|
|
157
|
+
const trackContent = row?.children.item(2);
|
|
156
158
|
expect(onToggleTrackHidden).toHaveBeenCalledWith(0, false);
|
|
157
159
|
expect(trackContent).toBeInstanceOf(HTMLElement);
|
|
158
160
|
if (!(trackContent instanceof HTMLElement)) {
|
|
@@ -454,27 +456,27 @@ describe("shouldAutoScrollTimeline", () => {
|
|
|
454
456
|
});
|
|
455
457
|
|
|
456
458
|
describe("getTimelineFitPps (min 60s extent + fit headroom)", () => {
|
|
457
|
-
const viewport = 632; // usable width = 632 - GUTTER -
|
|
459
|
+
const viewport = 632; // usable width = 632 - GUTTER - TRACKS_LEFT_PAD - 2
|
|
458
460
|
|
|
459
461
|
it("computes fit pps against the 60s floor for short compositions", () => {
|
|
460
462
|
// A 10s comp maps 60s onto the viewport → the comp takes ~1/6 of the width.
|
|
461
463
|
// (10 * 1.2 = 12s of headroom-padded content is still under the 60s floor.)
|
|
462
464
|
const pps = getTimelineFitPps(viewport, 10);
|
|
463
|
-
expect(pps).toBeCloseTo((viewport - GUTTER - 2) / MIN_TIMELINE_EXTENT_S);
|
|
464
|
-
expect(10 * pps).toBeCloseTo((viewport - GUTTER - 2) / 6);
|
|
465
|
+
expect(pps).toBeCloseTo((viewport - GUTTER - TRACKS_LEFT_PAD - 2) / MIN_TIMELINE_EXTENT_S);
|
|
466
|
+
expect(10 * pps).toBeCloseTo((viewport - GUTTER - TRACKS_LEFT_PAD - 2) / 6);
|
|
465
467
|
});
|
|
466
468
|
|
|
467
469
|
it("fits duration * FIT_ZOOM_HEADROOM (not the bare duration) for long compositions", () => {
|
|
468
470
|
expect(getTimelineFitPps(viewport, 60)).toBeCloseTo(
|
|
469
|
-
(viewport - GUTTER - 2) / (60 * FIT_ZOOM_HEADROOM),
|
|
471
|
+
(viewport - GUTTER - TRACKS_LEFT_PAD - 2) / (60 * FIT_ZOOM_HEADROOM),
|
|
470
472
|
);
|
|
471
473
|
expect(getTimelineFitPps(viewport, 120)).toBeCloseTo(
|
|
472
|
-
(viewport - GUTTER - 2) / (120 * FIT_ZOOM_HEADROOM),
|
|
474
|
+
(viewport - GUTTER - TRACKS_LEFT_PAD - 2) / (120 * FIT_ZOOM_HEADROOM),
|
|
473
475
|
);
|
|
474
476
|
});
|
|
475
477
|
|
|
476
478
|
it("leaves CapCut-style trailing headroom: the comp ends at 1/1.2 of the usable width", () => {
|
|
477
|
-
const usable = viewport - GUTTER - 2;
|
|
479
|
+
const usable = viewport - GUTTER - TRACKS_LEFT_PAD - 2;
|
|
478
480
|
const pps = getTimelineFitPps(viewport, 120);
|
|
479
481
|
// Composition content occupies usable/1.2 px; the remaining ~17% is empty
|
|
480
482
|
// droppable ruler/lane surface past the end.
|
|
@@ -484,16 +486,16 @@ describe("getTimelineFitPps (min 60s extent + fit headroom)", () => {
|
|
|
484
486
|
|
|
485
487
|
it("falls back to 100 pps before the viewport is measured", () => {
|
|
486
488
|
expect(getTimelineFitPps(0, 10)).toBe(100);
|
|
487
|
-
expect(getTimelineFitPps(GUTTER, 10)).toBe(100);
|
|
489
|
+
expect(getTimelineFitPps(GUTTER + TRACKS_LEFT_PAD, 10)).toBe(100);
|
|
488
490
|
expect(getTimelineFitPps(Number.NaN, 10)).toBe(100);
|
|
489
491
|
});
|
|
490
492
|
|
|
491
493
|
it("uses the floor for zero/invalid durations", () => {
|
|
492
494
|
expect(getTimelineFitPps(viewport, 0)).toBeCloseTo(
|
|
493
|
-
(viewport - GUTTER - 2) / MIN_TIMELINE_EXTENT_S,
|
|
495
|
+
(viewport - GUTTER - TRACKS_LEFT_PAD - 2) / MIN_TIMELINE_EXTENT_S,
|
|
494
496
|
);
|
|
495
497
|
expect(getTimelineFitPps(viewport, Number.NaN)).toBeCloseTo(
|
|
496
|
-
(viewport - GUTTER - 2) / MIN_TIMELINE_EXTENT_S,
|
|
498
|
+
(viewport - GUTTER - TRACKS_LEFT_PAD - 2) / MIN_TIMELINE_EXTENT_S,
|
|
497
499
|
);
|
|
498
500
|
});
|
|
499
501
|
});
|
|
@@ -509,7 +511,7 @@ describe("getTimelineDisplayContentWidth", () => {
|
|
|
509
511
|
it("still fills the viewport when that is larger than the 60s floor", () => {
|
|
510
512
|
expect(
|
|
511
513
|
getTimelineDisplayContentWidth({ trackContentWidth: 200, viewportWidth: 2000, pps: 5 }),
|
|
512
|
-
).toBe(2000 - GUTTER - 2);
|
|
514
|
+
).toBe(2000 - GUTTER - TRACKS_LEFT_PAD - 2);
|
|
513
515
|
});
|
|
514
516
|
|
|
515
517
|
it("tracks a drag ghost past every other bound (drag-to-extend)", () => {
|
|
@@ -599,20 +601,28 @@ describe("getTimelineScrollLeftForZoomAnchor", () => {
|
|
|
599
601
|
});
|
|
600
602
|
|
|
601
603
|
describe("getTimelinePlayheadLeft", () => {
|
|
602
|
-
it("offsets the wrapper by half the head width so the line CENTER = GUTTER + t*pps", () => {
|
|
604
|
+
it("offsets the wrapper by half the head width so the line CENTER = GUTTER + TRACKS_LEFT_PAD + t*pps", () => {
|
|
603
605
|
// Wrapper left + PLAYHEAD_HEAD_W/2 (where the 1px line is centered) must
|
|
604
|
-
// equal GUTTER + t*pps at any zoom.
|
|
605
|
-
expect(getTimelinePlayheadLeft(4, 20) + PLAYHEAD_HEAD_W / 2).toBe(
|
|
606
|
-
|
|
606
|
+
// equal GUTTER + TRACKS_LEFT_PAD + t*pps at any zoom.
|
|
607
|
+
expect(getTimelinePlayheadLeft(4, 20) + PLAYHEAD_HEAD_W / 2).toBe(
|
|
608
|
+
GUTTER + TRACKS_LEFT_PAD + 4 * 20,
|
|
609
|
+
);
|
|
610
|
+
expect(getTimelinePlayheadLeft(10, 7.5) + PLAYHEAD_HEAD_W / 2).toBe(
|
|
611
|
+
GUTTER + TRACKS_LEFT_PAD + 75,
|
|
612
|
+
);
|
|
607
613
|
});
|
|
608
614
|
|
|
609
|
-
it("centers the line exactly on the
|
|
610
|
-
expect(getTimelinePlayheadLeft(0, 20) + PLAYHEAD_HEAD_W / 2).toBe(GUTTER);
|
|
615
|
+
it("centers the line exactly on the left pad's end (the 00:00 tick) at t = 0", () => {
|
|
616
|
+
expect(getTimelinePlayheadLeft(0, 20) + PLAYHEAD_HEAD_W / 2).toBe(GUTTER + TRACKS_LEFT_PAD);
|
|
611
617
|
});
|
|
612
618
|
|
|
613
619
|
it("guards invalid input", () => {
|
|
614
|
-
expect(getTimelinePlayheadLeft(Number.NaN, 20)).toBe(
|
|
615
|
-
|
|
620
|
+
expect(getTimelinePlayheadLeft(Number.NaN, 20)).toBe(
|
|
621
|
+
GUTTER + TRACKS_LEFT_PAD - PLAYHEAD_HEAD_W / 2,
|
|
622
|
+
);
|
|
623
|
+
expect(getTimelinePlayheadLeft(4, Number.NaN)).toBe(
|
|
624
|
+
GUTTER + TRACKS_LEFT_PAD - PLAYHEAD_HEAD_W / 2,
|
|
625
|
+
);
|
|
616
626
|
});
|
|
617
627
|
});
|
|
618
628
|
|
|
@@ -681,7 +691,7 @@ describe("resolveTimelineAssetDrop", () => {
|
|
|
681
691
|
trackHeight: 72,
|
|
682
692
|
trackOrder: [0, 3, 7],
|
|
683
693
|
},
|
|
684
|
-
|
|
694
|
+
480, // rectLeft(100) + GUTTER + TRACKS_LEFT_PAD + 3s*100pps
|
|
685
695
|
// clientY updated for TRACKS_TOP_PAD=72: rectTop(200) + RULER_H(24) +
|
|
686
696
|
// TRACKS_TOP_PAD(72) + TRACK_H(48) + TRACK_H/2(24) = 368 → row 1 → track 3.
|
|
687
697
|
368,
|
|
@@ -702,7 +712,7 @@ describe("resolveTimelineAssetDrop", () => {
|
|
|
702
712
|
trackHeight: 72,
|
|
703
713
|
trackOrder: [0, 3, 7],
|
|
704
714
|
},
|
|
705
|
-
250,
|
|
715
|
+
250 + TRACKS_LEFT_PAD,
|
|
706
716
|
600,
|
|
707
717
|
),
|
|
708
718
|
).toEqual({ start: 1.18, track: 8 });
|