@hyperframes/studio 0.8.137 → 0.8.139
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-D_Mh6hbx.js → hyperframes-player-CckOoCJ8.js} +1 -1
- package/dist/assets/{index-CHEctGzc.js → index-BmIPrb8I.js} +1 -1
- package/dist/assets/{index-B3lJsA-e.js → index-Br1Whc0f.js} +189 -189
- package/dist/assets/{index-CC_E-Cbl.css → index-CVd4O6eH.css} +1 -1
- package/dist/assets/{index-rtccfg9b.js → index-DsEkE5_G.js} +1 -1
- package/dist/{chunk-4S3AYCOG.js → chunk-E422JDF5.js} +2 -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 +2 -1
- package/dist/index.html +2 -2
- package/dist/index.js +867 -499
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +9 -8
- package/src/App.tsx +11 -11
- package/src/components/editor/DomEditOverlay.tsx +9 -5
- package/src/components/editor/LayersPanel.test.ts +25 -0
- package/src/components/editor/LayersPanel.tsx +1 -1
- package/src/components/editor/PropertyPanel.test.tsx +600 -748
- 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/manualEditsDom.ts +7 -7
- 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/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/hooks/draggedGsapPosition.ts +8 -0
- package/src/hooks/editMoment.ts +13 -0
- 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.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/useConsoleErrorCapture.test.tsx +87 -0
- package/src/hooks/useConsoleErrorCapture.ts +18 -22
- 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/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/usePreviewPersistence.history.test.tsx +7 -2
- package/src/hooks/usePreviewPersistence.ts +1 -4
- package/src/hooks/useTimelineDeleteOps.test.tsx +10 -0
- package/src/hooks/useTimelineDeleteOps.ts +9 -10
- package/src/index.ts +1 -0
- 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/timelineElementHelpers.ts +0 -11
- package/src/player/previewReloading.test.ts +48 -0
- package/src/player/previewReloading.ts +48 -0
- package/src/styles/studio.css +9 -0
- package/src/utils/gsapUndoRestore.browser.test.ts +21 -12
- 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-E422JDF5.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
|
@@ -22,7 +22,7 @@ interface TimelineSpies {
|
|
|
22
22
|
|
|
23
23
|
function renderHarness(
|
|
24
24
|
initialProps: HarnessProps,
|
|
25
|
-
options: { timelineElements?: TimelineElement[] } = {},
|
|
25
|
+
options: { timelineElements?: TimelineElement[]; previewDoc?: Document } = {},
|
|
26
26
|
): {
|
|
27
27
|
current: () => ReturnType<typeof useDomSelection>;
|
|
28
28
|
rerender: (props: HarnessProps) => void;
|
|
@@ -49,7 +49,11 @@ function renderHarness(
|
|
|
49
49
|
isMasterView: false,
|
|
50
50
|
compIdToSrc: new Map(),
|
|
51
51
|
captionEditMode: false,
|
|
52
|
-
previewIframeRef: {
|
|
52
|
+
previewIframeRef: {
|
|
53
|
+
current: options.previewDoc
|
|
54
|
+
? ({ contentDocument: options.previewDoc } as HTMLIFrameElement)
|
|
55
|
+
: null,
|
|
56
|
+
},
|
|
53
57
|
timelineElements: options.timelineElements ?? [],
|
|
54
58
|
getTimelineSelectionSet: () => publishedSet,
|
|
55
59
|
setSelectedTimelineElementId: timeline.setSelectedTimelineElementId,
|
|
@@ -307,6 +311,21 @@ describe("useDomSelection", () => {
|
|
|
307
311
|
harness.cleanup();
|
|
308
312
|
});
|
|
309
313
|
|
|
314
|
+
it("drops the selection when the reloaded preview no longer has its element", async () => {
|
|
315
|
+
const selection = makeSelection("Headline", document.createElement("div"));
|
|
316
|
+
const harness = renderHarness(
|
|
317
|
+
{ activeCompPath: "index.html", projectId: "project-1", refreshKey: 0 },
|
|
318
|
+
{ previewDoc: document.implementation.createHTMLDocument("") },
|
|
319
|
+
);
|
|
320
|
+
act(() => harness.current().applyDomSelection(selection));
|
|
321
|
+
|
|
322
|
+
await act(() => harness.current().refreshDomEditSelectionFromPreview(selection));
|
|
323
|
+
|
|
324
|
+
expect(harness.current().domEditSelection).toBeNull();
|
|
325
|
+
expect(harness.timeline.setSelectedTimelineElementId).toHaveBeenLastCalledWith(null);
|
|
326
|
+
harness.cleanup();
|
|
327
|
+
});
|
|
328
|
+
|
|
310
329
|
it("clears a committed selection when the active composition path changes", () => {
|
|
311
330
|
const { selection, harness } = setupSelectedHarness();
|
|
312
331
|
expect(harness.current().domEditSelection).toBe(selection);
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
beginStudioPendingEdit,
|
|
10
10
|
isStudioEditSaving,
|
|
11
11
|
setStudioPendingEditClaimClock,
|
|
12
|
+
setStudioWaitingPressCancel,
|
|
12
13
|
trackStudioPendingEdit,
|
|
13
14
|
} from "../utils/studioPendingEdits";
|
|
14
15
|
|
|
@@ -18,6 +19,7 @@ let root: Root | null = null;
|
|
|
18
19
|
afterEach(() => {
|
|
19
20
|
act(() => root?.unmount());
|
|
20
21
|
setStudioPendingEditClaimClock(null);
|
|
22
|
+
setStudioWaitingPressCancel(null);
|
|
21
23
|
});
|
|
22
24
|
|
|
23
25
|
type RestoreFiles = Record<string, { previous: string; restored: string }>;
|
|
@@ -71,6 +73,24 @@ const PREDICTED = { id: "e1", files: { "index.html": { previous: "B", restored:
|
|
|
71
73
|
const SERVER_FILES = { "index.html": { previous: "B", restored: "A2" } };
|
|
72
74
|
|
|
73
75
|
describe("useEditHistoryActions", () => {
|
|
76
|
+
it("takes back a canvas press still waiting to run, with no history step", async () => {
|
|
77
|
+
const { deps, actions } = mount({ ok: true, label: "Undid: Move" });
|
|
78
|
+
const cancel = vi.fn(() => (setStudioWaitingPressCancel(null), true));
|
|
79
|
+
setStudioWaitingPressCancel(cancel);
|
|
80
|
+
const revert = vi.fn(() => () => {});
|
|
81
|
+
const saving = beginStudioPendingEdit(revert);
|
|
82
|
+
await act(() => actions.redo());
|
|
83
|
+
expect(cancel, "redo leaves the press").not.toHaveBeenCalled();
|
|
84
|
+
await act(() => actions.undo());
|
|
85
|
+
expect(cancel).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(revert, "the press is newer than any edit still saving").not.toHaveBeenCalled();
|
|
87
|
+
expect(deps.editHistory.undo).not.toHaveBeenCalled();
|
|
88
|
+
|
|
89
|
+
await act(() => actions.undo());
|
|
90
|
+
expect(revert, "the following Cmd+Z takes back the edit still saving").toHaveBeenCalledTimes(1);
|
|
91
|
+
saving.settle();
|
|
92
|
+
});
|
|
93
|
+
|
|
74
94
|
it("asks for the edit claimed after the key's claim count only when an edit was saving at the key", async () => {
|
|
75
95
|
const { deps, actions } = mount({ ok: false, reason: "empty" });
|
|
76
96
|
await act(() => actions.undo());
|
|
@@ -5,6 +5,7 @@ import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordin
|
|
|
5
5
|
import type { RestoreFiles } from "../utils/gsapUndoRestore";
|
|
6
6
|
import {
|
|
7
7
|
beginStudioShownRestore,
|
|
8
|
+
cancelNewestStudioWaitingPress,
|
|
8
9
|
hasStudioPendingEdits,
|
|
9
10
|
paintBackNewestStudioPendingEdit,
|
|
10
11
|
} from "../utils/studioPendingEdits";
|
|
@@ -79,6 +80,7 @@ export function useEditHistoryActions({
|
|
|
79
80
|
const apply = useCallback(
|
|
80
81
|
async (direction: "undo" | "redo") => {
|
|
81
82
|
const noun = direction === "undo" ? "Undo" : "Redo";
|
|
83
|
+
if (direction === "undo" && cancelNewestStudioWaitingPress()) return;
|
|
82
84
|
// An edit still saving is newer than any step this tab predicts, so it paints back first.
|
|
83
85
|
const pendingEditShown = direction === "undo" ? paintBackNewestStudioPendingEdit() : null;
|
|
84
86
|
const predicted = pendingEditShown ? null : (editHistory.predict?.(direction) ?? null);
|
|
@@ -42,6 +42,7 @@ vi.mock("./useSafeGsapCommitMutation", () => ({
|
|
|
42
42
|
}));
|
|
43
43
|
|
|
44
44
|
import { useGsapAwareEditing } from "./useGsapAwareEditing";
|
|
45
|
+
import { isPreviewChanging } from "../player/previewReloading";
|
|
45
46
|
import { tryGsapRotationIntercept } from "./gsapRuntimeBridge";
|
|
46
47
|
|
|
47
48
|
afterEach(() => {
|
|
@@ -235,6 +236,43 @@ describe("useGsapAwareEditing keeps the route a gesture chose at press", () => {
|
|
|
235
236
|
});
|
|
236
237
|
});
|
|
237
238
|
|
|
239
|
+
describe("useGsapAwareEditing holds a canvas press while a drop saves", () => {
|
|
240
|
+
it.each([
|
|
241
|
+
["a move through the script", false, "move"],
|
|
242
|
+
["a group move through the script", false, "group"],
|
|
243
|
+
["a move saved as CSS", true, "move"],
|
|
244
|
+
])("%s", async (_, plainTranslate, kind) => {
|
|
245
|
+
let land: () => void = () => {};
|
|
246
|
+
const landed = new Promise<void>((resolve) => (land = resolve));
|
|
247
|
+
mocks.drag.mockImplementation(async () => {
|
|
248
|
+
await landed;
|
|
249
|
+
return { status: "persisted" };
|
|
250
|
+
});
|
|
251
|
+
const save = vi.fn().mockReturnValue(landed);
|
|
252
|
+
const stageElementPositionOffset = vi.fn(() => ({ save, rollback: vi.fn() }));
|
|
253
|
+
const { pathOffsetCommit, groupCommit, root } = mountGroupHandler({
|
|
254
|
+
stageElementPositionOffset,
|
|
255
|
+
});
|
|
256
|
+
const box = { element: document.createElement("div"), id: "box", selector: "#box" };
|
|
257
|
+
const selection = box as unknown as DomEditSelection;
|
|
258
|
+
const to = { x: 40, y: 20 };
|
|
259
|
+
let saved!: Promise<unknown>;
|
|
260
|
+
act(() => {
|
|
261
|
+
saved =
|
|
262
|
+
kind === "group"
|
|
263
|
+
? groupCommit([{ selection, next: to, plainTranslate }])
|
|
264
|
+
: pathOffsetCommit(selection, to, { plainTranslate });
|
|
265
|
+
});
|
|
266
|
+
expect(isPreviewChanging(), "from the drop").toBe(!plainTranslate);
|
|
267
|
+
await act(async () => {
|
|
268
|
+
land();
|
|
269
|
+
await saved;
|
|
270
|
+
});
|
|
271
|
+
expect(isPreviewChanging()).toBe(false);
|
|
272
|
+
act(() => root.unmount());
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
238
276
|
describe("useGsapAwareEditing keeps the box route a resize chose at press", () => {
|
|
239
277
|
it.each([
|
|
240
278
|
["the CSS route, on an element GSAP has since taken over", true],
|
|
@@ -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,
|