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 --- apps/web/src/lib/commands/media/add-media-asset.ts | 8 +- .../src/lib/commands/media/remove-media-asset.ts | 202 +++++++++++---------- apps/web/src/lib/commands/preview-tracker.ts | 46 ++--- .../commands/project/update-project-settings.ts | 92 +++++----- apps/web/src/lib/commands/scene/delete-scene.ts | 118 ++++++------ apps/web/src/lib/commands/scene/index.ts | 14 +- apps/web/src/lib/commands/scene/move-bookmark.ts | 118 ++++++------ apps/web/src/lib/commands/scene/remove-bookmark.ts | 118 ++++++------ apps/web/src/lib/commands/scene/rename-scene.ts | 92 +++++----- apps/web/src/lib/commands/scene/toggle-bookmark.ts | 104 +++++------ apps/web/src/lib/commands/scene/update-bookmark.ts | 110 +++++------ .../src/lib/commands/timeline/clipboard/paste.ts | 34 ++-- .../commands/timeline/element/delete-elements.ts | 59 +++--- .../timeline/element/duplicate-elements.ts | 14 +- .../timeline/element/effects/add-effect.ts | 10 +- .../lib/commands/timeline/element/effects/index.ts | 10 +- .../timeline/element/effects/remove-effect.ts | 10 +- .../timeline/element/effects/reorder-effect.ts | 10 +- .../timeline/element/effects/toggle-effect.ts | 10 +- .../element/effects/update-effect-params.ts | 10 +- .../web/src/lib/commands/timeline/element/index.ts | 22 +-- .../commands/timeline/element/insert-element.ts | 44 +++-- .../keyframes/remove-effect-param-keyframe.ts | 10 +- .../timeline/element/keyframes/remove-keyframe.ts | 10 +- .../timeline/element/keyframes/retime-keyframe.ts | 10 +- .../keyframes/update-scalar-keyframe-curve.ts | 10 +- .../keyframes/upsert-effect-param-keyframe.ts | 10 +- .../timeline/element/keyframes/upsert-keyframe.ts | 10 +- .../lib/commands/timeline/element/masks/index.ts | 4 +- .../commands/timeline/element/masks/remove-mask.ts | 10 +- .../timeline/element/masks/toggle-mask-inverted.ts | 10 +- .../lib/commands/timeline/element/move-elements.ts | 122 +++++++++---- .../commands/timeline/element/split-elements.ts | 20 +- .../element/toggle-source-audio-separation.ts | 87 ++++----- .../commands/timeline/element/update-elements.ts | 20 +- apps/web/src/lib/commands/timeline/index.ts | 10 +- .../src/lib/commands/timeline/track/add-track.ts | 86 +++++++-- .../lib/commands/timeline/track/remove-track.ts | 65 +++---- .../commands/timeline/track/toggle-track-mute.ts | 80 ++++---- .../timeline/track/toggle-track-visibility.ts | 85 +++++---- .../src/lib/commands/timeline/tracks-snapshot.ts | 6 +- 41 files changed, 1040 insertions(+), 880 deletions(-) (limited to 'apps/web/src/lib/commands') diff --git a/apps/web/src/lib/commands/media/add-media-asset.ts b/apps/web/src/lib/commands/media/add-media-asset.ts index 8b3067a1..12e0906e 100644 --- a/apps/web/src/lib/commands/media/add-media-asset.ts +++ b/apps/web/src/lib/commands/media/add-media-asset.ts @@ -54,11 +54,15 @@ export class AddMediaAssetCommand extends Command { assets: currentAssets.filter((asset) => asset.id !== this.assetId), }); - const currentTracks = editor.timeline.getTracks(); + const currentTracks = editor.scenes.getActiveScene().tracks; const orphanedElements: Array<{ trackId: string; elementId: string }> = []; - for (const track of currentTracks) { + for (const track of [ + ...currentTracks.overlay, + currentTracks.main, + ...currentTracks.audio, + ]) { for (const element of track.elements) { if (hasMediaId(element) && element.mediaId === this.assetId) { orphanedElements.push({ diff --git a/apps/web/src/lib/commands/media/remove-media-asset.ts b/apps/web/src/lib/commands/media/remove-media-asset.ts index 1a938de7..5601a7e3 100644 --- a/apps/web/src/lib/commands/media/remove-media-asset.ts +++ b/apps/web/src/lib/commands/media/remove-media-asset.ts @@ -1,99 +1,103 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { EditorCore } from "@/core"; -import type { MediaAsset } from "@/lib/media/types"; -import { storageService } from "@/services/storage/service"; -import { videoCache } from "@/services/video-cache/service"; -import { hasMediaId } from "@/lib/timeline/element-utils"; -import type { TimelineTrack } from "@/lib/timeline"; - -export class RemoveMediaAssetCommand extends Command { - private savedAssets: MediaAsset[] | null = null; - private savedTracks: TimelineTrack[] | null = null; - private removedAsset: MediaAsset | null = null; - - constructor( - private projectId: string, - private assetId: string, - ) { - super(); - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - const assets = editor.media.getAssets(); - - this.savedAssets = [...assets]; - this.savedTracks = editor.timeline.getTracks(); - - this.removedAsset = - assets.find((media) => media.id === this.assetId) ?? null; - - if (!this.removedAsset) { - console.error("Media asset not found:", this.assetId); - return; - } - - if (this.removedAsset.url) { - URL.revokeObjectURL(this.removedAsset.url); - } - if (this.removedAsset.thumbnailUrl) { - URL.revokeObjectURL(this.removedAsset.thumbnailUrl); - } - - videoCache.clearVideo({ mediaId: this.assetId }); - - editor.media.setAssets({ - assets: assets.filter((media) => media.id !== this.assetId), - }); - - const elementsToRemove: Array<{ trackId: string; elementId: string }> = []; - - for (const track of this.savedTracks) { - for (const element of track.elements) { - if (hasMediaId(element) && element.mediaId === this.assetId) { - elementsToRemove.push({ trackId: track.id, elementId: element.id }); - } - } - } - - if (elementsToRemove.length > 0) { - editor.timeline.deleteElements({ elements: elementsToRemove }); - } - - storageService - .deleteMediaAsset({ projectId: this.projectId, id: this.assetId }) - .catch((error) => { - console.error("Failed to delete media item:", error); - }); - } - - undo(): void { - const editor = EditorCore.getInstance(); - - if (this.savedAssets && this.removedAsset) { - const restoredAsset: MediaAsset = { - ...this.removedAsset, - url: URL.createObjectURL(this.removedAsset.file), - }; - - editor.media.setAssets({ - assets: this.savedAssets.map((a) => - a.id === this.assetId ? restoredAsset : a, - ), - }); - - storageService - .saveMediaAsset({ - projectId: this.projectId, - mediaAsset: restoredAsset, - }) - .catch((error) => { - console.error("Failed to restore media item on undo:", error); - }); - } - - if (this.savedTracks) { - editor.timeline.updateTracks(this.savedTracks); - } - } -} +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { EditorCore } from "@/core"; +import type { MediaAsset } from "@/lib/media/types"; +import { storageService } from "@/services/storage/service"; +import { videoCache } from "@/services/video-cache/service"; +import { hasMediaId } from "@/lib/timeline/element-utils"; +import type { SceneTracks } from "@/lib/timeline"; + +export class RemoveMediaAssetCommand extends Command { + private savedAssets: MediaAsset[] | null = null; + private savedTracks: SceneTracks | null = null; + private removedAsset: MediaAsset | null = null; + + constructor( + private projectId: string, + private assetId: string, + ) { + super(); + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + const assets = editor.media.getAssets(); + + this.savedAssets = [...assets]; + this.savedTracks = editor.scenes.getActiveScene().tracks; + + this.removedAsset = + assets.find((media) => media.id === this.assetId) ?? null; + + if (!this.removedAsset) { + console.error("Media asset not found:", this.assetId); + return; + } + + if (this.removedAsset.url) { + URL.revokeObjectURL(this.removedAsset.url); + } + if (this.removedAsset.thumbnailUrl) { + URL.revokeObjectURL(this.removedAsset.thumbnailUrl); + } + + videoCache.clearVideo({ mediaId: this.assetId }); + + editor.media.setAssets({ + assets: assets.filter((media) => media.id !== this.assetId), + }); + + const elementsToRemove: Array<{ trackId: string; elementId: string }> = []; + + for (const track of [ + ...this.savedTracks.overlay, + this.savedTracks.main, + ...this.savedTracks.audio, + ]) { + for (const element of track.elements) { + if (hasMediaId(element) && element.mediaId === this.assetId) { + elementsToRemove.push({ trackId: track.id, elementId: element.id }); + } + } + } + + if (elementsToRemove.length > 0) { + editor.timeline.deleteElements({ elements: elementsToRemove }); + } + + storageService + .deleteMediaAsset({ projectId: this.projectId, id: this.assetId }) + .catch((error) => { + console.error("Failed to delete media item:", error); + }); + } + + undo(): void { + const editor = EditorCore.getInstance(); + + if (this.savedAssets && this.removedAsset) { + const restoredAsset: MediaAsset = { + ...this.removedAsset, + url: URL.createObjectURL(this.removedAsset.file), + }; + + editor.media.setAssets({ + assets: this.savedAssets.map((a) => + a.id === this.assetId ? restoredAsset : a, + ), + }); + + storageService + .saveMediaAsset({ + projectId: this.projectId, + mediaAsset: restoredAsset, + }) + .catch((error) => { + console.error("Failed to restore media item on undo:", error); + }); + } + + if (this.savedTracks) { + editor.timeline.updateTracks(this.savedTracks); + } + } +} diff --git a/apps/web/src/lib/commands/preview-tracker.ts b/apps/web/src/lib/commands/preview-tracker.ts index 38ef4edd..68c8dee3 100644 --- a/apps/web/src/lib/commands/preview-tracker.ts +++ b/apps/web/src/lib/commands/preview-tracker.ts @@ -1,23 +1,23 @@ -export class PreviewTracker { - private snapshot: T | null = null; - - begin({ state }: { state: T }): void { - if (this.snapshot === null) { - this.snapshot = structuredClone(state); - } - } - - isActive(): boolean { - return this.snapshot !== null; - } - - getSnapshot(): T | null { - return this.snapshot; - } - - end(): T | null { - const snapshot = this.snapshot; - this.snapshot = null; - return snapshot; - } -} +export class PreviewTracker { + private snapshot: T | null = null; + + begin({ state }: { state: T }): void { + if (this.snapshot === null) { + this.snapshot = structuredClone(state); + } + } + + isActive(): boolean { + return this.snapshot !== null; + } + + getSnapshot(): T | null { + return this.snapshot; + } + + end(): T | null { + const snapshot = this.snapshot; + this.snapshot = null; + return snapshot; + } +} diff --git a/apps/web/src/lib/commands/project/update-project-settings.ts b/apps/web/src/lib/commands/project/update-project-settings.ts index b4cbb125..32258e12 100644 --- a/apps/web/src/lib/commands/project/update-project-settings.ts +++ b/apps/web/src/lib/commands/project/update-project-settings.ts @@ -1,46 +1,46 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { EditorCore } from "@/core"; -import type { TProject, TProjectSettings } from "@/lib/project/types"; - -export class UpdateProjectSettingsCommand extends Command { - private savedSettings: TProjectSettings | null = null; - private savedUpdatedAt: Date | null = null; - - constructor(private updates: Partial) { - super(); - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - const activeProject = editor.project.getActive(); - if (!activeProject) return; - - this.savedSettings = activeProject.settings; - this.savedUpdatedAt = activeProject.metadata.updatedAt; - - const updatedProject: TProject = { - ...activeProject, - settings: { ...activeProject.settings, ...this.updates }, - metadata: { ...activeProject.metadata, updatedAt: new Date() }, - }; - - editor.project.setActiveProject({ project: updatedProject }); - editor.save.markDirty(); - } - - undo(): void { - if (!this.savedSettings || !this.savedUpdatedAt) return; - const editor = EditorCore.getInstance(); - const activeProject = editor.project.getActive(); - if (!activeProject) return; - - const updatedProject: TProject = { - ...activeProject, - settings: this.savedSettings, - metadata: { ...activeProject.metadata, updatedAt: this.savedUpdatedAt }, - }; - - editor.project.setActiveProject({ project: updatedProject }); - editor.save.markDirty(); - } -} +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { EditorCore } from "@/core"; +import type { TProject, TProjectSettings } from "@/lib/project/types"; + +export class UpdateProjectSettingsCommand extends Command { + private savedSettings: TProjectSettings | null = null; + private savedUpdatedAt: Date | null = null; + + constructor(private updates: Partial) { + super(); + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + const activeProject = editor.project.getActive(); + if (!activeProject) return; + + this.savedSettings = activeProject.settings; + this.savedUpdatedAt = activeProject.metadata.updatedAt; + + const updatedProject: TProject = { + ...activeProject, + settings: { ...activeProject.settings, ...this.updates }, + metadata: { ...activeProject.metadata, updatedAt: new Date() }, + }; + + editor.project.setActiveProject({ project: updatedProject }); + editor.save.markDirty(); + } + + undo(): void { + if (!this.savedSettings || !this.savedUpdatedAt) return; + const editor = EditorCore.getInstance(); + const activeProject = editor.project.getActive(); + if (!activeProject) return; + + const updatedProject: TProject = { + ...activeProject, + settings: this.savedSettings, + metadata: { ...activeProject.metadata, updatedAt: this.savedUpdatedAt }, + }; + + editor.project.setActiveProject({ project: updatedProject }); + editor.save.markDirty(); + } +} diff --git a/apps/web/src/lib/commands/scene/delete-scene.ts b/apps/web/src/lib/commands/scene/delete-scene.ts index 1c433687..ab269b12 100644 --- a/apps/web/src/lib/commands/scene/delete-scene.ts +++ b/apps/web/src/lib/commands/scene/delete-scene.ts @@ -1,59 +1,59 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { EditorCore } from "@/core"; -import type { TScene } from "@/lib/timeline"; -import { canDeleteScene, getFallbackSceneAfterDelete } from "@/lib/scenes"; - -export class DeleteSceneCommand extends Command { - private savedScenes: TScene[] | null = null; - private savedActiveSceneId: string | null = null; - private deletedScene: TScene | null = null; - - constructor(private sceneId: string) { - super(); - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - const scenes = editor.scenes.getScenes(); - const activeScene = editor.scenes.getActiveScene(); - - this.savedScenes = [...scenes]; - this.savedActiveSceneId = activeScene?.id ?? null; - - this.deletedScene = scenes.find((s) => s.id === this.sceneId) ?? null; - - if (!this.deletedScene) { - console.error("Scene not found:", this.sceneId); - return; - } - - const { canDelete, reason } = canDeleteScene({ scene: this.deletedScene }); - if (!canDelete) { - console.error("Cannot delete scene:", reason); - return; - } - - const updatedScenes = scenes.filter((s) => s.id !== this.sceneId); - - const newActiveScene = getFallbackSceneAfterDelete({ - scenes: updatedScenes, - deletedSceneId: this.sceneId, - currentSceneId: activeScene?.id ?? null, - }); - - editor.scenes.setScenes({ - scenes: updatedScenes, - activeSceneId: newActiveScene?.id, - }); - } - - undo(): void { - if (this.savedScenes && this.deletedScene) { - const editor = EditorCore.getInstance(); - editor.scenes.setScenes({ - scenes: this.savedScenes, - activeSceneId: this.savedActiveSceneId ?? undefined, - }); - } - } -} +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { EditorCore } from "@/core"; +import type { TScene } from "@/lib/timeline"; +import { canDeleteScene, getFallbackSceneAfterDelete } from "@/lib/scenes"; + +export class DeleteSceneCommand extends Command { + private savedScenes: TScene[] | null = null; + private savedActiveSceneId: string | null = null; + private deletedScene: TScene | null = null; + + constructor(private sceneId: string) { + super(); + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + const scenes = editor.scenes.getScenes(); + const activeScene = editor.scenes.getActiveScene(); + + this.savedScenes = [...scenes]; + this.savedActiveSceneId = activeScene?.id ?? null; + + this.deletedScene = scenes.find((s) => s.id === this.sceneId) ?? null; + + if (!this.deletedScene) { + console.error("Scene not found:", this.sceneId); + return; + } + + const { canDelete, reason } = canDeleteScene({ scene: this.deletedScene }); + if (!canDelete) { + console.error("Cannot delete scene:", reason); + return; + } + + const updatedScenes = scenes.filter((s) => s.id !== this.sceneId); + + const newActiveScene = getFallbackSceneAfterDelete({ + scenes: updatedScenes, + deletedSceneId: this.sceneId, + currentSceneId: activeScene?.id ?? null, + }); + + editor.scenes.setScenes({ + scenes: updatedScenes, + activeSceneId: newActiveScene?.id, + }); + } + + undo(): void { + if (this.savedScenes && this.deletedScene) { + const editor = EditorCore.getInstance(); + editor.scenes.setScenes({ + scenes: this.savedScenes, + activeSceneId: this.savedActiveSceneId ?? undefined, + }); + } + } +} diff --git a/apps/web/src/lib/commands/scene/index.ts b/apps/web/src/lib/commands/scene/index.ts index b680108d..2b843c99 100644 --- a/apps/web/src/lib/commands/scene/index.ts +++ b/apps/web/src/lib/commands/scene/index.ts @@ -1,7 +1,7 @@ -export { CreateSceneCommand } from "./create-scene"; -export { DeleteSceneCommand } from "./delete-scene"; -export { RenameSceneCommand } from "./rename-scene"; -export { ToggleBookmarkCommand } from "./toggle-bookmark"; -export { RemoveBookmarkCommand } from "./remove-bookmark"; -export { UpdateBookmarkCommand } from "./update-bookmark"; -export { MoveBookmarkCommand } from "./move-bookmark"; +export { CreateSceneCommand } from "./create-scene"; +export { DeleteSceneCommand } from "./delete-scene"; +export { RenameSceneCommand } from "./rename-scene"; +export { ToggleBookmarkCommand } from "./toggle-bookmark"; +export { RemoveBookmarkCommand } from "./remove-bookmark"; +export { UpdateBookmarkCommand } from "./update-bookmark"; +export { MoveBookmarkCommand } from "./move-bookmark"; diff --git a/apps/web/src/lib/commands/scene/move-bookmark.ts b/apps/web/src/lib/commands/scene/move-bookmark.ts index f9b0664c..103caa74 100644 --- a/apps/web/src/lib/commands/scene/move-bookmark.ts +++ b/apps/web/src/lib/commands/scene/move-bookmark.ts @@ -1,59 +1,59 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { EditorCore } from "@/core"; -import type { TScene } from "@/lib/timeline"; -import { updateSceneInArray } from "@/lib/scenes"; -import { getFrameTime, moveBookmarkInArray } from "@/lib/timeline/bookmarks"; - -export class MoveBookmarkCommand extends Command { - private savedScenes: TScene[] | null = null; - - constructor( - private fromTime: number, - private toTime: number, - ) { - super(); - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - const activeScene = editor.scenes.getActiveScene(); - const activeProject = editor.project.getActive(); - - if (!activeScene || !activeProject) { - return; - } - - const scenes = editor.scenes.getScenes(); - this.savedScenes = [...scenes]; - - const fromFrameTime = getFrameTime({ - time: this.fromTime, - fps: activeProject.settings.fps, - }); - const toFrameTime = getFrameTime({ - time: this.toTime, - fps: activeProject.settings.fps, - }); - - const updatedBookmarks = moveBookmarkInArray({ - bookmarks: activeScene.bookmarks, - fromTime: fromFrameTime, - toTime: toFrameTime, - }); - - const updatedScenes = updateSceneInArray({ - scenes, - sceneId: activeScene.id, - updates: { bookmarks: updatedBookmarks }, - }); - - editor.scenes.setScenes({ scenes: updatedScenes }); - } - - undo(): void { - if (this.savedScenes) { - const editor = EditorCore.getInstance(); - editor.scenes.setScenes({ scenes: this.savedScenes }); - } - } -} +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { EditorCore } from "@/core"; +import type { TScene } from "@/lib/timeline"; +import { updateSceneInArray } from "@/lib/scenes"; +import { getFrameTime, moveBookmarkInArray } from "@/lib/timeline/bookmarks"; + +export class MoveBookmarkCommand extends Command { + private savedScenes: TScene[] | null = null; + + constructor( + private fromTime: number, + private toTime: number, + ) { + super(); + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + const activeScene = editor.scenes.getActiveScene(); + const activeProject = editor.project.getActive(); + + if (!activeScene || !activeProject) { + return; + } + + const scenes = editor.scenes.getScenes(); + this.savedScenes = [...scenes]; + + const fromFrameTime = getFrameTime({ + time: this.fromTime, + fps: activeProject.settings.fps, + }); + const toFrameTime = getFrameTime({ + time: this.toTime, + fps: activeProject.settings.fps, + }); + + const updatedBookmarks = moveBookmarkInArray({ + bookmarks: activeScene.bookmarks, + fromTime: fromFrameTime, + toTime: toFrameTime, + }); + + const updatedScenes = updateSceneInArray({ + scenes, + sceneId: activeScene.id, + updates: { bookmarks: updatedBookmarks }, + }); + + editor.scenes.setScenes({ scenes: updatedScenes }); + } + + undo(): void { + if (this.savedScenes) { + const editor = EditorCore.getInstance(); + editor.scenes.setScenes({ scenes: this.savedScenes }); + } + } +} diff --git a/apps/web/src/lib/commands/scene/remove-bookmark.ts b/apps/web/src/lib/commands/scene/remove-bookmark.ts index cee41d36..5eac920f 100644 --- a/apps/web/src/lib/commands/scene/remove-bookmark.ts +++ b/apps/web/src/lib/commands/scene/remove-bookmark.ts @@ -1,59 +1,59 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { EditorCore } from "@/core"; -import type { TScene } from "@/lib/timeline"; -import { updateSceneInArray } from "@/lib/scenes"; -import { - getFrameTime, - removeBookmarkFromArray, -} from "@/lib/timeline/bookmarks"; - -export class RemoveBookmarkCommand extends Command { - private savedScenes: TScene[] | null = null; - private frameTime: number = 0; - - constructor(private time: number) { - super(); - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - const activeScene = editor.scenes.getActiveScene(); - const activeProject = editor.project.getActive(); - - if (!activeScene || !activeProject) { - return; - } - - const scenes = editor.scenes.getScenes(); - this.savedScenes = [...scenes]; - - this.frameTime = getFrameTime({ - time: this.time, - fps: activeProject.settings.fps, - }); - - const updatedBookmarks = removeBookmarkFromArray({ - bookmarks: activeScene.bookmarks, - frameTime: this.frameTime, - }); - - if (updatedBookmarks.length === activeScene.bookmarks.length) { - return; - } - - const updatedScenes = updateSceneInArray({ - scenes, - sceneId: activeScene.id, - updates: { bookmarks: updatedBookmarks }, - }); - - editor.scenes.setScenes({ scenes: updatedScenes }); - } - - undo(): void { - if (this.savedScenes) { - const editor = EditorCore.getInstance(); - editor.scenes.setScenes({ scenes: this.savedScenes }); - } - } -} +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { EditorCore } from "@/core"; +import type { TScene } from "@/lib/timeline"; +import { updateSceneInArray } from "@/lib/scenes"; +import { + getFrameTime, + removeBookmarkFromArray, +} from "@/lib/timeline/bookmarks"; + +export class RemoveBookmarkCommand extends Command { + private savedScenes: TScene[] | null = null; + private frameTime: number = 0; + + constructor(private time: number) { + super(); + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + const activeScene = editor.scenes.getActiveScene(); + const activeProject = editor.project.getActive(); + + if (!activeScene || !activeProject) { + return; + } + + const scenes = editor.scenes.getScenes(); + this.savedScenes = [...scenes]; + + this.frameTime = getFrameTime({ + time: this.time, + fps: activeProject.settings.fps, + }); + + const updatedBookmarks = removeBookmarkFromArray({ + bookmarks: activeScene.bookmarks, + frameTime: this.frameTime, + }); + + if (updatedBookmarks.length === activeScene.bookmarks.length) { + return; + } + + const updatedScenes = updateSceneInArray({ + scenes, + sceneId: activeScene.id, + updates: { bookmarks: updatedBookmarks }, + }); + + editor.scenes.setScenes({ scenes: updatedScenes }); + } + + undo(): void { + if (this.savedScenes) { + const editor = EditorCore.getInstance(); + editor.scenes.setScenes({ scenes: this.savedScenes }); + } + } +} diff --git a/apps/web/src/lib/commands/scene/rename-scene.ts b/apps/web/src/lib/commands/scene/rename-scene.ts index 06d0414f..ccdc85d6 100644 --- a/apps/web/src/lib/commands/scene/rename-scene.ts +++ b/apps/web/src/lib/commands/scene/rename-scene.ts @@ -1,46 +1,46 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { EditorCore } from "@/core"; -import type { TScene } from "@/lib/timeline"; -import { updateSceneInArray } from "@/lib/scenes"; - -export class RenameSceneCommand extends Command { - private savedScenes: TScene[] | null = null; - private previousName: string | null = null; - - constructor( - private sceneId: string, - private newName: string, - ) { - super(); - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - const scenes = editor.scenes.getScenes(); - - this.savedScenes = [...scenes]; - - const scene = scenes.find((s) => s.id === this.sceneId); - if (!scene) { - console.error("Scene not found:", this.sceneId); - return; - } - - this.previousName = scene.name; - - const updatedScenes = updateSceneInArray({ - scenes, - sceneId: this.sceneId, - updates: { name: this.newName, updatedAt: new Date() }, - }); - - editor.scenes.setScenes({ scenes: updatedScenes }); - } - - undo(): void { - if (this.savedScenes && this.previousName !== null) { - const editor = EditorCore.getInstance(); - editor.scenes.setScenes({ scenes: this.savedScenes }); - } - } -} +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { EditorCore } from "@/core"; +import type { TScene } from "@/lib/timeline"; +import { updateSceneInArray } from "@/lib/scenes"; + +export class RenameSceneCommand extends Command { + private savedScenes: TScene[] | null = null; + private previousName: string | null = null; + + constructor( + private sceneId: string, + private newName: string, + ) { + super(); + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + const scenes = editor.scenes.getScenes(); + + this.savedScenes = [...scenes]; + + const scene = scenes.find((s) => s.id === this.sceneId); + if (!scene) { + console.error("Scene not found:", this.sceneId); + return; + } + + this.previousName = scene.name; + + const updatedScenes = updateSceneInArray({ + scenes, + sceneId: this.sceneId, + updates: { name: this.newName, updatedAt: new Date() }, + }); + + editor.scenes.setScenes({ scenes: updatedScenes }); + } + + undo(): void { + if (this.savedScenes && this.previousName !== null) { + const editor = EditorCore.getInstance(); + editor.scenes.setScenes({ scenes: this.savedScenes }); + } + } +} diff --git a/apps/web/src/lib/commands/scene/toggle-bookmark.ts b/apps/web/src/lib/commands/scene/toggle-bookmark.ts index 5df599ad..cb4fa655 100644 --- a/apps/web/src/lib/commands/scene/toggle-bookmark.ts +++ b/apps/web/src/lib/commands/scene/toggle-bookmark.ts @@ -1,52 +1,52 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { EditorCore } from "@/core"; -import type { TScene } from "@/lib/timeline"; -import { updateSceneInArray } from "@/lib/scenes"; -import { getFrameTime, toggleBookmarkInArray } from "@/lib/timeline/bookmarks"; - -export class ToggleBookmarkCommand extends Command { - private savedScenes: TScene[] | null = null; - private frameTime: number = 0; - - constructor(private time: number) { - super(); - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - const activeScene = editor.scenes.getActiveScene(); - const activeProject = editor.project.getActive(); - - if (!activeScene || !activeProject) { - return; - } - - const scenes = editor.scenes.getScenes(); - this.savedScenes = [...scenes]; - - this.frameTime = getFrameTime({ - time: this.time, - fps: activeProject.settings.fps, - }); - - const updatedBookmarks = toggleBookmarkInArray({ - bookmarks: activeScene.bookmarks, - frameTime: this.frameTime, - }); - - const updatedScenes = updateSceneInArray({ - scenes, - sceneId: activeScene.id, - updates: { bookmarks: updatedBookmarks }, - }); - - editor.scenes.setScenes({ scenes: updatedScenes }); - } - - undo(): void { - if (this.savedScenes) { - const editor = EditorCore.getInstance(); - editor.scenes.setScenes({ scenes: this.savedScenes }); - } - } -} +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { EditorCore } from "@/core"; +import type { TScene } from "@/lib/timeline"; +import { updateSceneInArray } from "@/lib/scenes"; +import { getFrameTime, toggleBookmarkInArray } from "@/lib/timeline/bookmarks"; + +export class ToggleBookmarkCommand extends Command { + private savedScenes: TScene[] | null = null; + private frameTime: number = 0; + + constructor(private time: number) { + super(); + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + const activeScene = editor.scenes.getActiveScene(); + const activeProject = editor.project.getActive(); + + if (!activeScene || !activeProject) { + return; + } + + const scenes = editor.scenes.getScenes(); + this.savedScenes = [...scenes]; + + this.frameTime = getFrameTime({ + time: this.time, + fps: activeProject.settings.fps, + }); + + const updatedBookmarks = toggleBookmarkInArray({ + bookmarks: activeScene.bookmarks, + frameTime: this.frameTime, + }); + + const updatedScenes = updateSceneInArray({ + scenes, + sceneId: activeScene.id, + updates: { bookmarks: updatedBookmarks }, + }); + + editor.scenes.setScenes({ scenes: updatedScenes }); + } + + undo(): void { + if (this.savedScenes) { + const editor = EditorCore.getInstance(); + editor.scenes.setScenes({ scenes: this.savedScenes }); + } + } +} diff --git a/apps/web/src/lib/commands/scene/update-bookmark.ts b/apps/web/src/lib/commands/scene/update-bookmark.ts index f383ab5d..1d1c7283 100644 --- a/apps/web/src/lib/commands/scene/update-bookmark.ts +++ b/apps/web/src/lib/commands/scene/update-bookmark.ts @@ -1,55 +1,55 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { EditorCore } from "@/core"; -import type { Bookmark, TScene } from "@/lib/timeline"; -import { updateSceneInArray } from "@/lib/scenes"; -import { getFrameTime, updateBookmarkInArray } from "@/lib/timeline/bookmarks"; - -export class UpdateBookmarkCommand extends Command { - private savedScenes: TScene[] | null = null; - - constructor( - private time: number, - private updates: Partial>, - ) { - super(); - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - const activeScene = editor.scenes.getActiveScene(); - const activeProject = editor.project.getActive(); - - if (!activeScene || !activeProject) { - return; - } - - const scenes = editor.scenes.getScenes(); - this.savedScenes = [...scenes]; - - const frameTime = getFrameTime({ - time: this.time, - fps: activeProject.settings.fps, - }); - - const updatedBookmarks = updateBookmarkInArray({ - bookmarks: activeScene.bookmarks, - frameTime, - updates: this.updates, - }); - - const updatedScenes = updateSceneInArray({ - scenes, - sceneId: activeScene.id, - updates: { bookmarks: updatedBookmarks }, - }); - - editor.scenes.setScenes({ scenes: updatedScenes }); - } - - undo(): void { - if (this.savedScenes) { - const editor = EditorCore.getInstance(); - editor.scenes.setScenes({ scenes: this.savedScenes }); - } - } -} +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { EditorCore } from "@/core"; +import type { Bookmark, TScene } from "@/lib/timeline"; +import { updateSceneInArray } from "@/lib/scenes"; +import { getFrameTime, updateBookmarkInArray } from "@/lib/timeline/bookmarks"; + +export class UpdateBookmarkCommand extends Command { + private savedScenes: TScene[] | null = null; + + constructor( + private time: number, + private updates: Partial>, + ) { + super(); + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + const activeScene = editor.scenes.getActiveScene(); + const activeProject = editor.project.getActive(); + + if (!activeScene || !activeProject) { + return; + } + + const scenes = editor.scenes.getScenes(); + this.savedScenes = [...scenes]; + + const frameTime = getFrameTime({ + time: this.time, + fps: activeProject.settings.fps, + }); + + const updatedBookmarks = updateBookmarkInArray({ + bookmarks: activeScene.bookmarks, + frameTime, + updates: this.updates, + }); + + const updatedScenes = updateSceneInArray({ + scenes, + sceneId: activeScene.id, + updates: { bookmarks: updatedBookmarks }, + }); + + editor.scenes.setScenes({ scenes: updatedScenes }); + } + + undo(): void { + if (this.savedScenes) { + const editor = EditorCore.getInstance(); + editor.scenes.setScenes({ scenes: this.savedScenes }); + } + } +} diff --git a/apps/web/src/lib/commands/timeline/clipboard/paste.ts b/apps/web/src/lib/commands/timeline/clipboard/paste.ts index 9fa4b83b..6ccf96fb 100644 --- a/apps/web/src/lib/commands/timeline/clipboard/paste.ts +++ b/apps/web/src/lib/commands/timeline/clipboard/paste.ts @@ -1,21 +1,16 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { EditorCore } from "@/core"; import type { - TimelineTrack, + SceneTracks, TimelineElement, ClipboardItem, } from "@/lib/timeline"; import { generateUUID } from "@/utils/id"; -import { - applyPlacement, - resolveTrackPlacement, - isMainTrack, - enforceMainTrackStart, -} from "@/lib/timeline/placement"; +import { applyPlacement, resolveTrackPlacement, enforceMainTrackStart } from "@/lib/timeline/placement"; import { cloneAnimations } from "@/lib/animation"; export class PasteCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private pastedElements: { trackId: string; elementId: string }[] = []; private readonly time: number; private readonly clipboardItems: ClipboardItem[]; @@ -36,14 +31,14 @@ export class PasteCommand extends Command { if (this.clipboardItems.length === 0) return undefined; const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; this.pastedElements = []; const minStart = Math.min( ...this.clipboardItems.map((item) => item.element.startTime), ); - let updatedTracks = [...this.savedState]; + let updatedTracks = this.savedState; const itemsByTrackId = groupClipboardItemsByTrackId({ clipboardItems: this.clipboardItems, }); @@ -60,9 +55,11 @@ export class PasteCommand extends Command { } const trackType = items[0].trackType; - const sourceTrackIndex = updatedTracks.findIndex( - (track) => track.id === trackId, - ); + const sourceTrackIndex = [ + ...updatedTracks.overlay, + updatedTracks.main, + ...updatedTracks.audio, + ].findIndex((track) => track.id === trackId); const placementResult = resolveTrackPlacement({ tracks: updatedTracks, trackType, @@ -78,8 +75,15 @@ export class PasteCommand extends Command { let elementsForPlacement = elementsToAdd; if (placementResult.kind === "existingTrack") { - const targetTrack = updatedTracks[placementResult.trackIndex]; - if (isMainTrack(targetTrack)) { + const targetTrack = + placementResult.trackIndex < updatedTracks.overlay.length + ? updatedTracks.overlay[placementResult.trackIndex] + : placementResult.trackIndex === updatedTracks.overlay.length + ? updatedTracks.main + : updatedTracks.audio[ + placementResult.trackIndex - updatedTracks.overlay.length - 1 + ]; + if (targetTrack?.id === updatedTracks.main.id) { const earliestElement = elementsToAdd.reduce((earliest, element) => element.startTime < earliest.startTime ? element : earliest, ); diff --git a/apps/web/src/lib/commands/timeline/element/delete-elements.ts b/apps/web/src/lib/commands/timeline/element/delete-elements.ts index c8cc4494..8d93c6ba 100644 --- a/apps/web/src/lib/commands/timeline/element/delete-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/delete-elements.ts @@ -1,9 +1,28 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks } from "@/lib/timeline"; import { EditorCore } from "@/core"; +import type { TimelineTrack } from "@/lib/timeline"; + +function removeTrackElements({ + track, + elements, +}: { + track: TTrack; + elements: { trackId: string; elementId: string }[]; +}): TTrack { + const nextElements = track.elements.filter( + (element) => + !elements.some( + (target) => + target.trackId === track.id && target.elementId === element.id, + ), + ); + + return { ...track, elements: nextElements } as TTrack; +} export class DeleteElementsCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly elements: { trackId: string; elementId: string }[]; constructor({ @@ -17,28 +36,20 @@ export class DeleteElementsCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); - - const updatedTracks = this.savedState.map((track) => { - const elementsToDeleteOnTrack = this.elements.filter( - (target) => target.trackId === track.id, - ); - - if (elementsToDeleteOnTrack.length === 0) { - return track; - } - - const elements = track.elements.filter( - (element) => - !this.elements.some( - (target) => - target.trackId === track.id && - target.elementId === element.id, - ), - ); - - return { ...track, elements } as typeof track; - }); + this.savedState = editor.scenes.getActiveScene().tracks; + + const updatedTracks: SceneTracks = { + overlay: this.savedState.overlay.map((track) => + removeTrackElements({ track, elements: this.elements }), + ), + main: removeTrackElements({ + track: this.savedState.main, + elements: this.elements, + }), + audio: this.savedState.audio.map((track) => + removeTrackElements({ track, elements: this.elements }), + ), + }; editor.timeline.updateTracks(updatedTracks); diff --git a/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts b/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts index 35be3da2..ab7a4a25 100644 --- a/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts @@ -1,5 +1,5 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { TimelineElement, TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks, TimelineElement } from "@/lib/timeline"; import { generateUUID } from "@/utils/id"; import { EditorCore } from "@/core"; import { applyPlacement, resolveTrackPlacement } from "@/lib/timeline/placement"; @@ -11,7 +11,7 @@ interface DuplicateElementsParams { export class DuplicateElementsCommand extends Command { private duplicatedElements: { trackId: string; elementId: string }[] = []; - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private elements: DuplicateElementsParams["elements"]; constructor({ elements }: DuplicateElementsParams) { @@ -21,12 +21,16 @@ export class DuplicateElementsCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; this.duplicatedElements = []; - let updatedTracks = [...this.savedState]; + let updatedTracks = this.savedState; - for (const track of this.savedState) { + for (const track of [ + ...this.savedState.overlay, + this.savedState.main, + ...this.savedState.audio, + ]) { const elementsToDuplicate = this.elements.filter( (elementEntry) => elementEntry.trackId === track.id, ); diff --git a/apps/web/src/lib/commands/timeline/element/effects/add-effect.ts b/apps/web/src/lib/commands/timeline/element/effects/add-effect.ts index 0c117f3b..902ca341 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/add-effect.ts +++ b/apps/web/src/lib/commands/timeline/element/effects/add-effect.ts @@ -1,7 +1,7 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; -import type { TimelineTrack, VisualElement } from "@/lib/timeline"; +import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; +import type { SceneTracks, VisualElement } from "@/lib/timeline"; import { buildDefaultEffectInstance } from "@/lib/effects"; function addEffectToElement({ @@ -17,7 +17,7 @@ function addEffectToElement({ } export class AddClipEffectCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private effectId: string | null = null; private readonly trackId: string; private readonly elementId: string; @@ -40,9 +40,9 @@ export class AddClipEffectCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/effects/index.ts b/apps/web/src/lib/commands/timeline/element/effects/index.ts index b03c51e6..133ce661 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/index.ts +++ b/apps/web/src/lib/commands/timeline/element/effects/index.ts @@ -1,5 +1,5 @@ -export { AddClipEffectCommand } from "./add-effect"; -export { RemoveClipEffectCommand } from "./remove-effect"; -export { ToggleClipEffectCommand } from "./toggle-effect"; -export { UpdateClipEffectParamsCommand } from "./update-effect-params"; -export { ReorderClipEffectsCommand } from "./reorder-effect"; +export { AddClipEffectCommand } from "./add-effect"; +export { RemoveClipEffectCommand } from "./remove-effect"; +export { ToggleClipEffectCommand } from "./toggle-effect"; +export { UpdateClipEffectParamsCommand } from "./update-effect-params"; +export { ReorderClipEffectsCommand } from "./reorder-effect"; diff --git a/apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts b/apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts index f876a1d1..f453f2cc 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts +++ b/apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts @@ -1,7 +1,7 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; -import type { TimelineTrack, VisualElement } from "@/lib/timeline"; +import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; +import type { SceneTracks, VisualElement } from "@/lib/timeline"; function removeEffectFromElement({ element, @@ -16,7 +16,7 @@ function removeEffectFromElement({ } export class RemoveClipEffectCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly effectId: string; @@ -38,9 +38,9 @@ export class RemoveClipEffectCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts b/apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts index e63430be..28b7c4e1 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts +++ b/apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts @@ -1,7 +1,7 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; -import type { TimelineTrack, VisualElement } from "@/lib/timeline"; +import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; +import type { SceneTracks, VisualElement } from "@/lib/timeline"; function reorderEffectsOnElement({ element, @@ -19,7 +19,7 @@ function reorderEffectsOnElement({ } export class ReorderClipEffectsCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly fromIndex: number; @@ -45,9 +45,9 @@ export class ReorderClipEffectsCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts b/apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts index 0329f1a0..a703cd94 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts +++ b/apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts @@ -1,7 +1,7 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; -import type { TimelineTrack, VisualElement } from "@/lib/timeline"; +import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; +import type { SceneTracks, VisualElement } from "@/lib/timeline"; export function toggleEffectOnElement({ element, @@ -18,7 +18,7 @@ export function toggleEffectOnElement({ } export class ToggleClipEffectCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly effectId: string; @@ -40,9 +40,9 @@ export class ToggleClipEffectCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts b/apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts index 058706ec..5e75c1f0 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts +++ b/apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts @@ -1,8 +1,8 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; +import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; import type { ParamValues } from "@/lib/params"; -import type { TimelineTrack, VisualElement } from "@/lib/timeline"; +import type { SceneTracks, VisualElement } from "@/lib/timeline"; function updateEffectParamsOnElement({ element, @@ -32,7 +32,7 @@ function updateEffectParamsOnElement({ } export class UpdateClipEffectParamsCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly effectId: string; @@ -58,9 +58,9 @@ export class UpdateClipEffectParamsCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/index.ts b/apps/web/src/lib/commands/timeline/element/index.ts index eabdc385..ff365801 100644 --- a/apps/web/src/lib/commands/timeline/element/index.ts +++ b/apps/web/src/lib/commands/timeline/element/index.ts @@ -1,11 +1,11 @@ -export { InsertElementCommand } from "./insert-element"; -export { DeleteElementsCommand } from "./delete-elements"; -export { DuplicateElementsCommand } from "./duplicate-elements"; -export { SplitElementsCommand } from "./split-elements"; -export { UpdateElementsCommand } from "./update-elements"; -export { ToggleSourceAudioSeparationCommand } from "./toggle-source-audio-separation"; -export { MoveElementCommand } from "./move-elements"; - -export * from "./keyframes"; -export * from "./effects"; -export * from "./masks"; +export { InsertElementCommand } from "./insert-element"; +export { DeleteElementsCommand } from "./delete-elements"; +export { DuplicateElementsCommand } from "./duplicate-elements"; +export { SplitElementsCommand } from "./split-elements"; +export { UpdateElementsCommand } from "./update-elements"; +export { ToggleSourceAudioSeparationCommand } from "./toggle-source-audio-separation"; +export { MoveElementCommand } from "./move-elements"; + +export * from "./keyframes"; +export * from "./effects"; +export * from "./masks"; diff --git a/apps/web/src/lib/commands/timeline/element/insert-element.ts b/apps/web/src/lib/commands/timeline/element/insert-element.ts index b06631d8..0ac11ebd 100644 --- a/apps/web/src/lib/commands/timeline/element/insert-element.ts +++ b/apps/web/src/lib/commands/timeline/element/insert-element.ts @@ -2,6 +2,7 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { EditorCore } from "@/core"; import type { CreateTimelineElement, + SceneTracks, TimelineTrack, TimelineElement, TrackType, @@ -30,7 +31,7 @@ export interface InsertElementParams { export class InsertElementCommand extends Command { private elementId: string; - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private targetTrackId: string | null = null; constructor({ element, placement }: InsertElementParams) { @@ -45,21 +46,22 @@ export class InsertElementCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); - - if (!this.savedState) { - console.error("Tracks not available"); - return; - } + this.savedState = editor.scenes.getActiveScene().tracks; if (!this.validateElementBasics({ element: this.element })) { return; } - const totalElementsInTimeline = this.savedState.reduce( - (total, t) => total + t.elements.length, - 0, - ); + const totalElementsInTimeline = + this.savedState.main.elements.length + + this.savedState.overlay.reduce( + (total, track) => total + track.elements.length, + 0, + ) + + this.savedState.audio.reduce( + (total, track) => total + track.elements.length, + 0, + ); const isFirstElement = totalElementsInTimeline === 0; const newElement = this.buildElement({ element: this.element }); @@ -194,9 +196,9 @@ export class InsertElementCommand extends Command { tracks, element, }: { - tracks: TimelineTrack[]; + tracks: SceneTracks; element: TimelineElement; - }): { updatedTracks: TimelineTrack[]; targetTrackId: string } | null { + }): { updatedTracks: SceneTracks; targetTrackId: string } | null { const placement = this.placement; if ( @@ -231,7 +233,11 @@ export class InsertElementCommand extends Command { }); if (!placementResult) { if (placement.mode === "explicit") { - const targetTrack = tracks.find((track) => track.id === placement.trackId); + const targetTrack = + tracks.main.id === placement.trackId + ? tracks.main + : tracks.overlay.find((track) => track.id === placement.trackId) ?? + tracks.audio.find((track) => track.id === placement.trackId); if (!targetTrack) { console.error("Track not found:", placement.trackId); return null; @@ -256,7 +262,7 @@ export class InsertElementCommand extends Command { } : element; - return applyPlacement({ + const appliedPlacement = applyPlacement({ tracks, placementResult, elements: [elementToPlace], @@ -265,5 +271,13 @@ export class InsertElementCommand extends Command { ? placement.insertIndex : undefined, }); + if (!appliedPlacement) { + return null; + } + + return { + updatedTracks: appliedPlacement.updatedTracks, + targetTrackId: appliedPlacement.targetTrackId, + }; } } diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts b/apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts index 36f8a6b1..b1ef4aef 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts +++ b/apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts @@ -1,12 +1,12 @@ import { EditorCore } from "@/core"; import { Command, type CommandResult } from "@/lib/commands/base-command"; import { removeEffectParamKeyframe } from "@/lib/animation/effect-param-channel"; -import { updateElementInTracks } from "@/lib/timeline"; +import { updateElementInSceneTracks } from "@/lib/timeline"; import { isVisualElement } from "@/lib/timeline/element-utils"; -import type { TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks } from "@/lib/timeline"; export class RemoveEffectParamKeyframeCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly effectId: string; @@ -36,9 +36,9 @@ export class RemoveEffectParamKeyframeCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts b/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts index 35aa0860..c757055d 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts +++ b/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts @@ -7,9 +7,9 @@ import { resolveAnimationTarget, } from "@/lib/animation"; import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { updateElementInTracks } from "@/lib/timeline"; +import { updateElementInSceneTracks } from "@/lib/timeline"; import type { AnimationPath, AnimationValue } from "@/lib/animation/types"; -import type { TimelineElement, TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks, TimelineElement } from "@/lib/timeline"; function sampleValueBeforeRemoval({ element, @@ -86,7 +86,7 @@ function removeKeyframeAndPersist({ } export class RemoveKeyframeCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly propertyPath: AnimationPath; @@ -112,9 +112,9 @@ export class RemoveKeyframeCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts b/apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts index 89491823..ad7f5651 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts +++ b/apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts @@ -1,12 +1,12 @@ import { EditorCore } from "@/core"; import { resolveAnimationTarget, retimeElementKeyframe } from "@/lib/animation"; import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { updateElementInTracks } from "@/lib/timeline"; +import { updateElementInSceneTracks } from "@/lib/timeline"; import type { AnimationPath } from "@/lib/animation/types"; -import type { TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks } from "@/lib/timeline"; export class RetimeKeyframeCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly propertyPath: AnimationPath; @@ -36,9 +36,9 @@ export class RetimeKeyframeCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts b/apps/web/src/lib/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts index f1eaab9d..9a4c2304 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts +++ b/apps/web/src/lib/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts @@ -4,15 +4,15 @@ import { updateScalarKeyframeCurve, } from "@/lib/animation"; import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { updateElementInTracks } from "@/lib/timeline"; +import { updateElementInSceneTracks } from "@/lib/timeline"; import type { AnimationPath, ScalarCurveKeyframePatch, } from "@/lib/animation/types"; -import type { TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks } from "@/lib/timeline"; export class UpdateScalarKeyframeCurveCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly propertyPath: AnimationPath; @@ -46,9 +46,9 @@ export class UpdateScalarKeyframeCurveCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts b/apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts index d0f01104..f436721a 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts +++ b/apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts @@ -5,13 +5,13 @@ import { resolveAnimationTarget, upsertPathKeyframe, } from "@/lib/animation"; -import { updateElementInTracks } from "@/lib/timeline"; +import { updateElementInSceneTracks } from "@/lib/timeline"; import { isVisualElement } from "@/lib/timeline/element-utils"; import type { AnimationInterpolation } from "@/lib/animation/types"; -import type { TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks } from "@/lib/timeline"; export class UpsertEffectParamKeyframeCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly effectId: string; @@ -53,9 +53,9 @@ export class UpsertEffectParamKeyframeCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts b/apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts index bd56749a..f77e69b8 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts +++ b/apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts @@ -1,8 +1,8 @@ import { EditorCore } from "@/core"; import { Command, type CommandResult } from "@/lib/commands/base-command"; import { resolveAnimationTarget, upsertPathKeyframe } from "@/lib/animation"; -import { updateElementInTracks } from "@/lib/timeline"; -import type { TimelineTrack } from "@/lib/timeline"; +import { updateElementInSceneTracks } from "@/lib/timeline"; +import type { SceneTracks } from "@/lib/timeline"; import type { AnimationPath, AnimationInterpolation, @@ -10,7 +10,7 @@ import type { } from "@/lib/animation/types"; export class UpsertKeyframeCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly propertyPath: AnimationPath; @@ -48,9 +48,9 @@ export class UpsertKeyframeCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/masks/index.ts b/apps/web/src/lib/commands/timeline/element/masks/index.ts index 5ceec931..acc47184 100644 --- a/apps/web/src/lib/commands/timeline/element/masks/index.ts +++ b/apps/web/src/lib/commands/timeline/element/masks/index.ts @@ -1,2 +1,2 @@ -export { RemoveMaskCommand } from "./remove-mask"; -export { ToggleMaskInvertedCommand } from "./toggle-mask-inverted"; +export { RemoveMaskCommand } from "./remove-mask"; +export { ToggleMaskInvertedCommand } from "./toggle-mask-inverted"; diff --git a/apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts b/apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts index b0daced7..9f6c5f77 100644 --- a/apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts +++ b/apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts @@ -1,7 +1,7 @@ import { EditorCore } from "@/core"; import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { isMaskableElement, updateElementInTracks } from "@/lib/timeline"; -import type { TimelineTrack, MaskableElement } from "@/lib/timeline"; +import { isMaskableElement, updateElementInSceneTracks } from "@/lib/timeline"; +import type { SceneTracks, MaskableElement } from "@/lib/timeline"; function removeMaskFromElement({ element, @@ -16,7 +16,7 @@ function removeMaskFromElement({ } export class RemoveMaskCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly maskId: string; @@ -38,9 +38,9 @@ export class RemoveMaskCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts b/apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts index bc812398..2b5c1694 100644 --- a/apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts +++ b/apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts @@ -1,8 +1,8 @@ import { EditorCore } from "@/core"; import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { isMaskableElement, updateElementInTracks } from "@/lib/timeline"; +import { isMaskableElement, updateElementInSceneTracks } from "@/lib/timeline"; import type { Mask } from "@/lib/masks/types"; -import type { TimelineTrack, MaskableElement } from "@/lib/timeline"; +import type { SceneTracks, MaskableElement } from "@/lib/timeline"; export function toggleMaskInvertedOnElement({ element, @@ -27,7 +27,7 @@ export function toggleMaskInvertedOnElement({ } export class ToggleMaskInvertedCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly maskId: string; @@ -49,9 +49,9 @@ export class ToggleMaskInvertedCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/move-elements.ts b/apps/web/src/lib/commands/timeline/element/move-elements.ts index cf3afa6d..c1fa6897 100644 --- a/apps/web/src/lib/commands/timeline/element/move-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/move-elements.ts @@ -1,18 +1,23 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { EditorCore } from "@/core"; import type { - TimelineTrack, + SceneTracks, TimelineElement, TrackType, + TimelineTrack, } from "@/lib/timeline"; import { buildEmptyTrack, validateElementTrackCompatibility, enforceMainTrackStart, } from "@/lib/timeline/placement"; +import { + findTrackInSceneTracks, + updateTrackInSceneTracks, +} from "@/lib/timeline/track-element-update"; export class MoveElementCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly sourceTrackId: string; private readonly targetTrackId: string; private readonly elementId: string; @@ -42,11 +47,12 @@ export class MoveElementCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const sourceTrack = this.savedState.find( - (track) => track.id === this.sourceTrackId, - ); + const sourceTrack = findTrackInSceneTracks({ + tracks: this.savedState, + trackId: this.sourceTrackId, + }); const element = sourceTrack?.elements.find( (trackElement) => trackElement.id === this.elementId, ); @@ -55,15 +61,21 @@ export class MoveElementCommand extends Command { throw new Error("Source track or element not found"); } - let targetTrack = this.savedState.find((track) => track.id === this.targetTrackId); + let targetTrack = findTrackInSceneTracks({ + tracks: this.savedState, + trackId: this.targetTrackId, + }); let tracksToUpdate = this.savedState; if (!targetTrack && this.createTrack) { const newTrack = buildEmptyTrack({ id: this.targetTrackId, type: this.createTrack.type, }); - tracksToUpdate = [...this.savedState]; - tracksToUpdate.splice(this.createTrack.index, 0, newTrack); + tracksToUpdate = insertTrackAtDisplayIndex({ + tracks: this.savedState, + track: newTrack, + insertIndex: this.createTrack.index, + }); targetTrack = newTrack; } if (!targetTrack) { @@ -94,32 +106,34 @@ export class MoveElementCommand extends Command { const isSameTrack = this.sourceTrackId === this.targetTrackId; - const updatedTracks = tracksToUpdate.map((track): TimelineTrack => { - if (isSameTrack && track.id === this.sourceTrackId) { - return { - ...track, - elements: track.elements.map((trackElement) => - trackElement.id === this.elementId ? movedElement : trackElement, - ), - } as typeof track; - } - - if (track.id === this.sourceTrackId) { - const remainingElements = track.elements.filter( - (trackElement) => trackElement.id !== this.elementId, - ); - return { ...track, elements: remainingElements } as typeof track; - } - - if (track.id === this.targetTrackId) { - return { - ...track, - elements: [...track.elements, movedElement], - } as typeof track; - } - - return track; - }); + const updatedTracks = isSameTrack + ? updateTrackInSceneTracks({ + tracks: tracksToUpdate, + trackId: this.sourceTrackId, + update: (track) => ({ + ...track, + elements: track.elements.map((trackElement) => + trackElement.id === this.elementId ? movedElement : trackElement, + ), + }), + }) + : updateTrackInSceneTracks({ + tracks: updateTrackInSceneTracks({ + tracks: tracksToUpdate, + trackId: this.sourceTrackId, + update: (track) => ({ + ...track, + elements: track.elements.filter( + (trackElement) => trackElement.id !== this.elementId, + ), + }), + }), + trackId: this.targetTrackId, + update: (track) => ({ + ...track, + elements: [...track.elements, movedElement], + }), + }); editor.timeline.updateTracks(updatedTracks); return undefined; @@ -132,3 +146,41 @@ export class MoveElementCommand extends Command { } } } + +function insertTrackAtDisplayIndex({ + tracks, + track, + insertIndex, +}: { + tracks: SceneTracks; + track: TimelineTrack; + insertIndex: number; +}): SceneTracks { + if (track.type === "audio") { + const audioInsertIndex = Math.max( + 0, + Math.min(insertIndex - tracks.overlay.length - 1, tracks.audio.length), + ); + return { + ...tracks, + audio: [ + ...tracks.audio.slice(0, audioInsertIndex), + track, + ...tracks.audio.slice(audioInsertIndex), + ], + }; + } + + const overlayInsertIndex = Math.max( + 0, + Math.min(insertIndex, tracks.overlay.length), + ); + return { + ...tracks, + overlay: [ + ...tracks.overlay.slice(0, overlayInsertIndex), + track, + ...tracks.overlay.slice(overlayInsertIndex), + ], + }; +} diff --git a/apps/web/src/lib/commands/timeline/element/split-elements.ts b/apps/web/src/lib/commands/timeline/element/split-elements.ts index 79722b4c..9799f7e8 100644 --- a/apps/web/src/lib/commands/timeline/element/split-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/split-elements.ts @@ -1,5 +1,5 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks, TimelineElement } from "@/lib/timeline"; import { generateUUID } from "@/utils/id"; import { EditorCore } from "@/core"; import { isRetimableElement } from "@/lib/timeline"; @@ -7,7 +7,7 @@ import { splitAnimationsAtTime } from "@/lib/animation"; import { getSourceSpanAtClipTime } from "@/lib/retime"; export class SplitElementsCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private rightSideElements: { trackId: string; elementId: string }[] = []; private readonly elements: { trackId: string; elementId: string }[]; private readonly splitTime: number; @@ -34,10 +34,12 @@ export class SplitElementsCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; this.rightSideElements = []; - const updatedTracks = this.savedState.map((track) => { + const splitTrack = ( + track: TTrack, + ): TTrack => { const elementsToSplit = this.elements.filter( (target) => target.trackId === track.id, ); @@ -148,8 +150,14 @@ export class SplitElementsCommand extends Command { ]; }); - return { ...track, elements } as typeof track; - }); + return { ...track, elements } as TTrack; + }; + + const updatedTracks: SceneTracks = { + overlay: this.savedState.overlay.map((track) => splitTrack(track)), + main: splitTrack(this.savedState.main), + audio: this.savedState.audio.map((track) => splitTrack(track)), + }; editor.timeline.updateTracks(updatedTracks); diff --git a/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts b/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts index fc195cba..36b3b7a7 100644 --- a/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts +++ b/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts @@ -3,18 +3,20 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { buildSeparatedAudioElement, canExtractSourceAudio, - canRecoverSourceAudio, + isSourceAudioSeparated, } from "@/lib/timeline/audio-separation"; -import { - applyPlacement, - resolveTrackPlacement, -} from "@/lib/timeline/placement"; -import { updateElementInTracks } from "@/lib/timeline/track-element-update"; -import type { TimelineTrack, VideoElement } from "@/lib/timeline/types"; +import { buildEmptyTrack } from "@/lib/timeline/placement"; +import { updateElementInSceneTracks } from "@/lib/timeline/track-element-update"; +import type { + AudioTrack, + SceneTracks, + TimelineElement, + VideoElement, +} from "@/lib/timeline/types"; import { generateUUID } from "@/utils/id"; export class ToggleSourceAudioSeparationCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; constructor( private readonly params: { @@ -27,27 +29,27 @@ export class ToggleSourceAudioSeparationCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const trackIndex = this.savedState.findIndex( + const sourceTrack = [ + ...this.savedState.overlay, + this.savedState.main, + ...this.savedState.audio, + ].find( (track) => track.id === this.params.trackId, ); - if (trackIndex < 0) { + if (!sourceTrack) { return; } - - const sourceTrack = this.savedState[trackIndex]; const sourceElement = sourceTrack.elements.find( (element) => element.id === this.params.elementId, - ); - if (!sourceElement) { - return; - } - if (sourceElement.type !== "video") { + ) as TimelineElement | undefined; + if (!sourceElement || sourceElement.type !== "video") { return; } + const videoElement: VideoElement = sourceElement; - if (canRecoverSourceAudio(sourceElement)) { + if (isSourceAudioSeparated({ element: videoElement })) { editor.timeline.updateTracks( updateSourceAudioEnabled({ tracks: this.savedState, @@ -62,47 +64,34 @@ export class ToggleSourceAudioSeparationCommand extends Command { const mediaAsset = editor .media .getAssets() - .find((asset) => asset.id === sourceElement.mediaId); - if (!canExtractSourceAudio(sourceElement, mediaAsset)) { + .find((asset) => asset.id === videoElement.mediaId); + if (!canExtractSourceAudio(videoElement, mediaAsset)) { return; } - if (sourceElement.duration <= 0) { + if (videoElement.duration <= 0) { return; } const separatedAudioElement = { ...buildSeparatedAudioElement({ - sourceElement, + sourceElement: videoElement, }), id: generateUUID(), }; - const placementResult = resolveTrackPlacement({ - tracks: this.savedState, - trackType: "audio", - timeSpans: [ - { - startTime: separatedAudioElement.startTime, - duration: separatedAudioElement.duration, - }, - ], - strategy: { type: "aboveSource", sourceTrackIndex: trackIndex }, - }); - if (!placementResult) { - return; - } - - const appliedPlacement = applyPlacement({ - tracks: this.savedState, - placementResult, + const newAudioTrack = { + ...buildEmptyTrack({ + id: generateUUID(), + type: "audio", + }), elements: [separatedAudioElement], - }); - if (!appliedPlacement) { - return; - } + } as AudioTrack; editor.timeline.updateTracks( updateSourceAudioEnabled({ - tracks: appliedPlacement.updatedTracks, + tracks: { + ...this.savedState, + audio: [...this.savedState.audio, newAudioTrack], + }, trackId: this.params.trackId, elementId: this.params.elementId, isSourceAudioEnabled: false, @@ -126,12 +115,12 @@ function updateSourceAudioEnabled({ elementId, isSourceAudioEnabled, }: { - tracks: TimelineTrack[]; + tracks: SceneTracks; trackId: string; elementId: string; isSourceAudioEnabled: boolean; -}): TimelineTrack[] { - return updateElementInTracks({ +}): SceneTracks { + return updateElementInSceneTracks({ tracks, trackId, elementId, diff --git a/apps/web/src/lib/commands/timeline/element/update-elements.ts b/apps/web/src/lib/commands/timeline/element/update-elements.ts index abeec842..54d1ca88 100644 --- a/apps/web/src/lib/commands/timeline/element/update-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/update-elements.ts @@ -1,11 +1,14 @@ import { EditorCore } from "@/core"; import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { TimelineElement, TimelineTrack } from "@/lib/timeline"; -import { updateElementInTracks } from "@/lib/timeline"; +import type { SceneTracks, TimelineElement } from "@/lib/timeline"; +import { + findTrackInSceneTracks, + updateElementInSceneTracks, +} from "@/lib/timeline"; import { applyElementUpdate } from "@/lib/timeline/update-pipeline"; export class UpdateElementsCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly updates: Array<{ trackId: string; elementId: string; @@ -27,13 +30,14 @@ export class UpdateElementsCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; let updatedTracks = this.savedState; for (const updateEntry of this.updates) { - const currentTrack = updatedTracks.find( - (track) => track.id === updateEntry.trackId, - ); + const currentTrack = findTrackInSceneTracks({ + tracks: updatedTracks, + trackId: updateEntry.trackId, + }); const currentElement = currentTrack?.elements.find( (element) => element.id === updateEntry.elementId, ); @@ -50,7 +54,7 @@ export class UpdateElementsCommand extends Command { }, }); - updatedTracks = updateElementInTracks({ + updatedTracks = updateElementInSceneTracks({ tracks: updatedTracks, trackId: updateEntry.trackId, elementId: updateEntry.elementId, diff --git a/apps/web/src/lib/commands/timeline/index.ts b/apps/web/src/lib/commands/timeline/index.ts index d0bfd5e9..cb285d94 100644 --- a/apps/web/src/lib/commands/timeline/index.ts +++ b/apps/web/src/lib/commands/timeline/index.ts @@ -1,5 +1,5 @@ -export * from "./track"; -export * from "./element"; -export * from "./clipboard"; - -export { TracksSnapshotCommand } from "./tracks-snapshot"; +export * from "./track"; +export * from "./element"; +export * from "./clipboard"; + +export { TracksSnapshotCommand } from "./tracks-snapshot"; diff --git a/apps/web/src/lib/commands/timeline/track/add-track.ts b/apps/web/src/lib/commands/timeline/track/add-track.ts index 29bdbb0a..66b9c348 100644 --- a/apps/web/src/lib/commands/timeline/track/add-track.ts +++ b/apps/web/src/lib/commands/timeline/track/add-track.ts @@ -1,5 +1,5 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { TrackType, TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks, TrackType } from "@/lib/timeline"; import { generateUUID } from "@/utils/id"; import { EditorCore } from "@/core"; import { @@ -9,7 +9,7 @@ import { export class AddTrackCommand extends Command { private trackId: string; - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; constructor( private type: TrackType, @@ -21,21 +21,28 @@ export class AddTrackCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const newTrack: TimelineTrack = buildEmptyTrack({ - id: this.trackId, - type: this.type, - }); - - const updatedTracks = [...(this.savedState || [])]; const insertIndex = this.index ?? getDefaultInsertIndexForTrack({ - tracks: updatedTracks, + tracks: this.savedState, trackType: this.type, }); - updatedTracks.splice(insertIndex, 0, newTrack); + + const updatedTracks = + this.type === "audio" + ? buildAudioTrackState({ + tracks: this.savedState, + insertIndex, + trackId: this.trackId, + }) + : buildOverlayTrackState({ + tracks: this.savedState, + insertIndex, + trackId: this.trackId, + trackType: this.type, + }); editor.timeline.updateTracks(updatedTracks); return undefined; @@ -52,3 +59,60 @@ export class AddTrackCommand extends Command { return this.trackId; } } + +function buildAudioTrackState({ + tracks, + insertIndex, + trackId, +}: { + tracks: SceneTracks; + insertIndex: number; + trackId: string; +}): SceneTracks { + const audioInsertIndex = Math.max( + 0, + insertIndex - tracks.overlay.length - 1, + ); + const newTrack = buildEmptyTrack({ + id: trackId, + type: "audio", + }); + return { + ...tracks, + audio: [ + ...tracks.audio.slice(0, audioInsertIndex), + newTrack, + ...tracks.audio.slice(audioInsertIndex), + ], + }; +} + +function buildOverlayTrackState({ + tracks, + insertIndex, + trackId, + trackType, +}: { + tracks: SceneTracks; + insertIndex: number; + trackId: string; + trackType: Exclude; +}): SceneTracks { + const overlayInsertIndex = Math.min(insertIndex, tracks.overlay.length); + const newTrack = + trackType === "video" + ? buildEmptyTrack({ id: trackId, type: "video" }) + : trackType === "text" + ? buildEmptyTrack({ id: trackId, type: "text" }) + : trackType === "graphic" + ? buildEmptyTrack({ id: trackId, type: "graphic" }) + : buildEmptyTrack({ id: trackId, type: "effect" }); + return { + ...tracks, + overlay: [ + ...tracks.overlay.slice(0, overlayInsertIndex), + newTrack, + ...tracks.overlay.slice(overlayInsertIndex), + ], + }; +} diff --git a/apps/web/src/lib/commands/timeline/track/remove-track.ts b/apps/web/src/lib/commands/timeline/track/remove-track.ts index c2ea7ef0..5fcaa898 100644 --- a/apps/web/src/lib/commands/timeline/track/remove-track.ts +++ b/apps/web/src/lib/commands/timeline/track/remove-track.ts @@ -1,35 +1,30 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { EditorCore } from "@/core"; -import type { TimelineTrack } from "@/lib/timeline"; -import { getMainTrack } from "@/lib/timeline/placement"; - -export class RemoveTrackCommand extends Command { - private savedState: TimelineTrack[] | null = null; - - constructor(private trackId: string) { - super(); - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); - const targetTrack = this.savedState.find( - (track) => track.id === this.trackId, - ); - const mainTrack = getMainTrack({ tracks: this.savedState }); - if (mainTrack?.id === targetTrack?.id) { - return; - } - const updatedTracks = this.savedState.filter( - (track) => track.id !== this.trackId, - ); - editor.timeline.updateTracks(updatedTracks); - } - - undo(): void { - if (this.savedState) { - const editor = EditorCore.getInstance(); - editor.timeline.updateTracks(this.savedState); - } - } -} +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { EditorCore } from "@/core"; +import type { SceneTracks } from "@/lib/timeline"; + +export class RemoveTrackCommand extends Command { + private savedState: SceneTracks | null = null; + + constructor(private trackId: string) { + super(); + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.scenes.getActiveScene().tracks; + const updatedTracks: SceneTracks = { + ...this.savedState, + overlay: this.savedState.overlay.filter((track) => track.id !== this.trackId), + audio: this.savedState.audio.filter((track) => track.id !== this.trackId), + }; + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } +} diff --git a/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts b/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts index 513fffc7..7ffe2fd7 100644 --- a/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts +++ b/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts @@ -1,39 +1,41 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { TimelineTrack } from "@/lib/timeline"; -import { EditorCore } from "@/core"; -import { canTrackHaveAudio } from "@/lib/timeline"; - -export class ToggleTrackMuteCommand extends Command { - private savedState: TimelineTrack[] | null = null; - - constructor(private trackId: string) { - super(); - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); - - const targetTrack = this.savedState.find( - (track) => track.id === this.trackId, - ); - if (!targetTrack) { - return; - } - - const updatedTracks = this.savedState.map((track) => - track.id === this.trackId && canTrackHaveAudio(track) - ? { ...track, muted: !track.muted } - : track, - ); - - editor.timeline.updateTracks(updatedTracks); - } - - undo(): void { - if (this.savedState) { - const editor = EditorCore.getInstance(); - editor.timeline.updateTracks(this.savedState); - } - } -} +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import type { SceneTracks } from "@/lib/timeline"; +import { EditorCore } from "@/core"; +import { canTrackHaveAudio, findTrackInSceneTracks, updateTrackInSceneTracks } from "@/lib/timeline"; + +export class ToggleTrackMuteCommand extends Command { + private savedState: SceneTracks | null = null; + + constructor(private trackId: string) { + super(); + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.scenes.getActiveScene().tracks; + + const targetTrack = findTrackInSceneTracks({ + tracks: this.savedState, + trackId: this.trackId, + }); + if (!targetTrack) { + return; + } + + const updatedTracks = updateTrackInSceneTracks({ + tracks: this.savedState, + trackId: this.trackId, + update: (track) => + canTrackHaveAudio(track) ? { ...track, muted: !track.muted } : track, + }); + + editor.timeline.updateTracks(updatedTracks); + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } +} diff --git a/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts b/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts index 077c21db..2292c83a 100644 --- a/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts +++ b/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts @@ -1,40 +1,45 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { TimelineTrack } from "@/lib/timeline"; -import { EditorCore } from "@/core"; -import { canTrackBeHidden } from "@/lib/timeline"; - -export class ToggleTrackVisibilityCommand extends Command { - private savedState: TimelineTrack[] | null = null; - - constructor(private trackId: string) { - super(); - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); - - const targetTrack = this.savedState.find( - (track) => track.id === this.trackId, - ); - if (!targetTrack) { - return; - } - - const updatedTracks = this.savedState.map((track) => { - if (track.id === this.trackId && canTrackBeHidden(track)) { - return { ...track, hidden: !track.hidden }; - } - return track; - }); - - editor.timeline.updateTracks(updatedTracks); - } - - undo(): void { - if (this.savedState) { - const editor = EditorCore.getInstance(); - editor.timeline.updateTracks(this.savedState); - } - } -} +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import type { SceneTracks } from "@/lib/timeline"; +import { EditorCore } from "@/core"; +import { canTrackBeHidden, findTrackInSceneTracks, updateTrackInSceneTracks } from "@/lib/timeline"; + +export class ToggleTrackVisibilityCommand extends Command { + private savedState: SceneTracks | null = null; + + constructor(private trackId: string) { + super(); + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.scenes.getActiveScene().tracks; + + const targetTrack = findTrackInSceneTracks({ + tracks: this.savedState, + trackId: this.trackId, + }); + if (!targetTrack) { + return; + } + + const updatedTracks = updateTrackInSceneTracks({ + tracks: this.savedState, + trackId: this.trackId, + update: (track) => { + if (canTrackBeHidden(track)) { + return { ...track, hidden: !track.hidden }; + } + return track; + }, + }); + + editor.timeline.updateTracks(updatedTracks); + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } +} diff --git a/apps/web/src/lib/commands/timeline/tracks-snapshot.ts b/apps/web/src/lib/commands/timeline/tracks-snapshot.ts index 2493f19b..daed6a9e 100644 --- a/apps/web/src/lib/commands/timeline/tracks-snapshot.ts +++ b/apps/web/src/lib/commands/timeline/tracks-snapshot.ts @@ -1,11 +1,11 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks } from "@/lib/timeline"; import { EditorCore } from "@/core"; export class TracksSnapshotCommand extends Command { constructor( - private before: TimelineTrack[], - private after: TimelineTrack[], + private before: SceneTracks, + private after: SceneTracks, ) { super(); } -- cgit v1.3.1