From cbd1b8212370c67b34a70dccd21bd82de72fead2 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 7 Apr 2026 04:41:27 +0200 Subject: refactor: structured track layout per scene --- .../editor/panels/assets/views/captions.tsx | 2 +- .../src/components/editor/panels/preview/index.tsx | 4 ++- .../components/editor/panels/properties/index.tsx | 2 +- .../editor/panels/properties/tabs/masks-tab.tsx | 4 ++- .../editor/panels/timeline/drop-target.ts | 16 ++++++--- .../editor/panels/timeline/graph-editor/session.ts | 8 ++--- .../panels/timeline/graph-editor/use-controller.ts | 3 +- .../components/editor/panels/timeline/index.tsx | 38 ++++++++++++++++++---- 8 files changed, 56 insertions(+), 21 deletions(-) (limited to 'apps/web/src/components/editor/panels') diff --git a/apps/web/src/components/editor/panels/assets/views/captions.tsx b/apps/web/src/components/editor/panels/assets/views/captions.tsx index f22c0211..c89e587b 100644 --- a/apps/web/src/components/editor/panels/assets/views/captions.tsx +++ b/apps/web/src/components/editor/panels/assets/views/captions.tsx @@ -59,7 +59,7 @@ export function Captions() { setProcessingStep("Extracting audio..."); const audioBlob = await extractTimelineAudio({ - tracks: editor.timeline.getTracks(), + tracks: editor.scenes.getActiveScene().tracks, mediaAssets: editor.media.getAssets(), totalDuration: editor.timeline.getTotalDuration(), }); diff --git a/apps/web/src/components/editor/panels/preview/index.tsx b/apps/web/src/components/editor/panels/preview/index.tsx index 786d9a46..369d854c 100644 --- a/apps/web/src/components/editor/panels/preview/index.tsx +++ b/apps/web/src/components/editor/panels/preview/index.tsx @@ -73,7 +73,9 @@ export function PreviewPanel() { function RenderTreeController() { const editor = useEditor(); - const tracks = useEditor((e) => e.timeline.getRenderTracks()); + const tracks = useEditor( + (e) => e.timeline.getPreviewTracks() ?? e.scenes.getActiveScene().tracks, + ); const mediaAssets = useEditor((e) => e.media.getAssets()); const activeProject = useEditor((e) => e.project.getActive()); diff --git a/apps/web/src/components/editor/panels/properties/index.tsx b/apps/web/src/components/editor/panels/properties/index.tsx index 3f3ff1f3..75319ef3 100644 --- a/apps/web/src/components/editor/panels/properties/index.tsx +++ b/apps/web/src/components/editor/panels/properties/index.tsx @@ -17,7 +17,7 @@ import { EmptyView } from "./empty-view"; export function PropertiesPanel() { const editor = useEditor(); - useEditor((e) => e.timeline.getTracks()); + useEditor((e) => e.scenes.getActiveSceneOrNull()); useEditor((e) => e.media.getAssets()); const { selectedElements } = useElementSelection(); const { activeTabPerType, setActiveTab } = usePropertiesStore(); diff --git a/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx b/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx index 9b0ffc88..8148b457 100644 --- a/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx +++ b/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx @@ -87,7 +87,9 @@ export function MasksTab({ element, trackId }: MasksTabProps) { fallback: element, }); const maskDefs = masksRegistry.getAll(); - const tracks = useEditor((e) => e.timeline.getRenderTracks()); + const tracks = useEditor( + (e) => e.timeline.getPreviewTracks() ?? e.scenes.getActiveScene().tracks, + ); const currentTime = useEditor((e) => e.playback.getCurrentTime()); const mediaAssets = useEditor((e) => e.media.getAssets()); const canvasSize = useEditor( diff --git a/apps/web/src/components/editor/panels/timeline/drop-target.ts b/apps/web/src/components/editor/panels/timeline/drop-target.ts index dbd4efaf..f65170fe 100644 --- a/apps/web/src/components/editor/panels/timeline/drop-target.ts +++ b/apps/web/src/components/editor/panels/timeline/drop-target.ts @@ -105,6 +105,8 @@ export function computeDropTarget({ excludeElementId, targetElementTypes, }: ComputeDropTargetParams): DropTarget { + const orderedTracks = [...tracks.overlay, tracks.main, ...tracks.audio]; + const mainTrackIndex = tracks.overlay.length; const xPosition = typeof startTimeOverride === "number" ? startTimeOverride @@ -112,7 +114,7 @@ export function computeDropTarget({ ? playheadTime : Math.max(0, mouseX / (pixelsPerSecond * zoomLevel)); - if (tracks.length === 0) { + if (orderedTracks.length === 0) { const placementResult = resolveTrackPlacement({ tracks, elementType, @@ -139,7 +141,11 @@ export function computeDropTarget({ }; } - const trackAtMouse = getTrackAtY({ mouseY, tracks, verticalDragDirection }); + const trackAtMouse = getTrackAtY({ + mouseY, + tracks: orderedTracks, + verticalDragDirection, + }); if (!trackAtMouse) { const isAboveAllTracks = mouseY < 0; @@ -150,7 +156,7 @@ export function computeDropTarget({ timeSpans: [{ startTime: xPosition, duration: elementDuration, excludeElementId }], strategy: { type: "preferIndex", - trackIndex: isAboveAllTracks ? 0 : tracks.length - 1, + trackIndex: isAboveAllTracks ? 0 : orderedTracks.length - 1, hoverDirection: isAboveAllTracks ? "above" : "below", createNewTrackOnly: true, }, @@ -171,12 +177,12 @@ export function computeDropTarget({ } const { trackIndex, relativeY } = trackAtMouse; - const track = tracks[trackIndex]; + const track = orderedTracks[trackIndex]; if (targetElementTypes && targetElementTypes.length > 0) { const targetElement = findElementAtPosition({ mouseX, - tracks, + tracks: orderedTracks, trackIndex, targetElementTypes, pixelsPerSecond, diff --git a/apps/web/src/components/editor/panels/timeline/graph-editor/session.ts b/apps/web/src/components/editor/panels/timeline/graph-editor/session.ts index e16690e5..15657bfb 100644 --- a/apps/web/src/components/editor/panels/timeline/graph-editor/session.ts +++ b/apps/web/src/components/editor/panels/timeline/graph-editor/session.ts @@ -13,7 +13,7 @@ import type { ScalarGraphKeyframeContext, SelectedKeyframeRef, } from "@/lib/animation/types"; -import type { TimelineElement, TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks, TimelineElement } from "@/lib/timeline"; const GRAPH_LINEAR_CURVE: NormalizedCubicBezier = [0, 0, 1, 1]; const FLAT_VALUE_EPSILON = 1e-6; @@ -91,10 +91,10 @@ function findElementByKeyframe({ tracks, keyframe, }: { - tracks: TimelineTrack[]; + tracks: SceneTracks; keyframe: SelectedKeyframeRef; }): { element: TimelineElement; trackId: string; elementId: string } | null { - for (const track of tracks) { + for (const track of [...tracks.overlay, tracks.main, ...tracks.audio]) { if (track.id !== keyframe.trackId) { continue; } @@ -180,7 +180,7 @@ export function resolveGraphEditorSelectionState({ selectedKeyframes, preferredComponentKey, }: { - tracks: TimelineTrack[]; + tracks: SceneTracks; selectedKeyframes: SelectedKeyframeRef[]; preferredComponentKey?: string | null; }): GraphEditorSelectionState { diff --git a/apps/web/src/components/editor/panels/timeline/graph-editor/use-controller.ts b/apps/web/src/components/editor/panels/timeline/graph-editor/use-controller.ts index 41df702f..d0303b6f 100644 --- a/apps/web/src/components/editor/panels/timeline/graph-editor/use-controller.ts +++ b/apps/web/src/components/editor/panels/timeline/graph-editor/use-controller.ts @@ -15,7 +15,8 @@ import { export function useGraphEditorController() { const editor = useEditor(); const renderTracks = useEditor((currentEditor) => - currentEditor.timeline.getRenderTracks(), + currentEditor.timeline.getPreviewTracks() ?? + currentEditor.scenes.getActiveScene().tracks, ); const { selectedKeyframes } = useKeyframeSelection(); const [open, setOpen] = useState(false); diff --git a/apps/web/src/components/editor/panels/timeline/index.tsx b/apps/web/src/components/editor/panels/timeline/index.tsx index 11abfd68..883011d1 100644 --- a/apps/web/src/components/editor/panels/timeline/index.tsx +++ b/apps/web/src/components/editor/panels/timeline/index.tsx @@ -58,7 +58,6 @@ import { } from "./track-layout"; import { SELECTED_TRACK_ROW_CLASS } from "./theme"; import { TIMELINE_HORIZONTAL_WHEEL_STEP_PX } from "./interaction"; -import { isMainTrack } from "@/lib/timeline/placement"; import { TimelineToolbar } from "./timeline-toolbar"; import { useElementSelection } from "@/hooks/timeline/element/use-element-selection"; import { useTimelineSeek } from "@/hooks/timeline/use-timeline-seek"; @@ -112,7 +111,15 @@ export function Timeline() { } = useElementSelection(); const editor = useEditor(); const timeline = editor.timeline; - const tracks = useEditor((editor) => editor.timeline.getTracks()); + const scene = useEditor((currentEditor) => currentEditor.scenes.getActiveSceneOrNull()); + const tracks = useMemo( + () => + scene + ? [...scene.tracks.overlay, scene.tracks.main, ...scene.tracks.audio] + : [], + [scene], + ); + const mainTrackId = scene?.tracks.main.id ?? null; const seek = (time: number) => editor.playback.seek({ time }); const timelineRef = useRef(null); @@ -424,13 +431,13 @@ export function Timeline() { /> @@ -508,6 +515,7 @@ export function Timeline() { {tracks.length > 0 && ( e.timeline.getTracks()); + const scene = useEditor((e) => e.scenes.getActiveSceneOrNull()); + const tracks = useMemo( + () => + scene + ? [...scene.tracks.overlay, scene.tracks.main, ...scene.tracks.audio] + : [], + [scene], + ); const { selectedElements } = useElementSelection(); const tracksWithSelection = useMemo( () => new Set(selectedElements.map((el) => el.trackId)), @@ -650,6 +665,7 @@ function TrackLabelsPanel({ function TimelineTrackRows({ dragElementId, + mainTrackId, zoomLevel, dragState, tracksScrollRef, @@ -665,6 +681,7 @@ function TimelineTrackRows({ dropTarget, }: { dragElementId: string | null; + mainTrackId: string | null; zoomLevel: number; dragState: ElementDragState; tracksScrollRef: React.RefObject; @@ -684,7 +701,14 @@ function TimelineTrackRows({ dropTarget: DropTarget | null; }) { const timeline = useEditor((e) => e.timeline); - const tracks = useEditor((e) => e.timeline.getTracks()); + const scene = useEditor((e) => e.scenes.getActiveSceneOrNull()); + const tracks = useMemo( + () => + scene + ? [...scene.tracks.overlay, scene.tracks.main, ...scene.tracks.audio] + : [], + [scene], + ); const { selectedElements } = useElementSelection(); const tracksWithSelection = useMemo( () => new Set(selectedElements.map((el) => el.trackId)), @@ -779,7 +803,7 @@ function TimelineTrackRows({ ? "Show track" : "Hide track"} - {!isMainTrack(track) && ( + {track.id !== mainTrackId && ( } onClick={(event: React.MouseEvent) => { -- cgit v1.3.1