0cfddd8930
Neuer GPU-Renderer fuer den Grundriss (src/plan/glPlan/): Earcut-Tessellierung (konkav-faehig), gehrte Linienzuege (Miter), echte Papier-mm-Strichbreiten im Massstab (repliziert den SVG-printStrokeVb-Pfad), Hybrid mit scharfem SVG-Text- Overlay. GPU ist der Standardpfad; der SVG-Renderer bleibt automatischer Fallback, falls WebGL2/Shader nicht verfuegbar sind. Imperativer Pan (rAF + CSS-transform) fuer fluessige Interaktion ohne React-Re-Render je Frame. Enthaelt zudem den bisher nicht committeten Arbeitsstand des Browser-BIM (Oeffnungen, Treppen, Raeume, Decken, DXF-Export, Materialbibliothek, Kontext- Import, Tauri-Compute-Boundary-PoC).
220 lines
9.3 KiB
TypeScript
220 lines
9.3 KiB
TypeScript
// Command-Engine — Verallgemeinerung des `Tool`-Interfaces (src/tools/types.ts)
|
|
// zu einem Rhino-artigen Befehlsmodell (docs/design/rhino-command-system.md §3.1).
|
|
//
|
|
// Ein `Command` ist eine kleine State-Machine: jeder Schritt zeigt einen Prompt,
|
|
// nimmt bestimmte Eingabe-Arten an (Punkt | Zahl | Option | Auswahl) und liefert
|
|
// aus (state, input) den nächsten Zustand + ein `CommandResult` (Vorschau +
|
|
// optional commit/done). Damit speisen Maus-Picks UND getippte Koordinaten/
|
|
// Optionen DENSELBEN Schritt — es gibt nur einen Eingabepfad.
|
|
//
|
|
// Bezeichner englisch, sichtbarer Text via t() (i18n-Keys, Namespace `cmd.*`).
|
|
|
|
import type { Project, Vec2 } from "../model/types";
|
|
import type { TranslationKey } from "../i18n";
|
|
import type { DrawingLevel } from "../model/types";
|
|
import type { SnapResult, ToolDraft } from "../tools/types";
|
|
|
|
export type { DraftShape, SnapResult, ToolDraft } from "../tools/types";
|
|
|
|
// ── Eingabe-Arten ────────────────────────────────────────────────────────────
|
|
|
|
/** Arten von Eingaben, die ein Schritt annehmen kann. */
|
|
export type AcceptKind = "point" | "number" | "option" | "selection";
|
|
|
|
/**
|
|
* Geparste, noch nicht aufgelöste Roh-Eingabe aus der Command-Line bzw. der
|
|
* Maus. Discriminated Union (§2.7). Die Engine löst `point`-bezogene Varianten
|
|
* mit `lastPoint` + Cursor-Richtung zu einem konkreten Modellpunkt auf, bevor
|
|
* sie den Schritt aufruft.
|
|
*/
|
|
export type CmdInput =
|
|
| { kind: "point"; point: Vec2; snap?: SnapResult | null }
|
|
| { kind: "number"; value: number }
|
|
| { kind: "option"; id: string }
|
|
| { kind: "text"; text: string }
|
|
| { kind: "selection"; wallIds: string[]; drawingIds: string[] };
|
|
|
|
// ── Optionen (klickbare Inline-Klammern, §2.4) ───────────────────────────────
|
|
|
|
/**
|
|
* Eine Inline-Option eines Schritts. `value` (gesetzt) → wird als „Name=Value"
|
|
* angezeigt und ist togglebar/abfragbar; ohne `value` ist es eine Action-Option
|
|
* (verzweigt sofort). `id` ist der englische Schlüssel (auch zum Tippen, Präfix).
|
|
*/
|
|
export interface CmdOption {
|
|
id: string;
|
|
/** Sichtbares Label (i18n). */
|
|
labelKey: TranslationKey;
|
|
/** Aktueller Wert (Toggle/Value-Option); fehlt → Action-Option. */
|
|
value?: string;
|
|
}
|
|
|
|
// ── Kontext + Ergebnis ───────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Die aktuelle Auswahl (Rhino: erst selektieren, dann Befehl). Editierbefehle
|
|
* (Move/Copy/Offset) lesen sie hieraus, statt sie selbst zu picken. `wallIds`
|
|
* sind die gewählten Wände, `drawingId` ist das gewählte 2D-Element (sich
|
|
* gegenseitig ausschließend, wie in selectionSlice).
|
|
*/
|
|
export interface CommandSelection {
|
|
wallIds: string[];
|
|
drawingId: string | null;
|
|
/**
|
|
* ALLE gewählten 2D-Elemente (Mehrfachauswahl). `drawingId` bleibt das einzeln
|
|
* selektierte Element (Move/Copy/Mirror operieren darauf); Mengen-Aktionen wie
|
|
* Join/Split lesen `drawingIds`. Optional/abwärtskompatibel: fehlt es, gilt
|
|
* `drawingId` (falls gesetzt) als einziges Element.
|
|
*/
|
|
drawingIds?: string[];
|
|
}
|
|
|
|
/** Live-Kontext, den ein Befehl bei jedem Schritt erhält (analog ToolContext). */
|
|
export interface CommandContext {
|
|
project: Project;
|
|
/** Aktive Zeichnungsebene (Ziel neuer Elemente). */
|
|
level: DrawingLevel;
|
|
/** Default-Kategorie-Code für neue Elemente. */
|
|
defaultCategoryCode: string;
|
|
/** Aktiver Linienstil-Code für 2D-Primitive. */
|
|
activeLineStyleId: string;
|
|
/**
|
|
* Aktiver Wandtyp (für den `wall`-Befehl). Liefert Schichtaufbau + Dicke neuer
|
|
* Wände. Aus dem Store gefüllt (analog ToolContext.activeWallTypeId).
|
|
*/
|
|
activeWallTypeId: string;
|
|
/**
|
|
* Letzter gesetzter Punkt im laufenden Befehl (für `r`/polar-Auflösung); null
|
|
* am ersten Punkt.
|
|
*/
|
|
lastPoint: Vec2 | null;
|
|
/**
|
|
* Vorab gesetzte Auswahl (Wände + ein 2D-Element). Editierbefehle operieren
|
|
* darauf (Rhino: erst selektieren, dann Befehl). Beim Befehlsstart aus dem
|
|
* Store gefüllt.
|
|
*/
|
|
selection: CommandSelection;
|
|
}
|
|
|
|
/** Was ein Befehls-Schritt nach außen meldet (analog ToolResult + commit/done). */
|
|
export interface CommandResult {
|
|
/** Neuer Vorschau-Zustand; null = nichts zu zeigen. */
|
|
draft: ToolDraft | null;
|
|
/** Bei Abschluss: immutable Mutation, die die Engine über setProject anwendet. */
|
|
commit?: (p: Project) => Project;
|
|
/** true → Befehl ist fertig und kehrt in den Ruhezustand zurück. */
|
|
done?: boolean;
|
|
}
|
|
|
|
// ── Befehls-Zustand ──────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Interner Zustand eines laufenden Befehls. Basis-Form (`phase` + `lastPoint`);
|
|
* jeder Befehl verfeinert sie zu seiner eigenen Discriminated Union und castet
|
|
* beim Eintritt. `lastPoint` lebt im Zustand, damit der Parser `r`/polar relativ
|
|
* dazu auflösen kann.
|
|
*/
|
|
export interface CommandState {
|
|
phase: string;
|
|
/** Letzter gesetzter Punkt (für relative/polare Eingabe), null am Anfang. */
|
|
lastPoint: Vec2 | null;
|
|
}
|
|
|
|
// ── Tab-Feld-Zyklus (Präzisionseingabe §2.7, erweitert) ──────────────────────
|
|
|
|
/**
|
|
* Ein geordnetes Zahlenfeld eines Schritts (z. B. Länge, Winkel, Breite, Höhe,
|
|
* Radius). Mit Tab zyklt der Nutzer durch die Felder; eine getippte Zahl LOCKT
|
|
* das aktive Feld, ungelockte Felder folgen weiter der Maus.
|
|
*/
|
|
export interface CommandField {
|
|
/** Englischer Schlüssel (Identität des Feldes, z. B. „length"). */
|
|
id: string;
|
|
/** Sichtbares Label (i18n, Namespace `cmd.field.*`). */
|
|
labelKey: TranslationKey;
|
|
}
|
|
|
|
// ── Befehls-Schnittstelle ────────────────────────────────────────────────────
|
|
|
|
/** Die Command-Schnittstelle (reine Funktionen über einen internen State). */
|
|
export interface Command {
|
|
/** Befehlsname (englisch, in der Command-Line getippt), z. B. „line". */
|
|
name: string;
|
|
/** UI-Label-Key (i18n). */
|
|
labelKey: TranslationKey;
|
|
/** Prompt-Text-Key des aktuellen Schritts. */
|
|
prompt(state: CommandState): TranslationKey;
|
|
/** Eingabe-Arten, die der aktuelle Schritt annimmt. */
|
|
accepts(state: CommandState): AcceptKind[];
|
|
/** Inline-Optionen des aktuellen Schritts (klickbar/tippbar). */
|
|
options(state: CommandState): CmdOption[];
|
|
/** Nur auf Geschossen aktiv? (analog floorOnly bei Tools.) */
|
|
floorOnly?: boolean;
|
|
/** Initialer Schritt-Zustand. */
|
|
init(): CommandState;
|
|
/** Eine Eingabe (Punkt/Zahl/Option/Auswahl) verarbeiten. */
|
|
onInput(
|
|
state: CommandState,
|
|
input: CmdInput,
|
|
ctx: CommandContext,
|
|
): [CommandState, CommandResult];
|
|
/** Hover/Bewegung (roher bzw. gesnappter Cursor): nur Vorschau. */
|
|
onMove(
|
|
state: CommandState,
|
|
point: Vec2,
|
|
snap: SnapResult | null,
|
|
ctx: CommandContext,
|
|
): [CommandState, CommandResult];
|
|
/** Enter/Space/Rechtsklick: Schritt bestätigen / mehrteilig beenden. */
|
|
onConfirm(state: CommandState, ctx: CommandContext): [CommandState, CommandResult];
|
|
/** Esc: Entwurf verwerfen, Befehl beenden. */
|
|
onCancel(state: CommandState): [CommandState, CommandResult];
|
|
|
|
/**
|
|
* Sofort-Befehl (optional): läuft direkt beim Start OHNE Punkt-Picken und liefert
|
|
* sein Ergebnis (i. d. R. commit + done). Für Aktionen auf der bestehenden
|
|
* Auswahl (z. B. Join/Split), die keine interaktive Geste brauchen. Ist diese
|
|
* Methode gesetzt, ruft die Engine sie unmittelbar nach `init()` auf und der
|
|
* Befehl kehrt (bei done) sofort in den Ruhezustand zurück. So ist „Ctrl+J" und
|
|
* getipptes „join" derselbe Pfad. Fehlt sie, verhält sich der Befehl wie gehabt
|
|
* (interaktiv, Punkt-getrieben).
|
|
*/
|
|
autoRun?(ctx: CommandContext): CommandResult;
|
|
|
|
// ── Optionaler Tab-Feld-Zyklus (additiv; Befehle ohne diese Methoden
|
|
// verhalten sich exakt wie bisher) ──────────────────────────────────────
|
|
|
|
/**
|
|
* Geordnete Zahlenfelder des AKTUELLEN Schritts (leer/undefined = kein
|
|
* Feld-Modus). Beispiel Linie-Endschritt: [length, angle].
|
|
*/
|
|
fields?(state: CommandState): CommandField[];
|
|
|
|
/**
|
|
* Berechnet den resultierenden Modellpunkt aus den gelockten Feldern + dem
|
|
* Cursor. Ungelockte Felder folgen dem Cursor (Maus); gelockte Felder
|
|
* überschreiben. Liefert null, wenn (noch) kein Punkt bestimmbar ist.
|
|
*/
|
|
pointFromFields?(
|
|
state: CommandState,
|
|
locks: Record<string, number>,
|
|
cursor: Vec2 | null,
|
|
): Vec2 | null;
|
|
|
|
/**
|
|
* LIVE-Zahlenwerte je Feld für die aktuelle Cursor-Position (Maus-Vorschau in
|
|
* der Befehlszeile). Gelockte Felder behalten ihren Lock; ungelockte Felder
|
|
* folgen der Maus — dieser Wert zeigt, was das Feld GERADE annimmt. Liefert die
|
|
* Werte je Feld-ID; fehlt ein Feld (oder kein Cursor), zeigt die UI „—".
|
|
* Additiv: Befehle ohne diese Methode zeigen ungelockte Felder als „—".
|
|
*/
|
|
fieldValues?(
|
|
state: CommandState,
|
|
locks: Record<string, number>,
|
|
cursor: Vec2 | null,
|
|
): Record<string, number>;
|
|
}
|
|
|
|
/** Convenience-Re-Export für Befehls-Module. */
|
|
export type { Project, Vec2 } from "../model/types";
|