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