@hyperframes/studio 0.7.54 → 0.7.56
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/{index-uBY329wb.js → index-BWnOxAiH.js} +1 -1
- package/dist/assets/{index-CMHYjEZ5.js → index-C4csZims.js} +1 -1
- package/dist/assets/index-CmVCjZjp.js +423 -0
- package/dist/assets/index-D-GyYi2d.css +1 -0
- package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
- package/dist/chunk-5QSIMBEJ.js.map +1 -0
- package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
- package/dist/index.d.ts +146 -144
- package/dist/index.html +2 -2
- package/dist/index.js +40582 -37803
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +106 -96
- package/src/components/EditorShell.tsx +253 -0
- package/src/components/StudioGlobalDragOverlay.tsx +1 -3
- package/src/components/StudioLeftSidebar.tsx +13 -3
- package/src/components/StudioRightPanel.tsx +8 -3
- package/src/components/TimelineToolbar.test.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +179 -121
- package/src/components/editor/CanvasContextMenu.test.tsx +115 -0
- package/src/components/editor/CanvasContextMenu.tsx +198 -0
- package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
- package/src/components/editor/DomEditCropHandles.tsx +64 -29
- package/src/components/editor/DomEditOverlay.test.ts +245 -279
- package/src/components/editor/DomEditOverlay.tsx +158 -126
- package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
- package/src/components/editor/DomEditRotateHandle.tsx +38 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
- package/src/components/editor/LayersPanel.test.ts +66 -2
- package/src/components/editor/LayersPanel.tsx +43 -1
- package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
- package/src/components/editor/OffCanvasIndicators.tsx +49 -10
- package/src/components/editor/SnapGuideOverlay.tsx +28 -23
- package/src/components/editor/SnapToolbar.test.tsx +0 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +208 -0
- package/src/components/editor/canvasContextMenuZOrder.test.ts +435 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +352 -0
- package/src/components/editor/domEditNudge.test.ts +80 -0
- package/src/components/editor/domEditNudge.ts +44 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
- package/src/components/editor/domEditOverlayGeometry.ts +280 -20
- package/src/components/editor/domEditOverlayGestures.ts +44 -41
- package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
- package/src/components/editor/domEditResizeLocal.test.ts +131 -0
- package/src/components/editor/domEditResizeLocal.ts +125 -0
- package/src/components/editor/domEditingDom.ts +1 -1
- package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
- package/src/components/editor/manualOffsetDrag.ts +55 -12
- package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
- package/src/components/editor/resizeDraft.ts +108 -0
- package/src/components/editor/snapEngine.test.ts +12 -78
- package/src/components/editor/snapEngine.ts +13 -53
- package/src/components/editor/useDomEditNudge.test.tsx +228 -0
- package/src/components/editor/useDomEditNudge.ts +254 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
- package/src/components/editor/useDomEditOverlayRects.ts +9 -2
- package/src/components/nle/AssetPreviewOverlay.tsx +147 -0
- package/src/components/nle/NLEContext.test.ts +144 -0
- package/src/components/nle/NLEContext.tsx +328 -0
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewOverlays.tsx +240 -0
- package/src/components/nle/PreviewPane.tsx +163 -0
- package/src/components/nle/TimelinePane.test.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +293 -0
- package/src/components/nle/TimelineResizeDivider.tsx +8 -2
- package/src/components/nle/useCompositionStack.test.tsx +44 -0
- package/src/components/nle/useCompositionStack.ts +17 -9
- package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
- package/src/components/sidebar/AssetCard.tsx +326 -0
- package/src/components/sidebar/AssetContextMenu.tsx +15 -30
- package/src/components/sidebar/AssetsTab.test.ts +96 -0
- package/src/components/sidebar/AssetsTab.tsx +71 -206
- package/src/components/sidebar/AudioRow.tsx +39 -2
- package/src/components/sidebar/BlocksTab.tsx +8 -1
- package/src/components/sidebar/LeftSidebar.tsx +4 -1
- package/src/components/sidebar/assetHelpers.ts +29 -0
- package/src/contexts/DomEditContext.tsx +9 -0
- package/src/contexts/StudioContext.tsx +9 -8
- package/src/contexts/TimelineEditContext.tsx +3 -7
- package/src/hooks/deleteSelectedKeyframes.ts +35 -0
- package/src/hooks/domEditCommitTypes.ts +12 -1
- package/src/hooks/domSelectionTestHarness.ts +14 -0
- package/src/hooks/gestureTransaction.test.ts +311 -0
- package/src/hooks/gestureTransaction.ts +199 -0
- package/src/hooks/gsapDragCommit.test.ts +141 -41
- package/src/hooks/gsapDragCommit.ts +64 -74
- package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
- package/src/hooks/gsapResizeIntercept.test.ts +69 -0
- package/src/hooks/gsapResizeIntercept.ts +38 -10
- package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
- package/src/hooks/gsapRuntimeBridge.ts +4 -4
- package/src/hooks/gsapScriptCommitTypes.ts +16 -5
- package/src/hooks/gsapShared.test.ts +19 -1
- package/src/hooks/gsapShared.ts +14 -0
- package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
- package/src/hooks/timelineEditingGsap.ts +14 -0
- package/src/hooks/timelineEditingHelpers.test.ts +34 -1
- package/src/hooks/timelineEditingHelpers.ts +22 -48
- package/src/hooks/timelineMoveAdapter.test.ts +77 -0
- package/src/hooks/timelineMoveAdapter.ts +37 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
- package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
- package/src/hooks/useAppHotkeys.ts +16 -25
- package/src/hooks/useBlockHandlers.ts +55 -27
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +154 -1
- package/src/hooks/useDomEditCommits.ts +100 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -3
- package/src/hooks/useDomEditWiring.ts +7 -9
- package/src/hooks/useDomGeometryCommits.ts +17 -2
- package/src/hooks/useDomSelection.test.ts +2 -36
- package/src/hooks/useDomSelection.ts +81 -51
- package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
- package/src/hooks/useElementLifecycleOps.test.tsx +258 -0
- package/src/hooks/useElementLifecycleOps.ts +40 -71
- package/src/hooks/useEnableKeyframes.test.ts +142 -1
- package/src/hooks/useEnableKeyframes.ts +73 -29
- package/src/hooks/useGestureCommit.test.tsx +130 -0
- package/src/hooks/useGestureCommit.ts +32 -8
- package/src/hooks/useGroupCommits.ts +7 -28
- package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
- package/src/hooks/useGsapAwareEditing.ts +115 -20
- package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
- package/src/hooks/useGsapKeyframeOps.ts +38 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
- package/src/hooks/useGsapScriptCommits.ts +134 -50
- package/src/hooks/useGsapSelectionHandlers.ts +49 -16
- package/src/hooks/useMusicBeatAnalysis.ts +72 -36
- package/src/hooks/usePersistentEditHistory.test.ts +33 -0
- package/src/hooks/usePersistentEditHistory.ts +92 -58
- package/src/hooks/usePreviewPersistence.ts +25 -7
- package/src/hooks/useRazorSplit.history.test.tsx +303 -0
- package/src/hooks/useRazorSplit.test.ts +269 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
- package/src/hooks/useRazorSplit.ts +107 -36
- package/src/hooks/useRenderClipContent.ts +24 -6
- package/src/hooks/useStudioContextValue.ts +15 -5
- package/src/hooks/useStudioUrlState.ts +2 -4
- package/src/hooks/useTimelineEditing.test.tsx +20 -29
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +18 -4
- package/src/index.ts +1 -1
- package/src/player/components/ImageThumbnail.test.tsx +173 -0
- package/src/player/components/ImageThumbnail.tsx +160 -0
- package/src/player/components/PlayheadIndicator.tsx +50 -5
- package/src/player/components/Timeline.test.ts +102 -19
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +207 -521
- package/src/player/components/TimelineClip.tsx +4 -1
- package/src/player/components/TimelineClipDiamonds.tsx +9 -40
- package/src/player/components/TimelineEmptyState.tsx +3 -1
- package/src/player/components/TimelineLanes.tsx +487 -0
- package/src/player/components/TimelineOverlays.tsx +122 -0
- package/src/player/components/VideoThumbnail.test.tsx +152 -0
- package/src/player/components/VideoThumbnail.tsx +28 -7
- package/src/player/components/thumbnailUtils.test.ts +127 -0
- package/src/player/components/thumbnailUtils.ts +54 -0
- package/src/player/components/timelineCallbacks.ts +18 -18
- package/src/player/components/timelineClipChildren.tsx +39 -0
- package/src/player/components/timelineClipDragCommit.test.ts +943 -0
- package/src/player/components/timelineClipDragCommit.ts +401 -0
- package/src/player/components/timelineClipDragPreview.test.ts +144 -0
- package/src/player/components/timelineClipDragPreview.ts +323 -38
- package/src/player/components/timelineClipDragTypes.ts +65 -0
- package/src/player/components/timelineCollision.test.ts +477 -0
- package/src/player/components/timelineCollision.ts +263 -0
- package/src/player/components/timelineDragDrop.ts +69 -60
- package/src/player/components/timelineEditCapabilities.ts +52 -0
- package/src/player/components/timelineEditing.test.ts +1 -1
- package/src/player/components/timelineEditing.ts +76 -59
- package/src/player/components/timelineGroupEditing.test.ts +126 -0
- package/src/player/components/timelineGroupEditing.ts +133 -0
- package/src/player/components/timelineGroupResizeCommit.ts +54 -0
- package/src/player/components/timelineLayout.test.ts +95 -23
- package/src/player/components/timelineLayout.ts +120 -61
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineMultiDragPreview.test.ts +127 -0
- package/src/player/components/timelineMultiDragPreview.ts +106 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineSnapping.test.ts +134 -0
- package/src/player/components/timelineSnapping.ts +110 -0
- package/src/player/components/timelineStackingSync.test.ts +244 -0
- package/src/player/components/timelineStackingSync.ts +331 -0
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineZones.test.ts +425 -0
- package/src/player/components/timelineZones.ts +198 -0
- package/src/player/components/timelineZoom.test.ts +67 -0
- package/src/player/components/timelineZoom.ts +51 -0
- package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
- package/src/player/components/useTimelineClipDrag.ts +380 -386
- package/src/player/components/useTimelineEditPinning.ts +121 -0
- package/src/player/components/useTimelineGeometry.ts +129 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -16
- package/src/player/components/useTimelineRangeSelection.ts +341 -50
- package/src/player/components/useTimelineStackingSync.test.tsx +67 -0
- package/src/player/components/useTimelineStackingSync.ts +83 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +0 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.ts +48 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.test.ts +219 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +104 -4
- package/src/player/index.ts +3 -2
- package/src/player/lib/time.test.ts +19 -19
- package/src/player/lib/time.ts +6 -2
- package/src/player/lib/timelineDOM.test.ts +25 -98
- package/src/player/lib/timelineDOM.ts +12 -81
- package/src/player/lib/timelineElementHelpers.ts +54 -5
- package/src/player/lib/timelineIframeHelpers.ts +2 -0
- package/src/player/store/playerStore.ts +62 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetClickBehavior.test.ts +104 -0
- package/src/utils/assetClickBehavior.ts +75 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/canvasNudgeGate.test.ts +31 -0
- package/src/utils/canvasNudgeGate.ts +32 -0
- package/src/utils/editHistory.test.ts +35 -0
- package/src/utils/gsapSoftReload.test.ts +119 -1
- package/src/utils/gsapSoftReload.ts +153 -0
- package/src/utils/mediaTypes.ts +3 -2
- package/src/utils/resizeDebug.ts +55 -0
- package/src/utils/rootDuration.test.ts +90 -1
- package/src/utils/rootDuration.ts +76 -13
- package/src/utils/sdkCutover.ts +4 -0
- package/src/utils/studioFileHistory.ts +37 -7
- package/src/utils/studioHelpers.ts +62 -0
- package/src/utils/studioPreviewHelpers.test.ts +65 -8
- package/src/utils/studioPreviewHelpers.ts +10 -0
- package/src/utils/studioTelemetry.ts +4 -1
- package/src/utils/studioUiPreferences.test.ts +38 -0
- package/src/utils/studioUiPreferences.ts +27 -0
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.ts +62 -0
- package/src/utils/timelineDiscovery.ts +0 -17
- package/src/utils/timelineInspector.test.ts +121 -0
- package/src/utils/timelineInspector.ts +32 -1
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/assets/index-pRhCpGPz.js +0 -423
- package/dist/chunk-SOTCF4DF.js.map +0 -1
- package/src/components/StudioPreviewArea.tsx +0 -500
- package/src/components/nle/NLELayout.test.ts +0 -12
- package/src/components/nle/NLELayout.tsx +0 -591
- package/src/player/components/TimelineLayerGutter.tsx +0 -61
- package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
- package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
- package/src/player/components/timelineSnapTargets.test.ts +0 -144
- package/src/player/components/timelineSnapTargets.ts +0 -164
- package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
- package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
- package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
- package/src/utils/timelineDiscovery.test.ts +0 -90
- /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CommitMutation,
|
|
3
|
+
CommitMutationCall,
|
|
4
|
+
CommitMutationOptions,
|
|
5
|
+
} from "./gsapScriptCommitTypes";
|
|
6
|
+
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
7
|
+
|
|
8
|
+
type PixelRect = Pick<DOMRect, "x" | "y" | "width" | "height">;
|
|
9
|
+
|
|
10
|
+
export type TxCommit = (commitMutation: CommitMutation) => CommitMutation;
|
|
11
|
+
|
|
12
|
+
export interface GestureTransaction {
|
|
13
|
+
element: HTMLElement;
|
|
14
|
+
label: string;
|
|
15
|
+
settle(): void;
|
|
16
|
+
persist(commit: TxCommit): Promise<void>;
|
|
17
|
+
restore(): void;
|
|
18
|
+
skipPixelAssert?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let transactionCounter = 0;
|
|
22
|
+
const transactionCommits = new WeakSet<CommitMutation>();
|
|
23
|
+
|
|
24
|
+
/** Whether a commit function already belongs to an active gesture transaction. */
|
|
25
|
+
export function isGestureTransactionCommit(commitMutation: CommitMutation): boolean {
|
|
26
|
+
return transactionCommits.has(commitMutation);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readPixelRect(element: HTMLElement): PixelRect {
|
|
30
|
+
const { x, y, width, height } = element.getBoundingClientRect();
|
|
31
|
+
return { x, y, width, height };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function transactionOptions(
|
|
35
|
+
options: CommitMutationOptions,
|
|
36
|
+
coalesceKey: string,
|
|
37
|
+
label: string,
|
|
38
|
+
): CommitMutationOptions {
|
|
39
|
+
// The transaction owns the undo label: every wrapped mutation records under
|
|
40
|
+
// `label`, so the coalesced entry reads as the gesture (e.g. "Resize layer")
|
|
41
|
+
// rather than whichever sub-mutation happened to land last (the offset
|
|
42
|
+
// persist's "Move layer").
|
|
43
|
+
const { coalesceKey: _coalesceKey, skipReload: _skipReload, softReload, ...rest } = options;
|
|
44
|
+
return softReload
|
|
45
|
+
? { ...rest, label, softReload: true, coalesceKey, coalesceMs: Number.POSITIVE_INFINITY }
|
|
46
|
+
: { ...rest, label, skipReload: true, coalesceKey, coalesceMs: Number.POSITIVE_INFINITY };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function pixelDelta(before: PixelRect, after: PixelRect): PixelRect {
|
|
50
|
+
return {
|
|
51
|
+
x: after.x - before.x,
|
|
52
|
+
y: after.y - before.y,
|
|
53
|
+
width: after.width - before.width,
|
|
54
|
+
height: after.height - before.height,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function exceedsPixelTolerance(delta: PixelRect): boolean {
|
|
59
|
+
return Object.values(delta).some((value) => Math.abs(value) > 1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function roundToOneDecimal(value: number): number {
|
|
63
|
+
return Math.round(value * 10) / 10;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type BufferedCommit = CommitMutationCall & { dispatch: CommitMutation };
|
|
67
|
+
|
|
68
|
+
function mergeTransactionOptions(calls: BufferedCommit[]): CommitMutationOptions {
|
|
69
|
+
const reloadCall = calls.find(({ options }) => options.softReload);
|
|
70
|
+
const source = reloadCall?.options ?? calls.at(-1)?.options;
|
|
71
|
+
if (!source) throw new Error("Cannot merge an empty gesture transaction");
|
|
72
|
+
const { skipReload: _skipReload, instantPatch: _instantPatch, ...options } = source;
|
|
73
|
+
return reloadCall ? { ...options, softReload: true } : { ...options, skipReload: true };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Dispatch a transaction's buffered commits and return the number of preview
|
|
78
|
+
* reloads that ACTUALLY happened — a batch collapses every buffered softReload
|
|
79
|
+
* into one reload, so reload telemetry reflects the real cost, not the count of
|
|
80
|
+
* softReload requests.
|
|
81
|
+
*/
|
|
82
|
+
function reloadsRequested(calls: BufferedCommit[]): number {
|
|
83
|
+
return calls.filter(({ options }) => options.softReload).length;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function dispatchBufferedCommits(calls: BufferedCommit[]): Promise<number> {
|
|
87
|
+
const first = calls[0];
|
|
88
|
+
if (!first) return 0;
|
|
89
|
+
if (calls.length === 1) {
|
|
90
|
+
await first.dispatch(first.selection, first.mutation, first.options);
|
|
91
|
+
return reloadsRequested(calls);
|
|
92
|
+
}
|
|
93
|
+
const canBatch = calls.every(
|
|
94
|
+
({ dispatch, selection }) =>
|
|
95
|
+
dispatch === first.dispatch && selection.sourceFile === first.selection.sourceFile,
|
|
96
|
+
);
|
|
97
|
+
if (canBatch && first.dispatch.batch) {
|
|
98
|
+
await first.dispatch.batch(
|
|
99
|
+
calls.map(({ selection, mutation, options }) => ({ selection, mutation, options })),
|
|
100
|
+
mergeTransactionOptions(calls),
|
|
101
|
+
);
|
|
102
|
+
// One server write, one reload — the reload count collapses with the batch.
|
|
103
|
+
return reloadsRequested(calls) > 0 ? 1 : 0;
|
|
104
|
+
}
|
|
105
|
+
for (const { dispatch, selection, mutation, options } of calls) {
|
|
106
|
+
await dispatch(selection, mutation, options);
|
|
107
|
+
}
|
|
108
|
+
return reloadsRequested(calls);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Dev-only [hf-commit] lifecycle trace. The production observability lives in
|
|
113
|
+
* the trackStudioEvent commit_* events (always on); these console lines are a
|
|
114
|
+
* developer aid and stay out of end users' consoles.
|
|
115
|
+
*/
|
|
116
|
+
function traceCommit(stage: string, data: Record<string, unknown>): void {
|
|
117
|
+
if (import.meta.env.DEV) console.info(`[hf-commit] ${stage}`, data);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Owns the visual + persistence + history lifecycle for one gesture release.
|
|
122
|
+
* `settle` deliberately runs before the first promise is created or awaited.
|
|
123
|
+
*/
|
|
124
|
+
export function runGestureTransaction(tx: GestureTransaction): Promise<void> {
|
|
125
|
+
const startedAt = performance.now();
|
|
126
|
+
const coalesceKey = `tx:${tx.label}:${++transactionCounter}`;
|
|
127
|
+
let mutationCount = 0;
|
|
128
|
+
let reloadCount = 0;
|
|
129
|
+
const bufferedCommits: BufferedCommit[] = [];
|
|
130
|
+
traceCommit("start", { label: tx.label, coalesceKey });
|
|
131
|
+
tx.settle();
|
|
132
|
+
traceCommit("settled", { label: tx.label, coalesceKey });
|
|
133
|
+
|
|
134
|
+
const before = !tx.skipPixelAssert ? readPixelRect(tx.element) : null;
|
|
135
|
+
const commit: TxCommit = (commitMutation) => {
|
|
136
|
+
const wrapped: CommitMutation = (selection, mutation, options) => {
|
|
137
|
+
mutationCount += 1;
|
|
138
|
+
bufferedCommits.push({
|
|
139
|
+
dispatch: commitMutation,
|
|
140
|
+
selection,
|
|
141
|
+
mutation,
|
|
142
|
+
options: transactionOptions(options, coalesceKey, tx.label),
|
|
143
|
+
});
|
|
144
|
+
return Promise.resolve();
|
|
145
|
+
};
|
|
146
|
+
transactionCommits.add(wrapped);
|
|
147
|
+
return wrapped;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return tx
|
|
151
|
+
.persist(commit)
|
|
152
|
+
.then(async () => {
|
|
153
|
+
reloadCount = await dispatchBufferedCommits(bufferedCommits);
|
|
154
|
+
const durationMs = Math.round(performance.now() - startedAt);
|
|
155
|
+
traceCommit("persisted", { label: tx.label, coalesceKey });
|
|
156
|
+
if (before) {
|
|
157
|
+
const after = readPixelRect(tx.element);
|
|
158
|
+
const delta = pixelDelta(before, after);
|
|
159
|
+
if (exceedsPixelTolerance(delta)) {
|
|
160
|
+
if (import.meta.env.DEV) {
|
|
161
|
+
console.error("[hf-commit] persist changed pixels", {
|
|
162
|
+
label: tx.label,
|
|
163
|
+
before,
|
|
164
|
+
after,
|
|
165
|
+
delta,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
trackStudioEvent("commit_invariant_violation", {
|
|
169
|
+
label: tx.label,
|
|
170
|
+
delta_x: roundToOneDecimal(delta.x),
|
|
171
|
+
delta_y: roundToOneDecimal(delta.y),
|
|
172
|
+
delta_w: roundToOneDecimal(delta.width),
|
|
173
|
+
delta_h: roundToOneDecimal(delta.height),
|
|
174
|
+
mutation_count: mutationCount,
|
|
175
|
+
reload_count: reloadCount,
|
|
176
|
+
duration_ms: durationMs,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
trackStudioEvent("commit_transaction", {
|
|
181
|
+
label: tx.label,
|
|
182
|
+
mutation_count: mutationCount,
|
|
183
|
+
reload_count: reloadCount,
|
|
184
|
+
duration_ms: durationMs,
|
|
185
|
+
pixel_asserted: before !== null,
|
|
186
|
+
});
|
|
187
|
+
})
|
|
188
|
+
.catch((error: unknown) => {
|
|
189
|
+
tx.restore();
|
|
190
|
+
trackStudioEvent("commit_transaction_failed", {
|
|
191
|
+
label: tx.label,
|
|
192
|
+
mutation_count: mutationCount,
|
|
193
|
+
error_name: error instanceof Error ? error.name : "unknown",
|
|
194
|
+
restore_ran: true,
|
|
195
|
+
});
|
|
196
|
+
traceCommit("restore", { label: tx.label, coalesceKey });
|
|
197
|
+
throw error;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
1
3
|
import { describe, expect, it, beforeEach } from "vitest";
|
|
2
4
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
3
5
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
@@ -5,6 +7,7 @@ import { commitGsapPositionFromDrag } from "./gsapDragPositionCommit";
|
|
|
5
7
|
import {
|
|
6
8
|
commitStaticGsapPosition,
|
|
7
9
|
commitStaticGsapRotation,
|
|
10
|
+
commitStaticGsapSize,
|
|
8
11
|
findExistingPositionWrite,
|
|
9
12
|
parkPlayheadOnKeyframe,
|
|
10
13
|
type GsapDragCommitCallbacks,
|
|
@@ -280,7 +283,7 @@ const existingRotationSet = (): GsapAnimation =>
|
|
|
280
283
|
describe("commitStaticGsapPosition — instantPatch (value-only set)", () => {
|
|
281
284
|
beforeEach(() => usePlayerStore.setState({ currentTime: 0, activeKeyframePct: null }));
|
|
282
285
|
|
|
283
|
-
it("
|
|
286
|
+
it("updates an existing set atomically and derives its instantPatch from that mutation", async () => {
|
|
284
287
|
const { commits, callbacks } = optionRecordingCallbacks();
|
|
285
288
|
|
|
286
289
|
await commitStaticGsapPosition(
|
|
@@ -292,69 +295,122 @@ describe("commitStaticGsapPosition — instantPatch (value-only set)", () => {
|
|
|
292
295
|
callbacks,
|
|
293
296
|
);
|
|
294
297
|
|
|
295
|
-
expect(commits).toHaveLength(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
expect(commits[0].options.instantPatch).toEqual({
|
|
301
|
-
selector: "#puck-a",
|
|
302
|
-
change: { kind: "set", props: { x: -50 } },
|
|
298
|
+
expect(commits).toHaveLength(1);
|
|
299
|
+
expect(commits[0].mutation).toEqual({
|
|
300
|
+
type: "update-properties",
|
|
301
|
+
animationId: "#puck-a-set",
|
|
302
|
+
properties: { x: -50, y: 30 },
|
|
303
303
|
});
|
|
304
|
-
|
|
305
|
-
expect(commits[
|
|
306
|
-
expect(commits[1].options.instantPatch).toEqual({
|
|
304
|
+
expect(commits[0].options.softReload).toBe(true);
|
|
305
|
+
expect(commits[0].options.instantPatch).toEqual({
|
|
307
306
|
selector: "#puck-a",
|
|
308
307
|
change: { kind: "set", props: { x: -50, y: 30 } },
|
|
309
308
|
});
|
|
309
|
+
const mutation = commits[0].mutation as { properties: Record<string, number> };
|
|
310
|
+
const patch = commits[0].options.instantPatch as {
|
|
311
|
+
change: { props: Record<string, number> };
|
|
312
|
+
};
|
|
313
|
+
expect(patch.change.props).toEqual(mutation.properties);
|
|
310
314
|
});
|
|
311
315
|
|
|
312
|
-
it("
|
|
316
|
+
it("ADDS a global gsap.set with a global-set instantPatch (off-timeline, no flash)", async () => {
|
|
313
317
|
const { commits, callbacks } = optionRecordingCallbacks();
|
|
314
318
|
|
|
315
319
|
await commitStaticGsapPosition(
|
|
316
320
|
selection(),
|
|
317
321
|
{ x: -50, y: 30 },
|
|
322
|
+
// fallow-ignore-next-line code-duplication
|
|
318
323
|
{ x: 0, y: 0 },
|
|
319
324
|
"#puck-a",
|
|
320
|
-
|
|
325
|
+
null, // no existing set → `add` a new base gsap.set
|
|
321
326
|
callbacks,
|
|
322
327
|
);
|
|
323
328
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
change: { props: Record<string, number> };
|
|
330
|
-
};
|
|
331
|
-
const yPatch = commits[1].options.instantPatch as {
|
|
332
|
-
change: { props: Record<string, number> };
|
|
333
|
-
};
|
|
334
|
-
expect(xPatch.change.props[xMutation.property]).toBe(xMutation.value);
|
|
335
|
-
expect(yPatch.change.props[yMutation.property]).toBe(yMutation.value);
|
|
336
|
-
// The y commit's combined patch also carries the x mutation's value.
|
|
337
|
-
expect(yPatch.change.props[xMutation.property]).toBe(xMutation.value);
|
|
329
|
+
expect(commits).toHaveLength(1);
|
|
330
|
+
expect(commits[0].mutation.type).toBe("add");
|
|
331
|
+
expect((commits[0].mutation as { global?: boolean }).global).toBe(true);
|
|
332
|
+
const patch = commits[0].options.instantPatch as { change: { kind: string } } | undefined;
|
|
333
|
+
expect(patch?.change.kind).toBe("global-set");
|
|
338
334
|
});
|
|
339
335
|
|
|
340
|
-
it("
|
|
336
|
+
it("creates one undo entry when updating an existing static position set", async () => {
|
|
341
337
|
const { commits, callbacks } = optionRecordingCallbacks();
|
|
342
338
|
|
|
343
339
|
await commitStaticGsapPosition(
|
|
344
340
|
selection(),
|
|
345
341
|
{ x: -50, y: 30 },
|
|
346
|
-
// fallow-ignore-next-line code-duplication
|
|
347
342
|
{ x: 0, y: 0 },
|
|
348
343
|
"#puck-a",
|
|
349
|
-
|
|
344
|
+
existingPositionSet(),
|
|
345
|
+
callbacks,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
expect(commits).toHaveLength(1);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const existingSizeSet = (): GsapAnimation =>
|
|
353
|
+
({
|
|
354
|
+
id: "#puck-a-size-set",
|
|
355
|
+
targetSelector: "#puck-a",
|
|
356
|
+
method: "set",
|
|
357
|
+
properties: { width: 100, height: 80 },
|
|
358
|
+
}) as unknown as GsapAnimation;
|
|
359
|
+
|
|
360
|
+
describe("commitStaticGsapSize", () => {
|
|
361
|
+
it("updates an existing set with one update-properties mutation", async () => {
|
|
362
|
+
const { commits, callbacks } = optionRecordingCallbacks();
|
|
363
|
+
|
|
364
|
+
await commitStaticGsapSize(
|
|
365
|
+
selection(),
|
|
366
|
+
{ width: 300.4, height: 199.6 },
|
|
367
|
+
"#puck-a",
|
|
368
|
+
existingSizeSet(),
|
|
369
|
+
callbacks,
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
expect(commits).toHaveLength(1);
|
|
373
|
+
expect(commits[0].mutation).toEqual({
|
|
374
|
+
type: "update-properties",
|
|
375
|
+
animationId: "#puck-a-size-set",
|
|
376
|
+
properties: { width: 300, height: 200 },
|
|
377
|
+
});
|
|
378
|
+
expect(commits.map((commit) => commit.mutation.type)).not.toContain("delete");
|
|
379
|
+
expect(commits.map((commit) => commit.mutation.type)).not.toContain("add");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("adds exactly one set when no existing size set exists", async () => {
|
|
383
|
+
const { commits, callbacks } = optionRecordingCallbacks();
|
|
384
|
+
|
|
385
|
+
await commitStaticGsapSize(
|
|
386
|
+
selection(),
|
|
387
|
+
{ width: 300, height: 200 },
|
|
388
|
+
"#puck-a",
|
|
389
|
+
null,
|
|
390
|
+
callbacks,
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
expect(commits).toHaveLength(1);
|
|
394
|
+
expect(commits[0].mutation).toMatchObject({
|
|
395
|
+
type: "add",
|
|
396
|
+
targetSelector: "#puck-a",
|
|
397
|
+
method: "set",
|
|
398
|
+
properties: { width: 300, height: 200 },
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("creates one undo entry for an existing static size set", async () => {
|
|
403
|
+
const { commits, callbacks } = optionRecordingCallbacks();
|
|
404
|
+
|
|
405
|
+
await commitStaticGsapSize(
|
|
406
|
+
selection(),
|
|
407
|
+
{ width: 300, height: 200 },
|
|
408
|
+
"#puck-a",
|
|
409
|
+
existingSizeSet(),
|
|
350
410
|
callbacks,
|
|
351
411
|
);
|
|
352
412
|
|
|
353
413
|
expect(commits).toHaveLength(1);
|
|
354
|
-
expect(commits[0].mutation.type).toBe("add");
|
|
355
|
-
expect((commits[0].mutation as { global?: boolean }).global).toBe(true);
|
|
356
|
-
const patch = commits[0].options.instantPatch as { change: { kind: string } } | undefined;
|
|
357
|
-
expect(patch?.change.kind).toBe("global-set");
|
|
358
414
|
});
|
|
359
415
|
});
|
|
360
416
|
|
|
@@ -386,7 +442,7 @@ describe("static position hold recognition + heal (frozen duration-0 keyframed t
|
|
|
386
442
|
expect(found?.id).toBe("#puck-a-frozen");
|
|
387
443
|
});
|
|
388
444
|
|
|
389
|
-
it("
|
|
445
|
+
it("heals a keyframed hold with one transaction ordered add before delete", async () => {
|
|
390
446
|
const { commits, callbacks } = optionRecordingCallbacks();
|
|
391
447
|
|
|
392
448
|
await commitStaticGsapPosition(
|
|
@@ -399,11 +455,55 @@ describe("static position hold recognition + heal (frozen duration-0 keyframed t
|
|
|
399
455
|
);
|
|
400
456
|
|
|
401
457
|
const types = commits.map((c) => c.mutation.type);
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
expect(
|
|
405
|
-
expect(
|
|
406
|
-
expect(
|
|
458
|
+
expect(types).toEqual(["add", "delete"]);
|
|
459
|
+
expect(types).not.toContain("update-properties");
|
|
460
|
+
expect((commits[0].mutation as { method?: string }).method).toBe("set");
|
|
461
|
+
expect(commits[0].options.coalesceKey).toBe(commits[1].options.coalesceKey);
|
|
462
|
+
expect(commits[0].options.coalesceKey).toMatch(/^tx:Move layer:\d+$/);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("keeps the original hold when the replacement add fails", async () => {
|
|
466
|
+
const mutationTypes: string[] = [];
|
|
467
|
+
const callbacks: GsapDragCommitCallbacks = {
|
|
468
|
+
commitMutation: async (_selection, mutation) => {
|
|
469
|
+
mutationTypes.push(mutation.type as string);
|
|
470
|
+
throw new Error("add failed");
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
await expect(
|
|
475
|
+
commitStaticGsapPosition(
|
|
476
|
+
selection(),
|
|
477
|
+
{ x: -50, y: 30 },
|
|
478
|
+
{ x: 0, y: 0 },
|
|
479
|
+
"#puck-a",
|
|
480
|
+
keyframedZeroDurationHold(),
|
|
481
|
+
callbacks,
|
|
482
|
+
),
|
|
483
|
+
).rejects.toThrow("add failed");
|
|
484
|
+
expect(mutationTypes).toEqual(["add"]);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("leaves a recoverable duplicate when delete fails after the replacement add", async () => {
|
|
488
|
+
let holdCount = 1;
|
|
489
|
+
const callbacks: GsapDragCommitCallbacks = {
|
|
490
|
+
commitMutation: async (_selection, mutation) => {
|
|
491
|
+
if (mutation.type === "add") holdCount += 1;
|
|
492
|
+
if (mutation.type === "delete") throw new Error("delete failed");
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
await expect(
|
|
497
|
+
commitStaticGsapPosition(
|
|
498
|
+
selection(),
|
|
499
|
+
{ x: -50, y: 30 },
|
|
500
|
+
{ x: 0, y: 0 },
|
|
501
|
+
"#puck-a",
|
|
502
|
+
keyframedZeroDurationHold(),
|
|
503
|
+
callbacks,
|
|
504
|
+
),
|
|
505
|
+
).rejects.toThrow("delete failed");
|
|
506
|
+
expect(holdCount).toBe(2);
|
|
407
507
|
});
|
|
408
508
|
});
|
|
409
509
|
|
|
@@ -15,10 +15,8 @@ import { roundTo3 } from "../utils/rounding";
|
|
|
15
15
|
import { computeElementPercentage } from "./gsapShared";
|
|
16
16
|
import { computeDraggedGsapPosition } from "./draggedGsapPosition";
|
|
17
17
|
import type { RuntimeTweenChange } from "./gsapRuntimePatch";
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
setPatchFromUpdateProperty,
|
|
21
|
-
} from "./gsapDragStaticSetHelpers";
|
|
18
|
+
import { isGestureTransactionCommit, runGestureTransaction } from "./gestureTransaction";
|
|
19
|
+
import { setPatchFromUpdateProperty } from "./gsapDragStaticSetHelpers";
|
|
22
20
|
export {
|
|
23
21
|
findExistingPositionWrite,
|
|
24
22
|
findRotationSetAnimation,
|
|
@@ -65,6 +63,46 @@ export function parkPlayheadOnKeyframe(anim: GsapAnimation, pct: number): void {
|
|
|
65
63
|
usePlayerStore.getState().requestSeek(roundTo3(ts + (pct / 100) * td));
|
|
66
64
|
}
|
|
67
65
|
|
|
66
|
+
async function replaceKeyframedPositionHold(
|
|
67
|
+
selection: DomEditSelection,
|
|
68
|
+
selector: string,
|
|
69
|
+
existingSet: GsapAnimation,
|
|
70
|
+
properties: { x: number; y: number },
|
|
71
|
+
commitMutation: GsapDragCommitCallbacks["commitMutation"],
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
const persist = async (commit: GsapDragCommitCallbacks["commitMutation"]) => {
|
|
74
|
+
await commit(
|
|
75
|
+
selection,
|
|
76
|
+
{
|
|
77
|
+
type: "add",
|
|
78
|
+
targetSelector: selector,
|
|
79
|
+
method: "set",
|
|
80
|
+
position: 0,
|
|
81
|
+
properties,
|
|
82
|
+
global: true,
|
|
83
|
+
},
|
|
84
|
+
{ label: "Move layer", skipReload: true },
|
|
85
|
+
);
|
|
86
|
+
await commit(
|
|
87
|
+
selection,
|
|
88
|
+
{ type: "delete", animationId: existingSet.id },
|
|
89
|
+
{ label: "Move layer", softReload: true },
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (isGestureTransactionCommit(commitMutation)) {
|
|
94
|
+
await persist(commitMutation);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await runGestureTransaction({
|
|
98
|
+
element: selection.element,
|
|
99
|
+
label: "Move layer",
|
|
100
|
+
settle: () => undefined,
|
|
101
|
+
persist: async (commit) => persist(commit(commitMutation)),
|
|
102
|
+
restore: () => undefined,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
68
106
|
// ── Dynamic keyframe materialization ──────────────────────────────────────
|
|
69
107
|
|
|
70
108
|
export async function materializeIfDynamic(
|
|
@@ -116,7 +154,7 @@ export async function materializeIfDynamic(
|
|
|
116
154
|
* Commit a STATIC element drag as a `tl.set("#el",{x,y})` — the single-source
|
|
117
155
|
* position channel for elements with no position animation. Idempotent: a
|
|
118
156
|
* re-nudge of an element that already has a `set` UPDATES that set's x/y
|
|
119
|
-
*
|
|
157
|
+
* in one `update-properties` mutation rather than stacking a second set or
|
|
120
158
|
* converting it to keyframes (plan R2 / KTD3). New elements get one `add`
|
|
121
159
|
* mutation with `method:"set"` at position 0.
|
|
122
160
|
*/
|
|
@@ -132,70 +170,30 @@ export async function commitStaticGsapPosition(
|
|
|
132
170
|
if (existingSet) {
|
|
133
171
|
if (existingSet.keyframes) {
|
|
134
172
|
// Keyframed zero-duration hold (drag-path corruption): can't update-property
|
|
135
|
-
// into keyframes
|
|
136
|
-
|
|
137
|
-
await
|
|
173
|
+
// into keyframes. Add the replacement first so either failure leaves at
|
|
174
|
+
// least one hold on disk, then delete the corrupt tween in one transaction.
|
|
175
|
+
await replaceKeyframedPositionHold(
|
|
138
176
|
selection,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
selection,
|
|
144
|
-
{
|
|
145
|
-
type: "add",
|
|
146
|
-
targetSelector: selector,
|
|
147
|
-
method: "set",
|
|
148
|
-
position: 0,
|
|
149
|
-
properties: { x: newX, y: newY },
|
|
150
|
-
global: true,
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
label: "Move layer",
|
|
154
|
-
softReload: true,
|
|
155
|
-
coalesceKey,
|
|
156
|
-
instantPatch: { selector, change: { kind: "global-set", props: { x: newX, y: newY } } },
|
|
157
|
-
},
|
|
177
|
+
selector,
|
|
178
|
+
existingSet,
|
|
179
|
+
{ x: newX, y: newY },
|
|
180
|
+
callbacks.commitMutation,
|
|
158
181
|
);
|
|
159
182
|
return;
|
|
160
183
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const coalesceKey = `gsap:set-nudge:${existingSet.id}`;
|
|
164
|
-
// Build each mutation FIRST, then derive its instantPatch from the SAME
|
|
165
|
-
// object that's POSTed — so a future caller can't ship a clean mutation with
|
|
166
|
-
// a stale/malformed patch (the validated `value` flows straight into the
|
|
167
|
-
// patch). `findUnsafeMutationValues` validates the mutation upstream.
|
|
168
|
-
const xMutation = {
|
|
169
|
-
type: "update-property",
|
|
184
|
+
const mutation = {
|
|
185
|
+
type: "update-properties",
|
|
170
186
|
animationId: existingSet.id,
|
|
171
|
-
|
|
172
|
-
value: newX,
|
|
173
|
-
} as const;
|
|
174
|
-
const yMutation = {
|
|
175
|
-
type: "update-property",
|
|
176
|
-
animationId: existingSet.id,
|
|
177
|
-
property: "y",
|
|
178
|
-
value: newY,
|
|
187
|
+
properties: { x: newX, y: newY },
|
|
179
188
|
} as const;
|
|
180
|
-
// Patch BOTH coalesced commits. If the SECOND POST fails server-side, the
|
|
181
|
-
// first (x) already persisted — patching its commit too means the live
|
|
182
|
-
// preview still reflects what DID persist. The x commit carries skipReload
|
|
183
|
-
// (no reload), so its instantPatch gives instant feedback without a reload;
|
|
184
|
-
// the y commit triggers the soft reload (skipped when the patch applies).
|
|
185
189
|
const global = !!existingSet.global;
|
|
186
|
-
await callbacks.commitMutation(selection,
|
|
187
|
-
label: "Move layer",
|
|
188
|
-
skipReload: true,
|
|
189
|
-
coalesceKey,
|
|
190
|
-
instantPatch: setPatchFromUpdateProperty(selector, xMutation, global),
|
|
191
|
-
});
|
|
192
|
-
await callbacks.commitMutation(selection, yMutation, {
|
|
190
|
+
await callbacks.commitMutation(selection, mutation, {
|
|
193
191
|
label: "Move layer",
|
|
194
192
|
softReload: true,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
193
|
+
instantPatch: {
|
|
194
|
+
selector,
|
|
195
|
+
change: { kind: global ? "global-set" : "set", props: mutation.properties },
|
|
196
|
+
},
|
|
199
197
|
});
|
|
200
198
|
return;
|
|
201
199
|
}
|
|
@@ -279,7 +277,7 @@ export async function commitStaticGsapRotation(
|
|
|
279
277
|
* tween: one keyframe at the playhead % renders NaN/0 at every other frame, so
|
|
280
278
|
* the element collapses/disappears (worst when resized off the 0% mark). A `set`
|
|
281
279
|
* holds the size at all times. Re-resizing an element that already has a size
|
|
282
|
-
* `set` UPDATES it in place
|
|
280
|
+
* `set` UPDATES it in place with one `update-properties`; a new element
|
|
283
281
|
* gets one `add` with `method:"set"`.
|
|
284
282
|
*/
|
|
285
283
|
export async function commitStaticGsapSize(
|
|
@@ -292,18 +290,11 @@ export async function commitStaticGsapSize(
|
|
|
292
290
|
const width = Math.round(size.width);
|
|
293
291
|
const height = Math.round(size.height);
|
|
294
292
|
if (existingSet) {
|
|
295
|
-
await callbacks.commitMutation(
|
|
296
|
-
selection,
|
|
297
|
-
{ type: "delete", animationId: existingSet.id },
|
|
298
|
-
{ label: "Resize layer", skipReload: true },
|
|
299
|
-
);
|
|
300
293
|
await callbacks.commitMutation(
|
|
301
294
|
selection,
|
|
302
295
|
{
|
|
303
|
-
type: "
|
|
304
|
-
|
|
305
|
-
method: "set",
|
|
306
|
-
position: 0,
|
|
296
|
+
type: "update-properties",
|
|
297
|
+
animationId: existingSet.id,
|
|
307
298
|
properties: { width, height },
|
|
308
299
|
},
|
|
309
300
|
{ label: "Resize layer", softReload: true },
|
|
@@ -394,10 +385,9 @@ export async function commitKeyframedSizeFromResize(
|
|
|
394
385
|
properties: Math.abs(p - pct) < 0.05 ? { width: newW, height: newH } : { ...prior },
|
|
395
386
|
}));
|
|
396
387
|
|
|
397
|
-
// Add the size keyframe tween FIRST, then delete the old global hold. The
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
// commit triggers the reload.
|
|
388
|
+
// Add the size keyframe tween FIRST, then delete the old global hold. The gesture
|
|
389
|
+
// transport applies both in one ordered batch; a plain commit fallback keeps the
|
|
390
|
+
// same recoverable ordering. Only the transaction's result triggers the reload.
|
|
401
391
|
const addLabel = `Resize (size keyframe ${pct.toFixed(0)}%)`;
|
|
402
392
|
await callbacks.commitMutation(
|
|
403
393
|
selection,
|