diff --git a/packages/studio/src/player/components/Timeline.test.ts b/packages/studio/src/player/components/Timeline.test.ts index 550a9b1ed..55a1df906 100644 --- a/packages/studio/src/player/components/Timeline.test.ts +++ b/packages/studio/src/player/components/Timeline.test.ts @@ -653,8 +653,14 @@ describe("Timeline provider boundary", () => { const caret = () => host.querySelector('button[aria-label$=" lanes"]'); expect(caret()?.getAttribute("aria-label")).toBe("Show Track 1 lanes"); + // The shared volume row is drawn whether or not the caret is open — + // automation is the track's own content, not something the caret discloses — + // so the row already reserves its height here. + expect(row?.style.height).toBe(`${TRACK_H + AUTOMATION_LANE_H}px`); + act(() => caret()?.click()); - // One shared volume row, and BOTH clips hold it open. + // BOTH clips hold the caret open; height is unchanged, since the clips carry + // automation and no keyframe lanes. expectTrackExpansion(row, ["narration-1", "narration-2"], TRACK_H + AUTOMATION_LANE_H); // Every clip bar on the row is capped to one track height. Only the clip @@ -667,7 +673,9 @@ describe("Timeline provider boundary", () => { ).toEqual([`${TRACK_H - 2 * CLIP_Y}px`, `${TRACK_H - 2 * CLIP_Y}px`]); act(() => caret()?.click()); - expectTrackExpansion(row, [], TRACK_H); + // Collapsed again — and the automation row stays, with its height still + // reserved. Only keyframe lanes come and go with the caret. + expectTrackExpansion(row, [], TRACK_H + AUTOMATION_LANE_H); act(() => root.unmount()); }); @@ -677,6 +685,47 @@ describe("Timeline provider boundary", () => { // which threw away each one's hover state and any gesture in flight. Pressing // a lane to select its clip therefore made the handles vanish under the // pointer, which is the one gesture the read-only lane exists to support. + // The rule this replaced: automation lanes only drew while the keyframe caret + // was open, so an audio track's envelopes hid behind a control that is about + // tweens — and a clip with automation but no tweens had lanes reachable only + // by opening a disclosure that showed nothing else. + it("draws automation lanes with the caret closed, and caps the clip bars over them", () => { + const host = createSizedTimelineHost(720); + usePlayerStore.setState({ + duration: 8, + timelineReady: true, + elements: [ + { + id: "narration-1", + tag: "audio", + start: 0, + duration: 4, + track: 0, + automation: JSON.stringify({ + version: 1, + lanes: [{ target: "volume", points: [{ t: 0, v: 1 }] }], + }), + }, + ], + }); + const root = createRoot(host); + act(() => root.render(React.createElement(Timeline))); + + // Nothing expanded — the caret has not been touched. + expect(usePlayerStore.getState().expandedClipIds).toEqual(new Set()); + expect(host.querySelectorAll(".hf-automation-lane")).toHaveLength(1); + + // The row reserves the lane's height, and the clip bar is capped to one + // track height so its waveform cannot paint over the envelope below. + const row = host.querySelector('[data-el-id="narration-1"]')?.parentElement + ?.parentElement; + expect(row?.style.height).toBe(`${TRACK_H + AUTOMATION_LANE_H}px`); + expect(host.querySelector('[data-el-id="narration-1"]')?.style.height).toBe( + `${TRACK_H - 2 * CLIP_Y}px`, + ); + act(() => root.unmount()); + }); + it("keeps the automation lanes mounted when the selection moves along the row", () => { const host = createSizedTimelineHost(720); const automation = JSON.stringify({ diff --git a/packages/studio/src/player/components/TimelineGroupHeader.tsx b/packages/studio/src/player/components/TimelineGroupHeader.tsx index d8890ce09..f2376ae33 100644 --- a/packages/studio/src/player/components/TimelineGroupHeader.tsx +++ b/packages/studio/src/player/components/TimelineGroupHeader.tsx @@ -10,10 +10,6 @@ interface TimelineGroupHeaderProps { /** Caret: shows/hides the member rows beneath this group (structural). */ isExpanded: boolean; onToggleExpanded: () => void; - /** `∿`: shows/hides the group's own automation-lane rows. */ - laneCount: number; - isLaneOpen: boolean; - onToggleLanes: () => void; /** `add: true` (⌘/Ctrl-click) toggles membership; a plain click is exclusive. */ /** C1: the group's serialized `data-fx-chain`, when set. */ fxChain?: string; @@ -28,7 +24,8 @@ interface TimelineGroupHeaderProps { /** * A group's own row header: caret (member disclosure) + `▤` + label + count + - * FX + `∿ n` (lane disclosure). + * FX. Its automation lanes are always drawn, so there is no disclosure for + * them. */ /** @@ -83,9 +80,6 @@ export function TimelineGroupHeader({ memberCount, isExpanded, onToggleExpanded, - laneCount, - isLaneOpen, - onToggleLanes, fxChain, onFxChainChange, onFxChainPreview, @@ -141,33 +135,6 @@ export function TimelineGroupHeader({ auditionSpans={auditionSpans} onOpenRack={onOpenFxRack} /> - {/* No lanes, no control: an author who opens it meets an empty row and - learns nothing. A track header already gates its own `∿` this way - (`disclosable`); the group's was the one that still offered a - disclosure over nothing. Automation appears by being written — from - the rack or a keyframe — not by opening this, so nothing is - unreachable while it is hidden. */} - {laneCount > 0 && ( - - )} ); diff --git a/packages/studio/src/player/components/TimelineGroupRow.test.tsx b/packages/studio/src/player/components/TimelineGroupRow.test.tsx index 0b89493a7..cea7b2080 100644 --- a/packages/studio/src/player/components/TimelineGroupRow.test.tsx +++ b/packages/studio/src/player/components/TimelineGroupRow.test.tsx @@ -55,9 +55,7 @@ function renderRow(overrides: Partial = {}) { contentOrigin={232} theme={defaultTimelineTheme} collapsedGroupIds={new Set()} - expandedLaneOwnerIds={new Set()} toggleGroupExpanded={vi.fn()} - toggleLaneOwnerExpanded={vi.fn()} lanes={{ bind: () => ({ lanes: [] }) } as never} pps={10} currentTime={0} @@ -94,18 +92,12 @@ describe("TimelineGroupRow", () => { expect(onSetElementAttributeQuiet).not.toHaveBeenCalled(); }); - // A disclosure over nothing tells the author their group has no automation - // only AFTER they open an empty row. Track headers already gate their own - // toggle on having something to disclose; the group's did not. - it("hides the lane toggle until the group actually automates something", () => { - const laneToggle = (host: HTMLElement) => - Array.from(host.querySelectorAll("button")).find((b) => - /lanes$/.test(b.getAttribute("aria-label") ?? ""), - ); - - expect(laneToggle(renderRow().host)).toBeUndefined(); - - const automated = renderRow({ + // Automation lanes are always drawn, so there is no toggle to offer: the + // group's row shows its envelopes the way it shows its name. (This replaced a + // rule that hid the toggle when the count was zero — the toggle itself is + // gone now.) + it("renders no lane disclosure on the group header", () => { + const { host } = renderRow({ fxChain: JSON.stringify({ version: 1, nodes: [{ type: "peaking", id: "p1", params: { frequency: 1000, gain: -3, q: 1 } }], @@ -115,6 +107,9 @@ describe("TimelineGroupRow", () => { lanes: [{ target: "fx.p1.gain", points: [{ t: 0, v: 0 }] }], }), }); - expect(laneToggle(automated.host)).toBeDefined(); + const laneToggle = Array.from(host.querySelectorAll("button")).find((b) => + /lanes$/.test(b.getAttribute("aria-label") ?? ""), + ); + expect(laneToggle).toBeUndefined(); }); }); diff --git a/packages/studio/src/player/components/TimelineGroupRow.tsx b/packages/studio/src/player/components/TimelineGroupRow.tsx index 1d27189da..586a9db33 100644 --- a/packages/studio/src/player/components/TimelineGroupRow.tsx +++ b/packages/studio/src/player/components/TimelineGroupRow.tsx @@ -8,7 +8,6 @@ import type { TimelineTrackGroupInfo } from "./useTimelineTrackDerivations"; import type { TimelineLogicalRow } from "./timelineKeyboardNavigation"; import { TimelineTrackRow } from "./TimelineTrackRow"; import { TimelineGroupHeader } from "./TimelineGroupHeader"; -import { groupAutomationLanes } from "./automationLaneData"; import { groupAutomationElement } from "./groupAutomationElement"; import { TimelineAutomationLaneSlot } from "./TimelineAutomationLane"; import { TimelineGroupLaneLabels } from "./TimelineGroupLaneLabels"; @@ -35,9 +34,7 @@ interface TimelineGroupRowProps { theme: TimelineTheme; rovingTargetId?: string | null; collapsedGroupIds: ReadonlySet; - expandedLaneOwnerIds: ReadonlySet; toggleGroupExpanded: (id: string) => void; - toggleLaneOwnerExpanded: (id: string) => void; lanes: UseAutomationLanesResult; pps: number; currentTime: number; @@ -61,9 +58,7 @@ export function TimelineGroupRow({ theme, rovingTargetId = null, collapsedGroupIds, - expandedLaneOwnerIds, toggleGroupExpanded, - toggleLaneOwnerExpanded, lanes, pps, currentTime, @@ -85,7 +80,6 @@ export function TimelineGroupRow({ // its name in the header does. const domSelection = useDomEditSelectionContextOptional()?.domEditSelection ?? null; const isGroupSelected = domSelection?.id === group.id; - const isLaneOpen = expandedLaneOwnerIds.has(group.id); // Optional, like every sibling row: Timeline renders outside the edit // provider in read-only hosts (Timeline.test.ts asserts it), and the throwing // hook took the whole timeline down with it the moment a group existed — @@ -127,13 +121,6 @@ export function TimelineGroupRow({ memberCount={group.memberTracks.length} isExpanded={!collapsedGroupIds.has(group.id)} onToggleExpanded={() => toggleGroupExpanded(group.id)} - // The GROUP's own lanes, not its members'. `∿` is per-row (groups doc - // §5: "∿ is lit on vo-1 but not vo-2, the same control per row"), and - // counting the members' here made the group advertise curves it does - // not own and cannot show. - laneCount={groupAutomationLanes([groupElement]).length} - isLaneOpen={isLaneOpen} - onToggleLanes={() => toggleLaneOwnerExpanded(group.id)} fxChain={group.fxChain} onFxChainChange={(next) => writeGroupFxChain(next, false)} onFxChainPreview={(next) => writeGroupFxChain(next, true)} @@ -147,47 +134,44 @@ export function TimelineGroupRow({ columnWidth={contentOrigin >= LABEL_COL_W ? LABEL_COL_W : contentOrigin} theme={theme} /> - {/* The group's OWN curves, under the strip. Selected-gated exactly like a - clip's: the binder writes through the dom-edit selection, so a lane is - editable once the group is selected — which clicking its name does. */} + {/* The group's OWN curves, always drawn — there is no disclosure for + them. Selected-gated exactly like a clip's: the binder writes through + the dom-edit selection, so a lane is editable once the group is + selected, which clicking its name does. */} {/* The label column for those lanes, on the accent rail. Outside the offset content cell below, because the labels belong to the sticky gutter the row header occupies, not to the scrolling canvas. */} - {isLaneOpen && ( - = LABEL_COL_W ? LABEL_COL_W : contentOrigin} - gutterBackground={theme.gutterBackground} + = LABEL_COL_W ? LABEL_COL_W : contentOrigin} + gutterBackground={theme.gutterBackground} + accentColor={GROUP_LANE_ACCENT} + /> + {/* The same offset content cell a track row wraps its lanes in — the slot + positions absolutely, so mounted straight on the row it resolved + against the row instead and drew the envelope across the label gutter + from x=0. */} +
+ isGroupSelected} + lanes={lanes} + pps={pps} + // Directly under the header row. + laneCount={0} + topOffset={TRACK_H} accentColor={GROUP_LANE_ACCENT} + currentTime={currentTime} + beatTimes={beatTimes} /> - )} - {isLaneOpen && ( - // The same offset content cell a track row wraps its lanes in — the - // slot positions absolutely, so mounted straight on the row it resolved - // against the row instead and drew the envelope across the label gutter - // from x=0. -
- isGroupSelected} - lanes={lanes} - pps={pps} - // Below the strip, which sits directly under the header row. - laneCount={0} - topOffset={TRACK_H} - accentColor={GROUP_LANE_ACCENT} - currentTime={currentTime} - beatTimes={beatTimes} - /> -
- )} +
); } diff --git a/packages/studio/src/player/components/TimelineLanes.test.tsx b/packages/studio/src/player/components/TimelineLanes.test.tsx index 70a07b531..3e113bc92 100644 --- a/packages/studio/src/player/components/TimelineLanes.test.tsx +++ b/packages/studio/src/player/components/TimelineLanes.test.tsx @@ -119,7 +119,6 @@ function renderLanes(options: RenderLanesOptions = {}): { selectedElementIds: next.selectedElementIds ?? new Set(), expandedClipIds: new Set(next.expandedClipIds ?? []), collapsedGroupIds: new Set(), - expandedLaneOwnerIds: new Set(), groups: [], trackGroupOf: new Map(), gsapAnimations, diff --git a/packages/studio/src/player/components/TimelineLanes.tsx b/packages/studio/src/player/components/TimelineLanes.tsx index 6e1cc2895..c10395c8e 100644 --- a/packages/studio/src/player/components/TimelineLanes.tsx +++ b/packages/studio/src/player/components/TimelineLanes.tsx @@ -11,6 +11,7 @@ import { TimelineGroupRow } from "./TimelineGroupRow"; import { useTimelineLaneRowIndexes, useTimelineGroupDisclosure } from "./useTimelineLaneRowIndexes"; import { isTrackRowExpanded, + trackAutomationLaneCount, resolveTrackKeyframeClip, trackShowsBeatStrip, } from "./useTimelineTrackLayout"; @@ -100,8 +101,7 @@ export function TimelineLanes({ // from resolving into a second timeline that renders the same logical rows. const lanesIdPrefix = `timeline-lanes${useId().replaceAll(":", "")}`; const expandedClipIds = usePlayerStore((s) => s.expandedClipIds); - const { collapsedGroupIds, expandedLaneOwnerIds, toggleGroupExpanded, toggleLaneOwnerExpanded } = - useTimelineGroupDisclosure(); + const { collapsedGroupIds, toggleGroupExpanded } = useTimelineGroupDisclosure(); const automationLanes = useAutomationLanes(); // A group's automation clock is COMPOSITION time (groups doc §1.3), so its // synthetic lane element spans the whole composition rather than a clip. @@ -179,9 +179,7 @@ export function TimelineLanes({ theme={theme} rovingTargetId={keyboard.rovingTargetId} collapsedGroupIds={collapsedGroupIds} - expandedLaneOwnerIds={expandedLaneOwnerIds} toggleGroupExpanded={toggleGroupExpanded} - toggleLaneOwnerExpanded={toggleLaneOwnerExpanded} lanes={automationLanes} pps={pps} currentTime={currentTime} @@ -230,12 +228,13 @@ export function TimelineLanes({ ); const keyframeClipKey = keyframeClip?.key ?? keyframeClip?.id; const rowExpanded = isTrackRowExpanded(els, expandedClipIds); - // How tall a clip BAR is drawn. An expanded row is mostly lanes, and a - // clip left to fill it painted its waveform straight over them — so the - // bar is capped for every clip on the row, not just the one whose - // property lanes are showing. Undefined means "fill the row", which is - // right only while it is collapsed and the row is nothing but bar. - const clipBarHeight = rowExpanded ? TRACK_H - 2 * CLIP_Y : undefined; + // How tall a clip BAR is drawn. A row with lanes under it is mostly + // lanes, and a clip left to fill it painted its waveform over them — + // so the bar is capped for every clip on the row. Undefined means + // "fill the row", right only when the row is nothing BUT bar, which a + // collapsed row no longer is: automation lanes are always drawn. + const hasAutomationRows = trackAutomationLaneCount(els) > 0; + const clipBarHeight = rowExpanded || hasAutomationRows ? TRACK_H - 2 * CLIP_Y : undefined; // The clips whose envelopes this row draws, at their dragged positions. // Once per row, not once per clip in the map below. const automationElements = els.map(getPreviewElement); @@ -578,27 +577,32 @@ export function TimelineLanes({ any gesture mid-flight), so pressing a lane to select its clip made the handles you were reaching for disappear. - Mounted in BOTH disclosure states, empty while collapsed, so - the caret's aria-controls resolves either way — same reason - the keyframe lanes are. Absolute positions inside resolve - against this same relative row, so the geometry is unchanged - by the move. */} + Always drawn, in both caret states: an envelope is the + track's own content, and gating it on the keyframe caret hid + audio automation behind a control about tweens. The row + reserves height to match (see `trackHeights`). Absolute + positions resolve against this same relative row, so the + geometry is unchanged by the move. */}
- {rowExpanded ? ( - { - const key = getTimelineElementIdentity(element); - return selectedElementId === key || selectedElementIds.has(key); - }} - lanes={automationLanes} - pps={pps} - laneCount={keyframeClipKey ? (laneCounts.get(keyframeClipKey) ?? 0) : 0} - accentColor={getTrackStyle(keyframeClip?.tag ?? "").accent} - currentTime={currentTime} - beatTimes={beatAnalysis?.beatTimes} - /> - ) : null} + { + const key = getTimelineElementIdentity(element); + return selectedElementId === key || selectedElementIds.has(key); + }} + lanes={automationLanes} + pps={pps} + // Automation stacks UNDER the keyframe lanes, so the offset + // is how many of those are drawn — none while collapsed. + // Passing the count regardless left the lanes below an + // empty gap, past the row's bottom. + laneCount={ + rowExpanded && keyframeClipKey ? (laneCounts.get(keyframeClipKey) ?? 0) : 0 + } + accentColor={getTrackStyle(keyframeClip?.tag ?? "").accent} + currentTime={currentTime} + beatTimes={beatAnalysis?.beatTimes} + />
diff --git a/packages/studio/src/player/components/timelineKeyboardNavigation.ts b/packages/studio/src/player/components/timelineKeyboardNavigation.ts index 351bfb564..d3306910f 100644 --- a/packages/studio/src/player/components/timelineKeyboardNavigation.ts +++ b/packages/studio/src/player/components/timelineKeyboardNavigation.ts @@ -80,7 +80,6 @@ export interface BuildTimelineLogicalRowsInput { /** Groups the caret has COLLAPSED — absent means expanded, the default. */ collapsedGroupIds: ReadonlySet; /** Rows (clip id or group id) whose automation-lane rows the `∿` button opened. */ - expandedLaneOwnerIds: ReadonlySet; groups: readonly TimelineTrackGroupInfo[]; trackGroupOf: ReadonlyMap; gsapAnimations: ReadonlyMap; @@ -207,16 +206,6 @@ function propertyItems( return items; } -/** A clip's lanes are visible when either the caret or the `∿` button opened it. */ -function isRowOpen( - activeId: string | null, - expandedClipIds: ReadonlySet, - expandedLaneOwnerIds: ReadonlySet, -): boolean { - if (activeId === null) return false; - return expandedClipIds.has(activeId) || expandedLaneOwnerIds.has(activeId); -} - /** A single automation-lane row, one level deeper than the track/group row that owns it. */ function buildLaneRow( track: number, @@ -252,7 +241,6 @@ export function buildTimelineLogicalRows({ selectedElementIds, expandedClipIds, collapsedGroupIds, - expandedLaneOwnerIds, groups, trackGroupOf, gsapAnimations, @@ -273,7 +261,7 @@ export function buildTimelineLogicalRows({ selectedElementIds, gsapAnimations, ); - const expanded = isRowOpen(activeId, expandedClipIds, expandedLaneOwnerIds) && lanes.length > 0; + const expanded = activeId !== null && expandedClipIds.has(activeId) && lanes.length > 0; rows.push({ id: trackId, kind: "row", @@ -314,10 +302,10 @@ export function buildTimelineLogicalRows({ expanded: groupExpanded, items: [], }); - if (expandedLaneOwnerIds.has(group.id)) { - // The group's own member list, not `trackMap`: a COLLAPSED group can have - // its lane shelf open, and its members are absent from the display list — - // so looking them up there emitted zero lane rows for exactly that case. + { + // The group's own member list, not `trackMap`: a COLLAPSED group still + // draws its lanes, and its members are absent from the display list — so + // looking them up there emitted zero lane rows for exactly that case. for (const laneGroup of groupAutomationLanes(group.memberElements)) { rows.push({ id: `${groupRowId}::${laneGroup.key}`, diff --git a/packages/studio/src/player/components/timelineLayout.ts b/packages/studio/src/player/components/timelineLayout.ts index 74d201feb..0fd2b9f10 100644 --- a/packages/studio/src/player/components/timelineLayout.ts +++ b/packages/studio/src/player/components/timelineLayout.ts @@ -105,9 +105,13 @@ export function trackHeights( let laneCount = 0; let automationLanes = 0; for (const clip of clips) { + // Automation rows are ALWAYS drawn — an envelope is the track's own + // content, not a detail the keyframe caret discloses — so their height is + // reserved whether or not the row is expanded. Reserving it only when + // expanded clipped every lane on a collapsed row. + automationLanes = Math.max(automationLanes, clip.automationLaneCount ?? 0); if (!expandedClipIds?.has(clip.clipId)) continue; laneCount = Math.max(laneCount, clip.laneCount); - automationLanes = Math.max(automationLanes, clip.automationLaneCount ?? 0); } return ( TRACK_H + Math.max(0, Math.trunc(laneCount)) * LANE_H + automationLanes * AUTOMATION_LANE_H diff --git a/packages/studio/src/player/components/useTimelineLaneRowIndexes.ts b/packages/studio/src/player/components/useTimelineLaneRowIndexes.ts index 7ef978c5c..793421ff1 100644 --- a/packages/studio/src/player/components/useTimelineLaneRowIndexes.ts +++ b/packages/studio/src/player/components/useTimelineLaneRowIndexes.ts @@ -3,13 +3,12 @@ import { usePlayerStore } from "../store/playerStore"; import type { TimelineLogicalRow } from "./timelineKeyboardNavigation"; import type { TimelineTrackGroupInfo } from "./useTimelineTrackDerivations"; -/** The four pieces of group-disclosure state a group row's header reads and writes. */ +/** The group-disclosure state a group row's header reads and writes. Member + * rows only: automation lanes are always drawn, so they have no disclosure. */ export function useTimelineGroupDisclosure() { return { collapsedGroupIds: usePlayerStore((s) => s.collapsedGroupIds), - expandedLaneOwnerIds: usePlayerStore((s) => s.expandedLaneOwnerIds), toggleGroupExpanded: usePlayerStore((s) => s.toggleGroupExpanded), - toggleLaneOwnerExpanded: usePlayerStore((s) => s.toggleLaneOwnerExpanded), }; } diff --git a/packages/studio/src/player/components/useTimelineLogicalFocus.ts b/packages/studio/src/player/components/useTimelineLogicalFocus.ts index 8f6fe0e09..4f3d7e711 100644 --- a/packages/studio/src/player/components/useTimelineLogicalFocus.ts +++ b/packages/studio/src/player/components/useTimelineLogicalFocus.ts @@ -36,7 +36,6 @@ interface TimelineLogicalFocusInput { export function useTimelineLogicalFocus(input: TimelineLogicalFocusInput) { const expandedClipIds = usePlayerStore((state) => state.expandedClipIds); const collapsedGroupIds = usePlayerStore((state) => state.collapsedGroupIds); - const expandedLaneOwnerIds = usePlayerStore((state) => state.expandedLaneOwnerIds); const projectId = usePlayerStore((state) => state.timelineProjectId); const logicalRows = useTimelineLogicalRows({ tracks: input.tracks, @@ -46,7 +45,6 @@ export function useTimelineLogicalFocus(input: TimelineLogicalFocusInput) { selectedElementIds: input.selectedElementIds, expandedClipIds, collapsedGroupIds, - expandedLaneOwnerIds, groups: input.groups, trackGroupOf: input.trackGroupOf, gsapAnimations: input.gsapAnimations, diff --git a/packages/studio/src/player/components/useTimelineLogicalRows.test.tsx b/packages/studio/src/player/components/useTimelineLogicalRows.test.tsx index 1ba44d32e..a955c1c04 100644 --- a/packages/studio/src/player/components/useTimelineLogicalRows.test.tsx +++ b/packages/studio/src/player/components/useTimelineLogicalRows.test.tsx @@ -23,7 +23,6 @@ const laneCounts = new Map(); const selectedElementIds = new Set(); const expandedClipIds = new Set(); const collapsedGroupIds = new Set(); -const expandedLaneOwnerIds = new Set(); const groups: never[] = []; const trackGroupOf = new Map(); const gsapAnimations = new Map(); @@ -38,7 +37,6 @@ function Harness({ snapshots }: { snapshots: Array, ): number[] { if (groups.length === 0) return rowHeights; const groupByAnchor = new Map(groups.map((group) => [group.anchorKey, group])); return tracks.map(([track], index) => { const group = groupByAnchor.get(track); - if (!group || !expandedLaneOwnerIds.has(group.id)) return rowHeights[index] ?? TRACK_H; - // The group's own automation rows, which its `∿` discloses. A row sized - // without them clipped every lane it had just promised in the count. + if (!group) return rowHeights[index] ?? TRACK_H; return TRACK_H + groupOwnLaneCount(group) * AUTOMATION_LANE_H; }); } @@ -165,7 +162,6 @@ function useTimelineRowHeights( groups: readonly TimelineTrackGroupInfo[], ) { const expandedClipIds = usePlayerStore((s) => s.expandedClipIds); - const expandedLaneOwnerIds = usePlayerStore((s) => s.expandedLaneOwnerIds); const { laneCounts, rowGeometry } = useMemo(() => { const laneCounts = computeLaneCounts(tracks, gsapAnimations); // Keyframe lanes follow only the active clip, so a track with several @@ -199,7 +195,6 @@ function useTimelineRowHeights( tracks, trackHeights(heightTracks, expandedClipIds), groups, - expandedLaneOwnerIds, ); return { laneCounts, @@ -208,15 +203,7 @@ function useTimelineRowHeights( rowHeights, ), }; - }, [ - expandedClipIds, - expandedLaneOwnerIds, - gsapAnimations, - groups, - tracks, - selectedElementId, - selectedElementIds, - ]); + }, [expandedClipIds, gsapAnimations, groups, tracks, selectedElementId, selectedElementIds]); const rowGeometryRef = useRef(rowGeometry); rowGeometryRef.current = rowGeometry; return { diff --git a/packages/studio/src/player/store/keyframeSlice.ts b/packages/studio/src/player/store/keyframeSlice.ts index d18bd31b4..d0417257e 100644 --- a/packages/studio/src/player/store/keyframeSlice.ts +++ b/packages/studio/src/player/store/keyframeSlice.ts @@ -75,10 +75,6 @@ export interface KeyframeSlice { collapsedGroupIds: Set; toggleGroupExpanded: (id: string) => void; - /** Rows (clip id or group id) whose automation-lane rows the `∿` button opened. */ - expandedLaneOwnerIds: Set; - toggleLaneOwnerExpanded: (id: string) => void; - /** * Project/session/element-scoped request. Its nonce is monotonic across store * resets so a stale consumer can never collide with a later request. @@ -144,15 +140,6 @@ export function createKeyframeSlice( return { collapsedGroupIds: next }; }), - expandedLaneOwnerIds: new Set(), - toggleLaneOwnerExpanded: (id) => - set((state) => { - const next = new Set(state.expandedLaneOwnerIds); - if (next.has(id)) next.delete(id); - else next.add(id); - return { expandedLaneOwnerIds: next }; - }), - focusedEaseSegment: null, focusedEaseRequestNonce: 0, setFocusedEaseSegment: (target) => diff --git a/packages/studio/src/player/store/playerStore.ts b/packages/studio/src/player/store/playerStore.ts index 3cae18b6d..c6a06ee05 100644 --- a/packages/studio/src/player/store/playerStore.ts +++ b/packages/studio/src/player/store/playerStore.ts @@ -277,7 +277,6 @@ export function createTimelineResetState() { expandedClipIds: new Set(), // Per-composition: ids from comp A match nothing in B, silencing all of it. collapsedGroupIds: new Set(), - expandedLaneOwnerIds: new Set(), focusedEaseSegment: null, selectedElementIds: new Set(), requestedSeekTime: null,