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 { EditorCore } from "@/core";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { MediaAsset } from "@/lib/media/types";
|
import type { MediaAsset } from "@/lib/media/types";
|
||||||
|
|
@ -23,7 +23,7 @@ export class AddMediaAssetCommand extends Command {
|
||||||
this.assetId = generateUUID();
|
this.assetId = generateUUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedAssets = [...editor.media.getAssets()];
|
this.savedAssets = [...editor.media.getAssets()];
|
||||||
|
|
||||||
|
|
@ -80,6 +80,8 @@ export class AddMediaAssetCommand extends Command {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
undo(): void {
|
undo(): void {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import type { MediaAsset } from "@/lib/media/types";
|
import type { MediaAsset } from "@/lib/media/types";
|
||||||
import { storageService } from "@/services/storage/service";
|
import { storageService } from "@/services/storage/service";
|
||||||
|
|
@ -18,7 +18,7 @@ export class RemoveMediaAssetCommand extends Command {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
const assets = editor.media.getAssets();
|
const assets = editor.media.getAssets();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import type { TProject, TProjectSettings } from "@/lib/project/types";
|
import type { TProject, TProjectSettings } from "@/lib/project/types";
|
||||||
|
|
||||||
|
|
@ -10,7 +10,7 @@ export class UpdateProjectSettingsCommand extends Command {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
const activeProject = editor.project.getActive();
|
const activeProject = editor.project.getActive();
|
||||||
if (!activeProject) return;
|
if (!activeProject) return;
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,41 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import type { TScene } from "@/lib/timeline";
|
import type { TScene } from "@/lib/timeline";
|
||||||
import { buildDefaultScene } from "@/lib/scenes";
|
import { buildDefaultScene } from "@/lib/scenes";
|
||||||
|
|
||||||
export class CreateSceneCommand extends Command {
|
export class CreateSceneCommand extends Command {
|
||||||
private savedScenes: TScene[] | null = null;
|
private savedScenes: TScene[] | null = null;
|
||||||
private createdScene: TScene | null = null;
|
private createdScene: TScene | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private name: string,
|
private name: string,
|
||||||
private isMain: boolean = false,
|
private isMain: boolean = false,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedScenes = [...editor.scenes.getScenes()];
|
this.savedScenes = [...editor.scenes.getScenes()];
|
||||||
|
|
||||||
this.createdScene = buildDefaultScene({
|
this.createdScene = buildDefaultScene({
|
||||||
name: this.name,
|
name: this.name,
|
||||||
isMain: this.isMain,
|
isMain: this.isMain,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedScenes = [...this.savedScenes, this.createdScene];
|
const updatedScenes = [...this.savedScenes, this.createdScene];
|
||||||
editor.scenes.setScenes({ scenes: updatedScenes });
|
editor.scenes.setScenes({ scenes: updatedScenes });
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
if (this.savedScenes) {
|
undo(): void {
|
||||||
const editor = EditorCore.getInstance();
|
if (this.savedScenes) {
|
||||||
editor.scenes.setScenes({ scenes: this.savedScenes });
|
const editor = EditorCore.getInstance();
|
||||||
}
|
editor.scenes.setScenes({ scenes: this.savedScenes });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
getSceneId(): string {
|
|
||||||
return this.createdScene?.id ?? "";
|
getSceneId(): string {
|
||||||
}
|
return this.createdScene?.id ?? "";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import type { TScene } from "@/lib/timeline";
|
import type { TScene } from "@/lib/timeline";
|
||||||
import { canDeleteScene, getFallbackSceneAfterDelete } from "@/lib/scenes";
|
import { canDeleteScene, getFallbackSceneAfterDelete } from "@/lib/scenes";
|
||||||
|
|
@ -12,7 +12,7 @@ export class DeleteSceneCommand extends Command {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
const scenes = editor.scenes.getScenes();
|
const scenes = editor.scenes.getScenes();
|
||||||
const activeScene = editor.scenes.getActiveScene();
|
const activeScene = editor.scenes.getActiveScene();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import type { TScene } from "@/lib/timeline";
|
import type { TScene } from "@/lib/timeline";
|
||||||
import { updateSceneInArray } from "@/lib/scenes";
|
import { updateSceneInArray } from "@/lib/scenes";
|
||||||
|
|
@ -14,7 +14,7 @@ export class MoveBookmarkCommand extends Command {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
const activeScene = editor.scenes.getActiveScene();
|
const activeScene = editor.scenes.getActiveScene();
|
||||||
const activeProject = editor.project.getActive();
|
const activeProject = editor.project.getActive();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import type { TScene } from "@/lib/timeline";
|
import type { TScene } from "@/lib/timeline";
|
||||||
import { updateSceneInArray } from "@/lib/scenes";
|
import { updateSceneInArray } from "@/lib/scenes";
|
||||||
|
|
@ -15,7 +15,7 @@ export class RemoveBookmarkCommand extends Command {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
const activeScene = editor.scenes.getActiveScene();
|
const activeScene = editor.scenes.getActiveScene();
|
||||||
const activeProject = editor.project.getActive();
|
const activeProject = editor.project.getActive();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import type { TScene } from "@/lib/timeline";
|
import type { TScene } from "@/lib/timeline";
|
||||||
import { updateSceneInArray } from "@/lib/scenes";
|
import { updateSceneInArray } from "@/lib/scenes";
|
||||||
|
|
@ -14,7 +14,7 @@ export class RenameSceneCommand extends Command {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
const scenes = editor.scenes.getScenes();
|
const scenes = editor.scenes.getScenes();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import type { TScene } from "@/lib/timeline";
|
import type { TScene } from "@/lib/timeline";
|
||||||
import { updateSceneInArray } from "@/lib/scenes";
|
import { updateSceneInArray } from "@/lib/scenes";
|
||||||
|
|
@ -12,7 +12,7 @@ export class ToggleBookmarkCommand extends Command {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
const activeScene = editor.scenes.getActiveScene();
|
const activeScene = editor.scenes.getActiveScene();
|
||||||
const activeProject = editor.project.getActive();
|
const activeProject = editor.project.getActive();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import type { Bookmark, TScene } from "@/lib/timeline";
|
import type { Bookmark, TScene } from "@/lib/timeline";
|
||||||
import { updateSceneInArray } from "@/lib/scenes";
|
import { updateSceneInArray } from "@/lib/scenes";
|
||||||
|
|
@ -14,7 +14,7 @@ export class UpdateBookmarkCommand extends Command {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
const activeScene = editor.scenes.getActiveScene();
|
const activeScene = editor.scenes.getActiveScene();
|
||||||
const activeProject = editor.project.getActive();
|
const activeProject = editor.project.getActive();
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export class PasteCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): CommandResult | undefined {
|
execute(): CommandResult | undefined {
|
||||||
if (this.clipboardItems.length === 0) return;
|
if (this.clipboardItems.length === 0) return undefined;
|
||||||
|
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
@ -123,6 +123,7 @@ export class PasteCommand extends Command {
|
||||||
if (this.pastedElements.length > 0) {
|
if (this.pastedElements.length > 0) {
|
||||||
return { select: this.pastedElements };
|
return { select: this.pastedElements };
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
undo(): void {
|
undo(): void {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export class DeleteElementsCommand extends Command {
|
||||||
this.rippleEnabled = rippleEnabled;
|
this.rippleEnabled = rippleEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): CommandResult {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ export class DuplicateElementsCommand extends Command {
|
||||||
select: this.duplicatedElements,
|
select: this.duplicatedElements,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
undo(): void {
|
undo(): void {
|
||||||
|
|
|
||||||
|
|
@ -1,74 +1,75 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
|
import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
|
||||||
import type { TimelineTrack, VisualElement } from "@/lib/timeline";
|
import type { TimelineTrack, VisualElement } from "@/lib/timeline";
|
||||||
import { buildDefaultEffectInstance } from "@/lib/effects";
|
import { buildDefaultEffectInstance } from "@/lib/effects";
|
||||||
|
|
||||||
function addEffectToElement({
|
function addEffectToElement({
|
||||||
element,
|
element,
|
||||||
effectType,
|
effectType,
|
||||||
}: {
|
}: {
|
||||||
element: VisualElement;
|
element: VisualElement;
|
||||||
effectType: string;
|
effectType: string;
|
||||||
}): VisualElement {
|
}): VisualElement {
|
||||||
const instance = buildDefaultEffectInstance({ effectType });
|
const instance = buildDefaultEffectInstance({ effectType });
|
||||||
const currentEffects = element.effects ?? [];
|
const currentEffects = element.effects ?? [];
|
||||||
return { ...element, effects: [...currentEffects, instance] };
|
return { ...element, effects: [...currentEffects, instance] };
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AddClipEffectCommand extends Command {
|
export class AddClipEffectCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private effectId: string | null = null;
|
private effectId: string | null = null;
|
||||||
private readonly trackId: string;
|
private readonly trackId: string;
|
||||||
private readonly elementId: string;
|
private readonly elementId: string;
|
||||||
private readonly effectType: string;
|
private readonly effectType: string;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
trackId,
|
trackId,
|
||||||
elementId,
|
elementId,
|
||||||
effectType,
|
effectType,
|
||||||
}: {
|
}: {
|
||||||
trackId: string;
|
trackId: string;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
effectType: string;
|
effectType: string;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.trackId = trackId;
|
this.trackId = trackId;
|
||||||
this.elementId = elementId;
|
this.elementId = elementId;
|
||||||
this.effectType = effectType;
|
this.effectType = effectType;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
const updatedTracks = updateElementInTracks({
|
const updatedTracks = updateElementInTracks({
|
||||||
tracks: this.savedState,
|
tracks: this.savedState,
|
||||||
trackId: this.trackId,
|
trackId: this.trackId,
|
||||||
elementId: this.elementId,
|
elementId: this.elementId,
|
||||||
elementPredicate: isVisualElement,
|
elementPredicate: isVisualElement,
|
||||||
update: (element) => {
|
update: (element) => {
|
||||||
const updated = addEffectToElement({
|
const updated = addEffectToElement({
|
||||||
element: element as VisualElement,
|
element: element as VisualElement,
|
||||||
effectType: this.effectType,
|
effectType: this.effectType,
|
||||||
});
|
});
|
||||||
const effects = updated.effects ?? [];
|
const effects = updated.effects ?? [];
|
||||||
this.effectId = effects[effects.length - 1]?.id ?? null;
|
this.effectId = effects[effects.length - 1]?.id ?? null;
|
||||||
return updated;
|
return updated;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
if (this.savedState) {
|
undo(): void {
|
||||||
const editor = EditorCore.getInstance();
|
if (this.savedState) {
|
||||||
editor.timeline.updateTracks(this.savedState);
|
const editor = EditorCore.getInstance();
|
||||||
}
|
editor.timeline.updateTracks(this.savedState);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
getEffectId(): string | null {
|
|
||||||
return this.effectId;
|
getEffectId(): string | null {
|
||||||
}
|
return this.effectId;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,66 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
|
import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
|
||||||
import type { TimelineTrack, VisualElement } from "@/lib/timeline";
|
import type { TimelineTrack, VisualElement } from "@/lib/timeline";
|
||||||
|
|
||||||
function removeEffectFromElement({
|
function removeEffectFromElement({
|
||||||
element,
|
element,
|
||||||
effectId,
|
effectId,
|
||||||
}: {
|
}: {
|
||||||
element: VisualElement;
|
element: VisualElement;
|
||||||
effectId: string;
|
effectId: string;
|
||||||
}): VisualElement {
|
}): VisualElement {
|
||||||
const currentEffects = element.effects ?? [];
|
const currentEffects = element.effects ?? [];
|
||||||
const filtered = currentEffects.filter((effect) => effect.id !== effectId);
|
const filtered = currentEffects.filter((effect) => effect.id !== effectId);
|
||||||
return { ...element, effects: filtered };
|
return { ...element, effects: filtered };
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RemoveClipEffectCommand extends Command {
|
export class RemoveClipEffectCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private readonly trackId: string;
|
private readonly trackId: string;
|
||||||
private readonly elementId: string;
|
private readonly elementId: string;
|
||||||
private readonly effectId: string;
|
private readonly effectId: string;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
trackId,
|
trackId,
|
||||||
elementId,
|
elementId,
|
||||||
effectId,
|
effectId,
|
||||||
}: {
|
}: {
|
||||||
trackId: string;
|
trackId: string;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
effectId: string;
|
effectId: string;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.trackId = trackId;
|
this.trackId = trackId;
|
||||||
this.elementId = elementId;
|
this.elementId = elementId;
|
||||||
this.effectId = effectId;
|
this.effectId = effectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
const updatedTracks = updateElementInTracks({
|
const updatedTracks = updateElementInTracks({
|
||||||
tracks: this.savedState,
|
tracks: this.savedState,
|
||||||
trackId: this.trackId,
|
trackId: this.trackId,
|
||||||
elementId: this.elementId,
|
elementId: this.elementId,
|
||||||
elementPredicate: isVisualElement,
|
elementPredicate: isVisualElement,
|
||||||
update: (element) => {
|
update: (element) => {
|
||||||
return removeEffectFromElement({
|
return removeEffectFromElement({
|
||||||
element: element as VisualElement,
|
element: element as VisualElement,
|
||||||
effectId: this.effectId,
|
effectId: this.effectId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
if (this.savedState) {
|
undo(): void {
|
||||||
const editor = EditorCore.getInstance();
|
if (this.savedState) {
|
||||||
editor.timeline.updateTracks(this.savedState);
|
const editor = EditorCore.getInstance();
|
||||||
}
|
editor.timeline.updateTracks(this.savedState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,74 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
|
import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
|
||||||
import type { TimelineTrack, VisualElement } from "@/lib/timeline";
|
import type { TimelineTrack, VisualElement } from "@/lib/timeline";
|
||||||
|
|
||||||
function reorderEffectsOnElement({
|
function reorderEffectsOnElement({
|
||||||
element,
|
element,
|
||||||
fromIndex,
|
fromIndex,
|
||||||
toIndex,
|
toIndex,
|
||||||
}: {
|
}: {
|
||||||
element: VisualElement;
|
element: VisualElement;
|
||||||
fromIndex: number;
|
fromIndex: number;
|
||||||
toIndex: number;
|
toIndex: number;
|
||||||
}): VisualElement {
|
}): VisualElement {
|
||||||
const effects = [...(element.effects ?? [])];
|
const effects = [...(element.effects ?? [])];
|
||||||
const [moved] = effects.splice(fromIndex, 1);
|
const [moved] = effects.splice(fromIndex, 1);
|
||||||
effects.splice(toIndex, 0, moved);
|
effects.splice(toIndex, 0, moved);
|
||||||
return { ...element, effects };
|
return { ...element, effects };
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ReorderClipEffectsCommand extends Command {
|
export class ReorderClipEffectsCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private readonly trackId: string;
|
private readonly trackId: string;
|
||||||
private readonly elementId: string;
|
private readonly elementId: string;
|
||||||
private readonly fromIndex: number;
|
private readonly fromIndex: number;
|
||||||
private readonly toIndex: number;
|
private readonly toIndex: number;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
trackId,
|
trackId,
|
||||||
elementId,
|
elementId,
|
||||||
fromIndex,
|
fromIndex,
|
||||||
toIndex,
|
toIndex,
|
||||||
}: {
|
}: {
|
||||||
trackId: string;
|
trackId: string;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
fromIndex: number;
|
fromIndex: number;
|
||||||
toIndex: number;
|
toIndex: number;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.trackId = trackId;
|
this.trackId = trackId;
|
||||||
this.elementId = elementId;
|
this.elementId = elementId;
|
||||||
this.fromIndex = fromIndex;
|
this.fromIndex = fromIndex;
|
||||||
this.toIndex = toIndex;
|
this.toIndex = toIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
const updatedTracks = updateElementInTracks({
|
const updatedTracks = updateElementInTracks({
|
||||||
tracks: this.savedState,
|
tracks: this.savedState,
|
||||||
trackId: this.trackId,
|
trackId: this.trackId,
|
||||||
elementId: this.elementId,
|
elementId: this.elementId,
|
||||||
elementPredicate: isVisualElement,
|
elementPredicate: isVisualElement,
|
||||||
update: (element) => {
|
update: (element) => {
|
||||||
return reorderEffectsOnElement({
|
return reorderEffectsOnElement({
|
||||||
element: element as VisualElement,
|
element: element as VisualElement,
|
||||||
fromIndex: this.fromIndex,
|
fromIndex: this.fromIndex,
|
||||||
toIndex: this.toIndex,
|
toIndex: this.toIndex,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
if (this.savedState) {
|
undo(): void {
|
||||||
const editor = EditorCore.getInstance();
|
if (this.savedState) {
|
||||||
editor.timeline.updateTracks(this.savedState);
|
const editor = EditorCore.getInstance();
|
||||||
}
|
editor.timeline.updateTracks(this.savedState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,67 +1,68 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
|
import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
|
||||||
import type { TimelineTrack, VisualElement } from "@/lib/timeline";
|
import type { TimelineTrack, VisualElement } from "@/lib/timeline";
|
||||||
|
|
||||||
export function toggleEffectOnElement({
|
export function toggleEffectOnElement({
|
||||||
element,
|
element,
|
||||||
effectId,
|
effectId,
|
||||||
}: {
|
}: {
|
||||||
element: VisualElement;
|
element: VisualElement;
|
||||||
effectId: string;
|
effectId: string;
|
||||||
}): VisualElement {
|
}): VisualElement {
|
||||||
const currentEffects = element.effects ?? [];
|
const currentEffects = element.effects ?? [];
|
||||||
const updated = currentEffects.map((effect) =>
|
const updated = currentEffects.map((effect) =>
|
||||||
effect.id === effectId ? { ...effect, enabled: !effect.enabled } : effect,
|
effect.id === effectId ? { ...effect, enabled: !effect.enabled } : effect,
|
||||||
);
|
);
|
||||||
return { ...element, effects: updated };
|
return { ...element, effects: updated };
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ToggleClipEffectCommand extends Command {
|
export class ToggleClipEffectCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private readonly trackId: string;
|
private readonly trackId: string;
|
||||||
private readonly elementId: string;
|
private readonly elementId: string;
|
||||||
private readonly effectId: string;
|
private readonly effectId: string;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
trackId,
|
trackId,
|
||||||
elementId,
|
elementId,
|
||||||
effectId,
|
effectId,
|
||||||
}: {
|
}: {
|
||||||
trackId: string;
|
trackId: string;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
effectId: string;
|
effectId: string;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.trackId = trackId;
|
this.trackId = trackId;
|
||||||
this.elementId = elementId;
|
this.elementId = elementId;
|
||||||
this.effectId = effectId;
|
this.effectId = effectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
const updatedTracks = updateElementInTracks({
|
const updatedTracks = updateElementInTracks({
|
||||||
tracks: this.savedState,
|
tracks: this.savedState,
|
||||||
trackId: this.trackId,
|
trackId: this.trackId,
|
||||||
elementId: this.elementId,
|
elementId: this.elementId,
|
||||||
elementPredicate: isVisualElement,
|
elementPredicate: isVisualElement,
|
||||||
update: (element) => {
|
update: (element) => {
|
||||||
return toggleEffectOnElement({
|
return toggleEffectOnElement({
|
||||||
element: element as VisualElement,
|
element: element as VisualElement,
|
||||||
effectId: this.effectId,
|
effectId: this.effectId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
if (this.savedState) {
|
undo(): void {
|
||||||
const editor = EditorCore.getInstance();
|
if (this.savedState) {
|
||||||
editor.timeline.updateTracks(this.savedState);
|
const editor = EditorCore.getInstance();
|
||||||
}
|
editor.timeline.updateTracks(this.savedState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
|
import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
|
||||||
import type { ParamValues } from "@/lib/params";
|
import type { ParamValues } from "@/lib/params";
|
||||||
|
|
@ -56,7 +56,7 @@ export class UpdateClipEffectParamsCommand extends Command {
|
||||||
this.params = params;
|
this.params = params;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
|
|
@ -75,6 +75,7 @@ export class UpdateClipEffectParamsCommand extends Command {
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
undo(): void {
|
undo(): void {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ export { ToggleElementsVisibilityCommand } from "./toggle-elements-visibility";
|
||||||
export { ToggleElementsMutedCommand } from "./toggle-elements-muted";
|
export { ToggleElementsMutedCommand } from "./toggle-elements-muted";
|
||||||
export { ToggleSourceAudioSeparationCommand } from "./toggle-source-audio-separation";
|
export { ToggleSourceAudioSeparationCommand } from "./toggle-source-audio-separation";
|
||||||
export { MoveElementCommand } from "./move-elements";
|
export { MoveElementCommand } from "./move-elements";
|
||||||
|
|
||||||
export * from "./keyframes";
|
export * from "./keyframes";
|
||||||
export * from "./effects";
|
export * from "./effects";
|
||||||
export * from "./masks";
|
export * from "./masks";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import type {
|
import type {
|
||||||
CreateTimelineElement,
|
CreateTimelineElement,
|
||||||
|
|
@ -42,7 +42,7 @@ export class InsertElementCommand extends Command {
|
||||||
private element: CreateTimelineElement;
|
private element: CreateTimelineElement;
|
||||||
private placement: InsertElementPlacement;
|
private placement: InsertElementPlacement;
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,69 @@
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { removeEffectParamKeyframe } from "@/lib/animation/effect-param-channel";
|
import { removeEffectParamKeyframe } from "@/lib/animation/effect-param-channel";
|
||||||
import { updateElementInTracks } from "@/lib/timeline";
|
import { updateElementInTracks } from "@/lib/timeline";
|
||||||
import { isVisualElement } from "@/lib/timeline/element-utils";
|
import { isVisualElement } from "@/lib/timeline/element-utils";
|
||||||
import type { TimelineTrack } from "@/lib/timeline";
|
import type { TimelineTrack } from "@/lib/timeline";
|
||||||
|
|
||||||
export class RemoveEffectParamKeyframeCommand extends Command {
|
export class RemoveEffectParamKeyframeCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private readonly trackId: string;
|
private readonly trackId: string;
|
||||||
private readonly elementId: string;
|
private readonly elementId: string;
|
||||||
private readonly effectId: string;
|
private readonly effectId: string;
|
||||||
private readonly paramKey: string;
|
private readonly paramKey: string;
|
||||||
private readonly keyframeId: string;
|
private readonly keyframeId: string;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
trackId,
|
trackId,
|
||||||
elementId,
|
elementId,
|
||||||
effectId,
|
effectId,
|
||||||
paramKey,
|
paramKey,
|
||||||
keyframeId,
|
keyframeId,
|
||||||
}: {
|
}: {
|
||||||
trackId: string;
|
trackId: string;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
effectId: string;
|
effectId: string;
|
||||||
paramKey: string;
|
paramKey: string;
|
||||||
keyframeId: string;
|
keyframeId: string;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.trackId = trackId;
|
this.trackId = trackId;
|
||||||
this.elementId = elementId;
|
this.elementId = elementId;
|
||||||
this.effectId = effectId;
|
this.effectId = effectId;
|
||||||
this.paramKey = paramKey;
|
this.paramKey = paramKey;
|
||||||
this.keyframeId = keyframeId;
|
this.keyframeId = keyframeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
const updatedTracks = updateElementInTracks({
|
const updatedTracks = updateElementInTracks({
|
||||||
tracks: this.savedState,
|
tracks: this.savedState,
|
||||||
trackId: this.trackId,
|
trackId: this.trackId,
|
||||||
elementId: this.elementId,
|
elementId: this.elementId,
|
||||||
elementPredicate: isVisualElement,
|
elementPredicate: isVisualElement,
|
||||||
update: (element) => {
|
update: (element) => {
|
||||||
const animations = removeEffectParamKeyframe({
|
const animations = removeEffectParamKeyframe({
|
||||||
animations: element.animations,
|
animations: element.animations,
|
||||||
effectId: this.effectId,
|
effectId: this.effectId,
|
||||||
paramKey: this.paramKey,
|
paramKey: this.paramKey,
|
||||||
keyframeId: this.keyframeId,
|
keyframeId: this.keyframeId,
|
||||||
});
|
});
|
||||||
return { ...element, animations };
|
return { ...element, animations };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
if (!this.savedState) {
|
undo(): void {
|
||||||
return;
|
if (!this.savedState) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
const editor = EditorCore.getInstance();
|
|
||||||
editor.timeline.updateTracks(this.savedState);
|
const editor = EditorCore.getInstance();
|
||||||
}
|
editor.timeline.updateTracks(this.savedState);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,138 +1,139 @@
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import {
|
import {
|
||||||
getChannel,
|
getChannel,
|
||||||
getChannelValueAtTime,
|
getChannelValueAtTime,
|
||||||
removeElementKeyframe,
|
removeElementKeyframe,
|
||||||
resolveAnimationTarget,
|
resolveAnimationTarget,
|
||||||
} from "@/lib/animation";
|
} from "@/lib/animation";
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { updateElementInTracks } from "@/lib/timeline";
|
import { updateElementInTracks } from "@/lib/timeline";
|
||||||
import type { AnimationPath, AnimationValue } from "@/lib/animation/types";
|
import type { AnimationPath, AnimationValue } from "@/lib/animation/types";
|
||||||
import type { TimelineElement, TimelineTrack } from "@/lib/timeline";
|
import type { TimelineElement, TimelineTrack } from "@/lib/timeline";
|
||||||
|
|
||||||
function sampleValueBeforeRemoval({
|
function sampleValueBeforeRemoval({
|
||||||
element,
|
element,
|
||||||
propertyPath,
|
propertyPath,
|
||||||
keyframeId,
|
keyframeId,
|
||||||
}: {
|
}: {
|
||||||
element: TimelineElement;
|
element: TimelineElement;
|
||||||
propertyPath: AnimationPath;
|
propertyPath: AnimationPath;
|
||||||
keyframeId: string;
|
keyframeId: string;
|
||||||
}): AnimationValue | null {
|
}): AnimationValue | null {
|
||||||
const channel = getChannel({
|
const channel = getChannel({
|
||||||
animations: element.animations,
|
animations: element.animations,
|
||||||
propertyPath,
|
propertyPath,
|
||||||
});
|
});
|
||||||
const keyframe = channel?.keyframes.find(
|
const keyframe = channel?.keyframes.find(
|
||||||
(candidate) => candidate.id === keyframeId,
|
(candidate) => candidate.id === keyframeId,
|
||||||
);
|
);
|
||||||
if (!channel || !keyframe) {
|
if (!channel || !keyframe) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = resolveAnimationTarget({ element, path: propertyPath });
|
const target = resolveAnimationTarget({ element, path: propertyPath });
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const baseValue = target.getBaseValue();
|
const baseValue = target.getBaseValue();
|
||||||
if (baseValue === null) {
|
if (baseValue === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getChannelValueAtTime({
|
return getChannelValueAtTime({
|
||||||
channel,
|
channel,
|
||||||
time: keyframe.time,
|
time: keyframe.time,
|
||||||
fallbackValue: baseValue,
|
fallbackValue: baseValue,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeKeyframeAndPersist({
|
function removeKeyframeAndPersist({
|
||||||
element,
|
element,
|
||||||
propertyPath,
|
propertyPath,
|
||||||
keyframeId,
|
keyframeId,
|
||||||
}: {
|
}: {
|
||||||
element: TimelineElement;
|
element: TimelineElement;
|
||||||
propertyPath: AnimationPath;
|
propertyPath: AnimationPath;
|
||||||
keyframeId: string;
|
keyframeId: string;
|
||||||
}): TimelineElement {
|
}): TimelineElement {
|
||||||
const target = resolveAnimationTarget({ element, path: propertyPath });
|
const target = resolveAnimationTarget({ element, path: propertyPath });
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueBefore = sampleValueBeforeRemoval({
|
const valueBefore = sampleValueBeforeRemoval({
|
||||||
element,
|
element,
|
||||||
propertyPath,
|
propertyPath,
|
||||||
keyframeId,
|
keyframeId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const nextAnimations = removeElementKeyframe({
|
const nextAnimations = removeElementKeyframe({
|
||||||
animations: element.animations,
|
animations: element.animations,
|
||||||
propertyPath,
|
propertyPath,
|
||||||
keyframeId,
|
keyframeId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isChannelNowEmpty =
|
const isChannelNowEmpty =
|
||||||
getChannel({ animations: nextAnimations, propertyPath }) === undefined;
|
getChannel({ animations: nextAnimations, propertyPath }) === undefined;
|
||||||
const shouldPersistToBase = isChannelNowEmpty && valueBefore !== null;
|
const shouldPersistToBase = isChannelNowEmpty && valueBefore !== null;
|
||||||
|
|
||||||
const baseElement = shouldPersistToBase
|
const baseElement = shouldPersistToBase
|
||||||
? target.setBaseValue(valueBefore)
|
? target.setBaseValue(valueBefore)
|
||||||
: element;
|
: element;
|
||||||
|
|
||||||
return { ...baseElement, animations: nextAnimations };
|
return { ...baseElement, animations: nextAnimations };
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RemoveKeyframeCommand extends Command {
|
export class RemoveKeyframeCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private readonly trackId: string;
|
private readonly trackId: string;
|
||||||
private readonly elementId: string;
|
private readonly elementId: string;
|
||||||
private readonly propertyPath: AnimationPath;
|
private readonly propertyPath: AnimationPath;
|
||||||
private readonly keyframeId: string;
|
private readonly keyframeId: string;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
trackId,
|
trackId,
|
||||||
elementId,
|
elementId,
|
||||||
propertyPath,
|
propertyPath,
|
||||||
keyframeId,
|
keyframeId,
|
||||||
}: {
|
}: {
|
||||||
trackId: string;
|
trackId: string;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
propertyPath: AnimationPath;
|
propertyPath: AnimationPath;
|
||||||
keyframeId: string;
|
keyframeId: string;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.trackId = trackId;
|
this.trackId = trackId;
|
||||||
this.elementId = elementId;
|
this.elementId = elementId;
|
||||||
this.propertyPath = propertyPath;
|
this.propertyPath = propertyPath;
|
||||||
this.keyframeId = keyframeId;
|
this.keyframeId = keyframeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
const updatedTracks = updateElementInTracks({
|
const updatedTracks = updateElementInTracks({
|
||||||
tracks: this.savedState,
|
tracks: this.savedState,
|
||||||
trackId: this.trackId,
|
trackId: this.trackId,
|
||||||
elementId: this.elementId,
|
elementId: this.elementId,
|
||||||
update: (element) =>
|
update: (element) =>
|
||||||
removeKeyframeAndPersist({
|
removeKeyframeAndPersist({
|
||||||
element,
|
element,
|
||||||
propertyPath: this.propertyPath,
|
propertyPath: this.propertyPath,
|
||||||
keyframeId: this.keyframeId,
|
keyframeId: this.keyframeId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
if (!this.savedState) {
|
undo(): void {
|
||||||
return;
|
if (!this.savedState) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
const editor = EditorCore.getInstance();
|
|
||||||
editor.timeline.updateTracks(this.savedState);
|
const editor = EditorCore.getInstance();
|
||||||
}
|
editor.timeline.updateTracks(this.savedState);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,74 +1,75 @@
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { resolveAnimationTarget, retimeElementKeyframe } from "@/lib/animation";
|
import { resolveAnimationTarget, retimeElementKeyframe } from "@/lib/animation";
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { updateElementInTracks } from "@/lib/timeline";
|
import { updateElementInTracks } from "@/lib/timeline";
|
||||||
import type { AnimationPath } from "@/lib/animation/types";
|
import type { AnimationPath } from "@/lib/animation/types";
|
||||||
import type { TimelineTrack } from "@/lib/timeline";
|
import type { TimelineTrack } from "@/lib/timeline";
|
||||||
|
|
||||||
export class RetimeKeyframeCommand extends Command {
|
export class RetimeKeyframeCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private readonly trackId: string;
|
private readonly trackId: string;
|
||||||
private readonly elementId: string;
|
private readonly elementId: string;
|
||||||
private readonly propertyPath: AnimationPath;
|
private readonly propertyPath: AnimationPath;
|
||||||
private readonly keyframeId: string;
|
private readonly keyframeId: string;
|
||||||
private readonly nextTime: number;
|
private readonly nextTime: number;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
trackId,
|
trackId,
|
||||||
elementId,
|
elementId,
|
||||||
propertyPath,
|
propertyPath,
|
||||||
keyframeId,
|
keyframeId,
|
||||||
nextTime,
|
nextTime,
|
||||||
}: {
|
}: {
|
||||||
trackId: string;
|
trackId: string;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
propertyPath: AnimationPath;
|
propertyPath: AnimationPath;
|
||||||
keyframeId: string;
|
keyframeId: string;
|
||||||
nextTime: number;
|
nextTime: number;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.trackId = trackId;
|
this.trackId = trackId;
|
||||||
this.elementId = elementId;
|
this.elementId = elementId;
|
||||||
this.propertyPath = propertyPath;
|
this.propertyPath = propertyPath;
|
||||||
this.keyframeId = keyframeId;
|
this.keyframeId = keyframeId;
|
||||||
this.nextTime = nextTime;
|
this.nextTime = nextTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
const updatedTracks = updateElementInTracks({
|
const updatedTracks = updateElementInTracks({
|
||||||
tracks: this.savedState,
|
tracks: this.savedState,
|
||||||
trackId: this.trackId,
|
trackId: this.trackId,
|
||||||
elementId: this.elementId,
|
elementId: this.elementId,
|
||||||
update: (element) => {
|
update: (element) => {
|
||||||
if (!resolveAnimationTarget({ element, path: this.propertyPath })) {
|
if (!resolveAnimationTarget({ element, path: this.propertyPath })) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundedTime = Math.max(0, Math.min(this.nextTime, element.duration));
|
const boundedTime = Math.max(0, Math.min(this.nextTime, element.duration));
|
||||||
return {
|
return {
|
||||||
...element,
|
...element,
|
||||||
animations: retimeElementKeyframe({
|
animations: retimeElementKeyframe({
|
||||||
animations: element.animations,
|
animations: element.animations,
|
||||||
propertyPath: this.propertyPath,
|
propertyPath: this.propertyPath,
|
||||||
keyframeId: this.keyframeId,
|
keyframeId: this.keyframeId,
|
||||||
time: boundedTime,
|
time: boundedTime,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
if (!this.savedState) {
|
undo(): void {
|
||||||
return;
|
if (!this.savedState) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
const editor = EditorCore.getInstance();
|
|
||||||
editor.timeline.updateTracks(this.savedState);
|
const editor = EditorCore.getInstance();
|
||||||
}
|
editor.timeline.updateTracks(this.savedState);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,84 +1,85 @@
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { upsertEffectParamKeyframe } from "@/lib/animation/effect-param-channel";
|
import { upsertEffectParamKeyframe } from "@/lib/animation/effect-param-channel";
|
||||||
import { updateElementInTracks } from "@/lib/timeline";
|
import { updateElementInTracks } from "@/lib/timeline";
|
||||||
import { isVisualElement } from "@/lib/timeline/element-utils";
|
import { isVisualElement } from "@/lib/timeline/element-utils";
|
||||||
import type { TimelineTrack } from "@/lib/timeline";
|
import type { TimelineTrack } from "@/lib/timeline";
|
||||||
|
|
||||||
export class UpsertEffectParamKeyframeCommand extends Command {
|
export class UpsertEffectParamKeyframeCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private readonly trackId: string;
|
private readonly trackId: string;
|
||||||
private readonly elementId: string;
|
private readonly elementId: string;
|
||||||
private readonly effectId: string;
|
private readonly effectId: string;
|
||||||
private readonly paramKey: string;
|
private readonly paramKey: string;
|
||||||
private readonly time: number;
|
private readonly time: number;
|
||||||
private readonly value: number;
|
private readonly value: number;
|
||||||
private readonly interpolation: "linear" | "hold" | undefined;
|
private readonly interpolation: "linear" | "hold" | undefined;
|
||||||
private readonly keyframeId: string | undefined;
|
private readonly keyframeId: string | undefined;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
trackId,
|
trackId,
|
||||||
elementId,
|
elementId,
|
||||||
effectId,
|
effectId,
|
||||||
paramKey,
|
paramKey,
|
||||||
time,
|
time,
|
||||||
value,
|
value,
|
||||||
interpolation,
|
interpolation,
|
||||||
keyframeId,
|
keyframeId,
|
||||||
}: {
|
}: {
|
||||||
trackId: string;
|
trackId: string;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
effectId: string;
|
effectId: string;
|
||||||
paramKey: string;
|
paramKey: string;
|
||||||
time: number;
|
time: number;
|
||||||
value: number;
|
value: number;
|
||||||
interpolation?: "linear" | "hold";
|
interpolation?: "linear" | "hold";
|
||||||
keyframeId?: string;
|
keyframeId?: string;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.trackId = trackId;
|
this.trackId = trackId;
|
||||||
this.elementId = elementId;
|
this.elementId = elementId;
|
||||||
this.effectId = effectId;
|
this.effectId = effectId;
|
||||||
this.paramKey = paramKey;
|
this.paramKey = paramKey;
|
||||||
this.time = time;
|
this.time = time;
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.interpolation = interpolation;
|
this.interpolation = interpolation;
|
||||||
this.keyframeId = keyframeId;
|
this.keyframeId = keyframeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
const updatedTracks = updateElementInTracks({
|
const updatedTracks = updateElementInTracks({
|
||||||
tracks: this.savedState,
|
tracks: this.savedState,
|
||||||
trackId: this.trackId,
|
trackId: this.trackId,
|
||||||
elementId: this.elementId,
|
elementId: this.elementId,
|
||||||
elementPredicate: isVisualElement,
|
elementPredicate: isVisualElement,
|
||||||
update: (element) => {
|
update: (element) => {
|
||||||
const boundedTime = Math.max(0, Math.min(this.time, element.duration));
|
const boundedTime = Math.max(0, Math.min(this.time, element.duration));
|
||||||
const animations = upsertEffectParamKeyframe({
|
const animations = upsertEffectParamKeyframe({
|
||||||
animations: element.animations,
|
animations: element.animations,
|
||||||
effectId: this.effectId,
|
effectId: this.effectId,
|
||||||
paramKey: this.paramKey,
|
paramKey: this.paramKey,
|
||||||
time: boundedTime,
|
time: boundedTime,
|
||||||
value: this.value,
|
value: this.value,
|
||||||
interpolation: this.interpolation,
|
interpolation: this.interpolation,
|
||||||
keyframeId: this.keyframeId,
|
keyframeId: this.keyframeId,
|
||||||
});
|
});
|
||||||
return { ...element, animations };
|
return { ...element, animations };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
if (!this.savedState) {
|
undo(): void {
|
||||||
return;
|
if (!this.savedState) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
const editor = EditorCore.getInstance();
|
|
||||||
editor.timeline.updateTracks(this.savedState);
|
const editor = EditorCore.getInstance();
|
||||||
}
|
editor.timeline.updateTracks(this.savedState);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,95 +1,96 @@
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { resolveAnimationTarget, upsertPathKeyframe } from "@/lib/animation";
|
import { resolveAnimationTarget, upsertPathKeyframe } from "@/lib/animation";
|
||||||
import { updateElementInTracks } from "@/lib/timeline";
|
import { updateElementInTracks } from "@/lib/timeline";
|
||||||
import type { TimelineTrack } from "@/lib/timeline";
|
import type { TimelineTrack } from "@/lib/timeline";
|
||||||
import type {
|
import type {
|
||||||
AnimationPath,
|
AnimationPath,
|
||||||
AnimationInterpolation,
|
AnimationInterpolation,
|
||||||
AnimationValue,
|
AnimationValue,
|
||||||
} from "@/lib/animation/types";
|
} from "@/lib/animation/types";
|
||||||
|
|
||||||
export class UpsertKeyframeCommand extends Command {
|
export class UpsertKeyframeCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private readonly trackId: string;
|
private readonly trackId: string;
|
||||||
private readonly elementId: string;
|
private readonly elementId: string;
|
||||||
private readonly propertyPath: AnimationPath;
|
private readonly propertyPath: AnimationPath;
|
||||||
private readonly time: number;
|
private readonly time: number;
|
||||||
private readonly value: AnimationValue;
|
private readonly value: AnimationValue;
|
||||||
private readonly interpolation: AnimationInterpolation | undefined;
|
private readonly interpolation: AnimationInterpolation | undefined;
|
||||||
private readonly keyframeId: string | undefined;
|
private readonly keyframeId: string | undefined;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
trackId,
|
trackId,
|
||||||
elementId,
|
elementId,
|
||||||
propertyPath,
|
propertyPath,
|
||||||
time,
|
time,
|
||||||
value,
|
value,
|
||||||
interpolation,
|
interpolation,
|
||||||
keyframeId,
|
keyframeId,
|
||||||
}: {
|
}: {
|
||||||
trackId: string;
|
trackId: string;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
propertyPath: AnimationPath;
|
propertyPath: AnimationPath;
|
||||||
time: number;
|
time: number;
|
||||||
value: AnimationValue;
|
value: AnimationValue;
|
||||||
interpolation?: AnimationInterpolation;
|
interpolation?: AnimationInterpolation;
|
||||||
keyframeId?: string;
|
keyframeId?: string;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.trackId = trackId;
|
this.trackId = trackId;
|
||||||
this.elementId = elementId;
|
this.elementId = elementId;
|
||||||
this.propertyPath = propertyPath;
|
this.propertyPath = propertyPath;
|
||||||
this.time = time;
|
this.time = time;
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.interpolation = interpolation;
|
this.interpolation = interpolation;
|
||||||
this.keyframeId = keyframeId;
|
this.keyframeId = keyframeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
const updatedTracks = updateElementInTracks({
|
const updatedTracks = updateElementInTracks({
|
||||||
tracks: this.savedState,
|
tracks: this.savedState,
|
||||||
trackId: this.trackId,
|
trackId: this.trackId,
|
||||||
elementId: this.elementId,
|
elementId: this.elementId,
|
||||||
update: (element) => {
|
update: (element) => {
|
||||||
const target = resolveAnimationTarget({
|
const target = resolveAnimationTarget({
|
||||||
element,
|
element,
|
||||||
path: this.propertyPath,
|
path: this.propertyPath,
|
||||||
});
|
});
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundedTime = Math.max(0, Math.min(this.time, element.duration));
|
const boundedTime = Math.max(0, Math.min(this.time, element.duration));
|
||||||
return {
|
return {
|
||||||
...element,
|
...element,
|
||||||
animations: upsertPathKeyframe({
|
animations: upsertPathKeyframe({
|
||||||
animations: element.animations,
|
animations: element.animations,
|
||||||
propertyPath: this.propertyPath,
|
propertyPath: this.propertyPath,
|
||||||
time: boundedTime,
|
time: boundedTime,
|
||||||
value: this.value,
|
value: this.value,
|
||||||
interpolation: this.interpolation,
|
interpolation: this.interpolation,
|
||||||
keyframeId: this.keyframeId,
|
keyframeId: this.keyframeId,
|
||||||
valueKind: target.valueKind,
|
valueKind: target.valueKind,
|
||||||
defaultInterpolation: target.defaultInterpolation,
|
defaultInterpolation: target.defaultInterpolation,
|
||||||
numericRange: target.numericRange,
|
numericRange: target.numericRange,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
if (!this.savedState) {
|
undo(): void {
|
||||||
return;
|
if (!this.savedState) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
const editor = EditorCore.getInstance();
|
|
||||||
editor.timeline.updateTracks(this.savedState);
|
const editor = EditorCore.getInstance();
|
||||||
}
|
editor.timeline.updateTracks(this.savedState);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,64 +1,65 @@
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { isMaskableElement, updateElementInTracks } from "@/lib/timeline";
|
import { isMaskableElement, updateElementInTracks } from "@/lib/timeline";
|
||||||
import type { TimelineTrack, MaskableElement } from "@/lib/timeline";
|
import type { TimelineTrack, MaskableElement } from "@/lib/timeline";
|
||||||
|
|
||||||
function removeMaskFromElement({
|
function removeMaskFromElement({
|
||||||
element,
|
element,
|
||||||
maskId,
|
maskId,
|
||||||
}: {
|
}: {
|
||||||
element: MaskableElement;
|
element: MaskableElement;
|
||||||
maskId: string;
|
maskId: string;
|
||||||
}): MaskableElement {
|
}): MaskableElement {
|
||||||
const currentMasks = element.masks ?? [];
|
const currentMasks = element.masks ?? [];
|
||||||
const filteredMasks = currentMasks.filter((mask) => mask.id !== maskId);
|
const filteredMasks = currentMasks.filter((mask) => mask.id !== maskId);
|
||||||
return { ...element, masks: filteredMasks };
|
return { ...element, masks: filteredMasks };
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RemoveMaskCommand extends Command {
|
export class RemoveMaskCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private readonly trackId: string;
|
private readonly trackId: string;
|
||||||
private readonly elementId: string;
|
private readonly elementId: string;
|
||||||
private readonly maskId: string;
|
private readonly maskId: string;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
trackId,
|
trackId,
|
||||||
elementId,
|
elementId,
|
||||||
maskId,
|
maskId,
|
||||||
}: {
|
}: {
|
||||||
trackId: string;
|
trackId: string;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
maskId: string;
|
maskId: string;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.trackId = trackId;
|
this.trackId = trackId;
|
||||||
this.elementId = elementId;
|
this.elementId = elementId;
|
||||||
this.maskId = maskId;
|
this.maskId = maskId;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
const updatedTracks = updateElementInTracks({
|
const updatedTracks = updateElementInTracks({
|
||||||
tracks: this.savedState,
|
tracks: this.savedState,
|
||||||
trackId: this.trackId,
|
trackId: this.trackId,
|
||||||
elementId: this.elementId,
|
elementId: this.elementId,
|
||||||
elementPredicate: isMaskableElement,
|
elementPredicate: isMaskableElement,
|
||||||
update: (element) =>
|
update: (element) =>
|
||||||
removeMaskFromElement({
|
removeMaskFromElement({
|
||||||
element: element as MaskableElement,
|
element: element as MaskableElement,
|
||||||
maskId: this.maskId,
|
maskId: this.maskId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
if (this.savedState) {
|
undo(): void {
|
||||||
const editor = EditorCore.getInstance();
|
if (this.savedState) {
|
||||||
editor.timeline.updateTracks(this.savedState);
|
const editor = EditorCore.getInstance();
|
||||||
}
|
editor.timeline.updateTracks(this.savedState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,75 +1,76 @@
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { isMaskableElement, updateElementInTracks } from "@/lib/timeline";
|
import { isMaskableElement, updateElementInTracks } from "@/lib/timeline";
|
||||||
import type { Mask } from "@/lib/masks/types";
|
import type { Mask } from "@/lib/masks/types";
|
||||||
import type { TimelineTrack, MaskableElement } from "@/lib/timeline";
|
import type { TimelineTrack, MaskableElement } from "@/lib/timeline";
|
||||||
|
|
||||||
export function toggleMaskInvertedOnElement({
|
export function toggleMaskInvertedOnElement({
|
||||||
element,
|
element,
|
||||||
maskId,
|
maskId,
|
||||||
}: {
|
}: {
|
||||||
element: MaskableElement;
|
element: MaskableElement;
|
||||||
maskId: string;
|
maskId: string;
|
||||||
}): MaskableElement {
|
}): MaskableElement {
|
||||||
const currentMasks = element.masks ?? [];
|
const currentMasks = element.masks ?? [];
|
||||||
const toggleMask = <TMask extends Mask>(mask: TMask): TMask => ({
|
const toggleMask = <TMask extends Mask>(mask: TMask): TMask => ({
|
||||||
...mask,
|
...mask,
|
||||||
params: {
|
params: {
|
||||||
...mask.params,
|
...mask.params,
|
||||||
inverted: !mask.params.inverted,
|
inverted: !mask.params.inverted,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const updatedMasks = currentMasks.map((mask) =>
|
const updatedMasks = currentMasks.map((mask) =>
|
||||||
mask.id !== maskId ? mask : toggleMask(mask),
|
mask.id !== maskId ? mask : toggleMask(mask),
|
||||||
);
|
);
|
||||||
|
|
||||||
return { ...element, masks: updatedMasks };
|
return { ...element, masks: updatedMasks };
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ToggleMaskInvertedCommand extends Command {
|
export class ToggleMaskInvertedCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private readonly trackId: string;
|
private readonly trackId: string;
|
||||||
private readonly elementId: string;
|
private readonly elementId: string;
|
||||||
private readonly maskId: string;
|
private readonly maskId: string;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
trackId,
|
trackId,
|
||||||
elementId,
|
elementId,
|
||||||
maskId,
|
maskId,
|
||||||
}: {
|
}: {
|
||||||
trackId: string;
|
trackId: string;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
maskId: string;
|
maskId: string;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.trackId = trackId;
|
this.trackId = trackId;
|
||||||
this.elementId = elementId;
|
this.elementId = elementId;
|
||||||
this.maskId = maskId;
|
this.maskId = maskId;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
const updatedTracks = updateElementInTracks({
|
const updatedTracks = updateElementInTracks({
|
||||||
tracks: this.savedState,
|
tracks: this.savedState,
|
||||||
trackId: this.trackId,
|
trackId: this.trackId,
|
||||||
elementId: this.elementId,
|
elementId: this.elementId,
|
||||||
elementPredicate: isMaskableElement,
|
elementPredicate: isMaskableElement,
|
||||||
update: (element) =>
|
update: (element) =>
|
||||||
toggleMaskInvertedOnElement({
|
toggleMaskInvertedOnElement({
|
||||||
element: element as MaskableElement,
|
element: element as MaskableElement,
|
||||||
maskId: this.maskId,
|
maskId: this.maskId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
if (this.savedState) {
|
undo(): void {
|
||||||
const editor = EditorCore.getInstance();
|
if (this.savedState) {
|
||||||
editor.timeline.updateTracks(this.savedState);
|
const editor = EditorCore.getInstance();
|
||||||
}
|
editor.timeline.updateTracks(this.savedState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,145 +1,146 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import type {
|
import type {
|
||||||
TimelineTrack,
|
TimelineTrack,
|
||||||
TimelineElement,
|
TimelineElement,
|
||||||
TrackType,
|
TrackType,
|
||||||
} from "@/lib/timeline";
|
} from "@/lib/timeline";
|
||||||
import {
|
import {
|
||||||
buildEmptyTrack,
|
buildEmptyTrack,
|
||||||
validateElementTrackCompatibility,
|
validateElementTrackCompatibility,
|
||||||
enforceMainTrackStart,
|
enforceMainTrackStart,
|
||||||
} from "@/lib/timeline/placement";
|
} from "@/lib/timeline/placement";
|
||||||
import { rippleShiftElements } from "@/lib/timeline/ripple-utils";
|
import { rippleShiftElements } from "@/lib/timeline/ripple-utils";
|
||||||
|
|
||||||
export class MoveElementCommand extends Command {
|
export class MoveElementCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private readonly sourceTrackId: string;
|
private readonly sourceTrackId: string;
|
||||||
private readonly targetTrackId: string;
|
private readonly targetTrackId: string;
|
||||||
private readonly elementId: string;
|
private readonly elementId: string;
|
||||||
private readonly newStartTime: number;
|
private readonly newStartTime: number;
|
||||||
private readonly createTrack: { type: TrackType; index: number } | undefined;
|
private readonly createTrack: { type: TrackType; index: number } | undefined;
|
||||||
private readonly rippleEnabled: boolean;
|
private readonly rippleEnabled: boolean;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
sourceTrackId,
|
sourceTrackId,
|
||||||
targetTrackId,
|
targetTrackId,
|
||||||
elementId,
|
elementId,
|
||||||
newStartTime,
|
newStartTime,
|
||||||
createTrack,
|
createTrack,
|
||||||
rippleEnabled = false,
|
rippleEnabled = false,
|
||||||
}: {
|
}: {
|
||||||
sourceTrackId: string;
|
sourceTrackId: string;
|
||||||
targetTrackId: string;
|
targetTrackId: string;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
newStartTime: number;
|
newStartTime: number;
|
||||||
createTrack?: { type: TrackType; index: number };
|
createTrack?: { type: TrackType; index: number };
|
||||||
rippleEnabled?: boolean;
|
rippleEnabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.sourceTrackId = sourceTrackId;
|
this.sourceTrackId = sourceTrackId;
|
||||||
this.targetTrackId = targetTrackId;
|
this.targetTrackId = targetTrackId;
|
||||||
this.elementId = elementId;
|
this.elementId = elementId;
|
||||||
this.newStartTime = newStartTime;
|
this.newStartTime = newStartTime;
|
||||||
this.createTrack = createTrack;
|
this.createTrack = createTrack;
|
||||||
this.rippleEnabled = rippleEnabled;
|
this.rippleEnabled = rippleEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
const sourceTrack = this.savedState.find(
|
const sourceTrack = this.savedState.find(
|
||||||
(track) => track.id === this.sourceTrackId,
|
(track) => track.id === this.sourceTrackId,
|
||||||
);
|
);
|
||||||
const element = sourceTrack?.elements.find(
|
const element = sourceTrack?.elements.find(
|
||||||
(trackElement) => trackElement.id === this.elementId,
|
(trackElement) => trackElement.id === this.elementId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!sourceTrack || !element) {
|
if (!sourceTrack || !element) {
|
||||||
throw new Error("Source track or element not found");
|
throw new Error("Source track or element not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetTrack = this.savedState.find((track) => track.id === this.targetTrackId);
|
let targetTrack = this.savedState.find((track) => track.id === this.targetTrackId);
|
||||||
let tracksToUpdate = this.savedState;
|
let tracksToUpdate = this.savedState;
|
||||||
if (!targetTrack && this.createTrack) {
|
if (!targetTrack && this.createTrack) {
|
||||||
const newTrack = buildEmptyTrack({
|
const newTrack = buildEmptyTrack({
|
||||||
id: this.targetTrackId,
|
id: this.targetTrackId,
|
||||||
type: this.createTrack.type,
|
type: this.createTrack.type,
|
||||||
});
|
});
|
||||||
tracksToUpdate = [...this.savedState];
|
tracksToUpdate = [...this.savedState];
|
||||||
tracksToUpdate.splice(this.createTrack.index, 0, newTrack);
|
tracksToUpdate.splice(this.createTrack.index, 0, newTrack);
|
||||||
targetTrack = newTrack;
|
targetTrack = newTrack;
|
||||||
}
|
}
|
||||||
if (!targetTrack) {
|
if (!targetTrack) {
|
||||||
throw new Error("Target track not found");
|
throw new Error("Target track not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const validation = validateElementTrackCompatibility({
|
const validation = validateElementTrackCompatibility({
|
||||||
element,
|
element,
|
||||||
track: targetTrack,
|
track: targetTrack,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!validation.isValid) {
|
if (!validation.isValid) {
|
||||||
throw new Error(validation.errorMessage);
|
throw new Error(validation.errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const adjustedStartTime = enforceMainTrackStart({
|
const adjustedStartTime = enforceMainTrackStart({
|
||||||
tracks: tracksToUpdate,
|
tracks: tracksToUpdate,
|
||||||
targetTrackId: this.targetTrackId,
|
targetTrackId: this.targetTrackId,
|
||||||
requestedStartTime: this.newStartTime,
|
requestedStartTime: this.newStartTime,
|
||||||
excludeElementId: this.elementId,
|
excludeElementId: this.elementId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// keyframe times remain clip-local, so moving only changes element startTime.
|
// keyframe times remain clip-local, so moving only changes element startTime.
|
||||||
const movedElement: TimelineElement = {
|
const movedElement: TimelineElement = {
|
||||||
...element,
|
...element,
|
||||||
startTime: adjustedStartTime,
|
startTime: adjustedStartTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSameTrack = this.sourceTrackId === this.targetTrackId;
|
const isSameTrack = this.sourceTrackId === this.targetTrackId;
|
||||||
|
|
||||||
const updatedTracks = tracksToUpdate.map((track): TimelineTrack => {
|
const updatedTracks = tracksToUpdate.map((track): TimelineTrack => {
|
||||||
if (isSameTrack && track.id === this.sourceTrackId) {
|
if (isSameTrack && track.id === this.sourceTrackId) {
|
||||||
return {
|
return {
|
||||||
...track,
|
...track,
|
||||||
elements: track.elements.map((trackElement) =>
|
elements: track.elements.map((trackElement) =>
|
||||||
trackElement.id === this.elementId ? movedElement : trackElement,
|
trackElement.id === this.elementId ? movedElement : trackElement,
|
||||||
),
|
),
|
||||||
} as typeof track;
|
} as typeof track;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track.id === this.sourceTrackId) {
|
if (track.id === this.sourceTrackId) {
|
||||||
const remainingElements = track.elements.filter(
|
const remainingElements = track.elements.filter(
|
||||||
(trackElement) => trackElement.id !== this.elementId,
|
(trackElement) => trackElement.id !== this.elementId,
|
||||||
);
|
);
|
||||||
const shiftedElements = this.rippleEnabled
|
const shiftedElements = this.rippleEnabled
|
||||||
? rippleShiftElements({
|
? rippleShiftElements({
|
||||||
elements: remainingElements,
|
elements: remainingElements,
|
||||||
afterTime: element.startTime,
|
afterTime: element.startTime,
|
||||||
shiftAmount: element.duration,
|
shiftAmount: element.duration,
|
||||||
})
|
})
|
||||||
: remainingElements;
|
: remainingElements;
|
||||||
return { ...track, elements: shiftedElements } as typeof track;
|
return { ...track, elements: shiftedElements } as typeof track;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track.id === this.targetTrackId) {
|
if (track.id === this.targetTrackId) {
|
||||||
return {
|
return {
|
||||||
...track,
|
...track,
|
||||||
elements: [...track.elements, movedElement],
|
elements: [...track.elements, movedElement],
|
||||||
} as typeof track;
|
} as typeof track;
|
||||||
}
|
}
|
||||||
|
|
||||||
return track;
|
return track;
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
if (this.savedState) {
|
undo(): void {
|
||||||
const editor = EditorCore.getInstance();
|
if (this.savedState) {
|
||||||
editor.timeline.updateTracks(this.savedState);
|
const editor = EditorCore.getInstance();
|
||||||
}
|
editor.timeline.updateTracks(this.savedState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,114 +1,115 @@
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { clampRetimeRate } from "@/lib/retime/rate";
|
import { clampRetimeRate } from "@/lib/retime/rate";
|
||||||
import { clampAnimationsToDuration } from "@/lib/animation";
|
import { clampAnimationsToDuration } from "@/lib/animation";
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { getTimelineDurationForSourceSpan, getSourceSpanAtClipTime } from "@/lib/retime";
|
import { getTimelineDurationForSourceSpan, getSourceSpanAtClipTime } from "@/lib/retime";
|
||||||
import { isRetimableElement, updateElementInTracks } from "@/lib/timeline";
|
import { isRetimableElement, updateElementInTracks } from "@/lib/timeline";
|
||||||
import type { RetimeConfig, TimelineTrack } from "@/lib/timeline";
|
import type { RetimeConfig, TimelineTrack } from "@/lib/timeline";
|
||||||
|
|
||||||
function getSourceDuration({
|
function getSourceDuration({
|
||||||
trimStart,
|
trimStart,
|
||||||
trimEnd,
|
trimEnd,
|
||||||
duration,
|
duration,
|
||||||
sourceDuration,
|
sourceDuration,
|
||||||
retime,
|
retime,
|
||||||
}: {
|
}: {
|
||||||
trimStart: number;
|
trimStart: number;
|
||||||
trimEnd: number;
|
trimEnd: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
sourceDuration?: number;
|
sourceDuration?: number;
|
||||||
retime?: RetimeConfig;
|
retime?: RetimeConfig;
|
||||||
}): number {
|
}): number {
|
||||||
if (typeof sourceDuration === "number") {
|
if (typeof sourceDuration === "number") {
|
||||||
return sourceDuration;
|
return sourceDuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
trimStart +
|
trimStart +
|
||||||
getSourceSpanAtClipTime({
|
getSourceSpanAtClipTime({
|
||||||
clipTime: duration,
|
clipTime: duration,
|
||||||
retime,
|
retime,
|
||||||
}) +
|
}) +
|
||||||
trimEnd
|
trimEnd
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateElementRetimeCommand extends Command {
|
export class UpdateElementRetimeCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private readonly trackId: string;
|
private readonly trackId: string;
|
||||||
private readonly elementId: string;
|
private readonly elementId: string;
|
||||||
private readonly retime: RetimeConfig | undefined;
|
private readonly retime: RetimeConfig | undefined;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
trackId,
|
trackId,
|
||||||
elementId,
|
elementId,
|
||||||
retime,
|
retime,
|
||||||
}: {
|
}: {
|
||||||
trackId: string;
|
trackId: string;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
retime?: RetimeConfig;
|
retime?: RetimeConfig;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.trackId = trackId;
|
this.trackId = trackId;
|
||||||
this.elementId = elementId;
|
this.elementId = elementId;
|
||||||
this.retime = retime;
|
this.retime = retime;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
const updatedTracks = updateElementInTracks({
|
const updatedTracks = updateElementInTracks({
|
||||||
tracks: this.savedState,
|
tracks: this.savedState,
|
||||||
trackId: this.trackId,
|
trackId: this.trackId,
|
||||||
elementId: this.elementId,
|
elementId: this.elementId,
|
||||||
elementPredicate: isRetimableElement,
|
elementPredicate: isRetimableElement,
|
||||||
update: (element) => {
|
update: (element) => {
|
||||||
if (!isRetimableElement(element)) {
|
if (!isRetimableElement(element)) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextRetime = this.retime
|
const nextRetime = this.retime
|
||||||
? {
|
? {
|
||||||
...this.retime,
|
...this.retime,
|
||||||
rate: clampRetimeRate({ rate: this.retime.rate }),
|
rate: clampRetimeRate({ rate: this.retime.rate }),
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
const sourceDuration = getSourceDuration({
|
const sourceDuration = getSourceDuration({
|
||||||
trimStart: element.trimStart,
|
trimStart: element.trimStart,
|
||||||
trimEnd: element.trimEnd,
|
trimEnd: element.trimEnd,
|
||||||
duration: element.duration,
|
duration: element.duration,
|
||||||
sourceDuration: element.sourceDuration,
|
sourceDuration: element.sourceDuration,
|
||||||
retime: element.retime,
|
retime: element.retime,
|
||||||
});
|
});
|
||||||
const visibleSourceSpan = Math.max(
|
const visibleSourceSpan = Math.max(
|
||||||
0,
|
0,
|
||||||
sourceDuration - element.trimStart - element.trimEnd,
|
sourceDuration - element.trimStart - element.trimEnd,
|
||||||
);
|
);
|
||||||
const nextDuration = getTimelineDurationForSourceSpan({
|
const nextDuration = getTimelineDurationForSourceSpan({
|
||||||
sourceSpan: visibleSourceSpan,
|
sourceSpan: visibleSourceSpan,
|
||||||
retime: nextRetime,
|
retime: nextRetime,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...element,
|
...element,
|
||||||
retime: nextRetime,
|
retime: nextRetime,
|
||||||
duration: nextDuration,
|
duration: nextDuration,
|
||||||
animations: clampAnimationsToDuration({
|
animations: clampAnimationsToDuration({
|
||||||
animations: element.animations,
|
animations: element.animations,
|
||||||
duration: nextDuration,
|
duration: nextDuration,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
if (this.savedState) {
|
undo(): void {
|
||||||
const editor = EditorCore.getInstance();
|
if (this.savedState) {
|
||||||
editor.timeline.updateTracks(this.savedState);
|
const editor = EditorCore.getInstance();
|
||||||
}
|
editor.timeline.updateTracks(this.savedState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,7 @@ export class SplitElementsCommand extends Command {
|
||||||
select: this.rightSideElements,
|
select: this.rightSideElements,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
undo(): void {
|
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 type { TimelineTrack } from "@/lib/timeline";
|
||||||
import { canElementHaveAudio } from "@/lib/timeline/element-utils";
|
import { canElementHaveAudio } from "@/lib/timeline/element-utils";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
|
|
@ -10,7 +10,7 @@ export class ToggleElementsMutedCommand extends Command {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import type { TimelineTrack } from "@/lib/timeline";
|
import type { TimelineTrack } from "@/lib/timeline";
|
||||||
import { canElementBeHidden } from "@/lib/timeline/element-utils";
|
import { canElementBeHidden } from "@/lib/timeline/element-utils";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
|
|
@ -10,7 +10,7 @@ export class ToggleElementsVisibilityCommand extends Command {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
|
|
@ -36,6 +36,7 @@ export class ToggleElementsVisibilityCommand extends Command {
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
undo(): void {
|
undo(): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import {
|
import {
|
||||||
buildSeparatedAudioElement,
|
buildSeparatedAudioElement,
|
||||||
canExtractSourceAudio,
|
canExtractSourceAudio,
|
||||||
|
|
@ -25,7 +25,7 @@ export class ToggleSourceAudioSeparationCommand extends Command {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
|
|
@ -43,6 +43,9 @@ export class ToggleSourceAudioSeparationCommand extends Command {
|
||||||
if (!sourceElement) {
|
if (!sourceElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (sourceElement.type !== "video") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (canRecoverSourceAudio({ element: sourceElement })) {
|
if (canRecoverSourceAudio({ element: sourceElement })) {
|
||||||
editor.timeline.updateTracks(
|
editor.timeline.updateTracks(
|
||||||
|
|
@ -59,9 +62,7 @@ export class ToggleSourceAudioSeparationCommand extends Command {
|
||||||
const mediaAsset = editor
|
const mediaAsset = editor
|
||||||
.media
|
.media
|
||||||
.getAssets()
|
.getAssets()
|
||||||
.find((asset) =>
|
.find((asset) => asset.id === sourceElement.mediaId);
|
||||||
sourceElement.type === "video" ? asset.id === sourceElement.mediaId : false,
|
|
||||||
);
|
|
||||||
if (!canExtractSourceAudio({ element: sourceElement, mediaAsset })) {
|
if (!canExtractSourceAudio({ element: sourceElement, mediaAsset })) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,58 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import type { TimelineTrack } from "@/lib/timeline";
|
import type { TimelineTrack } from "@/lib/timeline";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { clampAnimationsToDuration } from "@/lib/animation";
|
import { clampAnimationsToDuration } from "@/lib/animation";
|
||||||
|
|
||||||
export class UpdateElementDurationCommand extends Command {
|
export class UpdateElementDurationCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private readonly trackId: string;
|
private readonly trackId: string;
|
||||||
private readonly elementId: string;
|
private readonly elementId: string;
|
||||||
private readonly duration: number;
|
private readonly duration: number;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
trackId,
|
trackId,
|
||||||
elementId,
|
elementId,
|
||||||
duration,
|
duration,
|
||||||
}: {
|
}: {
|
||||||
trackId: string;
|
trackId: string;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.trackId = trackId;
|
this.trackId = trackId;
|
||||||
this.elementId = elementId;
|
this.elementId = elementId;
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
const updatedTracks = this.savedState.map((track) => {
|
const updatedTracks = this.savedState.map((track) => {
|
||||||
if (track.id !== this.trackId) return track;
|
if (track.id !== this.trackId) return track;
|
||||||
const newElements = track.elements.map((element) =>
|
const newElements = track.elements.map((element) =>
|
||||||
element.id === this.elementId
|
element.id === this.elementId
|
||||||
? {
|
? {
|
||||||
...element,
|
...element,
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
animations: clampAnimationsToDuration({
|
animations: clampAnimationsToDuration({
|
||||||
animations: element.animations,
|
animations: element.animations,
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
: element,
|
: element,
|
||||||
);
|
);
|
||||||
return { ...track, elements: newElements } as typeof track;
|
return { ...track, elements: newElements } as typeof track;
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
if (this.savedState) {
|
undo(): void {
|
||||||
const editor = EditorCore.getInstance();
|
if (this.savedState) {
|
||||||
editor.timeline.updateTracks(this.savedState);
|
const editor = EditorCore.getInstance();
|
||||||
}
|
editor.timeline.updateTracks(this.savedState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,70 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import type { TimelineTrack } from "@/lib/timeline";
|
import type { TimelineTrack } from "@/lib/timeline";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { enforceMainTrackStart } from "@/lib/timeline/placement";
|
import { enforceMainTrackStart } from "@/lib/timeline/placement";
|
||||||
|
|
||||||
export class UpdateElementStartTimeCommand extends Command {
|
export class UpdateElementStartTimeCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private readonly elements: { trackId: string; elementId: string }[];
|
private readonly elements: { trackId: string; elementId: string }[];
|
||||||
private readonly startTime: number;
|
private readonly startTime: number;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
elements,
|
elements,
|
||||||
startTime,
|
startTime,
|
||||||
}: {
|
}: {
|
||||||
elements: { trackId: string; elementId: string }[];
|
elements: { trackId: string; elementId: string }[];
|
||||||
startTime: number;
|
startTime: number;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.elements = elements;
|
this.elements = elements;
|
||||||
this.startTime = startTime;
|
this.startTime = startTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
const currentTracks = this.savedState;
|
const currentTracks = this.savedState;
|
||||||
const updatedTracks = currentTracks.map((track) => {
|
const updatedTracks = currentTracks.map((track) => {
|
||||||
const hasElementsToUpdate = this.elements.some(
|
const hasElementsToUpdate = this.elements.some(
|
||||||
(elementEntry) => elementEntry.trackId === track.id,
|
(elementEntry) => elementEntry.trackId === track.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasElementsToUpdate) {
|
if (!hasElementsToUpdate) {
|
||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newElements = track.elements.map((element) => {
|
const newElements = track.elements.map((element) => {
|
||||||
const shouldUpdate = this.elements.some(
|
const shouldUpdate = this.elements.some(
|
||||||
(elementEntry) =>
|
(elementEntry) =>
|
||||||
elementEntry.elementId === element.id &&
|
elementEntry.elementId === element.id &&
|
||||||
elementEntry.trackId === track.id,
|
elementEntry.trackId === track.id,
|
||||||
);
|
);
|
||||||
if (!shouldUpdate) {
|
if (!shouldUpdate) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseStartTime = Math.max(0, this.startTime);
|
const baseStartTime = Math.max(0, this.startTime);
|
||||||
const adjustedStartTime = enforceMainTrackStart({
|
const adjustedStartTime = enforceMainTrackStart({
|
||||||
tracks: currentTracks,
|
tracks: currentTracks,
|
||||||
targetTrackId: track.id,
|
targetTrackId: track.id,
|
||||||
requestedStartTime: baseStartTime,
|
requestedStartTime: baseStartTime,
|
||||||
excludeElementId: element.id,
|
excludeElementId: element.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ...element, startTime: adjustedStartTime };
|
return { ...element, startTime: adjustedStartTime };
|
||||||
});
|
});
|
||||||
return { ...track, elements: newElements } as typeof track;
|
return { ...track, elements: newElements } as typeof track;
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
if (this.savedState) {
|
undo(): void {
|
||||||
const editor = EditorCore.getInstance();
|
if (this.savedState) {
|
||||||
editor.timeline.updateTracks(this.savedState);
|
const editor = EditorCore.getInstance();
|
||||||
}
|
editor.timeline.updateTracks(this.savedState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import type { TimelineTrack } from "@/lib/timeline";
|
import type { TimelineTrack } from "@/lib/timeline";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { clampAnimationsToDuration } from "@/lib/animation";
|
import { clampAnimationsToDuration } from "@/lib/animation";
|
||||||
|
|
@ -38,7 +38,7 @@ export class UpdateElementTrimCommand extends Command {
|
||||||
this.rippleEnabled = rippleEnabled;
|
this.rippleEnabled = rippleEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
|
|
@ -104,6 +104,7 @@ export class UpdateElementTrimCommand extends Command {
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
undo(): void {
|
undo(): void {
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,48 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import type { TimelineElement, TimelineTrack } from "@/lib/timeline";
|
import type { TimelineElement, TimelineTrack } from "@/lib/timeline";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { updateElementInTracks } from "@/lib/timeline";
|
import { updateElementInTracks } from "@/lib/timeline";
|
||||||
|
|
||||||
export class UpdateElementCommand extends Command {
|
export class UpdateElementCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private readonly trackId: string;
|
private readonly trackId: string;
|
||||||
private readonly elementId: string;
|
private readonly elementId: string;
|
||||||
private readonly updates: Partial<TimelineElement>;
|
private readonly updates: Partial<TimelineElement>;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
trackId,
|
trackId,
|
||||||
elementId,
|
elementId,
|
||||||
updates,
|
updates,
|
||||||
}: {
|
}: {
|
||||||
trackId: string;
|
trackId: string;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
updates: Partial<TimelineElement>;
|
updates: Partial<TimelineElement>;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.trackId = trackId;
|
this.trackId = trackId;
|
||||||
this.elementId = elementId;
|
this.elementId = elementId;
|
||||||
this.updates = updates;
|
this.updates = updates;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
const updatedTracks = updateElementInTracks({
|
const updatedTracks = updateElementInTracks({
|
||||||
tracks: this.savedState,
|
tracks: this.savedState,
|
||||||
trackId: this.trackId,
|
trackId: this.trackId,
|
||||||
elementId: this.elementId,
|
elementId: this.elementId,
|
||||||
update: (element) => ({ ...element, ...this.updates }) as TimelineElement,
|
update: (element) => ({ ...element, ...this.updates }) as TimelineElement,
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
if (this.savedState) {
|
undo(): void {
|
||||||
const editor = EditorCore.getInstance();
|
if (this.savedState) {
|
||||||
editor.timeline.updateTracks(this.savedState);
|
const editor = EditorCore.getInstance();
|
||||||
}
|
editor.timeline.updateTracks(this.savedState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,54 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import type { TrackType, TimelineTrack } from "@/lib/timeline";
|
import type { TrackType, TimelineTrack } from "@/lib/timeline";
|
||||||
import { generateUUID } from "@/utils/id";
|
import { generateUUID } from "@/utils/id";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import {
|
import {
|
||||||
buildEmptyTrack,
|
buildEmptyTrack,
|
||||||
getDefaultInsertIndexForTrack,
|
getDefaultInsertIndexForTrack,
|
||||||
} from "@/lib/timeline/placement";
|
} from "@/lib/timeline/placement";
|
||||||
|
|
||||||
export class AddTrackCommand extends Command {
|
export class AddTrackCommand extends Command {
|
||||||
private trackId: string;
|
private trackId: string;
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private type: TrackType,
|
private type: TrackType,
|
||||||
private index?: number,
|
private index?: number,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.trackId = generateUUID();
|
this.trackId = generateUUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
const newTrack: TimelineTrack = buildEmptyTrack({
|
const newTrack: TimelineTrack = buildEmptyTrack({
|
||||||
id: this.trackId,
|
id: this.trackId,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedTracks = [...(this.savedState || [])];
|
const updatedTracks = [...(this.savedState || [])];
|
||||||
const insertIndex =
|
const insertIndex =
|
||||||
this.index ??
|
this.index ??
|
||||||
getDefaultInsertIndexForTrack({
|
getDefaultInsertIndexForTrack({
|
||||||
tracks: updatedTracks,
|
tracks: updatedTracks,
|
||||||
trackType: this.type,
|
trackType: this.type,
|
||||||
});
|
});
|
||||||
updatedTracks.splice(insertIndex, 0, newTrack);
|
updatedTracks.splice(insertIndex, 0, newTrack);
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
if (this.savedState) {
|
undo(): void {
|
||||||
const editor = EditorCore.getInstance();
|
if (this.savedState) {
|
||||||
editor.timeline.updateTracks(this.savedState);
|
const editor = EditorCore.getInstance();
|
||||||
}
|
editor.timeline.updateTracks(this.savedState);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
getTrackId(): string {
|
|
||||||
return this.trackId;
|
getTrackId(): string {
|
||||||
}
|
return this.trackId;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import type { TimelineTrack } from "@/lib/timeline";
|
import type { TimelineTrack } from "@/lib/timeline";
|
||||||
import { getMainTrack } from "@/lib/timeline/placement";
|
import { getMainTrack } from "@/lib/timeline/placement";
|
||||||
|
|
@ -10,7 +10,7 @@ export class RemoveTrackCommand extends Command {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
const targetTrack = this.savedState.find(
|
const targetTrack = this.savedState.find(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import type { TimelineTrack } from "@/lib/timeline";
|
import type { TimelineTrack } from "@/lib/timeline";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { canTrackHaveAudio } from "@/lib/timeline";
|
import { canTrackHaveAudio } from "@/lib/timeline";
|
||||||
|
|
@ -10,7 +10,7 @@ export class ToggleTrackMuteCommand extends Command {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import type { TimelineTrack } from "@/lib/timeline";
|
import type { TimelineTrack } from "@/lib/timeline";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { canTrackBeHidden } from "@/lib/timeline";
|
import { canTrackBeHidden } from "@/lib/timeline";
|
||||||
|
|
@ -10,7 +10,7 @@ export class ToggleTrackVisibilityCommand extends Command {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,21 @@
|
||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
||||||
import type { TimelineTrack } from "@/lib/timeline";
|
import type { TimelineTrack } from "@/lib/timeline";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
|
|
||||||
export class TracksSnapshotCommand extends Command {
|
export class TracksSnapshotCommand extends Command {
|
||||||
constructor(
|
constructor(
|
||||||
private before: TimelineTrack[],
|
private before: TimelineTrack[],
|
||||||
private after: TimelineTrack[],
|
private after: TimelineTrack[],
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
EditorCore.getInstance().timeline.updateTracks(this.after);
|
EditorCore.getInstance().timeline.updateTracks(this.after);
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
undo(): void {
|
|
||||||
EditorCore.getInstance().timeline.updateTracks(this.before);
|
undo(): void {
|
||||||
}
|
EditorCore.getInstance().timeline.updateTracks(this.before);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue