summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/commands/timeline/element/update-element-trim.ts
blob: b2c7741871b2ad32947fcd8e470a814e6726d223 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import { Command } 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/track-utils";

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(): void {
		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);
	}

	undo(): void {
		if (this.savedState) {
			const editor = EditorCore.getInstance();
			editor.timeline.updateTracks(this.savedState);
		}
	}
}