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 { toast } from "sonner";
import type { MediaAsset } from "@/lib/media/types";
@ -23,7 +23,7 @@ export class AddMediaAssetCommand extends Command {
this.assetId = generateUUID();
}
execute(): void {
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
this.savedAssets = [...editor.media.getAssets()];
@ -80,6 +80,8 @@ export class AddMediaAssetCommand extends Command {
});
}
});
return undefined;
}
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 type { MediaAsset } from "@/lib/media/types";
import { storageService } from "@/services/storage/service";
@ -18,7 +18,7 @@ export class RemoveMediaAssetCommand extends Command {
super();
}
execute(): void {
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
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 type { TProject, TProjectSettings } from "@/lib/project/types";
@ -10,7 +10,7 @@ export class UpdateProjectSettingsCommand extends Command {
super();
}
execute(): void {
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
const activeProject = editor.project.getActive();
if (!activeProject) return;

View File

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

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 type { TScene } from "@/lib/timeline";
import { canDeleteScene, getFallbackSceneAfterDelete } from "@/lib/scenes";
@ -12,7 +12,7 @@ export class DeleteSceneCommand extends Command {
super();
}
execute(): void {
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
const scenes = editor.scenes.getScenes();
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 type { TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes";
@ -14,7 +14,7 @@ export class MoveBookmarkCommand extends Command {
super();
}
execute(): void {
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
const activeScene = editor.scenes.getActiveScene();
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 type { TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes";
@ -15,7 +15,7 @@ export class RemoveBookmarkCommand extends Command {
super();
}
execute(): void {
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
const activeScene = editor.scenes.getActiveScene();
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 type { TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes";
@ -14,7 +14,7 @@ export class RenameSceneCommand extends Command {
super();
}
execute(): void {
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
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 type { TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes";
@ -12,7 +12,7 @@ export class ToggleBookmarkCommand extends Command {
super();
}
execute(): void {
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
const activeScene = editor.scenes.getActiveScene();
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 type { Bookmark, TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes";
@ -14,7 +14,7 @@ export class UpdateBookmarkCommand extends Command {
super();
}
execute(): void {
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
const activeScene = editor.scenes.getActiveScene();
const activeProject = editor.project.getActive();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -10,6 +10,7 @@ export { ToggleElementsVisibilityCommand } from "./toggle-elements-visibility";
export { ToggleElementsMutedCommand } from "./toggle-elements-muted";
export { ToggleSourceAudioSeparationCommand } from "./toggle-source-audio-separation";
export { MoveElementCommand } from "./move-elements";
export * from "./keyframes";
export * from "./effects";
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 type {
CreateTimelineElement,
@ -42,7 +42,7 @@ export class InsertElementCommand extends Command {
private element: CreateTimelineElement;
private placement: InsertElementPlacement;
execute(): void {
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,47 +1,48 @@
import { Command } from "@/lib/commands/base-command";
import type { TimelineElement, TimelineTrack } from "@/lib/timeline";
import { EditorCore } from "@/core";
import { updateElementInTracks } from "@/lib/timeline";
export class UpdateElementCommand extends Command {
private savedState: TimelineTrack[] | null = null;
private readonly trackId: string;
private readonly elementId: string;
private readonly updates: Partial<TimelineElement>;
constructor({
trackId,
elementId,
updates,
}: {
trackId: string;
elementId: string;
updates: Partial<TimelineElement>;
}) {
super();
this.trackId = trackId;
this.elementId = elementId;
this.updates = updates;
}
execute(): void {
const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks();
const updatedTracks = updateElementInTracks({
tracks: this.savedState,
trackId: this.trackId,
elementId: this.elementId,
update: (element) => ({ ...element, ...this.updates }) as TimelineElement,
});
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 { TimelineElement, TimelineTrack } from "@/lib/timeline";
import { EditorCore } from "@/core";
import { updateElementInTracks } from "@/lib/timeline";
export class UpdateElementCommand extends Command {
private savedState: TimelineTrack[] | null = null;
private readonly trackId: string;
private readonly elementId: string;
private readonly updates: Partial<TimelineElement>;
constructor({
trackId,
elementId,
updates,
}: {
trackId: string;
elementId: string;
updates: Partial<TimelineElement>;
}) {
super();
this.trackId = trackId;
this.elementId = elementId;
this.updates = updates;
}
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks();
const updatedTracks = updateElementInTracks({
tracks: this.savedState,
trackId: this.trackId,
elementId: this.elementId,
update: (element) => ({ ...element, ...this.updates }) as TimelineElement,
});
editor.timeline.updateTracks(updatedTracks);
return undefined;
}
undo(): void {
if (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 type { TrackType, TimelineTrack } from "@/lib/timeline";
import { generateUUID } from "@/utils/id";
import { EditorCore } from "@/core";
import {
buildEmptyTrack,
getDefaultInsertIndexForTrack,
} from "@/lib/timeline/placement";
export class AddTrackCommand extends Command {
private trackId: string;
private savedState: TimelineTrack[] | null = null;
constructor(
private type: TrackType,
private index?: number,
) {
super();
this.trackId = generateUUID();
}
execute(): void {
const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks();
const newTrack: TimelineTrack = buildEmptyTrack({
id: this.trackId,
type: this.type,
});
const updatedTracks = [...(this.savedState || [])];
const insertIndex =
this.index ??
getDefaultInsertIndexForTrack({
tracks: updatedTracks,
trackType: this.type,
});
updatedTracks.splice(insertIndex, 0, newTrack);
editor.timeline.updateTracks(updatedTracks);
}
undo(): void {
if (this.savedState) {
const editor = EditorCore.getInstance();
editor.timeline.updateTracks(this.savedState);
}
}
getTrackId(): string {
return this.trackId;
}
}
import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TrackType, TimelineTrack } from "@/lib/timeline";
import { generateUUID } from "@/utils/id";
import { EditorCore } from "@/core";
import {
buildEmptyTrack,
getDefaultInsertIndexForTrack,
} from "@/lib/timeline/placement";
export class AddTrackCommand extends Command {
private trackId: string;
private savedState: TimelineTrack[] | null = null;
constructor(
private type: TrackType,
private index?: number,
) {
super();
this.trackId = generateUUID();
}
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks();
const newTrack: TimelineTrack = buildEmptyTrack({
id: this.trackId,
type: this.type,
});
const updatedTracks = [...(this.savedState || [])];
const insertIndex =
this.index ??
getDefaultInsertIndexForTrack({
tracks: updatedTracks,
trackType: this.type,
});
updatedTracks.splice(insertIndex, 0, newTrack);
editor.timeline.updateTracks(updatedTracks);
return undefined;
}
undo(): void {
if (this.savedState) {
const editor = EditorCore.getInstance();
editor.timeline.updateTracks(this.savedState);
}
}
getTrackId(): string {
return this.trackId;
}
}

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 type { TimelineTrack } from "@/lib/timeline";
import { getMainTrack } from "@/lib/timeline/placement";
@ -10,7 +10,7 @@ export class RemoveTrackCommand extends Command {
super();
}
execute(): void {
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks();
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 { EditorCore } from "@/core";
import { canTrackHaveAudio } from "@/lib/timeline";
@ -10,7 +10,7 @@ export class ToggleTrackMuteCommand extends Command {
super();
}
execute(): void {
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
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 { EditorCore } from "@/core";
import { canTrackBeHidden } from "@/lib/timeline";
@ -10,7 +10,7 @@ export class ToggleTrackVisibilityCommand extends Command {
super();
}
execute(): void {
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks();

View File

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