5 Commits

Author SHA1 Message Date
karim 9e73c684c3 App.tsx: Hell/Dunkel-State nach theme/useThemeMode.ts
Reiner Umzug von useState + Setter-Wrapper (persistiert in localStorage via
themeMode.ts) in einen eigenen Hook. Verhaltensgleich.
2026-07-21 16:24:33 +02:00
karim 6637533162 App.tsx: Dialog-Sichtbarkeits-State nach state/useDialogState.ts
Reiner Umzug von neun useState-Deklarationen (Ressourcen-Fenster, Layout-
Ansicht, Import-/Export-/Einstellungs-Dialoge, Drop-Feedback) in einen
eigenen Hook — bewusst KEIN Store-Slice (dieser State ist laut den
Original-Kommentaren explizit lokal/transient, keine Undo/Redo- oder
Cross-Component-Relevanz). Verhaltensgleich.
2026-07-21 16:15:59 +02:00
karim 3b79e49cc6 App.tsx entschlackt: Kontextmenüs nach src/menus/, Views nach src/views/
Reiner Umzug (verhaltensgleich, byte-identische Funktionskörper): Kontextmenü-
Builder (buildMenuItems + layer/level/plan/viewport-Menüs) nach
src/menus/contextMenu.tsx; InlineEditor sowie Content/LevelPlanView/
SectionPlanView (der View-Router der Hauptansicht) nach src/views/. App.tsx
7130 → 5917 Zeilen. Setzt die in CONVENTIONS.md/state-architecture.md
vorgesehene, bisher nie umgesetzte Struktur endlich um (STATUS.md §4.3).

