@hyperframes/studio 0.8.114 → 0.8.115
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-aXNJKiIg.js → hyperframes-player-C-9N8nk0.js} +1 -1
- package/dist/assets/{index-Bn2RJNTJ.js → index-BoUM6Xqb.js} +1 -1
- package/dist/assets/{index-Dz7doz9l.js → index-CAvZ0AA-.js} +201 -201
- package/dist/assets/{index-C9N-DGjC.js → index-CVuorIMv.js} +1 -1
- package/dist/assets/{index-BYnsJnJV.css → index-t1LH6H5H.css} +1 -1
- package/dist/{chunk-K47E2ZJA.js → chunk-5EKWJZLM.js} +504 -17
- package/dist/chunk-5EKWJZLM.js.map +1 -0
- package/dist/{chunk-VWT7STY3.js → chunk-J6Q572FA.js} +2 -2
- package/dist/{chunk-BIUXP3PG.js → chunk-TXZMMOMA.js} +43 -276
- package/dist/chunk-TXZMMOMA.js.map +1 -0
- package/dist/{domEditingLayers-V67MS4WP.js → domEditingLayers-D7VYFJJD.js} +3 -5
- package/dist/index.d.ts +43 -19
- package/dist/index.html +2 -2
- package/dist/index.js +2939 -2596
- 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 +2 -2
- package/src/components/LinkedSelectionToggle.tsx +3 -2
- package/src/components/dock/Dock.tsx +5 -7
- package/src/components/editor/CanvasContextMenu.tsx +3 -2
- package/src/components/editor/DomEditCropHandles.test.tsx +27 -0
- package/src/components/editor/DomEditCropHandles.tsx +7 -4
- package/src/components/editor/DomEditOverlay.tsx +5 -1
- package/src/components/editor/FileTreeNodes.tsx +13 -10
- package/src/components/editor/MotionPathOverlay.tsx +7 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +12 -2
- package/src/components/editor/SnapToolbar.test.tsx +100 -1
- package/src/components/editor/SnapToolbar.tsx +43 -5
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +48 -1
- package/src/components/editor/audioFxTelemetry.ts +2 -3
- package/src/components/editor/commitPreflightHandles.test.tsx +2 -2
- package/src/components/editor/cropResize.ts +4 -3
- package/src/components/editor/domEditingDom.ts +4 -0
- package/src/components/editor/domEditingLayers.ts +1 -5
- package/src/components/editor/manualEdits.ts +1 -0
- package/src/components/editor/manualEditsParsing.ts +7 -0
- package/src/components/editor/marqueeCommit.test.tsx +106 -37
- package/src/components/editor/marqueeCommit.ts +24 -8
- package/src/components/editor/motionPathCommit.test.ts +29 -1
- package/src/components/editor/motionPathCommit.ts +21 -9
- package/src/components/editor/probeSourceElement.test.ts +45 -0
- package/src/components/editor/probeSourceElement.ts +39 -15
- package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -7
- package/src/components/editor/propertyPanelFxAddMenu.tsx +11 -2
- package/src/components/editor/propertyPanelFxCarveModule.tsx +15 -5
- package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +3 -0
- package/src/components/editor/propertyPanelFxNodeParams.tsx +15 -2
- package/src/components/editor/propertyPanelFxNodeRow.tsx +3 -0
- package/src/components/editor/propertyPanelFxPresetRun.tsx +10 -2
- package/src/components/editor/propertyPanelFxSection.test.tsx +56 -0
- package/src/components/editor/propertyPanelFxSection.tsx +30 -28
- package/src/components/editor/propertyPanelTypes.ts +6 -3
- package/src/components/editor/useDomEditNudge.test.tsx +37 -0
- package/src/components/editor/useDomEditNudge.ts +2 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +5 -0
- package/src/components/editor/useDomEditZOrder.ts +12 -2
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +1 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +7 -1
- package/src/components/sidebar/AssetContextMenu.tsx +4 -4
- package/src/components/ui/menuStyle.test.ts +61 -0
- package/src/components/ui/menuStyle.ts +10 -0
- package/src/contexts/DomEditContext.tsx +1 -1
- package/src/contexts/TimelineEditContext.tsx +1 -0
- package/src/domEditExports.test.tsx +9 -1
- package/src/hooks/domEditCommitRunner.ts +1 -1
- package/src/hooks/elementOffsetStager.ts +5 -4
- package/src/hooks/gsapDragCommit.ts +2 -20
- package/src/hooks/gsapDragPositionCommit.ts +18 -3
- package/src/hooks/gsapGestureOutcome.test.ts +161 -0
- package/src/hooks/gsapGestureOutcome.ts +64 -0
- package/src/hooks/gsapKeyframeCommit.ts +3 -12
- package/src/hooks/gsapResizeIntercept.test.ts +33 -0
- package/src/hooks/gsapResizeIntercept.ts +1 -0
- package/src/hooks/gsapScriptCommitTypes.ts +5 -0
- package/src/hooks/gsapShared.writeTarget.test.ts +20 -0
- package/src/hooks/keyframeCacheAstLoad.ts +6 -5
- package/src/hooks/plainRotation.ts +3 -2
- package/src/hooks/previewPromotionSelection.test.tsx +1 -1
- package/src/hooks/refreshTimelineRowText.test.ts +65 -0
- package/src/hooks/refreshTimelineRowText.ts +29 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +46 -5
- package/src/hooks/useAnimatedPropertyCommit.ts +2 -2
- package/src/hooks/useBlockedTimelineEditToast.ts +12 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +18 -23
- package/src/hooks/useDomEditTextCommits.test.tsx +18 -0
- package/src/hooks/useDomEditTextCommits.ts +5 -1
- package/src/hooks/useDomEditWiring.ts +4 -2
- package/src/hooks/useDomGeometryCommit.test.tsx +25 -6
- package/src/hooks/useDomGeometryCommit.ts +7 -4
- package/src/hooks/useDomGeometryCommits.test.tsx +1 -0
- package/src/hooks/useDomGeometryCommits.ts +3 -2
- package/src/hooks/useDomSelection.test.ts +26 -0
- package/src/hooks/useDomSelection.ts +10 -3
- package/src/hooks/useDomSelectionTypes.ts +6 -1
- package/src/hooks/useEnableKeyframes.test.ts +2 -2
- package/src/hooks/useEnableKeyframes.ts +5 -3
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +102 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +31 -6
- package/src/hooks/useGestureCommit.test.tsx +225 -55
- package/src/hooks/useGestureCommit.ts +86 -55
- package/src/hooks/useGsapAwareEditing.test.tsx +11 -6
- package/src/hooks/useGsapAwareEditing.ts +96 -195
- package/src/hooks/useGsapAwareGroupMove.ts +179 -0
- package/src/hooks/useGsapKeyframeOps.sdkUsage.test.tsx +114 -0
- package/src/hooks/useGsapKeyframeOps.ts +36 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +57 -0
- package/src/hooks/useGsapScriptCommits.ts +3 -0
- package/src/hooks/useGsapSelectionHandlers.test.tsx +1 -1
- package/src/hooks/useGsapSelectionHandlers.ts +24 -18
- package/src/hooks/useGsapTweenCache.ts +18 -12
- package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +76 -2
- package/src/hooks/usePreviewInteraction.test.ts +21 -0
- package/src/hooks/usePreviewInteraction.ts +2 -0
- package/src/hooks/useRenderClipContent.test.ts +18 -0
- package/src/hooks/useRenderClipContent.ts +4 -5
- package/src/hooks/useTimelineDeleteOps.test.tsx +30 -22
- package/src/hooks/useTimelineDeleteOps.ts +21 -27
- package/src/hooks/useTimelineEditing.test.tsx +1 -1
- package/src/player/components/AutomationSelectionMenu.tsx +3 -2
- package/src/player/components/ClipContextMenu.test.tsx +73 -0
- package/src/player/components/ClipContextMenu.tsx +33 -31
- package/src/player/components/KeyframeDiamondContextMenu.tsx +8 -9
- package/src/player/components/OutOfSyncBadge.tsx +5 -5
- package/src/player/components/TextClipContent.test.ts +19 -0
- package/src/player/components/TextClipContent.tsx +27 -0
- package/src/player/components/TrackGapContextMenu.tsx +3 -6
- package/src/player/components/clipMenuToolItems.test.tsx +23 -0
- package/src/player/components/clipMenuToolItems.tsx +13 -9
- package/src/player/components/timelineCallbacks.ts +1 -0
- package/src/player/components/timelineClipDragGestureLifecycle.ts +1 -1
- package/src/player/components/timelineClipGestureHandlers.test.ts +94 -0
- package/src/player/components/timelineClipGestureHandlers.ts +20 -2
- package/src/player/components/timelineEditing.ts +3 -1
- package/src/player/components/timelineLinkSelection.ts +35 -1
- package/src/player/components/useTimelineClipDrag.keyboard.test.tsx +14 -0
- package/src/player/components/useTimelineClipDrag.ts +6 -7
- package/src/player/hooks/timelinePlayerSync.test.ts +6 -0
- package/src/player/hooks/timelinePlayerSync.ts +6 -1
- package/src/player/lib/timelineElementHelpers.test.ts +87 -1
- package/src/player/lib/timelineElementHelpers.ts +6 -15
- package/src/player/lib/timelineText.ts +40 -0
- package/src/player/store/timelineElement.ts +12 -0
- package/src/styles/theme.css +3 -0
- package/src/utils/keyframeUsage.ts +79 -0
- package/src/utils/previewFeatureUsage.test.ts +32 -0
- package/src/utils/previewFeatureUsage.ts +46 -0
- package/src/utils/studioTelemetry.test.ts +107 -0
- package/src/utils/studioTelemetry.ts +49 -4
- package/src/webmcp/StudioAgentTools.tsx +2 -2
- package/dist/chunk-BIUXP3PG.js.map +0 -1
- package/dist/chunk-K47E2ZJA.js.map +0 -1
- /package/dist/{chunk-VWT7STY3.js.map → chunk-J6Q572FA.js.map} +0 -0
- /package/dist/{domEditingLayers-V67MS4WP.js.map → domEditingLayers-D7VYFJJD.js.map} +0 -0
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { useGsapAwareGroupMove } from "./useGsapAwareGroupMove";
|
|
2
|
+
import { observeGsapGesture } from "./gsapGestureOutcome";
|
|
3
|
+
import type { DomEditPersistOutcome } from "./domEditCommitTypes";
|
|
1
4
|
/**
|
|
2
5
|
* GSAP-aware move/resize/rotation wrappers that intercept geometry commits
|
|
3
6
|
* for animated elements and route them through script mutation instead of
|
|
@@ -21,14 +24,9 @@ import {
|
|
|
21
24
|
useGsapSaveFailureTelemetry,
|
|
22
25
|
useSafeGsapCommitMutation,
|
|
23
26
|
} from "./useSafeGsapCommitMutation";
|
|
24
|
-
import type {
|
|
25
|
-
CommitMutation,
|
|
26
|
-
CommitMutationCall,
|
|
27
|
-
CommitMutationOptions,
|
|
28
|
-
} from "./gsapScriptCommitTypes";
|
|
27
|
+
import type { CommitMutation, CommitMutationOptions } from "./gsapScriptCommitTypes";
|
|
29
28
|
import { setElementGsapPosition } from "../utils/elementGsap";
|
|
30
29
|
import { logResize, logResizeSettle } from "../utils/resizeDebug";
|
|
31
|
-
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
|
32
30
|
import type { MoveCommitOptions } from "../components/editor/domEditOverlayGestures";
|
|
33
31
|
import { runGestureTransaction } from "./gestureTransaction";
|
|
34
32
|
import {
|
|
@@ -40,29 +38,13 @@ import {
|
|
|
40
38
|
} from "./gsapRuntimeKeyframes";
|
|
41
39
|
import { assertGsapEditPersisted, saveMove } from "./gsapEditOutcome";
|
|
42
40
|
import type { GsapAnimationFetchOptions } from "./useGsapAnimationFetchFallback";
|
|
43
|
-
import {
|
|
41
|
+
import { type ElementOffsetStagerDeps } from "./elementOffsetStager";
|
|
44
42
|
import {
|
|
45
43
|
prepareCropResize,
|
|
46
44
|
saveCropResize,
|
|
47
45
|
writeSizeWithCrop,
|
|
48
46
|
} from "../components/editor/cropResize";
|
|
49
47
|
|
|
50
|
-
// Distinct coalesceKey per group drag so consecutive group drags don't fold
|
|
51
|
-
// into one another's undo entry (module-local counter, not Date.now()).
|
|
52
|
-
let groupDragCommitCounter = 0;
|
|
53
|
-
|
|
54
|
-
function firstPreflightFailure(
|
|
55
|
-
results: PromiseSettledResult<void>[],
|
|
56
|
-
updates: DomEditGroupPathOffsetCommit[],
|
|
57
|
-
): { error: unknown; selection: DomEditSelection } | null {
|
|
58
|
-
for (const [index, result] of results.entries()) {
|
|
59
|
-
if (result.status !== "rejected") continue;
|
|
60
|
-
const selection = updates[index]?.selection;
|
|
61
|
-
if (selection) return { error: result.reason, selection };
|
|
62
|
-
}
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
48
|
export interface UseGsapAwareEditingParams {
|
|
67
49
|
domEditSelection: DomEditSelection | null;
|
|
68
50
|
selectedGsapAnimations: GsapAnimation[];
|
|
@@ -86,15 +68,18 @@ export interface UseGsapAwareEditingParams {
|
|
|
86
68
|
next: { x: number; y: number },
|
|
87
69
|
plainTranslate: boolean,
|
|
88
70
|
coalesceKey?: string,
|
|
89
|
-
) => { save: () => Promise<
|
|
90
|
-
handleDomRotationCommit: (
|
|
71
|
+
) => { save: () => Promise<DomEditPersistOutcome | undefined>; rollback: () => void };
|
|
72
|
+
handleDomRotationCommit: (
|
|
73
|
+
selection: DomEditSelection,
|
|
74
|
+
next: RotationCommit,
|
|
75
|
+
) => Promise<DomEditPersistOutcome | undefined>;
|
|
91
76
|
handleDomBoxSizeCommit: (
|
|
92
77
|
selection: DomEditSelection,
|
|
93
78
|
next: { width: number; height: number },
|
|
94
79
|
offset?: { x: number; y: number },
|
|
95
80
|
restore?: () => void,
|
|
96
81
|
undoKey?: string,
|
|
97
|
-
) => Promise<
|
|
82
|
+
) => Promise<DomEditPersistOutcome | undefined>;
|
|
98
83
|
commitPositionPatchToHtml: ElementOffsetStagerDeps["commitPositionPatchToHtml"];
|
|
99
84
|
// GSAP script commit ops (from useGsapScriptCommits)
|
|
100
85
|
addGsapAnimation: (
|
|
@@ -162,10 +147,12 @@ export function useGsapAwareEditing({
|
|
|
162
147
|
next: { x: number; y: number },
|
|
163
148
|
modifiers?: MoveCommitOptions,
|
|
164
149
|
) => {
|
|
165
|
-
|
|
166
|
-
if (modifiers?.plainTranslate ?? !gsapWritesPosition(selection.element))
|
|
167
|
-
|
|
168
|
-
|
|
150
|
+
const writes = observeGsapGesture(gsapCommitMutation);
|
|
151
|
+
if (modifiers?.plainTranslate ?? !gsapWritesPosition(selection.element)) {
|
|
152
|
+
const result = await stageElementPositionOffset(selection, next, true).save();
|
|
153
|
+
return writes.finish(result?.changed === true);
|
|
154
|
+
}
|
|
155
|
+
if (writes.commit) {
|
|
169
156
|
try {
|
|
170
157
|
const ownedAnimations = getGsapAnimationsForSelection(selection);
|
|
171
158
|
const targetAnimations = Array.isArray(ownedAnimations)
|
|
@@ -176,11 +163,15 @@ export function useGsapAwareEditing({
|
|
|
176
163
|
next,
|
|
177
164
|
targetAnimations,
|
|
178
165
|
previewIframeRef.current,
|
|
179
|
-
|
|
166
|
+
writes.commit,
|
|
180
167
|
makeFetchFallback(selection),
|
|
181
168
|
modifiers,
|
|
182
169
|
);
|
|
183
|
-
await saveMove(outcome, () =>
|
|
170
|
+
await saveMove(outcome, async () => {
|
|
171
|
+
const result = await stageElementPositionOffset(selection, next, false).save();
|
|
172
|
+
writes.recordDomResult(result);
|
|
173
|
+
});
|
|
174
|
+
return writes.finish();
|
|
184
175
|
} catch (error) {
|
|
185
176
|
trackGsapInteractionFailure(error, selection, "drag", "Move animated layer");
|
|
186
177
|
throw error;
|
|
@@ -197,137 +188,14 @@ export function useGsapAwareEditing({
|
|
|
197
188
|
],
|
|
198
189
|
);
|
|
199
190
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
// otherwise each member records its own entry and it takes N presses to undo.
|
|
209
|
-
const coalesceKey = `group-drag:${++groupDragCommitCounter}`;
|
|
210
|
-
// Members are written one at a time, and a re-render re-runs the script with the OLD
|
|
211
|
-
// position of every member not yet written, so they snap back until their own write
|
|
212
|
-
// lands. The drafts are already on screen: hold the render until the last member.
|
|
213
|
-
let renderOnCommit = false;
|
|
214
|
-
const previewFallbackLatch = { pending: false };
|
|
215
|
-
const withGroupOptions = (options: CommitMutationOptions): CommitMutationOptions => ({
|
|
216
|
-
...options,
|
|
217
|
-
coalesceKey,
|
|
218
|
-
coalesceMs: Number.POSITIVE_INFINITY,
|
|
219
|
-
deferPreviewSync: !renderOnCommit,
|
|
220
|
-
previewFallbackLatch,
|
|
221
|
-
});
|
|
222
|
-
// Every member writes the same file. Queue their mutations and send them as
|
|
223
|
-
// ONE request instead of one round trip per member: the server reads, parses
|
|
224
|
-
// and writes the composition once, and the preview patches once.
|
|
225
|
-
const queued: CommitMutationCall[] = [];
|
|
226
|
-
const flushQueued = async () => {
|
|
227
|
-
if (queued.length === 0) return;
|
|
228
|
-
const calls = queued.splice(0, queued.length);
|
|
229
|
-
if (!gsapCommitMutation.batch) {
|
|
230
|
-
for (const call of calls) {
|
|
231
|
-
await gsapCommitMutation(call.selection, call.mutation, call.options);
|
|
232
|
-
}
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
await gsapCommitMutation.batch(calls, {
|
|
236
|
-
...(calls.at(-1)?.options ?? { label: "Move animated layer (group)" }),
|
|
237
|
-
label: "Move animated layer (group)",
|
|
238
|
-
});
|
|
239
|
-
};
|
|
240
|
-
const coalescedCommit: typeof gsapCommitMutation = (selection, mutation, options) => {
|
|
241
|
-
queued.push({ selection, mutation, options: withGroupOptions(options) });
|
|
242
|
-
return Promise.resolve();
|
|
243
|
-
};
|
|
244
|
-
const preflightAnimations = new Map<DomEditSelection, GsapAnimation[]>();
|
|
245
|
-
// Members saved on themselves, each with its route: true for its CSS translate.
|
|
246
|
-
const offsetMembers = new Map<DomEditSelection, boolean>();
|
|
247
|
-
// Editability is user-atomic: prove every member can be written before the first source
|
|
248
|
-
// mutation, so a blocked member never leaves earlier siblings partially moved. Preflights
|
|
249
|
-
// write nothing and share one in-flight parse per file, so they run together.
|
|
250
|
-
const preflightResults = await Promise.allSettled(
|
|
251
|
-
updates.map(async ({ selection, plainTranslate }) => {
|
|
252
|
-
if (plainTranslate ?? !gsapWritesPosition(selection.element)) {
|
|
253
|
-
refuseGsapTakeover(selection.element, showToast);
|
|
254
|
-
return void offsetMembers.set(selection, true);
|
|
255
|
-
}
|
|
256
|
-
const animations = await makeFetchFallback(selection, { failOnFetchError: true })();
|
|
257
|
-
preflightAnimations.set(selection, animations);
|
|
258
|
-
const outcome = await tryGsapDragIntercept(
|
|
259
|
-
selection,
|
|
260
|
-
{ x: 0, y: 0 },
|
|
261
|
-
animations,
|
|
262
|
-
previewIframeRef.current,
|
|
263
|
-
coalescedCommit,
|
|
264
|
-
undefined,
|
|
265
|
-
{ preflightOnly: true, group: true },
|
|
266
|
-
);
|
|
267
|
-
if (outcome.status === "element-offset") offsetMembers.set(selection, false);
|
|
268
|
-
assertGsapEditPersisted(outcome);
|
|
269
|
-
}),
|
|
270
|
-
);
|
|
271
|
-
const preflightFailure = firstPreflightFailure(preflightResults, updates);
|
|
272
|
-
if (preflightFailure) {
|
|
273
|
-
trackGsapInteractionFailure(
|
|
274
|
-
preflightFailure.error,
|
|
275
|
-
preflightFailure.selection,
|
|
276
|
-
"drag",
|
|
277
|
-
"Move animated layer (group)",
|
|
278
|
-
);
|
|
279
|
-
throw preflightFailure.error;
|
|
280
|
-
}
|
|
281
|
-
const lastScriptWrite = updates.findLastIndex(
|
|
282
|
-
({ selection }) => !offsetMembers.has(selection),
|
|
283
|
-
);
|
|
284
|
-
for (const [index, { selection, next }] of updates.entries()) {
|
|
285
|
-
renderOnCommit = index === lastScriptWrite;
|
|
286
|
-
const plain = offsetMembers.get(selection);
|
|
287
|
-
if (plain !== undefined) {
|
|
288
|
-
await stageElementPositionOffset(selection, next, plain, coalesceKey).save();
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
try {
|
|
292
|
-
const outcome = await tryGsapDragIntercept(
|
|
293
|
-
selection,
|
|
294
|
-
next,
|
|
295
|
-
preflightAnimations.get(selection) ?? [],
|
|
296
|
-
previewIframeRef.current,
|
|
297
|
-
coalescedCommit,
|
|
298
|
-
// The intercept re-reads the file to resolve a stale or shared tween.
|
|
299
|
-
// Anything already queued has to be on disk before that read, or it
|
|
300
|
-
// resolves against a file missing writes it is about to build on.
|
|
301
|
-
async () => {
|
|
302
|
-
await flushQueued();
|
|
303
|
-
return makeFetchFallback(selection, { fresh: true })();
|
|
304
|
-
},
|
|
305
|
-
{ preflightPassed: true },
|
|
306
|
-
);
|
|
307
|
-
assertGsapEditPersisted(outcome);
|
|
308
|
-
} catch (error) {
|
|
309
|
-
trackGsapInteractionFailure(error, selection, "drag", "Move animated layer (group)");
|
|
310
|
-
throw error;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
try {
|
|
314
|
-
await flushQueued();
|
|
315
|
-
} catch (error) {
|
|
316
|
-
// The aggregate write has no uniquely failing member; do not misattribute
|
|
317
|
-
// its telemetry to whichever member happened to be last in the array.
|
|
318
|
-
trackGsapInteractionFailure(error, null, "drag", "Move animated layer (group)");
|
|
319
|
-
throw error;
|
|
320
|
-
}
|
|
321
|
-
},
|
|
322
|
-
[
|
|
323
|
-
gsapCommitMutation,
|
|
324
|
-
previewIframeRef,
|
|
325
|
-
makeFetchFallback,
|
|
326
|
-
trackGsapInteractionFailure,
|
|
327
|
-
stageElementPositionOffset,
|
|
328
|
-
showToast,
|
|
329
|
-
],
|
|
330
|
-
);
|
|
191
|
+
const handleGsapAwareGroupPathOffsetCommit = useGsapAwareGroupMove({
|
|
192
|
+
gsapCommitMutation,
|
|
193
|
+
previewIframeRef,
|
|
194
|
+
makeFetchFallback,
|
|
195
|
+
trackGsapInteractionFailure,
|
|
196
|
+
stageElementPositionOffset,
|
|
197
|
+
showToast,
|
|
198
|
+
});
|
|
331
199
|
|
|
332
200
|
const handleGsapAwareBoxSizeCommit = useCallback(
|
|
333
201
|
async (
|
|
@@ -336,8 +204,11 @@ export function useGsapAwareEditing({
|
|
|
336
204
|
offset?: { x: number; y: number },
|
|
337
205
|
restore: () => void = () => undefined,
|
|
338
206
|
) => {
|
|
339
|
-
|
|
340
|
-
|
|
207
|
+
const writes = observeGsapGesture(gsapCommitMutation);
|
|
208
|
+
if (!gsapWritesBox(selection.element)) {
|
|
209
|
+
const result = await handleDomBoxSizeCommit(selection, next, offset, restore);
|
|
210
|
+
return writes.finish(result?.changed === true);
|
|
211
|
+
}
|
|
341
212
|
let targetAnimations: GsapAnimation[];
|
|
342
213
|
try {
|
|
343
214
|
const ownedAnimations = getGsapAnimationsForSelection(selection);
|
|
@@ -367,7 +238,28 @@ export function useGsapAwareEditing({
|
|
|
367
238
|
let anchorMove: ReturnType<typeof stageElementPositionOffset> | null = null;
|
|
368
239
|
const stageCrop = prepareCropResize(selection.element);
|
|
369
240
|
let cropUndoKey: string | null = null;
|
|
370
|
-
|
|
241
|
+
const saveResizeAnchor = async (
|
|
242
|
+
ownsDragOffset: boolean,
|
|
243
|
+
commitMutation: CommitMutation,
|
|
244
|
+
coalesceKey: string,
|
|
245
|
+
) => {
|
|
246
|
+
if (offset && !ownsDragOffset) {
|
|
247
|
+
const dragOutcome = await tryGsapDragIntercept(
|
|
248
|
+
selection,
|
|
249
|
+
offset,
|
|
250
|
+
targetAnimations,
|
|
251
|
+
previewIframeRef.current,
|
|
252
|
+
commitMutation,
|
|
253
|
+
makeFetchFallback(selection),
|
|
254
|
+
);
|
|
255
|
+
// Saved after the size, under its undo key, so the two are one step.
|
|
256
|
+
await saveMove(dragOutcome, async () => {
|
|
257
|
+
const plain = !gsapWritesPosition(selection.element);
|
|
258
|
+
anchorMove = stageElementPositionOffset(selection, offset, plain, coalesceKey);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
await runGestureTransaction({
|
|
371
263
|
element: selection.element,
|
|
372
264
|
label: "Resize layer",
|
|
373
265
|
settle: () => {
|
|
@@ -383,8 +275,8 @@ export function useGsapAwareEditing({
|
|
|
383
275
|
setElementGsapPosition(selection.element, newX, newY);
|
|
384
276
|
},
|
|
385
277
|
persist: async (commit, coalesceKey) => {
|
|
386
|
-
if (
|
|
387
|
-
const commitMutation = commit(
|
|
278
|
+
if (writes.commit) {
|
|
279
|
+
const commitMutation = commit(writes.commit);
|
|
388
280
|
try {
|
|
389
281
|
const outcome = await tryGsapResizeIntercept(
|
|
390
282
|
selection,
|
|
@@ -396,9 +288,16 @@ export function useGsapAwareEditing({
|
|
|
396
288
|
);
|
|
397
289
|
assertGsapEditPersisted(outcome);
|
|
398
290
|
// Saved before the buffered GSAP writes, so their reload stays the gesture's last render.
|
|
399
|
-
if (outcome.status === "element-size")
|
|
400
|
-
await handleDomBoxSizeCommit(
|
|
401
|
-
|
|
291
|
+
if (outcome.status === "element-size") {
|
|
292
|
+
const result = await handleDomBoxSizeCommit(
|
|
293
|
+
selection,
|
|
294
|
+
next,
|
|
295
|
+
undefined,
|
|
296
|
+
undefined,
|
|
297
|
+
coalesceKey,
|
|
298
|
+
);
|
|
299
|
+
writes.recordDomResult(result);
|
|
300
|
+
} else cropUndoKey = coalesceKey;
|
|
402
301
|
// What the resize did, not what its tweens suggest: a scale hold still commits a size.
|
|
403
302
|
const ownsDragOffset =
|
|
404
303
|
outcome.status === "persisted" && outcome.ownsDragOffset === true;
|
|
@@ -409,21 +308,7 @@ export function useGsapAwareEditing({
|
|
|
409
308
|
});
|
|
410
309
|
// A resize that moved the element itself has already written
|
|
411
310
|
// where it landed. Everything else leaves the anchor to the drag.
|
|
412
|
-
|
|
413
|
-
const dragOutcome = await tryGsapDragIntercept(
|
|
414
|
-
selection,
|
|
415
|
-
offset,
|
|
416
|
-
targetAnimations,
|
|
417
|
-
previewIframeRef.current,
|
|
418
|
-
commitMutation,
|
|
419
|
-
makeFetchFallback(selection),
|
|
420
|
-
);
|
|
421
|
-
// Saved after the size, under its undo key, so the two are one step.
|
|
422
|
-
await saveMove(dragOutcome, async () => {
|
|
423
|
-
const plain = !gsapWritesPosition(selection.element);
|
|
424
|
-
anchorMove = stageElementPositionOffset(selection, offset, plain, coalesceKey);
|
|
425
|
-
});
|
|
426
|
-
}
|
|
311
|
+
await saveResizeAnchor(ownsDragOffset, commitMutation, coalesceKey);
|
|
427
312
|
logResizeSettle(selection.element, ownsDragOffset ? "gsap-scale" : "gsap-size");
|
|
428
313
|
return;
|
|
429
314
|
} catch (error) {
|
|
@@ -434,10 +319,17 @@ export function useGsapAwareEditing({
|
|
|
434
319
|
throw new Error("Resize of a GSAP-owned box has no GSAP writer");
|
|
435
320
|
},
|
|
436
321
|
afterBufferedCommitsSaved: async () => {
|
|
437
|
-
await anchorMove?.save();
|
|
322
|
+
const anchorResult = await anchorMove?.save();
|
|
323
|
+
writes.recordDomResult(anchorResult);
|
|
438
324
|
// Only now is the size live for every caller, drag or not.
|
|
439
325
|
if (cropUndoKey) {
|
|
440
|
-
await saveCropResize(
|
|
326
|
+
const cropResult = await saveCropResize(
|
|
327
|
+
stageCrop,
|
|
328
|
+
selection,
|
|
329
|
+
commitPositionPatchToHtml,
|
|
330
|
+
cropUndoKey,
|
|
331
|
+
);
|
|
332
|
+
writes.recordDomResult(cropResult);
|
|
441
333
|
}
|
|
442
334
|
},
|
|
443
335
|
restore: () => {
|
|
@@ -446,6 +338,7 @@ export function useGsapAwareEditing({
|
|
|
446
338
|
},
|
|
447
339
|
skipPixelAssert: hasLivePositionTween,
|
|
448
340
|
});
|
|
341
|
+
return writes.finish();
|
|
449
342
|
},
|
|
450
343
|
[
|
|
451
344
|
handleDomBoxSizeCommit,
|
|
@@ -461,9 +354,12 @@ export function useGsapAwareEditing({
|
|
|
461
354
|
|
|
462
355
|
const handleGsapAwareRotationCommit = useCallback(
|
|
463
356
|
async (selection: DomEditSelection, next: RotationCommit) => {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
357
|
+
const writes = observeGsapGesture(gsapCommitMutation);
|
|
358
|
+
if (next.plain || !gsapWritesRotation(selection.element)) {
|
|
359
|
+
const result = await handleDomRotationCommit(selection, next);
|
|
360
|
+
return writes.finish(result?.changed === true);
|
|
361
|
+
}
|
|
362
|
+
if (writes.commit) {
|
|
467
363
|
try {
|
|
468
364
|
const targetAnimations = await getGsapAnimationsForSelection(selection);
|
|
469
365
|
// A keyframe or a tl.set; a computed source rejects, so the gesture restores its draft.
|
|
@@ -472,10 +368,11 @@ export function useGsapAwareEditing({
|
|
|
472
368
|
next.angle,
|
|
473
369
|
targetAnimations,
|
|
474
370
|
previewIframeRef.current,
|
|
475
|
-
|
|
371
|
+
writes.commit,
|
|
476
372
|
makeFetchFallback(selection),
|
|
477
373
|
);
|
|
478
374
|
assertGsapEditPersisted(outcome);
|
|
375
|
+
return writes.finish();
|
|
479
376
|
} catch (error) {
|
|
480
377
|
trackGsapInteractionFailure(error, selection, "rotation", "Rotate animated layer");
|
|
481
378
|
throw error;
|
|
@@ -564,12 +461,16 @@ export function useGsapAwareEditing({
|
|
|
564
461
|
);
|
|
565
462
|
|
|
566
463
|
const commitMutation = useCallback(
|
|
567
|
-
async (
|
|
568
|
-
|
|
464
|
+
async (
|
|
465
|
+
mutation: Record<string, unknown>,
|
|
466
|
+
options: CommitMutationOptions,
|
|
467
|
+
selection = domEditSelection,
|
|
468
|
+
) => {
|
|
469
|
+
if (!selection) return;
|
|
569
470
|
// Return (await) the safe-commit chain so consumers that `await
|
|
570
471
|
// session.commitMutation(...)` (gesture recording, enable-keyframes) run
|
|
571
472
|
// their post-actions only after the server save has settled.
|
|
572
|
-
await safeGsapCommit(
|
|
473
|
+
await safeGsapCommit(selection, mutation, options);
|
|
573
474
|
},
|
|
574
475
|
[domEditSelection, safeGsapCommit],
|
|
575
476
|
);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
3
|
+
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
|
4
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
5
|
+
import type { CommitMutationCall, CommitMutationOptions } from "./gsapScriptCommitTypes";
|
|
6
|
+
import type { UseGsapAwareEditingParams } from "./useGsapAwareEditing";
|
|
7
|
+
import { observeGsapGesture } from "./gsapGestureOutcome";
|
|
8
|
+
import { gsapWritesPosition } from "./gsapRuntimeKeyframes";
|
|
9
|
+
import { refuseGsapTakeover } from "./elementOffsetStager";
|
|
10
|
+
import { tryGsapDragIntercept } from "./gsapRuntimeBridge";
|
|
11
|
+
import { assertGsapEditPersisted } from "./gsapEditOutcome";
|
|
12
|
+
|
|
13
|
+
// A distinct key keeps consecutive group drags in separate undo entries.
|
|
14
|
+
let groupDragCommitCounter = 0;
|
|
15
|
+
|
|
16
|
+
function firstPreflightFailure(
|
|
17
|
+
results: PromiseSettledResult<void>[],
|
|
18
|
+
updates: DomEditGroupPathOffsetCommit[],
|
|
19
|
+
): { error: unknown; selection: DomEditSelection } | null {
|
|
20
|
+
for (const [index, result] of results.entries()) {
|
|
21
|
+
if (result.status !== "rejected") continue;
|
|
22
|
+
const selection = updates[index]?.selection;
|
|
23
|
+
if (selection) return { error: result.reason, selection };
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useGsapAwareGroupMove({
|
|
29
|
+
gsapCommitMutation,
|
|
30
|
+
previewIframeRef,
|
|
31
|
+
makeFetchFallback,
|
|
32
|
+
trackGsapInteractionFailure,
|
|
33
|
+
stageElementPositionOffset,
|
|
34
|
+
showToast,
|
|
35
|
+
}: Pick<
|
|
36
|
+
UseGsapAwareEditingParams,
|
|
37
|
+
| "gsapCommitMutation"
|
|
38
|
+
| "previewIframeRef"
|
|
39
|
+
| "makeFetchFallback"
|
|
40
|
+
| "trackGsapInteractionFailure"
|
|
41
|
+
| "stageElementPositionOffset"
|
|
42
|
+
| "showToast"
|
|
43
|
+
>) {
|
|
44
|
+
const handleGsapAwareGroupPathOffsetCommit = useCallback(
|
|
45
|
+
async (
|
|
46
|
+
updates: DomEditGroupPathOffsetCommit[],
|
|
47
|
+
): Promise<import("../utils/previewFeatureUsage").GeometryCommitResult> => {
|
|
48
|
+
const writes = observeGsapGesture(gsapCommitMutation);
|
|
49
|
+
const writer = writes.commit;
|
|
50
|
+
if (!writer) return { ok: true, changed: false };
|
|
51
|
+
// One coalesce key across slow writes keeps the group drag in one undo entry.
|
|
52
|
+
const coalesceKey = `group-drag:${++groupDragCommitCounter}`;
|
|
53
|
+
// Hold the render until every member is saved, or unwritten members snap back.
|
|
54
|
+
let renderOnCommit = false;
|
|
55
|
+
const previewFallbackLatch = { pending: false };
|
|
56
|
+
const withGroupOptions = (options: CommitMutationOptions): CommitMutationOptions => ({
|
|
57
|
+
...options,
|
|
58
|
+
coalesceKey,
|
|
59
|
+
coalesceMs: Number.POSITIVE_INFINITY,
|
|
60
|
+
deferPreviewSync: !renderOnCommit,
|
|
61
|
+
previewFallbackLatch,
|
|
62
|
+
});
|
|
63
|
+
// Batch members in the same file to avoid repeating its read, parse, write and preview patch.
|
|
64
|
+
const queued: CommitMutationCall[] = [];
|
|
65
|
+
const flushQueued = async () => {
|
|
66
|
+
if (queued.length === 0) return;
|
|
67
|
+
const calls = queued.splice(0, queued.length);
|
|
68
|
+
if (!writer.batch) {
|
|
69
|
+
for (const call of calls) {
|
|
70
|
+
await writer(call.selection, call.mutation, call.options);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
await writer.batch(calls, {
|
|
75
|
+
...(calls.at(-1)?.options ?? { label: "Move animated layer (group)" }),
|
|
76
|
+
label: "Move animated layer (group)",
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
const coalescedCommit: typeof gsapCommitMutation = (selection, mutation, options) => {
|
|
80
|
+
queued.push({ selection, mutation, options: withGroupOptions(options) });
|
|
81
|
+
return Promise.resolve();
|
|
82
|
+
};
|
|
83
|
+
const preflightAnimations = new Map<DomEditSelection, GsapAnimation[]>();
|
|
84
|
+
// Members saved on themselves, each with its route: true for its CSS translate.
|
|
85
|
+
const offsetMembers = new Map<DomEditSelection, boolean>();
|
|
86
|
+
// Editability is user-atomic: prove every member can be written before the first source
|
|
87
|
+
// mutation, so a blocked member never leaves earlier siblings partially moved. Preflights
|
|
88
|
+
// write nothing and share one in-flight parse per file, so they run together.
|
|
89
|
+
const preflightResults = await Promise.allSettled(
|
|
90
|
+
updates.map(async ({ selection, plainTranslate }) => {
|
|
91
|
+
if (plainTranslate ?? !gsapWritesPosition(selection.element)) {
|
|
92
|
+
refuseGsapTakeover(selection.element, showToast);
|
|
93
|
+
return void offsetMembers.set(selection, true);
|
|
94
|
+
}
|
|
95
|
+
const animations = await makeFetchFallback(selection, { failOnFetchError: true })();
|
|
96
|
+
preflightAnimations.set(selection, animations);
|
|
97
|
+
const outcome = await tryGsapDragIntercept(
|
|
98
|
+
selection,
|
|
99
|
+
{ x: 0, y: 0 },
|
|
100
|
+
animations,
|
|
101
|
+
previewIframeRef.current,
|
|
102
|
+
coalescedCommit,
|
|
103
|
+
undefined,
|
|
104
|
+
{ preflightOnly: true, group: true },
|
|
105
|
+
);
|
|
106
|
+
if (outcome.status === "element-offset") offsetMembers.set(selection, false);
|
|
107
|
+
assertGsapEditPersisted(outcome);
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
const preflightFailure = firstPreflightFailure(preflightResults, updates);
|
|
111
|
+
if (preflightFailure) {
|
|
112
|
+
trackGsapInteractionFailure(
|
|
113
|
+
preflightFailure.error,
|
|
114
|
+
preflightFailure.selection,
|
|
115
|
+
"drag",
|
|
116
|
+
"Move animated layer (group)",
|
|
117
|
+
);
|
|
118
|
+
throw preflightFailure.error;
|
|
119
|
+
}
|
|
120
|
+
const lastScriptWrite = updates.findLastIndex(
|
|
121
|
+
({ selection }) => !offsetMembers.has(selection),
|
|
122
|
+
);
|
|
123
|
+
for (const [index, { selection, next }] of updates.entries()) {
|
|
124
|
+
renderOnCommit = index === lastScriptWrite;
|
|
125
|
+
const plain = offsetMembers.get(selection);
|
|
126
|
+
if (plain !== undefined) {
|
|
127
|
+
const result = await stageElementPositionOffset(
|
|
128
|
+
selection,
|
|
129
|
+
next,
|
|
130
|
+
plain,
|
|
131
|
+
coalesceKey,
|
|
132
|
+
).save();
|
|
133
|
+
writes.recordDomResult(result);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const outcome = await tryGsapDragIntercept(
|
|
138
|
+
selection,
|
|
139
|
+
next,
|
|
140
|
+
preflightAnimations.get(selection) ?? [],
|
|
141
|
+
previewIframeRef.current,
|
|
142
|
+
coalescedCommit,
|
|
143
|
+
// The intercept re-reads the file to resolve a stale or shared tween.
|
|
144
|
+
// Anything already queued has to be on disk before that read, or it
|
|
145
|
+
// resolves against a file missing writes it is about to build on.
|
|
146
|
+
async () => {
|
|
147
|
+
await flushQueued();
|
|
148
|
+
return makeFetchFallback(selection, { fresh: true })();
|
|
149
|
+
},
|
|
150
|
+
{ preflightPassed: true },
|
|
151
|
+
);
|
|
152
|
+
assertGsapEditPersisted(outcome);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
trackGsapInteractionFailure(error, selection, "drag", "Move animated layer (group)");
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
await flushQueued();
|
|
160
|
+
return writes.finish();
|
|
161
|
+
} catch (error) {
|
|
162
|
+
// The aggregate write has no uniquely failing member; do not misattribute
|
|
163
|
+
// its telemetry to whichever member happened to be last in the array.
|
|
164
|
+
trackGsapInteractionFailure(error, null, "drag", "Move animated layer (group)");
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
[
|
|
169
|
+
gsapCommitMutation,
|
|
170
|
+
previewIframeRef,
|
|
171
|
+
makeFetchFallback,
|
|
172
|
+
trackGsapInteractionFailure,
|
|
173
|
+
stageElementPositionOffset,
|
|
174
|
+
showToast,
|
|
175
|
+
],
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return handleGsapAwareGroupPathOffsetCommit;
|
|
179
|
+
}
|