Vollständige Bestandsaufnahme der Codebasis als neue STATUS.md (Kennzahlen, Feature-Inventar, Mist-Liste: toter Code, verwaiste WASM-Crates, Doku-Widersprüche). ARCHITECTURE.md/README.md/CONVENTIONS.md waren noch auf dem Tag-1-Planungsstand (Electron/Three.js/OpenCascade/Zustand/HLR-Worker) und beschrieben nicht mehr, was tatsächlich gebaut wurde (eigene Rust/WASM-Engines, eigener Store, analytische Rust-Schnitt-Pipeline, Tauri auf macOS + Electron auf Linux). ROADMAP.md und HANDOVER.md als historisch markiert (Hinweis-Box), Inhalt unverändert.
20 KiB
Architektur — Dossier (Desktop-CAAD)
Stand: 2026-07-21 (grundlegend überarbeitet — siehe STATUS.md für die volle Bestandsaufnahme inkl. Mist-Liste, die diese Überarbeitung begründet). Vision/Phasen (historisch, Tag-1-Stand): ROADMAP.md. Konventionen: CONVENTIONS.md. Detail-Designs (teils ebenfalls veraltet, siehe Hinweis in docs/README.md): docs/design/.
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: 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.
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 (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. 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 §5.
┌──────────────────────────────────────────┐
│ Project (semantisches Modell, JSON) │ ← einzige Wahrheit
│ resources · types · drawingLevels · │
│ layers · walls/doors/openings/stairs/… │
└──────────────┬───────────────────────────┘
│ pure derive()
┌───────────────────────┼────────────────────────────┬───────────────┐
▼ ▼ ▼ ▼
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 ─────────────┘
1. Repo-Struktur (IST-Zustand)
src/
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 (unverändert gegenüber der Vision)
- Zeichnungsebenen (
DrawingLevel) — Geschoss (kind:"floor",floorHeight/cutHeight/baseElevation), Schnitt/Ansicht (kind:"section"|"elevation"), oder freie Zeichnung (kind:"drawing"). - 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 …).
2.2 Elemente — typisierte Arrays statt Element[]-Union
Anders als ursprünglich geplant hält Project (src/model/types.ts:2095)
pro Bauteiltyp ein eigenes (meist optionales) Array:
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?
}
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 Ressourcen & Aufbauten (unverändert gegenüber der Vision)
interface Resources { lineStyles: LineStyle[]; hatches: Hatch[]; components: Component[]; }
interface WallType { id; name; layers: { componentId; thickness }[]; }
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 Layouts / Ausschnitte
ViewSnapshot (Kamera/Massstab/Detailgrad/Sichtbarkeiten/Override-Preset) in
ViewSnapshotFolder-Bäumen; Layout (Papierformat, mehrere
LayoutViewports, freie LayoutAnnotations, optionale MasterLayout-Vererbung)
in LayoutFolder-Bäumen. Beide gehören ins Dokument (nicht LocalStorage).
3. State, Persistenz, Undo/Redo
3.1 Store — eigener useSyncExternalStore, nicht Zustand
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).
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.
3.2 Persistenz
- 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.tsleitet einzelne Operationen (aktuell nurcomputeJoins) unter Tauri viainvokean 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
Eigener History-Ring im projectSlice (kein DOSSIER-Sticky-Bus, kein
Cache-Stale-Problem, da alle Sichten pure Ableitungen sind).
4. Rendering
4.1 Zwei 3D-Viewports
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
PlanView.tsx(SVG) — bleibt immer im DOM (Hit-Testing/Grips), unabhängig vom aktiven Zeichenpfad.plan/glPlan/— eigener TypeScript-WebGL2-Renderer.useWasmPlanRenderer.ts→ Rust-render2d(WGSL, wgpu nativ, WebGPU/WebGL2-Fallback im Web, echtes Text-Rendering viaglyphon).
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:
App.tsx (section3dCutId/section3dPlane)
→ Wasm3DViewport.tsx (section3d-Prop → setSectionPlane)
→ src-tauri/render3d/src/{section.rs, section_boolean.rs, section_fill.rs}
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.
4.4 Öffnungen als Löcher (kein Mesh-Boolean)
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.
4.5 Materialien
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.MeshStandardMaterials mit
physisch korrekter Kachelgrösse (UV in Weltmetern).
5. React-Panel-Struktur
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.
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).
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 → Dossier — Mapping-Tabelle (Kernkonzepte)
| DOSSIER (Rhino-Plugin) | Dossier (Tauri) | Anmerkung |
|---|---|---|
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 übernommen wurde — und was bewusst anders lief
Ü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).
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 (weiterhin gültig): keine Abstraktion ohne konkretes Problem; erst lesen, dann editieren; Geometrie nie „nebenbei" refactoren.
8. Reihenfolge bei Code-Arbeit
- STATUS.md (aktueller Ist-Zustand) + dieses Dokument lesen.
- Das betroffene Modul lesen (nicht raten) — bei Zweifel: welcher der mehreren Renderer/Pfade ist gerade aktiv (§4.1/§4.2)?
- Erst danach editieren;
Projectimmer immutabel via den Store ändern. - Verifizieren:
npx tsc -b,npm test(Vitest),cargo testin den betroffenen Crates, bei Rust-Änderungennpm run build:engine{,3d}(WASM neu bauen, nur.rscommitten —src/engine/pkg*/ist gitignored). Bei render3d/Wasm3DViewport-Änderungen testet der Nutzer selbst in der Tauri-Dev-App (nicht per Puppeteer/Browser verifizierbar). - Dieses Dokument aktuell halten, wenn sich Patterns ändern — insbesondere nicht wieder in „Ziel-Struktur"-Beschreibungen abdriften, die Monate lang niemand nachführt.