@hyperframes/studio 0.8.6 → 0.8.8
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-DdNgW-Np.js → hyperframes-player-DYTqZd0d.js} +1 -1
- package/dist/assets/{index-bv8b5eiQ.js → index-B69X8UsY.js} +1 -1
- package/dist/assets/index-Ba9zbpT9.css +1 -0
- package/dist/assets/{index-Ci7Uy8fR.js → index-D6CV2x61.js} +1 -1
- package/dist/assets/index-ng4tq6YE.js +428 -0
- package/dist/index.d.ts +108 -18
- package/dist/index.html +2 -2
- package/dist/index.js +10335 -7201
- 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 +3 -5
- 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 +253 -1
- package/src/hooks/timelineTrackVisibility.ts +207 -8
- 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 +8 -8
- 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 +8 -1
- 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.test.tsx +2 -1
- package/src/player/components/TimelineTrackHeader.tsx +74 -98
- package/src/player/components/TimelineTrackPlainHeader.test.tsx +161 -0
- package/src/player/components/TimelineTrackPlainHeader.tsx +126 -0
- package/src/player/components/VideoThumbnail.tsx +6 -1
- package/src/player/components/menuKeyboardNav.ts +47 -0
- package/src/player/components/timelineCallbacks.ts +40 -1
- 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.test.ts +103 -0
- package/src/player/components/useTimelineTrackDerivations.ts +143 -6
- package/src/player/components/useTimelineTrackLayout.ts +61 -4
- package/src/player/hooks/previewMessageRouter.ts +127 -0
- 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-Cy6fGN1u.js +0 -428
- package/dist/assets/index-DcF4s1PG.css +0 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Row derivation for audio groups.
|
|
5
|
+
*
|
|
6
|
+
* This ran behind the `audio-groups` canary at 0% until the rollout, so it had
|
|
7
|
+
* never executed in the enabled state in any suite — the off-cohort branch
|
|
8
|
+
* returned raw tracks and stopped. Now it reorders rows and emits synthetic
|
|
9
|
+
* anchor rows for every user, which is exactly the pair of invariants pinned
|
|
10
|
+
* here: an ungrouped project is untouched, and a group's members become
|
|
11
|
+
* contiguous under an anchor at `memberTracks[0] - 0.5`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React, { act } from "react";
|
|
15
|
+
import { createRoot } from "react-dom/client";
|
|
16
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
17
|
+
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
18
|
+
import { useTimelineTrackDerivations } from "./useTimelineTrackDerivations";
|
|
19
|
+
|
|
20
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
usePlayerStore.getState().reset();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function clip(id: string, track: number, extra: Partial<TimelineElement> = {}): TimelineElement {
|
|
27
|
+
return { id, label: id, tag: "audio", start: 0, duration: 1, track, ...extra };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function derive(elements: TimelineElement[]): ReturnType<typeof useTimelineTrackDerivations> {
|
|
31
|
+
let result: ReturnType<typeof useTimelineTrackDerivations> | undefined;
|
|
32
|
+
function Probe() {
|
|
33
|
+
result = useTimelineTrackDerivations(elements);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const root = createRoot(document.createElement("div"));
|
|
37
|
+
act(() => root.render(React.createElement(Probe)));
|
|
38
|
+
act(() => root.unmount());
|
|
39
|
+
if (!result) throw new Error("derivations did not render");
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("useTimelineTrackDerivations", () => {
|
|
44
|
+
it("leaves an ungrouped project in raw ascending order", () => {
|
|
45
|
+
const { tracks, groups, trackGroupOf } = derive([clip("b", 2), clip("a", 0), clip("c", 1)]);
|
|
46
|
+
|
|
47
|
+
expect(tracks.map(([track]) => track)).toEqual([0, 1, 2]);
|
|
48
|
+
expect(tracks.map(([, els]) => els.map((el) => el.id))).toEqual([["a"], ["c"], ["b"]]);
|
|
49
|
+
expect(groups).toEqual([]);
|
|
50
|
+
expect(trackGroupOf.size).toBe(0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// The reordering case: track 1 sits between the group's two members in raw
|
|
54
|
+
// order and must not be dragged into the group.
|
|
55
|
+
it("pulls interleaved members contiguous under a synthetic anchor row", () => {
|
|
56
|
+
const { tracks, groups, trackGroupOf } = derive([
|
|
57
|
+
clip("vo-1", 0, { audioGroup: "voiceover", audioGroupLabel: "Voiceover" }),
|
|
58
|
+
clip("music", 1),
|
|
59
|
+
clip("vo-2", 2, { audioGroup: "voiceover" }),
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
// Anchor immediately above the lowest member, then both members, then the
|
|
63
|
+
// ungrouped track keeps its own position after them.
|
|
64
|
+
expect(tracks.map(([track]) => track)).toEqual([-0.5, 0, 2, 1]);
|
|
65
|
+
// The anchor row owns no clips of its own.
|
|
66
|
+
expect(tracks[0]?.[1]).toEqual([]);
|
|
67
|
+
|
|
68
|
+
expect(groups).toHaveLength(1);
|
|
69
|
+
expect(groups[0]).toMatchObject({
|
|
70
|
+
id: "voiceover",
|
|
71
|
+
label: "Voiceover",
|
|
72
|
+
anchorKey: -0.5,
|
|
73
|
+
memberTracks: [0, 2],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(trackGroupOf.get(0)?.id).toBe("voiceover");
|
|
77
|
+
expect(trackGroupOf.get(2)?.id).toBe("voiceover");
|
|
78
|
+
expect(trackGroupOf.has(1)).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("carries the group element's own label, volume and mute onto the row", () => {
|
|
82
|
+
const { groups } = derive([
|
|
83
|
+
clip("vo-1", 3, {
|
|
84
|
+
audioGroup: "vo",
|
|
85
|
+
audioGroupLabel: "VO bus",
|
|
86
|
+
audioGroupVolume: 0.5,
|
|
87
|
+
audioGroupHidden: true,
|
|
88
|
+
}),
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
expect(groups[0]).toMatchObject({
|
|
92
|
+
label: "VO bus",
|
|
93
|
+
anchorKey: 2.5,
|
|
94
|
+
volume: 0.5,
|
|
95
|
+
hidden: true,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("falls back to the group id and unity volume when the bus carries neither", () => {
|
|
100
|
+
const { groups } = derive([clip("sfx-1", 0, { audioGroup: "sfx" })]);
|
|
101
|
+
expect(groups[0]).toMatchObject({ id: "sfx", label: "sfx", volume: 1, hidden: false });
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -2,19 +2,151 @@ import { useMemo } from "react";
|
|
|
2
2
|
import type { TimelineElement } from "../store/playerStore";
|
|
3
3
|
import { getTrackStyle, type TrackVisualStyle } from "./timelineIcons";
|
|
4
4
|
|
|
5
|
+
/** One resolved audio group, positioned in the row order. */
|
|
6
|
+
export interface TimelineTrackGroupInfo {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
/**
|
|
10
|
+
* Synthetic sort key for the group's own row — the same fractional-key
|
|
11
|
+
* convention sub-composition expansion already uses (see
|
|
12
|
+
* timelineTrackDisplay.ts): the first (lowest) member track's number minus
|
|
13
|
+
* 0.5, so it slots in immediately above that member.
|
|
14
|
+
*/
|
|
15
|
+
anchorKey: number;
|
|
16
|
+
/** Member track numbers, ascending. */
|
|
17
|
+
memberTracks: number[];
|
|
18
|
+
/** The group element's `data-volume`, mirrored from a member's parse (B7's slider). */
|
|
19
|
+
volume: number;
|
|
20
|
+
/** The group element's `data-hidden`, mirrored from a member's parse (B5's group mute). */
|
|
21
|
+
hidden: boolean;
|
|
22
|
+
/** The group element's serialized `data-fx-chain`, mirrored from a member's parse (C1's FX entry). */
|
|
23
|
+
fxChain?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface GroupMembership {
|
|
27
|
+
trackToGroupId: Map<number, string>;
|
|
28
|
+
memberTracksByGroup: Map<string, number[]>;
|
|
29
|
+
labelByGroup: Map<string, string>;
|
|
30
|
+
volumeByGroup: Map<string, number>;
|
|
31
|
+
hiddenByGroup: Map<string, boolean>;
|
|
32
|
+
fxChainByGroup: Map<string, string | undefined>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Which track belongs to which group, and each group's label/volume/hidden/fxChain — one pass over raw tracks. */
|
|
36
|
+
function resolveGroupMembership(rawTracks: [number, TimelineElement[]][]): GroupMembership {
|
|
37
|
+
const trackToGroupId = new Map<number, string>();
|
|
38
|
+
const memberTracksByGroup = new Map<string, number[]>();
|
|
39
|
+
const labelByGroup = new Map<string, string>();
|
|
40
|
+
const volumeByGroup = new Map<string, number>();
|
|
41
|
+
const hiddenByGroup = new Map<string, boolean>();
|
|
42
|
+
const fxChainByGroup = new Map<string, string | undefined>();
|
|
43
|
+
for (const [trackNum, elements] of rawTracks) {
|
|
44
|
+
const owner = elements.find((el) => el.audioGroup);
|
|
45
|
+
if (!owner?.audioGroup) continue;
|
|
46
|
+
trackToGroupId.set(trackNum, owner.audioGroup);
|
|
47
|
+
if (!labelByGroup.has(owner.audioGroup)) {
|
|
48
|
+
labelByGroup.set(owner.audioGroup, owner.audioGroupLabel ?? owner.audioGroup);
|
|
49
|
+
volumeByGroup.set(owner.audioGroup, owner.audioGroupVolume ?? 1);
|
|
50
|
+
hiddenByGroup.set(owner.audioGroup, owner.audioGroupHidden ?? false);
|
|
51
|
+
fxChainByGroup.set(owner.audioGroup, owner.audioGroupFxChain);
|
|
52
|
+
}
|
|
53
|
+
const members = memberTracksByGroup.get(owner.audioGroup) ?? [];
|
|
54
|
+
members.push(trackNum);
|
|
55
|
+
memberTracksByGroup.set(owner.audioGroup, members);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
trackToGroupId,
|
|
59
|
+
memberTracksByGroup,
|
|
60
|
+
labelByGroup,
|
|
61
|
+
volumeByGroup,
|
|
62
|
+
hiddenByGroup,
|
|
63
|
+
fxChainByGroup,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One group's resolved row info, built once the first time its id is seen. */
|
|
68
|
+
function buildGroupInfo(
|
|
69
|
+
groupId: string,
|
|
70
|
+
fallbackTrackNum: number,
|
|
71
|
+
membership: GroupMembership,
|
|
72
|
+
): TimelineTrackGroupInfo {
|
|
73
|
+
const memberTracks = [...(membership.memberTracksByGroup.get(groupId) ?? [])].sort(
|
|
74
|
+
(a, b) => a - b,
|
|
75
|
+
);
|
|
76
|
+
const fxChain = membership.fxChainByGroup.get(groupId);
|
|
77
|
+
return {
|
|
78
|
+
id: groupId,
|
|
79
|
+
label: membership.labelByGroup.get(groupId) ?? groupId,
|
|
80
|
+
anchorKey: (memberTracks[0] ?? fallbackTrackNum) - 0.5,
|
|
81
|
+
memberTracks,
|
|
82
|
+
volume: membership.volumeByGroup.get(groupId) ?? 1,
|
|
83
|
+
hidden: membership.hiddenByGroup.get(groupId) ?? false,
|
|
84
|
+
...(fxChain ? { fxChain } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Push a group's synthetic anchor row plus its members' rows, contiguously. */
|
|
89
|
+
function emitGroupRows(
|
|
90
|
+
info: TimelineTrackGroupInfo,
|
|
91
|
+
rawByTrack: ReadonlyMap<number, TimelineElement[]>,
|
|
92
|
+
trackGroupOf: Map<number, TimelineTrackGroupInfo>,
|
|
93
|
+
tracks: [number, TimelineElement[]][],
|
|
94
|
+
): void {
|
|
95
|
+
tracks.push([info.anchorKey, []]);
|
|
96
|
+
for (const member of info.memberTracks) {
|
|
97
|
+
trackGroupOf.set(member, info);
|
|
98
|
+
tracks.push([member, rawByTrack.get(member) ?? []]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Pull each group's members out of raw ascending track order and re-emit them
|
|
104
|
+
* contiguously, directly beneath a synthetic anchor row. Ungrouped tracks keep
|
|
105
|
+
* their position; a group's members move up to sit under its anchor even when
|
|
106
|
+
* other (ungrouped) tracks were interleaved between them.
|
|
107
|
+
*/
|
|
108
|
+
function groupTimelineTracks(rawTracks: [number, TimelineElement[]][]): {
|
|
109
|
+
tracks: [number, TimelineElement[]][];
|
|
110
|
+
groups: TimelineTrackGroupInfo[];
|
|
111
|
+
trackGroupOf: Map<number, TimelineTrackGroupInfo>;
|
|
112
|
+
} {
|
|
113
|
+
const membership = resolveGroupMembership(rawTracks);
|
|
114
|
+
const rawByTrack = new Map(rawTracks);
|
|
115
|
+
const groups: TimelineTrackGroupInfo[] = [];
|
|
116
|
+
const trackGroupOf = new Map<number, TimelineTrackGroupInfo>();
|
|
117
|
+
const emitted = new Set<string>();
|
|
118
|
+
const tracks: [number, TimelineElement[]][] = [];
|
|
119
|
+
|
|
120
|
+
for (const [trackNum, elements] of rawTracks) {
|
|
121
|
+
const groupId = membership.trackToGroupId.get(trackNum);
|
|
122
|
+
if (!groupId) {
|
|
123
|
+
tracks.push([trackNum, elements]);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (emitted.has(groupId)) continue;
|
|
127
|
+
emitted.add(groupId);
|
|
128
|
+
const info = buildGroupInfo(groupId, trackNum, membership);
|
|
129
|
+
groups.push(info);
|
|
130
|
+
emitGroupRows(info, rawByTrack, trackGroupOf, tracks);
|
|
131
|
+
}
|
|
132
|
+
return { tracks, groups, trackGroupOf };
|
|
133
|
+
}
|
|
134
|
+
|
|
5
135
|
/**
|
|
6
136
|
* 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.
|
|
137
|
+
* clip grouping (`tracks`, group-aware order), per-lane visual styles, the
|
|
138
|
+
* matching `trackOrder`, and audio-group membership. Extracted from
|
|
139
|
+
* Timeline.tsx as a cohesive unit (600-line studio cap); each memo keys on the
|
|
140
|
+
* expanded display element set exactly as before.
|
|
11
141
|
*/
|
|
12
142
|
export function useTimelineTrackDerivations(expandedElements: TimelineElement[]): {
|
|
13
143
|
tracks: [number, TimelineElement[]][];
|
|
14
144
|
trackStyles: Map<number, TrackVisualStyle>;
|
|
15
145
|
trackOrder: number[];
|
|
146
|
+
groups: TimelineTrackGroupInfo[];
|
|
147
|
+
trackGroupOf: Map<number, TimelineTrackGroupInfo>;
|
|
16
148
|
} {
|
|
17
|
-
const
|
|
149
|
+
const rawTracks = useMemo(() => {
|
|
18
150
|
const map = new Map<number, TimelineElement[]>();
|
|
19
151
|
for (const el of expandedElements) {
|
|
20
152
|
const list = map.get(el.track) ?? [];
|
|
@@ -24,6 +156,11 @@ export function useTimelineTrackDerivations(expandedElements: TimelineElement[])
|
|
|
24
156
|
return Array.from(map.entries()).sort(([a], [b]) => a - b);
|
|
25
157
|
}, [expandedElements]);
|
|
26
158
|
|
|
159
|
+
const { tracks, groups, trackGroupOf } = useMemo(
|
|
160
|
+
() => groupTimelineTracks(rawTracks),
|
|
161
|
+
[rawTracks],
|
|
162
|
+
);
|
|
163
|
+
|
|
27
164
|
const trackStyles = useMemo(() => {
|
|
28
165
|
const map = new Map<number, TrackVisualStyle>();
|
|
29
166
|
for (const [trackNum, els] of tracks) {
|
|
@@ -34,5 +171,5 @@ export function useTimelineTrackDerivations(expandedElements: TimelineElement[])
|
|
|
34
171
|
|
|
35
172
|
const trackOrder = useMemo(() => tracks.map(([trackNum]) => trackNum), [tracks]);
|
|
36
173
|
|
|
37
|
-
return { tracks, trackStyles, trackOrder };
|
|
174
|
+
return { tracks, trackStyles, trackOrder, groups, trackGroupOf };
|
|
38
175
|
}
|
|
@@ -7,12 +7,14 @@ 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,
|
|
10
11
|
TRACK_H,
|
|
11
12
|
createTimelineRowGeometry,
|
|
12
13
|
type TimelineRowGeometry,
|
|
13
14
|
trackHeights,
|
|
14
15
|
type TimelineTrackHeightClip,
|
|
15
16
|
} from "./timelineLayout";
|
|
17
|
+
import type { TimelineTrackGroupInfo } from "./useTimelineTrackDerivations";
|
|
16
18
|
|
|
17
19
|
export { getTrackStyle } from "./timelineIcons";
|
|
18
20
|
|
|
@@ -127,13 +129,35 @@ function computeLaneCounts(
|
|
|
127
129
|
return laneCounts;
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
/** Group anchor rows have no elements of their own (`groupTimelineTracks`
|
|
133
|
+
* pushes them as `[anchorKey, []]`), so `trackHeights` — which only ever
|
|
134
|
+
* looks at a row's clips — always gives them TRACK_H. Override those
|
|
135
|
+
* specific rows post-hoc: TRACK_H while collapsed, +STRIP_H once the
|
|
136
|
+
* group's own `∿` (bus strip) is open. */
|
|
137
|
+
function applyGroupStripHeights(
|
|
138
|
+
tracks: readonly (readonly [number, readonly TimelineElement[]])[],
|
|
139
|
+
rowHeights: number[],
|
|
140
|
+
groups: readonly TimelineTrackGroupInfo[],
|
|
141
|
+
expandedLaneOwnerIds: ReadonlySet<string>,
|
|
142
|
+
): number[] {
|
|
143
|
+
if (groups.length === 0) return rowHeights;
|
|
144
|
+
const groupByAnchor = new Map(groups.map((group) => [group.anchorKey, group]));
|
|
145
|
+
return tracks.map(([track], index) => {
|
|
146
|
+
const group = groupByAnchor.get(track);
|
|
147
|
+
if (!group || !expandedLaneOwnerIds.has(group.id)) return rowHeights[index] ?? TRACK_H;
|
|
148
|
+
return TRACK_H + STRIP_H;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
130
152
|
function useTimelineRowHeights(
|
|
131
153
|
tracks: [number, TimelineElement[]][],
|
|
132
154
|
gsapAnimations: Map<string, GsapAnimation[]>,
|
|
133
155
|
selectedElementId: string | null,
|
|
134
156
|
selectedElementIds: ReadonlySet<string>,
|
|
157
|
+
groups: readonly TimelineTrackGroupInfo[],
|
|
135
158
|
) {
|
|
136
159
|
const expandedClipIds = usePlayerStore((s) => s.expandedClipIds);
|
|
160
|
+
const expandedLaneOwnerIds = usePlayerStore((s) => s.expandedLaneOwnerIds);
|
|
137
161
|
const { laneCounts, rowGeometry } = useMemo(() => {
|
|
138
162
|
const laneCounts = computeLaneCounts(tracks, gsapAnimations);
|
|
139
163
|
// Keyframe lanes follow only the active clip, so a track with several
|
|
@@ -163,7 +187,12 @@ function useTimelineRowHeights(
|
|
|
163
187
|
},
|
|
164
188
|
];
|
|
165
189
|
});
|
|
166
|
-
const rowHeights =
|
|
190
|
+
const rowHeights = applyGroupStripHeights(
|
|
191
|
+
tracks,
|
|
192
|
+
trackHeights(heightTracks, expandedClipIds),
|
|
193
|
+
groups,
|
|
194
|
+
expandedLaneOwnerIds,
|
|
195
|
+
);
|
|
167
196
|
return {
|
|
168
197
|
laneCounts,
|
|
169
198
|
rowGeometry: createTimelineRowGeometry(
|
|
@@ -171,7 +200,15 @@ function useTimelineRowHeights(
|
|
|
171
200
|
rowHeights,
|
|
172
201
|
),
|
|
173
202
|
};
|
|
174
|
-
}, [
|
|
203
|
+
}, [
|
|
204
|
+
expandedClipIds,
|
|
205
|
+
expandedLaneOwnerIds,
|
|
206
|
+
gsapAnimations,
|
|
207
|
+
groups,
|
|
208
|
+
tracks,
|
|
209
|
+
selectedElementId,
|
|
210
|
+
selectedElementIds,
|
|
211
|
+
]);
|
|
175
212
|
const rowGeometryRef = useRef<TimelineRowGeometry>(rowGeometry);
|
|
176
213
|
rowGeometryRef.current = rowGeometry;
|
|
177
214
|
return {
|
|
@@ -188,7 +225,8 @@ export function useTimelineTrackLayout(
|
|
|
188
225
|
selectedElementId: string | null,
|
|
189
226
|
selectedElementIds: ReadonlySet<string>,
|
|
190
227
|
) {
|
|
191
|
-
const { tracks, trackStyles, trackOrder } =
|
|
228
|
+
const { tracks, trackStyles, trackOrder, groups, trackGroupOf } =
|
|
229
|
+
useTimelineTrackDerivations(expandedElements);
|
|
192
230
|
const trackOrderRef = useRef(trackOrder);
|
|
193
231
|
trackOrderRef.current = trackOrder;
|
|
194
232
|
const { laneCounts, rowGeometry, rowGeometryRef, rowHeights } = useTimelineRowHeights(
|
|
@@ -196,6 +234,7 @@ export function useTimelineTrackLayout(
|
|
|
196
234
|
gsapAnimations,
|
|
197
235
|
selectedElementId,
|
|
198
236
|
selectedElementIds,
|
|
237
|
+
groups,
|
|
199
238
|
);
|
|
200
239
|
|
|
201
240
|
return {
|
|
@@ -207,6 +246,8 @@ export function useTimelineTrackLayout(
|
|
|
207
246
|
rowGeometry,
|
|
208
247
|
rowGeometryRef,
|
|
209
248
|
rowHeights,
|
|
249
|
+
groups,
|
|
250
|
+
trackGroupOf,
|
|
210
251
|
};
|
|
211
252
|
}
|
|
212
253
|
|
|
@@ -227,7 +268,23 @@ function useDisplayRowHeights(
|
|
|
227
268
|
function useDisplayTrackOrder(draggedClip: DraggedClipState | null, trackOrder: number[]) {
|
|
228
269
|
return useMemo(() => {
|
|
229
270
|
if (!draggedClip?.started || trackOrder.includes(draggedClip.previewTrack)) return trackOrder;
|
|
230
|
-
|
|
271
|
+
// A group's members sit out of raw numeric order (pulled under their
|
|
272
|
+
// anchor row), so a plain numeric sort here would undo that grouping the
|
|
273
|
+
// moment a clip drags onto a brand-new track. Insert the new preview
|
|
274
|
+
// track only relative to other REAL (integer) tracks, leaving any
|
|
275
|
+
// fractional group-anchor keys exactly where grouping placed them.
|
|
276
|
+
const preview = draggedClip.previewTrack;
|
|
277
|
+
const result: number[] = [];
|
|
278
|
+
let inserted = false;
|
|
279
|
+
for (const key of trackOrder) {
|
|
280
|
+
if (!inserted && Number.isInteger(key) && key > preview) {
|
|
281
|
+
result.push(preview);
|
|
282
|
+
inserted = true;
|
|
283
|
+
}
|
|
284
|
+
result.push(key);
|
|
285
|
+
}
|
|
286
|
+
if (!inserted) result.push(preview);
|
|
287
|
+
return result;
|
|
231
288
|
}, [draggedClip, trackOrder]);
|
|
232
289
|
}
|
|
233
290
|
|