Compare commits
366 Commits
7b9f72e79f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e73c684c3 | |||
| 6637533162 | |||
| 3b79e49cc6 | |||
| a6c2c04736 | |||
| 956a85d93f | |||
| f95674aa09 | |||
| 2ac5c27fa7 | |||
| 68a0459d0e | |||
| 0ab0361e3d | |||
| 31b76a2f02 | |||
| 7d368786ba | |||
| a631ddaace | |||
| cc74396854 | |||
| d4575df29f | |||
| e61634c3b9 | |||
| dc241ba63e | |||
| e6a7738170 | |||
| ae47b4f024 | |||
| ee0f1dfb07 | |||
| 9705890fd2 | |||
| 35a6834072 | |||
| 7dc8f0d5c6 | |||
| a84dc7ac73 | |||
| 6b89ad5bdb | |||
| 814e9a8656 | |||
| a51fd212d3 | |||
| bd8612ee74 | |||
| d2758efc8f | |||
| 441e4a319f | |||
| 3e7ad1f4e5 | |||
| fb5ffb457c | |||
| d21af4ac5a | |||
| 4a01754b4d | |||
| ce7454039f | |||
| a5849111e3 | |||
| 3d66dc8967 | |||
| e65a6b7de1 | |||
| 6ac054ea71 | |||
| ed724be481 | |||
| 00bff27b02 | |||
| 61313777da | |||
| 7c3a6f3f35 | |||
| 0240a23a02 | |||
| e309e54af7 | |||
| 13cc6a0d6a | |||
| 3ecb8c45de | |||
| f98c962ba3 | |||
| cdae20acbe | |||
| 1c31e680c1 | |||
| d34e1cb1c1 | |||
| 5f1b38a420 | |||
| 31d7aefcb7 | |||
| bc2778562c | |||
| 06803206e5 | |||
| 38a4d8ca3a | |||
| ee9aeda01e | |||
| a612ae55cd | |||
| c02a02cffe | |||
| 983d061a0c | |||
| 1945fecd11 | |||
| 4c4c9907af | |||
| b96714641a | |||
| 242b850beb | |||
| 7898a0d158 | |||
| 8f85e135ee | |||
| 038054fee6 | |||
| 3980a06a6c | |||
| b86fca2c96 | |||
| fa59c0a9dc | |||
| a6db682e9d | |||
| d43e2c151e | |||
| ef7eb295d9 | |||
| 456ecc5445 | |||
| 975ce3f745 | |||
| 4b0f23d123 | |||
| 61c2a48f52 | |||
| 39bceedc17 | |||
| 3f042eb091 | |||
| a3a6b51db4 | |||
| ce728703f6 | |||
| 340f950f9c | |||
| 0e02c2b20f | |||
| 355c1d4dd1 | |||
| c91b9ab1f2 | |||
| f248e2ae80 | |||
| 9a00c8e2f2 | |||
| 83dcdd0501 | |||
| aee884639e | |||
| cdbef992f3 | |||
| 83abc1d87b | |||
| b029ce0139 | |||
| 37f4fe8947 | |||
| a8a1835b68 | |||
| 897a2dcd43 | |||
| db44317d95 | |||
| c9baff58b0 | |||
| 4319e12e35 | |||
| 3a986ecd2f | |||
| e99bb2413f | |||
| bfb80b363b | |||
| 9a38636bf2 | |||
| d131683304 | |||
| 82d5710ddb | |||
| e3cdc86f46 | |||
| 2e13ec3097 | |||
| c734802555 | |||
| 1bbe814ab1 | |||
| 4ef40a02c4 | |||
| 9b6dd80d39 | |||
| a409e8e6b4 | |||
| 826685ceaf | |||
| 64f61797d8 | |||
| eaf57e2292 | |||
| 7cfdf59c29 | |||
| 8cfe01837e | |||
| 80121a3f9a | |||
| 89112eff9b | |||
| 7b22f8fdf7 | |||
| 38bc36d36d | |||
| 31f2d63362 | |||
| 1195d2a054 | |||
| 89e737b25e | |||
| 4beae72eea | |||
| 586c1c99bf | |||
| 4ac2ecb504 | |||
| 973ac6d04f | |||
| d0b9d22141 | |||
| 938d6421f9 | |||
| 9a65900377 | |||
| 142ba7ec20 | |||
| 492e1f811a | |||
| dbe7d374cd | |||
| fa40429e63 | |||
| 35299307d6 | |||
| 889cbb2c12 | |||
| 9133c0961d | |||
| b640bbe606 | |||
| 0b56d777bc | |||
| 1c09b6e7c2 | |||
| 4b77047916 | |||
| 670823e98d | |||
| 71e75b99f0 | |||
| 3a2cef3886 | |||
| d01cb82a89 | |||
| 8d688b982e | |||
| 5b60136a19 | |||
| 39ddd9b501 | |||
| 9c911e6d43 | |||
| cdc71621c4 | |||
| b9b75f7ad1 | |||
| e82d4b084c | |||
| 12441d36fb | |||
| 2c8ad8fe34 | |||
| 3697b7e2d8 | |||
| a750e87517 | |||
| 456bf29984 | |||
| c40764a25f | |||
| ff68ec3a5c | |||
| 72a40ccaaf | |||
| fe22cbf0df | |||
| 4e3b074f5c | |||
| 033cabe80f | |||
| 456ebc8098 | |||
| 13c3294f4d | |||
| 85011cb8ef | |||
| 3a206060e3 | |||
| 9d6e86d4c0 | |||
| ace0dc62a8 | |||
| fe9f396750 | |||
| 00733d8180 | |||
| 077e774303 | |||
| bd2b12bfb7 | |||
| 5130a050ff | |||
| e454eab1a8 | |||
| 67195d7dd5 | |||
| dd76ec89fd | |||
| 376aa67566 | |||
| 4ac99d37cb | |||
| 45e19b7293 | |||
| c794feef9e | |||
| c5b5ca600c | |||
| 4ef73c9198 | |||
| 4b93ac9cbb | |||
| 05bc5aa6e1 | |||
| d36c84689f | |||
| 8f9123ce0d | |||
| a44572bf7b | |||
| 79ea6b0b3b | |||
| 83da278ed2 | |||
| 33c2c6c22e | |||
| b7551d4930 | |||
| 25daec6be9 | |||
| 5229174551 | |||
| df0e5e432a | |||
| 715e950d7c | |||
| dbf78a9d75 | |||
| 26fbd11b73 | |||
| 6e5998ce72 | |||
| b65c676643 | |||
| ae18766b01 | |||
| c7128fe669 | |||
| b37c9f467b | |||
| 08b0b23a68 | |||
| 4a26c34db1 | |||
| 903dc19cec | |||
| 09c5178c85 | |||
| 8750c8f547 | |||
| 028a3637b4 | |||
| 74eecf7a73 | |||
| 0ca3b1dd57 | |||
| ecefe61611 | |||
| 2d96a864da | |||
| dec431579e | |||
| 9520191bde | |||
| 9136cad0ee | |||
| c7e080f639 | |||
| aa4205bf0b | |||
| 9a80d9cf42 | |||
| ea86cf7456 | |||
| 05cfade483 | |||
| 87a30b7061 | |||
| 23dddf893e | |||
| 45ded83294 | |||
| 77f32f14ce | |||
| 07042ebc75 | |||
| ecf262ef07 | |||
| 9acce9d7cd | |||
| cf8b638f33 | |||
| 20cf870c4c | |||
| e095b51d15 | |||
| 65d6e843fb | |||
| c79d15dda4 | |||
| 6035b8a8c2 | |||
| 6c04935711 | |||
| 8c96a5dfb9 | |||
| 8547d38e9a | |||
| 41d8fafa4e | |||
| 0d3a0a081f | |||
| d1df9d4d5c | |||
| e11743d6e7 | |||
| c54a2f916c | |||
| fde27f6838 | |||
| 00c90857ad | |||
| 018fef56bf | |||
| 1db661f19a | |||
| 6ea562ceda | |||
| e137353b95 | |||
| 16d32223a4 | |||
| 8f4fac633f | |||
| d4065c5a41 | |||
| d0b94a75ec | |||
| 8b6b9291a1 | |||
| 7e8764b21b | |||
| efc9dd87ce | |||
| f724c088e0 | |||
| 44942e6976 | |||
| e0d71691e1 | |||
| 9f6d4e8858 | |||
| 19d002d403 | |||
| 2c9633438d | |||
| 19fcafbcab | |||
| 4aed168760 | |||
| 2eeafb9aca | |||
| 7d3e0943e1 | |||
| d0f26cd874 | |||
| 777e02c927 | |||
| c34b7e5771 | |||
| b55ab9eb8f | |||
| bd13495923 | |||
| 683242ba78 | |||
| e54bb318bb | |||
| a302d512d9 | |||
| f09ba0e49f | |||
| 07475cbe6c | |||
| 87ef7988d1 | |||
| f3966f99e9 | |||
| f3639cfb15 | |||
| 6288f755a8 | |||
| 724db0f9bf | |||
| c2d6893a59 | |||
| c6d2641664 | |||
| 3d4c4985a9 | |||
| 912f0bc16d | |||
| ac7af9b9ef | |||
| 400ec890b0 | |||
| 558a24cf98 | |||
| e29f5a2a7c | |||
| b03614c35b | |||
| 1086225f7b | |||
| ca1ed1b0d2 | |||
| 04c7334cc4 | |||
| 93b70fa95a | |||
| fae4f6fcb6 | |||
| d65d84183a | |||
| 1f6da09b2b | |||
| 5fd0161bc7 | |||
| f4e70902d0 | |||
| af0b044fca | |||
| 692dd3f719 | |||
| 4d721a62f7 | |||
| b028fc6b31 | |||
| 581ddccf1a | |||
| 1fd688896c | |||
| f9a5e763fd | |||
| ec2568276d | |||
| 2b6b99059d | |||
| 658536029b | |||
| 675c68de55 | |||
| e9dc423a12 | |||
| 6aaef8f46a | |||
| eaf6d8ed7b | |||
| d30f79e162 | |||
| bde1f3f8d1 | |||
| 2dd7fe339d | |||
| 3c4d983a7d | |||
| 094d8b4504 | |||
| 330c01bbbd | |||
| d2503c1191 | |||
| e3f460c9f8 | |||
| 28b5492def | |||
| 4a62dbf83c | |||
| 8a79f6cbaa | |||
| 9371b65b3b | |||
| e8f5e7272d | |||
| f95df75ea1 | |||
| 918f60c498 | |||
| 23410f9b9e | |||
| 98994c96aa | |||
| 0e484746de | |||
| 87d12b976f | |||
| 519735c782 | |||
| 47f1c24bb3 | |||
| 1bbcc7038f | |||
| e6466a5027 | |||
| 1ab8c7e496 | |||
| 0e839b50d6 | |||
| 623b243306 | |||
| 6a7bbeec2f | |||
| cbf54e6b65 | |||
| 543d06adb5 | |||
| a3b25da96d | |||
| 2a2b3b294c | |||
| 926dedca40 | |||
| 4311a7dfbd | |||
| 31ac91b2a7 | |||
| 0189eed5ac | |||
| 45f9d0c381 | |||
| faa84e98af | |||
| e82f0b6de1 | |||
| 128c04a5d0 | |||
| 19f99382f6 | |||
| ec181998ca | |||
| c400a96575 | |||
| b4c3c2de4a | |||
| 788f4d58ca | |||
| 229169cea1 | |||
| f4cd16b7ac | |||
| e7ea1eeddd | |||
| c96239a794 | |||
| 260320af2e | |||
| f08ef13fe8 | |||
| 3d2d4d6321 | |||
| cfe5249440 | |||
| b9731a4979 | |||
| ce3b594403 | |||
| 94a5af6b6f |
@@ -11,3 +11,7 @@ scripts/*.png
|
||||
# Editor / OS
|
||||
.DS_Store
|
||||
*.log
|
||||
|
||||
# Generierte native-Viewport-Szenen (aus sampleProject via scripts/dump-native-scene.mjs)
|
||||
src-tauri/assets/native2d_scene.json
|
||||
src-tauri/assets/native3d_walls.json
|
||||
|
||||
@@ -1,452 +1,390 @@
|
||||
# Architektur — Standalone Browser-BIM (cad)
|
||||
# Architektur — Dossier (Desktop-CAAD)
|
||||
|
||||
> Stand: 2026-06-29
|
||||
> Vision, Phasen und Backlog: [ROADMAP.md](ROADMAP.md). Konventionen: [CONVENTIONS.md](CONVENTIONS.md).
|
||||
> Detail-Designs: [docs/design/elements.md](docs/design/elements.md) ·
|
||||
> [docs/design/plans-output.md](docs/design/plans-output.md) ·
|
||||
> [docs/design/resources-graphics.md](docs/design/resources-graphics.md).
|
||||
> Stand: 2026-07-21 (grundlegend überarbeitet — siehe [STATUS.md](STATUS.md) für
|
||||
> die volle Bestandsaufnahme inkl. Mist-Liste, die diese Überarbeitung begründet).
|
||||
> Vision/Phasen (historisch, Tag-1-Stand): [ROADMAP.md](ROADMAP.md). Konventionen:
|
||||
> [CONVENTIONS.md](CONVENTIONS.md). Detail-Designs (teils ebenfalls veraltet,
|
||||
> siehe Hinweis in [docs/README.md](docs/README.md)): [docs/design/](docs/design/).
|
||||
|
||||
Dieses Dokument beschreibt, **wie** die Standalone-Browser-App gebaut wird und
|
||||
wie sie die Konzepte des DOSSIER-Rhino-Plugins in Browser-Äquivalente übersetzt.
|
||||
DOSSIER ist ein Rhino-8-Plugin (Python + React-WebView); `cad` ist die *eigen
|
||||
ständige* Browser-Variante: kein Rhino-Document, kein `doc.Strings`, kein
|
||||
IronPython — stattdessen ein **eigenes typisiertes Datenmodell in TypeScript**,
|
||||
gerendert über **Three.js** (3D) und **SVG** (Plan), gespeichert als **JSON-Datei**.
|
||||
Dieses Dokument beschreibt, **wie** Dossier tatsächlich gebaut ist — nicht wie
|
||||
es am ersten Tag geplant war. Dossier ist die eigenständige Neuimplementierung
|
||||
des Rhino-Plugins [DOSSIER](https://git.openbureau.ch/karim/dossier): dieselbe
|
||||
Denkweise (Geschosse, Kategorie-Ebenen, mehrschichtige Bauteile,
|
||||
Prioritäts-Verschneidung), aber als **native Desktop-App** mit einem **eigenen
|
||||
typisierten Datenmodell in TypeScript** und **zwei eigenen Rust/WASM-Rendering-
|
||||
Engines** („Nordstern") statt Rhino-Dokument/IronPython.
|
||||
|
||||
Alle Bezeichner im Code sind **englisch** (Vectorworks-Terminologie); Prosa und
|
||||
UI-Texte sind deutsch. Einheiten intern in **Metern**.
|
||||
**Desktop-Rahmen (plattformabhängig, wegen WebGPU):** auf **macOS Tauri**
|
||||
(WKWebView unterstützt WebGPU), auf **Linux Electron/Chromium** (Tauris
|
||||
Linux-Webview WebKitGTK unterstützt WebGPU nicht zuverlässig — die
|
||||
render2d/render3d-Engines brauchen es). Beide teilen dieselbe React-App und
|
||||
randlose Titelleiste; Laufzeit-Erkennung über `window.__TAURI__` bzw.
|
||||
`window.dossierWindow` (Electron-`contextBridge`). Details: STATUS.md §2.7.
|
||||
|
||||
Alle Bezeichner im Code sind **englisch**; Prosa und UI-Texte sind deutsch.
|
||||
Einheiten intern in **Metern**.
|
||||
|
||||
---
|
||||
|
||||
## 0. Leitprinzip — ein Modell, viele Darstellungen
|
||||
|
||||
Das semantische Gebäudemodell ist die **einzige Wahrheit**. Jede Sicht (3D,
|
||||
Grundriss, Schnitt, Ansicht) ist eine **reine Ableitung** (`derive`) daraus.
|
||||
Das semantische Gebäudemodell (`Project`) ist die **einzige Wahrheit**. Jede
|
||||
Sicht (3D, Grundriss, Schnitt, Ansicht) ist eine **reine Ableitung** daraus.
|
||||
Darstellung (Detailgrad, Stile, Schraffuren, Overrides) wird **beim Rendern**
|
||||
angewandt, nie in die Geometrie eingebacken.
|
||||
angewandt, nie in die Geometrie eingebacken. Dieses Prinzip hat sich über drei
|
||||
Wochen und ~125.000 Zeilen Code bewährt und wird strikt gehalten — es ist der
|
||||
einzige Teil der ursprünglichen Architektur-Vision, der **unverändert** Bestand
|
||||
hat. Alle konkreten Technologie-Entscheidungen darunter (Rendering-Engine,
|
||||
State-Store, Schnitt-Mechanismus) sind anders gelaufen als am Tag 1 geplant;
|
||||
Details dazu in [STATUS.md](STATUS.md) §5.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ Project (semantisches Modell, JSON) │ ← einzige Wahrheit
|
||||
│ resources · types · designLevels · │
|
||||
│ layers · elements │
|
||||
│ resources · types · drawingLevels · │
|
||||
│ layers · walls/doors/openings/stairs/… │
|
||||
└──────────────┬───────────────────────────┘
|
||||
│ pure derive()
|
||||
┌───────────────────────┼────────────────────────────┐
|
||||
▼ ▼ ▼
|
||||
Scene3D (Three.js) PlanModel → SVG SectionModel → SVG
|
||||
buildScene() generatePlan() generateSection() (HLR)
|
||||
│ │ │
|
||||
└────────── beide lesen dieselben joins/components/styles ──────────┘
|
||||
┌───────────────────────┼────────────────────────────┬───────────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
3D-Viewport 2D-Plan (generatePlan) 3D-Live-Schnitt Export
|
||||
Viewport3D (three.js) PlanView (SVG) · glPlan (GL2) render3d/section IFC/DXF/
|
||||
ODER Wasm3DViewport ODER render2d (Rust/WGSL) (Rust, analytisch)PDF/STL
|
||||
(Rust/wgpu, Default)
|
||||
│ │ │ │
|
||||
└────────── alle lesen dieselben joins/components/styles ─────────────┘
|
||||
```
|
||||
|
||||
Das steht im Spike bereits: `generatePlan.ts` und `Viewport3D.tsx` extrudieren
|
||||
**dasselbe** gehrte Band-Polygon (`clippedBand`). Diese Symmetrie ist der Kern
|
||||
und wird beim Ausbau strikt gehalten.
|
||||
|
||||
---
|
||||
|
||||
## 1. Repo-Struktur (Ziel)
|
||||
|
||||
Wächst aus dem heutigen `src/` (model/plan/viewport/ui). Module sind **klein und
|
||||
fachlich geschnitten** — wir vermeiden bewusst den `elemente.py`-Monolithen
|
||||
(7244 LOC) aus DOSSIER (dort als Schwachstelle #4.1 dokumentiert).
|
||||
## 1. Repo-Struktur (IST-Zustand)
|
||||
|
||||
```
|
||||
src/
|
||||
model/
|
||||
types.ts // Project, DesignLevel, LayerCategory, Element-Union (existiert)
|
||||
geometry.ts // 2D/3D-Mathe ohne Kernel (existiert)
|
||||
joins.ts // Wand-Verschneidung (Gehrung; später Prio-T/X) (existiert)
|
||||
sampleProject.ts // Demo-Haus (existiert)
|
||||
project.ts // Factory, Defaults, Migrationen
|
||||
selectors.ts // abgeleitete Reads (visibleCodes, elementsOnLevel…)
|
||||
ids.ts // newId(prefix) — UUID-Erzeugung
|
||||
elements/ // pro Bauteil ein Modul (Daten + Generierung)
|
||||
wall.ts opening.ts slab.ts stair.ts roof.ts structure.ts space.ts
|
||||
resources/
|
||||
componentManager.ts hatchManager.ts lineManager.ts symbolLibrary.ts
|
||||
overrides.ts // regelbasierte Engine (Condition → Action)
|
||||
store/
|
||||
store.ts // Zustand-Store: { project, ui }, Aktionen, Undo/Redo
|
||||
persistence.ts // Datei save/load (JSON), Autosave (IndexedDB)
|
||||
history.ts // Undo/Redo-Ring
|
||||
plan/
|
||||
generatePlan.ts // Grundriss aus Footprint + Symbolik (existiert)
|
||||
generateSection.ts // Schnitt/Ansicht aus 3D-Projektion (HLR-Worker)
|
||||
PlanView.tsx // SVG-Renderer + Pan/Zoom/Grips (existiert)
|
||||
primitives.ts // Primitive-Union, SVG-Serializer, DXF/PDF-Export
|
||||
viewport/
|
||||
Viewport3D.tsx // Three.js-Szene aus dem Modell (existiert)
|
||||
scene.ts // buildScene(project) → THREE.Group (Layer-Spiegel)
|
||||
clip.ts // Plan-/Schnitt-Clipping über THREE.Plane
|
||||
camera.ts // Kamera-Presets, Norden-Rotation
|
||||
sheets/
|
||||
layout.ts // Sheet/Viewport-Datenmodell
|
||||
SheetEditor.tsx // Plansatz-Editor
|
||||
exportPdf.ts // Vektor-PDF (svg → pdf-lib / jsPDF)
|
||||
workers/
|
||||
geometry.worker.ts // Booleans (Öffnungen) + HLR via Comlink
|
||||
ui/
|
||||
App.tsx Navigator panels… // React-Oberfläche (heute in App.tsx, wird gesplittet)
|
||||
model/ Project-Schema (types.ts, ~2500 LOC), joins.ts (Wand-Gehrung/
|
||||
-Prio-Stösse), parametricWalls.ts, roomStamp.ts, terrain.ts,
|
||||
geoRebase.ts, sampleProject.ts
|
||||
geometry/ 2D-Kernel (kernel2d.ts: offset/trim/fillet/split — LIVE, siehe
|
||||
§3), ceiling/opening/roomArea/roomBoundary/stair/roof/column.ts,
|
||||
polygonHoles.ts
|
||||
commands/ Rhino-artiges Kommandosystem: types/registry/engine/parseInput.ts
|
||||
+ cmds/ (ein Modul je Kommando: wall, opening, stair, roof,
|
||||
column, room, line/rect/circle/arc, move/mirror/copy/offset/
|
||||
trim/join, extrude, import, terrain, measure, georef, …)
|
||||
tools/ Interaktive Zeichenwerkzeuge, snapping.ts, transform.ts (Grips)
|
||||
compute/ Compute-Boundary: leitet Ops (aktuell nur computeJoins) unter
|
||||
Tauri via `invoke` an Rust weiter, sonst TS-Fallback
|
||||
plan/ generatePlan.ts (2D-Ableitung), PlanView.tsx (SVG + Pan/Zoom/
|
||||
Grips), glPlan/ (eigener WebGL2-Renderer), toSection.ts/
|
||||
toElevation.ts (Schnitt/Ansicht-Ableitung), toWalls3d.ts,
|
||||
toRenderScene.ts (Szene für Rust-render2d), wallMeshCut.ts
|
||||
viewport/ Viewport3D.tsx (three.js, „Free") UND Wasm3DViewport.tsx
|
||||
(Rust/wgpu „Nordstern", Default/editierbar), raycast3d.ts
|
||||
section/ TOTER Code (OCCT-WASM-HLR-Spike, keine Aufrufer mehr) — Schnitt
|
||||
läuft über render3d/section.rs, siehe §4.3
|
||||
export/ exportIfc.ts (IFC4), exportDxf.ts/dxfWriter.ts, exportPdf.ts,
|
||||
layoutPdf.ts (Mehrseiten-PDF pro Ordner), exportMesh.ts (STL/OBJ),
|
||||
exportSchedule.ts (CSV-Bauteilliste), sceneToPrintSvg.ts
|
||||
materials/ ambientcg.ts (Live-Suche ambientCG-API), library.ts (13
|
||||
gebündelte Starter-Materialien), runtime.ts (PBR-Material aus
|
||||
ComponentMaterial via three.js TextureLoader)
|
||||
io/ DXF/DWG-Import, Swisstopo (swissBUILDINGS3D/-ALTI3D/SWISSIMAGE,
|
||||
LV95↔WGS84), OSM-Overpass, .lin/.pat-Parser, projectFile.ts
|
||||
(.obp Speichern/Laden, Tauri-Lock)
|
||||
overrides/ Regelbasierte Override-Engine (Bedingung → Aktion)
|
||||
state/ Eigener Store auf `useSyncExternalStore` (KEIN Zustand/Redux):
|
||||
store.ts + Slices (project/history/selection/view/layout/site/
|
||||
notify), appStore.ts komponiert sie
|
||||
panels/ Dock-/Floating-Panel-System (Dock/FloatingPanel/TabStrip/
|
||||
registry/layout) + Panels (Tools, Attributes, ObjectInfo, Layers,
|
||||
DrawingLevels, Site, RoomBalance, Elements, ViewSnapshots, Layouts)
|
||||
ui/ App.tsx (Shell, **7.130 LOC — noch nicht auf dünne Shell
|
||||
reduziert**, siehe STATUS.md §4.3), TopBar, ResourceManager.tsx
|
||||
(Material-/Hatch-/Line-/Typ-Editoren, natives Fenster),
|
||||
ContextMenu, CommandLine.tsx, LayoutSheet/-Menu, ribbon/
|
||||
native/ NUR Tauri: native Fenster (Resources/Settings/DrawingLevels/
|
||||
LayerSettings/ContextImport), macOS-Menüleiste, Fenster-Chrome —
|
||||
jede Funktion no-opt via `isTauriRuntime()` im Browser
|
||||
editors/ booleanOps.ts (2D-Boolean via polygon-clipping), splitJoin.ts
|
||||
text/ Rich-Text (richText.ts, RichTextEditor.tsx, renderHtml.ts)
|
||||
theme/ Hell/Dunkel, Akzentfarben
|
||||
i18n/ de.ts/en.ts Wörterbücher, eigener t()-Mechanismus
|
||||
engine/ Reine WASM-Lade-Glue: engine3d.ts (pkg3d), truckSolid.ts
|
||||
(pkgTruck), plan/useWasmPlanRenderer.ts (pkg) — pkgGeometry und
|
||||
pkgDwgImport sind gebaut, aber unbenutzt (siehe STATUS.md §4.1)
|
||||
|
||||
src-tauri/
|
||||
src/ Tauri-Host (Fenster, native Dialoge, fs4-Exklusiv-Lock)
|
||||
render2d/ 2D-Plan-GPU-Renderer (wgpu/WGSL, + glyphon-Text), auch → WASM
|
||||
render3d/ „Nordstern" 3D-Engine (wgpu/WGSL): Wandextrusion + Schicht-
|
||||
bänder + Gehrung, LIVE 2D=3D-Schnitt (section*.rs, analytisch,
|
||||
kein HLR), Kanten-Extraktion, Materialtextur-Arrays,
|
||||
Aerial-Drape, Render-Styles (Shaded/White/Textured/Wireframe/
|
||||
Hidden/ShadedEdges) — auch → WASM
|
||||
kernel2d/ Rust-Port von geometry/kernel2d.ts — NUR Paritätstest, nicht
|
||||
produktiv (WASM war < 100 Wänden langsamer als TS)
|
||||
geometry/ Wand-Join-Mathe — Rust-Port, UNBENUTZT (kein Aufrufer)
|
||||
trucksolid/ CSG/Extrusion (`truck`-Crate + `csgrs`-Booleans) → WASM,
|
||||
genutzt vom Extrude-Kommando; Boolean NICHT an Wände/
|
||||
Öffnungen angeschlossen
|
||||
dwgimport/ DXF-Parser-Spike (`acadrust`) → WASM, UNBENUTZT (DWG-Import
|
||||
läuft über npm `@mlightcad/libredwg-web`)
|
||||
```
|
||||
|
||||
Jedes Crate unter `src-tauri/` ausser dem Host ist ein **eigenständiges
|
||||
Cargo-Package** (kein gemeinsamer Workspace), das sowohl headless
|
||||
(`cargo test`) als auch per `wasm-pack --features web` baut und dann via
|
||||
`src/engine/pkg*/` von der TS-Seite geladen wird (`npm run build:engine{,3d,
|
||||
Geometry,Kernel2d,DwgImport}`/`build:truck`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Datenmodell
|
||||
|
||||
### 2.1 Zwei unabhängige Achsen (DOSSIER-Modell, im Spike umgesetzt)
|
||||
### 2.1 Zwei unabhängige Achsen (unverändert gegenüber der Vision)
|
||||
|
||||
DOSSIER trennt zwei orthogonale Konzepte, persistiert als zwei getrennte
|
||||
JSON-Bäume (`dossier_zeichnungsebenen`, `dossier_ebenen`). Wir übernehmen das
|
||||
1:1 in `types.ts` (existiert bereits als `drawingLevels` + `layers`):
|
||||
1. **Zeichnungsebenen** (`DrawingLevel`) — Geschoss (`kind:"floor"`,
|
||||
`floorHeight`/`cutHeight`/`baseElevation`), Schnitt/Ansicht
|
||||
(`kind:"section"|"elevation"`), oder freie Zeichnung (`kind:"drawing"`).
|
||||
2. **Ebenen** (`LayerCategory`) — das Grafik-Kategorie-Schema (Baum,
|
||||
`{code, name, color, lw, visible, locked, hatch?, children}`), in jedem
|
||||
Geschoss gültig. Codes 1:1 aus DOSSIER (`00 Raster · 01 Vermessung ·
|
||||
20 Wände (└21 Türen/Fenster, 25 Stützen) · 30 Decken · 31 Dächer ·
|
||||
40 Treppen · 50 Text · 60 Räume · 80 Plangrafik …`).
|
||||
|
||||
1. **Zeichnungsebenen** (`DesignLevel` / `DrawingLevel`) — die obersten
|
||||
Dokument-Abschnitte. Zwei produktive Arten:
|
||||
- **Geschoss** (`kind:"floor"`): `floorHeight`, `cutHeight`,
|
||||
`baseElevation` (akkumuliert via `recomputeFloorElevations`), `visible`/`locked`.
|
||||
- **Schnitt/Ansicht** (`kind:"section"|"elevation"`): `linePoints`,
|
||||
`directionSign`, Höhenbereich, Tiefe.
|
||||
- **Zeichnung** (`kind:"drawing"`): freie 2D-Ebene ohne Geschossbezug.
|
||||
2. **Ebenen** (`LayerCategory`) — das **Grafik-Kategorie-Schema**, das in *jedem*
|
||||
Geschoss gilt. Baum mit `{code, name, color, lw, visible, locked, hatch?, children}`.
|
||||
Codes 1:1 wie DOSSIER (`DEFAULT_LAYER_SCHEMA` aus `launcher/src/App.jsx`):
|
||||
`00 Raster · 01 Vermessung · 20 Wände (└21 Türen/Fenster, 22 Möbel, 25 Stützen) ·
|
||||
30 Decken · 31 Dächer · 35 Träger · 50 Text · 60 Plangrafik …`
|
||||
### 2.2 Elemente — typisierte Arrays statt `Element[]`-Union
|
||||
|
||||
Ein **Element** kennt sein **Geschoss** (`floorId`) *und* seine **Ebene**
|
||||
(`categoryCode`). Sichtbarkeit ergibt sich aus dem Schnitt (Geschoss `geschoss × code`
|
||||
Matrix), genau wie DOSSIERs `apply_visibility(z_mode, e_mode)` in `layer_builder.py`.
|
||||
|
||||
> **Begriffsnotiz:** Heute heißt der Typ im Code `DrawingLevel`. ROADMAP §5 nennt
|
||||
> die Modell-Eingabe (Geschosse) Vectorworks-konform **Design Layer** und die
|
||||
> abgeleitete Ausgabe **Drawing Layer / Sheet**. Wir behalten `DrawingLevel` als
|
||||
> Union (kind=floor ≙ Design Layer, kind=section/elevation/drawing ≙ Drawing
|
||||
> Layer) und führen `Sheet` erst separat ein (§2.5, plans-output.md). Kein Rename
|
||||
> ohne expliziten Auftrag.
|
||||
|
||||
### 2.2 Ressourcen-Bibliotheken (Vectorworks-Stil)
|
||||
|
||||
Neuer Block `Project.resources` (heute provisorisch als flache `materials[]`).
|
||||
Alles verweist **per id** — zentral änderbar. Details: resources-graphics.md.
|
||||
Anders als ursprünglich geplant hält `Project` (`src/model/types.ts:2095`)
|
||||
**pro Bauteiltyp ein eigenes (meist optionales) Array**:
|
||||
|
||||
```ts
|
||||
interface Resources {
|
||||
lineStyles: LineStyle[]; // Line Manager: { id, name, weight(mm), color, dash:number[] }
|
||||
hatches: Hatch[]; // Hatch Manager: { id, name, pattern, scale, angle, lineStyleId }
|
||||
components: Component[]; // Component Manager (= DOSSIER-Material erweitert):
|
||||
// { id, name, hatchId, color3d, texture3d?, joinPriority }
|
||||
interface Project {
|
||||
walls: Wall[];
|
||||
ceilings?: Ceiling[];
|
||||
roofs?: Roof[];
|
||||
doors: Door[]; // ÄLTER — siehe openings; bewusste Doppelspur
|
||||
openings?: Opening[]; // NEUER, allgemeiner: kind:"window"|"door"
|
||||
stairs?: Stair[];
|
||||
rooms?: Room[];
|
||||
columns?: Column[];
|
||||
extrudedSolids?: ExtrudedSolid[];
|
||||
drawings2d: Drawing2D[];
|
||||
context?: ContextObject[]; // Terrain/Importe — NICHT semantisch
|
||||
parametricWalls?: ParametricWall[];
|
||||
overrideRules?: OverrideRule[];
|
||||
viewSnapshots?: ViewSnapshot[];
|
||||
viewSnapshotFolders?: ViewSnapshotFolder[];
|
||||
layouts?: Layout[];
|
||||
masterLayouts?: MasterLayout[];
|
||||
layoutFolders?: LayoutFolder[];
|
||||
// + Bibliotheken: lineStyles, hatches, components, wallTypes, roofTypes?,
|
||||
// doorTypes?, windowTypes?, stairTypes?, ceilingTypes?
|
||||
// + drawingLevels, layers, geoAnchor?, referenceElevationMasl?
|
||||
}
|
||||
```
|
||||
|
||||
`joinPriority` ist DOSSIERs `_MATERIAL_PRIO` (Beton 800 … Putz 100) als
|
||||
**Daten** statt Hardcode — steuert die Prioritäts-T-Verschneidung (Risiko #1).
|
||||
Ein `Element`-Typalias existiert noch (`kind:"door"|"window"` etc.), wird aber
|
||||
nirgends im Code referenziert — Selektion/Element-Baum arbeiten direkt auf den
|
||||
typisierten Arrays. `doors`/`openings` sind eine **bekannte, noch nicht
|
||||
konsolidierte Doppelspur** (PENDENZEN.md).
|
||||
|
||||
### 2.3 Aufbauten (mehrschichtige Typen)
|
||||
### 2.3 Ressourcen & Aufbauten (unverändert gegenüber der Vision)
|
||||
|
||||
```ts
|
||||
interface Resources { lineStyles: LineStyle[]; hatches: Hatch[]; components: Component[]; }
|
||||
interface WallType { id; name; layers: { componentId; thickness }[]; }
|
||||
interface SlabType { id; name; layers: { componentId; thickness }[]; }
|
||||
```
|
||||
|
||||
Schichtdicke liegt am Layer, **Priorität am Component** (so muss man Prio nur
|
||||
einmal pflegen). 3D und Plan lesen dieselben `layers[]`.
|
||||
`joinPriority` sitzt am `Component` (Daten statt Hardcode) und steuert die
|
||||
Prioritäts-T-/X-Verschneidung — **fertig implementiert**, inklusive Rust-Port
|
||||
für den 3D-Live-Schnitt (`section_boolean.rs` = Port von
|
||||
`toSection.ts::subtractDominantBands`).
|
||||
|
||||
### 2.4 Elemente
|
||||
### 2.4 Layouts / Ausschnitte
|
||||
|
||||
Diskriminierte Union `Element` (heute `Wall | Door`), erweitert um
|
||||
`Window | Slab | Stair | Roof | Column | Beam | Space | Draw2d`. Jedes Element:
|
||||
`{ id, type, floorId, categoryCode, ... }`. Gehostete Elemente (Tür/Fenster)
|
||||
tragen `hostWallId` statt `floorId` (Geschoss ergibt sich aus der Wand). Volle
|
||||
Felder: elements.md.
|
||||
|
||||
### 2.5 Pläne / Output (eigene Achse)
|
||||
|
||||
```ts
|
||||
interface Sheet { // Plansatz-Blatt (≙ DOSSIER Layout/PageView)
|
||||
id; name; paper: "A0".."A4"|"Letter"; landscape: boolean;
|
||||
viewports: SheetViewport[]; // platzierte Ableitungen + Titelblock
|
||||
folder?: string;
|
||||
}
|
||||
interface SheetViewport { // ≙ DOSSIER Detail + gebundener Ausschnitt
|
||||
id; rect: Rect; sourceViewId: string; // referenziert DrawingLevel oder ViewSnapshot
|
||||
scale: number; // 1:N
|
||||
}
|
||||
interface ViewSnapshot { // ≙ DOSSIER Ausschnitt (View-Snapshot)
|
||||
id; name; folder?;
|
||||
camera: CameraState; scale: number; detailLevel: DetailLevel;
|
||||
visibility: VisibilityState; // pro Geschoss/Ebene
|
||||
overrides?: { presetId?; enabled };
|
||||
}
|
||||
```
|
||||
`ViewSnapshot` (Kamera/Massstab/Detailgrad/Sichtbarkeiten/Override-Preset) in
|
||||
`ViewSnapshotFolder`-Bäumen; `Layout` (Papierformat, mehrere
|
||||
`LayoutViewport`s, freie `LayoutAnnotation`s, optionale `MasterLayout`-Vererbung)
|
||||
in `LayoutFolder`-Bäumen. Beide gehören ins Dokument (nicht LocalStorage).
|
||||
|
||||
---
|
||||
|
||||
## 3. State, Persistenz, Undo/Redo
|
||||
|
||||
### 3.1 Store — Zustand
|
||||
### 3.1 Store — eigener `useSyncExternalStore`, nicht Zustand
|
||||
|
||||
Heute: `useState<Project>` in `App.tsx`, immutabel via `setProject`. Das skaliert
|
||||
nicht für Werkzeuge + Undo. Migration auf **Zustand** (ROADMAP §4):
|
||||
Entgegen der ursprünglichen Empfehlung (Zustand-Library) wurde ein
|
||||
**abhängigkeitsfreier Store** gebaut (`src/state/store.ts`, gleiches Muster wie
|
||||
`src/i18n`): `createStore()` komponiert Slice-Fabriken über eine gemeinsame
|
||||
`RootState`. `appStore.ts` fügt zusammen: `projectSlice` (Projekt + Undo/Redo,
|
||||
`setProject` als einziger Mutations-Einstiegspunkt), `historySlice`,
|
||||
`selectionSlice`, `viewSlice`, `layoutSlice`, `siteSlice`, `notifySlice`
|
||||
(Toast/Confirm-Ersatz für `window.alert`, in Tauris WKWebView deaktiviert).
|
||||
Komponenten lesen über `useStore(selector)`.
|
||||
|
||||
```ts
|
||||
interface AppState {
|
||||
project: Project; // das Dokument
|
||||
ui: { // flüchtig, NICHT persistiert/undobar
|
||||
activeLevelId; activeViewType; selection: string[];
|
||||
activeTool: ToolId; hover; cameraByView; planTransform;
|
||||
};
|
||||
// Aktionen mutieren via Immer-Producer; jede Modell-Mutation pusht History.
|
||||
apply(mutator: (p: Project) => void): void;
|
||||
undo(): void; redo(): void;
|
||||
}
|
||||
```
|
||||
**Nicht umgesetzt** (geplant in `docs/design/state-architecture.md`): die
|
||||
Extraktion von View-Routing nach `src/views/` und Kontextmenü-Aufbau nach
|
||||
`src/menus/` — beide Ordner existieren nicht, diese Logik liegt weiterhin
|
||||
inline in `App.tsx` (7.130 Zeilen). Siehe STATUS.md §4.3.
|
||||
|
||||
**Trennung Modell ↔ UI** ist hart: nur `project` ist undobar und wird gespeichert.
|
||||
`ui` (Auswahl, Kamera, aktives Werkzeug) lebt separat. Das ersetzt DOSSIERs
|
||||
`sc.sticky`-Bus — wo DOSSIER cross-modul über Sticky-Keys kommuniziert, lesen bei
|
||||
uns einfach alle Komponenten denselben Store (reaktiv). **Kein Sticky, kein
|
||||
Bridge-Polling, keine `is not None`-Lücken** (DOSSIER-Schwachstelle #4.4).
|
||||
### 3.2 Persistenz
|
||||
|
||||
### 3.2 Persistenz — JSON-Datei statt `doc.Strings`
|
||||
|
||||
DOSSIER speichert *alles* in `doc.Strings` (Key-Value am Rhino-Document) +
|
||||
globale Presets als JSON-Dateien im User-Home. Browser-Äquivalent:
|
||||
|
||||
| DOSSIER | Browser (cad) |
|
||||
|---|---|
|
||||
| `doc.Strings.SetString(key, json)` | Feld im `Project`-Objekt (ein JSON-Baum) |
|
||||
| `.3dm`-Datei mit eingebetteten Strings | **`.cad.json`-Datei** via File System Access API |
|
||||
| Globale Presets (`~/Library/.../*.json`) | **LocalStorage** (`cad.presets.*`) + Export/Import |
|
||||
| Launcher schreibt Settings-Datei | **IndexedDB** Autosave (Phase 5) |
|
||||
|
||||
```ts
|
||||
// persistence.ts
|
||||
const FORMAT_VERSION = 1;
|
||||
function serialize(p: Project): string // JSON.stringify({ version, project })
|
||||
function deserialize(text: string): Project // + migrate(version) bis aktuell
|
||||
async function saveToFile(p: Project): Promise<void> // showSaveFilePicker → .cad.json
|
||||
async function loadFromFile(): Promise<Project> // showOpenFilePicker; Fallback <input type=file>
|
||||
async function autosave(p: Project) // IndexedDB, debounced 2 s
|
||||
```
|
||||
|
||||
- **Save/Load:** [File System Access API](https://developer.mozilla.org/docs/Web/API/File_System_Access_API)
|
||||
(`showSaveFilePicker`/`showOpenFilePicker`); Fallback Download-Blob + `<input type=file>`
|
||||
für Firefox/Safari.
|
||||
- **Autosave/Recovery:** IndexedDB (via `idb`), entkoppelt vom expliziten Speichern.
|
||||
- **Cross-Projekt-Presets:** LocalStorage mit Namespacing (`cad.presets.overrides`,
|
||||
`cad.presets.wallTypes`, …) + JSON-Export/Import — ersetzt DOSSIERs
|
||||
`~/Library/.../override_presets.json` (overrides.py).
|
||||
- **Migrationen:** `migrate(data, fromVersion)`-Kette, additiv. DOSSIER macht das
|
||||
per Sticky-Prefix (`traite_`→`pause_`→`dossier_`); wir machen es versioniert im
|
||||
Datei-Schema.
|
||||
- **Datei:** eigenes `.obp`-Format (`src/io/projectFile.ts`), unter Tauri über
|
||||
native Speichern/Öffnen-Dialoge (`plugin-fs`/`plugin-dialog`), im Browser
|
||||
Blob-Fallback. Ältere `.json`-Projekte bleiben ladbar.
|
||||
OS-Level-Exklusiv-Lock (`fs4`-Crate, `src-tauri/src/lock.rs`) verhindert
|
||||
Doppelöffnen desselben Projekts.
|
||||
- **Compute-Boundary:** `src/compute/index.ts` leitet einzelne Operationen
|
||||
(aktuell nur `computeJoins`) unter Tauri via `invoke` an eine native Rust-
|
||||
Implementierung weiter; im Browser bzw. für nicht migrierte Ops (Room-
|
||||
Detection, DWG-Parsing) läuft die TS-Implementierung.
|
||||
|
||||
### 3.3 Undo/Redo
|
||||
|
||||
DOSSIER überlässt Undo Rhino (und hat dadurch Cache-Stale-Bugs, Schwachstelle
|
||||
#4.3). Wir besitzen das Dokument selbst → **eigener History-Ring**:
|
||||
|
||||
```ts
|
||||
// history.ts — Snapshots des project-Teilbaums (strukturelles Sharing via Immer-Patches)
|
||||
interface History { undo: Patch[][]; redo: Patch[][]; }
|
||||
```
|
||||
|
||||
Jede `apply()`-Mutation erzeugt Immer-Patches → Push auf `undo`. Da abgeleitete
|
||||
Sichten **pure** sind, ist nach `undo()` kein Cache zu invalidieren — der
|
||||
ganze Cache-Stale-Komplex aus DOSSIER (`_JOINTS_CACHE_KEY`, Material-Cache)
|
||||
entfällt strukturell.
|
||||
Eigener History-Ring im `projectSlice` (kein DOSSIER-Sticky-Bus, kein
|
||||
Cache-Stale-Problem, da alle Sichten pure Ableitungen sind).
|
||||
|
||||
---
|
||||
|
||||
## 4. Rendering
|
||||
|
||||
### 4.1 Three.js-Szene spiegelt den Ebenen-Baum
|
||||
### 4.1 Zwei 3D-Viewports
|
||||
|
||||
DOSSIER baut in Rhino eine **Layer-Hierarchie** `Zeichnungsebene → CODE_Name-Sublayer`
|
||||
(`layer_builder.build_layers`) und hängt jedes Objekt an den passenden Sublayer.
|
||||
Browser-Spiegel: ein **`THREE.Group`-Baum** mit identischer Struktur, gebaut aus
|
||||
dem Modell (nicht persistiert — reine Ableitung):
|
||||
- **`Viewport3D.tsx`** — three.js, die „Free"-Stufe.
|
||||
- **`Wasm3DViewport.tsx`** — Rust/wgpu, „Nordstern", editierbar, **Default**.
|
||||
Ein Settings-Schalter wählt die Engine; WebGL2/three.js nur noch als
|
||||
explizite Wahl oder Fallback.
|
||||
|
||||
`render3d` (10.246 LOC) baut Wandextrusion inkl. Schichtbändern und
|
||||
Prioritäts-Gehrung (`compute_wall_miters`), Materialtextur-Arrays (pro
|
||||
Wandschicht) + separate Aerial-Drape-Textur, Render-Styles (Shaded/White/
|
||||
Textured/Wireframe/Hidden/ShadedEdges via Kanten-Extraktion mit
|
||||
Crease-Erkennung). **Dächer/Treppen/Stützen haben KEINE eigene Rust-Geometrie**
|
||||
— sie werden vollständig in TypeScript erzeugt (`geometry/roof.ts`,
|
||||
`emitRoofs`/`emitColumns`) und Rust nur als vorberechnetes Dreiecks-Mesh
|
||||
(`MeshInput`/`append_context_mesh`, derselbe generische Importpfad wie für
|
||||
swissBUILDINGS3D/DXF) übergeben.
|
||||
|
||||
### 4.2 Grundriss = drei koexistierende Renderer
|
||||
|
||||
1. **`PlanView.tsx`** (SVG) — bleibt immer im DOM (Hit-Testing/Grips),
|
||||
unabhängig vom aktiven Zeichenpfad.
|
||||
2. **`plan/glPlan/`** — eigener TypeScript-WebGL2-Renderer.
|
||||
3. **`useWasmPlanRenderer.ts`** → Rust-`render2d` (WGSL, wgpu nativ,
|
||||
WebGPU/WebGL2-Fallback im Web, echtes Text-Rendering via `glyphon`).
|
||||
|
||||
Beide GPU-Pfade fallen bei Init-Fehler still auf SVG zurück.
|
||||
|
||||
### 4.3 Schnitt/Ansicht — analytische Rust-Pipeline, KEIN HLR
|
||||
|
||||
Der ursprünglich geplante OCCT-WASM-HLR-Pfad (`src/section/hlr.ts`/`occt.ts`)
|
||||
ist **toter Code** (keine Aufrufer mehr). Der tatsächlich funktionierende
|
||||
Live-Schnitt nutzt aus, dass jedes Bauteil ein Prisma mit konstantem
|
||||
Querschnitt ist — eine Schnittebene liefert dadurch **immer** ein
|
||||
achsparalleles Rechteck, nie ein Trapez, wodurch HLR unnötig wird:
|
||||
|
||||
```
|
||||
scene
|
||||
└─ levelGroup[floorId] (y-Offset = baseElevation) ← Geschoss
|
||||
└─ categoryGroup[categoryCode] (userData.code) ← Ebene
|
||||
└─ element meshes (userData.elementId)
|
||||
App.tsx (section3dCutId/section3dPlane)
|
||||
→ Wasm3DViewport.tsx (section3d-Prop → setSectionPlane)
|
||||
→ src-tauri/render3d/src/{section.rs, section_boolean.rs, section_fill.rs}
|
||||
```
|
||||
|
||||
```ts
|
||||
// scene.ts
|
||||
function buildScene(project: Project, vis: VisibilityState): THREE.Group
|
||||
function syncScene(root: THREE.Group, project, vis) // diff statt full rebuild (Perf)
|
||||
```
|
||||
`section_boolean.rs` ist ein 1:1-Port von `toSection.ts::subtractDominantBands`
|
||||
— 2D-Plan-Schnitt und 3D-Live-Schnitt nutzen dieselbe Prioritäts-Logik und
|
||||
stimmen dadurch exakt überein. Kein Worker, kein Comlink (beides war geplant,
|
||||
existiert nirgends im Projekt) — läuft synchron GPU-seitig.
|
||||
|
||||
- **Sichtbarkeit:** `categoryGroup.visible = vis.codes.has(code)` und
|
||||
`levelGroup.visible = vis.floors.has(floorId)` — entspricht DOSSIERs
|
||||
`apply_visibility`, aber als simples `.visible`-Toggle statt Layer-Table-Modify.
|
||||
- **Farbe/Material:** Component → `MeshStandardMaterial` (PBR später), per
|
||||
`componentId` gecacht (wie heute `layerMaterial`-Cache, aber projektweit).
|
||||
- **„Grau/gesperrt"-Modi** (DOSSIER `grey`/`grey_locked`): graues Override-Material
|
||||
auf der Gruppe statt Layer-Color-Tausch.
|
||||
### 4.4 Öffnungen als Löcher (kein Mesh-Boolean)
|
||||
|
||||
### 4.2 Grundriss = Clipping-Ebene + symbolische Generierung
|
||||
Fenster/Türen schneiden echte achsparallele Rechteck-Löcher aus dem Wandkörper
|
||||
(`plan/toWalls3d.ts` `subtractSpans`, gespiegelt in `render3d::mesh.rs`).
|
||||
Ein generisches Mesh-CSG-Boolean existiert bereits (`trucksolid::boolean_mesh`,
|
||||
`csgrs`, für das Extrude-Kommando genutzt), ist aber nicht an die
|
||||
Wand/Öffnungs-Pipeline angeschlossen.
|
||||
|
||||
DOSSIER zeigt den Grundriss eines Geschosses, indem es eine **Clipping-Plane** auf
|
||||
`okff + schnitthöhe` legt, Normale −Z (`layer_builder.update_clipping_plane`).
|
||||
Zwei Browser-Pfade — **beide nötig** (ROADMAP §3):
|
||||
### 4.5 Materialien
|
||||
|
||||
1. **3D-Viewport im Plan-Modus:** `THREE.Plane(normal=(0,0,-1), constant=cutZ)` über
|
||||
`renderer.clippingPlanes` + `material.clippingPlanes`. `clip.ts` setzt die Ebene
|
||||
pro aktivem Geschoss (`cutZ = baseElevation + cutHeight`), Kamera orthografisch
|
||||
von oben. So sieht man den **echten geschnittenen Volumenkörper**.
|
||||
2. **Symbolischer SVG-Grundriss** (der Hauptweg, schnell + sauber): `generatePlan.ts`
|
||||
erzeugt Vektor-Primitive **direkt aus Parametern** — Wand-Schnittbänder,
|
||||
Öffnungs-Aussparungen, Tür-Schwenkbögen — *ohne* Mesh zu schneiden. Steht im
|
||||
Spike. Ausbau: Schraffuren, Lauflinien, Bemaßung, LoD (plans-output.md).
|
||||
|
||||
### 4.3 Schnitt/Ansicht = Kamera + Schnittebenen + HLR
|
||||
|
||||
DOSSIER setzt 1–2 Clipping-Planes (Cut + Back), Parallel-Projektion senkrecht zur
|
||||
Linie, zoomt auf die BBox (`schnitte.activate_schnitt`). Browser:
|
||||
|
||||
- **3D-Vorschau:** zwei `THREE.Plane` (Cut auf der Linie, Back in `directionSign`-
|
||||
Richtung um `depthBack` versetzt) + orthografische Kamera senkrecht zur Linie.
|
||||
- **Vektor-Ergebnis (Risiko #4):** Hidden-Line-Removal durch das zusammengebaute
|
||||
Gebäude → saubere Linien als SVG. Via **OpenCascade.js** (`HLRBRep`) im
|
||||
**Web Worker** (Comlink), Ergebnis gecacht pro (Schnitt, sichtbare Ebenen,
|
||||
Modell-Hash). Geschnittene Bauteile bekommen Component-Schraffur (Section-Style,
|
||||
resources-graphics.md). Details: plans-output.md.
|
||||
|
||||
### 4.4 SVG-Renderer + Geometrie-Booleans
|
||||
|
||||
- **Plan-Output:** SVG (`PlanView.tsx`). `Primitive`-Union (polygon/line/arc/text/
|
||||
hatch) → SVG-Elemente; derselbe Serializer speist DXF- und PDF-Export.
|
||||
- **Öffnungs-Booleans (Risiko #2):** Tür/Fenster schneidet Loch in Wand. Für 3D
|
||||
reicht heute das Aussparen per Segment-Extrusion (im Spike). Für exakte B-Rep-
|
||||
Verschneidung (und IFC) **OpenCascade.js** *oder* **Manifold** im Worker —
|
||||
Entscheidung in Phase 0 (ROADMAP §8): OCC = exakt/B-Rep, Manifold = schnell/Mesh.
|
||||
`materials/library.ts` (13 gebündelte PBR-Starter, ambientCG CC0) +
|
||||
`materials/ambientcg.ts` (Live-Suche der kompletten ambientCG-Bibliothek,
|
||||
1K/2K/4K-Auflösungswahl, On-Demand-Download via `jszip`, Proxy wegen CORS) →
|
||||
`materials/runtime.ts` baut daraus gecachte `THREE.MeshStandardMaterial`s mit
|
||||
physisch korrekter Kachelgrösse (UV in Weltmetern).
|
||||
|
||||
---
|
||||
|
||||
## 5. React-Panel-Struktur
|
||||
|
||||
DOSSIER ist eine Sammlung **getrennter WebView-Panels** (EBENEN, ELEMENTE,
|
||||
GESTALTUNG, OBERLEISTE, MASSSTAB, AUSSCHNITTE, DIMENSIONEN, LAYOUTS, OVERRIDES),
|
||||
die über die Python-Bridge + `sc.sticky` kommunizieren. Im Browser ist alles
|
||||
**eine SPA** mit einem geteilten Store — die Panels werden zu Docking-Bereichen.
|
||||
Dock-/Floating-Panel-System (`src/panels/`: Dock, FloatingPanel, TabStrip,
|
||||
Registry) mit eingebauten Panels: Tools, Attributes, ObjectInfo,
|
||||
DrawingLevels, Layers, Site (Kontext/Terrain-Import), RoomBalance
|
||||
(SIA-416-CSV), Elements (Bauteilbaum), ViewSnapshots, Layouts.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ TopBar Werkzeuge · Ansichtstyp · Massstab · Snaps · Save/Load │ ≙ OBERLEISTE+MASSSTAB
|
||||
├──────────────┬──────────────────────────────────────┬───────────────┤
|
||||
│ Navigator │ Viewport (Three.js oder SVG-Plan) │ Inspector │
|
||||
│ ┌──────────┐ │ ┌────────────────────────────────┐ │ ┌───────────┐ │
|
||||
│ │Zeichnungs│ │ │ 3D / Grundriss / Schnitt / │ │ │ aktives │ │ ≙ ELEMENTE/
|
||||
│ │-ebenen │ │ │ Ansicht — abgeleitet aus dem │ │ │ Element + │ │ GESTALTUNG-
|
||||
│ ├──────────┤ │ │ Modell │ │ │ Stil │ │ Properties
|
||||
│ │ Ebenen │ │ └────────────────────────────────┘ │ └───────────┘ │
|
||||
│ │ (Baum) │ │ │ Manager-Tabs: │
|
||||
│ │ │ │ │ Components · │ ≙ Resource-
|
||||
│ └──────────┘ │ │ Hatch · Line │ Manager
|
||||
├──────────────┴──────────────────────────────────────┴───────────────┤
|
||||
│ BottomBar Koordinaten · Snaps · Statusmeldungen │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
Modal/Drawer: Ausschnitte · Sheets/Layouts · Overrides · Kamera-Presets · SIA-Bilanz
|
||||
```
|
||||
Der **Resource Manager läuft bewusst NICHT als Dock-Panel**, sondern als
|
||||
eigenständiges (unter Tauri natives) Fenster — ebenso Settings,
|
||||
DrawingLevels-Detaileditor, LayerSettings und ContextImport
|
||||
(`src/native/*Window.ts` + `*WindowApp.tsx`), jeweils mit
|
||||
`isTauriRuntime()`-Gate und ohne separate Browser-Variante (im Browser bleibt
|
||||
die entsprechende In-App-Overlay-Variante aktiv, wo vorhanden).
|
||||
|
||||
**Komponenten-Map** (heute alles in `App.tsx`; wird gesplittet):
|
||||
|
||||
| Bereich | DOSSIER-Panel | cad-Komponente |
|
||||
|---|---|---|
|
||||
| Zeichnungsebenen-Liste | EBENEN (oben) | `Navigator/DrawingLevels.tsx` |
|
||||
| Ebenen-Baum | EBENEN (Baum) | `Navigator/LayerTree.tsx` (Spike: `CategoryRow`) |
|
||||
| Top-Bar (View/Snaps/Massstab) | OBERLEISTE, MASSSTAB | `TopBar.tsx` |
|
||||
| Element-Eigenschaften | ELEMENTE, ELEMENT-PROPERTIES | `Inspector/ElementProps.tsx` |
|
||||
| Stil/Attribute der Auswahl | GESTALTUNG | `Inspector/StylePanel.tsx` |
|
||||
| Component/Hatch/Line | (Project-Settings, mass_style) | `managers/*Manager.tsx` |
|
||||
| View-Snapshots | AUSSCHNITTE | `panels/Snapshots.tsx` |
|
||||
| Plansätze | LAYOUTS | `sheets/SheetEditor.tsx` |
|
||||
| Regelbasierte Overrides | OVERRIDES | `panels/Overrides.tsx` |
|
||||
| Bemaßung | DIMENSIONEN | `panels/Dimensions.tsx` |
|
||||
| Element-Übersicht (BIM-Tree) | ELEMENTE-ÜBERSICHT | `panels/ElementTree.tsx` |
|
||||
| Kamera-Presets | KAMERA | `panels/CameraPresets.tsx` |
|
||||
|
||||
**Werkzeug-System** (ersetzt DOSSIERs Rhino-Command-Aliases `cmd/wand.py` etc.):
|
||||
ein `Tool`-Interface mit `onPointerDown/Move/Up`, das Vorschau-Primitive
|
||||
zurückgibt und beim Bestätigen `store.apply()` ruft. Pointer-Events auf Canvas/SVG
|
||||
mit Snap-Engine (Endpunkt/Mitte/Schnitt/Ortho/Raster). Tabelle der Tools:
|
||||
elements.md §8.
|
||||
**Werkzeug-System:** `Tool`-Interface (`src/tools/types.ts`) für Zeichenwerkzeuge
|
||||
mit Snap-Engine; daneben das umfangreichere **Kommandosystem** (`src/commands/`)
|
||||
für Rhino-artige getippte Eingabe (`5,3`/`r5,3`/`5<45`, Tab-Feld-Zyklus in
|
||||
`CommandLine.tsx`) — beide koexistieren, decken unterschiedliche
|
||||
Interaktionsstile ab.
|
||||
|
||||
---
|
||||
|
||||
## 6. Rhino → Browser — Mapping-Tabelle
|
||||
## 6. Rhino → Dossier — Mapping-Tabelle (Kernkonzepte)
|
||||
|
||||
| DOSSIER (Rhino-Plugin) | cad (Standalone-Browser) | Anmerkung |
|
||||
| DOSSIER (Rhino-Plugin) | Dossier (Tauri) | Anmerkung |
|
||||
|---|---|---|
|
||||
| **Rhino `RhinoDoc`** | `Project` (TS-Objekt im Store) | einzige Wahrheit |
|
||||
| **`doc.Strings[key]=json`** | Feld im `Project`-JSON | persistiert in `.cad.json` |
|
||||
| **`.3dm`-Datei** | **`.cad.json`** via File System Access API | + IndexedDB-Autosave |
|
||||
| **Globale Presets (~/Library/*.json)** | **LocalStorage** + Export/Import | cross-Projekt |
|
||||
| **`sc.sticky` (cross-modul Bus)** | **Zustand-Store** (reaktiv) | kein Polling, kein None-Bug |
|
||||
| **`panel_base.BaseBridge` / WebView-IPC** | direkte React-Props/Store | keine `document.title`-Hacks |
|
||||
| **`document.title="RHINOMSG::"` Polling** | entfällt | SPA, kein WebView |
|
||||
| **Eto.Forms Satelliten-Fenster** | React-Modal/Drawer | z.B. Ausschnitt-Settings |
|
||||
| **Rhino Layer-Tabelle (Sublayer-Baum)** | `LayerCategory[]`-Baum + `THREE.Group`-Spiegel | `scene.ts` |
|
||||
| **`layer_builder.build_layers`** | `buildScene` (Ableitung) | Gruppen statt Layer |
|
||||
| **`layer.PlotWeight`** | `LineStyle.weight` (mm) → SVG `stroke-width` | massstabsabh. (§plans) |
|
||||
| **`SectionStyle` (Layer)** | Component-Schraffur beim Schnitt-Rendern | resources-graphics.md |
|
||||
| **Clipping-Plane (`AddClippingPlane`)** | `THREE.Plane` + `renderer.clippingPlanes` | `clip.ts` |
|
||||
| **`vp.ChangeToParallelProjection`** | `THREE.OrthographicCamera` | `camera.ts` |
|
||||
| **`vp.GetFrustum()` → Massstab** | Frustum-Breite (ortho) → 1:N | gleiche Mathe (§plans) |
|
||||
| **CoreGraphics DPI-Auto-Detect** | `window.devicePixelRatio` + CSS-px | Browser kennt DPI nativ |
|
||||
| **`Rhino.Geometry.Brep`-Booleans** | OpenCascade.js / Manifold (Worker) | Öffnungen, HLR |
|
||||
| **`HLRBRep` (über Rhino-Display)** | OpenCascade.js `HLRBRep` (Worker) | Schnitt/Ansicht-Linien |
|
||||
| **Rhino-Grips + DisplayConduit** | Pointer-Events auf SVG/Canvas + Overlay | grip-editing (elements.md) |
|
||||
| **`Rhino.Input.Custom.GetPoint`** | `Tool`-Pointer-Handler + Snap | Werkzeug-System |
|
||||
| **`MouseCallback` (Doppelklick Schnitt)** | `onDoubleClick` auf SVG-Symbol | plans-output.md |
|
||||
| **Rhino Undo/Redo** | eigener History-Ring (Immer-Patches) | kein Cache-Stale |
|
||||
| **`HatchPattern`-Tabelle** | `Hatch`-Ressourcen + SVG `<pattern>` | resources-graphics.md |
|
||||
| **`Linetype`-Tabelle** | `LineStyle.dash[]` → SVG `stroke-dasharray` | resources-graphics.md |
|
||||
| **`TextEntity` / Rich-Text** | SVG `<text>` / HTML-Annotation | plans-output.md |
|
||||
| **`RhinoPageView` (Layout)** | `Sheet` + `SheetEditor` | plans-output.md |
|
||||
| **`FilePdf` Multi-Page-Export** | svg → `pdf-lib`/`jsPDF` (Vektor) | plans-output.md |
|
||||
| **Swisstopo/OSM via .NET HttpClient** | `fetch()` (CORS-fähige STAC/Overpass-APIs) | Phase 4 |
|
||||
| **LV95↔WGS84 (Python-Formeln)** | dieselben Formeln in TS portiert | Phase 4 |
|
||||
| **IronPython 2.7/3 Runtime-Risiken** | entfällt (TS/Browser) | — |
|
||||
| Rhino `RhinoDoc` | `Project` (TS-Objekt im eigenen Store) | einzige Wahrheit |
|
||||
| `doc.Strings[key]=json` | Feld im `Project`-JSON | persistiert in `.obp` |
|
||||
| `.3dm`-Datei | **`.obp`**-Datei (Tauri `plugin-fs`) | + Blob-Fallback im Browser |
|
||||
| `sc.sticky` (cross-modul Bus) | eigener `useSyncExternalStore`-Store | kein Polling |
|
||||
| Rhino Layer-Tabelle | `LayerCategory[]`-Baum, Sichtbarkeit als `.visible`-Flag | pro Renderer umgesetzt |
|
||||
| Clipping-Plane (`AddClippingPlane`) | `render3d::section.rs` (analytisch, Rechteck-Subtraktion) | KEIN HLR |
|
||||
| `HLRBRep` | — (ungenutzt; OCCT-Spike ist toter Code) | ersetzt durch obiges |
|
||||
| `Rhino.Geometry.Brep`-Booleans | `trucksolid::boolean_mesh` (csgrs) | existiert, nicht an Wände angeschlossen |
|
||||
| Rhino-Grips + DisplayConduit | Pointer-Events + Grip-Overlay (2D vollständig, 3D nur Basis, kein Snap) | siehe STATUS.md §4.7 |
|
||||
| Swisstopo via .NET HttpClient | `fetch()` (CORS-offen) | radiusgenau zugeschnitten (nicht ganze STAC-Kachel) |
|
||||
| IronPython-Laufzeit-Risiken | entfällt | TS/Rust |
|
||||
|
||||
---
|
||||
|
||||
## 7. Was wir von DOSSIER übernehmen — und was nicht
|
||||
## 7. Was übernommen wurde — und was bewusst anders lief
|
||||
|
||||
**Übernehmen (bewährte Konzepte):**
|
||||
- Zwei-Achsen-Dokumentmodell (Zeichnungsebenen × Ebenen).
|
||||
- Layer-Codes/Farben/lw 1:1 (`DEFAULT_LAYER_SCHEMA`).
|
||||
- Component/Hatch/Line-Manager mit id-Verweisen + `joinPriority`.
|
||||
- Ansichtstypen = Kamera + optionaler Schnitt (vereinheitlicht).
|
||||
- LoD (`darstellung`: einfach/standard/detail) mit Dokument-Override.
|
||||
- Regelbasierte Overrides (Condition → Action, additive Priorität, reversibel).
|
||||
- Ausschnitte (View-Snapshots), Layer-Kombinationen, Massstab-pro-Viewport.
|
||||
- SIA-416-Räume, Norden-Rotation, Stil-Kataloge.
|
||||
**Übernommen (Prinzipien, bewährt):**
|
||||
- Zwei-Achsen-Dokumentmodell, Layer-Codes 1:1, Component/Hatch/Line-Manager
|
||||
mit `joinPriority`, LoD (grob/mittel/fein), regelbasierte Overrides,
|
||||
Ausschnitte, SIA-416-Räume, Norden-Rotation.
|
||||
- Pure-Ableitungs-Architektur (kein Cache-Stale, kein Sticky-Bus).
|
||||
|
||||
**Bewusst anders (Browser-nativ, Schwachstellen vermeiden):**
|
||||
- **Kein Sticky-Bus** → ein reaktiver Store (vermeidet DOSSIER #4.4/#4.5/#4.6).
|
||||
- **Kein Monolith** → Bauteil-Module statt 7244-LOC-`elemente.py` (#4.1).
|
||||
- **Pure Ableitungen** statt mutierter Doc-Objekte → kein Cache-Stale (#4.3),
|
||||
kein Undo/Redo-Loch.
|
||||
- **Versioniertes Datei-Schema** statt UserString-Sticky-Migration.
|
||||
- **Persistenz im Project-JSON** statt verteilt über `doc.Strings`.
|
||||
**Anders gelaufen als geplant (siehe STATUS.md §5 für die volle Tabelle):**
|
||||
- Eigene Rust/WASM-Rendering-Engines statt Three.js/OpenCascade.js/web-ifc.
|
||||
- Typisierte Arrays pro Bauteiltyp statt `Element[]`-Union.
|
||||
- Eigener Store statt Zustand-Library.
|
||||
- Analytische Rust-Schnitt-Pipeline statt HLR/Worker/Comlink.
|
||||
- App.tsx wurde **nicht** wie geplant auf eine dünne Shell reduziert
|
||||
(`src/views/`/`src/menus/` wurden nie angelegt) — bekannter, unbereinigter
|
||||
Punkt, siehe STATUS.md §4.3.
|
||||
|
||||
**Anti-Over-Engineering (aus DOSSIER-CONVENTIONS.md übernommen):** keine Abstraktion
|
||||
ohne konkretes Problem; kein `try/catch: {}` als Bug-Versteck; erst grep/lesen,
|
||||
dann editieren; Wand-Geometrie nie „nebenbei" refactoren.
|
||||
**Anti-Over-Engineering (weiterhin gültig):** keine Abstraktion ohne konkretes
|
||||
Problem; erst lesen, dann editieren; Geometrie nie „nebenbei" refactoren.
|
||||
|
||||
---
|
||||
|
||||
## 8. Reihenfolge bei Code-Arbeit
|
||||
|
||||
1. **Dieses Dokument + das relevante Detail-Design** (elements/plans-output/
|
||||
resources-graphics) lesen.
|
||||
2. **Dann das betroffene Modul** lesen (nicht raten).
|
||||
3. **Erst danach editieren**; `Project` immer immutabel via `store.apply()` ändern.
|
||||
4. **Verifizieren:** `npx tsc -b`, `npm run build`, Screenshot via
|
||||
`node scripts/probe.mjs` — Geometrie visuell prüfen.
|
||||
5. **Dieses Dokument aktuell halten**, wenn sich Patterns/Mapping ändern.
|
||||
1. **STATUS.md** (aktueller Ist-Zustand) + dieses Dokument lesen.
|
||||
2. **Das betroffene Modul** lesen (nicht raten) — bei Zweifel: welcher der
|
||||
mehreren Renderer/Pfade ist gerade aktiv (§4.1/§4.2)?
|
||||
3. **Erst danach editieren**; `Project` immer immutabel via den Store ändern.
|
||||
4. **Verifizieren:** `npx tsc -b`, `npm test` (Vitest), `cargo test` in den
|
||||
betroffenen Crates, bei Rust-Änderungen `npm run build:engine{,3d}` (WASM
|
||||
neu bauen, nur `.rs` committen — `src/engine/pkg*/` ist gitignored). Bei
|
||||
render3d/Wasm3DViewport-Änderungen testet der Nutzer selbst in der
|
||||
Tauri-Dev-App (nicht per Puppeteer/Browser verifizierbar).
|
||||
5. **Dieses Dokument aktuell halten**, wenn sich Patterns ändern — insbesondere
|
||||
nicht wieder in „Ziel-Struktur"-Beschreibungen abdriften, die Monate lang
|
||||
niemand nachführt.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Projekt-Konventionen — Browser-BIM (cad)
|
||||
# Projekt-Konventionen — Dossier
|
||||
|
||||
Siehe [ROADMAP.md](ROADMAP.md) für Vision, Architektur und Phasen.
|
||||
Siehe [ARCHITECTURE.md](ARCHITECTURE.md) für die aktuelle Architektur,
|
||||
[STATUS.md](STATUS.md) für den vollständigen Ist-Zustand und
|
||||
[ROADMAP.md](ROADMAP.md) für die ursprüngliche (historische) Produktvision.
|
||||
|
||||
## Code-Konventionen (verbindlich)
|
||||
|
||||
@@ -15,16 +17,26 @@ Siehe [ROADMAP.md](ROADMAP.md) für Vision, Architektur und Phasen.
|
||||
CCW-Wicklung zeigt `+n` nach innen. Schichten werden außen (−T/2) → innen (+T/2)
|
||||
gestapelt.
|
||||
|
||||
## Code-Struktur (kein God-Component)
|
||||
## Code-Struktur (kein God-Component — bisher nur teilweise erreicht)
|
||||
|
||||
- **`App.tsx` bleibt ein dünner Shell** (Store-Provider, Oberleiste, Docks+View-Router,
|
||||
Statusleiste, Floating-Panels, Ressourcen-Overlay) — keine Geschäftslogik darin.
|
||||
- **Globaler Zustand in einem Store** (`src/state/`, Slices: project/selection/view/layout).
|
||||
Komponenten lesen Zustand über Store-Hooks statt Prop-Drilling.
|
||||
- **Features als eigene Module:** `src/views/` (View-Router-Teile), `src/editors/`
|
||||
(Inline-Editoren), `src/menus/` (Kontextmenü-Builder), `src/panels/`, `src/ui/`.
|
||||
- **Ziel:** `App.tsx` bleibt ein dünner Shell (Store-Provider, Oberleiste,
|
||||
Docks+View-Router, Statusleiste, Floating-Panels, Ressourcen-Overlay) — keine
|
||||
Geschäftslogik darin. **Realität (Stand 2026-07-21):** `App.tsx` ist mit
|
||||
~7.100 Zeilen die grösste Datei des Projekts und enthält weiterhin
|
||||
View-Umschaltung und Kontextmenü-Aufbau inline — die geplante Auslagerung
|
||||
nach `src/views/`/`src/menus/` (unten) ist **nie passiert**, siehe
|
||||
[STATUS.md](STATUS.md) §4.3. Neue, grössere Features sollten trotzdem nicht
|
||||
weiter in `App.tsx` wachsen; wo möglich in `src/panels/`, `src/editors/`
|
||||
oder ein neues Modul auslagern statt die Datei weiter zu vergrössern.
|
||||
- **Globaler Zustand in einem Store** (`src/state/`, eigener Store auf
|
||||
`useSyncExternalStore` — **kein** Zustand/Redux/Immer — mit Slices:
|
||||
project/history/selection/view/layout/site/notify). Komponenten lesen
|
||||
Zustand über `useStore(selector)` statt Prop-Drilling.
|
||||
- **Features als eigene Module:** `src/editors/` (Inline-Editoren),
|
||||
`src/panels/`, `src/ui/`. `src/views/` und `src/menus/` waren geplant, wurden
|
||||
aber nie angelegt — bei Bedarf gilt das als offener Aufräum-Punkt, nicht als
|
||||
bestehende Struktur.
|
||||
- Ziel: modular + parallel bearbeitbar (verschiedene Features ≠ dieselbe Datei).
|
||||
Siehe `docs/design/state-architecture.md`.
|
||||
|
||||
## UI-Konventionen
|
||||
|
||||
@@ -57,8 +69,10 @@ Rendern angewandt, nie in die Geometrie eingebacken.
|
||||
|
||||
## Arbeitsweise (für Beiträge)
|
||||
|
||||
- Substanzielle, mehrstufige Arbeit an **Subagenten** delegieren, wo möglich.
|
||||
- Substanzielle, mehrstufige Arbeit schrittweise in isolierten Schritten angehen.
|
||||
- Änderungen verifizieren: `npx tsc -b`, `npm run build`, und Screenshot via
|
||||
`node scripts/probe.mjs` (schreibt `scripts/probe.png`) bzw. `probe-ff*.mjs` für
|
||||
Firefox-Fälle. Screenshot ansehen und Geometrie visuell prüfen.
|
||||
- Dev-Server läuft via `npm run dev` (Vite, Port 5173).
|
||||
- Dev-Server läuft via `npm run dev` (Vite, Port 5187). Nativer Rahmen
|
||||
plattformabhängig: `npm run tauri:dev` auf macOS, `npm run electron` auf Linux
|
||||
(WebKitGTK kann kein zuverlässiges WebGPU → dort Chromium/Electron).
|
||||
|
||||
@@ -1,422 +1,255 @@
|
||||
# HANDOVER — Browser-BIM (cad), Standalone-Port von DOSSIER
|
||||
> **Hinweis (2026-07-21):** Dieser Block (Stand 07-09) ist nicht mehr der
|
||||
> neueste Stand — `git log`/[PENDENZEN.md](PENDENZEN.md) reichen bis 07-20/17.
|
||||
> Für den aktuellen Ist-Zustand siehe [STATUS.md](STATUS.md). HANDOVER.md bleibt
|
||||
> ein Append-Log (ältere Sessions unten); es wird nicht rückwirkend aktualisiert
|
||||
> — bei neuen Übergaben oben einen neuen Block ergänzen, nicht diesen editieren.
|
||||
|
||||
> Für die nächste Instanz. Stand: 2026-06-29. Lies zuerst
|
||||
> `CONVENTIONS.md`, `ROADMAP.md` und die Projektnotizen (siehe unten).
|
||||
# HANDOVER — Stand 2026-07-09 (autonome Session, ALLES committet)
|
||||
|
||||
## >>> COMMIT-REGEL (verbindlich, IMMER beachten) <<<
|
||||
Lange autonome Session (Nutzer-Auftrag: „arbeite die Pendenzen/Funktionen durch,
|
||||
selbstständig committen, nicht mehr nachfragen"). **Abweichung vom bisherigen
|
||||
Protokoll: in dieser Session wurde committet** (ausdrücklich beauftragt). Keine
|
||||
KI-Spuren, deutsche Commits, keine Co-Authored-By-Trailer.
|
||||
|
||||
Dieses Repo darf **keinerlei Hinweise auf KI-Werkzeuge** enthalten — weder im
|
||||
Code/Doku noch in der Git-Historie.
|
||||
## Verifikations-Baseline (zuletzt grün)
|
||||
`npx tsc --noEmit` sauber · `npx vitest run` **620**. (Rust/WASM unverändert →
|
||||
kein Neubau nötig.) Der akkumulierte, über frühere Sessions gewachsene grüne
|
||||
Stand wurde zuerst als EIN Basis-Commit `3529930` gelandet, danach jedes Feature
|
||||
einzeln.
|
||||
|
||||
- **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.
|
||||
## In dieser Session gebaut + committet (neueste zuerst)
|
||||
- **Dächer** (`1195d2a` Modell+Geometrie, `31f2d63` Werkzeug+2D+3D): neues
|
||||
Roof-Element + `geometry/roof.ts` (roofGeometry für Flach/Pult/Sattel/Walm/
|
||||
Mansarde/Zelt, auf der Umriss-BBox, First entlang X/Y). Befehl „Dach" (BIM-
|
||||
Ribbon, Alias dach/rf): Rechteck aufziehen, Dachform als Inline-Option wählbar.
|
||||
2D-Plan (Traufe/First/Grat/Knick, generatePlan) + 3D-Flächen (emitRoofs,
|
||||
terrakotta). Demo-Satteldach RF1 im Seed. +19 Tests. **OFFEN (Folge-Increment):
|
||||
Auswahl/Attribut-Editieren/Löschen platzierter Dächer** (Form/Neigung/Überstand
|
||||
im Panel ändern, Klick-Auswahl, Delete) — mirror des Decken-Selektionspfads
|
||||
über ~12 App.tsx-Stellen + PlanView-Pick + selectionInfo + host + Panel.
|
||||
- **Fenster/Tür-Feedback** (`4beae72`, `89e737b`): (a) Fenster wirkten im 3D
|
||||
flach → Rahmen füllt jetzt die VOLLE Wanddicke (tiefe Laibung), Scheibe dünn
|
||||
mittig; Ursache war frameThickness (Profilbreite) fälschlich als Einbautiefe.
|
||||
(b) Detailgrad grob/mittel/fein wirkt jetzt auch im 3D (grob=Loch+Scheibe,
|
||||
mittel=Rahmen, fein=+Sprossen), durchgereicht bis projectToModel3d({detail}).
|
||||
(c) Fenster↔Tür-Umschalter im Objekt-Info entfernt (nur noch Anzeige).
|
||||
(d) **Kernursache „sieht im 3D gar nicht so aus": platzierte Öffnungen bekamen
|
||||
keinen typeId** → resolveOpeningFrame lieferte null → keine Rahmen. appendOpening
|
||||
weist jetzt den ersten Fenster-/Türtyp zu.
|
||||
- **Öffnung ⚙-Knopf** (`586c1c9`): springt in den Tür-/Fenstertyp-Editor.
|
||||
- **Decken-Griffe im 3D** (`973ac6d`): gewählte Decken haben im wgpu-Viewport
|
||||
Eckpunkt- + Kanten-Mittelpunkt-Griffe (rautenförmig) + Verschiebe-Griff →
|
||||
moveCeilingGrip/Edge/By. Prop-Kette Wasm3DViewport←Viewport3D←App. three.js-
|
||||
Sicht unverändert (Default wgpu). **Visuell noch NICHT in Tauri abgenommen.**
|
||||
- **Text-Styling-Leiste** (`d0b9d22`): selektierter Freitext/Textspalte ist jetzt
|
||||
Formatier-Ziel der Oberleiste (Schrift/fett/kursiv/Farbe via Drawing2D-Text.marks;
|
||||
Grösse bleibt über Modell-Höhe, nicht pt). generatePlan/PlanView rendern die Marks.
|
||||
- **Schichttrennlinie als Wand-Referenzlinie** (`938d642`): Wall.referenceOffset
|
||||
(freier Achsversatz) übersteuert left/center/right; Object-Info-Dropdown listet
|
||||
je interne Fuge einen Eintrag. wallReferenceOffset bleibt EINE Quelle (2D/Schnitt/3D).
|
||||
- **Mess-Werkzeug** (`492e1f8`): Polygonzug mit Länge + FLÄCHE (ab 3 Ecken),
|
||||
Live-Werte dauerhaft im Objekt-Info-Panel (ToolDraft.measure→PanelHost.measurement),
|
||||
Rechtsklick beendet Pfad + armiert neuen (mehrere nacheinander).
|
||||
- **Tür/Fenster tief** (`fa40429` Modell/Seeds/RM-Editor, `142ba7e` 2D, `9a65900` 3D):
|
||||
DoorType/WindowType um frameKind (Zarge/Blockrahmen), frameWidth, insetFromFace/
|
||||
insetFace (Schichteinzug), transomHeight (Oberlicht), mullionRows (Kämpfer)
|
||||
erweitert. 2D: opening-frame/-transom-Primitive. 3D: emitOpeningFrames (Rahmen/
|
||||
Sprossen/Kämpfer) + einzugs-/oberlicht-bewusste Scheiben. **3D visuell noch NICHT
|
||||
in Tauri abgenommen.**
|
||||
- **LICENSE** (`dbe7d37`): offizieller AGPL-3.0-Text von gnu.org.
|
||||
- **CSV-Element-Set (D2)** war bereits im Baseline-Stand vorhanden (verifiziert).
|
||||
- **window.prompt/confirm-Sweep**: keine offenen Vorkommen mehr (bereits sauber).
|
||||
|
||||
## >>> STAND 2026-06-30 (Fortsetzung, spät) — ZUERST lesen <<<
|
||||
|
||||
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).
|
||||
|
||||
**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).
|
||||
|
||||
**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`.
|
||||
|
||||
**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**.
|
||||
|
||||
**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`.
|
||||
## Offene Fäden / bewusst NICHT gemacht
|
||||
1. **Tauri-Visualabnahme** der 3D-Änderungen (Tür/Fenster-Rahmen, Decken-Griffe)
|
||||
steht aus — Nutzer testet selbst (render3d/Viewport nicht browserverifizierbar).
|
||||
2. **Rechtsklick=Abbruch generell**: bewusst NICHT global von confirm→cancel
|
||||
umgestellt (würde Polylinien-Abschluss brechen); fürs Mess-Werkzeug via onConfirm
|
||||
gelöst (commit-frei → confirm≙abbruch).
|
||||
3. **Bauteil-Einstellungs-Button** (Discoverability: von der gewählten Öffnung in
|
||||
den RM-Typeditor springen) noch offen — Typeditor selbst ist über das
|
||||
Ressourcen-Fenster (Tür-/Fenstertypen-Tabs) erreichbar.
|
||||
4. Grosse Deferred-Items unverändert: 2D-Schnitt editierbar, Schnitt-Verschneidung
|
||||
(wartet auf `.obp`), Feld-Controller 3D, Schnitt/Ansicht Phase 3, truck-Live-Wiring.
|
||||
5. **Konventionen bleiben:** ab jetzt wieder Standard = NICHT committen, ausser der
|
||||
Nutzer beauftragt es erneut. Keine KI-Spuren.
|
||||
|
||||
---
|
||||
|
||||
## >>> 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`.
|
||||
# HANDOVER — Stand 2026-07-08 spät (Schnitt-Parität + Projektdatei, ALLES uncommittet)
|
||||
|
||||
**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`).
|
||||
Fortsetzungs-Session (nach dem älteren 2026-07-08-Handover weiter unten). **Alles uncommittet.** Schwerpunkt: 3D-Live-Schnitt = 2D-Schnitt + eigene Projektdatei.
|
||||
|
||||
**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.
|
||||
## Verifikations-Baseline (zuletzt grün)
|
||||
`npx tsc --noEmit` sauber · `npx vitest run` **588** · `cargo test -p render3d --features render` **76** · `cargo check --features native3d` grün · WASM `npm run build:engine3d` neu gebaut (nötig nach Rust/WGSL-Änderungen). **Vor Weiterarbeit einmal `tsc`+`vitest` neu fahren.**
|
||||
|
||||
**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).
|
||||
## Was diese Session gebaut wurde
|
||||
- **3D-Live-Schnitt = 2D-Schnitt (Kernfunktion):** (a) 45°-Schraffur-Verdrehung gefixt (Shader auf 2D-`parallelLines`-Konvention kalibriert, angle 0 = horizontal); (b) geschichteter Bodenaufbau (Decke per-Schicht statt Einkörper, `emitSlabs`); (c) **Prioritäts-Verschneidung** von 2D nach Rust portiert (`section_boolean.rs` = Port von `subtractDominantBands`/`subtractRect`; Bänder tragen `join_priority` via `CutBandMeta`); (d) `collectCeilingCutters` emittiert per-Schicht-Cutter → Backstein (50) schneidet Estrich/Dämmung (30/20) korrekt weg, nur Beton (100) kappt ihn; (e) **einstellbare Schichttrennlinien** aus `jointLineStyleId`; (f) **Schraffur-Strichstärke pro Hatch** aus deren `lineStyleId` (`hatch-hair` 0.02 / `thin` 0.13); (g) `relativeToWall`-Orientierung: Boden-Dämmung vertikal, Wand horizontal (via `resolveHatch(axisAngle)` wie 2D). Betrifft `src-tauri/render3d/src/{shaders,gpu,section,section_fill,section_boolean,types}.rs` + `src/plan/toWalls3d.ts`. `native.rs` `WallInput`-Literal um die additiven Felder ergänzt.
|
||||
- **Projekt als Einzeldatei `.obp`** („openbureau project"): `src/io/projectFile.ts` (`PROJECT_EXT`, nativer Öffnen/Speichern-Dialog, alte `.json` weiter ladbar) + **OS-Lock gegen Doppelöffnung** (`src-tauri/src/lock.rs`, `fs4` exklusiver flock über gehaltenes Handle → Auto-Freigabe bei Absturz/Exit; `LockConflictDialog`). QA: Lock-Logik korrekt, einzige Lücke: „schreibgeschützt" == „erzwingen" auf Datenebene (kein app-weiter Read-only-Modus).
|
||||
- **Identität:** Open-Source-CAAD statt „BIM für Wohnbau"; `about.desc` (de/en) + README + `package.json` (`license: AGPL-3.0-or-later`, `author`). **LICENSE-File fehlt noch** (README verlinkt darauf) — offizieller AGPL-Text muss rein (nicht fabrizieren; `curl gnu.org/licenses/agpl-3.0.txt`). About-Dialog-Umbruch gefixt (`.about-dialog { white-space: normal }` — er erbt `nowrap` von `.topbar`).
|
||||
- **2D:** Text-Platzierungswerkzeug als 2D-Element (`text`-Command + Freitext-Eingabe in der Command-Engine); 2D-Zeichenwerkzeuge auf `"drawing"`-Ebenen freigegeben (ToolsPanel-Gating = `floorOnly`, Parität mit Ribbon).
|
||||
- **Teil B — 2D-Annotationen auf Layout-Blättern:** `LayoutAnnotation` (line/rect/text in mm) + CRUD (`layoutModel.ts`) + Editor in `LayoutSheet.tsx` (zeichnen/selektieren/verschieben/löschen, `PromptDialog` für Text) + `layoutSheetMath.ts` (Hit-Tests). Bewusst ohne: Resize-Griffe, Text-Rotation, Snapping.
|
||||
|
||||
**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.
|
||||
## Offene Fäden
|
||||
1. **Idee 1 — per-Wand „an Decke enden"** (Terminierung ≠ Priorität): AGENT LÄUFT gerade (per-Wand `sliceTermination`, Default off). Details/Design im Memory `design-schicht-verschneidung-3d-schnitt.md`. **Idee 2 (Schichteinzüge, per-Schicht Endversätze)** = größeres Folge-Feature, noch nicht gebaut.
|
||||
2. **Ressourcen-Fenster als eigenes OS-Fenster** (Nutzer-Wunsch): machbar (Tauri `WebviewWindow` + BroadcastChannel-Sync, „Ausklappen"-Button, Browser bleibt intern). Noch NICHT gebaut — braucht Store-Sync-Architektur-Entscheidung.
|
||||
3. **Visuelle Tauri-Abnahme** aller 3D-Schnitt-Änderungen steht aus (render3d nur naga-validiert + WASM gebaut; Nutzer testet selbst). Der Backstein hat prioritäts-korrekt noch ein Stück oberhalb des Betons — das ist Idee 1 (Option), nicht ein Bug.
|
||||
4. **Konventionen:** wie immer — keine KI-Spuren, nicht committen, disjunkte Agent-Lanes, echte Eigenschaft prüfen (nicht nur grüne Tests).
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
# HANDOVER — Stand 2026-07-08 (Account-/Instanzwechsel, ALLES uncommittet)
|
||||
|
||||
## 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).
|
||||
Übergabe an eine frische Instanz (Nutzer wechselt Account, Budget aufgebraucht). **Alles unten ist UNCOMMITTET** (Bearbeiter committet nie selbst). Zuerst `PENDENZEN.md` lesen (Single Source of Truth, ✅-Erledigt-Liste + offene Items ganz konkret). Diese Session war eine sehr lange Feature-/Fix-Serie mit vielen Sub-Agents.
|
||||
|
||||
### 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`).
|
||||
## Verifikations-Baseline (zuletzt grün)
|
||||
`npx tsc --noEmit` sauber · `npx vitest run` **491/491** · `cargo check --manifest-path src-tauri/Cargo.toml` grün · `vite build` OK. (Der letzte Layout-Viewport-Agent meldete 491; nach dem interaktiven Ausschnitte-Fix waren es 465 → 491 ist der aktuelle Stand nach dem Layout-2b-Agenten.) **Vor Weiterarbeit einmal `tsc`+`vitest` neu fahren**, da alles uncommittet nebeneinander liegt.
|
||||
|
||||
**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).
|
||||
## Was diese Session gebaut wurde (alles in PENDENZEN ✅ dokumentiert)
|
||||
- **Interop-Export:** IFC4 (`exportIfc.ts`, jetzt `IfcTriangulatedFaceSet` mit ausgeschnittenen Fenster/Tür-Löchern + SOLIDE Wände — Winding-Fix), STL+OBJ (`exportMesh.ts`), gemeinsamer Loch-Ausschnitt `src/plan/wallMeshCut.ts` (Port von render3d `extrude_layer_segment_with_holes`, `isWatertight` via Gauss/Volumen). Schnellexport-Dialog (`ExportSaveDialog`, Format+Name).
|
||||
- **Nativer Speichern-Dialog:** Tauri `plugin-dialog`+`plugin-fs` (Cargo/lib.rs/capabilities), `src/io/saveFile.ts` (`saveTextFile`: nativ unter Tauri, Blob-Fallback im Browser). App.tsx `downloadTextFile`→`saveTextFile`.
|
||||
- **Ausschnitte (View-Snapshots) Phase 1:** `Project.viewSnapshots`, `state/viewSnapshots.ts`, `ViewSnapshotsPanel` (Speicher-Fix: Inline-Input statt window.prompt).
|
||||
- **Layout-Blätter Phase 2a+2b:** Modell `Layout`/`LayoutViewport`/`MasterLayout` (`Project.layouts`/`masterLayouts`), In-Viewport-Editor (`activeLayoutId` in App.tsx → `LayoutSheetView`/`LayoutSheet.tsx`, Maus-Editing, `layoutSheetMath.ts`), `LayoutsPanel`. Floating `LayoutEditor` wurde ersetzt.
|
||||
- **Tragwerk-Stützen:** `Column`/`ColumnProfile`, `geometry/column.ts`, Platzieren/2D/3D/Selektion/Attribute/move.
|
||||
- **Weiteres:** A1 Override-Regel-Engine, kernel2d Phase 5 komplett, Bauteil-CSV volles Element-Set, SIA-416 AGF, Kamera-Presets Kardinal+Iso, Decken-UK/OK, BIM-Tree-Panel, ObjectInfo-Volumen, Basis-Material-Texturen, OSM 7 Kat., IndexedDB-Kern, Rich-Text super/sub, Auto-Zoom-Geo, Raumstempel Personen/Rundung, diverse TopBar-Feinschliffe (Export-Sammelmenü, Über-Dialog, Petrol-Punkt, Detailgrad+Darstellung gestapelt, Arbeitsumgebung in Settings).
|
||||
|
||||
**Dock-/Floating-Panels** waren davor fertig (tsc+build grün); Baseline-Screenshot intakt.
|
||||
## ⚠️ WICHTIG / offene Fäden (Details in PENDENZEN, Abschnitt „In Arbeit"/oben)
|
||||
1. **NOCH NICHTS VISUELL IN TAURI ABGENOMMEN:** IFC/OBJ/STL-Geometrie, nativer Speichern-Dialog, Layout-im-Viewport, Ausschnitte — alle agent-/testverifiziert, aber der Nutzer hat sie im laufenden Fenster noch NICHT gesehen. **Tauri-Dev NEU STARTEN** (`npm run tauri:dev`, ggf. Vite separat auf Port 5187 — es gibt KEIN `beforeDevCommand`) wegen der neuen Rust-Plugins.
|
||||
2. **`window.prompt`/`confirm`-SWEEP:** in Tauri-WKWebView DEAKTIVIERT (→ null). Gefixt: ViewSnapshotsPanel, LayoutsPanel. NOCH betroffen: `ComboMenu` (Kombinationen speichern), TopBar Massstab „frei" (`topbar.scale.prompt`), evtl. weitere. Grep `window.prompt|window.confirm` über `src/`, alle auf Inline-Eingaben umstellen.
|
||||
3. **NÄCHSTES GROSSES ITEM (fertig gescoped, war startbereit als Opus-Agent):** „Layouts als Ordner-/Baumstruktur" — voller Auftrag steht in PENDENZEN (Ordner-Baum, ein „+" für Ordner/Layout/Masterlayout, Inline-Rename, Layout-Erstell-Dialog mit Master-Vorlage/freie Grösse, Masterlayout mit Grössenwahl, Ordner→Mehrseiten-PDF). Direkt umsetzbar.
|
||||
4. **Konventionen:** keine Fremd-Tool-Spuren/Co-Authored-By im Repo; Sub-Agents in disjunkte Datei-Lanes schicken (i18n de.ts/en.ts + App.tsx sind Hotspots → exklusiv EINEM Agent pro Welle, sonst NUL-Byte-Kollision); grüne Tests NICHT als alleinigen Beweis nehmen (echte Eigenschaft prüfen — z. B. beim IFC-Solidity-Fix war der naive Manifold-Test falsch, Gauss/Volumen war richtig).
|
||||
|
||||
### 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.
|
||||
---
|
||||
|
||||
### 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).
|
||||
# HANDOVER — Stand 2026-07-07 abends (Instanzwechsel, alles uncommittet)
|
||||
|
||||
### 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.
|
||||
Übergabe an eine frische Instanz (Nutzer wechselt bewusst die Instanz). **Alles unten Gelistete ist NOCH UNCOMMITTET** — Bearbeiter committet laut Arbeitsprotokoll nicht selbst. Zuerst `CONVENTIONS.md` + `PENDENZEN.md` lesen (dort steht die volle, aktuelle Aufgaben-Historie inkl. aller Details der truck-Integration/Deep-Review-Fixes unten — hier nur die Kurzfassung + der offene Faden).
|
||||
|
||||
## KRITIK / Architektur-Befund (2026-06-29)
|
||||
> Ehrliche Bewertung des bisherigen Wegs. Für die nächste Instanz als Entscheidungsgrundlage.
|
||||
## Sofort-Kontext für die neue Instanz
|
||||
|
||||
**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.
|
||||
**Environment:** macOS-Gerät, Toolchain vollständig (node/npm/cargo/wasm-pack), `npm run tauri:build` läuft. Kein Blocker.
|
||||
|
||||
**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.**
|
||||
**Verifikations-Baseline (zuletzt geprüft, alles grün):** `npx tsc --noEmit` sauber · `npx vitest run` 341/341 · `cargo test` in `src-tauri/trucksolid` 15/15 · `cargo test --manifest-path src-tauri/Cargo.toml -p render3d` 58/58.
|
||||
|
||||
**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.
|
||||
**Was in dieser Session geschah (chronologisch, Details je in PENDENZEN.md „truck-Integration" Phase 5 / Backlog):**
|
||||
1. Vorherige Session ist wegen RAM-Absturz des Geräts abgebrochen (lief eine LOKALE LLM auf dem Laptop, nicht diese Instanz) — Stand war: truck-Integration Phasen 1–4 fertig (Extrusion + Verjüngung/Taper + Boolean-CSG via `csgrs`), aber noch uncommittet.
|
||||
2. Diese Instanz hat den übernommenen Stand **deep-reviewt** (8-Winkel-Review: Korrektheit/Reuse/Simplify/Efficiency/Altitude/Conventions) und mehrere echte Bugs gefunden + gefixt — volle Liste in PENDENZEN.md unter „truck-Integration" → „Deep-Review + Fixes (2026-07-07)": konkave Profil-Triangulierung (Fächer→Ohr-Clipping), 6 fehlende Selektions-Resets für Extrusionen, Mirror/Copy ignorierten Extrusions-Auswahl, DXF-Export verlor den Layer, `truckMeshCache`-Leak + Flacker-Bug, unnötige `fitTargetDist`-Neuberechnung, fehlende i18n (Schnittebene-Button + Farbwähler-Tooltip).
|
||||
3. Danach auf Nutzer-Wunsch **eigenständig durch PENDENZEN.md** gearbeitet (nur unblockierte Items, keine Design-Entscheidungen geraten): **Ortho-Ray-Picking gefixt** (`cameraRay` in `src/viewport/raycast3d.ts` konnte nur perspektivisch — jetzt echte parallele Strahlen bei `perspective:false`, rückwärtskompatibel; +2 Tests). Siehe PENDENZEN.md „3D-REST" Abschnitt.
|
||||
|
||||
## 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-07-07 (Folge-Instanz) — Bauteil-CSV volles Element-Set (D2, vertieft A6), umgesetzt exakt nach dem Scope unten; Treppen-Fläche = null (leer), Aggregat-Zellen leer statt 0.00. Details/Status in PENDENZEN.md „✅ Erledigt".
|
||||
|
||||
### ✅ 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.
|
||||
`src/export/exportSchedule.ts` deckt aktuell nur **Wand + Decke** ab (Muster: `evalWall`/`evalCeiling` → `ScheduleRow`, siehe Datei). PENDENZEN-Backlog-Item „AUDIT (DOSSIER-Studie)" nennt D2 = „Bauteil-CSV mit vollem Element-Set". Ich habe das untersucht und **bewusst gescoped, aber NICHT umgesetzt** (Instanzwechsel kam dazwischen) — der Datei-Zustand ist unverändert/clean (ein Testballon-Import wurde wieder entfernt, `tsc` ist grün). Damit die nächste Instanz nicht neu recherchieren muss, hier die bereits getroffenen, gut begründeten Scope-Entscheidungen:
|
||||
|
||||
### 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.
|
||||
- **Aufnehmen:** `Door` (Tür), `Opening` (kind="window"→Fenster, kind="door"→Öffnung), `Stair` (Treppe), `ExtrudedSolid` (Extrusion, aus der truck-Integration).
|
||||
- **NICHT aufnehmen: `Room`** — Räume haben bereits einen eigenen, dedizierten CSV-Export (`roomsToCsv` in `src/geometry/roomArea.ts`, SIA-Flächenbilanz-Spalten). Eine zweite Raum-Zeile in der allgemeinen Bauteilliste wäre Duplikation.
|
||||
- **Spalten-Mapping** (bestehende feste Header „Länge/Höhe/Dicke/Fläche" — nur dort füllen, wo eine Ableitung wirklich verlässlich ist, sonst leer lassen wie es `evalCeiling` heute schon für Decken macht):
|
||||
- Door/Opening: `length = width` (lichte Breite), `heightM = height`, `thickness = null` (keine sinnvolle Entsprechung), `area = width * height`. Geschoss auflösen über `hostWallId` → `project.walls.find(w => w.id === hostWallId)?.floorId` (neuer kleiner Helper nötig, Wand evtl. verwaist behandeln wie bei `evalWall`/`evalCeiling`).
|
||||
- Stair: `length = runLength + (run2Length ?? 0)` (Näherung, bei Wendel evtl. wenig aussagekräftig — akzeptabel, da nur „verlässlich Ableitbares" Anspruch gilt), `heightM = stair.totalRise ?? null` (KEINE Geschosshöhen-Fallback-Auflösung nachbauen — Kopplung/Risiko vermeiden), `thickness = null`, `area`: **noch nicht entschieden** — eine echte Footprint-Fläche bräuchte `geometry/stair.ts`s `stairGeometry()` (Kopplung an Geschoss-Höhen-Fallback) → einfachster sicherer Weg ist vermutlich `area = null` lassen (ehrlich statt erfunden), aber nochmal gegen die bestehenden Tests/Konventionen prüfen, bevor final entschieden wird.
|
||||
- ExtrudedSolid: `typeName = "Extrusion"`, Geschoss über `levelId`, `length = null`, `heightM = solid.height`, `thickness = null`, `area = polygonArea(solid.points)` (Import aus `geometry/roomArea.ts`, bereits im File importiert).
|
||||
- Neue `kind`-Werte in `ScheduleRow`/`TypeAggregate`: `"Tür" | "Fenster" | "Öffnung" | "Treppe" | "Extrusion"` zusätzlich zu `"Wand" | "Decke"`.
|
||||
- **Aggregat-Zeile (`aggregateByType`/Summary-Block):** die bestehende Zeile `fmtNum(agg.kind === "Wand" ? agg.totalLength : null)` sollte verallgemeinert werden zu `agg.totalLength > 0 ? fmtNum(agg.totalLength) : ""` — dann zeigen Tür/Fenster/Öffnung/Treppe (die jetzt auch eine `length`-Spalte befüllen) automatisch korrekt eine Summe, während Decke/Extrusion (deren `length` immer `null`→0 bleibt) weiterhin korrekt leer bleiben. Kein Sonderfall pro Kind nötig.
|
||||
- **Tests:** `src/export/exportSchedule.test.ts` folgt einem klaren `fixtureProject()`-Muster (2 Wände + 1 Decke) — beim Erweitern eine Tür/ein Fenster/eine Treppe/eine Extrusion in die Fixture aufnehmen und je einen Zeilen-Assert wie bei den bestehenden Wand-/Decken-Zeilen ergänzen.
|
||||
- Warum nicht in dieser Session fertig: Instanzwechsel kam mitten in der Umsetzung. **Kein Blocker, keine Nutzer-Entscheidung nötig** — reine Fleissarbeit nach obigem Schema, dann `tsc`/`vitest` grün prüfen und in PENDENZEN.md abhaken.
|
||||
|
||||
### 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).
|
||||
## Aus dieser Session bewusst NICHT angefasst (recherchiert, aber zu unsicher/blockiert)
|
||||
|
||||
### >>> 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).
|
||||
- **Treppe Pfeil-Style „filled" + Tür `swing_invert`/`aussenseite`** (PENDENZEN „BAUTEILE… Gruppe B"): die Referenzquelle (`/tmp/dossier-ref/rhino/*.py`) existiert auf **diesem** Gerät/dieser Session NICHT (geprüft, `find` liefert nichts) — ohne das exakte Rhino-Verhalten zu kennen, wäre die visuelle Ausgestaltung geraten. Nicht angefasst, um nicht falsch zu raten.
|
||||
- **E2b Schraffur-Kachel-Motiv:** `MotifEditor` (`src/ui/MotifEditor.tsx`) wird schon für Linienstil-Motive wiederverwendet (1D, entlang einer Linie) — ein Flächen-Kachel-Motiv für `HatchStyle` wäre ein NEUES Datenmodell (2D-Tile-Grid), kein reines Wiederverwenden. Braucht eigentlich einen Scope-Entscheid.
|
||||
- Alles, was in PENDENZEN.md explizit „Mit Nutzer klären"/„Nutzer-Entscheid nötig" trägt (Ribbon 3D-Tab, kernel2d Phase 6, Feld-Controller, Schnitt/Ansicht Phase 3, Teamwork-Granularität, Geo-Block-Priorität, Textur-Pipeline-Scope, STRATEGIE-Priorisierung, Bildschraffur-Freigabe) — bewusst übersprungen, nicht neu bewertet.
|
||||
|
||||
### 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.
|
||||
## Wichtige Memory-Notiz (persistiert, gilt für zukünftige Instanzen dieses Repos)
|
||||
Der Nutzer lässt teils eine **lokale LLM auf seinem Laptop autonom** an diesem Projekt arbeiten (kann abstürzen, z. B. RAM). Deren Output sah in dieser Session strukturell sauber + gut getestet aus, enthielt aber einen echten geometrischen Bug (Fächer-Triangulierung bricht bei konkaven Profilen), den die eigene Testsuite NICHT gefangen hatte (Tests prüften nur Form/Anzahl, nicht geometrische Korrektheit). **Bei Übernahme von Arbeit aus solchen Sessions: grüne Tests allein NICHT als Beweis nehmen, gezielt nachprüfen ob die Tests die richtige Eigenschaft testen.**
|
||||
|
||||
### 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`).
|
||||
# HANDOVER — Stand 2026-07-04 (Schraffur/Linien-Epic + Folgethemen)
|
||||
|
||||
## 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.
|
||||
Ü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.
|
||||
|
||||
## 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.
|
||||
> **➡️ 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.
|
||||
|
||||
## 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).
|
||||
## ⚠️ NACHTRAG 2026-07-04 abends — Unterbruch, 3 Slices verloren
|
||||
|
||||
> **STAND-UPDATE (Session 4, neues Gerät):** **Environment behoben** — Toolchain vorhanden (`node`/`npm`/`cargo` unter `/usr/bin`, `wasm-pack` via `npx` 0.15.0; es war die Flatpak-Sandbox); fehlendes Rust-Target per `sudo dnf install rust-std-static-wasm32-unknown-unknown` (+ lld) nachinstalliert; beide WASM-Engines gebaut → App läuft. **Slice 1 (Locked-Iso `cb8fae5`) + Slice 2 (Joins Phase 1c `c5a344d`) committet.** **Slice 3 (Öffnungen als Boolean-Löcher + Deckentrim-nur-3D) ✅ committet `1407c68`:** Fenster/Türen = 1 Wandkörper pro Schicht-Band mit rechteckigen `holes` (render3d stanzt sie per achsparalleler Rechteck-Gitter-Zerlegung + 4 Laibungsquads/Loch), Segment-Boxen weg; `holes`/`openings` schliessen sich im Emitter aus; Schnitt-Pfad (layered=false) segmentiert unverändert weiter. Deckentrim `trimWallTopForCeilings` gilt nur noch im 3D-Pfad, Schnitt-Pfad bekommt volle Wandhöhe. Alle Datei-Edits waren gelandet — unabhängig verifiziert: cargo test 56 (inkl. beider Pflicht-Tests versetzt-überlappende Fenster + Tür-Loch-bis-Boden), vitest 230, build:engine3d + tsc sauber, Trace clean. **Damit sind alle drei verlorenen 3b-Slices rekonstruiert.** **Memory-Direktiven `priority-based-joins.md` + `openings-as-booleans.md` wiederhergestellt.**
|
||||
>
|
||||
> **⚠️ PUSH BLOCKIERT:** keine Git-Credentials auf diesem Gerät (kein Helper, keine `~/.git-credentials`, kein SSH-Key; Remote HTTPS `git.openbureau.ch/karim/DOSSIER-STANDALONE`). `cb8fae5` + `c5a344d` (+ folgender Öffnungen-Commit) sind **lokal committet, NICHT gepusht** (`ahead of origin/master`). Sobald Gitea-PAT (repo-write) oder SSH-Key da ist: `git push origin master`. Bis dahin holt das andere Gerät die Arbeit nicht.
|
||||
>
|
||||
> Der folgende Block bleibt als historischer Kontext/Vorlage für den offenen Slice 3 stehen.
|
||||
|
||||
Die Arbeit wurde unerwartet unterbrochen — mehrere Tasks liefen parallel. Diese Übergabe wurde nachträglich rekonstruiert. **Der tatsächliche HEAD-Commit ist `03f0c40`** (Nordstern-3D Live-Schnittebene) — alles danach existierte nur im Arbeitsbaum der alten Maschine und ist auf **diesem** Checkout (neues Gerät, `git status` sauber bis auf `package-lock.json`) **nicht vorhanden**. Diese drei Slices waren in echter Arbeit (nicht nur geplant) und gelten als verloren — neu beauftragen, nicht nur „prüfen ob gelandet":
|
||||
|
||||
1. **Locked-Iso-Fix** (`src/viewport/Wasm3DViewport.tsx`, `OrbitState` um `ortho: boolean` + `orthoHalfHeight: number` erweitern, `orbitCamera()`/Preset-Effect/Pan/Zoom entsprechend anpassen) — Bug: Iso-Ansicht kippt bei der ersten Kamerabewegung sofort in Perspektive statt orthografisch (parallel) zu bleiben. War fertig gebaut und quantitativ verifiziert (Parallelogramm-Kantenvergleich vor/nach Orbit, 225/225 Tests grün), aber **nicht mehr committet**, bevor der Unterbruch kam. Kleinster, unabhängigster der drei Fixes — zuerst neu machen.
|
||||
2. **Joins Phase 1c — Durchgangswand echt aufbrechen** (`spanCutouts` in `src/model/joins.ts` + `src/plan/generatePlan.ts` + Rust-Parität `src-tauri/geometry/src/lib.rs`): Nutzer-Befund nach Phase 1b war, dass der Innenputz der DURCHGANGSWAND am T-Stoss nur überdeckt wird (Zeichenreihenfolge), nicht geometrisch ausgeschnitten — Fugen-/Nahflächenlinien laufen noch durch den Backstein-Durchstoss, es „liest" noch nicht wie ein einziger Join. Das war die **explizite Priorität des Nutzers** für diese Session. Die Umsetzung war weit fortgeschritten (schrieb bereits in `joins.ts`), dann abgebrochen. **Nicht committet, nicht im Baum.** Vollständige Aufgabenbeschreibung mit allen Datei:Zeile-Ankern steht im Transkript bei „Agent:Phase 1c: Durchgangswand aufbrechen" — 1:1 als Vorlage für den Neu-Auftrag wiederverwenden.
|
||||
3. **Öffnungen als echte Boolean-Löcher** (`RWall.holes`, `src/plan/toWalls3d.ts`, `src-tauri/render3d/src/mesh.rs`, additiv `types.rs`): EIN Wandkörper mit rechteckigen Löchern statt der heutigen Pfeiler/Brüstung/Sturz-Ersatzkörper (sichtbare Segment-Nähte, keine echten Löcher). Pflicht-Testfall (Nutzer-Direktive, Kernmotivation des Slices): zwei Fenster in derselben Wand mit überlappendem Achsen-Intervall, aber unterschiedlicher Höhe (z. B. u=[1.0,2.0]/z=[0.9,2.1] und u=[1.5,2.5]/z=[0.3,0.7]) — die alte Segment-Zerlegung kann das nicht abbilden, echte Löcher schon. Zusatz-Auftrag im selben Slice: `trimWallTopForCeilings` (Phase-3-Deckentrim, Commit `e4b8df6`) darf **nur** im 3D-Viewer-Pfad (`layeredWalls:true`) gelten, **nicht** im Schnitt-Pfad (`layeredWalls:false`) — der Schnitt braucht die volle, ungetrimmte Wandhöhe für seine eigene schichtweise Prioritäts-Subtraktion (`subtractDominantBands`), sonst zeigt der Schnitt die Wände fälschlich abgeschnitten. Fortschritt bei Abbruch **unklar** (letztes Status-Update deutete auf „noch in der Kartierungsphase" hin) — vermutlich am wenigsten weit von den dreien; komplett neu beauftragen. Vollständiger Auftragstext im Transkript bei „Agent:Oeffnungen als Boolean-Loecher" + die zwei Zusatz-Direktiven (Decken-Trim-Fix, Pflicht-Testfall versetzte Fenster) kurz danach.
|
||||
|
||||
**Zwei Engine-Direktiven vom alten Stand fehlen** (`priority-based-joins.md`, `openings-as-booleans.md`): die in Punkt 2/3 beschriebenen Zielverhalten waren gesichert (u. a. „Trennlinien an Merge-Kontakten verschwinden immer", „Öffnungen dürfen nie wieder zu Segmenten vereinfacht werden"). Beim Neu-Beauftragen diese Direktiven wieder explizit festhalten.
|
||||
|
||||
**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.
|
||||
|
||||
**Aufräum-Hinweis:** `Edit MEMORY md Added 1.txt` (lag unversioniert im Repo-Root) und die lokale `package-lock.json`-Änderung waren dirty/untracked. Die txt-Datei gehört nicht ins Repo — außerhalb verschieben oder löschen. `package-lock.json` prüfen (`git diff package-lock.json`) und nur committen, falls die Dependency-Änderung beabsichtigt ist.
|
||||
|
||||
### 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
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
## NACHTRAG Session 2 (2026-07-04 nachmittags/abends) — TopBar-Redesign + 3D-Editierbarkeit
|
||||
|
||||
**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×).
|
||||
|
||||
**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).
|
||||
|
||||
**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).
|
||||
|
||||
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.
|
||||
|
||||
## Konventionen (ZWINGEND — zuerst lesen)
|
||||
- **Keine Fremd-Tool-Spuren** im Repo: vor jedem Commit Trace-Scan (`git diff | grep -iE "co-authored|generated with"`) → muss leer sein. Commits deutsch, sachlich, KEINE Co-Authored-By-Trailer.
|
||||
- **NICHT ANFASSEN (fremde WIP):** `.gitignore`, `public/assets/materials/manifest.json`, `src/materials/library.ts`, `scripts/fetch-materials.mjs` — bleiben dauerhaft dirty, nie stagen.
|
||||
- **Verifikation:** `tsc --noEmit` clean + `vitest run` + trace-scan vor jedem Commit. Nur Task-Dateien stagen (Kollisionen: parallele Tasks disjunkte Datei-Lanes geben; i18n exklusiv einem Task).
|
||||
- **Engine = „Nordstern"** (render2d/render3d/WASM). Referenz DOSSIER-Rhino: `https://git.openbureau.ch/karim/dossier` (Alias git.kgva.ch), geklont `/tmp/dossier-ref`.
|
||||
|
||||
## 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`).
|
||||
|
||||
## 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.
|
||||
|
||||
## Verifikation
|
||||
`npx tsc --noEmit` + `npx vitest run` (zuletzt 152 grün) + trace-scan vor jedem Commit.
|
||||
|
||||
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
@@ -0,0 +1,300 @@
|
||||
# PENDENZEN — Aufgaben-Queue (Single Source of Truth)
|
||||
|
||||
> **Diese Datei ist die einzige verbindliche Aufgabenliste.** HANDOVER.md enthält nur
|
||||
> noch Kontext/Konventionen/Environment/Zielmodelle — keinen Backlog mehr.
|
||||
|
||||
## Arbeitsprotokoll (ZWINGEND)
|
||||
|
||||
**Bearbeiter:**
|
||||
1. **Zuerst diese Datei lesen**, dann `CONVENTIONS.md` + verlinkte Detailquellen des Items.
|
||||
2. Oberstes nicht-blockiertes Item aus **🔧 In Arbeit** bzw. sonst **⏭️ Als Nächstes** nehmen, **fertig** machen, verifizieren (`npx tsc --noEmit` + `npx vitest run` + trace-scan), abhaken, eine Ergebniszeile nach **✅ Erledigt** schreiben (mit Commit-Hash, sobald committet).
|
||||
3. **Rückfrage-Recht:** Bei Unklarheit/Design-Entscheid NICHT raten — Item mit `❓` markieren, kurze Frage darunter notieren, nächstes freies Item nehmen. Fragen sammeln sich unter **❓ Offene Rückfragen**.
|
||||
4. **Kein Arbeitsschritt endet ohne aktualisierte Liste.** Status hier ist immer aktuell, sonst driftet es wieder.
|
||||
5. Bearbeiter **committet nicht selbst** und delegiert Unteraufgaben nicht weiter.
|
||||
|
||||
**Planer** (mit dem Nutzer, kuratiert):
|
||||
- Nimmt Nutzer-Feedback, pflegt daraus die Liste (rein/umpriorisieren/aufspalten). Führt selbst keine Queue-Items aus.
|
||||
- Prüft **❓**-Zeilen mit dem Nutzer, wandelt sie in konkrete Items.
|
||||
|
||||
---
|
||||
|
||||
## ⛔ Blocker (zuerst klären)
|
||||
|
||||
- [x] ~~**Toolchain prüfen**~~ — **ERLEDIGT 2026-07-04 (macOS-Gerät):** `node`/`npm`/`cargo` (1.96.0)/`wasm-pack` (0.15.0) alle vorhanden. `npm install` + `npm approve-scripts esbuild wasm-pack …` (npm gated Install-Scripts → `allowScripts`-Block in `package.json`, ungetrackt gelassen), `rustup target add wasm32-unknown-unknown`, beide WASM-Engines gebaut, Frontend gebaut, **`npm run tauri:build` → Mac-App (`cad.app` + `cad_0.1.0_aarch64.dmg`, arm64) läuft.** Verifikations-Baseline grün: `tsc --noEmit` sauber, `vitest run` 230/230, `cargo test render3d` 56/56. **Kein Blocker mehr für Engine-Slices auf diesem Gerät.**
|
||||
|
||||
## 🔧 In Arbeit
|
||||
|
||||
- [ ] **GEO-BLOCK Folgepunkte (2026-07-12, nach dem swissBUILDINGS3D-DXF-Fix)** — Nutzer-Report, noch NICHT umgesetzt:
|
||||
- ✅ **Georeferenzierung (`ae47b4f` + `e61634c`):** `Project.geoAnchor` (persistenter Modell↔LV95-Referenzpunkt) ergänzt — der ERSTE Import in einem Projekt setzt ihn, ALLE folgenden Importe verwenden denselben Anker statt je eigenständig `makeOrigin(center)` neu zu berechnen. Zusätzlich neuer Befehl **„georef"** (Button „Neuer Bezugspunkt" in SitePanel): verschiebt per Klick das GESAMTE Projekt (`model/geoRebase.ts::translateProject`, reine Translation über alle Bauteile/2D-Geometrie/Kontext-Schicht/Schnittlinien) so, dass der geklickte Punkt zum neuen Modell-Ursprung wird — `geoAnchor` wandert automatisch mit, der LV95-Bezug bleibt exakt erhalten (rekonstruierbar, auch für einen künftigen georeferenzierten Export). Löst das vom Nutzer beschriebene Workflow: Kataster/3D zuerst importieren (landet ggf. weit vom Ursprung), dann selbst einen praktischen Bezugspunkt am Modell wählen. **Bewusst NUR Translation, keine Rotation:** `Roof.ridgeAxis` ist hart auf `"x"|"y"` beschränkt (keine frei gedrehten Dächer im Datenmodell) und mehrere Winkel-Felder (`Column.rotation`, Text-/Bild-Rotation, Türschwenk) müssten bei einer Drehung mitgeführt werden — eine Rotation, damit „orthogonal zur Parzellengrenze" gezeichnet werden kann, ist ein eigener, grösserer und riskanterer Schritt (Datenmodell-Erweiterung nötig), bewusst nicht mitgemacht. +17 Tests (`geoRebase.test.ts`, `georef.test.ts`), Suite 820 grün.
|
||||
- ✅ **Importierte Gebäude/Terrain sind jetzt anwählbare Meshes im 3D-Viewport (`e6a7738`):** Klick im WASM-3D-Viewport pickt Kontext-Meshes per Raycast (`raycast3d.ts`/`pickGeometry` in `toWalls3d.ts`), Auswahl bekommt einen eigenen Kanal (`selectedContextObjectIds`), Bounding-Box-Umriss als Highlight (volles Dreiecks-Wireframe wäre bei grossen Importen zu dicht), Entf-Taste löscht, `SitePanel`-Liste hebt die Auswahl hervor. **Bewusst NICHT umgesetzt:** die „auf einer Ebene"-Zuordnung (kein `categoryCode`/Layer-Feld an `ImportedMesh`/`TerrainMesh`, keine Sichtbarkeits-Kopplung an die Ebenen-Verwaltung) und keine volle Attribut-Bearbeitung (nur Name-Anzeige/Löschen, keine `deriveSelection()`-Integration ins Attribute-Panel — als zu gross für diesen Schritt eingeschätzt). **Ebenen-Zuordnung bleibt offen, ggf. eigener Design-Entscheid nötig** (eine feste „Import"-Ebene vs. frei zuweisbar).
|
||||
- ✅ **Zwei echte Bugs im swissBUILDINGS3D-Pfad behoben (`35a6834`+`9705890`):** (1) Absturz bei riesigen Kacheln (JSZip „Invalid string length" — DXF komprimiert stark, eine Kachel unter dem 150-MB-Limit kann entpackt trotzdem >700 MB Text ergeben; nur `asset.size` aus der STAC-API zu prüfen reichte nicht, jetzt zusätzlich die JSZip-interne Grössenschätzung + genereller Try/Catch, übersprungene Kacheln landen sichtbar in `skippedTiles`/UI-Meldung statt stumm 0 Gebäude). (2) **Der eigentliche „Import funktioniert nicht"-Bug:** die installierte `dxf-parser`-Version liefert Polyface-Mesh-Face-Indizes als vier einzelne Felder (`faceA`/`faceB`/`faceC`/`faceD`, Gruppencodes 71–74) statt als `faces`-Array — unser Code prüfte auf das Array, das nie existierte, jede swissBUILDINGS3D-DXF-Kachel ergab dadurch 0 Dreiecke. Live gegen echte Kacheln verifiziert: jetzt tausende Dreiecke mit realistischen Höhenwerten. **Damit ist der Gebäude-Import technisch lauffähig — die beiden Design-Punkte oben (Georeferenzierung, Ebenen-Zuordnung) bleiben offen.**
|
||||
|
||||
- [ ] **3D-Kanten-Folgepunkte ("Schattiert mit Kanten", 2026-07-12)** — nach dem Fix der falschen Flächendiagonalen (`7dc8f0d`) zeigte der Nutzer drei weitere Beobachtungen, NOCH NICHT behoben:
|
||||
- **Fenster-Rahmenecken überlappen statt sauber zu stossen** (Screenshot: doppelte/kreuzende Linien an den Blendrahmen-Ecken bei „fein"). Vermutlich KEIN Kanten-Rendering-Bug, sondern echte überlappende Geometrie — die Rahmen-/Sprossen-Riegel (`openingAxisBox` in `toWalls3d.ts`) sind separate Boxen ohne Gehrung/Union an den Stössen. **Nutzer-Folgewunsch dazu:** bei „fein" einstellbar machen, ob die Ecke vertikal-dominant, horizontal-dominant oder auf Gehrung gelöst wird.
|
||||
- **Dach ebenfalls kein sauberer Verschnitt am First/Grat** (Screenshot: kleines Störtriangle exakt am First-Apex). Gleiche Kategorie — mehrere Dachflächen (`emitRoofs`) sind separate, nicht verschnittene Meshes.
|
||||
- **Im OG viele vertikale Striche auf den Wandflächen** — noch NICHT diagnostiziert (nicht als Textur-Pipeline-Artefakt bestätigt, evtl. eine mehrschichtige Wandtyp-Verkleidung mit vielen dünnen Einzelelementen). **Braucht weitere Untersuchung**, idealerweise mit Angabe welcher Wandtyp/welche Schicht im betroffenen Projekt verwendet wird.
|
||||
- Alle drei sind vermutlich Fälle für eine echte Boolean-Verschneidung (Kandidat: `csgrs`/`boolean_mesh`, bereits als WASM-Export vorhanden aus der truck-Integration, s. u.) statt nur Kanten-Heuristik — grösserer, eigener Task.
|
||||
|
||||
- [ ] **Warteschlange „für danach" (Nutzer 2026-07-12, noch nicht begonnen):**
|
||||
- Luftbild/SWISSIMAGE wahlweise als Drapierung auf das Terrain-Mesh ODER als eigenständiges „Dokument" einfügbar (Bild/PDF generell als einfügbares Dokument-Objekt — noch keine Anforderungen präzisiert).
|
||||
- Import-Mesh (swissBUILDINGS3D u. Ä.) wahlweise geglättet („gerundet") oder kantig/eckig belassen, wählbar beim Import oder am Objekt.
|
||||
|
||||
- [ ] **truck-Integration (Profil-Extrusion / B-Rep)** (Plan: [docs/design/truck-plan.md](docs/design/truck-plan.md)). Ziel: Nutzer zeichnet 2D-Querschnitt → truck-Extrusion → 3D-Körper + später Boolean gegen Wand/Decke. Umfang gesamt: ~4–6 Wochen. Fortschritt:
|
||||
- [x] Phase 1: Geometrie-Schicht — Crate `src-tauri/trucksolid` (`extrude_polygon_core`/`extrude_circle_core`, `truck_modeling` nur zur B-Rep-Validierung/`try_attach_plane`/`tsweep`, Tessellierung manuell aus den Ring-Koordinaten — truck-rendimesh-Tessellierung für Solids in 0.3 nicht verfügbar, daher Abweichung vom Übergabe-Dokument), WASM-Bindings (Feature `web`) + `build:truck`-Script + `exclude`-Eintrag, TS-Wrapper `src/engine/truckSolid.ts` (`extrudePolygon`/`extrudeCircle`), `RMeshKind` um `"extrusion"` erweitert (`toWalls3d.ts`). Verifiziert: `cargo test` 5/5 grün, `npm run build:truck` sauber, `tsc --noEmit` 0 Fehler, `vitest run` 339/339 grün.
|
||||
- [x] Phase 2 (MVP, Nutzer-Entscheid 2026-07-06: **"Nur Viewer-Wiring"**, kein Werkzeug/Store) — Ende-zu-Ende-Beweis, dass die Pipeline bis ins Bild funktioniert: ein fest verdrahtetes L-Profil (`emitTruckFixture` in `toWalls3d.ts`, 2 m abseits vom Ursprung) wird bei Bedarf per `trucksolid`-WASM extrudiert und erscheint im 3D-Viewport. **Zwei echte Lücken dabei gefunden und geschlossen:** (1) `render3d::types::MeshKind` kannte nur `Terrain`/`Imported` — ein `kind:"extrusion"` ohne passende Rust-Variante hätte die serde-Deserialisierung des GESAMTEN Modell-Pushes zum Absturz gebracht (nicht nur die Fixture); `Extrusion`-Variante + warmes Orange als Default-Farbe ergänzt (`cargo test` render3d 58/58 weiterhin grün). (2) `updateModel(project)` läuft nur bei Projektänderung (`useEffect`-Dep `project`) — die asynchron ladende Fixture hätte trotz Erfolg NIE einen Re-Push ausgelöst; Fix via Browser-Event `TRUCK_FIXTURE_READY_EVENT` (`toWalls3d.ts` dispatcht, `Wasm3DViewport.tsx` hört + stösst `updateModel` erneut an). **Visuell verifiziert** (Playwright, `?engine=wasm`, Chromium mit `--use-angle=metal` für echten WebGPU-Adapter headed): orangene Extrusion sichtbar neben dem Demo-Haus im Viewport. `tsc --noEmit` + `vitest run` 339/339 + `cargo test` render3d 58/58 grün. **Noch uncommittet** (Bearbeiter committet nicht selbst).
|
||||
- [x] Phase 3: UI-Werkzeug + Store-Integration — **erledigt 2026-07-06:** `extrude`-Befehl (`src/commands/cmds/extrude.ts`, Alias „ex") nimmt Polylinie/Rechteck/Kreis als Profil (vorselektiert ODER Klick), Höhe tippen oder Enter für Default; Quell-Zeichnung wird beim Commit ENTFERNT (nicht dupliziert) — der Grundriss zeigt die Extrusions-Footprint-Kontur (`generatePlan.ts`, warmes Orange `#d98c40`) statt der alten Linie. `project.extrudedSolids`-Array statt Fixture. **Volle Auswahl-Parität mit Wand/Decke/Raum** (Nutzer-Entscheid „Voll: wie Wand/Decke/Raum"): eigener Selektionskanal, Klick-Priorität in `PlanView.tsx`, Attribute-Panel-Sektion (Höhe editierbar → löst Re-Extrusion aus, Grundfläche, Geschoss), Löschen, Ctrl+A, `move`-Befehl (`src/tools/transform.ts`). Kreis-Profil nutzt die echte `extrudeCircle`-WASM-Rundextrusion (kein N-Eck-Fallback) über eine 48-Eck-Tessellierung für 2D/Auswahl. Verifiziert: `tsc`/`vitest` 339/339 grün, Playwright-Durchlauf (zeichnen→extrudieren→auswählen→Höhe ändern→löschen) ohne Konsolenfehler. **Bewusst NICHT gebaut** (Konsistenz mit Raum/Decke/Treppe/Öffnung, die das auch nicht haben): Marquee-Auswahl, Klick-Drag-Verschieben. **Noch uncommittet.**
|
||||
- [x] Phase 4: Boolean gegen Wand/Decke — Mesh-CSG statt B-Rep-Boolean. **Spike 1, 2026-07-06 (Monstertruck-Fork getestet, nicht nur Recherche):** eigenes Rust-Testprogramm gegen `monstertruck-solid` 0.3.2 (Fork von `ricosjp/truck`, wirbt mit gehärteten Booleans) — der Crate-eigene Smoke-Test (echter Überlapp, versetzt in allen 3 Achsen) läuft sauber (`or`/`and` liefern exakte Volumina). Aber: **jede Konstellation mit deckungsgleichen/berührenden Seitenflächen schlägt fehl** — exakt der Praxisfall „Extrusion flächenbündig an Wand" oder „Extrusion mit gleichem Wandquerschnitt eingebunden" (gleiche y/z-Ausdehnung wie die Wand, nur in x versetzt): 0 Überlapp → `Internal{operation:"or"/"and"}`-Fehler; 1e-4 UND sogar 0.1 m sichtbarer Überlapp bei deckungsgleichen Seitenflächen → derselbe interne Fehler; 1e-9 Gleitkomma-Rauschen (winziger Spalt statt exakt 0) → **kein Fehler, aber STILL FALSCHES Ergebnis** (2 unverschmolzene Shells, Volumen = Summe statt Vereinigung). Sobald y/z zusätzlich leicht versetzt sind (keine deckungsgleichen Seiten mehr), funktioniert derselbe x-Überlapp einwandfrei. **Schluss: B-Rep-Booleans (truck/monstertruck) sind für diesen Projekt-Praxisfall (Extrusion mit Wandbreite/-höhe eingebunden) grundsätzlich ungeeignet** — teils Absturz, teils stiller Falsch-Wert.
|
||||
|
||||
**Spike 2, 2026-07-06 (`csgrs`, Mesh-Ebenen-CSG/BSP-Baum statt B-Rep):** strukturell anderer Ansatz — rechnet auf Dreiecks-Ebene statt über analytische Kurven/Flächen-Schnitte, damit unempfindlich gegen genau die koinzidenten-Flächen-Fälle, an denen truck/monstertruck scheiterten. Gegen dieselben Testfälle geprüft: generischer Überlapp exakt korrekt (`or`/`and`); flächenbündig (0 Überlapp) → `or` exakt 2.0 (korrekt verschmolzen, kein doppeltes Volumen), `and` liefert korrekt LEER (als Trimesh-Fehler statt sauberem `None` — muss beim Aufrufer als "leer" interpretiert werden); 1e-9-Rauschen → weiterhin exakt korrekt (kein falsches Doppel-Volumen wie bei monstertruck); 1e-4 winziger Überlapp → exakt korrekt; **der kritische Praxisfall (0.1 m Einbindetiefe, deckungsgleicher Wandquerschnitt)** → `or`/`difference` beide exakt korrekt. Jedes Ergebnis analytisch exakt (nicht nur "nah dran").
|
||||
- Packungs-Caveat: alle crates.io-Releases (0.16.0–0.20.1) sind wegen einer harten, zurückgezogenen `core2`-Abhängigkeit nicht installierbar — auf dem unveröffentlichten `main`-Branch bereits behoben. **Nutzerautorisiert** ("Git-Fetch für den Spike erlauben", danach "alles klar mach das" zur Umsetzung als echte Abhängigkeit): `trucksolid/Cargo.toml` pinnt `csgrs` auf Commit `5e7a37a8803d4e56617734687edc9b98f4ebeed7` (Git-Dependency, kein crates.io-Release — Risiko: kein durchnummeriertes/geprüftes Release, sollte bei Gelegenheit auf ein echtes `0.21.0`-Release umgestellt werden, sobald verfügbar).
|
||||
- **Umgesetzt:** `src-tauri/trucksolid/src/boolean.rs` — `boolean_mesh_core(a, b, op)` baut aus flachen Positions-/Indices-Arrays `csgrs::mesh::Mesh`-Polygone (`Polygon::new`/`Vertex::new`, Normale je Dreieck aus dem Kreuzprodukt, NICHT aus den Eingabe-Normalen — die Ebenen-Orientierung fürs BSP kommt aus der Vertex-REIHENFOLGE/Winding, nicht aus einem mitgelieferten Normalenfeld), ruft `union`/`difference`/`intersection` (Trait `csgrs::csg::CSG`) auf, tesselliert das Ergebnis zurück über `Triangulated3D::visit_triangles`. `empty: bool` im Output normalisiert den "Trimesh-Fehler bei leerem Schnitt"-Fall. WASM-Export `boolean_mesh` (Feature `web`, JSON-Schnittstelle wie `extrude_polygon`) + TS-Wrapper `booleanMesh()` in `src/engine/truckSolid.ts`. **Wichtige Voraussetzung für Aufrufer:** beide Eingabe-Meshes müssen konsistent nach AUSSEN gewundene Dreiecke haben (Rechte-Hand-Regel) — falsches Winding liefert ein falsches Ergebnis, OHNE Fehler zu werfen (im eigenen Test zunächst selbst hineingetappt: alle 6 Quader-Seiten der Test-Fixture waren invertiert, dadurch schlugen 3 von 4 Boolean-Tests fehl, bis das Winding korrigiert wurde — reiner Test-Fixture-Bug, nicht im Produktivcode).
|
||||
- Verifiziert: `cargo test` in `trucksolid` 11/11 grün (inkl. `wall_minus_embedded_extrusion_matching_cross_section`, `flush_touching_union_is_exact`), `cargo build --target wasm32-unknown-unknown --features web` sauber (csgrs + truck-modeling gemeinsam im selben WASM-Modul, keine Konflikte), `npm run build:truck` (echter `wasm-pack`-Build) sauber — `boolean_mesh` ist reell im generierten `.d.ts` exportiert.
|
||||
- **Noch offen (bewusst NICHT gebaut, eigene Scope-Entscheidung nötig):** die eigentliche Verdrahtung in die Live-3D-Szene (WANN soll eine Wand automatisch um eine eingebundene Extrusion gekürzt werden? Bei jeder geometrischen Überlappung? Nur bei explizit markierten Fällen?) ist eine UX/Scope-Frage, keine technische — analog zum Feld-Controller-Rückstand unten nicht blind entschieden, sondern auf Nutzer-Entscheid wartend. Aktuell bleibt eine Extrusion weiterhin ein unabhängiges, nicht mit der Wand verschmolzenes Solid im 3D (wie seit Phase 2/3).
|
||||
- [x] Phase 5: Verjüngung (Taper) — `taper` 0 (Prisma) … 1 (Spitze/Kegel-Pyramide), linear zum Profil-Schwerpunkt skaliert. Rust-Kern (`extrude_polygon_core`/`extrude_circle_core` in `lib.rs`), WASM/TS-Wrapper, dritte Befehls-Phase in `extrude.ts` (Höhe → Verjüngung, Enter für persistenten Default), `ExtrudedSolid.taper?`, Attribute-Panel-Feld (editierbar → Re-Extrusion). Tests: Kegel-Volumen (Divergenzsatz gg. analytisch), Pyramidenstumpf-Schwerpunkt-Skalierung, Range-Check.
|
||||
- [x] **Deep-Review + Fixes (2026-07-07, nach Geräte-Absturz der vorherigen Session):** 8-Winkel-Review (Korrektheit/Reuse/Simplify/Efficiency/Altitude/Conventions) über den gesamten uncommitteten Stand ergab mehrere echte Bugs, alle gefixt:
|
||||
- **Konkave Profile falsch trianguliert** (`trucksolid/src/lib.rs`): Deckel/Boden nutzten eine Fächer-Triangulierung ab Vertex 0 — korrekt nur für konvexe/von Vertex 0 aus sternförmige Polygone. Bei einem T-Träger (genau das im Plan genannte Zielprofil) erzeugte das nachweislich Phantom-Dreiecke quer durch die konkave Kerbe (nachgerechnet: 2 von 6 Deckel-Dreiecken lagen mit Schwerpunkt ausserhalb). Fix: Ohr-Clipping-Triangulierung (portiert von der bereits vorhandenen, robusten `render3d::mesh::triangulate`) ersetzt den Fächer; Schwerpunkt-Berechnung für die Verjüngung ebenfalls auf die korrekte flächen-gewichtete Formel umgestellt (vorher reiner Vertex-Mittelwert, bei asymmetrischen Profilen verzerrt). Neuer Regressionstest `t_beam_caps_stay_inside_polygon` (prüft, dass jeder Deckel-/Boden-Dreieck-Schwerpunkt im wahren Polygon liegt). `cargo test` trucksolid 15/15 grün.
|
||||
- **Auswahl-Zustand (`selectedExtrudedSolidIds`) an 6 Stellen in `App.tsx` nicht zurückgesetzt**, wo alle anderen Auswahl-Arten es bereits werden: Geschosswechsel-Effekt, `onOpenStampEditor`, die vier alten three.js-Viewport-Pick-Handler (Wand/Treppe/Decke/Öffnung), `onViewport3dPick`. Ohne Fix: Extrusion bleibt nach Geschosswechsel/anderer Auswahl "geisterhaft" mit-selektiert (falsches Panel, Löschen träfe das falsche Objekt).
|
||||
- **Mirror/Copy ignorierten eine reine Extrusions-Auswahl** (`hasSelection`/`toTransformSel` in `mirror.ts`/`copy.ts` kannten `extrudedSolidId` nicht, obwohl `transform.ts`/`move.ts` es längst unterstützen) → stiller No-op statt Spiegeln/Kopieren.
|
||||
- **DXF-Export verlor den Layer von Extrusions-Footprints**: da `extrude` die Quell-Zeichnung (mit `categoryCode`) entfernt, fiel `layerFor()` in `exportDxf.ts` auf `LAYER_DEFAULT` zurück. Fix: eigener `EXTRUSION`-Layer (gleiches Orange wie 2D/3D-Darstellung).
|
||||
- **`truckMeshCache` (toWalls3d.ts) ohne Eviction** — wuchs unbegrenzt über eine Session (Extrudieren→Kopieren→Löschen in Serie liess Mesh-Daten toter Körper im Speicher). Fix: gelöschte Ids werden bei jedem `emitExtrudedSolids`-Lauf aus dem Cache entfernt. **Gleichzeitig behoben:** sichtbares Flackern beim Tippen von Höhe/Verjüngung im Attribute-Panel (jeder Tastendruck = neue Signatur = Cache-Eintrag wurde sofort auf `mesh:null` gesetzt, bevor die neue Extrusion geladen war) — die alte Mesh bleibt jetzt sichtbar, bis die neue fertig ist.
|
||||
- **`fitTargetDist` im 3D-Viewport** (Wasm3DViewport.tsx) rief bei JEDER Projekt-Änderung (auch während eines laufenden Griff-Drags) unnötig die komplette Wand-/Öffnungs-/Decken-Flatten-Pipeline auf, nur um eine Bounding-Box für die Schnittebene zu ziehen — die aber nur bei aktiver Schnittebene gebraucht wird. Fix: Aufruf hinter `sectionActive` gegated.
|
||||
- **Nutzer-Report währenddessen:** Schnittebene-Umschalter (Wasm3DViewport.tsx) hatte `title`/`aria-label` hart auf Deutsch verdrahtet statt über `t()` — einzige Stelle im 3D-Viewport, die das i18n-System umging. Fix + Audit der ganzen App auf dasselbe Muster (title/aria-label/placeholder ausserhalb `t()`): genau eine weitere Stelle gefunden (`ColorHexField.tsx` Swatch-Tooltip „Farbe wählen"), beide jetzt über neue i18n-Keys (`viewport3d.section.*`, `attr.pickColor`).
|
||||
- Verifiziert nach jedem Schritt: `tsc --noEmit` sauber, `cargo test` trucksolid grün. `vitest run`/`cargo test render3d` am Ende der ganzen Fix-Reihe erneut komplett gegengeprüft (339/339, 58/58, 15/15).
|
||||
|
||||
- [ ] **kernel2d-Port nach Rust/WASM** (Plan: [PORT_PLAN.md](PORT_PLAN.md), Crate `src-tauri/kernel2d`, TS-Referenz bleibt `kernel2d.ts`, Differential-Harness `src/geometry/kernel2d.parity.test.ts`). Fortschritt:
|
||||
- [x] Phase 1: Crate-Skelett + WASM-Fassade + `build:kernel2d` — `a78e7c7`
|
||||
- [x] Phase 2: Primitive/Schnitt/Fläche/Kreis + Batch-Fassaden + Diff-Harness (Zufall+Golden) — `9e2c521`
|
||||
- [x] Phase 3: Offset (Miter + 1e-9-Fallback) + Fillet — `c8ea2bf`
|
||||
- [x] Phase 4: Trim/Split/Join (`trimPolyline`, `splitAtIntersections`, `joinChains` — Löwenanteil) — `28471c1`
|
||||
- [x] Phase 5: `roomArea`-Flächen/`ceiling`/`stair`/`roomBoundary` portiert (vitest 263, 33 Parity) — `a608a59`. ✅ **Phase 5 komplett (2026-07-07):** `opening` portiert (`wall_axis_length`/`opening_interval`/`wall_axis_frame`/`opening_jambs`/`opening_gap_quad`/`opening_center`/`door_symbol`/`window_symbol` + `along`-Helper, geflachte Signaturen nach PORT_PLAN; `openingVerticalExtent` bleibt bewusst TS — echte Modell-Kopplung an drawingLevels). +7 Parity-Tests (Suite 40 Parity). `cargo test` kernel2d 18/18, `build:kernel2d` sauber, vitest 348/348. Produktive Aufrufer unverändert (Phase 6 weiter offen/Nutzer-Entscheid). **Noch uncommittet.**
|
||||
- [ ] Phase 6: TS-Fassade umstellen (alt → `kernel2d.legacy.ts`), Suite grün, `npm run build` + WASM sauber. **Achtung Laufzeit-Entscheid:** macht die Live-App synchron WASM-abhängig (Init + Per-Call-Marshalling) — heute nutzt die App KEINE WASM-Geometrie zur Laufzeit; vor Umstellung mit Nutzer klären.
|
||||
- **Join-Durchstich verworfen (2026-07-05, gemessen):** `computeJoins` live auf WASM zu legen lohnt NICHT. Benchmark TS vs. WASM inkl. JSON-Marshalling (Median ms/Aufruf, 200 Iter): 6 W → TS 0.018/WASM 0.022 · 20 → 0.027/0.043 · 50 → 0.082/0.102 · 120 → 0.331/0.246 · 300 → 1.68/0.67. Crossover erst ~100+ Wände; realistische Plan-/Geschossmengen liegen darunter → TS schneller, und selbst 300 Wände sind mit TS 1,7 ms (nicht wahrnehmbar). Marshalling frisst den Rust-Vorteil, plus dauerhafte Rust↔TS-Paritätspflicht. **Fazit:** reines TS behalten; WASM für Joins nicht weiterverfolgen. WASM lohnt erst bei echten Rechen-Hotspots (Booleans/Tessellierung).
|
||||
|
||||
## ⚠️ Zu prüfen (evtl. schon erledigt / Status unklar)
|
||||
|
||||
- [x] ~~**Snap an Wand-Schichttrennlinien**~~ — **gelandet `339202b`** (`wallLayerBoundarySegments` in `src/tools/snapping.ts`), verifiziert vorhanden.
|
||||
|
||||
## ⏭️ Als Nächstes
|
||||
|
||||
- [ ] **BIM-Elemente „wirklich 1:1" (Tür/Fenster/Dach/Decke)** — volle Studie + Priorisierung: **[docs/design/bim-elements-depth-study.md](docs/design/bim-elements-depth-study.md)** (623 Zeilen, Referenzmatrix VW/ArchiCAD/Revit/Allplan gegen IST). ✅ **Bereits erledigt 2026-07-10:** Fenster-Schachtelung Blendrahmen→Flügelrahmen→Glas + fest/öffenbar + Einbaulage 2D (`9a38636`); Mansard-Untertypen Giebel/Walm/Zelt + Knick (`bfb80b3`); Dach-Dicke 3D (`e99bb24`) + **Dach-Schichtlogik RoofType/Layer[]** (`3a986ec`); zweiflügelige Türen leafCount 2D+3D (`4319e12`); getrennter Traufe/Ortgang-Überstand (`c9baff5`); Glas/glazingPanes 2D+3D + Rollladenkasten (`2e13ec3`/`d131683`). **Offen (nach Doc-Priorität):**
|
||||
- [x] ~~**Decken-Aussparungen**~~ — **erledigt 2026-07-10 (`37f4fe8`+`cdbef99`+`aee8846`+`83dcdd0`):** `Ceiling.openings?: Vec2[][]` + **Brücken-Trick** (`mergeHoles` in `src/geometry/polygonHoles.ts`: Löcher über schmale Brücken in den Aussenring → EIN einfacher Ring, paritätskorrekt für SVG/Hatch/Ear-Clipping/Pick — KEIN Loch-Support in den Primitiven nötig, kein Rust-Change). 2D-Poché-Loch (Brückenkanten via noStrokeEdges) + 3D-Slab-Loch; Befehl „Deckenloch" (Aliase deckenloch/aussparung, BIM-Ribbon); Panel listet Aussparungen mit Entfernen. +11 Tests.
|
||||
- [x] ~~**Dach in 2D-Aufsicht + Vertikalschnitt**~~ — **erledigt 2026-07-10 (`9a00c8e`+`f248e2a`):** Grundriss-Aufsicht trägt die Ansichts-Schraffur (viewHatchId) der Eindeckungs-Schicht; Vertikalschnitt via `appendRoofSections` (analytischer TS-Schnitt: Cyrus–Beck gegen die Aufsichts-Polygone, lineare Oberkante, Dicke/cos(Neigung), Schicht-Bänder aussen→innen mit Bauteil-Schraffur). **Offen:** Ansichts-/Silhouettenkanten des Dachs in Elevationen (nur Schnitt-Polygone bisher).
|
||||
- [x] ~~**RoofType-ResourceManager-Tab**~~ — **erledigt `83abc1d`** (RoofStylesTab auf LayeredStylesTab-Rumpf; anlegen/editieren/löschen, Löschen geschützt bei Verwendung).
|
||||
- [ ] **Decken-Randschicht-Override** (`Ceiling.edgeOverride`, Ringzone anderer Aufbau, Tropfkante/Randdämmung) — P1; **Deckentrenn-Werkzeug** für thermisch getrennte Auskragung (Isokorb, UI-only) — P1.
|
||||
- [ ] **Tür 1:1 Rest:** `glazingRatio` (Teilverglasung), Kassettentür-Geometrie, `frameKind` im 3D (Zarge vs. Blockrahmen), echtes Schwellenprofil; Alt-`Door[]`-Pfad in `Opening` konsolidieren (technische Schuld, zwei Datenwege).
|
||||
- [ ] **Fenster 1:1 Rest:** asymmetrische Rahmenbreiten, Bank/Nische, Laibungsverkleidung, Form (Rund/Spitz/Schräg). ~~echtes Sprossengitter~~ ✅ **erledigt 2026-07-12 (`00bff27`):** `mullionCols` (vertikale Sprossen-Spalten) spiegelbildlich zu `mullionRows` — 3D-Rahmen-Riegel, Ansichts-Trennlinien + Glasscheiben als echtes rows×cols-Raster, Eingabefelder in `OpeningEditorDialog`/`ResourceManager`.
|
||||
- [ ] **Dach 1:1 Rest:** Kniestock/Drempel, Krüppelwalm, Kehlen bei L-Grundriss (Straight-Skeleton), Gauben/Dachfenster, Aufschieblinge. ~~Dach im Vertikalschnitt~~ ✅ `f248e2a`.
|
||||
- [ ] **Geneigte Decke** (Rampe/Gefälle), Deckenspiegel (zweite abgehängte Fläche) — P2/P3.
|
||||
- **Nicht bauen** (Doc §6): VW-Massketten-Apparat, volle Sichtbarkeitsmatrix, Eck-Fenster/-Dach generisch, Beschlags-Produktbibliothek, IFC-Void-Semantik nachrüsten.
|
||||
|
||||
- [ ] **`make2D`-Befehl (Sicht → 2D-Zeichnung mit Füllungen)** (Nutzer-Wunsch 2026-07-10). Aus der AKTUELLEN Sicht — egal ob Grundriss, Schnitt oder 3D — eine flache 2D-Zeichnung aus reinen 2D-Geometrien erzeugen (Linien + Füllungen/Schraffuren, „mit allem"). Zwei Ausgaben: (a) als neue `Drawing2D`-Elemente ins Modell einfügen (auf einer Ziel-Ebene), ODER (b) in die Zwischenablage kopieren (SVG/DXF-Fragment) zum Einfügen anderswo. Vorbild: Vectorworks „2D-Darstellung erzeugen" / Rhino `Make2D`. **Bausteine vorhanden:** Grundriss/Schnitt laufen bereits über `generatePlan()`/`generateSectionPlan()` → RScene → SVG (`sceneToPrintSvg`); für 3D braucht es eine Projektion (HLR/Silhouette) der `projectToModel3d`-Meshes auf die Bildebene (neuer Teil). MVP: Grundriss/Schnitt → Drawing2D + Clipboard; 3D-Projektion als zweite Phase. Scope/Format (SVG vs. DXF vs. native Drawing2D) mit Nutzer schärfen.
|
||||
|
||||
- [x] ~~**Dächer: Auswahl + Attribut-Editieren + Löschen**~~ — **erledigt (`80121a3` + Folge-Commits `7cfdf59`/`eaf57e2`/`64f6179`/`826685c`):** Klick-Auswahl (Traufe-Pick-Polygon + pickRoof), `selectedRoofIds`/`updateRoof`/`RoofInfo`/`roofSelection`; RoofSection-Panel voll editierbar (Form, Firstrichtung X/Y, Neigung(en), **Breite/Tiefe**, Überstand, Dicke, **Traufhöhe**) + Firsthöhe/Fläche read-only; **Auswahl-Hervorhebung im 2D UND 3D** (Draht-Umriss); Löschen; Abwählen an ALLEN Reset-Stellen. **Optional Folge:** ~~3D-Griffe zum Ziehen (Traufe/First)~~ ✅ **erledigt `4ef40a0`** (Eckpunkt-Resize + Verschieben + First-Griff für Neigung). **Noch offen:** Dachfenster, Kehlen bei nicht-rechteckigem Grundriss (Straight-Skeleton).
|
||||
|
||||
- [ ] **Ribbon-UI + modulare Bars** (Nutzer-Vision 2026-07-05, bestätigt: Tab-Schema **2D·3D·BIM·Ansichten**). Voller Plan + datengetriebene Architektur: **[docs/design/ribbon-ui-plan.md](docs/design/ribbon-ui-plan.md)**. Ribbon-Oberleiste mit Tabs ersetzt die Werkzeug-Sidebar; datengetriebene Registry (RibbonItem = tool|command|action) ermöglicht auch eine **modulare Custom-Bar** (gleiche Items, vom Nutzer gewählt). Attribute bekommen volle Höhe, Objektinfo darunter gemergt; XYZ-Box oben rechts bleibt. **Phasen:** ~~(1) Gerüst + 2D/BIM-Tab~~ ✅ (`9d6e86d`), ~~(2) TopBar → Ansichten-Tab mergen~~ ✅ (`85011cb`), ~~(2b) Tabs in die TopBar-Zeile~~ ✅ (`456ebc8`), ~~(2c) OCS-Chrome (kleine Wortmarke + Quick-Access-Icons statt Burger, Zeile 26px)~~ ✅ (`4e3b074`), ~~(2d) Text + Ansichten auf eine Leiste, „Ansichten" als Standard-Tab zuerst~~ ✅ (`fe22cbf`), (3) **teilweise** ✅: Werkzeug-Sidebar aus Default-Layout raus + Attribute volle linke Höhe + Wandtyp-/Deckentyp-Picker ins Attribute-Panel verschoben (Nutzer-Entscheid), `LAYOUT_VERSION`→8; ~~**offen:** Objektinfo unter Attribute mergen~~ ✅ (`3a2cef3`): element-spezifische Abschnitte (Wand/Decke/Öffnung/Treppe/Raum) + Wand-Referenzlinie ins Attribute-Panel verschoben; ObjectInfo trägt nur noch Bezugspunkt + Masse (Nutzer-Wunsch). ~~(4) modulare Custom-Bar~~ ✅ (Tab „Eigene" + +-Picker, localStorage-persistiert). **Offen:** 3D-Tab füllen (aktuell leer; ggf. 3D-spezifisch statt Doppelung mit Ansichten), Band-Höhe/Abstände + 26px-Zeile visuell im Tauri abnehmen, Dauer-Zoom-Anzeige (Statusleiste?) klären. ✅ Vorarbeit: Eigenschaften-Grid im OCS-Stil (`00733d8`), Kreis+Bogen-Werkzeuge (`e454eab`/`bd2b12b`). **Nächster Schritt: im Tauri visuell prüfen (Band-Höhe, Icons, Aktiv-Highlight), dann Phase 2/3.**
|
||||
|
||||
- [x] ~~**SPIKE — Bild-Texturen in `render3d`**~~ — **erledigt `0ca3b1d`** (verifiziert 2026-07-05): `RenderStyle::Textured` real, prozedurales 256×256-Schachbrett (kein Asset/`image`-Crate), UVs planar in Metern, Textur-Bind-Group group 1, `MESH_TEXTURED_WGSL` (gleiche Beleuchtung, Albedo aus `textureSample`), `spike3d` per `T` umschaltbar. Alt-Vertexpfad `[pos,normal,color]` bitgleich (Regressionstest). `cargo test` 58 grün (59 mit `--features render`, inkl. naga-Test); `spike3d`-Build sauber; keine neuen Deps, Default-Build unverändert. Auftrag: [SPIKE_TEXTUR_render3d.md](SPIKE_TEXTUR_render3d.md). **Lücken bis „richtig gutes" Texturing → siehe 3D-REST unten.**
|
||||
|
||||
## 📋 Backlog (Priorität grob absteigend)
|
||||
|
||||
- [x] ~~**Shift-Ortho beim Körper-Verschieben (2D) fehlte**~~ — **erledigt 2026-07-06:** Nutzer-Report — Shift zum H/V-Einrasten wirkte beim Ziehen eines Vertex-Griffs (`onGripMove`) und beim freien Kanten-Zug (`onEdgeMove`), aber NICHT beim Verschieben eines ganzen Elements per Körper-Griff (`onMoveBody` — Wand/2D-Element/Decke/Treppe/Raum als Ganzes): `PlanView.tsx` reichte `mods` an dieser einen Stelle schlicht nicht durch. Fix: `GripHandlers.onMoveBody` bekommt optionales drittes Argument `mods: ToolMods`, `App.tsx` zwingt bei `mods.shift` das Delta auf die dominante Achse (H/V) — dieselbe Regel wie beim freien Kanten-Zug. `tsc`/`vitest` 339/339 grün.
|
||||
- [ ] **Feld-Controller (getippte Länge/Winkel relativ zum Ausgangspunkt) fehlt beim Körper-Verschieben + im 3D.** Nutzer-Wunsch 2026-07-06: „allgemein brauchen wir ein System bei 2D- und 3D-Elementen, dass wenn man einen Punkt anwählt, man ihn verschiebt — mit der Option, Länge und Winkel relativ zur alten Position zu wählen." Bestandsaufnahme: **existiert bereits** für den 2D-Vertex-Griff-Drag (`gripEditRef`/„Feld-Controller", Tab öffnet/zykelt Länge↔Winkel-Sperre, `gripEditPoint` in `App.tsx`) — fehlt aber (a) beim 2D-Körper-Verschieben (`onMoveBody`, ganzes Element ziehen) und (b) komplett im neuen 3D-Griffsystem (`Wasm3DViewport.tsx`, s. „3D-Griffe/Editieren" oben — weder Vertex- noch Verschiebe-Griff haben dort eine Locks/HUD-Eingabe). Für (b) zusätzlich zu klären: wie eine getippte Zahl im 3D-Viewport erfasst wird, ohne mit der freien Maus-Navigation (Orbit/Pan) zu kollidieren — eigenes UI-Element (z. B. ein kleines Eingabefeld neben dem gezogenen Griff-Button) naheliegend, aber nicht mit dem 2D-HUD-Muster identisch übertragbar. Mit Nutzer Scope/Reihenfolge klären (2D-Körper zuerst, da mechanische Erweiterung des bestehenden Feld-Controllers; 3D danach als grössere UI-Frage).
|
||||
- [x] ~~**Zeichenwerkzeuge ergänzen: Kreis + Bogen.**~~ — **komplett erledigt (2026-07-05):** beide Werkzeuge + Center-/Quadrant-Snaps + WebGL-Sichtbarkeitsfix (`2c8ad8f`). Details in den Unterpunkten:
|
||||
- [x] ~~**Kreis-Toolbar** (trivial)~~ — **erledigt `e454eab` (2026-07-05):** `ToolId`+`"circle"`, Platzhalter `circleTool` (nicht floorOnly), `TOOL_COMMAND`+`TOOL_ORDER`, Kreis-Icon in `ToolsPanel`, i18n `tool.circle`/`tool.circle.hint`. tsc + Suite 331 grün. (Kreise rendern seit `4ac99d3` glatt als `<circle>`.)
|
||||
- [x] ~~**Bogen-Werkzeug** (mittel)~~ — **erledigt (2026-07-05):** `arcCommand` in `src/commands/cmds/arc.ts` (3-Klick: Mittelpunkt → Start/Radius → Endwinkel, CCW; Vorschau via `arcPts`/`circlePts`), registriert in `registry.ts` (Alias `a`/`bogen`), `"arc"` als ToolId + Toolbar-Eintrag (Bogen-Icon) + i18n. tsc + Suite 331 grün. ✅ **Center-/Quadrant-Snaps ergänzt (2026-07-05):** `collectCircles` + Snap-Block in `snapping.ts` — Mittelpunkt + Quadranten (Kreis: alle 4; Bogen: nur im Spannbereich) unter der `center`-Einstellung, Bogen-Endpunkte unter `endpoint`; +3 Tests (Suite 334).
|
||||
|
||||
- [ ] **BAUTEILE aufs Rhino-Niveau heben (Treppe/Fenster/Tür).** Vergleich Rhino-Plugin ↔ TS + priorisierte Ansätze: **[RESEARCH_BAUTEILE_RHINO.md](RESEARCH_BAUTEILE_RHINO.md)**. ✅ **Gruppe A (2D, Items 1–6) komplett — verifiziert 2026-07-05:** (1) Treppe-Outline (`stairOutline`, gerade/L/Wendel, `generatePlan.ts:2186`), (2) Fenster-Brüstungslinie (`window-sill`, gepunktet, `sillHeight>0`, `:1810`), (3) Tür-Sturzlinien (`door-lintel` + `lintelLines` keine/innen/aussen/beide, gestrichelt, `:1711`), (4) Treppe-Referenz links/mitte/rechts, (5) Fenster-Flügel-Mittelpfosten, (6) Tür `wandoeffnung`. **Offen:** Gruppe B (2D mittel: Tür-Schwung am Rahmen, Fenster/Tür-Presets) — Feinpolish. ✅ **`swing_invert` gilt als abgedeckt** (2026-07-17): die Aufschlagseite ist über `Opening.swing` (links/rechts) UND der Scharnier-Pfosten über `Opening.hinge` (start/ende) bereits voll im Objekt-Info wählbar (4 Kombinationen = jede Schwenklage), ein separater Invert-Flag wäre redundant. ✅ **Treppen-Pfeil-Style `filled` erledigt 2026-07-17** (uncommittet): `Stair.arrowStyle?:"line"|"filled"` (additiv), gefülltes Dreieck-Polygon statt zwei offener Linien in `addStairSymbol` (`generatePlan.ts`), Segment-Umschalter im Objekt-Info (`onSetStairArrowStyle`), `StairInfo.arrowStyle`, +2 Tests (`generatePlan.stairArrow.test.ts`). tsc/vitest 869 grün. ✅ **Fenster-Anschlag-Striche erledigt** (`8d688b9`, Laibungsstriche quer zur Wand bei „fein", analog Tür; +2 Tests). Gruppe C (3D: Rahmen/Blatt/Glas/Sims als Mesh — wartet auf Mesh-/B-Rep-Pipeline). **Der große Rest ist „Schnitt- vs. Ansichts-Darstellung" (eigenes Item unten).**
|
||||
- [ ] **DWG/DXF-Import via `acadrust` (weiterbauen).** ✅ Spike `763a558`: `acadrust` 0.4 (MPL-2.0, pure Rust) **baut zu wasm32** (Crate `src-tauri/dwgimport`, 839 KB), parst DXF aus Byte-Buffer (`DxfReader::from_reader`+`Cursor`), headless getestet. **Offen:** (1) Entity→DOSSIER-Modell-Mapping (LINE/ARC/… → Wand/Öffnung — die eigentliche Domainarbeit, Wochen), (2) Datei-Upload-Glue im Browser (`<input type=file>`→Uint8Array→`parse_dxf_summary_json`, trivial), (3) DWG-binär (`DwgReader::from_reader` analog, aber R13–R2018-Korrektheit unverifiziert), (4) WASM-Größe (nalgebra Haupttreiber). **Klarstellung:** der TS-DXF/DWG-Import (`parseDxf`/`parseDwg`/`dxfToDrawings`) + Upload-UI (`App.tsx`, `ImportDialog.tsx`) existieren längst und funktionieren — der acadrust-Weg wäre eine Rust-Neuimplementierung des Lesens (nur DWG-**Schreiben** ist eine echte Lücke). ✅ **2887794 (2026-07-05): Kurven-Abdeckungslücke geschlossen** — `parseDxf` deckt jetzt ARC/CIRCLE/ELLIPSE (tesselliert zu Konturen, Winkel Radiant, voller Umlauf geschlossen) zusätzlich zu LINE/LWPOLYLINE/POLYLINE/MESH ab; 5 Tests, volle Suite 307 grün. ✅ **c481373 (2026-07-05): SPLINE + INSERT ergänzt** — `parseDxf` wertet SPLINE als echte B-Spline (De Boor, Grad/Knoten; Fallback fitPoints/Kontrollpolygon) aus und expandiert INSERT-Block-Referenzen (2D-Transform Scale/Rotation/Basispunkt + MINSERT-Array + verschachtelte Blöcke, Tiefe ≤8) zu transformierten Konturen; Kontur-Dispatch in gemeinsamen `collectContours` refaktoriert; +7 Tests, volle Suite 314 grün. **Bekannte Grenzen:** rationale SPLINE-Gewichte ignoriert (dxf-parser liefert sie nicht); Block-interne MESH/3DFACE-Entities werden im 2D-Import nicht expandiert. ✅ **c29f27e (2026-07-05): HATCH ergänzt** — dxf-parser hat KEINEN HATCH-Handler (verwarf HATCH stumm); Lösung via `registerEntityHandler` + eigenem `HatchHandler` (sammelt rohe Gruppencodes) + testbarer `hatchContours`-Auswertung: Randpfade (Polyline-Pfade + Linien-/Bogen-Kanten, Bögen über vorhandene Tessellierung) → geschlossene Konturen mit `Contour.filled`; `contoursToDrawings` macht daraus gefüllte `polyline`-Drawing2D (fillColor-Default, restylebar). +6 Tests, Suite 320 grün. ✅ **05bc5aa (2026-07-05): HATCH-Ellipse/Spline-Kanten** ergänzt (Kantentyp 3/4 tesselliert; B-Spline-Sampling in `sampleBSpline` extrahiert). ✅ **4b93ac9 (2026-07-05): TEXT/MTEXT** — `parseDxf` liefert `DxfImportResult.texts` (`ImportedText`: Position/Höhe-in-Metern/Winkel-Radiant; MTEXT-Formatcodes grob gesäubert); `textsToDrawings` → `{shape:"text"}`-Drawing2D; **Darstellung neu**: `addDrawing2D` emittiert ein schlankes `kind:"drawingText"`-Primitiv, PlanView rendert es rein per SVG (modellverankert, Rotation; GPU-Guard so, dass es in ALLEN Renderer-Modi im SVG bleibt); `toRenderScene` überspringt es; ImportDialog zählt/importiert Texte. +5 Tests, Suite 327 grün. **Bekannte Grenzen HATCH:** Bulges an Polyline-Rändern als Sehne; Insel-Loops = eigene Ringe (keine echten Löcher). **Bekannte Grenzen TEXT:** importierte Texte (pointerEvents:none) noch nicht per Canvas-Klick selektierbar; MTEXT-Feinformatierung flachgeklopft; Block-interne TEXT/MTEXT nicht expandiert. **Text im Tauri visuell abgenommen (Nutzer 2026-07-05)** — auch gedreht korrekt. ✅ **4ac99d3: CIRCLE/ARC als echte glatte Formen** — `Contour.curve` trägt die wahre Kreis-/Bogen-Geometrie (pts bleiben für Kontext/3D); `contoursToDrawings` baut `{shape:"circle"|"arc"}`; neue Primitive `drawingCircle` (SVG `<circle>`) + `drawingArc` (SVG-Bogenpfad), `toRenderScene` tesselliert sie für den nativen Pfad; +4 Tests, Suite 331. Ellipse bleibt tesselliert (kein Ellipsen-Primitiv). **Weiter offen:** Entity→Wand-Semantik (die dicke Domainarbeit); DWG-Schreiben (einzige echte Export-Lücke).
|
||||
- [ ] **STRATEGIE — „von BIM-Tool zu echtem CAD".** Direkt am Quellcode studierte Referenzen (OpenCADStudio/truck/acadrust) + Web-Import-Landkarte → konkrete, priorisierte Ansätze in **[RESEARCH_CAD_APPROACHES.md](RESEARCH_CAD_APPROACHES.md)**. Kern: (1) generisches Entity-Modell + Trait-Dispatch, (2) modeless Command-System (`StepInput`-Funnel + Kommandozeile), (3) DWG/DXF-Round-Trip via `acadrust` (MPL-2.0, pure Rust, WASM-tauglich), (4) volle Object-Snap-Schicht, (5) 3D-B-Rep später selektiv via `truck` (Apache-2.0, Geometrie-Crates WASM-fähig, Booleans/Fillets noch instabil). Der `kernel2d`-Rust/WASM-Kurs ist damit bestätigt. **Mit Nutzer priorisieren, welcher Ansatz zuerst.**
|
||||
|
||||
- [ ] **Schnitt- vs. Ansichts-Darstellung nach Schnitthöhe (BIM-Standard).** ✅ **Phase 1 (Wand unter Schnittebene) erledigt (2026-07-05):** `generatePlan.addWallPoche` hat einen `viewOnly`-Zweig — erreicht eine Wand die Grundriss-Schnitthöhe nicht (`wall.height < floor.cutHeight`, z. B. 0.3-m-Brüstung bei 1 m), wird sie nur als Ansichts-Umriss (Haarlinie, `fill:"none"`, keine Schraffur) gezeichnet statt als Schnitt-Poché; normale Wände (≥ Schnitthöhe) unberührt. Segment-/Gehrungs-/Öffnungs-Logik geteilt. +2 Tests (`generatePlan.viewwall.test.ts`, Suite 336). ✅ **Phase 2 (Decke über Ebene = gestrichelte Überkopf-Linie) erledigt (2026-07-05, `39ddd9b`):** der freie Decken-Umriss (Überstände/Balkone, wo keine Wand verdeckt) ist jetzt gestrichelte Haarlinie (`OVERHEAD_DASH`) statt kräftiger Volllinie — BIM-Konvention „Aufsicht auf Bauteil über einem". Decken-FLÄCHE war schon Ansicht (viewHatchId, weiß). +1 Test. **Offen (Phase 3):** per-Component View-/Cut-Weight + eigene View-Schraffur (Slot-Trennung); cut/Aufsicht/Untersicht (Deckenspiegel/Reflected Ceiling Plan); Unterzüge; Feinheiten. Kern-Idee (Nutzer): jedes Bauteil bekommt getrennt eine **Schnittlinie** (kräftig, z. B. 0.25–0.35 mm, + Schnitt-Poché) und eine **Ansichtslinie** (Haarlinie); WELCHE gilt, entscheidet die z-Ausdehnung des Bauteils vs. die **Schnitthöhe** des Grundrisses (Default ~1 m):
|
||||
- Bauteil wird von der Schnittebene GESCHNITTEN → Schnittlinie + Schnitt-Poché (heutiges Wandverhalten).
|
||||
- Bauteil liegt ganz UNTER der Ebene (z. B. 30-cm-Wand bei 1 m Schnitthöhe, Brüstung, Podest) → nur „von oben gesehen" = **Ansichtslinie/Haarlinie, KEINE Schnitt-Poché**. ← genau der vom Nutzer genannte Fall.
|
||||
- Bauteil liegt ganz ÜBER der Ebene (Decke/Slab, Unterzug) → Ansicht, üblicherweise **gestrichelte** Überkopf-Haarlinie.
|
||||
- **Passt konsistent zum bereits existierenden `viewHatchId` (Ansichts-Schraffur) vs. Schnitt-Schraffur** — die Linienstärke-Dualität (View-/Cut-Weight je Component) ist die natürliche Erweiterung derselben Logik.
|
||||
- **Nicht nur cut/view, sondern cut / AUFSICHT / UNTERSICHT (Nutzer):** ein Bauteil sieht von oben anders aus als von unten. Der Grundriss (Blick nach unten) zeigt Bauteile UNTER der Ebene in **Aufsicht** (Oberseite); ein **Deckenspiegel/Reflected Ceiling Plan** (Blick nach oben) zeigt Bauteile ÜBER der Ebene in **Untersicht** (Unterseite — z. B. Kassettendecke, Leuchten). Also je Component potenziell **drei** Darstellungs-Slots (Schnitt / Aufsicht / Untersicht) × {Schraffur + Linienstärke}. Das heutige `viewHatchId` ist faktisch EIN View-Slot und vermischt Auf-/Untersicht; sauber wäre die Trennung. WELCHER Slot gilt, entscheidet die **Blickrichtung der Sicht** (Grundriss ↓ / Deckenspiegel ↑) UND die z-Lage relativ zur Schnittebene.
|
||||
- **Verallgemeinert den Decken-Footprint-Clip** (`a2f6923`): „Decke unter Wand verdeckt" ist ein Spezialfall von „Ansichtsbauteil vs. schneidende/überdeckende Bauteile".
|
||||
- Aufwand: mittel–groß, phasenweise machbar (1: z-Extent-vs-Schnitthöhe-Klassifikation cut/above/below; 2: Ansichtslinie-Weight je Component + Haarlinie/gestrichelt; 3: 30-cm-Wände & Slabs verdrahten). Nicht zu komplex im Konzept — es ist der reguläre CAD/BIM-Weg (ArchiCAD/Vectorworks/Revit). Mit Nutzer Detailgrade/Defaults festlegen.
|
||||
|
||||
|
||||
- [x] ~~**Ebene-Schraffur editierbar**~~ — **gelandet `dcb6ed5`**: Kategorie-Dialog (`App.tsx`, `editor.hatch`-Feld) hat `<select>` auf `cat.hatch` + `HatchSwatch`-Vorschau. Verifiziert vorhanden.
|
||||
- [ ] **GEO-BLOCK** (gemeinsame Dateien io/geoContext/swissTopo/terrain/ContextImportDialog/Viewport3D/siteSlice):
|
||||
- ✅ **swissBUILDINGS3D 1:1 + swissALTI3D-Terrain erledigt 2026-07-12 (`ed724be`):** Root Cause war `swissTopo.ts::fetchBuildings` (generalisierter 1:25'000-Kartografie-Layer, nur Grundriss, `DEFAULT_BUILDING_HEIGHT=9`-Pauschalkiste) + grobe `profile.json`-Terrain-Näherung. Fix: neues `stacApi.ts` (gemeinsamer STAC-Client) + `swissBuildings3d.ts` (echte Wände/Dach-Meshes aus swissBUILDINGS3D-DXF-Kacheln, Generation **2.0 (stabil) UND 3.0 (Beta)** wählbar, über den bestehenden `dxfParser.ts` eingelesen — kein neuer Parser nötig) + `swissAlti3d.ts` (echtes swissALTI3D-Höhenraster, **0.5 m/2 m** wählbare Punktdichte statt Näherung). `ContextImportDialog.tsx` entsprechend erweitert (Gebäude-Modus aus/vereinfacht/2.0/3.0, Terrain-Auflösung). Referenz war das Rhino-Vorgänger-Plugin (`git.kgva.ch/karim/DOSSIER`, `rhino/swisstopo.py`). Pipeline nutzt **render3d** (nicht Three.js, Nutzer-Entscheid 2026-07-12 „scheiss auf three.js darstellung render3d ist fokus").
|
||||
- ~~**Nordstern-Geo-Rendering**~~ — **war bereits erledigt** (`35299307d`, 2026-07-09, stand hier fälschlich noch als offen): `emitMeshes` in `toWalls3d.ts` liest `project.context` (`importedMesh`/`terrainMesh`) bereits und speist sie in `projectToModel3d` ein — keine weitere Verdrahtung nötig, war Voraussetzung für den swissBUILDINGS3D-Fix oben und stand schon.
|
||||
- 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.
|
||||
- **3D-Mesh-DXF/DWG-Import** (heute DXF nur 2D bei manuellem Import — der Mesh-Pfad selbst ist über `dxfParser.ts` schon 3D-fähig, s. o.); Building-Draping; höhere DTM-Auflösung.
|
||||
- [x] ~~**ResourceManager Bauteile-Tab** auf Master-Detail~~ — **bereits Master-Detail** (`ComponentsTab`/`ComponentDetail` in `src/ui/ResourceManager.tsx`, Liste links `res-md-list` / Detail rechts). Verifiziert vorhanden.
|
||||
- [x] ~~**Einstellungs-Fenster (Rest)**~~ — **erledigt:** Verdrahtung war schon **da** (`viewSlice.snapColor`/`marqueeColor` → PlanView `SnapMarker`/Marquee, Defaults aus `theme/accents.ts`, Projekt-MüM-Feld `referenceElevationMasl`); der einzig offene Punkt (Snap/Endpunkt-Default „aki") ist längst entschieden (2026-07-04: Sora #5FA1C9, s. „❓ Offene Rückfragen"). Zeile war stehen geblieben, obwohl die Frage schon geschlossen war.
|
||||
- [ ] **Bildschraffur:** ambientCG/CGI-Colorfiles als Quelle (Material-Lib WIP — erst nach Freigabe); Bild-Filter Sättigung/Helligkeit/Kontrast/SW (`image.filters`).
|
||||
- [x] ~~**Tragwerk-Start: Stützen (Column) — MVP end-to-end**~~ (DOSSIER-Audit A4) — **erledigt 2026-07-07:** `Column`/`ColumnProfile` (rect/round) als platzierte Profil-Extrusion, `Project.columns`, `columnFootprint()` (gemeinsame Geometrie 2D/3D/Selektion/Transform), `columnVerticalExtent` (UK/OK-Anker wie Wand). Platzieren via BIM-Ribbon-Befehl (`column`, Alias stütze; Default Rect 0.3×0.3, Höhe=Geschosshöhe, Rechteck/Rund-Toggle + Live-Vorschau), 2D-Poché auf Kat. 50 (`addColumnPoche`, Component/Hatch-Auflösung), 3D-Prisma (`emitColumns`, synchron, Rust unberührt), eigener Selektionskanal (ALLE Reset-Stellen gespiegelt), `ColumnSection` (Profil/Masse/Höhe/Drehung editierbar), Löschen/move/copy/mirror, DXF-Layer, i18n. +8 Tests (2D-Position/Rotation, 3D-Box/Prisma, vertikale Lage). tsc/vitest 422. **Bewusst ausgelassen:** 3D-Pick (wie ExtrudedSolid), Marquee, Schedule-Zeile, eigene ToolId (Extrude-Muster gefolgt). **Noch uncommittet. Nutzer prüft 3D/2D visuell.**
|
||||
- [x] ~~**Schnellexport-Dialog (Format + Dateiname) für Topbar-Export**~~ (Nutzer-Wunsch) — **erledigt 2026-07-07:** Klick auf CSV/IFC/OBJ/STL im Topbar-Export-Menü öffnet jetzt `ExportSaveDialog` (Format-Dropdown + Dateiname-Feld, Endung folgt dem Format, Enter/Speichern), statt sofort mit Default-Namen zu laden. PDF/DXF behalten ihre eigenen Options-Dialoge. `runExport(format, filename)` in App.tsx erzeugt+lädt. +i18n `exportSave.*`. tsc/vitest 434. **Offen (bewusst, Nutzer nannte es selbst als Folge):** echter Speicherort-Picker („wo") braucht den nativen Tauri-Save-Dialog (plugin-dialog/fs, Rust) — aktuell Download-Ordner + frei wählbarer Dateiname; die späteren „Ausschnitte" bekommen vordefinierten Namen+Ort. **Noch uncommittet.**
|
||||
- [x] ~~**Nativer Tauri-Speichern-Dialog für Exporte**~~ (Nutzer „ausnahmsweise JA") — **erledigt 2026-07-07:** `@tauri-apps/plugin-dialog`+`plugin-fs` eingebunden (Cargo/lib.rs/capabilities `dialog:allow-save`+`fs:allow-write-text-file`, `cargo check` grün), Util `src/io/saveFile.ts` (`saveTextFile`: unter Tauri nativer „Speichern unter"-Dialog `save()`+`writeTextFile`, sonst Blob-Fallback mit octet-stream + verzögertem revoke; +2 Tests). App.tsx-`downloadTextFile` ruft jetzt `saveTextFile` (Import ergänzt). **Nutzer testet den nativen Dialog in Tauri.** **Scope-Hinweis:** falls fs-Scope-Fehler, `fs:scope` `$HOME/**` nachrüsten. **Noch uncommittet.**
|
||||
- [x] ~~**Ausschnitte liessen sich nicht speichern (Tauri)**~~ — **erledigt 2026-07-08:** Ursache `window.prompt` ist im Tauri-WKWebView DEAKTIVIERT (→ null, kein Name, nichts gespeichert). `ViewSnapshotsPanel` nutzt jetzt In-App-Inline-Eingaben statt prompt/confirm (Speichern + Umbenennen inline, Löschen ohne confirm). tsc/vitest 465.
|
||||
- [x] ~~**Layouts als Ordner-/Baumstruktur**~~ — **erledigt 2026-07-08:** Modell additiv `LayoutFolder{id,name,parentId?}` + `Layout.folderId?` + `Project.layoutFolders?`; freie Grösse `customWidthMm?`/`customHeightMm?` an Layout UND MasterLayout (übersteuert paper/orientation), zentraler Helfer `effectiveSheetSizeMm()`. Pure Logik `layoutModel.ts`: `buildLayoutTree`, `collectFolderLayouts`, `deleteFolder` (Kinder reparent auf Elternebene), `folderPdfPages`. `LayoutsPanel.tsx` als Baum (auf/zuklappbare Ordner, Master als eigener Vorlagen-Abschnitt), EIN „+"-Dropdown {Ordner·Layout·Masterlayout}, neues Element landet SOFORT im Inline-Rename (ViewSnapshotsPanel-Muster, kein window.prompt). Zwei Erstell-Dialoge (`LayoutCreateDialogs.tsx`): Layout mit Master-Vorlage-Dropdown ODER freie Grösse (A4/A3/Custom-mm); Masterlayout mit Format/freie Grösse+Ausrichtung. **Ordner→Mehrseiten-PDF funktioniert:** `src/export/layoutPdf.ts` (`buildFolderPdf`/`saveFolderPdf`, jsPDF, `addPage` je Layout mit eigener Grösse, Viewports via generatePlan→planToPrintSvg, Master-Titelblock als Vektor), neuer `saveBinaryFile` in `saveFile.ts` (nativer Speichern-Dialog für PDF-Bytes). +14 Tests (Custom-Grösse, Ordner-CRUD, Baumaufbau inkl. verwaister Refs, Reparenting, PDF-Seitenplan). **Nachtrag (Orchestrator):** `LayoutSheet.tsx` (In-Viewport-Editor, vom Agent bewusst nicht angefasst) nutzte noch `sheetSizeMm(layout.paper,...)` ohne Custom-Grösse zu respektieren — auf `effectiveSheetSizeMm(master ?? layout)` umgestellt (identische Regel wie der PDF-Export), damit Blätter mit freier Grösse auch im Viewport korrekt dargestellt werden. tsc/vitest 505. **Noch uncommittet. Nutzer prüft visuell in Tauri** (+-Menü, Ordner-Zuordnung, Dialoge, Mehrseiten-PDF-Reihenfolge/Grössen).
|
||||
- [x] ~~**Icons/A0-A6+B0-B6/aktiver Ordner/Drag&Drop im Layouts-Baum**~~ — **erledigt 2026-07-08:** eigene inline-SVG-Icons (`FolderIcon` auf/zu, `LayoutSheetIcon`, `MasterSheetIcon` mit Stern-Akzent), Muster wie die bestehenden Tab-Icons. `LayoutPaperFormat` volle ISO-216-Reihe A0–A6+B0–B6 (`PAGE_MM`/`PAPER_FORMATS`/`PAPER_FORMAT_GROUPS`), Dropdowns gruppiert A-/B-Reihe. „Aktiver Ordner" war schon korrekt (verifiziert, keine Änderung nötig). HTML5-Drag&Drop (Layout/Ordner zwischen Ordnern, Root-Drop, Ziel-Highlight) mit Zyklus-Schutz (`isDescendant`, `moveFolderToFolder` verwirft Selbst-Verschachtelung als No-op). `layoutPdf.ts`/`LayoutSheet.tsx` bekommen die neuen Formate automatisch (laufen über `effectiveSheetSizeMm`/`PAGE_MM`, kein Hardcoding). +14 Tests. tsc/vitest 519. **Noch uncommittet. Nutzer prüft visuell.**
|
||||
- [x] ~~**Ausschnitte-Panel: Footer-Bar + Anwahl-Persistenz**~~ — **erledigt 2026-07-08 (Agent starb erst beim Schluss-Testlauf, aber vollständig verdrahtet + grün):** Footer im ViewSnapshotsPanel zeigt bei angewähltem Ausschnitt Massstab, passende Ebenen-/Zeichnungskombi (Deep-Equal `boolMapEqual`/`matchingComboName` gegen gespeicherte Combos), aktive Override-Namen, + Inline-Rename in der Footer-Bar. Anwahl bleibt markiert bis Abweichung: `selectedViewSnapshotId`-State + `snapshotMatchesLiveState`-Vergleich in App.tsx (bei Änderung eines erfassten Feldes → Auswahl weg). host.ts um `selectedViewSnapshotId`/`listLayerCombos`/`loadLayerCombo`/`listDrawingCombos`/`loadDrawingCombo` erweitert. tsc sauber, vitest 532 (+13). **Noch uncommittet. Nutzer prüft visuell.**
|
||||
- [x] ~~**Ausschnitte-Panel: Ordner wie bei Layouts**~~ — **erledigt 2026-07-08:** echte Ordner-/Baumstruktur (spiegelbildlich zu Layouts). Modell additiv `ViewSnapshotFolder{id,name,parentId?}` + `ViewSnapshot.folderId?` (altes `folder?`-Namensfeld bleibt LEGACY-kompatibel), `Project.viewSnapshotFolders?`. Pure Baum-Logik `src/state/viewSnapshotFolders.ts` (`buildViewSnapshotTree`/`moveSnapshotToFolder`/`moveFolder` mit Zyklus-Schutz/`deleteFolder`-Reparent, +20 Tests). Panel: auf/zuklappbare Ordner, zwei Header-Buttons (FolderPlus/Plus), aktiver-Ordner-State, Inline-Rename, Löschen, Drag&Drop. **Footer-Bar + Anwahl-Persistenz UNVERÄNDERT erhalten** (unter dem Baum). +i18n. tsc/vitest 558. **Noch uncommittet. Nutzer prüft visuell.**
|
||||
- [x] **2D-Zeichnen auf „Zeichnung"-Ebenen UND auf Layout-Blättern freigeben; 3D/BIM dort sperren (Nutzer-Wunsch 2026-07-08):** ✅ **ALLE drei Teile erledigt 2026-07-08.** **TEIL B erledigt 2026-07-08:** 2D-Annotationen (Linie/Rechteck/Text in mm-Papier) direkt auf Layout-Blättern — `LayoutAnnotation` an `Layout` (additiv) + CRUD (`layoutModel.ts`: create/add/remove/patchAnnotation) + Editor in `LayoutSheet.tsx` (Tool-Buttons, zeichnen/Vorschau/commit, `pickAnnotation`/`translateAnnotation` in `layoutSheetMath.ts`, Selektion+Verschieben+Löschen, HUD mit Farbe/Stärke, `PromptDialog` für Text — kein window.prompt) + Handler in App.tsx. Tests: `layoutModel.test.ts` + `layoutSheetMath.test.ts`, Suite 588 grün. Bewusst ohne: Resize-Griffe, Text-Rotation, Snapping, Sheet-Clamping beim Verschieben. **TEIL A erledigt 2026-07-08** — die 2D-Zeichenwerkzeuge (select/line/polyline/rect/circle/arc/text) sind bereits nicht `floorOnly`, die zugehörigen Commands (line/rect/polyline/circle/arc/text) auch nicht → die Command-Engine erlaubt das Zeichnen auf jeder Ebene, und eine `"drawing"`-Ebene rendert schon die volle interaktive `LevelPlanView` (App.tsx:5515). Der einzige echte Blocker war das UI-Gating: die **ToolsPanel-Sidebar** sperrte ALLE Werkzeuge ausser „select", sobald `!toolsEnabled` (App.tsx `activeLevel.kind === "floor"`). Fix: `ToolsPanel.tsx` gatet jetzt per `tool.floorOnly && !toolsEnabled` — **Parität mit dem Ribbon** (`RibbonBar.tsx`, das schon so gatete). Damit sind auf „drawing"-Ebenen die 2D-Tools nutzbar, BIM/3D-Tools (wall/ceiling/window/door/stair/room/column, alle `floorOnly`) bleiben grau. `AttributesPanel.tsx:78` bleibt unverändert (gatet nur den Wandtyp/Deckentyp-Picker, der ohnehin nur bei floorOnly-Tools erscheint). Verifiziert: `tsc` sauber, `vitest` 558/558. TEIL B (grösser, OFFEN): **auf Layout-Blättern 2D zeichnen** — der `LayoutSheet`-Viewport-Editor kann bisher nur Viewports platzieren; für Annotationen (Linien/Text/Rechtecke direkt aufs Blatt) fehlt ein Annotations-Datenmodell am `Layout` + das Zeichnen/Rendern auf dem Blatt (mm-Koordinaten). Scope Teil B mit Nutzer/als eigene Phase. **TEIL C erledigt 2026-07-08 („endlich"): Text-Platzierungswerkzeug als 2D-Element.** Das `{shape:"text"}`-Drawing2D-Modell + Rendering (`generatePlan` → `drawingText`) + Transform (`transform.ts` `at`) existierten schon (DXF-Import-Weg); es fehlte nur das PLATZIER-Werkzeug. Umgesetzt: neues `text`-Command (`src/commands/cmds/text.ts`: Ankerpunkt klicken → Freitext in der Command-Line eingeben → commit `Drawing2D {shape:"text", height:0.25m, angle:0}`), Placeholder-Tool `text` (nicht floorOnly) gekoppelt via `TOOL_COMMAND`, in Registry + Aliase (tx/txt/beschriftung) + `ToolId` + `TOOL_ORDER` + Ribbon-2D-Gruppe + ToolIcon (Serifen-A) + i18n (de/en). Neu: die Command-Engine routet jetzt Freitext-Eingabe (`AcceptKind` um `"text"` erweitert, `engine.ts` `routeTypedInput` reicht bei `accepts:["text"]` die getippte Zeile 1:1 durch — vor der numerischen Auflösung, damit auch Zahlen/Kommas als Label ankommen). Nutzer-Test: Text-Tool wählen → in den Plan klicken → Label tippen → Enter. **Noch uncommittet, WASM-unabhängig (rein TS).**
|
||||
- [ ] **Masterlayout im Viewport ansehen + Elemente darauf platzieren (Nutzer-Wunsch 2026-07-08) — NACH Rollback+Footer-Agenten (gleiche Dateien LayoutsPanel/LayoutSheet):** ein Masterlayout soll wie ein Layout im `LayoutSheet`-In-Viewport-Editor geöffnet werden können, um Plankopf-Elemente/Rahmen VISUELL darauf zu platzieren (statt nur über ein Formular mit festen Titelblock-Textfeldern). `LayoutSheet.tsx` arbeitet aktuell nur mit `Layout` (hat `viewports: LayoutViewport[]`); für Master fehlt sowohl das Öffnen im Viewport als auch ein Datenmodell für frei platzierbare Grafik-/Text-Elemente (Plankopf-Felder, Rahmen-Linien) auf dem Master. Scope/Umfang mit Nutzer klären, bevor gebaut wird: welche Elementarten (Text-Feld mit Platzhalter-Variable wie {ProjectName}/{Scale}/{Date}, Linie/Rechteck als Rahmen, Logo/Bild?), wie sie sich von den normalen `LayoutViewport`s unterscheiden (kein Ausschnitt-Bezug, rein grafisch), Doppelklick-Navigation vom `LayoutsPanel` aus zum Öffnen eines Masters im Viewport (analog `onOpenLayout`).
|
||||
- [x] ~~**LayoutsPanel: Doppel-Name + zwei Buttons je Abschnitt + Master in Ordnern**~~ — **erledigt 2026-07-08:** Doppel-„LAYOUTS" behoben durch Umbenennung des ÄUSSEREN Panel-Titels auf „Mappe" (DE) / „Portfolio" (EN, neuer Key `layouts.panelTitle`); innere Abschnitte bleiben „Layouts"/„Masterlayouts". Kombiniertes „+"-Dropdown ersetzt durch zwei Icon-Buttons je Abschnitt: `FolderPlusIcon` (Ordner) + `PlusIcon` (Layout bzw. Masterlayout). Masterlayouts jetzt in eigenen Ordnern: `MasterLayout.folderId?` + `LayoutFolder.kind?:"layout"|"master"` (fehlt=layout, rückwärtskompatibel), getrennte Bäume (`buildMasterTree`, kind-Filter), eigener `activeMasterFolderId`. Bug gefixt: `deleteFolder`/`moveFolderToFolder` verloren `kind` beim Reparent → `stripParentId`. +6 Tests. PanelFrame unangetastet. tsc/vitest 538. **Bewusst offen:** Drag&Drop im Master-Baum (bestehende Master nur beim Anlegen in Ordner). **Noch uncommittet. Nutzer prüft visuell.**
|
||||
- [x] ~~**Fixer, unlöschbarer „Masterlayout"-Ordner**~~ — **VERWORFEN 2026-07-08 (Nutzer-Kurskorrektur):** gebaut, dann sofort zurückgebaut — Nutzer sah die bestehende Lösung (Masterlayouts als separate Kategorie/Abschnitt ausserhalb der Ordnerstruktur, aus dem vorherigen Layouts-Agenten) live und wollte sie explizit BEHALTEN, keinen Ordner. Vollständig zurückgerollt (`layoutModel.ts`/`LayoutsPanel.tsx`/`App.tsx`/i18n), verifiziert tsc sauber + vitest exakt Baseline 519 (keine Regression). Masterlayouts bleiben separater Abschnitt.
|
||||
- [x] ~~**`window.prompt`/`confirm` überall ersetzen (Tauri-WKWebView deaktiviert sie) — SWEEP**~~ — **erledigt 2026-07-08:** neue generische `src/ui/PromptDialog.tsx` (Muster ExportSaveDialog: Overlay+Dialog, Enter bestätigt, Esc schliesst) ersetzt ALLE verbliebenen `window.prompt`-Aufrufe: `ComboMenu` (TopBar, Ebenen-/Zeichnungskombination speichern), `LayoutMenu` (Arbeitsumgebung speichern), Text-Grösse „frei" (`TextGroup`), Massstab „frei" (`ViewRibbonTab`), sowie die Tastatur-Flows O/P (Array-Kopien/Verteilen-Anzahl, `App.tsx`: `promptCount()` entfernt, ersetzt durch `pendingCopyMode`-State + Dialog). `window.confirm` war bereits an allen Fundstellen vorher entfernt (ViewSnapshots/LayoutsPanel). Verifiziert: `grep -rn 'window\.prompt(\|window\.confirm('` über src/ liefert NICHTS mehr. tsc sauber, vitest 491/491. **Noch uncommittet.**
|
||||
- [x] ~~**Layout im Viewport editierbar (wie ArchiCAD/VW/OpenCADStudio) — Phase 2b**~~ — **erledigt 2026-07-08 (Agent-verifiziert, NICHT visuell abgenommen):** Blatt jetzt erstklassiger Ansichtsmodus im Haupt-Viewport (`activeLayoutId` in App.tsx → `LayoutSheetView` statt Content), schwebendes `LayoutEditor`-Fenster ENTFERNT. Maus-Editing (`LayoutSheet.tsx`): Viewport aufziehen→Ausschnitt-Bindung (Inline-Dropdown, kein prompt), auswählen/verschieben/8-Griff-Resize (Commit bei Pointer-Up, Live-Ghost), löschen (Entf), Massstab-HUD; Pan (Mitte/Space)/Zoom-to-Cursor/Einpassen. Pure Transform+Hit-Test `layoutSheetMath.ts` (+26 Tests). `LayoutsPanel` window.prompt/confirm ebenfalls auf Inline umgestellt. +i18n `layouts.*`, CSS `.layout-sheet-*`. tsc/vitest 491, vite build OK. **NOCH VISUELL IN TAURI ZU PRÜFEN.** Offen: Einrasten an Kanten/Nachbarn, Ribbon-Massstab wirkt im Layout-Modus nicht (nicht deaktiviert). **Noch uncommittet.**
|
||||
- [x] ~~**Layout-Blätter mit Masterlayout — Phase 2a**~~ (A3, setzt auf Ausschnitte auf) — **erledigt 2026-07-08:** Modell `Layout`/`LayoutViewport`/`MasterLayout` + `Project.layouts`/`masterLayouts` (im Dokument). `LayoutsPanel` (Layouts+Master anlegen/umbenennen/löschen, im Rechts-Dock, `LAYOUT_VERSION` 10→11). `LayoutEditor` (schwebendes Fenster wie ResourceManager): Blatt im mm-Seitenverhältnis (Zoom/Einpassen), Titelblock (Master-Vererbung via `resolveTitleBlock`, leere Felder → Defaults Projektname/Blattname/Massstab/Datum), und jeder Viewport rendert ECHT den Plan seines gebundenen Ausschnitts via `generatePlan`→`planToPrintSvg` (viewBox-mm = Viewport-mm, 100%-Einpassung + Clipping). Viewport hinzufügen (Ausschnitt-Picker)/entfernen, Position/Grösse/Massstab numerisch; Papier/Ausrichtung/Master pro Blatt. +16 Tests (Blatt-Geometrie, immutables CRUD, Master-Vererbung, mm-Geometrie). +i18n `layouts.*`. **Ehrliche Grenzen:** Live-Viewport-Render nur per Code korrekt, NICHT im Browser abgenommen (Nutzer prüft); Ausschnitte auf Schnitt/Ansicht (activeLevelId≠Geschoss) → leerer Plan (nur Grundriss-Ausschnitte füllen den Viewport); Titelblock-Font ≠ PDF-Helvetica (Phase-2b-PDF baut es separat). **Bewusst Phase 2b:** Multi-Page-PDF-Export, Maus-Drag/Resize der Viewports, „Alle aktualisieren", erweiterte Kamera-POV (Augen-/Zielhöhe). tsc/vitest 465. **Noch uncommittet.**
|
||||
- [x] ~~**Ausschnitte / View-Snapshots — Phase 1**~~ (A2, Fundament) — **erledigt 2026-07-07:** `ViewSnapshot` (name/folder + viewType/view3d/fov/scaleDenominator/detail/activeLevelId + Ebenen-/Zeichnungs-Sichtbarkeit + aktive Override-Ids) am `Project.viewSnapshots` (im Dokument). Pure Capture/Apply-Logik `src/state/viewSnapshots.ts` (+8 Tests: Roundtrip, defensives Auffüllen, Override-Menge). App.tsx: Capture sammelt View-State + `snapshotLayer/DrawingVisibility` + aktive Overrides; Apply Reihenfolge Geschoss→View→Sichtbarkeit→Overrides, Massstab-Zoom per 1 rAF. Neues `ViewSnapshotsPanel` (Liste/Ordner, Speichern/Umbenennen/Löschen), registriert im Default-Rechts-Dock (`LAYOUT_VERSION` 9→10). +i18n `viewsnap.*`. **Bewusst offen Phase 2:** Layout-Blätter+Masterlayout (A3), Viewport-Platzierung + Ausschnitt-Bindung, „Alle aktualisieren", Multi-Page-PDF, erweiterte Kamera-POV (Augen-/Zielhöhe getrennt), Ordner-Drag&Drop; Apply erzeugt mehrere Undo-Schritte (bekannt). **Noch uncommittet.**
|
||||
- [x] ~~**IFC-Wände nicht solid (oben/unten offen)**~~ — **erledigt 2026-07-07:** ECHTE Ursache war eine invertierte Wicklung, NICHT eine fehlende Fläche — der Y-up→Z-up-Achsen-Swap im IFC-Export (`(mx,my,mz)→(mx,mz,my−base)`) ist eine Reflexion (Determinante −1) und kehrte jedes Dreieck um → Normalen nach innen → Viewer cullt Vorderseiten → hohl. Fix: beim Swap Dreieck umdrehen (`i0,i2,i1`). `Closed=.T.` gesetzt, verifiziert über `isWatertight` (`wallMeshCut.ts`): NICHT der naive „jede Kante von 2 Dreiecken"-Test (der meldet die legitimen T-Stösse aus vollen Deckel/Boden-Streifen fälschlich als offen), sondern das T-Stoss-robuste Gauss-/Divergenz-Kriterium `∮n dA=0` + signiertes Volumen >0. STL/OBJ waren korrekt (kein Swap). +15 Tests (Wasserdicht/Winding, inkl. IFC-Face-Set-Volumen aus dem geparsten SPF). tsc/vitest 449, cargo check grün. **Noch uncommittet. Nutzer prüft im Viewer.**
|
||||
- [x] ~~**Exporte an 3D angleichen: Öffnungen ausschneiden + Joins (STL/OBJ/IFC)**~~ (Nutzer: „es sollte so raus wie es im 3d ist") — **erledigt 2026-07-07:** neuer TS-Helfer `src/plan/wallMeshCut.ts` portiert `render3d/mesh.rs::extrude_layer_segment_with_holes` 1:1 (Koordinaten-Kompression `solidSubrects`, Langseiten/Deckel/Boden/Stirnkappen minus Loch-Intervalle, bis zu 4 Laibungsquads pro Loch, konsistentes Winding). STL/OBJ: Wände mit `holes` → ausgeschnittenes Mesh (sonst klassische Box); Joins kommen aus `pickGeometry`. IFC: Wände als `IfcTriangulatedFaceSet` (IFC4, gespeist aus demselben Loch-Mesh) → sichtbar wie 3D in JEDEM Viewer (behebt „Void nicht subtrahiert"); Tür/Fenster bleiben eigene `IfcDoor`/`IfcWindow`-Objekte, die das Loch füllen; IfcOpeningElement/Void/Fill entfallen (Loch steckt im Mesh). +12 Geometrie-Tests (Loch-Region hat keine Voll-Wand-Dreiecke, Laibungen vorhanden, dangling-refs grün). tsc/vitest 434. **Ehrliche Rest-Lücken:** echte Miter-Gehrungsflächen (Export nutzt die achsparallele pickGeometry-Näherung), Schicht-Farben (IFC-FaceSet ohne per-Vertex-Farbe/IfcStyledItem), Fensterglas/Rahmendetail. **Noch uncommittet. Nutzer prüft im Viewer.**
|
||||
- [x] ~~**Layout-Auswahl in die Einstellungen, umbenannt „Arbeitsumgebung"**~~ — **erledigt 2026-07-07:** `LayoutMenu` (Fenster-/Dock-Layouts) aus der TopBar-Zeile in `SettingsDialog` verschoben (neue erste Sektion), `layoutMenu`-Prop von TopBar → SettingsDialog umgehängt (ReactNode-Import in TopBar entfernt, da sonst ungenutzt). i18n `layout.*` umbenannt (DE „Arbeitsumgebung", EN „Workspace") + neue `settings.section.workspace`. tsc/vitest 423. **Noch uncommittet.**
|
||||
- [x] ~~**Export-Sammelmenü + Über-Dialog + Petrol-Punkt (TopBar-Feinschliff)**~~ — **erledigt 2026-07-07:** (1) Die 6 einzeln aufgereihten Export-Icons (PDF/DXF/CSV/IFC/OBJ/STL) zu EINEM „Export"-Knopf (`ExportMenu`, `ios_share`) mit Dropdown-Popover zusammengefasst — entschlackt die Chrome-Zeile; Import bleibt separat. (2) Klick auf die Wortmarke „dossier" öffnet einen In-App-„Über"-Dialog (`AboutDialog`: Marke, Version 0.1.0, Kurzbeschreibung, OSS-Lizenzen; Esc/Klick-ausserhalb schliesst). Wortmarke ist jetzt `<button>` (Chrome zurückgesetzt, Optik identisch). +i18n `file.export`/`about.*`, CSS (`tb-menu`/`about-*`). tsc/vitest 422. **Fix (Nutzer-Report „IFC nicht anwählbar"):** Popover per `createPortal` nach body (entkommt dem Topbar-Stacking-Context, der die unteren Einträge unter dem Ribbon verdeckte) + zweiter `popRef` im Outside-Click-Handler (sonst schloss der mousedown das Menü vor dem Klick) — Muster wie `ContextMenu`/`Dropdown`. **Hinweis:** In-App-Dialog statt nativem macOS-About-Panel (das bräuchte ein Rust-Command in src-tauri) — falls der native Panel gewünscht ist, Folge-Item. **Noch uncommittet.**
|
||||
- [x] ~~**Brand-Punkt petrolgrün**~~ — **erledigt 2026-07-07:** `.brand-dot` fix `#0f766e` (petrolgrün wie DOSSIER-Rhino-Plugin) statt `var(--accent)`. Feinton per Nutzer justierbar.
|
||||
- [x] ~~**OSM-Import auf 7 Kategorien**~~ — **erledigt 2026-07-07:** 3 neue Kontext-Kategorien im Overpass-Import — `parking` (amenity=parking, Fläche), `railway` (railway~rail|tram, Linie), `forest` (landuse=forest + natural=wood, aus `green` herausgelöst; `green` jetzt nur Parks/Wiesen). `OsmSelection`/`buildQuery`/`categorize`/`isClosedCategory` (osm.ts), `GeoCategory`/Labels/Layer (geoContext.ts), 3 Checkboxen (ContextImportDialog.tsx), i18n `ctxImport.src.*`. tsc/vitest 414. **Noch uncommittet.**
|
||||
- [x] ~~**IndexedDB-Persistenz-Kern**~~ — **erledigt 2026-07-07:** `src/state/projectStore.ts` — async CRUD `saveProject`/`loadProject`/`listProjects`/`deleteProject` über IndexedDB (DB „dossier"/Store „projects", keyPath name, `{name,savedAt,project}`), guard-sicher (fehlt `indexedDB` → degradiert: load→null/list→[]/save+delete no-op, 1× warn) — läuft daher auch in vitest/node. +7 Guard-Tests (414). **UI-Wiring (Speichern/Öffnen-Menü, Recent-Liste, Auto-Save) NOCH OFFEN — Folge-Item.** **Noch uncommittet.**
|
||||
- [x] ~~**ObjectInfo Wand: UK/OK-Reihenfolge vertauscht**~~ — **erledigt 2026-07-07:** im Wand-Abschnitt steht jetzt OK (Oberkante) oben, UK (Unterkante) darunter — räumlich passend (Nutzer-Report „stimmt visuell nicht überein"); Decken-Abschnitt hatte die Reihenfolge schon → jetzt konsistent. Reiner JSX-Reorder. tsc/vitest 414. **Noch uncommittet.**
|
||||
- [x] ~~**STL- + OBJ-Export (3D-Mesh, Interop)**~~ — **erledigt 2026-07-07:** pures Modul `src/export/exportMesh.ts` (`exportObj`/`exportStl`, ASCII-STL) aus `pickGeometry(project)` (echte 3D-Geometrie inkl. Joins/Decken-Dominanz): Wand-Bänder als 12-Dreieck-Boxen, Decken/Extrusionen als triangulierte Prismen (bestehender `triangulate()` aus glPlanCompile, kein WASM), y-up rechtshändig. Voll verdrahtet: Quick-Access-Buttons (`view_in_ar`/`landscape`) + `onExportObj`/`onExportStl` in App.tsx (Blob-Download) + i18n `file.exportObj`/`file.exportStl`. 8 Kern-Tests (Index-Bounds, facet/vertex-Konsistenz, Tri-Zahl-Plausibilität). tsc/vitest 407. **Bewusste v1-Limitation:** Öffnungslöcher NICHT ausgeschnitten (Wände volle Boxen) — ladbar in Blender/MeshLab, aber nicht öffnungsgenau; öffnungsgenaue Variante = Folge-Item. Extrusionen nutzen Prisma-Triangulierung statt async truckSolid-WASM (bewusst, wegen synchronem pure-Kern). **Noch uncommittet.** (IFC-Import + DWG-Schreiben bleiben offen.)
|
||||
- [x] ~~**Raumstempel: Personenzahl + Flächen-Rundung**~~ — **erledigt 2026-07-07:** `RoomStamp.occupancy?`/`roundingStep?` (additiv, optional). `formatStampArea(area, step?)` in `roomStamp.ts` rundet auf Vielfache von `step` (0.01/0.1/0.5/1 m²), sonst 2 Nachkommastellen wie bisher; Personenzahl als „{n} Pers."-Zeile im Stempel. Editor-Zeilen (Dropdown Rundung + Zahlfeld Personen). Drag&Drop-Builder bleibt out of scope. +6 Tests, +i18n. tsc/vitest 399. **Noch uncommittet.**
|
||||
- [x] ~~**Auto-Zoom nach Geo-Import**~~ — **erledigt 2026-07-07:** nach swisstopo/OSM/Terrain-Import (`onAddContextObjects`) passt die Plan-Ansicht automatisch auf die neuen Objekte ein. `PlanViewHandle.fitBounds(pts)` (dünner Wrapper um `fitBoxFor`), `contextObjectsBounds()` bildet die Gesamt-BBox über alle neuen ContextObjects (contourSet-Punkte + Mesh-/Terrain-Positions, Z ignoriert), `addContextObjectsAndFit` in App.tsx verdrahtet. DXF-Import-Pfad (eigene viewCenter-Logik) bewusst unberührt. Kein i18n, host.ts unverändert. tsc/vitest 393. **Noch uncommittet.**
|
||||
- [x] ~~**Rich-Text Hoch-/Tiefstellung (super/sub)**~~ — **erledigt 2026-07-07:** zwei Toolbar-Buttons in `RichTextEditor.tsx` (Muster der bestehenden B/I/U/S-Buttons; super/sub-Exklusivität war im Modell `richText.ts::applyMark` schon gelöst); +i18n `rt.super`/`rt.sub`. Teil des ROADMAP-Items „Rich-Text-Annotationen" (Maskierung/Rahmen bleiben offen/blockiert). tsc/vitest 393. **Noch uncommittet.**
|
||||
- [x] ~~**B2 — Objekt-Info numerisch erweitern**~~ — **erledigt 2026-07-07:** im ObjectInfo-Panel sind X/Y jetzt editierbar (Commit verschiebt die Selektion so, dass der gewählte Bezugspunkt auf die Koordinate wandert, via neuem Host-Callback `onMoveSelectionBy`), dazu ein Dreh-Feld (`RotateField`, Delta-Grad um den Anker, `onRotateSelectionAround`) und für einzelne Drawing2D-`line`/`circle` ein Längen- bzw. Radius-Feld (`onSetDrawingLineLength`/`onSetDrawingCircleRadius`). Move/Rotate nutzen die bestehende `commitTransform`-Maschinerie (transform.ts unverändert), wirken für wall/drawing2d/extrudedSolid; für ceiling/opening/stair/room ist der Aufruf No-op (Feld rendert, revertiert — gleiches Muster wie das bestehende Breite/Höhe-Resize). +i18n. tsc/vitest 393. **Noch uncommittet.**
|
||||
- [x] ~~**Komponenten-Thumbnail: PBR-Kugel-Vorschau (D3-Rest)**~~ — **erledigt 2026-07-07:** ComponentsTab-Listeneintrag + Detail-Preview in `ResourceManager.tsx` zeigen jetzt bei gesetztem `component.material` die live gerenderte PBR-Kugel (`ComponentMaterialSphere`, lazy via IntersectionObserver + `requestMaterialPreview`, Muster von `MaterialLibraryTile`); `materialAssetOf` löst `libraryId` gegen `MATERIAL_LIBRARY` auf, sonst Ad-hoc-Asset aus den eigenen Map-URLs. Fallback-Kette Material→Hatch→Color unverändert. Nur `ResourceManager.tsx`, kein i18n. tsc/vitest 393. **Noch uncommittet.**
|
||||
- [ ] **E2b Schraffur-Kachel-Motiv** (MotifEditor für HatchStyle-Tile wiederverwenden).
|
||||
- [x] ~~**Linienstile aufräumen**~~ — **erledigt (2026-07-05):** die drei funktional identischen 0.13-Volllinien (`thin`/`hatch-line`/`joint-massive` — alle weight 0.13, `dash:null`, gleiche Farbe) auf EINE kanonische „Volllinie 0.13" (`thin`) zusammengeführt; alle weight-only-Stile klar als „Volllinie X.XX" benannt; Schraffur-Referenzen (`hatch-line`→`thin`, inkl. `ResourceManager.patToHatches`) + Schichtfugen (`joint-massive`→`thin`) remappt; `hatch-dash` (einzige gestrichelte) bleibt. Ids stabil gelassen (geladene Projekte + interne Refs bleiben heil; Rendering-Gewichte unverändert). 7→5 Stile. tsc + Suite 331 grün.
|
||||
- [ ] 2D-Plan z-Anordnen (Kombo-Schraffuren); Bild-Schraffur GL/DXF (heute Fallback).
|
||||
- [ ] **AUSSCHNITTE- + LAYOUT-SYSTEM (Nutzer-Vision 2026-07-07, grosser Hebel).** Zwei zusammenhängende Systeme (DOSSIER A2→A3, ROADMAP §7 Phase 3 + §11 Pläne):
|
||||
- **Ausschnitte / View-Snapshots (A2):** benannte, gespeicherte Ansichten, die einen kompletten Darstellungszustand einfangen und wiederherstellen. **Kern-Einsicht des Nutzers: das muss NICHT neu erfunden werden — die Bausteine existieren schon und werden nur KOMPONIERT:** ein Ausschnitt = Referenz/Snapshot auf {**Ebenenkombination** (`src/state/visibilitySets.ts` `LayerCombo`) + **Zeichnungsebenen-Kombination/-Einstellungen** (`DrawingCombo` ebd.) + aktive **Overrides**-Regelmenge (`Project.overrideRules`, A1) + Kamera/Ansichtstyp/View3d + **Massstab** + **Detailgrad**}. Also im Wesentlichen ein Container, der diese bereits vorhandenen Zustände bündelt, benennt (Ordner/Presets) und per Klick wiederherstellt. Speicherort: Projekt (nicht localStorage — Ausschnitte gehören zum Dokument). Voraussetzung für die halbe Pläne-Tabelle (Layouts, Multi-Page-PDF, Detail-Bindung, Massstab-pro-Viewport).
|
||||
- **Layout-System mit Masterlayout + Layouts (A3):** Druck-/Plan-Blätter (A4/A3-Papierformate), auf denen mehrere **Details/Viewports** platziert werden, jedes an einen Ausschnitt-Snapshot gebunden (bei „Alle aktualisieren" ziehen sie nach). Ein **Masterlayout** (gemeinsame Elemente: Titelblock/Planrahmen/Logo/Plankopf, wie Vorlagen-Master in InDesign/ArchiCAD) wird von den einzelnen **Layouts** geerbt/überlagert — Änderung am Master schlägt auf alle Layouts durch. Pro Layout eigener Massstab pro Viewport (Auto-DPI, Plotweight-/Schraffur-Skalierung, `sceneToPrintSvg.ts` als Basis), PDF-Export pro Blatt bzw. Multi-Page.
|
||||
- **Erweiterte Kamera-Settings als Teil des Ausschnitts (Nutzer 2026-07-07):** mehr einstellbare Kamera-Parameter, die der Ausschnitt MIT speichert — u. a. **Zielhöhe** und **Augenhöhe** (Eye/Target-Z getrennt), FOV, Blickrichtung/Distanz; die Parameter dürfen **pro Projektionsart unterschiedlich** sein (Perspektive vs. Isometrie/Ortho haben je eigene sinnvolle Sets — Iso braucht z. B. orthoHalfHeight statt FOV/Augenhöhe). Heutiger Zustand: nur FOV global (`fov` in App-State, CameraMenu) + Orbit-State intern im `Wasm3DViewport` (`OrbitState` yaw/pitch/dist/target, `orthoHalfHeight`) — nicht als benannte, persistierbare Kamera-Definition nach aussen geführt. Schritt dazu: Kamera-Zustand als serialisierbares Objekt exponieren (get/set am Viewport), UI-Felder (Augenhöhe/Zielhöhe/Distanz) im Kamera-Popover, dann im Ausschnitt-Modell mitführen.
|
||||
- **Phasen-Vorschlag:** (1) Ausschnitt-Datenmodell + Speichern/Wiederherstellen (komponiert die bestehenden Combos/Overrides/View-State) + Ausschnitte-Panel (Liste/Ordner, wie `visibilitySets`-Muster). (2) Masterlayout + Layout-Blatt-Modell + Viewport-Platzierung, Viewport→Ausschnitt-Bindung. (3) Master-Vererbung + „Alle aktualisieren" + Multi-Page-PDF@DPI. Mit Nutzer Detailgrade/Papier-Defaults/Ordnerstruktur festlegen.
|
||||
- [ ] **SCHNITTEBENEN als editierbare, gekoppelte Objekte (2D↔3D) — Nutzer-Vision 2026-07-07 „ein richtig ausgereiftes Tool".** Schnittebenen sollen sichtbare, im Grundriss platzier- und editierbare Objekte sein, die 2D-Schnitt und 3D-Live-Schnitt verbinden. Bausteine der Vision (verbindlich festhalten):
|
||||
- ✅ **Phase 1 erledigt 2026-07-17 (Kopplung + Navigation, KEIN Rust — plan: `.claude/plans/cozy-bubbling-goose.md`):** der 3D-Live-Schnitt (`Wasm3DViewport`) folgt jetzt der gewählten Schnittlinie statt einer fest verdrahteten horizontalen Ebene — die Engine kann beliebige Ebenen längst (`set_section_plane(point,normal)`), es fehlte nur die Bindung. App-State `section3dCutId` + abgeleitete `section3dPlane` via `sectionPlaneFromLevel` (world point+normal aus `linePoints`/`directionSign`, `plan/toSection.ts`) → durch `Viewport3D`→`Wasm3DViewport` (neue Props `section3d`/`onClearSection3d`). Schalter „Im 3D schneiden" in der Schnittlinien-Sektion des Objekt-Info-Panels (`host.section3dCutActive`/`onToggleSection3dCut`); der Viewport-Overlay-Knopf ist an die aktive Linie gebunden und hebt sie auf. **Doppelklick auf die Schnittlinie im Grundriss → springt in die 2D-Schnittansicht** (`PlanView.onOpenSectionLevel` → `onSelectLevel`). 2D-Editieren (Endpunkte/Blickrichtung/Tiefe) existierte schon im Objekt-Info. `tsc` + `vitest` 866 grün. **Nutzer prüft visuell in Tauri** (render3d-Pfad).
|
||||
- ✅ **Phase 2 Teil „eigene Ebene / Sichtbarkeit" erledigt 2026-07-17:** eine unsichtbare Schnitt-/Ansichtsebene (`visible===false`) zeigt ihre Führungslinie im Grundriss nicht mehr (`addSectionLines` in `generatePlan.ts`, +1 Test). Ein-/ausblendbar wie andere Ebenen (Zeichnungsebenen-Panel).
|
||||
- **Offen Phase 2 (engine-schwer, mit Nutzer im Tauri-Loop):** Farbe der 3D-Schnittebene + **Cut-away-Schleier/Schraffur auf der weggeschnittenen Seite** (wgpu-Shader/`section_fill.rs`/`section.rs`) — blind über Nacht bewusst NICHT gebaut (unverifizierbar ohne Tauri-Sicht, s. Arbeitsstil „Nutzer prüft render3d visuell").
|
||||
- **Offen Phase 3 (engine-schwer):** 3D-POV je Schnitt (Augen-/Zielhöhe/Winkel getrennt, pro Projektionsart) im Datenmodell + eigenes 3D-Schnittfenster (Doppelklick 3D-Symbol → Clip+Kamera); koppelt an das Ausschnitte-/Kamera-System. Kamera-Kopplung an `Wasm3DViewport`-Orbit → Tauri-Verifikation nötig.
|
||||
- Restliche Vision-Bausteine (unverändert offen):
|
||||
- **Eigene Zeichnungs-/Grafik-Ebene „Schnittebenen":** die Schnittlinie des 2D-Schnitts liegt auf einer eigenen Ebene (ein-/ausblendbar wie andere Layer). Auch die 3D-Schnittebene wird im GRUNDRISS als Objekt dargestellt (Linie/Symbol) — vor allem, um die 3D-Schnittebene dort PRÄZISE zu setzen (im 2D positionieren statt im 3D fummeln).
|
||||
- **3D-Schnittebene über Topbar ein/aus** (existiert teilweise: Viewport-Toggle) + **Farbe**; alles HINTER der Ebene (die weggeschnittene Seite) wird mit einer **Misch-/Andeutungsschraffur umhüllt**, damit visuell klar ist, dass dieser Teil abgeschnitten wird — sowohl im 3D als auch als Vorschau im 2D.
|
||||
- **Doppelklick auf eine Schnittebene → springt in die zugehörige Schnitt-Ansicht:** 2D-Schnittebene-Symbol → 2D-Schnittfenster; 3D-Schnittebene-Symbol → 3D-Schnittfenster (Perspektive/Ansicht mit gesetzter Clip-Ebene). Zwei Objektarten, zwei Zielansichten, gleiche Interaktion.
|
||||
- **3D-Schnitt-POV-Parameter:** die 3D-Schnittebene definiert zusätzlich Blickpunkt (POV), **Augenhöhe** + **Zielhöhe** (getrennt) und **Blickwinkel** — dieselben erweiterten Kamera-Settings wie im [Ausschnitte-System oben] (dort pro Projektionsart mitgespeichert). Ein Schnitt ist damit ein Ausschnitt mit Clip-Ebene + Kamera-Definition.
|
||||
- Verknüpfung: baut auf dem bestehenden 2D-Schnitt (`DrawingLevel kind:"section"`, `linePoints`/`directionSign`) + der 3D-Live-Schnittebene (`03f0c40`, Clip-Uniform) auf; koppelt eng mit dem Ausschnitte-/Layout-System (View-Snapshots) und den erweiterten Kamera-Settings. Grosses, phasenweises Feature — mit Nutzer Reihenfolge/Scope festlegen (zuerst: 3D-Schnittebene als 2D-Grundriss-Objekt platzier-/editierbar + Doppelklick-Navigation).
|
||||
- [ ] **AUDIT (DOSSIER-Studie):** ~~A1 Override-Regel-Engine~~ ✅ (2026-07-07, s. Erledigt); 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; D2 volles Element-Set erledigt 2026-07-07.) Belege in `/tmp/dossier-ref/rhino/*.py`.
|
||||
- [ ] Elemente im Schnitt anwählbar; render3d 2D-Schraffur auf 3D-Flächen.
|
||||
|
||||
- [ ] **TEAMWORK — kollaboratives Bearbeiten (Supabase self-hosted).** Projekte auf einem self-hosted Supabase-Stack speichern; mehrere Nutzer bearbeiten dasselbe Projekt gleichzeitig. Kernkonzept: **pessimistisches Object-Locking** (wie Revit Worksharing / ArchiCAD Teamwork) — alle Objekte sind zunächst gesperrt; ein Nutzer *reserviert* die Elemente, die er bearbeiten möchte (exklusiver Schreibzugriff), und *gibt sie frei*, sobald er fertig ist. Freigabe → sofort für alle anderen sichtbar via Supabase Realtime (WebSocket-Kanal). Kein Merge-Konflikt nötig, weil niemals zwei Nutzer dasselbe Objekt gleichzeitig schreiben. Grobe Schichten:
|
||||
- **Auth + Projekt-Liste:** Supabase Auth (Email/Magic-Link), Projektübersicht, Öffnen/Schliessen.
|
||||
- **Lock-Service:** Tabelle `object_locks (project_id, object_id, user_id, locked_at)` mit Row-Level Security; `reserveObjects(ids[])` / `releaseObjects(ids[])` als Supabase-RPC; Optimistisches Check-in via DB-Constraint (doppelte Reserve → Fehler → UI-Feedback).
|
||||
- **Realtime-Sync:** Supabase Realtime-Channel pro Projekt; bei Freigabe werden die veränderten Objekte (JSON-Patch oder ganzes Objekt-Payload, TBD) gepusht; lokaler Store merged incoming changes sofort.
|
||||
- **Presence:** Wer ist online, wer hat welche Objekte reserviert (farbige User-Badges an reservierten Elementen im Plan).
|
||||
- **Offline-Guard:** Beim Verbindungsabbruch Locks automatisch nach Timeout freigeben (DB-seitig: `locked_at + interval` prüfen).
|
||||
- **Umfang/Granularität TBD mit Nutzer:** Object = Wand/Raum/Drawing2D-Element? Geschoss? Layer? Feinere Granularität = mehr Parallelarbeit, aber komplexeres UI.
|
||||
- **Aufwand:** gross (2–4 Wochen echte Arbeit). Erst sinnvoll, wenn Kern-CAD-Features stabil. Mit Nutzer Granularität + Hosting-Setup klären bevor Implementierung startet.
|
||||
|
||||
- [x] ~~**2D-Zeichnungen (`drawings2d`) fehlen im nativen render3d/WASM-Pfad bei z=0.**~~ — **erledigt 2026-07-06:** Nutzer-Report — früher wurden 2D-Plan-Elemente auch im 3D als flache Referenz bei z=0 gezeigt, das gab es nur noch im alten three.js-Fallback (`addDrawing2DLines`), nicht im nativen `render3d`/wgpu-Pfad (heute Standard-Renderer). Fix OHNE Rust-Änderungen: neuer Emitter `emitDrawingLines` (`src/plan/toWalls3d.ts`) baut je 2D-Zeichnung (line/polyline/rect — Parität zu `drawing2DSegments` im three.js-Fallback, Kreis/Bogen/Text bewusst aussen vor) ein dünnes Ribbon-Mesh (2 Dreiecke je Segment, `DRAWING_LINE_HALF_WIDTH` 0.008 m) knapp über der Geschossebene (`DRAWING_LINE_ELEVATION_EPS` 0.01 m), Farbe via `drawingColor3d` (color→LineStyle→Kategorie, wie `drawing2DColor`) → hexToRgb. Läuft über den bereits bestehenden generischen `RMesh`/`MeshInput`-Kanal (`kind:"imported"`, `append_context_mesh` rendert ohnehin doppelseitig) — kein neuer Rust-Typ nötig. `tsc`/`vitest` 339/339 grün. **Nutzer-bestätigt** (2026-07-06, Tauri-Dev-App selbst getestet): Linien aus dem 2D-Grundriss sichtbar in der Perspektive. **Noch uncommittet.**
|
||||
|
||||
### 3D-REST (engine-schwer, bewusst NICHT blind — mit Nutzer angehen)
|
||||
- [x] ~~**Wand-Joins in 3D**~~ — **erledigt 2026-07-07:** `toWalls3d.ts` wendet jetzt die per-Schicht-Cuts aus `computeJoins` (pro Geschoss, `computeJoinsByFloor`) auf den layered-3D-Pfad an — dieselben Zahlen wie generatePlan im 2D: End-Cuts/Merge an L-/T-Stössen (Band-Mittellinie geschnitten, Kern läuft bis zur Rückgrat-Nahfläche durch, Putz trimmt), Durchgangswand-Span-Cutouts (Band zerfällt in Achsen-Teilstücke). **Decken-Dominanz per Schicht** (Scope-Erweiterung): `trimWallTopForCeilings` (kappte die ganze Wand) ersetzt durch per-Schicht-z-Subtraktion analog `subtractDominantBands` — Wand behält volle Höhe, nur Bänder mit strikt niedrigerer joinPriority verlieren das Decken-z-Intervall (Kern läuft durch; dominiertes Band spaltet vertikal in unter/über). Öffnungs-Löcher je Teilstück korrekt rebased. Rust unverändert (Boxen parametrisch). Pick/Highlight jetzt konsistent verschnitten. vitest 383 (25 in toWalls3d.test). **Auslassung ehrlich:** Achsenbox bildet gekippte Miter-Stirnfläche nicht exakt ab (Band-Länge = Mittellinien-Schnitt) — passgenau bei rechten/stumpfen Winkeln, kleine Rest-Approximation an sehr spitzen. **Noch uncommittet.** **Nutzer prüft visuell in der Tauri-App.**
|
||||
- [x] ~~**Decken-Schnitt in 3D schneidet zu viele Schichten („Kranz durch die Dämmung")**~~ — **erledigt 2026-07-07:** `collectCeilingCutters`/`emitWall` in `toWalls3d.ts` prüft jetzt PER BAND, ob die Decken-Outline die Band-Mittellinie (`wall.start + u·t + n·offset`) im Grundriss überdeckt — via vorhandenem `pointInOutline` (`geometry/ceiling.ts`), Achse in ~5-cm-Schritten gerastert + Bisektion an den Überdeckungsgrenzen (neue Helfer `bandCoverageIntervals`/`bisectBoundary`); nur überdeckte Achs-Teilstücke bekommen den Decken-z-Schnitt, unbedeckte behalten volle Höhe. Repliziert `subtractDominantBands` (echter geometrischer Überlapp statt wandweiter BBox). Am Sample: Backstein/Innenputz (innen) geschnitten, Dämmung/Aussenputz (aussen) laufen voll durch — kein Kranz. Die 3 falschen Alt-Tests des Vorgängers korrigiert (kodierten „alle Schichten geschnitten"), +2 neue (Voll-Überdeckung schneidet weiterhin alle; Teil-Längen-Überdeckung splittet entlang der Achse). Schnitt-Pfad/L-T-Joins/holes/Rust unberührt. tsc sauber, vitest 393. **Noch uncommittet. Nutzer prüft visuell.**
|
||||
- [~] **3D-Live-Schnitt: echte Bauteil-Schraffuren statt prozeduralem 45°-Muster (Nutzer-Wunsch 2026-07-07).** — **implementiert 2026-07-08 (wartet auf visuelle Nutzer-Verifikation nach WASM-Rebuild).** Der ganze Weg von der 2D-Hatch-Definition bis in den wgpu-Cap-Pass ist gelegt: `toWalls3d.ts` bildet je Materiallage/Decke via neuem `hatchPatternId()`/`resolveComponentHatch()` (aus `getHatch(comp.hatchId)`) ein `Hatch{pattern,angle,scale}` → `WallInput.hatch`/`SlabInput.hatch` (additiv, `#[serde(default)]`) → `section.rs` reicht es über `Prism`→`CutPolygon.hatch` → `section_fill.rs` schreibt es je Cap-Vertex (`CAP_FLOATS_PER_VERTEX` 5→8: `[pos,u,v,pattern,angle_rad,scale]`) → `gpu.rs` Cap-Pipeline-Vertexlayout erweitert → `shaders.rs` `CAP_WGSL` wählt prozedural das Muster: `0 none`→weiss, `1 solid`→Vollton (Beton-Poché), `2 diagonal`→45° (bitgleich zum alten Muster bei angle=0/scale=1 → rückwärtskompatibel), `3 crosshatch`→Kreuz, `4 insulation`→Zickzack. Monochrom (Tinte auf Papier, nur MUSTER variiert — bewusst keine Farbe). Da der 3D-Viewer je Schicht EINE Box emittiert (`layeredWalls:true`), trägt jede geschnittene Schicht ihr eigenes Muster; ohne aufgelöste Schraffur → Fallback-Diagonale. `cargo test -p render3d --features render` 63 grün (+2 section_fill-Tests, WGSL-Validierung inkl. Cap-Shader). **WASM (`npm run build:engine3d`) noch NICHT gebaut (brauchte Netz) — Nutzer muss rebuilden + Tauri-Dev neu starten, sonst greift nichts.** `cargo check --target wasm32 --features web` grün. **Noch uncommittet.**
|
||||
- [ ] **3D-Snapping beim Ziehen (Nutzer-Wunsch 2026-07-07): alle Elemente sollen im 3D auf andere Punkte einrasten.** Heute rastet das 3D-Griff-Ziehen NICHT ein — es projiziert die Maus nur frei auf eine Ebene (`rayPlaneY`/`rayPlane` in `src/viewport/Wasm3DViewport.tsx:576-582`); Snap wurde beim 3D-Griffsystem bewusst weggelassen (three.js-Snap blieb 2D-exklusiv). Nutzer nennt konkret: **Wände** beim Verschieben auf andere Punkte snappen, **Decken** ebenso, „eigentlich alle Elemente". Gewünscht ist also eine gemeinsame 3D-Snap-Schicht (Kandidatenpunkte: Wand-Endpunkte/Ecken, Decken-Eckpunkte, Extrusions-/Öffnungs-Anker, evtl. Rasterpunkte), gegen die der gezogene Griff/Körper im Weltraum einrastet — analog zur 2D-Snap-Infrastruktur (`computeSnap` in `src/tools/snapping.ts`). Zu klären mit Nutzer/Scope: welche Snap-Ziele in 3D (nur Endpunkte oder auch Kanten/Flächen/Raster?), Snap-Radius im Bildschirm- vs. Weltraum, visuelles Feedback (3D-Snap-Marker), und ob die 2D-`computeSnap`-Kandidatenlogik wiederverwendbar ist oder eine eigene 3D-Variante braucht. Betrifft `Wasm3DViewport.tsx` (Drag-Pfad) + neue 3D-Snap-Hilfsschicht; die Store-Callbacks (`onEdit3dVertex`/`onEdit3dBody`/`onEdit3dWallTop`) bleiben.
|
||||
- [~] **Textur-/PBR-Pipeline** (Sampler/Bindings/UV in wgpu; dann `textured`-Style + `Component.texture3d`/Material echt rendern). ✅ **Erster Durchstich** (`0ca3b1d`, Schachbrett). ✅ **Farb-Textur-Array implementiert 2026-07-08 (wartet auf visuelle Nutzer-Verifikation nach WASM-Rebuild):** Statt Schachbrett zeigt eine Wand mit zugewiesenem Material jetzt dessen Farb-Map. Voller Weg: `WallInput.materialIndex: Option<u32>` (1-basiert; 0 = Schachbrett-Fallback) → `mesh.rs` `build_scene_mesh_textured()` füllt je Vertex die Material-Ebene (dieselben Extrusionsfns → Geometrie-Parität) → `gpu.rs`: Schachbrett-Textur zu `texture_2d_array` erweitert, `set_material_textures()` baut `[Schachbrett, Mat0, Mat1, …]`, zweiter Vertex-Buffer für die Ebene → `shaders.rs` `MESH_TEXTURED_WGSL` samplt die Array-Ebene je Band (`layer<0`→Schachbrett) → `web.rs` WASM-Export `set_material_textures(rgba, layer_count)` → TS: `wallMaterialColorMaps()`/`resolveComponentMaterialLayer()` (deterministische Array-Ordnung), neuer `src/viewport/materialTextures.ts` dekodiert die Farb-Maps im Browser (ImageBitmap→Canvas→getImageData 256²), `useWasm3dRenderer.updateModel` lädt sie asynchron hoch (Signatur-Cache, Fallback auf Schachbrett bis geladen). **Ehrlich als Rest:** nur Farb-Map (keine Normal-/Roughness-/Metalness → Beleuchtung wie Shaded); der native Tauri-Push (`nativeSync.ts`) lädt keine Texturen → dort Schachbrett-Fallback (das WASM/Nordstern-Viewport im „Texturiert"-Modus hat den vollen Weg). `cargo check --target wasm32 --features web` grün, `cargo test` 63 grün. **WASM noch NICHT gebaut (Netz) — Nutzer: `npm run build:engine3d` + Tauri-Neustart.** **Verbleibende PBR-Lücken (~15–25 PT):** Normal-/Roughness-/Metallic-Maps + Cook-Torrance-BRDF + Tangentenraum ~5–8 · Mipmaps (Blit-Pass) + anisotropes Filtern ~1–2 · Bild-Datei-Laden serverseitig statt Browser-Dekodierung ~1–2 · nativer Tauri-Textur-Push ~2–3 · UI/Persistenz-Feinschliff ~5–8. **Noch uncommittet.**
|
||||
- [x] ~~**Wand-Schicht-Bänder in 3D** Option B~~ — **bereits umgesetzt** in `resolveWallBands(layered=true)` (`src/plan/toWalls3d.ts:236-241`): 3D-Viewer-Pfad liefert je Materiallage ein eigenes `WallBand` (Dicke + Component-Albedo + Normalen-Versatz), `pushSegment` emittiert jede Lage als eigene Voll-Box. `dominantLayerColor` ist nur noch Fallback für den Schnitt-Einkörper-Pfad (`layered=false`). Verifiziert per Code-Lesung.
|
||||
- [x] ~~**Ortho-Ray-Picking**~~ — **erledigt 2026-07-07:** `cameraRay` (`src/viewport/raycast3d.ts`) war IMMER ein perspektivischer Pinhole-Strahl (ein Ursprung `eye`, Richtung variiert je Pixel) — für Front/Top/Side/Iso-Presets (die render3d orthografisch zeichnet) nur eine Näherung. Fix: `cameraRay` nimmt jetzt optional `perspective`/`orthoHalfHeight` (neue optionale Felder auf `RayCamera`, rückwärtskompatibel — fehlt `perspective`, bleibt das Verhalten exakt wie vorher) und baut bei `perspective:false` echte PARALLELE Strahlen (Ursprung wandert lateral mit dem Pixel, Richtung immer `f` — exakte Umkehrung von `worldToScreen`s bereits vorhandenem Ortho-Zweig). Beide Aufrufer in `Wasm3DViewport.tsx` (Klick-Pick + Griff-Drag) reichten schon das volle `orbitCamera(o)`-Objekt (inkl. `perspective`/`orthoHalfHeight`) durch — der Bug sass rein in `cameraRay`, keine Änderung an den Aufrufstellen nötig. +2 Tests (`raycast3d.test.ts`: parallele Strahlen, Rückwärtskompatibilität ohne `perspective`-Feld). `tsc`/`vitest` 341/341 grün.
|
||||
- [x] ~~**3D-Griffe/Editieren im nativen render3d/wasm-Pfad**~~ — **erledigt 2026-07-06:** existierte bereits vollständig im three.js-Fallback (`Viewport3D.tsx`/`drawGrips`), fehlte aber im nativen wasm-Pfad (`Wasm3DViewport.tsx`, seit `WASM_ENGINE_ACTIVE` der Standard-Renderer) — genau wie beim drawings2d-z0-Fund oben eine „existiert, aber auf dem falschen Pfad"-Lücke. Feasibility-Spike (Nutzer-Entscheid „Spike jetzt starten") zuerst nur Wand-Endpunkte, dann auf volle Parität ausgebaut: Wand-Endpunkt-/Höhen-/Verschiebe-Griff + 2D-Zeichnungs-Vertex-/Verschiebe-Griff, dieselben App.tsx-Callbacks (`onEdit3dVertex`/`onEdit3dBody`/`onEdit3dWallTop`) wie die three.js-Sicht. **Wichtige Design-Korrektur unterwegs:** erster Versuch zeichnete Griff-Marker als In-Szene-Geometrie (3-Achsen-Kreuz, dann Drahtgitter-Kugel) über den bestehenden `setHighlightLines`-Kanal — Nutzer-Feedback zweimal `"immernoch kein GUI button sondern geometrie"`: nicht klar genug als klickbarer Punkt erkennbar UND farblich identisch mit dem Auswahl-Umriss auf derselben Ecke. Gelöst durch Umstieg auf ECHTE DOM-Buttons (`position:absolute`-Kreise über dem Canvas, Farben wie drei.js: Vertex orange/Höhe blau/Verschieben grün), deren Bildschirmposition ein `requestAnimationFrame`-Loop aus der aktuellen Kamera nachführt (`worldToScreen`, neue Umkehrfunktion zu `cameraRay` in `raycast3d.ts`) — kein WASM/GPU-Push nötig, nur 2D-Projektionsmathe. Ziehen läuft über `rayPlaneY`/`rayPlane` (neu in `raycast3d.ts`) + dieselben Store-Callbacks. `drawingGripVertices`/`drawingMoveAnchor` nach `src/viewport/drawingGrips.ts` ausgelagert (gemeinsam von beiden Renderern genutzt, sonst zyklischer Import). **Bewusst NICHT gebaut:** Snap/Koinzidenz-Mitführen beim 3D-Ziehen (bleibt three.js-exklusiv). `tsc`/`vitest` 339/339 grün. **Nutzer-bestätigt** (Wand-Endpunkt-Drag funktional in der Tauri-App getestet, dann Marker-Sichtbarkeit korrigiert). **Noch uncommittet.**
|
||||
|
||||
## ❓ Offene Rückfragen (an den Nutzer)
|
||||
|
||||
- [x] ~~**„aki"** Snap/Endpunkt-Farbe~~ — **entschieden 2026-07-04: Sora #5FA1C9** als endgültiger Default festgeschrieben (`src/theme/accents.ts` `DEFAULT_SNAP_COLOR`, Doc aktualisiert). Frage geschlossen.
|
||||
- [ ] Floating-ResourceManager „headless" = ganz ohne Titelleiste? (aktuell MIT)
|
||||
- [ ] Geo-Block: Projekt-MüM zuerst oder Nordstern-Geo-Rendering?
|
||||
- [ ] Verifizieren/klären: Isometrie „echte" orthographische Iso (teilw. durch Locked-Iso `cb8fae5` adressiert)? · TopBar-Detailgrad (zoom% ganz aus Footer)? · DOSSIER-Audit welche Features konkret übernommen?
|
||||
|
||||
---
|
||||
|
||||
## ✅ Erledigt (Verlauf, neueste oben)
|
||||
|
||||
_Nur jüngste Session; ältere Historie siehe `git log` und HANDOVER-Narrative._
|
||||
|
||||
- [x] 2026-07-17 **Schnittebenen 2D↔3D — Phase 1 (Kopplung + Navigation) + Phase-2-Sichtbarkeit** (uncommittet) — der 3D-Live-Schnitt folgt jetzt der gewählten Grundriss-Schnittlinie statt einer fest verdrahteten horizontalen Ebene (`Wasm3DViewport`/`Viewport3D` neue Props `section3d`/`onClearSection3d`; App-State `section3dCutId` → `sectionPlaneFromLevel`). „Im 3D schneiden"-Schalter im Objekt-Info (`host.section3dCutActive`/`onToggleSection3dCut`), Viewport-Knopf an die Linie gebunden. Doppelklick auf die Schnittlinie → 2D-Schnittansicht (`PlanView.onOpenSectionLevel`). Unsichtbare Schnitt-/Ansichtsebene blendet ihre Grundriss-Linie aus (`addSectionLines`). `tsc` + `vitest` 867 grün (+1). Cut-away-Schleier/Farbe (Phase 2) + POV/3D-Schnittfenster (Phase 3) bewusst offen (engine-schwer, Tauri-Verifikation nötig — s. Backlog-Eintrag). Nutzer prüft Phase 1 visuell in Tauri. UI-Nebenarbeit derselben Session (uncommittet): Werkzeug-Panel (Symbole/Liste-Umschalter, neue SIA-Icons, helle Auswahl), Topbar-Quick-Access-Icons entfernt (dupliziert das native Menü), Zahnrad→Einstellungen in Zeichnungsebenen/Ebenen-Köpfen, Footerbar + Snap-Marker + Maß-HUD auf die helle Pillen-Sprache umgestellt.
|
||||
- [x] 2026-07-12 **swissBUILDINGS3D-Import auf Suchradius zugeschnitten** (`31b76a2`) — Nutzer-Frage „importiert es wirklich georeferenziert, oder landet alles am 0-Punkt?" Live gegen die echte STAC-API geprüft: Georeferenzierung selbst war KORREKT (`geocode`/`makeOrigin`/`shiftMeshToOrigin` liefern echte, nicht-triviale LV95-Koordinaten). Echter Bug daneben gefunden: eine STAC-Kachel liefert ALLE Gebäude der Kachel als EIN Mesh (`parseDxf` sammelt alles in denselben Puffer) — ohne Zuschnitt landete bei jedem Import die KOMPLETTE Kachel im Modell, bis zu mehreren km über den Suchradius hinaus (verifiziert: zwei 1.4 km auseinanderliegende Adressen derselben Kachel lieferten VORHER identische, ungeschnittene 62'532-Dreiecke-Geometrie). Neues `clipMeshToBbox` (`swissBuildings3d.ts`) schneidet + kompaktiert auf den gesuchten Radius. +4 Tests, Suite 827 grün.
|
||||
- [x] 2026-07-12 **Fenster/Tür: 2D+3D setzen "aussen" jetzt auf dieselbe Wandfläche** (`a631dda`) — Nutzer-Report „im 2D innen bündig und im 3D aussen bündig". Root Cause: das 2D-Vorzeichen für `insetFace:"aussen"` (`windowSymbol` in `geometry/opening.ts`) war gegenüber der ECHTEN Aussenschicht der Wand (erste Wandtyp-Schicht, kleinster Achs-Offset in `addWallPoche`/`pushSegment`) invertiert — landete am inneren statt äusseren Wandputz. Dieselbe Verwechslung steckte auch in `addOpeningFrameBand` (Tür-Rahmenband), den Sturzlinien und dem Rollladenkasten (alle in `generatePlan.ts`) — alle vier gefixt. Die 3D-Seite (`openingAxisBox`/`resolveFrameNormalRange` in `toWalls3d.ts`) war bereits korrekt (zwei kompensierende Vorzeichen ergaben zufällig das richtige Ergebnis) und blieb unverändert. +1 Regressionstest, der 2D- und 3D-Rahmenposition direkt gegen die bekannte Aussenschicht der Wand prüft (statt nur relative Deltas). Suite 823 grün.
|
||||
- [x] 2026-07-12 **Georeferenzierung: persistenter Standort-Bezug statt Neuberechnung je Import (Teil-Fix)** (`ae47b4f`) — erster Design-Punkt aus dem GEO-BLOCK-Report umgesetzt. Neues `Project.geoAnchor` (`{lv95, model, label?}`) verbindet einmalig „dieser LV95-Punkt = dieser Modellpunkt"; der ERSTE Import in einem Projekt setzt ihn automatisch, alle folgenden Importe (`ContextImportDialog`) verwenden denselben Anker statt je unabhängig `makeOrigin(center)` neu zu berechnen — behebt die Lage-Inkonsistenz zwischen mehreren Importen in einem Projekt. `tsc` sauber, Suite 794 grün. **Bewusst Teil-Fix:** Anker sitzt immer bei Modell-(0,0), es gibt noch kein Werkzeug, um ihn auf einen beliebigen, vom Nutzer gewählten Modellpunkt zu legen — bleibt offen.
|
||||
- [x] 2026-07-12 **Kontext-Meshes (importierte Gebäude/Terrain) im 3D-Viewport anwählbar** (`e6a7738`) — zweiter Design-Punkt aus dem GEO-BLOCK-Report umgesetzt. Raycast/`pickGeometry` (`raycast3d.ts`/`toWalls3d.ts`) um Kontext-Meshes erweitert (rohe Dreiecke aus `project.context`, kaputte Indizes robust übersprungen), neuer Auswahl-Kanal `selectedContextObjectIds` (Store-Slice + `onViewport3dPick`-Zweig, konsistent in ALLE bestehenden Auswahl-Reset-Stellen eingehängt), Highlight als achsenparallele Bounding-Box (volles Dreiecks-Wireframe wäre bei importierten Gebäuden mit tausenden Dreiecken unbrauchbar dicht), Entf-Taste löscht die Auswahl (reused `project.context`-Filter im bestehenden Lösch-Handler), `SitePanel`-Liste hebt die im 3D gewählte Zeile hervor (`host.selectedContextObjectIds`). +9 Tests (`raycast3d.test.ts`, `toWalls3d.test.ts`), Suite 803 grün, `tsc` sauber. **Bewusst nicht umgesetzt:** Ebenen-/Layer-Zuordnung (`categoryCode` an `ImportedMesh`/`TerrainMesh`), volle Attribut-Panel-Integration (`deriveSelection()`) — als eigener, grösserer Schritt eingeschätzt, s. „🔧 In Arbeit" oben.
|
||||
- [x] 2026-07-12 **3D-Ansicht: „Schattiert mit Kanten" (BIM-Look) + Fix falscher Flächendiagonalen** (`a84dc7a`+`7dc8f0d`) — neuer `RenderStyle::ShadedEdges` (render3d): Bauteilfarben + dunkle Modell-Kanten obenauf, wie Revit/ArchiCAD. Direkt danach Nutzer-Report: sichtbare Dreiecks-Diagonalen auf Dach/Fensterglas im neuen Modus. Root Cause: Kontext-Meshes (Dach/Glas/Rahmen, auch swissBUILDINGS3D-Import) werden wegen aktivem Backface-Culling IMMER doppelseitig aufgebaut (`mesh.rs::push_ctx_tri`, Dreieck + gespiegelte Rückseite) — jede Kante bekam dadurch ein exakt entgegengesetztes Normalen-Paar, das die Knick-Erkennung fälschlich als Kante wertete. Fix in `edges.rs::should_draw`: Rückseiten-Duplikate (dot≈-1) werden vor der Rand-/Knick-Entscheidung zusammengeführt. +11 Rust-Tests, 88/88 grün (`--features render`). **Drei Folgepunkte dabei entdeckt, noch offen** (s. „🔧 In Arbeit" oben): Fenster-Rahmenecken/Dach-First ohne sauberen Verschnitt (vermutlich fehlende Boolean-Union, kein Kanten-Bug), OG-Wandflächen mit vielen vertikalen Strichen (nicht diagnostiziert).
|
||||
- [x] 2026-07-12 **swissBUILDINGS3D-Import repariert: Absturz + eigentlicher „funktioniert nicht"-Bug** (`35a6834`+`9705890`) — zwei getrennte, echte Bugs gefunden und behoben. (1) Harter Absturz (JSZip „Invalid string length") bei Kacheln, deren ENTPACKTE Grösse (DXF komprimiert stark) die max. JS-String-Länge sprengt, obwohl die ZIP-Grösse selbst unter dem Limit lag — `downloadAssetText` prüft jetzt zusätzlich die JSZip-interne Grössenschätzung und fängt alle Fehler sicher ab; übersprungene Kacheln landen sichtbar in `skippedTiles`/einer UI-Meldung statt eines stummen Leer-Ergebnisses. (2) **Der eigentliche Grund, warum der Import nie Gebäude lieferte:** die installierte `dxf-parser`-Version liefert Polyface-Mesh-Face-Indizes als vier einzelne Felder (`faceA..faceD`, Gruppencodes 71–74) statt als erwartetes `faces`-Array — `addPolyfaceMesh` prüfte auf ein Array, das nie existierte, jede swissBUILDINGS3D-DXF-Kachel (exakt dieses Format) ergab dadurch 0 Dreiecke. Live gegen echte Kacheln verifiziert (vorher 0 Meshes, jetzt tausende Dreiecke mit realistischen Höhenwerten). +3 Tests mit rohem DXF-Text (deckt auch die zugrundeliegende Bibliothek ab), Suite 794 grün. **Georeferenzierung und „anwählbares Mesh auf Ebene statt Geo-Panel-Eintrag" bleiben als eigene, ungelöste Design-Fragen offen** (s. „🔧 In Arbeit" oben).
|
||||
- [x] 2026-07-12 **Fenster-Grundriss komplett überarbeitet** (`e65a6b7`..`d2758ef`, iterativ über mehrere Live-Prüfungsrunden in Tauri) — Nutzer-Report mit SIA-Referenzbildern deckte mehrere übereinanderliegende Probleme auf, alle einzeln gefixt + verifiziert:
|
||||
- Sims/Anschlag-Kerben nutzten pauschal die volle Wandfläche statt der tatsächlichen (ggf. per `insetFromFace` eingezogenen) Rahmen-Aussenkante.
|
||||
- Stulp-Marken waren kleine, von der Rahmentiefe unabhängige Quadrate statt Profilquerschnitte über die GANZE Rahmentiefe; die Blendrahmen-Querschnittsblöcke an den Laibungs-Enden fehlten komplett — beide jetzt als `meetingMarks` in `windowSymbol` berechnet (inkl. Laibungs-Enden), `generatePlan.ts` nutzt sie direkt statt einer zweiten, abweichenden Neuberechnung.
|
||||
- Zusätzliche `window-mullion`-Trennlinie lag redundant NEBEN dem neuen Rahmenblock (entfernt, für typisierte Fenster reicht der Block; Alt-Pfad ohne Typ behält die Linie).
|
||||
- Glaslinie(n) im Feld (mittel: 1, fein: Doppellinie) komplett entfernt — Nutzer wollte dort keine Haarlinie, unabhängig von Detailgrad/Position.
|
||||
- Pauschale Brüstungslinie (Wandachse, unabhängig von Rahmenposition) UND die „Oberlicht-Andeutung" (gestrichelt bei `transomHeight>0`, ebenfalls immer auf der Wandachse unabhängig davon ob die Schnittebene den Kämpfer trifft) beide entfernt — architektonisch nicht aussagekräftig für einen horizontalen Grundriss-Schnitt.
|
||||
- **Ersatz:** `WindowType.sillLine` — konfigurierbare Auf-/Untersicht-Andeutung (Fläche aussen/innen/beide, Blickrichtung Auf-/Untersicht), an der echten Rahmenkante statt der Wandachse.
|
||||
- **Neu:** `Opening.frameLine`/`sashLine`/`sillLineStyle` — Farbe/Strichstärke je Linien-Kategorie (Blendrahmen+Stulp / Flügelrahmen+Sprossen / Sims) im Objekt-Info-Panel einstellbar, Default = bisheriges Verhalten.
|
||||
- +~30 Tests über mehrere Dateien, Suite 790 grün.
|
||||
- [x] 2026-07-12 **Dach-Wand-Verschneidung (Z-Fighting im 3D behoben)** (`a584911`) — Nutzer-Report mit Screenshot (flackerndes Rausch-Muster wo Wand-Oberkante die Dachschräge kreuzt). Root Cause: Wände bekamen ihre Höhe unabhängig vom Dach, `collectCeilingCutters` kappte nur gegen `project.ceilings`, nie gegen `project.roofs` — wo die geneigte Dachfläche den flachen Wand-Top kreuzte, überlappten sich beide Volumen. Fix: `roofUndersideAt` (`geometry/roof.ts`, Ebenengleichung je Dachfläche + Punkt-in-Polygon) liefert die Dach-Unterkante an einem Grundriss-Punkt; `clipPieceToRoofs` (`toWalls3d.ts`) zerlegt betroffene Wand-Achsenstücke in feine ~15-cm-Schritte und klemmt jeden auf die dort gemessene Unterkante — Treppenstufen-Annäherung an eine echte geneigte Giebelwand-Stirnfläche (render3d-Wandkörper haben nur einen flachen Top; eine echte Schrägfläche bräuchte einen neuen Mesh-Pfad, bewusst nicht gebaut). Ohne Dach im selben Geschoss unverändertes Verhalten. +9 Tests (5 `roofUndersideAt`, 3 Integration inkl. Regressionsfund einer legitim gekappten Putz-Gehrungsspitze im Sample-Projekt), Suite 781 grün. Verwandt: [[design-schicht-verschneidung-3d-schnitt]] (gleiche Kategorie wie „Wand endet an Decke", hier für geneigte statt horizontale Cutter).
|
||||
- [x] 2026-07-12 **Fenster-Grundriss grob/mittel/fein nach SIA 400 klar unterschieden** (`13cc6a0`) — Nutzer-Report „mittel und fein sind genau gleich, grob ist viel zu grob" behoben: `windowSymbol` (`geometry/opening.ts`) staffelt jetzt nach SIA 400 Anhang B.9.1 Fig. 36–38 statt DIN — grob (1:100) nur eine Glaslinie, mittel (1:50) Blendrahmen + Flügel-Trennlinien + 1 Stulp-Quadrat je Flügelstoss, fein (1:20) zusätzlich verschachtelte Flügelrahmen + Glas-Doppellinie (Isolierverglasung) + 2 Stulp-Quadrate. `generatePlan.ts` reicht `detail` durch, rendert die neuen Stulp-Marken (`window-stulp`) nur im typisierten Pfad (kein Fake-Stoss bei reinem `wingCount`-Altfall). Referenzdoku `docs/research/sia400-fenster-tueren.md` (Fig.-Transkription aus der SIA-400-PDF, lokal excluded). +9 Tests, Suite 753 grün. Visuell per Puppeteer verifiziert. ✅ **Folge-Fix `e309e54`:** Öffnungssymbol-Kommentare in `toElevation.ts`/`toWalls3d.ts` waren fälschlich „DIN" benannt (Grundlage ist SIA 400 B.9.1.3) — reine Doku-Korrektur, Symbol-Geometrie selbst war bereits korrekt (SIA-PDF zeigt nur Sinnbild-Namen, keine Pfeilgrafiken, daher keine Geometrieänderung). ✅ **Wand-Poché bei „grob" jetzt immer vollschwarz** (`0240a23`, 2026-07-12) — war zuvor nur schwarz, wenn das dominante Bauteil selbst `pattern:"solid"` hatte (bei mehrschichtigen Wandtypen mit Backstein/Dämmung/Verputz blieb es weiss). Fix (unconditional `HATCH_INK` bei grob) von Hermes/Qwen3 geliefert (zweiter Versuch, erster war die o. g. Fehleinschätzung), von mir verifiziert + um fehlenden Regressionstest + Aufräumen der toten `backbonePocheFill`-Hilfsfunktion ergänzt. +2 Tests, Suite 755 grün.
|
||||
|
||||
- [x] 2026-07-11 **Schnitt/Ansicht auf VW-Niveau (grosser Block)** — Schnitt ENTSPIEGELT (`983d061`, u-Achse = Betrachter-Rechts, Rust+TS koordiniert, Engine neu gebaut); Kanten geschnittener Bauteile gefiltert + verdeckte Kanten opt-in (`4c4c990`); Wand-Terminierung wirkt im Schnitt auch bei Decken-ANSCHLUSS ±5 cm (`a612ae5`); Dämmschraffur-Orientierung Wand/Decke korrekt (Sprossen quer zur Schicht, empirisch verifiziert, `a612ae5`+`38a4d8c`). Ansicht als LINIENZEICHNUNG (`c02a02c`): weisse Flächen + XOR-Silhouette (xorOutlineEdges — Teilbox-Innenkanten heben sich auf), Fenster mit Blendrahmen→Flügelprofil→Glas, Sims mit Tropfkante, DIN-Symbole exakt auf den Glasfeld-Ecken (`ee9aeda`). **3D-Öffnungen VW-fein** (Merge `31d7aef`): vorstehende Flügelrahmen, DIN-Symbole auf dem Glas (openingPlaneBox-Prismen), 3D-Fensterbank + Tropfkante, Zargen-Umgriff; Staffelung grob/mittel/fein. Display/Print-Umschalter in allen 2D-Darstellungen. **Offen:** Tauri-Visualabnahme 3D; Schnitt-Auswahl-UX (Treffer nur auf der Poché).
|
||||
- [x] 2026-07-11 **Lineale + VW-Zeichen-Feedback komplett** (`1945fec`, `8f85e13`, `7898a0d`, `242b850`) — Lineale oben/links in allen 2D-Ansichten (Meter-Ticks, Cursor-Marker); Cursor-HUD im Programm-Look mit Live-Echo getippter Werte (oben+unten synchron), Tab-Feldwechsel, Direkt-Tippen; Δx/Δy beim Rechteck; lange Führungslinien + Winkelbogen mit 0°-Referenz.
|
||||
|
||||
- [x] 2026-07-11 **Schnitt ausgebaut** (`b86fca2`) — Befehl `sectionline`/`viewline` (Aliase schnittlinie/ansichtslinie, BIM-Ribbon „Schnitte"): zwei Klicks setzen die Schnitt-/Ansichtslinie (neue Ebene bei Bedarf). Schnittführungs-Symbol im Grundriss (Strichpunkt, Endmarken, Richtungspfeile, Label) + Pick-Band; Schnittlinie selektierbar mit Attribut-Sektion (Name, Blickrichtung umkehren, Endpunkte, Tiefe, Löschen/Entf). **Editieren IM Schnitt**: Cut-Polygone tragen die Quell-Element-Id (sourceId, durch Schicht-Zerlegung/Terminierung/Dominanz propagiert) → Klick im Schnitt wählt das Bauteil, Panels editieren, Schnitt rechnet neu. Schnitt-Tiefe `DrawingLevel.depth` (Owner-Distanz-Näherung, TODO exakte Kanten-Tiefe bräuchte section.rs). **Offen:** Endpunkt-Drag-Griffe der Schnittlinie im Grundriss.
|
||||
- [x] 2026-07-11 **Ansicht (Elevation) als echte 2D-Darstellung** (`456ecc5`+`a6db682`) — `toElevation.ts`: Painter-Projektion (Fassaden/Decken fern→nah, Rückseiten-Culling, Tiefen-Graustaffelung), Fenster/Türen als Rahmen+Glas, Bodenlinie, **Dächer** (Flächen + Giebel aus roofGeometry) und **Schatten ein/aus** (45°-Schlagschatten auskragender Decken UND Dachflächen, Sutherland–Hodgman-geclippt; Toggle in der Ansichts-Leiste, `DrawingLevel.shadows`). Rein TS/synchron ohne WASM. **Offen:** exakter Hidden-Line statt Painter (dokumentierte Näherung), Material-/viewHatch-Füllungen je Fassade.
|
||||
- [x] 2026-07-11 **VW-Zeichengefühl: Cursor-HUD + Winkelraster** (`975ce3f`) — beim Zeichnen L/W-Kästchen am Cursor (`L: 3.118m W: 60.000°`, VW-Stil), weiches Einrasten auf 15°-Vielfache (±2.5°, konfigurierbar) mit Winkel-Badge + gestrichelter Führungslinie; Vorrang Objekt-Snap > Shift/Ortho > Winkelraster > Raster; Toggle in der Fang-Leiste. Das bis dahin nie gerenderte `ToolDraft.hud` lebt jetzt (line/polyline/wall/rect/circle/arc).
|
||||
- [x] 2026-07-11 **Dach im 3D anwählbar** (`355c1d4`) + **2D-Schnitt = 3D Wand-Terminierung** (`0e02c2b`) — Ray-Dreieck-Pick über Dachflächen/Giebel; applyWallTermination spiegelt terminateOrSubtractSpans im (u,v)-Schnittraum (below/above kappen an der Decken-UK/OK).
|
||||
|
||||
- [x] 2026-07-10 **Fenster-/Tür-Einstellungsdialog + reiche Öffnungs-Parameter** (`1bbe814`+`c734802`+`2e13ec3`, Studie [docs/design/window-editor-vectorworks-study.md](docs/design/window-editor-vectorworks-study.md)) — Nutzer-Feedback „Fenster/Türen mega mager". Das ⚙ der Öffnungs-Sektion öffnet neu einen dedizierten Dialog (`OpeningEditorDialog.tsx`): Kategorie-Sidebar (Basis/Grösse/Rahmen/Flügel/Sonnenschutz bzw. Türblatt), Flügeltabelle (Flügel/Pfosten + Öffnungsart + Anschlag je Flügel), Live-2D-Frontalansicht, Stil-Leiste mit „Als Stil speichern" (klont Typ → neuer benannter WindowType/DoorType). Modell additiv: `SashDef[]`/`shading`/`glazingPanes` + Helfer `sashesOfWindowType`/`glazingPanesOf`. Renderer konsumiert sie: 2D-Flügeltrennlinien aus Flügelbreiten + Öffnungsandeutung, glazingPanes-Glaslinien, gestrichelte Rollladenkasten-Kontur; 3D mehrscheibige Verglasung. +26 Tests, 659/659 grün. **Offen (P1+, Studie §4):** asymmetrische Rahmenbreiten, Oberlicht/Unterlicht als eigene Felder, Sprossengitter, Bank/Nische, Form (Rund/Spitz), echte wgpu-3D-Vorschau im Dialog, 3D-Rollladenkasten-Box.
|
||||
- [x] 2026-07-10 **Dach-3D-Ziehgriffe** (`4ef40a0`) — gewählte Dächer haben im wgpu-Viewport Eckpunkt-Griffe (achsparalleles Resize, Gegenecke fix), einen Verschiebe-Griff und einen First-Griff (vertikal ziehen → Dachneigung, Firstlage bleibt). Store `moveRoofGrip`/`moveRoofBy` (coalescing) + `onEdit3dRoofPitch` (Höhe→Neigung). +3 Tests. Schliesst die „optionale Folge" des Dach-Features (s. u.). **Tauri-Visualabnahme offen.**
|
||||
- [x] 2026-07-10 **Messwert im Objekt-Info** (`9b6dd80`) — Live-Messwerte erschienen nicht im Panel; Ursache war ein eingefrorenes Memo (`baseHost`-Deps ohne `draft`). Fix: `measurement` pro Render live in `hostWithMode` injiziert.
|
||||
|
||||
- [x] 2026-07-09 **Dächer (Grundfeature)** (`1195d2a`+`31f2d63`) — Roof-Element + `geometry/roof.ts` (Flach/Pult/Sattel/Walm/Mansarde/Zelt, BBox-basiert, First X/Y). Befehl „Dach" (BIM-Ribbon, Rechteck aufziehen, Form als Inline-Option), 2D-Plan (Traufe/First/Grat/Knick), 3D (emitRoofs, terrakotta), Demo-Dach RF1. +19 Tests. **Offen:** Auswahl/Attribut-Editieren/Löschen (s. „Als Nächstes").
|
||||
- [x] 2026-07-09 **Fenster/Tür-Feedback** (`4beae72`+`89e737b`) — Fenster im 3D tiefer (Rahmen füllt Wanddicke, dünne Scheibe mittig); Detailgrad grob/mittel/fein wirkt im 3D; Fenster↔Tür-Umschalter entfernt; platzierte Öffnungen bekommen Standard-Typ (sonst kein Rahmen). Kernursache „sieht im 3D nicht so aus" = fehlender typeId behoben.
|
||||
|
||||
- [x] 2026-07-09 **Decken-Griffe im 3D** (`973ac6d`) — gewählte Decken haben im wgpu-Viewport Eckpunkt- + Kanten-Mittelpunkt-Griffe (rautenförmig, eigene Farbe) + Verschiebe-Griff → moveCeilingGrip/moveCeilingEdge/moveCeilingBy (Store-Actions existierten schon fürs 2D). Neu: 3D-Griffgeometrie (computeGrips), Kanten-Drag (GripDrag „edge"), Prop-Kette Wasm3DViewport←Viewport3D←App (editCeilings/onEditEdge, Ziel um ceilingId). three.js-Sicht unverändert. **Tauri-Visualabnahme offen.**
|
||||
- [x] 2026-07-09 **Text-Styling-Leiste wirkt auf Freitext** (`d0b9d22`) — selektierter Freitext/Textspalte (Drawing2D shape „text") ist Formatier-Ziel der Oberleiste: Drawing2D-Text.marks (Schrift/fett/kursiv/Farbe); Grösse bleibt über Modell-Höhe (Meter, nicht pt). App.textTarget adaptiert via docFromText/plainText; generatePlan/PlanView rendern die Marks. +3 Tests.
|
||||
- [x] 2026-07-09 **Schichttrennlinie als Wand-Referenzlinie** (`938d642`) — Wall.referenceOffset (freier Achsversatz) übersteuert left/center/right; Object-Info-Dropdown listet je interne Fuge einen Eintrag (kumulierte Schichtdicken in selectionInfo). wallReferenceOffset bleibt EINE Quelle → 2D/Schnitt/3D erben es. +3 Tests.
|
||||
- [x] 2026-07-09 **Mess-Werkzeug: Polygonzug + Fläche + Objekt-Info** (`492e1f8`) — Länge je Segment + aufsummiert + FLÄCHE (ab 3 Ecken, Gauss); Live-Werte dauerhaft im Objekt-Info-Panel (ToolDraft.measure→PanelHost.measurement→ObjectInfoPanel); Rechtsklick beendet Pfad + armiert neuen (mehrere nacheinander). +4 Tests.
|
||||
- [x] 2026-07-09 **Tür/Fenster tief ausgearbeitet** (`fa40429`/`142ba7e`/`9a65900`) — DoorType/WindowType um frameKind (Zarge/Blockrahmen), frameWidth, insetFromFace/insetFace (Schichteinzug), transomHeight (Oberlicht), mullionRows (Kämpfer). ResourceManager-Typeditor + Seeds (Haustür Blockrahmen/Oberlicht, Fenster 2-flügl.+Oberlicht). 2D: opening-frame/-transom-Primitive. 3D: emitOpeningFrames (Rahmen/Sprossen/Kämpfer) + einzugs-/oberlicht-bewusste Scheiben. +13 Tests. **3D-Visualabnahme offen.**
|
||||
- [x] 2026-07-09 **LICENSE** (`dbe7d37`) — offizieller AGPL-3.0-Text (gnu.org).
|
||||
|
||||
- [x] 2026-07-07 **IFC4-Export (erste Scheibe)** — reiner Kern `src/export/exportIfc.ts` (`exportIfcSpf(project)→string`, kein WASM/Dependency, direkt IFC-SPF-Text, Muster wie exportDxf). Räumliche Hierarchie IfcProject→IfcSite→IfcBuilding→IfcBuildingStorey (je `kind:"floor"`), Elemente via IfcRelContainedInSpatialStructure. Geometrie durchgängig IfcExtrudedAreaSolid: Wand→IfcWall, Decke→IfcSlab, Öffnung→IfcOpeningElement+IfcRelVoidsElement + Tür/Fenster-Füllung+IfcRelFillsElement, Extrusion→IfcBuildingElementProxy, Treppe→IfcStair (vereinfachter Hüllkörper, keine Stufengeometrie). IFC-22-Zeichen-GUIDs deterministisch aus Element-id (FNV→128bit→IFC-Base64, gegen IfcOpenShell-Referenz verifiziert). SI-Einheiten, OwnerHistory. Quick-Access-Button (`deployed_code`) + `onExportIfc` in App.tsx (Blob `model/ifc`, `<name>.ifc`) + i18n `file.exportIfc`. 8 Tests (wichtigster: KEINE dangling refs — jede `#N`-Referenz definiert + eindeutig). vitest 391. **Bewusst offen/vereinfacht:** IfcMaterialLayerSet (DirectionSense/Offset-Semantik ohne Viewer zu riskant — Folge-Item), Treppen-Stufengeometrie, Tür/Fenster nutzt dieselbe Box wie die Öffnung (kein Rahmen/Blatt). `host.ts` bewusst NICHT angefasst (Export-Callbacks leben in TopBarProps, nicht PanelHostValue — konsistent mit DXF/Schedule). **⚠️ Unit-Tests beweisen nur STRUKTURELLE STEP-Validität — Nutzer muss die .ifc in echtem Viewer (BIMcollab Zoom / IFC.js / Revit) gegenprüfen.** **Noch uncommittet.** (STL/OBJ-Export + IFC-Import + DWG-Schreiben bleiben offen — s. Interop-Befund.)
|
||||
- [x] 2026-07-07 **ObjectInfo: Volumen/Länge bei Wand + Volumen bei Decke** (Nutzer-Wunsch) — `selectionInfo.ts`: WallInfo um `length`/`grossVolume`/`openingVolume`/`netVolume` (Netto = Länge×Dicke×Höhe − Σ Öffnungen[Breite×Höhe×Dicke]), CeilingInfo um `volume` (Fläche×Dicke). Panel zeigt bei Wand Länge + Netto-Volumen (Tooltip Brutto−Öffnungen), bei Decke Fläche + Volumen. +i18n `objinfo.volume`/`objinfo.wall.length`/`objinfo.wall.volumeGross`. tsc/vitest 391. **Noch uncommittet.**
|
||||
- [x] 2026-07-07 **UI-Feinschliff-Runde (Nutzer-Session, Tauri live abgenommen):** (1) Kamera-Settings (FOV-Popover) ZUSÄTZLICH oben in der TopBar-Chrome neben CSV, dafür UNTEN aus beiden Ribbon-Tabs entfernt → unten saubere 2×4 View-Icons (Nutzer-Korrektur: erst waren fälschlich alle Presets mit oben). (2) ObjectInfo: Bezugspunkt-Würfel fix quadratisch (64px, skaliert nicht mehr mit Panelbreite), X/Y/Z als Spalte rechts daneben (`objinfo-refrow`/`objinfo-xyzcol`). (3) Wand/Decken-UK/OK-Unterzeile in normale Label/Wert-Struktur: „Referenzgeschoss" + Dropdown in der Wertspalte, bei Eigene „Eigene Höhe" linksbündig (+2 i18n-Keys `objinfo.anchor.*`). (4) Ansichten-Ribbon: Detailgrad + Darstellung übereinander gestapelt (eine `tb-stack`-Gruppe), alte separate Darstellungs-Gruppe entfernt. tsc sauber, vitest 377/377. **Noch uncommittet.**
|
||||
- [x] 2026-07-07 **Basis-Materialien mit Default-Textur** (Nutzer-Wunsch „nicht dass ich sie zuweisen muss und jeder Reset zerstört es") — Seed-Components in `src/model/sampleProject.ts` tragen ab Werk `material` (via `materialFromAsset`, exakt der ResourceManager-Zuweisungsweg): Aussen-/Innenputz→Plaster001, Backstein→Bricks104, Beton→Concrete048. Bewusst OHNE: Dämmung + Estrich (kein passendes Asset in der 12er-Bibliothek — nicht geraten). Alt-Projekte unberührt (Feld optional). three.js-Viewport rendert automatisch texturiert; nativer Renderer weiterhin ohne Texturen (bekannter 3D-REST). tsc sauber, vitest 377/377. **Noch uncommittet.**
|
||||
- [x] 2026-07-07 **Element-Übersicht / BIM-Tree-Panel** (ROADMAP §11, Phase-1) — neues `src/panels/ElementTreePanel.tsx`: dreistufiger Baum Geschoss→Bauteilklasse→Element aus `scheduleRows()` (Textfilter mit Auto-Aufklappen; Klick = selektieren, Shift/Doppelklick = selektieren+zoomen). `ScheduleRow.floorId` additiv (CSV unverändert), Host-Callback `onSelectScheduleRow` (App.tsx setzt korrekten Selektionskanal, wechselt bei Bedarf Geschoss + defert 1 rAF gegen den Geschosswechsel-Reset). Registriert in `builtinPanels.tsx` + Default-Rechts-Dock (`LAYOUT_VERSION` 8→9). Legacy `project.doors` bewusst ausgefiltert (tot; echte Türen sind `Opening kind=door`). +i18n `elements.*`. vitest 377/377, tsc sauber. **Bekannte Grobheit:** Zoom nutzt `fit()` (ganzer Plan) statt `fitSelection()`, weil letzteres nur lokal geklickte Indizes kennt und `PlanView.tsx` gesperrt war — Element wird per Auswahl-Outline sichtbar. **Nebenbei:** NUL-Byte in `exportSchedule.ts` (Kollision paralleler Schreibzugriffe) repariert. **Noch uncommittet.**
|
||||
- [x] 2026-07-07 **Decken-UK-Verdrahtung nachgezogen** (Orchestrator) — `onSetCeilingBottom`-Handler in App.tsx ergänzt (Muster `onSetCeilingTop`); das UI-Feld aus dem Decken-UK/OK-Item ist damit funktional statt inert. tsc sauber, vitest 377/377.
|
||||
- [x] 2026-07-07 **Kamera-Presets: Kardinal + Iso-Oktanten** (Milestone B3-Teil, OHNE Nord-Rotation — die bleibt Nutzer-Entscheid) — `View3d` jetzt front/back/side(=Rechts)/left/top/iso + 3 weitere obere Iso-Oktanten (vorne-links/hinten-rechts/hinten-links; untere 4 bewusst weggelassen) + perspective. Rust `CameraPreset`/`preset_camera`/`web.rs` synchron erweitert (+2 Tests, cargo render3d 60/60), three.js-Pfad (`Viewport3D.tsx`) in Parität, `VIEW3D_ORBIT`-Seeds analytisch. UI: 4 Kardinal-Buttons + Iso-Split-Button mit Oktanten-Popover (ViewRibbonTab + Ribbon3dTab). WASM-Rebuild sauber, tsc sauber, vitest 372/372. **Visuelle Abnahme im Tauri offen.** **Noch uncommittet.**
|
||||
- [x] 2026-07-07 **Decken UK/OK-Override** (ROADMAP §11, Phase-1-Item) — `Ceiling.bottom?: VerticalAnchor` (additiv), `ceilingVerticalExtent` löst bottom-Anchor vor `zTop−thickness` auf (exakt das Wand-Muster); wirkt automatisch in 3D (`toWalls3d`) UND Schnitt (`toSection`), da beide denselben Resolver konsumieren. ObjectInfo: UK-Zeile jetzt editierbare `VerticalAnchorRow` (i18n-Key existierte). Neue Tests `model/wall.test.ts` (5). vitest 377/377, tsc sauber. **⚠️ Folge-Zeile:** `onSetCeilingBottom`-Handler in App.tsx (~5 Zeilen, Muster onSetCeilingTop) war ausserhalb der Agent-Lane — wird vom Orchestrator nachverdrahtet, bis dahin ist das UI-Feld inert. **Noch uncommittet.**
|
||||
- [x] 2026-07-07 **A1 Override-Regel-Engine** (Top-Item des DOSSIER-Audits) — `OverrideRule` (condition: layer_name|object_name × equals|contains|starts_with|not_equals → actions: color/lineweight/linetypeId) additiv an `Project.overrideRules`; pure Engine `src/overrides/engine.ts` (`effectiveOverrides`: additiv, oberste aktive Regel gewinnt pro Feld, case-insensitiv, layer_name matcht Name ODER Code); Einhängung als Render-Dekoration in `generatePlan.ts` NACH der By-Layer/By-Object-Kette (Wände/Decken/Räume/Öffnungen/Treppen/Drawing2D; Projektdaten unverändert, jederzeit reversibel); ResourceManager-Tab „Overrides" (Master-Detail, Prioritätsliste ▲/▼, Aktiv-Checkbox). +18 Tests (engine 12, generatePlan-Integration 6). vitest 372/372, tsc sauber. **Bewusst offen:** user_string-Bedingung (kein Tag-Feld am Element), 3D-/Schnitt-Pfad, Presets/Templates (C2), Drag-Reorder; Overrides-Tab im ANGEDOCKTEN ResourcesPanel-Adapter vorerst nur lesend (Handler optional gehalten — Mini-Folge-Item). **Noch uncommittet.**
|
||||
- [x] 2026-07-07 **SIA-416: AGF-Kategorie ergänzt** (Lücken-Schluss aus dem Milestone-Abgleich) — `SiaCategory`+`AGF` in `geometry/roomArea.ts`, Rollup NUR in GF (nicht NF/NGF): Bilanz-Reihenfolge HNF·NNF·NF(Σ)·VF·FF·NGF(Σ)·KGF·AGF·GF(Σ), GF = NGF+KGF+AGF. Dropdown/Bilanz-Panel/CSV übernehmen generisch ohne Änderung. Neue Testdatei `roomArea.test.ts` (8 Tests). vitest 366/366 grün. **Noch uncommittet.**
|
||||
- [x] 2026-07-07 **D2 Bauteil-CSV volles Element-Set** — `exportSchedule.ts` deckt jetzt Tür/Fenster/Öffnung/Treppe/Extrusion zusätzlich zu Wand/Decke ab, exakt nach dem in HANDOVER.md hinterlegten Scope (Tür/Öffnung: Breite/Höhe/Fläche, Geschoss via Wirtswand; Treppe: Lauflänge + totalRise, Fläche bewusst leer; Extrusion: Höhe + polygonArea, Geschoss via levelId; Räume weiterhin im eigenen Raum-CSV). Aggregat-Zeile generalisiert (`totalLength`/`totalArea` > 0 ? Wert : leer — kein irreführendes 0.00 bei Treppe). Tests erweitert (Fixture + Zeilen-Asserts je neuem Element). `tsc` sauber, `vitest` 341/341 grün. **Noch uncommittet.**
|
||||
|
||||
- [x] 2026-07-06 **`extrude`-Befehl im 3D-Ribbon-Tab** — `Ribbon3dTab` (`src/ui/TopBar.tsx`) kannte nur Kamera/Darstellungsart, nicht den seit Phase 3 existierenden `extrude`-Befehl; neue Gruppe mit Befehls-Button (gleiches Muster wie `RibbonButton`s Befehls-Zweig: `CommandIcon` + Label aus der Registry, deaktiviert ohne aktives Geschoss, aktiv-Highlight bei laufendem Befehl). `onRunCommand`/`activeCommand` neu durchgereicht (App.tsx). tsc sauber.
|
||||
- [x] 2026-07-06 **truck-Integration Phase 4 (Boolean) — Mesh-CSG via `csgrs` umgesetzt, technisch bewiesen** (Details s. oben bei „Phase 4"): zweiter Spike (`csgrs`, Mesh-Ebenen-BSP statt B-Rep) löst exakt den Fall, an dem `monstertruck-solid` scheiterte; nutzerautorisiert als echte (Git-gepinnte) Abhängigkeit in `trucksolid` eingebaut (`boolean.rs`, WASM-Export `boolean_mesh`, TS-Wrapper `booleanMesh()`). `cargo test` 11/11, `wasm32-unknown-unknown`-Build + echter `wasm-pack`-Build sauber. UI-Verdrahtung (wann automatisch subtrahieren) bewusst offen gelassen — Scope-Entscheidung, kein Technik-Risiko mehr.
|
||||
- [x] 2026-07-05 **Schnitt/Ansicht Phase 2: Decke über Schnittebene = gestrichelte Überkopf-Linie** (`39ddd9b`) — freier Decken-Umriss (Balkon-/Vordach-Überstände) jetzt gestrichelte Haarlinie (`OVERHEAD_DASH` in `generatePlan.ts`, `addCeilingPoche`) statt Volllinie; Decken-Fläche war schon Ansicht (viewHatchId). `lwMm`-Param entfernt (feste Haarlinie). +1 Test, Suite 337.
|
||||
- [x] 2026-07-05 **Schnitt/Ansicht Phase 1: Wand unter Schnittebene = Ansichtslinie** — `addWallPoche(viewOnly)` in `generatePlan.ts`: `wall.height < floor.cutHeight` (Brüstung/Podestrand) → Umriss-Haarlinie (`fill:"none"`, NO_HATCH) statt Schnitt-Poché; normale Wände unberührt (Segment-/Gehrungs-/Öffnungslogik geteilt). +2 Tests (`generatePlan.viewwall.test.ts`), Suite 336. Erster Baustein des BIM-Standard-Items „Schnitt vs. Ansicht nach Schnitthöhe".
|
||||
- [x] 2026-07-05 **Ribbon Phase 4: modulare Custom-Bar** (Tab „Eigene") — datengetrieben auf der bestehenden Registry: neuer `custom`-Tab, `CustomBar` in `RibbonBar.tsx` rendert die vom Nutzer gewählten Items + einen „+ Hinzufügen"-Picker (Dropdown mit Häkchen, listet ALLE Werkzeuge/Befehle aus `ALL_RIBBON_ITEMS`, Klick = an/abwählen). `itemKey`/`itemFromKey` für stabile Persistenz; State in App (`customItems`) via localStorage (`dossier.ribbon.customItems`). Gleiche `RibbonButton`-Aktivierung wie normale Tabs (kein Sonderweg). tsc + Suite 334 grün.
|
||||
- [x] 2026-07-05 **Ribbon Phase 3 (Teil): Werkzeug-Sidebar raus** — `DEFAULT_LEFT_GROUPS` nur noch `attributes` (volle linke Höhe), `LAYOUT_VERSION` 7→8 (gespeicherte Layouts fallen auf neuen Default zurück → sichtbar). Wandtyp-/Deckentyp-Picker von `ToolsPanel` ins `AttributesPanel` verschoben (Nutzer-Entscheid „ins Attribute-Panel") — erscheint bei aktivem Wand-/Decken-Werkzeug (Sektion „Neues Bauteil", auch ohne Auswahl), Host-State unverändert. `ToolsPanel` bleibt registriert (per Fenster-Menü andockbar), `Dropdown`-Import dort entfernt. tsc + Suite 334 grün. **Offen:** Objektinfo unter Attribute mergen.
|
||||
- [x] 2026-07-05 **Fix: gezeichnete/importierte Kreise+Bögen im WebGL unsichtbar** — der WebGL-Compiler (`glPlan/glPlanCompile.ts`, Default-Renderer) dispatchte `polygon`/`line`/`arc`, aber NICHT `drawingCircle`/`drawingArc` → seit `4ac99d3` (Kreise als echtes `drawingCircle`-Primitiv statt 64-Eck-Polygon) fielen sie im GL still raus (SVG-/WASM-Pfad hatten sie, GL nicht). Zweig ergänzt: bildschirm-adaptive Tessellierung wie beim `arc`-Primitiv (Kreis geschlossen + optionale Vollton-Füllung, Bogen a0..a1 offen). tsc + Suite 334 grün. **Nutzer-Report** („zeichne Kreis, bleibt nicht sichtbar").
|
||||
- [x] 2026-07-05 **Textur-Spike verifiziert erledigt** (`0ca3b1d`) — bei der Backlog-Abarbeitung festgestellt, dass die render3d-Textur-Spike (`RenderStyle::Textured`, Schachbrett, planare Meter-UVs, `MESH_TEXTURED_WGSL` group 1, `spike3d`-`T`-Toggle) bereits vollständig umgesetzt + committet war; `cargo test` 58/59 grün, keine neuen Deps, Alt-Pfad bitgleich. „Als Nächstes"-Eintrag war veraltet → abgehakt; PBR-Restlückenliste (~18–29 PT) in 3D-REST übernommen.
|
||||
- [x] 2026-07-05 **Ribbon-Feinschliff (OCS-Zeile)** — Tabs in die TopBar-Zeile verlegt (`456ebc8`, eine Leiste Chrome+Tabs, Inhalt darunter; Tab-State in App); OCS-Chrome: kleine Wortmarke „dossier." + Quick-Access-Icons für ALLE Datei-/Export-Aktionen statt Burger-Menü, Zeile auf 26px (`4e3b074`); Text-/Font-Formatierung mit der Ansichts-/Zoom-Steuerung auf EINE Leiste gelegt, „Ansichten" als Standard-Tab zuerst + initial aktiv (`fe22cbf`). Alle tsc + 331 Tests grün. Visuelle Abnahme (26px, Icon-Sitz) noch offen.
|
||||
- [x] 2026-07-05 **Ribbon Phase 2: TopBar → „Ansichten"-Tab gemergt** (`85011cb`, Nutzer-Entscheid „voll mergen") — die Ansichts-/Zoom-/Darstellungs-Cluster der TopBar (View-Grid+Kamera, Ebenen-/Zeichnungs-Kombis, Detailgrad, Massstab/Zoom, Darstellungsart) als `ViewRibbonTab` (in `TopBar.tsx`) in den Ansichten-Tab verschoben; App reicht sie als `viewsContent`-Node an `RibbonBar` (analog Layout-Menü). TopBar ist jetzt schmale globale Leiste (Marke/Ressourcen · Text · Datei/Export/Einstellungen · Fensterknöpfe). `TopBarProps` entsprechend verschlankt. Visuelle Feinabstimmung offen.
|
||||
- [x] 2026-07-05 **Ribbon-UI Phase 1** (`9d6e86d`) — datengetriebene Tab-Leiste (`src/ui/ribbon/`: `ribbonItems.ts` Registry, `RibbonBar.tsx`, `CommandIcon.tsx`) unter der TopBar, additiv (Sidebar bleibt). Tabs 2D·3D·BIM·Ansichten; 2D=Zeichnen(select/line/polyline/rect/circle/arc)+Ändern(move/copy/mirror/offset/trim/join), BIM=Bauteile(wall/window/door/stair/ceiling/room); 3D/Ansichten noch leer. Ein Aktivierungs-Pfad: tool→`onSelectTool`, command→`onRunCommand`(engine.start); Aktiv-Highlight über `activeTool`/`engineView.commandName`. `ToolIcon` aus ToolsPanel exportiert (wiederverwendet).
|
||||
- [x] 2026-07-05 **Attribut-Panel: ein Grid, volle Breite** (`ace0dc6`) — drei getrennte Grids zu EINEM durchgehenden Zwei-Spalten-Grid gemerged (Wertspalte über alle Sektionen bündig), Dropdown-Pills füllen die Wertspalte (fixe Breiten raus), doppeltes Eigen-Padding + zweiter „Attribute"-Titel entfernt. **Nutzer-Report.**
|
||||
- [x] 2026-07-05 **Eigenschaften-Grid OCS-Stil** (`00733d8`) — `.attr-*` als klares Grid: Sektion-Balken, Zeilentrenner, füllende linksbündige Wertfelder (Texte/Zahlen/Dropdowns einheitlich). Erste Stufe der Ribbon-UI-Vision.
|
||||
- [x] 2026-07-05 **Zoom-Scroll-Fix** (`077e774`) — Dokument-Overscroll/Rubberband gesperrt (`html,body,#root { overflow:hidden; overscroll-behavior:none }`); Viewport-Zoom zieht die UI nicht mehr mit (macOS). **Nutzer-Report.**
|
||||
- [x] 2026-07-05 **Kreis- + Bogen-Werkzeug** (`e454eab`, `bd2b12b`) — Kreis-Toolbar (circleCommand gekoppelt) + neues `arcCommand` (3-Klick CCW), beide mit Toolbar-Icon/i18n; rendern glatt via `drawingCircle`/`drawingArc`.
|
||||
|
||||
- [x] 2026-07-05 **DXF-Import: Platzierungsoption** (`dd76ec8`) — ImportDialog fragt „relativ zum Nullpunkt" ODER „in die Mitte der aktuellen Ansicht"; `PlanViewHandle.viewCenterModel()` neu; `runImport` verschiebt den Gesamt-Umriss (bbox-Mitte → Ansichtsmitte). tsc + Suite 331 grün.
|
||||
- [x] 2026-07-05 **Zeichnungen Copy/Paste über Geschosse** (`376aa67`) — Ctrl/Cmd+C kopiert gewählte Drawing2D tief; Ctrl/Cmd+V fügt Klone (neue IDs) auf dem AKTIVEN Geschoss ein und wählt sie. Für z. B. importiertes Mobiliar. Textfeld-Fokus bleibt natives Copy/Paste.
|
||||
- [x] 2026-07-05 **Tauri Drag&Drop + Import-Dialog-Fix** (`c5b5ca6`, `45e19b7`) — `import`-Befehl als `autoRun` (Datei-Dialog öffnet synchron in der Geste, HMR-Falle via Config-Relaunch behoben); `dragDropEnabled:false` (Tauris natives Drag-Drop fing HTML-Drops ab). **Nutzer-bestätigt: Dialog + Text gehen.**
|
||||
- [x] 2026-07-05 **Layout: Befehlszeile in Mitte-Spalte** (`c794fee`) — nur Viewport-breit, Docks gewinnen Höhe. **Nutzer-bestätigt „sieht clean aus".**
|
||||
|
||||
- [x] 2026-07-05 **Textur-Spike render3d** (`RenderStyle::Textured` real) — zweiter Vertex-Pfad `[pos,normal,uv]` aus dem Mesh abgeleitet (Alt-Pfad bitgleich, per Test belegt), prozedurales 256×256-Schachbrett (kein Asset/`image`-Crate), Textur-Bind-Group group 1, `MESH_TEXTURED_WGSL` (gleiche Beleuchtung, Albedo aus Sampler), Pipeline bitidentisch zur Haupt-Pipeline, `spike3d` per `T` umschaltbar. Verifiziert: cargo test 58/59 (inkl. naga-Test) grün, alle 4 Builds sauber. **Visuelle Fenster-Abnahme durch Nutzer bestanden 2026-07-05** (Schachbrett korrekt, `T`-Umschaltung ok). Erster Durchstich der Textur-/PBR-Pipeline; ehrliche Lückenliste im Bericht — `8556037`
|
||||
- [x] 2026-07-04 **Grundriss: Decke drückt nicht mehr durch die Wände** — Decken-Umriss an Wand-Footprints (OBB) geclippt; Füllfläche strokelos, Umriss nur über unverdeckte Teilstücke (Überstände). Deckungsgleiche Decke ⇒ Umriss entfällt. vitest 230, tsc sauber — `a2f6923`
|
||||
- [x] 2026-07-04 **T-Stoss-Putznaht entfernt** — L-Seitenlinie nur noch bei materialFREMDEM Nah-Putz; materialgleicher Putz verschmilzt nahtlos (Nutzer-Direktive „Naht entfernen"). TS+Rust synchron, 3 Tests gezogen, cargo 8/8 — `a5ebfa7`
|
||||
- [x] 2026-07-04 **Snap-Farbe entschieden: Sora #5FA1C9** als endgültiger Default festgeschrieben (`DEFAULT_SNAP_COLOR`, Doc), „aki"-❓ geschlossen.
|
||||
- [x] 2026-07-04 **Mac-Build via Tauri + Queue-Abgleich** — Toolchain auf macOS-Gerät verifiziert, `cad.app`/`cad_0.1.0_aarch64.dmg` gebaut & gestartet. Verifikations-Baseline grün (tsc/vitest 230/cargo 56). Queue-Reconciliation: Toolchain-Blocker, Snap-Slice (`339202b`), Ebene-Schraffur (`dcb6ed5`), Bauteile-Tab-Master-Detail, Einstellungs-Verdrahtung UND Wand-Schicht-Bänder 3D (Option B, `toWalls3d.ts`) waren allesamt **bereits gelandet**, aber in PENDENZEN noch als offen gelistet → abgehakt. (kein Code-Commit)
|
||||
- [x] 2026-07-04 **Öffnungen als echte Boolean-Löcher** (+ Deckentrim-nur-3D) — Fenster/Türen = 1 Wandkörper mit rechteckigen `holes` (Rechteck-Gitter-Zerlegung + Laibungsquads), Segment-Boxen weg; Deckentrim nur noch im 3D-Pfad, Schnitt volle Höhe. Pflicht-Testfall versetzt-überlappende Fenster grün. cargo test 56, vitest 230, build:engine3d + tsc sauber — `1407c68`
|
||||
- [x] 2026-07-04 **Joins Phase 1c** Durchgangswand am T-Stoss echt aufbrechen (spanCutouts) — `c5a344d`
|
||||
- [x] 2026-07-04 **Locked-Iso-Fix** freie Kamera bleibt nach Ortho-Preset orthografisch — `cb8fae5`
|
||||
- [x] 2026-07-04 **3D-Live-Schnittebene** mit schraffierten Schnittflächen — `03f0c40`
|
||||
- [x] 2026-07-04 **Joins Phase 2** Merge-Regel im Schnitt (gleiche Komponente verschmilzt) — `82d354f`
|
||||
@@ -0,0 +1,124 @@
|
||||
# PORT_PLAN — kernel2d nach Rust/WASM (hinter identischem TS-Interface)
|
||||
|
||||
Stand: 2026-07-04. **Dieser Plan ist der geforderte erste Schritt — noch kein Code.**
|
||||
Ziel: `src/geometry/kernel2d.ts` (+ reine Geometrie aus room/ceiling/opening/stair) in
|
||||
ein Rust-Crate portieren, zu WASM bauen, hinter einer TS-Fassade mit *exakt gleichen*
|
||||
Signaturen einhängen. TS-Kernel bleibt als Referenz (`kernel2d.legacy.ts`).
|
||||
Differential-Test: Rust-WASM == TS-Legacy auf identischen Eingaben (epsilon je Funktion).
|
||||
|
||||
---
|
||||
|
||||
## 1. Scope-Inventar (was wirklich portiert wird)
|
||||
|
||||
**Externe JS-Geometrie-Libs: KEINE.** Der Kernel ist handgeschriebene f64-Mathematik,
|
||||
einzige Abhängigkeit sind die Vektor-Helfer aus `src/model/geometry.ts`
|
||||
(`add/sub/scale/len/normalize/leftNormal/cross/dot/lineIntersect`) — trivial mit zu
|
||||
portieren. `polygon-clipping`/`delaunator` werden im Kernel **nicht** genutzt.
|
||||
|
||||
### Voll im Scope (reine Geometrie)
|
||||
- **`kernel2d.ts`** — komplett: Primitive, Schnitt, Offset, Trim/Split/Join, Kreis, Fläche/Orientierung, Fillet.
|
||||
- **`roomBoundary.ts`** — komplett: `detectRooms`, `roomFromPointInside(Faces)`, `pointInPolygon`, `WallSegment`/`WallFace`/`DetectRoomsOptions` (generische Geometrie-Typen, `thickness: number`).
|
||||
- **`ceiling.ts`** — komplett: `normalizeOutline`, `isValidOutline`, `ceilingArea`, `outlineBBox`, `outlineCentroid`, `pointInOutline` (generische Polygon-Utilities; Name irreführend, keine Decken-Semantik).
|
||||
|
||||
### Teilweise im Scope
|
||||
- **`roomArea.ts`** — NUR `signedArea`, `polygonArea`, `perimeter`, `centroid`. Alles ab `SiaCategory` (`siaLabel`, `evaluateRoom`, `balance`, `roomsToCsv`) ist SIA-416-Domänenlogik/CSV → **bleibt TS**.
|
||||
- **`stair.ts`** — portierbar, aber `stairGeometry` nimmt `Stair`. „Scheinkopplung": es werden nur geometrische Felder gelesen (`shape/start/dir/runLength/width/stepCount/…`), `totalRise` kommt bereits aufgelöst vom Aufrufer. Port via Rust-Struct `StairParams` (strukturgleich, null Model-Semantik).
|
||||
|
||||
### ⚠️ Scope-Spannung: `opening.ts` (im Auftrag genannt, aber stark model-gekoppelt)
|
||||
7 von 9 Exporten binden `Wall`/`Opening` direkt ein; `getWallType`/`wallTypeThickness`/`wallReferenceOffset`/`wallVerticalExtent` lösen gegen `Project` auf.
|
||||
- **Portierbar mit abgeflachter Signatur** (Vec2/number statt Wall/Opening): `wallAxisLength`, `openingInterval`, `wallAxisFrame`, `openingJambs`, `openingCenter`, `doorSymbol`, `openingGapQuad`/`windowSymbol` (letztere brauchen `thickness`/`refOff` als `number`-Parameter).
|
||||
- **Bleibt TS** (braucht `Project`/Geschoss-Auflösung): `openingVerticalExtent` (via `wallVerticalExtent` → `Project.drawingLevels`).
|
||||
- **→ Entscheidung nötig** (siehe §7): Der Auftrag verbietet Änderungen an Aufrufstellen außer dem Import-Pfad. Ein Port von `opening` würde die *Signaturen* ändern (Wall→Vec2/number) und damit die Aufrufstellen brechen — das widerspricht „keine Änderung an Aufrufstellen". Empfehlung: **opening in Phase 1 ausklammern**, nur den echt reinen Kern (kernel2d/roomBoundary/ceiling/roomArea-Flächen/stair) portieren.
|
||||
|
||||
---
|
||||
|
||||
## 2. Crate-vs-Portieren — pro Operation
|
||||
|
||||
Akzeptanzkriterium ist **Differential-Parität gegen die naive TS-Routine**, nicht „geometrisch besser". Jedes Fremd-Crate mit anderem Algorithmus bricht die Parität per Konstruktion → Default = **PORTIEREN** (TS-Routinen sind 5–40 Zeilen f64, 1:1 übersetzbar).
|
||||
|
||||
| Operation | geprüftes Crate | Entscheidung | Grund |
|
||||
|---|---|---|---|
|
||||
| `offsetPolyline`/`offsetSegment` | cavalier_contours 0.6 | **PORTIEREN** | Crate liefert **Arcs (bulge)** statt `Vec2[]`, **heilt Selbstschnitte**, gibt mehrere Polylinien — TS heilt bewusst NICHT. Semantik nicht angleichbar. |
|
||||
| Segment/Line-Schnitt | geo / robust | **PORTIEREN** | Cramer-Formel; jede andere denom-/Epsilon-Politik driftet in Parallel-Grenzfällen. |
|
||||
| Trim/Split/`segmentPolylineHits` | geo `Relate` | **PORTIEREN** | Projektspezifisch (t-Dedup `1e-6`, Pick-nächster-Bogen, Wrap-Logik). |
|
||||
| Kreis-Schnitte | — | **PORTIEREN** | Quadratik mit projekt-EPS-Disc-Klemmung. |
|
||||
| `signedArea`/`isCCW` | geo `Area` | **PORTIEREN** | Shoelace-Summierung in **identischer Vertex-Reihenfolge** (f64 nicht assoziativ). |
|
||||
| `filletCorner` | — | **PORTIEREN** | `acos/atan2/tan(θ/2)`-Kette + projekt-Cutoffs; liefert projekt-spezifische `Fillet`-Struktur. |
|
||||
| `detectRooms` / `joinChains` | geo / i_overlay | **PORTIEREN** | Topologie-/reihenfolgeabhängig; andere Kantendurchlauf-Reihenfolge → andere (gleich gültige) Ringe → Parität bricht. |
|
||||
| point-in-polygon | robust | **PORTIEREN** (robust nur intern, optional) | Siehe §3. |
|
||||
|
||||
Fremd-Crates (cavalier_contours/geo/i_overlay) wären für einen späteren *Tier-2-Rewrite* (echter Arc-Offset, Boolean-Ops) wertvoll — das ist ein **anderes Produkt**, nicht dieser paritätserhaltende Port.
|
||||
|
||||
---
|
||||
|
||||
## 3. `robust`-Prädikate vs. Differential-Parität (Spannung auflösen)
|
||||
|
||||
TS testet Orientierung überall via naives `cross()` gegen `EPS`. `robust::orient2d` liefert das **exakte** Vorzeichen — weicht von naiv **nur in der nahe-degenerierten Zone** ab (fast-parallele Segmente, Null-Fläche-Polygone, Punkt-auf-Kante). Genau dort schlägt der Diff-Test am ehesten an. Man kann nicht gleichzeitig „bit-Parität gegen naiv" und „robuste Prädikate" im *selben* Vergleich haben.
|
||||
|
||||
**Entscheidung:**
|
||||
1. **v1 portiert die naiven `cross`-Vergleiche 1:1** (KEIN `robust`) → Zufalls-Diff-Test wird bit-nah grün. Robustheit kommt aus denselben f64-Formeln + demselben `EPS` wie TS.
|
||||
2. **Additiv, getrennt:** `robust` nur *intern* in `detectRooms`/point-in-polygon hinter optionalem Feature `robust-predicates`, flankiert von **Golden-Cases, die die KORREKTE (robuste) Antwort asserten** (nicht TS-Parität). Diese Fälle sind aus dem Zufalls-Diff-Test ausgenommen.
|
||||
3. Zwei Testklassen, nie gemischt: **Zufalls-Parität = naiv**, **Golden-Korrektheit = robust**.
|
||||
|
||||
---
|
||||
|
||||
## 4. Crate-Setup & Vite (exakter Klon von `src-tauri/geometry`)
|
||||
|
||||
Ort: **`src-tauri/kernel2d`** (analog render2d/render3d/geometry; das gesamte Tooling ist auf `src-tauri/<crate>` + `../../src/engine/pkg<X>` verdrahtet). *Namens-Hinweis:* Auftrag sagt `crates/kernel2d` — siehe §7.
|
||||
|
||||
- `Cargo.toml`: `crate-type = ["cdylib","rlib"]`; Features `default=[]`, `web=[wasm-bindgen, serde_json, console_error_panic_hook]`, `robust-predicates=[robust]` (additiv, nicht in web-Default).
|
||||
- `src-tauri/Cargo.toml`: `exclude = [..., "kernel2d"]` erweitern (sonst „multiple workspace roots").
|
||||
- `src/lib.rs`: reiner f64-Rechenkern feature-frei (`cargo test`-bar, headless) + `#[cfg(feature="web")]` JSON-**Batch**-Fassade pro Operation (`offset_polylines_json`, `intersect_batch_json`, `fillet_batch_json`, `circle_intersect_batch_json`, `detect_rooms_json`), Muster `compute_joins_json`.
|
||||
- `examples/parity.rs`: Klon von `geometry/examples/parity.rs` (Batch-JSON stdin→stdout) — nativer Diff-Kanal.
|
||||
- `package.json`: `"build:kernel2d": "wasm-pack build src-tauri/kernel2d --release --target web --out-dir ../../src/engine/pkgKernel2d --out-name kernel2d --no-default-features --features web"`.
|
||||
- **Vite: kein Config-Eintrag nötig** — `--target web`-Pakete werden als normales ES-Modul importiert, Vite bündelt `kernel2d_bg.wasm` automatisch (`new URL(..., import.meta.url)`). `pkgKernel2d/` ist git-ignoriert (self-`.gitignore = *`) → vor `vitest`/`build` muss `build:kernel2d` laufen (CI-Schritt).
|
||||
- **TS-Fassade** `src/geometry/kernel2d.ts` wird dünner Wrapper (init-WASM, JSON-Marshalling, gleiche Exports); Alt-Impl → `src/geometry/kernel2d.legacy.ts` (nicht löschen, ist die Diff-Referenz).
|
||||
|
||||
---
|
||||
|
||||
## 5. Differential-Test-Harness
|
||||
|
||||
- **Grobkörnige WASM-Grenze:** eine Batch-Funktion je Operation (N Polylinien rein, N Ergebnisse raus). Keine Per-Punkt-Calls (jeder Call marshallt einen String = O(n)-Kopie).
|
||||
- **Diff-Kanal:** Der Auftrag verlangt **Rust-WASM** vs. TS-Legacy → primär WASM (via `initSync`). Zusätzlich `cargo run --example parity` (nativ) als schneller Sekundär-Kanal (bit-identisch zu WASM für `+ - * / sqrt`; **Ausnahme** `atan2/acos/tan` in Fillet: libm nativ ≠ wasm um letzte ULP → Winkel-Epsilon).
|
||||
- **vitest-Init synchron:** `initSync({ module: readFileSync(pkgKernel2d/kernel2d_bg.wasm) })` (kein `fetch`), einmal in `beforeAll`.
|
||||
- **Zufallsgeneratoren:** seed-basiert (Seed im Testnamen), Polylinien 3–20 Vertices, Koordinaten `[-100,100] m`, `closed`/`d` zufällig; zusätzlich Cluster nahe `0` und `1e-6..1e-3`, um Toleranzschwellen zu treffen.
|
||||
- **Vergleichsreihenfolge:** zuerst **Struktur exakt** (Array-Längen, closed-Flags, Punktzahl, null/nicht-null), dann Werte mit op-Epsilon. Struktur ist der schärfste Paritäts-Wächter.
|
||||
|
||||
### Epsilon pro Funktion
|
||||
| Größe | Toleranz | Grund |
|
||||
|---|---|---|
|
||||
| Punktkoordinaten (Offset/Trim/Split/Kreis) | `abs 1e-9` | Bestehender Paritätstest nutzt `1e-9` und besteht bit-nah. |
|
||||
| Fläche (`signedArea`) | `rel 1e-9·max(1,\|A\|)` | Shoelace ∝ coord² → absolute ULP wächst mit Flächengröße; relativ skaliert korrekt. |
|
||||
| Winkel (`filletCorner`) | `abs 1e-7 rad` | `atan2/acos` libm-abhängig (nativ↔wasm ULP-Drift); 1e-7 rad ≈ 5.7e-6°, weit unter Zeichenrelevanz. |
|
||||
| Parameter t/s | `abs 1e-9` | TS dedupliziert erst ab `1e-6` → kleinere Diffs ändern nie die Struktur. |
|
||||
| Struktur | **exakt** | Kein Epsilon. Hier bricht ein Fremd-Crate. |
|
||||
|
||||
### Golden-Cases (explizit, aus Zufallstest teils ausgenommen)
|
||||
kollineare Tripel · Null-Länge-Segmente (Dublett-Vertex) · Offset-Selbstschnitt (enges U, großes d — hier bräche cavalier_contours) · spitze Fillet-Winkel (θ→0) · fast-paralleler Schnitt (denom knapp <>EPS → **Korrektheits-Golden/robust**) · konzentrische/tangentiale Kreise · Punkt exakt auf Polygonkante (**Korrektheits-Golden**).
|
||||
|
||||
---
|
||||
|
||||
## 6. Kritische Paritäts-Details (MÜSSEN exakt repliziert werden)
|
||||
- `len` = `Math.hypot` → **`f64::hypot`** (nicht `(x²+y²).sqrt()`).
|
||||
- `normalize` Null-Guard: `len || 1` → `if l==0.0 {1.0} else {l}` (Ergebnis `{0,0}`, kein NaN).
|
||||
- **Zwei Epsilons:** `EPS=1e-7` (kernel2d) UND hartkodiert **`1e-9`** in `lineIntersect` (Offset-Miter-Fallback hängt daran — geometrischer Sprung, nicht epsilon).
|
||||
- Dedup-Schwelle `1e-6`, Fillet-Kollinearität `1e-4` (nicht EPS).
|
||||
- **Stabile Sortierung** (JS `Array.sort` ist stabil): Rust `sort_by`, nicht `sort_unstable_by`.
|
||||
- Term-Reihenfolge in `cross`, `signedArea`-Summierung, Kreis-Diskriminante `B*B−4*A*C` exakt beibehalten (f64 nicht assoziativ; kein Kahan/Reorder).
|
||||
- Modulo-Indizierung `(i+len-1)%len` (usize-Unterlauf vermeiden).
|
||||
- `joinChains`: greedy `i<j`-erster-Treffer-dann-Neustart exakt nachbilden (reihenfolgeabhängiges Ergebnis).
|
||||
|
||||
---
|
||||
|
||||
## 7. Offene Entscheidungen (vor Coding klären)
|
||||
1. **`opening` im Scope?** Portieren würde Signaturen (Wall→Vec2/number) und damit Aufrufstellen ändern — widerspricht „keine Änderung an Aufrufstellen außer Import-Pfad". **Empfehlung: opening in Phase 1 ausklammern.**
|
||||
2. **Crate-Ort:** Auftrag `crates/kernel2d` vs. Repo-Konvention `src-tauri/kernel2d` (analog render2d/render3d). **Empfehlung: `src-tauri/kernel2d`** (Tooling passt out-of-the-box).
|
||||
3. **Diff-Kanal:** Auftrag verlangt Rust-**WASM**; nativer `parity`-Kanal ist schneller (kein wasm-pack in CI) und bit-identisch außer Fillet-Transzendente. **Empfehlung: WASM primär (Auftragskonform) + nativ sekundär.**
|
||||
|
||||
## 8. Phasen (Reihenfolge)
|
||||
1. Crate-Skelett `src-tauri/kernel2d` + Build-Script + Workspace-exclude + leere WASM-Fassade → `build:kernel2d` grün.
|
||||
2. Primitive + Schnitt + Fläche + Kreis portieren (trivial–mittel) + Batch-Fassade + Diff-Test-Harness (Zufall+Golden) → grün.
|
||||
3. Offset (Miter+1e-9-Fallback) + Fillet portieren → Golden für Selbstschnitt/spitze Winkel grün.
|
||||
4. Trim/Split/Join (`trimPolyline`, `splitAtIntersections`, `joinChains` — der Löwenanteil) → Struktur-Golden grün.
|
||||
5. `roomBoundary` (`detectRooms`) + `ceiling` + `roomArea`-Flächen + `stair` (StairParams).
|
||||
6. TS-Fassade umstellen (Alt → `.legacy.ts`), Import-Pfade der Aufrufstellen unverändert lassen, bestehende Suite grün, `npm run build` + WASM-Build sauber.
|
||||
@@ -1,103 +1,146 @@
|
||||
# DOSSIER Standalone
|
||||
# Dossier
|
||||
|
||||
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.
|
||||
Open-Source-CAAD (Computer Aided Architecture Design). Modelliert ein Gebäude
|
||||
aus semantischen Bauteilen und zieht daraus saubere, normgerechte 2D-Pläne —
|
||||
ohne Revit. **Desktop-App** — auf macOS über **Tauri**, auf Linux über eine
|
||||
**Electron**-Shell (weil Tauris Linux-Webview WebKitGTK kein zuverlässiges
|
||||
WebGPU bietet, das die Rendering-Engines brauchen); dieselbe Codebasis läuft
|
||||
auch im Browser (eingeschränkt, ohne native Fenster/Dateisystem-Integration).
|
||||
|
||||
Das ist die eigenständige Browser-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.
|
||||
Das ist die eigenständige Neuimplementierung des Rhino-Plugins
|
||||
[DOSSIER](https://git.openbureau.ch/karim/dossier): dieselbe Denkweise
|
||||
(Geschosse, Kategorie-Ebenen, mehrschichtige Bauteile, Prioritäts-
|
||||
Verschneidung), aber mit eigenem Datenmodell + eigener Rendering-Engine
|
||||
(Rust/WASM/WebGPU, intern „Nordstern" genannt) statt Rhino-Aufsatz.
|
||||
|
||||
## 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. Schnitte und Ansichten laufen
|
||||
über eine eigene, analytische Rust-Pipeline (kein Hidden-Line-Removal nötig,
|
||||
siehe [ARCHITECTURE.md](ARCHITECTURE.md) §4.3) und sind seit Juli live im
|
||||
3D-Viewport verdrahtet, nicht nur ein Spike.
|
||||
|
||||
## 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.
|
||||
Aus dem ursprünglichen Risiko-Spike ist binnen weniger Wochen ein
|
||||
**funktionsreiches Desktop-BIM-Tool** geworden: eigenes semantisches
|
||||
Gebäudemodell, zwei eigene Rust/WASM-Rendering-Engines, IFC/DXF/PDF/STL/OBJ-
|
||||
Export, Swisstopo-Import, SIA-416-Flächen, Materialbibliothek, Layouts/
|
||||
Plansätze, native Tauri-Fenster. Eine vollständige, ehrliche Bestandsaufnahme
|
||||
(inklusive offener Punkte und bekannter technischer Schulden) steht in
|
||||
**[STATUS.md](STATUS.md)**.
|
||||
|
||||
**Funktioniert:**
|
||||
- Semantisches Modell mit **mehrschichtigen Wänden** und automatischer
|
||||
**L-Ecken-Gehrung** (`computeJoins`); dieselbe Logik speist Plan *und* 3D.
|
||||
- **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.
|
||||
- **Rhino-artiges Befehlssystem** mit getippten Koordinaten (`5,3` · `r5,3` ·
|
||||
`5<45`) und Tab-Feld-Zyklus (Länge → Winkel …).
|
||||
- **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.
|
||||
**Funktioniert (Auszug — volle Liste in STATUS.md §3):**
|
||||
- Semantisches Modell mit **mehrschichtigen Wänden**, L-Eck-Gehrung **und**
|
||||
Prioritäts-T-/X-Stössen (`joinPriority` am Component) — konsistent in
|
||||
Grundriss, 3D-Viewport und 3D-Live-Schnitt.
|
||||
- **Parametrische Wände**, Decken, Treppen (gerade/L/Wendel), Dächer
|
||||
(Flach/Pult/Sattel/Walm/Mansarde/Zelt), Stützen, SIA-416-Räume mit
|
||||
automatischer Bilanz + CSV-Export.
|
||||
- **Türen & Fenster** gehostet in Wänden, mit Rahmen/Zarge/Kämpfer/Oberlicht,
|
||||
Detailgrad grob/mittel/fein, echte Rechteck-Löcher im 3D-Wandkörper.
|
||||
- Rhino-artiges **Kommandosystem** mit getippten Koordinaten (`5,3` · `r5,3` ·
|
||||
`5<45`) und Tab-Feld-Zyklus; Snapping, Grips, Trim/Split/Join, 2D-Booleans.
|
||||
- **Live 2D↔3D-Schnitt**: eine Schnittebene im 3D-Viewport folgt derselben
|
||||
Prioritäts-Logik wie der 2D-Plan-Schnitt (Rust-Port, keine Diskrepanz).
|
||||
- **Vektor-Export:** PDF (Einzel- **und Mehrseiten-Layouts**), DXF, **IFC4**
|
||||
(mit echten Fenster-/Tür-Löchern), STL/OBJ, CSV-Bauteilliste.
|
||||
- **Materialbibliothek**: 13 gebündelte PBR-Starter **plus** Live-Suche der
|
||||
kompletten ambientCG-Bibliothek (1K/2K/4K, On-Demand-Download).
|
||||
- **Layouts/Plansätze** mit mehreren Viewports pro Blatt, Ausschnitte
|
||||
(View-Snapshots), Kamera-Presets, Norden-Rotation.
|
||||
- **Import:** DXF/DWG, Swisstopo (swissBUILDINGS3D/-ALTI3D/SWISSIMAGE,
|
||||
radiusgenau zugeschnitten), OSM-Kontextimport, `.lin`/`.pat`.
|
||||
- **Native Desktop-Integration** (Tauri): eigene randlose Fenster, native
|
||||
Speichern/Öffnen-Dialoge, eigenes `.obp`-Dateiformat mit OS-Level-Lock gegen
|
||||
Doppelöffnen, mehrere native Zusatzfenster (Resources, Settings, …).
|
||||
- **Resource Manager** (Component/Hatch/Line/Typ-Editoren), regelbasierte
|
||||
Overrides, Panel-System (dockbar/floatend), i18n (de/en).
|
||||
|
||||
**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.
|
||||
- **Prioritäts-T-/X-Stöße** mehrschichtiger Wände (Beton läuft durch, Putz
|
||||
verbindet seitlich) — das berüchtigte Risiko #1.
|
||||
|
||||
Details und die Begründungen stehen im
|
||||
[HANDOVER](HANDOVER.md) (laufendes Arbeitsprotokoll) und in der
|
||||
[ROADMAP](ROADMAP.md) (Vision, Phasen, Backlog).
|
||||
**Bewusst noch offen** (Details + volle Liste: STATUS.md §4.7):
|
||||
- **Echtes Mesh-Boolean für Öffnungen** — funktioniert heute über achsparallele
|
||||
Rechteck-Löcher; ein generisches CSG-Boolean existiert bereits
|
||||
(`trucksolid`/`csgrs`), ist aber nicht an die Wand-Pipeline angeschlossen.
|
||||
- **3D-Griffsystem**: Feld-Controller (Tab-Zyklus) und Snapping fehlen für
|
||||
3D-Grip-Drag (im 2D vollständig vorhanden).
|
||||
- **DWG/DXF-Domänenimport** (Entitäten → echte Wände/Öffnungen) — Lesen
|
||||
funktioniert, das Mapping auf das Modell ist unbegonnen; DWG-Schreiben fehlt.
|
||||
- **`make2D`**-Kommando (3D-Ansicht → flacher 2D-Plan mit Füllungen).
|
||||
- Bekannte, noch nicht bereinigte Doppelspur `Door[]`/`Opening[]` im Modell.
|
||||
|
||||
## Stack
|
||||
|
||||
Bewusst leichtgewichtig — Three.js ist reiner Display-Layer, kein schwerer
|
||||
Geometrie-Kernel verfrüht eingezogen.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Shell | **Tauri** auf macOS (WKWebView+WebGPU) · **Electron/Chromium** auf Linux (WebKitGTK kann kein WebGPU) — beide randlos, eigene Titelleiste, gemeinsame React-App |
|
||||
| Frontend | React + TypeScript + Vite |
|
||||
| 3D | Three.js |
|
||||
| 2D-Plan | eigener SVG-Renderer |
|
||||
| Geometrie | eigener 2D-Kernel (`src/geometry/kernel2d.ts`), `delaunator` fürs Terrain |
|
||||
| Import | `dxf-parser`, eigene `.lin`/`.pat`-Parser |
|
||||
| 3D | eigener Rust/WASM/WebGPU-Renderer „Nordstern" (`render3d`, Default), Three.js als leichtgewichtiger Zweitpfad |
|
||||
| 2D-Plan | eigener Rust/WASM/WebGPU-Renderer (`render2d`), plus ein TS/WebGL2-Renderer (`plan/glPlan`), plus SVG (`PlanView.tsx`, immer als Interaktions-/Fallback-Ebene aktiv) |
|
||||
| Geometrie | eigener 2D-Kernel (`src/geometry/kernel2d.ts`); ein Rust-Port (`src-tauri/kernel2d`) existiert nur als Paritätstest, läuft nicht produktiv |
|
||||
| CSG/Extrusion | `trucksolid`-Crate (`truck` + `csgrs`), fürs Extrude-Kommando |
|
||||
| Export | eigener IFC4-Writer, `jspdf`/`svg2pdf.js` (Vektor-PDF), eigener DXF-Writer, STL/OBJ |
|
||||
| Import | `dxf-parser`, `@mlightcad/libredwg-web` (DWG), eigene `.lin`/`.pat`-Parser |
|
||||
| Materialien | `jszip` (ambientCG-Zip-Entpacken), `three.js` `TextureLoader` |
|
||||
|
||||
Geplant, aber noch nicht eingezogen: `rhino3dm` (NURBS / `.3dm`), web-ifc (IFC),
|
||||
OpenCascade/Manifold (exakte Booleans + HLR). Siehe ROADMAP §4.
|
||||
`opencascade.js` steht noch als Dependency in `package.json`, wird aber nur
|
||||
noch von totem Code (`src/section/hlr.ts`, superseded durch die Rust-
|
||||
Schnitt-Pipeline) importiert. Details zu allen Rust-Crates (inkl. zwei
|
||||
aktuell unbenutzten WASM-Builds) in [ARCHITECTURE.md](ARCHITECTURE.md) §1
|
||||
und [STATUS.md](STATUS.md) §4.1.
|
||||
|
||||
## Entwicklung
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # Vite, http://localhost:5173
|
||||
npm run dev # Vite, http://localhost:5187
|
||||
npm run tauri:dev # Tauri-Fenster (macOS — nativer Zielrahmen dort)
|
||||
npm run electron # Electron-Fenster (Linux — WebGPU über Chromium statt WebKitGTK)
|
||||
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).
|
||||
WASM-Engines nach Rust-Änderungen neu bauen (nur `.rs` committen,
|
||||
`src/engine/pkg*/` ist gitignored):
|
||||
|
||||
```bash
|
||||
npm run build:engine # render2d
|
||||
npm run build:engine3d # render3d
|
||||
npm run build:truck # trucksolid
|
||||
```
|
||||
|
||||
render3d/Wasm3DViewport-Änderungen sind nicht per Puppeteer/Browser
|
||||
verifizierbar — im laufenden `tauri:dev`-Fenster selbst testen.
|
||||
|
||||
## Aufbau
|
||||
|
||||
```
|
||||
src/
|
||||
model/ semantisches Modell + Ableitungen (types, geometry, joins, terrain)
|
||||
geometry/ 2D-Kernel (offset/trim/fillet/intersect)
|
||||
model/ semantisches Modell (types, joins, parametricWalls, roomStamp, terrain)
|
||||
geometry/ 2D-Kernel (offset/trim/fillet/intersect, ceiling/opening/roomArea/stair/roof/column)
|
||||
commands/ Rhino-artiges Befehlssystem (engine, parser, registry, cmds/)
|
||||
tools/ interaktive Zeichenwerkzeuge + Snapping + Transformationen
|
||||
plan/ Grundriss-Ableitung (generatePlan) + PlanView (SVG)
|
||||
viewport/ Viewport3D (Three.js)
|
||||
plan/ Grundriss-Ableitung (generatePlan) + PlanView (SVG) + glPlan (WebGL2) + Rust-Bindings
|
||||
viewport/ Viewport3D (Three.js) + Wasm3DViewport (Rust/wgpu „Nordstern", Default)
|
||||
section/ TOTER Code (OCCT-HLR-Spike) — Schnitt läuft über render3d, siehe ARCHITECTURE.md §4.3
|
||||
export/ IFC4-, PDF-, DXF-, STL/OBJ-, CSV-Export aus derselben Plan-/Modell-Struktur
|
||||
materials/ ambientCG-Live-Suche + gebündelte Starter-Bibliothek + PBR-Runtime
|
||||
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)
|
||||
state/ eigener Store (useSyncExternalStore) + Slices (project/selection/view/layout/…)
|
||||
native/ Tauri-only: native Fenster, macOS-Menüleiste, Fenster-Chrome
|
||||
ui/ App.tsx-Shell, Top-Bar, Resource Manager, Kontextmenü, Kommandozeile, Ribbon
|
||||
io/ Import/Export (DXF, DWG, .lin, .pat), Swisstopo/LV95, OSM-Kontext
|
||||
i18n/ Wörterbücher de/en
|
||||
src-tauri/ 6 Rust-Crates (render2d/render3d/geometry/kernel2d/trucksolid/dwgimport,
|
||||
je headless UND per wasm-pack baubar) + der Tauri-Host selbst
|
||||
```
|
||||
|
||||
## Konventionen
|
||||
@@ -106,17 +149,30 @@ src/
|
||||
(Design Layer, Component, Hatch, Wall Style). **UI-Texte sind deutsch**, immer
|
||||
über `t('key')` — keine hartcodierten Strings im JSX.
|
||||
- Intern alles in **Metern**; Anzeige via `formatM`.
|
||||
- `App.tsx` bleibt dünner Shell, Zustand lebt im Store. Verbindliches in
|
||||
[CONVENTIONS.md](CONVENTIONS.md).
|
||||
- Verbindliches in [CONVENTIONS.md](CONVENTIONS.md).
|
||||
|
||||
## Weiterlesen
|
||||
|
||||
- [ROADMAP.md](ROADMAP.md) — Produktvision, Architektur-Entscheidungen, Phasen 0–7, DOSSIER-Backlog
|
||||
- [ARCHITECTURE.md](ARCHITECTURE.md) — Technische Architektur im Detail
|
||||
- [HANDOVER.md](HANDOVER.md) — aktueller Arbeitsstand, Befunde, nächste Schritte
|
||||
- [docs/](docs/) — Design-Specs (Befehlssystem, Zeichenwerkzeuge, Wand-Joins, Backend …)
|
||||
- [STATUS.md](STATUS.md) — **vollständige, ehrliche Bestandsaufnahme**: Zahlen,
|
||||
Feature-Inventar, Mist-Liste, Doku-Widersprüche, Tag-1-Vision vs. heute
|
||||
- [ARCHITECTURE.md](ARCHITECTURE.md) — technische Architektur im Detail (Ist-Zustand)
|
||||
- [ROADMAP.md](ROADMAP.md) — ursprüngliche Produktvision (Tag-1-Stand, historisch)
|
||||
- [HANDOVER.md](HANDOVER.md) / [PENDENZEN.md](PENDENZEN.md) — laufendes
|
||||
Arbeitsprotokoll bzw. Backlog (Single Source of Truth für offene Punkte)
|
||||
- [docs/](docs/) — Design-Specs (teils ebenfalls Tag-1-Vision, siehe Hinweis
|
||||
in `docs/README.md`)
|
||||
|
||||
## Lizenz
|
||||
|
||||
Copyright © 2026 Karim Gabriele Varano. Veröffentlicht unter der
|
||||
**GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)** —
|
||||
siehe [LICENSE](LICENSE). Dossier ist Teil der **openbureau**-Suite.
|
||||
|
||||
Die AGPL verlangt, dass auch bei Betrieb als Netzwerk-/Webdienst der (ggf.
|
||||
geänderte) Quellcode für die Nutzer verfügbar gemacht wird. Drittkomponenten
|
||||
behalten ihre jeweiligen Lizenzen (siehe „Über"-Dialog in der App).
|
||||
|
||||
---
|
||||
|
||||
Privates Projekt. Die UI ist deutsch; Schweizer Spezifika (SIA-416-Flächen,
|
||||
Swisstopo-Geodaten) sind als Differenzierer eingeplant.
|
||||
Die UI ist deutsch; Schweizer Spezifika (SIA-416-Flächen, Swisstopo-Geodaten)
|
||||
sind ein bewusster Differenzierer.
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# RESEARCH — Bauteile Treppe/Fenster/Tür: Rhino-Plugin als Vorbild
|
||||
|
||||
Stand: 2026-07-05. Vergleich der authoritativen Rhino-Plugin-Implementierung
|
||||
(`/Users/karim/PROJECTS/DOSSIER/rhino/`, C#/Python + RhinoCommon-B-Rep) mit
|
||||
DOSSIER-STANDALONEs vereinfachter TS-Umsetzung. Ziel: Bauteile aufs Rhino-Niveau
|
||||
heben. Belege = Rhino-Funktionsnamen.
|
||||
|
||||
**Grundsatz:** Rhino hat echten B-Rep-Kernel (3D). DOSSIER hat extrudierte Meshes +
|
||||
`kernel2d` (2D). Darum getrennt: **2D-Symbol-Logik** (leicht in TS/kernel2d
|
||||
übernehmbar) vs. **3D** (braucht Mesh-Pipeline bzw. später truck).
|
||||
|
||||
## Treppe (Rhino `treppe.py`, 1784 Z. — massiv reicher als `stair.ts`)
|
||||
- **Typen:** gerade / L (3- und 4-Punkt mit Podest) / Wendel (inkl. Spindel-Cone bei r_in<0.05). TS hat alle drei, aber simpler.
|
||||
- **3D-Querschnitt-Modi** `massiv`/`flach`/`plattenrand` (`_treppe_profile_2d`) — TS: nur Stufenboxen. **fehlt.**
|
||||
- **2D-Plansymbol** (`_make_treppe_2d_symbol`): Tritte, Lauflinie (4 Pfeil-Styles: klassisch/filled/breit/voll, visuell zentriert via `mid_off`), **Aussenlinie/Outline** (alle 3 Typen), Bruchlinie, Podest-Hexagon. TS: Tritte+Bruch+Lauflinie(1 Style) da; **Aussenlinie fehlt komplett**, Referenz links/mitte/rechts fehlt.
|
||||
- **Soll-Schrittmass** (2S+A, Lock), Show-Flags, Grips — fehlen in TS.
|
||||
|
||||
## Fenster (Rhino `elements.py` `_make_oeffnung_pieces`)
|
||||
- **3D:** Rahmen (BooleanDiff), Mittelpfosten je Flügel (1–4), Glas (einfach 12mm / Doppel 2×6+16), Sims aussen (4 Styles), Rahmen-Offset innen/mitte/aussen. TS: nur 2D-Rahmen+Glaslinien, Wand-Boolean-Loch (`1407c68`); kein Rahmen-3D.
|
||||
- **2D:** TS hat Rahmen + 1–2 Glaslinien (grob/mittel/fein). **Fehlt:** Brüstungslinie im Plan, Flügel-Mittelpfosten, Anschlag-Striche (nur Tür hat sie), Flügelanzahl.
|
||||
|
||||
## Tür (Rhino `elements.py`)
|
||||
- **Typen:** `normal` / `wandoeffnung` (reiner Durchbruch, kein Blatt). **Rahmen:** `zarge` (3-seitig, im Wandquerschnitt) / `block` (+5cm Überhang).
|
||||
- **2D-Schwung** (`_make_tuer_swing_curves`): Blatt+Arc, hinge_side/open_angle/aussenseite/swing_invert; Bogen-Anlage am Rahmen (standard/detail). **2D-Sturzlinien** (`_make_tuer_sturz_curves`, gestrichelt, Modi keine/innen/aussen/beide — SIA: Sturz über Schnittebene → gestrichelt). TS: Blatt+Arc+Anschlag (fein) da; **Sturzlinien fehlen komplett**, `wandoeffnung`-Typ + swing_invert fehlen.
|
||||
|
||||
---
|
||||
|
||||
## Gesamtreihenfolge „was zuerst" (Agenten-Empfehlung)
|
||||
|
||||
**Gruppe A — 2D-leicht, hoher Plan-Wert, kernel2d-kompatibel:**
|
||||
1. **Treppe Aussenlinie/Outline** (alle 3 Typen; gerade=4 Linien, L via Linienschnitt `_line_intersect_xy`, Wendel=2 Bögen+radiale). Ohne sie wirkt der Plan unfertig.
|
||||
2. **Fenster Brüstungslinie** im Plan (1 gepunktete Linie bei sillHeight>0). XS.
|
||||
3. **Tür Sturzlinien** (gestrichelt, SIA) — Schulfall der geplanten Schnitt/Ansichts-Logik.
|
||||
4. **Treppe Referenz links/mitte/rechts** + Lauflinie visuell zentriert.
|
||||
5. **Fenster Flügel-Mittelpfosten** (2D, `wingCount` ins Modell).
|
||||
6. **Tür `wandoeffnung`-Typ** (reine Öffnung ohne Blatt/Symbol).
|
||||
7. Treppe Show-Flags + `obere_dashed`; 8. Fenster Anschlag-Striche.
|
||||
|
||||
**Gruppe B — 2D mittel:** Tür-Schwung am Rahmen; Treppe Pfeil-Style `filled`; Fenster/Tür Styles/Presets; swing_invert.
|
||||
|
||||
**Gruppe C — 3D (nach Mesh-/B-Rep-Pipeline):** Treppe `flach`-Querschnitt; Rahmen als Mesh (Fenster/Tür); Türblatt+Glas; Sims; L-Podest-Hexagon; Wendel helikoidale Unterseite (truck).
|
||||
|
||||
**Bezug PENDENZEN:** Gruppe-A-Items 1–3 (Aussenlinie/Brüstung/Sturz) fallen in das Backlog-Item „Schnitt- vs. Ansichts-Darstellung nach Schnitthöhe" — Sturzlinie = Bauteil unter Schnittebene → gestrichelte Überkopf-Projektion. Gruppe C wartet auf den Textur-/Mesh-Pipeline-Nachfolger bzw. truck.
|
||||
@@ -0,0 +1,162 @@
|
||||
# RESEARCH — Von BIM-Tool zu „echtem CAD": Ansätze aus fortgeschrittenen Programmen
|
||||
|
||||
Stand: 2026-07-05. Direkt am Quellcode studiert (Repos nach `/tmp/cad-study` geklont,
|
||||
liegen NICHT im Projekt). Ziel: konkrete, verfolgbare Ansätze, um DOSSIER von einem
|
||||
spezialisierten BIM/Architektur-Werkzeug zu einem allgemeineren CAD zu erweitern.
|
||||
|
||||
## Studierte Programme
|
||||
|
||||
| Repo | Was | Lizenz | Stack | Für uns |
|
||||
|---|---|---|---|---|
|
||||
| [OpenCADStudio](https://github.com/HakanSeven12/OpenCADStudio) | Eigenständige 2D/3D-CAD-App, ~200k LOC Rust | GPL-3.0 | Iced (GUI) + wgpu + WASM-Web-Build | **App-Architektur-Blaupause** (Entity-Modell, Command-System, Snap, Grips). Fast unser Stack. |
|
||||
| [truck](https://github.com/ricosjp/truck) | Reiner Rust-CAD-**Kernel** (NURBS/B-Rep/Mesh/Boolean/STEP) | Apache-2.0 | 16 Crates, wgpu-Rendering + `truck-js` (WASM) | **3D-Kernel-Blaupause**. OpenCADStudio nutzt ihn als 3D-Backend. |
|
||||
| [acadrust](https://github.com/HakanSeven12/acadrust) | Pure-Rust **DWG+DXF** R13–R2018 read+write, 41 Entity-Typen | **MPL-2.0** | nom/byteorder/flate2/nalgebra/encoding_rs (alles pure Rust) | **DWG/DXF-Rückgrat**, MPL-2.0 nutzbar, sehr wahrscheinlich WASM-baubar. |
|
||||
|
||||
**Lizenz-Klarstellung:** `acadrust` ist **MPL-2.0** (file-level copyleft — für uns nutzbar,
|
||||
keine GPL-Ansteckung), NICHT GPL. `truck` ist Apache-2.0 (frei nutzbar). Nur OpenCADStudio
|
||||
selbst ist GPL-3.0 — wir lesen es als **Architektur-Referenz**, kopieren keinen Code.
|
||||
|
||||
---
|
||||
|
||||
## Die zentrale Lektion: was ein „echtes CAD" architektonisch ausmacht
|
||||
|
||||
DOSSIER ist heute **domänenzentriert** (Wand/Öffnung/Component/Plan-Pipeline, TS-Modell).
|
||||
Ein echtes CAD (Rhino/AutoCAD-artig) hat stattdessen einen **generischen Kern**. Aus
|
||||
OpenCADStudios `src/` destilliert:
|
||||
|
||||
### 1. Generisches Entity-Modell mit Traits (statt Wand-Spezialfall)
|
||||
`src/entities/` = 41 Entity-Typen (line, arc, circle, lwpolyline, spline, ellipse, hatch,
|
||||
text, mtext, dimension, leader, insert/block, table, viewport, mesh, solid3d, …). Jeder
|
||||
Typ implementiert ein **Trait-Set** statt einer Sonderbehandlung:
|
||||
- `TruckConvertible` → 3D-Kernel-Geometrie
|
||||
- `FallbackTess` → Display-Geometrie (Punkte/Snap-Vertices)
|
||||
- `Grippable` → editierbare Griffe + Hover-Menü (Add Vertex / Convert to Arc / Reverse / Lengthen…)
|
||||
- `PropertyEditable` → Eigenschaften-Panel
|
||||
- `Transformable` → Move/Rotate/Scale/Mirror
|
||||
- `MassProps` → Fläche/Umfang für Abfragen
|
||||
|
||||
→ **DOSSIER-Lehre:** ein generisches `Entity`-Modell mit Trait-Dispatch neben das BIM-Modell
|
||||
stellen. Wir haben Ansätze (`Drawing2D`, splitJoin-Editoren, kernel2d), aber keine einheitliche
|
||||
Entity-Taxonomie mit Traits.
|
||||
|
||||
### 2. Modeless Command-System — der eine `StepInput`-Step-Machine
|
||||
`src/command.rs`: EIN Enum `StepInput { Point / Text / EntityPick / StructurePick /
|
||||
SelectionComplete }`. **Jede** Eingabequelle (Kommandozeile, Viewport-Klick, Pick, Selektion,
|
||||
Dynamic Input, Plugin-API, Headless-Script) übersetzt in `StepInput` und läuft durch **eine**
|
||||
`feed_command`-Funktion → treibt die aktive `CadCommand`-Step-Machine, egal woher der Schritt kam.
|
||||
|
||||
→ **DOSSIER-Lehre:** Das ist der größte UX-Unterschied. Echtes CAD = „Befehl tippen → Punkte
|
||||
picken → Optionen". DOSSIER ist heute Tool-Button + Direktmanipulation. Ein `StepInput`-Funnel +
|
||||
Kommandozeile wäre der Hebel, um beliebige Werkzeuge einheitlich, scriptbar und headless-testbar
|
||||
zu machen.
|
||||
|
||||
### 3. Object-Snap-Engine (eigenständig, groß)
|
||||
`src/snap.rs` = 70 KB nur für OSNAP (endpoint/midpoint/center/intersection/tangent/
|
||||
perpendicular/…). Wir haben Teil-Snapping; ein echtes CAD hat eine dedizierte, vollständige
|
||||
Snap-Schicht.
|
||||
|
||||
### 4. Universelle Grip-Editierung
|
||||
Jede Entity liefert Griffe + kontextuelle Griff-Menüs mit teils numerischem Follow-up
|
||||
(„Lengthen" fragt Wert an der Kommandozeile ab). Einheitlich über das `Grippable`-Trait.
|
||||
|
||||
### 5. `CadDocument` als Dokumentmodell = das DWG/DXF-Objektmodell
|
||||
OpenCADStudio nutzt `acadrust::CadDocument` DIREKT als sein Datenmodell (Entities + Objects +
|
||||
Tables: Layer/Linetype/Style/Blocks + XData). D. h. das native CAD-Austauschformat IST das
|
||||
interne Modell → verlustfreier Round-Trip „for free".
|
||||
|
||||
---
|
||||
|
||||
## truck — 3D-Kernel-Blaupause (+ harte WASM-Realität)
|
||||
|
||||
Saubere Schichtung (Ship-of-Theseus, kleine ersetzbare Crates):
|
||||
|
||||
```
|
||||
truck-base Basistraits/Toleranz (cgmath)
|
||||
truck-geotrait ParametricCurve / ParametricSurface Traits
|
||||
truck-geometry Knot-Vektor, B-Spline, NURBS ← Kurven/Flächen
|
||||
truck-topology vertex/edge/wire/face/shell/solid ← B-Rep-Topologie
|
||||
truck-modeling Geometrie + Topologie integriert ← Solids bauen
|
||||
truck-polymesh Polygon-Datenstruktur + Meshing
|
||||
truck-meshalgo Tessellation der Shapes
|
||||
truck-shapeops Boolean-Ops auf Solids (v0.4 — früh)
|
||||
truck-stepio STEP read/write (v0.3)
|
||||
truck-platform wgpu-Grafik-Utility ┐ Rendering — brauchen wir NICHT
|
||||
truck-rendimpl Shape/Mesh-Visualisierung ┘ (wir haben eigenen wgpu-Renderer „Nordstern")
|
||||
truck-js WASM-Wrapper (v0.2)
|
||||
```
|
||||
|
||||
**WASM-Realität (zwei Befunde abgeglichen):**
|
||||
- OpenCADStudio pinnt `truck-meshalgo` 0.4 und schaltet den `solid3d`-Feature **im WASM-Build ab**,
|
||||
weil dessen ACIS-/vtkio-Pfad über `xz2 → lzma-sys` (C-Bibliothek) nicht nach wasm32 kommt →
|
||||
**im Web ist OpenCADStudio nur 2D.**
|
||||
- Neuere `truck-meshalgo` (0.6) gated `rayon`/`vtkio` selbst per `cfg(not(target_arch="wasm32"))`
|
||||
aus → **baut zu wasm32, aber single-threaded**. Der reine Geometrie-Stack
|
||||
(base → geotrait → geometry → topology → polymesh → meshalgo → modeling → shapeops) hat keine
|
||||
native-only-Deps und ist **sauber vom wgpu-Rendering trennbar** (truck-platform/-rendimpl weglassen).
|
||||
|
||||
**Reifegrad (Apache-2.0, aktiv, ~1 Hauptentwickler; ausdrücklich KEIN Produktionskern):**
|
||||
- NURBS/B-Spline + B-Rep-Topologie: **stabil** (v0.5/0.6).
|
||||
- Booleans (`truck-shapeops`): `and`/`or` vorhanden, aber **instabil bei coincident geometry**
|
||||
(Issue #114); **`difference` fehlt** (#85); **Fillets fehlen komplett**. Der Fork
|
||||
[`monstertruck`](https://github.com/virtualritz/monstertruck) hat difference + rolling-ball-Fillet.
|
||||
→ Für unsere Wandöffnungen (boolean holes) haben wir das ohnehin schon selbst gelöst (`spanCutouts`).
|
||||
- `truck-stepio`: STEP **schreiben** (aber NICHT für boolean-operierte Shapes) + **lesen** (v0.6 beta,
|
||||
neues `truck_stepio::in`).
|
||||
- `truck-js`: fertiger wasm-bindgen-Wrapper (kein npm-Paket → selbst per `wasm-pack` bauen), bündelt
|
||||
modeling/shapeops/meshalgo/stepio; `IntoWasm`-Muster.
|
||||
|
||||
---
|
||||
|
||||
## acadrust — DWG/DXF-Rückgrat
|
||||
|
||||
Pure Rust, **MPL-2.0**, `CadDocument`-Objektmodell (entities/objects/tables/xdata/classes),
|
||||
41 Entity-Typen, DWG „208/208 roundtrip-perfect", serde optional, failsafe-Parsing, ~40 Codepages.
|
||||
**Alle Deps pure Rust** (nom/byteorder/flate2-miniz/nalgebra/indexmap/ahash/encoding_rs) → sehr
|
||||
wahrscheinlich zu wasm32 baubar (Datei-I/O-API müsste auf Bytes/Reader statt `std::fs` umgestellt
|
||||
werden — vermutlich schon vorhanden). Heute nutzen wir JS `dxf-parser` (nur 2D) + `libredwg-web`.
|
||||
|
||||
---
|
||||
|
||||
## Web-Import-Landkarte (für den GEO-BLOCK / Import-Backlog)
|
||||
|
||||
Pro Format die aktive, web-/WASM-taugliche Lib (Stand Mitte 2026):
|
||||
|
||||
| Format | Empfohlene Lib | Lizenz | Anmerkung |
|
||||
|---|---|---|---|
|
||||
| DWG/DXF (browser) | `@mlightcad/cad-viewer` / `libredwg-web` (nutzen wir) **oder** `acadrust` (Rust) | MIT / GPL-3.0 / **MPL-2.0** | acadrust = Rust-Weg, MPL-2.0 |
|
||||
| STEP/IGES/BREP | `occt-wasm` (OCCT V8, ~4.5 MB) oder unser `opencascade.js` | LGPL-2.1 | occt-wasm = moderne TS-API |
|
||||
| **IFC** (BIM!) | `web-ifc` (ThatOpen) | MPL-2.0 | De-facto-Standard, read+write, sehr aktiv |
|
||||
| SHP/GIS | `shpjs` + `proj4` | MIT | direkt für GEO-BLOCK (SWISSIMAGE/Terrain) |
|
||||
| Punktwolken LAS/LAZ | `@loaders.gl/las` (+ `laz-perf`) | MIT/Apache | Rendering: `potree-core` oder deck.gl |
|
||||
| E57 | `e57-js` (Emscripten/libE57Format) | MIT | neu, funktional, sehr obskur |
|
||||
| glTF/OBJ/STL | three.js-Loader (haben wir via `three`) | MIT | |
|
||||
| 3MF | `lib3mf` (WASM) oder `THREE.3MFLoader` | BSD-2 / MIT | |
|
||||
| PDF-Vektor | `pdfjs-dist` `getOperatorList()` | Apache-2.0 | kein fertiges PDF→DXF; roher Pfad-Stream |
|
||||
|
||||
---
|
||||
|
||||
## Priorisierte Ansätze für DOSSIER (die eigentliche Antwort)
|
||||
|
||||
Reihenfolge = Nutzen × Machbarkeit, ohne die BIM-Stärke zu opfern.
|
||||
|
||||
1. **Generisches Entity-Modell + Trait-Dispatch** (Fundament). Neben das BIM-Modell eine
|
||||
generische Entity-Schicht (line/arc/circle/polyline/spline/text/dimension/hatch/block) mit
|
||||
Traits à la OpenCADStudio (Display/Grips/Props/Transform). Unser `kernel2d` liefert schon die
|
||||
Geometrie-Operationen dafür. **Größter struktureller Hebel.**
|
||||
2. **Modeless Command-System** (`StepInput`-Funnel + Kommandozeile). Ein Eingabe-Enum, eine
|
||||
`feed_command`-Schleife, `CadCommand`-Step-Machines. Macht Werkzeuge einheitlich, scriptbar,
|
||||
headless-testbar. **Größter UX-Hebel Richtung „echtes CAD".**
|
||||
3. **DWG/DXF-Round-Trip über `acadrust`** (Rust/WASM). MPL-2.0, pure Rust → passt in unseren
|
||||
neuen `src-tauri`-Kern. Ersetzt langfristig die JS-Parser, bringt echten Import/Export.
|
||||
Erst: WASM-Baubarkeit + Bytes-API verifizieren (Spike wie beim Textur-Spike).
|
||||
4. **Vollständige Object-Snap-Schicht** als eigenes Modul (endpoint/mid/center/intersection/
|
||||
tangent/perp/…), aufbauend auf `kernel2d`.
|
||||
5. **3D-B-Rep später, selektiv** (truck-geometry/-topology/-modeling als reine Crates, OHNE
|
||||
truck-Rendering — wir haben Nordstern). NUR wenn wir echte NURBS/Solids/Booleans brauchen; der
|
||||
Meshing-/Boolean-Pfad ist WASM-problematisch (native C-Deps) und Booleans sind noch früh.
|
||||
Bis dahin: truck als **Referenz** lesen, nicht einbinden.
|
||||
|
||||
**Kernaussage:** DOSSIERs Rust-nach-WASM-Kurs (den wir mit `kernel2d` gerade gehen) ist genau
|
||||
richtig — OpenCADStudio bestätigt ihn. Der Weg zu „echtem CAD" führt über (1) generisches
|
||||
Entity-Modell und (2) modeless Command-System; (3) `acadrust` bringt echten DWG/DXF-Austausch.
|
||||
3D-B-Rep (truck) ist ein späterer, selektiver Schritt mit klaren WASM-Vorbehalten.
|
||||
@@ -1,6 +1,16 @@
|
||||
# Browser-BIM für Wohnbau — Architektur & Produkt-Roadmap
|
||||
|
||||
> Arbeitstitel: **cad** (Name später)
|
||||
> **Historisches Dokument (Tag-1-Vision, Stand 2026-06-28).** Vieles hier als
|
||||
> „Phase 2–5"/„Backlog" gelistete ist inzwischen längst gebaut (Treppen, Dächer,
|
||||
> Stützen, SIA-416, Swisstopo, OSM, Layouts, Ausschnitte, Kamera-Presets …), und
|
||||
> mehrere Technologie-Entscheidungen liefen anders (eigene Rust/WASM-Engines
|
||||
> statt Three.js/OpenCascade.js/web-ifc, siehe unten §4/§8). Für den aktuellen
|
||||
> Ist-Zustand: **[STATUS.md](STATUS.md)** (Bestandsaufnahme + Mist-Liste) und
|
||||
> **[ARCHITECTURE.md](ARCHITECTURE.md)** (aktuelle Architektur). Dieses Dokument
|
||||
> bleibt als ursprüngliche Produktvision/Phasenplan stehen, wird aber nicht mehr
|
||||
> laufend nachgeführt.
|
||||
>
|
||||
> Arbeitstitel: **cad** (Name später: **Dossier**)
|
||||
> Ausrichtung: **BIM-first** · Nische: **Wohnbau / Einfamilienhäuser**
|
||||
> Stand: 2026-06-28
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
# SPIKE — Bild-Texturen in `render3d` (`RenderStyle::Textured` real machen)
|
||||
|
||||
Stand: 2026-07-05. **Auftrag/Übergabe für einen Agenten. Kleinster ehrlicher
|
||||
Durchstich — kein Produktfeature, keine Integration.**
|
||||
|
||||
Ziel: Beweisen, dass der bestehende wgpu-3D-Renderer echte **Bild-Texturen** auf
|
||||
Wandflächen darstellen kann, sichtbar im `spike3d`-Fenster. Am Ende steht eine
|
||||
belastbare Aussage, wie viel Arbeit „richtig gutes texturiertes 3D" wirklich ist —
|
||||
statt Spekulation.
|
||||
|
||||
---
|
||||
|
||||
## 0. Ausgangslage (verifiziert am 2026-07-05)
|
||||
|
||||
- `render3d` ist **kein** Three.js-Wrapper, sondern ein eigenständiger wgpu-Renderer
|
||||
(~6400 LOC): echte GPU-Pipeline (wgpu 29), Tiefenpuffer, MSAA, WGSL-Shader.
|
||||
Läuft nativ (winit-Spike), headless (naga-validiert) und im Browser (WebGPU).
|
||||
- **`RenderStyle::Textured` existiert bereits als Stub** (`gpu.rs:34` Enum-Variante,
|
||||
`gpu.rs:47` Parse aus `"textured"`) — es gibt aber **kein echtes Texturing**:
|
||||
kein Sampler, keine Textur-Bind-Group, keine Bilddaten.
|
||||
- Die `cap_pipeline` mit Layout `[pos vec3, uv vec2]` + `CAP_WGSL` ist **nicht** für
|
||||
Bildtexturen, sondern für die **Schnittflächen-Kappen** (prozedurale Schraffur);
|
||||
die UVs steuern dort den Schraffur-Abstand. **Nicht damit verwechseln.**
|
||||
- **Günstig für uns:** Die Muster, die der Spike braucht, existieren schon —
|
||||
ein UV-tragendes Vertex-Layout und eine zweite/dritte Pipeline, die sich die
|
||||
`Globals`-Bind-Group teilt (`grid`, `cap`). Der texturierte Mesh-Pfad reiht sich
|
||||
1:1 in dieses Muster ein.
|
||||
|
||||
---
|
||||
|
||||
## 1. Echte Symbole — vor dem Coding lesen
|
||||
|
||||
| Was | Ort |
|
||||
|---|---|
|
||||
| Vertex heute interleaved `[pos.xyz, normal.xyz, color.rgb]`, `FLOATS_PER_VERTEX`, `Mesh`-Struct | `src-tauri/render3d/src/types.rs:322` ff. |
|
||||
| Quad-Emitter (Normale + Farbe je Vertex, Reihenfolge `(0,1,2)+(0,2,3)`) | `src-tauri/render3d/src/mesh.rs:886` |
|
||||
| Wand-Extrusion / Mesh-Bau | `src-tauri/render3d/src/mesh.rs` — `extrude_wall` (`:91`), `build_walls_mesh` (`:911`) |
|
||||
| Haupt-Pipeline + `Globals`-Bind-Group (group 0) | `src-tauri/render3d/src/gpu.rs:232`–`:320` |
|
||||
| Vorlage „zweite Pipeline teilt sich Globals" — Grid | `src-tauri/render3d/src/gpu.rs:329` |
|
||||
| Vorlage „Pipeline mit UV-Layout `[pos vec3, uv vec2]`" — Cap | `src-tauri/render3d/src/gpu.rs:408` |
|
||||
| `RenderStyle`-Enum + Stub `Textured` | `src-tauri/render3d/src/gpu.rs:34`, `:47` |
|
||||
| Pipeline-Bindung im Render-Pass (Muster für Stil-Umschaltung) | `src-tauri/render3d/src/gpu.rs:847` |
|
||||
| Shader als WGSL-Konstanten (`MESH_WGSL`, `CAP_WGSL`) | `src-tauri/render3d/src/shaders.rs` |
|
||||
| Beleuchtungsmodell (hemisphärisch + Directional + Fill) — Doku | `src-tauri/render3d/src/shaders.rs:1`–`40` |
|
||||
| naga-WGSL-Validierung headless (Test-Vorlage) | `src-tauri/render3d/src/lib.rs:924` (`cap_module`) |
|
||||
| Fenster-Spike mit Orbit-Kamera | `src-tauri/render3d/src/bin/spike3d.rs` |
|
||||
|
||||
---
|
||||
|
||||
## 2. Umfang — exakt das, nicht mehr
|
||||
|
||||
### 2.1 UVs auf Wandflächen
|
||||
- In der Quad-Emitter-Funktion (`mesh.rs:886`) je Vertex eine **UV** berechnen:
|
||||
planare Projektion in **Metern** — `u` = Distanz entlang der Wandachse,
|
||||
`v` = Höhe (z). Textur-Raster damit weltmassstäblich (z. B. 1 Kachel = 1 m).
|
||||
- Den bestehenden `[pos, normal, color]`-Pfad **bitgleich unangetastet** lassen.
|
||||
Zwei zulässige Wege (Agent wählt begründet):
|
||||
1. **Separates additives UV-Array** in `Mesh` (Default leer/None), oder
|
||||
2. **Paralleler `build_walls_mesh_textured`** → interleaved
|
||||
`[pos.xyz, normal.xyz, uv.xy]`.
|
||||
- Regressionstests für den Alt-Pfad müssen grün bleiben (siehe §4).
|
||||
|
||||
### 2.2 Test-Textur prozedural (kein Asset, keine `image`-Crate)
|
||||
- Ein **256×256 RGBA-Schachbrett/Grid im Code** generieren (`Vec<u8>`).
|
||||
- `device.create_texture` + `queue.write_texture` + `Sampler`
|
||||
(`FilterMode::Linear`, `AddressMode::Repeat`). Mipmaps optional (nice-to-have für
|
||||
flache Blickwinkel; kein Muss für den Spike).
|
||||
- Selbstständig, damit der Spike ohne Dateipfade/Asset-Pipeline läuft.
|
||||
|
||||
### 2.3 Textur-Bind-Group (group 1)
|
||||
- Neue Bind-Group-Layout mit `texture_view` (`TextureSampleType::Float`) +
|
||||
`sampler`. **`Globals` bleibt group 0** und unverändert.
|
||||
|
||||
### 2.4 Textured-Pipeline + WGSL (`MESH_TEXTURED_WGSL`)
|
||||
- Vertex-Layout `[pos vec3, normal vec3, uv vec2]`, `TriangleList`.
|
||||
- **Dieselbe Beleuchtung wie `MESH_WGSL`** (hemisphärisches Ambient + Directional +
|
||||
Fill) — nur **Albedo = `textureSample(tex, samp, uv)`** statt Vertex-Farbe.
|
||||
- Depth-Format, MSAA (`SAMPLE_COUNT`) und Color-Target **identisch** zur
|
||||
Haupt-Pipeline (sonst inkompatibler Render-Pass).
|
||||
- Pipeline-Layout bindet group 0 (Globals) **und** group 1 (Textur).
|
||||
|
||||
### 2.5 Verdrahten
|
||||
- Bei `RenderStyle::Textured` im Render-Pass die neue Pipeline + beide Bind-Groups
|
||||
setzen (Muster: `cap_pipeline`-Bindung bei `gpu.rs:847`).
|
||||
|
||||
### 2.6 Spike sichtbar machen
|
||||
- `spike3d.rs` so erweitern, dass der Stil auf `Textured` schaltbar ist
|
||||
(Tastendruck, z. B. `T`, **oder** Startkonstante). Die Demo-Wände sollen
|
||||
texturiert im Orbit erscheinen.
|
||||
|
||||
---
|
||||
|
||||
## 3. Randbedingungen (hart)
|
||||
|
||||
- **Nur** die `render3d`-Crate. `src/web.rs` und die Tauri-/`native3d`-Oberfläche
|
||||
**nicht** anfassen.
|
||||
- Feature-gegatet unter dem bestehenden `render`/`window`-Feature.
|
||||
**Default-Build und Default-Darstellung bleiben unverändert.**
|
||||
- **Keine neuen Dependencies** (insbesondere **kein `image`-Crate**) für den Spike.
|
||||
- Term-/Reihenfolge-sensible Geometrie (Parität) wird **nicht** berührt — es kommt
|
||||
nur additiv ein UV-Kanal + ein zweiter Render-Pfad dazu.
|
||||
- Kommentar-Stil und Sprache (Deutsch, ausführliche Begründungs-Kommentare) wie im
|
||||
umgebenden Code beibehalten.
|
||||
|
||||
---
|
||||
|
||||
## 4. Akzeptanz / Verifikation
|
||||
|
||||
1. `cargo test` (im Crate-Verzeichnis `src-tauri/render3d`) **grün**, inklusive:
|
||||
- bestehende Mesh-Regression (Alt-Pfad `[pos,normal,color]` unverändert),
|
||||
- **neuer naga-Validierungstest** für `MESH_TEXTURED_WGSL` (Vorlage:
|
||||
`lib.rs:924`).
|
||||
2. `cargo run --features window --bin spike3d` zeigt die Demo-Wände mit
|
||||
**erkennbarer, korrekt gemappter** Schachbrett-Textur:
|
||||
- Raster weltmassstäblich (in Metern), keine Verzerrung an Gehrungen/Ecken,
|
||||
- beleuchtet wie im Shaded-Modus (Volumen bleibt ablesbar).
|
||||
3. Umschalten Shaded ↔ Textured zur Laufzeit (oder per Startkonstante) funktioniert
|
||||
ohne Re-Meshing-Crash.
|
||||
|
||||
---
|
||||
|
||||
## 5. Abschlussbericht (vom Agenten am Ende zu liefern)
|
||||
|
||||
- Welche Dateien geändert/hinzugefügt wurden und warum.
|
||||
- Wie die UVs projiziert werden (Achswahl, Massstab, Verhalten an Gehrungen).
|
||||
- Welcher der beiden UV-Wege (§2.1) gewählt wurde und weshalb.
|
||||
- **Ehrliche Lückenliste für „richtig gutes" Texturing:** Asset-/Bild-Datei-Laden,
|
||||
Material→Textur-Zuordnung (Wandtyp/Layer → Material), Normal-/Roughness-Maps
|
||||
(PBR), anisotropes Filtern + Mipmaps, Web-Pfad (`web.rs`/WebGPU), UI zum
|
||||
Zuweisen. Grobschätzung Aufwand je Punkt.
|
||||
|
||||
---
|
||||
|
||||
## 6. Nicht im Scope
|
||||
|
||||
Asset-/Bild-Datei-Laden · Material-System · mehrere Texturen gleichzeitig ·
|
||||
PBR/Normal-Maps · Web-Pfad (`web.rs`) · jegliche UI · Anbindung unter die Webview.
|
||||
@@ -0,0 +1,342 @@
|
||||
# STATUS — Codebase-Analyse
|
||||
|
||||
> Stand: 2026-07-21 · Vollständige Bestandsaufnahme von Code + Dokumentation.
|
||||
> Ersetzt NICHT [ROADMAP.md](ROADMAP.md)/[ARCHITECTURE.md](ARCHITECTURE.md) (die wurden
|
||||
> im gleichen Zug überarbeitet), sondern begründet die Überarbeitung mit Zahlen und
|
||||
> Befunden. [HANDOVER.md](HANDOVER.md) und [PENDENZEN.md](PENDENZEN.md) bleiben die
|
||||
> laufenden Arbeitsprotokolle (nicht rückwirkend umgeschrieben).
|
||||
|
||||
## 0. TL;DR
|
||||
|
||||
„Dossier" (Arbeitstitel `cad`, Rhino-Vorbild `DOSSIER`) ist in **3 Wochen**
|
||||
(erster Commit 2026-06-30, 362 Commits bis 2026-07-20) von einem Risiko-Spike zu
|
||||
einem **funktionsreichen Desktop-CAD/BIM-Tool** gewachsen: eigenes semantisches
|
||||
Gebäudemodell, zwei eigene Rust/WASM-Rendering-Engines („Nordstern"), ein
|
||||
Rhino-artiges Kommandosystem, IFC/DXF/PDF/STL/OBJ-Export, Swisstopo-Import,
|
||||
SIA-416-Flächen, Materialbibliothek (statisch + live von ambientCG), Layouts/
|
||||
Plansätze, Ausschnitte, native Tauri-Fenster. **~125.000 Zeilen Code** (TS+Rust,
|
||||
inkl. Tests), gebaut über viele autonome Agent-Sessions.
|
||||
|
||||
Die drei zentralen Vision-Dokumente (ARCHITECTURE.md, README.md, ROADMAP.md)
|
||||
stammen aus der **allerersten Woche** (Stand 28./29.6.) und beschreiben einen
|
||||
Plan, der in der Zwischenzeit an vielen Stellen überholt, anders gelöst oder
|
||||
längst umgesetzt wurde (z. B. HLR/OCCT→eigene Rust-Schnitt-Pipeline,
|
||||
„Booleans noch offen"→teilweise längst gelöst). Diese Doku-Drift war der Auslöser
|
||||
für diese Analyse; die Docs sind im gleichen Zug revidiert worden.
|
||||
|
||||
## 1. Kennzahlen
|
||||
|
||||
### Code-Umfang
|
||||
|
||||
| Bereich | Dateien | LOC (ohne Tests) | Tests |
|
||||
|---|---:|---:|---:|
|
||||
| `src/` (TypeScript, gesamt) | ~230 | 87.736 | 869 (Vitest, 71 Dateien) / 16.855 LOC |
|
||||
| `src-tauri/render3d` („Nordstern" 3D) | 14 | 10.246 | 97 `#[test]` |
|
||||
| `src-tauri/render2d` (2D-WGSL-Renderer) | 11 | 3.530 | 18 `#[test]` |
|
||||
| `src-tauri/kernel2d` (Rust-Geometriekern, Paritätstest) | 1 | 3.383 | 18 `#[test]` |
|
||||
| `src-tauri/geometry` (Wand-Join-Mathe, **unbenutzt**) | 1 | 1.057 | 8 `#[test]` |
|
||||
| `src-tauri/trucksolid` (CSG/Extrusion, `truck`+`csgrs`) | 2 | 758 | 15 `#[test]` |
|
||||
| `src-tauri/dwgimport` (DXF-Parser-Spike, **unbenutzt**) | 1 | 284 | 1 `#[test]` |
|
||||
| `src-tauri/src` (Tauri-Host: Fenster, Dialoge, Lock) | 4 | 1.044 | — |
|
||||
| **Gesamt** | | **~108.000** (ohne Tests) / **~125.000** (mit Tests) | 869 Vitest + 157 Rust-Tests |
|
||||
|
||||
Größte TS-Bereiche: `plan/` (23.626 LOC — Plan-Ableitung + 3 Renderer),
|
||||
`ui/` (12.897 LOC — App-Shell, ResourceManager, Ribbon), `panels/` (9.538 LOC),
|
||||
`model/` (7.043 LOC inkl. Tests), `commands/` (6.973 LOC), `io/` (5.299 LOC),
|
||||
`state/` (5.297 LOC), `geometry/` (5.559 LOC), `viewport/` (5.165 LOC),
|
||||
`export/` (4.280 LOC). `src/App.tsx` allein ist **7.130 Zeilen**.
|
||||
|
||||
### Tempo
|
||||
|
||||
362 Commits in 3 Wochen; Woche 27 (30.6.–6.7.): 233 Commits, Woche 28: 126,
|
||||
danach starker Rückgang (Woche 30 bislang 3) — die Session-Dichte hat spürbar
|
||||
abgenommen, nicht das Projekt gestoppt (siehe PENDENZEN.md, weiterhin aktiv).
|
||||
|
||||
## 2. Architektur, wie sie WIRKLICH ist (nicht wie geplant)
|
||||
|
||||
### 2.1 Datenmodell — kein `Element[]`-Union, sondern typisierte Arrays
|
||||
|
||||
`ARCHITECTURE.md` (alt) plante eine diskriminierte Union `Element = Wall | Door |
|
||||
Window | …`. Tatsächlich hält `Project` (`src/model/types.ts:2095`) **pro
|
||||
Bauteiltyp ein eigenes optionales Array**: `walls`, `ceilings?`, `roofs?`,
|
||||
`doors`, `openings?` (Fenster/Türen gehostet in Wänden, `kind:"window"|"door"`),
|
||||
`stairs?`, `rooms?`, `columns?`, `extrudedSolids?`, `drawings2d`, `context?`
|
||||
(Importe/Terrain), plus die Bibliotheks-/Typtabellen (`lineStyles`, `hatches`,
|
||||
`components`, `wallTypes`, `roofTypes?`, `doorTypes?`, …) und die
|
||||
Dokument-Ebene (`viewSnapshots?`, `layouts?`, `masterLayouts?`, …). Der Typ-Alias
|
||||
`Element` (types.ts:1601) existiert zwar noch, wird aber **nirgends** verwendet
|
||||
(Element-Baum/Selektion arbeiten direkt auf den typisierten Arrays). Praktisch
|
||||
funktioniert das gut (jeder Bauteiltyp hat sein eigenes, spezifisches Interface),
|
||||
ist aber eine bewusste Abweichung vom ursprünglichen Uniform-Union-Plan.
|
||||
|
||||
### 2.2 State — kein Zustand/Redux/Immer, sondern ein Eigenbau
|
||||
|
||||
`docs/design/state-architecture.md` empfahl **Zustand**. Gebaut wurde stattdessen
|
||||
ein **abhängigkeitsfreier Store auf `useSyncExternalStore`** (`src/state/store.ts`,
|
||||
gleiches Muster wie `src/i18n`). `createStore()` komponiert Slice-Fabriken
|
||||
(`projectSlice` inkl. Undo/Redo, `historySlice`, `selectionSlice`, `viewSlice`,
|
||||
`layoutSlice`, `siteSlice`, `notifySlice`) über eine gemeinsame `RootState`.
|
||||
Funktioniert, aber: der geplante Folgeschritt „App.tsx wird dünne Shell,
|
||||
View-Routing nach `src/views/`, Kontextmenüs nach `src/menus/`" ist **nicht**
|
||||
passiert — `src/views/` und `src/menus/` existieren nicht, View-Umschaltung und
|
||||
Kontextmenü-Aufbau liegen weiterhin inline in `App.tsx` (7.130 Zeilen). Das ist
|
||||
der deutlichste Doku-vs-Code-Widerspruch im ganzen Repo (CONVENTIONS.md
|
||||
verlangt explizit das Gegenteil).
|
||||
|
||||
### 2.3 Rendering — drei 2D-Pfade, zwei 3D-Viewports
|
||||
|
||||
**2D-Plan:** es gibt tatsächlich **drei** koexistierende Renderer, nicht einen:
|
||||
1. `PlanView.tsx` (SVG) — Referenz-/Fallback-Pfad, bleibt IMMER im DOM für
|
||||
Hit-Testing/Grips, unabhängig davon was zeichnet.
|
||||
2. `plan/glPlan/` — eigener TypeScript-WebGL2-Renderer (`glPlanCompile/-Render/
|
||||
-Shaders/-Hatch.ts`).
|
||||
3. `useWasmPlanRenderer.ts` → Rust-`render2d`-Crate (WGSL, nativ via wgpu,
|
||||
Web via WebGPU/WebGL2-Fallback, inkl. echtem Text-Rendering via `glyphon`).
|
||||
|
||||
Beide GPU-Pfade fallen bei Initialisierungsfehler still auf SVG zurück.
|
||||
|
||||
**3D:** ebenfalls zwei Viewports: `Viewport3D.tsx` (three.js, „Free"-Stufe) und
|
||||
`Wasm3DViewport.tsx` (Rust/wgpu „Nordstern", editierbar, **Default**). Ein
|
||||
Settings-Schalter wählt die Engine.
|
||||
|
||||
### 2.4 Schnitt/Section — NICHT über HLR, sondern eigene Rust-Pipeline
|
||||
|
||||
`src/section/hlr.ts` + `occt.ts` (der ursprüngliche OpenCascade.js-HLR-Spike aus
|
||||
Phase 0) hat **keinen einzigen Aufrufer mehr** im gesamten `src/` — toter Code.
|
||||
Der tatsächliche, funktionierende Live-Schnitt läuft über einen völlig anderen,
|
||||
analytischen Mechanismus: `App.tsx` (`section3dCutId`/`section3dPlane`) →
|
||||
`Wasm3DViewport.tsx` (`section3d`-Prop → `setSectionPlane`) →
|
||||
`src-tauri/render3d/src/{section.rs, section_boolean.rs, section_fill.rs}`.
|
||||
Die Rust-Seite nutzt aus, dass jedes Bauteil ein Prisma mit konstantem
|
||||
Querschnitt ist — eine Schnittebene liefert dadurch immer ein
|
||||
achsparalleles Rechteck, nie ein Trapez; `section_boolean.rs` ist ein 1:1-Port
|
||||
von `toSection.ts::subtractDominantBands`, damit 2D-Plan-Schnitt und
|
||||
3D-Live-Schnitt exakt übereinstimmen. Kein Worker, kein Comlink (beides war
|
||||
geplant, keines existiert) — läuft synchron/GPU-seitig.
|
||||
|
||||
### 2.5 Öffnungen als Löcher — echt, aber kein Mesh-Boolean
|
||||
|
||||
Fenster/Türen schneiden echte achsparallele Rechteck-Löcher aus dem Wandkörper
|
||||
(`plan/toWalls3d.ts` `RHole`/`subtractSpans`, gespiegelt in
|
||||
`render3d/{mesh.rs,section.rs}`) — funktioniert, ist aber KEIN generisches
|
||||
Mesh-Boolean. Ein echtes CSG-Boolean existiert bereits (`trucksolid::boolean_mesh`,
|
||||
`csgrs`-basiert, 15 Rust-Tests) und wird von `src/engine/truckSolid.ts` für das
|
||||
Extrusions-Kommando genutzt — ist aber **nicht** an die Wand/Öffnungs-Pipeline
|
||||
angeschlossen (bestätigt: `booleanMesh` hat ausserhalb von `truckSolid.ts`
|
||||
keinen Aufrufer).
|
||||
|
||||
### 2.6 Rust-Workspace: sechs unabhängige Crates, nicht ein Workspace
|
||||
|
||||
`src-tauri/Cargo.toml` bindet nur den Tauri-Host (`cad-tauri`) als Workspace-
|
||||
Mitglied; `render2d/render3d/geometry/kernel2d/trucksolid/dwgimport` sind
|
||||
**eigenständige Cargo-Packages**, die dem Host nur optional (Features
|
||||
`native2d`/`native3d`, standardmässig AUS) als Path-Dependency zugespielt
|
||||
werden. Jedes Crate muss headless (`cargo test`) UND per `wasm-pack --features
|
||||
web` bauen, ohne den Tauri-Toolchain-Zwang zu erben — bewusst so geschnitten.
|
||||
Geteilte Abhängigkeiten: `wgpu 29`/`naga 29` (render2d+render3d, versionsgekoppelt
|
||||
wegen `glyphon 0.11`), `truck-modeling`+`csgrs`(gepinnter Git-Rev)+`nalgebra`
|
||||
nur in `trucksolid`.
|
||||
|
||||
### 2.7 Zwei Desktop-Rahmen: Tauri (macOS) + Electron (Linux)
|
||||
|
||||
Die App läuft plattformabhängig in **zwei verschiedenen nativen Rahmen** —
|
||||
das ist Absicht, kein Wildwuchs, und hängt an **WebGPU**:
|
||||
|
||||
- **macOS → Tauri.** WKWebView unterstützt WebGPU, das die render2d/render3d-
|
||||
WASM-Engines brauchen. `src-tauri/tauri.conf.json` (Identifier
|
||||
`ch.dossier.cad`, eigene Titelleiste, `trafficLightPosition`) +
|
||||
`isTauriRuntime()`-Gates an 6+ Stellen in `App.tsx` + vier eigene native
|
||||
Zusatzfenster (`src/native/`: Resources, Settings, DrawingLevels,
|
||||
LayerSettings, ContextImport).
|
||||
- **Linux → Electron.** Tauris Linux-Webview **WebKitGTK unterstützt WebGPU
|
||||
nicht zuverlässig** → dort läuft die App über eine Electron/Chromium-Shell
|
||||
(`scripts/electron-main.cjs` + `electron-preload.cjs`, gestartet via
|
||||
`npm run electron`). Der Kommentar in `electron-main.cjs` sagt es explizit:
|
||||
„Ersetzt WebKitGTK durch Chromium, damit WebGPU zuverlässig läuft."
|
||||
|
||||
Beide teilen sich **dieselbe** React-App und dieselbe randlose eigene
|
||||
Titelleiste; die Laufzeit erkennt den Host über `window.__TAURI__` (Tauri)
|
||||
bzw. `window.dossierWindow` (Electron, per `contextBridge` injiziert). Die
|
||||
Fenstersteuerung (`src/ui/WindowControls.tsx`) ist an beide Wege angebunden.
|
||||
Kein Rust-Backend nötig auf dem Electron-Pfad — `computeJoins` hat einen
|
||||
TS-Fallback (`src/compute/index.ts`). Electron ist also **kein** totes Gleis,
|
||||
sondern der aktive Linux-Zielrahmen.
|
||||
|
||||
## 3. Feature-Inventar (was tatsächlich funktioniert)
|
||||
|
||||
### Modell & Bauteile
|
||||
- Mehrschichtige Wände (`WallType.layers[]`) mit L-Eck-Gehrung UND
|
||||
Prioritäts-T-/X-Stössen (`joinPriority` am Component) — **fertig**, in 2D-Plan,
|
||||
3D-Viewport UND 3D-Live-Schnitt konsistent (Rust-Port `section_boolean.rs`).
|
||||
- Parametrische Wände (`ParametricWall`: Grid/Modul/Sequenz/Referenzlinie/
|
||||
bedingte Dicke) lösen sich zu konkreten `Wall[]` auf.
|
||||
- Decken (Slabs, `ceilings?`) mit Aussparungen, eigenem Typkatalog.
|
||||
- Türen/Fenster gehostet in Wänden, mit Rahmen/Zarge/Blockrahmen, Kämpfer,
|
||||
Oberlicht, Detailgrad grob/mittel/fein (2D UND 3D), Schwenkbogen; daneben
|
||||
existiert weiterhin ein **älteres, separates `Door[]`** neben `Opening[]` —
|
||||
laut PENDENZEN.md explizit als offene Doppelspur/Aufräum-Punkt vermerkt.
|
||||
- Treppen (gerade/L/Wendel), geschossübergreifend, 2D-Symbol mit Lauflinie/Pfeil.
|
||||
- Dächer (Flach/Pult/Sattel/Walm/Mansarde/Zelt) über Rechteck-Umriss, First/
|
||||
Traufe/Grat im 2D-Plan.
|
||||
- Stützen (Column) mit Profilbibliothek (Quadrat/Rechteck/Rund/I/Rohr).
|
||||
- Räume (SIA-416: HNF/NNF/VF/FF/GF/AGF) mit automatischer Bilanz + CSV-Export,
|
||||
Raumstempel-Editor (Drag&Drop-Felder).
|
||||
- Extrudierte Volumenkörper (truck-Integration: konkave Profile, Verjüngung).
|
||||
- Kontext-Layer (Terrain-TIN, importierte Meshes, Konturen) — semantisch getrennt.
|
||||
|
||||
### Zeichnen & Bedienung
|
||||
- Rhino-artiges Kommandosystem (`commands/`): getippte Koordinaten
|
||||
(`5,3`/`r5,3`/`5<45`), Tab-Feld-Zyklus für Präzisionseingabe
|
||||
(`src/ui/CommandLine.tsx`), Alias/Autocomplete, ~25 Kommandos (wall, ceiling,
|
||||
opening, stair, column, roof, room, line/polyline/rect/circle/arc, text,
|
||||
move/mirror/copy/offset/trim/join, extrude, import, terrain, measure,
|
||||
Schnittlinie, Georef).
|
||||
- Snapping (Endpunkt/Mitte/Schnittpunkt/Lot/Raster/Ortho), Grips, Array,
|
||||
Trim/Split/Join, 2D-Booleans (Union/Subtract/Intersect via `polygon-clipping`).
|
||||
- Messwerkzeug (Polygonzug, Länge + Fläche).
|
||||
- Rich-Text-Annotationen (Bold/Kursiv/Hoch-/Tiefstellung).
|
||||
|
||||
### Darstellung / Ressourcen
|
||||
- Resource Manager: Line/Hatch/Component-Manager, Wand-/Decken-/Tür-/Fenster-/
|
||||
Treppen-/Dach-Typeditoren — als eigenständiges natives Fenster (nicht als
|
||||
Dock-Panel).
|
||||
- Materialbibliothek: 13 fest gebündelte PBR-Starter (ambientCG, lokale
|
||||
Texturen) **plus** Live-Suche der kompletten ambientCG-Bibliothek (Auflösung
|
||||
1K/2K/4K, on-demand Download+Entpacken via `jszip`, Proxy wegen CORS) — heute
|
||||
bereinigt (siehe Commit-Historie dieser Session).
|
||||
- Regelbasierte Overrides (Bedingung → Farbe/Strichstärke/Schraffur/Sichtbarkeit).
|
||||
- Detailgrad grob/mittel/fein je Bauteil + Dokument-Override.
|
||||
- Hell-/Dunkel-Theme, Akzentfarben.
|
||||
|
||||
### Pläne / Output
|
||||
- Ausschnitte (View-Snapshots: Kamera, Massstab, Detailgrad, Sichtbarkeiten,
|
||||
Override-Preset) in Ordnerstruktur.
|
||||
- Layout-Blätter (Plansätze): Papierformat/-grösse, mehrere Viewports pro
|
||||
Blatt, Masterlayout-Vererbung (Titelblock), Ordnerstruktur, freie 2D-Annotationen.
|
||||
- Vektor-Export: PDF (Einzelblatt UND **Mehrseiten pro Ordner**,
|
||||
`layoutPdf.ts::buildFolderPdf`), DXF, IFC4 (mit echten Fenster-/Tür-Löchern,
|
||||
deterministischen GUIDs), STL, OBJ, CSV-Bauteil-Schedule (volles Element-Set).
|
||||
- Kamera-Presets (Kardinal + Iso), Norden-Rotation.
|
||||
|
||||
### Import / Kontext
|
||||
- DXF (Konturen), DWG (`@mlightcad/libredwg-web`, WASM), `.lin`/`.pat`.
|
||||
- Swisstopo: swissBUILDINGS3D (radiusgenau zugeschnitten, nicht die ganze
|
||||
STAC-Kachel), swissALTI3D, SWISSIMAGE-Orthofoto-Draping, LV95↔WGS84,
|
||||
Georeferenzierung über EINEN Vermessungspunkt (E/N/H, `geoAnchor`).
|
||||
- OSM/Overpass-Kontextimport (7 Kategorien).
|
||||
- Terrain-Mesh-Generator.
|
||||
|
||||
### Desktop-Integration (Tauri)
|
||||
- Eigene randlose Fenster mit nativer Titelleiste (macOS-Ampel-Position).
|
||||
- Native Speichern/Öffnen-Dialoge (`plugin-fs`/`plugin-dialog`), eigenes
|
||||
`.obp`-Projektdateiformat.
|
||||
- OS-Level-Exklusiv-Lock gegen Doppelöffnen desselben Projekts (`fs4`-Crate).
|
||||
- Vier eigenständige native Zusatzfenster (Resources, Settings, DrawingLevels,
|
||||
LayerSettings, ContextImport) statt Overlay/Modal.
|
||||
- Native macOS-Menüleiste.
|
||||
- i18n de/en durchgängig, eigener `t()`-Mechanismus (kein i18next).
|
||||
|
||||
## 4. Mist-Liste — Befunde, Doku-Widersprüche, offene Fäden
|
||||
|
||||
### 4.1 Verwaiste WASM-Crates (gebaut, aber nirgends importiert)
|
||||
- **`src-tauri/geometry`** (1.057 LOC, 8 Tests) → `pkgGeometry` — **null**
|
||||
Importstellen in `src/`. Wand-Join-Mathe existiert redundant als TS
|
||||
(`src/model/joins.ts`) UND als Rust-Port, aber nur die TS-Version läuft.
|
||||
- **`src-tauri/dwgimport`** (284 LOC) → `pkgDwgImport` — **null** Importstellen;
|
||||
DWG-Import läuft stattdessen über `@mlightcad/libredwg-web` (npm-Paket).
|
||||
- **`src-tauri/kernel2d`** (3.383 LOC, 18 Tests) → `pkgKernel2d` — wird nur von
|
||||
einem Paritätstest (`kernel2d.parity.test.ts`) konsumiert, nicht produktiv.
|
||||
Die TS-Version `src/geometry/kernel2d.ts` (886 Zeilen) ist die tatsächlich
|
||||
laufende Implementierung. Laut PENDENZEN.md bewusst so belassen: ein
|
||||
Join-Benchmark zeigte WASM unter ~100 Wänden **langsamer** als die naive
|
||||
TS-Routine. Kein Bug, aber die drei Crates zusammen sind ~4.700 Zeilen Rust
|
||||
(+44 Tests), die aktuell nichts zur Laufzeit beitragen ausser einem
|
||||
Korrektheits-Cross-Check für kernel2d.
|
||||
|
||||
**Empfehlung:** entweder (a) `geometry`- und `dwgimport`-Crate + ihre
|
||||
`build:*`-Scripts entfernen (kein Nutzen, nur Wartungslast), oder (b)
|
||||
explizit als „Referenzimplementierung/Zukunftsoption" in ARCHITECTURE.md
|
||||
dokumentieren, damit niemand sie für aktiv hält.
|
||||
|
||||
### 4.2 Toter Code
|
||||
- `src/section/hlr.ts` + `occt.ts` + `occt-wasm.d.ts` (473 LOC) — OCCT-WASM-
|
||||
HLR-Spike aus Phase 0, **keine Aufrufer mehr**. Ersetzt durch die analytische
|
||||
Rust-Schnitt-Pipeline (§2.4). `opencascade.js` bleibt als npm-Dependency
|
||||
bestehen, obwohl nur noch dieser tote Code sie importiert.
|
||||
- `src/export/planToPrintSvg.ts` — Kommentar im Code selbst sagt „ERSETZT durch
|
||||
`sceneToPrintSvg.ts`", ist aber noch im Baum.
|
||||
- `Element`-Typalias (`src/model/types.ts:1601`) — definiert, nirgends benutzt.
|
||||
|
||||
### 4.3 Das grösste Doku-vs-Code-Problem: App.tsx
|
||||
CONVENTIONS.md verlangt seit Tag 1 „App.tsx bleibt dünner Shell, keine
|
||||
Geschäftslogik". `docs/design/state-architecture.md` plante explizit die
|
||||
Extraktion nach `src/views/` (View-Routing) und `src/menus/`
|
||||
(Kontextmenü-Aufbau). Beide Ordner **existieren nicht**. `App.tsx` ist mit
|
||||
**7.130 Zeilen** die grösste Einzeldatei des Projekts und enthält weiterhin
|
||||
View-Umschaltung und Kontextmenü-Konstruktion inline. Das ist der genaue
|
||||
„God-Component"-Zustand, den die Doku von Anfang an vermeiden wollte.
|
||||
|
||||
### 4.4 Bekannte Doppelspur: `Door[]` vs. `Opening[]`
|
||||
`Project` führt sowohl ein älteres `doors: Door[]` als auch das neuere,
|
||||
allgemeinere `openings?: Opening[]` (`kind:"door"|"window"`). Laut
|
||||
PENDENZEN.md ist das erkannt und als Aufräum-Punkt vorgemerkt, aber nicht
|
||||
konsolidiert.
|
||||
|
||||
### 4.5 Doku-Widersprüche (README/ARCHITECTURE/CONVENTIONS vs. Realität)
|
||||
| Dokument | Behauptung | Realität |
|
||||
|---|---|---|
|
||||
| README.md | Shell = Electron, Tauri „ausrangiert" | Falsch andersrum gedacht: **beide** sind aktiv — Tauri auf macOS (WKWebView+WebGPU), Electron auf Linux (WebKitGTK kann kein WebGPU); siehe §2.7 |
|
||||
| README.md | „HLR noch nicht ans UI verdrahtet, Views sind Stubs" | Der OCCT-HLR-Pfad stimmt (tot), aber Schnitte/Ansichten funktionieren real über eine andere, eigene Rust-Pipeline |
|
||||
| README.md | „Prioritäts-T-/X-Stösse … Risiko #1" unter „bewusst offen" | Seit 7.7. erledigt, inkl. Rust-Port |
|
||||
| README.md | „PDF-Export ist noch single-sheet" | `layoutPdf.ts::buildFolderPdf` erzeugt echte Mehrseiten-PDFs pro Ordner |
|
||||
| README.md | Engine-Liste nennt nur render2d/render3d | Es gibt 6 Rust-Crates (+kernel2d/geometry/trucksolid/dwgimport) |
|
||||
| CONVENTIONS.md | Struktur-Ziel `src/views/`, `src/menus/` | Existieren nicht; Logik liegt in `App.tsx` |
|
||||
| CONVENTIONS.md | Dev-Port 5173 | Tatsächlich 5187 (`vite.config.ts`, `tauri.conf.json`) |
|
||||
| ARCHITECTURE.md | Ziel-Struktur `store/`, `sheets/`, `workers/geometry.worker.ts` (Comlink) | Tatsächlich `state/`, `panels/layoutModel.ts`; kein Worker/Comlink irgendwo im Projekt |
|
||||
| ARCHITECTURE.md | HLR „im Web Worker (Comlink)" | Kein Comlink im Projekt; Schnitt läuft synchron GPU-seitig in Rust |
|
||||
| HANDOVER.md | Neuester Block: „Stand 2026-07-09" | Jüngster Commit + PENDENZEN.md sind vom 17.–20.7. — 8+ Tage/mehrere Sessions veraltet |
|
||||
|
||||
### 4.6 Codequalität — besser als der Tempo vermuten lässt
|
||||
Trotz 3 Wochen / 362 Commits über viele autonome Sessions: **keine** FIXME/HACK/
|
||||
XXX-Marker im ganzen Projekt; nur 2 TODOs (beide bekannt/harmlos: Geländer in
|
||||
`Viewport3D.tsx:2649`, Kanten-Tiefe in `toSection.ts:1093`); `eslint-disable`
|
||||
fast ausschliesslich `react-hooks/exhaustive-deps` (bewusst); keine
|
||||
`_v2`/`_old`/`_backup`-Dateileichen. Die eigentliche Aufgabenliste lebt
|
||||
diszipliniert in PENDENZEN.md statt in Code-Kommentaren verstreut — gesünder
|
||||
als der Durchschnitt für dieses Bau-Tempo.
|
||||
|
||||
### 4.7 Genuine offene Punkte (Auszug aus PENDENZEN.md, Details dort)
|
||||
- Geo-Block: Layer-Zuordnung für importierte Gebäude/Terrain, reale Höhen +
|
||||
Projekt-müM-Draping, SWISSIMAGE-Draping.
|
||||
- 3D-Feinschliff: Fensterrahmen-Ecken bei „fein" überlappen (kein Gehrungs-
|
||||
Union), Dach-First/Grat hat unverschmolzene Dreiecke, unerklärte
|
||||
Vertikalstreifen auf oberen Wandflächen (undiagnostiziert).
|
||||
- truck-Boolean nicht an die Wand/Öffnungs-Pipeline angeschlossen (Scope-
|
||||
Entscheid mit Nutzer ausstehend).
|
||||
- Feld-Controller (Tab-Zyklus) fehlt für Body-Move und für 3D-Griffe generell
|
||||
(nur 2D-Einzelpunkt-Drag hat ihn); 3D-Griff-Drag hat gar kein Snapping.
|
||||
- `make2D`-Kommando (3D→flacher 2D-Plan mit Füllungen) ungebaut.
|
||||
- Ribbon-3D-Tab leer; einige Punkte visuell noch nicht in Tauri abgenommen
|
||||
(u. a. Materialfarben-Textur-Array, Schraffur-Schnittfüllung — laut
|
||||
PENDENZEN als „[~] implementiert, aber unverifiziert" markiert).
|
||||
- DWG/DXF-Domänen-Mapping (Entitäten → Wände/Öffnungen) unbegonnen; DWG-Schreiben
|
||||
fehlt (Lesen über `libredwg-web` vorhanden).
|
||||
- Teamwork/Kollaboration (Supabase) bewusst nicht begonnen, gilt als späte Phase.
|
||||
|
||||
## 5. Vergleich: Tag-1-Vision vs. heute
|
||||
|
||||
| Vision (28./29.6.) | Heute |
|
||||
|---|---|
|
||||
| Three.js als einziger 3D-Renderer, OpenCascade.js für Booleans/HLR | Eigene Rust/WASM-Engines („Nordstern") für 2D+3D; three.js nur noch „Free"-Fallback; OCCT-Pfad tot |
|
||||
| Ein `Element[]`-Union | Typisierte Arrays pro Bauteiltyp auf `Project` |
|
||||
| Zustand-Store | Eigener `useSyncExternalStore`-Store |
|
||||
| Web Worker + Comlink für HLR | Synchrone, analytische Rust-GPU-Schnitt-Pipeline |
|
||||
| „Booleans: Entscheidung in Phase 0" | Trucksolid/csgrs-CSG existiert, ist getestet, aber nicht an Wände angeschlossen |
|
||||
| web-ifc für IFC | Eigener IFC4-Writer (`exportIfc.ts`) |
|
||||
| Phase 2–5 grösstenteils „Backlog" | Treppen, Dächer, Stützen, SIA-416, Swisstopo, OSM, Kamera-Presets, Layouts, Ausschnitte, Terrain — alles bereits gebaut |
|
||||
|
||||
Kurz: die **Prinzipien** (ein Modell, viele Ableitungen; Darstellung erst beim
|
||||
Rendern; keine Cache-Stale-Bugs) haben gehalten und wurden korrekt umgesetzt.
|
||||
Die **konkreten Technologie-Entscheidungen** sind fast durchgängig anders
|
||||
gelaufen als geplant — meist zugunsten einer eigenen, schnelleren Rust/WASM-
|
||||
Lösung statt einer Drittbibliothek.
|
||||
@@ -0,0 +1,175 @@
|
||||
# ARCHITEKTUR-BRIEFING: Tauri + wgpu (Korrigiert)
|
||||
|
||||
**Stand:** 2026-07-01 — **KORREKTUR** (vorherige Dokumente waren unvollständig)
|
||||
|
||||
---
|
||||
|
||||
## Die Entscheidung (FINAL)
|
||||
|
||||
**Browser-CAD (alt) → Desktop Tauri-App mit Rust-Backend + wgpu-Rendering (neu)**
|
||||
|
||||
| Aspekt | Alt | Neu |
|
||||
|---|---|---|
|
||||
| **App-Form** | Browser-Tab | Desktop Tauri-Window |
|
||||
| **Frontend** | React/Vite (TS) | React/Vite (TS) — UNVERÄNDERT |
|
||||
| **2D-Rendering** | SVG-Plan | SVG-Plan — UNVERÄNDERT |
|
||||
| **3D-Rendering** | three.js/WebGL | **wgpu** (Vulkan/Metal/DX12) |
|
||||
| **Rechenintensive Ops** | TS in Browser | **Rust im Backend** |
|
||||
| **Betriebssystem** | cross-platform browser | Windows/macOS/Linux Desktop App |
|
||||
|
||||
---
|
||||
|
||||
## Warum dieser Umstieg?
|
||||
|
||||
**Problem mit three.js/WebGL:**
|
||||
- komplexe Möbel = 100k+ Polygone
|
||||
- mehrere parallele Ops (kernel2d, bool-Ops, DXF-Parser, SIA-Raumerkennung)
|
||||
- WebGL hat harte Limits (GPU VRAM, draw calls, single-threaded)
|
||||
- → Laggy, nicht skalierbar für Professional CAD
|
||||
|
||||
**Lösung: Tauri + wgpu + Rust**
|
||||
- **wgpu** = low-level GPU API (direkt zu Vulkan/Metal/DX12, nicht WebGL)
|
||||
- **Rust** = rechenintensive Ops parallelisiert + native performance
|
||||
- **Desktop** = native App, nicht Browser (bessere Kontrolle, bessere Perf)
|
||||
- **React-Frontend bleibt** = UI/Sketching/Panels unverändert (wgpu nur für 3D-Display)
|
||||
|
||||
---
|
||||
|
||||
## Der neue Stack (FINAL)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Desktop Tauri-Window │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ React/Vite (UI-Shell, State, Panels) │
|
||||
│ ├─ SVG 2D-Plan (Grundriss) │
|
||||
│ └─ wgpu 3D-Viewport (3D-Ansicht + Rendering) │
|
||||
│ │
|
||||
│ ↓ invoke (IPC) ↓ │
|
||||
│ │
|
||||
│ Rust-Backend (src-tauri/) │
|
||||
│ ├─ computeJoins() — Wand-Eckverbindungen │
|
||||
│ ├─ kernel2d() — Offset/Trim/Extend/… │
|
||||
│ ├─ parseShapeFromDwg() — Geometrie-Import │
|
||||
│ ├─ detectRooms() — SIA-Raumerkennung │
|
||||
│ └─ booleanOps() — Union/Differenz/Schnitt │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Nicht verändert:**
|
||||
- App.tsx, state/, commands/, panels/, model/types.ts
|
||||
- UI-Logik, Befehlssystem, Sketching-Tools
|
||||
|
||||
**Neu/geändert:**
|
||||
- `src-tauri/` — Rust-Crate für Backend
|
||||
- `src/compute/index.ts` — Boundary (invoke + TS-Fallback)
|
||||
- Rendering-Engine: **three.js → wgpu**
|
||||
- Build: `npm run tauri:build` statt `npm run build`
|
||||
|
||||
---
|
||||
|
||||
## Die Aufgabe (vier Agents)
|
||||
|
||||
**Siehe: `docs/design/tauri-migration-plan.md`**
|
||||
|
||||
Vier parallele Agents:
|
||||
|
||||
1. **Agent: Tauri-Shell Setup** → `src-tauri/` + Cargo.toml + main.rs (Tauri window registrieren)
|
||||
2. **Agent: Compute-Boundary (TS)** → `src/compute/index.ts` (invoke-Wrapper + TS-Fallbacks)
|
||||
3. **Agent: Rust compute_joins Impl** → `src-tauri/src/geometry.rs` (erste Op, Proof-of-Concept)
|
||||
4. **Agent: Vite/Package-Integration** → vite.config.ts + package.json (Tauri-Plugins, scripts)
|
||||
|
||||
**Deliverable:** lauffähige Desktop-App, wand-Edit triggert Rust-Op, Output identisch TS-Version.
|
||||
|
||||
---
|
||||
|
||||
## Dev-Workflow (post-Tauri)
|
||||
|
||||
```bash
|
||||
# Dev: Terminal 1 (Vite)
|
||||
npm run dev # localhost:5173
|
||||
|
||||
# Dev: Terminal 2 (Tauri)
|
||||
npm run tauri:dev # öffnet Desktop-Window, zeigt auf localhost:5173
|
||||
|
||||
# Build
|
||||
npm run tauri:build # → Windows .exe / macOS .app / Linux .deb
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rendering: three.js → wgpu (Wichtig!)
|
||||
|
||||
**3D-Viewport wird NEU implementiert in wgpu:**
|
||||
- Nicht: "drei.js mit Rust-Fallback"
|
||||
- **Ja:** wgpu als native GPU-Renderer (skaliert auf 100k+ Polygone)
|
||||
|
||||
**2D-Plan bleibt SVG** (keine Änderung).
|
||||
|
||||
**Timeline für wgpu-Impl:**
|
||||
- Milestone 1 (jetzt): Tauri-Shell + Rust-Compute-Ops
|
||||
- Milestone 2 (nächst): wgpu 3D-Viewport-Impl (ersetzt drei.js/WebGL)
|
||||
- Milestone 3: Live clipping/sectioning in wgpu
|
||||
|
||||
---
|
||||
|
||||
## Migration-Reihenfolge
|
||||
|
||||
**Phase 1 (Tauri-Shell + Proof-of-Concept):**
|
||||
- [ ] `computeJoins` (Wand-Ecken) → Rust
|
||||
|
||||
**Phase 2 (Rendering-Umbau):**
|
||||
- [ ] wgpu Viewport-Impl (ersetzt drei.js)
|
||||
- [ ] Instancing/LOD für Möbel-Geometrie
|
||||
|
||||
**Phase 3 (weitere Ops):**
|
||||
- [ ] `kernel2d` (Offset/Trim) → Rust
|
||||
- [ ] DXF/DWG-Parser → Rust
|
||||
- [ ] detectRooms → Rust
|
||||
- [ ] booleanOps → Rust
|
||||
|
||||
---
|
||||
|
||||
## Was sich NICHT ändert
|
||||
|
||||
- `src/App.tsx`, `src/state/`, `src/commands/`
|
||||
- UI-Panels, Zeichenwerkzeuge, Befehlssystem
|
||||
- Semantisches Modell (types.ts)
|
||||
- i18n, Styling
|
||||
|
||||
## Was ändert sich
|
||||
|
||||
- **Engine:** three.js/WebGL → wgpu
|
||||
- **Backend:** TS-Only → Tauri + Rust
|
||||
- **Distribution:** Browser → Desktop App
|
||||
- **Build-Prozess:** `npm run build` → `npm run tauri:build`
|
||||
|
||||
---
|
||||
|
||||
## Fehlerquellen (zur Klarheit)
|
||||
|
||||
❌ **FALSCH:** "Wir bleiben bei three.js"
|
||||
✅ **RICHTIG:** "three.js → wgpu, Rust-Compute + Desktop Tauri"
|
||||
|
||||
❌ **FALSCH:** "Tauri + three.js als Frontend-Engine"
|
||||
✅ **RICHTIG:** "Tauri-Shell + React UI + wgpu 3D-Rendering + Rust-Compute"
|
||||
|
||||
---
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
1. **Lesen:** `docs/design/tauri-migration-plan.md` (technisch, konkret)
|
||||
2. **Spawn:** vier Agents (parallel, unabhängig)
|
||||
3. **Verifizieren:** `npm run tauri:dev` → App läuft, erste Op in Rust funktioniert
|
||||
4. **Aktualisieren:** HANDOVER.md mit Milestone-1-Status
|
||||
|
||||
---
|
||||
|
||||
## Fragen?
|
||||
|
||||
- **"Warum Desktop statt Browser?"** → Skalierbarkeit (native GPU + Rust-Compute)
|
||||
- **"Warum wgpu statt drei.js?"** → wgpu skaliert auf 100k+ Polygone, three.js/WebGL hat Limits
|
||||
- **"Bleibt React?"** → Ja, React/Vite UI bleibt, nur 3D-Engine wechselt
|
||||
- **"Wann ist wgpu-Impl fertig?"** → Milestone 2 (nach Tauri-Shell)
|
||||
@@ -1,6 +1,13 @@
|
||||
# Dokumentation — Standalone Browser-BIM (cad)
|
||||
|
||||
> Stand: 2026-06-29 · Übergeordnet: [ROADMAP.md](../ROADMAP.md) (Vision & Phasen) ·
|
||||
> Stand: 2026-06-29 · **Historische Recherche-/Design-Dokumente aus der ersten
|
||||
> Woche.** Mehrere Kern-Empfehlungen hier (replicad/OCCT als B-Rep-Kernel,
|
||||
> Manifold, web-ifc, `three/webgpu`) wurden im tatsächlichen Bau **nicht**
|
||||
> umgesetzt — stattdessen entstanden eigene Rust/WASM-Rendering-Engines
|
||||
> („Nordstern"). Für den aktuellen Ist-Zustand: **[../STATUS.md](../STATUS.md)**
|
||||
> und **[../ARCHITECTURE.md](../ARCHITECTURE.md)**. Die Dokumente unten sind als
|
||||
> Recherche-Hintergrund weiterhin lesenswert, aber nicht mehr aktueller Plan.
|
||||
> Übergeordnet: [ROADMAP.md](../ROADMAP.md) (Vision & Phasen, ebenfalls historisch) ·
|
||||
> [CONVENTIONS.md](../CONVENTIONS.md) (Konventionen) · [ARCHITECTURE.md](../ARCHITECTURE.md).
|
||||
|
||||
Dieses Verzeichnis bündelt die Recherche- und Design-Dokumente für `cad`, die
|
||||
@@ -48,6 +55,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,623 @@
|
||||
# BIM-Elementtiefe — Tür, Fenster, Dach, Decke ("wirklich 1:1")
|
||||
|
||||
## 0. Zweck und Abgrenzung
|
||||
|
||||
DOSSIER modelliert Bauteile heute semantisch (kein reines Zeichenprogramm) und
|
||||
hat für Wand/Decke bereits eine mehrschichtige Aufbaulogik (`Component[]` via
|
||||
`WallType`/`CeilingType`). Türen und Fenster haben seit
|
||||
`docs/design/window-editor-vectorworks-study.md` einen dedizierten,
|
||||
phasierten Ausbauplan (Flügeltabelle, Verglasung, Sonnenschutz). Was fehlt,
|
||||
ist die gleiche Tiefe für **Dach** (keine Schichtlogik, keine Mansard-
|
||||
Untertypen, kein Kehl-/Gaubenmodell) und für die **Decken-Randterminierung**
|
||||
(Deckenrand als reines Polygon ohne Kantendetail).
|
||||
|
||||
Dieses Dokument nimmt die vier Bauteile Tür, Fenster, Dach, Decke und hält
|
||||
sie gegen die reale Tiefe vollständiger BIM-Programme (Vectorworks Architektur,
|
||||
ArchiCAD, Revit, Allplan). Ziel ist NICHT, jedes Feature dieser Programme zu
|
||||
kopieren, sondern zu benennen, welche Lücken einen echten "1:1-Zuwachs" für
|
||||
ein Einfamilienhaus-/Wohnbau-Tool wie DOSSIER bringen — und welche reiner
|
||||
Ballast wären (Abschnitt 6).
|
||||
|
||||
Für Fenster/Tür dupliziert dieses Dokument NICHT die Mapping-Tabelle aus
|
||||
`window-editor-vectorworks-study.md` — es referenziert sie und ergänzt, was
|
||||
dort fehlt (v. a. Tür-Tiefe, die die Studie nur am Rand behandelt, und die
|
||||
architektonischen Grenzen des heutigen Öffnungsmodells). Für Wand-Schichtlogik
|
||||
siehe `docs/design/parametric-walls.md` (Raster/Modul-Regeln, nicht
|
||||
Gegenstand hier) und `docs/design/elements.md` (ältere Gesamtplanung, Stand
|
||||
vor der aktuellen `Component`/`Layer`-Implementierung — dort abweichende
|
||||
Typnamen wie `Slab`/`ProfileDef`, hier durchgängig der IST-Code zitiert).
|
||||
|
||||
Alle IST-Aussagen sind mit Datei:Zeile belegt (verifiziert per Lesen des
|
||||
Codes, Stand dieses Commits). Status-Legende der Lücken-Tabellen: **✓**
|
||||
vorhanden und gerendert · **~** Feld existiert, Renderer liest es nicht oder
|
||||
nur grob · **✗** fehlt vollständig. Priorität P0 (grösster 1:1-Zuwachs, bald)
|
||||
… P3 (Ballast, nur auf Nachfrage). Aufwand grob in Personentagen (PT).
|
||||
|
||||
---
|
||||
|
||||
## 1. Gemeinsames Fundament: das Schicht-Muster
|
||||
|
||||
Der zentrale Baustein, den DOSSIER bereits hat und der sich wiederverwenden
|
||||
lässt, ist `Component`/`Layer`:
|
||||
|
||||
- `Component` (`src/model/types.ts:196`) — ein Bauteil-Material: Poché-Farbe
|
||||
(`color`/`foreground`/`background`), Schnitt-Schraffur (`hatchId`) UND
|
||||
Ansichts-Schraffur (`viewHatchId`, für unaufgeschnittene Aufsicht),
|
||||
optionales PBR-Material (`material`), Kürzel (`abbrev`) und ein
|
||||
Verschneidungs-Rang (`joinPriority`, `types.ts:241`) für die Boolean-
|
||||
Dominanz am Stoss.
|
||||
- `Layer` (`types.ts:245`) — eine Schicht: `componentId` + `thickness` +
|
||||
optionaler Fugen-Linienstil (`jointLineStyleId`).
|
||||
- `WallType` (`types.ts:260`) und `CeilingType` (`types.ts:273`) sind BEIDE
|
||||
nur `{ id, name, layers: Layer[] }` — bewusst derselbe Typ, "das
|
||||
horizontale Gegenstück zum WallType" (Kommentar `types.ts:266-271`).
|
||||
|
||||
Das Muster ist also bereits zweimal (Wand, Decke) verifiziert:
|
||||
`ceilingThickness()` (`types.ts:2104`) summiert die Layer-Dicken,
|
||||
`emitSlabs()` (`src/plan/toWalls3d.ts:1352-1419`) stapelt sie im 3D als
|
||||
einzelne `RSlab`-Scheiben proportional in `[zBottom, zTop]`, `addCeilingPoche`
|
||||
(`src/plan/generatePlan.ts:2453`) zeichnet die Aufsicht mit der
|
||||
Ansichts-Schraffur der ERSTEN Schicht, und `toSection.ts:633`
|
||||
(`splitSlabLayers`) zerlegt die Decke im ECHTEN Schnitt in
|
||||
Einzel-Bänder je Schicht (mit `resolveCeilingSectionStyle`,
|
||||
`generatePlan.ts:591`, als Schraffur-/Farbquelle).
|
||||
|
||||
**Der Dach-Vorschlag in Abschnitt 4 ist im Kern: dasselbe Muster ein drittes
|
||||
Mal anwenden.** Das ist der günstigste Weg zu echter Dach-1:1-Tiefe, weil
|
||||
Layer-Resolver, Schraffur-Ketten (`resolveHatch`/`resolveForeground` etc.,
|
||||
`generatePlan.ts:409-536`) und die 3D-Stapel-Logik bereits bestehen und nur
|
||||
auf einen neuen Aufbau-Typ angewendet werden müssen statt neu erfunden.
|
||||
|
||||
Architektonische Randbemerkung: `Project` führt bereits `wallTypes`,
|
||||
`ceilingTypes?`, `doorTypes?`, `windowTypes?`, `stairTypes?`
|
||||
(`types.ts:1936-1958`) als eigene Bibliotheken — aber **kein `roofTypes?`**.
|
||||
`Roof` (`types.ts:1022`) trägt nur ein einzelnes `thickness: number`
|
||||
(`types.ts:1044`), keinen Aufbau-Verweis. Das ist die strukturelle Lücke,
|
||||
die Abschnitt 4 schliesst.
|
||||
|
||||
---
|
||||
|
||||
## 2. Tür (Door)
|
||||
|
||||
### 2.1 Was ein vollständiges BIM-Tool bietet
|
||||
|
||||
| Bereich | Typische Parameter (VW/ArchiCAD/Revit/Allplan) |
|
||||
|---|---|
|
||||
| Bauart | Dreh-, Schiebe- (auf/vor Wand), Falt-, Pendel-, Karusselltür, reiner Durchbruch |
|
||||
| Blattzahl/-teilung | 1-/2-flügelig, Gangflügel + Standflügel (unterschiedliche Breite), Seitenteile links/rechts |
|
||||
| Blattausführung | glatt, kassettiert, Glasfüllung (Anteil/Sprossenbild), Brandschutz-/Schallschutz-Kennwert |
|
||||
| Rahmen/Zarge | Zarge vs. Blockrahmen, Rahmenbreite je Kante, Zargentiefe, Bekleidung/Abdeckleiste, Falz |
|
||||
| Schwelle | ohne, Alu-Flachschwelle, Anschlagdichtung, Bodenanschluss/Gefälle bei Aussentüren |
|
||||
| Sturz/Oberlicht | festverglastes Oberlicht mit eigenem Rahmen, Kämpfer, Sprossenbild |
|
||||
| Seitenteile | fest verglaste Seitenteile links/rechts, eigene Breite |
|
||||
| Form | rechteckig, Rundbogen, Segmentbogen, Stichbogen — bei Aussen-/Haustüren verbreitet |
|
||||
| Beschlag | Drücker/Knauf-Typ, Schild, Schliesszylinder, Bänder sichtbar/verdeckt |
|
||||
| 2D-Darstellung | Blatt + Schwenkbogen (Grundriss), eigene Ansichtssymbolik in Schnitt/Elevation, Sturzlinien |
|
||||
| Material/Schichten | Blatt-Kernaufbau (bei Brand-/Schallschutztüren mehrschichtig, analog Wand) |
|
||||
| IFC-Rolle | `IfcDoor` mit `IfcDoorType` (PredefinedType), `OverallWidth/Height`, `IfcDoorPanelProperties`, Void in der Wirtswand |
|
||||
|
||||
### 2.2 IST in DOSSIER
|
||||
|
||||
`Opening` mit `kind: "door"` (`src/model/types.ts:1068`) referenziert
|
||||
optional einen `DoorType` (`types.ts:296`). Vorhanden am Typ: `kind`
|
||||
("dreh"/"schiebe"/"wandoeffnung", :303), `leafCount` (1|2, :305), `leafStyle`
|
||||
("glatt"/"kassette"/"glas", :307), `glazingRatio` (:309), `frameThickness`/
|
||||
`frameDepth`/`frameKind`("zarge"/"blockrahmen")/`frameWidth`
|
||||
(:311-328), `insetFromFace`/`insetFace` (:335-337), `transomHeight` (:343),
|
||||
`threshold` (:349). Am Element selbst: `swing`/`hinge`/`swingAngle`/
|
||||
`openingDir` (:1104-1108), `lintelLines` (Sturzlinien, :1119) und
|
||||
`doorType: "normal"|"wandoeffnung"` (:1100) — ein zweites, mit `DoorType.kind`
|
||||
teilweise redundantes Feld.
|
||||
|
||||
Renderer-Konsum, real geprüft:
|
||||
|
||||
- **2D-Blatt ist IMMER einflügelig.** `addOpeningSymbol` (Tür-Zweig,
|
||||
`src/plan/generatePlan.ts:2091-2213`) zeichnet genau EINE Blattlinie
|
||||
(`sym.hinge → sym.openEnd`) und EINEN Schwenkbogen — `DoorType.leafCount`
|
||||
wird an keiner Stelle in `generatePlan.ts` gelesen (kein Treffer für
|
||||
`leafCount` im ganzen Plan-Renderer). Eine zweiflügelige Tür sieht im Plan
|
||||
aus wie eine einflügelige.
|
||||
- **3D genauso**: `resolveOpeningFrame` (`src/plan/toWalls3d.ts:1861-1886`)
|
||||
setzt für Türen hart `wingCount: 1`, unabhängig von `leafCount`.
|
||||
- `leafStyle` wirkt NUR binär: `"glas"` schaltet eine volle Verglasung frei
|
||||
(`glazed: dt.leafStyle === "glas"`, `toWalls3d.ts:1883`) — `glazingRatio`
|
||||
(Teilverglasung, z. B. 60 % Glasanteil im oberen Blattbereich) wird an
|
||||
keiner Stelle gelesen. "kassette" (Kassettentür) hat keine eigene Geometrie,
|
||||
fällt auf dieselbe Quader-Darstellung wie "glatt" zurück.
|
||||
`frameKind: "blockrahmen"` wirkt NUR im 2D-Rahmenband
|
||||
(`generatePlan.ts:1996`, `isBlock`), im 3D gibt es keinen Unterschied zur
|
||||
Zarge (`frameMeshesForOpening`, `toWalls3d.ts:1936`, kennt kein
|
||||
`frameKind`).
|
||||
- `threshold` steuert `hasSill` im 3D-Rahmen (`toWalls3d.ts:1880`), was einen
|
||||
einfachen Schwellen-Riegel zeichnet — kein eigenes Schwellenprofil,
|
||||
keine Gefälle-/Dichtungsdarstellung.
|
||||
- Es gibt eine ZWEITE, ältere Tür-Repräsentation: `Door`
|
||||
(`types.ts:1333`, `project.doors: Door[]`) mit eigenem Symbol-Renderer
|
||||
`addDoorSymbol` (`generatePlan.ts:1840-1899`) — strukturell identisch zum
|
||||
`Opening`-Pfad, aber ohne jeden Typ-Bezug. Zwei parallele Datenwege für
|
||||
dieselbe Bauteilart sind selbst technische Schuld, nicht nur ein
|
||||
BIM-Feature-Gap.
|
||||
- Form (Rundbogen etc.) existiert nicht: `Opening.width`/`height` sind ein
|
||||
reines Rechteck, `wallGaps`/`buildWallFootprints`
|
||||
(`generatePlan.ts:1363-1457`) schneiden nur rechteckige Bänder aus der
|
||||
Wand-Poché.
|
||||
- IFC-Export: `IfcDoor` wird erzeugt (`src/export/exportIfc.ts:30-31`), aber
|
||||
als reine Box-Geometrie ohne `IfcDoorType`/`PredefinedType` und ohne
|
||||
`IfcOpeningElement`-Void (bewusste Design-Entscheidung, siehe Kommentar
|
||||
`exportIfc.ts:18-31`: das Loch steckt bereits im geschnittenen Wand-Mesh).
|
||||
|
||||
### 2.3 Lücken (Tür)
|
||||
|
||||
| Feature | Status | Priorität | Aufwand |
|
||||
|---|---|---|---|
|
||||
| Zweiflügelige Tür rendert 2 Blätter (2D+3D) | ✗ (`leafCount` ungelesen) | **P0** | 2 PT |
|
||||
| Teilverglasung nach `glazingRatio` (2D-Linie + 3D-Split) | ✗ | P1 | 1.5 PT |
|
||||
| Kassettentür eigene Blattgeometrie (Füllungsfelder) | ✗ | P2 | 2 PT |
|
||||
| `frameKind` (Blockrahmen) auch im 3D wirksam | ~ | P1 | 1 PT |
|
||||
| Seitenteile (feste Verglasung links/rechts der Tür) | ✗ | P1 | 2 PT |
|
||||
| Rundbogen-/Segmentbogen-Türform | ✗ | P2 | 4 PT (braucht gekrümmten Wandausschnitt, s. §5.3) |
|
||||
| Schwellenprofil (Alu-Flachschwelle, Dichtung) statt Riegel | ✗ | P2 | 1 PT |
|
||||
| `Door`/`Opening`-Doppelpfad konsolidieren | technische Schuld | P1 | 3 PT (Migration) |
|
||||
| Beschlag (Drücker/Knauf) als Mesh + 2D-Symbol | ✗ | P2 | 1.5 PT |
|
||||
| IFC `IfcDoorType`/PredefinedType/OverallWidth-Height-Properties | ~ | P2 | 1 PT |
|
||||
|
||||
### 2.4 Umsetzungsvorschlag
|
||||
|
||||
**Modell**: `DoorType.leafs?: { width: number; hingeSide: "left"|"right";
|
||||
fixed?: boolean }[]` analog `WindowType.sashes` (`SashDef`, `types.ts:367`) —
|
||||
bewusst dieselbe Struktur, damit `resolveSashSpans` (`toWalls3d.ts:2019`) UND
|
||||
die 2D-Pfostenlinien-Logik direkt wiederverwendet werden können, statt eine
|
||||
Tür-eigene Variante zu bauen. Ein `fixed: true`-Leaf ist das Seitenteil.
|
||||
`glazingRatio` wandert vom Skalar zu einer klaren Geometrie: Kämpferhöhe
|
||||
innerhalb des Blatts, gerendert wie das bestehende Oberlicht
|
||||
(`transomHeight`), nur INNERHALB des Blattrahmens statt darüber.
|
||||
|
||||
**2D**: `addOpeningSymbol` (Tür-Zweig) über die Leaf-Liste iterieren statt
|
||||
einer festen Blattlinie; pro Leaf ein eigenes `hinge`/`swing` (Default:
|
||||
alternierend wie bei Fenstern, `sashesOfWindowType`, `types.ts:2132`).
|
||||
|
||||
**3D**: `resolveOpeningFrame` liefert `wingCount = leafs.length` statt hart 1;
|
||||
`frameMeshesForOpening` (bereits generisch über `params.sashes`) übernimmt
|
||||
die Mehrflügel-Darstellung ohne Änderung — das ist der Vorteil der
|
||||
Struktur-Wiederverwendung.
|
||||
|
||||
**UI**: Im Tür-Editor (sofern nach dem Muster von
|
||||
`window-editor-vectorworks-study.md` gebaut) eine Flügeltabelle wie bei
|
||||
Fenstern, nur mit Tür-Vokabular (Gangflügel/Standflügel statt Flügel 1/2).
|
||||
|
||||
---
|
||||
|
||||
## 3. Fenster (Window)
|
||||
|
||||
Für Fenster existiert bereits eine vollständige Referenzmatrix in
|
||||
`docs/design/window-editor-vectorworks-study.md` §2 (Basiseinstellungen,
|
||||
Grösse, Brüstung, Rahmen, Flügeltabelle, Laibung/Form/Ober-Unterlicht,
|
||||
Sonnenschutz/Beschlag, Attribute/IFC) mit eigener P0–P3-Phasierung. Diese
|
||||
Studie ist der massgebliche Bezugspunkt; hier nur die Delta-Punkte, die dort
|
||||
fehlen oder seither vom IST abweichen.
|
||||
|
||||
### 3.1 IST-Ergänzung (was die Studie nicht/knapp behandelt)
|
||||
|
||||
- `WindowType.glazing` (einfach/zweifach/dreifach) ist bis heute NICHT
|
||||
renderwirksam — bestätigt weiterhin: `glazingPanesOf()` (`types.ts:2149`)
|
||||
wird von `glassPanesForOpening` in `toWalls3d.ts` konsumiert, aber die
|
||||
Studie selbst vermerkt (Zeile 61-65 dort), dass der 2D-Pfad die
|
||||
Scheibenzahl weiterhin allein aus `DetailLevel` ableitet, nicht aus
|
||||
`glazingPanes`. Das ist über ein Jahr nach der Studie noch offen — ein
|
||||
Hinweis, dass P0/P1-Posten aus Fenster-Studien real liegen bleiben, wenn
|
||||
niemand sie explizit nachzieht.
|
||||
- `sillBoard` (Fensterbank keine/innen/aussen/beide, `types.ts:447`) ist
|
||||
weiterhin ungenutzt (kein Treffer für `sillBoard` in `toWalls3d.ts` oder
|
||||
`generatePlan.ts` ausserhalb der Typ-Definition und des Editors) —
|
||||
entspricht dem in der Studie als P1 markierten "Fensterbank erstellen".
|
||||
|
||||
### 3.2 Architektonische Grenze: gekrümmte Öffnungen
|
||||
|
||||
Sowohl die Fenster-Studie (§2.6, "Form Eckig/Schräg/Spitz/Rund", P2, 4 PT)
|
||||
als auch dieser Auftrag nennen Rundbogen-/Spitzbogenfenster. Das ist teurer,
|
||||
als die Aufwandschätzung suggeriert, weil das gesamte Öffnungsmodell auf
|
||||
GERADEN Bändern basiert: `buildWallFootprints`/`wallGaps`
|
||||
(`generatePlan.ts:1363-1457`) schneiden ein rechteckiges Intervall `[from,to]`
|
||||
entlang der Wandachse aus der Poché, `openingAxisBox`
|
||||
(`toWalls3d.ts:1739`) baut im 3D ebenso einen achsparallelen Quader. Eine
|
||||
Bogenform braucht entweder (a) eine gekrümmte Zusatzkontur, die die
|
||||
Rechteck-Aussparung oben kappt (2D: zusätzliche Polygon-Boolean gegen die
|
||||
Poché; 3D: gekrümmte Deckfläche statt ebenem Sturz) oder (b) ein komplett
|
||||
neues, polygonbasiertes Öffnungsmodell. Vorschlag: (a) zuerst — ein
|
||||
`headShape: "eckig"|"segment"|"spitz"|"rund"` mit Zusatzparametern
|
||||
(Stichhöhe/Radius), das NUR die obere Kante der bestehenden Rechteck-Öffnung
|
||||
ersetzt, während Pfosten/Sohlbank rechteckig bleiben. Deckt die reale
|
||||
Mehrheit der Fälle (Haustür mit Rundbogen, Dachflächenfenster-Giebel) ohne
|
||||
das Kernmodell umzubauen.
|
||||
|
||||
### 3.3 Lücken (Fenster, Delta zur Studie)
|
||||
|
||||
| Feature | Status | Priorität | Aufwand |
|
||||
|---|---|---|---|
|
||||
| `glazing`/`glazingPanes` 2D-wirksam (Scheibenzahl statt nur DetailLevel) | ~ (weiter offen seit Studie) | **P0** | 1 PT |
|
||||
| `sillBoard` gerendert (2D-Kontur + 3D-Box) | ✗ | P1 | 2 PT |
|
||||
| Kopfform (Rundbogen/Spitzbogen/Schräge) über Zusatzkontur | ✗ | P2 | 5 PT (s. §3.2) |
|
||||
| Echtes Sprossengitter (Glasteilung UNABHÄNGIG von Flügelrahmen) | ✗ | P1 | 2 PT |
|
||||
| Alle übrigen Fenster-Lücken | siehe window-editor-vectorworks-study.md §2/§4 | — | — |
|
||||
|
||||
---
|
||||
|
||||
## 4. Dach (Roof)
|
||||
|
||||
### 4.1 Was ein vollständiges BIM-Tool bietet
|
||||
|
||||
| Bereich | Typische Parameter |
|
||||
|---|---|
|
||||
| Grundform | Pult, Sattel, Walm, Krüppelwalm, Zeltdach, Mansarde (mit Untertyp Giebel-/Walm-/Zeltmansarde), Flach/Terrassendach, Sheddach, Tonnendach, freie Neigungsflächen je Kante |
|
||||
| Grundriss | beliebiges Polygon (nicht nur Rechteck), automatische Kehlen/Grate über Straight-Skeleton bei L-/T-/U-Grundrissen |
|
||||
| Neigung | je Dachfläche einzeln editierbar, unterschiedliche Neigungen je Seite |
|
||||
| Schichtaufbau | Eindeckung (Ziegel/Blech/Bitumen), Lattung, Konterlattung, Unterdach/-spannbahn, Sparren/Dämmung zwischen Sparren, Dampfbremse, Innenverkleidung — analog Wandaufbau, mit Deckenanschluss |
|
||||
| Überstand | Traufe und Ortgang UNABHÄNGIG editierbar (Betrag + Ausbildung), Aufschiebling (Neigungsknick am Traufende), Ortganddetail (Windbrett, Blech) |
|
||||
| Kniestock/Drempel | vertikale Wandaufkantung zwischen Deckenoberkante und Dach-Traufpunkt, definiert First-/Trauf-Geometrie mit |
|
||||
| Öffnungen im Dach | Dachflächenfenster (schräg, in der Dachebene), Dachgauben (Schlepp-, Sattel-, Walm-, Spitzgaube, Fledermausgaube) als eigenständige Sekundärdächer mit eigenem First |
|
||||
| First/Grat/Kehl/Ortgang | eigene Linientypen in 2D-Ansicht (Dachaufsicht) UND im Schnitt sichtbar (Sparrenlage, Dämmstärke) |
|
||||
| Material/Poché | Dachfläche in der Aufsicht mit Eindeckungs-Symbol/-Schraffur (Ziegel-Textur o. Ä.), im Schnitt Vollschichten wie eine geneigte Wand |
|
||||
| IFC-Rolle | `IfcRoof` (aggregiert `IfcRoofType`), Dachflächen selbst oft als `IfcSlab`-artige Elemente mit `IfcMaterialLayerSetUsage`, Gauben als eigene `IfcRoof`/`IfcBuildingElementProxy`-Unterobjekte |
|
||||
|
||||
### 4.2 IST in DOSSIER
|
||||
|
||||
`Roof` (`src/model/types.ts:1022-1047`) rechnet ausschliesslich auf der
|
||||
**Bounding-Box** des Umrisses (Kommentar `types.ts:1016-1020`: "First entlang
|
||||
einer Hauptachse ... die gängige, intuitive Vereinfachung"). Geometrie kommt
|
||||
aus `roofGeometry()`/`computeCanonical()` (`src/geometry/roof.ts:80-267`),
|
||||
explizit **"ohne Straight-Skeleton"** (Kommentar `roof.ts:5`). Unterstützte
|
||||
`RoofShape` (`types.ts:1013`): flach/pult/sattel/walm/mansarde/zelt — je EINE
|
||||
feste Berechnung, keine Untertypen. Mansarde hat eine feste, nicht editierbare
|
||||
Knick-Geometrie (`d1 = halfD * 0.4`, `roof.ts:181` — 40 % der Tiefe von der
|
||||
Traufe, hart codiert, keine Möglichkeit den Umbruchpunkt zu verschieben).
|
||||
|
||||
Konkrete, verifizierte Lücken:
|
||||
|
||||
- **Kein Schichtaufbau.** `Roof.thickness` (`types.ts:1044`) ist ein
|
||||
einzelner Skalar. Er wird an KEINER Stelle im Code gelesen (`grep
|
||||
"roof.thickness"` über `toWalls3d.ts`/`generatePlan.ts`/`roof.ts` liefert
|
||||
null Treffer) — das Feld existiert im Typ, ist aber komplett tot. Die
|
||||
3D-Dachfläche ist eine unendlich dünne, einfarbige Fläche
|
||||
(`emitRoofs()`, `src/plan/toWalls3d.ts:1670-1684`: nur `plane.pts`/
|
||||
`gable`-Fans, EINE Farbe `ROOF_RGB` bzw. `roof.color`, keine Dicke, keine
|
||||
Materialschichten).
|
||||
- **Kein `RoofType`.** `Project` hat `wallTypes`, `ceilingTypes?`,
|
||||
`doorTypes?`, `windowTypes?`, `stairTypes?` (`types.ts:1936-1958`), aber
|
||||
kein `roofTypes?`. Es gibt keinen Bauteil-Bibliothekseintrag für Dächer.
|
||||
- **2D-Grundriss zeigt keine Poché.** `addRoof()`
|
||||
(`src/plan/generatePlan.ts:2549-2605`) zeichnet AUSSCHLIESSLICH Linien
|
||||
(Traufe/First/Grat/Knick, je feste Strichstärke `ROOF_EAVES_MM`/
|
||||
`ROOF_RIDGE_MM`/`ROOF_HIP_MM`) — keine Fläche, keine Schraffur, keine
|
||||
Materialkennzeichnung. Deckungsgleich mit Wand/Decke, die BEIDE eine
|
||||
Poché-Füllung mit Schraffur haben (`addWallPoche`, `addCeilingPoche`),
|
||||
bleibt das Dach in der Aufsicht ein reines Liniendiagramm.
|
||||
- **Kein Schnitt.** `src/plan/toSection.ts` hat KEINE Roof-Behandlung (kein
|
||||
Treffer für `roof`/`Roof` im gesamten Datei-Grep). Ein Vertikalschnitt
|
||||
durch ein Gebäude mit Satteldach zeigt heute keine Dachlinie, keine
|
||||
Sparrenlage, keine Firstprojektion — ein Kernstück der BIM-1:1-Erwartung
|
||||
fehlt vollständig.
|
||||
- **Kein IFC.** `exportIfc.ts` erzeugt `IfcWall`, `IfcSlab`, `IfcDoor`/
|
||||
`IfcWindow`, `IfcStair`, `IfcBuildingElementProxy` (Kommentar
|
||||
`exportIfc.ts:18-36`) — `Roof`/`IfcRoof` ist in der Abbildungsliste NICHT
|
||||
aufgeführt und wird beim Export komplett übersprungen.
|
||||
- **Kein Straight-Skeleton, kein L-Grundriss mit Kehle.** Ein L-förmiger
|
||||
Baukörper mit durchgehendem Satteldach (Kehle an der Innenecke) lässt sich
|
||||
nicht abbilden — die BBox-Rechnung würde ein Rechteck über die ganze
|
||||
L-Ausdehnung legen.
|
||||
- **Keine Gauben, keine Dachfenster.** Kein Treffer für Gaube/Dormer im
|
||||
gesamten `src`-Baum (verifiziert per Suche).
|
||||
- **Kein Kniestock als Bauteilbeziehung.** `baseElevation` (`types.ts:1042`)
|
||||
erlaubt zwar, die Traufhöhe manuell über die Geschoss-Oberkante zu heben
|
||||
(ein Kommentar in `ObjectInfoPanel.tsx:549` erwähnt das explizit als
|
||||
Nutzungsmuster), aber es gibt keine Wand-Dach-Kopplung, die einen
|
||||
Drempel/Kniestock als eigenes, vermasstes Bauteil führt — der Nutzer muss
|
||||
die Zahl manuell abstimmen.
|
||||
- **Traufe/Ortgang nicht unabhängig.** `overhang` (`types.ts:1038`) ist EIN
|
||||
Wert "ringsum" — Traufüberstand und Ortgangüberstand (oft unterschiedlich,
|
||||
z. B. 0.5 m Traufe / 0.3 m Ortgang) sind nicht trennbar.
|
||||
- **UI**: `ObjectInfoPanel.tsx:479-561` bietet volle Instanz-Bearbeitung
|
||||
(shape/ridgeAxis/pitch/pitchUpper/width/depth/overhang/thickness/
|
||||
baseElevation), aber keine Typ-/Stil-Verwaltung (kein `ResourceManager`-
|
||||
Eintrag für Dächer, anders als Wand/Decke/Tür/Fenster/Treppe).
|
||||
|
||||
### 4.3 Dach-Schichtlogik — konkreter Vorschlag (Kernthema dieses Dokuments)
|
||||
|
||||
Der Vorschlag überträgt exakt das `WallType`/`CeilingType`-Muster:
|
||||
|
||||
```
|
||||
export interface RoofType {
|
||||
id: string;
|
||||
name: string;
|
||||
/** Aussen (Eindeckung) → innen (Verkleidung), analog WallType.layers. */
|
||||
layers: Layer[];
|
||||
}
|
||||
```
|
||||
|
||||
Kein neuer Layer-Typ nötig — `Layer` (`types.ts:245`) ist bereits
|
||||
"Bauteil + Dicke + optionaler Fugen-Linienstil", unabhängig davon ob sie
|
||||
horizontal (Decke), vertikal (Wand) oder GENEIGT (Dach) gestapelt wird, weil
|
||||
die Stapel-Richtung beim jeweiligen Renderer entschieden wird, nicht im
|
||||
Datentyp. Typische Schichtfolge eines Steildachs (Eindeckung → Konterlattung
|
||||
→ Lattung → Unterdach/Unterdeckbahn → Sparren+Dämmung → Dampfbremse →
|
||||
Innenverkleidung/GKB) bildet sich 1:1 auf `Layer[]` ab, jede Schicht bekommt
|
||||
ein `Component` mit eigener Schraffur/Farbe/Material wie bei Wand/Decke.
|
||||
|
||||
`Roof.thickness: number` wird zu `Roof.roofTypeId?: string` (Verweis, analog
|
||||
`Ceiling.ceilingTypeId`) mit optionaler `thicknessOverride?: number` — exakt
|
||||
das Muster aus `Ceiling.thickness?` (`types.ts:952-953`, "Optionale
|
||||
Übersteuerung der Gesamtdicke ... sonst Typ-Dicke"). `Project.roofTypes?:
|
||||
RoofType[]` ergänzt die Bibliothek.
|
||||
|
||||
**3D-Konsum**: `emitRoofs()` (`toWalls3d.ts:1670`) baut heute EINE
|
||||
Dreiecksfläche je Dachfläche+Giebel. Mit Layern wird daraus — analog
|
||||
`emitSlabs()` (`toWalls3d.ts:1352-1419`, das bereits genau diese
|
||||
Proportional-Stapel-Logik für Decken hat) — ein Stapel PARALLEL versetzter
|
||||
Flächen entlang der Flächennormalen (nicht entlang Z wie bei der Decke,
|
||||
sondern entlang der Dachflächen-Normalen `n`, siehe `RoofGeometry.planes`
|
||||
in `roof.ts:19-21`). Jede Schicht wird zum eigenen `RMesh` mit eigener Farbe/
|
||||
Schraffur-Metadaten (`RCutMeta`, wie bei `emitSlabs`). Der Versatz macht
|
||||
zugleich die Dachdicke sichtbar (heute unendlich dünn) — ein Nebengewinn ohne
|
||||
Mehraufwand.
|
||||
|
||||
**2D-Konsum (Grundriss/Aufsicht)**: `addRoof()` bekommt eine Poché-Fläche
|
||||
analog `addCeilingPoche` — Füllung mit der `viewHatchId` der obersten Schicht
|
||||
(Eindeckungssymbol), gerahmt von den bestehenden Traufe/First/Grat/Knick-
|
||||
Linien (die bleiben unverändert, sie sind flächen-unabhängig).
|
||||
|
||||
**2D-Konsum (Schnitt, der grössere Umbau)**: `toSection.ts` braucht einen
|
||||
neuen Roof-Zweig. Ansatz: die Dachebene mit der Schnittebene schneiden
|
||||
(Ebene-Ebene-Schnitt, da `RoofPlane` eben ist), daraus ein Liniensegment je
|
||||
betroffener Dachfläche gewinnen, dann `Layer[]` senkrecht ZUR
|
||||
Dachneigung als Bandsequenz auftragen (wie `resolveWallBands`,
|
||||
`toWalls3d.ts:521`, aber gedreht um den Neigungswinkel `pitchDeg`) und mit
|
||||
`splitSlabLayers`-Logik (`toSection.ts:633`) füllen. Das ist der aufwendigste
|
||||
Einzelposten dieses Dokuments (siehe Prioritätsliste), aber ohne ihn bleibt
|
||||
"Dach im Schnitt" eine reine Lücke.
|
||||
|
||||
**UI**: Ein `RoofType`-Eintrag im `ResourceManager`
|
||||
(`src/ui/ResourceManager.tsx`), identisch zum bestehenden Ceiling-Typ-Editor
|
||||
(Schicht-Liste, Dicke, Bauteil-Zuweisung) — kein neues UI-Paradigma.
|
||||
|
||||
### 4.4 Dach 1:1 — Formen, Kehlen, Gauben (konkreter Vorschlag)
|
||||
|
||||
Reihenfolge nach Aufwand/Nutzen, NICHT alles auf einmal:
|
||||
|
||||
1. **Traufe/Ortgang trennen**: `overhang` → `{ eaves: number; gable: number
|
||||
}`. Kleine, lokale Änderung in `roof.ts` (zwei statt einer Offset-Variable
|
||||
je nach Kantentyp), grosser optischer Gewinn (das ist die häufigste
|
||||
Rückmeldung "sieht nicht echt aus" bei Steildächern mit gleich langem
|
||||
Überstand allseitig).
|
||||
2. **Mansard-Untertyp** (`mansardVariant: "walm"|"giebel"|"zelt"`, wie in der
|
||||
älteren Planung `elements.md:298` bereits vorgesehen, aber nie gebaut):
|
||||
steuert nur, wie die STIRNSEITE der Mansarde behandelt wird (heute IMMER
|
||||
`gables` = vertikale Giebelfläche, `roof.ts:200-203`) — bei "walm" wird
|
||||
daraus eine geneigte Fläche wie beim Walmdach. Mittlerer Aufwand, da die
|
||||
Mansard-Berechnung (`roof.ts:176-206`) bereits alle Eckpunkte hat, nur die
|
||||
Gable-Erzeugung muss konditional werden.
|
||||
3. **Editierbarer Mansard-Knickpunkt** (`kinkDepthRatio?: number` statt hart
|
||||
`0.4`, `roof.ts:181`) — 0.5 PT, reine Parametrisierung einer bestehenden
|
||||
Konstante.
|
||||
4. **Krüppelwalm** (`hipTruncation?: number`, 0 = voller Walm, 1 = voller
|
||||
Giebel/Sattel): der First bleibt voll lang, nur ein kleines Walmstück am
|
||||
First-Ende — technisch eine Variation der bestehenden `walm`-Berechnung
|
||||
(`roof.ts:147-174`, der First-Verkürzungs-Faktor `halfD` wird
|
||||
parametrisiert statt fix).
|
||||
5. **Dachflächenfenster** (kein neues Bauteil — ein `Opening`-ähnliches
|
||||
Element, das in eine Dachfläche statt eine Wand einschneidet): neuer
|
||||
`hostRoofId`-Pfad, eigenständiger Vorschlag; lohnt sich erst NACH der
|
||||
Schichtlogik, weil das Fenster sonst nicht "in die Dämmebene" passt.
|
||||
6. **Gauben** (Schlepp-/Sattelgaube als eigenständiges Sekundär-`Roof` mit
|
||||
eigenem `outline`/`baseElevation`, das in die Hauptdachfläche einschneidet):
|
||||
grösster Einzelposten, weil er eine echte Boolean-Verschneidung zwischen
|
||||
zwei Dachkörpern braucht (ähnlich der bestehenden Wand-Boolean-Dominanz
|
||||
über `joinPriority`, aber räumlich in 3D). Realistisch erst nach einem
|
||||
Mesh-Boolean-Werkzeug (die begonnene truck-Integration,
|
||||
`src-tauri/trucksolid/`, ist ein Kandidat dafür).
|
||||
7. **L-/T-Grundriss mit Kehle (Straight-Skeleton)**: bewusst NICHT vor 6,
|
||||
weil es die Bounding-Box-Vereinfachung komplett ersetzt (neue
|
||||
Geometrie-Engine, kein inkrementeller Ausbau von `roof.ts`) — separates,
|
||||
grosses Vorhaben, siehe Prioritätsliste.
|
||||
|
||||
### 4.5 Lücken (Dach)
|
||||
|
||||
| Feature | Status | Priorität | Aufwand |
|
||||
|---|---|---|---|
|
||||
| `RoofType`/`Layer[]`-Schichtaufbau (Modell) | ✗ (`thickness` toter Skalar) | **P0** | 3 PT |
|
||||
| 3D-Schichten (gestapelte Flächen entlang Normalen) | ✗ | **P0** | 3 PT |
|
||||
| 2D-Aufsicht-Poché (Eindeckungssymbol) | ✗ | P1 | 1.5 PT |
|
||||
| Dach im Vertikalschnitt (Ebene-Ebene-Schnitt + Bänder) | ✗ | **P0** | 5 PT |
|
||||
| Traufe/Ortgang getrennter Überstand | ✗ | **P0** | 1 PT |
|
||||
| Mansard-Untertyp (Walm/Giebel/Zelt-Stirn) | ✗ | P1 | 2 PT |
|
||||
| Editierbarer Mansard-Knick | ✗ | P2 | 0.5 PT |
|
||||
| Krüppelwalm | ✗ | P2 | 1.5 PT |
|
||||
| Dachflächenfenster | ✗ | P2 | 4 PT |
|
||||
| Gauben (Boolean-Einschnitt) | ✗ | P3 | 8+ PT |
|
||||
| L-/T-Grundriss, Straight-Skeleton-Kehle | ✗ | P3 | 10+ PT |
|
||||
| `RoofType`-Ressourcen-UI | ✗ | P1 (folgt aus RoofType) | 1 PT |
|
||||
| IFC `IfcRoof`-Export | ✗ | P2 | 1.5 PT |
|
||||
|
||||
---
|
||||
|
||||
## 5. Decke (Ceiling / Slab)
|
||||
|
||||
### 5.1 Was ein vollständiges BIM-Tool bietet
|
||||
|
||||
| Bereich | Typische Parameter |
|
||||
|---|---|
|
||||
| Grundfläche | beliebiges Polygon inkl. Aussparungen (Treppenauge, Schacht, Kamindurchbruch) |
|
||||
| Schichtaufbau | Rohdecke, Trittschalldämmung, Estrich, Bodenbelag — analog Wand, mit Deckenspiegel/Untersicht (abgehängte Decke, Akustikplatten) als eigene Schicht(en) |
|
||||
| Randausbildung | gerader Rand, auskragender Balkon-/Vordachrand mit thermischer Trennung (Isokorb/Randdämmstreifen), Randschalung/Abschalungsprofil, Attika-Anschluss, Tropfkante |
|
||||
| Deckenspiegel | abgehängte Untersicht mit eigener Höhe/Raster (Akustik-/Gipskarton-Decke), UNABHÄNGIG von der tragenden Rohdecke |
|
||||
| Öffnungen | Deckenaussparungen als eigene, editierbare Polygone (nicht nur Gesamtumriss) |
|
||||
| Neigung | geneigte Decke (Garagenrampe, Terrasse mit Gefälle) — nicht nur horizontal |
|
||||
| 2D-Darstellung | Aufsicht mit Ansichts-Poché (unaufgeschnitten), Schnitt mit Vollschichten je Lage, Deckenspiegel-Plan (reflected ceiling plan) als eigene Zeichnungsart |
|
||||
| IFC-Rolle | `IfcSlab` (PredefinedType FLOOR/ROOF/BASESLAB), `IfcMaterialLayerSetUsage` für den Schichtaufbau, `IfcCovering` für abgehängte Decken |
|
||||
|
||||
### 5.2 IST in DOSSIER
|
||||
|
||||
`Ceiling` (`types.ts:928-1000`) hat bereits die stärkste Tiefe der vier
|
||||
Bauteile in diesem Dokument: geschlossenes Umriss-Polygon (`outline`),
|
||||
`ceilingTypeId` mit Legacy-Fallback auf `wallTypeId` (`getCeilingType`,
|
||||
`types.ts:2089`), volle Attribut-Override-Kette (`foreground`/`background`/
|
||||
`strokeWeight`/`hatchId` + `*Source`, wie bei `Wall`) und unabhängige
|
||||
vertikale Bindung von OK/UK über `VerticalAnchor` (`top?`/`bottom?`,
|
||||
`types.ts:990-999` — "floor"-gebunden oder "custom"-Z, exakt wie bei
|
||||
`Wall.top`/`Wall.bottom`, `types.ts:887-893`). 3D-Schichtstapel ist
|
||||
implementiert (`emitSlabs`, `toWalls3d.ts:1352-1419`), Schnitt-Schichtsplit
|
||||
ebenfalls (`splitSlabLayers`, `toSection.ts:633`).
|
||||
|
||||
Verifizierte Lücken:
|
||||
|
||||
- **Keine Aussparungen.** `Ceiling.outline: Vec2[]` ist EIN geschlossenes
|
||||
Polygon (`types.ts:936-939`) — kein `openings?: Vec2[][]` für
|
||||
Treppenauge/Schacht/Kamin. Ein Treppenloch in der Decke muss heute über
|
||||
die Aussenkontur der Decke "herumgeschnitten" werden (Decke als
|
||||
komplexes, nicht-konvexes Polygon), nicht als saubere Innenaussparung.
|
||||
(`elements.md:217` sah dieses Feld in der älteren Planung explizit vor,
|
||||
es wurde nie in `types.ts` übernommen.)
|
||||
- **Kein Randdetail.** Die Decke ist über die gesamte `outline` exakt
|
||||
`thickness` dick, EINHEITLICH. Es gibt kein Feld für eine abweichende
|
||||
Randausbildung (Aufkantung, Randdämmstreifen, Tropfkante, andere Dicke am
|
||||
Balkonrand). Ein auskragender Balkon lässt sich zwar über eine erweiterte
|
||||
`outline` modellieren, bekommt aber zwangsläufig denselben Vollschicht-
|
||||
Aufbau wie die Innendecke — eine thermisch getrennte Balkonplatte
|
||||
(Isokorb) ist nicht abbildbar.
|
||||
- **Kein Deckenspiegel.** Abgehängte Untersicht (Akustik-/GKB-Decke mit
|
||||
eigener, tieferer Kote) existiert nicht als eigenes Konzept — nur der
|
||||
tragende Aufbau über `ceilingTypeId`.
|
||||
Ein "Deckenspiegel-Plan" (reflected ceiling plan) fehlt als Zeichnungsart
|
||||
komplett (`DrawingLevelKind`, `types.ts:722`, kennt nur "floor"/"section"/
|
||||
"elevation"/"drawing").
|
||||
(Randbemerkung: Beleuchtungsplanung/Deckenspiegel ist ein Elektro-Thema und
|
||||
damit bewusst ausserhalb des DOSSIER-Kernscopes — siehe "nicht bauen",
|
||||
Abschnitt 6. Die reine Geometrie einer zweiten, tiefer liegenden Fläche
|
||||
bleibt aber ein legitimer BIM-1:1-Punkt.)
|
||||
- **Keine Neigung.** `top`/`bottom` sind je EIN `VerticalAnchor` (ein
|
||||
Z-Wert), keine Neigungsebene — eine geneigte Garagen-/Terrassendecke ist
|
||||
nicht modellierbar, nur über mehrere ebene Teildecken behelfsweise
|
||||
annäherbar.
|
||||
- **2D-Aufsicht nutzt nur die erste Schicht.** `addCeilingPoche`
|
||||
(`generatePlan.ts:2453-2536`) liest `wt.layers[0]` (Kommentar/Code
|
||||
`generatePlan.ts:2465`) für die Ansichts-Schraffur — bei einer
|
||||
mehrschichtigen Decke (z. B. Beton unten, Dämmung oben) zeigt die Aufsicht
|
||||
immer nur die OBERSTE (erste) Schicht, was für eine unaufgeschnittene
|
||||
Draufsicht baupraktisch korrekt ist (man sieht von unten die
|
||||
Untersicht/erste Lage), aber nicht konfigurierbar ist, welche Lage als
|
||||
"sichtbare" gilt, falls der Deckenaufbau umgekehrt sortiert wäre.
|
||||
- IFC: `IfcSlab` wird erzeugt (`exportIfc.ts:28`), aber laut Kopfkommentar
|
||||
(`exportIfc.ts:38-41`) bewusst OHNE `IfcMaterialLayerSet`/-`Usage` — der
|
||||
Export verliert den Schichtaufbau, den DOSSIER intern bereits hat.
|
||||
|
||||
### 5.3 Deckenrand/Randterminierung — vertieft
|
||||
|
||||
Der Nutzer nennt explizit "Deckenränder, Stirnabschlüsse, Anschluss an Wand/
|
||||
Aussenkante, auskragende Ränder, Randabschalung" als Schwerpunkt. IST-Bild:
|
||||
keines davon existiert als eigenes Konzept — die Decke ist ein reines
|
||||
Extrusions-Polygon. Konkreter Vorschlag, dreistufig nach Aufwand:
|
||||
|
||||
1. **Aussparungen** (`Ceiling.openings?: Vec2[][]`) — niedrigster Aufwand,
|
||||
grösster praktischer Nutzen (Treppenauge ist im Wohnbau der Regelfall,
|
||||
nicht die Ausnahme). 2D: zusätzliche Ausschnitts-Polygone in
|
||||
`addCeilingPoche` (Loch im Fill, zusätzliche Randlinien). 3D: `emitSlabs`
|
||||
bekommt Löcher im Extrusions-Profil (analog dem bereits vorhandenen
|
||||
Loch-Schnitt bei Wand-Öffnungen, `wallMeshCut.ts`, als Vorlage).
|
||||
2. **Randschicht-Override** (`Ceiling.edgeOverride?: { ringOffset: number;
|
||||
ceilingTypeId: string }` — ein schmaler Innenring der Decke entlang des
|
||||
Randes bekommt einen ANDEREN Layer-Aufbau, z. B. mit zusätzlicher
|
||||
Randdämmschicht oder reduzierter Dicke für eine Tropfkante). Technisch:
|
||||
`emitSlabs` erzeugt für den Ringbereich einen zweiten Layer-Stapel mit dem
|
||||
Override-Typ, geometrisch als Offset-Polygon-Differenz (`outline` minus
|
||||
`outline.offset(-ringOffset)`), eine Operation, die für Wandbänder
|
||||
bereits ähnlich existiert (`buildWallFootprints`).
|
||||
3. **Thermisch getrennte Auskragung (Isokorb-Fall)**: ein eigenes,
|
||||
sekundäres `Ceiling`-Objekt für den auskragenden Teil mit eigenem
|
||||
`ceilingTypeId` (dünnerer/anderer Aufbau) UND eigener `top`/`bottom`-
|
||||
Bindung, das an die Hauptdecke stösst — kein neues Feld nötig, nur eine
|
||||
UI-Erleichterung ("Deckenrand abtrennen"-Werkzeug, das die Decke entlang
|
||||
einer gewählten Kante in zwei `Ceiling`-Objekte teilt). Niedrigster
|
||||
Modell-Aufwand, weil er das bestehende Mehrfach-Decken-Prinzip nutzt statt
|
||||
ein neues Konzept einzuführen.
|
||||
|
||||
### 5.4 Lücken (Decke)
|
||||
|
||||
| Feature | Status | Priorität | Aufwand |
|
||||
|---|---|---|---|
|
||||
| Aussparungen (`openings?: Vec2[][]`) in 2D+3D | ✗ | **P0** | 3 PT |
|
||||
| Randschicht-Override (Ringzone anderer Aufbau) | ✗ | P1 | 3 PT |
|
||||
| Deckentrenn-Werkzeug für Isokorb-Fall (UI, kein neues Modellfeld) | ✗ | P1 | 1.5 PT |
|
||||
| Geneigte Decke (Rampe/Gefälle) | ✗ | P2 | 3 PT |
|
||||
| Deckenspiegel (zweite, abgehängte Fläche) | ✗ | P3 | 2 PT (reine Geometrie) |
|
||||
| IFC `IfcMaterialLayerSetUsage` für Decke (UND Wand) | ~ (bewusst ausgelassen) | P2 | 2 PT |
|
||||
| Konfigurierbare "oberste Schicht" für Aufsicht-Poché | ✓ implizit (Layer-Reihenfolge = Sortierung) | — | — |
|
||||
|
||||
---
|
||||
|
||||
## 6. Konsolidierte Priorisierung (über alle vier Bauteile)
|
||||
|
||||
Sortiert nach "1:1-Zuwachs pro Aufwand", nicht nach Aufwand allein:
|
||||
|
||||
1. **Dach-Schichtaufbau (Modell + 3D-Stapel)** — schliesst die grösste
|
||||
strukturelle Lücke (totes `thickness`-Feld, kein `RoofType`) und liefert
|
||||
sofort sichtbaren Tiefengewinn im 3D (§4.3, ~6 PT gesamt).
|
||||
2. **Traufe/Ortgang getrennter Überstand** — 1 PT, sofortiger optischer
|
||||
Sprung bei jedem Steildach-Projekt (§4.4 Punkt 1).
|
||||
3. **Zweiflügelige Tür rendert wirklich 2 Blätter** — das `leafCount`-Feld
|
||||
existiert seit der Türtyp-Einführung, wird aber komplett ignoriert; sehr
|
||||
sichtbarer Bug-artiger Gap (§2.3, 2 PT).
|
||||
4. **Deckenaussparungen** — Treppenauge ist der Wohnbau-Regelfall, heute nur
|
||||
über Umweg (nicht-konvexes Aussenpolygon) lösbar (§5.3 Punkt 1, 3 PT).
|
||||
5. **Fenster-Glazing 2D-wirksam** — seit über einem Jahr als P0 in der
|
||||
Fenster-Studie dokumentiert und weiterhin offen; niedriger Aufwand,
|
||||
sollte nicht liegen bleiben (§3.1, 1 PT).
|
||||
6. **Dach im Vertikalschnitt** — grösster Einzelposten (5 PT), aber ohne ihn
|
||||
bleibt jeder Gebäudeschnitt mit Steildach unvollständig; das ist die
|
||||
Art Lücke, die bei einer Bemusterung/Baueingabe sofort auffällt.
|
||||
7. **Deckenrand-Override / Isokorb-Trennwerkzeug** — folgt danach, weil er
|
||||
auf demselben Mehrfach-Decken-Prinzip aufbaut wie Punkt 4.
|
||||
8. **Mansard-Untertyp + editierbarer Knick** — mittlere Priorität, weil
|
||||
Mansarde in der Schweiz/Süddeutschland baupraktisch häufig ist und die
|
||||
heutige starre 40 %-Konstante sichtbar unrealistisch wirkt.
|
||||
9. **Tür/Fenster-Rahmentiefe** (asymmetrische Rahmenbreiten, Beschlag,
|
||||
Kopfform) — bewusst NACH den strukturellen Lücken, weil sie additive
|
||||
Detailverbesserungen an einem bereits funktionierenden Pfad sind, während
|
||||
1–7 fehlende oder falsch dargestellte Kernfunktionen betreffen.
|
||||
10. **Gauben, L-Grundriss/Kehle, Dachflächenfenster** — grösste Einzel-
|
||||
Aufwände (8–10+ PT), architektonisch am voraussetzungsreichsten (Boolean-
|
||||
Werkzeug bzw. neue Geometrie-Engine); erst nach 1–9 angehen.
|
||||
|
||||
### 6.1 NICHT bauen (VW/Revit-Ballast ohne Nutzen für DOSSIER)
|
||||
|
||||
- **Vollständiger Massketten-/Bezugsapparat** (VW B1..B5/H1..H7,
|
||||
Roh-/Fertigmass-Umschaltung) — DOSSIER arbeitet mit lichten Massen, das
|
||||
genügt für ein Schweizer Wohnbau-/Kleinprojekt-Tool (bereits so in
|
||||
`window-editor-vectorworks-study.md` §5 entschieden, hier bestätigt für
|
||||
Dach/Decke: keine Dach-Rohmass-/Fertigmass-Unterscheidung).
|
||||
- **Sichtbarkeitsmatrix 3D-Objekte × Ansichten** (Augen-Tabelle je
|
||||
Kategorie×Ansicht×Detailstufe) — DOSSIERs Layer-Sichtbarkeit +
|
||||
`DetailLevel` deckt den praktischen Bedarf; eine volle Matrix ist
|
||||
Verwaltungsaufwand ohne Mehrwert für Einzelprojekte.
|
||||
- **Eckfenster/Eckdach als generischer Sonderfall über zwei Wirtsbauteile**
|
||||
— seltene Geometrie, hoher Modellierungsaufwand (zwei Hosts, ein Element);
|
||||
bei Bedarf als manueller Workaround (zwei separate Öffnungen) lösbar.
|
||||
Ebenso: freie Neigungsflächen je Dachkante (VW erlaubt jede Kante einzeln
|
||||
zu kippen) — für die abgedeckten Standardformen (Pult/Sattel/Walm/
|
||||
Mansarde/Zelt/Krüppelwalm) nicht nötig; wer eine Freiform-Dachlandschaft
|
||||
braucht, ist besser mit den `ExtrudedSolid`/truck-Werkzeugen bedient.
|
||||
- **Vollständige Beschlags-/Baubeschlag-Bibliothek** (Marken-Beschlagsätze,
|
||||
Schliessplan) — Beschlag als generisches Griff-Mesh (§2.4) genügt für die
|
||||
visuelle 1:1-Wirkung; eine Beschlags-PRODUKTBIBLIOTHEK ist Kataloggeschäft,
|
||||
kein CAD-Kernfeature.
|
||||
- **Deckenspiegel als vollwertige Beleuchtungsplanung** (Leuchtenraster,
|
||||
Lichtberechnung) — reine Geometrie einer zweiten Fläche ist ok (P3), die
|
||||
Elektro-/Lichtplanungslogik selbst liegt ausserhalb des Tool-Zwecks.
|
||||
- **IFC `IfcOpeningElement`/Void-Semantik nachrüsten** — bewusste
|
||||
Design-Entscheidung im bestehenden Export (`exportIfc.ts:22-27`), NICHT
|
||||
revidieren: die heutige "Loch steckt im Mesh"-Lösung liefert visuelle
|
||||
Parität in jedem Viewer ohne Boolean-Pflicht beim Empfänger; der reine
|
||||
IFC4-Purismus (Wand als parametrische Extrusion + Void) würde
|
||||
bestehende, bewusst getroffene Trade-offs zunichtemachen.
|
||||
- **Straight-Skeleton/Kehlen und Gauben SOFORT** — nicht "nicht bauen", aber
|
||||
bewusst zurückgestellt (§6, Punkt 10): ohne die Schichtlogik (Punkt 1)
|
||||
vorher zu bauen, würde jede Kehlen-/Gauben-Lösung auf dem unendlich dünnen,
|
||||
ungeschichteten Dach aufsetzen und müsste bei Einführung der Schichten
|
||||
ohnehin neu gefasst werden.
|
||||
@@ -0,0 +1,110 @@
|
||||
# DOSSIER-Feature-Audit (A1–A6, B1–B4, C1–C3, D1–D3, E)
|
||||
|
||||
Ausführliche Fassung des Audits, auf das HANDOVER.md Backlog-Punkt 9 nur noch mit
|
||||
Einzeilern verweist. Abgleich DOSSIER-Rhino-Referenzrepo (`/tmp/dossier-ref`) gegen
|
||||
den aktuellen Browser-Port. Belege (Datei:Zeile) beziehen sich auf `/tmp/dossier-ref/rhino/*.py`.
|
||||
|
||||
Status-Symbole: ❌ nicht übernommen · 🟡 teilweise übernommen.
|
||||
|
||||
## A — Grösserer Funktionsumfang
|
||||
|
||||
**A1 · ❌ Grafische Overrides — Regel-Engine** (`overrides.py:7-40`)
|
||||
ArchiCAD/Vectorworks-Stil: Regeln der Form `condition{type: layer_name|user_string|
|
||||
object_name, operator: equals|contains|starts_with|not_equals, value, key} →
|
||||
actions{color, lineweight, linetype}`, additiv angewendet, oberste Regel gewinnt,
|
||||
reversibel. Cross-Doc-Presets + Rule-Templates (`list_presets:147`,
|
||||
`list_rule_templates:218`). Der Port hat nur manuelle Per-Instanz-Farbe (By-Layer/
|
||||
By-Object/eigener Wert, kein Regelwerk). Wert: enorm für Architektur-Pläne
|
||||
(Bestand grau, Brandabschnitte, Bauphasen farblich markieren). Aufwand: M-L.
|
||||
|
||||
**A2 · ❌ Ausschnitte / View-Snapshots** (`ausschnitte.py:466-531`)
|
||||
Benannte, gespeicherte Ansichten, die Kamera-Zustand + Layer-Sichtbarkeit +
|
||||
Massstab + Detailgrad einfangen und wiederherstellen; organisiert in Ordnern +
|
||||
Presets (`_capture_camera:78`, `_capture_layers:157`, `_capture:466`). Wert:
|
||||
zentral für Wiederverwendbarkeit und Voraussetzung für A3. Aufwand: L.
|
||||
|
||||
**A3 · ❌ Print-Layout-Blätter** (`layouts.py:7-8`)
|
||||
Layout-Seiten mit mehreren Details, jedes Detail an einen Ausschnitt-Snapshot
|
||||
gebunden (`_BIND_KEY:28`), Papierformate A4/A3, Ordnerstruktur, PDF-Export pro
|
||||
Layout-Blatt. Der Port exportiert aktuell nur eine einzelne Zeichnung nach PDF.
|
||||
Aufwand: L, hängt an A2.
|
||||
|
||||
**A4 · ❌ Tragwerk-Elemente** (`elemente.py`)
|
||||
Stütze, Träger, Unterzug, I-Profil als eigene Elementtypen. Der Port kennt nur
|
||||
Wand/Decke/Öffnung/Treppe/Raum. Wert: vervollständigt den BIM-Elementsatz.
|
||||
Aufwand: M je Typ.
|
||||
|
||||
**A5 · 🟡 Öffnungen viel reicher** (`elemente.py:59-68`)
|
||||
Mehrere Flügel (`OEFF_FLUEGEL`), Sims innen+aussen mit eigenen Stilen
|
||||
(`SIMS_AUS`/`SIMS_IN`), Glas-Toggle (`OEFF_GLAS`), Rahmen-Lage aussen/mittig/innen
|
||||
(`RAHMEN_POS`), Rahmen-Profilbreite/-tiefe, Detailgrad einfach/standard/detail =
|
||||
SIA-400-Darstellung (`OEFF_DARSTELLUNG:68`). Der Port hat nur swing/hinge/
|
||||
frameDepth/frameThickness. Aufwand: M.
|
||||
|
||||
**A6 · 🟡 Element-Schedule / Bauteilliste** (`elemente_uebersicht.py:45,214`)
|
||||
Voller Element-Überblick + SIA-Flächenbilanz + CSV-Export
|
||||
(`_export_bilanz:214`). Der Port hat nur einen Raum-CSV-Export. Aufwand: M.
|
||||
|
||||
## B — Werkzeuge & Objekt-Info
|
||||
|
||||
**B1 · ❌ Text-Platzierungs-Werkzeug** (`text_create.py:972,847,105`)
|
||||
On-Canvas-Text-Objekte als eigenständiges Zeichenwerkzeug (nicht nur
|
||||
Raumstempel): Textstile, Fonts, Rich-Text fett/kursiv/unterstrichen,
|
||||
Ausrichtung, Symbol-Einfügung, „auf Selektion anwenden". Der Port hat den
|
||||
Rich-Text-Editor (`src/text/RichTextEditor.tsx`) bereits, aber kein
|
||||
Platzierungs-Werkzeug, um damit ein freistehendes Textobjekt zu zeichnen
|
||||
(Shortcut „1" ist bewusst noch unbelegt). Aufwand: M.
|
||||
|
||||
**B2 · 🟡 Object-Info numerisch erweitern** (`dimensions.py:342-350,240,267`)
|
||||
Position, Rotation um Z-Achse (`_rotate_around_axis:240`), Kreis-Radius,
|
||||
Linien-Länge (`_set_line_length:267`), Rechteck Breite/Höhe,
|
||||
Koordinatensystem World/CPlane, 9-Punkt-Referenz. Der Port kann nur per
|
||||
Anker-Griff skalieren. Aufwand: S-M.
|
||||
|
||||
**B3 · ❌ Kamera-Presets + Nordwinkel** (`kamera.py:28,36-45,87`)
|
||||
Nicht in HANDOVER.md gelistet, aber im Audit gefunden: Kardinal-Presets
|
||||
(N/O/S/W), Iso-Oktanten, Rotation um einen einstellbaren Nordwinkel
|
||||
(georeferenziert, relevant für Swisstopo-Kontext).
|
||||
|
||||
**B4 · ❌ Massstab-Toolbar-Funktionen** (nicht separat referenziert, Teil der
|
||||
Toolbar-Logik) — Zoom 1:1/auf Selektion, Linienstärken-Set, Grid/Ortho/
|
||||
Referenzlinien-Toggles direkt aus der Symbolleiste.
|
||||
|
||||
## C — Zusätzlich gefunden, nicht in HANDOVER.md
|
||||
|
||||
**C1 · LoD pro Ansicht** — Darstellung einfach/standard/detail je Ansicht
|
||||
umschaltbar (SIA-400-Detailgrad-Konvention), nicht nur global.
|
||||
|
||||
**C2 · Override-Presets + Rule-Templates als Bibliothek** — vertieft A1: die
|
||||
Regeln selbst sind wiederverwendbare, benannte Presets/Templates, nicht nur
|
||||
Ad-hoc-Zustand pro Dokument.
|
||||
|
||||
**C3 · Mass-Style-Presets** — Dezimalstellen/Rundung als benannte,
|
||||
wiederverwendbare Bemassungs-Stile.
|
||||
|
||||
## D — Weitere Backlog-Ergänzungen aus dem Audit
|
||||
|
||||
**D1** Per-Layout-PDF-Export (vertieft A3).
|
||||
**D2** Bauteil-CSV mit vollem Element-Set (vertieft A6).
|
||||
**D3** Komponenten-Thumbnails (visuelle Vorschau im Component-Manager).
|
||||
|
||||
## E — Bewusst NICHT zu portieren
|
||||
|
||||
Rhino-/Windows-gebundene Implementierungsdetails ohne Browser-Äquivalent:
|
||||
Window-Layout-XML-Persistierung, Auto-DPI via CoreGraphics, Custom-Grips-Code
|
||||
(Rhino-SDK-spezifisch), nativer `.3dm`-Geometrie-Import.
|
||||
|
||||
## Priorisierung (aus dem Original-Audit)
|
||||
|
||||
1. A1 (Override-Regel-Engine)
|
||||
2. A2 (View-Snapshots)
|
||||
3. A3 (Print-Layout-Blätter)
|
||||
4. A5 (reichere Öffnungen)
|
||||
5. B1 (Text-Platzierungs-Werkzeug)
|
||||
6. A4 (Tragwerk)
|
||||
7. B2 (Object-Info numerisch)
|
||||
8. A6 (Bauteil-Schedule)
|
||||
|
||||
Siehe auch `ROADMAP.md` §11 für eine noch breitere, unabhängig entstandene
|
||||
Backlog-Liste (4-Wege-Survey über Bauteile/Darstellung/Pläne/Kontext) mit
|
||||
teilweiser Überschneidung.
|
||||
@@ -0,0 +1,137 @@
|
||||
# Engine-Nordstern — Headless-Rendering (PNG-Export & Golden-Image-Tests)
|
||||
|
||||
> Betrifft `src-tauri/render2d` (Feature `headless`). Bezug: HANDOVER.md,
|
||||
> Abschnitt ENGINE-NORDSTERN, Punkt 3 ("deterministisches Headless-Rendering,
|
||||
> PNG-Export und Golden-Image-Tests ohne Fenster").
|
||||
|
||||
## Warum
|
||||
|
||||
`render2d` trennt die serde-only-Tessellierung (Feature-los, headless testbar)
|
||||
von der GPU-Schicht (Feature `render`, wgpu). Der Fenster-Pfad (`gpu::Renderer`,
|
||||
Feature `window`) braucht dafür bislang eine `wgpu::Surface` — also ein echtes
|
||||
Fenster mit Wayland-/X11-Session. Für deterministische PNG-Exporte und
|
||||
Golden-Image-Tests (CI, Regressions-Screenshots) ist das unnötig: wgpu kann
|
||||
genauso gut in eine `wgpu::Texture` rendern, ganz ohne Surface/Fenster.
|
||||
|
||||
`gpu::Renderer::render` nahm bereits vorher nur `Device`/`Queue`/`TextureView`
|
||||
entgegen — Surface- oder Offscreen-Textur macht für den Draw-Code keinen
|
||||
Unterschied. Der Offscreen-Pfad (`src/headless.rs`) dupliziert daher NICHTS,
|
||||
sondern baut nur Device/Queue ohne Surface sowie eine eigene Ziel-Textur +
|
||||
Buffer-Readback drumherum.
|
||||
|
||||
## Feature-Gating
|
||||
|
||||
Neues Cargo-Feature `headless = ["render", "dep:image"]`:
|
||||
- zieht `render` (wgpu/glyphon/bytemuck/pollster) plus die `image`-Crate
|
||||
(nur der PNG-Codec, `default-features = false, features = ["png"]`).
|
||||
- **berührt den wasm/web-Build nicht**: `cargo check --target wasm32-unknown-unknown
|
||||
--no-default-features --features web` zieht `image` nicht mit.
|
||||
- Binary `render_png` und Test `golden` sind zusätzlich per
|
||||
`required-features = ["headless"]` in `Cargo.toml` abgesichert.
|
||||
|
||||
## API
|
||||
|
||||
```rust
|
||||
pub struct HeadlessRenderer { /* Device, Queue, gpu::Renderer */ }
|
||||
|
||||
impl HeadlessRenderer {
|
||||
/// Instance/Adapter/Device OHNE Surface (Backend fest auf Vulkan gepinnt).
|
||||
/// `Err`, wenn kein Adapter verfügbar ist (z.B. CI-Runner ohne GPU) — die
|
||||
/// Aufrufer (CLI, Golden-Test) behandeln das, statt zu paniken.
|
||||
pub fn new() -> Result<Self, String>;
|
||||
|
||||
/// Rendert `scene` in ein `width`x`height`-Bild (Papier-Maßstab
|
||||
/// `paper_scale_n`, z.B. `100.0` für 1:100). Lädt die Szene bei jedem Aufruf
|
||||
/// neu hoch (kein Zwischenzustand nötig für CLI-/Test-Anwendungsfall).
|
||||
pub fn render_to_image(
|
||||
&mut self,
|
||||
scene: &Scene,
|
||||
width: u32,
|
||||
height: u32,
|
||||
view_box: ViewBox,
|
||||
paper_scale_n: f32,
|
||||
) -> RgbaImage; // { width, height, pixels: Vec<u8> (straff gepackt, RGBA8) }
|
||||
}
|
||||
|
||||
impl RgbaImage {
|
||||
pub fn encode_png(&self) -> Vec<u8>;
|
||||
/// Gegenstück für den Golden-Test: Referenz-PNG -> straff gepacktes RGBA8.
|
||||
pub fn decode_png(bytes: &[u8]) -> Result<Self, String>;
|
||||
}
|
||||
```
|
||||
|
||||
Backend bewusst auf **Vulkan** gepinnt (nicht das Standard-Backend-Set): Vulkan
|
||||
rendert offscreen ohne jede Fenster-/Display-Server-Abhängigkeit. GL bräuchte auf
|
||||
Linux i.d.R. einen EGL/GLX-Kontext, der ohne aktive Display-Session (X11/Wayland)
|
||||
Sonderfälle hat — auf dieser Maschine ist ohnehin ein echter Vulkan-ICD (RADV)
|
||||
vorhanden, daher kein Rückgriff auf `lavapipe`/`llvmpipe` nötig gewesen.
|
||||
|
||||
## Row-Alignment-Falle (wichtig für zukünftige Agenten)
|
||||
|
||||
`wgpu::Queue::copy_texture_to_buffer` / `CommandEncoder::copy_texture_to_buffer`
|
||||
verlangt, dass `bytes_per_row` im `ImageDataLayout` ein Vielfaches von
|
||||
`wgpu::COPY_BYTES_PER_ROW_ALIGNMENT` (256) ist. Bei RGBA8 (4 Byte/Pixel) trifft
|
||||
das **nur zufällig** zu — z.B. Breite 300 px → 1200 Byte/Zeile, kein Vielfaches
|
||||
von 256, der Copy schlägt sonst mit einem Validierungsfehler fehl (oder liefert
|
||||
verzerrte Zeilen, je nach Backend).
|
||||
|
||||
Lösung in `headless::HeadlessRenderer::read_pixels`:
|
||||
1. `padded_bytes_per_row = align_up(width * 4, 256)` — der Zielpuffer wird auf
|
||||
diese (größere oder gleiche) Zeilenbreite alloziert.
|
||||
2. Nach dem Mapping wird **jede Zeile einzeln** von `padded_bytes_per_row` auf
|
||||
die echte Breite (`width * 4`) zurückgeschnitten und in einen straff
|
||||
gepackten `Vec<u8>` kopiert — sonst hätte das Ausgabebild pro Zeile
|
||||
Garbage-Padding am rechten Rand.
|
||||
|
||||
## Referenzbild neu erzeugen
|
||||
|
||||
Bei einer **gewollten** Rendering-Änderung (z.B. neue Shader, neue Demo-Szene,
|
||||
geänderte Antialiasing-Parameter) weicht das Golden-Bild ab. Referenzbild
|
||||
danach bewusst neu erzeugen:
|
||||
|
||||
```sh
|
||||
cd src-tauri/render2d
|
||||
cargo run --features headless --bin render_png -- --width 990 --height 630 --out target/headless-demo.png
|
||||
cp target/headless-demo.png tests/golden/demo.png
|
||||
```
|
||||
|
||||
Vorher unbedingt das erzeugte PNG (`target/headless-demo.png`) visuell prüfen
|
||||
(z.B. mit einem Bildbetrachter oder Read-Tool eines Agenten) — nicht blind
|
||||
kopieren. Bei einem fehlgeschlagenen Testlauf schreibt der Golden-Test
|
||||
automatisch ein Diff-Bild nach `target/golden-diff.png` (abweichende Pixel
|
||||
signalrot markiert, Rest unverändert) — das hilft beim Einordnen, ob die
|
||||
Abweichung erwartet (Layout-/Farbänderung) oder ein Bug ist (z.B. verschobene
|
||||
Geometrie, fehlender Text).
|
||||
|
||||
## Golden-Test
|
||||
|
||||
`tests/golden.rs`, Test `demo_szene_entspricht_referenzbild`:
|
||||
- rendert die Demo-Szene (`demo::demo_scene`, dieselbe wie der Fenster-Spike)
|
||||
in 990×630 und vergleicht sie gegen `tests/golden/demo.png`.
|
||||
- Toleranz: ein Pixel gilt als "abweichend", wenn irgendein Kanal-Delta > 2 ist
|
||||
(deckt AA-Rundungsrauschen ab); der Test schlägt fehl, wenn mehr als 0,5%
|
||||
aller Pixel abweichen.
|
||||
- `#[ignore]`, weil eine Vulkan-fähige GPU auf CI-Runnern nicht garantiert ist
|
||||
(und ein Software-Rasterizer wie llvmpipe anderes AA liefern würde als das
|
||||
Referenzbild). Lokal explizit ausführen:
|
||||
`cargo test --features headless -- --include-ignored`.
|
||||
- Bei Abweichung über der Toleranz schreibt der Test zusätzlich zum Diff-Bild
|
||||
auch das Ist-Bild nach `target/golden-actual.png` — nach visueller Prüfung
|
||||
ist das die Vorlage für eine gewollte Referenz-Aktualisierung.
|
||||
- `HeadlessRenderer::new()` liefert bei fehlendem Adapter `Err` statt Panik —
|
||||
der Test überspringt sich dann sauber (`eprintln!` + `return`) statt rot zu
|
||||
schlagen, falls doch mal ohne `--ignored`/ohne GPU ausgeführt.
|
||||
|
||||
## Offener Punkt: Text-Determinismus zwischen GPU-Treibern
|
||||
|
||||
Der Textpass läuft über `glyphon`/`cosmic-text` (Systemfonts, `Inter`). Die
|
||||
exakte Glyphen-Rasterisierung (Subpixel-Antialiasing, Hinting) kann je nach
|
||||
installierter Font-Version, Fontconfig-Konfiguration und GPU-Treiber leicht
|
||||
variieren — auch bei identischer Geometrie. Auf **derselben** Maschine (gleicher
|
||||
Treiber, gleiche Font-Version) ist der Test wie erwartet bit-nah deterministisch
|
||||
(hier: 0 abweichende Pixel bei zwei aufeinanderfolgenden Läufen). Bei einem
|
||||
Wechsel der Maschine/des Treibers/der Fontconfig-Version ist nicht
|
||||
auszuschließen, dass die 0,5%-Toleranz für Textkanten knapp wird — sollte das
|
||||
in der Praxis auftreten: entweder Toleranz für den Text-Bereich lockern, oder
|
||||
den Text testweise aus der Golden-Szene ausklammern und separat (z.B. nur
|
||||
Zeilenbreite/Position, nicht Pixel) prüfen.
|
||||
@@ -0,0 +1,511 @@
|
||||
# Engine-Nordstern 1 — Strichbreiten-Audit (Papier-mm end-to-end)
|
||||
|
||||
> Bezug: HANDOVER.md, Abschnitt ENGINE-NORDSTERN Punkt 1 ("Papier-mm-exakte
|
||||
> Strichbreiten überall — Bildschirm bei jedem Massstab/Zoom = Druck"). Dieses
|
||||
> Dokument ist ein Prüfbericht, kein Umbau — es fasst zusammen, wie die
|
||||
> Strichbreite heute vom Modell bis zum Papier läuft, wo sie divergieren kann,
|
||||
> und was konkret zu tun wäre. Nichts hieraus wurde umgesetzt.
|
||||
|
||||
Betroffene Pfade: `src/plan/PlanView.tsx` (SVG-Default + Print-Vorschau),
|
||||
`src/plan/glPlan/*` (WebGL2, aktueller Default-GPU-Pfad), `src-tauri/render2d`
|
||||
(WASM/WebGPU, `?engine=wasm`), `src/export/sceneToPrintSvg.ts` +
|
||||
`src/export/exportPdf.ts` (Vektor-PDF). Referenz-Baseline für die
|
||||
2D-Engine-Parität ist laut Commit `ce6bd26` **`?gl=0`** (reiner SVG-Pfad) —
|
||||
NICHT der App-Default (der ist WebGL2, siehe Finding 1).
|
||||
|
||||
## 1. Modell der Wahrheit
|
||||
|
||||
So sollte eine Strichbreite in diesem Code korrekt fliessen:
|
||||
|
||||
1. **Quelle**: `generatePlan.ts` legt jede Strichbreite als `weightMm` /
|
||||
`strokeWidthMm` in **echten Papier-Millimetern** ab (Kommentarkopf,
|
||||
`generatePlan.ts:76-79`: "Alle Stricharten sind in mm Papier definiert").
|
||||
Diese Zahl ist unabhängig von Zoom, Massstab und Render-Pfad — eine 0.18 mm
|
||||
Wand-Umrisslinie bleibt 0.18 mm, ganz gleich wo sie später landet.
|
||||
2. **Bildschirm bei Massstab 1:N**: 1 Modell-Meter entspricht auf Papier
|
||||
`1000/N` mm. Der Bildschirm zeigt `PX_PER_M = 90` viewBox-Einheiten je
|
||||
Modell-Meter (`PlanView.tsx:23`). Eine `mm`-Breite belegt daher
|
||||
`mm · N/1000 · PX_PER_M` viewBox-Einheiten — das ist exakt
|
||||
`printStrokeVb()` (`PlanView.tsx:2598-2600`). Multipliziert mit der
|
||||
Geräte-px-je-viewBox-Einheit-Skala (`meet`, `PlanView.tsx:2074-2078`) ergibt
|
||||
das die tatsächliche Bildschirmbreite in Geräte-Pixeln. Reinzoomen (kleinere
|
||||
viewBox-Breite, grösseres `meet`) macht die Linie dicker — genau wie beim
|
||||
Herausvergrössern eines gedruckten Plans mit der Lupe. Das gilt für JEDE
|
||||
Zoomstufe gleichermassen: 250 % Zoom bei 1:50 zeigt exakt die 5-fache
|
||||
Pixelbreite von 100 % Zoom bei 1:50, und bei gegebenem Zoom ist 1:50 exakt
|
||||
doppelt so dick wie 1:100 (halber Nenner → doppelt so viele Weltmeter je
|
||||
Papiermm).
|
||||
3. **Druck/PDF**: dieselbe Formel, nur ohne Geräte-px-Zwischenschritt — direkt
|
||||
`mm` bleibt `mm` — die Seite ist direkt in echten
|
||||
Papiermillimetern aufgespannt (`sceneToPrintSvg.ts:99-125`). Zusätzlich wird
|
||||
auf ISO-nahe Stiftstufen gerundet (`PEN_STEPS`,
|
||||
`sceneToPrintSvg.ts:44`), weil ein reales Zeichengerät/Plotter nur endlich
|
||||
viele Stiftbreiten kennt.
|
||||
4. **Eine Wahrheit**: Seit `382771b` bauen sowohl der Viewport
|
||||
(`useWasmPlanRenderer.ts`/`nativeSync.ts`) als auch der PDF-Export
|
||||
(`exportPdf.ts:73`) dieselbe `planToRenderScene(plan)`-Szene — der PDF-Pfad
|
||||
ist nur ein anderes *Ziel* derselben Szene, kein zweiter Interpret.
|
||||
|
||||
Korrekt hiesse also: **jede** der vier Anzeige-/Exportarten (SVG-Default,
|
||||
SVG-Print-Vorschau, GPU-Viewport, PDF) muss aus **derselben** `weightMm`, für
|
||||
**denselben** `N`, exakt dieselbe Papier-mm-Breite ergeben — bis auf die
|
||||
bewusste PEN_STEPS-Rundung im Druckpfad, die dokumentiert und überall
|
||||
gleichermassen sichtbar sein sollte (ist sie nicht, siehe Finding 2).
|
||||
|
||||
Ausdrücklich **kein** Teil dieses Modells: der Haarlinien-Modus
|
||||
(`lineMode: "display"`, App-Default, `viewSlice.ts:38,74`). Er ist als
|
||||
bewusster Papier-mm-*Ausstieg* gedacht ("Display: all lines as constant
|
||||
hairlines (calm editing)", `en.ts:173`) — 1 Geräte-px, konstant, unabhängig
|
||||
von Zoom/Massstab. Er muss also NICHT der Papier-mm-Formel folgen, aber er
|
||||
muss in JEDEM Render-Pfad *gleichermassen* als Ausstieg wirken. Tut er nicht
|
||||
(Finding 1).
|
||||
|
||||
## 2. Pfad-für-Pfad-Trace
|
||||
|
||||
### 2a. SVG-Default (App-Start ohne `?gl=0`, `lineMode:"display"`)
|
||||
|
||||
Nur aktiv, wenn WebGL2 fehlschlägt (`wantGl` ist sonst `true`, s. Finding 1) —
|
||||
de facto der reine Fallback-Pfad.
|
||||
|
||||
- `PlanView.tsx:2613`: `print = !hairline && paperScale != null && paperScale > 0`
|
||||
— mit `hairline=true` (Default) ist `print` immer `false`.
|
||||
- `PlanView.tsx:2617-2618`:
|
||||
```ts
|
||||
const weight = (mm: number): number =>
|
||||
hairline ? HAIRLINE_PX : print ? printStrokeVb(mm, paperScale!) : mmToPx(mm);
|
||||
```
|
||||
`HAIRLINE_PX = 1` (`PlanView.tsx:2583`), Einheit: Geräte-unabhängiger
|
||||
CSS-Pixel, gezeichnet mit `vector-effect="non-scaling-stroke"`
|
||||
(`PlanView.tsx:2621`, `vfx`) → bleibt beim Pan/Zoom optisch exakt 1 px, egal
|
||||
wie stark reingezoomt wird. Papier-mm spielt hier explizit KEINE Rolle.
|
||||
|
||||
### 2b. SVG-Print-Vorschau (`lineMode:"print"`, weiterhin `?gl=0` oder
|
||||
WebGL2-Fallback)
|
||||
|
||||
- `print = true`, `weight(mm) = printStrokeVb(mm, paperScale!)`
|
||||
(`PlanView.tsx:2598-2600`):
|
||||
```ts
|
||||
function printStrokeVb(mm: number, n: number): number {
|
||||
return Math.max(1e-4, (mm * n) / 1000) * PX_PER_M;
|
||||
}
|
||||
```
|
||||
`vectorEffect` entfällt (`vfx = undefined`, `PlanView.tsx:2621`) — die Linie
|
||||
skaliert MIT der Geometrie beim Zoomen, wie physisches Papier unter der
|
||||
Lupe. `paperScale` selbst ist der **stabile, gemessene** 1:N-Nenner
|
||||
(`PlanView.tsx:458-460`), nicht der live aus jedem Radzoom abgeleitete Wert
|
||||
— sonst würde Reinzoomen die Linien nicht dicker, sondern konstant halten
|
||||
(das wäre wieder Haarlinien-Verhalten). Keine PEN_STEPS-Rundung — die rohe
|
||||
`mm`-Zahl wird 1:1 in viewBox-Einheiten übersetzt.
|
||||
|
||||
### 2c. GPU-Viewport (WebGL2 `useGlPlanRenderer.ts` — App-DEFAULT; WASM
|
||||
`useWasmPlanRenderer.ts` bei `?engine=wasm`)
|
||||
|
||||
- Beide Hooks reichen `paperScaleN` unverändert an die Engine durch:
|
||||
`useWasmPlanRenderer.ts:131-149` (`render(viewBox, paperScaleN=100,
|
||||
textScaleN=100)` → `r.set_paper_scale(paperScaleN)`); WebGL2 analog
|
||||
(`useGlPlanRenderer.ts:102-116`, kein `textScaleN`-Parameter überhaupt, s.
|
||||
Finding 3).
|
||||
- `PlanView.tsx` ruft in JEDEM Render-Aufruf
|
||||
(`PlanView.tsx:494`,`502`,`1080`) `renderGl(view, paperScaleForGl(view),
|
||||
textScaleForGl())`. `paperScaleForGl` (`PlanView.tsx:475-476`):
|
||||
```ts
|
||||
const paperScaleForGl = (v: ViewBox): number =>
|
||||
paperScaleRef.current ?? scaleFromView(v, svgRef.current) ?? 100;
|
||||
```
|
||||
**Kein `hairline`-Zweig.** Egal ob `lineMode` "display" oder "print" ist,
|
||||
hier kommt immer ein echter 1:N-Papier-Nenner heraus.
|
||||
- Rust-Seite (`gpu.rs:606-616`):
|
||||
```rust
|
||||
let mm_px = mm_to_device_px(view_box, vw, vh, self.paper_scale_n);
|
||||
```
|
||||
`mm_to_device_px` (`ortho.rs:96-99`) reproduziert exakt `printStrokeVb`:
|
||||
`(paper_scale_n/1000.0) * PX_PER_M * meet` — `PX_PER_M = 90.0`
|
||||
(`tessellate.rs:22`, identisch zu TS). Die reine Mathematik ist also
|
||||
deckungsgleich zum SVG-Print-Pfad (2b) — aber sie läuft **immer**, auch wenn
|
||||
der Nutzer "Display: Haarlinien" gewählt hat. Siehe Finding 1.
|
||||
- Stiftbreite pro Batch im Shader (`shaders.rs:91`, `LINE_WGSL`):
|
||||
`width_px = max(0.6, stroke_px * stroke_scale) * miter` — harte Untergrenze
|
||||
0.6 Geräte-px, die weder die SVG- noch die PDF-Seite kennt (Finding 6).
|
||||
Identische Formel für Bögen (`shaders.rs:209`, `ARC_WGSL`) und im
|
||||
WebGL2-Pfad (`glPlanRender.ts:243-249`, Kommentar "klemmt bei ~0.6 px").
|
||||
- Schraffur-Musterlinien laufen NICHT über `mm_px`, sondern über
|
||||
`width_screen`/`px_per_screen` (`gpu.rs:661-665`): Geräte-px = Breite ×
|
||||
`meet`-Skala statt × mm→px — bewusst zoom-skalierend wie das SVG-`<pattern>`,
|
||||
nicht papierkonstant (s. `toRenderScene.ts:371-375`, Finding 4).
|
||||
|
||||
### 2d. Vektor-PDF (`exportPdf.ts` → `sceneToPrintSvg.ts`)
|
||||
|
||||
- `exportPdf.ts:73`: `const scene = planToRenderScene(plan);` — dieselbe
|
||||
Szene wie 2c, unabhängig vom aktuell gewählten `lineMode` der Ansicht.
|
||||
- `sceneToPrintSvg.ts:243-244`:
|
||||
```ts
|
||||
const effectiveMm = widthScreen ? (widthMm / PX_PER_M) * mmPerM : widthMm;
|
||||
const strokeMm = quantizePen(effectiveMm);
|
||||
```
|
||||
`PEN_STEPS = [0.13, 0.18, 0.25, 0.35, 0.5, 0.7, 1.0]`, `MIN_PEN_MM = 0.13`
|
||||
(`sceneToPrintSvg.ts:44,47`). JEDE Linie/Umriss/Bogen/Schraffur wird auf die
|
||||
nächsthöhere Stufe gerundet, bevor sie ins SVG/PDF geht
|
||||
(`sceneToPrintSvg.ts:251,274,301`). Das ist der EINZIGE der vier Pfade, der
|
||||
überhaupt quantisiert.
|
||||
- `widthScreen`-Konvertierung: `widthMm` (hier eigentlich viewBox-Einheiten,
|
||||
siehe 2c) wird über `/PX_PER_M * mmPerM` zurück in Weltmeter und dann in
|
||||
Papier-mm beim GEWÄHLTEN `opts.scaleDenominator` übersetzt — nicht beim
|
||||
Massstab, den die Live-Ansicht gerade zeigt (Finding 4).
|
||||
- Text: `sizeMm` kommt unverändert aus `RText.sizeMm`
|
||||
(`sceneToPrintSvg.ts:325`, `t.sizeMm`, keine Nachskalierung) — aber die
|
||||
VERTIKALE Zeilenposition dieser Texte wurde in `toRenderScene.ts` mit einem
|
||||
festen `STAMP_REF_N = 100` in Weltmeter umgerechnet (`toRenderScene.ts:163-165,
|
||||
481-483`), unabhängig vom tatsächlich gewählten Export-`N` (Finding 3).
|
||||
|
||||
## 3. Findings (nach Schwere geordnet)
|
||||
|
||||
### 1. Haarlinien-Modus (App-Default) wird vom GPU-Renderer komplett ignoriert — betrifft den Standard-Zustand der App
|
||||
|
||||
`lineMode` startet auf `"display"` (`viewSlice.ts:74`), und WebGL2 ist der
|
||||
Default-Renderer (`wantGl` ist `true`, ausser `?gl=0`,
|
||||
`PlanView.tsx:433-437`) — **d. h. im frisch geladenen, unkonfigurierten App-
|
||||
Zustand rendert bereits der GPU-Pfad, und die "Display: Haarlinien"-Option
|
||||
tut nichts.** `paperScaleForGl` (`PlanView.tsx:475-476`) liest nur
|
||||
`paperScaleRef.current ?? scaleFromView(...) ?? 100` — der `hairline`-Boolean
|
||||
aus den Props (`PlanView.tsx:2571`) wird an dieser Stelle nie geprüft, obwohl
|
||||
das benachbarte `textScaleForGl` (`PlanView.tsx:485-488`) exakt diesen Zweig
|
||||
korrekt hat:
|
||||
```ts
|
||||
const textScaleForGl = (): number => {
|
||||
const print = !hairline && paperScale != null && paperScale > 0;
|
||||
return print ? paperScale! : 100;
|
||||
};
|
||||
```
|
||||
Ergebnis: Text (Raumstempel) fällt im Haarlinien-Modus korrekt auf die
|
||||
Referenzskala 100 zurück, aber jede Wand-/Tür-/2D-Zeichenlinie wird trotzdem
|
||||
mit dem echten (gemessenen oder geschätzten) `paperScale` gezeichnet — sie
|
||||
wird beim Reinzoomen dicker statt konstant zu bleiben, exakt das Gegenteil
|
||||
dessen, was der Menüpunkt verspricht ("calm editing", `en.ts:173`). Der Bug
|
||||
betrifft sowohl `useGlPlanRenderer.ts` (Default) als auch
|
||||
`useWasmPlanRenderer.ts` (`?engine=wasm`) — keiner der beiden `render()`-
|
||||
Aufrufe kennt einen `hairline`-Parameter überhaupt.
|
||||
|
||||
### 2. PEN_STEPS-Quantisierung existiert NUR im PDF-Export — beide Live-Vorschauen (SVG-Print und GPU) zeigen unquantisierte Rohwerte
|
||||
|
||||
Konkret, mit den tatsächlichen Konstanten aus `generatePlan.ts`:
|
||||
|
||||
- `LAYER_LINE_MM = 0.13` (`generatePlan.ts:82`), `LAYER_DETAIL_FACTOR.fein =
|
||||
0.7` (`generatePlan.ts:90-93`) → Schichtfuge im Detailgrad "fein":
|
||||
`0.13 · 0.7 = 0.091 mm` (`generatePlan.ts:1022`,
|
||||
`strokeWidthMm: LAYER_LINE_MM * LAYER_DETAIL_FACTOR[detail]`). Die
|
||||
Bildschirm-Print-Vorschau zeigt genau diese 0.091 mm (`printStrokeVb(0.091,
|
||||
N)`); der PDF-Export klemmt via `quantizePen` auf `MIN_PEN_MM = 0.13` mm —
|
||||
**+43 % dicker im Druck als in der Vorschau.**
|
||||
- Türschwenkbogen: `doorLwMm * 0.6` (`generatePlan.ts:902`) mit dem üblichen
|
||||
Fallback `doorLwMm = LAYER_LINE_MM = 0.13` (`generatePlan.ts:354`) ergibt
|
||||
`0.078 mm`. Screen zeigt 0.078 mm (nahezu unsichtbar dünn bei kleinen
|
||||
Massstäben), PDF klemmt auf 0.13 mm — **+67 %.**
|
||||
- Wand-Umriss im Detailgrad "grob": `outlineMm = wallLwMm ·
|
||||
OUTLINE_DETAIL_FACTOR.grob (1.6)` (`generatePlan.ts:84-88, 648, 796`); beim
|
||||
häufigen Fallback `WALL_FALLBACK_MM = 0.18` (`generatePlan.ts:97`) ergibt das
|
||||
`0.288 mm`. `quantizePen(0.288)` rundet auf die nächste Stufe `0.35`
|
||||
(`PEN_STEPS`, da `0.25 < 0.288 ≤ 0.35`) — **+21.5 % im Druck.**
|
||||
|
||||
Das systematische Muster: der Detailgrad "fein" existiert explizit, um
|
||||
Nebenlinien DÜNNER zu machen (`generatePlan.ts:52-58`,
|
||||
`LAYER_DETAIL_FACTOR.fein = 0.7`) — aber sobald der berechnete Wert unter
|
||||
`MIN_PEN_MM` fällt, hebt die PDF-Quantisierung ihn wieder auf die Untergrenze
|
||||
an, ohne dass die Bildschirm-Vorschau (weder SVG-Print noch GPU) davon
|
||||
irgendetwas zeigt. Wer die Druckvorschau am Bildschirm beurteilt, sieht NICHT,
|
||||
was tatsächlich gedruckt wird.
|
||||
|
||||
### 3. Stempel-Zeilenabstand ist nur bei Massstab 1:100 exakt — SVG- und RenderScene-Pfad nutzen zwei verschiedene Formeln für dieselbe Grösse
|
||||
|
||||
Bereits als bekannte Alt-Lücke in HANDOVER.md (Commit `382771b`, Punkt 6)
|
||||
vermerkt; hier präzise verortet:
|
||||
|
||||
- SVG (`PlanView.tsx:2738-2739`):
|
||||
```ts
|
||||
const nRef = print ? paperScale! : 100;
|
||||
const unitPerPt = (1 / 72) * 0.0254 * nRef * PX_PER_M;
|
||||
```
|
||||
`nRef` folgt im Druckmodus dem TATSÄCHLICH aktiven `paperScale` — der
|
||||
Zeilenabstand (`lineGap = baseFs * 1.3`, `PlanView.tsx:2741`) ist für jedes
|
||||
`N` korrekt in viewBox-Einheiten, weil er direkt aus `unitPerPt` (das `N`
|
||||
enthält) abgeleitet wird.
|
||||
- RenderScene (`toRenderScene.ts:163-165`):
|
||||
```ts
|
||||
const STAMP_REF_N = 100;
|
||||
const MM_TO_M = STAMP_REF_N / 1000;
|
||||
```
|
||||
und (`toRenderScene.ts:481-483`):
|
||||
```ts
|
||||
const baseH = baseMm * MM_TO_M; // Basisgröße in Modell-Metern
|
||||
const lineGap = baseH * 1.3;
|
||||
```
|
||||
Hier ist der Umrechnungsfaktor `MM_TO_M` FEST auf `N = 100` verdrahtet,
|
||||
unabhängig davon, mit welchem `N` der WASM-Viewport oder der PDF-Export
|
||||
tatsächlich rendert (`paper_scale_n` in `gpu.rs`, `opts.scaleDenominator` in
|
||||
`sceneToPrintSvg.ts`). Bei `N = 100` sind beide Formeln identisch (das war
|
||||
offenbar der Verifikationsfall in `382771b`); bei jedem anderen `N` — z. B.
|
||||
1:50 — driftet der vertikale Zeilenabstand der Stempel-Mehrzeiler
|
||||
proportional zum Verhältnis `N_wahr/100` auseinander, während die einzelne
|
||||
Zeilenhöhe (`sizeMm`) selbst korrekt bleibt. Betroffen: WASM-Viewport (2c)
|
||||
UND PDF-Export (2d) gleichermassen (beide beziehen `RText` aus derselben
|
||||
`toRenderScene`-Funktion); die WebGL2-Ansicht ist NICHT betroffen, weil sie
|
||||
Text grundsätzlich nicht selbst zeichnet, sondern die SVG-Overlay-Ebene
|
||||
weiterverwendet (`PlanView.tsx:1592`,
|
||||
`useGpuRenderer ? wantWasm || p.kind !== "text" : ...` — bei WebGL2
|
||||
(`wantWasm=false`) werden Text-Primitive NIE aus dem SVG-Rendering
|
||||
herausgefiltert).
|
||||
|
||||
### 4. Schraffur-Strichbreite im PDF hängt vom GEWÄHLTEN Export-Massstab ab, nicht vom Massstab der Live-Vorschau
|
||||
|
||||
`hatchPx` wird in `toRenderScene.ts:374-375` genau einmal, massstabsunabhängig
|
||||
berechnet:
|
||||
```ts
|
||||
const hatchMm = p.hatch.lineWeight > 0 ? p.hatch.lineWeight : 0.13;
|
||||
const hatchPx = Math.max(0.6, hatchMm * (1 / 0.13));
|
||||
```
|
||||
— eine reine viewBox-Grösse (analog zum SVG-`<pattern>`-Strich, der
|
||||
absichtlich mit dem Zoom mitskaliert, damit die Schraffur bei jedem Zoom
|
||||
gleich dicht aussieht: Kommentar `toRenderScene.ts:371-373`). Erst beim
|
||||
Export wird daraus in `sceneToPrintSvg.ts:243`
|
||||
`effectiveMm = (widthMm / PX_PER_M) * mmPerM` — und `mmPerM = 1000 /
|
||||
opts.scaleDenominator` (`sceneToPrintSvg.ts:101`) verwendet den vom
|
||||
Export-Dialog gewählten Massstab, NICHT den `paperScale`, den der Nutzer
|
||||
gerade in der "Print"-Live-Vorschau sieht (`exportPdf.ts` ruft
|
||||
`sceneToPrintSvg` mit `opts.scaleDenominator` aus den Export-Optionen,
|
||||
`exportPdf.ts:61-79`, völlig unabhängig vom `PlanView`-State).
|
||||
|
||||
Beispiel: `lineWeight = 0.13` mm (Standard-Fuge, `getHatch`-Default) →
|
||||
`hatchPx = max(0.6, 0.13 · (1/0.13)) = 1.0` viewBox-Einheiten.
|
||||
- Export bei 1:50 (`mmPerM = 20`): `effectiveMm = (1.0/90)·20 = 0.222 mm` →
|
||||
`quantizePen` → **0.25 mm**.
|
||||
- Export bei 1:200 (`mmPerM = 5`): `effectiveMm = (1.0/90)·5 = 0.056 mm` →
|
||||
auf `MIN_PEN_MM` geklemmt → **0.13 mm**.
|
||||
|
||||
Fast die doppelte Strichstärke allein durch die Wahl des Export-Massstabs,
|
||||
bei UNVERÄNDERTER Quell-`lineWeight` — und ohne dass die Bildschirm-
|
||||
"Print"-Vorschau (die ja mit dem live gemessenen `paperScale` arbeitet,
|
||||
nicht mit `opts.scaleDenominator`) das anzeigen könnte, solange beide Werte
|
||||
nicht zufällig übereinstimmen.
|
||||
|
||||
### 5. Dieselbe Formel existiert vierfach, nur durch Kommentare (nicht durch Typen/Code) synchron gehalten
|
||||
|
||||
`PX_PER_M = 90` ist unabhängig deklariert in: `PlanView.tsx:23` (SVG),
|
||||
`glPlan/glPlanRender.ts:12` (WebGL2), `tessellate.rs:22` (Rust, geteilt
|
||||
zwischen WASM-Viewport und Headless/Golden-Test) und `sceneToPrintSvg.ts:64`
|
||||
(PDF-Serializer, mit explizitem Kommentar "MUSS mit dem dortigen
|
||||
uebereinstimmen, sonst driftet die Schraffur-Dichte des PDFs vom Viewport
|
||||
ab", `sceneToPrintSvg.ts:61-62`). Die Hatch-Dichte-Formel
|
||||
`Math.max(0.6, weightMm · (1/0.13))` ist separat dupliziert in
|
||||
`PlanView.tsx:2344-2345` (`hatchStrokePx`, SVG-`<pattern>`) und
|
||||
`toRenderScene.ts:375` (`hatchPx`, GPU + PDF) — beide mit demselben
|
||||
"magischen" Faktor `1/0.13`, ohne gemeinsame Konstante. Aktuell sind alle vier
|
||||
Kopien konsistent; das Risiko ist rein prospektiv (nächste Tuning-Änderung an
|
||||
einer Stelle vergisst die anderen drei) — aber genau das ist der
|
||||
Mechanismus, über den Findings wie 1-4 überhaupt erst entstehen können, ohne
|
||||
dass ein Typfehler oder Test anschlägt.
|
||||
|
||||
### 6. Drei unabhängige, nicht aufeinander abgestimmte Mindestbreiten-Politiken
|
||||
|
||||
- SVG-Print: `Math.max(1e-4, ...)` (`PlanView.tsx:2599`) — praktisch keine
|
||||
Untergrenze, verlässt sich auf das Antialiasing des Browsers.
|
||||
- GPU (WebGL2 UND WASM, Linien UND Bögen): harter Floor von **0.6
|
||||
Geräte-Pixel** (`shaders.rs:91,209`; `glPlanRender.ts:243-249`,
|
||||
Kommentar "klemmt bei ~0.6 px").
|
||||
- PDF: **`MIN_PEN_MM = 0.13` mm** (`sceneToPrintSvg.ts:38` in
|
||||
`planToPrintSvg.ts`, äquivalent `sceneToPrintSvg.ts:47`) — eine
|
||||
Papier-mm-Grösse, kein Pixelwert, konzeptionell etwas anderes als die
|
||||
GPU-Pixel-Untergrenze.
|
||||
|
||||
Auswirkung: bei sehr kleinem Massstab (weit rausgezoomt oder grosses `N`,
|
||||
z. B. Übersichtsplan 1:500) hält der GPU-Pfad sehr dünne Linien künstlich bei
|
||||
0.6 px sichtbar, während dieselbe Linie im SVG-Pfad fast verschwindet und im
|
||||
PDF auf eine ganz andere (mm-basierte, massstabsabhängige) Grösse geklemmt
|
||||
wird. Kein Pfad kennt die Politik der anderen beiden. Niedrigere Priorität
|
||||
als 1-4, weil hier keine der drei Politiken die *exportierte* Papier-mm-Wahrheit
|
||||
verändert (die bleibt PDF-exklusiv über `MIN_PEN_MM`) — es geht nur um
|
||||
Bildschirm-Konsistenz zwischen SVG/GPU bei Extremzoom.
|
||||
|
||||
### 7. Toter Zweitpfad `planToPrintSvg.ts` dupliziert PEN_STEPS/mm-Logik komplett, unbenutzt aber vorhanden
|
||||
|
||||
`src/export/planToPrintSvg.ts` trägt seit `382771b` einen Kopfkommentar
|
||||
"ERSETZT durch sceneToPrintSvg.ts … wird vom PDF-Export NICHT MEHR
|
||||
verwendet" (`planToPrintSvg.ts:1-6`) und ist tatsächlich nirgends mehr
|
||||
importiert (verifiziert per Grep über `src/`). Er enthält jedoch weiterhin
|
||||
eine eigene, unabhängige Kopie von `PEN_STEPS`/`MIN_PEN_MM`/`quantizePen`
|
||||
(`planToPrintSvg.ts:35,38,40-45`) und einer kompletten Plan→SVG-Serialisierung
|
||||
inkl. eigener `PX_PER_M`-Handhabung (`planToPrintSvg.ts:326`). Kein aktiver
|
||||
Bug, aber eine Falle: Copy-Paste-Wiederverwendung dieses Altpfads (z. B. für
|
||||
einen zukünftigen DXF/PNG-Export) würde eine dritte, potenziell abweichende
|
||||
Quantisierungs-Tabelle in den Baum ziehen.
|
||||
|
||||
## 4. Vorgeschlagene Fixes
|
||||
|
||||
### zu Finding 1 (Haarlinien-Modus im GPU-Pfad)
|
||||
|
||||
Kleinste, korrekte Lösung: den `hairline`-Zustand als eigenen Parameter bis
|
||||
in den Shader durchreichen, analog zu `paper_scale_n`/`text_scale_n`.
|
||||
|
||||
- **Rust (`src-tauri/render2d/src/gpu.rs`)**: neues Feld
|
||||
`pub hairline: bool` auf `Renderer` (Default `false`, neben `paper_scale_n`
|
||||
bei `gpu.rs:422`). In `Renderer::render` (`gpu.rs:606-616`): wenn
|
||||
`self.hairline`, `mm_px` NICHT aus `mm_to_device_px(...)` berechnen, sondern
|
||||
auf einen konstanten Geräte-px-Wert setzen (z. B. `1.0`), UND dafür sorgen,
|
||||
dass der Shader `width_mm` in diesem Fall ignoriert (sonst bleibt die
|
||||
RELATIVE Differenz zwischen z. B. 0.13 mm und 0.35 mm bestehen, nur global
|
||||
skaliert — nicht das gewünschte "alle Linien exakt 1 px"). Sauberster Weg:
|
||||
ein neues `hairline: u32`-Feld in die pro-Frame-Uniform (`Globals`,
|
||||
`shaders.rs:20-23` und `ArcGlobals`), im Fragment-/Vertex-Shader
|
||||
(`shaders.rs:91`, `LINE_WGSL`; `shaders.rs:209`, `ARC_WGSL`) per
|
||||
`select(...)` auf einen festen `1.0`-px-Wert umschalten statt
|
||||
`max(0.6, stroke_px * stroke_scale)`.
|
||||
- **`src-tauri/render2d/src/web.rs`**: neue Methode `set_hairline(&mut self,
|
||||
on: bool)` neben `set_paper_scale`/`set_text_scale`
|
||||
(`web.rs:146-155`-Nachbarschaft).
|
||||
- **`src/plan/useWasmPlanRenderer.ts`**: `render()`
|
||||
(`useWasmPlanRenderer.ts:131-155`) um einen `hairline: boolean`-Parameter
|
||||
erweitern, `r.set_hairline(hairline)` vor `r.render()` aufrufen.
|
||||
- **`src/plan/useGlPlanRenderer.ts`**: analog — `render()`
|
||||
(Signatur aktuell `(viewBox, paperScaleN=100)`) um `hairline` erweitern;
|
||||
im WebGL2-Shader-Uniform-Pfad (`glPlanRender.ts:159-165,213-215`)
|
||||
`mmToDevicePx` bei `hairline===true` durch einen konstanten Wert ersetzen
|
||||
und im Fragment-Shader denselben `select`-Trick wie oben anwenden
|
||||
(`glPlanShaders.ts`, dort wo "~0.6 px"-Klemmung passiert, siehe Finding 6).
|
||||
- **`src/plan/PlanView.tsx`**: `paperScaleForGl` (`PlanView.tsx:475-476`)
|
||||
bleibt wie sie ist (wird weiter für den Massstab-Nenner gebraucht, sobald
|
||||
der Nutzer zurück auf "Print" schaltet); stattdessen an allen drei
|
||||
`renderGl(...)`-Aufrufstellen (`PlanView.tsx:494,502,1080`) zusätzlich
|
||||
`hairline` durchreichen: `renderGl(view, paperScaleForGl(view),
|
||||
textScaleForGl(), hairline)`.
|
||||
|
||||
### zu Finding 2 (PEN_STEPS nur im PDF)
|
||||
|
||||
Zwei mögliche Richtungen, je nach gewünschtem Produktverhalten:
|
||||
|
||||
- **Option A (Vorschau matcht Druck)**: `quantizePen`
|
||||
(`sceneToPrintSvg.ts:50-54`) in ein gemeinsames Modul auslagern (z. B.
|
||||
`src/plan/penSteps.ts`, exportiert `PEN_STEPS`, `MIN_PEN_MM`,
|
||||
`quantizePen`), von `sceneToPrintSvg.ts` UND von `PlanView.tsx`
|
||||
(`printStrokeVb`, `PlanView.tsx:2598-2600`) importieren und dort ebenfalls
|
||||
anwenden: `printStrokeVb(mm, n) = Math.max(1e-4, (quantizePen(mm) * n) /
|
||||
1000) * PX_PER_M`. Für den GPU-Pfad müsste die Quantisierung dann VOR dem
|
||||
Scene-Bau passieren (in `toRenderScene.ts`, auf `widthMm` jeder
|
||||
`RLine`/`ROutline`/`RPolyline`/`RArc`, ausgenommen `widthScreen`-Einträge),
|
||||
damit WASM/WebGL2-Viewport dieselben Stufen zeigen wie SVG und PDF.
|
||||
- **Option B (Vorschau bleibt Rohgrösse, aber sichtbar gemacht)**: falls die
|
||||
feinkörnige Vorschau bewusst erhalten bleiben soll, zumindest den
|
||||
Stiftstufen-Sprung an der Stelle sichtbar machen, an der die Detailgrad-
|
||||
Faktoren definiert werden (`generatePlan.ts:84-93`) — z. B. ein
|
||||
Entwickler-/Lint-Test, der bei jedem `OUTLINE_DETAIL_FACTOR`/
|
||||
`LAYER_DETAIL_FACTOR`-Wert prüft, ob das Produkt mit den üblichen
|
||||
Fallback-`weightMm`-Werten (`WALL_FALLBACK_MM`, `LAYER_LINE_MM`) exakt auf
|
||||
einer `PEN_STEPS`-Stufe landet, und sonst warnt.
|
||||
|
||||
Empfehlung: Option A — sie erfüllt den Nordstern wörtlich ("Bildschirm bei
|
||||
jedem Massstab = Druck").
|
||||
|
||||
### zu Finding 3 (STAMP_REF_N)
|
||||
|
||||
In `toRenderScene.ts` `STAMP_REF_N = 100` (`toRenderScene.ts:163`) durch den
|
||||
tatsächlichen Ziel-Massstab ersetzen. `planToRenderScene(plan)` kennt aktuell
|
||||
keinen `N`-Parameter (`exportPdf.ts:73` ruft es ohne Massstabsangabe auf) —
|
||||
die Funktion müsste ein optionales `paperScaleN`-Argument bekommen (Default
|
||||
100, um den Viewport-Aufruf ohne Massstabskontext — `nativeSync.ts` — nicht
|
||||
zu brechen, dort ist 100 ohnehin die richtige Referenz für den WASM-Viewport
|
||||
im Anzeigemodus, s. `gpu.rs:422`), und `exportPdf.ts:73` müsste
|
||||
`planToRenderScene(plan, opts.scaleDenominator)` aufrufen. `MM_TO_M`
|
||||
(`toRenderScene.ts:165`) dann aus diesem Parameter statt aus der Konstante
|
||||
ableiten.
|
||||
|
||||
### zu Finding 4 (Hatch-mm im PDF folgt dem Export-N, nicht dem Vorschau-N)
|
||||
|
||||
Kein Bug im engeren Sinn (die Formel ist in sich konsistent — die Schraffur
|
||||
soll ja pro *gewähltem Blatt-Massstab* eine sinnvolle Dichte haben), aber die
|
||||
Bildschirm-„Print"-Vorschau sollte denselben `N` verwenden, den der
|
||||
Export-Dialog tatsächlich benutzen wird, sonst lügt die Vorschau. Fix:
|
||||
`paperScale` in `PlanView.tsx` beim Öffnen des Export-Dialogs (bzw. der
|
||||
Export-Dialog selbst) mit `opts.scaleDenominator` vorbelegen/synchronisieren,
|
||||
statt zwei unabhängige State-Quellen zu pflegen — Ort: dort, wo der
|
||||
Export-Dialog `scaleDenominator` initialisiert (App.tsx, PDF-Export-UI) einen
|
||||
Default aus `paperScale`/`liveScale` (`App.tsx`, `onScale`-Callback,
|
||||
`PlanView.tsx:794`) übernehmen.
|
||||
|
||||
### zu Finding 5 (vierfache Formel-Duplikation)
|
||||
|
||||
Eine gemeinsame TS-Konstantendatei `src/plan/renderConstants.ts` mit
|
||||
`PX_PER_M = 90`, `HATCH_DENSITY_FACTOR = 1/0.13`, `HATCH_MIN_PX = 0.6`
|
||||
anlegen; `PlanView.tsx`, `glPlan/glPlanRender.ts`, `toRenderScene.ts`,
|
||||
`sceneToPrintSvg.ts` importieren daraus statt eigener Literale. Für die
|
||||
Rust-Seite (`tessellate.rs:22`, `shaders.rs:142`) ist echtes Teilen über die
|
||||
Sprachgrenze hinweg nicht trivial — dort bleibt nur ein Kommentar-Link auf die
|
||||
TS-Konstante plus ein Parity-Test (siehe Abschnitt 5), der bei Abweichung
|
||||
fehlschlägt statt bei stillem Drift.
|
||||
|
||||
### zu Finding 6 (drei Mindestbreiten-Politiken)
|
||||
|
||||
Niedrige Priorität, aber falls angegangen: den 0.6-px-Floor aus den Shadern
|
||||
(`shaders.rs:91,209`, `glPlanShaders.ts`) als benannte Konstante
|
||||
exportieren/dokumentieren und explizit von `1e-4` (SVG) und `MIN_PEN_MM=0.13`
|
||||
(PDF) abgrenzen — mindestens per Kommentar klarstellen, dass die drei
|
||||
UNTERSCHIEDLICHE Zwecke haben (GPU: Pixel-Sichtbarkeits-Floor;
|
||||
PDF: Papier-mm-Stiftgrenze) und NICHT synchronisiert werden müssen, damit
|
||||
niemand versehentlich versucht, sie anzugleichen und dabei die jeweils
|
||||
andere Semantik bricht.
|
||||
|
||||
### zu Finding 7 (toter Pfad)
|
||||
|
||||
`src/export/planToPrintSvg.ts` entfernen (nach kurzer Rücksprache, ob der
|
||||
Kopfkommentar-Vermerk "bleibt nur als Referenz stehen" noch gewollt ist) oder,
|
||||
falls er als Referenz bleiben soll, seine `PEN_STEPS`/`quantizePen`-Kopie
|
||||
durch einen Import aus der in Finding 2 vorgeschlagenen gemeinsamen
|
||||
`penSteps.ts` ersetzen.
|
||||
|
||||
## 5. Verifikations-Rezept
|
||||
|
||||
Was schon existiert:
|
||||
|
||||
- `scripts/probe-engine-parity.mjs` vergleicht `?gl=0` gegen `?engine=wasm`
|
||||
rein visuell (Screenshot-Diff von Auge, `probe-parity-svg.png` vs.
|
||||
`probe-parity-wasm.png`) — prüft NICHT quantitativ, ob Strichbreiten in
|
||||
Pixeln übereinstimmen, und deckt weder den Haarlinien-Modus noch den
|
||||
WebGL2-Default-Pfad noch den PDF-Export ab.
|
||||
- `src-tauri/render2d/tests/golden.rs` vergleicht die Demo-Szene
|
||||
Pixel-für-Pixel gegen `tests/golden/demo.png` (Toleranz: Kanal-Delta > 2 auf
|
||||
< 0.5 % der Pixel, `docs/design/engine-headless.md`) — ein reiner
|
||||
GPU-Regressionstest, ohne SVG- oder PDF-Vergleich.
|
||||
- Die einzige bisher dokumentierte mm-genaue Messung ist manuell:
|
||||
HANDOVER.md, Commit `382771b` — `pdftoppm`-Messung eines exportierten PDFs
|
||||
(53.51×43.69 mm gegen erwartete 53.45×43.45 mm bei 1:100), einmalig, nicht
|
||||
als wiederholbares Skript hinterlegt.
|
||||
|
||||
Um den Nordstern ("Bildschirm bei jedem Zoom/Massstab = Druck") tatsächlich
|
||||
beweisbar zu machen, fehlt ein quantitativer End-to-End-Test, der:
|
||||
|
||||
1. Für eine feste Test-Szene (idealerweise die bestehende `demo::demo_scene`
|
||||
aus `src-tauri/render2d/src/demo.rs`, die bereits sowohl einen `width_mm`-
|
||||
als auch einen `width_screen`-Strich enthält, `demo.rs:34,49-50`) bei
|
||||
mehreren `N` (1:10, 1:50, 1:100) UND mehreren Zoomstufen:
|
||||
- den PDF-Export erzeugt und via `pdftoppm -r <dpi>` in ein PNG rendert,
|
||||
dort die Strichbreite in Pixeln misst und in mm zurückrechnet (bekannte
|
||||
DPI ⇒ bekannte mm/px);
|
||||
- denselben Frame headless über `HeadlessRenderer::render_to_image`
|
||||
(`src-tauri/render2d/src/headless.rs`) mit identischem `paper_scale_n`
|
||||
rendert und dieselbe Pixel-Breite misst;
|
||||
- beide mm-Werte gegen den erwarteten `mm · N/1000`-Sollwert UND
|
||||
gegeneinander vergleicht, mit einer Toleranz, die die PEN_STEPS-Rundung
|
||||
(Finding 2, sobald behoben: siehe Option A) einbezieht.
|
||||
2. Den Haarlinien-Modus separat abdeckt: ein Screenshot-Vergleich bei zwei
|
||||
verschiedenen Zoomstufen im `lineMode:"display"` — die gemessene
|
||||
Pixelbreite MUSS bei beiden Zoomstufen identisch sein (Beweis, dass
|
||||
Finding 1 behoben ist), sowohl für WebGL2 (`?gl` ohne `=0`, App-Default)
|
||||
als auch für WASM (`?engine=wasm`).
|
||||
3. `probe-engine-parity.mjs` um eine tatsächliche Pixel-Differenz-Metrik
|
||||
erweitert (statt nur Kindanzahl/Warnungen zu loggen wie aktuell
|
||||
`probe-engine-parity.mjs:36-44`) — z. B. eine bekannte Referenzlinie im
|
||||
Testmodell an fester Bildschirmposition, deren Strichbreite in beiden
|
||||
Screenshots per Pixel-Sampling gemessen und verglichen wird.
|
||||
|
||||
Bis dieser Test existiert, bleibt jede Aussage über "Bildschirm = Druck" eine
|
||||
Behauptung, die nur durch manuelles `pdftoppm`-Nachmessen einzelner
|
||||
Stichproben gedeckt ist — die in diesem Audit gefundenen Divergenzen (v. a.
|
||||
Finding 1 und 2) hätten mit den heutigen Probes nicht auffallen können, weil
|
||||
keiner von ihnen den Default-Zustand der App (`lineMode:"display"`, WebGL2)
|
||||
gegen den Druckpfad misst.
|
||||
@@ -0,0 +1,186 @@
|
||||
# Schnitt/Ansicht-Pipeline: analytische Prismen-Projektion (`render3d::section`)
|
||||
|
||||
> Gegenstueck zum OCCT-WASM-HLR-Spike (`docs/welle-c-hlr-spike/FEASIBILITY.md`):
|
||||
> statt eines generischen CAD-Kernels nutzt dieser Ansatz eine Invariante des
|
||||
> Modells, um Schnitt/Ansicht rein analytisch (kein Hidden-Line-Removal-Solver)
|
||||
> zu berechnen. Implementiert in `src-tauri/render3d/src/section.rs`.
|
||||
|
||||
## Ansatz
|
||||
|
||||
Jedes Bauteil in diesem Modell ist ein **Prisma**: ein 2D-Grundriss-Fussabdruck-
|
||||
Polygon (Wand-Band bzw. Decken-Umriss, siehe `mesh.rs`), konstant extrudiert
|
||||
ueber ein Hoehenintervall `[z0, z1]`. Diese Einschraenkung — konstanter
|
||||
Querschnitt ueber die gesamte Hoehe — macht die Schnittgeometrie trivial im
|
||||
Vergleich zu generischem HLR:
|
||||
|
||||
- **Schnitt einer vertikalen Ebene mit einem Prisma** = 2D-Geraden/Polygon-
|
||||
Clipping IM GRUNDRISS (die Schnittebene projiziert im Grundriss auf eine
|
||||
Linie) → ein oder mehrere u-Intervalle, in denen die Linie das Fussabdruck-
|
||||
Polygon durchquert. Jedes Intervall × `[z0, z1]` ist das Cut-Rechteck. Da die
|
||||
Hoehe unabhaengig von der Grundriss-Position ist, ist das Ergebnis **immer
|
||||
ein Rechteck**, nie ein Trapez — auch bei diagonalen Wandachsen.
|
||||
- **Projektion/Ansicht** (Kanten hinter der Ebene, in Blickrichtung) reduziert
|
||||
sich auf ein **2D-Sichtbarkeitsproblem im Grundriss** kombiniert mit einem
|
||||
Hoehen-Ueberlappungstest: da alle Seitenflaechen der Prismen vertikal sind,
|
||||
genuegt ein Sichtstrahl-Test im Grundriss (verdeckt ein naeheres Prisma-
|
||||
Fussabdruckpolygon die Sichtlinie?) plus Ueberlappung der Hoehenintervalle.
|
||||
|
||||
Das ersetzt einen 66-MB-WASM-CAD-Kernel durch closed-form Arithmetik in reinem
|
||||
Rust, ohne zusaetzliches Laufzeitgewicht ueber `render3d` hinaus.
|
||||
|
||||
## `SectionOutput`-Format
|
||||
|
||||
Modul: `render3d::section`. Alle Groessen in **Metern**.
|
||||
|
||||
```rust
|
||||
pub struct SectionPlane { pub point: [f32; 3], pub normal: [f32; 3] }
|
||||
|
||||
pub struct SectionOutput {
|
||||
pub cut_polygons: Vec<CutPolygon>, // JSON: "cutPolygons"
|
||||
pub visible_edges: Vec<SectionEdge>, // JSON: "visibleEdges"
|
||||
pub hidden_edges: Vec<SectionEdge>, // JSON: "hiddenEdges"
|
||||
}
|
||||
|
||||
pub struct CutPolygon { pub component: ComponentRef, pub color: Rgb, pub pts: Vec<[f32; 2]> }
|
||||
pub struct SectionEdge { pub component: ComponentRef, pub a: [f32; 2], pub b: [f32; 2] }
|
||||
pub struct ComponentRef { pub kind: ComponentKind /* Wall | Slab */, pub index: usize }
|
||||
```
|
||||
|
||||
**Koordinatensystem der Ausgabe (u, v):**
|
||||
|
||||
- Ursprung: `SectionPlane::point`, projiziert.
|
||||
- `u` (horizontal): Strecke entlang der Schnittebene, senkrecht zur
|
||||
Blickrichtung, berechnet als `normalize(cross(normal, world_up))` — dieselbe
|
||||
rechtshaendige Konvention wie `math::look_at`s `right`-Vektor. Bei den vier
|
||||
Standard-Konstruktoren (`looking_plus_x/minus_x/plus_y/minus_y`) ist `u`
|
||||
direkt die jeweils andere Grundriss-Achse.
|
||||
- `v` (vertikal, "Hoehe"): `v = world.y`, ABSOLUT. Diese Codebasis ist
|
||||
durchgaengig Y-up (`types.rs`/`mesh.rs`/`math.rs`: „world.y = Hoehe"); `v`
|
||||
folgt bewusst dieser etablierten Konvention statt einer wortwoertlichen
|
||||
„world.z"-Lesart, um modulübergreifend konsistent zu bleiben.
|
||||
- Nur **vertikale** Schnittebenen (Normale ohne Hoehen-Komponente) sind
|
||||
unterstuetzt — Grundriss-/Horizontalschnitte bleiben Sache der bestehenden
|
||||
2D-Plan-Pipeline.
|
||||
|
||||
`cut_polygons` sind bei diesem Modell immer Rechtecke (siehe oben), aber als
|
||||
generischer Punktering abgelegt — kompatibel zu `render2d::types::FillPolygon`
|
||||
(`pts: Vec<Point>`), dem vorgesehenen Zielformat fuer die spaetere 1:1-
|
||||
Uebersetzung in die Plan-/Schnitt-Ansicht.
|
||||
|
||||
## Oeffnungen (Tueren/Fenster)
|
||||
|
||||
`WallInput` hat seit diesem Nachtrag ein Feld `openings: Vec<Opening>`
|
||||
(`types.rs`), unabhaengig von der reinen Achse/Dicke/Hoehe: je Oeffnung ein
|
||||
Intervall entlang der Wandachse (`from`/`to`, Meter ab `start`) plus Bruestungs-
|
||||
und Kopfhoehe (`sill`/`height`, relativ zu `base_elevation`). Eine Tuer ist der
|
||||
Sonderfall `sill == 0.0`.
|
||||
|
||||
**Cut-Polygone.** Trifft die Schnittlinie eine Wand exakt im Bereich einer
|
||||
Oeffnung, splittet `wall_cut_rectangles` das sonst einzelne Vollrechteck
|
||||
`[z0,z1]` in bis zu ZWEI Teil-Rechtecke: Bruestung `[z0, z0+sill]` und Sturz
|
||||
`[z0+sill+height, z1]`. Gewaehlt wurde diese Mehrfach-Rechteck-Darstellung
|
||||
BEWUSST gegenueber einem Loch-Polygon (Ring mit Aussparung): mehrere einfache,
|
||||
konvexe Ringe sind direkt kompatibel zum Zielformat `render2d::types::
|
||||
FillPolygon` (nur einfache Ringe, kein Loch-Format), waehrend ein
|
||||
Loch-Polygon eine zusaetzliche Datenstruktur (Ring-mit-Loch) noetig gemacht
|
||||
haette, die die Zielstruktur (noch) nicht kennt. Ausserhalb einer Oeffnung
|
||||
bleibt es beim einzelnen Vollrechteck (Regressionsfall).
|
||||
|
||||
Die Zuordnung "welche Wandachsen-Position entspricht dieser Schnittposition"
|
||||
ist fuer den STANDARDFALL (Schnittebene senkrecht zur Wandachse — der uebliche
|
||||
architektonische Wandquerschnitt) EXAKT konstant ueber das Cut-Intervall. Fuer
|
||||
schraege Wand/Ebene-Kombinationen wird sie als AFFINE Funktion (`axis_map`)
|
||||
exakt mitgefuehrt (keine Naeherung noetig, da die Abbildung linear ist).
|
||||
|
||||
**Projektion/Ansicht.** Eine Wand mit Oeffnungen bekommt zusaetzlich zur
|
||||
Boxen-Drahtsilhouette die vier Rahmenkanten jeder Oeffnung (zwei Leibungen,
|
||||
Sturz, Bruestung), berechnet auf der Wandachsen-Mittellinie (dieselbe
|
||||
Vereinfachung wie die Bounding-Box-Verdeckung generell — keine eigene
|
||||
Dicken-Aufloesung der Leibungsflaeche). Fuer die Verdeckung wird jede
|
||||
Wand-Bounding-Box um ihre Oeffnungen als "Loch" reduziert
|
||||
(`PrismBounds::opening_voids`): ein anderes (oder dasselbe) Bauteil hinter der
|
||||
Wand wird in genau dem (u, Hoehe)-Rechteck der Oeffnung NICHT von dieser Wand
|
||||
verdeckt — "Durchblick". Die eigenen Rahmenkanten einer Oeffnung werden dabei
|
||||
NICHT gegen das eigene Bauteil auf Verdeckung geprueft (ein Loch kann sich
|
||||
nicht selbst verdecken); gegen alle anderen Bauteile gilt die normale
|
||||
Verdeckungslogik unveraendert (inkl. der bereits dokumentierten
|
||||
Selbstverdeckung der Rueckseite eines ANDEREN Bauteils durch dessen eigene
|
||||
Vorderseite).
|
||||
|
||||
GENAUIGKEIT (bewusste Vereinfachung): die u-Zuordnung fuer den Durchblick ist
|
||||
nur DANN aussagekraeftig, wenn die Wandachse hinreichend parallel zur u-Achse
|
||||
der Schnittebene steht — das ist GENAU der Fall, in dem man die Wand als
|
||||
Elevation/Ansicht von vorne sieht (und ein Fenster ueberhaupt als Durchblick
|
||||
sichtbar waere). Steht die Wand naeher an "senkrecht zur u-Achse" (Wand auf
|
||||
Kante gesehen bzw. der reine Cut-Fall), wird die Durchblick-Berechnung
|
||||
uebersprungen und die Wand bleibt fuer die Verdeckung VOLL UNDURCHSICHTIG
|
||||
(konservativ hidden) — Schwelle `AXIS_ALIGN_EPS = 1e-3` in `section.rs`. Ein
|
||||
Kante-auf-Kante gesehenes Fenster liefert ohnehin keine sinnvolle
|
||||
Durchblick-Flaeche in der Projektion.
|
||||
|
||||
Getestet in `section::tests` (u. a. `schnitt_durch_fenster_liefert_bruestung_
|
||||
und_sturz`, `schnitt_neben_dem_fenster_liefert_volles_rechteck`,
|
||||
`ansicht_zeigt_fensterrahmen_sichtbar_und_durchblick_bei_dahinterliegender_
|
||||
kante`) und im Beweis-SVG (`examples/section_svg.rs`): Schenkel A der L-Wand
|
||||
hat dort ein Fenster, eine kurze zusaetzliche Wand steht dahinter genau im
|
||||
Fensterband und erscheint im SVG als durchgezogene (sichtbare) Linie zwischen
|
||||
Bruestungs- und Sturzhoehe, gestrichelt (verdeckt) darueber/darunter.
|
||||
|
||||
## Bekannte Luecken
|
||||
|
||||
- **Keine Wandknoten-Verschneidung (T-/X-Stoesse).** Wie in `mesh.rs`
|
||||
(M1-Stand) werden Waende als eigenstaendige, stumpf abgeschlossene Quader
|
||||
behandelt, die sich an Ecken UEBERLAPPEN statt sich zu vereinen (kein
|
||||
Miter-Join). Der Schnitt-Extraktor erbt das: an einem L-/T-Knoten kann eine
|
||||
Wand als „in die andere eingebettet" verdeckt erscheinen (im Testmodul
|
||||
`section::tests` bewusst als reales, erwartetes Verhalten dokumentiert und
|
||||
geprueft — kein Bug dieses Moduls, sondern ein Artefakt der fehlenden
|
||||
Verschneidungslogik weiter oben in der Pipeline).
|
||||
- **Oeffnungen sind NICHT in der Vollkoerper-Extrusion (`mesh.rs`)
|
||||
nachgezogen.** `wall_prism`/`wall_cut_rectangles`/die Verdeckung in
|
||||
`section.rs` werten `WallInput::openings` vollstaendig aus (siehe Abschnitt
|
||||
"Oeffnungen" oben), aber `mesh::extrude_wall` extrudiert weiterhin die volle
|
||||
Wandflaeche ohne Aussparung (`mesh.rs`-Moduldoc: „Oeffnungen kommen in
|
||||
spaeteren Milestones"). Cut-/Ansichts-Pipeline und 3D-Solid-Mesh sind bis zum
|
||||
Nachziehen von `mesh.rs` also bewusst inkonsistent — eine bekannte, separate
|
||||
Luecke (nicht Gegenstand dieses Nachtrags).
|
||||
- **Oeffnungs-Durchblick nutzt dieselbe achsparallele Bounding-Box-Naeherung**
|
||||
wie die allgemeine Verdeckung (kein exaktes Polygon-Clipping der Lochflaeche
|
||||
gegen dahinterliegende Kanten) und wird bei Wand-Orientierungen nahe
|
||||
"senkrecht zur u-Achse" konservativ auf "kein Durchblick" zurueckgestuft
|
||||
(siehe Abschnitt "Oeffnungen" oben, `AXIS_ALIGN_EPS`).
|
||||
- **Keine echte Component-/Material-Id.** `WallInput`/`SlabInput` haben aktuell
|
||||
keine eigene Id; `ComponentRef` referenziert daher nur `(Art, Index im
|
||||
Eingabe-Array)` + die rohe Albedo-Farbe als Material-Platzhalter. Sobald ein
|
||||
echtes Ressourcen-/Material-System existiert, sollte `ComponentRef` auf eine
|
||||
stabile Id umgestellt werden (Indizes sind nicht stabil ueber Modell-Edits).
|
||||
- **Verdeckungstest nutzt Bounding-Boxen, nicht die exakte Fussabdruckform.**
|
||||
Fuer Wand-Baender und (i. d. R. konvexe) Deckenumrisse ist das exakt; bei
|
||||
stark konkaven oder diagonalen Grundrissen kann es zu Ueberverdeckung
|
||||
fuehren (ein Prisma blockiert dann auch Bereiche seiner eigenen „Nischen").
|
||||
- **Kein generisches HLR.** Der Ansatz funktioniert NUR, weil alle Bauteile
|
||||
Prismen mit konstantem Querschnitt sind. Fuer echte gekrümmte oder nicht-
|
||||
prismatische Geometrie (Bogenwaende, Freiformdaecher, …) waere er nicht
|
||||
anwendbar — dafuer bliebe der OCCT-Weg (oder eine eigene, generischere
|
||||
HLR-Implementierung) die Referenz.
|
||||
- **Keine robuste Sonderfallbehandlung fuer Vertices exakt auf der
|
||||
Schnittlinie** (Toleranz-basiert, kein Tie-Breaking/Pertubation) — die
|
||||
Testszenarien vermeiden diesen Fall bewusst.
|
||||
|
||||
## Vergleich zum OCCT-WASM-Spike
|
||||
|
||||
| | OCCT-WASM (`docs/welle-c-hlr-spike`) | Dieser Ansatz (`render3d::section`) |
|
||||
|---|---|---|
|
||||
| Verfahren | Generisches HLR (`HLRAppli_ReflectLines`) | Analytische Prismen-Projektion |
|
||||
| Anwendbarkeit | Beliebige BREP-Geometrie | Nur Prismen (konstanter Querschnitt über Höhe) |
|
||||
| Zusaetzliches Gewicht | ~62,8 MB WASM (~19,6 MB gzip), separater Lazy-Chunk | Keins — reines Rust in `render3d`, kein weiteres WASM-Asset |
|
||||
| Modul-Init | ~450 ms (Browser) / ~680 ms (Node) | Kein Initialisierungsschritt (kein Fremd-Modul zu laden) |
|
||||
| Rechenzeit (L-Wand + Platte) | ~21 ms (reiner HLR-Lauf, gemessen im Browser) | Nicht separat gemessen (kein Millisekunden-Timer im Test); die Operationen sind reine Vektor-/Intervall-Arithmetik über wenige Kanten (O(Anzahl-Prismen × Kanten-pro-Prisma) mit kleinen Konstanten) und liegen der Groessenordnung nach klar unter 1 ms fuer Szenen dieser Groesse — eine belastbare Messung steht noch aus |
|
||||
| Sichtbarkeit (verdeckte Kanten) | Exakt (echter HLR-Solver) | Naeherung über Bounding-Boxen im Grundriss + Hoehenintervall-Ueberlappung; exakt fuer achsparallele/konvexe Fussabdruecke |
|
||||
| Reifegrad | Isolierter Spike, nicht verdrahtet | Isolierter Spike (dieses Modul), nicht in `render2d`/die Plan-Ansicht verdrahtet |
|
||||
|
||||
**Fazit:** Fuer den ueberwiegenden Regelfall dieses Projekts (Waende, Decken —
|
||||
alles Prismen) ist die analytische Loesung der pragmatischere Weg: kein
|
||||
zusaetzliches WASM-Gewicht, keine Fremd-Bibliothek, headless testbar wie der
|
||||
Rest von `render3d`. Der OCCT-Weg bleibt die Referenz, falls/sobald echte
|
||||
generische Volumenkoerper (Booleans, gekrümmte Flaechen) ins Modell kommen.
|
||||
@@ -0,0 +1,672 @@
|
||||
# Parametrische Wände
|
||||
|
||||
Status: Implementiert (Phase A — Typ-System und Resolver in `src/model/`, kein UI).
|
||||
Dieses Dokument spezifiziert die **Parametrischen Wände**: regelbasierte Definitionen,
|
||||
die beim Auflösen eine Liste von `Wall`-Elementen erzeugen, anstatt sie einzeln vom
|
||||
Nutzer zeichnen zu lassen.
|
||||
|
||||
Bezugsdokumente: [elements.md](elements.md) (Wand-/Türmodell),
|
||||
[drawing-tools.md](drawing-tools.md) (Werkzeugsystem, Direktzeichnen),
|
||||
[state-architecture.md](state-architecture.md) (Projekt-Store),
|
||||
[resources-graphics.md](resources-graphics.md) (WallType/Component-Auflösung).
|
||||
|
||||
Implementierungsdateien:
|
||||
- `src/model/types.ts` — `ParametricWall`, `ParametricRule` und alle Regel-Varianten.
|
||||
- `src/model/parametricWalls.ts` — `resolveParametricWall()`, `applyRule()` und
|
||||
Hilfsfunktionen.
|
||||
|
||||
---
|
||||
|
||||
## 0. Überblick
|
||||
|
||||
Eine **parametrische Wand** (`ParametricWall`) ist kein festes `Wall`-Element, sondern
|
||||
ein **Regelwerk**, das beim Auflösen (`resolveParametricWall`) eine Menge von `Wall[]`-
|
||||
Elementen generiert. Die erzeugten Wände sind gewöhnliche `Wall`-Objekte; sie
|
||||
unterscheiden sich lediglich in ihrer Herkunft. Das semantische Modell (`Project`)
|
||||
bleibt die einzige Wahrheit — parametrische Wände sind eine Ressource in der
|
||||
Ressourcen-Bibliothek, nicht eine separate Laufzeit-Geometrie-Schicht.
|
||||
|
||||
```
|
||||
Project.parametricWalls: ParametricWall[]
|
||||
│
|
||||
│ resolveParametricWall(pw, floorId, context, defaultWallType)
|
||||
▼
|
||||
Wall[] ──→ normales Rendering über generatePlan / Viewport3D
|
||||
```
|
||||
|
||||
Erzeugte Wände können entweder **temporär** (zur Laufzeit, als Ergänzung zu
|
||||
`project.walls` im Rendering-Pfad) oder **eingebacken** (als `Wall[]` fest in
|
||||
`Project.walls` gespeichert) behandelt werden. Phase A legt nur den Auflöser fest;
|
||||
die Auswahl liegt bei der aufrufenden Komponente.
|
||||
|
||||
---
|
||||
|
||||
## 1. Motivation
|
||||
|
||||
### 1.1 Schnellere Modellierung von Regelgrundrissen
|
||||
|
||||
Schweizer Wohnbauten folgen häufig einem 3-m-Achsraster (SIA-Norm, Modul-/
|
||||
Skelettbauweise). Zwanzig Wände eines Rasters von Hand zu zeichnen ist fehleranfällig
|
||||
und verhindert spätere parametrische Änderungen (z. B. Geschossanzahl, Rasterweite,
|
||||
Wandtyp).
|
||||
|
||||
Eine `GridRule` erzeugt dieses Muster aus wenigen Parametern (Achsabstand, Richtung,
|
||||
Bereich) und lässt sich mit einer einzigen Zahl auf „4-m-Büroraster" umstellen.
|
||||
|
||||
### 1.2 Kongruenz mit FreeCAD BIM / IFC
|
||||
|
||||
FreeCAD BIM kennt **ParametricObjects**, die ihre Geometrie aus Regeln ableiten (z. B.
|
||||
`ArchWall` mit `Length`, `Width`, `Height`). Obwohl das Datenformat hier kein IFC ist,
|
||||
schafft ein ähnliches Abstraktionsniveau eine spätere Brücke: Beim IFC-Export können
|
||||
parametrische Wände als `IfcWallStandardCase` mit konstanten Attributen exportiert
|
||||
werden — kein Informationsverlust gegenüber manuell gezeichneten Wänden.
|
||||
|
||||
### 1.3 Bedingte Wandtypen ohne manuelle Klassifizierung
|
||||
|
||||
Außenwände sind dicker als Innenwände; Trennwände zwischen Einheiten erfordern
|
||||
Schallschutz. Eine `ConditionalThicknessRule` (`condition: "exterior" → thickType`)
|
||||
weist den richtigen Wandtyp automatisch aus der geometrischen Lage zu — ohne dass der
|
||||
Nutzer jeden Wandabschnitt einzeln klassifizieren muss.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architektur
|
||||
|
||||
### 2.1 Typen (`src/model/types.ts`)
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Eine parametrische Wand-Regel — generiert automatisch Wall[]-Einträge für
|
||||
* ein gegebenes Geschoss. Lebt in Project.parametricWalls[].
|
||||
*/
|
||||
export interface ParametricWall {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
/**
|
||||
* Geordnete Liste der anzuwendenden Regeln. Spätere Regeln können die
|
||||
* Ausgabe früherer verfeinern (z. B. Dickenzuweisung nach Raster).
|
||||
*/
|
||||
rules: ParametricRule[];
|
||||
/**
|
||||
* Rückfall-Wandtyp, falls eine Regel keinen eigenen `wallTypeId` nennt.
|
||||
*/
|
||||
defaultWallTypeId: string;
|
||||
}
|
||||
|
||||
/** Diskriminierte Union aller Regel-Varianten. */
|
||||
export type ParametricRule =
|
||||
| GridRule
|
||||
| ModuleRule
|
||||
| ConditionalThicknessRule
|
||||
| ReferenceLineRule
|
||||
| SequenceRule;
|
||||
```
|
||||
|
||||
### 2.2 Einbettung ins Projekt
|
||||
|
||||
```ts
|
||||
export interface Project {
|
||||
// … bestehende Felder …
|
||||
/**
|
||||
* Parametrische Wanddefinitionen (Ressourcen-Bibliothek). Optional, damit
|
||||
* bestehende Projekte/Tests ohne `parametricWalls` gültig bleiben.
|
||||
*/
|
||||
parametricWalls?: ParametricWall[];
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Resolver-Kontext (`src/model/parametricWalls.ts`)
|
||||
|
||||
```ts
|
||||
export interface ParametricContext {
|
||||
/** Das Ziel-Geschoss. */
|
||||
floor: DrawingLevel;
|
||||
/**
|
||||
* Optionale Rasterachsen (Phase C: verlinkter Grid-Ressource). Fehlen sie,
|
||||
* berechnet die Engine die Achsen aus GridRule.spacing.
|
||||
*/
|
||||
gridAxes?: { x: number[]; y: number[] };
|
||||
/**
|
||||
* Optionales Clipping-Polygon (Meter). Fehlt es, reicht das Raster über
|
||||
* einen Standardbereich (0 … spacing × 10).
|
||||
*/
|
||||
boundaryGeometry?: { boundary: Vec2[] };
|
||||
/**
|
||||
* Bereits im Projekt vorhandene Wände des Geschosses. Werden von
|
||||
* refinierenden Regeln (ConditionalThicknessRule, ReferenceLineRule) genutzt.
|
||||
*/
|
||||
existingWalls?: Wall[];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Regel-Varianten
|
||||
|
||||
### 3.1 GridRule — Achsraster
|
||||
|
||||
Erzeugt parallele Wände auf einem gleichmäßigen Raster. Typischer Einsatz: Schweizer
|
||||
Wohnbau-Achsraster (3 m), Büro-Konstruktionsraster (6 m), strukturelle Raster mit
|
||||
fester Stützweite.
|
||||
|
||||
```ts
|
||||
export interface GridRule {
|
||||
type: "grid";
|
||||
/**
|
||||
* Optionaler Verweis auf eine Grid-Ressource (Phase C). Für Phase A wird
|
||||
* stattdessen `spacing` genutzt.
|
||||
*/
|
||||
gridId?: string;
|
||||
/** Rasterabstand in Metern (Default: 3.0). */
|
||||
spacing?: number;
|
||||
/**
|
||||
* Achsrichtungen: „x" = nur Wände entlang der Y-Achse,
|
||||
* „y" = nur Wände entlang der X-Achse, „both" = Vollraster.
|
||||
*/
|
||||
directions: "x" | "y" | "both";
|
||||
/** Optionaler Verweis auf Clipping-Polygon. */
|
||||
boundaryId?: string;
|
||||
/** Optionale Wandtyp-Übersteuerung; sonst defaultWallTypeId. */
|
||||
wallTypeId?: string;
|
||||
/** Lage der Wandachse über die Dicke (Vectorworks-Stil). */
|
||||
referenceLine?: WallReferenceLine;
|
||||
/** Optionale Höhenübersteuerung in Metern; sonst Geschosshöhe. */
|
||||
height?: number;
|
||||
}
|
||||
```
|
||||
|
||||
**Geometrieausgabe (top-down Grundriss):**
|
||||
|
||||
```
|
||||
directions: "x", spacing: 3.0, Bereich 0…12 m:
|
||||
|
||||
y
|
||||
│
|
||||
12 ──────────────────────────
|
||||
│
|
||||
9 ──────────────────────────
|
||||
│
|
||||
6 ──────────────────────────
|
||||
│
|
||||
3 ──────────────────────────
|
||||
│
|
||||
0 ──────────────────────────
|
||||
│
|
||||
└──────────────────────────► x
|
||||
0 12
|
||||
```
|
||||
|
||||
**Wann verwenden:**
|
||||
- Tragende Wände auf fester Stützweite (Wohnbau 3 m, Büro 6 m).
|
||||
- Vollraster (`"both"`) für strukturelle Rastersysteme.
|
||||
- In Kombination mit `ConditionalThicknessRule` zur automatischen Außen/Innen-Klassifizierung.
|
||||
|
||||
**Beispiel: Schweizer 3-m-Wohnraster**
|
||||
|
||||
```ts
|
||||
const pw: ParametricWall = {
|
||||
id: "pw-eg-raster",
|
||||
name: "EG Längswände 3m-Raster",
|
||||
defaultWallTypeId: "wt-innen-15",
|
||||
rules: [
|
||||
{
|
||||
type: "grid",
|
||||
spacing: 3.0,
|
||||
directions: "x", // Wände in X-Richtung (y = 0, 3, 6, 9, 12)
|
||||
wallTypeId: "wt-innen-15",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Auflösung:
|
||||
const walls = resolveParametricWall(pw, "floor-eg", {
|
||||
floor: egFloor,
|
||||
boundaryGeometry: { boundary: rectBoundary(0, 0, 12, 12) },
|
||||
}, defaultWallType);
|
||||
// → 5 Wände bei y = 0, 3, 6, 9, 12, je 12 m lang
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 ModuleRule — Bay-/Jochbauweise
|
||||
|
||||
Unterteilt eine Referenzspanne in gleiche Module und erzeugt Querwände an jedem
|
||||
Teilungspunkt. Typisch für Bürogebäude (6-m-Joch) oder Reihenhäuser mit modularer
|
||||
Erschließung.
|
||||
|
||||
```ts
|
||||
export interface ModuleRule {
|
||||
type: "module";
|
||||
/** Modulmaß in Metern (z. B. 6.0, 3.6). */
|
||||
moduleSize: number;
|
||||
/** Ausrichtung der Trennwände: „x" = Querwände senkrecht zu X, „y" = zu Y. */
|
||||
direction: "x" | "y";
|
||||
/**
|
||||
* Optionaler Verweis auf eine Referenzwand, die die Spannweite definiert.
|
||||
* Fehlt er, wird die Geschoss-Ausdehnung (Bounding-Box) genutzt.
|
||||
*/
|
||||
referenceWallId?: string;
|
||||
/** Optionale Wandtyp-Übersteuerung; sonst defaultWallTypeId. */
|
||||
wallTypeId?: string;
|
||||
referenceLine?: WallReferenceLine;
|
||||
height?: number;
|
||||
}
|
||||
```
|
||||
|
||||
**Wann verwenden:**
|
||||
- Wenn sich Querwände aus einer Referenzspanne (Fassade, Achswand) ergeben.
|
||||
- Vorzug vor `GridRule`, wenn nur in eine Richtung unterteilt wird und eine
|
||||
Referenzwand die Spanne definiert.
|
||||
|
||||
**Beispiel: 6-m-Bay-Bürogebäude**
|
||||
|
||||
```ts
|
||||
const pw: ParametricWall = {
|
||||
id: "pw-buero-joch",
|
||||
name: "Büro 6m-Joch",
|
||||
defaultWallTypeId: "wt-beton-20",
|
||||
rules: [
|
||||
{
|
||||
type: "module",
|
||||
moduleSize: 6.0,
|
||||
direction: "x", // Querwände senkrecht zur X-Achse
|
||||
// referenceWallId: "W-sudfassade" → Spanne aus der Südwand ableiten
|
||||
},
|
||||
],
|
||||
};
|
||||
// resolveParametricWall → Querwände bei x = 6, 12, 18, 24, 30 (bei 36-m-Fassade)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.3 ConditionalThicknessRule — Bedingte Wandtypen
|
||||
|
||||
Weist bereits erzeugten Wänden (aus vorherigen Regeln in der Sequenz) einen anderen
|
||||
Wandtyp zu — abhängig von einer Bedingung. Gibt modifizierte **Kopien** zurück; die
|
||||
Eingabe-Wände werden nicht mutiert.
|
||||
|
||||
```ts
|
||||
export interface ConditionalThicknessRule {
|
||||
type: "conditional-thickness";
|
||||
/**
|
||||
* Bedingung für den Treffer:
|
||||
* • „exterior" — Wand liegt am Außenrand (Bounding-Box des Kontexts).
|
||||
* • „interior" — Wand liegt im Inneren.
|
||||
* • „bearing" — tragende Wand (Heuristikum: Wand läuft ±10° zu X/Y-Achse).
|
||||
* • beliebiger String — benutzerdefiniertes Tag (Phase C: Wall.tags[]).
|
||||
*/
|
||||
condition: "exterior" | "interior" | "bearing" | string;
|
||||
/** Ziel-Wandtyp, der bei Treffer gesetzt wird. */
|
||||
wallTypeId: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Wann verwenden:**
|
||||
- Immer in Kombination mit `GridRule` oder `ModuleRule` (als zweite Regel in
|
||||
`ParametricWall.rules`): Raster erzeugt, Dicke verfeinert.
|
||||
- Wenn Außen- und Innenwände denselben geometrischen Ursprung haben, aber
|
||||
verschiedene Aufbauten benötigen.
|
||||
|
||||
**Beispiel: Außen dick, Innen dünn**
|
||||
|
||||
```ts
|
||||
const pw: ParametricWall = {
|
||||
id: "pw-eg-komplett",
|
||||
name: "EG Vollraster mit Außenwand-Differenzierung",
|
||||
defaultWallTypeId: "wt-innen-15",
|
||||
rules: [
|
||||
{
|
||||
type: "grid", spacing: 3.0, directions: "both",
|
||||
wallTypeId: "wt-innen-15",
|
||||
},
|
||||
{
|
||||
type: "conditional-thickness",
|
||||
condition: "exterior",
|
||||
wallTypeId: "wt-aussen-36", // Außenwände erhalten dicken Aufbau
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 ReferenceLineRule — Wandachsen-Lage
|
||||
|
||||
Setzt `referenceLine` bei passenden Wänden einheitlich (Vectorworks-Stil: Achse
|
||||
links/rechts/mittig). Gibt modifizierte Kopien zurück.
|
||||
|
||||
```ts
|
||||
export interface ReferenceLineRule {
|
||||
type: "reference-line";
|
||||
/** Neue Lage der Wandachse, die einheitlich gesetzt wird. */
|
||||
referenceLine: WallReferenceLine; // "left" | "center" | "right"
|
||||
/**
|
||||
* Filterziel:
|
||||
* • „all" — alle Wände im aktuellen Satz.
|
||||
* • „exterior" — nur Außenwände.
|
||||
* • beliebiger String — benutzerdefiniertes Tag (Phase C).
|
||||
*/
|
||||
target: "all" | "exterior" | string;
|
||||
}
|
||||
```
|
||||
|
||||
**Wann verwenden:**
|
||||
- Außenwände auf `"left"` setzen (Achse liegt auf der Fassadenfläche).
|
||||
- Als abschließende Regel in einer `SequenceRule` nach Raster und Dickenzuweisung.
|
||||
|
||||
---
|
||||
|
||||
### 3.5 SequenceRule — Zusammenfassung von Unterregeln
|
||||
|
||||
Fasst mehrere Regeln als atomare Einheit zusammen. Jede Unterregel erhält die Ausgabe
|
||||
der vorherigen als `existingWalls` — so können spätere Regeln frühere verfeinern.
|
||||
|
||||
```ts
|
||||
export interface SequenceRule {
|
||||
type: "sequence";
|
||||
rules: ParametricRule[];
|
||||
/**
|
||||
* Wenn true: Abbruch nach der ersten Unterregel, die mindestens eine Wand
|
||||
* generiert/verändert hat (Short-Circuit-Fallback).
|
||||
*/
|
||||
stopOnMatch?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**Wann verwenden:**
|
||||
- Um eine zusammengehörige Kombination (Raster → Dicke → Referenzlinie) als
|
||||
Untermodul wiederzuverwenden — z. B. in unterschiedlichen Geschossen mit leicht
|
||||
abweichenden Parametern.
|
||||
|
||||
---
|
||||
|
||||
## 4. Resolver-API (`src/model/parametricWalls.ts`)
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Löst ein ParametricWall-Regelwerk zu einem Wall[]-Array für ein gegebenes
|
||||
* Geschoss auf.
|
||||
*
|
||||
* Ablauf:
|
||||
* 1. Regelwerk sequenziell ausführen; jede Regel erhält die Ausgabe der
|
||||
* vorherigen als existingWalls (ermöglicht Verfeinerung).
|
||||
* 2. Duplikate (gleicher Start-/Endpunkt innerhalb tolerance) entfernen.
|
||||
* 3. Bereinigte Wall[]-Liste zurückgeben.
|
||||
*
|
||||
* Die Ausgabe ist sofort bereit zur Einfügung in project.walls. Es werden
|
||||
* keine Seiteneffekte erzeugt — kein Store, kein Dispatch, kein React.
|
||||
*
|
||||
* @param parametricWall Das Regelwerk.
|
||||
* @param floorId ID des Ziel-Geschosses.
|
||||
* @param context Kontext (Geschoss-Objekt, Grid-Achsen, Grenzen, …).
|
||||
* @param defaultWallType Fallback-Wandtyp, wenn eine Regel keinen nennt.
|
||||
* @param tolerance Näherungstoleranz für Duplikat-Erkennung (Meter, Default 0.01).
|
||||
* @returns Wall[]-Array, bereit zur Einfügung.
|
||||
*/
|
||||
export function resolveParametricWall(
|
||||
parametricWall: ParametricWall,
|
||||
floorId: string,
|
||||
context: ParametricContext,
|
||||
defaultWallType: WallType,
|
||||
tolerance?: number,
|
||||
): Wall[];
|
||||
|
||||
/**
|
||||
* Dispatcher: delegiert eine Regel an die passende Implementierung.
|
||||
* Exportiert für Unit-Tests und erweiterbare Regeltypen.
|
||||
*/
|
||||
export function applyRule(rule: ParametricRule, ctx: RuleCtx): Wall[];
|
||||
|
||||
/**
|
||||
* Entfernt doppelte Wände: zwei Wände gelten als Duplikat, wenn Start- und
|
||||
* Endpunkt jeweils innerhalb tolerance übereinstimmen (vorwärts und rückwärts).
|
||||
*/
|
||||
export function deduplicateWalls(walls: Wall[], tolerance?: number): Wall[];
|
||||
```
|
||||
|
||||
### 4.1 Höhenauflösung
|
||||
|
||||
Die Wandhöhe (`Wall.height`) ergibt sich nach folgender Priorität:
|
||||
|
||||
1. `rule.height`, falls an der einzelnen Regel gesetzt.
|
||||
2. `context.floor.floorHeight` des Zielgeschosses.
|
||||
3. Fallback: 2.6 m (globaler Default, CONVENTIONS.md).
|
||||
|
||||
### 4.2 ID-Schema
|
||||
|
||||
```
|
||||
"pw-<floorId>-gx-<counter>" // GridRule, X-Achse
|
||||
"pw-<floorId>-gy-<counter>" // GridRule, Y-Achse
|
||||
"pw-<floorId>-mx-<counter>" // ModuleRule, X-Teilung
|
||||
"pw-<floorId>-ct-<counter>" // ConditionalThicknessRule
|
||||
"pw-<floorId>-rl-<counter>" // ReferenceLineRule
|
||||
```
|
||||
|
||||
IDs sind sessionlokal (Zähler startet bei 0 je Modullade). Eingebrannte Wände
|
||||
erhalten beim Commit neue stabile IDs über `uniqueId("W")` — konsistent mit dem
|
||||
Wand-Werkzeug (vgl. [drawing-tools.md §8](drawing-tools.md#8-id-vergabe--immutabilität)).
|
||||
|
||||
### 4.3 Duplikat-Erkennung
|
||||
|
||||
`deduplicateWalls` vergleicht Start-/Endpunkte beider Wände (vorwärts: A→B == A→B,
|
||||
und rückwärts: A→B == B→A) innerhalb einer Toleranz von 1 cm (0.01 m). Die **erste**
|
||||
Instanz wird behalten; spätere Duplikate werden verworfen. Dies ist wichtig bei
|
||||
Vollrastern (`"both"`), bei denen X- und Y-Wände exakt auf einem Rasterpunkt
|
||||
zusammentreffen könnten.
|
||||
|
||||
### 4.4 Verhalten bei ungültigen Eingaben
|
||||
|
||||
| Situation | Verhalten |
|
||||
|-----------|-----------|
|
||||
| `spacing <= 0` oder `moduleSize <= 0` | `[]` |
|
||||
| `referenceWallId` nicht in `existingWalls` | Fallback auf Bounding-Box, kein Fehler |
|
||||
| Unbekannter `condition`-String | `matchesCondition` gibt `false` zurück (kein Treffer) |
|
||||
| Unbekannter `SequenceRule`-Untertyp | TypeScript exhaustiveness-Guard, `[]` |
|
||||
| Segment mit `|end - start| < 1e-6` m | Kann durch deduplicateWalls entfernt werden |
|
||||
|
||||
---
|
||||
|
||||
## 5. Integration ins Projekt
|
||||
|
||||
### 5.1 Ressourcen-Speicherung
|
||||
|
||||
`ParametricWall`-Einträge leben unter `Project.parametricWalls` (optionales Array).
|
||||
Sie sind Teil des `.cad.json`-Dokuments und werden mit dem Rest des Projekts gespeichert.
|
||||
|
||||
```ts
|
||||
// sampleProject.ts — Beispieleintrag
|
||||
export const sampleProject: Project = {
|
||||
// …
|
||||
parametricWalls: [
|
||||
{
|
||||
id: "pw-eg-raster",
|
||||
name: "EG Längswände 3m-Raster",
|
||||
defaultWallTypeId: "wt-innen-15",
|
||||
rules: [
|
||||
{ type: "grid", spacing: 3.0, directions: "x" },
|
||||
{ type: "conditional-thickness", condition: "exterior",
|
||||
wallTypeId: "wt-aussen-36" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### 5.2 Rendering ohne UI (Phase A)
|
||||
|
||||
In Phase A werden parametrische Wände **nicht** automatisch gerendert. Der Auflöser
|
||||
ist eine reine Funktion; Aufrufer müssen ihn explizit einbinden. Mögliche Verwendung
|
||||
in `generatePlan` oder `Viewport3D`:
|
||||
|
||||
```ts
|
||||
// generatePlan.ts (Ergänzung, Phase A)
|
||||
const defaultWallType = project.wallTypes[0];
|
||||
const extraWalls = (project.parametricWalls ?? []).flatMap((pw) =>
|
||||
resolveParametricWall(pw, activeLevelId, {
|
||||
floor: activeFloor,
|
||||
boundaryGeometry: projectBoundary,
|
||||
}, defaultWallType)
|
||||
);
|
||||
const allWalls = [...project.walls, ...extraWalls];
|
||||
// … allWalls statt project.walls in der Rendering-Pipeline verwenden
|
||||
```
|
||||
|
||||
### 5.3 Keine UI in Phase A
|
||||
|
||||
Kein Command, kein Panel, kein Formular. `ParametricWall`-Einträge werden in Phase A
|
||||
ausschließlich **programmatisch** (Unit-Tests, `sampleProject`, direkte JSON-Bearbeitung
|
||||
des Projekts) erstellt.
|
||||
|
||||
---
|
||||
|
||||
## 6. Ausblick: Folge-Phasen
|
||||
|
||||
### Phase B — UI und Command-Schnittstelle
|
||||
|
||||
- Neues Command (z. B. `PWWALL`) oder Ressourcen-Manager-Tab „Parametrische Wände"
|
||||
mit Formular-Editor je Regeltyp.
|
||||
- „Einbrennen" (Flatten): `ParametricWall` → feste `Wall[]` in `Project.walls`
|
||||
einfügen und den `ParametricWall`-Eintrag entfernen (unidirektional, Undo über Store).
|
||||
- Auswahl parametrischer Wände im Plan (als Gruppe); Grip-Editing der Raster-Parameter
|
||||
und Spannweiten.
|
||||
|
||||
### Phase C — Grid-Ressource und Schnittpunkt-Clipping
|
||||
|
||||
- `GridResource`: ein projektweites, benanntes Koordinatenraster (LV95-Offset,
|
||||
Rasterweite, Drehung), auf das mehrere `GridRule`-Instanzen via `gridId` verweisen.
|
||||
- Präzises Clipping: erzeugte Wände werden am Gebäudeumriss getrimmt — exakte
|
||||
`lineIntersect`-Berechnung statt Bounding-Box-Approximation.
|
||||
- Benutzerdefinierte Tags (`Wall.tags[]`) für komplexe `ConditionalThicknessRule`-
|
||||
Bedingungen jenseits von „exterior/interior/bearing".
|
||||
- IFC-Export: `ParametricWall`-Gruppen → `IfcWallStandardCase` mit parametrischen
|
||||
Attributen und `IfcRelDefinesByType`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Vollständige Anwendungsbeispiele
|
||||
|
||||
### 7.1 Schweizer Wohnbau: 3-m-Raster EG + 1.OG
|
||||
|
||||
Zwei-Geschoss-Wohnhaus, typisches CH-Wohnbauraster. Die Längswände beider Geschosse
|
||||
entstehen aus zwei `ParametricWall`-Einträgen mit identischen Regeln, unterschieden
|
||||
nur durch `floorId` beim Auflösen:
|
||||
|
||||
```
|
||||
Top-down (Grundriss):
|
||||
|
||||
y=12 ──────────────────────────── (W5)
|
||||
y=9 ──────────────────────────── (W4)
|
||||
y=6 ──────────────────────────── (W3)
|
||||
y=3 ──────────────────────────── (W2)
|
||||
y=0 ──────────────────────────── (W1)
|
||||
↑
|
||||
x=0 x=12
|
||||
```
|
||||
|
||||
```ts
|
||||
const rasterRegel: ParametricWall = {
|
||||
id: "pw-laengswand-raster",
|
||||
name: "Längswände 3m-Raster",
|
||||
defaultWallTypeId: "wt-innen-15",
|
||||
rules: [
|
||||
{ type: "grid", spacing: 3.0, directions: "x" },
|
||||
// Außenwände (y=0 und y=12) erhalten den dicken Aufbau:
|
||||
{ type: "conditional-thickness", condition: "exterior",
|
||||
wallTypeId: "wt-aussen-36" },
|
||||
// Außenwände: Achse liegt auf der Fassadenfläche:
|
||||
{ type: "reference-line", referenceLine: "left", target: "exterior" },
|
||||
],
|
||||
};
|
||||
|
||||
// EG auflösen:
|
||||
const wallsEG = resolveParametricWall(rasterRegel, "floor-eg",
|
||||
{ floor: egFloor, boundaryGeometry: { boundary: rect(0,0,12,12) } },
|
||||
project.wallTypes[0]);
|
||||
|
||||
// 1.OG auflösen (gleiche Regel, anderes Geschoss):
|
||||
const wallsOG = resolveParametricWall(rasterRegel, "floor-og1",
|
||||
{ floor: ogFloor, boundaryGeometry: { boundary: rect(0,0,12,12) } },
|
||||
project.wallTypes[0]);
|
||||
// Änderung spacing: 3.5 → beide Geschosse sofort konsistent.
|
||||
```
|
||||
|
||||
### 7.2 Vollraster mit Außen/Innen-Differenzierung
|
||||
|
||||
Gebäudeumriss als Rechteck; die Randwände erhalten automatisch den dicken
|
||||
Außenwand-Typ, alle anderen den dünnen Innenwand-Typ:
|
||||
|
||||
```ts
|
||||
const vollraster: ParametricWall = {
|
||||
id: "pw-eg-vollraster",
|
||||
name: "EG Vollraster mit Differenzierung",
|
||||
defaultWallTypeId: "wt-innen-15",
|
||||
rules: [
|
||||
{ type: "grid", spacing: 3.0, directions: "both" },
|
||||
{ type: "conditional-thickness", condition: "exterior",
|
||||
wallTypeId: "wt-aussen-36" },
|
||||
{ type: "conditional-thickness", condition: "interior",
|
||||
wallTypeId: "wt-innen-15" },
|
||||
{ type: "reference-line", referenceLine: "left", target: "exterior" },
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
```
|
||||
─────┬─────┬─────┬─────
|
||||
│ │ │ │ │
|
||||
─────┼─────┼─────┼─────
|
||||
│ │ │ │ │
|
||||
─────┴─────┴─────┴─────
|
||||
|
||||
Rand-Segmente: wt-aussen-36 (dicker Aufbau)
|
||||
Innen-Segmente: wt-innen-15 (dünner Aufbau)
|
||||
```
|
||||
|
||||
### 7.3 Modulbauweise: 6-m-Joch, Bürogebäude
|
||||
|
||||
Längliches Bürogebäude, 36 m × 12 m, 6-m-Joch. Querwände entstehen automatisch;
|
||||
Entwurfsänderung (z. B. auf 7.2-m-Joch) erfordert eine einzige Zahl:
|
||||
|
||||
```ts
|
||||
const joch: ParametricWall = {
|
||||
id: "pw-buero-joch",
|
||||
name: "Büro 6m-Joch",
|
||||
defaultWallTypeId: "wt-beton-20",
|
||||
rules: [
|
||||
{
|
||||
type: "module",
|
||||
moduleSize: 6.0,
|
||||
direction: "x", // Querwände senkrecht zur X-Achse
|
||||
// referenceWallId: "W-sudfassade" → Spanne aus Referenzwand
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// resolveParametricWall → Querwände bei x = 6, 12, 18, 24, 30
|
||||
// (bei Bounding-Box minX=0, maxX=36, Enden selbst ausgespart)
|
||||
|
||||
// Änderung auf 7.2-m-Joch: moduleSize: 7.2
|
||||
// → 4 Trennwände bei x ≈ 7.2, 14.4, 21.6, 28.8 — automatisch neu berechnet.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Architektur-Garantien
|
||||
|
||||
- **Modell bleibt einzige Wahrheit.** `ParametricWall`-Definitionen sind Daten in
|
||||
`Project.parametricWalls`; `resolveParametricWall` ist eine **reine Funktion** ohne
|
||||
Side-Effects. Keine globale Laufzeit-Geometrie-Schicht.
|
||||
- **Erzeugte Wände sind gewöhnliche `Wall`-Objekte.** Alle nachgelagerten Systeme
|
||||
(`generatePlan`, `Viewport3D`, `computeJoins`) arbeiten unverändert; sie müssen
|
||||
nicht zwischen „parametrisch erzeugten" und „direkt gezeichneten" Wänden
|
||||
unterscheiden.
|
||||
- **Fehlertoleranz statt Absturz.** Unbekannte Regeltypen liefern `[]`; der TypeScript-
|
||||
exhaustiveness-Guard fängt fehlende `case`-Zweige zur Compilezeit. Unbekannte
|
||||
Bedingungsstrings in `ConditionalThicknessRule` geben `false` (kein Treffer) statt
|
||||
zu werfen.
|
||||
- **Keine vorzeitige Generalisierung.** Phase A liefert fünf Regel-Varianten und
|
||||
einen Auflöser. UI, Command-Schnittstelle und Grid-Ressource folgen in Phase B/C.
|
||||
- **Immutabilität.** Verfeinerungsregeln (`ConditionalThicknessRule`,
|
||||
`ReferenceLineRule`) geben modifizierte **Kopien** zurück; `existingWalls` werden
|
||||
nie mutiert — konsistent mit der `setProject`-Konvention (CONVENTIONS.md).
|
||||
@@ -0,0 +1,74 @@
|
||||
# Ribbon-UI + modulare Bars — Plan
|
||||
|
||||
Nutzer-Vision (2026-07-05, mit OCS/AutoCAD-Ribbon als Referenz). Ziel: klare,
|
||||
einheitliche Werkzeug-/Eigenschaften-Darstellung; Werkzeug-Sidebar entfällt.
|
||||
|
||||
## Zielbild
|
||||
|
||||
- **Ribbon-Oberleiste mit Tabs: 2D · 3D · BIM · Ansichten.** Jeder Tab zeigt
|
||||
gruppierte Werkzeug-/Aktions-Icons (wie OCS Draw/Model/Insert/Annotate/View).
|
||||
- **2D**: Zeichnen (Select/Linie/Polylinie/Rechteck/Kreis/Bogen/Text) · Ändern
|
||||
(Move/Copy/Mirror/Offset/Trim/Join/Boolean).
|
||||
- **3D**: Volumen/Boolean (vorhandene 3D-Aktionen), Kamera-Presets.
|
||||
- **BIM**: Wand/Fenster/Tür/Treppe/Decke/Raum (die Bauteile).
|
||||
- **Ansichten**: Geschoss/Schnitt/Ansicht-Wechsel, Zoom/Einpassen, Render-Modi.
|
||||
- **Werkzeug-Sidebar entfällt** → rechtes Panel (Attribute) bekommt volle Höhe.
|
||||
- **Objektinfo unter die Attribute** mergen (Wandstil-Definition etc. zusammen).
|
||||
- **XYZ-Referenzpunkt-Box oben rechts bleibt** (vom Nutzer ausdrücklich gewünscht).
|
||||
- **Eigenschaften-Grid im OCS-Stil** — ✅ bereits erledigt (`00733d8`): Sektion-
|
||||
Balken, Zeilentrenner, füllende linksbündige Wertfelder (`.attr-*`).
|
||||
|
||||
## Architektur — DATENGETRIEBEN (ermöglicht modulare Custom-Bar)
|
||||
|
||||
Kern: EINE Registry beschreibt alle Ribbon-Elemente; Ribbon-Tabs UND eine
|
||||
benutzerdefinierte Custom-Bar sind bloß zwei Ansichten über dieselbe Registry.
|
||||
|
||||
```
|
||||
RibbonItem =
|
||||
| { kind: "tool"; id: ToolId } // aktiviert ein Werkzeug (onSelectTool)
|
||||
| { kind: "command"; name: string } // startet einen Engine-Befehl (engine.start)
|
||||
| { kind: "action"; id: string; run: () => void } // freie App-Aktion (Zoom, Render-Modus …)
|
||||
|
||||
RibbonGroup = { titleKey: string; items: RibbonItem[] }
|
||||
RibbonTab = { id: "2d"|"3d"|"bim"|"views"; labelKey: string; groups: RibbonGroup[] }
|
||||
RIBBON: RibbonTab[]
|
||||
```
|
||||
|
||||
- Icons: `ToolIcon` (vorhanden) für `tool`-Items; für `command`/`action` eine
|
||||
kleine Icon-Map (SVG) analog `ToolsPanel`.
|
||||
- Aktivierung: `tool` → `onSelectTool(id)` · `command` → `engine.start(name)` ·
|
||||
`action` → `run()`. Aktiv-Highlight über `activeTool` bzw. laufenden Befehl.
|
||||
- **Custom-Bar (modular)**: der Nutzer wählt beliebige `RibbonItem`s in eine
|
||||
persistierte Liste (`viewSlice`/Projekt); eine `CustomBar`-Komponente rendert
|
||||
genau diese. Gleiche Item-Typen, gleiche Aktivierung — kein Sonderweg.
|
||||
|
||||
## Phasen
|
||||
|
||||
1. ✅ **Gerüst + 2D/BIM-Tab** (`9d6e86d`). `RibbonBar` unter der TopBar (additiv).
|
||||
Datengetriebene Registry (`src/ui/ribbon/ribbonItems.ts`), 2D-Tab (Zeichnen +
|
||||
Ändern) und BIM-Tab (Bauteile) gefüllt, Tab-State lokal, Modify-Icons ergänzt.
|
||||
2. ✅ **TopBar → „Ansichten"-Tab gemergt** (`85011cb`, Nutzer-Entscheid „voll
|
||||
mergen"). Die Ansichts-/Zoom-/Darstellungs-Cluster der TopBar (View-Grid +
|
||||
Kamera, Ebenen-/Zeichnungs-Kombis, Detailgrad, Massstab/Zoom, Darstellungsart)
|
||||
sind als `ViewRibbonTab` (in `TopBar.tsx`) in den „Ansichten"-Tab gewandert;
|
||||
App reicht sie als `viewsContent`-Node an `RibbonBar` (wie das Layout-Menü).
|
||||
Die **Tab-Reiter sitzen in der TopBar-Zeile** (`RibbonTabs`, in TopBar
|
||||
gerendert; Tab-State in App), der Ribbon-Inhalt (`RibbonBar`) folgt darunter →
|
||||
nur ZWEI Bänder. Die TopBar ist eine **schmale Leiste** (Höhe 40px): Marke/
|
||||
Ressourcen · Tabs · Datei/Export/Einstellungen · Fensterknöpfe. Die Text-/
|
||||
Font-Formatierung (`TextGroup`) liegt im eigenen **„Text"-Tab** (Reihenfolge
|
||||
2D·3D·BIM·Text·Ansichten; `RibbonBar.tabContent` trägt „text" + „views").
|
||||
- **Offen (visuell iterieren):** ob Zoom/Massstab zusätzlich als Dauer-Anzeige
|
||||
(Statusleiste) sichtbar sein soll; Default-Tab (aktuell 2D); 3D-Tab noch
|
||||
leer (Kamera-Presets/3D-Aktionen füllen).
|
||||
3. **Sidebar raus** (ToolsPanel aus Default-Layout) + rechtes Panel volle Höhe;
|
||||
Objektinfo unter Attribute mergen.
|
||||
4. **Modulare Custom-Bar**: Item-Picker + persistierte Custom-Bar-Ansicht.
|
||||
|
||||
## Hinweise / Randbedingungen
|
||||
|
||||
- Additiv bauen: bestehende Kommandozeile + Nummern-Shortcuts + `ToolsPanel`
|
||||
bleiben funktionsfähig, bis Phase 3 die Sidebar bewusst entfernt.
|
||||
- Werkzeug-Kopplung Tool↔Befehl existiert bereits (`TOOL_COMMAND`) — Ribbon nutzt
|
||||
denselben `onSelectTool`/`engine.start`-Pfad (EIN Pfad, keine Duplikate).
|
||||
- Empfehlung: Phase 1+ in fokussierter Session mit visueller Iteration im Tauri.
|
||||
@@ -0,0 +1,306 @@
|
||||
# Architektur-Pivot: Tauri + Rust-Backend (2026-07-01)
|
||||
|
||||
## Entscheidung
|
||||
|
||||
**Alte Welt:** Browser-CAD (React/Vite + WebGL/three.js)
|
||||
**Neue Welt:** Desktop Tauri-App (React/Vite Frontend + Rust-Backend + **wgpu 3D-Rendering**)
|
||||
|
||||
**Grund:** Komplexe Möbel mit vielen Polygonen (100k+) + mehrere parallele rechenintensive Ops → WebGL/three.js wird zum Bottleneck. **wgpu** (low-level GPU-API auf Vulkan/Metal/DX12) + Rust-Compute skaliert native.
|
||||
|
||||
**Scope:** Desktop-only Release (Milestone 1). WASM-Fallback für Browser später wenn gebraucht.
|
||||
|
||||
---
|
||||
|
||||
## Post-Migration Stack
|
||||
|
||||
### Frontend (React/Vite — Komponenten + State, unverändert)
|
||||
|
||||
```
|
||||
src/
|
||||
App.tsx ← Shell-Komponente
|
||||
compute/index.ts ← Compute-Boundary (neu)
|
||||
model/types.ts ← Semantisches Modell
|
||||
commands/ ← Befehlssystem
|
||||
panels/ ← UI-Panels
|
||||
plan/PlanView.tsx ← 2D-SVG-Rendering
|
||||
viewport/Viewport3D.tsx ← three.js 3D-Display
|
||||
ui/ ← Topbar, Dialogs, etc.
|
||||
state/ ← Redux-Slices (project, selection, view, layout)
|
||||
...
|
||||
```
|
||||
|
||||
**Rolle:** User-Input-Handling, 2D/3D-Darstellung (Display-Layer), State-Management.
|
||||
|
||||
### Backend (Rust/Tauri — neu)
|
||||
|
||||
```
|
||||
src-tauri/
|
||||
src/
|
||||
main.rs ← Tauri window + invoke-handler registration
|
||||
geometry.rs ← compute_joins(), kernel2d(), etc.
|
||||
parsers/
|
||||
dwg.rs ← DXF/DWG-Geometrie-Parsing
|
||||
dxf.rs
|
||||
sia/
|
||||
room_detection.rs ← detectRooms() (SIA-416)
|
||||
...
|
||||
Cargo.toml ← Dependencies (serde, tauri, …)
|
||||
```
|
||||
|
||||
**Rolle:** Rechenintensive Ops, Geometrie-Kernel, Parsing, SIA-Raumerkennung.
|
||||
|
||||
### IPC: Tauri invoke (async, serde)
|
||||
|
||||
```typescript
|
||||
// Frontend ruft Rust auf
|
||||
const result = await invoke<JoinInfo[]>('compute_joins', { walls, joints });
|
||||
|
||||
// Rust bearbeitet + serialisiert Ergebnis
|
||||
#[tauri::command]
|
||||
async fn compute_joins(input: JoinInput) -> Result<JoinOutput, String> {
|
||||
geometry::compute_joins(input).map_err(|e| e.to_string())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compute-Boundary (Key Design)
|
||||
|
||||
**Neue Datei:** `src/compute/index.ts` — einziger Eingang für rechenintensive Ops.
|
||||
|
||||
```typescript
|
||||
// Beispiel-Schnittstellen
|
||||
export async function computeJoins(walls: Wall[], joints: Array<[number, number]>): Promise<JoinInfo[]> { … }
|
||||
export async function computeKernel2D(op: 'offset'|'trim', geom: Polyline, …): Promise<Polyline[]> { … }
|
||||
export async function detectRooms(…): Promise<Room[]> { … }
|
||||
export async function parseShapeFromDwg(…): Promise<DwgGeometry> { … }
|
||||
```
|
||||
|
||||
**Hinter der Boundary:**
|
||||
1. Versuche Tauri invoke zu Rust (`#[tauri::command]`)
|
||||
2. Fallback auf lokale TS-Impl wenn Rust nicht verfügbar (während Migration)
|
||||
|
||||
**Effekt:** Aufrufort im Code bleibt stabil; Caller sieht nicht, ob Op schon in Rust ist oder noch TS.
|
||||
|
||||
**Migrationsfluss:**
|
||||
```
|
||||
1. TS-Impl existiert (z.B. src/model/joins.ts)
|
||||
2. Neue Op in Compute-Boundary mit Invoke+Fallback
|
||||
3. Parallel: Rust-Impl in src-tauri/src/geometry.rs
|
||||
4. Tests: Rust-Output == TS-Output (Parität)
|
||||
5. TS-Impl bleibt (Fallback, wird nicht entfernt bis Rust stable)
|
||||
6. Eventuell: TS-Impl löschen wenn Rust bewährt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dev-Workflow (post-Tauri)
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# Terminal 1: Vite dev-server
|
||||
npm run dev # localhost:5173
|
||||
|
||||
# Terminal 2: Tauri dev
|
||||
npm run tauri:dev # öffnet Tauri-Fenster, zeigt auf localhost:5173
|
||||
# Rust hot-reload + TS hot-reload gleichzeitig
|
||||
```
|
||||
|
||||
**Voraussetzungen:**
|
||||
- Node.js + npm (wie heute)
|
||||
- Rust + Cargo (neu)
|
||||
- Tauri CLI: `npm install -D @tauri-apps/cli`
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
# Single command
|
||||
npm run tauri:build
|
||||
|
||||
# Erzeugt:
|
||||
# - Windows: src-tauri/target/release/cad.exe
|
||||
# - macOS: src-tauri/target/release/bundle/macos/cad.app
|
||||
# - Linux: src-tauri/target/release/bundle/deb/cad_*.deb (oder Flatpak)
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Rust-Unit-Tests
|
||||
cargo test # in src-tauri/
|
||||
|
||||
# TS-Tests (unverändert)
|
||||
npm run test
|
||||
|
||||
# Integration-Test: App starten + Aktion prüfen
|
||||
npm run tauri:dev # manuell testen oder Puppeteer-Probe erweitern
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GPU-Strategy (für später)
|
||||
|
||||
**Milestone 1 (jetzt):** CPU-Ops in Rust (kernel2d, joins, parsing, SIA).
|
||||
|
||||
**Milestone 2 (später):** GPU-Compute via wgpu
|
||||
- Tauri + wgpu Renderer (optional, nicht erforderlich)
|
||||
- ODER drei.js bleibt, Rust handelt CPU-Ops, three.js handelt Display
|
||||
- GPU-Heavy-Ops (z.B. große Boolean-Operationen) können in wgpu laufen, aber MVP braucht das nicht
|
||||
|
||||
**Aktueller Plan:** three.js bleibt für 3D-Display (skaliert ausreichend für Möbel-Geometrie mit Instancing + LOD).
|
||||
|
||||
---
|
||||
|
||||
## Migration Strategy: Ops nach Priorisierung
|
||||
|
||||
**Phase 1 (aktuell — Tauri-Shell + Proof-of-Concept):**
|
||||
- [ ] `computeJoins` (Wand-Eckverbindungen) → Rust
|
||||
- Gründe: klein, häufig, zeigt invoke-Flow
|
||||
|
||||
**Phase 2 (nächst):**
|
||||
- [ ] `kernel2d` (Offset/Trim/Extend/Fillet) → Rust
|
||||
- Gründe: Rechenlast ⭐⭐, Frequenz hoch
|
||||
- [ ] DXF/DWG-Parser → Rust (Geometrie-Extraktion)
|
||||
- Gründe: Rechenlast ⭐⭐, Frequenz mittel (Import-Dialog)
|
||||
|
||||
**Phase 3 (später):**
|
||||
- [ ] `detectRooms` (SIA-416 Raumerkennung) → Rust
|
||||
- Gründe: Rechenlast ⭐, async-freundlich
|
||||
- [ ] `booleanOps` (Union/Differenz/Schnitt) → Rust
|
||||
- Gründe: Rechenlast ⭐⭐, Frequenz gering (ad-hoc)
|
||||
|
||||
---
|
||||
|
||||
## Folgen für bestehenden Code
|
||||
|
||||
### Was ändert sich NICHT
|
||||
|
||||
- `src/model/types.ts` — semantisches Modell bleibt in TS (Frontend kennt es)
|
||||
- `src/state/` — Redux-Store unverändert
|
||||
- `src/ui/` — Komponenten unverändert
|
||||
- `src/plan/PlanView.tsx` — SVG-Rendering unverändert
|
||||
- `src/viewport/Viewport3D.tsx` — three.js-Rendering unverändert
|
||||
- `src/commands/` — Befehlssystem unverändert
|
||||
|
||||
### Was ändert sich
|
||||
|
||||
- **Neue `src/compute/index.ts`** — alle rechenintensiven Ops laufen durch hier
|
||||
- **Neue `src-tauri/`** — Rust-Backend
|
||||
- **Vite-Config:** Tauri plugin hinzufügen
|
||||
- **Package.json:** tauri scripts hinzufügen
|
||||
- **Build-Prozess:** `npm run tauri:build` statt `npm run build`
|
||||
|
||||
### Was wird migriert (schrittweise)
|
||||
|
||||
- `src/model/joins.ts` → `src-tauri/src/geometry.rs` (Phase 1)
|
||||
- `src/geometry/kernel2d.ts` → `src-tauri/src/geometry.rs` (Phase 2)
|
||||
- `src/io/{dxfParser, dwgParser}.ts` → `src-tauri/src/parsers/` (Phase 2)
|
||||
- `src/geometry/{roomArea, roomBoundary}.ts` → `src-tauri/src/sia/room_detection.rs` (Phase 3)
|
||||
- `src/editors/booleanOps.ts` → `src-tauri/src/geometry.rs` (Phase 3)
|
||||
|
||||
**Wichtig:** TS-Versionen bleiben als Fallback (nicht gelöscht).
|
||||
|
||||
---
|
||||
|
||||
## Distribution (später)
|
||||
|
||||
### Desktop Binaries (post-Tauri)
|
||||
|
||||
- **Windows:** `.exe` (standalone executable)
|
||||
- **macOS:** `.app` bundle (code-signed)
|
||||
- **Linux:** `.deb` package ODER **Flatpak** (preferred)
|
||||
- Flatpak = moderne WebKitGTK6 immer dabei, unabhängig von Distro-Alter
|
||||
|
||||
### Browser (wenn gebraucht)
|
||||
|
||||
- **WASM-Fallback** für `src/compute/` Ops (Rust → WASM via wasm-bindgen)
|
||||
- Later-phase feature, nicht Milestone 1
|
||||
|
||||
---
|
||||
|
||||
## Technische Details
|
||||
|
||||
### Serialisierung (TS ↔ Rust)
|
||||
|
||||
**serde + serde_json** für Geometrie-Typen:
|
||||
|
||||
```rust
|
||||
// Rust
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Vec2 { pub x: f64, pub y: f64 }
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Wall {
|
||||
pub id: String,
|
||||
pub start: Vec2,
|
||||
pub end: Vec2,
|
||||
// …
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// TS (type-safe invoke)
|
||||
interface Vec2 { x: number; y: number }
|
||||
interface Wall { id: string; start: Vec2; end: Vec2; /* … */ }
|
||||
|
||||
await invoke<JoinInfo[]>('compute_joins', { walls: Wall[] })
|
||||
```
|
||||
|
||||
### Tauri Security (default)
|
||||
|
||||
- Invoke-Handler sind Rust-side validiert
|
||||
- Whitelist-Makro (`#[tauri::command]`) registered nur explizit erlaubte Functions
|
||||
- CORS/CSP Policy default secure
|
||||
- Keine arbitrary-Script-Execution (native app)
|
||||
|
||||
---
|
||||
|
||||
## Abhängigkeiten (neu post-Tauri)
|
||||
|
||||
### Frontend (npm)
|
||||
- Bestehende: react, vite, three.js, redux, etc.
|
||||
- Neu: `@tauri-apps/api` (JS-SDK für invoke)
|
||||
- Optional später: `@tauri-apps/cli` dev-dependency (bereits in package.json)
|
||||
|
||||
### Backend (Cargo)
|
||||
```toml
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["webkit2gtk-6.0"] } # GTK4
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
# später: wgpu, delaunator, opencascade-sys, etc.
|
||||
```
|
||||
|
||||
### System
|
||||
- Rust 1.70+
|
||||
- GTK4 dev libraries (Linux only, auto-handled by Tauri)
|
||||
- Xcode Command Line Tools (macOS, auto-checked)
|
||||
|
||||
---
|
||||
|
||||
## Next Steps (Koordination)
|
||||
|
||||
**Aufgabe für nächste Phase:**
|
||||
→ Siehe `docs/design/tauri-migration-plan.md` (Schritt-für-Schritt, vier parallele Agents)
|
||||
|
||||
**HANDOVER.md:** wird aktualisiert nach Tauri-Shell stabil.
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Läuft die App noch im Browser?**
|
||||
A: Nein (Milestone 1). Desktop-only. WASM-Fallback für Browser später wenn gebraucht.
|
||||
|
||||
**Q: Was passiert mit dem existing TS-Code?**
|
||||
A: Bleibt unverändert (außer neue Compute-Boundary). TS-Implementierungen = Fallback bis Rust stabil.
|
||||
|
||||
**Q: Muss ich Rust können um das Projekt zu verstehen?**
|
||||
A: Nein. Frontend bleibt React/TS. Rust ist "blackbox" hinter invoke. Aber bei Rust-Bugs muss man rein.
|
||||
|
||||
**Q: Wann ist Tauri-Shell fertig?**
|
||||
A: Nach den vier Agents (Schritt 1–4 in tauri-migration-plan.md), ~1–2 Wochen.
|
||||
|
||||
**Q: Kann ich lokal testen?**
|
||||
A: Ja, `npm run tauri:dev` öffnet lokale App. Alles wie heute, nur Rust hinten dran.
|
||||
@@ -0,0 +1,243 @@
|
||||
# Tauri-Migration + Compute-Boundary — Aufgabe für nächste Instanz
|
||||
|
||||
**Entscheidung (fix, 2026-07-01):** Browser-CAD → **Desktop Tauri-App mit Rust-Backend + wgpu-Rendering.**
|
||||
|
||||
**Grund:** komplexe Möbel mit vielen Polygonen (100k+) + mehrere parallele rechenintensive Ops → WebGL/three.js wird zum Blocker. **wgpu** (low-level GPU-API) + Rust-Compute skaliert native.
|
||||
|
||||
**Scope:** Desktop-only Release (Milestone 1). WASM-Fallback für Browser später wenn gebraucht.
|
||||
|
||||
**Rendering-Engine:** three.js → **wgpu** (Milestone 2)
|
||||
|
||||
---
|
||||
|
||||
## Aufgabe: Shell aufsetzen + Compute-Boundary + erste Op migrieren
|
||||
|
||||
### Schritt 0 — Compute-Boundary (TS-Kontrakt)
|
||||
|
||||
**Neu:** `src/compute/index.ts` — die einzige Stelle, durch die alle rechenintensiven Ops laufen.
|
||||
|
||||
```typescript
|
||||
// src/compute/index.ts — einheitliche Schnittstelle
|
||||
export async function computeKernel2D(op: 'offset'|'trim', …): Promise<Polyline[]> { … }
|
||||
export async function detectRooms(…): Promise<Room[]> { … }
|
||||
export async function computeJoins(…): Promise<JoinInfo[]> { … } // ← erste Op
|
||||
export async function parseShapeFromDwg(…): Promise<DwgGeometry> { … }
|
||||
```
|
||||
|
||||
**Hinter der Boundary:**
|
||||
- Erst Tauri-invoke zu Rust `#[tauri::command]`
|
||||
- Fallback auf lokale TS-Impl (bleibt unberührt, bis Rust stabil)
|
||||
- `catch(err) → console.warn('Rust failed, using TS fallback'); return tsImpl(…)`
|
||||
|
||||
**Effekt:** Aufrufort im Code bleibt stabil; Caller sieht nicht, ob Op schon in Rust ist oder noch TS.
|
||||
|
||||
---
|
||||
|
||||
### Schritt 1 — Tauri-Shell aufsetzen
|
||||
|
||||
**Struktur:**
|
||||
```
|
||||
repo/
|
||||
src-tauri/ ← neue Rust-Seite (Tauri-Konvention)
|
||||
src/
|
||||
main.rs ← Tauri window + invoke handlers
|
||||
geometry.rs ← compute_joins() + weitere Ops später
|
||||
...
|
||||
Cargo.toml
|
||||
src/ ← React/TS (unverändert)
|
||||
compute/
|
||||
index.ts ← Compute-Boundary
|
||||
...
|
||||
vite.config.ts ← Tauri plugin integrieren
|
||||
package.json ← tauri scripts
|
||||
```
|
||||
|
||||
**Setup:**
|
||||
1. `cargo init --name cad-tauri src-tauri` (oder `src-tauri` manuell anlegen)
|
||||
2. `Cargo.toml`: Tauri v2 einbinden mit Feature `webkit2gtk-6.0` (GTK4)
|
||||
```toml
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["webkit2gtk-6.0"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
```
|
||||
3. `src-tauri/src/main.rs`: Minimal-Fenster, invoke-Handler registrieren
|
||||
```rust
|
||||
#[tauri::command]
|
||||
async fn compute_joins(input: JoinInput) -> Result<JoinOutput, String> {
|
||||
// Rust-Impl
|
||||
geometry::compute_joins(input).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![compute_joins])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
```
|
||||
4. `vite.config.ts`: Tauri plugin + dev-server-Integration
|
||||
```typescript
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 5173 } // Tauri dev zeigt hier drauf
|
||||
})
|
||||
```
|
||||
5. `package.json`: Tauri scripts hinzufügen
|
||||
```json
|
||||
"scripts": {
|
||||
"tauri": "tauri",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:build": "tauri build"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Schritt 2 — Erste Op migrieren: `computeJoins` (Wand-Eckverbindungen)
|
||||
|
||||
**Warum `computeJoins` zuerst?**
|
||||
- Läuft häufig (bei jedem Wall-Edit)
|
||||
- Klein und fokussiert (~50 Zeilen Kernlogik)
|
||||
- Proof-of-Concept für Tauri-invoke-Flow
|
||||
- Danach `kernel2d` parallel hochfahren
|
||||
|
||||
**Rust-Impl:** `src-tauri/src/geometry.rs`
|
||||
|
||||
```rust
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Vec2 { pub x: f64, pub y: f64 }
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct WallJoinInput {
|
||||
pub walls: Vec<Wall>,
|
||||
pub joints: Vec<(usize, usize)>, // wall indices
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct JoinInfo { /* … */ }
|
||||
|
||||
pub fn compute_joins(input: WallJoinInput) -> Result<Vec<JoinInfo>, Box<dyn std::error::Error>> {
|
||||
// Port der Logik aus src/model/joins.ts
|
||||
// L-Ecken, T-Stösse, +-Kreuzungen
|
||||
Ok(vec![]) // Placeholder
|
||||
}
|
||||
```
|
||||
|
||||
**TS-Fallback bleibt:** `src/model/joins.ts` (LS vor Rust-Port)
|
||||
|
||||
**Compute-Boundary:** `src/compute/index.ts`
|
||||
```typescript
|
||||
export async function computeJoins(walls: Wall[], joints: Array<[number, number]>): Promise<JoinInfo[]> {
|
||||
try {
|
||||
return await invoke<JoinInfo[]>('compute_joins', { walls, joints });
|
||||
} catch (err) {
|
||||
console.warn('Rust compute_joins failed, using TS fallback:', err);
|
||||
return joinsTS(walls, joints); // Fallback
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Schritt 3 — Migrations-Parität testen
|
||||
|
||||
**Test-Fixtures:**
|
||||
- Aus `src/model/sampleProject.ts` exportieren: Wand-Arrays mit bekannten L/T/±-Konfigurationen
|
||||
- Rust-Unit-Tests: gleiche Fixtures → gleiche JoinInfo-Outputs
|
||||
- Vergleich: `actual == expected`
|
||||
|
||||
**Beispiel (Rust):**
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_l_corner() {
|
||||
let input = WallJoinInput { /* L-shaped walls */ };
|
||||
let result = compute_joins(input).unwrap();
|
||||
assert_eq!(result[0].kind, JoinKind::LCorner);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Schritt 4 — Vite/Tauri-Integration
|
||||
|
||||
**Dev-Workflow:**
|
||||
```bash
|
||||
npm run tauri:dev
|
||||
# → Vite dev-server (localhost:5173) lädt React-App
|
||||
# → Tauri-window zeigt auf :5173
|
||||
# → invoke() ruft Rust-Commands auf
|
||||
```
|
||||
|
||||
**Build-Workflow:**
|
||||
```bash
|
||||
npm run build # Vite → dist/
|
||||
npm run tauri:build # Tauri packt dist/ + Rust-Binary
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deliverable (Proof-of-Concept)
|
||||
|
||||
**Git-Stand nach dieser Aufgabe:**
|
||||
|
||||
- [ ] `src-tauri/` Verzeichnis mit `Cargo.toml` + `src/main.rs` + `src/geometry.rs`
|
||||
- [ ] `Cargo.toml` buildet sauber (`cargo check` 0 Fehler)
|
||||
- [ ] `src/compute/index.ts` mit `computeJoins()` Schnittstelle (invoke + TS-fallback)
|
||||
- [ ] Rust `compute_joins()` implementiert, Tests pass (`cargo test`)
|
||||
- [ ] `package.json` `tauri` scripts hinzugefügt
|
||||
- [ ] **App läuft:** `npm run tauri:dev` → Tauri-Fenster öffnet, Wand-Edit triggert Rust-Op, Output identisch TS-Version
|
||||
- [ ] **Trace-Scan sauber** (kein TS/Rust-Code übrig, beide Impl. aktiv)
|
||||
- [ ] HANDOVER.md aktualisiert: `computeJoins` migriert, nächste Ops in Queue
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
# Build-Check
|
||||
cargo check # 0 Fehler
|
||||
|
||||
# Rust-Tests
|
||||
cargo test
|
||||
|
||||
# App end-to-end
|
||||
npm run tauri:dev
|
||||
# → Wand zeichnen + editieren → computeJoins() über Rust aufgerufen
|
||||
# → Plan + 3D aktualisiert wie vorher
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Nächste Ops (Priorisierung)
|
||||
|
||||
Nach `computeJoins` stabil:
|
||||
|
||||
1. **`kernel2d`** (Offset/Trim/Extend) — großer Hebel, läuft häufig
|
||||
2. **DXF/DWG-Parser** (Geometrie) — heavy, aber niedrige Frequenz
|
||||
3. **`detectRooms`** (SIA-Raumerkennung) — async, kann auf Hintergrund ziehen
|
||||
4. **`booleanOps`** (Union/Differenz/Schnitt) — Kandidat für später
|
||||
|
||||
---
|
||||
|
||||
## Offene Punkte (NICHT jetzt)
|
||||
|
||||
- **GPU-Compute (wgpu):** Erst nach CPU-Ops stabil (kernel2d, joins, parsing)
|
||||
- **WASM-Fallback:** Nur wenn Browser-Support nötig wird
|
||||
- **Flatpak-Distribution:** Nach Tauri-Shell stable + erste Ops migriert
|
||||
|
||||
---
|
||||
|
||||
## Referenzen
|
||||
|
||||
- Tauri v2 Docs: https://tauri.app/v1/guides/getting-started/prerequisites
|
||||
- Serde: https://serde.rs/
|
||||
- CONVENTIONS.md: Identifiers englisch, UI-Text via `t()`, …
|
||||
- Commit-Regel: kein AI-Attribution im Repo
|
||||
@@ -0,0 +1,151 @@
|
||||
# Oberleiste – Angleichung an DOSSIER (Umsetzungs-Spezifikation)
|
||||
|
||||
Verbindliche Vorlage für den Umbau der Top-Bar (`src/ui/TopBar.tsx`,
|
||||
`src/styles.css`, Verdrahtung in `src/App.tsx`). Quelle: das DOSSIER-Rhino-
|
||||
Plugin (`ToolbarApp.jsx`, `components/BarControls.jsx`, `TextEditorApp.jsx`).
|
||||
Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
## 0. Grundprimitive (neu, DOSSIER-konform)
|
||||
|
||||
Alle Leisten-Controls teilen dieselbe Höhe und Pillenform.
|
||||
|
||||
- `BAR_H = 22px` Basis-Höhe. Segmentierte Pillen `BAR_H + 2 = 24px`
|
||||
(`box-sizing:border-box`, 1px Rand inbegriffen).
|
||||
- Pille: `border:1px solid var(--border)`, `border-radius:999px`,
|
||||
`background:var(--input)`. Hover (interaktiv): `border-color:var(--accent-border)`,
|
||||
`background:var(--accent-dim)`.
|
||||
- Aktiver Zustand (Toggle AN / aktive Segmentzelle): `background:var(--accent)`,
|
||||
`color:#fff`.
|
||||
|
||||
### BarCombo (Pillen-Dropdown)
|
||||
Wir haben bereits `src/ui/Dropdown.tsx`. Der Dropdown-Trigger MUSS optisch der
|
||||
Pille entsprechen (Höhe 24, radius 999, obige Farben). Ein optionales Icon sitzt
|
||||
LINKS **ausserhalb** der Pille (18px breit, `var(--muted)`), ein optionaler
|
||||
Zahnrad-Knopf („settings") sitzt rechts **innerhalb** der Pille. Prüfen, ob
|
||||
`Dropdown` bereits so aussieht; falls nicht → Trigger-CSS angleichen (Klasse
|
||||
`tb-dd-trigger`). KEINE zweite Dropdown-Implementierung bauen.
|
||||
|
||||
### Segmentpille (3er/4er)
|
||||
Aussencontainer `display:inline-flex; height:24px; border:1px solid var(--border);
|
||||
border-radius:999px; overflow:hidden`. Zellen ohne eigenen Radius; interne Trenner
|
||||
über `border-left:1px solid var(--border)` (erste Zelle ohne). Aktive Zelle
|
||||
`var(--accent)`/#fff, inaktiv `var(--input)`/`var(--ink)`, Hover
|
||||
`var(--accent-dim)`/`var(--accent)`. Genutzt für: Ansichts-Icons, Zoom (%/fit/center),
|
||||
**B/I/U**, **L/C/R**.
|
||||
|
||||
### BarButton (quadratischer Icon-Knopf)
|
||||
22×22, `border-radius:999px`, sonst wie Pille. Aktiv = Akzentfüllung, Icon #fff.
|
||||
|
||||
## 1. Reihenfolge der Gruppen (links → rechts)
|
||||
|
||||
1. Marke (bestehend, unverändert).
|
||||
2. Ansichts-Gruppe (bestehend `view-grid`; Zellen auf Segmentpillen-Look bringen).
|
||||
3. Sichtbarkeits-Kombinationen (bestehend, `BarCombo`-Look).
|
||||
4. Detailgrad + Massstab (gestapelt, `BarCombo`-Look).
|
||||
5. **Massstab/Zoom-Cluster NEU** (siehe §2) — ersetzt die heutige Gruppe mit der
|
||||
DOPPELTEN Zoom-Anzeige.
|
||||
6. Darstellungsart (bestehend, `BarCombo`).
|
||||
7. **Text-Gruppe NEU** (siehe §3) — die zentrale neue Leiste.
|
||||
8. Referenzlinien / Linien-Modus (bestehend).
|
||||
9. Rechts: Layout · Ressourcen · Projektname.
|
||||
|
||||
## 2. Massstab/Zoom-Cluster (ersetzt Doppel-Zoom-Bug)
|
||||
|
||||
HEUTE FALSCH: In `TopBar.tsx` wird `tb-zoom` (Zoom %) ZWEIMAL gerendert
|
||||
(einmal im `tb-zoomstack`, einmal darunter als eigener `<span>`). Die zweite,
|
||||
lose `<span className="tb-zoom">…%</span>` ersatzlos ENTFERNEN.
|
||||
|
||||
NEUES Layout — 2×2-Raster (`display:grid; grid-template-columns:auto auto;
|
||||
gap:4px 6px; align-items:center`):
|
||||
|
||||
- **Spalte 1, beide Zeilen** (`grid-row:1 / span 2`): EINE kombinierte Stat-Pille,
|
||||
`width:70px`, Höhe `BAR_H*2+6 = 50px`, `border-radius:14px` (NICHT 999),
|
||||
`border:1px solid var(--border)`, `background:var(--input)`, Innen zwei Zeilen
|
||||
mittig, getrennt durch 1px-Linie (`var(--border)`):
|
||||
- oben: Live-Massstab `1:N` (Akzentfarbe, `var(--font-mono)`, 11px, 700)
|
||||
- unten: Zoom `NN%` (`var(--ink-2)`, mono, 11px)
|
||||
- „am Massstab" (Zoom==gewählter Massstab): Pille `background:var(--accent-dim)`,
|
||||
`border-color:var(--accent)`, Text `var(--accent)`.
|
||||
- Nicht-Plan-Ansicht: beide Werte „—".
|
||||
- **Spalte 2, Zeile 1**: Massstab-Dropdown (`BarCombo`, ~140px, mono) + Print/PDF
|
||||
bleibt separat. (Massstab-Dropdown ist der bestehende `scaleOptions`-Dropdown.)
|
||||
- **Spalte 2, Zeile 2**: Zoom-Segmentpille mit 3 Zellen — `%` (=`onZoom100`,
|
||||
Label „1:1"/100 %), `fit_screen` (=`onFit`), `center_focus_strong`
|
||||
(=`onFitSelection`). Material-Icons. Daneben ggf. Referenzlinien-BarButton.
|
||||
|
||||
Export-Knöpfe (PDF/DXF) wandern in eine eigene kleine BarButton-Reihe rechts vom
|
||||
Cluster (Icons `picture_as_pdf` / `download`) ODER bleiben Pillen — Hauptsache
|
||||
NICHT mehr Teil des Zoom-Blocks, damit der Cluster ruhig bleibt.
|
||||
|
||||
## 3. Text-Gruppe in der Oberleiste (NEU – Kern dieser Aufgabe)
|
||||
|
||||
Immer sichtbar. 3×2-Raster (`grid-template-columns:110px 130px 80px; gap:4px 6px`).
|
||||
Setzt Defaults für neuen Text UND formatiert die aktuelle Auswahl live.
|
||||
|
||||
Zeile 1:
|
||||
- **Stil-Preset** `BarCombo` (110px): Optionen aus `DEFAULT_PRESETS`
|
||||
(Titel/Untertitel/Label/Notiz) + „— Stil —".
|
||||
- **Font** `BarCombo` (130px): Systemfont-Liste (mind. Helvetica, Arial, Inter,
|
||||
Times New Roman, Georgia, Courier New). `applyMark(doc,range,'font',v)`.
|
||||
- **Grösse** `BarCombo` (80px): Presets in pt `[8,9,10,11,12,14,18,24,36,48]`
|
||||
+ „Eigene…" → Zahl-Input-Pille. `applyMark(...,'sizePt',n)`.
|
||||
|
||||
Zeile 2:
|
||||
- **B/I/U** Segmentpille (110px, Icons `format_bold`/`format_italic`/
|
||||
`format_underlined`): `toggleMark(doc,range,'bold'|'italic'|'underline')`.
|
||||
Aktiv-Zustand aus `isMarkActive(doc,range,mark)`.
|
||||
- **L/C/R** Segmentpille (130px, Icons `format_align_left`/`_center`/`_right`):
|
||||
setzt `paragraph.align` im Bereich.
|
||||
- **„+"-Text-Button** (80px, BarButton/Pille, Icon `add`, Label „Text"): startet
|
||||
das Text-Werkzeug (neues Textobjekt platzieren). Falls das Text-Annotation-
|
||||
Werkzeug noch nicht existiert, Button vorerst `disabled` mit Tooltip
|
||||
(kein stiller No-Op) — aber Verdrahtung vorbereiten.
|
||||
|
||||
**Auswahl-Bewusstsein (WICHTIG):** Prop `textTarget` (oder aus App-State): entweder
|
||||
`null` (nichts Text-artiges selektiert → Controls setzen nur Defaults, Ränder
|
||||
normal) ODER `{ doc: RichTextDoc, range: TextRange|null, apply: (doc)=>void }`
|
||||
für den aktuell selektierten Raumstempel/Text. Ist `textTarget != null`, tragen
|
||||
die Zeile-2-Pillen `border-color:var(--accent)` (Akzent-Glow), und alle Aktionen
|
||||
wirken auf `textTarget.doc` via `textTarget.apply(newDoc)`. Ohne aktive Range
|
||||
(nur Objekt selektiert, kein Editor offen) wirkt Formatierung auf das GANZE Doc.
|
||||
|
||||
Quelle der `textTarget`-Daten: der Raum-Agent exponiert Stempel-Doc + Setter
|
||||
(`setRoomStampDoc(roomId, doc)`) im App-State (siehe Peer-Absprache). App leitet
|
||||
für den selektierten Raum `{doc: room.stampDoc, range: activeStampRange,
|
||||
apply: d => setRoomStampDoc(room.id, d)}` an die Text-Gruppe.
|
||||
|
||||
## 4. Text-Inhalt bearbeiten: kleines Fenster (kein Footer)
|
||||
|
||||
Doppelklick auf einen Raumstempel/ein Textobjekt öffnet ein **schwebendes
|
||||
Dialog-Fenster** (nicht den Footer, kein Panel-Aufklappen). Umsetzung: neue
|
||||
Komponente `src/ui/TextEditorDialog.tsx` — ein zentriertes/абgesetztes Fenster
|
||||
(~560×420, `--shadow-3`, `border-radius:8px`, Titel „Text bearbeiten",
|
||||
Kopf mit Schliessen-✕), Body = der bestehende `src/text/RichTextEditor.tsx`
|
||||
(er bringt seine eigene Mini-Toolbar mit — das ist hier ok, weil es ein eigenes
|
||||
Fenster ist), Fuss = „Abbrechen" / „Übernehmen". „Übernehmen" ruft
|
||||
`setRoomStampDoc(roomId, editedDoc)`.
|
||||
|
||||
Der Doppelklick-Handler lebt in App (Plan-View/Viewport → onDoubleClick auf
|
||||
Stempel-Hit → `openTextEditor(roomId)`), NICHT im Footer. Falls der Raum-Agent
|
||||
den Footer benutzt hat: diesen Pfad entfernen und durch den Dialog ersetzen.
|
||||
|
||||
## 5. Farb-/Stil-Tokens
|
||||
|
||||
Bestehende CSS-Variablen weiterverwenden (`--panel`,`--input`,`--border`,
|
||||
`--accent`,`--accent-dim`,`--accent-border`,`--ink`,`--ink-2`,`--muted`,
|
||||
`--font-mono`,`--shadow-1..3`). KEINE neuen Farbwerte hart kodieren. Falls ein
|
||||
Token fehlt (z. B. `--accent-border`), prüfen und ggf. aus bestehenden ableiten.
|
||||
|
||||
## 6. i18n
|
||||
|
||||
Neue Keys in de.ts UND en.ts: `text.style`, `text.font`, `text.size`,
|
||||
`text.size.custom`, `text.bold/italic/underline`, `text.align.left/center/right`,
|
||||
`text.add`, `text.add.hint`, `text.editTitle`, `text.apply`, `text.cancel`,
|
||||
`text.selectedHint`. Presets-Namen über bestehende `rt.*`/Preset-Keys, sofern da.
|
||||
|
||||
## 7. Gate (Pflicht)
|
||||
|
||||
`rm -f tsconfig.tsbuildinfo && npx tsc -b` grün, `npm run build` grün, keine
|
||||
Trace-Scan (grep auf Co-Authored/Generated), Boot-Probe
|
||||
(`node scripts/probe.mjs`) ohne Konsolenfehler, Screenshot der Oberleiste.
|
||||
KEIN Commit.
|
||||
@@ -0,0 +1,403 @@
|
||||
# truck-Integration — Profil-Extrusion (B-Rep → 3D-Mesh)
|
||||
|
||||
> Übergabe-Dokument für die Implementierung. Lies zuerst CONVENTIONS.md.
|
||||
|
||||
## Ziel
|
||||
|
||||
Nutzer zeichnen im Grundriss ein geschlossenes Polygon (z. B. L-Profil einer Stütze)
|
||||
und können es als 3D-Körper auf eine Höhe extrudieren. Das Ergebnis erscheint sofort
|
||||
im 3D-Viewport neben Wänden und Decken.
|
||||
|
||||
**MVP-Scope (dieser Auftrag):**
|
||||
- Neue Rust/WASM-Crate `trucksolid` mit zwei Funktionen: `extrude_polygon` + `extrude_circle`
|
||||
- Neue TS-Wrapper-Datei `src/engine/truckSolid.ts` (WASM laden + typisierte API)
|
||||
- KEIN neues Werkzeug, KEIN UI — nur die Geometrie-Schicht. UI kommt in einem
|
||||
separaten Folgeauftrag.
|
||||
|
||||
---
|
||||
|
||||
## Architektur-Kontext
|
||||
|
||||
```
|
||||
src-tauri/
|
||||
render3d/ ← bestehend: wgpu-Renderer (wasm-pack → pkg3d/)
|
||||
kernel2d/ ← bestehend: 2D-Geometrie-Kern
|
||||
trucksolid/ ← NEU (dieser Auftrag)
|
||||
Cargo.toml
|
||||
src/
|
||||
lib.rs
|
||||
|
||||
src/engine/
|
||||
pkg3d/ ← render3d WASM-Output
|
||||
pkgTruck/ ← NEU: trucksolid WASM-Output (wasm-pack Ziel)
|
||||
truckSolid.ts ← NEU: TS-Wrapper
|
||||
|
||||
src/plan/
|
||||
toWalls3d.ts ← bestehend: baut RenderScene; emitMeshes() muss erweitert werden
|
||||
```
|
||||
|
||||
Das Koordinatensystem in DOSSIER ist **Modell-Meter: X = rechts, Y = oben (Grundriss),
|
||||
Z = Höhe**. render3d erwartet `positions` im gleichen System — `mesh.rs` macht intern
|
||||
`(x, y, z) → (x, z, y)` für wgpu (Y-up world). Truck arbeitet in 3D; wir bauen
|
||||
das Polygon in der XY-Ebene (z=0) und extrudieren nach +Z.
|
||||
|
||||
---
|
||||
|
||||
## 1 — Rust-Crate `src-tauri/trucksolid/`
|
||||
|
||||
### 1.1 Cargo.toml
|
||||
|
||||
Exakt dasselbe Muster wie `src-tauri/kernel2d/Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[workspace] # entkoppelt vom cad-tauri-Workspace (Pflicht)
|
||||
|
||||
[package]
|
||||
name = "trucksolid"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Profil-Extrusion via truck (B-Rep → tesselliertes Mesh für render3d)"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
web = [
|
||||
"dep:wasm-bindgen",
|
||||
"dep:serde_json",
|
||||
"dep:console_error_panic_hook",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1", optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
console_error_panic_hook = { version = "0.1", optional = true }
|
||||
|
||||
# truck: nur die stabilen Crates (KEIN truck-modeling — Booleans instabil)
|
||||
truck-geometry = "0.3"
|
||||
truck-topology = "0.3"
|
||||
truck-rendimesh = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1"
|
||||
```
|
||||
|
||||
**Wichtig:** `truck-modeling` (die Crate mit Boolean-Operatoren) wird NICHT eingebunden.
|
||||
Nur `truck-geometry`, `truck-topology` und `truck-rendimesh` — die sind stabil.
|
||||
|
||||
### 1.2 src/lib.rs — Kern-Logik
|
||||
|
||||
#### Eingabe/Ausgabe-Typen
|
||||
|
||||
```rust
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Input: flaches Array [x0,y0, x1,y1, ...] in Modell-Metern (Grundriss-XY).
|
||||
/// Muss ≥ 3 Punkte enthalten; Wiederholung des ersten Punkts am Ende optional.
|
||||
/// Reihenfolge CCW oder CW — truck normalisiert selbst.
|
||||
#[derive(Deserialize)]
|
||||
pub struct ExtrudePolyInput {
|
||||
pub points: Vec<f64>, // flat: [x0,y0, x1,y1, ...]
|
||||
pub height: f64, // Extrusionshöhe in Metern (> 0)
|
||||
}
|
||||
|
||||
/// Output: trianguliertes Mesh, kompatibel mit render3d::types::MeshInput.
|
||||
/// positions: flat [x0,y0,z0, x1,y1,z1, ...] in Modell-Metern
|
||||
/// indices: Dreiecks-Indizes (je 3 = 1 Dreieck), 0-basiert
|
||||
#[derive(Serialize)]
|
||||
pub struct MeshOutput {
|
||||
pub positions: Vec<f32>,
|
||||
pub indices: Vec<u32>,
|
||||
}
|
||||
```
|
||||
|
||||
#### Polygon-Extrusion (truck-API)
|
||||
|
||||
```rust
|
||||
use truck_geometry::prelude::*;
|
||||
use truck_topology::*;
|
||||
|
||||
pub fn extrude_polygon_core(pts: &[(f64, f64)], height: f64) -> Result<MeshOutput, String> {
|
||||
if pts.len() < 3 { return Err("min 3 Punkte".into()); }
|
||||
if height <= 0.0 { return Err("height muss > 0 sein".into()); }
|
||||
|
||||
// 1. Punkte in der XY-Ebene (z=0) als truck-Vertices
|
||||
let verts: Vec<Vertex<Point3>> = pts.iter()
|
||||
.map(|(x, y)| Vertex::new(Point3::new(*x, *y, 0.0)))
|
||||
.collect();
|
||||
|
||||
// 2. Kanten: je zwei aufeinanderfolgende Vertices verbinden, Ring schliessen
|
||||
let edges: Vec<Edge<_, Line<Point3>>> = verts.windows(2)
|
||||
.chain(std::iter::once([verts.last().unwrap(), &verts[0]].as_slice()))
|
||||
.map(|w| {
|
||||
let p0 = *w[0].point();
|
||||
let p1 = *w[1].point();
|
||||
Edge::new(&w[0], &w[1], Line(p0, p1))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 3. Wire (geschlossener Kantenzug)
|
||||
let wire = Wire::from_iter(edges);
|
||||
|
||||
// 4. Planare Face aus dem Wire
|
||||
// truck-topology::Face::new braucht die äussere Boundary + optional Holes
|
||||
let face = Face::new(vec![wire]);
|
||||
|
||||
// 5. Lineare Extrusion: tsweep entlang +Z um `height`
|
||||
let solid = face.tsweep(&Vector3::new(0.0, 0.0, height));
|
||||
|
||||
// 6. Tessellieren (chord-tolerance in Metern — 0.005 = 5 mm)
|
||||
use truck_rendimesh::MeshedShape;
|
||||
let mesh = solid.triangulation(0.005).to_polygon();
|
||||
|
||||
// 7. positions + indices extrahieren
|
||||
let positions: Vec<f32> = mesh.positions().iter()
|
||||
.flat_map(|p| [p.x as f32, p.y as f32, p.z as f32])
|
||||
.collect();
|
||||
let indices: Vec<u32> = mesh.tri_faces().iter()
|
||||
.flat_map(|tri| tri.iter().map(|idx| idx.pos as u32))
|
||||
.collect();
|
||||
|
||||
Ok(MeshOutput { positions, indices })
|
||||
}
|
||||
```
|
||||
|
||||
> **Achtung:** Die exakte truck-API (Methoden-Namen, Trait-Imports, tsweep-Signatur)
|
||||
> kann je nach veröffentlichter Version leicht abweichen. Prüfe die Docs von
|
||||
> `truck-topology 0.3` und `truck-rendimesh 0.3` auf docs.rs. Die Struktur oben
|
||||
> ist das Ziel-Pattern — passe Methoden-Signaturen an, wenn der Compiler es verlangt.
|
||||
> Ändere NICHT die Eingabe/Ausgabe-JSON-Struktur.
|
||||
|
||||
#### Kreis-Extrusion (Zylinder)
|
||||
|
||||
```rust
|
||||
pub fn extrude_circle_core(cx: f64, cy: f64, r: f64, height: f64) -> Result<MeshOutput, String> {
|
||||
// Kreis in XY-Ebene tessellieren (N Segmente je nach r),
|
||||
// dann wie extrude_polygon_core aufrufen.
|
||||
// Alternativ: truck-geometry::Ellipse/Circle direkt nutzen, falls vorhanden.
|
||||
let n = (2.0 * std::f64::consts::PI * r / 0.02).ceil().max(16.0) as usize;
|
||||
let pts: Vec<(f64, f64)> = (0..n)
|
||||
.map(|i| {
|
||||
let a = 2.0 * std::f64::consts::PI * i as f64 / n as f64;
|
||||
(cx + r * a.cos(), cy + r * a.sin())
|
||||
})
|
||||
.collect();
|
||||
extrude_polygon_core(&pts, height)
|
||||
}
|
||||
```
|
||||
|
||||
#### WASM-Bindings (Feature "web")
|
||||
|
||||
```rust
|
||||
#[cfg(feature = "web")]
|
||||
mod web {
|
||||
use super::*;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn init() {
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
|
||||
/// Extrudiert ein Polygon-Profil.
|
||||
/// `input_json`: `{ "points": [x0,y0,…], "height": 2.5 }`
|
||||
/// Rückgabe: `{ "positions": […], "indices": […] }` oder throws JsError.
|
||||
#[wasm_bindgen]
|
||||
pub fn extrude_polygon(input_json: &str) -> Result<String, JsError> {
|
||||
let input: ExtrudePolyInput = serde_json::from_str(input_json)
|
||||
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||
let pts: Vec<(f64, f64)> = input.points.chunks(2)
|
||||
.map(|c| (c[0], c[1]))
|
||||
.collect();
|
||||
let mesh = extrude_polygon_core(&pts, input.height)
|
||||
.map_err(|e| JsError::new(&e))?;
|
||||
serde_json::to_string(&mesh).map_err(|e| JsError::new(&e.to_string()))
|
||||
}
|
||||
|
||||
/// Extrudiert einen Kreis-Querschnitt (Zylinder).
|
||||
/// `input_json`: `{ "cx": 0, "cy": 0, "r": 0.15, "height": 3.0 }`
|
||||
#[wasm_bindgen]
|
||||
pub fn extrude_circle(input_json: &str) -> Result<String, JsError> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct In { cx: f64, cy: f64, r: f64, height: f64 }
|
||||
let i: In = serde_json::from_str(input_json)
|
||||
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||
let mesh = extrude_circle_core(i.cx, i.cy, i.r, i.height)
|
||||
.map_err(|e| JsError::new(&e))?;
|
||||
serde_json::to_string(&mesh).map_err(|e| JsError::new(&e.to_string()))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 Tests (headless, ohne Feature "web")
|
||||
|
||||
Mindestens diese drei Tests müssen mit `cargo test` grün sein:
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn quad_extrusion_vertex_count() {
|
||||
// 1m × 1m Quadrat, 2m hoch → 8 Eckpunkte minimum (6 Flächen × 2 Dreiecke)
|
||||
let pts = vec![(0.0,0.0),(1.0,0.0),(1.0,1.0),(0.0,1.0)];
|
||||
let m = extrude_polygon_core(&pts, 2.0).unwrap();
|
||||
assert!(m.positions.len() >= 8 * 3); // ≥ 8 Vertices × 3 floats
|
||||
assert_eq!(m.indices.len() % 3, 0); // vollständige Dreiecke
|
||||
assert!(m.indices.len() >= 12 * 3); // ≥ 12 Dreiecke (Quader)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn l_profile_extrusion() {
|
||||
// L-Profil: 6 Punkte
|
||||
let pts = vec![
|
||||
(0.0,0.0),(0.3,0.0),(0.3,0.1),(0.1,0.1),(0.1,0.3),(0.0,0.3),
|
||||
];
|
||||
let m = extrude_polygon_core(&pts, 3.0).unwrap();
|
||||
assert_eq!(m.indices.len() % 3, 0);
|
||||
assert!(m.positions.len() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cylinder_extrusion() {
|
||||
let m = extrude_circle_core(0.0, 0.0, 0.15, 3.0).unwrap();
|
||||
assert_eq!(m.indices.len() % 3, 0);
|
||||
assert!(m.positions.len() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_too_few_points() {
|
||||
assert!(extrude_polygon_core(&[(0.0,0.0),(1.0,0.0)], 1.0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_height() {
|
||||
let pts = vec![(0.0,0.0),(1.0,0.0),(0.5,1.0)];
|
||||
assert!(extrude_polygon_core(&pts, 0.0).is_err());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2 — Build-Script (package.json)
|
||||
|
||||
Füge in `package.json` unter `"scripts"` hinzu:
|
||||
|
||||
```json
|
||||
"build:truck": "wasm-pack build src-tauri/trucksolid --release --target web --out-dir ../../src/engine/pkgTruck --out-name trucksolid --no-default-features --features web"
|
||||
```
|
||||
|
||||
Und in `src-tauri/Cargo.toml` unter `exclude`:
|
||||
|
||||
```toml
|
||||
exclude = ["render2d", "render3d", "geometry", "kernel2d", "dwgimport", "trucksolid"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3 — TS-Wrapper `src/engine/truckSolid.ts`
|
||||
|
||||
Gleiche Lade-Pattern wie `src/plan/useWasmPlanRenderer.ts` (render2d) und
|
||||
`src/viewport/useWasm3dRenderer.ts` (render3d):
|
||||
|
||||
```typescript
|
||||
// Lazy-Singleton: WASM einmalig laden, dann gecacht.
|
||||
let modulePromise: Promise<typeof import("../../engine/pkgTruck/trucksolid")> | null = null;
|
||||
|
||||
async function getModule() {
|
||||
if (!modulePromise) {
|
||||
modulePromise = import("../../engine/pkgTruck/trucksolid").then(async (m) => {
|
||||
await m.default(); // WASM-Binary initialisieren
|
||||
return m;
|
||||
});
|
||||
}
|
||||
return modulePromise;
|
||||
}
|
||||
|
||||
export interface ExtrudedMesh {
|
||||
positions: number[];
|
||||
indices: number[];
|
||||
}
|
||||
|
||||
/** Extrudiert ein geschlossenes Polygon-Profil (Modell-Meter XY) um `height` m. */
|
||||
export async function extrudePolygon(
|
||||
points: number[], // flat [x0,y0, x1,y1, ...]
|
||||
height: number,
|
||||
): Promise<ExtrudedMesh> {
|
||||
const m = await getModule();
|
||||
const json = m.extrude_polygon(JSON.stringify({ points, height }));
|
||||
return JSON.parse(json) as ExtrudedMesh;
|
||||
}
|
||||
|
||||
/** Extrudiert einen Kreis-Querschnitt (Zylinder). */
|
||||
export async function extrudeCircle(
|
||||
cx: number, cy: number, r: number, height: number,
|
||||
): Promise<ExtrudedMesh> {
|
||||
const m = await getModule();
|
||||
const json = m.extrude_circle(JSON.stringify({ cx, cy, r, height }));
|
||||
return JSON.parse(json) as ExtrudedMesh;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4 — Integration in toWalls3d.ts (Vorbereitung)
|
||||
|
||||
In `src/plan/toWalls3d.ts` ist `RMeshKind` bereits definiert:
|
||||
|
||||
```typescript
|
||||
export type RMeshKind = "terrain" | "imported";
|
||||
```
|
||||
|
||||
Erweitere auf `"extrusion"` (damit der Renderer später eine eigene Farbe/Darstellung
|
||||
wählen kann, auch wenn heute noch kein Unterschied besteht):
|
||||
|
||||
```typescript
|
||||
export type RMeshKind = "terrain" | "imported" | "extrusion";
|
||||
```
|
||||
|
||||
Die `emitMeshes()`-Funktion liest bereits `project.drawings2d` und ähnliche Arrays.
|
||||
Für extrudierte Körper wird es später ein `project.extrudedSolids`-Array geben
|
||||
(oder ähnlich — das ist Teil des UI-Folgeauftrags). Die `emitMeshes()`-Erweiterung
|
||||
kommt dann.
|
||||
|
||||
---
|
||||
|
||||
## 5 — Verifikations-Checkliste
|
||||
|
||||
Bevor du als fertig meldest, müssen alle Punkte grün sein:
|
||||
|
||||
- [ ] `cd src-tauri/trucksolid && cargo test` → alle 5 Tests grün, keine Warnings
|
||||
- [ ] `npm run build:truck` → `src/engine/pkgTruck/trucksolid.js` + `.wasm` erzeugt
|
||||
- [ ] `npx tsc --noEmit` (aus Root) → 0 Fehler
|
||||
- [ ] `npx vitest run` → alle bestehenden Tests weiterhin grün (keine Regression)
|
||||
- [ ] `src-tauri/Cargo.toml` hat `"trucksolid"` im `exclude`-Array
|
||||
- [ ] `package.json` hat `"build:truck"` im `"scripts"`-Block
|
||||
|
||||
---
|
||||
|
||||
## 6 — Was NICHT in diesem Auftrag
|
||||
|
||||
- Kein neues UI/Werkzeug (kommt später)
|
||||
- Kein `truck-modeling` (Boolean-Operationen — instabil upstream)
|
||||
- Kein STEP-Export (kommt in Folgeauftrag wenn Basisschicht steht)
|
||||
- Kein Eintrag in `project.extrudedSolids` oder Store (UI-Auftrag)
|
||||
- Keine Änderungen an render3d, generatePlan, App.tsx
|
||||
|
||||
---
|
||||
|
||||
## 7 — Koordinatensystem-Reminder
|
||||
|
||||
| DOSSIER-Modell | truck (im Code) | render3d (wgpu) |
|
||||
|---|---|---|
|
||||
| X = rechts | X = rechts | X = rechts |
|
||||
| Y = oben (Grundriss) | Y = oben (Grundriss) | Z = oben (Y-up swapped) |
|
||||
| Z = Höhe | Z = Höhe | Y = Höhe |
|
||||
|
||||
`mesh.rs` in render3d macht bereits `(model.x, model.y, model.z) → (x, z, y)` beim
|
||||
Hochladen. Du musst also in truck DOSSIER-Koordinaten verwenden (XY-Ebene = Grundriss,
|
||||
Z = Höhe) — das ist konsistent mit der bestehenden `MeshInput.positions`-Konvention.
|
||||
@@ -0,0 +1,581 @@
|
||||
# WebGL2 GPU-Accelerated 2D Plan Renderer — Architecture
|
||||
|
||||
## Executive Summary
|
||||
|
||||
A WebGL2 canvas renderer for PlanView's heavy geometry (polygons, lines, hatches) with CPU fallback. Geometry tessellates once per plan and caches; pan/zoom only updates a transform-matrix uniform. Screen-space stroke width via vertex shader normal expansion. Thin SVG overlay handles text, grips, snap markers, tool preview.
|
||||
|
||||
**No npm dependencies** — raw WebGL2 + TypeScript.
|
||||
|
||||
---
|
||||
|
||||
## Current State (SVG Bottleneck)
|
||||
|
||||
**PlanView.tsx** renders `Primitive[]` (polygon/line/arc/text) → SVG DOM:
|
||||
- ~2500 LOC: pan/zoom via viewBox, toScreen() scaling (1 meter = 90 viewBox units)
|
||||
- `Primitive` types (generatePlan.ts:133):
|
||||
- **polygon**: `pts: Vec2[]`, fill/stroke/strokeWidthMm, hatch (solid/insulation/diagonal/crosshatch)
|
||||
- **line**: a/b endpoints, className, weightMm, dash[], optional color
|
||||
- **arc**: center, from/to points, r, className, weightMm, dash[]
|
||||
- **text**: anchor, RichTextDoc, roomStamp metadata
|
||||
- Bottleneck: **pan/zoom re-renders entire SVG DOM** → Cairo rasterizes geometry at 144 Hz
|
||||
|
||||
**Key constants:**
|
||||
- `PX_PER_M = 90` (viewBox units per meter; Modell-Y up → SVG-Y down via negation)
|
||||
- `PAD = 60` (margin in viewBox units)
|
||||
- `mmToPx(mm) = (mm / 25.4) * dpi()` (stroke width: constant screen-px via non-scaling-stroke)
|
||||
- `ZOOM_MAX/MIN = 50/0.2` (pan/zoom bounds)
|
||||
|
||||
---
|
||||
|
||||
## Architecture: PlanRenderer (WebGL2 + Fallback SVG)
|
||||
|
||||
### Module Structure
|
||||
|
||||
```
|
||||
src/plan/
|
||||
├── PlanRenderer.ts (Main GPU/CPU dispatcher)
|
||||
├── glPlan/
|
||||
│ ├── glPlanCompile.ts (Tessellation & buffer upload)
|
||||
│ ├── glPlanShaders.ts (Vertex/fragment sources + compilation)
|
||||
│ ├── glPlanRender.ts (Draw loop: matrix uniform, state mgmt)
|
||||
│ └── glPlanTypes.ts (TypeScript interfaces for GPU data)
|
||||
└── PlanView.tsx (React wrapper, unchanged API)
|
||||
```
|
||||
|
||||
### High-Level Flow
|
||||
|
||||
```
|
||||
PlanView.tsx
|
||||
↓ [receives plan: Plan]
|
||||
↓
|
||||
PlanRenderer (new abstraction)
|
||||
↓
|
||||
├─→ GPU path [if WebGL2 available && flag=true]
|
||||
│ ├─ glPlanCompile() → upload tessellated geometry to VRAM
|
||||
│ ├─ glPlanRender() → draw with pan/zoom matrix uniform
|
||||
│ └─ [fast pan/zoom via matrix only]
|
||||
│
|
||||
└─→ Fallback: SVG [if WebGL fails || flag=false]
|
||||
└─ existing PlanView render path (toScreen + DOM)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GPU Path: Tessellation & Shaders
|
||||
|
||||
### 1. Tessellation Strategy
|
||||
|
||||
#### **Polygon** → Fans + Ear Clipping
|
||||
- **Input**: Primitive.polygon = { pts: Vec2[], fill, stroke, strokeWidthMm, hatch }
|
||||
- **Output**: Indexed triangle mesh
|
||||
- **Algorithm**: Earcut2D (existing JS library logic, inlined to avoid npm)
|
||||
- Convert polygon pts to 2D float32 array in **world space** (meters)
|
||||
- Earcut → triangle indices
|
||||
- Store: `{ vertices: Float32Array, indices: Uint32Array, color: vec4, hasHatch: bool }`
|
||||
- **Hatch rendering**: Bake hatch as texture or re-implement in fragment shader (MVP: solid fill only; hatch deferred)
|
||||
|
||||
#### **Line** → Quad Expansion (Screen-Space Width)
|
||||
- **Input**: Primitive.line = { a, b, weightMm, cls, dash?, color }
|
||||
- **Output**: Degenerate quad (2 triangles) with screen-space normal offset
|
||||
- **Strategy**:
|
||||
1. Vertex shader receives `{ pos: vec2, side: float }` (side = ±1 for left/right edge)
|
||||
2. Transform pos to clip space via matrix uniform
|
||||
3. Compute screen-space perpendicular via `dFdx/dFdy` or pre-compute normal in CPU
|
||||
4. Expand by `(weightMm / 25.4) * dpi * (screenPixelsPerClipUnit)` in clip space
|
||||
5. Fragment shader: solid color (no dash MVP; dashing deferred or CPU pre-tessellation)
|
||||
|
||||
#### **Arc** → Line Segments (Polyline → Quads)
|
||||
- **Input**: Primitive.arc = { center, from, to, r, weightMm, cls, dash }
|
||||
- **Output**: Tessellate arc to ~30 line segments (adaptive based on radius/zoom), expand each as quad
|
||||
- Fallback: SVG arc for MVP
|
||||
|
||||
#### **Text, Grips, Snap-Markers, Tool-Preview**
|
||||
- **Stays in SVG overlay** (thin, non-bottleneck)
|
||||
- Render above WebGL canvas at z-order 1
|
||||
|
||||
---
|
||||
|
||||
### 2. Shader Sources (GLSL 3.00 ES)
|
||||
|
||||
#### **Vertex Shader: Solid Fill (polygon)**
|
||||
```glsl
|
||||
#version 300 es
|
||||
precision highp float;
|
||||
|
||||
uniform mat4 viewProjection; // pan/zoom as 2×3 affine (expand to mat4)
|
||||
|
||||
layout(location=0) in vec2 position; // world-space (meters)
|
||||
layout(location=1) in vec4 color; // fill color
|
||||
|
||||
out VS_OUT {
|
||||
flat vec4 vertexColor;
|
||||
} vs_out;
|
||||
|
||||
void main() {
|
||||
vec4 clipPos = viewProjection * vec4(position, 0.0, 1.0);
|
||||
gl_Position = clipPos;
|
||||
vs_out.vertexColor = color;
|
||||
}
|
||||
```
|
||||
|
||||
#### **Vertex Shader: Screen-Space Stroked Line**
|
||||
```glsl
|
||||
#version 300 es
|
||||
precision highp float;
|
||||
|
||||
uniform mat4 viewProjection; // world → clip space
|
||||
uniform vec2 screenSize; // canvas (width, height) in pixels
|
||||
uniform float strokeWidthMm; // millimeters
|
||||
uniform float dpi; // 96 * devicePixelRatio
|
||||
|
||||
layout(location=0) in vec2 position; // world-space endpoint
|
||||
layout(location=1) in float sideFlag; // ±1.0 (left/right edge)
|
||||
layout(location=2) in vec4 lineColor; // stroke color
|
||||
|
||||
out VS_OUT {
|
||||
flat vec4 vertexColor;
|
||||
} vs_out;
|
||||
|
||||
void main() {
|
||||
vec4 clipPos = viewProjection * vec4(position, 0.0, 1.0);
|
||||
|
||||
// Convert stroke width (mm) → screen pixels
|
||||
float strokePx = (strokeWidthMm / 25.4) * dpi;
|
||||
// Convert screen pixels → normalized device coords (NDC)
|
||||
// NDC ∈ [-1,1]²; screen (0,screenSize) → NDC [-1,1]
|
||||
float strokeNdc = (strokePx / screenSize.x) * 2.0;
|
||||
|
||||
// Expand in clip space (simple; assumes aspect ≈ 1)
|
||||
vec4 expanded = clipPos + vec4(sideFlag * strokeNdc, 0.0, 0.0, 0.0);
|
||||
|
||||
gl_Position = expanded;
|
||||
vs_out.vertexColor = lineColor;
|
||||
}
|
||||
```
|
||||
|
||||
#### **Fragment Shader (both)**
|
||||
```glsl
|
||||
#version 300 es
|
||||
precision highp float;
|
||||
|
||||
in VS_OUT {
|
||||
flat vec4 vertexColor;
|
||||
} fs_in;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
void main() {
|
||||
fragColor = fs_in.vertexColor;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. GPU Data Structures (TypeScript)
|
||||
|
||||
**glPlanTypes.ts:**
|
||||
```typescript
|
||||
export interface GLGeometryBatch {
|
||||
/** Vertex buffer: interleaved (x, y, [z if 3D], ...) in world space. */
|
||||
vertexBuffer: WebGLBuffer;
|
||||
vertexCount: number;
|
||||
|
||||
/** Index buffer (triangles for fill, degenerate quads for strokes). */
|
||||
indexBuffer: WebGLBuffer;
|
||||
indexCount: number;
|
||||
|
||||
/** Vertex Array Object (VAO) binds VBO + IBO. */
|
||||
vao: WebGLVertexArrayObject;
|
||||
|
||||
/** Per-batch metadata. */
|
||||
batches: Array<{
|
||||
kind: "polygon" | "line" | "arc";
|
||||
indexStart: number;
|
||||
indexCount: number;
|
||||
color: [r: number, g: number, b: number, a: number]; // RGBA [0,1]
|
||||
hasHatch: boolean;
|
||||
hatchPattern?: "solid" | "insulation" | "diagonal" | "crosshatch";
|
||||
strokeWidthMm?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface GLPlanRenderState {
|
||||
// Pan/zoom transform: world (meters) → clip space
|
||||
viewMatrix: Matrix3 | Matrix4; // 2×3 affine
|
||||
projMatrix: Matrix4; // orthographic
|
||||
|
||||
// Viewport size & DPI for screen-space stroke width
|
||||
screenWidth: number;
|
||||
screenHeight: number;
|
||||
dpi: number;
|
||||
|
||||
// Compiled shaders
|
||||
solidFillProgram: WebGLProgram;
|
||||
strokeProgram: WebGLProgram;
|
||||
|
||||
// Geometry cache (tessellated once per plan)
|
||||
geometryBatch: GLGeometryBatch | null;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MVP API: PlanRenderer Class
|
||||
|
||||
### Interface
|
||||
|
||||
```typescript
|
||||
export class PlanRenderer {
|
||||
/**
|
||||
* Create renderer with WebGL2 context + fallback config.
|
||||
*/
|
||||
constructor(
|
||||
canvas: HTMLCanvasElement,
|
||||
options?: {
|
||||
enableGpu?: boolean; // default: true
|
||||
enableGpuFallback?: boolean; // SVG fallback if GL fails
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Compile and cache geometry from primitives.
|
||||
* Call once per plan change.
|
||||
*/
|
||||
compilePlan(plan: Plan): Promise<void>;
|
||||
|
||||
/**
|
||||
* Set pan/zoom transform matrix.
|
||||
* Call on every view change (pan, zoom, fit).
|
||||
*/
|
||||
setViewMatrix(viewBox: { x, y, w, h }, canvasSize: { w, h }): void;
|
||||
|
||||
/**
|
||||
* Render one frame: clear, draw batches, composite.
|
||||
* Called from requestAnimationFrame loop.
|
||||
*/
|
||||
render(): void;
|
||||
|
||||
/**
|
||||
* Release WebGL resources.
|
||||
*/
|
||||
dispose(): void;
|
||||
|
||||
/**
|
||||
* Query GPU availability / fallback state.
|
||||
*/
|
||||
isGpuReady(): boolean;
|
||||
isFallbackActive(): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Usage in PlanView
|
||||
|
||||
**Before** (SVG only):
|
||||
```tsx
|
||||
function PlanView({ plan, ... }) {
|
||||
return (
|
||||
<svg ref={svgRef}>
|
||||
<defs>{hatches}</defs>
|
||||
{plan.primitives.map((p, i) => <PrimitiveShape ... />)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**After** (GPU + SVG fallback):
|
||||
```tsx
|
||||
function PlanView({ plan, ... }) {
|
||||
const rendererRef = useRef<PlanRenderer | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
rendererRef.current = new PlanRenderer(canvas, { enableGpu: true });
|
||||
rendererRef.current.compilePlan(plan);
|
||||
}, [plan]);
|
||||
|
||||
useEffect(() => {
|
||||
rendererRef.current?.setViewMatrix(view, { w: canvasWidth, h: canvasHeight });
|
||||
}, [view, canvasWidth, canvasHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
const frame = () => {
|
||||
rendererRef.current?.render();
|
||||
rafId = requestAnimationFrame(frame);
|
||||
};
|
||||
rafId = requestAnimationFrame(frame);
|
||||
return () => cancelAnimationFrame(rafId);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
{/* GPU canvas (or SVG fallback if GL unavailable) */}
|
||||
<canvas ref={canvasRef} style={{ position: "absolute" }} />
|
||||
{/* Thin SVG overlay: text, grips, snap-markers, tool preview */}
|
||||
<svg ref={svgRef} style={{ position: "absolute", zIndex: 1 }}>
|
||||
{/* text, grips, snaps only; geometry stays in WebGL */}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Flow: From Primitives → GPU
|
||||
|
||||
### 1. **Compile Phase** (glPlanCompile.ts)
|
||||
|
||||
```typescript
|
||||
export function compilePlan(gl: WebGL2RenderingContext, plan: Plan): GLGeometryBatch {
|
||||
const batches: BatchInfo[] = [];
|
||||
const vertices: number[] = [];
|
||||
const indices: number[] = [];
|
||||
let indexOffset = 0;
|
||||
|
||||
for (const prim of plan.primitives) {
|
||||
if (prim.kind === "polygon") {
|
||||
const { verts, inds } = tessellatePolygon(prim.pts);
|
||||
const color = parseColor(prim.fill);
|
||||
batches.push({
|
||||
kind: "polygon",
|
||||
indexStart: indexOffset,
|
||||
indexCount: inds.length,
|
||||
color,
|
||||
hasHatch: prim.hatch.pattern !== "none",
|
||||
hatchPattern: prim.hatch.pattern,
|
||||
});
|
||||
vertices.push(...verts);
|
||||
indices.push(...inds.map((i) => i + indexOffset));
|
||||
indexOffset += verts.length / 2;
|
||||
} else if (prim.kind === "line") {
|
||||
const { verts, inds } = tessellateLineQuad(prim.a, prim.b);
|
||||
const color = parseColor(prim.color || "black");
|
||||
batches.push({
|
||||
kind: "line",
|
||||
indexStart: indexOffset,
|
||||
indexCount: inds.length,
|
||||
color,
|
||||
strokeWidthMm: prim.weightMm,
|
||||
});
|
||||
vertices.push(...verts);
|
||||
indices.push(...inds.map((i) => i + indexOffset));
|
||||
indexOffset += verts.length / 2;
|
||||
}
|
||||
// arc → polyline → quads (deferred for MVP)
|
||||
}
|
||||
|
||||
const vbo = gl.createBuffer()!;
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
|
||||
|
||||
const ibo = gl.createBuffer()!;
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indices), gl.STATIC_DRAW);
|
||||
|
||||
const vao = gl.createVertexArray()!;
|
||||
gl.bindVertexArray(vao);
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
||||
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 8, 0); // position
|
||||
gl.enableVertexAttribArray(0);
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
|
||||
|
||||
return { vertexBuffer: vbo, indexBuffer: ibo, vao, batches, vertexCount: vertices.length, indexCount: indices.length };
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **Render Phase** (glPlanRender.ts)
|
||||
|
||||
```typescript
|
||||
export function renderPlan(
|
||||
gl: WebGL2RenderingContext,
|
||||
state: GLPlanRenderState,
|
||||
batch: GLGeometryBatch
|
||||
): void {
|
||||
gl.clearColor(1, 1, 1, 1); // white background
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
gl.useProgram(state.solidFillProgram);
|
||||
const mvpLoc = gl.getUniformLocation(state.solidFillProgram, "viewProjection");
|
||||
const mvp = mat4.multiply(state.projMatrix, state.viewMatrix);
|
||||
gl.uniformMatrix4fv(mvpLoc, false, mvp);
|
||||
|
||||
gl.bindVertexArray(batch.vao);
|
||||
|
||||
for (const b of batch.batches) {
|
||||
const colorLoc = gl.getUniformLocation(state.solidFillProgram, "vertexColor");
|
||||
gl.uniform4f(colorLoc, b.color[0], b.color[1], b.color[2], b.color[3]);
|
||||
|
||||
gl.drawElements(gl.TRIANGLES, b.indexCount, gl.UNSIGNED_INT, b.indexStart * 4);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tessellation Details
|
||||
|
||||
### Earcut (Polygon Triangulation)
|
||||
|
||||
**Inlined earcut logic (no npm):**
|
||||
```typescript
|
||||
function tessellatePolygon(pts: Vec2[]): { verts: number[]; inds: number[] } {
|
||||
// Convert Vec2[] → flat float array
|
||||
const coords = pts.flatMap((p) => [p.x, p.y]);
|
||||
|
||||
// Earcut2D: robust polygon triangulation
|
||||
// → Returns index array (triplets = triangles)
|
||||
const triangles = earcut(coords);
|
||||
|
||||
// Vertex buffer: just positions (x, y) in world space (meters)
|
||||
const verts = coords;
|
||||
|
||||
return { verts, inds: triangles };
|
||||
}
|
||||
|
||||
// Simplified earcut (full version ~200 LOC; reference libtess2 or earcut.js)
|
||||
function earcut(data: number[], hole?: number[], dim?: number): number[] {
|
||||
// ... iterative ear clipping, complexity O(n²) worst-case
|
||||
// Returns Uint32Array of triangle indices
|
||||
}
|
||||
```
|
||||
|
||||
### Line Quad Expansion
|
||||
|
||||
```typescript
|
||||
function tessellateLineQuad(
|
||||
a: Vec2, b: Vec2,
|
||||
widthMm: number = 0.5
|
||||
): { verts: number[]; inds: number[] } {
|
||||
// World-space endpoints; width (mm) will be expanded in vertex shader
|
||||
|
||||
// Create a degenerate quad: 2 triangles
|
||||
// Vertices: [a_left, a_right, b_left, b_right]
|
||||
// (normal expansion happens in VS)
|
||||
|
||||
const verts = [
|
||||
a.x, a.y, 0.0, // vertex 0: a, left flag
|
||||
a.x, a.y, 1.0, // vertex 1: a, right flag
|
||||
b.x, b.y, 0.0, // vertex 2: b, left flag
|
||||
b.x, b.y, 1.0, // vertex 3: b, right flag
|
||||
];
|
||||
|
||||
// Two triangles: (0, 1, 2) and (1, 3, 2)
|
||||
const inds = [0, 1, 2, 1, 3, 2];
|
||||
|
||||
return { verts, inds };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pan/Zoom Matrix Transform
|
||||
|
||||
### View Box → Clip Space
|
||||
|
||||
```typescript
|
||||
function buildViewMatrix(
|
||||
viewBox: { x, y, w, h },
|
||||
canvasSize: { w, h }
|
||||
): Matrix4 {
|
||||
// 1. World space (meters, origin at model 0,0) → viewBox units (PX_PER_M=90)
|
||||
const scale = PX_PER_M; // 1 meter → 90 viewBox units
|
||||
|
||||
// 2. ViewBox viewport: x,y,w,h in viewBox units → NDC [-1,+1]²
|
||||
// Orthographic projection (no perspective).
|
||||
const ortho = mat4.ortho(
|
||||
viewBox.x,
|
||||
viewBox.x + viewBox.w,
|
||||
viewBox.y,
|
||||
viewBox.y + viewBox.h,
|
||||
-1, 1
|
||||
);
|
||||
|
||||
// 3. Scale from viewBox units → world (invert PX_PER_M)
|
||||
const scaleMatrix = mat4.scale(mat4.identity(), [1/scale, 1/scale, 1]);
|
||||
|
||||
return mat4.multiply(ortho, scaleMatrix);
|
||||
}
|
||||
```
|
||||
|
||||
Whenever PlanView calls `setView(viewBox)` or `onWheel()` → call `setViewMatrix()` → GPU re-renders with new matrix uniform (no tessellation).
|
||||
|
||||
---
|
||||
|
||||
## Fallback Strategy: SVG Renderer Flag
|
||||
|
||||
**Global flag** in PlanView or app state:
|
||||
```typescript
|
||||
const [useGpuRenderer, setUseGpuRenderer] = useState(true);
|
||||
```
|
||||
|
||||
**Render path branching:**
|
||||
```typescript
|
||||
return useGpuRenderer && rendererRef.current?.isGpuReady()
|
||||
? <canvas ref={canvasRef} />
|
||||
: <svg ref={svgRef}>{/* existing SVG rendering */}</svg>;
|
||||
```
|
||||
|
||||
**When GL fails** (e.g., no WebGL2 support, Out-Of-Memory):
|
||||
1. Renderer catches error in `compilePlan()`
|
||||
2. Sets internal `fallbackActive = true`
|
||||
3. Returns gracefully (app renders SVG path instead)
|
||||
4. User sees same plan, slower but functional
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order (MVP → Iteration)
|
||||
|
||||
### Phase 1: Core (Week 1)
|
||||
1. **glPlanTypes.ts** — TypeScript interfaces for GPU state
|
||||
2. **glPlanShaders.ts** — Compile vertex/fragment shaders, handle GL errors
|
||||
3. **glPlanCompile.ts** — Tessellation (earcut inlined), buffer upload
|
||||
4. **glPlanRender.ts** — Draw loop, matrix uniform, clear/present
|
||||
5. **PlanRenderer.ts** — Main class, dispatcher (GPU vs SVG fallback)
|
||||
6. **PlanView.tsx** — Wire renderer, canvas overlay, canvas lifecycle
|
||||
|
||||
### Phase 2: Hatches & Lines (Week 2)
|
||||
- Improve line tessellation: proper screen-space width (dFdx/dFdy or pre-computed normals)
|
||||
- Hatch patterns: texture-based or procedural fragment shader (diagonal/insulation)
|
||||
- Arc tessellation: polyline → quads
|
||||
|
||||
### Phase 3: Polish (Week 3)
|
||||
- Stroke dashing via geometry or fragment shader
|
||||
- Greyed opacity blending
|
||||
- Hit testing integration (point-in-triangle for GPU)
|
||||
- Performance profiling, batch merging
|
||||
|
||||
---
|
||||
|
||||
## Performance Targets
|
||||
|
||||
| Operation | SVG (Current) | GPU (Target) | Notes |
|
||||
|-----------|---------------|--------------|-------|
|
||||
| **Tessellation** | — | 10–50 ms | Once per plan |
|
||||
| **Pan/Zoom 60 Hz** | 16 ms (re-render SVG) | <1 ms (matrix uniform) | Matrix upload negligible |
|
||||
| **Pan/Zoom 144 Hz** | 7 ms (bottleneck) | <0.5 ms | 28× speedup expected |
|
||||
| **Geometry: 1000 polygons** | 50–100 ms SVG render | 1–5 ms GPU draw | CPU tessellation pipelined |
|
||||
|
||||
User: AMD RX 7800 XT → easily capable of 4K+ geometry at 144 Hz.
|
||||
|
||||
---
|
||||
|
||||
## Known Deferred Items (Post-MVP)
|
||||
|
||||
- **Hatches**: Solid fill only MVP; insulation/diagonal/crosshatch in Phase 2 via texture or procedural shader
|
||||
- **Dashing**: Not in MVP (complex with screen-space strokes); either CPU pre-tessellation or fragment shader alpha-discard
|
||||
- **Arcs**: Fallback to SVG for MVP; GPU polyline expansion in Phase 2
|
||||
- **Text, Grips, Snaps**: Stay in SVG overlay indefinitely (no GPU benefit; text rendering nontrivial)
|
||||
- **Hit Testing**: Keep in CPU/SVG for MVP; GPU pick-buffer deferred
|
||||
- **Color/Opacity Blending**: Basic for MVP; advanced (multiply, screen, dodge) deferred
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- **Earcut.js**: https://github.com/mapbox/earcut — polygon triangulation (logic to inline)
|
||||
- **three.js line expansion**: https://github.com/mrdoob/three.js/blob/master/src/renderers/webgl/WebGLGeometries.js
|
||||
- **OpenGL Perspective Division**: https://en.wikibooks.org/wiki/OpenGL_Programming/Modern_OpenGL_Tutorial_Polygon_offset
|
||||
- **Screen-Space Stroke Width**: https://forum.libcinder.org/topic/smooth-line-rendering-using-geometry-shaders
|
||||
- PlanView source: `/home/karim/cad/src/plan/PlanView.tsx` (2500 LOC)
|
||||
- Primitive types: `/home/karim/cad/src/plan/generatePlan.ts:133`
|
||||
@@ -0,0 +1,80 @@
|
||||
# Briefing — Nativer wgpu-2D-Renderer (parallele Instanz)
|
||||
|
||||
> Für den Entwickler, der den nativen GPU-Renderer baut. Isoliert vom
|
||||
> Web-Renderer; koordiniert über dieses Dokument.
|
||||
|
||||
## COMMIT-REGEL (verbindlich)
|
||||
Dieses Repo darf **keinerlei Fremd-Tool-Hinweise** enthalten — nicht im Code,
|
||||
in Kommentaren oder in der Git-Historie. Kommentare deutsch, Identifier englisch.
|
||||
**Kein Commit ohne Absprache.**
|
||||
|
||||
## Warum
|
||||
Der 2D-Plan wird aktuell in einem WebGL-Canvas gerendert (Web-Stack). In Chromium
|
||||
ist das 144-Hz-flüssig, aber **Tauris Linux-Webview = WebKitGTK bremst** (langsamer
|
||||
Compositor, vermutlich 60-Hz-rAF-Cap). Bewiesen: dieselbe App im `chromium --app`-
|
||||
Fenster = butterweich, im Tauri-Fenster = zäh — trotz imperativem Pan, CSS-transform-
|
||||
Overlay, DMABUF-Tweaks. Siehe Memo `webkitgtk-bottleneck`.
|
||||
|
||||
**Lösung:** die schwere Grafik **nativ mit wgpu** rendern (Rust), die Webview macht
|
||||
nur noch UI-Chrome (Panels/Buttons). So bleibt Tauri (natives Rust-Backend, winziges
|
||||
Bundle) UND wir umgehen den Webview-Compositor komplett.
|
||||
|
||||
## Referenz-Implementierung (NICHT wegwerfen — 1:1 portierbar)
|
||||
Der WebGL-Renderer unter `src/plan/glPlan/` ist die **arbeitende Referenz**. Die
|
||||
harte Arbeit ist dort schon gelöst und portiert konzeptuell direkt nach wgpu:
|
||||
- `glPlanCompile.ts` — **Earcut-Triangulierung** (konkav-fähig), Linien→Quads mit
|
||||
Normale+Seiten-Flag, Batch-Merging, Farb-Parsing. Unit-Tests: `glPlanCompile.test.ts`.
|
||||
- `glPlanShaders.ts` — Vertex/Fragment (GLSL 300 es). **WGSL ≈ GLSL** — direkt übersetzbar.
|
||||
- Füll-VS: Bildschirm-Position → Clip via `viewProj`-Matrix.
|
||||
- Linien-VS: bildschirmkonstante bzw. papier-mm-Breite via Normalen-Offset im Clip
|
||||
(siehe `strokeScale`/`strokePx`-Logik — Papier-mm × Massstab N × meet-Skala).
|
||||
- `glPlanRender.ts` — Ortho-Matrix (aspekt-korrekt = SVG `xMidYMid meet`), Fill-/Line-
|
||||
Pass, `computeOrthoMatrix`.
|
||||
- Koordinaten-Konvention: BILDSCHIRM-Raum `sx = mx·PX_PER_M, sy = -my·PX_PER_M`
|
||||
(`PX_PER_M=90`), Modell-Y hoch → Bildschirm-Y runter.
|
||||
- Strichbreite = **echte Papier-mm im Massstab**: `Breite_px = mm · N/1000 · PX_PER_M ·
|
||||
meetSkala` (repliziert SVG `printStrokeVb`). Am Beispiel 0.35 mm @ 1:100 = 0.35 mm.
|
||||
|
||||
Die **Daten** kommen aus `src/plan/generatePlan.ts` → `Plan.primitives` (Union-Typ
|
||||
`Primitive`: polygon/line/arc/text). Text bleibt Overlay (nicht wgpu).
|
||||
|
||||
## Die EINE harte Frage (zuerst spiken/recherchieren)
|
||||
**Wie rendert wgpu in das Tauri-Fenster neben der Webview?** Optionen recherchieren:
|
||||
1. Natives Child-Surface unter der Webview (raw-window-handle), Webview transparent
|
||||
drüber für UI-Chrome. Vermutlich der Zielweg.
|
||||
2. Eigenes wgpu-Fenster (entkoppelt) — nur für den Spike, um Rendering von der
|
||||
Fenster-Integration zu trennen.
|
||||
3. wgpu→Textur→Webview — verwirft den Zweck (Copy-Overhead), nur Notnagel.
|
||||
|
||||
**Empfehlung:** Rendering-Spike ZUERST entkoppelt (Option 2, standalone `winit`+wgpu-
|
||||
Fenster), das die `Primitive` als gefüllte Polygone + Striche zeichnet und Pan/Zoom
|
||||
per Matrix-Uniform macht. Fenster-Integration in Tauri ist der zweite, separate Schritt.
|
||||
|
||||
## Meilensteine
|
||||
- **M0 (Research):** kurzer Bericht `docs/design/wgpu-integration-findings.md` — wie
|
||||
wgpu-Surface + Tauri/webview koexistieren (raw-window-handle, Transparenz, Z-Order,
|
||||
Input-Routing). Quellen verlinken.
|
||||
- **M1 (Rendering-Spike, standalone):** neue Crate `src-tauri/render2d/` (serde-Input
|
||||
= geflachte Primitive), wgpu + WGSL:
|
||||
- Earcut-Port (oder `lyon`/`earcutr` Crate evaluieren) → Dreiecke.
|
||||
- Füll-Pipeline (gefüllte Polygone, Ortho-Matrix-Uniform).
|
||||
- Linien-Pipeline (Quad-Expansion, echte Papier-mm-Breite).
|
||||
- `cargo test` für die Tessellierung (Muster: `src-tauri/geometry` hat 4/4 Tests —
|
||||
z. B. konkaves L flächentreu, wie `glPlanCompile.test.ts`).
|
||||
- `cargo build` grün. (Visuelle Fenster-Verifikation braucht Display → an Mensch/
|
||||
Display-Session übergeben; im Bericht vermerken.)
|
||||
- **M2 (Tauri-Integration):** Surface unter die Webview, Pan/Zoom-Input aus der
|
||||
Webview an den Renderer, Parität mit dem WebGL-Pfad messen (144 Hz).
|
||||
- **M3:** Schraffur, Bögen, Auswahl-Highlight; danach 3D (three.js→wgpu).
|
||||
|
||||
## Koordination (WICHTIG)
|
||||
- **Eigener Branch** (z. B. `feature/wgpu-renderer`), NICHT auf `master`/
|
||||
`feature/parametric-walls` committen. Isolierter Worktree.
|
||||
- **Nur** `src-tauri/` + neue Rust-Module + `docs/`. **NICHT** `src/App.tsx`,
|
||||
`src/plan/*`, `types.ts` anfassen (dort laufen parallel Features).
|
||||
- Der Web-WebGL-Renderer bleibt bestehen (Browser-Lauffähigkeit + Referenz).
|
||||
- Ergebnisse/Diffs zurückliefern; Projektleitung committet.
|
||||
|
||||
## Gates
|
||||
`cargo check`/`cargo test`/`cargo build` grün. Trace-Scan sauber (COMMIT-REGEL).
|
||||
`npx tsc -b` + `npm run build` müssen unberührt grün bleiben (keine Web-Änderungen).
|
||||
@@ -0,0 +1,263 @@
|
||||
# Briefing — Nativer wgpu-3D-Renderer (M0 Design + M1 Spike)
|
||||
|
||||
> Schwester-Dokument zu `wgpu-2d-renderer-briefing.md`. Beschreibt den Weg vom
|
||||
> jetzigen three.js-3D-View (WebGL im WebKitGTK-Webview) zu einer nativen
|
||||
> wgpu-Engine (Rust). Dieses Dokument ist der ANFANG: M0 (Bestandsaufnahme +
|
||||
> Port-Plan) und M1 (entkoppelter Standalone-Spike). Noch NICHT die Migration.
|
||||
|
||||
## Commit-/Spuren-Regel
|
||||
Wie im ganzen Repo: keine Fremd-Tool-Hinweise im Code, in Kommentaren oder der Historie. Kommentare deutsch, Identifier englisch. Kein Commit ohne Absprache.
|
||||
|
||||
## Warum
|
||||
Der 3D-View laeuft heute als three.js/WebGL im Tauri-Webview (WebKitGTK). Wie beim
|
||||
2D-Plan bremst dieser Compositor unter Linux (siehe `wgpu-2d-renderer-briefing.md`
|
||||
und Memo `webkitgtk-bottleneck`). Die schwere 3D-Grafik soll daher **nativ mit wgpu**
|
||||
gerendert werden (Rust), die Webview macht nur noch UI-Chrome. Das umgeht den
|
||||
Webview-Compositor komplett und teilt sich die Toolchain mit dem 2D-Renderer
|
||||
(`src-tauri/render2d/`, gleiche Feature-Stufung, gleiche Test-Muster).
|
||||
|
||||
---
|
||||
|
||||
## 1. Bestandsaufnahme — was der three.js-View rendert
|
||||
|
||||
Referenz: `src/viewport/Viewport3D.tsx` (analysiert), plus die Geometrie-Grundlage
|
||||
in `src/model/geometry.ts`, `src/model/wall.ts`, `src/model/joins.ts`,
|
||||
`src/geometry/opening.ts`, `src/geometry/stair.ts`. Zeilenangaben beziehen sich auf
|
||||
den Stand der Analyse.
|
||||
|
||||
### Koordinaten-Konvention (verbindlich)
|
||||
Das Modell ist 2D in Metern (`x`, `y`) plus Hoehe `z`. Die 3D-Welt ist **Y-up**:
|
||||
|
||||
```
|
||||
world.x = model.x
|
||||
world.y = Hoehe (z)
|
||||
world.z = model.y
|
||||
```
|
||||
|
||||
D.h. **der Grundriss liegt in der XZ-Ebene, die Extrusion laeuft entlang +Y.**
|
||||
Belegt u.a. in `Viewport3D.tsx`:
|
||||
- Kommentar (~Z. 473): „Modell (x,y,z) → Three (x, z, y) (Z = Hoehe nach oben)".
|
||||
- Kontext-Mesh (~Z. 1679–1684): `verts[i]=pos.x; verts[i+1]=pos.z (Hoehe); verts[i+2]=pos.y`.
|
||||
- Wand-Griffe (~Z. 764): `new THREE.Vector3(wall.start.x, zBottom, wall.start.y)`.
|
||||
- Workplane-Raycast (~Z. 627): `{ x: hit.x, y: hit.z }` (Three → Modell).
|
||||
|
||||
Diese Konvention ist in `render3d` 1:1 uebernommen (`types.rs`, `mesh.rs`).
|
||||
|
||||
### Waende (der Kern)
|
||||
- Funktion `addWallMeshes()` / `addLayerPrism()` (~Z. 1939–2039, 2237–2271).
|
||||
- **Mesh-Weg:** `THREE.ExtrudeGeometry` (~Z. 2248) ueber die 2D-Bandform, die
|
||||
`clippedBand(p1, p2, offA, offB, startCut, endCut)` liefert (~Z. 2237; Funktion in
|
||||
`src/model/geometry.ts:87`). Extrudiert wird um `depth = zTop - zBottom`.
|
||||
- Die Bandform kommt aus Achse + Dicke: `wallBand`/`wallCorners`
|
||||
(`geometry.ts:53`/`:70`) versetzen die Achse um `thickness/2` entlang der
|
||||
**Links-Normale** `leftNormal(u) = (-u.y, u.x)` (`geometry.ts:17`), CCW-Umlauf.
|
||||
- `ExtrudeGeometry` liegt in der XY-Ebene und waechst entlang +Z; three.js dreht das
|
||||
Prisma daher um +90 Grad um X und setzt es auf `topY` (~Z. 2266–2271). In wgpu
|
||||
extrudieren wir direkt in world (XZ-Grundriss, +Y-Hoehe) und sparen die Drehung.
|
||||
- **Hoehe/Basis:** `wallVerticalExtent(project, wall)` (`wall.ts:56`) liefert
|
||||
absolute `zBottom`/`zTop` (aus `wall.bottom`/`wall.top`-Ankern bzw. Geschoss-
|
||||
`baseElevation + wall.height`).
|
||||
- **Mehrschichtig:** je `wt.layers`-Schicht ein eigenes Prisma mit Dicken-Offset
|
||||
(~Z. 1991–2015). M1 extrudiert vereinfacht EINE Schicht (Gesamtdicke).
|
||||
- **Ecken/Gehrung:** `computeJoins()` (`joins.ts:45`) berechnet Schnittlinien
|
||||
(`startCut`/`endCut`), die `clippedBand` an L-Ecken auf Gehrung zieht
|
||||
(`miterLine`, `joins.ts:101`). M1 laesst das noch weg (stumpfe Enden).
|
||||
|
||||
### Oeffnungen (Fenster/Tueren)
|
||||
- `addOpeningMeshes()` (~Z. 2056–2169) + Segmentierung in `addWallMeshes` (~Z.
|
||||
1968–2035). **Kein CSG/Boolean:** die Wand wird entlang der Achse in Segmente
|
||||
zerlegt (`openingInterval`, `geometry/opening.ts:31`), und je Oeffnung entstehen
|
||||
bis zu drei Prismen: Wand DAVOR, **Bruestung** unter dem Fenster (`sillRel`),
|
||||
**Sturz** ueber der Oeffnung (`headRel`). Rahmen/Fluegel als `BoxGeometry`;
|
||||
Glas semitransparent, Tuerfluegel um `swingAngle` gedreht.
|
||||
|
||||
### Treppen
|
||||
- `addStairMeshes()` (~Z. 2357–2409). `stairGeometry()` (`geometry/stair.ts`)
|
||||
liefert Trittflaechen (Footprint + Steig-Hoehe) + optionalen Podest-Umriss; jede
|
||||
Stufe als extrudierter Block (`ExtrudeGeometry`), Hoehe = `stairVerticalExtent`.
|
||||
|
||||
### Decken/Platten
|
||||
- `addCeilingMesh()` (~Z. 2290–2347). `ceiling.outline` als `ExtrudeGeometry`, Tiefe
|
||||
= Deckenstaerke, waechst nach unten von `zTop` (`ceilingVerticalExtent`, `wall.ts:80`).
|
||||
|
||||
### Raeume
|
||||
- Nicht als eigenstaendige 3D-Koerper gerendert (2D-Grundriss-Repraesentation).
|
||||
|
||||
### Kontext/Gelaende
|
||||
- `buildContext()` (~Z. 1651–1718). Terrain/importierte Meshes als rohe
|
||||
`BufferGeometry` (Positions/Indices, Koordinaten-Swap wie oben); Hoehenlinien als
|
||||
`LineSegments`. Dazu ein `GridHelper` (~Z. 421) auf OKFF-Hoehe.
|
||||
|
||||
### Materialien
|
||||
- `MeshLambertMaterial` (Waende/Oeffnungen/Treppen, per Komponente eingefaerbt,
|
||||
~Z. 2176–2206), `MeshStandardMaterial` (Weiss-/Textur-Modus + Terrain, PBR:
|
||||
`roughness`/`metalness`/`aoMap`, ~Z. 445–491, 2001–2015), `MeshBasicMaterial`
|
||||
(Hidden-Line-Flaechen + immer-oben-Marker), `LineBasicMaterial` (Kanten/2D-
|
||||
Zeichnungen). Render-Modi: shaded / white / textured / wireframe / hidden-line.
|
||||
|
||||
### Beleuchtung
|
||||
- `AmbientLight(0xffffff, 0.6)` (~Z. 416) + `DirectionalLight(0xffffff, 1.1)` bei
|
||||
`(6, 12, 4)` (~Z. 417). **Keine Schatten** konfiguriert. Keine Hemisphere/Point-
|
||||
Lights.
|
||||
|
||||
### Kamera + Presets
|
||||
- Zwei Kameras: `PerspectiveCamera(fov, 1, 0.1, 1000)` (~Z. 367) und
|
||||
`OrthographicCamera(-1,1,1,-1, 0.1, 5000)` (~Z. 374). `applyView3d()` (~Z.
|
||||
1566–1633) setzt fuenf Presets:
|
||||
- **front** — Richtung `(0,0,1)`, orthografisch.
|
||||
- **side** — Richtung `(1,0,0)`, orthografisch.
|
||||
- **top** — Richtung `(0,1,~0)`, orthografisch (Rotation gesperrt).
|
||||
- **iso** — Richtung `(1,1,1)` normiert, orthografisch.
|
||||
- **perspective** — Richtung `(0.62,0.5,0.7)` normiert, perspektivisch.
|
||||
- Umschalten perspektiv/ortho ueber `active = perspective ? camera : orthoCamera`
|
||||
(~Z. 1602); Ortho-Frustum aus den Modell-Bounds (`updateOrthoFrustum`, ~Z. 1522).
|
||||
- **OrbitControls** (~Z. 391–413): Mitteltaste orbit, Shift+Mitte pan, Rad zoom
|
||||
(linke/rechte Taste fuer Auswahl/Kontextmenue umgewidmet).
|
||||
|
||||
### Griffe / Gizmos
|
||||
- Editier-Griffe (`SphereGeometry`, ~Z. 711–814): Endpunkt (orange), Hoehe (blau),
|
||||
Verschieben (gruen); `depthTest:false` (immer sichtbar). Drag ueber Workplane-
|
||||
Raycast. Fuer den nativen Renderer spaeter relevant (eigener Overlay-Pass).
|
||||
|
||||
### Schnittebene
|
||||
- **Nicht implementiert:** keine `renderer.clippingPlanes` / `localClippingEnabled`.
|
||||
Schnitte laufen aktuell 2D. Fuer wgpu ein eigenständiger spaeterer Milestone
|
||||
(Clip-Distances im Shader oder Stencil-Capping).
|
||||
|
||||
### Tiefe / Culling
|
||||
- Tiefentest three.js-Standard aktiv. Backface-Culling per Default (Ausnahme:
|
||||
Terrain/Import `DoubleSide`). Diverse Overlays mit `depthTest:false`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Port-Plan nach wgpu
|
||||
|
||||
### Datenfluss
|
||||
Web-Modell → **geflachte Eingabe** (`WallInput`, spaeter Oeffnungen/Treppen/Decken)
|
||||
→ `render3d`-Mesh-Erzeugung → GPU-Buffers → Draw. Analog zum 2D-Pfad (`Scene` →
|
||||
Tessellierung → Buffers). Die Eingabe ist bewusst serde-only und GPU-frei, damit die
|
||||
Mesh-Logik headless testbar bleibt.
|
||||
|
||||
### Mesh-Erzeugung (Waende extrudieren)
|
||||
- Band aus Achse + Dicke ueber die Links-Normale (`(-u.y, u.x) * thickness/2`,
|
||||
CCW), exakt wie `wallCorners`. Extrusion in world: XZ-Grundriss, +Y von
|
||||
`base_elevation` bis `+height`.
|
||||
- Ein Quader = 6 Seiten, je eigene Vertices mit Flaechen-Normale (flaches Shading,
|
||||
korrektes Backface-Culling). 24 Vertices / 36 Indizes je Wand.
|
||||
- Spaeter: mehrschichtige Waende (je Schicht ein Prisma), Gehrung
|
||||
(`computeJoins`/`clippedBand`-Port), Oeffnungs-Segmentierung (Bruestung/Sturz).
|
||||
|
||||
### Kamera (View/Projektion, Presets)
|
||||
- `look_at` (right-handed, Kamera blickt entlang -Z im View-Raum), `perspective`
|
||||
und `orthographic` — beide auf **Clip-Z in [0,1]** (wgpu-Konvention, NICHT [-1,1]).
|
||||
- Fuenf Presets (`preset_camera`): front/top/side orthografisch achsparallel,
|
||||
iso/persp perspektivisch. `top` mit up=-Z, damit Modell-Y im Bild nach unten
|
||||
zeigt (wie die 2D-Sicht).
|
||||
- Orbit-Kamera aus Yaw/Pitch/Distanz (`orbit_eye`, Pitch geklemmt gegen Pol-Flip).
|
||||
|
||||
### Beleuchtung
|
||||
- Zunaechst EIN Directional-Light (Richtung ZUM Licht) + ambienter Sockel im
|
||||
Fragment-Shader (WGSL) — das GPU-Aequivalent zu `AmbientLight(0.6)` +
|
||||
`DirectionalLight(1.1)@(6,12,4)`. Diffuses Lambert. PBR (Rauheit/Metallik/
|
||||
Texturen/AO) spaeter.
|
||||
|
||||
### Tiefenpuffer + Culling
|
||||
- `Depth32Float`-Attachment, `depth_compare = Less`, `depth_write = true`.
|
||||
- `front_face = Ccw`, `cull_mode = Back` (die Extrusion liefert konsistent nach
|
||||
aussen zeigende CCW-Flaechen).
|
||||
|
||||
### Matrix-Mathematik
|
||||
- Handgerechnet (kein `glam`) in der serde-only Schicht — begruendet in `math.rs`:
|
||||
die Standard-Schicht soll wie in render2d ohne Zusatz-Crates headless test-/baubar
|
||||
bleiben; der Umfang (perspective/ortho/look_at + Orbit) ist klein und exakt
|
||||
testbar. Ein spaeterer Wechsel zu `glam` (nur in der GPU-Schicht) bleibt moeglich,
|
||||
ohne die Kamera-Tests anzufassen. Alles spalten-major, direkt als Uniform ladbar.
|
||||
|
||||
### Milestones M2..Mn
|
||||
- **M2 — Mehrschichtige Waende + Gehrung:** Port von `computeJoins`/`miterLine` +
|
||||
`clippedBand` → gehrte Bandformen je Schicht; Farben/Materialien je Komponente.
|
||||
- **M3 — Oeffnungen:** Achsen-Segmentierung (Bruestung/Sturz) + Rahmen/Glas/Fluegel
|
||||
als eigene Meshes; Tuerschwenk-Winkel.
|
||||
- **M4 — Treppen + Decken:** Port von `stairGeometry`/`ceilingVerticalExtent`.
|
||||
- **M5 — Kontext/Gelaende:** rohe Terrain-/Import-Meshes + Hoehenlinien + Grid.
|
||||
- **M6 — Materialien (PBR):** `MeshStandardMaterial`-Aequivalent (roughness/
|
||||
metalness/albedo/AO-Textur); Render-Modi shaded/white/textured/wireframe/hidden.
|
||||
- **M7 — Schnittebene:** Clip-Distances im Shader oder Stencil-Capping (Feature, das
|
||||
der three.js-View gar nicht hat — echter Mehrwert).
|
||||
- **M8 — Griffe/Gizmos + Picking:** Overlay-Pass (immer-oben) + GPU-/Ray-Picking.
|
||||
- **M9 — Tauri-Integration:** Surface unter der Webview (raw-window-handle,
|
||||
Z-Order, Input-Routing) — siehe `wgpu-2d-renderer-briefing.md` M2 (gleiches
|
||||
Integrations-Problem; einmal loesen, fuer 2D+3D nutzen). Kamera-Presets/Orbit-
|
||||
Input aus der Webview an den Renderer.
|
||||
|
||||
---
|
||||
|
||||
## 3. Was in `src-tauri/render3d/` steht (M1)
|
||||
|
||||
Neue, eigenstaendige Crate (eigener leerer `[workspace]`-Block, wie render2d), damit
|
||||
`cargo test`/`build` unabhaengig vom Tauri-Workspace laufen. Feature-Stufung 1:1 wie
|
||||
render2d:
|
||||
|
||||
- `Cargo.toml` — Features `default` (serde-only) / `render` (wgpu) / `window`
|
||||
(winit-Spike). `[[bin]] spike3d` mit `required-features = ["window"]`.
|
||||
- `src/types.rs` — serde-only Eingabe: `WallInput { start, end, thickness, height,
|
||||
base_elevation, color }`, `Camera` (+ `Projection`), `CameraPreset`; Ausgabe
|
||||
`Mesh` (interleaved `[pos.xyz, normal.xyz, color.rgb]` + Indizes) mit
|
||||
`vertex_count`/`triangle_count`/`bounds`. Koordinaten-Konvention dokumentiert.
|
||||
- `src/mesh.rs` — Wand-Extrusion: `extrude_wall`/`build_walls_mesh`. Band ueber
|
||||
Links-Normale, Quader mit sechs eigenen Seiten, nach aussen zeigende Normalen.
|
||||
- `src/math.rs` — `Mat4` (spalten-major), `perspective`/`orthographic` (Clip-Z
|
||||
[0,1]), `look_at`, `view_projection`, `orbit_eye`, `preset_camera` (fuenf Presets).
|
||||
- `src/shaders.rs` — WGSL (`MESH_WGSL`): View-Projektion-Uniform + Directional-
|
||||
Light + ambienter Sockel im Fragment-Shader.
|
||||
- `src/gpu.rs` (Feature `render`) — `Renderer`: eine Pipeline mit Tiefenpuffer,
|
||||
View-Projektions-Uniform, Backface-Culling. `upload_walls` → GPU-Buffers,
|
||||
`render(camera, viewport)`.
|
||||
- `src/bin/spike3d.rs` (Feature `window`) — winit-Fenster mit Demo-Raum (5
|
||||
extrudierte Waende) + **Orbit-Kamera** (linke Maustaste dreht Yaw/Pitch, Rad
|
||||
zoomt Abstand). Matrix-getrieben, kein Re-Meshing beim Kamera-Wechsel.
|
||||
- `src/lib.rs` — Modul-Deklarationen, Re-Exports, Tests.
|
||||
|
||||
### Tests (`cargo test`, default-Feature)
|
||||
Muster wie render2d/`glPlanCompile.test.ts`:
|
||||
- Quader-Zaehlung (eine Wand → 24 Vertices / 36 Indizes / 12 Dreiecke).
|
||||
- Mehrere Waende addieren sich.
|
||||
- Bounding-Box deckt Laenge/Dicke/Hoehe ab; `base_elevation` verschiebt in Y.
|
||||
- Deckel-Normale = +Y; **alle Mantel-Normalen zeigen nach aussen** (Dot mit
|
||||
„Vertex − Zentrum" ≥ 0 → Backface-Culling korrekt).
|
||||
- Diagonale Wand; degenerierte Wand (Start==Ende) erzeugt nichts (kein Absturz).
|
||||
- Kamera: `look_at` setzt Ziel auf view-z=-dist; Perspektive klemmt z in [0,1];
|
||||
`orbit_eye` haelt den Abstand; Presets setzen die richtige Projektionsart.
|
||||
- Mit `--features render`: WGSL headless via `naga` (Parser + Validator) validiert.
|
||||
|
||||
---
|
||||
|
||||
## 4. Build-/Test-Ergebnis
|
||||
|
||||
Alle Gates gruen (Toolchain: cargo 1.96, wgpu 22, winit 0.30):
|
||||
- `cargo test` (default) — **12/12** gruen (Mesh + Kamera).
|
||||
- `cargo test --features render` — **13/13** gruen (inkl. WGSL-naga-Validierung).
|
||||
- `cargo build` (default), `--features render`, `--features window` — je gruen,
|
||||
**keine Warnungen**.
|
||||
- Trace-Scan sauber (keine KI-Spuren).
|
||||
- Web-Gates unberuehrt (nur `src-tauri/` + `docs/` angefasst; `src/` nur gelesen).
|
||||
|
||||
**Visuelle Fenster-Verifikation** ist headless NICHT moeglich. Auf einer aktiven
|
||||
Display-Session pruefbar mit:
|
||||
|
||||
```
|
||||
cargo run --features window --bin spike3d
|
||||
```
|
||||
|
||||
Erwartet: ein Raum aus extrudierten Waenden mit diffuser Beleuchtung; linke
|
||||
Maustaste dreht die Orbit-Kamera, das Rad zoomt.
|
||||
|
||||
---
|
||||
|
||||
## 5. Naechste Schritte
|
||||
1. **M2** starten: `computeJoins`/`clippedBand`-Port für gehrte, mehrschichtige
|
||||
Waende (die Bandmath ist im Web bereits verifiziert — gleiche Tests portieren).
|
||||
2. Oeffnungs-Segmentierung (M3) auf demselben Extrusions-Kern.
|
||||
3. Die **Tauri-Integration (M9)** gemeinsam mit dem 2D-Renderer loesen (ein Surface-
|
||||
Unterbau, ein Input-Routing) — das ist der eigentliche Engpass, nicht das
|
||||
Rendering. Erst standalone spiken (dieser Stand), dann unter die Webview.
|
||||
@@ -0,0 +1,449 @@
|
||||
# Fenster-/Tür-Editor — Studie & Designdokument (Referenz: Vectorworks „Fenster bearbeiten")
|
||||
|
||||
Status: Studie/Entwurf (KEIN Code). Ziel: den heute als „mega mager" empfundenen
|
||||
Fenster-/Tür-Editor zu einem eigenständigen, reichen **Einstellungs-Dialog** mit
|
||||
Kategorie-Sidebar, Live-Vorschau (2D + 3D) und **„Als Stil speichern"** ausbauen.
|
||||
Diese Datei ordnet die Vectorworks-Referenz dem bestehenden DOSSIER-Modell zu und
|
||||
schlägt einen realistischen, phasierten Plan vor.
|
||||
|
||||
Konvention: Bezeichner englisch, UI-Text/Kommentare deutsch. Meter als Grundmaß.
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
**Was ein guter DOSSIER-Fenster-/Tür-Editor sein sollte.** Ein eigener modaler
|
||||
Dialog „Fenster-/Tür-Einstellungen" — nicht die heutige, in den Ressourcen-Manager
|
||||
eingebettete Formularspalte (`WindowStylesTab`/`DoorStylesTab` in
|
||||
`src/ui/ResourceManager.tsx`), die pro Feld nur eine `FieldRow` zeigt und keinerlei
|
||||
Vorschau bietet. Der neue Dialog hat drei Zonen (wie Vectorworks):
|
||||
|
||||
1. **Kategorie-Sidebar** links (Basis, Größe/Position, Rahmen, Flügel/Sprossen,
|
||||
Oberlicht/Unterlicht, Laibung/Bank, Sonnenschutz/Rollladen, Attribute/Darstellung,
|
||||
Detaillierung).
|
||||
2. **Parameter-Panel** in der Mitte (die Controls der gewählten Kategorie).
|
||||
3. **Live-Vorschau** rechts: eine 2D-Plan-Vorschau (aus `generatePlan()`) und eine
|
||||
3D-Ansicht/Elevation (aus `projectToModel3d()`), beide sofort aktualisiert.
|
||||
|
||||
**Die eine wichtigste strukturelle Änderung.** Der Editier-Primärort wandert vom
|
||||
Ressourcen-Tab in einen **dedizierten `OpeningEditorDialog`**, der TYP-Parameter
|
||||
(wiederverwendbarer Stil = `WindowType`/`DoorType`) und INSTANZ-Parameter (dieses
|
||||
`Opening`) im selben Fenster editiert und oben eine Stil-Leiste trägt:
|
||||
`Stil: [Dropdown]` · `Fenster speichern…` (aktuelle Konfiguration als neuen
|
||||
benannten `WindowType`/`DoorType` ablegen) · `Einstellungen zurücksetzen…`
|
||||
(auf den Stil zurückfallen). Das ⚙ in `ObjectInfoPanel.OpeningSection`
|
||||
(`src/panels/ObjectInfoPanel.tsx:731`) öffnet künftig DIESEN Dialog statt den
|
||||
Ressourcen-Manager-Tab.
|
||||
|
||||
**Begriffsklärung.** Der Nutzer-Ausdruck „als **Wandstil** speichern" ist ein
|
||||
Versprecher — gemeint ist „als **Fensterstil/Türstil** (Bauteilstil) speichern",
|
||||
also ein neuer Eintrag in `project.windowTypes` bzw. `project.doorTypes`, analog
|
||||
`WallType`/`CeilingType`/`StairType`. Es entsteht KEIN neuer Wandtyp.
|
||||
|
||||
**Warum das der Hebel ist.** Alle geplante Tiefe (mehrflügelig, Sprossenraster,
|
||||
Rollladen, Bank/Nische, Ober-/Unterlicht, Verglasungsanzahl) braucht (a) mehr
|
||||
Felder auf `WindowType`/`DoorType` und (b) eine UI, die sie ohne Formularwust
|
||||
zeigt und deren Wirkung sofort sichtbar macht. Ohne Live-Vorschau bleibt ein
|
||||
reicher Parametersatz unbenutzbar. Erst der Dialog macht die Tiefe zugänglich; die
|
||||
Modellfelder allein (Abschnitt 4) reichen nicht.
|
||||
|
||||
---
|
||||
|
||||
## 2. Vollständige Mapping-Tabelle (VW-Referenz → DOSSIER)
|
||||
|
||||
Legende — Status: **✓** vorhanden · **~** teilweise (Feld existiert, Renderer liest
|
||||
es nicht ODER nur grob) · **✗** fehlt. Ebene: **T** = Typ (Stil, wiederverwendbar,
|
||||
auf `WindowType`/`DoorType`) · **I** = Instanz (auf `Opening`). Priorität P0
|
||||
(erste reiche Scheibe) … P3 (VW-Ballast). Aufwand grob in Personentagen (PT).
|
||||
|
||||
Wichtiger Ist-Befund aus dem Code (Renderer-Konsum-Lücken):
|
||||
|
||||
- `WindowType.glazing` (einfach/zweifach/dreifach) ist editierbar, wird aber von
|
||||
KEINEM Renderer gelesen. 3D zeichnet stets EINE Scheibe (`glassPanesForOpening`
|
||||
in `src/plan/toWalls3d.ts:1965` ignoriert `glazing`); 2D leitet die Glaslinien-
|
||||
Anzahl allein aus `DetailLevel` ab (`addOpeningSymbol` in
|
||||
`src/plan/generatePlan.ts:2214`, `glassCount = detail==="fein" ? 2 : 1`).
|
||||
- `WindowType.kind`/`DoorType.kind` (dreh/kipp/drehkipp/fest/schiebe …) schlagen
|
||||
sich NICHT in 2D/3D-Geometrie nieder.
|
||||
- `DoorType.leafCount` (1/2), `leafStyle`/`glazingRatio` (außer `leafStyle==="glas"`
|
||||
→ 3D-Verglasung an) sind ungenutzt.
|
||||
- `WindowType.sillBoard` ungenutzt; `DoorType.threshold` → `hasSill` genutzt.
|
||||
- 3D-Mittelpfosten/Kämpfer und Sprossen werden nur bei `detail==="fein"` emittiert
|
||||
(`frameMeshesForOpening` in `src/plan/toWalls3d.ts:1902`, ab :1938).
|
||||
|
||||
### 2.1 Basiseinstellungen
|
||||
|
||||
| VW-Control | Status | DOSSIER-Feld (Vorschlag) | 2D-Wirkung | 3D-Wirkung | Ebene | Prio | Aufwand |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Fensterart („Für normale Wand") | ✗ | `WindowType.wallKind?: "normal"\|"eck"` (später) | — | — | T | P3 | 0.5 |
|
||||
| Öffnungsart (Dreh/Kipp/…) | ~ | `WindowType.kind` existiert, kein Render | Öffnungssymbol/Öffnungslinien | Öffnungslinien 3D | T | P1 | 2 |
|
||||
| Einfügepunkt längs (Mitte/…) | ✓ | `Opening.position` + Bezugspunkt in `ObjectInfoPanel` | Position im Plan | Position | I | — | — |
|
||||
| Einfügepunkt quer (Fensterseite außen/…) | ~ | `insetFromFace`/`insetFace` (T) | Band-Lage | Rahmen-Normalenlage (`resolveFrameNormalRange` :1803) | T | P1 | 0.5 |
|
||||
| Versatz im Fassadenmodul | ✗ | — (Fassadensystem fehlt in DOSSIER) | — | — | — | P3 | — |
|
||||
| Laibung (wählen) | ~ | `insetFromFace` deckt Teil ab | Laibungsstriche (fein) | Laibungstiefe | T | P2 | 1 |
|
||||
| Klasse | ~ | `Opening.categoryCode` (LayerCategory) | Farbe/Strich | — | I | — | — |
|
||||
| Darstellung höchste Detaillierung | ✓ | `Opening.detailLevel` + Ansichts-DetailLevel | Symbolstufe | Meshstufe | I | — | — |
|
||||
| Eigenes Symbol verwenden | ✗ | `WindowType.symbolId?` (Drawing2D-Ref) | Symbol-Override | — | T | P3 | 3 |
|
||||
|
||||
### 2.2 Fenstergröße & Bemaßung
|
||||
|
||||
| VW-Control | Status | DOSSIER-Feld | 2D | 3D | Ebene | Prio | Aufwand |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Fensterbreite (Wert) | ✓ | `Opening.width` (+ `defaultWidth` T) | ✓ | ✓ | I/T | — | — |
|
||||
| Fensterhöhe (Wert) | ✓ | `Opening.height` (+ `defaultHeight` T) | ✓ | ✓ | I/T | — | — |
|
||||
| Bezug B1..B5 / H1..H7 (Roh-/Fertigmaß) | ✗ | `WindowType.dimRef?: {...}` | Bemaßungsschema | — | T | P3 | 3+ |
|
||||
| „Bemaßung automatisch" (außen/innen) | ✗ | — (DOSSIER hat noch keine parametrische Öffnungs-Bemaßung) | Maßketten | — | T | P3 | 5+ |
|
||||
|
||||
Der ganze VW-Maßketten-/Bezugsapparat (B1..B5, H1..H7, Roh-/Fertigmaß-Umschaltung)
|
||||
ist **P3/„nicht bauen"** — siehe Abschnitt 5. DOSSIER trägt lichte Maße
|
||||
(`width`/`height`), das genügt.
|
||||
|
||||
### 2.3 Höhe Brüstung/Sturz
|
||||
|
||||
| VW-Control | Status | DOSSIER-Feld | 2D | 3D | Ebene | Prio | Aufwand |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Position definieren durch (Brüstungshöhe/…) | ✓ | `Opening.sillHeight` (+ `defaultSillHeight` T) | — | vertikale Lage (`openingVerticalExtent`) | I | — | — |
|
||||
| Abstand Brüstungshöhe (Wert) | ✓ | `Opening.sillHeight` | — | ✓ | I | — | — |
|
||||
| „bezieht sich auf" (Wandaußenseite/…) | ✗ | — (immer Wand-UK-relativ) | — | — | — | P3 | — |
|
||||
| „auf Ebenenbasishöhe" | ~ | Wand-UK ergibt sich aus Geschoss (`baseElevation`) | — | ✓ | — | — | — |
|
||||
|
||||
### 2.4 Rahmenwerte
|
||||
|
||||
| VW-Control | Status | DOSSIER-Feld | 2D | 3D | Ebene | Prio | Aufwand |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Rahmenbreiten (4 Kanten, asymmetrisch) | ~ | heute nur `frameWidth` (eine Zahl) → `WindowType.frameWidths?: {left,right,top,bottom}` | Rahmenkontur (`windowSymbol`/`addOpeningFrameBand`) | Rahmen-Boxen (`frameMeshesForOpening`) | T | P2 | 2 |
|
||||
| Symmetrisch-Toggle | ✗ | `WindowType.frameSymmetric?: boolean` | — | — | T | P2 | 0.5 |
|
||||
| Flügel mittig / Versatz | ✗ | `WindowType.sashOffset?: number` | Aufschlag-Linie | Flügel-Box-Lage | T | P2 | 1 |
|
||||
| Rahmenstärke quer zur Wand | ✓ | `frameThickness` / `frameDepth` (→ `resolveAcrossWallDepth` :1780) | — | Rahmentiefe | T | — | — |
|
||||
| Schnitt-/Außenansicht-Rahmenbreiten | ~ | von `frameWidth` mitgezeichnet | Bandbreite | — | T | P2 | 1 |
|
||||
|
||||
### 2.5 Flügeleinteilung (KERN-Tiefe — höchster Wert)
|
||||
|
||||
| VW-Control | Status | DOSSIER-Feld | 2D | 3D | Ebene | Prio | Aufwand |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Flügeltabelle (Nr/Typ/Breite/Anschlag/…) | ~ | `wingCount` (Zahl) → **neu** `WindowType.sashes: SashDef[]` (siehe §4) | Pfostenlinien (`mullionLines`) heute nur gleichmäßig | Pfosten-Boxen nur gleichmäßig | T | **P0** | 4 |
|
||||
| Typ Flügel \| Pfosten | ✗ | `SashDef.kind: "fluegel"\|"pfosten"` | Pfostenlage frei | Pfosten frei | T | P0 | (inkl.) |
|
||||
| Aut. Breite / Flügelbreite | ~ | `SashDef.autoWidth`/`width` (heute nur gleichmäßig) | Teilungslage | Teilungslage | T | P1 | 1 |
|
||||
| Pfostenbreite / „alle gleich" | ~ | `SashDef.postWidth`, `WindowType.uniformPosts` | Pfostendicke | Pfostenbox-Breite | T | P1 | 1 |
|
||||
| Anschlag (Drehbar links/Drehkipp rechts) | ✗ | `SashDef.opening: OpeningKind`, `SashDef.hingeSide: "left"\|"right"` | Öffnungslinien/Pfeil je Flügel | Öffnungslinien 3D | T | **P0** | 2 |
|
||||
| Aufschlag (Abstand zu Rahmen) | ✗ | `SashDef.rebate?: number` | Aufschlag-Linie | — | T | P2 | 0.5 |
|
||||
| Winkel / 3D-Öffnung | ✗ | `SashDef.openAngle?: number` | — | Flügel gekippt/offen | T | P2 | 1.5 |
|
||||
| Griffart | ✗ | `SashDef.handle?: HandleKind` | — | Griff-Mesh (§2.7) | T | P2 | 1 |
|
||||
| Kämpfer-Zeilen (horizontale Teilung) | ~ | `mullionRows` existiert | Querlinien | Kämpfer-Boxen (fein) | T | P1 | — |
|
||||
|
||||
Dies ist die **wertvollste Lücke**: VW modelliert je Flügel Typ, Breite,
|
||||
Öffnungsrichtung, Anschlag, Griff. DOSSIER hat nur eine gleichmäßige `wingCount`.
|
||||
Die `SashDef[]`-Tabelle (§4, Phase P0/P1) ist der zentrale Ausbau.
|
||||
|
||||
### 2.6 Laibungsverkleidung · Form · Ober-/Unterlicht · Nische/Bank
|
||||
|
||||
| VW-Control | Status | DOSSIER-Feld | 2D | 3D | Ebene | Prio | Aufwand |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Laibungsverkleidung erstellen | ✗ | `WindowType.reveal?: {create, depth, thickness}` | Laibungsband | Laibungs-Boxen | T | P2 | 1.5 |
|
||||
| Form (Eckig/Schräg/Spitz/Rund) | ✗ | `WindowType.headShape?: HeadShape` + Eckmaße | Öffnungsumriss-Form | Bogen-/Schräg-Mesh | T | P2 | 4 |
|
||||
| Oberlicht erstellen + Höhe/Rahmen/Sprossen | ~ | `transomHeight` existiert (feste Scheibe) → `WindowType.transom?: {height, frame, grid}` | Kämpferlinie + Feld | zweite Scheibe (`glassPanesForOpening` :1988) | T | P1 | 2 |
|
||||
| Unterlicht (unteres festes Feld) | ✗ | `WindowType.underlight?: {height, frame, grid}` | Feld | Scheibe unten | T | P2 | 1.5 |
|
||||
| Sprossen (horiz./vert. im Feld) | ~ | `mullionRows` grob → `WindowType.muntins?: {rows, cols}` je Feld | Sprossengitter | Sprossen-Boxen (fein) | T | P1 | 2 |
|
||||
| Nische aussparen (außen/innen) | ✗ | `WindowType.niche?: {aussen, innen, ...}` | Nischenkontur | Nischen-Aussparung | T | P2 | 2 |
|
||||
| Fensterbank erstellen (außen/innen) | ~ | `sillBoard` existiert, ungenutzt → `WindowType.sill?: {aussen, innen, typ, masse}` | Bankkontur | Bank-Box | T | P1 | 2 |
|
||||
| Banktyp/Winkel/ΔZ/Endverkröpfung | ✗ | Unterfelder von `sill` | Detail | Detail-Mesh | T | P3 | 2 |
|
||||
|
||||
### 2.7 Sonnenschutz · Beschlag · Geländer · Heizkörper · Eckfenster
|
||||
|
||||
| VW-Control | Status | DOSSIER-Feld | 2D | 3D | Ebene | Prio | Aufwand |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Sonnenschutz/Rollladen erstellen + Typ | ✗ | `WindowType.shading?: {create, kind: "raffstore"\|"rollladen"\|…, box, projection}` | Kasten-/Kastenlinien im Grundriss | Kastenbox + Auskragung | T | **P0/P1** | 2.5 |
|
||||
| Rollladenkasten-Maße | ✗ | `shading.box: {w,h,d}` | Rechteck | Box-Mesh | T | P0 | (inkl.) |
|
||||
| „Nische erzeugen"/„Kasten symm."/Lamellenwinkel | ✗ | `shading`-Unterfelder | — | Detail | T | P2 | 1 |
|
||||
| Beschlag (Griff/Knauf) Geometrie/Maße | ✗ | `SashDef.handle` + `WindowType.handleGeom?` | — | Griff-/Knauf-Mesh | T | P2 | 2 |
|
||||
| Geländer (Position/Höhe/Bauteile) | ✗ | `WindowType.railing?: {...}` | Geländerlinien | Geländer-Mesh | T | P3 | 3 |
|
||||
| Heizkörper | ✗ | — (eigenes Bauteil, nicht Fenster) | — | — | — | P3 | — |
|
||||
| Eckfenster | ✗ | eigener Sonderfall (zwei Wände) | — | — | T | P3 | 5+ |
|
||||
|
||||
### 2.8 Attribute · Schnitte/Ansichten · Detaillierung · IFC/Energos
|
||||
|
||||
| VW-Control | Status | DOSSIER-Feld | 2D | 3D | Ebene | Prio | Aufwand |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Attribut-Tabelle je Bestandteil (Klasse/Material/Stift/Linie/…) | ~ | DOSSIER hat By-Layer/By-Object-Attribute (`foreground`/`background`/`hatchId`/`strokeWeight` an Wand/Decke, NICHT an `Opening`) → `Opening.foreground?` etc. + evtl. je Bestandteil | Farbe/Strich der Bestandteile | Material | I/T | P2 | 3 |
|
||||
| „Klassenattribute zuweisen/entfernen" | ~ | `AttributeSource` („layer"/„object") existiert für Wand/Decke | — | — | I | P2 | 1 |
|
||||
| Automatische 2D-Darstellungen (Horizontal/Ansicht/Querschnitt) | ~ | `generatePlan` erzeugt Grundriss; Schnitt/Ansicht sind Platzhalter (`DrawingLevelKind`) | — | — | — | P3 | — |
|
||||
| Sichtbarkeitsmatrix 3D-Objekte × Ansichten (Oben/Unten/…/Querschnitt) | ✗ | **nicht bauen** | — | — | — | P3 | — |
|
||||
| Detaillierung: Option × Kategorie × Detailstufe (Augen-Tabelle) | ~ | `DetailLevel` (grob/mittel/fein) fließt in 2D + 3D | Sichtbarkeit je Stufe | Meshstufe | I | P2 | 2 |
|
||||
| „Für 3D die Tiefe/Mittlere/Hohe Detaillierung verwenden" | ✓ | `Model3dOptions.detail` (`toWalls3d.ts:2145`) | — | Meshstufe | — | — | — |
|
||||
| Beschriftung | ✗ | eigenes Text/Tag-System | Label | — | — | P3 | — |
|
||||
| Infos/IFC-Daten/Infopalette/Energos | ✗ | **nicht bauen** | — | — | — | P3 | — |
|
||||
| „Mehrere ändern" | ~ | Multi-Selektion patcht bereits gemeinsame Felder | — | — | I | P2 | 1 |
|
||||
|
||||
---
|
||||
|
||||
## 3. Der dedizierte Dialog
|
||||
|
||||
### 3.1 Komponente & Einbettung
|
||||
|
||||
Neue Komponente **`src/ui/OpeningEditorDialog.tsx`** (Muster: die bestehenden
|
||||
`*Dialog.tsx` in `src/ui/`, z. B. `SettingsDialog.tsx`, `TextEditorDialog.tsx`,
|
||||
und das Overlay-Muster `res-overlay` + `role="dialog"` aus
|
||||
`ResourceManager.tsx:371`). Props (über `usePanelHost`/App-State geliefert):
|
||||
|
||||
```
|
||||
open: boolean
|
||||
openingId: string // die editierte Instanz
|
||||
kind: "window" | "door" // steuert Sidebar-Sätze + welche Type-Liste
|
||||
project: Project // read (Typ-Listen, Bauteile, LayerCategories)
|
||||
onPatchOpening(patch) // Instanz-Felder
|
||||
onPatchType(typeId, patch) // Typ-Felder (aktueller Stil)
|
||||
onSaveAsStyle(name, snapshot)// „Fenster/Tür speichern…" → neuer WindowType/DoorType
|
||||
onResetToStyle() // „zurücksetzen…"
|
||||
onClose()
|
||||
```
|
||||
|
||||
**Öffnen aus dem ⚙.** `ObjectInfoPanel.OpeningSection`
|
||||
(`src/panels/ObjectInfoPanel.tsx:731`, `onClick → host.onEditOpeningType(...)`)
|
||||
ruft künftig einen neuen Host-Handler `host.onOpenOpeningEditor(openingId)` statt
|
||||
`setResourcesTab(...) + setResourcesOpen(true)` (heutige Verdrahtung in
|
||||
`src/App.tsx:3130`). App hält `openingEditorId: string | null` als State und
|
||||
rendert `<OpeningEditorDialog open={openingEditorId!=null} .../>`. Der bisherige
|
||||
Ressourcen-Tab bleibt als Bibliotheks-/Massenpflege bestehen, ist aber nicht mehr
|
||||
der Primärort — das ⚙ landet direkt im reichen Dialog.
|
||||
|
||||
### 3.2 Kategorie-Sidebar (realistischer Teilmenge)
|
||||
|
||||
Reihenfolge und Sichtbarkeit je `kind`:
|
||||
|
||||
1. **Basis** — Öffnungsart (`kind`), Einfügepunkt quer (`insetFace`/`insetFromFace`),
|
||||
Klasse (`categoryCode`), Detaillierung (`detailLevel`).
|
||||
2. **Größe/Position** — `width`, `height`, `sillHeight`, `position` (I),
|
||||
`defaultWidth/Height/SillHeight` (T).
|
||||
3. **Rahmen** — `frameThickness`, `frameDepth`, `frameWidth`/`frameWidths`,
|
||||
`frameKind` (Tür), `sashOffset`.
|
||||
4. **Flügel/Sprossen** — die `SashDef[]`-Tabelle (Flügel/Pfosten einfügen,
|
||||
Öffnungsrichtung/Anschlag je Flügel), Kämpfer-Zeilen, Sprossengitter.
|
||||
5. **Oberlicht/Unterlicht** — `transom`, `underlight` (je Feld: Höhe, Rahmen, Gitter).
|
||||
6. **Laibung/Bank** — `reveal`, `sill` (außen/innen), `niche`.
|
||||
7. **Sonnenschutz/Rollladen** — `shading` (Typ, Kastenmaße, Auskragung).
|
||||
8. **Attribute/Darstellung** — Bestandteil-Attribute (Farbe/Strich/Material).
|
||||
9. **Detaillierung** — welche Bestandteile bei grob/mittel/fein sichtbar sind.
|
||||
|
||||
Sidebar-Sätze: bei `kind==="door"` entfallen Oberlicht/Unterlicht-Gitter-Details,
|
||||
Sonnenschutz und Bank; dafür Türblatt (`leafCount`, `leafStyle`, `threshold`,
|
||||
`swing`/`hinge`/`openingDir`/`swingAngle`).
|
||||
|
||||
### 3.3 Live-Vorschau
|
||||
|
||||
**2D (der billige, sofort machbare Teil).** DOSSIER hat den kompletten
|
||||
Plan→SVG-Pfad bereits: `generatePlan()` (`src/plan/generatePlan.ts:632`) →
|
||||
`toRenderScene.ts` (RScene) → `sceneToPrintSvg()` (`src/export/sceneToPrintSvg.ts:109`)
|
||||
bzw. `planToPrintSvg()` (`src/export/planToPrintSvg.ts:155`). Für die Vorschau ein
|
||||
**Mini-Projekt** bauen (eine kurze Wand + das editierte `Opening` mit den aktuellen
|
||||
Dialog-Werten), durch `generatePlan()` schicken und als SVG in ein Vorschau-`<div>`
|
||||
rendern — Grundriss oben, optional eine Elevations-Skizze. Reagiert auf jeden
|
||||
Patch reaktiv (React-State → neu generieren; günstig, da nur ein Element).
|
||||
|
||||
**3D/Elevation.** Zwei Optionen, in aufsteigendem Aufwand:
|
||||
|
||||
- **(A) günstig, empfohlen für den ersten Wurf:** eine reine 2D-**Ansicht/Elevation**
|
||||
aus denselben Plan-Primitiven — VW's „Vorschau, schattiert" ist für uns nicht
|
||||
nötig; eine saubere Frontalansicht (Rahmen/Flügel/Sprossen/Glas als SVG) zeigt
|
||||
die Flügeleinteilung am aussagekräftigsten und nutzt exakt die neuen Felder.
|
||||
- **(B) echte 3D-Vorschau:** `projectToModel3d()` (`src/plan/toWalls3d.ts:2149`)
|
||||
auf das Mini-Projekt anwenden und im wgpu-Viewport (`Wasm3DViewport.tsx`)
|
||||
darstellen. Teurer (eigener Render-Kontext im Modal, WASM-Instanz) und laut
|
||||
Projekt-Memo NICHT per Browser/Puppeteer verifizierbar — der Nutzer testet 3D
|
||||
selbst in der Tauri-Dev-App. Deshalb 3D-Vorschau erst NACH der 2D-Vorschau, als
|
||||
eigene Phase.
|
||||
|
||||
Empfehlung: erste Scheibe nur **2D-Grundriss + 2D-Elevation**; echte wgpu-Vorschau
|
||||
später.
|
||||
|
||||
### 3.4 „Als Stil speichern"-Flow
|
||||
|
||||
Drei Aktionen in der Stil-Leiste:
|
||||
|
||||
- **Fenster/Tür speichern… (`onSaveAsStyle`)** — nimmt einen Schnappschuss der
|
||||
aktuellen (Typ-relevanten) Dialogwerte, fragt einen Namen ab (`PromptDialog.tsx`)
|
||||
und legt einen NEUEN `WindowType`/`DoorType` in `project.windowTypes`/`doorTypes`
|
||||
an (CRUD-Factories existieren: `addWindowType`/`addDoorType` in `src/App.tsx`
|
||||
ab :2273/:2304). Das editierte `Opening.typeId` zeigt danach auf den neuen Stil.
|
||||
- **Update dieses Stils** — `patchWindowType`/`patchDoorType` (App :2319/:2287) auf
|
||||
den aktuell referenzierten `typeId` anwenden (wirkt auf alle Instanzen des Stils).
|
||||
- **Einstellungen zurücksetzen… (`onResetToStyle`)** — die instanzseitigen
|
||||
Overrides verwerfen und wieder die Typ-Defaults ziehen (Instanz-Felder auf
|
||||
`undefined` setzen, sodass die Resolver `getWindowType`/`getDoorType`
|
||||
(`src/model/types.ts:2060`/:2064) greifen).
|
||||
|
||||
TYP-vs-INSTANZ-Regel im Dialog sichtbar machen: Typ-Felder tragen eine dezente
|
||||
„Stil"-Markierung; ein instanzseitig übersteuertes Feld zeigt einen „abweichend
|
||||
vom Stil"-Indikator + „zurücksetzen". Das ist genau VW's Stil-/Override-Logik.
|
||||
|
||||
---
|
||||
|
||||
## 4. Modell-Erweiterungsplan (phasiert)
|
||||
|
||||
Alle Felder additiv/optional (Alt-Projekte laden unverändert — die bestehende
|
||||
Konvention der Datei, vgl. `wingCount?`, `sliceTermination` Default). Neue Felder
|
||||
auf `WindowType`/`DoorType` in `src/model/types.ts`.
|
||||
|
||||
### P0 — die erste reiche Scheibe (Kern-Tiefe)
|
||||
|
||||
```ts
|
||||
// Ein Flügel- oder Pfosten-Eintrag der Flügeleinteilung.
|
||||
type OpeningKind = "dreh" | "kipp" | "drehkipp" | "fest" | "schiebe";
|
||||
interface SashDef {
|
||||
kind: "fluegel" | "pfosten";
|
||||
autoWidth?: boolean; // gleichmäßig aufteilen (Default true)
|
||||
width?: number; // feste Flügelbreite (m), wenn !autoWidth
|
||||
postWidth?: number; // Pfostenbreite (m), nur kind="pfosten"
|
||||
opening?: OpeningKind; // Öffnungsart DIESES Flügels
|
||||
hingeSide?: "left" | "right"; // Anschlag
|
||||
}
|
||||
interface WindowType {
|
||||
// … bestehend …
|
||||
sashes?: SashDef[]; // ersetzt/erweitert wingCount; fehlt ⇒ aus wingCount abgeleitet
|
||||
shading?: { // Rollladen/Sonnenschutz-Kasten (P0-Minimalfassung)
|
||||
create: boolean;
|
||||
kind: "rollladen" | "raffstore" | "markise";
|
||||
box: { width: number; height: number; depth: number };
|
||||
};
|
||||
glazingPanes?: 1 | 2 | 3; // Verglasung endlich RENDER-wirksam (heute: glazing unbenutzt)
|
||||
}
|
||||
```
|
||||
|
||||
Rationale/Render-Freischaltung:
|
||||
- `sashes` — schaltet echte, ungleichmäßige Flügel/Pfosten + Öffnungsrichtung je
|
||||
Flügel frei; treibt neue Pfostenlinien in `windowSymbol`/`mullionLines`
|
||||
(`src/geometry/opening.ts`) und Pfosten-Boxen in `frameMeshesForOpening`
|
||||
(`src/plan/toWalls3d.ts:1938`). Höchster sichtbarer Mehrwert.
|
||||
- `shading` — der Rollladenkasten ist ein einzelnes Kästchen: eine 2D-Rechteckkontur
|
||||
über der Öffnung (neuer Zeichenzweig in `addOpeningSymbol`) + eine Box in
|
||||
`emitOpeningFrames`/eigener Emitter. Wenig Code, große „Reichhaltigkeit".
|
||||
- `glazingPanes` — `glassPanesForOpening` (:1965) liest die Scheibenzahl statt
|
||||
konstant EINE Scheibe zu zeichnen; 2D-Glaslinien-Anzahl analog. Schließt die
|
||||
auffälligste Konsum-Lücke.
|
||||
|
||||
### P1 — Tiefe der Flügel/Felder
|
||||
|
||||
```ts
|
||||
interface SashDef { /* + */ rebate?: number; openAngle?: number; handle?: "drueck"|"knauf"|"none"; }
|
||||
interface WindowType {
|
||||
frameWidths?: { left: number; right: number; top: number; bottom: number };
|
||||
frameSymmetric?: boolean;
|
||||
uniformPosts?: boolean;
|
||||
transom?: { height: number; frame?: number; muntins?: { rows: number; cols: number } };
|
||||
sill?: { aussen?: SillDef; innen?: SillDef };
|
||||
muntins?: { rows: number; cols: number }; // Sprossen im Hauptfeld
|
||||
}
|
||||
```
|
||||
|
||||
Rationale: asymmetrische Rahmenbreiten, richtiges Oberlicht (statt nur feste Scheibe
|
||||
via `transomHeight`), Fensterbank (heute `sillBoard` ungenutzt), Sprossengitter.
|
||||
Alles hängt an vorhandenen Zeichen-/Mesh-Funktionen; nur Parameterausbau.
|
||||
|
||||
### P2 — Detailbestandteile
|
||||
|
||||
```ts
|
||||
interface WindowType {
|
||||
reveal?: { create: boolean; depth: number; thickness: number }; // Laibungsverkleidung
|
||||
underlight?: { height: number; frame?: number }; // Unterlicht
|
||||
niche?: { aussen?: boolean; innen?: boolean; depth: number }; // Nische
|
||||
headShape?: "eckig" | "schraeg_links" | "schraeg_rechts" | "spitz" | "rund";
|
||||
handleGeom?: { kind: "quader" | "rund"; masse: Record<string, number> };
|
||||
}
|
||||
interface Opening { foreground?: string; background?: string; strokeWeight?: number; } // Bestandteil-Attribute
|
||||
```
|
||||
|
||||
Rationale: Form (Rundbogen/Schräge → neue Öffnungsumriss-Geometrie), Laibung/Nische,
|
||||
Beschlag-Geometrie, per-Instanz-Attribute (analog Wand/Decke, die es schon haben).
|
||||
|
||||
### P3 — VW-Ballast (nur wenn je nachgefragt)
|
||||
|
||||
Bemaßungs-Bezugsschemata (B1..B5/H1..H7), Sichtbarkeitsmatrix 3D×Ansichten,
|
||||
IFC/Energos, Geländer, Heizkörper, Eckfenster, eigenes Symbol, Fassadenmodul-Versatz.
|
||||
Siehe Abschnitt 5.
|
||||
|
||||
**Höchster-Wert-Reihenfolge (verdichtet):** (1) `sashes` inkl. Öffnungsrichtung je
|
||||
Flügel, (2) mehrscheibige Verglasung, (3) Rollladenkasten, (4) Bank/Nische,
|
||||
(5) Sprossengitter, (6) Form (Rund/Spitz/Schräg), (7) Ober-/Unterlicht als eigene
|
||||
Felder.
|
||||
|
||||
---
|
||||
|
||||
## 5. Was NICHT bauen / Risiken
|
||||
|
||||
**Nicht bauen (VW-Komplexität ohne DOSSIER-Nutzen):**
|
||||
|
||||
- **Sichtbarkeitsmatrix „3D-Objekte × 9 Ansichten"** (Oben/Unten/Horizontalschnitt/
|
||||
Vorne/Hinten/Frontalschnitt/Links/Rechts/Querschnitt). DOSSIER rendert heute
|
||||
primär Grundriss; Schnitt/Ansicht sind `DrawingLevelKind`-Platzhalter. Eine
|
||||
Matrix mit ~13×9 An/Aus-Zellen ist Pflegehölle ohne Zielrenderer. `DetailLevel`
|
||||
(grob/mittel/fein) genügt als Sichtbarkeitsachse.
|
||||
- **Bemaßungs-Bezugssystem (Roh-/Fertigmaß, B1..B5/H1..H7).** DOSSIER trägt lichte
|
||||
Maße; eine parametrische Öffnungs-Maßkette existiert nicht. Sehr teuer, geringer
|
||||
Nutzen für ein CAAD-Werkzeug dieser Reife.
|
||||
- **IFC-Datenmapping-Tiefe, Energos, Infopalette.** Kein IFC-Export-Pfad vorhanden;
|
||||
bewusst weglassen (kein Schein-Feature, vgl. die Modell-Doku-Konvention).
|
||||
- **Heizkörper, Geländer, Eckfenster** als Fenster-Unterobjekte — das sind eigene
|
||||
Bauteile bzw. topologische Sonderfälle (zwei Wände). Nicht ins Fenster packen.
|
||||
- **Eigenes Symbol verwenden** (Drawing2D-Override je Stil) — erst wenn ein
|
||||
Symbol-/Blocksystem existiert.
|
||||
|
||||
**Risiken:**
|
||||
|
||||
- **3D-Vorschau nicht als „korrekt" verkaufen.** Laut Projekt-Memo werden
|
||||
`render3d`/`Wasm3DViewport`-Änderungen NICHT per Puppeteer/Browser verifiziert;
|
||||
der Nutzer testet 3D selbst in der Tauri-Dev-App. Der Dialog darf 3D anzeigen,
|
||||
aber diese Studie/Implementierung behauptet keine visuelle Korrektheit der
|
||||
3D-Vorschau — nur der 2D-Pfad (`generatePlan`→SVG) ist headless prüfbar.
|
||||
- **Typ-vs-Instanz-Verwirrung.** Ohne klaren „vom Stil abweichend"-Indikator wird
|
||||
unklar, ob eine Änderung den Stil (alle Instanzen) oder nur dieses Fenster trifft.
|
||||
Muss im UI explizit sein (§3.4).
|
||||
- **`sashes` vs. `wingCount` Migration.** `wingCount` bleibt als Fallback; `sashes`
|
||||
hat Vorrang, wenn gesetzt. Resolver in `resolveOpeningFrame`
|
||||
(`toWalls3d.ts:1845`) und `windowSymbol` müssen beide Wege beherrschen, sonst
|
||||
brechen Alt-Projekte oder die `ObjectInfoPanel`-Flügelanzahl-Eingabe.
|
||||
- **Modal-Render-Kosten.** Live-Vorschau bei jedem Tastendruck neu generieren ist
|
||||
ok für ein Mini-Projekt, aber Patches sollten (leicht) entprellt werden.
|
||||
|
||||
---
|
||||
|
||||
## 6. Empfohlene erste Scheibe (P0, konkret)
|
||||
|
||||
Kleinstes End-to-End-Inkrement, das sich schon „reich" anfühlt:
|
||||
|
||||
**Umfang:** Dialog-Shell + Live-2D-Vorschau + Flügeleinteilung (n Flügel mit
|
||||
Öffnungsrichtung je Flügel) + mehrscheibige Verglasung + Rollladenkasten +
|
||||
„Als Stil speichern".
|
||||
|
||||
**Build-Reihenfolge:**
|
||||
|
||||
1. **Modell (P0-Felder).** `SashDef`, `WindowType.sashes?`, `WindowType.shading?`,
|
||||
`WindowType.glazingPanes?` in `src/model/types.ts` ergänzen (additiv). Resolver
|
||||
`getWindowType` bleibt; `wingCount`→`sashes`-Ableitung als Helper.
|
||||
2. **Renderer-Konsum (2D zuerst, headless prüfbar).**
|
||||
- `windowSymbol`/`mullionLines` (`src/geometry/opening.ts`) auf `sashes` umstellen
|
||||
(ungleichmäßige Pfostenlagen + Öffnungsrichtung je Flügel als Öffnungslinien).
|
||||
- `glassPanesForOpening`/2D-Glaslinien auf `glazingPanes` (statt konstant 1/detail).
|
||||
- neuer Zeichenzweig in `addOpeningSymbol` (`src/plan/generatePlan.ts:2214`) für
|
||||
die Rollladenkasten-Kontur; 3D-Box später.
|
||||
- Unit-Tests gegen `generatePlan()`-Output (Primitive zählen) — das ist der
|
||||
verifizierbare Beweis, nicht nur „grüne Typecheck".
|
||||
3. **Dialog-Shell.** `src/ui/OpeningEditorDialog.tsx` (Overlay `res-overlay` +
|
||||
`role="dialog"`), Sidebar mit den Kategorien Basis / Größe/Position / Rahmen /
|
||||
Flügel-Sprossen / Sonnenschutz. Nur diese fünf für P0.
|
||||
4. **Live-2D-Vorschau.** Mini-Projekt (kurze Wand + editiertes `Opening`) →
|
||||
`generatePlan()` → `sceneToPrintSvg()`/`planToPrintSvg()` → `<div>`. Grundriss +
|
||||
Elevations-SVG. Reaktiv auf Patches (leicht entprellt).
|
||||
5. **Flügel-Tabelle.** UI-Tabelle mit „Flügel einfügen / Pfosten einfügen / Löschen"
|
||||
und je Zeile Öffnungsart + Anschlag (`SashDef`). Schreibt `WindowType.sashes`.
|
||||
6. **„Als Stil speichern".** Stil-Leiste + `onSaveAsStyle` → `addWindowType`
|
||||
(`src/App.tsx:2304`) mit `PromptDialog`-Namensabfrage; „zurücksetzen" verwirft
|
||||
Instanz-Overrides.
|
||||
7. **⚙-Verdrahtung.** `ObjectInfoPanel.OpeningSection` (:731) → neuer Host-Handler
|
||||
`onOpenOpeningEditor(openingId)`; App-State `openingEditorId`. Ressourcen-Tab
|
||||
bleibt als Bibliothek erhalten.
|
||||
|
||||
**Definition of Done (P0):** Aus dem ⚙ öffnet sich der Dialog; man setzt 3 Flügel,
|
||||
davon einer Dreh-links / einer Dreh-rechts / einer fest, wählt Dreifachverglasung
|
||||
und einen Rollladenkasten; die 2D-Vorschau zeigt die Pfosten/Öffnungslinien/Kasten
|
||||
sofort; „Speichern…" legt einen benannten Fensterstil an, der im Typ-Dropdown der
|
||||
`OpeningSection` erscheint. Verifikation über `generatePlan()`-Primitiv-Tests
|
||||
(2D) — 3D-Box erst danach, vom Nutzer in der Tauri-App geprüft.
|
||||
@@ -0,0 +1,82 @@
|
||||
# SIA 400 — Fenster-/Türdarstellung (Grundriss, Schnitt, Ansicht)
|
||||
|
||||
> Quelle: SIA 400:2000 „Planbearbeitung im Hochbau", Anhang B.9 (Darstellung von
|
||||
> Bauteilen), Figuren 36–42. Das PDF liegt im Projektwurzel (lokal excluded,
|
||||
> NICHT committen — urheberrechtlich geschützt). Dies ist die verbindliche
|
||||
> Darstellungsnorm (Schweiz) — NICHT DIN. Massstab = Detailgrad:
|
||||
> **1:100 = grob, 1:50 = mittel, 1:20 = fein.**
|
||||
|
||||
## B.9.1.1 Fenster im Grundriss (Figuren 36/37/38)
|
||||
|
||||
Regel (Zitat): „Die Darstellung der Fensterkonstruktionen erfolgt nach denselben
|
||||
Regeln, unabhängig davon ob es sich um Holz-, Holz-Metall-, Metall- oder
|
||||
Kunststoff-Fenster handelt."
|
||||
|
||||
### Figur 36 — Massstab 1:100 (= grob)
|
||||
- Wand **voll schwarz** (Poché massiv), keine Schraffur.
|
||||
- Fenster = **eine dünne Rahmen-/Glasband-Andeutung** über die Öffnung, sehr
|
||||
schematisch — im Wesentlichen EIN schmales Rechteck/Band mit wenigen Marken,
|
||||
KEINE Flügel-/Anschlagdetails, KEINE Laibungsmarken.
|
||||
|
||||
### Figur 37 — Massstab 1:50 (= mittel)
|
||||
- Wand **materialschraffiert** (Bänder: Hinterlüftung/Dämmung senkrecht
|
||||
gestrichelt, Backstein/Struktur diagonal). Drei Wandaufbauten gezeigt
|
||||
(Backstein+Aussendämmung hinterlüftet / Holzelementbau / Backstein verputzt).
|
||||
- Fenster: **Blendrahmen als schmales Band** nahe der Aussenfläche, dünne
|
||||
Kontur; **Glaslinie**; **ein kleines Quadrat mittig** (Stulp/Flügelstoss).
|
||||
Laibung/Öffnungstiefe sichtbar. Einfach, aber als Fenster lesbar.
|
||||
|
||||
### Figur 38 — Massstab 1:20 (= fein)
|
||||
- Wand voll schraffiert (Kreuzschraffur Backstein, Diagonalschraffur Dämmung).
|
||||
- Fenster: **volles Rahmenprofil** — Blendrahmen + Flügelrahmen als mehrere
|
||||
parallele Linien (Profiltiefe); **Glas als Doppellinie** (Isolierverglasung
|
||||
IV, zwei enge Parallelen); **zwei Stulp-Quadrate mittig** (Flügelstoss der
|
||||
zwei Flügel); **Anschlag/Laibung** mit Rahmen-Wand-Detail (Falz/Rebate),
|
||||
gestrichelte Anschlagmarken an den Ecken; Anschlagwinkel an den Laibungen.
|
||||
|
||||
## B.9.1.2 Fenster im Schnitt (Figuren 39/40, M 1:50)
|
||||
- Rahmen im Schnitt mit Brüstung/Sturz, Bemassung (z. B. Brüstung +0.90,
|
||||
Sturzhöhe). Fenstertüren (Figur 40): Rahmen bis Boden, Schwelle.
|
||||
|
||||
## B.9.1.3 Sinnbilder Fenster (Öffnungsart in der ANSICHT)
|
||||
Liste der Öffnungsarten (SIA-Benennung):
|
||||
- Fest im Rahmen verglast (kein Symbol)
|
||||
- Drehflügel einflüglig mit Verschluss, **Band rechts/links** (Dreieck, Spitze zur BANDSEITE)
|
||||
- Drehflügel fest mit Band und Plattenverschraubung
|
||||
- Zweiflüglig mit **Öffnungsreihenfolge** (1 = erstöffnend, 2 = zweitöffnend)
|
||||
- Kippflügel mit Verschluss (Dreieck)
|
||||
- Kippflügel fest, für Reinigung bedienbar
|
||||
- Klappflügel mit Verschluss
|
||||
- Drehkippflügel, Band rechts
|
||||
- Schwingflügelfenster
|
||||
- Wendeflügelfenster, Achse in der Mitte
|
||||
- Vertikales Schiebefenster (nach oben schiebbar, oberer Flügel fest)
|
||||
|
||||
Konvention: Dreieck-/Winkelsinnbild, **Spitze zeigt zur Bandseite (Anschlag)**,
|
||||
Basis zur Griffseite. (Genaues Sinnbild-Blatt S. 38 rechts.)
|
||||
|
||||
## B.9.1.4 Kurzzeichen Fenster/Sonnenschutz
|
||||
KL Klappladen · SL Schiebeladen · ROL Rolladen · LAM Lamellenstoren ·
|
||||
RAF Rafflamellenstoren · FAL Faltrolladen · K Kurbel · DV Doppelverglasung ·
|
||||
IV Isolierverglasung · IV3 Dreifachisolierverglasung · BFB Beton-Fensterbank ·
|
||||
MFB Metall-Fensterbank · FFB Faserzement-Fensterbank.
|
||||
|
||||
## B.9.2 Türen im Grundriss (Figuren 41/42) + Sinnbilder (B.9.2.2)
|
||||
- **Türblatt als Linie ab Band + Viertelkreisbogen** (Schwingbahn). Bandseite =
|
||||
Bogenmittelpunkt. Zweiflüglig/Doppeltüre = zwei Bögen.
|
||||
- Zargenarten: Futterrahmen/Zarge, Blockrahmen/Profil, Blendrahmen — je mit
|
||||
Anschlag-/Schwellenmark. Bei Niveaudifferenz Schwellenstrich.
|
||||
- Weitere Sinnbilder: Pendeltüre (Bogen beidseitig gestrichelt), Falttüre
|
||||
(Zickzack), Faltschiebetor, Drehtüre (Kreis mit X), Kipptor, Schiebetüre
|
||||
(ausserhalb/in der Wand, Pfeil), Harmonikatüre.
|
||||
|
||||
## Umsetzung im Code (Soll)
|
||||
- `generatePlan.ts` Fenster-Zweig + `geometry/opening.ts::windowSymbol`:
|
||||
drei DEUTLICH getrennte Detailstufen statt heute (mittel≈fein):
|
||||
- **grob:** 1 Rahmen-/Glasband (Rechteck), sonst nichts.
|
||||
- **mittel:** Blendrahmen-Rechteck + Glaslinie + 1 Stulp-Quadrat mittig +
|
||||
Flügel-Trennlinien; keine verschachtelten Flügelrahmen, kein Anschlag.
|
||||
- **fein:** verschachtelte Blend-+Flügelrahmen (mehrlinig) + Glas-Doppellinie
|
||||
(IV) + Stulp-Quadrate + Anschlag/Laibungsmarken.
|
||||
- Öffnungssymbole (Ansicht) auf SIA umstellen (Spitze zur Bandseite), Türbögen
|
||||
bleiben (schon SIA-konform).
|
||||
@@ -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 |
@@ -15,7 +15,7 @@
|
||||
rel="icon"
|
||||
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%232f6df6'/%3E%3Cg fill='white'%3E%3Crect x='3' y='3' width='4' height='4'/%3E%3Crect x='9' y='3' width='4' height='4'/%3E%3Crect x='3' y='9' width='4' height='4'/%3E%3Crect x='9' y='9' width='4' height='4'/%3E%3C/g%3E%3C/svg%3E"
|
||||
/>
|
||||
<title>cad — Phase 0 Spike</title>
|
||||
<title>Dossier</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,29 +1,64 @@
|
||||
{
|
||||
"name": "cad",
|
||||
"name": "dossier",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Karim Gabriele Varano",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"tauri": "tauri",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:build": "tauri build",
|
||||
"dump:native": "node scripts/dump-native-scene.mjs",
|
||||
"shell": "scripts/chromium-shell.sh",
|
||||
"electron": "scripts/electron-shell.sh",
|
||||
"build:engine": "wasm-pack build src-tauri/render2d --release --target web --out-dir ../../src/engine/pkg --out-name render2d --no-default-features --features web",
|
||||
"build:engine3d": "wasm-pack build src-tauri/render3d --release --target web --out-dir ../../src/engine/pkg3d --out-name render3d --no-default-features --features web",
|
||||
"build:geometry": "wasm-pack build src-tauri/geometry --release --target web --out-dir ../../src/engine/pkgGeometry --out-name geometry --no-default-features --features web",
|
||||
"build:kernel2d": "wasm-pack build src-tauri/kernel2d --release --target web --out-dir ../../src/engine/pkgKernel2d --out-name kernel2d --no-default-features --features web",
|
||||
"build:dwgimport": "wasm-pack build src-tauri/dwgimport --release --target web --out-dir ../../src/engine/pkgDwgImport --out-name dwgimport --no-default-features --features web",
|
||||
"build:truck": "wasm-pack build src-tauri/trucksolid --release --target web --out-dir ../../src/engine/pkgTruck --out-name trucksolid --no-default-features --features web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mlightcad/libredwg-web": "^0.7.7",
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||
"@tauri-apps/plugin-fs": "^2.5.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"
|
||||
},
|
||||
"allowScripts": {
|
||||
"core-js@3.49.0": true,
|
||||
"esbuild@0.21.5": true,
|
||||
"fsevents@2.3.3": true,
|
||||
"fsevents@2.3.2": true,
|
||||
"wasm-pack@0.15.0": true
|
||||
}
|
||||
}
|
||||
|
||||
|
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: 303 KiB |
|
After Width: | Height: | Size: 394 KiB |
|
After Width: | Height: | Size: 215 KiB |
|
After Width: | Height: | Size: 702 KiB |
|
After Width: | Height: | Size: 551 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 |