@hyperframes/studio 0.8.11 → 0.8.12
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-C6J6fsAD.js → hyperframes-player-B243szNa.js} +1 -1
- package/dist/assets/{index-BX0piiWc.js → index-BTP86sD-.js} +1 -1
- package/dist/assets/index-BZfUJ2He.js +431 -0
- package/dist/assets/{index-1vMlv05L.js → index-DQ_lkuqY.js} +1 -1
- package/dist/assets/index-yGhfxxoL.css +1 -0
- package/dist/{chunk-ZALQ3CW7.js → chunk-6BT6DTB4.js} +2 -1
- package/dist/{domEditingLayers-XRVXRTNX.js → domEditingLayers-URA7BLWE.js} +2 -2
- package/dist/index.d.ts +84 -43
- package/dist/index.html +2 -2
- package/dist/index.js +6431 -5515
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/App.tsx +0 -2
- package/src/components/StudioRightPanel.tsx +15 -57
- package/src/components/editor/PropertyPanel.test.tsx +101 -3
- package/src/components/editor/PropertyPanel.tsx +15 -1
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +35 -20
- package/src/components/editor/PropertyPanelFlat.tsx +82 -21
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +21 -0
- package/src/components/editor/TimelineFxPopover.tsx +24 -4
- package/src/components/editor/audioFxRevealTarget.test.ts +59 -0
- package/src/components/editor/audioFxRevealTarget.ts +113 -0
- package/src/components/editor/audioFxSignalPath.test.ts +50 -0
- package/src/components/editor/audioFxSignalPath.ts +71 -0
- package/src/components/editor/audioFxSummary.test.ts +8 -0
- package/src/components/editor/audioFxSummary.ts +28 -16
- package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +101 -0
- package/src/components/editor/propertyPanelAudioFxGroup.tsx +92 -2
- package/src/components/editor/propertyPanelFlatClosedGroup.tsx +46 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +31 -0
- package/src/components/editor/propertyPanelFxCarveModule.tsx +203 -125
- package/src/components/editor/propertyPanelFxControls.test.tsx +50 -0
- package/src/components/editor/propertyPanelFxControls.tsx +10 -2
- package/src/components/editor/propertyPanelFxEqModule.tsx +3 -0
- package/src/components/editor/propertyPanelFxNodeRow.tsx +11 -1
- package/src/components/editor/propertyPanelFxPresetMenu.tsx +12 -2
- package/src/components/editor/propertyPanelFxRackChain.tsx +12 -5
- package/src/components/editor/propertyPanelFxSection.test.tsx +42 -0
- package/src/components/editor/propertyPanelFxSection.tsx +63 -4
- package/src/components/editor/propertyPanelFxSectionTypes.ts +19 -0
- package/src/components/editor/useAudioFxRevealSection.ts +64 -0
- package/src/components/editor/useAuditionTransport.test.ts +36 -0
- package/src/components/editor/useAuditionTransport.ts +78 -0
- package/src/components/editor/useFxAudition.ts +12 -1
- package/src/components/editor/useFxCarve.ts +0 -0
- package/src/components/editor/useFxCarveGrouping.test.ts +118 -0
- package/src/components/editor/useFxCarveGrouping.ts +139 -4
- package/src/components/editor/useFxCarveNodes.ts +153 -0
- package/src/components/editor/useFxLevelling.ts +5 -34
- package/src/components/nle/NLEContext.tsx +3 -10
- package/src/components/nle/PreviewPane.tsx +0 -1
- package/src/components/nle/useTimelineEditCallbacks.ts +5 -1
- package/src/contexts/DesignPanelInputContext.tsx +6 -5
- package/src/contexts/DomEditContext.tsx +10 -3
- package/src/contexts/FileManagerContext.tsx +3 -2
- package/src/contexts/PanelLayoutContext.tsx +3 -2
- package/src/contexts/StudioContext.tsx +7 -3
- package/src/contexts/TimelineEditContext.tsx +6 -2
- package/src/contexts/VariablePromoteContext.tsx +6 -2
- package/src/contexts/ViewModeContext.tsx +2 -2
- package/src/hooks/domEditDeleteMembers.ts +34 -0
- package/src/hooks/timelineAudioGroupCreate.ts +345 -0
- package/src/hooks/timelineAudioGroupVolume.test.ts +204 -0
- package/src/hooks/timelineAudioGroupVolume.ts +135 -3
- package/src/hooks/timelineEditingHelpers.test.ts +86 -0
- package/src/hooks/timelineEditingHelpers.ts +19 -7
- package/src/hooks/timelineElementFxAttribute.ts +0 -3
- package/src/hooks/timelineTrackVisibility.test.ts +95 -13
- package/src/hooks/timelineTrackVisibility.ts +6 -182
- package/src/hooks/useAudioGroupCarveAssignment.test.tsx +139 -0
- package/src/hooks/useBlockedTimelineEditToast.ts +27 -0
- package/src/hooks/useDomEditAttributeCommits.ts +13 -0
- package/src/hooks/useDomEditSession.test.tsx +57 -2
- package/src/hooks/useDomEditSession.ts +23 -24
- package/src/hooks/useEffectiveTimelineDuration.ts +9 -7
- package/src/hooks/useLivePlayheadTime.ts +3 -1
- package/src/hooks/useRemoveBackground.ts +70 -0
- package/src/hooks/useTimelineEditing.ts +5 -19
- package/src/hooks/useTimelineEditingTypes.ts +7 -0
- package/src/player/components/AutomationSelectionMenu.tsx +4 -0
- package/src/player/components/LayerDisclosureRow.tsx +59 -25
- package/src/player/components/PlayerControls.tsx +0 -38
- package/src/player/components/Timeline.test.ts +40 -1
- package/src/player/components/Timeline.tsx +5 -2
- package/src/player/components/TimelineAutomationLane.test.tsx +67 -0
- package/src/player/components/TimelineAutomationLane.tsx +89 -12
- package/src/player/components/TimelineAutomationLaneSlot.tsx +22 -3
- package/src/player/components/TimelineFxButton.test.tsx +167 -21
- package/src/player/components/TimelineFxButton.tsx +156 -31
- package/src/player/components/TimelineGroupHeader.test.tsx +0 -5
- package/src/player/components/TimelineGroupHeader.tsx +100 -102
- package/src/player/components/TimelineGroupLaneLabels.tsx +88 -0
- package/src/player/components/TimelineGroupRow.test.tsx +146 -0
- package/src/player/components/TimelineGroupRow.tsx +123 -64
- package/src/player/components/TimelineLanes.test.tsx +3 -3
- package/src/player/components/TimelineLanes.tsx +25 -23
- package/src/player/components/TimelineTrackHeader.test.tsx +298 -13
- package/src/player/components/TimelineTrackHeader.tsx +301 -381
- package/src/player/components/TimelineTrackPlainHeader.test.tsx +47 -69
- package/src/player/components/TimelineTrackPlainHeader.tsx +64 -54
- package/src/player/components/automationLaneData.test.ts +94 -0
- package/src/player/components/automationLaneData.ts +23 -0
- package/src/player/components/groupAutomationElement.test.ts +51 -0
- package/src/player/components/groupAutomationElement.ts +37 -0
- package/src/player/components/timelineCallbacks.ts +6 -2
- package/src/player/components/timelineKeyboardNavigation.test.ts +2 -2
- package/src/player/components/timelineKeyboardNavigation.ts +25 -8
- package/src/player/components/timelineLayout.ts +0 -1
- package/src/player/components/timelineViewModel.ts +30 -1
- package/src/player/components/trackHeaderLabelRows.tsx +328 -0
- package/src/player/components/useTimelineClipDisclosure.ts +41 -0
- package/src/player/components/useTimelineLaneRowIndexes.ts +1 -1
- package/src/player/components/useTimelineLogicalFocus.ts +2 -2
- package/src/player/components/useTimelineLogicalRows.test.tsx +2 -2
- package/src/player/components/useTimelineLogicalRows.ts +3 -3
- package/src/player/components/useTimelineTrackDerivations.ts +47 -8
- package/src/player/components/useTimelineTrackLayout.test.ts +119 -0
- package/src/player/components/useTimelineTrackLayout.ts +12 -4
- package/src/player/hooks/previewMessageRouter.ts +4 -11
- package/src/player/hooks/timelineSyncHydration.ts +395 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +83 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +40 -1
- package/src/player/hooks/useTimelinePlayer.ts +2 -7
- package/src/player/hooks/useTimelineSyncCallbacks.ts +43 -219
- package/src/player/lib/automationStoreSync.test.ts +26 -0
- package/src/player/lib/automationStoreSync.ts +28 -4
- package/src/player/lib/runtimeAudioId.test.ts +58 -0
- package/src/player/lib/timelineDOM.test.ts +115 -0
- package/src/player/lib/timelineDOM.ts +3 -32
- package/src/player/lib/timelineElementHelpers.ts +27 -0
- package/src/player/lib/timelineGroupInfo.ts +134 -0
- package/src/player/lib/timelineIframeHelpers.test.ts +21 -0
- package/src/player/lib/timelineIframeHelpers.ts +17 -0
- package/src/player/store/keyframeSlice.ts +79 -5
- package/src/player/store/playerStore.ts +37 -47
- package/src/player/store/playerStoreDevHandle.ts +22 -0
- package/src/player/store/playerStoreSelection.ts +41 -0
- package/src/player/store/timelineElement.ts +32 -0
- package/src/utils/hmrStableContext.ts +64 -0
- package/src/utils/timelineInspector.test.ts +35 -1
- package/src/utils/timelineInspector.ts +38 -0
- package/dist/assets/index-Ba9zbpT9.css +0 -1
- package/dist/assets/index-DoW5v5eS.js +0 -428
- package/src/hooks/useAudioSoloBridge.ts +0 -61
- package/src/hooks/useGroupLevel.ts +0 -36
- package/src/player/components/TimelineGroupBusStrip.test.tsx +0 -146
- package/src/player/components/TimelineGroupBusStrip.tsx +0 -103
- package/src/player/components/TimelineSoloButton.tsx +0 -32
- package/src/player/store/audioSoloSlice.test.ts +0 -113
- package/src/player/store/audioSoloSlice.ts +0 -43
- package/src/player/store/groupLevels.ts +0 -33
- /package/dist/{chunk-ZALQ3CW7.js.map → chunk-6BT6DTB4.js.map} +0 -0
- /package/dist/{domEditingLayers-XRVXRTNX.js.map → domEditingLayers-URA7BLWE.js.map} +0 -0
|
@@ -12,7 +12,7 @@ import type { TimelineElement } from "../store/playerStore";
|
|
|
12
12
|
import type { ClipManifestClip } from "./playbackTypes";
|
|
13
13
|
import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
|
|
14
14
|
import { readClipTiming } from "@hyperframes/core/composition-contract";
|
|
15
|
-
import {
|
|
15
|
+
import { groupInfoFor } from "./timelineGroupInfo";
|
|
16
16
|
import {
|
|
17
17
|
resolveMediaElement,
|
|
18
18
|
applyMediaMetadataFromElement,
|
|
@@ -68,37 +68,6 @@ function resolveClipTag(clip: ClipManifestClip): string {
|
|
|
68
68
|
return clip.tagName || clip.kind || "div";
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
// One `<hf-audio-group>` scan per document, not per clip — resolveAudioGroups
|
|
72
|
-
// walks the whole tree, and a parse touches every clip in it.
|
|
73
|
-
interface GroupInfo {
|
|
74
|
-
label: string;
|
|
75
|
-
volume: number;
|
|
76
|
-
hidden: boolean;
|
|
77
|
-
fxChain?: string;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const groupInfoCache = new WeakMap<Document, Map<string, GroupInfo>>();
|
|
81
|
-
|
|
82
|
-
function groupInfoFor(doc: Document | null | undefined, groupId: string): GroupInfo {
|
|
83
|
-
if (!doc) return { label: groupId, volume: 1, hidden: false };
|
|
84
|
-
let info = groupInfoCache.get(doc);
|
|
85
|
-
if (!info) {
|
|
86
|
-
info = new Map(
|
|
87
|
-
resolveAudioGroups(doc).map((group) => [
|
|
88
|
-
group.id,
|
|
89
|
-
{
|
|
90
|
-
label: group.label,
|
|
91
|
-
volume: group.volume,
|
|
92
|
-
hidden: group.hidden,
|
|
93
|
-
...(group.fxChain ? { fxChain: group.fxChain } : {}),
|
|
94
|
-
},
|
|
95
|
-
]),
|
|
96
|
-
);
|
|
97
|
-
groupInfoCache.set(doc, info);
|
|
98
|
-
}
|
|
99
|
-
return info.get(groupId) ?? { label: groupId, volume: 1, hidden: false };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
71
|
// fallow-ignore-next-line complexity
|
|
103
72
|
export function createTimelineElementFromManifestClip(params: {
|
|
104
73
|
clip: ClipManifestClip;
|
|
@@ -178,6 +147,7 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
178
147
|
entry.audioGroupVolume = info.volume;
|
|
179
148
|
entry.audioGroupHidden = info.hidden;
|
|
180
149
|
if (info.fxChain) entry.audioGroupFxChain = info.fxChain;
|
|
150
|
+
if (info.automation) entry.audioGroupAutomation = info.automation;
|
|
181
151
|
}
|
|
182
152
|
const fxChain = hostEl.getAttribute("data-fx-chain");
|
|
183
153
|
if (fxChain) entry.fxChain = fxChain;
|
|
@@ -405,6 +375,7 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
|
|
405
375
|
entry.audioGroupVolume = domGroupInfo.volume;
|
|
406
376
|
entry.audioGroupHidden = domGroupInfo.hidden;
|
|
407
377
|
if (domGroupInfo.fxChain) entry.audioGroupFxChain = domGroupInfo.fxChain;
|
|
378
|
+
if (domGroupInfo.automation) entry.audioGroupAutomation = domGroupInfo.automation;
|
|
408
379
|
}
|
|
409
380
|
|
|
410
381
|
// Sub-compositions
|
|
@@ -11,6 +11,7 @@ import type { TimelineElement } from "../store/playerStore";
|
|
|
11
11
|
import type { ClipManifestClip } from "./playbackTypes";
|
|
12
12
|
import { isFinitePositive } from "./playbackAdapter";
|
|
13
13
|
import { getSourceScopedSelectorIndex } from "../../utils/sourceScopedSelectorIndex";
|
|
14
|
+
import { HF_AUDIO_GROUP_TAG } from "@hyperframes/core/audio-groups";
|
|
14
15
|
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
16
17
|
// Layer-reveal lift transparency
|
|
@@ -81,6 +82,14 @@ function normalizePlaybackRate(raw: number): number {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
export function isTimelineIgnoredElement(el: Element): boolean {
|
|
85
|
+
// An `<hf-audio-group>` is a mixer bus, not a clip: it carries the group's
|
|
86
|
+
// label, fader, mute and FX chain, has no timing of its own, and is drawn as
|
|
87
|
+
// a GROUP ROW by the group derivation. Left in, the implicit-layer fallback
|
|
88
|
+
// also gave it an ordinary full-duration track — so a grouped composition
|
|
89
|
+
// showed "Voiceover • 0.0s – 12.0s" as a phantom clip directly above the real
|
|
90
|
+
// group header. Harmless-looking, but that row is draggable and trimmable,
|
|
91
|
+
// and writing timing onto the bus is meaningless.
|
|
92
|
+
if (el.tagName.toLowerCase() === HF_AUDIO_GROUP_TAG) return true;
|
|
84
93
|
return Boolean(
|
|
85
94
|
el.closest(
|
|
86
95
|
[
|
|
@@ -346,6 +355,24 @@ export function getTimelineElementIdentity(element: { key?: string | null; id: s
|
|
|
346
355
|
return element.key ?? element.id;
|
|
347
356
|
}
|
|
348
357
|
|
|
358
|
+
/**
|
|
359
|
+
* The id space the RUNTIME matches on — a bare DOM id, never a store key.
|
|
360
|
+
*
|
|
361
|
+
* Studio addresses rows by `buildTimelineElementKey`'s composite
|
|
362
|
+
* `<sourceFile>#<domId>`, but everything audio in `@hyperframes/core` keys off
|
|
363
|
+
* the live document: `resolveAudioGroups` collects `member.id`,
|
|
364
|
+
* `resolveCarveSourceIds` goes through `getElementById`. Anything crossing into
|
|
365
|
+
* that space — a group membership list, a carve source — has to be
|
|
366
|
+
* converted here first; a composite key silently matches nothing.
|
|
367
|
+
*
|
|
368
|
+
* `null` for a row with no DOM id at all (selector-addressed elements): such an
|
|
369
|
+
* element cannot be grouped, because `resolveAudioGroups` skips
|
|
370
|
+
* members without an `id` and would build a group that is half there.
|
|
371
|
+
*/
|
|
372
|
+
export function runtimeAudioId(element: { domId?: string | null }): string | null {
|
|
373
|
+
return element.domId || null;
|
|
374
|
+
}
|
|
375
|
+
|
|
349
376
|
/**
|
|
350
377
|
* Timeline store key for a z-reorder entry built OUTSIDE the timeline
|
|
351
378
|
* expansion (canvas context menu / LayersPanel), so the reorder commit can
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The audio-group scan cached per preview document, and the mechanism that
|
|
3
|
+
* keeps it honest.
|
|
4
|
+
*
|
|
5
|
+
* Split out of `timelineDOM.ts` (600-line studio ceiling). Self-contained: the
|
|
6
|
+
* cache, its revision counter, the observer that bumps it, and the one reader.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
HF_AUDIO_GROUP_ATTR,
|
|
11
|
+
HF_AUDIO_GROUP_TAG,
|
|
12
|
+
resolveAudioGroups,
|
|
13
|
+
} from "@hyperframes/core/audio-groups";
|
|
14
|
+
import { HF_AUDIO_AUTOMATION_ATTR } from "@hyperframes/core/audio-automation";
|
|
15
|
+
import { HF_AUDIO_FX_ATTR } from "@hyperframes/core/audio-fx";
|
|
16
|
+
|
|
17
|
+
// One `<hf-audio-group>` scan per document, not per clip — resolveAudioGroups
|
|
18
|
+
// walks the whole tree, and a parse touches every clip in it.
|
|
19
|
+
interface GroupInfo {
|
|
20
|
+
label: string;
|
|
21
|
+
volume: number;
|
|
22
|
+
hidden: boolean;
|
|
23
|
+
fxChain?: string;
|
|
24
|
+
automation?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The cached scan, plus the DOM revision it was taken at.
|
|
29
|
+
*
|
|
30
|
+
* Keeping the revision beside the entry is what makes staleness *detectable*
|
|
31
|
+
* rather than a documented obligation. The cache is keyed on the document, and
|
|
32
|
+
* group edits are applied as live patches precisely so the iframe never
|
|
33
|
+
* reloads, so the key alone never changes: an explicit "remember to invalidate"
|
|
34
|
+
* contract silently rots the first time a new writer forgets — which is exactly
|
|
35
|
+
* what happened with the FX rack, whose group writes go through the DOM editor
|
|
36
|
+
* rather than the timeline's own writers.
|
|
37
|
+
*/
|
|
38
|
+
const groupInfoCache = new WeakMap<
|
|
39
|
+
Document,
|
|
40
|
+
{ revision: number; entries: Map<string, GroupInfo> }
|
|
41
|
+
>();
|
|
42
|
+
|
|
43
|
+
/** Bumped by every observed mutation to group state in a document. */
|
|
44
|
+
const groupRevisions = new WeakMap<Document, number>();
|
|
45
|
+
const groupObservers = new WeakSet<Document>();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Watch a document for any change to group state, so the cache expires itself.
|
|
49
|
+
*
|
|
50
|
+
* One observer per document, attached the first time a group is read from it.
|
|
51
|
+
* It watches the attributes a group's identity is made of, anywhere in the
|
|
52
|
+
* tree, plus added/removed nodes — which covers a group element appearing, a
|
|
53
|
+
* member joining or leaving, and any group attribute being edited, by any
|
|
54
|
+
* writer, without that writer having to know this cache exists.
|
|
55
|
+
*/
|
|
56
|
+
function observeGroupState(doc: Document): void {
|
|
57
|
+
if (groupObservers.has(doc) || typeof MutationObserver === "undefined" || !doc.body) return;
|
|
58
|
+
groupObservers.add(doc);
|
|
59
|
+
const observer = new MutationObserver((records) => {
|
|
60
|
+
// `childList` fires for EVERY node added or removed anywhere in the live
|
|
61
|
+
// preview, which on a composition that churns nodes during playback
|
|
62
|
+
// (SplitText, a typewriter, anything runtime-inserted) would expire this
|
|
63
|
+
// cache permanently and put it back to one whole-tree scan per parse. Only
|
|
64
|
+
// a group ELEMENT appearing or leaving actually changes the answer, so
|
|
65
|
+
// childList records are filtered rather than trusted; attribute records
|
|
66
|
+
// always count, because the filter below already narrowed them.
|
|
67
|
+
const relevant = records.some(
|
|
68
|
+
(record) =>
|
|
69
|
+
record.type !== "childList" ||
|
|
70
|
+
[...record.addedNodes, ...record.removedNodes].some(
|
|
71
|
+
(node) =>
|
|
72
|
+
node instanceof Element &&
|
|
73
|
+
(node.tagName.toLowerCase() === HF_AUDIO_GROUP_TAG ||
|
|
74
|
+
node.hasAttribute(HF_AUDIO_GROUP_ATTR) ||
|
|
75
|
+
node.querySelector?.(`${HF_AUDIO_GROUP_TAG},[${HF_AUDIO_GROUP_ATTR}]`) != null),
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
if (relevant) groupRevisions.set(doc, (groupRevisions.get(doc) ?? 0) + 1);
|
|
79
|
+
});
|
|
80
|
+
observer.observe(doc.body, {
|
|
81
|
+
subtree: true,
|
|
82
|
+
childList: true,
|
|
83
|
+
attributes: true,
|
|
84
|
+
// Every attribute `resolveAudioGroups`/`buildGroup` reads, including
|
|
85
|
+
// `data-automation` — which `GroupInfo` does not cache TODAY, so omitting it
|
|
86
|
+
// was inert, but group automation lanes already exist and the first person
|
|
87
|
+
// to cache one would have got a silently never-firing observer. That is the
|
|
88
|
+
// precise rot this observer replaced.
|
|
89
|
+
attributeFilter: [
|
|
90
|
+
HF_AUDIO_GROUP_ATTR,
|
|
91
|
+
HF_AUDIO_FX_ATTR,
|
|
92
|
+
HF_AUDIO_AUTOMATION_ATTR,
|
|
93
|
+
"data-label",
|
|
94
|
+
"data-volume",
|
|
95
|
+
"data-hidden",
|
|
96
|
+
"id",
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Drop the cached group scan for a document.
|
|
103
|
+
*
|
|
104
|
+
* Belt to the observer's braces: a caller that has just written and wants the
|
|
105
|
+
* very next read to be honest cannot wait for the observer's microtask. Callers
|
|
106
|
+
* that forget are no longer punished — the revision check catches them.
|
|
107
|
+
*/
|
|
108
|
+
export function invalidateGroupInfoCache(doc: Document | null | undefined): void {
|
|
109
|
+
if (doc) groupInfoCache.delete(doc);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function groupInfoFor(doc: Document | null | undefined, groupId: string): GroupInfo {
|
|
113
|
+
if (!doc) return { label: groupId, volume: 1, hidden: false };
|
|
114
|
+
observeGroupState(doc);
|
|
115
|
+
const revision = groupRevisions.get(doc) ?? 0;
|
|
116
|
+
const cached = groupInfoCache.get(doc);
|
|
117
|
+
let info = cached && cached.revision === revision ? cached.entries : undefined;
|
|
118
|
+
if (!info) {
|
|
119
|
+
info = new Map(
|
|
120
|
+
resolveAudioGroups(doc).map((group) => [
|
|
121
|
+
group.id,
|
|
122
|
+
{
|
|
123
|
+
label: group.label,
|
|
124
|
+
volume: group.volume,
|
|
125
|
+
hidden: group.hidden,
|
|
126
|
+
...(group.fxChain ? { fxChain: group.fxChain } : {}),
|
|
127
|
+
...(group.automation ? { automation: group.automation } : {}),
|
|
128
|
+
},
|
|
129
|
+
]),
|
|
130
|
+
);
|
|
131
|
+
groupInfoCache.set(doc, { revision, entries: info });
|
|
132
|
+
}
|
|
133
|
+
return info.get(groupId) ?? { label: groupId, volume: 1, hidden: false };
|
|
134
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
import {
|
|
4
|
+
applyPreviewAudioFlags,
|
|
4
5
|
buildMissingCompositionElements,
|
|
5
6
|
scrubPreviewAudio,
|
|
6
7
|
setPreviewMediaVolume,
|
|
@@ -87,3 +88,23 @@ describe("scrubPreviewAudio", () => {
|
|
|
87
88
|
stopScrubPreviewAudio();
|
|
88
89
|
});
|
|
89
90
|
});
|
|
91
|
+
|
|
92
|
+
describe("applyPreviewAudioFlags", () => {
|
|
93
|
+
// Everything pushed here is state the runtime loses on reload and nothing else
|
|
94
|
+
// re-sends, so the push has to carry all of it every time. Volume in
|
|
95
|
+
// particular: the transport comes back at unity, so a preview the author had
|
|
96
|
+
// turned down came back loud.
|
|
97
|
+
it("re-pushes mute and volume together", () => {
|
|
98
|
+
const iframe = document.createElement("iframe");
|
|
99
|
+
document.body.append(iframe);
|
|
100
|
+
const postMessage = vi.spyOn(iframe.contentWindow!, "postMessage");
|
|
101
|
+
|
|
102
|
+
applyPreviewAudioFlags(iframe, true, 0.4);
|
|
103
|
+
|
|
104
|
+
const actions = postMessage.mock.calls.map(
|
|
105
|
+
(call) => (call[0] as { action?: string }).action ?? "",
|
|
106
|
+
);
|
|
107
|
+
expect(actions).toContain("set-muted");
|
|
108
|
+
expect(actions).toContain("set-volume");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -142,6 +142,23 @@ export function setPreviewMediaVolume(iframe: HTMLIFrameElement | null, volume:
|
|
|
142
142
|
} catch {}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Everything the preview runtime has to be told about audio after it loads.
|
|
147
|
+
* Called from `applyPreviewAudioState`, which is the path that re-runs after a
|
|
148
|
+
* preview reload — the runtime comes back with the transport at its defaults
|
|
149
|
+
* and nothing else pushes them again.
|
|
150
|
+
*/
|
|
151
|
+
export function applyPreviewAudioFlags(
|
|
152
|
+
iframe: HTMLIFrameElement | null,
|
|
153
|
+
muted: boolean,
|
|
154
|
+
volume: number,
|
|
155
|
+
): void {
|
|
156
|
+
setPreviewMediaMuted(iframe, muted);
|
|
157
|
+
// Volume too: the transport comes back at unity after a reload, so a preview
|
|
158
|
+
// the author had turned down came back loud.
|
|
159
|
+
setPreviewMediaVolume(iframe, volume);
|
|
160
|
+
}
|
|
161
|
+
|
|
145
162
|
export function setPreviewPlaybackRate(
|
|
146
163
|
iframe: HTMLIFrameElement | null,
|
|
147
164
|
playbackRate: number,
|
|
@@ -34,6 +34,38 @@ export interface FocusedEaseSegment {
|
|
|
34
34
|
|
|
35
35
|
type FocusedEaseSegmentTarget = Omit<FocusedEaseSegment, "projectId" | "sessionEpoch" | "nonce">;
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* A request to reveal one automated parameter in the audio FX rack.
|
|
39
|
+
*
|
|
40
|
+
* `elementKey` is the timeline element whose rack it belongs to — a group's own
|
|
41
|
+
* id for a group lane, the clip's key otherwise — so the panel can refuse a
|
|
42
|
+
* request aimed at something it is not showing.
|
|
43
|
+
*/
|
|
44
|
+
export interface RevealedAudioFxTarget {
|
|
45
|
+
elementKey: string;
|
|
46
|
+
/** The lane's own `fx.<node>.<param>` / `volume` target. */
|
|
47
|
+
automationTarget: string;
|
|
48
|
+
projectId: string | null;
|
|
49
|
+
sessionEpoch: number;
|
|
50
|
+
nonce: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type RevealedAudioFxTargetRequest = Omit<
|
|
54
|
+
RevealedAudioFxTarget,
|
|
55
|
+
"projectId" | "sessionEpoch" | "nonce"
|
|
56
|
+
>;
|
|
57
|
+
|
|
58
|
+
/** Whether a reveal request still belongs to what is on screen. */
|
|
59
|
+
export function isRevealedAudioFxRequestCurrent(
|
|
60
|
+
request: RevealedAudioFxTarget,
|
|
61
|
+
state: TimelineSessionIdentity,
|
|
62
|
+
): boolean {
|
|
63
|
+
return (
|
|
64
|
+
request.projectId === state.timelineProjectId &&
|
|
65
|
+
request.sessionEpoch === state.timelineSessionEpoch
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
37
69
|
interface TimelineSessionIdentity {
|
|
38
70
|
timelineProjectId: string | null;
|
|
39
71
|
timelineSessionEpoch: number;
|
|
@@ -63,8 +95,16 @@ export interface KeyframeSlice {
|
|
|
63
95
|
/** Union-expand clips (keyframed clips are expanded by default on load). */
|
|
64
96
|
expandClips: (ids: readonly string[]) => void;
|
|
65
97
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Groups whose member rows the caret has HIDDEN (structural, not lanes).
|
|
100
|
+
*
|
|
101
|
+
* Inverted deliberately. As an expanded-set, "not in the set" could not tell
|
|
102
|
+
* never-touched from deliberately-collapsed, so every group defaulted to
|
|
103
|
+
* collapsed — and since nothing seeds the set on create, grouping three
|
|
104
|
+
* tracks made all three vanish behind a header the user had not yet learned
|
|
105
|
+
* to open. Groups are expanded until someone closes one.
|
|
106
|
+
*/
|
|
107
|
+
collapsedGroupIds: Set<string>;
|
|
68
108
|
toggleGroupExpanded: (id: string) => void;
|
|
69
109
|
|
|
70
110
|
/** Rows (clip id or group id) whose automation-lane rows the `∿` button opened. */
|
|
@@ -80,6 +120,19 @@ export interface KeyframeSlice {
|
|
|
80
120
|
setFocusedEaseSegment: (target: FocusedEaseSegmentTarget) => void;
|
|
81
121
|
clearFocusedEaseSegment: (nonce: number) => void;
|
|
82
122
|
|
|
123
|
+
/**
|
|
124
|
+
* "Show me this automated parameter in the rack" — raised by clicking an
|
|
125
|
+
* automation lane's label in the timeline, consumed by the property panel.
|
|
126
|
+
*
|
|
127
|
+
* Session-stamped and nonce-guarded exactly like `focusedEaseSegment`: a
|
|
128
|
+
* request outlives the click, so one made against a different project or
|
|
129
|
+
* before a reload must not reopen a rack on whatever is mounted later.
|
|
130
|
+
*/
|
|
131
|
+
revealedAudioFxTarget: RevealedAudioFxTarget | null;
|
|
132
|
+
revealedAudioFxNonce: number;
|
|
133
|
+
setRevealedAudioFxTarget: (target: RevealedAudioFxTargetRequest) => void;
|
|
134
|
+
clearRevealedAudioFxTarget: (nonce: number) => void;
|
|
135
|
+
|
|
83
136
|
/** Keyframe data per element id, populated from parsed GSAP animations. */
|
|
84
137
|
keyframeCache: Map<string, KeyframeCacheEntry>;
|
|
85
138
|
/** Unmerged source tweens per element; expanded property lanes read this, never keyframeCache. */
|
|
@@ -127,13 +180,13 @@ export function createKeyframeSlice(
|
|
|
127
180
|
return { expandedClipIds: next };
|
|
128
181
|
}),
|
|
129
182
|
|
|
130
|
-
|
|
183
|
+
collapsedGroupIds: new Set(),
|
|
131
184
|
toggleGroupExpanded: (id) =>
|
|
132
185
|
set((state) => {
|
|
133
|
-
const next = new Set(state.
|
|
186
|
+
const next = new Set(state.collapsedGroupIds);
|
|
134
187
|
if (next.has(id)) next.delete(id);
|
|
135
188
|
else next.add(id);
|
|
136
|
-
return {
|
|
189
|
+
return { collapsedGroupIds: next };
|
|
137
190
|
}),
|
|
138
191
|
|
|
139
192
|
expandedLaneOwnerIds: new Set(),
|
|
@@ -166,6 +219,27 @@ export function createKeyframeSlice(
|
|
|
166
219
|
state.focusedEaseSegment?.nonce === nonce ? { focusedEaseSegment: null } : state,
|
|
167
220
|
),
|
|
168
221
|
|
|
222
|
+
revealedAudioFxTarget: null,
|
|
223
|
+
revealedAudioFxNonce: 0,
|
|
224
|
+
setRevealedAudioFxTarget: (target) =>
|
|
225
|
+
set((state) => {
|
|
226
|
+
const nonce = state.revealedAudioFxNonce + 1;
|
|
227
|
+
const { timelineProjectId, timelineSessionEpoch } = getTimelineSessionIdentity();
|
|
228
|
+
return {
|
|
229
|
+
revealedAudioFxNonce: nonce,
|
|
230
|
+
revealedAudioFxTarget: {
|
|
231
|
+
...target,
|
|
232
|
+
projectId: timelineProjectId,
|
|
233
|
+
sessionEpoch: timelineSessionEpoch,
|
|
234
|
+
nonce,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}),
|
|
238
|
+
clearRevealedAudioFxTarget: (nonce) =>
|
|
239
|
+
set((state) =>
|
|
240
|
+
state.revealedAudioFxTarget?.nonce === nonce ? { revealedAudioFxTarget: null } : state,
|
|
241
|
+
),
|
|
242
|
+
|
|
169
243
|
keyframeCache: new Map(),
|
|
170
244
|
setKeyframeCache: (elementId, data) =>
|
|
171
245
|
set((state) => {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { create } from "zustand";
|
|
2
|
+
import { attachPlayerStoreDevHandle } from "./playerStoreDevHandle";
|
|
3
|
+
import { nextSelectionSet, revealTargetsSelection } from "./playerStoreSelection";
|
|
2
4
|
import type { MusicBeatAnalysis } from "@hyperframes/core/beats";
|
|
3
5
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
4
6
|
import type { BeatEditState } from "../../utils/beatEditing";
|
|
@@ -14,15 +16,14 @@ import {
|
|
|
14
16
|
createAutomationSelectionSlice,
|
|
15
17
|
type AutomationSelectionSlice,
|
|
16
18
|
} from "./automationSelectionSlice";
|
|
19
|
+
import { createEditingModeSlice, type EditingModeSlice } from "./editingModeSlice";
|
|
17
20
|
import { createTimelineFocusRequest, type TimelineFocusRequest } from "./timelineFocusState";
|
|
18
21
|
import { createThumbnailSlice, type ThumbnailSlice } from "./thumbnailSlice";
|
|
19
|
-
import { createAudioSoloSlice, type AudioSoloSlice } from "./audioSoloSlice";
|
|
20
|
-
import { createEditingModeSlice, type EditingModeSlice } from "./editingModeSlice";
|
|
21
22
|
|
|
22
23
|
export type { KeyframeCacheEntry } from "./keyframeSlice";
|
|
23
24
|
export { liveTime } from "./liveTime";
|
|
24
25
|
|
|
25
|
-
import type { TimelineElement } from "./timelineElement";
|
|
26
|
+
import type { TimelineElement, TimelineElementPatch } from "./timelineElement";
|
|
26
27
|
|
|
27
28
|
export type { TimelineElement };
|
|
28
29
|
export type ZoomMode = "fit" | "manual";
|
|
@@ -50,12 +51,7 @@ function resolveElementSelection(
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
interface PlayerState
|
|
53
|
-
extends
|
|
54
|
-
KeyframeSlice,
|
|
55
|
-
AutomationSelectionSlice,
|
|
56
|
-
ThumbnailSlice,
|
|
57
|
-
AudioSoloSlice,
|
|
58
|
-
EditingModeSlice {
|
|
54
|
+
extends KeyframeSlice, AutomationSelectionSlice, ThumbnailSlice, EditingModeSlice {
|
|
59
55
|
isPlaying: boolean;
|
|
60
56
|
currentTime: number;
|
|
61
57
|
duration: number;
|
|
@@ -135,22 +131,7 @@ interface PlayerState
|
|
|
135
131
|
setSelectedElementId: (id: string | null, options?: SelectElementOptions) => void;
|
|
136
132
|
/** Move the selection anchor within an active multi-selection without collapsing it. */
|
|
137
133
|
setSelectionAnchor: (id: string | null) => void;
|
|
138
|
-
updateElement: (
|
|
139
|
-
elementId: string,
|
|
140
|
-
updates: Partial<
|
|
141
|
-
Pick<
|
|
142
|
-
TimelineElement,
|
|
143
|
-
| "start"
|
|
144
|
-
| "duration"
|
|
145
|
-
| "track"
|
|
146
|
-
| "zIndex"
|
|
147
|
-
| "hasExplicitZIndex"
|
|
148
|
-
| "playbackStart"
|
|
149
|
-
| "hidden"
|
|
150
|
-
| "audioGroup"
|
|
151
|
-
>
|
|
152
|
-
>,
|
|
153
|
-
) => void;
|
|
134
|
+
updateElement: (elementId: string, updates: TimelineElementPatch) => void;
|
|
154
135
|
setZoomMode: (mode: ZoomMode) => void;
|
|
155
136
|
setManualZoomPercent: (percent: number) => void;
|
|
156
137
|
bumpZEditVersion: () => void;
|
|
@@ -241,6 +222,19 @@ export interface DomClipChild {
|
|
|
241
222
|
hostId: string;
|
|
242
223
|
label: string;
|
|
243
224
|
stackingContextId: string;
|
|
225
|
+
/**
|
|
226
|
+
* The child's audio-group state, read off its live element during the DOM
|
|
227
|
+
* walk — the only place that sees it. A sub-composition can declare a group
|
|
228
|
+
* and its members entirely within itself, and those members never reach the
|
|
229
|
+
* flat store, so an expanded child has no twin to inherit membership from.
|
|
230
|
+
*/
|
|
231
|
+
audioGroup?: string;
|
|
232
|
+
audioGroupLabel?: string;
|
|
233
|
+
audioGroupVolume?: number;
|
|
234
|
+
audioGroupHidden?: boolean;
|
|
235
|
+
audioGroupFxChain?: string;
|
|
236
|
+
/** The group element's `data-automation`, mirrored the same way. */
|
|
237
|
+
audioGroupAutomation?: string;
|
|
244
238
|
}
|
|
245
239
|
|
|
246
240
|
interface BeatHistoryEntry {
|
|
@@ -271,9 +265,11 @@ export function createTimelineResetState() {
|
|
|
271
265
|
// paste through `sel.elementKey === paste.elementKey` to a stale t0.
|
|
272
266
|
automationSelection: null,
|
|
273
267
|
expandedClipIds: new Set<string>(),
|
|
274
|
-
|
|
268
|
+
// Per-composition: ids from comp A match nothing in B, silencing all of it.
|
|
269
|
+
collapsedGroupIds: new Set<string>(),
|
|
275
270
|
expandedLaneOwnerIds: new Set<string>(),
|
|
276
271
|
focusedEaseSegment: null,
|
|
272
|
+
revealedAudioFxTarget: null,
|
|
277
273
|
selectedElementIds: new Set<string>(),
|
|
278
274
|
requestedSeekTime: null,
|
|
279
275
|
lintFindingsByElement: new Map<string, { count: number; messages: string[] }>(),
|
|
@@ -323,7 +319,6 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|
|
323
319
|
...createThumbnailSlice(set),
|
|
324
320
|
|
|
325
321
|
...createAutomationSelectionSlice(set),
|
|
326
|
-
...createAudioSoloSlice(set, get),
|
|
327
322
|
...createEditingModeSlice(set),
|
|
328
323
|
|
|
329
324
|
activeKeyframePct: null,
|
|
@@ -519,18 +514,23 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|
|
519
514
|
// echoes that must preserve a group go through setSelectionAnchor instead.
|
|
520
515
|
setSelectedElementId: (id, options) =>
|
|
521
516
|
set((s) => {
|
|
522
|
-
const
|
|
523
|
-
const selectedElementIds = preserveSet
|
|
524
|
-
? new Set(s.selectedElementIds)
|
|
525
|
-
: options?.preserveSet
|
|
526
|
-
? new Set<string>()
|
|
527
|
-
: id
|
|
528
|
-
? new Set([id])
|
|
529
|
-
: new Set<string>();
|
|
517
|
+
const selectedElementIds = nextSelectionSet(s.selectedElementIds, id, options?.preserveSet);
|
|
530
518
|
// Selecting a different element drops any active keyframe selection — otherwise
|
|
531
519
|
// a stale activeKeyframePct from a prior diamond click would force the next drag
|
|
532
520
|
// to "modify" a keyframe on the new element. A diamond click sets the pct AFTER
|
|
533
521
|
// calling setSelectedElementId, so this never clobbers a genuine keyframe select.
|
|
522
|
+
// A reveal request survives the selection it is FOR. `openClipFxRack`
|
|
523
|
+
// raises the request and then selects the clip asynchronously, so the
|
|
524
|
+
// selection lands afterwards and used to clear the very request that
|
|
525
|
+
// caused it — the panel then read null and the section never opened.
|
|
526
|
+
// Any OTHER selection still drops it: a request aimed elsewhere is stale.
|
|
527
|
+
//
|
|
528
|
+
// Compared across the ID-SPACE BOUNDARY, which is why this needs saying:
|
|
529
|
+
// a request carries the BARE dom id (`runtimeAudioId`, because the panel
|
|
530
|
+
// and the runtime speak that), while this store's ids are
|
|
531
|
+
// `sourceFile#domId`. A direct `===` was silently never true — the exact
|
|
532
|
+
// shape of failure the id-space split produces.
|
|
533
|
+
const revealSurvives = revealTargetsSelection(s.revealedAudioFxTarget, id);
|
|
534
534
|
return id !== s.selectedElementId
|
|
535
535
|
? {
|
|
536
536
|
selectedElementId: id,
|
|
@@ -538,6 +538,7 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|
|
538
538
|
activeKeyframePct: null,
|
|
539
539
|
motionPathArmed: false,
|
|
540
540
|
focusedEaseSegment: null,
|
|
541
|
+
...(revealSurvives ? {} : { revealedAudioFxTarget: null }),
|
|
541
542
|
}
|
|
542
543
|
: { selectedElementId: id, selectedElementIds };
|
|
543
544
|
}),
|
|
@@ -579,15 +580,4 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|
|
579
580
|
reset: () => set(createTimelineResetState()),
|
|
580
581
|
}));
|
|
581
582
|
|
|
582
|
-
|
|
583
|
-
try {
|
|
584
|
-
return import.meta.env.DEV === true;
|
|
585
|
-
} catch {
|
|
586
|
-
// Turbopack and other non-Vite bundlers may not provide import.meta.env.
|
|
587
|
-
return false;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
if (isDevBuild() && typeof window !== "undefined") {
|
|
591
|
-
// Console handle for dumping live Studio state during bug-bash reproduction.
|
|
592
|
-
(window as unknown as { __playerStore?: typeof usePlayerStore }).__playerStore = usePlayerStore;
|
|
593
|
-
}
|
|
583
|
+
attachPlayerStoreDevHandle(usePlayerStore);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A console handle on the live store, dev builds only.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `playerStore.ts` to keep it under the studio's 600-line cap. The
|
|
5
|
+
* dev check is its own function because `import.meta.env` is absent under
|
|
6
|
+
* Turbopack and other non-Vite bundlers, where reading it throws.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function isDevBuild(): boolean {
|
|
10
|
+
try {
|
|
11
|
+
return import.meta.env.DEV === true;
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Expose `store` as `window.__playerStore` for dumping live Studio state
|
|
18
|
+
* during bug-bash reproduction. No-op outside a dev build or a browser. */
|
|
19
|
+
export function attachPlayerStoreDevHandle(store: unknown): void {
|
|
20
|
+
if (!isDevBuild() || typeof window === "undefined") return;
|
|
21
|
+
(window as unknown as { __playerStore?: unknown }).__playerStore = store;
|
|
22
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selection-set arithmetic for the player store, and the reveal request's
|
|
3
|
+
* cross-id-space match.
|
|
4
|
+
*
|
|
5
|
+
* Its own module so `playerStore.ts` stays under the studio's 600-line cap.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { splitTimelineElementKey } from "../lib/timelineElementHelpers";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The id set a selection change leaves behind.
|
|
12
|
+
*
|
|
13
|
+
* `preserveSet` means "keep the multi-selection if this id is already in it" —
|
|
14
|
+
* a DOM→store echo re-announcing a member must not collapse the set — and
|
|
15
|
+
* anything else is a genuine single selection.
|
|
16
|
+
*/
|
|
17
|
+
export function nextSelectionSet(
|
|
18
|
+
current: ReadonlySet<string>,
|
|
19
|
+
id: string | null,
|
|
20
|
+
preserveSet: boolean | undefined,
|
|
21
|
+
): Set<string> {
|
|
22
|
+
if (preserveSet) return id && current.has(id) ? new Set(current) : new Set<string>();
|
|
23
|
+
return id ? new Set([id]) : new Set<string>();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Is a pending reveal request aimed at the element being selected?
|
|
28
|
+
*
|
|
29
|
+
* Compared across the ID-SPACE BOUNDARY, which is why it is a named function:
|
|
30
|
+
* a request carries the BARE dom id (`runtimeAudioId` — the panel and the
|
|
31
|
+
* runtime speak that), while the store's ids are `sourceFile#domId`. A direct
|
|
32
|
+
* `===` between the two is silently never true, which is the exact shape of
|
|
33
|
+
* failure the split produces and how the reveal came to be dead.
|
|
34
|
+
*/
|
|
35
|
+
export function revealTargetsSelection(
|
|
36
|
+
request: { elementKey: string } | null,
|
|
37
|
+
id: string | null,
|
|
38
|
+
): boolean {
|
|
39
|
+
if (!request || id === null) return false;
|
|
40
|
+
return request.elementKey === splitTimelineElementKey(id).domId;
|
|
41
|
+
}
|