Compare commits
5 Commits
f95674aa09
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e73c684c3 | |||
| 6637533162 | |||
| 3b79e49cc6 | |||
| a6c2c04736 | |||
| 956a85d93f |
-19
@@ -12,25 +12,6 @@ scripts/*.png
|
||||
.DS_Store
|
||||
*.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)
|
||||
src-tauri/assets/native2d_scene.json
|
||||
src-tauri/assets/native3d_walls.json
|
||||
|
||||
+297
-359
@@ -1,452 +1,390 @@
|
||||
# Architektur — Standalone Browser-BIM (cad)
|
||||
# Architektur — Dossier (Desktop-CAAD)
|
||||
|
||||
> Stand: 2026-06-29
|
||||
> Vision, Phasen und Backlog: [ROADMAP.md](ROADMAP.md). Konventionen: [CONVENTIONS.md](CONVENTIONS.md).
|
||||
> Detail-Designs: [docs/design/elements.md](docs/design/elements.md) ·
|
||||
> [docs/design/plans-output.md](docs/design/plans-output.md) ·
|
||||
> [docs/design/resources-graphics.md](docs/design/resources-graphics.md).
|
||||
> Stand: 2026-07-21 (grundlegend überarbeitet — siehe [STATUS.md](STATUS.md) für
|
||||
> die volle Bestandsaufnahme inkl. Mist-Liste, die diese Überarbeitung begründet).
|
||||
> Vision/Phasen (historisch, Tag-1-Stand): [ROADMAP.md](ROADMAP.md). Konventionen:
|
||||
> [CONVENTIONS.md](CONVENTIONS.md). Detail-Designs (teils ebenfalls veraltet,
|
||||
> siehe Hinweis in [docs/README.md](docs/README.md)): [docs/design/](docs/design/).
|
||||
|
||||
Dieses Dokument beschreibt, **wie** die Standalone-Browser-App gebaut wird und
|
||||
wie sie die Konzepte des DOSSIER-Rhino-Plugins in Browser-Äquivalente übersetzt.
|
||||
DOSSIER ist ein Rhino-8-Plugin (Python + React-WebView); `cad` ist die *eigen
|
||||
ständige* Browser-Variante: kein Rhino-Document, kein `doc.Strings`, kein
|
||||
IronPython — stattdessen ein **eigenes typisiertes Datenmodell in TypeScript**,
|
||||
gerendert über **Three.js** (3D) und **SVG** (Plan), gespeichert als **JSON-Datei**.
|
||||
Dieses Dokument beschreibt, **wie** Dossier tatsächlich gebaut ist — nicht wie
|
||||
es am ersten Tag geplant war. Dossier ist die eigenständige Neuimplementierung
|
||||
des Rhino-Plugins [DOSSIER](https://git.openbureau.ch/karim/dossier): dieselbe
|
||||
Denkweise (Geschosse, Kategorie-Ebenen, mehrschichtige Bauteile,
|
||||
Prioritäts-Verschneidung), aber als **native Desktop-App** mit einem **eigenen
|
||||
typisierten Datenmodell in TypeScript** und **zwei eigenen Rust/WASM-Rendering-
|
||||
Engines** („Nordstern") statt Rhino-Dokument/IronPython.
|
||||
|
||||
Alle Bezeichner im Code sind **englisch** (Vectorworks-Terminologie); Prosa und
|
||||
UI-Texte sind deutsch. Einheiten intern in **Metern**.
|
||||
**Desktop-Rahmen (plattformabhängig, wegen WebGPU):** auf **macOS Tauri**
|
||||
(WKWebView unterstützt WebGPU), auf **Linux Electron/Chromium** (Tauris
|
||||
Linux-Webview WebKitGTK unterstützt WebGPU nicht zuverlässig — die
|
||||
render2d/render3d-Engines brauchen es). Beide teilen dieselbe React-App und
|
||||
randlose Titelleiste; Laufzeit-Erkennung über `window.__TAURI__` bzw.
|
||||
`window.dossierWindow` (Electron-`contextBridge`). Details: STATUS.md §2.7.
|
||||
|
||||
Alle Bezeichner im Code sind **englisch**; Prosa und UI-Texte sind deutsch.
|
||||
Einheiten intern in **Metern**.
|
||||
|
||||
---
|
||||
|
||||
## 0. Leitprinzip — ein Modell, viele Darstellungen
|
||||
|
||||
Das semantische Gebäudemodell ist die **einzige Wahrheit**. Jede Sicht (3D,
|
||||
Grundriss, Schnitt, Ansicht) ist eine **reine Ableitung** (`derive`) daraus.
|
||||
Das semantische Gebäudemodell (`Project`) ist die **einzige Wahrheit**. Jede
|
||||
Sicht (3D, Grundriss, Schnitt, Ansicht) ist eine **reine Ableitung** daraus.
|
||||
Darstellung (Detailgrad, Stile, Schraffuren, Overrides) wird **beim Rendern**
|
||||
angewandt, nie in die Geometrie eingebacken.
|
||||
angewandt, nie in die Geometrie eingebacken. Dieses Prinzip hat sich über drei
|
||||
Wochen und ~125.000 Zeilen Code bewährt und wird strikt gehalten — es ist der
|
||||
einzige Teil der ursprünglichen Architektur-Vision, der **unverändert** Bestand
|
||||
hat. Alle konkreten Technologie-Entscheidungen darunter (Rendering-Engine,
|
||||
State-Store, Schnitt-Mechanismus) sind anders gelaufen als am Tag 1 geplant;
|
||||
Details dazu in [STATUS.md](STATUS.md) §5.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ Project (semantisches Modell, JSON) │ ← einzige Wahrheit
|
||||
│ resources · types · designLevels · │
|
||||
│ layers · elements │
|
||||
│ resources · types · drawingLevels · │
|
||||
│ layers · walls/doors/openings/stairs/… │
|
||||
└──────────────┬───────────────────────────┘
|
||||
│ pure derive()
|
||||
┌───────────────────────┼────────────────────────────┐
|
||||
▼ ▼ ▼
|
||||
Scene3D (Three.js) PlanModel → SVG SectionModel → SVG
|
||||
buildScene() generatePlan() generateSection() (HLR)
|
||||
│ │ │
|
||||
└────────── beide lesen dieselben joins/components/styles ──────────┘
|
||||
┌───────────────────────┼────────────────────────────┬───────────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
3D-Viewport 2D-Plan (generatePlan) 3D-Live-Schnitt Export
|
||||
Viewport3D (three.js) PlanView (SVG) · glPlan (GL2) render3d/section IFC/DXF/
|
||||
ODER Wasm3DViewport ODER render2d (Rust/WGSL) (Rust, analytisch)PDF/STL
|
||||
(Rust/wgpu, Default)
|
||||
│ │ │ │
|
||||
└────────── alle lesen dieselben joins/components/styles ─────────────┘
|
||||
```
|
||||
|
||||
Das steht im Spike bereits: `generatePlan.ts` und `Viewport3D.tsx` extrudieren
|
||||
**dasselbe** gehrte Band-Polygon (`clippedBand`). Diese Symmetrie ist der Kern
|
||||
und wird beim Ausbau strikt gehalten.
|
||||
|
||||
---
|
||||
|
||||
## 1. Repo-Struktur (Ziel)
|
||||
|
||||
Wächst aus dem heutigen `src/` (model/plan/viewport/ui). Module sind **klein und
|
||||
fachlich geschnitten** — wir vermeiden bewusst den `elemente.py`-Monolithen
|
||||
(7244 LOC) aus DOSSIER (dort als Schwachstelle #4.1 dokumentiert).
|
||||
## 1. Repo-Struktur (IST-Zustand)
|
||||
|
||||
```
|
||||
src/
|
||||
model/
|
||||
types.ts // Project, DesignLevel, LayerCategory, Element-Union (existiert)
|
||||
geometry.ts // 2D/3D-Mathe ohne Kernel (existiert)
|
||||
joins.ts // Wand-Verschneidung (Gehrung; später Prio-T/X) (existiert)
|
||||
sampleProject.ts // Demo-Haus (existiert)
|
||||
project.ts // Factory, Defaults, Migrationen
|
||||
selectors.ts // abgeleitete Reads (visibleCodes, elementsOnLevel…)
|
||||
ids.ts // newId(prefix) — UUID-Erzeugung
|
||||
elements/ // pro Bauteil ein Modul (Daten + Generierung)
|
||||
wall.ts opening.ts slab.ts stair.ts roof.ts structure.ts space.ts
|
||||
resources/
|
||||
componentManager.ts hatchManager.ts lineManager.ts symbolLibrary.ts
|
||||
overrides.ts // regelbasierte Engine (Condition → Action)
|
||||
store/
|
||||
store.ts // Zustand-Store: { project, ui }, Aktionen, Undo/Redo
|
||||
persistence.ts // Datei save/load (JSON), Autosave (IndexedDB)
|
||||
history.ts // Undo/Redo-Ring
|
||||
plan/
|
||||
generatePlan.ts // Grundriss aus Footprint + Symbolik (existiert)
|
||||
generateSection.ts // Schnitt/Ansicht aus 3D-Projektion (HLR-Worker)
|
||||
PlanView.tsx // SVG-Renderer + Pan/Zoom/Grips (existiert)
|
||||
primitives.ts // Primitive-Union, SVG-Serializer, DXF/PDF-Export
|
||||
viewport/
|
||||
Viewport3D.tsx // Three.js-Szene aus dem Modell (existiert)
|
||||
scene.ts // buildScene(project) → THREE.Group (Layer-Spiegel)
|
||||
clip.ts // Plan-/Schnitt-Clipping über THREE.Plane
|
||||
camera.ts // Kamera-Presets, Norden-Rotation
|
||||
sheets/
|
||||
layout.ts // Sheet/Viewport-Datenmodell
|
||||
SheetEditor.tsx // Plansatz-Editor
|
||||
exportPdf.ts // Vektor-PDF (svg → pdf-lib / jsPDF)
|
||||
workers/
|
||||
geometry.worker.ts // Booleans (Öffnungen) + HLR via Comlink
|
||||
ui/
|
||||
App.tsx Navigator panels… // React-Oberfläche (heute in App.tsx, wird gesplittet)
|
||||
model/ Project-Schema (types.ts, ~2500 LOC), joins.ts (Wand-Gehrung/
|
||||
-Prio-Stösse), parametricWalls.ts, roomStamp.ts, terrain.ts,
|
||||
geoRebase.ts, sampleProject.ts
|
||||
geometry/ 2D-Kernel (kernel2d.ts: offset/trim/fillet/split — LIVE, siehe
|
||||
§3), ceiling/opening/roomArea/roomBoundary/stair/roof/column.ts,
|
||||
polygonHoles.ts
|
||||
commands/ Rhino-artiges Kommandosystem: types/registry/engine/parseInput.ts
|
||||
+ cmds/ (ein Modul je Kommando: wall, opening, stair, roof,
|
||||
column, room, line/rect/circle/arc, move/mirror/copy/offset/
|
||||
trim/join, extrude, import, terrain, measure, georef, …)
|
||||
tools/ Interaktive Zeichenwerkzeuge, snapping.ts, transform.ts (Grips)
|
||||
compute/ Compute-Boundary: leitet Ops (aktuell nur computeJoins) unter
|
||||
Tauri via `invoke` an Rust weiter, sonst TS-Fallback
|
||||
plan/ generatePlan.ts (2D-Ableitung), PlanView.tsx (SVG + Pan/Zoom/
|
||||
Grips), glPlan/ (eigener WebGL2-Renderer), toSection.ts/
|
||||
toElevation.ts (Schnitt/Ansicht-Ableitung), toWalls3d.ts,
|
||||
toRenderScene.ts (Szene für Rust-render2d), wallMeshCut.ts
|
||||
viewport/ Viewport3D.tsx (three.js, „Free") UND Wasm3DViewport.tsx
|
||||
(Rust/wgpu „Nordstern", Default/editierbar), raycast3d.ts
|
||||
section/ TOTER Code (OCCT-WASM-HLR-Spike, keine Aufrufer mehr) — Schnitt
|
||||
läuft über render3d/section.rs, siehe §4.3
|
||||
export/ exportIfc.ts (IFC4), exportDxf.ts/dxfWriter.ts, exportPdf.ts,
|
||||
layoutPdf.ts (Mehrseiten-PDF pro Ordner), exportMesh.ts (STL/OBJ),
|
||||
exportSchedule.ts (CSV-Bauteilliste), sceneToPrintSvg.ts
|
||||
materials/ ambientcg.ts (Live-Suche ambientCG-API), library.ts (13
|
||||
gebündelte Starter-Materialien), runtime.ts (PBR-Material aus
|
||||
ComponentMaterial via three.js TextureLoader)
|
||||
io/ DXF/DWG-Import, Swisstopo (swissBUILDINGS3D/-ALTI3D/SWISSIMAGE,
|
||||
LV95↔WGS84), OSM-Overpass, .lin/.pat-Parser, projectFile.ts
|
||||
(.obp Speichern/Laden, Tauri-Lock)
|
||||
overrides/ Regelbasierte Override-Engine (Bedingung → Aktion)
|
||||
state/ Eigener Store auf `useSyncExternalStore` (KEIN Zustand/Redux):
|
||||
store.ts + Slices (project/history/selection/view/layout/site/
|
||||
notify), appStore.ts komponiert sie
|
||||
panels/ Dock-/Floating-Panel-System (Dock/FloatingPanel/TabStrip/
|
||||
registry/layout) + Panels (Tools, Attributes, ObjectInfo, Layers,
|
||||
DrawingLevels, Site, RoomBalance, Elements, ViewSnapshots, Layouts)
|
||||
ui/ App.tsx (Shell, **7.130 LOC — noch nicht auf dünne Shell
|
||||
reduziert**, siehe STATUS.md §4.3), TopBar, ResourceManager.tsx
|
||||
(Material-/Hatch-/Line-/Typ-Editoren, natives Fenster),
|
||||
ContextMenu, CommandLine.tsx, LayoutSheet/-Menu, ribbon/
|
||||
native/ NUR Tauri: native Fenster (Resources/Settings/DrawingLevels/
|
||||
LayerSettings/ContextImport), macOS-Menüleiste, Fenster-Chrome —
|
||||
jede Funktion no-opt via `isTauriRuntime()` im Browser
|
||||
editors/ booleanOps.ts (2D-Boolean via polygon-clipping), splitJoin.ts
|
||||
text/ Rich-Text (richText.ts, RichTextEditor.tsx, renderHtml.ts)
|
||||
theme/ Hell/Dunkel, Akzentfarben
|
||||
i18n/ de.ts/en.ts Wörterbücher, eigener t()-Mechanismus
|
||||
engine/ Reine WASM-Lade-Glue: engine3d.ts (pkg3d), truckSolid.ts
|
||||
(pkgTruck), plan/useWasmPlanRenderer.ts (pkg) — pkgGeometry und
|
||||
pkgDwgImport sind gebaut, aber unbenutzt (siehe STATUS.md §4.1)
|
||||
|
||||
src-tauri/
|
||||
src/ Tauri-Host (Fenster, native Dialoge, fs4-Exklusiv-Lock)
|
||||
render2d/ 2D-Plan-GPU-Renderer (wgpu/WGSL, + glyphon-Text), auch → WASM
|
||||
render3d/ „Nordstern" 3D-Engine (wgpu/WGSL): Wandextrusion + Schicht-
|
||||
bänder + Gehrung, LIVE 2D=3D-Schnitt (section*.rs, analytisch,
|
||||
kein HLR), Kanten-Extraktion, Materialtextur-Arrays,
|
||||
Aerial-Drape, Render-Styles (Shaded/White/Textured/Wireframe/
|
||||
Hidden/ShadedEdges) — auch → WASM
|
||||
kernel2d/ Rust-Port von geometry/kernel2d.ts — NUR Paritätstest, nicht
|
||||
produktiv (WASM war < 100 Wänden langsamer als TS)
|
||||
geometry/ Wand-Join-Mathe — Rust-Port, UNBENUTZT (kein Aufrufer)
|
||||
trucksolid/ CSG/Extrusion (`truck`-Crate + `csgrs`-Booleans) → WASM,
|
||||
genutzt vom Extrude-Kommando; Boolean NICHT an Wände/
|
||||
Öffnungen angeschlossen
|
||||
dwgimport/ DXF-Parser-Spike (`acadrust`) → WASM, UNBENUTZT (DWG-Import
|
||||
läuft über npm `@mlightcad/libredwg-web`)
|
||||
```
|
||||
|
||||
Jedes Crate unter `src-tauri/` ausser dem Host ist ein **eigenständiges
|
||||
Cargo-Package** (kein gemeinsamer Workspace), das sowohl headless
|
||||
(`cargo test`) als auch per `wasm-pack --features web` baut und dann via
|
||||
`src/engine/pkg*/` von der TS-Seite geladen wird (`npm run build:engine{,3d,
|
||||
Geometry,Kernel2d,DwgImport}`/`build:truck`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Datenmodell
|
||||
|
||||
### 2.1 Zwei unabhängige Achsen (DOSSIER-Modell, im Spike umgesetzt)
|
||||
### 2.1 Zwei unabhängige Achsen (unverändert gegenüber der Vision)
|
||||
|
||||
DOSSIER trennt zwei orthogonale Konzepte, persistiert als zwei getrennte
|
||||
JSON-Bäume (`dossier_zeichnungsebenen`, `dossier_ebenen`). Wir übernehmen das
|
||||
1:1 in `types.ts` (existiert bereits als `drawingLevels` + `layers`):
|
||||
1. **Zeichnungsebenen** (`DrawingLevel`) — Geschoss (`kind:"floor"`,
|
||||
`floorHeight`/`cutHeight`/`baseElevation`), Schnitt/Ansicht
|
||||
(`kind:"section"|"elevation"`), oder freie Zeichnung (`kind:"drawing"`).
|
||||
2. **Ebenen** (`LayerCategory`) — das Grafik-Kategorie-Schema (Baum,
|
||||
`{code, name, color, lw, visible, locked, hatch?, children}`), in jedem
|
||||
Geschoss gültig. Codes 1:1 aus DOSSIER (`00 Raster · 01 Vermessung ·
|
||||
20 Wände (└21 Türen/Fenster, 25 Stützen) · 30 Decken · 31 Dächer ·
|
||||
40 Treppen · 50 Text · 60 Räume · 80 Plangrafik …`).
|
||||
|
||||
1. **Zeichnungsebenen** (`DesignLevel` / `DrawingLevel`) — die obersten
|
||||
Dokument-Abschnitte. Zwei produktive Arten:
|
||||
- **Geschoss** (`kind:"floor"`): `floorHeight`, `cutHeight`,
|
||||
`baseElevation` (akkumuliert via `recomputeFloorElevations`), `visible`/`locked`.
|
||||
- **Schnitt/Ansicht** (`kind:"section"|"elevation"`): `linePoints`,
|
||||
`directionSign`, Höhenbereich, Tiefe.
|
||||
- **Zeichnung** (`kind:"drawing"`): freie 2D-Ebene ohne Geschossbezug.
|
||||
2. **Ebenen** (`LayerCategory`) — das **Grafik-Kategorie-Schema**, das in *jedem*
|
||||
Geschoss gilt. Baum mit `{code, name, color, lw, visible, locked, hatch?, children}`.
|
||||
Codes 1:1 wie DOSSIER (`DEFAULT_LAYER_SCHEMA` aus `launcher/src/App.jsx`):
|
||||
`00 Raster · 01 Vermessung · 20 Wände (└21 Türen/Fenster, 22 Möbel, 25 Stützen) ·
|
||||
30 Decken · 31 Dächer · 35 Träger · 50 Text · 60 Plangrafik …`
|
||||
### 2.2 Elemente — typisierte Arrays statt `Element[]`-Union
|
||||
|
||||
Ein **Element** kennt sein **Geschoss** (`floorId`) *und* seine **Ebene**
|
||||
(`categoryCode`). Sichtbarkeit ergibt sich aus dem Schnitt (Geschoss `geschoss × code`
|
||||
Matrix), genau wie DOSSIERs `apply_visibility(z_mode, e_mode)` in `layer_builder.py`.
|
||||
|
||||
> **Begriffsnotiz:** Heute heißt der Typ im Code `DrawingLevel`. ROADMAP §5 nennt
|
||||
> die Modell-Eingabe (Geschosse) Vectorworks-konform **Design Layer** und die
|
||||
> abgeleitete Ausgabe **Drawing Layer / Sheet**. Wir behalten `DrawingLevel` als
|
||||
> Union (kind=floor ≙ Design Layer, kind=section/elevation/drawing ≙ Drawing
|
||||
> Layer) und führen `Sheet` erst separat ein (§2.5, plans-output.md). Kein Rename
|
||||
> ohne expliziten Auftrag.
|
||||
|
||||
### 2.2 Ressourcen-Bibliotheken (Vectorworks-Stil)
|
||||
|
||||
Neuer Block `Project.resources` (heute provisorisch als flache `materials[]`).
|
||||
Alles verweist **per id** — zentral änderbar. Details: resources-graphics.md.
|
||||
Anders als ursprünglich geplant hält `Project` (`src/model/types.ts:2095`)
|
||||
**pro Bauteiltyp ein eigenes (meist optionales) Array**:
|
||||
|
||||
```ts
|
||||
interface Resources {
|
||||
lineStyles: LineStyle[]; // Line Manager: { id, name, weight(mm), color, dash:number[] }
|
||||
hatches: Hatch[]; // Hatch Manager: { id, name, pattern, scale, angle, lineStyleId }
|
||||
components: Component[]; // Component Manager (= DOSSIER-Material erweitert):
|
||||
// { id, name, hatchId, color3d, texture3d?, joinPriority }
|
||||
interface Project {
|
||||
walls: Wall[];
|
||||
ceilings?: Ceiling[];
|
||||
roofs?: Roof[];
|
||||
doors: Door[]; // ÄLTER — siehe openings; bewusste Doppelspur
|
||||
openings?: Opening[]; // NEUER, allgemeiner: kind:"window"|"door"
|
||||
stairs?: Stair[];
|
||||
rooms?: Room[];
|
||||
columns?: Column[];
|
||||
extrudedSolids?: ExtrudedSolid[];
|
||||
drawings2d: Drawing2D[];
|
||||
context?: ContextObject[]; // Terrain/Importe — NICHT semantisch
|
||||
parametricWalls?: ParametricWall[];
|
||||
overrideRules?: OverrideRule[];
|
||||
viewSnapshots?: ViewSnapshot[];
|
||||
viewSnapshotFolders?: ViewSnapshotFolder[];
|
||||
layouts?: Layout[];
|
||||
masterLayouts?: MasterLayout[];
|
||||
layoutFolders?: LayoutFolder[];
|
||||
// + Bibliotheken: lineStyles, hatches, components, wallTypes, roofTypes?,
|
||||
// doorTypes?, windowTypes?, stairTypes?, ceilingTypes?
|
||||
// + drawingLevels, layers, geoAnchor?, referenceElevationMasl?
|
||||
}
|
||||
```
|
||||
|
||||
`joinPriority` ist DOSSIERs `_MATERIAL_PRIO` (Beton 800 … Putz 100) als
|
||||
**Daten** statt Hardcode — steuert die Prioritäts-T-Verschneidung (Risiko #1).
|
||||
Ein `Element`-Typalias existiert noch (`kind:"door"|"window"` etc.), wird aber
|
||||
nirgends im Code referenziert — Selektion/Element-Baum arbeiten direkt auf den
|
||||
typisierten Arrays. `doors`/`openings` sind eine **bekannte, noch nicht
|
||||
konsolidierte Doppelspur** (PENDENZEN.md).
|
||||
|
||||
### 2.3 Aufbauten (mehrschichtige Typen)
|
||||
### 2.3 Ressourcen & Aufbauten (unverändert gegenüber der Vision)
|
||||
|
||||
```ts
|
||||
interface Resources { lineStyles: LineStyle[]; hatches: Hatch[]; components: Component[]; }
|
||||
interface WallType { id; name; layers: { componentId; thickness }[]; }
|
||||
interface SlabType { id; name; layers: { componentId; thickness }[]; }
|
||||
```
|
||||
|
||||
Schichtdicke liegt am Layer, **Priorität am Component** (so muss man Prio nur
|
||||
einmal pflegen). 3D und Plan lesen dieselben `layers[]`.
|
||||
`joinPriority` sitzt am `Component` (Daten statt Hardcode) und steuert die
|
||||
Prioritäts-T-/X-Verschneidung — **fertig implementiert**, inklusive Rust-Port
|
||||
für den 3D-Live-Schnitt (`section_boolean.rs` = Port von
|
||||
`toSection.ts::subtractDominantBands`).
|
||||
|
||||
### 2.4 Elemente
|
||||
### 2.4 Layouts / Ausschnitte
|
||||
|
||||
Diskriminierte Union `Element` (heute `Wall | Door`), erweitert um
|
||||
`Window | Slab | Stair | Roof | Column | Beam | Space | Draw2d`. Jedes Element:
|
||||
`{ id, type, floorId, categoryCode, ... }`. Gehostete Elemente (Tür/Fenster)
|
||||
tragen `hostWallId` statt `floorId` (Geschoss ergibt sich aus der Wand). Volle
|
||||
Felder: elements.md.
|
||||
|
||||
### 2.5 Pläne / Output (eigene Achse)
|
||||
|
||||
```ts
|
||||
interface Sheet { // Plansatz-Blatt (≙ DOSSIER Layout/PageView)
|
||||
id; name; paper: "A0".."A4"|"Letter"; landscape: boolean;
|
||||
viewports: SheetViewport[]; // platzierte Ableitungen + Titelblock
|
||||
folder?: string;
|
||||
}
|
||||
interface SheetViewport { // ≙ DOSSIER Detail + gebundener Ausschnitt
|
||||
id; rect: Rect; sourceViewId: string; // referenziert DrawingLevel oder ViewSnapshot
|
||||
scale: number; // 1:N
|
||||
}
|
||||
interface ViewSnapshot { // ≙ DOSSIER Ausschnitt (View-Snapshot)
|
||||
id; name; folder?;
|
||||
camera: CameraState; scale: number; detailLevel: DetailLevel;
|
||||
visibility: VisibilityState; // pro Geschoss/Ebene
|
||||
overrides?: { presetId?; enabled };
|
||||
}
|
||||
```
|
||||
`ViewSnapshot` (Kamera/Massstab/Detailgrad/Sichtbarkeiten/Override-Preset) in
|
||||
`ViewSnapshotFolder`-Bäumen; `Layout` (Papierformat, mehrere
|
||||
`LayoutViewport`s, freie `LayoutAnnotation`s, optionale `MasterLayout`-Vererbung)
|
||||
in `LayoutFolder`-Bäumen. Beide gehören ins Dokument (nicht LocalStorage).
|
||||
|
||||
---
|
||||
|
||||
## 3. State, Persistenz, Undo/Redo
|
||||
|
||||
### 3.1 Store — Zustand
|
||||
### 3.1 Store — eigener `useSyncExternalStore`, nicht Zustand
|
||||
|
||||
Heute: `useState<Project>` in `App.tsx`, immutabel via `setProject`. Das skaliert
|
||||
nicht für Werkzeuge + Undo. Migration auf **Zustand** (ROADMAP §4):
|
||||
Entgegen der ursprünglichen Empfehlung (Zustand-Library) wurde ein
|
||||
**abhängigkeitsfreier Store** gebaut (`src/state/store.ts`, gleiches Muster wie
|
||||
`src/i18n`): `createStore()` komponiert Slice-Fabriken über eine gemeinsame
|
||||
`RootState`. `appStore.ts` fügt zusammen: `projectSlice` (Projekt + Undo/Redo,
|
||||
`setProject` als einziger Mutations-Einstiegspunkt), `historySlice`,
|
||||
`selectionSlice`, `viewSlice`, `layoutSlice`, `siteSlice`, `notifySlice`
|
||||
(Toast/Confirm-Ersatz für `window.alert`, in Tauris WKWebView deaktiviert).
|
||||
Komponenten lesen über `useStore(selector)`.
|
||||
|
||||
```ts
|
||||
interface AppState {
|
||||
project: Project; // das Dokument
|
||||
ui: { // flüchtig, NICHT persistiert/undobar
|
||||
activeLevelId; activeViewType; selection: string[];
|
||||
activeTool: ToolId; hover; cameraByView; planTransform;
|
||||
};
|
||||
// Aktionen mutieren via Immer-Producer; jede Modell-Mutation pusht History.
|
||||
apply(mutator: (p: Project) => void): void;
|
||||
undo(): void; redo(): void;
|
||||
}
|
||||
```
|
||||
**Nicht umgesetzt** (geplant in `docs/design/state-architecture.md`): die
|
||||
Extraktion von View-Routing nach `src/views/` und Kontextmenü-Aufbau nach
|
||||
`src/menus/` — beide Ordner existieren nicht, diese Logik liegt weiterhin
|
||||
inline in `App.tsx` (7.130 Zeilen). Siehe STATUS.md §4.3.
|
||||
|
||||
**Trennung Modell ↔ UI** ist hart: nur `project` ist undobar und wird gespeichert.
|
||||
`ui` (Auswahl, Kamera, aktives Werkzeug) lebt separat. Das ersetzt DOSSIERs
|
||||
`sc.sticky`-Bus — wo DOSSIER cross-modul über Sticky-Keys kommuniziert, lesen bei
|
||||
uns einfach alle Komponenten denselben Store (reaktiv). **Kein Sticky, kein
|
||||
Bridge-Polling, keine `is not None`-Lücken** (DOSSIER-Schwachstelle #4.4).
|
||||
### 3.2 Persistenz
|
||||
|
||||
### 3.2 Persistenz — JSON-Datei statt `doc.Strings`
|
||||
|
||||
DOSSIER speichert *alles* in `doc.Strings` (Key-Value am Rhino-Document) +
|
||||
globale Presets als JSON-Dateien im User-Home. Browser-Äquivalent:
|
||||
|
||||
| DOSSIER | Browser (cad) |
|
||||
|---|---|
|
||||
| `doc.Strings.SetString(key, json)` | Feld im `Project`-Objekt (ein JSON-Baum) |
|
||||
| `.3dm`-Datei mit eingebetteten Strings | **`.cad.json`-Datei** via File System Access API |
|
||||
| Globale Presets (`~/Library/.../*.json`) | **LocalStorage** (`cad.presets.*`) + Export/Import |
|
||||
| Launcher schreibt Settings-Datei | **IndexedDB** Autosave (Phase 5) |
|
||||
|
||||
```ts
|
||||
// persistence.ts
|
||||
const FORMAT_VERSION = 1;
|
||||
function serialize(p: Project): string // JSON.stringify({ version, project })
|
||||
function deserialize(text: string): Project // + migrate(version) bis aktuell
|
||||
async function saveToFile(p: Project): Promise<void> // showSaveFilePicker → .cad.json
|
||||
async function loadFromFile(): Promise<Project> // showOpenFilePicker; Fallback <input type=file>
|
||||
async function autosave(p: Project) // IndexedDB, debounced 2 s
|
||||
```
|
||||
|
||||
- **Save/Load:** [File System Access API](https://developer.mozilla.org/docs/Web/API/File_System_Access_API)
|
||||
(`showSaveFilePicker`/`showOpenFilePicker`); Fallback Download-Blob + `<input type=file>`
|
||||
für Firefox/Safari.
|
||||
- **Autosave/Recovery:** IndexedDB (via `idb`), entkoppelt vom expliziten Speichern.
|
||||
- **Cross-Projekt-Presets:** LocalStorage mit Namespacing (`cad.presets.overrides`,
|
||||
`cad.presets.wallTypes`, …) + JSON-Export/Import — ersetzt DOSSIERs
|
||||
`~/Library/.../override_presets.json` (overrides.py).
|
||||
- **Migrationen:** `migrate(data, fromVersion)`-Kette, additiv. DOSSIER macht das
|
||||
per Sticky-Prefix (`traite_`→`pause_`→`dossier_`); wir machen es versioniert im
|
||||
Datei-Schema.
|
||||
- **Datei:** eigenes `.obp`-Format (`src/io/projectFile.ts`), unter Tauri über
|
||||
native Speichern/Öffnen-Dialoge (`plugin-fs`/`plugin-dialog`), im Browser
|
||||
Blob-Fallback. Ältere `.json`-Projekte bleiben ladbar.
|
||||
OS-Level-Exklusiv-Lock (`fs4`-Crate, `src-tauri/src/lock.rs`) verhindert
|
||||
Doppelöffnen desselben Projekts.
|
||||
- **Compute-Boundary:** `src/compute/index.ts` leitet einzelne Operationen
|
||||
(aktuell nur `computeJoins`) unter Tauri via `invoke` an eine native Rust-
|
||||
Implementierung weiter; im Browser bzw. für nicht migrierte Ops (Room-
|
||||
Detection, DWG-Parsing) läuft die TS-Implementierung.
|
||||
|
||||
### 3.3 Undo/Redo
|
||||
|
||||
DOSSIER überlässt Undo Rhino (und hat dadurch Cache-Stale-Bugs, Schwachstelle
|
||||
#4.3). Wir besitzen das Dokument selbst → **eigener History-Ring**:
|
||||
|
||||
```ts
|
||||
// history.ts — Snapshots des project-Teilbaums (strukturelles Sharing via Immer-Patches)
|
||||
interface History { undo: Patch[][]; redo: Patch[][]; }
|
||||
```
|
||||
|
||||
Jede `apply()`-Mutation erzeugt Immer-Patches → Push auf `undo`. Da abgeleitete
|
||||
Sichten **pure** sind, ist nach `undo()` kein Cache zu invalidieren — der
|
||||
ganze Cache-Stale-Komplex aus DOSSIER (`_JOINTS_CACHE_KEY`, Material-Cache)
|
||||
entfällt strukturell.
|
||||
Eigener History-Ring im `projectSlice` (kein DOSSIER-Sticky-Bus, kein
|
||||
Cache-Stale-Problem, da alle Sichten pure Ableitungen sind).
|
||||
|
||||
---
|
||||
|
||||
## 4. Rendering
|
||||
|
||||
### 4.1 Three.js-Szene spiegelt den Ebenen-Baum
|
||||
### 4.1 Zwei 3D-Viewports
|
||||
|
||||
DOSSIER baut in Rhino eine **Layer-Hierarchie** `Zeichnungsebene → CODE_Name-Sublayer`
|
||||
(`layer_builder.build_layers`) und hängt jedes Objekt an den passenden Sublayer.
|
||||
Browser-Spiegel: ein **`THREE.Group`-Baum** mit identischer Struktur, gebaut aus
|
||||
dem Modell (nicht persistiert — reine Ableitung):
|
||||
- **`Viewport3D.tsx`** — three.js, die „Free"-Stufe.
|
||||
- **`Wasm3DViewport.tsx`** — Rust/wgpu, „Nordstern", editierbar, **Default**.
|
||||
Ein Settings-Schalter wählt die Engine; WebGL2/three.js nur noch als
|
||||
explizite Wahl oder Fallback.
|
||||
|
||||
`render3d` (10.246 LOC) baut Wandextrusion inkl. Schichtbändern und
|
||||
Prioritäts-Gehrung (`compute_wall_miters`), Materialtextur-Arrays (pro
|
||||
Wandschicht) + separate Aerial-Drape-Textur, Render-Styles (Shaded/White/
|
||||
Textured/Wireframe/Hidden/ShadedEdges via Kanten-Extraktion mit
|
||||
Crease-Erkennung). **Dächer/Treppen/Stützen haben KEINE eigene Rust-Geometrie**
|
||||
— sie werden vollständig in TypeScript erzeugt (`geometry/roof.ts`,
|
||||
`emitRoofs`/`emitColumns`) und Rust nur als vorberechnetes Dreiecks-Mesh
|
||||
(`MeshInput`/`append_context_mesh`, derselbe generische Importpfad wie für
|
||||
swissBUILDINGS3D/DXF) übergeben.
|
||||
|
||||
### 4.2 Grundriss = drei koexistierende Renderer
|
||||
|
||||
1. **`PlanView.tsx`** (SVG) — bleibt immer im DOM (Hit-Testing/Grips),
|
||||
unabhängig vom aktiven Zeichenpfad.
|
||||
2. **`plan/glPlan/`** — eigener TypeScript-WebGL2-Renderer.
|
||||
3. **`useWasmPlanRenderer.ts`** → Rust-`render2d` (WGSL, wgpu nativ,
|
||||
WebGPU/WebGL2-Fallback im Web, echtes Text-Rendering via `glyphon`).
|
||||
|
||||
Beide GPU-Pfade fallen bei Init-Fehler still auf SVG zurück.
|
||||
|
||||
### 4.3 Schnitt/Ansicht — analytische Rust-Pipeline, KEIN HLR
|
||||
|
||||
Der ursprünglich geplante OCCT-WASM-HLR-Pfad (`src/section/hlr.ts`/`occt.ts`)
|
||||
ist **toter Code** (keine Aufrufer mehr). Der tatsächlich funktionierende
|
||||
Live-Schnitt nutzt aus, dass jedes Bauteil ein Prisma mit konstantem
|
||||
Querschnitt ist — eine Schnittebene liefert dadurch **immer** ein
|
||||
achsparalleles Rechteck, nie ein Trapez, wodurch HLR unnötig wird:
|
||||
|
||||
```
|
||||
scene
|
||||
└─ levelGroup[floorId] (y-Offset = baseElevation) ← Geschoss
|
||||
└─ categoryGroup[categoryCode] (userData.code) ← Ebene
|
||||
└─ element meshes (userData.elementId)
|
||||
App.tsx (section3dCutId/section3dPlane)
|
||||
→ Wasm3DViewport.tsx (section3d-Prop → setSectionPlane)
|
||||
→ src-tauri/render3d/src/{section.rs, section_boolean.rs, section_fill.rs}
|
||||
```
|
||||
|
||||
```ts
|
||||
// scene.ts
|
||||
function buildScene(project: Project, vis: VisibilityState): THREE.Group
|
||||
function syncScene(root: THREE.Group, project, vis) // diff statt full rebuild (Perf)
|
||||
```
|
||||
`section_boolean.rs` ist ein 1:1-Port von `toSection.ts::subtractDominantBands`
|
||||
— 2D-Plan-Schnitt und 3D-Live-Schnitt nutzen dieselbe Prioritäts-Logik und
|
||||
stimmen dadurch exakt überein. Kein Worker, kein Comlink (beides war geplant,
|
||||
existiert nirgends im Projekt) — läuft synchron GPU-seitig.
|
||||
|
||||
- **Sichtbarkeit:** `categoryGroup.visible = vis.codes.has(code)` und
|
||||
`levelGroup.visible = vis.floors.has(floorId)` — entspricht DOSSIERs
|
||||
`apply_visibility`, aber als simples `.visible`-Toggle statt Layer-Table-Modify.
|
||||
- **Farbe/Material:** Component → `MeshStandardMaterial` (PBR später), per
|
||||
`componentId` gecacht (wie heute `layerMaterial`-Cache, aber projektweit).
|
||||
- **„Grau/gesperrt"-Modi** (DOSSIER `grey`/`grey_locked`): graues Override-Material
|
||||
auf der Gruppe statt Layer-Color-Tausch.
|
||||
### 4.4 Öffnungen als Löcher (kein Mesh-Boolean)
|
||||
|
||||
### 4.2 Grundriss = Clipping-Ebene + symbolische Generierung
|
||||
Fenster/Türen schneiden echte achsparallele Rechteck-Löcher aus dem Wandkörper
|
||||
(`plan/toWalls3d.ts` `subtractSpans`, gespiegelt in `render3d::mesh.rs`).
|
||||
Ein generisches Mesh-CSG-Boolean existiert bereits (`trucksolid::boolean_mesh`,
|
||||
`csgrs`, für das Extrude-Kommando genutzt), ist aber nicht an die
|
||||
Wand/Öffnungs-Pipeline angeschlossen.
|
||||
|
||||
DOSSIER zeigt den Grundriss eines Geschosses, indem es eine **Clipping-Plane** auf
|
||||
`okff + schnitthöhe` legt, Normale −Z (`layer_builder.update_clipping_plane`).
|
||||
Zwei Browser-Pfade — **beide nötig** (ROADMAP §3):
|
||||
### 4.5 Materialien
|
||||
|
||||
1. **3D-Viewport im Plan-Modus:** `THREE.Plane(normal=(0,0,-1), constant=cutZ)` über
|
||||
`renderer.clippingPlanes` + `material.clippingPlanes`. `clip.ts` setzt die Ebene
|
||||
pro aktivem Geschoss (`cutZ = baseElevation + cutHeight`), Kamera orthografisch
|
||||
von oben. So sieht man den **echten geschnittenen Volumenkörper**.
|
||||
2. **Symbolischer SVG-Grundriss** (der Hauptweg, schnell + sauber): `generatePlan.ts`
|
||||
erzeugt Vektor-Primitive **direkt aus Parametern** — Wand-Schnittbänder,
|
||||
Öffnungs-Aussparungen, Tür-Schwenkbögen — *ohne* Mesh zu schneiden. Steht im
|
||||
Spike. Ausbau: Schraffuren, Lauflinien, Bemaßung, LoD (plans-output.md).
|
||||
|
||||
### 4.3 Schnitt/Ansicht = Kamera + Schnittebenen + HLR
|
||||
|
||||
DOSSIER setzt 1–2 Clipping-Planes (Cut + Back), Parallel-Projektion senkrecht zur
|
||||
Linie, zoomt auf die BBox (`schnitte.activate_schnitt`). Browser:
|
||||
|
||||
- **3D-Vorschau:** zwei `THREE.Plane` (Cut auf der Linie, Back in `directionSign`-
|
||||
Richtung um `depthBack` versetzt) + orthografische Kamera senkrecht zur Linie.
|
||||
- **Vektor-Ergebnis (Risiko #4):** Hidden-Line-Removal durch das zusammengebaute
|
||||
Gebäude → saubere Linien als SVG. Via **OpenCascade.js** (`HLRBRep`) im
|
||||
**Web Worker** (Comlink), Ergebnis gecacht pro (Schnitt, sichtbare Ebenen,
|
||||
Modell-Hash). Geschnittene Bauteile bekommen Component-Schraffur (Section-Style,
|
||||
resources-graphics.md). Details: plans-output.md.
|
||||
|
||||
### 4.4 SVG-Renderer + Geometrie-Booleans
|
||||
|
||||
- **Plan-Output:** SVG (`PlanView.tsx`). `Primitive`-Union (polygon/line/arc/text/
|
||||
hatch) → SVG-Elemente; derselbe Serializer speist DXF- und PDF-Export.
|
||||
- **Öffnungs-Booleans (Risiko #2):** Tür/Fenster schneidet Loch in Wand. Für 3D
|
||||
reicht heute das Aussparen per Segment-Extrusion (im Spike). Für exakte B-Rep-
|
||||
Verschneidung (und IFC) **OpenCascade.js** *oder* **Manifold** im Worker —
|
||||
Entscheidung in Phase 0 (ROADMAP §8): OCC = exakt/B-Rep, Manifold = schnell/Mesh.
|
||||
`materials/library.ts` (13 gebündelte PBR-Starter, ambientCG CC0) +
|
||||
`materials/ambientcg.ts` (Live-Suche der kompletten ambientCG-Bibliothek,
|
||||
1K/2K/4K-Auflösungswahl, On-Demand-Download via `jszip`, Proxy wegen CORS) →
|
||||
`materials/runtime.ts` baut daraus gecachte `THREE.MeshStandardMaterial`s mit
|
||||
physisch korrekter Kachelgrösse (UV in Weltmetern).
|
||||
|
||||
---
|
||||
|
||||
## 5. React-Panel-Struktur
|
||||
|
||||
DOSSIER ist eine Sammlung **getrennter WebView-Panels** (EBENEN, ELEMENTE,
|
||||
GESTALTUNG, OBERLEISTE, MASSSTAB, AUSSCHNITTE, DIMENSIONEN, LAYOUTS, OVERRIDES),
|
||||
die über die Python-Bridge + `sc.sticky` kommunizieren. Im Browser ist alles
|
||||
**eine SPA** mit einem geteilten Store — die Panels werden zu Docking-Bereichen.
|
||||
Dock-/Floating-Panel-System (`src/panels/`: Dock, FloatingPanel, TabStrip,
|
||||
Registry) mit eingebauten Panels: Tools, Attributes, ObjectInfo,
|
||||
DrawingLevels, Layers, Site (Kontext/Terrain-Import), RoomBalance
|
||||
(SIA-416-CSV), Elements (Bauteilbaum), ViewSnapshots, Layouts.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ TopBar Werkzeuge · Ansichtstyp · Massstab · Snaps · Save/Load │ ≙ OBERLEISTE+MASSSTAB
|
||||
├──────────────┬──────────────────────────────────────┬───────────────┤
|
||||
│ Navigator │ Viewport (Three.js oder SVG-Plan) │ Inspector │
|
||||
│ ┌──────────┐ │ ┌────────────────────────────────┐ │ ┌───────────┐ │
|
||||
│ │Zeichnungs│ │ │ 3D / Grundriss / Schnitt / │ │ │ aktives │ │ ≙ ELEMENTE/
|
||||
│ │-ebenen │ │ │ Ansicht — abgeleitet aus dem │ │ │ Element + │ │ GESTALTUNG-
|
||||
│ ├──────────┤ │ │ Modell │ │ │ Stil │ │ Properties
|
||||
│ │ Ebenen │ │ └────────────────────────────────┘ │ └───────────┘ │
|
||||
│ │ (Baum) │ │ │ Manager-Tabs: │
|
||||
│ │ │ │ │ Components · │ ≙ Resource-
|
||||
│ └──────────┘ │ │ Hatch · Line │ Manager
|
||||
├──────────────┴──────────────────────────────────────┴───────────────┤
|
||||
│ BottomBar Koordinaten · Snaps · Statusmeldungen │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
Modal/Drawer: Ausschnitte · Sheets/Layouts · Overrides · Kamera-Presets · SIA-Bilanz
|
||||
```
|
||||
Der **Resource Manager läuft bewusst NICHT als Dock-Panel**, sondern als
|
||||
eigenständiges (unter Tauri natives) Fenster — ebenso Settings,
|
||||
DrawingLevels-Detaileditor, LayerSettings und ContextImport
|
||||
(`src/native/*Window.ts` + `*WindowApp.tsx`), jeweils mit
|
||||
`isTauriRuntime()`-Gate und ohne separate Browser-Variante (im Browser bleibt
|
||||
die entsprechende In-App-Overlay-Variante aktiv, wo vorhanden).
|
||||
|
||||
**Komponenten-Map** (heute alles in `App.tsx`; wird gesplittet):
|
||||
|
||||
| Bereich | DOSSIER-Panel | cad-Komponente |
|
||||
|---|---|---|
|
||||
| Zeichnungsebenen-Liste | EBENEN (oben) | `Navigator/DrawingLevels.tsx` |
|
||||
| Ebenen-Baum | EBENEN (Baum) | `Navigator/LayerTree.tsx` (Spike: `CategoryRow`) |
|
||||
| Top-Bar (View/Snaps/Massstab) | OBERLEISTE, MASSSTAB | `TopBar.tsx` |
|
||||
| Element-Eigenschaften | ELEMENTE, ELEMENT-PROPERTIES | `Inspector/ElementProps.tsx` |
|
||||
| Stil/Attribute der Auswahl | GESTALTUNG | `Inspector/StylePanel.tsx` |
|
||||
| Component/Hatch/Line | (Project-Settings, mass_style) | `managers/*Manager.tsx` |
|
||||
| View-Snapshots | AUSSCHNITTE | `panels/Snapshots.tsx` |
|
||||
| Plansätze | LAYOUTS | `sheets/SheetEditor.tsx` |
|
||||
| Regelbasierte Overrides | OVERRIDES | `panels/Overrides.tsx` |
|
||||
| Bemaßung | DIMENSIONEN | `panels/Dimensions.tsx` |
|
||||
| Element-Übersicht (BIM-Tree) | ELEMENTE-ÜBERSICHT | `panels/ElementTree.tsx` |
|
||||
| Kamera-Presets | KAMERA | `panels/CameraPresets.tsx` |
|
||||
|
||||
**Werkzeug-System** (ersetzt DOSSIERs Rhino-Command-Aliases `cmd/wand.py` etc.):
|
||||
ein `Tool`-Interface mit `onPointerDown/Move/Up`, das Vorschau-Primitive
|
||||
zurückgibt und beim Bestätigen `store.apply()` ruft. Pointer-Events auf Canvas/SVG
|
||||
mit Snap-Engine (Endpunkt/Mitte/Schnitt/Ortho/Raster). Tabelle der Tools:
|
||||
elements.md §8.
|
||||
**Werkzeug-System:** `Tool`-Interface (`src/tools/types.ts`) für Zeichenwerkzeuge
|
||||
mit Snap-Engine; daneben das umfangreichere **Kommandosystem** (`src/commands/`)
|
||||
für Rhino-artige getippte Eingabe (`5,3`/`r5,3`/`5<45`, Tab-Feld-Zyklus in
|
||||
`CommandLine.tsx`) — beide koexistieren, decken unterschiedliche
|
||||
Interaktionsstile ab.
|
||||
|
||||
---
|
||||
|
||||
## 6. Rhino → Browser — Mapping-Tabelle
|
||||
## 6. Rhino → Dossier — Mapping-Tabelle (Kernkonzepte)
|
||||
|
||||
| DOSSIER (Rhino-Plugin) | cad (Standalone-Browser) | Anmerkung |
|
||||
| DOSSIER (Rhino-Plugin) | Dossier (Tauri) | Anmerkung |
|
||||
|---|---|---|
|
||||
| **Rhino `RhinoDoc`** | `Project` (TS-Objekt im Store) | einzige Wahrheit |
|
||||
| **`doc.Strings[key]=json`** | Feld im `Project`-JSON | persistiert in `.cad.json` |
|
||||
| **`.3dm`-Datei** | **`.cad.json`** via File System Access API | + IndexedDB-Autosave |
|
||||
| **Globale Presets (~/Library/*.json)** | **LocalStorage** + Export/Import | cross-Projekt |
|
||||
| **`sc.sticky` (cross-modul Bus)** | **Zustand-Store** (reaktiv) | kein Polling, kein None-Bug |
|
||||
| **`panel_base.BaseBridge` / WebView-IPC** | direkte React-Props/Store | keine `document.title`-Hacks |
|
||||
| **`document.title="RHINOMSG::"` Polling** | entfällt | SPA, kein WebView |
|
||||
| **Eto.Forms Satelliten-Fenster** | React-Modal/Drawer | z.B. Ausschnitt-Settings |
|
||||
| **Rhino Layer-Tabelle (Sublayer-Baum)** | `LayerCategory[]`-Baum + `THREE.Group`-Spiegel | `scene.ts` |
|
||||
| **`layer_builder.build_layers`** | `buildScene` (Ableitung) | Gruppen statt Layer |
|
||||
| **`layer.PlotWeight`** | `LineStyle.weight` (mm) → SVG `stroke-width` | massstabsabh. (§plans) |
|
||||
| **`SectionStyle` (Layer)** | Component-Schraffur beim Schnitt-Rendern | resources-graphics.md |
|
||||
| **Clipping-Plane (`AddClippingPlane`)** | `THREE.Plane` + `renderer.clippingPlanes` | `clip.ts` |
|
||||
| **`vp.ChangeToParallelProjection`** | `THREE.OrthographicCamera` | `camera.ts` |
|
||||
| **`vp.GetFrustum()` → Massstab** | Frustum-Breite (ortho) → 1:N | gleiche Mathe (§plans) |
|
||||
| **CoreGraphics DPI-Auto-Detect** | `window.devicePixelRatio` + CSS-px | Browser kennt DPI nativ |
|
||||
| **`Rhino.Geometry.Brep`-Booleans** | OpenCascade.js / Manifold (Worker) | Öffnungen, HLR |
|
||||
| **`HLRBRep` (über Rhino-Display)** | OpenCascade.js `HLRBRep` (Worker) | Schnitt/Ansicht-Linien |
|
||||
| **Rhino-Grips + DisplayConduit** | Pointer-Events auf SVG/Canvas + Overlay | grip-editing (elements.md) |
|
||||
| **`Rhino.Input.Custom.GetPoint`** | `Tool`-Pointer-Handler + Snap | Werkzeug-System |
|
||||
| **`MouseCallback` (Doppelklick Schnitt)** | `onDoubleClick` auf SVG-Symbol | plans-output.md |
|
||||
| **Rhino Undo/Redo** | eigener History-Ring (Immer-Patches) | kein Cache-Stale |
|
||||
| **`HatchPattern`-Tabelle** | `Hatch`-Ressourcen + SVG `<pattern>` | resources-graphics.md |
|
||||
| **`Linetype`-Tabelle** | `LineStyle.dash[]` → SVG `stroke-dasharray` | resources-graphics.md |
|
||||
| **`TextEntity` / Rich-Text** | SVG `<text>` / HTML-Annotation | plans-output.md |
|
||||
| **`RhinoPageView` (Layout)** | `Sheet` + `SheetEditor` | plans-output.md |
|
||||
| **`FilePdf` Multi-Page-Export** | svg → `pdf-lib`/`jsPDF` (Vektor) | plans-output.md |
|
||||
| **Swisstopo/OSM via .NET HttpClient** | `fetch()` (CORS-fähige STAC/Overpass-APIs) | Phase 4 |
|
||||
| **LV95↔WGS84 (Python-Formeln)** | dieselben Formeln in TS portiert | Phase 4 |
|
||||
| **IronPython 2.7/3 Runtime-Risiken** | entfällt (TS/Browser) | — |
|
||||
| Rhino `RhinoDoc` | `Project` (TS-Objekt im eigenen Store) | einzige Wahrheit |
|
||||
| `doc.Strings[key]=json` | Feld im `Project`-JSON | persistiert in `.obp` |
|
||||
| `.3dm`-Datei | **`.obp`**-Datei (Tauri `plugin-fs`) | + Blob-Fallback im Browser |
|
||||
| `sc.sticky` (cross-modul Bus) | eigener `useSyncExternalStore`-Store | kein Polling |
|
||||
| Rhino Layer-Tabelle | `LayerCategory[]`-Baum, Sichtbarkeit als `.visible`-Flag | pro Renderer umgesetzt |
|
||||
| Clipping-Plane (`AddClippingPlane`) | `render3d::section.rs` (analytisch, Rechteck-Subtraktion) | KEIN HLR |
|
||||
| `HLRBRep` | — (ungenutzt; OCCT-Spike ist toter Code) | ersetzt durch obiges |
|
||||
| `Rhino.Geometry.Brep`-Booleans | `trucksolid::boolean_mesh` (csgrs) | existiert, nicht an Wände angeschlossen |
|
||||
| Rhino-Grips + DisplayConduit | Pointer-Events + Grip-Overlay (2D vollständig, 3D nur Basis, kein Snap) | siehe STATUS.md §4.7 |
|
||||
| Swisstopo via .NET HttpClient | `fetch()` (CORS-offen) | radiusgenau zugeschnitten (nicht ganze STAC-Kachel) |
|
||||
| IronPython-Laufzeit-Risiken | entfällt | TS/Rust |
|
||||
|
||||
---
|
||||
|
||||
## 7. Was wir von DOSSIER übernehmen — und was nicht
|
||||
## 7. Was übernommen wurde — und was bewusst anders lief
|
||||
|
||||
**Übernehmen (bewährte Konzepte):**
|
||||
- Zwei-Achsen-Dokumentmodell (Zeichnungsebenen × Ebenen).
|
||||
- Layer-Codes/Farben/lw 1:1 (`DEFAULT_LAYER_SCHEMA`).
|
||||
- Component/Hatch/Line-Manager mit id-Verweisen + `joinPriority`.
|
||||
- Ansichtstypen = Kamera + optionaler Schnitt (vereinheitlicht).
|
||||
- LoD (`darstellung`: einfach/standard/detail) mit Dokument-Override.
|
||||
- Regelbasierte Overrides (Condition → Action, additive Priorität, reversibel).
|
||||
- Ausschnitte (View-Snapshots), Layer-Kombinationen, Massstab-pro-Viewport.
|
||||
- SIA-416-Räume, Norden-Rotation, Stil-Kataloge.
|
||||
**Übernommen (Prinzipien, bewährt):**
|
||||
- Zwei-Achsen-Dokumentmodell, Layer-Codes 1:1, Component/Hatch/Line-Manager
|
||||
mit `joinPriority`, LoD (grob/mittel/fein), regelbasierte Overrides,
|
||||
Ausschnitte, SIA-416-Räume, Norden-Rotation.
|
||||
- Pure-Ableitungs-Architektur (kein Cache-Stale, kein Sticky-Bus).
|
||||
|
||||
**Bewusst anders (Browser-nativ, Schwachstellen vermeiden):**
|
||||
- **Kein Sticky-Bus** → ein reaktiver Store (vermeidet DOSSIER #4.4/#4.5/#4.6).
|
||||
- **Kein Monolith** → Bauteil-Module statt 7244-LOC-`elemente.py` (#4.1).
|
||||
- **Pure Ableitungen** statt mutierter Doc-Objekte → kein Cache-Stale (#4.3),
|
||||
kein Undo/Redo-Loch.
|
||||
- **Versioniertes Datei-Schema** statt UserString-Sticky-Migration.
|
||||
- **Persistenz im Project-JSON** statt verteilt über `doc.Strings`.
|
||||
**Anders gelaufen als geplant (siehe STATUS.md §5 für die volle Tabelle):**
|
||||
- Eigene Rust/WASM-Rendering-Engines statt Three.js/OpenCascade.js/web-ifc.
|
||||
- Typisierte Arrays pro Bauteiltyp statt `Element[]`-Union.
|
||||
- Eigener Store statt Zustand-Library.
|
||||
- Analytische Rust-Schnitt-Pipeline statt HLR/Worker/Comlink.
|
||||
- App.tsx wurde **nicht** wie geplant auf eine dünne Shell reduziert
|
||||
(`src/views/`/`src/menus/` wurden nie angelegt) — bekannter, unbereinigter
|
||||
Punkt, siehe STATUS.md §4.3.
|
||||
|
||||
**Anti-Over-Engineering (aus DOSSIER-CONVENTIONS.md übernommen):** keine Abstraktion
|
||||
ohne konkretes Problem; kein `try/catch: {}` als Bug-Versteck; erst grep/lesen,
|
||||
dann editieren; Wand-Geometrie nie „nebenbei" refactoren.
|
||||
**Anti-Over-Engineering (weiterhin gültig):** keine Abstraktion ohne konkretes
|
||||
Problem; erst lesen, dann editieren; Geometrie nie „nebenbei" refactoren.
|
||||
|
||||
---
|
||||
|
||||
## 8. Reihenfolge bei Code-Arbeit
|
||||
|
||||
1. **Dieses Dokument + das relevante Detail-Design** (elements/plans-output/
|
||||
resources-graphics) lesen.
|
||||
2. **Dann das betroffene Modul** lesen (nicht raten).
|
||||
3. **Erst danach editieren**; `Project` immer immutabel via `store.apply()` ändern.
|
||||
4. **Verifizieren:** `npx tsc -b`, `npm run build`, Screenshot via
|
||||
`node scripts/probe.mjs` — Geometrie visuell prüfen.
|
||||
5. **Dieses Dokument aktuell halten**, wenn sich Patterns/Mapping ändern.
|
||||
1. **STATUS.md** (aktueller Ist-Zustand) + dieses Dokument lesen.
|
||||
2. **Das betroffene Modul** lesen (nicht raten) — bei Zweifel: welcher der
|
||||
mehreren Renderer/Pfade ist gerade aktiv (§4.1/§4.2)?
|
||||
3. **Erst danach editieren**; `Project` immer immutabel via den Store ändern.
|
||||
4. **Verifizieren:** `npx tsc -b`, `npm test` (Vitest), `cargo test` in den
|
||||
betroffenen Crates, bei Rust-Änderungen `npm run build:engine{,3d}` (WASM
|
||||
neu bauen, nur `.rs` committen — `src/engine/pkg*/` ist gitignored). Bei
|
||||
render3d/Wasm3DViewport-Änderungen testet der Nutzer selbst in der
|
||||
Tauri-Dev-App (nicht per Puppeteer/Browser verifizierbar).
|
||||
5. **Dieses Dokument aktuell halten**, wenn sich Patterns ändern — insbesondere
|
||||
nicht wieder in „Ziel-Struktur"-Beschreibungen abdriften, die Monate lang
|
||||
niemand nachführt.
|
||||
|
||||
+25
-11
@@ -1,6 +1,8 @@
|
||||
# Projekt-Konventionen — Browser-BIM (cad)
|
||||
# Projekt-Konventionen — Dossier
|
||||
|
||||
Siehe [ROADMAP.md](ROADMAP.md) für Vision, Architektur und Phasen.
|
||||
Siehe [ARCHITECTURE.md](ARCHITECTURE.md) für die aktuelle Architektur,
|
||||
[STATUS.md](STATUS.md) für den vollständigen Ist-Zustand und
|
||||
[ROADMAP.md](ROADMAP.md) für die ursprüngliche (historische) Produktvision.
|
||||
|
||||
## Code-Konventionen (verbindlich)
|
||||
|
||||
@@ -15,16 +17,26 @@ Siehe [ROADMAP.md](ROADMAP.md) für Vision, Architektur und Phasen.
|
||||
CCW-Wicklung zeigt `+n` nach innen. Schichten werden außen (−T/2) → innen (+T/2)
|
||||
gestapelt.
|
||||
|
||||
## Code-Struktur (kein God-Component)
|
||||
## Code-Struktur (kein God-Component — bisher nur teilweise erreicht)
|
||||
|
||||
- **`App.tsx` bleibt ein dünner Shell** (Store-Provider, Oberleiste, Docks+View-Router,
|
||||
Statusleiste, Floating-Panels, Ressourcen-Overlay) — keine Geschäftslogik darin.
|
||||
- **Globaler Zustand in einem Store** (`src/state/`, Slices: project/selection/view/layout).
|
||||
Komponenten lesen Zustand über Store-Hooks statt Prop-Drilling.
|
||||
- **Features als eigene Module:** `src/views/` (View-Router-Teile), `src/editors/`
|
||||
(Inline-Editoren), `src/menus/` (Kontextmenü-Builder), `src/panels/`, `src/ui/`.
|
||||
- **Ziel:** `App.tsx` bleibt ein dünner Shell (Store-Provider, Oberleiste,
|
||||
Docks+View-Router, Statusleiste, Floating-Panels, Ressourcen-Overlay) — keine
|
||||
Geschäftslogik darin. **Realität (Stand 2026-07-21):** `App.tsx` ist mit
|
||||
~7.100 Zeilen die grösste Datei des Projekts und enthält weiterhin
|
||||
View-Umschaltung und Kontextmenü-Aufbau inline — die geplante Auslagerung
|
||||
nach `src/views/`/`src/menus/` (unten) ist **nie passiert**, siehe
|
||||
[STATUS.md](STATUS.md) §4.3. Neue, grössere Features sollten trotzdem nicht
|
||||
weiter in `App.tsx` wachsen; wo möglich in `src/panels/`, `src/editors/`
|
||||
oder ein neues Modul auslagern statt die Datei weiter zu vergrössern.
|
||||
- **Globaler Zustand in einem Store** (`src/state/`, eigener Store auf
|
||||
`useSyncExternalStore` — **kein** Zustand/Redux/Immer — mit Slices:
|
||||
project/history/selection/view/layout/site/notify). Komponenten lesen
|
||||
Zustand über `useStore(selector)` statt Prop-Drilling.
|
||||
- **Features als eigene Module:** `src/editors/` (Inline-Editoren),
|
||||
`src/panels/`, `src/ui/`. `src/views/` und `src/menus/` waren geplant, wurden
|
||||
aber nie angelegt — bei Bedarf gilt das als offener Aufräum-Punkt, nicht als
|
||||
bestehende Struktur.
|
||||
- Ziel: modular + parallel bearbeitbar (verschiedene Features ≠ dieselbe Datei).
|
||||
Siehe `docs/design/state-architecture.md`.
|
||||
|
||||
## UI-Konventionen
|
||||
|
||||
@@ -61,4 +73,6 @@ Rendern angewandt, nie in die Geometrie eingebacken.
|
||||
- Änderungen verifizieren: `npx tsc -b`, `npm run build`, und Screenshot via
|
||||
`node scripts/probe.mjs` (schreibt `scripts/probe.png`) bzw. `probe-ff*.mjs` für
|
||||
Firefox-Fälle. Screenshot ansehen und Geometrie visuell prüfen.
|
||||
- Dev-Server läuft via `npm run dev` (Vite, Port 5173).
|
||||
- Dev-Server läuft via `npm run dev` (Vite, Port 5187). Nativer Rahmen
|
||||
plattformabhängig: `npm run tauri:dev` auf macOS, `npm run electron` auf Linux
|
||||
(WebKitGTK kann kein zuverlässiges WebGPU → dort Chromium/Electron).
|
||||
|
||||
@@ -1,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)
|
||||
|
||||
Lange autonome Session (Nutzer-Auftrag: „arbeite die Pendenzen/Funktionen durch,
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
# DOSSIER Standalone
|
||||
# Dossier
|
||||
|
||||
Open-Source-CAAD (Computer Aided Architecture Design). Modelliert ein Gebäude
|
||||
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
|
||||
vollständige Fassung.
|
||||
ohne Revit. **Desktop-App** — auf macOS über **Tauri**, auf Linux über eine
|
||||
**Electron**-Shell (weil Tauris Linux-Webview WebKitGTK kein zuverlässiges
|
||||
WebGPU bietet, das die Rendering-Engines brauchen); dieselbe Codebasis läuft
|
||||
auch im Browser (eingeschränkt, ohne native Fenster/Dateisystem-Integration).
|
||||
|
||||
Das ist die eigenständige Standalone-Variante des Rhino-Plugins
|
||||
[DOSSIER](https://git.kgva.ch/karim/DOSSIER): dieselbe Denkweise (Geschosse,
|
||||
Kategorie-Ebenen, mehrschichtige Bauteile, Prioritäts-Verschneidung), aber als
|
||||
React-App mit eigener Rendering-Engine (Rust/WASM/WebGPU) statt als Rhino-Aufsatz.
|
||||
Als Desktop-App (Tauri) läuft sie im eigenen Fenster mit voller Engine-Leistung;
|
||||
browserseitig ist derselbe Kern zugänglich, die App ist die vollständige Fassung.
|
||||
Das ist die eigenständige Neuimplementierung des Rhino-Plugins
|
||||
[DOSSIER](https://git.openbureau.ch/karim/dossier): dieselbe Denkweise
|
||||
(Geschosse, Kategorie-Ebenen, mehrschichtige Bauteile, Prioritäts-
|
||||
Verschneidung), aber mit eigenem Datenmodell + eigener Rendering-Engine
|
||||
(Rust/WASM/WebGPU, intern „Nordstern" genannt) statt Rhino-Aufsatz.
|
||||
|
||||
## Grundgedanke
|
||||
|
||||
@@ -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
|
||||
3D-Mesh, sondern **analytisch aus den Bauteil-Parametern** (Wandachsen + Dicken →
|
||||
Linien, Öffnungen → Lücken + Symbol). PDF-Export ist derselbe Plan, nur mit
|
||||
echten mm-Stiftstärken statt Bildschirm-Hairlines — kein zweites Rendering.
|
||||
Schnitte und Ansichten brauchen den zweiten Weg — echte 3D-Projektion mit
|
||||
verdeckten Kanten (HLR) —, dessen Machbarkeit per Spike bewiesen, aber noch
|
||||
nicht ans UI angebunden ist.
|
||||
echten mm-Stiftstärken statt Bildschirm-Hairlines. Schnitte und Ansichten laufen
|
||||
über eine eigene, analytische Rust-Pipeline (kein Hidden-Line-Removal nötig,
|
||||
siehe [ARCHITECTURE.md](ARCHITECTURE.md) §4.3) und sind seit Juli live im
|
||||
3D-Viewport verdrahtet, nicht nur ein Spike.
|
||||
|
||||
## Stand heute
|
||||
|
||||
Ehrlich eingeordnet: aus dem Risiko-Spike ist ein **benutzbares 2D-CAD mit
|
||||
abgeleiteter 3D-Sicht, Vektor-PDF/DXF-Export und einer parametrischen
|
||||
Wand-Engine** geworden. Der einfachere Teil steht und ist per Screenshot/Probe
|
||||
verifiziert; die schwierigen BIM-Kernrisiken sind bewusst noch offen.
|
||||
Aus dem ursprünglichen Risiko-Spike ist binnen weniger Wochen ein
|
||||
**funktionsreiches Desktop-BIM-Tool** geworden: eigenes semantisches
|
||||
Gebäudemodell, zwei eigene Rust/WASM-Rendering-Engines, IFC/DXF/PDF/STL/OBJ-
|
||||
Export, Swisstopo-Import, SIA-416-Flächen, Materialbibliothek, Layouts/
|
||||
Plansätze, native Tauri-Fenster. Eine vollständige, ehrliche Bestandsaufnahme
|
||||
(inklusive offener Punkte und bekannter technischer Schulden) steht in
|
||||
**[STATUS.md](STATUS.md)**.
|
||||
|
||||
**Funktioniert:**
|
||||
- Semantisches Modell mit **mehrschichtigen Wänden** und automatischer
|
||||
**L-Ecken-Gehrung** (`computeJoins`); dieselbe Logik speist Plan *und* 3D.
|
||||
- **Parametrische Wände** (`ParametricWall`-Regelwerke: Grid/Modul/Sequenz/
|
||||
Referenzlinie/bedingte Dicke) lösen sich zu konkreten `Wall`-Objekten auf,
|
||||
statt jede Wand einzeln von Hand zu ziehen.
|
||||
- **Dokumentmodell wie in DOSSIER:** Zeichnungsebenen (Geschosse + Schnitte/
|
||||
Ansichten/Zeichnungen) × Kategorie-Ebenen (Code-Baum 1:1, `20 Wände`, `30 Decken` …).
|
||||
- **Zeichnen & Editieren** im Grundriss: Wand, Linie, Polylinie, Rechteck, Kreis,
|
||||
Öffnungen, Treppen, Decken, Raumstempel; Snapping (Endpunkt/Mittelpunkt/
|
||||
Schnittpunkt/Lot/Raster/Ortho), Grips, Move/Copy/Offset, Spiegeln/Drehen/Array,
|
||||
Trim/Split/Join.
|
||||
- **Rhino-artiges Befehlssystem** mit getippten Koordinaten (`5,3` · `r5,3` ·
|
||||
`5<45`) und Tab-Feld-Zyklus (Länge → Winkel …).
|
||||
- **Vektor-Export:** PDF (A4/A3, Titelblock, echte mm-Stiftstärken nach ISO-Pen-
|
||||
Steps, keine Rasterbilder) und DXF — beide aus derselben `Plan`-Struktur wie
|
||||
der Bildschirm.
|
||||
- **PBR-Material-Bibliothek** (ambientCG-Import, `manifest.json`) für die
|
||||
3D-Ansicht.
|
||||
- **Resource Manager** (Component / Hatch / Line) — alles per id referenziert,
|
||||
zentral änderbar.
|
||||
- **Panel-System** (dockbar, stapelbar, floatend), Top-Bar + Statusleiste
|
||||
(echter Maßstab 1:N, Cursor X/Y), eigenes Kontextmenü, **i18n** (de/en).
|
||||
- **Import:** DXF/DWG (Konturen/Mesh) → Terrain-TIN, Swisstopo/LV95-Geokontext,
|
||||
OSM-Kontextimport; `.lin`/`.pat` für Linien/Schraffuren.
|
||||
**Funktioniert (Auszug — volle Liste in STATUS.md §3):**
|
||||
- Semantisches Modell mit **mehrschichtigen Wänden**, L-Eck-Gehrung **und**
|
||||
Prioritäts-T-/X-Stössen (`joinPriority` am Component) — konsistent in
|
||||
Grundriss, 3D-Viewport und 3D-Live-Schnitt.
|
||||
- **Parametrische Wände**, Decken, Treppen (gerade/L/Wendel), Dächer
|
||||
(Flach/Pult/Sattel/Walm/Mansarde/Zelt), Stützen, SIA-416-Räume mit
|
||||
automatischer Bilanz + CSV-Export.
|
||||
- **Türen & Fenster** gehostet in Wänden, mit Rahmen/Zarge/Kämpfer/Oberlicht,
|
||||
Detailgrad grob/mittel/fein, echte Rechteck-Löcher im 3D-Wandkörper.
|
||||
- Rhino-artiges **Kommandosystem** mit getippten Koordinaten (`5,3` · `r5,3` ·
|
||||
`5<45`) und Tab-Feld-Zyklus; Snapping, Grips, Trim/Split/Join, 2D-Booleans.
|
||||
- **Live 2D↔3D-Schnitt**: eine Schnittebene im 3D-Viewport folgt derselben
|
||||
Prioritäts-Logik wie der 2D-Plan-Schnitt (Rust-Port, keine Diskrepanz).
|
||||
- **Vektor-Export:** PDF (Einzel- **und Mehrseiten-Layouts**), DXF, **IFC4**
|
||||
(mit echten Fenster-/Tür-Löchern), STL/OBJ, CSV-Bauteilliste.
|
||||
- **Materialbibliothek**: 13 gebündelte PBR-Starter **plus** Live-Suche der
|
||||
kompletten ambientCG-Bibliothek (1K/2K/4K, On-Demand-Download).
|
||||
- **Layouts/Plansätze** mit mehreren Viewports pro Blatt, Ausschnitte
|
||||
(View-Snapshots), Kamera-Presets, Norden-Rotation.
|
||||
- **Import:** DXF/DWG, Swisstopo (swissBUILDINGS3D/-ALTI3D/SWISSIMAGE,
|
||||
radiusgenau zugeschnitten), OSM-Kontextimport, `.lin`/`.pat`.
|
||||
- **Native Desktop-Integration** (Tauri): eigene randlose Fenster, native
|
||||
Speichern/Öffnen-Dialoge, eigenes `.obp`-Dateiformat mit OS-Level-Lock gegen
|
||||
Doppelöffnen, mehrere native Zusatzfenster (Resources, Settings, …).
|
||||
- **Resource Manager** (Component/Hatch/Line/Typ-Editoren), regelbasierte
|
||||
Overrides, Panel-System (dockbar/floatend), i18n (de/en).
|
||||
|
||||
**Bewusst noch offen** (die eigentlich harten Teile):
|
||||
- Echte **3D-Booleans** für Öffnungen — Türen/Fenster sind im Plan eine Lücke,
|
||||
noch kein geschnittenes Volumen.
|
||||
- **HLR** (verdeckte Kanten) für Schnitte und Ansichten: Machbarkeit per
|
||||
OCCT-WASM-Spike bewiesen (`docs/welle-c-hlr-spike/`, `src/section/hlr.ts`),
|
||||
aber noch nicht ans UI/Dokumentmodell verdrahtet — Views sind noch Stubs.
|
||||
- **Prioritäts-T-/X-Stöße** mehrschichtiger Wände (Beton läuft durch, Putz
|
||||
verbindet seitlich) — das berüchtigte Risiko #1.
|
||||
- **Multi-Page-Layouts/Ausschnitte** (mehrere Viewports pro Blatt) — PDF-Export
|
||||
ist noch single-sheet.
|
||||
|
||||
Details und die Begründungen stehen im
|
||||
[HANDOVER](HANDOVER.md) (laufendes Arbeitsprotokoll) und in der
|
||||
[ROADMAP](ROADMAP.md) (Vision, Phasen, Backlog).
|
||||
**Bewusst noch offen** (Details + volle Liste: STATUS.md §4.7):
|
||||
- **Echtes Mesh-Boolean für Öffnungen** — funktioniert heute über achsparallele
|
||||
Rechteck-Löcher; ein generisches CSG-Boolean existiert bereits
|
||||
(`trucksolid`/`csgrs`), ist aber nicht an die Wand-Pipeline angeschlossen.
|
||||
- **3D-Griffsystem**: Feld-Controller (Tab-Zyklus) und Snapping fehlen für
|
||||
3D-Grip-Drag (im 2D vollständig vorhanden).
|
||||
- **DWG/DXF-Domänenimport** (Entitäten → echte Wände/Öffnungen) — Lesen
|
||||
funktioniert, das Mapping auf das Modell ist unbegonnen; DWG-Schreiben fehlt.
|
||||
- **`make2D`**-Kommando (3D-Ansicht → flacher 2D-Plan mit Füllungen).
|
||||
- Bekannte, noch nicht bereinigte Doppelspur `Door[]`/`Opening[]` im Modell.
|
||||
|
||||
## Stack
|
||||
|
||||
Bewusst leichtgewichtig — Three.js ist reiner Display-Layer, kein schwerer
|
||||
Geometrie-Kernel verfrüht eingezogen.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Shell | 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 |
|
||||
| 3D | Three.js, plus eigener nativer Rust/WASM/WebGPU-Renderer (`render3d`) |
|
||||
| 2D-Plan | eigener SVG-Renderer, plus eigener nativer Rust/WASM/WebGPU-Renderer (`render2d`) |
|
||||
| Geometrie | eigener 2D-Kernel (`src/geometry/kernel2d.ts`), `delaunator` fürs Terrain |
|
||||
| Export | `jspdf`/`svg2pdf.js` (Vektor-PDF), eigener DXF-Writer |
|
||||
| 3D | eigener Rust/WASM/WebGPU-Renderer „Nordstern" (`render3d`, Default), Three.js als leichtgewichtiger Zweitpfad |
|
||||
| 2D-Plan | eigener Rust/WASM/WebGPU-Renderer (`render2d`), plus ein TS/WebGL2-Renderer (`plan/glPlan`), plus SVG (`PlanView.tsx`, immer als Interaktions-/Fallback-Ebene aktiv) |
|
||||
| Geometrie | eigener 2D-Kernel (`src/geometry/kernel2d.ts`); ein Rust-Port (`src-tauri/kernel2d`) existiert nur als Paritätstest, läuft nicht produktiv |
|
||||
| CSG/Extrusion | `trucksolid`-Crate (`truck` + `csgrs`), fürs Extrude-Kommando |
|
||||
| Export | eigener IFC4-Writer, `jspdf`/`svg2pdf.js` (Vektor-PDF), eigener DXF-Writer, STL/OBJ |
|
||||
| Import | `dxf-parser`, `@mlightcad/libredwg-web` (DWG), eigene `.lin`/`.pat`-Parser |
|
||||
| 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),
|
||||
Manifold (exakte 3D-Booleans). OCCT-WASM ist für HLR bereits als Spike da (s.o.),
|
||||
aber noch nicht an Dokumentmodell/UI angebunden. Siehe ROADMAP §4.
|
||||
`opencascade.js` steht noch als Dependency in `package.json`, wird aber nur
|
||||
noch von totem Code (`src/section/hlr.ts`, superseded durch die Rust-
|
||||
Schnitt-Pipeline) importiert. Details zu allen Rust-Crates (inkl. zwei
|
||||
aktuell unbenutzten WASM-Builds) in [ARCHITECTURE.md](ARCHITECTURE.md) §1
|
||||
und [STATUS.md](STATUS.md) §4.1.
|
||||
|
||||
## Entwicklung
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # Vite, http://localhost: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
|
||||
npm run build # tsc -b && vite build
|
||||
npm test # vitest run
|
||||
```
|
||||
|
||||
Verifiziert wird visuell: `node scripts/probe.mjs` rendert die App headless und
|
||||
schreibt `scripts/probe.png` — Screenshot ansehen, Geometrie prüfen. Weitere
|
||||
`scripts/probe-*.mjs` decken einzelne Features ab (PDF, Trim, Materialien,
|
||||
Theme, Boolean-Ops …); für Firefox-Fälle gibt es `scripts/probe-ff*.mjs`
|
||||
(Playwright).
|
||||
WASM-Engines nach Rust-Änderungen neu bauen (nur `.rs` committen,
|
||||
`src/engine/pkg*/` ist gitignored):
|
||||
|
||||
```bash
|
||||
npm run build:engine # render2d
|
||||
npm run build:engine3d # render3d
|
||||
npm run build:truck # trucksolid
|
||||
```
|
||||
|
||||
render3d/Wasm3DViewport-Änderungen sind nicht per Puppeteer/Browser
|
||||
verifizierbar — im laufenden `tauri:dev`-Fenster selbst testen.
|
||||
|
||||
## Aufbau
|
||||
|
||||
```
|
||||
src/
|
||||
model/ semantisches Modell + Ableitungen (types, parametricWalls, roomStamp, joins, terrain)
|
||||
geometry/ 2D-Kernel (offset/trim/fillet/intersect, ceiling, opening, roomArea/-Boundary, stair)
|
||||
model/ semantisches Modell (types, joins, parametricWalls, roomStamp, terrain)
|
||||
geometry/ 2D-Kernel (offset/trim/fillet/intersect, ceiling/opening/roomArea/stair/roof/column)
|
||||
commands/ Rhino-artiges Befehlssystem (engine, parser, registry, cmds/)
|
||||
tools/ interaktive Zeichenwerkzeuge + Snapping + Transformationen
|
||||
plan/ Grundriss-Ableitung (generatePlan) + PlanView (SVG) + glPlan (WebGL2-Renderer)
|
||||
viewport/ Viewport3D (Three.js)
|
||||
section/ HLR-Spike (OCCT-WASM) — Schnitte/Ansichten, noch nicht verdrahtet
|
||||
export/ PDF- und DXF-Export aus derselben Plan-Struktur wie der Bildschirm
|
||||
materials/ PBR-Material-Bibliothek (ambientCG-Import, Runtime)
|
||||
text/ Rich-Text (Beschriftungen, Textobjekte)
|
||||
plan/ Grundriss-Ableitung (generatePlan) + PlanView (SVG) + glPlan (WebGL2) + Rust-Bindings
|
||||
viewport/ Viewport3D (Three.js) + Wasm3DViewport (Rust/wgpu „Nordstern", Default)
|
||||
section/ TOTER Code (OCCT-HLR-Spike) — Schnitt läuft über render3d, siehe ARCHITECTURE.md §4.3
|
||||
export/ IFC4-, PDF-, DXF-, STL/OBJ-, CSV-Export aus derselben Plan-/Modell-Struktur
|
||||
materials/ ambientCG-Live-Suche + gebündelte Starter-Bibliothek + PBR-Runtime
|
||||
panels/ dockbares Panel-System + die einzelnen Paletten
|
||||
state/ Store + Slices (project/selection/view/layout)
|
||||
ui/ Top-Bar, Statusleiste, Kontextmenü, Resource Manager, Command-Line
|
||||
state/ eigener Store (useSyncExternalStore) + Slices (project/selection/view/layout/…)
|
||||
native/ Tauri-only: native Fenster, macOS-Menüleiste, Fenster-Chrome
|
||||
ui/ App.tsx-Shell, Top-Bar, Resource Manager, Kontextmenü, Kommandozeile, Ribbon
|
||||
io/ Import/Export (DXF, DWG, .lin, .pat), Swisstopo/LV95, OSM-Kontext
|
||||
i18n/ Wörterbücher de/en
|
||||
src-tauri/ Rust-Crates (render2d/render3d) — headless per wasm-pack zu WASM
|
||||
gebaut (npm run build:engine), Tauri-Host selbst ist ausrangiert
|
||||
src-tauri/ 6 Rust-Crates (render2d/render3d/geometry/kernel2d/trucksolid/dwgimport,
|
||||
je headless UND per wasm-pack baubar) + der Tauri-Host selbst
|
||||
```
|
||||
|
||||
## 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
|
||||
über `t('key')` — keine hartcodierten Strings im JSX.
|
||||
- Intern alles in **Metern**; Anzeige via `formatM`.
|
||||
- `App.tsx` bleibt dünner Shell, Zustand lebt im Store. Verbindliches in
|
||||
[CONVENTIONS.md](CONVENTIONS.md).
|
||||
- Verbindliches in [CONVENTIONS.md](CONVENTIONS.md).
|
||||
|
||||
## Weiterlesen
|
||||
|
||||
- [ROADMAP.md](ROADMAP.md) — Produktvision, Architektur-Entscheidungen, Phasen 0–7, DOSSIER-Backlog
|
||||
- [ARCHITECTURE.md](ARCHITECTURE.md) — Technische Architektur im Detail
|
||||
- [HANDOVER.md](HANDOVER.md) — aktueller Arbeitsstand, Befunde, nächste Schritte
|
||||
- [docs/](docs/) — Design-Specs (Befehlssystem, Zeichenwerkzeuge, Wand-Joins, Backend …)
|
||||
- [STATUS.md](STATUS.md) — **vollständige, ehrliche Bestandsaufnahme**: Zahlen,
|
||||
Feature-Inventar, Mist-Liste, Doku-Widersprüche, Tag-1-Vision vs. heute
|
||||
- [ARCHITECTURE.md](ARCHITECTURE.md) — technische Architektur im Detail (Ist-Zustand)
|
||||
- [ROADMAP.md](ROADMAP.md) — ursprüngliche Produktvision (Tag-1-Stand, historisch)
|
||||
- [HANDOVER.md](HANDOVER.md) / [PENDENZEN.md](PENDENZEN.md) — laufendes
|
||||
Arbeitsprotokoll bzw. Backlog (Single Source of Truth für offene Punkte)
|
||||
- [docs/](docs/) — Design-Specs (teils ebenfalls Tag-1-Vision, siehe Hinweis
|
||||
in `docs/README.md`)
|
||||
|
||||
## Lizenz
|
||||
|
||||
Copyright © 2026 Karim Gabriele Varano. Veröffentlicht unter der
|
||||
**GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)** —
|
||||
siehe [LICENSE](LICENSE). DOSSIER ist Teil der **openbureau**-Suite.
|
||||
siehe [LICENSE](LICENSE). Dossier ist Teil der **openbureau**-Suite.
|
||||
|
||||
Die AGPL verlangt, dass auch bei Betrieb als Netzwerk-/Webdienst der (ggf.
|
||||
geänderte) Quellcode für die Nutzer verfügbar gemacht wird. Drittkomponenten
|
||||
@@ -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)
|
||||
sind als Differenzierer eingeplant.
|
||||
sind ein bewusster Differenzierer.
|
||||
|
||||
+11
-1
@@ -1,6 +1,16 @@
|
||||
# Browser-BIM für Wohnbau — Architektur & Produkt-Roadmap
|
||||
|
||||
> Arbeitstitel: **cad** (Name später)
|
||||
> **Historisches Dokument (Tag-1-Vision, Stand 2026-06-28).** Vieles hier als
|
||||
> „Phase 2–5"/„Backlog" gelistete ist inzwischen längst gebaut (Treppen, Dächer,
|
||||
> Stützen, SIA-416, Swisstopo, OSM, Layouts, Ausschnitte, Kamera-Presets …), und
|
||||
> mehrere Technologie-Entscheidungen liefen anders (eigene Rust/WASM-Engines
|
||||
> statt Three.js/OpenCascade.js/web-ifc, siehe unten §4/§8). Für den aktuellen
|
||||
> Ist-Zustand: **[STATUS.md](STATUS.md)** (Bestandsaufnahme + Mist-Liste) und
|
||||
> **[ARCHITECTURE.md](ARCHITECTURE.md)** (aktuelle Architektur). Dieses Dokument
|
||||
> bleibt als ursprüngliche Produktvision/Phasenplan stehen, wird aber nicht mehr
|
||||
> laufend nachgeführt.
|
||||
>
|
||||
> Arbeitstitel: **cad** (Name später: **Dossier**)
|
||||
> Ausrichtung: **BIM-first** · Nische: **Wohnbau / Einfamilienhäuser**
|
||||
> Stand: 2026-06-28
|
||||
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
# STATUS — Codebase-Analyse
|
||||
|
||||
> Stand: 2026-07-21 · Vollständige Bestandsaufnahme von Code + Dokumentation.
|
||||
> Ersetzt NICHT [ROADMAP.md](ROADMAP.md)/[ARCHITECTURE.md](ARCHITECTURE.md) (die wurden
|
||||
> im gleichen Zug überarbeitet), sondern begründet die Überarbeitung mit Zahlen und
|
||||
> Befunden. [HANDOVER.md](HANDOVER.md) und [PENDENZEN.md](PENDENZEN.md) bleiben die
|
||||
> laufenden Arbeitsprotokolle (nicht rückwirkend umgeschrieben).
|
||||
|
||||
## 0. TL;DR
|
||||
|
||||
„Dossier" (Arbeitstitel `cad`, Rhino-Vorbild `DOSSIER`) ist in **3 Wochen**
|
||||
(erster Commit 2026-06-30, 362 Commits bis 2026-07-20) von einem Risiko-Spike zu
|
||||
einem **funktionsreichen Desktop-CAD/BIM-Tool** gewachsen: eigenes semantisches
|
||||
Gebäudemodell, zwei eigene Rust/WASM-Rendering-Engines („Nordstern"), ein
|
||||
Rhino-artiges Kommandosystem, IFC/DXF/PDF/STL/OBJ-Export, Swisstopo-Import,
|
||||
SIA-416-Flächen, Materialbibliothek (statisch + live von ambientCG), Layouts/
|
||||
Plansätze, Ausschnitte, native Tauri-Fenster. **~125.000 Zeilen Code** (TS+Rust,
|
||||
inkl. Tests), gebaut über viele autonome Agent-Sessions.
|
||||
|
||||
Die drei zentralen Vision-Dokumente (ARCHITECTURE.md, README.md, ROADMAP.md)
|
||||
stammen aus der **allerersten Woche** (Stand 28./29.6.) und beschreiben einen
|
||||
Plan, der in der Zwischenzeit an vielen Stellen überholt, anders gelöst oder
|
||||
längst umgesetzt wurde (z. B. HLR/OCCT→eigene Rust-Schnitt-Pipeline,
|
||||
„Booleans noch offen"→teilweise längst gelöst). Diese Doku-Drift war der Auslöser
|
||||
für diese Analyse; die Docs sind im gleichen Zug revidiert worden.
|
||||
|
||||
## 1. Kennzahlen
|
||||
|
||||
### Code-Umfang
|
||||
|
||||
| Bereich | Dateien | LOC (ohne Tests) | Tests |
|
||||
|---|---:|---:|---:|
|
||||
| `src/` (TypeScript, gesamt) | ~230 | 87.736 | 869 (Vitest, 71 Dateien) / 16.855 LOC |
|
||||
| `src-tauri/render3d` („Nordstern" 3D) | 14 | 10.246 | 97 `#[test]` |
|
||||
| `src-tauri/render2d` (2D-WGSL-Renderer) | 11 | 3.530 | 18 `#[test]` |
|
||||
| `src-tauri/kernel2d` (Rust-Geometriekern, Paritätstest) | 1 | 3.383 | 18 `#[test]` |
|
||||
| `src-tauri/geometry` (Wand-Join-Mathe, **unbenutzt**) | 1 | 1.057 | 8 `#[test]` |
|
||||
| `src-tauri/trucksolid` (CSG/Extrusion, `truck`+`csgrs`) | 2 | 758 | 15 `#[test]` |
|
||||
| `src-tauri/dwgimport` (DXF-Parser-Spike, **unbenutzt**) | 1 | 284 | 1 `#[test]` |
|
||||
| `src-tauri/src` (Tauri-Host: Fenster, Dialoge, Lock) | 4 | 1.044 | — |
|
||||
| **Gesamt** | | **~108.000** (ohne Tests) / **~125.000** (mit Tests) | 869 Vitest + 157 Rust-Tests |
|
||||
|
||||
Größte TS-Bereiche: `plan/` (23.626 LOC — Plan-Ableitung + 3 Renderer),
|
||||
`ui/` (12.897 LOC — App-Shell, ResourceManager, Ribbon), `panels/` (9.538 LOC),
|
||||
`model/` (7.043 LOC inkl. Tests), `commands/` (6.973 LOC), `io/` (5.299 LOC),
|
||||
`state/` (5.297 LOC), `geometry/` (5.559 LOC), `viewport/` (5.165 LOC),
|
||||
`export/` (4.280 LOC). `src/App.tsx` allein ist **7.130 Zeilen**.
|
||||
|
||||
### Tempo
|
||||
|
||||
362 Commits in 3 Wochen; Woche 27 (30.6.–6.7.): 233 Commits, Woche 28: 126,
|
||||
danach starker Rückgang (Woche 30 bislang 3) — die Session-Dichte hat spürbar
|
||||
abgenommen, nicht das Projekt gestoppt (siehe PENDENZEN.md, weiterhin aktiv).
|
||||
|
||||
## 2. Architektur, wie sie WIRKLICH ist (nicht wie geplant)
|
||||
|
||||
### 2.1 Datenmodell — kein `Element[]`-Union, sondern typisierte Arrays
|
||||
|
||||
`ARCHITECTURE.md` (alt) plante eine diskriminierte Union `Element = Wall | Door |
|
||||
Window | …`. Tatsächlich hält `Project` (`src/model/types.ts:2095`) **pro
|
||||
Bauteiltyp ein eigenes optionales Array**: `walls`, `ceilings?`, `roofs?`,
|
||||
`doors`, `openings?` (Fenster/Türen gehostet in Wänden, `kind:"window"|"door"`),
|
||||
`stairs?`, `rooms?`, `columns?`, `extrudedSolids?`, `drawings2d`, `context?`
|
||||
(Importe/Terrain), plus die Bibliotheks-/Typtabellen (`lineStyles`, `hatches`,
|
||||
`components`, `wallTypes`, `roofTypes?`, `doorTypes?`, …) und die
|
||||
Dokument-Ebene (`viewSnapshots?`, `layouts?`, `masterLayouts?`, …). Der Typ-Alias
|
||||
`Element` (types.ts:1601) existiert zwar noch, wird aber **nirgends** verwendet
|
||||
(Element-Baum/Selektion arbeiten direkt auf den typisierten Arrays). Praktisch
|
||||
funktioniert das gut (jeder Bauteiltyp hat sein eigenes, spezifisches Interface),
|
||||
ist aber eine bewusste Abweichung vom ursprünglichen Uniform-Union-Plan.
|
||||
|
||||
### 2.2 State — kein Zustand/Redux/Immer, sondern ein Eigenbau
|
||||
|
||||
`docs/design/state-architecture.md` empfahl **Zustand**. Gebaut wurde stattdessen
|
||||
ein **abhängigkeitsfreier Store auf `useSyncExternalStore`** (`src/state/store.ts`,
|
||||
gleiches Muster wie `src/i18n`). `createStore()` komponiert Slice-Fabriken
|
||||
(`projectSlice` inkl. Undo/Redo, `historySlice`, `selectionSlice`, `viewSlice`,
|
||||
`layoutSlice`, `siteSlice`, `notifySlice`) über eine gemeinsame `RootState`.
|
||||
Funktioniert, aber: der geplante Folgeschritt „App.tsx wird dünne Shell,
|
||||
View-Routing nach `src/views/`, Kontextmenüs nach `src/menus/`" ist **nicht**
|
||||
passiert — `src/views/` und `src/menus/` existieren nicht, View-Umschaltung und
|
||||
Kontextmenü-Aufbau liegen weiterhin inline in `App.tsx` (7.130 Zeilen). Das ist
|
||||
der deutlichste Doku-vs-Code-Widerspruch im ganzen Repo (CONVENTIONS.md
|
||||
verlangt explizit das Gegenteil).
|
||||
|
||||
### 2.3 Rendering — drei 2D-Pfade, zwei 3D-Viewports
|
||||
|
||||
**2D-Plan:** es gibt tatsächlich **drei** koexistierende Renderer, nicht einen:
|
||||
1. `PlanView.tsx` (SVG) — Referenz-/Fallback-Pfad, bleibt IMMER im DOM für
|
||||
Hit-Testing/Grips, unabhängig davon was zeichnet.
|
||||
2. `plan/glPlan/` — eigener TypeScript-WebGL2-Renderer (`glPlanCompile/-Render/
|
||||
-Shaders/-Hatch.ts`).
|
||||
3. `useWasmPlanRenderer.ts` → Rust-`render2d`-Crate (WGSL, nativ via wgpu,
|
||||
Web via WebGPU/WebGL2-Fallback, inkl. echtem Text-Rendering via `glyphon`).
|
||||
|
||||
Beide GPU-Pfade fallen bei Initialisierungsfehler still auf SVG zurück.
|
||||
|
||||
**3D:** ebenfalls zwei Viewports: `Viewport3D.tsx` (three.js, „Free"-Stufe) und
|
||||
`Wasm3DViewport.tsx` (Rust/wgpu „Nordstern", editierbar, **Default**). Ein
|
||||
Settings-Schalter wählt die Engine.
|
||||
|
||||
### 2.4 Schnitt/Section — NICHT über HLR, sondern eigene Rust-Pipeline
|
||||
|
||||
`src/section/hlr.ts` + `occt.ts` (der ursprüngliche OpenCascade.js-HLR-Spike aus
|
||||
Phase 0) hat **keinen einzigen Aufrufer mehr** im gesamten `src/` — toter Code.
|
||||
Der tatsächliche, funktionierende Live-Schnitt läuft über einen völlig anderen,
|
||||
analytischen Mechanismus: `App.tsx` (`section3dCutId`/`section3dPlane`) →
|
||||
`Wasm3DViewport.tsx` (`section3d`-Prop → `setSectionPlane`) →
|
||||
`src-tauri/render3d/src/{section.rs, section_boolean.rs, section_fill.rs}`.
|
||||
Die Rust-Seite nutzt aus, dass jedes Bauteil ein Prisma mit konstantem
|
||||
Querschnitt ist — eine Schnittebene liefert dadurch immer ein
|
||||
achsparalleles Rechteck, nie ein Trapez; `section_boolean.rs` ist ein 1:1-Port
|
||||
von `toSection.ts::subtractDominantBands`, damit 2D-Plan-Schnitt und
|
||||
3D-Live-Schnitt exakt übereinstimmen. Kein Worker, kein Comlink (beides war
|
||||
geplant, keines existiert) — läuft synchron/GPU-seitig.
|
||||
|
||||
### 2.5 Öffnungen als Löcher — echt, aber kein Mesh-Boolean
|
||||
|
||||
Fenster/Türen schneiden echte achsparallele Rechteck-Löcher aus dem Wandkörper
|
||||
(`plan/toWalls3d.ts` `RHole`/`subtractSpans`, gespiegelt in
|
||||
`render3d/{mesh.rs,section.rs}`) — funktioniert, ist aber KEIN generisches
|
||||
Mesh-Boolean. Ein echtes CSG-Boolean existiert bereits (`trucksolid::boolean_mesh`,
|
||||
`csgrs`-basiert, 15 Rust-Tests) und wird von `src/engine/truckSolid.ts` für das
|
||||
Extrusions-Kommando genutzt — ist aber **nicht** an die Wand/Öffnungs-Pipeline
|
||||
angeschlossen (bestätigt: `booleanMesh` hat ausserhalb von `truckSolid.ts`
|
||||
keinen Aufrufer).
|
||||
|
||||
### 2.6 Rust-Workspace: sechs unabhängige Crates, nicht ein Workspace
|
||||
|
||||
`src-tauri/Cargo.toml` bindet nur den Tauri-Host (`cad-tauri`) als Workspace-
|
||||
Mitglied; `render2d/render3d/geometry/kernel2d/trucksolid/dwgimport` sind
|
||||
**eigenständige Cargo-Packages**, die dem Host nur optional (Features
|
||||
`native2d`/`native3d`, standardmässig AUS) als Path-Dependency zugespielt
|
||||
werden. Jedes Crate muss headless (`cargo test`) UND per `wasm-pack --features
|
||||
web` bauen, ohne den Tauri-Toolchain-Zwang zu erben — bewusst so geschnitten.
|
||||
Geteilte Abhängigkeiten: `wgpu 29`/`naga 29` (render2d+render3d, versionsgekoppelt
|
||||
wegen `glyphon 0.11`), `truck-modeling`+`csgrs`(gepinnter Git-Rev)+`nalgebra`
|
||||
nur in `trucksolid`.
|
||||
|
||||
### 2.7 Zwei Desktop-Rahmen: Tauri (macOS) + Electron (Linux)
|
||||
|
||||
Die App läuft plattformabhängig in **zwei verschiedenen nativen Rahmen** —
|
||||
das ist Absicht, kein Wildwuchs, und hängt an **WebGPU**:
|
||||
|
||||
- **macOS → Tauri.** WKWebView unterstützt WebGPU, das die render2d/render3d-
|
||||
WASM-Engines brauchen. `src-tauri/tauri.conf.json` (Identifier
|
||||
`ch.dossier.cad`, eigene Titelleiste, `trafficLightPosition`) +
|
||||
`isTauriRuntime()`-Gates an 6+ Stellen in `App.tsx` + vier eigene native
|
||||
Zusatzfenster (`src/native/`: Resources, Settings, DrawingLevels,
|
||||
LayerSettings, ContextImport).
|
||||
- **Linux → Electron.** Tauris Linux-Webview **WebKitGTK unterstützt WebGPU
|
||||
nicht zuverlässig** → dort läuft die App über eine Electron/Chromium-Shell
|
||||
(`scripts/electron-main.cjs` + `electron-preload.cjs`, gestartet via
|
||||
`npm run electron`). Der Kommentar in `electron-main.cjs` sagt es explizit:
|
||||
„Ersetzt WebKitGTK durch Chromium, damit WebGPU zuverlässig läuft."
|
||||
|
||||
Beide teilen sich **dieselbe** React-App und dieselbe randlose eigene
|
||||
Titelleiste; die Laufzeit erkennt den Host über `window.__TAURI__` (Tauri)
|
||||
bzw. `window.dossierWindow` (Electron, per `contextBridge` injiziert). Die
|
||||
Fenstersteuerung (`src/ui/WindowControls.tsx`) ist an beide Wege angebunden.
|
||||
Kein Rust-Backend nötig auf dem Electron-Pfad — `computeJoins` hat einen
|
||||
TS-Fallback (`src/compute/index.ts`). Electron ist also **kein** totes Gleis,
|
||||
sondern der aktive Linux-Zielrahmen.
|
||||
|
||||
## 3. Feature-Inventar (was tatsächlich funktioniert)
|
||||
|
||||
### Modell & Bauteile
|
||||
- Mehrschichtige Wände (`WallType.layers[]`) mit L-Eck-Gehrung UND
|
||||
Prioritäts-T-/X-Stössen (`joinPriority` am Component) — **fertig**, in 2D-Plan,
|
||||
3D-Viewport UND 3D-Live-Schnitt konsistent (Rust-Port `section_boolean.rs`).
|
||||
- Parametrische Wände (`ParametricWall`: Grid/Modul/Sequenz/Referenzlinie/
|
||||
bedingte Dicke) lösen sich zu konkreten `Wall[]` auf.
|
||||
- Decken (Slabs, `ceilings?`) mit Aussparungen, eigenem Typkatalog.
|
||||
- Türen/Fenster gehostet in Wänden, mit Rahmen/Zarge/Blockrahmen, Kämpfer,
|
||||
Oberlicht, Detailgrad grob/mittel/fein (2D UND 3D), Schwenkbogen; daneben
|
||||
existiert weiterhin ein **älteres, separates `Door[]`** neben `Opening[]` —
|
||||
laut PENDENZEN.md explizit als offene Doppelspur/Aufräum-Punkt vermerkt.
|
||||
- Treppen (gerade/L/Wendel), geschossübergreifend, 2D-Symbol mit Lauflinie/Pfeil.
|
||||
- Dächer (Flach/Pult/Sattel/Walm/Mansarde/Zelt) über Rechteck-Umriss, First/
|
||||
Traufe/Grat im 2D-Plan.
|
||||
- Stützen (Column) mit Profilbibliothek (Quadrat/Rechteck/Rund/I/Rohr).
|
||||
- Räume (SIA-416: HNF/NNF/VF/FF/GF/AGF) mit automatischer Bilanz + CSV-Export,
|
||||
Raumstempel-Editor (Drag&Drop-Felder).
|
||||
- Extrudierte Volumenkörper (truck-Integration: konkave Profile, Verjüngung).
|
||||
- Kontext-Layer (Terrain-TIN, importierte Meshes, Konturen) — semantisch getrennt.
|
||||
|
||||
### Zeichnen & Bedienung
|
||||
- Rhino-artiges Kommandosystem (`commands/`): getippte Koordinaten
|
||||
(`5,3`/`r5,3`/`5<45`), Tab-Feld-Zyklus für Präzisionseingabe
|
||||
(`src/ui/CommandLine.tsx`), Alias/Autocomplete, ~25 Kommandos (wall, ceiling,
|
||||
opening, stair, column, roof, room, line/polyline/rect/circle/arc, text,
|
||||
move/mirror/copy/offset/trim/join, extrude, import, terrain, measure,
|
||||
Schnittlinie, Georef).
|
||||
- Snapping (Endpunkt/Mitte/Schnittpunkt/Lot/Raster/Ortho), Grips, Array,
|
||||
Trim/Split/Join, 2D-Booleans (Union/Subtract/Intersect via `polygon-clipping`).
|
||||
- Messwerkzeug (Polygonzug, Länge + Fläche).
|
||||
- Rich-Text-Annotationen (Bold/Kursiv/Hoch-/Tiefstellung).
|
||||
|
||||
### Darstellung / Ressourcen
|
||||
- Resource Manager: Line/Hatch/Component-Manager, Wand-/Decken-/Tür-/Fenster-/
|
||||
Treppen-/Dach-Typeditoren — als eigenständiges natives Fenster (nicht als
|
||||
Dock-Panel).
|
||||
- Materialbibliothek: 13 fest gebündelte PBR-Starter (ambientCG, lokale
|
||||
Texturen) **plus** Live-Suche der kompletten ambientCG-Bibliothek (Auflösung
|
||||
1K/2K/4K, on-demand Download+Entpacken via `jszip`, Proxy wegen CORS) — heute
|
||||
bereinigt (siehe Commit-Historie dieser Session).
|
||||
- Regelbasierte Overrides (Bedingung → Farbe/Strichstärke/Schraffur/Sichtbarkeit).
|
||||
- Detailgrad grob/mittel/fein je Bauteil + Dokument-Override.
|
||||
- Hell-/Dunkel-Theme, Akzentfarben.
|
||||
|
||||
### Pläne / Output
|
||||
- Ausschnitte (View-Snapshots: Kamera, Massstab, Detailgrad, Sichtbarkeiten,
|
||||
Override-Preset) in Ordnerstruktur.
|
||||
- Layout-Blätter (Plansätze): Papierformat/-grösse, mehrere Viewports pro
|
||||
Blatt, Masterlayout-Vererbung (Titelblock), Ordnerstruktur, freie 2D-Annotationen.
|
||||
- Vektor-Export: PDF (Einzelblatt UND **Mehrseiten pro Ordner**,
|
||||
`layoutPdf.ts::buildFolderPdf`), DXF, IFC4 (mit echten Fenster-/Tür-Löchern,
|
||||
deterministischen GUIDs), STL, OBJ, CSV-Bauteil-Schedule (volles Element-Set).
|
||||
- Kamera-Presets (Kardinal + Iso), Norden-Rotation.
|
||||
|
||||
### Import / Kontext
|
||||
- DXF (Konturen), DWG (`@mlightcad/libredwg-web`, WASM), `.lin`/`.pat`.
|
||||
- Swisstopo: swissBUILDINGS3D (radiusgenau zugeschnitten, nicht die ganze
|
||||
STAC-Kachel), swissALTI3D, SWISSIMAGE-Orthofoto-Draping, LV95↔WGS84,
|
||||
Georeferenzierung über EINEN Vermessungspunkt (E/N/H, `geoAnchor`).
|
||||
- OSM/Overpass-Kontextimport (7 Kategorien).
|
||||
- Terrain-Mesh-Generator.
|
||||
|
||||
### Desktop-Integration (Tauri)
|
||||
- Eigene randlose Fenster mit nativer Titelleiste (macOS-Ampel-Position).
|
||||
- Native Speichern/Öffnen-Dialoge (`plugin-fs`/`plugin-dialog`), eigenes
|
||||
`.obp`-Projektdateiformat.
|
||||
- OS-Level-Exklusiv-Lock gegen Doppelöffnen desselben Projekts (`fs4`-Crate).
|
||||
- Vier eigenständige native Zusatzfenster (Resources, Settings, DrawingLevels,
|
||||
LayerSettings, ContextImport) statt Overlay/Modal.
|
||||
- Native macOS-Menüleiste.
|
||||
- i18n de/en durchgängig, eigener `t()`-Mechanismus (kein i18next).
|
||||
|
||||
## 4. Mist-Liste — Befunde, Doku-Widersprüche, offene Fäden
|
||||
|
||||
### 4.1 Verwaiste WASM-Crates (gebaut, aber nirgends importiert)
|
||||
- **`src-tauri/geometry`** (1.057 LOC, 8 Tests) → `pkgGeometry` — **null**
|
||||
Importstellen in `src/`. Wand-Join-Mathe existiert redundant als TS
|
||||
(`src/model/joins.ts`) UND als Rust-Port, aber nur die TS-Version läuft.
|
||||
- **`src-tauri/dwgimport`** (284 LOC) → `pkgDwgImport` — **null** Importstellen;
|
||||
DWG-Import läuft stattdessen über `@mlightcad/libredwg-web` (npm-Paket).
|
||||
- **`src-tauri/kernel2d`** (3.383 LOC, 18 Tests) → `pkgKernel2d` — wird nur von
|
||||
einem Paritätstest (`kernel2d.parity.test.ts`) konsumiert, nicht produktiv.
|
||||
Die TS-Version `src/geometry/kernel2d.ts` (886 Zeilen) ist die tatsächlich
|
||||
laufende Implementierung. Laut PENDENZEN.md bewusst so belassen: ein
|
||||
Join-Benchmark zeigte WASM unter ~100 Wänden **langsamer** als die naive
|
||||
TS-Routine. Kein Bug, aber die drei Crates zusammen sind ~4.700 Zeilen Rust
|
||||
(+44 Tests), die aktuell nichts zur Laufzeit beitragen ausser einem
|
||||
Korrektheits-Cross-Check für kernel2d.
|
||||
|
||||
**Empfehlung:** entweder (a) `geometry`- und `dwgimport`-Crate + ihre
|
||||
`build:*`-Scripts entfernen (kein Nutzen, nur Wartungslast), oder (b)
|
||||
explizit als „Referenzimplementierung/Zukunftsoption" in ARCHITECTURE.md
|
||||
dokumentieren, damit niemand sie für aktiv hält.
|
||||
|
||||
### 4.2 Toter Code
|
||||
- `src/section/hlr.ts` + `occt.ts` + `occt-wasm.d.ts` (473 LOC) — OCCT-WASM-
|
||||
HLR-Spike aus Phase 0, **keine Aufrufer mehr**. Ersetzt durch die analytische
|
||||
Rust-Schnitt-Pipeline (§2.4). `opencascade.js` bleibt als npm-Dependency
|
||||
bestehen, obwohl nur noch dieser tote Code sie importiert.
|
||||
- `src/export/planToPrintSvg.ts` — Kommentar im Code selbst sagt „ERSETZT durch
|
||||
`sceneToPrintSvg.ts`", ist aber noch im Baum.
|
||||
- `Element`-Typalias (`src/model/types.ts:1601`) — definiert, nirgends benutzt.
|
||||
|
||||
### 4.3 Das grösste Doku-vs-Code-Problem: App.tsx
|
||||
CONVENTIONS.md verlangt seit Tag 1 „App.tsx bleibt dünner Shell, keine
|
||||
Geschäftslogik". `docs/design/state-architecture.md` plante explizit die
|
||||
Extraktion nach `src/views/` (View-Routing) und `src/menus/`
|
||||
(Kontextmenü-Aufbau). Beide Ordner **existieren nicht**. `App.tsx` ist mit
|
||||
**7.130 Zeilen** die grösste Einzeldatei des Projekts und enthält weiterhin
|
||||
View-Umschaltung und Kontextmenü-Konstruktion inline. Das ist der genaue
|
||||
„God-Component"-Zustand, den die Doku von Anfang an vermeiden wollte.
|
||||
|
||||
### 4.4 Bekannte Doppelspur: `Door[]` vs. `Opening[]`
|
||||
`Project` führt sowohl ein älteres `doors: Door[]` als auch das neuere,
|
||||
allgemeinere `openings?: Opening[]` (`kind:"door"|"window"`). Laut
|
||||
PENDENZEN.md ist das erkannt und als Aufräum-Punkt vorgemerkt, aber nicht
|
||||
konsolidiert.
|
||||
|
||||
### 4.5 Doku-Widersprüche (README/ARCHITECTURE/CONVENTIONS vs. Realität)
|
||||
| Dokument | Behauptung | Realität |
|
||||
|---|---|---|
|
||||
| README.md | Shell = Electron, Tauri „ausrangiert" | Falsch andersrum gedacht: **beide** sind aktiv — Tauri auf macOS (WKWebView+WebGPU), Electron auf Linux (WebKitGTK kann kein WebGPU); siehe §2.7 |
|
||||
| README.md | „HLR noch nicht ans UI verdrahtet, Views sind Stubs" | Der OCCT-HLR-Pfad stimmt (tot), aber Schnitte/Ansichten funktionieren real über eine andere, eigene Rust-Pipeline |
|
||||
| README.md | „Prioritäts-T-/X-Stösse … Risiko #1" unter „bewusst offen" | Seit 7.7. erledigt, inkl. Rust-Port |
|
||||
| README.md | „PDF-Export ist noch single-sheet" | `layoutPdf.ts::buildFolderPdf` erzeugt echte Mehrseiten-PDFs pro Ordner |
|
||||
| README.md | Engine-Liste nennt nur render2d/render3d | Es gibt 6 Rust-Crates (+kernel2d/geometry/trucksolid/dwgimport) |
|
||||
| CONVENTIONS.md | Struktur-Ziel `src/views/`, `src/menus/` | Existieren nicht; Logik liegt in `App.tsx` |
|
||||
| CONVENTIONS.md | Dev-Port 5173 | Tatsächlich 5187 (`vite.config.ts`, `tauri.conf.json`) |
|
||||
| ARCHITECTURE.md | Ziel-Struktur `store/`, `sheets/`, `workers/geometry.worker.ts` (Comlink) | Tatsächlich `state/`, `panels/layoutModel.ts`; kein Worker/Comlink irgendwo im Projekt |
|
||||
| ARCHITECTURE.md | HLR „im Web Worker (Comlink)" | Kein Comlink im Projekt; Schnitt läuft synchron GPU-seitig in Rust |
|
||||
| HANDOVER.md | Neuester Block: „Stand 2026-07-09" | Jüngster Commit + PENDENZEN.md sind vom 17.–20.7. — 8+ Tage/mehrere Sessions veraltet |
|
||||
|
||||
### 4.6 Codequalität — besser als der Tempo vermuten lässt
|
||||
Trotz 3 Wochen / 362 Commits über viele autonome Sessions: **keine** FIXME/HACK/
|
||||
XXX-Marker im ganzen Projekt; nur 2 TODOs (beide bekannt/harmlos: Geländer in
|
||||
`Viewport3D.tsx:2649`, Kanten-Tiefe in `toSection.ts:1093`); `eslint-disable`
|
||||
fast ausschliesslich `react-hooks/exhaustive-deps` (bewusst); keine
|
||||
`_v2`/`_old`/`_backup`-Dateileichen. Die eigentliche Aufgabenliste lebt
|
||||
diszipliniert in PENDENZEN.md statt in Code-Kommentaren verstreut — gesünder
|
||||
als der Durchschnitt für dieses Bau-Tempo.
|
||||
|
||||
### 4.7 Genuine offene Punkte (Auszug aus PENDENZEN.md, Details dort)
|
||||
- Geo-Block: Layer-Zuordnung für importierte Gebäude/Terrain, reale Höhen +
|
||||
Projekt-müM-Draping, SWISSIMAGE-Draping.
|
||||
- 3D-Feinschliff: Fensterrahmen-Ecken bei „fein" überlappen (kein Gehrungs-
|
||||
Union), Dach-First/Grat hat unverschmolzene Dreiecke, unerklärte
|
||||
Vertikalstreifen auf oberen Wandflächen (undiagnostiziert).
|
||||
- truck-Boolean nicht an die Wand/Öffnungs-Pipeline angeschlossen (Scope-
|
||||
Entscheid mit Nutzer ausstehend).
|
||||
- Feld-Controller (Tab-Zyklus) fehlt für Body-Move und für 3D-Griffe generell
|
||||
(nur 2D-Einzelpunkt-Drag hat ihn); 3D-Griff-Drag hat gar kein Snapping.
|
||||
- `make2D`-Kommando (3D→flacher 2D-Plan mit Füllungen) ungebaut.
|
||||
- Ribbon-3D-Tab leer; einige Punkte visuell noch nicht in Tauri abgenommen
|
||||
(u. a. Materialfarben-Textur-Array, Schraffur-Schnittfüllung — laut
|
||||
PENDENZEN als „[~] implementiert, aber unverifiziert" markiert).
|
||||
- DWG/DXF-Domänen-Mapping (Entitäten → Wände/Öffnungen) unbegonnen; DWG-Schreiben
|
||||
fehlt (Lesen über `libredwg-web` vorhanden).
|
||||
- Teamwork/Kollaboration (Supabase) bewusst nicht begonnen, gilt als späte Phase.
|
||||
|
||||
## 5. Vergleich: Tag-1-Vision vs. heute
|
||||
|
||||
| Vision (28./29.6.) | Heute |
|
||||
|---|---|
|
||||
| Three.js als einziger 3D-Renderer, OpenCascade.js für Booleans/HLR | Eigene Rust/WASM-Engines („Nordstern") für 2D+3D; three.js nur noch „Free"-Fallback; OCCT-Pfad tot |
|
||||
| Ein `Element[]`-Union | Typisierte Arrays pro Bauteiltyp auf `Project` |
|
||||
| Zustand-Store | Eigener `useSyncExternalStore`-Store |
|
||||
| Web Worker + Comlink für HLR | Synchrone, analytische Rust-GPU-Schnitt-Pipeline |
|
||||
| „Booleans: Entscheidung in Phase 0" | Trucksolid/csgrs-CSG existiert, ist getestet, aber nicht an Wände angeschlossen |
|
||||
| web-ifc für IFC | Eigener IFC4-Writer (`exportIfc.ts`) |
|
||||
| Phase 2–5 grösstenteils „Backlog" | Treppen, Dächer, Stützen, SIA-416, Swisstopo, OSM, Kamera-Presets, Layouts, Ausschnitte, Terrain — alles bereits gebaut |
|
||||
|
||||
Kurz: die **Prinzipien** (ein Modell, viele Ableitungen; Darstellung erst beim
|
||||
Rendern; keine Cache-Stale-Bugs) haben gehalten und wurden korrekt umgesetzt.
|
||||
Die **konkreten Technologie-Entscheidungen** sind fast durchgängig anders
|
||||
gelaufen als geplant — meist zugunsten einer eigenen, schnelleren Rust/WASM-
|
||||
Lösung statt einer Drittbibliothek.
|
||||
+8
-1
@@ -1,6 +1,13 @@
|
||||
# Dokumentation — Standalone Browser-BIM (cad)
|
||||
|
||||
> Stand: 2026-06-29 · Übergeordnet: [ROADMAP.md](../ROADMAP.md) (Vision & Phasen) ·
|
||||
> Stand: 2026-06-29 · **Historische Recherche-/Design-Dokumente aus der ersten
|
||||
> Woche.** Mehrere Kern-Empfehlungen hier (replicad/OCCT als B-Rep-Kernel,
|
||||
> Manifold, web-ifc, `three/webgpu`) wurden im tatsächlichen Bau **nicht**
|
||||
> umgesetzt — stattdessen entstanden eigene Rust/WASM-Rendering-Engines
|
||||
> („Nordstern"). Für den aktuellen Ist-Zustand: **[../STATUS.md](../STATUS.md)**
|
||||
> und **[../ARCHITECTURE.md](../ARCHITECTURE.md)**. Die Dokumente unten sind als
|
||||
> Recherche-Hintergrund weiterhin lesenswert, aber nicht mehr aktueller Plan.
|
||||
> Übergeordnet: [ROADMAP.md](../ROADMAP.md) (Vision & Phasen, ebenfalls historisch) ·
|
||||
> [CONVENTIONS.md](../CONVENTIONS.md) (Konventionen) · [ARCHITECTURE.md](../ARCHITECTURE.md).
|
||||
|
||||
Dieses Verzeichnis bündelt die Recherche- und Design-Dokumente für `cad`, die
|
||||
|
||||
@@ -154,889 +154,6 @@
|
||||
"displacement": "/assets/materials/Ground103/displacement.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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+5
-890
@@ -1,7 +1,9 @@
|
||||
// Auto-generated built-in material library (ambientCG, CC0 1.0).
|
||||
// Static asset paths under /public; loadable directly via three.js TextureLoader.
|
||||
// Eingebaute Material-Bibliothek (ambientCG, CC0 1.0) — 13 fest gebündelte
|
||||
// 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.
|
||||
// Broad set populated by: node scripts/fetch-materials.mjs --library <DOSSIER-LIBRARY-path>
|
||||
|
||||
export type MaterialMapKind =
|
||||
| 'color'
|
||||
@@ -179,893 +181,6 @@ export const MATERIAL_LIBRARY: MaterialAsset[] = [
|
||||
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;
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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
@@ -2080,9 +2080,16 @@ body {
|
||||
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 {
|
||||
fill: var(--accent-dim);
|
||||
fill-opacity: 0.35;
|
||||
stroke: var(--accent);
|
||||
stroke-width: 2;
|
||||
vector-effect: non-scaling-stroke;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user