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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
|
import type { EditorCore } from "@/core";
import type { Command, CommandResult } from "@/lib/commands";
import { applyRippleAdjustments, computeRippleAdjustments } from "@/lib/ripple";
import type { ElementRef, SceneTracks } from "@/lib/timeline/types";
interface CommandHistoryEntry {
command: Command;
previousSelection: ElementRef[];
selectionOverride?: ElementRef[];
}
export class CommandManager {
public isRippleEnabled = false;
private history: CommandHistoryEntry[] = [];
private redoStack: CommandHistoryEntry[] = [];
private reactors: Array<() => void> = [];
constructor(private editor: EditorCore) {}
execute({ command }: { command: Command }): Command {
const beforeTracks = this.isRippleEnabled
? this.editor.scenes.getActiveSceneOrNull()?.tracks ?? null
: null;
const previousSelection = this.getSelectionSnapshot();
const result = command.execute();
this.applyRippleIfEnabled({ beforeTracks });
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 beforeTracks = this.isRippleEnabled
? this.editor.scenes.getActiveSceneOrNull()?.tracks ?? null
: null;
const previousSelection = this.getSelectionSnapshot();
const result = entry.command.redo();
this.applyRippleIfEnabled({ beforeTracks });
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();
}
}
private applyRippleIfEnabled({
beforeTracks,
}: {
beforeTracks: SceneTracks | null;
}): void {
if (!this.isRippleEnabled || !beforeTracks) {
return;
}
const afterTracks = this.editor.scenes.getActiveSceneOrNull()?.tracks;
if (!afterTracks) {
return;
}
const adjustments = computeRippleAdjustments({
beforeTracks,
afterTracks,
});
if (adjustments.length === 0) {
return;
}
const tracksWithRipple = applyRippleAdjustments({
tracks: afterTracks,
adjustments,
});
this.editor.timeline.updateTracks(tracksWithRipple);
}
}
|