mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
refactor(studio): split the two files over the 600-line cap (#3313)
The file size check has been failing on main. It is diff-scoped on a PR but full-scans packages/studio on push, so two files that crept over the cap were only ever caught after merge, and every release since has been red: TimelineAutomationLane.tsx 674 lines StudioRightPanel.tsx 609 lines Both are pure code moves. No behavior change. TimelineAutomationLane.tsx keeps the single-lane editor and gives up the track-level layer: ClipLaneRow, ClipAutomationLanes and TimelineAutomationLaneSlot move to TimelineAutomationLaneSlot.tsx, which is the name its test file already used. The dependency runs one way, slot -> lane, so there is no cycle. 674 -> 499. StudioRightPanel.tsx gives up its props interface to a sibling .types.ts. That block is the part that changes least, so moving it keeps the component's own diffs small; two in-flight branches touch this file and both are based on a 556-line copy of it, so keeping the cut away from the body matters. 609 -> 568. Verified by running the CI rule's full-scan branch over the 679 tracked packages/studio source files: no file over 600, exit 0. Studio suite green at 2790 passed across 226 files, and typecheck clean.
This commit is contained in:
@@ -1,19 +1,19 @@
|
||||
import { useCallback, useEffect, useRef, type MutableRefObject } from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import type { StudioRightPanelProps } from "./StudioRightPanel.types";
|
||||
|
||||
export type { StudioRightPanelProps };
|
||||
import { PropertyPanel } from "./editor/PropertyPanel";
|
||||
import { LayersPanel } from "./editor/LayersPanel";
|
||||
import { CaptionPropertyPanel } from "../captions/components/CaptionPropertyPanel";
|
||||
import { BlockParamsPanel } from "./editor/BlockParamsPanel";
|
||||
import { RenderQueue } from "./renders/RenderQueue";
|
||||
import { SlideshowPanel } from "./panels/SlideshowPanel";
|
||||
import { VariablesPanel, type StudioEditPersistenceProps } from "./panels/VariablesPanel";
|
||||
import { VariablesPanel } from "./panels/VariablesPanel";
|
||||
import { PanelTabButton } from "./PanelTabButton";
|
||||
import { usePreviewVariablesStore } from "../hooks/previewVariablesStore";
|
||||
import type { RenderJob } from "./renders/useRenderQueue";
|
||||
import type { BlockParam } from "@hyperframes/core/registry";
|
||||
import { STUDIO_FLAT_INSPECTOR_ENABLED } from "./editor/manualEditingAvailability";
|
||||
import type { Composition } from "@hyperframes/sdk";
|
||||
import type { EditHistoryKind } from "../utils/editHistory";
|
||||
import { useSlideshowPersist, type UseSlideshowPersistParams } from "../hooks/useSlideshowPersist";
|
||||
import { useSlideshowPersist } from "../hooks/useSlideshowPersist";
|
||||
import { useSlideshowTabState } from "../hooks/useSlideshowTabState";
|
||||
import { DesignPanelPromoteProvider } from "./DesignPanelPromoteProvider";
|
||||
import { useStudioPlaybackContext, useStudioShellContext } from "../contexts/StudioContext";
|
||||
@@ -27,52 +27,10 @@ import {
|
||||
EMPTY_COLOR_GRADING_SCOPE_RESULT,
|
||||
type ColorGradingScope,
|
||||
} from "./studioColorGradingScope";
|
||||
import type {
|
||||
AddMediaOverlayHandler,
|
||||
BackgroundRemovalProgress,
|
||||
} from "./editor/propertyPanelTypes";
|
||||
import { timelineKeysForSelections, type ToggleHiddenHandler } from "../utils/studioHelpers";
|
||||
import type { BackgroundRemovalProgress } from "./editor/propertyPanelTypes";
|
||||
import { timelineKeysForSelections } from "../utils/studioHelpers";
|
||||
import { useInspectorSplitResize } from "../hooks/useInspectorSplitResize";
|
||||
|
||||
export interface StudioRightPanelProps extends StudioEditPersistenceProps {
|
||||
designPanelActive: boolean;
|
||||
activeBlockParams?: {
|
||||
blockName: string;
|
||||
blockTitle: string;
|
||||
params: BlockParam[];
|
||||
compositionPath: string;
|
||||
} | null;
|
||||
onCloseBlockParams?: () => void;
|
||||
recordingState?: "idle" | "recording" | "preview";
|
||||
recordingDuration?: number;
|
||||
onToggleRecording?: () => void;
|
||||
/** Dependencies for the Slideshow persist callback, threaded from App.tsx. */
|
||||
sdkSession: Composition | null;
|
||||
publishSdkSession: NonNullable<UseSlideshowPersistParams["publishSdkSession"]>;
|
||||
/**
|
||||
* Forces THIS `sdkSession` to re-open from disk. DesignPanelPromoteProvider
|
||||
* opens its own separate SDK session scoped to the selected element's own
|
||||
* file (needed so promoting inside a sub-composition binds a variable there,
|
||||
* not on the host) — for a top-level selection that's the SAME file this
|
||||
* session already has open, so a write through that other session leaves
|
||||
* this one holding stale in-memory content. The self-write-echo registry
|
||||
* that normally suppresses redundant reloads is keyed by file path only, not
|
||||
* by session instance, so it wrongly treats the sibling session's write as
|
||||
* "our own echo" and never reloads on its own — this must be called
|
||||
* explicitly after such a write.
|
||||
*/
|
||||
forceReloadSdkSession?: () => void;
|
||||
reloadPreview: () => void;
|
||||
domEditSaveTimestampRef: MutableRefObject<number>;
|
||||
recordEdit: (entry: {
|
||||
label: string;
|
||||
kind: EditHistoryKind;
|
||||
files: Record<string, { before: string; after: string }>;
|
||||
}) => Promise<void>;
|
||||
onToggleElementHidden?: ToggleHiddenHandler;
|
||||
onAddMediaOverlay?: AddMediaOverlayHandler;
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
export function StudioRightPanel({
|
||||
designPanelActive,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Props for StudioRightPanel.
|
||||
*
|
||||
* Kept beside the component rather than inside it: the panel is at the file-size
|
||||
* cap, and this block is the part that changes least, so moving it keeps the
|
||||
* component's own diffs small and readable.
|
||||
*/
|
||||
|
||||
import type { MutableRefObject } from "react";
|
||||
import type { StudioEditPersistenceProps } from "./panels/VariablesPanel";
|
||||
import type { BlockParam } from "@hyperframes/core/registry";
|
||||
import type { Composition } from "@hyperframes/sdk";
|
||||
import type { EditHistoryKind } from "../utils/editHistory";
|
||||
import type { UseSlideshowPersistParams } from "../hooks/useSlideshowPersist";
|
||||
import type { AddMediaOverlayHandler } from "./editor/propertyPanelTypes";
|
||||
import type { ToggleHiddenHandler } from "../utils/studioHelpers";
|
||||
|
||||
export interface StudioRightPanelProps extends StudioEditPersistenceProps {
|
||||
designPanelActive: boolean;
|
||||
activeBlockParams?: {
|
||||
blockName: string;
|
||||
blockTitle: string;
|
||||
params: BlockParam[];
|
||||
compositionPath: string;
|
||||
} | null;
|
||||
onCloseBlockParams?: () => void;
|
||||
recordingState?: "idle" | "recording" | "preview";
|
||||
recordingDuration?: number;
|
||||
onToggleRecording?: () => void;
|
||||
/** Dependencies for the Slideshow persist callback, threaded from App.tsx. */
|
||||
sdkSession: Composition | null;
|
||||
publishSdkSession: NonNullable<UseSlideshowPersistParams["publishSdkSession"]>;
|
||||
/**
|
||||
* Forces THIS `sdkSession` to re-open from disk. DesignPanelPromoteProvider
|
||||
* opens its own separate SDK session scoped to the selected element's own
|
||||
* file (needed so promoting inside a sub-composition binds a variable there,
|
||||
* not on the host) — for a top-level selection that's the SAME file this
|
||||
* session already has open, so a write through that other session leaves
|
||||
* this one holding stale in-memory content. The self-write-echo registry
|
||||
* that normally suppresses redundant reloads is keyed by file path only, not
|
||||
* by session instance, so it wrongly treats the sibling session's write as
|
||||
* "our own echo" and never reloads on its own — this must be called
|
||||
* explicitly after such a write.
|
||||
*/
|
||||
forceReloadSdkSession?: () => void;
|
||||
reloadPreview: () => void;
|
||||
domEditSaveTimestampRef: MutableRefObject<number>;
|
||||
recordEdit: (entry: {
|
||||
label: string;
|
||||
kind: EditHistoryKind;
|
||||
files: Record<string, { before: string; after: string }>;
|
||||
}) => Promise<void>;
|
||||
onToggleElementHidden?: ToggleHiddenHandler;
|
||||
onAddMediaOverlay?: AddMediaOverlayHandler;
|
||||
}
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
type MouseEvent as ReactMouseEvent,
|
||||
} from "react";
|
||||
import {
|
||||
resolveAutomationRange,
|
||||
sampleAutomationLane,
|
||||
type AutomationRange,
|
||||
type HfAutomation,
|
||||
@@ -42,13 +41,7 @@ import { AUTOMATION_LANE_H } from "./automationLaneHeight";
|
||||
import { generateShape, type AutomationShapeId } from "./automationShapes";
|
||||
import { simplifyPoints } from "./automationSimplify";
|
||||
import { pointInSelection, pointsIn, replaceRange } from "./automationLaneSelection";
|
||||
import { getTimelineLaneTop } from "./timelineLayout";
|
||||
import { defaultTimelineTheme } from "./timelineTheme";
|
||||
import { groupAutomationLanes } from "./automationLaneData";
|
||||
import { isAudioTimelineElement } from "../../utils/timelineInspector";
|
||||
import { getTimelineElementIdentity } from "../lib/timelineElementHelpers";
|
||||
import type { TimelineElement } from "../store/playerStore";
|
||||
import type { UseAutomationLanesResult } from "./useAutomationLanes";
|
||||
|
||||
/**
|
||||
* Drawn radius of a breakpoint.
|
||||
@@ -504,171 +497,3 @@ export function TimelineAutomationLane({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Which shared rows one clip draws into, and with which of its lanes. */
|
||||
interface ClipLaneRow {
|
||||
lane: HfAutomationLane;
|
||||
rowIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* One clip's envelopes, each in the shared row its property owns.
|
||||
*
|
||||
* Its own component because every clip on the row needs its own binding, its own
|
||||
* gestures and its own selection box — a shared row is a shared lane track, not a
|
||||
* shared envelope, and two clips' curves must never drag as one thing. Hooks
|
||||
* cannot run in a loop, so the loop is over components.
|
||||
*/
|
||||
function ClipAutomationLanes({
|
||||
element,
|
||||
rows,
|
||||
isSelected,
|
||||
lanes,
|
||||
pps,
|
||||
top,
|
||||
accentColor,
|
||||
currentTime,
|
||||
beatTimes,
|
||||
}: {
|
||||
element: TimelineElement;
|
||||
rows: readonly ClipLaneRow[];
|
||||
isSelected: boolean;
|
||||
lanes: UseAutomationLanesResult;
|
||||
pps: number;
|
||||
/** y of the first automation row on this track. */
|
||||
top: number;
|
||||
accentColor: string;
|
||||
currentTime: number;
|
||||
beatTimes?: readonly number[];
|
||||
}) {
|
||||
// Beats inside this clip, in the clip's own frame — the lane's times are
|
||||
// clip-local, and a beat outside the clip can never be snapped to anyway.
|
||||
const snapTimes = useMemo(
|
||||
() =>
|
||||
(beatTimes ?? [])
|
||||
.filter((t) => t >= element.start && t <= element.start + element.duration)
|
||||
.map((t) => t - element.start),
|
||||
[beatTimes, element.start, element.duration],
|
||||
);
|
||||
const bound = lanes.bind(element, isSelected);
|
||||
// Stale-selection guard: the selected lane's target can vanish out from under
|
||||
// it (e.g. its effect got deleted from the chain, dropping the lane), leaving
|
||||
// a rectangle selecting nothing. Clear it rather than let it point at a
|
||||
// target that no longer draws. Above the empty-rows return, because a clip
|
||||
// that draws nothing is exactly when a selection goes stale.
|
||||
useEffect(() => {
|
||||
const target = bound.selection?.target;
|
||||
if (target !== undefined && !bound.lanes.some((lane) => lane.target === target)) {
|
||||
bound.onRangeClear();
|
||||
}
|
||||
}, [bound]);
|
||||
if (rows.length === 0) return null;
|
||||
const inClip = currentTime >= element.start && currentTime <= element.start + element.duration;
|
||||
return (
|
||||
<>
|
||||
{rows.map(({ lane, rowIndex }) => {
|
||||
const range = resolveAutomationRange(lane.target, bound.chain ?? undefined);
|
||||
// A lane whose target no longer resolves was already dropped upstream;
|
||||
// this is belt and braces so a row can never draw on the wrong axis.
|
||||
if (!range) return null;
|
||||
return (
|
||||
<TimelineAutomationLane
|
||||
key={lane.target}
|
||||
duration={element.duration}
|
||||
widthPx={Math.max(element.duration * pps, 4)}
|
||||
leftPx={element.start * pps}
|
||||
topPx={top + rowIndex * AUTOMATION_LANE_H}
|
||||
automation={bound.automation}
|
||||
target={lane.target}
|
||||
range={range}
|
||||
accentColor={accentColor}
|
||||
playheadSec={inClip ? currentTime - element.start : null}
|
||||
onPreview={bound.onPreview}
|
||||
onCommit={bound.onCommit}
|
||||
onSelect={bound.onSelect}
|
||||
snapTimes={snapTimes}
|
||||
readOnly={bound.readOnly}
|
||||
rangeSelection={
|
||||
bound.selection?.target === lane.target
|
||||
? {
|
||||
t0: bound.selection.t0,
|
||||
t1: bound.selection.t1,
|
||||
v0: bound.selection.v0,
|
||||
v1: bound.selection.v1,
|
||||
}
|
||||
: null
|
||||
}
|
||||
onRangeSelect={(t0, t1, v0, v1) => bound.onRangeSelect(lane.target, t0, t1, v0, v1)}
|
||||
onRangeClear={bound.onRangeClear}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export interface TimelineAutomationLaneSlotProps {
|
||||
/** Every clip on the track, in row order — not just the selected one. */
|
||||
elements: readonly TimelineElement[];
|
||||
isSelected: (element: TimelineElement) => boolean;
|
||||
lanes: UseAutomationLanesResult;
|
||||
pps: number;
|
||||
/** Keyframe lanes already stacked above, which automation sits under. */
|
||||
laneCount: number;
|
||||
accentColor: string;
|
||||
/** Composition-time playhead; the slot converts it to clip-local. */
|
||||
currentTime: number;
|
||||
/** Composition-time beat grid; the slot converts it to clip-local too. */
|
||||
beatTimes?: readonly number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Every automated parameter on this TRACK, one lane per row — the way a DAW
|
||||
* stacks them, so two envelopes can be read and edited without swapping a
|
||||
* control to see either.
|
||||
*
|
||||
* Rows belong to the track, not to a clip: clips sharing a row share a row per
|
||||
* property (see `groupAutomationLanes`), each drawing over its own span, and a
|
||||
* clip that does not automate that property leaves its stretch empty. Binding one
|
||||
* clip at a time is what made the visible envelopes change with the selection.
|
||||
*/
|
||||
export function TimelineAutomationLaneSlot({
|
||||
elements,
|
||||
isSelected,
|
||||
lanes,
|
||||
pps,
|
||||
laneCount,
|
||||
accentColor,
|
||||
currentTime,
|
||||
beatTimes,
|
||||
}: TimelineAutomationLaneSlotProps) {
|
||||
const clips = elements.filter(isAudioTimelineElement);
|
||||
const rowsByClip = new Map<string, ClipLaneRow[]>();
|
||||
groupAutomationLanes(clips).forEach((group, rowIndex) => {
|
||||
for (const entry of group.entries) {
|
||||
const key = getTimelineElementIdentity(entry.element);
|
||||
const rows = rowsByClip.get(key);
|
||||
if (rows) rows.push({ lane: entry.lane, rowIndex });
|
||||
else rowsByClip.set(key, [{ lane: entry.lane, rowIndex }]);
|
||||
}
|
||||
});
|
||||
const top = getTimelineLaneTop(laneCount);
|
||||
return (
|
||||
<>
|
||||
{clips.map((element) => (
|
||||
<ClipAutomationLanes
|
||||
key={getTimelineElementIdentity(element)}
|
||||
element={element}
|
||||
rows={rowsByClip.get(getTimelineElementIdentity(element)) ?? []}
|
||||
isSelected={isSelected(element)}
|
||||
lanes={lanes}
|
||||
pps={pps}
|
||||
top={top}
|
||||
accentColor={accentColor}
|
||||
currentTime={currentTime}
|
||||
beatTimes={beatTimes}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { act } from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { TimelineAutomationLaneSlot } from "./TimelineAutomationLane";
|
||||
import { TimelineAutomationLaneSlot } from "./TimelineAutomationLaneSlot";
|
||||
import { AUTOMATION_LANE_H } from "./automationLaneHeight";
|
||||
import { PAD_X } from "./automationLaneGeometry";
|
||||
import { getTimelineLaneTop } from "./timelineLayout";
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Track-level automation: the stack of shared rows an audio track draws, and
|
||||
* one clip's envelopes within them.
|
||||
*
|
||||
* Split from TimelineAutomationLane.tsx, which owns the single-lane editor this
|
||||
* renders many of. The dependency runs one way, slot -> lane, so the editor
|
||||
* stays readable on its own and this file keeps the row-layout concern.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { resolveAutomationRange, type HfAutomationLane } from "@hyperframes/core/audio-automation";
|
||||
import { TimelineAutomationLane } from "./TimelineAutomationLane";
|
||||
import { AUTOMATION_LANE_H } from "./automationLaneHeight";
|
||||
import { getTimelineLaneTop } from "./timelineLayout";
|
||||
import { groupAutomationLanes } from "./automationLaneData";
|
||||
import { isAudioTimelineElement } from "../../utils/timelineInspector";
|
||||
import { getTimelineElementIdentity } from "../lib/timelineElementHelpers";
|
||||
import type { TimelineElement } from "../store/playerStore";
|
||||
import type { UseAutomationLanesResult } from "./useAutomationLanes";
|
||||
|
||||
/** Which shared rows one clip draws into, and with which of its lanes. */
|
||||
interface ClipLaneRow {
|
||||
lane: HfAutomationLane;
|
||||
rowIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* One clip's envelopes, each in the shared row its property owns.
|
||||
*
|
||||
* Its own component because every clip on the row needs its own binding, its own
|
||||
* gestures and its own selection box — a shared row is a shared lane track, not a
|
||||
* shared envelope, and two clips' curves must never drag as one thing. Hooks
|
||||
* cannot run in a loop, so the loop is over components.
|
||||
*/
|
||||
function ClipAutomationLanes({
|
||||
element,
|
||||
rows,
|
||||
isSelected,
|
||||
lanes,
|
||||
pps,
|
||||
top,
|
||||
accentColor,
|
||||
currentTime,
|
||||
beatTimes,
|
||||
}: {
|
||||
element: TimelineElement;
|
||||
rows: readonly ClipLaneRow[];
|
||||
isSelected: boolean;
|
||||
lanes: UseAutomationLanesResult;
|
||||
pps: number;
|
||||
/** y of the first automation row on this track. */
|
||||
top: number;
|
||||
accentColor: string;
|
||||
currentTime: number;
|
||||
beatTimes?: readonly number[];
|
||||
}) {
|
||||
// Beats inside this clip, in the clip's own frame — the lane's times are
|
||||
// clip-local, and a beat outside the clip can never be snapped to anyway.
|
||||
const snapTimes = useMemo(
|
||||
() =>
|
||||
(beatTimes ?? [])
|
||||
.filter((t) => t >= element.start && t <= element.start + element.duration)
|
||||
.map((t) => t - element.start),
|
||||
[beatTimes, element.start, element.duration],
|
||||
);
|
||||
const bound = lanes.bind(element, isSelected);
|
||||
// Stale-selection guard: the selected lane's target can vanish out from under
|
||||
// it (e.g. its effect got deleted from the chain, dropping the lane), leaving
|
||||
// a rectangle selecting nothing. Clear it rather than let it point at a
|
||||
// target that no longer draws. Above the empty-rows return, because a clip
|
||||
// that draws nothing is exactly when a selection goes stale.
|
||||
useEffect(() => {
|
||||
const target = bound.selection?.target;
|
||||
if (target !== undefined && !bound.lanes.some((lane) => lane.target === target)) {
|
||||
bound.onRangeClear();
|
||||
}
|
||||
}, [bound]);
|
||||
if (rows.length === 0) return null;
|
||||
const inClip = currentTime >= element.start && currentTime <= element.start + element.duration;
|
||||
return (
|
||||
<>
|
||||
{rows.map(({ lane, rowIndex }) => {
|
||||
const range = resolveAutomationRange(lane.target, bound.chain ?? undefined);
|
||||
// A lane whose target no longer resolves was already dropped upstream;
|
||||
// this is belt and braces so a row can never draw on the wrong axis.
|
||||
if (!range) return null;
|
||||
return (
|
||||
<TimelineAutomationLane
|
||||
key={lane.target}
|
||||
duration={element.duration}
|
||||
widthPx={Math.max(element.duration * pps, 4)}
|
||||
leftPx={element.start * pps}
|
||||
topPx={top + rowIndex * AUTOMATION_LANE_H}
|
||||
automation={bound.automation}
|
||||
target={lane.target}
|
||||
range={range}
|
||||
accentColor={accentColor}
|
||||
playheadSec={inClip ? currentTime - element.start : null}
|
||||
onPreview={bound.onPreview}
|
||||
onCommit={bound.onCommit}
|
||||
onSelect={bound.onSelect}
|
||||
snapTimes={snapTimes}
|
||||
readOnly={bound.readOnly}
|
||||
rangeSelection={
|
||||
bound.selection?.target === lane.target
|
||||
? {
|
||||
t0: bound.selection.t0,
|
||||
t1: bound.selection.t1,
|
||||
v0: bound.selection.v0,
|
||||
v1: bound.selection.v1,
|
||||
}
|
||||
: null
|
||||
}
|
||||
onRangeSelect={(t0, t1, v0, v1) => bound.onRangeSelect(lane.target, t0, t1, v0, v1)}
|
||||
onRangeClear={bound.onRangeClear}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export interface TimelineAutomationLaneSlotProps {
|
||||
/** Every clip on the track, in row order — not just the selected one. */
|
||||
elements: readonly TimelineElement[];
|
||||
isSelected: (element: TimelineElement) => boolean;
|
||||
lanes: UseAutomationLanesResult;
|
||||
pps: number;
|
||||
/** Keyframe lanes already stacked above, which automation sits under. */
|
||||
laneCount: number;
|
||||
accentColor: string;
|
||||
/** Composition-time playhead; the slot converts it to clip-local. */
|
||||
currentTime: number;
|
||||
/** Composition-time beat grid; the slot converts it to clip-local too. */
|
||||
beatTimes?: readonly number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Every automated parameter on this TRACK, one lane per row — the way a DAW
|
||||
* stacks them, so two envelopes can be read and edited without swapping a
|
||||
* control to see either.
|
||||
*
|
||||
* Rows belong to the track, not to a clip: clips sharing a row share a row per
|
||||
* property (see `groupAutomationLanes`), each drawing over its own span, and a
|
||||
* clip that does not automate that property leaves its stretch empty. Binding one
|
||||
* clip at a time is what made the visible envelopes change with the selection.
|
||||
*/
|
||||
export function TimelineAutomationLaneSlot({
|
||||
elements,
|
||||
isSelected,
|
||||
lanes,
|
||||
pps,
|
||||
laneCount,
|
||||
accentColor,
|
||||
currentTime,
|
||||
beatTimes,
|
||||
}: TimelineAutomationLaneSlotProps) {
|
||||
const clips = elements.filter(isAudioTimelineElement);
|
||||
const rowsByClip = new Map<string, ClipLaneRow[]>();
|
||||
groupAutomationLanes(clips).forEach((group, rowIndex) => {
|
||||
for (const entry of group.entries) {
|
||||
const key = getTimelineElementIdentity(entry.element);
|
||||
const rows = rowsByClip.get(key);
|
||||
if (rows) rows.push({ lane: entry.lane, rowIndex });
|
||||
else rowsByClip.set(key, [{ lane: entry.lane, rowIndex }]);
|
||||
}
|
||||
});
|
||||
const top = getTimelineLaneTop(laneCount);
|
||||
return (
|
||||
<>
|
||||
{clips.map((element) => (
|
||||
<ClipAutomationLanes
|
||||
key={getTimelineElementIdentity(element)}
|
||||
element={element}
|
||||
rows={rowsByClip.get(getTimelineElementIdentity(element)) ?? []}
|
||||
isSelected={isSelected(element)}
|
||||
lanes={lanes}
|
||||
pps={pps}
|
||||
top={top}
|
||||
accentColor={accentColor}
|
||||
currentTime={currentTime}
|
||||
beatTimes={beatTimes}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { BeatStrip, BeatBackgroundLines } from "./BeatStrip";
|
||||
import { TimelineClip } from "./TimelineClip";
|
||||
import { TimelineCompactDiamonds } from "./TimelineCompactDiamonds";
|
||||
import { TimelinePropertyLanes } from "./TimelinePropertyLanes";
|
||||
import { TimelineAutomationLaneSlot } from "./TimelineAutomationLane";
|
||||
import { TimelineAutomationLaneSlot } from "./TimelineAutomationLaneSlot";
|
||||
import { useAutomationLanes } from "./useAutomationLanes";
|
||||
import { useAutomationSelectionKeyboard } from "../../hooks/useAutomationSelectionKeyboard";
|
||||
import { TimelineTrackHeader } from "./TimelineTrackHeader";
|
||||
|
||||
Reference in New Issue
Block a user