Files
DOSSIER-STANDALONE/docs/design/rhino-command-system.md
T
karim ca859c4aa4 Browser-BIM (cad): semantisches Modell, abgeleitete 2D/3D-Sichten, Zeichenwerkzeuge
Standalone-Browser-Port von DOSSIER. Enthaelt das semantische Modell mit
Plan-/3D-Ableitung, Zeichen- und Editierwerkzeuge, Rhino-artiges Befehlssystem,
dockbares Panel-System, Resource-Manager, DXF/.lin/.pat-Import, i18n (de/en)
sowie Projektdokumentation und Probe-Harness.
2026-06-30 20:52:27 +02:00

371 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Handoff — Rhino-artiges Befehlssystem + Modellier-Werkzeuge
> Für die Instanz, die das Befehlssystem (Tab-getriggert) und die Rhino-artigen
> Modellierfunktionen baut. Stand: 2026-06-30. Zuerst lesen: `CONVENTIONS.md`,
> `ROADMAP.md`, `HANDOVER.md`, `docs/design/drawing-tools.md`. Volle Autonomie,
> selbst bestätigen (Memory `proceed-autonomously`, `wire-dont-stub`, `prefer-agents`).
Dieses Dokument hat drei Teile:
1. **Was schon steht** (worauf du aufbaust — exakte Dateien/Typen/Actions).
2. **Rhino-Referenz** (Interaktionsmodell, das nachzubilden ist).
3. **Konkreter Bauplan für DIESE Codebase** (Architektur, Dateien, Reihenfolge).
---
## TL;DR — die Kernidee
Es gibt **kein** Befehlssystem, keine Command-Line, keinen Tab-Handler. Das baust du
greenfield. **Aber:** Das Werkzeug-System ist bereits eine saubere Pure-Function-Registry
(`src/tools/`) mit generischem Controller in `App.tsx`, und die Mutations-Schicht
(`projectSlice`) ist umfassend. Ein neues Modellier-Tool steckst du durch Hinzufügen eines
`Tool`-Objekts ein — **PlanView muss dafür nicht angefasst werden**.
Das Befehlssystem ist im Kern eine **State-Machine-Engine über prompt → pick/type →
options**, plus eine **Command-Line-UI** (Statusleiste), plus ein **Koordinaten-Parser**.
Befehle dispatchen auf bestehende Store-Actions + `setActiveTool`/`setProject`.
**Wichtigster konzeptioneller Sprung:** Die heutigen Tools haben je eine *eigene*
ad-hoc-Phasenlogik (`onClick`/`onMove`). Rhino-Feel verlangt eine **gemeinsame
Prompt/Option/Numerik-Engine**, die alle Befehle teilen. Plane das als Verallgemeinerung
des bestehenden `Tool`-Interfaces, nicht als Parallelwelt daneben (sonst zwei Eingabe-Pfade,
die divergieren).
---
# TEIL 1 — Was schon steht (Baufundament)
Stack: React 18 + TS + Vite, `three` 0.169 (nur 3D-Display). Einheiten intern **Meter**.
Eigener winziger Store (`useSyncExternalStore`, kein Redux/Zustand). Identifier englisch,
UI-Text deutsch via `t()`. Strict tsc (`noUnusedLocals` → ungenutzte Vars brechen den Build).
## 1.1 Werkzeug-System — `src/tools/`
- **`Tool`-Interface** `src/tools/types.ts:139` — reine Funktionen über internen `ToolState`
(Discriminated Union je Tool). Handler geben `[nextState, ToolResult]` zurück. Tools
schreiben NIE Plan-Primitive; sie geben `commit(project) => project` zurück.
- `ToolId` `types.ts:11`: `"select" | "wall" | "line" | "polyline" | "rect"`.
- `ToolContext` `types.ts:72`: `{ project, level, defaultCategoryCode, activeWallTypeId, activeLineStyleId }`.
- `ToolPointer` `types.ts:85`: `{ raw, snap, point (=snap?.point ?? raw), shift, ctrl, alt, button }`.
- `ToolResult` `types.ts:121`: `{ draft, commit?, done? }`. `ToolDraft` `types.ts:109`:
`{ preview: DraftShape[], vertices: Vec2[], snap?, hud?: {at,text} }`.
- **Registry** `src/tools/tools.ts:375`: `TOOLS: Record<ToolId,Tool>`, `getTool(id)` `:384`,
`TOOL_ORDER` `:389`. Implementiert: select (Platzhalter), wall, line, polyline, rect.
- `uniqueId(prefix)` `types.ts:188` — ID-Generator.
## 1.2 Controller / Verdrahtung — alles in `App.tsx` (NICHT in den Tool-Dateien)
- Aktives Tool: `const [activeTool,setActiveTool]=useState<ToolId>("select")` `App.tsx:197`.
- Laufender Zustand in **Ref** `toolStateRef` `App.tsx:205` (kein Re-Render je Mausschritt).
Live-Vorschau `const [draft,setDraft]` `App.tsx:206`.
- **Controller** `runToolStep(kind,raw,pxPerMeter,mods)` `App.tsx:350`: baut `ToolContext`
(`toolCtx` `:294`), snappt via `snapFor` `:322` (→ `computeSnap`), baut `ToolPointer`,
ruft `tool.onClick/onMove`, speichert State in Ref, `applyToolResult` `:339` wendet
draft/commit/done an. `toolHandlers` `App.tsx:419` verbindet PlanView↔Controller.
- Tool-Tasten `App.tsx:448`: Esc=Abbruch/zurück-zu-select, Enter=Commit-Geste, Backspace=Punkt zurück.
## 1.3 Semantisches Modell — `src/model/types.ts`
- `type Element = Wall | Door | Drawing2D` `:251`. `Vec2={x,y}`. **Kein Slab/Stair-Typ.**
- `Project` `:254`: `{ ..., wallTypes[], drawingLevels[], layers[], walls[], doors[], drawings2d[] }`.
- `Wall` `:150`: `{ id,type:"wall", floorId, categoryCode, start, end, wallTypeId, height, color? }`
(Mittellinie + mehrschichtiger `WallType`).
- **Plan-Primitive `Drawing2DGeom`** `:200` — die Geom-Typen existieren bereits ALLE:
`line | polyline | rect | circle | arc | text`. Aber Tools erzeugen heute nur line/polyline/rect,
und `drawingVertices` (Grips) kennt nur diese drei. **circle/arc/text sind im Typ da, aber
nicht durchgängig gerendert/editierbar** — Lücke, kein Neubau nötig.
- `Drawing2D` `:209`: `{ id,type:"drawing2d", levelId, categoryCode, geom, lineStyleId?, hatchId?, color?, fillColor?, weightMm? }`.
## 1.4 Geometrie — `src/model/geometry.ts`
`sub,add,scale,len,normalize`; `leftNormal(a)={x:-a.y,y:a.x}` `:17` (Wand-Normale-Konvention);
`cross`, `lineIntersect(a,da,b,db)`, `along`, `wallBand`, `wallCorners` `:70`, `clippedBand` `:87`.
`src/model/joins.ts`: `computeJoins(project,walls)` `:44` (nur L-Ecken gehrt; T/X eckig).
**Für Offset/Trim/Fillet (Rhino-Kern) gibt es NOCH KEIN 2D-Geometrie-Kernel** — Kurven/Kurven-
Schnitt, Polylinien-Offset usw. musst du ergänzen (siehe Bauplan §3.4).
## 1.5 Store — `src/state/`
- `createStore` `store.ts:51` über `useSyncExternalStore`. **Actions leben IM State**
(`useStore(s=>s.action)`, referenzstabil). `RootState = Project & Selection & View & Layout`
`appStore.ts:21`. Exports `useStore`, `getState`, `setState`.
- **projectSlice**: `project` + `setProject(next|(p)=>p)`. Mutationen u.a. `addFloor`,
`addCategory`, `setElementColor/Weight/Fill`, `resizeElement`, `moveGripOf`, `moveElementByOf`,
`moveEdgeOf`, `commitTransformOn`. **Es gibt keine generische „addWall/addDrawing2d"-Action**
Tools committen via `setProject`. (Beim Befehlssystem ggf. saubere Actions ergänzen.)
- **Aktive Zeichenebene + aktive Kategorie liegen im viewSlice**, NICHT in selection:
`activeLevelId` `viewSlice.ts:46`/`setActiveLevelId`, `activeCategoryCode` `:42`/`setActiveCategoryCode`.
- selectionSlice: `selectedWallIds[]`, `selectedDrawingId` + Setter/`clearSelection`.
## 1.6 Views, Eingabe, Koordinaten — `src/plan/PlanView.tsx` (SVG-Vektor)
- Modell → `Plan`-Primitive via `generatePlan` `src/plan/generatePlan.ts:203`. `Primitive` =
`polygon|line|arc` (polygons tragen `wallId`/`drawingId` für Hit-Test).
- **Transform (entscheidend):** `PX_PER_M=90` `:20`; `toScreen(p)={x:p.x*90,y:-p.y*90}` `:31`
(fixer Welt-Ursprung 0,0; Y flippt). Invers `viewToModel` `:440`. SVG `viewBox`=State `view`;
Pan/Zoom ändern nur `view`, nie das Modell↔Screen-Mapping.
- **`rawModelAt(clientX,clientY)`** `:446` = aktuelle Mauswelt-Position in Meter (der Eine-Aufruf,
den ein Tool/Befehl braucht). `currentPxPerMeter()` `:488`.
- **Pointer-Events** alle am `<svg>` `:910`: down `:553`, move `:625`, up `:715`, wheel `:820`,
dblclick `:847`, contextmenu `:857`. Schema: Mitte=Pan, Links=Select/Marquee/Tool, Rechts=Menü.
Bei `toolActive` `:268` routen Links-Events zu `toolHandlers`. **PlanView meldet bereits
`(rawModelAt, currentPxPerMeter, toolMods)` nach oben** — neue Tools brauchen hier NICHTS.
- **Snapping** `src/tools/snapping.ts`: `computeSnap(input)` `:88` — endpoint/midpoint/intersection/
onEdge/grid/ortho mit Prioritätstabelle. Wird in App (`snapFor`) konsumiert, nicht in PlanView.
`applyAngleConstraint` für Ortho. `SnapSettings`/`DEFAULT_SNAP` in `tools/types.ts:36/55`.
- 3D `src/viewport/Viewport3D.tsx` (three.js, Raycaster): nur Anzeige+Auswahl, **keine
Zeichenwerkzeuge**. 3D-Authoring = eigene spätere Phase (Raycast auf Arbeitsebene).
## 1.7 Tastatur / globale Eingabe — **kein Dispatch-System**
- `main.tsx:38` globaler `contextmenu`→preventDefault; `:42` blockt Ctrl/Cmd+A außerhalb Inputs;
`isTextEntry(el)` `:24`.
- App-useEffects mit `window.addEventListener("keydown")`: Tool-Tasten `:448`, Delete `:604`,
Transform-Shortcuts m/s/d + u/i/o/p `:637`. **Jeder Guard wiederholt inline den
INPUT/TEXTAREA/contentEditable-Check** — es gibt keine geteilte Keymap. Dein Tab-Handler +
Command-Input kommt als neuer globaler `keydown` dazu (siehe §3.2).
## 1.8 UI-Shell + i18n
- `App.tsx` (~2200 Z., enthält noch ToolController/Grips/Transform). JSX `:1043`: TopBar → body
(Dock links, Content-View-Router, TransformBar, Dock rechts, Floating) → StatusBar →
ResourceManager → ContextMenu → InlineEditor. Panel-Daten via `PanelHostContext` (`baseHost`
`App.tsx:729`, Typ `host.ts`).
- `StatusBar.tsx` — Footer: links `hint` (Tool-Hinweis), rechts X/Y, Einheit, Massstab 1:N, Zoom,
aktives Geschoss, aktive Ebene. **Bester Ort für die Command-Line** (Rhino hat sie klassisch unten).
- **i18n** `src/i18n/`: `t(key,params?)` `index.ts:67`, `useT()` `:84`. Flaches `as const`-Dict,
Punkt-Namespaces (`tool.*`,`snap.*`,`transform.*`,`status.*`…). `de.ts` (Quelle, ~309 Keys) +
`en.ts`; `TranslationKey=keyof typeof de` erzwingt Parität. **Neue Keys IMMER in beide Dateien.**
Keine hartcodierten JSX-Strings.
## 1.9 Verifizieren
- `npx tsc -b` · `npm run build` · Dev `npm run dev` (Vite 5173, `host:true`).
- Screenshot `node scripts/probe.mjs``scripts/probe.png` (Puppeteer headless, `deviceScaleFactor:2`,
URL via `PROBE_URL`). Viele task-Probes existieren (`probe-tools.mjs`, `probe-line.mjs`,
`probe-transform.mjs` …) — gute Vorlagen, um Tools/Befehle programmatisch zu treiben.
**Screenshot ansehen + Geometrie prüfen**, nicht nur „kompiliert".
---
# TEIL 2 — Rhino-Referenz (das Interaktionsmodell)
## 2.1 Die Command-Line ist das Rückgrat
**Alles ist ein Befehl**, und die Command-Line **hört immer zu**: Tastenanschläge gehen an die
Command-Line, wenn sie nicht von einem Feld konsumiert werden. Kein „Tool aktiv vs. Eingabe aktiv".
Die Zeile hat gleichzeitig drei Rollen: **Eingabe** (Befehl/Wert tippen), **Prompt**
(„Start of line", „Next point"), **Optionen** (eckige, klickbare Inline-Optionen).
## 2.2 Befehl aufrufen
- Namen tippen, z. B. `Line`. **Präfix-Autocomplete** (case-insensitiv): `L``Li``Lin` zeigt
Kandidatenliste mit Best-Match. **Tab/Pfeile** akzeptieren Vorschlag, **Enter/Leertaste** führt aus.
- **Aliase**: nutzerdefinierte Kürzel → Makro (z. B. `L``!_Line`, `cp``!_Copy`). Werden VOR
Autocomplete gematcht. (Minimal: Einzelbuchstabe→Befehl.)
## 2.3 Enter / Leertaste / Rechtsklick (leicht falsch gemacht)
- **Enter = Leertaste** in der Command-Line. Beide: Befehl ausführen / Default akzeptieren /
mehrteiligen Befehl **beenden** / bei **leerer** Zeile **letzten Befehl wiederholen**.
- **Rechtsklick im Viewport = Enter.** Also: Rechtsklick beendet Polyline UND wiederholt bei
leerer Zeile den letzten Befehl. → `lastCommand` speichern, bei Leer-Enter/Rechtsklick neu starten.
## 2.4 Inline-Optionen (klickbare Klammern)
```
Start of line ( BothSides=No Chamfer Mode=Distance ):
```
- Jede Option **klickbar UND tippbar** (genug Buchstaben zur Eindeutigkeit + Enter).
- **Toggle** `Name=Value` flippt beim Klick. **Value**-Option fragt Unterwert ab. **Action**-Option
(ohne `=`) verzweigt sofort.
- Optionen sind **innerhalb des Befehls persistent**, viele **über Aufrufe hinweg** (letzte
Offset-Distanz, Array-Anzahl, Fillet-Radius merken). **Zuletzt benutzte Optionswerte je Befehl
persistieren** — Nutzer erwarten das.
## 2.5 Sub-Prompts = State-Machine
Befehle laufen Prompts ab. `Line`: „Start of line:" → Punkt → „End of line:" → Punkt → fertig.
`Polyline`: „Start" → „Next point ( Close Undo ):" → … → **Enter** beendet. Prompt-Text ist
sichtbar und lehrreich („Next point. Press Enter when done") — literal nachbilden.
## 2.6 Transparente/verschachtelbare Befehle
Manche Befehle (Zoom/Pan, Osnap-Toggle, alles mit `'`-Präfix) laufen **innerhalb** eines anderen,
ohne ihn abzubrechen, und kehren zum Original-Prompt zurück. → Command-Runner braucht einen **Stack**.
## 2.7 Koordinaten- & Numerik-Eingabe (Herz der Präzision)
| Eingabe | Bedeutung |
|---|---|
| `5,3` / `5,3,2` | absolut X,Y(,Z) |
| `r5,3` | **relativ** zum letzten Punkt (das `r`-Idiom) |
| `<45` | Winkel-Constraint auf 45°, dann Maus/Distanz |
| `5<45` | **polar**: Distanz 5 unter 45° vom letzten Punkt |
| Zahl tippen während Drag | **Distanz-Lock**: Richtung per Maus, Länge per Zahl+Enter (meistgenutzte Geste) |
| Zahl + **Tab** | Lock umschalten (Länge fix → Winkel folgt Maus, oder umgekehrt) |
Das Feld parst **kontextabhängig**: Befehlsname / Optionsbuchstabe / Koordinate / nackte Zahl —
je nach Befehlszustand. Das Live-Tool muss **einen primären Skalar** (Länge/Radius/Distanz)
exponieren, an den eine getippte Zahl bindet.
## 2.8 Osnaps + Ortho + Gumball
- **Osnaps** (persistente Toggles): End, Mid, Cen, Int, Perp, Near, Quad, Tan, Point. Pro Mausschritt
gegen nahe Geometrie geprüft (Pixel-Toleranz), Marker+Label am Cursor; liefert **exakte
Modellkoordinate** (nie Roh-Maus, wenn Snap aktiv). One-Shot-Osnap überschreibt für den nächsten Pick.
- **Ortho** (F8): Winkelraster (90°/konfigurierbar), **Shift** togglet temporär. **Grid Snap** (F9).
**SmartTrack**: temporäre Hilfslinien aus zuletzt gehoverten Punkten.
- **Gumball**: On-Object-Widget (Pfeile=Move, Bögen=Rotate, Handles=Scale); Handle klicken →
Zahl tippen für exakten Transform. Direkt-Manipulations-Gegenstück zu getippten Befehlen.
> Präzisionsmodell = **(Snap ODER getippte Koordinate) × (Ortho/Winkel-Constraint) ×
> (Distanz-Constraint)**, in EINEM Pick komponierbar.
## 2.9 Auswahl-Modell (links/rechts-Regel exakt)
- Klick = wählen; Shift+Klick add; Ctrl+Klick remove.
- **Links→rechts = Window** (nur voll umschlossene; **durchgezogenes** Rechteck).
- **Rechts→links = Crossing** (auch berührte; **gestricheltes** Rechteck). Richtung bestimmt
Modus — starke Konvention, exakt nachbilden.
- **SelLast** (zuletzt erzeugte/gewählte erneut wählen) ist enorm nützlich („erzeugen, dann sofort
bewegen"). Min. `SelLast`, `SelAll`, `SelNone`, `Invert`.
## 2.10 Befehls-Prompt-Sequenzen (Kurz)
2D: **Line** (2 Pkt) · **Polyline** (Close/Undo, Enter beendet) · **Rectangle** (Ecke+Ecke, oder
Breite/Höhe tippen; 3Point/Center) · **Circle** (Center+Radius; 2P/3P/Tan) · **Arc** (Center-Start-End /
3Point) · **Offset** (Kurve wählen → Seite klicken/Distanz tippen; Distanz persistent) ·
**Fillet/Chamfer** (Kurve1→Kurve2, Radius/Distances persistent) · **Trim** (Schneider wählen→Enter→
wegzuschneidendes Stück klicken) · **Split** · **Extend** · **Join** · **Explode** ·
**Move/Copy/Rotate/Scale/Mirror** (Auswahl→Basispunkt→Ziel; Copy-Option) · **ArrayRect/ArrayPolar** ·
**Group/Ungroup**.
3D (braucht CSG, später): **ExtrudeCrv** (geschlossene Kurve→Solid, Cap) · **Box** · **Boolean
Union/Difference/Intersection** · **Cap** · **Gumball-Face-Drag = PushPull** · Loft/Sweep/Revolve.
---
# TEIL 3 — Bauplan für DIESE Codebase
> Ziel: nutzbarer 2D-Architektur-Drafter mit Rhino-Feel, dann einfaches Massing. Halte das
> ROADMAP-Prinzip: **ein semantisches Modell → Sichten abgeleitet**; Extrusionshöhe ist eine
> Eigenschaft, nie eingebackene Geometrie.
## 3.0 Kuratierungs-Prinzip (WICHTIG — Nutzer-Vorgabe)
**NICHT den ganzen Rhino-Katalog stumpf portieren.** Wir bauen ein **Wohnbau-BIM**, keinen
NURBS-Allzweck-Modeller. Nimm nur, was dem Wohnbau-Workflow dient; lass den Rest weg, bis er
konkret gebraucht wird. Faustregel: *Brauche ich das, um ein Einfamilienhaus zu zeichnen und
daraus Pläne zu ziehen?* Wenn nein → weglassen.
**Bewusst WEGLASSEN (vorerst):** Loft / Sweep1+2 / Revolve (Sonderformen, kaum Wohnbau) ·
freie NURBS-Kurven (`Curve`/`InterpCrv` Grad>1, Deformable, FromFoci) · Ellipse · Tangent/
Bisector/4Point-Linienvarianten · SmartTrack (nett, nicht kritisch) · der volle `Sel*`-Zoo
(nur SelLast/SelAll/SelNone/Invert) · Knot/Vertex/Tan-Osnaps. Alle leicht später additiv
nachrüstbar — kein Grund, sie jetzt mitzuschleppen.
**Booleans sind KEIN „nice to have später"** — sie werden gebraucht, **sobald Tür/Fenster als
echte 3D-Öffnung** kommen (heute schneidet `Door` nur eine Plan-Lücke, kein 3D-Boolean, siehe
HANDOVER). Darum: CSG/Booleans an die **Tür/Fenster-Phase koppeln** und dann reinnehmen — nicht
ans Ende schieben. ABER (das ist der „nicht stumpf"-Teil):
> Für **rechteckige** Öffnungen in extrudierten Wänden braucht es **keinen allgemeinen
> Boolean-Kernel**. Eine analytische **Wand-minus-Box-Subtraktion** (Öffnung als parametrische
> Aussparung im Wand-Solid) ist einfacher, robuster und für 95 % Wohnbau ausreichend. Den
> allgemeinen CSG-Boolean (`rhino3dm`) erst ziehen, wenn schräge/runde/verschnittene Fälle
> wirklich auftreten. Also: **Öffnungen zuerst analytisch, allgemeine Booleans erst bei Bedarf.**
## 3.1 Leitentscheidung: Engine verallgemeinern, nicht parallel bauen
Baue eine gemeinsame **Command-Engine**, die das bestehende `Tool`-Interface erweitert/ablöst,
sodass es **einen** Eingabepfad gibt (Maus + Tastatur + Command-Line speisen dieselbe Maschine).
Konkret: ein `Command`-Modell, das je Schritt einen **Prompt** (Text), erwartete **Eingabearten**
(Punkt | Zahl | Option | Auswahl) und **Optionen** beschreibt. Die heutigen Tools werden zu
Befehlen dieser Engine (wall/line/polyline/rect lassen sich 1:1 portieren — ihre Phasenlogik ist
schon eine Mini-State-Machine).
**Warum nicht das alte Tool-Interface unangetastet lassen und Command-Line nur draufsetzen?**
Weil die Command-Line getippte Koordinaten/Optionen in denselben Schritt einspeisen muss, in dem
die Maus pickt. Zwei getrennte Pfade divergieren garantiert (Snapping, Constraints, HUD doppelt).
## 3.2 Neue Dateien (Vorschlag)
- `src/commands/engine.ts` — Command-Runner: aktiver Befehl, Prompt-Stack (für transparente
Befehle §2.6), `lastCommand`-Wiederholung, Routing von Maus-Pick / getippter Eingabe / Option-Klick
in den aktuellen Schritt. Hält `CommandState`.
- `src/commands/types.ts``Command`-Interface (Verallgemeinerung von `Tool`): Schritte mit
`prompt: TranslationKey`, `accepts: ("point"|"number"|"option"|"selection")[]`, `options: CmdOption[]`,
`onInput(state,input,ctx): [state, CommandResult]`. `CommandResult` wie `ToolResult` (+`commit`).
- `src/commands/parseInput.ts` — Koordinaten-Parser (§2.7): `5,3` · `r5,3` · `5<45` · `<45` ·
nackte Zahl (Distanz-Lock) · Optionsbuchstabe. Liefert eine Discriminated Union, die die Engine
in einen Modellpunkt/Constraint auflöst (mit `lastPoint` für `r`/polar).
- `src/commands/registry.ts``COMMANDS: Record<string,Command>` + Aliase + Autocomplete (Präfix).
- `src/ui/CommandLine.tsx` — die Command-Line-UI **in/über der Statusleiste** (`StatusBar.tsx`):
zeigt Prompt + klickbare Optionen + Texteingabe; Autocomplete-Dropdown. Tab fokussiert sie.
- (später) `src/geometry/kernel2d.ts` — 2D-Kernel für Offset/Trim/Fillet/Schnitt (§3.4).
- (viel später) `src/geometry/solid3d.ts` o. `rhino3dm`-Anbindung für Massing/Booleans (§3.5).
## 3.3 Verdrahtung (minimal-invasiv)
- **Globaler Tab-Handler**: neuer `window.keydown` in App (gleicher Guard wie `App.tsx:448`
INPUT/TEXTAREA/contentEditable überspringen). Tab → Command-Line fokussieren/öffnen. Jeder
getippte Buchstabe ohne aktives Tool startet den Befehlsmodus (Rhino „hört immer zu" — optional
in Phase 2; Phase 1 reicht Tab).
- **Command-Line → Engine → Store**: Befehle dispatchen auf `setActiveTool` (für tool-artige) bzw.
direkt auf Store-Actions / `setProject`. Nutze `getState()/setState()` (referenzstabil) aus
`appStore.ts`.
- **Pick-Eingabe**: die Engine konsumiert dieselben `(rawModelAt, currentPxPerMeter, toolMods)`,
die PlanView schon hochmeldet (`ToolHandlers`). `computeSnap` für Punktfang wiederverwenden.
→ PlanView braucht im Idealfall **keine Änderung** (höchstens: Window/Crossing-Marquee-Visual
durchgezogen vs. gestrichelt nach Drag-Richtung, §2.9 — heute evtl. nur ein Modus).
- **Prompt/HUD**: Prompt-Text in die Statusleiste (`StatusBar` `hint` existiert schon). Distanz/
Winkel-HUD am Cursor existiert in `ToolDraft.hud`.
## 3.4 Reihenfolge (Tiers — strikt 2D zuerst)
**Tier 0 — Substrat (VOR jedem Befehl; das ist der „Feel"):**
1. Command-Runner + Command-Line-UI (Prompt → pick/type → Optionen; Enter/Space/Rechtsklick =
bestätigen/beenden/wiederholen; `lastCommand`).
2. Koordinaten-Parser (`x,y` · `rdx,dy` · `dist<angle` · nackte-Zahl-Lock).
3. Osnaps (End/Mid/Cen/Int/Perp/Near) — `computeSnap` ist da, ggf. Cen/Perp/Near ergänzen.
4. Ortho (90°/45°, Shift-Toggle) + Grid-Snap — teils vorhanden (`applyAngleConstraint`).
5. Auswahl: Klick, Shift/Ctrl add/remove, **Window vs. Crossing** (durchgezogen/gestrichelt,
links/rechts-Regel).
**Tier 1 — 2D-Pflicht (reines SVG/2D), grobe Baufolge:**
6. **Line** (validiert die ganze pick/snap/constrain-Schleife) → 7. **Polyline** (Close/Undo) →
8. **Rectangle** (Ecke + Center/3Point) → 9. **Circle** (Center+Radius). Diese vier portieren die
heutigen Tools auf die Engine + numerische Eingabe.
10. **Move** → 11. **Copy** (wiederholend) → 12. **Offset** (persistente Distanz — DAS Architektur-
Primitiv) → 13. **Trim** + **Split** → 14. **Join** + **Explode**. **Undo/Redo** durchgängig
annehmen (heute? — prüfen; ggf. Command-History/Undo-Stack im Store ergänzen).
**Tier 2 — 2D stark nützlich:** Rotate/Scale/Mirror (Copy-Option) · Fillet/Chamfer · Arc ·
Extend · ArrayRect/ArrayPolar · Group/Ungroup · Gumball(2D) · Sel*-Helfer (min. SelLast).
**Tier 3 — Massing + Öffnungen (an Tür/Fenster-Phase gekoppelt):**
- **Öffnungen zuerst analytisch:** Tür/Fenster als parametrische Aussparung im Wand-Solid
(Wand-Extrude minus Öffnungs-Box) — KEIN allgemeiner Boolean-Kernel nötig (§3.0). Das ist der
kritische, roadmap-markierte 🔴-Teil (echte 3D-Öffnung statt nur Plan-Lücke) und kommt MIT
Tür/Fenster, nicht danach.
- **Massing-Befehle:** ExtrudeCrv (geschlossene Plan-Kurve → gecapptes Solid) → Box →
Gumball-Face-Drag-PushPull.
- **Allgemeine Booleans** (Union/Difference/Intersection) **erst bei Bedarf** (schräge/runde/
verschnittene Fälle): **kein eigener Kernel — `rhino3dm` (WASM-openNURBS)** als `src/io/`-Schicht
(Roadmap-Entscheid, HANDOVER). Bis dahin reicht die analytische Subtraktion.
- **Weggelassen:** Loft/Sweep/Revolve/OffsetSrf (§3.0 — Sonderformen, kaum Wohnbau).
## 3.5 Was sauber 2D ist vs. was hart ist
- **Sauber SVG/2D:** Line, Polyline, Rect, Circle, Arc, Move/Copy/Rotate/Scale/Mirror, Array, Group,
Control-Point-Edit, Gumball(2D). Affine Transforms + Kurven-Schnitt.
- **Echte Arbeit (2D-Kernel nötig):** **Offset, Trim, Fillet** brauchen kompetenten Kurven-Schnitt
und Polylinien-Offset — dafür Zeit einplanen (`src/geometry/kernel2d.ts`).
- **Braucht 3D/CSG:** Extrude, Box, Boolean*, Cap, OffsetSrf, Loft/Sweep/Revolve, Face-Drag. Booleans
sind das Korrektheits-Zentrum → `rhino3dm`.
## 3.6 Gotchas (aus Rhino-Verhalten + dieser Codebase)
- **Command-Line hört immer zu** — Tasten global routen, aber die `isTextEntry`-Disziplin
(`main.tsx:24`) + die Native-App-Regeln (kein Ctrl+A/keine Textauswahl, CONVENTIONS.md) wahren.
- **Zuletzt benutzte Optionswerte je Befehl persistieren** (Offset-Distanz, Array-Anzahl, Fillet-Radius).
- **Enter = Rechtsklick = Wiederholen/Bestätigen/Mehrteiliges-Beenden** — alle drei auf EIN Signal.
- **Distanz-Lock:** Live-Befehl muss EINEN primären Skalar exponieren, an den eine getippte Zahl bindet.
- **Window vs. Crossing** über Drag-Richtung + durchgezogen/gestrichelt — nicht global ein Modus.
- **Osnap liefert exakte Modellkoordinate** — nie Roh-Maus, wenn Snap aktiv (`ToolPointer.point`).
- **Strict tsc** (`noUnusedLocals`) — ungenutzte Vars/Parameter brechen `npm run build`.
- **i18n**: jeder sichtbare String über `t('key')`, Keys in `de.ts` UND `en.ts` (Parität erzwungen).
- **Keine generische addWall/addDrawing2d-Action** — entweder via `setProject` committen (wie heute)
oder beim Refactor saubere Actions im `projectSlice` ergänzen (besser für Undo/Redo).
- **App.tsx ist bereits ~2200 Z.** (God-Component-Kritik in HANDOVER). Lege Command-Engine in
`src/commands/`, halte App-Verdrahtung dünn (nur Tab-Handler + CommandLine-Mount + Dispatch-Brücke).
## 3.7 Verifikations-Drehbuch
Pro Tier eine Probe (Vorlage: `scripts/probe-tools.mjs`/`probe-transform.mjs`): Befehl per Command-
Line tippen → Punkte/Werte tippen → Screenshot → Geometrie visuell prüfen. Tier 0 zuerst headless
treiben (Tab → „line" → „0,0" Enter → „r3,0" Enter → Linie im PNG sichtbar). `npx tsc -b` +
`npm run build` grün halten. **Screenshot ansehen**, nicht nur Kompilat vertrauen (Memory `wire-dont-stub`).
---
## Anhang — Minimaler erster Meilenstein (konkret)
1. `src/commands/types.ts` + `engine.ts` + `parseInput.ts` (Tier 0.1/0.2).
2. `src/ui/CommandLine.tsx`, in `StatusBar` gemountet; Tab-Handler in App.
3. `Line` als erster Command (portiert `lineTool`), inkl. getippter `0,0` / `r3,0` / `3<45`.
4. Probe `scripts/probe-command-line.mjs`: Tab→line→zwei getippte Koordinaten→PNG prüfen.
5. Dann Polyline/Rect/Circle, danach Move/Copy/Offset.
Damit steht der Rhino-Feel-Kern, und jeder weitere Befehl ist additiv (neues `Command`-Objekt in
`registry.ts`, keine PlanView-/App-Änderung).