@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
|
@@ -17,6 +17,7 @@ import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
|
17
17
|
import { tryGsapDragIntercept, tryGsapRotationIntercept } from "./gsapRuntimeBridge";
|
|
18
18
|
import { tryGsapResizeIntercept } from "./gsapResizeIntercept";
|
|
19
19
|
import { computeDraggedGsapPosition, freezeDragStamp } from "./draggedGsapPosition";
|
|
20
|
+
import { whileScriptWrites } from "../player/previewReloading";
|
|
20
21
|
import { readGsapPositionFromIframe } from "./gsapPositionDetection";
|
|
21
22
|
import { selectorFromSelection } from "./gsapShared";
|
|
22
23
|
import { useAnimatedPropertyCommit } from "./useAnimatedPropertyCommit";
|
|
@@ -153,7 +154,10 @@ export function useGsapAwareEditing({
|
|
|
153
154
|
const result = await stageElementPositionOffset(selection, next, true).save();
|
|
154
155
|
return writes.finish(result?.changed === true);
|
|
155
156
|
}
|
|
156
|
-
|
|
157
|
+
const writer = writes.commit;
|
|
158
|
+
if (!writer) return;
|
|
159
|
+
return whileScriptWrites(async () => {
|
|
160
|
+
const stamp = freezeDragStamp(selection.element);
|
|
157
161
|
try {
|
|
158
162
|
const ownedAnimations = getGsapAnimationsForSelection(selection);
|
|
159
163
|
const targetAnimations = Array.isArray(ownedAnimations)
|
|
@@ -164,9 +168,9 @@ export function useGsapAwareEditing({
|
|
|
164
168
|
next,
|
|
165
169
|
targetAnimations,
|
|
166
170
|
previewIframeRef.current,
|
|
167
|
-
|
|
171
|
+
writer,
|
|
168
172
|
makeFetchFallback(selection),
|
|
169
|
-
modifiers,
|
|
173
|
+
{ ...modifiers, stamp },
|
|
170
174
|
);
|
|
171
175
|
await saveMove(outcome, async () => {
|
|
172
176
|
const staged = writes.drawKeepingUndone(() =>
|
|
@@ -179,7 +183,7 @@ export function useGsapAwareEditing({
|
|
|
179
183
|
trackGsapInteractionFailure(error, selection, "drag", "Move animated layer");
|
|
180
184
|
throw error;
|
|
181
185
|
}
|
|
182
|
-
}
|
|
186
|
+
});
|
|
183
187
|
},
|
|
184
188
|
[
|
|
185
189
|
gsapCommitMutation,
|
|
@@ -209,149 +213,153 @@ export function useGsapAwareEditing({
|
|
|
209
213
|
restore: () => void = () => undefined,
|
|
210
214
|
route?: { plainTranslate: boolean },
|
|
211
215
|
) => {
|
|
212
|
-
const stamp = freezeDragStamp(selection.element);
|
|
213
216
|
const writes = observeGsapGesture(gsapCommitMutation);
|
|
214
217
|
if (route?.plainTranslate ?? editsPlainCss(selection.element, "resize")) {
|
|
215
218
|
const result = await handleDomBoxSizeCommit(selection, next, offset, restore);
|
|
216
219
|
return writes.finish(result?.changed === true);
|
|
217
220
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
? hasNonHoldTweenForElement(
|
|
231
|
-
previewIframeRef.current,
|
|
232
|
-
selector,
|
|
233
|
-
undefined,
|
|
234
|
-
POSITION_CHANNELS,
|
|
235
|
-
)
|
|
236
|
-
: false;
|
|
237
|
-
logResize("commit-route", {
|
|
238
|
-
next,
|
|
239
|
-
offset: offset ?? null,
|
|
240
|
-
scaleRoute,
|
|
241
|
-
animCount: targetAnimations.length,
|
|
242
|
-
animGroups: targetAnimations.map((a) => `${a.propertyGroup}:${a.method}`),
|
|
243
|
-
});
|
|
244
|
-
let anchorMove: ReturnType<typeof stageElementPositionOffset> | null = null;
|
|
245
|
-
const stageCrop = prepareCropResize(selection.element);
|
|
246
|
-
let cropUndoKey: string | null = null;
|
|
247
|
-
const saveResizeAnchor = async (
|
|
248
|
-
ownsDragOffset: boolean,
|
|
249
|
-
commitMutation: CommitMutation,
|
|
250
|
-
coalesceKey: string,
|
|
251
|
-
) => {
|
|
252
|
-
if (offset && !ownsDragOffset) {
|
|
253
|
-
const dragOutcome = await tryGsapDragIntercept(
|
|
254
|
-
selection,
|
|
255
|
-
offset,
|
|
256
|
-
targetAnimations,
|
|
257
|
-
previewIframeRef.current,
|
|
258
|
-
commitMutation,
|
|
259
|
-
makeFetchFallback(selection),
|
|
260
|
-
{ stamp },
|
|
261
|
-
);
|
|
262
|
-
// Saved after the size, under its undo key, so the two are one step.
|
|
263
|
-
await saveMove(dragOutcome, async () => {
|
|
264
|
-
const plainAfterSettle = editsPlainCss(selection.element, "move");
|
|
265
|
-
anchorMove = writes.drawKeepingUndone(() =>
|
|
266
|
-
stageElementPositionOffset(selection, offset, plainAfterSettle, coalesceKey),
|
|
267
|
-
);
|
|
268
|
-
});
|
|
221
|
+
return whileScriptWrites(async () => {
|
|
222
|
+
const stamp = freezeDragStamp(selection.element);
|
|
223
|
+
let targetAnimations: GsapAnimation[];
|
|
224
|
+
try {
|
|
225
|
+
const ownedAnimations = getGsapAnimationsForSelection(selection);
|
|
226
|
+
targetAnimations = Array.isArray(ownedAnimations)
|
|
227
|
+
? ownedAnimations
|
|
228
|
+
: await ownedAnimations;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
restore();
|
|
231
|
+
trackGsapInteractionFailure(error, selection, "resize", "Resize animated layer");
|
|
232
|
+
throw error;
|
|
269
233
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
234
|
+
const scaleRoute = targetAnimations.some((anim) => anim.propertyGroup === "scale");
|
|
235
|
+
const selector = selectorFromSelection(selection);
|
|
236
|
+
const hasLivePositionTween = selector
|
|
237
|
+
? hasNonHoldTweenForElement(
|
|
238
|
+
previewIframeRef.current,
|
|
239
|
+
selector,
|
|
240
|
+
undefined,
|
|
241
|
+
POSITION_CHANNELS,
|
|
242
|
+
)
|
|
243
|
+
: false;
|
|
244
|
+
logResize("commit-route", {
|
|
245
|
+
next,
|
|
246
|
+
offset: offset ?? null,
|
|
247
|
+
scaleRoute,
|
|
248
|
+
animCount: targetAnimations.length,
|
|
249
|
+
animGroups: targetAnimations.map((a) => `${a.propertyGroup}:${a.method}`),
|
|
250
|
+
});
|
|
251
|
+
let anchorMove: ReturnType<typeof stageElementPositionOffset> | null = null;
|
|
252
|
+
const stageCrop = prepareCropResize(selection.element);
|
|
253
|
+
let cropUndoKey: string | null = null;
|
|
254
|
+
const saveResizeAnchor = async (
|
|
255
|
+
ownsDragOffset: boolean,
|
|
256
|
+
commitMutation: CommitMutation,
|
|
257
|
+
coalesceKey: string,
|
|
258
|
+
) => {
|
|
259
|
+
if (offset && !ownsDragOffset) {
|
|
260
|
+
const dragOutcome = await tryGsapDragIntercept(
|
|
261
|
+
selection,
|
|
286
262
|
offset,
|
|
287
|
-
|
|
288
|
-
|
|
263
|
+
targetAnimations,
|
|
264
|
+
previewIframeRef.current,
|
|
265
|
+
commitMutation,
|
|
266
|
+
makeFetchFallback(selection),
|
|
267
|
+
{ stamp },
|
|
289
268
|
);
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
269
|
+
// Saved after the size, under its undo key, so the two are one step.
|
|
270
|
+
await saveMove(dragOutcome, async () => {
|
|
271
|
+
const plainAfterSettle = editsPlainCss(selection.element, "move");
|
|
272
|
+
anchorMove = writes.drawKeepingUndone(() =>
|
|
273
|
+
stageElementPositionOffset(selection, offset, plainAfterSettle, coalesceKey),
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
await runGestureTransaction({
|
|
279
|
+
element: selection.element,
|
|
280
|
+
label: "Resize layer",
|
|
281
|
+
draw: writes.drawKeepingUndone,
|
|
282
|
+
settle: () => {
|
|
283
|
+
// Scale resize settles its center-scale residual after the scale commit
|
|
284
|
+
// renders. Width/height can settle its anchored position immediately.
|
|
285
|
+
if (!offset || scaleRoute || !selector) return;
|
|
286
|
+
writes.drawKeepingUndone(() => {
|
|
287
|
+
const gsapPos = readGsapPositionFromIframe(previewIframeRef.current, selector) ?? {
|
|
288
|
+
x: 0,
|
|
289
|
+
y: 0,
|
|
290
|
+
};
|
|
291
|
+
const { newX, newY } = computeDraggedGsapPosition(
|
|
292
|
+
selection.element,
|
|
305
293
|
offset,
|
|
306
|
-
|
|
294
|
+
gsapPos,
|
|
307
295
|
stamp,
|
|
308
296
|
);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
297
|
+
logResize("sync-settle", { gsapPos, offset, newX, newY });
|
|
298
|
+
setElementGsapPosition(selection.element, newX, newY);
|
|
299
|
+
});
|
|
300
|
+
},
|
|
301
|
+
persist: async (commit, coalesceKey) => {
|
|
302
|
+
if (writes.commit) {
|
|
303
|
+
const commitMutation = commit(writes.commit);
|
|
304
|
+
try {
|
|
305
|
+
const outcome = await tryGsapResizeIntercept(
|
|
306
|
+
selection,
|
|
307
|
+
next,
|
|
308
|
+
targetAnimations,
|
|
309
|
+
previewIframeRef.current,
|
|
310
|
+
commitMutation,
|
|
311
|
+
makeFetchFallback(selection),
|
|
312
|
+
offset,
|
|
313
|
+
writes.drawKeepingUndone,
|
|
314
|
+
stamp,
|
|
314
315
|
);
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
316
|
+
assertGsapEditPersisted(outcome);
|
|
317
|
+
// Saved before the buffered GSAP writes, so their reload stays the gesture's last render.
|
|
318
|
+
if (outcome.status === "element-size") {
|
|
319
|
+
const result = await writes.drawKeepingUndone(() =>
|
|
320
|
+
handleDomBoxSizeCommit(selection, next, undefined, undefined, coalesceKey),
|
|
321
|
+
);
|
|
322
|
+
writes.recordDomResult(result);
|
|
323
|
+
} else cropUndoKey = coalesceKey;
|
|
324
|
+
// What the resize did, not what its tweens suggest: a scale hold still commits a size.
|
|
325
|
+
const ownsDragOffset =
|
|
326
|
+
outcome.status === "persisted" && outcome.ownsDragOffset === true;
|
|
327
|
+
logResize("intercept-handled", {
|
|
328
|
+
scaleRoute,
|
|
329
|
+
ownsDragOffset,
|
|
330
|
+
willForwardOffset: !!(offset && !ownsDragOffset),
|
|
331
|
+
});
|
|
332
|
+
// A resize that moved the element itself has already written
|
|
333
|
+
// where it landed. Everything else leaves the anchor to the drag.
|
|
334
|
+
await saveResizeAnchor(ownsDragOffset, commitMutation, coalesceKey);
|
|
335
|
+
logResizeSettle(selection.element, ownsDragOffset ? "gsap-scale" : "gsap-size");
|
|
336
|
+
return;
|
|
337
|
+
} catch (error) {
|
|
338
|
+
trackGsapInteractionFailure(error, selection, "resize", "Resize animated layer");
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
333
341
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
342
|
+
throw new Error("Resize of a GSAP-owned box has no GSAP writer");
|
|
343
|
+
},
|
|
344
|
+
afterBufferedCommitsSaved: async () => {
|
|
345
|
+
const anchorResult = await anchorMove?.save();
|
|
346
|
+
writes.recordDomResult(anchorResult);
|
|
347
|
+
// Only now is the size live for every caller, drag or not.
|
|
348
|
+
if (cropUndoKey) {
|
|
349
|
+
const cropResult = await writes.drawKeepingUndone(() =>
|
|
350
|
+
saveCropResize(stageCrop, selection, commitPositionPatchToHtml, cropUndoKey!),
|
|
351
|
+
);
|
|
352
|
+
writes.recordDomResult(cropResult);
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
restore: () => {
|
|
356
|
+
anchorMove?.rollback();
|
|
357
|
+
restore();
|
|
358
|
+
},
|
|
359
|
+
skipPixelAssert: hasLivePositionTween,
|
|
360
|
+
});
|
|
361
|
+
return writes.finish();
|
|
353
362
|
});
|
|
354
|
-
return writes.finish();
|
|
355
363
|
},
|
|
356
364
|
[
|
|
357
365
|
handleDomBoxSizeCommit,
|
|
@@ -372,7 +380,10 @@ export function useGsapAwareEditing({
|
|
|
372
380
|
const result = await handleDomRotationCommit(selection, next);
|
|
373
381
|
return writes.finish(result?.changed === true);
|
|
374
382
|
}
|
|
375
|
-
|
|
383
|
+
const writer = writes.commit;
|
|
384
|
+
if (!writer) return;
|
|
385
|
+
return whileScriptWrites(async () => {
|
|
386
|
+
const stamp = freezeDragStamp(selection.element);
|
|
376
387
|
try {
|
|
377
388
|
const targetAnimations = await getGsapAnimationsForSelection(selection);
|
|
378
389
|
// A keyframe or a tl.set; a computed source rejects, so the gesture restores its draft.
|
|
@@ -381,8 +392,9 @@ export function useGsapAwareEditing({
|
|
|
381
392
|
next.angle,
|
|
382
393
|
targetAnimations,
|
|
383
394
|
previewIframeRef.current,
|
|
384
|
-
|
|
395
|
+
writer,
|
|
385
396
|
makeFetchFallback(selection),
|
|
397
|
+
stamp,
|
|
386
398
|
);
|
|
387
399
|
assertGsapEditPersisted(outcome);
|
|
388
400
|
return writes.finish();
|
|
@@ -390,7 +402,7 @@ export function useGsapAwareEditing({
|
|
|
390
402
|
trackGsapInteractionFailure(error, selection, "rotation", "Rotate animated layer");
|
|
391
403
|
throw error;
|
|
392
404
|
}
|
|
393
|
-
}
|
|
405
|
+
});
|
|
394
406
|
},
|
|
395
407
|
[
|
|
396
408
|
gsapCommitMutation,
|
|
@@ -8,8 +8,11 @@ import { observeGsapGesture } from "./gsapGestureOutcome";
|
|
|
8
8
|
import { editsPlainCss } from "./gsapRuntimeKeyframes";
|
|
9
9
|
import { refuseGsapTakeover } from "./elementOffsetStager";
|
|
10
10
|
import { tryGsapDragIntercept } from "./gsapRuntimeBridge";
|
|
11
|
+
import { freezeDragStamp } from "./draggedGsapPosition";
|
|
11
12
|
import { assertGsapEditPersisted } from "./gsapEditOutcome";
|
|
12
13
|
import { firstPreflightFailure } from "./gsapGroupPreflight";
|
|
14
|
+
import { whileScriptWrites } from "../player/previewReloading";
|
|
15
|
+
import type { GeometryCommitResult } from "../utils/previewFeatureUsage";
|
|
13
16
|
|
|
14
17
|
// A distinct key keeps consecutive group drags in separate undo entries.
|
|
15
18
|
let groupDragCommitCounter = 0;
|
|
@@ -36,131 +39,141 @@ export function useGsapAwareGroupMove({
|
|
|
36
39
|
async (
|
|
37
40
|
updates: DomEditGroupPathOffsetCommit[],
|
|
38
41
|
options: { refusalToast?: boolean } = {},
|
|
39
|
-
): Promise<
|
|
42
|
+
): Promise<GeometryCommitResult> => {
|
|
40
43
|
const writes = observeGsapGesture(gsapCommitMutation);
|
|
41
44
|
const writer = writes.commit;
|
|
42
45
|
if (!writer) return { ok: true, changed: false };
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
await writer(call.selection, call.mutation, call.options);
|
|
64
|
-
}
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
await writer.batch(calls, {
|
|
68
|
-
...(calls.at(-1)?.options ?? { label: "Move animated layer (group)" }),
|
|
69
|
-
label: "Move animated layer (group)",
|
|
46
|
+
const scripted = updates.some(
|
|
47
|
+
({ selection, plainTranslate }) =>
|
|
48
|
+
!(plainTranslate ?? editsPlainCss(selection.element, "move")),
|
|
49
|
+
);
|
|
50
|
+
const commitGroup = async (): Promise<GeometryCommitResult> => {
|
|
51
|
+
const stamps = new Map(
|
|
52
|
+
updates.map((u) => [u.selection, freezeDragStamp(u.selection.element)]),
|
|
53
|
+
);
|
|
54
|
+
const toastRefusal = options.refusalToast !== false;
|
|
55
|
+
// One coalesce key across slow writes keeps the group drag in one undo entry.
|
|
56
|
+
const coalesceKey = `group-drag:${++groupDragCommitCounter}`;
|
|
57
|
+
// Hold the render until every member is saved, or unwritten members snap back.
|
|
58
|
+
let renderOnCommit = false;
|
|
59
|
+
const previewFallbackLatch = { pending: false };
|
|
60
|
+
const withGroupOptions = (options: CommitMutationOptions): CommitMutationOptions => ({
|
|
61
|
+
...options,
|
|
62
|
+
coalesceKey,
|
|
63
|
+
coalesceMs: Number.POSITIVE_INFINITY,
|
|
64
|
+
deferPreviewSync: !renderOnCommit,
|
|
65
|
+
previewFallbackLatch,
|
|
70
66
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// write nothing and share one in-flight parse per file, so they run together.
|
|
82
|
-
const preflightResults = await Promise.allSettled(
|
|
83
|
-
updates.map(async ({ selection, next, plainTranslate }) => {
|
|
84
|
-
if (plainTranslate ?? editsPlainCss(selection.element, "move")) {
|
|
85
|
-
refuseGsapTakeover(selection.element, toastRefusal ? showToast : () => {});
|
|
86
|
-
return void offsetMembers.set(selection, true);
|
|
67
|
+
// Batch members in the same file to avoid repeating its read, parse, write and preview patch.
|
|
68
|
+
const queued: CommitMutationCall[] = [];
|
|
69
|
+
const flushQueued = async () => {
|
|
70
|
+
if (queued.length === 0) return;
|
|
71
|
+
const calls = queued.splice(0, queued.length);
|
|
72
|
+
if (!writer.batch) {
|
|
73
|
+
for (const call of calls) {
|
|
74
|
+
await writer(call.selection, call.mutation, call.options);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
87
77
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
78
|
+
await writer.batch(calls, {
|
|
79
|
+
...(calls.at(-1)?.options ?? { label: "Move animated layer (group)" }),
|
|
80
|
+
label: "Move animated layer (group)",
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
const coalescedCommit: typeof gsapCommitMutation = (selection, mutation, options) => {
|
|
84
|
+
queued.push({ selection, mutation, options: withGroupOptions(options) });
|
|
85
|
+
return Promise.resolve();
|
|
86
|
+
};
|
|
87
|
+
const preflightAnimations = new Map<DomEditSelection, GsapAnimation[]>();
|
|
88
|
+
// Members saved on themselves, each with its route: true for its CSS translate.
|
|
89
|
+
const offsetMembers = new Map<DomEditSelection, boolean>();
|
|
90
|
+
// Editability is user-atomic: prove every member can be written before the first source
|
|
91
|
+
// mutation, so a blocked member never leaves earlier siblings partially moved. Preflights
|
|
92
|
+
// write nothing and share one in-flight parse per file, so they run together.
|
|
93
|
+
const preflightResults = await Promise.allSettled(
|
|
94
|
+
updates.map(async ({ selection, next, plainTranslate }) => {
|
|
95
|
+
if (plainTranslate ?? editsPlainCss(selection.element, "move")) {
|
|
96
|
+
refuseGsapTakeover(selection.element, toastRefusal ? showToast : () => {});
|
|
97
|
+
return void offsetMembers.set(selection, true);
|
|
98
|
+
}
|
|
99
|
+
const animations = await makeFetchFallback(selection, { failOnFetchError: true })();
|
|
100
|
+
preflightAnimations.set(selection, animations);
|
|
101
|
+
const outcome = await tryGsapDragIntercept(
|
|
102
|
+
selection,
|
|
103
|
+
next,
|
|
104
|
+
animations,
|
|
105
|
+
previewIframeRef.current,
|
|
106
|
+
coalescedCommit,
|
|
107
|
+
undefined,
|
|
108
|
+
{ preflightOnly: true, group: true },
|
|
109
|
+
);
|
|
110
|
+
if (outcome.status === "element-offset") offsetMembers.set(selection, false);
|
|
111
|
+
assertGsapEditPersisted(outcome);
|
|
112
|
+
}),
|
|
116
113
|
);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
114
|
+
const preflightFailure = firstPreflightFailure(
|
|
115
|
+
preflightResults,
|
|
116
|
+
updates,
|
|
117
|
+
offsetMembers,
|
|
118
|
+
activeCompPath,
|
|
119
|
+
);
|
|
120
|
+
if (preflightFailure) {
|
|
121
|
+
trackGsapInteractionFailure(
|
|
122
|
+
preflightFailure.error,
|
|
123
|
+
preflightFailure.selection,
|
|
124
|
+
"drag",
|
|
125
|
+
"Move animated layer (group)",
|
|
126
|
+
toastRefusal,
|
|
128
127
|
);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
throw preflightFailure.error;
|
|
129
|
+
}
|
|
130
|
+
const lastScriptWrite = updates.findLastIndex(
|
|
131
|
+
({ selection }) => !offsetMembers.has(selection),
|
|
132
|
+
);
|
|
133
|
+
for (const [index, { selection, next }] of updates.entries()) {
|
|
134
|
+
renderOnCommit = index === lastScriptWrite;
|
|
135
|
+
const plain = offsetMembers.get(selection);
|
|
136
|
+
if (plain !== undefined) {
|
|
137
|
+
const staged = writes.drawKeepingUndone(() =>
|
|
138
|
+
stageElementPositionOffset(selection, next, plain, coalesceKey),
|
|
139
|
+
);
|
|
140
|
+
const result = await staged.save();
|
|
141
|
+
writes.recordDomResult(result);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const outcome = await tryGsapDragIntercept(
|
|
146
|
+
selection,
|
|
147
|
+
next,
|
|
148
|
+
preflightAnimations.get(selection) ?? [],
|
|
149
|
+
previewIframeRef.current,
|
|
150
|
+
coalescedCommit,
|
|
151
|
+
// The intercept re-reads the file to resolve a stale or shared tween.
|
|
152
|
+
// Anything already queued has to be on disk before that read, or it
|
|
153
|
+
// resolves against a file missing writes it is about to build on.
|
|
154
|
+
async () => {
|
|
155
|
+
await flushQueued();
|
|
156
|
+
return makeFetchFallback(selection, { fresh: true })();
|
|
157
|
+
},
|
|
158
|
+
{ preflightPassed: true, stamp: stamps.get(selection) },
|
|
159
|
+
);
|
|
160
|
+
assertGsapEditPersisted(outcome);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
trackGsapInteractionFailure(error, selection, "drag", "Move animated layer (group)");
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
132
165
|
}
|
|
133
166
|
try {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
next,
|
|
137
|
-
preflightAnimations.get(selection) ?? [],
|
|
138
|
-
previewIframeRef.current,
|
|
139
|
-
coalescedCommit,
|
|
140
|
-
// The intercept re-reads the file to resolve a stale or shared tween.
|
|
141
|
-
// Anything already queued has to be on disk before that read, or it
|
|
142
|
-
// resolves against a file missing writes it is about to build on.
|
|
143
|
-
async () => {
|
|
144
|
-
await flushQueued();
|
|
145
|
-
return makeFetchFallback(selection, { fresh: true })();
|
|
146
|
-
},
|
|
147
|
-
{ preflightPassed: true },
|
|
148
|
-
);
|
|
149
|
-
assertGsapEditPersisted(outcome);
|
|
167
|
+
await flushQueued();
|
|
168
|
+
return writes.finish();
|
|
150
169
|
} catch (error) {
|
|
151
|
-
|
|
170
|
+
// The aggregate write has no uniquely failing member; do not misattribute
|
|
171
|
+
// its telemetry to whichever member happened to be last in the array.
|
|
172
|
+
trackGsapInteractionFailure(error, null, "drag", "Move animated layer (group)");
|
|
152
173
|
throw error;
|
|
153
174
|
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
await flushQueued();
|
|
157
|
-
return writes.finish();
|
|
158
|
-
} catch (error) {
|
|
159
|
-
// The aggregate write has no uniquely failing member; do not misattribute
|
|
160
|
-
// its telemetry to whichever member happened to be last in the array.
|
|
161
|
-
trackGsapInteractionFailure(error, null, "drag", "Move animated layer (group)");
|
|
162
|
-
throw error;
|
|
163
|
-
}
|
|
175
|
+
};
|
|
176
|
+
return scripted ? whileScriptWrites(commitGroup) : commitGroup();
|
|
164
177
|
},
|
|
165
178
|
[
|
|
166
179
|
gsapCommitMutation,
|