diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-07 04:41:27 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-07 04:41:27 +0200 |
| commit | cbd1b8212370c67b34a70dccd21bd82de72fead2 (patch) | |
| tree | d97cbee2aae22c22b59c59a50e9f8a6af0ae8ccf /apps/web/src/components/editor/panels | |
| parent | 4f7d401a974047d37673ee8e3840dceaaddc295c (diff) | |
| download | opencut-cbd1b8212370c67b34a70dccd21bd82de72fead2.tar.gz | |
refactor: structured track layout per scene
Diffstat (limited to 'apps/web/src/components/editor/panels')
8 files changed, 56 insertions, 21 deletions
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<TimelineTrack[]>( + () => + 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<HTMLDivElement>(null); @@ -424,13 +431,13 @@ export function Timeline() { /> <DragLine dropTarget={dropTarget} - tracks={timeline.getTracks()} + tracks={tracks} isVisible={isDragOver && !dropTarget?.targetElement} headerHeight={timelineHeaderHeight} /> <DragLine dropTarget={dragDropTarget} - tracks={timeline.getTracks()} + tracks={tracks} isVisible={dragState.isDragging} headerHeight={timelineHeaderHeight} /> @@ -508,6 +515,7 @@ export function Timeline() { {tracks.length > 0 && ( <TimelineTrackRows dragElementId={dragState.elementId} + mainTrackId={mainTrackId} zoomLevel={zoomLevel} dragState={dragState} tracksScrollRef={tracksScrollRef} @@ -574,7 +582,14 @@ function TrackLabelsPanel({ hasHorizontalScrollbar: boolean; }) { const editor = useEditor(); - const tracks = useEditor((e) => e.timeline.getTracks()); + const scene = useEditor((e) => e.scenes.getActiveSceneOrNull()); + const tracks = useMemo<TimelineTrack[]>( + () => + 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<HTMLDivElement | null>; @@ -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<TimelineTrack[]>( + () => + 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"} </ContextMenuItem> - {!isMainTrack(track) && ( + {track.id !== mainTrackId && ( <ContextMenuItem icon={<HugeiconsIcon icon={Delete02Icon} />} onClick={(event: React.MouseEvent) => { |
