Merge branch 'refactor-selection-commands'
This commit is contained in:
commit
cc150c7cfb
|
|
@ -27,7 +27,7 @@ export class EditorCore {
|
||||||
private constructor() {
|
private constructor() {
|
||||||
registerDefaultEffects();
|
registerDefaultEffects();
|
||||||
registerDefaultMasks();
|
registerDefaultMasks();
|
||||||
this.command = new CommandManager();
|
this.command = new CommandManager(this);
|
||||||
this.timeline = new TimelineManager(this);
|
this.timeline = new TimelineManager(this);
|
||||||
this.playback = new PlaybackManager(this);
|
this.playback = new PlaybackManager(this);
|
||||||
this.scenes = new ScenesManager(this);
|
this.scenes = new ScenesManager(this);
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,103 @@
|
||||||
import type { Command } from "@/lib/commands";
|
import type { EditorCore } from "@/core";
|
||||||
|
import type { Command, CommandResult } from "@/lib/commands";
|
||||||
export class CommandManager {
|
import type { ElementRef } from "@/lib/timeline/types";
|
||||||
private history: Command[] = [];
|
|
||||||
private redoStack: Command[] = [];
|
interface CommandHistoryEntry {
|
||||||
|
command: Command;
|
||||||
execute({ command }: { command: Command }): Command {
|
previousSelection: ElementRef[];
|
||||||
command.execute();
|
selectionOverride?: ElementRef[];
|
||||||
this.history.push(command);
|
}
|
||||||
this.redoStack = [];
|
|
||||||
return command;
|
export class CommandManager {
|
||||||
}
|
private history: CommandHistoryEntry[] = [];
|
||||||
|
private redoStack: CommandHistoryEntry[] = [];
|
||||||
push({ command }: { command: Command }): void {
|
|
||||||
this.history.push(command);
|
constructor(private editor: EditorCore) {}
|
||||||
this.redoStack = [];
|
|
||||||
}
|
execute({ command }: { command: Command }): Command {
|
||||||
|
const previousSelection = this.getSelectionSnapshot();
|
||||||
undo(): void {
|
const result = command.execute();
|
||||||
if (this.history.length === 0) return;
|
const selectionOverride = this.applySelectionOverride(result);
|
||||||
const command = this.history.pop();
|
this.history.push({
|
||||||
command?.undo();
|
command,
|
||||||
if (command) {
|
previousSelection,
|
||||||
this.redoStack.push(command);
|
selectionOverride,
|
||||||
}
|
});
|
||||||
}
|
this.redoStack = [];
|
||||||
|
return command;
|
||||||
redo(): void {
|
}
|
||||||
if (this.redoStack.length === 0) return;
|
|
||||||
const command = this.redoStack.pop();
|
push({ command }: { command: Command }): void {
|
||||||
command?.redo();
|
this.history.push({
|
||||||
if (command) {
|
command,
|
||||||
this.history.push(command);
|
previousSelection: this.getSelectionSnapshot(),
|
||||||
}
|
});
|
||||||
}
|
this.redoStack = [];
|
||||||
|
}
|
||||||
canUndo(): boolean {
|
|
||||||
return this.history.length > 0;
|
undo(): void {
|
||||||
}
|
if (this.history.length === 0) return;
|
||||||
|
const entry = this.history.pop();
|
||||||
canRedo(): boolean {
|
entry?.command.undo();
|
||||||
return this.redoStack.length > 0;
|
if (entry) {
|
||||||
}
|
// Only restore selection for commands that explicitly changed it.
|
||||||
|
// Commands without selection intent leave selection untouched,
|
||||||
clear(): void {
|
// preserving any UI-driven selection changes (clicks, box select)
|
||||||
this.history = [];
|
// that happened between commands. Commands that remove elements
|
||||||
this.redoStack = [];
|
// 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.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,7 @@ export class TimelineManager {
|
||||||
time: number;
|
time: number;
|
||||||
clipboardItems: ClipboardItem[];
|
clipboardItems: ClipboardItem[];
|
||||||
}): { trackId: string; elementId: string }[] {
|
}): { trackId: string; elementId: string }[] {
|
||||||
const command = new PasteCommand(time, clipboardItems);
|
const command = new PasteCommand({ time, clipboardItems });
|
||||||
this.editor.command.execute({ command });
|
this.editor.command.execute({ command });
|
||||||
return command.getPastedElements();
|
return command.getPastedElements();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -261,7 +261,6 @@ export function useEditorActions() {
|
||||||
elements: selectedElements,
|
elements: selectedElements,
|
||||||
rippleEnabled: rippleEditingEnabled,
|
rippleEnabled: rippleEditingEnabled,
|
||||||
});
|
});
|
||||||
editor.selection.clearSelection();
|
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
|
import type { ElementRef } from "@/lib/timeline/types";
|
||||||
|
|
||||||
|
export interface CommandResult {
|
||||||
|
select?: ElementRef[];
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class Command {
|
export abstract class Command {
|
||||||
abstract execute(): void;
|
abstract execute(): CommandResult | undefined;
|
||||||
|
|
||||||
undo(): void {
|
undo(): void {
|
||||||
throw new Error("Undo not implemented for this command");
|
throw new Error("Undo not implemented for this command");
|
||||||
}
|
}
|
||||||
|
|
||||||
redo(): void {
|
redo(): CommandResult | undefined {
|
||||||
this.execute();
|
return this.execute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,21 @@
|
||||||
import { Command } from "./base-command";
|
import { Command, type CommandResult } from "./base-command";
|
||||||
|
|
||||||
export class BatchCommand extends Command {
|
export class BatchCommand extends Command {
|
||||||
constructor(private commands: Command[]) {
|
constructor(private commands: Command[]) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
|
let latestSelectionResult: CommandResult | undefined;
|
||||||
|
|
||||||
for (const command of this.commands) {
|
for (const command of this.commands) {
|
||||||
command.execute();
|
const result = command.execute();
|
||||||
|
if (result?.select !== undefined) {
|
||||||
|
latestSelectionResult = result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return latestSelectionResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
undo(): void {
|
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) {
|
for (const command of this.commands) {
|
||||||
command.execute();
|
const result = command.redo();
|
||||||
|
if (result?.select !== undefined) {
|
||||||
|
latestSelectionResult = result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return latestSelectionResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export { Command } from "./base-command";
|
export { Command } from "./base-command";
|
||||||
|
export type { CommandResult } from "./base-command";
|
||||||
export { BatchCommand } from "./batch-command";
|
export { BatchCommand } from "./batch-command";
|
||||||
|
|
||||||
export * from "./timeline";
|
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 { EditorCore } from "@/core";
|
||||||
import type {
|
import type {
|
||||||
TimelineTrack,
|
TimelineTrack,
|
||||||
|
|
@ -17,21 +17,26 @@ import { cloneAnimations } from "@/lib/animation";
|
||||||
export class PasteCommand extends Command {
|
export class PasteCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private pastedElements: { trackId: string; elementId: string }[] = [];
|
private pastedElements: { trackId: string; elementId: string }[] = [];
|
||||||
private previousSelection: { trackId: string; elementId: string }[] = [];
|
private readonly time: number;
|
||||||
|
private readonly clipboardItems: ClipboardItem[];
|
||||||
|
|
||||||
constructor(
|
constructor({
|
||||||
private time: number,
|
time,
|
||||||
private clipboardItems: ClipboardItem[],
|
clipboardItems,
|
||||||
) {
|
}: {
|
||||||
|
time: number;
|
||||||
|
clipboardItems: ClipboardItem[];
|
||||||
|
}) {
|
||||||
super();
|
super();
|
||||||
|
this.time = time;
|
||||||
|
this.clipboardItems = clipboardItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
if (this.clipboardItems.length === 0) return;
|
if (this.clipboardItems.length === 0) return;
|
||||||
|
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
this.previousSelection = editor.selection.getSelectedElements();
|
|
||||||
this.pastedElements = [];
|
this.pastedElements = [];
|
||||||
|
|
||||||
const minStart = Math.min(
|
const minStart = Math.min(
|
||||||
|
|
@ -116,7 +121,7 @@ export class PasteCommand extends Command {
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
|
|
||||||
if (this.pastedElements.length > 0) {
|
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) {
|
if (this.savedState) {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
editor.timeline.updateTracks(this.savedState);
|
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 type { TimelineTrack } from "@/lib/timeline";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { rippleShiftElements } from "@/lib/timeline";
|
import { rippleShiftElements } from "@/lib/timeline";
|
||||||
|
|
@ -21,7 +21,7 @@ export class DeleteElementsCommand extends Command {
|
||||||
this.rippleEnabled = rippleEnabled;
|
this.rippleEnabled = rippleEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
|
|
@ -69,6 +69,10 @@ export class DeleteElementsCommand extends Command {
|
||||||
.filter((track) => track.elements.length > 0 || isMainTrack(track));
|
.filter((track) => track.elements.length > 0 || isMainTrack(track));
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
|
|
||||||
|
return {
|
||||||
|
select: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
undo(): void {
|
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 type { TimelineElement, TimelineTrack } from "@/lib/timeline";
|
||||||
import { generateUUID } from "@/utils/id";
|
import { generateUUID } from "@/utils/id";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
|
|
@ -12,7 +12,6 @@ interface DuplicateElementsParams {
|
||||||
export class DuplicateElementsCommand extends Command {
|
export class DuplicateElementsCommand extends Command {
|
||||||
private duplicatedElements: { trackId: string; elementId: string }[] = [];
|
private duplicatedElements: { trackId: string; elementId: string }[] = [];
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private previousSelection: { trackId: string; elementId: string }[] = [];
|
|
||||||
private elements: DuplicateElementsParams["elements"];
|
private elements: DuplicateElementsParams["elements"];
|
||||||
|
|
||||||
constructor({ elements }: DuplicateElementsParams) {
|
constructor({ elements }: DuplicateElementsParams) {
|
||||||
|
|
@ -20,10 +19,9 @@ export class DuplicateElementsCommand extends Command {
|
||||||
this.elements = elements;
|
this.elements = elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
this.previousSelection = editor.selection.getSelectedElements();
|
|
||||||
this.duplicatedElements = [];
|
this.duplicatedElements = [];
|
||||||
|
|
||||||
let updatedTracks = [...this.savedState];
|
let updatedTracks = [...this.savedState];
|
||||||
|
|
@ -89,9 +87,9 @@ export class DuplicateElementsCommand extends Command {
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
|
|
||||||
if (this.duplicatedElements.length > 0) {
|
if (this.duplicatedElements.length > 0) {
|
||||||
editor.selection.setSelectedElements({
|
return {
|
||||||
elements: this.duplicatedElements,
|
select: this.duplicatedElements,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,9 +97,6 @@ export class DuplicateElementsCommand extends Command {
|
||||||
if (this.savedState) {
|
if (this.savedState) {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
editor.timeline.updateTracks(this.savedState);
|
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 type { TimelineTrack } from "@/lib/timeline";
|
||||||
import { generateUUID } from "@/utils/id";
|
import { generateUUID } from "@/utils/id";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
|
|
@ -9,7 +9,6 @@ import { getSourceSpanAtClipTime } from "@/lib/retime";
|
||||||
export class SplitElementsCommand extends Command {
|
export class SplitElementsCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
private rightSideElements: { trackId: string; elementId: string }[] = [];
|
private rightSideElements: { trackId: string; elementId: string }[] = [];
|
||||||
private previousSelection: { trackId: string; elementId: string }[] = [];
|
|
||||||
private readonly elements: { trackId: string; elementId: string }[];
|
private readonly elements: { trackId: string; elementId: string }[];
|
||||||
private readonly splitTime: number;
|
private readonly splitTime: number;
|
||||||
private readonly retainSide: "both" | "left" | "right";
|
private readonly retainSide: "both" | "left" | "right";
|
||||||
|
|
@ -37,10 +36,9 @@ export class SplitElementsCommand extends Command {
|
||||||
return this.rightSideElements;
|
return this.rightSideElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): CommandResult | undefined {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
this.previousSelection = editor.selection.getSelectedElements();
|
|
||||||
this.rightSideElements = [];
|
this.rightSideElements = [];
|
||||||
|
|
||||||
const updatedTracks = this.savedState.map((track) => {
|
const updatedTracks = this.savedState.map((track) => {
|
||||||
|
|
@ -173,9 +171,9 @@ export class SplitElementsCommand extends Command {
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
|
|
||||||
if (this.rightSideElements.length > 0) {
|
if (this.rightSideElements.length > 0) {
|
||||||
editor.selection.setSelectedElements({
|
return {
|
||||||
elements: this.rightSideElements,
|
select: this.rightSideElements,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,9 +181,6 @@ export class SplitElementsCommand extends Command {
|
||||||
if (this.savedState) {
|
if (this.savedState) {
|
||||||
const editor = EditorCore.getInstance();
|
const editor = EditorCore.getInstance();
|
||||||
editor.timeline.updateTracks(this.savedState);
|
editor.timeline.updateTracks(this.savedState);
|
||||||
editor.selection.setSelectedElements({
|
|
||||||
elements: this.previousSelection,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue