diff options
Diffstat (limited to 'apps/web/src/core/managers')
| -rw-r--r-- | apps/web/src/core/managers/commands.ts | 34 | ||||
| -rw-r--r-- | apps/web/src/core/managers/selection-manager.ts | 268 | ||||
| -rw-r--r-- | apps/web/src/core/managers/timeline-manager.ts | 52 |
3 files changed, 264 insertions, 90 deletions
diff --git a/apps/web/src/core/managers/commands.ts b/apps/web/src/core/managers/commands.ts index 2f652a85..7cdd6373 100644 --- a/apps/web/src/core/managers/commands.ts +++ b/apps/web/src/core/managers/commands.ts @@ -1,12 +1,13 @@ import type { EditorCore } from "@/core"; import type { Command, CommandResult } from "@/lib/commands"; +import type { EditorSelectionSnapshot } from "@/lib/selection/editor-selection"; import { applyRippleAdjustments, computeRippleAdjustments } from "@/lib/ripple"; -import type { ElementRef, SceneTracks } from "@/lib/timeline/types"; +import type { SceneTracks } from "@/lib/timeline/types"; interface CommandHistoryEntry { command: Command; - previousSelection: ElementRef[]; - selectionOverride?: ElementRef[]; + previousSelection: EditorSelectionSnapshot; + selectionOverride?: EditorSelectionSnapshot; } export class CommandManager { @@ -19,7 +20,7 @@ export class CommandManager { execute({ command }: { command: Command }): Command { const beforeTracks = this.isRippleEnabled - ? this.editor.scenes.getActiveSceneOrNull()?.tracks ?? null + ? (this.editor.scenes.getActiveSceneOrNull()?.tracks ?? null) : null; const previousSelection = this.getSelectionSnapshot(); const result = command.execute(); @@ -55,11 +56,11 @@ export class CommandManager { // 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. + // that happened between commands. Commands that remove editor-owned + // selection targets must declare a selection override to clear stale refs. if (entry.selectionOverride !== undefined) { - this.editor.selection.setSelectedElements({ - elements: [...entry.previousSelection], + this.editor.selection.restoreSnapshot({ + snapshot: entry.previousSelection, }); } this.redoStack.push(entry); @@ -74,7 +75,7 @@ export class CommandManager { } const beforeTracks = this.isRippleEnabled - ? this.editor.scenes.getActiveSceneOrNull()?.tracks ?? null + ? (this.editor.scenes.getActiveSceneOrNull()?.tracks ?? null) : null; const previousSelection = this.getSelectionSnapshot(); const result = entry.command.redo(); @@ -102,20 +103,19 @@ export class CommandManager { this.redoStack = []; } - private getSelectionSnapshot(): ElementRef[] { - return [...this.editor.selection.getSelectedElements()]; + private getSelectionSnapshot(): EditorSelectionSnapshot { + return this.editor.selection.getSnapshot(); } private applySelectionOverride( result: CommandResult | undefined, - ): ElementRef[] | undefined { - if (result?.select === undefined) { + ): EditorSelectionSnapshot | undefined { + if (!result?.selection) { return undefined; } - - const selectionOverride = [...result.select]; - this.editor.selection.setSelectedElements({ elements: selectionOverride }); - return selectionOverride; + return this.editor.selection.applySelectionPatch({ + patch: result.selection, + }); } private runReactors(): void { diff --git a/apps/web/src/core/managers/selection-manager.ts b/apps/web/src/core/managers/selection-manager.ts index 9bafc6d7..5da4fa60 100644 --- a/apps/web/src/core/managers/selection-manager.ts +++ b/apps/web/src/core/managers/selection-manager.ts @@ -1,73 +1,195 @@ -import type { EditorCore } from "@/core";
-import type { SelectedKeyframeRef } from "@/lib/animation/types";
-import type { ElementRef } from "@/lib/timeline/types";
-
-export class SelectionManager {
- private selectedElements: ElementRef[] = [];
- private selectedKeyframes: SelectedKeyframeRef[] = [];
- private keyframeSelectionAnchor: SelectedKeyframeRef | null = null;
- private listeners = new Set<() => void>();
-
- constructor(editor: EditorCore) {
- void editor;
- }
-
- getSelectedElements(): ElementRef[] {
- return this.selectedElements;
- }
-
- getSelectedKeyframes(): SelectedKeyframeRef[] {
- return this.selectedKeyframes;
- }
-
- getKeyframeSelectionAnchor(): SelectedKeyframeRef | null {
- return this.keyframeSelectionAnchor;
- }
-
- setSelectedElements({ elements }: { elements: ElementRef[] }): void {
- this.selectedElements = elements;
- this.selectedKeyframes = [];
- this.keyframeSelectionAnchor = null;
- this.notify();
- }
-
- setSelectedKeyframes({
- keyframes,
- anchorKeyframe,
- }: {
- keyframes: SelectedKeyframeRef[];
- anchorKeyframe?: SelectedKeyframeRef | null;
- }): void {
- this.selectedKeyframes = keyframes;
- if (anchorKeyframe !== undefined) {
- this.keyframeSelectionAnchor = anchorKeyframe;
- } else if (keyframes.length === 0) {
- this.keyframeSelectionAnchor = null;
- }
- this.notify();
- }
-
- clearSelection(): void {
- this.selectedElements = [];
- this.selectedKeyframes = [];
- this.keyframeSelectionAnchor = null;
- this.notify();
- }
-
- clearKeyframeSelection(): void {
- this.selectedKeyframes = [];
- this.keyframeSelectionAnchor = null;
- this.notify();
- }
-
- subscribe(listener: () => void): () => void {
- this.listeners.add(listener);
- return () => this.listeners.delete(listener);
- }
-
- private notify(): void {
- this.listeners.forEach((fn) => {
- fn();
- });
- }
-}
+import type { EditorCore } from "@/core"; +import type { SelectedKeyframeRef } from "@/lib/animation/types"; +import type { + EditorSelectionKind, + EditorSelectionPatch, + EditorSelectionSnapshot, + SelectedMaskPointSelection, +} from "@/lib/selection/editor-selection"; +import type { ElementRef } from "@/lib/timeline/types"; + +export class SelectionManager { + private selectedElements: ElementRef[] = []; + private selectedKeyframes: SelectedKeyframeRef[] = []; + private keyframeSelectionAnchor: SelectedKeyframeRef | null = null; + private selectedMaskPoints: SelectedMaskPointSelection | null = null; + private listeners = new Set<() => void>(); + + constructor(editor: EditorCore) { + void editor; + } + + getSelectedElements(): ElementRef[] { + return this.selectedElements; + } + + getSelectedKeyframes(): SelectedKeyframeRef[] { + return this.selectedKeyframes; + } + + getKeyframeSelectionAnchor(): SelectedKeyframeRef | null { + return this.keyframeSelectionAnchor; + } + + getSelectedMaskPointSelection(): SelectedMaskPointSelection | null { + return this.selectedMaskPoints; + } + + getActiveSelectionKind(): EditorSelectionKind | null { + if ((this.selectedMaskPoints?.pointIds.length ?? 0) > 0) { + return "mask-points"; + } + if (this.selectedKeyframes.length > 0) { + return "keyframes"; + } + if (this.selectedElements.length > 0) { + return "elements"; + } + return null; + } + + getSnapshot(): EditorSelectionSnapshot { + return { + selectedElements: [...this.selectedElements], + selectedKeyframes: [...this.selectedKeyframes], + keyframeSelectionAnchor: this.keyframeSelectionAnchor, + selectedMaskPoints: this.selectedMaskPoints + ? { + ...this.selectedMaskPoints, + pointIds: [...this.selectedMaskPoints.pointIds], + } + : null, + }; + } + + setSelectedElements({ elements }: { elements: ElementRef[] }): void { + this.selectedElements = elements; + this.selectedKeyframes = []; + this.keyframeSelectionAnchor = null; + this.selectedMaskPoints = null; + this.notify(); + } + + setSelectedKeyframes({ + keyframes, + anchorKeyframe, + }: { + keyframes: SelectedKeyframeRef[]; + anchorKeyframe?: SelectedKeyframeRef | null; + }): void { + this.selectedKeyframes = keyframes; + if (anchorKeyframe !== undefined) { + this.keyframeSelectionAnchor = anchorKeyframe; + } else if (keyframes.length === 0) { + this.keyframeSelectionAnchor = null; + } + this.selectedMaskPoints = null; + this.notify(); + } + + setSelectedMaskPoints({ + selection, + }: { + selection: SelectedMaskPointSelection | null; + }): void { + this.selectedMaskPoints = + selection && selection.pointIds.length > 0 + ? { + ...selection, + pointIds: [...selection.pointIds], + } + : null; + this.selectedKeyframes = []; + this.keyframeSelectionAnchor = null; + this.notify(); + } + + clearSelection(): void { + this.selectedElements = []; + this.selectedKeyframes = []; + this.keyframeSelectionAnchor = null; + this.selectedMaskPoints = null; + this.notify(); + } + + clearKeyframeSelection(): void { + this.selectedKeyframes = []; + this.keyframeSelectionAnchor = null; + this.notify(); + } + + clearMaskPointSelection(): void { + if (!this.selectedMaskPoints) { + return; + } + this.selectedMaskPoints = null; + this.notify(); + } + + clearMostSpecificSelection(): boolean { + const activeSelectionKind = this.getActiveSelectionKind(); + if (activeSelectionKind === "mask-points") { + this.clearMaskPointSelection(); + return true; + } + if (activeSelectionKind === "keyframes") { + this.clearKeyframeSelection(); + return true; + } + if (activeSelectionKind === "elements") { + this.setSelectedElements({ elements: [] }); + return true; + } + return false; + } + + applySelectionPatch({ + patch, + }: { + patch: EditorSelectionPatch; + }): EditorSelectionSnapshot { + if (patch.selectedElements !== undefined) { + this.selectedElements = [...patch.selectedElements]; + } + if (patch.selectedKeyframes !== undefined) { + this.selectedKeyframes = [...patch.selectedKeyframes]; + } + if (patch.keyframeSelectionAnchor !== undefined) { + this.keyframeSelectionAnchor = patch.keyframeSelectionAnchor; + } + if (patch.selectedMaskPoints !== undefined) { + this.selectedMaskPoints = patch.selectedMaskPoints + ? { + ...patch.selectedMaskPoints, + pointIds: [...patch.selectedMaskPoints.pointIds], + } + : null; + } + this.notify(); + return this.getSnapshot(); + } + + restoreSnapshot({ snapshot }: { snapshot: EditorSelectionSnapshot }): void { + this.selectedElements = [...snapshot.selectedElements]; + this.selectedKeyframes = [...snapshot.selectedKeyframes]; + this.keyframeSelectionAnchor = snapshot.keyframeSelectionAnchor; + this.selectedMaskPoints = snapshot.selectedMaskPoints + ? { + ...snapshot.selectedMaskPoints, + pointIds: [...snapshot.selectedMaskPoints.pointIds], + } + : null; + this.notify(); + } + + subscribe(listener: () => void): () => void { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } + + private notify(): void { + this.listeners.forEach((fn) => { + fn(); + }); + } +} diff --git a/apps/web/src/core/managers/timeline-manager.ts b/apps/web/src/core/managers/timeline-manager.ts index c1398e8b..898a7e89 100644 --- a/apps/web/src/core/managers/timeline-manager.ts +++ b/apps/web/src/core/managers/timeline-manager.ts @@ -1,4 +1,5 @@ import type { EditorCore } from "@/core"; +import type { ElementBounds } from "@/lib/preview/element-bounds"; import type { ParamValues } from "@/lib/params"; import type { SceneTracks, @@ -44,6 +45,8 @@ import { RetimeKeyframeCommand, UpdateScalarKeyframeCurveCommand, AddClipEffectCommand, + DeleteCustomMaskPointsCommand, + InsertCustomMaskPointCommand, RemoveClipEffectCommand, UpdateClipEffectParamsCommand, ToggleClipEffectCommand, @@ -344,6 +347,55 @@ export class TimelineManager { this.editor.command.execute({ command }); } + deleteCustomMaskPoints({ + trackId, + elementId, + maskId, + pointIds, + }: { + trackId: string; + elementId: string; + maskId: string; + pointIds: string[]; + }): void { + if (pointIds.length === 0) { + return; + } + const command = new DeleteCustomMaskPointsCommand({ + trackId, + elementId, + maskId, + pointIds, + }); + this.editor.command.execute({ command }); + } + + insertCustomMaskPoint({ + trackId, + elementId, + maskId, + segmentIndex, + canvasPoint, + bounds, + }: { + trackId: string; + elementId: string; + maskId: string; + segmentIndex: number; + canvasPoint: { x: number; y: number }; + bounds: ElementBounds; + }): void { + const command = new InsertCustomMaskPointCommand({ + trackId, + elementId, + maskId, + segmentIndex, + canvasPoint, + bounds, + }); + this.editor.command.execute({ command }); + } + updateClipEffectParams({ trackId, elementId, |
