task(A2): tool dispatcher behind flag
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* 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<string, unknown>;
|
||||
/** 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;
|
||||
}
|
||||
|
||||
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(): ToolContext {
|
||||
const toolId = this.active?.manifest.id ?? '';
|
||||
return {
|
||||
options: this.deps.getOptions?.(toolId) ?? {},
|
||||
setStatus: (msg) => this.setStatus(msg),
|
||||
setPreview: (el) => this.deps.setPreview?.(el),
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user