@hyperframes/studio 0.8.113 → 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-DzJbtQVO.js → hyperframes-player-C-9N8nk0.js} +1 -1
- package/dist/assets/{index-n5FUn1xv.js → index-BoUM6Xqb.js} +1 -1
- package/dist/assets/{index-DRhF3v4Z.js → index-CAvZ0AA-.js} +204 -204
- package/dist/assets/{index-DgwDQK5G.js → index-CVuorIMv.js} +1 -1
- package/dist/assets/index-t1LH6H5H.css +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/{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 +46 -22
- package/dist/index.html +2 -2
- package/dist/index.js +3899 -3487
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +10 -8
- package/src/App.tsx +84 -86
- package/src/components/EditorShell.tsx +3 -3
- package/src/components/LinkedSelectionToggle.tsx +3 -2
- package/src/components/StudioHeader.tsx +4 -4
- package/src/components/StudioLeftPanels.tsx +3 -3
- package/src/components/StudioOverlays.tsx +4 -4
- package/src/components/StudioRightPanels.tsx +14 -9
- package/src/components/StudioToast.tsx +1 -1
- package/src/components/TimelineToolbar.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/InlineTextToolbar.tsx +11 -27
- 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/colorBrowserCanvas.test.ts +116 -0
- package/src/components/editor/colorValue.test.ts +16 -0
- package/src/components/editor/colorValue.ts +100 -24
- package/src/components/editor/commitPreflightHandles.test.tsx +2 -2
- package/src/components/editor/cropResize.test.ts +11 -0
- package/src/components/editor/cropResize.ts +6 -6
- package/src/components/editor/domEditingDom.ts +4 -0
- package/src/components/editor/domEditingLayers.ts +1 -5
- package/src/components/editor/gradientValue.ts +28 -36
- package/src/components/editor/gsapAnimatesProperty.ts +0 -54
- package/src/components/editor/inlineTextStyleRange.test.ts +31 -1
- package/src/components/editor/inlineTextStyleRange.ts +5 -2
- package/src/components/editor/inlineTextStyleRead.ts +9 -0
- package/src/components/editor/manualEdits.ts +1 -0
- package/src/components/editor/manualEditsDom.ts +2 -2
- package/src/components/editor/manualEditsParsing.ts +7 -0
- package/src/components/editor/manualEditsSeekReapply.ts +2 -2
- 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/propertyPanelColor.tsx +1 -1
- 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/reapplyPositionEditsAfterSeek.test.ts +25 -0
- 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/gsapRuntimeKeyframes.ts +1 -1
- package/src/hooks/gsapRuntimeReaders.test.ts +48 -0
- package/src/hooks/gsapRuntimeReaders.ts +3 -90
- 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/useAppHotkeys.ts +11 -5
- 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/useFrameCapture.ts +15 -8
- package/src/hooks/useGestureCommit.test.tsx +225 -55
- package/src/hooks/useGestureCommit.ts +90 -56
- package/src/hooks/useGestureTrailOverlay.tsx +33 -0
- 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/usePanelLayout.ts +5 -1
- 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/useShellSlices.ts +53 -0
- package/src/hooks/useStableHandlers.test.tsx +77 -0
- package/src/hooks/useStableHandlers.ts +44 -0
- package/src/hooks/useStudioSdkSessions.ts +2 -1
- package/src/hooks/useStudioSidePanels.test.tsx +44 -0
- package/src/hooks/useStudioSidePanels.tsx +23 -0
- package/src/hooks/useTimelineAddAtPlayhead.ts +16 -8
- 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/hooks/useTimelineEditing.ts +33 -27
- 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/frameCapture.ts +3 -1
- package/src/utils/gsapUndoRestore.browser.test.ts +4 -2
- 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/assets/index-DbfKVTId.css +0 -1
- 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
|
@@ -43,3 +43,10 @@ export function readFileChangeAffectedCompositions(payload: unknown): readonly s
|
|
|
43
43
|
if (!Array.isArray(value) || !value.every((path) => typeof path === "string")) return null;
|
|
44
44
|
return value.map(normalizeStudioFileChangePath);
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
export function mergeFileChangeAffectedCompositions(waiting: unknown, incoming: unknown): unknown {
|
|
48
|
+
const before = readFileChangeAffectedCompositions(waiting);
|
|
49
|
+
const after = readFileChangeAffectedCompositions(incoming);
|
|
50
|
+
const merged = before && after ? [...new Set([...before, ...after])] : null;
|
|
51
|
+
return { ...asPayloadRecord(incoming), affectedCompositions: merged };
|
|
52
|
+
}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { applyStudioBoxSize, applyStudioPathOffset, applyStudioRotation } from "./manualEditsDom";
|
|
13
13
|
import { applyStudioMotionFromDom } from "./studioMotion";
|
|
14
14
|
import { STUDIO_MOTION_ATTR, STUDIO_MOTION_TIMELINE_ID } from "./studioMotionTypes";
|
|
15
|
-
import {
|
|
15
|
+
import { gsapWritesChannels } from "../../hooks/gsapRuntimeKeyframes";
|
|
16
16
|
|
|
17
17
|
function queryStudioElements(doc: Document, attr: string): HTMLElement[] {
|
|
18
18
|
const ctor = doc.defaultView?.HTMLElement;
|
|
@@ -35,7 +35,7 @@ function queryStudioElements(doc: Document, attr: string): HTMLElement[] {
|
|
|
35
35
|
function reapplyPathOffsets(doc: Document): void {
|
|
36
36
|
for (const el of queryStudioElements(doc, STUDIO_PATH_OFFSET_ATTR)) {
|
|
37
37
|
// Unlike size below, the offset channels add up: applying both doubles the move.
|
|
38
|
-
if (
|
|
38
|
+
if (gsapWritesChannels(el, ["x", "y"])) continue;
|
|
39
39
|
const x = el.style.getPropertyValue(STUDIO_OFFSET_X_PROP);
|
|
40
40
|
const y = el.style.getPropertyValue(STUDIO_OFFSET_Y_PROP);
|
|
41
41
|
if (!x && !y) continue;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
|
-
import { act, useRef } from "react";
|
|
2
|
+
import { act, useRef, type RefObject } from "react";
|
|
3
3
|
import type { Root } from "react-dom/client";
|
|
4
4
|
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
5
5
|
import { installReactActEnvironment, mountReactHarness } from "../../hooks/domSelectionTestHarness";
|
|
6
|
+
import { trackStudioEvent } from "../../utils/studioTelemetry";
|
|
7
|
+
import type { DomSelectionResult } from "../../hooks/useDomSelectionTypes";
|
|
6
8
|
import { useMarqueeGestures } from "./marqueeCommit";
|
|
7
9
|
|
|
10
|
+
vi.mock("../../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
11
|
+
|
|
8
12
|
// Layout stands in for a real preview: every element is visible and sits at the band's start.
|
|
9
13
|
vi.mock("./domEditingElement", async (importOriginal) => ({
|
|
10
14
|
...(await importOriginal<typeof import("./domEditingElement")>()),
|
|
@@ -40,9 +44,53 @@ function Overlay() {
|
|
|
40
44
|
);
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
function MarqueeSurface({
|
|
48
|
+
overlayRef,
|
|
49
|
+
marquee,
|
|
50
|
+
}: {
|
|
51
|
+
overlayRef: RefObject<HTMLDivElement | null>;
|
|
52
|
+
marquee: ReturnType<typeof useMarqueeGestures>;
|
|
53
|
+
}) {
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
ref={overlayRef}
|
|
57
|
+
data-band-overlay
|
|
58
|
+
onPointerDown={marquee.begin}
|
|
59
|
+
onPointerMove={marquee.onPointerMove}
|
|
60
|
+
onPointerUp={marquee.onPointerUp}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function TestMarquee({
|
|
66
|
+
iframeRef,
|
|
67
|
+
onSelect,
|
|
68
|
+
resolveHits,
|
|
69
|
+
}: {
|
|
70
|
+
iframeRef: RefObject<HTMLIFrameElement | null>;
|
|
71
|
+
onSelect: (selections: HTMLElement[], additive: boolean) => DomSelectionResult | void;
|
|
72
|
+
resolveHits: (elements: HTMLElement[]) => HTMLElement[] | Promise<HTMLElement[]>;
|
|
73
|
+
}) {
|
|
74
|
+
const overlayRef = useRef<HTMLDivElement>(null);
|
|
75
|
+
const marquee = useMarqueeGestures({
|
|
76
|
+
iframeRef,
|
|
77
|
+
overlayRef,
|
|
78
|
+
activeCompositionPathRef: useRef<string | null>("index.html"),
|
|
79
|
+
onMarqueeSelectRef: { current: onSelect },
|
|
80
|
+
resolveHits,
|
|
81
|
+
});
|
|
82
|
+
return <MarqueeSurface overlayRef={overlayRef} marquee={marquee} />;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const pointer = (
|
|
86
|
+
type: string,
|
|
87
|
+
clientX: number,
|
|
88
|
+
clientY: number,
|
|
89
|
+
selector = "[data-overlay]",
|
|
90
|
+
shiftKey = false,
|
|
91
|
+
) =>
|
|
44
92
|
act(() => {
|
|
45
|
-
document.querySelector(
|
|
93
|
+
document.querySelector(selector)!.dispatchEvent(
|
|
46
94
|
new PointerEvent(type, {
|
|
47
95
|
bubbles: true,
|
|
48
96
|
buttons: type === "pointerup" ? 0 : 1,
|
|
@@ -50,6 +98,7 @@ const pointer = (type: string, clientX: number, clientY: number) =>
|
|
|
50
98
|
pointerId: 1,
|
|
51
99
|
clientX,
|
|
52
100
|
clientY,
|
|
101
|
+
shiftKey,
|
|
53
102
|
}),
|
|
54
103
|
);
|
|
55
104
|
});
|
|
@@ -68,6 +117,7 @@ const hostListener = (e: KeyboardEvent) => {
|
|
|
68
117
|
};
|
|
69
118
|
|
|
70
119
|
beforeEach(() => {
|
|
120
|
+
vi.clearAllMocks();
|
|
71
121
|
hostEscapes = [];
|
|
72
122
|
window.addEventListener("keydown", hostListener);
|
|
73
123
|
root = mountReactHarness(<Overlay />);
|
|
@@ -128,42 +178,17 @@ it("a reload promoted mid-band selects from the preview on screen, not the retir
|
|
|
128
178
|
const [retired, live] = [preview(), preview()];
|
|
129
179
|
const picked: HTMLElement[][] = [];
|
|
130
180
|
const iframeRef = { current: retired as HTMLIFrameElement | null };
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
activeCompositionPathRef: useRef<string | null>("index.html"),
|
|
137
|
-
onMarqueeSelectRef: useRef(() => undefined),
|
|
138
|
-
resolveHits: async (elements: HTMLElement[]) => {
|
|
181
|
+
const band = mountReactHarness(
|
|
182
|
+
<TestMarquee
|
|
183
|
+
iframeRef={iframeRef}
|
|
184
|
+
onSelect={() => undefined}
|
|
185
|
+
resolveHits={async (elements) => {
|
|
139
186
|
picked.push(elements);
|
|
140
187
|
return [];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
ref={overlayRef}
|
|
146
|
-
data-band-overlay
|
|
147
|
-
onPointerDown={marquee.begin}
|
|
148
|
-
onPointerMove={marquee.onPointerMove}
|
|
149
|
-
onPointerUp={marquee.onPointerUp}
|
|
150
|
-
/>
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
const band = mountReactHarness(<Band />);
|
|
154
|
-
const fire = (type: string, x: number, y: number) =>
|
|
155
|
-
act(() => {
|
|
156
|
-
document.querySelector("[data-band-overlay]")!.dispatchEvent(
|
|
157
|
-
new PointerEvent(type, {
|
|
158
|
-
bubbles: true,
|
|
159
|
-
button: 0,
|
|
160
|
-
buttons: type === "pointerup" ? 0 : 1,
|
|
161
|
-
pointerId: 1,
|
|
162
|
-
clientX: x,
|
|
163
|
-
clientY: y,
|
|
164
|
-
}),
|
|
165
|
-
);
|
|
166
|
-
});
|
|
188
|
+
}}
|
|
189
|
+
/>,
|
|
190
|
+
);
|
|
191
|
+
const fire = (type: string, x: number, y: number) => pointer(type, x, y, "[data-band-overlay]");
|
|
167
192
|
try {
|
|
168
193
|
fire("pointerdown", 0, 0);
|
|
169
194
|
fire("pointermove", 120, 90);
|
|
@@ -175,3 +200,47 @@ it("a reload promoted mid-band selects from the preview on screen, not the retir
|
|
|
175
200
|
act(() => band.unmount());
|
|
176
201
|
}
|
|
177
202
|
});
|
|
203
|
+
|
|
204
|
+
it.each([
|
|
205
|
+
[2, false, 2, 0],
|
|
206
|
+
[1, true, 2, 1],
|
|
207
|
+
[2, true, 2, 1],
|
|
208
|
+
[1, true, 1, 0],
|
|
209
|
+
])(
|
|
210
|
+
"counts marquee receipt: %s hits, changed=%s, group=%s",
|
|
211
|
+
async (hits, changed, count, events) => {
|
|
212
|
+
const iframe = document.createElement("iframe");
|
|
213
|
+
const doc = document.implementation.createHTMLDocument("preview");
|
|
214
|
+
doc.body.innerHTML =
|
|
215
|
+
'<div data-composition-id="main"><h1 id="title">Title</h1><p id="subtitle">Subtitle</p></div>';
|
|
216
|
+
Object.defineProperty(iframe, "contentDocument", { value: doc });
|
|
217
|
+
const apply = vi.fn((_selections: HTMLElement[], _additive: boolean) => ({ changed, count }));
|
|
218
|
+
const band = mountReactHarness(
|
|
219
|
+
<TestMarquee
|
|
220
|
+
iframeRef={{ current: iframe }}
|
|
221
|
+
onSelect={apply}
|
|
222
|
+
resolveHits={(elements) => elements.slice(0, hits)}
|
|
223
|
+
/>,
|
|
224
|
+
);
|
|
225
|
+
const fire = (type: string, x: number, y: number) =>
|
|
226
|
+
pointer(type, x, y, "[data-band-overlay]", true);
|
|
227
|
+
try {
|
|
228
|
+
fire("pointerdown", 0, 0);
|
|
229
|
+
fire("pointermove", 120, 90);
|
|
230
|
+
await act(async () => {
|
|
231
|
+
fire("pointerup", 120, 90);
|
|
232
|
+
});
|
|
233
|
+
expect(apply).toHaveBeenCalledTimes(1);
|
|
234
|
+
expect(apply.mock.calls[0]![0]).toHaveLength(hits);
|
|
235
|
+
expect(trackStudioEvent).toHaveBeenCalledTimes(events);
|
|
236
|
+
if (events)
|
|
237
|
+
expect(trackStudioEvent).toHaveBeenCalledWith("feature_used", {
|
|
238
|
+
feature: "multi_select",
|
|
239
|
+
surface: "preview",
|
|
240
|
+
method: "drag",
|
|
241
|
+
});
|
|
242
|
+
} finally {
|
|
243
|
+
act(() => band.unmount());
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { DomSelectionResult } from "../../hooks/useDomSelectionTypes";
|
|
2
|
+
import { trackPreviewFeatureUsed } from "../../utils/previewFeatureUsage";
|
|
1
3
|
// fallow-ignore-file code-duplication
|
|
2
4
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
5
|
import type { DomEditSelection } from "./domEditing";
|
|
@@ -18,6 +20,16 @@ interface MarqueeState {
|
|
|
18
20
|
target: Element;
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
function currentMarqueeCandidates(
|
|
24
|
+
cached: MarqueeHit[] | null,
|
|
25
|
+
iframe: HTMLIFrameElement,
|
|
26
|
+
overlay: HTMLDivElement,
|
|
27
|
+
path: string,
|
|
28
|
+
): MarqueeHit[] {
|
|
29
|
+
if (cached?.every((hit) => hit.element.ownerDocument === iframe.contentDocument)) return cached;
|
|
30
|
+
return collectMarqueeCandidates(iframe, overlay, path);
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
const MARQUEE_THRESHOLD_PX = 4;
|
|
22
34
|
|
|
23
35
|
interface MarqueeHit {
|
|
@@ -103,7 +115,9 @@ export interface MarqueeGesturesDeps<T = DomEditSelection> {
|
|
|
103
115
|
iframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
104
116
|
overlayRef: React.RefObject<HTMLDivElement | null>;
|
|
105
117
|
activeCompositionPathRef: React.RefObject<string | null>;
|
|
106
|
-
onMarqueeSelectRef: React.RefObject<
|
|
118
|
+
onMarqueeSelectRef: React.RefObject<
|
|
119
|
+
((selections: T[], additive: boolean) => DomSelectionResult | void) | undefined
|
|
120
|
+
>;
|
|
107
121
|
/** Turns the elements a drag touched into picks; without it, Studio's edit selections. */
|
|
108
122
|
resolveHits?: (elements: HTMLElement[]) => T[] | Promise<T[]>;
|
|
109
123
|
selectionRef?: React.RefObject<DomEditSelection | null>;
|
|
@@ -127,6 +141,10 @@ export interface MarqueeGestures {
|
|
|
127
141
|
onPointerCancel: () => void;
|
|
128
142
|
}
|
|
129
143
|
|
|
144
|
+
function trackMarqueeSelection(result: DomSelectionResult | void): void {
|
|
145
|
+
if (result?.changed && result.count > 1) trackPreviewFeatureUsed("multi_select", "drag");
|
|
146
|
+
}
|
|
147
|
+
|
|
130
148
|
function releaseCapture(m: MarqueeState): void {
|
|
131
149
|
try {
|
|
132
150
|
m.target.releasePointerCapture(m.pointerId);
|
|
@@ -160,18 +178,16 @@ export function useMarqueeGestures<T>(deps: MarqueeGesturesDeps<T>): MarqueeGest
|
|
|
160
178
|
const overlay = deps.overlayRef.current;
|
|
161
179
|
if (!iframe || !overlay || !deps.onMarqueeSelectRef.current) return;
|
|
162
180
|
const acp = deps.activeCompositionPathRef.current ?? "index.html";
|
|
163
|
-
const
|
|
164
|
-
const measuredOnScreen = cached?.every(
|
|
165
|
-
(hit) => hit.element.ownerDocument === iframe.contentDocument,
|
|
166
|
-
);
|
|
167
|
-
const candidates =
|
|
168
|
-
cached && measuredOnScreen ? cached : collectMarqueeCandidates(iframe, overlay, acp);
|
|
181
|
+
const candidates = currentMarqueeCandidates(candidatesRef.current, iframe, overlay, acp);
|
|
169
182
|
const elements = hitsWithin(rect, candidates).map((hit) => hit.element);
|
|
170
183
|
const resolveHits = deps.resolveHits;
|
|
171
184
|
const picks = resolveHits
|
|
172
185
|
? await resolveHits(elements)
|
|
173
186
|
: ((await resolveDomEditSelections(elements, acp)) as T[]);
|
|
174
|
-
deps.onMarqueeSelectRef.current
|
|
187
|
+
const applySelection = deps.onMarqueeSelectRef.current;
|
|
188
|
+
if (!applySelection) return;
|
|
189
|
+
const result = applySelection(picks, additive);
|
|
190
|
+
trackMarqueeSelection(result);
|
|
175
191
|
},
|
|
176
192
|
[
|
|
177
193
|
deps.iframeRef,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { beforeEach, describe, it, expect, vi } from "vitest";
|
|
3
3
|
import type { GsapAnimation } from "@hyperframes/parsers/gsap-parser";
|
|
4
4
|
import { editableAnimationId } from "./motionPathSelection";
|
|
5
5
|
import {
|
|
@@ -148,3 +148,31 @@ describe("commitCreatePath", () => {
|
|
|
148
148
|
);
|
|
149
149
|
});
|
|
150
150
|
});
|
|
151
|
+
|
|
152
|
+
vi.mock("../../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
153
|
+
import { trackStudioEvent } from "../../utils/studioTelemetry";
|
|
154
|
+
import type { CommitFn } from "./motionPathCommit";
|
|
155
|
+
beforeEach(() => vi.clearAllMocks());
|
|
156
|
+
describe("motion path usage at the writer", () => {
|
|
157
|
+
it("counts a changed drag once without coordinates or target ids", async () => {
|
|
158
|
+
const commit: CommitFn = async (_mutation, options) => {
|
|
159
|
+
options.onResult?.({ ok: true, changed: true });
|
|
160
|
+
};
|
|
161
|
+
await commitNode({ type: "keyframe", pct: 50 }, 120, 30, "private-target", commit);
|
|
162
|
+
expect(trackStudioEvent).toHaveBeenCalledExactlyOnceWith("feature_used", {
|
|
163
|
+
feature: "motion_path",
|
|
164
|
+
surface: "preview",
|
|
165
|
+
method: "drag",
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
it.each([undefined, { ok: true }, { ok: true, changed: false }, { ok: false, changed: true }])(
|
|
169
|
+
"does not count absent or unsuccessful results (%j)",
|
|
170
|
+
async (result) => {
|
|
171
|
+
const commit: CommitFn = async (_mutation, options) => {
|
|
172
|
+
if (result) options.onResult?.(result);
|
|
173
|
+
};
|
|
174
|
+
await commitAddWaypoint("private-target", 1, 20, 40, commit);
|
|
175
|
+
expect(trackStudioEvent).not.toHaveBeenCalled();
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { CommitMutationOptions } from "../../hooks/gsapScriptCommitTypes";
|
|
2
|
+
import { trackPreviewFeatureUsed, type PreviewMethod } from "../../utils/previewFeatureUsage";
|
|
1
3
|
/**
|
|
2
4
|
* Commit helpers for the motion-path overlay. Each maps a canvas gesture to a
|
|
3
5
|
* GSAP source mutation routed through the (selection-bound) commit facade, which
|
|
@@ -7,11 +9,21 @@ import type { MotionNodeRef } from "./motionPathGeometry";
|
|
|
7
9
|
|
|
8
10
|
export type CommitFn = (
|
|
9
11
|
mutation: Record<string, unknown>,
|
|
10
|
-
options:
|
|
12
|
+
options: CommitMutationOptions,
|
|
11
13
|
) => Promise<void>;
|
|
12
14
|
|
|
13
15
|
const NEW_PATH_DURATION = 1.5;
|
|
14
16
|
|
|
17
|
+
function motionPathCommitOptions(label: string, method: PreviewMethod): CommitMutationOptions {
|
|
18
|
+
return {
|
|
19
|
+
label,
|
|
20
|
+
softReload: true,
|
|
21
|
+
onResult: (result) => {
|
|
22
|
+
if (result.ok && result.changed === true) trackPreviewFeatureUsed("motion_path", method);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
export function commitNode(
|
|
16
28
|
ref: MotionNodeRef,
|
|
17
29
|
x: number,
|
|
@@ -23,10 +35,10 @@ export function commitNode(
|
|
|
23
35
|
ref.type === "keyframe"
|
|
24
36
|
? { type: "update-keyframe", animationId, percentage: ref.pct, properties: { x, y } }
|
|
25
37
|
: { type: "update-motion-path-point", animationId, pointIndex: ref.index, x, y };
|
|
26
|
-
return commit(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
return commit(
|
|
39
|
+
mutation,
|
|
40
|
+
motionPathCommitOptions(ref.type === "keyframe" ? "Move keyframe" : "Move waypoint", "drag"),
|
|
41
|
+
);
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
export function commitAddWaypoint(
|
|
@@ -38,7 +50,7 @@ export function commitAddWaypoint(
|
|
|
38
50
|
): Promise<void> {
|
|
39
51
|
return commit(
|
|
40
52
|
{ type: "add-motion-path-point", animationId, index, x, y },
|
|
41
|
-
|
|
53
|
+
motionPathCommitOptions("Add waypoint", "button"),
|
|
42
54
|
);
|
|
43
55
|
}
|
|
44
56
|
|
|
@@ -54,7 +66,7 @@ export function commitAddKeyframe(
|
|
|
54
66
|
// at that pct) and converts a flat tween to keyframes form when needed.
|
|
55
67
|
return commit(
|
|
56
68
|
{ type: "add-keyframe", animationId, percentage, properties: { x, y } },
|
|
57
|
-
|
|
69
|
+
motionPathCommitOptions("Add keyframe", "button"),
|
|
58
70
|
);
|
|
59
71
|
}
|
|
60
72
|
|
|
@@ -65,7 +77,7 @@ export function commitRemoveWaypoint(
|
|
|
65
77
|
): Promise<void> {
|
|
66
78
|
return commit(
|
|
67
79
|
{ type: "remove-motion-path-point", animationId, index },
|
|
68
|
-
|
|
80
|
+
motionPathCommitOptions("Remove waypoint", "button"),
|
|
69
81
|
);
|
|
70
82
|
}
|
|
71
83
|
|
|
@@ -78,6 +90,6 @@ export function commitCreatePath(
|
|
|
78
90
|
): Promise<void> {
|
|
79
91
|
return commit(
|
|
80
92
|
{ type: "add-motion-path", targetSelector, position, duration: NEW_PATH_DURATION, x, y },
|
|
81
|
-
|
|
93
|
+
motionPathCommitOptions("Create motion path", "button"),
|
|
82
94
|
);
|
|
83
95
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { probeSourceElement } from "./probeSourceElement";
|
|
3
|
+
|
|
4
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
5
|
+
|
|
6
|
+
const answer = (body: unknown, ok = true) =>
|
|
7
|
+
vi.fn(() => Promise.resolve({ ok, json: () => Promise.resolve(body) }));
|
|
8
|
+
|
|
9
|
+
describe("probeSourceElement", () => {
|
|
10
|
+
it("sends the asks for one file in one tick as a single request", async () => {
|
|
11
|
+
const fetchMock = answer({ exists: [true, false, true] });
|
|
12
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
13
|
+
|
|
14
|
+
const results = await Promise.all([
|
|
15
|
+
probeSourceElement("p", "index.html", { id: "a" }),
|
|
16
|
+
probeSourceElement("p", "index.html", { id: "b" }),
|
|
17
|
+
probeSourceElement("p", "index.html", { id: "c" }),
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
expect(results).toEqual([true, false, true]);
|
|
21
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
22
|
+
const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit];
|
|
23
|
+
expect(JSON.parse(String(init.body)).targets).toEqual([{ id: "a" }, { id: "b" }, { id: "c" }]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("keeps one request per file", async () => {
|
|
27
|
+
const fetchMock = answer({ exists: [true] });
|
|
28
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
29
|
+
await Promise.all([
|
|
30
|
+
probeSourceElement("p", "index.html", { id: "a" }),
|
|
31
|
+
probeSourceElement("p", "compositions/x.html", { id: "a" }),
|
|
32
|
+
]);
|
|
33
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("reads a failed or malformed answer as existing", async () => {
|
|
37
|
+
vi.stubGlobal("fetch", answer({}, false));
|
|
38
|
+
expect(await probeSourceElement("p", "index.html", { id: "a" })).toBe(true);
|
|
39
|
+
vi.stubGlobal(
|
|
40
|
+
"fetch",
|
|
41
|
+
vi.fn(() => Promise.reject(new Error("offline"))),
|
|
42
|
+
);
|
|
43
|
+
expect(await probeSourceElement("p", "index.html", { id: "a" })).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -1,29 +1,53 @@
|
|
|
1
1
|
import { buildProjectApiPath } from "../../utils/projectRouting";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
target:
|
|
7
|
-
|
|
3
|
+
type ProbeTarget = { id?: string; hfId?: string; selector?: string; selectorIndex?: number };
|
|
4
|
+
|
|
5
|
+
interface PendingProbe {
|
|
6
|
+
target: ProbeTarget;
|
|
7
|
+
resolve: (exists: boolean) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const pending = new Map<string, PendingProbe[]>();
|
|
11
|
+
|
|
12
|
+
async function sendBatch(projectId: string, sourceFile: string, probes: PendingProbe[]) {
|
|
13
|
+
let exists: boolean[] = [];
|
|
8
14
|
try {
|
|
9
15
|
const response = await fetch(
|
|
10
16
|
buildProjectApiPath(
|
|
11
17
|
projectId,
|
|
12
|
-
`/file-mutations/probe-
|
|
18
|
+
`/file-mutations/probe-elements/${encodeURIComponent(sourceFile)}`,
|
|
13
19
|
),
|
|
14
20
|
{
|
|
15
21
|
method: "POST",
|
|
16
22
|
headers: { "Content-Type": "application/json" },
|
|
17
|
-
body: JSON.stringify({ target }),
|
|
23
|
+
body: JSON.stringify({ targets: probes.map((probe) => probe.target) }),
|
|
18
24
|
},
|
|
19
25
|
);
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
if (response.ok) {
|
|
27
|
+
const data = await response.json();
|
|
28
|
+
if (Array.isArray(data?.exists)) exists = data.exists;
|
|
29
|
+
}
|
|
30
|
+
} catch {}
|
|
31
|
+
probes.forEach((probe, index) => probe.resolve(exists[index] !== false));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function probeSourceElement(
|
|
35
|
+
projectId: string,
|
|
36
|
+
sourceFile: string,
|
|
37
|
+
target: ProbeTarget,
|
|
38
|
+
): Promise<boolean> {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
const key = `${projectId}\0${sourceFile}`;
|
|
41
|
+
let probes = pending.get(key);
|
|
42
|
+
if (!probes) {
|
|
43
|
+
const batch: PendingProbe[] = [];
|
|
44
|
+
probes = batch;
|
|
45
|
+
pending.set(key, batch);
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
pending.delete(key);
|
|
48
|
+
void sendBatch(projectId, sourceFile, batch);
|
|
49
|
+
}, 0);
|
|
24
50
|
}
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
51
|
+
probes.push({ target, resolve });
|
|
52
|
+
});
|
|
29
53
|
}
|
|
@@ -193,7 +193,7 @@ export function ColorField({
|
|
|
193
193
|
const resolveColorGestureValue = useCallback((nextValue: string) => {
|
|
194
194
|
const source = nextValue.startsWith("#") ? "hex" : "picker";
|
|
195
195
|
// Only a COMPLETE hex resolves, so a half-typed one neither previews nor
|
|
196
|
-
// commits.
|
|
196
|
+
// commits. 3 and 6 digits count as complete (the field edits RGB only): gating on
|
|
197
197
|
// 6 alone silently dropped #F00 and friends, which the old onBlur committed.
|
|
198
198
|
if (source === "hex" && !/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(nextValue)) return null;
|
|
199
199
|
const nextColor = parseCssColor(nextValue);
|
|
@@ -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>
|