summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/commands/timeline
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-06 00:02:23 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-06 00:02:23 +0200
commit44d8c4c8764d49a8e8d0ae713432c07f07cb7597 (patch)
tree4657187279adb475ea710231feb3d0bd4add36ed /apps/web/src/lib/commands/timeline
parent20235d0724a70e6787b6cbdbfa054b1c5d8f62e9 (diff)
downloadopencut-44d8c4c8764d49a8e8d0ae713432c07f07cb7597.tar.gz
refactor: unify element updates into pipeline and centralize ripple
Diffstat (limited to 'apps/web/src/lib/commands/timeline')
-rw-r--r--apps/web/src/lib/commands/timeline/element/delete-elements.ts57
-rw-r--r--apps/web/src/lib/commands/timeline/element/index.ts8
-rw-r--r--apps/web/src/lib/commands/timeline/element/move-elements.ts14
-rw-r--r--apps/web/src/lib/commands/timeline/element/retime/index.ts1
-rw-r--r--apps/web/src/lib/commands/timeline/element/retime/update-element-retime.ts115
-rw-r--r--apps/web/src/lib/commands/timeline/element/split-elements.ts19
-rw-r--r--apps/web/src/lib/commands/timeline/element/toggle-elements-muted.ts57
-rw-r--r--apps/web/src/lib/commands/timeline/element/toggle-elements-visibility.ts48
-rw-r--r--apps/web/src/lib/commands/timeline/element/update-element-duration.ts58
-rw-r--r--apps/web/src/lib/commands/timeline/element/update-element-start-time.ts70
-rw-r--r--apps/web/src/lib/commands/timeline/element/update-element-trim.ts116
-rw-r--r--apps/web/src/lib/commands/timeline/element/update-element.ts48
-rw-r--r--apps/web/src/lib/commands/timeline/element/update-elements.ts71
13 files changed, 90 insertions, 592 deletions
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 1e1ad661..c8cc4494 100644
--- a/apps/web/src/lib/commands/timeline/element/delete-elements.ts
+++ b/apps/web/src/lib/commands/timeline/element/delete-elements.ts
@@ -1,23 +1,18 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline";
import { EditorCore } from "@/core";
-import { rippleShiftElements } from "@/lib/timeline";
export class DeleteElementsCommand extends Command {
private savedState: TimelineTrack[] | null = null;
private readonly elements: { trackId: string; elementId: string }[];
- private readonly rippleEnabled: boolean;
constructor({
elements,
- rippleEnabled = false,
}: {
elements: { trackId: string; elementId: string }[];
- rippleEnabled?: boolean;
}) {
super();
this.elements = elements;
- this.rippleEnabled = rippleEnabled;
}
execute(): CommandResult | undefined {
@@ -25,45 +20,25 @@ export class DeleteElementsCommand extends Command {
this.savedState = editor.timeline.getTracks();
const updatedTracks = this.savedState.map((track) => {
- const elementsToDeleteOnTrack = this.elements.filter(
- (target) => target.trackId === track.id,
- );
- const hasElementsToDelete = elementsToDeleteOnTrack.length > 0;
+ const elementsToDeleteOnTrack = this.elements.filter(
+ (target) => target.trackId === track.id,
+ );
- if (!hasElementsToDelete) {
- return track;
- }
+ if (elementsToDeleteOnTrack.length === 0) {
+ return track;
+ }
- const deletedElementInfos = elementsToDeleteOnTrack
- .map((target) =>
- track.elements.find((element) => element.id === target.elementId),
- )
- .filter((element): element is NonNullable<typeof element> => element !== undefined)
- .map((element) => ({ startTime: element.startTime, duration: element.duration }));
+ const elements = track.elements.filter(
+ (element) =>
+ !this.elements.some(
+ (target) =>
+ target.trackId === track.id &&
+ target.elementId === element.id,
+ ),
+ );
- let elements = track.elements.filter(
- (element) =>
- !this.elements.some(
- (target) =>
- target.trackId === track.id && target.elementId === element.id,
- ),
- );
-
- if (this.rippleEnabled && deletedElementInfos.length > 0) {
- const sortedByStartDesc = [...deletedElementInfos].sort(
- (a, b) => b.startTime - a.startTime,
- );
- for (const { startTime, duration } of sortedByStartDesc) {
- elements = rippleShiftElements({
- elements,
- afterTime: startTime,
- shiftAmount: duration,
- });
- }
- }
-
- return { ...track, elements } as typeof track;
- });
+ return { ...track, elements } as typeof track;
+ });
editor.timeline.updateTracks(updatedTracks);
diff --git a/apps/web/src/lib/commands/timeline/element/index.ts b/apps/web/src/lib/commands/timeline/element/index.ts
index 60e12ddd..eabdc385 100644
--- a/apps/web/src/lib/commands/timeline/element/index.ts
+++ b/apps/web/src/lib/commands/timeline/element/index.ts
@@ -1,17 +1,11 @@
export { InsertElementCommand } from "./insert-element";
export { DeleteElementsCommand } from "./delete-elements";
export { DuplicateElementsCommand } from "./duplicate-elements";
-export { UpdateElementTrimCommand } from "./update-element-trim";
-export { UpdateElementDurationCommand } from "./update-element-duration";
-export { UpdateElementStartTimeCommand } from "./update-element-start-time";
export { SplitElementsCommand } from "./split-elements";
-export { UpdateElementCommand } from "./update-element";
-export { ToggleElementsVisibilityCommand } from "./toggle-elements-visibility";
-export { ToggleElementsMutedCommand } from "./toggle-elements-muted";
+export { UpdateElementsCommand } from "./update-elements";
export { ToggleSourceAudioSeparationCommand } from "./toggle-source-audio-separation";
export { MoveElementCommand } from "./move-elements";
export * from "./keyframes";
export * from "./effects";
export * from "./masks";
-export * from "./retime";
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 e944ca81..cf3afa6d 100644
--- a/apps/web/src/lib/commands/timeline/element/move-elements.ts
+++ b/apps/web/src/lib/commands/timeline/element/move-elements.ts
@@ -10,7 +10,6 @@ import {
validateElementTrackCompatibility,
enforceMainTrackStart,
} from "@/lib/timeline/placement";
-import { rippleShiftElements } from "@/lib/timeline/ripple-utils";
export class MoveElementCommand extends Command {
private savedState: TimelineTrack[] | null = null;
@@ -19,7 +18,6 @@ export class MoveElementCommand extends Command {
private readonly elementId: string;
private readonly newStartTime: number;
private readonly createTrack: { type: TrackType; index: number } | undefined;
- private readonly rippleEnabled: boolean;
constructor({
sourceTrackId,
@@ -27,14 +25,12 @@ export class MoveElementCommand extends Command {
elementId,
newStartTime,
createTrack,
- rippleEnabled = false,
}: {
sourceTrackId: string;
targetTrackId: string;
elementId: string;
newStartTime: number;
createTrack?: { type: TrackType; index: number };
- rippleEnabled?: boolean;
}) {
super();
this.sourceTrackId = sourceTrackId;
@@ -42,7 +38,6 @@ export class MoveElementCommand extends Command {
this.elementId = elementId;
this.newStartTime = newStartTime;
this.createTrack = createTrack;
- this.rippleEnabled = rippleEnabled;
}
execute(): CommandResult | undefined {
@@ -113,14 +108,7 @@ export class MoveElementCommand extends Command {
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;
+ return { ...track, elements: remainingElements } as typeof track;
}
if (track.id === this.targetTrackId) {
diff --git a/apps/web/src/lib/commands/timeline/element/retime/index.ts b/apps/web/src/lib/commands/timeline/element/retime/index.ts
deleted file mode 100644
index e9c24c44..00000000
--- a/apps/web/src/lib/commands/timeline/element/retime/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { UpdateElementRetimeCommand } from "./update-element-retime";
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
deleted file mode 100644
index 474396a1..00000000
--- a/apps/web/src/lib/commands/timeline/element/retime/update-element-retime.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-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 250a1ce6..79722b4c 100644
--- a/apps/web/src/lib/commands/timeline/element/split-elements.ts
+++ b/apps/web/src/lib/commands/timeline/element/split-elements.ts
@@ -2,7 +2,7 @@ import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline";
import { generateUUID } from "@/utils/id";
import { EditorCore } from "@/core";
-import { isRetimableElement, rippleShiftElements } from "@/lib/timeline";
+import { isRetimableElement } from "@/lib/timeline";
import { splitAnimationsAtTime } from "@/lib/animation";
import { getSourceSpanAtClipTime } from "@/lib/retime";
@@ -12,24 +12,20 @@ export class SplitElementsCommand extends Command {
private readonly elements: { trackId: string; elementId: string }[];
private readonly splitTime: number;
private readonly retainSide: "both" | "left" | "right";
- private readonly rippleEnabled: boolean;
constructor({
elements,
splitTime,
retainSide = "both",
- rippleEnabled = false,
}: {
elements: { trackId: string; elementId: string }[];
splitTime: number;
retainSide?: "both" | "left" | "right";
- rippleEnabled?: boolean;
}) {
super();
this.elements = elements;
this.splitTime = splitTime;
this.retainSide = retainSide;
- this.rippleEnabled = rippleEnabled;
}
getRightSideElements(): { trackId: string; elementId: string }[] {
@@ -50,8 +46,6 @@ export class SplitElementsCommand extends Command {
return track;
}
- let leftVisibleDurationForRipple: number | null = null;
-
let elements = track.elements.flatMap((element) => {
const shouldSplit = elementsToSplit.some(
(target) => target.elementId === element.id,
@@ -106,9 +100,6 @@ export class SplitElementsCommand extends Command {
}
if (this.retainSide === "right") {
- if (this.rippleEnabled && elementsToSplit.length === 1) {
- leftVisibleDurationForRipple = leftVisibleDuration;
- }
const newId = generateUUID();
this.rightSideElements.push({
trackId: track.id,
@@ -157,14 +148,6 @@ export class SplitElementsCommand extends Command {
];
});
- if (this.rippleEnabled && leftVisibleDurationForRipple !== null) {
- elements = rippleShiftElements({
- elements,
- afterTime: this.splitTime,
- shiftAmount: leftVisibleDurationForRipple,
- });
- }
-
return { ...track, elements } as typeof track;
});
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
deleted file mode 100644
index 75b77562..00000000
--- a/apps/web/src/lib/commands/timeline/element/toggle-elements-muted.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-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";
-
-export class ToggleElementsMutedCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
-
- constructor(private elements: { trackId: string; elementId: string }[]) {
- super();
- }
-
- execute(): CommandResult | undefined {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const mutableElements = this.elements.filter(({ trackId, elementId }) => {
- const track = this.savedState?.find((t) => t.id === trackId);
- const element = track?.elements.find((e) => e.id === elementId);
- return element && canElementHaveAudio(element);
- });
-
- if (mutableElements.length === 0) {
- return;
- }
-
- const shouldMute = mutableElements.some(({ trackId, elementId }) => {
- const track = this.savedState?.find((t) => t.id === trackId);
- const element = track?.elements.find((e) => e.id === elementId);
- return element && canElementHaveAudio(element) && !element.muted;
- });
-
- const updatedTracks = this.savedState.map((track) => {
- const newElements = track.elements.map((element) => {
- const shouldUpdate = mutableElements.some(
- ({ trackId, elementId }) =>
- track.id === trackId && element.id === elementId,
- );
- return shouldUpdate &&
- canElementHaveAudio(element) &&
- element.muted !== shouldMute
- ? { ...element, muted: shouldMute }
- : 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);
- }
- }
-}
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
deleted file mode 100644
index 271b0a00..00000000
--- a/apps/web/src/lib/commands/timeline/element/toggle-elements-visibility.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-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";
-
-export class ToggleElementsVisibilityCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
-
- constructor(private elements: { trackId: string; elementId: string }[]) {
- super();
- }
-
- execute(): CommandResult | undefined {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const shouldHide = this.elements.some(({ trackId, elementId }) => {
- const track = this.savedState?.find((t) => t.id === trackId);
- const element = track?.elements.find((e) => e.id === elementId);
- return element && canElementBeHidden(element) && !element.hidden;
- });
-
- const updatedTracks = this.savedState.map((track) => {
- const newElements = track.elements.map((element) => {
- const shouldUpdate = this.elements.some(
- ({ trackId, elementId }) =>
- track.id === trackId && element.id === elementId,
- );
- return shouldUpdate &&
- canElementBeHidden(element) &&
- element.hidden !== shouldHide
- ? { ...element, hidden: shouldHide }
- : 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-duration.ts b/apps/web/src/lib/commands/timeline/element/update-element-duration.ts
deleted file mode 100644
index 7ad62000..00000000
--- a/apps/web/src/lib/commands/timeline/element/update-element-duration.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-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
deleted file mode 100644
index 6232d539..00000000
--- a/apps/web/src/lib/commands/timeline/element/update-element-start-time.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-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
deleted file mode 100644
index e3cb596b..00000000
--- a/apps/web/src/lib/commands/timeline/element/update-element-trim.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import { Command, type CommandResult } from "@/lib/commands/base-command";
-import type { TimelineTrack } from "@/lib/timeline";
-import { EditorCore } from "@/core";
-import { clampAnimationsToDuration } from "@/lib/animation";
-import { isRetimableElement, rippleShiftElements } from "@/lib/timeline";
-import { enforceMainTrackStart } from "@/lib/timeline/placement";
-
-export class UpdateElementTrimCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
- private readonly elementId: string;
- private readonly trimStart: number;
- private readonly trimEnd: number;
- private readonly startTime: number | undefined;
- private readonly duration: number | undefined;
- private readonly rippleEnabled: boolean;
-
- constructor({
- elementId,
- trimStart,
- trimEnd,
- startTime,
- duration,
- rippleEnabled = false,
- }: {
- elementId: string;
- trimStart: number;
- trimEnd: number;
- startTime?: number;
- duration?: number;
- rippleEnabled?: boolean;
- }) {
- super();
- this.elementId = elementId;
- this.trimStart = trimStart;
- this.trimEnd = trimEnd;
- this.startTime = startTime;
- this.duration = duration;
- this.rippleEnabled = rippleEnabled;
- }
-
- execute(): CommandResult | undefined {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const updatedTracks = this.savedState.map((track) => {
- const targetElement = track.elements.find(
- (element) => element.id === this.elementId,
- );
- if (!targetElement) return track;
-
- const nextDuration = this.duration ?? targetElement.duration;
- const requestedStartTime = this.startTime ?? targetElement.startTime;
- const nextStartTime = enforceMainTrackStart({
- tracks: this.savedState ?? [],
- targetTrackId: track.id,
- requestedStartTime,
- excludeElementId: this.elementId,
- });
-
- const oldEndTime = targetElement.startTime + targetElement.duration;
- const newEndTime = nextStartTime + nextDuration;
- const shiftAmount = oldEndTime - newEndTime;
-
- const updatedElement = {
- ...targetElement,
- trimStart: this.trimStart,
- trimEnd: this.trimEnd,
- startTime: nextStartTime,
- duration: nextDuration,
- ...(isRetimableElement(targetElement)
- ? { retime: targetElement.retime }
- : {}),
- animations: clampAnimationsToDuration({
- animations: targetElement.animations,
- duration: nextDuration,
- }),
- };
-
- if (this.rippleEnabled && Math.abs(shiftAmount) > 0) {
- const shiftedOthers = rippleShiftElements({
- elements: track.elements.filter(
- (element) => element.id !== this.elementId,
- ),
- afterTime: oldEndTime,
- shiftAmount,
- });
- return {
- ...track,
- elements: track.elements.map((element) =>
- element.id === this.elementId
- ? updatedElement
- : (shiftedOthers.find((shifted) => shifted.id === element.id) ??
- element),
- ),
- } as typeof track;
- }
-
- return {
- ...track,
- elements: track.elements.map((element) =>
- element.id === this.elementId ? updatedElement : element,
- ),
- } 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.ts b/apps/web/src/lib/commands/timeline/element/update-element.ts
deleted file mode 100644
index 45b2603f..00000000
--- a/apps/web/src/lib/commands/timeline/element/update-element.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-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/element/update-elements.ts b/apps/web/src/lib/commands/timeline/element/update-elements.ts
new file mode 100644
index 00000000..abeec842
--- /dev/null
+++ b/apps/web/src/lib/commands/timeline/element/update-elements.ts
@@ -0,0 +1,71 @@
+import { EditorCore } from "@/core";
+import { Command, type CommandResult } from "@/lib/commands/base-command";
+import type { TimelineElement, TimelineTrack } from "@/lib/timeline";
+import { updateElementInTracks } from "@/lib/timeline";
+import { applyElementUpdate } from "@/lib/timeline/update-pipeline";
+
+export class UpdateElementsCommand extends Command {
+ private savedState: TimelineTrack[] | null = null;
+ private readonly updates: Array<{
+ trackId: string;
+ elementId: string;
+ patch: Partial<TimelineElement>;
+ }>;
+
+ constructor({
+ updates,
+ }: {
+ updates: Array<{
+ trackId: string;
+ elementId: string;
+ patch: Partial<TimelineElement>;
+ }>;
+ }) {
+ super();
+ this.updates = updates;
+ }
+
+ execute(): CommandResult | undefined {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.timeline.getTracks();
+ let updatedTracks = this.savedState;
+
+ for (const updateEntry of this.updates) {
+ const currentTrack = updatedTracks.find(
+ (track) => track.id === updateEntry.trackId,
+ );
+ const currentElement = currentTrack?.elements.find(
+ (element) => element.id === updateEntry.elementId,
+ );
+ if (!currentTrack || !currentElement) {
+ continue;
+ }
+
+ const nextElement = applyElementUpdate({
+ element: currentElement,
+ patch: updateEntry.patch,
+ context: {
+ tracks: updatedTracks,
+ trackId: updateEntry.trackId,
+ },
+ });
+
+ updatedTracks = updateElementInTracks({
+ tracks: updatedTracks,
+ trackId: updateEntry.trackId,
+ elementId: updateEntry.elementId,
+ update: () => nextElement,
+ });
+ }
+
+ editor.timeline.updateTracks(updatedTracks);
+ return undefined;
+ }
+
+ undo(): void {
+ if (this.savedState) {
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ }
+ }
+}