Compare commits
148 Commits
master
...
db6f3ae0a0
| Author | SHA1 | Date | |
|---|---|---|---|
| db6f3ae0a0 | |||
| 1407c68232 | |||
| dd9984d3c4 | |||
| c5a344d3f2 | |||
| cb8fae5cb4 | |||
| 03f0c402b5 | |||
| 82d354f5e7 | |||
| afd03f1808 | |||
| bf97d501be | |||
| c36a7e0d54 | |||
| 6c473558ef | |||
| 5ed3549c66 | |||
| da39f0c8d5 | |||
| fc0a76feea | |||
| e4b8df6ae8 | |||
| 32497bcfab | |||
| 18557170aa | |||
| d74dda1ab5 | |||
| 0ddf99de08 | |||
| e1698b7c3d | |||
| f2b21d1f69 | |||
| 79d1187af7 | |||
| 7dd324d55e | |||
| e42c336478 | |||
| de7a26b89e | |||
| 8bc472a272 | |||
| 2dc40cd788 | |||
| 02bd2616fc | |||
| fabf97a402 | |||
| ac0d9efc84 | |||
| 6ef57b6b6b | |||
| 85a721f03b | |||
| 7c02ea0498 | |||
| 77a5940616 | |||
| 0f0cf83a4b | |||
| 3c3746d529 | |||
| d9937e5afa | |||
| 3d3bbdf64c | |||
| 576e6ba91b | |||
| fd890a0d74 | |||
| 368a2ffcdb | |||
| 7b58745855 | |||
| dcb6ed5d31 | |||
| 3b90c4c072 | |||
| c9cdb58275 | |||
| 339202b355 | |||
| d02781e8d2 | |||
| 9695748fbc | |||
| 23d81bee7b | |||
| 7569524d81 | |||
| f22970f321 | |||
| ebed3f5311 | |||
| ae17900cb2 | |||
| 25cebbd98c | |||
| 3d1fa40402 | |||
| b4dd396768 | |||
| f0e777efc5 | |||
| 0613bb596b | |||
| f5923d302d | |||
| 87ebd91ba2 | |||
| bf3e8bf9fc | |||
| 7b65f3bce6 | |||
| a33f124931 | |||
| 989e61c261 | |||
| 5d99701024 | |||
| 940c38eb33 | |||
| a2c8fcd6de | |||
| 0c82d01cd9 | |||
| 56bbcec2a8 | |||
| b6c1a9a3c6 | |||
| b83fd81399 | |||
| 633480252e | |||
| 5b70742efa | |||
| b0db956013 | |||
| d788cbb32b | |||
| b1e7fdf8fc | |||
| 3aee2e05ee | |||
| 88cc9df0b6 | |||
| e173978135 | |||
| 6b6c18b43d | |||
| 17523d422c | |||
| 7bcf69c16c | |||
| b6d14ca2b2 | |||
| bd80eb1c59 | |||
| 3a0652a337 | |||
| 4211633e3e | |||
| ac55233c1a | |||
| 5496f8c007 | |||
| 694e666160 | |||
| 47b9fa5aa7 | |||
| f2d36632cb | |||
| bd0e7195bb | |||
| 96628c5aaa | |||
| fc0cab9ec7 | |||
| b353ed34ae | |||
| b6e902a809 | |||
| 7d2c9803d6 | |||
| 7cf867a1a0 | |||
| 0e1c69e316 | |||
| 5a10ea5404 | |||
| fd862c4a8b | |||
| 056ca2c288 | |||
| 3402e6ac96 | |||
| b4c4a2cc6b | |||
| 382771b2e7 | |||
| a8e491e534 | |||
| 76029dbf68 | |||
| 4b02e40238 | |||
| ce6bd26ff7 | |||
| 54211b1443 | |||
| c8a4188618 | |||
| 7ca5197b6b | |||
| a010584d9b | |||
| e6d061cc91 | |||
| 0161b0231d | |||
| 34317e53f4 | |||
| 27e41077b1 | |||
| a37b1c0cc9 | |||
| 4eb635cc89 | |||
| 4734c04ec0 | |||
| 2f971a54c0 | |||
| 8cfe6c2521 | |||
| 665cbce1f9 | |||
| 6eead6d493 | |||
| 7b9f72e79f | |||
| cd0f976b4f | |||
| 919bc67bfa | |||
| ec5cbf6fa0 | |||
| f1b802299f | |||
| 0337422ae6 | |||
| d39244989b | |||
| 01424c1e22 | |||
| 1811e82005 | |||
| 0a1aaedef8 | |||
| 483ad3f278 | |||
| d27622d581 | |||
| f315c80748 | |||
| bda256d3a4 | |||
| c93b168f5c | |||
| a971cd610b | |||
| 48bbcaed1e | |||
| 6d4b4e66c1 | |||
| 27bb5a42ec | |||
| 8fd8987b70 | |||
| 7b3b597abc | |||
| 4a08161e4b | |||
| 39236c65c7 | |||
| b9dc1838c7 |
@@ -1,422 +1,92 @@
|
||||
# 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 Session 3b (2026-07-04 abends) — Session hart abgebrochen (Spend-Limit), 3 Slices mitten in der Arbeit 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. Der zuständige Agent stallte kurz vor Schluss (Watchdog), aber alle Datei-Edits waren gelandet — Hauptinstanz hat 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 letzte Session endete nicht durch normales Handover, sondern weil das monatliche Spend-Limit des KI-Anbieters **mitten in laufender Arbeit** griff (mehrere Agenten liefen parallel, „arbeite die nächsten 2h selbstständig"-Direktive des Nutzers). Diese Übergabe wurde nachträglich aus dem rohen Chat-Transkript (`Edit MEMORY md Added 1.txt`, lag im Repo-Root, siehe Aufräum-Hinweis unten) 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 von einem Agenten (kleineres Modell) fertig gebaut UND bereits quantitativ verifiziert (Parallelogramm-Kantenvergleich vor/nach Orbit per Screenshot-Analyse, 225/225 Tests grün), aber **nicht mehr committet**, bevor die Session starb — der komplette Fix-Ansatz inkl. Diff-Beschreibung steht im Transkript ab „Bash Verify locked-iso file isolated". 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. Der Agent (grosses Modell) war weit in der Umsetzung (schrieb bereits in `joins.ts`), letzte sichtbare Nachricht vor dem Abbruch: „The join looks correct — W9's brick runs through and W1's near plaster is notched" (Agent wollte gerade einen finalen Bestätigungs-Screenshot holen) — dann `failed` durch API-Fehler (Spend-Limit). **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 Memory-Direktiven aus der alten Session sind ebenfalls weg** (`priority-based-joins.md`, `openings-as-booleans.md` — auf der alten Maschine im Instanz-Memory-Verzeichnis abgelegt, existieren auf diesem Gerät nicht): die in Punkt 2/3 beschriebenen Zielverhalten waren dort als dauerhafte Engine-Direktiven 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 ins Memory dieser Instanz schreiben, nicht nur einmalig in den Auftragstext — sonst geht dieselbe Information beim nächsten Abbruch wieder verloren.
|
||||
|
||||
**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` (roher Chat-Export dieser Session, lag unversioniert im Repo-Root) und die lokale `package-lock.json`-Änderung (vermutlich von einem `npm install` auf diesem Gerät) waren dirty/untracked. Die txt-Datei gehört NICHT ins Repo (Konvention: keine KI-Spuren) — außerhalb des Repos verschieben oder löschen, sobald nichts mehr daraus gebraucht wird. `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 KI-/Assistant-Spuren** im Repo: vor jedem Commit `git diff | grep -iE "claude|anthropic|opus|sonnet|generated with|co-authored|gpt|openai|assistant"` → 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.
|
||||
- **Agenten-Regeln:** Subagenten default **Sonnet** (Nutzer-Wunsch 2026-07-04); Opus nur bei intricater Geometrie. Agenten dürfen NICHT weiterdelegieren (Agent/Task-Tool verbieten — sonst Spawn-Schleifen), NICHT committen, keine Memory. Hauptinstanz verifiziert (`tsc --noEmit` clean + `vitest run`) + trace-scan, committet dann selbst, staged nur die Task-Dateien (Kollisionen: parallelen Agenten disjunkte Datei-Lanes geben; i18n exklusiv einem Agenten).
|
||||
- **Budget:** Session-Ende bei 98% — Sparflamme.
|
||||
- **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 (Worker liest zuerst PENDENZEN, arbeitet top-down, hakt ab, Rückfrage-Recht via ❓).
|
||||
|
||||
## 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,78 @@
|
||||
# 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)
|
||||
|
||||
**Worker-Instanz** (führt aus):
|
||||
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. Worker **committet nicht selbst** und delegiert nicht weiter (Agent/Task verboten) — das macht die Hauptinstanz. (Konvention HANDOVER.)
|
||||
|
||||
**Planungs-Instanz** (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)
|
||||
|
||||
- [ ] **Toolchain prüfen:** `which node npm cargo wasm-pack`. Auf dem alten Gerät fehlten sie (vermutlich Flatpak-Sandbox von VSCodium). Nutzer wechselt auf natives VSCodium → evtl. behoben. Nur falls immer noch nichts: Node LTS + Rust (rustup) + `wasm-pack` installieren, `npm install`, einmal `cargo build --manifest-path src-tauri/render3d/Cargo.toml`. **Ohne Toolchain keine Verifikation → keinen Engine-Slice blind bauen.** | Detail: HANDOVER §Nachtrag-3b, Environment-Lücke
|
||||
|
||||
## 🔧 In Arbeit
|
||||
|
||||
_(leer — Worker trägt hier ein, was er gerade anpackt)_
|
||||
|
||||
## ⚠️ Zu prüfen (evtl. schon erledigt / Status unklar)
|
||||
|
||||
- [ ] **Snap an Wand-Schichttrennlinien** (war Agent-Slice, Session 3): `git status`/`git log` prüfen — sollte `src/tools/snapping.ts` (+ ggf. `src/model/geometry.ts` + Test) geändert haben. Falls nicht gelandet: neu beauftragen (Schicht-Offsetlinien aus `addWallPoche`-Logik in generatePlan spiegeln [nur LESEN], als Snap-Targets einspeisen). | Detail: HANDOVER §IN FLIGHT Session-Ende
|
||||
|
||||
## ⏭️ Als Nächstes
|
||||
|
||||
_(Öffnungen als Boolean-Löcher ist jetzt in 🔧 In Arbeit)_
|
||||
|
||||
## 📋 Backlog (Priorität grob absteigend)
|
||||
|
||||
- [ ] **Ebene-Schraffur editierbar:** „Nach Ebene"-Schraffur ist im Resolve fertig, aber `LayerCategory.hatch` im Kategorie-Dialog (`App.tsx` ~3350, editiert nur name/color/lw) noch nicht editierbar. Kleine Folge-Arbeit.
|
||||
- [ ] **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.
|
||||
- [ ] **ResourceManager Bauteile-Tab** auf Master-Detail (wie Wandstile) — BEVOR Bauteil-Logik angefasst wird.
|
||||
- [ ] **Einstellungs-Fenster:** Auswahlrahmen Default **Yuyake #CEB188**; Snap/Endpunkt Default = ❓„aki" (s.u.); freier Picker + Accents-Presets; verdrahten in PlanView (Selektionsrahmen + Snap-Marker). Dazu Projekt-MüM-Feld.
|
||||
- [ ] **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).
|
||||
- [ ] **Wand-Schicht-Bänder in 3D** Option B (jede Materiallage als eigener extrudierter Teilquader statt repräsentativer Farbe — pro-Schicht-Farben in `RWall.layers` liegen vor).
|
||||
- [ ] **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)
|
||||
|
||||
- [ ] **„aki"** = welcher Accent für Snap/Endpunkt-Farbe? (nicht in Accents: Ajisai/Sakura/Suna/Ichigo/Yuyake/Sora/Kusa/Kori/Amagumo/Yuki; vorläufig Sora #5FA1C9)
|
||||
- [ ] 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-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`
|
||||
@@ -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,175 @@
|
||||
# HAUPTINSTANZ-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 (für Hauptinstanz)
|
||||
|
||||
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,112 @@
|
||||
# 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. Ursprünglich von einem Opus-Subagent erarbeitet, der das
|
||||
DOSSIER-Rhino-Referenzrepo (`/tmp/dossier-ref`) gegen den aktuellen Browser-Port
|
||||
abgeglichen hat — das Ergebnis lag bisher nur in einem Session-Transkript, nicht
|
||||
im Repo. 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
|
||||
AI-Spuren (grep auf Claude/Anthropic/AI/Generated/Co-Authored), 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,81 @@
|
||||
# Briefing — Nativer wgpu-2D-Renderer (parallele Instanz)
|
||||
|
||||
> Für die Instanz/den Agenten, der den nativen GPU-Renderer baut. Isoliert vom
|
||||
> Web-Renderer; koordiniert über dieses Dokument.
|
||||
|
||||
## COMMIT-REGEL (verbindlich)
|
||||
Dieses Repo darf **keinerlei Hinweise auf KI-Werkzeuge** enthalten — nicht im Code,
|
||||
in Kommentaren oder in der Git-Historie. Keine `Co-Authored-By`-Trailer, keine
|
||||
„Generated with"-Zeilen, keine Modell-/Agenten-Namen. Kommentare deutsch,
|
||||
Identifier englisch. **Kein Commit ohne Ansage der Hauptinstanz.**
|
||||
|
||||
## 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 (die Hauptinstanz arbeitet dort an Features).
|
||||
- Der Web-WebGL-Renderer bleibt bestehen (Browser-Lauffähigkeit + Referenz).
|
||||
- Ergebnisse/Diffs zurückliefern; die Hauptinstanz committet und pflegt das Memory.
|
||||
|
||||
## 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,265 @@
|
||||
# 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 Hinweise auf KI-Werkzeuge — nicht im Code, in Kommentaren
|
||||
oder in der Historie. Kommentare deutsch, Identifier englisch. Kein Commit ohne
|
||||
Ansage der Hauptinstanz.
|
||||
|
||||
## 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,44 @@
|
||||
"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"
|
||||
},
|
||||
"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.
|
||||
@@ -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.
|
||||