mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
refactor: structured track layout per scene
This commit is contained in:
@@ -10,7 +10,6 @@ import { AudioManager } from "./managers/audio-manager";
|
||||
import { SelectionManager } from "./managers/selection-manager";
|
||||
import { registerDefaultEffects } from "@/lib/effects";
|
||||
import { registerDefaultMasks } from "@/lib/masks";
|
||||
import { isMainTrack } from "@/lib/timeline/placement";
|
||||
|
||||
export class EditorCore {
|
||||
private static instance: EditorCore | null = null;
|
||||
@@ -39,11 +38,21 @@ export class EditorCore {
|
||||
this.audio = new AudioManager(this);
|
||||
this.selection = new SelectionManager(this);
|
||||
this.command.registerReactor(() => {
|
||||
const tracks = this.timeline.getTracks();
|
||||
const prunedTracks = tracks.filter(
|
||||
(track) => track.elements.length > 0 || isMainTrack(track),
|
||||
);
|
||||
if (prunedTracks.length !== tracks.length) {
|
||||
const activeScene = this.scenes.getActiveSceneOrNull();
|
||||
if (!activeScene) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tracks = activeScene.tracks;
|
||||
const prunedTracks = {
|
||||
...tracks,
|
||||
overlay: tracks.overlay.filter((track) => track.elements.length > 0),
|
||||
audio: tracks.audio.filter((track) => track.elements.length > 0),
|
||||
};
|
||||
if (
|
||||
prunedTracks.overlay.length !== tracks.overlay.length ||
|
||||
prunedTracks.audio.length !== tracks.audio.length
|
||||
) {
|
||||
this.timeline.updateTracks(prunedTracks);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { EditorCore } from "@/core";
|
||||
import type { ParamValues } from "@/lib/params";
|
||||
import type {
|
||||
SceneTracks,
|
||||
TrackType,
|
||||
TimelineTrack,
|
||||
TimelineElement,
|
||||
@@ -8,6 +9,10 @@ import type {
|
||||
RetimeConfig,
|
||||
} from "@/lib/timeline";
|
||||
import { calculateTotalDuration } from "@/lib/timeline";
|
||||
import {
|
||||
findTrackInSceneTracks,
|
||||
updateElementInSceneTracks,
|
||||
} from "@/lib/timeline/track-element-update";
|
||||
import {
|
||||
canElementBeHidden,
|
||||
canElementHaveAudio,
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user