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

25 KiB
Raw Blame History

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.mjsscripts/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): LLiLin 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.tsCommand-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.tsCOMMANDS: 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).