From c46d28891fee62ca2d3132da876af6411a41d9da Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 15 Apr 2026 15:45:50 +0200 Subject: feat: text / custom masks, fix preview pan, refactor selection --- apps/web/src/core/managers/selection-manager.ts | 268 +++++++++++++++++------- 1 file changed, 195 insertions(+), 73 deletions(-) (limited to 'apps/web/src/core/managers/selection-manager.ts') 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(); + }); + } +} -- cgit v1.3.1