/** * dispatcher.ts – InteractionDispatcher (Task A2). * * Ersetzt langfristig den 1157-Zeilen-Switch der InteractionEngine: * - Routet DOM-Pointer-/Key-Events an das AKTIVE V2-Werkzeug (ToolExtensionV2) * - Normalisiert Events (pointer.ts) zu Weltkoordinaten * - Führt eine ToolSession als Down/Up-Guard * - Kein Werkzeugwissen! Alle Logik liegt in den Plugins. * * Aktivierung per Feature-Flag VITE_TOOL_DISPATCHER=v2 (CanvasArea, A2 Schritt 4). */ import type { ToolExtensionV2, ToolContext } from '../plugins/types'; import { pluginRegistry } from '../plugins/PluginRegistry'; import { toToolPointerEvent } from './pointer'; import { ToolSession } from './session'; import type { ZoomPanController } from '../canvas/ZoomPanController'; export interface DispatcherDeps { zoomPan: ZoomPanController; /** Liefert Optionsstände je ToolId (typisch: toolStore.optionValues). */ getOptions?(toolId: string): Record; /** Statuszeile (typisch: StatusBar-Bridge). */ setStatus?(msg: string): void; /** Live-Vorschau-Ziel (typisch RenderEngine.setPreviewElement). */ setPreview?(el: unknown | null): void; /** Tool-Nachschlag überschreibbar (Default: aktive Registry-Tools). */ toolLookup?(toolId: string): ToolExtensionV2 | undefined; /** Dokument-Fassade für Tools (ab A3; lazy pro Event abgefragt). */ getDoc?(): import('../kernel/document/CADDocument').CADDocument | undefined; /** Selektions-Bridge für Tools (ab A3; lazy pro Event abgefragt). */ getSelectionBridge?(): import('../plugins/types').ToolSelectionBridge; } export class InteractionDispatcher { private readonly canvas: HTMLCanvasElement; private readonly deps: DispatcherDeps; private readonly session = new ToolSession(); private active: ToolExtensionV2 | null = null; private bound = false; /** Zuletzt gesetzter Status-Text (für Tests/Debug). */ lastStatus = ''; constructor(canvas: HTMLCanvasElement, deps: DispatcherDeps) { this.canvas = canvas; this.deps = deps; this.attach(); } /** Setzt das aktive Werkzeug (null = deaktiviert Routing). */ setActive(toolId: string | null): void { if (this.active && this.session.isActive) { // Sitzung sauber abbrechen, bevor gewechselt wird this.active.handlers.cancel?.(this.buildContext()); this.session.end(); } if (!toolId) { this.active = null; return; } const lookup = this.deps.toolLookup ?? ((id: string) => pluginRegistry.getToolV2(id)); const tool = lookup(toolId); if (!tool) { console.warn(`[InteractionDispatcher] kein aktives V2-Tool '${toolId}'`); this.active = null; return; } this.active = tool; this.setStatus(`${tool.manifest.label} aktiv`); } /** Aktives Werkzeug (oder null). */ getActive(): ToolExtensionV2 | null { return this.active; } /** Entfernt alle Event-Listener (aufräumen beim Unmount). */ destroy(): void { if (!this.bound) return; this.canvas.removeEventListener('pointerdown', this.onDown); window.removeEventListener('pointermove', this.onMove); window.removeEventListener('pointerup', this.onUp); window.removeEventListener('keydown', this.onKey); this.bound = false; } // ── intern ─────────────────────────────────────────────── private attach(): void { if (this.bound) return; this.canvas.addEventListener('pointerdown', this.onDown); window.addEventListener('pointermove', this.onMove); window.addEventListener('pointerup', this.onUp); window.addEventListener('keydown', this.onKey); this.bound = true; } private buildContext(): import('../plugins/types').FullToolContext { const toolId = this.active?.manifest.id ?? ''; return { options: this.deps.getOptions?.(toolId) ?? {}, setStatus: (msg) => this.setStatus(msg), setPreview: (el) => this.deps.setPreview?.(el), doc: this.deps.getDoc?.(), selection: this.deps.getSelectionBridge?.(), }; } private setStatus(msg: string): void { this.lastStatus = msg; this.deps.setStatus?.(msg); } private onDown = (e: PointerEvent): void => { if (!this.active) return; // Rechtsklick = Abbruch wie ESC if (e.button === 2) { this.cancelActive(); return; } const tpe = toToolPointerEvent(e, this.canvas, this.deps.zoomPan); this.session.begin(); this.active.handlers.down?.(tpe, this.buildContext()); }; private onMove = (e: PointerEvent): void => { if (!this.active || !this.session.isActive) return; const tpe = toToolPointerEvent(e, this.canvas, this.deps.zoomPan); this.active.handlers.move?.(tpe, this.buildContext()); }; private onUp = (e: PointerEvent): void => { if (!this.active || !this.session.isActive) return; const tpe = toToolPointerEvent(e, this.canvas, this.deps.zoomPan); try { this.active.handlers.up?.(tpe, this.buildContext()); } finally { this.session.end(); } }; private onKey = (e: KeyboardEvent): void => { if (!this.active) return; if (e.key === 'Escape') { this.cancelActive(); return; } const consumed = this.active.handlers.key?.(e, this.buildContext()) === true; if (consumed) e.preventDefault(); }; private cancelActive(): void { if (!this.active) return; this.active.handlers.cancel?.(this.buildContext()); this.session.end(); } }