// Tool-System für das aktive Zeichnen im Grundriss (docs/design/drawing-tools.md). // // Werkzeuge sind reine Funktionen über einen internen `ToolState` // (Discriminated Union je Werkzeug). Sie greifen NUR über `commit(project)` // immutabel auf das Modell zu und schreiben nie Plan-Primitive — die Ansicht // bleibt abgeleitet. Bezeichner englisch, UI-Text via t(). import type { DrawingLevel, Project, Vec2 } from "../model/types"; /** Werkzeug-Identität. */ export type ToolId = | "select" | "wall" | "ceiling" | "window" | "door" | "stair" | "room" | "line" | "polyline" | "rect"; // ── Snapping ─────────────────────────────────────────────────────────────── export type SnapKind = | "endpoint" | "midpoint" | "intersection" | "center" | "quadrant" | "onEdge" | "grid" | "ortho" | "extension"; export interface SnapResult { /** Gefangener Punkt (Meter). */ point: Vec2; kind: SnapKind; /** Quell-Bezugspunkt (für Hilfslinien bei ortho/extension), optional. */ refA?: Vec2; /** Bildschirm-Distanz Cursor→Snap (px) — für die Auswahl des Besten. */ distPx: number; } export interface SnapSettings { enabled: boolean; endpoint: boolean; midpoint: boolean; intersection: boolean; center: boolean; onEdge: boolean; grid: boolean; /** Rasterweite in Metern. */ gridSize: number; /** Ortho/Winkelraster (Shift erzwingt zusätzlich). */ ortho: boolean; /** Winkelraster in Grad (z. B. 90 = H/V). */ angleStep: number; /** Fangradius am Bildschirm in Pixeln. */ tolerancePx: number; } /** Default-Snap-Einstellungen (endpoint/midpoint/intersection/onEdge + grid). */ export const DEFAULT_SNAP: SnapSettings = { enabled: true, endpoint: true, midpoint: true, intersection: true, center: false, onEdge: true, grid: true, gridSize: 0.1, ortho: false, angleStep: 90, tolerancePx: 12, }; // ── Werkzeug-Schnittstelle ────────────────────────────────────────────────── /** Live-Kontext, den ein Werkzeug bei jedem Schritt erhält. */ export interface ToolContext { project: Project; /** Aktives Geschoss/die aktive Zeichnungsebene (Ziel der neuen Elemente). */ level: DrawingLevel; /** Default-Kategorie-Code für neue Elemente. */ defaultCategoryCode: string; /** Aktiver Wandtyp für das Wand-Werkzeug. */ activeWallTypeId: string; /** Aktiver Linienstil-Code für 2D-Primitive. */ activeLineStyleId: string; } /** Ein an einer Modellposition ausgelöstes Pointer-Ereignis (fertig gesnappt). */ export interface ToolPointer { /** Roher Modellpunkt (vor Snapping), in Metern. */ raw: Vec2; /** Gesnappter Punkt + Marker-Info; null = kein Snap. */ snap: SnapResult | null; /** Effektiver Punkt = snap?.point ?? raw. */ point: Vec2; shift: boolean; ctrl: boolean; alt: boolean; /** 0 = links, 2 = rechts. */ button: number; } /** * Leichte Vorschau-Form (Rubber-Band). Bewusst NICHT der volle `Primitive`-Typ * (kein HatchRender nötig); PlanView zeichnet sie mit einer Vorschau-CSS-Klasse * (gestrichelt, Akzentfarbe). Modell-Meter; PlanView projiziert via toScreen. */ export type DraftShape = | { kind: "line"; a: Vec2; b: Vec2 } | { kind: "poly"; pts: Vec2[]; closed?: boolean }; /** Darstellbare Vorschau (Rubber-Band). */ export interface ToolDraft { /** Vorschau-Formen (gestrichelt/halbtransparent gezeichnet). */ preview: DraftShape[]; /** Bereits gesetzte „feste" Stützpunkte (kleine Quadrate). */ vertices: Vec2[]; /** Aktiver Snap (für den Bildschirm-Marker), optional. */ snap?: SnapResult | null; /** Optionaler Maß-/Winkel-Text am Cursor (z. B. „3.20 m · 90°"). */ hud?: { at: Vec2; text: string }; } /** Was ein Werkzeug-Schritt nach außen meldet. */ export interface ToolResult { /** Neuer Vorschau-Zustand; null = nichts zu zeigen. */ draft: ToolDraft | null; /** Bei Abschluss: Mutation, die App über setProject anwendet. */ commit?: (p: Project) => Project; /** true → Werkzeug ist fertig und kehrt in seinen Ruhezustand zurück. */ done?: boolean; } /** * Interner Zustand eines Werkzeugs. Basis-Form; jedes Werkzeug verfeinert sie zu * einer eigenen Discriminated Union (z. B. WallState) und castet beim Eintritt. */ export interface ToolState { phase: string; } /** Die Werkzeug-Schnittstelle (reine Funktionen über einen internen State). */ export interface Tool { id: ToolId; /** UI-Label-Key (i18n), z. B. „tool.wall". */ labelKey: string; /** Statuszeilen-Hinweis-Key je Phase. */ hintKey: (state: ToolState) => string; /** Nur auf Geschossen aktiv? (Wand braucht ein Geschoss.) */ floorOnly?: boolean; /** Initialer Ruhezustand. */ init(): ToolState; /** Klick (Punkt setzen). */ onClick(state: ToolState, p: ToolPointer, ctx: ToolContext): [ToolState, ToolResult]; /** Bewegung (Hover/Drag): nur Vorschau, nie Commit. */ onMove(state: ToolState, p: ToolPointer, ctx: ToolContext): [ToolState, ToolResult]; /** Doppelklick/Enter/Rechtsklick: mehrteilige Werkzeuge abschließen. */ onCommitGesture(state: ToolState, ctx: ToolContext): [ToolState, ToolResult]; /** Esc: Entwurf verwerfen, im selben Werkzeug bleiben. */ onCancel(state: ToolState): [ToolState, ToolResult]; /** Backspace: letzten gesetzten Punkt zurücknehmen (optional). */ onUndoPoint?(state: ToolState, ctx: ToolContext): [ToolState, ToolResult]; } // ── PlanView-Brücke ────────────────────────────────────────────────────────── // PlanView meldet rohe Modellpunkte + die aktuelle Pixel/Meter-Skala nach oben; // der ToolController (App) snappt, fährt das Werkzeug und hält den `draft`, // den PlanView als Overlay zeichnet. So bleibt die Snap-/Modell-Logik außerhalb // der reinen Darstellungs-/Eingabeschicht. export interface ToolMods { shift: boolean; ctrl: boolean; alt: boolean; } export interface ToolHandlers { /** Hover/Drag: roher Modellpunkt (Meter) + Pixel/Meter + Modifikatoren. */ onToolMove(raw: Vec2, pxPerMeter: number, mods: ToolMods): void; /** Klick (Punkt setzen). */ onToolClick(raw: Vec2, pxPerMeter: number, mods: ToolMods): void; /** Doppelklick / Rechtsklick: mehrteiliges Werkzeug abschließen. */ onToolCommit(): void; /** Aktuelle Vorschau (vom Controller gehalten). */ draft: ToolDraft | null; } // ── ID-Vergabe ─────────────────────────────────────────────────────────────── let idCounter = 0; /** Neue eindeutige ID, konsistent mit der Praxis in App (`prefix-Date-counter`). */ export function uniqueId(prefix: string): string { idCounter += 1; return `${prefix}-${Date.now()}-${idCounter}`; }