feat: centralize selection management

This commit is contained in:
Maze Winther
2026-03-31 22:53:16 +02:00
parent 0f5dca4f12
commit f8fb238b22
11 changed files with 164 additions and 94 deletions
+9 -3
View File
@@ -1,11 +1,17 @@
import type { ElementRef } from "@/lib/timeline/types";
export interface CommandResult {
select?: ElementRef[];
}
export abstract class Command {
abstract execute(): void;
abstract execute(): CommandResult | undefined;
undo(): void {
throw new Error("Undo not implemented for this command");
}
redo(): void {
this.execute();
redo(): CommandResult | undefined {
return this.execute();
}
}
+19 -5
View File
@@ -1,14 +1,21 @@
import { Command } from "./base-command";
import { Command, type CommandResult } from "./base-command";
export class BatchCommand extends Command {
constructor(private commands: Command[]) {
super();
}
execute(): void {
execute(): CommandResult | undefined {
let latestSelectionResult: CommandResult | undefined;
for (const command of this.commands) {
command.execute();
const result = command.execute();
if (result?.select !== undefined) {
latestSelectionResult = result;
}
}
return latestSelectionResult;
}
undo(): void {
@@ -17,9 +24,16 @@ export class BatchCommand extends Command {
}
}
redo(): void {
redo(): CommandResult | undefined {
let latestSelectionResult: CommandResult | undefined;
for (const command of this.commands) {
command.execute();
const result = command.redo();
if (result?.select !== undefined) {
latestSelectionResult = result;
}
}
return latestSelectionResult;
}
}
+1
View File
@@ -1,4 +1,5 @@
export { Command } from "./base-command";
export type { CommandResult } from "./base-command";
export { BatchCommand } from "./batch-command";
export * from "./timeline";
@@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core";
import type {
TimelineTrack,
@@ -17,21 +17,26 @@ import { cloneAnimations } from "@/lib/animation";
export class PasteCommand extends Command {
private savedState: TimelineTrack[] | null = null;
private pastedElements: { trackId: string; elementId: string }[] = [];
private previousSelection: { trackId: string; elementId: string }[] = [];
private readonly time: number;
private readonly clipboardItems: ClipboardItem[];
constructor(
private time: number,
private clipboardItems: ClipboardItem[],
) {
constructor({
time,
clipboardItems,
}: {
time: number;
clipboardItems: ClipboardItem[];
}) {
super();
this.time = time;
this.clipboardItems = clipboardItems;
}
execute(): void {
execute(): CommandResult | undefined {
if (this.clipboardItems.length === 0) return;
const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks();
this.previousSelection = editor.selection.getSelectedElements();
this.pastedElements = [];
const minStart = Math.min(
@@ -116,7 +121,7 @@ export class PasteCommand extends Command {
editor.timeline.updateTracks(updatedTracks);
if (this.pastedElements.length > 0) {
editor.selection.setSelectedElements({ elements: this.pastedElements });
return { select: this.pastedElements };
}
}
@@ -124,9 +129,6 @@ export class PasteCommand extends Command {
if (this.savedState) {
const editor = EditorCore.getInstance();
editor.timeline.updateTracks(this.savedState);
editor.selection.setSelectedElements({
elements: this.previousSelection,
});
}
}
@@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline";
import { EditorCore } from "@/core";
import { rippleShiftElements } from "@/lib/timeline";
@@ -21,7 +21,7 @@ export class DeleteElementsCommand extends Command {
this.rippleEnabled = rippleEnabled;
}
execute(): void {
execute(): CommandResult {
const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks();
@@ -69,6 +69,10 @@ export class DeleteElementsCommand extends Command {
.filter((track) => track.elements.length > 0 || isMainTrack(track));
editor.timeline.updateTracks(updatedTracks);
return {
select: [],
};
}
undo(): void {
@@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineElement, TimelineTrack } from "@/lib/timeline";
import { generateUUID } from "@/utils/id";
import { EditorCore } from "@/core";
@@ -12,7 +12,6 @@ interface DuplicateElementsParams {
export class DuplicateElementsCommand extends Command {
private duplicatedElements: { trackId: string; elementId: string }[] = [];
private savedState: TimelineTrack[] | null = null;
private previousSelection: { trackId: string; elementId: string }[] = [];
private elements: DuplicateElementsParams["elements"];
constructor({ elements }: DuplicateElementsParams) {
@@ -20,10 +19,9 @@ export class DuplicateElementsCommand extends Command {
this.elements = elements;
}
execute(): void {
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks();
this.previousSelection = editor.selection.getSelectedElements();
this.duplicatedElements = [];
let updatedTracks = [...this.savedState];
@@ -89,9 +87,9 @@ export class DuplicateElementsCommand extends Command {
editor.timeline.updateTracks(updatedTracks);
if (this.duplicatedElements.length > 0) {
editor.selection.setSelectedElements({
elements: this.duplicatedElements,
});
return {
select: this.duplicatedElements,
};
}
}
@@ -99,9 +97,6 @@ export class DuplicateElementsCommand extends Command {
if (this.savedState) {
const editor = EditorCore.getInstance();
editor.timeline.updateTracks(this.savedState);
editor.selection.setSelectedElements({
elements: this.previousSelection,
});
}
}
@@ -1,4 +1,4 @@
import { Command } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline";
import { generateUUID } from "@/utils/id";
import { EditorCore } from "@/core";
@@ -9,7 +9,6 @@ import { getSourceSpanAtClipTime } from "@/lib/retime";
export class SplitElementsCommand extends Command {
private savedState: TimelineTrack[] | null = null;
private rightSideElements: { trackId: string; elementId: string }[] = [];
private previousSelection: { trackId: string; elementId: string }[] = [];
private readonly elements: { trackId: string; elementId: string }[];
private readonly splitTime: number;
private readonly retainSide: "both" | "left" | "right";
@@ -37,10 +36,9 @@ export class SplitElementsCommand extends Command {
return this.rightSideElements;
}
execute(): void {
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks();
this.previousSelection = editor.selection.getSelectedElements();
this.rightSideElements = [];
const updatedTracks = this.savedState.map((track) => {
@@ -173,9 +171,9 @@ export class SplitElementsCommand extends Command {
editor.timeline.updateTracks(updatedTracks);
if (this.rightSideElements.length > 0) {
editor.selection.setSelectedElements({
elements: this.rightSideElements,
});
return {
select: this.rightSideElements,
};
}
}
@@ -183,9 +181,6 @@ export class SplitElementsCommand extends Command {
if (this.savedState) {
const editor = EditorCore.getInstance();
editor.timeline.updateTracks(this.savedState);
editor.selection.setSelectedElements({
elements: this.previousSelection,
});
}
}
}