diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
| commit | 3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch) | |
| tree | 57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/lib/commands | |
| parent | 729d10592f4e802f91c555c309ff3ba4b119f843 (diff) | |
| download | opencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz | |
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/lib/commands')
49 files changed, 0 insertions, 3128 deletions
diff --git a/apps/web/src/lib/commands/base-command.ts b/apps/web/src/lib/commands/base-command.ts deleted file mode 100644 index b244f2f3..00000000 --- a/apps/web/src/lib/commands/base-command.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { EditorSelectionPatch } from "@/lib/selection/editor-selection"; -import type { ElementRef } from "@/lib/timeline/types"; - -export interface CommandResult { - selection?: EditorSelectionPatch; -} - -export function createElementSelectionResult( - selectedElements: ElementRef[], -): CommandResult { - return { - selection: { - selectedElements, - selectedKeyframes: [], - keyframeSelectionAnchor: null, - selectedMaskPoints: null, - }, - }; -} - -export abstract class Command { - abstract execute(): CommandResult | undefined; - - undo(): void { - throw new Error("Undo not implemented for this command"); - } - - redo(): CommandResult | undefined { - return this.execute(); - } -} diff --git a/apps/web/src/lib/commands/batch-command.ts b/apps/web/src/lib/commands/batch-command.ts deleted file mode 100644 index c4ee7e0b..00000000 --- a/apps/web/src/lib/commands/batch-command.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Command, type CommandResult } from "./base-command"; - -export class BatchCommand extends Command { - constructor(private commands: Command[]) { - super(); - } - - execute(): CommandResult | undefined { - let latestSelectionResult: CommandResult | undefined; - - for (const command of this.commands) { - const result = command.execute(); - if (result?.selection !== undefined) { - latestSelectionResult = result; - } - } - - return latestSelectionResult; - } - - undo(): void { - for (const command of [...this.commands].reverse()) { - command.undo(); - } - } - - redo(): CommandResult | undefined { - let latestSelectionResult: CommandResult | undefined; - - for (const command of this.commands) { - const result = command.redo(); - if (result?.selection !== undefined) { - latestSelectionResult = result; - } - } - - return latestSelectionResult; - } -} diff --git a/apps/web/src/lib/commands/index.ts b/apps/web/src/lib/commands/index.ts deleted file mode 100644 index 78927ca9..00000000 --- a/apps/web/src/lib/commands/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { Command } from "./base-command"; -export type { CommandResult } from "./base-command"; -export { BatchCommand } from "./batch-command"; - -export * from "./timeline"; -export * from "./media"; -export * from "./scene"; -export * from "./project"; diff --git a/apps/web/src/lib/commands/media/add-media-asset.ts b/apps/web/src/lib/commands/media/add-media-asset.ts deleted file mode 100644 index 12e0906e..00000000 --- a/apps/web/src/lib/commands/media/add-media-asset.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { EditorCore } from "@/core"; -import { toast } from "sonner"; -import type { MediaAsset } from "@/lib/media/types"; -import { generateUUID } from "@/utils/id"; -import { storageService } from "@/services/storage/service"; -import type { FrameRate } from "opencut-wasm"; -import { hasMediaId } from "@/lib/timeline/element-utils"; -import { frameRatesEqual, getHighestImportedVideoFps } from "@/lib/fps/utils"; -import { UpdateProjectSettingsCommand } from "@/lib/commands/project"; - -export class AddMediaAssetCommand extends Command { - private assetId: string; - private savedAssets: MediaAsset[] | null = null; - private createdAsset: MediaAsset | null = null; - private previousProjectFps: FrameRate | null = null; - private appliedProjectFps: FrameRate | null = null; - - constructor( - private projectId: string, - private asset: Omit<MediaAsset, "id">, - ) { - super(); - this.assetId = generateUUID(); - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedAssets = [...editor.media.getAssets()]; - - this.createdAsset = { - ...this.asset, - id: this.assetId, - }; - - editor.media.setAssets({ - assets: [...this.savedAssets, this.createdAsset], - }); - this.previousProjectFps = editor.project.getActiveOrNull()?.settings.fps ?? null; - this.appliedProjectFps = editor.project.ratchetFpsForImportedMedia({ - importedAssets: [this.createdAsset], - }); - - storageService - .saveMediaAsset({ - projectId: this.projectId, - mediaAsset: this.createdAsset, - }) - .catch((error) => { - console.error("Failed to save media item:", error); - - const currentAssets = editor.media.getAssets(); - editor.media.setAssets({ - assets: currentAssets.filter((asset) => asset.id !== this.assetId), - }); - - const currentTracks = editor.scenes.getActiveScene().tracks; - const orphanedElements: Array<{ trackId: string; elementId: string }> = - []; - - 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({ - trackId: track.id, - elementId: element.id, - }); - } - } - } - - if (orphanedElements.length > 0) { - editor.timeline.deleteElements({ elements: orphanedElements }); - } - - this.restoreProjectFpsAfterFailedSave({ editor }); - - if (storageService.isQuotaExceededError({ error })) { - toast.error("Not enough browser storage", { - description: error instanceof Error ? error.message : undefined, - }); - } - }); - - return undefined; - } - - undo(): void { - if (this.savedAssets) { - const editor = EditorCore.getInstance(); - editor.media.setAssets({ assets: this.savedAssets }); - - if (this.createdAsset) { - storageService - .deleteMediaAsset({ projectId: this.projectId, id: this.assetId }) - .catch((error) => { - console.error("Failed to delete media item on undo:", error); - }); - } - } - } - - getAssetId(): string { - return this.assetId; - } - - private restoreProjectFpsAfterFailedSave({ - editor, - }: { - editor: EditorCore; - }): void { - if (this.previousProjectFps === null || this.appliedProjectFps === null) return; - - const activeProject = editor.project.getActiveOrNull(); - if (!activeProject) return; - if (!this.appliedProjectFps || !frameRatesEqual(activeProject.settings.fps, this.appliedProjectFps)) return; - - const highestRemainingVideoFps = getHighestImportedVideoFps({ - mediaAssets: editor.media.getAssets(), - }); - const appliedFpsFloat = this.appliedProjectFps.numerator / this.appliedProjectFps.denominator; - if ( - highestRemainingVideoFps !== null && - highestRemainingVideoFps >= appliedFpsFloat - ) { - return; - } - - new UpdateProjectSettingsCommand({ fps: this.previousProjectFps }).execute(); - } -} diff --git a/apps/web/src/lib/commands/media/index.ts b/apps/web/src/lib/commands/media/index.ts deleted file mode 100644 index 466222f2..00000000 --- a/apps/web/src/lib/commands/media/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { AddMediaAssetCommand } from "./add-media-asset"; -export { RemoveMediaAssetCommand } from "./remove-media-asset"; diff --git a/apps/web/src/lib/commands/media/remove-media-asset.ts b/apps/web/src/lib/commands/media/remove-media-asset.ts deleted file mode 100644 index 85d187d9..00000000 --- a/apps/web/src/lib/commands/media/remove-media-asset.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { EditorCore } from "@/core"; -import type { MediaAsset } from "@/lib/media/types"; -import { buildWaveformSourceKey } from "@/lib/media/waveform-summary"; -import { storageService } from "@/services/storage/service"; -import { videoCache } from "@/services/video-cache/service"; -import { waveformCache } from "@/services/waveform-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 }); - waveformCache.clearSource({ - sourceKey: buildWaveformSourceKey({ - kind: "media", - id: 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 deleted file mode 100644 index 68c8dee3..00000000 --- a/apps/web/src/lib/commands/preview-tracker.ts +++ /dev/null @@ -1,23 +0,0 @@ -export class PreviewTracker<T> { - 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/index.ts b/apps/web/src/lib/commands/project/index.ts deleted file mode 100644 index a9f26edc..00000000 --- a/apps/web/src/lib/commands/project/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./update-project-settings"; diff --git a/apps/web/src/lib/commands/project/update-project-settings.ts b/apps/web/src/lib/commands/project/update-project-settings.ts deleted file mode 100644 index 32258e12..00000000 --- a/apps/web/src/lib/commands/project/update-project-settings.ts +++ /dev/null @@ -1,46 +0,0 @@ -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<TProjectSettings>) { - 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/create-scene.ts b/apps/web/src/lib/commands/scene/create-scene.ts deleted file mode 100644 index 1cc72c0f..00000000 --- a/apps/web/src/lib/commands/scene/create-scene.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { EditorCore } from "@/core"; -import type { TScene } from "@/lib/timeline"; -import { buildDefaultScene } from "@/lib/scenes"; - -export class CreateSceneCommand extends Command { - private savedScenes: TScene[] | null = null; - private createdScene: TScene | null = null; - - constructor( - private name: string, - private isMain: boolean = false, - ) { - super(); - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedScenes = [...editor.scenes.getScenes()]; - - this.createdScene = buildDefaultScene({ - name: this.name, - isMain: this.isMain, - }); - - const updatedScenes = [...this.savedScenes, this.createdScene]; - editor.scenes.setScenes({ scenes: updatedScenes }); - return undefined; - } - - undo(): void { - if (this.savedScenes) { - const editor = EditorCore.getInstance(); - editor.scenes.setScenes({ scenes: this.savedScenes }); - } - } - - getSceneId(): string { - return this.createdScene?.id ?? ""; - } -} diff --git a/apps/web/src/lib/commands/scene/delete-scene.ts b/apps/web/src/lib/commands/scene/delete-scene.ts deleted file mode 100644 index ab269b12..00000000 --- a/apps/web/src/lib/commands/scene/delete-scene.ts +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index 2b843c99..00000000 --- a/apps/web/src/lib/commands/scene/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 103caa74..00000000 --- a/apps/web/src/lib/commands/scene/move-bookmark.ts +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index 5eac920f..00000000 --- a/apps/web/src/lib/commands/scene/remove-bookmark.ts +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index ccdc85d6..00000000 --- a/apps/web/src/lib/commands/scene/rename-scene.ts +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index cb4fa655..00000000 --- a/apps/web/src/lib/commands/scene/toggle-bookmark.ts +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index 1d1c7283..00000000 --- a/apps/web/src/lib/commands/scene/update-bookmark.ts +++ /dev/null @@ -1,55 +0,0 @@ -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<Omit<Bookmark, "time">>, - ) { - 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/index.ts b/apps/web/src/lib/commands/timeline/clipboard/index.ts deleted file mode 100644 index e893b62b..00000000 --- a/apps/web/src/lib/commands/timeline/clipboard/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { PasteCommand } from "./paste"; -export { PasteKeyframesCommand } from "./paste-keyframes"; diff --git a/apps/web/src/lib/commands/timeline/clipboard/paste-keyframes.ts b/apps/web/src/lib/commands/timeline/clipboard/paste-keyframes.ts deleted file mode 100644 index 210afbd3..00000000 --- a/apps/web/src/lib/commands/timeline/clipboard/paste-keyframes.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { EditorCore } from "@/core"; -import { - getKeyframeAtTime, - resolveAnimationTarget, - updateScalarKeyframeCurve, - upsertPathKeyframe, -} from "@/lib/animation"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { KeyframeClipboardItem } from "@/lib/clipboard"; -import type { SceneTracks, TimelineElement } from "@/lib/timeline"; -import { updateElementInSceneTracks } from "@/lib/timeline"; -import { generateUUID } from "@/utils/id"; - -function pasteKeyframesIntoElement({ - element, - time, - clipboardItems, -}: { - element: TimelineElement; - time: number; - clipboardItems: KeyframeClipboardItem[]; -}): TimelineElement { - let nextElement = element; - - for (const item of clipboardItems) { - const target = resolveAnimationTarget({ - element: nextElement, - path: item.propertyPath, - }); - if (!target) { - continue; - } - - const keyframeTime = Math.max( - 0, - Math.min(time + item.timeOffset, nextElement.duration), - ); - const nextAnimations = upsertPathKeyframe({ - animations: nextElement.animations, - propertyPath: item.propertyPath, - time: keyframeTime, - value: item.value, - interpolation: item.interpolation, - keyframeId: generateUUID(), - kind: target.kind, - defaultInterpolation: target.defaultInterpolation, - coerceValue: target.coerceValue, - }); - const pastedKeyframe = getKeyframeAtTime({ - animations: nextAnimations, - propertyPath: item.propertyPath, - time: keyframeTime, - }); - - let patchedAnimations = nextAnimations; - if (pastedKeyframe) { - for (const curvePatch of item.curvePatches) { - const nextPatchedAnimations = updateScalarKeyframeCurve({ - animations: patchedAnimations, - propertyPath: item.propertyPath, - componentKey: curvePatch.componentKey, - keyframeId: pastedKeyframe.id, - patch: curvePatch.patch, - }); - patchedAnimations = nextPatchedAnimations ?? patchedAnimations; - } - } - - nextElement = { - ...nextElement, - animations: patchedAnimations, - }; - } - - return nextElement; -} - -export class PasteKeyframesCommand extends Command { - private savedState: SceneTracks | null = null; - private readonly trackId: string; - private readonly elementId: string; - private readonly time: number; - private readonly clipboardItems: KeyframeClipboardItem[]; - - constructor({ - trackId, - elementId, - time, - clipboardItems, - }: { - trackId: string; - elementId: string; - time: number; - clipboardItems: KeyframeClipboardItem[]; - }) { - super(); - this.trackId = trackId; - this.elementId = elementId; - this.time = time; - this.clipboardItems = clipboardItems; - } - - execute(): CommandResult | undefined { - if (this.clipboardItems.length === 0) { - return undefined; - } - - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - const updatedTracks = updateElementInSceneTracks({ - tracks: this.savedState, - trackId: this.trackId, - elementId: this.elementId, - update: (element) => - pasteKeyframesIntoElement({ - element, - time: this.time, - clipboardItems: this.clipboardItems, - }), - }); - - editor.timeline.updateTracks(updatedTracks); - return undefined; - } - - undo(): void { - if (!this.savedState) { - return; - } - - const editor = EditorCore.getInstance(); - editor.timeline.updateTracks(this.savedState); - } -} diff --git a/apps/web/src/lib/commands/timeline/element/delete-elements.ts b/apps/web/src/lib/commands/timeline/element/delete-elements.ts deleted file mode 100644 index 0fc7948f..00000000 --- a/apps/web/src/lib/commands/timeline/element/delete-elements.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { SceneTracks } from "@/lib/timeline"; -import { EditorCore } from "@/core"; -import type { TimelineTrack } from "@/lib/timeline"; - -function removeTrackElements<TTrack extends TimelineTrack>({ - 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: SceneTracks | null = null; - private readonly elements: { trackId: string; elementId: string }[]; - - constructor({ - elements, - }: { - elements: { trackId: string; elementId: string }[]; - }) { - super(); - this.elements = elements; - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - 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); - - return { - selection: { - selectedElements: [], - selectedKeyframes: [], - keyframeSelectionAnchor: null, - selectedMaskPoints: null, - }, - }; - } - - undo(): void { - if (this.savedState) { - const editor = EditorCore.getInstance(); - editor.timeline.updateTracks(this.savedState); - } - } -} 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 deleted file mode 100644 index 902ca341..00000000 --- a/apps/web/src/lib/commands/timeline/element/effects/add-effect.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; -import type { SceneTracks, VisualElement } from "@/lib/timeline"; -import { buildDefaultEffectInstance } from "@/lib/effects"; - -function addEffectToElement({ - element, - effectType, -}: { - element: VisualElement; - effectType: string; -}): VisualElement { - const instance = buildDefaultEffectInstance({ effectType }); - const currentEffects = element.effects ?? []; - return { ...element, effects: [...currentEffects, instance] }; -} - -export class AddClipEffectCommand extends Command { - private savedState: SceneTracks | null = null; - private effectId: string | null = null; - private readonly trackId: string; - private readonly elementId: string; - private readonly effectType: string; - - constructor({ - trackId, - elementId, - effectType, - }: { - trackId: string; - elementId: string; - effectType: string; - }) { - super(); - this.trackId = trackId; - this.elementId = elementId; - this.effectType = effectType; - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - const updatedTracks = updateElementInSceneTracks({ - tracks: this.savedState, - trackId: this.trackId, - elementId: this.elementId, - elementPredicate: isVisualElement, - update: (element) => { - const updated = addEffectToElement({ - element: element as VisualElement, - effectType: this.effectType, - }); - const effects = updated.effects ?? []; - this.effectId = effects[effects.length - 1]?.id ?? null; - return updated; - }, - }); - - editor.timeline.updateTracks(updatedTracks); - return undefined; - } - - undo(): void { - if (this.savedState) { - const editor = EditorCore.getInstance(); - editor.timeline.updateTracks(this.savedState); - } - } - - getEffectId(): string | null { - return this.effectId; - } -} diff --git a/apps/web/src/lib/commands/timeline/element/effects/index.ts b/apps/web/src/lib/commands/timeline/element/effects/index.ts deleted file mode 100644 index 133ce661..00000000 --- a/apps/web/src/lib/commands/timeline/element/effects/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index f453f2cc..00000000 --- a/apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; -import type { SceneTracks, VisualElement } from "@/lib/timeline"; - -function removeEffectFromElement({ - element, - effectId, -}: { - element: VisualElement; - effectId: string; -}): VisualElement { - const currentEffects = element.effects ?? []; - const filtered = currentEffects.filter((effect) => effect.id !== effectId); - return { ...element, effects: filtered }; -} - -export class RemoveClipEffectCommand extends Command { - private savedState: SceneTracks | null = null; - private readonly trackId: string; - private readonly elementId: string; - private readonly effectId: string; - - constructor({ - trackId, - elementId, - effectId, - }: { - trackId: string; - elementId: string; - effectId: string; - }) { - super(); - this.trackId = trackId; - this.elementId = elementId; - this.effectId = effectId; - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - const updatedTracks = updateElementInSceneTracks({ - tracks: this.savedState, - trackId: this.trackId, - elementId: this.elementId, - elementPredicate: isVisualElement, - update: (element) => { - return removeEffectFromElement({ - element: element as VisualElement, - effectId: this.effectId, - }); - }, - }); - - 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/element/effects/reorder-effect.ts b/apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts deleted file mode 100644 index 28b7c4e1..00000000 --- a/apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; -import type { SceneTracks, VisualElement } from "@/lib/timeline"; - -function reorderEffectsOnElement({ - element, - fromIndex, - toIndex, -}: { - element: VisualElement; - fromIndex: number; - toIndex: number; -}): VisualElement { - const effects = [...(element.effects ?? [])]; - const [moved] = effects.splice(fromIndex, 1); - effects.splice(toIndex, 0, moved); - return { ...element, effects }; -} - -export class ReorderClipEffectsCommand extends Command { - private savedState: SceneTracks | null = null; - private readonly trackId: string; - private readonly elementId: string; - private readonly fromIndex: number; - private readonly toIndex: number; - - constructor({ - trackId, - elementId, - fromIndex, - toIndex, - }: { - trackId: string; - elementId: string; - fromIndex: number; - toIndex: number; - }) { - super(); - this.trackId = trackId; - this.elementId = elementId; - this.fromIndex = fromIndex; - this.toIndex = toIndex; - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - const updatedTracks = updateElementInSceneTracks({ - tracks: this.savedState, - trackId: this.trackId, - elementId: this.elementId, - elementPredicate: isVisualElement, - update: (element) => { - return reorderEffectsOnElement({ - element: element as VisualElement, - fromIndex: this.fromIndex, - toIndex: this.toIndex, - }); - }, - }); - - 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/element/effects/toggle-effect.ts b/apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts deleted file mode 100644 index a703cd94..00000000 --- a/apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; -import type { SceneTracks, VisualElement } from "@/lib/timeline"; - -export function toggleEffectOnElement({ - element, - effectId, -}: { - element: VisualElement; - effectId: string; -}): VisualElement { - const currentEffects = element.effects ?? []; - const updated = currentEffects.map((effect) => - effect.id === effectId ? { ...effect, enabled: !effect.enabled } : effect, - ); - return { ...element, effects: updated }; -} - -export class ToggleClipEffectCommand extends Command { - private savedState: SceneTracks | null = null; - private readonly trackId: string; - private readonly elementId: string; - private readonly effectId: string; - - constructor({ - trackId, - elementId, - effectId, - }: { - trackId: string; - elementId: string; - effectId: string; - }) { - super(); - this.trackId = trackId; - this.elementId = elementId; - this.effectId = effectId; - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - const updatedTracks = updateElementInSceneTracks({ - tracks: this.savedState, - trackId: this.trackId, - elementId: this.elementId, - elementPredicate: isVisualElement, - update: (element) => { - return toggleEffectOnElement({ - element: element as VisualElement, - effectId: this.effectId, - }); - }, - }); - - 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/element/effects/update-effect-params.ts b/apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts deleted file mode 100644 index 5e75c1f0..00000000 --- a/apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; -import type { ParamValues } from "@/lib/params"; -import type { SceneTracks, VisualElement } from "@/lib/timeline"; - -function updateEffectParamsOnElement({ - element, - effectId, - params, -}: { - element: VisualElement; - effectId: string; - params: Partial<ParamValues>; -}): VisualElement { - const currentEffects = element.effects ?? []; - const updated = currentEffects.map((effect) => { - if (effect.id !== effectId) { - return effect; - } - - const nextParams = { ...effect.params }; - for (const [key, value] of Object.entries(params)) { - if (value !== undefined) { - nextParams[key] = value; - } - } - - return { ...effect, params: nextParams }; - }); - return { ...element, effects: updated }; -} - -export class UpdateClipEffectParamsCommand extends Command { - private savedState: SceneTracks | null = null; - private readonly trackId: string; - private readonly elementId: string; - private readonly effectId: string; - private readonly params: Partial<ParamValues>; - - constructor({ - trackId, - elementId, - effectId, - params, - }: { - trackId: string; - elementId: string; - effectId: string; - params: Partial<ParamValues>; - }) { - super(); - this.trackId = trackId; - this.elementId = elementId; - this.effectId = effectId; - this.params = params; - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - const updatedTracks = updateElementInSceneTracks({ - tracks: this.savedState, - trackId: this.trackId, - elementId: this.elementId, - elementPredicate: isVisualElement, - update: (element) => { - return updateEffectParamsOnElement({ - element: element as VisualElement, - effectId: this.effectId, - params: this.params, - }); - }, - }); - - 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/element/index.ts b/apps/web/src/lib/commands/timeline/element/index.ts deleted file mode 100644 index ff365801..00000000 --- a/apps/web/src/lib/commands/timeline/element/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index ed05b8c6..00000000 --- a/apps/web/src/lib/commands/timeline/element/insert-element.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { EditorCore } from "@/core"; -import type { - CreateTimelineElement, - SceneTracks, - TimelineElement, - TrackType, -} from "@/lib/timeline"; -import { generateUUID } from "@/utils/id"; -import { requiresMediaId } from "@/lib/timeline/element-utils"; -import type { MediaAsset } from "@/lib/media/types"; -import { DEFAULT_NEW_ELEMENT_DURATION } from "@/lib/timeline/creation"; -import { floatToFrameRate } from "@/lib/fps/utils"; -import { graphicsRegistry, registerDefaultGraphics } from "@/lib/graphics"; -import { - applyPlacement, - canElementGoOnTrack, - resolveTrackPlacement, - validateElementTrackCompatibility, -} from "@/lib/timeline/placement"; - -type InsertElementPlacement = - | { mode: "explicit"; trackId: string } - | { mode: "auto"; trackType?: TrackType; insertIndex?: number }; - -export interface InsertElementParams { - element: CreateTimelineElement; - placement: InsertElementPlacement; -} - -export class InsertElementCommand extends Command { - private elementId: string; - private savedState: SceneTracks | null = null; - private targetTrackId: string | null = null; - - constructor({ element, placement }: InsertElementParams) { - super(); - this.elementId = generateUUID(); - this.element = element; - this.placement = placement; - } - - private element: CreateTimelineElement; - private placement: InsertElementPlacement; - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - if (!this.validateElementBasics({ element: this.element })) { - return; - } - - 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 }); - const updateResult = this.applyPlacementResult({ - tracks: this.savedState, - element: newElement, - }); - - if (!updateResult) { - return; - } - - const { updatedTracks, targetTrackId } = updateResult; - this.targetTrackId = targetTrackId; - - const isVisualMedia = - newElement.type === "video" || newElement.type === "image"; - - if (isFirstElement && isVisualMedia) { - const mediaAssets = editor.media.getAssets(); - const activeProject = editor.project.getActive(); - const asset = mediaAssets.find( - (item: MediaAsset) => item.id === newElement.mediaId, - ); - - if (asset?.width && asset?.height) { - const nextCanvasSize = { width: asset.width, height: asset.height }; - const shouldSetOriginalCanvasSize = - !activeProject?.settings.originalCanvasSize; - editor.project.updateSettings({ - settings: { - canvasSize: nextCanvasSize, - ...(shouldSetOriginalCanvasSize - ? { originalCanvasSize: nextCanvasSize } - : {}), - }, - pushHistory: false, - }); - } - - if (asset?.type === "video" && asset?.fps) { - editor.project.updateSettings({ - settings: { fps: floatToFrameRate(asset.fps) }, - pushHistory: false, - }); - } - } - - editor.timeline.updateTracks(updatedTracks); - - return { - selection: { - selectedElements: [ - { trackId: targetTrackId, elementId: this.elementId }, - ], - selectedKeyframes: [], - keyframeSelectionAnchor: null, - selectedMaskPoints: null, - }, - }; - } - - undo(): void { - if (this.savedState) { - const editor = EditorCore.getInstance(); - editor.timeline.updateTracks(this.savedState); - } - } - - getElementId(): string { - return this.elementId; - } - - getTrackId(): string | null { - return this.targetTrackId; - } - - private buildElement({ - element, - }: { - element: CreateTimelineElement; - }): TimelineElement { - return { - ...element, - id: this.elementId, - startTime: element.startTime, - trimStart: element.trimStart ?? 0, - trimEnd: element.trimEnd ?? 0, - duration: element.duration ?? DEFAULT_NEW_ELEMENT_DURATION, - } as TimelineElement; - } - - private validateElementBasics({ - element, - }: { - element: CreateTimelineElement; - }): boolean { - if (requiresMediaId({ element }) && !("mediaId" in element)) { - console.error("Element requires mediaId"); - return false; - } - - if ( - element.type === "audio" && - element.sourceType === "library" && - !element.sourceUrl - ) { - console.error("Library audio element must have sourceUrl"); - return false; - } - - if (element.type === "sticker" && !element.stickerId) { - console.error("Sticker element must have stickerId"); - return false; - } - - if (element.type === "graphic") { - registerDefaultGraphics(); - if ( - !element.definitionId || - !graphicsRegistry.has(element.definitionId) - ) { - console.error("Graphic element must have a valid definitionId"); - return false; - } - } - - if (element.type === "text" && !element.content) { - console.error("Text element must have content"); - return false; - } - - if (element.type === "effect" && !element.effectType) { - console.error("Effect element must have effectType"); - return false; - } - - return true; - } - - private applyPlacementResult({ - tracks, - element, - }: { - tracks: SceneTracks; - element: TimelineElement; - }): { updatedTracks: SceneTracks; targetTrackId: string } | null { - const placement = this.placement; - - if ( - placement.mode === "auto" && - placement.trackType && - !canElementGoOnTrack({ - elementType: element.type, - trackType: placement.trackType, - }) - ) { - console.error( - `${element.type} elements cannot be placed on ${placement.trackType} tracks`, - ); - return null; - } - - const placementResult = resolveTrackPlacement({ - tracks, - ...(placement.mode === "auto" && placement.trackType - ? { trackType: placement.trackType } - : { elementType: element.type }), - timeSpans: [ - { - startTime: element.startTime, - duration: element.duration, - }, - ], - strategy: - placement.mode === "explicit" - ? { type: "explicit", trackId: placement.trackId } - : { type: "firstAvailable" }, - }); - if (!placementResult) { - if (placement.mode === "explicit") { - 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; - } - - const validation = validateElementTrackCompatibility({ - element, - track: targetTrack, - }); - console.error(validation.errorMessage); - } - - return null; - } - - const elementToPlace = - placementResult.kind === "existingTrack" - ? { - ...element, - startTime: placementResult.adjustedStartTime ?? element.startTime, - } - : element; - - const appliedPlacement = applyPlacement({ - tracks, - placementResult, - elements: [elementToPlace], - newTrackInsertIndexOverride: - placement.mode === "auto" && typeof placement.insertIndex === "number" - ? 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/index.ts b/apps/web/src/lib/commands/timeline/element/keyframes/index.ts deleted file mode 100644 index 319a827b..00000000 --- a/apps/web/src/lib/commands/timeline/element/keyframes/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./remove-effect-param-keyframe"; -export * from "./remove-keyframe"; -export * from "./retime-keyframe"; -export * from "./update-scalar-keyframe-curve"; -export * from "./upsert-effect-param-keyframe"; -export * from "./upsert-keyframe"; 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 deleted file mode 100644 index b1ef4aef..00000000 --- a/apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { EditorCore } from "@/core"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { removeEffectParamKeyframe } from "@/lib/animation/effect-param-channel"; -import { updateElementInSceneTracks } from "@/lib/timeline"; -import { isVisualElement } from "@/lib/timeline/element-utils"; -import type { SceneTracks } from "@/lib/timeline"; - -export class RemoveEffectParamKeyframeCommand extends Command { - private savedState: SceneTracks | null = null; - private readonly trackId: string; - private readonly elementId: string; - private readonly effectId: string; - private readonly paramKey: string; - private readonly keyframeId: string; - - constructor({ - trackId, - elementId, - effectId, - paramKey, - keyframeId, - }: { - trackId: string; - elementId: string; - effectId: string; - paramKey: string; - keyframeId: string; - }) { - super(); - this.trackId = trackId; - this.elementId = elementId; - this.effectId = effectId; - this.paramKey = paramKey; - this.keyframeId = keyframeId; - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - const updatedTracks = updateElementInSceneTracks({ - tracks: this.savedState, - trackId: this.trackId, - elementId: this.elementId, - elementPredicate: isVisualElement, - update: (element) => { - const animations = removeEffectParamKeyframe({ - animations: element.animations, - effectId: this.effectId, - paramKey: this.paramKey, - keyframeId: this.keyframeId, - }); - return { ...element, animations }; - }, - }); - - editor.timeline.updateTracks(updatedTracks); - return undefined; - } - - undo(): void { - if (!this.savedState) { - return; - } - - const editor = EditorCore.getInstance(); - editor.timeline.updateTracks(this.savedState); - } -} 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 deleted file mode 100644 index 3c3d1c6b..00000000 --- a/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { EditorCore } from "@/core"; -import { - hasKeyframesForPath, - removeElementKeyframe, - resolveAnimationTarget, -} from "@/lib/animation"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { updateElementInSceneTracks } from "@/lib/timeline"; -import type { AnimationPath, AnimationValue } from "@/lib/animation/types"; -import type { SceneTracks, TimelineElement } from "@/lib/timeline"; - -function removeKeyframeAndPersist({ - element, - propertyPath, - keyframeId, - valueAtPlayhead, -}: { - element: TimelineElement; - propertyPath: AnimationPath; - keyframeId: string; - valueAtPlayhead: AnimationValue | null; -}): TimelineElement { - const target = resolveAnimationTarget({ element, path: propertyPath }); - if (!target) { - return element; - } - - const nextAnimations = removeElementKeyframe({ - animations: element.animations, - propertyPath, - keyframeId, - }); - - const isChannelNowEmpty = !hasKeyframesForPath({ - animations: nextAnimations, - propertyPath, - }); - - // When clearing all keyframes, preserve the value at the playhead (what the user was seeing) - const shouldPersistToBase = isChannelNowEmpty && valueAtPlayhead !== null; - - const baseElement = shouldPersistToBase - ? target.setBaseValue({ value: valueAtPlayhead }) - : element; - - return { ...baseElement, animations: nextAnimations }; -} - -export class RemoveKeyframeCommand extends Command { - private savedState: SceneTracks | null = null; - private readonly trackId: string; - private readonly elementId: string; - private readonly propertyPath: AnimationPath; - private readonly keyframeId: string; - private readonly valueAtPlayhead: AnimationValue | null; - - constructor({ - trackId, - elementId, - propertyPath, - keyframeId, - valueAtPlayhead, - }: { - trackId: string; - elementId: string; - propertyPath: AnimationPath; - keyframeId: string; - valueAtPlayhead: AnimationValue | null; - }) { - super(); - this.trackId = trackId; - this.elementId = elementId; - this.propertyPath = propertyPath; - this.keyframeId = keyframeId; - this.valueAtPlayhead = valueAtPlayhead; - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - const updatedTracks = updateElementInSceneTracks({ - tracks: this.savedState, - trackId: this.trackId, - elementId: this.elementId, - update: (element) => - removeKeyframeAndPersist({ - element, - propertyPath: this.propertyPath, - keyframeId: this.keyframeId, - valueAtPlayhead: this.valueAtPlayhead, - }), - }); - - editor.timeline.updateTracks(updatedTracks); - return undefined; - } - - undo(): void { - if (!this.savedState) { - return; - } - - const editor = EditorCore.getInstance(); - editor.timeline.updateTracks(this.savedState); - } -} 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 deleted file mode 100644 index ad7f5651..00000000 --- a/apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { EditorCore } from "@/core"; -import { resolveAnimationTarget, retimeElementKeyframe } from "@/lib/animation"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { updateElementInSceneTracks } from "@/lib/timeline"; -import type { AnimationPath } from "@/lib/animation/types"; -import type { SceneTracks } from "@/lib/timeline"; - -export class RetimeKeyframeCommand extends Command { - private savedState: SceneTracks | null = null; - private readonly trackId: string; - private readonly elementId: string; - private readonly propertyPath: AnimationPath; - private readonly keyframeId: string; - private readonly nextTime: number; - - constructor({ - trackId, - elementId, - propertyPath, - keyframeId, - nextTime, - }: { - trackId: string; - elementId: string; - propertyPath: AnimationPath; - keyframeId: string; - nextTime: number; - }) { - super(); - this.trackId = trackId; - this.elementId = elementId; - this.propertyPath = propertyPath; - this.keyframeId = keyframeId; - this.nextTime = nextTime; - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - const updatedTracks = updateElementInSceneTracks({ - tracks: this.savedState, - trackId: this.trackId, - elementId: this.elementId, - update: (element) => { - if (!resolveAnimationTarget({ element, path: this.propertyPath })) { - return element; - } - - const boundedTime = Math.max(0, Math.min(this.nextTime, element.duration)); - return { - ...element, - animations: retimeElementKeyframe({ - animations: element.animations, - propertyPath: this.propertyPath, - keyframeId: this.keyframeId, - time: boundedTime, - }), - }; - }, - }); - - editor.timeline.updateTracks(updatedTracks); - return undefined; - } - - undo(): void { - if (!this.savedState) { - return; - } - - const editor = EditorCore.getInstance(); - editor.timeline.updateTracks(this.savedState); - } -} 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 deleted file mode 100644 index 9a4c2304..00000000 --- a/apps/web/src/lib/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { EditorCore } from "@/core"; -import { - resolveAnimationTarget, - updateScalarKeyframeCurve, -} from "@/lib/animation"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { updateElementInSceneTracks } from "@/lib/timeline"; -import type { - AnimationPath, - ScalarCurveKeyframePatch, -} from "@/lib/animation/types"; -import type { SceneTracks } from "@/lib/timeline"; - -export class UpdateScalarKeyframeCurveCommand extends Command { - private savedState: SceneTracks | null = null; - private readonly trackId: string; - private readonly elementId: string; - private readonly propertyPath: AnimationPath; - private readonly componentKey: string; - private readonly keyframeId: string; - private readonly patch: ScalarCurveKeyframePatch; - - constructor({ - trackId, - elementId, - propertyPath, - componentKey, - keyframeId, - patch, - }: { - trackId: string; - elementId: string; - propertyPath: AnimationPath; - componentKey: string; - keyframeId: string; - patch: ScalarCurveKeyframePatch; - }) { - super(); - this.trackId = trackId; - this.elementId = elementId; - this.propertyPath = propertyPath; - this.componentKey = componentKey; - this.keyframeId = keyframeId; - this.patch = patch; - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - const updatedTracks = updateElementInSceneTracks({ - tracks: this.savedState, - trackId: this.trackId, - elementId: this.elementId, - update: (element) => { - if (!resolveAnimationTarget({ element, path: this.propertyPath })) { - return element; - } - - return { - ...element, - animations: updateScalarKeyframeCurve({ - animations: element.animations, - propertyPath: this.propertyPath, - componentKey: this.componentKey, - keyframeId: this.keyframeId, - patch: this.patch, - }), - }; - }, - }); - - editor.timeline.updateTracks(updatedTracks); - return undefined; - } - - undo(): void { - if (!this.savedState) { - return; - } - - const editor = EditorCore.getInstance(); - editor.timeline.updateTracks(this.savedState); - } -} 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 deleted file mode 100644 index f436721a..00000000 --- a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { EditorCore } from "@/core"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { - buildEffectParamPath, - resolveAnimationTarget, - upsertPathKeyframe, -} from "@/lib/animation"; -import { updateElementInSceneTracks } from "@/lib/timeline"; -import { isVisualElement } from "@/lib/timeline/element-utils"; -import type { AnimationInterpolation } from "@/lib/animation/types"; -import type { SceneTracks } from "@/lib/timeline"; - -export class UpsertEffectParamKeyframeCommand extends Command { - private savedState: SceneTracks | null = null; - private readonly trackId: string; - private readonly elementId: string; - private readonly effectId: string; - private readonly paramKey: string; - private readonly time: number; - private readonly value: number | string | boolean; - private readonly interpolation: AnimationInterpolation | undefined; - private readonly keyframeId: string | undefined; - - constructor({ - trackId, - elementId, - effectId, - paramKey, - time, - value, - interpolation, - keyframeId, - }: { - trackId: string; - elementId: string; - effectId: string; - paramKey: string; - time: number; - value: number | string | boolean; - interpolation?: AnimationInterpolation; - keyframeId?: string; - }) { - super(); - this.trackId = trackId; - this.elementId = elementId; - this.effectId = effectId; - this.paramKey = paramKey; - this.time = time; - this.value = value; - this.interpolation = interpolation; - this.keyframeId = keyframeId; - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - const updatedTracks = updateElementInSceneTracks({ - tracks: this.savedState, - trackId: this.trackId, - elementId: this.elementId, - elementPredicate: isVisualElement, - update: (element) => { - const boundedTime = Math.max(0, Math.min(this.time, element.duration)); - const propertyPath = buildEffectParamPath({ - effectId: this.effectId, - paramKey: this.paramKey, - }); - const target = resolveAnimationTarget({ - element, - path: propertyPath, - }); - if (!target) { - return element; - } - - const animations = upsertPathKeyframe({ - animations: element.animations, - propertyPath, - time: boundedTime, - value: this.value, - interpolation: this.interpolation, - keyframeId: this.keyframeId, - kind: target.kind, - defaultInterpolation: target.defaultInterpolation, - coerceValue: target.coerceValue, - }); - return { ...element, animations }; - }, - }); - - editor.timeline.updateTracks(updatedTracks); - return undefined; - } - - undo(): void { - if (!this.savedState) { - return; - } - - const editor = EditorCore.getInstance(); - editor.timeline.updateTracks(this.savedState); - } -} 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 deleted file mode 100644 index f77e69b8..00000000 --- a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { EditorCore } from "@/core"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { resolveAnimationTarget, upsertPathKeyframe } from "@/lib/animation"; -import { updateElementInSceneTracks } from "@/lib/timeline"; -import type { SceneTracks } from "@/lib/timeline"; -import type { - AnimationPath, - AnimationInterpolation, - AnimationValue, -} from "@/lib/animation/types"; - -export class UpsertKeyframeCommand extends Command { - private savedState: SceneTracks | null = null; - private readonly trackId: string; - private readonly elementId: string; - private readonly propertyPath: AnimationPath; - private readonly time: number; - private readonly value: AnimationValue; - private readonly interpolation: AnimationInterpolation | undefined; - private readonly keyframeId: string | undefined; - - constructor({ - trackId, - elementId, - propertyPath, - time, - value, - interpolation, - keyframeId, - }: { - trackId: string; - elementId: string; - propertyPath: AnimationPath; - time: number; - value: AnimationValue; - interpolation?: AnimationInterpolation; - keyframeId?: string; - }) { - super(); - this.trackId = trackId; - this.elementId = elementId; - this.propertyPath = propertyPath; - this.time = time; - this.value = value; - this.interpolation = interpolation; - this.keyframeId = keyframeId; - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - const updatedTracks = updateElementInSceneTracks({ - tracks: this.savedState, - trackId: this.trackId, - elementId: this.elementId, - update: (element) => { - const target = resolveAnimationTarget({ - element, - path: this.propertyPath, - }); - if (!target) { - return element; - } - - const boundedTime = Math.max(0, Math.min(this.time, element.duration)); - return { - ...element, - animations: upsertPathKeyframe({ - animations: element.animations, - propertyPath: this.propertyPath, - time: boundedTime, - value: this.value, - interpolation: this.interpolation, - keyframeId: this.keyframeId, - kind: target.kind, - defaultInterpolation: target.defaultInterpolation, - coerceValue: target.coerceValue, - }), - }; - }, - }); - - editor.timeline.updateTracks(updatedTracks); - return undefined; - } - - undo(): void { - if (!this.savedState) { - return; - } - - const editor = EditorCore.getInstance(); - editor.timeline.updateTracks(this.savedState); - } -} diff --git a/apps/web/src/lib/commands/timeline/element/masks/delete-custom-mask-points.ts b/apps/web/src/lib/commands/timeline/element/masks/delete-custom-mask-points.ts deleted file mode 100644 index 346eb8d9..00000000 --- a/apps/web/src/lib/commands/timeline/element/masks/delete-custom-mask-points.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { EditorCore } from "@/core"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { - getCustomMaskClosedStateAfterPointRemoval, - removeCustomMaskPoints, -} from "@/lib/masks/custom-path"; -import type { CustomMask } from "@/lib/masks/types"; -import { isMaskableElement, updateElementInSceneTracks } from "@/lib/timeline"; -import type { MaskableElement, SceneTracks } from "@/lib/timeline"; - -function deletePointsFromCustomMask({ - mask, - pointIds, -}: { - mask: CustomMask; - pointIds: string[]; -}): CustomMask { - const points = mask.params.path; - const nextPoints = removeCustomMaskPoints({ points, pointIds }); - if (nextPoints.length === points.length) { - return mask; - } - - return { - ...mask, - params: { - ...mask.params, - path: nextPoints, - closed: getCustomMaskClosedStateAfterPointRemoval({ - wasClosed: mask.params.closed, - remainingPointCount: nextPoints.length, - }), - }, - }; -} - -function deletePointsFromElementMask({ - element, - maskId, - pointIds, -}: { - element: MaskableElement; - maskId: string; - pointIds: string[]; -}): { element: MaskableElement; didDeletePoints: boolean } { - const currentMasks = element.masks ?? []; - let didDeletePoints = false; - const nextMasks = currentMasks.map((mask) => { - if (mask.id !== maskId || mask.type !== "custom") { - return mask; - } - - const nextMask = deletePointsFromCustomMask({ - mask, - pointIds, - }); - didDeletePoints ||= nextMask !== mask; - return nextMask; - }); - - return { - element: didDeletePoints ? { ...element, masks: nextMasks } : element, - didDeletePoints, - }; -} - -export class DeleteCustomMaskPointsCommand extends Command { - private savedState: SceneTracks | null = null; - private readonly trackId: string; - private readonly elementId: string; - private readonly maskId: string; - private readonly pointIds: string[]; - - constructor({ - trackId, - elementId, - maskId, - pointIds, - }: { - trackId: string; - elementId: string; - maskId: string; - pointIds: string[]; - }) { - super(); - this.trackId = trackId; - this.elementId = elementId; - this.maskId = maskId; - this.pointIds = pointIds; - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - let didDeletePoints = false; - const updatedTracks = updateElementInSceneTracks({ - tracks: this.savedState, - trackId: this.trackId, - elementId: this.elementId, - elementPredicate: isMaskableElement, - update: (element) => { - const result = deletePointsFromElementMask({ - element: element as MaskableElement, - maskId: this.maskId, - pointIds: this.pointIds, - }); - didDeletePoints ||= result.didDeletePoints; - return result.element; - }, - }); - - if (didDeletePoints) { - editor.timeline.updateTracks(updatedTracks); - return { - selection: { - selectedMaskPoints: null, - }, - }; - } - - 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/element/masks/index.ts b/apps/web/src/lib/commands/timeline/element/masks/index.ts deleted file mode 100644 index dd6373b1..00000000 --- a/apps/web/src/lib/commands/timeline/element/masks/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { DeleteCustomMaskPointsCommand } from "./delete-custom-mask-points"; -export { InsertCustomMaskPointCommand } from "./insert-custom-mask-point"; -export { RemoveMaskCommand } from "./remove-mask"; -export { ToggleMaskInvertedCommand } from "./toggle-mask-inverted"; diff --git a/apps/web/src/lib/commands/timeline/element/masks/insert-custom-mask-point.ts b/apps/web/src/lib/commands/timeline/element/masks/insert-custom-mask-point.ts deleted file mode 100644 index 9f837836..00000000 --- a/apps/web/src/lib/commands/timeline/element/masks/insert-custom-mask-point.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { EditorCore } from "@/core"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { insertPointOnCustomMaskSegment } from "@/lib/masks/definitions/custom"; -import type { ElementBounds } from "@/lib/preview/element-bounds"; -import type { CustomMask } from "@/lib/masks/types"; -import { isMaskableElement, updateElementInSceneTracks } from "@/lib/timeline"; -import type { MaskableElement, SceneTracks } from "@/lib/timeline"; - -function insertPointIntoCustomMask({ - mask, - segmentIndex, - canvasPoint, - bounds, -}: { - mask: CustomMask; - segmentIndex: number; - canvasPoint: { x: number; y: number }; - bounds: ElementBounds; -}): { mask: CustomMask; insertedPointId: string | null } { - const result = insertPointOnCustomMaskSegment({ - params: mask.params, - segmentIndex, - canvasPoint, - bounds, - }); - if (!result) { - return { - mask, - insertedPointId: null, - }; - } - - return { - mask: { - ...mask, - params: result.params, - }, - insertedPointId: result.pointId, - }; -} - -function insertPointIntoElementMask({ - element, - maskId, - segmentIndex, - canvasPoint, - bounds, -}: { - element: MaskableElement; - maskId: string; - segmentIndex: number; - canvasPoint: { x: number; y: number }; - bounds: ElementBounds; -}): { - element: MaskableElement; - didInsertPoint: boolean; - insertedPointId: string | null; -} { - const currentMasks = element.masks ?? []; - let insertedPointId: string | null = null; - let didInsertPoint = false; - - const nextMasks = currentMasks.map((mask) => { - if (mask.id !== maskId || mask.type !== "custom") { - return mask; - } - - const result = insertPointIntoCustomMask({ - mask, - segmentIndex, - canvasPoint, - bounds, - }); - if (result.insertedPointId) { - insertedPointId = result.insertedPointId; - didInsertPoint = true; - } - return result.mask; - }); - - return { - element: didInsertPoint ? { ...element, masks: nextMasks } : element, - didInsertPoint, - insertedPointId, - }; -} - -export class InsertCustomMaskPointCommand extends Command { - private savedState: SceneTracks | null = null; - private readonly trackId: string; - private readonly elementId: string; - private readonly maskId: string; - private readonly segmentIndex: number; - private readonly canvasPoint: { x: number; y: number }; - private readonly bounds: ElementBounds; - - constructor({ - trackId, - elementId, - maskId, - segmentIndex, - canvasPoint, - bounds, - }: { - trackId: string; - elementId: string; - maskId: string; - segmentIndex: number; - canvasPoint: { x: number; y: number }; - bounds: ElementBounds; - }) { - super(); - this.trackId = trackId; - this.elementId = elementId; - this.maskId = maskId; - this.segmentIndex = segmentIndex; - this.canvasPoint = canvasPoint; - this.bounds = bounds; - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - let didInsertPoint = false; - let insertedPointId: string | null = null; - const updatedTracks = updateElementInSceneTracks({ - tracks: this.savedState, - trackId: this.trackId, - elementId: this.elementId, - elementPredicate: isMaskableElement, - update: (element) => { - const result = insertPointIntoElementMask({ - element: element as MaskableElement, - maskId: this.maskId, - segmentIndex: this.segmentIndex, - canvasPoint: this.canvasPoint, - bounds: this.bounds, - }); - didInsertPoint ||= result.didInsertPoint; - insertedPointId ??= result.insertedPointId; - return result.element; - }, - }); - - if (!didInsertPoint || !insertedPointId) { - return undefined; - } - - editor.timeline.updateTracks(updatedTracks); - return { - selection: { - selectedMaskPoints: { - trackId: this.trackId, - elementId: this.elementId, - maskId: this.maskId, - pointIds: [insertedPointId], - }, - }, - }; - } - - undo(): void { - if (this.savedState) { - const editor = EditorCore.getInstance(); - editor.timeline.updateTracks(this.savedState); - } - } -} 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 deleted file mode 100644 index 9f6c5f77..00000000 --- a/apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { EditorCore } from "@/core"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { isMaskableElement, updateElementInSceneTracks } from "@/lib/timeline"; -import type { SceneTracks, MaskableElement } from "@/lib/timeline"; - -function removeMaskFromElement({ - element, - maskId, -}: { - element: MaskableElement; - maskId: string; -}): MaskableElement { - const currentMasks = element.masks ?? []; - const filteredMasks = currentMasks.filter((mask) => mask.id !== maskId); - return { ...element, masks: filteredMasks }; -} - -export class RemoveMaskCommand extends Command { - private savedState: SceneTracks | null = null; - private readonly trackId: string; - private readonly elementId: string; - private readonly maskId: string; - - constructor({ - trackId, - elementId, - maskId, - }: { - trackId: string; - elementId: string; - maskId: string; - }) { - super(); - this.trackId = trackId; - this.elementId = elementId; - this.maskId = maskId; - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - const updatedTracks = updateElementInSceneTracks({ - tracks: this.savedState, - trackId: this.trackId, - elementId: this.elementId, - elementPredicate: isMaskableElement, - update: (element) => - removeMaskFromElement({ - element: element as MaskableElement, - maskId: this.maskId, - }), - }); - - 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/element/masks/toggle-mask-inverted.ts b/apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts deleted file mode 100644 index 2b5c1694..00000000 --- a/apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { EditorCore } from "@/core"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { isMaskableElement, updateElementInSceneTracks } from "@/lib/timeline"; -import type { Mask } from "@/lib/masks/types"; -import type { SceneTracks, MaskableElement } from "@/lib/timeline"; - -export function toggleMaskInvertedOnElement({ - element, - maskId, -}: { - element: MaskableElement; - maskId: string; -}): MaskableElement { - const currentMasks = element.masks ?? []; - const toggleMask = <TMask extends Mask>(mask: TMask): TMask => ({ - ...mask, - params: { - ...mask.params, - inverted: !mask.params.inverted, - }, - }); - const updatedMasks = currentMasks.map((mask) => - mask.id !== maskId ? mask : toggleMask(mask), - ); - - return { ...element, masks: updatedMasks }; -} - -export class ToggleMaskInvertedCommand extends Command { - private savedState: SceneTracks | null = null; - private readonly trackId: string; - private readonly elementId: string; - private readonly maskId: string; - - constructor({ - trackId, - elementId, - maskId, - }: { - trackId: string; - elementId: string; - maskId: string; - }) { - super(); - this.trackId = trackId; - this.elementId = elementId; - this.maskId = maskId; - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - const updatedTracks = updateElementInSceneTracks({ - tracks: this.savedState, - trackId: this.trackId, - elementId: this.elementId, - elementPredicate: isMaskableElement, - update: (element) => - toggleMaskInvertedOnElement({ - element: element as MaskableElement, - maskId: this.maskId, - }), - }); - - 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/element/toggle-source-audio-separation.ts b/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts deleted file mode 100644 index 7300f540..00000000 --- a/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { EditorCore } from "@/core"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { - buildSeparatedAudioElement, - canExtractSourceAudio, - isSourceAudioSeparated, -} from "@/lib/timeline/audio-separation"; -import { - applyPlacement, - resolveTrackPlacement, -} from "@/lib/timeline/placement"; -import { updateElementInSceneTracks } from "@/lib/timeline/track-element-update"; -import type { - SceneTracks, - TimelineElement, - VideoElement, -} from "@/lib/timeline/types"; -import { generateUUID } from "@/utils/id"; - -export class ToggleSourceAudioSeparationCommand extends Command { - private savedState: SceneTracks | null = null; - - constructor( - private readonly params: { - trackId: string; - elementId: string; - }, - ) { - super(); - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - const sourceTrack = [ - ...this.savedState.overlay, - this.savedState.main, - ...this.savedState.audio, - ].find((track) => track.id === this.params.trackId); - if (!sourceTrack) { - return; - } - const sourceElement = sourceTrack.elements.find( - (element) => element.id === this.params.elementId, - ) as TimelineElement | undefined; - if (!sourceElement || sourceElement.type !== "video") { - return; - } - const videoElement: VideoElement = sourceElement; - - if (isSourceAudioSeparated({ element: videoElement })) { - editor.timeline.updateTracks( - updateSourceAudioEnabled({ - tracks: this.savedState, - trackId: this.params.trackId, - elementId: this.params.elementId, - isSourceAudioEnabled: true, - }), - ); - return; - } - - const mediaAsset = editor.media - .getAssets() - .find((asset) => asset.id === videoElement.mediaId); - if (!canExtractSourceAudio(videoElement, mediaAsset)) { - return; - } - if (videoElement.duration <= 0) { - return; - } - - const separatedAudioElement = { - ...buildSeparatedAudioElement({ - sourceElement: videoElement, - }), - id: generateUUID(), - }; - const placementResult = resolveTrackPlacement({ - tracks: this.savedState, - trackType: "audio", - timeSpans: [ - { - startTime: separatedAudioElement.startTime, - duration: separatedAudioElement.duration, - }, - ], - strategy: { type: "firstAvailable" }, - }); - if (!placementResult) { - return; - } - const appliedPlacement = applyPlacement({ - tracks: this.savedState, - placementResult, - elements: [separatedAudioElement], - }); - if (!appliedPlacement) { - return; - } - - editor.timeline.updateTracks( - updateSourceAudioEnabled({ - tracks: appliedPlacement.updatedTracks, - trackId: this.params.trackId, - elementId: this.params.elementId, - isSourceAudioEnabled: false, - }), - ); - } - - undo(): void { - if (!this.savedState) { - return; - } - - const editor = EditorCore.getInstance(); - editor.timeline.updateTracks(this.savedState); - } -} - -function updateSourceAudioEnabled({ - tracks, - trackId, - elementId, - isSourceAudioEnabled, -}: { - tracks: SceneTracks; - trackId: string; - elementId: string; - isSourceAudioEnabled: boolean; -}): SceneTracks { - return updateElementInSceneTracks({ - tracks, - trackId, - elementId, - elementPredicate: (element): element is VideoElement => - element.type === "video", - update: (element) => ({ - ...element, - isSourceAudioEnabled, - }), - }); -} diff --git a/apps/web/src/lib/commands/timeline/element/update-elements.ts b/apps/web/src/lib/commands/timeline/element/update-elements.ts deleted file mode 100644 index 54d1ca88..00000000 --- a/apps/web/src/lib/commands/timeline/element/update-elements.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { EditorCore } from "@/core"; -import { Command, type CommandResult } from "@/lib/commands/base-command"; -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: SceneTracks | null = null; - private readonly updates: Array<{ - trackId: string; - elementId: string; - patch: Partial<TimelineElement>; - }>; - - constructor({ - updates, - }: { - updates: Array<{ - trackId: string; - elementId: string; - patch: Partial<TimelineElement>; - }>; - }) { - super(); - this.updates = updates; - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - let updatedTracks = this.savedState; - - for (const updateEntry of this.updates) { - const currentTrack = findTrackInSceneTracks({ - tracks: updatedTracks, - trackId: updateEntry.trackId, - }); - const currentElement = currentTrack?.elements.find( - (element) => element.id === updateEntry.elementId, - ); - if (!currentTrack || !currentElement) { - continue; - } - - const nextElement = applyElementUpdate({ - element: currentElement, - patch: updateEntry.patch, - context: { - tracks: updatedTracks, - trackId: updateEntry.trackId, - }, - }); - - updatedTracks = updateElementInSceneTracks({ - tracks: updatedTracks, - trackId: updateEntry.trackId, - elementId: updateEntry.elementId, - update: () => nextElement, - }); - } - - 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/index.ts b/apps/web/src/lib/commands/timeline/index.ts deleted file mode 100644 index cb285d94..00000000 --- a/apps/web/src/lib/commands/timeline/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index a577c693..00000000 --- a/apps/web/src/lib/commands/timeline/track/add-track.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { SceneTracks, TrackType } from "@/lib/timeline"; -import { generateUUID } from "@/utils/id"; -import { EditorCore } from "@/core"; -import { - buildEmptyTrack, - getDefaultInsertIndexForTrack, -} from "@/lib/timeline/placement"; - -export class AddTrackCommand extends Command { - private trackId: string; - private savedState: SceneTracks | null = null; - - constructor( - private type: TrackType, - private index?: number, - ) { - super(); - this.trackId = generateUUID(); - } - - execute(): CommandResult | undefined { - const editor = EditorCore.getInstance(); - this.savedState = editor.scenes.getActiveScene().tracks; - - const insertIndex = - this.index ?? - getDefaultInsertIndexForTrack({ - tracks: this.savedState, - trackType: this.type, - }); - - 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; - } - - undo(): void { - if (this.savedState) { - const editor = EditorCore.getInstance(); - editor.timeline.updateTracks(this.savedState); - } - } - - getTrackId(): string { - 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<TrackType, "audio">; -}): 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/index.ts b/apps/web/src/lib/commands/timeline/track/index.ts deleted file mode 100644 index 9d4bd9c3..00000000 --- a/apps/web/src/lib/commands/timeline/track/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { AddTrackCommand } from "./add-track"; -export { RemoveTrackCommand } from "./remove-track"; -export { ToggleTrackMuteCommand } from "./toggle-track-mute"; -export { ToggleTrackVisibilityCommand } from "./toggle-track-visibility"; diff --git a/apps/web/src/lib/commands/timeline/track/remove-track.ts b/apps/web/src/lib/commands/timeline/track/remove-track.ts deleted file mode 100644 index 5fcaa898..00000000 --- a/apps/web/src/lib/commands/timeline/track/remove-track.ts +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 7ffe2fd7..00000000 --- a/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index 2292c83a..00000000 --- a/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index daed6a9e..00000000 --- a/apps/web/src/lib/commands/timeline/tracks-snapshot.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { SceneTracks } from "@/lib/timeline"; -import { EditorCore } from "@/core"; - -export class TracksSnapshotCommand extends Command { - constructor( - private before: SceneTracks, - private after: SceneTracks, - ) { - super(); - } - - execute(): CommandResult | undefined { - EditorCore.getInstance().timeline.updateTracks(this.after); - return undefined; - } - - undo(): void { - EditorCore.getInstance().timeline.updateTracks(this.before); - } -} |
