@hyperframes/studio 0.8.114 → 0.8.115
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-aXNJKiIg.js → hyperframes-player-C-9N8nk0.js} +1 -1
- package/dist/assets/{index-Bn2RJNTJ.js → index-BoUM6Xqb.js} +1 -1
- package/dist/assets/{index-Dz7doz9l.js → index-CAvZ0AA-.js} +201 -201
- package/dist/assets/{index-C9N-DGjC.js → index-CVuorIMv.js} +1 -1
- package/dist/assets/{index-BYnsJnJV.css → index-t1LH6H5H.css} +1 -1
- package/dist/{chunk-K47E2ZJA.js → chunk-5EKWJZLM.js} +504 -17
- package/dist/chunk-5EKWJZLM.js.map +1 -0
- package/dist/{chunk-VWT7STY3.js → chunk-J6Q572FA.js} +2 -2
- package/dist/{chunk-BIUXP3PG.js → chunk-TXZMMOMA.js} +43 -276
- package/dist/chunk-TXZMMOMA.js.map +1 -0
- package/dist/{domEditingLayers-V67MS4WP.js → domEditingLayers-D7VYFJJD.js} +3 -5
- package/dist/index.d.ts +43 -19
- package/dist/index.html +2 -2
- package/dist/index.js +2939 -2596
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +9 -8
- package/src/App.tsx +2 -2
- package/src/components/LinkedSelectionToggle.tsx +3 -2
- package/src/components/dock/Dock.tsx +5 -7
- package/src/components/editor/CanvasContextMenu.tsx +3 -2
- package/src/components/editor/DomEditCropHandles.test.tsx +27 -0
- package/src/components/editor/DomEditCropHandles.tsx +7 -4
- package/src/components/editor/DomEditOverlay.tsx +5 -1
- package/src/components/editor/FileTreeNodes.tsx +13 -10
- package/src/components/editor/MotionPathOverlay.tsx +7 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +12 -2
- package/src/components/editor/SnapToolbar.test.tsx +100 -1
- package/src/components/editor/SnapToolbar.tsx +43 -5
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +48 -1
- package/src/components/editor/audioFxTelemetry.ts +2 -3
- package/src/components/editor/commitPreflightHandles.test.tsx +2 -2
- package/src/components/editor/cropResize.ts +4 -3
- package/src/components/editor/domEditingDom.ts +4 -0
- package/src/components/editor/domEditingLayers.ts +1 -5
- package/src/components/editor/manualEdits.ts +1 -0
- package/src/components/editor/manualEditsParsing.ts +7 -0
- package/src/components/editor/marqueeCommit.test.tsx +106 -37
- package/src/components/editor/marqueeCommit.ts +24 -8
- package/src/components/editor/motionPathCommit.test.ts +29 -1
- package/src/components/editor/motionPathCommit.ts +21 -9
- package/src/components/editor/probeSourceElement.test.ts +45 -0
- package/src/components/editor/probeSourceElement.ts +39 -15
- package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -7
- package/src/components/editor/propertyPanelFxAddMenu.tsx +11 -2
- package/src/components/editor/propertyPanelFxCarveModule.tsx +15 -5
- package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +3 -0
- package/src/components/editor/propertyPanelFxNodeParams.tsx +15 -2
- package/src/components/editor/propertyPanelFxNodeRow.tsx +3 -0
- package/src/components/editor/propertyPanelFxPresetRun.tsx +10 -2
- package/src/components/editor/propertyPanelFxSection.test.tsx +56 -0
- package/src/components/editor/propertyPanelFxSection.tsx +30 -28
- package/src/components/editor/propertyPanelTypes.ts +6 -3
- package/src/components/editor/useDomEditNudge.test.tsx +37 -0
- package/src/components/editor/useDomEditNudge.ts +2 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +5 -0
- package/src/components/editor/useDomEditZOrder.ts +12 -2
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +1 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +7 -1
- package/src/components/sidebar/AssetContextMenu.tsx +4 -4
- package/src/components/ui/menuStyle.test.ts +61 -0
- package/src/components/ui/menuStyle.ts +10 -0
- package/src/contexts/DomEditContext.tsx +1 -1
- package/src/contexts/TimelineEditContext.tsx +1 -0
- package/src/domEditExports.test.tsx +9 -1
- package/src/hooks/domEditCommitRunner.ts +1 -1
- package/src/hooks/elementOffsetStager.ts +5 -4
- package/src/hooks/gsapDragCommit.ts +2 -20
- package/src/hooks/gsapDragPositionCommit.ts +18 -3
- package/src/hooks/gsapGestureOutcome.test.ts +161 -0
- package/src/hooks/gsapGestureOutcome.ts +64 -0
- package/src/hooks/gsapKeyframeCommit.ts +3 -12
- package/src/hooks/gsapResizeIntercept.test.ts +33 -0
- package/src/hooks/gsapResizeIntercept.ts +1 -0
- package/src/hooks/gsapScriptCommitTypes.ts +5 -0
- package/src/hooks/gsapShared.writeTarget.test.ts +20 -0
- package/src/hooks/keyframeCacheAstLoad.ts +6 -5
- package/src/hooks/plainRotation.ts +3 -2
- package/src/hooks/previewPromotionSelection.test.tsx +1 -1
- package/src/hooks/refreshTimelineRowText.test.ts +65 -0
- package/src/hooks/refreshTimelineRowText.ts +29 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +46 -5
- package/src/hooks/useAnimatedPropertyCommit.ts +2 -2
- package/src/hooks/useBlockedTimelineEditToast.ts +12 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +18 -23
- package/src/hooks/useDomEditTextCommits.test.tsx +18 -0
- package/src/hooks/useDomEditTextCommits.ts +5 -1
- package/src/hooks/useDomEditWiring.ts +4 -2
- package/src/hooks/useDomGeometryCommit.test.tsx +25 -6
- package/src/hooks/useDomGeometryCommit.ts +7 -4
- package/src/hooks/useDomGeometryCommits.test.tsx +1 -0
- package/src/hooks/useDomGeometryCommits.ts +3 -2
- package/src/hooks/useDomSelection.test.ts +26 -0
- package/src/hooks/useDomSelection.ts +10 -3
- package/src/hooks/useDomSelectionTypes.ts +6 -1
- package/src/hooks/useEnableKeyframes.test.ts +2 -2
- package/src/hooks/useEnableKeyframes.ts +5 -3
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +102 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +31 -6
- package/src/hooks/useGestureCommit.test.tsx +225 -55
- package/src/hooks/useGestureCommit.ts +86 -55
- package/src/hooks/useGsapAwareEditing.test.tsx +11 -6
- package/src/hooks/useGsapAwareEditing.ts +96 -195
- package/src/hooks/useGsapAwareGroupMove.ts +179 -0
- package/src/hooks/useGsapKeyframeOps.sdkUsage.test.tsx +114 -0
- package/src/hooks/useGsapKeyframeOps.ts +36 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +57 -0
- package/src/hooks/useGsapScriptCommits.ts +3 -0
- package/src/hooks/useGsapSelectionHandlers.test.tsx +1 -1
- package/src/hooks/useGsapSelectionHandlers.ts +24 -18
- package/src/hooks/useGsapTweenCache.ts +18 -12
- package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +76 -2
- package/src/hooks/usePreviewInteraction.test.ts +21 -0
- package/src/hooks/usePreviewInteraction.ts +2 -0
- package/src/hooks/useRenderClipContent.test.ts +18 -0
- package/src/hooks/useRenderClipContent.ts +4 -5
- package/src/hooks/useTimelineDeleteOps.test.tsx +30 -22
- package/src/hooks/useTimelineDeleteOps.ts +21 -27
- package/src/hooks/useTimelineEditing.test.tsx +1 -1
- package/src/player/components/AutomationSelectionMenu.tsx +3 -2
- package/src/player/components/ClipContextMenu.test.tsx +73 -0
- package/src/player/components/ClipContextMenu.tsx +33 -31
- package/src/player/components/KeyframeDiamondContextMenu.tsx +8 -9
- package/src/player/components/OutOfSyncBadge.tsx +5 -5
- package/src/player/components/TextClipContent.test.ts +19 -0
- package/src/player/components/TextClipContent.tsx +27 -0
- package/src/player/components/TrackGapContextMenu.tsx +3 -6
- package/src/player/components/clipMenuToolItems.test.tsx +23 -0
- package/src/player/components/clipMenuToolItems.tsx +13 -9
- package/src/player/components/timelineCallbacks.ts +1 -0
- package/src/player/components/timelineClipDragGestureLifecycle.ts +1 -1
- package/src/player/components/timelineClipGestureHandlers.test.ts +94 -0
- package/src/player/components/timelineClipGestureHandlers.ts +20 -2
- package/src/player/components/timelineEditing.ts +3 -1
- package/src/player/components/timelineLinkSelection.ts +35 -1
- package/src/player/components/useTimelineClipDrag.keyboard.test.tsx +14 -0
- package/src/player/components/useTimelineClipDrag.ts +6 -7
- package/src/player/hooks/timelinePlayerSync.test.ts +6 -0
- package/src/player/hooks/timelinePlayerSync.ts +6 -1
- package/src/player/lib/timelineElementHelpers.test.ts +87 -1
- package/src/player/lib/timelineElementHelpers.ts +6 -15
- package/src/player/lib/timelineText.ts +40 -0
- package/src/player/store/timelineElement.ts +12 -0
- package/src/styles/theme.css +3 -0
- package/src/utils/keyframeUsage.ts +79 -0
- package/src/utils/previewFeatureUsage.test.ts +32 -0
- package/src/utils/previewFeatureUsage.ts +46 -0
- package/src/utils/studioTelemetry.test.ts +107 -0
- package/src/utils/studioTelemetry.ts +49 -4
- package/src/webmcp/StudioAgentTools.tsx +2 -2
- package/dist/chunk-BIUXP3PG.js.map +0 -1
- package/dist/chunk-K47E2ZJA.js.map +0 -1
- /package/dist/{chunk-VWT7STY3.js.map → chunk-J6Q572FA.js.map} +0 -0
- /package/dist/{domEditingLayers-V67MS4WP.js.map → domEditingLayers-D7VYFJJD.js.map} +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PropertyPanelProps } from "./propertyPanelTypes";
|
|
1
2
|
import { useEffect, useState } from "react";
|
|
2
3
|
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
3
4
|
import { Plus, X } from "../../icons/SystemIcons";
|
|
@@ -246,13 +247,15 @@ export function FlatTextSection({
|
|
|
246
247
|
element: DomEditSelection;
|
|
247
248
|
styles: Record<string, string>;
|
|
248
249
|
fontAssets: ImportedFontAsset[];
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
250
|
+
} & Pick<
|
|
251
|
+
PropertyPanelProps,
|
|
252
|
+
| "onImportFonts"
|
|
253
|
+
| "onSetText"
|
|
254
|
+
| "onSetTextFieldStyle"
|
|
255
|
+
| "onPreviewTextFieldStyle"
|
|
256
|
+
| "onAddTextField"
|
|
257
|
+
| "onRemoveTextField"
|
|
258
|
+
>) {
|
|
256
259
|
const track = useTrackDesignInput();
|
|
257
260
|
const [activeFieldKey, setActiveFieldKey] = useState<string | null>(
|
|
258
261
|
element.textFields[0]?.key ?? null,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
1
2
|
/**
|
|
2
3
|
* The add-effect shelf: Tone, the composite jobs, and the raw registry.
|
|
3
4
|
*
|
|
@@ -81,6 +82,7 @@ export function FxAddMenu({
|
|
|
81
82
|
withJob,
|
|
82
83
|
withEffect,
|
|
83
84
|
}: FxAddMenuProps) {
|
|
85
|
+
const trackInput = useTrackDesignInput();
|
|
84
86
|
return (
|
|
85
87
|
<div
|
|
86
88
|
className="hf-fx-add-menu space-y-1.5 rounded-[4px] border border-panel-border-input p-1.5"
|
|
@@ -108,8 +110,15 @@ export function FxAddMenu({
|
|
|
108
110
|
title="Listen to this track and even out its loud and quiet parts."
|
|
109
111
|
disabled={disabled || analysing}
|
|
110
112
|
onClick={() => {
|
|
111
|
-
if (levelled)
|
|
112
|
-
|
|
113
|
+
if (levelled) {
|
|
114
|
+
if (onRemoveLevel) {
|
|
115
|
+
onRemoveLevel();
|
|
116
|
+
trackInput("button", "remove-levelling");
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
onLevel();
|
|
120
|
+
trackInput("button", "add-levelling");
|
|
121
|
+
}
|
|
113
122
|
onClose();
|
|
114
123
|
}}
|
|
115
124
|
// The one module here that cannot answer instantly: it has to
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
1
2
|
/**
|
|
2
3
|
* The voiceover carve, as one module in the FX rack.
|
|
3
4
|
*
|
|
@@ -165,6 +166,7 @@ export function FxCarveModule({
|
|
|
165
166
|
onCarvePreview(carve: HfCarveSettings): void;
|
|
166
167
|
}) {
|
|
167
168
|
const on = carve.enabled;
|
|
169
|
+
const trackInput = useTrackDesignInput();
|
|
168
170
|
const soleVoice = soleCarveVoice(sourceOptions, carve.sources);
|
|
169
171
|
const summary = carveSummary({ nodes, carve, analysing });
|
|
170
172
|
// The carve's own colour, used three ways: the module's left edge, the title,
|
|
@@ -215,7 +217,10 @@ export function FxCarveModule({
|
|
|
215
217
|
aria-pressed={on}
|
|
216
218
|
title={on ? "Switch the carve off" : "Switch the carve on"}
|
|
217
219
|
disabled={disabled}
|
|
218
|
-
onClick={() =>
|
|
220
|
+
onClick={() => {
|
|
221
|
+
onCarveChange({ ...carve, enabled: !on });
|
|
222
|
+
trackInput("toggle", "carve-enabled");
|
|
223
|
+
}}
|
|
219
224
|
>
|
|
220
225
|
{on ? "On" : "Off"}
|
|
221
226
|
</button>
|
|
@@ -250,7 +255,10 @@ export function FxCarveModule({
|
|
|
250
255
|
value={carve.strength}
|
|
251
256
|
disabled={disabled || carve.sources.length === 0}
|
|
252
257
|
onChange={(_k, v) => onCarvePreview({ ...carve, strength: Number(v) })}
|
|
253
|
-
onCommit={(_k, v) =>
|
|
258
|
+
onCommit={(_k, v) => {
|
|
259
|
+
onCarveChange({ ...carve, strength: Number(v) });
|
|
260
|
+
trackInput("slider", "carve-strength");
|
|
261
|
+
}}
|
|
254
262
|
/>
|
|
255
263
|
</div>
|
|
256
264
|
{/* What the analysis made of all that. Divided rather than boxed: these
|
|
@@ -334,6 +342,7 @@ function CarveSourceRow({
|
|
|
334
342
|
disabled?: boolean;
|
|
335
343
|
onCarveChange(carve: HfCarveSettings): void;
|
|
336
344
|
}) {
|
|
345
|
+
const trackInput = useTrackDesignInput();
|
|
337
346
|
return (
|
|
338
347
|
<div className="hf-fx-row flex min-h-6 items-center gap-2">
|
|
339
348
|
{/* Wraps like every other name in this column (see FxParamRow) — one
|
|
@@ -366,14 +375,15 @@ function CarveSourceRow({
|
|
|
366
375
|
data-carve-source={o.id}
|
|
367
376
|
checked={carve.sources.includes(o.id)}
|
|
368
377
|
disabled={disabled}
|
|
369
|
-
onChange={(e) =>
|
|
378
|
+
onChange={(e) => {
|
|
370
379
|
onCarveChange({
|
|
371
380
|
...carve,
|
|
372
381
|
sources: e.target.checked
|
|
373
382
|
? [...carve.sources, o.id]
|
|
374
383
|
: carve.sources.filter((id) => id !== o.id),
|
|
375
|
-
})
|
|
376
|
-
|
|
384
|
+
});
|
|
385
|
+
trackInput("toggle", "carve-source");
|
|
386
|
+
}}
|
|
377
387
|
/>
|
|
378
388
|
<span className="truncate">{o.label}</span>
|
|
379
389
|
</label>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
1
2
|
/**
|
|
2
3
|
* An effect row's open face: the derived or primary one-knob control, and
|
|
3
4
|
* Details underneath it.
|
|
@@ -54,6 +55,7 @@ function FxNodeDerivedKnob({
|
|
|
54
55
|
onUpdate(index: number, patch: Partial<HfAudioFxNode>): void;
|
|
55
56
|
trackKind?: string;
|
|
56
57
|
}) {
|
|
58
|
+
const trackInput = useTrackDesignInput();
|
|
57
59
|
if (!derived) return null;
|
|
58
60
|
return (
|
|
59
61
|
<>
|
|
@@ -66,6 +68,7 @@ function FxNodeDerivedKnob({
|
|
|
66
68
|
onCommit={(_k, v) => {
|
|
67
69
|
trackProfileCommitted(node.type, Number(v), { trackKind });
|
|
68
70
|
onUpdate(index, { params: applyAudioFxProfile(node.type, Number(v), params) });
|
|
71
|
+
trackInput("slider", "profile-strength");
|
|
69
72
|
}}
|
|
70
73
|
/>
|
|
71
74
|
</div>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
1
2
|
/**
|
|
2
3
|
* An open effect's knobs, with whatever automation surface applies to them.
|
|
3
4
|
*
|
|
@@ -69,6 +70,7 @@ export function FxNodeParams({
|
|
|
69
70
|
index: number;
|
|
70
71
|
disabled: boolean;
|
|
71
72
|
}) {
|
|
73
|
+
const trackInput = useTrackDesignInput();
|
|
72
74
|
const nodeId = node.id;
|
|
73
75
|
// Lanes address a node by id; the controls know their own parameter keys. This
|
|
74
76
|
// is the one place that translation belongs.
|
|
@@ -97,14 +99,25 @@ export function FxNodeParams({
|
|
|
97
99
|
if (before[key] === value) continue;
|
|
98
100
|
if (typeof value !== "number" && typeof value !== "string") continue;
|
|
99
101
|
trackParamCommitted(node.type, key, value, "details", { trackKind });
|
|
102
|
+
trackInput(typeof value === "number" ? "slider" : "select", key);
|
|
100
103
|
}
|
|
101
104
|
onUpdate(index, { params: next });
|
|
102
105
|
}}
|
|
103
106
|
automatedKeys={automatedKeysOf(node, def.params, automatedTargets)}
|
|
104
|
-
onAutomate={
|
|
107
|
+
onAutomate={
|
|
108
|
+
nodeId && onAutomateParam
|
|
109
|
+
? (key) => {
|
|
110
|
+
onAutomateParam(nodeId, key);
|
|
111
|
+
trackInput("button", "automate-param");
|
|
112
|
+
}
|
|
113
|
+
: undefined
|
|
114
|
+
}
|
|
105
115
|
onRemoveAutomation={
|
|
106
116
|
nodeId && onRemoveParamAutomation
|
|
107
|
-
? (key) =>
|
|
117
|
+
? (key) => {
|
|
118
|
+
onRemoveParamAutomation(nodeId, key);
|
|
119
|
+
trackInput("button", "remove-param-automation");
|
|
120
|
+
}
|
|
108
121
|
: undefined
|
|
109
122
|
}
|
|
110
123
|
/>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
1
2
|
/**
|
|
2
3
|
* One effect in the FX rack: its header controls, and its knobs when open.
|
|
3
4
|
*
|
|
@@ -246,6 +247,7 @@ export function FxNodeRow({
|
|
|
246
247
|
onPreview,
|
|
247
248
|
trackKind,
|
|
248
249
|
}: FxNodeRowProps) {
|
|
250
|
+
const trackInput = useTrackDesignInput();
|
|
249
251
|
const registryDef = getAudioFxDef(node.type);
|
|
250
252
|
const def = useMemo(() => (registryDef ? plainDef(registryDef) : null), [registryDef]);
|
|
251
253
|
const primary = registryDef ? primaryParamOf(registryDef) : null;
|
|
@@ -306,6 +308,7 @@ export function FxNodeRow({
|
|
|
306
308
|
onToggleBypass={() => {
|
|
307
309
|
trackNodeBypassed(node.type, !bypassed, { trackKind });
|
|
308
310
|
onUpdate(index, { enabled: bypassed });
|
|
311
|
+
trackInput("toggle", "effect-bypass");
|
|
309
312
|
}}
|
|
310
313
|
onMove={(delta) => onMove(index, delta)}
|
|
311
314
|
onRemove={() => onRemove(index)}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
1
2
|
/**
|
|
2
3
|
* One preset's bracket in the rack: its own header, on/off switch, amount lane
|
|
3
4
|
* and the rows it wraps — or, for a run with no preset, just the rows.
|
|
@@ -93,6 +94,7 @@ export function FxPresetRun({
|
|
|
93
94
|
onAutomate,
|
|
94
95
|
onRemoveAutomation,
|
|
95
96
|
}: FxPresetRunProps) {
|
|
97
|
+
const trackInput = useTrackDesignInput();
|
|
96
98
|
const rows = run.items.map(({ node, i }) => (
|
|
97
99
|
<FxNodeRow
|
|
98
100
|
// Keyed by id, as the carve module's list above already is. On
|
|
@@ -183,7 +185,10 @@ export function FxPresetRun({
|
|
|
183
185
|
aria-pressed={runOn}
|
|
184
186
|
title={runOn ? `Switch ${preset.label} off` : `Switch ${preset.label} back on`}
|
|
185
187
|
disabled={disabled}
|
|
186
|
-
onClick={() =>
|
|
188
|
+
onClick={() => {
|
|
189
|
+
onSetAmount(runOn ? 0 : 1);
|
|
190
|
+
trackInput("toggle", "preset-amount");
|
|
191
|
+
}}
|
|
187
192
|
>
|
|
188
193
|
{runOn ? "On" : "Off"}
|
|
189
194
|
</button>
|
|
@@ -205,7 +210,10 @@ export function FxPresetRun({
|
|
|
205
210
|
disabled={disabled || automated}
|
|
206
211
|
automated={automated}
|
|
207
212
|
onChange={(_k, v) => onSetAmount(Number(v), false)}
|
|
208
|
-
onCommit={(_k, v) =>
|
|
213
|
+
onCommit={(_k, v) => {
|
|
214
|
+
onSetAmount(Number(v));
|
|
215
|
+
trackInput("slider", "preset-amount");
|
|
216
|
+
}}
|
|
209
217
|
onAutomate={onAutomate}
|
|
210
218
|
onRemoveAutomation={onRemoveAutomation}
|
|
211
219
|
/>
|
|
@@ -1787,3 +1787,59 @@ describe("voiceover carve visibility", () => {
|
|
|
1787
1787
|
expect(carveBlock(host)).toBeTruthy();
|
|
1788
1788
|
});
|
|
1789
1789
|
});
|
|
1790
|
+
|
|
1791
|
+
vi.mock("../../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
1792
|
+
import { trackStudioEvent } from "../../utils/studioTelemetry";
|
|
1793
|
+
import { __resetDesignInputThrottle } from "../../utils/designInputTracking";
|
|
1794
|
+
import { DesignPanelInputProvider } from "../../contexts/DesignPanelInputContext";
|
|
1795
|
+
function renderTrackedFx(props: Partial<Parameters<typeof FxSection>[0]> = {}) {
|
|
1796
|
+
vi.mocked(trackStudioEvent).mockClear();
|
|
1797
|
+
__resetDesignInputThrottle();
|
|
1798
|
+
return renderInto(
|
|
1799
|
+
<DesignPanelInputProvider ui="flat" section="media">
|
|
1800
|
+
<FxSection
|
|
1801
|
+
chain={chainOf("lowpass", "gain")}
|
|
1802
|
+
onChainChange={vi.fn()}
|
|
1803
|
+
carve={null}
|
|
1804
|
+
onCarveChange={vi.fn()}
|
|
1805
|
+
sourceOptions={[]}
|
|
1806
|
+
{...props}
|
|
1807
|
+
/>
|
|
1808
|
+
</DesignPanelInputProvider>,
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
describe("custom FX inspector input usage", () => {
|
|
1813
|
+
it.each([
|
|
1814
|
+
[".hf-fx-bypass", "toggle", "effect-bypass"],
|
|
1815
|
+
['.hf-fx-move[title="Move down"]', "button", "move-effect"],
|
|
1816
|
+
[".hf-fx-remove", "button", "remove-effect"],
|
|
1817
|
+
])("tracks %s through design_input once", (selector, control, name) => {
|
|
1818
|
+
const onChainChange = vi.fn();
|
|
1819
|
+
const { host, root } = renderTrackedFx({ onChainChange });
|
|
1820
|
+
click(host.querySelector(selector));
|
|
1821
|
+
expect(onChainChange).toHaveBeenCalledTimes(1);
|
|
1822
|
+
expect(
|
|
1823
|
+
vi.mocked(trackStudioEvent).mock.calls.filter(([event]) => event === "design_input"),
|
|
1824
|
+
).toEqual([["design_input", { ui: "flat", section: "media", control, name }]]);
|
|
1825
|
+
expect(vi.mocked(trackStudioEvent).mock.calls.some(([event]) => event === "feature_used")).toBe(
|
|
1826
|
+
false,
|
|
1827
|
+
);
|
|
1828
|
+
act(() => root.unmount());
|
|
1829
|
+
});
|
|
1830
|
+
it.each([false, true])("tracks a levelling button once (already levelled: %s)", (levelled) => {
|
|
1831
|
+
const action = vi.fn();
|
|
1832
|
+
const { host, root } = renderTrackedFx({ levelled, onLevel: action, onRemoveLevel: action });
|
|
1833
|
+
click(byText(host, "button", "+ effect"));
|
|
1834
|
+
vi.mocked(trackStudioEvent).mockClear();
|
|
1835
|
+
click(byText(host, "button", levelled ? "Remove levelling" : "Even Out Levels"));
|
|
1836
|
+
expect(action).toHaveBeenCalledTimes(1);
|
|
1837
|
+
expect(trackStudioEvent).toHaveBeenCalledExactlyOnceWith("design_input", {
|
|
1838
|
+
ui: "flat",
|
|
1839
|
+
section: "media",
|
|
1840
|
+
control: "button",
|
|
1841
|
+
name: levelled ? "remove-levelling" : "add-levelling",
|
|
1842
|
+
});
|
|
1843
|
+
act(() => root.unmount());
|
|
1844
|
+
});
|
|
1845
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
1
2
|
/**
|
|
2
3
|
* The FX section for an audio element: the chain, plus the voiceover carve.
|
|
3
4
|
*
|
|
@@ -120,13 +121,17 @@ export function FxSection({
|
|
|
120
121
|
// could only name a source it must not.
|
|
121
122
|
const showCarve = !carvedAgainstBy && (sourceOptions.length > 0 || carve !== null);
|
|
122
123
|
|
|
124
|
+
const trackInput = useTrackDesignInput();
|
|
123
125
|
const [adding, setAdding] = useState(false);
|
|
124
126
|
const [picking, setPicking] = useState(false);
|
|
125
127
|
const [openNode, setOpenNode] = useState<number | null>(0);
|
|
126
128
|
|
|
127
129
|
const mutate = useCallback(
|
|
128
|
-
(nodes: HfAudioFxNode[]
|
|
129
|
-
|
|
130
|
+
(nodes: HfAudioFxNode[], input?: string, control = "button") => {
|
|
131
|
+
onChainChange({ ...chain, nodes });
|
|
132
|
+
if (input) trackInput(control, input);
|
|
133
|
+
},
|
|
134
|
+
[chain, onChainChange, trackInput],
|
|
130
135
|
);
|
|
131
136
|
|
|
132
137
|
// Dragging a knob previews without persisting; releasing it commits once.
|
|
@@ -156,7 +161,7 @@ export function FxSection({
|
|
|
156
161
|
// old chain back over the write that just landed is a race the author
|
|
157
162
|
// hears as the preset arriving and then leaving again.
|
|
158
163
|
clearAudition();
|
|
159
|
-
mutate(next.nodes);
|
|
164
|
+
mutate(next.nodes, "apply-preset");
|
|
160
165
|
// Land on the first node the preset wrote, so the author can hear what
|
|
161
166
|
// arrived and immediately see what it is made of.
|
|
162
167
|
setOpenNode(next.nodes.findIndex((n) => n.fromPreset === id));
|
|
@@ -169,7 +174,7 @@ export function FxSection({
|
|
|
169
174
|
(job: HfAudioFxJob) => {
|
|
170
175
|
clearAudition();
|
|
171
176
|
trackNodeAdded(job.type, "job", job.id, { trackKind });
|
|
172
|
-
mutate(withJob(chain, job).nodes);
|
|
177
|
+
mutate(withJob(chain, job).nodes, "add-job");
|
|
173
178
|
setOpenNode(chain.nodes.length);
|
|
174
179
|
setAdding(false);
|
|
175
180
|
},
|
|
@@ -180,7 +185,7 @@ export function FxSection({
|
|
|
180
185
|
(type: string) => {
|
|
181
186
|
clearAudition();
|
|
182
187
|
trackNodeAdded(type, "effect", null, { trackKind });
|
|
183
|
-
mutate(withEffect(chain, type).nodes);
|
|
188
|
+
mutate(withEffect(chain, type).nodes, "add-effect");
|
|
184
189
|
setOpenNode(chain.nodes.length);
|
|
185
190
|
setAdding(false);
|
|
186
191
|
},
|
|
@@ -218,26 +223,18 @@ export function FxSection({
|
|
|
218
223
|
[chain, mutate, onChainPreview],
|
|
219
224
|
);
|
|
220
225
|
|
|
221
|
-
|
|
222
|
-
* Take a preset back out whole, lanes and all.
|
|
223
|
-
*
|
|
224
|
-
* Same contract as removing one node — an orphaned lane keeps driving a
|
|
225
|
-
* parameter that is no longer in the graph, and with ids minted lowest-free
|
|
226
|
-
* the next effect added inherits it.
|
|
227
|
-
*/
|
|
226
|
+
// Remove the run and its lanes together so new nodes cannot inherit orphaned automation.
|
|
228
227
|
const removeRun = useCallback(
|
|
229
228
|
(items: { node: HfAudioFxNode; i: number }[], presetId?: string) => {
|
|
230
|
-
//
|
|
231
|
-
// replaces the whole attribute, so a loop kept only its last write and left
|
|
232
|
-
// the other nodes' lanes behind as orphans. The preset id goes with it —
|
|
233
|
-
// the `fx.preset.<id>` amount lane belongs to the preset, not to any node,
|
|
234
|
-
// so nothing else would ever collect it, and re-applying the preset later
|
|
235
|
-
// resurrected the old ramp.
|
|
229
|
+
// Remove node and preset automation together before replacing the chain.
|
|
236
230
|
const ids = items.map(({ node }) => node.id).filter((id): id is string => Boolean(id));
|
|
237
231
|
if (ids.length > 0 || presetId) onRemoveNodesAutomation?.(ids, presetId);
|
|
238
232
|
if (presetId) trackPresetRemoved(presetId, { trackKind });
|
|
239
233
|
const slots = new Set(items.map((item) => item.i));
|
|
240
|
-
mutate(
|
|
234
|
+
mutate(
|
|
235
|
+
chain.nodes.filter((_, i) => !slots.has(i)),
|
|
236
|
+
"remove-preset",
|
|
237
|
+
);
|
|
241
238
|
setOpenNode(null);
|
|
242
239
|
},
|
|
243
240
|
[chain.nodes, mutate, onRemoveNodesAutomation, trackKind],
|
|
@@ -254,7 +251,10 @@ export function FxSection({
|
|
|
254
251
|
const removedId = removed?.id;
|
|
255
252
|
if (removedId) onRemoveNodeAutomation?.(removedId);
|
|
256
253
|
if (removed) trackNodeRemoved(removed.type, nodeOrigin(removed), { trackKind });
|
|
257
|
-
mutate(
|
|
254
|
+
mutate(
|
|
255
|
+
chain.nodes.filter((_, i) => i !== index),
|
|
256
|
+
"remove-effect",
|
|
257
|
+
);
|
|
258
258
|
setOpenNode(null);
|
|
259
259
|
},
|
|
260
260
|
[chain.nodes, mutate, onRemoveNodeAutomation, trackKind],
|
|
@@ -372,7 +372,7 @@ export function FxSection({
|
|
|
372
372
|
clearAudition();
|
|
373
373
|
const { chain: next, eqId } = addAudioEq(chain);
|
|
374
374
|
trackNodeAdded("eq", "eq", null, { trackKind });
|
|
375
|
-
mutate(next.nodes);
|
|
375
|
+
mutate(next.nodes, "add-eq");
|
|
376
376
|
setOpenEq(eqId);
|
|
377
377
|
setAdding(false);
|
|
378
378
|
}, [chain, mutate, clearAudition, trackKind]);
|
|
@@ -386,20 +386,18 @@ export function FxSection({
|
|
|
386
386
|
);
|
|
387
387
|
const commitEqBand = useCallback(
|
|
388
388
|
(eqId: string, band: string, gain: number) =>
|
|
389
|
-
mutate(setAudioEqBandGain(chain, eqId, band, gain).nodes),
|
|
389
|
+
mutate(setAudioEqBandGain(chain, eqId, band, gain).nodes, "eq-gain", "slider"),
|
|
390
390
|
[chain, mutate],
|
|
391
391
|
);
|
|
392
392
|
const removeEq = useCallback(
|
|
393
393
|
(eqId: string) => {
|
|
394
|
-
//
|
|
395
|
-
// last-write-wins bug and was only unreachable because an EQ band row
|
|
396
|
-
// offers no automation toggle today.
|
|
394
|
+
// Remove the EQ automation in one write.
|
|
397
395
|
const ids = chain.nodes
|
|
398
396
|
.filter((node) => node.fromEq === eqId)
|
|
399
397
|
.map((node) => node.id)
|
|
400
398
|
.filter((id): id is string => Boolean(id));
|
|
401
399
|
if (ids.length > 0) onRemoveNodesAutomation?.(ids);
|
|
402
|
-
mutate(removeAudioEq(chain, eqId).nodes);
|
|
400
|
+
mutate(removeAudioEq(chain, eqId).nodes, "remove-eq");
|
|
403
401
|
},
|
|
404
402
|
[chain, mutate, onRemoveNodesAutomation],
|
|
405
403
|
);
|
|
@@ -412,7 +410,7 @@ export function FxSection({
|
|
|
412
410
|
const [moved] = next.splice(index, 1);
|
|
413
411
|
next.splice(target, 0, moved!);
|
|
414
412
|
if (moved) trackNodeMoved(moved.type, delta < 0 ? "up" : "down", { trackKind });
|
|
415
|
-
mutate(next);
|
|
413
|
+
mutate(next, "move-effect");
|
|
416
414
|
setOpenNode(target);
|
|
417
415
|
},
|
|
418
416
|
[chain.nodes, mutate, trackKind],
|
|
@@ -448,6 +446,7 @@ export function FxSection({
|
|
|
448
446
|
return () => {
|
|
449
447
|
trackPresetAutomated(presetId, true, { trackKind });
|
|
450
448
|
onAutomatePreset(presetId, amount);
|
|
449
|
+
trackInput("button", "automate-preset");
|
|
451
450
|
};
|
|
452
451
|
};
|
|
453
452
|
|
|
@@ -456,7 +455,10 @@ export function FxSection({
|
|
|
456
455
|
presetId: string | undefined,
|
|
457
456
|
): (() => void) | undefined => {
|
|
458
457
|
if (!presetId || !onRemovePresetAutomation || !presetAutomated.has(presetId)) return undefined;
|
|
459
|
-
return () =>
|
|
458
|
+
return () => {
|
|
459
|
+
onRemovePresetAutomation(presetId);
|
|
460
|
+
trackInput("button", "remove-preset-automation");
|
|
461
|
+
};
|
|
460
462
|
};
|
|
461
463
|
|
|
462
464
|
return (
|
|
@@ -82,12 +82,15 @@ export interface PropertyPanelProps {
|
|
|
82
82
|
element: DomEditSelection,
|
|
83
83
|
next: { x: number; y: number },
|
|
84
84
|
route?: { plainTranslate: boolean },
|
|
85
|
-
) => void | Promise<void>;
|
|
85
|
+
) => void | Promise<void | import("../../utils/previewFeatureUsage").GeometryCommitResult>;
|
|
86
86
|
onSetManualSize: (
|
|
87
87
|
element: DomEditSelection,
|
|
88
88
|
next: { width: number; height: number },
|
|
89
|
-
) => void | Promise<void>;
|
|
90
|
-
onSetManualRotation: (
|
|
89
|
+
) => void | Promise<void | import("../../utils/previewFeatureUsage").GeometryCommitResult>;
|
|
90
|
+
onSetManualRotation: (
|
|
91
|
+
element: DomEditSelection,
|
|
92
|
+
next: { angle: number },
|
|
93
|
+
) => void | Promise<void | import("../../utils/previewFeatureUsage").GeometryCommitResult>;
|
|
91
94
|
onSetText: (value: string, fieldKey?: string) => void;
|
|
92
95
|
onSetTextFieldStyle: (fieldKey: string, property: string, value: string) => void;
|
|
93
96
|
onPreviewTextFieldStyle?: (fieldKey: string, property: string, value: string) => void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { trackStudioEvent } from "../../utils/studioTelemetry";
|
|
1
2
|
// @vitest-environment happy-dom
|
|
2
3
|
import React, { act } from "react";
|
|
3
4
|
import { createRoot } from "react-dom/client";
|
|
@@ -423,3 +424,39 @@ describe("useDomEditNudge — a commit that throws", () => {
|
|
|
423
424
|
act(() => root.unmount());
|
|
424
425
|
});
|
|
425
426
|
});
|
|
427
|
+
|
|
428
|
+
vi.mock("../../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
429
|
+
describe("nudge usage", () => {
|
|
430
|
+
beforeEach(() => {
|
|
431
|
+
vi.clearAllMocks();
|
|
432
|
+
vi.useFakeTimers();
|
|
433
|
+
__resetForTests();
|
|
434
|
+
});
|
|
435
|
+
afterEach(() => vi.useRealTimers());
|
|
436
|
+
it.each([true, false])(
|
|
437
|
+
"counts a whole key burst only when the writer changed source (%s)",
|
|
438
|
+
async (changed) => {
|
|
439
|
+
const root = createRoot(document.body.appendChild(document.createElement("div")));
|
|
440
|
+
const element = document.body.appendChild(document.createElement("div"));
|
|
441
|
+
const commit = vi.fn().mockResolvedValue({ ok: true, changed });
|
|
442
|
+
act(() =>
|
|
443
|
+
root.render(
|
|
444
|
+
<Harness selection={makeSelection("Dot", element)} onPathOffsetCommit={commit} />,
|
|
445
|
+
),
|
|
446
|
+
);
|
|
447
|
+
act(() => {
|
|
448
|
+
dispatchArrowRight();
|
|
449
|
+
dispatchArrowRight();
|
|
450
|
+
});
|
|
451
|
+
await act(async () => vi.advanceTimersByTimeAsync(CANVAS_NUDGE_COMMIT_DEBOUNCE_MS + 10));
|
|
452
|
+
expect(commit).toHaveBeenCalledTimes(1);
|
|
453
|
+
expect(vi.mocked(trackStudioEvent).mock.calls).toEqual(
|
|
454
|
+
changed
|
|
455
|
+
? [["feature_used", { feature: "nudge", surface: "preview", method: "keyboard" }]]
|
|
456
|
+
: [],
|
|
457
|
+
);
|
|
458
|
+
act(() => root.unmount());
|
|
459
|
+
element.remove();
|
|
460
|
+
},
|
|
461
|
+
);
|
|
462
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { trackPreviewEditResult } from "../../utils/previewFeatureUsage";
|
|
1
2
|
/**
|
|
2
3
|
* Canvas arrow-key nudge for DomEditOverlay: arrows move the selected
|
|
3
4
|
* element(s) 1 composition px, Shift = 10. Each keypress previews through the
|
|
@@ -173,6 +174,7 @@ export function useDomEditNudge(params: UseDomEditNudgeParams): { flushNudge: ()
|
|
|
173
174
|
for (const member of session.members)
|
|
174
175
|
restoreStudioPathOffset(member.element, member.initialPathOffset);
|
|
175
176
|
saved = Promise.resolve(commit)
|
|
177
|
+
.then((result) => trackPreviewEditResult("nudge", "keyboard", result))
|
|
176
178
|
.catch(() => {
|
|
177
179
|
for (const member of session.members) {
|
|
178
180
|
if (isStudioManualEditGestureCurrent(member.element, member.gestureToken)) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { trackPreviewEditResult } from "../../utils/previewFeatureUsage";
|
|
1
2
|
// fallow-ignore-file code-duplication
|
|
2
3
|
/**
|
|
3
4
|
* Gesture handling for DomEditOverlay.
|
|
@@ -353,6 +354,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
353
354
|
const groupSaved = Promise.resolve(
|
|
354
355
|
groupEdit.adopt(() => opts.onGroupPathOffsetCommitRef.current(updates)),
|
|
355
356
|
)
|
|
357
|
+
.then((result) => trackPreviewEditResult("move", "drag", result))
|
|
356
358
|
.catch(() => {
|
|
357
359
|
for (const member of groupG.members) {
|
|
358
360
|
if (
|
|
@@ -442,6 +444,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
442
444
|
applyRotationDraft(sel.element, finalRotation.angle, g.plainRotation);
|
|
443
445
|
const commit = g.plainRotation ? { ...finalRotation, plain: g.plainRotation } : finalRotation;
|
|
444
446
|
void Promise.resolve(opts.onRotationCommitRef.current(sel, commit))
|
|
447
|
+
.then((result) => trackPreviewEditResult("rotate", "drag", result))
|
|
445
448
|
.catch((error) => {
|
|
446
449
|
logGestureCommitFailure("rotate commit failed", error);
|
|
447
450
|
if (
|
|
@@ -488,6 +491,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
488
491
|
}),
|
|
489
492
|
),
|
|
490
493
|
)
|
|
494
|
+
.then((result) => trackPreviewEditResult("move", "drag", result))
|
|
491
495
|
.catch(() => {
|
|
492
496
|
if (
|
|
493
497
|
g.pathOffsetMember?.gestureToken &&
|
|
@@ -534,6 +538,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
534
538
|
void Promise.resolve(
|
|
535
539
|
opts.onBoxSizeCommitRef.current(sel, finalSize, finalOffset ?? undefined, restore),
|
|
536
540
|
)
|
|
541
|
+
.then((result) => trackPreviewEditResult("resize", "drag", result))
|
|
537
542
|
.catch((error) => {
|
|
538
543
|
logGestureCommitFailure("resize commit failed", error);
|
|
539
544
|
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { trackPreviewFeatureUsed } from "../../utils/previewFeatureUsage";
|
|
1
2
|
import { useCallback, useMemo } from "react";
|
|
2
3
|
import { useDomEditActionsContext } from "../../contexts/DomEditContext";
|
|
3
4
|
import { readHfId, type DomEditSelection } from "./domEditing";
|
|
@@ -156,8 +157,13 @@ export function useDomEditZOrder(): DomEditZOrder {
|
|
|
156
157
|
// One serialized z→lane transaction: the mirror runs only AFTER a durable z commit and
|
|
157
158
|
// no second gesture interleaves (see runZLaneGesture). A failed z commit has already
|
|
158
159
|
// toasted and rolled back, so the catch only keeps its rejection from going unhandled.
|
|
160
|
+
let changed = false;
|
|
159
161
|
runZLaneGesture({
|
|
160
|
-
commitZ: () =>
|
|
162
|
+
commitZ: async () => {
|
|
163
|
+
const result = await handleDomZIndexReorderCommit(entries, coalesceKey, action);
|
|
164
|
+
changed = result?.durable === true && result.changed;
|
|
165
|
+
return result;
|
|
166
|
+
},
|
|
161
167
|
mirror: () =>
|
|
162
168
|
mirrorZOrderToTimeline({
|
|
163
169
|
selectionKey: entries.find((e) => e.element === sel.element)?.key,
|
|
@@ -166,7 +172,11 @@ export function useDomEditZOrder(): DomEditZOrder {
|
|
|
166
172
|
sourceFile: sel.sourceFile,
|
|
167
173
|
coalesceKey,
|
|
168
174
|
}),
|
|
169
|
-
})
|
|
175
|
+
})
|
|
176
|
+
.then(() => {
|
|
177
|
+
if (changed) trackPreviewFeatureUsed("z_order", "button");
|
|
178
|
+
})
|
|
179
|
+
.catch(() => undefined);
|
|
170
180
|
return true;
|
|
171
181
|
},
|
|
172
182
|
[activeCompPath, handleDomZIndexReorderCommit, mirrorZOrderToTimeline, readOnly],
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TimelineEditOutcome } from "../../hooks/timelineEditPermission";
|
|
2
|
+
import { trackKeyframeUsage } from "../../utils/keyframeUsage";
|
|
2
3
|
import { useCallback, useMemo } from "react";
|
|
3
4
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
4
5
|
import type { TimelineElement } from "../../player";
|
|
@@ -259,7 +260,12 @@ export function useTimelineEditCallbacks({
|
|
|
259
260
|
if (!selection) return;
|
|
260
261
|
// Serial: each removal rewrites the same source file, so dispatching
|
|
261
262
|
// them together would have the later writes read a pre-edit document.
|
|
262
|
-
|
|
263
|
+
let changed = false;
|
|
264
|
+
for (const anim of anims) {
|
|
265
|
+
const result = await handleGsapRemoveAllKeyframes(anim.id, selection, false);
|
|
266
|
+
changed = result || changed;
|
|
267
|
+
}
|
|
268
|
+
if (changed) trackKeyframeUsage("remove_all");
|
|
263
269
|
});
|
|
264
270
|
},
|
|
265
271
|
onDeleteKeyframe: (elId, keyframe) => {
|