summaryrefslogtreecommitdiff
path: root/apps/web/src/core/managers/commands.ts
blob: 918fc33eb381c4d684c81d69d2997853b0fadc98 (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
import type { EditorCore } from "@/core";
import type { Command, CommandResult } from "@/lib/commands";
import type { ElementRef } from "@/lib/timeline/types";

interface CommandHistoryEntry {
	command: Command;
	previousSelection: ElementRef[];
	selectionOverride?: ElementRef[];
}

export class CommandManager {
	private history: CommandHistoryEntry[] = [];
	private redoStack: CommandHistoryEntry[] = [];
	private reactors: Array<() => void> = [];

	constructor(private editor: EditorCore) {}

	execute({ command }: { command: Command }): Command {
		const previousSelection = this.getSelectionSnapshot();
		const result = command.execute();
		const selectionOverride = this.applySelectionOverride(result);
		this.runReactors();
		this.history.push({
			command,
			previousSelection,
			selectionOverride,
		});
		this.redoStack = [];
		return command;
	}

	push({ command }: { command: Command }): void {
		this.history.push({
			command,
			previousSelection: this.getSelectionSnapshot(),
		});
		this.redoStack = [];
	}

	registerReactor(reactor: () => void): void {
		this.reactors.push(reactor);
	}

	undo(): void {
		if (this.history.length === 0) return;
		const entry = this.history.pop();
		entry?.command.undo();
		if (entry) {
			// Only restore selection for commands that explicitly changed it.
			// Commands without selection intent leave selection untouched,
			// preserving any UI-driven selection changes (clicks, box select)
			// that happened between commands. Commands that remove elements
			// must declare { select: [] } to clear stale refs.
			if (entry.selectionOverride !== undefined) {
				this.editor.selection.setSelectedElements({
					elements: [...entry.previousSelection],
				});
			}
			this.redoStack.push(entry);
		}
	}

	redo(): void {
		if (this.redoStack.length === 0) return;
		const entry = this.redoStack.pop();
		if (!entry) {
			return;
		}

		const previousSelection = this.getSelectionSnapshot();
		const result = entry.command.redo();
		const selectionOverride = this.applySelectionOverride(result);
		this.runReactors();

		this.history.push({
			command: entry.command,
			previousSelection,
			selectionOverride,
		});
	}

	canUndo(): boolean {
		return this.history.length > 0;
	}

	canRedo(): boolean {
		return this.redoStack.length > 0;
	}

	clear(): void {
		this.history = [];
		this.redoStack = [];
	}

	private getSelectionSnapshot(): ElementRef[] {
		return [...this.editor.selection.getSelectedElements()];
	}

	private applySelectionOverride(
		result: CommandResult | undefined,
	): ElementRef[] | undefined {
		if (result?.select === undefined) {
			return undefined;
		}

		const selectionOverride = [...result.select];
		this.editor.selection.setSelectedElements({ elements: selectionOverride });
		return selectionOverride;
	}

	private runReactors(): void {
		for (const reactor of this.reactors) {
			reactor();
		}
	}
}