@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
-
import type
|
|
2
|
+
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
3
3
|
import { getTrackStyle, type TrackVisualStyle } from "./timelineIcons";
|
|
4
4
|
|
|
5
5
|
/** One resolved audio group, positioned in the row order. */
|
|
@@ -15,12 +15,26 @@ export interface TimelineTrackGroupInfo {
|
|
|
15
15
|
anchorKey: number;
|
|
16
16
|
/** Member track numbers, ascending. */
|
|
17
17
|
memberTracks: number[];
|
|
18
|
+
/**
|
|
19
|
+
* Every clip under this group, in member-track order — INDEPENDENT of whether
|
|
20
|
+
* the group is expanded.
|
|
21
|
+
*
|
|
22
|
+
* Collapsing a group stops emitting its member rows into `tracks`, so anything
|
|
23
|
+
* that recovered member elements by looking them up there got an empty list in
|
|
24
|
+
* the default (collapsed) state — silently disabling the
|
|
25
|
+
* automation-lane count, and the bus strip's member labels. Membership is not
|
|
26
|
+
* a display concern, so it does not travel through the display list.
|
|
27
|
+
*/
|
|
28
|
+
memberElements: TimelineElement[];
|
|
18
29
|
/** The group element's `data-volume`, mirrored from a member's parse (B7's slider). */
|
|
19
30
|
volume: number;
|
|
20
31
|
/** The group element's `data-hidden`, mirrored from a member's parse (B5's group mute). */
|
|
21
32
|
hidden: boolean;
|
|
22
33
|
/** The group element's serialized `data-fx-chain`, mirrored from a member's parse (C1's FX entry). */
|
|
23
34
|
fxChain?: string;
|
|
35
|
+
/** The group element's serialized `data-automation` — the group's OWN lanes,
|
|
36
|
+
* which are what its `∿` discloses (groups doc §5). */
|
|
37
|
+
automation?: string;
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
interface GroupMembership {
|
|
@@ -30,6 +44,7 @@ interface GroupMembership {
|
|
|
30
44
|
volumeByGroup: Map<string, number>;
|
|
31
45
|
hiddenByGroup: Map<string, boolean>;
|
|
32
46
|
fxChainByGroup: Map<string, string | undefined>;
|
|
47
|
+
automationByGroup: Map<string, string | undefined>;
|
|
33
48
|
}
|
|
34
49
|
|
|
35
50
|
/** Which track belongs to which group, and each group's label/volume/hidden/fxChain — one pass over raw tracks. */
|
|
@@ -40,6 +55,7 @@ function resolveGroupMembership(rawTracks: [number, TimelineElement[]][]): Group
|
|
|
40
55
|
const volumeByGroup = new Map<string, number>();
|
|
41
56
|
const hiddenByGroup = new Map<string, boolean>();
|
|
42
57
|
const fxChainByGroup = new Map<string, string | undefined>();
|
|
58
|
+
const automationByGroup = new Map<string, string | undefined>();
|
|
43
59
|
for (const [trackNum, elements] of rawTracks) {
|
|
44
60
|
const owner = elements.find((el) => el.audioGroup);
|
|
45
61
|
if (!owner?.audioGroup) continue;
|
|
@@ -49,6 +65,7 @@ function resolveGroupMembership(rawTracks: [number, TimelineElement[]][]): Group
|
|
|
49
65
|
volumeByGroup.set(owner.audioGroup, owner.audioGroupVolume ?? 1);
|
|
50
66
|
hiddenByGroup.set(owner.audioGroup, owner.audioGroupHidden ?? false);
|
|
51
67
|
fxChainByGroup.set(owner.audioGroup, owner.audioGroupFxChain);
|
|
68
|
+
automationByGroup.set(owner.audioGroup, owner.audioGroupAutomation);
|
|
52
69
|
}
|
|
53
70
|
const members = memberTracksByGroup.get(owner.audioGroup) ?? [];
|
|
54
71
|
members.push(trackNum);
|
|
@@ -61,6 +78,7 @@ function resolveGroupMembership(rawTracks: [number, TimelineElement[]][]): Group
|
|
|
61
78
|
volumeByGroup,
|
|
62
79
|
hiddenByGroup,
|
|
63
80
|
fxChainByGroup,
|
|
81
|
+
automationByGroup,
|
|
64
82
|
};
|
|
65
83
|
}
|
|
66
84
|
|
|
@@ -69,33 +87,50 @@ function buildGroupInfo(
|
|
|
69
87
|
groupId: string,
|
|
70
88
|
fallbackTrackNum: number,
|
|
71
89
|
membership: GroupMembership,
|
|
90
|
+
rawByTrack: ReadonlyMap<number, TimelineElement[]>,
|
|
72
91
|
): TimelineTrackGroupInfo {
|
|
73
92
|
const memberTracks = [...(membership.memberTracksByGroup.get(groupId) ?? [])].sort(
|
|
74
93
|
(a, b) => a - b,
|
|
75
94
|
);
|
|
76
95
|
const fxChain = membership.fxChainByGroup.get(groupId);
|
|
96
|
+
const automation = membership.automationByGroup.get(groupId);
|
|
77
97
|
return {
|
|
78
98
|
id: groupId,
|
|
79
99
|
label: membership.labelByGroup.get(groupId) ?? groupId,
|
|
100
|
+
// Exactly x.5, which sub-composition child rows are now kept strictly below
|
|
101
|
+
// (`useExpandedTimelineElements`) — they used to be able to land here and
|
|
102
|
+
// collide, duplicating the group header.
|
|
80
103
|
anchorKey: (memberTracks[0] ?? fallbackTrackNum) - 0.5,
|
|
81
104
|
memberTracks,
|
|
105
|
+
memberElements: memberTracks.flatMap((track) => rawByTrack.get(track) ?? []),
|
|
82
106
|
volume: membership.volumeByGroup.get(groupId) ?? 1,
|
|
83
107
|
hidden: membership.hiddenByGroup.get(groupId) ?? false,
|
|
84
108
|
...(fxChain ? { fxChain } : {}),
|
|
109
|
+
...(automation ? { automation } : {}),
|
|
85
110
|
};
|
|
86
111
|
}
|
|
87
112
|
|
|
88
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* Push a group's synthetic anchor row plus its members' rows, contiguously.
|
|
115
|
+
*
|
|
116
|
+
* A collapsed group emits its anchor and nothing else. `buildTimelineLogicalRows`
|
|
117
|
+
* already stops at the anchor when collapsed, so leaving the member rows in
|
|
118
|
+
* `tracks` left row geometry reserving a full row each for rows that then
|
|
119
|
+
* rendered as `null` — a header trailed by its members' worth of blank,
|
|
120
|
+
* unreachable dead space. Membership still lands in `trackGroupOf`: a collapsed
|
|
121
|
+
* member is hidden, not ungrouped.
|
|
122
|
+
*/
|
|
89
123
|
function emitGroupRows(
|
|
90
124
|
info: TimelineTrackGroupInfo,
|
|
91
125
|
rawByTrack: ReadonlyMap<number, TimelineElement[]>,
|
|
92
126
|
trackGroupOf: Map<number, TimelineTrackGroupInfo>,
|
|
93
127
|
tracks: [number, TimelineElement[]][],
|
|
128
|
+
expanded: boolean,
|
|
94
129
|
): void {
|
|
95
130
|
tracks.push([info.anchorKey, []]);
|
|
96
131
|
for (const member of info.memberTracks) {
|
|
97
132
|
trackGroupOf.set(member, info);
|
|
98
|
-
tracks.push([member, rawByTrack.get(member) ?? []]);
|
|
133
|
+
if (expanded) tracks.push([member, rawByTrack.get(member) ?? []]);
|
|
99
134
|
}
|
|
100
135
|
}
|
|
101
136
|
|
|
@@ -105,7 +140,10 @@ function emitGroupRows(
|
|
|
105
140
|
* their position; a group's members move up to sit under its anchor even when
|
|
106
141
|
* other (ungrouped) tracks were interleaved between them.
|
|
107
142
|
*/
|
|
108
|
-
function groupTimelineTracks(
|
|
143
|
+
function groupTimelineTracks(
|
|
144
|
+
rawTracks: [number, TimelineElement[]][],
|
|
145
|
+
collapsedGroupIds: ReadonlySet<string>,
|
|
146
|
+
): {
|
|
109
147
|
tracks: [number, TimelineElement[]][];
|
|
110
148
|
groups: TimelineTrackGroupInfo[];
|
|
111
149
|
trackGroupOf: Map<number, TimelineTrackGroupInfo>;
|
|
@@ -125,9 +163,9 @@ function groupTimelineTracks(rawTracks: [number, TimelineElement[]][]): {
|
|
|
125
163
|
}
|
|
126
164
|
if (emitted.has(groupId)) continue;
|
|
127
165
|
emitted.add(groupId);
|
|
128
|
-
const info = buildGroupInfo(groupId, trackNum, membership);
|
|
166
|
+
const info = buildGroupInfo(groupId, trackNum, membership, rawByTrack);
|
|
129
167
|
groups.push(info);
|
|
130
|
-
emitGroupRows(info, rawByTrack, trackGroupOf, tracks);
|
|
168
|
+
emitGroupRows(info, rawByTrack, trackGroupOf, tracks, !collapsedGroupIds.has(groupId));
|
|
131
169
|
}
|
|
132
170
|
return { tracks, groups, trackGroupOf };
|
|
133
171
|
}
|
|
@@ -156,9 +194,10 @@ export function useTimelineTrackDerivations(expandedElements: TimelineElement[])
|
|
|
156
194
|
return Array.from(map.entries()).sort(([a], [b]) => a - b);
|
|
157
195
|
}, [expandedElements]);
|
|
158
196
|
|
|
197
|
+
const collapsedGroupIds = usePlayerStore((s) => s.collapsedGroupIds);
|
|
159
198
|
const { tracks, groups, trackGroupOf } = useMemo(
|
|
160
|
-
() => groupTimelineTracks(rawTracks),
|
|
161
|
-
[rawTracks],
|
|
199
|
+
() => groupTimelineTracks(rawTracks, collapsedGroupIds),
|
|
200
|
+
[rawTracks, collapsedGroupIds],
|
|
162
201
|
);
|
|
163
202
|
|
|
164
203
|
const trackStyles = useMemo(() => {
|
|
@@ -38,6 +38,125 @@ function renderTrackLayout(
|
|
|
38
38
|
return { layout, unmount: () => act(() => root.unmount()) };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
describe("collapsed audio groups", () => {
|
|
42
|
+
const member = (id: string, track: number): TimelineElement => ({
|
|
43
|
+
id,
|
|
44
|
+
domId: id,
|
|
45
|
+
tag: "audio",
|
|
46
|
+
start: 0,
|
|
47
|
+
duration: 5,
|
|
48
|
+
track,
|
|
49
|
+
audioGroup: "voiceover",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/** `collapsed` seeds the collapsed set — expanded is the default state. */
|
|
53
|
+
function renderGrouped(collapsed = false): {
|
|
54
|
+
layout: ReturnType<typeof useTimelineTrackLayout>;
|
|
55
|
+
unmount: () => void;
|
|
56
|
+
} {
|
|
57
|
+
if (collapsed) usePlayerStore.setState({ collapsedGroupIds: new Set(["voiceover"]) });
|
|
58
|
+
const elements = [member("voice-1", 0), member("voice-2", 1)];
|
|
59
|
+
let layout: ReturnType<typeof useTimelineTrackLayout> | undefined;
|
|
60
|
+
function Probe() {
|
|
61
|
+
layout = useTimelineTrackLayout(elements, new Map(), null, new Set());
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const root = createRoot(document.createElement("div"));
|
|
65
|
+
act(() => root.render(React.createElement(Probe)));
|
|
66
|
+
if (!layout) throw new Error("Timeline track layout did not render");
|
|
67
|
+
return { layout, unmount: () => act(() => root.unmount()) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The `∿` area holds the group's own automation rows. Sized without them,
|
|
71
|
+
// every lane the count had just promised was clipped out of the row — which
|
|
72
|
+
// is what "expanding automation on a group doesn't show the automation"
|
|
73
|
+
// looked like from outside.
|
|
74
|
+
it("reserves room for the group's own automation rows, not just the strip", () => {
|
|
75
|
+
const automation = JSON.stringify({
|
|
76
|
+
version: 1,
|
|
77
|
+
lanes: [
|
|
78
|
+
{
|
|
79
|
+
target: "volume",
|
|
80
|
+
points: [
|
|
81
|
+
{ t: 0, v: 1 },
|
|
82
|
+
{ t: 5, v: 0.4 },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
const elements = [
|
|
88
|
+
{ ...member("voice-1", 0), audioGroupAutomation: automation },
|
|
89
|
+
{ ...member("voice-2", 1), audioGroupAutomation: automation },
|
|
90
|
+
];
|
|
91
|
+
let layout: ReturnType<typeof useTimelineTrackLayout> | undefined;
|
|
92
|
+
function Probe() {
|
|
93
|
+
layout = useTimelineTrackLayout(elements, new Map(), null, new Set());
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const root = createRoot(document.createElement("div"));
|
|
97
|
+
act(() => {
|
|
98
|
+
usePlayerStore.setState({ expandedLaneOwnerIds: new Set(["voiceover"]) });
|
|
99
|
+
root.render(React.createElement(Probe));
|
|
100
|
+
});
|
|
101
|
+
// The group's anchor row: the first member track minus 0.5.
|
|
102
|
+
const anchorIndex = layout!.tracks.findIndex(([track]) => track === -0.5);
|
|
103
|
+
expect(anchorIndex).toBeGreaterThanOrEqual(0);
|
|
104
|
+
const openHeight = layout!.rowHeights[anchorIndex];
|
|
105
|
+
// One lane of headroom beyond the header row itself.
|
|
106
|
+
expect(openHeight).toBe(TRACK_H + AUTOMATION_LANE_H);
|
|
107
|
+
act(() => root.unmount());
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// buildTimelineLogicalRows stops emitting member rows once a group is
|
|
111
|
+
// collapsed, so TimelineLanes renders null for them. Rows left in `tracks`
|
|
112
|
+
// still reserve height, turning that null into visible dead space — the row
|
|
113
|
+
// list and the logical rows have to agree.
|
|
114
|
+
it("emits only the anchor row while the group is collapsed", () => {
|
|
115
|
+
const { layout, unmount } = renderGrouped(true);
|
|
116
|
+
expect(layout.groups).toHaveLength(1);
|
|
117
|
+
expect(layout.groups[0]!.memberTracks).toEqual([0, 1]);
|
|
118
|
+
// The anchor (0 - 0.5) and nothing else.
|
|
119
|
+
expect(layout.trackOrder).toEqual([-0.5]);
|
|
120
|
+
expect(layout.rowGeometry.rowHeights).toHaveLength(1);
|
|
121
|
+
// Membership still resolves — collapsed is hidden, not ungrouped.
|
|
122
|
+
expect(layout.trackGroupOf.get(0)?.id).toBe("voiceover");
|
|
123
|
+
unmount();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Membership is not a display concern. Half-lit solo, the automation-lane
|
|
127
|
+
// count and the bus strip's member labels all read the group's members, and
|
|
128
|
+
// all three silently degraded to empty when those were recovered from the
|
|
129
|
+
// display list — which a collapsed group does not appear in. Collapsed is the
|
|
130
|
+
// default, so that was every group until someone opened it.
|
|
131
|
+
it("carries its member elements even while collapsed", () => {
|
|
132
|
+
const { layout, unmount } = renderGrouped(true);
|
|
133
|
+
expect(layout.trackOrder).toEqual([-0.5]); // collapsed: no member rows
|
|
134
|
+
expect(layout.groups[0]!.memberElements.map((el) => el.id)).toEqual(["voice-1", "voice-2"]);
|
|
135
|
+
unmount();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// The reason the set is stored inverted. As an expanded-set, "absent" could
|
|
139
|
+
// not tell never-touched from deliberately-collapsed, so a freshly created
|
|
140
|
+
// group started collapsed — grouping three tracks made all three vanish
|
|
141
|
+
// behind a header the user had not yet learned to open.
|
|
142
|
+
it("is expanded by default, with nothing seeded", () => {
|
|
143
|
+
const { layout, unmount } = renderGrouped();
|
|
144
|
+
expect(usePlayerStore.getState().collapsedGroupIds.size).toBe(0);
|
|
145
|
+
expect(layout.trackOrder).toEqual([-0.5, 0, 1]);
|
|
146
|
+
unmount();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("emits the member rows once the group is expanded", () => {
|
|
150
|
+
// Expanded is the default now — nothing to seed.
|
|
151
|
+
const { layout, unmount } = renderGrouped();
|
|
152
|
+
expect(layout.trackOrder).toEqual([-0.5, 0, 1]);
|
|
153
|
+
for (const track of layout.groups[0]!.memberTracks) {
|
|
154
|
+
expect(layout.rowGeometry.getRowHeight(layout.rowGeometry.getRowIndex(track))).toBe(TRACK_H);
|
|
155
|
+
}
|
|
156
|
+
unmount();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
41
160
|
describe("useTimelineTrackLayout", () => {
|
|
42
161
|
it("counts a flat tween lane and reserves its expanded row height", () => {
|
|
43
162
|
const elements: TimelineElement[] = [
|
|
@@ -7,7 +7,6 @@ import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
|
7
7
|
import type { DraggedClipState } from "./timelineClipDragTypes";
|
|
8
8
|
import { useTimelineTrackDerivations } from "./useTimelineTrackDerivations";
|
|
9
9
|
import {
|
|
10
|
-
STRIP_H,
|
|
11
10
|
TRACK_H,
|
|
12
11
|
createTimelineRowGeometry,
|
|
13
12
|
type TimelineRowGeometry,
|
|
@@ -15,6 +14,13 @@ import {
|
|
|
15
14
|
type TimelineTrackHeightClip,
|
|
16
15
|
} from "./timelineLayout";
|
|
17
16
|
import type { TimelineTrackGroupInfo } from "./useTimelineTrackDerivations";
|
|
17
|
+
import { groupAutomationElement } from "./groupAutomationElement";
|
|
18
|
+
import { AUTOMATION_LANE_H } from "./automationLaneHeight";
|
|
19
|
+
|
|
20
|
+
/** Automation rows the GROUP itself owns — its `data-automation`, not its members'. */
|
|
21
|
+
function groupOwnLaneCount(group: TimelineTrackGroupInfo): number {
|
|
22
|
+
return groupAutomationLanes([groupAutomationElement(group, 0)]).length;
|
|
23
|
+
}
|
|
18
24
|
|
|
19
25
|
export { getTrackStyle } from "./timelineIcons";
|
|
20
26
|
|
|
@@ -132,8 +138,8 @@ function computeLaneCounts(
|
|
|
132
138
|
/** Group anchor rows have no elements of their own (`groupTimelineTracks`
|
|
133
139
|
* pushes them as `[anchorKey, []]`), so `trackHeights` — which only ever
|
|
134
140
|
* looks at a row's clips — always gives them TRACK_H. Override those
|
|
135
|
-
* specific rows post-hoc: TRACK_H while collapsed,
|
|
136
|
-
*
|
|
141
|
+
* specific rows post-hoc: TRACK_H while collapsed, plus the group's own
|
|
142
|
+
* automation rows once its `∿` is open. */
|
|
137
143
|
function applyGroupStripHeights(
|
|
138
144
|
tracks: readonly (readonly [number, readonly TimelineElement[]])[],
|
|
139
145
|
rowHeights: number[],
|
|
@@ -145,7 +151,9 @@ function applyGroupStripHeights(
|
|
|
145
151
|
return tracks.map(([track], index) => {
|
|
146
152
|
const group = groupByAnchor.get(track);
|
|
147
153
|
if (!group || !expandedLaneOwnerIds.has(group.id)) return rowHeights[index] ?? TRACK_H;
|
|
148
|
-
|
|
154
|
+
// The group's own automation rows, which its `∿` discloses. A row sized
|
|
155
|
+
// without them clipped every lane it had just promised in the count.
|
|
156
|
+
return TRACK_H + groupOwnLaneCount(group) * AUTOMATION_LANE_H;
|
|
149
157
|
});
|
|
150
158
|
}
|
|
151
159
|
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Extracted from `useTimelinePlayer`, which had grown past the studio's 600-line
|
|
5
5
|
* file cap and carried a `fallow-ignore-next-line complexity` on this function
|
|
6
|
-
* admitting the same thing. Nothing here is new logic — it is the same
|
|
7
|
-
* branches (accept-gate,
|
|
8
|
-
*
|
|
6
|
+
* admitting the same thing. Nothing here is new logic — it is the same three
|
|
7
|
+
* branches (accept-gate, state, timeline) against the same refs, with the
|
|
8
|
+
* suppression retired rather than moved. The group-levels branch went with the
|
|
9
|
+
* level meter it fed (see the group volume/meter removal).
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
12
13
|
import type { ClipManifestClip, IframeWindow, PlaybackAdapter } from "../lib/playbackTypes";
|
|
13
14
|
import { hasTimelinePerformanceFixtureLease } from "../lib/timelinePerformanceFixture";
|
|
14
15
|
import { acceptStudioRuntimeMessage } from "../lib/runtimeProtocol";
|
|
15
|
-
import { groupLevels, parseGroupLevelsMessage } from "../store/groupLevels";
|
|
16
16
|
import { parseTimelineFromDOM } from "../lib/timelineDOM";
|
|
17
17
|
|
|
18
18
|
/** What `processTimelineMessage` accepts — the clip-manifest postMessage. */
|
|
@@ -60,12 +60,6 @@ function acceptedPreviewMessage(
|
|
|
60
60
|
return acceptStudioRuntimeMessage(data) ? data : null;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
/** One meter reading per group with an active member. */
|
|
64
|
-
function handleGroupLevelsMessage(data: PreviewMessage): void {
|
|
65
|
-
const levels = parseGroupLevelsMessage(data);
|
|
66
|
-
if (levels) groupLevels.notify(levels);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
63
|
/**
|
|
70
64
|
* A `state` tick doubles as a recovery hook: if the store still has no
|
|
71
65
|
* elements, read the manifest straight off the iframe, and if no `timeline`
|
|
@@ -118,7 +112,6 @@ export function createPreviewMessageHandler(
|
|
|
118
112
|
const iframe = deps.iframeRef.current;
|
|
119
113
|
const data = acceptedPreviewMessage(e, iframe);
|
|
120
114
|
if (!data) return;
|
|
121
|
-
if (data.type === "group-levels") return handleGroupLevelsMessage(data);
|
|
122
115
|
if (data.type === "state") return handleStateMessage(deps, iframe);
|
|
123
116
|
if (data.type === "timeline" && Array.isArray(data.clips)) {
|
|
124
117
|
handleTimelineMessage(deps, iframe, data as unknown as ClipManifestMessage);
|