diff options
Diffstat (limited to 'apps/web/src/lib/commands/timeline')
32 files changed, 1431 insertions, 1405 deletions
diff --git a/apps/web/src/lib/commands/timeline/clipboard/paste.ts b/apps/web/src/lib/commands/timeline/clipboard/paste.ts index 59e1af1c..9fa4b83b 100644 --- a/apps/web/src/lib/commands/timeline/clipboard/paste.ts +++ b/apps/web/src/lib/commands/timeline/clipboard/paste.ts @@ -33,7 +33,7 @@ export class PasteCommand extends Command { } execute(): CommandResult | undefined { - if (this.clipboardItems.length === 0) return; + if (this.clipboardItems.length === 0) return undefined; const editor = EditorCore.getInstance(); this.savedState = editor.timeline.getTracks(); @@ -123,6 +123,7 @@ export class PasteCommand extends Command { if (this.pastedElements.length > 0) { return { select: this.pastedElements }; } + return undefined; } undo(): void { diff --git a/apps/web/src/lib/commands/timeline/element/delete-elements.ts b/apps/web/src/lib/commands/timeline/element/delete-elements.ts index 9177a3f8..1e1ad661 100644 --- a/apps/web/src/lib/commands/timeline/element/delete-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/delete-elements.ts @@ -20,7 +20,7 @@ export class DeleteElementsCommand extends Command { this.rippleEnabled = rippleEnabled; } - execute(): CommandResult { + execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); this.savedState = editor.timeline.getTracks(); diff --git a/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts b/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts index ad31ae9c..35be3da2 100644 --- a/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts @@ -91,6 +91,7 @@ export class DuplicateElementsCommand extends Command { select: this.duplicatedElements, }; } + return undefined; } undo(): void { diff --git a/apps/web/src/lib/commands/timeline/element/effects/add-effect.ts b/apps/web/src/lib/commands/timeline/element/effects/add-effect.ts index 3d27b345..0c117f3b 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/add-effect.ts +++ b/apps/web/src/lib/commands/timeline/element/effects/add-effect.ts @@ -1,74 +1,75 @@ -import { Command } from "@/lib/commands/base-command";
-import { EditorCore } from "@/core";
-import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
-import type { TimelineTrack, VisualElement } from "@/lib/timeline";
-import { buildDefaultEffectInstance } from "@/lib/effects";
-
-function addEffectToElement({
- element,
- effectType,
-}: {
- element: VisualElement;
- effectType: string;
-}): VisualElement {
- const instance = buildDefaultEffectInstance({ effectType });
- const currentEffects = element.effects ?? [];
- return { ...element, effects: [...currentEffects, instance] };
-}
-
-export class AddClipEffectCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
- private effectId: string | null = null;
- private readonly trackId: string;
- private readonly elementId: string;
- private readonly effectType: string;
-
- constructor({
- trackId,
- elementId,
- effectType,
- }: {
- trackId: string;
- elementId: string;
- effectType: string;
- }) {
- super();
- this.trackId = trackId;
- this.elementId = elementId;
- this.effectType = effectType;
- }
-
- execute(): void {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const updatedTracks = updateElementInTracks({
- tracks: this.savedState,
- trackId: this.trackId,
- elementId: this.elementId,
- elementPredicate: isVisualElement,
- update: (element) => {
- const updated = addEffectToElement({
- element: element as VisualElement,
- effectType: this.effectType,
- });
- const effects = updated.effects ?? [];
- this.effectId = effects[effects.length - 1]?.id ?? null;
- return updated;
- },
- });
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (this.savedState) {
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
- }
-
- getEffectId(): string | null {
- return this.effectId;
- }
-}
+import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { EditorCore } from "@/core"; +import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; +import type { TimelineTrack, VisualElement } from "@/lib/timeline"; +import { buildDefaultEffectInstance } from "@/lib/effects"; + +function addEffectToElement({ + element, + effectType, +}: { + element: VisualElement; + effectType: string; +}): VisualElement { + const instance = buildDefaultEffectInstance({ effectType }); + const currentEffects = element.effects ?? []; + return { ...element, effects: [...currentEffects, instance] }; +} + +export class AddClipEffectCommand extends Command { + private savedState: TimelineTrack[] | null = null; + private effectId: string | null = null; + private readonly trackId: string; + private readonly elementId: string; + private readonly effectType: string; + + constructor({ + trackId, + elementId, + effectType, + }: { + trackId: string; + elementId: string; + effectType: string; + }) { + super(); + this.trackId = trackId; + this.elementId = elementId; + this.effectType = effectType; + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.timeline.getTracks(); + + const updatedTracks = updateElementInTracks({ + tracks: this.savedState, + trackId: this.trackId, + elementId: this.elementId, + elementPredicate: isVisualElement, + update: (element) => { + const updated = addEffectToElement({ + element: element as VisualElement, + effectType: this.effectType, + }); + const effects = updated.effects ?? []; + this.effectId = effects[effects.length - 1]?.id ?? null; + return updated; + }, + }); + + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } + + getEffectId(): string | null { + return this.effectId; + } +} diff --git a/apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts b/apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts index 88b445f3..f876a1d1 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts +++ b/apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts @@ -1,65 +1,66 @@ -import { Command } from "@/lib/commands/base-command";
-import { EditorCore } from "@/core";
-import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
-import type { TimelineTrack, VisualElement } from "@/lib/timeline";
-
-function removeEffectFromElement({
- element,
- effectId,
-}: {
- element: VisualElement;
- effectId: string;
-}): VisualElement {
- const currentEffects = element.effects ?? [];
- const filtered = currentEffects.filter((effect) => effect.id !== effectId);
- return { ...element, effects: filtered };
-}
-
-export class RemoveClipEffectCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
- private readonly trackId: string;
- private readonly elementId: string;
- private readonly effectId: string;
-
- constructor({
- trackId,
- elementId,
- effectId,
- }: {
- trackId: string;
- elementId: string;
- effectId: string;
- }) {
- super();
- this.trackId = trackId;
- this.elementId = elementId;
- this.effectId = effectId;
- }
-
- execute(): void {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const updatedTracks = updateElementInTracks({
- tracks: this.savedState,
- trackId: this.trackId,
- elementId: this.elementId,
- elementPredicate: isVisualElement,
- update: (element) => {
- return removeEffectFromElement({
- element: element as VisualElement,
- effectId: this.effectId,
- });
- },
- });
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (this.savedState) {
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
- }
-}
+import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { EditorCore } from "@/core"; +import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; +import type { TimelineTrack, VisualElement } from "@/lib/timeline"; + +function removeEffectFromElement({ + element, + effectId, +}: { + element: VisualElement; + effectId: string; +}): VisualElement { + const currentEffects = element.effects ?? []; + const filtered = currentEffects.filter((effect) => effect.id !== effectId); + return { ...element, effects: filtered }; +} + +export class RemoveClipEffectCommand extends Command { + private savedState: TimelineTrack[] | null = null; + private readonly trackId: string; + private readonly elementId: string; + private readonly effectId: string; + + constructor({ + trackId, + elementId, + effectId, + }: { + trackId: string; + elementId: string; + effectId: string; + }) { + super(); + this.trackId = trackId; + this.elementId = elementId; + this.effectId = effectId; + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.timeline.getTracks(); + + const updatedTracks = updateElementInTracks({ + tracks: this.savedState, + trackId: this.trackId, + elementId: this.elementId, + elementPredicate: isVisualElement, + update: (element) => { + return removeEffectFromElement({ + element: element as VisualElement, + effectId: this.effectId, + }); + }, + }); + + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } +} diff --git a/apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts b/apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts index af211a29..e63430be 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts +++ b/apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts @@ -1,73 +1,74 @@ -import { Command } from "@/lib/commands/base-command";
-import { EditorCore } from "@/core";
-import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
-import type { TimelineTrack, VisualElement } from "@/lib/timeline";
-
-function reorderEffectsOnElement({
- element,
- fromIndex,
- toIndex,
-}: {
- element: VisualElement;
- fromIndex: number;
- toIndex: number;
-}): VisualElement {
- const effects = [...(element.effects ?? [])];
- const [moved] = effects.splice(fromIndex, 1);
- effects.splice(toIndex, 0, moved);
- return { ...element, effects };
-}
-
-export class ReorderClipEffectsCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
- private readonly trackId: string;
- private readonly elementId: string;
- private readonly fromIndex: number;
- private readonly toIndex: number;
-
- constructor({
- trackId,
- elementId,
- fromIndex,
- toIndex,
- }: {
- trackId: string;
- elementId: string;
- fromIndex: number;
- toIndex: number;
- }) {
- super();
- this.trackId = trackId;
- this.elementId = elementId;
- this.fromIndex = fromIndex;
- this.toIndex = toIndex;
- }
-
- execute(): void {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const updatedTracks = updateElementInTracks({
- tracks: this.savedState,
- trackId: this.trackId,
- elementId: this.elementId,
- elementPredicate: isVisualElement,
- update: (element) => {
- return reorderEffectsOnElement({
- element: element as VisualElement,
- fromIndex: this.fromIndex,
- toIndex: this.toIndex,
- });
- },
- });
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (this.savedState) {
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
- }
-}
+import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { EditorCore } from "@/core"; +import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; +import type { TimelineTrack, VisualElement } from "@/lib/timeline"; + +function reorderEffectsOnElement({ + element, + fromIndex, + toIndex, +}: { + element: VisualElement; + fromIndex: number; + toIndex: number; +}): VisualElement { + const effects = [...(element.effects ?? [])]; + const [moved] = effects.splice(fromIndex, 1); + effects.splice(toIndex, 0, moved); + return { ...element, effects }; +} + +export class ReorderClipEffectsCommand extends Command { + private savedState: TimelineTrack[] | null = null; + private readonly trackId: string; + private readonly elementId: string; + private readonly fromIndex: number; + private readonly toIndex: number; + + constructor({ + trackId, + elementId, + fromIndex, + toIndex, + }: { + trackId: string; + elementId: string; + fromIndex: number; + toIndex: number; + }) { + super(); + this.trackId = trackId; + this.elementId = elementId; + this.fromIndex = fromIndex; + this.toIndex = toIndex; + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.timeline.getTracks(); + + const updatedTracks = updateElementInTracks({ + tracks: this.savedState, + trackId: this.trackId, + elementId: this.elementId, + elementPredicate: isVisualElement, + update: (element) => { + return reorderEffectsOnElement({ + element: element as VisualElement, + fromIndex: this.fromIndex, + toIndex: this.toIndex, + }); + }, + }); + + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } +} diff --git a/apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts b/apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts index 2e55b676..0329f1a0 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts +++ b/apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts @@ -1,67 +1,68 @@ -import { Command } from "@/lib/commands/base-command";
-import { EditorCore } from "@/core";
-import { isVisualElement, updateElementInTracks } from "@/lib/timeline";
-import type { TimelineTrack, VisualElement } from "@/lib/timeline";
-
-export function toggleEffectOnElement({
- element,
- effectId,
-}: {
- element: VisualElement;
- effectId: string;
-}): VisualElement {
- const currentEffects = element.effects ?? [];
- const updated = currentEffects.map((effect) =>
- effect.id === effectId ? { ...effect, enabled: !effect.enabled } : effect,
- );
- return { ...element, effects: updated };
-}
-
-export class ToggleClipEffectCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
- private readonly trackId: string;
- private readonly elementId: string;
- private readonly effectId: string;
-
- constructor({
- trackId,
- elementId,
- effectId,
- }: {
- trackId: string;
- elementId: string;
- effectId: string;
- }) {
- super();
- this.trackId = trackId;
- this.elementId = elementId;
- this.effectId = effectId;
- }
-
- execute(): void {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const updatedTracks = updateElementInTracks({
- tracks: this.savedState,
- trackId: this.trackId,
- elementId: this.elementId,
- elementPredicate: isVisualElement,
- update: (element) => {
- return toggleEffectOnElement({
- element: element as VisualElement,
- effectId: this.effectId,
- });
- },
- });
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (this.savedState) {
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
- }
-}
+import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { EditorCore } from "@/core"; +import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; +import type { TimelineTrack, VisualElement } from "@/lib/timeline"; + +export function toggleEffectOnElement({ + element, + effectId, +}: { + element: VisualElement; + effectId: string; +}): VisualElement { + const currentEffects = element.effects ?? []; + const updated = currentEffects.map((effect) => + effect.id === effectId ? { ...effect, enabled: !effect.enabled } : effect, + ); + return { ...element, effects: updated }; +} + +export class ToggleClipEffectCommand extends Command { + private savedState: TimelineTrack[] | null = null; + private readonly trackId: string; + private readonly elementId: string; + private readonly effectId: string; + + constructor({ + trackId, + elementId, + effectId, + }: { + trackId: string; + elementId: string; + effectId: string; + }) { + super(); + this.trackId = trackId; + this.elementId = elementId; + this.effectId = effectId; + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.timeline.getTracks(); + + const updatedTracks = updateElementInTracks({ + tracks: this.savedState, + trackId: this.trackId, + elementId: this.elementId, + elementPredicate: isVisualElement, + update: (element) => { + return toggleEffectOnElement({ + element: element as VisualElement, + effectId: this.effectId, + }); + }, + }); + + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } +} diff --git a/apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts b/apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts index c3443c22..058706ec 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts +++ b/apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts @@ -1,4 +1,4 @@ -import { Command } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/lib/commands/base-command"; import { EditorCore } from "@/core"; import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; import type { ParamValues } from "@/lib/params"; @@ -56,7 +56,7 @@ export class UpdateClipEffectParamsCommand extends Command { this.params = params; } - execute(): void { + execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); this.savedState = editor.timeline.getTracks(); @@ -75,6 +75,7 @@ export class UpdateClipEffectParamsCommand extends Command { }); editor.timeline.updateTracks(updatedTracks); + return undefined; } undo(): void { diff --git a/apps/web/src/lib/commands/timeline/element/index.ts b/apps/web/src/lib/commands/timeline/element/index.ts index 5ce48d55..60e12ddd 100644 --- a/apps/web/src/lib/commands/timeline/element/index.ts +++ b/apps/web/src/lib/commands/timeline/element/index.ts @@ -10,6 +10,7 @@ export { ToggleElementsVisibilityCommand } from "./toggle-elements-visibility"; export { ToggleElementsMutedCommand } from "./toggle-elements-muted";
export { ToggleSourceAudioSeparationCommand } from "./toggle-source-audio-separation";
export { MoveElementCommand } from "./move-elements";
+
export * from "./keyframes";
export * from "./effects";
export * from "./masks";
diff --git a/apps/web/src/lib/commands/timeline/element/insert-element.ts b/apps/web/src/lib/commands/timeline/element/insert-element.ts index 2119d219..fbef1613 100644 --- a/apps/web/src/lib/commands/timeline/element/insert-element.ts +++ b/apps/web/src/lib/commands/timeline/element/insert-element.ts @@ -1,4 +1,4 @@ -import { Command } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core";
import type {
CreateTimelineElement,
@@ -42,7 +42,7 @@ export class InsertElementCommand extends Command { private element: CreateTimelineElement;
private placement: InsertElementPlacement;
- execute(): void {
+ execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks();
diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts b/apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts index aafa3c22..36f8a6b1 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts +++ b/apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts @@ -1,68 +1,69 @@ -import { EditorCore } from "@/core";
-import { Command } from "@/lib/commands/base-command";
-import { removeEffectParamKeyframe } from "@/lib/animation/effect-param-channel";
-import { updateElementInTracks } from "@/lib/timeline";
-import { isVisualElement } from "@/lib/timeline/element-utils";
-import type { TimelineTrack } from "@/lib/timeline";
-
-export class RemoveEffectParamKeyframeCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
- private readonly trackId: string;
- private readonly elementId: string;
- private readonly effectId: string;
- private readonly paramKey: string;
- private readonly keyframeId: string;
-
- constructor({
- trackId,
- elementId,
- effectId,
- paramKey,
- keyframeId,
- }: {
- trackId: string;
- elementId: string;
- effectId: string;
- paramKey: string;
- keyframeId: string;
- }) {
- super();
- this.trackId = trackId;
- this.elementId = elementId;
- this.effectId = effectId;
- this.paramKey = paramKey;
- this.keyframeId = keyframeId;
- }
-
- execute(): void {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const updatedTracks = updateElementInTracks({
- tracks: this.savedState,
- trackId: this.trackId,
- elementId: this.elementId,
- elementPredicate: isVisualElement,
- update: (element) => {
- const animations = removeEffectParamKeyframe({
- animations: element.animations,
- effectId: this.effectId,
- paramKey: this.paramKey,
- keyframeId: this.keyframeId,
- });
- return { ...element, animations };
- },
- });
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (!this.savedState) {
- return;
- }
-
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
-}
+import { EditorCore } from "@/core"; +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { removeEffectParamKeyframe } from "@/lib/animation/effect-param-channel"; +import { updateElementInTracks } from "@/lib/timeline"; +import { isVisualElement } from "@/lib/timeline/element-utils"; +import type { TimelineTrack } from "@/lib/timeline"; + +export class RemoveEffectParamKeyframeCommand extends Command { + private savedState: TimelineTrack[] | null = null; + private readonly trackId: string; + private readonly elementId: string; + private readonly effectId: string; + private readonly paramKey: string; + private readonly keyframeId: string; + + constructor({ + trackId, + elementId, + effectId, + paramKey, + keyframeId, + }: { + trackId: string; + elementId: string; + effectId: string; + paramKey: string; + keyframeId: string; + }) { + super(); + this.trackId = trackId; + this.elementId = elementId; + this.effectId = effectId; + this.paramKey = paramKey; + this.keyframeId = keyframeId; + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.timeline.getTracks(); + + const updatedTracks = updateElementInTracks({ + tracks: this.savedState, + trackId: this.trackId, + elementId: this.elementId, + elementPredicate: isVisualElement, + update: (element) => { + const animations = removeEffectParamKeyframe({ + animations: element.animations, + effectId: this.effectId, + paramKey: this.paramKey, + keyframeId: this.keyframeId, + }); + return { ...element, animations }; + }, + }); + + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (!this.savedState) { + return; + } + + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } +} diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts b/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts index 56676aa5..b5c7550d 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts +++ b/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts @@ -1,138 +1,139 @@ -import { EditorCore } from "@/core";
-import {
- getChannel,
- getChannelValueAtTime,
- removeElementKeyframe,
- resolveAnimationTarget,
-} from "@/lib/animation";
-import { Command } from "@/lib/commands/base-command";
-import { updateElementInTracks } from "@/lib/timeline";
-import type { AnimationPath, AnimationValue } from "@/lib/animation/types";
-import type { TimelineElement, TimelineTrack } from "@/lib/timeline";
-
-function sampleValueBeforeRemoval({
- element,
- propertyPath,
- keyframeId,
-}: {
- element: TimelineElement;
- propertyPath: AnimationPath;
- keyframeId: string;
-}): AnimationValue | null {
- const channel = getChannel({
- animations: element.animations,
- propertyPath,
- });
- const keyframe = channel?.keyframes.find(
- (candidate) => candidate.id === keyframeId,
- );
- if (!channel || !keyframe) {
- return null;
- }
-
- const target = resolveAnimationTarget({ element, path: propertyPath });
- if (!target) {
- return null;
- }
- const baseValue = target.getBaseValue();
- if (baseValue === null) {
- return null;
- }
-
- return getChannelValueAtTime({
- channel,
- time: keyframe.time,
- fallbackValue: baseValue,
- });
-}
-
-function removeKeyframeAndPersist({
- element,
- propertyPath,
- keyframeId,
-}: {
- element: TimelineElement;
- propertyPath: AnimationPath;
- keyframeId: string;
-}): TimelineElement {
- const target = resolveAnimationTarget({ element, path: propertyPath });
- if (!target) {
- return element;
- }
-
- const valueBefore = sampleValueBeforeRemoval({
- element,
- propertyPath,
- keyframeId,
- });
-
- const nextAnimations = removeElementKeyframe({
- animations: element.animations,
- propertyPath,
- keyframeId,
- });
-
- const isChannelNowEmpty =
- getChannel({ animations: nextAnimations, propertyPath }) === undefined;
- const shouldPersistToBase = isChannelNowEmpty && valueBefore !== null;
-
- const baseElement = shouldPersistToBase
- ? target.setBaseValue(valueBefore)
- : element;
-
- return { ...baseElement, animations: nextAnimations };
-}
-
-export class RemoveKeyframeCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
- private readonly trackId: string;
- private readonly elementId: string;
- private readonly propertyPath: AnimationPath;
- private readonly keyframeId: string;
-
- constructor({
- trackId,
- elementId,
- propertyPath,
- keyframeId,
- }: {
- trackId: string;
- elementId: string;
- propertyPath: AnimationPath;
- keyframeId: string;
- }) {
- super();
- this.trackId = trackId;
- this.elementId = elementId;
- this.propertyPath = propertyPath;
- this.keyframeId = keyframeId;
- }
-
- execute(): void {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const updatedTracks = updateElementInTracks({
- tracks: this.savedState,
- trackId: this.trackId,
- elementId: this.elementId,
- update: (element) =>
- removeKeyframeAndPersist({
- element,
- propertyPath: this.propertyPath,
- keyframeId: this.keyframeId,
- }),
- });
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (!this.savedState) {
- return;
- }
-
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
-}
+import { EditorCore } from "@/core"; +import { + getChannel, + getChannelValueAtTime, + removeElementKeyframe, + resolveAnimationTarget, +} from "@/lib/animation"; +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { updateElementInTracks } from "@/lib/timeline"; +import type { AnimationPath, AnimationValue } from "@/lib/animation/types"; +import type { TimelineElement, TimelineTrack } from "@/lib/timeline"; + +function sampleValueBeforeRemoval({ + element, + propertyPath, + keyframeId, +}: { + element: TimelineElement; + propertyPath: AnimationPath; + keyframeId: string; +}): AnimationValue | null { + const channel = getChannel({ + animations: element.animations, + propertyPath, + }); + const keyframe = channel?.keyframes.find( + (candidate) => candidate.id === keyframeId, + ); + if (!channel || !keyframe) { + return null; + } + + const target = resolveAnimationTarget({ element, path: propertyPath }); + if (!target) { + return null; + } + const baseValue = target.getBaseValue(); + if (baseValue === null) { + return null; + } + + return getChannelValueAtTime({ + channel, + time: keyframe.time, + fallbackValue: baseValue, + }); +} + +function removeKeyframeAndPersist({ + element, + propertyPath, + keyframeId, +}: { + element: TimelineElement; + propertyPath: AnimationPath; + keyframeId: string; +}): TimelineElement { + const target = resolveAnimationTarget({ element, path: propertyPath }); + if (!target) { + return element; + } + + const valueBefore = sampleValueBeforeRemoval({ + element, + propertyPath, + keyframeId, + }); + + const nextAnimations = removeElementKeyframe({ + animations: element.animations, + propertyPath, + keyframeId, + }); + + const isChannelNowEmpty = + getChannel({ animations: nextAnimations, propertyPath }) === undefined; + const shouldPersistToBase = isChannelNowEmpty && valueBefore !== null; + + const baseElement = shouldPersistToBase + ? target.setBaseValue(valueBefore) + : element; + + return { ...baseElement, animations: nextAnimations }; +} + +export class RemoveKeyframeCommand extends Command { + private savedState: TimelineTrack[] | null = null; + private readonly trackId: string; + private readonly elementId: string; + private readonly propertyPath: AnimationPath; + private readonly keyframeId: string; + + constructor({ + trackId, + elementId, + propertyPath, + keyframeId, + }: { + trackId: string; + elementId: string; + propertyPath: AnimationPath; + keyframeId: string; + }) { + super(); + this.trackId = trackId; + this.elementId = elementId; + this.propertyPath = propertyPath; + this.keyframeId = keyframeId; + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.timeline.getTracks(); + + const updatedTracks = updateElementInTracks({ + tracks: this.savedState, + trackId: this.trackId, + elementId: this.elementId, + update: (element) => + removeKeyframeAndPersist({ + element, + propertyPath: this.propertyPath, + keyframeId: this.keyframeId, + }), + }); + + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (!this.savedState) { + return; + } + + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } +} diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts b/apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts index 7818ed0d..89491823 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts +++ b/apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts @@ -1,74 +1,75 @@ -import { EditorCore } from "@/core";
-import { resolveAnimationTarget, retimeElementKeyframe } from "@/lib/animation";
-import { Command } from "@/lib/commands/base-command";
-import { updateElementInTracks } from "@/lib/timeline";
-import type { AnimationPath } from "@/lib/animation/types";
-import type { TimelineTrack } from "@/lib/timeline";
-
-export class RetimeKeyframeCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
- private readonly trackId: string;
- private readonly elementId: string;
- private readonly propertyPath: AnimationPath;
- private readonly keyframeId: string;
- private readonly nextTime: number;
-
- constructor({
- trackId,
- elementId,
- propertyPath,
- keyframeId,
- nextTime,
- }: {
- trackId: string;
- elementId: string;
- propertyPath: AnimationPath;
- keyframeId: string;
- nextTime: number;
- }) {
- super();
- this.trackId = trackId;
- this.elementId = elementId;
- this.propertyPath = propertyPath;
- this.keyframeId = keyframeId;
- this.nextTime = nextTime;
- }
-
- execute(): void {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const updatedTracks = updateElementInTracks({
- tracks: this.savedState,
- trackId: this.trackId,
- elementId: this.elementId,
- update: (element) => {
- if (!resolveAnimationTarget({ element, path: this.propertyPath })) {
- return element;
- }
-
- const boundedTime = Math.max(0, Math.min(this.nextTime, element.duration));
- return {
- ...element,
- animations: retimeElementKeyframe({
- animations: element.animations,
- propertyPath: this.propertyPath,
- keyframeId: this.keyframeId,
- time: boundedTime,
- }),
- };
- },
- });
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (!this.savedState) {
- return;
- }
-
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
-}
+import { EditorCore } from "@/core"; +import { resolveAnimationTarget, retimeElementKeyframe } from "@/lib/animation"; +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { updateElementInTracks } from "@/lib/timeline"; +import type { AnimationPath } from "@/lib/animation/types"; +import type { TimelineTrack } from "@/lib/timeline"; + +export class RetimeKeyframeCommand extends Command { + private savedState: TimelineTrack[] | null = null; + private readonly trackId: string; + private readonly elementId: string; + private readonly propertyPath: AnimationPath; + private readonly keyframeId: string; + private readonly nextTime: number; + + constructor({ + trackId, + elementId, + propertyPath, + keyframeId, + nextTime, + }: { + trackId: string; + elementId: string; + propertyPath: AnimationPath; + keyframeId: string; + nextTime: number; + }) { + super(); + this.trackId = trackId; + this.elementId = elementId; + this.propertyPath = propertyPath; + this.keyframeId = keyframeId; + this.nextTime = nextTime; + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.timeline.getTracks(); + + const updatedTracks = updateElementInTracks({ + tracks: this.savedState, + trackId: this.trackId, + elementId: this.elementId, + update: (element) => { + if (!resolveAnimationTarget({ element, path: this.propertyPath })) { + return element; + } + + const boundedTime = Math.max(0, Math.min(this.nextTime, element.duration)); + return { + ...element, + animations: retimeElementKeyframe({ + animations: element.animations, + propertyPath: this.propertyPath, + keyframeId: this.keyframeId, + time: boundedTime, + }), + }; + }, + }); + + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (!this.savedState) { + return; + } + + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } +} diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts b/apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts index 8e541b72..edbf5198 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts +++ b/apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts @@ -1,84 +1,85 @@ -import { EditorCore } from "@/core";
-import { Command } from "@/lib/commands/base-command";
-import { upsertEffectParamKeyframe } from "@/lib/animation/effect-param-channel";
-import { updateElementInTracks } from "@/lib/timeline";
-import { isVisualElement } from "@/lib/timeline/element-utils";
-import type { TimelineTrack } from "@/lib/timeline";
-
-export class UpsertEffectParamKeyframeCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
- private readonly trackId: string;
- private readonly elementId: string;
- private readonly effectId: string;
- private readonly paramKey: string;
- private readonly time: number;
- private readonly value: number;
- private readonly interpolation: "linear" | "hold" | undefined;
- private readonly keyframeId: string | undefined;
-
- constructor({
- trackId,
- elementId,
- effectId,
- paramKey,
- time,
- value,
- interpolation,
- keyframeId,
- }: {
- trackId: string;
- elementId: string;
- effectId: string;
- paramKey: string;
- time: number;
- value: number;
- interpolation?: "linear" | "hold";
- keyframeId?: string;
- }) {
- super();
- this.trackId = trackId;
- this.elementId = elementId;
- this.effectId = effectId;
- this.paramKey = paramKey;
- this.time = time;
- this.value = value;
- this.interpolation = interpolation;
- this.keyframeId = keyframeId;
- }
-
- execute(): void {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const updatedTracks = updateElementInTracks({
- tracks: this.savedState,
- trackId: this.trackId,
- elementId: this.elementId,
- elementPredicate: isVisualElement,
- update: (element) => {
- const boundedTime = Math.max(0, Math.min(this.time, element.duration));
- const animations = upsertEffectParamKeyframe({
- animations: element.animations,
- effectId: this.effectId,
- paramKey: this.paramKey,
- time: boundedTime,
- value: this.value,
- interpolation: this.interpolation,
- keyframeId: this.keyframeId,
- });
- return { ...element, animations };
- },
- });
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (!this.savedState) {
- return;
- }
-
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
-}
+import { EditorCore } from "@/core"; +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { upsertEffectParamKeyframe } from "@/lib/animation/effect-param-channel"; +import { updateElementInTracks } from "@/lib/timeline"; +import { isVisualElement } from "@/lib/timeline/element-utils"; +import type { TimelineTrack } from "@/lib/timeline"; + +export class UpsertEffectParamKeyframeCommand extends Command { + private savedState: TimelineTrack[] | null = null; + private readonly trackId: string; + private readonly elementId: string; + private readonly effectId: string; + private readonly paramKey: string; + private readonly time: number; + private readonly value: number; + private readonly interpolation: "linear" | "hold" | undefined; + private readonly keyframeId: string | undefined; + + constructor({ + trackId, + elementId, + effectId, + paramKey, + time, + value, + interpolation, + keyframeId, + }: { + trackId: string; + elementId: string; + effectId: string; + paramKey: string; + time: number; + value: number; + interpolation?: "linear" | "hold"; + keyframeId?: string; + }) { + super(); + this.trackId = trackId; + this.elementId = elementId; + this.effectId = effectId; + this.paramKey = paramKey; + this.time = time; + this.value = value; + this.interpolation = interpolation; + this.keyframeId = keyframeId; + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.timeline.getTracks(); + + const updatedTracks = updateElementInTracks({ + tracks: this.savedState, + trackId: this.trackId, + elementId: this.elementId, + elementPredicate: isVisualElement, + update: (element) => { + const boundedTime = Math.max(0, Math.min(this.time, element.duration)); + const animations = upsertEffectParamKeyframe({ + animations: element.animations, + effectId: this.effectId, + paramKey: this.paramKey, + time: boundedTime, + value: this.value, + interpolation: this.interpolation, + keyframeId: this.keyframeId, + }); + return { ...element, animations }; + }, + }); + + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (!this.savedState) { + return; + } + + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } +} diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts b/apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts index 4b697705..98589987 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts +++ b/apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts @@ -1,95 +1,96 @@ -import { EditorCore } from "@/core";
-import { Command } from "@/lib/commands/base-command";
-import { resolveAnimationTarget, upsertPathKeyframe } from "@/lib/animation";
-import { updateElementInTracks } from "@/lib/timeline";
-import type { TimelineTrack } from "@/lib/timeline";
-import type {
- AnimationPath,
- AnimationInterpolation,
- AnimationValue,
-} from "@/lib/animation/types";
-
-export class UpsertKeyframeCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
- private readonly trackId: string;
- private readonly elementId: string;
- private readonly propertyPath: AnimationPath;
- private readonly time: number;
- private readonly value: AnimationValue;
- private readonly interpolation: AnimationInterpolation | undefined;
- private readonly keyframeId: string | undefined;
-
- constructor({
- trackId,
- elementId,
- propertyPath,
- time,
- value,
- interpolation,
- keyframeId,
- }: {
- trackId: string;
- elementId: string;
- propertyPath: AnimationPath;
- time: number;
- value: AnimationValue;
- interpolation?: AnimationInterpolation;
- keyframeId?: string;
- }) {
- super();
- this.trackId = trackId;
- this.elementId = elementId;
- this.propertyPath = propertyPath;
- this.time = time;
- this.value = value;
- this.interpolation = interpolation;
- this.keyframeId = keyframeId;
- }
-
- execute(): void {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const updatedTracks = updateElementInTracks({
- tracks: this.savedState,
- trackId: this.trackId,
- elementId: this.elementId,
- update: (element) => {
- const target = resolveAnimationTarget({
- element,
- path: this.propertyPath,
- });
- if (!target) {
- return element;
- }
-
- const boundedTime = Math.max(0, Math.min(this.time, element.duration));
- return {
- ...element,
- animations: upsertPathKeyframe({
- animations: element.animations,
- propertyPath: this.propertyPath,
- time: boundedTime,
- value: this.value,
- interpolation: this.interpolation,
- keyframeId: this.keyframeId,
- valueKind: target.valueKind,
- defaultInterpolation: target.defaultInterpolation,
- numericRange: target.numericRange,
- }),
- };
- },
- });
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (!this.savedState) {
- return;
- }
-
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
-}
+import { EditorCore } from "@/core"; +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { resolveAnimationTarget, upsertPathKeyframe } from "@/lib/animation"; +import { updateElementInTracks } from "@/lib/timeline"; +import type { TimelineTrack } from "@/lib/timeline"; +import type { + AnimationPath, + AnimationInterpolation, + AnimationValue, +} from "@/lib/animation/types"; + +export class UpsertKeyframeCommand extends Command { + private savedState: TimelineTrack[] | null = null; + private readonly trackId: string; + private readonly elementId: string; + private readonly propertyPath: AnimationPath; + private readonly time: number; + private readonly value: AnimationValue; + private readonly interpolation: AnimationInterpolation | undefined; + private readonly keyframeId: string | undefined; + + constructor({ + trackId, + elementId, + propertyPath, + time, + value, + interpolation, + keyframeId, + }: { + trackId: string; + elementId: string; + propertyPath: AnimationPath; + time: number; + value: AnimationValue; + interpolation?: AnimationInterpolation; + keyframeId?: string; + }) { + super(); + this.trackId = trackId; + this.elementId = elementId; + this.propertyPath = propertyPath; + this.time = time; + this.value = value; + this.interpolation = interpolation; + this.keyframeId = keyframeId; + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.timeline.getTracks(); + + const updatedTracks = updateElementInTracks({ + tracks: this.savedState, + trackId: this.trackId, + elementId: this.elementId, + update: (element) => { + const target = resolveAnimationTarget({ + element, + path: this.propertyPath, + }); + if (!target) { + return element; + } + + const boundedTime = Math.max(0, Math.min(this.time, element.duration)); + return { + ...element, + animations: upsertPathKeyframe({ + animations: element.animations, + propertyPath: this.propertyPath, + time: boundedTime, + value: this.value, + interpolation: this.interpolation, + keyframeId: this.keyframeId, + valueKind: target.valueKind, + defaultInterpolation: target.defaultInterpolation, + numericRange: target.numericRange, + }), + }; + }, + }); + + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (!this.savedState) { + return; + } + + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } +} diff --git a/apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts b/apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts index 71c8b413..b0daced7 100644 --- a/apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts +++ b/apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts @@ -1,64 +1,65 @@ -import { EditorCore } from "@/core";
-import { Command } from "@/lib/commands/base-command";
-import { isMaskableElement, updateElementInTracks } from "@/lib/timeline";
-import type { TimelineTrack, MaskableElement } from "@/lib/timeline";
-
-function removeMaskFromElement({
- element,
- maskId,
-}: {
- element: MaskableElement;
- maskId: string;
-}): MaskableElement {
- const currentMasks = element.masks ?? [];
- const filteredMasks = currentMasks.filter((mask) => mask.id !== maskId);
- return { ...element, masks: filteredMasks };
-}
-
-export class RemoveMaskCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
- private readonly trackId: string;
- private readonly elementId: string;
- private readonly maskId: string;
-
- constructor({
- trackId,
- elementId,
- maskId,
- }: {
- trackId: string;
- elementId: string;
- maskId: string;
- }) {
- super();
- this.trackId = trackId;
- this.elementId = elementId;
- this.maskId = maskId;
- }
-
- execute(): void {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const updatedTracks = updateElementInTracks({
- tracks: this.savedState,
- trackId: this.trackId,
- elementId: this.elementId,
- elementPredicate: isMaskableElement,
- update: (element) =>
- removeMaskFromElement({
- element: element as MaskableElement,
- maskId: this.maskId,
- }),
- });
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (this.savedState) {
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
- }
-}
+import { EditorCore } from "@/core"; +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { isMaskableElement, updateElementInTracks } from "@/lib/timeline"; +import type { TimelineTrack, MaskableElement } from "@/lib/timeline"; + +function removeMaskFromElement({ + element, + maskId, +}: { + element: MaskableElement; + maskId: string; +}): MaskableElement { + const currentMasks = element.masks ?? []; + const filteredMasks = currentMasks.filter((mask) => mask.id !== maskId); + return { ...element, masks: filteredMasks }; +} + +export class RemoveMaskCommand extends Command { + private savedState: TimelineTrack[] | null = null; + private readonly trackId: string; + private readonly elementId: string; + private readonly maskId: string; + + constructor({ + trackId, + elementId, + maskId, + }: { + trackId: string; + elementId: string; + maskId: string; + }) { + super(); + this.trackId = trackId; + this.elementId = elementId; + this.maskId = maskId; + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.timeline.getTracks(); + + const updatedTracks = updateElementInTracks({ + tracks: this.savedState, + trackId: this.trackId, + elementId: this.elementId, + elementPredicate: isMaskableElement, + update: (element) => + removeMaskFromElement({ + element: element as MaskableElement, + maskId: this.maskId, + }), + }); + + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } +} diff --git a/apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts b/apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts index cfa02842..bc812398 100644 --- a/apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts +++ b/apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts @@ -1,75 +1,76 @@ -import { EditorCore } from "@/core";
-import { Command } from "@/lib/commands/base-command";
-import { isMaskableElement, updateElementInTracks } from "@/lib/timeline";
-import type { Mask } from "@/lib/masks/types";
-import type { TimelineTrack, MaskableElement } from "@/lib/timeline";
-
-export function toggleMaskInvertedOnElement({
- element,
- maskId,
-}: {
- element: MaskableElement;
- maskId: string;
-}): MaskableElement {
- const currentMasks = element.masks ?? [];
- const toggleMask = <TMask extends Mask>(mask: TMask): TMask => ({
- ...mask,
- params: {
- ...mask.params,
- inverted: !mask.params.inverted,
- },
- });
- const updatedMasks = currentMasks.map((mask) =>
- mask.id !== maskId ? mask : toggleMask(mask),
- );
-
- return { ...element, masks: updatedMasks };
-}
-
-export class ToggleMaskInvertedCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
- private readonly trackId: string;
- private readonly elementId: string;
- private readonly maskId: string;
-
- constructor({
- trackId,
- elementId,
- maskId,
- }: {
- trackId: string;
- elementId: string;
- maskId: string;
- }) {
- super();
- this.trackId = trackId;
- this.elementId = elementId;
- this.maskId = maskId;
- }
-
- execute(): void {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const updatedTracks = updateElementInTracks({
- tracks: this.savedState,
- trackId: this.trackId,
- elementId: this.elementId,
- elementPredicate: isMaskableElement,
- update: (element) =>
- toggleMaskInvertedOnElement({
- element: element as MaskableElement,
- maskId: this.maskId,
- }),
- });
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (this.savedState) {
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
- }
-}
+import { EditorCore } from "@/core"; +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { isMaskableElement, updateElementInTracks } from "@/lib/timeline"; +import type { Mask } from "@/lib/masks/types"; +import type { TimelineTrack, MaskableElement } from "@/lib/timeline"; + +export function toggleMaskInvertedOnElement({ + element, + maskId, +}: { + element: MaskableElement; + maskId: string; +}): MaskableElement { + const currentMasks = element.masks ?? []; + const toggleMask = <TMask extends Mask>(mask: TMask): TMask => ({ + ...mask, + params: { + ...mask.params, + inverted: !mask.params.inverted, + }, + }); + const updatedMasks = currentMasks.map((mask) => + mask.id !== maskId ? mask : toggleMask(mask), + ); + + return { ...element, masks: updatedMasks }; +} + +export class ToggleMaskInvertedCommand extends Command { + private savedState: TimelineTrack[] | null = null; + private readonly trackId: string; + private readonly elementId: string; + private readonly maskId: string; + + constructor({ + trackId, + elementId, + maskId, + }: { + trackId: string; + elementId: string; + maskId: string; + }) { + super(); + this.trackId = trackId; + this.elementId = elementId; + this.maskId = maskId; + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.timeline.getTracks(); + + const updatedTracks = updateElementInTracks({ + tracks: this.savedState, + trackId: this.trackId, + elementId: this.elementId, + elementPredicate: isMaskableElement, + update: (element) => + toggleMaskInvertedOnElement({ + element: element as MaskableElement, + maskId: this.maskId, + }), + }); + + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } +} diff --git a/apps/web/src/lib/commands/timeline/element/move-elements.ts b/apps/web/src/lib/commands/timeline/element/move-elements.ts index 6d400964..e944ca81 100644 --- a/apps/web/src/lib/commands/timeline/element/move-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/move-elements.ts @@ -1,145 +1,146 @@ -import { Command } from "@/lib/commands/base-command";
-import { EditorCore } from "@/core";
-import type {
- TimelineTrack,
- TimelineElement,
- TrackType,
-} from "@/lib/timeline";
-import {
- buildEmptyTrack,
- validateElementTrackCompatibility,
- enforceMainTrackStart,
-} from "@/lib/timeline/placement";
-import { rippleShiftElements } from "@/lib/timeline/ripple-utils";
-
-export class MoveElementCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
- private readonly sourceTrackId: string;
- private readonly targetTrackId: string;
- private readonly elementId: string;
- private readonly newStartTime: number;
- private readonly createTrack: { type: TrackType; index: number } | undefined;
- private readonly rippleEnabled: boolean;
-
- constructor({
- sourceTrackId,
- targetTrackId,
- elementId,
- newStartTime,
- createTrack,
- rippleEnabled = false,
- }: {
- sourceTrackId: string;
- targetTrackId: string;
- elementId: string;
- newStartTime: number;
- createTrack?: { type: TrackType; index: number };
- rippleEnabled?: boolean;
- }) {
- super();
- this.sourceTrackId = sourceTrackId;
- this.targetTrackId = targetTrackId;
- this.elementId = elementId;
- this.newStartTime = newStartTime;
- this.createTrack = createTrack;
- this.rippleEnabled = rippleEnabled;
- }
-
- execute(): void {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const sourceTrack = this.savedState.find(
- (track) => track.id === this.sourceTrackId,
- );
- const element = sourceTrack?.elements.find(
- (trackElement) => trackElement.id === this.elementId,
- );
-
- if (!sourceTrack || !element) {
- throw new Error("Source track or element not found");
- }
-
- let targetTrack = this.savedState.find((track) => track.id === this.targetTrackId);
- let tracksToUpdate = this.savedState;
- if (!targetTrack && this.createTrack) {
- const newTrack = buildEmptyTrack({
- id: this.targetTrackId,
- type: this.createTrack.type,
- });
- tracksToUpdate = [...this.savedState];
- tracksToUpdate.splice(this.createTrack.index, 0, newTrack);
- targetTrack = newTrack;
- }
- if (!targetTrack) {
- throw new Error("Target track not found");
- }
-
- const validation = validateElementTrackCompatibility({
- element,
- track: targetTrack,
- });
-
- if (!validation.isValid) {
- throw new Error(validation.errorMessage);
- }
-
- const adjustedStartTime = enforceMainTrackStart({
- tracks: tracksToUpdate,
- targetTrackId: this.targetTrackId,
- requestedStartTime: this.newStartTime,
- excludeElementId: this.elementId,
- });
-
- // keyframe times remain clip-local, so moving only changes element startTime.
- const movedElement: TimelineElement = {
- ...element,
- startTime: adjustedStartTime,
- };
-
- const isSameTrack = this.sourceTrackId === this.targetTrackId;
-
- const updatedTracks = tracksToUpdate.map((track): TimelineTrack => {
- if (isSameTrack && track.id === this.sourceTrackId) {
- return {
- ...track,
- elements: track.elements.map((trackElement) =>
- trackElement.id === this.elementId ? movedElement : trackElement,
- ),
- } as typeof track;
- }
-
- if (track.id === this.sourceTrackId) {
- const remainingElements = track.elements.filter(
- (trackElement) => trackElement.id !== this.elementId,
- );
- const shiftedElements = this.rippleEnabled
- ? rippleShiftElements({
- elements: remainingElements,
- afterTime: element.startTime,
- shiftAmount: element.duration,
- })
- : remainingElements;
- return { ...track, elements: shiftedElements } as typeof track;
- }
-
- if (track.id === this.targetTrackId) {
- return {
- ...track,
- elements: [...track.elements, movedElement],
- } as typeof track;
- }
-
- return track;
- });
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (this.savedState) {
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
- }
-}
+import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { EditorCore } from "@/core"; +import type { + TimelineTrack, + TimelineElement, + TrackType, +} from "@/lib/timeline"; +import { + buildEmptyTrack, + validateElementTrackCompatibility, + enforceMainTrackStart, +} from "@/lib/timeline/placement"; +import { rippleShiftElements } from "@/lib/timeline/ripple-utils"; + +export class MoveElementCommand extends Command { + private savedState: TimelineTrack[] | null = null; + private readonly sourceTrackId: string; + private readonly targetTrackId: string; + private readonly elementId: string; + private readonly newStartTime: number; + private readonly createTrack: { type: TrackType; index: number } | undefined; + private readonly rippleEnabled: boolean; + + constructor({ + sourceTrackId, + targetTrackId, + elementId, + newStartTime, + createTrack, + rippleEnabled = false, + }: { + sourceTrackId: string; + targetTrackId: string; + elementId: string; + newStartTime: number; + createTrack?: { type: TrackType; index: number }; + rippleEnabled?: boolean; + }) { + super(); + this.sourceTrackId = sourceTrackId; + this.targetTrackId = targetTrackId; + this.elementId = elementId; + this.newStartTime = newStartTime; + this.createTrack = createTrack; + this.rippleEnabled = rippleEnabled; + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.timeline.getTracks(); + + const sourceTrack = this.savedState.find( + (track) => track.id === this.sourceTrackId, + ); + const element = sourceTrack?.elements.find( + (trackElement) => trackElement.id === this.elementId, + ); + + if (!sourceTrack || !element) { + throw new Error("Source track or element not found"); + } + + let targetTrack = this.savedState.find((track) => track.id === this.targetTrackId); + let tracksToUpdate = this.savedState; + if (!targetTrack && this.createTrack) { + const newTrack = buildEmptyTrack({ + id: this.targetTrackId, + type: this.createTrack.type, + }); + tracksToUpdate = [...this.savedState]; + tracksToUpdate.splice(this.createTrack.index, 0, newTrack); + targetTrack = newTrack; + } + if (!targetTrack) { + throw new Error("Target track not found"); + } + + const validation = validateElementTrackCompatibility({ + element, + track: targetTrack, + }); + + if (!validation.isValid) { + throw new Error(validation.errorMessage); + } + + const adjustedStartTime = enforceMainTrackStart({ + tracks: tracksToUpdate, + targetTrackId: this.targetTrackId, + requestedStartTime: this.newStartTime, + excludeElementId: this.elementId, + }); + + // keyframe times remain clip-local, so moving only changes element startTime. + const movedElement: TimelineElement = { + ...element, + startTime: adjustedStartTime, + }; + + const isSameTrack = this.sourceTrackId === this.targetTrackId; + + const updatedTracks = tracksToUpdate.map((track): TimelineTrack => { + if (isSameTrack && track.id === this.sourceTrackId) { + return { + ...track, + elements: track.elements.map((trackElement) => + trackElement.id === this.elementId ? movedElement : trackElement, + ), + } as typeof track; + } + + if (track.id === this.sourceTrackId) { + const remainingElements = track.elements.filter( + (trackElement) => trackElement.id !== this.elementId, + ); + const shiftedElements = this.rippleEnabled + ? rippleShiftElements({ + elements: remainingElements, + afterTime: element.startTime, + shiftAmount: element.duration, + }) + : remainingElements; + return { ...track, elements: shiftedElements } as typeof track; + } + + if (track.id === this.targetTrackId) { + return { + ...track, + elements: [...track.elements, movedElement], + } as typeof track; + } + + return track; + }); + + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } +} diff --git a/apps/web/src/lib/commands/timeline/element/retime/update-element-retime.ts b/apps/web/src/lib/commands/timeline/element/retime/update-element-retime.ts index f0574ce7..474396a1 100644 --- a/apps/web/src/lib/commands/timeline/element/retime/update-element-retime.ts +++ b/apps/web/src/lib/commands/timeline/element/retime/update-element-retime.ts @@ -1,114 +1,115 @@ -import { EditorCore } from "@/core";
-import { clampRetimeRate } from "@/lib/retime/rate";
-import { clampAnimationsToDuration } from "@/lib/animation";
-import { Command } from "@/lib/commands/base-command";
-import { getTimelineDurationForSourceSpan, getSourceSpanAtClipTime } from "@/lib/retime";
-import { isRetimableElement, updateElementInTracks } from "@/lib/timeline";
-import type { RetimeConfig, TimelineTrack } from "@/lib/timeline";
-
-function getSourceDuration({
- trimStart,
- trimEnd,
- duration,
- sourceDuration,
- retime,
-}: {
- trimStart: number;
- trimEnd: number;
- duration: number;
- sourceDuration?: number;
- retime?: RetimeConfig;
-}): number {
- if (typeof sourceDuration === "number") {
- return sourceDuration;
- }
-
- return (
- trimStart +
- getSourceSpanAtClipTime({
- clipTime: duration,
- retime,
- }) +
- trimEnd
- );
-}
-
-export class UpdateElementRetimeCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
- private readonly trackId: string;
- private readonly elementId: string;
- private readonly retime: RetimeConfig | undefined;
-
- constructor({
- trackId,
- elementId,
- retime,
- }: {
- trackId: string;
- elementId: string;
- retime?: RetimeConfig;
- }) {
- super();
- this.trackId = trackId;
- this.elementId = elementId;
- this.retime = retime;
- }
-
- execute(): void {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const updatedTracks = updateElementInTracks({
- tracks: this.savedState,
- trackId: this.trackId,
- elementId: this.elementId,
- elementPredicate: isRetimableElement,
- update: (element) => {
- if (!isRetimableElement(element)) {
- return element;
- }
-
- const nextRetime = this.retime
- ? {
- ...this.retime,
- rate: clampRetimeRate({ rate: this.retime.rate }),
- }
- : undefined;
- const sourceDuration = getSourceDuration({
- trimStart: element.trimStart,
- trimEnd: element.trimEnd,
- duration: element.duration,
- sourceDuration: element.sourceDuration,
- retime: element.retime,
- });
- const visibleSourceSpan = Math.max(
- 0,
- sourceDuration - element.trimStart - element.trimEnd,
- );
- const nextDuration = getTimelineDurationForSourceSpan({
- sourceSpan: visibleSourceSpan,
- retime: nextRetime,
- });
-
- return {
- ...element,
- retime: nextRetime,
- duration: nextDuration,
- animations: clampAnimationsToDuration({
- animations: element.animations,
- duration: nextDuration,
- }),
- };
- },
- });
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (this.savedState) {
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
- }
-}
+import { EditorCore } from "@/core"; +import { clampRetimeRate } from "@/lib/retime/rate"; +import { clampAnimationsToDuration } from "@/lib/animation"; +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { getTimelineDurationForSourceSpan, getSourceSpanAtClipTime } from "@/lib/retime"; +import { isRetimableElement, updateElementInTracks } from "@/lib/timeline"; +import type { RetimeConfig, TimelineTrack } from "@/lib/timeline"; + +function getSourceDuration({ + trimStart, + trimEnd, + duration, + sourceDuration, + retime, +}: { + trimStart: number; + trimEnd: number; + duration: number; + sourceDuration?: number; + retime?: RetimeConfig; +}): number { + if (typeof sourceDuration === "number") { + return sourceDuration; + } + + return ( + trimStart + + getSourceSpanAtClipTime({ + clipTime: duration, + retime, + }) + + trimEnd + ); +} + +export class UpdateElementRetimeCommand extends Command { + private savedState: TimelineTrack[] | null = null; + private readonly trackId: string; + private readonly elementId: string; + private readonly retime: RetimeConfig | undefined; + + constructor({ + trackId, + elementId, + retime, + }: { + trackId: string; + elementId: string; + retime?: RetimeConfig; + }) { + super(); + this.trackId = trackId; + this.elementId = elementId; + this.retime = retime; + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.timeline.getTracks(); + + const updatedTracks = updateElementInTracks({ + tracks: this.savedState, + trackId: this.trackId, + elementId: this.elementId, + elementPredicate: isRetimableElement, + update: (element) => { + if (!isRetimableElement(element)) { + return element; + } + + const nextRetime = this.retime + ? { + ...this.retime, + rate: clampRetimeRate({ rate: this.retime.rate }), + } + : undefined; + const sourceDuration = getSourceDuration({ + trimStart: element.trimStart, + trimEnd: element.trimEnd, + duration: element.duration, + sourceDuration: element.sourceDuration, + retime: element.retime, + }); + const visibleSourceSpan = Math.max( + 0, + sourceDuration - element.trimStart - element.trimEnd, + ); + const nextDuration = getTimelineDurationForSourceSpan({ + sourceSpan: visibleSourceSpan, + retime: nextRetime, + }); + + return { + ...element, + retime: nextRetime, + duration: nextDuration, + animations: clampAnimationsToDuration({ + animations: element.animations, + duration: nextDuration, + }), + }; + }, + }); + + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } +} diff --git a/apps/web/src/lib/commands/timeline/element/split-elements.ts b/apps/web/src/lib/commands/timeline/element/split-elements.ts index e9742906..250a1ce6 100644 --- a/apps/web/src/lib/commands/timeline/element/split-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/split-elements.ts @@ -175,6 +175,7 @@ export class SplitElementsCommand extends Command { select: this.rightSideElements, }; } + return undefined; } undo(): void { diff --git a/apps/web/src/lib/commands/timeline/element/toggle-elements-muted.ts b/apps/web/src/lib/commands/timeline/element/toggle-elements-muted.ts index 30310806..75b77562 100644 --- a/apps/web/src/lib/commands/timeline/element/toggle-elements-muted.ts +++ b/apps/web/src/lib/commands/timeline/element/toggle-elements-muted.ts @@ -1,4 +1,4 @@ -import { Command } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline";
import { canElementHaveAudio } from "@/lib/timeline/element-utils";
import { EditorCore } from "@/core";
@@ -10,7 +10,7 @@ export class ToggleElementsMutedCommand extends Command { super();
}
- execute(): void {
+ execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks();
diff --git a/apps/web/src/lib/commands/timeline/element/toggle-elements-visibility.ts b/apps/web/src/lib/commands/timeline/element/toggle-elements-visibility.ts index 690f61aa..271b0a00 100644 --- a/apps/web/src/lib/commands/timeline/element/toggle-elements-visibility.ts +++ b/apps/web/src/lib/commands/timeline/element/toggle-elements-visibility.ts @@ -1,4 +1,4 @@ -import { Command } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline";
import { canElementBeHidden } from "@/lib/timeline/element-utils";
import { EditorCore } from "@/core";
@@ -10,7 +10,7 @@ export class ToggleElementsVisibilityCommand extends Command { super();
}
- execute(): void {
+ execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks();
@@ -36,6 +36,7 @@ export class ToggleElementsVisibilityCommand extends Command { });
editor.timeline.updateTracks(updatedTracks);
+ return undefined;
}
undo(): void {
diff --git a/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts b/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts index d0907c5b..c14a5a42 100644 --- a/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts +++ b/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts @@ -1,5 +1,5 @@ import { EditorCore } from "@/core"; -import { Command } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/lib/commands/base-command"; import { buildSeparatedAudioElement, canExtractSourceAudio, @@ -25,7 +25,7 @@ export class ToggleSourceAudioSeparationCommand extends Command { super(); } - execute(): void { + execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); this.savedState = editor.timeline.getTracks(); @@ -43,6 +43,9 @@ export class ToggleSourceAudioSeparationCommand extends Command { if (!sourceElement) { return; } + if (sourceElement.type !== "video") { + return; + } if (canRecoverSourceAudio({ element: sourceElement })) { editor.timeline.updateTracks( @@ -59,9 +62,7 @@ export class ToggleSourceAudioSeparationCommand extends Command { const mediaAsset = editor .media .getAssets() - .find((asset) => - sourceElement.type === "video" ? asset.id === sourceElement.mediaId : false, - ); + .find((asset) => asset.id === sourceElement.mediaId); if (!canExtractSourceAudio({ element: sourceElement, mediaAsset })) { return; } diff --git a/apps/web/src/lib/commands/timeline/element/update-element-duration.ts b/apps/web/src/lib/commands/timeline/element/update-element-duration.ts index 4280dd4d..7ad62000 100644 --- a/apps/web/src/lib/commands/timeline/element/update-element-duration.ts +++ b/apps/web/src/lib/commands/timeline/element/update-element-duration.ts @@ -1,57 +1,58 @@ -import { Command } from "@/lib/commands/base-command";
-import type { TimelineTrack } from "@/lib/timeline";
-import { EditorCore } from "@/core";
-import { clampAnimationsToDuration } from "@/lib/animation";
-
-export class UpdateElementDurationCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
- private readonly trackId: string;
- private readonly elementId: string;
- private readonly duration: number;
-
- constructor({
- trackId,
- elementId,
- duration,
- }: {
- trackId: string;
- elementId: string;
- duration: number;
- }) {
- super();
- this.trackId = trackId;
- this.elementId = elementId;
- this.duration = duration;
- }
-
- execute(): void {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const updatedTracks = this.savedState.map((track) => {
- if (track.id !== this.trackId) return track;
- const newElements = track.elements.map((element) =>
- element.id === this.elementId
- ? {
- ...element,
- duration: this.duration,
- animations: clampAnimationsToDuration({
- animations: element.animations,
- duration: this.duration,
- }),
- }
- : element,
- );
- return { ...track, elements: newElements } as typeof track;
- });
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (this.savedState) {
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
- }
-}
+import { Command, type CommandResult } from "@/lib/commands/base-command"; +import type { TimelineTrack } from "@/lib/timeline"; +import { EditorCore } from "@/core"; +import { clampAnimationsToDuration } from "@/lib/animation"; + +export class UpdateElementDurationCommand extends Command { + private savedState: TimelineTrack[] | null = null; + private readonly trackId: string; + private readonly elementId: string; + private readonly duration: number; + + constructor({ + trackId, + elementId, + duration, + }: { + trackId: string; + elementId: string; + duration: number; + }) { + super(); + this.trackId = trackId; + this.elementId = elementId; + this.duration = duration; + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.timeline.getTracks(); + + const updatedTracks = this.savedState.map((track) => { + if (track.id !== this.trackId) return track; + const newElements = track.elements.map((element) => + element.id === this.elementId + ? { + ...element, + duration: this.duration, + animations: clampAnimationsToDuration({ + animations: element.animations, + duration: this.duration, + }), + } + : element, + ); + return { ...track, elements: newElements } as typeof track; + }); + + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } +} diff --git a/apps/web/src/lib/commands/timeline/element/update-element-start-time.ts b/apps/web/src/lib/commands/timeline/element/update-element-start-time.ts index ff81cff2..6232d539 100644 --- a/apps/web/src/lib/commands/timeline/element/update-element-start-time.ts +++ b/apps/web/src/lib/commands/timeline/element/update-element-start-time.ts @@ -1,69 +1,70 @@ -import { Command } from "@/lib/commands/base-command";
-import type { TimelineTrack } from "@/lib/timeline";
-import { EditorCore } from "@/core";
-import { enforceMainTrackStart } from "@/lib/timeline/placement";
-
-export class UpdateElementStartTimeCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
- private readonly elements: { trackId: string; elementId: string }[];
- private readonly startTime: number;
-
- constructor({
- elements,
- startTime,
- }: {
- elements: { trackId: string; elementId: string }[];
- startTime: number;
- }) {
- super();
- this.elements = elements;
- this.startTime = startTime;
- }
-
- execute(): void {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const currentTracks = this.savedState;
- const updatedTracks = currentTracks.map((track) => {
- const hasElementsToUpdate = this.elements.some(
- (elementEntry) => elementEntry.trackId === track.id,
- );
-
- if (!hasElementsToUpdate) {
- return track;
- }
-
- const newElements = track.elements.map((element) => {
- const shouldUpdate = this.elements.some(
- (elementEntry) =>
- elementEntry.elementId === element.id &&
- elementEntry.trackId === track.id,
- );
- if (!shouldUpdate) {
- return element;
- }
-
- const baseStartTime = Math.max(0, this.startTime);
- const adjustedStartTime = enforceMainTrackStart({
- tracks: currentTracks,
- targetTrackId: track.id,
- requestedStartTime: baseStartTime,
- excludeElementId: element.id,
- });
-
- return { ...element, startTime: adjustedStartTime };
- });
- return { ...track, elements: newElements } as typeof track;
- });
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (this.savedState) {
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
- }
-}
+import { Command, type CommandResult } from "@/lib/commands/base-command"; +import type { TimelineTrack } from "@/lib/timeline"; +import { EditorCore } from "@/core"; +import { enforceMainTrackStart } from "@/lib/timeline/placement"; + +export class UpdateElementStartTimeCommand extends Command { + private savedState: TimelineTrack[] | null = null; + private readonly elements: { trackId: string; elementId: string }[]; + private readonly startTime: number; + + constructor({ + elements, + startTime, + }: { + elements: { trackId: string; elementId: string }[]; + startTime: number; + }) { + super(); + this.elements = elements; + this.startTime = startTime; + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.timeline.getTracks(); + + const currentTracks = this.savedState; + const updatedTracks = currentTracks.map((track) => { + const hasElementsToUpdate = this.elements.some( + (elementEntry) => elementEntry.trackId === track.id, + ); + + if (!hasElementsToUpdate) { + return track; + } + + const newElements = track.elements.map((element) => { + const shouldUpdate = this.elements.some( + (elementEntry) => + elementEntry.elementId === element.id && + elementEntry.trackId === track.id, + ); + if (!shouldUpdate) { + return element; + } + + const baseStartTime = Math.max(0, this.startTime); + const adjustedStartTime = enforceMainTrackStart({ + tracks: currentTracks, + targetTrackId: track.id, + requestedStartTime: baseStartTime, + excludeElementId: element.id, + }); + + return { ...element, startTime: adjustedStartTime }; + }); + return { ...track, elements: newElements } as typeof track; + }); + + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } +} diff --git a/apps/web/src/lib/commands/timeline/element/update-element-trim.ts b/apps/web/src/lib/commands/timeline/element/update-element-trim.ts index 13ee66dd..e3cb596b 100644 --- a/apps/web/src/lib/commands/timeline/element/update-element-trim.ts +++ b/apps/web/src/lib/commands/timeline/element/update-element-trim.ts @@ -1,4 +1,4 @@ -import { Command } from "@/lib/commands/base-command"; +import { Command, type CommandResult } from "@/lib/commands/base-command"; import type { TimelineTrack } from "@/lib/timeline"; import { EditorCore } from "@/core"; import { clampAnimationsToDuration } from "@/lib/animation"; @@ -38,7 +38,7 @@ export class UpdateElementTrimCommand extends Command { this.rippleEnabled = rippleEnabled; } - execute(): void { + execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); this.savedState = editor.timeline.getTracks(); @@ -104,6 +104,7 @@ export class UpdateElementTrimCommand extends Command { }); editor.timeline.updateTracks(updatedTracks); + return undefined; } undo(): void { diff --git a/apps/web/src/lib/commands/timeline/element/update-element.ts b/apps/web/src/lib/commands/timeline/element/update-element.ts index 494fa1f6..45b2603f 100644 --- a/apps/web/src/lib/commands/timeline/element/update-element.ts +++ b/apps/web/src/lib/commands/timeline/element/update-element.ts @@ -1,47 +1,48 @@ -import { Command } from "@/lib/commands/base-command";
-import type { TimelineElement, TimelineTrack } from "@/lib/timeline";
-import { EditorCore } from "@/core";
-import { updateElementInTracks } from "@/lib/timeline";
-
-export class UpdateElementCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
- private readonly trackId: string;
- private readonly elementId: string;
- private readonly updates: Partial<TimelineElement>;
-
- constructor({
- trackId,
- elementId,
- updates,
- }: {
- trackId: string;
- elementId: string;
- updates: Partial<TimelineElement>;
- }) {
- super();
- this.trackId = trackId;
- this.elementId = elementId;
- this.updates = updates;
- }
-
- execute(): void {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const updatedTracks = updateElementInTracks({
- tracks: this.savedState,
- trackId: this.trackId,
- elementId: this.elementId,
- update: (element) => ({ ...element, ...this.updates }) as TimelineElement,
- });
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (this.savedState) {
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
- }
-}
+import { Command, type CommandResult } from "@/lib/commands/base-command"; +import type { TimelineElement, TimelineTrack } from "@/lib/timeline"; +import { EditorCore } from "@/core"; +import { updateElementInTracks } from "@/lib/timeline"; + +export class UpdateElementCommand extends Command { + private savedState: TimelineTrack[] | null = null; + private readonly trackId: string; + private readonly elementId: string; + private readonly updates: Partial<TimelineElement>; + + constructor({ + trackId, + elementId, + updates, + }: { + trackId: string; + elementId: string; + updates: Partial<TimelineElement>; + }) { + super(); + this.trackId = trackId; + this.elementId = elementId; + this.updates = updates; + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.timeline.getTracks(); + + const updatedTracks = updateElementInTracks({ + tracks: this.savedState, + trackId: this.trackId, + elementId: this.elementId, + update: (element) => ({ ...element, ...this.updates }) as TimelineElement, + }); + + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } +} diff --git a/apps/web/src/lib/commands/timeline/track/add-track.ts b/apps/web/src/lib/commands/timeline/track/add-track.ts index 104c0462..29bdbb0a 100644 --- a/apps/web/src/lib/commands/timeline/track/add-track.ts +++ b/apps/web/src/lib/commands/timeline/track/add-track.ts @@ -1,53 +1,54 @@ -import { Command } from "@/lib/commands/base-command";
-import type { TrackType, TimelineTrack } from "@/lib/timeline";
-import { generateUUID } from "@/utils/id";
-import { EditorCore } from "@/core";
-import {
- buildEmptyTrack,
- getDefaultInsertIndexForTrack,
-} from "@/lib/timeline/placement";
-
-export class AddTrackCommand extends Command {
- private trackId: string;
- private savedState: TimelineTrack[] | null = null;
-
- constructor(
- private type: TrackType,
- private index?: number,
- ) {
- super();
- this.trackId = generateUUID();
- }
-
- execute(): void {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const newTrack: TimelineTrack = buildEmptyTrack({
- id: this.trackId,
- type: this.type,
- });
-
- const updatedTracks = [...(this.savedState || [])];
- const insertIndex =
- this.index ??
- getDefaultInsertIndexForTrack({
- tracks: updatedTracks,
- trackType: this.type,
- });
- updatedTracks.splice(insertIndex, 0, newTrack);
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (this.savedState) {
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
- }
-
- getTrackId(): string {
- return this.trackId;
- }
-}
+import { Command, type CommandResult } from "@/lib/commands/base-command"; +import type { TrackType, TimelineTrack } from "@/lib/timeline"; +import { generateUUID } from "@/utils/id"; +import { EditorCore } from "@/core"; +import { + buildEmptyTrack, + getDefaultInsertIndexForTrack, +} from "@/lib/timeline/placement"; + +export class AddTrackCommand extends Command { + private trackId: string; + private savedState: TimelineTrack[] | null = null; + + constructor( + private type: TrackType, + private index?: number, + ) { + super(); + this.trackId = generateUUID(); + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.timeline.getTracks(); + + const newTrack: TimelineTrack = buildEmptyTrack({ + id: this.trackId, + type: this.type, + }); + + const updatedTracks = [...(this.savedState || [])]; + const insertIndex = + this.index ?? + getDefaultInsertIndexForTrack({ + tracks: updatedTracks, + trackType: this.type, + }); + updatedTracks.splice(insertIndex, 0, newTrack); + + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } + + getTrackId(): string { + return this.trackId; + } +} diff --git a/apps/web/src/lib/commands/timeline/track/remove-track.ts b/apps/web/src/lib/commands/timeline/track/remove-track.ts index f35edbe4..c2ea7ef0 100644 --- a/apps/web/src/lib/commands/timeline/track/remove-track.ts +++ b/apps/web/src/lib/commands/timeline/track/remove-track.ts @@ -1,4 +1,4 @@ -import { Command } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core";
import type { TimelineTrack } from "@/lib/timeline";
import { getMainTrack } from "@/lib/timeline/placement";
@@ -10,7 +10,7 @@ export class RemoveTrackCommand extends Command { super();
}
- execute(): void {
+ execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks();
const targetTrack = this.savedState.find(
diff --git a/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts b/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts index 0094f1de..513fffc7 100644 --- a/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts +++ b/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts @@ -1,4 +1,4 @@ -import { Command } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline";
import { EditorCore } from "@/core";
import { canTrackHaveAudio } from "@/lib/timeline";
@@ -10,7 +10,7 @@ export class ToggleTrackMuteCommand extends Command { super();
}
- execute(): void {
+ execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks();
diff --git a/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts b/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts index 6d74e238..077c21db 100644 --- a/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts +++ b/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts @@ -1,4 +1,4 @@ -import { Command } from "@/lib/commands/base-command";
+import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline";
import { EditorCore } from "@/core";
import { canTrackBeHidden } from "@/lib/timeline";
@@ -10,7 +10,7 @@ export class ToggleTrackVisibilityCommand extends Command { super();
}
- execute(): void {
+ execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks();
diff --git a/apps/web/src/lib/commands/timeline/tracks-snapshot.ts b/apps/web/src/lib/commands/timeline/tracks-snapshot.ts index d397431c..2493f19b 100644 --- a/apps/web/src/lib/commands/timeline/tracks-snapshot.ts +++ b/apps/web/src/lib/commands/timeline/tracks-snapshot.ts @@ -1,20 +1,21 @@ -import { Command } from "@/lib/commands/base-command";
-import type { TimelineTrack } from "@/lib/timeline";
-import { EditorCore } from "@/core";
-
-export class TracksSnapshotCommand extends Command {
- constructor(
- private before: TimelineTrack[],
- private after: TimelineTrack[],
- ) {
- super();
- }
-
- execute(): void {
- EditorCore.getInstance().timeline.updateTracks(this.after);
- }
-
- undo(): void {
- EditorCore.getInstance().timeline.updateTracks(this.before);
- }
-}
+import { Command, type CommandResult } from "@/lib/commands/base-command"; +import type { TimelineTrack } from "@/lib/timeline"; +import { EditorCore } from "@/core"; + +export class TracksSnapshotCommand extends Command { + constructor( + private before: TimelineTrack[], + private after: TimelineTrack[], + ) { + super(); + } + + execute(): CommandResult | undefined { + EditorCore.getInstance().timeline.updateTracks(this.after); + return undefined; + } + + undo(): void { + EditorCore.getInstance().timeline.updateTracks(this.before); + } +} |
