@hyperframes/studio 0.8.106 → 0.8.108
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-dl6K6ABp.js → hyperframes-player-BakGew45.js} +1 -1
- package/dist/assets/{index-ooaZZhKf.js → index-C5dyUHwk.js} +1 -1
- package/dist/assets/{index-CMiYVGv5.js → index-C842tcVa.js} +203 -208
- package/dist/assets/{index-DKT5NF1Y.js → index-CPe7d_wO.js} +1 -1
- package/dist/assets/{index-lrT-gfHa.css → index-ncVJ5IVu.css} +1 -1
- package/dist/chunk-6LUNLTHR.js +611 -0
- package/dist/chunk-6LUNLTHR.js.map +1 -0
- package/dist/{chunk-ICTZJPQQ.js → chunk-AOWVEVEU.js} +3 -494
- package/dist/chunk-AOWVEVEU.js.map +1 -0
- package/dist/{chunk-46XP7J3B.js → chunk-PQFEBX32.js} +5 -3
- package/dist/chunk-PQFEBX32.js.map +1 -0
- package/dist/{domEditingLayers-AQBDFNUC.js → domEditingLayers-JQPQGGT7.js} +3 -3
- package/dist/index.d.ts +377 -12
- package/dist/index.html +2 -2
- package/dist/index.js +13445 -11874
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +8 -8
- package/src/App.tsx +6 -3
- package/src/agentToolsExport.test.ts +8 -0
- package/src/captions/keyboard.test.ts +6 -0
- package/src/components/EditorShell.tsx +2 -0
- package/src/components/StudioRightPanels.tsx +2 -0
- package/src/components/TimelineToolbar.test.tsx +27 -0
- package/src/components/TimelineToolbar.tsx +2 -5
- package/src/components/editor/CropPresetBar.tsx +69 -0
- package/src/components/editor/DomEditCropHandles.test.tsx +44 -0
- package/src/components/editor/DomEditCropHandles.tsx +123 -95
- package/src/components/editor/DomEditOverlay.tsx +12 -18
- package/src/components/editor/PropertyPanel.tsx +1 -14
- package/src/components/editor/PropertyPanelFlat.tsx +3 -6
- package/src/components/editor/SnapToolbar.test.tsx +32 -1
- package/src/components/editor/SnapToolbar.tsx +3 -4
- package/src/components/editor/carveOutput.ts +54 -0
- package/src/components/editor/cropPresetStore.ts +33 -0
- package/src/components/editor/cropPresets.test.ts +41 -0
- package/src/components/editor/cropPresets.ts +24 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +5 -2
- package/src/components/editor/domEditingLayers.test.ts +21 -0
- package/src/components/editor/domEditingLayers.ts +2 -0
- package/src/components/editor/dragUndoPaint.test.ts +71 -0
- package/src/components/editor/fontAssets.test.ts +12 -22
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +96 -33
- package/src/components/editor/gsapAnimatesProperty.ts +3 -0
- package/src/components/editor/manualEdits.test.ts +4 -3
- package/src/components/editor/manualEditsDom.ts +28 -5
- package/src/components/editor/manualEditsDomPatches.ts +5 -1
- package/src/components/editor/manualOffsetDrag.ts +15 -1
- package/src/components/editor/mediaAudioEdits.test.ts +271 -0
- package/src/components/editor/mediaAudioEdits.ts +282 -0
- package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +32 -4
- package/src/components/editor/propertyPanelAudioFxGroup.tsx +3 -1
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +290 -87
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +17 -20
- package/src/components/editor/propertyPanelFlatProps.ts +1 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +17 -27
- package/src/components/editor/propertyPanelTypes.ts +2 -0
- package/src/components/editor/useDomEditNudge.test.tsx +134 -2
- package/src/components/editor/useDomEditNudge.ts +46 -23
- package/src/components/editor/useDomEditOverlayGestures.ts +17 -6
- package/src/components/editor/useFxCarve.ts +3 -61
- package/src/components/editor/useFxCarveGrouping.test.ts +13 -0
- package/src/components/editor/useFxCarveGrouping.ts +8 -3
- package/src/components/nle/AudioMeterStrip.test.tsx +53 -0
- package/src/components/nle/AudioMeterStrip.tsx +60 -2
- package/src/components/nle/useTimelineEditCallbacks.ts +4 -0
- package/src/components/panels/SlideshowPanel.test.ts +27 -0
- package/src/components/panels/SlideshowPanel.tsx +12 -7
- package/src/contexts/DomEditContext.test.tsx +62 -0
- package/src/contexts/DomEditContext.tsx +94 -77
- package/src/contexts/TimelineEditContext.tsx +1 -0
- package/src/hooks/canvasQuickSaves.test.tsx +72 -7
- package/src/hooks/domEditCommitTypes.ts +10 -0
- package/src/hooks/domEditPersistFailure.ts +12 -1
- package/src/hooks/domStyleCommit.fonts.test.ts +5 -4
- package/src/hooks/domStyleCommit.ts +2 -8
- package/src/hooks/gsapDragCommit.ts +3 -8
- package/src/hooks/gsapDragPositionCommit.ts +6 -1
- package/src/hooks/gsapEditOutcome.ts +2 -1
- package/src/hooks/gsapResizeElementSize.test.tsx +136 -0
- package/src/hooks/gsapResizeGeometrySweep.test.ts +3 -1
- package/src/hooks/gsapResizeIntercept.test.ts +3 -11
- package/src/hooks/gsapResizeIntercept.ts +6 -2
- package/src/hooks/gsapResizeMixedTween.test.ts +1 -2
- package/src/hooks/gsapRuntimeBridge.test.ts +55 -0
- package/src/hooks/gsapRuntimeBridge.ts +5 -3
- package/src/hooks/gsapRuntimeKeyframes.ts +1 -3
- package/src/hooks/inspectorSaveReload.test.tsx +107 -0
- package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +16 -1
- package/src/hooks/useAppHotkeys.test.ts +17 -0
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -1
- package/src/hooks/useAppHotkeys.ts +3 -3
- package/src/hooks/useAutomationSelectionKeyboard.test.tsx +17 -0
- package/src/hooks/useAutomationSelectionKeyboard.ts +3 -9
- package/src/hooks/useDomEditAttributeCommits.ts +71 -29
- package/src/hooks/useDomEditCommits.test.tsx +172 -92
- package/src/hooks/useDomEditCommits.ts +2 -0
- package/src/hooks/useDomEditCommitsHelpers.ts +4 -6
- package/src/hooks/useDomEditPersist.ts +28 -13
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -3
- package/src/hooks/useDomEditTextCommits.ts +4 -8
- package/src/hooks/useDomGeometryCommits.ts +11 -2
- package/src/hooks/useEditHistoryActions.paint.test.tsx +163 -3
- package/src/hooks/useEditHistoryActions.ts +13 -4
- package/src/hooks/useElementPicker.test.ts +62 -13
- package/src/hooks/useElementPicker.ts +10 -2
- package/src/hooks/useFreezeFrame.test.ts +56 -0
- package/src/hooks/useFreezeFrame.ts +121 -0
- package/src/hooks/useGsapAwareEditing.ts +6 -6
- package/src/hooks/useGsapScriptCommits.test.tsx +19 -0
- package/src/hooks/useGsapScriptCommits.ts +5 -4
- package/src/hooks/useInlineTextEdit.test.tsx +27 -0
- package/src/hooks/useInlineTextEdit.ts +9 -7
- package/src/hooks/useKeyframeKeyboard.test.tsx +15 -0
- package/src/hooks/useKeyframeKeyboard.ts +2 -8
- package/src/hooks/usePersistentEditHistory.test.ts +9 -6
- package/src/hooks/usePersistentEditHistory.ts +1 -1
- package/src/hooks/usePreviewPersistence.ts +6 -0
- package/src/hooks/useRazorSplit.ts +14 -1
- package/src/hooks/useRenderClipContent.test.ts +102 -0
- package/src/hooks/useRenderClipContent.ts +30 -23
- package/src/hooks/useTimelineEditing.ts +2 -2
- package/src/hooks/useTrackPendingTimelineEdit.ts +2 -6
- package/src/index.ts +8 -0
- package/src/player/components/AudibleVideoClipContent.tsx +29 -0
- package/src/player/components/ClipBadges.test.tsx +82 -0
- package/src/player/components/ClipBadges.tsx +67 -0
- package/src/player/components/ClipContextMenu.tsx +22 -0
- package/src/player/components/ClipPeakMarks.test.tsx +41 -0
- package/src/player/components/ClipPeakMarks.tsx +71 -0
- package/src/player/components/Timeline.test.ts +0 -21
- package/src/player/components/Timeline.tsx +0 -1
- package/src/player/components/TimelineClip.tsx +2 -0
- package/src/player/components/TimelineFxButton.test.tsx +2 -2
- package/src/player/components/TimelineTrackHeader.test.tsx +37 -0
- package/src/player/components/TimelineTrackHeader.tsx +12 -25
- package/src/player/components/clipMenuAudioItems.test.tsx +125 -0
- package/src/player/components/clipMenuAudioItems.tsx +108 -0
- package/src/player/components/clipMenuDuck.test.ts +117 -0
- package/src/player/components/clipMenuDuck.ts +142 -0
- package/src/player/components/clipMenuNormalize.test.ts +95 -0
- package/src/player/components/clipMenuNormalize.ts +96 -0
- package/src/player/components/clipMenuToolItems.test.tsx +139 -0
- package/src/player/components/clipMenuToolItems.tsx +235 -0
- package/src/player/components/clipPeakRuns.test.ts +38 -0
- package/src/player/components/clipPeakRuns.ts +59 -0
- package/src/player/components/clipToolAttrs.test.ts +161 -0
- package/src/player/components/clipToolAttrs.ts +187 -0
- package/src/player/components/timelineCallbacks.ts +1 -0
- package/src/player/components/timelineLayout.ts +0 -28
- package/src/player/components/useClipToolState.ts +92 -0
- package/src/player/hooks/usePlaybackKeyboard.test.ts +43 -0
- package/src/player/lib/playbackShortcuts.ts +3 -7
- package/src/player/lib/timelineElementHelpers.test.ts +16 -0
- package/src/player/lib/timelineElementHelpers.ts +25 -2
- package/src/player/lib/timelineViewportBudgets.test.ts +2 -0
- package/src/player/lib/timelineViewportBudgets.ts +4 -0
- package/src/timelineChromeExports.test.tsx +7 -0
- package/src/utils/audioMeterMath.test.ts +37 -0
- package/src/utils/audioMeterMath.ts +9 -3
- package/src/utils/exportPeakCeiling.ts +1 -0
- package/src/utils/gsapSoftReload.ts +3 -2
- package/src/utils/gsapUndoRestore.browser.test.ts +94 -0
- package/src/utils/gsapUndoRestore.test.ts +145 -1
- package/src/utils/gsapUndoRestore.ts +70 -21
- package/src/utils/previewRelativePath.ts +19 -0
- package/src/utils/sourcePatcher.ts +2 -2
- package/src/utils/studioFontHelpers.ts +2 -23
- package/src/utils/studioHelpers.test.ts +30 -0
- package/src/utils/studioHelpers.ts +18 -4
- package/src/utils/studioPendingEdits.test.ts +129 -0
- package/src/utils/studioPendingEdits.ts +61 -18
- package/src/utils/timelineInspector.test.ts +33 -2
- package/src/utils/timelineInspector.ts +16 -4
- package/src/utils/typingTarget.test.ts +13 -0
- package/src/utils/typingTarget.ts +5 -2
- package/src/webmcp/useStudioAgentTools.ts +3 -1
- package/dist/chunk-46XP7J3B.js.map +0 -1
- package/dist/chunk-DI4LCJHL.js +0 -102
- package/dist/chunk-DI4LCJHL.js.map +0 -1
- package/dist/chunk-ICTZJPQQ.js.map +0 -1
- /package/dist/{domEditingLayers-AQBDFNUC.js.map → domEditingLayers-JQPQGGT7.js.map} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
+
import { isVideoAudible } from "../../player/lib/timelineElementHelpers";
|
|
2
3
|
import { Check, ClipboardList, Film, Music, Scissors } from "../../icons/SystemIcons";
|
|
3
4
|
import type { DomEditSelection } from "./domEditing";
|
|
4
5
|
import {
|
|
@@ -22,23 +23,29 @@ import {
|
|
|
22
23
|
audioGainToFaderPosition,
|
|
23
24
|
audioGainToText,
|
|
24
25
|
} from "@hyperframes/core/audio-gain";
|
|
26
|
+
import type { CommitDomAttributeBatch } from "../../hooks/domEditCommitTypes";
|
|
27
|
+
import { commitCutout, commitHasAudioToggle, commitMutedToggle } from "./mediaAudioEdits";
|
|
25
28
|
|
|
26
29
|
// fallow-ignore-next-line complexity
|
|
27
30
|
export function MediaSection({
|
|
31
|
+
projectId = null,
|
|
28
32
|
projectDir,
|
|
29
33
|
element,
|
|
30
34
|
styles,
|
|
31
35
|
onSetStyle,
|
|
32
36
|
onSetAttribute,
|
|
33
37
|
onSetHtmlAttribute,
|
|
38
|
+
onSetAttributeBatch,
|
|
34
39
|
onRemoveBackground,
|
|
35
40
|
}: {
|
|
41
|
+
projectId?: string | null;
|
|
36
42
|
projectDir: string | null;
|
|
37
43
|
element: DomEditSelection;
|
|
38
44
|
styles: Record<string, string>;
|
|
39
45
|
onSetStyle: (prop: string, value: string) => void | Promise<unknown>;
|
|
40
46
|
onSetAttribute: (attr: string, value: string) => void | Promise<void>;
|
|
41
47
|
onSetHtmlAttribute: (attr: string, value: string | null) => void | Promise<void>;
|
|
48
|
+
onSetAttributeBatch: CommitDomAttributeBatch;
|
|
42
49
|
onRemoveBackground?: (
|
|
43
50
|
inputPath: string,
|
|
44
51
|
options: {
|
|
@@ -62,7 +69,11 @@ export function MediaSection({
|
|
|
62
69
|
|
|
63
70
|
const hasLoop = el.hasAttribute("loop");
|
|
64
71
|
const hasMuted = el.hasAttribute("muted");
|
|
65
|
-
const hasAudio =
|
|
72
|
+
const hasAudio = isVideoAudible({
|
|
73
|
+
tag: el.tagName,
|
|
74
|
+
hasAudioAttr: element.dataAttributes["has-audio"],
|
|
75
|
+
muted: el.hasAttribute("muted"),
|
|
76
|
+
});
|
|
66
77
|
|
|
67
78
|
const playbackRate = Number.parseFloat(element.dataAttributes["playback-rate"] ?? "1") || 1;
|
|
68
79
|
|
|
@@ -96,13 +107,7 @@ export function MediaSection({
|
|
|
96
107
|
setCreatePlate(false);
|
|
97
108
|
}, [srcAttr]);
|
|
98
109
|
|
|
99
|
-
const
|
|
100
|
-
await onSetHtmlAttribute("src", result.outputPath);
|
|
101
|
-
if (isVideo) {
|
|
102
|
-
await onSetAttribute("has-audio", "");
|
|
103
|
-
await onSetHtmlAttribute("muted", "true");
|
|
104
|
-
}
|
|
105
|
-
};
|
|
110
|
+
const mediaEdit = { element, projectId, projectSrc, commit: onSetAttributeBatch };
|
|
106
111
|
|
|
107
112
|
const runBackgroundRemoval = async () => {
|
|
108
113
|
if (!onRemoveBackground || !projectSrc || removeBusy) return;
|
|
@@ -115,13 +120,8 @@ export function MediaSection({
|
|
|
115
120
|
quality,
|
|
116
121
|
onProgress: setRemoveProgress,
|
|
117
122
|
});
|
|
118
|
-
await
|
|
119
|
-
setRemoveProgress({
|
|
120
|
-
status: "complete",
|
|
121
|
-
progress: 100,
|
|
122
|
-
stage: "Applied cutout",
|
|
123
|
-
...result,
|
|
124
|
-
});
|
|
123
|
+
const stage = await commitCutout(mediaEdit, result.outputPath, hasAudio);
|
|
124
|
+
setRemoveProgress({ status: "complete", progress: 100, stage, ...result });
|
|
125
125
|
} catch (error) {
|
|
126
126
|
setRemoveProgress({
|
|
127
127
|
status: "failed",
|
|
@@ -320,9 +320,7 @@ export function MediaSection({
|
|
|
320
320
|
<SegmentedControl
|
|
321
321
|
trackName="Muted"
|
|
322
322
|
value={hasMuted ? "on" : "off"}
|
|
323
|
-
onChange={(next) =>
|
|
324
|
-
void onSetHtmlAttribute("muted", next === "on" ? "true" : null);
|
|
325
|
-
}}
|
|
323
|
+
onChange={(next) => void commitMutedToggle(mediaEdit, next === "on")}
|
|
326
324
|
options={[
|
|
327
325
|
{ label: "On", value: "on" },
|
|
328
326
|
{ label: "Off", value: "off" },
|
|
@@ -337,15 +335,7 @@ export function MediaSection({
|
|
|
337
335
|
<SegmentedControl
|
|
338
336
|
trackName="Has audio track"
|
|
339
337
|
value={hasAudio ? "yes" : "no"}
|
|
340
|
-
onChange={(next) =>
|
|
341
|
-
if (next === "yes") {
|
|
342
|
-
void onSetAttribute("has-audio", "true");
|
|
343
|
-
void onSetHtmlAttribute("muted", null);
|
|
344
|
-
} else {
|
|
345
|
-
void onSetAttribute("has-audio", "");
|
|
346
|
-
void onSetHtmlAttribute("muted", "true");
|
|
347
|
-
}
|
|
348
|
-
}}
|
|
338
|
+
onChange={(next) => void commitHasAudioToggle(mediaEdit, next === "yes")}
|
|
349
339
|
options={[
|
|
350
340
|
{ label: "Yes", value: "yes" },
|
|
351
341
|
{ label: "No", value: "no" },
|
|
@@ -3,6 +3,7 @@ import type { ArcPathSegment, GsapAnimation } from "@hyperframes/parsers/gsap-pa
|
|
|
3
3
|
import type { DomEditSelection } from "./domEditing";
|
|
4
4
|
import type { ImportedFontAsset } from "./fontAssets";
|
|
5
5
|
import type { GsapAnimationEditCallbacks } from "./gsapAnimationCallbacks";
|
|
6
|
+
import type { CommitDomAttributeBatch } from "../../hooks/domEditCommitTypes";
|
|
6
7
|
|
|
7
8
|
export interface BackgroundRemovalProgress {
|
|
8
9
|
status: "processing" | "complete" | "failed";
|
|
@@ -68,6 +69,7 @@ export interface PropertyPanelProps {
|
|
|
68
69
|
value: string | null,
|
|
69
70
|
) => Promise<{ changedFiles: number; changedElements: number }>;
|
|
70
71
|
onSetHtmlAttribute: (attr: string, value: string | null) => void | Promise<void>;
|
|
72
|
+
onSetAttributeBatch: CommitDomAttributeBatch;
|
|
71
73
|
onRemoveBackground?: (
|
|
72
74
|
inputPath: string,
|
|
73
75
|
options: {
|
|
@@ -6,6 +6,7 @@ import { installReactActEnvironment, makeSelection } from "../../hooks/domSelect
|
|
|
6
6
|
import { useDomEditNudge, type UseDomEditNudgeParams } from "./useDomEditNudge";
|
|
7
7
|
import { CANVAS_NUDGE_COMMIT_DEBOUNCE_MS, CANVAS_NUDGE_STEP_PX } from "./domEditNudge";
|
|
8
8
|
import { __resetForTests } from "../../utils/canvasNudgeGate";
|
|
9
|
+
import { flushStudioPendingEdits, hasStudioPendingEdits } from "../../utils/studioPendingEdits";
|
|
9
10
|
import type { DomEditSelection } from "./domEditing";
|
|
10
11
|
import type { OverlayRect } from "./domEditOverlayGeometry";
|
|
11
12
|
|
|
@@ -27,6 +28,7 @@ const REST_RECT: OverlayRect = {
|
|
|
27
28
|
// Stable across renders on purpose: the test targets the `selection` identity
|
|
28
29
|
// key specifically, so `groupSelections` must not itself be a source of churn.
|
|
29
30
|
const EMPTY_GROUP_SELECTIONS: DomEditSelection[] = [];
|
|
31
|
+
let flushNudge = () => {};
|
|
30
32
|
|
|
31
33
|
function Harness({
|
|
32
34
|
selection,
|
|
@@ -35,7 +37,7 @@ function Harness({
|
|
|
35
37
|
selection: DomEditSelection | null;
|
|
36
38
|
onPathOffsetCommit: UseDomEditNudgeParams["onPathOffsetCommitRef"]["current"];
|
|
37
39
|
}) {
|
|
38
|
-
useDomEditNudge({
|
|
40
|
+
flushNudge = useDomEditNudge({
|
|
39
41
|
selection,
|
|
40
42
|
groupSelections: EMPTY_GROUP_SELECTIONS,
|
|
41
43
|
allowCanvasMovement: true,
|
|
@@ -49,7 +51,7 @@ function Harness({
|
|
|
49
51
|
onBlockedMoveRef: makeRef(() => {}),
|
|
50
52
|
onPathOffsetCommitRef: makeRef(onPathOffsetCommit),
|
|
51
53
|
onGroupPathOffsetCommitRef: makeRef(async () => {}),
|
|
52
|
-
});
|
|
54
|
+
}).flushNudge;
|
|
53
55
|
return null;
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -259,6 +261,51 @@ describe("useDomEditNudge — selection cleanup keyed on stable identity", () =>
|
|
|
259
261
|
});
|
|
260
262
|
});
|
|
261
263
|
|
|
264
|
+
describe("useDomEditNudge — a focused native player owns the arrow keys", () => {
|
|
265
|
+
beforeEach(() => {
|
|
266
|
+
vi.useFakeTimers();
|
|
267
|
+
__resetForTests();
|
|
268
|
+
});
|
|
269
|
+
let root: ReturnType<typeof createRoot> | null = null;
|
|
270
|
+
afterEach(() => {
|
|
271
|
+
act(() => root?.unmount());
|
|
272
|
+
root = null;
|
|
273
|
+
document.body.innerHTML = "";
|
|
274
|
+
vi.useRealTimers();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it.each(["video", "audio"])(
|
|
278
|
+
"does not nudge the selection while a <%s controls> has focus",
|
|
279
|
+
(tag) => {
|
|
280
|
+
root = createRoot(document.body.appendChild(document.createElement("div")));
|
|
281
|
+
const element = document.body.appendChild(document.createElement("div"));
|
|
282
|
+
element.id = "dot";
|
|
283
|
+
const player = document.body.appendChild(document.createElement(tag));
|
|
284
|
+
player.setAttribute("controls", "");
|
|
285
|
+
const commit = vi.fn();
|
|
286
|
+
act(() => {
|
|
287
|
+
root?.render(
|
|
288
|
+
React.createElement(Harness, {
|
|
289
|
+
selection: makeSelection("Dot", element),
|
|
290
|
+
onPathOffsetCommit: commit,
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
const event = new KeyboardEvent("keydown", {
|
|
295
|
+
key: "ArrowRight",
|
|
296
|
+
bubbles: true,
|
|
297
|
+
cancelable: true,
|
|
298
|
+
});
|
|
299
|
+
act(() => {
|
|
300
|
+
player.dispatchEvent(event);
|
|
301
|
+
vi.advanceTimersByTime(CANVAS_NUDGE_COMMIT_DEBOUNCE_MS + 10);
|
|
302
|
+
});
|
|
303
|
+
expect(event.defaultPrevented).toBe(false);
|
|
304
|
+
expect(commit).not.toHaveBeenCalled();
|
|
305
|
+
},
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
262
309
|
describe("useDomEditNudge carries the route its press chose", () => {
|
|
263
310
|
beforeEach(() => {
|
|
264
311
|
vi.useFakeTimers();
|
|
@@ -291,3 +338,88 @@ describe("useDomEditNudge carries the route its press chose", () => {
|
|
|
291
338
|
element.remove();
|
|
292
339
|
});
|
|
293
340
|
});
|
|
341
|
+
|
|
342
|
+
describe("useDomEditNudge — undo right after a burst", () => {
|
|
343
|
+
it("undo's drain commits a burst still inside its debounce and waits for its save", async () => {
|
|
344
|
+
__resetForTests();
|
|
345
|
+
vi.useFakeTimers();
|
|
346
|
+
const root = createRoot(document.body.appendChild(document.createElement("div")));
|
|
347
|
+
const element = document.body.appendChild(document.createElement("div"));
|
|
348
|
+
element.id = "dot-undo";
|
|
349
|
+
let saved!: () => void;
|
|
350
|
+
const commit = vi.fn(() => new Promise<void>((resolve) => (saved = resolve)));
|
|
351
|
+
act(() => {
|
|
352
|
+
root.render(
|
|
353
|
+
React.createElement(Harness, {
|
|
354
|
+
selection: makeSelection("Dot", element),
|
|
355
|
+
onPathOffsetCommit: commit,
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
act(() => dispatchArrowRight());
|
|
360
|
+
|
|
361
|
+
let drained = false;
|
|
362
|
+
const drain = flushStudioPendingEdits().then(() => (drained = true));
|
|
363
|
+
expect(commit).toHaveBeenCalledTimes(1);
|
|
364
|
+
vi.useRealTimers();
|
|
365
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
366
|
+
expect(drained).toBe(false);
|
|
367
|
+
saved();
|
|
368
|
+
await drain;
|
|
369
|
+
expect(drained).toBe(true);
|
|
370
|
+
act(() => root.unmount());
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("counts the burst as a pending edit from its first key until its save lands", async () => {
|
|
374
|
+
__resetForTests();
|
|
375
|
+
vi.useFakeTimers();
|
|
376
|
+
const root = createRoot(document.body.appendChild(document.createElement("div")));
|
|
377
|
+
const element = document.body.appendChild(document.createElement("div"));
|
|
378
|
+
element.id = "dot-pending";
|
|
379
|
+
let saved!: () => void;
|
|
380
|
+
const commit = vi.fn(() => new Promise<void>((resolve) => (saved = resolve)));
|
|
381
|
+
act(() => {
|
|
382
|
+
root.render(
|
|
383
|
+
React.createElement(Harness, {
|
|
384
|
+
selection: makeSelection("Dot", element),
|
|
385
|
+
onPathOffsetCommit: commit,
|
|
386
|
+
}),
|
|
387
|
+
);
|
|
388
|
+
});
|
|
389
|
+
expect(hasStudioPendingEdits()).toBe(false);
|
|
390
|
+
act(() => dispatchArrowRight());
|
|
391
|
+
expect(hasStudioPendingEdits()).toBe(true);
|
|
392
|
+
act(() => vi.advanceTimersByTime(CANVAS_NUDGE_COMMIT_DEBOUNCE_MS + 10));
|
|
393
|
+
expect(commit).toHaveBeenCalledTimes(1);
|
|
394
|
+
expect(hasStudioPendingEdits()).toBe(true);
|
|
395
|
+
vi.useRealTimers();
|
|
396
|
+
saved();
|
|
397
|
+
await vi.waitFor(() => expect(hasStudioPendingEdits()).toBe(false));
|
|
398
|
+
act(() => root.unmount());
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe("useDomEditNudge — a commit that throws", () => {
|
|
403
|
+
it("still ends the burst's pending edit, so undo and export never wait on it", async () => {
|
|
404
|
+
__resetForTests();
|
|
405
|
+
const root = createRoot(document.body.appendChild(document.createElement("div")));
|
|
406
|
+
const element = document.body.appendChild(document.createElement("div"));
|
|
407
|
+
element.id = "dot-throws";
|
|
408
|
+
const commit = vi.fn(() => {
|
|
409
|
+
throw new Error("The commit threw.");
|
|
410
|
+
});
|
|
411
|
+
act(() => {
|
|
412
|
+
root.render(
|
|
413
|
+
React.createElement(Harness, {
|
|
414
|
+
selection: makeSelection("Dot", element),
|
|
415
|
+
onPathOffsetCommit: commit,
|
|
416
|
+
}),
|
|
417
|
+
);
|
|
418
|
+
});
|
|
419
|
+
act(() => dispatchArrowRight());
|
|
420
|
+
expect(hasStudioPendingEdits()).toBe(true);
|
|
421
|
+
expect(() => flushNudge()).toThrow("The commit threw.");
|
|
422
|
+
await vi.waitFor(() => expect(hasStudioPendingEdits()).toBe(false));
|
|
423
|
+
act(() => root.unmount());
|
|
424
|
+
});
|
|
425
|
+
});
|
|
@@ -9,6 +9,10 @@ import { useCallback, useEffect, useRef, type RefObject } from "react";
|
|
|
9
9
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
10
10
|
import { ownsPlainKeys } from "../../utils/typingTarget";
|
|
11
11
|
import { acquireCanvasNudgeKeys } from "../../utils/canvasNudgeGate";
|
|
12
|
+
import {
|
|
13
|
+
addStudioPendingEditFlushListener,
|
|
14
|
+
beginStudioPendingEdit,
|
|
15
|
+
} from "../../utils/studioPendingEdits";
|
|
12
16
|
import type { DomEditSelection } from "./domEditing";
|
|
13
17
|
import {
|
|
14
18
|
type GroupOverlayItem,
|
|
@@ -27,6 +31,7 @@ import {
|
|
|
27
31
|
applyManualOffsetNudgeDraft,
|
|
28
32
|
createManualOffsetDragMember,
|
|
29
33
|
endManualOffsetDragMembers,
|
|
34
|
+
manualOffsetMoveRevert,
|
|
30
35
|
restoreManualOffsetDragMembers,
|
|
31
36
|
type ManualOffsetDragMember,
|
|
32
37
|
} from "./manualOffsetDrag";
|
|
@@ -43,6 +48,7 @@ interface NudgeSession {
|
|
|
43
48
|
/** Accumulated delta of the burst, in composition px. */
|
|
44
49
|
accum: { x: number; y: number };
|
|
45
50
|
timer: ReturnType<typeof setTimeout> | null;
|
|
51
|
+
edit: ReturnType<typeof beginStudioPendingEdit>;
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
export interface UseDomEditNudgeParams {
|
|
@@ -127,7 +133,7 @@ function resolveSingleNudgeTarget(
|
|
|
127
133
|
|
|
128
134
|
/**
|
|
129
135
|
* True when a keydown must not start/extend a nudge: canvas movement disabled,
|
|
130
|
-
* a pointer gesture already owns the element, or
|
|
136
|
+
* a pointer gesture already owns the element, or focus is in a field or a native player.
|
|
131
137
|
*/
|
|
132
138
|
function shouldIgnoreNudgeKey(p: UseDomEditNudgeParams, event: KeyboardEvent): boolean {
|
|
133
139
|
if (!p.allowCanvasMovement || event.defaultPrevented) return true;
|
|
@@ -143,31 +149,42 @@ export function useDomEditNudge(params: UseDomEditNudgeParams): { flushNudge: ()
|
|
|
143
149
|
// Commit the pending burst: one source write per burst = one undo entry.
|
|
144
150
|
// Mirrors the drag's onPointerUp — same commit callbacks, same failure
|
|
145
151
|
// restore, same member teardown.
|
|
146
|
-
const commitSession = () => {
|
|
152
|
+
const commitSession = (): Promise<unknown> | undefined => {
|
|
147
153
|
const session = sessionRef.current;
|
|
148
|
-
if (!session) return;
|
|
154
|
+
if (!session) return undefined;
|
|
149
155
|
sessionRef.current = null;
|
|
150
156
|
if (session.timer) clearTimeout(session.timer);
|
|
151
|
-
const updates: DomEditGroupPathOffsetCommit[] = session.members.map((member) => ({
|
|
152
|
-
selection: member.selection,
|
|
153
|
-
next: applyManualOffsetNudgeCommit(member, session.accum),
|
|
154
|
-
plainTranslate: member.plainTranslate,
|
|
155
|
-
}));
|
|
156
157
|
const p = paramsRef.current;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
158
|
+
let saved: Promise<unknown> | undefined;
|
|
159
|
+
try {
|
|
160
|
+
const updates: DomEditGroupPathOffsetCommit[] = session.members.map((member) => ({
|
|
161
|
+
selection: member.selection,
|
|
162
|
+
next: applyManualOffsetNudgeCommit(member, session.accum),
|
|
163
|
+
plainTranslate: member.plainTranslate,
|
|
164
|
+
}));
|
|
165
|
+
const commit = session.edit.adopt(() =>
|
|
166
|
+
session.isGroup
|
|
167
|
+
? p.onGroupPathOffsetCommitRef.current(updates)
|
|
168
|
+
: p.onPathOffsetCommitRef.current(updates[0].selection, updates[0].next, {
|
|
169
|
+
plainTranslate: updates[0].plainTranslate,
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
if (session.edit.reverted())
|
|
173
|
+
for (const member of session.members)
|
|
174
|
+
restoreStudioPathOffset(member.element, member.initialPathOffset);
|
|
175
|
+
saved = Promise.resolve(commit)
|
|
176
|
+
.catch(() => {
|
|
177
|
+
for (const member of session.members) {
|
|
178
|
+
if (isStudioManualEditGestureCurrent(member.element, member.gestureToken)) {
|
|
179
|
+
restoreStudioPathOffset(member.element, member.initialPathOffset);
|
|
180
|
+
}
|
|
167
181
|
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
182
|
+
})
|
|
183
|
+
.finally(() => endManualOffsetDragMembers(session.members));
|
|
184
|
+
return saved;
|
|
185
|
+
} finally {
|
|
186
|
+
session.edit.settle(saved);
|
|
187
|
+
}
|
|
171
188
|
};
|
|
172
189
|
const commitSessionRef = useRef(commitSession);
|
|
173
190
|
commitSessionRef.current = commitSession;
|
|
@@ -179,6 +196,7 @@ export function useDomEditNudge(params: UseDomEditNudgeParams): { flushNudge: ()
|
|
|
179
196
|
if (session.timer) clearTimeout(session.timer);
|
|
180
197
|
restoreManualOffsetDragMembers(session.members);
|
|
181
198
|
endManualOffsetDragMembers(session.members);
|
|
199
|
+
session.edit.settle();
|
|
182
200
|
}, [params.allowCanvasMovement]);
|
|
183
201
|
|
|
184
202
|
// Build drag members for the current target set — the same member snapshot a
|
|
@@ -205,7 +223,8 @@ export function useDomEditNudge(params: UseDomEditNudgeParams): { flushNudge: ()
|
|
|
205
223
|
if (members.length === 0) return null;
|
|
206
224
|
// Same side effect a drag start has (pauses preview playback).
|
|
207
225
|
p.onManualDragStartRef.current?.();
|
|
208
|
-
|
|
226
|
+
const edit = beginStudioPendingEdit(manualOffsetMoveRevert(members));
|
|
227
|
+
return { members, isGroup, accum: { x: 0, y: 0 }, timer: null, edit };
|
|
209
228
|
};
|
|
210
229
|
|
|
211
230
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
@@ -231,8 +250,12 @@ export function useDomEditNudge(params: UseDomEditNudgeParams): { flushNudge: ()
|
|
|
231
250
|
// Capture, like the other app-level key handlers, so a focused panel
|
|
232
251
|
// can't swallow the nudge before it reaches us.
|
|
233
252
|
window.addEventListener("keydown", listener, true);
|
|
253
|
+
const stopCommitOnUndoDrain = addStudioPendingEditFlushListener(() =>
|
|
254
|
+
commitSessionRef.current(),
|
|
255
|
+
);
|
|
234
256
|
return () => {
|
|
235
257
|
window.removeEventListener("keydown", listener, true);
|
|
258
|
+
stopCommitOnUndoDrain();
|
|
236
259
|
commitSessionRef.current();
|
|
237
260
|
};
|
|
238
261
|
});
|
|
@@ -245,7 +268,7 @@ export function useDomEditNudge(params: UseDomEditNudgeParams): { flushNudge: ()
|
|
|
245
268
|
const selectionKey = selectionIdentityKey(params.selection);
|
|
246
269
|
const groupSelectionsKey = groupSelectionsIdentityKey(params.groupSelections);
|
|
247
270
|
// eslint-disable-next-line no-restricted-syntax
|
|
248
|
-
useEffect(() => () => commitSessionRef.current(), [selectionKey, groupSelectionsKey]);
|
|
271
|
+
useEffect(() => () => void commitSessionRef.current(), [selectionKey, groupSelectionsKey]);
|
|
249
272
|
|
|
250
273
|
// Claim the arrow keys from the playback frame-step while the selection is
|
|
251
274
|
// nudgeable (see canvasNudgeGate — listener order is mount-dependent, so
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
applyManualOffsetDragDraft,
|
|
12
12
|
endManualOffsetDragMembers,
|
|
13
13
|
restoreManualOffsetDragMembers,
|
|
14
|
+
manualOffsetMoveRevert,
|
|
14
15
|
} from "./manualOffsetDrag";
|
|
15
16
|
import { applyRotationDraft, restoreRotationDraft } from "./rotationDraft";
|
|
16
17
|
import {
|
|
@@ -57,6 +58,7 @@ import { logResize, logResizeMove, logResizeSettle } from "../../utils/resizeDeb
|
|
|
57
58
|
import { logDrag, logDragSettle, readDragPositions } from "../../utils/dragDebug";
|
|
58
59
|
import { createGroupDragMover } from "./groupDragMove";
|
|
59
60
|
import { DomEditSaveQueueOpenError } from "../../utils/domEditSaveQueue";
|
|
61
|
+
import { beginStudioPendingEdit } from "../../utils/studioPendingEdits";
|
|
60
62
|
|
|
61
63
|
function isTap(g: { startX: number; startY: number; travelled?: boolean }, e: React.PointerEvent) {
|
|
62
64
|
return (
|
|
@@ -344,7 +346,10 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
344
346
|
),
|
|
345
347
|
at: readDragPositions(groupG.members),
|
|
346
348
|
});
|
|
347
|
-
|
|
349
|
+
const groupEdit = beginStudioPendingEdit(manualOffsetMoveRevert(groupG.members));
|
|
350
|
+
const groupSaved = Promise.resolve(
|
|
351
|
+
groupEdit.adopt(() => opts.onGroupPathOffsetCommitRef.current(updates)),
|
|
352
|
+
)
|
|
348
353
|
.catch(() => {
|
|
349
354
|
for (const member of groupG.members) {
|
|
350
355
|
if (
|
|
@@ -363,6 +368,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
363
368
|
// so this is where a snap-back would show.
|
|
364
369
|
logDragSettle("settle", groupG.members);
|
|
365
370
|
});
|
|
371
|
+
groupEdit.settle(groupSaved);
|
|
366
372
|
return;
|
|
367
373
|
}
|
|
368
374
|
|
|
@@ -469,11 +475,15 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
469
475
|
box.style.left = `${nextBoxLeft}px`;
|
|
470
476
|
box.style.top = `${nextBoxTop}px`;
|
|
471
477
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
478
|
+
const member = g.pathOffsetMember;
|
|
479
|
+
const edit = beginStudioPendingEdit(manualOffsetMoveRevert([member]));
|
|
480
|
+
const saved = Promise.resolve(
|
|
481
|
+
edit.adopt(() =>
|
|
482
|
+
opts.onPathOffsetCommitRef.current(sel, finalOffset, {
|
|
483
|
+
altKey: e.altKey,
|
|
484
|
+
plainTranslate: member.plainTranslate,
|
|
485
|
+
}),
|
|
486
|
+
),
|
|
477
487
|
)
|
|
478
488
|
.catch(() => {
|
|
479
489
|
if (
|
|
@@ -485,6 +495,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
485
495
|
.finally(() => {
|
|
486
496
|
if (g.pathOffsetMember) endManualOffsetDragMembers([g.pathOffsetMember]);
|
|
487
497
|
});
|
|
498
|
+
edit.settle(saved);
|
|
488
499
|
} else {
|
|
489
500
|
opts.suppressNextBoxClickRef.current = true;
|
|
490
501
|
const finalSize = readStudioBoxSize(sel.element);
|
|
@@ -29,11 +29,11 @@ import {
|
|
|
29
29
|
resolveNextCarveSettings,
|
|
30
30
|
} from "./useFxCarveGrouping.js";
|
|
31
31
|
import { type HfAutomation } from "@hyperframes/core/audio-automation";
|
|
32
|
-
import { automationAttrValue, HF_AUDIO_AUTOMATION_ATTR } from "./propertyPanelAutomation";
|
|
33
32
|
import { trackCarveChanged } from "./audioFxTelemetry.js";
|
|
34
33
|
import type { DomEditSelection } from "./domEditingTypes";
|
|
35
34
|
import { usePlayerStore } from "../../player";
|
|
36
|
-
import {
|
|
35
|
+
import { dropCarveOutput, resolveCarveVoices, withoutCarveLanes } from "./carveOutput.js";
|
|
36
|
+
import { carveLanes, measureCarve, mintCarveNodes } from "./useFxCarveNodes.js";
|
|
37
37
|
import { readClipClock } from "./clipAudioClock.js";
|
|
38
38
|
import { spanOf } from "./propertyPanelAudioFxGroupUtils.js";
|
|
39
39
|
import type { AudioTrackOption } from "./propertyPanelFxCarveModule.js";
|
|
@@ -55,16 +55,6 @@ function carveAction(
|
|
|
55
55
|
return "strength";
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/** Lanes belonging to nodes the carve generated, which a re-run replaces. */
|
|
59
|
-
function withoutCarveLanes(automation: HfAutomation, chain: HfAudioFxChain): HfAutomation {
|
|
60
|
-
const prefixes = chain.nodes.filter((n) => n.fromCarve && n.id).map((n) => `fx.${n.id}.`);
|
|
61
|
-
if (prefixes.length === 0) return automation;
|
|
62
|
-
return {
|
|
63
|
-
version: automation.version,
|
|
64
|
-
lanes: automation.lanes.filter((lane) => !prefixes.some((p) => lane.target.startsWith(p))),
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
58
|
/**
|
|
69
59
|
* Every setting here describes the filters, so changing one rebuilds them.
|
|
70
60
|
* There is no apply button: a carve naming a voice with no filters behind it
|
|
@@ -86,54 +76,6 @@ function carveNeedsReanalysis(
|
|
|
86
76
|
);
|
|
87
77
|
}
|
|
88
78
|
|
|
89
|
-
/**
|
|
90
|
-
* Every named voice that is actually there with something to decode. A source
|
|
91
|
-
* naming a deleted track is skipped rather than failing the whole analysis.
|
|
92
|
-
*
|
|
93
|
-
* Read out to plain values here rather than carrying elements around: it is
|
|
94
|
-
* what lets the src and the start be non-null by construction downstream
|
|
95
|
-
* instead of by assertion.
|
|
96
|
-
*/
|
|
97
|
-
function resolveCarveVoices(doc: Document, sources: readonly string[]): CarveClip[] {
|
|
98
|
-
const voices: CarveClip[] = [];
|
|
99
|
-
for (const id of sources) {
|
|
100
|
-
const el = doc.getElementById(id);
|
|
101
|
-
// By tag name, not `instanceof HTMLAudioElement`: these elements belong to
|
|
102
|
-
// the composition's iframe document, so the constructor they were made
|
|
103
|
-
// from is not this realm's and the instanceof is false for every one.
|
|
104
|
-
if (el?.tagName !== "AUDIO") continue;
|
|
105
|
-
const src = el.getAttribute("src");
|
|
106
|
-
if (!src) continue;
|
|
107
|
-
const clock = readClipClock((name) => el.getAttribute(name));
|
|
108
|
-
voices.push({ src, start: el.getAttribute("data-start"), clock });
|
|
109
|
-
}
|
|
110
|
-
return voices;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* What the carve generated is only justified by the voices it was measured
|
|
115
|
-
* from: switched off, or left naming none — every source deleted, say — there
|
|
116
|
-
* is nothing those filters are making room for. Left behind they keep dipping
|
|
117
|
-
* the bed with nothing in the panel to explain them.
|
|
118
|
-
*/
|
|
119
|
-
async function dropCarveOutput(
|
|
120
|
-
chain: HfAudioFxChain,
|
|
121
|
-
automation: HfAutomation,
|
|
122
|
-
onSetAttributeQuiet: (attr: string, value: string | null) => void | Promise<void>,
|
|
123
|
-
): Promise<void> {
|
|
124
|
-
const carriedOver = withoutCarveLanes(automation, chain);
|
|
125
|
-
if (carriedOver.lanes.length !== automation.lanes.length) {
|
|
126
|
-
await onSetAttributeQuiet(HF_AUDIO_AUTOMATION_ATTR, automationAttrValue(carriedOver) || null);
|
|
127
|
-
}
|
|
128
|
-
const kept = chain.nodes.filter((n) => !n.fromCarve);
|
|
129
|
-
if (kept.length !== chain.nodes.length) {
|
|
130
|
-
await onSetAttributeQuiet(
|
|
131
|
-
HF_AUDIO_FX_ATTR,
|
|
132
|
-
kept.length ? serializeAudioFxChain({ version: 1, nodes: kept }) : null,
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
79
|
export function useFxCarve(
|
|
138
80
|
element: DomEditSelection,
|
|
139
81
|
chain: HfAudioFxChain,
|
|
@@ -179,7 +121,7 @@ export function useFxCarve(
|
|
|
179
121
|
} => {
|
|
180
122
|
const doc = element.element?.ownerDocument;
|
|
181
123
|
if (!doc || !couldBeBed) return { sourceOptions: [], autoSourceIds: [] };
|
|
182
|
-
const others = Array.from(doc.querySelectorAll
|
|
124
|
+
const others = Array.from(doc.querySelectorAll("audio[id], video[id]")).filter(
|
|
183
125
|
(a) => a.id !== element.id,
|
|
184
126
|
);
|
|
185
127
|
// Only tracks that are actually playing while this bed is. A voice somewhere
|
|
@@ -54,6 +54,19 @@ describe("collectCarveCandidates", () => {
|
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
describe("collectCarveCandidates with video", () => {
|
|
58
|
+
it("offers an ungrouped audible video, not a muted one", () => {
|
|
59
|
+
const doc = previewDoc(`
|
|
60
|
+
<video id="a-roll"></video>
|
|
61
|
+
<video id="b-roll" muted></video>
|
|
62
|
+
<audio id="bed"></audio>
|
|
63
|
+
`);
|
|
64
|
+
const others = Array.from(doc.querySelectorAll("video[id]"));
|
|
65
|
+
const ids = collectCarveCandidates(doc, others, () => true, "bed").map((c) => c.id);
|
|
66
|
+
expect(ids).toEqual(["a-roll"]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
57
70
|
describe("carverAgainst", () => {
|
|
58
71
|
// The far-end guard has to see through a GROUP source. A plural carve names a
|
|
59
72
|
// group — that is what the lint rule pushes authors toward — so matching raw
|
|
@@ -20,6 +20,11 @@ import {
|
|
|
20
20
|
resolveAudioGroups,
|
|
21
21
|
resolveCarveSourceIds,
|
|
22
22
|
} from "@hyperframes/core/audio-groups";
|
|
23
|
+
import { isAudibleVideoNode } from "../../player/lib/timelineElementHelpers";
|
|
24
|
+
|
|
25
|
+
export function isCarveVoiceElement(el: Element | null): el is Element {
|
|
26
|
+
return el !== null && (el.tagName === "AUDIO" || isAudibleVideoNode(el));
|
|
27
|
+
}
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* An id for a new voiceover group, de-duped against every id already in the
|
|
@@ -130,7 +135,7 @@ function excludedFor(groupByMember: Map<string, { id: string }>, bedId?: string)
|
|
|
130
135
|
|
|
131
136
|
export function collectCarveCandidates(
|
|
132
137
|
doc: Document,
|
|
133
|
-
others: readonly
|
|
138
|
+
others: readonly Element[],
|
|
134
139
|
overlapsBed: (a: Element) => boolean,
|
|
135
140
|
/** The bed's own id, so neither it nor the group it belongs to is offered. */
|
|
136
141
|
bedId?: string,
|
|
@@ -141,7 +146,7 @@ export function collectCarveCandidates(
|
|
|
141
146
|
const excluded = excludedFor(groupByMember, bedId);
|
|
142
147
|
const offeredGroupIds = new Set<string>();
|
|
143
148
|
const described: CarveCandidate[] = [];
|
|
144
|
-
for (const a of others) {
|
|
149
|
+
for (const a of others.filter(isCarveVoiceElement)) {
|
|
145
150
|
const group = groupByMember.get(a.id);
|
|
146
151
|
if (excluded.has(a.id) || (group && excluded.has(group.id))) continue;
|
|
147
152
|
if (!group) {
|
|
@@ -161,7 +166,7 @@ export function collectCarveCandidates(
|
|
|
161
166
|
// iframe document, so the constructor is a different realm's and the
|
|
162
167
|
// instanceof is false for every one (mirrors resolveCarveVoices in
|
|
163
168
|
// useFxCarve.ts).
|
|
164
|
-
.filter((el): el is HTMLElement => el
|
|
169
|
+
.filter((el): el is HTMLElement => isCarveVoiceElement(el));
|
|
165
170
|
if (!members.some(overlapsBed)) continue;
|
|
166
171
|
offeredGroupIds.add(group.id);
|
|
167
172
|
described.push({
|