Files
karim a6c2c04736 Doku: STATUS.md (Codebase-Analyse) + Kern-Docs an den Ist-Zustand angeglichen
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.
2026-07-21 13:35:58 +02:00

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)

  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 …).

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.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

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

  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:

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

  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.