@hyperframes/studio 0.8.114 → 0.8.116
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-mGYvPVpw.js} +1 -1
- package/dist/assets/{index-Bn2RJNTJ.js → index-BWHt0-ir.js} +1 -1
- package/dist/assets/{index-Dz7doz9l.js → index-DYNaXPVf.js} +203 -203
- package/dist/assets/{index-C9N-DGjC.js → index-Dr3hNpvS.js} +1 -1
- package/dist/assets/{index-BYnsJnJV.css → index-t1LH6H5H.css} +1 -1
- package/dist/{chunk-BIUXP3PG.js → chunk-3F47EFCT.js} +142 -277
- package/dist/chunk-3F47EFCT.js.map +1 -0
- 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/{domEditingLayers-V67MS4WP.js → domEditingLayers-OL7GLSS4.js} +3 -5
- package/dist/index.d.ts +146 -40
- package/dist/index.html +2 -2
- package/dist/index.js +4021 -3591
- 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/captions/hooks/useCaptionSync.ts +10 -6
- package/src/components/LinkedSelectionToggle.tsx +3 -2
- package/src/components/ProjectUnreachableBanner.tsx +3 -3
- 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/domGroupMoveBy.test.tsx +252 -0
- package/src/components/editor/domGroupMoveBy.ts +95 -0
- package/src/components/editor/dragUndoPaint.test.ts +5 -5
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
- package/src/components/editor/manualEditingAvailability.ts +8 -0
- package/src/components/editor/manualEdits.ts +1 -0
- package/src/components/editor/manualEditsParsing.ts +7 -0
- package/src/components/editor/manualOffsetDrag.ts +5 -6
- 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/motionPathSelection.test.ts +6 -31
- package/src/components/editor/probeSourceElement.test.ts +45 -0
- package/src/components/editor/probeSourceElement.ts +41 -16
- package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -7
- package/src/components/editor/propertyPanelFont.tsx +3 -2
- 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/useColorGradingController.ts +2 -1
- 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/editor/useFxCarveNodes.ts +2 -1
- package/src/components/editor/useFxLevelling.ts +2 -1
- package/src/components/feedback/projectProvenance.ts +2 -1
- package/src/components/nle/NLEContext.test.ts +1 -0
- package/src/components/nle/NLEContext.tsx +2 -1
- package/src/components/nle/NLEPreview.tsx +12 -10
- package/src/components/nle/PreviewPoster.tsx +2 -1
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +1 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +7 -1
- package/src/components/renders/useFfmpegStatus.ts +2 -1
- package/src/components/renders/useRenderQueue.ts +5 -4
- package/src/components/sidebar/AssetContextMenu.tsx +4 -4
- package/src/components/sidebar/AssetsTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
- package/src/components/ui/menuStyle.test.ts +61 -0
- package/src/components/ui/menuStyle.ts +10 -0
- package/src/contexts/DomEditContext.tsx +5 -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/domSelectionTestHarness.ts +23 -0
- package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
- package/src/hooks/domStyleCommit.ts +8 -1
- package/src/hooks/draggedGsapPosition.ts +10 -0
- package/src/hooks/elementOffsetStager.ts +5 -4
- package/src/hooks/gsapDragCommit.test.ts +75 -164
- package/src/hooks/gsapDragCommit.ts +4 -125
- package/src/hooks/gsapDragPositionCommit.ts +36 -384
- package/src/hooks/gsapEditOutcome.ts +22 -2
- package/src/hooks/gsapGestureOutcome.test.ts +161 -0
- package/src/hooks/gsapGestureOutcome.ts +64 -0
- package/src/hooks/gsapGroupPreflight.ts +34 -0
- package/src/hooks/gsapKeyframeCommit.ts +3 -12
- package/src/hooks/gsapMutationClient.ts +4 -3
- package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
- package/src/hooks/gsapParsedTween.test.ts +62 -0
- package/src/hooks/gsapParsedTween.ts +243 -0
- package/src/hooks/gsapPositionDetection.test.ts +78 -0
- package/src/hooks/gsapPositionDetection.ts +52 -33
- package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
- package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
- package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
- package/src/hooks/gsapResizeIntercept.test.ts +96 -11
- package/src/hooks/gsapResizeIntercept.ts +79 -168
- package/src/hooks/gsapResizeSweep.test.ts +16 -33
- package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
- package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
- package/src/hooks/gsapRuntimeBridge.ts +105 -131
- package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
- package/src/hooks/gsapScriptCommitTypes.ts +13 -0
- package/src/hooks/gsapShared.ts +7 -0
- package/src/hooks/gsapShared.writeTarget.test.ts +45 -57
- package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
- package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
- package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
- package/src/hooks/gsapValueAtPlayhead.ts +292 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
- package/src/hooks/keyframeCacheAstLoad.ts +8 -6
- package/src/hooks/newTweenTarget.test.ts +41 -76
- 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/timelineTimingSync.test.ts +1 -0
- package/src/hooks/timelineTimingSync.ts +2 -1
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +46 -5
- package/src/hooks/useAnimatedPropertyCommit.ts +2 -2
- package/src/hooks/useAskAgentModal.ts +2 -1
- package/src/hooks/useBlockCatalog.ts +2 -1
- package/src/hooks/useBlockedTimelineEditToast.ts +12 -3
- package/src/hooks/useCaptionDetection.ts +2 -1
- package/src/hooks/useCompositionContentLoader.ts +2 -1
- package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
- package/src/hooks/useDomEditPositionPatchCommit.ts +18 -23
- package/src/hooks/useDomEditSession.test.tsx +30 -1
- package/src/hooks/useDomEditSession.ts +10 -0
- package/src/hooks/useDomEditTextCommits.test.tsx +41 -0
- package/src/hooks/useDomEditTextCommits.ts +13 -3
- package/src/hooks/useDomEditWiring.ts +4 -2
- package/src/hooks/useDomGeometryCommit.test.tsx +25 -6
- package/src/hooks/useDomGeometryCommit.ts +8 -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/useEditHistoryActions.paint.test.tsx +82 -20
- package/src/hooks/useEditHistoryActions.test.tsx +15 -0
- package/src/hooks/useEditHistoryActions.ts +16 -5
- package/src/hooks/useElementLifecycleOps.ts +2 -1
- package/src/hooks/useEnableKeyframes.test.ts +34 -3
- package/src/hooks/useEnableKeyframes.ts +7 -4
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +121 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +35 -8
- package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
- package/src/hooks/useFileManager.ts +14 -10
- package/src/hooks/useFileTree.ts +3 -2
- package/src/hooks/useFrameCapture.ts +5 -1
- package/src/hooks/useFreezeFrame.ts +3 -2
- package/src/hooks/useGestureCommit.test.tsx +225 -55
- package/src/hooks/useGestureCommit.ts +86 -55
- package/src/hooks/useGroupCommits.ts +2 -1
- package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
- package/src/hooks/useGsapAwareEditing.test.tsx +28 -35
- package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
- package/src/hooks/useGsapAwareEditing.ts +101 -195
- package/src/hooks/useGsapAwareGroupMove.ts +179 -0
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
- package/src/hooks/useGsapKeyframeOps.sdkUsage.test.tsx +114 -0
- package/src/hooks/useGsapKeyframeOps.ts +36 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +60 -1
- package/src/hooks/useGsapScriptCommits.ts +15 -10
- 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/useLintModal.ts +2 -1
- package/src/hooks/usePersistentEditHistory.test.ts +277 -1
- package/src/hooks/usePersistentEditHistory.ts +47 -8
- 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/usePreviewPersistence.history.test.tsx +14 -3
- package/src/hooks/usePreviewPersistence.ts +13 -11
- package/src/hooks/useProjectFileWriter.ts +20 -19
- package/src/hooks/useRemoveBackground.ts +2 -1
- package/src/hooks/useRenderClipContent.test.ts +18 -0
- package/src/hooks/useRenderClipContent.ts +4 -5
- package/src/hooks/useSdkSession.ts +2 -1
- package/src/hooks/useServerConnection.ts +4 -3
- package/src/hooks/useStudioSelectionPublisher.ts +2 -1
- package/src/hooks/useThumbnailStill.test.ts +100 -0
- package/src/hooks/useThumbnailStill.ts +37 -0
- package/src/hooks/useTimelineDeleteOps.test.tsx +30 -22
- package/src/hooks/useTimelineDeleteOps.ts +22 -27
- package/src/hooks/useTimelineEditing.test.tsx +1 -1
- package/src/index.ts +4 -0
- package/src/player/components/AudioWaveform.tsx +3 -2
- 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/CompositionThumbnail.tsx +2 -1
- 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/clipMenuNormalize.ts +2 -1
- package/src/player/components/clipMenuToolItems.test.tsx +23 -0
- package/src/player/components/clipMenuToolItems.tsx +13 -9
- package/src/player/components/clipPeakMap.ts +2 -1
- 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/hooks/useTimelinePlayer.ts +5 -1
- package/src/player/lib/thumbnailScheduler.test.ts +16 -0
- package/src/player/lib/thumbnailScheduler.ts +10 -2
- 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/sceneSwap.ts +2 -1
- package/src/player/store/timelineElement.ts +12 -0
- package/src/styles/theme.css +3 -0
- package/src/telemetry/client.ts +2 -1
- package/src/utils/blockInstaller.ts +2 -1
- package/src/utils/elementGsap.test.ts +9 -0
- package/src/utils/elementGsap.ts +7 -0
- package/src/utils/gsapSoftReload.ts +2 -3
- package/src/utils/gsapUndoRestore.browser.test.ts +51 -45
- package/src/utils/gsapUndoRestore.ts +3 -6
- 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/previewPrefetch.ts +2 -1
- package/src/utils/razorSplitTransaction.ts +12 -8
- package/src/utils/softReloadTargets.ts +1 -3
- package/src/utils/studioApiFetch.test.ts +75 -0
- package/src/utils/studioApiFetch.ts +13 -0
- package/src/utils/studioFileHistory.ts +4 -1
- package/src/utils/studioHelpers.ts +7 -1
- package/src/utils/studioPendingEdits.test.ts +23 -6
- package/src/utils/studioPendingEdits.ts +32 -8
- package/src/utils/studioTelemetry.test.ts +107 -0
- package/src/utils/studioTelemetry.ts +51 -5
- package/src/utils/timelineCompositionInsert.ts +3 -2
- package/src/webmcp/StudioAgentTools.tsx +4 -3
- 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-OL7GLSS4.js.map} +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
2
|
-
import { PROPERTY_GROUPS } from "@hyperframes/parsers/gsap-constants";
|
|
3
2
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
4
3
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
5
|
-
import { resolveTweenStart
|
|
4
|
+
import { resolveTweenStart } from "../utils/globalTimeCompiler";
|
|
6
5
|
import { KEYFRAME_PCT_MATCH, resolveEditableTweenDuration } from "./gsapShared";
|
|
7
6
|
import { roundTo3 } from "../utils/rounding";
|
|
8
7
|
import { computeDraggedGsapPosition } from "./draggedGsapPosition";
|
|
@@ -10,8 +9,9 @@ import {
|
|
|
10
9
|
type GsapDragCommitCallbacks,
|
|
11
10
|
computeCurrentPercentage,
|
|
12
11
|
parkPlayheadOnKeyframe,
|
|
13
|
-
materializeIfDynamic,
|
|
14
12
|
} from "./gsapDragCommit";
|
|
13
|
+
import type { GsapEditOutcome } from "./gsapEditOutcome";
|
|
14
|
+
import { commitValueAtPlayhead, planValueEdit } from "./gsapValueAtPlayhead";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* The tween's keyframes with one inserted at `percentage`. Any existing keyframe
|
|
@@ -38,232 +38,26 @@ export function buildTemporalArcKeyframes(
|
|
|
38
38
|
].sort((a, b) => a.percentage - b.percentage);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
/** commitGsapPositionFromDrag's refusal, decided without writing. */
|
|
42
|
+
export function gsapPositionFromDragOutcome(
|
|
42
43
|
selection: DomEditSelection,
|
|
43
44
|
anim: GsapAnimation,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const newStart = Math.min(targetTime, tweenStart);
|
|
54
|
-
const newEnd = Math.max(targetTime, tweenEnd);
|
|
55
|
-
const newDuration = Math.max(0.01, newEnd - newStart);
|
|
56
|
-
const existingKfs = anim.keyframes?.keyframes ?? [];
|
|
57
|
-
const remappedKfs: Array<{ percentage: number; properties: Record<string, number | string> }> =
|
|
58
|
-
[];
|
|
59
|
-
for (const kf of existingKfs) {
|
|
60
|
-
const absTime = tweenStart + (kf.percentage / 100) * tweenDuration;
|
|
61
|
-
const newPct = Math.round(((absTime - newStart) / newDuration) * 1000) / 10;
|
|
62
|
-
const props: Record<string, number | string> = { ...kf.properties };
|
|
63
|
-
for (const k of Object.keys(properties)) {
|
|
64
|
-
if (!(k in props) && backfillDefaults?.[k] != null) props[k] = backfillDefaults[k];
|
|
65
|
-
}
|
|
66
|
-
remappedKfs.push({ percentage: newPct, properties: props });
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const targetPct = Math.round(((targetTime - newStart) / newDuration) * 1000) / 10;
|
|
70
|
-
remappedKfs.push({ percentage: targetPct, properties });
|
|
71
|
-
|
|
72
|
-
remappedKfs.sort((a, b) => a.percentage - b.percentage);
|
|
73
|
-
|
|
74
|
-
await callbacks.commitMutation(
|
|
75
|
-
selection,
|
|
76
|
-
{
|
|
77
|
-
type: "replace-with-keyframes",
|
|
78
|
-
animationId: anim.id,
|
|
79
|
-
targetSelector: anim.targetSelector,
|
|
80
|
-
position: roundTo3(newStart),
|
|
81
|
-
duration: roundTo3(newDuration),
|
|
82
|
-
keyframes: remappedKfs,
|
|
83
|
-
},
|
|
84
|
-
{ label: `Move layer (extended keyframe)`, softReload: true, beforeReload },
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// fallow-ignore-next-line complexity
|
|
89
|
-
async function commitKeyframedPosition(
|
|
90
|
-
selection: DomEditSelection,
|
|
91
|
-
anim: GsapAnimation,
|
|
92
|
-
properties: Record<string, number>,
|
|
93
|
-
callbacks: GsapDragCommitCallbacks,
|
|
94
|
-
beforeReload?: () => void,
|
|
95
|
-
backfillDefaults?: Record<string, number>,
|
|
96
|
-
): Promise<void> {
|
|
97
|
-
const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
|
|
98
|
-
const computedPct = computeCurrentPercentage(selection, anim);
|
|
99
|
-
const pct = activeKeyframePct ?? computedPct;
|
|
100
|
-
await callbacks.commitMutation(
|
|
101
|
-
selection,
|
|
102
|
-
{
|
|
103
|
-
type: "add-keyframe",
|
|
104
|
-
animationId: anim.id,
|
|
105
|
-
percentage: pct,
|
|
106
|
-
properties,
|
|
107
|
-
...(backfillDefaults ? { backfillDefaults } : {}),
|
|
108
|
-
},
|
|
109
|
-
{ label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload },
|
|
110
|
-
);
|
|
111
|
-
if (activeKeyframePct != null) {
|
|
112
|
-
setActiveKeyframePct(null);
|
|
113
|
-
parkPlayheadOnKeyframe(anim, pct);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
interface DragRuntimeGsap {
|
|
118
|
-
getProperty: (target: Element, key: string) => unknown;
|
|
119
|
-
set: (target: Element, vars: Record<string, unknown>) => void;
|
|
120
|
-
}
|
|
121
|
-
interface DragRuntimeTimeline {
|
|
122
|
-
seek: (time: number) => void;
|
|
123
|
-
}
|
|
124
|
-
interface DragRuntime {
|
|
125
|
-
gsapLib: DragRuntimeGsap;
|
|
126
|
-
el: Element;
|
|
127
|
-
mainTl: DragRuntimeTimeline;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function resolveDragRuntime(
|
|
131
|
-
iframe: HTMLIFrameElement | null | undefined,
|
|
132
|
-
selector: string | undefined,
|
|
133
|
-
): DragRuntime | null {
|
|
134
|
-
if (!iframe || !selector) return null;
|
|
135
|
-
const win = iframe.contentWindow as
|
|
136
|
-
| (Window & {
|
|
137
|
-
gsap?: Partial<DragRuntimeGsap>;
|
|
138
|
-
__timelines?: Record<string, Partial<DragRuntimeTimeline>>;
|
|
139
|
-
})
|
|
140
|
-
| null;
|
|
141
|
-
const gsap = win?.gsap;
|
|
142
|
-
if (typeof gsap?.getProperty !== "function" || typeof gsap.set !== "function") return null;
|
|
143
|
-
let el: Element | null = null;
|
|
144
|
-
try {
|
|
145
|
-
el = iframe.contentDocument?.querySelector(selector) ?? null;
|
|
146
|
-
} catch {
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
if (!el) return null;
|
|
150
|
-
const timelines = win?.__timelines;
|
|
151
|
-
const mainTl = timelines ? Object.values(timelines)[0] : undefined;
|
|
152
|
-
if (typeof mainTl?.seek !== "function") return null;
|
|
153
|
-
return {
|
|
154
|
-
gsapLib: gsap as DragRuntimeGsap,
|
|
155
|
-
el,
|
|
156
|
-
mainTl: mainTl as DragRuntimeTimeline,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// fallow-ignore-next-line complexity
|
|
161
|
-
async function commitFlatViaKeyframes(
|
|
162
|
-
selection: DomEditSelection,
|
|
163
|
-
anim: GsapAnimation,
|
|
164
|
-
properties: Record<string, number>,
|
|
165
|
-
callbacks: GsapDragCommitCallbacks,
|
|
166
|
-
beforeReload?: () => void,
|
|
167
|
-
iframe?: HTMLIFrameElement | null,
|
|
168
|
-
selector?: string,
|
|
169
|
-
backfillDefaults?: Record<string, number>,
|
|
170
|
-
): Promise<void> {
|
|
171
|
-
const ct = usePlayerStore.getState().currentTime;
|
|
172
|
-
const ts = resolveTweenStart(anim);
|
|
173
|
-
const td = resolveEditableTweenDuration(anim, selection);
|
|
174
|
-
const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
|
|
175
|
-
const outsideRange =
|
|
176
|
-
activeKeyframePct == null && ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
|
|
177
|
-
|
|
178
|
-
const resolvedFromValues: Record<string, number | string> = {};
|
|
179
|
-
const runtime = resolveDragRuntime(iframe, selector);
|
|
180
|
-
if (runtime && ts !== null) {
|
|
181
|
-
const { gsapLib, el, mainTl } = runtime;
|
|
182
|
-
const draggedValues: Record<string, number> = {};
|
|
183
|
-
const channelsTheClearWipes = new Set([
|
|
184
|
-
...Object.keys(properties),
|
|
185
|
-
...PROPERTY_GROUPS.position,
|
|
186
|
-
]);
|
|
187
|
-
for (const key of channelsTheClearWipes) {
|
|
188
|
-
const v = Number(gsapLib.getProperty(el, key));
|
|
189
|
-
if (Number.isFinite(v)) draggedValues[key] = v;
|
|
190
|
-
}
|
|
191
|
-
try {
|
|
192
|
-
gsapLib.set(el, { clearProps: Object.keys(properties).join(",") });
|
|
193
|
-
mainTl.seek(ts);
|
|
194
|
-
for (const key of Object.keys(properties)) {
|
|
195
|
-
const v = Number(gsapLib.getProperty(el, key));
|
|
196
|
-
if (Number.isFinite(v)) resolvedFromValues[key] = roundTo3(v);
|
|
197
|
-
}
|
|
198
|
-
mainTl.seek(ct);
|
|
199
|
-
} catch {
|
|
200
|
-
for (const key of Object.keys(resolvedFromValues)) delete resolvedFromValues[key];
|
|
201
|
-
} finally {
|
|
202
|
-
if (Object.keys(draggedValues).length > 0) gsapLib.set(el, draggedValues);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (outsideRange && ts !== null) {
|
|
207
|
-
const coalesceKey = `gsap:convert-drag:${anim.id}`;
|
|
208
|
-
await callbacks.commitMutation(
|
|
209
|
-
selection,
|
|
210
|
-
{
|
|
211
|
-
type: "convert-to-keyframes",
|
|
212
|
-
animationId: anim.id,
|
|
213
|
-
...(Object.keys(resolvedFromValues).length > 0 ? { resolvedFromValues } : {}),
|
|
214
|
-
},
|
|
215
|
-
{ label: "Convert to keyframes for drag", skipReload: true, coalesceKey },
|
|
216
|
-
);
|
|
217
|
-
const fresh = callbacks.fetchAnimations ? await callbacks.fetchAnimations() : [];
|
|
218
|
-
// By id first: a target with several tweens (two `to`s on the same selector)
|
|
219
|
-
// matches the selector lookup on whichever one happens to be first, and the
|
|
220
|
-
// extend-and-add below would then rewrite a tween the drag never touched.
|
|
221
|
-
const converted =
|
|
222
|
-
fresh.find((a) => a.id === anim.id && a.keyframes) ??
|
|
223
|
-
fresh.find((a) => a.targetSelector === anim.targetSelector && a.keyframes) ??
|
|
224
|
-
anim;
|
|
225
|
-
const convertedStart = resolveTweenStart(converted) ?? ts;
|
|
226
|
-
const convertedDur = resolveTweenDuration(converted) || td;
|
|
227
|
-
await extendTweenAndAddKeyframe(
|
|
228
|
-
selection,
|
|
229
|
-
converted,
|
|
230
|
-
properties,
|
|
231
|
-
ct,
|
|
232
|
-
convertedStart,
|
|
233
|
-
convertedDur,
|
|
234
|
-
callbacks,
|
|
235
|
-
beforeReload,
|
|
236
|
-
);
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const coalesceKey = `gsap:convert-drag:${anim.id}`;
|
|
241
|
-
// fallow-ignore-next-line code-duplication
|
|
242
|
-
await callbacks.commitMutation(
|
|
243
|
-
selection,
|
|
244
|
-
{
|
|
245
|
-
type: "convert-to-keyframes",
|
|
246
|
-
animationId: anim.id,
|
|
247
|
-
...(Object.keys(resolvedFromValues).length > 0 ? { resolvedFromValues } : {}),
|
|
248
|
-
},
|
|
249
|
-
{ label: "Convert to keyframes for drag", skipReload: true, coalesceKey },
|
|
250
|
-
);
|
|
251
|
-
const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
|
|
252
|
-
const editedSelected = activeKeyframePct != null;
|
|
253
|
-
if (editedSelected) setActiveKeyframePct(null);
|
|
254
|
-
|
|
255
|
-
await callbacks.commitMutation(
|
|
256
|
-
selection,
|
|
257
|
-
{
|
|
258
|
-
type: "add-keyframe",
|
|
259
|
-
animationId: anim.id,
|
|
260
|
-
percentage: pct,
|
|
261
|
-
properties,
|
|
262
|
-
...(backfillDefaults ? { backfillDefaults } : {}),
|
|
263
|
-
},
|
|
264
|
-
{ label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload, coalesceKey },
|
|
45
|
+
studioOffset: { x: number; y: number },
|
|
46
|
+
gsapPos: { x: number; y: number },
|
|
47
|
+
iframe: HTMLIFrameElement | null,
|
|
48
|
+
): GsapEditOutcome {
|
|
49
|
+
if (anim.arcPath?.enabled) return { status: "persisted" };
|
|
50
|
+
const { newX, newY, baseGsapX, baseGsapY } = computeDraggedGsapPosition(
|
|
51
|
+
selection.element,
|
|
52
|
+
studioOffset,
|
|
53
|
+
gsapPos,
|
|
265
54
|
);
|
|
266
|
-
|
|
55
|
+
const plan = planValueEdit(selection, anim, { x: newX, y: newY }, iframe, {
|
|
56
|
+
backfill: { x: baseGsapX, y: baseGsapY },
|
|
57
|
+
});
|
|
58
|
+
return plan.ok
|
|
59
|
+
? { status: "persisted" }
|
|
60
|
+
: { status: "blocked", reason: "keyframes-uneditable", detail: plan.reason };
|
|
267
61
|
}
|
|
268
62
|
|
|
269
63
|
// fallow-ignore-next-line code-duplication
|
|
@@ -274,9 +68,8 @@ export async function commitGsapPositionFromDrag(
|
|
|
274
68
|
studioOffset: { x: number; y: number },
|
|
275
69
|
gsapPos: { x: number; y: number },
|
|
276
70
|
iframe: HTMLIFrameElement | null,
|
|
277
|
-
selector: string,
|
|
278
71
|
callbacks: GsapDragCommitCallbacks,
|
|
279
|
-
): Promise<
|
|
72
|
+
): Promise<GsapEditOutcome> {
|
|
280
73
|
const el = selection.element;
|
|
281
74
|
// fallow-ignore-next-line code-duplication
|
|
282
75
|
const { newX, newY, baseGsapX, baseGsapY } = computeDraggedGsapPosition(
|
|
@@ -293,8 +86,6 @@ export async function commitGsapPositionFromDrag(
|
|
|
293
86
|
el.removeAttribute("data-hf-drag-initial-offset-y");
|
|
294
87
|
};
|
|
295
88
|
|
|
296
|
-
const backfillDefaults: Record<string, number> = { x: baseGsapX, y: baseGsapY };
|
|
297
|
-
const ct = usePlayerStore.getState().currentTime;
|
|
298
89
|
if (anim.arcPath?.enabled) {
|
|
299
90
|
const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
|
|
300
91
|
const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
|
|
@@ -319,14 +110,15 @@ export async function commitGsapPositionFromDrag(
|
|
|
319
110
|
);
|
|
320
111
|
setActiveKeyframePct(null);
|
|
321
112
|
parkPlayheadOnKeyframe(anim, pct);
|
|
322
|
-
return;
|
|
113
|
+
return { status: "persisted" };
|
|
323
114
|
}
|
|
324
115
|
|
|
325
116
|
const tweenStart = resolveTweenStart(anim);
|
|
326
117
|
// Clip-wide, same as applyArcKeyframeAtPlayhead: authoring GSAP's 0.5s
|
|
327
118
|
// default here would collapse a duration-less arc's window on drag.
|
|
328
119
|
const tweenDuration = resolveEditableTweenDuration(anim, selection);
|
|
329
|
-
if (tweenStart === null || tweenDuration <= 0 || keyframes.length < 2)
|
|
120
|
+
if (tweenStart === null || tweenDuration <= 0 || keyframes.length < 2)
|
|
121
|
+
return { status: "persisted" };
|
|
330
122
|
const temporalKeyframes = buildTemporalArcKeyframes(anim, pct, { x: newX, y: newY });
|
|
331
123
|
await callbacks.commitMutation(
|
|
332
124
|
selection,
|
|
@@ -339,158 +131,18 @@ export async function commitGsapPositionFromDrag(
|
|
|
339
131
|
keyframes: temporalKeyframes,
|
|
340
132
|
ease: "none",
|
|
341
133
|
},
|
|
342
|
-
{
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const effectiveAnim = newId ? { ...anim, id: newId } : anim;
|
|
349
|
-
const dragProps: Record<string, number> = { x: newX, y: newY };
|
|
350
|
-
|
|
351
|
-
const ts = resolveTweenStart(effectiveAnim);
|
|
352
|
-
const td = resolveEditableTweenDuration(effectiveAnim, selection);
|
|
353
|
-
const outsideRange = ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
|
|
354
|
-
const hasSelectedKeyframe = usePlayerStore.getState().activeKeyframePct != null;
|
|
355
|
-
if (outsideRange && !hasSelectedKeyframe) {
|
|
356
|
-
await extendTweenAndAddKeyframe(
|
|
357
|
-
selection,
|
|
358
|
-
effectiveAnim,
|
|
359
|
-
dragProps,
|
|
360
|
-
ct,
|
|
361
|
-
ts,
|
|
362
|
-
td,
|
|
363
|
-
callbacks,
|
|
364
|
-
restoreOffset,
|
|
365
|
-
backfillDefaults,
|
|
366
|
-
);
|
|
367
|
-
} else {
|
|
368
|
-
await commitKeyframedPosition(
|
|
369
|
-
selection,
|
|
370
|
-
effectiveAnim,
|
|
371
|
-
dragProps,
|
|
372
|
-
callbacks,
|
|
373
|
-
restoreOffset,
|
|
374
|
-
backfillDefaults,
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
} else if (anim.method === "from" || anim.method === "fromTo") {
|
|
378
|
-
const ct = usePlayerStore.getState().currentTime;
|
|
379
|
-
const ts = resolveTweenStart(anim);
|
|
380
|
-
const td = resolveEditableTweenDuration(anim, selection);
|
|
381
|
-
const hasSelectedKeyframe = usePlayerStore.getState().activeKeyframePct != null;
|
|
382
|
-
const outsideRange =
|
|
383
|
-
!hasSelectedKeyframe && ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
|
|
384
|
-
const dragProps: Record<string, number> = { x: newX, y: newY };
|
|
385
|
-
|
|
386
|
-
if (outsideRange && ts !== null) {
|
|
387
|
-
await callbacks.commitMutation(
|
|
388
|
-
selection,
|
|
389
|
-
{ type: "split-into-property-groups", animationId: anim.id },
|
|
390
|
-
{ label: "Split from() for drag", skipReload: true },
|
|
391
|
-
);
|
|
392
|
-
|
|
393
|
-
const allAnims = callbacks.fetchAnimations ? await callbacks.fetchAnimations() : [];
|
|
394
|
-
const existingPosAnim = allAnims.find(
|
|
395
|
-
(a) => a.propertyGroup === "position" && a.targetSelector === anim.targetSelector,
|
|
396
|
-
);
|
|
397
|
-
|
|
398
|
-
if (existingPosAnim?.keyframes) {
|
|
399
|
-
const posTs = resolveTweenStart(existingPosAnim);
|
|
400
|
-
const posTd = resolveEditableTweenDuration(existingPosAnim, selection);
|
|
401
|
-
if (posTs !== null) {
|
|
402
|
-
await extendTweenAndAddKeyframe(
|
|
403
|
-
selection,
|
|
404
|
-
existingPosAnim,
|
|
405
|
-
{ x: newX, y: newY },
|
|
406
|
-
ct,
|
|
407
|
-
posTs,
|
|
408
|
-
posTd,
|
|
409
|
-
callbacks,
|
|
410
|
-
restoreOffset,
|
|
411
|
-
backfillDefaults,
|
|
412
|
-
);
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const newStart = Math.min(ct, ts);
|
|
418
|
-
const newEnd = Math.max(ct, ts + td);
|
|
419
|
-
const newDuration = Math.max(0.01, newEnd - newStart);
|
|
420
|
-
const dragBefore = ct < ts;
|
|
421
|
-
const origStartPct = Math.round(((ts - newStart) / newDuration) * 1000) / 10;
|
|
422
|
-
const origEndPct = Math.round(((ts + td - newStart) / newDuration) * 1000) / 10;
|
|
423
|
-
|
|
424
|
-
const keyframes: Array<{ percentage: number; properties: Record<string, number | string> }> =
|
|
425
|
-
[];
|
|
426
|
-
if (dragBefore) {
|
|
427
|
-
keyframes.push({ percentage: 0, properties: { x: newX, y: newY } });
|
|
428
|
-
if (origStartPct > 0.5 && origStartPct < 99.5) {
|
|
429
|
-
keyframes.push({ percentage: origStartPct, properties: { x: 0, y: 0 } });
|
|
430
|
-
}
|
|
431
|
-
keyframes.push({ percentage: 100, properties: { x: 0, y: 0 } });
|
|
432
|
-
} else {
|
|
433
|
-
keyframes.push({ percentage: 0, properties: { x: 0, y: 0 } });
|
|
434
|
-
if (origEndPct > 0.5 && origEndPct < 99.5) {
|
|
435
|
-
keyframes.push({ percentage: origEndPct, properties: { x: 0, y: 0 } });
|
|
436
|
-
}
|
|
437
|
-
keyframes.push({ percentage: 100, properties: { x: newX, y: newY } });
|
|
438
|
-
}
|
|
439
|
-
keyframes.sort((a, b) => a.percentage - b.percentage);
|
|
440
|
-
|
|
441
|
-
const baseKf = {
|
|
442
|
-
targetSelector: anim.targetSelector,
|
|
443
|
-
position: roundTo3(newStart),
|
|
444
|
-
duration: roundTo3(newDuration),
|
|
445
|
-
keyframes,
|
|
446
|
-
};
|
|
447
|
-
await callbacks.commitMutation(
|
|
448
|
-
selection,
|
|
449
|
-
existingPosAnim
|
|
450
|
-
? { type: "replace-with-keyframes", animationId: existingPosAnim.id, ...baseKf }
|
|
451
|
-
: { type: "add-with-keyframes", ...baseKf },
|
|
452
|
-
{ label: "Move layer (from extended)", softReload: true, beforeReload: restoreOffset },
|
|
453
|
-
);
|
|
454
|
-
} else {
|
|
455
|
-
const coalesceKey = `gsap:convert-drag:${anim.id}`;
|
|
456
|
-
await callbacks.commitMutation(
|
|
457
|
-
selection,
|
|
458
|
-
{
|
|
459
|
-
type: "convert-to-keyframes",
|
|
460
|
-
animationId: anim.id,
|
|
461
|
-
},
|
|
462
|
-
{ label: "Convert from() for drag", skipReload: true, coalesceKey },
|
|
463
|
-
);
|
|
464
|
-
const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
|
|
465
|
-
const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
|
|
466
|
-
if (activeKeyframePct != null) setActiveKeyframePct(null);
|
|
467
|
-
await callbacks.commitMutation(
|
|
468
|
-
selection,
|
|
469
|
-
{
|
|
470
|
-
type: "add-keyframe",
|
|
471
|
-
animationId: anim.id,
|
|
472
|
-
percentage: pct,
|
|
473
|
-
properties: dragProps,
|
|
474
|
-
...(backfillDefaults ? { backfillDefaults } : {}),
|
|
475
|
-
},
|
|
476
|
-
{
|
|
477
|
-
label: `Move layer (keyframe ${pct}%)`,
|
|
478
|
-
softReload: true,
|
|
479
|
-
beforeReload: restoreOffset,
|
|
480
|
-
coalesceKey,
|
|
481
|
-
},
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
} else {
|
|
485
|
-
await commitFlatViaKeyframes(
|
|
486
|
-
selection,
|
|
487
|
-
anim,
|
|
488
|
-
{ x: newX, y: newY },
|
|
489
|
-
callbacks,
|
|
490
|
-
restoreOffset,
|
|
491
|
-
iframe,
|
|
492
|
-
selector,
|
|
493
|
-
backfillDefaults,
|
|
134
|
+
{
|
|
135
|
+
label: "Move layer (new keyframe)",
|
|
136
|
+
keyframeAction: "add",
|
|
137
|
+
softReload: true,
|
|
138
|
+
beforeReload: restoreOffset,
|
|
139
|
+
},
|
|
494
140
|
);
|
|
141
|
+
return { status: "persisted" };
|
|
495
142
|
}
|
|
143
|
+
return commitValueAtPlayhead(selection, anim, { x: newX, y: newY }, iframe, callbacks, {
|
|
144
|
+
label: "Move layer",
|
|
145
|
+
backfill: { x: baseGsapX, y: baseGsapY },
|
|
146
|
+
beforeReload: restoreOffset,
|
|
147
|
+
});
|
|
496
148
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { editabilityForProvenance, type GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
2
2
|
|
|
3
|
-
export type GsapEditBlockReason =
|
|
3
|
+
export type GsapEditBlockReason =
|
|
4
|
+
| "no-selector"
|
|
5
|
+
| "unroll-required"
|
|
6
|
+
| "source-uneditable"
|
|
7
|
+
| "keyframes-uneditable"
|
|
8
|
+
| "mixed-files";
|
|
4
9
|
|
|
5
10
|
/**
|
|
6
11
|
* Which of the nine situations produced a block. The user-facing `reason` stays
|
|
@@ -21,7 +26,18 @@ export type GsapEditBlockDetail =
|
|
|
21
26
|
| "no-position-tween"
|
|
22
27
|
| "live-rotation-no-source-tween"
|
|
23
28
|
| "live-resize-no-source-tween"
|
|
24
|
-
| "zero-duration-tween"
|
|
29
|
+
| "zero-duration-tween"
|
|
30
|
+
| PlayheadEditRefusal;
|
|
31
|
+
|
|
32
|
+
/** Why an edit at the playhead could not be written as keyframes of the file's tween. */
|
|
33
|
+
export type PlayheadEditRefusal =
|
|
34
|
+
| "eased-keyframes"
|
|
35
|
+
| "simple-array-keyframes"
|
|
36
|
+
| "unknown-ease"
|
|
37
|
+
| "implicit-end-unknown"
|
|
38
|
+
| "not-a-tween"
|
|
39
|
+
| "no-timing"
|
|
40
|
+
| "shared-tween";
|
|
25
41
|
|
|
26
42
|
export type GsapEditOutcome =
|
|
27
43
|
| {
|
|
@@ -52,6 +68,10 @@ export const GSAP_EDIT_BLOCK_COPY: Record<GsapEditBlockReason, string> = {
|
|
|
52
68
|
"unroll-required":
|
|
53
69
|
"This motion comes from a helper or loop. Choose Unroll to edit it explicitly.",
|
|
54
70
|
"source-uneditable": "This animation is computed at runtime. Edit the animation in the Code tab.",
|
|
71
|
+
"keyframes-uneditable":
|
|
72
|
+
"Studio can't add this edit to the animation's keyframes. Edit this animation in the Code tab.",
|
|
73
|
+
"mixed-files":
|
|
74
|
+
"These layers are animated in different files. Move each file's layers separately.",
|
|
55
75
|
};
|
|
56
76
|
|
|
57
77
|
export class GsapEditBlockedError extends Error {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { trackKeyframeCommit } from "../utils/keyframeUsage";
|
|
3
|
+
import { observeGsapGesture } from "./gsapGestureOutcome";
|
|
4
|
+
import type { CommitMutation, MutationResult } from "./gsapScriptCommitTypes";
|
|
5
|
+
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
6
|
+
|
|
7
|
+
vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
8
|
+
beforeEach(() => vi.clearAllMocks());
|
|
9
|
+
const selection = {} as Parameters<CommitMutation>[0];
|
|
10
|
+
const mutation = {
|
|
11
|
+
type: "add-keyframe",
|
|
12
|
+
animationId: "private-target",
|
|
13
|
+
properties: { text: "private-content" },
|
|
14
|
+
};
|
|
15
|
+
function writer(result?: MutationResult): CommitMutation {
|
|
16
|
+
const commit: CommitMutation = async (_selection, _mutation, options) => {
|
|
17
|
+
if (result) options.onResult?.(result);
|
|
18
|
+
};
|
|
19
|
+
commit.batch = async (_calls, options) => {
|
|
20
|
+
if (result) options.onResult?.(result);
|
|
21
|
+
};
|
|
22
|
+
return commit;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("GSAP gesture usage", () => {
|
|
26
|
+
it("counts a multi-write add gesture once from successful writer results", async () => {
|
|
27
|
+
const observed = observeGsapGesture(
|
|
28
|
+
writer({ ok: true, changed: true, mutationChanges: [true] }),
|
|
29
|
+
);
|
|
30
|
+
const onResult = vi.fn();
|
|
31
|
+
await observed.commit!(
|
|
32
|
+
selection,
|
|
33
|
+
{ type: "convert-to-keyframes" },
|
|
34
|
+
{ label: "Convert", onResult },
|
|
35
|
+
);
|
|
36
|
+
await observed.commit!(selection, mutation, { label: "Add" });
|
|
37
|
+
expect(trackStudioEvent).not.toHaveBeenCalled();
|
|
38
|
+
expect(observed.finish()).toEqual({ ok: true, changed: true });
|
|
39
|
+
expect(onResult).toHaveBeenCalledWith({ ok: true, changed: true, mutationChanges: [true] });
|
|
40
|
+
expect(trackStudioEvent).toHaveBeenCalledExactlyOnceWith("keyframe", { action: "add" });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("counts a batched insertion once", async () => {
|
|
44
|
+
const observed = observeGsapGesture(
|
|
45
|
+
writer({ ok: true, changed: true, mutationChanges: [true, true] }),
|
|
46
|
+
);
|
|
47
|
+
await observed.commit!.batch!(
|
|
48
|
+
[
|
|
49
|
+
{ selection, mutation, options: { label: "Add" } },
|
|
50
|
+
{ selection, mutation, options: { label: "Add" } },
|
|
51
|
+
],
|
|
52
|
+
{ label: "Add together" },
|
|
53
|
+
);
|
|
54
|
+
expect(observed.finish().changed).toBe(true);
|
|
55
|
+
expect(trackStudioEvent).toHaveBeenCalledExactlyOnceWith("keyframe", { action: "add" });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("retains insertion metadata from an earlier member of a mixed batch", async () => {
|
|
59
|
+
const observed = observeGsapGesture(
|
|
60
|
+
writer({ ok: true, changed: true, mutationChanges: [true, true] }),
|
|
61
|
+
);
|
|
62
|
+
await observed.commit!.batch!(
|
|
63
|
+
[
|
|
64
|
+
{
|
|
65
|
+
selection,
|
|
66
|
+
mutation: { type: "replace-with-keyframes" },
|
|
67
|
+
options: { label: "Extend", keyframeAction: "add" },
|
|
68
|
+
},
|
|
69
|
+
{ selection, mutation: { type: "update-keyframe" }, options: { label: "Update" } },
|
|
70
|
+
],
|
|
71
|
+
{ label: "Update" },
|
|
72
|
+
);
|
|
73
|
+
observed.finish();
|
|
74
|
+
expect(trackStudioEvent).toHaveBeenCalledExactlyOnceWith("keyframe", { action: "add" });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("uses semantic insertion metadata for a replacement", async () => {
|
|
78
|
+
const observed = observeGsapGesture(
|
|
79
|
+
writer({ ok: true, changed: true, mutationChanges: [true] }),
|
|
80
|
+
);
|
|
81
|
+
await observed.commit!(
|
|
82
|
+
selection,
|
|
83
|
+
{ type: "replace-with-keyframes" },
|
|
84
|
+
{ label: "Replace", keyframeAction: "add" },
|
|
85
|
+
);
|
|
86
|
+
observed.finish();
|
|
87
|
+
expect(trackStudioEvent).toHaveBeenCalledExactlyOnceWith("keyframe", { action: "add" });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it.each([undefined, { ok: true }, { ok: true, changed: false }, { ok: false, changed: true }])(
|
|
91
|
+
"does not count resolution without a changed successful write (%j)",
|
|
92
|
+
async (result) => {
|
|
93
|
+
const observed = observeGsapGesture(writer(result));
|
|
94
|
+
await observed.commit!(selection, mutation, { label: "Add" });
|
|
95
|
+
expect(observed.finish()).toEqual({ ok: true, changed: false });
|
|
96
|
+
expect(trackStudioEvent).not.toHaveBeenCalled();
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
it("does not count missing writers or an unfinished group", async () => {
|
|
101
|
+
expect(observeGsapGesture(null).finish()).toEqual({ ok: true, changed: false });
|
|
102
|
+
const observed = observeGsapGesture(writer());
|
|
103
|
+
await observed.commit!(selection, mutation, { label: "Add" });
|
|
104
|
+
expect(observed.finish(true).changed).toBe(false);
|
|
105
|
+
expect(trackStudioEvent).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("reports a successful plain DOM gesture without inventing keyframes", () => {
|
|
109
|
+
expect(observeGsapGesture(null).finish(true).changed).toBe(true);
|
|
110
|
+
expect(trackStudioEvent).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it.each([
|
|
115
|
+
[[false, true], 0],
|
|
116
|
+
[[true, false], 1],
|
|
117
|
+
[undefined, 0],
|
|
118
|
+
] as const)(
|
|
119
|
+
"attributes mixed batches only to changed members (%j)",
|
|
120
|
+
async (mutationChanges, count) => {
|
|
121
|
+
const result = {
|
|
122
|
+
ok: true,
|
|
123
|
+
changed: true,
|
|
124
|
+
mutationChanges: mutationChanges && [...mutationChanges],
|
|
125
|
+
};
|
|
126
|
+
const observed = observeGsapGesture(writer(result));
|
|
127
|
+
const mutations = [mutation, { type: "update-property" }];
|
|
128
|
+
await observed.commit!.batch!(
|
|
129
|
+
mutations.map((mutation) => ({ selection, mutation, options: { label: "Edit" } })),
|
|
130
|
+
{ label: "Edit" },
|
|
131
|
+
);
|
|
132
|
+
expect(observed.finish()).toEqual({ ok: true, changed: true });
|
|
133
|
+
expect(trackStudioEvent).toHaveBeenCalledTimes(count);
|
|
134
|
+
if (count) expect(trackStudioEvent).toHaveBeenCalledWith("keyframe", { action: "add" });
|
|
135
|
+
vi.mocked(trackStudioEvent).mockClear();
|
|
136
|
+
trackKeyframeCommit(mutations, result, { label: "Edit" });
|
|
137
|
+
expect(trackStudioEvent).toHaveBeenCalledTimes(count);
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
it("excludes a suppressed preparation from mixed-batch attribution", async () => {
|
|
142
|
+
const result = { ok: true, changed: true, mutationChanges: [true, false] };
|
|
143
|
+
const calls = [
|
|
144
|
+
{
|
|
145
|
+
selection,
|
|
146
|
+
mutation: { type: "convert-to-keyframes" },
|
|
147
|
+
options: { label: "Prepare", keyframeTelemetry: false },
|
|
148
|
+
},
|
|
149
|
+
{ selection, mutation, options: { label: "Add" } },
|
|
150
|
+
];
|
|
151
|
+
const observed = observeGsapGesture(writer(result));
|
|
152
|
+
await observed.commit!.batch!(calls, { label: "Add" });
|
|
153
|
+
expect(observed.finish()).toEqual({ ok: true, changed: true });
|
|
154
|
+
trackKeyframeCommit(
|
|
155
|
+
calls.map((call) => call.mutation),
|
|
156
|
+
result,
|
|
157
|
+
{ label: "Add" },
|
|
158
|
+
calls.map((call) => call.options),
|
|
159
|
+
);
|
|
160
|
+
expect(trackStudioEvent).not.toHaveBeenCalled();
|
|
161
|
+
});
|