Compare commits
183 Commits
db6f3ae0a0
...
05bc5aa6e1
| Author | SHA1 | Date | |
|---|---|---|---|
| 05bc5aa6e1 | |||
| d36c84689f | |||
| 8f9123ce0d | |||
| a44572bf7b | |||
| 79ea6b0b3b | |||
| 83da278ed2 | |||
| 33c2c6c22e | |||
| b7551d4930 | |||
| 25daec6be9 | |||
| 5229174551 | |||
| df0e5e432a | |||
| 715e950d7c | |||
| dbf78a9d75 | |||
| 26fbd11b73 | |||
| 6e5998ce72 | |||
| b65c676643 | |||
| ae18766b01 | |||
| c7128fe669 | |||
| b37c9f467b | |||
| 08b0b23a68 | |||
| 4a26c34db1 | |||
| 903dc19cec | |||
| 09c5178c85 | |||
| 8750c8f547 | |||
| 028a3637b4 | |||
| 74eecf7a73 | |||
| 0ca3b1dd57 | |||
| ecefe61611 | |||
| 2d96a864da | |||
| dec431579e | |||
| 9520191bde | |||
| 9136cad0ee | |||
| c7e080f639 | |||
| aa4205bf0b | |||
| 9a80d9cf42 | |||
| ea86cf7456 | |||
| 05cfade483 | |||
| 87a30b7061 | |||
| 23dddf893e | |||
| 45ded83294 | |||
| 77f32f14ce | |||
| 07042ebc75 | |||
| ecf262ef07 | |||
| 9acce9d7cd | |||
| cf8b638f33 | |||
| 20cf870c4c | |||
| e095b51d15 | |||
| 65d6e843fb | |||
| c79d15dda4 | |||
| 6035b8a8c2 | |||
| 6c04935711 | |||
| 8c96a5dfb9 | |||
| 8547d38e9a | |||
| 41d8fafa4e | |||
| 0d3a0a081f | |||
| d1df9d4d5c | |||
| e11743d6e7 | |||
| c54a2f916c | |||
| fde27f6838 | |||
| 00c90857ad | |||
| 018fef56bf | |||
| 1db661f19a | |||
| 6ea562ceda | |||
| e137353b95 | |||
| 16d32223a4 | |||
| 8f4fac633f | |||
| d4065c5a41 | |||
| d0b94a75ec | |||
| 8b6b9291a1 | |||
| 7e8764b21b | |||
| efc9dd87ce | |||
| f724c088e0 | |||
| 44942e6976 | |||
| e0d71691e1 | |||
| 9f6d4e8858 | |||
| 19d002d403 | |||
| 2c9633438d | |||
| 19fcafbcab | |||
| 4aed168760 | |||
| 2eeafb9aca | |||
| 7d3e0943e1 | |||
| d0f26cd874 | |||
| 777e02c927 | |||
| c34b7e5771 | |||
| b55ab9eb8f | |||
| bd13495923 | |||
| 683242ba78 | |||
| e54bb318bb | |||
| a302d512d9 | |||
| f09ba0e49f | |||
| 07475cbe6c | |||
| 87ef7988d1 | |||
| f3966f99e9 | |||
| f3639cfb15 | |||
| 6288f755a8 | |||
| 724db0f9bf | |||
| c2d6893a59 | |||
| c6d2641664 | |||
| 3d4c4985a9 | |||
| 912f0bc16d | |||
| ac7af9b9ef | |||
| 400ec890b0 | |||
| 558a24cf98 | |||
| e29f5a2a7c | |||
| b03614c35b | |||
| 1086225f7b | |||
| ca1ed1b0d2 | |||
| 04c7334cc4 | |||
| 93b70fa95a | |||
| fae4f6fcb6 | |||
| d65d84183a | |||
| 1f6da09b2b | |||
| 5fd0161bc7 | |||
| f4e70902d0 | |||
| af0b044fca | |||
| 692dd3f719 | |||
| 4d721a62f7 | |||
| b028fc6b31 | |||
| 581ddccf1a | |||
| 1fd688896c | |||
| f9a5e763fd | |||
| ec2568276d | |||
| 2b6b99059d | |||
| 658536029b | |||
| 675c68de55 | |||
| e9dc423a12 | |||
| 6aaef8f46a | |||
| eaf6d8ed7b | |||
| d30f79e162 | |||
| bde1f3f8d1 | |||
| 2dd7fe339d | |||
| 3c4d983a7d | |||
| 094d8b4504 | |||
| 330c01bbbd | |||
| d2503c1191 | |||
| e3f460c9f8 | |||
| 28b5492def | |||
| 4a62dbf83c | |||
| 8a79f6cbaa | |||
| 9371b65b3b | |||
| e8f5e7272d | |||
| f95df75ea1 | |||
| 918f60c498 | |||
| 23410f9b9e | |||
| 98994c96aa | |||
| 0e484746de | |||
| 87d12b976f | |||
| 519735c782 | |||
| 47f1c24bb3 | |||
| 1bbcc7038f | |||
| e6466a5027 | |||
| 1ab8c7e496 | |||
| 0e839b50d6 | |||
| 623b243306 | |||
| 6a7bbeec2f | |||
| cbf54e6b65 | |||
| 543d06adb5 | |||
| a3b25da96d | |||
| 2a2b3b294c | |||
| 926dedca40 | |||
| 4311a7dfbd | |||
| 31ac91b2a7 | |||
| 0189eed5ac | |||
| 45f9d0c381 | |||
| faa84e98af | |||
| e82f0b6de1 | |||
| 128c04a5d0 | |||
| 19f99382f6 | |||
| ec181998ca | |||
| c400a96575 | |||
| b4c3c2de4a | |||
| 788f4d58ca | |||
| 229169cea1 | |||
| f4cd16b7ac | |||
| e7ea1eeddd | |||
| c96239a794 | |||
| 260320af2e | |||
| f08ef13fe8 | |||
| 3d2d4d6321 | |||
| cfe5249440 | |||
| b9731a4979 | |||
| ce3b594403 | |||
| 94a5af6b6f |
@@ -57,7 +57,7 @@ Rendern angewandt, nie in die Geometrie eingebacken.
|
||||
|
||||
## Arbeitsweise (für Beiträge)
|
||||
|
||||
- Substanzielle, mehrstufige Arbeit an **Subagenten** delegieren, wo möglich.
|
||||
- Substanzielle, mehrstufige Arbeit schrittweise in isolierten Schritten angehen.
|
||||
- Änderungen verifizieren: `npx tsc -b`, `npm run build`, und Screenshot via
|
||||
`node scripts/probe.mjs` (schreibt `scripts/probe.png`) bzw. `probe-ff*.mjs` für
|
||||
Firefox-Fälle. Screenshot ansehen und Geometrie visuell prüfen.
|
||||
|
||||
@@ -1,422 +1,91 @@
|
||||
# HANDOVER — Browser-BIM (cad), Standalone-Port von DOSSIER
|
||||
# HANDOVER — Stand 2026-07-04 (Schraffur/Linien-Epic + Folgethemen)
|
||||
|
||||
> Für die nächste Instanz. Stand: 2026-06-29. Lies zuerst
|
||||
> `CONVENTIONS.md`, `ROADMAP.md` und die Projektnotizen (siehe unten).
|
||||
Übergabe an eine frische Instanz. Alles unten Gelistete ist committet, sofern nicht anders vermerkt. Ältere Handover-Stände (< 2026-07-04) siehe git-Historie. Zuerst `CONVENTIONS.md` lesen.
|
||||
|
||||
## >>> COMMIT-REGEL (verbindlich, IMMER beachten) <<<
|
||||
> **➡️ Aufgaben-Queue steht in `PENDENZEN.md`** (Single Source of Truth, priorisierte Checkliste + Arbeitsprotokoll). HANDOVER = nur noch Kontext/Konventionen/Environment/Zielmodelle, KEIN Backlog mehr. Neue Pendenzen/Rückfragen dort eintragen, nicht hier.
|
||||
|
||||
Dieses Repo darf **keinerlei Hinweise auf KI-Werkzeuge** enthalten — weder im
|
||||
Code/Doku noch in der Git-Historie.
|
||||
## ⚠️ NACHTRAG 2026-07-04 abends — Unterbruch, 3 Slices verloren
|
||||
|
||||
- **Commit-Messages:** sachlich, in der Sprache des Projekts. **NIEMALS**
|
||||
`Co-Authored-By:`-Trailer, „Generated with …"-Zeilen, Tool-/Modellnamen oder
|
||||
sonstige Urheber-Hinweise auf einen Assistenten.
|
||||
- **Dateien/Kommentare:** keine Erwähnung von Assistenten, Modellen oder Agenten.
|
||||
Wer hier weiterarbeitet, schreibt so, als wäre es Handarbeit des Teams.
|
||||
- Vor jedem Push kurz prüfen: `git log` und `git diff` frei von solchen Spuren.
|
||||
> **STAND-UPDATE (Session 4, neues Gerät):** **Environment behoben** — Toolchain vorhanden (`node`/`npm`/`cargo` unter `/usr/bin`, `wasm-pack` via `npx` 0.15.0; es war die Flatpak-Sandbox); fehlendes Rust-Target per `sudo dnf install rust-std-static-wasm32-unknown-unknown` (+ lld) nachinstalliert; beide WASM-Engines gebaut → App läuft. **Slice 1 (Locked-Iso `cb8fae5`) + Slice 2 (Joins Phase 1c `c5a344d`) committet.** **Slice 3 (Öffnungen als Boolean-Löcher + Deckentrim-nur-3D) ✅ committet `1407c68`:** Fenster/Türen = 1 Wandkörper pro Schicht-Band mit rechteckigen `holes` (render3d stanzt sie per achsparalleler Rechteck-Gitter-Zerlegung + 4 Laibungsquads/Loch), Segment-Boxen weg; `holes`/`openings` schliessen sich im Emitter aus; Schnitt-Pfad (layered=false) segmentiert unverändert weiter. Deckentrim `trimWallTopForCeilings` gilt nur noch im 3D-Pfad, Schnitt-Pfad bekommt volle Wandhöhe. Alle Datei-Edits waren gelandet — unabhängig verifiziert: cargo test 56 (inkl. beider Pflicht-Tests versetzt-überlappende Fenster + Tür-Loch-bis-Boden), vitest 230, build:engine3d + tsc sauber, Trace clean. **Damit sind alle drei verlorenen 3b-Slices rekonstruiert.** **Memory-Direktiven `priority-based-joins.md` + `openings-as-booleans.md` wiederhergestellt.**
|
||||
>
|
||||
> **⚠️ PUSH BLOCKIERT:** keine Git-Credentials auf diesem Gerät (kein Helper, keine `~/.git-credentials`, kein SSH-Key; Remote HTTPS `git.openbureau.ch/karim/DOSSIER-STANDALONE`). `cb8fae5` + `c5a344d` (+ folgender Öffnungen-Commit) sind **lokal committet, NICHT gepusht** (`ahead of origin/master`). Sobald Gitea-PAT (repo-write) oder SSH-Key da ist: `git push origin master`. Bis dahin holt das andere Gerät die Arbeit nicht.
|
||||
>
|
||||
> Der folgende Block bleibt als historischer Kontext/Vorlage für den offenen Slice 3 stehen.
|
||||
|
||||
## >>> STAND 2026-06-30 (Fortsetzung, spät) — ZUERST lesen <<<
|
||||
Die Arbeit wurde unerwartet unterbrochen — mehrere Tasks liefen parallel. Diese Übergabe wurde nachträglich rekonstruiert. **Der tatsächliche HEAD-Commit ist `03f0c40`** (Nordstern-3D Live-Schnittebene) — alles danach existierte nur im Arbeitsbaum der alten Maschine und ist auf **diesem** Checkout (neues Gerät, `git status` sauber bis auf `package-lock.json`) **nicht vorhanden**. Diese drei Slices waren in echter Arbeit (nicht nur geplant) und gelten als verloren — neu beauftragen, nicht nur „prüfen ob gelandet":
|
||||
|
||||
Fortsetzungs-Lauf (UI + Import + Wand-Attribute + Selektion/Editieren). Alles
|
||||
unten ist `npx tsc -b` + `npm run build` GRÜN und per Screenshot/Probe verifiziert,
|
||||
außer „LÄUFT" markiert. Viel wurde an **Subagenten** delegiert (Dateien landen
|
||||
auf der Platte, unabhängig von Benachrichtigungen).
|
||||
1. **Locked-Iso-Fix** (`src/viewport/Wasm3DViewport.tsx`, `OrbitState` um `ortho: boolean` + `orthoHalfHeight: number` erweitern, `orbitCamera()`/Preset-Effect/Pan/Zoom entsprechend anpassen) — Bug: Iso-Ansicht kippt bei der ersten Kamerabewegung sofort in Perspektive statt orthografisch (parallel) zu bleiben. War fertig gebaut und quantitativ verifiziert (Parallelogramm-Kantenvergleich vor/nach Orbit, 225/225 Tests grün), aber **nicht mehr committet**, bevor der Unterbruch kam. Kleinster, unabhängigster der drei Fixes — zuerst neu machen.
|
||||
2. **Joins Phase 1c — Durchgangswand echt aufbrechen** (`spanCutouts` in `src/model/joins.ts` + `src/plan/generatePlan.ts` + Rust-Parität `src-tauri/geometry/src/lib.rs`): Nutzer-Befund nach Phase 1b war, dass der Innenputz der DURCHGANGSWAND am T-Stoss nur überdeckt wird (Zeichenreihenfolge), nicht geometrisch ausgeschnitten — Fugen-/Nahflächenlinien laufen noch durch den Backstein-Durchstoss, es „liest" noch nicht wie ein einziger Join. Das war die **explizite Priorität des Nutzers** für diese Session. Die Umsetzung war weit fortgeschritten (schrieb bereits in `joins.ts`), dann abgebrochen. **Nicht committet, nicht im Baum.** Vollständige Aufgabenbeschreibung mit allen Datei:Zeile-Ankern steht im Transkript bei „Agent:Phase 1c: Durchgangswand aufbrechen" — 1:1 als Vorlage für den Neu-Auftrag wiederverwenden.
|
||||
3. **Öffnungen als echte Boolean-Löcher** (`RWall.holes`, `src/plan/toWalls3d.ts`, `src-tauri/render3d/src/mesh.rs`, additiv `types.rs`): EIN Wandkörper mit rechteckigen Löchern statt der heutigen Pfeiler/Brüstung/Sturz-Ersatzkörper (sichtbare Segment-Nähte, keine echten Löcher). Pflicht-Testfall (Nutzer-Direktive, Kernmotivation des Slices): zwei Fenster in derselben Wand mit überlappendem Achsen-Intervall, aber unterschiedlicher Höhe (z. B. u=[1.0,2.0]/z=[0.9,2.1] und u=[1.5,2.5]/z=[0.3,0.7]) — die alte Segment-Zerlegung kann das nicht abbilden, echte Löcher schon. Zusatz-Auftrag im selben Slice: `trimWallTopForCeilings` (Phase-3-Deckentrim, Commit `e4b8df6`) darf **nur** im 3D-Viewer-Pfad (`layeredWalls:true`) gelten, **nicht** im Schnitt-Pfad (`layeredWalls:false`) — der Schnitt braucht die volle, ungetrimmte Wandhöhe für seine eigene schichtweise Prioritäts-Subtraktion (`subtractDominantBands`), sonst zeigt der Schnitt die Wände fälschlich abgeschnitten. Fortschritt bei Abbruch **unklar** (letztes Status-Update deutete auf „noch in der Kartierungsphase" hin) — vermutlich am wenigsten weit von den dreien; komplett neu beauftragen. Vollständiger Auftragstext im Transkript bei „Agent:Oeffnungen als Boolean-Loecher" + die zwei Zusatz-Direktiven (Decken-Trim-Fix, Pflicht-Testfall versetzte Fenster) kurz danach.
|
||||
|
||||
**Fertig & verifiziert (diese Fortsetzung):**
|
||||
- **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.
|
||||
- **Topbar DOSSIER-Stil:** Icon-Grid 4-oben/3-unten (Grundriss integriert), Kombis
|
||||
gestapelt, kompakte Selects. `src/ui/TopBar.tsx`.
|
||||
- **Themed Dropdowns** statt nativer `<select>`: `src/ui/Dropdown.tsx` (Wert- + Aktions-
|
||||
Modus, Popover im Kontextmenü-Stil, kein blaues OS-Menü). In TopBar verdrahtet.
|
||||
- **Kontextmenü/Dropdown-Politur:** Material-Symbols-Icon-Font in `index.html` geladen
|
||||
+ `.material-symbols-outlined`-Regel in styles.css (sonst Roh-Ligaturtext); Größen
|
||||
runterskaliert (`.ctx-menu` 11.5px, Icons 13–14px, weniger Padding, Radius 10).
|
||||
- **Geschoss-Z-Raster:** `Viewport3D` Bodenraster auf `gridElevation` (= aktives
|
||||
Geschoss `baseElevation`).
|
||||
- **DXF/DWG-Import als DIALOG + Drag&Drop:** `src/ui/ImportDialog.tsx`,
|
||||
`src/io/dxfToDrawings.ts`; Ziel-Zeichnungsebene wählen / neue „Zeichnung" anlegen;
|
||||
Layer→Kategorie-Handling; App-weiter Drop-Handler.
|
||||
- **DWG in-app parsen:** `@mlightcad/libredwg-web` (WASM, installiert), `src/io/dwgParser.ts`
|
||||
(lazy, mappt DwgDatabase→{meshes,contours}), `vite.config.ts`-Aliase für WASM
|
||||
(`virtual:libredwg-glue` + `?url`), `src/io/libredwg-web.d.ts`. Gegen echte DWGs getestet
|
||||
(11.668 Konturen). Fallback→ODA-Hinweis bei Lib-Fehler.
|
||||
- **Wand-Objekt-Info-Attribute** (additiv, abwärtskompatibel): Modell `Wall.referenceLine`
|
||||
(`left|center|right`, default center) + `Wall.bottom/top: VerticalAnchor`
|
||||
(`{mode:"floor",floorId,offset?}|{mode:"custom",z}`) in `src/model/types.ts`; Resolver
|
||||
`src/model/wall.ts` (`wallReferenceOffset`, `wallVerticalExtent`, `nextFloorAbove`);
|
||||
angewandt in `generatePlan`, `model/joins.ts`, `Viewport3D`. Panel
|
||||
`src/panels/ObjectInfoPanel.tsx`; `src/state/selectionInfo.ts` `Selection.wall: WallInfo`;
|
||||
Host-Setter + `projectSlice` `updateWall`/`setWallThickness`. Einschichtig = 1-Layer-WallType.
|
||||
- **Panel-Text:** `.attr-*`/`.objinfo-*` Untertitel fett + Kontrast (`--label` statt `--muted`).
|
||||
- **Selektion-Überarbeitung:** `selectedDrawingId`→`selectedDrawingIds[]`
|
||||
(`selectionSlice`); Marquee wählt **Linien** (`marqueeHitDrawings`), **Ctrl/Cmd+A**,
|
||||
**Multi-Delete**, **Multi-Highlight** (`.plan-sel-draw`), **Shift-Klick-Mehrfachauswahl**
|
||||
(`PlanSelection.shift` → `onPlanSelect` toggelt).
|
||||
**Zwei Engine-Direktiven vom alten Stand fehlen** (`priority-based-joins.md`, `openings-as-booleans.md`): die in Punkt 2/3 beschriebenen Zielverhalten waren gesichert (u. a. „Trennlinien an Merge-Kontakten verschwinden immer", „Öffnungen dürfen nie wieder zu Segmenten vereinfacht werden"). Beim Neu-Beauftragen diese Direktiven wieder explizit festhalten.
|
||||
|
||||
**LÄUFT bei Übergabe (auf Resume verifizieren, NICHT doppelt bauen):**
|
||||
- **Editier-Welle 2 — Split/Join/Segment** (Agent): Ctrl+S Split (Rechteck→zwei
|
||||
geschlossene; nur bis Schnittpunkt), Ctrl+J Join, Alt+Klick=Segment löschen. Berührt
|
||||
`App.tsx`, `PlanView.tsx`, `kernel2d.ts` (`splitPolylineAtParam`/`splitClosedByChord`/
|
||||
`removeSegment`/`splitAtIntersections`/`joinChains`), `projectSlice.ts`, ggf.
|
||||
`src/editors/splitJoin.ts`. **Resume:** `git diff --stat`, `npx tsc -b`, `npm run build`,
|
||||
`node scripts/probe-splitjoin.mjs`.
|
||||
**Environment-Lücke auf diesem Gerät:** `node`/`npm`/`cargo`/`wasm-pack` waren zum Zeitpunkt dieser Übergabe **nicht auf dem PATH** (geprüft: kein Treffer, auch nicht unter der VSCodium-Flatpak-Datenlage). **Vermutliche Ursache: VSCodium lief als Flatpak** — Flatpak-Sandboxing blendet system-installierte Toolchains (Node/Rust unter `/usr/...` oder `~/.cargo`) typischerweise aus, auch wenn sie auf dem Host tatsächlich installiert sind. Der Nutzer wechselt gerade auf **natives VSCodium** (kein Flatpak mehr) — das könnte die Lücke von selbst beheben, MUSS aber nach dem Wechsel neu geprüft werden (`which node npm cargo wasm-pack`), bevor man von "Toolchain fehlt" auf "Toolchain neu installieren" schließt. Erst wenn nach dem Wechsel auf natives VSCodium immer noch nichts gefunden wird, wirklich neu installieren (Node LTS + Rust via rustup + `wasm-pack`, `npm install`, einmal `cargo build --manifest-path src-tauri/render3d/Cargo.toml` durchlaufen lassen). Ohne Toolchain ist die in `CONVENTIONS.md` vorgeschriebene Verifikation (`tsc`/`vitest`/`cargo test`/`wasm-pack`) nicht möglich — das zuerst klären, bevor einer der drei Slices neu beauftragt wird, sonst baut ein Agent blind ohne Verifikation.
|
||||
|
||||
**Offene Wellen (Todo-Spiegel — Todo-Liste lebt im Kontext):**
|
||||
1. Editier-Welle 3: **koinzidente Endpunkt-Griffe gemeinsam ziehen** + verbundene Enden
|
||||
propagieren beim Seiten-Verschieben (topologisch, das Tiefste).
|
||||
2. **GUI-Werkzeug ↔ Befehlszeile koppeln**: Werkzeug-Klick startet denselben Command →
|
||||
Werte unten eintippbar; danach **Cursor-Wert entfernen/optional**.
|
||||
3. **Selektions-Direkt-Edit**: Länge/Winkel des gewählten Elements im Befehlsfeld (Tab-Zyklus).
|
||||
4. **Swisstopo/openbureau-Importer-Dialog** (Ort+Radius). Fluss steht: Geocode
|
||||
`api3.geo.admin.ch/.../SearchServer` (sr=2056→LV95) → ±Radius → WGS84-bbox → STAC
|
||||
`data.geo.admin.ch/api/stac/v1`, Collections `ch.swisstopo.swissalti3d` +
|
||||
`ch.swisstopo.swissbuildings3d_3_0` → Download → TIN/Mesh→Kontext. LV95↔WGS84 nötig;
|
||||
**Browser-CORS** das Risiko. Referenz: DOSSIER `rhino/swisstopo.py`.
|
||||
5. **3D→2D-Dokument**: live + statisch (MVP-Kantenprojektion → HLR) für Vektor-PDF.
|
||||
Hinweis: `generatePlan` liefert SCHON Vektoren (SVG) → Plan-PDF bereits vektoriell.
|
||||
6. **Topbar-Rest iconisieren** (Zoom/Referenzlinien/Linien-Modus/Ressourcen).
|
||||
7. **Wand als Command** (+ Wandstärke-Tab nur bei einschichtig) + **Trim**.
|
||||
**Aufräum-Hinweis:** `Edit MEMORY md Added 1.txt` (lag unversioniert im Repo-Root) und die lokale `package-lock.json`-Änderung waren dirty/untracked. Die txt-Datei gehört nicht ins Repo — außerhalb verschieben oder löschen. `package-lock.json` prüfen (`git diff package-lock.json`) und nur committen, falls die Dependency-Änderung beabsichtigt ist.
|
||||
|
||||
**Resume-Checkliste:** 1. CONVENTIONS.md + diesen HANDOVER + Memory lesen. 2. `npx tsc -b` &
|
||||
`npm run build` (grün?). 3. `npm run dev -- --port 5187 --strictPort` (User schaut :5187).
|
||||
4. Split/Join-Stand prüfen (git diff, Probe). 5. Arbeitsweise: **autonom, keine
|
||||
Rückfragen** (User hat Vollrechte), substanzielle Arbeit an Subagenten delegieren,
|
||||
jede Änderung per `tsc`+`build`+Screenshot verifizieren. `node scripts/probe.mjs`.
|
||||
### Empfohlene Reihenfolge zum Wiederaufsetzen
|
||||
1. Nach dem Wechsel auf natives VSCodium zuerst `which node npm cargo wasm-pack` prüfen — evtl. war es nur die Flatpak-Sandbox, die die Toolchain versteckt hat. Nur falls immer noch nichts gefunden wird: Node + Rust + wasm-pack installieren + `npm install`. Blocker für alles Weitere.
|
||||
2. Locked-Iso-Fix neu bauen (klein, unabhängig, Spezifikation im Transkript vollständig vorhanden).
|
||||
3. Joins Phase 1c (Nutzer-Priorität dieser Session) — Direktive zuerst wieder ins Memory schreiben, dann Agent mit dem Auftragstext aus dem Transkript beauftragen.
|
||||
4. Öffnungen als Boolean-Löcher + Deckentrim-nur-3D-Zusatz (dieselbe Rust-Crate wie Phase 1c — nacheinander, nicht parallel, wegen Datei-Overlap in `mesh.rs`/`toWalls3d.ts`).
|
||||
5. Repo aufräumen (txt-Datei raus, `package-lock.json` klären), danach diesen Abschnitt auf „erledigt" aktualisieren.
|
||||
|
||||
---
|
||||
## NACHTRAG Session 3 (2026-07-04 mittags) — Prioritaets-Joins + Schnitt-Sanierung
|
||||
|
||||
## >>> STAND 2026-06-30 (Über-Nacht-Run) — älter <<<
|
||||
Großer autonomer Build-Lauf. Alles unten ist tsc + build GRÜN und per Screenshot
|
||||
verifiziert (sofern nicht „läuft" markiert). Befehlssystem-Bauplan:
|
||||
`docs/design/rhino-command-system.md`.
|
||||
**Prioritäts-Join-Epic (Kern-Direktive, siehe unten „Verschneidungs-Zielmodell"):** Joins werden nach `Component.joinPriority` materialbewusst aufgelöst — konsistent in Grundriss, Schnitt UND 3D. Gelandet:
|
||||
- `e1698b7` **Wand-T-Verbindungen** (T-Knoten 3 Enden + Mittelspannen-Stoss) in `computeJoins`; generatePlan wendet die Cuts unverändert an. `0ddf99d` Rust-Parität (geometry-Crate). `d74dda1` Sample: Querwand **W9** (neuer Wandtyp `iw` Innenputz/Backstein/Innenputz) als T-Demo, Tür/Fenster beiseite gerückt.
|
||||
- `fc0a76f` **Phase 1 materialbewusst**: `resolveJoinPriority` (merge/coexist/trim), `WallCuts.layerCuts` (per-Schicht-Cuts + L-Seitenlinien), Backstein-Kern verschmilzt, Putz bricht als L. `bf97d50` **Phase 1b**: Kern sticht nur durch den Nah-Putz und stoppt an der Rückgrat-Nahfläche (mergeCut bei `offT + sign*(tT/2 − nearPlaster)`), KEINE Achs-Überlappung; Naht entfällt über stroke:none/noStrokeEdges/Zeichenreihenfolge. Rust jeweils synchron.
|
||||
- `e4b8df6` **Phase 3 (3D)**: Wand wird an der Deckenunterkante getrimmt, wenn eine Decke höherer joinPriority bündig aufliegt (`trimWallTopForCeilings` in toWalls3d, BBox-Footprint-Näherung dokumentiert) → kein Z-Fighting Wand/Decke.
|
||||
|
||||
**Fertig & verifiziert:**
|
||||
- **Rhino-Befehlssystem (Tier 0 + Tier 1 Zeichnen):** `src/commands/` — `types.ts`
|
||||
(Command-Interface), `engine.ts` (State-Machine + lastCommand + Eingabe-Routing),
|
||||
`parseInput.ts` (`5,3`·`r5,3`·`5<45`·`<45`·nackte Zahl), `registry.ts` (+Aliase
|
||||
`l/pl/rec/c`), `cmds/{line,polyline,rect,circle}.ts`. UI: `src/ui/CommandLine.tsx`
|
||||
(über der Statusleiste, Tab fokussiert). Befehle: **Line, Polyline (Close/Undo),
|
||||
Rectangle, Circle** — alle mit getippten Koordinaten, per Probe gezeichnet.
|
||||
Koexistenz mit Alt-Tools (Werkzeugleiste). `scripts/probe-command-line.mjs`,
|
||||
`probe-draw-commands.mjs`.
|
||||
- **Tab-Feld-Zyklus** (Rhino-Präzision §2.7): beim Zeichnen Tab durch Felder
|
||||
(Linie/Polylinie: Länge→Winkel; Rechteck: Breite→Höhe; Kreis: Radius). Zahl lockt
|
||||
das aktive Feld, ungelockte folgen der Maus. Additive optionale Command-Methoden
|
||||
`fields()`/`pointFromFields()` + Engine-Feldzustand + CommandLine-Boxen. Verifiziert
|
||||
exakt (3 m/45°, Rechteck 4×2). `scripts/probe-tab-fields.mjs`.
|
||||
- **Tier-1-Editierbefehle:** **Move** (Auswahl verschieben), **Copy** (wiederholend
|
||||
duplizieren), **Offset** (parallele Kurve via `kernel2d.offsetPolyline`, persistente
|
||||
Distanz, Seite per Klick) — `cmds/{move,copy,offset}.ts`, Aliase `m/cp/o`. `CommandContext`
|
||||
um `selection` erweitert (App `host.context()` füllt sie aus dem Store). Offset verifiziert
|
||||
(paralleler Versatz exakt 0.5 m). `scripts/probe-offset2.mjs`.
|
||||
- **2D-Geometrie-Kernel** `src/geometry/kernel2d.ts` — Offset/Trim/Extend/Fillet +
|
||||
Segment-/Linien-/Kreis-Schnitt + Fläche/Wicklung. 16/16 Unit-Checks
|
||||
(`scripts/test-kernel2d.ts`, via `npx esbuild … | node`). Basis für Tier-1-Editierbefehle.
|
||||
- **Kontext/Gelände Phase 1** (paralleler Strang): `src/io/dxfParser.ts` (DXF: 3DFACE/
|
||||
MESH/POLYLINE→Mesh, LWPOLYLINE/LINE→Konturen; DWG bewusst nur via ODA→DXF-Hinweis),
|
||||
`src/model/terrain.ts` (`generateTerrainFromContours` → TIN via delaunator),
|
||||
`src/state/siteSlice.ts` (`addContextObject`/`generateTerrain`/…), `Project.context`
|
||||
(ContextObject = ImportedMesh|ContourSet|TerrainMesh, „dumme" Kontext-Geometrie, NICHT
|
||||
semantisch), `Viewport3D` rendert Kontext/Terrain. Deps `dxf-parser`+`delaunator`.
|
||||
- **Echte Isometrie (Orthographic-Kamera):** `Viewport3D` schaltet front/top/side/iso auf
|
||||
OrthographicCamera (parallele Projektion), Perspektive bleibt perspektivisch. Verifiziert.
|
||||
- **Plan-Tinte-Fix:** 2D-Linien waren unsichtbar (CSS `.draw2d{stroke:var(--ink)}` hell auf
|
||||
hellem Papier). CSS-stroke entfernt + dunkler Fallback in generatePlan → 2D dunkel sichtbar.
|
||||
- **Bugfixes:** 2D-Füllungen (rect/polyline rendern Fill+Schraffur+`fillColor`); Kanten-Griff-
|
||||
Dreiecke sitzen auf der Linie + inkrementelles Dragging (keine Akkumulation); Plan-View
|
||||
wandert nicht mehr mit (fixer Welt-Ursprung, Reset nur bei Geschoss-Wechsel via `resetKey`).
|
||||
**Schnitt-Sanierung (Regressionen aus der 3D-Schicht-Umstellung `de7a26b` behoben):**
|
||||
- `da39f0c` per-Schicht-3D-Bänder nutzten die RECHTE Normale statt `leftNormal` → Schichten in 3D+Schnitt gespiegelt (Backstein aussen). Eine Zeile in `pushSegment` gedreht.
|
||||
- `6c47355` Schnitt bekam durch die per-Schicht-Boxen segments×layers Cut-Polygone → Owner-Index-Versatz → einheitliches Diagonal-Muster. Fix: `projectToModel3d(project, { layeredWalls:false })` für `computeSection` (Voll-Box pro Wand); 3D-Viewer behält Default true.
|
||||
- `5ed3549` **Alle Schraffuren monochrom**: HATCH_INK `#0f0f0f` / HATCH_PAPER `#f0f0f0` (resolveHatch erzwingt Ink; Albedo-Fallback im Schnitt entfernt). Print-Mono separat/unangetastet.
|
||||
- `c36a7e0` Wandrelative Muster drehen im Schnitt mit der Wand (`SECTION_WALL_AXIS_ANGLE_DEG=90` an resolveHatch) → Dämmung horizontal quer zur Dicke.
|
||||
|
||||
**LÄUFT gerade (Agent):**
|
||||
- **Tab-Feld-Zyklus** (Nutzer-Wunsch): beim Zeichnen Tab durch Felder (Länge→Winkel→
|
||||
Breite/Höhe/Radius), Zahl lockt Feld, Rest folgt Maus. Additive optionale Command-Methoden
|
||||
`fields()`/`pointFromFields()` + Engine-Feldzustand + CommandLine-Boxen.
|
||||
**Sonstiges:** `1855717` Renderer-Umschalter aus der Statusleiste in die **Settings**, Default **Nordstern** (WebGL2 nur explizit/Fallback). `32497bc` TopBar: Zoom-Cluster von Grid auf Flex (Phantom-Zeile +6px weg, jetzt bündig mit Zweizeilern), Bar 56→60px.
|
||||
|
||||
**NÄCHSTE SCHRITTE (Reihenfolge):**
|
||||
1. Tab-Feld-Zyklus verifizieren (Agent-Gate).
|
||||
2. **Tier-1-Editierbefehle:** Move/Copy/Offset(nutzt kernel2d)/Trim/Split/Join/Explode.
|
||||
Undo/Redo-Stack im Store erwägen (Commits laufen über `setProject`).
|
||||
3. **Terrain-Integration:** Konturen im Plan (generatePlan/PlanView), Import-/Terrain-Befehle
|
||||
+ Site-Panel (`src/panels/`) + i18n + DWG→DXF-Hinweis-UI; `.dxf`-File-Picker → `parseDxf`.
|
||||
4. **Wand-Stärke-Feld:** Wand ist noch ein Alt-Tool — als Command portieren, dann Tab-Feld
|
||||
`thickness` NUR bei einschichtigen/freien Wänden (mehrschichtige Typen: Stärke vorgegeben).
|
||||
5. Tier 2 (Rotate/Scale/Mirror/Arc/Fillet/Array/Group/Gumball2D), Tier 3 (ExtrudeCrv/Box/
|
||||
PushPull + analytische Wand-Öffnungen, §3.0 Bauplan).
|
||||
**IN FLIGHT bei Checkpoint:** (1) **3D-Live-Schnitt** (Opus-Agent): Clip-Plane-Uniform + discard in MESH/GRID_WGSL, `section_fill.rs` Cut-Caps aus `cut_section` mit prozeduraler 45°-Schraffur (CAP_WGSL), `set_section_plane` (web.rs cached walls/slabs), Viewport-Toggle; WASM-Rebuild nötig. (2) **Joins Phase 2** (Opus-Agent): Merge-Regel (gleiche Prio+Komponente) in der Schnitt-Dominanz `subtractDominantBands`. — `git status` prüfen, verifizieren, committen.
|
||||
|
||||
**Gotchas (frisch gelernt):**
|
||||
- Ein Prozess-Neustart killt laufende Hintergrund-Agenten (Status `failed`); nach
|
||||
jedem Wiederaufnehmen `src/commands/`-Existenz + `npx tsc -b` prüfen, Abgestürztes neu starten.
|
||||
- Inline `{...s, feld}` als Tupel-Return löst TS-Excess-Property-Check aus → erst typisierte
|
||||
Variable (siehe cmds/line.ts-Muster).
|
||||
- Ein Subagent ist bei diesem Befehls-Build geflaket (spawnte Research statt zu bauen) →
|
||||
verzahnte/quer schneidende Arbeit an einen fähigen Subagenten delegieren oder selbst machen.
|
||||
**NÄCHSTER Engine-Slice (Nutzer-Direktive, queued):** **Öffnungen als echte Boolean-Löcher** — Fenster/Türen sind heute Pfeiler/Brüstung/Sturz-Ersatzkörper (keine CSG, Triangulator lochfrei) → sichtbare Segment-Nähte. Ziel: EIN Wandkörper, Löcher via earcut-with-holes + Laibungsflächen; behebt auch die dokumentierte mesh↔cut_section-Öffnungs-Inkonsistenz. Läuft im selben Crate wie der 3D-Schnitt → erst danach starten.
|
||||
|
||||
---
|
||||
**Verschneidungs-Zielmodell (Nutzer, verbindlich):** gleiche Komponente+Priorität = verschmelzen (Naht weg); schwächere Schicht bricht auf und schliesst als L; höhere Priorität schneidet bauteilübergreifend (Decke>Wand); Joins IMMER in allen drei Sichten identisch. Schraffur-Modell: Schnitt-Schraffur vs. Oberflächen-Schraffur, aktuell ALLE monochrom fg #0f0f0f auf bg #f0f0f0.
|
||||
|
||||
## Was das ist
|
||||
Standalone-**Browser-BIM** (React+TS+Vite+Three.js) für Wohnbau — die Browser-Variante
|
||||
des Rhino-Plugins **DOSSIER** (Referenz-Repo, public/klonbar: https://git.kgva.ch/karim/DOSSIER;
|
||||
falls weg: neu klonen nach `scratchpad/DOSSIER`). Prinzip: **ein semantisches Modell →
|
||||
alle Sichten (Plan/3D/Schnitt) abgeleitet**, Darstellung erst beim Rendern.
|
||||
## NACHTRAG Session 2 (2026-07-04 nachmittags/abends) — TopBar-Redesign + 3D-Editierbarkeit
|
||||
|
||||
## Aktueller Stand (tsc + build GRÜN)
|
||||
Funktioniert & verifiziert: semantisches Modell · **mehrschichtige Wände** mit
|
||||
**L-Ecken-Gehrung** · Tür mit Öffnung + Schwenkbogen · **Dokumentmodell** (Zeichnungsebenen
|
||||
= Geschosse+Schnitte/Ansichten/Zeichnungen; Ebenen = Kategorie-Baum, DOSSIER-Codes 1:1) ·
|
||||
**3D zeigt EG+OG gestapelt** · **Resource-Manager** (Component/Hatch/Line, als Fenster-Overlay) ·
|
||||
**Panel-System** (Docks links/rechts, Tabs, 5 Anzeige-Modi) · **Top-Bar + Footer** (Massstab
|
||||
echt 1:N, Detailgrad, Render-Modus Schattiert/Draht/Kanten, Referenzlinien, Cursor X/Y live) ·
|
||||
**Maus:** Mitte=Pan/Orbit, Links=Auswahl(+Marquee), Rechts=eigenes Kontextmenü · **Native-App**
|
||||
(kein Browser-Rechtsklick/Textauswahl) · **i18n** (`src/i18n/`, de+en, `t('key')`) ·
|
||||
**aktive Zeichenwerkzeuge** (Wand/Linie/Polylinie/Rechteck + Snapping endpoint/midpoint/
|
||||
intersection/onEdge/grid/ortho mit Fang-Menü, Live-Vorschau+HUD — `src/tools/`, Phase 1+2).
|
||||
**TopBar/UI gelandet:** DOSSIER-Wortmarke + Ressourcen-Icon links (Rhino-Stil), Massstab/Zoom-Cluster bereinigt (Duplikat weg, Zoom-Aktionen unter Massstab-Dropdown gestapelt, gleiche Breite, Ring bei „am Massstab" statt Flächen-Aufhellung), PDF/DXF raus → **Datei-Burger-Menü** rechts (Speichern/Öffnen als echter Projekt-JSON-Download/-Upload, Import, Export). **Einstellungs-Fenster** (`SettingsDialog.tsx`): Accent-Palette (`src/theme/accents.ts`), Auswahlrahmen-Farbe (Default Yuyake #CEB188) + Snap-Farbe (vorläufig **Sora #5FA1C9** gewählt — „aki"-Rückfrage bleibt offen, per Picker änderbar), Projekt-MüM-Feld (`project.referenceElevationMasl`, nur gespeichert, Draping folgt). **Custom Fenstersteuerung** (_/□/X) für Electron (`electron-preload.cjs` + IPC, `WindowControls.tsx`, TopBar = Drag-Region). **Dark-Theme ist jetzt fester Standard** (vorher an OS-`prefers-color-scheme` gekoppelt → zeigte fälschlich Light; Light bleibt Opt-in via `:root[data-theme="light"]`). Alle TopBar-Pillen auf dieselbe stets-dunkle Kontextmenü-Fläche vereinheitlicht; Panel-Köpfe flach (kein hellerer Balken) + feste 40px-Höhe. **Undo/Redo** (`historySlice.ts`, Ctrl+Z/Y, Coalescing für Drags). **15 gebündelte OFL-Schriften** (`public/fonts/`, `src/text/fonts.css`) statt Systemfonts. **Farbfelder**: Hex separat editierbar (`ColorHexField.tsx`). **Text-Zeilenhöhe-Regler** ersetzt „+ Text" (Text wird eigenes Werkzeug, AUDIT B1). **Linienstil** im Attribute-Panel editierbar. 2D-Zoom-Grenzen stark erweitert (20000×/0.005×).
|
||||
|
||||
### Gerade fertig & verifiziert (2026-06-29)
|
||||
**Aktive Zeichenwerkzeuge — Phase 1 + 2** (`docs/design/drawing-tools.md`): Tool-System in
|
||||
**`src/tools/`** (types · snapping · tools/registry) + Verdrahtung in PlanView/App/TopBar/
|
||||
generatePlan. Per probe verifiziert (`scripts/probe-tools.mjs`, `probe-line.mjs`, `probe-phase2.mjs`):
|
||||
- **Werkzeugleiste** (TopBar): Auswahl/Wand/Linie/**Polylinie/Rechteck** + Wandtyp-Dropdown +
|
||||
**Fang-Menü** (Häkchen je Snap-Art, Rasterweite, Winkelraster — `position:fixed`, da Topbar clippt).
|
||||
Nur im Grundriss aktiv.
|
||||
- **Wand-Werkzeug**: Achs-Polylinie → je Segment ein `Wall`; Live-Band-Vorschau + HUD (Länge·Winkel);
|
||||
Rechts-/Doppelklick/Enter committet; **Gehrung automatisch aus `computeJoins`** (mehrschichtiger L-Stoss).
|
||||
- **2D-Werkzeuge**: Linie (2-Klick), **Polylinie** (Klick auf Start schließt, Doppel-/Rechtsklick beendet
|
||||
offen), **Rechteck** (zwei Ecken) → `Drawing2D` (line/polyline/rect); in `generatePlan` abgeleitet
|
||||
(`addDrawing2D`, `color` am line-Primitiv, Bounds erweitert).
|
||||
- **Snapping**: endpoint · midpoint · intersection · onEdge(Lot) · grid · ortho(Shift); Prioritäts-
|
||||
gewichtet (`PRIORITY` in snapping.ts); Bildschirm-Marker je Art (Quadrat/Dreieck/✕/Raute/Punkt) +
|
||||
Ortho-Hilfslinie; Ctrl = Fang aus. Einstellbar über das Fang-Menü (`snap` State in App).
|
||||
- **Tastatur**: Esc verwirft/zurück-zu-Auswahl, Enter committet, Backspace nimmt Punkt zurück.
|
||||
- Modell: `Drawing2D` + `Project.drawings2d` (types.ts), `Element` erweitert. i18n de+en (`tool.*`,`snap.*`).
|
||||
- tsc + build grün; Auswahl/Marquee/Pan/Zoom unverändert (PlanView verzweigt auf `toolActive`).
|
||||
**3D-Editierbarkeit gelandet (Nordstern/render3d, NICHT three.js — Free=three.js-Viewer, Premium=editierbarer Nordstern, siehe Memory):**
|
||||
- **R1** `85a721f` Boden-Referenzraster auf y=baseElevation des aktiven Geschosses, ein-/ausschaltbar (Overlay-Button, `viewSlice.grid3dVisible`; eigene LineList-Pipeline `grid.rs`, `set_ground_grid`). Kamera: Links=Orbit, **Mitte/Rechts=Pan** (vorher Pan nur auf Shift+Mitte versteckt), Rad=Zoom-to-Cursor.
|
||||
- **R2** `6ef57b6` View-Styles **wireframe** + **hidden-line** + Fill-Light (`set_render_style`, `edges.rs` = Feature-Edge-Extraktion mit Crease-Dedup, Hidden-Line via Depth-Bias-Flächen + Kanten obenauf). `textured` → vorerst wie shaded (keine Textur-Pipeline).
|
||||
- **R3** `ac0d9ef` **Klick-Auswahl** im 3D (`raycast3d.ts`: OBB-Wand-/Prisma-Slab-Schnitt → Store-Selektion → Attribute+Objekt-Info-Panel). `toWalls3d.pickGeometry()` mit wallId/ceilingId.
|
||||
- **R4** `fabf97a` **Auswahl-Highlight**: orange Outline, immer sichtbar (No-Depth-Linien-Pipeline `set_highlight_lines`, `selectionHighlightLines()`).
|
||||
- **R5** `02bd261` **Materialfarben** im 3D: Wände/Decken in Farbe der dicksten Schicht statt Einheitsgrau.
|
||||
- **weiter:** `8bc472a` 2D **z-Anordnen** (Kontextmenü nach vorn/hinten, `reorderDrawings`); `de7a26b` **per-Schicht Wand-Bänder** (jede Materiallage als eigener farbiger Quader, wallId bleibt → Pick/Highlight ganze Wand); `e42c336` **AUDIT A6 Bauteil-Schedule-CSV** (Datei-Menü, `exportSchedule.ts`); `7dd324d` **Glasscheiben in Fenstern** (A5-Teil, RMesh in jeder Fensteröffnung).
|
||||
- Alle Runden **visuell im Browser verifiziert** (Playwright + WebGPU-Flags: `--enable-unsafe-webgpu --enable-features=Vulkan --use-gl=angle --use-angle=vulkan --no-sandbox`, URL `?engine=wasm`, Iso-View klicken).
|
||||
|
||||
**Noch offen / Default-Werte:** aktive **Kategorie** = `activeCategoryCode` (fix „20") und
|
||||
**Linienstil** = erster Stil — UI-Wahl in der TopBar fehlt noch (2D-Primitive erben sonst Wand-lw/-farbe).
|
||||
**3D-REST (engine-schwer, bewusst NICHT blind gemacht — mit Nutzer angehen):**
|
||||
- **Schnitt-Schraffuren** (2D-Poché/Schraffur auf 3D-Schnittflächen; `section.rs`, ~66KB, komplex).
|
||||
- **Textur-/PBR-Pipeline** (Sampler/Bindings/UV in wgpu; dann `textured`-Style + `Component.texture3d`/Material echt rendern).
|
||||
- **Wand-Schicht-Bänder** in 3D (Option B aus R5: jede Materiallage als eigener extrudierter Teilquader statt nur repräsentativer Farbe — pro-Schicht-Farben liegen in `RWall.layers` schon vor).
|
||||
- **Ortho-Ray-Picking** (aktuell perspektivischer Pick-Strahl; für Front/Top/Side-Presets vor der ersten Navigation leicht ungenau).
|
||||
- **3D-Griffe/Editieren** (Wand-Endpunkte im 3D ziehen etc. — bisher nur Auswahl + Panel-Edit).
|
||||
|
||||
**Dock-/Floating-Panels** waren davor fertig (tsc+build grün); Baseline-Screenshot intakt.
|
||||
WASM-Workflow: `src/engine/pkg3d/` ist gitignore't → nach Rust-Änderung `npm run build:engine3d`, nur die `.rs` committen. DOSSIER-Audit-Details jetzt in `docs/design/dossier-feature-audit.md` (A1–A6/B1–B4/C/D/E) + `ROADMAP.md` §11.
|
||||
|
||||
### Ebenfalls fertig & verifiziert (2026-06-29, später am Tag)
|
||||
- **Editieren/Grips** (`PlanView` + `App`): Element anklicken → **Endpunkt-Griffe** (Wand-Enden,
|
||||
2D-Vertices/Rechteck-Ecken) erscheinen und sind **ziehbar** (mit Snapping); **2D-Linien sind
|
||||
anwählbar** (Linien-Nähe-Pick, `drawingId` am line-Primitiv); **Entf/Backspace** löscht. Wand-
|
||||
Gehrung folgt live. `moveGrip`/`drawingVertices` in App.
|
||||
- **Shift = Ortho** beim Griff-Ziehen (Bezug = Nachbar-Vertex; H/V bzw. Winkelraster).
|
||||
- **Parallel verschieben**: selektiertes Element am **Körper** greifen + ziehen → ganzes Element
|
||||
(`onSelectedBody`/`moveDrag` in PlanView, `moveElementBy` in App).
|
||||
- **Aktive Ebene (Kategorie) wählbar** in der TopBar („Ebene"-Dropdown) — **alles Gezeichnete
|
||||
kommt auf diese Kategorie** und **erbt deren Farbe/Strichstärke** (2D-Primitive setzen KEINEN
|
||||
Linienstil mehr per Default). Statusleiste zeigt die aktive Ebene. `activeCategoryCode` State.
|
||||
- **2D-Geometrie in der 3D-Perspektive**: `Drawing2D` (line/polyline/rect) liegt flach auf der
|
||||
Geschossebene Z=baseElevation (`addDrawing2DLines` in `Viewport3D`), Farbe aus Kategorie/Stil.
|
||||
(Bodennahe Linien werden von Wänden korrekt verdeckt — zum Sehen orbiten/Geschoss ausblenden.)
|
||||
- Probes: `probe-grips.mjs`, `probe-3d2d.mjs`, `probe-edit2.mjs`. tsc + build grün.
|
||||
## Konventionen (ZWINGEND — zuerst lesen)
|
||||
- **Keine Fremd-Tool-Spuren** im Repo: vor jedem Commit Trace-Scan (`git diff | grep -iE "co-authored|generated with"`) → muss leer sein. Commits deutsch, sachlich, KEINE Co-Authored-By-Trailer.
|
||||
- **NICHT ANFASSEN (fremde WIP):** `.gitignore`, `public/assets/materials/manifest.json`, `src/materials/library.ts`, `scripts/fetch-materials.mjs` — bleiben dauerhaft dirty, nie stagen.
|
||||
- **Verifikation:** `tsc --noEmit` clean + `vitest run` + trace-scan vor jedem Commit. Nur Task-Dateien stagen (Kollisionen: parallele Tasks disjunkte Datei-Lanes geben; i18n exklusiv einem Task).
|
||||
- **Engine = „Nordstern"** (render2d/render3d/WASM). Referenz DOSSIER-Rhino: `https://git.openbureau.ch/karim/dossier` (Alias git.kgva.ch), geklont `/tmp/dossier-ref`.
|
||||
|
||||
### Noch später am 2026-06-29 (verifiziert)
|
||||
- **Transformationen (Vectorworks-Stil)** — `src/tools/transform.ts` + `src/ui/TransformBar.tsx` +
|
||||
App. Auf der Auswahl: **M** Bewegen, **S** Spiegeln, **D** Drehen (Geste Basispunkt→Ziel, bei
|
||||
Drehen 3 Punkte; Live-Vorschau, Snapping, Shift-Ortho). **Modusleiste U/I/O/P**: U bewegen ·
|
||||
I Kopie · O N Kopien (Anzahl-Prompt) · P verteilen. Probe `probe-transform.mjs`/`probe-array.mjs`
|
||||
(move/copy/array verifiziert). Routing via `toolInputActive`-Prop in PlanView.
|
||||
- **Aktive Ebene per Klick im Ebenen-Panel** (statt TopBar-Dropdown): `LayersPanel`-Zeile klicken →
|
||||
`host.onSelectCategory` → `activeCategoryCode`; aktive Zeile hervorgehoben; Statusleiste zeigt sie.
|
||||
TopBar-Ebene-Dropdown entfernt. (probe-layer.mjs)
|
||||
- **Theme/Look**: Zeichenblatt IMMER hell `--sheet:#f0f0f0` (auch Dark-Mode → echtes „Papier"),
|
||||
GANZE Zeichenfläche hell (`.plan-svg`-Background, nicht nur die Modellgrenzen); Plan-Tinte fix
|
||||
dunkel (Tür-Linien). Dark-Theme etwas abgedunkelt (Panels #1c1c1c). **3D-Orbit ohne Nachlauf**
|
||||
(`enableDamping=false` — kontrollierter).
|
||||
## In dieser Session gelandet (Auswahl Commits)
|
||||
Schraffur/Linien-Epic komplett: Typmodell (vector/image Hatch, dash/zigzag/custom Line, Component Vordergrund/Hintergrund), ResourceManager **Master-Detail** (Schraffuren/Linien/Wandstile/Deckenstile, `9695748`), Rendering neuer Typen (Bild-`<pattern>`, random modellraum-verankert, Zickzack, Custom-Motiv-Editor `src/ui/MotifEditor.tsx`), modulares Linien-Segment-System (Strich/Punkt/Lücke), Attribut Vordergrund/Hintergrund + **By-Layer/By-Object-Quellen** (Nach Ebene/Bauteil/eigener Wert für Vordergrund/Hintergrund/Strichstärke/Schraffur, `d02781e`).
|
||||
Weiter: Gehrung spitze Winkel (2D+Schnitt+Rust), TopBar/Footer-Reorg, Zoom-% am Massstab, Tool-Shortcuts 1..0, Verschiebe-Dreieck flacher + Snap, Statusleiste „Nordstern", ResourceManager als **floating nicht-modales Fenster**, **swisstopo** Gebäude(extrudiert)+Terrain-Mesh in 3D (`f22970f`), Schnitt-Wandschicht-Orientierung (`7569524`), **Schnitt-Boolean-Dominanz** nach joinPriority (`23d81be`).
|
||||
|
||||
### Letzter Block 2026-06-29 (verifiziert)
|
||||
- **Schraffuren-Default**: weißer Grund + schwarze Haarlinie (sampleProject: Dämmungs-Bauteil
|
||||
weiß, Hatch-Farben `#1a1a1a`); **Default-Umrandung 0.18 mm** (`WALL_FALLBACK_MM`, addCategory).
|
||||
- **Stiftstärken-Vorgabe** `PEN_WEIGHTS` (0.02…2.0) als `<datalist>` im Linienstil-Editor.
|
||||
- **Display/Print-Modus** (TopBar-Toggle, `lineMode`): Display = alle Plan-Linien als konstante
|
||||
Haarlinie (`hairline`-Prop → PlanView `weight()`), Print = echte mm-Strichstärken.
|
||||
- **`.lin`/`.pat`-Import**: Parser `src/io/linParser.ts`/`patParser.ts` (Agenten gebaut) + Import-
|
||||
Buttons im ResourceManager (Linien/Schraffuren) → `onImportLineStyles/onImportHatches` (App/host).
|
||||
.lin voll (Dash); **.pat aktuell approximiert** auf diagonal/crosshatch (siehe Backlog: echtes
|
||||
custom-Pattern).
|
||||
- **Werkzeug-Palette** (`src/panels/ToolsPanel.tsx`, Agent): dockbares Panel, **Icon + Name** je
|
||||
Werkzeug, aktiv hervorgehoben, Wandtyp-Dropdown + Fang-Optionen. Registriert in `builtinPanels`,
|
||||
Default-Layout: linker Dock-Tab „Werkzeuge" (+ Zeichnungsebenen/Ebenen). **`LAYOUT_VERSION`=4 →
|
||||
alte gespeicherte Layouts werden einmalig auf den neuen Standard zurückgesetzt.**
|
||||
- Probes: `probe-display.mjs`, `probe-import.mjs`, `probe-tools-panel.mjs`. tsc + build grün.
|
||||
## Backlog & Rückfragen → `PENDENZEN.md`
|
||||
Der frühere „Offener Backlog"/„Offene Rückfragen"/„IN FLIGHT"-Abschnitt ist vollständig nach `PENDENZEN.md` migriert (Single Source of Truth). Dort steht auch das Arbeitsprotokoll.
|
||||
|
||||
## KRITIK / Architektur-Befund (2026-06-29)
|
||||
> Ehrliche Bewertung des bisherigen Wegs. Für die nächste Instanz als Entscheidungsgrundlage.
|
||||
|
||||
**Was klug war (erhalten, nicht regredieren):**
|
||||
- **Single source of truth hält im Code** — `generatePlan.ts` *und* `Viewport3D.tsx` nutzen
|
||||
dieselbe `computeJoins`/`clippedBand` aus `src/model/`. Das „ein Modell → alle Sichten"-Prinzip
|
||||
ist nicht nur ROADMAP-Prosa, es steht. Beim Refactor diese Trennung Modell↔Sicht bewahren.
|
||||
- **Grundriss analytisch** aus Parametern statt Mesh-Schnitt (Weg A) — richtig.
|
||||
- **Bewusst leichter Stack** (Three.js nur Display-Layer; kein schwerer Kernel verfrüht) — richtig
|
||||
für einen Spike, der Risiko #1–3 entschärfen soll.
|
||||
- i18n via `t()` wird eingehalten; `tsc -b` ist grün.
|
||||
|
||||
**Hauptkritik (zu beheben):**
|
||||
1. **God-Component gegen eigene Regel.** `App.tsx` ist **~2461 Zeilen mit ~29 `useState`**.
|
||||
CONVENTIONS.md fordert wörtlich „dünner Shell, keine Geschäftslogik". Das ist verletzt.
|
||||
2. **Kein Store.** `src/state/` existiert NICHT, obwohl CONVENTIONS.md Store+Slices
|
||||
(project/selection/view/layout) vorschreibt. Zustand = lokale Hooks → Prop-Drilling.
|
||||
- Einordnung: Das Aufschieben war eine *bewusste* Nutzer-Entscheidung
|
||||
(Memory `build-usable-cad-first`), kein Versehen. **Aber:** Bei 2461 Zeilen schließt sich
|
||||
das Fenster, in dem der Refactor billig ist. Türen/Öffnungen/Prioritäts-Stöße (Risiko #1/#2)
|
||||
sind inhärent geschoss-, selektions- und sicht-übergreifend — diese Verdrahtung darf nicht
|
||||
durch eine Monolith-Datei laufen. **Empfehlung: Store ziehen VOR der nächsten Feature-Welle.**
|
||||
|
||||
**Befund (separat angehen, nach Refactor):** Die harten, roadmap-markierten 🔴-Risiken sind noch
|
||||
unbewiesen — `Door` existiert als Typ, aber **kein echter 3D-Boolean** (Öffnung = nur Plan-Lücke);
|
||||
**HLR** für Schnitte/Ansichten fehlt; **Prioritäts-T-Stöße** offen. Das einfache Drittel ist bewiesen,
|
||||
das schwere aufgeschoben. Die Frage „ist es wirklich CAD?" entscheidet sich erst, wenn diese landen.
|
||||
|
||||
## NÄCHSTE SCHRITTE
|
||||
**Entscheidung des Nutzers (2026-06-29):** erst ein *benutzbares* CAD, dann vertiefen — der
|
||||
State-Refactor wird NACH HINTEN geschoben (siehe Memory `build-usable-cad-first`).
|
||||
> ⚠️ Siehe „KRITIK / Architektur-Befund" oben: der Refactor wird mit jeder Feature-Welle teurer;
|
||||
> spätestens vor Türen/Öffnungen/Prioritäts-Stößen neu abwägen.
|
||||
|
||||
### ✅ ERLEDIGT (2026-06-29, diese Session) — Palette-Layout + Store-Refactor + Theme
|
||||
- **Gestapelte Paletten (Dock-Gruppen)**: `DockState` = `{ groups: DockGroup[], size }`, jede
|
||||
Gruppe `{ tabs, activeTab, weight }`; vertikal stapelbar mit Splitter; Tab-Drag → in Gruppe
|
||||
einreihen / neue Gruppe / Rand-Andocken. `LAYOUT_VERSION=6`. (`types.ts`/`layout.ts`/`Dock.tsx`/
|
||||
`TabStrip.tsx`/`panelDrag.tsx`/`App.tsx`.) Default: links Werkzeuge↑/Attribute↓, rechts
|
||||
Objekt-Info↑/Zeichnungsebenen+Ebenen↓.
|
||||
- **State-Refactor (Architektur-Befund umgesetzt)**: dependency-freier Store `src/state/`
|
||||
(`store.ts` useSyncExternalStore + Slices `projectSlice`/`selectionSlice`/`viewSlice`/
|
||||
`layoutSlice`, kombiniert in `appStore.ts`). App.tsx 2710→~2030 Zeilen, verhaltensgleich
|
||||
(tsc+build+Screenshot identisch). **Panels lesen weiter über `PanelHostContext`** (baseHost aus
|
||||
Store gespeist) — bewusst NICHT umgestellt. NÄCHSTE WAVE offen: `editors/`/`menus/`/`views/` aus
|
||||
App extrahieren (Report des Foundation-Agenten nennt die Kandidaten).
|
||||
- **Selektions-Kontrakt** (`src/state/selectionInfo.ts` + host.ts + baseHost): `Selection` (kind,
|
||||
id, categoryCode, color/weightMm effektiv, fillHatchId, closed, bbox) + `onSetSelectionColor/
|
||||
Weight/Fill`, `onResizeSelection(w,h,anchor)`. Modell: `Wall.color?`, `Drawing2D.weightMm?`
|
||||
ergänzt; generatePlan wendet beide an. Wand = Weight/Fill bewusst No-op (erbt aus Ebene).
|
||||
- **Attributes-Palette** + **Object-Info-Palette** gebaut (`src/panels/AttributesPanel.tsx`,
|
||||
`ObjectInfoPanel.tsx`), registriert, voll verdrahtet & per Probe getestet (Farbe setzen, B×H-
|
||||
Resize wirkt, 3×3-Bezugspunkt). Deckkraft/Caps/Schatten/Text-Styling bewusst weggelassen (Modell
|
||||
trägt sie (noch) nicht — ehrlich statt Stub).
|
||||
- **Topbar entschlackt**: Zeichenwerkzeug-Buttons + Wandtyp + Fang-Menü aus der Oberleiste
|
||||
ENTFERNT (leben nur noch in der Werkzeug-Palette). `SnapMenu`/`SNAP_TOGGLES` aus `TopBar.tsx` raus.
|
||||
- **Dark-Theme vertieft** (`styles.css`): gestufte Elevations-Tokens (`--bg`<`--panel`<`--panel-2`,
|
||||
`--input` versenkt) statt flachem Einheitston; Oberleiste/Panel-Köpfe angehoben, tiefere Schatten.
|
||||
`--sheet` bleibt hell. Siehe Memory `ui-depth-dark`.
|
||||
- **CSS-Altlast behoben**: ein `*/` in einem Kommentar (`.nav-*/.res-*`) hatte die ganze `.dock`-
|
||||
Regel verschluckt (`display:flex` nie aktiv) — gefixt.
|
||||
|
||||
### Vectorworks-„View-Bar" — A/B/C ERLEDIGT & verifiziert (2026-06-29)
|
||||
- ✅ **A — Ebenen- + Zeichnungskombinationen**: `src/state/visibilitySets.ts` (localStorage
|
||||
`cad.layercombos.<name>` / `cad.drawingcombos.<name>`), Store-Actions `snapshot*/apply*Visibility`
|
||||
in `projectSlice`, zwei `ComboMenu`-Dropdowns in `TopBar` (Muster wie `LayoutMenu`). Round-Trip
|
||||
per Probe verifiziert. (Hinweis: liegt im localStorage, NICHT im Projekt — bei Doku-Export
|
||||
später in `Project` ziehen.)
|
||||
- ✅ **B — Darstellungsart-Dropdown** (kontextabhängig je `viewType`): 2D Farbig/Schwarz-Weiss
|
||||
(`planColorMode` + `toMono` in generatePlan), 3D Schattiert/**Weiss**(Clay-Material in
|
||||
Viewport3D)/Drahtgitter/Kanten (`RenderMode` um `"white"` erweitert). Ersetzt die alte
|
||||
Render-Modus-Buttongruppe. Screenshots bestätigt.
|
||||
- ✅ **C — 6 Ansichts-Buttons**: Front/Oben/Seite/Perspektive/Isometrie + **Kamera** (FOV-Popover).
|
||||
`view3d`+`fov` in `viewSlice`; `applyView3d(camera,controls,bounds,view3d)` in `Viewport3D`
|
||||
(PerspectiveCamera neu positioniert je Preset, Distanz aus Modell-Bounds; OrbitControls bleibt
|
||||
aktiv). Preset-Klick wechselt nötigenfalls in die Perspektive. Screenshots top/front/iso/persp
|
||||
klar verschieden. **Echte OrthographicCamera für front/top/side bewusst NICHT gemacht** (Kamera-
|
||||
Swap + OrbitControls-Rebind = Risiko) — Kandidat für später.
|
||||
|
||||
### Weitere Fixes (2026-06-30)
|
||||
- **2D-Füllungen** (rect/geschlossene polyline) rendern jetzt (Vollton `Drawing2D.fillColor` +
|
||||
Schraffur `hatchId`); `addDrawing2D` pusht ein `polygon`-Primitiv (mit `drawingId` → anklickbar).
|
||||
Attribute-Palette hat „Füllfarbe". (Kreis-Füllung offen — Kreis-Primitiv im Plan fehlt noch.)
|
||||
- **Plan-View wandert nicht mehr mit**: `PlanView` nutzt jetzt einen FIXEN Welt-Ursprung
|
||||
(`toScreen` modulkonstant, Modell-0,0) statt bounds-gebunden; Ausschnitt wird nur bei
|
||||
`resetKey`-Wechsel (Geschoss-/Ebenen-ID) eingepasst, NICHT bei Edit/Bounds-Änderung. Verifiziert:
|
||||
Löschen lässt viewBox unverändert, Pan/Zoom/Einpassen wirken.
|
||||
- **Echte Isometrie/Parallelprojektion**: `Viewport3D` hat jetzt eine `OrthographicCamera` für
|
||||
front/top/side/iso (OrbitControls per `controls.object`-Swap umgebunden), Perspektive bleibt
|
||||
`PerspectiveCamera`. Verifiziert (parallele Kanten).
|
||||
|
||||
### >>> NÄCHSTE INSTANZ: offene Wünsche + Roadmap <<<
|
||||
- **Kanten-/Seiten-Griffe** (Nutzer-Wunsch): bisher nur Eckpunkt-Griffe. Gewünscht: Seiten ziehen
|
||||
(z. B. Rechteck-Kante), mit dreieckigem Anfasser nach außen. Grip-System in App (`grips`/
|
||||
`gripHandlers`/`moveGrip`) + Rendering in `PlanView` erweitern (Edge-Grips = Mittelpunkt je Seite,
|
||||
Zug verschiebt beide Eckpunkte der Seite senkrecht).
|
||||
- **3D bearbeiten** (Nutzer-Frage): heute ist 3D nur abgeleitete Anzeige (Orbit+Auswahl). Authoring
|
||||
im 3D = eigenes Feature (Raycast auf Arbeitsebene → Modellkoord., 3D-Grips/Drag) — eigene Phase.
|
||||
### Text-Styling (View-Bar Rest) + Roadmap
|
||||
1. **Text-Styling** (Nutzer-Wunsch). VORAUSSETZUNG: **Text wird noch NICHT gerendert** —
|
||||
`Drawing2D` mit `geom.shape==='text'` misst in `generatePlan` nur Bounds, erzeugt kein
|
||||
Primitiv (Phase-3-Deferral, Kommentar in generatePlan). Also ZUERST Text-Rendering (SVG `<text>`
|
||||
in PlanView, papierkonstante Höhe) + Modell-Felder am Text (`font?`, `bold?`, `italic?`,
|
||||
`anchor?`), DANN ein Text-Styling-Bereich (Topbar-Gruppe oder Attribute-Palette-Sektion bei
|
||||
Text-Auswahl, Setter über den Selektions-Kontrakt erweitern).
|
||||
2. **Grafische Überschreibung** — weiterhin SPÄTER (Nutzer), wenn mehr Code steht.
|
||||
3. **Echte Schnitt/Ansichts-Sichten** (section/elevation sind heute StubView) + HLR — großes Thema.
|
||||
4. **Refactor-Rest**: `editors/`/`menus/`/`views/` aus App.tsx extrahieren (App noch ~2030 Z.).
|
||||
- **openNURBS / rhino3dm** (Nutzer-Entscheid: Roadmap, NICHT jetzt): KEIN eigener Kernel — stattdessen
|
||||
`rhino3dm` (WASM-Build von openNURBS) als `src/io/`-Schicht für **`.3dm`-Import/Export + NURBS**,
|
||||
sobald gebraucht. Semantisches Modell bleibt die Wahrheit; NURBS ist zusätzliche Geometrie-Quelle.
|
||||
- **Echtes custom-`.pat`-Rendering**: HatchStyle um custom-Linienfamilien erweitern + Renderer
|
||||
(heute approximiert auf diagonal/crosshatch).
|
||||
|
||||
### Offene Wünsche des Nutzers (Backlog, priorisiert)
|
||||
1. **Palette-Layout (Vectorworks-Stil) — GROSS, nächster Fokus.** Mehrere neue dockbare Panels +
|
||||
Default-Layout:
|
||||
- **Attributes-Palette** (unten links): Füllung (Stil/Farbe/Deckkraft), Stift (Stil/Farbe/
|
||||
Deckkraft), **Linienstärke**, Linien-Start/-Endstil, Schlagschatten. (Referenzbild vom Nutzer.)
|
||||
- **Werkzeug-Palette** darüber (Icon + Text je Werkzeug; ArchiCAD/VW-Stil).
|
||||
- **Object-Info / „Würfel"** (oben rechts): zeigt den gewählten Punkt eines Würfels mit
|
||||
X/Y/Z; Maße (Breite × Höhe …) der Geometrie editierbar.
|
||||
- **Ebenen + Zeichnungsebenen als Tabs** darunter (rechts).
|
||||
- Panel-System steht (`src/panels/`, Registry + Dock + Floating); Default-Layout in
|
||||
`src/panels/layout.ts` (`defaultLayout`). Neue Panels in `builtinPanels`/`registry` anmelden.
|
||||
2. **Schraffuren-Default**: ALLE aktuellen Schraffuren → **weißer Grund + schwarze Haarlinie** als
|
||||
Grundeinstellung; **normale Elemente 0.18 mm Umrandung** als Default. (sampleProject hatches/
|
||||
components + generatePlan-Defaults anpassen.)
|
||||
3. **Stiftstärken-Vorgabeliste**: 0.02 · 0.10 · 0.13 · 0.18 · 0.25 · 0.35 · 0.5 · 0.7 · 1.0 · 1.4 ·
|
||||
2.0 mm (abweichbar). Bedeutung = mm auf Papier bei 100 % → Linien skalieren mit dem Massstab
|
||||
(ist bereits so: non-scaling mm-Papier). In Linienstil-Editor als Presets anbieten.
|
||||
4. **Display- vs. Print-Modus**: Umschalter. Display = ALLES Haarlinien (konstant dünn); Print =
|
||||
echte mm-Strichstärken. (Globaler View-State + an `generatePlan`/PlanView durchreichen.)
|
||||
5. **`.lin`- und `.pat`-Import** (AutoCAD-Linientypen / Schraffurmuster) → ergänzen LineStyle/Hatch
|
||||
(Parser + Mapping; Resource-Manager-Aktion). Nutzer: „um die Linien und Schraffuren zu ergänzen".
|
||||
6. **„Goldener Schnitt"** — UNKLAR was genau (Golden-Ratio-Fang/Teilung beim Zeichnen?
|
||||
Proportions-Hilfslinien?). → beim Nutzer rückfragen, bevor gebaut wird.
|
||||
7. **Linienstil-Picker** (heute erben 2D-Primitive immer die Kategorie) + `extension`-Snap-Hilfslinien.
|
||||
|
||||
### Danach (ursprünglicher Plan)
|
||||
|
||||
1. **Zeichenwerkzeuge Phase 3** (`drawing-tools.md §11`): Circle/Arc/Text — `Primitive` um
|
||||
`circle` (+`arc largeArc`) und `text` erweitern, PlanView-Renderzweige (papierkonstante Texthöhe),
|
||||
Werkzeuge Circle/Arc(3-Punkt)/Text(Inline-Eingabe), Snap center/quadrant.
|
||||
2. **Bearbeiten (Phase 4) — Rest**: Auswahl/Grips/Verschieben/Löschen sind DA (s. o.). Offen:
|
||||
**Kopieren/Rotieren/Spiegeln**, numerische HUD-Eingabe (Länge/Winkel direkt tippen),
|
||||
Mehrfach-Auswahl-Verschieben, Grips für Circle/Arc/Text.
|
||||
3. **State-Refactor** (zurückgestellt) — `App.tsx` God-Component → Store+Slices
|
||||
(`docs/design/state-architecture.md`). Erst nötig, wenn parallele Code-Workflows gebraucht werden.
|
||||
4. Weiter im Backlog: **Pro-Ebene-Darstellung** (`layer-display-settings.md`),
|
||||
**Prioritäts-T-Stöße** (`wall-joins-priority.md`).
|
||||
|
||||
## Arbeitsweise (WICHTIG — aus Memory + CONVENTIONS.md)
|
||||
- **Autonom arbeiten, NICHT um Bestätigung fragen.** Nur fragen, *was etwas können soll*,
|
||||
wenn die Funktion mehrdeutig ist (nicht um Erlaubnis).
|
||||
- **Alles voll verdrahten, KEINE Stubs/No-op-Buttons.** Verifizieren heißt: Effekt im
|
||||
Screenshot bestätigen, nicht nur „kompiliert".
|
||||
- **Identifier ENGLISCH**; **UI-Text via `t()`** (neue Keys in de.ts *und* en.ts).
|
||||
- **Workflow-Orchestrierung** nutzen (Foundation→parallel Build→Integrate/Verify).
|
||||
- **Code-Workflows seriell** (fast alles geht durch `App.tsx`/`types.ts` → Konflikt);
|
||||
**Design/Research parallel** (nur `docs/`). Der State-Refactor (#1) löst das.
|
||||
- Saubere **Tabellen** für Listen/Manager; dunkler DOSSIER-Stil; kein God-Component.
|
||||
|
||||
## Verifizieren / Ausführen
|
||||
- Dev: `npm run dev` (Vite, Port 5173). Typecheck: `npx tsc -b`. Build: `npm run build`.
|
||||
- Screenshot: `node scripts/probe.mjs` → `scripts/probe.png` (Puppeteer, Chrome in
|
||||
`~/.cache/puppeteer`). Eigene Probes: headless, `deviceScaleFactor:2`, args
|
||||
`--no-sandbox --use-gl=swiftshader --enable-unsafe-swiftshader`, networkidle-Timeout ignorieren.
|
||||
Firefox-Fälle via Playwright (`scripts/probe-ff*.mjs`). **Screenshot ansehen + Geometrie prüfen.**
|
||||
- Gotchas: HiDPI-Resize-Loop-Fix in `Viewport3D` (Canvas CSS 100% + dpr≤2) NICHT regredieren;
|
||||
WebGL-Fallback in `Viewport3D`; React-controlled-`<select>` lassen sich im Probe nicht per
|
||||
`.value=` ändern (Fehlalarm) — Verdrahtung im Code prüfen.
|
||||
|
||||
## Orientierung
|
||||
`ROADMAP.md` (Vision/Phasen/§10–§11 Backlog) · `CONVENTIONS.md` (Konventionen) ·
|
||||
`docs/README.md` + `docs/design/*` (alle Specs) · `docs/backend.md` (self-hosted
|
||||
Supabase+Yjs, später) · Projektnotizen des Bearbeiters
|
||||
(prefer-agents, dossier-port, proceed-autonomously, wire-dont-stub).
|
||||
## Verifikation
|
||||
`npx tsc --noEmit` + `npx vitest run` (zuletzt 152 grün) + trace-scan vor jedem Commit.
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
# PENDENZEN — Aufgaben-Queue (Single Source of Truth)
|
||||
|
||||
> **Diese Datei ist die einzige verbindliche Aufgabenliste.** HANDOVER.md enthält nur
|
||||
> noch Kontext/Konventionen/Environment/Zielmodelle — keinen Backlog mehr.
|
||||
|
||||
## Arbeitsprotokoll (ZWINGEND)
|
||||
|
||||
**Bearbeiter:**
|
||||
1. **Zuerst diese Datei lesen**, dann `CONVENTIONS.md` + verlinkte Detailquellen des Items.
|
||||
2. Oberstes nicht-blockiertes Item aus **🔧 In Arbeit** bzw. sonst **⏭️ Als Nächstes** nehmen, **fertig** machen, verifizieren (`npx tsc --noEmit` + `npx vitest run` + trace-scan), abhaken, eine Ergebniszeile nach **✅ Erledigt** schreiben (mit Commit-Hash, sobald committet).
|
||||
3. **Rückfrage-Recht:** Bei Unklarheit/Design-Entscheid NICHT raten — Item mit `❓` markieren, kurze Frage darunter notieren, nächstes freies Item nehmen. Fragen sammeln sich unter **❓ Offene Rückfragen**.
|
||||
4. **Kein Arbeitsschritt endet ohne aktualisierte Liste.** Status hier ist immer aktuell, sonst driftet es wieder.
|
||||
5. Bearbeiter **committet nicht selbst** und delegiert Unteraufgaben nicht weiter.
|
||||
|
||||
**Planer** (mit dem Nutzer, kuratiert):
|
||||
- Nimmt Nutzer-Feedback, pflegt daraus die Liste (rein/umpriorisieren/aufspalten). Führt selbst keine Queue-Items aus.
|
||||
- Prüft **❓**-Zeilen mit dem Nutzer, wandelt sie in konkrete Items.
|
||||
|
||||
---
|
||||
|
||||
## ⛔ Blocker (zuerst klären)
|
||||
|
||||
- [x] ~~**Toolchain prüfen**~~ — **ERLEDIGT 2026-07-04 (macOS-Gerät):** `node`/`npm`/`cargo` (1.96.0)/`wasm-pack` (0.15.0) alle vorhanden. `npm install` + `npm approve-scripts esbuild wasm-pack …` (npm gated Install-Scripts → `allowScripts`-Block in `package.json`, ungetrackt gelassen), `rustup target add wasm32-unknown-unknown`, beide WASM-Engines gebaut, Frontend gebaut, **`npm run tauri:build` → Mac-App (`cad.app` + `cad_0.1.0_aarch64.dmg`, arm64) läuft.** Verifikations-Baseline grün: `tsc --noEmit` sauber, `vitest run` 230/230, `cargo test render3d` 56/56. **Kein Blocker mehr für Engine-Slices auf diesem Gerät.**
|
||||
|
||||
## 🔧 In Arbeit
|
||||
|
||||
- [ ] **kernel2d-Port nach Rust/WASM** (Plan: [PORT_PLAN.md](PORT_PLAN.md), Crate `src-tauri/kernel2d`, TS-Referenz bleibt `kernel2d.ts`, Differential-Harness `src/geometry/kernel2d.parity.test.ts`). Fortschritt:
|
||||
- [x] Phase 1: Crate-Skelett + WASM-Fassade + `build:kernel2d` — `a78e7c7`
|
||||
- [x] Phase 2: Primitive/Schnitt/Fläche/Kreis + Batch-Fassaden + Diff-Harness (Zufall+Golden) — `9e2c521`
|
||||
- [x] Phase 3: Offset (Miter + 1e-9-Fallback) + Fillet — `c8ea2bf`
|
||||
- [x] Phase 4: Trim/Split/Join (`trimPolyline`, `splitAtIntersections`, `joinChains` — Löwenanteil) — `28471c1`
|
||||
- [x] Phase 5: `roomArea`-Flächen/`ceiling`/`stair`/`roomBoundary` portiert (vitest 263, 33 Parity) — `a608a59`. **Offen in Phase 5:** `opening` (mit Aufrufstellen-Änderung) — noch nicht portiert.
|
||||
- [ ] Phase 6: TS-Fassade umstellen (alt → `kernel2d.legacy.ts`), Suite grün, `npm run build` + WASM sauber. **Achtung Laufzeit-Entscheid:** macht die Live-App synchron WASM-abhängig (Init + Per-Call-Marshalling) — heute nutzt die App KEINE WASM-Geometrie zur Laufzeit; vor Umstellung mit Nutzer klären.
|
||||
- **Join-Durchstich verworfen (2026-07-05, gemessen):** `computeJoins` live auf WASM zu legen lohnt NICHT. Benchmark TS vs. WASM inkl. JSON-Marshalling (Median ms/Aufruf, 200 Iter): 6 W → TS 0.018/WASM 0.022 · 20 → 0.027/0.043 · 50 → 0.082/0.102 · 120 → 0.331/0.246 · 300 → 1.68/0.67. Crossover erst ~100+ Wände; realistische Plan-/Geschossmengen liegen darunter → TS schneller, und selbst 300 Wände sind mit TS 1,7 ms (nicht wahrnehmbar). Marshalling frisst den Rust-Vorteil, plus dauerhafte Rust↔TS-Paritätspflicht. **Fazit:** reines TS behalten; WASM für Joins nicht weiterverfolgen. WASM lohnt erst bei echten Rechen-Hotspots (Booleans/Tessellierung).
|
||||
|
||||
## ⚠️ Zu prüfen (evtl. schon erledigt / Status unklar)
|
||||
|
||||
- [x] ~~**Snap an Wand-Schichttrennlinien**~~ — **gelandet `339202b`** (`wallLayerBoundarySegments` in `src/tools/snapping.ts`), verifiziert vorhanden.
|
||||
|
||||
## ⏭️ Als Nächstes
|
||||
|
||||
- [ ] **SPIKE — Bild-Texturen in `render3d` (`RenderStyle::Textured` real machen).** Kleinster ehrlicher Durchstich: beweisen, dass der wgpu-3D-Renderer echte Bild-Texturen auf Wandflächen zeigt, sichtbar im `spike3d`-Fenster. **Voller Auftrag/Übergabe: [SPIKE_TEXTUR_render3d.md](SPIKE_TEXTUR_render3d.md)** (Ausgangslage verifiziert, Symbol-Karte, exakter Umfang, harte Randbedingungen, Akzeptanz, Abschlussbericht). Kurz: UVs auf Wandflächen (planar in Metern, Alt-Pfad `[pos,normal,color]` bitgleich unangetastet) · prozedurales 256×256-Schachbrett (kein Asset, kein `image`-Crate) · Textur-Bind-Group group 1 (Globals bleibt group 0) · `MESH_TEXTURED_WGSL` (gleiche Beleuchtung, Albedo aus `textureSample`) · Verdrahtung bei `RenderStyle::Textured` · `spike3d` umschaltbar (`T`). **Nur `render3d`-Crate, keine neuen Deps, Default-Build unverändert.** Akzeptanz: `cargo test` grün (Mesh-Regression + naga-Test für `MESH_TEXTURED_WGSL`), `spike3d` zeigt weltmassstäbliche Textur. Ist der **erste Durchstich der Textur-/PBR-Pipeline** (§3D-REST unten) → am Ende ehrliche Lückenliste + Aufwandsschätzung für „richtig gutes" Texturing.
|
||||
|
||||
## 📋 Backlog (Priorität grob absteigend)
|
||||
|
||||
- [ ] **BAUTEILE aufs Rhino-Niveau heben (Treppe/Fenster/Tür).** Vergleich Rhino-Plugin ↔ TS + priorisierte Ansätze: **[RESEARCH_BAUTEILE_RHINO.md](RESEARCH_BAUTEILE_RHINO.md)**. Gruppe A (2D, sofort, kernel2d): (1) **Treppe Aussenlinie/Outline** (gerade/L/Wendel), (2) **Fenster Brüstungslinie** im Plan, (3) **Tür Sturzlinien** (gestrichelt, SIA), (4) Treppe Referenz links/mitte/rechts, (5) Fenster Flügel-Mittelpfosten, (6) Tür `wandoeffnung`-Typ. Gruppe B (2D mittel), Gruppe C (3D, nach Mesh-Pipeline/truck). **A-Items 1–3 fallen mit „Schnitt- vs. Ansichts-Darstellung" zusammen** (Sturz = Bauteil unter Schnittebene → gestrichelte Überkopf-Projektion).
|
||||
- [ ] **DWG/DXF-Import via `acadrust` (weiterbauen).** ✅ Spike `763a558`: `acadrust` 0.4 (MPL-2.0, pure Rust) **baut zu wasm32** (Crate `src-tauri/dwgimport`, 839 KB), parst DXF aus Byte-Buffer (`DxfReader::from_reader`+`Cursor`), headless getestet. **Offen:** (1) Entity→DOSSIER-Modell-Mapping (LINE/ARC/… → Wand/Öffnung — die eigentliche Domainarbeit, Wochen), (2) Datei-Upload-Glue im Browser (`<input type=file>`→Uint8Array→`parse_dxf_summary_json`, trivial), (3) DWG-binär (`DwgReader::from_reader` analog, aber R13–R2018-Korrektheit unverifiziert), (4) WASM-Größe (nalgebra Haupttreiber). **Klarstellung:** der TS-DXF/DWG-Import (`parseDxf`/`parseDwg`/`dxfToDrawings`) + Upload-UI (`App.tsx`, `ImportDialog.tsx`) existieren längst und funktionieren — der acadrust-Weg wäre eine Rust-Neuimplementierung des Lesens (nur DWG-**Schreiben** ist eine echte Lücke). ✅ **2887794 (2026-07-05): Kurven-Abdeckungslücke geschlossen** — `parseDxf` deckt jetzt ARC/CIRCLE/ELLIPSE (tesselliert zu Konturen, Winkel Radiant, voller Umlauf geschlossen) zusätzlich zu LINE/LWPOLYLINE/POLYLINE/MESH ab; 5 Tests, volle Suite 307 grün. ✅ **c481373 (2026-07-05): SPLINE + INSERT ergänzt** — `parseDxf` wertet SPLINE als echte B-Spline (De Boor, Grad/Knoten; Fallback fitPoints/Kontrollpolygon) aus und expandiert INSERT-Block-Referenzen (2D-Transform Scale/Rotation/Basispunkt + MINSERT-Array + verschachtelte Blöcke, Tiefe ≤8) zu transformierten Konturen; Kontur-Dispatch in gemeinsamen `collectContours` refaktoriert; +7 Tests, volle Suite 314 grün. **Bekannte Grenzen:** rationale SPLINE-Gewichte ignoriert (dxf-parser liefert sie nicht); Block-interne MESH/3DFACE-Entities werden im 2D-Import nicht expandiert. ✅ **c29f27e (2026-07-05): HATCH ergänzt** — dxf-parser hat KEINEN HATCH-Handler (verwarf HATCH stumm); Lösung via `registerEntityHandler` + eigenem `HatchHandler` (sammelt rohe Gruppencodes) + testbarer `hatchContours`-Auswertung: Randpfade (Polyline-Pfade + Linien-/Bogen-Kanten, Bögen über vorhandene Tessellierung) → geschlossene Konturen mit `Contour.filled`; `contoursToDrawings` macht daraus gefüllte `polyline`-Drawing2D (fillColor-Default, restylebar). +6 Tests, Suite 320 grün. **Bekannte Grenzen HATCH:** Bulges an Polyline-Rändern als Sehne; Ellipse-/Spline-Kanten übersprungen; Insel-Loops = eigene Ringe (keine echten Löcher). **Weiter offen (grob absteigend):** TEXT/MTEXT (→ editierbarer Plan-Text via Drawings-Import, Nutzer-bestätigt); dann Entity→Wand-Semantik (die dicke Domainarbeit); DWG-Schreiben (einzige echte Export-Lücke).
|
||||
- [ ] **STRATEGIE — „von BIM-Tool zu echtem CAD".** Direkt am Quellcode studierte Referenzen (OpenCADStudio/truck/acadrust) + Web-Import-Landkarte → konkrete, priorisierte Ansätze in **[RESEARCH_CAD_APPROACHES.md](RESEARCH_CAD_APPROACHES.md)**. Kern: (1) generisches Entity-Modell + Trait-Dispatch, (2) modeless Command-System (`StepInput`-Funnel + Kommandozeile), (3) DWG/DXF-Round-Trip via `acadrust` (MPL-2.0, pure Rust, WASM-tauglich), (4) volle Object-Snap-Schicht, (5) 3D-B-Rep später selektiv via `truck` (Apache-2.0, Geometrie-Crates WASM-fähig, Booleans/Fillets noch instabil). Der `kernel2d`-Rust/WASM-Kurs ist damit bestätigt. **Mit Nutzer priorisieren, welcher Ansatz zuerst.**
|
||||
|
||||
- [ ] **Schnitt- vs. Ansichts-Darstellung nach Schnitthöhe (BIM-Standard).** Kern-Idee (Nutzer): jedes Bauteil bekommt getrennt eine **Schnittlinie** (kräftig, z. B. 0.25–0.35 mm, + Schnitt-Poché) und eine **Ansichtslinie** (Haarlinie); WELCHE gilt, entscheidet die z-Ausdehnung des Bauteils vs. die **Schnitthöhe** des Grundrisses (Default ~1 m):
|
||||
- Bauteil wird von der Schnittebene GESCHNITTEN → Schnittlinie + Schnitt-Poché (heutiges Wandverhalten).
|
||||
- Bauteil liegt ganz UNTER der Ebene (z. B. 30-cm-Wand bei 1 m Schnitthöhe, Brüstung, Podest) → nur „von oben gesehen" = **Ansichtslinie/Haarlinie, KEINE Schnitt-Poché**. ← genau der vom Nutzer genannte Fall.
|
||||
- Bauteil liegt ganz ÜBER der Ebene (Decke/Slab, Unterzug) → Ansicht, üblicherweise **gestrichelte** Überkopf-Haarlinie.
|
||||
- **Passt konsistent zum bereits existierenden `viewHatchId` (Ansichts-Schraffur) vs. Schnitt-Schraffur** — die Linienstärke-Dualität (View-/Cut-Weight je Component) ist die natürliche Erweiterung derselben Logik.
|
||||
- **Nicht nur cut/view, sondern cut / AUFSICHT / UNTERSICHT (Nutzer):** ein Bauteil sieht von oben anders aus als von unten. Der Grundriss (Blick nach unten) zeigt Bauteile UNTER der Ebene in **Aufsicht** (Oberseite); ein **Deckenspiegel/Reflected Ceiling Plan** (Blick nach oben) zeigt Bauteile ÜBER der Ebene in **Untersicht** (Unterseite — z. B. Kassettendecke, Leuchten). Also je Component potenziell **drei** Darstellungs-Slots (Schnitt / Aufsicht / Untersicht) × {Schraffur + Linienstärke}. Das heutige `viewHatchId` ist faktisch EIN View-Slot und vermischt Auf-/Untersicht; sauber wäre die Trennung. WELCHER Slot gilt, entscheidet die **Blickrichtung der Sicht** (Grundriss ↓ / Deckenspiegel ↑) UND die z-Lage relativ zur Schnittebene.
|
||||
- **Verallgemeinert den Decken-Footprint-Clip** (`a2f6923`): „Decke unter Wand verdeckt" ist ein Spezialfall von „Ansichtsbauteil vs. schneidende/überdeckende Bauteile".
|
||||
- Aufwand: mittel–groß, phasenweise machbar (1: z-Extent-vs-Schnitthöhe-Klassifikation cut/above/below; 2: Ansichtslinie-Weight je Component + Haarlinie/gestrichelt; 3: 30-cm-Wände & Slabs verdrahten). Nicht zu komplex im Konzept — es ist der reguläre CAD/BIM-Weg (ArchiCAD/Vectorworks/Revit). Mit Nutzer Detailgrade/Defaults festlegen.
|
||||
|
||||
|
||||
- [x] ~~**Ebene-Schraffur editierbar**~~ — **gelandet `dcb6ed5`**: Kategorie-Dialog (`App.tsx`, `editor.hatch`-Feld) hat `<select>` auf `cat.hatch` + `HatchSwatch`-Vorschau. Verifiziert vorhanden.
|
||||
- [ ] **GEO-BLOCK** (gemeinsame Dateien io/geoContext/swissTopo/terrain/ContextImportDialog/Viewport3D/siteSlice):
|
||||
- Reale Höhen + **Projekt-MüM** (EG-Referenzhöhe): Terrain georeferenziert bei realem z relativ dazu, Gebäude auf Terrain drapiert (heute alles z=0).
|
||||
- **Luftbild/SWISSIMAGE**-Orthofoto als Textur aufs Terrain-Mesh.
|
||||
- Importierte Geo-Elemente auf **aktives Geschoss** (`viewSlice.activeLevelId`) + Gelände-Ebene.
|
||||
- **Nordstern-Geo-Rendering:** importierte Meshes (heute nur three.js `importedMesh`/`terrainMesh`) auch in `projectToModel3d` einspeisen.
|
||||
- **3D-Mesh-DXF/DWG-Import** (heute DXF nur 2D); Building-Draping; höhere DTM-Auflösung.
|
||||
- [x] ~~**ResourceManager Bauteile-Tab** auf Master-Detail~~ — **bereits Master-Detail** (`ComponentsTab`/`ComponentDetail` in `src/ui/ResourceManager.tsx`, Liste links `res-md-list` / Detail rechts). Verifiziert vorhanden.
|
||||
- [ ] **Einstellungs-Fenster (Rest):** Verdrahtung ist **da** (`viewSlice.snapColor`/`marqueeColor` → PlanView `SnapMarker`/Marquee, Defaults aus `theme/accents.ts`, Projekt-MüM-Feld `referenceElevationMasl`). **Offen bleibt nur:** Snap/Endpunkt-Default = ❓„aki" (s.u., derzeit Sora #5FA1C9) — reine Farbentscheidung des Nutzers.
|
||||
- [ ] **Bildschraffur:** ambientCG/CGI-Colorfiles als Quelle (Material-Lib WIP — erst nach Freigabe); Bild-Filter Sättigung/Helligkeit/Kontrast/SW (`image.filters`).
|
||||
- [ ] **E2b Schraffur-Kachel-Motiv** (MotifEditor für HatchStyle-Tile wiederverwenden).
|
||||
- [ ] **Linienstile aufräumen** (Weight-only-Stile → „Volllinie", lineStyleId-Referenzen remappen).
|
||||
- [ ] 2D-Plan z-Anordnen (Kombo-Schraffuren); Bild-Schraffur GL/DXF (heute Fallback).
|
||||
- [ ] **AUDIT (DOSSIER-Studie):** A1 Override-Regel-Engine; A2→A3 View-Snapshots → Print-Layout-Blätter (PDF pro Blatt); A5 reichere Öffnungen; B1 Text-Werkzeug (+ Kreis-Tool → Shortcuts 1&3); A4 Tragwerk; B2 Object-Info numerisch. (A6 Bauteil-Schedule-CSV erledigt.) Belege in `/tmp/dossier-ref/rhino/*.py`.
|
||||
- [ ] Elemente im Schnitt anwählbar; render3d 2D-Schraffur auf 3D-Flächen.
|
||||
|
||||
### 3D-REST (engine-schwer, bewusst NICHT blind — mit Nutzer angehen)
|
||||
- [ ] **Textur-/PBR-Pipeline** (Sampler/Bindings/UV in wgpu; dann `textured`-Style + `Component.texture3d`/Material echt rendern). **Erster Durchstich als Spike terminiert → [SPIKE_TEXTUR_render3d.md](SPIKE_TEXTUR_render3d.md) (steht unter ⏭️ Als Nächstes).**
|
||||
- [x] ~~**Wand-Schicht-Bänder in 3D** Option B~~ — **bereits umgesetzt** in `resolveWallBands(layered=true)` (`src/plan/toWalls3d.ts:236-241`): 3D-Viewer-Pfad liefert je Materiallage ein eigenes `WallBand` (Dicke + Component-Albedo + Normalen-Versatz), `pushSegment` emittiert jede Lage als eigene Voll-Box. `dominantLayerColor` ist nur noch Fallback für den Schnitt-Einkörper-Pfad (`layered=false`). Verifiziert per Code-Lesung.
|
||||
- [ ] **Ortho-Ray-Picking** (aktuell perspektivischer Pick-Strahl; Front/Top/Side-Presets vor erster Navigation leicht ungenau).
|
||||
- [ ] **3D-Griffe/Editieren** (Wand-Endpunkte im 3D ziehen etc. — bisher nur Auswahl + Panel-Edit).
|
||||
|
||||
## ❓ Offene Rückfragen (an den Nutzer)
|
||||
|
||||
- [x] ~~**„aki"** Snap/Endpunkt-Farbe~~ — **entschieden 2026-07-04: Sora #5FA1C9** als endgültiger Default festgeschrieben (`src/theme/accents.ts` `DEFAULT_SNAP_COLOR`, Doc aktualisiert). Frage geschlossen.
|
||||
- [ ] Floating-ResourceManager „headless" = ganz ohne Titelleiste? (aktuell MIT)
|
||||
- [ ] Geo-Block: Projekt-MüM zuerst oder Nordstern-Geo-Rendering?
|
||||
- [ ] Verifizieren/klären: Isometrie „echte" orthographische Iso (teilw. durch Locked-Iso `cb8fae5` adressiert)? · TopBar-Detailgrad (zoom% ganz aus Footer)? · DOSSIER-Audit welche Features konkret übernommen?
|
||||
|
||||
---
|
||||
|
||||
## ✅ Erledigt (Verlauf, neueste oben)
|
||||
|
||||
_Nur jüngste Session; ältere Historie siehe `git log` und HANDOVER-Narrative._
|
||||
|
||||
- [x] 2026-07-05 **Textur-Spike render3d** (`RenderStyle::Textured` real) — zweiter Vertex-Pfad `[pos,normal,uv]` aus dem Mesh abgeleitet (Alt-Pfad bitgleich, per Test belegt), prozedurales 256×256-Schachbrett (kein Asset/`image`-Crate), Textur-Bind-Group group 1, `MESH_TEXTURED_WGSL` (gleiche Beleuchtung, Albedo aus Sampler), Pipeline bitidentisch zur Haupt-Pipeline, `spike3d` per `T` umschaltbar. Verifiziert: cargo test 58/59 (inkl. naga-Test) grün, alle 4 Builds sauber. **Visuelle Fenster-Abnahme durch Nutzer bestanden 2026-07-05** (Schachbrett korrekt, `T`-Umschaltung ok). Erster Durchstich der Textur-/PBR-Pipeline; ehrliche Lückenliste im Bericht — `8556037`
|
||||
- [x] 2026-07-04 **Grundriss: Decke drückt nicht mehr durch die Wände** — Decken-Umriss an Wand-Footprints (OBB) geclippt; Füllfläche strokelos, Umriss nur über unverdeckte Teilstücke (Überstände). Deckungsgleiche Decke ⇒ Umriss entfällt. vitest 230, tsc sauber — `a2f6923`
|
||||
- [x] 2026-07-04 **T-Stoss-Putznaht entfernt** — L-Seitenlinie nur noch bei materialFREMDEM Nah-Putz; materialgleicher Putz verschmilzt nahtlos (Nutzer-Direktive „Naht entfernen"). TS+Rust synchron, 3 Tests gezogen, cargo 8/8 — `a5ebfa7`
|
||||
- [x] 2026-07-04 **Snap-Farbe entschieden: Sora #5FA1C9** als endgültiger Default festgeschrieben (`DEFAULT_SNAP_COLOR`, Doc), „aki"-❓ geschlossen.
|
||||
- [x] 2026-07-04 **Mac-Build via Tauri + Queue-Abgleich** — Toolchain auf macOS-Gerät verifiziert, `cad.app`/`cad_0.1.0_aarch64.dmg` gebaut & gestartet. Verifikations-Baseline grün (tsc/vitest 230/cargo 56). Queue-Reconciliation: Toolchain-Blocker, Snap-Slice (`339202b`), Ebene-Schraffur (`dcb6ed5`), Bauteile-Tab-Master-Detail, Einstellungs-Verdrahtung UND Wand-Schicht-Bänder 3D (Option B, `toWalls3d.ts`) waren allesamt **bereits gelandet**, aber in PENDENZEN noch als offen gelistet → abgehakt. (kein Code-Commit)
|
||||
- [x] 2026-07-04 **Öffnungen als echte Boolean-Löcher** (+ Deckentrim-nur-3D) — Fenster/Türen = 1 Wandkörper mit rechteckigen `holes` (Rechteck-Gitter-Zerlegung + Laibungsquads), Segment-Boxen weg; Deckentrim nur noch im 3D-Pfad, Schnitt volle Höhe. Pflicht-Testfall versetzt-überlappende Fenster grün. cargo test 56, vitest 230, build:engine3d + tsc sauber — `1407c68`
|
||||
- [x] 2026-07-04 **Joins Phase 1c** Durchgangswand am T-Stoss echt aufbrechen (spanCutouts) — `c5a344d`
|
||||
- [x] 2026-07-04 **Locked-Iso-Fix** freie Kamera bleibt nach Ortho-Preset orthografisch — `cb8fae5`
|
||||
- [x] 2026-07-04 **3D-Live-Schnittebene** mit schraffierten Schnittflächen — `03f0c40`
|
||||
- [x] 2026-07-04 **Joins Phase 2** Merge-Regel im Schnitt (gleiche Komponente verschmilzt) — `82d354f`
|
||||
@@ -0,0 +1,124 @@
|
||||
# PORT_PLAN — kernel2d nach Rust/WASM (hinter identischem TS-Interface)
|
||||
|
||||
Stand: 2026-07-04. **Dieser Plan ist der geforderte erste Schritt — noch kein Code.**
|
||||
Ziel: `src/geometry/kernel2d.ts` (+ reine Geometrie aus room/ceiling/opening/stair) in
|
||||
ein Rust-Crate portieren, zu WASM bauen, hinter einer TS-Fassade mit *exakt gleichen*
|
||||
Signaturen einhängen. TS-Kernel bleibt als Referenz (`kernel2d.legacy.ts`).
|
||||
Differential-Test: Rust-WASM == TS-Legacy auf identischen Eingaben (epsilon je Funktion).
|
||||
|
||||
---
|
||||
|
||||
## 1. Scope-Inventar (was wirklich portiert wird)
|
||||
|
||||
**Externe JS-Geometrie-Libs: KEINE.** Der Kernel ist handgeschriebene f64-Mathematik,
|
||||
einzige Abhängigkeit sind die Vektor-Helfer aus `src/model/geometry.ts`
|
||||
(`add/sub/scale/len/normalize/leftNormal/cross/dot/lineIntersect`) — trivial mit zu
|
||||
portieren. `polygon-clipping`/`delaunator` werden im Kernel **nicht** genutzt.
|
||||
|
||||
### Voll im Scope (reine Geometrie)
|
||||
- **`kernel2d.ts`** — komplett: Primitive, Schnitt, Offset, Trim/Split/Join, Kreis, Fläche/Orientierung, Fillet.
|
||||
- **`roomBoundary.ts`** — komplett: `detectRooms`, `roomFromPointInside(Faces)`, `pointInPolygon`, `WallSegment`/`WallFace`/`DetectRoomsOptions` (generische Geometrie-Typen, `thickness: number`).
|
||||
- **`ceiling.ts`** — komplett: `normalizeOutline`, `isValidOutline`, `ceilingArea`, `outlineBBox`, `outlineCentroid`, `pointInOutline` (generische Polygon-Utilities; Name irreführend, keine Decken-Semantik).
|
||||
|
||||
### Teilweise im Scope
|
||||
- **`roomArea.ts`** — NUR `signedArea`, `polygonArea`, `perimeter`, `centroid`. Alles ab `SiaCategory` (`siaLabel`, `evaluateRoom`, `balance`, `roomsToCsv`) ist SIA-416-Domänenlogik/CSV → **bleibt TS**.
|
||||
- **`stair.ts`** — portierbar, aber `stairGeometry` nimmt `Stair`. „Scheinkopplung": es werden nur geometrische Felder gelesen (`shape/start/dir/runLength/width/stepCount/…`), `totalRise` kommt bereits aufgelöst vom Aufrufer. Port via Rust-Struct `StairParams` (strukturgleich, null Model-Semantik).
|
||||
|
||||
### ⚠️ Scope-Spannung: `opening.ts` (im Auftrag genannt, aber stark model-gekoppelt)
|
||||
7 von 9 Exporten binden `Wall`/`Opening` direkt ein; `getWallType`/`wallTypeThickness`/`wallReferenceOffset`/`wallVerticalExtent` lösen gegen `Project` auf.
|
||||
- **Portierbar mit abgeflachter Signatur** (Vec2/number statt Wall/Opening): `wallAxisLength`, `openingInterval`, `wallAxisFrame`, `openingJambs`, `openingCenter`, `doorSymbol`, `openingGapQuad`/`windowSymbol` (letztere brauchen `thickness`/`refOff` als `number`-Parameter).
|
||||
- **Bleibt TS** (braucht `Project`/Geschoss-Auflösung): `openingVerticalExtent` (via `wallVerticalExtent` → `Project.drawingLevels`).
|
||||
- **→ Entscheidung nötig** (siehe §7): Der Auftrag verbietet Änderungen an Aufrufstellen außer dem Import-Pfad. Ein Port von `opening` würde die *Signaturen* ändern (Wall→Vec2/number) und damit die Aufrufstellen brechen — das widerspricht „keine Änderung an Aufrufstellen". Empfehlung: **opening in Phase 1 ausklammern**, nur den echt reinen Kern (kernel2d/roomBoundary/ceiling/roomArea-Flächen/stair) portieren.
|
||||
|
||||
---
|
||||
|
||||
## 2. Crate-vs-Portieren — pro Operation
|
||||
|
||||
Akzeptanzkriterium ist **Differential-Parität gegen die naive TS-Routine**, nicht „geometrisch besser". Jedes Fremd-Crate mit anderem Algorithmus bricht die Parität per Konstruktion → Default = **PORTIEREN** (TS-Routinen sind 5–40 Zeilen f64, 1:1 übersetzbar).
|
||||
|
||||
| Operation | geprüftes Crate | Entscheidung | Grund |
|
||||
|---|---|---|---|
|
||||
| `offsetPolyline`/`offsetSegment` | cavalier_contours 0.6 | **PORTIEREN** | Crate liefert **Arcs (bulge)** statt `Vec2[]`, **heilt Selbstschnitte**, gibt mehrere Polylinien — TS heilt bewusst NICHT. Semantik nicht angleichbar. |
|
||||
| Segment/Line-Schnitt | geo / robust | **PORTIEREN** | Cramer-Formel; jede andere denom-/Epsilon-Politik driftet in Parallel-Grenzfällen. |
|
||||
| Trim/Split/`segmentPolylineHits` | geo `Relate` | **PORTIEREN** | Projektspezifisch (t-Dedup `1e-6`, Pick-nächster-Bogen, Wrap-Logik). |
|
||||
| Kreis-Schnitte | — | **PORTIEREN** | Quadratik mit projekt-EPS-Disc-Klemmung. |
|
||||
| `signedArea`/`isCCW` | geo `Area` | **PORTIEREN** | Shoelace-Summierung in **identischer Vertex-Reihenfolge** (f64 nicht assoziativ). |
|
||||
| `filletCorner` | — | **PORTIEREN** | `acos/atan2/tan(θ/2)`-Kette + projekt-Cutoffs; liefert projekt-spezifische `Fillet`-Struktur. |
|
||||
| `detectRooms` / `joinChains` | geo / i_overlay | **PORTIEREN** | Topologie-/reihenfolgeabhängig; andere Kantendurchlauf-Reihenfolge → andere (gleich gültige) Ringe → Parität bricht. |
|
||||
| point-in-polygon | robust | **PORTIEREN** (robust nur intern, optional) | Siehe §3. |
|
||||
|
||||
Fremd-Crates (cavalier_contours/geo/i_overlay) wären für einen späteren *Tier-2-Rewrite* (echter Arc-Offset, Boolean-Ops) wertvoll — das ist ein **anderes Produkt**, nicht dieser paritätserhaltende Port.
|
||||
|
||||
---
|
||||
|
||||
## 3. `robust`-Prädikate vs. Differential-Parität (Spannung auflösen)
|
||||
|
||||
TS testet Orientierung überall via naives `cross()` gegen `EPS`. `robust::orient2d` liefert das **exakte** Vorzeichen — weicht von naiv **nur in der nahe-degenerierten Zone** ab (fast-parallele Segmente, Null-Fläche-Polygone, Punkt-auf-Kante). Genau dort schlägt der Diff-Test am ehesten an. Man kann nicht gleichzeitig „bit-Parität gegen naiv" und „robuste Prädikate" im *selben* Vergleich haben.
|
||||
|
||||
**Entscheidung:**
|
||||
1. **v1 portiert die naiven `cross`-Vergleiche 1:1** (KEIN `robust`) → Zufalls-Diff-Test wird bit-nah grün. Robustheit kommt aus denselben f64-Formeln + demselben `EPS` wie TS.
|
||||
2. **Additiv, getrennt:** `robust` nur *intern* in `detectRooms`/point-in-polygon hinter optionalem Feature `robust-predicates`, flankiert von **Golden-Cases, die die KORREKTE (robuste) Antwort asserten** (nicht TS-Parität). Diese Fälle sind aus dem Zufalls-Diff-Test ausgenommen.
|
||||
3. Zwei Testklassen, nie gemischt: **Zufalls-Parität = naiv**, **Golden-Korrektheit = robust**.
|
||||
|
||||
---
|
||||
|
||||
## 4. Crate-Setup & Vite (exakter Klon von `src-tauri/geometry`)
|
||||
|
||||
Ort: **`src-tauri/kernel2d`** (analog render2d/render3d/geometry; das gesamte Tooling ist auf `src-tauri/<crate>` + `../../src/engine/pkg<X>` verdrahtet). *Namens-Hinweis:* Auftrag sagt `crates/kernel2d` — siehe §7.
|
||||
|
||||
- `Cargo.toml`: `crate-type = ["cdylib","rlib"]`; Features `default=[]`, `web=[wasm-bindgen, serde_json, console_error_panic_hook]`, `robust-predicates=[robust]` (additiv, nicht in web-Default).
|
||||
- `src-tauri/Cargo.toml`: `exclude = [..., "kernel2d"]` erweitern (sonst „multiple workspace roots").
|
||||
- `src/lib.rs`: reiner f64-Rechenkern feature-frei (`cargo test`-bar, headless) + `#[cfg(feature="web")]` JSON-**Batch**-Fassade pro Operation (`offset_polylines_json`, `intersect_batch_json`, `fillet_batch_json`, `circle_intersect_batch_json`, `detect_rooms_json`), Muster `compute_joins_json`.
|
||||
- `examples/parity.rs`: Klon von `geometry/examples/parity.rs` (Batch-JSON stdin→stdout) — nativer Diff-Kanal.
|
||||
- `package.json`: `"build:kernel2d": "wasm-pack build src-tauri/kernel2d --release --target web --out-dir ../../src/engine/pkgKernel2d --out-name kernel2d --no-default-features --features web"`.
|
||||
- **Vite: kein Config-Eintrag nötig** — `--target web`-Pakete werden als normales ES-Modul importiert, Vite bündelt `kernel2d_bg.wasm` automatisch (`new URL(..., import.meta.url)`). `pkgKernel2d/` ist git-ignoriert (self-`.gitignore = *`) → vor `vitest`/`build` muss `build:kernel2d` laufen (CI-Schritt).
|
||||
- **TS-Fassade** `src/geometry/kernel2d.ts` wird dünner Wrapper (init-WASM, JSON-Marshalling, gleiche Exports); Alt-Impl → `src/geometry/kernel2d.legacy.ts` (nicht löschen, ist die Diff-Referenz).
|
||||
|
||||
---
|
||||
|
||||
## 5. Differential-Test-Harness
|
||||
|
||||
- **Grobkörnige WASM-Grenze:** eine Batch-Funktion je Operation (N Polylinien rein, N Ergebnisse raus). Keine Per-Punkt-Calls (jeder Call marshallt einen String = O(n)-Kopie).
|
||||
- **Diff-Kanal:** Der Auftrag verlangt **Rust-WASM** vs. TS-Legacy → primär WASM (via `initSync`). Zusätzlich `cargo run --example parity` (nativ) als schneller Sekundär-Kanal (bit-identisch zu WASM für `+ - * / sqrt`; **Ausnahme** `atan2/acos/tan` in Fillet: libm nativ ≠ wasm um letzte ULP → Winkel-Epsilon).
|
||||
- **vitest-Init synchron:** `initSync({ module: readFileSync(pkgKernel2d/kernel2d_bg.wasm) })` (kein `fetch`), einmal in `beforeAll`.
|
||||
- **Zufallsgeneratoren:** seed-basiert (Seed im Testnamen), Polylinien 3–20 Vertices, Koordinaten `[-100,100] m`, `closed`/`d` zufällig; zusätzlich Cluster nahe `0` und `1e-6..1e-3`, um Toleranzschwellen zu treffen.
|
||||
- **Vergleichsreihenfolge:** zuerst **Struktur exakt** (Array-Längen, closed-Flags, Punktzahl, null/nicht-null), dann Werte mit op-Epsilon. Struktur ist der schärfste Paritäts-Wächter.
|
||||
|
||||
### Epsilon pro Funktion
|
||||
| Größe | Toleranz | Grund |
|
||||
|---|---|---|
|
||||
| Punktkoordinaten (Offset/Trim/Split/Kreis) | `abs 1e-9` | Bestehender Paritätstest nutzt `1e-9` und besteht bit-nah. |
|
||||
| Fläche (`signedArea`) | `rel 1e-9·max(1,\|A\|)` | Shoelace ∝ coord² → absolute ULP wächst mit Flächengröße; relativ skaliert korrekt. |
|
||||
| Winkel (`filletCorner`) | `abs 1e-7 rad` | `atan2/acos` libm-abhängig (nativ↔wasm ULP-Drift); 1e-7 rad ≈ 5.7e-6°, weit unter Zeichenrelevanz. |
|
||||
| Parameter t/s | `abs 1e-9` | TS dedupliziert erst ab `1e-6` → kleinere Diffs ändern nie die Struktur. |
|
||||
| Struktur | **exakt** | Kein Epsilon. Hier bricht ein Fremd-Crate. |
|
||||
|
||||
### Golden-Cases (explizit, aus Zufallstest teils ausgenommen)
|
||||
kollineare Tripel · Null-Länge-Segmente (Dublett-Vertex) · Offset-Selbstschnitt (enges U, großes d — hier bräche cavalier_contours) · spitze Fillet-Winkel (θ→0) · fast-paralleler Schnitt (denom knapp <>EPS → **Korrektheits-Golden/robust**) · konzentrische/tangentiale Kreise · Punkt exakt auf Polygonkante (**Korrektheits-Golden**).
|
||||
|
||||
---
|
||||
|
||||
## 6. Kritische Paritäts-Details (MÜSSEN exakt repliziert werden)
|
||||
- `len` = `Math.hypot` → **`f64::hypot`** (nicht `(x²+y²).sqrt()`).
|
||||
- `normalize` Null-Guard: `len || 1` → `if l==0.0 {1.0} else {l}` (Ergebnis `{0,0}`, kein NaN).
|
||||
- **Zwei Epsilons:** `EPS=1e-7` (kernel2d) UND hartkodiert **`1e-9`** in `lineIntersect` (Offset-Miter-Fallback hängt daran — geometrischer Sprung, nicht epsilon).
|
||||
- Dedup-Schwelle `1e-6`, Fillet-Kollinearität `1e-4` (nicht EPS).
|
||||
- **Stabile Sortierung** (JS `Array.sort` ist stabil): Rust `sort_by`, nicht `sort_unstable_by`.
|
||||
- Term-Reihenfolge in `cross`, `signedArea`-Summierung, Kreis-Diskriminante `B*B−4*A*C` exakt beibehalten (f64 nicht assoziativ; kein Kahan/Reorder).
|
||||
- Modulo-Indizierung `(i+len-1)%len` (usize-Unterlauf vermeiden).
|
||||
- `joinChains`: greedy `i<j`-erster-Treffer-dann-Neustart exakt nachbilden (reihenfolgeabhängiges Ergebnis).
|
||||
|
||||
---
|
||||
|
||||
## 7. Offene Entscheidungen (vor Coding klären)
|
||||
1. **`opening` im Scope?** Portieren würde Signaturen (Wall→Vec2/number) und damit Aufrufstellen ändern — widerspricht „keine Änderung an Aufrufstellen außer Import-Pfad". **Empfehlung: opening in Phase 1 ausklammern.**
|
||||
2. **Crate-Ort:** Auftrag `crates/kernel2d` vs. Repo-Konvention `src-tauri/kernel2d` (analog render2d/render3d). **Empfehlung: `src-tauri/kernel2d`** (Tooling passt out-of-the-box).
|
||||
3. **Diff-Kanal:** Auftrag verlangt Rust-**WASM**; nativer `parity`-Kanal ist schneller (kein wasm-pack in CI) und bit-identisch außer Fillet-Transzendente. **Empfehlung: WASM primär (Auftragskonform) + nativ sekundär.**
|
||||
|
||||
## 8. Phasen (Reihenfolge)
|
||||
1. Crate-Skelett `src-tauri/kernel2d` + Build-Script + Workspace-exclude + leere WASM-Fassade → `build:kernel2d` grün.
|
||||
2. Primitive + Schnitt + Fläche + Kreis portieren (trivial–mittel) + Batch-Fassade + Diff-Test-Harness (Zufall+Golden) → grün.
|
||||
3. Offset (Miter+1e-9-Fallback) + Fillet portieren → Golden für Selbstschnitt/spitze Winkel grün.
|
||||
4. Trim/Split/Join (`trimPolyline`, `splitAtIntersections`, `joinChains` — der Löwenanteil) → Struktur-Golden grün.
|
||||
5. `roomBoundary` (`detectRooms`) + `ceiling` + `roomArea`-Flächen + `stair` (StairParams).
|
||||
6. TS-Fassade umstellen (Alt → `.legacy.ts`), Import-Pfade der Aufrufstellen unverändert lassen, bestehende Suite grün, `npm run build` + WASM-Build sauber.
|
||||
@@ -1,54 +1,75 @@
|
||||
# DOSSIER Standalone
|
||||
|
||||
Browser-BIM für Wohnbau. Ein Werkzeug, um ein Wohnhaus aus semantischen
|
||||
Bauteilen zu modellieren und daraus saubere, normgerechte 2D-Pläne zu ziehen —
|
||||
ohne Revit, ohne Installation, im Browser.
|
||||
Eigenständiges BIM-Werkzeug für Wohnbau. Modelliert ein Wohnhaus aus
|
||||
semantischen Bauteilen und zieht daraus saubere, normgerechte 2D-Pläne —
|
||||
ohne Revit.
|
||||
|
||||
Das ist die eigenständige Browser-Variante des Rhino-Plugins
|
||||
Das ist die eigenständige Standalone-Variante des Rhino-Plugins
|
||||
[DOSSIER](https://git.kgva.ch/karim/DOSSIER): dieselbe Denkweise (Geschosse,
|
||||
Kategorie-Ebenen, mehrschichtige Bauteile, Prioritäts-Verschneidung), aber als
|
||||
React/Three.js-App statt als Rhino-Aufsatz.
|
||||
React/Three.js-App in einer Electron-Desktop-Shell statt als Rhino-Aufsatz.
|
||||
Kein Browser-Tab-Produkt: eigenes Fenster, eigene Rendering-Engine
|
||||
(Rust/WASM/WebGPU), gebaut für flüssiges CAD-Arbeiten statt für den Webview
|
||||
kleinster gemeinsamer Nenner.
|
||||
|
||||
## Grundgedanke
|
||||
|
||||
Es gibt **ein semantisches Modell** als einzige Wahrheit. Jede Ansicht — 3D,
|
||||
Grundriss, Schnitt, Ansicht — wird daraus **abgeleitet**. Darstellung
|
||||
Grundriss, Schnitt, Ansicht, PDF — wird daraus **abgeleitet**. Darstellung
|
||||
(Detailgrad, Linienstärken, Schraffuren) wird erst beim Rendern angewandt, nie
|
||||
in die Geometrie eingebacken. Wer eine Wand verschiebt, verschiebt sie überall.
|
||||
|
||||
Konkret heißt das auch: Der Grundriss entsteht nicht aus einem zerschnittenen
|
||||
3D-Mesh, sondern **analytisch aus den Bauteil-Parametern** (Wandachsen + Dicken →
|
||||
Linien, Öffnungen → Lücken + Symbol). Schnitte und Ansichten brauchen später den
|
||||
zweiten Weg — echte 3D-Projektion mit verdeckten Kanten.
|
||||
Linien, Öffnungen → Lücken + Symbol). PDF-Export ist derselbe Plan, nur mit
|
||||
echten mm-Stiftstärken statt Bildschirm-Hairlines — kein zweites Rendering.
|
||||
Schnitte und Ansichten brauchen den zweiten Weg — echte 3D-Projektion mit
|
||||
verdeckten Kanten (HLR) —, dessen Machbarkeit per Spike bewiesen, aber noch
|
||||
nicht ans UI angebunden ist.
|
||||
|
||||
## Stand heute
|
||||
|
||||
Ehrlich eingeordnet: aus dem Risiko-Spike ist ein **benutzbares 2D-CAD mit
|
||||
abgeleiteter 3D-Sicht** geworden. Der einfachere Teil steht und ist per
|
||||
Screenshot verifiziert; die schwierigen BIM-Kernrisiken sind bewusst noch offen.
|
||||
abgeleiteter 3D-Sicht, Vektor-PDF/DXF-Export und einer parametrischen
|
||||
Wand-Engine** geworden. Der einfachere Teil steht und ist per Screenshot/Probe
|
||||
verifiziert; die schwierigen BIM-Kernrisiken sind bewusst noch offen.
|
||||
|
||||
**Funktioniert:**
|
||||
- Semantisches Modell mit **mehrschichtigen Wänden** und automatischer
|
||||
**L-Ecken-Gehrung** (`computeJoins`); dieselbe Logik speist Plan *und* 3D.
|
||||
- **Parametrische Wände** (`ParametricWall`-Regelwerke: Grid/Modul/Sequenz/
|
||||
Referenzlinie/bedingte Dicke) lösen sich zu konkreten `Wall`-Objekten auf,
|
||||
statt jede Wand einzeln von Hand zu ziehen.
|
||||
- **Dokumentmodell wie in DOSSIER:** Zeichnungsebenen (Geschosse + Schnitte/
|
||||
Ansichten/Zeichnungen) × Kategorie-Ebenen (Code-Baum 1:1, `20 Wände`, `30 Decken` …).
|
||||
- **Zeichnen & Editieren** im Grundriss: Wand, Linie, Polylinie, Rechteck, Kreis;
|
||||
Snapping (Endpunkt/Mittelpunkt/Schnittpunkt/Lot/Raster/Ortho), Grips, Move/Copy/
|
||||
Offset, Spiegeln/Drehen/Array.
|
||||
- **Zeichnen & Editieren** im Grundriss: Wand, Linie, Polylinie, Rechteck, Kreis,
|
||||
Öffnungen, Treppen, Decken, Raumstempel; Snapping (Endpunkt/Mittelpunkt/
|
||||
Schnittpunkt/Lot/Raster/Ortho), Grips, Move/Copy/Offset, Spiegeln/Drehen/Array,
|
||||
Trim/Split/Join.
|
||||
- **Rhino-artiges Befehlssystem** mit getippten Koordinaten (`5,3` · `r5,3` ·
|
||||
`5<45`) und Tab-Feld-Zyklus (Länge → Winkel …).
|
||||
- **Vektor-Export:** PDF (A4/A3, Titelblock, echte mm-Stiftstärken nach ISO-Pen-
|
||||
Steps, keine Rasterbilder) und DXF — beide aus derselben `Plan`-Struktur wie
|
||||
der Bildschirm.
|
||||
- **PBR-Material-Bibliothek** (ambientCG-Import, `manifest.json`) für die
|
||||
3D-Ansicht.
|
||||
- **Resource Manager** (Component / Hatch / Line) — alles per id referenziert,
|
||||
zentral änderbar.
|
||||
- **Panel-System** (dockbar, stapelbar, floatend), Top-Bar + Statusleiste
|
||||
(echter Maßstab 1:N, Cursor X/Y), eigenes Kontextmenü, **i18n** (de/en).
|
||||
- **Import:** DXF (Konturen/Mesh) → Terrain-TIN; `.lin`/`.pat` für Linien/Schraffuren.
|
||||
- **Import:** DXF/DWG (Konturen/Mesh) → Terrain-TIN, Swisstopo/LV95-Geokontext,
|
||||
OSM-Kontextimport; `.lin`/`.pat` für Linien/Schraffuren.
|
||||
|
||||
**Bewusst noch offen** (die eigentlich harten Teile):
|
||||
- Echte **3D-Booleans** für Öffnungen — Türen/Fenster sind im Plan eine Lücke,
|
||||
noch kein geschnittenes Volumen.
|
||||
- **HLR** (verdeckte Kanten) für Schnitte und Ansichten — diese Views sind Stubs.
|
||||
- **HLR** (verdeckte Kanten) für Schnitte und Ansichten: Machbarkeit per
|
||||
OCCT-WASM-Spike bewiesen (`docs/welle-c-hlr-spike/`, `src/section/hlr.ts`),
|
||||
aber noch nicht ans UI/Dokumentmodell verdrahtet — Views sind noch Stubs.
|
||||
- **Prioritäts-T-/X-Stöße** mehrschichtiger Wände (Beton läuft durch, Putz
|
||||
verbindet seitlich) — das berüchtigte Risiko #1.
|
||||
- **Multi-Page-Layouts/Ausschnitte** (mehrere Viewports pro Blatt) — PDF-Export
|
||||
ist noch single-sheet.
|
||||
|
||||
Details und die Begründungen stehen im
|
||||
[HANDOVER](HANDOVER.md) (laufendes Arbeitsprotokoll) und in der
|
||||
@@ -61,43 +82,57 @@ Geometrie-Kernel verfrüht eingezogen.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Shell | Electron (eigenes randloses App-Fenster, kein Browser-Tab) |
|
||||
| Frontend | React + TypeScript + Vite |
|
||||
| 3D | Three.js |
|
||||
| 2D-Plan | eigener SVG-Renderer |
|
||||
| 3D | Three.js, plus eigener nativer Rust/WASM/WebGPU-Renderer (`render3d`) |
|
||||
| 2D-Plan | eigener SVG-Renderer, plus eigener nativer Rust/WASM/WebGPU-Renderer (`render2d`) |
|
||||
| Geometrie | eigener 2D-Kernel (`src/geometry/kernel2d.ts`), `delaunator` fürs Terrain |
|
||||
| Import | `dxf-parser`, eigene `.lin`/`.pat`-Parser |
|
||||
| Export | `jspdf`/`svg2pdf.js` (Vektor-PDF), eigener DXF-Writer |
|
||||
| Import | `dxf-parser`, `@mlightcad/libredwg-web` (DWG), eigene `.lin`/`.pat`-Parser |
|
||||
| Schnitt/HLR-Spike | `opencascade.js` (OCCT-WASM) — isoliert, noch nicht verdrahtet |
|
||||
|
||||
Geplant, aber noch nicht eingezogen: `rhino3dm` (NURBS / `.3dm`), web-ifc (IFC),
|
||||
OpenCascade/Manifold (exakte Booleans + HLR). Siehe ROADMAP §4.
|
||||
Manifold (exakte 3D-Booleans). OCCT-WASM ist für HLR bereits als Spike da (s.o.),
|
||||
aber noch nicht an Dokumentmodell/UI angebunden. Siehe ROADMAP §4.
|
||||
|
||||
## Entwicklung
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # Vite, http://localhost:5173
|
||||
npm run dev # Vite, http://localhost:5187
|
||||
npm run electron # Electron-Fenster (Dev-Server + randlose App-Shell)
|
||||
npx tsc -b # Typecheck
|
||||
npm run build # tsc -b && vite build
|
||||
npm test # vitest run
|
||||
```
|
||||
|
||||
Verifiziert wird visuell: `node scripts/probe.mjs` rendert die App headless und
|
||||
schreibt `scripts/probe.png` — Screenshot ansehen, Geometrie prüfen. Für
|
||||
Firefox-Fälle gibt es `scripts/probe-ff*.mjs` (Playwright).
|
||||
schreibt `scripts/probe.png` — Screenshot ansehen, Geometrie prüfen. Weitere
|
||||
`scripts/probe-*.mjs` decken einzelne Features ab (PDF, Trim, Materialien,
|
||||
Theme, Boolean-Ops …); für Firefox-Fälle gibt es `scripts/probe-ff*.mjs`
|
||||
(Playwright).
|
||||
|
||||
## Aufbau
|
||||
|
||||
```
|
||||
src/
|
||||
model/ semantisches Modell + Ableitungen (types, geometry, joins, terrain)
|
||||
geometry/ 2D-Kernel (offset/trim/fillet/intersect)
|
||||
model/ semantisches Modell + Ableitungen (types, parametricWalls, roomStamp, joins, terrain)
|
||||
geometry/ 2D-Kernel (offset/trim/fillet/intersect, ceiling, opening, roomArea/-Boundary, stair)
|
||||
commands/ Rhino-artiges Befehlssystem (engine, parser, registry, cmds/)
|
||||
tools/ interaktive Zeichenwerkzeuge + Snapping + Transformationen
|
||||
plan/ Grundriss-Ableitung (generatePlan) + PlanView (SVG)
|
||||
plan/ Grundriss-Ableitung (generatePlan) + PlanView (SVG) + glPlan (WebGL2-Renderer)
|
||||
viewport/ Viewport3D (Three.js)
|
||||
section/ HLR-Spike (OCCT-WASM) — Schnitte/Ansichten, noch nicht verdrahtet
|
||||
export/ PDF- und DXF-Export aus derselben Plan-Struktur wie der Bildschirm
|
||||
materials/ PBR-Material-Bibliothek (ambientCG-Import, Runtime)
|
||||
text/ Rich-Text (Beschriftungen, Textobjekte)
|
||||
panels/ dockbares Panel-System + die einzelnen Paletten
|
||||
state/ Store + Slices (project/selection/view/layout)
|
||||
ui/ Top-Bar, Statusleiste, Kontextmenü, Resource Manager, Command-Line
|
||||
io/ Import/Export (DXF, .lin, .pat)
|
||||
io/ Import/Export (DXF, DWG, .lin, .pat), Swisstopo/LV95, OSM-Kontext
|
||||
i18n/ Wörterbücher de/en
|
||||
src-tauri/ Rust-Crates (render2d/render3d) — headless per wasm-pack zu WASM
|
||||
gebaut (npm run build:engine), Tauri-Host selbst ist ausrangiert
|
||||
```
|
||||
|
||||
## Konventionen
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# RESEARCH — Bauteile Treppe/Fenster/Tür: Rhino-Plugin als Vorbild
|
||||
|
||||
Stand: 2026-07-05. Vergleich der authoritativen Rhino-Plugin-Implementierung
|
||||
(`/Users/karim/PROJECTS/DOSSIER/rhino/`, C#/Python + RhinoCommon-B-Rep) mit
|
||||
DOSSIER-STANDALONEs vereinfachter TS-Umsetzung. Ziel: Bauteile aufs Rhino-Niveau
|
||||
heben. Belege = Rhino-Funktionsnamen.
|
||||
|
||||
**Grundsatz:** Rhino hat echten B-Rep-Kernel (3D). DOSSIER hat extrudierte Meshes +
|
||||
`kernel2d` (2D). Darum getrennt: **2D-Symbol-Logik** (leicht in TS/kernel2d
|
||||
übernehmbar) vs. **3D** (braucht Mesh-Pipeline bzw. später truck).
|
||||
|
||||
## Treppe (Rhino `treppe.py`, 1784 Z. — massiv reicher als `stair.ts`)
|
||||
- **Typen:** gerade / L (3- und 4-Punkt mit Podest) / Wendel (inkl. Spindel-Cone bei r_in<0.05). TS hat alle drei, aber simpler.
|
||||
- **3D-Querschnitt-Modi** `massiv`/`flach`/`plattenrand` (`_treppe_profile_2d`) — TS: nur Stufenboxen. **fehlt.**
|
||||
- **2D-Plansymbol** (`_make_treppe_2d_symbol`): Tritte, Lauflinie (4 Pfeil-Styles: klassisch/filled/breit/voll, visuell zentriert via `mid_off`), **Aussenlinie/Outline** (alle 3 Typen), Bruchlinie, Podest-Hexagon. TS: Tritte+Bruch+Lauflinie(1 Style) da; **Aussenlinie fehlt komplett**, Referenz links/mitte/rechts fehlt.
|
||||
- **Soll-Schrittmass** (2S+A, Lock), Show-Flags, Grips — fehlen in TS.
|
||||
|
||||
## Fenster (Rhino `elements.py` `_make_oeffnung_pieces`)
|
||||
- **3D:** Rahmen (BooleanDiff), Mittelpfosten je Flügel (1–4), Glas (einfach 12mm / Doppel 2×6+16), Sims aussen (4 Styles), Rahmen-Offset innen/mitte/aussen. TS: nur 2D-Rahmen+Glaslinien, Wand-Boolean-Loch (`1407c68`); kein Rahmen-3D.
|
||||
- **2D:** TS hat Rahmen + 1–2 Glaslinien (grob/mittel/fein). **Fehlt:** Brüstungslinie im Plan, Flügel-Mittelpfosten, Anschlag-Striche (nur Tür hat sie), Flügelanzahl.
|
||||
|
||||
## Tür (Rhino `elements.py`)
|
||||
- **Typen:** `normal` / `wandoeffnung` (reiner Durchbruch, kein Blatt). **Rahmen:** `zarge` (3-seitig, im Wandquerschnitt) / `block` (+5cm Überhang).
|
||||
- **2D-Schwung** (`_make_tuer_swing_curves`): Blatt+Arc, hinge_side/open_angle/aussenseite/swing_invert; Bogen-Anlage am Rahmen (standard/detail). **2D-Sturzlinien** (`_make_tuer_sturz_curves`, gestrichelt, Modi keine/innen/aussen/beide — SIA: Sturz über Schnittebene → gestrichelt). TS: Blatt+Arc+Anschlag (fein) da; **Sturzlinien fehlen komplett**, `wandoeffnung`-Typ + swing_invert fehlen.
|
||||
|
||||
---
|
||||
|
||||
## Gesamtreihenfolge „was zuerst" (Agenten-Empfehlung)
|
||||
|
||||
**Gruppe A — 2D-leicht, hoher Plan-Wert, kernel2d-kompatibel:**
|
||||
1. **Treppe Aussenlinie/Outline** (alle 3 Typen; gerade=4 Linien, L via Linienschnitt `_line_intersect_xy`, Wendel=2 Bögen+radiale). Ohne sie wirkt der Plan unfertig.
|
||||
2. **Fenster Brüstungslinie** im Plan (1 gepunktete Linie bei sillHeight>0). XS.
|
||||
3. **Tür Sturzlinien** (gestrichelt, SIA) — Schulfall der geplanten Schnitt/Ansichts-Logik.
|
||||
4. **Treppe Referenz links/mitte/rechts** + Lauflinie visuell zentriert.
|
||||
5. **Fenster Flügel-Mittelpfosten** (2D, `wingCount` ins Modell).
|
||||
6. **Tür `wandoeffnung`-Typ** (reine Öffnung ohne Blatt/Symbol).
|
||||
7. Treppe Show-Flags + `obere_dashed`; 8. Fenster Anschlag-Striche.
|
||||
|
||||
**Gruppe B — 2D mittel:** Tür-Schwung am Rahmen; Treppe Pfeil-Style `filled`; Fenster/Tür Styles/Presets; swing_invert.
|
||||
|
||||
**Gruppe C — 3D (nach Mesh-/B-Rep-Pipeline):** Treppe `flach`-Querschnitt; Rahmen als Mesh (Fenster/Tür); Türblatt+Glas; Sims; L-Podest-Hexagon; Wendel helikoidale Unterseite (truck).
|
||||
|
||||
**Bezug PENDENZEN:** Gruppe-A-Items 1–3 (Aussenlinie/Brüstung/Sturz) fallen in das Backlog-Item „Schnitt- vs. Ansichts-Darstellung nach Schnitthöhe" — Sturzlinie = Bauteil unter Schnittebene → gestrichelte Überkopf-Projektion. Gruppe C wartet auf den Textur-/Mesh-Pipeline-Nachfolger bzw. truck.
|
||||
@@ -0,0 +1,162 @@
|
||||
# RESEARCH — Von BIM-Tool zu „echtem CAD": Ansätze aus fortgeschrittenen Programmen
|
||||
|
||||
Stand: 2026-07-05. Direkt am Quellcode studiert (Repos nach `/tmp/cad-study` geklont,
|
||||
liegen NICHT im Projekt). Ziel: konkrete, verfolgbare Ansätze, um DOSSIER von einem
|
||||
spezialisierten BIM/Architektur-Werkzeug zu einem allgemeineren CAD zu erweitern.
|
||||
|
||||
## Studierte Programme
|
||||
|
||||
| Repo | Was | Lizenz | Stack | Für uns |
|
||||
|---|---|---|---|---|
|
||||
| [OpenCADStudio](https://github.com/HakanSeven12/OpenCADStudio) | Eigenständige 2D/3D-CAD-App, ~200k LOC Rust | GPL-3.0 | Iced (GUI) + wgpu + WASM-Web-Build | **App-Architektur-Blaupause** (Entity-Modell, Command-System, Snap, Grips). Fast unser Stack. |
|
||||
| [truck](https://github.com/ricosjp/truck) | Reiner Rust-CAD-**Kernel** (NURBS/B-Rep/Mesh/Boolean/STEP) | Apache-2.0 | 16 Crates, wgpu-Rendering + `truck-js` (WASM) | **3D-Kernel-Blaupause**. OpenCADStudio nutzt ihn als 3D-Backend. |
|
||||
| [acadrust](https://github.com/HakanSeven12/acadrust) | Pure-Rust **DWG+DXF** R13–R2018 read+write, 41 Entity-Typen | **MPL-2.0** | nom/byteorder/flate2/nalgebra/encoding_rs (alles pure Rust) | **DWG/DXF-Rückgrat**, MPL-2.0 nutzbar, sehr wahrscheinlich WASM-baubar. |
|
||||
|
||||
**Lizenz-Klarstellung:** `acadrust` ist **MPL-2.0** (file-level copyleft — für uns nutzbar,
|
||||
keine GPL-Ansteckung), NICHT GPL. `truck` ist Apache-2.0 (frei nutzbar). Nur OpenCADStudio
|
||||
selbst ist GPL-3.0 — wir lesen es als **Architektur-Referenz**, kopieren keinen Code.
|
||||
|
||||
---
|
||||
|
||||
## Die zentrale Lektion: was ein „echtes CAD" architektonisch ausmacht
|
||||
|
||||
DOSSIER ist heute **domänenzentriert** (Wand/Öffnung/Component/Plan-Pipeline, TS-Modell).
|
||||
Ein echtes CAD (Rhino/AutoCAD-artig) hat stattdessen einen **generischen Kern**. Aus
|
||||
OpenCADStudios `src/` destilliert:
|
||||
|
||||
### 1. Generisches Entity-Modell mit Traits (statt Wand-Spezialfall)
|
||||
`src/entities/` = 41 Entity-Typen (line, arc, circle, lwpolyline, spline, ellipse, hatch,
|
||||
text, mtext, dimension, leader, insert/block, table, viewport, mesh, solid3d, …). Jeder
|
||||
Typ implementiert ein **Trait-Set** statt einer Sonderbehandlung:
|
||||
- `TruckConvertible` → 3D-Kernel-Geometrie
|
||||
- `FallbackTess` → Display-Geometrie (Punkte/Snap-Vertices)
|
||||
- `Grippable` → editierbare Griffe + Hover-Menü (Add Vertex / Convert to Arc / Reverse / Lengthen…)
|
||||
- `PropertyEditable` → Eigenschaften-Panel
|
||||
- `Transformable` → Move/Rotate/Scale/Mirror
|
||||
- `MassProps` → Fläche/Umfang für Abfragen
|
||||
|
||||
→ **DOSSIER-Lehre:** ein generisches `Entity`-Modell mit Trait-Dispatch neben das BIM-Modell
|
||||
stellen. Wir haben Ansätze (`Drawing2D`, splitJoin-Editoren, kernel2d), aber keine einheitliche
|
||||
Entity-Taxonomie mit Traits.
|
||||
|
||||
### 2. Modeless Command-System — der eine `StepInput`-Step-Machine
|
||||
`src/command.rs`: EIN Enum `StepInput { Point / Text / EntityPick / StructurePick /
|
||||
SelectionComplete }`. **Jede** Eingabequelle (Kommandozeile, Viewport-Klick, Pick, Selektion,
|
||||
Dynamic Input, Plugin-API, Headless-Script) übersetzt in `StepInput` und läuft durch **eine**
|
||||
`feed_command`-Funktion → treibt die aktive `CadCommand`-Step-Machine, egal woher der Schritt kam.
|
||||
|
||||
→ **DOSSIER-Lehre:** Das ist der größte UX-Unterschied. Echtes CAD = „Befehl tippen → Punkte
|
||||
picken → Optionen". DOSSIER ist heute Tool-Button + Direktmanipulation. Ein `StepInput`-Funnel +
|
||||
Kommandozeile wäre der Hebel, um beliebige Werkzeuge einheitlich, scriptbar und headless-testbar
|
||||
zu machen.
|
||||
|
||||
### 3. Object-Snap-Engine (eigenständig, groß)
|
||||
`src/snap.rs` = 70 KB nur für OSNAP (endpoint/midpoint/center/intersection/tangent/
|
||||
perpendicular/…). Wir haben Teil-Snapping; ein echtes CAD hat eine dedizierte, vollständige
|
||||
Snap-Schicht.
|
||||
|
||||
### 4. Universelle Grip-Editierung
|
||||
Jede Entity liefert Griffe + kontextuelle Griff-Menüs mit teils numerischem Follow-up
|
||||
(„Lengthen" fragt Wert an der Kommandozeile ab). Einheitlich über das `Grippable`-Trait.
|
||||
|
||||
### 5. `CadDocument` als Dokumentmodell = das DWG/DXF-Objektmodell
|
||||
OpenCADStudio nutzt `acadrust::CadDocument` DIREKT als sein Datenmodell (Entities + Objects +
|
||||
Tables: Layer/Linetype/Style/Blocks + XData). D. h. das native CAD-Austauschformat IST das
|
||||
interne Modell → verlustfreier Round-Trip „for free".
|
||||
|
||||
---
|
||||
|
||||
## truck — 3D-Kernel-Blaupause (+ harte WASM-Realität)
|
||||
|
||||
Saubere Schichtung (Ship-of-Theseus, kleine ersetzbare Crates):
|
||||
|
||||
```
|
||||
truck-base Basistraits/Toleranz (cgmath)
|
||||
truck-geotrait ParametricCurve / ParametricSurface Traits
|
||||
truck-geometry Knot-Vektor, B-Spline, NURBS ← Kurven/Flächen
|
||||
truck-topology vertex/edge/wire/face/shell/solid ← B-Rep-Topologie
|
||||
truck-modeling Geometrie + Topologie integriert ← Solids bauen
|
||||
truck-polymesh Polygon-Datenstruktur + Meshing
|
||||
truck-meshalgo Tessellation der Shapes
|
||||
truck-shapeops Boolean-Ops auf Solids (v0.4 — früh)
|
||||
truck-stepio STEP read/write (v0.3)
|
||||
truck-platform wgpu-Grafik-Utility ┐ Rendering — brauchen wir NICHT
|
||||
truck-rendimpl Shape/Mesh-Visualisierung ┘ (wir haben eigenen wgpu-Renderer „Nordstern")
|
||||
truck-js WASM-Wrapper (v0.2)
|
||||
```
|
||||
|
||||
**WASM-Realität (zwei Befunde abgeglichen):**
|
||||
- OpenCADStudio pinnt `truck-meshalgo` 0.4 und schaltet den `solid3d`-Feature **im WASM-Build ab**,
|
||||
weil dessen ACIS-/vtkio-Pfad über `xz2 → lzma-sys` (C-Bibliothek) nicht nach wasm32 kommt →
|
||||
**im Web ist OpenCADStudio nur 2D.**
|
||||
- Neuere `truck-meshalgo` (0.6) gated `rayon`/`vtkio` selbst per `cfg(not(target_arch="wasm32"))`
|
||||
aus → **baut zu wasm32, aber single-threaded**. Der reine Geometrie-Stack
|
||||
(base → geotrait → geometry → topology → polymesh → meshalgo → modeling → shapeops) hat keine
|
||||
native-only-Deps und ist **sauber vom wgpu-Rendering trennbar** (truck-platform/-rendimpl weglassen).
|
||||
|
||||
**Reifegrad (Apache-2.0, aktiv, ~1 Hauptentwickler; ausdrücklich KEIN Produktionskern):**
|
||||
- NURBS/B-Spline + B-Rep-Topologie: **stabil** (v0.5/0.6).
|
||||
- Booleans (`truck-shapeops`): `and`/`or` vorhanden, aber **instabil bei coincident geometry**
|
||||
(Issue #114); **`difference` fehlt** (#85); **Fillets fehlen komplett**. Der Fork
|
||||
[`monstertruck`](https://github.com/virtualritz/monstertruck) hat difference + rolling-ball-Fillet.
|
||||
→ Für unsere Wandöffnungen (boolean holes) haben wir das ohnehin schon selbst gelöst (`spanCutouts`).
|
||||
- `truck-stepio`: STEP **schreiben** (aber NICHT für boolean-operierte Shapes) + **lesen** (v0.6 beta,
|
||||
neues `truck_stepio::in`).
|
||||
- `truck-js`: fertiger wasm-bindgen-Wrapper (kein npm-Paket → selbst per `wasm-pack` bauen), bündelt
|
||||
modeling/shapeops/meshalgo/stepio; `IntoWasm`-Muster.
|
||||
|
||||
---
|
||||
|
||||
## acadrust — DWG/DXF-Rückgrat
|
||||
|
||||
Pure Rust, **MPL-2.0**, `CadDocument`-Objektmodell (entities/objects/tables/xdata/classes),
|
||||
41 Entity-Typen, DWG „208/208 roundtrip-perfect", serde optional, failsafe-Parsing, ~40 Codepages.
|
||||
**Alle Deps pure Rust** (nom/byteorder/flate2-miniz/nalgebra/indexmap/ahash/encoding_rs) → sehr
|
||||
wahrscheinlich zu wasm32 baubar (Datei-I/O-API müsste auf Bytes/Reader statt `std::fs` umgestellt
|
||||
werden — vermutlich schon vorhanden). Heute nutzen wir JS `dxf-parser` (nur 2D) + `libredwg-web`.
|
||||
|
||||
---
|
||||
|
||||
## Web-Import-Landkarte (für den GEO-BLOCK / Import-Backlog)
|
||||
|
||||
Pro Format die aktive, web-/WASM-taugliche Lib (Stand Mitte 2026):
|
||||
|
||||
| Format | Empfohlene Lib | Lizenz | Anmerkung |
|
||||
|---|---|---|---|
|
||||
| DWG/DXF (browser) | `@mlightcad/cad-viewer` / `libredwg-web` (nutzen wir) **oder** `acadrust` (Rust) | MIT / GPL-3.0 / **MPL-2.0** | acadrust = Rust-Weg, MPL-2.0 |
|
||||
| STEP/IGES/BREP | `occt-wasm` (OCCT V8, ~4.5 MB) oder unser `opencascade.js` | LGPL-2.1 | occt-wasm = moderne TS-API |
|
||||
| **IFC** (BIM!) | `web-ifc` (ThatOpen) | MPL-2.0 | De-facto-Standard, read+write, sehr aktiv |
|
||||
| SHP/GIS | `shpjs` + `proj4` | MIT | direkt für GEO-BLOCK (SWISSIMAGE/Terrain) |
|
||||
| Punktwolken LAS/LAZ | `@loaders.gl/las` (+ `laz-perf`) | MIT/Apache | Rendering: `potree-core` oder deck.gl |
|
||||
| E57 | `e57-js` (Emscripten/libE57Format) | MIT | neu, funktional, sehr obskur |
|
||||
| glTF/OBJ/STL | three.js-Loader (haben wir via `three`) | MIT | |
|
||||
| 3MF | `lib3mf` (WASM) oder `THREE.3MFLoader` | BSD-2 / MIT | |
|
||||
| PDF-Vektor | `pdfjs-dist` `getOperatorList()` | Apache-2.0 | kein fertiges PDF→DXF; roher Pfad-Stream |
|
||||
|
||||
---
|
||||
|
||||
## Priorisierte Ansätze für DOSSIER (die eigentliche Antwort)
|
||||
|
||||
Reihenfolge = Nutzen × Machbarkeit, ohne die BIM-Stärke zu opfern.
|
||||
|
||||
1. **Generisches Entity-Modell + Trait-Dispatch** (Fundament). Neben das BIM-Modell eine
|
||||
generische Entity-Schicht (line/arc/circle/polyline/spline/text/dimension/hatch/block) mit
|
||||
Traits à la OpenCADStudio (Display/Grips/Props/Transform). Unser `kernel2d` liefert schon die
|
||||
Geometrie-Operationen dafür. **Größter struktureller Hebel.**
|
||||
2. **Modeless Command-System** (`StepInput`-Funnel + Kommandozeile). Ein Eingabe-Enum, eine
|
||||
`feed_command`-Schleife, `CadCommand`-Step-Machines. Macht Werkzeuge einheitlich, scriptbar,
|
||||
headless-testbar. **Größter UX-Hebel Richtung „echtes CAD".**
|
||||
3. **DWG/DXF-Round-Trip über `acadrust`** (Rust/WASM). MPL-2.0, pure Rust → passt in unseren
|
||||
neuen `src-tauri`-Kern. Ersetzt langfristig die JS-Parser, bringt echten Import/Export.
|
||||
Erst: WASM-Baubarkeit + Bytes-API verifizieren (Spike wie beim Textur-Spike).
|
||||
4. **Vollständige Object-Snap-Schicht** als eigenes Modul (endpoint/mid/center/intersection/
|
||||
tangent/perp/…), aufbauend auf `kernel2d`.
|
||||
5. **3D-B-Rep später, selektiv** (truck-geometry/-topology/-modeling als reine Crates, OHNE
|
||||
truck-Rendering — wir haben Nordstern). NUR wenn wir echte NURBS/Solids/Booleans brauchen; der
|
||||
Meshing-/Boolean-Pfad ist WASM-problematisch (native C-Deps) und Booleans sind noch früh.
|
||||
Bis dahin: truck als **Referenz** lesen, nicht einbinden.
|
||||
|
||||
**Kernaussage:** DOSSIERs Rust-nach-WASM-Kurs (den wir mit `kernel2d` gerade gehen) ist genau
|
||||
richtig — OpenCADStudio bestätigt ihn. Der Weg zu „echtem CAD" führt über (1) generisches
|
||||
Entity-Modell und (2) modeless Command-System; (3) `acadrust` bringt echten DWG/DXF-Austausch.
|
||||
3D-B-Rep (truck) ist ein späterer, selektiver Schritt mit klaren WASM-Vorbehalten.
|
||||
@@ -0,0 +1,137 @@
|
||||
# SPIKE — Bild-Texturen in `render3d` (`RenderStyle::Textured` real machen)
|
||||
|
||||
Stand: 2026-07-05. **Auftrag/Übergabe für einen Agenten. Kleinster ehrlicher
|
||||
Durchstich — kein Produktfeature, keine Integration.**
|
||||
|
||||
Ziel: Beweisen, dass der bestehende wgpu-3D-Renderer echte **Bild-Texturen** auf
|
||||
Wandflächen darstellen kann, sichtbar im `spike3d`-Fenster. Am Ende steht eine
|
||||
belastbare Aussage, wie viel Arbeit „richtig gutes texturiertes 3D" wirklich ist —
|
||||
statt Spekulation.
|
||||
|
||||
---
|
||||
|
||||
## 0. Ausgangslage (verifiziert am 2026-07-05)
|
||||
|
||||
- `render3d` ist **kein** Three.js-Wrapper, sondern ein eigenständiger wgpu-Renderer
|
||||
(~6400 LOC): echte GPU-Pipeline (wgpu 29), Tiefenpuffer, MSAA, WGSL-Shader.
|
||||
Läuft nativ (winit-Spike), headless (naga-validiert) und im Browser (WebGPU).
|
||||
- **`RenderStyle::Textured` existiert bereits als Stub** (`gpu.rs:34` Enum-Variante,
|
||||
`gpu.rs:47` Parse aus `"textured"`) — es gibt aber **kein echtes Texturing**:
|
||||
kein Sampler, keine Textur-Bind-Group, keine Bilddaten.
|
||||
- Die `cap_pipeline` mit Layout `[pos vec3, uv vec2]` + `CAP_WGSL` ist **nicht** für
|
||||
Bildtexturen, sondern für die **Schnittflächen-Kappen** (prozedurale Schraffur);
|
||||
die UVs steuern dort den Schraffur-Abstand. **Nicht damit verwechseln.**
|
||||
- **Günstig für uns:** Die Muster, die der Spike braucht, existieren schon —
|
||||
ein UV-tragendes Vertex-Layout und eine zweite/dritte Pipeline, die sich die
|
||||
`Globals`-Bind-Group teilt (`grid`, `cap`). Der texturierte Mesh-Pfad reiht sich
|
||||
1:1 in dieses Muster ein.
|
||||
|
||||
---
|
||||
|
||||
## 1. Echte Symbole — vor dem Coding lesen
|
||||
|
||||
| Was | Ort |
|
||||
|---|---|
|
||||
| Vertex heute interleaved `[pos.xyz, normal.xyz, color.rgb]`, `FLOATS_PER_VERTEX`, `Mesh`-Struct | `src-tauri/render3d/src/types.rs:322` ff. |
|
||||
| Quad-Emitter (Normale + Farbe je Vertex, Reihenfolge `(0,1,2)+(0,2,3)`) | `src-tauri/render3d/src/mesh.rs:886` |
|
||||
| Wand-Extrusion / Mesh-Bau | `src-tauri/render3d/src/mesh.rs` — `extrude_wall` (`:91`), `build_walls_mesh` (`:911`) |
|
||||
| Haupt-Pipeline + `Globals`-Bind-Group (group 0) | `src-tauri/render3d/src/gpu.rs:232`–`:320` |
|
||||
| Vorlage „zweite Pipeline teilt sich Globals" — Grid | `src-tauri/render3d/src/gpu.rs:329` |
|
||||
| Vorlage „Pipeline mit UV-Layout `[pos vec3, uv vec2]`" — Cap | `src-tauri/render3d/src/gpu.rs:408` |
|
||||
| `RenderStyle`-Enum + Stub `Textured` | `src-tauri/render3d/src/gpu.rs:34`, `:47` |
|
||||
| Pipeline-Bindung im Render-Pass (Muster für Stil-Umschaltung) | `src-tauri/render3d/src/gpu.rs:847` |
|
||||
| Shader als WGSL-Konstanten (`MESH_WGSL`, `CAP_WGSL`) | `src-tauri/render3d/src/shaders.rs` |
|
||||
| Beleuchtungsmodell (hemisphärisch + Directional + Fill) — Doku | `src-tauri/render3d/src/shaders.rs:1`–`40` |
|
||||
| naga-WGSL-Validierung headless (Test-Vorlage) | `src-tauri/render3d/src/lib.rs:924` (`cap_module`) |
|
||||
| Fenster-Spike mit Orbit-Kamera | `src-tauri/render3d/src/bin/spike3d.rs` |
|
||||
|
||||
---
|
||||
|
||||
## 2. Umfang — exakt das, nicht mehr
|
||||
|
||||
### 2.1 UVs auf Wandflächen
|
||||
- In der Quad-Emitter-Funktion (`mesh.rs:886`) je Vertex eine **UV** berechnen:
|
||||
planare Projektion in **Metern** — `u` = Distanz entlang der Wandachse,
|
||||
`v` = Höhe (z). Textur-Raster damit weltmassstäblich (z. B. 1 Kachel = 1 m).
|
||||
- Den bestehenden `[pos, normal, color]`-Pfad **bitgleich unangetastet** lassen.
|
||||
Zwei zulässige Wege (Agent wählt begründet):
|
||||
1. **Separates additives UV-Array** in `Mesh` (Default leer/None), oder
|
||||
2. **Paralleler `build_walls_mesh_textured`** → interleaved
|
||||
`[pos.xyz, normal.xyz, uv.xy]`.
|
||||
- Regressionstests für den Alt-Pfad müssen grün bleiben (siehe §4).
|
||||
|
||||
### 2.2 Test-Textur prozedural (kein Asset, keine `image`-Crate)
|
||||
- Ein **256×256 RGBA-Schachbrett/Grid im Code** generieren (`Vec<u8>`).
|
||||
- `device.create_texture` + `queue.write_texture` + `Sampler`
|
||||
(`FilterMode::Linear`, `AddressMode::Repeat`). Mipmaps optional (nice-to-have für
|
||||
flache Blickwinkel; kein Muss für den Spike).
|
||||
- Selbstständig, damit der Spike ohne Dateipfade/Asset-Pipeline läuft.
|
||||
|
||||
### 2.3 Textur-Bind-Group (group 1)
|
||||
- Neue Bind-Group-Layout mit `texture_view` (`TextureSampleType::Float`) +
|
||||
`sampler`. **`Globals` bleibt group 0** und unverändert.
|
||||
|
||||
### 2.4 Textured-Pipeline + WGSL (`MESH_TEXTURED_WGSL`)
|
||||
- Vertex-Layout `[pos vec3, normal vec3, uv vec2]`, `TriangleList`.
|
||||
- **Dieselbe Beleuchtung wie `MESH_WGSL`** (hemisphärisches Ambient + Directional +
|
||||
Fill) — nur **Albedo = `textureSample(tex, samp, uv)`** statt Vertex-Farbe.
|
||||
- Depth-Format, MSAA (`SAMPLE_COUNT`) und Color-Target **identisch** zur
|
||||
Haupt-Pipeline (sonst inkompatibler Render-Pass).
|
||||
- Pipeline-Layout bindet group 0 (Globals) **und** group 1 (Textur).
|
||||
|
||||
### 2.5 Verdrahten
|
||||
- Bei `RenderStyle::Textured` im Render-Pass die neue Pipeline + beide Bind-Groups
|
||||
setzen (Muster: `cap_pipeline`-Bindung bei `gpu.rs:847`).
|
||||
|
||||
### 2.6 Spike sichtbar machen
|
||||
- `spike3d.rs` so erweitern, dass der Stil auf `Textured` schaltbar ist
|
||||
(Tastendruck, z. B. `T`, **oder** Startkonstante). Die Demo-Wände sollen
|
||||
texturiert im Orbit erscheinen.
|
||||
|
||||
---
|
||||
|
||||
## 3. Randbedingungen (hart)
|
||||
|
||||
- **Nur** die `render3d`-Crate. `src/web.rs` und die Tauri-/`native3d`-Oberfläche
|
||||
**nicht** anfassen.
|
||||
- Feature-gegatet unter dem bestehenden `render`/`window`-Feature.
|
||||
**Default-Build und Default-Darstellung bleiben unverändert.**
|
||||
- **Keine neuen Dependencies** (insbesondere **kein `image`-Crate**) für den Spike.
|
||||
- Term-/Reihenfolge-sensible Geometrie (Parität) wird **nicht** berührt — es kommt
|
||||
nur additiv ein UV-Kanal + ein zweiter Render-Pfad dazu.
|
||||
- Kommentar-Stil und Sprache (Deutsch, ausführliche Begründungs-Kommentare) wie im
|
||||
umgebenden Code beibehalten.
|
||||
|
||||
---
|
||||
|
||||
## 4. Akzeptanz / Verifikation
|
||||
|
||||
1. `cargo test` (im Crate-Verzeichnis `src-tauri/render3d`) **grün**, inklusive:
|
||||
- bestehende Mesh-Regression (Alt-Pfad `[pos,normal,color]` unverändert),
|
||||
- **neuer naga-Validierungstest** für `MESH_TEXTURED_WGSL` (Vorlage:
|
||||
`lib.rs:924`).
|
||||
2. `cargo run --features window --bin spike3d` zeigt die Demo-Wände mit
|
||||
**erkennbarer, korrekt gemappter** Schachbrett-Textur:
|
||||
- Raster weltmassstäblich (in Metern), keine Verzerrung an Gehrungen/Ecken,
|
||||
- beleuchtet wie im Shaded-Modus (Volumen bleibt ablesbar).
|
||||
3. Umschalten Shaded ↔ Textured zur Laufzeit (oder per Startkonstante) funktioniert
|
||||
ohne Re-Meshing-Crash.
|
||||
|
||||
---
|
||||
|
||||
## 5. Abschlussbericht (vom Agenten am Ende zu liefern)
|
||||
|
||||
- Welche Dateien geändert/hinzugefügt wurden und warum.
|
||||
- Wie die UVs projiziert werden (Achswahl, Massstab, Verhalten an Gehrungen).
|
||||
- Welcher der beiden UV-Wege (§2.1) gewählt wurde und weshalb.
|
||||
- **Ehrliche Lückenliste für „richtig gutes" Texturing:** Asset-/Bild-Datei-Laden,
|
||||
Material→Textur-Zuordnung (Wandtyp/Layer → Material), Normal-/Roughness-Maps
|
||||
(PBR), anisotropes Filtern + Mipmaps, Web-Pfad (`web.rs`/WebGPU), UI zum
|
||||
Zuweisen. Grobschätzung Aufwand je Punkt.
|
||||
|
||||
---
|
||||
|
||||
## 6. Nicht im Scope
|
||||
|
||||
Asset-/Bild-Datei-Laden · Material-System · mehrere Texturen gleichzeitig ·
|
||||
PBR/Normal-Maps · Web-Pfad (`web.rs`) · jegliche UI · Anbindung unter die Webview.
|
||||
@@ -0,0 +1,175 @@
|
||||
# ARCHITEKTUR-BRIEFING: Tauri + wgpu (Korrigiert)
|
||||
|
||||
**Stand:** 2026-07-01 — **KORREKTUR** (vorherige Dokumente waren unvollständig)
|
||||
|
||||
---
|
||||
|
||||
## Die Entscheidung (FINAL)
|
||||
|
||||
**Browser-CAD (alt) → Desktop Tauri-App mit Rust-Backend + wgpu-Rendering (neu)**
|
||||
|
||||
| Aspekt | Alt | Neu |
|
||||
|---|---|---|
|
||||
| **App-Form** | Browser-Tab | Desktop Tauri-Window |
|
||||
| **Frontend** | React/Vite (TS) | React/Vite (TS) — UNVERÄNDERT |
|
||||
| **2D-Rendering** | SVG-Plan | SVG-Plan — UNVERÄNDERT |
|
||||
| **3D-Rendering** | three.js/WebGL | **wgpu** (Vulkan/Metal/DX12) |
|
||||
| **Rechenintensive Ops** | TS in Browser | **Rust im Backend** |
|
||||
| **Betriebssystem** | cross-platform browser | Windows/macOS/Linux Desktop App |
|
||||
|
||||
---
|
||||
|
||||
## Warum dieser Umstieg?
|
||||
|
||||
**Problem mit three.js/WebGL:**
|
||||
- komplexe Möbel = 100k+ Polygone
|
||||
- mehrere parallele Ops (kernel2d, bool-Ops, DXF-Parser, SIA-Raumerkennung)
|
||||
- WebGL hat harte Limits (GPU VRAM, draw calls, single-threaded)
|
||||
- → Laggy, nicht skalierbar für Professional CAD
|
||||
|
||||
**Lösung: Tauri + wgpu + Rust**
|
||||
- **wgpu** = low-level GPU API (direkt zu Vulkan/Metal/DX12, nicht WebGL)
|
||||
- **Rust** = rechenintensive Ops parallelisiert + native performance
|
||||
- **Desktop** = native App, nicht Browser (bessere Kontrolle, bessere Perf)
|
||||
- **React-Frontend bleibt** = UI/Sketching/Panels unverändert (wgpu nur für 3D-Display)
|
||||
|
||||
---
|
||||
|
||||
## Der neue Stack (FINAL)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Desktop Tauri-Window │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ React/Vite (UI-Shell, State, Panels) │
|
||||
│ ├─ SVG 2D-Plan (Grundriss) │
|
||||
│ └─ wgpu 3D-Viewport (3D-Ansicht + Rendering) │
|
||||
│ │
|
||||
│ ↓ invoke (IPC) ↓ │
|
||||
│ │
|
||||
│ Rust-Backend (src-tauri/) │
|
||||
│ ├─ computeJoins() — Wand-Eckverbindungen │
|
||||
│ ├─ kernel2d() — Offset/Trim/Extend/… │
|
||||
│ ├─ parseShapeFromDwg() — Geometrie-Import │
|
||||
│ ├─ detectRooms() — SIA-Raumerkennung │
|
||||
│ └─ booleanOps() — Union/Differenz/Schnitt │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Nicht verändert:**
|
||||
- App.tsx, state/, commands/, panels/, model/types.ts
|
||||
- UI-Logik, Befehlssystem, Sketching-Tools
|
||||
|
||||
**Neu/geändert:**
|
||||
- `src-tauri/` — Rust-Crate für Backend
|
||||
- `src/compute/index.ts` — Boundary (invoke + TS-Fallback)
|
||||
- Rendering-Engine: **three.js → wgpu**
|
||||
- Build: `npm run tauri:build` statt `npm run build`
|
||||
|
||||
---
|
||||
|
||||
## Die Aufgabe (vier Agents)
|
||||
|
||||
**Siehe: `docs/design/tauri-migration-plan.md`**
|
||||
|
||||
Vier parallele Agents:
|
||||
|
||||
1. **Agent: Tauri-Shell Setup** → `src-tauri/` + Cargo.toml + main.rs (Tauri window registrieren)
|
||||
2. **Agent: Compute-Boundary (TS)** → `src/compute/index.ts` (invoke-Wrapper + TS-Fallbacks)
|
||||
3. **Agent: Rust compute_joins Impl** → `src-tauri/src/geometry.rs` (erste Op, Proof-of-Concept)
|
||||
4. **Agent: Vite/Package-Integration** → vite.config.ts + package.json (Tauri-Plugins, scripts)
|
||||
|
||||
**Deliverable:** lauffähige Desktop-App, wand-Edit triggert Rust-Op, Output identisch TS-Version.
|
||||
|
||||
---
|
||||
|
||||
## Dev-Workflow (post-Tauri)
|
||||
|
||||
```bash
|
||||
# Dev: Terminal 1 (Vite)
|
||||
npm run dev # localhost:5173
|
||||
|
||||
# Dev: Terminal 2 (Tauri)
|
||||
npm run tauri:dev # öffnet Desktop-Window, zeigt auf localhost:5173
|
||||
|
||||
# Build
|
||||
npm run tauri:build # → Windows .exe / macOS .app / Linux .deb
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rendering: three.js → wgpu (Wichtig!)
|
||||
|
||||
**3D-Viewport wird NEU implementiert in wgpu:**
|
||||
- Nicht: "drei.js mit Rust-Fallback"
|
||||
- **Ja:** wgpu als native GPU-Renderer (skaliert auf 100k+ Polygone)
|
||||
|
||||
**2D-Plan bleibt SVG** (keine Änderung).
|
||||
|
||||
**Timeline für wgpu-Impl:**
|
||||
- Milestone 1 (jetzt): Tauri-Shell + Rust-Compute-Ops
|
||||
- Milestone 2 (nächst): wgpu 3D-Viewport-Impl (ersetzt drei.js/WebGL)
|
||||
- Milestone 3: Live clipping/sectioning in wgpu
|
||||
|
||||
---
|
||||
|
||||
## Migration-Reihenfolge
|
||||
|
||||
**Phase 1 (Tauri-Shell + Proof-of-Concept):**
|
||||
- [ ] `computeJoins` (Wand-Ecken) → Rust
|
||||
|
||||
**Phase 2 (Rendering-Umbau):**
|
||||
- [ ] wgpu Viewport-Impl (ersetzt drei.js)
|
||||
- [ ] Instancing/LOD für Möbel-Geometrie
|
||||
|
||||
**Phase 3 (weitere Ops):**
|
||||
- [ ] `kernel2d` (Offset/Trim) → Rust
|
||||
- [ ] DXF/DWG-Parser → Rust
|
||||
- [ ] detectRooms → Rust
|
||||
- [ ] booleanOps → Rust
|
||||
|
||||
---
|
||||
|
||||
## Was sich NICHT ändert
|
||||
|
||||
- `src/App.tsx`, `src/state/`, `src/commands/`
|
||||
- UI-Panels, Zeichenwerkzeuge, Befehlssystem
|
||||
- Semantisches Modell (types.ts)
|
||||
- i18n, Styling
|
||||
|
||||
## Was ändert sich
|
||||
|
||||
- **Engine:** three.js/WebGL → wgpu
|
||||
- **Backend:** TS-Only → Tauri + Rust
|
||||
- **Distribution:** Browser → Desktop App
|
||||
- **Build-Prozess:** `npm run build` → `npm run tauri:build`
|
||||
|
||||
---
|
||||
|
||||
## Fehlerquellen (zur Klarheit)
|
||||
|
||||
❌ **FALSCH:** "Wir bleiben bei three.js"
|
||||
✅ **RICHTIG:** "three.js → wgpu, Rust-Compute + Desktop Tauri"
|
||||
|
||||
❌ **FALSCH:** "Tauri + three.js als Frontend-Engine"
|
||||
✅ **RICHTIG:** "Tauri-Shell + React UI + wgpu 3D-Rendering + Rust-Compute"
|
||||
|
||||
---
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
1. **Lesen:** `docs/design/tauri-migration-plan.md` (technisch, konkret)
|
||||
2. **Spawn:** vier Agents (parallel, unabhängig)
|
||||
3. **Verifizieren:** `npm run tauri:dev` → App läuft, erste Op in Rust funktioniert
|
||||
4. **Aktualisieren:** HANDOVER.md mit Milestone-1-Status
|
||||
|
||||
---
|
||||
|
||||
## Fragen?
|
||||
|
||||
- **"Warum Desktop statt Browser?"** → Skalierbarkeit (native GPU + Rust-Compute)
|
||||
- **"Warum wgpu statt drei.js?"** → wgpu skaliert auf 100k+ Polygone, three.js/WebGL hat Limits
|
||||
- **"Bleibt React?"** → Ja, React/Vite UI bleibt, nur 3D-Engine wechselt
|
||||
- **"Wann ist wgpu-Impl fertig?"** → Milestone 2 (nach Tauri-Shell)
|
||||
@@ -48,6 +48,14 @@ Undo/Redo** eliminiert die Cache-Stale-Bugs strukturell. Ziel-Repo-Struktur mit
|
||||
**kleinen Bauteil-Modulen** statt des 7244-LOC-`elemente.py`-Monolithen; Rendering
|
||||
über einen `THREE.Group`-Baum, der den Ebenen-Baum spiegelt.
|
||||
|
||||
### [design/parametric-walls.md](design/parametric-walls.md)
|
||||
Regelbasierte Wandgenerierung als Alternative zum Direktzeichnen. Vier Regel-Varianten
|
||||
(`GridRule`, `ModuleRule`, `ConditionalRule`, `PolylineRule`) erzeugen `Wall[]`-Arrays
|
||||
über einen reinen Auflöser (`resolveParametricWall`). Deckungsbereich: Schweizer 3-m-
|
||||
Wohnraster, bedingte Außen-/Innenwand-Dicken, 6-m-Jochbauweise. Phase A: Typsystem +
|
||||
Resolver isoliert, kein UI. Phase B: Command + Formular-Editor. Phase C: Grid-Ressource
|
||||
und IFC-Export.
|
||||
|
||||
### [design/elements.md](design/elements.md)
|
||||
Legt **Daten, Generierung (3D + Plan) und Grip-Editing pro Bauteil** fest. Wichtigste
|
||||
Empfehlung: die **Prioritäts-T-/X-Verschneidung mehrschichtiger Wände** (Backbone-
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# DOSSIER-Feature-Audit (A1–A6, B1–B4, C1–C3, D1–D3, E)
|
||||
|
||||
Ausführliche Fassung des Audits, auf das HANDOVER.md Backlog-Punkt 9 nur noch mit
|
||||
Einzeilern verweist. Abgleich DOSSIER-Rhino-Referenzrepo (`/tmp/dossier-ref`) gegen
|
||||
den aktuellen Browser-Port. Belege (Datei:Zeile) beziehen sich auf `/tmp/dossier-ref/rhino/*.py`.
|
||||
|
||||
Status-Symbole: ❌ nicht übernommen · 🟡 teilweise übernommen.
|
||||
|
||||
## A — Grösserer Funktionsumfang
|
||||
|
||||
**A1 · ❌ Grafische Overrides — Regel-Engine** (`overrides.py:7-40`)
|
||||
ArchiCAD/Vectorworks-Stil: Regeln der Form `condition{type: layer_name|user_string|
|
||||
object_name, operator: equals|contains|starts_with|not_equals, value, key} →
|
||||
actions{color, lineweight, linetype}`, additiv angewendet, oberste Regel gewinnt,
|
||||
reversibel. Cross-Doc-Presets + Rule-Templates (`list_presets:147`,
|
||||
`list_rule_templates:218`). Der Port hat nur manuelle Per-Instanz-Farbe (By-Layer/
|
||||
By-Object/eigener Wert, kein Regelwerk). Wert: enorm für Architektur-Pläne
|
||||
(Bestand grau, Brandabschnitte, Bauphasen farblich markieren). Aufwand: M-L.
|
||||
|
||||
**A2 · ❌ Ausschnitte / View-Snapshots** (`ausschnitte.py:466-531`)
|
||||
Benannte, gespeicherte Ansichten, die Kamera-Zustand + Layer-Sichtbarkeit +
|
||||
Massstab + Detailgrad einfangen und wiederherstellen; organisiert in Ordnern +
|
||||
Presets (`_capture_camera:78`, `_capture_layers:157`, `_capture:466`). Wert:
|
||||
zentral für Wiederverwendbarkeit und Voraussetzung für A3. Aufwand: L.
|
||||
|
||||
**A3 · ❌ Print-Layout-Blätter** (`layouts.py:7-8`)
|
||||
Layout-Seiten mit mehreren Details, jedes Detail an einen Ausschnitt-Snapshot
|
||||
gebunden (`_BIND_KEY:28`), Papierformate A4/A3, Ordnerstruktur, PDF-Export pro
|
||||
Layout-Blatt. Der Port exportiert aktuell nur eine einzelne Zeichnung nach PDF.
|
||||
Aufwand: L, hängt an A2.
|
||||
|
||||
**A4 · ❌ Tragwerk-Elemente** (`elemente.py`)
|
||||
Stütze, Träger, Unterzug, I-Profil als eigene Elementtypen. Der Port kennt nur
|
||||
Wand/Decke/Öffnung/Treppe/Raum. Wert: vervollständigt den BIM-Elementsatz.
|
||||
Aufwand: M je Typ.
|
||||
|
||||
**A5 · 🟡 Öffnungen viel reicher** (`elemente.py:59-68`)
|
||||
Mehrere Flügel (`OEFF_FLUEGEL`), Sims innen+aussen mit eigenen Stilen
|
||||
(`SIMS_AUS`/`SIMS_IN`), Glas-Toggle (`OEFF_GLAS`), Rahmen-Lage aussen/mittig/innen
|
||||
(`RAHMEN_POS`), Rahmen-Profilbreite/-tiefe, Detailgrad einfach/standard/detail =
|
||||
SIA-400-Darstellung (`OEFF_DARSTELLUNG:68`). Der Port hat nur swing/hinge/
|
||||
frameDepth/frameThickness. Aufwand: M.
|
||||
|
||||
**A6 · 🟡 Element-Schedule / Bauteilliste** (`elemente_uebersicht.py:45,214`)
|
||||
Voller Element-Überblick + SIA-Flächenbilanz + CSV-Export
|
||||
(`_export_bilanz:214`). Der Port hat nur einen Raum-CSV-Export. Aufwand: M.
|
||||
|
||||
## B — Werkzeuge & Objekt-Info
|
||||
|
||||
**B1 · ❌ Text-Platzierungs-Werkzeug** (`text_create.py:972,847,105`)
|
||||
On-Canvas-Text-Objekte als eigenständiges Zeichenwerkzeug (nicht nur
|
||||
Raumstempel): Textstile, Fonts, Rich-Text fett/kursiv/unterstrichen,
|
||||
Ausrichtung, Symbol-Einfügung, „auf Selektion anwenden". Der Port hat den
|
||||
Rich-Text-Editor (`src/text/RichTextEditor.tsx`) bereits, aber kein
|
||||
Platzierungs-Werkzeug, um damit ein freistehendes Textobjekt zu zeichnen
|
||||
(Shortcut „1" ist bewusst noch unbelegt). Aufwand: M.
|
||||
|
||||
**B2 · 🟡 Object-Info numerisch erweitern** (`dimensions.py:342-350,240,267`)
|
||||
Position, Rotation um Z-Achse (`_rotate_around_axis:240`), Kreis-Radius,
|
||||
Linien-Länge (`_set_line_length:267`), Rechteck Breite/Höhe,
|
||||
Koordinatensystem World/CPlane, 9-Punkt-Referenz. Der Port kann nur per
|
||||
Anker-Griff skalieren. Aufwand: S-M.
|
||||
|
||||
**B3 · ❌ Kamera-Presets + Nordwinkel** (`kamera.py:28,36-45,87`)
|
||||
Nicht in HANDOVER.md gelistet, aber im Audit gefunden: Kardinal-Presets
|
||||
(N/O/S/W), Iso-Oktanten, Rotation um einen einstellbaren Nordwinkel
|
||||
(georeferenziert, relevant für Swisstopo-Kontext).
|
||||
|
||||
**B4 · ❌ Massstab-Toolbar-Funktionen** (nicht separat referenziert, Teil der
|
||||
Toolbar-Logik) — Zoom 1:1/auf Selektion, Linienstärken-Set, Grid/Ortho/
|
||||
Referenzlinien-Toggles direkt aus der Symbolleiste.
|
||||
|
||||
## C — Zusätzlich gefunden, nicht in HANDOVER.md
|
||||
|
||||
**C1 · LoD pro Ansicht** — Darstellung einfach/standard/detail je Ansicht
|
||||
umschaltbar (SIA-400-Detailgrad-Konvention), nicht nur global.
|
||||
|
||||
**C2 · Override-Presets + Rule-Templates als Bibliothek** — vertieft A1: die
|
||||
Regeln selbst sind wiederverwendbare, benannte Presets/Templates, nicht nur
|
||||
Ad-hoc-Zustand pro Dokument.
|
||||
|
||||
**C3 · Mass-Style-Presets** — Dezimalstellen/Rundung als benannte,
|
||||
wiederverwendbare Bemassungs-Stile.
|
||||
|
||||
## D — Weitere Backlog-Ergänzungen aus dem Audit
|
||||
|
||||
**D1** Per-Layout-PDF-Export (vertieft A3).
|
||||
**D2** Bauteil-CSV mit vollem Element-Set (vertieft A6).
|
||||
**D3** Komponenten-Thumbnails (visuelle Vorschau im Component-Manager).
|
||||
|
||||
## E — Bewusst NICHT zu portieren
|
||||
|
||||
Rhino-/Windows-gebundene Implementierungsdetails ohne Browser-Äquivalent:
|
||||
Window-Layout-XML-Persistierung, Auto-DPI via CoreGraphics, Custom-Grips-Code
|
||||
(Rhino-SDK-spezifisch), nativer `.3dm`-Geometrie-Import.
|
||||
|
||||
## Priorisierung (aus dem Original-Audit)
|
||||
|
||||
1. A1 (Override-Regel-Engine)
|
||||
2. A2 (View-Snapshots)
|
||||
3. A3 (Print-Layout-Blätter)
|
||||
4. A5 (reichere Öffnungen)
|
||||
5. B1 (Text-Platzierungs-Werkzeug)
|
||||
6. A4 (Tragwerk)
|
||||
7. B2 (Object-Info numerisch)
|
||||
8. A6 (Bauteil-Schedule)
|
||||
|
||||
Siehe auch `ROADMAP.md` §11 für eine noch breitere, unabhängig entstandene
|
||||
Backlog-Liste (4-Wege-Survey über Bauteile/Darstellung/Pläne/Kontext) mit
|
||||
teilweiser Überschneidung.
|
||||
@@ -0,0 +1,137 @@
|
||||
# Engine-Nordstern — Headless-Rendering (PNG-Export & Golden-Image-Tests)
|
||||
|
||||
> Betrifft `src-tauri/render2d` (Feature `headless`). Bezug: HANDOVER.md,
|
||||
> Abschnitt ENGINE-NORDSTERN, Punkt 3 ("deterministisches Headless-Rendering,
|
||||
> PNG-Export und Golden-Image-Tests ohne Fenster").
|
||||
|
||||
## Warum
|
||||
|
||||
`render2d` trennt die serde-only-Tessellierung (Feature-los, headless testbar)
|
||||
von der GPU-Schicht (Feature `render`, wgpu). Der Fenster-Pfad (`gpu::Renderer`,
|
||||
Feature `window`) braucht dafür bislang eine `wgpu::Surface` — also ein echtes
|
||||
Fenster mit Wayland-/X11-Session. Für deterministische PNG-Exporte und
|
||||
Golden-Image-Tests (CI, Regressions-Screenshots) ist das unnötig: wgpu kann
|
||||
genauso gut in eine `wgpu::Texture` rendern, ganz ohne Surface/Fenster.
|
||||
|
||||
`gpu::Renderer::render` nahm bereits vorher nur `Device`/`Queue`/`TextureView`
|
||||
entgegen — Surface- oder Offscreen-Textur macht für den Draw-Code keinen
|
||||
Unterschied. Der Offscreen-Pfad (`src/headless.rs`) dupliziert daher NICHTS,
|
||||
sondern baut nur Device/Queue ohne Surface sowie eine eigene Ziel-Textur +
|
||||
Buffer-Readback drumherum.
|
||||
|
||||
## Feature-Gating
|
||||
|
||||
Neues Cargo-Feature `headless = ["render", "dep:image"]`:
|
||||
- zieht `render` (wgpu/glyphon/bytemuck/pollster) plus die `image`-Crate
|
||||
(nur der PNG-Codec, `default-features = false, features = ["png"]`).
|
||||
- **berührt den wasm/web-Build nicht**: `cargo check --target wasm32-unknown-unknown
|
||||
--no-default-features --features web` zieht `image` nicht mit.
|
||||
- Binary `render_png` und Test `golden` sind zusätzlich per
|
||||
`required-features = ["headless"]` in `Cargo.toml` abgesichert.
|
||||
|
||||
## API
|
||||
|
||||
```rust
|
||||
pub struct HeadlessRenderer { /* Device, Queue, gpu::Renderer */ }
|
||||
|
||||
impl HeadlessRenderer {
|
||||
/// Instance/Adapter/Device OHNE Surface (Backend fest auf Vulkan gepinnt).
|
||||
/// `Err`, wenn kein Adapter verfügbar ist (z.B. CI-Runner ohne GPU) — die
|
||||
/// Aufrufer (CLI, Golden-Test) behandeln das, statt zu paniken.
|
||||
pub fn new() -> Result<Self, String>;
|
||||
|
||||
/// Rendert `scene` in ein `width`x`height`-Bild (Papier-Maßstab
|
||||
/// `paper_scale_n`, z.B. `100.0` für 1:100). Lädt die Szene bei jedem Aufruf
|
||||
/// neu hoch (kein Zwischenzustand nötig für CLI-/Test-Anwendungsfall).
|
||||
pub fn render_to_image(
|
||||
&mut self,
|
||||
scene: &Scene,
|
||||
width: u32,
|
||||
height: u32,
|
||||
view_box: ViewBox,
|
||||
paper_scale_n: f32,
|
||||
) -> RgbaImage; // { width, height, pixels: Vec<u8> (straff gepackt, RGBA8) }
|
||||
}
|
||||
|
||||
impl RgbaImage {
|
||||
pub fn encode_png(&self) -> Vec<u8>;
|
||||
/// Gegenstück für den Golden-Test: Referenz-PNG -> straff gepacktes RGBA8.
|
||||
pub fn decode_png(bytes: &[u8]) -> Result<Self, String>;
|
||||
}
|
||||
```
|
||||
|
||||
Backend bewusst auf **Vulkan** gepinnt (nicht das Standard-Backend-Set): Vulkan
|
||||
rendert offscreen ohne jede Fenster-/Display-Server-Abhängigkeit. GL bräuchte auf
|
||||
Linux i.d.R. einen EGL/GLX-Kontext, der ohne aktive Display-Session (X11/Wayland)
|
||||
Sonderfälle hat — auf dieser Maschine ist ohnehin ein echter Vulkan-ICD (RADV)
|
||||
vorhanden, daher kein Rückgriff auf `lavapipe`/`llvmpipe` nötig gewesen.
|
||||
|
||||
## Row-Alignment-Falle (wichtig für zukünftige Agenten)
|
||||
|
||||
`wgpu::Queue::copy_texture_to_buffer` / `CommandEncoder::copy_texture_to_buffer`
|
||||
verlangt, dass `bytes_per_row` im `ImageDataLayout` ein Vielfaches von
|
||||
`wgpu::COPY_BYTES_PER_ROW_ALIGNMENT` (256) ist. Bei RGBA8 (4 Byte/Pixel) trifft
|
||||
das **nur zufällig** zu — z.B. Breite 300 px → 1200 Byte/Zeile, kein Vielfaches
|
||||
von 256, der Copy schlägt sonst mit einem Validierungsfehler fehl (oder liefert
|
||||
verzerrte Zeilen, je nach Backend).
|
||||
|
||||
Lösung in `headless::HeadlessRenderer::read_pixels`:
|
||||
1. `padded_bytes_per_row = align_up(width * 4, 256)` — der Zielpuffer wird auf
|
||||
diese (größere oder gleiche) Zeilenbreite alloziert.
|
||||
2. Nach dem Mapping wird **jede Zeile einzeln** von `padded_bytes_per_row` auf
|
||||
die echte Breite (`width * 4`) zurückgeschnitten und in einen straff
|
||||
gepackten `Vec<u8>` kopiert — sonst hätte das Ausgabebild pro Zeile
|
||||
Garbage-Padding am rechten Rand.
|
||||
|
||||
## Referenzbild neu erzeugen
|
||||
|
||||
Bei einer **gewollten** Rendering-Änderung (z.B. neue Shader, neue Demo-Szene,
|
||||
geänderte Antialiasing-Parameter) weicht das Golden-Bild ab. Referenzbild
|
||||
danach bewusst neu erzeugen:
|
||||
|
||||
```sh
|
||||
cd src-tauri/render2d
|
||||
cargo run --features headless --bin render_png -- --width 990 --height 630 --out target/headless-demo.png
|
||||
cp target/headless-demo.png tests/golden/demo.png
|
||||
```
|
||||
|
||||
Vorher unbedingt das erzeugte PNG (`target/headless-demo.png`) visuell prüfen
|
||||
(z.B. mit einem Bildbetrachter oder Read-Tool eines Agenten) — nicht blind
|
||||
kopieren. Bei einem fehlgeschlagenen Testlauf schreibt der Golden-Test
|
||||
automatisch ein Diff-Bild nach `target/golden-diff.png` (abweichende Pixel
|
||||
signalrot markiert, Rest unverändert) — das hilft beim Einordnen, ob die
|
||||
Abweichung erwartet (Layout-/Farbänderung) oder ein Bug ist (z.B. verschobene
|
||||
Geometrie, fehlender Text).
|
||||
|
||||
## Golden-Test
|
||||
|
||||
`tests/golden.rs`, Test `demo_szene_entspricht_referenzbild`:
|
||||
- rendert die Demo-Szene (`demo::demo_scene`, dieselbe wie der Fenster-Spike)
|
||||
in 990×630 und vergleicht sie gegen `tests/golden/demo.png`.
|
||||
- Toleranz: ein Pixel gilt als "abweichend", wenn irgendein Kanal-Delta > 2 ist
|
||||
(deckt AA-Rundungsrauschen ab); der Test schlägt fehl, wenn mehr als 0,5%
|
||||
aller Pixel abweichen.
|
||||
- `#[ignore]`, weil eine Vulkan-fähige GPU auf CI-Runnern nicht garantiert ist
|
||||
(und ein Software-Rasterizer wie llvmpipe anderes AA liefern würde als das
|
||||
Referenzbild). Lokal explizit ausführen:
|
||||
`cargo test --features headless -- --include-ignored`.
|
||||
- Bei Abweichung über der Toleranz schreibt der Test zusätzlich zum Diff-Bild
|
||||
auch das Ist-Bild nach `target/golden-actual.png` — nach visueller Prüfung
|
||||
ist das die Vorlage für eine gewollte Referenz-Aktualisierung.
|
||||
- `HeadlessRenderer::new()` liefert bei fehlendem Adapter `Err` statt Panik —
|
||||
der Test überspringt sich dann sauber (`eprintln!` + `return`) statt rot zu
|
||||
schlagen, falls doch mal ohne `--ignored`/ohne GPU ausgeführt.
|
||||
|
||||
## Offener Punkt: Text-Determinismus zwischen GPU-Treibern
|
||||
|
||||
Der Textpass läuft über `glyphon`/`cosmic-text` (Systemfonts, `Inter`). Die
|
||||
exakte Glyphen-Rasterisierung (Subpixel-Antialiasing, Hinting) kann je nach
|
||||
installierter Font-Version, Fontconfig-Konfiguration und GPU-Treiber leicht
|
||||
variieren — auch bei identischer Geometrie. Auf **derselben** Maschine (gleicher
|
||||
Treiber, gleiche Font-Version) ist der Test wie erwartet bit-nah deterministisch
|
||||
(hier: 0 abweichende Pixel bei zwei aufeinanderfolgenden Läufen). Bei einem
|
||||
Wechsel der Maschine/des Treibers/der Fontconfig-Version ist nicht
|
||||
auszuschließen, dass die 0,5%-Toleranz für Textkanten knapp wird — sollte das
|
||||
in der Praxis auftreten: entweder Toleranz für den Text-Bereich lockern, oder
|
||||
den Text testweise aus der Golden-Szene ausklammern und separat (z.B. nur
|
||||
Zeilenbreite/Position, nicht Pixel) prüfen.
|
||||
@@ -0,0 +1,511 @@
|
||||
# Engine-Nordstern 1 — Strichbreiten-Audit (Papier-mm end-to-end)
|
||||
|
||||
> Bezug: HANDOVER.md, Abschnitt ENGINE-NORDSTERN Punkt 1 ("Papier-mm-exakte
|
||||
> Strichbreiten überall — Bildschirm bei jedem Massstab/Zoom = Druck"). Dieses
|
||||
> Dokument ist ein Prüfbericht, kein Umbau — es fasst zusammen, wie die
|
||||
> Strichbreite heute vom Modell bis zum Papier läuft, wo sie divergieren kann,
|
||||
> und was konkret zu tun wäre. Nichts hieraus wurde umgesetzt.
|
||||
|
||||
Betroffene Pfade: `src/plan/PlanView.tsx` (SVG-Default + Print-Vorschau),
|
||||
`src/plan/glPlan/*` (WebGL2, aktueller Default-GPU-Pfad), `src-tauri/render2d`
|
||||
(WASM/WebGPU, `?engine=wasm`), `src/export/sceneToPrintSvg.ts` +
|
||||
`src/export/exportPdf.ts` (Vektor-PDF). Referenz-Baseline für die
|
||||
2D-Engine-Parität ist laut Commit `ce6bd26` **`?gl=0`** (reiner SVG-Pfad) —
|
||||
NICHT der App-Default (der ist WebGL2, siehe Finding 1).
|
||||
|
||||
## 1. Modell der Wahrheit
|
||||
|
||||
So sollte eine Strichbreite in diesem Code korrekt fliessen:
|
||||
|
||||
1. **Quelle**: `generatePlan.ts` legt jede Strichbreite als `weightMm` /
|
||||
`strokeWidthMm` in **echten Papier-Millimetern** ab (Kommentarkopf,
|
||||
`generatePlan.ts:76-79`: "Alle Stricharten sind in mm Papier definiert").
|
||||
Diese Zahl ist unabhängig von Zoom, Massstab und Render-Pfad — eine 0.18 mm
|
||||
Wand-Umrisslinie bleibt 0.18 mm, ganz gleich wo sie später landet.
|
||||
2. **Bildschirm bei Massstab 1:N**: 1 Modell-Meter entspricht auf Papier
|
||||
`1000/N` mm. Der Bildschirm zeigt `PX_PER_M = 90` viewBox-Einheiten je
|
||||
Modell-Meter (`PlanView.tsx:23`). Eine `mm`-Breite belegt daher
|
||||
`mm · N/1000 · PX_PER_M` viewBox-Einheiten — das ist exakt
|
||||
`printStrokeVb()` (`PlanView.tsx:2598-2600`). Multipliziert mit der
|
||||
Geräte-px-je-viewBox-Einheit-Skala (`meet`, `PlanView.tsx:2074-2078`) ergibt
|
||||
das die tatsächliche Bildschirmbreite in Geräte-Pixeln. Reinzoomen (kleinere
|
||||
viewBox-Breite, grösseres `meet`) macht die Linie dicker — genau wie beim
|
||||
Herausvergrössern eines gedruckten Plans mit der Lupe. Das gilt für JEDE
|
||||
Zoomstufe gleichermassen: 250 % Zoom bei 1:50 zeigt exakt die 5-fache
|
||||
Pixelbreite von 100 % Zoom bei 1:50, und bei gegebenem Zoom ist 1:50 exakt
|
||||
doppelt so dick wie 1:100 (halber Nenner → doppelt so viele Weltmeter je
|
||||
Papiermm).
|
||||
3. **Druck/PDF**: dieselbe Formel, nur ohne Geräte-px-Zwischenschritt — direkt
|
||||
`mm` bleibt `mm` — die Seite ist direkt in echten
|
||||
Papiermillimetern aufgespannt (`sceneToPrintSvg.ts:99-125`). Zusätzlich wird
|
||||
auf ISO-nahe Stiftstufen gerundet (`PEN_STEPS`,
|
||||
`sceneToPrintSvg.ts:44`), weil ein reales Zeichengerät/Plotter nur endlich
|
||||
viele Stiftbreiten kennt.
|
||||
4. **Eine Wahrheit**: Seit `382771b` bauen sowohl der Viewport
|
||||
(`useWasmPlanRenderer.ts`/`nativeSync.ts`) als auch der PDF-Export
|
||||
(`exportPdf.ts:73`) dieselbe `planToRenderScene(plan)`-Szene — der PDF-Pfad
|
||||
ist nur ein anderes *Ziel* derselben Szene, kein zweiter Interpret.
|
||||
|
||||
Korrekt hiesse also: **jede** der vier Anzeige-/Exportarten (SVG-Default,
|
||||
SVG-Print-Vorschau, GPU-Viewport, PDF) muss aus **derselben** `weightMm`, für
|
||||
**denselben** `N`, exakt dieselbe Papier-mm-Breite ergeben — bis auf die
|
||||
bewusste PEN_STEPS-Rundung im Druckpfad, die dokumentiert und überall
|
||||
gleichermassen sichtbar sein sollte (ist sie nicht, siehe Finding 2).
|
||||
|
||||
Ausdrücklich **kein** Teil dieses Modells: der Haarlinien-Modus
|
||||
(`lineMode: "display"`, App-Default, `viewSlice.ts:38,74`). Er ist als
|
||||
bewusster Papier-mm-*Ausstieg* gedacht ("Display: all lines as constant
|
||||
hairlines (calm editing)", `en.ts:173`) — 1 Geräte-px, konstant, unabhängig
|
||||
von Zoom/Massstab. Er muss also NICHT der Papier-mm-Formel folgen, aber er
|
||||
muss in JEDEM Render-Pfad *gleichermassen* als Ausstieg wirken. Tut er nicht
|
||||
(Finding 1).
|
||||
|
||||
## 2. Pfad-für-Pfad-Trace
|
||||
|
||||
### 2a. SVG-Default (App-Start ohne `?gl=0`, `lineMode:"display"`)
|
||||
|
||||
Nur aktiv, wenn WebGL2 fehlschlägt (`wantGl` ist sonst `true`, s. Finding 1) —
|
||||
de facto der reine Fallback-Pfad.
|
||||
|
||||
- `PlanView.tsx:2613`: `print = !hairline && paperScale != null && paperScale > 0`
|
||||
— mit `hairline=true` (Default) ist `print` immer `false`.
|
||||
- `PlanView.tsx:2617-2618`:
|
||||
```ts
|
||||
const weight = (mm: number): number =>
|
||||
hairline ? HAIRLINE_PX : print ? printStrokeVb(mm, paperScale!) : mmToPx(mm);
|
||||
```
|
||||
`HAIRLINE_PX = 1` (`PlanView.tsx:2583`), Einheit: Geräte-unabhängiger
|
||||
CSS-Pixel, gezeichnet mit `vector-effect="non-scaling-stroke"`
|
||||
(`PlanView.tsx:2621`, `vfx`) → bleibt beim Pan/Zoom optisch exakt 1 px, egal
|
||||
wie stark reingezoomt wird. Papier-mm spielt hier explizit KEINE Rolle.
|
||||
|
||||
### 2b. SVG-Print-Vorschau (`lineMode:"print"`, weiterhin `?gl=0` oder
|
||||
WebGL2-Fallback)
|
||||
|
||||
- `print = true`, `weight(mm) = printStrokeVb(mm, paperScale!)`
|
||||
(`PlanView.tsx:2598-2600`):
|
||||
```ts
|
||||
function printStrokeVb(mm: number, n: number): number {
|
||||
return Math.max(1e-4, (mm * n) / 1000) * PX_PER_M;
|
||||
}
|
||||
```
|
||||
`vectorEffect` entfällt (`vfx = undefined`, `PlanView.tsx:2621`) — die Linie
|
||||
skaliert MIT der Geometrie beim Zoomen, wie physisches Papier unter der
|
||||
Lupe. `paperScale` selbst ist der **stabile, gemessene** 1:N-Nenner
|
||||
(`PlanView.tsx:458-460`), nicht der live aus jedem Radzoom abgeleitete Wert
|
||||
— sonst würde Reinzoomen die Linien nicht dicker, sondern konstant halten
|
||||
(das wäre wieder Haarlinien-Verhalten). Keine PEN_STEPS-Rundung — die rohe
|
||||
`mm`-Zahl wird 1:1 in viewBox-Einheiten übersetzt.
|
||||
|
||||
### 2c. GPU-Viewport (WebGL2 `useGlPlanRenderer.ts` — App-DEFAULT; WASM
|
||||
`useWasmPlanRenderer.ts` bei `?engine=wasm`)
|
||||
|
||||
- Beide Hooks reichen `paperScaleN` unverändert an die Engine durch:
|
||||
`useWasmPlanRenderer.ts:131-149` (`render(viewBox, paperScaleN=100,
|
||||
textScaleN=100)` → `r.set_paper_scale(paperScaleN)`); WebGL2 analog
|
||||
(`useGlPlanRenderer.ts:102-116`, kein `textScaleN`-Parameter überhaupt, s.
|
||||
Finding 3).
|
||||
- `PlanView.tsx` ruft in JEDEM Render-Aufruf
|
||||
(`PlanView.tsx:494`,`502`,`1080`) `renderGl(view, paperScaleForGl(view),
|
||||
textScaleForGl())`. `paperScaleForGl` (`PlanView.tsx:475-476`):
|
||||
```ts
|
||||
const paperScaleForGl = (v: ViewBox): number =>
|
||||
paperScaleRef.current ?? scaleFromView(v, svgRef.current) ?? 100;
|
||||
```
|
||||
**Kein `hairline`-Zweig.** Egal ob `lineMode` "display" oder "print" ist,
|
||||
hier kommt immer ein echter 1:N-Papier-Nenner heraus.
|
||||
- Rust-Seite (`gpu.rs:606-616`):
|
||||
```rust
|
||||
let mm_px = mm_to_device_px(view_box, vw, vh, self.paper_scale_n);
|
||||
```
|
||||
`mm_to_device_px` (`ortho.rs:96-99`) reproduziert exakt `printStrokeVb`:
|
||||
`(paper_scale_n/1000.0) * PX_PER_M * meet` — `PX_PER_M = 90.0`
|
||||
(`tessellate.rs:22`, identisch zu TS). Die reine Mathematik ist also
|
||||
deckungsgleich zum SVG-Print-Pfad (2b) — aber sie läuft **immer**, auch wenn
|
||||
der Nutzer "Display: Haarlinien" gewählt hat. Siehe Finding 1.
|
||||
- Stiftbreite pro Batch im Shader (`shaders.rs:91`, `LINE_WGSL`):
|
||||
`width_px = max(0.6, stroke_px * stroke_scale) * miter` — harte Untergrenze
|
||||
0.6 Geräte-px, die weder die SVG- noch die PDF-Seite kennt (Finding 6).
|
||||
Identische Formel für Bögen (`shaders.rs:209`, `ARC_WGSL`) und im
|
||||
WebGL2-Pfad (`glPlanRender.ts:243-249`, Kommentar "klemmt bei ~0.6 px").
|
||||
- Schraffur-Musterlinien laufen NICHT über `mm_px`, sondern über
|
||||
`width_screen`/`px_per_screen` (`gpu.rs:661-665`): Geräte-px = Breite ×
|
||||
`meet`-Skala statt × mm→px — bewusst zoom-skalierend wie das SVG-`<pattern>`,
|
||||
nicht papierkonstant (s. `toRenderScene.ts:371-375`, Finding 4).
|
||||
|
||||
### 2d. Vektor-PDF (`exportPdf.ts` → `sceneToPrintSvg.ts`)
|
||||
|
||||
- `exportPdf.ts:73`: `const scene = planToRenderScene(plan);` — dieselbe
|
||||
Szene wie 2c, unabhängig vom aktuell gewählten `lineMode` der Ansicht.
|
||||
- `sceneToPrintSvg.ts:243-244`:
|
||||
```ts
|
||||
const effectiveMm = widthScreen ? (widthMm / PX_PER_M) * mmPerM : widthMm;
|
||||
const strokeMm = quantizePen(effectiveMm);
|
||||
```
|
||||
`PEN_STEPS = [0.13, 0.18, 0.25, 0.35, 0.5, 0.7, 1.0]`, `MIN_PEN_MM = 0.13`
|
||||
(`sceneToPrintSvg.ts:44,47`). JEDE Linie/Umriss/Bogen/Schraffur wird auf die
|
||||
nächsthöhere Stufe gerundet, bevor sie ins SVG/PDF geht
|
||||
(`sceneToPrintSvg.ts:251,274,301`). Das ist der EINZIGE der vier Pfade, der
|
||||
überhaupt quantisiert.
|
||||
- `widthScreen`-Konvertierung: `widthMm` (hier eigentlich viewBox-Einheiten,
|
||||
siehe 2c) wird über `/PX_PER_M * mmPerM` zurück in Weltmeter und dann in
|
||||
Papier-mm beim GEWÄHLTEN `opts.scaleDenominator` übersetzt — nicht beim
|
||||
Massstab, den die Live-Ansicht gerade zeigt (Finding 4).
|
||||
- Text: `sizeMm` kommt unverändert aus `RText.sizeMm`
|
||||
(`sceneToPrintSvg.ts:325`, `t.sizeMm`, keine Nachskalierung) — aber die
|
||||
VERTIKALE Zeilenposition dieser Texte wurde in `toRenderScene.ts` mit einem
|
||||
festen `STAMP_REF_N = 100` in Weltmeter umgerechnet (`toRenderScene.ts:163-165,
|
||||
481-483`), unabhängig vom tatsächlich gewählten Export-`N` (Finding 3).
|
||||
|
||||
## 3. Findings (nach Schwere geordnet)
|
||||
|
||||
### 1. Haarlinien-Modus (App-Default) wird vom GPU-Renderer komplett ignoriert — betrifft den Standard-Zustand der App
|
||||
|
||||
`lineMode` startet auf `"display"` (`viewSlice.ts:74`), und WebGL2 ist der
|
||||
Default-Renderer (`wantGl` ist `true`, ausser `?gl=0`,
|
||||
`PlanView.tsx:433-437`) — **d. h. im frisch geladenen, unkonfigurierten App-
|
||||
Zustand rendert bereits der GPU-Pfad, und die "Display: Haarlinien"-Option
|
||||
tut nichts.** `paperScaleForGl` (`PlanView.tsx:475-476`) liest nur
|
||||
`paperScaleRef.current ?? scaleFromView(...) ?? 100` — der `hairline`-Boolean
|
||||
aus den Props (`PlanView.tsx:2571`) wird an dieser Stelle nie geprüft, obwohl
|
||||
das benachbarte `textScaleForGl` (`PlanView.tsx:485-488`) exakt diesen Zweig
|
||||
korrekt hat:
|
||||
```ts
|
||||
const textScaleForGl = (): number => {
|
||||
const print = !hairline && paperScale != null && paperScale > 0;
|
||||
return print ? paperScale! : 100;
|
||||
};
|
||||
```
|
||||
Ergebnis: Text (Raumstempel) fällt im Haarlinien-Modus korrekt auf die
|
||||
Referenzskala 100 zurück, aber jede Wand-/Tür-/2D-Zeichenlinie wird trotzdem
|
||||
mit dem echten (gemessenen oder geschätzten) `paperScale` gezeichnet — sie
|
||||
wird beim Reinzoomen dicker statt konstant zu bleiben, exakt das Gegenteil
|
||||
dessen, was der Menüpunkt verspricht ("calm editing", `en.ts:173`). Der Bug
|
||||
betrifft sowohl `useGlPlanRenderer.ts` (Default) als auch
|
||||
`useWasmPlanRenderer.ts` (`?engine=wasm`) — keiner der beiden `render()`-
|
||||
Aufrufe kennt einen `hairline`-Parameter überhaupt.
|
||||
|
||||
### 2. PEN_STEPS-Quantisierung existiert NUR im PDF-Export — beide Live-Vorschauen (SVG-Print und GPU) zeigen unquantisierte Rohwerte
|
||||
|
||||
Konkret, mit den tatsächlichen Konstanten aus `generatePlan.ts`:
|
||||
|
||||
- `LAYER_LINE_MM = 0.13` (`generatePlan.ts:82`), `LAYER_DETAIL_FACTOR.fein =
|
||||
0.7` (`generatePlan.ts:90-93`) → Schichtfuge im Detailgrad "fein":
|
||||
`0.13 · 0.7 = 0.091 mm` (`generatePlan.ts:1022`,
|
||||
`strokeWidthMm: LAYER_LINE_MM * LAYER_DETAIL_FACTOR[detail]`). Die
|
||||
Bildschirm-Print-Vorschau zeigt genau diese 0.091 mm (`printStrokeVb(0.091,
|
||||
N)`); der PDF-Export klemmt via `quantizePen` auf `MIN_PEN_MM = 0.13` mm —
|
||||
**+43 % dicker im Druck als in der Vorschau.**
|
||||
- Türschwenkbogen: `doorLwMm * 0.6` (`generatePlan.ts:902`) mit dem üblichen
|
||||
Fallback `doorLwMm = LAYER_LINE_MM = 0.13` (`generatePlan.ts:354`) ergibt
|
||||
`0.078 mm`. Screen zeigt 0.078 mm (nahezu unsichtbar dünn bei kleinen
|
||||
Massstäben), PDF klemmt auf 0.13 mm — **+67 %.**
|
||||
- Wand-Umriss im Detailgrad "grob": `outlineMm = wallLwMm ·
|
||||
OUTLINE_DETAIL_FACTOR.grob (1.6)` (`generatePlan.ts:84-88, 648, 796`); beim
|
||||
häufigen Fallback `WALL_FALLBACK_MM = 0.18` (`generatePlan.ts:97`) ergibt das
|
||||
`0.288 mm`. `quantizePen(0.288)` rundet auf die nächste Stufe `0.35`
|
||||
(`PEN_STEPS`, da `0.25 < 0.288 ≤ 0.35`) — **+21.5 % im Druck.**
|
||||
|
||||
Das systematische Muster: der Detailgrad "fein" existiert explizit, um
|
||||
Nebenlinien DÜNNER zu machen (`generatePlan.ts:52-58`,
|
||||
`LAYER_DETAIL_FACTOR.fein = 0.7`) — aber sobald der berechnete Wert unter
|
||||
`MIN_PEN_MM` fällt, hebt die PDF-Quantisierung ihn wieder auf die Untergrenze
|
||||
an, ohne dass die Bildschirm-Vorschau (weder SVG-Print noch GPU) davon
|
||||
irgendetwas zeigt. Wer die Druckvorschau am Bildschirm beurteilt, sieht NICHT,
|
||||
was tatsächlich gedruckt wird.
|
||||
|
||||
### 3. Stempel-Zeilenabstand ist nur bei Massstab 1:100 exakt — SVG- und RenderScene-Pfad nutzen zwei verschiedene Formeln für dieselbe Grösse
|
||||
|
||||
Bereits als bekannte Alt-Lücke in HANDOVER.md (Commit `382771b`, Punkt 6)
|
||||
vermerkt; hier präzise verortet:
|
||||
|
||||
- SVG (`PlanView.tsx:2738-2739`):
|
||||
```ts
|
||||
const nRef = print ? paperScale! : 100;
|
||||
const unitPerPt = (1 / 72) * 0.0254 * nRef * PX_PER_M;
|
||||
```
|
||||
`nRef` folgt im Druckmodus dem TATSÄCHLICH aktiven `paperScale` — der
|
||||
Zeilenabstand (`lineGap = baseFs * 1.3`, `PlanView.tsx:2741`) ist für jedes
|
||||
`N` korrekt in viewBox-Einheiten, weil er direkt aus `unitPerPt` (das `N`
|
||||
enthält) abgeleitet wird.
|
||||
- RenderScene (`toRenderScene.ts:163-165`):
|
||||
```ts
|
||||
const STAMP_REF_N = 100;
|
||||
const MM_TO_M = STAMP_REF_N / 1000;
|
||||
```
|
||||
und (`toRenderScene.ts:481-483`):
|
||||
```ts
|
||||
const baseH = baseMm * MM_TO_M; // Basisgröße in Modell-Metern
|
||||
const lineGap = baseH * 1.3;
|
||||
```
|
||||
Hier ist der Umrechnungsfaktor `MM_TO_M` FEST auf `N = 100` verdrahtet,
|
||||
unabhängig davon, mit welchem `N` der WASM-Viewport oder der PDF-Export
|
||||
tatsächlich rendert (`paper_scale_n` in `gpu.rs`, `opts.scaleDenominator` in
|
||||
`sceneToPrintSvg.ts`). Bei `N = 100` sind beide Formeln identisch (das war
|
||||
offenbar der Verifikationsfall in `382771b`); bei jedem anderen `N` — z. B.
|
||||
1:50 — driftet der vertikale Zeilenabstand der Stempel-Mehrzeiler
|
||||
proportional zum Verhältnis `N_wahr/100` auseinander, während die einzelne
|
||||
Zeilenhöhe (`sizeMm`) selbst korrekt bleibt. Betroffen: WASM-Viewport (2c)
|
||||
UND PDF-Export (2d) gleichermassen (beide beziehen `RText` aus derselben
|
||||
`toRenderScene`-Funktion); die WebGL2-Ansicht ist NICHT betroffen, weil sie
|
||||
Text grundsätzlich nicht selbst zeichnet, sondern die SVG-Overlay-Ebene
|
||||
weiterverwendet (`PlanView.tsx:1592`,
|
||||
`useGpuRenderer ? wantWasm || p.kind !== "text" : ...` — bei WebGL2
|
||||
(`wantWasm=false`) werden Text-Primitive NIE aus dem SVG-Rendering
|
||||
herausgefiltert).
|
||||
|
||||
### 4. Schraffur-Strichbreite im PDF hängt vom GEWÄHLTEN Export-Massstab ab, nicht vom Massstab der Live-Vorschau
|
||||
|
||||
`hatchPx` wird in `toRenderScene.ts:374-375` genau einmal, massstabsunabhängig
|
||||
berechnet:
|
||||
```ts
|
||||
const hatchMm = p.hatch.lineWeight > 0 ? p.hatch.lineWeight : 0.13;
|
||||
const hatchPx = Math.max(0.6, hatchMm * (1 / 0.13));
|
||||
```
|
||||
— eine reine viewBox-Grösse (analog zum SVG-`<pattern>`-Strich, der
|
||||
absichtlich mit dem Zoom mitskaliert, damit die Schraffur bei jedem Zoom
|
||||
gleich dicht aussieht: Kommentar `toRenderScene.ts:371-373`). Erst beim
|
||||
Export wird daraus in `sceneToPrintSvg.ts:243`
|
||||
`effectiveMm = (widthMm / PX_PER_M) * mmPerM` — und `mmPerM = 1000 /
|
||||
opts.scaleDenominator` (`sceneToPrintSvg.ts:101`) verwendet den vom
|
||||
Export-Dialog gewählten Massstab, NICHT den `paperScale`, den der Nutzer
|
||||
gerade in der "Print"-Live-Vorschau sieht (`exportPdf.ts` ruft
|
||||
`sceneToPrintSvg` mit `opts.scaleDenominator` aus den Export-Optionen,
|
||||
`exportPdf.ts:61-79`, völlig unabhängig vom `PlanView`-State).
|
||||
|
||||
Beispiel: `lineWeight = 0.13` mm (Standard-Fuge, `getHatch`-Default) →
|
||||
`hatchPx = max(0.6, 0.13 · (1/0.13)) = 1.0` viewBox-Einheiten.
|
||||
- Export bei 1:50 (`mmPerM = 20`): `effectiveMm = (1.0/90)·20 = 0.222 mm` →
|
||||
`quantizePen` → **0.25 mm**.
|
||||
- Export bei 1:200 (`mmPerM = 5`): `effectiveMm = (1.0/90)·5 = 0.056 mm` →
|
||||
auf `MIN_PEN_MM` geklemmt → **0.13 mm**.
|
||||
|
||||
Fast die doppelte Strichstärke allein durch die Wahl des Export-Massstabs,
|
||||
bei UNVERÄNDERTER Quell-`lineWeight` — und ohne dass die Bildschirm-
|
||||
"Print"-Vorschau (die ja mit dem live gemessenen `paperScale` arbeitet,
|
||||
nicht mit `opts.scaleDenominator`) das anzeigen könnte, solange beide Werte
|
||||
nicht zufällig übereinstimmen.
|
||||
|
||||
### 5. Dieselbe Formel existiert vierfach, nur durch Kommentare (nicht durch Typen/Code) synchron gehalten
|
||||
|
||||
`PX_PER_M = 90` ist unabhängig deklariert in: `PlanView.tsx:23` (SVG),
|
||||
`glPlan/glPlanRender.ts:12` (WebGL2), `tessellate.rs:22` (Rust, geteilt
|
||||
zwischen WASM-Viewport und Headless/Golden-Test) und `sceneToPrintSvg.ts:64`
|
||||
(PDF-Serializer, mit explizitem Kommentar "MUSS mit dem dortigen
|
||||
uebereinstimmen, sonst driftet die Schraffur-Dichte des PDFs vom Viewport
|
||||
ab", `sceneToPrintSvg.ts:61-62`). Die Hatch-Dichte-Formel
|
||||
`Math.max(0.6, weightMm · (1/0.13))` ist separat dupliziert in
|
||||
`PlanView.tsx:2344-2345` (`hatchStrokePx`, SVG-`<pattern>`) und
|
||||
`toRenderScene.ts:375` (`hatchPx`, GPU + PDF) — beide mit demselben
|
||||
"magischen" Faktor `1/0.13`, ohne gemeinsame Konstante. Aktuell sind alle vier
|
||||
Kopien konsistent; das Risiko ist rein prospektiv (nächste Tuning-Änderung an
|
||||
einer Stelle vergisst die anderen drei) — aber genau das ist der
|
||||
Mechanismus, über den Findings wie 1-4 überhaupt erst entstehen können, ohne
|
||||
dass ein Typfehler oder Test anschlägt.
|
||||
|
||||
### 6. Drei unabhängige, nicht aufeinander abgestimmte Mindestbreiten-Politiken
|
||||
|
||||
- SVG-Print: `Math.max(1e-4, ...)` (`PlanView.tsx:2599`) — praktisch keine
|
||||
Untergrenze, verlässt sich auf das Antialiasing des Browsers.
|
||||
- GPU (WebGL2 UND WASM, Linien UND Bögen): harter Floor von **0.6
|
||||
Geräte-Pixel** (`shaders.rs:91,209`; `glPlanRender.ts:243-249`,
|
||||
Kommentar "klemmt bei ~0.6 px").
|
||||
- PDF: **`MIN_PEN_MM = 0.13` mm** (`sceneToPrintSvg.ts:38` in
|
||||
`planToPrintSvg.ts`, äquivalent `sceneToPrintSvg.ts:47`) — eine
|
||||
Papier-mm-Grösse, kein Pixelwert, konzeptionell etwas anderes als die
|
||||
GPU-Pixel-Untergrenze.
|
||||
|
||||
Auswirkung: bei sehr kleinem Massstab (weit rausgezoomt oder grosses `N`,
|
||||
z. B. Übersichtsplan 1:500) hält der GPU-Pfad sehr dünne Linien künstlich bei
|
||||
0.6 px sichtbar, während dieselbe Linie im SVG-Pfad fast verschwindet und im
|
||||
PDF auf eine ganz andere (mm-basierte, massstabsabhängige) Grösse geklemmt
|
||||
wird. Kein Pfad kennt die Politik der anderen beiden. Niedrigere Priorität
|
||||
als 1-4, weil hier keine der drei Politiken die *exportierte* Papier-mm-Wahrheit
|
||||
verändert (die bleibt PDF-exklusiv über `MIN_PEN_MM`) — es geht nur um
|
||||
Bildschirm-Konsistenz zwischen SVG/GPU bei Extremzoom.
|
||||
|
||||
### 7. Toter Zweitpfad `planToPrintSvg.ts` dupliziert PEN_STEPS/mm-Logik komplett, unbenutzt aber vorhanden
|
||||
|
||||
`src/export/planToPrintSvg.ts` trägt seit `382771b` einen Kopfkommentar
|
||||
"ERSETZT durch sceneToPrintSvg.ts … wird vom PDF-Export NICHT MEHR
|
||||
verwendet" (`planToPrintSvg.ts:1-6`) und ist tatsächlich nirgends mehr
|
||||
importiert (verifiziert per Grep über `src/`). Er enthält jedoch weiterhin
|
||||
eine eigene, unabhängige Kopie von `PEN_STEPS`/`MIN_PEN_MM`/`quantizePen`
|
||||
(`planToPrintSvg.ts:35,38,40-45`) und einer kompletten Plan→SVG-Serialisierung
|
||||
inkl. eigener `PX_PER_M`-Handhabung (`planToPrintSvg.ts:326`). Kein aktiver
|
||||
Bug, aber eine Falle: Copy-Paste-Wiederverwendung dieses Altpfads (z. B. für
|
||||
einen zukünftigen DXF/PNG-Export) würde eine dritte, potenziell abweichende
|
||||
Quantisierungs-Tabelle in den Baum ziehen.
|
||||
|
||||
## 4. Vorgeschlagene Fixes
|
||||
|
||||
### zu Finding 1 (Haarlinien-Modus im GPU-Pfad)
|
||||
|
||||
Kleinste, korrekte Lösung: den `hairline`-Zustand als eigenen Parameter bis
|
||||
in den Shader durchreichen, analog zu `paper_scale_n`/`text_scale_n`.
|
||||
|
||||
- **Rust (`src-tauri/render2d/src/gpu.rs`)**: neues Feld
|
||||
`pub hairline: bool` auf `Renderer` (Default `false`, neben `paper_scale_n`
|
||||
bei `gpu.rs:422`). In `Renderer::render` (`gpu.rs:606-616`): wenn
|
||||
`self.hairline`, `mm_px` NICHT aus `mm_to_device_px(...)` berechnen, sondern
|
||||
auf einen konstanten Geräte-px-Wert setzen (z. B. `1.0`), UND dafür sorgen,
|
||||
dass der Shader `width_mm` in diesem Fall ignoriert (sonst bleibt die
|
||||
RELATIVE Differenz zwischen z. B. 0.13 mm und 0.35 mm bestehen, nur global
|
||||
skaliert — nicht das gewünschte "alle Linien exakt 1 px"). Sauberster Weg:
|
||||
ein neues `hairline: u32`-Feld in die pro-Frame-Uniform (`Globals`,
|
||||
`shaders.rs:20-23` und `ArcGlobals`), im Fragment-/Vertex-Shader
|
||||
(`shaders.rs:91`, `LINE_WGSL`; `shaders.rs:209`, `ARC_WGSL`) per
|
||||
`select(...)` auf einen festen `1.0`-px-Wert umschalten statt
|
||||
`max(0.6, stroke_px * stroke_scale)`.
|
||||
- **`src-tauri/render2d/src/web.rs`**: neue Methode `set_hairline(&mut self,
|
||||
on: bool)` neben `set_paper_scale`/`set_text_scale`
|
||||
(`web.rs:146-155`-Nachbarschaft).
|
||||
- **`src/plan/useWasmPlanRenderer.ts`**: `render()`
|
||||
(`useWasmPlanRenderer.ts:131-155`) um einen `hairline: boolean`-Parameter
|
||||
erweitern, `r.set_hairline(hairline)` vor `r.render()` aufrufen.
|
||||
- **`src/plan/useGlPlanRenderer.ts`**: analog — `render()`
|
||||
(Signatur aktuell `(viewBox, paperScaleN=100)`) um `hairline` erweitern;
|
||||
im WebGL2-Shader-Uniform-Pfad (`glPlanRender.ts:159-165,213-215`)
|
||||
`mmToDevicePx` bei `hairline===true` durch einen konstanten Wert ersetzen
|
||||
und im Fragment-Shader denselben `select`-Trick wie oben anwenden
|
||||
(`glPlanShaders.ts`, dort wo "~0.6 px"-Klemmung passiert, siehe Finding 6).
|
||||
- **`src/plan/PlanView.tsx`**: `paperScaleForGl` (`PlanView.tsx:475-476`)
|
||||
bleibt wie sie ist (wird weiter für den Massstab-Nenner gebraucht, sobald
|
||||
der Nutzer zurück auf "Print" schaltet); stattdessen an allen drei
|
||||
`renderGl(...)`-Aufrufstellen (`PlanView.tsx:494,502,1080`) zusätzlich
|
||||
`hairline` durchreichen: `renderGl(view, paperScaleForGl(view),
|
||||
textScaleForGl(), hairline)`.
|
||||
|
||||
### zu Finding 2 (PEN_STEPS nur im PDF)
|
||||
|
||||
Zwei mögliche Richtungen, je nach gewünschtem Produktverhalten:
|
||||
|
||||
- **Option A (Vorschau matcht Druck)**: `quantizePen`
|
||||
(`sceneToPrintSvg.ts:50-54`) in ein gemeinsames Modul auslagern (z. B.
|
||||
`src/plan/penSteps.ts`, exportiert `PEN_STEPS`, `MIN_PEN_MM`,
|
||||
`quantizePen`), von `sceneToPrintSvg.ts` UND von `PlanView.tsx`
|
||||
(`printStrokeVb`, `PlanView.tsx:2598-2600`) importieren und dort ebenfalls
|
||||
anwenden: `printStrokeVb(mm, n) = Math.max(1e-4, (quantizePen(mm) * n) /
|
||||
1000) * PX_PER_M`. Für den GPU-Pfad müsste die Quantisierung dann VOR dem
|
||||
Scene-Bau passieren (in `toRenderScene.ts`, auf `widthMm` jeder
|
||||
`RLine`/`ROutline`/`RPolyline`/`RArc`, ausgenommen `widthScreen`-Einträge),
|
||||
damit WASM/WebGL2-Viewport dieselben Stufen zeigen wie SVG und PDF.
|
||||
- **Option B (Vorschau bleibt Rohgrösse, aber sichtbar gemacht)**: falls die
|
||||
feinkörnige Vorschau bewusst erhalten bleiben soll, zumindest den
|
||||
Stiftstufen-Sprung an der Stelle sichtbar machen, an der die Detailgrad-
|
||||
Faktoren definiert werden (`generatePlan.ts:84-93`) — z. B. ein
|
||||
Entwickler-/Lint-Test, der bei jedem `OUTLINE_DETAIL_FACTOR`/
|
||||
`LAYER_DETAIL_FACTOR`-Wert prüft, ob das Produkt mit den üblichen
|
||||
Fallback-`weightMm`-Werten (`WALL_FALLBACK_MM`, `LAYER_LINE_MM`) exakt auf
|
||||
einer `PEN_STEPS`-Stufe landet, und sonst warnt.
|
||||
|
||||
Empfehlung: Option A — sie erfüllt den Nordstern wörtlich ("Bildschirm bei
|
||||
jedem Massstab = Druck").
|
||||
|
||||
### zu Finding 3 (STAMP_REF_N)
|
||||
|
||||
In `toRenderScene.ts` `STAMP_REF_N = 100` (`toRenderScene.ts:163`) durch den
|
||||
tatsächlichen Ziel-Massstab ersetzen. `planToRenderScene(plan)` kennt aktuell
|
||||
keinen `N`-Parameter (`exportPdf.ts:73` ruft es ohne Massstabsangabe auf) —
|
||||
die Funktion müsste ein optionales `paperScaleN`-Argument bekommen (Default
|
||||
100, um den Viewport-Aufruf ohne Massstabskontext — `nativeSync.ts` — nicht
|
||||
zu brechen, dort ist 100 ohnehin die richtige Referenz für den WASM-Viewport
|
||||
im Anzeigemodus, s. `gpu.rs:422`), und `exportPdf.ts:73` müsste
|
||||
`planToRenderScene(plan, opts.scaleDenominator)` aufrufen. `MM_TO_M`
|
||||
(`toRenderScene.ts:165`) dann aus diesem Parameter statt aus der Konstante
|
||||
ableiten.
|
||||
|
||||
### zu Finding 4 (Hatch-mm im PDF folgt dem Export-N, nicht dem Vorschau-N)
|
||||
|
||||
Kein Bug im engeren Sinn (die Formel ist in sich konsistent — die Schraffur
|
||||
soll ja pro *gewähltem Blatt-Massstab* eine sinnvolle Dichte haben), aber die
|
||||
Bildschirm-„Print"-Vorschau sollte denselben `N` verwenden, den der
|
||||
Export-Dialog tatsächlich benutzen wird, sonst lügt die Vorschau. Fix:
|
||||
`paperScale` in `PlanView.tsx` beim Öffnen des Export-Dialogs (bzw. der
|
||||
Export-Dialog selbst) mit `opts.scaleDenominator` vorbelegen/synchronisieren,
|
||||
statt zwei unabhängige State-Quellen zu pflegen — Ort: dort, wo der
|
||||
Export-Dialog `scaleDenominator` initialisiert (App.tsx, PDF-Export-UI) einen
|
||||
Default aus `paperScale`/`liveScale` (`App.tsx`, `onScale`-Callback,
|
||||
`PlanView.tsx:794`) übernehmen.
|
||||
|
||||
### zu Finding 5 (vierfache Formel-Duplikation)
|
||||
|
||||
Eine gemeinsame TS-Konstantendatei `src/plan/renderConstants.ts` mit
|
||||
`PX_PER_M = 90`, `HATCH_DENSITY_FACTOR = 1/0.13`, `HATCH_MIN_PX = 0.6`
|
||||
anlegen; `PlanView.tsx`, `glPlan/glPlanRender.ts`, `toRenderScene.ts`,
|
||||
`sceneToPrintSvg.ts` importieren daraus statt eigener Literale. Für die
|
||||
Rust-Seite (`tessellate.rs:22`, `shaders.rs:142`) ist echtes Teilen über die
|
||||
Sprachgrenze hinweg nicht trivial — dort bleibt nur ein Kommentar-Link auf die
|
||||
TS-Konstante plus ein Parity-Test (siehe Abschnitt 5), der bei Abweichung
|
||||
fehlschlägt statt bei stillem Drift.
|
||||
|
||||
### zu Finding 6 (drei Mindestbreiten-Politiken)
|
||||
|
||||
Niedrige Priorität, aber falls angegangen: den 0.6-px-Floor aus den Shadern
|
||||
(`shaders.rs:91,209`, `glPlanShaders.ts`) als benannte Konstante
|
||||
exportieren/dokumentieren und explizit von `1e-4` (SVG) und `MIN_PEN_MM=0.13`
|
||||
(PDF) abgrenzen — mindestens per Kommentar klarstellen, dass die drei
|
||||
UNTERSCHIEDLICHE Zwecke haben (GPU: Pixel-Sichtbarkeits-Floor;
|
||||
PDF: Papier-mm-Stiftgrenze) und NICHT synchronisiert werden müssen, damit
|
||||
niemand versehentlich versucht, sie anzugleichen und dabei die jeweils
|
||||
andere Semantik bricht.
|
||||
|
||||
### zu Finding 7 (toter Pfad)
|
||||
|
||||
`src/export/planToPrintSvg.ts` entfernen (nach kurzer Rücksprache, ob der
|
||||
Kopfkommentar-Vermerk "bleibt nur als Referenz stehen" noch gewollt ist) oder,
|
||||
falls er als Referenz bleiben soll, seine `PEN_STEPS`/`quantizePen`-Kopie
|
||||
durch einen Import aus der in Finding 2 vorgeschlagenen gemeinsamen
|
||||
`penSteps.ts` ersetzen.
|
||||
|
||||
## 5. Verifikations-Rezept
|
||||
|
||||
Was schon existiert:
|
||||
|
||||
- `scripts/probe-engine-parity.mjs` vergleicht `?gl=0` gegen `?engine=wasm`
|
||||
rein visuell (Screenshot-Diff von Auge, `probe-parity-svg.png` vs.
|
||||
`probe-parity-wasm.png`) — prüft NICHT quantitativ, ob Strichbreiten in
|
||||
Pixeln übereinstimmen, und deckt weder den Haarlinien-Modus noch den
|
||||
WebGL2-Default-Pfad noch den PDF-Export ab.
|
||||
- `src-tauri/render2d/tests/golden.rs` vergleicht die Demo-Szene
|
||||
Pixel-für-Pixel gegen `tests/golden/demo.png` (Toleranz: Kanal-Delta > 2 auf
|
||||
< 0.5 % der Pixel, `docs/design/engine-headless.md`) — ein reiner
|
||||
GPU-Regressionstest, ohne SVG- oder PDF-Vergleich.
|
||||
- Die einzige bisher dokumentierte mm-genaue Messung ist manuell:
|
||||
HANDOVER.md, Commit `382771b` — `pdftoppm`-Messung eines exportierten PDFs
|
||||
(53.51×43.69 mm gegen erwartete 53.45×43.45 mm bei 1:100), einmalig, nicht
|
||||
als wiederholbares Skript hinterlegt.
|
||||
|
||||
Um den Nordstern ("Bildschirm bei jedem Zoom/Massstab = Druck") tatsächlich
|
||||
beweisbar zu machen, fehlt ein quantitativer End-to-End-Test, der:
|
||||
|
||||
1. Für eine feste Test-Szene (idealerweise die bestehende `demo::demo_scene`
|
||||
aus `src-tauri/render2d/src/demo.rs`, die bereits sowohl einen `width_mm`-
|
||||
als auch einen `width_screen`-Strich enthält, `demo.rs:34,49-50`) bei
|
||||
mehreren `N` (1:10, 1:50, 1:100) UND mehreren Zoomstufen:
|
||||
- den PDF-Export erzeugt und via `pdftoppm -r <dpi>` in ein PNG rendert,
|
||||
dort die Strichbreite in Pixeln misst und in mm zurückrechnet (bekannte
|
||||
DPI ⇒ bekannte mm/px);
|
||||
- denselben Frame headless über `HeadlessRenderer::render_to_image`
|
||||
(`src-tauri/render2d/src/headless.rs`) mit identischem `paper_scale_n`
|
||||
rendert und dieselbe Pixel-Breite misst;
|
||||
- beide mm-Werte gegen den erwarteten `mm · N/1000`-Sollwert UND
|
||||
gegeneinander vergleicht, mit einer Toleranz, die die PEN_STEPS-Rundung
|
||||
(Finding 2, sobald behoben: siehe Option A) einbezieht.
|
||||
2. Den Haarlinien-Modus separat abdeckt: ein Screenshot-Vergleich bei zwei
|
||||
verschiedenen Zoomstufen im `lineMode:"display"` — die gemessene
|
||||
Pixelbreite MUSS bei beiden Zoomstufen identisch sein (Beweis, dass
|
||||
Finding 1 behoben ist), sowohl für WebGL2 (`?gl` ohne `=0`, App-Default)
|
||||
als auch für WASM (`?engine=wasm`).
|
||||
3. `probe-engine-parity.mjs` um eine tatsächliche Pixel-Differenz-Metrik
|
||||
erweitert (statt nur Kindanzahl/Warnungen zu loggen wie aktuell
|
||||
`probe-engine-parity.mjs:36-44`) — z. B. eine bekannte Referenzlinie im
|
||||
Testmodell an fester Bildschirmposition, deren Strichbreite in beiden
|
||||
Screenshots per Pixel-Sampling gemessen und verglichen wird.
|
||||
|
||||
Bis dieser Test existiert, bleibt jede Aussage über "Bildschirm = Druck" eine
|
||||
Behauptung, die nur durch manuelles `pdftoppm`-Nachmessen einzelner
|
||||
Stichproben gedeckt ist — die in diesem Audit gefundenen Divergenzen (v. a.
|
||||
Finding 1 und 2) hätten mit den heutigen Probes nicht auffallen können, weil
|
||||
keiner von ihnen den Default-Zustand der App (`lineMode:"display"`, WebGL2)
|
||||
gegen den Druckpfad misst.
|
||||
@@ -0,0 +1,186 @@
|
||||
# Schnitt/Ansicht-Pipeline: analytische Prismen-Projektion (`render3d::section`)
|
||||
|
||||
> Gegenstueck zum OCCT-WASM-HLR-Spike (`docs/welle-c-hlr-spike/FEASIBILITY.md`):
|
||||
> statt eines generischen CAD-Kernels nutzt dieser Ansatz eine Invariante des
|
||||
> Modells, um Schnitt/Ansicht rein analytisch (kein Hidden-Line-Removal-Solver)
|
||||
> zu berechnen. Implementiert in `src-tauri/render3d/src/section.rs`.
|
||||
|
||||
## Ansatz
|
||||
|
||||
Jedes Bauteil in diesem Modell ist ein **Prisma**: ein 2D-Grundriss-Fussabdruck-
|
||||
Polygon (Wand-Band bzw. Decken-Umriss, siehe `mesh.rs`), konstant extrudiert
|
||||
ueber ein Hoehenintervall `[z0, z1]`. Diese Einschraenkung — konstanter
|
||||
Querschnitt ueber die gesamte Hoehe — macht die Schnittgeometrie trivial im
|
||||
Vergleich zu generischem HLR:
|
||||
|
||||
- **Schnitt einer vertikalen Ebene mit einem Prisma** = 2D-Geraden/Polygon-
|
||||
Clipping IM GRUNDRISS (die Schnittebene projiziert im Grundriss auf eine
|
||||
Linie) → ein oder mehrere u-Intervalle, in denen die Linie das Fussabdruck-
|
||||
Polygon durchquert. Jedes Intervall × `[z0, z1]` ist das Cut-Rechteck. Da die
|
||||
Hoehe unabhaengig von der Grundriss-Position ist, ist das Ergebnis **immer
|
||||
ein Rechteck**, nie ein Trapez — auch bei diagonalen Wandachsen.
|
||||
- **Projektion/Ansicht** (Kanten hinter der Ebene, in Blickrichtung) reduziert
|
||||
sich auf ein **2D-Sichtbarkeitsproblem im Grundriss** kombiniert mit einem
|
||||
Hoehen-Ueberlappungstest: da alle Seitenflaechen der Prismen vertikal sind,
|
||||
genuegt ein Sichtstrahl-Test im Grundriss (verdeckt ein naeheres Prisma-
|
||||
Fussabdruckpolygon die Sichtlinie?) plus Ueberlappung der Hoehenintervalle.
|
||||
|
||||
Das ersetzt einen 66-MB-WASM-CAD-Kernel durch closed-form Arithmetik in reinem
|
||||
Rust, ohne zusaetzliches Laufzeitgewicht ueber `render3d` hinaus.
|
||||
|
||||
## `SectionOutput`-Format
|
||||
|
||||
Modul: `render3d::section`. Alle Groessen in **Metern**.
|
||||
|
||||
```rust
|
||||
pub struct SectionPlane { pub point: [f32; 3], pub normal: [f32; 3] }
|
||||
|
||||
pub struct SectionOutput {
|
||||
pub cut_polygons: Vec<CutPolygon>, // JSON: "cutPolygons"
|
||||
pub visible_edges: Vec<SectionEdge>, // JSON: "visibleEdges"
|
||||
pub hidden_edges: Vec<SectionEdge>, // JSON: "hiddenEdges"
|
||||
}
|
||||
|
||||
pub struct CutPolygon { pub component: ComponentRef, pub color: Rgb, pub pts: Vec<[f32; 2]> }
|
||||
pub struct SectionEdge { pub component: ComponentRef, pub a: [f32; 2], pub b: [f32; 2] }
|
||||
pub struct ComponentRef { pub kind: ComponentKind /* Wall | Slab */, pub index: usize }
|
||||
```
|
||||
|
||||
**Koordinatensystem der Ausgabe (u, v):**
|
||||
|
||||
- Ursprung: `SectionPlane::point`, projiziert.
|
||||
- `u` (horizontal): Strecke entlang der Schnittebene, senkrecht zur
|
||||
Blickrichtung, berechnet als `normalize(cross(normal, world_up))` — dieselbe
|
||||
rechtshaendige Konvention wie `math::look_at`s `right`-Vektor. Bei den vier
|
||||
Standard-Konstruktoren (`looking_plus_x/minus_x/plus_y/minus_y`) ist `u`
|
||||
direkt die jeweils andere Grundriss-Achse.
|
||||
- `v` (vertikal, "Hoehe"): `v = world.y`, ABSOLUT. Diese Codebasis ist
|
||||
durchgaengig Y-up (`types.rs`/`mesh.rs`/`math.rs`: „world.y = Hoehe"); `v`
|
||||
folgt bewusst dieser etablierten Konvention statt einer wortwoertlichen
|
||||
„world.z"-Lesart, um modulübergreifend konsistent zu bleiben.
|
||||
- Nur **vertikale** Schnittebenen (Normale ohne Hoehen-Komponente) sind
|
||||
unterstuetzt — Grundriss-/Horizontalschnitte bleiben Sache der bestehenden
|
||||
2D-Plan-Pipeline.
|
||||
|
||||
`cut_polygons` sind bei diesem Modell immer Rechtecke (siehe oben), aber als
|
||||
generischer Punktering abgelegt — kompatibel zu `render2d::types::FillPolygon`
|
||||
(`pts: Vec<Point>`), dem vorgesehenen Zielformat fuer die spaetere 1:1-
|
||||
Uebersetzung in die Plan-/Schnitt-Ansicht.
|
||||
|
||||
## Oeffnungen (Tueren/Fenster)
|
||||
|
||||
`WallInput` hat seit diesem Nachtrag ein Feld `openings: Vec<Opening>`
|
||||
(`types.rs`), unabhaengig von der reinen Achse/Dicke/Hoehe: je Oeffnung ein
|
||||
Intervall entlang der Wandachse (`from`/`to`, Meter ab `start`) plus Bruestungs-
|
||||
und Kopfhoehe (`sill`/`height`, relativ zu `base_elevation`). Eine Tuer ist der
|
||||
Sonderfall `sill == 0.0`.
|
||||
|
||||
**Cut-Polygone.** Trifft die Schnittlinie eine Wand exakt im Bereich einer
|
||||
Oeffnung, splittet `wall_cut_rectangles` das sonst einzelne Vollrechteck
|
||||
`[z0,z1]` in bis zu ZWEI Teil-Rechtecke: Bruestung `[z0, z0+sill]` und Sturz
|
||||
`[z0+sill+height, z1]`. Gewaehlt wurde diese Mehrfach-Rechteck-Darstellung
|
||||
BEWUSST gegenueber einem Loch-Polygon (Ring mit Aussparung): mehrere einfache,
|
||||
konvexe Ringe sind direkt kompatibel zum Zielformat `render2d::types::
|
||||
FillPolygon` (nur einfache Ringe, kein Loch-Format), waehrend ein
|
||||
Loch-Polygon eine zusaetzliche Datenstruktur (Ring-mit-Loch) noetig gemacht
|
||||
haette, die die Zielstruktur (noch) nicht kennt. Ausserhalb einer Oeffnung
|
||||
bleibt es beim einzelnen Vollrechteck (Regressionsfall).
|
||||
|
||||
Die Zuordnung "welche Wandachsen-Position entspricht dieser Schnittposition"
|
||||
ist fuer den STANDARDFALL (Schnittebene senkrecht zur Wandachse — der uebliche
|
||||
architektonische Wandquerschnitt) EXAKT konstant ueber das Cut-Intervall. Fuer
|
||||
schraege Wand/Ebene-Kombinationen wird sie als AFFINE Funktion (`axis_map`)
|
||||
exakt mitgefuehrt (keine Naeherung noetig, da die Abbildung linear ist).
|
||||
|
||||
**Projektion/Ansicht.** Eine Wand mit Oeffnungen bekommt zusaetzlich zur
|
||||
Boxen-Drahtsilhouette die vier Rahmenkanten jeder Oeffnung (zwei Leibungen,
|
||||
Sturz, Bruestung), berechnet auf der Wandachsen-Mittellinie (dieselbe
|
||||
Vereinfachung wie die Bounding-Box-Verdeckung generell — keine eigene
|
||||
Dicken-Aufloesung der Leibungsflaeche). Fuer die Verdeckung wird jede
|
||||
Wand-Bounding-Box um ihre Oeffnungen als "Loch" reduziert
|
||||
(`PrismBounds::opening_voids`): ein anderes (oder dasselbe) Bauteil hinter der
|
||||
Wand wird in genau dem (u, Hoehe)-Rechteck der Oeffnung NICHT von dieser Wand
|
||||
verdeckt — "Durchblick". Die eigenen Rahmenkanten einer Oeffnung werden dabei
|
||||
NICHT gegen das eigene Bauteil auf Verdeckung geprueft (ein Loch kann sich
|
||||
nicht selbst verdecken); gegen alle anderen Bauteile gilt die normale
|
||||
Verdeckungslogik unveraendert (inkl. der bereits dokumentierten
|
||||
Selbstverdeckung der Rueckseite eines ANDEREN Bauteils durch dessen eigene
|
||||
Vorderseite).
|
||||
|
||||
GENAUIGKEIT (bewusste Vereinfachung): die u-Zuordnung fuer den Durchblick ist
|
||||
nur DANN aussagekraeftig, wenn die Wandachse hinreichend parallel zur u-Achse
|
||||
der Schnittebene steht — das ist GENAU der Fall, in dem man die Wand als
|
||||
Elevation/Ansicht von vorne sieht (und ein Fenster ueberhaupt als Durchblick
|
||||
sichtbar waere). Steht die Wand naeher an "senkrecht zur u-Achse" (Wand auf
|
||||
Kante gesehen bzw. der reine Cut-Fall), wird die Durchblick-Berechnung
|
||||
uebersprungen und die Wand bleibt fuer die Verdeckung VOLL UNDURCHSICHTIG
|
||||
(konservativ hidden) — Schwelle `AXIS_ALIGN_EPS = 1e-3` in `section.rs`. Ein
|
||||
Kante-auf-Kante gesehenes Fenster liefert ohnehin keine sinnvolle
|
||||
Durchblick-Flaeche in der Projektion.
|
||||
|
||||
Getestet in `section::tests` (u. a. `schnitt_durch_fenster_liefert_bruestung_
|
||||
und_sturz`, `schnitt_neben_dem_fenster_liefert_volles_rechteck`,
|
||||
`ansicht_zeigt_fensterrahmen_sichtbar_und_durchblick_bei_dahinterliegender_
|
||||
kante`) und im Beweis-SVG (`examples/section_svg.rs`): Schenkel A der L-Wand
|
||||
hat dort ein Fenster, eine kurze zusaetzliche Wand steht dahinter genau im
|
||||
Fensterband und erscheint im SVG als durchgezogene (sichtbare) Linie zwischen
|
||||
Bruestungs- und Sturzhoehe, gestrichelt (verdeckt) darueber/darunter.
|
||||
|
||||
## Bekannte Luecken
|
||||
|
||||
- **Keine Wandknoten-Verschneidung (T-/X-Stoesse).** Wie in `mesh.rs`
|
||||
(M1-Stand) werden Waende als eigenstaendige, stumpf abgeschlossene Quader
|
||||
behandelt, die sich an Ecken UEBERLAPPEN statt sich zu vereinen (kein
|
||||
Miter-Join). Der Schnitt-Extraktor erbt das: an einem L-/T-Knoten kann eine
|
||||
Wand als „in die andere eingebettet" verdeckt erscheinen (im Testmodul
|
||||
`section::tests` bewusst als reales, erwartetes Verhalten dokumentiert und
|
||||
geprueft — kein Bug dieses Moduls, sondern ein Artefakt der fehlenden
|
||||
Verschneidungslogik weiter oben in der Pipeline).
|
||||
- **Oeffnungen sind NICHT in der Vollkoerper-Extrusion (`mesh.rs`)
|
||||
nachgezogen.** `wall_prism`/`wall_cut_rectangles`/die Verdeckung in
|
||||
`section.rs` werten `WallInput::openings` vollstaendig aus (siehe Abschnitt
|
||||
"Oeffnungen" oben), aber `mesh::extrude_wall` extrudiert weiterhin die volle
|
||||
Wandflaeche ohne Aussparung (`mesh.rs`-Moduldoc: „Oeffnungen kommen in
|
||||
spaeteren Milestones"). Cut-/Ansichts-Pipeline und 3D-Solid-Mesh sind bis zum
|
||||
Nachziehen von `mesh.rs` also bewusst inkonsistent — eine bekannte, separate
|
||||
Luecke (nicht Gegenstand dieses Nachtrags).
|
||||
- **Oeffnungs-Durchblick nutzt dieselbe achsparallele Bounding-Box-Naeherung**
|
||||
wie die allgemeine Verdeckung (kein exaktes Polygon-Clipping der Lochflaeche
|
||||
gegen dahinterliegende Kanten) und wird bei Wand-Orientierungen nahe
|
||||
"senkrecht zur u-Achse" konservativ auf "kein Durchblick" zurueckgestuft
|
||||
(siehe Abschnitt "Oeffnungen" oben, `AXIS_ALIGN_EPS`).
|
||||
- **Keine echte Component-/Material-Id.** `WallInput`/`SlabInput` haben aktuell
|
||||
keine eigene Id; `ComponentRef` referenziert daher nur `(Art, Index im
|
||||
Eingabe-Array)` + die rohe Albedo-Farbe als Material-Platzhalter. Sobald ein
|
||||
echtes Ressourcen-/Material-System existiert, sollte `ComponentRef` auf eine
|
||||
stabile Id umgestellt werden (Indizes sind nicht stabil ueber Modell-Edits).
|
||||
- **Verdeckungstest nutzt Bounding-Boxen, nicht die exakte Fussabdruckform.**
|
||||
Fuer Wand-Baender und (i. d. R. konvexe) Deckenumrisse ist das exakt; bei
|
||||
stark konkaven oder diagonalen Grundrissen kann es zu Ueberverdeckung
|
||||
fuehren (ein Prisma blockiert dann auch Bereiche seiner eigenen „Nischen").
|
||||
- **Kein generisches HLR.** Der Ansatz funktioniert NUR, weil alle Bauteile
|
||||
Prismen mit konstantem Querschnitt sind. Fuer echte gekrümmte oder nicht-
|
||||
prismatische Geometrie (Bogenwaende, Freiformdaecher, …) waere er nicht
|
||||
anwendbar — dafuer bliebe der OCCT-Weg (oder eine eigene, generischere
|
||||
HLR-Implementierung) die Referenz.
|
||||
- **Keine robuste Sonderfallbehandlung fuer Vertices exakt auf der
|
||||
Schnittlinie** (Toleranz-basiert, kein Tie-Breaking/Pertubation) — die
|
||||
Testszenarien vermeiden diesen Fall bewusst.
|
||||
|
||||
## Vergleich zum OCCT-WASM-Spike
|
||||
|
||||
| | OCCT-WASM (`docs/welle-c-hlr-spike`) | Dieser Ansatz (`render3d::section`) |
|
||||
|---|---|---|
|
||||
| Verfahren | Generisches HLR (`HLRAppli_ReflectLines`) | Analytische Prismen-Projektion |
|
||||
| Anwendbarkeit | Beliebige BREP-Geometrie | Nur Prismen (konstanter Querschnitt über Höhe) |
|
||||
| Zusaetzliches Gewicht | ~62,8 MB WASM (~19,6 MB gzip), separater Lazy-Chunk | Keins — reines Rust in `render3d`, kein weiteres WASM-Asset |
|
||||
| Modul-Init | ~450 ms (Browser) / ~680 ms (Node) | Kein Initialisierungsschritt (kein Fremd-Modul zu laden) |
|
||||
| Rechenzeit (L-Wand + Platte) | ~21 ms (reiner HLR-Lauf, gemessen im Browser) | Nicht separat gemessen (kein Millisekunden-Timer im Test); die Operationen sind reine Vektor-/Intervall-Arithmetik über wenige Kanten (O(Anzahl-Prismen × Kanten-pro-Prisma) mit kleinen Konstanten) und liegen der Groessenordnung nach klar unter 1 ms fuer Szenen dieser Groesse — eine belastbare Messung steht noch aus |
|
||||
| Sichtbarkeit (verdeckte Kanten) | Exakt (echter HLR-Solver) | Naeherung über Bounding-Boxen im Grundriss + Hoehenintervall-Ueberlappung; exakt fuer achsparallele/konvexe Fussabdruecke |
|
||||
| Reifegrad | Isolierter Spike, nicht verdrahtet | Isolierter Spike (dieses Modul), nicht in `render2d`/die Plan-Ansicht verdrahtet |
|
||||
|
||||
**Fazit:** Fuer den ueberwiegenden Regelfall dieses Projekts (Waende, Decken —
|
||||
alles Prismen) ist die analytische Loesung der pragmatischere Weg: kein
|
||||
zusaetzliches WASM-Gewicht, keine Fremd-Bibliothek, headless testbar wie der
|
||||
Rest von `render3d`. Der OCCT-Weg bleibt die Referenz, falls/sobald echte
|
||||
generische Volumenkoerper (Booleans, gekrümmte Flaechen) ins Modell kommen.
|
||||
@@ -0,0 +1,672 @@
|
||||
# Parametrische Wände
|
||||
|
||||
Status: Implementiert (Phase A — Typ-System und Resolver in `src/model/`, kein UI).
|
||||
Dieses Dokument spezifiziert die **Parametrischen Wände**: regelbasierte Definitionen,
|
||||
die beim Auflösen eine Liste von `Wall`-Elementen erzeugen, anstatt sie einzeln vom
|
||||
Nutzer zeichnen zu lassen.
|
||||
|
||||
Bezugsdokumente: [elements.md](elements.md) (Wand-/Türmodell),
|
||||
[drawing-tools.md](drawing-tools.md) (Werkzeugsystem, Direktzeichnen),
|
||||
[state-architecture.md](state-architecture.md) (Projekt-Store),
|
||||
[resources-graphics.md](resources-graphics.md) (WallType/Component-Auflösung).
|
||||
|
||||
Implementierungsdateien:
|
||||
- `src/model/types.ts` — `ParametricWall`, `ParametricRule` und alle Regel-Varianten.
|
||||
- `src/model/parametricWalls.ts` — `resolveParametricWall()`, `applyRule()` und
|
||||
Hilfsfunktionen.
|
||||
|
||||
---
|
||||
|
||||
## 0. Überblick
|
||||
|
||||
Eine **parametrische Wand** (`ParametricWall`) ist kein festes `Wall`-Element, sondern
|
||||
ein **Regelwerk**, das beim Auflösen (`resolveParametricWall`) eine Menge von `Wall[]`-
|
||||
Elementen generiert. Die erzeugten Wände sind gewöhnliche `Wall`-Objekte; sie
|
||||
unterscheiden sich lediglich in ihrer Herkunft. Das semantische Modell (`Project`)
|
||||
bleibt die einzige Wahrheit — parametrische Wände sind eine Ressource in der
|
||||
Ressourcen-Bibliothek, nicht eine separate Laufzeit-Geometrie-Schicht.
|
||||
|
||||
```
|
||||
Project.parametricWalls: ParametricWall[]
|
||||
│
|
||||
│ resolveParametricWall(pw, floorId, context, defaultWallType)
|
||||
▼
|
||||
Wall[] ──→ normales Rendering über generatePlan / Viewport3D
|
||||
```
|
||||
|
||||
Erzeugte Wände können entweder **temporär** (zur Laufzeit, als Ergänzung zu
|
||||
`project.walls` im Rendering-Pfad) oder **eingebacken** (als `Wall[]` fest in
|
||||
`Project.walls` gespeichert) behandelt werden. Phase A legt nur den Auflöser fest;
|
||||
die Auswahl liegt bei der aufrufenden Komponente.
|
||||
|
||||
---
|
||||
|
||||
## 1. Motivation
|
||||
|
||||
### 1.1 Schnellere Modellierung von Regelgrundrissen
|
||||
|
||||
Schweizer Wohnbauten folgen häufig einem 3-m-Achsraster (SIA-Norm, Modul-/
|
||||
Skelettbauweise). Zwanzig Wände eines Rasters von Hand zu zeichnen ist fehleranfällig
|
||||
und verhindert spätere parametrische Änderungen (z. B. Geschossanzahl, Rasterweite,
|
||||
Wandtyp).
|
||||
|
||||
Eine `GridRule` erzeugt dieses Muster aus wenigen Parametern (Achsabstand, Richtung,
|
||||
Bereich) und lässt sich mit einer einzigen Zahl auf „4-m-Büroraster" umstellen.
|
||||
|
||||
### 1.2 Kongruenz mit FreeCAD BIM / IFC
|
||||
|
||||
FreeCAD BIM kennt **ParametricObjects**, die ihre Geometrie aus Regeln ableiten (z. B.
|
||||
`ArchWall` mit `Length`, `Width`, `Height`). Obwohl das Datenformat hier kein IFC ist,
|
||||
schafft ein ähnliches Abstraktionsniveau eine spätere Brücke: Beim IFC-Export können
|
||||
parametrische Wände als `IfcWallStandardCase` mit konstanten Attributen exportiert
|
||||
werden — kein Informationsverlust gegenüber manuell gezeichneten Wänden.
|
||||
|
||||
### 1.3 Bedingte Wandtypen ohne manuelle Klassifizierung
|
||||
|
||||
Außenwände sind dicker als Innenwände; Trennwände zwischen Einheiten erfordern
|
||||
Schallschutz. Eine `ConditionalThicknessRule` (`condition: "exterior" → thickType`)
|
||||
weist den richtigen Wandtyp automatisch aus der geometrischen Lage zu — ohne dass der
|
||||
Nutzer jeden Wandabschnitt einzeln klassifizieren muss.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architektur
|
||||
|
||||
### 2.1 Typen (`src/model/types.ts`)
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Eine parametrische Wand-Regel — generiert automatisch Wall[]-Einträge für
|
||||
* ein gegebenes Geschoss. Lebt in Project.parametricWalls[].
|
||||
*/
|
||||
export interface ParametricWall {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
/**
|
||||
* Geordnete Liste der anzuwendenden Regeln. Spätere Regeln können die
|
||||
* Ausgabe früherer verfeinern (z. B. Dickenzuweisung nach Raster).
|
||||
*/
|
||||
rules: ParametricRule[];
|
||||
/**
|
||||
* Rückfall-Wandtyp, falls eine Regel keinen eigenen `wallTypeId` nennt.
|
||||
*/
|
||||
defaultWallTypeId: string;
|
||||
}
|
||||
|
||||
/** Diskriminierte Union aller Regel-Varianten. */
|
||||
export type ParametricRule =
|
||||
| GridRule
|
||||
| ModuleRule
|
||||
| ConditionalThicknessRule
|
||||
| ReferenceLineRule
|
||||
| SequenceRule;
|
||||
```
|
||||
|
||||
### 2.2 Einbettung ins Projekt
|
||||
|
||||
```ts
|
||||
export interface Project {
|
||||
// … bestehende Felder …
|
||||
/**
|
||||
* Parametrische Wanddefinitionen (Ressourcen-Bibliothek). Optional, damit
|
||||
* bestehende Projekte/Tests ohne `parametricWalls` gültig bleiben.
|
||||
*/
|
||||
parametricWalls?: ParametricWall[];
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Resolver-Kontext (`src/model/parametricWalls.ts`)
|
||||
|
||||
```ts
|
||||
export interface ParametricContext {
|
||||
/** Das Ziel-Geschoss. */
|
||||
floor: DrawingLevel;
|
||||
/**
|
||||
* Optionale Rasterachsen (Phase C: verlinkter Grid-Ressource). Fehlen sie,
|
||||
* berechnet die Engine die Achsen aus GridRule.spacing.
|
||||
*/
|
||||
gridAxes?: { x: number[]; y: number[] };
|
||||
/**
|
||||
* Optionales Clipping-Polygon (Meter). Fehlt es, reicht das Raster über
|
||||
* einen Standardbereich (0 … spacing × 10).
|
||||
*/
|
||||
boundaryGeometry?: { boundary: Vec2[] };
|
||||
/**
|
||||
* Bereits im Projekt vorhandene Wände des Geschosses. Werden von
|
||||
* refinierenden Regeln (ConditionalThicknessRule, ReferenceLineRule) genutzt.
|
||||
*/
|
||||
existingWalls?: Wall[];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Regel-Varianten
|
||||
|
||||
### 3.1 GridRule — Achsraster
|
||||
|
||||
Erzeugt parallele Wände auf einem gleichmäßigen Raster. Typischer Einsatz: Schweizer
|
||||
Wohnbau-Achsraster (3 m), Büro-Konstruktionsraster (6 m), strukturelle Raster mit
|
||||
fester Stützweite.
|
||||
|
||||
```ts
|
||||
export interface GridRule {
|
||||
type: "grid";
|
||||
/**
|
||||
* Optionaler Verweis auf eine Grid-Ressource (Phase C). Für Phase A wird
|
||||
* stattdessen `spacing` genutzt.
|
||||
*/
|
||||
gridId?: string;
|
||||
/** Rasterabstand in Metern (Default: 3.0). */
|
||||
spacing?: number;
|
||||
/**
|
||||
* Achsrichtungen: „x" = nur Wände entlang der Y-Achse,
|
||||
* „y" = nur Wände entlang der X-Achse, „both" = Vollraster.
|
||||
*/
|
||||
directions: "x" | "y" | "both";
|
||||
/** Optionaler Verweis auf Clipping-Polygon. */
|
||||
boundaryId?: string;
|
||||
/** Optionale Wandtyp-Übersteuerung; sonst defaultWallTypeId. */
|
||||
wallTypeId?: string;
|
||||
/** Lage der Wandachse über die Dicke (Vectorworks-Stil). */
|
||||
referenceLine?: WallReferenceLine;
|
||||
/** Optionale Höhenübersteuerung in Metern; sonst Geschosshöhe. */
|
||||
height?: number;
|
||||
}
|
||||
```
|
||||
|
||||
**Geometrieausgabe (top-down Grundriss):**
|
||||
|
||||
```
|
||||
directions: "x", spacing: 3.0, Bereich 0…12 m:
|
||||
|
||||
y
|
||||
│
|
||||
12 ──────────────────────────
|
||||
│
|
||||
9 ──────────────────────────
|
||||
│
|
||||
6 ──────────────────────────
|
||||
│
|
||||
3 ──────────────────────────
|
||||
│
|
||||
0 ──────────────────────────
|
||||
│
|
||||
└──────────────────────────► x
|
||||
0 12
|
||||
```
|
||||
|
||||
**Wann verwenden:**
|
||||
- Tragende Wände auf fester Stützweite (Wohnbau 3 m, Büro 6 m).
|
||||
- Vollraster (`"both"`) für strukturelle Rastersysteme.
|
||||
- In Kombination mit `ConditionalThicknessRule` zur automatischen Außen/Innen-Klassifizierung.
|
||||
|
||||
**Beispiel: Schweizer 3-m-Wohnraster**
|
||||
|
||||
```ts
|
||||
const pw: ParametricWall = {
|
||||
id: "pw-eg-raster",
|
||||
name: "EG Längswände 3m-Raster",
|
||||
defaultWallTypeId: "wt-innen-15",
|
||||
rules: [
|
||||
{
|
||||
type: "grid",
|
||||
spacing: 3.0,
|
||||
directions: "x", // Wände in X-Richtung (y = 0, 3, 6, 9, 12)
|
||||
wallTypeId: "wt-innen-15",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Auflösung:
|
||||
const walls = resolveParametricWall(pw, "floor-eg", {
|
||||
floor: egFloor,
|
||||
boundaryGeometry: { boundary: rectBoundary(0, 0, 12, 12) },
|
||||
}, defaultWallType);
|
||||
// → 5 Wände bei y = 0, 3, 6, 9, 12, je 12 m lang
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 ModuleRule — Bay-/Jochbauweise
|
||||
|
||||
Unterteilt eine Referenzspanne in gleiche Module und erzeugt Querwände an jedem
|
||||
Teilungspunkt. Typisch für Bürogebäude (6-m-Joch) oder Reihenhäuser mit modularer
|
||||
Erschließung.
|
||||
|
||||
```ts
|
||||
export interface ModuleRule {
|
||||
type: "module";
|
||||
/** Modulmaß in Metern (z. B. 6.0, 3.6). */
|
||||
moduleSize: number;
|
||||
/** Ausrichtung der Trennwände: „x" = Querwände senkrecht zu X, „y" = zu Y. */
|
||||
direction: "x" | "y";
|
||||
/**
|
||||
* Optionaler Verweis auf eine Referenzwand, die die Spannweite definiert.
|
||||
* Fehlt er, wird die Geschoss-Ausdehnung (Bounding-Box) genutzt.
|
||||
*/
|
||||
referenceWallId?: string;
|
||||
/** Optionale Wandtyp-Übersteuerung; sonst defaultWallTypeId. */
|
||||
wallTypeId?: string;
|
||||
referenceLine?: WallReferenceLine;
|
||||
height?: number;
|
||||
}
|
||||
```
|
||||
|
||||
**Wann verwenden:**
|
||||
- Wenn sich Querwände aus einer Referenzspanne (Fassade, Achswand) ergeben.
|
||||
- Vorzug vor `GridRule`, wenn nur in eine Richtung unterteilt wird und eine
|
||||
Referenzwand die Spanne definiert.
|
||||
|
||||
**Beispiel: 6-m-Bay-Bürogebäude**
|
||||
|
||||
```ts
|
||||
const pw: ParametricWall = {
|
||||
id: "pw-buero-joch",
|
||||
name: "Büro 6m-Joch",
|
||||
defaultWallTypeId: "wt-beton-20",
|
||||
rules: [
|
||||
{
|
||||
type: "module",
|
||||
moduleSize: 6.0,
|
||||
direction: "x", // Querwände senkrecht zur X-Achse
|
||||
// referenceWallId: "W-sudfassade" → Spanne aus der Südwand ableiten
|
||||
},
|
||||
],
|
||||
};
|
||||
// resolveParametricWall → Querwände bei x = 6, 12, 18, 24, 30 (bei 36-m-Fassade)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.3 ConditionalThicknessRule — Bedingte Wandtypen
|
||||
|
||||
Weist bereits erzeugten Wänden (aus vorherigen Regeln in der Sequenz) einen anderen
|
||||
Wandtyp zu — abhängig von einer Bedingung. Gibt modifizierte **Kopien** zurück; die
|
||||
Eingabe-Wände werden nicht mutiert.
|
||||
|
||||
```ts
|
||||
export interface ConditionalThicknessRule {
|
||||
type: "conditional-thickness";
|
||||
/**
|
||||
* Bedingung für den Treffer:
|
||||
* • „exterior" — Wand liegt am Außenrand (Bounding-Box des Kontexts).
|
||||
* • „interior" — Wand liegt im Inneren.
|
||||
* • „bearing" — tragende Wand (Heuristikum: Wand läuft ±10° zu X/Y-Achse).
|
||||
* • beliebiger String — benutzerdefiniertes Tag (Phase C: Wall.tags[]).
|
||||
*/
|
||||
condition: "exterior" | "interior" | "bearing" | string;
|
||||
/** Ziel-Wandtyp, der bei Treffer gesetzt wird. */
|
||||
wallTypeId: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Wann verwenden:**
|
||||
- Immer in Kombination mit `GridRule` oder `ModuleRule` (als zweite Regel in
|
||||
`ParametricWall.rules`): Raster erzeugt, Dicke verfeinert.
|
||||
- Wenn Außen- und Innenwände denselben geometrischen Ursprung haben, aber
|
||||
verschiedene Aufbauten benötigen.
|
||||
|
||||
**Beispiel: Außen dick, Innen dünn**
|
||||
|
||||
```ts
|
||||
const pw: ParametricWall = {
|
||||
id: "pw-eg-komplett",
|
||||
name: "EG Vollraster mit Außenwand-Differenzierung",
|
||||
defaultWallTypeId: "wt-innen-15",
|
||||
rules: [
|
||||
{
|
||||
type: "grid", spacing: 3.0, directions: "both",
|
||||
wallTypeId: "wt-innen-15",
|
||||
},
|
||||
{
|
||||
type: "conditional-thickness",
|
||||
condition: "exterior",
|
||||
wallTypeId: "wt-aussen-36", // Außenwände erhalten dicken Aufbau
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 ReferenceLineRule — Wandachsen-Lage
|
||||
|
||||
Setzt `referenceLine` bei passenden Wänden einheitlich (Vectorworks-Stil: Achse
|
||||
links/rechts/mittig). Gibt modifizierte Kopien zurück.
|
||||
|
||||
```ts
|
||||
export interface ReferenceLineRule {
|
||||
type: "reference-line";
|
||||
/** Neue Lage der Wandachse, die einheitlich gesetzt wird. */
|
||||
referenceLine: WallReferenceLine; // "left" | "center" | "right"
|
||||
/**
|
||||
* Filterziel:
|
||||
* • „all" — alle Wände im aktuellen Satz.
|
||||
* • „exterior" — nur Außenwände.
|
||||
* • beliebiger String — benutzerdefiniertes Tag (Phase C).
|
||||
*/
|
||||
target: "all" | "exterior" | string;
|
||||
}
|
||||
```
|
||||
|
||||
**Wann verwenden:**
|
||||
- Außenwände auf `"left"` setzen (Achse liegt auf der Fassadenfläche).
|
||||
- Als abschließende Regel in einer `SequenceRule` nach Raster und Dickenzuweisung.
|
||||
|
||||
---
|
||||
|
||||
### 3.5 SequenceRule — Zusammenfassung von Unterregeln
|
||||
|
||||
Fasst mehrere Regeln als atomare Einheit zusammen. Jede Unterregel erhält die Ausgabe
|
||||
der vorherigen als `existingWalls` — so können spätere Regeln frühere verfeinern.
|
||||
|
||||
```ts
|
||||
export interface SequenceRule {
|
||||
type: "sequence";
|
||||
rules: ParametricRule[];
|
||||
/**
|
||||
* Wenn true: Abbruch nach der ersten Unterregel, die mindestens eine Wand
|
||||
* generiert/verändert hat (Short-Circuit-Fallback).
|
||||
*/
|
||||
stopOnMatch?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**Wann verwenden:**
|
||||
- Um eine zusammengehörige Kombination (Raster → Dicke → Referenzlinie) als
|
||||
Untermodul wiederzuverwenden — z. B. in unterschiedlichen Geschossen mit leicht
|
||||
abweichenden Parametern.
|
||||
|
||||
---
|
||||
|
||||
## 4. Resolver-API (`src/model/parametricWalls.ts`)
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Löst ein ParametricWall-Regelwerk zu einem Wall[]-Array für ein gegebenes
|
||||
* Geschoss auf.
|
||||
*
|
||||
* Ablauf:
|
||||
* 1. Regelwerk sequenziell ausführen; jede Regel erhält die Ausgabe der
|
||||
* vorherigen als existingWalls (ermöglicht Verfeinerung).
|
||||
* 2. Duplikate (gleicher Start-/Endpunkt innerhalb tolerance) entfernen.
|
||||
* 3. Bereinigte Wall[]-Liste zurückgeben.
|
||||
*
|
||||
* Die Ausgabe ist sofort bereit zur Einfügung in project.walls. Es werden
|
||||
* keine Seiteneffekte erzeugt — kein Store, kein Dispatch, kein React.
|
||||
*
|
||||
* @param parametricWall Das Regelwerk.
|
||||
* @param floorId ID des Ziel-Geschosses.
|
||||
* @param context Kontext (Geschoss-Objekt, Grid-Achsen, Grenzen, …).
|
||||
* @param defaultWallType Fallback-Wandtyp, wenn eine Regel keinen nennt.
|
||||
* @param tolerance Näherungstoleranz für Duplikat-Erkennung (Meter, Default 0.01).
|
||||
* @returns Wall[]-Array, bereit zur Einfügung.
|
||||
*/
|
||||
export function resolveParametricWall(
|
||||
parametricWall: ParametricWall,
|
||||
floorId: string,
|
||||
context: ParametricContext,
|
||||
defaultWallType: WallType,
|
||||
tolerance?: number,
|
||||
): Wall[];
|
||||
|
||||
/**
|
||||
* Dispatcher: delegiert eine Regel an die passende Implementierung.
|
||||
* Exportiert für Unit-Tests und erweiterbare Regeltypen.
|
||||
*/
|
||||
export function applyRule(rule: ParametricRule, ctx: RuleCtx): Wall[];
|
||||
|
||||
/**
|
||||
* Entfernt doppelte Wände: zwei Wände gelten als Duplikat, wenn Start- und
|
||||
* Endpunkt jeweils innerhalb tolerance übereinstimmen (vorwärts und rückwärts).
|
||||
*/
|
||||
export function deduplicateWalls(walls: Wall[], tolerance?: number): Wall[];
|
||||
```
|
||||
|
||||
### 4.1 Höhenauflösung
|
||||
|
||||
Die Wandhöhe (`Wall.height`) ergibt sich nach folgender Priorität:
|
||||
|
||||
1. `rule.height`, falls an der einzelnen Regel gesetzt.
|
||||
2. `context.floor.floorHeight` des Zielgeschosses.
|
||||
3. Fallback: 2.6 m (globaler Default, CONVENTIONS.md).
|
||||
|
||||
### 4.2 ID-Schema
|
||||
|
||||
```
|
||||
"pw-<floorId>-gx-<counter>" // GridRule, X-Achse
|
||||
"pw-<floorId>-gy-<counter>" // GridRule, Y-Achse
|
||||
"pw-<floorId>-mx-<counter>" // ModuleRule, X-Teilung
|
||||
"pw-<floorId>-ct-<counter>" // ConditionalThicknessRule
|
||||
"pw-<floorId>-rl-<counter>" // ReferenceLineRule
|
||||
```
|
||||
|
||||
IDs sind sessionlokal (Zähler startet bei 0 je Modullade). Eingebrannte Wände
|
||||
erhalten beim Commit neue stabile IDs über `uniqueId("W")` — konsistent mit dem
|
||||
Wand-Werkzeug (vgl. [drawing-tools.md §8](drawing-tools.md#8-id-vergabe--immutabilität)).
|
||||
|
||||
### 4.3 Duplikat-Erkennung
|
||||
|
||||
`deduplicateWalls` vergleicht Start-/Endpunkte beider Wände (vorwärts: A→B == A→B,
|
||||
und rückwärts: A→B == B→A) innerhalb einer Toleranz von 1 cm (0.01 m). Die **erste**
|
||||
Instanz wird behalten; spätere Duplikate werden verworfen. Dies ist wichtig bei
|
||||
Vollrastern (`"both"`), bei denen X- und Y-Wände exakt auf einem Rasterpunkt
|
||||
zusammentreffen könnten.
|
||||
|
||||
### 4.4 Verhalten bei ungültigen Eingaben
|
||||
|
||||
| Situation | Verhalten |
|
||||
|-----------|-----------|
|
||||
| `spacing <= 0` oder `moduleSize <= 0` | `[]` |
|
||||
| `referenceWallId` nicht in `existingWalls` | Fallback auf Bounding-Box, kein Fehler |
|
||||
| Unbekannter `condition`-String | `matchesCondition` gibt `false` zurück (kein Treffer) |
|
||||
| Unbekannter `SequenceRule`-Untertyp | TypeScript exhaustiveness-Guard, `[]` |
|
||||
| Segment mit `|end - start| < 1e-6` m | Kann durch deduplicateWalls entfernt werden |
|
||||
|
||||
---
|
||||
|
||||
## 5. Integration ins Projekt
|
||||
|
||||
### 5.1 Ressourcen-Speicherung
|
||||
|
||||
`ParametricWall`-Einträge leben unter `Project.parametricWalls` (optionales Array).
|
||||
Sie sind Teil des `.cad.json`-Dokuments und werden mit dem Rest des Projekts gespeichert.
|
||||
|
||||
```ts
|
||||
// sampleProject.ts — Beispieleintrag
|
||||
export const sampleProject: Project = {
|
||||
// …
|
||||
parametricWalls: [
|
||||
{
|
||||
id: "pw-eg-raster",
|
||||
name: "EG Längswände 3m-Raster",
|
||||
defaultWallTypeId: "wt-innen-15",
|
||||
rules: [
|
||||
{ type: "grid", spacing: 3.0, directions: "x" },
|
||||
{ type: "conditional-thickness", condition: "exterior",
|
||||
wallTypeId: "wt-aussen-36" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### 5.2 Rendering ohne UI (Phase A)
|
||||
|
||||
In Phase A werden parametrische Wände **nicht** automatisch gerendert. Der Auflöser
|
||||
ist eine reine Funktion; Aufrufer müssen ihn explizit einbinden. Mögliche Verwendung
|
||||
in `generatePlan` oder `Viewport3D`:
|
||||
|
||||
```ts
|
||||
// generatePlan.ts (Ergänzung, Phase A)
|
||||
const defaultWallType = project.wallTypes[0];
|
||||
const extraWalls = (project.parametricWalls ?? []).flatMap((pw) =>
|
||||
resolveParametricWall(pw, activeLevelId, {
|
||||
floor: activeFloor,
|
||||
boundaryGeometry: projectBoundary,
|
||||
}, defaultWallType)
|
||||
);
|
||||
const allWalls = [...project.walls, ...extraWalls];
|
||||
// … allWalls statt project.walls in der Rendering-Pipeline verwenden
|
||||
```
|
||||
|
||||
### 5.3 Keine UI in Phase A
|
||||
|
||||
Kein Command, kein Panel, kein Formular. `ParametricWall`-Einträge werden in Phase A
|
||||
ausschließlich **programmatisch** (Unit-Tests, `sampleProject`, direkte JSON-Bearbeitung
|
||||
des Projekts) erstellt.
|
||||
|
||||
---
|
||||
|
||||
## 6. Ausblick: Folge-Phasen
|
||||
|
||||
### Phase B — UI und Command-Schnittstelle
|
||||
|
||||
- Neues Command (z. B. `PWWALL`) oder Ressourcen-Manager-Tab „Parametrische Wände"
|
||||
mit Formular-Editor je Regeltyp.
|
||||
- „Einbrennen" (Flatten): `ParametricWall` → feste `Wall[]` in `Project.walls`
|
||||
einfügen und den `ParametricWall`-Eintrag entfernen (unidirektional, Undo über Store).
|
||||
- Auswahl parametrischer Wände im Plan (als Gruppe); Grip-Editing der Raster-Parameter
|
||||
und Spannweiten.
|
||||
|
||||
### Phase C — Grid-Ressource und Schnittpunkt-Clipping
|
||||
|
||||
- `GridResource`: ein projektweites, benanntes Koordinatenraster (LV95-Offset,
|
||||
Rasterweite, Drehung), auf das mehrere `GridRule`-Instanzen via `gridId` verweisen.
|
||||
- Präzises Clipping: erzeugte Wände werden am Gebäudeumriss getrimmt — exakte
|
||||
`lineIntersect`-Berechnung statt Bounding-Box-Approximation.
|
||||
- Benutzerdefinierte Tags (`Wall.tags[]`) für komplexe `ConditionalThicknessRule`-
|
||||
Bedingungen jenseits von „exterior/interior/bearing".
|
||||
- IFC-Export: `ParametricWall`-Gruppen → `IfcWallStandardCase` mit parametrischen
|
||||
Attributen und `IfcRelDefinesByType`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Vollständige Anwendungsbeispiele
|
||||
|
||||
### 7.1 Schweizer Wohnbau: 3-m-Raster EG + 1.OG
|
||||
|
||||
Zwei-Geschoss-Wohnhaus, typisches CH-Wohnbauraster. Die Längswände beider Geschosse
|
||||
entstehen aus zwei `ParametricWall`-Einträgen mit identischen Regeln, unterschieden
|
||||
nur durch `floorId` beim Auflösen:
|
||||
|
||||
```
|
||||
Top-down (Grundriss):
|
||||
|
||||
y=12 ──────────────────────────── (W5)
|
||||
y=9 ──────────────────────────── (W4)
|
||||
y=6 ──────────────────────────── (W3)
|
||||
y=3 ──────────────────────────── (W2)
|
||||
y=0 ──────────────────────────── (W1)
|
||||
↑
|
||||
x=0 x=12
|
||||
```
|
||||
|
||||
```ts
|
||||
const rasterRegel: ParametricWall = {
|
||||
id: "pw-laengswand-raster",
|
||||
name: "Längswände 3m-Raster",
|
||||
defaultWallTypeId: "wt-innen-15",
|
||||
rules: [
|
||||
{ type: "grid", spacing: 3.0, directions: "x" },
|
||||
// Außenwände (y=0 und y=12) erhalten den dicken Aufbau:
|
||||
{ type: "conditional-thickness", condition: "exterior",
|
||||
wallTypeId: "wt-aussen-36" },
|
||||
// Außenwände: Achse liegt auf der Fassadenfläche:
|
||||
{ type: "reference-line", referenceLine: "left", target: "exterior" },
|
||||
],
|
||||
};
|
||||
|
||||
// EG auflösen:
|
||||
const wallsEG = resolveParametricWall(rasterRegel, "floor-eg",
|
||||
{ floor: egFloor, boundaryGeometry: { boundary: rect(0,0,12,12) } },
|
||||
project.wallTypes[0]);
|
||||
|
||||
// 1.OG auflösen (gleiche Regel, anderes Geschoss):
|
||||
const wallsOG = resolveParametricWall(rasterRegel, "floor-og1",
|
||||
{ floor: ogFloor, boundaryGeometry: { boundary: rect(0,0,12,12) } },
|
||||
project.wallTypes[0]);
|
||||
// Änderung spacing: 3.5 → beide Geschosse sofort konsistent.
|
||||
```
|
||||
|
||||
### 7.2 Vollraster mit Außen/Innen-Differenzierung
|
||||
|
||||
Gebäudeumriss als Rechteck; die Randwände erhalten automatisch den dicken
|
||||
Außenwand-Typ, alle anderen den dünnen Innenwand-Typ:
|
||||
|
||||
```ts
|
||||
const vollraster: ParametricWall = {
|
||||
id: "pw-eg-vollraster",
|
||||
name: "EG Vollraster mit Differenzierung",
|
||||
defaultWallTypeId: "wt-innen-15",
|
||||
rules: [
|
||||
{ type: "grid", spacing: 3.0, directions: "both" },
|
||||
{ type: "conditional-thickness", condition: "exterior",
|
||||
wallTypeId: "wt-aussen-36" },
|
||||
{ type: "conditional-thickness", condition: "interior",
|
||||
wallTypeId: "wt-innen-15" },
|
||||
{ type: "reference-line", referenceLine: "left", target: "exterior" },
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
```
|
||||
─────┬─────┬─────┬─────
|
||||
│ │ │ │ │
|
||||
─────┼─────┼─────┼─────
|
||||
│ │ │ │ │
|
||||
─────┴─────┴─────┴─────
|
||||
|
||||
Rand-Segmente: wt-aussen-36 (dicker Aufbau)
|
||||
Innen-Segmente: wt-innen-15 (dünner Aufbau)
|
||||
```
|
||||
|
||||
### 7.3 Modulbauweise: 6-m-Joch, Bürogebäude
|
||||
|
||||
Längliches Bürogebäude, 36 m × 12 m, 6-m-Joch. Querwände entstehen automatisch;
|
||||
Entwurfsänderung (z. B. auf 7.2-m-Joch) erfordert eine einzige Zahl:
|
||||
|
||||
```ts
|
||||
const joch: ParametricWall = {
|
||||
id: "pw-buero-joch",
|
||||
name: "Büro 6m-Joch",
|
||||
defaultWallTypeId: "wt-beton-20",
|
||||
rules: [
|
||||
{
|
||||
type: "module",
|
||||
moduleSize: 6.0,
|
||||
direction: "x", // Querwände senkrecht zur X-Achse
|
||||
// referenceWallId: "W-sudfassade" → Spanne aus Referenzwand
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// resolveParametricWall → Querwände bei x = 6, 12, 18, 24, 30
|
||||
// (bei Bounding-Box minX=0, maxX=36, Enden selbst ausgespart)
|
||||
|
||||
// Änderung auf 7.2-m-Joch: moduleSize: 7.2
|
||||
// → 4 Trennwände bei x ≈ 7.2, 14.4, 21.6, 28.8 — automatisch neu berechnet.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Architektur-Garantien
|
||||
|
||||
- **Modell bleibt einzige Wahrheit.** `ParametricWall`-Definitionen sind Daten in
|
||||
`Project.parametricWalls`; `resolveParametricWall` ist eine **reine Funktion** ohne
|
||||
Side-Effects. Keine globale Laufzeit-Geometrie-Schicht.
|
||||
- **Erzeugte Wände sind gewöhnliche `Wall`-Objekte.** Alle nachgelagerten Systeme
|
||||
(`generatePlan`, `Viewport3D`, `computeJoins`) arbeiten unverändert; sie müssen
|
||||
nicht zwischen „parametrisch erzeugten" und „direkt gezeichneten" Wänden
|
||||
unterscheiden.
|
||||
- **Fehlertoleranz statt Absturz.** Unbekannte Regeltypen liefern `[]`; der TypeScript-
|
||||
exhaustiveness-Guard fängt fehlende `case`-Zweige zur Compilezeit. Unbekannte
|
||||
Bedingungsstrings in `ConditionalThicknessRule` geben `false` (kein Treffer) statt
|
||||
zu werfen.
|
||||
- **Keine vorzeitige Generalisierung.** Phase A liefert fünf Regel-Varianten und
|
||||
einen Auflöser. UI, Command-Schnittstelle und Grid-Ressource folgen in Phase B/C.
|
||||
- **Immutabilität.** Verfeinerungsregeln (`ConditionalThicknessRule`,
|
||||
`ReferenceLineRule`) geben modifizierte **Kopien** zurück; `existingWalls` werden
|
||||
nie mutiert — konsistent mit der `setProject`-Konvention (CONVENTIONS.md).
|
||||
@@ -0,0 +1,306 @@
|
||||
# Architektur-Pivot: Tauri + Rust-Backend (2026-07-01)
|
||||
|
||||
## Entscheidung
|
||||
|
||||
**Alte Welt:** Browser-CAD (React/Vite + WebGL/three.js)
|
||||
**Neue Welt:** Desktop Tauri-App (React/Vite Frontend + Rust-Backend + **wgpu 3D-Rendering**)
|
||||
|
||||
**Grund:** Komplexe Möbel mit vielen Polygonen (100k+) + mehrere parallele rechenintensive Ops → WebGL/three.js wird zum Bottleneck. **wgpu** (low-level GPU-API auf Vulkan/Metal/DX12) + Rust-Compute skaliert native.
|
||||
|
||||
**Scope:** Desktop-only Release (Milestone 1). WASM-Fallback für Browser später wenn gebraucht.
|
||||
|
||||
---
|
||||
|
||||
## Post-Migration Stack
|
||||
|
||||
### Frontend (React/Vite — Komponenten + State, unverändert)
|
||||
|
||||
```
|
||||
src/
|
||||
App.tsx ← Shell-Komponente
|
||||
compute/index.ts ← Compute-Boundary (neu)
|
||||
model/types.ts ← Semantisches Modell
|
||||
commands/ ← Befehlssystem
|
||||
panels/ ← UI-Panels
|
||||
plan/PlanView.tsx ← 2D-SVG-Rendering
|
||||
viewport/Viewport3D.tsx ← three.js 3D-Display
|
||||
ui/ ← Topbar, Dialogs, etc.
|
||||
state/ ← Redux-Slices (project, selection, view, layout)
|
||||
...
|
||||
```
|
||||
|
||||
**Rolle:** User-Input-Handling, 2D/3D-Darstellung (Display-Layer), State-Management.
|
||||
|
||||
### Backend (Rust/Tauri — neu)
|
||||
|
||||
```
|
||||
src-tauri/
|
||||
src/
|
||||
main.rs ← Tauri window + invoke-handler registration
|
||||
geometry.rs ← compute_joins(), kernel2d(), etc.
|
||||
parsers/
|
||||
dwg.rs ← DXF/DWG-Geometrie-Parsing
|
||||
dxf.rs
|
||||
sia/
|
||||
room_detection.rs ← detectRooms() (SIA-416)
|
||||
...
|
||||
Cargo.toml ← Dependencies (serde, tauri, …)
|
||||
```
|
||||
|
||||
**Rolle:** Rechenintensive Ops, Geometrie-Kernel, Parsing, SIA-Raumerkennung.
|
||||
|
||||
### IPC: Tauri invoke (async, serde)
|
||||
|
||||
```typescript
|
||||
// Frontend ruft Rust auf
|
||||
const result = await invoke<JoinInfo[]>('compute_joins', { walls, joints });
|
||||
|
||||
// Rust bearbeitet + serialisiert Ergebnis
|
||||
#[tauri::command]
|
||||
async fn compute_joins(input: JoinInput) -> Result<JoinOutput, String> {
|
||||
geometry::compute_joins(input).map_err(|e| e.to_string())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compute-Boundary (Key Design)
|
||||
|
||||
**Neue Datei:** `src/compute/index.ts` — einziger Eingang für rechenintensive Ops.
|
||||
|
||||
```typescript
|
||||
// Beispiel-Schnittstellen
|
||||
export async function computeJoins(walls: Wall[], joints: Array<[number, number]>): Promise<JoinInfo[]> { … }
|
||||
export async function computeKernel2D(op: 'offset'|'trim', geom: Polyline, …): Promise<Polyline[]> { … }
|
||||
export async function detectRooms(…): Promise<Room[]> { … }
|
||||
export async function parseShapeFromDwg(…): Promise<DwgGeometry> { … }
|
||||
```
|
||||
|
||||
**Hinter der Boundary:**
|
||||
1. Versuche Tauri invoke zu Rust (`#[tauri::command]`)
|
||||
2. Fallback auf lokale TS-Impl wenn Rust nicht verfügbar (während Migration)
|
||||
|
||||
**Effekt:** Aufrufort im Code bleibt stabil; Caller sieht nicht, ob Op schon in Rust ist oder noch TS.
|
||||
|
||||
**Migrationsfluss:**
|
||||
```
|
||||
1. TS-Impl existiert (z.B. src/model/joins.ts)
|
||||
2. Neue Op in Compute-Boundary mit Invoke+Fallback
|
||||
3. Parallel: Rust-Impl in src-tauri/src/geometry.rs
|
||||
4. Tests: Rust-Output == TS-Output (Parität)
|
||||
5. TS-Impl bleibt (Fallback, wird nicht entfernt bis Rust stable)
|
||||
6. Eventuell: TS-Impl löschen wenn Rust bewährt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dev-Workflow (post-Tauri)
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# Terminal 1: Vite dev-server
|
||||
npm run dev # localhost:5173
|
||||
|
||||
# Terminal 2: Tauri dev
|
||||
npm run tauri:dev # öffnet Tauri-Fenster, zeigt auf localhost:5173
|
||||
# Rust hot-reload + TS hot-reload gleichzeitig
|
||||
```
|
||||
|
||||
**Voraussetzungen:**
|
||||
- Node.js + npm (wie heute)
|
||||
- Rust + Cargo (neu)
|
||||
- Tauri CLI: `npm install -D @tauri-apps/cli`
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
# Single command
|
||||
npm run tauri:build
|
||||
|
||||
# Erzeugt:
|
||||
# - Windows: src-tauri/target/release/cad.exe
|
||||
# - macOS: src-tauri/target/release/bundle/macos/cad.app
|
||||
# - Linux: src-tauri/target/release/bundle/deb/cad_*.deb (oder Flatpak)
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Rust-Unit-Tests
|
||||
cargo test # in src-tauri/
|
||||
|
||||
# TS-Tests (unverändert)
|
||||
npm run test
|
||||
|
||||
# Integration-Test: App starten + Aktion prüfen
|
||||
npm run tauri:dev # manuell testen oder Puppeteer-Probe erweitern
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GPU-Strategy (für später)
|
||||
|
||||
**Milestone 1 (jetzt):** CPU-Ops in Rust (kernel2d, joins, parsing, SIA).
|
||||
|
||||
**Milestone 2 (später):** GPU-Compute via wgpu
|
||||
- Tauri + wgpu Renderer (optional, nicht erforderlich)
|
||||
- ODER drei.js bleibt, Rust handelt CPU-Ops, three.js handelt Display
|
||||
- GPU-Heavy-Ops (z.B. große Boolean-Operationen) können in wgpu laufen, aber MVP braucht das nicht
|
||||
|
||||
**Aktueller Plan:** three.js bleibt für 3D-Display (skaliert ausreichend für Möbel-Geometrie mit Instancing + LOD).
|
||||
|
||||
---
|
||||
|
||||
## Migration Strategy: Ops nach Priorisierung
|
||||
|
||||
**Phase 1 (aktuell — Tauri-Shell + Proof-of-Concept):**
|
||||
- [ ] `computeJoins` (Wand-Eckverbindungen) → Rust
|
||||
- Gründe: klein, häufig, zeigt invoke-Flow
|
||||
|
||||
**Phase 2 (nächst):**
|
||||
- [ ] `kernel2d` (Offset/Trim/Extend/Fillet) → Rust
|
||||
- Gründe: Rechenlast ⭐⭐, Frequenz hoch
|
||||
- [ ] DXF/DWG-Parser → Rust (Geometrie-Extraktion)
|
||||
- Gründe: Rechenlast ⭐⭐, Frequenz mittel (Import-Dialog)
|
||||
|
||||
**Phase 3 (später):**
|
||||
- [ ] `detectRooms` (SIA-416 Raumerkennung) → Rust
|
||||
- Gründe: Rechenlast ⭐, async-freundlich
|
||||
- [ ] `booleanOps` (Union/Differenz/Schnitt) → Rust
|
||||
- Gründe: Rechenlast ⭐⭐, Frequenz gering (ad-hoc)
|
||||
|
||||
---
|
||||
|
||||
## Folgen für bestehenden Code
|
||||
|
||||
### Was ändert sich NICHT
|
||||
|
||||
- `src/model/types.ts` — semantisches Modell bleibt in TS (Frontend kennt es)
|
||||
- `src/state/` — Redux-Store unverändert
|
||||
- `src/ui/` — Komponenten unverändert
|
||||
- `src/plan/PlanView.tsx` — SVG-Rendering unverändert
|
||||
- `src/viewport/Viewport3D.tsx` — three.js-Rendering unverändert
|
||||
- `src/commands/` — Befehlssystem unverändert
|
||||
|
||||
### Was ändert sich
|
||||
|
||||
- **Neue `src/compute/index.ts`** — alle rechenintensiven Ops laufen durch hier
|
||||
- **Neue `src-tauri/`** — Rust-Backend
|
||||
- **Vite-Config:** Tauri plugin hinzufügen
|
||||
- **Package.json:** tauri scripts hinzufügen
|
||||
- **Build-Prozess:** `npm run tauri:build` statt `npm run build`
|
||||
|
||||
### Was wird migriert (schrittweise)
|
||||
|
||||
- `src/model/joins.ts` → `src-tauri/src/geometry.rs` (Phase 1)
|
||||
- `src/geometry/kernel2d.ts` → `src-tauri/src/geometry.rs` (Phase 2)
|
||||
- `src/io/{dxfParser, dwgParser}.ts` → `src-tauri/src/parsers/` (Phase 2)
|
||||
- `src/geometry/{roomArea, roomBoundary}.ts` → `src-tauri/src/sia/room_detection.rs` (Phase 3)
|
||||
- `src/editors/booleanOps.ts` → `src-tauri/src/geometry.rs` (Phase 3)
|
||||
|
||||
**Wichtig:** TS-Versionen bleiben als Fallback (nicht gelöscht).
|
||||
|
||||
---
|
||||
|
||||
## Distribution (später)
|
||||
|
||||
### Desktop Binaries (post-Tauri)
|
||||
|
||||
- **Windows:** `.exe` (standalone executable)
|
||||
- **macOS:** `.app` bundle (code-signed)
|
||||
- **Linux:** `.deb` package ODER **Flatpak** (preferred)
|
||||
- Flatpak = moderne WebKitGTK6 immer dabei, unabhängig von Distro-Alter
|
||||
|
||||
### Browser (wenn gebraucht)
|
||||
|
||||
- **WASM-Fallback** für `src/compute/` Ops (Rust → WASM via wasm-bindgen)
|
||||
- Later-phase feature, nicht Milestone 1
|
||||
|
||||
---
|
||||
|
||||
## Technische Details
|
||||
|
||||
### Serialisierung (TS ↔ Rust)
|
||||
|
||||
**serde + serde_json** für Geometrie-Typen:
|
||||
|
||||
```rust
|
||||
// Rust
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Vec2 { pub x: f64, pub y: f64 }
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Wall {
|
||||
pub id: String,
|
||||
pub start: Vec2,
|
||||
pub end: Vec2,
|
||||
// …
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// TS (type-safe invoke)
|
||||
interface Vec2 { x: number; y: number }
|
||||
interface Wall { id: string; start: Vec2; end: Vec2; /* … */ }
|
||||
|
||||
await invoke<JoinInfo[]>('compute_joins', { walls: Wall[] })
|
||||
```
|
||||
|
||||
### Tauri Security (default)
|
||||
|
||||
- Invoke-Handler sind Rust-side validiert
|
||||
- Whitelist-Makro (`#[tauri::command]`) registered nur explizit erlaubte Functions
|
||||
- CORS/CSP Policy default secure
|
||||
- Keine arbitrary-Script-Execution (native app)
|
||||
|
||||
---
|
||||
|
||||
## Abhängigkeiten (neu post-Tauri)
|
||||
|
||||
### Frontend (npm)
|
||||
- Bestehende: react, vite, three.js, redux, etc.
|
||||
- Neu: `@tauri-apps/api` (JS-SDK für invoke)
|
||||
- Optional später: `@tauri-apps/cli` dev-dependency (bereits in package.json)
|
||||
|
||||
### Backend (Cargo)
|
||||
```toml
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["webkit2gtk-6.0"] } # GTK4
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
# später: wgpu, delaunator, opencascade-sys, etc.
|
||||
```
|
||||
|
||||
### System
|
||||
- Rust 1.70+
|
||||
- GTK4 dev libraries (Linux only, auto-handled by Tauri)
|
||||
- Xcode Command Line Tools (macOS, auto-checked)
|
||||
|
||||
---
|
||||
|
||||
## Next Steps (Koordination)
|
||||
|
||||
**Aufgabe für nächste Phase:**
|
||||
→ Siehe `docs/design/tauri-migration-plan.md` (Schritt-für-Schritt, vier parallele Agents)
|
||||
|
||||
**HANDOVER.md:** wird aktualisiert nach Tauri-Shell stabil.
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Läuft die App noch im Browser?**
|
||||
A: Nein (Milestone 1). Desktop-only. WASM-Fallback für Browser später wenn gebraucht.
|
||||
|
||||
**Q: Was passiert mit dem existing TS-Code?**
|
||||
A: Bleibt unverändert (außer neue Compute-Boundary). TS-Implementierungen = Fallback bis Rust stabil.
|
||||
|
||||
**Q: Muss ich Rust können um das Projekt zu verstehen?**
|
||||
A: Nein. Frontend bleibt React/TS. Rust ist "blackbox" hinter invoke. Aber bei Rust-Bugs muss man rein.
|
||||
|
||||
**Q: Wann ist Tauri-Shell fertig?**
|
||||
A: Nach den vier Agents (Schritt 1–4 in tauri-migration-plan.md), ~1–2 Wochen.
|
||||
|
||||
**Q: Kann ich lokal testen?**
|
||||
A: Ja, `npm run tauri:dev` öffnet lokale App. Alles wie heute, nur Rust hinten dran.
|
||||
@@ -0,0 +1,243 @@
|
||||
# Tauri-Migration + Compute-Boundary — Aufgabe für nächste Instanz
|
||||
|
||||
**Entscheidung (fix, 2026-07-01):** Browser-CAD → **Desktop Tauri-App mit Rust-Backend + wgpu-Rendering.**
|
||||
|
||||
**Grund:** komplexe Möbel mit vielen Polygonen (100k+) + mehrere parallele rechenintensive Ops → WebGL/three.js wird zum Blocker. **wgpu** (low-level GPU-API) + Rust-Compute skaliert native.
|
||||
|
||||
**Scope:** Desktop-only Release (Milestone 1). WASM-Fallback für Browser später wenn gebraucht.
|
||||
|
||||
**Rendering-Engine:** three.js → **wgpu** (Milestone 2)
|
||||
|
||||
---
|
||||
|
||||
## Aufgabe: Shell aufsetzen + Compute-Boundary + erste Op migrieren
|
||||
|
||||
### Schritt 0 — Compute-Boundary (TS-Kontrakt)
|
||||
|
||||
**Neu:** `src/compute/index.ts` — die einzige Stelle, durch die alle rechenintensiven Ops laufen.
|
||||
|
||||
```typescript
|
||||
// src/compute/index.ts — einheitliche Schnittstelle
|
||||
export async function computeKernel2D(op: 'offset'|'trim', …): Promise<Polyline[]> { … }
|
||||
export async function detectRooms(…): Promise<Room[]> { … }
|
||||
export async function computeJoins(…): Promise<JoinInfo[]> { … } // ← erste Op
|
||||
export async function parseShapeFromDwg(…): Promise<DwgGeometry> { … }
|
||||
```
|
||||
|
||||
**Hinter der Boundary:**
|
||||
- Erst Tauri-invoke zu Rust `#[tauri::command]`
|
||||
- Fallback auf lokale TS-Impl (bleibt unberührt, bis Rust stabil)
|
||||
- `catch(err) → console.warn('Rust failed, using TS fallback'); return tsImpl(…)`
|
||||
|
||||
**Effekt:** Aufrufort im Code bleibt stabil; Caller sieht nicht, ob Op schon in Rust ist oder noch TS.
|
||||
|
||||
---
|
||||
|
||||
### Schritt 1 — Tauri-Shell aufsetzen
|
||||
|
||||
**Struktur:**
|
||||
```
|
||||
repo/
|
||||
src-tauri/ ← neue Rust-Seite (Tauri-Konvention)
|
||||
src/
|
||||
main.rs ← Tauri window + invoke handlers
|
||||
geometry.rs ← compute_joins() + weitere Ops später
|
||||
...
|
||||
Cargo.toml
|
||||
src/ ← React/TS (unverändert)
|
||||
compute/
|
||||
index.ts ← Compute-Boundary
|
||||
...
|
||||
vite.config.ts ← Tauri plugin integrieren
|
||||
package.json ← tauri scripts
|
||||
```
|
||||
|
||||
**Setup:**
|
||||
1. `cargo init --name cad-tauri src-tauri` (oder `src-tauri` manuell anlegen)
|
||||
2. `Cargo.toml`: Tauri v2 einbinden mit Feature `webkit2gtk-6.0` (GTK4)
|
||||
```toml
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["webkit2gtk-6.0"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
```
|
||||
3. `src-tauri/src/main.rs`: Minimal-Fenster, invoke-Handler registrieren
|
||||
```rust
|
||||
#[tauri::command]
|
||||
async fn compute_joins(input: JoinInput) -> Result<JoinOutput, String> {
|
||||
// Rust-Impl
|
||||
geometry::compute_joins(input).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![compute_joins])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
```
|
||||
4. `vite.config.ts`: Tauri plugin + dev-server-Integration
|
||||
```typescript
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 5173 } // Tauri dev zeigt hier drauf
|
||||
})
|
||||
```
|
||||
5. `package.json`: Tauri scripts hinzufügen
|
||||
```json
|
||||
"scripts": {
|
||||
"tauri": "tauri",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:build": "tauri build"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Schritt 2 — Erste Op migrieren: `computeJoins` (Wand-Eckverbindungen)
|
||||
|
||||
**Warum `computeJoins` zuerst?**
|
||||
- Läuft häufig (bei jedem Wall-Edit)
|
||||
- Klein und fokussiert (~50 Zeilen Kernlogik)
|
||||
- Proof-of-Concept für Tauri-invoke-Flow
|
||||
- Danach `kernel2d` parallel hochfahren
|
||||
|
||||
**Rust-Impl:** `src-tauri/src/geometry.rs`
|
||||
|
||||
```rust
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Vec2 { pub x: f64, pub y: f64 }
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct WallJoinInput {
|
||||
pub walls: Vec<Wall>,
|
||||
pub joints: Vec<(usize, usize)>, // wall indices
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct JoinInfo { /* … */ }
|
||||
|
||||
pub fn compute_joins(input: WallJoinInput) -> Result<Vec<JoinInfo>, Box<dyn std::error::Error>> {
|
||||
// Port der Logik aus src/model/joins.ts
|
||||
// L-Ecken, T-Stösse, +-Kreuzungen
|
||||
Ok(vec![]) // Placeholder
|
||||
}
|
||||
```
|
||||
|
||||
**TS-Fallback bleibt:** `src/model/joins.ts` (LS vor Rust-Port)
|
||||
|
||||
**Compute-Boundary:** `src/compute/index.ts`
|
||||
```typescript
|
||||
export async function computeJoins(walls: Wall[], joints: Array<[number, number]>): Promise<JoinInfo[]> {
|
||||
try {
|
||||
return await invoke<JoinInfo[]>('compute_joins', { walls, joints });
|
||||
} catch (err) {
|
||||
console.warn('Rust compute_joins failed, using TS fallback:', err);
|
||||
return joinsTS(walls, joints); // Fallback
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Schritt 3 — Migrations-Parität testen
|
||||
|
||||
**Test-Fixtures:**
|
||||
- Aus `src/model/sampleProject.ts` exportieren: Wand-Arrays mit bekannten L/T/±-Konfigurationen
|
||||
- Rust-Unit-Tests: gleiche Fixtures → gleiche JoinInfo-Outputs
|
||||
- Vergleich: `actual == expected`
|
||||
|
||||
**Beispiel (Rust):**
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_l_corner() {
|
||||
let input = WallJoinInput { /* L-shaped walls */ };
|
||||
let result = compute_joins(input).unwrap();
|
||||
assert_eq!(result[0].kind, JoinKind::LCorner);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Schritt 4 — Vite/Tauri-Integration
|
||||
|
||||
**Dev-Workflow:**
|
||||
```bash
|
||||
npm run tauri:dev
|
||||
# → Vite dev-server (localhost:5173) lädt React-App
|
||||
# → Tauri-window zeigt auf :5173
|
||||
# → invoke() ruft Rust-Commands auf
|
||||
```
|
||||
|
||||
**Build-Workflow:**
|
||||
```bash
|
||||
npm run build # Vite → dist/
|
||||
npm run tauri:build # Tauri packt dist/ + Rust-Binary
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deliverable (Proof-of-Concept)
|
||||
|
||||
**Git-Stand nach dieser Aufgabe:**
|
||||
|
||||
- [ ] `src-tauri/` Verzeichnis mit `Cargo.toml` + `src/main.rs` + `src/geometry.rs`
|
||||
- [ ] `Cargo.toml` buildet sauber (`cargo check` 0 Fehler)
|
||||
- [ ] `src/compute/index.ts` mit `computeJoins()` Schnittstelle (invoke + TS-fallback)
|
||||
- [ ] Rust `compute_joins()` implementiert, Tests pass (`cargo test`)
|
||||
- [ ] `package.json` `tauri` scripts hinzugefügt
|
||||
- [ ] **App läuft:** `npm run tauri:dev` → Tauri-Fenster öffnet, Wand-Edit triggert Rust-Op, Output identisch TS-Version
|
||||
- [ ] **Trace-Scan sauber** (kein TS/Rust-Code übrig, beide Impl. aktiv)
|
||||
- [ ] HANDOVER.md aktualisiert: `computeJoins` migriert, nächste Ops in Queue
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
# Build-Check
|
||||
cargo check # 0 Fehler
|
||||
|
||||
# Rust-Tests
|
||||
cargo test
|
||||
|
||||
# App end-to-end
|
||||
npm run tauri:dev
|
||||
# → Wand zeichnen + editieren → computeJoins() über Rust aufgerufen
|
||||
# → Plan + 3D aktualisiert wie vorher
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Nächste Ops (Priorisierung)
|
||||
|
||||
Nach `computeJoins` stabil:
|
||||
|
||||
1. **`kernel2d`** (Offset/Trim/Extend) — großer Hebel, läuft häufig
|
||||
2. **DXF/DWG-Parser** (Geometrie) — heavy, aber niedrige Frequenz
|
||||
3. **`detectRooms`** (SIA-Raumerkennung) — async, kann auf Hintergrund ziehen
|
||||
4. **`booleanOps`** (Union/Differenz/Schnitt) — Kandidat für später
|
||||
|
||||
---
|
||||
|
||||
## Offene Punkte (NICHT jetzt)
|
||||
|
||||
- **GPU-Compute (wgpu):** Erst nach CPU-Ops stabil (kernel2d, joins, parsing)
|
||||
- **WASM-Fallback:** Nur wenn Browser-Support nötig wird
|
||||
- **Flatpak-Distribution:** Nach Tauri-Shell stable + erste Ops migriert
|
||||
|
||||
---
|
||||
|
||||
## Referenzen
|
||||
|
||||
- Tauri v2 Docs: https://tauri.app/v1/guides/getting-started/prerequisites
|
||||
- Serde: https://serde.rs/
|
||||
- CONVENTIONS.md: Identifiers englisch, UI-Text via `t()`, …
|
||||
- Commit-Regel: kein AI-Attribution im Repo
|
||||
@@ -0,0 +1,151 @@
|
||||
# Oberleiste – Angleichung an DOSSIER (Umsetzungs-Spezifikation)
|
||||
|
||||
Verbindliche Vorlage für den Umbau der Top-Bar (`src/ui/TopBar.tsx`,
|
||||
`src/styles.css`, Verdrahtung in `src/App.tsx`). Quelle: das DOSSIER-Rhino-
|
||||
Plugin (`ToolbarApp.jsx`, `components/BarControls.jsx`, `TextEditorApp.jsx`).
|
||||
Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
## 0. Grundprimitive (neu, DOSSIER-konform)
|
||||
|
||||
Alle Leisten-Controls teilen dieselbe Höhe und Pillenform.
|
||||
|
||||
- `BAR_H = 22px` Basis-Höhe. Segmentierte Pillen `BAR_H + 2 = 24px`
|
||||
(`box-sizing:border-box`, 1px Rand inbegriffen).
|
||||
- Pille: `border:1px solid var(--border)`, `border-radius:999px`,
|
||||
`background:var(--input)`. Hover (interaktiv): `border-color:var(--accent-border)`,
|
||||
`background:var(--accent-dim)`.
|
||||
- Aktiver Zustand (Toggle AN / aktive Segmentzelle): `background:var(--accent)`,
|
||||
`color:#fff`.
|
||||
|
||||
### BarCombo (Pillen-Dropdown)
|
||||
Wir haben bereits `src/ui/Dropdown.tsx`. Der Dropdown-Trigger MUSS optisch der
|
||||
Pille entsprechen (Höhe 24, radius 999, obige Farben). Ein optionales Icon sitzt
|
||||
LINKS **ausserhalb** der Pille (18px breit, `var(--muted)`), ein optionaler
|
||||
Zahnrad-Knopf („settings") sitzt rechts **innerhalb** der Pille. Prüfen, ob
|
||||
`Dropdown` bereits so aussieht; falls nicht → Trigger-CSS angleichen (Klasse
|
||||
`tb-dd-trigger`). KEINE zweite Dropdown-Implementierung bauen.
|
||||
|
||||
### Segmentpille (3er/4er)
|
||||
Aussencontainer `display:inline-flex; height:24px; border:1px solid var(--border);
|
||||
border-radius:999px; overflow:hidden`. Zellen ohne eigenen Radius; interne Trenner
|
||||
über `border-left:1px solid var(--border)` (erste Zelle ohne). Aktive Zelle
|
||||
`var(--accent)`/#fff, inaktiv `var(--input)`/`var(--ink)`, Hover
|
||||
`var(--accent-dim)`/`var(--accent)`. Genutzt für: Ansichts-Icons, Zoom (%/fit/center),
|
||||
**B/I/U**, **L/C/R**.
|
||||
|
||||
### BarButton (quadratischer Icon-Knopf)
|
||||
22×22, `border-radius:999px`, sonst wie Pille. Aktiv = Akzentfüllung, Icon #fff.
|
||||
|
||||
## 1. Reihenfolge der Gruppen (links → rechts)
|
||||
|
||||
1. Marke (bestehend, unverändert).
|
||||
2. Ansichts-Gruppe (bestehend `view-grid`; Zellen auf Segmentpillen-Look bringen).
|
||||
3. Sichtbarkeits-Kombinationen (bestehend, `BarCombo`-Look).
|
||||
4. Detailgrad + Massstab (gestapelt, `BarCombo`-Look).
|
||||
5. **Massstab/Zoom-Cluster NEU** (siehe §2) — ersetzt die heutige Gruppe mit der
|
||||
DOPPELTEN Zoom-Anzeige.
|
||||
6. Darstellungsart (bestehend, `BarCombo`).
|
||||
7. **Text-Gruppe NEU** (siehe §3) — die zentrale neue Leiste.
|
||||
8. Referenzlinien / Linien-Modus (bestehend).
|
||||
9. Rechts: Layout · Ressourcen · Projektname.
|
||||
|
||||
## 2. Massstab/Zoom-Cluster (ersetzt Doppel-Zoom-Bug)
|
||||
|
||||
HEUTE FALSCH: In `TopBar.tsx` wird `tb-zoom` (Zoom %) ZWEIMAL gerendert
|
||||
(einmal im `tb-zoomstack`, einmal darunter als eigener `<span>`). Die zweite,
|
||||
lose `<span className="tb-zoom">…%</span>` ersatzlos ENTFERNEN.
|
||||
|
||||
NEUES Layout — 2×2-Raster (`display:grid; grid-template-columns:auto auto;
|
||||
gap:4px 6px; align-items:center`):
|
||||
|
||||
- **Spalte 1, beide Zeilen** (`grid-row:1 / span 2`): EINE kombinierte Stat-Pille,
|
||||
`width:70px`, Höhe `BAR_H*2+6 = 50px`, `border-radius:14px` (NICHT 999),
|
||||
`border:1px solid var(--border)`, `background:var(--input)`, Innen zwei Zeilen
|
||||
mittig, getrennt durch 1px-Linie (`var(--border)`):
|
||||
- oben: Live-Massstab `1:N` (Akzentfarbe, `var(--font-mono)`, 11px, 700)
|
||||
- unten: Zoom `NN%` (`var(--ink-2)`, mono, 11px)
|
||||
- „am Massstab" (Zoom==gewählter Massstab): Pille `background:var(--accent-dim)`,
|
||||
`border-color:var(--accent)`, Text `var(--accent)`.
|
||||
- Nicht-Plan-Ansicht: beide Werte „—".
|
||||
- **Spalte 2, Zeile 1**: Massstab-Dropdown (`BarCombo`, ~140px, mono) + Print/PDF
|
||||
bleibt separat. (Massstab-Dropdown ist der bestehende `scaleOptions`-Dropdown.)
|
||||
- **Spalte 2, Zeile 2**: Zoom-Segmentpille mit 3 Zellen — `%` (=`onZoom100`,
|
||||
Label „1:1"/100 %), `fit_screen` (=`onFit`), `center_focus_strong`
|
||||
(=`onFitSelection`). Material-Icons. Daneben ggf. Referenzlinien-BarButton.
|
||||
|
||||
Export-Knöpfe (PDF/DXF) wandern in eine eigene kleine BarButton-Reihe rechts vom
|
||||
Cluster (Icons `picture_as_pdf` / `download`) ODER bleiben Pillen — Hauptsache
|
||||
NICHT mehr Teil des Zoom-Blocks, damit der Cluster ruhig bleibt.
|
||||
|
||||
## 3. Text-Gruppe in der Oberleiste (NEU – Kern dieser Aufgabe)
|
||||
|
||||
Immer sichtbar. 3×2-Raster (`grid-template-columns:110px 130px 80px; gap:4px 6px`).
|
||||
Setzt Defaults für neuen Text UND formatiert die aktuelle Auswahl live.
|
||||
|
||||
Zeile 1:
|
||||
- **Stil-Preset** `BarCombo` (110px): Optionen aus `DEFAULT_PRESETS`
|
||||
(Titel/Untertitel/Label/Notiz) + „— Stil —".
|
||||
- **Font** `BarCombo` (130px): Systemfont-Liste (mind. Helvetica, Arial, Inter,
|
||||
Times New Roman, Georgia, Courier New). `applyMark(doc,range,'font',v)`.
|
||||
- **Grösse** `BarCombo` (80px): Presets in pt `[8,9,10,11,12,14,18,24,36,48]`
|
||||
+ „Eigene…" → Zahl-Input-Pille. `applyMark(...,'sizePt',n)`.
|
||||
|
||||
Zeile 2:
|
||||
- **B/I/U** Segmentpille (110px, Icons `format_bold`/`format_italic`/
|
||||
`format_underlined`): `toggleMark(doc,range,'bold'|'italic'|'underline')`.
|
||||
Aktiv-Zustand aus `isMarkActive(doc,range,mark)`.
|
||||
- **L/C/R** Segmentpille (130px, Icons `format_align_left`/`_center`/`_right`):
|
||||
setzt `paragraph.align` im Bereich.
|
||||
- **„+"-Text-Button** (80px, BarButton/Pille, Icon `add`, Label „Text"): startet
|
||||
das Text-Werkzeug (neues Textobjekt platzieren). Falls das Text-Annotation-
|
||||
Werkzeug noch nicht existiert, Button vorerst `disabled` mit Tooltip
|
||||
(kein stiller No-Op) — aber Verdrahtung vorbereiten.
|
||||
|
||||
**Auswahl-Bewusstsein (WICHTIG):** Prop `textTarget` (oder aus App-State): entweder
|
||||
`null` (nichts Text-artiges selektiert → Controls setzen nur Defaults, Ränder
|
||||
normal) ODER `{ doc: RichTextDoc, range: TextRange|null, apply: (doc)=>void }`
|
||||
für den aktuell selektierten Raumstempel/Text. Ist `textTarget != null`, tragen
|
||||
die Zeile-2-Pillen `border-color:var(--accent)` (Akzent-Glow), und alle Aktionen
|
||||
wirken auf `textTarget.doc` via `textTarget.apply(newDoc)`. Ohne aktive Range
|
||||
(nur Objekt selektiert, kein Editor offen) wirkt Formatierung auf das GANZE Doc.
|
||||
|
||||
Quelle der `textTarget`-Daten: der Raum-Agent exponiert Stempel-Doc + Setter
|
||||
(`setRoomStampDoc(roomId, doc)`) im App-State (siehe Peer-Absprache). App leitet
|
||||
für den selektierten Raum `{doc: room.stampDoc, range: activeStampRange,
|
||||
apply: d => setRoomStampDoc(room.id, d)}` an die Text-Gruppe.
|
||||
|
||||
## 4. Text-Inhalt bearbeiten: kleines Fenster (kein Footer)
|
||||
|
||||
Doppelklick auf einen Raumstempel/ein Textobjekt öffnet ein **schwebendes
|
||||
Dialog-Fenster** (nicht den Footer, kein Panel-Aufklappen). Umsetzung: neue
|
||||
Komponente `src/ui/TextEditorDialog.tsx` — ein zentriertes/абgesetztes Fenster
|
||||
(~560×420, `--shadow-3`, `border-radius:8px`, Titel „Text bearbeiten",
|
||||
Kopf mit Schliessen-✕), Body = der bestehende `src/text/RichTextEditor.tsx`
|
||||
(er bringt seine eigene Mini-Toolbar mit — das ist hier ok, weil es ein eigenes
|
||||
Fenster ist), Fuss = „Abbrechen" / „Übernehmen". „Übernehmen" ruft
|
||||
`setRoomStampDoc(roomId, editedDoc)`.
|
||||
|
||||
Der Doppelklick-Handler lebt in App (Plan-View/Viewport → onDoubleClick auf
|
||||
Stempel-Hit → `openTextEditor(roomId)`), NICHT im Footer. Falls der Raum-Agent
|
||||
den Footer benutzt hat: diesen Pfad entfernen und durch den Dialog ersetzen.
|
||||
|
||||
## 5. Farb-/Stil-Tokens
|
||||
|
||||
Bestehende CSS-Variablen weiterverwenden (`--panel`,`--input`,`--border`,
|
||||
`--accent`,`--accent-dim`,`--accent-border`,`--ink`,`--ink-2`,`--muted`,
|
||||
`--font-mono`,`--shadow-1..3`). KEINE neuen Farbwerte hart kodieren. Falls ein
|
||||
Token fehlt (z. B. `--accent-border`), prüfen und ggf. aus bestehenden ableiten.
|
||||
|
||||
## 6. i18n
|
||||
|
||||
Neue Keys in de.ts UND en.ts: `text.style`, `text.font`, `text.size`,
|
||||
`text.size.custom`, `text.bold/italic/underline`, `text.align.left/center/right`,
|
||||
`text.add`, `text.add.hint`, `text.editTitle`, `text.apply`, `text.cancel`,
|
||||
`text.selectedHint`. Presets-Namen über bestehende `rt.*`/Preset-Keys, sofern da.
|
||||
|
||||
## 7. Gate (Pflicht)
|
||||
|
||||
`rm -f tsconfig.tsbuildinfo && npx tsc -b` grün, `npm run build` grün, keine
|
||||
Trace-Scan (grep auf Co-Authored/Generated), Boot-Probe
|
||||
(`node scripts/probe.mjs`) ohne Konsolenfehler, Screenshot der Oberleiste.
|
||||
KEIN Commit.
|
||||
@@ -0,0 +1,581 @@
|
||||
# WebGL2 GPU-Accelerated 2D Plan Renderer — Architecture
|
||||
|
||||
## Executive Summary
|
||||
|
||||
A WebGL2 canvas renderer for PlanView's heavy geometry (polygons, lines, hatches) with CPU fallback. Geometry tessellates once per plan and caches; pan/zoom only updates a transform-matrix uniform. Screen-space stroke width via vertex shader normal expansion. Thin SVG overlay handles text, grips, snap markers, tool preview.
|
||||
|
||||
**No npm dependencies** — raw WebGL2 + TypeScript.
|
||||
|
||||
---
|
||||
|
||||
## Current State (SVG Bottleneck)
|
||||
|
||||
**PlanView.tsx** renders `Primitive[]` (polygon/line/arc/text) → SVG DOM:
|
||||
- ~2500 LOC: pan/zoom via viewBox, toScreen() scaling (1 meter = 90 viewBox units)
|
||||
- `Primitive` types (generatePlan.ts:133):
|
||||
- **polygon**: `pts: Vec2[]`, fill/stroke/strokeWidthMm, hatch (solid/insulation/diagonal/crosshatch)
|
||||
- **line**: a/b endpoints, className, weightMm, dash[], optional color
|
||||
- **arc**: center, from/to points, r, className, weightMm, dash[]
|
||||
- **text**: anchor, RichTextDoc, roomStamp metadata
|
||||
- Bottleneck: **pan/zoom re-renders entire SVG DOM** → Cairo rasterizes geometry at 144 Hz
|
||||
|
||||
**Key constants:**
|
||||
- `PX_PER_M = 90` (viewBox units per meter; Modell-Y up → SVG-Y down via negation)
|
||||
- `PAD = 60` (margin in viewBox units)
|
||||
- `mmToPx(mm) = (mm / 25.4) * dpi()` (stroke width: constant screen-px via non-scaling-stroke)
|
||||
- `ZOOM_MAX/MIN = 50/0.2` (pan/zoom bounds)
|
||||
|
||||
---
|
||||
|
||||
## Architecture: PlanRenderer (WebGL2 + Fallback SVG)
|
||||
|
||||
### Module Structure
|
||||
|
||||
```
|
||||
src/plan/
|
||||
├── PlanRenderer.ts (Main GPU/CPU dispatcher)
|
||||
├── glPlan/
|
||||
│ ├── glPlanCompile.ts (Tessellation & buffer upload)
|
||||
│ ├── glPlanShaders.ts (Vertex/fragment sources + compilation)
|
||||
│ ├── glPlanRender.ts (Draw loop: matrix uniform, state mgmt)
|
||||
│ └── glPlanTypes.ts (TypeScript interfaces for GPU data)
|
||||
└── PlanView.tsx (React wrapper, unchanged API)
|
||||
```
|
||||
|
||||
### High-Level Flow
|
||||
|
||||
```
|
||||
PlanView.tsx
|
||||
↓ [receives plan: Plan]
|
||||
↓
|
||||
PlanRenderer (new abstraction)
|
||||
↓
|
||||
├─→ GPU path [if WebGL2 available && flag=true]
|
||||
│ ├─ glPlanCompile() → upload tessellated geometry to VRAM
|
||||
│ ├─ glPlanRender() → draw with pan/zoom matrix uniform
|
||||
│ └─ [fast pan/zoom via matrix only]
|
||||
│
|
||||
└─→ Fallback: SVG [if WebGL fails || flag=false]
|
||||
└─ existing PlanView render path (toScreen + DOM)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GPU Path: Tessellation & Shaders
|
||||
|
||||
### 1. Tessellation Strategy
|
||||
|
||||
#### **Polygon** → Fans + Ear Clipping
|
||||
- **Input**: Primitive.polygon = { pts: Vec2[], fill, stroke, strokeWidthMm, hatch }
|
||||
- **Output**: Indexed triangle mesh
|
||||
- **Algorithm**: Earcut2D (existing JS library logic, inlined to avoid npm)
|
||||
- Convert polygon pts to 2D float32 array in **world space** (meters)
|
||||
- Earcut → triangle indices
|
||||
- Store: `{ vertices: Float32Array, indices: Uint32Array, color: vec4, hasHatch: bool }`
|
||||
- **Hatch rendering**: Bake hatch as texture or re-implement in fragment shader (MVP: solid fill only; hatch deferred)
|
||||
|
||||
#### **Line** → Quad Expansion (Screen-Space Width)
|
||||
- **Input**: Primitive.line = { a, b, weightMm, cls, dash?, color }
|
||||
- **Output**: Degenerate quad (2 triangles) with screen-space normal offset
|
||||
- **Strategy**:
|
||||
1. Vertex shader receives `{ pos: vec2, side: float }` (side = ±1 for left/right edge)
|
||||
2. Transform pos to clip space via matrix uniform
|
||||
3. Compute screen-space perpendicular via `dFdx/dFdy` or pre-compute normal in CPU
|
||||
4. Expand by `(weightMm / 25.4) * dpi * (screenPixelsPerClipUnit)` in clip space
|
||||
5. Fragment shader: solid color (no dash MVP; dashing deferred or CPU pre-tessellation)
|
||||
|
||||
#### **Arc** → Line Segments (Polyline → Quads)
|
||||
- **Input**: Primitive.arc = { center, from, to, r, weightMm, cls, dash }
|
||||
- **Output**: Tessellate arc to ~30 line segments (adaptive based on radius/zoom), expand each as quad
|
||||
- Fallback: SVG arc for MVP
|
||||
|
||||
#### **Text, Grips, Snap-Markers, Tool-Preview**
|
||||
- **Stays in SVG overlay** (thin, non-bottleneck)
|
||||
- Render above WebGL canvas at z-order 1
|
||||
|
||||
---
|
||||
|
||||
### 2. Shader Sources (GLSL 3.00 ES)
|
||||
|
||||
#### **Vertex Shader: Solid Fill (polygon)**
|
||||
```glsl
|
||||
#version 300 es
|
||||
precision highp float;
|
||||
|
||||
uniform mat4 viewProjection; // pan/zoom as 2×3 affine (expand to mat4)
|
||||
|
||||
layout(location=0) in vec2 position; // world-space (meters)
|
||||
layout(location=1) in vec4 color; // fill color
|
||||
|
||||
out VS_OUT {
|
||||
flat vec4 vertexColor;
|
||||
} vs_out;
|
||||
|
||||
void main() {
|
||||
vec4 clipPos = viewProjection * vec4(position, 0.0, 1.0);
|
||||
gl_Position = clipPos;
|
||||
vs_out.vertexColor = color;
|
||||
}
|
||||
```
|
||||
|
||||
#### **Vertex Shader: Screen-Space Stroked Line**
|
||||
```glsl
|
||||
#version 300 es
|
||||
precision highp float;
|
||||
|
||||
uniform mat4 viewProjection; // world → clip space
|
||||
uniform vec2 screenSize; // canvas (width, height) in pixels
|
||||
uniform float strokeWidthMm; // millimeters
|
||||
uniform float dpi; // 96 * devicePixelRatio
|
||||
|
||||
layout(location=0) in vec2 position; // world-space endpoint
|
||||
layout(location=1) in float sideFlag; // ±1.0 (left/right edge)
|
||||
layout(location=2) in vec4 lineColor; // stroke color
|
||||
|
||||
out VS_OUT {
|
||||
flat vec4 vertexColor;
|
||||
} vs_out;
|
||||
|
||||
void main() {
|
||||
vec4 clipPos = viewProjection * vec4(position, 0.0, 1.0);
|
||||
|
||||
// Convert stroke width (mm) → screen pixels
|
||||
float strokePx = (strokeWidthMm / 25.4) * dpi;
|
||||
// Convert screen pixels → normalized device coords (NDC)
|
||||
// NDC ∈ [-1,1]²; screen (0,screenSize) → NDC [-1,1]
|
||||
float strokeNdc = (strokePx / screenSize.x) * 2.0;
|
||||
|
||||
// Expand in clip space (simple; assumes aspect ≈ 1)
|
||||
vec4 expanded = clipPos + vec4(sideFlag * strokeNdc, 0.0, 0.0, 0.0);
|
||||
|
||||
gl_Position = expanded;
|
||||
vs_out.vertexColor = lineColor;
|
||||
}
|
||||
```
|
||||
|
||||
#### **Fragment Shader (both)**
|
||||
```glsl
|
||||
#version 300 es
|
||||
precision highp float;
|
||||
|
||||
in VS_OUT {
|
||||
flat vec4 vertexColor;
|
||||
} fs_in;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
void main() {
|
||||
fragColor = fs_in.vertexColor;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. GPU Data Structures (TypeScript)
|
||||
|
||||
**glPlanTypes.ts:**
|
||||
```typescript
|
||||
export interface GLGeometryBatch {
|
||||
/** Vertex buffer: interleaved (x, y, [z if 3D], ...) in world space. */
|
||||
vertexBuffer: WebGLBuffer;
|
||||
vertexCount: number;
|
||||
|
||||
/** Index buffer (triangles for fill, degenerate quads for strokes). */
|
||||
indexBuffer: WebGLBuffer;
|
||||
indexCount: number;
|
||||
|
||||
/** Vertex Array Object (VAO) binds VBO + IBO. */
|
||||
vao: WebGLVertexArrayObject;
|
||||
|
||||
/** Per-batch metadata. */
|
||||
batches: Array<{
|
||||
kind: "polygon" | "line" | "arc";
|
||||
indexStart: number;
|
||||
indexCount: number;
|
||||
color: [r: number, g: number, b: number, a: number]; // RGBA [0,1]
|
||||
hasHatch: boolean;
|
||||
hatchPattern?: "solid" | "insulation" | "diagonal" | "crosshatch";
|
||||
strokeWidthMm?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface GLPlanRenderState {
|
||||
// Pan/zoom transform: world (meters) → clip space
|
||||
viewMatrix: Matrix3 | Matrix4; // 2×3 affine
|
||||
projMatrix: Matrix4; // orthographic
|
||||
|
||||
// Viewport size & DPI for screen-space stroke width
|
||||
screenWidth: number;
|
||||
screenHeight: number;
|
||||
dpi: number;
|
||||
|
||||
// Compiled shaders
|
||||
solidFillProgram: WebGLProgram;
|
||||
strokeProgram: WebGLProgram;
|
||||
|
||||
// Geometry cache (tessellated once per plan)
|
||||
geometryBatch: GLGeometryBatch | null;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MVP API: PlanRenderer Class
|
||||
|
||||
### Interface
|
||||
|
||||
```typescript
|
||||
export class PlanRenderer {
|
||||
/**
|
||||
* Create renderer with WebGL2 context + fallback config.
|
||||
*/
|
||||
constructor(
|
||||
canvas: HTMLCanvasElement,
|
||||
options?: {
|
||||
enableGpu?: boolean; // default: true
|
||||
enableGpuFallback?: boolean; // SVG fallback if GL fails
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Compile and cache geometry from primitives.
|
||||
* Call once per plan change.
|
||||
*/
|
||||
compilePlan(plan: Plan): Promise<void>;
|
||||
|
||||
/**
|
||||
* Set pan/zoom transform matrix.
|
||||
* Call on every view change (pan, zoom, fit).
|
||||
*/
|
||||
setViewMatrix(viewBox: { x, y, w, h }, canvasSize: { w, h }): void;
|
||||
|
||||
/**
|
||||
* Render one frame: clear, draw batches, composite.
|
||||
* Called from requestAnimationFrame loop.
|
||||
*/
|
||||
render(): void;
|
||||
|
||||
/**
|
||||
* Release WebGL resources.
|
||||
*/
|
||||
dispose(): void;
|
||||
|
||||
/**
|
||||
* Query GPU availability / fallback state.
|
||||
*/
|
||||
isGpuReady(): boolean;
|
||||
isFallbackActive(): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Usage in PlanView
|
||||
|
||||
**Before** (SVG only):
|
||||
```tsx
|
||||
function PlanView({ plan, ... }) {
|
||||
return (
|
||||
<svg ref={svgRef}>
|
||||
<defs>{hatches}</defs>
|
||||
{plan.primitives.map((p, i) => <PrimitiveShape ... />)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**After** (GPU + SVG fallback):
|
||||
```tsx
|
||||
function PlanView({ plan, ... }) {
|
||||
const rendererRef = useRef<PlanRenderer | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
rendererRef.current = new PlanRenderer(canvas, { enableGpu: true });
|
||||
rendererRef.current.compilePlan(plan);
|
||||
}, [plan]);
|
||||
|
||||
useEffect(() => {
|
||||
rendererRef.current?.setViewMatrix(view, { w: canvasWidth, h: canvasHeight });
|
||||
}, [view, canvasWidth, canvasHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
const frame = () => {
|
||||
rendererRef.current?.render();
|
||||
rafId = requestAnimationFrame(frame);
|
||||
};
|
||||
rafId = requestAnimationFrame(frame);
|
||||
return () => cancelAnimationFrame(rafId);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
{/* GPU canvas (or SVG fallback if GL unavailable) */}
|
||||
<canvas ref={canvasRef} style={{ position: "absolute" }} />
|
||||
{/* Thin SVG overlay: text, grips, snap-markers, tool preview */}
|
||||
<svg ref={svgRef} style={{ position: "absolute", zIndex: 1 }}>
|
||||
{/* text, grips, snaps only; geometry stays in WebGL */}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Flow: From Primitives → GPU
|
||||
|
||||
### 1. **Compile Phase** (glPlanCompile.ts)
|
||||
|
||||
```typescript
|
||||
export function compilePlan(gl: WebGL2RenderingContext, plan: Plan): GLGeometryBatch {
|
||||
const batches: BatchInfo[] = [];
|
||||
const vertices: number[] = [];
|
||||
const indices: number[] = [];
|
||||
let indexOffset = 0;
|
||||
|
||||
for (const prim of plan.primitives) {
|
||||
if (prim.kind === "polygon") {
|
||||
const { verts, inds } = tessellatePolygon(prim.pts);
|
||||
const color = parseColor(prim.fill);
|
||||
batches.push({
|
||||
kind: "polygon",
|
||||
indexStart: indexOffset,
|
||||
indexCount: inds.length,
|
||||
color,
|
||||
hasHatch: prim.hatch.pattern !== "none",
|
||||
hatchPattern: prim.hatch.pattern,
|
||||
});
|
||||
vertices.push(...verts);
|
||||
indices.push(...inds.map((i) => i + indexOffset));
|
||||
indexOffset += verts.length / 2;
|
||||
} else if (prim.kind === "line") {
|
||||
const { verts, inds } = tessellateLineQuad(prim.a, prim.b);
|
||||
const color = parseColor(prim.color || "black");
|
||||
batches.push({
|
||||
kind: "line",
|
||||
indexStart: indexOffset,
|
||||
indexCount: inds.length,
|
||||
color,
|
||||
strokeWidthMm: prim.weightMm,
|
||||
});
|
||||
vertices.push(...verts);
|
||||
indices.push(...inds.map((i) => i + indexOffset));
|
||||
indexOffset += verts.length / 2;
|
||||
}
|
||||
// arc → polyline → quads (deferred for MVP)
|
||||
}
|
||||
|
||||
const vbo = gl.createBuffer()!;
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
|
||||
|
||||
const ibo = gl.createBuffer()!;
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indices), gl.STATIC_DRAW);
|
||||
|
||||
const vao = gl.createVertexArray()!;
|
||||
gl.bindVertexArray(vao);
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
||||
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 8, 0); // position
|
||||
gl.enableVertexAttribArray(0);
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
|
||||
|
||||
return { vertexBuffer: vbo, indexBuffer: ibo, vao, batches, vertexCount: vertices.length, indexCount: indices.length };
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **Render Phase** (glPlanRender.ts)
|
||||
|
||||
```typescript
|
||||
export function renderPlan(
|
||||
gl: WebGL2RenderingContext,
|
||||
state: GLPlanRenderState,
|
||||
batch: GLGeometryBatch
|
||||
): void {
|
||||
gl.clearColor(1, 1, 1, 1); // white background
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
gl.useProgram(state.solidFillProgram);
|
||||
const mvpLoc = gl.getUniformLocation(state.solidFillProgram, "viewProjection");
|
||||
const mvp = mat4.multiply(state.projMatrix, state.viewMatrix);
|
||||
gl.uniformMatrix4fv(mvpLoc, false, mvp);
|
||||
|
||||
gl.bindVertexArray(batch.vao);
|
||||
|
||||
for (const b of batch.batches) {
|
||||
const colorLoc = gl.getUniformLocation(state.solidFillProgram, "vertexColor");
|
||||
gl.uniform4f(colorLoc, b.color[0], b.color[1], b.color[2], b.color[3]);
|
||||
|
||||
gl.drawElements(gl.TRIANGLES, b.indexCount, gl.UNSIGNED_INT, b.indexStart * 4);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tessellation Details
|
||||
|
||||
### Earcut (Polygon Triangulation)
|
||||
|
||||
**Inlined earcut logic (no npm):**
|
||||
```typescript
|
||||
function tessellatePolygon(pts: Vec2[]): { verts: number[]; inds: number[] } {
|
||||
// Convert Vec2[] → flat float array
|
||||
const coords = pts.flatMap((p) => [p.x, p.y]);
|
||||
|
||||
// Earcut2D: robust polygon triangulation
|
||||
// → Returns index array (triplets = triangles)
|
||||
const triangles = earcut(coords);
|
||||
|
||||
// Vertex buffer: just positions (x, y) in world space (meters)
|
||||
const verts = coords;
|
||||
|
||||
return { verts, inds: triangles };
|
||||
}
|
||||
|
||||
// Simplified earcut (full version ~200 LOC; reference libtess2 or earcut.js)
|
||||
function earcut(data: number[], hole?: number[], dim?: number): number[] {
|
||||
// ... iterative ear clipping, complexity O(n²) worst-case
|
||||
// Returns Uint32Array of triangle indices
|
||||
}
|
||||
```
|
||||
|
||||
### Line Quad Expansion
|
||||
|
||||
```typescript
|
||||
function tessellateLineQuad(
|
||||
a: Vec2, b: Vec2,
|
||||
widthMm: number = 0.5
|
||||
): { verts: number[]; inds: number[] } {
|
||||
// World-space endpoints; width (mm) will be expanded in vertex shader
|
||||
|
||||
// Create a degenerate quad: 2 triangles
|
||||
// Vertices: [a_left, a_right, b_left, b_right]
|
||||
// (normal expansion happens in VS)
|
||||
|
||||
const verts = [
|
||||
a.x, a.y, 0.0, // vertex 0: a, left flag
|
||||
a.x, a.y, 1.0, // vertex 1: a, right flag
|
||||
b.x, b.y, 0.0, // vertex 2: b, left flag
|
||||
b.x, b.y, 1.0, // vertex 3: b, right flag
|
||||
];
|
||||
|
||||
// Two triangles: (0, 1, 2) and (1, 3, 2)
|
||||
const inds = [0, 1, 2, 1, 3, 2];
|
||||
|
||||
return { verts, inds };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pan/Zoom Matrix Transform
|
||||
|
||||
### View Box → Clip Space
|
||||
|
||||
```typescript
|
||||
function buildViewMatrix(
|
||||
viewBox: { x, y, w, h },
|
||||
canvasSize: { w, h }
|
||||
): Matrix4 {
|
||||
// 1. World space (meters, origin at model 0,0) → viewBox units (PX_PER_M=90)
|
||||
const scale = PX_PER_M; // 1 meter → 90 viewBox units
|
||||
|
||||
// 2. ViewBox viewport: x,y,w,h in viewBox units → NDC [-1,+1]²
|
||||
// Orthographic projection (no perspective).
|
||||
const ortho = mat4.ortho(
|
||||
viewBox.x,
|
||||
viewBox.x + viewBox.w,
|
||||
viewBox.y,
|
||||
viewBox.y + viewBox.h,
|
||||
-1, 1
|
||||
);
|
||||
|
||||
// 3. Scale from viewBox units → world (invert PX_PER_M)
|
||||
const scaleMatrix = mat4.scale(mat4.identity(), [1/scale, 1/scale, 1]);
|
||||
|
||||
return mat4.multiply(ortho, scaleMatrix);
|
||||
}
|
||||
```
|
||||
|
||||
Whenever PlanView calls `setView(viewBox)` or `onWheel()` → call `setViewMatrix()` → GPU re-renders with new matrix uniform (no tessellation).
|
||||
|
||||
---
|
||||
|
||||
## Fallback Strategy: SVG Renderer Flag
|
||||
|
||||
**Global flag** in PlanView or app state:
|
||||
```typescript
|
||||
const [useGpuRenderer, setUseGpuRenderer] = useState(true);
|
||||
```
|
||||
|
||||
**Render path branching:**
|
||||
```typescript
|
||||
return useGpuRenderer && rendererRef.current?.isGpuReady()
|
||||
? <canvas ref={canvasRef} />
|
||||
: <svg ref={svgRef}>{/* existing SVG rendering */}</svg>;
|
||||
```
|
||||
|
||||
**When GL fails** (e.g., no WebGL2 support, Out-Of-Memory):
|
||||
1. Renderer catches error in `compilePlan()`
|
||||
2. Sets internal `fallbackActive = true`
|
||||
3. Returns gracefully (app renders SVG path instead)
|
||||
4. User sees same plan, slower but functional
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order (MVP → Iteration)
|
||||
|
||||
### Phase 1: Core (Week 1)
|
||||
1. **glPlanTypes.ts** — TypeScript interfaces for GPU state
|
||||
2. **glPlanShaders.ts** — Compile vertex/fragment shaders, handle GL errors
|
||||
3. **glPlanCompile.ts** — Tessellation (earcut inlined), buffer upload
|
||||
4. **glPlanRender.ts** — Draw loop, matrix uniform, clear/present
|
||||
5. **PlanRenderer.ts** — Main class, dispatcher (GPU vs SVG fallback)
|
||||
6. **PlanView.tsx** — Wire renderer, canvas overlay, canvas lifecycle
|
||||
|
||||
### Phase 2: Hatches & Lines (Week 2)
|
||||
- Improve line tessellation: proper screen-space width (dFdx/dFdy or pre-computed normals)
|
||||
- Hatch patterns: texture-based or procedural fragment shader (diagonal/insulation)
|
||||
- Arc tessellation: polyline → quads
|
||||
|
||||
### Phase 3: Polish (Week 3)
|
||||
- Stroke dashing via geometry or fragment shader
|
||||
- Greyed opacity blending
|
||||
- Hit testing integration (point-in-triangle for GPU)
|
||||
- Performance profiling, batch merging
|
||||
|
||||
---
|
||||
|
||||
## Performance Targets
|
||||
|
||||
| Operation | SVG (Current) | GPU (Target) | Notes |
|
||||
|-----------|---------------|--------------|-------|
|
||||
| **Tessellation** | — | 10–50 ms | Once per plan |
|
||||
| **Pan/Zoom 60 Hz** | 16 ms (re-render SVG) | <1 ms (matrix uniform) | Matrix upload negligible |
|
||||
| **Pan/Zoom 144 Hz** | 7 ms (bottleneck) | <0.5 ms | 28× speedup expected |
|
||||
| **Geometry: 1000 polygons** | 50–100 ms SVG render | 1–5 ms GPU draw | CPU tessellation pipelined |
|
||||
|
||||
User: AMD RX 7800 XT → easily capable of 4K+ geometry at 144 Hz.
|
||||
|
||||
---
|
||||
|
||||
## Known Deferred Items (Post-MVP)
|
||||
|
||||
- **Hatches**: Solid fill only MVP; insulation/diagonal/crosshatch in Phase 2 via texture or procedural shader
|
||||
- **Dashing**: Not in MVP (complex with screen-space strokes); either CPU pre-tessellation or fragment shader alpha-discard
|
||||
- **Arcs**: Fallback to SVG for MVP; GPU polyline expansion in Phase 2
|
||||
- **Text, Grips, Snaps**: Stay in SVG overlay indefinitely (no GPU benefit; text rendering nontrivial)
|
||||
- **Hit Testing**: Keep in CPU/SVG for MVP; GPU pick-buffer deferred
|
||||
- **Color/Opacity Blending**: Basic for MVP; advanced (multiply, screen, dodge) deferred
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- **Earcut.js**: https://github.com/mapbox/earcut — polygon triangulation (logic to inline)
|
||||
- **three.js line expansion**: https://github.com/mrdoob/three.js/blob/master/src/renderers/webgl/WebGLGeometries.js
|
||||
- **OpenGL Perspective Division**: https://en.wikibooks.org/wiki/OpenGL_Programming/Modern_OpenGL_Tutorial_Polygon_offset
|
||||
- **Screen-Space Stroke Width**: https://forum.libcinder.org/topic/smooth-line-rendering-using-geometry-shaders
|
||||
- PlanView source: `/home/karim/cad/src/plan/PlanView.tsx` (2500 LOC)
|
||||
- Primitive types: `/home/karim/cad/src/plan/generatePlan.ts:133`
|
||||
@@ -0,0 +1,80 @@
|
||||
# Briefing — Nativer wgpu-2D-Renderer (parallele Instanz)
|
||||
|
||||
> Für den Entwickler, der den nativen GPU-Renderer baut. Isoliert vom
|
||||
> Web-Renderer; koordiniert über dieses Dokument.
|
||||
|
||||
## COMMIT-REGEL (verbindlich)
|
||||
Dieses Repo darf **keinerlei Fremd-Tool-Hinweise** enthalten — nicht im Code,
|
||||
in Kommentaren oder in der Git-Historie. Kommentare deutsch, Identifier englisch.
|
||||
**Kein Commit ohne Absprache.**
|
||||
|
||||
## Warum
|
||||
Der 2D-Plan wird aktuell in einem WebGL-Canvas gerendert (Web-Stack). In Chromium
|
||||
ist das 144-Hz-flüssig, aber **Tauris Linux-Webview = WebKitGTK bremst** (langsamer
|
||||
Compositor, vermutlich 60-Hz-rAF-Cap). Bewiesen: dieselbe App im `chromium --app`-
|
||||
Fenster = butterweich, im Tauri-Fenster = zäh — trotz imperativem Pan, CSS-transform-
|
||||
Overlay, DMABUF-Tweaks. Siehe Memo `webkitgtk-bottleneck`.
|
||||
|
||||
**Lösung:** die schwere Grafik **nativ mit wgpu** rendern (Rust), die Webview macht
|
||||
nur noch UI-Chrome (Panels/Buttons). So bleibt Tauri (natives Rust-Backend, winziges
|
||||
Bundle) UND wir umgehen den Webview-Compositor komplett.
|
||||
|
||||
## Referenz-Implementierung (NICHT wegwerfen — 1:1 portierbar)
|
||||
Der WebGL-Renderer unter `src/plan/glPlan/` ist die **arbeitende Referenz**. Die
|
||||
harte Arbeit ist dort schon gelöst und portiert konzeptuell direkt nach wgpu:
|
||||
- `glPlanCompile.ts` — **Earcut-Triangulierung** (konkav-fähig), Linien→Quads mit
|
||||
Normale+Seiten-Flag, Batch-Merging, Farb-Parsing. Unit-Tests: `glPlanCompile.test.ts`.
|
||||
- `glPlanShaders.ts` — Vertex/Fragment (GLSL 300 es). **WGSL ≈ GLSL** — direkt übersetzbar.
|
||||
- Füll-VS: Bildschirm-Position → Clip via `viewProj`-Matrix.
|
||||
- Linien-VS: bildschirmkonstante bzw. papier-mm-Breite via Normalen-Offset im Clip
|
||||
(siehe `strokeScale`/`strokePx`-Logik — Papier-mm × Massstab N × meet-Skala).
|
||||
- `glPlanRender.ts` — Ortho-Matrix (aspekt-korrekt = SVG `xMidYMid meet`), Fill-/Line-
|
||||
Pass, `computeOrthoMatrix`.
|
||||
- Koordinaten-Konvention: BILDSCHIRM-Raum `sx = mx·PX_PER_M, sy = -my·PX_PER_M`
|
||||
(`PX_PER_M=90`), Modell-Y hoch → Bildschirm-Y runter.
|
||||
- Strichbreite = **echte Papier-mm im Massstab**: `Breite_px = mm · N/1000 · PX_PER_M ·
|
||||
meetSkala` (repliziert SVG `printStrokeVb`). Am Beispiel 0.35 mm @ 1:100 = 0.35 mm.
|
||||
|
||||
Die **Daten** kommen aus `src/plan/generatePlan.ts` → `Plan.primitives` (Union-Typ
|
||||
`Primitive`: polygon/line/arc/text). Text bleibt Overlay (nicht wgpu).
|
||||
|
||||
## Die EINE harte Frage (zuerst spiken/recherchieren)
|
||||
**Wie rendert wgpu in das Tauri-Fenster neben der Webview?** Optionen recherchieren:
|
||||
1. Natives Child-Surface unter der Webview (raw-window-handle), Webview transparent
|
||||
drüber für UI-Chrome. Vermutlich der Zielweg.
|
||||
2. Eigenes wgpu-Fenster (entkoppelt) — nur für den Spike, um Rendering von der
|
||||
Fenster-Integration zu trennen.
|
||||
3. wgpu→Textur→Webview — verwirft den Zweck (Copy-Overhead), nur Notnagel.
|
||||
|
||||
**Empfehlung:** Rendering-Spike ZUERST entkoppelt (Option 2, standalone `winit`+wgpu-
|
||||
Fenster), das die `Primitive` als gefüllte Polygone + Striche zeichnet und Pan/Zoom
|
||||
per Matrix-Uniform macht. Fenster-Integration in Tauri ist der zweite, separate Schritt.
|
||||
|
||||
## Meilensteine
|
||||
- **M0 (Research):** kurzer Bericht `docs/design/wgpu-integration-findings.md` — wie
|
||||
wgpu-Surface + Tauri/webview koexistieren (raw-window-handle, Transparenz, Z-Order,
|
||||
Input-Routing). Quellen verlinken.
|
||||
- **M1 (Rendering-Spike, standalone):** neue Crate `src-tauri/render2d/` (serde-Input
|
||||
= geflachte Primitive), wgpu + WGSL:
|
||||
- Earcut-Port (oder `lyon`/`earcutr` Crate evaluieren) → Dreiecke.
|
||||
- Füll-Pipeline (gefüllte Polygone, Ortho-Matrix-Uniform).
|
||||
- Linien-Pipeline (Quad-Expansion, echte Papier-mm-Breite).
|
||||
- `cargo test` für die Tessellierung (Muster: `src-tauri/geometry` hat 4/4 Tests —
|
||||
z. B. konkaves L flächentreu, wie `glPlanCompile.test.ts`).
|
||||
- `cargo build` grün. (Visuelle Fenster-Verifikation braucht Display → an Mensch/
|
||||
Display-Session übergeben; im Bericht vermerken.)
|
||||
- **M2 (Tauri-Integration):** Surface unter die Webview, Pan/Zoom-Input aus der
|
||||
Webview an den Renderer, Parität mit dem WebGL-Pfad messen (144 Hz).
|
||||
- **M3:** Schraffur, Bögen, Auswahl-Highlight; danach 3D (three.js→wgpu).
|
||||
|
||||
## Koordination (WICHTIG)
|
||||
- **Eigener Branch** (z. B. `feature/wgpu-renderer`), NICHT auf `master`/
|
||||
`feature/parametric-walls` committen. Isolierter Worktree.
|
||||
- **Nur** `src-tauri/` + neue Rust-Module + `docs/`. **NICHT** `src/App.tsx`,
|
||||
`src/plan/*`, `types.ts` anfassen (dort laufen parallel Features).
|
||||
- Der Web-WebGL-Renderer bleibt bestehen (Browser-Lauffähigkeit + Referenz).
|
||||
- Ergebnisse/Diffs zurückliefern; Projektleitung committet.
|
||||
|
||||
## Gates
|
||||
`cargo check`/`cargo test`/`cargo build` grün. Trace-Scan sauber (COMMIT-REGEL).
|
||||
`npx tsc -b` + `npm run build` müssen unberührt grün bleiben (keine Web-Änderungen).
|
||||
@@ -0,0 +1,263 @@
|
||||
# Briefing — Nativer wgpu-3D-Renderer (M0 Design + M1 Spike)
|
||||
|
||||
> Schwester-Dokument zu `wgpu-2d-renderer-briefing.md`. Beschreibt den Weg vom
|
||||
> jetzigen three.js-3D-View (WebGL im WebKitGTK-Webview) zu einer nativen
|
||||
> wgpu-Engine (Rust). Dieses Dokument ist der ANFANG: M0 (Bestandsaufnahme +
|
||||
> Port-Plan) und M1 (entkoppelter Standalone-Spike). Noch NICHT die Migration.
|
||||
|
||||
## Commit-/Spuren-Regel
|
||||
Wie im ganzen Repo: keine Fremd-Tool-Hinweise im Code, in Kommentaren oder der Historie. Kommentare deutsch, Identifier englisch. Kein Commit ohne Absprache.
|
||||
|
||||
## Warum
|
||||
Der 3D-View laeuft heute als three.js/WebGL im Tauri-Webview (WebKitGTK). Wie beim
|
||||
2D-Plan bremst dieser Compositor unter Linux (siehe `wgpu-2d-renderer-briefing.md`
|
||||
und Memo `webkitgtk-bottleneck`). Die schwere 3D-Grafik soll daher **nativ mit wgpu**
|
||||
gerendert werden (Rust), die Webview macht nur noch UI-Chrome. Das umgeht den
|
||||
Webview-Compositor komplett und teilt sich die Toolchain mit dem 2D-Renderer
|
||||
(`src-tauri/render2d/`, gleiche Feature-Stufung, gleiche Test-Muster).
|
||||
|
||||
---
|
||||
|
||||
## 1. Bestandsaufnahme — was der three.js-View rendert
|
||||
|
||||
Referenz: `src/viewport/Viewport3D.tsx` (analysiert), plus die Geometrie-Grundlage
|
||||
in `src/model/geometry.ts`, `src/model/wall.ts`, `src/model/joins.ts`,
|
||||
`src/geometry/opening.ts`, `src/geometry/stair.ts`. Zeilenangaben beziehen sich auf
|
||||
den Stand der Analyse.
|
||||
|
||||
### Koordinaten-Konvention (verbindlich)
|
||||
Das Modell ist 2D in Metern (`x`, `y`) plus Hoehe `z`. Die 3D-Welt ist **Y-up**:
|
||||
|
||||
```
|
||||
world.x = model.x
|
||||
world.y = Hoehe (z)
|
||||
world.z = model.y
|
||||
```
|
||||
|
||||
D.h. **der Grundriss liegt in der XZ-Ebene, die Extrusion laeuft entlang +Y.**
|
||||
Belegt u.a. in `Viewport3D.tsx`:
|
||||
- Kommentar (~Z. 473): „Modell (x,y,z) → Three (x, z, y) (Z = Hoehe nach oben)".
|
||||
- Kontext-Mesh (~Z. 1679–1684): `verts[i]=pos.x; verts[i+1]=pos.z (Hoehe); verts[i+2]=pos.y`.
|
||||
- Wand-Griffe (~Z. 764): `new THREE.Vector3(wall.start.x, zBottom, wall.start.y)`.
|
||||
- Workplane-Raycast (~Z. 627): `{ x: hit.x, y: hit.z }` (Three → Modell).
|
||||
|
||||
Diese Konvention ist in `render3d` 1:1 uebernommen (`types.rs`, `mesh.rs`).
|
||||
|
||||
### Waende (der Kern)
|
||||
- Funktion `addWallMeshes()` / `addLayerPrism()` (~Z. 1939–2039, 2237–2271).
|
||||
- **Mesh-Weg:** `THREE.ExtrudeGeometry` (~Z. 2248) ueber die 2D-Bandform, die
|
||||
`clippedBand(p1, p2, offA, offB, startCut, endCut)` liefert (~Z. 2237; Funktion in
|
||||
`src/model/geometry.ts:87`). Extrudiert wird um `depth = zTop - zBottom`.
|
||||
- Die Bandform kommt aus Achse + Dicke: `wallBand`/`wallCorners`
|
||||
(`geometry.ts:53`/`:70`) versetzen die Achse um `thickness/2` entlang der
|
||||
**Links-Normale** `leftNormal(u) = (-u.y, u.x)` (`geometry.ts:17`), CCW-Umlauf.
|
||||
- `ExtrudeGeometry` liegt in der XY-Ebene und waechst entlang +Z; three.js dreht das
|
||||
Prisma daher um +90 Grad um X und setzt es auf `topY` (~Z. 2266–2271). In wgpu
|
||||
extrudieren wir direkt in world (XZ-Grundriss, +Y-Hoehe) und sparen die Drehung.
|
||||
- **Hoehe/Basis:** `wallVerticalExtent(project, wall)` (`wall.ts:56`) liefert
|
||||
absolute `zBottom`/`zTop` (aus `wall.bottom`/`wall.top`-Ankern bzw. Geschoss-
|
||||
`baseElevation + wall.height`).
|
||||
- **Mehrschichtig:** je `wt.layers`-Schicht ein eigenes Prisma mit Dicken-Offset
|
||||
(~Z. 1991–2015). M1 extrudiert vereinfacht EINE Schicht (Gesamtdicke).
|
||||
- **Ecken/Gehrung:** `computeJoins()` (`joins.ts:45`) berechnet Schnittlinien
|
||||
(`startCut`/`endCut`), die `clippedBand` an L-Ecken auf Gehrung zieht
|
||||
(`miterLine`, `joins.ts:101`). M1 laesst das noch weg (stumpfe Enden).
|
||||
|
||||
### Oeffnungen (Fenster/Tueren)
|
||||
- `addOpeningMeshes()` (~Z. 2056–2169) + Segmentierung in `addWallMeshes` (~Z.
|
||||
1968–2035). **Kein CSG/Boolean:** die Wand wird entlang der Achse in Segmente
|
||||
zerlegt (`openingInterval`, `geometry/opening.ts:31`), und je Oeffnung entstehen
|
||||
bis zu drei Prismen: Wand DAVOR, **Bruestung** unter dem Fenster (`sillRel`),
|
||||
**Sturz** ueber der Oeffnung (`headRel`). Rahmen/Fluegel als `BoxGeometry`;
|
||||
Glas semitransparent, Tuerfluegel um `swingAngle` gedreht.
|
||||
|
||||
### Treppen
|
||||
- `addStairMeshes()` (~Z. 2357–2409). `stairGeometry()` (`geometry/stair.ts`)
|
||||
liefert Trittflaechen (Footprint + Steig-Hoehe) + optionalen Podest-Umriss; jede
|
||||
Stufe als extrudierter Block (`ExtrudeGeometry`), Hoehe = `stairVerticalExtent`.
|
||||
|
||||
### Decken/Platten
|
||||
- `addCeilingMesh()` (~Z. 2290–2347). `ceiling.outline` als `ExtrudeGeometry`, Tiefe
|
||||
= Deckenstaerke, waechst nach unten von `zTop` (`ceilingVerticalExtent`, `wall.ts:80`).
|
||||
|
||||
### Raeume
|
||||
- Nicht als eigenstaendige 3D-Koerper gerendert (2D-Grundriss-Repraesentation).
|
||||
|
||||
### Kontext/Gelaende
|
||||
- `buildContext()` (~Z. 1651–1718). Terrain/importierte Meshes als rohe
|
||||
`BufferGeometry` (Positions/Indices, Koordinaten-Swap wie oben); Hoehenlinien als
|
||||
`LineSegments`. Dazu ein `GridHelper` (~Z. 421) auf OKFF-Hoehe.
|
||||
|
||||
### Materialien
|
||||
- `MeshLambertMaterial` (Waende/Oeffnungen/Treppen, per Komponente eingefaerbt,
|
||||
~Z. 2176–2206), `MeshStandardMaterial` (Weiss-/Textur-Modus + Terrain, PBR:
|
||||
`roughness`/`metalness`/`aoMap`, ~Z. 445–491, 2001–2015), `MeshBasicMaterial`
|
||||
(Hidden-Line-Flaechen + immer-oben-Marker), `LineBasicMaterial` (Kanten/2D-
|
||||
Zeichnungen). Render-Modi: shaded / white / textured / wireframe / hidden-line.
|
||||
|
||||
### Beleuchtung
|
||||
- `AmbientLight(0xffffff, 0.6)` (~Z. 416) + `DirectionalLight(0xffffff, 1.1)` bei
|
||||
`(6, 12, 4)` (~Z. 417). **Keine Schatten** konfiguriert. Keine Hemisphere/Point-
|
||||
Lights.
|
||||
|
||||
### Kamera + Presets
|
||||
- Zwei Kameras: `PerspectiveCamera(fov, 1, 0.1, 1000)` (~Z. 367) und
|
||||
`OrthographicCamera(-1,1,1,-1, 0.1, 5000)` (~Z. 374). `applyView3d()` (~Z.
|
||||
1566–1633) setzt fuenf Presets:
|
||||
- **front** — Richtung `(0,0,1)`, orthografisch.
|
||||
- **side** — Richtung `(1,0,0)`, orthografisch.
|
||||
- **top** — Richtung `(0,1,~0)`, orthografisch (Rotation gesperrt).
|
||||
- **iso** — Richtung `(1,1,1)` normiert, orthografisch.
|
||||
- **perspective** — Richtung `(0.62,0.5,0.7)` normiert, perspektivisch.
|
||||
- Umschalten perspektiv/ortho ueber `active = perspective ? camera : orthoCamera`
|
||||
(~Z. 1602); Ortho-Frustum aus den Modell-Bounds (`updateOrthoFrustum`, ~Z. 1522).
|
||||
- **OrbitControls** (~Z. 391–413): Mitteltaste orbit, Shift+Mitte pan, Rad zoom
|
||||
(linke/rechte Taste fuer Auswahl/Kontextmenue umgewidmet).
|
||||
|
||||
### Griffe / Gizmos
|
||||
- Editier-Griffe (`SphereGeometry`, ~Z. 711–814): Endpunkt (orange), Hoehe (blau),
|
||||
Verschieben (gruen); `depthTest:false` (immer sichtbar). Drag ueber Workplane-
|
||||
Raycast. Fuer den nativen Renderer spaeter relevant (eigener Overlay-Pass).
|
||||
|
||||
### Schnittebene
|
||||
- **Nicht implementiert:** keine `renderer.clippingPlanes` / `localClippingEnabled`.
|
||||
Schnitte laufen aktuell 2D. Fuer wgpu ein eigenständiger spaeterer Milestone
|
||||
(Clip-Distances im Shader oder Stencil-Capping).
|
||||
|
||||
### Tiefe / Culling
|
||||
- Tiefentest three.js-Standard aktiv. Backface-Culling per Default (Ausnahme:
|
||||
Terrain/Import `DoubleSide`). Diverse Overlays mit `depthTest:false`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Port-Plan nach wgpu
|
||||
|
||||
### Datenfluss
|
||||
Web-Modell → **geflachte Eingabe** (`WallInput`, spaeter Oeffnungen/Treppen/Decken)
|
||||
→ `render3d`-Mesh-Erzeugung → GPU-Buffers → Draw. Analog zum 2D-Pfad (`Scene` →
|
||||
Tessellierung → Buffers). Die Eingabe ist bewusst serde-only und GPU-frei, damit die
|
||||
Mesh-Logik headless testbar bleibt.
|
||||
|
||||
### Mesh-Erzeugung (Waende extrudieren)
|
||||
- Band aus Achse + Dicke ueber die Links-Normale (`(-u.y, u.x) * thickness/2`,
|
||||
CCW), exakt wie `wallCorners`. Extrusion in world: XZ-Grundriss, +Y von
|
||||
`base_elevation` bis `+height`.
|
||||
- Ein Quader = 6 Seiten, je eigene Vertices mit Flaechen-Normale (flaches Shading,
|
||||
korrektes Backface-Culling). 24 Vertices / 36 Indizes je Wand.
|
||||
- Spaeter: mehrschichtige Waende (je Schicht ein Prisma), Gehrung
|
||||
(`computeJoins`/`clippedBand`-Port), Oeffnungs-Segmentierung (Bruestung/Sturz).
|
||||
|
||||
### Kamera (View/Projektion, Presets)
|
||||
- `look_at` (right-handed, Kamera blickt entlang -Z im View-Raum), `perspective`
|
||||
und `orthographic` — beide auf **Clip-Z in [0,1]** (wgpu-Konvention, NICHT [-1,1]).
|
||||
- Fuenf Presets (`preset_camera`): front/top/side orthografisch achsparallel,
|
||||
iso/persp perspektivisch. `top` mit up=-Z, damit Modell-Y im Bild nach unten
|
||||
zeigt (wie die 2D-Sicht).
|
||||
- Orbit-Kamera aus Yaw/Pitch/Distanz (`orbit_eye`, Pitch geklemmt gegen Pol-Flip).
|
||||
|
||||
### Beleuchtung
|
||||
- Zunaechst EIN Directional-Light (Richtung ZUM Licht) + ambienter Sockel im
|
||||
Fragment-Shader (WGSL) — das GPU-Aequivalent zu `AmbientLight(0.6)` +
|
||||
`DirectionalLight(1.1)@(6,12,4)`. Diffuses Lambert. PBR (Rauheit/Metallik/
|
||||
Texturen/AO) spaeter.
|
||||
|
||||
### Tiefenpuffer + Culling
|
||||
- `Depth32Float`-Attachment, `depth_compare = Less`, `depth_write = true`.
|
||||
- `front_face = Ccw`, `cull_mode = Back` (die Extrusion liefert konsistent nach
|
||||
aussen zeigende CCW-Flaechen).
|
||||
|
||||
### Matrix-Mathematik
|
||||
- Handgerechnet (kein `glam`) in der serde-only Schicht — begruendet in `math.rs`:
|
||||
die Standard-Schicht soll wie in render2d ohne Zusatz-Crates headless test-/baubar
|
||||
bleiben; der Umfang (perspective/ortho/look_at + Orbit) ist klein und exakt
|
||||
testbar. Ein spaeterer Wechsel zu `glam` (nur in der GPU-Schicht) bleibt moeglich,
|
||||
ohne die Kamera-Tests anzufassen. Alles spalten-major, direkt als Uniform ladbar.
|
||||
|
||||
### Milestones M2..Mn
|
||||
- **M2 — Mehrschichtige Waende + Gehrung:** Port von `computeJoins`/`miterLine` +
|
||||
`clippedBand` → gehrte Bandformen je Schicht; Farben/Materialien je Komponente.
|
||||
- **M3 — Oeffnungen:** Achsen-Segmentierung (Bruestung/Sturz) + Rahmen/Glas/Fluegel
|
||||
als eigene Meshes; Tuerschwenk-Winkel.
|
||||
- **M4 — Treppen + Decken:** Port von `stairGeometry`/`ceilingVerticalExtent`.
|
||||
- **M5 — Kontext/Gelaende:** rohe Terrain-/Import-Meshes + Hoehenlinien + Grid.
|
||||
- **M6 — Materialien (PBR):** `MeshStandardMaterial`-Aequivalent (roughness/
|
||||
metalness/albedo/AO-Textur); Render-Modi shaded/white/textured/wireframe/hidden.
|
||||
- **M7 — Schnittebene:** Clip-Distances im Shader oder Stencil-Capping (Feature, das
|
||||
der three.js-View gar nicht hat — echter Mehrwert).
|
||||
- **M8 — Griffe/Gizmos + Picking:** Overlay-Pass (immer-oben) + GPU-/Ray-Picking.
|
||||
- **M9 — Tauri-Integration:** Surface unter der Webview (raw-window-handle,
|
||||
Z-Order, Input-Routing) — siehe `wgpu-2d-renderer-briefing.md` M2 (gleiches
|
||||
Integrations-Problem; einmal loesen, fuer 2D+3D nutzen). Kamera-Presets/Orbit-
|
||||
Input aus der Webview an den Renderer.
|
||||
|
||||
---
|
||||
|
||||
## 3. Was in `src-tauri/render3d/` steht (M1)
|
||||
|
||||
Neue, eigenstaendige Crate (eigener leerer `[workspace]`-Block, wie render2d), damit
|
||||
`cargo test`/`build` unabhaengig vom Tauri-Workspace laufen. Feature-Stufung 1:1 wie
|
||||
render2d:
|
||||
|
||||
- `Cargo.toml` — Features `default` (serde-only) / `render` (wgpu) / `window`
|
||||
(winit-Spike). `[[bin]] spike3d` mit `required-features = ["window"]`.
|
||||
- `src/types.rs` — serde-only Eingabe: `WallInput { start, end, thickness, height,
|
||||
base_elevation, color }`, `Camera` (+ `Projection`), `CameraPreset`; Ausgabe
|
||||
`Mesh` (interleaved `[pos.xyz, normal.xyz, color.rgb]` + Indizes) mit
|
||||
`vertex_count`/`triangle_count`/`bounds`. Koordinaten-Konvention dokumentiert.
|
||||
- `src/mesh.rs` — Wand-Extrusion: `extrude_wall`/`build_walls_mesh`. Band ueber
|
||||
Links-Normale, Quader mit sechs eigenen Seiten, nach aussen zeigende Normalen.
|
||||
- `src/math.rs` — `Mat4` (spalten-major), `perspective`/`orthographic` (Clip-Z
|
||||
[0,1]), `look_at`, `view_projection`, `orbit_eye`, `preset_camera` (fuenf Presets).
|
||||
- `src/shaders.rs` — WGSL (`MESH_WGSL`): View-Projektion-Uniform + Directional-
|
||||
Light + ambienter Sockel im Fragment-Shader.
|
||||
- `src/gpu.rs` (Feature `render`) — `Renderer`: eine Pipeline mit Tiefenpuffer,
|
||||
View-Projektions-Uniform, Backface-Culling. `upload_walls` → GPU-Buffers,
|
||||
`render(camera, viewport)`.
|
||||
- `src/bin/spike3d.rs` (Feature `window`) — winit-Fenster mit Demo-Raum (5
|
||||
extrudierte Waende) + **Orbit-Kamera** (linke Maustaste dreht Yaw/Pitch, Rad
|
||||
zoomt Abstand). Matrix-getrieben, kein Re-Meshing beim Kamera-Wechsel.
|
||||
- `src/lib.rs` — Modul-Deklarationen, Re-Exports, Tests.
|
||||
|
||||
### Tests (`cargo test`, default-Feature)
|
||||
Muster wie render2d/`glPlanCompile.test.ts`:
|
||||
- Quader-Zaehlung (eine Wand → 24 Vertices / 36 Indizes / 12 Dreiecke).
|
||||
- Mehrere Waende addieren sich.
|
||||
- Bounding-Box deckt Laenge/Dicke/Hoehe ab; `base_elevation` verschiebt in Y.
|
||||
- Deckel-Normale = +Y; **alle Mantel-Normalen zeigen nach aussen** (Dot mit
|
||||
„Vertex − Zentrum" ≥ 0 → Backface-Culling korrekt).
|
||||
- Diagonale Wand; degenerierte Wand (Start==Ende) erzeugt nichts (kein Absturz).
|
||||
- Kamera: `look_at` setzt Ziel auf view-z=-dist; Perspektive klemmt z in [0,1];
|
||||
`orbit_eye` haelt den Abstand; Presets setzen die richtige Projektionsart.
|
||||
- Mit `--features render`: WGSL headless via `naga` (Parser + Validator) validiert.
|
||||
|
||||
---
|
||||
|
||||
## 4. Build-/Test-Ergebnis
|
||||
|
||||
Alle Gates gruen (Toolchain: cargo 1.96, wgpu 22, winit 0.30):
|
||||
- `cargo test` (default) — **12/12** gruen (Mesh + Kamera).
|
||||
- `cargo test --features render` — **13/13** gruen (inkl. WGSL-naga-Validierung).
|
||||
- `cargo build` (default), `--features render`, `--features window` — je gruen,
|
||||
**keine Warnungen**.
|
||||
- Trace-Scan sauber (keine KI-Spuren).
|
||||
- Web-Gates unberuehrt (nur `src-tauri/` + `docs/` angefasst; `src/` nur gelesen).
|
||||
|
||||
**Visuelle Fenster-Verifikation** ist headless NICHT moeglich. Auf einer aktiven
|
||||
Display-Session pruefbar mit:
|
||||
|
||||
```
|
||||
cargo run --features window --bin spike3d
|
||||
```
|
||||
|
||||
Erwartet: ein Raum aus extrudierten Waenden mit diffuser Beleuchtung; linke
|
||||
Maustaste dreht die Orbit-Kamera, das Rad zoomt.
|
||||
|
||||
---
|
||||
|
||||
## 5. Naechste Schritte
|
||||
1. **M2** starten: `computeJoins`/`clippedBand`-Port für gehrte, mehrschichtige
|
||||
Waende (die Bandmath ist im Web bereits verifiziert — gleiche Tests portieren).
|
||||
2. Oeffnungs-Segmentierung (M3) auf demselben Extrusions-Kern.
|
||||
3. Die **Tauri-Integration (M9)** gemeinsam mit dem 2D-Renderer loesen (ein Surface-
|
||||
Unterbau, ein Input-Routing) — das ist der eigentliche Engpass, nicht das
|
||||
Rendering. Erst standalone spiken (dieser Stand), dann unter die Webview.
|
||||
@@ -0,0 +1,124 @@
|
||||
# Welle C — HLR-Spike: Feasibility-Report
|
||||
|
||||
> 2D-Vektor-Schnitte/-Ansichten aus dem 3D-Modell via Hidden-Line-Removal (HLR)
|
||||
> mit opencascade.js (OCCT-WASM). Isolierter De-Risking-Spike, NICHT in die App
|
||||
> verdrahtet. Einheiten: Meter.
|
||||
|
||||
## Ergebnis (kurz)
|
||||
|
||||
**HLR funktioniert in diesem Browser-Projekt.** opencascade.js initialisiert im
|
||||
Vite-Build, und der HLR-Lauf liefert korrekte, getrennte sichtbare/verdeckte
|
||||
2D-Kanten. Verifiziert im echten Browser (Chromium via Vite-Dev-Server) an einer
|
||||
L-Wand + Bodenplatte, Front-Ansicht:
|
||||
|
||||
| Messwert | Wert |
|
||||
|---|---|
|
||||
| Sichtbare Kanten (sharp) | **9** |
|
||||
| Verdeckte Kanten | **16** |
|
||||
| Reine HLR-Rechenzeit | **~21 ms** |
|
||||
| WASM-Größe (unkomprimiert) | **62.8 MB** (65 864 037 Bytes) |
|
||||
| WASM-Größe (gzip) | **~19.6 MB** |
|
||||
| WASM-Fetch (lokal, Dev) | ~113 ms |
|
||||
| Modul-Init (Emscripten instanziieren) | ~450 ms (Browser) / ~680 ms (Node) |
|
||||
|
||||
Beweis-Artefakte: `hlr-elevation-proof.svg` / `.png` in diesem Ordner — sichtbare
|
||||
Kanten durchgezogen, verdeckte gestrichelt. Die Zeichnung liest sich als korrekte
|
||||
Hidden-Line-Ansicht (Silhouette + sichtbare Front-Kanten voll, verdeckte hinten
|
||||
gestrichelt).
|
||||
|
||||
## Der genutzte API-Pfad (wichtig!)
|
||||
|
||||
Der geplante klassische Pfad (`HLRBRep_Algo`/`HLRBRep_PolyAlgo` +
|
||||
`HLRBRep_HLRToShape`/`HLRBRep_PolyHLRToShape`) ist im **prebuilt Vollbuild
|
||||
1.1.1 NICHT verfügbar**: diese Klassen sind nur als `Handle_…`-Smart-Pointer
|
||||
gebunden, ohne konstruierbare Roh-Klasse und ohne den `HLRToShape`-Extraktor.
|
||||
Ein direkter Aufbau darüber ist damit nicht möglich.
|
||||
|
||||
**Verwendeter, funktionierender Pfad:** `HLRAppli_ReflectLines` — der High-Level-
|
||||
OCCT-Wrapper, der intern GENAU den exakten HLR-Algorithmus (`HLRBRep_Algo`)
|
||||
fährt. Er ist als Klasse gebunden und liefert getrennt sichtbare/verdeckte
|
||||
Kanten nach Kanten-Typ:
|
||||
|
||||
```
|
||||
const rl = new oc.HLRAppli_ReflectLines(shape);
|
||||
rl.SetAxes(dirX, dirY, dirZ, atX, atY, atZ, upX, upY, upZ); // Ortho-Projektion
|
||||
rl.Perform();
|
||||
const T = oc.HLRBRep_TypeOfResultingEdge;
|
||||
// (typ, visible, in3d) → in3d=false liefert 2D-projizierte Kanten (Z≈0)
|
||||
const visSharp = rl.GetCompoundOf3dEdges(T.HLRBRep_Sharp, true, false);
|
||||
const visOutline = rl.GetCompoundOf3dEdges(T.HLRBRep_OutLine, true, false);
|
||||
const hidSharp = rl.GetCompoundOf3dEdges(T.HLRBRep_Sharp, false, false);
|
||||
```
|
||||
|
||||
Kanten-Extraktion: `TopExp_Explorer_2(compound, TopAbs_EDGE, TopAbs_SHAPE)` →
|
||||
`TopoDS.Edge_1(current)` → `new BRepAdaptor_Curve_2(edge)` →
|
||||
`FirstParameter/LastParameter/Value(u)` (`gp_Pnt` mit `.X() .Y() .Z()`).
|
||||
|
||||
**embind-Überladungen sind versioniert** (`_1`, `_2`, …) und die Nummerierung im
|
||||
Vollbuild folgt NICHT der Argumentzahl. Empirisch verifiziert:
|
||||
`BRepPrimAPI_MakeBox_1(dx,dy,dz)`, `BRepPrimAPI_MakeBox_3(gp_Pnt, gp_Pnt)`,
|
||||
`BRepAlgoAPI_Fuse_3(a, b)`, `gp_Pnt_3(x,y,z)`. Bei einem Versionswechsel des
|
||||
Pakets müssen diese Suffixe neu geprüft werden (Fehlermeldung nennt die
|
||||
erwartete Parameterzahl).
|
||||
|
||||
## Vite-/package.json-Änderungen (genau)
|
||||
|
||||
- `package.json`: Dependency `opencascade.js@^1.1.1` (Vollbuild).
|
||||
- `vite.config.ts` (additiv, analog zum bestehenden LibreDWG-Muster):
|
||||
- Zwei Aliase → virtuelle Module auf die echten Paket-Dateien:
|
||||
`virtual:occt-glue` → `dist/opencascade.wasm.js`,
|
||||
`virtual:occt-wasm-url` → `dist/opencascade.wasm.wasm?url`.
|
||||
- `optimizeDeps.exclude: ["opencascade.js"]` (esbuild kommt mit dem
|
||||
UMD-Wrapper + Node-Shims der Glue nicht klar → Vor-Bündeln ausschließen).
|
||||
- `src/section/occt-wasm.d.ts`: Ambient-Stubs für die zwei virtuellen Module.
|
||||
- Geladen wird per **dynamischem Import** in `src/section/occt.ts` → die 62-MB-
|
||||
WASM landet NICHT im Haupt-Bundle, sondern als separater Lazy-Chunk + Asset.
|
||||
|
||||
Verifiziert: `vite build` der App bleibt grün und unverändert groß (kein
|
||||
OCCT-Chunk, da der Spike nicht verdrahtet ist). Ein Lib-Build, der `hlr.ts`
|
||||
referenziert, splittet OCCT sauber in einen eigenen Glue-Chunk + WASM-Asset ab.
|
||||
`tsc` ist für die neuen Dateien grün. (`npm run build` schlägt aktuell in
|
||||
`tsc -b` fehl — ausschließlich wegen fehlender i18n-Keys in den parallel
|
||||
bearbeiteten Dateien `commands/cmds/stair.ts` / `state/*`, NICHT wegen dieses
|
||||
Spikes.)
|
||||
|
||||
## Dateien dieses Spikes
|
||||
|
||||
- `src/section/hlr.ts` — Kernmodul: Box-Specs → Fuse → HLR → 2D-Polylinien
|
||||
(`hlrFromBoxes`, `hlrShape`, `VIEWS`, `hlrToSvg`).
|
||||
- `src/section/occt.ts` — Lazy-Loader + schmale Typ-Fassade + Lade-Metriken.
|
||||
- `src/section/occt-wasm.d.ts` — Ambient-Deklarationen der virtuellen Module.
|
||||
- `docs/welle-c-hlr-spike/` — dieser Report + Proof-SVG/-PNG.
|
||||
|
||||
## Bewertung für Welle C
|
||||
|
||||
**Viabel.** Der exakte HLR liefert saubere, normgerechte Vektor-Kanten mit
|
||||
korrekter Sichtbarkeit — genau das, was Schnitt/Ansicht brauchen und was reines
|
||||
three.js-Kanten-Projizieren nicht robust liefert (dort fehlt echte
|
||||
Flächen-Verdeckung). Die 2D-Polylinien passen direkt in die bestehende
|
||||
SVG-Plan-Pipeline (`Pt2`/`Vec2`-kompatibel).
|
||||
|
||||
**Der Kostenpunkt ist die WASM-Größe (62.8 MB / ~19.6 MB gzip).** Für einen
|
||||
Spike akzeptabel; für Produktion zu groß, um sie eager zu laden.
|
||||
|
||||
### Empfohlener Integrations- + Größenreduktions-Plan
|
||||
|
||||
1. **Lazy laden** (bereits so gebaut): OCCT erst bei erster Schnitt-/Ansichts-
|
||||
Erzeugung dynamisch importieren; Ladezustand in der UI anzeigen. Der
|
||||
Grundriss läuft weiter ohne OCCT (parametrisch, wie bisher).
|
||||
2. **Web-Worker**: HLR im Worker fahren, damit der UI-Thread frei bleibt (die
|
||||
WASM ist groß, aber der HLR-Lauf selbst ist mit ~20 ms günstig).
|
||||
3. **Größe reduzieren — lohnt sich klar**: ein **Custom-Build** von
|
||||
opencascade.js (das Paket unterstützt `make.py`/Docker-Build mit einer
|
||||
Symbol-Whitelist). Für Welle C reicht ein minimaler Satz:
|
||||
`BRepPrimAPI_*` (bzw. der spätere Solid-Erzeuger), `BRepAlgoAPI_Fuse/Common`,
|
||||
`HLRAppli_ReflectLines` + `HLRBRep_TypeOfResultingEdge`, `TopExp_Explorer`,
|
||||
`TopoDS`, `BRepAdaptor_Curve`, `gp_*`. Erfahrungswerte solcher Minimal-Builds
|
||||
liegen bei **~5–15 MB WASM** statt 62 MB — Aufwand: ein reproduzierbarer
|
||||
Docker-Build in CI, der die `.wasm`/`.js` als Projekt-Asset eincheckt.
|
||||
Alternativ die **beta 2.0** prüfen (moderneres Build-System, evtl. bereits
|
||||
schlankere Module + die klassischen HLR-Klassen konstruierbar).
|
||||
4. **Fallback, falls die Größe untragbar bleibt**: three.js-basierte
|
||||
Kanten-Extraktion (`EdgesGeometry`) + eigene Sichtbarkeit via Depth-Peeling /
|
||||
GPU-Occlusion. Deutlich mehr Eigenaufwand, weniger robust bei Verschneidungen
|
||||
— daher nur zweite Wahl; OCCT-HLR bleibt der empfohlene Weg.
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,28 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.8 -0.8 5.6 3.9000000000000004" width="800">
|
||||
<rect x="-0.8" y="-0.8" width="5.6" height="3.9000000000000004" fill="#fff"/>
|
||||
<path d="M -0.3 2.6 L -0.3 2.3" fill="none" stroke="#111" stroke-width="0.02"/>
|
||||
<path d="M -0.3 2.6 L 4.3 2.6" fill="none" stroke="#111" stroke-width="0.02"/>
|
||||
<path d="M 4.3 2.6 L 4.3 2.3" fill="none" stroke="#111" stroke-width="0.02"/>
|
||||
<path d="M -0.3 2.3 L 4.3 2.3" fill="none" stroke="#111" stroke-width="0.02"/>
|
||||
<path d="M 0.2 2.3 L 0.2 -0.30000000000000027" fill="none" stroke="#111" stroke-width="0.02"/>
|
||||
<path d="M 0 -0.30000000000000027 L 0.2 -0.30000000000000027" fill="none" stroke="#111" stroke-width="0.02"/>
|
||||
<path d="M 0 2.3 L 0 -0.30000000000000027" fill="none" stroke="#111" stroke-width="0.02"/>
|
||||
<path d="M 4 2.3 L 4 -0.30000000000000027" fill="none" stroke="#111" stroke-width="0.02"/>
|
||||
<path d="M 0.2 -0.30000000000000027 L 4 -0.30000000000000027" fill="none" stroke="#111" stroke-width="0.02"/>
|
||||
<path d="M -0.3 2.6 L -0.3 2.3" fill="none" stroke="#999" stroke-width="0.02" stroke-dasharray="0.1 0.1"/>
|
||||
<path d="M -0.3 2.3 L 4.3 2.3" fill="none" stroke="#999" stroke-width="0.02" stroke-dasharray="0.1 0.1"/>
|
||||
<path d="M 0 2.3 L 0.2 2.3" fill="none" stroke="#999" stroke-width="0.02" stroke-dasharray="0.1 0.1"/>
|
||||
<path d="M 0.2 2.3 L 4 2.3" fill="none" stroke="#999" stroke-width="0.02" stroke-dasharray="0.1 0.1"/>
|
||||
<path d="M 0.2 2.3 L 4 2.3" fill="none" stroke="#999" stroke-width="0.02" stroke-dasharray="0.1 0.1"/>
|
||||
<path d="M 0 2.3 L 0.2 2.3" fill="none" stroke="#999" stroke-width="0.02" stroke-dasharray="0.1 0.1"/>
|
||||
<path d="M -0.3 2.6 L 4.3 2.6" fill="none" stroke="#999" stroke-width="0.02" stroke-dasharray="0.1 0.1"/>
|
||||
<path d="M 4.3 2.6 L 4.3 2.3" fill="none" stroke="#999" stroke-width="0.02" stroke-dasharray="0.1 0.1"/>
|
||||
<path d="M 4 2.3 L 4 -0.30000000000000027" fill="none" stroke="#999" stroke-width="0.02" stroke-dasharray="0.1 0.1"/>
|
||||
<path d="M 0.2 -0.30000000000000027 L 4 -0.30000000000000027" fill="none" stroke="#999" stroke-width="0.02" stroke-dasharray="0.1 0.1"/>
|
||||
<path d="M 0.2 -0.30000000000000027 L 0.2 2.3" fill="none" stroke="#999" stroke-width="0.02" stroke-dasharray="0.1 0.1"/>
|
||||
<path d="M 0.2 2.3 L 0.2 -0.30000000000000027" fill="none" stroke="#999" stroke-width="0.02" stroke-dasharray="0.1 0.1"/>
|
||||
<path d="M 0 -0.30000000000000027 L 0.2 -0.30000000000000027" fill="none" stroke="#999" stroke-width="0.02" stroke-dasharray="0.1 0.1"/>
|
||||
<path d="M 0 2.3 L 0 -0.30000000000000027" fill="none" stroke="#999" stroke-width="0.02" stroke-dasharray="0.1 0.1"/>
|
||||
<path d="M 0 2.3 L 0 -0.30000000000000027" fill="none" stroke="#999" stroke-width="0.02" stroke-dasharray="0.1 0.1"/>
|
||||
<path d="M 0 -0.30000000000000027 L 0.2 -0.30000000000000027" fill="none" stroke="#999" stroke-width="0.02" stroke-dasharray="0.1 0.1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,90 @@
|
||||
# M2 — nativer wgpu-2D-Renderer im Tauri-Fenster: Ansatzwahl
|
||||
|
||||
> Ziel: den bereits fluessig laufenden standalone `render2d`-Renderer aus dem
|
||||
> ECHTEN Tauri-Prozess heraus mit nativer GPU-Performance anzeigen — nicht in der
|
||||
> WebKitGTK-Webview (Perf-Flaschenhals), nicht in einem Chromium-Workaround.
|
||||
> Maschine: Linux, Wayland, AMD.
|
||||
|
||||
## Gewaehlter Ansatz: **B — separates natives Fenster (winit) im Tauri-Prozess**
|
||||
|
||||
Der Tauri-Hauptthread haelt weiterhin die GTK-Hauptschleife + das Webview-Fenster
|
||||
(HTML-Chrome). Aus dem Tauri-`setup`-Hook startet der Prozess auf einem
|
||||
Hintergrund-Thread ein **eigenes natives winit-Fenster** mit eigener wgpu-Surface,
|
||||
das die `render2d`-Demo-Szene (konkaves L + Raum + Papier-mm-Linien) rendert,
|
||||
pan-/zoombar. Renderer/Szene werden NICHT reimplementiert — es ist exakt der Code
|
||||
des verifizierten standalone `spike`-Bins (`render2d::gpu::Renderer` +
|
||||
`render2d::demo_scene`).
|
||||
|
||||
### Warum B (und was gegen A/C spricht)
|
||||
|
||||
- **Kein geteilter Wayland-Surface → keine Contention/Flicker.** winit spricht auf
|
||||
Linux DIREKT Wayland/X11 (Crates `wayland-client`/`x11rb`), es benutzt **kein
|
||||
GTK**. Das native Fenster bekommt eine eigene, von WebKitGTK voellig getrennte
|
||||
Wayland-Surface. Genau das umgeht das dokumentierte Problem aus der Vorrecherche
|
||||
(Roh-wgpu-Surface UNTER der Webview im selben Fenster → Flicker/Schwarzbild auf
|
||||
Wayland).
|
||||
- **Zwei Fenster-Stacks, aber KEIN Event-Loop-Konflikt.** tao (Tauri) fuehrt eine
|
||||
GTK-Hauptschleife auf dem Hauptthread; winit fuehrt eine eigene Schleife.
|
||||
winit 0.30 erlaubt die Event-Loop explizit auf einem Nicht-Haupt-Thread via
|
||||
`EventLoopBuilderExtWayland::with_any_thread(true)` (X11-Pendant analog). Der
|
||||
native Renderer laeuft daher auf `std::thread` „cad-native2d", der Tauri-/GTK-
|
||||
Hauptthread bleibt frei. Da winit nicht an GTK gebunden ist, kollidieren die
|
||||
beiden Schleifen nicht (getrennte Stacks, getrennte fds).
|
||||
|
||||
- **Ansatz A (wgpu-Surface im SELBEN Tauri-Fenster, Unter-/Overlay):** verworfen.
|
||||
taos `raw_window_handle_rwh_06()` liefert auf Wayland den `WaylandWindowHandle`
|
||||
der GTK-`ApplicationWindow` — also DIESELBE Surface, in die WebKitGTK
|
||||
komponiert. Eine zweite wgpu-Surface darauf ist genau die Contention, die die
|
||||
Vorrecherche als flackernd/schwarz auf diesem Setup markiert hat. Hoechstes
|
||||
Wayland-Risiko, fuer den Spike ungeeignet.
|
||||
- **Ansatz C (wgpu als Haupt-Surface, HTML nur duennes Overlay):** verworfen fuer
|
||||
den Spike. Sauberste Perf-Story, aber groesster Umbau: die ganze App muesste auf
|
||||
eine winit/tao-getriebene Haupt-Schleife umziehen und die Tauri-Webview zur
|
||||
Nebenrolle degradiert werden. Kein „minimaler realer Spike" mehr. Bleibt die
|
||||
Ziel-Endarchitektur-Option, aber nach B.
|
||||
|
||||
## Crates / Versionen (verifiziert aus der Lock-Datei)
|
||||
|
||||
| Rolle | Crate | Version |
|
||||
|---|---|---|
|
||||
| Tauri | `tauri` / `tauri-runtime-wry` | 2.11.5 / 2.11.4 |
|
||||
| Fenster (Webview-Seite) | `tao` | 0.35.3 |
|
||||
| Webview | `wry` | 0.55.1 |
|
||||
| Natives Renderfenster | `winit` | 0.30.13 |
|
||||
| GPU | `wgpu` / `wgpu-hal` | 22.1.0 / 22.0.0 |
|
||||
| Handle-Bruecke | `raw-window-handle` | **0.6.2 (identisch fuer tao UND wgpu/winit)** |
|
||||
| Renderer | `render2d` (Pfad) | 0.1.0, Feature `window` |
|
||||
|
||||
Der entscheidende Kompatibilitaets-Glueckstreffer: tao 0.35 und wgpu 22/winit 0.30
|
||||
teilen sich `raw-window-handle 0.6.2` — keine rwh-Versionsbruecke noetig. (Fuer
|
||||
Ansatz B wird der tao-Handle gar nicht gebraucht, weil das native Fenster eine
|
||||
eigene winit-Surface hat; die Versions-Parität ist aber die Voraussetzung fuer ein
|
||||
spaeteres A/Compositing, falls je gewuenscht.)
|
||||
|
||||
## Wayland-Caveats (fuer den Start auf dieser Maschine)
|
||||
|
||||
- **`WEBKIT_DISABLE_DMABUF_RENDERER=1`** bleibt fuer die Webview-Sichtbarkeit auf
|
||||
diesem Wayland/AMD-Setup noetig (Vorrecherche). Betrifft die WebKitGTK-Seite,
|
||||
nicht das wgpu-Fenster.
|
||||
- Das native winit-Fenster laeuft ueber Vulkan; GLES/EGL-Fallback wurde standalone
|
||||
ebenfalls funktionierend gesehen. `WGPU_BACKEND=gl` erzwingt bei Vulkan-Zicken
|
||||
den GL-Pfad.
|
||||
- `with_any_thread(true)` ist zwingend — ohne die Freigabe panict winit, weil es
|
||||
die Event-Loop sonst nur auf dem Hauptthread zulaesst (den haelt hier GTK).
|
||||
|
||||
## Bau-Isolierung
|
||||
|
||||
Alles hinter Cargo-Feature **`native2d`** in `cad-tauri` (opt-in). Der normale
|
||||
Build (`cargo build`, `npm run tauri:dev`) zieht weder winit noch wgpu und bleibt
|
||||
unveraendert. `render2d` bleibt ohne Tauri-/GTK-Abhaengigkeiten headless baubar
|
||||
und testbar.
|
||||
|
||||
## Naechster Schritt Richtung Vollbild-App (2D-Viewport + HTML-Chrome)
|
||||
|
||||
1. Szene aus dem echten Modell speisen: `Plan.primitives` (TS) → serde-`Scene` →
|
||||
ueber einen Tauri-`emit`/Command an den native2d-Thread (statt `demo_scene`).
|
||||
2. Pan/Zoom-State zwischen Webview-Chrome und native2d-Fenster synchronisieren
|
||||
(Tauri-Events beidseitig).
|
||||
3. Fenster-Kopplung: das native Fenster als Kind/als angedockten Bereich neben der
|
||||
Webview positionieren (Layout), spaeter ggf. Umstieg auf Ansatz C, wenn ein
|
||||
einziges Fenster gefordert ist.
|
||||
@@ -0,0 +1,42 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="576" height="420" viewBox="0 0 576.0 420.0">
|
||||
<rect x="0" y="0" width="576.0" height="420.0" fill="#ffffff"/>
|
||||
<polygon points="48.00,372.00 528.00,372.00 528.00,348.00 48.00,348.00" fill="#c9d2d6" stroke="#2b2b2b" stroke-width="1.2"/>
|
||||
<line x1="108.00" y1="348.00" x2="108.00" y2="48.00" stroke="#111111" stroke-width="1.6"/>
|
||||
<line x1="468.00" y1="348.00" x2="468.00" y2="48.00" stroke="#111111" stroke-width="1.6"/>
|
||||
<line x1="108.00" y1="348.00" x2="468.00" y2="348.00" stroke="#111111" stroke-width="1.6"/>
|
||||
<line x1="108.00" y1="48.00" x2="468.00" y2="48.00" stroke="#111111" stroke-width="1.6"/>
|
||||
<line x1="228.00" y1="252.00" x2="228.00" y2="132.00" stroke="#111111" stroke-width="1.6"/>
|
||||
<line x1="348.00" y1="252.00" x2="348.00" y2="132.00" stroke="#111111" stroke-width="1.6"/>
|
||||
<line x1="228.00" y1="252.00" x2="348.00" y2="252.00" stroke="#111111" stroke-width="1.6"/>
|
||||
<line x1="228.00" y1="132.00" x2="348.00" y2="132.00" stroke="#111111" stroke-width="1.6"/>
|
||||
<line x1="480.00" y1="348.00" x2="480.00" y2="48.00" stroke="#111111" stroke-width="1.6"/>
|
||||
<line x1="468.00" y1="348.00" x2="480.00" y2="348.00" stroke="#111111" stroke-width="1.6"/>
|
||||
<line x1="468.00" y1="48.00" x2="480.00" y2="48.00" stroke="#111111" stroke-width="1.6"/>
|
||||
<line x1="252.00" y1="252.00" x2="252.00" y2="132.00" stroke="#111111" stroke-width="1.6"/>
|
||||
<line x1="324.00" y1="252.00" x2="324.00" y2="132.00" stroke="#111111" stroke-width="1.6"/>
|
||||
<line x1="468.00" y1="348.00" x2="468.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="108.00" y1="348.00" x2="108.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="468.00" y1="348.00" x2="108.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="468.00" y1="48.00" x2="108.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="480.00" y1="348.00" x2="480.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="456.00" y1="348.00" x2="456.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="456.00" y1="348.00" x2="456.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="480.00" y1="348.00" x2="456.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="480.00" y1="48.00" x2="456.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="456.00" y1="348.00" x2="468.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="456.00" y1="48.00" x2="468.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="252.00" y1="348.00" x2="252.00" y2="252.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="252.00" y1="132.00" x2="252.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="324.00" y1="348.00" x2="324.00" y2="252.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="324.00" y1="132.00" x2="324.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="324.00" y1="348.00" x2="324.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="252.00" y1="348.00" x2="252.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="252.00" y1="348.00" x2="324.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="252.00" y1="48.00" x2="324.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="324.00" y1="348.00" x2="252.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="324.00" y1="48.00" x2="252.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="48.00" y1="372.00" x2="48.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="528.00" y1="372.00" x2="528.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="48.00" y1="372.00" x2="528.00" y2="372.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
<line x1="48.00" y1="348.00" x2="528.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -6,24 +6,47 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"tauri": "tauri",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:build": "tauri build",
|
||||
"dump:native": "node scripts/dump-native-scene.mjs",
|
||||
"shell": "scripts/chromium-shell.sh",
|
||||
"electron": "scripts/electron-shell.sh",
|
||||
"build:engine": "wasm-pack build src-tauri/render2d --release --target web --out-dir ../../src/engine/pkg --out-name render2d --no-default-features --features web",
|
||||
"build:engine3d": "wasm-pack build src-tauri/render3d --release --target web --out-dir ../../src/engine/pkg3d --out-name render3d --no-default-features --features web",
|
||||
"build:geometry": "wasm-pack build src-tauri/geometry --release --target web --out-dir ../../src/engine/pkgGeometry --out-name geometry --no-default-features --features web",
|
||||
"build:kernel2d": "wasm-pack build src-tauri/kernel2d --release --target web --out-dir ../../src/engine/pkgKernel2d --out-name kernel2d --no-default-features --features web",
|
||||
"build:dwgimport": "wasm-pack build src-tauri/dwgimport --release --target web --out-dir ../../src/engine/pkgDwgImport --out-name dwgimport --no-default-features --features web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mlightcad/libredwg-web": "^0.7.7",
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"delaunator": "^5.1.0",
|
||||
"dxf-parser": "^1.1.2",
|
||||
"jspdf": "^4.2.1",
|
||||
"jszip": "^3.10.1",
|
||||
"opencascade.js": "^1.1.1",
|
||||
"polygon-clipping": "^0.15.7",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"svg2pdf.js": "^2.7.0",
|
||||
"three": "^0.169.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.11.4",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/three": "^0.169.0",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"electron": "^43.0.0",
|
||||
"playwright": "^1.61.1",
|
||||
"puppeteer": "^25.2.1",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.8"
|
||||
"vite": "^5.4.8",
|
||||
"vitest": "^4.1.9",
|
||||
"wasm-pack": "^0.15.0"
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 620 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 679 KiB |
|
After Width: | Height: | Size: 296 KiB |
|
After Width: | Height: | Size: 644 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 481 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 686 KiB |
|
After Width: | Height: | Size: 958 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 816 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 832 KiB |
|
After Width: | Height: | Size: 836 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 686 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 772 KiB |
|
After Width: | Height: | Size: 883 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 468 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 834 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 772 KiB |
|
After Width: | Height: | Size: 335 KiB |
|
After Width: | Height: | Size: 523 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 752 KiB |
|
After Width: | Height: | Size: 720 KiB |
|
After Width: | Height: | Size: 336 KiB |
|
After Width: | Height: | Size: 714 KiB |
|
After Width: | Height: | Size: 382 KiB |
|
After Width: | Height: | Size: 720 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 972 KiB |
|
After Width: | Height: | Size: 344 KiB |
|
After Width: | Height: | Size: 809 KiB |
|
After Width: | Height: | Size: 588 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 359 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 687 KiB |
|
After Width: | Height: | Size: 565 KiB |
|
After Width: | Height: | Size: 512 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 679 KiB |
|
After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 1023 KiB |
|
After Width: | Height: | Size: 501 KiB |
|
After Width: | Height: | Size: 766 KiB |
|
After Width: | Height: | Size: 445 KiB |
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"source": "ambientCG (ambientcg.com)",
|
||||
"license": "CC0 1.0 Universal (Public Domain)",
|
||||
"resolution": "1K",
|
||||
"materials": [
|
||||
{
|
||||
"id": "Concrete048",
|
||||
"name": "Beton",
|
||||
"category": "Concrete",
|
||||
"maps": {
|
||||
"color": "/assets/materials/Concrete048/color.jpg",
|
||||
"normal": "/assets/materials/Concrete048/normal.jpg",
|
||||
"roughness": "/assets/materials/Concrete048/roughness.jpg",
|
||||
"displacement": "/assets/materials/Concrete048/displacement.jpg",
|
||||
"ao": "/assets/materials/Concrete048/ao.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Plaster001",
|
||||
"name": "Putz/Stuck",
|
||||
"category": "Plaster",
|
||||
"maps": {
|
||||
"color": "/assets/materials/Plaster001/color.jpg",
|
||||
"normal": "/assets/materials/Plaster001/normal.jpg",
|
||||
"roughness": "/assets/materials/Plaster001/roughness.jpg",
|
||||
"displacement": "/assets/materials/Plaster001/displacement.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Wood095",
|
||||
"name": "Holz-Diele",
|
||||
"category": "Wood",
|
||||
"maps": {
|
||||
"color": "/assets/materials/Wood095/color.jpg",
|
||||
"normal": "/assets/materials/Wood095/normal.jpg",
|
||||
"roughness": "/assets/materials/Wood095/roughness.jpg",
|
||||
"displacement": "/assets/materials/Wood095/displacement.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "WoodFloor051",
|
||||
"name": "Holz-Parkett",
|
||||
"category": "WoodFloor",
|
||||
"maps": {
|
||||
"color": "/assets/materials/WoodFloor051/color.jpg",
|
||||
"normal": "/assets/materials/WoodFloor051/normal.jpg",
|
||||
"roughness": "/assets/materials/WoodFloor051/roughness.jpg",
|
||||
"displacement": "/assets/materials/WoodFloor051/displacement.jpg",
|
||||
"ao": "/assets/materials/WoodFloor051/ao.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Bricks104",
|
||||
"name": "Backstein",
|
||||
"category": "Bricks",
|
||||
"maps": {
|
||||
"color": "/assets/materials/Bricks104/color.jpg",
|
||||
"normal": "/assets/materials/Bricks104/normal.jpg",
|
||||
"roughness": "/assets/materials/Bricks104/roughness.jpg",
|
||||
"displacement": "/assets/materials/Bricks104/displacement.jpg",
|
||||
"ao": "/assets/materials/Bricks104/ao.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Tiles141",
|
||||
"name": "Bodenfliesen",
|
||||
"category": "Tiles",
|
||||
"maps": {
|
||||
"color": "/assets/materials/Tiles141/color.jpg",
|
||||
"normal": "/assets/materials/Tiles141/normal.jpg",
|
||||
"roughness": "/assets/materials/Tiles141/roughness.jpg",
|
||||
"displacement": "/assets/materials/Tiles141/displacement.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Marble012",
|
||||
"name": "Naturstein/Marmor",
|
||||
"category": "Marble",
|
||||
"maps": {
|
||||
"color": "/assets/materials/Marble012/color.jpg",
|
||||
"normal": "/assets/materials/Marble012/normal.jpg",
|
||||
"roughness": "/assets/materials/Marble012/roughness.jpg",
|
||||
"displacement": "/assets/materials/Marble012/displacement.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "PavingStones150",
|
||||
"name": "Pflasterstein",
|
||||
"category": "PavingStones",
|
||||
"maps": {
|
||||
"color": "/assets/materials/PavingStones150/color.jpg",
|
||||
"normal": "/assets/materials/PavingStones150/normal.jpg",
|
||||
"roughness": "/assets/materials/PavingStones150/roughness.jpg",
|
||||
"displacement": "/assets/materials/PavingStones150/displacement.jpg",
|
||||
"ao": "/assets/materials/PavingStones150/ao.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Metal063",
|
||||
"name": "Metall",
|
||||
"category": "Metal",
|
||||
"maps": {
|
||||
"color": "/assets/materials/Metal063/color.jpg",
|
||||
"normal": "/assets/materials/Metal063/normal.jpg",
|
||||
"roughness": "/assets/materials/Metal063/roughness.jpg",
|
||||
"metalness": "/assets/materials/Metal063/metalness.jpg",
|
||||
"displacement": "/assets/materials/Metal063/displacement.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Gravel043",
|
||||
"name": "Kies/Schotter",
|
||||
"category": "Gravel",
|
||||
"maps": {
|
||||
"color": "/assets/materials/Gravel043/color.jpg",
|
||||
"normal": "/assets/materials/Gravel043/normal.jpg",
|
||||
"roughness": "/assets/materials/Gravel043/roughness.jpg",
|
||||
"displacement": "/assets/materials/Gravel043/displacement.jpg",
|
||||
"ao": "/assets/materials/Gravel043/ao.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Grass005",
|
||||
"name": "Gras",
|
||||
"category": "Grass",
|
||||
"maps": {
|
||||
"color": "/assets/materials/Grass005/color.jpg",
|
||||
"normal": "/assets/materials/Grass005/normal.jpg",
|
||||
"roughness": "/assets/materials/Grass005/roughness.jpg",
|
||||
"displacement": "/assets/materials/Grass005/displacement.jpg",
|
||||
"ao": "/assets/materials/Grass005/ao.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Ground103",
|
||||
"name": "Boden",
|
||||
"category": "Ground",
|
||||
"maps": {
|
||||
"color": "/assets/materials/Ground103/color.jpg",
|
||||
"normal": "/assets/materials/Ground103/normal.jpg",
|
||||
"roughness": "/assets/materials/Ground103/roughness.jpg",
|
||||
"displacement": "/assets/materials/Ground103/displacement.jpg",
|
||||
"ao": "/assets/materials/Ground103/ao.jpg"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The Archivo Project Authors (https://github.com/Omnibus-Type/Archivo)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2014 The DM Sans Project Authors (https://github.com/googlefonts/dm-fonts)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||