summaryrefslogtreecommitdiff
path: root/apps/web/src/core/managers
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-07 04:41:27 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-07 04:41:27 +0200
commitcbd1b8212370c67b34a70dccd21bd82de72fead2 (patch)
treed97cbee2aae22c22b59c59a50e9f8a6af0ae8ccf /apps/web/src/core/managers
parent4f7d401a974047d37673ee8e3840dceaaddc295c (diff)
downloadopencut-cbd1b8212370c67b34a70dccd21bd82de72fead2.tar.gz
refactor: structured track layout per scene
Diffstat (limited to 'apps/web/src/core/managers')
-rw-r--r--apps/web/src/core/managers/audio-manager.ts2
-rw-r--r--apps/web/src/core/managers/commands.ts17
-rw-r--r--apps/web/src/core/managers/project-manager.ts11
-rw-r--r--apps/web/src/core/managers/renderer-manager.ts2
-rw-r--r--apps/web/src/core/managers/scenes-manager.ts67
-rw-r--r--apps/web/src/core/managers/timeline-manager.ts106
6 files changed, 112 insertions, 93 deletions
diff --git a/apps/web/src/core/managers/audio-manager.ts b/apps/web/src/core/managers/audio-manager.ts
index aaeb7ade..076b998d 100644
--- a/apps/web/src/core/managers/audio-manager.ts
+++ b/apps/web/src/core/managers/audio-manager.ts
@@ -163,7 +163,7 @@ export class AudioManager {
this.playbackSessionId++;
this.playbackLatencyCompensationSeconds = 0;
- const tracks = this.editor.timeline.getTracks();
+ const tracks = this.editor.scenes.getActiveScene().tracks;
const mediaAssets = this.editor.media.getAssets();
const duration = this.editor.timeline.getTotalDuration();
diff --git a/apps/web/src/core/managers/commands.ts b/apps/web/src/core/managers/commands.ts
index a3665852..2f652a85 100644
--- a/apps/web/src/core/managers/commands.ts
+++ b/apps/web/src/core/managers/commands.ts
@@ -1,7 +1,7 @@
import type { EditorCore } from "@/core";
import type { Command, CommandResult } from "@/lib/commands";
import { applyRippleAdjustments, computeRippleAdjustments } from "@/lib/ripple";
-import type { TimelineTrack, ElementRef } from "@/lib/timeline/types";
+import type { ElementRef, SceneTracks } from "@/lib/timeline/types";
interface CommandHistoryEntry {
command: Command;
@@ -18,7 +18,9 @@ export class CommandManager {
constructor(private editor: EditorCore) {}
execute({ command }: { command: Command }): Command {
- const beforeTracks = this.isRippleEnabled ? this.editor.timeline.getTracks() : null;
+ const beforeTracks = this.isRippleEnabled
+ ? this.editor.scenes.getActiveSceneOrNull()?.tracks ?? null
+ : null;
const previousSelection = this.getSelectionSnapshot();
const result = command.execute();
this.applyRippleIfEnabled({ beforeTracks });
@@ -71,7 +73,9 @@ export class CommandManager {
return;
}
- const beforeTracks = this.isRippleEnabled ? this.editor.timeline.getTracks() : null;
+ const beforeTracks = this.isRippleEnabled
+ ? this.editor.scenes.getActiveSceneOrNull()?.tracks ?? null
+ : null;
const previousSelection = this.getSelectionSnapshot();
const result = entry.command.redo();
this.applyRippleIfEnabled({ beforeTracks });
@@ -123,13 +127,16 @@ export class CommandManager {
private applyRippleIfEnabled({
beforeTracks,
}: {
- beforeTracks: TimelineTrack[] | null;
+ beforeTracks: SceneTracks | null;
}): void {
if (!this.isRippleEnabled || !beforeTracks) {
return;
}
- const afterTracks = this.editor.timeline.getTracks();
+ const afterTracks = this.editor.scenes.getActiveSceneOrNull()?.tracks;
+ if (!afterTracks) {
+ return;
+ }
const adjustments = computeRippleAdjustments({
beforeTracks,
afterTracks,
diff --git a/apps/web/src/core/managers/project-manager.ts b/apps/web/src/core/managers/project-manager.ts
index 513eab94..6324b988 100644
--- a/apps/web/src/core/managers/project-manager.ts
+++ b/apps/web/src/core/managers/project-manager.ts
@@ -156,9 +156,14 @@ export class ProjectManager {
await this.editor.media.loadProjectMedia({ projectId: id });
- const allTracks = (project.scenes ?? []).flatMap((scene) => scene.tracks);
await loadFonts({
- families: getElementFontFamilies({ tracks: allTracks }),
+ families: [
+ ...new Set(
+ (project.scenes ?? []).flatMap((scene) =>
+ getElementFontFamilies({ tracks: scene.tracks }),
+ ),
+ ),
+ ],
});
if (!project.metadata.thumbnail) {
@@ -643,7 +648,7 @@ export class ProjectManager {
private async updateThumbnailFromTimeline(): Promise<boolean> {
if (!this.active) return false;
- const tracks = this.editor.timeline.getTracks();
+ const tracks = this.editor.scenes.getActiveScene().tracks;
const mediaAssets = this.editor.media.getAssets();
const duration = this.editor.timeline.getTotalDuration();
const { canvasSize, background } = this.active.settings;
diff --git a/apps/web/src/core/managers/renderer-manager.ts b/apps/web/src/core/managers/renderer-manager.ts
index e0d5b895..eec4bf37 100644
--- a/apps/web/src/core/managers/renderer-manager.ts
+++ b/apps/web/src/core/managers/renderer-manager.ts
@@ -139,7 +139,7 @@ export class RendererManager {
const { format, quality, fps, includeAudio } = options;
try {
- const tracks = this.editor.timeline.getTracks();
+ const tracks = this.editor.scenes.getActiveScene().tracks;
const mediaAssets = this.editor.media.getAssets();
const activeProject = this.editor.project.getActive();
diff --git a/apps/web/src/core/managers/scenes-manager.ts b/apps/web/src/core/managers/scenes-manager.ts
index 43938f96..3508a066 100644
--- a/apps/web/src/core/managers/scenes-manager.ts
+++ b/apps/web/src/core/managers/scenes-manager.ts
@@ -1,5 +1,5 @@
import type { EditorCore } from "@/core";
-import type { TimelineTrack, TScene } from "@/lib/timeline";
+import type { SceneTracks, TScene } from "@/lib/timeline";
import { storageService } from "@/services/storage/service";
import {
getMainScene,
@@ -12,7 +12,6 @@ import {
getFrameTime,
isBookmarkAtTime,
} from "@/lib/timeline/bookmarks";
-import { ensureMainTrack } from "@/lib/timeline/placement";
import {
CreateSceneCommand,
DeleteSceneCommand,
@@ -174,10 +173,7 @@ export class ScenesManager {
try {
const result = await storageService.loadProject({ id: projectId });
if (result?.project.scenes) {
- const { scenes: ensuredScenes, hasAddedMainTrack } =
- this.ensureScenesHaveMainTrack({
- scenes: result.project.scenes ?? [],
- });
+ const ensuredScenes = result.project.scenes ?? [];
const currentScene = findCurrentScene({
scenes: ensuredScenes,
currentSceneId: result.project.currentSceneId,
@@ -186,22 +182,6 @@ export class ScenesManager {
this.list = ensuredScenes;
this.active = currentScene;
this.notify();
-
- if (hasAddedMainTrack) {
- const activeProject = this.editor.project.getActive();
- if (activeProject) {
- const updatedProject = {
- ...activeProject,
- scenes: ensuredScenes,
- metadata: {
- ...activeProject.metadata,
- updatedAt: new Date(),
- },
- };
- this.editor.project.setActiveProject({ project: updatedProject });
- this.editor.save.markDirty({ force: true });
- }
- }
}
} catch (error) {
console.error("Failed to load project scenes:", error);
@@ -219,26 +199,24 @@ export class ScenesManager {
currentSceneId?: string;
}): void {
const ensuredScenes = ensureMainScene({ scenes });
- const { scenes: scenesWithMainTracks, hasAddedMainTrack } =
- this.ensureScenesHaveMainTrack({ scenes: ensuredScenes });
const currentScene = currentSceneId
- ? scenesWithMainTracks.find((s) => s.id === currentSceneId)
+ ? ensuredScenes.find((s) => s.id === currentSceneId)
: null;
- const fallbackScene = getMainScene({ scenes: scenesWithMainTracks });
+ const fallbackScene = getMainScene({ scenes: ensuredScenes });
- this.list = scenesWithMainTracks;
+ this.list = ensuredScenes;
this.active = currentScene || fallbackScene;
this.notify();
const hasAddedMainScene = ensuredScenes.length > scenes.length;
- if (hasAddedMainScene || hasAddedMainTrack) {
+ if (hasAddedMainScene) {
const activeProject = this.editor.project.getActive();
if (activeProject) {
const updatedProject = {
...activeProject,
- scenes: scenesWithMainTracks,
+ scenes: ensuredScenes,
metadata: {
...activeProject.metadata,
updatedAt: new Date(),
@@ -264,6 +242,10 @@ export class ScenesManager {
return this.active;
}
+ getActiveSceneOrNull(): TScene | null {
+ return this.active;
+ }
+
getScenes(): TScene[] {
return this.list;
}
@@ -307,7 +289,7 @@ export class ScenesManager {
});
}
- updateSceneTracks({ tracks }: { tracks: TimelineTrack[] }): void {
+ updateSceneTracks({ tracks }: { tracks: SceneTracks }): void {
if (!this.active) return;
const updatedScene: TScene = {
@@ -335,29 +317,4 @@ export class ScenesManager {
this.editor.project.setActiveProject({ project: updatedProject });
}
}
-
- private ensureScenesHaveMainTrack({ scenes }: { scenes: TScene[] }): {
- scenes: TScene[];
- hasAddedMainTrack: boolean;
- } {
- let hasAddedMainTrack = false;
- const ensuredScenes: TScene[] = [];
-
- for (const scene of scenes) {
- const existingTracks = scene.tracks ?? [];
- const updatedTracks = ensureMainTrack({ tracks: existingTracks });
- if (updatedTracks !== existingTracks) {
- hasAddedMainTrack = true;
- ensuredScenes.push({
- ...scene,
- tracks: updatedTracks,
- updatedAt: new Date(),
- });
- } else {
- ensuredScenes.push(scene);
- }
- }
-
- return { scenes: ensuredScenes, hasAddedMainTrack };
- }
}
diff --git a/apps/web/src/core/managers/timeline-manager.ts b/apps/web/src/core/managers/timeline-manager.ts
index a5cb428e..74870a95 100644
--- a/apps/web/src/core/managers/timeline-manager.ts
+++ b/apps/web/src/core/managers/timeline-manager.ts
@@ -1,6 +1,7 @@
import type { EditorCore } from "@/core";
import type { ParamValues } from "@/lib/params";
import type {
+ SceneTracks,
TrackType,
TimelineTrack,
TimelineElement,
@@ -9,6 +10,10 @@ import type {
} from "@/lib/timeline";
import { calculateTotalDuration } from "@/lib/timeline";
import {
+ findTrackInSceneTracks,
+ updateElementInSceneTracks,
+} from "@/lib/timeline/track-element-update";
+import {
canElementBeHidden,
canElementHaveAudio,
} from "@/lib/timeline/element-utils";
@@ -53,7 +58,7 @@ import type { InsertElementParams } from "@/lib/commands/timeline/element/insert
export class TimelineManager {
private listeners = new Set<() => void>();
private previewOverlay = new Map<string, Partial<TimelineElement>>();
- private previewTracks: TimelineTrack[] | null = null;
+ private previewTracks: SceneTracks | null = null;
constructor(private editor: EditorCore) {}
@@ -193,7 +198,12 @@ export class TimelineManager {
}
getTotalDuration(): number {
- return calculateTotalDuration({ tracks: this.getTracks() });
+ const activeScene = this.editor.scenes.getActiveSceneOrNull();
+ if (!activeScene) {
+ return 0;
+ }
+
+ return calculateTotalDuration({ tracks: activeScene.tracks });
}
getLastFrameTime(): number {
@@ -204,7 +214,12 @@ export class TimelineManager {
}
getTrackById({ trackId }: { trackId: string }): TimelineTrack | null {
- return this.getTracks().find((track) => track.id === trackId) ?? null;
+ const activeScene = this.editor.scenes.getActiveSceneOrNull();
+ if (!activeScene) {
+ return null;
+ }
+
+ return findTrackInSceneTracks({ tracks: activeScene.tracks, trackId });
}
getElementsWithTracks({
@@ -629,14 +644,20 @@ export class TimelineManager {
} as Partial<TimelineElement>;
this.previewOverlay.set(elementId, mergedOverlay);
}
- const committedTracks = this.editor.scenes.getActiveScene()?.tracks ?? [];
+ const committedTracks = this.editor.scenes.getActiveSceneOrNull()?.tracks;
+ if (!committedTracks) {
+ return;
+ }
this.previewTracks = this.applyPreviewOverlay(committedTracks);
this.notify();
}
commitPreview(): void {
if (this.previewOverlay.size === 0) return;
- const committedTracks = this.editor.scenes.getActiveScene()?.tracks ?? [];
+ const committedTracks = this.editor.scenes.getActiveSceneOrNull()?.tracks;
+ if (!committedTracks) {
+ return;
+ }
const afterTracks =
this.previewTracks ?? this.applyPreviewOverlay(committedTracks);
const command = new TracksSnapshotCommand(committedTracks, afterTracks);
@@ -653,19 +674,32 @@ export class TimelineManager {
this.notify();
}
- private applyPreviewOverlay(tracks: TimelineTrack[]): TimelineTrack[] {
+ private applyPreviewOverlay(tracks: SceneTracks): SceneTracks {
if (this.previewOverlay.size === 0) return tracks;
- return tracks.map((track) => {
- const hasOverlay = track.elements.some((el) =>
- this.previewOverlay.has(el.id),
+
+ const applyTrackOverlay = <TTrack extends TimelineTrack>(track: TTrack): TTrack => {
+ const hasOverlay = track.elements.some((element) =>
+ this.previewOverlay.has(element.id),
);
- if (!hasOverlay) return track;
- const newElements = track.elements.map((el) => {
- const overlay = this.previewOverlay.get(el.id);
- return overlay ? ({ ...el, ...overlay } as TimelineElement) : el;
+ if (!hasOverlay) {
+ return track;
+ }
+
+ const nextElements = track.elements.map((element) => {
+ const overlay = this.previewOverlay.get(element.id);
+ return overlay
+ ? ({ ...element, ...overlay } as TimelineElement)
+ : element;
});
- return { ...track, elements: newElements } as TimelineTrack;
- });
+
+ return { ...track, elements: nextElements } as TTrack;
+ };
+
+ return {
+ overlay: tracks.overlay.map((track) => applyTrackOverlay(track)),
+ main: applyTrackOverlay(tracks.main),
+ audio: tracks.audio.map((track) => applyTrackOverlay(track)),
+ };
}
duplicateElements({
@@ -734,13 +768,8 @@ export class TimelineManager {
this.updateElements({ updates: nextUpdates });
}
- getTracks(): TimelineTrack[] {
- return this.editor.scenes.getActiveScene()?.tracks ?? [];
- }
-
- getRenderTracks(): TimelineTrack[] {
- if (this.previewTracks !== null) return this.previewTracks;
- return this.getTracks();
+ getPreviewTracks(): SceneTracks | null {
+ return this.previewTracks ?? this.editor.scenes.getActiveSceneOrNull()?.tracks ?? null;
}
subscribe(listener: () => void): () => void {
@@ -771,14 +800,35 @@ export class TimelineManager {
}: {
elementId: string;
}): string | null {
- return (
- this.getTracks().find((track) =>
- track.elements.some((element) => element.id === elementId),
- )?.id ?? null
- );
+ const activeScene = this.editor.scenes.getActiveSceneOrNull();
+ if (!activeScene) {
+ return null;
+ }
+
+ if (
+ activeScene.tracks.main.elements.some(
+ (element) => element.id === elementId,
+ )
+ ) {
+ return activeScene.tracks.main.id;
+ }
+
+ for (const track of activeScene.tracks.overlay) {
+ if (track.elements.some((element) => element.id === elementId)) {
+ return track.id;
+ }
+ }
+
+ for (const track of activeScene.tracks.audio) {
+ if (track.elements.some((element) => element.id === elementId)) {
+ return track.id;
+ }
+ }
+
+ return null;
}
- updateTracks(newTracks: TimelineTrack[]): void {
+ updateTracks(newTracks: SceneTracks): void {
this.previewOverlay.clear();
this.previewTracks = null;
this.editor.scenes.updateSceneTracks({ tracks: newTracks });