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.
This commit is contained in:
2026-06-30 20:52:27 +02:00
commit ca859c4aa4
157 changed files with 37921 additions and 0 deletions
+370
View File
@@ -0,0 +1,370 @@
# 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).