2D-Plan-Renderer auf WebGL2 (GPU) + akkumulierter Funktionsstand

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).
This commit is contained in:
2026-07-02 00:12:39 +02:00
parent 7b3b597abc
commit 8fd8987b70
184 changed files with 29421 additions and 669 deletions
+363 -1
View File
@@ -41,7 +41,302 @@ Code/Doku noch in der Git-Historie.
vor dem Edit frisch lesen (der „modified since read"-Guard verhindert blindes
Überschreiben).
## >>> STAND 2026-06-30 (Fortsetzung, spät) — ZUERST lesen <<<
## >>> AUFGABE FÜR NEUE INSTANZ: 2D-Plan auf WebGL-GPU-Renderer (ZUERST) <<<
**Warum:** Tauri-Linux-Webview = **WebKitGTK**, das SVG/2D auf der **CPU (Cairo)** rastert.
User-GPU (AMD RX 7800 XT) langweilt sich; bei **144 Hz** (~6,9 ms/Frame) ruckelt Pan/Zoom/
**Objekt-Bewegen** sichtbar. three.js/WebGL (3D) läuft GPU-beschleunigt → NUR der 2D-SVG-Pfad
ist CPU-gebunden. Entscheidung mit User: 2D-Plan **von SVG auf WebGL2 (GPU)** umbauen.
**Architektur (mit User abgestimmt):**
- **Hybrid:** WebGL2-Canvas rendert die schwere Geometrie (Poché-Polygone, Linien, Schraffuren,
Kreise/Bögen als Segmente). **Dünne SVG-Ebene DARÜBER** behält Text (Raumstempel/Labels),
Griffe, Snap-Marker, Werkzeug-Vorschau (wenige Elemente → billig, DOM-Hit-Test + scharfer Text bleibt).
- **Raw WebGL2, KEINE neue npm-Dependency** (User will nicht „abhängiger" werden; nativ).
- **Geometrie pro Plan EINMAL tessellieren** (Cache), Pan/Zoom = nur Transform-Matrix-Uniform →
keine Re-Tessellation, GPU-buttrig. Objekt-Bewegen: Plan ändert sich → re-tessellieren (leichter als SVG-Reconcile+Cairo).
- **Linienbreite konstant in Screen-px** (wie heutiges `non-scaling-stroke`): im Vertex-Shader
expandieren (Position world → clip, dann Normalen-Offset in clip via Viewport-px). Miter an Ecken
später; Spike: Quads pro Segment.
- **SVG-Renderer als Fallback hinter einem Flag behalten**, bis GL-Parität erreicht → kein Risiko.
- Vorgehen: **erst Spike** (Linien+gefüllte Polygone + GPU-Pan/Zoom), Glätte bei 144 Hz messen
(dev :5187 läuft; `tauri:dev`-Fenster läuft), DANN Schraffur/Feinschliff/Text-Overlay/Hit-Test.
**Fixpunkte im Code:**
- `src/plan/PlanView.tsx` (~2500 Z.): `toScreen()` = Modell-m → viewBox-Einheiten, FIXER Ursprung,
`PX_PER_M=90`; `view={x,y,w,h}` treibt `<svg viewBox>`; Pan = setView, Zoom = setView(onWheel).
BEREITS optimiert: `primitiveEls`/`drawingRunEls` `useMemo` (Pan/Zoom 312→0 Re-Renders) +
rAF-Coalescing beim Objekt-Drag (`flushDragMove`/`enqueueDragMove`, 40→1 Update/Frame). Diese
bleiben nützlich; der GL-Renderer ersetzt die SVG-Primitiv-Ebene, NICHT die Interaktions-Logik/Griffe.
- `src/plan/generatePlan.ts`: `export type Primitive` (Z.133) — kinds: `polygon`{pts,fill,stroke,
strokeWidthMm,hatch,*Id}, `line`{a,b,weightMm?,strokeWidthMm?,dash?,color,cls,greyed,drawingId?},
`arc`{center,r,…}, `text`{at,doc,extraLines,basePt,color} (Text → SVG-Overlay, NICHT WebGL).
- Koordinaten/Breiten-Logik im SVG-Renderer: `PrimitiveShape` (~Z.2280) + `DrawingRunShape` — dort
steht, wie Haarlinie/mm/`paperScale`/`non-scaling-stroke` die Strichstärke bestimmen (für GL nachbilden).
- Hit-Test/Auswahl/Griffe laufen weiter über die bestehenden PlanView-Pfade (modellbasiert); GL ist nur Anzeige.
- **Vektor-PDF/-Export bleibt** (kommt aus `generatePlan`/DXF, nicht aus der Anzeigefläche) — NICHT anfassen.
**Gates:** `npx tsc -b` 0, `npm run build` grün, Trace-Scan sauber (keine KI-Spuren, COMMIT-REGEL oben).
Kein Commit ohne Ansage. Verifizieren wie üblich per Puppeteer gegen :5187 (Render korrekt + Glätte messen).
**GESAMT-TODO-LISTE (Stand 2026-07-01 Nacht) — im Hinterkopf behalten:**
ERLEDIGT+verifiziert: U/I/O/P-Kopiermodi · Backlog#1 Transform→CommandLine · Backlog#4 3D-Zeichnen +
Engine-Confirm-Bugfix · Raumstempel#2 (Feld-Modell) · Plan-Perf (Memo + rAF-Coalesce) · Tauri-M1-PoC
(geometry-Crate 4/4 + Parität + `tauri:dev`-Boot). OFFEN: **(A) 2D-WebGL-Renderer [DIESE AUFGABE]** ·
(B) Tauri-M1 Rest: Compute-Boundary in Render-Pfad einhängen (generatePlan sync → precompute+cache) +
nächste Ops (kernel2d→DXF/DWG→detectRooms) · (C) three.js→wgpu (M2, nach 2D-WebGL) · Parametric Walls
Phase B (andere Instanz) · Wand-Referenzlinien-Option unter Bezugspunkt · Viewport/Editier-Welle
(Treppen-Griffe, Öffnung-entlang-Wand, 2D auf Zeichenebenen, runde Linienenden, schattiert-Politur) ·
Smart-Join/Split + nahtfreie Wandverschneidung · Text/Stempel-Annotation (Frame+Rotation) · Welle B
(3D-Schnitt+Stencil-Capping) · Welle C (HLR verdrahten) · Overrides/Ausschnitte/Multi-Page-Layouts.
## >>> STAND 2026-07-01 (Tauri-PoC — App-frei, gelandet) <<<
**Tauri v2 + Rust-Compute-Boundary — Proof-of-Concept steht (3 App-freie Agents).**
Alle Gates grün, nichts an `App.tsx`/`src/model/` angefasst (nur NEUE Dateien +
additive Config). Ref: `docs/design/tauri-migration-plan.md` / `tauri-architecture.md`.
- **`src-tauri/` (Rust, neu):** Cargo-**Workspace** aus zwei Crates:
- `src-tauri/geometry/`**serde-only** Lib, Port von `computeJoins` (aus
`src/model/joins.ts`) inkl. Vektor-Helfer. Input flach: `WallInput{ id,start,end,
thickness,referenceOffset }`; Output `WallCuts{ wallId,startCut,endCut }` (Line=
point+dir). **`cargo test` grün: 4/4** (L-Ecke=geteilte Gehrung, freies Ende,
T-Stoss, kollinear → alle korrekt). Braucht KEIN webkit → hier testbar.
- `src-tauri/` App-Crate (`cad-tauri`) — Tauri-v2-Shell (`lib.rs` mit
`#[tauri::command] compute_joins` + `run()`, `main.rs`, `build.rs`,
`tauri.conf.json` devUrl **:5187**, `icons/icon.png`). **`cargo check` GRÜN** —
wider Erwarten NICHT von webkit blockiert: tauri v2 bindet `webkit2gtk-4.1`
(vorhanden); nur `-6.0` fehlte. `src-tauri/target` + `gen/` sind Build-Artefakte
(gitignored).
- **`src/compute/index.ts` (neu, Boundary):** einzige Stelle für schwere Ops.
`computeJoins(project,walls)` flacht ab → `invoke("compute_joins")` (geguardeter
dynamischer Import von `@tauri-apps/api/core`, Build bleibt grün OHNE das Paket)
→ bei null/Fehler `console.warn` + TS-Fallback (`joins.ts`). Zusätzlich
Durchreicher `detectRooms` (roomBoundary.ts) + `parseShapeFromDwg`(=`parseDwg`);
`computeKernel2D` bewusst ausgelassen (kein Einzel-Entry, ~30 Primitive).
- **`package.json`/`vite.config.ts`:** `@tauri-apps/cli`+`api` (v2) installiert,
Scripts `tauri`/`tauri:dev`/`tauri:build`; vite Port 5187 `strictPort`,
`clearScreen:false`, `envPrefix` — rein additiv.
**NÄCHSTE SCHRITTE (Tauri):** (1) **Echte TS↔Rust-Numerik-Parität** end-to-end prüfen
— erst sinnvoll, wenn die Boundary in einen Caller verdrahtet ist (aktuell bewusst
NICHT verdrahtet, weil das `generatePlan`/`App` async machen würde). (2) `computeJoins`
in den Wand-Render-Pfad einhängen (async-Welle). (3) `tauri:dev` end-to-end auf einer
Maschine MIT `webkit2gtk` (hier nur `cargo check`, kein Fenster-Boot getestet).
(4) Nächste Ops laut Plan: `kernel2d` → DXF/DWG → `detectRooms`.
## >>> STAND 2026-07-01 (Parametric Walls Phase A — ISOLIERT) <<<
**Neue Instanz (Opus) — Parallelisierung mit Agents (Sonnet):**
- **Branch:** `feature/parametric-walls` (isoliert, keine App.tsx-Änderungen)
- **Phase A FERTIG (alle Agents grün):**
- **Agent 1:** Types (`ParametricWall`, 5 `ParametricRule` Varianten) + Engine
(`resolveParametricWall()`, 5 rule handlers, helpers) in `src/model/parametricWalls.ts`
- **Agent 2:** 50 Unit-Tests (Vitest), alle grün; `npm test -- parametricWalls.test.ts`
- **Agent 3:** Design-Doku `docs/design/parametric-walls.md` + `docs/README.md` aktualisiert
- Verification: `tsc -b` ✓, `npm run build` ✓ (577 modules, 3.72s)
- **Phase B (UI-Integration) — für nächste Instanz mit Agents:**
- Agent 1: TopBar ParametricWall-Picker + UI-Komponente
- Agent 2: Command `pw` (`cmds/parametricWall.ts`)
- Agent 3: ResourceManager-Integration (ParametricWall CRUD)
- Dann merge → `master`, Screenshot-Verifikation
- **Koordination:** andere Instanz committet parallel (Text, Raum, HLR) auf `master`,
keine Konflikte (nur `types.ts`+`src/model/` erweitert, nicht `App.tsx`).
**Backlog #4 ERLEDIGT (Zeichnen im 3D-View — vorherige Instanz):** Werkzeug-Gate `toolsEnabled`
(`App.tsx` ~1877) von `viewType === "grundriss"` auf `activeLevel.kind ===
"floor"` erweitert → Werkzeuge sind jetzt AUCH in der Perspektive aktiv. Die
gesamte Zeichen-Pipeline war bereits verdrahtet (`Viewport3D.workplaneModel()`
raycastet den Cursor auf die Geschoss-OKFF; `onWorkplanePoint`/`onWorkplaneConfirm`
speisen dieselbe Engine wie der Grundriss). Ende-zu-Ende per Puppeteer verifiziert:
in iso-Ansicht Linie mit 2 Klicks gezeichnet → committet (`draw2d` 0→1, Prompt
zurück auf „Befehl:"); Picks liefern gültige Modell-Vec2 (raycast korrekt).
- **BUG-FIX unterwegs (Engine-Confirm, `src/commands/engine.ts` `confirm()`):**
Beim Testen von #4 fiel ein VORBESTEHENDER Bug auf (reproduziert IDENTISCH im
Grundriss, nicht durch #4 verursacht): mehrteilige Befehle (Wand/Polylinie)
liessen sich per Enter/Rechtsklick NIE abschliessen. Ursache: `confirm()` nahm
IMMER den Tab-Feld-Zweig (`fieldResultPoint`), sobald der Schritt Felder hatte —
auch OHNE gelocktes Feld → es wurde ein Cursor-Punkt gefüttert (bei Wand: Null-
Strecke, ignoriert) statt `onConfirm` (das den Zug committet) aufzurufen. Fix:
Feld-Zweig nur noch bei tatsächlich GELOCKTEM Feld (`Object.keys(this.locks).length
> 0`); ohne Lock fällt `confirm()` auf `onConfirm` durch. Verifiziert: Wand
schliesst jetzt per Rechtsklick im Grundriss (91→98) UND in 3D (99→106); Tab-Feld-
Fluss „Länge 4 tippen → Enter lockt → leeres Enter setzt Punkt" unverändert;
Linie-Auto-Commit (2 Klicks) unverändert. Gegatet: `tsc -b` 0, `build` grün, keine
Konsolenfehler. KEIN Commit.
- **Backlog #1 ERLEDIGT (Befehlsleiste-Transform vereinheitlicht):** Die laufende
Auswahl-Transformation (nur noch `rotate`/Taste `d` mit U/I/O/P; move/mirror
laufen längst über die Engine) speist jetzt die `CommandLine` statt der
schwebenden `TransformBar`. Dritter Zweig in der `<CommandLine>`-Ternäre parallel
zu `gripEdit` (`gripEdit ? … : activeTransform ? … : engineView…`): Prompt =
`transform.hint.${op}.${…}`, Inline-Optionen = U/I/O/P (`transformOptions()`,
aktiver Modus hervorgehoben via neuem `active?`-Feld an `CommandLineOption` +
`.cmdline-option.active` in styles.css, `×count` bei array/distribute), Live-/
lockbares **Winkel**-Feld beim Drehen bzw. **Distanz** beim Bewegen
(`transformFields()` aus neuem `transformDragInfoRef`), getippte Zahl committet
über `submitTransformValue()`. `d` fokussiert die CommandLine (wie `m`/`s`).
`src/ui/TransformBar.tsx` gelöscht; verwaiste `.transform-bar`/`.tf-*`-CSS +
`transform.bar`-Key harmlos stehen gelassen. Gegatet: `tsc -b` 0 Fehler, `build`
grün, Trace-Scan sauber, Puppeteer-Probe end-to-end (nach `d`: „Drehzentrum
klicken" + U/I/O/P; nach Zentrum+Bezug: „Zielwinkel klicken" + Live-Winkelfeld;
keine Konsolenfehler). KEIN Commit (Konvention der Vorsessions beibehalten).
## >>> STAND 2026-07-01 (Abend) — Raum + Topbar-Umbau + Backlog — ZUERST lesen <<<
Alle unten genannten Agenten GRÜN gelandet und gegatet (`tsc -b` 0 Fehler,
`npm run build` grün, Trace-Scan sauber, Boot-Probe ohne Konsolenfehler). Dev-
Server läuft/hält auf :5187. KEIN Commit gemacht (Commit-Regel oben beachten).
**Diese Session gelandet:**
- **Rich-Text-Kern (komplett):** `src/text/richText.ts` (Marks/RichTextDoc/TextRange,
toggleMark/applyMark/isMarkActive/DEFAULT_PRESETS/serialize…), `src/text/renderHtml.ts`
(docToHtml/docToLines/docToSvgText), `src/text/RichTextEditor.tsx` (contentEditable,
Toolbar B/I/U/S + Grösse/Farbe/Presets).
- **Raum-Slice (SIA-416), Agent gelandet:** `Room` in `model/types.ts` (id, floorId,
categoryCode "60", siaCategory HNF/NNF/VF/FF/KGF, boundary, stampAnchor?, stampDoc?);
`cmds/room.ts` (Modus inside=click-inside via `roomFromPointInside`, manual=Polylinie);
Plan: neues `text`-Primitive + Füllpolygon + Stempel via `docToLines` + Live-Fläche;
`panels/RoomBalancePanel.tsx` (SIA-Bilanz + CSV). **App-State für Stempel:**
`selectedRoomId`/`selectedRoom.stampDoc`, `projectSlice.setRoomStampDoc(roomId,doc)`,
`moveRoomStamp`, `setRoomStampAnchor`, `stampEditorRoomId`. Stempel-Editor = kleines
schwebendes Fenster (kein Footer).
- **DOSSIER-Referenz studiert → Spec `docs/design/topbar-dossier.md`** (verbindlich für
Topbar-Arbeit; Referenz-Klon lag unter `/tmp/DOSSIER`). Kernbefund: Text-Styling ist im
Original eine FEST in der Oberleiste sitzende Text-Gruppe (Stil/Font/Grösse + B/I/U +
L/C/R + „+Text"), Akzent-Glow bei Auswahl; Raumstempel-Typografie darüber; Text-INHALT
in eigenem kleinem Fenster; Zoom/Massstab als 2×2-Cluster (kombinierte Stat-Pille).
- **Topbar-Umbau (Agent gelandet), `src/ui/TopBar.tsx` + `styles.css` + `App.tsx` +
`ui/TextEditorDialog.tsx` (neu) + i18n:** Zoom-Doppelung entfernt; 2×2-Massstab/Zoom-
Cluster; einheitliche Segmentpillen (neue `Segment`-Komponente); **Text-Gruppe** in der
Topbar (`textTarget`-Prop = `null | {doc,range,apply}`, aus selektiertem Raum abgeleitet,
`apply``setRoomStampDoc`; ohne range wirkt Format auf ganzes Doc); Doppelklick öffnet
`TextEditorDialog`. Verwaiste alte CSS-Regeln (`.tb-zoom*`, `.stamp-editor*`) bewusst
stehen gelassen (harmlos). „+ Text"-Button verdrahtet, aber DEAKTIVIERT (noch kein
Text-Annotations-Werkzeug).
**Backlog / nächste Schritte (Priorität dieser Session, siehe auch Todo-Liste):**
1. **Befehlsleiste-Transform vereinheitlichen:** laufende Transformation (move/mirror/rotate)
soll die `CommandLine` speisen (Prompt „Basispunkt/Zielpunkt" + U/I/O/P als Inline-
Optionen) statt der separaten schwebenden `TransformBar`; freies Befehl-Tippen während der
Geste unterdrücken, Zahleneingabe (Distanz/Winkel) via `fields` bleibt. Muster: parallel
zum bestehenden `gripEdit`-Zweig bei `<CommandLine>` (~App.tsx:2793). U/I/O/P-Routing
(App.tsx ~15431548) NICHT kaputt machen — nur zusätzlich in die CommandLine spiegeln.
2. **Strukturierter Raumstempel (DOSSIER `RaumProperties`):** Stempel ist KEIN Freitext,
sondern ein FELD-Modell: Raumnummer · Raumname · Raumname-Zeile2 (in Listen als ein Name
zusammengezogen) · Bodenfläche (an/aus + Präfix) · Fensterfläche (an/aus + Präfix) ·
Nutzung HNF/… (an/aus). Zeilen-Layout-Editor (welches Feld auf welche Zeile) + Typografie
PRO Feld-Einheit (Raumname grösser, andere kleiner, eine Zeile kursiv). Ersetzt den
Freitext-`stampDoc`-Zwischenstand; die Topbar-Text-Gruppe formatiert dann die gewählte
Feld-Einheit (löst auch die „ganzes Doc statt Selektion"-Einschränkung).
3. **Wand-Optionen:** Referenzlinien-Option UNTER den Bezugspunkt setzen, gleiche Optik/
Anordnung wie die übrigen Optionen (`cmds/wall.ts` / ToolsPanel-Optionsreihenfolge).
4. **Zeichnen im 3D-View:** Cursor per Raycast auf die Arbeitsebene (okff des Geschosses)
projizieren → Weltkoordinaten als `engine.move/pick` einspeisen; `toolsEnabled`-Gate
(App.tsx ~1834, heute `viewType==="grundriss"`) auch für Perspektive öffnen.
5. **Viewport/Editier-Welle:** Treppen-Griffe (App.tsx grips-memo hat keinen stair-Fall),
Öffnung entlang Host-Wand verschieben (kein opening-Handler im Body-Move; `moveOpeningBy`
fehlt), 2D-Tools auf Zeichnungsebenen (`toolsEnabled` erweitern + Bau-Tool-Buttons grauen),
runde Linienenden (`stroke-linecap:round` auf offene Linien), „schattiert"-Politur
(ACES-Tonemapping+sRGB, HemisphereLight/RoomEnvironment-PMREM, MeshLambert→MeshStandard in
`Viewport3D.tsx` ~416449).
6. **Smart-Join/Split + NAHT-FREIE Wandverschneidung** (Poché-Union an Stössen, keine sichtbare
innere Naht, Gehrung/T-Stoss). 7. **Text/Stempel-Annotation** (aktiviert „+Text";
Frame+Rotation). 8. **Welle B** (3D-Schnittebene + Stencil-Capping + Schnittschraffur).
9. **Welle C** (HLR verdrahten; `src/section/hlr.ts` bereit). 10. Overrides/Ausschnitte/
Multi-Page-Layouts.
**Kollisions-Regel bestätigt:** `App.tsx`, `types.ts`, `generatePlan.ts`, `Viewport3D.tsx`,
`registry.ts`, `PlanView.tsx`, i18n, `styles.css` sind Hotspots → nur EIN App.tsx-Agent pro
Welle; parallele Tracks nur auf genuin App-freien neuen Modulen.
## >>> STAND 2026-07-01 (Bug-Batch Runde 1) — ZUERST lesen <<<
Runde-1-Agenten alle GRÜN gelandet (`tsc -b` + build + Probe verifiziert):
- **3D-Editier-Kamera-Bug (kritisch, `src/viewport/Viewport3D.tsx`):** Ursache = schwerer
Szenen-Aufbau-Effekt mit `project`-Dependency → jede Griff-Mutation baute Szene neu auf und
`applyView3d` resettete die Kamera auf Preset (→ „springt in Top-View", Rechts/Links-Pan-Illusion).
Fix: `camStateRef`-Snapshot (Pos/Target/up/ortho-Zoom/aktive Kam) im Render-Loop fortgeschrieben,
bei Neuaufbau WIEDERHERGESTELLT statt `applyView3d`; `applyView3d` nur beim ersten Aufbau;
`view3d`-Effekt guarded via `gripDragRef`; Top-View `enableRotate=false`; 2D-Drawings bekommen
`move`-Griff (Anker=center/at) → in 3D verschiebbar. Kamera pos/target numerisch identisch vor/nach
Drag/Löschen bewiesen.
- **Print-Linienstärke invertiert → korrekt (`src/plan/PlanView.tsx`, `styles.css`):** Print zeichnet jetzt
echte Papier-mm in viewBox-Einheiten OHNE `non-scaling-stroke`: `strokeVb=(mm·N/1000)·PX_PER_M`,
N=`paperScale` stabil (nur bei `applyScale`/erstem Messen gesetzt). Rein=dicker, raus=dünner;
1:100 vs 1:10 = exakt ×10. Display/Haarlinie unverändert. **Ecken-Miter:** CSS `stroke-linejoin:miter`
+ `buildDrawingRuns` bündelt zusammenhängende `line`-Primitive gleicher drawingId zu einem
`polyline`/`polygon` (Ringschluss erkannt) → gehrte Ecken statt Butt-Cap-Stufe. Primitive-Modell
(und Print-Export) unverändert.
- **ambientCG-Live-Bibliothek (`src/materials/ambientcg.ts` neu, `ResourceManager.tsx`):** komplette CC0-Lib
live durchsuchbar (Suche/Kategorie/Auflösung 1K-4K), jszip-Entpackung → Blob-URLs, NormalGL bevorzugt.
**CORS:** Such-JSON + `/get` brauchen Proxy → Vite-Dev-Proxy `/ambientcg` in `vite.config.ts`
(`VITE_AMBIENTCG_PROXY` für Prod); optionale Prod-Route `openbureau-core/api/src/routes/ambientcg.js`
(lesend, gemountet VOR Auth-Gate). Thumbnails direkt (ACAO:*). Offline → Schnellauswahl-Fallback.
**Runde 2 FERTIG (2026-07-01, alle GRÜN + Probe-verifiziert):**
- **2a** [App/engine/editors/registry, neu `cmds/mirror.ts`+`cmds/join.ts`]: Snapping erster Punkt gefixt
(Modifier shift/ctrl wurden im Command-Pfad verschluckt → jetzt via `EngineMods` durch `move/pick``host.snap`
gefädelt); Segment-Move für offene Polylinien/Linien (`EdgeGrip.free`, ganzer 2D-Delta, Nachbar-Segmente
folgen über geteilte Vertices); Kürzel↔Befehl vereinheitlicht (`M`→engine.start("move"), `S`→"mirror",
`Ctrl+J`→"join", identisch zum Tippen; `Command.autoRun` für nicht-interaktive Befehle; `CommandSelection.drawingIds`
für Mehrfachauswahl); **Mirror-Befehl** (2-Punkt-Achse, Vorschau, `commitTransform mirror/copy`).
- **2b** [cmds/{polyline,rect,wall}]: Polylinie **offen/geschlossen** (Toggle `m`, Ring-Fill); Rechteck **2pt/3pt/Zentrum**
(3pt = gedreht → closed polyline); **Wandstärke-Tab-Feld** nur bei einschichtigen Wandtypen (baut on-commit
einen einschichtigen Wandtyp der Zieldicke, Original bleibt).
- **Swisstopo/OSM-Kontext-Importer** [io/{lv95,geoContext,swissTopo,osm}, ui/ContextImportDialog, SitePanel]:
„Standort importieren" — geocode (geo.admin SearchServer, LV95 direkt), swisstopo-Gebäude
(`MapServer/identify` layer `ch.swisstopo.vec25-gebaeude`), OSM-Overpass (Gebäude/Strassen/Wasser/Grün).
LV95↔WGS84 (Bern round-trip 0.38 m) + Origin-Shift. CORS: geo.admin direkt, Overpass via neuer read-only-Route
`openbureau-core/api/src/routes/geoproxy.js` (vor Auth-Gate, host-allowlist) wenn `VITE_GEO_PROXY` gesetzt.
**TIER-1 FORTSCHRITT (2026-07-01):**
- **DXF-Vektor-Export FERTIG** [src/export/{dxfWriter,exportDxf}, ui/ExportDxfDialog, TopBar+App-Button]:
hand-rollierter R2000/AC1015-ASCII-Writer, `$INSUNITS=6` (Meter), 23 Layer aus Kategorien (ACI+True-Color+lw),
LINE/LWPOLYLINE(closed)/CIRCLE/ARC/TEXT; Quelle = `generatePlan()` in Meter-Modellraum. 5 m-Wand→5.345 m Span
verifiziert. DWG NICHT geshippt (libredwg-web nur lesend — ehrlich weggelassen).
- **Decke (Ceiling) FERTIG** [geometry/ceiling, cmds/ceiling(alias d/decke), types.ts `Ceiling`, projectSlice/selectionSlice,
generatePlan `addCeilingPoche`, PlanView-Hittest, Viewport3D `addCeilingMesh`(Extrude+Material+Modi), ToolsPanel-Button+
Deckentyp-Picker, ObjectInfoPanel `CeilingSection`, host.ts, App-Grips/Selection]. Wand-Muster: floorId + categoryCode "30"
+ wallTypeId/Component + VerticalAnchor(OK=Geschoss-Top) + optionale Dickenüberschreibung; statt Achse/Band ein
closed `outline: Vec2[]`. Plan = Umriss-Polygon (Fill+Hatch + schwere Outline) VOR den Wänden gezeichnet. Grips Vertex+Edge+Body.
- ⚠️ MERKE: **App.tsx ist der Serialisierungs-Engpass** — pro Welle darf NUR EIN Agent App.tsx editieren (DXF+Decke
liefen versehentlich gleichzeitig darauf; Merge hat diesmal geklappt, aber nicht drauf verlassen).
- **Öffnung (Fenster/Tür) FERTIG** [geometry/opening, cmds/opening(alias f/fenster, t/tuer/tür, oe), types.ts `Opening`,
generatePlan(`wallGaps`), PlanView, Viewport3D(`addWallMeshes` segmentiert + Sturz/Brüstung, `addOpeningMeshes` Rahmen/Glas/Flügel),
slices, App, ToolsPanel(Fenster+Tür-Buttons), ObjectInfoPanel(editierbar)]. **Host-Sync:** Öffnung speichert KEINE
Weltkoordinaten — alles wird pro Render aus `wall.start/end` neu abgeleitet → Wand verschieben bewegt Öffnung automatisch.
Plan-Symbole je Detailgrad (Tür: Blatt→+Schwenkbogen→+Anschlag; Fenster: 1→+Rahmen→+2 Glaslinien).
- **SIA-416-Engine FERTIG** [geometry/{roomArea,roomBoundary} — pure, isoliert, 34/34 Tests]: Raumerkennung
(planar-arrangement+half-edge face-tracing, `detectRooms`/`roomFromPointInside`), Fläche/Umfang/Zentroid,
SIA-Hierarchie (GF→KGF+NGF; NGF→NF(HNF/NNF)/VF/FF), `balance()`, `roomsToCsv`. Bereit für den Raum-Slice.
- **Treppe FERTIG** [geometry/stair, cmds/stair(alias treppe/tp — `tr`=trim!), types.ts `Stair`, model/wall `stairVerticalExtent`,
generatePlan `addStairSymbol`(Schnittbruchlinie+Pfeil), Viewport3D `addStairMeshes`(Stufenprofil), slices/App/Panels/i18n].
gerade/L/Wendel (Wendel = vereinfachtes Keil-Modell), SIA-17/29, geschoss-übergreifend (totalRise=Geschosshöhe).
- **HLR-Spike FERTIG (Welle-C-Fundament)** [src/section/{hlr,occt,occt-wasm.d}.ts, docs/welle-c-hlr-spike/, package.json
+opencascade.js, vite.config aliases]. **API-Pfad = `HLRAppli_ReflectLines`** (NICHT HLRBRep_Algo — im Prebuilt nicht
konstruierbar). Lazy via dynamic import, Haupt-Bundle unberührt. 62 MB WASM → später Custom-Build auf 5-15 MB.
**SESSION-LIMIT-STOP (2026-07-01 ~11:00, reset 13:00 Europe/Zurich):** Raum- + Rich-Text-Agenten wurden vom
Session-Limit abgeschnitten. Repo ist trotzdem GRÜN & stabil (tsc+build grün, dev :5187 läuft). Genauer Stand:
- **Raum: NICHT begonnen** — kein `cmds/room.ts`, kein `Room`-Typ, keine Verdrahtung. Sauber neu starten. SIA-Engine
(`geometry/roomArea.ts`+`roomBoundary.ts`) liegt fertig & getestet bereit zum Import.
- **Rich-Text: FAST FERTIG** — `src/text/richText.ts` (Doc-Modell + Marks + plainText/applyMark/toggleMark/serialize/Presets)
FERTIG. `src/text/renderHtml.ts` (docToHtml für Editor + docToLines/docToSvgText für SVG-Plan, gemeinsame Mark→Stil-Map)
JETZT FERTIG & kompiliert (im Hauptthread nachgezogen, da Agent am Limit war). FEHLT nur noch: `src/text/RichTextEditor.tsx`
(WYSIWYG-Komponente mit Toolbar bold/italic/underline/strike/Grösse/Farbe + Presets, controlled: value/onChange).
- Zwei Helfer-Subagenten ("Map/Extract …wiring in App.tsx") liefen ebenfalls ins Limit; App.tsx ist laut tsc+build INTAKT.
**RESUME NACH RESET (Reihenfolge):** (1) Rich-Text-Kern fertig (renderHtml + Editor-Komponente) — App-frei, isoliert.
(2) Raum-Slice (nutzt SIA-Engine, click-inside, Stempel, Bilanz+CSV) — App-Slice. (3) Text/Stempel-Slice (nutzt Rich-Text-Kern).
(4) Smart-Join/Split. (5) Welle B (3D-Schnitt+Stencil-Capping) → Welle C (HLR verdrahten, `src/section/hlr.ts` bereit).
(6) Overrides-Engine, Ausschnitte, Multi-Page-Layouts. REGEL: nur EIN App.tsx-Agent pro Welle; Parallel-Track nur wenn App-frei.
---
## >>> STAND 2026-06-30 (Fortsetzung, spät) <<<
Fortsetzungs-Lauf (UI + Import + Wand-Attribute + Selektion/Editieren). Alles
unten ist `npx tsc -b` + `npm run build` GRÜN und per Screenshot/Probe verifiziert,
@@ -49,6 +344,32 @@ außer „LÄUFT" markiert. Viel wurde an **Subagenten** delegiert (Dateien land
auf der Platte, unabhängig von Benachrichtigungen).
**Fertig & verifiziert (diese Fortsetzung):**
- **Vektor-PDF-Export des Grundrisses (KERN-ZIEL — fertig & verifiziert):**
`src/export/planToPrintSvg.ts` (Druck-SVG aus `generatePlan`, mm-Mapping im Maßstab,
echte mm-Linienstärken/ISO-Stifte, Schraffuren als Vektorlinien) + `src/export/exportPdf.ts`
(`jspdf`+`svg2pdf.js` → echtes Vektor-PDF, A4/A3 ×Hoch/Quer, Schriftfeld). TopBar-Button
„PDF" + `src/ui/ExportPdfDialog.tsx`. Verifiziert: 522 Pfade, 0 Rasterbilder, maßstabsgenau
(5,345×4,345 m → 53,45×43,45 mm @1:100). `scripts/probe-pdf.mjs`.
- **2D-Boolean (Union/Differenz/Schnitt):** `src/editors/booleanOps.ts` (polygon-clipping),
Kontextmenü „Vereinigen/Subtrahieren/Schneiden" + Ctrl/Cmd+Shift+U/D/I; geschlossene
Drawing2D, Differenz=erstes Element minus Rest, Loch→Außenring gefüllt + Innenringe als
Umriss. Verifiziert (Union 8/Differenz 6/Schnitt 4 Ecken). `scripts/probe-boolean.mjs`.
- **3D-1 — ERSTELLEN in 3D (Meilenstein):** der 3D-Viewport ist Erstellungs-Fläche;
dieselben Befehle (line/wall/rect/polyline) erzeugen Elemente direkt in 3D. Raycast auf
Geschoss-Arbeitsebene (`THREE.Plane` y=gridElevation) → Modell-`Vec2` (model.x=P.x,
model.y=P.z, aus `addDrawing2DLines` abgeleitet) → dieselbe `CommandEngine`
(`move/pick/confirm`). Live-3D-Draft (Rubber-Band + Snap-Marker, eigene `draftGroup`),
OrbitControls bei aktivem Befehl umgeschaltet, `pxPerMeter`-Fallback 90. Viewport3D-Props
`commandActive`/`onWorkplanePoint`/`onWorkplaneConfirm`/`draft`. Verifiziert (Wand in 3D →
in 3D + Plan sichtbar). `scripts/probe-3d-create-*.png`.
- **3D-2 — EDITIEREN in 3D (Meilenstein):** 3D-Griffe (eigene `gripGroup`) bei genau 1
Auswahl + `!commandActive`. Wand: Endpunkt-Kugeln (Achse), Höhen-Griff (OK via
`updateWall top:{mode:"custom",z}`), Verschiebe-Griff; Drawing2D: Vertices. Drag raycastet
auf Arbeitsebene bzw. vertikale Ebene → DIESELBEN Store-Actions wie Plan-Griffe
(`moveGripOf`/`moveElementByOf`/`updateWall`) → Plan+3D konsistent. Laufender Drag in
`gripDragRef` (übersteht den Szenen-Neuaufbau pro Move). Verifiziert (Endpunkt + Höhe in 3D
editiert, im Plan deckungsgleich). `scripts/probe-3d-edit-*.png`. **Offen (→ Editier-Welle 3):
3D-Snap beim Drag + Griffe „immer oben" (Tiefentest).**
- **Zwei-Ton-Dark-Theme + KEIN Petrol-Grün mehr:** `src/styles.css` Dark-Tokens neutral
(`--bg #0e0e0e`, `--panel #1d1d1d`, `--accent #4d4d4d` …); Kontextmenü-Tokens
`--ctx-hover/-text` neutral; grüner Fokus-Ring entfernt.
@@ -88,6 +409,47 @@ auf der Platte, unabhängig von Benachrichtigungen).
25 ok), `src/editors/splitJoin.ts`, App-Keydown (Ctrl/Cmd+S/J, `preventDefault`) +
`onSegmentCut`, PlanView Alt-Klick. Verifiziert (Rechteck→2 geschlossene, Join→1
Polylinie, Alt→Segment weg). `scripts/probe-splitjoin.mjs`. Wände bleiben no-op.
- **Editier-Welle 3 + Eingabe-Vereinheitlichung + Snap-Feinschliff (FERTIG):** 3D-Drag-Snap
(gleiche `computeSnap`) · Griffe/Marker immer-oben · koinzidente Endpunkt-Griffe ziehen
gemeinsam (Viewport3D). **GUI-Werkzeug** (line/polyline/rect) → `engine.start` +
Befehlsfeld-Fokus; **Shift = H/V-Ortho** beim Griff-Drag (2D+3D, `applyAngleConstraint`
aus `tools/snapping`). OFFEN: „Wand" noch Legacy (Wand-als-Command); Editieren läuft noch
AD-HOC, nicht durch die Engine → Tab-Wert + Referenz-Tracking offen. Snap-Marker
vereinheitlicht: 2D Endpunkt=Kreis/Mittelpunkt=Strich, 3D-Snap=`#ffcc44`, Marker kleiner;
2D-Vorschaulinie grau-Volllinie.
- **DWG-Parser erweitert (FIX „keine Geometrie gefunden") + Trim (FERTIG):** `io/dwgParser.ts`
neu — ARC/CIRCLE/ELLIPSE/SPLINE + **INSERT/Block-Expansion** (rekursiv, aus
`db.tables.BLOCK_RECORD.entries`, volle 2D-Affin-Transform) + Diagnose-Histogramm
(`DxfImportResult.diagnostics`, in ImportDialog). Real-DWG 37 INSERT → 67k Konturen.
**Trim** (`commands/cmds/trim.ts`, Alias `tr`, `kernel2d.trimPolyline`) — Quick-Trim bis
Schnittpunkt. Beide verifiziert.
- **Material-Bibliothek (Daten, FERTIG):** 12 CC0-Materialien von ambientCG →
`public/assets/materials/<id>/{color,normal,roughness,displacement,ao}.jpg` + `manifest.json`
+ `src/materials/library.ts` (PBR).
- **Welle A — 3D-Materialien (FERTIG & verifiziert):** `Component.material` (`ComponentMaterial`:
PBR-Map-URLs + `sizeM` Kachelgröße), `src/materials/runtime.ts` (`MaterialRuntime` → gecachte
`MeshStandardMaterial`, Kacheln in Welt-Metern via Extrude-UVs), `renderMode` „textured"
(TopBar-Darstellung), Material-Picker (ambientCG-Grid) + Upload + Kachelgröße im
`ResourceManager` (Bauteile-Tab). Backstein mit Normal-Map-Tiefe verifiziert.
`scripts/probe-materials.mjs`. Schnittschraffur-Slot frei für Welle B.
- **Editier-Eingabe + Topbar-Politur (FERTIG):** Befehlszeile zeigt **Live-Länge/-Winkel**
beim Zeichnen (`Command.fieldValues`), **Cursor-HUD entfernt**; **Seiten-Verschieben
propagiert verbundene Enden** (2D+3D); **Tab beim Vertex-Editieren** setzt Wert
(`gripEdit`-Controller in App — noch ad-hoc, NICHT durch die Engine). Topbar: Rest
iconisiert (Referenzlinien/Linien-Modus/Ressourcen), Linien-Modus → Dropdown, Aktions-
Buttons als Dropdown-Pill-Familie, **Detailgrad+Massstab gestapelt + 1:N/Zoom% kompakt**
(`.tb-zoomstack`).
- **Wand-als-Command (FERTIG & verifiziert):** `commands/cmds/wall.ts` (chainend, Achs+Band-
Draft, Tab-Felder; Wandfelder aus `CommandContext.activeWallTypeId`/`level`) + Registry
(Aliase `w`/`wand`) + App-Kopplung (`TOOL_COMMAND.wall`, Legacy-Wand umgeleitet). Wände in
2D+3D. `scripts/probe-wall-cmd.mjs`.
**OFFENE WELLEN (nächste Schritte):** Referenzobjekt-Achs-Tracking + Editieren VOLL durch die
Engine (heute `gripEdit` ad-hoc); **Welle B** (3D-Clip/Schnittebene + Stencil-Capping +
Schnittschraffur — nutzt das Material-System aus Welle A); **Welle C** (3D→2D-Schnitt/
Perspektive via HLR, opencascade.js-Spike); **Swisstopo-Importer** (Ort+Radius → Gebäude+
Gelände, geo.admin STAC/Geocode, CORS prüfen); Bögen/Kreise-Trim; Wandstärke-Tab bei
einschichtigen Wänden.
**Offene Wellen (Todo-Spiegel — Todo-Liste lebt im Kontext):**
1. Editier-Welle 3: **koinzidente Endpunkt-Griffe gemeinsam ziehen** + verbundene Enden