@hyperframes/studio 0.8.5 → 0.8.7
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-DcM3fVcp.js → hyperframes-player-bDcU464i.js} +1 -1
- package/dist/assets/index-Ba9zbpT9.css +1 -0
- package/dist/assets/index-CUXnyoIa.js +428 -0
- package/dist/assets/{index-DgJrvYGf.js → index-FkAs3zRu.js} +1 -1
- package/dist/assets/{index-Del6XWZj.js → index-MSOSP1-J.js} +1 -1
- package/dist/index.d.ts +98 -17
- package/dist/index.html +2 -2
- package/dist/index.js +10337 -7200
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +22 -23
- package/src/captions/components/CaptionAnimationPanel.tsx +46 -13
- package/src/captions/components/CaptionOverlay.tsx +172 -29
- package/src/captions/components/CaptionOverlayUtils.ts +54 -0
- package/src/captions/components/CaptionPropertyPanel.tsx +49 -23
- package/src/captions/components/CaptionTimeline.tsx +36 -79
- package/src/captions/components/shared.tsx +76 -1
- package/src/captions/hooks/useCaptionSync.ts +72 -13
- package/src/captions/keyboard.test.ts +12 -0
- package/src/captions/keyboard.ts +12 -1
- package/src/captions/store.ts +120 -14
- package/src/components/EditorShell.selectionSync.test.tsx +1 -1
- package/src/components/EditorShell.tsx +13 -1
- package/src/components/StudioRightPanel.tsx +2 -0
- package/src/components/StudioRightPanel.types.ts +1 -0
- package/src/components/editor/AnimationCard.tsx +2 -1
- package/src/components/editor/AnimationCardParts.tsx +6 -3
- package/src/components/editor/ArcPathControls.tsx +10 -4
- package/src/components/editor/BlockParamsPanel.test.tsx +170 -0
- package/src/components/editor/BlockParamsPanel.tsx +153 -11
- package/src/components/editor/BorderRadiusEditor.tsx +3 -22
- package/src/components/editor/FileTree.tsx +41 -12
- package/src/components/editor/FileTreeNodes.tsx +90 -20
- package/src/components/editor/GsapAnimationList.tsx +5 -0
- package/src/components/editor/KeyframeDiamond.tsx +3 -1
- package/src/components/editor/KeyframeEaseList.tsx +3 -2
- package/src/components/editor/KeyframeNavigation.tsx +6 -2
- package/src/components/editor/LayersPanel.tsx +11 -2
- package/src/components/editor/MotionPathNode.tsx +1 -0
- package/src/components/editor/PropertyPanel.test.tsx +7 -6
- package/src/components/editor/PropertyPanel.tsx +13 -5
- package/src/components/editor/PropertyPanelFlat.tsx +2 -0
- package/src/components/editor/SnapToolbar.tsx +20 -4
- package/src/components/editor/TimelineFxPopover.test.tsx +113 -0
- package/src/components/editor/TimelineFxPopover.tsx +138 -0
- package/src/components/editor/Transform3DCube.tsx +22 -1
- package/src/components/editor/manualOffsetDrag.test.ts +49 -0
- package/src/components/editor/manualOffsetDrag.ts +13 -1
- package/src/components/editor/propertyPanel3dTransform.tsx +11 -3
- package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +139 -3
- package/src/components/editor/propertyPanelAudioFxGroup.tsx +4 -0
- package/src/components/editor/propertyPanelColor.tsx +9 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +28 -7
- package/src/components/editor/propertyPanelCommitField.tsx +9 -1
- package/src/components/editor/propertyPanelFill.tsx +50 -2
- package/src/components/editor/propertyPanelFlatProps.ts +1 -0
- package/src/components/editor/propertyPanelFont.tsx +45 -9
- package/src/components/editor/propertyPanelFxPresetStyle.ts +18 -0
- package/src/components/editor/propertyPanelFxSection.test.tsx +3 -3
- package/src/components/editor/propertyPanelFxSection.tsx +8 -23
- package/src/components/editor/propertyPanelPrimitives.tsx +9 -12
- package/src/components/editor/propertyPanelStyleSections.tsx +21 -2
- package/src/components/editor/propertyPanelTransformCommit.ts +3 -3
- package/src/components/editor/propertyPanelTypes.ts +2 -0
- package/src/components/editor/useApplyAudioFxPreset.ts +36 -0
- package/src/components/editor/useFxCarve.ts +0 -0
- package/src/components/editor/useFxCarveGrouping.ts +132 -0
- package/src/components/nle/PreviewOverlays.tsx +69 -2
- package/src/components/nle/PreviewPane.tsx +1 -0
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +1 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +27 -0
- package/src/components/panels/SlideshowPanel.tsx +81 -12
- package/src/components/panels/SlideshowSubPanels.tsx +61 -7
- package/src/components/sidebar/AssetCard.test.tsx +2 -2
- package/src/components/sidebar/AssetCard.tsx +94 -46
- package/src/components/sidebar/AssetContextMenu.tsx +196 -42
- package/src/components/sidebar/AssetsTab.tsx +156 -96
- package/src/components/sidebar/AudioRow.tsx +95 -40
- package/src/components/sidebar/BlocksTab.tsx +40 -128
- package/src/components/sidebar/CompositionsTab.tsx +46 -34
- package/src/components/sidebar/LeftSidebar.tsx +87 -75
- package/src/components/sidebar/PromptPreviewModal.tsx +126 -0
- package/src/components/sidebar/assetHelpers.ts +8 -0
- package/src/contexts/TimelineEditContext.tsx +5 -0
- package/src/hooks/timelineAudioGroupVolume.ts +148 -0
- package/src/hooks/timelineEditingHelpers.ts +78 -1
- package/src/hooks/timelineElementFxAttribute.ts +150 -0
- package/src/hooks/timelineTrackVisibility.test.ts +222 -1
- package/src/hooks/timelineTrackVisibility.ts +187 -4
- package/src/hooks/useAppHotkeys.ts +22 -0
- package/src/hooks/useAudioSoloBridge.ts +61 -0
- package/src/hooks/useCaptionDetection.ts +22 -2
- package/src/hooks/useDomEditCommits.test.tsx +24 -0
- package/src/hooks/useEffectiveTimelineDuration.ts +20 -0
- package/src/hooks/useGroupLevel.ts +36 -0
- package/src/hooks/useHydrateActiveCompPathFromUrl.ts +36 -0
- package/src/hooks/useMusicBeatAnalysis.ts +13 -0
- package/src/hooks/useTimelineDeleteOps.ts +170 -0
- package/src/hooks/useTimelineEditing.ts +53 -129
- package/src/player/components/AudioWaveform.tsx +29 -26
- package/src/player/components/BeatStrip.test.tsx +5 -9
- package/src/player/components/BeatStrip.tsx +12 -5
- package/src/player/components/ClipContextMenu.tsx +8 -2
- package/src/player/components/EditModal.tsx +48 -7
- package/src/player/components/KeyframeDiamondContextMenu.tsx +90 -10
- package/src/player/components/LayerDisclosureRow.tsx +12 -13
- package/src/player/components/Player.tsx +43 -8
- package/src/player/components/PlayerControls.tsx +107 -63
- package/src/player/components/ShortcutsPanel.tsx +23 -4
- package/src/player/components/SpeedMenu.tsx +34 -42
- package/src/player/components/Timeline.test.ts +7 -7
- package/src/player/components/Timeline.tsx +7 -10
- package/src/player/components/TimelineFxButton.test.tsx +81 -0
- package/src/player/components/TimelineFxButton.tsx +143 -0
- package/src/player/components/TimelineGroupBusStrip.test.tsx +146 -0
- package/src/player/components/TimelineGroupBusStrip.tsx +103 -0
- package/src/player/components/TimelineGroupHeader.tsx +176 -0
- package/src/player/components/TimelineGroupRow.tsx +146 -0
- package/src/player/components/TimelineLanes.test.tsx +5 -0
- package/src/player/components/TimelineLanes.tsx +39 -33
- package/src/player/components/TimelineOverlays.tsx +34 -0
- package/src/player/components/TimelineSoloButton.tsx +32 -0
- package/src/player/components/TimelineTrackHeader.tsx +74 -98
- package/src/player/components/TimelineTrackPlainHeader.tsx +119 -0
- package/src/player/components/VideoThumbnail.tsx +6 -1
- package/src/player/components/menuKeyboardNav.ts +47 -0
- package/src/player/components/timelineCallbacks.ts +26 -0
- package/src/player/components/timelineKeyboardNavigation.test.ts +8 -0
- package/src/player/components/timelineKeyboardNavigation.ts +147 -30
- package/src/player/components/timelineLaneProps.ts +3 -0
- package/src/player/components/timelineLayout.ts +1 -1
- package/src/player/components/timelineNavigationIdentity.ts +4 -0
- package/src/player/components/useTimelineLaneRowIndexes.ts +38 -0
- package/src/player/components/useTimelineLogicalFocus.ts +9 -0
- package/src/player/components/useTimelineLogicalRows.test.tsx +8 -0
- package/src/player/components/useTimelineLogicalRows.ts +17 -12
- package/src/player/components/useTimelineMultiDragActorWindows.ts +30 -0
- package/src/player/components/useTimelineTrackDerivations.ts +150 -6
- package/src/player/components/useTimelineTrackLayout.ts +61 -4
- package/src/player/hooks/previewMessageRouter.ts +127 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +64 -17
- package/src/player/hooks/useExpandedTimelineElements.ts +8 -11
- package/src/player/hooks/useTimelinePlayer.ts +9 -47
- package/src/player/lib/timelineDOM.ts +51 -0
- package/src/player/store/audioSoloSlice.test.ts +113 -0
- package/src/player/store/audioSoloSlice.ts +43 -0
- package/src/player/store/editingModeSlice.ts +45 -0
- package/src/player/store/groupLevels.ts +33 -0
- package/src/player/store/keyframeSlice.ts +26 -0
- package/src/player/store/playerStore.ts +21 -22
- package/src/player/store/timelineElement.ts +10 -0
- package/src/telemetry/events.ts +12 -0
- package/dist/assets/index-Cx_UrhoW.js +0 -428
- package/dist/assets/index-DcF4s1PG.css +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { GsapAnimation, PropertyGroupName } from "@hyperframes/core/gsap-parser";
|
|
2
2
|
import type { TimelineElement } from "../store/playerStore";
|
|
3
3
|
import { getTimelinePropertyLanes } from "./TimelinePropertyLanes";
|
|
4
|
+
import { groupAutomationLanes } from "./automationLaneData";
|
|
4
5
|
import {
|
|
5
6
|
timelineKeyframeSelectionKey,
|
|
6
7
|
type TimelineKeyframeTarget,
|
|
@@ -8,11 +9,13 @@ import {
|
|
|
8
9
|
import {
|
|
9
10
|
timelineClipFocusId,
|
|
10
11
|
timelineEaseFocusId,
|
|
12
|
+
timelineGroupRowId,
|
|
11
13
|
timelineKeyframeFocusId,
|
|
12
14
|
timelinePropertyRowId,
|
|
13
15
|
timelineTrackRowId,
|
|
14
16
|
} from "./timelineNavigationIdentity";
|
|
15
17
|
import { resolveTrackKeyframeClip } from "./useTimelineTrackLayout";
|
|
18
|
+
import type { TimelineTrackGroupInfo } from "./useTimelineTrackDerivations";
|
|
16
19
|
|
|
17
20
|
export type TimelineNavigationKey =
|
|
18
21
|
| "ArrowLeft"
|
|
@@ -54,9 +57,11 @@ export interface TimelineLogicalRow {
|
|
|
54
57
|
kind: "row";
|
|
55
58
|
physicalTrackKey: number;
|
|
56
59
|
logicalIndex: number;
|
|
57
|
-
level: 1 | 2;
|
|
60
|
+
level: 1 | 2 | 3;
|
|
58
61
|
parentId: string | null;
|
|
59
62
|
elementId: string | null;
|
|
63
|
+
/** Set only on a group's own row (level 1, no clips of its own). */
|
|
64
|
+
groupId?: string;
|
|
60
65
|
expandable: boolean;
|
|
61
66
|
expanded: boolean;
|
|
62
67
|
propertyGroup?: PropertyGroupName;
|
|
@@ -65,13 +70,19 @@ export interface TimelineLogicalRow {
|
|
|
65
70
|
|
|
66
71
|
export type TimelineLogicalTarget = TimelineLogicalRow | TimelineLogicalItem;
|
|
67
72
|
|
|
68
|
-
interface BuildTimelineLogicalRowsInput {
|
|
73
|
+
export interface BuildTimelineLogicalRowsInput {
|
|
69
74
|
tracks: readonly (readonly [number, readonly TimelineElement[]])[];
|
|
70
75
|
displayTrackOrder: readonly number[];
|
|
71
76
|
laneCounts: ReadonlyMap<string, number>;
|
|
72
77
|
selectedElementId: string | null;
|
|
73
78
|
selectedElementIds: ReadonlySet<string>;
|
|
74
79
|
expandedClipIds: ReadonlySet<string>;
|
|
80
|
+
/** Groups whose member rows the caret has shown (structural, not lanes). */
|
|
81
|
+
expandedGroupIds: ReadonlySet<string>;
|
|
82
|
+
/** Rows (clip id or group id) whose automation-lane rows the `∿` button opened. */
|
|
83
|
+
expandedLaneOwnerIds: ReadonlySet<string>;
|
|
84
|
+
groups: readonly TimelineTrackGroupInfo[];
|
|
85
|
+
trackGroupOf: ReadonlyMap<number, TimelineTrackGroupInfo>;
|
|
75
86
|
gsapAnimations: ReadonlyMap<string, readonly GsapAnimation[]>;
|
|
76
87
|
}
|
|
77
88
|
|
|
@@ -106,6 +117,35 @@ function clipItems(rowId: string, elements: readonly TimelineElement[]): Timelin
|
|
|
106
117
|
});
|
|
107
118
|
}
|
|
108
119
|
|
|
120
|
+
/** A track's active clip (if any), its element id, and its automation lanes. */
|
|
121
|
+
function resolveActiveTrackClip(
|
|
122
|
+
elements: readonly TimelineElement[],
|
|
123
|
+
laneCounts: BuildTimelineLogicalRowsInput["laneCounts"],
|
|
124
|
+
selectedElementId: string | null,
|
|
125
|
+
selectedElementIds: ReadonlySet<string>,
|
|
126
|
+
gsapAnimations: BuildTimelineLogicalRowsInput["gsapAnimations"],
|
|
127
|
+
): {
|
|
128
|
+
activeClip: TimelineElement | null;
|
|
129
|
+
activeId: string | null;
|
|
130
|
+
lanes: ReturnType<typeof getTimelinePropertyLanes>;
|
|
131
|
+
} {
|
|
132
|
+
const activeClip = resolveTrackKeyframeClip(
|
|
133
|
+
elements,
|
|
134
|
+
laneCounts,
|
|
135
|
+
selectedElementId,
|
|
136
|
+
selectedElementIds,
|
|
137
|
+
);
|
|
138
|
+
const activeId = activeClip ? elementId(activeClip) : null;
|
|
139
|
+
const lanes = activeClip
|
|
140
|
+
? getTimelinePropertyLanes(
|
|
141
|
+
gsapAnimations.get(elementId(activeClip)) ?? [],
|
|
142
|
+
activeClip.start,
|
|
143
|
+
activeClip.duration,
|
|
144
|
+
)
|
|
145
|
+
: [];
|
|
146
|
+
return { activeClip, activeId, lanes };
|
|
147
|
+
}
|
|
148
|
+
|
|
109
149
|
function keyframeTarget(
|
|
110
150
|
keyframe: ReturnType<typeof getTimelinePropertyLanes>[number]["keyframes"][number],
|
|
111
151
|
): TimelineKeyframeTarget {
|
|
@@ -167,6 +207,42 @@ function propertyItems(
|
|
|
167
207
|
return items;
|
|
168
208
|
}
|
|
169
209
|
|
|
210
|
+
/** A clip's lanes are visible when either the caret or the `∿` button opened it. */
|
|
211
|
+
function isRowOpen(
|
|
212
|
+
activeId: string | null,
|
|
213
|
+
expandedClipIds: ReadonlySet<string>,
|
|
214
|
+
expandedLaneOwnerIds: ReadonlySet<string>,
|
|
215
|
+
): boolean {
|
|
216
|
+
if (activeId === null) return false;
|
|
217
|
+
return expandedClipIds.has(activeId) || expandedLaneOwnerIds.has(activeId);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** A single automation-lane row, one level deeper than the track/group row that owns it. */
|
|
221
|
+
function buildLaneRow(
|
|
222
|
+
track: number,
|
|
223
|
+
logicalIndex: number,
|
|
224
|
+
activeId: string,
|
|
225
|
+
activeClip: TimelineElement,
|
|
226
|
+
lane: ReturnType<typeof getTimelinePropertyLanes>[number],
|
|
227
|
+
level: 2 | 3,
|
|
228
|
+
parentId: string,
|
|
229
|
+
): TimelineLogicalRow {
|
|
230
|
+
const laneRowId = timelinePropertyRowId(activeId, lane.group);
|
|
231
|
+
return {
|
|
232
|
+
id: laneRowId,
|
|
233
|
+
kind: "row",
|
|
234
|
+
physicalTrackKey: track,
|
|
235
|
+
logicalIndex,
|
|
236
|
+
level,
|
|
237
|
+
parentId,
|
|
238
|
+
elementId: activeId,
|
|
239
|
+
expandable: false,
|
|
240
|
+
expanded: false,
|
|
241
|
+
propertyGroup: lane.group,
|
|
242
|
+
items: propertyItems(laneRowId, activeClip, lane.keyframes),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
170
246
|
/** Canonical model of the treegrid, independent of which virtual rows or clips are mounted. */
|
|
171
247
|
export function buildTimelineLogicalRows({
|
|
172
248
|
tracks,
|
|
@@ -175,57 +251,98 @@ export function buildTimelineLogicalRows({
|
|
|
175
251
|
selectedElementId,
|
|
176
252
|
selectedElementIds,
|
|
177
253
|
expandedClipIds,
|
|
254
|
+
expandedGroupIds,
|
|
255
|
+
expandedLaneOwnerIds,
|
|
256
|
+
groups,
|
|
257
|
+
trackGroupOf,
|
|
178
258
|
gsapAnimations,
|
|
179
259
|
}: BuildTimelineLogicalRowsInput): TimelineLogicalRow[] {
|
|
180
260
|
const trackMap = new Map(tracks);
|
|
261
|
+
const groupByAnchor = new Map(groups.map((group) => [group.anchorKey, group]));
|
|
181
262
|
const rows: TimelineLogicalRow[] = [];
|
|
182
|
-
|
|
263
|
+
|
|
264
|
+
// A real track's own row (level 1 ungrouped, level 2 under a group) plus,
|
|
265
|
+
// when its clip's lanes are open, the lane rows one level deeper.
|
|
266
|
+
function emitTrack(track: number, level: 1 | 2, parentId: string | null): void {
|
|
183
267
|
const elements = trackMap.get(track) ?? [];
|
|
184
268
|
const trackId = timelineTrackRowId(track);
|
|
185
|
-
const activeClip =
|
|
269
|
+
const { activeClip, activeId, lanes } = resolveActiveTrackClip(
|
|
186
270
|
elements,
|
|
187
271
|
laneCounts,
|
|
188
272
|
selectedElementId,
|
|
189
273
|
selectedElementIds,
|
|
274
|
+
gsapAnimations,
|
|
190
275
|
);
|
|
191
|
-
const
|
|
192
|
-
const lanes = activeClip
|
|
193
|
-
? getTimelinePropertyLanes(
|
|
194
|
-
gsapAnimations.get(elementId(activeClip)) ?? [],
|
|
195
|
-
activeClip.start,
|
|
196
|
-
activeClip.duration,
|
|
197
|
-
)
|
|
198
|
-
: [];
|
|
199
|
-
const expanded = activeId !== null && expandedClipIds.has(activeId) && lanes.length > 0;
|
|
276
|
+
const expanded = isRowOpen(activeId, expandedClipIds, expandedLaneOwnerIds) && lanes.length > 0;
|
|
200
277
|
rows.push({
|
|
201
278
|
id: trackId,
|
|
202
279
|
kind: "row",
|
|
203
280
|
physicalTrackKey: track,
|
|
204
281
|
logicalIndex: rows.length,
|
|
205
|
-
level
|
|
206
|
-
parentId
|
|
282
|
+
level,
|
|
283
|
+
parentId,
|
|
207
284
|
elementId: activeId,
|
|
208
285
|
expandable: lanes.length > 0,
|
|
209
286
|
expanded,
|
|
210
287
|
items: clipItems(trackId, elements),
|
|
211
288
|
});
|
|
212
|
-
if (!expanded || !activeClip)
|
|
289
|
+
if (!expanded || !activeClip || !activeId) return;
|
|
213
290
|
for (const lane of lanes) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
291
|
+
rows.push(
|
|
292
|
+
buildLaneRow(track, rows.length, activeId, activeClip, lane, level === 1 ? 2 : 3, trackId),
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// A group's own row (level 1) plus, when its `∿` is open, its own
|
|
298
|
+
// automation-lane rows (level 2) — structural content deferred to whatever
|
|
299
|
+
// step wires group automation editing; this reserves the rows and their
|
|
300
|
+
// count.
|
|
301
|
+
function emitGroup(group: TimelineTrackGroupInfo): void {
|
|
302
|
+
const groupRowId = timelineGroupRowId(group.id);
|
|
303
|
+
const groupExpanded = expandedGroupIds.has(group.id);
|
|
304
|
+
rows.push({
|
|
305
|
+
id: groupRowId,
|
|
306
|
+
kind: "row",
|
|
307
|
+
physicalTrackKey: group.anchorKey,
|
|
308
|
+
logicalIndex: rows.length,
|
|
309
|
+
level: 1,
|
|
310
|
+
parentId: null,
|
|
311
|
+
elementId: null,
|
|
312
|
+
groupId: group.id,
|
|
313
|
+
expandable: group.memberTracks.length > 0,
|
|
314
|
+
expanded: groupExpanded,
|
|
315
|
+
items: [],
|
|
316
|
+
});
|
|
317
|
+
if (expandedLaneOwnerIds.has(group.id)) {
|
|
318
|
+
const memberElements = group.memberTracks.flatMap((track) => trackMap.get(track) ?? []);
|
|
319
|
+
for (const laneGroup of groupAutomationLanes(memberElements)) {
|
|
320
|
+
rows.push({
|
|
321
|
+
id: `${groupRowId}::${laneGroup.key}`,
|
|
322
|
+
kind: "row",
|
|
323
|
+
physicalTrackKey: group.anchorKey,
|
|
324
|
+
logicalIndex: rows.length,
|
|
325
|
+
level: 2,
|
|
326
|
+
parentId: groupRowId,
|
|
327
|
+
elementId: null,
|
|
328
|
+
expandable: false,
|
|
329
|
+
expanded: false,
|
|
330
|
+
items: [],
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (!groupExpanded) return;
|
|
335
|
+
for (const track of group.memberTracks) emitTrack(track, 2, groupRowId);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
for (const key of displayTrackOrder) {
|
|
339
|
+
const group = groupByAnchor.get(key);
|
|
340
|
+
if (group) {
|
|
341
|
+
emitGroup(group);
|
|
342
|
+
continue;
|
|
228
343
|
}
|
|
344
|
+
if (trackGroupOf.has(key)) continue; // emitted above, under its group
|
|
345
|
+
emitTrack(key, 1, null);
|
|
229
346
|
}
|
|
230
347
|
return rows;
|
|
231
348
|
}
|
|
@@ -13,6 +13,7 @@ import type { MultiDragPreviewInput } from "./timelineMultiDragPreview";
|
|
|
13
13
|
import type { TimelineEditCallbacks } from "./timelineCallbacks";
|
|
14
14
|
import type { TimelineLogicalRow } from "./timelineKeyboardNavigation";
|
|
15
15
|
import type { TimelineClipRenderContext } from "./TimelineTypes";
|
|
16
|
+
import type { TimelineTrackGroupInfo } from "./useTimelineTrackDerivations";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Props shared by the scroll container ({@link import("./TimelineCanvas")}) and
|
|
@@ -99,6 +100,8 @@ export interface TimelineLaneBaseProps {
|
|
|
99
100
|
*/
|
|
100
101
|
onContextMenuLane?: (e: React.MouseEvent, track: number, time: number) => void;
|
|
101
102
|
beatAnalysis?: MusicBeatAnalysis | null;
|
|
103
|
+
/** Resolved audio groups, positioned in row order — see useTimelineTrackDerivations. */
|
|
104
|
+
groups: readonly TimelineTrackGroupInfo[];
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
/**
|
|
@@ -7,10 +7,10 @@ export const GUTTER = 32;
|
|
|
7
7
|
export const LABEL_COL_W = 232;
|
|
8
8
|
export const TRACK_H = 48;
|
|
9
9
|
export const LANE_H = 28;
|
|
10
|
+
export const STRIP_H = 40; // group bus strip (B7) — one fixed block, not per-property like LANE_H
|
|
10
11
|
export const RULER_H = 24;
|
|
11
12
|
export const CLIP_Y = 3;
|
|
12
13
|
export const CLIP_HANDLE_W = 18;
|
|
13
|
-
|
|
14
14
|
export interface TimelineBeatEntry {
|
|
15
15
|
readonly index: number;
|
|
16
16
|
readonly time: number;
|
|
@@ -13,6 +13,10 @@ export function timelineTrackRowId(track: number): string {
|
|
|
13
13
|
return stableId("track", track);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export function timelineGroupRowId(groupId: string): string {
|
|
17
|
+
return stableId("group", groupId);
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
export function timelinePropertyRowId(elementId: string, group: PropertyGroupName): string {
|
|
17
21
|
return stableId("property", elementId, group);
|
|
18
22
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { usePlayerStore } from "../store/playerStore";
|
|
3
|
+
import type { TimelineLogicalRow } from "./timelineKeyboardNavigation";
|
|
4
|
+
import type { TimelineTrackGroupInfo } from "./useTimelineTrackDerivations";
|
|
5
|
+
|
|
6
|
+
/** The four pieces of group-disclosure state a group row's header reads and writes. */
|
|
7
|
+
export function useTimelineGroupDisclosure() {
|
|
8
|
+
return {
|
|
9
|
+
expandedGroupIds: usePlayerStore((s) => s.expandedGroupIds),
|
|
10
|
+
expandedLaneOwnerIds: usePlayerStore((s) => s.expandedLaneOwnerIds),
|
|
11
|
+
toggleGroupExpanded: usePlayerStore((s) => s.toggleGroupExpanded),
|
|
12
|
+
toggleLaneOwnerExpanded: usePlayerStore((s) => s.toggleLaneOwnerExpanded),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Two lookups TimelineLanes needs once per render: a row's logical rows by
|
|
17
|
+
* physical key, and which physical key is a group's own anchor row. */
|
|
18
|
+
export function useTimelineLaneRowIndexes(
|
|
19
|
+
logicalRows: readonly TimelineLogicalRow[],
|
|
20
|
+
groups: readonly TimelineTrackGroupInfo[],
|
|
21
|
+
) {
|
|
22
|
+
const logicalRowsByTrack = useMemo(() => {
|
|
23
|
+
const byTrack = new Map<number, TimelineLogicalRow[]>();
|
|
24
|
+
for (const logicalRow of logicalRows) {
|
|
25
|
+
const trackRows = byTrack.get(logicalRow.physicalTrackKey) ?? [];
|
|
26
|
+
trackRows.push(logicalRow);
|
|
27
|
+
byTrack.set(logicalRow.physicalTrackKey, trackRows);
|
|
28
|
+
}
|
|
29
|
+
return byTrack;
|
|
30
|
+
}, [logicalRows]);
|
|
31
|
+
|
|
32
|
+
const groupByAnchor = useMemo(
|
|
33
|
+
() => new Map(groups.map((group) => [group.anchorKey, group])),
|
|
34
|
+
[groups],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return { logicalRowsByTrack, groupByAnchor };
|
|
38
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
|
3
3
|
import type { TimelineElement } from "../store/playerStore";
|
|
4
4
|
import type { TimelineRowGeometry } from "./timelineLayout";
|
|
5
5
|
import type { TimelineScrollViewportSnapshot } from "./useTimelineScrollViewport";
|
|
6
|
+
import type { TimelineTrackGroupInfo } from "./useTimelineTrackDerivations";
|
|
6
7
|
import { useTimelineFocusCoordinator } from "./useTimelineFocusCoordinator";
|
|
7
8
|
import { usePlayerStore } from "../store/playerStore";
|
|
8
9
|
import { useTimelineLogicalRows } from "./useTimelineLogicalRows";
|
|
@@ -15,6 +16,8 @@ interface TimelineLogicalFocusInput {
|
|
|
15
16
|
laneCounts: ReadonlyMap<string, number>;
|
|
16
17
|
selectedElementId: string | null;
|
|
17
18
|
selectedElementIds: ReadonlySet<string>;
|
|
19
|
+
groups: readonly TimelineTrackGroupInfo[];
|
|
20
|
+
trackGroupOf: ReadonlyMap<number, TimelineTrackGroupInfo>;
|
|
18
21
|
gsapAnimations: ReadonlyMap<string, readonly GsapAnimation[]>;
|
|
19
22
|
elements: readonly TimelineElement[];
|
|
20
23
|
pixelsPerSecond: number;
|
|
@@ -32,6 +35,8 @@ interface TimelineLogicalFocusInput {
|
|
|
32
35
|
|
|
33
36
|
export function useTimelineLogicalFocus(input: TimelineLogicalFocusInput) {
|
|
34
37
|
const expandedClipIds = usePlayerStore((state) => state.expandedClipIds);
|
|
38
|
+
const expandedGroupIds = usePlayerStore((state) => state.expandedGroupIds);
|
|
39
|
+
const expandedLaneOwnerIds = usePlayerStore((state) => state.expandedLaneOwnerIds);
|
|
35
40
|
const projectId = usePlayerStore((state) => state.timelineProjectId);
|
|
36
41
|
const logicalRows = useTimelineLogicalRows({
|
|
37
42
|
tracks: input.tracks,
|
|
@@ -40,6 +45,10 @@ export function useTimelineLogicalFocus(input: TimelineLogicalFocusInput) {
|
|
|
40
45
|
selectedElementId: input.selectedElementId,
|
|
41
46
|
selectedElementIds: input.selectedElementIds,
|
|
42
47
|
expandedClipIds,
|
|
48
|
+
expandedGroupIds,
|
|
49
|
+
expandedLaneOwnerIds,
|
|
50
|
+
groups: input.groups,
|
|
51
|
+
trackGroupOf: input.trackGroupOf,
|
|
43
52
|
gsapAnimations: input.gsapAnimations,
|
|
44
53
|
});
|
|
45
54
|
const focus = useTimelineFocusCoordinator({
|
|
@@ -22,6 +22,10 @@ const displayTrackOrder = tracks.map(([track]) => track);
|
|
|
22
22
|
const laneCounts = new Map<string, number>();
|
|
23
23
|
const selectedElementIds = new Set<string>();
|
|
24
24
|
const expandedClipIds = new Set<string>();
|
|
25
|
+
const expandedGroupIds = new Set<string>();
|
|
26
|
+
const expandedLaneOwnerIds = new Set<string>();
|
|
27
|
+
const groups: never[] = [];
|
|
28
|
+
const trackGroupOf = new Map();
|
|
25
29
|
const gsapAnimations = new Map();
|
|
26
30
|
|
|
27
31
|
function Harness({ snapshots }: { snapshots: Array<readonly TimelineLogicalRow[]> }) {
|
|
@@ -33,6 +37,10 @@ function Harness({ snapshots }: { snapshots: Array<readonly TimelineLogicalRow[]
|
|
|
33
37
|
selectedElementId: null,
|
|
34
38
|
selectedElementIds,
|
|
35
39
|
expandedClipIds,
|
|
40
|
+
expandedGroupIds,
|
|
41
|
+
expandedLaneOwnerIds,
|
|
42
|
+
groups,
|
|
43
|
+
trackGroupOf,
|
|
36
44
|
gsapAnimations,
|
|
37
45
|
});
|
|
38
46
|
snapshots.push(logicalRows);
|
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import {
|
|
3
|
+
buildTimelineLogicalRows,
|
|
4
|
+
type BuildTimelineLogicalRowsInput,
|
|
5
|
+
} from "./timelineKeyboardNavigation";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
tracks: readonly (readonly [number, readonly TimelineElement[]])[];
|
|
8
|
-
displayTrackOrder: readonly number[];
|
|
9
|
-
laneCounts: ReadonlyMap<string, number>;
|
|
10
|
-
selectedElementId: string | null;
|
|
11
|
-
selectedElementIds: ReadonlySet<string>;
|
|
12
|
-
expandedClipIds: ReadonlySet<string>;
|
|
13
|
-
gsapAnimations: ReadonlyMap<string, readonly GsapAnimation[]>;
|
|
14
|
-
}
|
|
7
|
+
type TimelineLogicalRowsInput = BuildTimelineLogicalRowsInput;
|
|
15
8
|
|
|
16
9
|
/** Shared by rendering and focus coordination; stable input refs preserve memo identity. */
|
|
17
10
|
export function useTimelineLogicalRows({
|
|
@@ -21,6 +14,10 @@ export function useTimelineLogicalRows({
|
|
|
21
14
|
selectedElementId,
|
|
22
15
|
selectedElementIds,
|
|
23
16
|
expandedClipIds,
|
|
17
|
+
expandedGroupIds,
|
|
18
|
+
expandedLaneOwnerIds,
|
|
19
|
+
groups,
|
|
20
|
+
trackGroupOf,
|
|
24
21
|
gsapAnimations,
|
|
25
22
|
}: TimelineLogicalRowsInput) {
|
|
26
23
|
return useMemo(
|
|
@@ -32,11 +29,19 @@ export function useTimelineLogicalRows({
|
|
|
32
29
|
selectedElementId,
|
|
33
30
|
selectedElementIds,
|
|
34
31
|
expandedClipIds,
|
|
32
|
+
expandedGroupIds,
|
|
33
|
+
expandedLaneOwnerIds,
|
|
34
|
+
groups,
|
|
35
|
+
trackGroupOf,
|
|
35
36
|
gsapAnimations,
|
|
36
37
|
}),
|
|
37
38
|
[
|
|
38
39
|
displayTrackOrder,
|
|
39
40
|
expandedClipIds,
|
|
41
|
+
expandedGroupIds,
|
|
42
|
+
expandedLaneOwnerIds,
|
|
43
|
+
groups,
|
|
44
|
+
trackGroupOf,
|
|
40
45
|
gsapAnimations,
|
|
41
46
|
laneCounts,
|
|
42
47
|
selectedElementId,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { isMultiDragActive, multiDragDeltaSeconds } from "./timelineMultiDragPreview";
|
|
2
|
+
import type { MultiDragPreviewInput } from "./timelineMultiDragPreview";
|
|
3
|
+
import type { TimelineTimeRange } from "../lib/timelineClipIndex";
|
|
4
|
+
|
|
5
|
+
/** The extra render window a live multi-drag needs: its passengers still draw
|
|
6
|
+
* at their ORIGIN position, offset by the drag delta, outside the normal
|
|
7
|
+
* virtualized range. Empty when nothing is dragging. */
|
|
8
|
+
export function useTimelineMultiDragActorWindows(
|
|
9
|
+
multiDragPreview: MultiDragPreviewInput | null,
|
|
10
|
+
rowsVirtualized: boolean,
|
|
11
|
+
renderTimeRange: TimelineTimeRange,
|
|
12
|
+
) {
|
|
13
|
+
const multiDragDelta =
|
|
14
|
+
multiDragPreview && isMultiDragActive(multiDragPreview)
|
|
15
|
+
? multiDragDeltaSeconds(multiDragPreview)
|
|
16
|
+
: 0;
|
|
17
|
+
const actorWindows =
|
|
18
|
+
rowsVirtualized && multiDragPreview && multiDragDelta !== 0
|
|
19
|
+
? [
|
|
20
|
+
{
|
|
21
|
+
range: {
|
|
22
|
+
start: renderTimeRange.start - multiDragDelta,
|
|
23
|
+
end: renderTimeRange.end - multiDragDelta,
|
|
24
|
+
},
|
|
25
|
+
identities: multiDragPreview.selectedKeys,
|
|
26
|
+
},
|
|
27
|
+
]
|
|
28
|
+
: [];
|
|
29
|
+
return actorWindows;
|
|
30
|
+
}
|
|
@@ -1,20 +1,153 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import type { TimelineElement } from "../store/playerStore";
|
|
3
|
+
import { isCanaryEnabled } from "../../telemetry/canary";
|
|
3
4
|
import { getTrackStyle, type TrackVisualStyle } from "./timelineIcons";
|
|
4
5
|
|
|
6
|
+
/** One resolved audio group, positioned in the row order. */
|
|
7
|
+
export interface TimelineTrackGroupInfo {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
/**
|
|
11
|
+
* Synthetic sort key for the group's own row — the same fractional-key
|
|
12
|
+
* convention sub-composition expansion already uses (see
|
|
13
|
+
* timelineTrackDisplay.ts): the first (lowest) member track's number minus
|
|
14
|
+
* 0.5, so it slots in immediately above that member.
|
|
15
|
+
*/
|
|
16
|
+
anchorKey: number;
|
|
17
|
+
/** Member track numbers, ascending. */
|
|
18
|
+
memberTracks: number[];
|
|
19
|
+
/** The group element's `data-volume`, mirrored from a member's parse (B7's slider). */
|
|
20
|
+
volume: number;
|
|
21
|
+
/** The group element's `data-hidden`, mirrored from a member's parse (B5's group mute). */
|
|
22
|
+
hidden: boolean;
|
|
23
|
+
/** The group element's serialized `data-fx-chain`, mirrored from a member's parse (C1's FX entry). */
|
|
24
|
+
fxChain?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface GroupMembership {
|
|
28
|
+
trackToGroupId: Map<number, string>;
|
|
29
|
+
memberTracksByGroup: Map<string, number[]>;
|
|
30
|
+
labelByGroup: Map<string, string>;
|
|
31
|
+
volumeByGroup: Map<string, number>;
|
|
32
|
+
hiddenByGroup: Map<string, boolean>;
|
|
33
|
+
fxChainByGroup: Map<string, string | undefined>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Which track belongs to which group, and each group's label/volume/hidden/fxChain — one pass over raw tracks. */
|
|
37
|
+
function resolveGroupMembership(rawTracks: [number, TimelineElement[]][]): GroupMembership {
|
|
38
|
+
const trackToGroupId = new Map<number, string>();
|
|
39
|
+
const memberTracksByGroup = new Map<string, number[]>();
|
|
40
|
+
const labelByGroup = new Map<string, string>();
|
|
41
|
+
const volumeByGroup = new Map<string, number>();
|
|
42
|
+
const hiddenByGroup = new Map<string, boolean>();
|
|
43
|
+
const fxChainByGroup = new Map<string, string | undefined>();
|
|
44
|
+
for (const [trackNum, elements] of rawTracks) {
|
|
45
|
+
const owner = elements.find((el) => el.audioGroup);
|
|
46
|
+
if (!owner?.audioGroup) continue;
|
|
47
|
+
trackToGroupId.set(trackNum, owner.audioGroup);
|
|
48
|
+
if (!labelByGroup.has(owner.audioGroup)) {
|
|
49
|
+
labelByGroup.set(owner.audioGroup, owner.audioGroupLabel ?? owner.audioGroup);
|
|
50
|
+
volumeByGroup.set(owner.audioGroup, owner.audioGroupVolume ?? 1);
|
|
51
|
+
hiddenByGroup.set(owner.audioGroup, owner.audioGroupHidden ?? false);
|
|
52
|
+
fxChainByGroup.set(owner.audioGroup, owner.audioGroupFxChain);
|
|
53
|
+
}
|
|
54
|
+
const members = memberTracksByGroup.get(owner.audioGroup) ?? [];
|
|
55
|
+
members.push(trackNum);
|
|
56
|
+
memberTracksByGroup.set(owner.audioGroup, members);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
trackToGroupId,
|
|
60
|
+
memberTracksByGroup,
|
|
61
|
+
labelByGroup,
|
|
62
|
+
volumeByGroup,
|
|
63
|
+
hiddenByGroup,
|
|
64
|
+
fxChainByGroup,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One group's resolved row info, built once the first time its id is seen. */
|
|
69
|
+
function buildGroupInfo(
|
|
70
|
+
groupId: string,
|
|
71
|
+
fallbackTrackNum: number,
|
|
72
|
+
membership: GroupMembership,
|
|
73
|
+
): TimelineTrackGroupInfo {
|
|
74
|
+
const memberTracks = [...(membership.memberTracksByGroup.get(groupId) ?? [])].sort(
|
|
75
|
+
(a, b) => a - b,
|
|
76
|
+
);
|
|
77
|
+
const fxChain = membership.fxChainByGroup.get(groupId);
|
|
78
|
+
return {
|
|
79
|
+
id: groupId,
|
|
80
|
+
label: membership.labelByGroup.get(groupId) ?? groupId,
|
|
81
|
+
anchorKey: (memberTracks[0] ?? fallbackTrackNum) - 0.5,
|
|
82
|
+
memberTracks,
|
|
83
|
+
volume: membership.volumeByGroup.get(groupId) ?? 1,
|
|
84
|
+
hidden: membership.hiddenByGroup.get(groupId) ?? false,
|
|
85
|
+
...(fxChain ? { fxChain } : {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Push a group's synthetic anchor row plus its members' rows, contiguously. */
|
|
90
|
+
function emitGroupRows(
|
|
91
|
+
info: TimelineTrackGroupInfo,
|
|
92
|
+
rawByTrack: ReadonlyMap<number, TimelineElement[]>,
|
|
93
|
+
trackGroupOf: Map<number, TimelineTrackGroupInfo>,
|
|
94
|
+
tracks: [number, TimelineElement[]][],
|
|
95
|
+
): void {
|
|
96
|
+
tracks.push([info.anchorKey, []]);
|
|
97
|
+
for (const member of info.memberTracks) {
|
|
98
|
+
trackGroupOf.set(member, info);
|
|
99
|
+
tracks.push([member, rawByTrack.get(member) ?? []]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Pull each group's members out of raw ascending track order and re-emit them
|
|
105
|
+
* contiguously, directly beneath a synthetic anchor row. Ungrouped tracks keep
|
|
106
|
+
* their position; a group's members move up to sit under its anchor even when
|
|
107
|
+
* other (ungrouped) tracks were interleaved between them.
|
|
108
|
+
*/
|
|
109
|
+
function groupTimelineTracks(rawTracks: [number, TimelineElement[]][]): {
|
|
110
|
+
tracks: [number, TimelineElement[]][];
|
|
111
|
+
groups: TimelineTrackGroupInfo[];
|
|
112
|
+
trackGroupOf: Map<number, TimelineTrackGroupInfo>;
|
|
113
|
+
} {
|
|
114
|
+
const membership = resolveGroupMembership(rawTracks);
|
|
115
|
+
const rawByTrack = new Map(rawTracks);
|
|
116
|
+
const groups: TimelineTrackGroupInfo[] = [];
|
|
117
|
+
const trackGroupOf = new Map<number, TimelineTrackGroupInfo>();
|
|
118
|
+
const emitted = new Set<string>();
|
|
119
|
+
const tracks: [number, TimelineElement[]][] = [];
|
|
120
|
+
|
|
121
|
+
for (const [trackNum, elements] of rawTracks) {
|
|
122
|
+
const groupId = membership.trackToGroupId.get(trackNum);
|
|
123
|
+
if (!groupId) {
|
|
124
|
+
tracks.push([trackNum, elements]);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (emitted.has(groupId)) continue;
|
|
128
|
+
emitted.add(groupId);
|
|
129
|
+
const info = buildGroupInfo(groupId, trackNum, membership);
|
|
130
|
+
groups.push(info);
|
|
131
|
+
emitGroupRows(info, rawByTrack, trackGroupOf, tracks);
|
|
132
|
+
}
|
|
133
|
+
return { tracks, groups, trackGroupOf };
|
|
134
|
+
}
|
|
135
|
+
|
|
5
136
|
/**
|
|
6
137
|
* Per-render track derivations Timeline.tsx feeds the canvas/lanes: the lane →
|
|
7
|
-
* clip grouping (`tracks`,
|
|
8
|
-
* `trackOrder`, and
|
|
9
|
-
* cohesive unit (600-line studio cap); each memo keys on the
|
|
10
|
-
* element set exactly as before.
|
|
138
|
+
* clip grouping (`tracks`, group-aware order), per-lane visual styles, the
|
|
139
|
+
* matching `trackOrder`, and audio-group membership. Extracted from
|
|
140
|
+
* Timeline.tsx as a cohesive unit (600-line studio cap); each memo keys on the
|
|
141
|
+
* expanded display element set exactly as before.
|
|
11
142
|
*/
|
|
12
143
|
export function useTimelineTrackDerivations(expandedElements: TimelineElement[]): {
|
|
13
144
|
tracks: [number, TimelineElement[]][];
|
|
14
145
|
trackStyles: Map<number, TrackVisualStyle>;
|
|
15
146
|
trackOrder: number[];
|
|
147
|
+
groups: TimelineTrackGroupInfo[];
|
|
148
|
+
trackGroupOf: Map<number, TimelineTrackGroupInfo>;
|
|
16
149
|
} {
|
|
17
|
-
const
|
|
150
|
+
const rawTracks = useMemo(() => {
|
|
18
151
|
const map = new Map<number, TimelineElement[]>();
|
|
19
152
|
for (const el of expandedElements) {
|
|
20
153
|
const list = map.get(el.track) ?? [];
|
|
@@ -24,6 +157,17 @@ export function useTimelineTrackDerivations(expandedElements: TimelineElement[])
|
|
|
24
157
|
return Array.from(map.entries()).sort(([a], [b]) => a - b);
|
|
25
158
|
}, [expandedElements]);
|
|
26
159
|
|
|
160
|
+
const { tracks, groups, trackGroupOf } = useMemo(() => {
|
|
161
|
+
if (!isCanaryEnabled("audio-groups")) {
|
|
162
|
+
return {
|
|
163
|
+
tracks: rawTracks,
|
|
164
|
+
groups: [],
|
|
165
|
+
trackGroupOf: new Map<number, TimelineTrackGroupInfo>(),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return groupTimelineTracks(rawTracks);
|
|
169
|
+
}, [rawTracks]);
|
|
170
|
+
|
|
27
171
|
const trackStyles = useMemo(() => {
|
|
28
172
|
const map = new Map<number, TrackVisualStyle>();
|
|
29
173
|
for (const [trackNum, els] of tracks) {
|
|
@@ -34,5 +178,5 @@ export function useTimelineTrackDerivations(expandedElements: TimelineElement[])
|
|
|
34
178
|
|
|
35
179
|
const trackOrder = useMemo(() => tracks.map(([trackNum]) => trackNum), [tracks]);
|
|
36
180
|
|
|
37
|
-
return { tracks, trackStyles, trackOrder };
|
|
181
|
+
return { tracks, trackStyles, trackOrder, groups, trackGroupOf };
|
|
38
182
|
}
|