refactor: update command execute methods to return CommandResult | undefined

This commit is contained in:
Maze Winther 2026-04-03 03:13:49 +02:00
parent 3516bd69f4
commit 1a8a1e889a
42 changed files with 1492 additions and 1463 deletions

View File

@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { toast } from "sonner"; import { toast } from "sonner";
import type { MediaAsset } from "@/lib/media/types"; import type { MediaAsset } from "@/lib/media/types";
@ -23,7 +23,7 @@ export class AddMediaAssetCommand extends Command {
this.assetId = generateUUID(); this.assetId = generateUUID();
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedAssets = [...editor.media.getAssets()]; this.savedAssets = [...editor.media.getAssets()];
@ -80,6 +80,8 @@ export class AddMediaAssetCommand extends Command {
}); });
} }
}); });
return undefined;
} }
undo(): void { undo(): void {

View File

@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import type { MediaAsset } from "@/lib/media/types"; import type { MediaAsset } from "@/lib/media/types";
import { storageService } from "@/services/storage/service"; import { storageService } from "@/services/storage/service";
@ -18,7 +18,7 @@ export class RemoveMediaAssetCommand extends Command {
super(); super();
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
const assets = editor.media.getAssets(); const assets = editor.media.getAssets();

View File

@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import type { TProject, TProjectSettings } from "@/lib/project/types"; import type { TProject, TProjectSettings } from "@/lib/project/types";
@ -10,7 +10,7 @@ export class UpdateProjectSettingsCommand extends Command {
super(); super();
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
const activeProject = editor.project.getActive(); const activeProject = editor.project.getActive();
if (!activeProject) return; if (!activeProject) return;

View File

@ -1,40 +1,41 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import type { TScene } from "@/lib/timeline"; import type { TScene } from "@/lib/timeline";
import { buildDefaultScene } from "@/lib/scenes"; import { buildDefaultScene } from "@/lib/scenes";
export class CreateSceneCommand extends Command { export class CreateSceneCommand extends Command {
private savedScenes: TScene[] | null = null; private savedScenes: TScene[] | null = null;
private createdScene: TScene | null = null; private createdScene: TScene | null = null;
constructor( constructor(
private name: string, private name: string,
private isMain: boolean = false, private isMain: boolean = false,
) { ) {
super(); super();
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedScenes = [...editor.scenes.getScenes()]; this.savedScenes = [...editor.scenes.getScenes()];
this.createdScene = buildDefaultScene({ this.createdScene = buildDefaultScene({
name: this.name, name: this.name,
isMain: this.isMain, isMain: this.isMain,
}); });
const updatedScenes = [...this.savedScenes, this.createdScene]; const updatedScenes = [...this.savedScenes, this.createdScene];
editor.scenes.setScenes({ scenes: updatedScenes }); editor.scenes.setScenes({ scenes: updatedScenes });
} return undefined;
}
undo(): void {
if (this.savedScenes) { undo(): void {
const editor = EditorCore.getInstance(); if (this.savedScenes) {
editor.scenes.setScenes({ scenes: this.savedScenes }); const editor = EditorCore.getInstance();
} editor.scenes.setScenes({ scenes: this.savedScenes });
} }
}
getSceneId(): string {
return this.createdScene?.id ?? ""; getSceneId(): string {
} return this.createdScene?.id ?? "";
} }
}

View File

@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import type { TScene } from "@/lib/timeline"; import type { TScene } from "@/lib/timeline";
import { canDeleteScene, getFallbackSceneAfterDelete } from "@/lib/scenes"; import { canDeleteScene, getFallbackSceneAfterDelete } from "@/lib/scenes";
@ -12,7 +12,7 @@ export class DeleteSceneCommand extends Command {
super(); super();
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
const scenes = editor.scenes.getScenes(); const scenes = editor.scenes.getScenes();
const activeScene = editor.scenes.getActiveScene(); const activeScene = editor.scenes.getActiveScene();

View File

@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import type { TScene } from "@/lib/timeline"; import type { TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes"; import { updateSceneInArray } from "@/lib/scenes";
@ -14,7 +14,7 @@ export class MoveBookmarkCommand extends Command {
super(); super();
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
const activeScene = editor.scenes.getActiveScene(); const activeScene = editor.scenes.getActiveScene();
const activeProject = editor.project.getActive(); const activeProject = editor.project.getActive();

View File

@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import type { TScene } from "@/lib/timeline"; import type { TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes"; import { updateSceneInArray } from "@/lib/scenes";
@ -15,7 +15,7 @@ export class RemoveBookmarkCommand extends Command {
super(); super();
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
const activeScene = editor.scenes.getActiveScene(); const activeScene = editor.scenes.getActiveScene();
const activeProject = editor.project.getActive(); const activeProject = editor.project.getActive();

View File

@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import type { TScene } from "@/lib/timeline"; import type { TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes"; import { updateSceneInArray } from "@/lib/scenes";
@ -14,7 +14,7 @@ export class RenameSceneCommand extends Command {
super(); super();
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
const scenes = editor.scenes.getScenes(); const scenes = editor.scenes.getScenes();

View File

@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import type { TScene } from "@/lib/timeline"; import type { TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes"; import { updateSceneInArray } from "@/lib/scenes";
@ -12,7 +12,7 @@ export class ToggleBookmarkCommand extends Command {
super(); super();
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
const activeScene = editor.scenes.getActiveScene(); const activeScene = editor.scenes.getActiveScene();
const activeProject = editor.project.getActive(); const activeProject = editor.project.getActive();

View File

@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import type { Bookmark, TScene } from "@/lib/timeline"; import type { Bookmark, TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes"; import { updateSceneInArray } from "@/lib/scenes";
@ -14,7 +14,7 @@ export class UpdateBookmarkCommand extends Command {
super(); super();
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
const activeScene = editor.scenes.getActiveScene(); const activeScene = editor.scenes.getActiveScene();
const activeProject = editor.project.getActive(); const activeProject = editor.project.getActive();

View File

@ -33,7 +33,7 @@ export class PasteCommand extends Command {
} }
execute(): CommandResult | undefined { execute(): CommandResult | undefined {
if (this.clipboardItems.length === 0) return; if (this.clipboardItems.length === 0) return undefined;
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
@ -123,6 +123,7 @@ export class PasteCommand extends Command {
if (this.pastedElements.length > 0) { if (this.pastedElements.length > 0) {
return { select: this.pastedElements }; return { select: this.pastedElements };
} }
return undefined;
} }
undo(): void { undo(): void {

View File

@ -20,7 +20,7 @@ export class DeleteElementsCommand extends Command {
this.rippleEnabled = rippleEnabled; this.rippleEnabled = rippleEnabled;
} }
execute(): CommandResult { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();

View File

@ -91,6 +91,7 @@ export class DuplicateElementsCommand extends Command {
select: this.duplicatedElements, select: this.duplicatedElements,
}; };
} }
return undefined;
} }
undo(): void { undo(): void {

View File

@ -1,74 +1,75 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
import type { TimelineTrack, VisualElement } from "@/lib/timeline"; import type { TimelineTrack, VisualElement } from "@/lib/timeline";
import { buildDefaultEffectInstance } from "@/lib/effects"; import { buildDefaultEffectInstance } from "@/lib/effects";
function addEffectToElement({ function addEffectToElement({
element, element,
effectType, effectType,
}: { }: {
element: VisualElement; element: VisualElement;
effectType: string; effectType: string;
}): VisualElement { }): VisualElement {
const instance = buildDefaultEffectInstance({ effectType }); const instance = buildDefaultEffectInstance({ effectType });
const currentEffects = element.effects ?? []; const currentEffects = element.effects ?? [];
return { ...element, effects: [...currentEffects, instance] }; return { ...element, effects: [...currentEffects, instance] };
} }
export class AddClipEffectCommand extends Command { export class AddClipEffectCommand extends Command {
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
private effectId: string | null = null; private effectId: string | null = null;
private readonly trackId: string; private readonly trackId: string;
private readonly elementId: string; private readonly elementId: string;
private readonly effectType: string; private readonly effectType: string;
constructor({ constructor({
trackId, trackId,
elementId, elementId,
effectType, effectType,
}: { }: {
trackId: string; trackId: string;
elementId: string; elementId: string;
effectType: string; effectType: string;
}) { }) {
super(); super();
this.trackId = trackId; this.trackId = trackId;
this.elementId = elementId; this.elementId = elementId;
this.effectType = effectType; this.effectType = effectType;
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
const updatedTracks = updateElementInTracks({ const updatedTracks = updateElementInTracks({
tracks: this.savedState, tracks: this.savedState,
trackId: this.trackId, trackId: this.trackId,
elementId: this.elementId, elementId: this.elementId,
elementPredicate: isVisualElement, elementPredicate: isVisualElement,
update: (element) => { update: (element) => {
const updated = addEffectToElement({ const updated = addEffectToElement({
element: element as VisualElement, element: element as VisualElement,
effectType: this.effectType, effectType: this.effectType,
}); });
const effects = updated.effects ?? []; const effects = updated.effects ?? [];
this.effectId = effects[effects.length - 1]?.id ?? null; this.effectId = effects[effects.length - 1]?.id ?? null;
return updated; return updated;
}, },
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
} return undefined;
}
undo(): void {
if (this.savedState) { undo(): void {
const editor = EditorCore.getInstance(); if (this.savedState) {
editor.timeline.updateTracks(this.savedState); const editor = EditorCore.getInstance();
} editor.timeline.updateTracks(this.savedState);
} }
}
getEffectId(): string | null {
return this.effectId; getEffectId(): string | null {
} return this.effectId;
} }
}

View File

@ -1,65 +1,66 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
import type { TimelineTrack, VisualElement } from "@/lib/timeline"; import type { TimelineTrack, VisualElement } from "@/lib/timeline";
function removeEffectFromElement({ function removeEffectFromElement({
element, element,
effectId, effectId,
}: { }: {
element: VisualElement; element: VisualElement;
effectId: string; effectId: string;
}): VisualElement { }): VisualElement {
const currentEffects = element.effects ?? []; const currentEffects = element.effects ?? [];
const filtered = currentEffects.filter((effect) => effect.id !== effectId); const filtered = currentEffects.filter((effect) => effect.id !== effectId);
return { ...element, effects: filtered }; return { ...element, effects: filtered };
} }
export class RemoveClipEffectCommand extends Command { export class RemoveClipEffectCommand extends Command {
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
private readonly trackId: string; private readonly trackId: string;
private readonly elementId: string; private readonly elementId: string;
private readonly effectId: string; private readonly effectId: string;
constructor({ constructor({
trackId, trackId,
elementId, elementId,
effectId, effectId,
}: { }: {
trackId: string; trackId: string;
elementId: string; elementId: string;
effectId: string; effectId: string;
}) { }) {
super(); super();
this.trackId = trackId; this.trackId = trackId;
this.elementId = elementId; this.elementId = elementId;
this.effectId = effectId; this.effectId = effectId;
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
const updatedTracks = updateElementInTracks({ const updatedTracks = updateElementInTracks({
tracks: this.savedState, tracks: this.savedState,
trackId: this.trackId, trackId: this.trackId,
elementId: this.elementId, elementId: this.elementId,
elementPredicate: isVisualElement, elementPredicate: isVisualElement,
update: (element) => { update: (element) => {
return removeEffectFromElement({ return removeEffectFromElement({
element: element as VisualElement, element: element as VisualElement,
effectId: this.effectId, effectId: this.effectId,
}); });
}, },
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
} return undefined;
}
undo(): void {
if (this.savedState) { undo(): void {
const editor = EditorCore.getInstance(); if (this.savedState) {
editor.timeline.updateTracks(this.savedState); const editor = EditorCore.getInstance();
} editor.timeline.updateTracks(this.savedState);
} }
} }
}

View File

@ -1,73 +1,74 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
import type { TimelineTrack, VisualElement } from "@/lib/timeline"; import type { TimelineTrack, VisualElement } from "@/lib/timeline";
function reorderEffectsOnElement({ function reorderEffectsOnElement({
element, element,
fromIndex, fromIndex,
toIndex, toIndex,
}: { }: {
element: VisualElement; element: VisualElement;
fromIndex: number; fromIndex: number;
toIndex: number; toIndex: number;
}): VisualElement { }): VisualElement {
const effects = [...(element.effects ?? [])]; const effects = [...(element.effects ?? [])];
const [moved] = effects.splice(fromIndex, 1); const [moved] = effects.splice(fromIndex, 1);
effects.splice(toIndex, 0, moved); effects.splice(toIndex, 0, moved);
return { ...element, effects }; return { ...element, effects };
} }
export class ReorderClipEffectsCommand extends Command { export class ReorderClipEffectsCommand extends Command {
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
private readonly trackId: string; private readonly trackId: string;
private readonly elementId: string; private readonly elementId: string;
private readonly fromIndex: number; private readonly fromIndex: number;
private readonly toIndex: number; private readonly toIndex: number;
constructor({ constructor({
trackId, trackId,
elementId, elementId,
fromIndex, fromIndex,
toIndex, toIndex,
}: { }: {
trackId: string; trackId: string;
elementId: string; elementId: string;
fromIndex: number; fromIndex: number;
toIndex: number; toIndex: number;
}) { }) {
super(); super();
this.trackId = trackId; this.trackId = trackId;
this.elementId = elementId; this.elementId = elementId;
this.fromIndex = fromIndex; this.fromIndex = fromIndex;
this.toIndex = toIndex; this.toIndex = toIndex;
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
const updatedTracks = updateElementInTracks({ const updatedTracks = updateElementInTracks({
tracks: this.savedState, tracks: this.savedState,
trackId: this.trackId, trackId: this.trackId,
elementId: this.elementId, elementId: this.elementId,
elementPredicate: isVisualElement, elementPredicate: isVisualElement,
update: (element) => { update: (element) => {
return reorderEffectsOnElement({ return reorderEffectsOnElement({
element: element as VisualElement, element: element as VisualElement,
fromIndex: this.fromIndex, fromIndex: this.fromIndex,
toIndex: this.toIndex, toIndex: this.toIndex,
}); });
}, },
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
} return undefined;
}
undo(): void {
if (this.savedState) { undo(): void {
const editor = EditorCore.getInstance(); if (this.savedState) {
editor.timeline.updateTracks(this.savedState); const editor = EditorCore.getInstance();
} editor.timeline.updateTracks(this.savedState);
} }
} }
}

View File

@ -1,67 +1,68 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
import type { TimelineTrack, VisualElement } from "@/lib/timeline"; import type { TimelineTrack, VisualElement } from "@/lib/timeline";
export function toggleEffectOnElement({ export function toggleEffectOnElement({
element, element,
effectId, effectId,
}: { }: {
element: VisualElement; element: VisualElement;
effectId: string; effectId: string;
}): VisualElement { }): VisualElement {
const currentEffects = element.effects ?? []; const currentEffects = element.effects ?? [];
const updated = currentEffects.map((effect) => const updated = currentEffects.map((effect) =>
effect.id === effectId ? { ...effect, enabled: !effect.enabled } : effect, effect.id === effectId ? { ...effect, enabled: !effect.enabled } : effect,
); );
return { ...element, effects: updated }; return { ...element, effects: updated };
} }
export class ToggleClipEffectCommand extends Command { export class ToggleClipEffectCommand extends Command {
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
private readonly trackId: string; private readonly trackId: string;
private readonly elementId: string; private readonly elementId: string;
private readonly effectId: string; private readonly effectId: string;
constructor({ constructor({
trackId, trackId,
elementId, elementId,
effectId, effectId,
}: { }: {
trackId: string; trackId: string;
elementId: string; elementId: string;
effectId: string; effectId: string;
}) { }) {
super(); super();
this.trackId = trackId; this.trackId = trackId;
this.elementId = elementId; this.elementId = elementId;
this.effectId = effectId; this.effectId = effectId;
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
const updatedTracks = updateElementInTracks({ const updatedTracks = updateElementInTracks({
tracks: this.savedState, tracks: this.savedState,
trackId: this.trackId, trackId: this.trackId,
elementId: this.elementId, elementId: this.elementId,
elementPredicate: isVisualElement, elementPredicate: isVisualElement,
update: (element) => { update: (element) => {
return toggleEffectOnElement({ return toggleEffectOnElement({
element: element as VisualElement, element: element as VisualElement,
effectId: this.effectId, effectId: this.effectId,
}); });
}, },
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
} return undefined;
}
undo(): void {
if (this.savedState) { undo(): void {
const editor = EditorCore.getInstance(); if (this.savedState) {
editor.timeline.updateTracks(this.savedState); const editor = EditorCore.getInstance();
} editor.timeline.updateTracks(this.savedState);
} }
} }
}

View File

@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
import type { ParamValues } from "@/lib/params"; import type { ParamValues } from "@/lib/params";
@ -56,7 +56,7 @@ export class UpdateClipEffectParamsCommand extends Command {
this.params = params; this.params = params;
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
@ -75,6 +75,7 @@ export class UpdateClipEffectParamsCommand extends Command {
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
return undefined;
} }
undo(): void { undo(): void {

View File

@ -10,6 +10,7 @@ export { ToggleElementsVisibilityCommand } from "./toggle-elements-visibility";
export { ToggleElementsMutedCommand } from "./toggle-elements-muted"; export { ToggleElementsMutedCommand } from "./toggle-elements-muted";
export { ToggleSourceAudioSeparationCommand } from "./toggle-source-audio-separation"; export { ToggleSourceAudioSeparationCommand } from "./toggle-source-audio-separation";
export { MoveElementCommand } from "./move-elements"; export { MoveElementCommand } from "./move-elements";
export * from "./keyframes"; export * from "./keyframes";
export * from "./effects"; export * from "./effects";
export * from "./masks"; export * from "./masks";

View File

@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import type { import type {
CreateTimelineElement, CreateTimelineElement,
@ -42,7 +42,7 @@ export class InsertElementCommand extends Command {
private element: CreateTimelineElement; private element: CreateTimelineElement;
private placement: InsertElementPlacement; private placement: InsertElementPlacement;
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();

View File

@ -1,68 +1,69 @@
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { removeEffectParamKeyframe } from "@/lib/animation/effect-param-channel"; import { removeEffectParamKeyframe } from "@/lib/animation/effect-param-channel";
import { updateElementInTracks } from "@/lib/timeline"; import { updateElementInTracks } from "@/lib/timeline";
import { isVisualElement } from "@/lib/timeline/element-utils"; import { isVisualElement } from "@/lib/timeline/element-utils";
import type { TimelineTrack } from "@/lib/timeline"; import type { TimelineTrack } from "@/lib/timeline";
export class RemoveEffectParamKeyframeCommand extends Command { export class RemoveEffectParamKeyframeCommand extends Command {
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
private readonly trackId: string; private readonly trackId: string;
private readonly elementId: string; private readonly elementId: string;
private readonly effectId: string; private readonly effectId: string;
private readonly paramKey: string; private readonly paramKey: string;
private readonly keyframeId: string; private readonly keyframeId: string;
constructor({ constructor({
trackId, trackId,
elementId, elementId,
effectId, effectId,
paramKey, paramKey,
keyframeId, keyframeId,
}: { }: {
trackId: string; trackId: string;
elementId: string; elementId: string;
effectId: string; effectId: string;
paramKey: string; paramKey: string;
keyframeId: string; keyframeId: string;
}) { }) {
super(); super();
this.trackId = trackId; this.trackId = trackId;
this.elementId = elementId; this.elementId = elementId;
this.effectId = effectId; this.effectId = effectId;
this.paramKey = paramKey; this.paramKey = paramKey;
this.keyframeId = keyframeId; this.keyframeId = keyframeId;
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
const updatedTracks = updateElementInTracks({ const updatedTracks = updateElementInTracks({
tracks: this.savedState, tracks: this.savedState,
trackId: this.trackId, trackId: this.trackId,
elementId: this.elementId, elementId: this.elementId,
elementPredicate: isVisualElement, elementPredicate: isVisualElement,
update: (element) => { update: (element) => {
const animations = removeEffectParamKeyframe({ const animations = removeEffectParamKeyframe({
animations: element.animations, animations: element.animations,
effectId: this.effectId, effectId: this.effectId,
paramKey: this.paramKey, paramKey: this.paramKey,
keyframeId: this.keyframeId, keyframeId: this.keyframeId,
}); });
return { ...element, animations }; return { ...element, animations };
}, },
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
} return undefined;
}
undo(): void {
if (!this.savedState) { undo(): void {
return; if (!this.savedState) {
} return;
}
const editor = EditorCore.getInstance();
editor.timeline.updateTracks(this.savedState); const editor = EditorCore.getInstance();
} editor.timeline.updateTracks(this.savedState);
} }
}

View File

@ -1,138 +1,139 @@
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { import {
getChannel, getChannel,
getChannelValueAtTime, getChannelValueAtTime,
removeElementKeyframe, removeElementKeyframe,
resolveAnimationTarget, resolveAnimationTarget,
} from "@/lib/animation"; } from "@/lib/animation";
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { updateElementInTracks } from "@/lib/timeline"; import { updateElementInTracks } from "@/lib/timeline";
import type { AnimationPath, AnimationValue } from "@/lib/animation/types"; import type { AnimationPath, AnimationValue } from "@/lib/animation/types";
import type { TimelineElement, TimelineTrack } from "@/lib/timeline"; import type { TimelineElement, TimelineTrack } from "@/lib/timeline";
function sampleValueBeforeRemoval({ function sampleValueBeforeRemoval({
element, element,
propertyPath, propertyPath,
keyframeId, keyframeId,
}: { }: {
element: TimelineElement; element: TimelineElement;
propertyPath: AnimationPath; propertyPath: AnimationPath;
keyframeId: string; keyframeId: string;
}): AnimationValue | null { }): AnimationValue | null {
const channel = getChannel({ const channel = getChannel({
animations: element.animations, animations: element.animations,
propertyPath, propertyPath,
}); });
const keyframe = channel?.keyframes.find( const keyframe = channel?.keyframes.find(
(candidate) => candidate.id === keyframeId, (candidate) => candidate.id === keyframeId,
); );
if (!channel || !keyframe) { if (!channel || !keyframe) {
return null; return null;
} }
const target = resolveAnimationTarget({ element, path: propertyPath }); const target = resolveAnimationTarget({ element, path: propertyPath });
if (!target) { if (!target) {
return null; return null;
} }
const baseValue = target.getBaseValue(); const baseValue = target.getBaseValue();
if (baseValue === null) { if (baseValue === null) {
return null; return null;
} }
return getChannelValueAtTime({ return getChannelValueAtTime({
channel, channel,
time: keyframe.time, time: keyframe.time,
fallbackValue: baseValue, fallbackValue: baseValue,
}); });
} }
function removeKeyframeAndPersist({ function removeKeyframeAndPersist({
element, element,
propertyPath, propertyPath,
keyframeId, keyframeId,
}: { }: {
element: TimelineElement; element: TimelineElement;
propertyPath: AnimationPath; propertyPath: AnimationPath;
keyframeId: string; keyframeId: string;
}): TimelineElement { }): TimelineElement {
const target = resolveAnimationTarget({ element, path: propertyPath }); const target = resolveAnimationTarget({ element, path: propertyPath });
if (!target) { if (!target) {
return element; return element;
} }
const valueBefore = sampleValueBeforeRemoval({ const valueBefore = sampleValueBeforeRemoval({
element, element,
propertyPath, propertyPath,
keyframeId, keyframeId,
}); });
const nextAnimations = removeElementKeyframe({ const nextAnimations = removeElementKeyframe({
animations: element.animations, animations: element.animations,
propertyPath, propertyPath,
keyframeId, keyframeId,
}); });
const isChannelNowEmpty = const isChannelNowEmpty =
getChannel({ animations: nextAnimations, propertyPath }) === undefined; getChannel({ animations: nextAnimations, propertyPath }) === undefined;
const shouldPersistToBase = isChannelNowEmpty && valueBefore !== null; const shouldPersistToBase = isChannelNowEmpty && valueBefore !== null;
const baseElement = shouldPersistToBase const baseElement = shouldPersistToBase
? target.setBaseValue(valueBefore) ? target.setBaseValue(valueBefore)
: element; : element;
return { ...baseElement, animations: nextAnimations }; return { ...baseElement, animations: nextAnimations };
} }
export class RemoveKeyframeCommand extends Command { export class RemoveKeyframeCommand extends Command {
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
private readonly trackId: string; private readonly trackId: string;
private readonly elementId: string; private readonly elementId: string;
private readonly propertyPath: AnimationPath; private readonly propertyPath: AnimationPath;
private readonly keyframeId: string; private readonly keyframeId: string;
constructor({ constructor({
trackId, trackId,
elementId, elementId,
propertyPath, propertyPath,
keyframeId, keyframeId,
}: { }: {
trackId: string; trackId: string;
elementId: string; elementId: string;
propertyPath: AnimationPath; propertyPath: AnimationPath;
keyframeId: string; keyframeId: string;
}) { }) {
super(); super();
this.trackId = trackId; this.trackId = trackId;
this.elementId = elementId; this.elementId = elementId;
this.propertyPath = propertyPath; this.propertyPath = propertyPath;
this.keyframeId = keyframeId; this.keyframeId = keyframeId;
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
const updatedTracks = updateElementInTracks({ const updatedTracks = updateElementInTracks({
tracks: this.savedState, tracks: this.savedState,
trackId: this.trackId, trackId: this.trackId,
elementId: this.elementId, elementId: this.elementId,
update: (element) => update: (element) =>
removeKeyframeAndPersist({ removeKeyframeAndPersist({
element, element,
propertyPath: this.propertyPath, propertyPath: this.propertyPath,
keyframeId: this.keyframeId, keyframeId: this.keyframeId,
}), }),
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
} return undefined;
}
undo(): void {
if (!this.savedState) { undo(): void {
return; if (!this.savedState) {
} return;
}
const editor = EditorCore.getInstance();
editor.timeline.updateTracks(this.savedState); const editor = EditorCore.getInstance();
} editor.timeline.updateTracks(this.savedState);
} }
}

View File

@ -1,74 +1,75 @@
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { resolveAnimationTarget, retimeElementKeyframe } from "@/lib/animation"; import { resolveAnimationTarget, retimeElementKeyframe } from "@/lib/animation";
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { updateElementInTracks } from "@/lib/timeline"; import { updateElementInTracks } from "@/lib/timeline";
import type { AnimationPath } from "@/lib/animation/types"; import type { AnimationPath } from "@/lib/animation/types";
import type { TimelineTrack } from "@/lib/timeline"; import type { TimelineTrack } from "@/lib/timeline";
export class RetimeKeyframeCommand extends Command { export class RetimeKeyframeCommand extends Command {
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
private readonly trackId: string; private readonly trackId: string;
private readonly elementId: string; private readonly elementId: string;
private readonly propertyPath: AnimationPath; private readonly propertyPath: AnimationPath;
private readonly keyframeId: string; private readonly keyframeId: string;
private readonly nextTime: number; private readonly nextTime: number;
constructor({ constructor({
trackId, trackId,
elementId, elementId,
propertyPath, propertyPath,
keyframeId, keyframeId,
nextTime, nextTime,
}: { }: {
trackId: string; trackId: string;
elementId: string; elementId: string;
propertyPath: AnimationPath; propertyPath: AnimationPath;
keyframeId: string; keyframeId: string;
nextTime: number; nextTime: number;
}) { }) {
super(); super();
this.trackId = trackId; this.trackId = trackId;
this.elementId = elementId; this.elementId = elementId;
this.propertyPath = propertyPath; this.propertyPath = propertyPath;
this.keyframeId = keyframeId; this.keyframeId = keyframeId;
this.nextTime = nextTime; this.nextTime = nextTime;
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
const updatedTracks = updateElementInTracks({ const updatedTracks = updateElementInTracks({
tracks: this.savedState, tracks: this.savedState,
trackId: this.trackId, trackId: this.trackId,
elementId: this.elementId, elementId: this.elementId,
update: (element) => { update: (element) => {
if (!resolveAnimationTarget({ element, path: this.propertyPath })) { if (!resolveAnimationTarget({ element, path: this.propertyPath })) {
return element; return element;
} }
const boundedTime = Math.max(0, Math.min(this.nextTime, element.duration)); const boundedTime = Math.max(0, Math.min(this.nextTime, element.duration));
return { return {
...element, ...element,
animations: retimeElementKeyframe({ animations: retimeElementKeyframe({
animations: element.animations, animations: element.animations,
propertyPath: this.propertyPath, propertyPath: this.propertyPath,
keyframeId: this.keyframeId, keyframeId: this.keyframeId,
time: boundedTime, time: boundedTime,
}), }),
}; };
}, },
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
} return undefined;
}
undo(): void {
if (!this.savedState) { undo(): void {
return; if (!this.savedState) {
} return;
}
const editor = EditorCore.getInstance();
editor.timeline.updateTracks(this.savedState); const editor = EditorCore.getInstance();
} editor.timeline.updateTracks(this.savedState);
} }
}

View File

@ -1,84 +1,85 @@
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { upsertEffectParamKeyframe } from "@/lib/animation/effect-param-channel"; import { upsertEffectParamKeyframe } from "@/lib/animation/effect-param-channel";
import { updateElementInTracks } from "@/lib/timeline"; import { updateElementInTracks } from "@/lib/timeline";
import { isVisualElement } from "@/lib/timeline/element-utils"; import { isVisualElement } from "@/lib/timeline/element-utils";
import type { TimelineTrack } from "@/lib/timeline"; import type { TimelineTrack } from "@/lib/timeline";
export class UpsertEffectParamKeyframeCommand extends Command { export class UpsertEffectParamKeyframeCommand extends Command {
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
private readonly trackId: string; private readonly trackId: string;
private readonly elementId: string; private readonly elementId: string;
private readonly effectId: string; private readonly effectId: string;
private readonly paramKey: string; private readonly paramKey: string;
private readonly time: number; private readonly time: number;
private readonly value: number; private readonly value: number;
private readonly interpolation: "linear" | "hold" | undefined; private readonly interpolation: "linear" | "hold" | undefined;
private readonly keyframeId: string | undefined; private readonly keyframeId: string | undefined;
constructor({ constructor({
trackId, trackId,
elementId, elementId,
effectId, effectId,
paramKey, paramKey,
time, time,
value, value,
interpolation, interpolation,
keyframeId, keyframeId,
}: { }: {
trackId: string; trackId: string;
elementId: string; elementId: string;
effectId: string; effectId: string;
paramKey: string; paramKey: string;
time: number; time: number;
value: number; value: number;
interpolation?: "linear" | "hold"; interpolation?: "linear" | "hold";
keyframeId?: string; keyframeId?: string;
}) { }) {
super(); super();
this.trackId = trackId; this.trackId = trackId;
this.elementId = elementId; this.elementId = elementId;
this.effectId = effectId; this.effectId = effectId;
this.paramKey = paramKey; this.paramKey = paramKey;
this.time = time; this.time = time;
this.value = value; this.value = value;
this.interpolation = interpolation; this.interpolation = interpolation;
this.keyframeId = keyframeId; this.keyframeId = keyframeId;
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
const updatedTracks = updateElementInTracks({ const updatedTracks = updateElementInTracks({
tracks: this.savedState, tracks: this.savedState,
trackId: this.trackId, trackId: this.trackId,
elementId: this.elementId, elementId: this.elementId,
elementPredicate: isVisualElement, elementPredicate: isVisualElement,
update: (element) => { update: (element) => {
const boundedTime = Math.max(0, Math.min(this.time, element.duration)); const boundedTime = Math.max(0, Math.min(this.time, element.duration));
const animations = upsertEffectParamKeyframe({ const animations = upsertEffectParamKeyframe({
animations: element.animations, animations: element.animations,
effectId: this.effectId, effectId: this.effectId,
paramKey: this.paramKey, paramKey: this.paramKey,
time: boundedTime, time: boundedTime,
value: this.value, value: this.value,
interpolation: this.interpolation, interpolation: this.interpolation,
keyframeId: this.keyframeId, keyframeId: this.keyframeId,
}); });
return { ...element, animations }; return { ...element, animations };
}, },
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
} return undefined;
}
undo(): void {
if (!this.savedState) { undo(): void {
return; if (!this.savedState) {
} return;
}
const editor = EditorCore.getInstance();
editor.timeline.updateTracks(this.savedState); const editor = EditorCore.getInstance();
} editor.timeline.updateTracks(this.savedState);
} }
}

View File

@ -1,95 +1,96 @@
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { resolveAnimationTarget, upsertPathKeyframe } from "@/lib/animation"; import { resolveAnimationTarget, upsertPathKeyframe } from "@/lib/animation";
import { updateElementInTracks } from "@/lib/timeline"; import { updateElementInTracks } from "@/lib/timeline";
import type { TimelineTrack } from "@/lib/timeline"; import type { TimelineTrack } from "@/lib/timeline";
import type { import type {
AnimationPath, AnimationPath,
AnimationInterpolation, AnimationInterpolation,
AnimationValue, AnimationValue,
} from "@/lib/animation/types"; } from "@/lib/animation/types";
export class UpsertKeyframeCommand extends Command { export class UpsertKeyframeCommand extends Command {
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
private readonly trackId: string; private readonly trackId: string;
private readonly elementId: string; private readonly elementId: string;
private readonly propertyPath: AnimationPath; private readonly propertyPath: AnimationPath;
private readonly time: number; private readonly time: number;
private readonly value: AnimationValue; private readonly value: AnimationValue;
private readonly interpolation: AnimationInterpolation | undefined; private readonly interpolation: AnimationInterpolation | undefined;
private readonly keyframeId: string | undefined; private readonly keyframeId: string | undefined;
constructor({ constructor({
trackId, trackId,
elementId, elementId,
propertyPath, propertyPath,
time, time,
value, value,
interpolation, interpolation,
keyframeId, keyframeId,
}: { }: {
trackId: string; trackId: string;
elementId: string; elementId: string;
propertyPath: AnimationPath; propertyPath: AnimationPath;
time: number; time: number;
value: AnimationValue; value: AnimationValue;
interpolation?: AnimationInterpolation; interpolation?: AnimationInterpolation;
keyframeId?: string; keyframeId?: string;
}) { }) {
super(); super();
this.trackId = trackId; this.trackId = trackId;
this.elementId = elementId; this.elementId = elementId;
this.propertyPath = propertyPath; this.propertyPath = propertyPath;
this.time = time; this.time = time;
this.value = value; this.value = value;
this.interpolation = interpolation; this.interpolation = interpolation;
this.keyframeId = keyframeId; this.keyframeId = keyframeId;
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
const updatedTracks = updateElementInTracks({ const updatedTracks = updateElementInTracks({
tracks: this.savedState, tracks: this.savedState,
trackId: this.trackId, trackId: this.trackId,
elementId: this.elementId, elementId: this.elementId,
update: (element) => { update: (element) => {
const target = resolveAnimationTarget({ const target = resolveAnimationTarget({
element, element,
path: this.propertyPath, path: this.propertyPath,
}); });
if (!target) { if (!target) {
return element; return element;
} }
const boundedTime = Math.max(0, Math.min(this.time, element.duration)); const boundedTime = Math.max(0, Math.min(this.time, element.duration));
return { return {
...element, ...element,
animations: upsertPathKeyframe({ animations: upsertPathKeyframe({
animations: element.animations, animations: element.animations,
propertyPath: this.propertyPath, propertyPath: this.propertyPath,
time: boundedTime, time: boundedTime,
value: this.value, value: this.value,
interpolation: this.interpolation, interpolation: this.interpolation,
keyframeId: this.keyframeId, keyframeId: this.keyframeId,
valueKind: target.valueKind, valueKind: target.valueKind,
defaultInterpolation: target.defaultInterpolation, defaultInterpolation: target.defaultInterpolation,
numericRange: target.numericRange, numericRange: target.numericRange,
}), }),
}; };
}, },
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
} return undefined;
}
undo(): void {
if (!this.savedState) { undo(): void {
return; if (!this.savedState) {
} return;
}
const editor = EditorCore.getInstance();
editor.timeline.updateTracks(this.savedState); const editor = EditorCore.getInstance();
} editor.timeline.updateTracks(this.savedState);
} }
}

View File

@ -1,64 +1,65 @@
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { isMaskableElement, updateElementInTracks } from "@/lib/timeline"; import { isMaskableElement, updateElementInTracks } from "@/lib/timeline";
import type { TimelineTrack, MaskableElement } from "@/lib/timeline"; import type { TimelineTrack, MaskableElement } from "@/lib/timeline";
function removeMaskFromElement({ function removeMaskFromElement({
element, element,
maskId, maskId,
}: { }: {
element: MaskableElement; element: MaskableElement;
maskId: string; maskId: string;
}): MaskableElement { }): MaskableElement {
const currentMasks = element.masks ?? []; const currentMasks = element.masks ?? [];
const filteredMasks = currentMasks.filter((mask) => mask.id !== maskId); const filteredMasks = currentMasks.filter((mask) => mask.id !== maskId);
return { ...element, masks: filteredMasks }; return { ...element, masks: filteredMasks };
} }
export class RemoveMaskCommand extends Command { export class RemoveMaskCommand extends Command {
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
private readonly trackId: string; private readonly trackId: string;
private readonly elementId: string; private readonly elementId: string;
private readonly maskId: string; private readonly maskId: string;
constructor({ constructor({
trackId, trackId,
elementId, elementId,
maskId, maskId,
}: { }: {
trackId: string; trackId: string;
elementId: string; elementId: string;
maskId: string; maskId: string;
}) { }) {
super(); super();
this.trackId = trackId; this.trackId = trackId;
this.elementId = elementId; this.elementId = elementId;
this.maskId = maskId; this.maskId = maskId;
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
const updatedTracks = updateElementInTracks({ const updatedTracks = updateElementInTracks({
tracks: this.savedState, tracks: this.savedState,
trackId: this.trackId, trackId: this.trackId,
elementId: this.elementId, elementId: this.elementId,
elementPredicate: isMaskableElement, elementPredicate: isMaskableElement,
update: (element) => update: (element) =>
removeMaskFromElement({ removeMaskFromElement({
element: element as MaskableElement, element: element as MaskableElement,
maskId: this.maskId, maskId: this.maskId,
}), }),
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
} return undefined;
}
undo(): void {
if (this.savedState) { undo(): void {
const editor = EditorCore.getInstance(); if (this.savedState) {
editor.timeline.updateTracks(this.savedState); const editor = EditorCore.getInstance();
} editor.timeline.updateTracks(this.savedState);
} }
} }
}

View File

@ -1,75 +1,76 @@
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { isMaskableElement, updateElementInTracks } from "@/lib/timeline"; import { isMaskableElement, updateElementInTracks } from "@/lib/timeline";
import type { Mask } from "@/lib/masks/types"; import type { Mask } from "@/lib/masks/types";
import type { TimelineTrack, MaskableElement } from "@/lib/timeline"; import type { TimelineTrack, MaskableElement } from "@/lib/timeline";
export function toggleMaskInvertedOnElement({ export function toggleMaskInvertedOnElement({
element, element,
maskId, maskId,
}: { }: {
element: MaskableElement; element: MaskableElement;
maskId: string; maskId: string;
}): MaskableElement { }): MaskableElement {
const currentMasks = element.masks ?? []; const currentMasks = element.masks ?? [];
const toggleMask = <TMask extends Mask>(mask: TMask): TMask => ({ const toggleMask = <TMask extends Mask>(mask: TMask): TMask => ({
...mask, ...mask,
params: { params: {
...mask.params, ...mask.params,
inverted: !mask.params.inverted, inverted: !mask.params.inverted,
}, },
}); });
const updatedMasks = currentMasks.map((mask) => const updatedMasks = currentMasks.map((mask) =>
mask.id !== maskId ? mask : toggleMask(mask), mask.id !== maskId ? mask : toggleMask(mask),
); );
return { ...element, masks: updatedMasks }; return { ...element, masks: updatedMasks };
} }
export class ToggleMaskInvertedCommand extends Command { export class ToggleMaskInvertedCommand extends Command {
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
private readonly trackId: string; private readonly trackId: string;
private readonly elementId: string; private readonly elementId: string;
private readonly maskId: string; private readonly maskId: string;
constructor({ constructor({
trackId, trackId,
elementId, elementId,
maskId, maskId,
}: { }: {
trackId: string; trackId: string;
elementId: string; elementId: string;
maskId: string; maskId: string;
}) { }) {
super(); super();
this.trackId = trackId; this.trackId = trackId;
this.elementId = elementId; this.elementId = elementId;
this.maskId = maskId; this.maskId = maskId;
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
const updatedTracks = updateElementInTracks({ const updatedTracks = updateElementInTracks({
tracks: this.savedState, tracks: this.savedState,
trackId: this.trackId, trackId: this.trackId,
elementId: this.elementId, elementId: this.elementId,
elementPredicate: isMaskableElement, elementPredicate: isMaskableElement,
update: (element) => update: (element) =>
toggleMaskInvertedOnElement({ toggleMaskInvertedOnElement({
element: element as MaskableElement, element: element as MaskableElement,
maskId: this.maskId, maskId: this.maskId,
}), }),
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
} return undefined;
}
undo(): void {
if (this.savedState) { undo(): void {
const editor = EditorCore.getInstance(); if (this.savedState) {
editor.timeline.updateTracks(this.savedState); const editor = EditorCore.getInstance();
} editor.timeline.updateTracks(this.savedState);
} }
} }
}

View File

@ -1,145 +1,146 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import type { import type {
TimelineTrack, TimelineTrack,
TimelineElement, TimelineElement,
TrackType, TrackType,
} from "@/lib/timeline"; } from "@/lib/timeline";
import { import {
buildEmptyTrack, buildEmptyTrack,
validateElementTrackCompatibility, validateElementTrackCompatibility,
enforceMainTrackStart, enforceMainTrackStart,
} from "@/lib/timeline/placement"; } from "@/lib/timeline/placement";
import { rippleShiftElements } from "@/lib/timeline/ripple-utils"; import { rippleShiftElements } from "@/lib/timeline/ripple-utils";
export class MoveElementCommand extends Command { export class MoveElementCommand extends Command {
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
private readonly sourceTrackId: string; private readonly sourceTrackId: string;
private readonly targetTrackId: string; private readonly targetTrackId: string;
private readonly elementId: string; private readonly elementId: string;
private readonly newStartTime: number; private readonly newStartTime: number;
private readonly createTrack: { type: TrackType; index: number } | undefined; private readonly createTrack: { type: TrackType; index: number } | undefined;
private readonly rippleEnabled: boolean; private readonly rippleEnabled: boolean;
constructor({ constructor({
sourceTrackId, sourceTrackId,
targetTrackId, targetTrackId,
elementId, elementId,
newStartTime, newStartTime,
createTrack, createTrack,
rippleEnabled = false, rippleEnabled = false,
}: { }: {
sourceTrackId: string; sourceTrackId: string;
targetTrackId: string; targetTrackId: string;
elementId: string; elementId: string;
newStartTime: number; newStartTime: number;
createTrack?: { type: TrackType; index: number }; createTrack?: { type: TrackType; index: number };
rippleEnabled?: boolean; rippleEnabled?: boolean;
}) { }) {
super(); super();
this.sourceTrackId = sourceTrackId; this.sourceTrackId = sourceTrackId;
this.targetTrackId = targetTrackId; this.targetTrackId = targetTrackId;
this.elementId = elementId; this.elementId = elementId;
this.newStartTime = newStartTime; this.newStartTime = newStartTime;
this.createTrack = createTrack; this.createTrack = createTrack;
this.rippleEnabled = rippleEnabled; this.rippleEnabled = rippleEnabled;
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
const sourceTrack = this.savedState.find( const sourceTrack = this.savedState.find(
(track) => track.id === this.sourceTrackId, (track) => track.id === this.sourceTrackId,
); );
const element = sourceTrack?.elements.find( const element = sourceTrack?.elements.find(
(trackElement) => trackElement.id === this.elementId, (trackElement) => trackElement.id === this.elementId,
); );
if (!sourceTrack || !element) { if (!sourceTrack || !element) {
throw new Error("Source track or element not found"); throw new Error("Source track or element not found");
} }
let targetTrack = this.savedState.find((track) => track.id === this.targetTrackId); let targetTrack = this.savedState.find((track) => track.id === this.targetTrackId);
let tracksToUpdate = this.savedState; let tracksToUpdate = this.savedState;
if (!targetTrack && this.createTrack) { if (!targetTrack && this.createTrack) {
const newTrack = buildEmptyTrack({ const newTrack = buildEmptyTrack({
id: this.targetTrackId, id: this.targetTrackId,
type: this.createTrack.type, type: this.createTrack.type,
}); });
tracksToUpdate = [...this.savedState]; tracksToUpdate = [...this.savedState];
tracksToUpdate.splice(this.createTrack.index, 0, newTrack); tracksToUpdate.splice(this.createTrack.index, 0, newTrack);
targetTrack = newTrack; targetTrack = newTrack;
} }
if (!targetTrack) { if (!targetTrack) {
throw new Error("Target track not found"); throw new Error("Target track not found");
} }
const validation = validateElementTrackCompatibility({ const validation = validateElementTrackCompatibility({
element, element,
track: targetTrack, track: targetTrack,
}); });
if (!validation.isValid) { if (!validation.isValid) {
throw new Error(validation.errorMessage); throw new Error(validation.errorMessage);
} }
const adjustedStartTime = enforceMainTrackStart({ const adjustedStartTime = enforceMainTrackStart({
tracks: tracksToUpdate, tracks: tracksToUpdate,
targetTrackId: this.targetTrackId, targetTrackId: this.targetTrackId,
requestedStartTime: this.newStartTime, requestedStartTime: this.newStartTime,
excludeElementId: this.elementId, excludeElementId: this.elementId,
}); });
// keyframe times remain clip-local, so moving only changes element startTime. // keyframe times remain clip-local, so moving only changes element startTime.
const movedElement: TimelineElement = { const movedElement: TimelineElement = {
...element, ...element,
startTime: adjustedStartTime, startTime: adjustedStartTime,
}; };
const isSameTrack = this.sourceTrackId === this.targetTrackId; const isSameTrack = this.sourceTrackId === this.targetTrackId;
const updatedTracks = tracksToUpdate.map((track): TimelineTrack => { const updatedTracks = tracksToUpdate.map((track): TimelineTrack => {
if (isSameTrack && track.id === this.sourceTrackId) { if (isSameTrack && track.id === this.sourceTrackId) {
return { return {
...track, ...track,
elements: track.elements.map((trackElement) => elements: track.elements.map((trackElement) =>
trackElement.id === this.elementId ? movedElement : trackElement, trackElement.id === this.elementId ? movedElement : trackElement,
), ),
} as typeof track; } as typeof track;
} }
if (track.id === this.sourceTrackId) { if (track.id === this.sourceTrackId) {
const remainingElements = track.elements.filter( const remainingElements = track.elements.filter(
(trackElement) => trackElement.id !== this.elementId, (trackElement) => trackElement.id !== this.elementId,
); );
const shiftedElements = this.rippleEnabled const shiftedElements = this.rippleEnabled
? rippleShiftElements({ ? rippleShiftElements({
elements: remainingElements, elements: remainingElements,
afterTime: element.startTime, afterTime: element.startTime,
shiftAmount: element.duration, shiftAmount: element.duration,
}) })
: remainingElements; : remainingElements;
return { ...track, elements: shiftedElements } as typeof track; return { ...track, elements: shiftedElements } as typeof track;
} }
if (track.id === this.targetTrackId) { if (track.id === this.targetTrackId) {
return { return {
...track, ...track,
elements: [...track.elements, movedElement], elements: [...track.elements, movedElement],
} as typeof track; } as typeof track;
} }
return track; return track;
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
} return undefined;
}
undo(): void {
if (this.savedState) { undo(): void {
const editor = EditorCore.getInstance(); if (this.savedState) {
editor.timeline.updateTracks(this.savedState); const editor = EditorCore.getInstance();
} editor.timeline.updateTracks(this.savedState);
} }
} }
}

View File

@ -1,114 +1,115 @@
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { clampRetimeRate } from "@/lib/retime/rate"; import { clampRetimeRate } from "@/lib/retime/rate";
import { clampAnimationsToDuration } from "@/lib/animation"; import { clampAnimationsToDuration } from "@/lib/animation";
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { getTimelineDurationForSourceSpan, getSourceSpanAtClipTime } from "@/lib/retime"; import { getTimelineDurationForSourceSpan, getSourceSpanAtClipTime } from "@/lib/retime";
import { isRetimableElement, updateElementInTracks } from "@/lib/timeline"; import { isRetimableElement, updateElementInTracks } from "@/lib/timeline";
import type { RetimeConfig, TimelineTrack } from "@/lib/timeline"; import type { RetimeConfig, TimelineTrack } from "@/lib/timeline";
function getSourceDuration({ function getSourceDuration({
trimStart, trimStart,
trimEnd, trimEnd,
duration, duration,
sourceDuration, sourceDuration,
retime, retime,
}: { }: {
trimStart: number; trimStart: number;
trimEnd: number; trimEnd: number;
duration: number; duration: number;
sourceDuration?: number; sourceDuration?: number;
retime?: RetimeConfig; retime?: RetimeConfig;
}): number { }): number {
if (typeof sourceDuration === "number") { if (typeof sourceDuration === "number") {
return sourceDuration; return sourceDuration;
} }
return ( return (
trimStart + trimStart +
getSourceSpanAtClipTime({ getSourceSpanAtClipTime({
clipTime: duration, clipTime: duration,
retime, retime,
}) + }) +
trimEnd trimEnd
); );
} }
export class UpdateElementRetimeCommand extends Command { export class UpdateElementRetimeCommand extends Command {
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
private readonly trackId: string; private readonly trackId: string;
private readonly elementId: string; private readonly elementId: string;
private readonly retime: RetimeConfig | undefined; private readonly retime: RetimeConfig | undefined;
constructor({ constructor({
trackId, trackId,
elementId, elementId,
retime, retime,
}: { }: {
trackId: string; trackId: string;
elementId: string; elementId: string;
retime?: RetimeConfig; retime?: RetimeConfig;
}) { }) {
super(); super();
this.trackId = trackId; this.trackId = trackId;
this.elementId = elementId; this.elementId = elementId;
this.retime = retime; this.retime = retime;
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
const updatedTracks = updateElementInTracks({ const updatedTracks = updateElementInTracks({
tracks: this.savedState, tracks: this.savedState,
trackId: this.trackId, trackId: this.trackId,
elementId: this.elementId, elementId: this.elementId,
elementPredicate: isRetimableElement, elementPredicate: isRetimableElement,
update: (element) => { update: (element) => {
if (!isRetimableElement(element)) { if (!isRetimableElement(element)) {
return element; return element;
} }
const nextRetime = this.retime const nextRetime = this.retime
? { ? {
...this.retime, ...this.retime,
rate: clampRetimeRate({ rate: this.retime.rate }), rate: clampRetimeRate({ rate: this.retime.rate }),
} }
: undefined; : undefined;
const sourceDuration = getSourceDuration({ const sourceDuration = getSourceDuration({
trimStart: element.trimStart, trimStart: element.trimStart,
trimEnd: element.trimEnd, trimEnd: element.trimEnd,
duration: element.duration, duration: element.duration,
sourceDuration: element.sourceDuration, sourceDuration: element.sourceDuration,
retime: element.retime, retime: element.retime,
}); });
const visibleSourceSpan = Math.max( const visibleSourceSpan = Math.max(
0, 0,
sourceDuration - element.trimStart - element.trimEnd, sourceDuration - element.trimStart - element.trimEnd,
); );
const nextDuration = getTimelineDurationForSourceSpan({ const nextDuration = getTimelineDurationForSourceSpan({
sourceSpan: visibleSourceSpan, sourceSpan: visibleSourceSpan,
retime: nextRetime, retime: nextRetime,
}); });
return { return {
...element, ...element,
retime: nextRetime, retime: nextRetime,
duration: nextDuration, duration: nextDuration,
animations: clampAnimationsToDuration({ animations: clampAnimationsToDuration({
animations: element.animations, animations: element.animations,
duration: nextDuration, duration: nextDuration,
}), }),
}; };
}, },
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
} return undefined;
}
undo(): void {
if (this.savedState) { undo(): void {
const editor = EditorCore.getInstance(); if (this.savedState) {
editor.timeline.updateTracks(this.savedState); const editor = EditorCore.getInstance();
} editor.timeline.updateTracks(this.savedState);
} }
} }
}

View File

@ -175,6 +175,7 @@ export class SplitElementsCommand extends Command {
select: this.rightSideElements, select: this.rightSideElements,
}; };
} }
return undefined;
} }
undo(): void { undo(): void {

View File

@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline"; import type { TimelineTrack } from "@/lib/timeline";
import { canElementHaveAudio } from "@/lib/timeline/element-utils"; import { canElementHaveAudio } from "@/lib/timeline/element-utils";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
@ -10,7 +10,7 @@ export class ToggleElementsMutedCommand extends Command {
super(); super();
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();

View File

@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline"; import type { TimelineTrack } from "@/lib/timeline";
import { canElementBeHidden } from "@/lib/timeline/element-utils"; import { canElementBeHidden } from "@/lib/timeline/element-utils";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
@ -10,7 +10,7 @@ export class ToggleElementsVisibilityCommand extends Command {
super(); super();
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
@ -36,6 +36,7 @@ export class ToggleElementsVisibilityCommand extends Command {
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
return undefined;
} }
undo(): void { undo(): void {

View File

@ -1,5 +1,5 @@
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { import {
buildSeparatedAudioElement, buildSeparatedAudioElement,
canExtractSourceAudio, canExtractSourceAudio,
@ -25,7 +25,7 @@ export class ToggleSourceAudioSeparationCommand extends Command {
super(); super();
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
@ -43,6 +43,9 @@ export class ToggleSourceAudioSeparationCommand extends Command {
if (!sourceElement) { if (!sourceElement) {
return; return;
} }
if (sourceElement.type !== "video") {
return;
}
if (canRecoverSourceAudio({ element: sourceElement })) { if (canRecoverSourceAudio({ element: sourceElement })) {
editor.timeline.updateTracks( editor.timeline.updateTracks(
@ -59,9 +62,7 @@ export class ToggleSourceAudioSeparationCommand extends Command {
const mediaAsset = editor const mediaAsset = editor
.media .media
.getAssets() .getAssets()
.find((asset) => .find((asset) => asset.id === sourceElement.mediaId);
sourceElement.type === "video" ? asset.id === sourceElement.mediaId : false,
);
if (!canExtractSourceAudio({ element: sourceElement, mediaAsset })) { if (!canExtractSourceAudio({ element: sourceElement, mediaAsset })) {
return; return;
} }

View File

@ -1,57 +1,58 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline"; import type { TimelineTrack } from "@/lib/timeline";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { clampAnimationsToDuration } from "@/lib/animation"; import { clampAnimationsToDuration } from "@/lib/animation";
export class UpdateElementDurationCommand extends Command { export class UpdateElementDurationCommand extends Command {
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
private readonly trackId: string; private readonly trackId: string;
private readonly elementId: string; private readonly elementId: string;
private readonly duration: number; private readonly duration: number;
constructor({ constructor({
trackId, trackId,
elementId, elementId,
duration, duration,
}: { }: {
trackId: string; trackId: string;
elementId: string; elementId: string;
duration: number; duration: number;
}) { }) {
super(); super();
this.trackId = trackId; this.trackId = trackId;
this.elementId = elementId; this.elementId = elementId;
this.duration = duration; this.duration = duration;
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
const updatedTracks = this.savedState.map((track) => { const updatedTracks = this.savedState.map((track) => {
if (track.id !== this.trackId) return track; if (track.id !== this.trackId) return track;
const newElements = track.elements.map((element) => const newElements = track.elements.map((element) =>
element.id === this.elementId element.id === this.elementId
? { ? {
...element, ...element,
duration: this.duration, duration: this.duration,
animations: clampAnimationsToDuration({ animations: clampAnimationsToDuration({
animations: element.animations, animations: element.animations,
duration: this.duration, duration: this.duration,
}), }),
} }
: element, : element,
); );
return { ...track, elements: newElements } as typeof track; return { ...track, elements: newElements } as typeof track;
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
} return undefined;
}
undo(): void {
if (this.savedState) { undo(): void {
const editor = EditorCore.getInstance(); if (this.savedState) {
editor.timeline.updateTracks(this.savedState); const editor = EditorCore.getInstance();
} editor.timeline.updateTracks(this.savedState);
} }
} }
}

View File

@ -1,69 +1,70 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline"; import type { TimelineTrack } from "@/lib/timeline";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { enforceMainTrackStart } from "@/lib/timeline/placement"; import { enforceMainTrackStart } from "@/lib/timeline/placement";
export class UpdateElementStartTimeCommand extends Command { export class UpdateElementStartTimeCommand extends Command {
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
private readonly elements: { trackId: string; elementId: string }[]; private readonly elements: { trackId: string; elementId: string }[];
private readonly startTime: number; private readonly startTime: number;
constructor({ constructor({
elements, elements,
startTime, startTime,
}: { }: {
elements: { trackId: string; elementId: string }[]; elements: { trackId: string; elementId: string }[];
startTime: number; startTime: number;
}) { }) {
super(); super();
this.elements = elements; this.elements = elements;
this.startTime = startTime; this.startTime = startTime;
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
const currentTracks = this.savedState; const currentTracks = this.savedState;
const updatedTracks = currentTracks.map((track) => { const updatedTracks = currentTracks.map((track) => {
const hasElementsToUpdate = this.elements.some( const hasElementsToUpdate = this.elements.some(
(elementEntry) => elementEntry.trackId === track.id, (elementEntry) => elementEntry.trackId === track.id,
); );
if (!hasElementsToUpdate) { if (!hasElementsToUpdate) {
return track; return track;
} }
const newElements = track.elements.map((element) => { const newElements = track.elements.map((element) => {
const shouldUpdate = this.elements.some( const shouldUpdate = this.elements.some(
(elementEntry) => (elementEntry) =>
elementEntry.elementId === element.id && elementEntry.elementId === element.id &&
elementEntry.trackId === track.id, elementEntry.trackId === track.id,
); );
if (!shouldUpdate) { if (!shouldUpdate) {
return element; return element;
} }
const baseStartTime = Math.max(0, this.startTime); const baseStartTime = Math.max(0, this.startTime);
const adjustedStartTime = enforceMainTrackStart({ const adjustedStartTime = enforceMainTrackStart({
tracks: currentTracks, tracks: currentTracks,
targetTrackId: track.id, targetTrackId: track.id,
requestedStartTime: baseStartTime, requestedStartTime: baseStartTime,
excludeElementId: element.id, excludeElementId: element.id,
}); });
return { ...element, startTime: adjustedStartTime }; return { ...element, startTime: adjustedStartTime };
}); });
return { ...track, elements: newElements } as typeof track; return { ...track, elements: newElements } as typeof track;
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
} return undefined;
}
undo(): void {
if (this.savedState) { undo(): void {
const editor = EditorCore.getInstance(); if (this.savedState) {
editor.timeline.updateTracks(this.savedState); const editor = EditorCore.getInstance();
} editor.timeline.updateTracks(this.savedState);
} }
} }
}

View File

@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline"; import type { TimelineTrack } from "@/lib/timeline";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { clampAnimationsToDuration } from "@/lib/animation"; import { clampAnimationsToDuration } from "@/lib/animation";
@ -38,7 +38,7 @@ export class UpdateElementTrimCommand extends Command {
this.rippleEnabled = rippleEnabled; this.rippleEnabled = rippleEnabled;
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
@ -104,6 +104,7 @@ export class UpdateElementTrimCommand extends Command {
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
return undefined;
} }
undo(): void { undo(): void {

View File

@ -1,47 +1,48 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineElement, TimelineTrack } from "@/lib/timeline"; import type { TimelineElement, TimelineTrack } from "@/lib/timeline";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { updateElementInTracks } from "@/lib/timeline"; import { updateElementInTracks } from "@/lib/timeline";
export class UpdateElementCommand extends Command { export class UpdateElementCommand extends Command {
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
private readonly trackId: string; private readonly trackId: string;
private readonly elementId: string; private readonly elementId: string;
private readonly updates: Partial<TimelineElement>; private readonly updates: Partial<TimelineElement>;
constructor({ constructor({
trackId, trackId,
elementId, elementId,
updates, updates,
}: { }: {
trackId: string; trackId: string;
elementId: string; elementId: string;
updates: Partial<TimelineElement>; updates: Partial<TimelineElement>;
}) { }) {
super(); super();
this.trackId = trackId; this.trackId = trackId;
this.elementId = elementId; this.elementId = elementId;
this.updates = updates; this.updates = updates;
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
const updatedTracks = updateElementInTracks({ const updatedTracks = updateElementInTracks({
tracks: this.savedState, tracks: this.savedState,
trackId: this.trackId, trackId: this.trackId,
elementId: this.elementId, elementId: this.elementId,
update: (element) => ({ ...element, ...this.updates }) as TimelineElement, update: (element) => ({ ...element, ...this.updates }) as TimelineElement,
}); });
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
} return undefined;
}
undo(): void {
if (this.savedState) { undo(): void {
const editor = EditorCore.getInstance(); if (this.savedState) {
editor.timeline.updateTracks(this.savedState); const editor = EditorCore.getInstance();
} editor.timeline.updateTracks(this.savedState);
} }
} }
}

View File

@ -1,53 +1,54 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TrackType, TimelineTrack } from "@/lib/timeline"; import type { TrackType, TimelineTrack } from "@/lib/timeline";
import { generateUUID } from "@/utils/id"; import { generateUUID } from "@/utils/id";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { import {
buildEmptyTrack, buildEmptyTrack,
getDefaultInsertIndexForTrack, getDefaultInsertIndexForTrack,
} from "@/lib/timeline/placement"; } from "@/lib/timeline/placement";
export class AddTrackCommand extends Command { export class AddTrackCommand extends Command {
private trackId: string; private trackId: string;
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
constructor( constructor(
private type: TrackType, private type: TrackType,
private index?: number, private index?: number,
) { ) {
super(); super();
this.trackId = generateUUID(); this.trackId = generateUUID();
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
const newTrack: TimelineTrack = buildEmptyTrack({ const newTrack: TimelineTrack = buildEmptyTrack({
id: this.trackId, id: this.trackId,
type: this.type, type: this.type,
}); });
const updatedTracks = [...(this.savedState || [])]; const updatedTracks = [...(this.savedState || [])];
const insertIndex = const insertIndex =
this.index ?? this.index ??
getDefaultInsertIndexForTrack({ getDefaultInsertIndexForTrack({
tracks: updatedTracks, tracks: updatedTracks,
trackType: this.type, trackType: this.type,
}); });
updatedTracks.splice(insertIndex, 0, newTrack); updatedTracks.splice(insertIndex, 0, newTrack);
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
} return undefined;
}
undo(): void {
if (this.savedState) { undo(): void {
const editor = EditorCore.getInstance(); if (this.savedState) {
editor.timeline.updateTracks(this.savedState); const editor = EditorCore.getInstance();
} editor.timeline.updateTracks(this.savedState);
} }
}
getTrackId(): string {
return this.trackId; getTrackId(): string {
} return this.trackId;
} }
}

View File

@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import type { TimelineTrack } from "@/lib/timeline"; import type { TimelineTrack } from "@/lib/timeline";
import { getMainTrack } from "@/lib/timeline/placement"; import { getMainTrack } from "@/lib/timeline/placement";
@ -10,7 +10,7 @@ export class RemoveTrackCommand extends Command {
super(); super();
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
const targetTrack = this.savedState.find( const targetTrack = this.savedState.find(

View File

@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline"; import type { TimelineTrack } from "@/lib/timeline";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { canTrackHaveAudio } from "@/lib/timeline"; import { canTrackHaveAudio } from "@/lib/timeline";
@ -10,7 +10,7 @@ export class ToggleTrackMuteCommand extends Command {
super(); super();
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();

View File

@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline"; import type { TimelineTrack } from "@/lib/timeline";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { canTrackBeHidden } from "@/lib/timeline"; import { canTrackBeHidden } from "@/lib/timeline";
@ -10,7 +10,7 @@ export class ToggleTrackVisibilityCommand extends Command {
super(); super();
} }
execute(): void { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();

View File

@ -1,20 +1,21 @@
import { Command } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline"; import type { TimelineTrack } from "@/lib/timeline";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
export class TracksSnapshotCommand extends Command { export class TracksSnapshotCommand extends Command {
constructor( constructor(
private before: TimelineTrack[], private before: TimelineTrack[],
private after: TimelineTrack[], private after: TimelineTrack[],
) { ) {
super(); super();
} }
execute(): void { execute(): CommandResult | undefined {
EditorCore.getInstance().timeline.updateTracks(this.after); EditorCore.getInstance().timeline.updateTracks(this.after);
} return undefined;
}
undo(): void {
EditorCore.getInstance().timeline.updateTracks(this.before); undo(): void {
} EditorCore.getInstance().timeline.updateTracks(this.before);
} }
}