Dazu ein unabhängiger CSS-Fix: .plan-selected (Auswahl-Hervorhebung im 2D-Plan)
fehlte die fill-opacity, die zwei Regeln weiter unten bei .plan-marquee/
.tool-preview-fill schon gesetzt ist. Im Dunkel-Theme ist --accent-dim
transparent (zufällig unauffällig), im neuen Standard-Hell-Theme aber fast
deckend weiss — dadurch verdeckte die Auswahl die Schraffur der gewählten
Wand/Decke/des Raums vollständig.
2026-07-21 14:47:40 +02:00
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
karim 956a85d93f Materialbibliothek: statisches Broad-Set entfernt, Live-ambientCG bleibt
Der aus der Desktop-Session übernommene statische 88er-Katalog listete 75
Materialien ohne mitgelieferte Texturen (kaputte Thumbnails in der
Schnellauswahl). Bibliothek auf die 13 tatsächlich gebündelten Starter
gekürzt; Fetch-Script und der zugehörige .gitignore-Block entfernt. Alles
darüber hinaus läuft über die bereits vorhandene Live-Suche (1K/2K/4K).
2026-07-21 13:35:44 +02:00
18 changed files with 2194 additions and 3829 deletions
-19
View File
@@ -12,25 +12,6 @@ scripts/*.png
.DS_Store .DS_Store
*.log *.log
# ── Broad material textures (fetched via scripts/fetch-materials.mjs) ─────────
# Binary JPGs from the DOSSIER-LIBRARY mirror are NOT committed here — run:
# node scripts/fetch-materials.mjs --library <DOSSIER-LIBRARY-path>
# to populate them locally. The 12 starter folders are explicitly excluded from
# this ignore rule so they stay tracked in git.
public/assets/materials/*/
!public/assets/materials/Bricks104/
!public/assets/materials/Concrete048/
!public/assets/materials/Grass005/
!public/assets/materials/Gravel043/
!public/assets/materials/Ground103/
!public/assets/materials/Marble012/
!public/assets/materials/Metal063/
!public/assets/materials/PavingStones150/
!public/assets/materials/Plaster001/
!public/assets/materials/Tiles141/
!public/assets/materials/Wood095/
!public/assets/materials/WoodFloor051/
# Generierte native-Viewport-Szenen (aus sampleProject via scripts/dump-native-scene.mjs) # Generierte native-Viewport-Szenen (aus sampleProject via scripts/dump-native-scene.mjs)
src-tauri/assets/native2d_scene.json src-tauri/assets/native2d_scene.json
src-tauri/assets/native3d_walls.json src-tauri/assets/native3d_walls.json
+297 -359
View File
@@ -1,452 +1,390 @@
# Architektur — Standalone Browser-BIM (cad) # Architektur — Dossier (Desktop-CAAD)
> Stand: 2026-06-29 > Stand: 2026-07-21 (grundlegend überarbeitet — siehe [STATUS.md](STATUS.md) für
> Vision, Phasen und Backlog: [ROADMAP.md](ROADMAP.md). Konventionen: [CONVENTIONS.md](CONVENTIONS.md). > die volle Bestandsaufnahme inkl. Mist-Liste, die diese Überarbeitung begründet).
> Detail-Designs: [docs/design/elements.md](docs/design/elements.md) · > Vision/Phasen (historisch, Tag-1-Stand): [ROADMAP.md](ROADMAP.md). Konventionen:
> [docs/design/plans-output.md](docs/design/plans-output.md) · > [CONVENTIONS.md](CONVENTIONS.md). Detail-Designs (teils ebenfalls veraltet,
> [docs/design/resources-graphics.md](docs/design/resources-graphics.md). > siehe Hinweis in [docs/README.md](docs/README.md)): [docs/design/](docs/design/).
Dieses Dokument beschreibt, **wie** die Standalone-Browser-App gebaut wird und Dieses Dokument beschreibt, **wie** Dossier tatsächlich gebaut ist — nicht wie
wie sie die Konzepte des DOSSIER-Rhino-Plugins in Browser-Äquivalente übersetzt. es am ersten Tag geplant war. Dossier ist die eigenständige Neuimplementierung
DOSSIER ist ein Rhino-8-Plugin (Python + React-WebView); `cad` ist die *eigen­ des Rhino-Plugins [DOSSIER](https://git.openbureau.ch/karim/dossier): dieselbe
ständige* Browser-Variante: kein Rhino-Document, kein `doc.Strings`, kein Denkweise (Geschosse, Kategorie-Ebenen, mehrschichtige Bauteile,
IronPython — stattdessen ein **eigenes typisiertes Datenmodell in TypeScript**, Prioritäts-Verschneidung), aber als **native Desktop-App** mit einem **eigenen
gerendert über **Three.js** (3D) und **SVG** (Plan), gespeichert als **JSON-Datei**. 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 **Desktop-Rahmen (plattformabhängig, wegen WebGPU):** auf **macOS Tauri**
UI-Texte sind deutsch. Einheiten intern in **Metern**. (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 ## 0. Leitprinzip — ein Modell, viele Darstellungen
Das semantische Gebäudemodell ist die **einzige Wahrheit**. Jede Sicht (3D, Das semantische Gebäudemodell (`Project`) ist die **einzige Wahrheit**. Jede
Grundriss, Schnitt, Ansicht) ist eine **reine Ableitung** (`derive`) daraus. Sicht (3D, Grundriss, Schnitt, Ansicht) ist eine **reine Ableitung** daraus.
Darstellung (Detailgrad, Stile, Schraffuren, Overrides) wird **beim Rendern** 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 │ Project (semantisches Modell, JSON) │ ← einzige Wahrheit
│ resources · types · designLevels · │ resources · types · drawingLevels · │
│ layers · elements │ layers · walls/doors/openings/stairs/…
└──────────────┬───────────────────────────┘ └──────────────┬───────────────────────────┘
│ pure derive() │ pure derive()
┌───────────────────────┼────────────────────────────┐ ┌───────────────────────┼────────────────────────────┬───────────────
▼ ▼ ▼ ▼ ▼ ▼
Scene3D (Three.js) PlanModel → SVG SectionModel → SVG 3D-Viewport 2D-Plan (generatePlan) 3D-Live-Schnitt Export
buildScene() generatePlan() generateSection() (HLR) Viewport3D (three.js) PlanView (SVG) · glPlan (GL2) render3d/section IFC/DXF/
│ │ │ ODER Wasm3DViewport ODER render2d (Rust/WGSL) (Rust, analytisch)PDF/STL
└────────── beide lesen dieselben joins/components/styles ──────────┘ (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) ## 1. Repo-Struktur (IST-Zustand)
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).
``` ```
src/ src/
model/ model/ Project-Schema (types.ts, ~2500 LOC), joins.ts (Wand-Gehrung/
types.ts // Project, DesignLevel, LayerCategory, Element-Union (existiert) -Prio-Stösse), parametricWalls.ts, roomStamp.ts, terrain.ts,
geometry.ts // 2D/3D-Mathe ohne Kernel (existiert) geoRebase.ts, sampleProject.ts
joins.ts // Wand-Verschneidung (Gehrung; später Prio-T/X) (existiert) geometry/ 2D-Kernel (kernel2d.ts: offset/trim/fillet/split — LIVE, siehe
sampleProject.ts // Demo-Haus (existiert) §3), ceiling/opening/roomArea/roomBoundary/stair/roof/column.ts,
project.ts // Factory, Defaults, Migrationen polygonHoles.ts
selectors.ts // abgeleitete Reads (visibleCodes, elementsOnLevel…) commands/ Rhino-artiges Kommandosystem: types/registry/engine/parseInput.ts
ids.ts // newId(prefix) — UUID-Erzeugung + cmds/ (ein Modul je Kommando: wall, opening, stair, roof,
elements/ // pro Bauteil ein Modul (Daten + Generierung) column, room, line/rect/circle/arc, move/mirror/copy/offset/
wall.ts opening.ts slab.ts stair.ts roof.ts structure.ts space.ts trim/join, extrude, import, terrain, measure, georef, …)
resources/ tools/ Interaktive Zeichenwerkzeuge, snapping.ts, transform.ts (Grips)
componentManager.ts hatchManager.ts lineManager.ts symbolLibrary.ts compute/ Compute-Boundary: leitet Ops (aktuell nur computeJoins) unter
overrides.ts // regelbasierte Engine (Condition → Action) Tauri via `invoke` an Rust weiter, sonst TS-Fallback
store/ plan/ generatePlan.ts (2D-Ableitung), PlanView.tsx (SVG + Pan/Zoom/
store.ts // Zustand-Store: { project, ui }, Aktionen, Undo/Redo Grips), glPlan/ (eigener WebGL2-Renderer), toSection.ts/
persistence.ts // Datei save/load (JSON), Autosave (IndexedDB) toElevation.ts (Schnitt/Ansicht-Ableitung), toWalls3d.ts,
history.ts // Undo/Redo-Ring toRenderScene.ts (Szene für Rust-render2d), wallMeshCut.ts
plan/ viewport/ Viewport3D.tsx (three.js, „Free") UND Wasm3DViewport.tsx
generatePlan.ts // Grundriss aus Footprint + Symbolik (existiert) (Rust/wgpu „Nordstern", Default/editierbar), raycast3d.ts
generateSection.ts // Schnitt/Ansicht aus 3D-Projektion (HLR-Worker) section/ TOTER Code (OCCT-WASM-HLR-Spike, keine Aufrufer mehr) — Schnitt
PlanView.tsx // SVG-Renderer + Pan/Zoom/Grips (existiert) läuft über render3d/section.rs, siehe §4.3
primitives.ts // Primitive-Union, SVG-Serializer, DXF/PDF-Export export/ exportIfc.ts (IFC4), exportDxf.ts/dxfWriter.ts, exportPdf.ts,
viewport/ layoutPdf.ts (Mehrseiten-PDF pro Ordner), exportMesh.ts (STL/OBJ),
Viewport3D.tsx // Three.js-Szene aus dem Modell (existiert) exportSchedule.ts (CSV-Bauteilliste), sceneToPrintSvg.ts
scene.ts // buildScene(project) → THREE.Group (Layer-Spiegel) materials/ ambientcg.ts (Live-Suche ambientCG-API), library.ts (13
clip.ts // Plan-/Schnitt-Clipping über THREE.Plane gebündelte Starter-Materialien), runtime.ts (PBR-Material aus
camera.ts // Kamera-Presets, Norden-Rotation ComponentMaterial via three.js TextureLoader)
sheets/ io/ DXF/DWG-Import, Swisstopo (swissBUILDINGS3D/-ALTI3D/SWISSIMAGE,
layout.ts // Sheet/Viewport-Datenmodell LV95↔WGS84), OSM-Overpass, .lin/.pat-Parser, projectFile.ts
SheetEditor.tsx // Plansatz-Editor (.obp Speichern/Laden, Tauri-Lock)
exportPdf.ts // Vektor-PDF (svg → pdf-lib / jsPDF) overrides/ Regelbasierte Override-Engine (Bedingung → Aktion)
workers/ state/ Eigener Store auf `useSyncExternalStore` (KEIN Zustand/Redux):
geometry.worker.ts // Booleans (Öffnungen) + HLR via Comlink store.ts + Slices (project/history/selection/view/layout/site/
ui/ notify), appStore.ts komponiert sie
App.tsx Navigator panels // React-Oberfläche (heute in App.tsx, wird gesplittet) 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. 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 1. **Zeichnungsebenen** (`DrawingLevel`) — Geschoss (`kind:"floor"`,
JSON-Bäume (`dossier_zeichnungsebenen`, `dossier_ebenen`). Wir übernehmen das `floorHeight`/`cutHeight`/`baseElevation`), Schnitt/Ansicht
1:1 in `types.ts` (existiert bereits als `drawingLevels` + `layers`): (`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 ### 2.2 Elemente — typisierte Arrays statt `Element[]`-Union
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 …`
Ein **Element** kennt sein **Geschoss** (`floorId`) *und* seine **Ebene** Anders als ursprünglich geplant hält `Project` (`src/model/types.ts:2095`)
(`categoryCode`). Sichtbarkeit ergibt sich aus dem Schnitt (Geschoss `geschoss × code` **pro Bauteiltyp ein eigenes (meist optionales) Array**:
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.
```ts ```ts
interface Resources { interface Project {
lineStyles: LineStyle[]; // Line Manager: { id, name, weight(mm), color, dash:number[] } walls: Wall[];
hatches: Hatch[]; // Hatch Manager: { id, name, pattern, scale, angle, lineStyleId } ceilings?: Ceiling[];
components: Component[]; // Component Manager (= DOSSIER-Material erweitert): roofs?: Roof[];
// { id, name, hatchId, color3d, texture3d?, joinPriority } 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 Ein `Element`-Typalias existiert noch (`kind:"door"|"window"` etc.), wird aber
**Daten** statt Hardcode — steuert die Prioritäts-T-Verschneidung (Risiko #1). 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 ```ts
interface Resources { lineStyles: LineStyle[]; hatches: Hatch[]; components: Component[]; }
interface WallType { id; name; layers: { componentId; thickness }[]; } 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 `joinPriority` sitzt am `Component` (Daten statt Hardcode) und steuert die
einmal pflegen). 3D und Plan lesen dieselben `layers[]`. 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 `ViewSnapshot` (Kamera/Massstab/Detailgrad/Sichtbarkeiten/Override-Preset) in
`Window | Slab | Stair | Roof | Column | Beam | Space | Draw2d`. Jedes Element: `ViewSnapshotFolder`-Bäumen; `Layout` (Papierformat, mehrere
`{ id, type, floorId, categoryCode, ... }`. Gehostete Elemente (Tür/Fenster) `LayoutViewport`s, freie `LayoutAnnotation`s, optionale `MasterLayout`-Vererbung)
tragen `hostWallId` statt `floorId` (Geschoss ergibt sich aus der Wand). Volle in `LayoutFolder`-Bäumen. Beide gehören ins Dokument (nicht LocalStorage).
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 };
}
```
--- ---
## 3. State, Persistenz, Undo/Redo ## 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 Entgegen der ursprünglichen Empfehlung (Zustand-Library) wurde ein
nicht für Werkzeuge + Undo. Migration auf **Zustand** (ROADMAP §4): **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 **Nicht umgesetzt** (geplant in `docs/design/state-architecture.md`): die
interface AppState { Extraktion von View-Routing nach `src/views/` und Kontextmenü-Aufbau nach
project: Project; // das Dokument `src/menus/` — beide Ordner existieren nicht, diese Logik liegt weiterhin
ui: { // flüchtig, NICHT persistiert/undobar inline in `App.tsx` (7.130 Zeilen). Siehe STATUS.md §4.3.
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;
}
```
**Trennung Modell ↔ UI** ist hart: nur `project` ist undobar und wird gespeichert. ### 3.2 Persistenz
`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 — JSON-Datei statt `doc.Strings` - **Datei:** eigenes `.obp`-Format (`src/io/projectFile.ts`), unter Tauri über
native Speichern/Öffnen-Dialoge (`plugin-fs`/`plugin-dialog`), im Browser
DOSSIER speichert *alles* in `doc.Strings` (Key-Value am Rhino-Document) + Blob-Fallback. Ältere `.json`-Projekte bleiben ladbar.
globale Presets als JSON-Dateien im User-Home. Browser-Äquivalent: OS-Level-Exklusiv-Lock (`fs4`-Crate, `src-tauri/src/lock.rs`) verhindert
Doppelöffnen desselben Projekts.
| DOSSIER | Browser (cad) | - **Compute-Boundary:** `src/compute/index.ts` leitet einzelne Operationen
|---|---| (aktuell nur `computeJoins`) unter Tauri via `invoke` an eine native Rust-
| `doc.Strings.SetString(key, json)` | Feld im `Project`-Objekt (ein JSON-Baum) | Implementierung weiter; im Browser bzw. für nicht migrierte Ops (Room-
| `.3dm`-Datei mit eingebetteten Strings | **`.cad.json`-Datei** via File System Access API | Detection, DWG-Parsing) läuft die TS-Implementierung.
| 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.
### 3.3 Undo/Redo ### 3.3 Undo/Redo
DOSSIER überlässt Undo Rhino (und hat dadurch Cache-Stale-Bugs, Schwachstelle Eigener History-Ring im `projectSlice` (kein DOSSIER-Sticky-Bus, kein
#4.3). Wir besitzen das Dokument selbst → **eigener History-Ring**: Cache-Stale-Problem, da alle Sichten pure Ableitungen sind).
```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.
--- ---
## 4. Rendering ## 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` - **`Viewport3D.tsx`** — three.js, die „Free"-Stufe.
(`layer_builder.build_layers`) und hängt jedes Objekt an den passenden Sublayer. - **`Wasm3DViewport.tsx`** — Rust/wgpu, „Nordstern", editierbar, **Default**.
Browser-Spiegel: ein **`THREE.Group`-Baum** mit identischer Struktur, gebaut aus Ein Settings-Schalter wählt die Engine; WebGL2/three.js nur noch als
dem Modell (nicht persistiert — reine Ableitung): 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 App.tsx (section3dCutId/section3dPlane)
└─ levelGroup[floorId] (y-Offset = baseElevation) ← Geschoss → Wasm3DViewport.tsx (section3d-Prop → setSectionPlane)
└─ categoryGroup[categoryCode] (userData.code) ← Ebene → src-tauri/render3d/src/{section.rs, section_boolean.rs, section_fill.rs}
└─ element meshes (userData.elementId)
``` ```
```ts `section_boolean.rs` ist ein 1:1-Port von `toSection.ts::subtractDominantBands`
// scene.ts — 2D-Plan-Schnitt und 3D-Live-Schnitt nutzen dieselbe Prioritäts-Logik und
function buildScene(project: Project, vis: VisibilityState): THREE.Group stimmen dadurch exakt überein. Kein Worker, kein Comlink (beides war geplant,
function syncScene(root: THREE.Group, project, vis) // diff statt full rebuild (Perf) existiert nirgends im Projekt) — läuft synchron GPU-seitig.
```
- **Sichtbarkeit:** `categoryGroup.visible = vis.codes.has(code)` und ### 4.4 Öffnungen als Löcher (kein Mesh-Boolean)
`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.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 ### 4.5 Materialien
`okff + schnitthöhe` legt, Normale Z (`layer_builder.update_clipping_plane`).
Zwei Browser-Pfade — **beide nötig** (ROADMAP §3):
1. **3D-Viewport im Plan-Modus:** `THREE.Plane(normal=(0,0,-1), constant=cutZ)` über `materials/library.ts` (13 gebündelte PBR-Starter, ambientCG CC0) +
`renderer.clippingPlanes` + `material.clippingPlanes`. `clip.ts` setzt die Ebene `materials/ambientcg.ts` (Live-Suche der kompletten ambientCG-Bibliothek,
pro aktivem Geschoss (`cutZ = baseElevation + cutHeight`), Kamera orthografisch 1K/2K/4K-Auflösungswahl, On-Demand-Download via `jszip`, Proxy wegen CORS) →
von oben. So sieht man den **echten geschnittenen Volumenkörper**. `materials/runtime.ts` baut daraus gecachte `THREE.MeshStandardMaterial`s mit
2. **Symbolischer SVG-Grundriss** (der Hauptweg, schnell + sauber): `generatePlan.ts` physisch korrekter Kachelgrösse (UV in Weltmetern).
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 12 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.
--- ---
## 5. React-Panel-Struktur ## 5. React-Panel-Struktur
DOSSIER ist eine Sammlung **getrennter WebView-Panels** (EBENEN, ELEMENTE, Dock-/Floating-Panel-System (`src/panels/`: Dock, FloatingPanel, TabStrip,
GESTALTUNG, OBERLEISTE, MASSSTAB, AUSSCHNITTE, DIMENSIONEN, LAYOUTS, OVERRIDES), Registry) mit eingebauten Panels: Tools, Attributes, ObjectInfo,
die über die Python-Bridge + `sc.sticky` kommunizieren. Im Browser ist alles DrawingLevels, Layers, Site (Kontext/Terrain-Import), RoomBalance
**eine SPA** mit einem geteilten Store — die Panels werden zu Docking-Bereichen. (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,
│ TopBar Werkzeuge · Ansichtstyp · Massstab · Snaps · Save/Load │ ≙ OBERLEISTE+MASSSTAB DrawingLevels-Detaileditor, LayerSettings und ContextImport
├──────────────┬──────────────────────────────────────┬───────────────┤ (`src/native/*Window.ts` + `*WindowApp.tsx`), jeweils mit
│ Navigator │ Viewport (Three.js oder SVG-Plan) │ Inspector │ `isTauriRuntime()`-Gate und ohne separate Browser-Variante (im Browser bleibt
│ ┌──────────┐ │ ┌────────────────────────────────┐ │ ┌───────────┐ │ die entsprechende In-App-Overlay-Variante aktiv, wo vorhanden).
│ │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
```
**Komponenten-Map** (heute alles in `App.tsx`; wird gesplittet): **Werkzeug-System:** `Tool`-Interface (`src/tools/types.ts`) für Zeichenwerkzeuge
mit Snap-Engine; daneben das umfangreichere **Kommandosystem** (`src/commands/`)
| Bereich | DOSSIER-Panel | cad-Komponente | für Rhino-artige getippte Eingabe (`5,3`/`r5,3`/`5<45`, Tab-Feld-Zyklus in
|---|---|---| `CommandLine.tsx`) — beide koexistieren, decken unterschiedliche
| Zeichnungsebenen-Liste | EBENEN (oben) | `Navigator/DrawingLevels.tsx` | Interaktionsstile ab.
| 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.
--- ---
## 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 | | Rhino `RhinoDoc` | `Project` (TS-Objekt im eigenen Store) | einzige Wahrheit |
| **`doc.Strings[key]=json`** | Feld im `Project`-JSON | persistiert in `.cad.json` | | `doc.Strings[key]=json` | Feld im `Project`-JSON | persistiert in `.obp` |
| **`.3dm`-Datei** | **`.cad.json`** via File System Access API | + IndexedDB-Autosave | | `.3dm`-Datei | **`.obp`**-Datei (Tauri `plugin-fs`) | + Blob-Fallback im Browser |
| **Globale Presets (~/Library/*.json)** | **LocalStorage** + Export/Import | cross-Projekt | | `sc.sticky` (cross-modul Bus) | eigener `useSyncExternalStore`-Store | kein Polling |
| **`sc.sticky` (cross-modul Bus)** | **Zustand-Store** (reaktiv) | kein Polling, kein None-Bug | | Rhino Layer-Tabelle | `LayerCategory[]`-Baum, Sichtbarkeit als `.visible`-Flag | pro Renderer umgesetzt |
| **`panel_base.BaseBridge` / WebView-IPC** | direkte React-Props/Store | keine `document.title`-Hacks | | Clipping-Plane (`AddClippingPlane`) | `render3d::section.rs` (analytisch, Rechteck-Subtraktion) | KEIN HLR |
| **`document.title="RHINOMSG::"` Polling** | entfällt | SPA, kein WebView | | `HLRBRep` | — (ungenutzt; OCCT-Spike ist toter Code) | ersetzt durch obiges |
| **Eto.Forms Satelliten-Fenster** | React-Modal/Drawer | z.B. Ausschnitt-Settings | | `Rhino.Geometry.Brep`-Booleans | `trucksolid::boolean_mesh` (csgrs) | existiert, nicht an Wände angeschlossen |
| **Rhino Layer-Tabelle (Sublayer-Baum)** | `LayerCategory[]`-Baum + `THREE.Group`-Spiegel | `scene.ts` | | Rhino-Grips + DisplayConduit | Pointer-Events + Grip-Overlay (2D vollständig, 3D nur Basis, kein Snap) | siehe STATUS.md §4.7 |
| **`layer_builder.build_layers`** | `buildScene` (Ableitung) | Gruppen statt Layer | | Swisstopo via .NET HttpClient | `fetch()` (CORS-offen) | radiusgenau zugeschnitten (nicht ganze STAC-Kachel) |
| **`layer.PlotWeight`** | `LineStyle.weight` (mm) → SVG `stroke-width` | massstabsabh. (§plans) | | IronPython-Laufzeit-Risiken | entfällt | TS/Rust |
| **`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) | — |
--- ---
## 7. Was wir von DOSSIER übernehmen — und was nicht ## 7. Was übernommen wurde — und was bewusst anders lief
**Übernehmen (bewährte Konzepte):** **Übernommen (Prinzipien, bewährt):**
- Zwei-Achsen-Dokumentmodell (Zeichnungsebenen × Ebenen). - Zwei-Achsen-Dokumentmodell, Layer-Codes 1:1, Component/Hatch/Line-Manager
- Layer-Codes/Farben/lw 1:1 (`DEFAULT_LAYER_SCHEMA`). mit `joinPriority`, LoD (grob/mittel/fein), regelbasierte Overrides,
- Component/Hatch/Line-Manager mit id-Verweisen + `joinPriority`. Ausschnitte, SIA-416-Räume, Norden-Rotation.
- Ansichtstypen = Kamera + optionaler Schnitt (vereinheitlicht). - Pure-Ableitungs-Architektur (kein Cache-Stale, kein Sticky-Bus).
- 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.
**Bewusst anders (Browser-nativ, Schwachstellen vermeiden):** **Anders gelaufen als geplant (siehe STATUS.md §5 für die volle Tabelle):**
- **Kein Sticky-Bus** → ein reaktiver Store (vermeidet DOSSIER #4.4/#4.5/#4.6). - Eigene Rust/WASM-Rendering-Engines statt Three.js/OpenCascade.js/web-ifc.
- **Kein Monolith** → Bauteil-Module statt 7244-LOC-`elemente.py` (#4.1). - Typisierte Arrays pro Bauteiltyp statt `Element[]`-Union.
- **Pure Ableitungen** statt mutierter Doc-Objekte → kein Cache-Stale (#4.3), - Eigener Store statt Zustand-Library.
kein Undo/Redo-Loch. - Analytische Rust-Schnitt-Pipeline statt HLR/Worker/Comlink.
- **Versioniertes Datei-Schema** statt UserString-Sticky-Migration. - App.tsx wurde **nicht** wie geplant auf eine dünne Shell reduziert
- **Persistenz im Project-JSON** statt verteilt über `doc.Strings`. (`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 **Anti-Over-Engineering (weiterhin gültig):** keine Abstraktion ohne konkretes
ohne konkretes Problem; kein `try/catch: {}` als Bug-Versteck; erst grep/lesen, Problem; erst lesen, dann editieren; Geometrie nie „nebenbei" refactoren.
dann editieren; Wand-Geometrie nie „nebenbei" refactoren.
--- ---
## 8. Reihenfolge bei Code-Arbeit ## 8. Reihenfolge bei Code-Arbeit
1. **Dieses Dokument + das relevante Detail-Design** (elements/plans-output/ 1. **STATUS.md** (aktueller Ist-Zustand) + dieses Dokument lesen.
resources-graphics) lesen. 2. **Das betroffene Modul** lesen (nicht raten) — bei Zweifel: welcher der
2. **Dann das betroffene Modul** lesen (nicht raten). mehreren Renderer/Pfade ist gerade aktiv (§4.1/§4.2)?
3. **Erst danach editieren**; `Project` immer immutabel via `store.apply()` ändern. 3. **Erst danach editieren**; `Project` immer immutabel via den Store ändern.
4. **Verifizieren:** `npx tsc -b`, `npm run build`, Screenshot via 4. **Verifizieren:** `npx tsc -b`, `npm test` (Vitest), `cargo test` in den
`node scripts/probe.mjs` — Geometrie visuell prüfen. betroffenen Crates, bei Rust-Änderungen `npm run build:engine{,3d}` (WASM
5. **Dieses Dokument aktuell halten**, wenn sich Patterns/Mapping ändern. 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.
+25 -11
View File
@@ -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) ## 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) CCW-Wicklung zeigt `+n` nach innen. Schichten werden außen (T/2) → innen (+T/2)
gestapelt. 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, - **Ziel:** `App.tsx` bleibt ein dünner Shell (Store-Provider, Oberleiste,
Statusleiste, Floating-Panels, Ressourcen-Overlay) — keine Geschäftslogik darin. Docks+View-Router, Statusleiste, Floating-Panels, Ressourcen-Overlay) — keine
- **Globaler Zustand in einem Store** (`src/state/`, Slices: project/selection/view/layout). Geschäftslogik darin. **Realität (Stand 2026-07-21):** `App.tsx` ist mit
Komponenten lesen Zustand über Store-Hooks statt Prop-Drilling. ~7.100 Zeilen die grösste Datei des Projekts und enthält weiterhin
- **Features als eigene Module:** `src/views/` (View-Router-Teile), `src/editors/` View-Umschaltung und Kontextmenü-Aufbau inline — die geplante Auslagerung
(Inline-Editoren), `src/menus/` (Kontextmenü-Builder), `src/panels/`, `src/ui/`. 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). - Ziel: modular + parallel bearbeitbar (verschiedene Features ≠ dieselbe Datei).
Siehe `docs/design/state-architecture.md`.
## UI-Konventionen ## UI-Konventionen
@@ -61,4 +73,6 @@ Rendern angewandt, nie in die Geometrie eingebacken.
- Änderungen verifizieren: `npx tsc -b`, `npm run build`, und Screenshot via - Ä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 `node scripts/probe.mjs` (schreibt `scripts/probe.png`) bzw. `probe-ff*.mjs` für
Firefox-Fälle. Screenshot ansehen und Geometrie visuell prüfen. 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).
+6
View File
@@ -1,3 +1,9 @@
> **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.
# HANDOVER — Stand 2026-07-09 (autonome Session, ALLES committet) # HANDOVER — Stand 2026-07-09 (autonome Session, ALLES committet)
Lange autonome Session (Nutzer-Auftrag: „arbeite die Pendenzen/Funktionen durch, Lange autonome Session (Nutzer-Auftrag: „arbeite die Pendenzen/Funktionen durch,
+105 -94
View File
@@ -1,16 +1,17 @@
# DOSSIER Standalone # Dossier
Open-Source-CAAD (Computer Aided Architecture Design). Modelliert ein Gebäude Open-Source-CAAD (Computer Aided Architecture Design). Modelliert ein Gebäude
aus semantischen Bauteilen und zieht daraus saubere, normgerechte 2D-Pläne — aus semantischen Bauteilen und zieht daraus saubere, normgerechte 2D-Pläne —
ohne Revit. Als Desktop-App und im Browser zugänglich; die Desktop-App ist die ohne Revit. **Desktop-App** — auf macOS über **Tauri**, auf Linux über eine
vollständige Fassung. **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 Standalone-Variante des Rhino-Plugins Das ist die eigenständige Neuimplementierung des Rhino-Plugins
[DOSSIER](https://git.kgva.ch/karim/DOSSIER): dieselbe Denkweise (Geschosse, [DOSSIER](https://git.openbureau.ch/karim/dossier): dieselbe Denkweise
Kategorie-Ebenen, mehrschichtige Bauteile, Prioritäts-Verschneidung), aber als (Geschosse, Kategorie-Ebenen, mehrschichtige Bauteile, Prioritäts-
React-App mit eigener Rendering-Engine (Rust/WASM/WebGPU) statt als Rhino-Aufsatz. Verschneidung), aber mit eigenem Datenmodell + eigener Rendering-Engine
Als Desktop-App (Tauri) läuft sie im eigenen Fenster mit voller Engine-Leistung; (Rust/WASM/WebGPU, intern „Nordstern" genannt) statt Rhino-Aufsatz.
browserseitig ist derselbe Kern zugänglich, die App ist die vollständige Fassung.
## Grundgedanke ## Grundgedanke
@@ -22,117 +23,124 @@ in die Geometrie eingebacken. Wer eine Wand verschiebt, verschiebt sie überall.
Konkret heißt das auch: Der Grundriss entsteht nicht aus einem zerschnittenen Konkret heißt das auch: Der Grundriss entsteht nicht aus einem zerschnittenen
3D-Mesh, sondern **analytisch aus den Bauteil-Parametern** (Wandachsen + Dicken → 3D-Mesh, sondern **analytisch aus den Bauteil-Parametern** (Wandachsen + Dicken →
Linien, Öffnungen → Lücken + Symbol). PDF-Export ist derselbe Plan, nur mit Linien, Öffnungen → Lücken + Symbol). PDF-Export ist derselbe Plan, nur mit
echten mm-Stiftstärken statt Bildschirm-Hairlines — kein zweites Rendering. echten mm-Stiftstärken statt Bildschirm-Hairlines. Schnitte und Ansichten laufen
Schnitte und Ansichten brauchen den zweiten Weg — echte 3D-Projektion mit über eine eigene, analytische Rust-Pipeline (kein Hidden-Line-Removal nötig,
verdeckten Kanten (HLR) —, dessen Machbarkeit per Spike bewiesen, aber noch siehe [ARCHITECTURE.md](ARCHITECTURE.md) §4.3) und sind seit Juli live im
nicht ans UI angebunden ist. 3D-Viewport verdrahtet, nicht nur ein Spike.
## Stand heute ## Stand heute
Ehrlich eingeordnet: aus dem Risiko-Spike ist ein **benutzbares 2D-CAD mit Aus dem ursprünglichen Risiko-Spike ist binnen weniger Wochen ein
abgeleiteter 3D-Sicht, Vektor-PDF/DXF-Export und einer parametrischen **funktionsreiches Desktop-BIM-Tool** geworden: eigenes semantisches
Wand-Engine** geworden. Der einfachere Teil steht und ist per Screenshot/Probe Gebäudemodell, zwei eigene Rust/WASM-Rendering-Engines, IFC/DXF/PDF/STL/OBJ-
verifiziert; die schwierigen BIM-Kernrisiken sind bewusst noch offen. 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:** **Funktioniert (Auszug — volle Liste in STATUS.md §3):**
- Semantisches Modell mit **mehrschichtigen Wänden** und automatischer - Semantisches Modell mit **mehrschichtigen Wänden**, L-Eck-Gehrung **und**
**L-Ecken-Gehrung** (`computeJoins`); dieselbe Logik speist Plan *und* 3D. Prioritäts-T-/X-Stössen (`joinPriority` am Component) — konsistent in
- **Parametrische Wände** (`ParametricWall`-Regelwerke: Grid/Modul/Sequenz/ Grundriss, 3D-Viewport und 3D-Live-Schnitt.
Referenzlinie/bedingte Dicke) lösen sich zu konkreten `Wall`-Objekten auf, - **Parametrische Wände**, Decken, Treppen (gerade/L/Wendel), Dächer
statt jede Wand einzeln von Hand zu ziehen. (Flach/Pult/Sattel/Walm/Mansarde/Zelt), Stützen, SIA-416-Räume mit
- **Dokumentmodell wie in DOSSIER:** Zeichnungsebenen (Geschosse + Schnitte/ automatischer Bilanz + CSV-Export.
Ansichten/Zeichnungen) × Kategorie-Ebenen (Code-Baum 1:1, `20 Wände`, `30 Decken` …). - **Türen & Fenster** gehostet in Wänden, mit Rahmen/Zarge/Kämpfer/Oberlicht,
- **Zeichnen & Editieren** im Grundriss: Wand, Linie, Polylinie, Rechteck, Kreis, Detailgrad grob/mittel/fein, echte Rechteck-Löcher im 3D-Wandkörper.
Öffnungen, Treppen, Decken, Raumstempel; Snapping (Endpunkt/Mittelpunkt/ - Rhino-artiges **Kommandosystem** mit getippten Koordinaten (`5,3` · `r5,3` ·
Schnittpunkt/Lot/Raster/Ortho), Grips, Move/Copy/Offset, Spiegeln/Drehen/Array, `5<45`) und Tab-Feld-Zyklus; Snapping, Grips, Trim/Split/Join, 2D-Booleans.
Trim/Split/Join. - **Live 2D↔3D-Schnitt**: eine Schnittebene im 3D-Viewport folgt derselben
- **Rhino-artiges Befehlssystem** mit getippten Koordinaten (`5,3` · `r5,3` · Prioritäts-Logik wie der 2D-Plan-Schnitt (Rust-Port, keine Diskrepanz).
`5<45`) und Tab-Feld-Zyklus (Länge → Winkel …). - **Vektor-Export:** PDF (Einzel- **und Mehrseiten-Layouts**), DXF, **IFC4**
- **Vektor-Export:** PDF (A4/A3, Titelblock, echte mm-Stiftstärken nach ISO-Pen- (mit echten Fenster-/Tür-Löchern), STL/OBJ, CSV-Bauteilliste.
Steps, keine Rasterbilder) und DXF — beide aus derselben `Plan`-Struktur wie - **Materialbibliothek**: 13 gebündelte PBR-Starter **plus** Live-Suche der
der Bildschirm. kompletten ambientCG-Bibliothek (1K/2K/4K, On-Demand-Download).
- **PBR-Material-Bibliothek** (ambientCG-Import, `manifest.json`) für die - **Layouts/Plansätze** mit mehreren Viewports pro Blatt, Ausschnitte
3D-Ansicht. (View-Snapshots), Kamera-Presets, Norden-Rotation.
- **Resource Manager** (Component / Hatch / Line) — alles per id referenziert, - **Import:** DXF/DWG, Swisstopo (swissBUILDINGS3D/-ALTI3D/SWISSIMAGE,
zentral änderbar. radiusgenau zugeschnitten), OSM-Kontextimport, `.lin`/`.pat`.
- **Panel-System** (dockbar, stapelbar, floatend), Top-Bar + Statusleiste - **Native Desktop-Integration** (Tauri): eigene randlose Fenster, native
(echter Maßstab 1:N, Cursor X/Y), eigenes Kontextmenü, **i18n** (de/en). Speichern/Öffnen-Dialoge, eigenes `.obp`-Dateiformat mit OS-Level-Lock gegen
- **Import:** DXF/DWG (Konturen/Mesh) → Terrain-TIN, Swisstopo/LV95-Geokontext, Doppelöffnen, mehrere native Zusatzfenster (Resources, Settings, …).
OSM-Kontextimport; `.lin`/`.pat` für Linien/Schraffuren. - **Resource Manager** (Component/Hatch/Line/Typ-Editoren), regelbasierte
Overrides, Panel-System (dockbar/floatend), i18n (de/en).
**Bewusst noch offen** (die eigentlich harten Teile): **Bewusst noch offen** (Details + volle Liste: STATUS.md §4.7):
- Echte **3D-Booleans** für Öffnungen — Türen/Fenster sind im Plan eine Lücke, - **Echtes Mesh-Boolean für Öffnungen**funktioniert heute über achsparallele
noch kein geschnittenes Volumen. Rechteck-Löcher; ein generisches CSG-Boolean existiert bereits
- **HLR** (verdeckte Kanten) für Schnitte und Ansichten: Machbarkeit per (`trucksolid`/`csgrs`), ist aber nicht an die Wand-Pipeline angeschlossen.
OCCT-WASM-Spike bewiesen (`docs/welle-c-hlr-spike/`, `src/section/hlr.ts`), - **3D-Griffsystem**: Feld-Controller (Tab-Zyklus) und Snapping fehlen für
aber noch nicht ans UI/Dokumentmodell verdrahtet — Views sind noch Stubs. 3D-Grip-Drag (im 2D vollständig vorhanden).
- **Prioritäts-T-/X-Stöße** mehrschichtiger Wände (Beton läuft durch, Putz - **DWG/DXF-Domänenimport** (Entitäten → echte Wände/Öffnungen) — Lesen
verbindet seitlich) — das berüchtigte Risiko #1. funktioniert, das Mapping auf das Modell ist unbegonnen; DWG-Schreiben fehlt.
- **Multi-Page-Layouts/Ausschnitte** (mehrere Viewports pro Blatt) — PDF-Export - **`make2D`**-Kommando (3D-Ansicht → flacher 2D-Plan mit Füllungen).
ist noch single-sheet. - Bekannte, noch nicht bereinigte Doppelspur `Door[]`/`Opening[]` im Modell.
Details und die Begründungen stehen im
[HANDOVER](HANDOVER.md) (laufendes Arbeitsprotokoll) und in der
[ROADMAP](ROADMAP.md) (Vision, Phasen, Backlog).
## Stack ## Stack
Bewusst leichtgewichtig — Three.js ist reiner Display-Layer, kein schwerer
Geometrie-Kernel verfrüht eingezogen.
| | | | | |
|---|---| |---|---|
| Shell | Electron (eigenes randloses App-Fenster, kein Browser-Tab) | | 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 | | Frontend | React + TypeScript + Vite |
| 3D | Three.js, plus eigener nativer Rust/WASM/WebGPU-Renderer (`render3d`) | | 3D | eigener Rust/WASM/WebGPU-Renderer „Nordstern" (`render3d`, Default), Three.js als leichtgewichtiger Zweitpfad |
| 2D-Plan | eigener SVG-Renderer, plus eigener nativer Rust/WASM/WebGPU-Renderer (`render2d`) | | 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`), `delaunator` fürs Terrain | | Geometrie | eigener 2D-Kernel (`src/geometry/kernel2d.ts`); ein Rust-Port (`src-tauri/kernel2d`) existiert nur als Paritätstest, läuft nicht produktiv |
| Export | `jspdf`/`svg2pdf.js` (Vektor-PDF), eigener DXF-Writer | | 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 | | Import | `dxf-parser`, `@mlightcad/libredwg-web` (DWG), eigene `.lin`/`.pat`-Parser |
| Schnitt/HLR-Spike | `opencascade.js` (OCCT-WASM) — isoliert, noch nicht verdrahtet | | Materialien | `jszip` (ambientCG-Zip-Entpacken), `three.js` `TextureLoader` |
Geplant, aber noch nicht eingezogen: `rhino3dm` (NURBS / `.3dm`), web-ifc (IFC), `opencascade.js` steht noch als Dependency in `package.json`, wird aber nur
Manifold (exakte 3D-Booleans). OCCT-WASM ist für HLR bereits als Spike da (s.o.), noch von totem Code (`src/section/hlr.ts`, superseded durch die Rust-
aber noch nicht an Dokumentmodell/UI angebunden. Siehe ROADMAP §4. 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 ## Entwicklung
```bash ```bash
npm install npm install
npm run dev # Vite, http://localhost:5187 npm run dev # Vite, http://localhost:5187
npm run electron # Electron-Fenster (Dev-Server + randlose App-Shell) 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 npx tsc -b # Typecheck
npm run build # tsc -b && vite build npm run build # tsc -b && vite build
npm test # vitest run npm test # vitest run
``` ```
Verifiziert wird visuell: `node scripts/probe.mjs` rendert die App headless und WASM-Engines nach Rust-Änderungen neu bauen (nur `.rs` committen,
schreibt `scripts/probe.png` — Screenshot ansehen, Geometrie prüfen. Weitere `src/engine/pkg*/` ist gitignored):
`scripts/probe-*.mjs` decken einzelne Features ab (PDF, Trim, Materialien,
Theme, Boolean-Ops …); für Firefox-Fälle gibt es `scripts/probe-ff*.mjs` ```bash
(Playwright). 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 ## Aufbau
``` ```
src/ src/
model/ semantisches Modell + Ableitungen (types, parametricWalls, roomStamp, joins, terrain) model/ semantisches Modell (types, joins, parametricWalls, roomStamp, terrain)
geometry/ 2D-Kernel (offset/trim/fillet/intersect, ceiling, opening, roomArea/-Boundary, stair) geometry/ 2D-Kernel (offset/trim/fillet/intersect, ceiling/opening/roomArea/stair/roof/column)
commands/ Rhino-artiges Befehlssystem (engine, parser, registry, cmds/) commands/ Rhino-artiges Befehlssystem (engine, parser, registry, cmds/)
tools/ interaktive Zeichenwerkzeuge + Snapping + Transformationen tools/ interaktive Zeichenwerkzeuge + Snapping + Transformationen
plan/ Grundriss-Ableitung (generatePlan) + PlanView (SVG) + glPlan (WebGL2-Renderer) plan/ Grundriss-Ableitung (generatePlan) + PlanView (SVG) + glPlan (WebGL2) + Rust-Bindings
viewport/ Viewport3D (Three.js) viewport/ Viewport3D (Three.js) + Wasm3DViewport (Rust/wgpu „Nordstern", Default)
section/ HLR-Spike (OCCT-WASM) — Schnitte/Ansichten, noch nicht verdrahtet section/ TOTER Code (OCCT-HLR-Spike) — Schnitt läuft über render3d, siehe ARCHITECTURE.md §4.3
export/ PDF- und DXF-Export aus derselben Plan-Struktur wie der Bildschirm export/ IFC4-, PDF-, DXF-, STL/OBJ-, CSV-Export aus derselben Plan-/Modell-Struktur
materials/ PBR-Material-Bibliothek (ambientCG-Import, Runtime) materials/ ambientCG-Live-Suche + gebündelte Starter-Bibliothek + PBR-Runtime
text/ Rich-Text (Beschriftungen, Textobjekte)
panels/ dockbares Panel-System + die einzelnen Paletten panels/ dockbares Panel-System + die einzelnen Paletten
state/ Store + Slices (project/selection/view/layout) state/ eigener Store (useSyncExternalStore) + Slices (project/selection/view/layout/…)
ui/ Top-Bar, Statusleiste, Kontextmenü, Resource Manager, Command-Line 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 io/ Import/Export (DXF, DWG, .lin, .pat), Swisstopo/LV95, OSM-Kontext
i18n/ Wörterbücher de/en i18n/ Wörterbücher de/en
src-tauri/ Rust-Crates (render2d/render3d) — headless per wasm-pack zu WASM src-tauri/ 6 Rust-Crates (render2d/render3d/geometry/kernel2d/trucksolid/dwgimport,
gebaut (npm run build:engine), Tauri-Host selbst ist ausrangiert je headless UND per wasm-pack baubar) + der Tauri-Host selbst
``` ```
## Konventionen ## Konventionen
@@ -141,21 +149,24 @@ src-tauri/ Rust-Crates (render2d/render3d) — headless per wasm-pack zu WAS
(Design Layer, Component, Hatch, Wall Style). **UI-Texte sind deutsch**, immer (Design Layer, Component, Hatch, Wall Style). **UI-Texte sind deutsch**, immer
über `t('key')` — keine hartcodierten Strings im JSX. über `t('key')` — keine hartcodierten Strings im JSX.
- Intern alles in **Metern**; Anzeige via `formatM`. - Intern alles in **Metern**; Anzeige via `formatM`.
- `App.tsx` bleibt dünner Shell, Zustand lebt im Store. Verbindliches in - Verbindliches in [CONVENTIONS.md](CONVENTIONS.md).
[CONVENTIONS.md](CONVENTIONS.md).
## Weiterlesen ## Weiterlesen
- [ROADMAP.md](ROADMAP.md) — Produktvision, Architektur-Entscheidungen, Phasen 07, DOSSIER-Backlog - [STATUS.md](STATUS.md) — **vollständige, ehrliche Bestandsaufnahme**: Zahlen,
- [ARCHITECTURE.md](ARCHITECTURE.md) — Technische Architektur im Detail Feature-Inventar, Mist-Liste, Doku-Widersprüche, Tag-1-Vision vs. heute
- [HANDOVER.md](HANDOVER.md) — aktueller Arbeitsstand, Befunde, nächste Schritte - [ARCHITECTURE.md](ARCHITECTURE.md) — technische Architektur im Detail (Ist-Zustand)
- [docs/](docs/) — Design-Specs (Befehlssystem, Zeichenwerkzeuge, Wand-Joins, Backend …) - [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 ## Lizenz
Copyright © 2026 Karim Gabriele Varano. Veröffentlicht unter der Copyright © 2026 Karim Gabriele Varano. Veröffentlicht unter der
**GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)** **GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)**
siehe [LICENSE](LICENSE). DOSSIER ist Teil der **openbureau**-Suite. siehe [LICENSE](LICENSE). Dossier ist Teil der **openbureau**-Suite.
Die AGPL verlangt, dass auch bei Betrieb als Netzwerk-/Webdienst der (ggf. Die AGPL verlangt, dass auch bei Betrieb als Netzwerk-/Webdienst der (ggf.
geänderte) Quellcode für die Nutzer verfügbar gemacht wird. Drittkomponenten geänderte) Quellcode für die Nutzer verfügbar gemacht wird. Drittkomponenten
@@ -164,4 +175,4 @@ behalten ihre jeweiligen Lizenzen (siehe „Über"-Dialog in der App).
--- ---
Die UI ist deutsch; Schweizer Spezifika (SIA-416-Flächen, Swisstopo-Geodaten) Die UI ist deutsch; Schweizer Spezifika (SIA-416-Flächen, Swisstopo-Geodaten)
sind als Differenzierer eingeplant. sind ein bewusster Differenzierer.
+11 -1
View File
@@ -1,6 +1,16 @@
# Browser-BIM für Wohnbau — Architektur & Produkt-Roadmap # 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 25"/„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** > Ausrichtung: **BIM-first** · Nische: **Wohnbau / Einfamilienhäuser**
> Stand: 2026-06-28 > Stand: 2026-06-28
+342
View File
@@ -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 25 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.
+8 -1
View File
@@ -1,6 +1,13 @@
# Dokumentation — Standalone Browser-BIM (cad) # 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). > [CONVENTIONS.md](../CONVENTIONS.md) (Konventionen) · [ARCHITECTURE.md](../ARCHITECTURE.md).
Dieses Verzeichnis bündelt die Recherche- und Design-Dokumente für `cad`, die Dieses Verzeichnis bündelt die Recherche- und Design-Dokumente für `cad`, die
-883
View File
@@ -154,889 +154,6 @@
"displacement": "/assets/materials/Ground103/displacement.jpg", "displacement": "/assets/materials/Ground103/displacement.jpg",
"ao": "/assets/materials/Ground103/ao.jpg" "ao": "/assets/materials/Ground103/ao.jpg"
} }
},
{
"id": "Asphalt023S",
"name": "Asphalt 1",
"category": "Asphalt",
"maps": {
"color": "/assets/materials/Asphalt023S/color.jpg",
"normal": "/assets/materials/Asphalt023S/normal.jpg",
"roughness": "/assets/materials/Asphalt023S/roughness.jpg",
"displacement": "/assets/materials/Asphalt023S/displacement.jpg",
"ao": "/assets/materials/Asphalt023S/ao.jpg"
}
},
{
"id": "Asphalt025C",
"name": "Asphalt 2",
"category": "Asphalt",
"maps": {
"color": "/assets/materials/Asphalt025C/color.jpg",
"normal": "/assets/materials/Asphalt025C/normal.jpg",
"roughness": "/assets/materials/Asphalt025C/roughness.jpg",
"displacement": "/assets/materials/Asphalt025C/displacement.jpg",
"ao": "/assets/materials/Asphalt025C/ao.jpg"
}
},
{
"id": "Asphalt031",
"name": "Asphalt 3",
"category": "Asphalt",
"maps": {
"color": "/assets/materials/Asphalt031/color.jpg",
"normal": "/assets/materials/Asphalt031/normal.jpg",
"roughness": "/assets/materials/Asphalt031/roughness.jpg",
"displacement": "/assets/materials/Asphalt031/displacement.jpg",
"ao": "/assets/materials/Asphalt031/ao.jpg"
}
},
{
"id": "Bricks051",
"name": "Backstein 1",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks051/color.jpg",
"normal": "/assets/materials/Bricks051/normal.jpg",
"roughness": "/assets/materials/Bricks051/roughness.jpg",
"displacement": "/assets/materials/Bricks051/displacement.jpg"
}
},
{
"id": "Bricks059",
"name": "Backstein 2",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks059/color.jpg",
"normal": "/assets/materials/Bricks059/normal.jpg",
"roughness": "/assets/materials/Bricks059/roughness.jpg",
"displacement": "/assets/materials/Bricks059/displacement.jpg",
"ao": "/assets/materials/Bricks059/ao.jpg"
}
},
{
"id": "Bricks060",
"name": "Backstein 3",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks060/color.jpg",
"normal": "/assets/materials/Bricks060/normal.jpg",
"roughness": "/assets/materials/Bricks060/roughness.jpg",
"displacement": "/assets/materials/Bricks060/displacement.jpg",
"ao": "/assets/materials/Bricks060/ao.jpg"
}
},
{
"id": "Bricks066",
"name": "Backstein 4",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks066/color.jpg",
"normal": "/assets/materials/Bricks066/normal.jpg",
"roughness": "/assets/materials/Bricks066/roughness.jpg",
"displacement": "/assets/materials/Bricks066/displacement.jpg",
"ao": "/assets/materials/Bricks066/ao.jpg"
}
},
{
"id": "Bricks074",
"name": "Backstein 5",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks074/color.jpg",
"normal": "/assets/materials/Bricks074/normal.jpg",
"roughness": "/assets/materials/Bricks074/roughness.jpg",
"displacement": "/assets/materials/Bricks074/displacement.jpg",
"ao": "/assets/materials/Bricks074/ao.jpg"
}
},
{
"id": "Bricks075A",
"name": "Backstein 6",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks075A/color.jpg",
"normal": "/assets/materials/Bricks075A/normal.jpg",
"roughness": "/assets/materials/Bricks075A/roughness.jpg",
"displacement": "/assets/materials/Bricks075A/displacement.jpg",
"ao": "/assets/materials/Bricks075A/ao.jpg"
}
},
{
"id": "Bricks084",
"name": "Backstein 7",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks084/color.jpg",
"normal": "/assets/materials/Bricks084/normal.jpg",
"roughness": "/assets/materials/Bricks084/roughness.jpg",
"displacement": "/assets/materials/Bricks084/displacement.jpg",
"ao": "/assets/materials/Bricks084/ao.jpg"
}
},
{
"id": "Bricks085",
"name": "Backstein 8",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks085/color.jpg",
"normal": "/assets/materials/Bricks085/normal.jpg",
"roughness": "/assets/materials/Bricks085/roughness.jpg",
"displacement": "/assets/materials/Bricks085/displacement.jpg",
"ao": "/assets/materials/Bricks085/ao.jpg"
}
},
{
"id": "Bricks089",
"name": "Backstein 9",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks089/color.jpg",
"normal": "/assets/materials/Bricks089/normal.jpg",
"roughness": "/assets/materials/Bricks089/roughness.jpg",
"displacement": "/assets/materials/Bricks089/displacement.jpg",
"ao": "/assets/materials/Bricks089/ao.jpg"
}
},
{
"id": "Bricks090",
"name": "Backstein 10",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks090/color.jpg",
"normal": "/assets/materials/Bricks090/normal.jpg",
"roughness": "/assets/materials/Bricks090/roughness.jpg",
"displacement": "/assets/materials/Bricks090/displacement.jpg",
"ao": "/assets/materials/Bricks090/ao.jpg"
}
},
{
"id": "Bricks091",
"name": "Backstein 11",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks091/color.jpg",
"normal": "/assets/materials/Bricks091/normal.jpg",
"roughness": "/assets/materials/Bricks091/roughness.jpg",
"displacement": "/assets/materials/Bricks091/displacement.jpg",
"ao": "/assets/materials/Bricks091/ao.jpg"
}
},
{
"id": "Bricks092",
"name": "Backstein 12",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks092/color.jpg",
"normal": "/assets/materials/Bricks092/normal.jpg",
"roughness": "/assets/materials/Bricks092/roughness.jpg",
"displacement": "/assets/materials/Bricks092/displacement.jpg",
"ao": "/assets/materials/Bricks092/ao.jpg"
}
},
{
"id": "Bricks093",
"name": "Backstein 13",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks093/color.jpg",
"normal": "/assets/materials/Bricks093/normal.jpg",
"roughness": "/assets/materials/Bricks093/roughness.jpg",
"displacement": "/assets/materials/Bricks093/displacement.jpg",
"ao": "/assets/materials/Bricks093/ao.jpg"
}
},
{
"id": "Bricks094",
"name": "Backstein 14",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks094/color.jpg",
"normal": "/assets/materials/Bricks094/normal.jpg",
"roughness": "/assets/materials/Bricks094/roughness.jpg",
"displacement": "/assets/materials/Bricks094/displacement.jpg",
"ao": "/assets/materials/Bricks094/ao.jpg"
}
},
{
"id": "Bricks097",
"name": "Backstein 15",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks097/color.jpg",
"normal": "/assets/materials/Bricks097/normal.jpg",
"roughness": "/assets/materials/Bricks097/roughness.jpg",
"displacement": "/assets/materials/Bricks097/displacement.jpg",
"ao": "/assets/materials/Bricks097/ao.jpg"
}
},
{
"id": "Bricks100",
"name": "Backstein 16",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks100/color.jpg",
"normal": "/assets/materials/Bricks100/normal.jpg",
"roughness": "/assets/materials/Bricks100/roughness.jpg",
"displacement": "/assets/materials/Bricks100/displacement.jpg",
"ao": "/assets/materials/Bricks100/ao.jpg"
}
},
{
"id": "Bricks101",
"name": "Backstein 17",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks101/color.jpg",
"normal": "/assets/materials/Bricks101/normal.jpg",
"roughness": "/assets/materials/Bricks101/roughness.jpg",
"displacement": "/assets/materials/Bricks101/displacement.jpg",
"ao": "/assets/materials/Bricks101/ao.jpg"
}
},
{
"id": "Bricks102",
"name": "Backstein 18",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks102/color.jpg",
"normal": "/assets/materials/Bricks102/normal.jpg",
"roughness": "/assets/materials/Bricks102/roughness.jpg",
"displacement": "/assets/materials/Bricks102/displacement.jpg",
"ao": "/assets/materials/Bricks102/ao.jpg"
}
},
{
"id": "Bricks103",
"name": "Backstein 19",
"category": "Bricks",
"maps": {
"color": "/assets/materials/Bricks103/color.jpg",
"normal": "/assets/materials/Bricks103/normal.jpg",
"roughness": "/assets/materials/Bricks103/roughness.jpg",
"displacement": "/assets/materials/Bricks103/displacement.jpg",
"ao": "/assets/materials/Bricks103/ao.jpg"
}
},
{
"id": "Chipboard004",
"name": "Sperrholz 1",
"category": "Chipboard",
"maps": {
"color": "/assets/materials/Chipboard004/color.jpg",
"normal": "/assets/materials/Chipboard004/normal.jpg",
"roughness": "/assets/materials/Chipboard004/roughness.jpg",
"displacement": "/assets/materials/Chipboard004/displacement.jpg"
}
},
{
"id": "Chipboard005",
"name": "Sperrholz 2",
"category": "Chipboard",
"maps": {
"color": "/assets/materials/Chipboard005/color.jpg",
"normal": "/assets/materials/Chipboard005/normal.jpg",
"roughness": "/assets/materials/Chipboard005/roughness.jpg",
"displacement": "/assets/materials/Chipboard005/displacement.jpg"
}
},
{
"id": "Chipboard007",
"name": "Sperrholz 3",
"category": "Chipboard",
"maps": {
"color": "/assets/materials/Chipboard007/color.jpg",
"normal": "/assets/materials/Chipboard007/normal.jpg",
"roughness": "/assets/materials/Chipboard007/roughness.jpg",
"displacement": "/assets/materials/Chipboard007/displacement.jpg"
}
},
{
"id": "Concrete024",
"name": "Beton 1",
"category": "Concrete",
"maps": {
"color": "/assets/materials/Concrete024/color.jpg",
"normal": "/assets/materials/Concrete024/normal.jpg",
"roughness": "/assets/materials/Concrete024/roughness.jpg",
"displacement": "/assets/materials/Concrete024/displacement.jpg"
}
},
{
"id": "Concrete025",
"name": "Beton 2",
"category": "Concrete",
"maps": {
"color": "/assets/materials/Concrete025/color.jpg",
"normal": "/assets/materials/Concrete025/normal.jpg",
"roughness": "/assets/materials/Concrete025/roughness.jpg",
"displacement": "/assets/materials/Concrete025/displacement.jpg",
"ao": "/assets/materials/Concrete025/ao.jpg"
}
},
{
"id": "Concrete030",
"name": "Beton 3",
"category": "Concrete",
"maps": {
"color": "/assets/materials/Concrete030/color.jpg",
"normal": "/assets/materials/Concrete030/normal.jpg",
"roughness": "/assets/materials/Concrete030/roughness.jpg",
"displacement": "/assets/materials/Concrete030/displacement.jpg",
"ao": "/assets/materials/Concrete030/ao.jpg"
}
},
{
"id": "Concrete031",
"name": "Beton 4",
"category": "Concrete",
"maps": {
"color": "/assets/materials/Concrete031/color.jpg",
"normal": "/assets/materials/Concrete031/normal.jpg",
"roughness": "/assets/materials/Concrete031/roughness.jpg",
"displacement": "/assets/materials/Concrete031/displacement.jpg",
"ao": "/assets/materials/Concrete031/ao.jpg"
}
},
{
"id": "Concrete032",
"name": "Beton 5",
"category": "Concrete",
"maps": {
"color": "/assets/materials/Concrete032/color.jpg",
"normal": "/assets/materials/Concrete032/normal.jpg",
"roughness": "/assets/materials/Concrete032/roughness.jpg",
"displacement": "/assets/materials/Concrete032/displacement.jpg",
"ao": "/assets/materials/Concrete032/ao.jpg"
}
},
{
"id": "Concrete033",
"name": "Beton 6",
"category": "Concrete",
"maps": {
"color": "/assets/materials/Concrete033/color.jpg",
"normal": "/assets/materials/Concrete033/normal.jpg",
"roughness": "/assets/materials/Concrete033/roughness.jpg",
"displacement": "/assets/materials/Concrete033/displacement.jpg",
"ao": "/assets/materials/Concrete033/ao.jpg"
}
},
{
"id": "Concrete034",
"name": "Beton 7",
"category": "Concrete",
"maps": {
"color": "/assets/materials/Concrete034/color.jpg",
"normal": "/assets/materials/Concrete034/normal.jpg",
"roughness": "/assets/materials/Concrete034/roughness.jpg",
"displacement": "/assets/materials/Concrete034/displacement.jpg"
}
},
{
"id": "Concrete036",
"name": "Beton 8",
"category": "Concrete",
"maps": {
"color": "/assets/materials/Concrete036/color.jpg",
"normal": "/assets/materials/Concrete036/normal.jpg",
"roughness": "/assets/materials/Concrete036/roughness.jpg",
"displacement": "/assets/materials/Concrete036/displacement.jpg"
}
},
{
"id": "Concrete040",
"name": "Beton 9",
"category": "Concrete",
"maps": {
"color": "/assets/materials/Concrete040/color.jpg",
"normal": "/assets/materials/Concrete040/normal.jpg",
"roughness": "/assets/materials/Concrete040/roughness.jpg",
"displacement": "/assets/materials/Concrete040/displacement.jpg",
"ao": "/assets/materials/Concrete040/ao.jpg"
}
},
{
"id": "Concrete042A",
"name": "Beton 10",
"category": "Concrete",
"maps": {
"color": "/assets/materials/Concrete042A/color.jpg",
"normal": "/assets/materials/Concrete042A/normal.jpg",
"roughness": "/assets/materials/Concrete042A/roughness.jpg",
"displacement": "/assets/materials/Concrete042A/displacement.jpg",
"ao": "/assets/materials/Concrete042A/ao.jpg",
"metalness": "/assets/materials/Concrete042A/metalness.jpg"
}
},
{
"id": "Concrete044C",
"name": "Beton 11",
"category": "Concrete",
"maps": {
"color": "/assets/materials/Concrete044C/color.jpg",
"normal": "/assets/materials/Concrete044C/normal.jpg",
"roughness": "/assets/materials/Concrete044C/roughness.jpg",
"displacement": "/assets/materials/Concrete044C/displacement.jpg",
"ao": "/assets/materials/Concrete044C/ao.jpg",
"metalness": "/assets/materials/Concrete044C/metalness.jpg"
}
},
{
"id": "Concrete044D",
"name": "Beton 12",
"category": "Concrete",
"maps": {
"color": "/assets/materials/Concrete044D/color.jpg",
"normal": "/assets/materials/Concrete044D/normal.jpg",
"roughness": "/assets/materials/Concrete044D/roughness.jpg",
"displacement": "/assets/materials/Concrete044D/displacement.jpg",
"ao": "/assets/materials/Concrete044D/ao.jpg",
"metalness": "/assets/materials/Concrete044D/metalness.jpg"
}
},
{
"id": "Concrete045",
"name": "Beton 13",
"category": "Concrete",
"maps": {
"color": "/assets/materials/Concrete045/color.jpg",
"normal": "/assets/materials/Concrete045/normal.jpg",
"roughness": "/assets/materials/Concrete045/roughness.jpg",
"displacement": "/assets/materials/Concrete045/displacement.jpg",
"ao": "/assets/materials/Concrete045/ao.jpg"
}
},
{
"id": "Concrete046",
"name": "Beton 14",
"category": "Concrete",
"maps": {
"color": "/assets/materials/Concrete046/color.jpg",
"normal": "/assets/materials/Concrete046/normal.jpg",
"roughness": "/assets/materials/Concrete046/roughness.jpg",
"displacement": "/assets/materials/Concrete046/displacement.jpg"
}
},
{
"id": "Concrete047A",
"name": "Beton 15",
"category": "Concrete",
"maps": {
"color": "/assets/materials/Concrete047A/color.jpg",
"normal": "/assets/materials/Concrete047A/normal.jpg",
"roughness": "/assets/materials/Concrete047A/roughness.jpg",
"displacement": "/assets/materials/Concrete047A/displacement.jpg",
"ao": "/assets/materials/Concrete047A/ao.jpg"
}
},
{
"id": "Fabric061",
"name": "Stoff 1",
"category": "Fabric",
"maps": {
"color": "/assets/materials/Fabric061/color.jpg",
"normal": "/assets/materials/Fabric061/normal.jpg",
"roughness": "/assets/materials/Fabric061/roughness.jpg",
"displacement": "/assets/materials/Fabric061/displacement.jpg",
"ao": "/assets/materials/Fabric061/ao.jpg"
}
},
{
"id": "Fabric066",
"name": "Stoff 2",
"category": "Fabric",
"maps": {
"color": "/assets/materials/Fabric066/color.jpg",
"normal": "/assets/materials/Fabric066/normal.jpg",
"roughness": "/assets/materials/Fabric066/roughness.jpg",
"displacement": "/assets/materials/Fabric066/displacement.jpg",
"ao": "/assets/materials/Fabric066/ao.jpg"
}
},
{
"id": "Fabric081C",
"name": "Stoff 3",
"category": "Fabric",
"maps": {
"color": "/assets/materials/Fabric081C/color.jpg",
"normal": "/assets/materials/Fabric081C/normal.jpg",
"roughness": "/assets/materials/Fabric081C/roughness.jpg",
"displacement": "/assets/materials/Fabric081C/displacement.jpg",
"ao": "/assets/materials/Fabric081C/ao.jpg"
}
},
{
"id": "Gravel023",
"name": "Kies 1",
"category": "Gravel",
"maps": {
"color": "/assets/materials/Gravel023/color.jpg",
"normal": "/assets/materials/Gravel023/normal.jpg",
"roughness": "/assets/materials/Gravel023/roughness.jpg",
"displacement": "/assets/materials/Gravel023/displacement.jpg",
"ao": "/assets/materials/Gravel023/ao.jpg"
}
},
{
"id": "Gravel041",
"name": "Kies 2",
"category": "Gravel",
"maps": {
"color": "/assets/materials/Gravel041/color.jpg",
"normal": "/assets/materials/Gravel041/normal.jpg",
"roughness": "/assets/materials/Gravel041/roughness.jpg",
"displacement": "/assets/materials/Gravel041/displacement.jpg",
"ao": "/assets/materials/Gravel041/ao.jpg"
}
},
{
"id": "Ground037",
"name": "Boden 1",
"category": "Ground",
"maps": {
"color": "/assets/materials/Ground037/color.jpg",
"normal": "/assets/materials/Ground037/normal.jpg",
"roughness": "/assets/materials/Ground037/roughness.jpg",
"displacement": "/assets/materials/Ground037/displacement.jpg",
"ao": "/assets/materials/Ground037/ao.jpg"
}
},
{
"id": "Ground054",
"name": "Boden 2",
"category": "Ground",
"maps": {
"color": "/assets/materials/Ground054/color.jpg",
"normal": "/assets/materials/Ground054/normal.jpg",
"roughness": "/assets/materials/Ground054/roughness.jpg",
"displacement": "/assets/materials/Ground054/displacement.jpg",
"ao": "/assets/materials/Ground054/ao.jpg"
}
},
{
"id": "Marble016",
"name": "Marmor 1",
"category": "Marble",
"maps": {
"color": "/assets/materials/Marble016/color.jpg",
"normal": "/assets/materials/Marble016/normal.jpg",
"roughness": "/assets/materials/Marble016/roughness.jpg",
"displacement": "/assets/materials/Marble016/displacement.jpg"
}
},
{
"id": "Marble021",
"name": "Marmor 2",
"category": "Marble",
"maps": {
"color": "/assets/materials/Marble021/color.jpg",
"normal": "/assets/materials/Marble021/normal.jpg",
"roughness": "/assets/materials/Marble021/roughness.jpg",
"displacement": "/assets/materials/Marble021/displacement.jpg"
}
},
{
"id": "Metal049A",
"name": "Metall 1",
"category": "Metal",
"maps": {
"color": "/assets/materials/Metal049A/color.jpg",
"normal": "/assets/materials/Metal049A/normal.jpg",
"roughness": "/assets/materials/Metal049A/roughness.jpg",
"displacement": "/assets/materials/Metal049A/displacement.jpg",
"metalness": "/assets/materials/Metal049A/metalness.jpg"
}
},
{
"id": "Metal055A",
"name": "Metall 2",
"category": "Metal",
"maps": {
"color": "/assets/materials/Metal055A/color.jpg",
"normal": "/assets/materials/Metal055A/normal.jpg",
"roughness": "/assets/materials/Metal055A/roughness.jpg",
"displacement": "/assets/materials/Metal055A/displacement.jpg",
"metalness": "/assets/materials/Metal055A/metalness.jpg"
}
},
{
"id": "OfficeCeiling001",
"name": "Bürodecke 1",
"category": "Office Ceiling",
"maps": {
"color": "/assets/materials/OfficeCeiling001/color.jpg",
"normal": "/assets/materials/OfficeCeiling001/normal.jpg",
"roughness": "/assets/materials/OfficeCeiling001/roughness.jpg",
"displacement": "/assets/materials/OfficeCeiling001/displacement.jpg",
"ao": "/assets/materials/OfficeCeiling001/ao.jpg",
"metalness": "/assets/materials/OfficeCeiling001/metalness.jpg"
}
},
{
"id": "OfficeCeiling002",
"name": "Bürodecke 2",
"category": "Office Ceiling",
"maps": {
"color": "/assets/materials/OfficeCeiling002/color.jpg",
"normal": "/assets/materials/OfficeCeiling002/normal.jpg",
"roughness": "/assets/materials/OfficeCeiling002/roughness.jpg",
"displacement": "/assets/materials/OfficeCeiling002/displacement.jpg",
"ao": "/assets/materials/OfficeCeiling002/ao.jpg",
"metalness": "/assets/materials/OfficeCeiling002/metalness.jpg"
}
},
{
"id": "OfficeCeiling003",
"name": "Bürodecke 3",
"category": "Office Ceiling",
"maps": {
"color": "/assets/materials/OfficeCeiling003/color.jpg",
"normal": "/assets/materials/OfficeCeiling003/normal.jpg",
"roughness": "/assets/materials/OfficeCeiling003/roughness.jpg",
"displacement": "/assets/materials/OfficeCeiling003/displacement.jpg",
"ao": "/assets/materials/OfficeCeiling003/ao.jpg",
"metalness": "/assets/materials/OfficeCeiling003/metalness.jpg"
}
},
{
"id": "PavingStones128",
"name": "Pflasterstein 1",
"category": "Paving Stones",
"maps": {
"color": "/assets/materials/PavingStones128/color.jpg",
"normal": "/assets/materials/PavingStones128/normal.jpg",
"roughness": "/assets/materials/PavingStones128/roughness.jpg",
"displacement": "/assets/materials/PavingStones128/displacement.jpg",
"ao": "/assets/materials/PavingStones128/ao.jpg"
}
},
{
"id": "PavingStones138",
"name": "Pflasterstein 2",
"category": "Paving Stones",
"maps": {
"color": "/assets/materials/PavingStones138/color.jpg",
"normal": "/assets/materials/PavingStones138/normal.jpg",
"roughness": "/assets/materials/PavingStones138/roughness.jpg",
"displacement": "/assets/materials/PavingStones138/displacement.jpg",
"ao": "/assets/materials/PavingStones138/ao.jpg"
}
},
{
"id": "Plaster002",
"name": "Putz 1",
"category": "Plaster",
"maps": {
"color": "/assets/materials/Plaster002/color.jpg",
"normal": "/assets/materials/Plaster002/normal.jpg",
"roughness": "/assets/materials/Plaster002/roughness.jpg",
"displacement": "/assets/materials/Plaster002/displacement.jpg"
}
},
{
"id": "Plaster007",
"name": "Putz 2",
"category": "Plaster",
"maps": {
"color": "/assets/materials/Plaster007/color.jpg",
"normal": "/assets/materials/Plaster007/normal.jpg",
"roughness": "/assets/materials/Plaster007/roughness.jpg",
"displacement": "/assets/materials/Plaster007/displacement.jpg",
"ao": "/assets/materials/Plaster007/ao.jpg"
}
},
{
"id": "Rock058",
"name": "Naturstein 1",
"category": "Rock",
"maps": {
"color": "/assets/materials/Rock058/color.jpg",
"normal": "/assets/materials/Rock058/normal.jpg",
"roughness": "/assets/materials/Rock058/roughness.jpg",
"displacement": "/assets/materials/Rock058/displacement.jpg",
"ao": "/assets/materials/Rock058/ao.jpg"
}
},
{
"id": "Rock063",
"name": "Naturstein 2",
"category": "Rock",
"maps": {
"color": "/assets/materials/Rock063/color.jpg",
"normal": "/assets/materials/Rock063/normal.jpg",
"roughness": "/assets/materials/Rock063/roughness.jpg",
"displacement": "/assets/materials/Rock063/displacement.jpg",
"ao": "/assets/materials/Rock063/ao.jpg"
}
},
{
"id": "Rock064",
"name": "Naturstein 3",
"category": "Rock",
"maps": {
"color": "/assets/materials/Rock064/color.jpg",
"normal": "/assets/materials/Rock064/normal.jpg",
"roughness": "/assets/materials/Rock064/roughness.jpg",
"displacement": "/assets/materials/Rock064/displacement.jpg",
"ao": "/assets/materials/Rock064/ao.jpg"
}
},
{
"id": "RoofingTiles012A",
"name": "Dach 1",
"category": "Roofing Tiles",
"maps": {
"color": "/assets/materials/RoofingTiles012A/color.jpg",
"normal": "/assets/materials/RoofingTiles012A/normal.jpg",
"roughness": "/assets/materials/RoofingTiles012A/roughness.jpg",
"displacement": "/assets/materials/RoofingTiles012A/displacement.jpg",
"ao": "/assets/materials/RoofingTiles012A/ao.jpg"
}
},
{
"id": "RoofingTiles014A",
"name": "Dach 3",
"category": "Roofing Tiles",
"maps": {
"color": "/assets/materials/RoofingTiles014A/color.jpg",
"normal": "/assets/materials/RoofingTiles014A/normal.jpg",
"roughness": "/assets/materials/RoofingTiles014A/roughness.jpg",
"displacement": "/assets/materials/RoofingTiles014A/displacement.jpg",
"ao": "/assets/materials/RoofingTiles014A/ao.jpg"
}
},
{
"id": "Terrazzo005",
"name": "Terrazzo 1",
"category": "Terrazzo",
"maps": {
"color": "/assets/materials/Terrazzo005/color.jpg",
"normal": "/assets/materials/Terrazzo005/normal.jpg",
"roughness": "/assets/materials/Terrazzo005/roughness.jpg",
"displacement": "/assets/materials/Terrazzo005/displacement.jpg"
}
},
{
"id": "Terrazzo013",
"name": "Terrazzo 2",
"category": "Terrazzo",
"maps": {
"color": "/assets/materials/Terrazzo013/color.jpg",
"normal": "/assets/materials/Terrazzo013/normal.jpg",
"roughness": "/assets/materials/Terrazzo013/roughness.jpg",
"displacement": "/assets/materials/Terrazzo013/displacement.jpg"
}
},
{
"id": "Terrazzo018",
"name": "Terrazzo 3",
"category": "Terrazzo",
"maps": {
"color": "/assets/materials/Terrazzo018/color.jpg",
"normal": "/assets/materials/Terrazzo018/normal.jpg",
"roughness": "/assets/materials/Terrazzo018/roughness.jpg",
"displacement": "/assets/materials/Terrazzo018/displacement.jpg"
}
},
{
"id": "Tiles139",
"name": "Fliesen 1",
"category": "Tiles",
"maps": {
"color": "/assets/materials/Tiles139/color.jpg",
"normal": "/assets/materials/Tiles139/normal.jpg",
"roughness": "/assets/materials/Tiles139/roughness.jpg",
"displacement": "/assets/materials/Tiles139/displacement.jpg"
}
},
{
"id": "Tiles140",
"name": "Fliesen 2",
"category": "Tiles",
"maps": {
"color": "/assets/materials/Tiles140/color.jpg",
"normal": "/assets/materials/Tiles140/normal.jpg",
"roughness": "/assets/materials/Tiles140/roughness.jpg",
"displacement": "/assets/materials/Tiles140/displacement.jpg"
}
},
{
"id": "Wood049",
"name": "Holz 1",
"category": "Wood",
"maps": {
"color": "/assets/materials/Wood049/color.jpg",
"normal": "/assets/materials/Wood049/normal.jpg",
"roughness": "/assets/materials/Wood049/roughness.jpg",
"displacement": "/assets/materials/Wood049/displacement.jpg"
}
},
{
"id": "Wood051",
"name": "Holz 2",
"category": "Wood",
"maps": {
"color": "/assets/materials/Wood051/color.jpg",
"normal": "/assets/materials/Wood051/normal.jpg",
"roughness": "/assets/materials/Wood051/roughness.jpg",
"displacement": "/assets/materials/Wood051/displacement.jpg"
}
},
{
"id": "Wood066",
"name": "Holz 3",
"category": "Wood",
"maps": {
"color": "/assets/materials/Wood066/color.jpg",
"normal": "/assets/materials/Wood066/normal.jpg",
"roughness": "/assets/materials/Wood066/roughness.jpg",
"displacement": "/assets/materials/Wood066/displacement.jpg"
}
},
{
"id": "Wood092",
"name": "Holz 4",
"category": "Wood",
"maps": {
"color": "/assets/materials/Wood092/color.jpg",
"normal": "/assets/materials/Wood092/normal.jpg",
"roughness": "/assets/materials/Wood092/roughness.jpg",
"displacement": "/assets/materials/Wood092/displacement.jpg"
}
},
{
"id": "Wood094",
"name": "Holz 5",
"category": "Wood",
"maps": {
"color": "/assets/materials/Wood094/color.jpg",
"normal": "/assets/materials/Wood094/normal.jpg",
"roughness": "/assets/materials/Wood094/roughness.jpg",
"displacement": "/assets/materials/Wood094/displacement.jpg"
}
},
{
"id": "WoodFloor064",
"name": "Holzboden 1",
"category": "Wood Floor",
"maps": {
"color": "/assets/materials/WoodFloor064/color.jpg",
"normal": "/assets/materials/WoodFloor064/normal.jpg",
"roughness": "/assets/materials/WoodFloor064/roughness.jpg",
"displacement": "/assets/materials/WoodFloor064/displacement.jpg"
}
},
{
"id": "WoodFloor070",
"name": "Holzboden 2",
"category": "Wood Floor",
"maps": {
"color": "/assets/materials/WoodFloor070/color.jpg",
"normal": "/assets/materials/WoodFloor070/normal.jpg",
"roughness": "/assets/materials/WoodFloor070/roughness.jpg",
"displacement": "/assets/materials/WoodFloor070/displacement.jpg"
}
} }
] ]
} }
-297
View File
@@ -1,297 +0,0 @@
#!/usr/bin/env node
/**
* fetch-materials.mjs — Copies mirrored materials from DOSSIER-LIBRARY into
* public/assets/materials/ and regenerates src/materials/library.ts and
* public/assets/materials/manifest.json.
*
* The 12 hand-authored starter entries in library.ts / manifest.json are
* preserved (they appear first). Broad-set entries are appended/merged
* (existing broad entries are replaced if the source has changed).
*
* Usage:
* node scripts/fetch-materials.mjs --library /home/karim/DOSSIER-LIBRARY
* node scripts/fetch-materials.mjs --library ../DOSSIER-LIBRARY --dry-run
*
* The --library flag (or DOSSIER_LIBRARY env var) points to the local mirror
* root. When a git remote is set up, pass a local clone path here.
*/
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { readdir } from 'fs/promises';
import { dirname, join, resolve } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const CAD_ROOT = join(__dirname, '..');
// ── CLI ───────────────────────────────────────────────────────────────────────
function parseArgs() {
const args = process.argv.slice(2);
let libraryPath = process.env.DOSSIER_LIBRARY ?? null;
let dryRun = false;
for (let i = 0; i < args.length; i++) {
if ((args[i] === '--library' || args[i] === '-l') && args[i + 1]) {
libraryPath = args[++i];
} else if (args[i] === '--dry-run') {
dryRun = true;
}
}
if (!libraryPath) {
console.error('Error: --library <path> is required (or set DOSSIER_LIBRARY env var)');
process.exit(1);
}
return { libraryPath: resolve(libraryPath), dryRun };
}
// ── German category labels ────────────────────────────────────────────────────
const CATEGORY_GERMAN = {
Bricks: 'Backstein',
Concrete: 'Beton',
Wood: 'Holz',
WoodFloor: 'Holzboden',
Plaster: 'Putz',
Metal: 'Metall',
Marble: 'Marmor',
Rock: 'Naturstein',
PavingStones: 'Pflasterstein',
Tiles: 'Fliesen',
Ground: 'Boden',
Asphalt: 'Asphalt',
Roof: 'Dach',
Roofing: 'Dach',
Plywood: 'Sperrholz',
Fabric: 'Stoff',
Gravel: 'Kies',
Terrazzo: 'Terrazzo',
OfficeCeiling: 'Bürodecke',
Grass: 'Gras',
// ambientCG displayCategory strings (as stored in index.json) — spaced /
// substituted names that differ from the API category keys above.
'Wood Floor': 'Holzboden',
'Paving Stones': 'Pflasterstein',
'Roofing Tiles': 'Dach',
'Roofing': 'Dach',
'Chipboard': 'Sperrholz',
'Office Ceiling': 'Bürodecke',
};
/** German label with a numeric suffix to disambiguate within the same category. */
function germanLabel(category, indexWithinCategory) {
const base = CATEGORY_GERMAN[category] ?? category;
return `${base} ${indexWithinCategory + 1}`;
}
// ── Starter material IDs (must never be replaced/removed) ─────────────────────
const STARTER_IDS = new Set([
'Concrete048', 'Plaster001', 'Wood095', 'WoodFloor051',
'Bricks104', 'Tiles141', 'Marble012', 'PavingStones150',
'Metal063', 'Gravel043', 'Grass005', 'Ground103',
]);
// ── Map files ─────────────────────────────────────────────────────────────────
const CANONICAL_MAPS = ['color', 'normal', 'roughness', 'displacement', 'ao', 'metalness'];
// ── Main ──────────────────────────────────────────────────────────────────────
async function main() {
const { libraryPath, dryRun } = parseArgs();
const ambientcgDir = join(libraryPath, 'ambientcg');
const indexPath = join(ambientcgDir, 'index.json');
if (!existsSync(indexPath)) {
console.error(`No index.json found at: ${indexPath}`);
console.error('Run scripts/mirror-ambientcg.mjs in DOSSIER-LIBRARY first.');
process.exit(1);
}
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
const mirroredAssets = index.assets ?? [];
console.log(`DOSSIER-LIBRARY: ${mirroredAssets.length} assets in index`);
if (dryRun) console.log('(dry-run mode — no files written)');
// ── Copy texture files ────────────────────────────────────────────────────
const destBase = join(CAD_ROOT, 'public', 'assets', 'materials');
let copied = 0;
let skipped = 0;
const broadEntries = []; // { assetId, category, displayName, maps }
// Track German label counters per category
const categoryCounters = {};
for (const asset of mirroredAssets) {
const { assetId, category, displayName, maps: mapKinds } = asset;
// Never overwrite starter materials
if (STARTER_IDS.has(assetId)) {
skipped++;
continue;
}
const srcDir = join(ambientcgDir, assetId);
const dstDir = join(destBase, assetId);
if (!existsSync(srcDir)) {
console.warn(` [warn] Source missing: ${srcDir}`);
continue;
}
if (!dryRun) {
mkdirSync(dstDir, { recursive: true });
}
const resolvedMaps = {};
for (const kind of CANONICAL_MAPS) {
const srcFile = join(srcDir, `${kind}.jpg`);
const dstFile = join(dstDir, `${kind}.jpg`);
if (existsSync(srcFile)) {
if (!dryRun) {
copyFileSync(srcFile, dstFile);
}
resolvedMaps[kind] = `/assets/materials/${assetId}/${kind}.jpg`;
copied++;
}
}
if (Object.keys(resolvedMaps).length === 0) {
console.warn(` [warn] No maps found for ${assetId}`);
continue;
}
// Build German label
const catKey = category ?? 'Other';
categoryCounters[catKey] = (categoryCounters[catKey] ?? 0);
const label = germanLabel(catKey, categoryCounters[catKey]);
categoryCounters[catKey]++;
broadEntries.push({
id: assetId,
name: label,
category: catKey,
maps: resolvedMaps,
});
}
console.log(`Copied ${copied} map files; ${skipped} starter materials unchanged`);
console.log(`Broad set: ${broadEntries.length} materials`);
// ── Regenerate library.ts ─────────────────────────────────────────────────
const libraryPath_ = join(CAD_ROOT, 'src', 'materials', 'library.ts');
const existingLibrary = readFileSync(libraryPath_, 'utf8');
// Extract starter entries from the existing file (everything in MATERIAL_LIBRARY[])
// We re-use the static text of the 12 starter entries verbatim.
const starterEntriesMatch = existingLibrary.match(
/export const MATERIAL_LIBRARY: MaterialAsset\[\] = \[([\s\S]*?)\];\s*\n\nexport default/
);
let starterBlock = '';
if (starterEntriesMatch) {
// Keep the existing starter block exactly as-is
starterBlock = starterEntriesMatch[1];
}
// Build broad entries as TS object literals
function mapsToTs(maps, indent) {
const lines = Object.entries(maps).map(
([k, v]) => `${indent} ${k}: "${v}",`
);
return `{\n${lines.join('\n')}\n${indent}}`;
}
const broadTs = broadEntries.map(e => {
return ` {
id: "${e.id}",
name: "${e.name}",
category: "${e.category}",
maps: ${mapsToTs(e.maps, ' ')},
}`;
}).join(',\n');
const separator = broadEntries.length > 0
? `\n\n // ── Broad set (ambientCG 2K — populated by scripts/fetch-materials.mjs) ──\n // Run "node scripts/fetch-materials.mjs --library <DOSSIER-LIBRARY-path>" to refresh.\n`
: '';
const newLibrary = `// Auto-generated built-in material library (ambientCG, CC0 1.0).
// Static asset paths under /public; loadable directly via three.js TextureLoader.
// Source data mirrors public/assets/materials/manifest.json.
// Broad set populated by: node scripts/fetch-materials.mjs --library <DOSSIER-LIBRARY-path>
export type MaterialMapKind =
| 'color'
| 'normal'
| 'roughness'
| 'metalness'
| 'displacement'
| 'ao';
export interface MaterialAsset {
/** ambientCG asset id, e.g. 'Concrete048'. */
id: string;
/** Localized display name (German). */
name: string;
/** ambientCG category key, e.g. 'Concrete'. */
category: string;
/** Absolute public paths to the available texture maps. */
maps: Partial<Record<MaterialMapKind, string>>;
}
export const MATERIAL_LIBRARY_SOURCE = "ambientCG (ambientcg.com)";
export const MATERIAL_LIBRARY_LICENSE = "CC0 1.0 Universal (Public Domain)";
export const MATERIAL_LIBRARY_RESOLUTION = "2K";
export const MATERIAL_LIBRARY: MaterialAsset[] = [${starterBlock}${separator}${broadEntries.length > 0 ? broadTs + ',\n' : ''}];
export default MATERIAL_LIBRARY;
`;
if (!dryRun) {
writeFileSync(libraryPath_, newLibrary, 'utf8');
console.log(`Written: ${libraryPath_}`);
} else {
console.log(`[dry-run] Would write: ${libraryPath_}`);
}
// ── Regenerate manifest.json ──────────────────────────────────────────────
const manifestPath = join(destBase, 'manifest.json');
const existingManifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
// Keep starter entries from existing manifest
const starterManifestEntries = existingManifest.materials.filter(
m => STARTER_IDS.has(m.id)
);
const broadManifestEntries = broadEntries.map(e => ({
id: e.id,
name: e.name,
category: e.category,
maps: e.maps,
}));
const newManifest = {
source: 'ambientCG (ambientcg.com)',
license: 'CC0 1.0 Universal (Public Domain)',
resolution: '2K',
materials: [...starterManifestEntries, ...broadManifestEntries],
};
if (!dryRun) {
writeFileSync(manifestPath, JSON.stringify(newManifest, null, 2) + '\n', 'utf8');
console.log(`Written: ${manifestPath}`);
} else {
console.log(`[dry-run] Would write: ${manifestPath}`);
}
console.log(`\nTotal in MATERIAL_LIBRARY: ${starterManifestEntries.length} starters + ${broadEntries.length} broad = ${starterManifestEntries.length + broadEntries.length}`);
console.log('\nDone. Next: npx tsc -b && npm run build');
}
main().catch(err => {
console.error('Fatal:', err);
process.exit(1);
});
+36 -1269
View File
File diff suppressed because it is too large Load Diff
+5 -890
View File
@@ -1,7 +1,9 @@
// Auto-generated built-in material library (ambientCG, CC0 1.0). // Eingebaute Material-Bibliothek (ambientCG, CC0 1.0) — 13 fest gebündelte
// Static asset paths under /public; loadable directly via three.js TextureLoader. // Starter-Materialien (Bilddateien liegen unter public/assets/materials/).
// Alles darüber hinaus läuft über die Live-Suche (src/materials/ambientcg.ts,
// Tab "Bibliothek durchsuchen"), die die komplette ambientCG-Bibliothek
// online durchsucht statt Texturen zu bündeln.
// Source data mirrors public/assets/materials/manifest.json. // Source data mirrors public/assets/materials/manifest.json.
// Broad set populated by: node scripts/fetch-materials.mjs --library <DOSSIER-LIBRARY-path>
export type MaterialMapKind = export type MaterialMapKind =
| 'color' | 'color'
@@ -179,893 +181,6 @@ export const MATERIAL_LIBRARY: MaterialAsset[] = [
ao: "/assets/materials/Ground103/ao.jpg", ao: "/assets/materials/Ground103/ao.jpg",
}, },
}, },
// ── Broad set (ambientCG 2K — populated by scripts/fetch-materials.mjs) ──
// Run "node scripts/fetch-materials.mjs --library <DOSSIER-LIBRARY-path>" to refresh.
{
id: "Asphalt023S",
name: "Asphalt 1",
category: "Asphalt",
maps: {
color: "/assets/materials/Asphalt023S/color.jpg",
normal: "/assets/materials/Asphalt023S/normal.jpg",
roughness: "/assets/materials/Asphalt023S/roughness.jpg",
displacement: "/assets/materials/Asphalt023S/displacement.jpg",
ao: "/assets/materials/Asphalt023S/ao.jpg",
},
},
{
id: "Asphalt025C",
name: "Asphalt 2",
category: "Asphalt",
maps: {
color: "/assets/materials/Asphalt025C/color.jpg",
normal: "/assets/materials/Asphalt025C/normal.jpg",
roughness: "/assets/materials/Asphalt025C/roughness.jpg",
displacement: "/assets/materials/Asphalt025C/displacement.jpg",
ao: "/assets/materials/Asphalt025C/ao.jpg",
},
},
{
id: "Asphalt031",
name: "Asphalt 3",
category: "Asphalt",
maps: {
color: "/assets/materials/Asphalt031/color.jpg",
normal: "/assets/materials/Asphalt031/normal.jpg",
roughness: "/assets/materials/Asphalt031/roughness.jpg",
displacement: "/assets/materials/Asphalt031/displacement.jpg",
ao: "/assets/materials/Asphalt031/ao.jpg",
},
},
{
id: "Bricks051",
name: "Backstein 1",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks051/color.jpg",
normal: "/assets/materials/Bricks051/normal.jpg",
roughness: "/assets/materials/Bricks051/roughness.jpg",
displacement: "/assets/materials/Bricks051/displacement.jpg",
},
},
{
id: "Bricks059",
name: "Backstein 2",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks059/color.jpg",
normal: "/assets/materials/Bricks059/normal.jpg",
roughness: "/assets/materials/Bricks059/roughness.jpg",
displacement: "/assets/materials/Bricks059/displacement.jpg",
ao: "/assets/materials/Bricks059/ao.jpg",
},
},
{
id: "Bricks060",
name: "Backstein 3",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks060/color.jpg",
normal: "/assets/materials/Bricks060/normal.jpg",
roughness: "/assets/materials/Bricks060/roughness.jpg",
displacement: "/assets/materials/Bricks060/displacement.jpg",
ao: "/assets/materials/Bricks060/ao.jpg",
},
},
{
id: "Bricks066",
name: "Backstein 4",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks066/color.jpg",
normal: "/assets/materials/Bricks066/normal.jpg",
roughness: "/assets/materials/Bricks066/roughness.jpg",
displacement: "/assets/materials/Bricks066/displacement.jpg",
ao: "/assets/materials/Bricks066/ao.jpg",
},
},
{
id: "Bricks074",
name: "Backstein 5",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks074/color.jpg",
normal: "/assets/materials/Bricks074/normal.jpg",
roughness: "/assets/materials/Bricks074/roughness.jpg",
displacement: "/assets/materials/Bricks074/displacement.jpg",
ao: "/assets/materials/Bricks074/ao.jpg",
},
},
{
id: "Bricks075A",
name: "Backstein 6",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks075A/color.jpg",
normal: "/assets/materials/Bricks075A/normal.jpg",
roughness: "/assets/materials/Bricks075A/roughness.jpg",
displacement: "/assets/materials/Bricks075A/displacement.jpg",
ao: "/assets/materials/Bricks075A/ao.jpg",
},
},
{
id: "Bricks084",
name: "Backstein 7",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks084/color.jpg",
normal: "/assets/materials/Bricks084/normal.jpg",
roughness: "/assets/materials/Bricks084/roughness.jpg",
displacement: "/assets/materials/Bricks084/displacement.jpg",
ao: "/assets/materials/Bricks084/ao.jpg",
},
},
{
id: "Bricks085",
name: "Backstein 8",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks085/color.jpg",
normal: "/assets/materials/Bricks085/normal.jpg",
roughness: "/assets/materials/Bricks085/roughness.jpg",
displacement: "/assets/materials/Bricks085/displacement.jpg",
ao: "/assets/materials/Bricks085/ao.jpg",
},
},
{
id: "Bricks089",
name: "Backstein 9",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks089/color.jpg",
normal: "/assets/materials/Bricks089/normal.jpg",
roughness: "/assets/materials/Bricks089/roughness.jpg",
displacement: "/assets/materials/Bricks089/displacement.jpg",
ao: "/assets/materials/Bricks089/ao.jpg",
},
},
{
id: "Bricks090",
name: "Backstein 10",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks090/color.jpg",
normal: "/assets/materials/Bricks090/normal.jpg",
roughness: "/assets/materials/Bricks090/roughness.jpg",
displacement: "/assets/materials/Bricks090/displacement.jpg",
ao: "/assets/materials/Bricks090/ao.jpg",
},
},
{
id: "Bricks091",
name: "Backstein 11",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks091/color.jpg",
normal: "/assets/materials/Bricks091/normal.jpg",
roughness: "/assets/materials/Bricks091/roughness.jpg",
displacement: "/assets/materials/Bricks091/displacement.jpg",
ao: "/assets/materials/Bricks091/ao.jpg",
},
},
{
id: "Bricks092",
name: "Backstein 12",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks092/color.jpg",
normal: "/assets/materials/Bricks092/normal.jpg",
roughness: "/assets/materials/Bricks092/roughness.jpg",
displacement: "/assets/materials/Bricks092/displacement.jpg",
ao: "/assets/materials/Bricks092/ao.jpg",
},
},
{
id: "Bricks093",
name: "Backstein 13",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks093/color.jpg",
normal: "/assets/materials/Bricks093/normal.jpg",
roughness: "/assets/materials/Bricks093/roughness.jpg",
displacement: "/assets/materials/Bricks093/displacement.jpg",
ao: "/assets/materials/Bricks093/ao.jpg",
},
},
{
id: "Bricks094",
name: "Backstein 14",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks094/color.jpg",
normal: "/assets/materials/Bricks094/normal.jpg",
roughness: "/assets/materials/Bricks094/roughness.jpg",
displacement: "/assets/materials/Bricks094/displacement.jpg",
ao: "/assets/materials/Bricks094/ao.jpg",
},
},
{
id: "Bricks097",
name: "Backstein 15",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks097/color.jpg",
normal: "/assets/materials/Bricks097/normal.jpg",
roughness: "/assets/materials/Bricks097/roughness.jpg",
displacement: "/assets/materials/Bricks097/displacement.jpg",
ao: "/assets/materials/Bricks097/ao.jpg",
},
},
{
id: "Bricks100",
name: "Backstein 16",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks100/color.jpg",
normal: "/assets/materials/Bricks100/normal.jpg",
roughness: "/assets/materials/Bricks100/roughness.jpg",
displacement: "/assets/materials/Bricks100/displacement.jpg",
ao: "/assets/materials/Bricks100/ao.jpg",
},
},
{
id: "Bricks101",
name: "Backstein 17",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks101/color.jpg",
normal: "/assets/materials/Bricks101/normal.jpg",
roughness: "/assets/materials/Bricks101/roughness.jpg",
displacement: "/assets/materials/Bricks101/displacement.jpg",
ao: "/assets/materials/Bricks101/ao.jpg",
},
},
{
id: "Bricks102",
name: "Backstein 18",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks102/color.jpg",
normal: "/assets/materials/Bricks102/normal.jpg",
roughness: "/assets/materials/Bricks102/roughness.jpg",
displacement: "/assets/materials/Bricks102/displacement.jpg",
ao: "/assets/materials/Bricks102/ao.jpg",
},
},
{
id: "Bricks103",
name: "Backstein 19",
category: "Bricks",
maps: {
color: "/assets/materials/Bricks103/color.jpg",
normal: "/assets/materials/Bricks103/normal.jpg",
roughness: "/assets/materials/Bricks103/roughness.jpg",
displacement: "/assets/materials/Bricks103/displacement.jpg",
ao: "/assets/materials/Bricks103/ao.jpg",
},
},
{
id: "Chipboard004",
name: "Sperrholz 1",
category: "Chipboard",
maps: {
color: "/assets/materials/Chipboard004/color.jpg",
normal: "/assets/materials/Chipboard004/normal.jpg",
roughness: "/assets/materials/Chipboard004/roughness.jpg",
displacement: "/assets/materials/Chipboard004/displacement.jpg",
},
},
{
id: "Chipboard005",
name: "Sperrholz 2",
category: "Chipboard",
maps: {
color: "/assets/materials/Chipboard005/color.jpg",
normal: "/assets/materials/Chipboard005/normal.jpg",
roughness: "/assets/materials/Chipboard005/roughness.jpg",
displacement: "/assets/materials/Chipboard005/displacement.jpg",
},
},
{
id: "Chipboard007",
name: "Sperrholz 3",
category: "Chipboard",
maps: {
color: "/assets/materials/Chipboard007/color.jpg",
normal: "/assets/materials/Chipboard007/normal.jpg",
roughness: "/assets/materials/Chipboard007/roughness.jpg",
displacement: "/assets/materials/Chipboard007/displacement.jpg",
},
},
{
id: "Concrete024",
name: "Beton 1",
category: "Concrete",
maps: {
color: "/assets/materials/Concrete024/color.jpg",
normal: "/assets/materials/Concrete024/normal.jpg",
roughness: "/assets/materials/Concrete024/roughness.jpg",
displacement: "/assets/materials/Concrete024/displacement.jpg",
},
},
{
id: "Concrete025",
name: "Beton 2",
category: "Concrete",
maps: {
color: "/assets/materials/Concrete025/color.jpg",
normal: "/assets/materials/Concrete025/normal.jpg",
roughness: "/assets/materials/Concrete025/roughness.jpg",
displacement: "/assets/materials/Concrete025/displacement.jpg",
ao: "/assets/materials/Concrete025/ao.jpg",
},
},
{
id: "Concrete030",
name: "Beton 3",
category: "Concrete",
maps: {
color: "/assets/materials/Concrete030/color.jpg",
normal: "/assets/materials/Concrete030/normal.jpg",
roughness: "/assets/materials/Concrete030/roughness.jpg",
displacement: "/assets/materials/Concrete030/displacement.jpg",
ao: "/assets/materials/Concrete030/ao.jpg",
},
},
{
id: "Concrete031",
name: "Beton 4",
category: "Concrete",
maps: {
color: "/assets/materials/Concrete031/color.jpg",
normal: "/assets/materials/Concrete031/normal.jpg",
roughness: "/assets/materials/Concrete031/roughness.jpg",
displacement: "/assets/materials/Concrete031/displacement.jpg",
ao: "/assets/materials/Concrete031/ao.jpg",
},
},
{
id: "Concrete032",
name: "Beton 5",
category: "Concrete",
maps: {
color: "/assets/materials/Concrete032/color.jpg",
normal: "/assets/materials/Concrete032/normal.jpg",
roughness: "/assets/materials/Concrete032/roughness.jpg",
displacement: "/assets/materials/Concrete032/displacement.jpg",
ao: "/assets/materials/Concrete032/ao.jpg",
},
},
{
id: "Concrete033",
name: "Beton 6",
category: "Concrete",
maps: {
color: "/assets/materials/Concrete033/color.jpg",
normal: "/assets/materials/Concrete033/normal.jpg",
roughness: "/assets/materials/Concrete033/roughness.jpg",
displacement: "/assets/materials/Concrete033/displacement.jpg",
ao: "/assets/materials/Concrete033/ao.jpg",
},
},
{
id: "Concrete034",
name: "Beton 7",
category: "Concrete",
maps: {
color: "/assets/materials/Concrete034/color.jpg",
normal: "/assets/materials/Concrete034/normal.jpg",
roughness: "/assets/materials/Concrete034/roughness.jpg",
displacement: "/assets/materials/Concrete034/displacement.jpg",
},
},
{
id: "Concrete036",
name: "Beton 8",
category: "Concrete",
maps: {
color: "/assets/materials/Concrete036/color.jpg",
normal: "/assets/materials/Concrete036/normal.jpg",
roughness: "/assets/materials/Concrete036/roughness.jpg",
displacement: "/assets/materials/Concrete036/displacement.jpg",
},
},
{
id: "Concrete040",
name: "Beton 9",
category: "Concrete",
maps: {
color: "/assets/materials/Concrete040/color.jpg",
normal: "/assets/materials/Concrete040/normal.jpg",
roughness: "/assets/materials/Concrete040/roughness.jpg",
displacement: "/assets/materials/Concrete040/displacement.jpg",
ao: "/assets/materials/Concrete040/ao.jpg",
},
},
{
id: "Concrete042A",
name: "Beton 10",
category: "Concrete",
maps: {
color: "/assets/materials/Concrete042A/color.jpg",
normal: "/assets/materials/Concrete042A/normal.jpg",
roughness: "/assets/materials/Concrete042A/roughness.jpg",
displacement: "/assets/materials/Concrete042A/displacement.jpg",
ao: "/assets/materials/Concrete042A/ao.jpg",
metalness: "/assets/materials/Concrete042A/metalness.jpg",
},
},
{
id: "Concrete044C",
name: "Beton 11",
category: "Concrete",
maps: {
color: "/assets/materials/Concrete044C/color.jpg",
normal: "/assets/materials/Concrete044C/normal.jpg",
roughness: "/assets/materials/Concrete044C/roughness.jpg",
displacement: "/assets/materials/Concrete044C/displacement.jpg",
ao: "/assets/materials/Concrete044C/ao.jpg",
metalness: "/assets/materials/Concrete044C/metalness.jpg",
},
},
{
id: "Concrete044D",
name: "Beton 12",
category: "Concrete",
maps: {
color: "/assets/materials/Concrete044D/color.jpg",
normal: "/assets/materials/Concrete044D/normal.jpg",
roughness: "/assets/materials/Concrete044D/roughness.jpg",
displacement: "/assets/materials/Concrete044D/displacement.jpg",
ao: "/assets/materials/Concrete044D/ao.jpg",
metalness: "/assets/materials/Concrete044D/metalness.jpg",
},
},
{
id: "Concrete045",
name: "Beton 13",
category: "Concrete",
maps: {
color: "/assets/materials/Concrete045/color.jpg",
normal: "/assets/materials/Concrete045/normal.jpg",
roughness: "/assets/materials/Concrete045/roughness.jpg",
displacement: "/assets/materials/Concrete045/displacement.jpg",
ao: "/assets/materials/Concrete045/ao.jpg",
},
},
{
id: "Concrete046",
name: "Beton 14",
category: "Concrete",
maps: {
color: "/assets/materials/Concrete046/color.jpg",
normal: "/assets/materials/Concrete046/normal.jpg",
roughness: "/assets/materials/Concrete046/roughness.jpg",
displacement: "/assets/materials/Concrete046/displacement.jpg",
},
},
{
id: "Concrete047A",
name: "Beton 15",
category: "Concrete",
maps: {
color: "/assets/materials/Concrete047A/color.jpg",
normal: "/assets/materials/Concrete047A/normal.jpg",
roughness: "/assets/materials/Concrete047A/roughness.jpg",
displacement: "/assets/materials/Concrete047A/displacement.jpg",
ao: "/assets/materials/Concrete047A/ao.jpg",
},
},
{
id: "Fabric061",
name: "Stoff 1",
category: "Fabric",
maps: {
color: "/assets/materials/Fabric061/color.jpg",
normal: "/assets/materials/Fabric061/normal.jpg",
roughness: "/assets/materials/Fabric061/roughness.jpg",
displacement: "/assets/materials/Fabric061/displacement.jpg",
ao: "/assets/materials/Fabric061/ao.jpg",
},
},
{
id: "Fabric066",
name: "Stoff 2",
category: "Fabric",
maps: {
color: "/assets/materials/Fabric066/color.jpg",
normal: "/assets/materials/Fabric066/normal.jpg",
roughness: "/assets/materials/Fabric066/roughness.jpg",
displacement: "/assets/materials/Fabric066/displacement.jpg",
ao: "/assets/materials/Fabric066/ao.jpg",
},
},
{
id: "Fabric081C",
name: "Stoff 3",
category: "Fabric",
maps: {
color: "/assets/materials/Fabric081C/color.jpg",
normal: "/assets/materials/Fabric081C/normal.jpg",
roughness: "/assets/materials/Fabric081C/roughness.jpg",
displacement: "/assets/materials/Fabric081C/displacement.jpg",
ao: "/assets/materials/Fabric081C/ao.jpg",
},
},
{
id: "Gravel023",
name: "Kies 1",
category: "Gravel",
maps: {
color: "/assets/materials/Gravel023/color.jpg",
normal: "/assets/materials/Gravel023/normal.jpg",
roughness: "/assets/materials/Gravel023/roughness.jpg",
displacement: "/assets/materials/Gravel023/displacement.jpg",
ao: "/assets/materials/Gravel023/ao.jpg",
},
},
{
id: "Gravel041",
name: "Kies 2",
category: "Gravel",
maps: {
color: "/assets/materials/Gravel041/color.jpg",
normal: "/assets/materials/Gravel041/normal.jpg",
roughness: "/assets/materials/Gravel041/roughness.jpg",
displacement: "/assets/materials/Gravel041/displacement.jpg",
ao: "/assets/materials/Gravel041/ao.jpg",
},
},
{
id: "Ground037",
name: "Boden 1",
category: "Ground",
maps: {
color: "/assets/materials/Ground037/color.jpg",
normal: "/assets/materials/Ground037/normal.jpg",
roughness: "/assets/materials/Ground037/roughness.jpg",
displacement: "/assets/materials/Ground037/displacement.jpg",
ao: "/assets/materials/Ground037/ao.jpg",
},
},
{
id: "Ground054",
name: "Boden 2",
category: "Ground",
maps: {
color: "/assets/materials/Ground054/color.jpg",
normal: "/assets/materials/Ground054/normal.jpg",
roughness: "/assets/materials/Ground054/roughness.jpg",
displacement: "/assets/materials/Ground054/displacement.jpg",
ao: "/assets/materials/Ground054/ao.jpg",
},
},
{
id: "Marble016",
name: "Marmor 1",
category: "Marble",
maps: {
color: "/assets/materials/Marble016/color.jpg",
normal: "/assets/materials/Marble016/normal.jpg",
roughness: "/assets/materials/Marble016/roughness.jpg",
displacement: "/assets/materials/Marble016/displacement.jpg",
},
},
{
id: "Marble021",
name: "Marmor 2",
category: "Marble",
maps: {
color: "/assets/materials/Marble021/color.jpg",
normal: "/assets/materials/Marble021/normal.jpg",
roughness: "/assets/materials/Marble021/roughness.jpg",
displacement: "/assets/materials/Marble021/displacement.jpg",
},
},
{
id: "Metal049A",
name: "Metall 1",
category: "Metal",
maps: {
color: "/assets/materials/Metal049A/color.jpg",
normal: "/assets/materials/Metal049A/normal.jpg",
roughness: "/assets/materials/Metal049A/roughness.jpg",
displacement: "/assets/materials/Metal049A/displacement.jpg",
metalness: "/assets/materials/Metal049A/metalness.jpg",
},
},
{
id: "Metal055A",
name: "Metall 2",
category: "Metal",
maps: {
color: "/assets/materials/Metal055A/color.jpg",
normal: "/assets/materials/Metal055A/normal.jpg",
roughness: "/assets/materials/Metal055A/roughness.jpg",
displacement: "/assets/materials/Metal055A/displacement.jpg",
metalness: "/assets/materials/Metal055A/metalness.jpg",
},
},
{
id: "OfficeCeiling001",
name: "Bürodecke 1",
category: "Office Ceiling",
maps: {
color: "/assets/materials/OfficeCeiling001/color.jpg",
normal: "/assets/materials/OfficeCeiling001/normal.jpg",
roughness: "/assets/materials/OfficeCeiling001/roughness.jpg",
displacement: "/assets/materials/OfficeCeiling001/displacement.jpg",
ao: "/assets/materials/OfficeCeiling001/ao.jpg",
metalness: "/assets/materials/OfficeCeiling001/metalness.jpg",
},
},
{
id: "OfficeCeiling002",
name: "Bürodecke 2",
category: "Office Ceiling",
maps: {
color: "/assets/materials/OfficeCeiling002/color.jpg",
normal: "/assets/materials/OfficeCeiling002/normal.jpg",
roughness: "/assets/materials/OfficeCeiling002/roughness.jpg",
displacement: "/assets/materials/OfficeCeiling002/displacement.jpg",
ao: "/assets/materials/OfficeCeiling002/ao.jpg",
metalness: "/assets/materials/OfficeCeiling002/metalness.jpg",
},
},
{
id: "OfficeCeiling003",
name: "Bürodecke 3",
category: "Office Ceiling",
maps: {
color: "/assets/materials/OfficeCeiling003/color.jpg",
normal: "/assets/materials/OfficeCeiling003/normal.jpg",
roughness: "/assets/materials/OfficeCeiling003/roughness.jpg",
displacement: "/assets/materials/OfficeCeiling003/displacement.jpg",
ao: "/assets/materials/OfficeCeiling003/ao.jpg",
metalness: "/assets/materials/OfficeCeiling003/metalness.jpg",
},
},
{
id: "PavingStones128",
name: "Pflasterstein 1",
category: "Paving Stones",
maps: {
color: "/assets/materials/PavingStones128/color.jpg",
normal: "/assets/materials/PavingStones128/normal.jpg",
roughness: "/assets/materials/PavingStones128/roughness.jpg",
displacement: "/assets/materials/PavingStones128/displacement.jpg",
ao: "/assets/materials/PavingStones128/ao.jpg",
},
},
{
id: "PavingStones138",
name: "Pflasterstein 2",
category: "Paving Stones",
maps: {
color: "/assets/materials/PavingStones138/color.jpg",
normal: "/assets/materials/PavingStones138/normal.jpg",
roughness: "/assets/materials/PavingStones138/roughness.jpg",
displacement: "/assets/materials/PavingStones138/displacement.jpg",
ao: "/assets/materials/PavingStones138/ao.jpg",
},
},
{
id: "Plaster002",
name: "Putz 1",
category: "Plaster",
maps: {
color: "/assets/materials/Plaster002/color.jpg",
normal: "/assets/materials/Plaster002/normal.jpg",
roughness: "/assets/materials/Plaster002/roughness.jpg",
displacement: "/assets/materials/Plaster002/displacement.jpg",
},
},
{
id: "Plaster007",
name: "Putz 2",
category: "Plaster",
maps: {
color: "/assets/materials/Plaster007/color.jpg",
normal: "/assets/materials/Plaster007/normal.jpg",
roughness: "/assets/materials/Plaster007/roughness.jpg",
displacement: "/assets/materials/Plaster007/displacement.jpg",
ao: "/assets/materials/Plaster007/ao.jpg",
},
},
{
id: "Rock058",
name: "Naturstein 1",
category: "Rock",
maps: {
color: "/assets/materials/Rock058/color.jpg",
normal: "/assets/materials/Rock058/normal.jpg",
roughness: "/assets/materials/Rock058/roughness.jpg",
displacement: "/assets/materials/Rock058/displacement.jpg",
ao: "/assets/materials/Rock058/ao.jpg",
},
},
{
id: "Rock063",
name: "Naturstein 2",
category: "Rock",
maps: {
color: "/assets/materials/Rock063/color.jpg",
normal: "/assets/materials/Rock063/normal.jpg",
roughness: "/assets/materials/Rock063/roughness.jpg",
displacement: "/assets/materials/Rock063/displacement.jpg",
ao: "/assets/materials/Rock063/ao.jpg",
},
},
{
id: "Rock064",
name: "Naturstein 3",
category: "Rock",
maps: {
color: "/assets/materials/Rock064/color.jpg",
normal: "/assets/materials/Rock064/normal.jpg",
roughness: "/assets/materials/Rock064/roughness.jpg",
displacement: "/assets/materials/Rock064/displacement.jpg",
ao: "/assets/materials/Rock064/ao.jpg",
},
},
{
id: "RoofingTiles012A",
name: "Dach 1",
category: "Roofing Tiles",
maps: {
color: "/assets/materials/RoofingTiles012A/color.jpg",
normal: "/assets/materials/RoofingTiles012A/normal.jpg",
roughness: "/assets/materials/RoofingTiles012A/roughness.jpg",
displacement: "/assets/materials/RoofingTiles012A/displacement.jpg",
ao: "/assets/materials/RoofingTiles012A/ao.jpg",
},
},
{
id: "RoofingTiles014A",
name: "Dach 3",
category: "Roofing Tiles",
maps: {
color: "/assets/materials/RoofingTiles014A/color.jpg",
normal: "/assets/materials/RoofingTiles014A/normal.jpg",
roughness: "/assets/materials/RoofingTiles014A/roughness.jpg",
displacement: "/assets/materials/RoofingTiles014A/displacement.jpg",
ao: "/assets/materials/RoofingTiles014A/ao.jpg",
},
},
{
id: "Terrazzo005",
name: "Terrazzo 1",
category: "Terrazzo",
maps: {
color: "/assets/materials/Terrazzo005/color.jpg",
normal: "/assets/materials/Terrazzo005/normal.jpg",
roughness: "/assets/materials/Terrazzo005/roughness.jpg",
displacement: "/assets/materials/Terrazzo005/displacement.jpg",
},
},
{
id: "Terrazzo013",
name: "Terrazzo 2",
category: "Terrazzo",
maps: {
color: "/assets/materials/Terrazzo013/color.jpg",
normal: "/assets/materials/Terrazzo013/normal.jpg",
roughness: "/assets/materials/Terrazzo013/roughness.jpg",
displacement: "/assets/materials/Terrazzo013/displacement.jpg",
},
},
{
id: "Terrazzo018",
name: "Terrazzo 3",
category: "Terrazzo",
maps: {
color: "/assets/materials/Terrazzo018/color.jpg",
normal: "/assets/materials/Terrazzo018/normal.jpg",
roughness: "/assets/materials/Terrazzo018/roughness.jpg",
displacement: "/assets/materials/Terrazzo018/displacement.jpg",
},
},
{
id: "Tiles139",
name: "Fliesen 1",
category: "Tiles",
maps: {
color: "/assets/materials/Tiles139/color.jpg",
normal: "/assets/materials/Tiles139/normal.jpg",
roughness: "/assets/materials/Tiles139/roughness.jpg",
displacement: "/assets/materials/Tiles139/displacement.jpg",
},
},
{
id: "Tiles140",
name: "Fliesen 2",
category: "Tiles",
maps: {
color: "/assets/materials/Tiles140/color.jpg",
normal: "/assets/materials/Tiles140/normal.jpg",
roughness: "/assets/materials/Tiles140/roughness.jpg",
displacement: "/assets/materials/Tiles140/displacement.jpg",
},
},
{
id: "Wood049",
name: "Holz 1",
category: "Wood",
maps: {
color: "/assets/materials/Wood049/color.jpg",
normal: "/assets/materials/Wood049/normal.jpg",
roughness: "/assets/materials/Wood049/roughness.jpg",
displacement: "/assets/materials/Wood049/displacement.jpg",
},
},
{
id: "Wood051",
name: "Holz 2",
category: "Wood",
maps: {
color: "/assets/materials/Wood051/color.jpg",
normal: "/assets/materials/Wood051/normal.jpg",
roughness: "/assets/materials/Wood051/roughness.jpg",
displacement: "/assets/materials/Wood051/displacement.jpg",
},
},
{
id: "Wood066",
name: "Holz 3",
category: "Wood",
maps: {
color: "/assets/materials/Wood066/color.jpg",
normal: "/assets/materials/Wood066/normal.jpg",
roughness: "/assets/materials/Wood066/roughness.jpg",
displacement: "/assets/materials/Wood066/displacement.jpg",
},
},
{
id: "Wood092",
name: "Holz 4",
category: "Wood",
maps: {
color: "/assets/materials/Wood092/color.jpg",
normal: "/assets/materials/Wood092/normal.jpg",
roughness: "/assets/materials/Wood092/roughness.jpg",
displacement: "/assets/materials/Wood092/displacement.jpg",
},
},
{
id: "Wood094",
name: "Holz 5",
category: "Wood",
maps: {
color: "/assets/materials/Wood094/color.jpg",
normal: "/assets/materials/Wood094/normal.jpg",
roughness: "/assets/materials/Wood094/roughness.jpg",
displacement: "/assets/materials/Wood094/displacement.jpg",
},
},
{
id: "WoodFloor064",
name: "Holzboden 1",
category: "Wood Floor",
maps: {
color: "/assets/materials/WoodFloor064/color.jpg",
normal: "/assets/materials/WoodFloor064/normal.jpg",
roughness: "/assets/materials/WoodFloor064/roughness.jpg",
displacement: "/assets/materials/WoodFloor064/displacement.jpg",
},
},
{
id: "WoodFloor070",
name: "Holzboden 2",
category: "Wood Floor",
maps: {
color: "/assets/materials/WoodFloor070/color.jpg",
normal: "/assets/materials/WoodFloor070/normal.jpg",
roughness: "/assets/materials/WoodFloor070/roughness.jpg",
displacement: "/assets/materials/WoodFloor070/displacement.jpg",
},
},
]; ];
export default MATERIAL_LIBRARY; export default MATERIAL_LIBRARY;
+322
View File
@@ -0,0 +1,322 @@
// Kontextmenü-Builder — baut die Menü-Einträge je Quelle (Ebene / Zeichnungs-
// ebene / Grundriss / 3D-Viewport). 1:1 nach docs/design/context-menu.md.
// JEDER Eintrag ist verdrahtet ODER klar deaktiviert mit Tooltip (`title`, nennt
// den Grund) — kein stiller No-Op.
//
// Extrahiert aus App.tsx (reine Funktionen, keine Hooks/kein Store-Zugriff — die
// Aktionen kommen als `MenuActions` von App herein). Bezeichner englisch,
// Kommentare deutsch (CONVENTIONS.md).
import { t } from "../i18n";
import type { LayerCategory, Project } from "../model/types";
import { flattenCategories } from "../model/types";
import { findCategory } from "../state/projectSlice";
import { MenuIcon } from "../ui/ContextMenu";
import type { ContextMenuItem } from "../ui/ContextMenu";
/** Offenes Kontextmenü (genau eines). `x/y` = Anker in Viewport-Px. */
export type MenuState =
| { kind: "layer"; code: string; x: number; y: number }
| { kind: "level"; id: string; x: number; y: number }
| { kind: "plan"; wallId: string | null; x: number; y: number }
| { kind: "viewport"; wallId: string | null; x: number; y: number }
| null;
/** Titel des Kontextmenüs je Quelle (Ebenenname / Geschossname / Wand/Ansicht). */
export function menuTitle(menu: NonNullable<MenuState>, project: Project): string {
switch (menu.kind) {
case "layer": {
const c = findCategory(project.layers, menu.code);
return c ? `${c.code} · ${c.name}` : menu.code;
}
case "level": {
const l = project.drawingLevels.find((z) => z.id === menu.id);
return l ? l.name : t("ctx.title.level");
}
case "plan":
case "viewport":
return menu.wallId ? t("ctx.title.wall") : t("ctx.title.view");
}
}
/** Aktionen, die der Kontextmenü-Builder von App braucht (alle verdrahtet). */
export interface MenuActions {
project: Project;
selectedWallId: string | null;
layerClipboard: { color: string; lw: number } | null;
openLayerEditor: (code: string) => void;
openLevelEditor: (id: string) => void;
addSubCategory: (parentCode: string) => void;
assignWallCategory: (wallId: string, code: string) => void;
duplicateCategory: (code: string) => void;
deleteCategory: (code: string) => void;
setLayerClipboard: (clip: { color: string; lw: number } | null) => void;
patchCategory: (code: string, patch: Partial<LayerCategory>) => void;
duplicateLevel: (id: string) => void;
deleteLevel: (id: string) => void;
clearSelection: () => void;
fit: () => void;
fitSelection: () => void;
/** Anzahl geschlossener Flächen in der aktuellen 2D-Auswahl (Boolean-Aktivierung). */
closedSelectionCount: number;
union: () => void;
difference: () => void;
intersect: () => void;
/** Anzahl aktuell selektierter 2D-Zeichnungselemente (z-Anordnen-Menü). */
drawingSelectionCount: number;
bringToFront: () => void;
bringForward: () => void;
sendBackward: () => void;
sendToBack: () => void;
}
/**
* Baut die Kontextmenü-Einträge je Quelle — 1:1 nach docs/design/context-menu.md.
* JEDER Eintrag ist verdrahtet ODER klar deaktiviert mit Tooltip (`title`,
* nennt den Grund) — kein stiller No-Op.
*/
export function buildMenuItems(
menu: NonNullable<MenuState>,
a: MenuActions,
): ContextMenuItem[] {
switch (menu.kind) {
case "layer":
return layerMenuItems(menu.code, a);
case "level":
return levelMenuItems(menu.id, a);
case "plan":
return planMenuItems(menu.wallId, a);
case "viewport":
return viewportMenuItems(menu.wallId, a);
}
}
/** Ebenen-Kontextmenü (Rechtsklick auf Ebenen-Zeile). */
function layerMenuItems(code: string, a: MenuActions): ContextMenuItem[] {
const total = flattenCategories(a.project.layers).length;
const hasSelection = a.selectedWallId !== null;
const hasClipboard = a.layerClipboard !== null;
return [
{
label: t("ctx.layerSettings"),
icon: <MenuIcon name="settings" />,
onClick: () => a.openLayerEditor(code),
},
{ divider: true },
{
label: t("ctx.addSubLayer"),
icon: <MenuIcon name="add" />,
onClick: () => a.addSubCategory(code),
},
{
label: t("ctx.assignSelection"),
icon: <MenuIcon name="move_down" />,
disabled: !hasSelection,
title: hasSelection ? undefined : t("ctx.assignSelection.disabled"),
onClick: () => {
if (a.selectedWallId) a.assignWallCategory(a.selectedWallId, code);
},
},
{ divider: true },
{
label: t("ctx.duplicate"),
icon: <MenuIcon name="content_copy" />,
onClick: () => a.duplicateCategory(code),
},
{
label: t("ctx.copyProps"),
icon: <MenuIcon name="colorize" />,
onClick: () => {
const c = findCategory(a.project.layers, code);
if (c) a.setLayerClipboard({ color: c.color, lw: c.lw });
},
},
{
label: t("ctx.pasteProps"),
icon: <MenuIcon name="format_paint" />,
disabled: !hasClipboard,
title: hasClipboard ? undefined : t("ctx.pasteProps.disabled"),
onClick: () => {
if (a.layerClipboard)
a.patchCategory(code, {
color: a.layerClipboard.color,
lw: a.layerClipboard.lw,
});
},
},
{ divider: true },
{
label: t("ctx.delete"),
icon: <MenuIcon name="delete" />,
danger: true,
disabled: total <= 1,
title: total <= 1 ? t("ctx.delete.lastLayer") : undefined,
onClick: () => a.deleteCategory(code),
},
];
}
/** Zeichnungsebenen-Kontextmenü (Rechtsklick auf Geschoss/Schnitt/Zeichnung). */
function levelMenuItems(id: string, a: MenuActions): ContextMenuItem[] {
const total = a.project.drawingLevels.length;
return [
{
label: t("ctx.levelSettings"),
icon: <MenuIcon name="settings" />,
onClick: () => a.openLevelEditor(id),
},
{ divider: true },
{
label: t("ctx.duplicate"),
icon: <MenuIcon name="content_copy" />,
onClick: () => a.duplicateLevel(id),
},
{ divider: true },
{
label: t("ctx.delete"),
icon: <MenuIcon name="delete" />,
danger: true,
disabled: total <= 1,
title: total <= 1 ? t("ctx.delete.lastLevel") : undefined,
onClick: () => a.deleteLevel(id),
},
];
}
/**
* Grundriss-Kontextmenü. Bezug ist die getroffene/gewählte Wand. Einträge sind
* verdrahtet: „Auf Auswahl einpassen" (PlanView-Handle) und „Auswahl aufheben".
* Ohne getroffene Wand bleibt nur „Einpassen". (Eigenschaften-Panel/Bemaßung
* folgen später — daher hier KEIN Schein-Eintrag.)
*/
function planMenuItems(
wallId: string | null,
a: MenuActions,
): ContextMenuItem[] {
// Boolean-Einträge (Vereinigen/Subtrahieren/Schneiden) auf die 2D-Auswahl.
// Aktiv ab 2 geschlossenen Flächen; sonst deaktiviert mit erklärendem Tooltip.
const boolEnabled = a.closedSelectionCount >= 2;
const boolTitle = boolEnabled ? undefined : t("ctx.boolean.disabled");
const booleanItems: ContextMenuItem[] = [
{
label: t("ctx.union"),
icon: <MenuIcon name="join_full" />,
disabled: !boolEnabled,
title: boolTitle,
onClick: a.union,
},
{
label: t("ctx.subtract"),
icon: <MenuIcon name="join_left" />,
disabled: !boolEnabled,
title: boolTitle,
onClick: a.difference,
},
{
label: t("ctx.intersect"),
icon: <MenuIcon name="join_inner" />,
disabled: !boolEnabled,
title: boolTitle,
onClick: a.intersect,
},
];
// Z-Anordnen (nach vorn/hinten holen) — wirkt auf die aktuelle 2D-Auswahl,
// sortiert `drawings2d` um (Array-Reihenfolge = Zeichenreihenfolge). Aktiv
// ab 1 selektiertem Element; sonst deaktiviert mit erklärendem Tooltip.
const arrangeEnabled = a.drawingSelectionCount >= 1;
const arrangeTitle = arrangeEnabled ? undefined : t("ctx.arrange.disabled");
const arrangeItems: ContextMenuItem[] = [
{
label: t("ctx.arrange.front"),
icon: <MenuIcon name="flip_to_front" />,
disabled: !arrangeEnabled,
title: arrangeTitle,
onClick: a.bringToFront,
},
{
label: t("ctx.arrange.forward"),
icon: <MenuIcon name="arrow_upward" />,
disabled: !arrangeEnabled,
title: arrangeTitle,
onClick: a.bringForward,
},
{
label: t("ctx.arrange.backward"),
icon: <MenuIcon name="arrow_downward" />,
disabled: !arrangeEnabled,
title: arrangeTitle,
onClick: a.sendBackward,
},
{
label: t("ctx.arrange.back"),
icon: <MenuIcon name="flip_to_back" />,
disabled: !arrangeEnabled,
title: arrangeTitle,
onClick: a.sendToBack,
},
];
if (wallId) {
return [
{
label: t("ctx.fitSelection"),
icon: <MenuIcon name="center_focus_strong" />,
onClick: a.fitSelection,
},
{
label: t("ctx.clearSelection"),
icon: <MenuIcon name="deselect" />,
onClick: a.clearSelection,
},
{ divider: true },
...booleanItems,
{ divider: true },
...arrangeItems,
{ divider: true },
{
label: t("ctx.fit"),
icon: <MenuIcon name="fit_screen" />,
onClick: a.fit,
},
];
}
return [
...booleanItems,
{ divider: true },
...arrangeItems,
{ divider: true },
{
label: t("ctx.fit"),
icon: <MenuIcon name="fit_screen" />,
onClick: a.fit,
},
];
}
/**
* 3D-Kontextmenü. Im 3D gibt es (noch) kein Einpassen-Handle wie im Grundriss;
* verdrahtet ist „Auswahl aufheben" (mit getroffener Wand). Ohne Wand ist das
* Menü leer-sinnvoll: ein deaktivierter Hinweis, kein stiller No-Op.
*/
function viewportMenuItems(
wallId: string | null,
a: MenuActions,
): ContextMenuItem[] {
if (wallId) {
return [
{
label: t("ctx.clearSelection"),
icon: <MenuIcon name="deselect" />,
onClick: a.clearSelection,
},
];
}
return [
{
label: t("ctx.noWallHit"),
disabled: true,
title: t("ctx.noWallHit.hint"),
},
];
}
+78
View File
@@ -0,0 +1,78 @@
// Bündelt die transienten Dialog-/Overlay-Sichtbarkeits-States der App-Shell
// (Ressourcen-Fenster, Layout-Ansicht, Import-/Export-/Einstellungs-Dialoge,
// Drop-Feedback). Bewusst KEIN Store-Slice: dieser State ist rein lokal (keine
// Undo/Redo-Relevanz, kein Cross-Component-Lesezugriff nötig — TopBar/
// AppMenuBar bekommen ihn weiterhin als Props von App() gereicht, unverändert)
// — nur aus App() herausgezogen, um die Funktion zu entlasten. Reine
// useState-Bündelung, keine Verhaltensänderung.
//
// Extrahiert aus App.tsx. Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
import { useState } from "react";
import type { DxfImportResult } from "../io/dxfParser";
import type { ExportSaveFormat } from "../ui/ExportSaveDialog";
import type { TabId as ResourceTabId } from "../ui/ResourceManager";
export function useDialogState() {
// Schwebendes Ressourcen-Fenster (Overlay über allem). Transienter UI-State,
// bleibt lokal. Wird über die Oberleiste geöffnet und über X / Hintergrund-
// Klick / Esc geschlossen.
const [resourcesOpen, setResourcesOpen] = useState(false);
// Angeforderter Start-Tab des Ressourcen-Fensters (z. B. „doorStyles", wenn man
// von einer gewählten Tür in den Typeditor springt).
const [resourcesTab, setResourcesTab] = useState<ResourceTabId>("components");
// Offener Fenster-/Tür-Einstellungsdialog (⚙ der OpeningSection) — ID der
// editierten Öffnung, oder null wenn geschlossen.
const [openingEditorId, setOpeningEditorId] = useState<string | null>(null);
// Aktives Layout-Blatt im HAUPT-Viewport (DOSSIER A3, Phase 2b): Id des offenen
// Layouts oder null (= normale Modell-Ansicht). Transienter UI-State; das
// Layout selbst lebt im Projekt (project.layouts). Ist es gesetzt, zeigt der
// Haupt-Viewport das Blatt (LayoutSheetView) statt Grundriss/3D.
const [activeLayoutId, setActiveLayoutId] = useState<string | null>(null);
// Modaler Import-Dialog (DXF/DWG). Offen, sobald eine Datei gewählt/fallen-
// gelassen wurde; hält Dateiname, geparstes Ergebnis (null bei DWG) und das
// DWG-Flag (kein Parser). null = kein Dialog offen. Transienter UI-State.
const [importState, setImportState] = useState<{
fileName: string;
isDwg: boolean;
parsed: DxfImportResult | null;
// true, solange DWG noch geparst wird (WASM lädt/läuft).
loading?: boolean;
} | null>(null);
// Modaler PDF-Export-Dialog (Grundriss → Vektor-PDF). true = offen.
const [exportOpen, setExportOpen] = useState(false);
const [exportDxfOpen, setExportDxfOpen] = useState(false);
// Modales Einstellungs-Fenster (Darstellungsfarben + Projekt-Felder).
const [settingsOpen, setSettingsOpen] = useState(false);
// Offener Schnellexport-Dialog: Format (aus dem Menü) oder null = geschlossen.
const [exportSaveFormat, setExportSaveFormat] = useState<ExportSaveFormat | null>(null);
// Visuelles Drop-Feedback (dezenter Rahmen), solange eine Datei über die App
// gezogen wird. Zählt dragenter/dragleave aus, um Flackern bei Kind-Elementen
// zu vermeiden.
const [dropActive, setDropActive] = useState(false);
return {
resourcesOpen,
setResourcesOpen,
resourcesTab,
setResourcesTab,
openingEditorId,
setOpeningEditorId,
activeLayoutId,
setActiveLayoutId,
importState,
setImportState,
exportOpen,
setExportOpen,
exportDxfOpen,
setExportDxfOpen,
settingsOpen,
setSettingsOpen,
exportSaveFormat,
setExportSaveFormat,
dropActive,
setDropActive,
};
}
+8 -1
View File
@@ -2080,9 +2080,16 @@ body {
opacity: 0.85; opacity: 0.85;
} }
/* Auswahl-Hervorhebung (Links-Klick): Akzentkontur + leichte Füllung. */ /* Auswahl-Hervorhebung (Links-Klick): Akzentkontur + leichte Füllung.
`fill-opacity` erzwingt die Transparenz unabhängig vom Theme — im Hell-Theme
ist `--accent-dim` fast deckend weiss (Nutzung als UI-Hintergrundfarbe an
~70 anderen Stellen), würde ohne eigene Opacity die Schraffur der gewählten
Wand/Decke/des Raums vollständig verdecken (nur im Dunkel-Theme zufällig
unauffällig, da dort selbst schon rgba mit niedriger Alpha). Gleiches Muster
wie .plan-marquee/.tool-preview-fill weiter unten. */
.plan-svg .plan-selected { .plan-svg .plan-selected {
fill: var(--accent-dim); fill: var(--accent-dim);
fill-opacity: 0.35;
stroke: var(--accent); stroke: var(--accent);
stroke-width: 2; stroke-width: 2;
vector-effect: non-scaling-stroke; vector-effect: non-scaling-stroke;
+18
View File
@@ -0,0 +1,18 @@
// React-State-Wrapper um den Hell/Dunkel-Umschalter (Einstellungen →
// Darstellung). Standard Hell (styles.css); Dunkel ist Opt-in und
// persistiert in localStorage (themeMode.ts).
//
// Extrahiert aus App.tsx. Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
import { useState } from "react";
import { getStoredThemeMode, setStoredThemeMode } from "./themeMode";
import type { ThemeMode } from "./themeMode";
export function useThemeModeState() {
const [themeMode, setThemeModeState] = useState<ThemeMode>(() => getStoredThemeMode());
const setThemeMode = (mode: ThemeMode) => {
setThemeModeState(mode);
setStoredThemeMode(mode);
};
return { themeMode, setThemeMode };
}
+74
View File
@@ -0,0 +1,74 @@
// Kleiner schwebender Editor (dunkel, wie das Kontextmenü) am Anker. Schließt
// über Außenklick / Esc. Rein darstellend: die Felder rufen die übergebenen
// Patch-Handler, die live auf den State wirken (kein Bestätigen nötig).
//
// Extrahiert aus App.tsx. Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
import React, { useEffect, useRef, useState } from "react";
import { t } from "../i18n";
export function InlineEditor({
x,
y,
title,
onClose,
children,
}: {
x: number;
y: number;
title: string;
onClose: () => void;
children: React.ReactNode;
}) {
const ref = useRef<HTMLDivElement>(null);
// An die Viewport-Ränder klemmen (wie das Kontextmenü).
const [pos, setPos] = useState({ x, y });
useEffect(() => {
const el = ref.current;
if (!el) return;
const r = el.getBoundingClientRect();
let nx = x;
let ny = y;
if (nx + r.width > window.innerWidth - 4) nx = window.innerWidth - r.width - 4;
if (ny + r.height > window.innerHeight - 4)
ny = window.innerHeight - r.height - 4;
setPos({ x: Math.max(4, nx), y: Math.max(4, ny) });
}, [x, y]);
// Schließen bei Außenklick / Esc.
useEffect(() => {
const onDown = (e: PointerEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) onClose();
};
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("pointerdown", onDown, true);
window.addEventListener("keydown", onKey);
return () => {
window.removeEventListener("pointerdown", onDown, true);
window.removeEventListener("keydown", onKey);
};
}, [onClose]);
return (
<div
ref={ref}
className="ctx-editor"
style={{ left: pos.x, top: pos.y }}
role="dialog"
aria-label={title}
>
<div className="ctx-editor-head">
<span className="ctx-editor-title">{title}</span>
<button
className="ctx-editor-close"
onClick={onClose}
aria-label={t("editor.close")}
>
×
</button>
</div>
<div className="ctx-editor-body">{children}</div>
</div>
);
}
+855
View File
@@ -0,0 +1,855 @@
// Haupt-Viewport-Inhalt (Mitte) — abhängig von aktiver Zeichnungsebene und
// Ansichtstyp. `Content` ist der View-Router: Geschoss→Grundriss/3D,
// Schnitt/Ansicht→SectionPlanView, freie Zeichnung→LevelPlanView. Die beiden
// Plan-Ableitungen (Grundriss bzw. Schnitt/Ansicht) zeichnen dieselbe PlanView.
//
// Extrahiert aus App.tsx (top-level, alles über Props — keine App-Closure).
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
import { useEffect, useMemo, useState } from "react";
import { t } from "../i18n";
import { formatM } from "../model/types";
import type {
Ceiling,
Drawing2D,
DrawingLevel,
EdgeGrip,
Project,
Roof,
Vec2,
Wall,
} from "../model/types";
import { generatePlan, generateSectionPlan } from "../plan/generatePlan";
import type { CategoryDisplayResolver } from "../plan/generatePlan";
import { computeSection } from "../plan/toSection";
import type { SectionOutput } from "../plan/toSection";
import { generateElevationPlan } from "../plan/toElevation";
import { pushNativeScene, pushNativeWalls } from "../plan/nativeSync";
import { selectionHighlightLines } from "../plan/toWalls3d";
import { PlanView } from "../plan/PlanView";
import type {
GripHandlers,
HudFieldsState,
MarqueeSelection,
PlanSelection,
PlanViewHandle,
} from "../plan/PlanView";
import { Viewport3D } from "../viewport/Viewport3D";
import type { DisplayResolver, ViewportContextInfo } from "../viewport/Viewport3D";
import type { Pick3dHit } from "../viewport/Wasm3DViewport";
import { DrawingTabs } from "../DrawingTabs";
import type { Drawing } from "../DrawingTabs";
import type { ToolDraft, ToolHandlers, ToolId } from "../tools/types";
import type { DetailLevel, RenderMode, View3d, ViewType } from "../ui/TopBar";
/**
* Akzentfarbe der 3D-Auswahl-Hervorhebung (Umriss-Linien, WASM-Viewport, s.
* `Content`/`selectionHighlightLines`). Kräftiges Orange hebt sich vom
* grauen Wand-/Deckenton, dem hellen Hidden-Line-Weiss und dem Bodenraster ab.
*/
const HIGHLIGHT_RGB: [number, number, number] = [1.0, 0.5, 0.1];
/** Hauptinhalt — abhängig von der aktiven Zeichnungsebene und dem Ansichtstyp. */
export function Content({
project,
level,
viewType,
visibleCodes,
detail,
referenceLines,
hairline,
marqueeColor,
snapColor,
renderMode,
view3d,
fov,
grid3dVisible,
onToggleGrid3d,
section3d,
onClearSection3d,
mono,
drawings,
onSelectLevel,
floorDisplay,
categoryDisplay,
displayKey,
planViewRef,
onCursor,
onScale,
selectedWallIds,
selectedCeilingIds,
selectedOpeningIds,
selectedStairIds,
selectedExtrudedSolidIds,
selectedColumnIds,
selectedRoomIds,
selectedRoofIds,
selectedContextObjectIds,
selectedSectionLineId,
roomStamp,
onStampMove,
onStampEdit,
onEditDrawingText,
onPlanSelect,
onPlanMarquee,
onSegmentCut,
onPlanContextMenu,
onViewportSelectWall,
onViewportSelectCeiling,
onViewportSelectOpening,
onViewportSelectStair,
onViewportContextMenu,
onViewport3dPick,
commandActive,
draft,
onWorkplanePoint,
onWorkplaneConfirm,
activeTool,
toolInputActive,
toolHandlers,
hudFields,
grips,
edgeGrips,
selectedDrawingId,
selectedDrawingIds,
gripHandlers,
edit3dWalls,
edit3dDrawings,
edit3dCeilings,
edit3dRoofs,
onEdit3dVertex,
onEdit3dBody,
onEdit3dEdge,
onEdit3dRoofPitch,
onEdit3dWallTop,
onEdit3dEnd,
}: {
project: Project;
level: DrawingLevel;
viewType: ViewType;
visibleCodes: Set<string>;
detail: DetailLevel;
referenceLines: boolean;
hairline: boolean;
/** Farbe des Auswahlrahmens (Marquee) im Grundriss. */
marqueeColor: string;
/** Farbe der Snap-/Endpunkt-Hervorhebung im Grundriss. */
snapColor: string;
renderMode: RenderMode;
/** Kanonischer 3D-Blickwinkel (Preset) der Perspektive. */
view3d: View3d;
/** Sichtwinkel der 3D-Perspektivkamera in Grad. */
fov: number;
/** Boden-Referenzraster im 3D-Viewport sichtbar (wirkt nur im WASM-Pfad). */
grid3dVisible: boolean;
/** Klick auf den Grid-Umschalter-Button im 3D-Viewport (WASM-Pfad). */
onToggleGrid3d: () => void;
/** Aktive 3D-Schnittebene (world point+normal) der gebundenen Schnittlinie,
* oder null. Speist Viewport3D/Wasm3DViewport (Live-Schnitt). */
section3d: { point: [number, number, number]; normal: [number, number, number] } | null;
/** Schnitt-Bindung im 3D-Viewport lösen (Klick auf den gebundenen Knopf). */
onClearSection3d: () => void;
/** Grundriss in Schwarz-Weiss (mono) statt mit Kategorie-/Element-Farben. */
mono: boolean;
/** Dokument-Tabs der Kopfzeile — ein Tab je Geschoss-Grundriss. */
drawings: Drawing[];
/** Aktiviert eine Zeichnungsebene (Geschoss) geteilt mit dem Panel
* Zeichnungsebenen"; siehe DrawingTabs.tsx. */
onSelectLevel: (id: string) => void;
floorDisplay: DisplayResolver;
categoryDisplay: CategoryDisplayResolver & DisplayResolver;
displayKey: string;
planViewRef: React.RefObject<PlanViewHandle>;
onCursor: (model: Vec2 | null) => void;
onScale: (n: number) => void;
selectedWallIds: string[];
selectedCeilingIds: string[];
selectedOpeningIds: string[];
selectedStairIds: string[];
selectedExtrudedSolidIds: string[];
selectedColumnIds: string[];
selectedRoomIds: string[];
selectedRoofIds: string[];
selectedContextObjectIds: string[];
selectedSectionLineId: string | null;
roomStamp: { roomId: string; anchor: Vec2 } | null;
onStampMove: (roomId: string, delta: Vec2) => void;
onStampEdit: (roomId: string) => void;
onEditDrawingText?: (drawingId: string) => void;
onPlanSelect: (sel: PlanSelection) => void;
onPlanMarquee: (sel: MarqueeSelection) => void;
onSegmentCut: (drawingId: string, model: Vec2) => void;
onPlanContextMenu: (info: {
clientX: number;
clientY: number;
model: Vec2;
}) => void;
onViewportSelectWall: (wallId: string | null) => void;
onViewportSelectCeiling: (ceilingId: string | null) => void;
onViewportSelectOpening: (openingId: string | null) => void;
onViewportSelectStair: (stairId: string | null) => void;
onViewportContextMenu: (info: ViewportContextInfo) => void;
/** Links-KLICK-Auswahl im WASM-3D-Viewport (TS-Raycast, Wand/Decke). */
onViewport3dPick: (hit: Pick3dHit, additive: boolean) => void;
/** Läuft ein zeichnender Befehl? Macht den 3D-Viewport zur Erstellungs-Fläche. */
commandActive: boolean;
/** Aktuelle Werkzeug-Vorschau (Rubber-Band) — auch in 3D gespiegelt. */
draft: ToolDraft | null;
/** Auf die Arbeitsebene projizierter Punkt → CommandEngine (move/pick). */
onWorkplanePoint: (kind: "move" | "pick", model: Vec2) => void;
/** Befehl bestätigen/beenden aus dem 3D-Viewport. */
onWorkplaneConfirm: () => void;
activeTool: ToolId;
toolInputActive: boolean;
toolHandlers: ToolHandlers;
hudFields: HudFieldsState | null;
grips: Vec2[];
edgeGrips: EdgeGrip[];
selectedDrawingId: string | null;
selectedDrawingIds?: string[];
gripHandlers: GripHandlers;
/** Alle gewählten Wände — Quelle der 3D-Editier-Griffe (Mehrfachauswahl ⇒ alle). */
edit3dWalls: Wall[];
/** Alle gewählten 2D-Elemente — 3D-Vertex-Griffe (Mehrfachauswahl ⇒ alle). */
edit3dDrawings: Drawing2D[];
/** Alle gewählten Decken — 3D-Eckpunkt-/Kanten-/Verschiebe-Griffe. */
edit3dCeilings: Ceiling[];
edit3dRoofs: Roof[];
onEdit3dVertex: (
target: { wallId?: string; drawingId?: string; ceilingId?: string; roofId?: string },
index: number,
model: Vec2,
excludeWallIds?: Set<string>,
excludeDrawingIds?: Set<string>,
) => Vec2;
onEdit3dBody: (
target: { wallId?: string; drawingId?: string; ceilingId?: string; roofId?: string },
delta: Vec2,
) => void;
onEdit3dEdge: (ceilingId: string, aIndex: number, bIndex: number, delta: Vec2) => void;
onEdit3dRoofPitch: (roofId: string, apexHeight: number) => void;
onEdit3dWallTop: (wallId: string, topZ: number) => void;
onEdit3dEnd: () => void;
}) {
// Auswahl-Hervorhebung im WASM-3D-Viewport: Umriss der gewählten Wand(e)/
// Decke(n) in Akzent-Orange, immer obenauf (s. selectionHighlightLines in
// plan/toWalls3d.ts + set_highlight_lines in useWasm3dRenderer). Wirkt NUR
// im WASM-Zweig (Viewport3D reicht sie nur dort an Wasm3DViewport durch);
// die three.js-Sicht hebt die Auswahl weiterhin über ihr eigenes Material
// hervor. Leere Auswahl → null (kein Highlight).
const highlightLines = useMemo(
() =>
selectedWallIds.length === 0 &&
selectedCeilingIds.length === 0 &&
selectedOpeningIds.length === 0 &&
selectedStairIds.length === 0 &&
selectedRoofIds.length === 0 &&
selectedContextObjectIds.length === 0
? null
: selectionHighlightLines(
project,
selectedWallIds,
selectedCeilingIds,
HIGHLIGHT_RGB,
selectedOpeningIds,
selectedStairIds,
selectedRoofIds,
selectedContextObjectIds,
),
[
project,
selectedWallIds,
selectedCeilingIds,
selectedOpeningIds,
selectedStairIds,
selectedRoofIds,
selectedContextObjectIds,
],
);
if (level.kind === "section" || level.kind === "elevation") {
return (
<main className="content">
<DrawingTabs
drawings={drawings}
activeLevelId={level.id}
onSelectLevel={onSelectLevel}
/>
<SectionPlanView
project={project}
level={level}
hairline={hairline}
marqueeColor={marqueeColor}
snapColor={snapColor}
mono={mono}
planViewRef={planViewRef}
onCursor={onCursor}
onScale={onScale}
selectedWallIds={selectedWallIds}
selectedCeilingIds={selectedCeilingIds}
selectedOpeningIds={selectedOpeningIds}
selectedStairIds={selectedStairIds}
selectedExtrudedSolidIds={selectedExtrudedSolidIds}
selectedColumnIds={selectedColumnIds}
selectedRoomIds={selectedRoomIds}
selectedRoofIds={selectedRoofIds}
selectedSectionLineId={selectedSectionLineId}
roomStamp={roomStamp}
onStampMove={onStampMove}
onStampEdit={onStampEdit}
onEditDrawingText={onEditDrawingText}
onOpenSectionLevel={onSelectLevel}
onPlanSelect={onPlanSelect}
onPlanMarquee={onPlanMarquee}
onSegmentCut={onSegmentCut}
onPlanContextMenu={onPlanContextMenu}
activeTool={activeTool}
toolInputActive={toolInputActive}
toolHandlers={toolHandlers}
hudFields={hudFields}
grips={grips}
edgeGrips={edgeGrips}
selectedDrawingId={selectedDrawingId}
selectedDrawingIds={selectedDrawingIds}
gripHandlers={gripHandlers}
/>
</main>
);
}
// Freie Zeichnung: dieselbe Plan-Ansicht wie ein Geschoss (generatePlan filtert
// `drawings2d` nach `level.id`), nur OHNE Geschoss-Eigenschaftsleiste und ohne
// 3D-Umschaltung — eine Zeichnung hat keine Höhe/3D. So sind importierte
// DXF-Polylinien hier sofort sichtbar und mit den 2D-Werkzeugen bearbeitbar.
if (level.kind === "drawing") {
return (
<main className="content">
<DrawingTabs
drawings={drawings}
activeLevelId={level.id}
onSelectLevel={onSelectLevel}
/>
<LevelPlanView
project={project}
level={level}
visibleCodes={visibleCodes}
categoryDisplay={categoryDisplay}
detail={detail}
referenceLines={referenceLines}
hairline={hairline}
marqueeColor={marqueeColor}
snapColor={snapColor}
mono={mono}
planViewRef={planViewRef}
onCursor={onCursor}
onScale={onScale}
selectedWallIds={selectedWallIds}
selectedCeilingIds={selectedCeilingIds}
selectedOpeningIds={selectedOpeningIds}
selectedStairIds={selectedStairIds}
selectedExtrudedSolidIds={selectedExtrudedSolidIds}
selectedColumnIds={selectedColumnIds}
selectedRoomIds={selectedRoomIds}
selectedRoofIds={selectedRoofIds}
selectedContextObjectIds={selectedContextObjectIds}
selectedSectionLineId={selectedSectionLineId}
roomStamp={roomStamp}
onStampMove={onStampMove}
onStampEdit={onStampEdit}
onEditDrawingText={onEditDrawingText}
onOpenSectionLevel={onSelectLevel}
onPlanSelect={onPlanSelect}
onPlanMarquee={onPlanMarquee}
onSegmentCut={onSegmentCut}
onPlanContextMenu={onPlanContextMenu}
activeTool={activeTool}
toolInputActive={toolInputActive}
toolHandlers={toolHandlers}
hudFields={hudFields}
grips={grips}
edgeGrips={edgeGrips}
selectedDrawingId={selectedDrawingId}
selectedDrawingIds={selectedDrawingIds}
gripHandlers={gripHandlers}
/>
</main>
);
}
// Geschoss: Dokument-Tabs + gewählte Ansicht. Geschosshöhe/Schnitthöhe
// werden in den Geschoss-Einstellungen gepflegt (Rechtsklick auf das
// Geschoss im Panel „Zeichnungsebenen"), nicht mehr hier in der Kopfzeile.
return (
<main className="content">
<DrawingTabs
drawings={drawings}
activeLevelId={level.id}
onSelectLevel={onSelectLevel}
/>
{viewType === "perspektive" ? (
<section className="pane">
<div className="pane-title">
{t("content.perspektiveTitle", { name: level.name })}
</div>
<Viewport3D
project={project}
visibleCodes={visibleCodes}
floorDisplay={floorDisplay}
categoryDisplay={categoryDisplay}
displayKey={displayKey}
renderMode={renderMode}
detail={detail}
view3d={view3d}
fov={fov}
gridElevation={level.baseElevation ?? 0}
gridVisible={grid3dVisible}
onToggleGrid={onToggleGrid3d}
section3d={section3d}
onClearSection3d={onClearSection3d}
onSelectWall={onViewportSelectWall}
onSelectCeiling={onViewportSelectCeiling}
onSelectOpening={onViewportSelectOpening}
onSelectStair={onViewportSelectStair}
onContextMenu={onViewportContextMenu}
onPick3d={onViewport3dPick}
highlightLines={highlightLines}
commandActive={commandActive}
draft={draft}
onWorkplanePoint={onWorkplanePoint}
onWorkplaneConfirm={onWorkplaneConfirm}
editWalls={edit3dWalls}
editDrawings={edit3dDrawings}
editCeilings={edit3dCeilings}
editRoofs={edit3dRoofs}
onEditVertex={onEdit3dVertex}
onEditBody={onEdit3dBody}
onEditEdge={onEdit3dEdge}
onEditRoofPitch={onEdit3dRoofPitch}
onEditWallTop={onEdit3dWallTop}
onEditEnd={onEdit3dEnd}
/>
</section>
) : (
<LevelPlanView
project={project}
level={level}
visibleCodes={visibleCodes}
categoryDisplay={categoryDisplay}
detail={detail}
referenceLines={referenceLines}
hairline={hairline}
marqueeColor={marqueeColor}
snapColor={snapColor}
mono={mono}
planViewRef={planViewRef}
onCursor={onCursor}
onScale={onScale}
selectedWallIds={selectedWallIds}
selectedCeilingIds={selectedCeilingIds}
selectedOpeningIds={selectedOpeningIds}
selectedStairIds={selectedStairIds}
selectedExtrudedSolidIds={selectedExtrudedSolidIds}
selectedColumnIds={selectedColumnIds}
selectedRoomIds={selectedRoomIds}
selectedRoofIds={selectedRoofIds}
selectedContextObjectIds={selectedContextObjectIds}
selectedSectionLineId={selectedSectionLineId}
roomStamp={roomStamp}
onStampMove={onStampMove}
onStampEdit={onStampEdit}
onEditDrawingText={onEditDrawingText}
onOpenSectionLevel={onSelectLevel}
onPlanSelect={onPlanSelect}
onPlanMarquee={onPlanMarquee}
onSegmentCut={onSegmentCut}
onPlanContextMenu={onPlanContextMenu}
activeTool={activeTool}
toolInputActive={toolInputActive}
toolHandlers={toolHandlers}
hudFields={hudFields}
grips={grips}
edgeGrips={edgeGrips}
selectedDrawingId={selectedDrawingId}
selectedDrawingIds={selectedDrawingIds}
gripHandlers={gripHandlers}
/>
)}
</main>
);
}
/** Abgeleiteter Grundriss eines Geschosses. */
function LevelPlanView({
project,
level,
visibleCodes,
categoryDisplay,
detail,
referenceLines,
hairline,
marqueeColor,
snapColor,
mono,
planViewRef,
onCursor,
onScale,
selectedWallIds,
selectedCeilingIds,
selectedOpeningIds,
selectedStairIds,
selectedExtrudedSolidIds,
selectedColumnIds,
selectedRoomIds,
selectedRoofIds,
selectedContextObjectIds,
selectedSectionLineId,
roomStamp,
onStampMove,
onStampEdit,
onEditDrawingText,
onOpenSectionLevel,
onPlanSelect,
onPlanMarquee,
onSegmentCut,
onPlanContextMenu,
activeTool,
toolInputActive,
toolHandlers,
hudFields,
grips,
edgeGrips,
selectedDrawingId,
selectedDrawingIds,
gripHandlers,
}: {
project: Project;
level: DrawingLevel;
visibleCodes: Set<string>;
categoryDisplay: CategoryDisplayResolver;
detail: DetailLevel;
referenceLines: boolean;
hairline: boolean;
marqueeColor: string;
snapColor: string;
mono: boolean;
planViewRef: React.RefObject<PlanViewHandle>;
onCursor: (model: Vec2 | null) => void;
onScale: (n: number) => void;
selectedWallIds: string[];
selectedCeilingIds: string[];
selectedOpeningIds: string[];
selectedStairIds: string[];
selectedExtrudedSolidIds: string[];
selectedColumnIds: string[];
selectedRoomIds: string[];
selectedRoofIds: string[];
selectedContextObjectIds?: string[];
selectedSectionLineId: string | null;
roomStamp: { roomId: string; anchor: Vec2 } | null;
onStampMove: (roomId: string, delta: Vec2) => void;
onStampEdit: (roomId: string) => void;
onEditDrawingText?: (drawingId: string) => void;
onOpenSectionLevel?: (id: string) => void;
onPlanSelect: (sel: PlanSelection) => void;
onPlanMarquee: (sel: MarqueeSelection) => void;
onSegmentCut: (drawingId: string, model: Vec2) => void;
onPlanContextMenu: (info: {
clientX: number;
clientY: number;
model: Vec2;
}) => void;
activeTool: ToolId;
toolInputActive: boolean;
toolHandlers: ToolHandlers;
hudFields: HudFieldsState | null;
grips: Vec2[];
edgeGrips: EdgeGrip[];
selectedDrawingId: string | null;
selectedDrawingIds?: string[];
gripHandlers: GripHandlers;
}) {
// Detailgrad + Referenzlinien fließen in die Plan-Erzeugung ein (Symbole,
// Schraffuren/Linienfeinheit bzw. Wand-Achslinien).
const plan = useMemo(
() =>
generatePlan(
project,
level.id,
visibleCodes,
categoryDisplay,
detail,
referenceLines,
mono,
),
[project, level.id, visibleCodes, categoryDisplay, detail, referenceLines, mono],
);
// Live-Spiegelung in die nativen wgpu-Fenster (No-op ohne Tauri, debounced):
// EXAKT der sichtbare Plan (gleiches Geschoss, gleicher Detailgrad) fürs
// 2D-Fenster, die Projekt-Wände fürs 3D-Fenster.
useEffect(() => {
pushNativeScene(plan);
}, [plan]);
useEffect(() => {
pushNativeWalls(project);
}, [project]);
// Bei einer freien Zeichnung gibt es keine Schnitthöhe → eigene Legende/Titel.
const isDrawing = level.kind === "drawing";
const caption = isDrawing
? t("content.drawingLegend")
: t("content.grundrissLegend", {
name: level.name,
cutHeight: formatM(level.cutHeight ?? 0),
});
return (
<section className="pane">
<div className="pane-title">
{isDrawing
? t("content.drawingTitle", { name: level.name })
: t("content.grundrissTitle", { name: level.name })}
</div>
<PlanView
ref={planViewRef}
plan={plan}
resetKey={level.id}
onCursor={onCursor}
onScale={onScale}
selectedWallIds={selectedWallIds}
selectedCeilingIds={selectedCeilingIds}
selectedOpeningIds={selectedOpeningIds}
selectedStairIds={selectedStairIds}
selectedExtrudedSolidIds={selectedExtrudedSolidIds}
selectedColumnIds={selectedColumnIds}
selectedRoomIds={selectedRoomIds}
selectedRoofIds={selectedRoofIds}
selectedContextObjectIds={selectedContextObjectIds}
selectedSectionLineIds={selectedSectionLineId ? [selectedSectionLineId] : []}
roomStamp={roomStamp}
onStampMove={onStampMove}
onStampEdit={onStampEdit}
onEditDrawingText={onEditDrawingText}
onOpenSectionLevel={onOpenSectionLevel}
onSelect={onPlanSelect}
onMarquee={onPlanMarquee}
onSegmentCut={onSegmentCut}
onContextMenu={onPlanContextMenu}
activeTool={activeTool}
toolInputActive={toolInputActive}
toolHandlers={toolHandlers}
hudFields={hudFields}
grips={grips}
edgeGrips={edgeGrips}
hairline={hairline}
marqueeColor={marqueeColor}
snapColor={snapColor}
selectedDrawingId={selectedDrawingId}
selectedDrawingIds={selectedDrawingIds}
gripHandlers={gripHandlers}
/>
<div className="plan-legend">{caption}</div>
</section>
);
}
/**
* Schnitt-/Ansichts-Ebene: der render3d-Schnitt-Extraktor (WASM, plan/toSection)
* durchschneidet das Modell entlang der Schnittlinie der Ebene und übersetzt das
* Ergebnis (Cut-Polygone + sichtbare/verdeckte Kanten) über generateSectionPlan
* in Plan-Primitive, die dieselbe PlanView wie ein Grundriss zeichnet.
*
* Die Berechnung ist asynchron (WASM lazy geladen); bis das Ergebnis da ist,
* bzw. wenn die Ebene keine Schnittlinie trägt oder das WASM-Modul den Export
* (noch) nicht kennt, wird ein Hinweis eingeblendet.
*/
function SectionPlanView({
project,
level,
hairline,
marqueeColor,
snapColor,
mono,
planViewRef,
onCursor,
onScale,
selectedWallIds,
selectedCeilingIds,
selectedOpeningIds,
selectedStairIds,
selectedExtrudedSolidIds,
selectedColumnIds,
selectedRoomIds,
selectedRoofIds,
selectedSectionLineId,
roomStamp,
onStampMove,
onStampEdit,
onEditDrawingText,
onOpenSectionLevel,
onPlanSelect,
onPlanMarquee,
onSegmentCut,
onPlanContextMenu,
activeTool,
toolInputActive,
toolHandlers,
hudFields,
grips,
edgeGrips,
selectedDrawingId,
selectedDrawingIds,
gripHandlers,
}: {
project: Project;
level: DrawingLevel;
hairline: boolean;
marqueeColor: string;
snapColor: string;
mono: boolean;
planViewRef: React.RefObject<PlanViewHandle>;
onCursor: (model: Vec2 | null) => void;
onScale: (n: number) => void;
selectedWallIds: string[];
selectedCeilingIds: string[];
selectedOpeningIds: string[];
selectedStairIds: string[];
selectedExtrudedSolidIds: string[];
selectedColumnIds: string[];
selectedRoomIds: string[];
selectedRoofIds: string[];
selectedSectionLineId: string | null;
roomStamp: { roomId: string; anchor: Vec2 } | null;
onStampMove: (roomId: string, delta: Vec2) => void;
onStampEdit: (roomId: string) => void;
onEditDrawingText?: (drawingId: string) => void;
onOpenSectionLevel?: (id: string) => void;
onPlanSelect: (sel: PlanSelection) => void;
onPlanMarquee: (sel: MarqueeSelection) => void;
onSegmentCut: (drawingId: string, model: Vec2) => void;
onPlanContextMenu: (info: { clientX: number; clientY: number; model: Vec2 }) => void;
activeTool: ToolId;
toolInputActive: boolean;
toolHandlers: ToolHandlers;
hudFields: HudFieldsState | null;
grips: Vec2[];
edgeGrips: EdgeGrip[];
selectedDrawingId: string | null;
selectedDrawingIds?: string[];
gripHandlers: GripHandlers;
}) {
// Die Ansicht (kind "elevation") läuft NICHT über den WASM-Schnitt, sondern
// über den rein TS-seitigen Flächen-Generator (Painter, toElevation.ts) —
// synchron, kein Lazy-Load. Nur der echte Schnitt braucht die WASM-Pipeline.
const isElevation = level.kind === "elevation";
const [output, setOutput] = useState<SectionOutput | null>(null);
const [status, setStatus] = useState<"loading" | "ready" | "empty" | "error">(
"loading",
);
useEffect(() => {
if (isElevation) return; // Ansicht: kein WASM-Schnitt (synchroner Generator).
let disposed = false;
setStatus("loading");
setOutput(null);
void (async () => {
try {
const out = await computeSection(project, level);
if (disposed) return;
if (!out) {
setStatus("empty");
return;
}
setOutput(out);
setStatus("ready");
} catch (e) {
console.warn("render3d-Schnitt fehlgeschlagen:", e);
if (!disposed) setStatus("error");
}
})();
return () => {
disposed = true;
};
}, [project, level, isElevation]);
// Ansichts-Plan: gefüllte Fassadenansicht inkl. optionalem Schatten
// (level.shadows). Liefert null, wenn die Ebene keine Ansichtslinie trägt.
const elevationPlan = useMemo(
() => (isElevation ? generateElevationPlan(project, level, { mono }) : null),
[isElevation, project, level, mono],
);
const sectionPlan = useMemo(
() =>
!isElevation && output
? generateSectionPlan(output, mono, { hiddenLines: level.hiddenLines })
: null,
[isElevation, output, mono, level.hiddenLines],
);
const plan = isElevation ? elevationPlan : sectionPlan;
const effStatus = isElevation ? (elevationPlan ? "ready" : "empty") : status;
const kindLabel = level.kind === "section" ? t("stub.section") : t("stub.elevation");
if (!plan || effStatus !== "ready") {
const note =
effStatus === "empty"
? t("section.empty", { kind: kindLabel })
: effStatus === "error"
? t("section.error", { kind: kindLabel })
: t("section.loading", { kind: kindLabel });
return (
<div className="placeholder">
<strong>{level.name}</strong>
<span>{note}</span>
</div>
);
}
return (
<section className="pane">
<div className="pane-title">
{t("content.sectionTitle", { name: level.name, kind: kindLabel })}
</div>
<PlanView
ref={planViewRef}
plan={plan}
resetKey={level.id}
onCursor={onCursor}
onScale={onScale}
selectedWallIds={selectedWallIds}
selectedCeilingIds={selectedCeilingIds}
selectedOpeningIds={selectedOpeningIds}
selectedStairIds={selectedStairIds}
selectedExtrudedSolidIds={selectedExtrudedSolidIds}
selectedColumnIds={selectedColumnIds}
selectedRoomIds={selectedRoomIds}
selectedRoofIds={selectedRoofIds}
selectedSectionLineIds={selectedSectionLineId ? [selectedSectionLineId] : []}
roomStamp={roomStamp}
onStampMove={onStampMove}
onStampEdit={onStampEdit}
onEditDrawingText={onEditDrawingText}
onOpenSectionLevel={onOpenSectionLevel}
onSelect={onPlanSelect}
onMarquee={onPlanMarquee}
onSegmentCut={onSegmentCut}
onContextMenu={onPlanContextMenu}
activeTool={activeTool}
toolInputActive={toolInputActive}
toolHandlers={toolHandlers}
hudFields={hudFields}
grips={grips}
edgeGrips={edgeGrips}
hairline={hairline}
marqueeColor={marqueeColor}
snapColor={snapColor}
selectedDrawingId={selectedDrawingId}
selectedDrawingIds={selectedDrawingIds}
gripHandlers={gripHandlers}
/>
<div className="plan-legend">{t("section.legend", { kind: kindLabel })}</div>
</section>
);
}