summaryrefslogtreecommitdiff
path: root/apps/web/src/commands/timeline/element/masks/delete-custom-mask-points.ts
blob: fb02554dacf9da66bdf8e8a8da0c2eb1ff27372a (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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import { EditorCore } from "@/core";
import { Command, type CommandResult } from "@/commands/base-command";
import {
	getFreeformPathClosedStateAfterPointRemoval,
	removeFreeformPathPoints,
} from "@/masks/freeform/path";
import type { FreeformPathMask } from "@/masks/types";
import { isMaskableElement, updateElementInSceneTracks } from "@/timeline";
import type { MaskableElement, SceneTracks } from "@/timeline";

function deletePointsFromFreeformPathMask({
	mask,
	pointIds,
}: {
	mask: FreeformPathMask;
	pointIds: string[];
}): FreeformPathMask {
	const points = mask.params.path;
	const nextPoints = removeFreeformPathPoints({ points, pointIds });
	if (nextPoints.length === points.length) {
		return mask;
	}

	return {
		...mask,
		params: {
			...mask.params,
			path: nextPoints,
			closed: getFreeformPathClosedStateAfterPointRemoval({
				wasClosed: mask.params.closed,
				remainingPointCount: nextPoints.length,
			}),
		},
	};
}

function deletePointsFromElementMask({
	element,
	maskId,
	pointIds,
}: {
	element: MaskableElement;
	maskId: string;
	pointIds: string[];
}): { element: MaskableElement; didDeletePoints: boolean } {
	const currentMasks = element.masks ?? [];
	let didDeletePoints = false;
	const nextMasks = currentMasks.map((mask) => {
		if (mask.id !== maskId || mask.type !== "freeform") {
			return mask;
		}

		const nextMask = deletePointsFromFreeformPathMask({
			mask,
			pointIds,
		});
		didDeletePoints ||= nextMask !== mask;
		return nextMask;
	});

	return {
		element: didDeletePoints ? { ...element, masks: nextMasks } : element,
		didDeletePoints,
	};
}

export class DeleteFreeformPathMaskPointsCommand extends Command {
	private savedState: SceneTracks | null = null;
	private readonly trackId: string;
	private readonly elementId: string;
	private readonly maskId: string;
	private readonly pointIds: string[];

	constructor({
		trackId,
		elementId,
		maskId,
		pointIds,
	}: {
		trackId: string;
		elementId: string;
		maskId: string;
		pointIds: string[];
	}) {
		super();
		this.trackId = trackId;
		this.elementId = elementId;
		this.maskId = maskId;
		this.pointIds = pointIds;
	}

	execute(): CommandResult | undefined {
		const editor = EditorCore.getInstance();
		this.savedState = editor.scenes.getActiveScene().tracks;

		let didDeletePoints = false;
		const updatedTracks = updateElementInSceneTracks({
			tracks: this.savedState,
			trackId: this.trackId,
			elementId: this.elementId,
			elementPredicate: isMaskableElement,
			update: (element) => {
				if (!isMaskableElement(element)) return element;
				const result = deletePointsFromElementMask({
					element,
					maskId: this.maskId,
					pointIds: this.pointIds,
				});
				didDeletePoints ||= result.didDeletePoints;
				return result.element;
			},
		});

		if (didDeletePoints) {
			editor.timeline.updateTracks(updatedTracks);
			return {
				selection: {
					selectedMaskPoints: null,
				},
			};
		}

		return undefined;
	}

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