@hyperframes/studio 0.8.98 → 0.8.100
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-C6muFtfk.js → hyperframes-player-fWt87gyh.js} +1 -1
- package/dist/assets/{index-B6vU39NM.js → index-D34Oy5DN.js} +1 -1
- package/dist/assets/{index-itc1XziP.js → index-D6TjRAvz.js} +171 -171
- package/dist/assets/{index-BxnoLgxG.js → index-DofdzdxJ.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/index.js +718 -438
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/components/editor/DomEditCropHandles.test.tsx +34 -66
- package/src/components/editor/DomEditCropHandles.tsx +30 -62
- package/src/components/editor/DomEditOverlay.test.ts +2 -2
- package/src/components/editor/PropertyPanel.tsx +3 -3
- package/src/components/editor/cropResize.test.ts +129 -0
- package/src/components/editor/cropResize.ts +143 -0
- package/src/components/editor/domEditOverlayCrop.ts +65 -15
- package/src/components/editor/domEditOverlayCropLift.test.ts +44 -0
- package/src/components/editor/domEditOverlayGestures.ts +2 -5
- package/src/components/editor/domEditOverlayStartGesture.test.ts +83 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +6 -4
- package/src/components/editor/elementPositionOffset.ts +5 -4
- package/src/components/editor/manualEdits.test.ts +22 -8
- package/src/components/editor/manualEditsDom.ts +27 -11
- package/src/components/editor/manualEditsParsing.ts +0 -4
- package/src/components/editor/manualEditsSnapshot.ts +3 -0
- package/src/components/editor/manualOffsetDrag.test.ts +84 -0
- package/src/components/editor/manualOffsetDrag.ts +50 -40
- package/src/components/editor/plainTranslate.test.ts +62 -0
- package/src/components/editor/plainTranslate.ts +85 -0
- package/src/components/editor/propertyPanelTransformCommit.ts +3 -2
- package/src/components/editor/useDomEditNudge.ts +1 -1
- package/src/hooks/domSelectionTestHarness.ts +8 -0
- package/src/hooks/draggedGsapPosition.ts +4 -2
- package/src/hooks/elementOffsetStager.ts +33 -2
- package/src/hooks/gsapDragCommit.ts +6 -6
- package/src/hooks/gsapEditOutcome.ts +6 -0
- package/src/hooks/gsapResizeIntercept.ts +5 -5
- package/src/hooks/gsapRuntimeBridge.test.ts +11 -0
- package/src/hooks/gsapRuntimeBridge.ts +3 -18
- package/src/hooks/gsapRuntimeKeyframes.test.ts +7 -0
- package/src/hooks/gsapRuntimeKeyframes.ts +69 -21
- package/src/hooks/timelineTimingSync.ts +40 -4
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +40 -1
- package/src/hooks/useAnimatedPropertyCommit.ts +11 -13
- package/src/hooks/useCropOverlay.ts +8 -2
- package/src/hooks/useDomEditCommits.test.tsx +18 -4
- package/src/hooks/useDomEditCommits.ts +3 -1
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -2
- package/src/hooks/useDomEditSession.ts +2 -0
- package/src/hooks/useDomEditTextCommits.test.tsx +62 -5
- package/src/hooks/useDomEditTextCommits.ts +35 -35
- package/src/hooks/useDomGeometryCommit.test.tsx +62 -4
- package/src/hooks/useDomGeometryCommit.ts +1 -0
- package/src/hooks/useDomGeometryCommits.test.tsx +116 -2
- package/src/hooks/useDomGeometryCommits.ts +5 -0
- package/src/hooks/useElementLifecycleOps.multiDelete.test.tsx +3 -1
- package/src/hooks/useGsapAwareEditing.test.tsx +147 -23
- package/src/hooks/useGsapAwareEditing.ts +48 -34
- package/src/hooks/useTimelineEditing.test.tsx +190 -135
- package/src/hooks/useTimelineEditing.ts +13 -13
- package/src/hooks/useTimelineGroupEditing.ts +13 -9
- package/src/player/components/useTimelineVirtualRows.test.tsx +30 -1
- package/src/utils/rounding.ts +5 -0
- package/src/utils/sdkEditTransaction.ts +10 -3
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
3
|
import React, { act } from "react";
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { afterEach, describe, expect, it, vi, type Mock } from "vitest";
|
|
5
5
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
6
6
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
7
7
|
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
|
8
|
-
import { mountReactHarness } from "./domSelectionTestHarness";
|
|
8
|
+
import { mountReactHarness, withInlineLayoutBox } from "./domSelectionTestHarness";
|
|
9
|
+
import { DomEditCropHandles } from "../components/editor/DomEditCropHandles";
|
|
10
|
+
import { applyStudioBoxSizeDraft } from "../components/editor/manualEdits";
|
|
9
11
|
|
|
10
12
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
13
|
|
|
@@ -20,7 +22,6 @@ const mocks = vi.hoisted(() => ({
|
|
|
20
22
|
|
|
21
23
|
vi.mock("./gsapResizeIntercept", () => ({ tryGsapResizeIntercept: mocks.resize }));
|
|
22
24
|
vi.mock("./gsapRuntimeBridge", () => ({
|
|
23
|
-
POSITION_CHANNELS: ["x", "y"],
|
|
24
25
|
tryGsapDragIntercept: mocks.drag,
|
|
25
26
|
tryGsapRotationIntercept: vi.fn(),
|
|
26
27
|
}));
|
|
@@ -45,6 +46,11 @@ afterEach(() => {
|
|
|
45
46
|
vi.clearAllMocks();
|
|
46
47
|
});
|
|
47
48
|
|
|
49
|
+
/** An element GSAP already renders the transform of, so moves take the GSAP route. */
|
|
50
|
+
function gsapPositioned(tag: string): HTMLElement {
|
|
51
|
+
return Object.assign(document.createElement(tag), { _gsap: { renderTransform: () => {} } });
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
function mountResizeHandler(
|
|
49
55
|
animations: GsapAnimation[],
|
|
50
56
|
targetAnimations: GsapAnimation[] = animations,
|
|
@@ -56,6 +62,7 @@ function mountResizeHandler(
|
|
|
56
62
|
const anchorRollback = vi.fn();
|
|
57
63
|
const elementOffset = vi.fn(() => ({ save: anchorSave, rollback: anchorRollback }));
|
|
58
64
|
const commitMutation = vi.fn().mockResolvedValue(undefined);
|
|
65
|
+
const commitPatch = vi.fn().mockResolvedValue(undefined);
|
|
59
66
|
let resize:
|
|
60
67
|
| ((
|
|
61
68
|
selection: DomEditSelection,
|
|
@@ -64,8 +71,9 @@ function mountResizeHandler(
|
|
|
64
71
|
restore?: () => void,
|
|
65
72
|
) => Promise<void>)
|
|
66
73
|
| null = null;
|
|
74
|
+
let property: ReturnType<typeof useGsapAwareEditing>["commitAnimatedProperty"] | null = null;
|
|
67
75
|
function Harness() {
|
|
68
|
-
|
|
76
|
+
const editing = useGsapAwareEditing({
|
|
69
77
|
domEditSelection: selection,
|
|
70
78
|
selectedGsapAnimations: animations,
|
|
71
79
|
gsapCommitMutation: commitMutation,
|
|
@@ -76,11 +84,14 @@ function mountResizeHandler(
|
|
|
76
84
|
trackGsapInteractionFailure: vi.fn(),
|
|
77
85
|
stageElementPositionOffset: elementOffset,
|
|
78
86
|
handleDomBoxSizeCommit: fallback,
|
|
87
|
+
commitPositionPatchToHtml: commitPatch,
|
|
79
88
|
addGsapAnimation: vi.fn(),
|
|
80
89
|
convertToKeyframes: vi.fn(),
|
|
81
90
|
setArcPath: vi.fn(),
|
|
82
91
|
updateArcSegment: vi.fn(),
|
|
83
|
-
})
|
|
92
|
+
});
|
|
93
|
+
resize = editing.handleGsapAwareBoxSizeCommit;
|
|
94
|
+
property = editing.commitAnimatedProperty;
|
|
84
95
|
return null;
|
|
85
96
|
}
|
|
86
97
|
const root = mountReactHarness(<Harness />);
|
|
@@ -91,7 +102,9 @@ function mountResizeHandler(
|
|
|
91
102
|
anchorSave,
|
|
92
103
|
anchorRollback,
|
|
93
104
|
commitMutation,
|
|
105
|
+
commitPatch,
|
|
94
106
|
resize: resize!,
|
|
107
|
+
property: property!,
|
|
95
108
|
root,
|
|
96
109
|
};
|
|
97
110
|
}
|
|
@@ -122,6 +135,7 @@ function mountGroupHandler({
|
|
|
122
135
|
trackGsapInteractionFailure,
|
|
123
136
|
stageElementPositionOffset,
|
|
124
137
|
handleDomBoxSizeCommit: vi.fn(),
|
|
138
|
+
commitPositionPatchToHtml: vi.fn(),
|
|
125
139
|
addGsapAnimation: vi.fn(),
|
|
126
140
|
convertToKeyframes: vi.fn(),
|
|
127
141
|
setArcPath: vi.fn(),
|
|
@@ -140,6 +154,28 @@ function mountGroupHandler({
|
|
|
140
154
|
};
|
|
141
155
|
}
|
|
142
156
|
|
|
157
|
+
describe("useGsapAwareEditing moves of an element GSAP does not position", () => {
|
|
158
|
+
it("saves the move on the element itself, with no GSAP write and no animation read", async () => {
|
|
159
|
+
const save = vi.fn().mockResolvedValue(undefined);
|
|
160
|
+
const stageElementPositionOffset = vi.fn(() => ({ save, rollback: vi.fn() }));
|
|
161
|
+
const gsapCommitMutation = vi.fn();
|
|
162
|
+
const makeFetchFallback = vi.fn();
|
|
163
|
+
const { pathOffsetCommit, root } = mountGroupHandler({
|
|
164
|
+
gsapCommitMutation,
|
|
165
|
+
makeFetchFallback,
|
|
166
|
+
stageElementPositionOffset,
|
|
167
|
+
});
|
|
168
|
+
const box = { element: document.createElement("div"), id: "box", selector: "#box" };
|
|
169
|
+
await act(() => pathOffsetCommit(box as unknown as DomEditSelection, { x: 130.25, y: 90 }));
|
|
170
|
+
expect(stageElementPositionOffset).toHaveBeenCalledWith(box, { x: 130.25, y: 90 });
|
|
171
|
+
expect(save).toHaveBeenCalledTimes(1);
|
|
172
|
+
expect(gsapCommitMutation).not.toHaveBeenCalled();
|
|
173
|
+
expect(makeFetchFallback).not.toHaveBeenCalled();
|
|
174
|
+
expect(mocks.drag).not.toHaveBeenCalled();
|
|
175
|
+
act(() => root.unmount());
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
143
179
|
describe("useGsapAwareEditing shared-tween moves", () => {
|
|
144
180
|
it("saves a single drag through the element's own offset", async () => {
|
|
145
181
|
mocks.drag.mockResolvedValue({ status: "element-offset" });
|
|
@@ -151,7 +187,7 @@ describe("useGsapAwareEditing shared-tween moves", () => {
|
|
|
151
187
|
makeFetchFallback: () => vi.fn().mockResolvedValue([]),
|
|
152
188
|
stageElementPositionOffset,
|
|
153
189
|
});
|
|
154
|
-
const word = { element:
|
|
190
|
+
const word = { element: gsapPositioned("span"), hfId: "w0", selector: ".w" };
|
|
155
191
|
await act(() => pathOffsetCommit(word as unknown as DomEditSelection, { x: 40, y: 20 }));
|
|
156
192
|
expect(stageElementPositionOffset).toHaveBeenCalledWith(word, { x: 40, y: 20 });
|
|
157
193
|
expect(save).toHaveBeenCalledTimes(1);
|
|
@@ -159,7 +195,7 @@ describe("useGsapAwareEditing shared-tween moves", () => {
|
|
|
159
195
|
act(() => root.unmount());
|
|
160
196
|
});
|
|
161
197
|
|
|
162
|
-
it("saves
|
|
198
|
+
it("saves shared-tween and GSAP-free group members on themselves, under the group key", async () => {
|
|
163
199
|
mocks.drag.mockImplementation(async (selection, _next, _a, _i, commit, _f, options) => {
|
|
164
200
|
if (selection.hfId === "w0") return { status: "element-offset" };
|
|
165
201
|
if (!options?.preflightOnly) await commit(selection, { type: "move" }, { label: "Move" });
|
|
@@ -172,19 +208,20 @@ describe("useGsapAwareEditing shared-tween moves", () => {
|
|
|
172
208
|
makeFetchFallback: () => vi.fn().mockResolvedValue([]),
|
|
173
209
|
stageElementPositionOffset,
|
|
174
210
|
});
|
|
175
|
-
const word = { element:
|
|
176
|
-
const box = { element:
|
|
211
|
+
const word = { element: gsapPositioned("span"), hfId: "w0", selector: ".w" };
|
|
212
|
+
const box = { element: gsapPositioned("div"), id: "box", selector: "#box" };
|
|
213
|
+
const plain = { element: document.createElement("div"), id: "plain", selector: "#plain" };
|
|
177
214
|
await act(() =>
|
|
178
215
|
groupCommit([
|
|
179
216
|
{ selection: word, next: { x: 40, y: 20 } },
|
|
180
217
|
{ selection: box, next: { x: 40, y: 20 } },
|
|
218
|
+
{ selection: plain, next: { x: 40, y: 20 } },
|
|
181
219
|
] as unknown as DomEditGroupPathOffsetCommit[]),
|
|
182
220
|
);
|
|
183
|
-
expect(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
);
|
|
221
|
+
const groupKey = expect.stringMatching(/^group-drag:\d+$/);
|
|
222
|
+
expect(stageElementPositionOffset).toHaveBeenCalledWith(word, { x: 40, y: 20 }, groupKey);
|
|
223
|
+
expect(stageElementPositionOffset).toHaveBeenCalledWith(plain, { x: 40, y: 20 }, groupKey);
|
|
224
|
+
expect(mocks.drag.mock.calls.some((call) => call[0] === plain)).toBe(false);
|
|
188
225
|
act(() => root.unmount());
|
|
189
226
|
});
|
|
190
227
|
});
|
|
@@ -197,7 +234,7 @@ describe("useGsapAwareEditing anchored resize", () => {
|
|
|
197
234
|
const h = mountResizeHandler([humanAnimation], [targetAnimation]);
|
|
198
235
|
const target = {
|
|
199
236
|
...h.selection,
|
|
200
|
-
element:
|
|
237
|
+
element: gsapPositioned("div"),
|
|
201
238
|
id: "agent-target",
|
|
202
239
|
selector: "#agent-target",
|
|
203
240
|
} as DomEditSelection;
|
|
@@ -257,6 +294,92 @@ describe("useGsapAwareEditing anchored resize", () => {
|
|
|
257
294
|
act(() => h.root.unmount());
|
|
258
295
|
});
|
|
259
296
|
|
|
297
|
+
function mountCropped() {
|
|
298
|
+
const h = mountResizeHandler([]);
|
|
299
|
+
const el = withInlineLayoutBox(h.selection.element);
|
|
300
|
+
el.id = "clip";
|
|
301
|
+
el.style.cssText = "width: 300px; height: 200px; clip-path: inset(0px 60px 0px 0px)";
|
|
302
|
+
document.body.append(el);
|
|
303
|
+
const rect = { left: 0, top: 0, width: 300, height: 200, editScaleX: 1, editScaleY: 1 };
|
|
304
|
+
const crop = mountReactHarness(
|
|
305
|
+
<DomEditCropHandles selection={h.selection} overlayRect={rect} onStyleCommit={vi.fn()} />,
|
|
306
|
+
);
|
|
307
|
+
return { ...h, el, crop };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function resizeLandsWhen(where: "intercept" | "dispatch", h: { commitMutation: Mock }) {
|
|
311
|
+
const land = (el: HTMLElement) => {
|
|
312
|
+
el.style.width = "450px";
|
|
313
|
+
el.style.height = "300px";
|
|
314
|
+
};
|
|
315
|
+
mocks.resize.mockImplementation(async (selection, _next, _a, _i, commit) => {
|
|
316
|
+
if (where === "intercept") land(selection.element);
|
|
317
|
+
await commit(selection, { type: "size" }, { label: "Resize", softReload: true });
|
|
318
|
+
return { status: "persisted" };
|
|
319
|
+
});
|
|
320
|
+
if (where === "dispatch") h.commitMutation.mockImplementation(async (s) => land(s.element));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const scaled = "inset(0px 90px 0px 0px)";
|
|
324
|
+
|
|
325
|
+
it("saves the crop scaled with a size that lands only when saved (the W/H fields)", async () => {
|
|
326
|
+
const h = mountCropped();
|
|
327
|
+
resizeLandsWhen("dispatch", h);
|
|
328
|
+
await act(() => h.resize(h.selection, { width: 450, height: 300 }));
|
|
329
|
+
|
|
330
|
+
expect(h.commitPatch).toHaveBeenCalledWith(
|
|
331
|
+
h.selection,
|
|
332
|
+
[{ type: "inline-style", property: "clip-path", value: scaled }],
|
|
333
|
+
expect.objectContaining({ coalesceKey: h.commitMutation.mock.calls[0]![2].coalesceKey }),
|
|
334
|
+
);
|
|
335
|
+
act(() => h.crop.unmount());
|
|
336
|
+
expect(h.el.style.getPropertyValue("clip-path")).toBe(scaled);
|
|
337
|
+
act(() => h.root.unmount());
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("scales from the box before a draft its caller applied (the agent tool)", async () => {
|
|
341
|
+
const h = mountCropped();
|
|
342
|
+
resizeLandsWhen("intercept", h);
|
|
343
|
+
applyStudioBoxSizeDraft(h.el, { width: 450, height: 300 });
|
|
344
|
+
await act(() => h.resize(h.selection, { width: 450, height: 300 }));
|
|
345
|
+
|
|
346
|
+
expect(h.commitPatch.mock.calls[0]![1]).toEqual([
|
|
347
|
+
{ type: "inline-style", property: "clip-path", value: scaled },
|
|
348
|
+
]);
|
|
349
|
+
act(() => h.crop.unmount());
|
|
350
|
+
act(() => h.root.unmount());
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("a W field on an animated element saves the scaled crop in the same undo step", async () => {
|
|
354
|
+
const h = mountCropped();
|
|
355
|
+
mocks.commitAnimatedProperties.mockImplementation(async (selection, _props, keyed) => {
|
|
356
|
+
await (keyed ?? h.commitMutation)(selection, { type: "set" }, { label: "Edit width" });
|
|
357
|
+
selection.element.style.width = "450px";
|
|
358
|
+
});
|
|
359
|
+
await act(() => h.property(h.selection, "width", 450));
|
|
360
|
+
|
|
361
|
+
const key = h.commitMutation.mock.calls[0]![2].coalesceKey;
|
|
362
|
+
expect(key).toBeTruthy();
|
|
363
|
+
expect(h.commitPatch).toHaveBeenCalledWith(
|
|
364
|
+
h.selection,
|
|
365
|
+
[{ type: "inline-style", property: "clip-path", value: "inset(0px 90px 0px 0px)" }],
|
|
366
|
+
expect.objectContaining({ coalesceKey: key }),
|
|
367
|
+
);
|
|
368
|
+
act(() => h.crop.unmount());
|
|
369
|
+
act(() => h.root.unmount());
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("shows the scaled crop when deselected before the save lands", async () => {
|
|
373
|
+
const h = mountCropped();
|
|
374
|
+
resizeLandsWhen("intercept", h);
|
|
375
|
+
const saved = h.resize(h.selection, { width: 450, height: 300 });
|
|
376
|
+
act(() => h.crop.unmount());
|
|
377
|
+
await act(() => saved);
|
|
378
|
+
|
|
379
|
+
expect(h.el.style.getPropertyValue("clip-path")).toBe(scaled);
|
|
380
|
+
act(() => h.root.unmount());
|
|
381
|
+
});
|
|
382
|
+
|
|
260
383
|
it("does not keep the anchor move when the size save fails", async () => {
|
|
261
384
|
resizeWritesSize();
|
|
262
385
|
const h = mountResizeHandler([]);
|
|
@@ -288,7 +411,7 @@ describe("useGsapAwareEditing anchored resize", () => {
|
|
|
288
411
|
commit = h.resize(h.selection, { width: 300, height: 200 }, { x: -50.2, y: -25.6 });
|
|
289
412
|
});
|
|
290
413
|
|
|
291
|
-
expect(mocks.setPosition).toHaveBeenCalledWith(h.selection.element, 70,
|
|
414
|
+
expect(mocks.setPosition).toHaveBeenCalledWith(h.selection.element, 70.2, 54.6);
|
|
292
415
|
expect(mocks.setPosition.mock.invocationCallOrder[0]).toBeLessThan(
|
|
293
416
|
mocks.resize.mock.invocationCallOrder[0]!,
|
|
294
417
|
);
|
|
@@ -351,11 +474,11 @@ describe("useGsapAwareEditing anchored resize", () => {
|
|
|
351
474
|
});
|
|
352
475
|
const updates = [
|
|
353
476
|
{
|
|
354
|
-
selection: { element:
|
|
477
|
+
selection: { element: gsapPositioned("div"), id: "a", selector: "#a" },
|
|
355
478
|
next: { x: 10, y: 10 },
|
|
356
479
|
},
|
|
357
480
|
{
|
|
358
|
-
selection: { element:
|
|
481
|
+
selection: { element: gsapPositioned("div"), id: "b", selector: "#b" },
|
|
359
482
|
next: { x: 10, y: 10 },
|
|
360
483
|
},
|
|
361
484
|
] as unknown as DomEditGroupPathOffsetCommit[];
|
|
@@ -389,12 +512,12 @@ describe("useGsapAwareEditing anchored resize", () => {
|
|
|
389
512
|
});
|
|
390
513
|
const updates = [
|
|
391
514
|
{
|
|
392
|
-
selection: { element:
|
|
515
|
+
selection: { element: gsapPositioned("div"), id: "ok", selector: "#ok" },
|
|
393
516
|
next: { x: 10, y: 10 },
|
|
394
517
|
},
|
|
395
518
|
{
|
|
396
519
|
selection: {
|
|
397
|
-
element:
|
|
520
|
+
element: gsapPositioned("div"),
|
|
398
521
|
id: "blocked",
|
|
399
522
|
selector: "#blocked",
|
|
400
523
|
},
|
|
@@ -430,7 +553,7 @@ describe("useGsapAwareEditing anchored resize", () => {
|
|
|
430
553
|
});
|
|
431
554
|
const updates = [
|
|
432
555
|
{
|
|
433
|
-
selection: { element:
|
|
556
|
+
selection: { element: gsapPositioned("div"), id: "a", selector: "#a" },
|
|
434
557
|
next: { x: 10, y: 10 },
|
|
435
558
|
},
|
|
436
559
|
] as unknown as DomEditGroupPathOffsetCommit[];
|
|
@@ -455,11 +578,11 @@ describe("useGsapAwareEditing anchored resize", () => {
|
|
|
455
578
|
});
|
|
456
579
|
const updates = [
|
|
457
580
|
{
|
|
458
|
-
selection: { element:
|
|
581
|
+
selection: { element: gsapPositioned("div"), id: "a", selector: "#a" },
|
|
459
582
|
next: { x: 10, y: 10 },
|
|
460
583
|
},
|
|
461
584
|
{
|
|
462
|
-
selection: { element:
|
|
585
|
+
selection: { element: gsapPositioned("div"), id: "b", selector: "#b" },
|
|
463
586
|
next: { x: 20, y: 20 },
|
|
464
587
|
},
|
|
465
588
|
] as unknown as DomEditGroupPathOffsetCommit[];
|
|
@@ -509,6 +632,7 @@ describe("useGsapAwareEditing anchored resize", () => {
|
|
|
509
632
|
*/
|
|
510
633
|
it("applies the anchor when the resize leaves the drop point to the caller", async () => {
|
|
511
634
|
mocks.resize.mockResolvedValue({ status: "persisted" });
|
|
635
|
+
mocks.drag.mockResolvedValue({ status: "persisted" });
|
|
512
636
|
const scale = { propertyGroup: "scale" } as GsapAnimation;
|
|
513
637
|
const h = mountResizeHandler([scale]);
|
|
514
638
|
await act(() => h.resize(h.selection, { width: 300, height: 200 }, { x: -50, y: -25 }));
|
|
@@ -10,11 +10,7 @@
|
|
|
10
10
|
import { useCallback } from "react";
|
|
11
11
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
12
12
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
13
|
-
import {
|
|
14
|
-
POSITION_CHANNELS,
|
|
15
|
-
tryGsapDragIntercept,
|
|
16
|
-
tryGsapRotationIntercept,
|
|
17
|
-
} from "./gsapRuntimeBridge";
|
|
13
|
+
import { tryGsapDragIntercept, tryGsapRotationIntercept } from "./gsapRuntimeBridge";
|
|
18
14
|
import { tryGsapResizeIntercept } from "./gsapResizeIntercept";
|
|
19
15
|
import { computeDraggedGsapPosition } from "./draggedGsapPosition";
|
|
20
16
|
import { readGsapPositionFromIframe } from "./gsapPositionDetection";
|
|
@@ -33,15 +29,19 @@ import { setElementGsapPosition } from "../utils/elementGsap";
|
|
|
33
29
|
import { logResize, logResizeSettle } from "../utils/resizeDebug";
|
|
34
30
|
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
|
35
31
|
import { runGestureTransaction } from "./gestureTransaction";
|
|
36
|
-
import {
|
|
37
|
-
|
|
32
|
+
import {
|
|
33
|
+
gsapWritesPosition,
|
|
34
|
+
hasNonHoldTweenForElement,
|
|
35
|
+
POSITION_CHANNELS,
|
|
36
|
+
} from "./gsapRuntimeKeyframes";
|
|
37
|
+
import { assertGsapEditPersisted, saveMove } from "./gsapEditOutcome";
|
|
38
38
|
import type { GsapAnimationFetchOptions } from "./useGsapAnimationFetchFallback";
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
39
|
+
import type { ElementOffsetStagerDeps } from "./elementOffsetStager";
|
|
40
|
+
import {
|
|
41
|
+
prepareCropResize,
|
|
42
|
+
saveCropResize,
|
|
43
|
+
writeSizeWithCrop,
|
|
44
|
+
} from "../components/editor/cropResize";
|
|
45
45
|
|
|
46
46
|
// Distinct coalesceKey per group drag so consecutive group drags don't fold
|
|
47
47
|
// into one another's undo entry (module-local counter, not Date.now()).
|
|
@@ -87,6 +87,7 @@ export interface UseGsapAwareEditingParams {
|
|
|
87
87
|
next: { width: number; height: number },
|
|
88
88
|
offset?: { x: number; y: number },
|
|
89
89
|
) => Promise<void>;
|
|
90
|
+
commitPositionPatchToHtml: ElementOffsetStagerDeps["commitPositionPatchToHtml"];
|
|
90
91
|
// GSAP script commit ops (from useGsapScriptCommits)
|
|
91
92
|
addGsapAnimation: (
|
|
92
93
|
sel: DomEditSelection,
|
|
@@ -130,6 +131,7 @@ export function useGsapAwareEditing({
|
|
|
130
131
|
trackGsapInteractionFailure,
|
|
131
132
|
stageElementPositionOffset,
|
|
132
133
|
handleDomBoxSizeCommit,
|
|
134
|
+
commitPositionPatchToHtml,
|
|
133
135
|
addGsapAnimation,
|
|
134
136
|
convertToKeyframes,
|
|
135
137
|
setArcPath,
|
|
@@ -151,6 +153,8 @@ export function useGsapAwareEditing({
|
|
|
151
153
|
next: { x: number; y: number },
|
|
152
154
|
modifiers?: { altKey?: boolean },
|
|
153
155
|
) => {
|
|
156
|
+
if (!gsapWritesPosition(selection.element))
|
|
157
|
+
return stageElementPositionOffset(selection, next).save();
|
|
154
158
|
if (gsapCommitMutation) {
|
|
155
159
|
try {
|
|
156
160
|
const ownedAnimations = getGsapAnimationsForSelection(selection);
|
|
@@ -183,11 +187,8 @@ export function useGsapAwareEditing({
|
|
|
183
187
|
],
|
|
184
188
|
);
|
|
185
189
|
|
|
186
|
-
// Multi-select (group) drag:
|
|
187
|
-
//
|
|
188
|
-
// gsap.set) — NEVER the deprecated `--hf-studio-offset` CSS var, and GSAP-animated
|
|
189
|
-
// elements are no longer blocked in a group. No CSS fallback: with no GSAP
|
|
190
|
-
// composition there's nothing to write (a no-op, exactly like the single-drag path).
|
|
190
|
+
// Multi-select (group) drag: each member takes the single drag's writer, so a member GSAP
|
|
191
|
+
// does not position is saved on itself and the rest go through the GSAP intercept.
|
|
191
192
|
const handleGsapAwareGroupPathOffsetCommit = useCallback(
|
|
192
193
|
async (updates: DomEditGroupPathOffsetCommit[]) => {
|
|
193
194
|
if (!gsapCommitMutation || updates.length === 0) return;
|
|
@@ -245,6 +246,7 @@ export function useGsapAwareEditing({
|
|
|
245
246
|
// turns N sequential round trips into one.
|
|
246
247
|
const preflightResults = await Promise.allSettled(
|
|
247
248
|
updates.map(async ({ selection }) => {
|
|
249
|
+
if (!gsapWritesPosition(selection.element)) return void offsetMembers.add(selection);
|
|
248
250
|
const animations = await makeFetchFallback(selection, { failOnFetchError: true })();
|
|
249
251
|
preflightAnimations.set(selection, animations);
|
|
250
252
|
const outcome = await tryGsapDragIntercept(
|
|
@@ -353,6 +355,8 @@ export function useGsapAwareEditing({
|
|
|
353
355
|
animGroups: targetAnimations.map((a) => `${a.propertyGroup}:${a.method}`),
|
|
354
356
|
});
|
|
355
357
|
let anchorMove: ReturnType<typeof stageElementPositionOffset> | null = null;
|
|
358
|
+
const stageCrop = prepareCropResize(selection.element);
|
|
359
|
+
let cropUndoKey: string | null = null;
|
|
356
360
|
return runGestureTransaction({
|
|
357
361
|
element: selection.element,
|
|
358
362
|
label: "Resize layer",
|
|
@@ -381,6 +385,7 @@ export function useGsapAwareEditing({
|
|
|
381
385
|
makeFetchFallback(selection),
|
|
382
386
|
);
|
|
383
387
|
assertGsapEditPersisted(outcome);
|
|
388
|
+
cropUndoKey = coalesceKey;
|
|
384
389
|
// What the resize actually did, not what its animations suggest
|
|
385
390
|
// it would do. An element whose scale is an instant hold has a
|
|
386
391
|
// scale-group tween and still commits width/height, so guessing
|
|
@@ -424,7 +429,13 @@ export function useGsapAwareEditing({
|
|
|
424
429
|
logResizeSettle(selection.element, "dom-route");
|
|
425
430
|
await handleDomBoxSizeCommit(selection, next, offset);
|
|
426
431
|
},
|
|
427
|
-
afterBufferedCommitsSaved: () =>
|
|
432
|
+
afterBufferedCommitsSaved: async () => {
|
|
433
|
+
await anchorMove?.save();
|
|
434
|
+
// Only now is the size live for every caller, drag or not.
|
|
435
|
+
if (cropUndoKey) {
|
|
436
|
+
await saveCropResize(stageCrop, selection, commitPositionPatchToHtml, cropUndoKey);
|
|
437
|
+
}
|
|
438
|
+
},
|
|
428
439
|
restore: () => {
|
|
429
440
|
anchorMove?.rollback();
|
|
430
441
|
restore();
|
|
@@ -434,6 +445,7 @@ export function useGsapAwareEditing({
|
|
|
434
445
|
},
|
|
435
446
|
[
|
|
436
447
|
handleDomBoxSizeCommit,
|
|
448
|
+
commitPositionPatchToHtml,
|
|
437
449
|
stageElementPositionOffset,
|
|
438
450
|
gsapCommitMutation,
|
|
439
451
|
previewIframeRef,
|
|
@@ -481,10 +493,7 @@ export function useGsapAwareEditing({
|
|
|
481
493
|
|
|
482
494
|
// ── Animated property commit ──
|
|
483
495
|
|
|
484
|
-
const {
|
|
485
|
-
commitAnimatedProperty: commitAnimatedPropertyRaw,
|
|
486
|
-
commitAnimatedProperties: commitAnimatedPropertiesRaw,
|
|
487
|
-
} = useAnimatedPropertyCommit({
|
|
496
|
+
const { commitAnimatedProperties: commitAnimatedPropertiesRaw } = useAnimatedPropertyCommit({
|
|
488
497
|
selectedGsapAnimations,
|
|
489
498
|
gsapCommitMutation,
|
|
490
499
|
addGsapAnimation: (sel, method, time) => addGsapAnimation(sel, method, time),
|
|
@@ -496,25 +505,30 @@ export function useGsapAwareEditing({
|
|
|
496
505
|
const commitAnimatedProperties = useCallback(
|
|
497
506
|
async (selection: DomEditSelection, properties: Record<string, number | string>) => {
|
|
498
507
|
try {
|
|
499
|
-
await
|
|
508
|
+
await writeSizeWithCrop(
|
|
509
|
+
selection,
|
|
510
|
+
properties,
|
|
511
|
+
gsapCommitMutation,
|
|
512
|
+
commitPositionPatchToHtml,
|
|
513
|
+
(keyed) => commitAnimatedPropertiesRaw(selection, properties, keyed),
|
|
514
|
+
);
|
|
500
515
|
} catch (error) {
|
|
501
516
|
trackGsapInteractionFailure(error, selection, "property", "Edit animated property");
|
|
502
517
|
throw error;
|
|
503
518
|
}
|
|
504
519
|
},
|
|
505
|
-
[
|
|
520
|
+
[
|
|
521
|
+
commitAnimatedPropertiesRaw,
|
|
522
|
+
commitPositionPatchToHtml,
|
|
523
|
+
gsapCommitMutation,
|
|
524
|
+
trackGsapInteractionFailure,
|
|
525
|
+
],
|
|
506
526
|
);
|
|
507
527
|
|
|
508
528
|
const commitAnimatedProperty = useCallback(
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
} catch (error) {
|
|
513
|
-
trackGsapInteractionFailure(error, selection, "property", "Edit animated property");
|
|
514
|
-
throw error;
|
|
515
|
-
}
|
|
516
|
-
},
|
|
517
|
-
[commitAnimatedPropertyRaw, trackGsapInteractionFailure],
|
|
529
|
+
(selection: DomEditSelection, property: string, value: number | string) =>
|
|
530
|
+
commitAnimatedProperties(selection, { [property]: value }),
|
|
531
|
+
[commitAnimatedProperties],
|
|
518
532
|
);
|
|
519
533
|
|
|
520
534
|
// ── Arc path wrappers ──
|