refactor: update command execute methods to return CommandResult | undefined
This commit is contained in:
parent
3516bd69f4
commit
1a8a1e889a
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 ?? "";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export class DuplicateElementsCommand extends Command {
|
|||
select: this.duplicatedElements,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,6 +175,7 @@ export class SplitElementsCommand extends Command {
|
|||
select: this.rightSideElements,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue