@hyperframes/studio 0.7.55 → 0.7.57
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--Z69cEkE.js +459 -0
- package/dist/assets/{index-CPetwHFV.js → index-Bf1x1y8H.js} +1 -1
- package/dist/assets/index-DfmYkU44.js +423 -0
- package/dist/assets/{index-BXaqaVKt.js → index-KsfE1bUu.js} +1 -1
- package/dist/assets/index-_pqzyxB1.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 +172 -144
- package/dist/index.html +2 -2
- package/dist/index.js +43613 -40346
- 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 +152 -1
- package/src/components/editor/CanvasContextMenu.tsx +33 -18
- 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 +126 -125
- 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 +53 -2
- 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 +106 -10
- package/src/components/editor/canvasContextMenuZOrder.test.ts +186 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +144 -22
- 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/propertyPanelColorGradingSection.tsx +15 -21
- 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/useCanvasContextMenuState.ts +92 -0
- 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/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +171 -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 +260 -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.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +343 -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 +56 -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 +23 -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 +125 -1
- package/src/hooks/timelineEditingHelpers.ts +53 -292
- package/src/hooks/timelineMoveAdapter.test.ts +81 -0
- package/src/hooks/timelineMoveAdapter.ts +39 -0
- package/src/hooks/timelineTimingSync.test.ts +150 -0
- package/src/hooks/timelineTimingSync.ts +412 -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/useCaptionDetection.ts +2 -0
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +245 -1
- package/src/hooks/useDomEditCommits.ts +162 -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 +423 -0
- package/src/hooks/useElementLifecycleOps.ts +53 -71
- package/src/hooks/useElementPicker.ts +7 -8
- 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 +148 -18
- 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/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +398 -241
- package/src/hooks/useTimelineEditing.ts +181 -265
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +167 -132
- 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/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +55 -5
- package/src/player/components/Timeline.test.ts +183 -23
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +218 -523
- 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/TimelineRuler.tsx +58 -48
- 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 +1026 -0
- package/src/player/components/timelineClipDragCommit.ts +498 -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 +42 -2
- 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 +207 -75
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +60 -0
- package/src/player/components/timelineStackingSync.ts +27 -2
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- 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/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +77 -0
- package/src/player/components/useTimelineStackingSync.ts +94 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
- package/src/player/hooks/useTimelinePlayer.ts +52 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
- package/src/player/index.ts +5 -2
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- 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 +21 -81
- package/src/player/lib/timelineElementHelpers.ts +73 -5
- package/src/player/lib/timelineIframeHelpers.ts +4 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +99 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- 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/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +67 -29
- package/src/utils/timelineDiscovery.ts +0 -17
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-BRwkMj0w.js +0 -423
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- 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
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { act, createElement, useRef } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
5
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
3
6
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
4
7
|
import {
|
|
@@ -7,8 +10,20 @@ import {
|
|
|
7
10
|
isPlayheadWithinTween,
|
|
8
11
|
promoteSetToKeyframes,
|
|
9
12
|
resolveNewTweenRange,
|
|
13
|
+
useEnableKeyframes,
|
|
10
14
|
type EnableKeyframesSession,
|
|
11
15
|
} from "./useEnableKeyframes";
|
|
16
|
+
import { usePlayerStore } from "../player/store/playerStore";
|
|
17
|
+
|
|
18
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
19
|
+
|
|
20
|
+
let cleanup: (() => void) | null = null;
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
cleanup?.();
|
|
23
|
+
cleanup = null;
|
|
24
|
+
vi.unstubAllGlobals();
|
|
25
|
+
window.location.hash = "";
|
|
26
|
+
});
|
|
12
27
|
|
|
13
28
|
function anim(overrides: Partial<GsapAnimation>): GsapAnimation {
|
|
14
29
|
return {
|
|
@@ -203,3 +218,129 @@ describe("promoteSetToKeyframes — auto endpoint", () => {
|
|
|
203
218
|
expect(kfs[0].percentage).toBe(0);
|
|
204
219
|
});
|
|
205
220
|
});
|
|
221
|
+
|
|
222
|
+
function renderEnableKeyframes(session: EnableKeyframesSession): () => Promise<void> {
|
|
223
|
+
let enable: (() => Promise<void>) | null = null;
|
|
224
|
+
function Probe() {
|
|
225
|
+
const sessionRef = useRef<EnableKeyframesSession | undefined>(session);
|
|
226
|
+
enable = useEnableKeyframes(sessionRef);
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
const container = document.createElement("div");
|
|
230
|
+
const root = createRoot(container);
|
|
231
|
+
act(() => root.render(createElement(Probe)));
|
|
232
|
+
cleanup = () => act(() => root.unmount());
|
|
233
|
+
if (!enable) throw new Error("hook did not initialize");
|
|
234
|
+
return enable;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function flatTweenResponses(flat: GsapAnimation, converted: GsapAnimation) {
|
|
238
|
+
const responses = [{ animations: [flat] }, { animations: [converted] }];
|
|
239
|
+
vi.stubGlobal(
|
|
240
|
+
"fetch",
|
|
241
|
+
vi.fn(async () => ({
|
|
242
|
+
ok: true,
|
|
243
|
+
json: async () => responses.shift() ?? { animations: [] },
|
|
244
|
+
})),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function makeElementSelection(): DomEditSelection {
|
|
249
|
+
const element = document.body.appendChild(document.createElement("div"));
|
|
250
|
+
element.id = "el";
|
|
251
|
+
return {
|
|
252
|
+
id: "el",
|
|
253
|
+
selector: "#el",
|
|
254
|
+
sourceFile: "index.html",
|
|
255
|
+
element,
|
|
256
|
+
} as DomEditSelection;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function stubFlatTweenConversion(id: string): {
|
|
260
|
+
flat: GsapAnimation;
|
|
261
|
+
converted: GsapAnimation;
|
|
262
|
+
} {
|
|
263
|
+
const flat = anim({ id, position: 1, duration: 1, properties: { x: 10 } });
|
|
264
|
+
const converted = anim({
|
|
265
|
+
id,
|
|
266
|
+
position: 1,
|
|
267
|
+
duration: 1,
|
|
268
|
+
keyframes: {
|
|
269
|
+
format: "object-array",
|
|
270
|
+
keyframes: [
|
|
271
|
+
{ percentage: 0, properties: { x: 0 } },
|
|
272
|
+
{ percentage: 100, properties: { x: 10 } },
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
flatTweenResponses(flat, converted);
|
|
277
|
+
return { flat, converted };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
describe("useEnableKeyframes — flat tween transaction", () => {
|
|
281
|
+
it("skips the convert reload and coalesces an outside-range terminal soft reload", async () => {
|
|
282
|
+
window.location.hash = "#/project/test-project";
|
|
283
|
+
usePlayerStore.setState({ currentTime: 3 });
|
|
284
|
+
const selection = makeElementSelection();
|
|
285
|
+
const { flat } = stubFlatTweenConversion("flat-1");
|
|
286
|
+
const handleConvert = vi.fn(async () => undefined);
|
|
287
|
+
const commitMutation = vi.fn(async () => undefined);
|
|
288
|
+
const enable = renderEnableKeyframes({
|
|
289
|
+
domEditSelection: selection,
|
|
290
|
+
selectedGsapAnimations: [flat],
|
|
291
|
+
previewIframeRef: {
|
|
292
|
+
current: {
|
|
293
|
+
contentWindow: { gsap: { getProperty: () => 10 } },
|
|
294
|
+
} as unknown as HTMLIFrameElement,
|
|
295
|
+
},
|
|
296
|
+
handleGsapAddAnimation: vi.fn(),
|
|
297
|
+
handleGsapConvertToKeyframes: handleConvert,
|
|
298
|
+
handleGsapRemoveKeyframe: vi.fn(),
|
|
299
|
+
commitMutation,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
await act(async () => enable());
|
|
303
|
+
|
|
304
|
+
const convertOptions = handleConvert.mock.calls[0]?.[3];
|
|
305
|
+
const terminalOptions = commitMutation.mock.calls[0]?.[1];
|
|
306
|
+
expect(convertOptions).toMatchObject({ skipReload: true, coalesceMs: Infinity });
|
|
307
|
+
expect(terminalOptions).toMatchObject({
|
|
308
|
+
softReload: true,
|
|
309
|
+
coalesceKey: convertOptions?.coalesceKey,
|
|
310
|
+
});
|
|
311
|
+
expect(convertOptions?.coalesceKey).toEqual(expect.any(String));
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("passes the convert coalesce key to an inside-range batch edit", async () => {
|
|
315
|
+
window.location.hash = "#/project/test-project";
|
|
316
|
+
usePlayerStore.setState({ currentTime: 1.5 });
|
|
317
|
+
const selection = makeElementSelection();
|
|
318
|
+
const { flat } = stubFlatTweenConversion("flat-2");
|
|
319
|
+
const handleConvert = vi.fn(async () => undefined);
|
|
320
|
+
const addKeyframeBatch = vi.fn(async () => undefined);
|
|
321
|
+
const enable = renderEnableKeyframes({
|
|
322
|
+
domEditSelection: selection,
|
|
323
|
+
selectedGsapAnimations: [flat],
|
|
324
|
+
previewIframeRef: {
|
|
325
|
+
current: {
|
|
326
|
+
contentWindow: { gsap: { getProperty: () => 5 } },
|
|
327
|
+
} as unknown as HTMLIFrameElement,
|
|
328
|
+
},
|
|
329
|
+
handleGsapAddAnimation: vi.fn(),
|
|
330
|
+
handleGsapConvertToKeyframes: handleConvert,
|
|
331
|
+
handleGsapRemoveKeyframe: vi.fn(),
|
|
332
|
+
handleGsapAddKeyframeBatch: addKeyframeBatch,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
await act(async () => enable());
|
|
336
|
+
|
|
337
|
+
const convertOptions = handleConvert.mock.calls[0]?.[3];
|
|
338
|
+
expect(addKeyframeBatch.mock.calls[0]?.[3]).toEqual({
|
|
339
|
+
softReload: true,
|
|
340
|
+
coalesceKey: convertOptions?.coalesceKey,
|
|
341
|
+
// Must carry the convert phase's infinite window so the inside-range
|
|
342
|
+
// apply folds into one undo entry regardless of round-trip latency.
|
|
343
|
+
coalesceMs: Infinity,
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
});
|
|
@@ -12,7 +12,7 @@ import type { GsapAnimation, GsapPercentageKeyframe } from "@hyperframes/core/gs
|
|
|
12
12
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
13
13
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
14
14
|
import { fetchParsedAnimations, getAnimationsForElement } from "./useGsapTweenCache";
|
|
15
|
-
import { selectorFromSelection, computeElementPercentage } from "./gsapShared";
|
|
15
|
+
import { selectorFromSelection, computeElementPercentage, isInstantHold } from "./gsapShared";
|
|
16
16
|
import {
|
|
17
17
|
resolveTweenStart,
|
|
18
18
|
resolveTweenDuration,
|
|
@@ -21,6 +21,9 @@ import {
|
|
|
21
21
|
import { POSITION_PROPS } from "./gsapRuntimeReaders";
|
|
22
22
|
import { roundTo3 } from "../utils/rounding";
|
|
23
23
|
import { nearestPointOnPath } from "../components/editor/motionPathGeometry";
|
|
24
|
+
import type { CommitMutationOptions } from "./gsapScriptCommitTypes";
|
|
25
|
+
|
|
26
|
+
let enableKeyframesTransactionCounter = 0;
|
|
24
27
|
|
|
25
28
|
export interface EnableKeyframesSession {
|
|
26
29
|
domEditSelection: DomEditSelection | null;
|
|
@@ -30,16 +33,19 @@ export interface EnableKeyframesSession {
|
|
|
30
33
|
handleGsapConvertToKeyframes: (
|
|
31
34
|
animId: string,
|
|
32
35
|
resolvedFromValues?: Record<string, number | string>,
|
|
36
|
+
duration?: number,
|
|
37
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
33
38
|
) => void | Promise<void>;
|
|
34
39
|
handleGsapRemoveKeyframe: (animId: string, pct: number) => void;
|
|
35
40
|
handleGsapAddKeyframeBatch?: (
|
|
36
41
|
animId: string,
|
|
37
42
|
pct: number,
|
|
38
43
|
properties: Record<string, number | string>,
|
|
44
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
39
45
|
) => Promise<void>;
|
|
40
46
|
commitMutation?: (
|
|
41
47
|
mutation: Record<string, unknown>,
|
|
42
|
-
options:
|
|
48
|
+
options: CommitMutationOptions,
|
|
43
49
|
) => Promise<void>;
|
|
44
50
|
}
|
|
45
51
|
|
|
@@ -107,6 +113,36 @@ export function buildExtendedKeyframes(
|
|
|
107
113
|
return { position: roundTo3(newStart), duration: newDuration, keyframes };
|
|
108
114
|
}
|
|
109
115
|
|
|
116
|
+
async function replaceSetWithSingleKeyframe(
|
|
117
|
+
session: EnableKeyframesSession,
|
|
118
|
+
sel: DomEditSelection,
|
|
119
|
+
setAnim: GsapAnimation,
|
|
120
|
+
t: number,
|
|
121
|
+
iframe: HTMLIFrameElement | null,
|
|
122
|
+
selector: string,
|
|
123
|
+
): Promise<void> {
|
|
124
|
+
const position = readElementPosition(iframe, sel, setAnim);
|
|
125
|
+
if (Object.keys(position).length === 0) {
|
|
126
|
+
for (const [key, held] of Object.entries(setAnim.properties ?? {})) {
|
|
127
|
+
if (typeof held === "number") position[key] = held;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (Object.keys(position).length === 0 || !session.commitMutation) return;
|
|
131
|
+
const range = resolveNewTweenRange(sel.dataAttributes?.start, sel.dataAttributes?.duration, t);
|
|
132
|
+
await session.commitMutation(
|
|
133
|
+
{
|
|
134
|
+
type: "replace-with-keyframes",
|
|
135
|
+
animationId: setAnim.id,
|
|
136
|
+
targetSelector: selector,
|
|
137
|
+
position: roundTo3(range.start),
|
|
138
|
+
duration: roundTo3(range.duration),
|
|
139
|
+
keyframes: [{ percentage: 0, properties: position }],
|
|
140
|
+
ease: setAnim.ease,
|
|
141
|
+
},
|
|
142
|
+
{ label: "Enable keyframes", softReload: true },
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
110
146
|
// fallow-ignore-next-line complexity
|
|
111
147
|
function readElementPosition(
|
|
112
148
|
iframe: HTMLIFrameElement | null,
|
|
@@ -199,6 +235,7 @@ async function applyKeyframeAtPlayhead(
|
|
|
199
235
|
kfAnim: GsapAnimation,
|
|
200
236
|
t: number,
|
|
201
237
|
iframe: HTMLIFrameElement | null,
|
|
238
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
202
239
|
): Promise<void> {
|
|
203
240
|
if (!isPlayheadWithinTween(kfAnim, t)) {
|
|
204
241
|
const position = readElementPosition(iframe, sel, kfAnim);
|
|
@@ -215,7 +252,11 @@ async function applyKeyframeAtPlayhead(
|
|
|
215
252
|
keyframes: extended.keyframes,
|
|
216
253
|
ease: kfAnim.ease,
|
|
217
254
|
},
|
|
218
|
-
{
|
|
255
|
+
{
|
|
256
|
+
label: "Add keyframe",
|
|
257
|
+
softReload: true,
|
|
258
|
+
...commitOverrides,
|
|
259
|
+
},
|
|
219
260
|
);
|
|
220
261
|
}
|
|
221
262
|
return;
|
|
@@ -229,7 +270,7 @@ async function applyKeyframeAtPlayhead(
|
|
|
229
270
|
if (session.handleGsapAddKeyframeBatch) {
|
|
230
271
|
const position = readElementPosition(iframe, sel, kfAnim);
|
|
231
272
|
if (Object.keys(position).length > 0) {
|
|
232
|
-
await session.handleGsapAddKeyframeBatch(kfAnim.id, pct, position);
|
|
273
|
+
await session.handleGsapAddKeyframeBatch(kfAnim.id, pct, position, commitOverrides);
|
|
233
274
|
}
|
|
234
275
|
}
|
|
235
276
|
}
|
|
@@ -259,27 +300,7 @@ export async function promoteSetToKeyframes(
|
|
|
259
300
|
// the set with a single keyframe at the playhead holding its value, matching the
|
|
260
301
|
// no-animation branch: one diamond the user can build motion from.
|
|
261
302
|
if (t <= setStart) {
|
|
262
|
-
|
|
263
|
-
if (Object.keys(position).length === 0) {
|
|
264
|
-
for (const key of Object.keys(setAnim.properties ?? {})) {
|
|
265
|
-
const held = setAnim.properties?.[key];
|
|
266
|
-
if (typeof held === "number") position[key] = held;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
if (Object.keys(position).length === 0) return;
|
|
270
|
-
const range = resolveNewTweenRange(sel.dataAttributes?.start, sel.dataAttributes?.duration, t);
|
|
271
|
-
await session.commitMutation(
|
|
272
|
-
{
|
|
273
|
-
type: "replace-with-keyframes",
|
|
274
|
-
animationId: setAnim.id,
|
|
275
|
-
targetSelector: selector,
|
|
276
|
-
position: roundTo3(range.start),
|
|
277
|
-
duration: roundTo3(range.duration),
|
|
278
|
-
keyframes: [{ percentage: 0, properties: position }],
|
|
279
|
-
ease: setAnim.ease,
|
|
280
|
-
},
|
|
281
|
-
{ label: "Enable keyframes", softReload: true },
|
|
282
|
-
);
|
|
303
|
+
await replaceSetWithSingleKeyframe(session, sel, setAnim, t, iframe, selector);
|
|
283
304
|
return;
|
|
284
305
|
}
|
|
285
306
|
const endPosition = readElementPosition(iframe, sel, setAnim);
|
|
@@ -395,8 +416,8 @@ export function useEnableKeyframes(
|
|
|
395
416
|
// the curve.
|
|
396
417
|
const arcAnim = anims.find((a) => a.arcPath);
|
|
397
418
|
const kfAnim = anims.find((a) => a.keyframes && !a.arcPath);
|
|
398
|
-
const setAnim = anims.find((a) => a
|
|
399
|
-
const flatAnim = anims.find((a) => !a.keyframes && !a.arcPath && a
|
|
419
|
+
const setAnim = anims.find((a) => isInstantHold(a) && !a.keyframes && !a.arcPath);
|
|
420
|
+
const flatAnim = anims.find((a) => !a.keyframes && !a.arcPath && !isInstantHold(a));
|
|
400
421
|
|
|
401
422
|
if (arcAnim) {
|
|
402
423
|
await applyArcWaypointAtPlayhead(session, sel, arcAnim, t, iframe);
|
|
@@ -409,9 +430,32 @@ export function useEnableKeyframes(
|
|
|
409
430
|
// resolvedFromValues, so the 0%/100% stops keep the real start→end motion
|
|
410
431
|
// (passing the playhead value would flatten it). Then apply uniformly so an
|
|
411
432
|
// out-of-range playhead extends the range just like a keyframe tween.
|
|
412
|
-
|
|
433
|
+
enableKeyframesTransactionCounter += 1;
|
|
434
|
+
const coalesceKey = `enable-keyframes:${flatAnim.id}:${enableKeyframesTransactionCounter}`;
|
|
435
|
+
const convertCommitOverrides: Partial<CommitMutationOptions> = {
|
|
436
|
+
skipReload: true,
|
|
437
|
+
coalesceKey,
|
|
438
|
+
coalesceMs: Number.POSITIVE_INFINITY,
|
|
439
|
+
};
|
|
440
|
+
await session.handleGsapConvertToKeyframes(
|
|
441
|
+
flatAnim.id,
|
|
442
|
+
undefined,
|
|
443
|
+
undefined,
|
|
444
|
+
convertCommitOverrides,
|
|
445
|
+
);
|
|
413
446
|
const converted = (await fetchAnimationsForElement(sel)).find((a) => a.keyframes);
|
|
414
|
-
if (converted)
|
|
447
|
+
if (converted) {
|
|
448
|
+
const applyCommitOverrides: Partial<CommitMutationOptions> = {
|
|
449
|
+
softReload: true,
|
|
450
|
+
coalesceKey,
|
|
451
|
+
// Must match the convert phase's window: editHistory keys coalescing
|
|
452
|
+
// off the incoming entry, so without Infinity here the apply (landing
|
|
453
|
+
// after two POSTs + a fetch) falls back to the 300ms default and the
|
|
454
|
+
// conversion splits into two undo entries under real latency.
|
|
455
|
+
coalesceMs: Number.POSITIVE_INFINITY,
|
|
456
|
+
};
|
|
457
|
+
await applyKeyframeAtPlayhead(session, sel, converted, t, iframe, applyCommitOverrides);
|
|
458
|
+
}
|
|
415
459
|
} else {
|
|
416
460
|
const position = readElementPosition(iframe, sel, null);
|
|
417
461
|
const { start: elStart, duration: elDuration } = resolveNewTweenRange(
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
5
|
+
import { usePlayerStore } from "../player";
|
|
6
|
+
import { mountReactHarness } from "./domSelectionTestHarness";
|
|
7
|
+
import type { CommitMutationOptions } from "./gsapScriptCommitTypes";
|
|
8
|
+
import { useGestureCommit } from "./useGestureCommit";
|
|
9
|
+
|
|
10
|
+
const gestureRecording = vi.hoisted(() => ({
|
|
11
|
+
startRecording: vi.fn(),
|
|
12
|
+
stopRecording: vi.fn(() => [
|
|
13
|
+
{ time: 0, properties: { x: 0, y: 0, opacity: 1 } },
|
|
14
|
+
{ time: 0.5, properties: { x: 50, y: 25, opacity: 0.5 } },
|
|
15
|
+
{ time: 1, properties: { x: 100, y: 50, opacity: 0 } },
|
|
16
|
+
]),
|
|
17
|
+
clearSamples: vi.fn(),
|
|
18
|
+
cancelRecording: vi.fn(),
|
|
19
|
+
isRecording: false,
|
|
20
|
+
recordingDuration: 0,
|
|
21
|
+
samplesRef: { current: [] },
|
|
22
|
+
trailRef: { current: [] },
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("./useGestureRecording", () => ({
|
|
26
|
+
useGestureRecording: () => gestureRecording,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock("../utils/rdpSimplify", () => ({
|
|
30
|
+
simplifyGestureSamples: () =>
|
|
31
|
+
new Map([
|
|
32
|
+
[0, { x: 0, y: 0, opacity: 1 }],
|
|
33
|
+
[50, { x: 50, y: 25, opacity: 0.5 }],
|
|
34
|
+
[100, { x: 100, y: 50, opacity: 0 }],
|
|
35
|
+
]),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
vi.mock("../utils/gestureSmoother", () => ({
|
|
39
|
+
smoothGestureKeyframes: (keyframes: unknown) => keyframes,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
vi.mock("../utils/velocityEaseFitter", () => ({
|
|
43
|
+
fitEasesFromVelocity: (keyframes: unknown) => keyframes,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
47
|
+
|
|
48
|
+
let cleanup: (() => void) | null = null;
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
cleanup?.();
|
|
52
|
+
cleanup = null;
|
|
53
|
+
usePlayerStore.getState().reset();
|
|
54
|
+
document.body.replaceChildren();
|
|
55
|
+
vi.clearAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function makeSelection(element: HTMLElement): DomEditSelection {
|
|
59
|
+
return {
|
|
60
|
+
id: element.id,
|
|
61
|
+
element,
|
|
62
|
+
label: "Card",
|
|
63
|
+
tagName: "div",
|
|
64
|
+
sourceFile: "index.html",
|
|
65
|
+
compositionPath: "index.html",
|
|
66
|
+
isCompositionHost: false,
|
|
67
|
+
isInsideLockedComposition: false,
|
|
68
|
+
boundingBox: { x: 0, y: 0, width: 100, height: 100 },
|
|
69
|
+
textContent: null,
|
|
70
|
+
dataAttributes: { start: "0", duration: "2" },
|
|
71
|
+
inlineStyles: {},
|
|
72
|
+
computedStyles: {},
|
|
73
|
+
textFields: [],
|
|
74
|
+
capabilities: {
|
|
75
|
+
canSelect: true,
|
|
76
|
+
canEditStyles: true,
|
|
77
|
+
canCrop: true,
|
|
78
|
+
canMove: true,
|
|
79
|
+
canResize: true,
|
|
80
|
+
canApplyManualOffset: true,
|
|
81
|
+
canApplyManualSize: true,
|
|
82
|
+
canApplyManualRotation: true,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("useGestureCommit", () => {
|
|
88
|
+
it("coalesces property-group commits and reloads only the terminal group", async () => {
|
|
89
|
+
const iframe = document.createElement("iframe");
|
|
90
|
+
document.body.append(iframe);
|
|
91
|
+
const element = document.createElement("div");
|
|
92
|
+
element.id = "card";
|
|
93
|
+
const commitMutation = vi.fn<
|
|
94
|
+
(mutation: Record<string, unknown>, options: CommitMutationOptions) => Promise<void>
|
|
95
|
+
>(async () => {});
|
|
96
|
+
const sessionRef = {
|
|
97
|
+
current: {
|
|
98
|
+
domEditSelection: makeSelection(element),
|
|
99
|
+
selectedGsapAnimations: [],
|
|
100
|
+
commitMutation,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
const captured: { hook: ReturnType<typeof useGestureCommit> | null } = { hook: null };
|
|
104
|
+
function Probe() {
|
|
105
|
+
captured.hook = useGestureCommit({
|
|
106
|
+
domEditSessionRef: sessionRef,
|
|
107
|
+
previewIframeRef: { current: iframe },
|
|
108
|
+
showToast: vi.fn(),
|
|
109
|
+
isGestureRecordingRef: { current: false },
|
|
110
|
+
});
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const root = mountReactHarness(<Probe />);
|
|
114
|
+
cleanup = () => act(() => root.unmount());
|
|
115
|
+
if (!captured.hook) throw new Error("hook did not initialize");
|
|
116
|
+
|
|
117
|
+
act(() => captured.hook?.handleToggleRecording());
|
|
118
|
+
act(() => captured.hook?.handleToggleRecording());
|
|
119
|
+
await act(async () => {
|
|
120
|
+
await vi.waitFor(() => expect(commitMutation).toHaveBeenCalledTimes(2));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const options = commitMutation.mock.calls.map((call) => call[1]);
|
|
124
|
+
expect(new Set(options.map((entry) => entry.coalesceKey)).size).toBe(1);
|
|
125
|
+
expect(options[0]).toEqual(expect.objectContaining({ coalesceMs: Infinity, skipReload: true }));
|
|
126
|
+
expect(options[0]).not.toHaveProperty("softReload");
|
|
127
|
+
expect(options[1]).toEqual(expect.objectContaining({ coalesceMs: Infinity, softReload: true }));
|
|
128
|
+
expect(options[1]).not.toHaveProperty("skipReload");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -10,8 +10,10 @@ import { smoothGestureKeyframes } from "../utils/gestureSmoother";
|
|
|
10
10
|
import { usePlayerStore } from "../player";
|
|
11
11
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
12
12
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
13
|
+
import type { CommitMutationOptions } from "./gsapScriptCommitTypes";
|
|
13
14
|
import { roundTo3 } from "../utils/rounding";
|
|
14
15
|
import { classifyPropertyGroup } from "@hyperframes/core/gsap-parser";
|
|
16
|
+
import { isInstantHold } from "./gsapShared";
|
|
15
17
|
|
|
16
18
|
type RecordedKeyframe = {
|
|
17
19
|
percentage: number;
|
|
@@ -68,10 +70,18 @@ interface GestureSessionRef {
|
|
|
68
70
|
selectedGsapAnimations?: GsapAnimation[];
|
|
69
71
|
commitMutation?: (
|
|
70
72
|
mutation: Record<string, unknown>,
|
|
71
|
-
options:
|
|
73
|
+
options: CommitMutationOptions,
|
|
72
74
|
) => Promise<void>;
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
/** Only the LAST group in a per-group commit loop reloads the preview; the
|
|
78
|
+
* earlier ones skip it, so a multi-group gesture recording is one reload. */
|
|
79
|
+
function reloadOnlyLast(index: number, count: number): Partial<CommitMutationOptions> {
|
|
80
|
+
return index === count - 1 ? { softReload: true } : { skipReload: true };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let gestureRecordingCommitCounter = 0;
|
|
84
|
+
|
|
75
85
|
interface UseGestureCommitParams {
|
|
76
86
|
domEditSessionRef: React.MutableRefObject<GestureSessionRef>;
|
|
77
87
|
previewIframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
@@ -112,6 +122,10 @@ export function useGestureCommit({
|
|
|
112
122
|
return;
|
|
113
123
|
}
|
|
114
124
|
commitInFlightRef.current = true;
|
|
125
|
+
const coalesceOptions = {
|
|
126
|
+
coalesceKey: `gesture-recording:${++gestureRecordingCommitCounter}`,
|
|
127
|
+
coalesceMs: Number.POSITIVE_INFINITY,
|
|
128
|
+
};
|
|
115
129
|
gestureStateRef.current = "idle";
|
|
116
130
|
isGestureRecordingRef.current = false;
|
|
117
131
|
const frozenSamples = gestureRecording.stopRecording();
|
|
@@ -175,9 +189,9 @@ export function useGestureCommit({
|
|
|
175
189
|
? allAnims.find((a) => a.propertyGroup === "position" && a.targetSelector === selector)
|
|
176
190
|
: undefined;
|
|
177
191
|
if (existingPositionTween) {
|
|
178
|
-
if (existingPositionTween
|
|
179
|
-
//
|
|
180
|
-
//
|
|
192
|
+
if (isInstantHold(existingPositionTween)) {
|
|
193
|
+
// An instant hold is not a tween to merge into — replace it with the
|
|
194
|
+
// recorded motion (which already starts from the held position).
|
|
181
195
|
await liveSession.commitMutation(
|
|
182
196
|
{
|
|
183
197
|
type: "replace-with-keyframes",
|
|
@@ -242,7 +256,8 @@ export function useGestureCommit({
|
|
|
242
256
|
// Emit one tween per property group so a mixed-prop gesture (e.g.
|
|
243
257
|
// x/y + opacity) doesn't collapse into an untagged legacy mixed
|
|
244
258
|
// tween that the position-only drag intercept can't edit.
|
|
245
|
-
|
|
259
|
+
const keyframeGroups = partitionKeyframesByGroup(keyframes);
|
|
260
|
+
for (const [index, groupKfs] of keyframeGroups.entries()) {
|
|
246
261
|
await liveSession.commitMutation(
|
|
247
262
|
{
|
|
248
263
|
type: "add-with-keyframes",
|
|
@@ -256,14 +271,19 @@ export function useGestureCommit({
|
|
|
256
271
|
// not inherit a sigmoid.
|
|
257
272
|
easeEach: "none",
|
|
258
273
|
},
|
|
259
|
-
{
|
|
274
|
+
{
|
|
275
|
+
label: "Gesture recording (new range)",
|
|
276
|
+
...coalesceOptions,
|
|
277
|
+
...reloadOnlyLast(index, keyframeGroups.length),
|
|
278
|
+
},
|
|
260
279
|
);
|
|
261
280
|
}
|
|
262
281
|
}
|
|
263
282
|
}
|
|
264
283
|
} else {
|
|
265
284
|
// No existing tween — same per-group split as the new-range branch above.
|
|
266
|
-
|
|
285
|
+
const keyframeGroups = partitionKeyframesByGroup(keyframes);
|
|
286
|
+
for (const [index, groupKfs] of keyframeGroups.entries()) {
|
|
267
287
|
await liveSession.commitMutation(
|
|
268
288
|
{
|
|
269
289
|
type: "add-with-keyframes",
|
|
@@ -274,7 +294,11 @@ export function useGestureCommit({
|
|
|
274
294
|
// Linear fallback (see above) — constant-speed segments stay linear.
|
|
275
295
|
easeEach: "none",
|
|
276
296
|
},
|
|
277
|
-
{
|
|
297
|
+
{
|
|
298
|
+
label: "Gesture recording",
|
|
299
|
+
...coalesceOptions,
|
|
300
|
+
...reloadOnlyLast(index, keyframeGroups.length),
|
|
301
|
+
},
|
|
278
302
|
);
|
|
279
303
|
}
|
|
280
304
|
}
|
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
readProjectFileContent,
|
|
4
|
+
saveProjectFilesWithHistory,
|
|
5
|
+
type DomEditCommitBaseParams,
|
|
6
|
+
} from "../utils/studioFileHistory";
|
|
4
7
|
import { buildDomEditPatchTarget, type DomEditSelection } from "../components/editor/domEditing";
|
|
5
|
-
import type { EditHistoryKind } from "../utils/editHistory";
|
|
6
8
|
|
|
7
|
-
interface
|
|
8
|
-
label: string;
|
|
9
|
-
kind: EditHistoryKind;
|
|
10
|
-
coalesceKey?: string;
|
|
11
|
-
files: Record<string, { before: string; after: string }>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface UseGroupCommitsParams {
|
|
15
|
-
activeCompPath: string | null;
|
|
16
|
-
showToast: (message: string, tone?: "error" | "info") => void;
|
|
17
|
-
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
18
|
-
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
19
|
-
editHistory: { recordEdit: (entry: RecordEditInput) => Promise<void> };
|
|
20
|
-
projectIdRef: React.MutableRefObject<string | null>;
|
|
21
|
-
reloadPreview: () => void;
|
|
22
|
-
clearDomSelection: () => void;
|
|
9
|
+
interface UseGroupCommitsParams extends DomEditCommitBaseParams {
|
|
23
10
|
/** Resync the SDK session after a server-side write (the wrapper/unwrap changes
|
|
24
11
|
* structure the in-memory doc doesn't know about). */
|
|
25
12
|
forceReloadSdkSession?: () => void;
|
|
@@ -81,15 +68,7 @@ async function commitStructuralMutation(
|
|
|
81
68
|
| "reloadPreview"
|
|
82
69
|
>,
|
|
83
70
|
): Promise<{ content?: string; groupId?: string }> {
|
|
84
|
-
const
|
|
85
|
-
if (!response.ok) {
|
|
86
|
-
throw await createStudioSaveHttpError(response, `Failed to read ${targetPath}`);
|
|
87
|
-
}
|
|
88
|
-
const data = (await response.json()) as { content?: string };
|
|
89
|
-
const originalContent = data.content;
|
|
90
|
-
if (typeof originalContent !== "string") {
|
|
91
|
-
throw new Error(`Missing file contents for ${targetPath}`);
|
|
92
|
-
}
|
|
71
|
+
const originalContent = await readProjectFileContent(pid, targetPath);
|
|
93
72
|
|
|
94
73
|
deps.domEditSaveTimestampRef.current = Date.now();
|
|
95
74
|
const mutateResponse = await fetch(
|