47 Commits

Author SHA1 Message Date
karim 3402e6ac96 HANDOVER: Übergabe an neue Instanz — Nordstern 2/3/4/5 gelandet, Schnitt-UI-Anbindung offen 2026-07-03 18:18:52 +02:00
karim b4c4a2cc6b wgpu 22 → 29, glyphon 0.6 → 0.11: requestDevice-Shim entfällt
glyphon 0.11 ist die neueste zu wgpu 29 passende Version (wgpu 30 existiert
bereits, glyphon pinnt aber ^29). Das 22er-Limit maxInterStageShaderComponents
wird von 29 nicht mehr in requiredLimits gesendet — src/engine/requestDeviceShim.ts
komplett entfernt, beide Hook-Importstellen (useWasmPlanRenderer, useWasm3dRenderer)
angepasst. pollster 0.3→0.4, naga 22→29 mitgezogen. Alle Draw-/Text-Pfade
(draw_sequence, glyphon ColorMode::Web, widthScreen-Polylinien, Headless/Golden)
unverändert funktionsfähig.

Verifiziert: cargo check nativ (render2d/render3d/src-tauri) grün, cargo test
render2d 17/17 + Golden bit-exakt, render3d 29/29, wasm32 --features web für
beide grün, npx tsc -b + npm run build grün.
2026-07-03 18:17:41 +02:00
karim 382771b2e7 Vektor-PDF aus der RenderScene: eine Szene, zwei Targets
exportPdf baut jetzt planToRenderScene(plan) — identischer Aufruf wie der
Engine-Viewport — und serialisiert über den neuen sceneToPrintSvg nach
Papier-mm: z-stabile Reihenfolge wie compile_scene, PEN_STEPS-Quantisierung
wie bisher, widthScreen-Schraffurbreiten via (widthPx/PX_PER_M)*(1000/N)
in mm umgerechnet, Texte neu als echte Vektor-Texte (alter Pfad liess sie
weg). planToPrintSvg bleibt als Referenz, im Kopf als abgeloest markiert.
probe-pdf.mjs: Icon-Button-Selektor nachgezogen, neue Assertions (Schraffur-
Polylinien vorhanden, alle stroke-widths auf PEN_STEPS). Messung: 3666
Vektor-Pfadoperatoren, 7 Text-Operatoren, 0 Rasterbilder; Inhalt per
pdftoppm 53.51x43.69 mm vs. erwartet 53.45x43.45 bei 1:100.
2026-07-03 08:46:35 +02:00
karim a8e491e534 HANDOVER: Stand Nordstern-Session 2026-07-03 (Engine-Meilensteine, offene Agent-Arbeit) 2026-07-03 08:42:27 +02:00
karim 76029dbf68 render3d-Schnitt: Öffnungen — Brüstung/Sturz-Teilpolygone und Durchblick
WallInput bekommt openings (from/to/sill/height, serde-default). Der Cut
durch eine Öffnung liefert mehrere einfache Rechtecke (Brüstung, Sturz,
Leibungen) statt Lochpolygon — kompatibel zu render2d::FillPolygon. Der
Verdeckungstest behandelt Öffnungen als Durchblick (u-Zuordnung exakt im
Elevationsfall, konservativ undurchsichtig bei kantennaher Ausrichtung);
Fensterrahmen-Kanten werden als projizierte Kanten ausgegeben. Tests
26 auf 29, Beweis-SVG mit Fenster + dahinterliegender Wand neu generiert
(sichtbares Band exakt im Fensterausschnitt). mesh.rs (3D-Vollkoerper)
beruecksichtigt Öffnungen weiterhin nicht — dokumentierte Luecke.
2026-07-03 08:36:35 +02:00
karim 4b02e40238 render2d: Headless-Rendering — PNG ohne Fenster + Golden-Image-Test
HeadlessRenderer (Feature headless) baut Instance/Adapter/Device ohne
Surface (Vulkan, kein Display-Server); render_to_image rendert in eine
Rgba8Unorm-Texture (non-sRGB wie ColorMode::Web) und liest mit 256-Byte-
Row-Padding zurueck. Draw-Code unveraendert geteilt mit dem Fenster-Pfad.
CLI-Bin render_png (Demo-Szene, 990x630); Demo additiv um 45-Grad-
Schraffur, Tuerblatt und Schwenkbogen erweitert, damit das Golden alle
Pfade abdeckt. Golden-Test mit eingechecktem Referenzbild: 0/623700 Pixel
Abweichung, bit-exakt reproduzierbar; bei Abweichung Diff-Dump nach
target/. Doku in docs/design/engine-headless.md.
2026-07-03 08:31:38 +02:00
karim ce6bd26ff7 2D-Engine-Parität: WASM-Pfad rendert deckungsgleich zum SVG-Referenzpfad
Sechs Lücken geschlossen: Text-Massstab vom Geometrie-Massstab entkoppelt
(set_text_scale, spiegelt SVG-Referenzskala); glyphon auf ColorMode::Web
(Text war linear-konvertiert zu dunkel); z-basierte Maler-Reihenfolge
(interleavte draw_sequence statt fills-vor-lines, Alt-Szenen unverändert);
CSS-Klassenfarben/-Opacities in toRenderScene gespiegelt (Türschwenk etc.);
greyed-Dimmung 0.3 auf allen Primitiven; Dämmschraffur am Modell-Ursprung
verankert (userSpaceOnUse), exakte Bézier-Wellenform statt Sinus und
kachelgekoppelte Strichbreite (widthScreen-Modus). Probe
scripts/probe-engine-parity.mjs vergleicht ?gl=0 gegen ?engine=wasm;
Rest-Diff nur AA/Glyphen-Rasterung. cargo 17/17, vitest 94/94, Builds grün.
2026-07-03 08:17:01 +02:00
karim 54211b1443 render3d: Schnitt-Modul — Cut-Polygone + sichtbare/verdeckte Kanten aus Prismen
Analytische Eigen-Engine-Alternative zum OCCT-HLR-Spike: Schnittebene
(Punkt+Normale) gegen extrudierte Fussabdruck-Prismen; Cut-Polygone in
(u,v)-Schnittkoordinaten mit Komponenten-Referenz (spaetere Schraffur),
Projektion der dahinterliegenden Kanten mit Verdeckungstest, getrennt
visible/hidden. SectionOutput serde-serialisierbar (Meter). 26 Tests,
Example section_svg schreibt docs/welle-c-hlr-spike/section-engine-proof.svg
(L-Wand+Bodenplatte: Poché, durchgezogene sichtbare, gestrichelte verdeckte
Kanten). Offene Punkte in docs/design/engine-section-pipeline.md.
2026-07-03 08:13:57 +02:00
karim c8a4188618 render3d im Browser: WASM/WebGPU-3D-Viewport hinter ?engine=wasm
Feature web (wasm-bindgen) + cdylib analog render2d; WebModelRenderer
mit Canvas-Surface, set_model (walls/slabs wie der native Push) und
set_camera. Projektion liefert bereits [0,1]-Clip-Z, math.rs unveraendert.
wgpu-22-requestDevice-Shim in src/engine/requestDeviceShim.ts geteilt.
Neuer Hook useWasm3dRenderer + Wasm3DViewport (Orbit/Pan/Zoom wie three.js-
Sicht); Viewport3D dispatcht per ?engine=wasm bzw. localStorage, three.js
bleibt Default. Build-Script build:engine3d (wasm-pack, src/engine/pkg3d).
Verifiziert headful per scripts/probe-engine3d.mjs (37 % Geometrie-Pixel);
headless praesentiert Chromium keine WebGPU-Frames (auch bei render2d).
2026-07-03 07:41:37 +02:00
karim 7ca5197b6b Resource Manager: Materialien-Tab mit PBR-Kugel-Vorschau, Suche und Kategorie-Chips
Geteilter Offscreen-three.js-Renderer (ein Kontext, serielle Queue, Cache
per Map-Signatur) rendert 128px-Kugeln lazy via IntersectionObserver.
Kategorie-Chips aus der Bibliothek abgeleitet, uneinheitliche Manifest-
Schreibweisen normalisiert; Suche und Kategorie kombinierbar. Kachel-Klick
markiert aktiv (gleiche Sprache wie MaterialPicker). Puppeteer-Probe
scripts/probe-material-tiles.mjs prüft Grid, Lazy-Render und Filter.
2026-07-03 02:04:33 +02:00
karim a010584d9b README: kritisch überarbeitet — Parametric Walls, PDF/DXF-Export, Materials-Lib, HLR-Spike-Stand ergänzt; Dev-Port korrigiert 2026-07-03 00:19:54 +02:00
karim e6d061cc91 README: keine Browser-App mehr, sondern Electron-Desktop-Shell mit eigener Engine 2026-07-03 00:10:24 +02:00
karim 0161b0231d HANDOVER: Electron-WebGPU-Befund korrigieren (funktioniert, kein Fallback)
Der vorherige Eintrag ging von einem WebGL2-Fallback aus; genauere Messung
(GPU-Status erst nach Initialisierung abfragen statt sofort bei whenReady)
zeigt: WebGPU/?engine=wasm laeuft in Electron einwandfrei, per Screenshot
verifiziert (RENDERER-Anzeige zeigt Engine aktiv).
2026-07-03 00:00:00 +02:00
karim 34317e53f4 Electron-Prototyp: eigenes App-Fenster statt Tauri/WebKitGTK
Ersetzt den Tauri-Webview-Weg testweise durch eine Electron-Shell (randlos,
ohne native Menüleiste, wie chromium-shell.sh), damit die WASM/WebGPU-Engine
zuverlässig laeuft statt in WebKitGTK. Rust-Backend nicht noetig, da
compute_joins einen TS-Fallback hat. npm run electron zum Starten.
2026-07-02 23:51:23 +02:00
karim 27e41077b1 Renderer-Umschalter: Wahl in localStorage persistieren (uebersteht Neustart ohne URL-Param) 2026-07-02 23:38:05 +02:00
karim a37b1c0cc9 UI: Renderer-Umschalter (WebGL2 / eigene Engine) in der Statusleiste 2026-07-02 23:35:01 +02:00
karim 4eb635cc89 WASM-Viewport: SVG-Raumtext bei aktiver Engine unterdruecken (kein Doppeltext) 2026-07-02 23:28:25 +02:00
karim 4734c04ec0 HANDOVER: naechster Schritt = Electron-Prototyp (Rezept fuer neue Session) 2026-07-02 23:18:27 +02:00
karim 2f971a54c0 HANDOVER: Richtungsentscheidung Electron/Chromium+WASM statt Tauri; all-native als Fernziel 2026-07-02 23:02:35 +02:00
karim 8cfe6c2521 HANDOVER: Engine-Nordstern — exakte Breiten, Druck=Bildschirm, Headless, HLR, WebKit-Unabhaengigkeit 2026-07-02 22:31:27 +02:00
karim 665cbce1f9 render2d im Browser: WASM/WebGPU-Viewport hinter ?engine=wasm
Integrations-Spike: die native 2D-Engine (src-tauri/render2d) laeuft jetzt als
WASM/WebGPU-Canvas in der App-UI, mit derselben Szene (planToRenderScene) und
ViewBox wie der WebGL2-Pfad; das SVG-Overlay (Text/Griffe/Auswahl) bleibt drueber.

- render2d: neues Feature "web" (wasm-bindgen/web-sys), Fassade WebPlanRenderer
  (new/set_scene/set_view_box/set_paper_scale/resize/render) auf Canvas-Surface.
  Geteilte GPU-Schicht (gpu.rs), native Fensterschicht (winit) unberuehrt.
- Font: cosmic-text findet auf wasm keine Systemfonts -> Inter-Bytes eingebettet
  (assets/Inter.ttf), Renderer.load_font + text_family aus der geladenen Familie.
- Frontend: useWasmPlanRenderer (gleiche Schnittstelle wie useGlPlanRenderer);
  PlanView waehlt die Engine per ?engine=wasm + navigator.gpu, sonst WebGL2/SVG.
- Build: npm-Script build:engine (wasm-pack, target web -> src/engine/pkg,
  gitignored). chromium-shell.sh mit WebGPU-Flags (--enable-unsafe-webgpu/Vulkan).
- Kompat-Shim: wgpu 22 sendet das spec-entfernte Limit maxInterStageShaderComponents,
  das neuere Chromium ablehnen -> im Hook vor requestDevice entfernt.

Verifiziert: cargo test -p render2d gruen (18), tsc gruen, wasm-Build gruen;
Headless-Chromium (?engine=wasm) zeichnet den Grundriss (Waende/Schraffur/
Tuerschwenk/Treppe/Raumtext) im WASM-Viewport, Fallback ohne Flag unveraendert.
2026-07-02 22:30:47 +02:00
karim 6eead6d493 2D-Bögen analytisch: exakter Kreis-Shader statt Segment-Tessellierung
Bögen im nativen 2D-wgpu-Renderer werden nicht mehr zoomabhängig in Segmente
zerlegt, sondern per SDF-Fragment-Shader (ARC_WGSL) mathematisch exakt rund
gerendert — bei jeder Zoomstufe ein echter Kreis, kein Vieleck, ohne
Neu-Tessellierung.

- compile_scene sammelt je Bogen EINE analytische Instanz (ArcInstanceData,
  Bildschirm-Raum-Parameter + Dash in Modell-Metern), zoom-invariant.
- Eigene Arc-Pipeline (ein Frame-Uniform, Quad je Instanz aus vertex_index):
  radiale Kante, Butt-Cap-Winkelclamp (beide Sweep-Vorzeichen) und Dash
  (Bogenlänge modulo Muster) analytisch antialiased; Strichbreite mit derselben
  mm->px-Formel wie die Linien.
- tessellate_arc + Zoom-Retessellierungs-Cache (last_scene/arc_px_per_m/
  maybe_retessellate) entfernt; upload_scene ohne px_per_m.
- Tests auf die neue Semantik umgeschrieben (Winkel-Parität, Bounding-Box,
  Dash-Mapping), ARC_WGSL per naga validiert.
2026-07-02 22:07:18 +02:00
karim 7b9f72e79f 2D-Plan: Strichmuster (Umriss/Linie/Bogen) im nativen wgpu-Renderer zeichnen
Papier-mm-Strichmuster (dash) wurden bisher komplett ignoriert und immer
durchgezogen gerendert — im nativen 2D-Fenster gab es dafür bislang gar keinen
Mechanismus. `split_dash` portiert den bereits für Schraffuren bewährten
`applyDashRuns`-Algorithmus (glPlanHatch.ts) nach Rust und zerlegt einen
Linienzug in seine "an"-Teilstücke; die Phase läuft dabei über bereits
verkettete Zeichnungszüge UND über die neu adaptiv tessellierten Bögen
hinweg durch, sodass z. B. der gestrichelte Türschwenk-Bogen gleichmäßig
gestrichelt bleibt statt an jedem Segment neu anzusetzen.
2026-07-02 21:45:57 +02:00
karim cd0f976b4f 2D-Plan-Qualität: Türschwenk-Bögen adaptiv rund tessellieren (nativer wgpu-Renderer)
Bögen (Türschwenke) wurden bisher einmalig in JS in eine feste Facettenzahl
zerlegt; beim Hineinzoomen wurden die Facetten sichtbar. Die Zerlegung
(`tessellate_arc`) wandert nach Rust und läuft jetzt zoomabhängig anhand einer
Sehnenabweichungs-Toleranz (Sagitta ≤0.3 Geräte-px), Segmentzahl auf 8..512
geklemmt. Die native GPU-Szene bekommt dafür einen eigenen `Arc`-Primitiv-Typ
(unvortessellliert); der Renderer merkt sich die zuletzt hochgeladene Szene und
tessellliert Bögen automatisch neu, sobald sich der Zoom seit dem letzten
Upload um mehr als Faktor 1.3 verändert hat.
2026-07-02 21:43:55 +02:00
karim 919bc67bfa 3D: Geschossdecken als extrudierte Polygone + hemisphärisches Licht
Deckenplatten (SlabInput) werden im render3d aus dem Grundriss-Umriss per
Ear-Clipping trianguliert und über die Deckendicke extrudiert (Deckel/Boden/
Mantel mit robust nach außen orientierten Normalen). Payload erweitert auf
{ walls, slabs } — blanke Wand-Arrays bleiben kompatibel. Beleuchtung auf
hemisphärisches Ambient (Himmel/Boden) + Directional-Sonne umgestellt, dezente
Kantenbetonung, hellerer Hintergrund (#f5f5f5). Beispiel-Geschossdecke im EG.
2026-07-02 21:08:34 +02:00
karim ec5cbf6fa0 3D-Politur: ACES-Tonemapping, PMREM-Environment, Standard-Materialien
Viewport3D nutzt jetzt ACESFilmicToneMapping + sRGB-Ausgabe (weiche
Lichter statt hartem Clipping) und eine neutrale Raum-Umgebung
(PMREMGenerator + RoomEnvironment) als IBL-Quelle für plausible
Reflexionen. Wände/Decken/Öffnungsrahmen/Glas/Türblatt/Treppen und das
Auswahl-Highlight laufen von MeshLambertMaterial auf MeshStandardMaterial
um (Rauigkeit/Metallizität je Bauteilart, moderate envMapIntensity);
direktes Licht entsprechend zurückgenommen, da die Umgebung nun mit-
trägt. Hintergrundfarbe, Hidden-Line- und Clay-Modus unverändert.
2026-07-02 21:04:22 +02:00
karim f1b802299f 3D-Wände mit Öffnungen: Türen/Fenster als Teilquader (Pfeiler+Brüstung+Sturz) 2026-07-02 21:00:19 +02:00
karim 0337422ae6 Echte Fonts im nativen 2D: glyphon-Glyphenatlas statt Browser-Overlay
Raumstempel-Text rendert jetzt im nativen wgpu-Fenster mit echten
TrueType-Glyphen (Inter, Systemfonts via cosmic-text) ueber einen
GPU-Glyphen-Atlas — im selben 4x-MSAA-Pass nach der Geometrie.
Schriftgroesse in Papier-mm (gleiche Formel wie Strichbreiten, skaliert
mit dem Zoom); die Bridge flacht Rich-Text-Stempel + Zusatzzeilen
zeilenweise auf serde-kompatible texts ab (Layout wie der SVG-Pfad).
2026-07-02 20:28:40 +02:00
karim d39244989b 2D-Umrisse polygonuebergreifend naehen: Gehrung an Wand-zu-Wand-Ecken 2026-07-02 20:12:29 +02:00
karim 01424c1e22 Natives 2D/3D live: Webview pusht Szene/Waende per Tauri-Command
Die nativen wgpu-Fenster spiegeln jetzt das LIVE-Modell statt des
eingefrorenen JSON-Snapshots: die Webview schiebt bei jeder Aenderung
(debounced 200 ms) den sichtbaren Plan (planToRenderScene) bzw. die
Projekt-Waende (projectToWalls3d) per push_native_scene/push_native_walls
an einen EventLoopProxy<UserEvent> der winit-Loop. Ansicht wird nur neu
eingepasst, solange im Fenster noch nicht gepannt/gezoomt/orbitiert
wurde; die JSON-Assets bleiben Start-Fallback. Ohne Tauri: No-op.
2026-07-02 20:10:38 +02:00
karim 1811e82005 Chromium-App-Shell: fluessiger Launcher fuer die CAD-Oberflaeche
npm run shell startet den Vite-Dev-Server bei Bedarf und oeffnet ihn in
einem randlosen Chromium-Fenster, um den WebKitGTK-Cairo-Flaschenhals
von tauri:dev zu umgehen.
2026-07-02 19:54:11 +02:00
karim 0a1aaedef8 render2d: 4x MSAA fuer glatte Linien (wie im Browser)
Beide Pipelines (Fill/Line) rendern mit sample_count=4 in eine lazily verwaltete
Multisample-Textur (ensure_msaa, Muster analog render3d::ensure_depth) und
resolven in die Surface-View. Kantige Haarlinien im nativen 2D-Viewport werden
dadurch knackscharf; Renderer-API unveraendert.
2026-07-02 19:44:14 +02:00
karim 483ad3f278 Nativer 2D-Viewport: Raumstempel-Text wieder entfernen
Eine Einstrich-Vektorschrift sieht neben dem echten Browser-Font schlecht aus;
Text bleibt dem Browser-/Overlay-Pfad ueberlassen. strokeFont.ts entfernt,
toRenderScene ueberspringt Text-Primitive wieder. Schraffuren/Poche/Ecken bleiben.
2026-07-02 19:40:19 +02:00
karim d27622d581 Nativer 2D-Viewport: Raumstempel-Text (Einstrich-Vektorschrift)
Neues strokeFont.ts (kompakte Einstrich-Schrift: A-Z, 0-9, Symbole inkl. ²/·).
toRenderScene wickelt Text-Primitive (Doc-Zeilen + Live-Zusatzzeilen) zu Glyph-
Polylinien in Modell-Metern ab (vertikal um den Anker zentriert, Ausrichtung je
Absatz) und speist sie wie die Schraffuren in die render2d-Szene. Erste Fassung
in Versalien; render2d bleibt textrenderer-frei. Damit zeigt der native Grundriss
den Raumstempel (Name/Fläche/SIA) analog zum Browser.
2026-07-02 19:05:01 +02:00
karim f315c80748 Nativer 2D-Viewport: Schraffuren wie im Browser
toRenderScene emittiert jetzt die aufs Polygon geclippten Musterlinien (glPlanHatch:
buildHatchRuns + applyDashRuns) als Polylinien — Daemmung/Diagonal/Kreuz erscheinen
im nativen wgpu-Grundriss identisch zum WebGL-Pfad. 'none'/'solid' brauchen keine
Linien (solid deckt die Fuellung farbig ab).
2026-07-02 18:58:13 +02:00
karim bda256d3a4 Native wgpu-Viewports (2D+3D) im Tauri-Prozess: echtes Modell + gehrte Ecken
- native.rs: EINE winit-EventLoop hostet 2D- und 3D-Fenster (winit erlaubt nur
  eine Loop pro Prozess) — loest den RecreationAttempt-Panic zweier Loops; ersetzt
  native2d.rs/native3d.rs. Feature-gegated (native2d/native3d, einzeln oder zusammen).
- render2d/render3d laden das ECHTE Modell aus assets/native2d_scene.json bzw.
  native3d_walls.json (Demo-Szene als Fallback); initialer Ausschnitt/Kamera aus
  den Modell-Grenzen gerahmt, initialer Redraw + gesetzte Fenstergroesse.
- TS-Konverter toRenderScene/toWalls3d + scripts/dump-native-scene erzeugen die
  JSON aus sampleProject/generatePlan (npm run dump:native).
- render2d: Scene.polylines fuer zusammenhaengende Umriss-/Zeichnungslaeufe →
  Gehrung statt Stumpfkappen an Wandecken/2D-Geometrien; MITER_LIMIT 4→8
  (deckungsgleich mit SVG stroke-miterlimit:8 und WebGL2).
- native3d-Feature + render3d-Pfad-Dep in der Tauri-Crate.
2026-07-02 08:54:26 +02:00
karim c93b168f5c Add native2d feature: wgpu 2D viewport window inside Tauri process
Spawns a GTK-free winit window with its own wgpu surface from Tauri's
setup hook on a background thread, sidestepping the WebKitGTK surface
contention on Wayland. Reuses render2d's renderer via a shared demo
module. Opt-in behind the native2d cargo feature; default build
unaffected.
2026-07-02 01:26:07 +02:00
karim a971cd610b 2D-Plan-Qualitaet: saubere Wandecken + glatte Dämmschraffur
Wandecken (generatePlan/glPlanCompile/PlanView): die diagonale Miter-Stirnkante
am L-Stoss wurde als Umriss gestrokt -> Barb-Ueberstand am Aussenapex + 45deg-
Naht zwischen gleichen Schichten. Fix: interne Join-Stirnkanten per neuem
noStrokeEdges vom Umriss ausnehmen (Fuellung bleibt volle Flaeche, laengs
laufende Materialfugen bleiben). Ecke = sauberer Miter ohne Ueberstand, gleiche
Schichten verschmelzen nahtlos ueber die Ecke. GPU- und SVG-Pfad teilen sich
noStrokeEdges.

Dämmschraffur (glPlanHatch): die Sinuswelle wurde pro Segment einzeln aufs
Polygon geclippt und mit Butt-Caps gestrokt -> fransige Fragmente an jeder
Wellenbiegung. Fix: kontinuierliche Punkt-Laeufe (clipPolylineToPolygon) als EIN
gehrter Streifen; Dash laeuft ueber die Laeufe. STEPS 12->22. Gerade Scharen
(diagonal/crosshatch) unveraendert.
2026-07-02 00:54:41 +02:00
karim 48bbcaed1e Nativer 3D-wgpu-Renderer (render3d, M0+M1): Wand-Extrusion, Kamera, Licht
Eigenstaendige Crate wie render2d (render/window-Stufung, serde-only Mesh-
schicht headless testbar). Wand-Extrusion (Band via Links-Normale, Quader mit
nach aussen zeigenden Normalen), handgerechnete Mat4 (perspektiv+ortho, wgpu-
Clip-Z [0,1], 5 Kamera-Presets), Directional-Light + Tiefenpuffer + Backface-
Culling. Orbit-Spike (cargo run --features window --bin spike3d). Plus Port-
Briefing mit M2..M9-Milestones (three.js-Viewport-Bestandsaufnahme).
2026-07-02 00:29:02 +02:00
karim 6d4b4e66c1 GPU-Schraffuren im WebGL2-Grundriss: diagonal/crosshatch/insulation + Dash
Muster in Modell-Metern erzeugt (massstabstreu wie SVG-<pattern>), konkav-
faehig auf das Fuellpolygon geclippt (even-odd-Scanline, halb-offene Kanten-
Konvention). Strichbreite in Papier-mm; Dash geometrisch aufgeloest. Emittiert
nach der Fuellung, vor dem Umriss (Stapelreihenfolge wie SVG). 12 Unit-Tests.
2026-07-02 00:28:11 +02:00
karim 27bb5a42ec Nativer 2D-wgpu-Renderer (render2d): Ear-Clipping-Fuellungen, gehrte Papier-mm-Linien, GPU-Pan/Zoom
Eigenstaendige Crate (render/window-Feature-Stufung), serde-only Tessellier-
schicht headless testbar. Linien als EIN gehrter Streifen (Miter-Bisektor +
1/cos-Laengenfaktor) statt Butt-Cap-Quads pro Segment -> saubere Ecken.
Standalone-Spike-Fenster via winit (cargo run --features window --bin spike).
2026-07-02 00:27:46 +02:00
karim 8fd8987b70 2D-Plan-Renderer auf WebGL2 (GPU) + akkumulierter Funktionsstand
Neuer GPU-Renderer fuer den Grundriss (src/plan/glPlan/): Earcut-Tessellierung
(konkav-faehig), gehrte Linienzuege (Miter), echte Papier-mm-Strichbreiten im
Massstab (repliziert den SVG-printStrokeVb-Pfad), Hybrid mit scharfem SVG-Text-
Overlay. GPU ist der Standardpfad; der SVG-Renderer bleibt automatischer Fallback,
falls WebGL2/Shader nicht verfuegbar sind. Imperativer Pan (rAF + CSS-transform)
fuer fluessige Interaktion ohne React-Re-Render je Frame.

Enthaelt zudem den bisher nicht committeten Arbeitsstand des Browser-BIM
(Oeffnungen, Treppen, Raeume, Decken, DXF-Export, Materialbibliothek, Kontext-
Import, Tauri-Compute-Boundary-PoC).
2026-07-02 00:12:39 +02:00
karim 7b3b597abc Add parametric walls unit tests
50 Vitest-Tests für die parametrische Wand-Engine (src/model/parametricWalls.ts):
GridRule, ModuleRule, ConditionalThicknessRule, ReferenceLineRule, SequenceRule,
deduplicateWalls, matchesCondition/matchesTarget, Integration via resolveParametricWall.
Vitest als devDependency + Testskript in package.json + vitest.config.ts ergänzt.
2026-07-01 20:33:38 +02:00
karim 4a08161e4b Add parametric walls design documentation 2026-07-01 20:32:07 +02:00
karim 39236c65c7 Add parametric wall types and engine skeleton 2026-07-01 20:06:19 +02:00
karim b9dc1838c7 HANDOVER: Koordinations- und Memory-Regeln fuer mehrere Instanzen/Agents ergaenzt 2026-06-30 21:45:10 +02:00
karim ca859c4aa4 Browser-BIM (cad): semantisches Modell, abgeleitete 2D/3D-Sichten, Zeichenwerkzeuge
Standalone-Browser-Port von DOSSIER. Enthaelt das semantische Modell mit
Plan-/3D-Ableitung, Zeichen- und Editierwerkzeuge, Rhino-artiges Befehlssystem,
dockbares Panel-System, Resource-Manager, DXF/.lin/.pat-Import, i18n (de/en)
sowie Projektdokumentation und Probe-Harness.
2026-06-30 20:52:27 +02:00
512 changed files with 21155 additions and 100228 deletions
-21
View File
@@ -11,24 +11,3 @@ scripts/*.png
# Editor / OS
.DS_Store
*.log
# Generierte native-Viewport-Szenen (aus sampleProject via scripts/dump-native-scene.mjs)
src-tauri/assets/native2d_scene.json
src-tauri/assets/native3d_walls.json
# Interne Doku (Handover/Pendenzen/Design-Notizen) — nur lokal, nicht im
# öffentlichen Gitea-Code-Browser. README.md bleibt als Repo-Beschreibung.
/ARCHITECTURE.md
/CONVENTIONS.md
/HANDOVER.md
/PENDENZEN.md
/PORT_PLAN.md
/RESEARCH_BAUTEILE_RHINO.md
/RESEARCH_CAD_APPROACHES.md
/ROADMAP.md
/SPIKE_TEXTUR_render3d.md
/STATUS.md
/docs/*.md
/docs/design/*.md
/docs/research/*.md
/docs/welle-c-hlr-spike/*.md
+452
View File
@@ -0,0 +1,452 @@
# Architektur — Standalone Browser-BIM (cad)
> 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).
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**.
Alle Bezeichner im Code sind **englisch** (Vectorworks-Terminologie); 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.
Darstellung (Detailgrad, Stile, Schraffuren, Overrides) wird **beim Rendern**
angewandt, nie in die Geometrie eingebacken.
```
┌──────────────────────────────────────────┐
│ Project (semantisches Modell, JSON) │ ← einzige Wahrheit
│ resources · types · designLevels · │
│ layers · elements │
└──────────────┬───────────────────────────┘
│ pure derive()
┌───────────────────────┼────────────────────────────┐
▼ ▼ ▼
Scene3D (Three.js) PlanModel → SVG SectionModel → SVG
buildScene() generatePlan() generateSection() (HLR)
│ │ │
└────────── beide 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).
```
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)
```
---
## 2. Datenmodell
### 2.1 Zwei unabhängige Achsen (DOSSIER-Modell, im Spike umgesetzt)
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** (`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 …`
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.
```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 }
}
```
`joinPriority` ist DOSSIERs `_MATERIAL_PRIO` (Beton 800 … Putz 100) als
**Daten** statt Hardcode — steuert die Prioritäts-T-Verschneidung (Risiko #1).
### 2.3 Aufbauten (mehrschichtige Typen)
```ts
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[]`.
### 2.4 Elemente
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 };
}
```
---
## 3. State, Persistenz, Undo/Redo
### 3.1 Store — Zustand
Heute: `useState<Project>` in `App.tsx`, immutabel via `setProject`. Das skaliert
nicht für Werkzeuge + Undo. Migration auf **Zustand** (ROADMAP §4):
```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;
}
```
**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 — 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.
### 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.
---
## 4. Rendering
### 4.1 Three.js-Szene spiegelt den Ebenen-Baum
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):
```
scene
└─ levelGroup[floorId] (y-Offset = baseElevation) ← Geschoss
└─ categoryGroup[categoryCode] (userData.code) ← Ebene
└─ element meshes (userData.elementId)
```
```ts
// scene.ts
function buildScene(project: Project, vis: VisibilityState): THREE.Group
function syncScene(root: THREE.Group, project, vis) // diff statt full rebuild (Perf)
```
- **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.2 Grundriss = Clipping-Ebene + symbolische Generierung
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):
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.
---
## 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.
```
┌─────────────────────────────────────────────────────────────────────┐
│ 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
```
**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.
---
## 6. Rhino → Browser — Mapping-Tabelle
| DOSSIER (Rhino-Plugin) | cad (Standalone-Browser) | 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) | — |
---
## 7. Was wir von DOSSIER übernehmen — und was nicht
**Ü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.
**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`.
**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.
---
## 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.
+64
View File
@@ -0,0 +1,64 @@
# Projekt-Konventionen — Browser-BIM (cad)
Siehe [ROADMAP.md](ROADMAP.md) für Vision, Architektur und Phasen.
## Code-Konventionen (verbindlich)
- **Alle Bezeichner im Code sind ENGLISCH** — Funktionen, Variablen, Typen, Felder,
Datei-/Modulnamen. Keine deutschen Bezeichner. (Beispiel: `computeJoins`, nicht
`verschneidungBerechnen`.)
- **UI-Texte und Kommentare dürfen Deutsch sein** (Nutzeroberfläche ist deutsch).
- **Domänen-Begriffe** möglichst nach Vectorworks-Terminologie benennen (englisch):
Design Layer, Sheet/Drawing Layer, Component, Class, Hatch, Wall Style, Viewport.
- **Einheiten:** intern alles in **Metern** (number). Anzeige via `formatM`.
- **Geometrie-Konventionen:** Wand-Normale `n = leftNormal(u) = (-u.y, u.x)`; bei
CCW-Wicklung zeigt `+n` nach innen. Schichten werden außen (−T/2) → innen (+T/2)
gestapelt.
## Code-Struktur (kein God-Component)
- **`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: modular + parallel bearbeitbar (verschiedene Features ≠ dieselbe Datei).
Siehe `docs/design/state-architecture.md`.
## UI-Konventionen
- **Listen-/Manager-Ansichten als saubere Tabellen:** eine Kopfzeile mit
Spaltentiteln (sticky), darunter kompakte Datenzeilen mit Inline-Edit pro Zelle.
KEINE wiederholten Feld-Beschriftungen pro Zeile. Gilt für Component-/Hatch-/
Line-Manager und ähnliche Listen.
- Dunkler DOSSIER-Stil; kompakt, ruhig, viel Inhalt pro Fläche.
- **UI-Text immer übersetzbar (i18n):** KEINE hartcodierten sichtbaren Strings im
JSX. Alle Texte über eine Übersetzungsfunktion `t('key')` aus einem Wörterbuch
(Default-Sprache Deutsch). Keys wie bei DOSSIER (`common.delete`, `layers.settings`,
`topbar.resources`). Neue Komponenten gleich mit `t(...)` schreiben. Identifier/Keys
bleiben englisch; nur die Wörterbuch-Werte sind die übersetzbaren Texte.
## Native-App-Verhalten (kein Browser-Standard)
Die App soll sich wie ein natives Programm anfühlen, nicht wie eine Webseite:
- **Browser-Kontextmenü global unterdrücken** (`document` `contextmenu` → `preventDefault`).
Nur unser eigenes `ContextMenu` erscheint; auf Flächen ohne eigenes Menü passiert nichts.
- **Keine Textauswahl / „Alles markieren":** `user-select: none` global; `user-select: text`
NUR in echten Eingaben (`input`, `textarea`, `[contenteditable]`). Ctrl+A außerhalb von
Eingaben unterbinden.
- Bild-/Element-Drag aus (`draggable=false` wo nötig); keine Browser-Drag-Gesten.
## Architektur-Prinzip
Ein **semantisches Modell** ist die einzige Wahrheit; jede Ansicht (3D, Grundriss,
Schnitt) wird **abgeleitet**. Darstellung (Detailgrad, Stile, Schraffuren) wird beim
Rendern angewandt, nie in die Geometrie eingebacken.
## Arbeitsweise (für Beiträge)
- Substanzielle, mehrstufige Arbeit an **Subagenten** delegieren, wo möglich.
- Ä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).
+968
View File
@@ -0,0 +1,968 @@
# HANDOVER — Browser-BIM (cad), Standalone-Port von DOSSIER
> Für die nächste Instanz. Stand: 2026-06-29. Lies zuerst
> `CONVENTIONS.md`, `ROADMAP.md` und die Projektnotizen (siehe unten).
## >>> COMMIT-REGEL (verbindlich, IMMER beachten) <<<
Dieses Repo darf **keinerlei Hinweise auf KI-Werkzeuge** enthalten — weder im
Code/Doku noch in der Git-Historie.
- **Commit-Messages:** sachlich, in der Sprache des Projekts. **NIEMALS**
`Co-Authored-By:`-Trailer, „Generated with …"-Zeilen, Tool-/Modellnamen oder
sonstige Urheber-Hinweise auf einen Assistenten.
- **Dateien/Kommentare:** keine Erwähnung von Assistenten, Modellen oder Agenten.
Wer hier weiterarbeitet, schreibt so, als wäre es Handarbeit des Teams.
- Vor jedem Push kurz prüfen: `git log` und `git diff` frei von solchen Spuren.
## >>> KOORDINATION & MEMORY (bei mehreren Instanzen/Agents) <<<
**Mehrere Hauptinstanzen gleichzeitig:**
- Git: nur **eine** Instanz committet/pusht auf `master`, ODER jede arbeitet auf
eigenem Branch und merged kontrolliert. `App.tsx`/`types.ts` ist der serielle
Flaschenhals — verschiedene Features ≠ dieselbe Datei.
- Dieser HANDOVER ist der **Koordinationskanal** zwischen Instanzen (liegt im
Repo, wird mitgepusht). Stand hier kurz festhalten, bevor du übergibst.
**Subagents:**
- Bekommen **kein** Memory automatisch — nur was im Prompt steht. Regeln (v. a.
die COMMIT-REGEL oben) explizit mitgeben, sonst kennt der Agent sie nicht.
- Agents **nicht** committen und **nicht** ins Memory schreiben lassen. Sie
liefern Diffs/Dateien/Ergebnisse zurück; die Hauptinstanz committet und pflegt
das Gedächtnis.
**Memory (`~/.claude/...`, außerhalb des Repos — leakt nie hierher):**
- In den Kontext geladen wird nur der schlanke **Index** (eine Zeile je Eintrag);
einzelne Fakten erscheinen nur bei Relevanz. Größe ist daher selten ein Problem.
- **Keine automatische Bereinigung.** Gepflegt wird beim Schreiben (Duplikate
aktualisieren statt anlegen, Überholtes löschen) oder auf Ansage.
- Nur **beständige** Fakten ablegen, ein Fakt pro Datei, Index-Zeile knapp. Bei
parallelen Schreibvorgängen ist der Index (MEMORY.md) die Contention-Stelle —
vor dem Edit frisch lesen (der „modified since read"-Guard verhindert blindes
Überschreiben).
## >>> STAND 2026-07-03 Abend (Nordstern-Session, Übergabe an neue Instanz) — ZUERST LESEN <<<
Ein Master-Branch (alle Feature-/Worktree-Branches konsolidiert und gelöscht), alles auf
origin/master gepusht, Working Tree sauber bis auf eine vorbestehende, unabhängige
Materials-Library-WIP (`.gitignore`, `public/assets/materials/manifest.json`,
`src/materials/library.ts`, `scripts/fetch-materials.mjs` — NICHT anfassen/reverten, ist
fremde in Arbeit befindliche Sache, nicht Teil der Engine-Session). Heute gelandet, acht
Commits in dieser Reihenfolge:
1. **7ca5197** Resource Manager: Materialien-Tab (PBR-Kugel-Vorschau via geteiltem Offscreen-
Renderer, Suche + Kategorie-Chips, lazy per IntersectionObserver).
2. **c8a4188** render3d als WASM/WebGPU-Viewport in der App (`?engine=wasm` bzw. localStorage
`cad.rendererMode`; three.js bleibt Default). Probe: scripts/probe-engine3d.mjs (HEADFUL=1
nötig — headless Chromium präsentiert keine WebGPU-Frames, gilt auch für render2d).
3. **54211b1 + 76029db** render3d/src/section.rs: Schnittebene → Cut-Polygone + sichtbare/
verdeckte projizierte Kanten, inkl. Öffnungen (Brüstung/Sturz-Teilrechtecke, Durchblick).
29 Tests, Beweis-SVG docs/welle-c-hlr-spike/section-engine-proof.svg, Doku
docs/design/engine-section-pipeline.md. Eigen-Engine-Ersatz für den OCCT-Pfad (66 MB WASM).
**mesh.rs (3D-Vollkörper-Extrusion) berücksichtigt Öffnungen weiterhin NICHT** — dokumentierte,
bewusst offene Lücke (Cut/Ansicht-Pipeline und 3D-Solid-Mesh sind bis dahin inkonsistent).
4. **ce6bd26** 2D-Engine-Parität: WASM-Pfad deckungsgleich zum SVG-Referenzpfad (**Referenz ist
`?gl=0`**, nicht der Default!). Fixes: text_scale entkoppelt, glyphon ColorMode::Web,
z-basierte Maler-Reihenfolge (draw_sequence), CSS-Klassenfarben in toRenderScene, greyed 0.3,
Dämmwellen-Bézier + userSpaceOnUse-Anker + widthScreen-Schraffurbreite. Probe:
scripts/probe-engine-parity.mjs.
5. **4b02e40** render2d Headless: Feature `headless`, HeadlessRenderer ohne Surface (Vulkan),
CLI-Bin render_png, Golden-Test (bit-exakt) tests/golden/demo.png, Doku
docs/design/engine-headless.md.
6. **382771b** Vektor-PDF aus der RenderScene (**Nordstern 2 abgeschlossen**): exportPdf baut
jetzt `planToRenderScene(plan)` — derselbe Aufruf wie der Viewport — und serialisiert über
den neuen `src/export/sceneToPrintSvg.ts` nach Papier-mm (PEN_STEPS-Quantisierung wie bisher,
widthScreen-Schraffurbreiten korrekt in mm umgerechnet, Texte jetzt als echte Vektor-Texte —
der alte Pfad liess sie komplett weg). `planToPrintSvg.ts` bleibt nur als Referenz stehen
(Kopfkommentar markiert sie als abgelöst). Verifiziert per `pdftoppm`: 53.51×43.69mm vs.
erwartet 53.45×43.45mm bei 1:100. **Bekannte Alt-Lücke, nicht neu:** Zeilenabstand
mehrzeiliger Stempeltexte in `toRenderScene.ts` ist nur bei Massstab 1:100 exakt (STAMP_REF_N),
bei anderen Export-Massstäben proportional leicht daneben — betrifft Viewport identisch, nicht
PDF-spezifisch, absichtlich nicht mitgefixt (out of scope für den additiven Umbau).
7. **b4c4a2c** wgpu 22 → 29, glyphon 0.6 → 0.11 (glyphon pinnt `^29`, obwohl wgpu 30 existiert).
`src/engine/requestDeviceShim.ts` komplett entfernt (das 22er-Limit
`maxInterStageShaderComponents` wird von 29 nicht mehr gesendet). Verifiziert: cargo check
nativ (render2d/render3d/src-tauri), cargo test render2d 17/17 + Golden weiterhin bit-exakt,
render3d 29/29, wasm32 --features web für beide, tsc + npm build — alles grün.
**Damit sind Nordstern 2, 3, 4 (Kern) und 5 gelandet.** Verifiziert per Screenshot: App läuft im
Electron-Fenster (`env -u ELECTRON_RUN_AS_NODE npm run electron`), 3D-Perspektive rendert das
echte Modell über den Engine-Renderer (Umschalter unten rechts: WebGL2/Engine).
**WICHTIG — was NICHT fertig ist (User hat das selbst am Screenshot bemerkt):** Die
Zeichnungsebenen vom Typ „Schnitt"/„Ansicht" (z.B. „Schnitt A", „Ansicht Süd" in der
Ebenen-Liste rechts) sind weiterhin **Stubs** — sie zeigen KEINEN echten Inhalt. Das
render3d-Schnitt-Modul (section.rs, Commits 54211b1/76029db) liefert das Rohmaterial
(SectionOutput: cut_polygons + visible_edges/hidden_edges), ist aber **noch nicht an
App.tsx/das Dokumentmodell angebunden** — es gibt noch keinen Code-Pfad, der beim Öffnen
einer Schnitt-/Ansichts-Zeichnungsebene das render3d-Modell durchschneidet und das Ergebnis
in Plan-Primitive für PlanView/render2d übersetzt. Das ist der wichtigste nächste Schritt
(Nordstern 4b, siehe Liste unten).
**Nächste Schritte, priorisiert:**
1. **[Nordstern 4b] Schnitt-Pipeline an UI anbinden** — grösster Brocken: SectionOutput
(Rust/render3d) → TS-Seite (WASM-Aufruf analog `toWalls3d.ts`/`nativeSync.ts`) → neue
Plan-Primitive-Kind(e) für Cut-Polygone + sichtbare/gestrichelte Kanten → generatePlan.ts
bindet das für DrawingLevel vom Typ Schnitt/Ansicht ein, statt des aktuellen Stub-Verhaltens.
PlanView.tsx rendert es wie normale Primitive (Parität-Infrastruktur aus ce6bd26 gilt 1:1).
2. **mesh.rs: Öffnungen im 3D-Vollkörper** — 3D-Ansicht zeigt Öffnungen zwar korrekt (das war
schon vorher so, per Mesh-Aussparung), aber das Schnitt-Modul rechnet gegen dieselben
Prismen — sobald 4b. steht, hier gegenprüfen ob mesh.rs nachgezogen werden muss oder ob es
getrennte Datenwege bleiben.
3. **Aufräumen:** `src-tauri/`-Ordner umbenennen/entflechten — User will keine Tauri-Reste im
Namensraum, Electron ist die Shell. render2d/render3d/geometry sind eigenständige Workspaces
ohne Tauri-Abhängigkeit, könnten z.B. nach `engine/` oder `crates/` wandern. Toten Tauri-Host
(`cad-tauri`, tauri.conf.json, native.rs/native2d/native3d) explizit mit dem User klären:
behalten (Beweis für späteren All-Native-Schritt) oder entfernen.
4. **[Nordstern 1]** Papier-mm-exakte Strichbreiten-Audit über den gesamten Engine-Pfad
(Screen bei jedem Zoom/Massstab = Druck) — nach 2/3/4/5 jetzt der einzige verbliebene
unbearbeitete Nordstern-Punkt.
5. Nicht-Engine-CAD-Backlog (Wand-Referenzlinien, Editier-Welle, nahtfreie Verschneidung,
Overrides/Multi-Page-Layouts) — weiter unten in diesem Dokument, nachrangig zur Engine-Arbeit.
**Arbeitsweise dieser Session (fortführen):** Pipeline aus bis zu 2 parallelen Sonnet-Subagents
(User-Regel: immer 2 am Laufen halten, nachfüllen sobald einer fertig ist). Jeder Agent bekommt
explizit zugewiesenes Terrain (Dateipfade), damit sich parallele Agents nicht überschreiben.
Ergebnisse werden von der Hauptinstanz verifiziert (Screenshots ansehen, Gates laufen lassen)
und committet — Agents committen nie selbst. Nach Prozessabstürzen (kam heute zweimal vor)
lässt sich ein Agent per SendMessage mit seiner agentId aus dem Transcript fortsetzen, sein
Working-Tree-Zwischenstand bleibt erhalten.
## >>> ENGINE-NORDSTERN (User-Direktive 2026-07-02): die volle Engine-Vision <<<
Der User hat explizit beauftragt, die Engine-Vision KOMPLETT anzustreben ("wir brauchen
exakte breiten usw. strebe bitte das komplett an!"). render2d/render3d sind nicht nur
Viewport-Ersatz, sondern werden die EINZIGE Render-Wahrheit des CAD:
1. **Papier-mm-exakte Strichbreiten** überall — Bildschirm bei jedem Maßstab/Zoom = Druck.
2. **Deterministische Plan-Ausgabe:** Druck/PDF mm-genau identisch zum Bildschirm, beides aus
render2d (kein separater Druckpfad). Vektor-PDF-Backend = dieselbe Szene, anderes Target.
3. **Headless-Rendering:** PNG/PDF-Export und Golden-Image-Tests ohne Fenster (die
serde-only/GPU-Trennung in render2d existiert genau dafür; wgpu kann offscreen).
4. **HLR/Schnitt-Pipelines** durch die Engine (render3d → Kantenextraktion → render2d-Szene).
5. **Eine Wahrheit:** SVG-/WebGL2-Geometriepfade im Browser schrittweise durch die
WASM/WebGPU-Engine ersetzen (`?engine=wasm`-Spike ist der Anfang); SVG bleibt nur
Interaktions-/Overlay-Schicht. Bereits gefundene Duplikations-Bugs: Wandecken-Kerben,
fehlende Strichelung im WebGL2-Pfad.
6. **WebKit-Unabhängigkeit** (explizit vom User betont): kein Render- oder Ausgabepfad darf
von WebKitGTK abhängen. Brücke = Chromium-Shell (`npm run shell`), Endzustand = All-Native
(ein Rust-Prozess, Engines + natives UI, kein Webview).
**RICHTUNGSENTSCHEIDUNG 2026-07-02: Electron statt Tauri, all-native aufgeschoben.**
Abgewogen mit dem User: (a) „auf Tauri bleiben" = WebKitGTK, das WEDER WebGPU (kein WASM-
Viewport) NOCH native-wgpu-Composite ins Fenster (Wayland tot) kann → Engine nur als separate
Fenster möglich, kein integrierter Viewport. (b) „all-native jetzt" = kein Shortcut, sondern
das GRÖSSERE Rewrite: ~46k Zeilen TS (UI ~18k + Modell-Gehirn ~5k + Interaktion ~23k) müssten
nach Rust; die Rust-Engines sind erst ~10% der App. → **Gewählt: Electron/Chromium-Shell +
WASM/WebGPU-Engine im Webview** (behält die funktionierende React-UI, verlässt WebKitGTK,
integrierter Engine-Viewport möglich). Die Engine-Rust-Crates sind in einem späteren All-Native-
Schritt 1:1 wiederverwendbar; nur die dünne WASM-Bindeschicht wäre dann Wegwerf. All-native
bleibt legitimes Fernziel, ist aber KEIN Weg zurück zu Tauri (Tauri IST der Webview-Shell).
>>> ELECTRON-PROTOTYP: FERTIG (2026-07-02). <<<
`electron` als devDep, `scripts/electron-main.cjs` (BrowserWindow, `Menu.setApplicationMenu(null)`
+ `frame:false`/`autoHideMenuBar` für randloses Fenster wie chromium-shell.sh, WebGPU-Flags
`enable-unsafe-webgpu`+`enable-features=Vulkan`), `scripts/electron-shell.sh` (Dev-Server-Guard
wie chromium-shell.sh), npm-Script `npm run electron`. Verifiziert per Screenshot: App startet
als eigenes randloses Fenster, lädt den echten Grundriss (nicht leer). WICHTIG:
- **Datei muss `.cjs` sein** (`package.json` hat `"type":"module"`, Electron-Main läuft als CJS).
- **`ELECTRON_RUN_AS_NODE=1`** kann aus dem umgebenden Prozess (z.B. VSCode-Electron-Host)
vererbt sein — zwingt Electron in den Node-Modus (`require('electron')` liefert nur einen
Pfad-String statt `{app,...}` → `app` ist undefined). Immer mit `env -u ELECTRON_RUN_AS_NODE`
starten, falls das Fenster nicht öffnet / `app.commandLine` undefined ist.
- **WebGPU/`?engine=wasm` funktioniert in Electron** — verifiziert per Screenshot (RENDERER-Anzeige
zeigt "Engine" aktiv, Grundriss korrekt gezeichnet). Ein einzelner Fehlversuch vorher war ein
GPU-Prozess-Init-Timing-Rennen (zu früh abgefragt), kein echter Bug — `app.getGPUFeatureStatus()`
zeigt nach ~2s alles `enabled`/`enabled_on` inkl. `webgpu` und `vulkan` (AMD RX 7800 XT/RADV).
KEINE Ozone-Platform-Flags nötig, `--ozone-platform=x11` NICHT verwenden (bricht die
Fenstererstellung, Vulkan-Surface/GetGeometry-Fehler). `electron --version` meldet v24, weil
`ELECTRON_RUN_AS_NODE=1` den Prozess in den Node-Modus zwingt und dann Nodes eigene Bundle-
Version zeigt (kein echter Versions-Mismatch — `env -u ELECTRON_RUN_AS_NODE electron --version`
meldet korrekt v43).
Danach: render3d-WASM, Doppeltext-Fix.<<<
Klarstellung an den User (er fragte, ob Webview = „billig"): NEIN — Figma (eigene C++/WASM-Engine
im Web-Shell), Onshape, VS Code zeigen, dass „eigene Engine + Web-UI" Top-Tier-Architektur ist.
CAD/BIM-Substanz = Modell + Engine + Normen (haben wir: parametrische Wände, SIA 416, DOSSIER-
Ebenen, ein-Modell→alle-Sichten), NICHT der Fenster-Shell.
Praxisregel ab jetzt: Darstellungs-Features NICHT mehr mehrfach (SVG/WebGL2/nativ) bauen,
sondern einmal in der Engine + dünne Anbindungen.
## >>> AUFGABE FÜR NEUE INSTANZ: 2D-Plan auf WebGL-GPU-Renderer (ZUERST) <<<
**Warum:** Tauri-Linux-Webview = **WebKitGTK**, das SVG/2D auf der **CPU (Cairo)** rastert.
User-GPU (AMD RX 7800 XT) langweilt sich; bei **144 Hz** (~6,9 ms/Frame) ruckelt Pan/Zoom/
**Objekt-Bewegen** sichtbar. three.js/WebGL (3D) läuft GPU-beschleunigt → NUR der 2D-SVG-Pfad
ist CPU-gebunden. Entscheidung mit User: 2D-Plan **von SVG auf WebGL2 (GPU)** umbauen.
**Sofortlösung fürs Vorführen/Entwickeln — Chromium-App-Shell:** `npm run shell`
(`scripts/chromium-shell.sh`) startet bei Bedarf den Vite-Dev-Server (Port 5187, Polling per
`curl`) und öffnet ihn dann in einem randlosen Chromium-Fenster (`--app=…`, eigenes Profil unter
`~/.cache/cad-chromium-shell`), statt im WebKitGTK-Webview von `tauri:dev`. Chromium rendert
dieselbe Seite GPU-beschleunigt und spürbar flüssiger. Kein Ersatz für den nativen wgpu-Pfad
(`render2d`/`render3d`), aber ein schneller Weg zu einer ruckelfreien Oberfläche, solange der
Electron/CEF-Shell (siehe Notizen zu „Render path decision") noch aussteht. Läuft rein im
Browser ohne Tauri-Backend — `src/compute/index.ts` guardet den einzigen `invoke()`-Aufruf
bereits per dynamischem Import mit TS-Fallback.
**Architektur (mit User abgestimmt):**
- **Hybrid:** WebGL2-Canvas rendert die schwere Geometrie (Poché-Polygone, Linien, Schraffuren,
Kreise/Bögen als Segmente). **Dünne SVG-Ebene DARÜBER** behält Text (Raumstempel/Labels),
Griffe, Snap-Marker, Werkzeug-Vorschau (wenige Elemente → billig, DOM-Hit-Test + scharfer Text bleibt).
- **Raw WebGL2, KEINE neue npm-Dependency** (User will nicht „abhängiger" werden; nativ).
- **Geometrie pro Plan EINMAL tessellieren** (Cache), Pan/Zoom = nur Transform-Matrix-Uniform →
keine Re-Tessellation, GPU-buttrig. Objekt-Bewegen: Plan ändert sich → re-tessellieren (leichter als SVG-Reconcile+Cairo).
- **Linienbreite konstant in Screen-px** (wie heutiges `non-scaling-stroke`): im Vertex-Shader
expandieren (Position world → clip, dann Normalen-Offset in clip via Viewport-px). Miter an Ecken
später; Spike: Quads pro Segment.
- **SVG-Renderer als Fallback hinter einem Flag behalten**, bis GL-Parität erreicht → kein Risiko.
- Vorgehen: **erst Spike** (Linien+gefüllte Polygone + GPU-Pan/Zoom), Glätte bei 144 Hz messen
(dev :5187 läuft; `tauri:dev`-Fenster läuft), DANN Schraffur/Feinschliff/Text-Overlay/Hit-Test.
**Fixpunkte im Code:**
- `src/plan/PlanView.tsx` (~2500 Z.): `toScreen()` = Modell-m → viewBox-Einheiten, FIXER Ursprung,
`PX_PER_M=90`; `view={x,y,w,h}` treibt `<svg viewBox>`; Pan = setView, Zoom = setView(onWheel).
BEREITS optimiert: `primitiveEls`/`drawingRunEls` `useMemo` (Pan/Zoom 312→0 Re-Renders) +
rAF-Coalescing beim Objekt-Drag (`flushDragMove`/`enqueueDragMove`, 40→1 Update/Frame). Diese
bleiben nützlich; der GL-Renderer ersetzt die SVG-Primitiv-Ebene, NICHT die Interaktions-Logik/Griffe.
- `src/plan/generatePlan.ts`: `export type Primitive` (Z.133) — kinds: `polygon`{pts,fill,stroke,
strokeWidthMm,hatch,*Id}, `line`{a,b,weightMm?,strokeWidthMm?,dash?,color,cls,greyed,drawingId?},
`arc`{center,r,…}, `text`{at,doc,extraLines,basePt,color} (Text → SVG-Overlay, NICHT WebGL).
- Koordinaten/Breiten-Logik im SVG-Renderer: `PrimitiveShape` (~Z.2280) + `DrawingRunShape` — dort
steht, wie Haarlinie/mm/`paperScale`/`non-scaling-stroke` die Strichstärke bestimmen (für GL nachbilden).
- Hit-Test/Auswahl/Griffe laufen weiter über die bestehenden PlanView-Pfade (modellbasiert); GL ist nur Anzeige.
- **Vektor-PDF/-Export bleibt** (kommt aus `generatePlan`/DXF, nicht aus der Anzeigefläche) — NICHT anfassen.
**Gates:** `npx tsc -b` 0, `npm run build` grün, Trace-Scan sauber (keine KI-Spuren, COMMIT-REGEL oben).
Kein Commit ohne Ansage. Verifizieren wie üblich per Puppeteer gegen :5187 (Render korrekt + Glätte messen).
**GESAMT-TODO-LISTE (Stand 2026-07-01 Nacht) — im Hinterkopf behalten:**
ERLEDIGT+verifiziert: U/I/O/P-Kopiermodi · Backlog#1 Transform→CommandLine · Backlog#4 3D-Zeichnen +
Engine-Confirm-Bugfix · Raumstempel#2 (Feld-Modell) · Plan-Perf (Memo + rAF-Coalesce) · Tauri-M1-PoC
(geometry-Crate 4/4 + Parität + `tauri:dev`-Boot). OFFEN: **(A) 2D-WebGL-Renderer [DIESE AUFGABE]** ·
(B) Tauri-M1 Rest: Compute-Boundary in Render-Pfad einhängen (generatePlan sync → precompute+cache) +
nächste Ops (kernel2d→DXF/DWG→detectRooms) · (C) three.js→wgpu (M2, nach 2D-WebGL) · Parametric Walls
Phase B (andere Instanz) · Wand-Referenzlinien-Option unter Bezugspunkt · Viewport/Editier-Welle
(Treppen-Griffe, Öffnung-entlang-Wand, 2D auf Zeichenebenen, runde Linienenden, schattiert-Politur) ·
Smart-Join/Split + nahtfreie Wandverschneidung · Text/Stempel-Annotation (Frame+Rotation) · Welle B
(3D-Schnitt+Stencil-Capping) · Welle C (HLR verdrahten) · Overrides/Ausschnitte/Multi-Page-Layouts.
## >>> STAND 2026-07-01 (Tauri-PoC — App-frei, gelandet) <<<
**Tauri v2 + Rust-Compute-Boundary — Proof-of-Concept steht (3 App-freie Agents).**
Alle Gates grün, nichts an `App.tsx`/`src/model/` angefasst (nur NEUE Dateien +
additive Config). Ref: `docs/design/tauri-migration-plan.md` / `tauri-architecture.md`.
- **`src-tauri/` (Rust, neu):** Cargo-**Workspace** aus zwei Crates:
- `src-tauri/geometry/` — **serde-only** Lib, Port von `computeJoins` (aus
`src/model/joins.ts`) inkl. Vektor-Helfer. Input flach: `WallInput{ id,start,end,
thickness,referenceOffset }`; Output `WallCuts{ wallId,startCut,endCut }` (Line=
point+dir). **`cargo test` grün: 4/4** (L-Ecke=geteilte Gehrung, freies Ende,
T-Stoss, kollinear → alle korrekt). Braucht KEIN webkit → hier testbar.
- `src-tauri/` App-Crate (`cad-tauri`) — Tauri-v2-Shell (`lib.rs` mit
`#[tauri::command] compute_joins` + `run()`, `main.rs`, `build.rs`,
`tauri.conf.json` devUrl **:5187**, `icons/icon.png`). **`cargo check` GRÜN** —
wider Erwarten NICHT von webkit blockiert: tauri v2 bindet `webkit2gtk-4.1`
(vorhanden); nur `-6.0` fehlte. `src-tauri/target` + `gen/` sind Build-Artefakte
(gitignored).
- **`src/compute/index.ts` (neu, Boundary):** einzige Stelle für schwere Ops.
`computeJoins(project,walls)` flacht ab → `invoke("compute_joins")` (geguardeter
dynamischer Import von `@tauri-apps/api/core`, Build bleibt grün OHNE das Paket)
→ bei null/Fehler `console.warn` + TS-Fallback (`joins.ts`). Zusätzlich
Durchreicher `detectRooms` (roomBoundary.ts) + `parseShapeFromDwg`(=`parseDwg`);
`computeKernel2D` bewusst ausgelassen (kein Einzel-Entry, ~30 Primitive).
- **`package.json`/`vite.config.ts`:** `@tauri-apps/cli`+`api` (v2) installiert,
Scripts `tauri`/`tauri:dev`/`tauri:build`; vite Port 5187 `strictPort`,
`clearScreen:false`, `envPrefix` — rein additiv.
**NÄCHSTE SCHRITTE (Tauri):** (1) **Echte TS↔Rust-Numerik-Parität** end-to-end prüfen
— erst sinnvoll, wenn die Boundary in einen Caller verdrahtet ist (aktuell bewusst
NICHT verdrahtet, weil das `generatePlan`/`App` async machen würde). (2) `computeJoins`
in den Wand-Render-Pfad einhängen (async-Welle). (3) `tauri:dev` end-to-end auf einer
Maschine MIT `webkit2gtk` (hier nur `cargo check`, kein Fenster-Boot getestet).
(4) Nächste Ops laut Plan: `kernel2d` → DXF/DWG → `detectRooms`.
## >>> STAND 2026-07-01 (Parametric Walls Phase A — ISOLIERT) <<<
**Neue Instanz (Opus) — Parallelisierung mit Agents (Sonnet):**
- **Branch:** `feature/parametric-walls` (isoliert, keine App.tsx-Änderungen)
- **Phase A FERTIG (alle Agents grün):**
- **Agent 1:** Types (`ParametricWall`, 5 `ParametricRule` Varianten) + Engine
(`resolveParametricWall()`, 5 rule handlers, helpers) in `src/model/parametricWalls.ts`
- **Agent 2:** 50 Unit-Tests (Vitest), alle grün; `npm test -- parametricWalls.test.ts`
- **Agent 3:** Design-Doku `docs/design/parametric-walls.md` + `docs/README.md` aktualisiert
- Verification: `tsc -b` ✓, `npm run build` ✓ (577 modules, 3.72s)
- **Phase B (UI-Integration) — für nächste Instanz mit Agents:**
- Agent 1: TopBar ParametricWall-Picker + UI-Komponente
- Agent 2: Command `pw` (`cmds/parametricWall.ts`)
- Agent 3: ResourceManager-Integration (ParametricWall CRUD)
- Dann merge → `master`, Screenshot-Verifikation
- **Koordination:** andere Instanz committet parallel (Text, Raum, HLR) auf `master`,
keine Konflikte (nur `types.ts`+`src/model/` erweitert, nicht `App.tsx`).
**Backlog #4 ERLEDIGT (Zeichnen im 3D-View — vorherige Instanz):** Werkzeug-Gate `toolsEnabled`
(`App.tsx` ~1877) von `viewType === "grundriss"` auf `activeLevel.kind ===
"floor"` erweitert → Werkzeuge sind jetzt AUCH in der Perspektive aktiv. Die
gesamte Zeichen-Pipeline war bereits verdrahtet (`Viewport3D.workplaneModel()`
raycastet den Cursor auf die Geschoss-OKFF; `onWorkplanePoint`/`onWorkplaneConfirm`
speisen dieselbe Engine wie der Grundriss). Ende-zu-Ende per Puppeteer verifiziert:
in iso-Ansicht Linie mit 2 Klicks gezeichnet → committet (`draw2d` 0→1, Prompt
zurück auf „Befehl:"); Picks liefern gültige Modell-Vec2 (raycast korrekt).
- **BUG-FIX unterwegs (Engine-Confirm, `src/commands/engine.ts` `confirm()`):**
Beim Testen von #4 fiel ein VORBESTEHENDER Bug auf (reproduziert IDENTISCH im
Grundriss, nicht durch #4 verursacht): mehrteilige Befehle (Wand/Polylinie)
liessen sich per Enter/Rechtsklick NIE abschliessen. Ursache: `confirm()` nahm
IMMER den Tab-Feld-Zweig (`fieldResultPoint`), sobald der Schritt Felder hatte —
auch OHNE gelocktes Feld → es wurde ein Cursor-Punkt gefüttert (bei Wand: Null-
Strecke, ignoriert) statt `onConfirm` (das den Zug committet) aufzurufen. Fix:
Feld-Zweig nur noch bei tatsächlich GELOCKTEM Feld (`Object.keys(this.locks).length
> 0`); ohne Lock fällt `confirm()` auf `onConfirm` durch. Verifiziert: Wand
schliesst jetzt per Rechtsklick im Grundriss (91→98) UND in 3D (99→106); Tab-Feld-
Fluss „Länge 4 tippen → Enter lockt → leeres Enter setzt Punkt" unverändert;
Linie-Auto-Commit (2 Klicks) unverändert. Gegatet: `tsc -b` 0, `build` grün, keine
Konsolenfehler. KEIN Commit.
- **Backlog #1 ERLEDIGT (Befehlsleiste-Transform vereinheitlicht):** Die laufende
Auswahl-Transformation (nur noch `rotate`/Taste `d` mit U/I/O/P; move/mirror
laufen längst über die Engine) speist jetzt die `CommandLine` statt der
schwebenden `TransformBar`. Dritter Zweig in der `<CommandLine>`-Ternäre parallel
zu `gripEdit` (`gripEdit ? … : activeTransform ? … : engineView…`): Prompt =
`transform.hint.${op}.${…}`, Inline-Optionen = U/I/O/P (`transformOptions()`,
aktiver Modus hervorgehoben via neuem `active?`-Feld an `CommandLineOption` +
`.cmdline-option.active` in styles.css, `×count` bei array/distribute), Live-/
lockbares **Winkel**-Feld beim Drehen bzw. **Distanz** beim Bewegen
(`transformFields()` aus neuem `transformDragInfoRef`), getippte Zahl committet
über `submitTransformValue()`. `d` fokussiert die CommandLine (wie `m`/`s`).
`src/ui/TransformBar.tsx` gelöscht; verwaiste `.transform-bar`/`.tf-*`-CSS +
`transform.bar`-Key harmlos stehen gelassen. Gegatet: `tsc -b` 0 Fehler, `build`
grün, Trace-Scan sauber, Puppeteer-Probe end-to-end (nach `d`: „Drehzentrum
klicken" + U/I/O/P; nach Zentrum+Bezug: „Zielwinkel klicken" + Live-Winkelfeld;
keine Konsolenfehler). KEIN Commit (Konvention der Vorsessions beibehalten).
## >>> STAND 2026-07-01 (Abend) — Raum + Topbar-Umbau + Backlog — ZUERST lesen <<<
Alle unten genannten Agenten GRÜN gelandet und gegatet (`tsc -b` 0 Fehler,
`npm run build` grün, Trace-Scan sauber, Boot-Probe ohne Konsolenfehler). Dev-
Server läuft/hält auf :5187. KEIN Commit gemacht (Commit-Regel oben beachten).
**Diese Session gelandet:**
- **Rich-Text-Kern (komplett):** `src/text/richText.ts` (Marks/RichTextDoc/TextRange,
toggleMark/applyMark/isMarkActive/DEFAULT_PRESETS/serialize…), `src/text/renderHtml.ts`
(docToHtml/docToLines/docToSvgText), `src/text/RichTextEditor.tsx` (contentEditable,
Toolbar B/I/U/S + Grösse/Farbe/Presets).
- **Raum-Slice (SIA-416), Agent gelandet:** `Room` in `model/types.ts` (id, floorId,
categoryCode "60", siaCategory HNF/NNF/VF/FF/KGF, boundary, stampAnchor?, stampDoc?);
`cmds/room.ts` (Modus inside=click-inside via `roomFromPointInside`, manual=Polylinie);
Plan: neues `text`-Primitive + Füllpolygon + Stempel via `docToLines` + Live-Fläche;
`panels/RoomBalancePanel.tsx` (SIA-Bilanz + CSV). **App-State für Stempel:**
`selectedRoomId`/`selectedRoom.stampDoc`, `projectSlice.setRoomStampDoc(roomId,doc)`,
`moveRoomStamp`, `setRoomStampAnchor`, `stampEditorRoomId`. Stempel-Editor = kleines
schwebendes Fenster (kein Footer).
- **DOSSIER-Referenz studiert → Spec `docs/design/topbar-dossier.md`** (verbindlich für
Topbar-Arbeit; Referenz-Klon lag unter `/tmp/DOSSIER`). Kernbefund: Text-Styling ist im
Original eine FEST in der Oberleiste sitzende Text-Gruppe (Stil/Font/Grösse + B/I/U +
L/C/R + „+Text"), Akzent-Glow bei Auswahl; Raumstempel-Typografie darüber; Text-INHALT
in eigenem kleinem Fenster; Zoom/Massstab als 2×2-Cluster (kombinierte Stat-Pille).
- **Topbar-Umbau (Agent gelandet), `src/ui/TopBar.tsx` + `styles.css` + `App.tsx` +
`ui/TextEditorDialog.tsx` (neu) + i18n:** Zoom-Doppelung entfernt; 2×2-Massstab/Zoom-
Cluster; einheitliche Segmentpillen (neue `Segment`-Komponente); **Text-Gruppe** in der
Topbar (`textTarget`-Prop = `null | {doc,range,apply}`, aus selektiertem Raum abgeleitet,
`apply`→`setRoomStampDoc`; ohne range wirkt Format auf ganzes Doc); Doppelklick öffnet
`TextEditorDialog`. Verwaiste alte CSS-Regeln (`.tb-zoom*`, `.stamp-editor*`) bewusst
stehen gelassen (harmlos). „+ Text"-Button verdrahtet, aber DEAKTIVIERT (noch kein
Text-Annotations-Werkzeug).
**Backlog / nächste Schritte (Priorität dieser Session, siehe auch Todo-Liste):**
1. **Befehlsleiste-Transform vereinheitlichen:** laufende Transformation (move/mirror/rotate)
soll die `CommandLine` speisen (Prompt „Basispunkt/Zielpunkt" + U/I/O/P als Inline-
Optionen) statt der separaten schwebenden `TransformBar`; freies Befehl-Tippen während der
Geste unterdrücken, Zahleneingabe (Distanz/Winkel) via `fields` bleibt. Muster: parallel
zum bestehenden `gripEdit`-Zweig bei `<CommandLine>` (~App.tsx:2793). U/I/O/P-Routing
(App.tsx ~1543–1548) NICHT kaputt machen — nur zusätzlich in die CommandLine spiegeln.
2. **Strukturierter Raumstempel (DOSSIER `RaumProperties`):** Stempel ist KEIN Freitext,
sondern ein FELD-Modell: Raumnummer · Raumname · Raumname-Zeile2 (in Listen als ein Name
zusammengezogen) · Bodenfläche (an/aus + Präfix) · Fensterfläche (an/aus + Präfix) ·
Nutzung HNF/… (an/aus). Zeilen-Layout-Editor (welches Feld auf welche Zeile) + Typografie
PRO Feld-Einheit (Raumname grösser, andere kleiner, eine Zeile kursiv). Ersetzt den
Freitext-`stampDoc`-Zwischenstand; die Topbar-Text-Gruppe formatiert dann die gewählte
Feld-Einheit (löst auch die „ganzes Doc statt Selektion"-Einschränkung).
3. **Wand-Optionen:** Referenzlinien-Option UNTER den Bezugspunkt setzen, gleiche Optik/
Anordnung wie die übrigen Optionen (`cmds/wall.ts` / ToolsPanel-Optionsreihenfolge).
4. **Zeichnen im 3D-View:** Cursor per Raycast auf die Arbeitsebene (okff des Geschosses)
projizieren → Weltkoordinaten als `engine.move/pick` einspeisen; `toolsEnabled`-Gate
(App.tsx ~1834, heute `viewType==="grundriss"`) auch für Perspektive öffnen.
5. **Viewport/Editier-Welle:** Treppen-Griffe (App.tsx grips-memo hat keinen stair-Fall),
Öffnung entlang Host-Wand verschieben (kein opening-Handler im Body-Move; `moveOpeningBy`
fehlt), 2D-Tools auf Zeichnungsebenen (`toolsEnabled` erweitern + Bau-Tool-Buttons grauen),
runde Linienenden (`stroke-linecap:round` auf offene Linien), „schattiert"-Politur
(ACES-Tonemapping+sRGB, HemisphereLight/RoomEnvironment-PMREM, MeshLambert→MeshStandard in
`Viewport3D.tsx` ~416–449).
6. **Smart-Join/Split + NAHT-FREIE Wandverschneidung** (Poché-Union an Stössen, keine sichtbare
innere Naht, Gehrung/T-Stoss). 7. **Text/Stempel-Annotation** (aktiviert „+Text";
Frame+Rotation). 8. **Welle B** (3D-Schnittebene + Stencil-Capping + Schnittschraffur).
9. **Welle C** (HLR verdrahten; `src/section/hlr.ts` bereit). 10. Overrides/Ausschnitte/
Multi-Page-Layouts.
**Kollisions-Regel bestätigt:** `App.tsx`, `types.ts`, `generatePlan.ts`, `Viewport3D.tsx`,
`registry.ts`, `PlanView.tsx`, i18n, `styles.css` sind Hotspots → nur EIN App.tsx-Agent pro
Welle; parallele Tracks nur auf genuin App-freien neuen Modulen.
## >>> STAND 2026-07-01 (Bug-Batch Runde 1) — ZUERST lesen <<<
Runde-1-Agenten alle GRÜN gelandet (`tsc -b` + build + Probe verifiziert):
- **3D-Editier-Kamera-Bug (kritisch, `src/viewport/Viewport3D.tsx`):** Ursache = schwerer
Szenen-Aufbau-Effekt mit `project`-Dependency → jede Griff-Mutation baute Szene neu auf und
`applyView3d` resettete die Kamera auf Preset (→ „springt in Top-View", Rechts/Links-Pan-Illusion).
Fix: `camStateRef`-Snapshot (Pos/Target/up/ortho-Zoom/aktive Kam) im Render-Loop fortgeschrieben,
bei Neuaufbau WIEDERHERGESTELLT statt `applyView3d`; `applyView3d` nur beim ersten Aufbau;
`view3d`-Effekt guarded via `gripDragRef`; Top-View `enableRotate=false`; 2D-Drawings bekommen
`move`-Griff (Anker=center/at) → in 3D verschiebbar. Kamera pos/target numerisch identisch vor/nach
Drag/Löschen bewiesen.
- **Print-Linienstärke invertiert → korrekt (`src/plan/PlanView.tsx`, `styles.css`):** Print zeichnet jetzt
echte Papier-mm in viewBox-Einheiten OHNE `non-scaling-stroke`: `strokeVb=(mm·N/1000)·PX_PER_M`,
N=`paperScale` stabil (nur bei `applyScale`/erstem Messen gesetzt). Rein=dicker, raus=dünner;
1:100 vs 1:10 = exakt ×10. Display/Haarlinie unverändert. **Ecken-Miter:** CSS `stroke-linejoin:miter`
+ `buildDrawingRuns` bündelt zusammenhängende `line`-Primitive gleicher drawingId zu einem
`polyline`/`polygon` (Ringschluss erkannt) → gehrte Ecken statt Butt-Cap-Stufe. Primitive-Modell
(und Print-Export) unverändert.
- **ambientCG-Live-Bibliothek (`src/materials/ambientcg.ts` neu, `ResourceManager.tsx`):** komplette CC0-Lib
live durchsuchbar (Suche/Kategorie/Auflösung 1K-4K), jszip-Entpackung → Blob-URLs, NormalGL bevorzugt.
**CORS:** Such-JSON + `/get` brauchen Proxy → Vite-Dev-Proxy `/ambientcg` in `vite.config.ts`
(`VITE_AMBIENTCG_PROXY` für Prod); optionale Prod-Route `openbureau-core/api/src/routes/ambientcg.js`
(lesend, gemountet VOR Auth-Gate). Thumbnails direkt (ACAO:*). Offline → Schnellauswahl-Fallback.
**Runde 2 FERTIG (2026-07-01, alle GRÜN + Probe-verifiziert):**
- **2a** [App/engine/editors/registry, neu `cmds/mirror.ts`+`cmds/join.ts`]: Snapping erster Punkt gefixt
(Modifier shift/ctrl wurden im Command-Pfad verschluckt → jetzt via `EngineMods` durch `move/pick`→`host.snap`
gefädelt); Segment-Move für offene Polylinien/Linien (`EdgeGrip.free`, ganzer 2D-Delta, Nachbar-Segmente
folgen über geteilte Vertices); Kürzel↔Befehl vereinheitlicht (`M`→engine.start("move"), `S`→"mirror",
`Ctrl+J`→"join", identisch zum Tippen; `Command.autoRun` für nicht-interaktive Befehle; `CommandSelection.drawingIds`
für Mehrfachauswahl); **Mirror-Befehl** (2-Punkt-Achse, Vorschau, `commitTransform mirror/copy`).
- **2b** [cmds/{polyline,rect,wall}]: Polylinie **offen/geschlossen** (Toggle `m`, Ring-Fill); Rechteck **2pt/3pt/Zentrum**
(3pt = gedreht → closed polyline); **Wandstärke-Tab-Feld** nur bei einschichtigen Wandtypen (baut on-commit
einen einschichtigen Wandtyp der Zieldicke, Original bleibt).
- **Swisstopo/OSM-Kontext-Importer** [io/{lv95,geoContext,swissTopo,osm}, ui/ContextImportDialog, SitePanel]:
„Standort importieren" — geocode (geo.admin SearchServer, LV95 direkt), swisstopo-Gebäude
(`MapServer/identify` layer `ch.swisstopo.vec25-gebaeude`), OSM-Overpass (Gebäude/Strassen/Wasser/Grün).
LV95↔WGS84 (Bern round-trip 0.38 m) + Origin-Shift. CORS: geo.admin direkt, Overpass via neuer read-only-Route
`openbureau-core/api/src/routes/geoproxy.js` (vor Auth-Gate, host-allowlist) wenn `VITE_GEO_PROXY` gesetzt.
**TIER-1 FORTSCHRITT (2026-07-01):**
- **DXF-Vektor-Export FERTIG** [src/export/{dxfWriter,exportDxf}, ui/ExportDxfDialog, TopBar+App-Button]:
hand-rollierter R2000/AC1015-ASCII-Writer, `$INSUNITS=6` (Meter), 23 Layer aus Kategorien (ACI+True-Color+lw),
LINE/LWPOLYLINE(closed)/CIRCLE/ARC/TEXT; Quelle = `generatePlan()` in Meter-Modellraum. 5 m-Wand→5.345 m Span
verifiziert. DWG NICHT geshippt (libredwg-web nur lesend — ehrlich weggelassen).
- **Decke (Ceiling) FERTIG** [geometry/ceiling, cmds/ceiling(alias d/decke), types.ts `Ceiling`, projectSlice/selectionSlice,
generatePlan `addCeilingPoche`, PlanView-Hittest, Viewport3D `addCeilingMesh`(Extrude+Material+Modi), ToolsPanel-Button+
Deckentyp-Picker, ObjectInfoPanel `CeilingSection`, host.ts, App-Grips/Selection]. Wand-Muster: floorId + categoryCode "30"
+ wallTypeId/Component + VerticalAnchor(OK=Geschoss-Top) + optionale Dickenüberschreibung; statt Achse/Band ein
closed `outline: Vec2[]`. Plan = Umriss-Polygon (Fill+Hatch + schwere Outline) VOR den Wänden gezeichnet. Grips Vertex+Edge+Body.
- ⚠️ MERKE: **App.tsx ist der Serialisierungs-Engpass** — pro Welle darf NUR EIN Agent App.tsx editieren (DXF+Decke
liefen versehentlich gleichzeitig darauf; Merge hat diesmal geklappt, aber nicht drauf verlassen).
- **Öffnung (Fenster/Tür) FERTIG** [geometry/opening, cmds/opening(alias f/fenster, t/tuer/tür, oe), types.ts `Opening`,
generatePlan(`wallGaps`), PlanView, Viewport3D(`addWallMeshes` segmentiert + Sturz/Brüstung, `addOpeningMeshes` Rahmen/Glas/Flügel),
slices, App, ToolsPanel(Fenster+Tür-Buttons), ObjectInfoPanel(editierbar)]. **Host-Sync:** Öffnung speichert KEINE
Weltkoordinaten — alles wird pro Render aus `wall.start/end` neu abgeleitet → Wand verschieben bewegt Öffnung automatisch.
Plan-Symbole je Detailgrad (Tür: Blatt→+Schwenkbogen→+Anschlag; Fenster: 1→+Rahmen→+2 Glaslinien).
- **SIA-416-Engine FERTIG** [geometry/{roomArea,roomBoundary} — pure, isoliert, 34/34 Tests]: Raumerkennung
(planar-arrangement+half-edge face-tracing, `detectRooms`/`roomFromPointInside`), Fläche/Umfang/Zentroid,
SIA-Hierarchie (GF→KGF+NGF; NGF→NF(HNF/NNF)/VF/FF), `balance()`, `roomsToCsv`. Bereit für den Raum-Slice.
- **Treppe FERTIG** [geometry/stair, cmds/stair(alias treppe/tp — `tr`=trim!), types.ts `Stair`, model/wall `stairVerticalExtent`,
generatePlan `addStairSymbol`(Schnittbruchlinie+Pfeil), Viewport3D `addStairMeshes`(Stufenprofil), slices/App/Panels/i18n].
gerade/L/Wendel (Wendel = vereinfachtes Keil-Modell), SIA-17/29, geschoss-übergreifend (totalRise=Geschosshöhe).
- **HLR-Spike FERTIG (Welle-C-Fundament)** [src/section/{hlr,occt,occt-wasm.d}.ts, docs/welle-c-hlr-spike/, package.json
+opencascade.js, vite.config aliases]. **API-Pfad = `HLRAppli_ReflectLines`** (NICHT HLRBRep_Algo — im Prebuilt nicht
konstruierbar). Lazy via dynamic import, Haupt-Bundle unberührt. 62 MB WASM → später Custom-Build auf 5-15 MB.
**SESSION-LIMIT-STOP (2026-07-01 ~11:00, reset 13:00 Europe/Zurich):** Raum- + Rich-Text-Agenten wurden vom
Session-Limit abgeschnitten. Repo ist trotzdem GRÜN & stabil (tsc+build grün, dev :5187 läuft). Genauer Stand:
- **Raum: NICHT begonnen** — kein `cmds/room.ts`, kein `Room`-Typ, keine Verdrahtung. Sauber neu starten. SIA-Engine
(`geometry/roomArea.ts`+`roomBoundary.ts`) liegt fertig & getestet bereit zum Import.
- **Rich-Text: FAST FERTIG** — `src/text/richText.ts` (Doc-Modell + Marks + plainText/applyMark/toggleMark/serialize/Presets)
FERTIG. `src/text/renderHtml.ts` (docToHtml für Editor + docToLines/docToSvgText für SVG-Plan, gemeinsame Mark→Stil-Map)
JETZT FERTIG & kompiliert (im Hauptthread nachgezogen, da Agent am Limit war). FEHLT nur noch: `src/text/RichTextEditor.tsx`
(WYSIWYG-Komponente mit Toolbar bold/italic/underline/strike/Grösse/Farbe + Presets, controlled: value/onChange).
- Zwei Helfer-Subagenten ("Map/Extract …wiring in App.tsx") liefen ebenfalls ins Limit; App.tsx ist laut tsc+build INTAKT.
**RESUME NACH RESET (Reihenfolge):** (1) Rich-Text-Kern fertig (renderHtml + Editor-Komponente) — App-frei, isoliert.
(2) Raum-Slice (nutzt SIA-Engine, click-inside, Stempel, Bilanz+CSV) — App-Slice. (3) Text/Stempel-Slice (nutzt Rich-Text-Kern).
(4) Smart-Join/Split. (5) Welle B (3D-Schnitt+Stencil-Capping) → Welle C (HLR verdrahten, `src/section/hlr.ts` bereit).
(6) Overrides-Engine, Ausschnitte, Multi-Page-Layouts. REGEL: nur EIN App.tsx-Agent pro Welle; Parallel-Track nur wenn App-frei.
---
## >>> STAND 2026-06-30 (Fortsetzung, spät) <<<
Fortsetzungs-Lauf (UI + Import + Wand-Attribute + Selektion/Editieren). Alles
unten ist `npx tsc -b` + `npm run build` GRÜN und per Screenshot/Probe verifiziert,
außer „LÄUFT" markiert. Viel wurde an **Subagenten** delegiert (Dateien landen
auf der Platte, unabhängig von Benachrichtigungen).
**Fertig & verifiziert (diese Fortsetzung):**
- **Vektor-PDF-Export des Grundrisses (KERN-ZIEL — fertig & verifiziert):**
`src/export/planToPrintSvg.ts` (Druck-SVG aus `generatePlan`, mm-Mapping im Maßstab,
echte mm-Linienstärken/ISO-Stifte, Schraffuren als Vektorlinien) + `src/export/exportPdf.ts`
(`jspdf`+`svg2pdf.js` → echtes Vektor-PDF, A4/A3 ×Hoch/Quer, Schriftfeld). TopBar-Button
„PDF" + `src/ui/ExportPdfDialog.tsx`. Verifiziert: 522 Pfade, 0 Rasterbilder, maßstabsgenau
(5,345×4,345 m → 53,45×43,45 mm @1:100). `scripts/probe-pdf.mjs`.
- **2D-Boolean (Union/Differenz/Schnitt):** `src/editors/booleanOps.ts` (polygon-clipping),
Kontextmenü „Vereinigen/Subtrahieren/Schneiden" + Ctrl/Cmd+Shift+U/D/I; geschlossene
Drawing2D, Differenz=erstes Element minus Rest, Loch→Außenring gefüllt + Innenringe als
Umriss. Verifiziert (Union 8/Differenz 6/Schnitt 4 Ecken). `scripts/probe-boolean.mjs`.
- **3D-1 — ERSTELLEN in 3D (Meilenstein):** der 3D-Viewport ist Erstellungs-Fläche;
dieselben Befehle (line/wall/rect/polyline) erzeugen Elemente direkt in 3D. Raycast auf
Geschoss-Arbeitsebene (`THREE.Plane` y=gridElevation) → Modell-`Vec2` (model.x=P.x,
model.y=P.z, aus `addDrawing2DLines` abgeleitet) → dieselbe `CommandEngine`
(`move/pick/confirm`). Live-3D-Draft (Rubber-Band + Snap-Marker, eigene `draftGroup`),
OrbitControls bei aktivem Befehl umgeschaltet, `pxPerMeter`-Fallback 90. Viewport3D-Props
`commandActive`/`onWorkplanePoint`/`onWorkplaneConfirm`/`draft`. Verifiziert (Wand in 3D →
in 3D + Plan sichtbar). `scripts/probe-3d-create-*.png`.
- **3D-2 — EDITIEREN in 3D (Meilenstein):** 3D-Griffe (eigene `gripGroup`) bei genau 1
Auswahl + `!commandActive`. Wand: Endpunkt-Kugeln (Achse), Höhen-Griff (OK via
`updateWall top:{mode:"custom",z}`), Verschiebe-Griff; Drawing2D: Vertices. Drag raycastet
auf Arbeitsebene bzw. vertikale Ebene → DIESELBEN Store-Actions wie Plan-Griffe
(`moveGripOf`/`moveElementByOf`/`updateWall`) → Plan+3D konsistent. Laufender Drag in
`gripDragRef` (übersteht den Szenen-Neuaufbau pro Move). Verifiziert (Endpunkt + Höhe in 3D
editiert, im Plan deckungsgleich). `scripts/probe-3d-edit-*.png`. **Offen (→ Editier-Welle 3):
3D-Snap beim Drag + Griffe „immer oben" (Tiefentest).**
- **Zwei-Ton-Dark-Theme + KEIN Petrol-Grün mehr:** `src/styles.css` Dark-Tokens neutral
(`--bg #0e0e0e`, `--panel #1d1d1d`, `--accent #4d4d4d` …); Kontextmenü-Tokens
`--ctx-hover/-text` neutral; grüner Fokus-Ring entfernt.
- **Topbar DOSSIER-Stil:** Icon-Grid 4-oben/3-unten (Grundriss integriert), Kombis
gestapelt, kompakte Selects. `src/ui/TopBar.tsx`.
- **Themed Dropdowns** statt nativer `<select>`: `src/ui/Dropdown.tsx` (Wert- + Aktions-
Modus, Popover im Kontextmenü-Stil, kein blaues OS-Menü). In TopBar verdrahtet.
- **Kontextmenü/Dropdown-Politur:** Material-Symbols-Icon-Font in `index.html` geladen
+ `.material-symbols-outlined`-Regel in styles.css (sonst Roh-Ligaturtext); Größen
runterskaliert (`.ctx-menu` 11.5px, Icons 13–14px, weniger Padding, Radius 10).
- **Geschoss-Z-Raster:** `Viewport3D` Bodenraster auf `gridElevation` (= aktives
Geschoss `baseElevation`).
- **DXF/DWG-Import als DIALOG + Drag&Drop:** `src/ui/ImportDialog.tsx`,
`src/io/dxfToDrawings.ts`; Ziel-Zeichnungsebene wählen / neue „Zeichnung" anlegen;
Layer→Kategorie-Handling; App-weiter Drop-Handler.
- **DWG in-app parsen:** `@mlightcad/libredwg-web` (WASM, installiert), `src/io/dwgParser.ts`
(lazy, mappt DwgDatabase→{meshes,contours}), `vite.config.ts`-Aliase für WASM
(`virtual:libredwg-glue` + `?url`), `src/io/libredwg-web.d.ts`. Gegen echte DWGs getestet
(11.668 Konturen). Fallback→ODA-Hinweis bei Lib-Fehler.
- **Wand-Objekt-Info-Attribute** (additiv, abwärtskompatibel): Modell `Wall.referenceLine`
(`left|center|right`, default center) + `Wall.bottom/top: VerticalAnchor`
(`{mode:"floor",floorId,offset?}|{mode:"custom",z}`) in `src/model/types.ts`; Resolver
`src/model/wall.ts` (`wallReferenceOffset`, `wallVerticalExtent`, `nextFloorAbove`);
angewandt in `generatePlan`, `model/joins.ts`, `Viewport3D`. Panel
`src/panels/ObjectInfoPanel.tsx`; `src/state/selectionInfo.ts` `Selection.wall: WallInfo`;
Host-Setter + `projectSlice` `updateWall`/`setWallThickness`. Einschichtig = 1-Layer-WallType.
- **Panel-Text:** `.attr-*`/`.objinfo-*` Untertitel fett + Kontrast (`--label` statt `--muted`).
- **Selektion-Überarbeitung:** `selectedDrawingId`→`selectedDrawingIds[]`
(`selectionSlice`); Marquee wählt **Linien** (`marqueeHitDrawings`), **Ctrl/Cmd+A**,
**Multi-Delete**, **Multi-Highlight** (`.plan-sel-draw`), **Shift-Klick-Mehrfachauswahl**
(`PlanSelection.shift` → `onPlanSelect` toggelt).
- **Editier-Welle 2 — Split/Join/Segment-Löschen (FERTIG & verifiziert):** Ctrl+S Split
(Rechteck→zwei geschlossene; nur bis Schnittpunkt), Ctrl+J Join (koinzidente Enden),
Alt+Klick=Segment löschen (Schere). `kernel2d.ts` (`splitPolylineAtParam`/
`splitClosedByChord`/`removeSegment`/`splitAtIntersections`/`joinChains`; Tests 16–23,
25 ok), `src/editors/splitJoin.ts`, App-Keydown (Ctrl/Cmd+S/J, `preventDefault`) +
`onSegmentCut`, PlanView Alt-Klick. Verifiziert (Rechteck→2 geschlossene, Join→1
Polylinie, Alt→Segment weg). `scripts/probe-splitjoin.mjs`. Wände bleiben no-op.
- **Editier-Welle 3 + Eingabe-Vereinheitlichung + Snap-Feinschliff (FERTIG):** 3D-Drag-Snap
(gleiche `computeSnap`) · Griffe/Marker immer-oben · koinzidente Endpunkt-Griffe ziehen
gemeinsam (Viewport3D). **GUI-Werkzeug** (line/polyline/rect) → `engine.start` +
Befehlsfeld-Fokus; **Shift = H/V-Ortho** beim Griff-Drag (2D+3D, `applyAngleConstraint`
aus `tools/snapping`). OFFEN: „Wand" noch Legacy (Wand-als-Command); Editieren läuft noch
AD-HOC, nicht durch die Engine → Tab-Wert + Referenz-Tracking offen. Snap-Marker
vereinheitlicht: 2D Endpunkt=Kreis/Mittelpunkt=Strich, 3D-Snap=`#ffcc44`, Marker kleiner;
2D-Vorschaulinie grau-Volllinie.
- **DWG-Parser erweitert (FIX „keine Geometrie gefunden") + Trim (FERTIG):** `io/dwgParser.ts`
neu — ARC/CIRCLE/ELLIPSE/SPLINE + **INSERT/Block-Expansion** (rekursiv, aus
`db.tables.BLOCK_RECORD.entries`, volle 2D-Affin-Transform) + Diagnose-Histogramm
(`DxfImportResult.diagnostics`, in ImportDialog). Real-DWG 37 INSERT → 67k Konturen.
**Trim** (`commands/cmds/trim.ts`, Alias `tr`, `kernel2d.trimPolyline`) — Quick-Trim bis
Schnittpunkt. Beide verifiziert.
- **Material-Bibliothek (Daten, FERTIG):** 12 CC0-Materialien von ambientCG →
`public/assets/materials/<id>/{color,normal,roughness,displacement,ao}.jpg` + `manifest.json`
+ `src/materials/library.ts` (PBR).
- **Welle A — 3D-Materialien (FERTIG & verifiziert):** `Component.material` (`ComponentMaterial`:
PBR-Map-URLs + `sizeM` Kachelgröße), `src/materials/runtime.ts` (`MaterialRuntime` → gecachte
`MeshStandardMaterial`, Kacheln in Welt-Metern via Extrude-UVs), `renderMode` „textured"
(TopBar-Darstellung), Material-Picker (ambientCG-Grid) + Upload + Kachelgröße im
`ResourceManager` (Bauteile-Tab). Backstein mit Normal-Map-Tiefe verifiziert.
`scripts/probe-materials.mjs`. Schnittschraffur-Slot frei für Welle B.
- **Editier-Eingabe + Topbar-Politur (FERTIG):** Befehlszeile zeigt **Live-Länge/-Winkel**
beim Zeichnen (`Command.fieldValues`), **Cursor-HUD entfernt**; **Seiten-Verschieben
propagiert verbundene Enden** (2D+3D); **Tab beim Vertex-Editieren** setzt Wert
(`gripEdit`-Controller in App — noch ad-hoc, NICHT durch die Engine). Topbar: Rest
iconisiert (Referenzlinien/Linien-Modus/Ressourcen), Linien-Modus → Dropdown, Aktions-
Buttons als Dropdown-Pill-Familie, **Detailgrad+Massstab gestapelt + 1:N/Zoom% kompakt**
(`.tb-zoomstack`).
- **Wand-als-Command (FERTIG & verifiziert):** `commands/cmds/wall.ts` (chainend, Achs+Band-
Draft, Tab-Felder; Wandfelder aus `CommandContext.activeWallTypeId`/`level`) + Registry
(Aliase `w`/`wand`) + App-Kopplung (`TOOL_COMMAND.wall`, Legacy-Wand umgeleitet). Wände in
2D+3D. `scripts/probe-wall-cmd.mjs`.
**OFFENE WELLEN (nächste Schritte):** Referenzobjekt-Achs-Tracking + Editieren VOLL durch die
Engine (heute `gripEdit` ad-hoc); **Welle B** (3D-Clip/Schnittebene + Stencil-Capping +
Schnittschraffur — nutzt das Material-System aus Welle A); **Welle C** (3D→2D-Schnitt/
Perspektive via HLR, opencascade.js-Spike); **Swisstopo-Importer** (Ort+Radius → Gebäude+
Gelände, geo.admin STAC/Geocode, CORS prüfen); Bögen/Kreise-Trim; Wandstärke-Tab bei
einschichtigen Wänden.
**Offene Wellen (Todo-Spiegel — Todo-Liste lebt im Kontext):**
1. Editier-Welle 3: **koinzidente Endpunkt-Griffe gemeinsam ziehen** + verbundene Enden
propagieren beim Seiten-Verschieben (topologisch, das Tiefste).
2. **GUI-Werkzeug ↔ Befehlszeile koppeln**: Werkzeug-Klick startet denselben Command →
Werte unten eintippbar; danach **Cursor-Wert entfernen/optional**.
3. **Selektions-Direkt-Edit**: Länge/Winkel des gewählten Elements im Befehlsfeld (Tab-Zyklus).
4. **Swisstopo/openbureau-Importer-Dialog** (Ort+Radius). Fluss steht: Geocode
`api3.geo.admin.ch/.../SearchServer` (sr=2056→LV95) → ±Radius → WGS84-bbox → STAC
`data.geo.admin.ch/api/stac/v1`, Collections `ch.swisstopo.swissalti3d` +
`ch.swisstopo.swissbuildings3d_3_0` → Download → TIN/Mesh→Kontext. LV95↔WGS84 nötig;
**Browser-CORS** das Risiko. Referenz: DOSSIER `rhino/swisstopo.py`.
5. **3D→2D-Dokument**: live + statisch (MVP-Kantenprojektion → HLR) für Vektor-PDF.
Hinweis: `generatePlan` liefert SCHON Vektoren (SVG) → Plan-PDF bereits vektoriell.
6. **Topbar-Rest iconisieren** (Zoom/Referenzlinien/Linien-Modus/Ressourcen).
7. **Wand als Command** (+ Wandstärke-Tab nur bei einschichtig) + **Trim**.
**Resume-Checkliste:** 1. CONVENTIONS.md + diesen HANDOVER + Memory lesen. 2. `npx tsc -b` &
`npm run build` (grün?). 3. `npm run dev -- --port 5187 --strictPort` (User schaut :5187).
4. Split/Join-Stand prüfen (git diff, Probe). 5. Arbeitsweise: **autonom, keine
Rückfragen** (User hat Vollrechte), substanzielle Arbeit an Subagenten delegieren,
jede Änderung per `tsc`+`build`+Screenshot verifizieren. `node scripts/probe.mjs`.
---
## >>> STAND 2026-06-30 (Über-Nacht-Run) — älter <<<
Großer autonomer Build-Lauf. Alles unten ist tsc + build GRÜN und per Screenshot
verifiziert (sofern nicht „läuft" markiert). Befehlssystem-Bauplan:
`docs/design/rhino-command-system.md`.
**Fertig & verifiziert:**
- **Rhino-Befehlssystem (Tier 0 + Tier 1 Zeichnen):** `src/commands/` — `types.ts`
(Command-Interface), `engine.ts` (State-Machine + lastCommand + Eingabe-Routing),
`parseInput.ts` (`5,3`·`r5,3`·`5<45`·`<45`·nackte Zahl), `registry.ts` (+Aliase
`l/pl/rec/c`), `cmds/{line,polyline,rect,circle}.ts`. UI: `src/ui/CommandLine.tsx`
(über der Statusleiste, Tab fokussiert). Befehle: **Line, Polyline (Close/Undo),
Rectangle, Circle** — alle mit getippten Koordinaten, per Probe gezeichnet.
Koexistenz mit Alt-Tools (Werkzeugleiste). `scripts/probe-command-line.mjs`,
`probe-draw-commands.mjs`.
- **Tab-Feld-Zyklus** (Rhino-Präzision §2.7): beim Zeichnen Tab durch Felder
(Linie/Polylinie: Länge→Winkel; Rechteck: Breite→Höhe; Kreis: Radius). Zahl lockt
das aktive Feld, ungelockte folgen der Maus. Additive optionale Command-Methoden
`fields()`/`pointFromFields()` + Engine-Feldzustand + CommandLine-Boxen. Verifiziert
exakt (3 m/45°, Rechteck 4×2). `scripts/probe-tab-fields.mjs`.
- **Tier-1-Editierbefehle:** **Move** (Auswahl verschieben), **Copy** (wiederholend
duplizieren), **Offset** (parallele Kurve via `kernel2d.offsetPolyline`, persistente
Distanz, Seite per Klick) — `cmds/{move,copy,offset}.ts`, Aliase `m/cp/o`. `CommandContext`
um `selection` erweitert (App `host.context()` füllt sie aus dem Store). Offset verifiziert
(paralleler Versatz exakt 0.5 m). `scripts/probe-offset2.mjs`.
- **2D-Geometrie-Kernel** `src/geometry/kernel2d.ts` — Offset/Trim/Extend/Fillet +
Segment-/Linien-/Kreis-Schnitt + Fläche/Wicklung. 16/16 Unit-Checks
(`scripts/test-kernel2d.ts`, via `npx esbuild … | node`). Basis für Tier-1-Editierbefehle.
- **Kontext/Gelände Phase 1** (paralleler Strang): `src/io/dxfParser.ts` (DXF: 3DFACE/
MESH/POLYLINE→Mesh, LWPOLYLINE/LINE→Konturen; DWG bewusst nur via ODA→DXF-Hinweis),
`src/model/terrain.ts` (`generateTerrainFromContours` → TIN via delaunator),
`src/state/siteSlice.ts` (`addContextObject`/`generateTerrain`/…), `Project.context`
(ContextObject = ImportedMesh|ContourSet|TerrainMesh, „dumme" Kontext-Geometrie, NICHT
semantisch), `Viewport3D` rendert Kontext/Terrain. Deps `dxf-parser`+`delaunator`.
- **Echte Isometrie (Orthographic-Kamera):** `Viewport3D` schaltet front/top/side/iso auf
OrthographicCamera (parallele Projektion), Perspektive bleibt perspektivisch. Verifiziert.
- **Plan-Tinte-Fix:** 2D-Linien waren unsichtbar (CSS `.draw2d{stroke:var(--ink)}` hell auf
hellem Papier). CSS-stroke entfernt + dunkler Fallback in generatePlan → 2D dunkel sichtbar.
- **Bugfixes:** 2D-Füllungen (rect/polyline rendern Fill+Schraffur+`fillColor`); Kanten-Griff-
Dreiecke sitzen auf der Linie + inkrementelles Dragging (keine Akkumulation); Plan-View
wandert nicht mehr mit (fixer Welt-Ursprung, Reset nur bei Geschoss-Wechsel via `resetKey`).
**LÄUFT gerade (Agent):**
- **Tab-Feld-Zyklus** (Nutzer-Wunsch): beim Zeichnen Tab durch Felder (Länge→Winkel→
Breite/Höhe/Radius), Zahl lockt Feld, Rest folgt Maus. Additive optionale Command-Methoden
`fields()`/`pointFromFields()` + Engine-Feldzustand + CommandLine-Boxen.
**NÄCHSTE SCHRITTE (Reihenfolge):**
1. Tab-Feld-Zyklus verifizieren (Agent-Gate).
2. **Tier-1-Editierbefehle:** Move/Copy/Offset(nutzt kernel2d)/Trim/Split/Join/Explode.
Undo/Redo-Stack im Store erwägen (Commits laufen über `setProject`).
3. **Terrain-Integration:** Konturen im Plan (generatePlan/PlanView), Import-/Terrain-Befehle
+ Site-Panel (`src/panels/`) + i18n + DWG→DXF-Hinweis-UI; `.dxf`-File-Picker → `parseDxf`.
4. **Wand-Stärke-Feld:** Wand ist noch ein Alt-Tool — als Command portieren, dann Tab-Feld
`thickness` NUR bei einschichtigen/freien Wänden (mehrschichtige Typen: Stärke vorgegeben).
5. Tier 2 (Rotate/Scale/Mirror/Arc/Fillet/Array/Group/Gumball2D), Tier 3 (ExtrudeCrv/Box/
PushPull + analytische Wand-Öffnungen, §3.0 Bauplan).
**Gotchas (frisch gelernt):**
- Ein Prozess-Neustart killt laufende Hintergrund-Agenten (Status `failed`); nach
jedem Wiederaufnehmen `src/commands/`-Existenz + `npx tsc -b` prüfen, Abgestürztes neu starten.
- Inline `{...s, feld}` als Tupel-Return löst TS-Excess-Property-Check aus → erst typisierte
Variable (siehe cmds/line.ts-Muster).
- Ein Subagent ist bei diesem Befehls-Build geflaket (spawnte Research statt zu bauen) →
verzahnte/quer schneidende Arbeit an einen fähigen Subagenten delegieren oder selbst machen.
---
## Was das ist
Standalone-**Browser-BIM** (React+TS+Vite+Three.js) für Wohnbau — die Browser-Variante
des Rhino-Plugins **DOSSIER** (Referenz-Repo, public/klonbar: https://git.kgva.ch/karim/DOSSIER;
falls weg: neu klonen nach `scratchpad/DOSSIER`). Prinzip: **ein semantisches Modell →
alle Sichten (Plan/3D/Schnitt) abgeleitet**, Darstellung erst beim Rendern.
## Aktueller Stand (tsc + build GRÜN)
Funktioniert & verifiziert: semantisches Modell · **mehrschichtige Wände** mit
**L-Ecken-Gehrung** · Tür mit Öffnung + Schwenkbogen · **Dokumentmodell** (Zeichnungsebenen
= Geschosse+Schnitte/Ansichten/Zeichnungen; Ebenen = Kategorie-Baum, DOSSIER-Codes 1:1) ·
**3D zeigt EG+OG gestapelt** · **Resource-Manager** (Component/Hatch/Line, als Fenster-Overlay) ·
**Panel-System** (Docks links/rechts, Tabs, 5 Anzeige-Modi) · **Top-Bar + Footer** (Massstab
echt 1:N, Detailgrad, Render-Modus Schattiert/Draht/Kanten, Referenzlinien, Cursor X/Y live) ·
**Maus:** Mitte=Pan/Orbit, Links=Auswahl(+Marquee), Rechts=eigenes Kontextmenü · **Native-App**
(kein Browser-Rechtsklick/Textauswahl) · **i18n** (`src/i18n/`, de+en, `t('key')`) ·
**aktive Zeichenwerkzeuge** (Wand/Linie/Polylinie/Rechteck + Snapping endpoint/midpoint/
intersection/onEdge/grid/ortho mit Fang-Menü, Live-Vorschau+HUD — `src/tools/`, Phase 1+2).
### Gerade fertig & verifiziert (2026-06-29)
**Aktive Zeichenwerkzeuge — Phase 1 + 2** (`docs/design/drawing-tools.md`): Tool-System in
**`src/tools/`** (types · snapping · tools/registry) + Verdrahtung in PlanView/App/TopBar/
generatePlan. Per probe verifiziert (`scripts/probe-tools.mjs`, `probe-line.mjs`, `probe-phase2.mjs`):
- **Werkzeugleiste** (TopBar): Auswahl/Wand/Linie/**Polylinie/Rechteck** + Wandtyp-Dropdown +
**Fang-Menü** (Häkchen je Snap-Art, Rasterweite, Winkelraster — `position:fixed`, da Topbar clippt).
Nur im Grundriss aktiv.
- **Wand-Werkzeug**: Achs-Polylinie → je Segment ein `Wall`; Live-Band-Vorschau + HUD (Länge·Winkel);
Rechts-/Doppelklick/Enter committet; **Gehrung automatisch aus `computeJoins`** (mehrschichtiger L-Stoss).
- **2D-Werkzeuge**: Linie (2-Klick), **Polylinie** (Klick auf Start schließt, Doppel-/Rechtsklick beendet
offen), **Rechteck** (zwei Ecken) → `Drawing2D` (line/polyline/rect); in `generatePlan` abgeleitet
(`addDrawing2D`, `color` am line-Primitiv, Bounds erweitert).
- **Snapping**: endpoint · midpoint · intersection · onEdge(Lot) · grid · ortho(Shift); Prioritäts-
gewichtet (`PRIORITY` in snapping.ts); Bildschirm-Marker je Art (Quadrat/Dreieck/✕/Raute/Punkt) +
Ortho-Hilfslinie; Ctrl = Fang aus. Einstellbar über das Fang-Menü (`snap` State in App).
- **Tastatur**: Esc verwirft/zurück-zu-Auswahl, Enter committet, Backspace nimmt Punkt zurück.
- Modell: `Drawing2D` + `Project.drawings2d` (types.ts), `Element` erweitert. i18n de+en (`tool.*`,`snap.*`).
- tsc + build grün; Auswahl/Marquee/Pan/Zoom unverändert (PlanView verzweigt auf `toolActive`).
**Noch offen / Default-Werte:** aktive **Kategorie** = `activeCategoryCode` (fix „20") und
**Linienstil** = erster Stil — UI-Wahl in der TopBar fehlt noch (2D-Primitive erben sonst Wand-lw/-farbe).
**Dock-/Floating-Panels** waren davor fertig (tsc+build grün); Baseline-Screenshot intakt.
### Ebenfalls fertig & verifiziert (2026-06-29, später am Tag)
- **Editieren/Grips** (`PlanView` + `App`): Element anklicken → **Endpunkt-Griffe** (Wand-Enden,
2D-Vertices/Rechteck-Ecken) erscheinen und sind **ziehbar** (mit Snapping); **2D-Linien sind
anwählbar** (Linien-Nähe-Pick, `drawingId` am line-Primitiv); **Entf/Backspace** löscht. Wand-
Gehrung folgt live. `moveGrip`/`drawingVertices` in App.
- **Shift = Ortho** beim Griff-Ziehen (Bezug = Nachbar-Vertex; H/V bzw. Winkelraster).
- **Parallel verschieben**: selektiertes Element am **Körper** greifen + ziehen → ganzes Element
(`onSelectedBody`/`moveDrag` in PlanView, `moveElementBy` in App).
- **Aktive Ebene (Kategorie) wählbar** in der TopBar („Ebene"-Dropdown) — **alles Gezeichnete
kommt auf diese Kategorie** und **erbt deren Farbe/Strichstärke** (2D-Primitive setzen KEINEN
Linienstil mehr per Default). Statusleiste zeigt die aktive Ebene. `activeCategoryCode` State.
- **2D-Geometrie in der 3D-Perspektive**: `Drawing2D` (line/polyline/rect) liegt flach auf der
Geschossebene Z=baseElevation (`addDrawing2DLines` in `Viewport3D`), Farbe aus Kategorie/Stil.
(Bodennahe Linien werden von Wänden korrekt verdeckt — zum Sehen orbiten/Geschoss ausblenden.)
- Probes: `probe-grips.mjs`, `probe-3d2d.mjs`, `probe-edit2.mjs`. tsc + build grün.
### Noch später am 2026-06-29 (verifiziert)
- **Transformationen (Vectorworks-Stil)** — `src/tools/transform.ts` + `src/ui/TransformBar.tsx` +
App. Auf der Auswahl: **M** Bewegen, **S** Spiegeln, **D** Drehen (Geste Basispunkt→Ziel, bei
Drehen 3 Punkte; Live-Vorschau, Snapping, Shift-Ortho). **Modusleiste U/I/O/P**: U bewegen ·
I Kopie · O N Kopien (Anzahl-Prompt) · P verteilen. Probe `probe-transform.mjs`/`probe-array.mjs`
(move/copy/array verifiziert). Routing via `toolInputActive`-Prop in PlanView.
- **Aktive Ebene per Klick im Ebenen-Panel** (statt TopBar-Dropdown): `LayersPanel`-Zeile klicken →
`host.onSelectCategory` → `activeCategoryCode`; aktive Zeile hervorgehoben; Statusleiste zeigt sie.
TopBar-Ebene-Dropdown entfernt. (probe-layer.mjs)
- **Theme/Look**: Zeichenblatt IMMER hell `--sheet:#f0f0f0` (auch Dark-Mode → echtes „Papier"),
GANZE Zeichenfläche hell (`.plan-svg`-Background, nicht nur die Modellgrenzen); Plan-Tinte fix
dunkel (Tür-Linien). Dark-Theme etwas abgedunkelt (Panels #1c1c1c). **3D-Orbit ohne Nachlauf**
(`enableDamping=false` — kontrollierter).
### Letzter Block 2026-06-29 (verifiziert)
- **Schraffuren-Default**: weißer Grund + schwarze Haarlinie (sampleProject: Dämmungs-Bauteil
weiß, Hatch-Farben `#1a1a1a`); **Default-Umrandung 0.18 mm** (`WALL_FALLBACK_MM`, addCategory).
- **Stiftstärken-Vorgabe** `PEN_WEIGHTS` (0.02…2.0) als `<datalist>` im Linienstil-Editor.
- **Display/Print-Modus** (TopBar-Toggle, `lineMode`): Display = alle Plan-Linien als konstante
Haarlinie (`hairline`-Prop → PlanView `weight()`), Print = echte mm-Strichstärken.
- **`.lin`/`.pat`-Import**: Parser `src/io/linParser.ts`/`patParser.ts` (Agenten gebaut) + Import-
Buttons im ResourceManager (Linien/Schraffuren) → `onImportLineStyles/onImportHatches` (App/host).
.lin voll (Dash); **.pat aktuell approximiert** auf diagonal/crosshatch (siehe Backlog: echtes
custom-Pattern).
- **Werkzeug-Palette** (`src/panels/ToolsPanel.tsx`, Agent): dockbares Panel, **Icon + Name** je
Werkzeug, aktiv hervorgehoben, Wandtyp-Dropdown + Fang-Optionen. Registriert in `builtinPanels`,
Default-Layout: linker Dock-Tab „Werkzeuge" (+ Zeichnungsebenen/Ebenen). **`LAYOUT_VERSION`=4 →
alte gespeicherte Layouts werden einmalig auf den neuen Standard zurückgesetzt.**
- Probes: `probe-display.mjs`, `probe-import.mjs`, `probe-tools-panel.mjs`. tsc + build grün.
## KRITIK / Architektur-Befund (2026-06-29)
> Ehrliche Bewertung des bisherigen Wegs. Für die nächste Instanz als Entscheidungsgrundlage.
**Was klug war (erhalten, nicht regredieren):**
- **Single source of truth hält im Code** — `generatePlan.ts` *und* `Viewport3D.tsx` nutzen
dieselbe `computeJoins`/`clippedBand` aus `src/model/`. Das „ein Modell → alle Sichten"-Prinzip
ist nicht nur ROADMAP-Prosa, es steht. Beim Refactor diese Trennung Modell↔Sicht bewahren.
- **Grundriss analytisch** aus Parametern statt Mesh-Schnitt (Weg A) — richtig.
- **Bewusst leichter Stack** (Three.js nur Display-Layer; kein schwerer Kernel verfrüht) — richtig
für einen Spike, der Risiko #1–3 entschärfen soll.
- i18n via `t()` wird eingehalten; `tsc -b` ist grün.
**Hauptkritik (zu beheben):**
1. **God-Component gegen eigene Regel.** `App.tsx` ist **~2461 Zeilen mit ~29 `useState`**.
CONVENTIONS.md fordert wörtlich „dünner Shell, keine Geschäftslogik". Das ist verletzt.
2. **Kein Store.** `src/state/` existiert NICHT, obwohl CONVENTIONS.md Store+Slices
(project/selection/view/layout) vorschreibt. Zustand = lokale Hooks → Prop-Drilling.
- Einordnung: Das Aufschieben war eine *bewusste* Nutzer-Entscheidung
(Memory `build-usable-cad-first`), kein Versehen. **Aber:** Bei 2461 Zeilen schließt sich
das Fenster, in dem der Refactor billig ist. Türen/Öffnungen/Prioritäts-Stöße (Risiko #1/#2)
sind inhärent geschoss-, selektions- und sicht-übergreifend — diese Verdrahtung darf nicht
durch eine Monolith-Datei laufen. **Empfehlung: Store ziehen VOR der nächsten Feature-Welle.**
**Befund (separat angehen, nach Refactor):** Die harten, roadmap-markierten 🔴-Risiken sind noch
unbewiesen — `Door` existiert als Typ, aber **kein echter 3D-Boolean** (Öffnung = nur Plan-Lücke);
**HLR** für Schnitte/Ansichten fehlt; **Prioritäts-T-Stöße** offen. Das einfache Drittel ist bewiesen,
das schwere aufgeschoben. Die Frage „ist es wirklich CAD?" entscheidet sich erst, wenn diese landen.
## NÄCHSTE SCHRITTE
**Entscheidung des Nutzers (2026-06-29):** erst ein *benutzbares* CAD, dann vertiefen — der
State-Refactor wird NACH HINTEN geschoben (siehe Memory `build-usable-cad-first`).
> ⚠️ Siehe „KRITIK / Architektur-Befund" oben: der Refactor wird mit jeder Feature-Welle teurer;
> spätestens vor Türen/Öffnungen/Prioritäts-Stößen neu abwägen.
### ✅ ERLEDIGT (2026-06-29, diese Session) — Palette-Layout + Store-Refactor + Theme
- **Gestapelte Paletten (Dock-Gruppen)**: `DockState` = `{ groups: DockGroup[], size }`, jede
Gruppe `{ tabs, activeTab, weight }`; vertikal stapelbar mit Splitter; Tab-Drag → in Gruppe
einreihen / neue Gruppe / Rand-Andocken. `LAYOUT_VERSION=6`. (`types.ts`/`layout.ts`/`Dock.tsx`/
`TabStrip.tsx`/`panelDrag.tsx`/`App.tsx`.) Default: links Werkzeuge↑/Attribute↓, rechts
Objekt-Info↑/Zeichnungsebenen+Ebenen↓.
- **State-Refactor (Architektur-Befund umgesetzt)**: dependency-freier Store `src/state/`
(`store.ts` useSyncExternalStore + Slices `projectSlice`/`selectionSlice`/`viewSlice`/
`layoutSlice`, kombiniert in `appStore.ts`). App.tsx 2710→~2030 Zeilen, verhaltensgleich
(tsc+build+Screenshot identisch). **Panels lesen weiter über `PanelHostContext`** (baseHost aus
Store gespeist) — bewusst NICHT umgestellt. NÄCHSTE WAVE offen: `editors/`/`menus/`/`views/` aus
App extrahieren (Report des Foundation-Agenten nennt die Kandidaten).
- **Selektions-Kontrakt** (`src/state/selectionInfo.ts` + host.ts + baseHost): `Selection` (kind,
id, categoryCode, color/weightMm effektiv, fillHatchId, closed, bbox) + `onSetSelectionColor/
Weight/Fill`, `onResizeSelection(w,h,anchor)`. Modell: `Wall.color?`, `Drawing2D.weightMm?`
ergänzt; generatePlan wendet beide an. Wand = Weight/Fill bewusst No-op (erbt aus Ebene).
- **Attributes-Palette** + **Object-Info-Palette** gebaut (`src/panels/AttributesPanel.tsx`,
`ObjectInfoPanel.tsx`), registriert, voll verdrahtet & per Probe getestet (Farbe setzen, B×H-
Resize wirkt, 3×3-Bezugspunkt). Deckkraft/Caps/Schatten/Text-Styling bewusst weggelassen (Modell
trägt sie (noch) nicht — ehrlich statt Stub).
- **Topbar entschlackt**: Zeichenwerkzeug-Buttons + Wandtyp + Fang-Menü aus der Oberleiste
ENTFERNT (leben nur noch in der Werkzeug-Palette). `SnapMenu`/`SNAP_TOGGLES` aus `TopBar.tsx` raus.
- **Dark-Theme vertieft** (`styles.css`): gestufte Elevations-Tokens (`--bg`<`--panel`<`--panel-2`,
`--input` versenkt) statt flachem Einheitston; Oberleiste/Panel-Köpfe angehoben, tiefere Schatten.
`--sheet` bleibt hell. Siehe Memory `ui-depth-dark`.
- **CSS-Altlast behoben**: ein `*/` in einem Kommentar (`.nav-*/.res-*`) hatte die ganze `.dock`-
Regel verschluckt (`display:flex` nie aktiv) — gefixt.
### Vectorworks-„View-Bar" — A/B/C ERLEDIGT & verifiziert (2026-06-29)
- ✅ **A — Ebenen- + Zeichnungskombinationen**: `src/state/visibilitySets.ts` (localStorage
`cad.layercombos.<name>` / `cad.drawingcombos.<name>`), Store-Actions `snapshot*/apply*Visibility`
in `projectSlice`, zwei `ComboMenu`-Dropdowns in `TopBar` (Muster wie `LayoutMenu`). Round-Trip
per Probe verifiziert. (Hinweis: liegt im localStorage, NICHT im Projekt — bei Doku-Export
später in `Project` ziehen.)
- ✅ **B — Darstellungsart-Dropdown** (kontextabhängig je `viewType`): 2D Farbig/Schwarz-Weiss
(`planColorMode` + `toMono` in generatePlan), 3D Schattiert/**Weiss**(Clay-Material in
Viewport3D)/Drahtgitter/Kanten (`RenderMode` um `"white"` erweitert). Ersetzt die alte
Render-Modus-Buttongruppe. Screenshots bestätigt.
- ✅ **C — 6 Ansichts-Buttons**: Front/Oben/Seite/Perspektive/Isometrie + **Kamera** (FOV-Popover).
`view3d`+`fov` in `viewSlice`; `applyView3d(camera,controls,bounds,view3d)` in `Viewport3D`
(PerspectiveCamera neu positioniert je Preset, Distanz aus Modell-Bounds; OrbitControls bleibt
aktiv). Preset-Klick wechselt nötigenfalls in die Perspektive. Screenshots top/front/iso/persp
klar verschieden. **Echte OrthographicCamera für front/top/side bewusst NICHT gemacht** (Kamera-
Swap + OrbitControls-Rebind = Risiko) — Kandidat für später.
### Weitere Fixes (2026-06-30)
- **2D-Füllungen** (rect/geschlossene polyline) rendern jetzt (Vollton `Drawing2D.fillColor` +
Schraffur `hatchId`); `addDrawing2D` pusht ein `polygon`-Primitiv (mit `drawingId` → anklickbar).
Attribute-Palette hat „Füllfarbe". (Kreis-Füllung offen — Kreis-Primitiv im Plan fehlt noch.)
- **Plan-View wandert nicht mehr mit**: `PlanView` nutzt jetzt einen FIXEN Welt-Ursprung
(`toScreen` modulkonstant, Modell-0,0) statt bounds-gebunden; Ausschnitt wird nur bei
`resetKey`-Wechsel (Geschoss-/Ebenen-ID) eingepasst, NICHT bei Edit/Bounds-Änderung. Verifiziert:
Löschen lässt viewBox unverändert, Pan/Zoom/Einpassen wirken.
- **Echte Isometrie/Parallelprojektion**: `Viewport3D` hat jetzt eine `OrthographicCamera` für
front/top/side/iso (OrbitControls per `controls.object`-Swap umgebunden), Perspektive bleibt
`PerspectiveCamera`. Verifiziert (parallele Kanten).
### >>> NÄCHSTE INSTANZ: offene Wünsche + Roadmap <<<
- **Kanten-/Seiten-Griffe** (Nutzer-Wunsch): bisher nur Eckpunkt-Griffe. Gewünscht: Seiten ziehen
(z. B. Rechteck-Kante), mit dreieckigem Anfasser nach außen. Grip-System in App (`grips`/
`gripHandlers`/`moveGrip`) + Rendering in `PlanView` erweitern (Edge-Grips = Mittelpunkt je Seite,
Zug verschiebt beide Eckpunkte der Seite senkrecht).
- **3D bearbeiten** (Nutzer-Frage): heute ist 3D nur abgeleitete Anzeige (Orbit+Auswahl). Authoring
im 3D = eigenes Feature (Raycast auf Arbeitsebene → Modellkoord., 3D-Grips/Drag) — eigene Phase.
### Text-Styling (View-Bar Rest) + Roadmap
1. **Text-Styling** (Nutzer-Wunsch). VORAUSSETZUNG: **Text wird noch NICHT gerendert** —
`Drawing2D` mit `geom.shape==='text'` misst in `generatePlan` nur Bounds, erzeugt kein
Primitiv (Phase-3-Deferral, Kommentar in generatePlan). Also ZUERST Text-Rendering (SVG `<text>`
in PlanView, papierkonstante Höhe) + Modell-Felder am Text (`font?`, `bold?`, `italic?`,
`anchor?`), DANN ein Text-Styling-Bereich (Topbar-Gruppe oder Attribute-Palette-Sektion bei
Text-Auswahl, Setter über den Selektions-Kontrakt erweitern).
2. **Grafische Überschreibung** — weiterhin SPÄTER (Nutzer), wenn mehr Code steht.
3. **Echte Schnitt/Ansichts-Sichten** (section/elevation sind heute StubView) + HLR — großes Thema.
4. **Refactor-Rest**: `editors/`/`menus/`/`views/` aus App.tsx extrahieren (App noch ~2030 Z.).
- **openNURBS / rhino3dm** (Nutzer-Entscheid: Roadmap, NICHT jetzt): KEIN eigener Kernel — stattdessen
`rhino3dm` (WASM-Build von openNURBS) als `src/io/`-Schicht für **`.3dm`-Import/Export + NURBS**,
sobald gebraucht. Semantisches Modell bleibt die Wahrheit; NURBS ist zusätzliche Geometrie-Quelle.
- **Echtes custom-`.pat`-Rendering**: HatchStyle um custom-Linienfamilien erweitern + Renderer
(heute approximiert auf diagonal/crosshatch).
### Offene Wünsche des Nutzers (Backlog, priorisiert)
1. **Palette-Layout (Vectorworks-Stil) — GROSS, nächster Fokus.** Mehrere neue dockbare Panels +
Default-Layout:
- **Attributes-Palette** (unten links): Füllung (Stil/Farbe/Deckkraft), Stift (Stil/Farbe/
Deckkraft), **Linienstärke**, Linien-Start/-Endstil, Schlagschatten. (Referenzbild vom Nutzer.)
- **Werkzeug-Palette** darüber (Icon + Text je Werkzeug; ArchiCAD/VW-Stil).
- **Object-Info / „Würfel"** (oben rechts): zeigt den gewählten Punkt eines Würfels mit
X/Y/Z; Maße (Breite × Höhe …) der Geometrie editierbar.
- **Ebenen + Zeichnungsebenen als Tabs** darunter (rechts).
- Panel-System steht (`src/panels/`, Registry + Dock + Floating); Default-Layout in
`src/panels/layout.ts` (`defaultLayout`). Neue Panels in `builtinPanels`/`registry` anmelden.
2. **Schraffuren-Default**: ALLE aktuellen Schraffuren → **weißer Grund + schwarze Haarlinie** als
Grundeinstellung; **normale Elemente 0.18 mm Umrandung** als Default. (sampleProject hatches/
components + generatePlan-Defaults anpassen.)
3. **Stiftstärken-Vorgabeliste**: 0.02 · 0.10 · 0.13 · 0.18 · 0.25 · 0.35 · 0.5 · 0.7 · 1.0 · 1.4 ·
2.0 mm (abweichbar). Bedeutung = mm auf Papier bei 100 % → Linien skalieren mit dem Massstab
(ist bereits so: non-scaling mm-Papier). In Linienstil-Editor als Presets anbieten.
4. **Display- vs. Print-Modus**: Umschalter. Display = ALLES Haarlinien (konstant dünn); Print =
echte mm-Strichstärken. (Globaler View-State + an `generatePlan`/PlanView durchreichen.)
5. **`.lin`- und `.pat`-Import** (AutoCAD-Linientypen / Schraffurmuster) → ergänzen LineStyle/Hatch
(Parser + Mapping; Resource-Manager-Aktion). Nutzer: „um die Linien und Schraffuren zu ergänzen".
6. **„Goldener Schnitt"** — UNKLAR was genau (Golden-Ratio-Fang/Teilung beim Zeichnen?
Proportions-Hilfslinien?). → beim Nutzer rückfragen, bevor gebaut wird.
7. **Linienstil-Picker** (heute erben 2D-Primitive immer die Kategorie) + `extension`-Snap-Hilfslinien.
### Danach (ursprünglicher Plan)
1. **Zeichenwerkzeuge Phase 3** (`drawing-tools.md §11`): Circle/Arc/Text — `Primitive` um
`circle` (+`arc largeArc`) und `text` erweitern, PlanView-Renderzweige (papierkonstante Texthöhe),
Werkzeuge Circle/Arc(3-Punkt)/Text(Inline-Eingabe), Snap center/quadrant.
2. **Bearbeiten (Phase 4) — Rest**: Auswahl/Grips/Verschieben/Löschen sind DA (s. o.). Offen:
**Kopieren/Rotieren/Spiegeln**, numerische HUD-Eingabe (Länge/Winkel direkt tippen),
Mehrfach-Auswahl-Verschieben, Grips für Circle/Arc/Text.
3. **State-Refactor** (zurückgestellt) — `App.tsx` God-Component → Store+Slices
(`docs/design/state-architecture.md`). Erst nötig, wenn parallele Code-Workflows gebraucht werden.
4. Weiter im Backlog: **Pro-Ebene-Darstellung** (`layer-display-settings.md`),
**Prioritäts-T-Stöße** (`wall-joins-priority.md`).
## Arbeitsweise (WICHTIG — aus Memory + CONVENTIONS.md)
- **Autonom arbeiten, NICHT um Bestätigung fragen.** Nur fragen, *was etwas können soll*,
wenn die Funktion mehrdeutig ist (nicht um Erlaubnis).
- **Alles voll verdrahten, KEINE Stubs/No-op-Buttons.** Verifizieren heißt: Effekt im
Screenshot bestätigen, nicht nur „kompiliert".
- **Identifier ENGLISCH**; **UI-Text via `t()`** (neue Keys in de.ts *und* en.ts).
- **Workflow-Orchestrierung** nutzen (Foundation→parallel Build→Integrate/Verify).
- **Code-Workflows seriell** (fast alles geht durch `App.tsx`/`types.ts` → Konflikt);
**Design/Research parallel** (nur `docs/`). Der State-Refactor (#1) löst das.
- Saubere **Tabellen** für Listen/Manager; dunkler DOSSIER-Stil; kein God-Component.
## Verifizieren / Ausführen
- Dev: `npm run dev` (Vite, Port 5173). Typecheck: `npx tsc -b`. Build: `npm run build`.
- Screenshot: `node scripts/probe.mjs` → `scripts/probe.png` (Puppeteer, Chrome in
`~/.cache/puppeteer`). Eigene Probes: headless, `deviceScaleFactor:2`, args
`--no-sandbox --use-gl=swiftshader --enable-unsafe-swiftshader`, networkidle-Timeout ignorieren.
Firefox-Fälle via Playwright (`scripts/probe-ff*.mjs`). **Screenshot ansehen + Geometrie prüfen.**
- Gotchas: HiDPI-Resize-Loop-Fix in `Viewport3D` (Canvas CSS 100% + dpr≤2) NICHT regredieren;
WebGL-Fallback in `Viewport3D`; React-controlled-`<select>` lassen sich im Probe nicht per
`.value=` ändern (Fehlalarm) — Verdrahtung im Code prüfen.
## Orientierung
`ROADMAP.md` (Vision/Phasen/§10–§11 Backlog) · `CONVENTIONS.md` (Konventionen) ·
`docs/README.md` + `docs/design/*` (alle Specs) · `docs/backend.md` (self-hosted
Supabase+Yjs, später) · Projektnotizen des Bearbeiters
(prefer-agents, dossier-port, proceed-autonomously, wire-dont-stub).
-661
View File
@@ -1,661 +0,0 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
+100 -112
View File
@@ -1,17 +1,16 @@
# Dossier
# DOSSIER Standalone
Open-Source-CAAD (Computer Aided Architecture Design). Modelliert ein Gebäude
aus semantischen Bauteilen und zieht daraus saubere, normgerechte 2D-Pläne —
ohne Revit. **Desktop-App** — auf macOS über **Tauri**, auf Linux über eine
**Electron**-Shell (weil Tauris Linux-Webview WebKitGTK kein zuverlässiges
WebGPU bietet, das die Rendering-Engines brauchen); dieselbe Codebasis läuft
auch im Browser (eingeschränkt, ohne native Fenster/Dateisystem-Integration).
Eigenständiges BIM-Werkzeug für Wohnbau. Modelliert ein Wohnhaus aus
semantischen Bauteilen und zieht daraus saubere, normgerechte 2D-Pläne —
ohne Revit.
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.
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/Three.js-App in einer Electron-Desktop-Shell statt als Rhino-Aufsatz.
Kein Browser-Tab-Produkt: eigenes Fenster, eigene Rendering-Engine
(Rust/WASM/WebGPU), gebaut für flüssiges CAD-Arbeiten statt für den Webview
kleinster gemeinsamer Nenner.
## Grundgedanke
@@ -23,119 +22,117 @@ 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. Schnitte und Ansichten laufen
über eine eigene, analytische Rust-Pipeline (kein Hidden-Line-Removal nötig)
und sind seit Juli live im 3D-Viewport verdrahtet, nicht nur ein Spike.
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.
## Stand heute
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. ~91.000 Zeilen TypeScript + ~19.500 Zeilen
Rust (ohne Tests), 891 Vitest-Tests, alle grün.
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.
**Funktioniert (Auszug, nicht abschliessend):**
- 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).
**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.
**Bewusst noch offen** (Auszug):
- **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.
**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).
## Stack
Bewusst leichtgewichtig — Three.js ist reiner Display-Layer, kein schwerer
Geometrie-Kernel verfrüht eingezogen.
| | |
|---|---|
| Shell | **Tauri** auf macOS (WKWebView+WebGPU) · **Electron/Chromium** auf Linux (WebKitGTK kann kein WebGPU) — beide randlos, eigene Titelleiste, gemeinsame React-App |
| Shell | Electron (eigenes randloses App-Fenster, kein Browser-Tab) |
| Frontend | React + TypeScript + Vite |
| 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 |
| 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 |
| Import | `dxf-parser`, `@mlightcad/libredwg-web` (DWG), eigene `.lin`/`.pat`-Parser |
| Materialien | `jszip` (ambientCG-Zip-Entpacken), `three.js` `TextureLoader` |
| Schnitt/HLR-Spike | `opencascade.js` (OCCT-WASM) — isoliert, noch nicht verdrahtet |
Der ursprünglich geplante OCCT/`opencascade.js`-HLR-Pfad für Schnitte ist
komplett entfernt (weder Dependency noch Code) — abgelöst durch die
analytische Rust-Schnitt-Pipeline (siehe Grundgedanke oben).
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.
## Entwicklung
```bash
npm install
npm run dev # Vite, http://localhost:5187
npm run tauri:dev # Tauri-Fenster (macOS — nativer Zielrahmen dort)
npm run electron # Electron-Fenster (Linux — WebGPU über Chromium statt WebKitGTK)
npx tsc -b # Typecheck
npm run build # tsc -b && vite build
npm test # vitest run
npm run dev # Vite, http://localhost:5187
npm run electron # Electron-Fenster (Dev-Server + randlose App-Shell)
npx tsc -b # Typecheck
npm run build # tsc -b && vite build
npm test # vitest run
```
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.
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).
## Aufbau
```
src/
model/ semantisches Modell (types, joins, parametricWalls, roomStamp, terrain)
geometry/ 2D-Kernel (offset/trim/fillet/intersect, ceiling/opening/roomArea/stair/roof/column)
model/ semantisches Modell + Ableitungen (types, parametricWalls, roomStamp, joins, terrain)
geometry/ 2D-Kernel (offset/trim/fillet/intersect, ceiling, opening, roomArea/-Boundary, stair)
commands/ Rhino-artiges Befehlssystem (engine, parser, registry, cmds/)
tools/ interaktive Zeichenwerkzeuge + Snapping + Transformationen
plan/ Grundriss-Ableitung (generatePlan) + PlanView (SVG) + glPlan (WebGL2) + Rust-Bindings
viewport/ Viewport3D (Three.js) + Wasm3DViewport (Rust/wgpu „Nordstern", Default)
export/ IFC4-, PDF-, DXF-, STL/OBJ-, CSV-Export aus derselben Plan-/Modell-Struktur
materials/ ambientCG-Live-Suche + gebündelte Starter-Bibliothek + PBR-Runtime
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)
panels/ dockbares Panel-System + die einzelnen Paletten
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
state/ Store + Slices (project/selection/view/layout)
ui/ Top-Bar, Statusleiste, Kontextmenü, Resource Manager, Command-Line
io/ Import/Export (DXF, DWG, .lin, .pat), Swisstopo/LV95, OSM-Kontext
i18n/ Wörterbücher de/en
src-tauri/ 5 eigenständige Rust-Crates (render2d/render3d/kernel2d/trucksolid/
dwgimport, je headless UND per wasm-pack baubar) + der Tauri-Host selbst
src-tauri/ Rust-Crates (render2d/render3d) — headless per wasm-pack zu WASM
gebaut (npm run build:engine), Tauri-Host selbst ist ausrangiert
```
## Konventionen
@@ -144,26 +141,17 @@ src-tauri/ 5 eigenständige Rust-Crates (render2d/render3d/kernel2d/trucksol
(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).
## Weiterlesen
Dieses README ist die einzige Doku im öffentlichen Repo — `STATUS.md`,
`ARCHITECTURE.md`, `ROADMAP.md`, `HANDOVER.md`, `PENDENZEN.md`,
`CONVENTIONS.md` und `docs/` sind bewusst per `.gitignore` ausgeschlossen
(interne Arbeitsnotizen/Backlog, kein öffentlicher Anspruch auf Vollständigkeit
oder Aktualität) und daher hier absichtlich **nicht** verlinkt.
## Lizenz
Copyright © 2026 Karim Gabriele Varano. Veröffentlicht unter der
**GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)** —
siehe [LICENSE](LICENSE). Dossier ist Teil der **openbureau**-Suite.
Die AGPL verlangt, dass auch bei Betrieb als Netzwerk-/Webdienst der (ggf.
geänderte) Quellcode für die Nutzer verfügbar gemacht wird. Drittkomponenten
behalten ihre jeweiligen Lizenzen (siehe „Über"-Dialog in der App).
- [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 …)
---
Die UI ist deutsch; Schweizer Spezifika (SIA-416-Flächen, Swisstopo-Geodaten)
sind ein bewusster Differenzierer.
Privates Projekt. Die UI ist deutsch; Schweizer Spezifika (SIA-416-Flächen,
Swisstopo-Geodaten) sind als Differenzierer eingeplant.
+382
View File
@@ -0,0 +1,382 @@
# Browser-BIM für Wohnbau — Architektur & Produkt-Roadmap
> Arbeitstitel: **cad** (Name später)
> Ausrichtung: **BIM-first** · Nische: **Wohnbau / Einfamilienhäuser**
> Stand: 2026-06-28
## 1. Produktvision
Ein **browserbasiertes BIM-Werkzeug** für Wohnbau, das zwei Dinge verbindet:
1. **Einfaches 3D-Gebäudemodell** — aus semantischen Bauteilen: Wände, Türen, Fenster, Treppen, Decken, Dächer, Räume.
2. **Schöne, normgerechte 2D-Pläne** — Grundrisse, Schnitte, Ansichten — automatisch aus dem Modell abgeleitet.
**Kernversprechen:** *Das schönste und einfachste Werkzeug, um ein Wohnhaus zu modellieren und daraus perfekte Pläne zu ziehen.* Nicht Revit nachbauen — radikaler Fokus auf Wohnbau + Plan-Qualität.
**Markt-Beleg:** Arcol, Snaptrude, TestFit zeigen, dass browserbasiertes BIM real ist und Nutzer schlanke, schöne Tools wollen statt der schwerfälligen Giganten (Revit/ArchiCAD).
---
## 2. Das mentale Modell — warum BIM anders ist als CAD
| | mechanisches CAD | **BIM (unser Weg)** |
|---|---|---|
| Bausteine | generische Volumenkörper | **semantische Bauteile** (Wand, Tür, Fenster…) |
| Beziehungen | keine | Tür *hostet* in Wand & schneidet Öffnung; Wände *verbinden* sich |
| Geschosse | — | **Stockwerke** als erste Klasse |
| 2D-Plan | Hidden-Line-Projektion | **symbolische Darstellung** (Schwenkbögen, Schraffuren, Lauflinien) |
| Standard | STEP | **IFC** |
---
## 2b. Kern-Prinzip: ein Modell, viele Darstellungen
Das **wichtigste Architektur-Prinzip**: Das semantische Modell ist die *eine
Wahrheit*; jede Ansicht (3D, Grundriss, Schnitt) ist eine **abgeleitete
Darstellung**. Im Spike steht das bereits. Daraus folgen direkt die Kern-Wünsche:
- **Modelldarstellungen / Detailgrade** — derselbe Tür/Fenster wird je nach
`detailLevel` (grob / mittel / fein) unterschiedlich gezeichnet (≙ Revit
„Detailgrad", ArchiCAD „Modelldarstellung"). Grob: Öffnung + Linie. Fein:
Rahmen, Blatt, Schwenkbogen, Anschlag.
- **Editierbare Stile** — Wandfarben, Linienstärken, Türlinien, Schraffuren als
**Stil-Schicht**, erst beim Rendern angewandt (nicht in die Geometrie
eingebacken). Pro Kategorie *und* pro Element überschreibbar.
- **Mehrschichtige Bauteile** — Wände/Decken mit **Schichtaufbau** (`layers[]`:
Material + Dicke + Priorität). 3D und Plan lesen dieselben Schichten.
- **In 2D *und* 3D zeichnen** — beide sind editierbare Sichten auf *ein* Modell;
Werkzeuge mutieren das Modell, alle Sichten re-derivieren reaktiv.
Schwierigkeit: Detailgrade/Stile/Schichten sind 🟢 gut machbar; 2D+3D-Editieren
🟡 mittel; **mehrschichtige Wand-Verschneidung** 🔴 der härteste Teil (Risiko #1).
---
## 2c. Arbeitsweise & Dokumentmodell (DOSSIER-Modell) ⭐
Referenz: **DOSSIER** (Rhino-Plugin des Nutzers, https://git.kgva.ch/karim/DOSSIER).
Dieses Projekt ist die **Standalone-Browser-Variante** davon. Zwei *unabhängige* Achsen:
- **Zeichnungsebenen** — die obersten Dokument-Abschnitte, zwei Arten:
- **Geschosse** (EG, 1OG …): `hoehe` (Geschosshöhe), `schnitthoehe` (Schnitthöhe),
`okff` (Niveau, akkumuliert), `visible`/`locked`.
- **Schnitte / Ansichten** (`type:"schnitt"`): Schnittlinie `linePts`, Richtung
`dirSign`, Höhenbereich, Tiefe.
- Ein **Geschoss** wird **im 3D-View ODER im Plan-View** betrachtet (Umschalter,
*keine* getrennten Daten). Plan-View = Clipping-Ebene auf `okff + schnitthoehe`.
- **Ebenen** — das **Grafik-Kategorien-Schema**, in *jedem* Geschoss vorhanden;
Baum-Knoten mit pro Ebene einstellbaren **Darstellungseinstellungen** (in den
„Ebeneneinstellungen…"): **Stift** = Typ/Linienstil + Farbe + Dicke (lw);
**Schraffur** = Typ + Skalierung + Rotation + **Stiftstärke der Schraffurlinien**.
Modell: `{code, name, visible, locked, lineStyleId|{type,color,lw}, hatchId|{type,scale,angle,lineWeight}, children}`.
Codes 1:1 wie DOSSIER:
`00 Raster · 01 Vermessung · 20 Wände (└21 Türen/Fenster) · 30 Decken · 31 Dächer
· 40 Treppen (└41 Treppen-2D) · 50 Tragwerk · 60 Räume · 80 Plangrafik …`
- **Elemente** (Wand, Decke, Treppe, Öffnung, Plangrafik, Text) liegen **auf den
Ebenen** und kennen ihr **Geschoss** *und* ihre **Ebene (Code)**.
- **2D-Zeichnen** (Linie, Polylinie, Rechteck, Kreis, Bogen, Text) findet auf der
Ebene `80 Plangrafik` (bzw. passender Kategorie) statt.
**Ansichtstypen = Kamera-Projektion + optionaler Schnitt** (vereinheitlicht):
| Typ | Projektion | Schnitt |
|---|---|---|
| **Grundriss** | Top-View (orthogonal) | horizontal auf `okff + schnitthöhe` |
| **Schnitt** | Front-View in eine Richtung (orthogonal) | vertikale Schnittebene (Geschnittenes + dahinter) |
| **Ansicht** | Front-View in eine Richtung (orthogonal) | kein Schnitt (Fassade außen) |
| **Perspektive** | 3D perspektivisch | — |
Sichtbarkeit pro Ansicht über Ein-/Ausschalten von Ebenen & Zeichnungsebenen.
Persistenz (DOSSIER): zwei getrennte JSON-Bäume `dossier_zeichnungsebenen` und
`dossier_ebenen`; Elemente tragen `geschoss`-id + Ebenen-`code`. Plan-View nutzt
eine Clipping-Ebene; weitere Konzepte: Overrides (regelbasiert), Ausschnitte
(View-Snapshots), Massstab (pro Viewport), Layer-Kombinationen, SIA-Räume.
## 2d. Resource Manager & Prioritäts-Verschneidung 🔴
Verwaltete Ressourcen-Bibliotheken wie in Vectorworks, jeweils mit eigenem Manager:
- **Line Manager** — Linienstile (Stärke, Farbe, Strichelung), wiederverwendbar.
- **Hatch Manager** — Schraffurstile (Muster, Maßstab, Winkel, Linienstil).
- **Component Manager** — Baustoffe mehrschichtiger Bauteile. Pro Component:
**Schraffur** (→ Hatch Manager), **3D-Textur**, Farbe und
**Verschneidungs-Priorität** (`joinPriority`).
Alles verweist per id auf diese Bibliotheken (Components nutzen Hatches, Hatches
nutzen Linienstile, 2D-Objekte & Ebenen-Defaults nutzen Linienstile/Schraffuren) —
zentral änderbar.
Regel: **höhere Priorität verschneidet sich zuerst / läuft durch.** Beispiel an
einer T-Ecke (Beton-Wand mit Innen- und Außenputz):
- **Beton** (höchste Prio) läuft in der Mitte **durch** den Stoß.
- **Putze** (niedrige Prio) **verbinden** sich jeweils auf ihrer Seite mit dem
angrenzenden Putz, gehen aber **nirgends durch** den Beton.
Das ist die anspruchsvollste Verschneidungs-Logik (Revit „Layer Priority /
Wrapping", Vectorworks „Component-Verschneidung"). Wir bauen sie stufenweise auf
der bereits funktionierenden L-Ecken-Gehrung auf.
---
## 3. Zwei Wege zum 2D-Plan (zentrale Architektur-Erkenntnis)
Architektur-Pläne entstehen auf **zwei verschiedenen Wegen** — das prägt die ganze Engine:
**A) Grundriss = aus dem semantischen 2D-Footprint + Symbolik**
Ein Grundriss ist ein horizontaler Schnitt auf ~1 m. Statt ein 3D-Mesh zu zerschneiden, generieren wir ihn **direkt aus den Parametern**: Wand-Achsen + Dicken → Linien; Öffnungen → Lücken + Tür-/Fenstersymbol; Treppe → Lauflinie. Schnell, exakt, sauber, vektorbasiert.
**B) Schnitt & Ansicht = aus 3D-Projektion (HLR)**
Vertikale Schnitte und Ansichten brauchen echte 3D-Projektion mit verdeckten Kanten (Hidden Line Removal) durch das zusammengebaute Gebäude.
→ Wir brauchen **beides**: einen sauberen 2D-Symbol-Renderer *und* einen Projektions-Pfad.
---
## 4. Tech-Stack
| Schicht | Wahl | Begründung |
|---|---|---|
| BIM-Datenmodell | **eigenes parametrisches Gebäudemodell** (TS) | web-ifc ist stark beim *Lesen/Anzeigen* von IFC, schwächer beim *Authoring/Editieren*. Für ein Editier-Tool brauchen wir ein eigenes, editierbares Modell. |
| IFC-Interop | **web-ifc** (ThatOpen, WASM) | Import/Export nach IFC — Brücke zu Revit/ArchiCAD. |
| 3D-Rendering | **Three.js** | Standard; ThatOpen baut darauf auf, also kompatibel. |
| Geometrie-Booleans | **OpenCascade.js** (Öffnungen) *oder* Manifold | Tür/Fenster schneidet Loch in Wand. OCC = exakt (B-Rep), Manifold = schnell (Mesh). Entscheidung in Phase 0. |
| Projektion/HLR | **OpenCascade.js** (`HLRBRep`) | Saubere Linien für Schnitte/Ansichten. |
| 2D-Pläne | **SVG** + eigener Symbol-Renderer | Vektor, druckbar, exportierbar (DXF/PDF). |
| Frontend | **React + TypeScript + Vite** | Schnelles HMR, großes Ökosystem. |
| Worker-Bridge | **Comlink** | Schwere Geometrie im Web Worker, UI bleibt flüssig. |
| State | **Zustand** o.ä. | Passt zum komplexen Dokumentmodell. |
| Persistenz (später) | Postgres + Object Storage | Versionierbare Projekte. |
---
## 5. Datenmodell (grob)
Vectorworks-orientiert: **Design Layers** (Modell-Eingabe) und **Drawing Layers**
(abgeleitete Ausgabe). Bauteile beziehen ihren Aufbau aus **Components** (verwaltet
im Component Manager).
```
Project
├─ Resources // verwaltete Bibliotheken (Vectorworks-Stil)
│ ├─ LineStyles[] // Line Manager: { id, name, weight, color, dash }
│ ├─ Hatches[] // Hatch Manager: { id, name, pattern, scale, angle, lineStyleId }
│ └─ Components[] // Component Manager: wiederverwendbare Baustoffe
│ Component { id, name, hatchId, texture3d, color, joinPriority }
│ // joinPriority: höher = verschneidet sich zuerst (geht durch)
├─ Grids (Achsraster) (optional)
├─ Types // mehrschichtige Aufbauten
│ ├─ WallType { id, name, layers: Layer[] }
│ └─ SlabType { id, name, layers: Layer[] }
│ Layer = { componentId, thickness } // Priorität liegt am Component
│
├─ DesignLayers ("Ebenen") // hier wird modelliert & 2D gezeichnet
│ DesignLayer { id, name, elevation(z), height(Δz),
│ defaultLineStyle, defaultHatch,
│ elements: Wall | Door | Window | Slab | Stair | Roof | Space ,
│ draw2d: Line | Polyline | Rect | Circle | Arc | Text }
│ Wall { axis, wallTypeId, height }
│ Door { hostWall, position, width, height, swing, symbolId }
│ Window { hostWall, position, width, height, sill }
│ Slab { boundary, slabTypeId } · Space { boundary, name } // Fläche auto
│
└─ DrawingLayers ("Zeichnungsebenen") // abgeleitete Ausgabe
DrawingLayer { id, name,
type: plan | section | elevation | drawing,
cutHeight(z), // bei plan: Schnitthöhe
sectionLine, // bei section
sourceDesignLayers[],
detailLevel: coarse|medium|fine,
scale, styleOverrides, dims[], labels[], annotations[] }
```
3D *und* jede Drawing Layer werden **aus den Design Layers abgeleitet**. `cutHeight`,
`detailLevel`, `Styles` und `Component`-Eigenschaften steuern, *wie* abgeleitet wird.
---
## 6. Die harten Risiken (früh angehen)
1. **Wand-Verbindungen / Cleanup** ⚠️ — Wo Wände aufeinandertreffen, müssen sie sauber verschneiden. **L-Ecken-Gehrung: ✅ erledigt.** Offen & berüchtigt schwer: **Prioritäts-basierte T-/X-Stöße bei mehrschichtigen Wänden** (Beton durch, Putz verbindet seitlich, geht nicht durch — siehe 2d). → stufenweise auf der Gehrung aufbauen.
2. **Gehostete Öffnungen** — Tür/Fenster muss synchron mit der Wand bleiben (verschieben, schneiden). → Saubere Host-Beziehung im Modell.
3. **Symbolischer Plan-Renderer** — normgerechte Darstellung (Schwenkbögen, Schraffuren der geschnittenen Bauteile, Lauflinien). → Eigenes Regelwerk; früh prototypen.
4. **Schnitt-/Ansichts-Projektion (HLR)** durch ganzes Gebäude — Performance. → Worker + Caching.
5. **IFC-Treue** — verlustarmer Round-Trip. → Früh mit echten IFC-Dateien testen.
6. **Geschoss-übergreifende Elemente** (Treppen, Lufträume).
---
## 7. Phasen-Roadmap
### Phase 0 — Spike: das größte Risiko zuerst
**Ziel:** Beweisen, dass der symbolische Plan-Pfad im Browser funktioniert.
- Eine Wand zeichnen, eine Tür platzieren → Öffnung wird geschnitten (3D).
- Daraus **Grundriss als SVG** generieren: Wand-Schnittlinien + Tür-**Schwenkbogen**.
- ✅ *Erfolg:* sauberer, schöner Grundriss-Ausschnitt aus einem semantischen Modell.
### Phase 1 — MVP: durchgehende Wohnbau-Scheibe
- **Dokumentmodell (Vectorworks-Stil):** Design Layers ("Ebenen") + Drawing Layers
("Zeichnungsebenen", Typ plan/section/elevation, mit `cutHeight`).
- **Resource Manager:** Line Manager, Hatch Manager, Component Manager (mit
`joinPriority`, Schraffur, 3D-Textur).
- **Wände** mehrschichtig (✅) mit Eck-Gehrung (✅); **Prioritäts-T-Stöße** (Risiko #1).
- **Türen & Fenster** gehostet in Wänden (Risiko #2).
- **2D-Zeichnen:** Linie, Polylinie, Rechteck, Kreis, Bogen mit Stilen.
- **Decken/Böden** (Slabs); 3D-Viewport + **live Grundriss**, Basis-Bemaßung.
- → aus DOSSIER (§11): Wand-Referenzlage (mid/left/right), Öffnungs-Detailgrad mit Dokument-Override, Decken-Aussparungen, Element-Übersicht (BIM-Tree).
- ✅ Ein einfaches Haus modellieren → saubere Pläne pro Geschoss.
### Phase 2 — Vollständiger Bauteil-Satz Wohnbau
- **Treppen** (mit Lauflinie im Plan), **Dächer**, Geländer.
- **Räume/Spaces** mit automatischer Flächenberechnung & Raumstempel.
- Stützen/Unterzüge (falls nötig).
- Materialien & einfache Visualisierung.
- → aus DOSSIER (§11): Treppen-Typen (gerade/L/Wendel) + geschoss­übergreifend, Dach-Typen (Pult/Sattel/Walm/Mansarde), Stützen-Profile, **SIA-416-Räume** + CSV, Raumstempel-Builder, Stil-Kataloge (Wände/Öffnungen).
### Phase 3 — Plan-/Dokumentations-Modul ⭐ (Differenzierung)
**Hier gewinnen wir. Maximale Politur.**
- **Grundrisse, Schnitte (HLR, Risiko #4), Ansichten.**
- **Automatische Bemaßung** (Außenketten, Achsen, Öffnungen) + manuelle.
- Schraffuren geschnittener Bauteile, Raumstempel, Beschriftungen, Symbole.
- **Plansätze/Sheets** mit Titelblock, Maßstäben, Layout.
- Schöne Typografie & Linienführung — genau das, was die Großen vermasseln.
- → aus DOSSIER (§11): **Massstab pro Viewport** (Auto-DPI, Plotweight-/Schraffur-Skalierung), Section-Style (3D-Schnittflächen), Ausschnitte (View-Snapshots) + Layer-Kombinationen, Kamera-Presets (Kardinal/Iso, **Norden-Rotation**), Detail-Bindung an Ausschnitt, Multi-Page-PDF @DPI, regelbasierte Overrides, Rich-Text-Annotationen.
### Phase 4 — Interop (Import/Export)
- **Import:** **DWG/DXF** (2D-Pläne/Bestand), **IFC** (BIM-Bestand), **STL/OBJ**
(Mesh-Referenzmodelle), **XYZ** (Punktwolken aus Vermessung).
- **Export:** **IFC** (Brücke zu Revit/ArchiCAD), **DWG/DXF**, **glTF/OBJ**.
- Round-Trip-Tests mit echten Dateien (Risiko #5).
- → aus DOSSIER (§11): **Swisstopo-Import** (swissBUILDINGS3D / swissALTI3D / SWISSIMAGE, LV95↔WGS84) ⭐ CH, OSM-Overpass-Kontext, Terrain-Mesh-Generator.
### Phase 5 — Persistenz & Konten
- Accounts, Projekte speichern/laden, Versionierung, Auto-Save.
- → aus DOSSIER (§11): Projekt-Persistierung auf Browser-Storage migrieren (IndexedDB statt doc.Strings); Presets/Favoriten cross-projekt (LocalStorage, Export/Import).
### Phase 6 — Export & Kollaboration
- Export: **PDF**, **DXF** (Pläne); **IFC**, **glTF** (3D).
- Teilen per Link, Kommentare, später Echtzeit-Co-Editing.
### Phase 7 — Produktisierung
- Performance-Härtung (große Modelle), Onboarding, Pricing, PWA/Offline.
---
## 8. Offene Fragen
- Booleans: OpenCascade (exakt) vs. Manifold (schnell) — Entscheidung in Phase 0.
- Welche Normen für Plandarstellung (SIA / DIN / …)? → beeinflusst Symbolik. *(Hinweis: User ist in der Schweiz → SIA prüfen.)*
- Wie viel Statik/Bauphysik (gar nicht / später)?
- Pricing-Modell (Freemium, pro Seat?).
---
## 9. Nächster konkreter Schritt
**Phase 0 starten:** Projekt scaffolden + Spike bauen — Wand + Tür mit geschnittener Öffnung → schöner Grundriss-Ausschnitt als SVG (mit Schwenkbogen). Das entschärft Risiko #1–#3 (Modell, Hosting, Symbolik) auf einmal.
---
## 10. Leitentscheidungen & UI-Architektur
### 10a. Leitentscheidungen aus der Recherche (Details in `docs/`, Index `docs/README.md`)
1. **Pure-Ableitungs-Architektur** als Fundament — ein semantisches Modell, alle Sichten abgeleitet, Darstellung erst beim Rendern (Store + Undo früh).
2. **OCCT/replicad im Web Worker** früh als Spike — kritischer Pfad für Schnitt/Ansicht (HLR), exakte Wand-Booleans und IFC. WASM-Größe + HLR-Kosten (pro Ansicht cachen) validieren.
3. **Component-getriebene Prioritäts-Verschneidung** (`joinPriority` am Component): höchstes gemeinsames Material läuft durch, Rest mitert. 2D-Plan analytisch, exakte 3D-Booleans im Worker.
4. **SVG/Paper-Space-Maßstabsmodell** — Strichstärke/Text/Hatch in mm, `dpi=96·devicePixelRatio`, ein Serializer für Bildschirm + PDF + DXF.
5. **CH-Spezifika als Differenzierer** ohne Backend — SIA-416 (reine Logik) + serverloser Swisstopo-Flow (CORS-offen, Parzelle/EGRID, Norden-Rotation, Origin-Shift).
### 10b. Panel-System (dockbar, Tabs, erweiterbar) — NEU
- **Docks links & rechts**; Panels in **Tab-Strips** gruppierbar (z. B. Zeichnungsebenen & Ebenen als Tabs eines Docks).
- **Panel-Registry** → eigene Panels und spätere **Plugins** registrieren sich und erscheinen als Panel.
- **Verschiebbar & floatend (am Tab gegriffen):** Ein Panel wird **am Tab selbst** (im Tab-Strip) gezogen → innerhalb des Docks **umsortieren**, ins andere Dock ziehen, oder aus dem Dock lösen. Andocken am **linken/rechten Rand** (Andock-Zonen beim Ziehen hervorheben); wird nicht angedockt, **schwebt** das Panel als freies (verschieb- und größenveränderbares) **Floating-Fenster** über dem Arbeitsbereich. Float-Position/Größe + Dock-Zustand werden gespeichert.
- **Fenster-Layouts speicherbar** (localStorage, benannte Layouts; Standard-Layout als Default).
- Der **Ressourcen-Manager** wird ebenfalls ein Panel (rechtes Dock).
- Pro Layer-Panel oben ein **Anzeige-Modus-Dropdown**: *nur aktive · alle anzeigen · aktive + andere grau* (DOSSIER/Vectorworks „Layer Options") — wirkt auf Plan & 3D.
### 10c. Plan-Navigation
- Grundriss/Schnitt/Ansicht: **Pan** (ziehen), **Zoom** (Mausrad zum Cursor), **Einpassen** — analog zum 3D-Viewport. SVG-`viewBox`-Transform.
### 10d. Backend & Kollaboration (Details: `docs/backend.md`)
- **Jetzt:** client-only (IndexedDB + Datei-Export/Import), kein Server. Modell JSON-serialisierbar + Edits als Operationen → **CRDT-fähig** halten.
- **Phase 5:** **Supabase self-hosted** (Docker Compose: Postgres + Auth + Storage) für Konten/Projekte/Dateien.
- **Phase 6:** **Yjs + Hocuspocus** (CRDT-Sync-Container, persistiert nach Postgres) für Echtzeit-Kollaboration. Alles self-hosted.
- Empfehlung: Stack **noch nicht** aufsetzen (würde den Modellierer ausbremsen); Weiche ist gestellt, Einführung additiv.
### 10e. Top-Bar & Footer/Status-Leiste (Vectorworks-Stil) — NEU
- **Top-Bar (Oberleiste, wie DOSSIER `toolbar.py`/`ToolbarApp.jsx`):** Ansichts-Umschalter (Grundriss/Perspektive/Schnitt/Ansicht), Render-/Darstellungsmodus, aktives Geschoss + aktive Ebene, Snapping-Schalter, Massstab, Werkzeug-Kontext, Einstellungen/Ressourcen.
- **Footer/Status-Leiste (wie Vectorworks unten):** Cursor-Koordinaten **X/Y/Z**, Einheit, aktueller **Massstab** (1:N) + **Zoom %**, aktives Geschoss/Ebene, **Snap-Status**, kurzer Werkzeug-Hinweis links.
- Beide an das Panel-/Dock-Layout angedockt; Inhalte aus dem Modell abgeleitet.
### 10f. Maus-Interaktion & Kontextmenü — NEU
- **Maus-Schema:** **Mitte = navigieren** (Plan: Pan · 3D: Orbit, `Shift`+Mitte: Pan) · **Links = Auswahl** (Einzelklick + **Markierrahmen/Aufziehrahmen** für Mehrfachauswahl im 2D) · **Rechts = Kontextmenü** · **Rad = Zoom** (zum Cursor).
- Marquee: Aufziehen von links→rechts = nur vollständig umschlossene Elemente; rechts→links = auch berührte (wie CAD-üblich).
- **Eigenes Kontextmenü-System** (gestylt, dunkel, wiederverwendbar) — kein Browser-Menü.
- **Ebenen-Kontextmenü 1:1 wie DOSSIER** (Einträge aus `layers_panel.py`/`DrawingLevelsApp.jsx` übernehmen) — auch auf Zeichnungsebenen.
- Kontextmenü generisch, damit Plan-Elemente, Panels & spätere Plugins eigene Einträge registrieren können.
---
## 11. Aus DOSSIER übernehmen — Backlog
Konkret im DOSSIER-Rhino-Plugin umgesetzte Features, die sich für den Standalone-Port lohnen. Bereits abgedeckt (Layer-Modell, mehrschichtige Wände, Prioritäts-Stöße, Component-/Line-/Hatch-Manager, Ansichtstypen, Detailgrade) ist hier **nicht** erneut gelistet — nur das Zusätzliche. Aufwand: S/M/L. Phase verweist auf §7.
> **⭐ CH-Schätze (Schweiz-spezifisch, kaum woanders verfügbar):**
> - **Swisstopo-Geodaten** — swissBUILDINGS3D (3D-Bestand), swissALTI3D (präzises Höhenmodell), SWISSIMAGE (10-cm-Orthofoto), offene STAC-APIs ohne Auth, inkl. **LV95↔WGS84**-Transformation. Echter Standort-Kontext per Knopfdruck statt manuellem CAD-Import.
> - **SIA-416-Flächen** — Raum-Klassifikation (HNF/NNF/VF/FF/GF/AGF) mit automatischer Bilanz + CSV/Excel-Export. Pflicht für CH-Energie-/Flächennachweise.
> - **Norden-Rotation** bei Kamera-Presets — Georeferenzierung passend zu Swisstopo/swissBUILDINGS.
### Bauteile
| Feature | Nutzen | Aufwand | Phase |
|---|---|---|---|
| Wand-Referenzlage (mid/left/right) | Achse intuitiv auf Aussenkante/Mitte legen; hilft beim Import fremder Dateien | S | 1 |
| Öffnungs-Detailgrad + Dokument-Override (`aktive_darstellung`) | LoD je Massstab (1:500 Rechteck → 1:50 Glas/Sims) global umschaltbar; kritisch für Mixed-Scale | M | 2 |
| Fenster/Tür mit Rahmen, Brüstung, Sims, Glas, Flügelzahl | Öffnung als vollwertiges Bauteil statt nur Loch; Render-Realismus | M | 2 |
| Tür-Schwenkbogen (Öffnungswinkel + Anschlagseite) | Öffnungsbahnen für Möblierung/Kollision; Standard-Plansymbol | M | 2 |
| Decken-Aussparungen (Treppenauge, Schächte, Kamin) | Konstruktiv echte Deckenöffnungen, nicht nur sichtbar | M | 1–2 |
| Decken UK/OK-Override | Abhängungen, schräge Brüstungen, abweichende Raumhöhen | S | 1 |
| Treppen-Typen gerade/L/Wendel + Stufen/Lauflinie/Podest | Volle Vertikalerschliessung, volumetrisch korrekt, Plan-Symbole | M | 2 |
| Treppe geschoss­übergreifend (`geschoss_end`, Höhen-Override) | Atrien, Rampen, Mehr-Geschoss-Läufe (Risiko #6) | S | 2 |
| Treppen-2D-Symbol mit Auf-/Abpfeil + Schnitt | Normgerechtes Plansymbol (Richtung, Stufenzahl, Lauflinie) | M | 2 |
| Dach-Typen Pult/Sattel/Walm/Mansarde + Neigung(en) | 3D-Volumen mit Gefälle, Kubatur, Material; Mansarde später (L) | M–L | 2–3 |
| Stützen-Profile (Quadrat/Rechteck/Rund/I/Rohr) + Drehung | Beton- und Stahltragwerk mit echtem Querschnitt | M | 2 |
| Träger achs-basiert, hängt unter Decken-OK | Unterzug folgt Deckenoberkante, weniger Fehler bei Updates | S | 2 |
| **SIA-416-Räume** (HNF/NNF/VF/FF/GF/AGF) + Bilanz-CSV ⭐ | CH-Flächennachweis, Excel-Export | M | 2 |
| Raum-Stempel-Builder (Drag-&-Drop-Felder) + Fläche-Rundung + Personen | Projekt-eigene Stempel-Layouts ohne Code; lesbare Listen; Brandschutz | M | 2–3 |
| Element-Übersicht (BIM-Tree Geschoss→Kind→Element, Suche, Zoom) | Inhaltsverzeichnis bei 100+ Elementen; Shift-Klick = Zoom | S | 1 |
| Stil-Kataloge Wände & Öffnungen (Presets) | Standard-Typen 1-Klick; globaler Stilwechsel | M | 2 |
| Grip-Editing (Wand-Endpunkte, Schnitt-Symbole im Plan) | Direktes Ziehen statt Dialog; 2D/3D-Sync; wichtig im Browser | L | 3–4 |
### Darstellung / Ressourcen
| Feature | Nutzen | Aufwand | Phase |
|---|---|---|---|
| Regelbasierte Overrides (Layer-/Tag-/Name-Regel, Priorität, Templates) | Automatische Farb-/Strich-/Linientyp-Anpassung; wiederverwendbar (vertieft §2c „Overrides") | M | 3 |
| Section-Style für 3D-Schnittflächen (Schraffur + Schnittkante/Silhouette) | 3D-Schnitt-Rendering im Viewport, nicht nur 2D-Plan | M | 4 |
| Massstabs-abhängige Linientyp-/Plotweight-Skalierung | Linientypen & Strichstärken bei 1:N korrekt sichtbar; PDF-Treue | M | 3–4 |
| Material-Bibliothek mit PBR (Rauheit/Reflexion/Transparenz) + Templates | Vertieft Component-Manager um Renderqualität; Seeds Beton/Holz/Dämmung | M | 3 |
| Rich-Text-Annotationen (Bold/Italic/Hoch-/Tiefstellung, Maskierung, Rahmen) | Bemaßungs-Indizes, formatierte Beschriftungen auf Canvas | M | 3 |
| LoD-bewusste Stil-UI (zeigt nur passende Controls je Geometrie-Typ) | Weniger kognitive Last (keine Füll-Optionen bei 3D-Auswahl) | S | 1 |
### Pläne / Output
| Feature | Nutzen | Aufwand | Phase |
|---|---|---|---|
| **Massstab pro Viewport** mit Auto-DPI + Schraffur-/Strich-Skalierung | Exakte Masse & lesbare Strichstärken ohne manuelle Kalibrierung | M | 3 |
| Ausschnitte / View-Snapshots (Kamera + Darstellung + Massstab) | Navigation über 50+ Ansichten; ersetzt Ordner-Wildwuchs (vertieft §2c) | M | 3 |
| Layer-Kombinationen als Presets (live oder eingefroren) | Bauphasen/Varianten/MEP per Klick statt manuellem Toggling (vertieft §2c) | S | 3 |
| Kamera-Presets (Kardinal N/O/S/W, Iso-Oktanten, **Norden-Rotation** ⭐) | Schnelle Ansichtswechsel; Georeferenzierung für Swisstopo | S | 3 |
| Detail↔Ausschnitt-Bindung + „Alle aktualisieren" | Titelblock/Detail synchron umbenennen; 1-Klick-Sync aller Schnitte | M | 3 |
| Multi-Page-PDF-Export @DPI (Vektor) | Druckfertige Plansätze — Kern-Output (ergänzt Phase-3-Sheets) | M | 3 |
| 9-Punkt-Bemaßung/Objekt-Info (lesen + verschieben/skalieren/rotieren) | Direktes Dimensionieren ohne Properties-Panel | M | 3 |
### Kontext / Daten
| Feature | Nutzen | Aufwand | Phase |
|---|---|---|---|
| **Swisstopo-Import** (swissBUILDINGS3D/ALTI3D/SWISSIMAGE) ⭐ CH | Authentischer Standort-Kontext, offene APIs, kein Auth | M | 4 |
| LV95↔WGS84-Transformation ⭐ CH | Karten-Anzeige + präzise CH-Koordinaten; Formeln direkt portierbar | S | 4 |
| OSM-Overpass-Import (Strassen/Gebäude/Wasser/Grün, 7 Kategorien) | Weltweiter, kostenloser Kontext; ergänzt Swisstopo | M | 4 |
| Terrain-Mesh-Generator (Mesh/TIN/NURBS-Patch/Höhenlinien, Volumen für Schnitt) | Geländemodell aus Höhendaten; Section-Cut-Füllung; portierbar zu Three.js | L | 4 |
| Auto-Zoom auf Import + Nullpunkt-Verschiebung (LV95→0/0/0) | Modellierungsgenauigkeit trotz Millionen-Koordinaten; UX-Standard | S | 4 |
| Projekt-Persistierung browser-nativ (IndexedDB statt doc.Strings) | Projekt kapselt seine Einstellungen lokal | M | 5 |
| Cross-Projekt-Presets (LocalStorage-Favoriten, Export/Import, Team-Sharing) | Einmal speichern, überall nutzen | S | 5 |
+175
View File
@@ -0,0 +1,175 @@
# HAUPTINSTANZ-BRIEFING: Tauri + wgpu (Korrigiert)
**Stand:** 2026-07-01 — **KORREKTUR** (vorherige Dokumente waren unvollständig)
---
## Die Entscheidung (FINAL)
**Browser-CAD (alt) → Desktop Tauri-App mit Rust-Backend + wgpu-Rendering (neu)**
| Aspekt | Alt | Neu |
|---|---|---|
| **App-Form** | Browser-Tab | Desktop Tauri-Window |
| **Frontend** | React/Vite (TS) | React/Vite (TS) — UNVERÄNDERT |
| **2D-Rendering** | SVG-Plan | SVG-Plan — UNVERÄNDERT |
| **3D-Rendering** | three.js/WebGL | **wgpu** (Vulkan/Metal/DX12) |
| **Rechenintensive Ops** | TS in Browser | **Rust im Backend** |
| **Betriebssystem** | cross-platform browser | Windows/macOS/Linux Desktop App |
---
## Warum dieser Umstieg?
**Problem mit three.js/WebGL:**
- komplexe Möbel = 100k+ Polygone
- mehrere parallele Ops (kernel2d, bool-Ops, DXF-Parser, SIA-Raumerkennung)
- WebGL hat harte Limits (GPU VRAM, draw calls, single-threaded)
- → Laggy, nicht skalierbar für Professional CAD
**Lösung: Tauri + wgpu + Rust**
- **wgpu** = low-level GPU API (direkt zu Vulkan/Metal/DX12, nicht WebGL)
- **Rust** = rechenintensive Ops parallelisiert + native performance
- **Desktop** = native App, nicht Browser (bessere Kontrolle, bessere Perf)
- **React-Frontend bleibt** = UI/Sketching/Panels unverändert (wgpu nur für 3D-Display)
---
## Der neue Stack (FINAL)
```
┌─────────────────────────────────────────────────────┐
│ Desktop Tauri-Window │
├─────────────────────────────────────────────────────┤
│ │
│ React/Vite (UI-Shell, State, Panels) │
│ ├─ SVG 2D-Plan (Grundriss) │
│ └─ wgpu 3D-Viewport (3D-Ansicht + Rendering) │
│ │
│ ↓ invoke (IPC) ↓ │
│ │
│ Rust-Backend (src-tauri/) │
│ ├─ computeJoins() — Wand-Eckverbindungen │
│ ├─ kernel2d() — Offset/Trim/Extend/… │
│ ├─ parseShapeFromDwg() — Geometrie-Import │
│ ├─ detectRooms() — SIA-Raumerkennung │
│ └─ booleanOps() — Union/Differenz/Schnitt │
│ │
└─────────────────────────────────────────────────────┘
```
**Nicht verändert:**
- App.tsx, state/, commands/, panels/, model/types.ts
- UI-Logik, Befehlssystem, Sketching-Tools
**Neu/geändert:**
- `src-tauri/` — Rust-Crate für Backend
- `src/compute/index.ts` — Boundary (invoke + TS-Fallback)
- Rendering-Engine: **three.js → wgpu**
- Build: `npm run tauri:build` statt `npm run build`
---
## Die Aufgabe (vier Agents)
**Siehe: `docs/design/tauri-migration-plan.md`**
Vier parallele Agents:
1. **Agent: Tauri-Shell Setup** → `src-tauri/` + Cargo.toml + main.rs (Tauri window registrieren)
2. **Agent: Compute-Boundary (TS)** → `src/compute/index.ts` (invoke-Wrapper + TS-Fallbacks)
3. **Agent: Rust compute_joins Impl** → `src-tauri/src/geometry.rs` (erste Op, Proof-of-Concept)
4. **Agent: Vite/Package-Integration** → vite.config.ts + package.json (Tauri-Plugins, scripts)
**Deliverable:** lauffähige Desktop-App, wand-Edit triggert Rust-Op, Output identisch TS-Version.
---
## Dev-Workflow (post-Tauri)
```bash
# Dev: Terminal 1 (Vite)
npm run dev # localhost:5173
# Dev: Terminal 2 (Tauri)
npm run tauri:dev # öffnet Desktop-Window, zeigt auf localhost:5173
# Build
npm run tauri:build # → Windows .exe / macOS .app / Linux .deb
```
---
## Rendering: three.js → wgpu (Wichtig!)
**3D-Viewport wird NEU implementiert in wgpu:**
- Nicht: "drei.js mit Rust-Fallback"
- **Ja:** wgpu als native GPU-Renderer (skaliert auf 100k+ Polygone)
**2D-Plan bleibt SVG** (keine Änderung).
**Timeline für wgpu-Impl:**
- Milestone 1 (jetzt): Tauri-Shell + Rust-Compute-Ops
- Milestone 2 (nächst): wgpu 3D-Viewport-Impl (ersetzt drei.js/WebGL)
- Milestone 3: Live clipping/sectioning in wgpu
---
## Migration-Reihenfolge
**Phase 1 (Tauri-Shell + Proof-of-Concept):**
- [ ] `computeJoins` (Wand-Ecken) → Rust
**Phase 2 (Rendering-Umbau):**
- [ ] wgpu Viewport-Impl (ersetzt drei.js)
- [ ] Instancing/LOD für Möbel-Geometrie
**Phase 3 (weitere Ops):**
- [ ] `kernel2d` (Offset/Trim) → Rust
- [ ] DXF/DWG-Parser → Rust
- [ ] detectRooms → Rust
- [ ] booleanOps → Rust
---
## Was sich NICHT ändert
- `src/App.tsx`, `src/state/`, `src/commands/`
- UI-Panels, Zeichenwerkzeuge, Befehlssystem
- Semantisches Modell (types.ts)
- i18n, Styling
## Was ändert sich
- **Engine:** three.js/WebGL → wgpu
- **Backend:** TS-Only → Tauri + Rust
- **Distribution:** Browser → Desktop App
- **Build-Prozess:** `npm run build` → `npm run tauri:build`
---
## Fehlerquellen (zur Klarheit)
❌ **FALSCH:** "Wir bleiben bei three.js"
✅ **RICHTIG:** "three.js → wgpu, Rust-Compute + Desktop Tauri"
❌ **FALSCH:** "Tauri + three.js als Frontend-Engine"
✅ **RICHTIG:** "Tauri-Shell + React UI + wgpu 3D-Rendering + Rust-Compute"
---
## Nächste Schritte (für Hauptinstanz)
1. **Lesen:** `docs/design/tauri-migration-plan.md` (technisch, konkret)
2. **Spawn:** vier Agents (parallel, unabhängig)
3. **Verifizieren:** `npm run tauri:dev` → App läuft, erste Op in Rust funktioniert
4. **Aktualisieren:** HANDOVER.md mit Milestone-1-Status
---
## Fragen?
- **"Warum Desktop statt Browser?"** → Skalierbarkeit (native GPU + Rust-Compute)
- **"Warum wgpu statt drei.js?"** → wgpu skaliert auf 100k+ Polygone, three.js/WebGL hat Limits
- **"Bleibt React?"** → Ja, React/Vite UI bleibt, nur 3D-Engine wechselt
- **"Wann ist wgpu-Impl fertig?"** → Milestone 2 (nach Tauri-Shell)
+160
View File
@@ -0,0 +1,160 @@
# Dokumentation — Standalone Browser-BIM (cad)
> Stand: 2026-06-29 · Übergeordnet: [ROADMAP.md](../ROADMAP.md) (Vision & Phasen) ·
> [CONVENTIONS.md](../CONVENTIONS.md) (Konventionen) · [ARCHITECTURE.md](../ARCHITECTURE.md).
Dieses Verzeichnis bündelt die Recherche- und Design-Dokumente für `cad`, die
eigenständige Browser-Variante des DOSSIER-Rhino-Plugins (React + TypeScript +
Three.js + SVG, alles client-side). **Leitprinzip aller Dokumente:** ein
semantisches Modell ist die einzige Wahrheit; jede Sicht (3D, Grundriss, Schnitt)
wird **abgeleitet**, Darstellung erst beim Rendern angewandt. Bezeichner im Code
englisch (Vectorworks-Terminologie), Prosa deutsch, Einheiten intern in Metern.
Die Dokumente sind in vier Gruppen geordnet: **Tech** (Bibliotheken/Kernel),
**Architektur/Design** (Aufbau & Bauteile), **UX** (Oberfläche & Interaktion),
**Swisstopo/SIA** (CH-Geodaten & Flächenstandards).
---
## Tech — Technologie- & Bibliotheksauswahl
### [research/tech-selection.md](research/tech-selection.md)
Evaluiert den kompletten Client-Stack für ein server­loses BIM-Werkzeug und
empfiehlt **`replicad`** (idiomatische TS-Schicht über `opencascade.js`/OCCT, MIT)
als primären B-Rep-Kernel im Web Worker, ergänzt durch **`Manifold`** (Apache-2.0)
für schnelle, robuste Mesh-Booleans auf Importgeometrie — denn nur ein echter
B-Rep-Kernel liefert exakte 2D-Ableitungen, und genau das löst replicads
`drawProjection` (OCC-HLR, `{visible, hidden}`-Kanten direkt im Browser). Weitere
Wahl: Import via **web-ifc + Fragments** (IFC), `dxf-parser` (DXF) und
`libredwg-web` (DWG, aber **GPL-3.0 → vorab klären/kapseln**); Vektor-Export über
**`svg2pdf.js` + `jsPDF`** (PDF) und **`@tarikjabiri/dxf`** (echte Hatch-Entities);
Schraffuren als SVG-`<pattern>` mit `userSpaceOnUse` (maßstabskorrekt); Rendering
über **`three/webgpu`** mit automatischem WebGL2-Fallback. Top-Risiken: DWG-Lizenz,
OCCT-WASM-Größe, HLR-Kosten (pro Ansicht cachen), WebGPU vor Migration benchmarken.
---
## Architektur/Design — Aufbau, Datenmodell & Bauteile
### [../ARCHITECTURE.md](../ARCHITECTURE.md)
Die übergreifende Standalone-Architektur und die systematische Übersetzung jedes
DOSSIER-Konzepts in ein Browser-Äquivalent (30-zeilige **Rhino→Browser-Mapping-
Tabelle**). Kern: das semantische `Project` (JSON) als einzige Wahrheit mit pure
`derive()` zu Scene3D/Plan/Section; ein **Zwei-Achsen-Datenmodell**
(`drawingLevels` × `layers`) plus `Resources`/`WallType`/`Element`/`Sheet`; ein
**Zustand-Store** ersetzt DOSSIERs `sc.sticky`-Bus, **`.cad.json`** (File System
Access API) + IndexedDB-Autosave ersetzen `doc.Strings`, und ein **Immer-Patch-
Undo/Redo** eliminiert die Cache-Stale-Bugs strukturell. Ziel-Repo-Struktur mit
**kleinen Bauteil-Modulen** statt des 7244-LOC-`elemente.py`-Monolithen; Rendering
über einen `THREE.Group`-Baum, der den Ebenen-Baum spiegelt.
### [design/parametric-walls.md](design/parametric-walls.md)
Regelbasierte Wandgenerierung als Alternative zum Direktzeichnen. Vier Regel-Varianten
(`GridRule`, `ModuleRule`, `ConditionalRule`, `PolylineRule`) erzeugen `Wall[]`-Arrays
über einen reinen Auflöser (`resolveParametricWall`). Deckungsbereich: Schweizer 3-m-
Wohnraster, bedingte Außen-/Innenwand-Dicken, 6-m-Jochbauweise. Phase A: Typsystem +
Resolver isoliert, kein UI. Phase B: Command + Formular-Editor. Phase C: Grid-Ressource
und IFC-Export.
### [design/elements.md](design/elements.md)
Legt **Daten, Generierung (3D + Plan) und Grip-Editing pro Bauteil** fest. Wichtigste
Empfehlung: die **Prioritäts-T-/X-Verschneidung mehrschichtiger Wände** (Backbone-
Algorithmus, Port von `_t_junction_layer_overrides`) — das höchstpriorisierte
gemeinsame Material läuft durch, der Rest mitert an; Priorität sitzt am **Component**
(`joinPriority` als Daten, nicht Hardcode). Deckt zudem gehostete Öffnungen mit
LoD-Stufen (`_OEFF_PIECE_DEFS`), Decken mit Aussparungen, Treppen (gerade/L/Wendel,
geschossübergreifend, normgerechtes 2D-Symbol), Dächer, Tragwerk und **SIA-416-Räume**
(Shoelace-Fläche, Stempel, Färbung über Override-Preset) ab; das `Tool`-Interface +
Snap-Engine ersetzt DOSSIERs Rhino-Command-Aliases.
### [design/plans-output.md](design/plans-output.md)
Der Weg zu **schönen, normgerechten, druckfertigen 2D-Plänen** (Vektor-PDF). Zentrale
Erkenntnis: Ansichten = Kamera + optionaler Schnitt, und es gibt **zwei Plan-Pfade**
(symbolischer Grundriss aus Parametern vs. Schnitt/Ansicht via **HLR im Worker**,
gecacht). Empfiehlt SVG/Paper-Space als Maßstabsmodell — Strichstärke/Schraffur sind
direkt in mm definiert (`dpi = 96·devicePixelRatio`, Hatch-Faktor `sqrt(N)/10`), was
DOSSIERs fragiles Plotweight-Rescaling überflüssig macht. Behandelt außerdem
Ausschnitte/View-Snapshots, Layer-Kombinationen, Kamera-Presets + Norden-Rotation,
Bemaßung sowie Sheets + Vektor-PDF-Export (`svg2pdf.js`/`jsPDF`, `PAPER_MM`).
### [design/resources-graphics.md](design/resources-graphics.md)
Die **Stil-Schicht**: verwaltete Ressourcen-Bibliotheken (Component-/Hatch-/Line-
Manager, alles per id referenziert), die `resolveStyle`-Kette
(ByLayer → Element-Style → Override) und die **regelbasierte Overrides-Engine**.
Schlüssel-Empfehlung: Overrides als **reine Render-Reads** modellieren (kein
Backup/Restore wie in DOSSIER, da nichts mutiert wird) — inklusive eines
**SIA-416-Presets** statt hartcodierter Färbung. Ergänzt Symbol-Bibliothek,
Rich-Text-Annotationen, den LoD-Resolver (`resolveDetail`) und den Section-Style für
geschnittene Bauteile; eine Tabelle zeigt, was der Browser hier gegenüber DOSSIER
vereinfacht.
---
## UX — Oberfläche, Interaktion & gefühlte Geschwindigkeit
### [research/ux-patterns.md](research/ux-patterns.md)
Untersucht UX-Muster moderner Browser-CAD/BIM-Tools (Arcol, Snaptrude, TestFit,
Onshape, Vectorworks, Figma) und leitet **priorisierte Leitplanken** ab. Empfehlung
für die Grundstruktur: eine feste, Figma-artige **3-Zonen-Shell**
(Navigator/Viewport/Inspector) — explizit gegen Paletten-Wildwuchs —, mit
Vectorworks-Navigation-Tabs für unsere zwei Achsen und einem zwei/drei-spaltigen
Resource-Manager als Vorbild. Größte Differenzierungs-Hebel laut Doku:
**Snapping/Inferencing** im Onshape-Stil (Vertex-Highlights, Achsenlinien, Shift
unterdrückt) und **Grip-Editing über Sicht-Grenzen** (Schnittlinie im Plan ziehen);
dazu perceived-performance-Muster (Skeletons, optimistic UI, 150-ms-Delay-then-show),
eine Command-Palette (Cmd/Ctrl-K) und learn-by-doing-Onboarding am Sample-Projekt.
---
## Swisstopo/SIA — Schweizer Geodaten & Flächenstandards
### [research/swisstopo-sia.md](research/swisstopo-sia.md)
Dokumentiert die **live getesteten** geo.admin.ch-Dienste und die SIA-Flächenlogik.
Überraschendster Befund: **alles ist ohne eigenen Backend-Proxy nutzbar** — alle vier
Hosts senden `access-control-allow-origin: *`, und der Height-Service antwortet
faktisch frei. Schlüssel fürs Browser-Gelände-Mesh ist **swissALTI3D als Cloud-
Optimized GeoTIFF** (Range-Requests via `geotiff.js`, kein Full-Download); die
**Parzelle** kommt direkt als LV95-Polygon + EGRID aus dem Identify-Service. Empfiehlt
einen konkreten Library-Satz (`proj4`, `geotiff`, `3DTilesRendererJS`/`loaders.gl`)
und ordnet die Umsetzung in ROADMAP-Phasen ein (Phase 2 SIA-Räume = reine Logik →
Phase 4a Koordinaten → 4b Gelände/Orthofoto → 4c Nachbargebäude). SIA-Teil:
verifizierte SIA-416-Formeln, DOSSIERs SIA-Logik 1:1 portierbar (Shoelace,
`compute_sia_bilanz`, CSV mit BOM); Origin-Shift (LV95 → 0/0/0) ist Pflicht wegen
float32-Jitter, Caching über IndexedDB.
---
## Top-5 Querschnitts-Empfehlungen für die ROADMAP
Diese fünf Punkte tauchen in mehreren Dokumenten auf und sollten die ROADMAP-Planung
und Priorisierung leiten:
1. **Pure-Ableitungs-Architektur als unverhandelbares Fundament** — ein
semantisches Modell, alle Sichten abgeleitet, Darstellung erst beim Rendern.
Trägt ARCHITECTURE.md, beide Plan-/Stil-Designs und die UX-Doku (billiger
Split-View, optimistic Edits, kein Cache-Stale-/Override-Restore-Aufwand). Muss
früh stehen (Store + Undo, Phase 0–1), weil sie alles Spätere prägt.
2. **OCCT/replicad im Web Worker früh als Spike absichern** — der B-Rep-Kernel und
sein `drawProjection`-HLR sind der kritische Pfad für Schnitt/Ansicht (Risiko #4)
*und* für exakte Wand-Booleans (Risiko #1) *und* für IFC. WASM-Größe, HLR-Kosten
(pro Ansicht cachen) und das Worker-Pattern sollten vor Phase 3 mit einer echten
Szene validiert werden.
3. **Component-getriebene Prioritäts-Verschneidung (Backbone-T/X) als zentrales
Geometrie-Risiko** — `joinPriority` als Daten am Component; höchstes gemeinsames
Material läuft durch, Rest mitert. Verbindet elements.md + resources-graphics.md;
2D-Plan rein analytisch, exakte 3D-Booleans im Worker. Stufenweise umsetzen
(Risiko #1, Phase 1).
4. **SVG/Paper-Space-Maßstabsmodell + maßstabskorrekte Schraffuren durchgängig** —
Strichstärke/Text/Hatch in mm, `dpi = 96·devicePixelRatio`, Hatch `sqrt(N)/10`,
SVG-`<pattern>` mit `userSpaceOnUse`. Eliminiert DOSSIERs Plotweight-Rescaling und
speist denselben Serializer für Bildschirm, PDF und DXF (tech-selection +
plans-output + resources-graphics).
5. **Schweiz-Spezifika als Differenzierer ohne Backend-Last** — SIA-416-Bilanz
(reine Logik, Phase 2, ⭐) und der serverlose Swisstopo-Flow (CORS-offen,
COG-Terrain, Parzelle/EGRID, Norden-Rotation, Origin-Shift). Klein im Aufwand,
groß im CH-Marktwert; SIA-Färbung läuft über das Override-Preset, nicht über
Sonderpfade.
+43
View File
@@ -0,0 +1,43 @@
# Backend & Kollaboration — Architekturentscheidung
> Stand: 2026-06-29 · Ziel: komplett self-hosted, kollaborations-offen
## Grundsatz
So lange wie möglich **client-only** bleiben; das Backend additiv einführen, ohne
den Kern umzubauen. Die Pure-Ableitungs-Architektur (ein serialisierbares Modell,
alle Sichten abgeleitet) ist bereits kollaborations-freundlich.
## Phasen
| Phase | Persistenz / Backend |
|---|---|
| **0–3** (Modellierer) | **Client-only**: IndexedDB + Datei-Export/Import (JSON). Offline-fähig (PWA möglich). Kein Server. |
| **5** (Konten/Persistenz) | **Supabase self-hosted** (Docker Compose): Postgres + Auth + Storage. Projekte, Versionen, Dateien (IFC/Pläne/Assets). Row-Level-Security pro Nutzer/Projekt. |
| **6** (Kollaboration) | **Yjs (CRDT)** + **Hocuspocus** Sync-Server (Container), persistiert Snapshots nach Postgres. Presence/Cursors. Optional Supabase-Realtime nur für leichte Broadcasts. |
## Warum Yjs/Hocuspocus statt reinem Supabase-Realtime
Gleichzeitiges Editieren eines strukturierten Dokuments braucht Konfliktauflösung
(CRDT). Yjs ist dafür Standard; Hocuspocus ist der self-hostbare Server dazu und
kann nach Postgres (Supabase) persistieren. Supabase-Realtime allein wäre nur
Pub/Sub ohne Merge-Semantik.
## Was wir JETZT schon richtig machen (damit Collab nicht blockiert)
- Dokumentmodell rein **JSON-serialisierbar**, keine Zyklen, stabile IDs.
- Edits immutable über `setProject` → später leicht auf Yjs-Doc abbildbar
(`Y.Map`/`Y.Array` je Sammlung: drawingLevels, layers, components, walls …).
- Kein Wahrheits-Zustand im Three.js-Scene-Graph oder im DOM — alles ableitbar.
- Ressourcen (Components/Hatches/Lines) als referenzierte Bibliotheken (IDs) →
gut mergebar.
## Self-hosted Stack (Skizze, Phase 5/6)
```
docker-compose:
supabase (postgres, gotrue auth, storage, kong gateway, studio)
hocuspocus (yjs websocket sync, persist -> postgres)
web (vite build, statisch via nginx/caddy)
```
Alles auf eigener Infrastruktur lauffähig; keine externe Cloud nötig.
## Offene Punkte
- Granularität der CRDT-Struktur (pro Sammlung vs. pro Element).
- Datei-Storage (Supabase Storage vs. S3-kompatibel/MinIO im selben Stack).
- Auth-Modell (E-Mail, OIDC/SSO fürs Büro).
+54
View File
@@ -0,0 +1,54 @@
# Kontextmenü & Anzeige-Modi — 1:1 wie DOSSIER
> Quelle: DOSSIER `src/components/ContextMenu.jsx` + `DrawingLevelsApp.jsx`/Ebenen-Panel.
> Maus-Schema (unsere Festlegung): **Mitte = navigieren** (Plan Pan / 3D Orbit, Shift+Mitte Pan) ·
> **Links = Auswahl** · **Rechts = Kontextmenü** · **Rad = Zoom**.
## ContextMenu-Komponente (generisch, wiederverwendbar)
`ContextMenu({ x, y, items, onClose, title })`
- **item**: `{ label, icon?, onClick, disabled?, danger?, shortcut?, divider? }`
- Fixed-Position mit Rand-Clamp (4px); min-width 200px; Radius 13px; weicher Schatten;
Mount-Animation `scale(.94) translateY(-5px)` 100ms; Item-Hover = `--accent-dim`;
`danger` = rote Schrift; `divider` = 1px Trenner; Titel oben (caps, 10px, muted).
- **Schließen:** Klick außerhalb · Escape · erneuter Rechtsklick · nach Item-Klick.
- z-index ~300 (unter Modals).
## Ebenen-Kontextmenü (Rechtsklick auf Ebenen-Zeile)
1. **Ebeneneinstellungen…** (`settings`) — Ebenen-Dialog *(Rhino-spez. → vorerst Stub)*
2. — Trenner —
3. **Sub-Ebene hinzufügen…** (`add`)
4. **Selektion hierher übertragen** (`move_down`) *(braucht Auswahl → später)*
5. — Trenner —
6. **Duplizieren** (`content_copy`) — Klon mit Suffix „ KOPIE"
7. **Eigenschaften kopieren** (`colorize`) — Farbe + Linienstärke
8. **Eigenschaften einfügen** (`format_paint`) — disabled wenn Clipboard leer
9. — Trenner —
10. **Löschen** (`delete`, danger) — disabled wenn ≤1 Ebene
Titel = Ebenenname/-code.
## Zeichnungsebenen-Kontextmenü (Rechtsklick auf Geschoss/Schnitt/Zeichnung)
1. **Einstellungen…** (`settings`)
2. — Trenner —
3. **Duplizieren** (`content_copy`) — Klon mit Suffix „ Kopie"
4. — Trenner —
5. **Löschen** (`delete`, danger) — disabled wenn ≤1 Ebene
Titel = Name.
## „+"-Menü (Zeichnungsebenen)
**Geschoss** (`layers`) · **Schnitt / Ansicht** (`content_cut`) · — · **Zeichnung** (`edit_note`)
## Anzeige-Modi (Dropdown oben im Panel) — **5 Modi** (für Ebenen UND Zeichnungsebenen)
| Wert | Label | Verhalten |
|---|---|---|
| `all_force` | **Alle anzeigen** | alle erzwungen sichtbar; Augen gedimmt; Klick aufs Auge → wechselt zu „Ausgewählte" |
| `all` | **Ausgewählte** | sichtbar nach per-Zeile-Flag |
| `active` | **Nur aktive** | nur aktive sichtbar; andere stark gedimmt |
| `grey` | **Andere grau** | aktive normal, andere 45% (Sichtbarkeits-Flags gelten) |
| `grey_locked` | **Andere grau & gesperrt** | wie grey + andere gesperrt |
Regel: Klick aufs Auge in `all_force`/`active` schaltet automatisch auf `all`.
*(Hinweis: Panel-System-Workflow hatte vorerst nur 3 Modi — beim Kontextmenü-Build auf diese 5 angleichen.)*
## Migration (was sofort geht / was Stub bleibt)
- Sofort: ContextMenu-Komponente 1:1; Duplizieren, Eigenschaften kopieren/einfügen, Löschen,
+Menü, 5 Anzeige-Modi (alles reine JSON-State-Operationen).
- Stub/später: „Ebeneneinstellungen…"/„Einstellungen…" (Dialog), „Sub-Ebene hinzufügen", „Selektion hierher übertragen".
+760
View File
@@ -0,0 +1,760 @@
# Aktive Zeichen- und Bearbeitungs-Werkzeuge
Status: Entwurf. Dieses Dokument spezifiziert das **Tool-System** für das aktive
Erzeugen von Modell-Elementen durch Zeichnen im Grundriss: Wände (Achs-Polylinie
→ `Wall` eines `WallType`) sowie reine 2D-Geometrie (Linie, Polylinie, Rechteck,
Kreis, Bogen, Text). Es definiert die Werkzeug-Zustandsmaschine, die Live-Vorschau
(Rubber-Band), das **Snapping** mit Bildschirm-Markern, die Ebenen-/Kategorie-/
Stil-Zuordnung neuer Elemente und das neue Element `Drawing2D` samt Ableitung in
`generatePlan`.
Bezugsdokumente: [elements.md](elements.md) (Wand-/Tür-Modell),
[resources-graphics.md](resources-graphics.md) (Stil-Auflösung),
[plans-output.md](plans-output.md) (Papier-Maßstab, mm-Strichstärken),
[context-menu.md](context-menu.md) (Maus-Schema).
## 0. Architektur-Prinzip (Bezug zum Repo)
Die App folgt der Regel **ein semantisches Modell ist die einzige Wahrheit; jede
Ansicht ist abgeleitet** (CONVENTIONS.md, `App.tsx`). Werkzeuge greifen darum NUR über
`setProject` immutabel auf das `Project`-Modell zu; sie schreiben NIE Geometrie
direkt in den Plan. Der `PlanView` bleibt eine reine Darstellungs-/Eingabe-
Schicht. Das Tool-System setzt genau an der bestehenden Naht in `PlanView` an:
- **Modell↔Screen.** `PlanView` rechnet bereits Cursor-Pixel → viewBox-Einheiten
(`clientToView`) → Modell-Meter (`viewToModel`). Diese Umrechnung ist die
Grundlage; Werkzeuge arbeiten ausschließlich in **Modell-Metern** (CONVENTIONS.md:
intern alles in Metern). Für Snap-Marker brauchen Werkzeuge zusätzlich die
Rückrichtung Modell → viewBox (`toScreen`, existiert bereits) bzw. Modell →
Client-Pixel.
- **Pointer-Handling.** `PlanView` besitzt heute drei Gesten an der linken Taste/
Mitte/rechts: Auswahl/Marquee, Pan, Kontextmenü. Das Tool-System schiebt sich
VOR diese Logik: ist ein aktives Zeichenwerkzeug gewählt (≠ `select`), übernimmt
das Werkzeug `pointerdown/move/up`; das `select`-Werkzeug delegiert an die heute
schon vorhandene Auswahl-/Marquee-Logik (kein Verhaltensbruch).
- **Pan/Zoom bleiben immer aktiv.** Mittlere Maustaste (Pan) und Mausrad (Zoom)
laufen unverändert weiter, auch während ein Zeichenwerkzeug aktiv ist — sonst
kann man beim Zeichnen nicht navigieren.
## 1. Datenfluss-Überblick
```
TopBar (Werkzeugleiste) --activeTool--> App-State
│
┌──── activeTool, wallTypeId, defaultCategoryCode ────┐
▼ ▼
PlanView ── pointerdown/move/up (Modellpunkt) ──> ToolController
▲ │
Snap-Marker + Rubber-Band-Overlay <── DraftState (Vorschau) ──┘
│ │
└──────────────── commit ──> onToolCommit(Element) ──> setProject
```
`activeTool` und die Werkzeug-Parameter (aktiver `WallType`, Default-Kategorie)
liegen als **View-State** in `App.tsx` — wie `viewType`, `detail`, `selectedWallIds`
bereits dort liegen. Der `ToolController` ist **frameworkfrei** (reines TS, kein
React-State pro Mausbewegung — analog zu `drag`/`marquee` als `useRef` in
`PlanView`), damit die Live-Vorschau ohne Re-Render des ganzen Baums läuft. Nur
beim **Commit** wird `setProject` (Re-Render) ausgelöst.
## 2. Koordinaten & Hilfsfunktionen
`PlanView` exportiert künftig zwei reine Konverter (heute intern vorhanden),
plus die effektive Pixel-pro-Meter-Skala für die Snap-Toleranz:
```ts
// PlanView-intern bereits da; wird als stabile Callbacks nach außen gereicht.
type ToModel = (clientX: number, clientY: number) => Vec2; // Pixel → Meter
type ToClient = (m: Vec2) => { x: number; y: number }; // Meter → Pixel
type PxPerMeter = () => number; // aktuelle meet-Skala * PX_PER_M (Snap-Toleranz)
```
`PxPerMeter` ergibt sich aus `meetScale(view) * PX_PER_M` (beides in `PlanView`
vorhanden). Snap-Toleranzen werden in **Bildschirm-Pixeln** definiert (z. B. 10 px)
und über `pxPerMeter` in Meter umgerechnet — so ist der Fangradius zoom-unabhängig
konstant am Bildschirm.
## 3. Tool-System
### 3.1 Werkzeug-Identität und Registry
```ts
export type ToolId =
| "select" // Default: Auswahl/Marquee (heutiges Verhalten)
| "wall" // Wand-Achs-Polylinie → Wall je Segment
| "line" // einzelne 2D-Strecke
| "polyline" // offene 2D-Polylinie
| "rect" // 2D-Rechteck (zwei Ecken)
| "circle" // 2D-Kreis (Zentrum + Radius)
| "arc" // 2D-Bogen (3-Punkt oder Zentrum-Start-Ende)
| "text"; // 2D-Textmarke
/** Live-Kontext, den ein Werkzeug bei jedem Schritt erhält. */
export interface ToolContext {
project: Project;
/** Aktives Geschoss/Zeichnungsebene (Ziel der neuen Elemente). */
level: DrawingLevel;
/** Default-Kategorie-Code für neue Elemente (siehe §6). */
defaultCategoryCode: string;
/** Aktiver Wandtyp für das Wand-Werkzeug. */
activeWallTypeId: string;
/** Aktiver Linienstil-Code für 2D-Primitive (Line Manager). */
activeLineStyleId: string;
/** Snapping-Einstellungen (an/aus je Typ, ortho, grid). */
snap: SnapSettings;
/** Pixel pro Meter (für Snap-Toleranz in Metern). */
pxPerMeter: number;
}
/** Ein an einer Modellposition ausgelöstes Pointer-Ereignis. */
export interface ToolPointer {
/** Roher Modellpunkt (vor Snapping), in Metern. */
raw: Vec2;
/** Gesnappter Punkt + Marker-Info (siehe §5). null = kein Snap. */
snap: SnapResult | null;
/** Effektiver Punkt = snap?.point ?? raw. */
point: Vec2;
/** Modifikatoren (Shift = Ortho erzwingen, Ctrl = Snap aus, Alt = …). */
shift: boolean;
ctrl: boolean;
alt: boolean;
button: number; // 0 links, 2 rechts
}
/** Was ein Werkzeug-Schritt nach außen meldet. */
export interface ToolResult {
/** Neuer Vorschau-Zustand (Rubber-Band-Geometrie); null = nichts zu zeigen. */
draft: ToolDraft | null;
/** Bei Abschluss: Mutation, die App über setProject anwendet. */
commit?: (p: Project) => Project;
/** true → Werkzeug ist fertig und kehrt in seinen Ruhezustand zurück. */
done?: boolean;
}
/** Die Werkzeug-Schnittstelle (reine Funktionen über einen internen State). */
export interface Tool {
id: ToolId;
/** UI-Label-Key (i18n), z. B. "tool.wall". */
labelKey: string;
/** Material-Symbol-Name für die Werkzeugleiste. */
icon: string;
/** Statuszeilen-Hinweis-Key je Phase (z. B. "tool.wall.firstPoint"). */
hintKey: (state: ToolState) => string;
/** Initialer Ruhezustand. */
init(): ToolState;
/** Klick/Tap (pointerdown→up ohne Drag, bzw. „setze Punkt"). */
onClick(state: ToolState, p: ToolPointer, ctx: ToolContext): [ToolState, ToolResult];
/** Bewegung (Hover/Drag): nur Vorschau, nie Commit. */
onMove(state: ToolState, p: ToolPointer, ctx: ToolContext): [ToolState, ToolResult];
/** Doppelklick/Enter: mehrteilige Werkzeuge abschließen (z. B. Polylinie). */
onCommitGesture(state: ToolState, ctx: ToolContext): [ToolState, ToolResult];
/** Esc: aktuellen Entwurf verwerfen, zurück in den Ruhezustand. */
onCancel(state: ToolState): [ToolState, ToolResult];
/** Backspace: letzten gesetzten Punkt zurücknehmen (mehrteilig). */
onUndoPoint?(state: ToolState, ctx: ToolContext): [ToolState, ToolResult];
}
```
`ToolState` ist je Werkzeug ein Discriminated Union (Beispiel Wand in §4). Der
`ToolController` hält genau eine aktive `Tool`-Instanz + deren `ToolState` in
einem `useRef` und ist die einzige Stelle, die diese Methoden aufruft.
### 3.2 Vorschau-Geometrie (Rubber-Band)
```ts
/** Darstellbare Vorschau — dieselben Primitive wie der Plan, plus Marker. */
export interface ToolDraft {
/** Vorschau-Primitive (gestrichelt/halbtransparent gezeichnet). */
preview: Primitive[];
/** Bereits gesetzte „feste" Stützpunkte (kleine Quadrate). */
vertices: Vec2[];
/** Optionaler Maß-/Winkel-Text am Cursor (z. B. "3.20 m, 90°"). */
hud?: { at: Vec2; text: string };
}
```
Wichtig: Die Vorschau benutzt **dieselben `Primitive`-Typen** wie `generatePlan`
(`polygon | line | arc`). Damit kann der Vorschau-Layer mit derselben
`PrimitiveShape`-Renderlogik gezeichnet werden (DRY) — nur mit einer
Vorschau-CSS-Klasse (gestrichelt, Akzentfarbe). Für die Wand-Vorschau kann das
Werkzeug sogar `generatePlan` auf einem **temporären Projekt** (Original + die in
Bau befindliche Wand) aufrufen, um echte gehrte Poché live zu zeigen; in der
ersten Phase reicht eine einfache Bandvorschau (`wallCorners`).
### 3.3 Zustandsmaschine (allgemein)
Jedes Werkzeug ist eine kleine Maschine über `pointerdown → move → up`. Da
`PlanView` Pointer-Capture nutzt, kommen `move`/`up` zuverlässig an. Generisches
Muster:
```
ruht ──pointerdown──> (Werkzeug setzt 1. Punkt / startet Drag)
▲ │
│ ├──move──> Vorschau (rubber-band), kein Commit
│ │
│ (mehrteilig) pointerdown──> Punkt anhängen, Vorschau weiter
│ │
└──Esc/Cancel─────────────┤
▼
Doppelklick/Enter/letzter Punkt ──> commit(project) ──> ruht
```
- **Klick-vs-Drag.** Wie heute in `PlanView` (`MARQUEE_THRESHOLD_PX`): unter der
Schwelle ist es ein „Punkt setzen" (Klick), darüber ein Drag. Rechteck/Kreis/
Linie unterstützen BEIDE Bedienarten: Zwei-Klick (Punkt, Punkt) ODER Drücken-
Ziehen-Loslassen. Polyline/Wall sind reine Klickfolgen mit Abschluss per
Doppelklick/Enter.
- **Esc** verwirft den Entwurf (`onCancel`) und bleibt im selben Werkzeug.
Zweites Esc (im Ruhezustand) schaltet zurück auf `select`.
- **Rechtsklick** während eines aktiven Entwurfs = „abschließen/abbrechen"
(CAD-üblich), KEIN Kontextmenü; im Ruhezustand öffnet Rechtsklick wie bisher
das Plan-Kontextmenü.
### 3.4 Einbettung in PlanView (Pointer-Routing)
`PlanView` bekommt zwei neue Props:
```ts
interface PlanViewProps {
// … bisherige Props …
/** Aktives Werkzeug; "select" = bisheriges Verhalten. */
activeTool?: ToolId;
/**
* Werkzeug-Treiber. PlanView ruft diese Callbacks mit fertig gesnappten
* Modellpunkten auf und rendert den zurückgegebenen Draft als Overlay.
*/
toolHandlers?: {
onToolDown(p: ToolPointer): void;
onToolMove(p: ToolPointer): void;
onToolUp(p: ToolPointer): void;
onToolDoubleClick(): void;
/** liefert die zu zeichnende Vorschau (von App/Controller gehalten). */
draft: ToolDraft | null;
};
}
```
Routing in `onPointerDown` (Ergänzung der bestehenden Methode):
```
onPointerDown(e):
if e.button === 1: → bestehender Pan (unverändert)
if e.button === 0:
if activeTool === "select": → bestehende Auswahl-/Marquee-Geste
else:
setPointerCapture
p = makeToolPointer(e) // raw → snap → point (§5)
toolHandlers.onToolDown(p)
if e.button === 2 (rechts):
if activeTool !== "select" && entwurf aktiv: toolHandlers.onToolUp({button:2,…}) // abschließen
else: bestehendes Kontextmenü
```
`onPointerMove`/`onPointerUp` analog: bei aktivem Zeichenwerkzeug an
`onToolMove`/`onToolUp` routen statt an Pan/Marquee. Der Cursor wird auf
`crosshair` gesetzt. `makeToolPointer` führt das Snapping aus (§5) und liefert den
fertigen `ToolPointer`.
Die **Snap-Marker** und der **Draft** werden als zusätzliche SVG-Gruppe NACH den
Plan-Primitiven, aber vor der Auswahl-Hervorhebung gerendert (immer obenauf,
`pointerEvents="none"`). Marker werden in viewBox-Einheiten über `toScreen`
positioniert (existiert bereits).
## 4. Werkzeug: Wand (Wall)
Das Wand-Werkzeug zeichnet eine **Achs-Polylinie**; jedes Segment wird zu einem
eigenständigen `Wall`-Element des aktiven `WallType` auf dem aktiven Geschoss.
Aufeinanderfolgende Segmente teilen sich einen Knoten → die bestehende
`computeJoins`-Verschneidung (in `generatePlan`) erzeugt automatisch saubere
Gehrungen an den Ecken. Kein zusätzlicher Join-Code nötig.
### 4.1 Zustand
```ts
type WallToolState =
| { phase: "idle" }
| {
phase: "drawing";
/** Bisher gesetzte Achs-Knoten (in Metern). */
points: Vec2[];
/** Aktuelle Cursor-Position (gesnappt) für die Rubber-Band-Vorschau. */
cursor: Vec2 | null;
};
```
### 4.2 Pseudocode
```
WallTool.onClick(state, p, ctx):
if state.phase === "idle":
return [{phase:"drawing", points:[p.point], cursor:p.point}, {draft: draftFor([p.point], p.point, ctx)}]
else: // weiteren Knoten anhängen
pts = [...state.points, p.point]
# Ortho/Snap haben p.point bereits ausgerichtet (§5).
return [{phase:"drawing", points: pts, cursor: p.point}, {draft: draftFor(pts, p.point, ctx)}]
WallTool.onMove(state, p, ctx):
if state.phase !== "drawing": return [state, {draft:null}]
return [{...state, cursor:p.point}, {draft: draftFor(state.points, p.point, ctx)}]
WallTool.onCommitGesture(state, ctx): // Doppelklick / Enter / Rechtsklick
if state.phase !== "drawing" || state.points.length < 2:
return [{phase:"idle"}, {draft:null, done:true}]
pts = state.points
return [{phase:"idle"}, {
draft: null, done: true,
commit: (proj) => appendWalls(proj, pts, ctx)
}]
WallTool.onCancel(state):
return [{phase:"idle"}, {draft:null, done:true}]
WallTool.onUndoPoint(state):
if state.phase==="drawing" && state.points.length>1:
return [{...state, points: state.points.slice(0,-1)}, {draft: …}]
return [{phase:"idle"}, {draft:null}]
```
`draftFor` baut die Vorschau: feste Segmente zwischen `points` + ein „lebendes"
Segment `points[last] → cursor`. Pro Segment werden die vier Band-Eckpunkte über
`wallCorners(a, b, thickness)` (vorhanden) berechnet und als Vorschau-`polygon`
gezeichnet; zusätzlich ein HUD mit Länge `|b−a|` und Winkel. `thickness =
wallTypeThickness(getWallType(...))`.
### 4.3 Commit ins Modell
```
appendWalls(project, pts, ctx):
newWalls = []
for i in 0 .. pts.length-2:
a = pts[i]; b = pts[i+1]
if |b-a| < EPS: continue // Null-Segmente überspringen
newWalls.push({
id: uniqueId("W"), // siehe §8 (ID-Vergabe)
type: "wall",
floorId: ctx.level.id, // aktives Geschoss
categoryCode: ctx.defaultCategoryCode, // §6
start: a, end: b,
wallTypeId: ctx.activeWallTypeId,
height: ctx.level.floorHeight ?? 2.6, // Geschosshöhe als Default
})
return { ...project, walls: [...project.walls, ...newWalls] }
```
Hinweise:
- **Höhe** erbt die lichte Geschosshöhe (`DrawingLevel.floorHeight`), Fallback 2.6 m.
- **Geschossbindung**: Das Wand-Werkzeug ist nur aktiv, wenn `level.kind === "floor"`
(sonst gibt es keine Wände). In `drawing`-Ebenen ist das Wand-Werkzeug
deaktiviert (nur 2D-Werkzeuge); siehe §6.
- Die Wicklung wird NICHT erzwungen — `leftNormal`-Konvention (CONVENTIONS.md) und
`computeJoins` arbeiten richtungsunabhängig pro Segment.
## 5. Snapping
Snapping läuft in `makeToolPointer` (PlanView) BEVOR der Punkt an das Werkzeug
geht. Es prüft mehrere Snap-Quellen, wählt die nächstgelegene innerhalb der
Toleranz und liefert sowohl den gefangenen Punkt als auch eine **Marker-Art** für
die Bildschirmdarstellung.
### 5.1 Typen
```ts
export type SnapKind =
| "endpoint" // Wand-Achsenende, Polylinien-Knoten, Primitiv-Endpunkt
| "midpoint" // Mitte einer Strecke/Wandachse
| "intersection" // Schnittpunkt zweier Achsen/Linien
| "center" // Kreis-/Bogenzentrum
| "quadrant" // Kreis-Quadrantenpunkte (0/90/180/270°)
| "onEdge" // nächster Punkt AUF einer Wandachse/Linie (Lot)
| "grid" // Rasterpunkt
| "ortho" // orthogonal/winkelrastriert zum vorigen Punkt
| "extension"; // Verlängerung einer Achse (gestrichelte Hilfslinie)
export interface SnapResult {
point: Vec2; // gefangener Punkt (Meter)
kind: SnapKind;
/** Quell-Element (für Marker/Hilfslinien), optional. */
refA?: Vec2;
refB?: Vec2;
/** Bildschirm-Distanz Cursor→Snap (px) — für die Auswahl des Besten. */
distPx: number;
}
export interface SnapSettings {
enabled: boolean; // Master-Schalter (Ctrl invertiert temporär)
endpoint: boolean;
midpoint: boolean;
intersection: boolean;
center: boolean;
onEdge: boolean;
grid: boolean;
gridSize: number; // Rasterweite in Metern, z. B. 0.10
ortho: boolean; // Shift erzwingt zusätzlich
angleStep: number; // Winkelraster in Grad (z. B. 45)
tolerancePx: number; // Fangradius am Bildschirm, z. B. 10
}
```
### 5.2 Snap-Kandidaten sammeln
Quellen pro Geschoss (gefiltert auf sichtbare Kategorien, wie der Plan):
| Snap | Quelle |
|------|--------|
| endpoint | `wall.start`, `wall.end` aller sichtbaren Wände; Knoten bereits gesetzter Draft-Punkte; `Drawing2D`-Vertices |
| midpoint | Mitte jeder Wandachse und jedes 2D-Segments |
| intersection | paarweise `lineIntersect` der Wandachsen (nur Paare, deren Boxen sich am Cursor nähern) |
| center/quadrant | Kreise/Bögen aus `Drawing2D` |
| onEdge | Lotfußpunkt des Cursors auf jede nahe Wandachse/2D-Linie |
| grid | Rundung des Cursors auf `gridSize` |
| ortho | Ausrichtung relativ zum letzten Draft-Punkt (§5.4) |
Performance: Kandidaten werden je `move` neu erzeugt, aber **früh nach
Bildschirm-Distanz gefiltert** (nur Punkte innerhalb ~`2·tolerancePx`). Bei
großen Modellen kann eine grobe Bounding-Box-Vorauswahl je Wand vorgeschaltet
werden; in den ersten Phasen genügt lineares Scannen (Wandzahl ist klein).
### 5.3 Auswahl-Pseudocode
```
computeSnap(rawModel, ctx, draftPoints, lastPoint):
if ctrl(): return null # Snap temporär aus
s = ctx.snap
tolM = s.tolerancePx / ctx.pxPerMeter # px-Toleranz → Meter
cands: SnapResult[] = []
if s.endpoint: cands += endpoints(...) filtered to within tolM
if s.midpoint: cands += midpoints(...)
if s.intersection: cands += intersections(...)
if s.center: cands += centers/quadrants(...)
if s.onEdge: cands += perpendicularFeet(...) # niedrigere Priorität
# Punkt-Snaps haben Vorrang vor Linien-/Raster-Snaps:
pick = argmin(cands, by distPx within tolM, tie-break by priority)
if pick: rawModel = pick.point
# Ortho/Winkelraster wirkt RELATIV zum letzten Punkt und ÜBERLAGERT:
if (s.ortho || shift()) && lastPoint:
rawModel = applyAngleConstraint(lastPoint, rawModel, s.angleStep)
# Wenn dabei auch ein Punkt-Snap nahe der Ortho-Linie liegt → bevorzugen.
if !pick && s.grid:
g = snapToGrid(rawModel, s.gridSize)
if dist(g, rawModel) within tolM: return {point:g, kind:"grid", …}
return pick ?? null
```
Prioritätsreihenfolge bei gleichem Abstand: `endpoint > intersection > midpoint >
center/quadrant > onEdge > grid`. Ortho/Winkelraster ist eine **Projektion**, kein
Punkt-Kandidat: es verschiebt den (ggf. schon gesnappten) Punkt auf die nächste
erlaubte Richtung vom letzten Knoten.
### 5.4 Ortho / Winkelraster
```
applyAngleConstraint(from, to, stepDeg):
d = to - from
ang = atan2(d.y, d.x)
k = round(ang / rad(stepDeg)) * rad(stepDeg)
len = |d|
return from + (cos(k), sin(k)) * len
```
Mit `stepDeg = 90` ist das klassisches Ortho (H/V); `45` erlaubt Diagonalen.
`Shift` erzwingt Ortho temporär unabhängig von der Einstellung.
### 5.5 Bildschirm-Marker
Pro aktivem Snap zeichnet `PlanView` ein Marker-Glyph an `toScreen(snap.point)`
(`pointerEvents="none"`, eigene CSS-Klassen, papierkonstante Größe via
non-scaling):
- `endpoint` → kleines Quadrat ▫
- `midpoint` → Dreieck �△
- `intersection` → ✕
- `center` → ○, `quadrant` → ◇
- `onEdge` → ⟂-Glyph
- `grid` → feiner Punkt
- `ortho`/`extension` → zusätzlich eine **gestrichelte Hilfslinie** von `refA`
(Bezugspunkt) zum Cursor
Marker erscheinen NUR während ein Zeichenwerkzeug aktiv ist. i18n-Tooltips/Status
(„Endpunkt", „Mittelpunkt", …) über `t('snap.endpoint')` etc.
## 6. Ebene, Kategorie und Stil neuer Elemente
Neue Elemente brauchen eine **Zeichnungsebene** (DrawingLevel) und eine
**Kategorie** (LayerCategory `code`) sowie — bei 2D-Primitiven — einen Stift/
Schraffur-Stil.
### 6.1 Zeichnungsebene (Ziel)
- Ziel ist **immer das aktive Geschoss/die aktive Zeichnungsebene** (`activeLevelId`
in `App.tsx`). Wände nur auf `kind === "floor"`. 2D-Primitive (`Drawing2D`) auf
jeder Ebene, also auch auf `kind === "drawing"` (freie 2D-Zeichnung).
### 6.2 Kategorie (categoryCode)
- Es gibt eine **aktive Kategorie** als View-State (`activeCategoryCode` in App,
neu). Default beim Start: der Code der gewählten Wand-Kategorie (im Sample „20"
Wände), bzw. die erste sichtbare Kategorie. Die Statusleiste zeigt heute schon
die „aktive Ebene" (`activeLayerName`); diese wird künftig von `activeCategoryCode`
gespeist statt nur aus der Auswahl abgeleitet.
- Neue Wände: `categoryCode = activeCategoryCode` (z. B. „20").
- Neue 2D-Primitive: ebenfalls `activeCategoryCode`. Sinnvoll ist eine eigene
2D-/Hilfslinien-Kategorie (z. B. „90 Zeichnung"); diese wird über die
Kategorie-Auswahl in der Statusleiste/Werkzeugleiste gesetzt.
- Die Kategorie liefert Farbe + Strichstärke (`LayerCategory.color`, `.lw`), genau
wie `generatePlan` es heute für Wände via `categoryLwMap` nutzt.
### 6.3 Stift/Schraffur
- **Wände** erhalten KEINEN eigenen Stift — ihr Erscheinungsbild kommt aus dem
`WallType` (Component → Hatch → LineStyle) und der Kategorie-`lw` (bestehender
Pfad in `generatePlan`).
- **2D-Primitive** referenzieren optional einen `LineStyle` aus dem Line Manager
(`activeLineStyleId`). Ohne expliziten Stil erben sie Farbe/Strichstärke aus der
Kategorie (`color`, `lw`). Flächige 2D-Primitive (geschlossenes Rechteck/Kreis/
Polyline) können optional eine Schraffur (`hatchId`) tragen.
## 7. Speicherung der 2D-Primitive: `Drawing2D`
2D-Geometrie wird als neues Modell-Element `Drawing2D` gespeichert — analog zu
`Wall`/`Door` ein semantisches Element, das beim Rendern abgeleitet wird (KEINE
vorab erzeugten Primitive im Modell). Damit bleibt die Architektur „Modell →
abgeleitete Ansicht" intakt.
### 7.1 Typ
```ts
/** Geometrie-Form eines 2D-Zeichenelements. */
export type Drawing2DGeom =
| { shape: "line"; a: Vec2; b: Vec2 }
| { shape: "polyline"; pts: Vec2[]; closed: boolean }
| { shape: "rect"; min: Vec2; max: Vec2 } // achsparallel
| { shape: "circle"; center: Vec2; r: number }
| {
shape: "arc";
center: Vec2;
r: number;
/** Start-/Endwinkel in Radiant (math. Konvention, CCW positiv). */
a0: number;
a1: number;
}
| { shape: "text"; at: Vec2; text: string; height: number; angle: number };
/** Ein freies 2D-Zeichenelement auf einer Zeichnungsebene. */
export interface Drawing2D {
id: string;
type: "drawing2d";
/** Zeichnungsebene (Geschoss ODER freie 2D-Ebene). */
levelId: string;
/** Grafik-Kategorie (Ebene) — liefert Farbe/Strichstärke als Default. */
categoryCode: string;
geom: Drawing2DGeom;
/** Optionaler Linienstil (Line Manager); sonst Kategorie-Default. */
lineStyleId?: string;
/** Optionale Schraffur für geschlossene Formen (Hatch Manager). */
hatchId?: string;
/** Optionale explizite Strichfarbe; sonst Kategorie-Farbe. */
color?: string;
}
```
Ergänzung am `Project`:
```ts
export interface Project {
// … bisher …
drawings2d: Drawing2D[]; // NEU
}
export type Element = Wall | Door | Drawing2D; // erweitert
```
`sampleProject` bekommt ein leeres `drawings2d: []`. Lösch-/Referenz-Regeln:
beim Löschen einer Zeichnungsebene werden auch deren `Drawing2D` entfernt (analog
zur bestehenden Wand-/Tür-Bereinigung in `deleteLevel`).
### 7.2 Ableitung in `generatePlan`
`generatePlan` rendert künftig zusätzlich die `Drawing2D` des Geschosses (gefiltert
wie Wände auf sichtbare Kategorien + `categoryDisplay`). Neue Funktion
`addDrawing2D(out, project, d, greyed, lwMm)`:
```
addDrawing2D(out, project, d):
color = d.color ?? categoryColor(d.categoryCode)
weight = lineStyle(d.lineStyleId)?.weight ?? categoryLw(d.categoryCode)
dash = lineStyle(d.lineStyleId)?.dash ?? null
switch d.geom.shape:
"line": out.push({kind:"line", a, b, cls:"draw2d", weightMm:weight, dash})
"polyline": for each segment → line-Primitive (closed → Schluss-Segment)
"rect": vier Kanten als line-Primitive (oder polygon, falls hatchId)
"circle": → als zwei 180°-Bögen (arc-Primitive) ODER neues Primitiv (s. u.)
"arc": → arc-Primitive (center/from/to/r aus a0,a1)
"text": → neues text-Primitiv (s. u.)
```
Dabei wird, wo möglich, der **vorhandene** `Primitive`-Vorrat (`line`, `arc`,
`polygon`) wiederverwendet — die Strichstärke kommt in mm Papier (wie der Rest des
Plans), Farbe über eine CSS-Klasse oder ein neues optionales `color`-Feld am
`line`-Primitive.
Zwei `Primitive`-Erweiterungen sind nötig:
```ts
// kreisförmige Vollkurve (Kreis) — sonst muss man sie in zwei Bögen zerlegen:
| { kind: "circle"; center: Vec2; r: number; cls: string; weightMm: number;
dash?: number[] | null; fill?: string; greyed?: boolean }
// Textmarke:
| { kind: "text"; at: Vec2; text: string; heightMm: number; angle: number;
cls: string; color?: string; greyed?: boolean }
```
`PlanView.renderPrimitive` bekommt entsprechende `case`-Zweige (`<circle>`,
`<text>`). Text wird in **Papier-Millimetern** dimensioniert (Höhe → `mmToPx`,
non-scaling), damit die Schrifthöhe beim Zoomen papierkonstant bleibt (analog zu
Strichstärken in `plans-output.md`).
Das `arc`-Primitiv zeichnet heute nur Kurzbögen (≤180°, `large-arc=0`). Für
beliebige 2D-Bögen wird es um ein `largeArc`-Flag erweitert (aus `|a1−a0|`
berechnet); abwärtskompatibel (Default 0).
## 8. ID-Vergabe & Immutabilität
- Neue IDs über einen kleinen Helfer `uniqueId(prefix)` (z. B.
`\`${prefix}-${Date.now()}-${counter++}\``), konsistent mit der bestehenden
Praxis in `App.tsx` (`floor-${Date.now()}` usw.). Wand-Präfix „W", 2D-Präfix
„dr2d".
- Alle Mutationen laufen über `setProject` immutabel (CONVENTIONS.md / App-Konvention).
Der `commit(project)` eines Werkzeugs ist eine reine Funktion `Project →
Project`; App ruft `setProject(prev => result.commit(prev))`.
## 9. App- und PlanView-Verdrahtung (konkret)
Neuer View-State in `App.tsx`:
```ts
const [activeTool, setActiveTool] = useState<ToolId>("select");
const [activeCategoryCode, setActiveCategoryCode] = useState<string>(/* erste Wand-Kat */);
const [activeWallTypeId, setActiveWallTypeId] = useState<string>(project.wallTypes[0].id);
const [activeLineStyleId, setActiveLineStyleId] = useState<string>(project.lineStyles[0].id);
const [snap, setSnap] = useState<SnapSettings>(DEFAULT_SNAP);
const toolStateRef = useRef<ToolState>(getTool(activeTool).init());
const [draft, setDraft] = useState<ToolDraft | null>(null);
```
Der `ToolController` ist eine kleine Hook/Klasse, die `toolStateRef` hält und die
`PlanView.toolHandlers` implementiert:
```
onToolDown(p): [st, res] = tool.onClick(toolStateRef.current, p, ctx)
toolStateRef.current = st; setDraft(res.draft)
if res.commit: setProject(res.commit)
if res.done: toolStateRef.current = tool.init()
onToolMove(p): [st, res] = tool.onMove(...); toolStateRef.current=st; setDraft(res.draft)
onToolDoubleClick(): [st,res]=tool.onCommitGesture(...); apply commit/done; setDraft(null)
```
Keyboard (global, nur wenn ein Zeichenwerkzeug aktiv ist):
`Esc → onCancel`, `Enter → onCommitGesture`, `Backspace → onUndoPoint`. Beim
Wechsel von `activeLevelId`/`viewType` wird der laufende Entwurf verworfen (analog
zur bestehenden Auswahl-Bereinigung in den `useEffect`s).
`ctx` (ToolContext) wird in App via `useMemo` aus Project + aktiven Selektionen
gebaut und an PlanView/Controller gereicht.
### 9.1 Werkzeugleiste (TopBar)
Eine neue Werkzeug-Gruppe in der `TopBar` (links, vor den Ansichts-Toggles), als
i18n-beschriftete Icon-Buttons (`t('tool.select')`, `t('tool.wall')`, …). Aktiv-
Zustand hervorgehoben. Daneben: Auswahl des aktiven `WallType` (für Wand) und der
aktiven Kategorie/des Linienstils (Dropdowns), sowie Snap-Toggles (kleines
Snap-Menü mit Checkboxen je `SnapKind`, Grid-Größe, Winkelraster). Wand-/2D-
Werkzeuge werden je nach `level.kind` aktiviert/deaktiviert (Tooltip nennt den
Grund — wie die bestehenden disabled-Menüpunkte in `App.tsx`).
### 9.2 i18n-Keys (neu, Auszug)
```
tool.select / tool.wall / tool.line / tool.polyline / tool.rect /
tool.circle / tool.arc / tool.text
tool.wall.firstPoint / tool.wall.nextPoint / tool.wall.finish
snap.endpoint / snap.midpoint / snap.intersection / snap.center /
snap.quadrant / snap.onEdge / snap.grid / snap.ortho
snap.settings / snap.gridSize / snap.angleStep
status.activeWallType / status.activeCategory / status.activeTool
```
Alle sichtbaren Strings über `t(...)` (CONVENTIONS.md). Identifier bleiben englisch.
## 10. Übrige Werkzeuge (Kurzspezifikation)
| Werkzeug | Eingabe | Zustand | Commit |
|----------|---------|---------|--------|
| **Line** | 2 Punkte (Klick-Klick oder Drag) | `{a?}` | `Drawing2D{shape:"line"}` |
| **Polyline** | n Punkte, Abschluss Doppelklick/Enter; `closed` per „C" oder Klick auf Start | `{pts}` | `Drawing2D{shape:"polyline"}` |
| **Rectangle** | 2 Ecken (Drag oder Klick-Klick) | `{p0?}` | `Drawing2D{shape:"rect"}` (min/max sortiert) |
| **Circle** | Zentrum + Radius-Punkt | `{center?}` | `Drawing2D{shape:"circle"}` |
| **Arc** | 3 Punkte (Start, durch, Ende) ODER Zentrum-Start-Ende (Modus-Toggle) | `{p0?,p1?}` | `Drawing2D{shape:"arc"}` (a0/a1 aus Punkten) |
| **Text** | 1 Punkt → Inline-Eingabefeld (wie `InlineEditor` in App) | `{at?}` | `Drawing2D{shape:"text"}` |
Alle nutzen dasselbe `Tool`-Interface, dasselbe Snapping und denselben Draft-/
Commit-Pfad. Text öffnet beim Setzen des Ankerpunkts ein kleines Overlay-Eingabe-
feld (an `toClient(at)` positioniert) und committet bei Enter/Blur.
3-Punkt-Bogen → Zentrum: Umkreismittelpunkt der drei Punkte (Schnitt der
Mittelsenkrechten via `lineIntersect`), `r`, `a0/a1` aus Start-/Endwinkel; Drehsinn
aus dem mittleren Punkt.
## 11. Phasenplan
**Phase 1 — Gerüst + Select + Wall + Line (MVP).**
1. `ToolId`, `Tool`, `ToolContext`, `ToolPointer`, `ToolDraft`, `ToolResult`,
`SnapResult`, `SnapSettings`, `Drawing2D`(+`Project.drawings2d`) als Typen.
2. `PlanView`: `toScreen`/`viewToModel`/`pxPerMeter` als Callbacks nach außen;
Pointer-Routing für `activeTool !== "select"`; Draft-/Marker-Overlay-Rendering;
Crosshair-Cursor.
3. `ToolController` + App-State (`activeTool`, `activeCategoryCode`,
`activeWallTypeId`, `snap`) + Keyboard (Esc/Enter/Backspace).
4. **WallTool** voll funktionsfähig (Polylinie → Wände, Live-Band-Vorschau, HUD,
Commit via `appendWalls`). Verschneidung kommt automatisch aus `computeJoins`.
5. **LineTool** als erstes 2D-Werkzeug; `generatePlan.addDrawing2D` für `line`;
`Drawing2D`-Löschung beim Geschoss-Löschen.
6. **Snapping Stufe 1**: endpoint + grid + ortho (Shift), mit Bildschirm-Markern.
7. TopBar-Werkzeuggruppe (select/wall/line) + WallType-/Kategorie-Auswahl;
i18n-Keys; Statusleiste zeigt aktives Werkzeug + Kategorie.
8. Verifizieren: `npx tsc -b`, `npm run build`, Screenshot via `scripts/probe.mjs`
(Wand zeichnen, Gehrung prüfen).
**Phase 2 — Snapping vervollständigen + 2D-Grundformen.**
- Snap: midpoint, intersection, onEdge (Lot), extension-Hilfslinien, Winkelraster
(45°), Snap-Einstellungsmenü in der TopBar.
- Werkzeuge: Polyline, Rectangle (inkl. optionaler Schraffur für geschlossene
Formen). `Primitive`-Erweiterung nur für tatsächlich gebrauchte Formen.
**Phase 3 — Kurven + Text.**
- `Primitive` um `circle` (+ `arc` `largeArc`) und `text` erweitern; PlanView-
Renderzweige; Text papierkonstant.
- Werkzeuge: Circle, Arc (3-Punkt), Text (Inline-Eingabe). Snap: center/quadrant.
**Phase 4 — Bearbeitung (Folge-Doku).**
- Grips/Editieren bestehender Elemente (Wand-Enden ziehen, 2D-Vertices verschieben),
Verschieben/Kopieren/Rotieren der Auswahl, numerische Direkteingabe von
Länge/Winkel im HUD. Baut auf demselben Snapping + Draft-Pfad auf. (Eigenes
Design-Dokument; hier nur als Ausblick.)
## 12. Architektur-Garantien (Checkliste)
- Modell bleibt einzige Wahrheit; Werkzeuge schreiben nur `Project`, nie Plan-
Primitive. Ansichten (Plan/3D) leiten ab.
- Alle Bezeichner englisch; alle UI-Texte über `t(...)`; Einheiten in Metern,
Anzeige via `formatM`; Strichstärken/Texthöhen in mm Papier (non-scaling).
- Native-App-Verhalten: kein Browser-Kontextmenü während des Zeichnens; keine
Textauswahl (außer Text-Eingabefeld); Pan/Zoom immer verfügbar.
- DRY: Vorschau nutzt dieselben `Primitive` + Renderlogik wie der Plan; Snapping
und Commit-Pfad sind werkzeugübergreifend geteilt.
</content>
</invoke>
+424
View File
@@ -0,0 +1,424 @@
# Design — Bauteile (Elements)
> Teil der Standalone-Architektur — siehe [../../ARCHITECTURE.md](../../ARCHITECTURE.md).
> Output/Pläne: [plans-output.md](plans-output.md). Ressourcen/Stile:
> [resources-graphics.md](resources-graphics.md).
Dieses Dokument legt die **Daten**, die **Generierung** (3D-Geometrie + Plan-
Symbolik) und das **Grip-Editing** je Bauteil fest und übersetzt DOSSIERs
`elemente.py` (7244 LOC, Monolith) in **kleine Bauteil-Module** (`src/model/
elements/wall.ts`, `opening.ts`, …). Bezeichner englisch, Prosa deutsch, Meter.
DOSSIERs Architektur dort: pro Element eine **Achse/Outline-Source** (editierbar)
+ ein **auto-generiertes Volumen** (`wand_axis`+`wand_volume`, Outline+Brep).
Browser-Äquivalent: das **semantische Element ist die Source**; Geometrie wird per
`generate*()` **abgeleitet** (nie persistiert). Das ist sauberer als DOSSIERs
zwei-Objekt-Modell und kennt kein Cache-Stale.
---
## 0. Gemeinsames Fundament
```ts
// src/model/elements/base.ts
interface ElementBase {
id: string;
type: ElementType; // "wall" | "window" | "door" | "slab" | "stair" | "roof"
// | "column" | "beam" | "space" | "draw2d"
floorId: string; // Zeichnungsebene (Geschoss); bei gehosteten via Host
categoryCode: string; // Ebene (Grafik-Kategorie), z.B. "20"
styleId?: string; // optionaler Element-Override-Stil (resources-graphics.md)
name?: string;
}
```
**Geometrie-Konvention** (aus CONVENTIONS.md, im Spike etabliert): Wand-Normale
`n = leftNormal(u) = (-u.y, u.x)`; bei CCW-Wicklung zeigt `+n` nach innen.
Schichten werden außen (`-T/2`) → innen (`+T/2`) gestapelt (`generatePlan.addWallPoche`,
`Viewport3D.addWallMeshes`).
**Detailgrad** (LoD) — DOSSIERs `darstellung` (`auto|einfach|standard|detail`):
```ts
type DetailLevel = "coarse" | "medium" | "fine"; // einfach | standard | detail
// Auflösung: Element-Wert "auto" → Dokument-/Snapshot-Wert; sonst Element-Wert.
function resolveDetail(el: ElementBase, doc: { detailLevel: DetailLevel }): DetailLevel
```
≙ DOSSIER `_resolve_oeff_darstellung` + `get_aktive_darstellung`. Steuert, *wie
viel* Symbolik gezeichnet wird (1:500 Rechteck → 1:50 Glas/Sims/Schwenkbogen).
**Generierungs-Signaturen** (jedes Modul exportiert beides):
```ts
function build3d(project, el, ctx): THREE.Object3D // Volumen (Schichten/Brep)
function generatePlan(project, el, ctx, lod): Primitive[] // Schnittflächen + Symbol
// ctx trägt baseElevation, joins, sichtbare Codes, resolver für Components/Styles
```
---
## 1. Wand (Wall) — mehrschichtig
### 1.1 Daten
```ts
interface Wall extends ElementBase {
type: "wall";
start: Vec2; end: Vec2; // Achse (Centerline) im Grundriss [im Spike]
wallTypeId: string; // → WallType.layers (außen→innen)
height: number;
reference: "mid" | "left" | "right"; // Referenzlage der Achse (DOSSIER _wand_referenz)
baseOffset?: number; // UK relativ zu OKFF (default 0)
topOffset?: number; // OK-Override (default = floorHeight)
jointRole?: "auto" | "through" | "butt"; // T-Stoss-Rolle (DOSSIER wand_joint_rolle)
// Mehrsegment-Wände (Polyline): optional axisPoints statt start/end
axisPoints?: Vec2[];
}
```
`reference` verschiebt die Achse auf Außenkante/Mitte (DOSSIER
`_wall_offsets_from_referenz`): hilft beim Modellieren *und* beim Import fremder
Pläne (ROADMAP §11). Offsets: `mid → [+T/2, -T/2]`, `left → [0, -T]`, `right → [+T, 0]`.
### 1.2 Generierung — 3D + Plan (Status: im Spike, einschichtig→mehrschichtig ✅)
Beide Sichten extrudieren/füllen **dasselbe gehrte Band-Polygon** pro Schicht.
Heute schon vorhanden:
- `geometry.clippedBand(start, end, offA, offB, startCut, endCut)` — Band mit
Gehrungsschnitt.
- `generatePlan.addWallPoche` — pro Schicht ein gefülltes Polygon (Component-Fill +
Schraffur), Öffnungen ausgespart.
- `Viewport3D.addLayerPrism` — dasselbe Polygon via `ExtrudeGeometry`.
### 1.3 Wand-Verschneidung (Joins) — Risiko #1
**Status: L-Ecken-Gehrung ✅** (`joins.computeJoins` → `miterLine`, robust gegen
Wicklung + ungleiche Dicken). **Offen: Prioritäts-T-/X-Stöße** bei mehrschichtigen
Wänden.
DOSSIERs gelöste Logik (`elemente._t_junction_layer_overrides`, `_wand_should_apply_t_miter`),
die wir portieren:
1. **Knoten finden:** Endpunkte auf Gitter runden (`roundKey`, existiert),
gruppieren. `==1` freies Ende, `==2` L-Ecke (Gehrung, ✅), `>2` T/X.
2. **Through-Wand bestimmen:** an einem T-Stoß läuft genau **eine** Wand durch.
Auswahl nach `jointRole` (DOSSIER-Regel), sonst nach Component-`joinPriority`:
```
my.role="through" → ich laufe durch (kein Miter)
my.role="butt" → ich stoße an (Miter)
beide "auto" → höhere joinPriority = Through-Wand
```
3. **Schicht-Durchdringung (Backbone):** **nur das Material mit der höchsten
gemeinsamen `joinPriority`** in *beiden* Wänden läuft durch und unioniert
(T-Form). Beispiel ROADMAP §2d: Beton (800) läuft mittig durch; Putze (100)
verbinden sich seitlich, gehen aber nirgends durch den Beton. Alle Nicht-
Backbone-Schichten der anstoßenden Wand mitern an der Through-Außenkante
(`standard_miter`). Ergebnis: gleichfarbige Außenlagen bilden automatisch
saubere L-Stöße.
```ts
// joins.ts — Erweiterung der bestehenden API
interface WallCuts { startCut: Line|null; endCut: Line|null;
// neu: pro-Schicht Overrides am T-Stoss
layerExtensions?: number[]; // wie weit jede Schicht in Through-Body drillt
layerMiters?: (Line|null)[]; // pro-Schicht Mitre (null = Backbone, läuft durch)
}
function computeJoins(project, walls): Map<string, WallCuts> // erweitert
```
**Implementierungsplan (stufenweise, Risiko #1):**
- (a) ✅ L-Gehrung bleibt.
- (b) T-Stoß ohne Schichten: Backbone = ganze Wand; Through union, Stem mitert.
- (c) T-Stoß mit Schichten: Backbone-Material-Logik wie oben (Port von
`_t_junction_layer_overrides`).
- (d) X-Stoß: paarweise als zwei T behandeln.
- **Booleans:** Union/Extension der Backbone-Säule via **OpenCascade.js/Manifold**
im Worker (`workers/geometry.worker.ts`), nur für 3D + exakten B-Rep-Export; der
2D-Plan bleibt rein analytisch (Polygon-Clipping, kein Kernel) — schnell.
- **Validierung:** Screenshot-Probe der T-Ecke (Beton durch, Putz seitlich).
### 1.4 Grip-Editing (Risiko #L, Phase 3–4)
DOSSIER: Display-Conduit zeichnet dicke Marker an Achs-Endpunkten, MouseCallback
fängt Klick → `GetPoint` mit Snap → `_replace_axis_vertex` → Volumen regeneriert
(`wand_grips.py`). Browser-Port:
- **Marker:** SVG-Kreise (r ≈ 7 px) an Endpunkten/Knicks der *selektierten* Wand,
als Overlay über dem Plan (unabhängig von Ebenen-Sichtbarkeit) — exakt DOSSIERs
Conduit-Idee.
- **Hit-Test:** Pointer-Distanz < 14 px (DOSSIER `_HIT_RADIUS_PX`).
- **Drag:** `pointerdown` auf Marker → Live-Preview-Linien zu Nachbar-Vertices →
Snap (Endpunkt/Ortho/Raster) → `pointerup` → `store.apply(p => wall.start = newPt)`.
Abgeleitete Sichten (Plan + 3D) re-derivieren reaktiv — kein manuelles Regen.
- Funktioniert für Line (2 Grips) und Polyline (jeder Knick ein Grip), wie DOSSIER.
---
## 2. Öffnungen (Window / Door) — gehostet, LoD
### 2.1 Daten
```ts
interface OpeningBase extends ElementBase {
hostWallId: string; // Host-Wand (Geschoss ergibt sich daraus) [im Spike]
position: number; // Abstand entlang Wandachse vom Wand-Start (m)
width: number; height: number;
reference: "mid" | "left" | "right"; // Lage des Klickpunkts in der Öffnung
detailLevel: DetailLevel | "auto";
frame?: { width: number; depth: number; pos: "outer"|"mid"|"inner"; offset: number };
outerSide: "left" | "right"; // welche Wandseite ist außen
}
interface Window extends OpeningBase {
type: "window";
sill: number; // Brüstungshöhe
sashes: 1|2|3|4; // Flügelzahl
sillProfileOut?: "none"|"narrow"|"standard"|"wide"; // Sims außen (DOSSIER _OEFF_SIMS_STYLES)
sillProfileIn?: "none"|"narrow"|"standard"|"wide";
glass: boolean;
}
interface Door extends OpeningBase {
type: "door";
swing: "left" | "right"; // Anschlagseite [im Spike]
hinge: "start" | "end"; // Scharnierpfosten [im Spike]
openAngle: number; // Plan-Öffnungswinkel 0–180 (default 90)
doorType: "normal" | "wall-opening"; // Wandöffnung = ohne Blatt
frameType: "casing" | "block"; // Zarge | Blockrahmen
lintel?: "none"|"inner"|"outer"|"both";// Sturzlinien-Anzeige (DOSSIER _OEFF_STURZ)
}
```
Felder 1:1 aus DOSSIERs `_OEFF_*`-Keys + `_OEFF_STYLE_FIELDS`. Presets (Fenster
Standard/Gross/Bandlage, Tür Innen/Eingang/Verglast, Wandöffnung) als
Style-Katalog (resources-graphics.md), seed wie `_OEFF_DEFAULT_STYLES`.
### 2.2 Host-Beziehung (Risiko #2)
Die Öffnung kennt ihre Wand (`hostWallId`); ihre Geometrie wird **relativ zur
Wandachse** berechnet (`opening.axisFrame(wall, position)` → Punkt + Tangente +
Normale, ≙ DOSSIER `_oeff_axis_frame`). Verschiebt sich die Wand, folgt die
Öffnung automatisch (sie hält keinen absoluten Punkt). Beim Plan/3D wird die
Wand an `[position, position+width]` ausgespart — steht im Spike (`addWallPoche`
Segmentierung, `addWallMeshes` Sturz).
### 2.3 Generierung nach LoD
| LoD | Plan-Symbol | 3D |
|---|---|---|
| **coarse** (1:200/500) | Öffnung als Lücke + dünne Linie | Aussparung, kein Rahmen |
| **medium** (1:100) | + Rahmenlinien, Tür-Schwenkbogen (`addDoorSymbol` ✅), Sturz gestrichelt | Aussparung + einfacher Rahmen-Quader |
| **fine** (1:50) | + Glas-Doppellinie, Sims, Flügel-Teilung, Anschlag | Rahmen + Blatt + Glas (transparent) + Sims (DOSSIER `_OEFF_PIECE_DEFS`) |
- **Tür-Schwenkbogen:** im Spike (`generatePlan.addDoorSymbol` — Blatt + Arc).
Ausbau: `openAngle`, lichte vs. volle Breite je LoD (Port von
`_make_tuer_swing_curves`).
- **3D-Stücke** (Rahmen/Glas/Flügel/Sims/Sturz) ≙ DOSSIER `_make_oeffnung_pieces`
/ `_OEFF_PIECE_DEFS` — jeweils eigene Component (Farbe + Transparenz: Glas
α≈0.88, IOR 1.5). Pieces landen auf Unter-Ebenen von `21 Türen/Fenster`.
---
## 3. Decke / Boden (Slab) — mit Aussparungen
### 3.1 Daten
```ts
interface Slab extends ElementBase {
type: "slab";
boundary: Vec2[]; // geschlossener Umriss (CCW)
slabTypeId: string; // mehrschichtig (analog WallType)
openings?: Vec2[][]; // Aussparungen: Treppenauge, Schacht, Kamin (DOSSIER aussp)
ukOverride?: number; okOverride?: number; // UK/OK statt auto (Abhängung, schräge Brüstung)
}
```
### 3.2 Generierung
- **Z-Auflösung:** `okOverride ?? (baseElevation_oberes_Geschoss)`,
`ukOverride ?? (ok - thickness)` — Port von `_resolve_decke_z`. Decke sitzt
standardmäßig zwischen zwei Geschossen.
- **3D:** `boundary` als `THREE.Shape`, Aussparungen als `shape.holes` (`THREE.Path`),
`ExtrudeGeometry` über die Schichten (≙ `_make_decke_volume(outline, holes)`).
- **Plan:** im Schnitt unter `cutHeight` meist nur Kante; Aussparungs-Ränder als
Linien; geschnittene Decke (in Schnitt-Ansicht) bekommt Schraffur.
- **Aussparung↔Decke:** Aussparung als geschlossene Curve, die räumlich in der
Decke liegt (`_find_decke_containing_point` / `_find_aussparungen_for_decke`).
Bei uns: `Slab.openings` direkt im Slab — keine separate Source nötig (einfacher
als DOSSIERs Parent-Child).
---
## 4. Treppe (Stair) — Typen, Lauflinie, geschossübergreifend
### 4.1 Daten
```ts
interface Stair extends ElementBase {
type: "stair";
kind: "straight" | "l-shaped" | "spiral"; // gerade | L | Wendel (DOSSIER _TREPPE_ARTEN)
run: Vec2[]; // Lauflinien-Stützpunkte (gerade: 2; L: 3; Wendel: Zentrum+Start)
width: number;
reference: "mid" | "left" | "right"; // Lage der Lauflinie zur Treppe
steps: number; // Anzahl Steigungen
mode: "solid" | "flat" | "slab-edge"; // massiv | flach | Plattenrand
runSlabThickness?: number; // Lauf-Plattendicke
floorEndId?: string; // Zielgeschoss (geschossübergreifend, Risiko #6)
heightOverride?: number; ukOverride?: number;
rules?: { riser:[lo,hi,on]; tread:[lo,hi,on]; stepGo:[lo,hi,on] }; // SIA-Komfortregeln
lockRiser?: { value: number }; // Schrittmass-Lock (S fix, N passt sich an)
// Plan-Symbol-Flags (DOSSIER _KEY_TREPPE_SHOW_*)
show?: { treads; runLine; outline; breakLine };
upperDashed?: boolean; // obere Stufen gestrichelt (über Schnitthöhe)
arrowStyle?: "classic"|"filled"|"double"|"line";
}
```
### 4.2 Generierung
- **Steigung/Auftritt:** `riser = height/steps`; `tread` aus Lauflinienlänge /
(steps−1). SIA-Komfort: `2·riser + tread ∈ [0.60, 0.65]` (DOSSIER
`_TREPPE_SOLL_DEFAULT`). Lock: ist `lockRiser` gesetzt, wird `steps` neu
berechnet statt `riser` zu ändern.
- **3D je `kind`:** gerade → Stapel von Tritt-Quadern oder massive Rampe;
L → zwei Läufe + Podest (`podestMin`); Wendel → um Zentrum rotierte Tritte
(Port `_make_treppe_*_preview` / Volume-Funktionen). `mode` steuert massiv vs.
Lauf-Platte.
- **Geschossübergreifend (Risiko #6):** Höhe = `(baseElevation[floorEndId] -
baseElevation[floorId])` falls `floorEndId` gesetzt; sonst Geschosshöhe. Treppe
taucht dann in beiden Geschoss-Grundrissen auf (mit Schnitt an `cutHeight`).
- **Plan-Symbol (normgerecht):** Lauflinie mit **Auf-/Abpfeil** (`arrowStyle`),
Stufenkanten, Bruchlinie an `cutHeight` (untere durchgezogen, obere gestrichelt
via `upperDashed`), Außenkante. ≙ DOSSIERs 2D-Treppensymbol; liegt auf Ebene
`40 Treppen`/`41 Treppen-2D`.
### 4.3 Grip-Editing
Lauflinien-Stützpunkte als Grips (wie Wand-Vertices, §1.4); Ziehen ändert
Geometrie + Stufenzahl reaktiv.
---
## 5. Dach (Roof)
### 5.1 Daten
```ts
interface Roof extends ElementBase {
type: "roof";
outline: Vec2[]; // Grundriss-Umriss
roofType: "mono" | "gable" | "hip" | "mansard"; // Pult|Sattel|Walm|Mansarde
thickness: number;
slope: number; // Grad (Hauptneigung)
eaveIndex?: number; // Index der Traufkante (Pult)
ridge?: "long" | "short"; // Firstrichtung (Sattel)
// Mansarde:
slopeLower?: number; kinkHeight?: number;
mansardVariant?: "hip" | "gable" | "hip-gable";
}
```
### 5.2 Generierung
Port von DOSSIERs `_make_pultdach/_satteldach/_walmdach/_mansardendach*` +
`_thicken_roof_inward`. Aufwand M–L (Mansarde später). Reihenfolge: Pult →
Sattel → Walm → Mansarde. 3D als Brep/Mesh über OpenCascade.js (Worker), da
Schräg-Verschneidung Booleans braucht. Plan: Firstlinien + Traufe + ggf.
Höhenkoten.
---
## 6. Tragwerk (Column / Beam)
### 6.1 Daten
```ts
interface ProfileDef {
shape: "square"|"rect"|"round"|"i-beam"|"tube"; // DOSSIER _TRAG_PROFILE
b?: number; h?: number; d?: number; t?: number; // Breite/Höhe/Durchm./Wanddicke
angle: number; // Rotation um Z
}
interface Column extends ElementBase { type:"column"; point: Vec2; profile: ProfileDef;
uk?: number; ok?: number; }
interface Beam extends ElementBase { type:"beam"; axis:[Vec2,Vec2]; profile: ProfileDef;
zTop?: number; // hängt unter Decken-OK (zTop = ok der Decke)
}
```
### 6.2 Generierung
- **Querschnitt:** `profileCurve(shape, b,h,d,t, angle)` (Port `_trag_profile_curve`)
→ für Stütze entlang Z extrudieren (`_make_stuetze_volume`), für Träger entlang
der Achse (`_make_traeger_volume`, Profil in der Schnitt-Ebene).
- **Träger achs-basiert unter Decke:** `zTop` default = OK der darüberliegenden
Decke → Unterzug folgt automatisch (weniger Update-Fehler, ROADMAP §11).
- Stützen liegen auf `25 Stützen`, Träger auf `35 Träger`.
---
## 7. Raum (Space) — SIA-416 + Stempel
### 7.1 Daten
```ts
interface Space extends ElementBase {
type: "space";
boundary: Vec2[]; // geschlossener Umriss
number?: string; spaceName?: string; function?: string;
sia?: "" | "HNF"|"NNF"|"VF"|"FF"|"GF"|"AGF"; // SIA-416-Klasse
persons?: number; // Personenbelegung (Brandschutz)
areaRounding: "exact"|"0.01"|"0.1"|"0.5"|"1";
stamp: StampConfig; // Raumstempel-Layout (s.u.)
fill?: string; // Füll-Hatch-Id (optional)
}
interface StampConfig { // ≙ DOSSIER Stempel-Builder
layout: FieldId[][]; // Zeilen × Felder, z.B. [["number","name"],["function"],["area"]]
font; bold; italic; textHeight; textMode:"fixed"|"scale"; align:"left"|"mid"|"right";
offset: Vec2; // Stempel-Position relativ zum Centroid (User-Move)
}
type FieldId = "number"|"name"|"function"|"area"|"sia";
```
### 7.2 Generierung & Bilanz
- **Fläche:** Shoelace-Formel über `boundary`, gerundet nach `areaRounding`
(`_resolve_raum_rundung`). Umfang analog.
- **Stempel:** als SVG-Text-Block aus `layout`-Zeilen am Centroid + `offset`
(User kann verschieben; Offset persistiert wie DOSSIER `stamp_dx/dy`).
`textMode:"scale"` → Texthöhe in Paper-mm × Massstab (plans-output.md).
- **SIA-Färbung:** über die **Overrides-Engine** (regelbasiert), nicht hartcodiert
— DOSSIER `_build_sia_preset_rules` erzeugt 4 Regeln `userString sia == hnf|nnf|vf|ff`
→ Farbe + Solid-Hatch. Bei uns: ein Override-Preset „SIA-416" (resources-graphics.md),
das auf `space.sia` matcht. Toggle = Preset aktivieren.
- **SIA-Bilanz + CSV:** `panels/SiaBalance.tsx` summiert Flächen je Klasse je
Geschoss → Tabelle + CSV-Export (`HNF/NNF/VF/FF/GF/AGF`). Pflicht für
CH-Flächennachweis (ROADMAP ⭐).
---
## 8. Werkzeuge (Tools) — ersetzt Rhino-Command-Aliases
DOSSIER hat pro Bauteil ein Command-Alias (`rhino/aliases/cmd/wand.py`, `tuer.py`,
`treppe.py`, …) das `GetPoint`-Interaktionen fährt. Browser: ein **Tool-Interface**
mit Pointer-Handlern + Snap.
```ts
interface Tool {
id: ToolId;
onPointerDown(pt: Vec2, snap: SnapResult, state): void;
onPointerMove(pt: Vec2, snap: SnapResult, state): Primitive[]; // Live-Preview
onPointerUp(pt: Vec2, snap: SnapResult, state): void;
commit(store): void; // ruft store.apply()
}
```
| Tool | DOSSIER-Alias | Kurzbeschrieb |
|---|---|---|
| `wall` | `cmd/wand` | Achse zeichnen (Linie/Polyline), Dicke/Referenz/Typ aus „last used" |
| `door`/`window` | `cmd/tuer`,`fenster` | Punkt auf Wandachse → hosten (Snap an Wand) |
| `slab` | `cmd/decke` | Umriss klicken; Aussparung als Loch |
| `stair` | `cmd/treppe` | Lauflinie + Breite + Stufen |
| `roof` | `cmd/dach` | Umriss + Typ + Neigung |
| `column`/`beam` | `cmd/stuetze`,`traeger` | Punkt / Achse + Profil |
| `space` | `cmd/raum` | Umriss → Fläche auto, Stempel |
| `draw2d` | `cmd/symbol`,`stempel` | Linie/Polyline/Rect/Kreis/Bogen/Text auf `60 Plangrafik` |
| `pipette` | `cmd/pipette` | Stil/Typ von Element übernehmen |
**Snap-Engine** (`tools/snap.ts`): Endpunkt, Mitte, Schnitt, senkrecht, Raster,
Ortho — ersetzt Rhinos OSnap. T-Snap an andere Wandachsen (Port
`_t_snap_to_wand_axis`, `_snap_endpoint_to_other_wand_axis`) sorgt für saubere
Knoten.
---
## 9. Element-Übersicht (BIM-Tree)
`panels/ElementTree.tsx`: Baum Geschoss → Bauteiltyp → Element, mit Suche und
Shift-Klick = Zoom (DOSSIER ELEMENTE-ÜBERSICHT). Inhaltsverzeichnis bei 100+
Elementen — reine Ableitung aus `project.elements`.
---
## 10. Reihenfolge der Umsetzung (verweist auf ROADMAP-Phasen)
1. **Phase 1:** Wand mehrschichtig ✅ + L-Gehrung ✅ → **Prio-T-Stoß** (§1.3);
Tür/Fenster gehostet (§2); Decke + Aussparung (§3); Wand-Referenzlage (§1.1);
Element-Übersicht (§9).
2. **Phase 2:** Treppe (§4), Dach (§5), Tragwerk (§6), SIA-Räume + Stempel (§7),
Stil-Kataloge.
3. **Phase 3–4:** Grip-Editing (§1.4/§4.3), exakte B-Rep-Booleans im Worker.
+137
View File
@@ -0,0 +1,137 @@
# Engine-Nordstern — Headless-Rendering (PNG-Export & Golden-Image-Tests)
> Betrifft `src-tauri/render2d` (Feature `headless`). Bezug: HANDOVER.md,
> Abschnitt ENGINE-NORDSTERN, Punkt 3 ("deterministisches Headless-Rendering,
> PNG-Export und Golden-Image-Tests ohne Fenster").
## Warum
`render2d` trennt die serde-only-Tessellierung (Feature-los, headless testbar)
von der GPU-Schicht (Feature `render`, wgpu). Der Fenster-Pfad (`gpu::Renderer`,
Feature `window`) braucht dafür bislang eine `wgpu::Surface` — also ein echtes
Fenster mit Wayland-/X11-Session. Für deterministische PNG-Exporte und
Golden-Image-Tests (CI, Regressions-Screenshots) ist das unnötig: wgpu kann
genauso gut in eine `wgpu::Texture` rendern, ganz ohne Surface/Fenster.
`gpu::Renderer::render` nahm bereits vorher nur `Device`/`Queue`/`TextureView`
entgegen — Surface- oder Offscreen-Textur macht für den Draw-Code keinen
Unterschied. Der Offscreen-Pfad (`src/headless.rs`) dupliziert daher NICHTS,
sondern baut nur Device/Queue ohne Surface sowie eine eigene Ziel-Textur +
Buffer-Readback drumherum.
## Feature-Gating
Neues Cargo-Feature `headless = ["render", "dep:image"]`:
- zieht `render` (wgpu/glyphon/bytemuck/pollster) plus die `image`-Crate
(nur der PNG-Codec, `default-features = false, features = ["png"]`).
- **berührt den wasm/web-Build nicht**: `cargo check --target wasm32-unknown-unknown
--no-default-features --features web` zieht `image` nicht mit.
- Binary `render_png` und Test `golden` sind zusätzlich per
`required-features = ["headless"]` in `Cargo.toml` abgesichert.
## API
```rust
pub struct HeadlessRenderer { /* Device, Queue, gpu::Renderer */ }
impl HeadlessRenderer {
/// Instance/Adapter/Device OHNE Surface (Backend fest auf Vulkan gepinnt).
/// `Err`, wenn kein Adapter verfügbar ist (z.B. CI-Runner ohne GPU) — die
/// Aufrufer (CLI, Golden-Test) behandeln das, statt zu paniken.
pub fn new() -> Result<Self, String>;
/// Rendert `scene` in ein `width`x`height`-Bild (Papier-Maßstab
/// `paper_scale_n`, z.B. `100.0` für 1:100). Lädt die Szene bei jedem Aufruf
/// neu hoch (kein Zwischenzustand nötig für CLI-/Test-Anwendungsfall).
pub fn render_to_image(
&mut self,
scene: &Scene,
width: u32,
height: u32,
view_box: ViewBox,
paper_scale_n: f32,
) -> RgbaImage; // { width, height, pixels: Vec<u8> (straff gepackt, RGBA8) }
}
impl RgbaImage {
pub fn encode_png(&self) -> Vec<u8>;
/// Gegenstück für den Golden-Test: Referenz-PNG -> straff gepacktes RGBA8.
pub fn decode_png(bytes: &[u8]) -> Result<Self, String>;
}
```
Backend bewusst auf **Vulkan** gepinnt (nicht das Standard-Backend-Set): Vulkan
rendert offscreen ohne jede Fenster-/Display-Server-Abhängigkeit. GL bräuchte auf
Linux i.d.R. einen EGL/GLX-Kontext, der ohne aktive Display-Session (X11/Wayland)
Sonderfälle hat — auf dieser Maschine ist ohnehin ein echter Vulkan-ICD (RADV)
vorhanden, daher kein Rückgriff auf `lavapipe`/`llvmpipe` nötig gewesen.
## Row-Alignment-Falle (wichtig für zukünftige Agenten)
`wgpu::Queue::copy_texture_to_buffer` / `CommandEncoder::copy_texture_to_buffer`
verlangt, dass `bytes_per_row` im `ImageDataLayout` ein Vielfaches von
`wgpu::COPY_BYTES_PER_ROW_ALIGNMENT` (256) ist. Bei RGBA8 (4 Byte/Pixel) trifft
das **nur zufällig** zu — z.B. Breite 300 px → 1200 Byte/Zeile, kein Vielfaches
von 256, der Copy schlägt sonst mit einem Validierungsfehler fehl (oder liefert
verzerrte Zeilen, je nach Backend).
Lösung in `headless::HeadlessRenderer::read_pixels`:
1. `padded_bytes_per_row = align_up(width * 4, 256)` — der Zielpuffer wird auf
diese (größere oder gleiche) Zeilenbreite alloziert.
2. Nach dem Mapping wird **jede Zeile einzeln** von `padded_bytes_per_row` auf
die echte Breite (`width * 4`) zurückgeschnitten und in einen straff
gepackten `Vec<u8>` kopiert — sonst hätte das Ausgabebild pro Zeile
Garbage-Padding am rechten Rand.
## Referenzbild neu erzeugen
Bei einer **gewollten** Rendering-Änderung (z.B. neue Shader, neue Demo-Szene,
geänderte Antialiasing-Parameter) weicht das Golden-Bild ab. Referenzbild
danach bewusst neu erzeugen:
```sh
cd src-tauri/render2d
cargo run --features headless --bin render_png -- --width 990 --height 630 --out target/headless-demo.png
cp target/headless-demo.png tests/golden/demo.png
```
Vorher unbedingt das erzeugte PNG (`target/headless-demo.png`) visuell prüfen
(z.B. mit einem Bildbetrachter oder Read-Tool eines Agenten) — nicht blind
kopieren. Bei einem fehlgeschlagenen Testlauf schreibt der Golden-Test
automatisch ein Diff-Bild nach `target/golden-diff.png` (abweichende Pixel
signalrot markiert, Rest unverändert) — das hilft beim Einordnen, ob die
Abweichung erwartet (Layout-/Farbänderung) oder ein Bug ist (z.B. verschobene
Geometrie, fehlender Text).
## Golden-Test
`tests/golden.rs`, Test `demo_szene_entspricht_referenzbild`:
- rendert die Demo-Szene (`demo::demo_scene`, dieselbe wie der Fenster-Spike)
in 990×630 und vergleicht sie gegen `tests/golden/demo.png`.
- Toleranz: ein Pixel gilt als "abweichend", wenn irgendein Kanal-Delta > 2 ist
(deckt AA-Rundungsrauschen ab); der Test schlägt fehl, wenn mehr als 0,5%
aller Pixel abweichen.
- `#[ignore]`, weil eine Vulkan-fähige GPU auf CI-Runnern nicht garantiert ist
(und ein Software-Rasterizer wie llvmpipe anderes AA liefern würde als das
Referenzbild). Lokal explizit ausführen:
`cargo test --features headless -- --include-ignored`.
- Bei Abweichung über der Toleranz schreibt der Test zusätzlich zum Diff-Bild
auch das Ist-Bild nach `target/golden-actual.png` — nach visueller Prüfung
ist das die Vorlage für eine gewollte Referenz-Aktualisierung.
- `HeadlessRenderer::new()` liefert bei fehlendem Adapter `Err` statt Panik —
der Test überspringt sich dann sauber (`eprintln!` + `return`) statt rot zu
schlagen, falls doch mal ohne `--ignored`/ohne GPU ausgeführt.
## Offener Punkt: Text-Determinismus zwischen GPU-Treibern
Der Textpass läuft über `glyphon`/`cosmic-text` (Systemfonts, `Inter`). Die
exakte Glyphen-Rasterisierung (Subpixel-Antialiasing, Hinting) kann je nach
installierter Font-Version, Fontconfig-Konfiguration und GPU-Treiber leicht
variieren — auch bei identischer Geometrie. Auf **derselben** Maschine (gleicher
Treiber, gleiche Font-Version) ist der Test wie erwartet bit-nah deterministisch
(hier: 0 abweichende Pixel bei zwei aufeinanderfolgenden Läufen). Bei einem
Wechsel der Maschine/des Treibers/der Fontconfig-Version ist nicht
auszuschließen, dass die 0,5%-Toleranz für Textkanten knapp wird — sollte das
in der Praxis auftreten: entweder Toleranz für den Text-Bereich lockern, oder
den Text testweise aus der Golden-Szene ausklammern und separat (z.B. nur
Zeilenbreite/Position, nicht Pixel) prüfen.
+186
View File
@@ -0,0 +1,186 @@
# Schnitt/Ansicht-Pipeline: analytische Prismen-Projektion (`render3d::section`)
> Gegenstueck zum OCCT-WASM-HLR-Spike (`docs/welle-c-hlr-spike/FEASIBILITY.md`):
> statt eines generischen CAD-Kernels nutzt dieser Ansatz eine Invariante des
> Modells, um Schnitt/Ansicht rein analytisch (kein Hidden-Line-Removal-Solver)
> zu berechnen. Implementiert in `src-tauri/render3d/src/section.rs`.
## Ansatz
Jedes Bauteil in diesem Modell ist ein **Prisma**: ein 2D-Grundriss-Fussabdruck-
Polygon (Wand-Band bzw. Decken-Umriss, siehe `mesh.rs`), konstant extrudiert
ueber ein Hoehenintervall `[z0, z1]`. Diese Einschraenkung — konstanter
Querschnitt ueber die gesamte Hoehe — macht die Schnittgeometrie trivial im
Vergleich zu generischem HLR:
- **Schnitt einer vertikalen Ebene mit einem Prisma** = 2D-Geraden/Polygon-
Clipping IM GRUNDRISS (die Schnittebene projiziert im Grundriss auf eine
Linie) → ein oder mehrere u-Intervalle, in denen die Linie das Fussabdruck-
Polygon durchquert. Jedes Intervall × `[z0, z1]` ist das Cut-Rechteck. Da die
Hoehe unabhaengig von der Grundriss-Position ist, ist das Ergebnis **immer
ein Rechteck**, nie ein Trapez — auch bei diagonalen Wandachsen.
- **Projektion/Ansicht** (Kanten hinter der Ebene, in Blickrichtung) reduziert
sich auf ein **2D-Sichtbarkeitsproblem im Grundriss** kombiniert mit einem
Hoehen-Ueberlappungstest: da alle Seitenflaechen der Prismen vertikal sind,
genuegt ein Sichtstrahl-Test im Grundriss (verdeckt ein naeheres Prisma-
Fussabdruckpolygon die Sichtlinie?) plus Ueberlappung der Hoehenintervalle.
Das ersetzt einen 66-MB-WASM-CAD-Kernel durch closed-form Arithmetik in reinem
Rust, ohne zusaetzliches Laufzeitgewicht ueber `render3d` hinaus.
## `SectionOutput`-Format
Modul: `render3d::section`. Alle Groessen in **Metern**.
```rust
pub struct SectionPlane { pub point: [f32; 3], pub normal: [f32; 3] }
pub struct SectionOutput {
pub cut_polygons: Vec<CutPolygon>, // JSON: "cutPolygons"
pub visible_edges: Vec<SectionEdge>, // JSON: "visibleEdges"
pub hidden_edges: Vec<SectionEdge>, // JSON: "hiddenEdges"
}
pub struct CutPolygon { pub component: ComponentRef, pub color: Rgb, pub pts: Vec<[f32; 2]> }
pub struct SectionEdge { pub component: ComponentRef, pub a: [f32; 2], pub b: [f32; 2] }
pub struct ComponentRef { pub kind: ComponentKind /* Wall | Slab */, pub index: usize }
```
**Koordinatensystem der Ausgabe (u, v):**
- Ursprung: `SectionPlane::point`, projiziert.
- `u` (horizontal): Strecke entlang der Schnittebene, senkrecht zur
Blickrichtung, berechnet als `normalize(cross(normal, world_up))` — dieselbe
rechtshaendige Konvention wie `math::look_at`s `right`-Vektor. Bei den vier
Standard-Konstruktoren (`looking_plus_x/minus_x/plus_y/minus_y`) ist `u`
direkt die jeweils andere Grundriss-Achse.
- `v` (vertikal, "Hoehe"): `v = world.y`, ABSOLUT. Diese Codebasis ist
durchgaengig Y-up (`types.rs`/`mesh.rs`/`math.rs`: „world.y = Hoehe"); `v`
folgt bewusst dieser etablierten Konvention statt einer wortwoertlichen
„world.z"-Lesart, um modulübergreifend konsistent zu bleiben.
- Nur **vertikale** Schnittebenen (Normale ohne Hoehen-Komponente) sind
unterstuetzt — Grundriss-/Horizontalschnitte bleiben Sache der bestehenden
2D-Plan-Pipeline.
`cut_polygons` sind bei diesem Modell immer Rechtecke (siehe oben), aber als
generischer Punktering abgelegt — kompatibel zu `render2d::types::FillPolygon`
(`pts: Vec<Point>`), dem vorgesehenen Zielformat fuer die spaetere 1:1-
Uebersetzung in die Plan-/Schnitt-Ansicht.
## Oeffnungen (Tueren/Fenster)
`WallInput` hat seit diesem Nachtrag ein Feld `openings: Vec<Opening>`
(`types.rs`), unabhaengig von der reinen Achse/Dicke/Hoehe: je Oeffnung ein
Intervall entlang der Wandachse (`from`/`to`, Meter ab `start`) plus Bruestungs-
und Kopfhoehe (`sill`/`height`, relativ zu `base_elevation`). Eine Tuer ist der
Sonderfall `sill == 0.0`.
**Cut-Polygone.** Trifft die Schnittlinie eine Wand exakt im Bereich einer
Oeffnung, splittet `wall_cut_rectangles` das sonst einzelne Vollrechteck
`[z0,z1]` in bis zu ZWEI Teil-Rechtecke: Bruestung `[z0, z0+sill]` und Sturz
`[z0+sill+height, z1]`. Gewaehlt wurde diese Mehrfach-Rechteck-Darstellung
BEWUSST gegenueber einem Loch-Polygon (Ring mit Aussparung): mehrere einfache,
konvexe Ringe sind direkt kompatibel zum Zielformat `render2d::types::
FillPolygon` (nur einfache Ringe, kein Loch-Format), waehrend ein
Loch-Polygon eine zusaetzliche Datenstruktur (Ring-mit-Loch) noetig gemacht
haette, die die Zielstruktur (noch) nicht kennt. Ausserhalb einer Oeffnung
bleibt es beim einzelnen Vollrechteck (Regressionsfall).
Die Zuordnung "welche Wandachsen-Position entspricht dieser Schnittposition"
ist fuer den STANDARDFALL (Schnittebene senkrecht zur Wandachse — der uebliche
architektonische Wandquerschnitt) EXAKT konstant ueber das Cut-Intervall. Fuer
schraege Wand/Ebene-Kombinationen wird sie als AFFINE Funktion (`axis_map`)
exakt mitgefuehrt (keine Naeherung noetig, da die Abbildung linear ist).
**Projektion/Ansicht.** Eine Wand mit Oeffnungen bekommt zusaetzlich zur
Boxen-Drahtsilhouette die vier Rahmenkanten jeder Oeffnung (zwei Leibungen,
Sturz, Bruestung), berechnet auf der Wandachsen-Mittellinie (dieselbe
Vereinfachung wie die Bounding-Box-Verdeckung generell — keine eigene
Dicken-Aufloesung der Leibungsflaeche). Fuer die Verdeckung wird jede
Wand-Bounding-Box um ihre Oeffnungen als "Loch" reduziert
(`PrismBounds::opening_voids`): ein anderes (oder dasselbe) Bauteil hinter der
Wand wird in genau dem (u, Hoehe)-Rechteck der Oeffnung NICHT von dieser Wand
verdeckt — "Durchblick". Die eigenen Rahmenkanten einer Oeffnung werden dabei
NICHT gegen das eigene Bauteil auf Verdeckung geprueft (ein Loch kann sich
nicht selbst verdecken); gegen alle anderen Bauteile gilt die normale
Verdeckungslogik unveraendert (inkl. der bereits dokumentierten
Selbstverdeckung der Rueckseite eines ANDEREN Bauteils durch dessen eigene
Vorderseite).
GENAUIGKEIT (bewusste Vereinfachung): die u-Zuordnung fuer den Durchblick ist
nur DANN aussagekraeftig, wenn die Wandachse hinreichend parallel zur u-Achse
der Schnittebene steht — das ist GENAU der Fall, in dem man die Wand als
Elevation/Ansicht von vorne sieht (und ein Fenster ueberhaupt als Durchblick
sichtbar waere). Steht die Wand naeher an "senkrecht zur u-Achse" (Wand auf
Kante gesehen bzw. der reine Cut-Fall), wird die Durchblick-Berechnung
uebersprungen und die Wand bleibt fuer die Verdeckung VOLL UNDURCHSICHTIG
(konservativ hidden) — Schwelle `AXIS_ALIGN_EPS = 1e-3` in `section.rs`. Ein
Kante-auf-Kante gesehenes Fenster liefert ohnehin keine sinnvolle
Durchblick-Flaeche in der Projektion.
Getestet in `section::tests` (u. a. `schnitt_durch_fenster_liefert_bruestung_
und_sturz`, `schnitt_neben_dem_fenster_liefert_volles_rechteck`,
`ansicht_zeigt_fensterrahmen_sichtbar_und_durchblick_bei_dahinterliegender_
kante`) und im Beweis-SVG (`examples/section_svg.rs`): Schenkel A der L-Wand
hat dort ein Fenster, eine kurze zusaetzliche Wand steht dahinter genau im
Fensterband und erscheint im SVG als durchgezogene (sichtbare) Linie zwischen
Bruestungs- und Sturzhoehe, gestrichelt (verdeckt) darueber/darunter.
## Bekannte Luecken
- **Keine Wandknoten-Verschneidung (T-/X-Stoesse).** Wie in `mesh.rs`
(M1-Stand) werden Waende als eigenstaendige, stumpf abgeschlossene Quader
behandelt, die sich an Ecken UEBERLAPPEN statt sich zu vereinen (kein
Miter-Join). Der Schnitt-Extraktor erbt das: an einem L-/T-Knoten kann eine
Wand als „in die andere eingebettet" verdeckt erscheinen (im Testmodul
`section::tests` bewusst als reales, erwartetes Verhalten dokumentiert und
geprueft — kein Bug dieses Moduls, sondern ein Artefakt der fehlenden
Verschneidungslogik weiter oben in der Pipeline).
- **Oeffnungen sind NICHT in der Vollkoerper-Extrusion (`mesh.rs`)
nachgezogen.** `wall_prism`/`wall_cut_rectangles`/die Verdeckung in
`section.rs` werten `WallInput::openings` vollstaendig aus (siehe Abschnitt
"Oeffnungen" oben), aber `mesh::extrude_wall` extrudiert weiterhin die volle
Wandflaeche ohne Aussparung (`mesh.rs`-Moduldoc: „Oeffnungen kommen in
spaeteren Milestones"). Cut-/Ansichts-Pipeline und 3D-Solid-Mesh sind bis zum
Nachziehen von `mesh.rs` also bewusst inkonsistent — eine bekannte, separate
Luecke (nicht Gegenstand dieses Nachtrags).
- **Oeffnungs-Durchblick nutzt dieselbe achsparallele Bounding-Box-Naeherung**
wie die allgemeine Verdeckung (kein exaktes Polygon-Clipping der Lochflaeche
gegen dahinterliegende Kanten) und wird bei Wand-Orientierungen nahe
"senkrecht zur u-Achse" konservativ auf "kein Durchblick" zurueckgestuft
(siehe Abschnitt "Oeffnungen" oben, `AXIS_ALIGN_EPS`).
- **Keine echte Component-/Material-Id.** `WallInput`/`SlabInput` haben aktuell
keine eigene Id; `ComponentRef` referenziert daher nur `(Art, Index im
Eingabe-Array)` + die rohe Albedo-Farbe als Material-Platzhalter. Sobald ein
echtes Ressourcen-/Material-System existiert, sollte `ComponentRef` auf eine
stabile Id umgestellt werden (Indizes sind nicht stabil ueber Modell-Edits).
- **Verdeckungstest nutzt Bounding-Boxen, nicht die exakte Fussabdruckform.**
Fuer Wand-Baender und (i. d. R. konvexe) Deckenumrisse ist das exakt; bei
stark konkaven oder diagonalen Grundrissen kann es zu Ueberverdeckung
fuehren (ein Prisma blockiert dann auch Bereiche seiner eigenen „Nischen").
- **Kein generisches HLR.** Der Ansatz funktioniert NUR, weil alle Bauteile
Prismen mit konstantem Querschnitt sind. Fuer echte gekrümmte oder nicht-
prismatische Geometrie (Bogenwaende, Freiformdaecher, …) waere er nicht
anwendbar — dafuer bliebe der OCCT-Weg (oder eine eigene, generischere
HLR-Implementierung) die Referenz.
- **Keine robuste Sonderfallbehandlung fuer Vertices exakt auf der
Schnittlinie** (Toleranz-basiert, kein Tie-Breaking/Pertubation) — die
Testszenarien vermeiden diesen Fall bewusst.
## Vergleich zum OCCT-WASM-Spike
| | OCCT-WASM (`docs/welle-c-hlr-spike`) | Dieser Ansatz (`render3d::section`) |
|---|---|---|
| Verfahren | Generisches HLR (`HLRAppli_ReflectLines`) | Analytische Prismen-Projektion |
| Anwendbarkeit | Beliebige BREP-Geometrie | Nur Prismen (konstanter Querschnitt über Höhe) |
| Zusaetzliches Gewicht | ~62,8 MB WASM (~19,6 MB gzip), separater Lazy-Chunk | Keins — reines Rust in `render3d`, kein weiteres WASM-Asset |
| Modul-Init | ~450 ms (Browser) / ~680 ms (Node) | Kein Initialisierungsschritt (kein Fremd-Modul zu laden) |
| Rechenzeit (L-Wand + Platte) | ~21 ms (reiner HLR-Lauf, gemessen im Browser) | Nicht separat gemessen (kein Millisekunden-Timer im Test); die Operationen sind reine Vektor-/Intervall-Arithmetik über wenige Kanten (O(Anzahl-Prismen × Kanten-pro-Prisma) mit kleinen Konstanten) und liegen der Groessenordnung nach klar unter 1 ms fuer Szenen dieser Groesse — eine belastbare Messung steht noch aus |
| Sichtbarkeit (verdeckte Kanten) | Exakt (echter HLR-Solver) | Naeherung über Bounding-Boxen im Grundriss + Hoehenintervall-Ueberlappung; exakt fuer achsparallele/konvexe Fussabdruecke |
| Reifegrad | Isolierter Spike, nicht verdrahtet | Isolierter Spike (dieses Modul), nicht in `render2d`/die Plan-Ansicht verdrahtet |
**Fazit:** Fuer den ueberwiegenden Regelfall dieses Projekts (Waende, Decken —
alles Prismen) ist die analytische Loesung der pragmatischere Weg: kein
zusaetzliches WASM-Gewicht, keine Fremd-Bibliothek, headless testbar wie der
Rest von `render3d`. Der OCCT-Weg bleibt die Referenz, falls/sobald echte
generische Volumenkoerper (Booleans, gekrümmte Flaechen) ins Modell kommen.
+498
View File
@@ -0,0 +1,498 @@
# Design — Ebenen-Darstellung (Layer Display Settings)
> Teil der Standalone-Architektur — siehe [../../ARCHITECTURE.md](../../ARCHITECTURE.md).
> Ressourcen/Stile: [resources-graphics.md](resources-graphics.md). Output/Pläne:
> [plans-output.md](plans-output.md). Kontextmenü/Inline-Editor:
> [context-menu.md](context-menu.md).
Dieses Dokument spezifiziert die **per-Ebene Darstellungseinstellungen** auf der
`LayerCategory` (Grafik-Kategorie) und den dazugehörigen Editor
„Ebeneneinstellungen…", der aus dem Ebenen-Kontextmenü geöffnet wird.
Heute trägt jede `LayerCategory` nur eine flache Strichstärke (`lw`), eine `color`
und eine optionale `hatch` (ein freier String, der nirgends aufgelöst wird). Das
reicht nicht: Eine Ebene soll — wie in Vectorworks/DOSSIER — einen vollständigen
**Stift (PEN)** und eine vollständige **Standard-Schraffur (HATCH)** definieren,
die beim Rendern angewandt werden. Bezeichner englisch, Prosa/UI-Text deutsch
(CONVENTIONS.md).
---
## 1. Zielbild
Jede Ebene definiert zwei Darstellungs-Aspekte, die in den Grundriss-Generator
einfließen:
- **PEN** — Linienstil der Ebene: `type` (durchgezogen / gestrichelt / …),
`color` und `lw` (Strichstärke in mm Papier). Steuert alle Umriss-/Symbol-Linien
der Elemente dieser Ebene (Wand-Umriss, Tür-Symbol, Referenzlinie).
- **HATCH** — Standard-Schraffur der Ebene: `pattern`, `scale`, `angle` und die
`lineWeight` der Musterlinien. Wird angewandt, wo ein Element keine eigene
Schraffur aus einem `Component` mitbringt (z. B. einschichtige/„grob"-Flächen,
reine 2D-Zeichnungsobjekte einer Ebene).
Beides folgt dem Architektur-Prinzip: **Darstellung wird beim Rendern aufgelöst,
nie in die Geometrie eingebacken.**
---
## 2. Reference vs. Inline — Entscheidung
Es gibt drei Modelle, ein Datum für PEN/HATCH einer Ebene zu halten:
1. **Pure inline** — die Ebene trägt `{type,color,lw}` und `{pattern,scale,angle,
lineWeight}` direkt. Einfach, aber: kein Wiederverwenden, kein zentrales
Ändern; widerspricht der Ressourcen-Architektur (resources-graphics.md §1:
„alles verweist per id, zentral änderbar").
2. **Pure reference** — die Ebene trägt nur `lineStyleId` / `hatchId`. Konsistent,
zentral, aber unflexibel: Eine Ebene kann z. B. nicht „den Stil X, aber in
ihrer eigenen Farbe" wollen, ohne einen Klon-Stil anzulegen.
3. **Reference + optionale per-Ebene Overrides** (EMPFOHLEN) — die Ebene
**verweist** auf eine `LineStyle`- bzw. `HatchStyle`-Ressource und darf
**einzelne Felder lokal überschreiben**. Das ist exakt das DOSSIER/Vectorworks-
Muster: ein Stil als Basis, regelbasierte/lokale Overrides obendrauf
(resources-graphics.md, `overrides.py`).
### Empfehlung: Reference + optionale Overrides
Begründung:
- **Zentrale Pflege bleibt erhalten:** Ändert man den Linienstil „Wand stark" im
Line Manager, ziehen alle Ebenen nach, die ihn referenzieren und das jeweilige
Feld nicht überschreiben.
- **Lokale Freiheit ohne Stil-Wildwuchs:** Eine Ebene kann punktuell `color` oder
`lw` anpassen (häufigster Fall: gleiche Strichart, andere Farbe), ohne einen
fast identischen Stil zu duplizieren.
- **Migrationsfähig:** Die heutige flache `{color, lw}` der Ebene wird zu reinen
Overrides über einem neutralen Basis-Stil — verlustfrei (siehe §6).
- **Konsistent mit der bestehenden Kette:** `Component → Hatch → LineStyle`
verweist bereits per id; Ebenen reihen sich nahtlos ein.
Die Overrides sind **sparse**: nur gesetzte Felder überschreiben. Ein leeres
Override-Objekt (oder `undefined`) bedeutet „komplett dem Stil folgen".
---
## 3. Datenmodell (TS)
### 3.1 LineStyle erweitern um `type`
`LineStyle` trägt heute schon `weight`, `color`, `dash`. Wir machen die
Strichart explizit benennbar (statt nur via `dash`-Array), damit der Editor ein
sauberes Dropdown anbietet und `dash` daraus ableiten kann.
```ts
/** Benannte Strichart eines Stifts (für UI-Dropdown). */
export type LineKind = "solid" | "dashed" | "dotted" | "dashdot";
/** mm-Strichmuster je Strichart (relativ zur Papier-mm). */
export const LINE_DASH: Record<LineKind, number[] | null> = {
solid: null,
dashed: [0.6, 0.4],
dotted: [0.1, 0.25],
dashdot: [0.6, 0.25, 0.1, 0.25],
};
export interface LineStyle {
id: string;
name: string;
/** NEU: benannte Strichart; `dash` wird daraus abgeleitet, falls nicht gesetzt. */
kind: LineKind;
/** Strichstärke in Millimetern (≙ Rhino PlotWeight). */
weight: number;
color: string;
/** Strichmuster in mm; `null` = durchgezogen. Optional — sonst aus `kind`. */
dash: number[] | null;
}
```
> Hinweis: `kind` ist additiv; bestehende `LineStyle`-Daten setzen es per Migration
> aus `dash` (§6).
### 3.2 PEN- und HATCH-Override-Typen
```ts
/**
* Per-Ebene Stift (PEN). Verweist auf einen LineStyle; einzelne Felder dürfen
* lokal überschrieben werden. Alle Override-Felder optional (sparse).
*/
export interface LayerPen {
/** Basis-Linienstil (Line Manager). */
lineStyleId: string;
/** Lokale Overrides — nur gesetzte Felder gewinnen. */
override?: {
kind?: LineKind;
color?: string;
/** Strichstärke in mm Papier. */
lw?: number;
};
}
/**
* Per-Ebene Standard-Schraffur (HATCH). Verweist auf einen HatchStyle; einzelne
* Felder dürfen lokal überschrieben werden. `enabled=false` = Ebene hat keine
* Default-Schraffur (Umriss-only).
*/
export interface LayerHatch {
/** Aktiv? false = keine Default-Schraffur dieser Ebene. */
enabled: boolean;
/** Basis-Schraffur (Hatch Manager). */
hatchId: string;
/** Lokale Overrides — nur gesetzte Felder gewinnen. */
override?: {
pattern?: HatchPattern;
scale?: number;
/** Drehung in Grad. */
angle?: number;
color?: string;
/** Strichstärke der Musterlinien in mm Papier. */
lineWeight?: number;
};
}
```
### 3.3 LayerCategory erweitern
```ts
export interface LayerCategory {
code: string;
name: string;
visible: boolean;
locked: boolean;
// ── NEU: vollständige Darstellung ──────────────────────────────────────────
/** Stift der Ebene (PEN) — Linien aller Elemente dieser Ebene. */
pen: LayerPen;
/** Standard-Schraffur der Ebene (HATCH). */
hatch: LayerHatch;
/** Unterkategorien (Baum). */
children?: LayerCategory[];
// ── DEPRECATED (nur Übergang; siehe Migration §6) ──────────────────────────
/** @deprecated → pen.override.color. */
color?: string;
/** @deprecated → pen.override.lw. */
lw?: number;
}
```
`color` und `lw` bleiben als optionale, deprecatete Felder bestehen, bis alle
Lesepfade auf den Resolver (§4) umgestellt sind, und werden dann entfernt. Die
Panel-Swatch (`LayersPanel`) liest künftig die **aufgelöste** Stift-Farbe.
---
## 4. Resolver — vom Modell zur Render-Entscheidung
Der Resolver löst PEN/HATCH einer Ebene gegen die Ressourcen-Bibliotheken auf und
wendet die Overrides an. Er ist die **einzige** Stelle, an der „Stil + Override"
zusammenfließen; Generator und Panel rufen nur ihn.
### 4.1 Aufgelöste Render-Typen
`HatchRender` existiert bereits in `generatePlan.ts`. Wir ergänzen ein paralleles
`PenRender` und exportieren beide Resolver aus einem neuen Modul
`src/model/layerStyle.ts` (damit Panel und Generator teilen).
```ts
/** Aufgelöster Stift einer Ebene — alles, was die Linie zu zeichnen braucht. */
export interface PenRender {
color: string;
/** Strichstärke in mm Papier. */
lw: number;
/** Strichmuster in mm Papier; null = durchgezogen. */
dash: number[] | null;
}
// HatchRender: bereits in generatePlan.ts definiert (pattern, scale, angle,
// color, lineWeight, dash). Wird nach layerStyle.ts gezogen und re-exportiert.
```
### 4.2 Resolver-Funktionen (Pseudocode)
```ts
function resolvePen(project: Project, layer: LayerCategory): PenRender {
const ls = getLineStyle(project, layer.pen.lineStyleId); // wirft, falls fehlend
const o = layer.pen.override ?? {};
const kind = o.kind ?? ls.kind;
return {
color: o.color ?? ls.color,
lw: o.lw ?? ls.weight,
// Override-kind setzt das dash neu; sonst Stil-dash bzw. aus kind abgeleitet.
dash: o.kind ? LINE_DASH[o.kind] : (ls.dash ?? LINE_DASH[ls.kind]),
};
}
function resolveLayerHatch(project: Project, layer: LayerCategory): HatchRender | null {
if (!layer.hatch.enabled) return null; // Ebene ohne Default-Schraffur
const h = getHatch(project, layer.hatch.hatchId); // wirft, falls fehlend
const o = layer.hatch.override ?? {};
// Musterlinien-Stärke: Override > LineStyle der Schraffur > Default 0.13 mm.
const baseLs = h.lineStyleId ? getLineStyle(project, h.lineStyleId) : null;
return {
pattern: o.pattern ?? h.pattern,
scale: o.scale ?? h.scale,
angle: o.angle ?? h.angle,
color: o.color ?? h.color,
lineWeight: o.lineWeight ?? baseLs?.weight ?? 0.13,
dash: baseLs?.dash ?? null,
};
}
```
Beide bauen eine `Map<code, …>` über den ganzen Baum, analog zur heutigen
`categoryLwMap`:
```ts
export function penMap(project: Project): Map<string, PenRender> {
const m = new Map<string, PenRender>();
for (const c of flattenCategories(project.layers)) m.set(c.code, resolvePen(project, c));
return m;
}
export function layerHatchMap(project: Project): Map<string, HatchRender | null> {
const m = new Map<string, HatchRender | null>();
for (const c of flattenCategories(project.layers))
m.set(c.code, resolveLayerHatch(project, c));
return m;
}
```
---
## 5. Einfluss auf `generatePlan`
Heute (generatePlan.ts):
- `categoryLwMap(project.layers)` liefert nur `lw` je Code; die Umriss-Strichstärke
kommt daraus, **Farbe** der Umrisse ist fest `POCHE_STROKE`.
- Schraffur kommt ausschließlich aus dem `Component` der jeweiligen Schicht
(`resolveHatch(project, comp.hatchId)`); die Ebenen-`hatch` wird **nicht** genutzt.
Änderungen (minimal-invasiv, additiv):
### 5.1 Pens ersetzen `lwByCode`
```ts
const pens = penMap(project); // statt categoryLwMap
const layerHatches = layerHatchMap(project);
…
const pen = pens.get(wall.categoryCode) ?? FALLBACK_PEN; // {color, lw, dash}
```
`addWallPoche` und `addDoorSymbol` bekommen statt `wallLwMm: number` /
`doorLwMm: number` jeweils das ganze `pen: PenRender`:
- **Wand-Umrisslinie:** `stroke: pen.color` (statt fix `POCHE_STROKE`),
`strokeWidthMm: pen.lw * OUTLINE_DETAIL_FACTOR[detail]`, `dash: pen.dash`.
→ Das `Primitive` „polygon" braucht ein optionales `dash?: number[] | null`
(Schichtfugen bleiben durchgezogen; nur die Umriss-Kontur nutzt `pen.dash`).
- **Schichtfugen:** behalten `POCHE_STROKE` und ihre dünne `LAYER_LINE_MM`
(interne Hilfslinien sind bewusst neutral, nicht stift-gefärbt).
- **Tür-Symbol / Referenzlinie:** `cls` bleibt, aber `weightMm` aus `pen.lw`,
und die PlanView darf die Stift-Farbe nutzen (`door-leaf` etc. erhalten optional
ein `stroke`-Feld am line/arc-Primitive; ansonsten greift die CSS-Klasse wie
bisher).
### 5.2 Default-Schraffur der Ebene
Die Ebenen-Schraffur greift dort, wo **keine Component-Schraffur** vorliegt:
- **`detail === "grob"`** (eine Sammelfläche, heute `NO_HATCH`): statt `NO_HATCH`
nun `layerHatches.get(wall.categoryCode) ?? NO_HATCH`. So bekommt die grobe
Poché die Standard-Schraffur der Ebene (z. B. ein leichtes Diagonalmuster),
falls die Ebene eine definiert; sonst bleibt sie ungeschraffiert.
- **mittel/fein, mehrschichtig:** unverändert — die Component-Schraffur je Schicht
hat Vorrang (spezifischer als die Ebene). Die Ebenen-Schraffur ist der
*Fallback*, nicht der Default-Override.
- **Reine 2D-Zeichnungsobjekte** (künftige `drawing`-Ebenen-Elemente ohne
Component): nutzen direkt `resolveLayerHatch` als ihre Füllschraffur.
Auflöse-Reihenfolge der Schraffur einer gezeichneten Fläche:
```
Component.hatch > LayerCategory.hatch (enabled) > keine Schraffur
```
### 5.3 Geänderte Signaturen (Zusammenfassung)
```ts
// vorher: addWallPoche(out, project, wall, doors, cuts, greyed, detail, wallLwMm)
function addWallPoche(out, project, wall, doors, cuts, greyed, detail,
pen: PenRender, layerHatch: HatchRender | null): void
// vorher: addDoorSymbol(out, wall, door, greyed, detail, doorLwMm)
function addDoorSymbol(out, wall, door, greyed, detail, pen: PenRender): void
```
`Primitive` (polygon) erhält optional `dash?: number[] | null`; line/arc erhalten
optional `stroke?: string`, damit Pen-Farbe durchschlagen kann (CSS-Klasse bleibt
Default).
---
## 6. Editor „Ebeneneinstellungen…"
Geöffnet wie heute über `layerMenuItems → openLayerEditor(code)` →
`setEditor({ kind: "layer", code, x, y })`. Der bestehende `InlineEditor`-Rahmen
(dunkel, am Anker, Esc/Außenklick schließt) und die `EditorField`-Zeilen bleiben;
der Inhalt wächst von 3 Feldern auf zwei kompakte Abschnitte **PEN** und **HATCH**.
Da der Editor jetzt mehr Felder trägt, wird er als **kompakte Sektions-Form**
gestaltet (zwei Gruppen mit Trenn-Überschrift), gemäß CONVENTIONS.md UI-Konventionen
(saubere Form, keine wiederholten Beschriftungen, DOSSIER-Stil, alles via `t()`).
### 6.1 Aufbau
```
┌ Ebene 20 ───────────────── ×
│ Name [ Wände ]
│
│ ── Stift (PEN) ──────────────
│ Linienstil [ Wand stark ▾ ] ← Dropdown über project.lineStyles
│ Strichart [ durchgezogen ▾ ] ← override.kind (leer = "vom Stil")
│ Farbe [■] [↺] ← override.color; ↺ = Override entfernen
│ Stärke [ 0.35 ] mm [↺] ← override.lw
│
│ ── Schraffur (HATCH) ────────
│ [✓] aktiv
│ Schraffur [ Beton ▾ ] ← Dropdown über project.hatches
│ Muster [ vom Stil ▾ ] ← override.pattern
│ Maßstab [ 1.00 ] [↺]
│ Drehung [ 45 ] ° [↺]
│ Farbe [■] [↺]
│ Linienst. [ 0.13 ] mm [↺]
└──────────────────────────────
```
- **Override-Semantik im UI:** Jedes Override-Feld zeigt entweder „vom Stil"
(Override leer → Platzhalter mit dem aufgelösten Stil-Wert als Hint) oder einen
konkreten Wert. Ein kleiner **Reset-Knopf `↺`** je Override-Feld löscht das
Override (setzt es zurück auf `undefined` → Feld folgt wieder dem Stil).
- **Live, kein Bestätigen:** wie der heutige Editor — jede Änderung ruft sofort
`patchCategory(code, patch)`.
- **i18n:** alle Labels über `t()`. Neue Keys (Beispiele):
`editor.pen`, `editor.lineStyle`, `editor.lineKind`, `editor.color`,
`editor.lineWeight`, `editor.hatch`, `editor.hatchEnabled`, `editor.pattern`,
`editor.scale`, `editor.rotation`, `editor.fromStyle`, `editor.resetOverride`.
Strichart-/Muster-Werte: `lineKind.solid`, `lineKind.dashed`, …,
`hatchPattern.solid`, `hatchPattern.diagonal`, … . Menü-Label bleibt
`ctx.layerSettings`.
### 6.2 Patch-Helfer
`patchCategory(code, patch: Partial<LayerCategory>)` bleibt die Schnittstelle.
Für die verschachtelten Overrides nutzt der Editor schmale Helfer (im App-Scope),
die sparse mergen und leere Overrides auf `undefined` kollabieren:
```ts
function setPenOverride(cat: LayerCategory, patch: Partial<LayerPen["override"]>) {
const next = pruneEmpty({ ...cat.pen.override, ...patch });
patchCategory(cat.code, { pen: { ...cat.pen, override: next } });
}
function setHatchOverride(cat, patch) { /* analog für cat.hatch.override */ }
// pruneEmpty: entfernt undefined-Felder; gibt undefined zurück, wenn leer.
```
`setLineStyleId` / `setHatchId` setzen nur die Referenz; `hatch.enabled` ist ein
Checkbox-Patch.
### 6.3 „Eigenschaften kopieren / einfügen"
Der bestehende `layerClipboard` (heute `{ color, lw }`) wird auf die volle
Darstellung erweitert: `{ pen, hatch }` (die Override-tragenden Strukturen, ohne
`code/name/visible/locked`). „Kopieren" liest `{ pen, hatch }` der Quelle,
„Einfügen" patcht sie auf das Ziel. So überträgt sich der komplette Stift +
Schraffur einer Ebene auf eine andere.
---
## 7. Migration bestehender Beispieldaten
Bestehende Projekte/Sample-Daten haben `LayerCategory { color, lw, hatch?: string }`
und `LineStyle { weight, color, dash }` (ohne `kind`). Eine reine Lese-Zeit-
Migration (`migrateProject(project)`), idempotent, beim Laden:
1. **LineStyle.kind ableiten** — aus `dash`:
```
dash == null || dash.length === 0 → "solid"
sonst, wenn min(dash) sehr klein → "dotted" (heuristisch)
sonst → "dashed"
```
(Eine genaue Zuordnung ist nicht nötig; `dash` bleibt führend, `kind` ist nur
für das Dropdown.)
2. **Neutralen Basis-Linienstil sicherstellen** — falls die Bibliothek noch keinen
generischen „Standard"-Stift hat, einen `lineStyle` mit
`{ id: "ls-default", name: "Standard", kind: "solid", weight: <Ebenen-lw>, color: "#000", dash: null }`
anlegen. (Pro Ebene wird der Stift referenziert; die Ebenen-spezifischen
`color`/`lw` wandern in das **Override**, nicht in den Stil — so bleibt der
Stil wiederverwendbar.)
3. **Pro LayerCategory `pen` bauen:**
```ts
pen = {
lineStyleId: "ls-default",
override: pruneEmpty({ color: cat.color, lw: cat.lw }),
}
```
Damit ist die Darstellung **pixelgenau wie vorher** (gleiche Farbe, gleiche lw),
nur jetzt über die Resolver-Kette.
4. **Pro LayerCategory `hatch` bauen** — aus dem alten `hatch?: string`:
- War `hatch` ein gültiger `HatchStyle.id` → `{ enabled: true, hatchId: hatch }`.
- War es ein Pattern-Name oder leer/unbekannt → `{ enabled: false, hatchId:
<erste Hatch-id der Bibliothek> }` (Referenz muss existieren, aber inaktiv).
So entsteht **keine** unbeabsichtigte Schraffur (Default heute: keine).
5. **Deprecated-Felder belassen** für eine Übergangsphase; nach Umstellung aller
Lesepfade (`generatePlan`, `LayersPanel`-Swatch, Clipboard) in einem zweiten
Schritt `color`/`lw` aus `LayerCategory` und der alte `hatch: string` entfernen.
Migration ist **idempotent**: Liegt `pen`/`hatch` bereits vor, wird die Ebene
unverändert durchgereicht.
---
## 8. Build-Plan (phasiert)
**Phase 1 — Datenmodell & Resolver (keine UI-Sichtbarkeit).**
- `LineKind` + `LINE_DASH`, `LineStyle.kind`, `LayerPen`, `LayerHatch`,
`LayerCategory.pen/hatch` in `types.ts`.
- `src/model/layerStyle.ts`: `PenRender`, `resolvePen`, `resolveLayerHatch`,
`penMap`, `layerHatchMap`; `HatchRender` hierher ziehen + re-exportieren.
- `migrateProject()` (Schritte §7) + Aufruf beim Laden/Seed.
- `npx tsc -b` grün.
**Phase 2 — Generator umstellen.**
- `generatePlan` nutzt `penMap`/`layerHatchMap` statt `categoryLwMap`.
- `Primitive`-polygon `dash?`, line/arc `stroke?` ergänzen; `addWallPoche`/
`addDoorSymbol`-Signaturen auf `PenRender` + `HatchRender|null`.
- Ebenen-Default-Schraffur in „grob" und für Schicht-lose Flächen verdrahten.
- Visuell prüfen via `node scripts/probe.mjs` (Geometrie unverändert, Farben/lw
identisch zur Migration).
**Phase 3 — Panel.**
- `LayersPanel`-Swatch liest aufgelöste Stift-Farbe (`resolvePen(...).color`).
**Phase 4 — Editor.**
- `InlineEditor`-Inhalt für `kind: "layer"` auf die PEN/HATCH-Sektionen erweitern
(§6), mit Dropdowns über `project.lineStyles` / `project.hatches`, Reset-Knöpfen,
neuen i18n-Keys.
- `layerClipboard` auf `{ pen, hatch }` erweitern; Kopieren/Einfügen anpassen.
**Phase 5 — Aufräumen.**
- Deprecatete `color`/`lw`/`hatch: string` aus `LayerCategory` entfernen, sobald
kein Lesepfad sie mehr nutzt; Sample-Daten direkt im neuen Format ablegen.
---
## 9. Offene Punkte / bewusst nicht jetzt
- **Pro-Geschoss-Overrides der Ebene** (eine Ebene anders je `DrawingLevel`):
nicht in dieser Iteration; das Schema gilt geschossübergreifend (types.ts).
Falls später nötig, als zweite Override-Ebene über demselben Resolver.
- **Regelbasierte Overrides** (resources-graphics.md, `overrides.py`): orthogonal;
würden nach der Ebenen-Auflösung greifen.
- **Linienstil-Endkappen/Joins** und feinere Dash-Skalierung: bleiben in der
PlanView (Darstellung), nicht im Modell.
+672
View File
@@ -0,0 +1,672 @@
# Parametrische Wände
Status: Implementiert (Phase A — Typ-System und Resolver in `src/model/`, kein UI).
Dieses Dokument spezifiziert die **Parametrischen Wände**: regelbasierte Definitionen,
die beim Auflösen eine Liste von `Wall`-Elementen erzeugen, anstatt sie einzeln vom
Nutzer zeichnen zu lassen.
Bezugsdokumente: [elements.md](elements.md) (Wand-/Türmodell),
[drawing-tools.md](drawing-tools.md) (Werkzeugsystem, Direktzeichnen),
[state-architecture.md](state-architecture.md) (Projekt-Store),
[resources-graphics.md](resources-graphics.md) (WallType/Component-Auflösung).
Implementierungsdateien:
- `src/model/types.ts` — `ParametricWall`, `ParametricRule` und alle Regel-Varianten.
- `src/model/parametricWalls.ts` — `resolveParametricWall()`, `applyRule()` und
Hilfsfunktionen.
---
## 0. Überblick
Eine **parametrische Wand** (`ParametricWall`) ist kein festes `Wall`-Element, sondern
ein **Regelwerk**, das beim Auflösen (`resolveParametricWall`) eine Menge von `Wall[]`-
Elementen generiert. Die erzeugten Wände sind gewöhnliche `Wall`-Objekte; sie
unterscheiden sich lediglich in ihrer Herkunft. Das semantische Modell (`Project`)
bleibt die einzige Wahrheit — parametrische Wände sind eine Ressource in der
Ressourcen-Bibliothek, nicht eine separate Laufzeit-Geometrie-Schicht.
```
Project.parametricWalls: ParametricWall[]
│
│ resolveParametricWall(pw, floorId, context, defaultWallType)
▼
Wall[] ──→ normales Rendering über generatePlan / Viewport3D
```
Erzeugte Wände können entweder **temporär** (zur Laufzeit, als Ergänzung zu
`project.walls` im Rendering-Pfad) oder **eingebacken** (als `Wall[]` fest in
`Project.walls` gespeichert) behandelt werden. Phase A legt nur den Auflöser fest;
die Auswahl liegt bei der aufrufenden Komponente.
---
## 1. Motivation
### 1.1 Schnellere Modellierung von Regelgrundrissen
Schweizer Wohnbauten folgen häufig einem 3-m-Achsraster (SIA-Norm, Modul-/
Skelettbauweise). Zwanzig Wände eines Rasters von Hand zu zeichnen ist fehleranfällig
und verhindert spätere parametrische Änderungen (z. B. Geschossanzahl, Rasterweite,
Wandtyp).
Eine `GridRule` erzeugt dieses Muster aus wenigen Parametern (Achsabstand, Richtung,
Bereich) und lässt sich mit einer einzigen Zahl auf „4-m-Büroraster" umstellen.
### 1.2 Kongruenz mit FreeCAD BIM / IFC
FreeCAD BIM kennt **ParametricObjects**, die ihre Geometrie aus Regeln ableiten (z. B.
`ArchWall` mit `Length`, `Width`, `Height`). Obwohl das Datenformat hier kein IFC ist,
schafft ein ähnliches Abstraktionsniveau eine spätere Brücke: Beim IFC-Export können
parametrische Wände als `IfcWallStandardCase` mit konstanten Attributen exportiert
werden — kein Informationsverlust gegenüber manuell gezeichneten Wänden.
### 1.3 Bedingte Wandtypen ohne manuelle Klassifizierung
Außenwände sind dicker als Innenwände; Trennwände zwischen Einheiten erfordern
Schallschutz. Eine `ConditionalThicknessRule` (`condition: "exterior" → thickType`)
weist den richtigen Wandtyp automatisch aus der geometrischen Lage zu — ohne dass der
Nutzer jeden Wandabschnitt einzeln klassifizieren muss.
---
## 2. Architektur
### 2.1 Typen (`src/model/types.ts`)
```ts
/**
* Eine parametrische Wand-Regel — generiert automatisch Wall[]-Einträge für
* ein gegebenes Geschoss. Lebt in Project.parametricWalls[].
*/
export interface ParametricWall {
id: string;
name: string;
description?: string;
/**
* Geordnete Liste der anzuwendenden Regeln. Spätere Regeln können die
* Ausgabe früherer verfeinern (z. B. Dickenzuweisung nach Raster).
*/
rules: ParametricRule[];
/**
* Rückfall-Wandtyp, falls eine Regel keinen eigenen `wallTypeId` nennt.
*/
defaultWallTypeId: string;
}
/** Diskriminierte Union aller Regel-Varianten. */
export type ParametricRule =
| GridRule
| ModuleRule
| ConditionalThicknessRule
| ReferenceLineRule
| SequenceRule;
```
### 2.2 Einbettung ins Projekt
```ts
export interface Project {
// … bestehende Felder …
/**
* Parametrische Wanddefinitionen (Ressourcen-Bibliothek). Optional, damit
* bestehende Projekte/Tests ohne `parametricWalls` gültig bleiben.
*/
parametricWalls?: ParametricWall[];
}
```
### 2.3 Resolver-Kontext (`src/model/parametricWalls.ts`)
```ts
export interface ParametricContext {
/** Das Ziel-Geschoss. */
floor: DrawingLevel;
/**
* Optionale Rasterachsen (Phase C: verlinkter Grid-Ressource). Fehlen sie,
* berechnet die Engine die Achsen aus GridRule.spacing.
*/
gridAxes?: { x: number[]; y: number[] };
/**
* Optionales Clipping-Polygon (Meter). Fehlt es, reicht das Raster über
* einen Standardbereich (0 … spacing × 10).
*/
boundaryGeometry?: { boundary: Vec2[] };
/**
* Bereits im Projekt vorhandene Wände des Geschosses. Werden von
* refinierenden Regeln (ConditionalThicknessRule, ReferenceLineRule) genutzt.
*/
existingWalls?: Wall[];
}
```
---
## 3. Regel-Varianten
### 3.1 GridRule — Achsraster
Erzeugt parallele Wände auf einem gleichmäßigen Raster. Typischer Einsatz: Schweizer
Wohnbau-Achsraster (3 m), Büro-Konstruktionsraster (6 m), strukturelle Raster mit
fester Stützweite.
```ts
export interface GridRule {
type: "grid";
/**
* Optionaler Verweis auf eine Grid-Ressource (Phase C). Für Phase A wird
* stattdessen `spacing` genutzt.
*/
gridId?: string;
/** Rasterabstand in Metern (Default: 3.0). */
spacing?: number;
/**
* Achsrichtungen: „x" = nur Wände entlang der Y-Achse,
* „y" = nur Wände entlang der X-Achse, „both" = Vollraster.
*/
directions: "x" | "y" | "both";
/** Optionaler Verweis auf Clipping-Polygon. */
boundaryId?: string;
/** Optionale Wandtyp-Übersteuerung; sonst defaultWallTypeId. */
wallTypeId?: string;
/** Lage der Wandachse über die Dicke (Vectorworks-Stil). */
referenceLine?: WallReferenceLine;
/** Optionale Höhenübersteuerung in Metern; sonst Geschosshöhe. */
height?: number;
}
```
**Geometrieausgabe (top-down Grundriss):**
```
directions: "x", spacing: 3.0, Bereich 0…12 m:
y
│
12 ──────────────────────────
│
9 ──────────────────────────
│
6 ──────────────────────────
│
3 ──────────────────────────
│
0 ──────────────────────────
│
└──────────────────────────► x
0 12
```
**Wann verwenden:**
- Tragende Wände auf fester Stützweite (Wohnbau 3 m, Büro 6 m).
- Vollraster (`"both"`) für strukturelle Rastersysteme.
- In Kombination mit `ConditionalThicknessRule` zur automatischen Außen/Innen-Klassifizierung.
**Beispiel: Schweizer 3-m-Wohnraster**
```ts
const pw: ParametricWall = {
id: "pw-eg-raster",
name: "EG Längswände 3m-Raster",
defaultWallTypeId: "wt-innen-15",
rules: [
{
type: "grid",
spacing: 3.0,
directions: "x", // Wände in X-Richtung (y = 0, 3, 6, 9, 12)
wallTypeId: "wt-innen-15",
},
],
};
// Auflösung:
const walls = resolveParametricWall(pw, "floor-eg", {
floor: egFloor,
boundaryGeometry: { boundary: rectBoundary(0, 0, 12, 12) },
}, defaultWallType);
// → 5 Wände bei y = 0, 3, 6, 9, 12, je 12 m lang
```
---
### 3.2 ModuleRule — Bay-/Jochbauweise
Unterteilt eine Referenzspanne in gleiche Module und erzeugt Querwände an jedem
Teilungspunkt. Typisch für Bürogebäude (6-m-Joch) oder Reihenhäuser mit modularer
Erschließung.
```ts
export interface ModuleRule {
type: "module";
/** Modulmaß in Metern (z. B. 6.0, 3.6). */
moduleSize: number;
/** Ausrichtung der Trennwände: „x" = Querwände senkrecht zu X, „y" = zu Y. */
direction: "x" | "y";
/**
* Optionaler Verweis auf eine Referenzwand, die die Spannweite definiert.
* Fehlt er, wird die Geschoss-Ausdehnung (Bounding-Box) genutzt.
*/
referenceWallId?: string;
/** Optionale Wandtyp-Übersteuerung; sonst defaultWallTypeId. */
wallTypeId?: string;
referenceLine?: WallReferenceLine;
height?: number;
}
```
**Wann verwenden:**
- Wenn sich Querwände aus einer Referenzspanne (Fassade, Achswand) ergeben.
- Vorzug vor `GridRule`, wenn nur in eine Richtung unterteilt wird und eine
Referenzwand die Spanne definiert.
**Beispiel: 6-m-Bay-Bürogebäude**
```ts
const pw: ParametricWall = {
id: "pw-buero-joch",
name: "Büro 6m-Joch",
defaultWallTypeId: "wt-beton-20",
rules: [
{
type: "module",
moduleSize: 6.0,
direction: "x", // Querwände senkrecht zur X-Achse
// referenceWallId: "W-sudfassade" → Spanne aus der Südwand ableiten
},
],
};
// resolveParametricWall → Querwände bei x = 6, 12, 18, 24, 30 (bei 36-m-Fassade)
```
---
### 3.3 ConditionalThicknessRule — Bedingte Wandtypen
Weist bereits erzeugten Wänden (aus vorherigen Regeln in der Sequenz) einen anderen
Wandtyp zu — abhängig von einer Bedingung. Gibt modifizierte **Kopien** zurück; die
Eingabe-Wände werden nicht mutiert.
```ts
export interface ConditionalThicknessRule {
type: "conditional-thickness";
/**
* Bedingung für den Treffer:
* • „exterior" — Wand liegt am Außenrand (Bounding-Box des Kontexts).
* • „interior" — Wand liegt im Inneren.
* • „bearing" — tragende Wand (Heuristikum: Wand läuft ±10° zu X/Y-Achse).
* • beliebiger String — benutzerdefiniertes Tag (Phase C: Wall.tags[]).
*/
condition: "exterior" | "interior" | "bearing" | string;
/** Ziel-Wandtyp, der bei Treffer gesetzt wird. */
wallTypeId: string;
}
```
**Wann verwenden:**
- Immer in Kombination mit `GridRule` oder `ModuleRule` (als zweite Regel in
`ParametricWall.rules`): Raster erzeugt, Dicke verfeinert.
- Wenn Außen- und Innenwände denselben geometrischen Ursprung haben, aber
verschiedene Aufbauten benötigen.
**Beispiel: Außen dick, Innen dünn**
```ts
const pw: ParametricWall = {
id: "pw-eg-komplett",
name: "EG Vollraster mit Außenwand-Differenzierung",
defaultWallTypeId: "wt-innen-15",
rules: [
{
type: "grid", spacing: 3.0, directions: "both",
wallTypeId: "wt-innen-15",
},
{
type: "conditional-thickness",
condition: "exterior",
wallTypeId: "wt-aussen-36", // Außenwände erhalten dicken Aufbau
},
],
};
```
---
### 3.4 ReferenceLineRule — Wandachsen-Lage
Setzt `referenceLine` bei passenden Wänden einheitlich (Vectorworks-Stil: Achse
links/rechts/mittig). Gibt modifizierte Kopien zurück.
```ts
export interface ReferenceLineRule {
type: "reference-line";
/** Neue Lage der Wandachse, die einheitlich gesetzt wird. */
referenceLine: WallReferenceLine; // "left" | "center" | "right"
/**
* Filterziel:
* • „all" — alle Wände im aktuellen Satz.
* • „exterior" — nur Außenwände.
* • beliebiger String — benutzerdefiniertes Tag (Phase C).
*/
target: "all" | "exterior" | string;
}
```
**Wann verwenden:**
- Außenwände auf `"left"` setzen (Achse liegt auf der Fassadenfläche).
- Als abschließende Regel in einer `SequenceRule` nach Raster und Dickenzuweisung.
---
### 3.5 SequenceRule — Zusammenfassung von Unterregeln
Fasst mehrere Regeln als atomare Einheit zusammen. Jede Unterregel erhält die Ausgabe
der vorherigen als `existingWalls` — so können spätere Regeln frühere verfeinern.
```ts
export interface SequenceRule {
type: "sequence";
rules: ParametricRule[];
/**
* Wenn true: Abbruch nach der ersten Unterregel, die mindestens eine Wand
* generiert/verändert hat (Short-Circuit-Fallback).
*/
stopOnMatch?: boolean;
}
```
**Wann verwenden:**
- Um eine zusammengehörige Kombination (Raster → Dicke → Referenzlinie) als
Untermodul wiederzuverwenden — z. B. in unterschiedlichen Geschossen mit leicht
abweichenden Parametern.
---
## 4. Resolver-API (`src/model/parametricWalls.ts`)
```ts
/**
* Löst ein ParametricWall-Regelwerk zu einem Wall[]-Array für ein gegebenes
* Geschoss auf.
*
* Ablauf:
* 1. Regelwerk sequenziell ausführen; jede Regel erhält die Ausgabe der
* vorherigen als existingWalls (ermöglicht Verfeinerung).
* 2. Duplikate (gleicher Start-/Endpunkt innerhalb tolerance) entfernen.
* 3. Bereinigte Wall[]-Liste zurückgeben.
*
* Die Ausgabe ist sofort bereit zur Einfügung in project.walls. Es werden
* keine Seiteneffekte erzeugt — kein Store, kein Dispatch, kein React.
*
* @param parametricWall Das Regelwerk.
* @param floorId ID des Ziel-Geschosses.
* @param context Kontext (Geschoss-Objekt, Grid-Achsen, Grenzen, …).
* @param defaultWallType Fallback-Wandtyp, wenn eine Regel keinen nennt.
* @param tolerance Näherungstoleranz für Duplikat-Erkennung (Meter, Default 0.01).
* @returns Wall[]-Array, bereit zur Einfügung.
*/
export function resolveParametricWall(
parametricWall: ParametricWall,
floorId: string,
context: ParametricContext,
defaultWallType: WallType,
tolerance?: number,
): Wall[];
/**
* Dispatcher: delegiert eine Regel an die passende Implementierung.
* Exportiert für Unit-Tests und erweiterbare Regeltypen.
*/
export function applyRule(rule: ParametricRule, ctx: RuleCtx): Wall[];
/**
* Entfernt doppelte Wände: zwei Wände gelten als Duplikat, wenn Start- und
* Endpunkt jeweils innerhalb tolerance übereinstimmen (vorwärts und rückwärts).
*/
export function deduplicateWalls(walls: Wall[], tolerance?: number): Wall[];
```
### 4.1 Höhenauflösung
Die Wandhöhe (`Wall.height`) ergibt sich nach folgender Priorität:
1. `rule.height`, falls an der einzelnen Regel gesetzt.
2. `context.floor.floorHeight` des Zielgeschosses.
3. Fallback: 2.6 m (globaler Default, CONVENTIONS.md).
### 4.2 ID-Schema
```
"pw-<floorId>-gx-<counter>" // GridRule, X-Achse
"pw-<floorId>-gy-<counter>" // GridRule, Y-Achse
"pw-<floorId>-mx-<counter>" // ModuleRule, X-Teilung
"pw-<floorId>-ct-<counter>" // ConditionalThicknessRule
"pw-<floorId>-rl-<counter>" // ReferenceLineRule
```
IDs sind sessionlokal (Zähler startet bei 0 je Modullade). Eingebrannte Wände
erhalten beim Commit neue stabile IDs über `uniqueId("W")` — konsistent mit dem
Wand-Werkzeug (vgl. [drawing-tools.md §8](drawing-tools.md#8-id-vergabe--immutabilität)).
### 4.3 Duplikat-Erkennung
`deduplicateWalls` vergleicht Start-/Endpunkte beider Wände (vorwärts: A→B == A→B,
und rückwärts: A→B == B→A) innerhalb einer Toleranz von 1 cm (0.01 m). Die **erste**
Instanz wird behalten; spätere Duplikate werden verworfen. Dies ist wichtig bei
Vollrastern (`"both"`), bei denen X- und Y-Wände exakt auf einem Rasterpunkt
zusammentreffen könnten.
### 4.4 Verhalten bei ungültigen Eingaben
| Situation | Verhalten |
|-----------|-----------|
| `spacing <= 0` oder `moduleSize <= 0` | `[]` |
| `referenceWallId` nicht in `existingWalls` | Fallback auf Bounding-Box, kein Fehler |
| Unbekannter `condition`-String | `matchesCondition` gibt `false` zurück (kein Treffer) |
| Unbekannter `SequenceRule`-Untertyp | TypeScript exhaustiveness-Guard, `[]` |
| Segment mit `|end - start| < 1e-6` m | Kann durch deduplicateWalls entfernt werden |
---
## 5. Integration ins Projekt
### 5.1 Ressourcen-Speicherung
`ParametricWall`-Einträge leben unter `Project.parametricWalls` (optionales Array).
Sie sind Teil des `.cad.json`-Dokuments und werden mit dem Rest des Projekts gespeichert.
```ts
// sampleProject.ts — Beispieleintrag
export const sampleProject: Project = {
// …
parametricWalls: [
{
id: "pw-eg-raster",
name: "EG Längswände 3m-Raster",
defaultWallTypeId: "wt-innen-15",
rules: [
{ type: "grid", spacing: 3.0, directions: "x" },
{ type: "conditional-thickness", condition: "exterior",
wallTypeId: "wt-aussen-36" },
],
},
],
};
```
### 5.2 Rendering ohne UI (Phase A)
In Phase A werden parametrische Wände **nicht** automatisch gerendert. Der Auflöser
ist eine reine Funktion; Aufrufer müssen ihn explizit einbinden. Mögliche Verwendung
in `generatePlan` oder `Viewport3D`:
```ts
// generatePlan.ts (Ergänzung, Phase A)
const defaultWallType = project.wallTypes[0];
const extraWalls = (project.parametricWalls ?? []).flatMap((pw) =>
resolveParametricWall(pw, activeLevelId, {
floor: activeFloor,
boundaryGeometry: projectBoundary,
}, defaultWallType)
);
const allWalls = [...project.walls, ...extraWalls];
// … allWalls statt project.walls in der Rendering-Pipeline verwenden
```
### 5.3 Keine UI in Phase A
Kein Command, kein Panel, kein Formular. `ParametricWall`-Einträge werden in Phase A
ausschließlich **programmatisch** (Unit-Tests, `sampleProject`, direkte JSON-Bearbeitung
des Projekts) erstellt.
---
## 6. Ausblick: Folge-Phasen
### Phase B — UI und Command-Schnittstelle
- Neues Command (z. B. `PWWALL`) oder Ressourcen-Manager-Tab „Parametrische Wände"
mit Formular-Editor je Regeltyp.
- „Einbrennen" (Flatten): `ParametricWall` → feste `Wall[]` in `Project.walls`
einfügen und den `ParametricWall`-Eintrag entfernen (unidirektional, Undo über Store).
- Auswahl parametrischer Wände im Plan (als Gruppe); Grip-Editing der Raster-Parameter
und Spannweiten.
### Phase C — Grid-Ressource und Schnittpunkt-Clipping
- `GridResource`: ein projektweites, benanntes Koordinatenraster (LV95-Offset,
Rasterweite, Drehung), auf das mehrere `GridRule`-Instanzen via `gridId` verweisen.
- Präzises Clipping: erzeugte Wände werden am Gebäudeumriss getrimmt — exakte
`lineIntersect`-Berechnung statt Bounding-Box-Approximation.
- Benutzerdefinierte Tags (`Wall.tags[]`) für komplexe `ConditionalThicknessRule`-
Bedingungen jenseits von „exterior/interior/bearing".
- IFC-Export: `ParametricWall`-Gruppen → `IfcWallStandardCase` mit parametrischen
Attributen und `IfcRelDefinesByType`.
---
## 7. Vollständige Anwendungsbeispiele
### 7.1 Schweizer Wohnbau: 3-m-Raster EG + 1.OG
Zwei-Geschoss-Wohnhaus, typisches CH-Wohnbauraster. Die Längswände beider Geschosse
entstehen aus zwei `ParametricWall`-Einträgen mit identischen Regeln, unterschieden
nur durch `floorId` beim Auflösen:
```
Top-down (Grundriss):
y=12 ──────────────────────────── (W5)
y=9 ──────────────────────────── (W4)
y=6 ──────────────────────────── (W3)
y=3 ──────────────────────────── (W2)
y=0 ──────────────────────────── (W1)
↑
x=0 x=12
```
```ts
const rasterRegel: ParametricWall = {
id: "pw-laengswand-raster",
name: "Längswände 3m-Raster",
defaultWallTypeId: "wt-innen-15",
rules: [
{ type: "grid", spacing: 3.0, directions: "x" },
// Außenwände (y=0 und y=12) erhalten den dicken Aufbau:
{ type: "conditional-thickness", condition: "exterior",
wallTypeId: "wt-aussen-36" },
// Außenwände: Achse liegt auf der Fassadenfläche:
{ type: "reference-line", referenceLine: "left", target: "exterior" },
],
};
// EG auflösen:
const wallsEG = resolveParametricWall(rasterRegel, "floor-eg",
{ floor: egFloor, boundaryGeometry: { boundary: rect(0,0,12,12) } },
project.wallTypes[0]);
// 1.OG auflösen (gleiche Regel, anderes Geschoss):
const wallsOG = resolveParametricWall(rasterRegel, "floor-og1",
{ floor: ogFloor, boundaryGeometry: { boundary: rect(0,0,12,12) } },
project.wallTypes[0]);
// Änderung spacing: 3.5 → beide Geschosse sofort konsistent.
```
### 7.2 Vollraster mit Außen/Innen-Differenzierung
Gebäudeumriss als Rechteck; die Randwände erhalten automatisch den dicken
Außenwand-Typ, alle anderen den dünnen Innenwand-Typ:
```ts
const vollraster: ParametricWall = {
id: "pw-eg-vollraster",
name: "EG Vollraster mit Differenzierung",
defaultWallTypeId: "wt-innen-15",
rules: [
{ type: "grid", spacing: 3.0, directions: "both" },
{ type: "conditional-thickness", condition: "exterior",
wallTypeId: "wt-aussen-36" },
{ type: "conditional-thickness", condition: "interior",
wallTypeId: "wt-innen-15" },
{ type: "reference-line", referenceLine: "left", target: "exterior" },
],
};
```
```
─────┬─────┬─────┬─────
│ │ │ │ │
─────┼─────┼─────┼─────
│ │ │ │ │
─────┴─────┴─────┴─────
Rand-Segmente: wt-aussen-36 (dicker Aufbau)
Innen-Segmente: wt-innen-15 (dünner Aufbau)
```
### 7.3 Modulbauweise: 6-m-Joch, Bürogebäude
Längliches Bürogebäude, 36 m × 12 m, 6-m-Joch. Querwände entstehen automatisch;
Entwurfsänderung (z. B. auf 7.2-m-Joch) erfordert eine einzige Zahl:
```ts
const joch: ParametricWall = {
id: "pw-buero-joch",
name: "Büro 6m-Joch",
defaultWallTypeId: "wt-beton-20",
rules: [
{
type: "module",
moduleSize: 6.0,
direction: "x", // Querwände senkrecht zur X-Achse
// referenceWallId: "W-sudfassade" → Spanne aus Referenzwand
},
],
};
// resolveParametricWall → Querwände bei x = 6, 12, 18, 24, 30
// (bei Bounding-Box minX=0, maxX=36, Enden selbst ausgespart)
// Änderung auf 7.2-m-Joch: moduleSize: 7.2
// → 4 Trennwände bei x ≈ 7.2, 14.4, 21.6, 28.8 — automatisch neu berechnet.
```
---
## 8. Architektur-Garantien
- **Modell bleibt einzige Wahrheit.** `ParametricWall`-Definitionen sind Daten in
`Project.parametricWalls`; `resolveParametricWall` ist eine **reine Funktion** ohne
Side-Effects. Keine globale Laufzeit-Geometrie-Schicht.
- **Erzeugte Wände sind gewöhnliche `Wall`-Objekte.** Alle nachgelagerten Systeme
(`generatePlan`, `Viewport3D`, `computeJoins`) arbeiten unverändert; sie müssen
nicht zwischen „parametrisch erzeugten" und „direkt gezeichneten" Wänden
unterscheiden.
- **Fehlertoleranz statt Absturz.** Unbekannte Regeltypen liefern `[]`; der TypeScript-
exhaustiveness-Guard fängt fehlende `case`-Zweige zur Compilezeit. Unbekannte
Bedingungsstrings in `ConditionalThicknessRule` geben `false` (kein Treffer) statt
zu werfen.
- **Keine vorzeitige Generalisierung.** Phase A liefert fünf Regel-Varianten und
einen Auflöser. UI, Command-Schnittstelle und Grid-Ressource folgen in Phase B/C.
- **Immutabilität.** Verfeinerungsregeln (`ConditionalThicknessRule`,
`ReferenceLineRule`) geben modifizierte **Kopien** zurück; `existingWalls` werden
nie mutiert — konsistent mit der `setProject`-Konvention (CONVENTIONS.md).
+338
View File
@@ -0,0 +1,338 @@
# Design — Pläne & Output
> Teil der Standalone-Architektur — siehe [../../ARCHITECTURE.md](../../ARCHITECTURE.md).
> Bauteile: [elements.md](elements.md). Ressourcen/Stile: [resources-graphics.md](resources-graphics.md).
Hier gewinnen wir (ROADMAP §3, Phase 3 ⭐): **schöne, normgerechte 2D-Pläne**,
automatisch aus dem Modell abgeleitet, druckfertig als Vektor-PDF. Dieses Dokument
übersetzt DOSSIERs `schnitte.py`, `massstab.py`, `ausschnitte.py`, `kamera.py`,
`dimensionen.py`, `layouts.py` in Browser-Module. Bezeichner englisch, Prosa
deutsch, Meter.
---
## 1. Ansichtstypen = Kamera-Projektion + optionaler Schnitt
Vereinheitlichtes Modell (ROADMAP §2c, im Spike als `DrawingLevelKind` angelegt):
| Typ | Projektion | Schnitt | Erzeugung |
|---|---|---|---|
| **Grundriss** | Ortho Top | horizontal auf `okff + cutHeight` | symbolisch aus Footprint (Pfad A) |
| **Schnitt** | Ortho Front (Richtung) | vertikale Schnittebene + Tiefe | 3D-Projektion/HLR (Pfad B) |
| **Ansicht** | Ortho Front (Richtung) | kein Schnitt (Fassade außen) | 3D-Projektion/HLR (Pfad B) |
| **Perspektive** | 3D perspektivisch | — | Three.js direkt |
```ts
type ViewType = "plan" | "section" | "elevation" | "perspective";
interface DerivedView { // was der Viewport gerade zeigt
type: ViewType;
levelId?: string; // Geschoss (plan) bzw. Schnitt/Ansicht (DrawingLevel)
camera: CameraState;
cut?: CutSpec; // Clipping-Spezifikation (s.u.)
detailLevel: DetailLevel;
}
interface CutSpec {
planes: { point: Vec3; normal: Vec3 }[]; // 1 (plan/elevation) oder 2 (section: cut+back)
}
```
**Zwei Wege zum Plan** (zentrale Architektur-Erkenntnis, ROADMAP §3) — wir bauen
**beide**:
- **A) Grundriss = symbolisch** aus den Parametern (`plan/generatePlan.ts`, im
Spike). Schnell, exakt, vektorbasiert. Kein Mesh-Schnitt.
- **B) Schnitt & Ansicht = 3D-Projektion mit Hidden-Line-Removal** (`plan/
generateSection.ts`, §4). Durch das zusammengebaute Gebäude.
---
## 2. Schnitt & Ansicht — Datenmodell & Aktivierung
DOSSIER speichert Schnitte als Zeichnungsebenen-Eintrag (`type:"schnitt"`) mit
`linePts/dirSign/depthBack/cutAtLine/heightMin/heightMax/projection`
(`schnitte.create_schnitt_entry`). Im Spike sind die Felder als `DrawingLevel`
(`kind:"section"|"elevation"`, `linePoints`, `directionSign`) angelegt — wir
ergänzen:
```ts
interface SectionLevel extends DrawingLevel { // kind: "section" | "elevation"
linePoints: [Vec2, Vec2];
directionSign: 1 | -1; // Blickrichtung (Pfeil im Plan)
depthBack: number; // Tiefe hinter der Schnittlinie (default 8)
cutAtLine: boolean; // true=Schnitt (cut+back), false=Ansicht (nur back)
heightMin: number; heightMax: number;
projection: "parallel" | "perspective";
}
```
**Aktivierung** (Port `schnitte.activate_schnitt`):
1. `view_dir` = senkrecht zur Linie in XY, Richtung = `directionSign`.
2. **3D-Vorschau:** `THREE.Plane`s setzen —
- Cut (nur `cutAtLine`): auf der Linie, Normale `+view_dir`.
- Back (immer): um `depthBack` in `+view_dir` versetzt, Normale `−view_dir`.
- via `renderer.localClippingEnabled = true`, `material.clippingPlanes`.
3. **Kamera:** `OrthographicCamera`, Position `mid − view_dir·dist`, Target `mid`,
Up `+Z`; Zoom auf BBox (`linePoints` + Höhenbereich + `depthBack`). Bei
`perspective`: `PerspectiveCamera` + FOV.
4. **Vektor-Ergebnis:** HLR (§4).
**2D-Plan-Symbol** (Schnittmarke im Grundriss, Port `make_schnitt_symbol`): Linie
+ Endpfeile in `view_dir`, Beschriftung. Bleibt im Grundriss sichtbar (liegt auf
einer eigenen Ebene, z.B. `18 Schnittlinien`). **Doppelklick** auf das Symbol
aktiviert den Schnitt (`onDoubleClick` auf das SVG-Symbol → `setActiveLevel(id)`,
≙ DOSSIER `_SchnittDoubleClickHandler`).
**Grip-Editing der Schnittlinie:** Endpunkte als Grips im Grundriss; Ziehen
aktualisiert `linePoints` + Symbol + (falls aktiv) Clipping — ohne Re-Zoom der
3D-View (DOSSIER `skip_view`-Flag-Äquivalent: Drag aktualisiert nur die Clip-
Ebenen, nicht die Kamera).
---
## 3. Massstab (Scale) — pro Viewport, Auto-DPI
### 3.1 Mathematik (Port `massstab._compute_scale`, identisch im Browser)
```
frustumWidth_world = ortho-Kamera-Breite in Modell-Einheiten (Meter)
frustumWidth_mm = frustumWidth_world * 1000 (Meter→mm)
screenWidth_mm = canvasWidthCssPx * 25.4 / dpi
N (1:N) = frustumWidth_mm / screenWidth_mm
```
- **Nur bei Orthografie** sinnvoll; in Perspektive zeigt die UI „—" (wie DOSSIER).
- **DPI:** Browser kennt das nativ — `dpi = 96 * window.devicePixelRatio` (CSS
definiert 1 px = 1/96 inch). Das ersetzt DOSSIERs CoreGraphics-JXA-Detection
komplett und ist exakter. Optional manuell kalibrierbar (Eingabe in den
Settings), persistiert pro Projekt.
- **Massstab setzen** (1:N → Zoom): `frustumWidth_world = screenWidth_mm · N /
1000`; bei `THREE.OrthographicCamera` `camera.zoom = canvasWidthCssPx /
(frustumWidth_world / metersPerPixelAtZoom1)` bzw. direkt `left/right` setzen.
```ts
// plan/scale.ts
function computeScale(view: { frustumWidthWorld; canvasCssWidthPx; dpi }): number|null // 1:N
function applyScale(camera: THREE.OrthographicCamera, n: number, canvasCssWidthPx, dpi): void
const SCALE_PRESETS = [1,5,10,20,25,50,100,200,500,1000]; // 1:N Dropdown
```
### 3.2 Massstabs-abhängige Skalierung (DOSSIER-Stärke)
Bei 1:N müssen **Strichstärken** und **Schraffuren** lesbar bleiben:
- **Plotweight → SVG stroke-width:** `strokeWidthPx = lwMm / 25.4 · dpi`
(Welt-unabhängig; die Linie ist im Plan immer z.B. 0.25 mm dick). DOSSIER
skaliert dafür die PlotWeights (`_apply_scaled_lineweights`); im SVG-Modell
rechnen wir die mm-Strichstärke direkt in Pixel — **viel einfacher**, da SVG
von Natur aus papierbezogen ist.
- **Schraffur-Skalierung:** DOSSIER nutzt `factor = sqrt(N)/10` (1:100 ⇒ 1.0,
1:50 ⇒ 0.71, 1:500 ⇒ 2.24; `apply_scaled_hatches`). Port: SVG `<pattern>`-
`patternTransform="scale(factor)"` bzw. `patternUnits` so wählen, dass das Muster
die gewünschte Paper-Dichte hat. Formel 1:1 übernehmen.
- **Linetype-Dash:** `stroke-dasharray` in mm→px, ebenfalls papierbezogen.
> **Kernvorteil gegenüber DOSSIER:** Weil der Plan **SVG/Paper-Space** ist,
> entfällt das fragile Welt↔Bildschirm-Plotweight-Rescaling (DOSSIER `write_plotweight`,
> `read_plotweight`, Print-Display-Toggle). Strichstärke und Maßlinien sind direkt
> in mm definiert und werden 1:1 gedruckt.
---
## 4. Schnitt/Ansicht-Projektion (HLR) — Risiko #4
Vertikale Schnitte/Ansichten brauchen **echte 3D-Projektion mit verdeckten
Kanten** durch das zusammengebaute Gebäude.
```ts
// plan/generateSection.ts (läuft im Web Worker via Comlink)
interface SectionRequest { meshes: SerializedBrep[]; cut: CutSpec; camera: CameraState; }
interface SectionResult {
cutLines: Primitive[]; // Schnittkanten (dick) — geschnittene Bauteile
cutFaces: Primitive[]; // Schnittflächen → Component-Schraffur (Poché)
visibleLines: Primitive[]; // sichtbare Projektion (dünn)
hiddenLines?: Primitive[]; // verdeckte (gestrichelt, optional)
}
function generateSection(req: SectionRequest): SectionResult
```
- **Kernel:** **OpenCascade.js** `HLRBRep_Algo` / `HLRBRep_HLRToShape` (B-Rep →
sichtbare/verdeckte Kanten). Eingabe = die Bauteil-Breps (Wände/Decken/Treppen…),
Projektionsrichtung aus `camera`. Alternativ Mesh-basiert (langsamer, weniger
sauber).
- **Schnittflächen-Schraffur (Section-Style):** wo die Cut-Plane ein Bauteil
durchschneidet, entsteht eine Fläche → mit der Component-Schraffur füllen
(resources-graphics.md). ≙ DOSSIER `SectionStyle` (Hatch + Schnittkante +
Silhouette), nur dass wir es als SVG-Fill rendern statt als Rhino-Layer-Property.
- **Performance:** schwer → **Worker + Cache**. Cache-Key =
hash(sichtbare Element-IDs + Geometrie-Hash + CutSpec + camera). Nur neu rechnen,
wenn sich relevante Eingaben ändern (ROADMAP Risiko #4). Geschnittene vs. dahinter
liegende Geometrie über die Back-Plane begrenzen (`depthBack`).
- **Stufenweise:** (a) Ansicht ohne Verdeckung (einfache Projektion) → (b) HLR
sichtbar → (c) verdeckte Kanten gestrichelt → (d) Schnittflächen-Poché.
---
## 5. Ausschnitte (View-Snapshots)
Navigation über 50+ Ansichten ohne Ordner-Wildwuchs (DOSSIER `ausschnitte.py`).
Ein Snapshot speichert **Kamera + Sichtbarkeit + Massstab + Darstellung + Overrides**.
```ts
// in Project: viewSnapshots: ViewSnapshot[]
interface ViewSnapshot {
id; name; folder?: string;
camera: CameraState; // pos/target/up/parallel/fov + frustumWidth (Zoom!)
scale: number; // 1:N (DOSSIER speichert "1:50"-String)
detailLevel: DetailLevel; // LoD-Override (DOSSIER darstellung)
visibility: VisibilityState; // pro Geschoss + pro Ebene visible/locked
layerCombinationId?: string; // ODER Verweis auf Layer-Kombi (live) — s.u.
overrides?: { presetId?: string; enabled: boolean };
}
interface CameraState { position; target; up; parallel; fov?; frustumWidth?; }
```
- **Save:** aktuellen `ui`-Zustand einfrieren (Port `_capture`: Kamera inkl.
Frustum-Breite für exakten Zoom-Restore, Layer-Sichtbarkeit, Massstab, LoD).
- **Restore:** Snapshot → `ui` + ggf. `project`-Sichtbarkeit anwenden (Port
`_restore`): Kamera, Sichtbarkeit (oder referenzierte Layer-Kombi), LoD,
optional Overrides-Preset. Da alles im Store liegt, ist das ein einfacher
State-Set — kein Multi-Panel-Force-Send-Tanz wie in DOSSIER.
- **Ordner, Umbenennen, Duplizieren, Settings-Drawer** wie DOSSIER (`_duplicate`,
`_set_field`, `_open_settings_window` → React-Drawer statt Eto-Form).
### 5.1 Layer-Kombinationen (Presets)
```ts
interface LayerCombination { id; name; visibility: VisibilityState; }
```
Bauphasen/Varianten/MEP per Klick (DOSSIER `_save_preset`/`apply_layer_preset_by_name`).
Snapshot kann **live** auf eine Kombi verweisen (folgt Änderungen) **oder**
eingefroren den `visibility`-Stand halten — genau DOSSIERs Wahl (`layerCombination`
vs. `layers`).
---
## 6. Kamera-Presets & Norden-Rotation ⭐
Port `kamera.py`. Schnelle Ansichtswechsel + Georeferenzierung (Swisstopo, Phase 4).
```ts
// viewport/camera.ts
function setCardinal(cam, dir: "N"|"E"|"S"|"W", northAngle: number): void
function setIso(cam, octant: "NE"|"SE"|"SW"|"NW"|..., northAngle: number): void
function setTop(cam, northAngle: number): void // Plan-Norden zeigt nach oben
// northAngle = Grad im Uhrzeigersinn von +Y (DOSSIER dossier_north_angle, default 0)
const north = (deg) => ({ x: Math.sin(rad(deg)), y: Math.cos(rad(deg)) });
interface CameraPreset { id; name; camera: CameraState; } // benutzerdefiniert, gespeichert
```
- **Norden-Rotation:** alle Kardinal-/Iso-Richtungen werden um `northAngle`
rotiert (Port `set_cardinal_view`, `_set_iso`, `set_top_view`). `northAngle`
liegt im `Project` (georeferenziert zu swissBUILDINGS).
- **Benutzer-Presets:** speichern/laden wie DOSSIER (`_load_presets`/`_save_presets`).
---
## 7. Bemaßung (Dimensions)
Port `dimensionen.py`. Maße werden **aus dem Modell abgeleitet** (Wand-Dicken,
Geschoss-Höhen, Öffnungen) + manuelle Maßketten.
```ts
interface Dimension {
id; floorId; categoryCode; // liegt auf einer Ebene
kind: "linear" | "chain" | "aligned" | "level"; // Einzel|Kette|ausgerichtet|Höhenkote
refs: DimRef[]; // Bezugspunkte (frei ODER an Element gebunden)
offset: number; // Abstand der Maßlinie vom Objekt
style: DimStyleId; // Pfeile, Texthöhe, Einheiten
}
type DimRef = { point: Vec2 } | { elementId: string; anchor: "start"|"end"|"jamb"|... };
```
- **Auto-Bemaßung** (Phase 3): Außenketten (Gebäude-Hülle), Achsketten (Achsraster),
Öffnungs-Ketten — aus der Geometrie generiert, dann editierbar.
- **9-Punkt-Objekt-Info** (DOSSIER ROADMAP §11): Bounding-Box-Maße lesen +
Element via Greifen verschieben/skalieren/rotieren — direkt im Plan.
- **Rich-Text-Indizes** (Bold/Hoch-/Tiefstellung) für Maßzahlen — als SVG
`<tspan>` mit `baseline-shift` (resources-graphics.md §Rich-Text).
- **Massstabsbezug:** Texthöhe/Pfeilgröße in **Paper-mm**, rendern × Massstab —
konsistent mit §3.2.
---
## 8. Plansätze (Sheets) & PDF-Export
DOSSIER nutzt Rhinos `RhinoPageView` + `Detail`-Viewports + `FilePdf`
(`layouts.py`). Browser-Äquivalent: eigenes Sheet-Modell + SVG → PDF.
### 8.1 Datenmodell
```ts
interface Sheet {
id; name; folder?;
paper: "A0"|"A1"|"A2"|"A3"|"A4"|"Letter"; landscape: boolean;
viewports: SheetViewport[];
titleBlock?: TitleBlock; // Titelblock (Projekt/Plan/Massstab/Datum)
}
interface SheetViewport { // ≙ DOSSIER Detail + gebundener Ausschnitt
id; rect: { x; y; w; h }; // Position auf dem Blatt (mm)
source: { kind: "level"; levelId } | { kind: "snapshot"; snapshotId };
scale: number; // 1:N
clipToRect: boolean;
}
const PAPER_MM = { A0:[841,1189], A1:[594,841], A2:[420,594], A3:[297,420],
A4:[210,297], Letter:[216,279] }; // Port PAPER_SIZES_MM
```
### 8.2 Sheet-Editor
`sheets/SheetEditor.tsx`: Blatt als SVG in mm, Viewports per Drag platzieren/
skalieren, Quelle (Geschoss/Snapshot) + Massstab zuweisen. Ein Viewport rendert
den abgeleiteten Plan/Schnitt **bei seinem Massstab** in sein `rect` (≙ DOSSIER
`apply_snapshot_to_detail`). Bei Änderung der Quelle re-derivieren (live), kein
manuelles Re-Sync nötig (DOSSIER war Snapshot-Mode).
### 8.3 Detail↔Ausschnitt-Bindung
`SheetViewport.source.snapshotId` ist die Bindung (DOSSIER `_BIND_KEY`). „Alle
aktualisieren" = alle Viewports neu rendern; weil rein abgeleitet, ist das
automatisch. Umbenennen synchronisiert Titelblock + Schnitt-Symbol (DOSSIER
Detail↔Ausschnitt-Sync).
### 8.4 PDF-Export (Vektor, Multi-Page, @DPI)
Port `layouts._export_pdf`, aber **vektorbasiert** (DOSSIER rasterte via
`ViewCaptureToFile` @DPI — wir bleiben Vektor → schärfer, kleiner):
```ts
// sheets/exportPdf.ts
async function exportSheetsPdf(sheets: Sheet[], opts: { vector: boolean }): Promise<Blob>
```
- **Vektor-Pfad (bevorzugt):** jeder Sheet-Viewport rendert seinen Plan als SVG;
SVG → PDF via **`svg2pdf.js` + `jsPDF`** (oder `pdf-lib` mit eigenem Pfad-
Emit). Eine PDF-Seite pro Sheet, Größe = `PAPER_MM`. Strichstärken/Schraffuren
sind bereits in mm (§3.2) → 1:1 druckbar.
- **Raster-Fallback** (Perspektiven/3D-Inhalte): Three.js `renderer` → Canvas →
PNG @DPI → in PDF-Seite (`px = mm/25.4·dpi`, Port der DOSSIER-Pixelrechnung).
- **Speichern:** Blob → File System Access API (`showSaveFilePicker`) / Download.
---
## 9. Primitive & SVG-Serializer (gemeinsame Basis)
Alle Pläne (Grundriss, Schnitt, Ansicht, Sheet-Viewport) sprechen dieselbe
`Primitive`-Sprache (heute in `generatePlan.ts`), erweitert um Schraffur/Text:
```ts
type Primitive =
| { kind:"polygon"; pts:Vec2[]; fill:string; stroke:string; strokeWidthMm:number; hatchId?:string }
| { kind:"line"; a:Vec2; b:Vec2; styleId:string } // styleId → LineStyle (mm, dash)
| { kind:"arc"; center:Vec2; from:Vec2; to:Vec2; r:number; styleId:string }
| { kind:"text"; at:Vec2; text:string; heightMm:number; align; font; rich?:RichRun[] }
| { kind:"symbol"; at:Vec2; symbolId:string; scale:number; angle:number }; // Symbol-Bibliothek
interface Plan { primitives: Primitive[]; bounds: Rect; }
```
- **SVG-Serializer** (`plan/primitives.ts`): Primitive → SVG-Elemente.
`strokeWidthMm` → px via `mm·dpi/25.4`; `hatchId` → `<pattern>`-Referenz;
`styleId` → `stroke`/`stroke-dasharray`. Derselbe Serializer für Bildschirm
*und* PDF.
- **DXF-Export** (Phase 4): dieselben Primitive → DXF-Entities (`dxf`-Writer-lib).
---
## 10. Umsetzungs-Reihenfolge (verweist auf ROADMAP-Phasen)
1. **Phase 1 (MVP):** Grundriss-Generator ✅ ausbauen (Schraffuren, LoD), Live-
Grundriss neben 3D, Basis-Bemaßung; Massstab pro Viewport (§3).
2. **Phase 3 ⭐:** Schnitt/Ansicht via HLR (§4, Worker), Auto-Bemaßung (§7),
Ausschnitte + Layer-Kombinationen (§5), Kamera-Presets + Norden (§6),
Sheets + Vektor-PDF (§8).
3. **Phase 4:** DXF-Export (§9), Detail↔Ausschnitt-Sync-Politur.
+293
View File
@@ -0,0 +1,293 @@
# Design — Ressourcen & Grafik
> Teil der Standalone-Architektur — siehe [../../ARCHITECTURE.md](../../ARCHITECTURE.md).
> Bauteile: [elements.md](elements.md). Output/Pläne: [plans-output.md](plans-output.md).
Die **Stil-Schicht**: verwaltete Ressourcen-Bibliotheken (Vectorworks-Stil),
regelbasierte Overrides, Symbol-/Text-Bibliotheken und Detailgrad-Steuerung. Sie
wird **beim Rendern angewandt, nie in die Geometrie eingebacken** (ROADMAP §2b).
Dieses Dokument übersetzt DOSSIERs `styles.py`/`gestaltung.py`, `mass_style.py`,
`overrides.py`, `library.py`, `text_create.py`. Bezeichner englisch, Prosa
deutsch.
---
## 1. Resource Manager — verwaltete Bibliotheken
Drei Bibliotheken im `Project.resources`-Block; **alles verweist per id**, zentral
änderbar (ROADMAP §2d). Verweis-Kette: 2D-Objekte/Ebenen → LineStyle/Hatch;
Hatch → LineStyle; Component → Hatch (+3D-Material).
```ts
interface Resources {
lineStyles: LineStyle[];
hatches: Hatch[];
components: Component[];
}
interface LineStyle { // Line Manager
id; name;
weight: number; // Strichstärke in mm (≙ Rhino PlotWeight)
color: string; // hex
dash: number[]; // Strichmuster in mm ([] = durchgezogen)
}
interface Hatch { // Hatch Manager
id; name;
pattern: PatternId; // "solid" | "diagonal" | "insulation" | "concrete" | ...
scale: number; // Grundmaßstab des Musters
angle: number; // Grad
lineStyleId: string; // Linien der Schraffur → Line Manager
}
interface Component { // Component Manager (= DOSSIER-Material, erweitert)
id; name;
hatchId: string; // Schnitt-Schraffur → Hatch Manager
color3d: string; // 3D-Diffusfarbe
texture3d?: TextureRef; // optionale PBR-Textur (Phase 3)
pbr?: { roughness; metalness; opacity; ior }; // Material-Bibliothek (ROADMAP §11)
joinPriority: number; // Verschneidungs-Rang (DOSSIER _MATERIAL_PRIO als Daten)
}
```
**Migration vom heutigen Stand:** Der Spike hat `Material { color, planFill, hatch }`
und `Layer { materialId, thickness, priority }`. Ziel: `Material → Component`
(`color→color3d`, `planFill→` Fill aus `hatch.pattern==solid`+Farbe, `hatch`-Enum
→ `hatchId`), `Layer.priority → Component.joinPriority` (Priorität wandert vom
Layer zum Component, damit man sie nur einmal pflegt — siehe elements.md §1.3).
### 1.1 Manager-UI
`managers/ComponentManager.tsx`, `HatchManager.tsx`, `LineManager.tsx` — je eine
Liste mit CRUD + Vorschau (Three-Sphere für Component-3D, SVG-Swatch für
Hatch/Line). **Seeds** beim ersten Projekt (DOSSIER-Defaults):
- LineStyles: 0.13 / 0.18 / 0.25 / 0.35 / 0.50 mm (aus `DEFAULT_LAYER_SCHEMA`-lw).
- Hatches: `solid`, `diagonal`, `concrete`, `insulation` (Dämmung).
- Components: Stahlbeton (prio 800), Beton (800), Mauerwerk (600), Ziegel (550),
Holzständer (400), Dämmung (200), Putz (100) — exakt DOSSIER `_MATERIAL_PRIO`
(elements.md §1.3). Plus Glas (transparent), Holz-Türblatt (DOSSIER
`_OEFF_PIECE_DEFS`).
### 1.2 Render-Anwendung
- **3D:** Component → `MeshStandardMaterial` (`color3d`/`pbr`), pro `componentId`
gecacht (`viewport/scene.ts`).
- **Plan/Schnitt:** geschnittene Schicht → Polygon mit `fill` (Component-Farbe) +
`<pattern>` aus `hatchId`. Pattern als SVG `<pattern>` mit `patternTransform`
für Massstab (plans-output.md §3.2). Der Hatch nutzt seinen `lineStyleId` für
die Musterlinien.
---
## 2. Mehrschichtige Aufbauten ↔ Ressourcen
`WallType.layers[].componentId` / `SlabType.layers[].componentId` verweisen auf
Components. 3D und Plan lesen dieselben Schichten (elements.md §1). Die
**Prioritäts-Verschneidung** (Risiko #1) liest `Component.joinPriority`:
höhere Priorität läuft am Stoß durch (Backbone), niedrigere stößt seitlich an —
Algorithmus in elements.md §1.3 (Port DOSSIER `_t_junction_layer_overrides`).
---
## 3. Stile & Element-Override (Selektions-Attribute)
DOSSIERs GESTALTUNG-Panel (`styles.py`) setzt Farbe/Lineweight/Linetype/Hatch auf
die *Selektion*. Browser-Äquivalent — zwei Ebenen, in Render-Reihenfolge:
```
ByLayer (Ebenen-Default) → Element-Style (styleId) → Override-Regeln → gerendert
```
```ts
// Effektiver Stil eines Elements (resolve beim Rendern, nie persistiert)
interface EffectiveStyle { color; lineStyleId; hatchId?; }
function resolveStyle(project, el, doc): EffectiveStyle {
// 1) Default aus LayerCategory(categoryCode)
// 2) überschrieben durch el.styleId (Element-Override, optional)
// 3) überschrieben durch passende Override-Regeln (§4)
}
```
- **Wall-/Opening-Stil-Kataloge** (Presets, ROADMAP §11): benannte Sätze von
Default-Werten (Wandtyp + Farbe + lw; Öffnung mit Rahmen/Sims/…). 1-Klick-
Anwendung, globaler Stilwechsel. Speicherung: pro Projekt + cross-Projekt
(LocalStorage), Seed wie DOSSIER `_OEFF_DEFAULT_STYLES` (elements.md §2.1).
- **LoD-bewusste Stil-UI** (DOSSIER ROADMAP §11): das Stil-Panel zeigt nur
passende Controls je Geometrietyp (keine Füll-Optionen bei einer 3D-/Linien-
Auswahl). `panels/StylePanel.tsx` schaltet Felder nach `selection`-Typ.
- **Pipette:** Stil/Typ von einem Element auf ein anderes übernehmen (DOSSIER
`cmd/pipette`).
---
## 4. Regelbasierte Overrides (Engine)
Port `overrides.py` (ArchiCAD Graphical Overrides / Vectorworks
Datenvisualisierung). Im Browser **viel einfacher**, weil Overrides reine
**Render-Transformationen** sind — kein UserString-Backup/Restore nötig (DOSSIER
musste Originalwerte sichern, weil es echte Rhino-Objekte mutierte; wir mutieren
nichts).
```ts
interface OverrideConfig { enabled: boolean; rules: OverrideRule[]; activePresetId?: string; }
interface OverrideRule {
id; name; enabled: boolean;
conditions: Condition[]; conditionsLogic: "and" | "or";
actions: { color?: string; lineWeight?: number; lineStyleId?: string;
hatchId?: string; hatchScale?: number };
}
interface Condition {
type: "category" | "userField" | "name" | "elementType"; // ≙ layer_name/user_string/object_name
operator: "equals"|"notEquals"|"contains"|"startsWith"|"endsWith";
value: string;
key?: string; // nur für userField (z.B. "sia")
}
```
**Auswertung** (Port `_compose_overrides`):
```ts
function composeOverrides(el, project, cfg): Partial<Actions> {
// additive: Actions aller matchenden, aktiven Regeln kombinieren;
// bei Konflikt für dieselbe Property gewinnt die Regel WEITER OBEN (kleinerer Index).
}
```
- **Anwendung:** `resolveStyle` (§3) ruft `composeOverrides` — Override liegt
über Element-Style. Reines Read beim Rendern → kein `apply_all`/`restore_all`,
kein Backup, **keine reversibilität nötig**. Toggle `enabled` rendert neu.
- **Live:** Da abgeleitet, schlägt jede Modell-/Regel-Änderung sofort durch (kein
`install_listeners`/`AddRhinoObject`-Hook wie DOSSIER).
- **Presets & Templates** (cross-Projekt): Preset = Satz Regeln, Template =
einzelne Regel; LocalStorage statt `~/Library/.../override_presets.json`
(`save_preset`/`load_preset`/`list_rule_templates` → `resources/overridePresets.ts`).
- **SIA-416-Preset** (elements.md §7): vier Regeln `userField sia == HNF|NNF|VF|FF`
→ Farbe + Solid-Hatch, Port `_build_sia_preset_rules`. Aktivieren = Preset
`activePresetId` setzen.
```ts
// resources/overrides.ts
function composeOverrides(el, project, cfg): Partial<OverrideAction>
function setActivePreset(project, presetId): Project // immutabel
const PRESETS_NS = "cad.presets.overrides"; // LocalStorage
```
---
## 5. Symbol-Bibliothek
Port `library.py` + `cmd/symbol`. Wiederverwendbare 2D-Symbole (Möbel, Sanitär,
Bäume, Nordpfeil, Pflanzen) für den Plan.
```ts
interface Symbol {
id; name; category: string; // "furniture" | "sanitary" | "vegetation" | "annotation"
svgPath: string; // Pfad-/Gruppen-Markup im Symbol-Koordinatensystem (m)
defaultScale: number;
}
interface SymbolInstance extends ElementBase { // type:"draw2d", subtype:"symbol"
symbolId: string; at: Vec2; scale: number; angle: number;
}
```
- **Speicherung:** mitgelieferte Symbole als statische Assets (SVG); Nutzer-
Symbole im Projekt + cross-Projekt (LocalStorage). DOSSIER nutzt Block-
Definitionen; bei uns SVG-Definition + Instanz-Transform (`<use>`-artig).
- **Picker:** `panels/SymbolPicker.tsx` (≙ DOSSIER `SymbolPicker.jsx`) — Grid mit
Vorschau, Drag in den Plan; Instanz auf Ebene `60 Plangrafik` (bzw. `22 Möbel`).
- **Render:** `Primitive{ kind:"symbol", ... }` → SVG `<g transform>` mit dem
Symbol-Markup (plans-output.md §9).
---
## 6. Text & Rich-Text-Annotationen
Port `text_create.py` + `text_editor.py`. Formatierte Beschriftungen auf Canvas.
```ts
interface TextElement extends ElementBase { // type:"draw2d", subtype:"text"
at: Vec2; runs: RichRun[];
heightMm: number; // Paper-mm (rendern × Massstab) ODER Modell-m
heightMode: "paper" | "model"; // DOSSIER raum_txt_modus fix|masstab
font: string; align: "left"|"mid"|"right"; angle: number;
mask?: boolean; // Hintergrund-Maskierung (verdeckt Linien darunter)
frame?: boolean; // Rahmen um den Text
}
interface RichRun {
text: string;
bold?; italic?; super?; sub?; // Hoch-/Tiefstellung (Maß-Indizes)
}
interface TextStyle { id; name; font; heightMm; bold; italic; } // Text-Presets
```
- **Render:** `<text>` mit `<tspan>` pro Run; `super/sub` via `baseline-shift` +
kleinerer `font-size`; `mask` via weißem `<rect>` darunter; `frame` via `<rect>`.
- **Editor:** Inline-Rich-Text-Editor (contentEditable oder leichter Custom-Editor)
→ `RichRun[]`. Fonts aus einer kuratierten Web-Font-Liste + System-Fonts
(DOSSIER `_list_system_fonts` mit Preferred-Liste DM Mono/Krungthep/…); im
Browser via `document.fonts` / `queryLocalFonts()` (wo verfügbar) + gebündelte
Web-Fonts.
- **Massstabsbezug:** `heightMode:"paper"` → Texthöhe in mm, gerendert × Massstab
(plans-output.md §3) — Beschriftung bleibt bei jedem Massstab lesbar.
---
## 7. Detailgrad (Level of Detail)
Querschnittsthema (ROADMAP §2b „Modelldarstellungen"). Drei Stufen, Dokument-/
Snapshot-weiter Override mit Per-Element-Ausnahme — DOSSIER
`darstellung`/`aktive_darstellung`.
```ts
type DetailLevel = "coarse" | "medium" | "fine"; // einfach | standard | detail
// Auflösung (Port _resolve_oeff_darstellung):
function resolveDetail(el, doc): DetailLevel {
const v = el.detailLevel ?? "auto";
return v === "auto" ? doc.detailLevel : v; // doc-Level hat IMMER konkreten Wert
}
```
- **Dokument-Ebene:** `Project.detailLevel` (Default `coarse`/einfach = 1:100,
DOSSIER `_DARSTELLUNG_DEFAULT_GLOBAL`). In der TopBar global umschaltbar; ein
ViewSnapshot kann ihn pro Ansicht überschreiben (plans-output.md §5).
- **Wirkung:** jedes Bauteil-`generatePlan`/`build3d` liest den aufgelösten LoD und
zeichnet entsprechend (elements.md: Tür coarse=Lücke, fine=Glas/Schwenkbogen/
Sims). Kritisch für Mixed-Scale-Pläne (1:50 Detail neben 1:200 Übersicht).
- **Schnitt-Schraffur** koppelt an LoD: grob ggf. nur Umriss, fein voll schraffiert.
---
## 8. Section-Style (3D-Schnittflächen)
Port DOSSIER `_apply_section_style` (`layer_builder.py`). Wo die Schnittebene ein
Bauteil durchschneidet: Schnittfläche bekommt die **Component-Schraffur**, die
Schnittkante einen dicken Rand, optional eine Silhouette.
```ts
interface SectionStyle { // pro Component (oder Ebene) ableitbar
hatchId?: string; hatchScale; hatchAngle;
boundaryShow: boolean; boundaryLineStyleId; boundaryWidthScale;
fillBackground: boolean;
}
```
- **3D-Viewport:** Three.js hat keinen nativen „Schnittflächen-Cap". Cap-Geometrie
selbst erzeugen: Schnittpolygon der Cut-Plane mit den Breps → Fläche mit
Hatch-Material (oder Stencil-Cap-Technik). Phase 4.
- **2D-Schnitt (SVG):** `generateSection` (plans-output.md §4) liefert `cutFaces`
→ mit `SectionStyle.hatchId` füllen. Das ist der Hauptweg; der 3D-Cap ist Bonus.
---
## 9. Was Browser hier einfacher macht (vs. DOSSIER)
| DOSSIER-Aufwand | entfällt im Browser, weil … |
|---|---|
| UserString-Backup/Restore bei Overrides (`_backup_original`/`_restore_original`) | Overrides sind reine Render-Reads — nichts wird mutiert |
| Hatch-Curve-Link über Sticky (`gestaltung_curve_hatch`, Pending-TTL) | Schraffur ist eine Eigenschaft des Polygons, kein separates Objekt |
| Plotweight-Welt↔Bildschirm-Rescaling (`write/read_plotweight`) | Strichstärke ist in mm im SVG-Paper-Space (plans-output.md §3.2) |
| `install_listeners` für Live-Override-Reapply | reaktiver Store re-rendert automatisch |
| SectionStyle-API-Reflection über Rhino-Versionen | wir definieren das Rendering selbst (SVG/Three) |
| Cross-doc Presets als Dateien im User-Home | LocalStorage + Export/Import |
---
## 10. Umsetzungs-Reihenfolge (verweist auf ROADMAP-Phasen)
1. **Phase 1:** Component/Hatch/Line-Manager (§1) + `resolveStyle` (§3) +
LoD-Grundgerüst (§7) + LoD-bewusste Stil-UI.
2. **Phase 2:** Stil-Kataloge (Wände/Öffnungen), Material-Seeds mit `joinPriority`.
3. **Phase 3:** Overrides-Engine + SIA-Preset (§4), Symbol-Bibliothek (§5),
Rich-Text (§6), PBR-Material-Bibliothek; massstabsabhängige Hatch-/Linetype-
Skalierung (plans-output.md §3.2).
4. **Phase 4:** Section-Style 3D-Cap (§8).
+370
View File
@@ -0,0 +1,370 @@
# Handoff — Rhino-artiges Befehlssystem + Modellier-Werkzeuge
> Für die Instanz, die das Befehlssystem (Tab-getriggert) und die Rhino-artigen
> Modellierfunktionen baut. Stand: 2026-06-30. Zuerst lesen: `CONVENTIONS.md`,
> `ROADMAP.md`, `HANDOVER.md`, `docs/design/drawing-tools.md`. Volle Autonomie,
> selbst bestätigen (Memory `proceed-autonomously`, `wire-dont-stub`, `prefer-agents`).
Dieses Dokument hat drei Teile:
1. **Was schon steht** (worauf du aufbaust — exakte Dateien/Typen/Actions).
2. **Rhino-Referenz** (Interaktionsmodell, das nachzubilden ist).
3. **Konkreter Bauplan für DIESE Codebase** (Architektur, Dateien, Reihenfolge).
---
## TL;DR — die Kernidee
Es gibt **kein** Befehlssystem, keine Command-Line, keinen Tab-Handler. Das baust du
greenfield. **Aber:** Das Werkzeug-System ist bereits eine saubere Pure-Function-Registry
(`src/tools/`) mit generischem Controller in `App.tsx`, und die Mutations-Schicht
(`projectSlice`) ist umfassend. Ein neues Modellier-Tool steckst du durch Hinzufügen eines
`Tool`-Objekts ein — **PlanView muss dafür nicht angefasst werden**.
Das Befehlssystem ist im Kern eine **State-Machine-Engine über prompt → pick/type →
options**, plus eine **Command-Line-UI** (Statusleiste), plus ein **Koordinaten-Parser**.
Befehle dispatchen auf bestehende Store-Actions + `setActiveTool`/`setProject`.
**Wichtigster konzeptioneller Sprung:** Die heutigen Tools haben je eine *eigene*
ad-hoc-Phasenlogik (`onClick`/`onMove`). Rhino-Feel verlangt eine **gemeinsame
Prompt/Option/Numerik-Engine**, die alle Befehle teilen. Plane das als Verallgemeinerung
des bestehenden `Tool`-Interfaces, nicht als Parallelwelt daneben (sonst zwei Eingabe-Pfade,
die divergieren).
---
# TEIL 1 — Was schon steht (Baufundament)
Stack: React 18 + TS + Vite, `three` 0.169 (nur 3D-Display). Einheiten intern **Meter**.
Eigener winziger Store (`useSyncExternalStore`, kein Redux/Zustand). Identifier englisch,
UI-Text deutsch via `t()`. Strict tsc (`noUnusedLocals` → ungenutzte Vars brechen den Build).
## 1.1 Werkzeug-System — `src/tools/`
- **`Tool`-Interface** `src/tools/types.ts:139` — reine Funktionen über internen `ToolState`
(Discriminated Union je Tool). Handler geben `[nextState, ToolResult]` zurück. Tools
schreiben NIE Plan-Primitive; sie geben `commit(project) => project` zurück.
- `ToolId` `types.ts:11`: `"select" | "wall" | "line" | "polyline" | "rect"`.
- `ToolContext` `types.ts:72`: `{ project, level, defaultCategoryCode, activeWallTypeId, activeLineStyleId }`.
- `ToolPointer` `types.ts:85`: `{ raw, snap, point (=snap?.point ?? raw), shift, ctrl, alt, button }`.
- `ToolResult` `types.ts:121`: `{ draft, commit?, done? }`. `ToolDraft` `types.ts:109`:
`{ preview: DraftShape[], vertices: Vec2[], snap?, hud?: {at,text} }`.
- **Registry** `src/tools/tools.ts:375`: `TOOLS: Record<ToolId,Tool>`, `getTool(id)` `:384`,
`TOOL_ORDER` `:389`. Implementiert: select (Platzhalter), wall, line, polyline, rect.
- `uniqueId(prefix)` `types.ts:188` — ID-Generator.
## 1.2 Controller / Verdrahtung — alles in `App.tsx` (NICHT in den Tool-Dateien)
- Aktives Tool: `const [activeTool,setActiveTool]=useState<ToolId>("select")` `App.tsx:197`.
- Laufender Zustand in **Ref** `toolStateRef` `App.tsx:205` (kein Re-Render je Mausschritt).
Live-Vorschau `const [draft,setDraft]` `App.tsx:206`.
- **Controller** `runToolStep(kind,raw,pxPerMeter,mods)` `App.tsx:350`: baut `ToolContext`
(`toolCtx` `:294`), snappt via `snapFor` `:322` (→ `computeSnap`), baut `ToolPointer`,
ruft `tool.onClick/onMove`, speichert State in Ref, `applyToolResult` `:339` wendet
draft/commit/done an. `toolHandlers` `App.tsx:419` verbindet PlanView↔Controller.
- Tool-Tasten `App.tsx:448`: Esc=Abbruch/zurück-zu-select, Enter=Commit-Geste, Backspace=Punkt zurück.
## 1.3 Semantisches Modell — `src/model/types.ts`
- `type Element = Wall | Door | Drawing2D` `:251`. `Vec2={x,y}`. **Kein Slab/Stair-Typ.**
- `Project` `:254`: `{ ..., wallTypes[], drawingLevels[], layers[], walls[], doors[], drawings2d[] }`.
- `Wall` `:150`: `{ id,type:"wall", floorId, categoryCode, start, end, wallTypeId, height, color? }`
(Mittellinie + mehrschichtiger `WallType`).
- **Plan-Primitive `Drawing2DGeom`** `:200` — die Geom-Typen existieren bereits ALLE:
`line | polyline | rect | circle | arc | text`. Aber Tools erzeugen heute nur line/polyline/rect,
und `drawingVertices` (Grips) kennt nur diese drei. **circle/arc/text sind im Typ da, aber
nicht durchgängig gerendert/editierbar** — Lücke, kein Neubau nötig.
- `Drawing2D` `:209`: `{ id,type:"drawing2d", levelId, categoryCode, geom, lineStyleId?, hatchId?, color?, fillColor?, weightMm? }`.
## 1.4 Geometrie — `src/model/geometry.ts`
`sub,add,scale,len,normalize`; `leftNormal(a)={x:-a.y,y:a.x}` `:17` (Wand-Normale-Konvention);
`cross`, `lineIntersect(a,da,b,db)`, `along`, `wallBand`, `wallCorners` `:70`, `clippedBand` `:87`.
`src/model/joins.ts`: `computeJoins(project,walls)` `:44` (nur L-Ecken gehrt; T/X eckig).
**Für Offset/Trim/Fillet (Rhino-Kern) gibt es NOCH KEIN 2D-Geometrie-Kernel** — Kurven/Kurven-
Schnitt, Polylinien-Offset usw. musst du ergänzen (siehe Bauplan §3.4).
## 1.5 Store — `src/state/`
- `createStore` `store.ts:51` über `useSyncExternalStore`. **Actions leben IM State**
(`useStore(s=>s.action)`, referenzstabil). `RootState = Project & Selection & View & Layout`
`appStore.ts:21`. Exports `useStore`, `getState`, `setState`.
- **projectSlice**: `project` + `setProject(next|(p)=>p)`. Mutationen u.a. `addFloor`,
`addCategory`, `setElementColor/Weight/Fill`, `resizeElement`, `moveGripOf`, `moveElementByOf`,
`moveEdgeOf`, `commitTransformOn`. **Es gibt keine generische „addWall/addDrawing2d"-Action** —
Tools committen via `setProject`. (Beim Befehlssystem ggf. saubere Actions ergänzen.)
- **Aktive Zeichenebene + aktive Kategorie liegen im viewSlice**, NICHT in selection:
`activeLevelId` `viewSlice.ts:46`/`setActiveLevelId`, `activeCategoryCode` `:42`/`setActiveCategoryCode`.
- selectionSlice: `selectedWallIds[]`, `selectedDrawingId` + Setter/`clearSelection`.
## 1.6 Views, Eingabe, Koordinaten — `src/plan/PlanView.tsx` (SVG-Vektor)
- Modell → `Plan`-Primitive via `generatePlan` `src/plan/generatePlan.ts:203`. `Primitive` =
`polygon|line|arc` (polygons tragen `wallId`/`drawingId` für Hit-Test).
- **Transform (entscheidend):** `PX_PER_M=90` `:20`; `toScreen(p)={x:p.x*90,y:-p.y*90}` `:31`
(fixer Welt-Ursprung 0,0; Y flippt). Invers `viewToModel` `:440`. SVG `viewBox`=State `view`;
Pan/Zoom ändern nur `view`, nie das Modell↔Screen-Mapping.
- **`rawModelAt(clientX,clientY)`** `:446` = aktuelle Mauswelt-Position in Meter (der Eine-Aufruf,
den ein Tool/Befehl braucht). `currentPxPerMeter()` `:488`.
- **Pointer-Events** alle am `<svg>` `:910`: down `:553`, move `:625`, up `:715`, wheel `:820`,
dblclick `:847`, contextmenu `:857`. Schema: Mitte=Pan, Links=Select/Marquee/Tool, Rechts=Menü.
Bei `toolActive` `:268` routen Links-Events zu `toolHandlers`. **PlanView meldet bereits
`(rawModelAt, currentPxPerMeter, toolMods)` nach oben** — neue Tools brauchen hier NICHTS.
- **Snapping** `src/tools/snapping.ts`: `computeSnap(input)` `:88` — endpoint/midpoint/intersection/
onEdge/grid/ortho mit Prioritätstabelle. Wird in App (`snapFor`) konsumiert, nicht in PlanView.
`applyAngleConstraint` für Ortho. `SnapSettings`/`DEFAULT_SNAP` in `tools/types.ts:36/55`.
- 3D `src/viewport/Viewport3D.tsx` (three.js, Raycaster): nur Anzeige+Auswahl, **keine
Zeichenwerkzeuge**. 3D-Authoring = eigene spätere Phase (Raycast auf Arbeitsebene).
## 1.7 Tastatur / globale Eingabe — **kein Dispatch-System**
- `main.tsx:38` globaler `contextmenu`→preventDefault; `:42` blockt Ctrl/Cmd+A außerhalb Inputs;
`isTextEntry(el)` `:24`.
- App-useEffects mit `window.addEventListener("keydown")`: Tool-Tasten `:448`, Delete `:604`,
Transform-Shortcuts m/s/d + u/i/o/p `:637`. **Jeder Guard wiederholt inline den
INPUT/TEXTAREA/contentEditable-Check** — es gibt keine geteilte Keymap. Dein Tab-Handler +
Command-Input kommt als neuer globaler `keydown` dazu (siehe §3.2).
## 1.8 UI-Shell + i18n
- `App.tsx` (~2200 Z., enthält noch ToolController/Grips/Transform). JSX `:1043`: TopBar → body
(Dock links, Content-View-Router, TransformBar, Dock rechts, Floating) → StatusBar →
ResourceManager → ContextMenu → InlineEditor. Panel-Daten via `PanelHostContext` (`baseHost`
`App.tsx:729`, Typ `host.ts`).
- `StatusBar.tsx` — Footer: links `hint` (Tool-Hinweis), rechts X/Y, Einheit, Massstab 1:N, Zoom,
aktives Geschoss, aktive Ebene. **Bester Ort für die Command-Line** (Rhino hat sie klassisch unten).
- **i18n** `src/i18n/`: `t(key,params?)` `index.ts:67`, `useT()` `:84`. Flaches `as const`-Dict,
Punkt-Namespaces (`tool.*`,`snap.*`,`transform.*`,`status.*`…). `de.ts` (Quelle, ~309 Keys) +
`en.ts`; `TranslationKey=keyof typeof de` erzwingt Parität. **Neue Keys IMMER in beide Dateien.**
Keine hartcodierten JSX-Strings.
## 1.9 Verifizieren
- `npx tsc -b` · `npm run build` · Dev `npm run dev` (Vite 5173, `host:true`).
- Screenshot `node scripts/probe.mjs` → `scripts/probe.png` (Puppeteer headless, `deviceScaleFactor:2`,
URL via `PROBE_URL`). Viele task-Probes existieren (`probe-tools.mjs`, `probe-line.mjs`,
`probe-transform.mjs` …) — gute Vorlagen, um Tools/Befehle programmatisch zu treiben.
**Screenshot ansehen + Geometrie prüfen**, nicht nur „kompiliert".
---
# TEIL 2 — Rhino-Referenz (das Interaktionsmodell)
## 2.1 Die Command-Line ist das Rückgrat
**Alles ist ein Befehl**, und die Command-Line **hört immer zu**: Tastenanschläge gehen an die
Command-Line, wenn sie nicht von einem Feld konsumiert werden. Kein „Tool aktiv vs. Eingabe aktiv".
Die Zeile hat gleichzeitig drei Rollen: **Eingabe** (Befehl/Wert tippen), **Prompt**
(„Start of line", „Next point"), **Optionen** (eckige, klickbare Inline-Optionen).
## 2.2 Befehl aufrufen
- Namen tippen, z. B. `Line`. **Präfix-Autocomplete** (case-insensitiv): `L`→`Li`→`Lin` zeigt
Kandidatenliste mit Best-Match. **Tab/Pfeile** akzeptieren Vorschlag, **Enter/Leertaste** führt aus.
- **Aliase**: nutzerdefinierte Kürzel → Makro (z. B. `L`→`!_Line`, `cp`→`!_Copy`). Werden VOR
Autocomplete gematcht. (Minimal: Einzelbuchstabe→Befehl.)
## 2.3 Enter / Leertaste / Rechtsklick (leicht falsch gemacht)
- **Enter = Leertaste** in der Command-Line. Beide: Befehl ausführen / Default akzeptieren /
mehrteiligen Befehl **beenden** / bei **leerer** Zeile **letzten Befehl wiederholen**.
- **Rechtsklick im Viewport = Enter.** Also: Rechtsklick beendet Polyline UND wiederholt bei
leerer Zeile den letzten Befehl. → `lastCommand` speichern, bei Leer-Enter/Rechtsklick neu starten.
## 2.4 Inline-Optionen (klickbare Klammern)
```
Start of line ( BothSides=No Chamfer Mode=Distance ):
```
- Jede Option **klickbar UND tippbar** (genug Buchstaben zur Eindeutigkeit + Enter).
- **Toggle** `Name=Value` flippt beim Klick. **Value**-Option fragt Unterwert ab. **Action**-Option
(ohne `=`) verzweigt sofort.
- Optionen sind **innerhalb des Befehls persistent**, viele **über Aufrufe hinweg** (letzte
Offset-Distanz, Array-Anzahl, Fillet-Radius merken). **Zuletzt benutzte Optionswerte je Befehl
persistieren** — Nutzer erwarten das.
## 2.5 Sub-Prompts = State-Machine
Befehle laufen Prompts ab. `Line`: „Start of line:" → Punkt → „End of line:" → Punkt → fertig.
`Polyline`: „Start" → „Next point ( Close Undo ):" → … → **Enter** beendet. Prompt-Text ist
sichtbar und lehrreich („Next point. Press Enter when done") — literal nachbilden.
## 2.6 Transparente/verschachtelbare Befehle
Manche Befehle (Zoom/Pan, Osnap-Toggle, alles mit `'`-Präfix) laufen **innerhalb** eines anderen,
ohne ihn abzubrechen, und kehren zum Original-Prompt zurück. → Command-Runner braucht einen **Stack**.
## 2.7 Koordinaten- & Numerik-Eingabe (Herz der Präzision)
| Eingabe | Bedeutung |
|---|---|
| `5,3` / `5,3,2` | absolut X,Y(,Z) |
| `r5,3` | **relativ** zum letzten Punkt (das `r`-Idiom) |
| `<45` | Winkel-Constraint auf 45°, dann Maus/Distanz |
| `5<45` | **polar**: Distanz 5 unter 45° vom letzten Punkt |
| Zahl tippen während Drag | **Distanz-Lock**: Richtung per Maus, Länge per Zahl+Enter (meistgenutzte Geste) |
| Zahl + **Tab** | Lock umschalten (Länge fix → Winkel folgt Maus, oder umgekehrt) |
Das Feld parst **kontextabhängig**: Befehlsname / Optionsbuchstabe / Koordinate / nackte Zahl —
je nach Befehlszustand. Das Live-Tool muss **einen primären Skalar** (Länge/Radius/Distanz)
exponieren, an den eine getippte Zahl bindet.
## 2.8 Osnaps + Ortho + Gumball
- **Osnaps** (persistente Toggles): End, Mid, Cen, Int, Perp, Near, Quad, Tan, Point. Pro Mausschritt
gegen nahe Geometrie geprüft (Pixel-Toleranz), Marker+Label am Cursor; liefert **exakte
Modellkoordinate** (nie Roh-Maus, wenn Snap aktiv). One-Shot-Osnap überschreibt für den nächsten Pick.
- **Ortho** (F8): Winkelraster (90°/konfigurierbar), **Shift** togglet temporär. **Grid Snap** (F9).
**SmartTrack**: temporäre Hilfslinien aus zuletzt gehoverten Punkten.
- **Gumball**: On-Object-Widget (Pfeile=Move, Bögen=Rotate, Handles=Scale); Handle klicken →
Zahl tippen für exakten Transform. Direkt-Manipulations-Gegenstück zu getippten Befehlen.
> Präzisionsmodell = **(Snap ODER getippte Koordinate) × (Ortho/Winkel-Constraint) ×
> (Distanz-Constraint)**, in EINEM Pick komponierbar.
## 2.9 Auswahl-Modell (links/rechts-Regel exakt)
- Klick = wählen; Shift+Klick add; Ctrl+Klick remove.
- **Links→rechts = Window** (nur voll umschlossene; **durchgezogenes** Rechteck).
- **Rechts→links = Crossing** (auch berührte; **gestricheltes** Rechteck). Richtung bestimmt
Modus — starke Konvention, exakt nachbilden.
- **SelLast** (zuletzt erzeugte/gewählte erneut wählen) ist enorm nützlich („erzeugen, dann sofort
bewegen"). Min. `SelLast`, `SelAll`, `SelNone`, `Invert`.
## 2.10 Befehls-Prompt-Sequenzen (Kurz)
2D: **Line** (2 Pkt) · **Polyline** (Close/Undo, Enter beendet) · **Rectangle** (Ecke+Ecke, oder
Breite/Höhe tippen; 3Point/Center) · **Circle** (Center+Radius; 2P/3P/Tan) · **Arc** (Center-Start-End /
3Point) · **Offset** (Kurve wählen → Seite klicken/Distanz tippen; Distanz persistent) ·
**Fillet/Chamfer** (Kurve1→Kurve2, Radius/Distances persistent) · **Trim** (Schneider wählen→Enter→
wegzuschneidendes Stück klicken) · **Split** · **Extend** · **Join** · **Explode** ·
**Move/Copy/Rotate/Scale/Mirror** (Auswahl→Basispunkt→Ziel; Copy-Option) · **ArrayRect/ArrayPolar** ·
**Group/Ungroup**.
3D (braucht CSG, später): **ExtrudeCrv** (geschlossene Kurve→Solid, Cap) · **Box** · **Boolean
Union/Difference/Intersection** · **Cap** · **Gumball-Face-Drag = PushPull** · Loft/Sweep/Revolve.
---
# TEIL 3 — Bauplan für DIESE Codebase
> Ziel: nutzbarer 2D-Architektur-Drafter mit Rhino-Feel, dann einfaches Massing. Halte das
> ROADMAP-Prinzip: **ein semantisches Modell → Sichten abgeleitet**; Extrusionshöhe ist eine
> Eigenschaft, nie eingebackene Geometrie.
## 3.0 Kuratierungs-Prinzip (WICHTIG — Nutzer-Vorgabe)
**NICHT den ganzen Rhino-Katalog stumpf portieren.** Wir bauen ein **Wohnbau-BIM**, keinen
NURBS-Allzweck-Modeller. Nimm nur, was dem Wohnbau-Workflow dient; lass den Rest weg, bis er
konkret gebraucht wird. Faustregel: *Brauche ich das, um ein Einfamilienhaus zu zeichnen und
daraus Pläne zu ziehen?* Wenn nein → weglassen.
**Bewusst WEGLASSEN (vorerst):** Loft / Sweep1+2 / Revolve (Sonderformen, kaum Wohnbau) ·
freie NURBS-Kurven (`Curve`/`InterpCrv` Grad>1, Deformable, FromFoci) · Ellipse · Tangent/
Bisector/4Point-Linienvarianten · SmartTrack (nett, nicht kritisch) · der volle `Sel*`-Zoo
(nur SelLast/SelAll/SelNone/Invert) · Knot/Vertex/Tan-Osnaps. Alle leicht später additiv
nachrüstbar — kein Grund, sie jetzt mitzuschleppen.
**Booleans sind KEIN „nice to have später"** — sie werden gebraucht, **sobald Tür/Fenster als
echte 3D-Öffnung** kommen (heute schneidet `Door` nur eine Plan-Lücke, kein 3D-Boolean, siehe
HANDOVER). Darum: CSG/Booleans an die **Tür/Fenster-Phase koppeln** und dann reinnehmen — nicht
ans Ende schieben. ABER (das ist der „nicht stumpf"-Teil):
> Für **rechteckige** Öffnungen in extrudierten Wänden braucht es **keinen allgemeinen
> Boolean-Kernel**. Eine analytische **Wand-minus-Box-Subtraktion** (Öffnung als parametrische
> Aussparung im Wand-Solid) ist einfacher, robuster und für 95 % Wohnbau ausreichend. Den
> allgemeinen CSG-Boolean (`rhino3dm`) erst ziehen, wenn schräge/runde/verschnittene Fälle
> wirklich auftreten. Also: **Öffnungen zuerst analytisch, allgemeine Booleans erst bei Bedarf.**
## 3.1 Leitentscheidung: Engine verallgemeinern, nicht parallel bauen
Baue eine gemeinsame **Command-Engine**, die das bestehende `Tool`-Interface erweitert/ablöst,
sodass es **einen** Eingabepfad gibt (Maus + Tastatur + Command-Line speisen dieselbe Maschine).
Konkret: ein `Command`-Modell, das je Schritt einen **Prompt** (Text), erwartete **Eingabearten**
(Punkt | Zahl | Option | Auswahl) und **Optionen** beschreibt. Die heutigen Tools werden zu
Befehlen dieser Engine (wall/line/polyline/rect lassen sich 1:1 portieren — ihre Phasenlogik ist
schon eine Mini-State-Machine).
**Warum nicht das alte Tool-Interface unangetastet lassen und Command-Line nur draufsetzen?**
Weil die Command-Line getippte Koordinaten/Optionen in denselben Schritt einspeisen muss, in dem
die Maus pickt. Zwei getrennte Pfade divergieren garantiert (Snapping, Constraints, HUD doppelt).
## 3.2 Neue Dateien (Vorschlag)
- `src/commands/engine.ts` — Command-Runner: aktiver Befehl, Prompt-Stack (für transparente
Befehle §2.6), `lastCommand`-Wiederholung, Routing von Maus-Pick / getippter Eingabe / Option-Klick
in den aktuellen Schritt. Hält `CommandState`.
- `src/commands/types.ts` — `Command`-Interface (Verallgemeinerung von `Tool`): Schritte mit
`prompt: TranslationKey`, `accepts: ("point"|"number"|"option"|"selection")[]`, `options: CmdOption[]`,
`onInput(state,input,ctx): [state, CommandResult]`. `CommandResult` wie `ToolResult` (+`commit`).
- `src/commands/parseInput.ts` — Koordinaten-Parser (§2.7): `5,3` · `r5,3` · `5<45` · `<45` ·
nackte Zahl (Distanz-Lock) · Optionsbuchstabe. Liefert eine Discriminated Union, die die Engine
in einen Modellpunkt/Constraint auflöst (mit `lastPoint` für `r`/polar).
- `src/commands/registry.ts` — `COMMANDS: Record<string,Command>` + Aliase + Autocomplete (Präfix).
- `src/ui/CommandLine.tsx` — die Command-Line-UI **in/über der Statusleiste** (`StatusBar.tsx`):
zeigt Prompt + klickbare Optionen + Texteingabe; Autocomplete-Dropdown. Tab fokussiert sie.
- (später) `src/geometry/kernel2d.ts` — 2D-Kernel für Offset/Trim/Fillet/Schnitt (§3.4).
- (viel später) `src/geometry/solid3d.ts` o. `rhino3dm`-Anbindung für Massing/Booleans (§3.5).
## 3.3 Verdrahtung (minimal-invasiv)
- **Globaler Tab-Handler**: neuer `window.keydown` in App (gleicher Guard wie `App.tsx:448` —
INPUT/TEXTAREA/contentEditable überspringen). Tab → Command-Line fokussieren/öffnen. Jeder
getippte Buchstabe ohne aktives Tool startet den Befehlsmodus (Rhino „hört immer zu" — optional
in Phase 2; Phase 1 reicht Tab).
- **Command-Line → Engine → Store**: Befehle dispatchen auf `setActiveTool` (für tool-artige) bzw.
direkt auf Store-Actions / `setProject`. Nutze `getState()/setState()` (referenzstabil) aus
`appStore.ts`.
- **Pick-Eingabe**: die Engine konsumiert dieselben `(rawModelAt, currentPxPerMeter, toolMods)`,
die PlanView schon hochmeldet (`ToolHandlers`). `computeSnap` für Punktfang wiederverwenden.
→ PlanView braucht im Idealfall **keine Änderung** (höchstens: Window/Crossing-Marquee-Visual
durchgezogen vs. gestrichelt nach Drag-Richtung, §2.9 — heute evtl. nur ein Modus).
- **Prompt/HUD**: Prompt-Text in die Statusleiste (`StatusBar` `hint` existiert schon). Distanz/
Winkel-HUD am Cursor existiert in `ToolDraft.hud`.
## 3.4 Reihenfolge (Tiers — strikt 2D zuerst)
**Tier 0 — Substrat (VOR jedem Befehl; das ist der „Feel"):**
1. Command-Runner + Command-Line-UI (Prompt → pick/type → Optionen; Enter/Space/Rechtsklick =
bestätigen/beenden/wiederholen; `lastCommand`).
2. Koordinaten-Parser (`x,y` · `rdx,dy` · `dist<angle` · nackte-Zahl-Lock).
3. Osnaps (End/Mid/Cen/Int/Perp/Near) — `computeSnap` ist da, ggf. Cen/Perp/Near ergänzen.
4. Ortho (90°/45°, Shift-Toggle) + Grid-Snap — teils vorhanden (`applyAngleConstraint`).
5. Auswahl: Klick, Shift/Ctrl add/remove, **Window vs. Crossing** (durchgezogen/gestrichelt,
links/rechts-Regel).
**Tier 1 — 2D-Pflicht (reines SVG/2D), grobe Baufolge:**
6. **Line** (validiert die ganze pick/snap/constrain-Schleife) → 7. **Polyline** (Close/Undo) →
8. **Rectangle** (Ecke + Center/3Point) → 9. **Circle** (Center+Radius). Diese vier portieren die
heutigen Tools auf die Engine + numerische Eingabe.
10. **Move** → 11. **Copy** (wiederholend) → 12. **Offset** (persistente Distanz — DAS Architektur-
Primitiv) → 13. **Trim** + **Split** → 14. **Join** + **Explode**. **Undo/Redo** durchgängig
annehmen (heute? — prüfen; ggf. Command-History/Undo-Stack im Store ergänzen).
**Tier 2 — 2D stark nützlich:** Rotate/Scale/Mirror (Copy-Option) · Fillet/Chamfer · Arc ·
Extend · ArrayRect/ArrayPolar · Group/Ungroup · Gumball(2D) · Sel*-Helfer (min. SelLast).
**Tier 3 — Massing + Öffnungen (an Tür/Fenster-Phase gekoppelt):**
- **Öffnungen zuerst analytisch:** Tür/Fenster als parametrische Aussparung im Wand-Solid
(Wand-Extrude minus Öffnungs-Box) — KEIN allgemeiner Boolean-Kernel nötig (§3.0). Das ist der
kritische, roadmap-markierte 🔴-Teil (echte 3D-Öffnung statt nur Plan-Lücke) und kommt MIT
Tür/Fenster, nicht danach.
- **Massing-Befehle:** ExtrudeCrv (geschlossene Plan-Kurve → gecapptes Solid) → Box →
Gumball-Face-Drag-PushPull.
- **Allgemeine Booleans** (Union/Difference/Intersection) **erst bei Bedarf** (schräge/runde/
verschnittene Fälle): **kein eigener Kernel — `rhino3dm` (WASM-openNURBS)** als `src/io/`-Schicht
(Roadmap-Entscheid, HANDOVER). Bis dahin reicht die analytische Subtraktion.
- **Weggelassen:** Loft/Sweep/Revolve/OffsetSrf (§3.0 — Sonderformen, kaum Wohnbau).
## 3.5 Was sauber 2D ist vs. was hart ist
- **Sauber SVG/2D:** Line, Polyline, Rect, Circle, Arc, Move/Copy/Rotate/Scale/Mirror, Array, Group,
Control-Point-Edit, Gumball(2D). Affine Transforms + Kurven-Schnitt.
- **Echte Arbeit (2D-Kernel nötig):** **Offset, Trim, Fillet** brauchen kompetenten Kurven-Schnitt
und Polylinien-Offset — dafür Zeit einplanen (`src/geometry/kernel2d.ts`).
- **Braucht 3D/CSG:** Extrude, Box, Boolean*, Cap, OffsetSrf, Loft/Sweep/Revolve, Face-Drag. Booleans
sind das Korrektheits-Zentrum → `rhino3dm`.
## 3.6 Gotchas (aus Rhino-Verhalten + dieser Codebase)
- **Command-Line hört immer zu** — Tasten global routen, aber die `isTextEntry`-Disziplin
(`main.tsx:24`) + die Native-App-Regeln (kein Ctrl+A/keine Textauswahl, CONVENTIONS.md) wahren.
- **Zuletzt benutzte Optionswerte je Befehl persistieren** (Offset-Distanz, Array-Anzahl, Fillet-Radius).
- **Enter = Rechtsklick = Wiederholen/Bestätigen/Mehrteiliges-Beenden** — alle drei auf EIN Signal.
- **Distanz-Lock:** Live-Befehl muss EINEN primären Skalar exponieren, an den eine getippte Zahl bindet.
- **Window vs. Crossing** über Drag-Richtung + durchgezogen/gestrichelt — nicht global ein Modus.
- **Osnap liefert exakte Modellkoordinate** — nie Roh-Maus, wenn Snap aktiv (`ToolPointer.point`).
- **Strict tsc** (`noUnusedLocals`) — ungenutzte Vars/Parameter brechen `npm run build`.
- **i18n**: jeder sichtbare String über `t('key')`, Keys in `de.ts` UND `en.ts` (Parität erzwungen).
- **Keine generische addWall/addDrawing2d-Action** — entweder via `setProject` committen (wie heute)
oder beim Refactor saubere Actions im `projectSlice` ergänzen (besser für Undo/Redo).
- **App.tsx ist bereits ~2200 Z.** (God-Component-Kritik in HANDOVER). Lege Command-Engine in
`src/commands/`, halte App-Verdrahtung dünn (nur Tab-Handler + CommandLine-Mount + Dispatch-Brücke).
## 3.7 Verifikations-Drehbuch
Pro Tier eine Probe (Vorlage: `scripts/probe-tools.mjs`/`probe-transform.mjs`): Befehl per Command-
Line tippen → Punkte/Werte tippen → Screenshot → Geometrie visuell prüfen. Tier 0 zuerst headless
treiben (Tab → „line" → „0,0" Enter → „r3,0" Enter → Linie im PNG sichtbar). `npx tsc -b` +
`npm run build` grün halten. **Screenshot ansehen**, nicht nur Kompilat vertrauen (Memory `wire-dont-stub`).
---
## Anhang — Minimaler erster Meilenstein (konkret)
1. `src/commands/types.ts` + `engine.ts` + `parseInput.ts` (Tier 0.1/0.2).
2. `src/ui/CommandLine.tsx`, in `StatusBar` gemountet; Tab-Handler in App.
3. `Line` als erster Command (portiert `lineTool`), inkl. getippter `0,0` / `r3,0` / `3<45`.
4. Probe `scripts/probe-command-line.mjs`: Tab→line→zwei getippte Koordinaten→PNG prüfen.
5. Dann Polyline/Rect/Circle, danach Move/Copy/Offset.
Damit steht der Rhino-Feel-Kern, und jeder weitere Befehl ist additiv (neues `Command`-Objekt in
`registry.ts`, keine PlanView-/App-Änderung).
+52
View File
@@ -0,0 +1,52 @@
# Zustands-Architektur & Code-Aufteilung (App.tsx entschlacken)
> Ziel: `App.tsx` von „God-Component" zu dünnem Shell. Globaler Zustand in einen
> Store, Features in eigene Module → **wartbar + parallel bearbeitbar**.
## Problem
`App.tsx` hält aktuell: Projekt-State, Auswahl, View-State (viewType/detailLevel/
renderMode/referenceLines/scale/activeLevel), Layout, ALLE Mutations-Handler
(floors/layers/components/hatches/lineStyles/walls/doors), Kontextmenü-Builder,
Inline-Editoren, Layout-Menü, View-Routing. → Risiko + Flaschenhals (jedes Feature
fasst App.tsx an → kein paralleles Arbeiten).
## Zielstruktur
```
src/state/
store.ts // Store (Zustand) — kombiniert die Slices, ein useStore-Hook
projectSlice.ts // project + alle Mutationen (floors, layers, components,
// hatches, lineStyles, walls, doors) inkl. recompute/refs-aware delete
selectionSlice.ts // selectedWallIds (+ marquee-Ergebnis)
viewSlice.ts // viewType, detailLevel, renderMode, referenceLines, activeLevelId, scale
layoutSlice.ts // wraps src/panels/layout.ts (docks + floating)
src/views/ // LevelPlanView, PerspectiveView, SectionStub, DrawingView (+ ViewRouter)
src/editors/ // FloorSettingsEditor, LayerSettingsEditor, … (heute inline in App)
src/menus/ // layerContextMenu(), levelContextMenu(), planContextMenu() — bauen ContextMenu-Items
src/ui/ // TopBar, StatusBar, ResourceManager, ContextMenu (bestehen)
src/panels/ // Docks/Panels (bestehen)
App.tsx // DÜNN: Store-Provider · TopBar · (Docks + ViewRouter) · StatusBar
// · Floating-Panels · Ressourcen-Overlay
```
## Store-Wahl: Zustand (empfohlen)
- Winzige Lib, kein Boilerplate, **Slices** gut teilbar, Selektoren verhindern
Re-Render-Sturm, kein Prop-Drilling. Passt zu „verschiedene Features = verschiedene
Slice-Dateien" → Parallelität.
- Alternative ohne Dependency: Context + useReducer oder `useSyncExternalStore`
(wie i18n). Mehr Boilerplate; bei der State-Menge ist Zustand ergonomischer.
- Komponenten: `const walls = useStore(s => s.walls)` / `useStore(s => s.addFloor)`.
PanelHostContext entfällt (Panels lesen direkt aus dem Store).
## Vorgehen (reiner Refactor — Verhalten MUSS identisch bleiben)
1. Store + Slices anlegen, Projekt-State + Mutationen aus App.tsx hierher ziehen
(1:1, gleiche Logik inkl. recomputeFloorElevations, refs-aware delete).
2. View-/Selection-/Layout-State in ihre Slices.
3. Inline-Editoren, Kontextmenü-Builder, View-Routing in `src/editors/`,`src/menus/`,`src/views/` extrahieren; sie lesen den Store.
4. App.tsx auf den Shell reduzieren.
5. Verifizieren: tsc + build + Screenshots — **pixel-/funktionsgleich** zu vorher
(Auswahl, Massstab, Kontextmenü, Panels, 3D/Plan, i18n). Reiner Umbau, kein
Feature-Wechsel.
## Auszahlung
- Danach editiert ein Wand-Feature `projectSlice`/`views`, ein Panel-Feature `panels`,
ein Editor `editors` — **disjunkte Dateien → mehrere Code-Workflows parallel** möglich.
+306
View File
@@ -0,0 +1,306 @@
# Architektur-Pivot: Tauri + Rust-Backend (2026-07-01)
## Entscheidung
**Alte Welt:** Browser-CAD (React/Vite + WebGL/three.js)
**Neue Welt:** Desktop Tauri-App (React/Vite Frontend + Rust-Backend + **wgpu 3D-Rendering**)
**Grund:** Komplexe Möbel mit vielen Polygonen (100k+) + mehrere parallele rechenintensive Ops → WebGL/three.js wird zum Bottleneck. **wgpu** (low-level GPU-API auf Vulkan/Metal/DX12) + Rust-Compute skaliert native.
**Scope:** Desktop-only Release (Milestone 1). WASM-Fallback für Browser später wenn gebraucht.
---
## Post-Migration Stack
### Frontend (React/Vite — Komponenten + State, unverändert)
```
src/
App.tsx ← Shell-Komponente
compute/index.ts ← Compute-Boundary (neu)
model/types.ts ← Semantisches Modell
commands/ ← Befehlssystem
panels/ ← UI-Panels
plan/PlanView.tsx ← 2D-SVG-Rendering
viewport/Viewport3D.tsx ← three.js 3D-Display
ui/ ← Topbar, Dialogs, etc.
state/ ← Redux-Slices (project, selection, view, layout)
...
```
**Rolle:** User-Input-Handling, 2D/3D-Darstellung (Display-Layer), State-Management.
### Backend (Rust/Tauri — neu)
```
src-tauri/
src/
main.rs ← Tauri window + invoke-handler registration
geometry.rs ← compute_joins(), kernel2d(), etc.
parsers/
dwg.rs ← DXF/DWG-Geometrie-Parsing
dxf.rs
sia/
room_detection.rs ← detectRooms() (SIA-416)
...
Cargo.toml ← Dependencies (serde, tauri, …)
```
**Rolle:** Rechenintensive Ops, Geometrie-Kernel, Parsing, SIA-Raumerkennung.
### IPC: Tauri invoke (async, serde)
```typescript
// Frontend ruft Rust auf
const result = await invoke<JoinInfo[]>('compute_joins', { walls, joints });
// Rust bearbeitet + serialisiert Ergebnis
#[tauri::command]
async fn compute_joins(input: JoinInput) -> Result<JoinOutput, String> {
geometry::compute_joins(input).map_err(|e| e.to_string())
}
```
---
## Compute-Boundary (Key Design)
**Neue Datei:** `src/compute/index.ts` — einziger Eingang für rechenintensive Ops.
```typescript
// Beispiel-Schnittstellen
export async function computeJoins(walls: Wall[], joints: Array<[number, number]>): Promise<JoinInfo[]> { … }
export async function computeKernel2D(op: 'offset'|'trim', geom: Polyline, …): Promise<Polyline[]> { … }
export async function detectRooms(…): Promise<Room[]> { … }
export async function parseShapeFromDwg(…): Promise<DwgGeometry> { … }
```
**Hinter der Boundary:**
1. Versuche Tauri invoke zu Rust (`#[tauri::command]`)
2. Fallback auf lokale TS-Impl wenn Rust nicht verfügbar (während Migration)
**Effekt:** Aufrufort im Code bleibt stabil; Caller sieht nicht, ob Op schon in Rust ist oder noch TS.
**Migrationsfluss:**
```
1. TS-Impl existiert (z.B. src/model/joins.ts)
2. Neue Op in Compute-Boundary mit Invoke+Fallback
3. Parallel: Rust-Impl in src-tauri/src/geometry.rs
4. Tests: Rust-Output == TS-Output (Parität)
5. TS-Impl bleibt (Fallback, wird nicht entfernt bis Rust stable)
6. Eventuell: TS-Impl löschen wenn Rust bewährt
```
---
## Dev-Workflow (post-Tauri)
### Development
```bash
# Terminal 1: Vite dev-server
npm run dev # localhost:5173
# Terminal 2: Tauri dev
npm run tauri:dev # öffnet Tauri-Fenster, zeigt auf localhost:5173
# Rust hot-reload + TS hot-reload gleichzeitig
```
**Voraussetzungen:**
- Node.js + npm (wie heute)
- Rust + Cargo (neu)
- Tauri CLI: `npm install -D @tauri-apps/cli`
### Build
```bash
# Single command
npm run tauri:build
# Erzeugt:
# - Windows: src-tauri/target/release/cad.exe
# - macOS: src-tauri/target/release/bundle/macos/cad.app
# - Linux: src-tauri/target/release/bundle/deb/cad_*.deb (oder Flatpak)
```
### Testing
```bash
# Rust-Unit-Tests
cargo test # in src-tauri/
# TS-Tests (unverändert)
npm run test
# Integration-Test: App starten + Aktion prüfen
npm run tauri:dev # manuell testen oder Puppeteer-Probe erweitern
```
---
## GPU-Strategy (für später)
**Milestone 1 (jetzt):** CPU-Ops in Rust (kernel2d, joins, parsing, SIA).
**Milestone 2 (später):** GPU-Compute via wgpu
- Tauri + wgpu Renderer (optional, nicht erforderlich)
- ODER drei.js bleibt, Rust handelt CPU-Ops, three.js handelt Display
- GPU-Heavy-Ops (z.B. große Boolean-Operationen) können in wgpu laufen, aber MVP braucht das nicht
**Aktueller Plan:** three.js bleibt für 3D-Display (skaliert ausreichend für Möbel-Geometrie mit Instancing + LOD).
---
## Migration Strategy: Ops nach Priorisierung
**Phase 1 (aktuell — Tauri-Shell + Proof-of-Concept):**
- [ ] `computeJoins` (Wand-Eckverbindungen) → Rust
- Gründe: klein, häufig, zeigt invoke-Flow
**Phase 2 (nächst):**
- [ ] `kernel2d` (Offset/Trim/Extend/Fillet) → Rust
- Gründe: Rechenlast ⭐⭐, Frequenz hoch
- [ ] DXF/DWG-Parser → Rust (Geometrie-Extraktion)
- Gründe: Rechenlast ⭐⭐, Frequenz mittel (Import-Dialog)
**Phase 3 (später):**
- [ ] `detectRooms` (SIA-416 Raumerkennung) → Rust
- Gründe: Rechenlast ⭐, async-freundlich
- [ ] `booleanOps` (Union/Differenz/Schnitt) → Rust
- Gründe: Rechenlast ⭐⭐, Frequenz gering (ad-hoc)
---
## Folgen für bestehenden Code
### Was ändert sich NICHT
- `src/model/types.ts` — semantisches Modell bleibt in TS (Frontend kennt es)
- `src/state/` — Redux-Store unverändert
- `src/ui/` — Komponenten unverändert
- `src/plan/PlanView.tsx` — SVG-Rendering unverändert
- `src/viewport/Viewport3D.tsx` — three.js-Rendering unverändert
- `src/commands/` — Befehlssystem unverändert
### Was ändert sich
- **Neue `src/compute/index.ts`** — alle rechenintensiven Ops laufen durch hier
- **Neue `src-tauri/`** — Rust-Backend
- **Vite-Config:** Tauri plugin hinzufügen
- **Package.json:** tauri scripts hinzufügen
- **Build-Prozess:** `npm run tauri:build` statt `npm run build`
### Was wird migriert (schrittweise)
- `src/model/joins.ts` → `src-tauri/src/geometry.rs` (Phase 1)
- `src/geometry/kernel2d.ts` → `src-tauri/src/geometry.rs` (Phase 2)
- `src/io/{dxfParser, dwgParser}.ts` → `src-tauri/src/parsers/` (Phase 2)
- `src/geometry/{roomArea, roomBoundary}.ts` → `src-tauri/src/sia/room_detection.rs` (Phase 3)
- `src/editors/booleanOps.ts` → `src-tauri/src/geometry.rs` (Phase 3)
**Wichtig:** TS-Versionen bleiben als Fallback (nicht gelöscht).
---
## Distribution (später)
### Desktop Binaries (post-Tauri)
- **Windows:** `.exe` (standalone executable)
- **macOS:** `.app` bundle (code-signed)
- **Linux:** `.deb` package ODER **Flatpak** (preferred)
- Flatpak = moderne WebKitGTK6 immer dabei, unabhängig von Distro-Alter
### Browser (wenn gebraucht)
- **WASM-Fallback** für `src/compute/` Ops (Rust → WASM via wasm-bindgen)
- Later-phase feature, nicht Milestone 1
---
## Technische Details
### Serialisierung (TS ↔ Rust)
**serde + serde_json** für Geometrie-Typen:
```rust
// Rust
#[derive(Serialize, Deserialize)]
pub struct Vec2 { pub x: f64, pub y: f64 }
#[derive(Serialize, Deserialize)]
pub struct Wall {
pub id: String,
pub start: Vec2,
pub end: Vec2,
// …
}
```
```typescript
// TS (type-safe invoke)
interface Vec2 { x: number; y: number }
interface Wall { id: string; start: Vec2; end: Vec2; /* … */ }
await invoke<JoinInfo[]>('compute_joins', { walls: Wall[] })
```
### Tauri Security (default)
- Invoke-Handler sind Rust-side validiert
- Whitelist-Makro (`#[tauri::command]`) registered nur explizit erlaubte Functions
- CORS/CSP Policy default secure
- Keine arbitrary-Script-Execution (native app)
---
## Abhängigkeiten (neu post-Tauri)
### Frontend (npm)
- Bestehende: react, vite, three.js, redux, etc.
- Neu: `@tauri-apps/api` (JS-SDK für invoke)
- Optional später: `@tauri-apps/cli` dev-dependency (bereits in package.json)
### Backend (Cargo)
```toml
[dependencies]
tauri = { version = "2", features = ["webkit2gtk-6.0"] } # GTK4
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# später: wgpu, delaunator, opencascade-sys, etc.
```
### System
- Rust 1.70+
- GTK4 dev libraries (Linux only, auto-handled by Tauri)
- Xcode Command Line Tools (macOS, auto-checked)
---
## Next Steps (Koordination)
**Aufgabe für nächste Phase:**
→ Siehe `docs/design/tauri-migration-plan.md` (Schritt-für-Schritt, vier parallele Agents)
**HANDOVER.md:** wird aktualisiert nach Tauri-Shell stabil.
---
## FAQ
**Q: Läuft die App noch im Browser?**
A: Nein (Milestone 1). Desktop-only. WASM-Fallback für Browser später wenn gebraucht.
**Q: Was passiert mit dem existing TS-Code?**
A: Bleibt unverändert (außer neue Compute-Boundary). TS-Implementierungen = Fallback bis Rust stabil.
**Q: Muss ich Rust können um das Projekt zu verstehen?**
A: Nein. Frontend bleibt React/TS. Rust ist "blackbox" hinter invoke. Aber bei Rust-Bugs muss man rein.
**Q: Wann ist Tauri-Shell fertig?**
A: Nach den vier Agents (Schritt 1–4 in tauri-migration-plan.md), ~1–2 Wochen.
**Q: Kann ich lokal testen?**
A: Ja, `npm run tauri:dev` öffnet lokale App. Alles wie heute, nur Rust hinten dran.
+243
View File
@@ -0,0 +1,243 @@
# Tauri-Migration + Compute-Boundary — Aufgabe für nächste Instanz
**Entscheidung (fix, 2026-07-01):** Browser-CAD → **Desktop Tauri-App mit Rust-Backend + wgpu-Rendering.**
**Grund:** komplexe Möbel mit vielen Polygonen (100k+) + mehrere parallele rechenintensive Ops → WebGL/three.js wird zum Blocker. **wgpu** (low-level GPU-API) + Rust-Compute skaliert native.
**Scope:** Desktop-only Release (Milestone 1). WASM-Fallback für Browser später wenn gebraucht.
**Rendering-Engine:** three.js → **wgpu** (Milestone 2)
---
## Aufgabe: Shell aufsetzen + Compute-Boundary + erste Op migrieren
### Schritt 0 — Compute-Boundary (TS-Kontrakt)
**Neu:** `src/compute/index.ts` — die einzige Stelle, durch die alle rechenintensiven Ops laufen.
```typescript
// src/compute/index.ts — einheitliche Schnittstelle
export async function computeKernel2D(op: 'offset'|'trim', …): Promise<Polyline[]> { … }
export async function detectRooms(…): Promise<Room[]> { … }
export async function computeJoins(…): Promise<JoinInfo[]> { … } // ← erste Op
export async function parseShapeFromDwg(…): Promise<DwgGeometry> { … }
```
**Hinter der Boundary:**
- Erst Tauri-invoke zu Rust `#[tauri::command]`
- Fallback auf lokale TS-Impl (bleibt unberührt, bis Rust stabil)
- `catch(err) → console.warn('Rust failed, using TS fallback'); return tsImpl(…)`
**Effekt:** Aufrufort im Code bleibt stabil; Caller sieht nicht, ob Op schon in Rust ist oder noch TS.
---
### Schritt 1 — Tauri-Shell aufsetzen
**Struktur:**
```
repo/
src-tauri/ ← neue Rust-Seite (Tauri-Konvention)
src/
main.rs ← Tauri window + invoke handlers
geometry.rs ← compute_joins() + weitere Ops später
...
Cargo.toml
src/ ← React/TS (unverändert)
compute/
index.ts ← Compute-Boundary
...
vite.config.ts ← Tauri plugin integrieren
package.json ← tauri scripts
```
**Setup:**
1. `cargo init --name cad-tauri src-tauri` (oder `src-tauri` manuell anlegen)
2. `Cargo.toml`: Tauri v2 einbinden mit Feature `webkit2gtk-6.0` (GTK4)
```toml
[dependencies]
tauri = { version = "2", features = ["webkit2gtk-6.0"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
```
3. `src-tauri/src/main.rs`: Minimal-Fenster, invoke-Handler registrieren
```rust
#[tauri::command]
async fn compute_joins(input: JoinInput) -> Result<JoinOutput, String> {
// Rust-Impl
geometry::compute_joins(input).map_err(|e| e.to_string())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![compute_joins])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
```
4. `vite.config.ts`: Tauri plugin + dev-server-Integration
```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: { port: 5173 } // Tauri dev zeigt hier drauf
})
```
5. `package.json`: Tauri scripts hinzufügen
```json
"scripts": {
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build"
}
```
---
### Schritt 2 — Erste Op migrieren: `computeJoins` (Wand-Eckverbindungen)
**Warum `computeJoins` zuerst?**
- Läuft häufig (bei jedem Wall-Edit)
- Klein und fokussiert (~50 Zeilen Kernlogik)
- Proof-of-Concept für Tauri-invoke-Flow
- Danach `kernel2d` parallel hochfahren
**Rust-Impl:** `src-tauri/src/geometry.rs`
```rust
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Vec2 { pub x: f64, pub y: f64 }
#[derive(Serialize, Deserialize)]
pub struct WallJoinInput {
pub walls: Vec<Wall>,
pub joints: Vec<(usize, usize)>, // wall indices
}
#[derive(Serialize, Deserialize)]
pub struct JoinInfo { /* … */ }
pub fn compute_joins(input: WallJoinInput) -> Result<Vec<JoinInfo>, Box<dyn std::error::Error>> {
// Port der Logik aus src/model/joins.ts
// L-Ecken, T-Stösse, +-Kreuzungen
Ok(vec![]) // Placeholder
}
```
**TS-Fallback bleibt:** `src/model/joins.ts` (LS vor Rust-Port)
**Compute-Boundary:** `src/compute/index.ts`
```typescript
export async function computeJoins(walls: Wall[], joints: Array<[number, number]>): Promise<JoinInfo[]> {
try {
return await invoke<JoinInfo[]>('compute_joins', { walls, joints });
} catch (err) {
console.warn('Rust compute_joins failed, using TS fallback:', err);
return joinsTS(walls, joints); // Fallback
}
}
```
---
### Schritt 3 — Migrations-Parität testen
**Test-Fixtures:**
- Aus `src/model/sampleProject.ts` exportieren: Wand-Arrays mit bekannten L/T/±-Konfigurationen
- Rust-Unit-Tests: gleiche Fixtures → gleiche JoinInfo-Outputs
- Vergleich: `actual == expected`
**Beispiel (Rust):**
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_l_corner() {
let input = WallJoinInput { /* L-shaped walls */ };
let result = compute_joins(input).unwrap();
assert_eq!(result[0].kind, JoinKind::LCorner);
}
}
```
---
### Schritt 4 — Vite/Tauri-Integration
**Dev-Workflow:**
```bash
npm run tauri:dev
# → Vite dev-server (localhost:5173) lädt React-App
# → Tauri-window zeigt auf :5173
# → invoke() ruft Rust-Commands auf
```
**Build-Workflow:**
```bash
npm run build # Vite → dist/
npm run tauri:build # Tauri packt dist/ + Rust-Binary
```
---
## Deliverable (Proof-of-Concept)
**Git-Stand nach dieser Aufgabe:**
- [ ] `src-tauri/` Verzeichnis mit `Cargo.toml` + `src/main.rs` + `src/geometry.rs`
- [ ] `Cargo.toml` buildet sauber (`cargo check` 0 Fehler)
- [ ] `src/compute/index.ts` mit `computeJoins()` Schnittstelle (invoke + TS-fallback)
- [ ] Rust `compute_joins()` implementiert, Tests pass (`cargo test`)
- [ ] `package.json` `tauri` scripts hinzugefügt
- [ ] **App läuft:** `npm run tauri:dev` → Tauri-Fenster öffnet, Wand-Edit triggert Rust-Op, Output identisch TS-Version
- [ ] **Trace-Scan sauber** (kein TS/Rust-Code übrig, beide Impl. aktiv)
- [ ] HANDOVER.md aktualisiert: `computeJoins` migriert, nächste Ops in Queue
**Verification:**
```bash
# Build-Check
cargo check # 0 Fehler
# Rust-Tests
cargo test
# App end-to-end
npm run tauri:dev
# → Wand zeichnen + editieren → computeJoins() über Rust aufgerufen
# → Plan + 3D aktualisiert wie vorher
```
---
## Nächste Ops (Priorisierung)
Nach `computeJoins` stabil:
1. **`kernel2d`** (Offset/Trim/Extend) — großer Hebel, läuft häufig
2. **DXF/DWG-Parser** (Geometrie) — heavy, aber niedrige Frequenz
3. **`detectRooms`** (SIA-Raumerkennung) — async, kann auf Hintergrund ziehen
4. **`booleanOps`** (Union/Differenz/Schnitt) — Kandidat für später
---
## Offene Punkte (NICHT jetzt)
- **GPU-Compute (wgpu):** Erst nach CPU-Ops stabil (kernel2d, joins, parsing)
- **WASM-Fallback:** Nur wenn Browser-Support nötig wird
- **Flatpak-Distribution:** Nach Tauri-Shell stable + erste Ops migriert
---
## Referenzen
- Tauri v2 Docs: https://tauri.app/v1/guides/getting-started/prerequisites
- Serde: https://serde.rs/
- CONVENTIONS.md: Identifiers englisch, UI-Text via `t()`, …
- Commit-Regel: kein AI-Attribution im Repo
+151
View File
@@ -0,0 +1,151 @@
# Oberleiste – Angleichung an DOSSIER (Umsetzungs-Spezifikation)
Verbindliche Vorlage für den Umbau der Top-Bar (`src/ui/TopBar.tsx`,
`src/styles.css`, Verdrahtung in `src/App.tsx`). Quelle: das DOSSIER-Rhino-
Plugin (`ToolbarApp.jsx`, `components/BarControls.jsx`, `TextEditorApp.jsx`).
Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md).
## 0. Grundprimitive (neu, DOSSIER-konform)
Alle Leisten-Controls teilen dieselbe Höhe und Pillenform.
- `BAR_H = 22px` Basis-Höhe. Segmentierte Pillen `BAR_H + 2 = 24px`
(`box-sizing:border-box`, 1px Rand inbegriffen).
- Pille: `border:1px solid var(--border)`, `border-radius:999px`,
`background:var(--input)`. Hover (interaktiv): `border-color:var(--accent-border)`,
`background:var(--accent-dim)`.
- Aktiver Zustand (Toggle AN / aktive Segmentzelle): `background:var(--accent)`,
`color:#fff`.
### BarCombo (Pillen-Dropdown)
Wir haben bereits `src/ui/Dropdown.tsx`. Der Dropdown-Trigger MUSS optisch der
Pille entsprechen (Höhe 24, radius 999, obige Farben). Ein optionales Icon sitzt
LINKS **ausserhalb** der Pille (18px breit, `var(--muted)`), ein optionaler
Zahnrad-Knopf („settings") sitzt rechts **innerhalb** der Pille. Prüfen, ob
`Dropdown` bereits so aussieht; falls nicht → Trigger-CSS angleichen (Klasse
`tb-dd-trigger`). KEINE zweite Dropdown-Implementierung bauen.
### Segmentpille (3er/4er)
Aussencontainer `display:inline-flex; height:24px; border:1px solid var(--border);
border-radius:999px; overflow:hidden`. Zellen ohne eigenen Radius; interne Trenner
über `border-left:1px solid var(--border)` (erste Zelle ohne). Aktive Zelle
`var(--accent)`/#fff, inaktiv `var(--input)`/`var(--ink)`, Hover
`var(--accent-dim)`/`var(--accent)`. Genutzt für: Ansichts-Icons, Zoom (%/fit/center),
**B/I/U**, **L/C/R**.
### BarButton (quadratischer Icon-Knopf)
22×22, `border-radius:999px`, sonst wie Pille. Aktiv = Akzentfüllung, Icon #fff.
## 1. Reihenfolge der Gruppen (links → rechts)
1. Marke (bestehend, unverändert).
2. Ansichts-Gruppe (bestehend `view-grid`; Zellen auf Segmentpillen-Look bringen).
3. Sichtbarkeits-Kombinationen (bestehend, `BarCombo`-Look).
4. Detailgrad + Massstab (gestapelt, `BarCombo`-Look).
5. **Massstab/Zoom-Cluster NEU** (siehe §2) — ersetzt die heutige Gruppe mit der
DOPPELTEN Zoom-Anzeige.
6. Darstellungsart (bestehend, `BarCombo`).
7. **Text-Gruppe NEU** (siehe §3) — die zentrale neue Leiste.
8. Referenzlinien / Linien-Modus (bestehend).
9. Rechts: Layout · Ressourcen · Projektname.
## 2. Massstab/Zoom-Cluster (ersetzt Doppel-Zoom-Bug)
HEUTE FALSCH: In `TopBar.tsx` wird `tb-zoom` (Zoom %) ZWEIMAL gerendert
(einmal im `tb-zoomstack`, einmal darunter als eigener `<span>`). Die zweite,
lose `<span className="tb-zoom">…%</span>` ersatzlos ENTFERNEN.
NEUES Layout — 2×2-Raster (`display:grid; grid-template-columns:auto auto;
gap:4px 6px; align-items:center`):
- **Spalte 1, beide Zeilen** (`grid-row:1 / span 2`): EINE kombinierte Stat-Pille,
`width:70px`, Höhe `BAR_H*2+6 = 50px`, `border-radius:14px` (NICHT 999),
`border:1px solid var(--border)`, `background:var(--input)`, Innen zwei Zeilen
mittig, getrennt durch 1px-Linie (`var(--border)`):
- oben: Live-Massstab `1:N` (Akzentfarbe, `var(--font-mono)`, 11px, 700)
- unten: Zoom `NN%` (`var(--ink-2)`, mono, 11px)
- „am Massstab" (Zoom==gewählter Massstab): Pille `background:var(--accent-dim)`,
`border-color:var(--accent)`, Text `var(--accent)`.
- Nicht-Plan-Ansicht: beide Werte „—".
- **Spalte 2, Zeile 1**: Massstab-Dropdown (`BarCombo`, ~140px, mono) + Print/PDF
bleibt separat. (Massstab-Dropdown ist der bestehende `scaleOptions`-Dropdown.)
- **Spalte 2, Zeile 2**: Zoom-Segmentpille mit 3 Zellen — `%` (=`onZoom100`,
Label „1:1"/100 %), `fit_screen` (=`onFit`), `center_focus_strong`
(=`onFitSelection`). Material-Icons. Daneben ggf. Referenzlinien-BarButton.
Export-Knöpfe (PDF/DXF) wandern in eine eigene kleine BarButton-Reihe rechts vom
Cluster (Icons `picture_as_pdf` / `download`) ODER bleiben Pillen — Hauptsache
NICHT mehr Teil des Zoom-Blocks, damit der Cluster ruhig bleibt.
## 3. Text-Gruppe in der Oberleiste (NEU – Kern dieser Aufgabe)
Immer sichtbar. 3×2-Raster (`grid-template-columns:110px 130px 80px; gap:4px 6px`).
Setzt Defaults für neuen Text UND formatiert die aktuelle Auswahl live.
Zeile 1:
- **Stil-Preset** `BarCombo` (110px): Optionen aus `DEFAULT_PRESETS`
(Titel/Untertitel/Label/Notiz) + „— Stil —".
- **Font** `BarCombo` (130px): Systemfont-Liste (mind. Helvetica, Arial, Inter,
Times New Roman, Georgia, Courier New). `applyMark(doc,range,'font',v)`.
- **Grösse** `BarCombo` (80px): Presets in pt `[8,9,10,11,12,14,18,24,36,48]`
+ „Eigene…" → Zahl-Input-Pille. `applyMark(...,'sizePt',n)`.
Zeile 2:
- **B/I/U** Segmentpille (110px, Icons `format_bold`/`format_italic`/
`format_underlined`): `toggleMark(doc,range,'bold'|'italic'|'underline')`.
Aktiv-Zustand aus `isMarkActive(doc,range,mark)`.
- **L/C/R** Segmentpille (130px, Icons `format_align_left`/`_center`/`_right`):
setzt `paragraph.align` im Bereich.
- **„+"-Text-Button** (80px, BarButton/Pille, Icon `add`, Label „Text"): startet
das Text-Werkzeug (neues Textobjekt platzieren). Falls das Text-Annotation-
Werkzeug noch nicht existiert, Button vorerst `disabled` mit Tooltip
(kein stiller No-Op) — aber Verdrahtung vorbereiten.
**Auswahl-Bewusstsein (WICHTIG):** Prop `textTarget` (oder aus App-State): entweder
`null` (nichts Text-artiges selektiert → Controls setzen nur Defaults, Ränder
normal) ODER `{ doc: RichTextDoc, range: TextRange|null, apply: (doc)=>void }`
für den aktuell selektierten Raumstempel/Text. Ist `textTarget != null`, tragen
die Zeile-2-Pillen `border-color:var(--accent)` (Akzent-Glow), und alle Aktionen
wirken auf `textTarget.doc` via `textTarget.apply(newDoc)`. Ohne aktive Range
(nur Objekt selektiert, kein Editor offen) wirkt Formatierung auf das GANZE Doc.
Quelle der `textTarget`-Daten: der Raum-Agent exponiert Stempel-Doc + Setter
(`setRoomStampDoc(roomId, doc)`) im App-State (siehe Peer-Absprache). App leitet
für den selektierten Raum `{doc: room.stampDoc, range: activeStampRange,
apply: d => setRoomStampDoc(room.id, d)}` an die Text-Gruppe.
## 4. Text-Inhalt bearbeiten: kleines Fenster (kein Footer)
Doppelklick auf einen Raumstempel/ein Textobjekt öffnet ein **schwebendes
Dialog-Fenster** (nicht den Footer, kein Panel-Aufklappen). Umsetzung: neue
Komponente `src/ui/TextEditorDialog.tsx` — ein zentriertes/абgesetztes Fenster
(~560×420, `--shadow-3`, `border-radius:8px`, Titel „Text bearbeiten",
Kopf mit Schliessen-✕), Body = der bestehende `src/text/RichTextEditor.tsx`
(er bringt seine eigene Mini-Toolbar mit — das ist hier ok, weil es ein eigenes
Fenster ist), Fuss = „Abbrechen" / „Übernehmen". „Übernehmen" ruft
`setRoomStampDoc(roomId, editedDoc)`.
Der Doppelklick-Handler lebt in App (Plan-View/Viewport → onDoubleClick auf
Stempel-Hit → `openTextEditor(roomId)`), NICHT im Footer. Falls der Raum-Agent
den Footer benutzt hat: diesen Pfad entfernen und durch den Dialog ersetzen.
## 5. Farb-/Stil-Tokens
Bestehende CSS-Variablen weiterverwenden (`--panel`,`--input`,`--border`,
`--accent`,`--accent-dim`,`--accent-border`,`--ink`,`--ink-2`,`--muted`,
`--font-mono`,`--shadow-1..3`). KEINE neuen Farbwerte hart kodieren. Falls ein
Token fehlt (z. B. `--accent-border`), prüfen und ggf. aus bestehenden ableiten.
## 6. i18n
Neue Keys in de.ts UND en.ts: `text.style`, `text.font`, `text.size`,
`text.size.custom`, `text.bold/italic/underline`, `text.align.left/center/right`,
`text.add`, `text.add.hint`, `text.editTitle`, `text.apply`, `text.cancel`,
`text.selectedHint`. Presets-Namen über bestehende `rt.*`/Preset-Keys, sofern da.
## 7. Gate (Pflicht)
`rm -f tsconfig.tsbuildinfo && npx tsc -b` grün, `npm run build` grün, keine
AI-Spuren (grep auf Claude/Anthropic/AI/Generated/Co-Authored), Boot-Probe
(`node scripts/probe.mjs`) ohne Konsolenfehler, Screenshot der Oberleiste.
KEIN Commit.
+43
View File
@@ -0,0 +1,43 @@
# Top-Bar (Oberleiste) & Footer/Status-Leiste — Design
> Referenz: DOSSIER `rhino/toolbar.py` + `src/ToolbarApp.jsx`. Hier auf unseren
> Standalone-Stack (React+TS, eigenes Modell) übersetzt. DOSSIER hat KEINEN Footer
> (delegiert an Rhinos eigene Leiste) — den Footer ergänzen wir neu (Vectorworks-Stil).
## Top-Bar — Gruppen (links → rechts)
1. **Marke/Logo** + Settings-Icons (Projekt-Einstellungen, App-Einstellungen).
2. **Ansicht** — Umschalter: Grundriss · Perspektive · Schnitt · Ansicht.
Später: 3D-Views Top/Iso + Himmelsrichtungen N/O/S/W (mit Nordwinkel-Rotation).
3. **Darstellung** — Render-/Anzeigemodus (Wireframe/Shaded/…) + **Detailgrad**
(grob/mittel/fein, ≙ DOSSIER „Darstellung" Einfach/Standard/Detail).
4. **Massstab & Zoom** — Live-Anzeige „1:N" + Dropdown (1:1,1:5,…,1:1000, frei) +
**Plan-Ansicht-Toggle** (Linienstärken für Druck) + Zoom-Buttons: 100% · Einpassen ·
Auswahl. Quelle: viewBox-Skala / `dpi = 96·devicePixelRatio` (siehe docs/design/plans-output.md).
5. **Overrides** (regelbasiert, Toggle + Preset) · **Masse** (Bemaßungs-Preset) — später.
6. **Anordnen (Z-Order)** — nach vorne/hinten (für 2D-Plangrafik) — später.
7. **Snapping** — Master-Osnap + Modi (End/Mitte/Schnittpunkt/Lot/Zentrum/Nah) +
**Raster** an/aus + **Referenzlinien** (Wandachsen) an/aus — wenn Zeichenwerkzeuge da sind.
8. **Text** — Stil/Font/Größe + B/I/U + Ausrichtung + „+" — mit den 2D-Werkzeugen.
### MVP jetzt (zu vorhandenem Modell)
- Ansichts-Umschalter (haben wir, ausbauen) · Detailgrad-Dropdown · **Massstab 1:N +
Zoom: Einpassen/Auswahl/100%** · Render-Modus · Referenzlinien-Toggle · Ressourcen.
## Footer / Status-Leiste (neu, unten, ~22 px)
`[Werkzeug-Hinweis] … [Cursor X/Y/Z] · [Einheit] · [Massstab 1:N] · [Zoom %] · [Geschoss] · [Ebene] · [Snap] … [Auswahl: n]`
- **Cursor X/Y/Z** — live aus der Plan-/3D-Position (Plan: aus viewBox-Inverse der Maus).
- **Einheit** — m (aus Projekt). **Massstab** 1:N + **Zoom %** — aus der View-Transform.
- **Aktives Geschoss** + **aktive Ebene** — aus Selection/State.
- **Snap-Status** — aktive Fänge (später). **Auswahl: n** — Anzahl selektierter Objekte.
- **Werkzeug-Hinweis** links — kontextueller Text des aktiven Werkzeugs.
### MVP jetzt
- Cursor X/Y (im Grundriss), Einheit, Massstab 1:N, Zoom %, aktives Geschoss + Ebene.
Snap/Werkzeug/Auswahl kommen mit den Zeichenwerkzeugen.
## Anbindung
Beide Leisten docken an das Panel-/Dock-Layout an (Top über den Docks, Footer darunter),
Inhalte rein aus dem Modell + View-State abgeleitet (keine Sonderzustände).
+636
View File
@@ -0,0 +1,636 @@
# Design — Prioritätsbasierte mehrschichtige Wand-Verschneidung (T/X)
> Teil der Standalone-Architektur — siehe [../../ARCHITECTURE.md](../../ARCHITECTURE.md).
> Aufbauend auf der bestehenden **L-Ecken-Gehrung** in `src/model/joins.ts`
> (`computeJoins`, `miterLine`) und `src/model/geometry.ts` (`clippedBand`,
> `lineIntersect`). Plan-Verbraucher: `src/plan/generatePlan.ts` (`addWallPoche`).
> Bezeichner **englisch**, Prosa deutsch, Einheiten **Meter**.
> **Hinweis Referenz:** Die im Auftrag genannte DOSSIER-Datei
> `rhino/wand_grips.py` ist in dieser Umgebung **nicht vorhanden** (das einzige
> auffindbare `dossier` ist ein Typst-Portfolio, kein Rhino-Plugin). Das Design
> stützt sich daher auf (a) den bestehenden Code, (b) die in CONVENTIONS.md/elements.md
> festgehaltenen Geometrie-Konventionen und (c) die etablierte Bau-Semantik der
> prioritätsbasierten Schichtverschneidung (Vectorworks „Komponenten-Verbindung",
> Revit „Layer Priority", ArchiCAD „Composite Priority"). Sobald `wand_grips.py`
> verfügbar ist, sollte Abschnitt 7 (Priorität/Backbone) gegen DOSSIERs konkrete
> Rangregel abgeglichen werden.
---
## 0. Problem & Leitbild
Heute (`computeJoins`) wird **nur die L-Ecke** behandelt: genau zwei Wandenden
treffen sich in einem Knoten, eine gemeinsame **Gehrungslinie** (`miterLine`)
schneidet beide Wände sauber. T-/X-Stöße (≥ 3 Enden) bleiben rechtwinklig
gekappt (`if (ends.length !== 2) continue;`) — die durchgehende Wand wird von der
ankommenden Wand nicht durchdrungen, und die Schichten überlappen sich oder
klaffen.
**Ziel:** An jedem Knoten — L (2 Enden), T (3), X/Kreuz (4+) — soll **jede
Schicht jeder Wand** genau bis zu der Fläche reichen, die ihre Verschneidungs-
Semantik vorgibt:
- Die **gemeinsame, höchstpriorisierte Schicht** (Backbone, z. B. Stahlbeton)
läuft **durch** den Stoß.
- **Niederpriorisierte Schichten** (z. B. Putz, Dämmung) **stoßen** an die nächst-
höhere Schicht des Nachbarn und **enden** dort.
- Putz **läuft auf jeder Seite** bis zur Betonfläche und endet dort; Putz
**überquert nie** den Beton (kanonisches Beispiel, siehe §7.1).
**Architektur-Prinzip (CONVENTIONS.md):** Das semantische Modell ist die einzige
Wahrheit. Die Verschneidung ist eine **reine Ableitung** (`Project + Wall[]` →
Trimm-Linien je Schicht-Band), nichts wird in die Geometrie eingebacken. Sie wird
beim Generieren des Plans angewandt und ist **deterministisch** und **idempotent**.
---
## 1. Geometrie-Konventionen (Wiederholung, verbindlich)
Aus CONVENTIONS.md und `geometry.ts`:
- Achsrichtung `u = normalize(end - start)`.
- Wand-Normale `n = leftNormal(u) = (-u.y, u.x)`.
- Schichten werden **außen → innen** gestapelt: Offset entlang `n` läuft von
`-T/2` (linke/„außen"-Kante) nach `+T/2` (rechte/„innen"-Kante). Eine Schicht `k`
belegt das Intervall `[off_k, off_k + thickness_k]` mit `off_0 = -T/2`.
- Eine **unendliche Gerade** ist `Line { point, dir }`; Schnittpunkt zweier
Geraden via `lineIntersect(a, da, b, db)` (null bei parallel).
- Ein Schicht-**Band** zwischen Offsets `offA, offB` wird über `clippedBand(start,
end, offA, offB, startCut, endCut)` gebildet; jede Bandlängskante wird mit einer
optionalen Schnittlinie verschnitten statt rechtwinklig gekappt.
---
## 2. Datenmodell — was schon da ist, was neu kommt
### 2.1 Vorhanden (`types.ts`)
```ts
interface Component { …; joinPriority: number; } // höher = läuft am Stoß durch
interface Layer { componentId: string; thickness: number; }
interface WallType { id; name; layers: Layer[]; } // layers: außen → innen
interface Wall { start: Vec2; end: Vec2; wallTypeId; height; … }
```
`joinPriority` ist bereits **pro Bauteil (Component)** definiert — genau richtig:
Priorität ist eine **Material-Eigenschaft**, nicht eine Schicht-Eigenschaft. Damit
verschneidet sich Beton mit Beton unabhängig vom Wandtyp.
### 2.2 Neue, abgeleitete Strukturen (keine neuen persistenten Felder)
Die heutige `WallCuts`-Struktur (eine Schnittlinie je **Wandende**) reicht für L
(Gehrung) — aber **nicht** für T/X, weil dort verschiedene Schichten **verschiedene**
Trimm-Linien brauchen (Beton läuft durch, Putz stoppt früher). Wir erweitern auf
**pro Schicht, pro Ende** eine eigene Trimm-Linie:
```ts
// src/model/joins.ts (erweitert)
/** Eine gerichtete Halbkante eines Wandendes an einem Knoten. */
interface WallEnd {
wallId: string;
end: "start" | "end";
}
/** Trimm-Linie + Klassifikation für GENAU EIN Schicht-Band an EINEM Ende. */
interface LayerCut {
/** Schnittgerade in Weltkoordinaten; null = rechtwinklig kappen (Default). */
line: Line | null;
/**
* "miter" – Gehrung (L): geteilte Diagonale mit dem Nachbarn.
* "butt" – Band stößt stumpf an eine Nachbarfläche (niedrigere Priorität).
* "through" – Band läuft durch den Knoten (höchste Priorität / Backbone).
* "square" – freies Ende, rechtwinklig (line === null).
*/
kind: "miter" | "butt" | "through" | "square";
}
/** Pro Wandende: eine Trimm-Linie je Schicht-Index (parallel zu WallType.layers). */
interface EndCuts {
/** layerCuts[k] gilt für layers[k]; Länge === wt.layers.length. */
layerCuts: LayerCut[];
}
/** Ersetzt die alte WallCuts: jetzt schichtweise an beiden Enden. */
interface WallJoin {
start: EndCuts;
end: EndCuts;
}
export type JoinMap = Map<string /*wallId*/, WallJoin>;
```
**Abwärtskompatibilität:** Die alte L-Gehrung ist der Spezialfall „alle
`layerCuts[k].line` an einem Ende sind dieselbe `miter`-Linie". `clippedBand`
bleibt unverändert; der Plan-Generator ruft es jetzt **je Schicht mit der
schichtspezifischen Linie** auf (siehe §8).
### 2.3 Knoten-Repräsentation
```ts
/** Ein Verschneidungsknoten: alle Wandenden, die sich (gerundet) berühren. */
interface Junction {
key: string; // roundKey(p)
p: Vec2; // Knotenposition (Mittel der Enden)
arms: Arm[]; // sortiert nach Außenwinkel (CCW)
}
/** Ein „Arm" = ein Wandende, gesehen als Strahl, der vom Knoten WEGzeigt. */
interface Arm {
we: WallEnd;
wall: Wall;
/** Richtung VOM Knoten weg in die Wand hinein (immer normiert). */
dirOut: Vec2; // = end==="start" ? +u : -u
/** Außenwinkel atan2(dirOut.y, dirOut.x) für Sortierung. */
angle: number;
total: number; // Gesamtdicke der Wand
/** Schicht-Profil, vom Knoten aus gesehen (siehe §3.2). */
layers: ArmLayer[];
}
/** Eine Schicht eines Arms, mit ihren beiden Längs-Flächengeraden am Knoten. */
interface ArmLayer {
index: number; // Index in wt.layers
componentId: string;
priority: number; // Component.joinPriority
/** Offsets entlang n der Wand: [innerEdge..outerEdge] des Bandes. */
off0: number; off1: number;
/** Die zwei Längsflächen als Geraden (point am Knoten, dir = u der Wand). */
faceA: Line; faceB: Line;
}
```
---
## 3. Knotenerkennung (Junction Detection)
### 3.1 Knoten clustern
Identisch zur heutigen Logik, nur ohne die `length !== 2`-Abbruchbedingung:
```
function buildJunctions(walls): Junction[]
map = Map<key, WallEnd[]>
for w in walls:
map.push(roundKey(w.start), {wallId:w.id, end:"start"})
map.push(roundKey(w.end), {wallId:w.id, end:"end"})
out = []
for (key, ends) in map:
if ends.length < 2: continue // freies Ende → alle Schichten "square"
arms = ends.map(buildArm)
arms.sort(by angle) // CCW um den Knoten
out.push({ key, p: avgEndPoint(ends), arms })
return out
```
`roundKey` (vorhanden) gruppiert Endpunkte auf ein 0.1-mm-Gitter. **Wichtig für
T-Stöße:** Bei einem echten T endet die ankommende Wand **auf der Achse** der
durchgehenden Wand, nicht an deren Endpunkt. Solche Knoten werden über die
Endpunkt-Gruppierung **nicht** gefunden, wenn die durchgehende Wand dort kein Ende
hat. Daher zusätzlich (siehe §3.3) eine **Achs-Auf-Achs-Inzidenz**.
### 3.2 Arm-Schichtprofil (kanonische Orientierung)
Damit Schichten zweier Arme vergleichbar sind, muss jeder Arm seine Schichten in
**konsistenter Welt-Orientierung** kennen. Wir speichern je Schicht ihre beiden
**Längsflächen** als Geraden mit Stützpunkt am Knoten und Richtung `u`:
```
function buildArm(we): Arm
wall = byId(we.wallId); u = dirOf(wall); n = leftNormal(u)
dirOut = we.end==="start" ? u : negate(u) // vom Knoten in die Wand
j = we.end==="start" ? wall.start : wall.end
total = wallTypeThickness(wt)
off = -total/2
layers = []
for (k, layer) in wt.layers:
off0 = off; off1 = off + layer.thickness
faceA = { point: j + n*off0, dir: u }
faceB = { point: j + n*off1, dir: u }
layers.push({ index:k, componentId, priority, off0, off1, faceA, faceB })
off = off1
return { we, wall, dirOut, angle: atan2(dirOut), total, layers }
```
### 3.3 T-Stoß-Erkennung (Achs-auf-Achs)
Zusätzlich zu Endpunkt-Clustern: ein Wandende `e` (Punkt `P`) bildet einen
**T-Stoß** mit Wand `B`, wenn `P` (innerhalb Toleranz) auf der **Strecke** `B.start
→ B.end` liegt, aber **nicht** auf deren Endpunkten:
```
function findTeeIncidences(walls):
for endpoint P of each wall A (as WallEnd e):
for each wall B != A:
if pointOnSegment(P, B.start, B.end, tol) and not nearEndpoint(P, B):
// virtueller Knoten: A endet, B läuft durch.
registerTee(P, armOf(e), passThroughWall=B)
```
`B` wird hier als **durchgehender Strang** behandelt (kein Ende am Knoten); im
Junction-Modell taucht `B` als zwei kollineare „Arme" auf (Richtung `+u` und
`−u`), die der Prioritätsalgorithmus (§5) automatisch als „durchlaufend" erkennt
(zwei kollineare Arme gleicher Wand → ihre Schichten enden nie gegeneinander).
**MVP-Vereinfachung (Phase 1, §10):** T-Stöße zunächst NUR über koinzidente
Endpunkte (B hat dort tatsächlich einen Eckpunkt, z. B. weil die Wand dort geteilt
wurde). Echte Achs-auf-Achs-T-Stöße (B durchgehend) folgen in Phase 3.
---
## 4. Winkel-Sektoren & Nachbar-Flächen
Für die Prioritätsauflösung muss man wissen, **welche Fläche eines Arms welcher
Fläche des Nachbarn gegenübersteht**. Die Arme sind CCW nach `angle` sortiert.
Zwischen zwei aufeinanderfolgenden Armen `arms[i]` und `arms[i+1]` (zyklisch) liegt
ein **Sektor** (Keil). Jeder Sektor wird von **einer Längsfläche jedes der beiden
Arme** begrenzt:
```
arm[i+1]
\ Sektor S_i
\ /
faceR(i+1)\ ___ faceL(i)
X (Knoten)
/
/
arm[i]
```
Konvention: pro Arm hat die **äußere** Schicht (Index 0, Offset `-T/2`) die Fläche,
die in den **CCW-vorausgehenden** Sektor zeigt; die **innere** Schicht den
**nachfolgenden**. Konkret bestimmen wir die „dem Sektor zugewandte" Fläche jeder
Schicht über das Vorzeichen von `cross(dirOut, sektorrichtung)` — analog zur
`lbCloser`-Heuristik in `miterLine`, aber pro Sektor statt global.
```
function sectorFaces(armLeft, armRight):
// armRight ist CCW vor armLeft (Sektor liegt zwischen ihnen).
// Wähle für jeden Arm die Schicht-Flächen, die in den Sektor zeigen.
faceOf(arm, towards): pick faceA or faceB of each layer by sign of
cross(arm.dirOut, towards - knoten)
```
Diese Sektor-Sicht verallgemeinert die bestehende `miterLine`: bei genau zwei
Armen (L) gibt es zwei Sektoren (innen/außen), und die Mittel-Gehrungslinie ergibt
sich wie bisher aus dem Schnitt der gegenüberliegenden Außen- bzw. Innenflächen.
---
## 5. Prioritätsauflösung — das Kernstück
### 5.1 Idee
An einem Knoten konkurrieren Schichten verschiedener Arme um denselben Raum. Regel:
> Eine Schicht **läuft durch** (`through`), wenn sie zur **höchsten am Knoten
> präsenten Priorität** gehört **und** auf der „gegenüberliegenden" Seite eine
> Schicht **gleichen Materials** (oder ≥ gleicher Priorität) existiert, an die sie
> nahtlos anschließt. Andernfalls **stößt** sie (`butt`) an die nächsthöher-
> priorisierte Nachbarfläche und endet dort.
Praktisch lösen wir das **fläche-gegen-fläche** je Schicht: Für jede Schicht `L`
eines Arms suchen wir die **Trimm-Fläche**, die ihr Band beendet. Das ist die
**erste** (vom Knoten aus, entlang `dirOut`) der gegenüberliegenden Flächen mit
**echt höherer Priorität**; existiert keine, läuft die Schicht bis zur
Knoten-Mittelachse durch.
### 5.2 Pro Schicht: Trimm-Fläche finden
```
function resolveLayerCut(arm, layer, junction): LayerCut
// Kandidaten: alle Schichten ALLER ANDEREN Arme, deren Band den
// Halbraum dieses Layers überlappt (Offset-Überlapp im gemeinsamen
// Sektor) UND deren Priorität die Trimm-Entscheidung bestimmt.
candidates = []
for other in junction.arms where other.wall.id-end != arm.id-end:
for ol in other.layers:
if overlapsInSector(layer, ol, arm, other):
candidates.push({ other, ol, face: facingFace(ol, towards arm) })
// Höchste am Knoten präsente Priorität (global, für "through").
maxPrio = max over all candidate.ol.priority and layer.priority
if layer.priority == maxPrio and hasCollinearSamePrio(arm, layer, junction):
// Backbone: läuft durch bis zur Mittel-/Gehrungslinie.
return { line: miterMidline(arm, layer, junction), kind: "through" }
// Sonst: stoße an die NÄCHSTE Fläche höherer Priorität entlang dirOut.
blockers = candidates.filter(c => c.ol.priority > layer.priority)
if blockers.empty:
// niemand höher → Gehrung mit dem Nachbarn gleicher Stufe (L-Fall)
return { line: miterMidline(arm, layer, junction), kind: "miter" }
nearest = argmin over blockers of distanceAlong(dirOut, face)
return { line: nearest.face, kind: "butt" }
```
Hilfsbegriffe:
- `overlapsInSector(layer, ol, …)` — projiziert beide Bänder auf die **Normale des
Sektors** und prüft Intervall-Überlapp `[off0,off1] ∩ [off0',off1'] ≠ ∅`. Nur
überlappende Bänder können sich gegenseitig trimmen.
- `facingFace(ol, towards arm)` — die der `arm`-Seite zugewandte Längsfläche von
`ol` (die `faceA`/`faceB` von §3.2), als `Line`.
- `distanceAlong(dirOut, face)` — Abstand des Schnittpunkts `faceLine ∩ layerAxis`
vom Knoten, gemessen entlang `dirOut`. Negative bzw. hinter dem Knoten liegende
Treffer werden verworfen (eine Trimm-Fläche muss **vor** dem Band liegen).
- `miterMidline(...)` — die gemeinsame Diagonale für gleichrangige Begegnung; für
zwei Arme exakt die heutige `miterLine`.
- `hasCollinearSamePrio(...)` — true, wenn auf der „anderen Seite" des Knotens eine
Schicht **gleichen Materials/Priorität** existiert, in die das Band nahtlos
übergeht (Backbone-Kontinuität, §7).
### 5.3 Resultat in `LayerCut.line` schreiben
`resolveLayerCut` liefert für `layers[k]` eine `Line | null`. Diese wird in
`WallJoin[end].layerCuts[k]` abgelegt. `clippedBand` kappt damit **dieses eine
Band** an genau dieser Linie — alle anderen Schichten derselben Wand können andere
Linien (oder `null`) haben. Das ist die ganze Verkabelung zum Renderer.
---
## 6. Master-Pseudocode `computeJoins` (neu)
```
function computeJoins(project, walls): JoinMap
result = init each wall → { start:{layerCuts:[square…]}, end:{layerCuts:[square…]} }
junctions = buildJunctions(walls) // §3.1
// (Phase 3: junctions += findTeeIncidences(walls)) // §3.3
for J in junctions:
if J.arms.length == 1: continue // freies Ende: alles "square"
if J.arms.length == 2:
resolveL(J, project, result) // bestehende Gehrung, schichtweise
else:
resolveTorX(J, project, result) // §5, pro Arm pro Schicht
return result
function resolveTorX(J, project, result):
for arm in J.arms:
cuts = []
for layer in arm.layers:
cuts[layer.index] = resolveLayerCut(arm, layer, J) // §5.2
writeEnd(result, arm.we, cuts) // start oder end
function resolveL(J, project, result):
[a, b] = J.arms
// Wie heute: eine gemeinsame Gehrungslinie pro Sektor; aber wenn die
// Dicken/Schichten ungleich sind, kann pro Schicht über §5.2 verfeinert werden.
// MVP: eine miterLine für alle Schichten beider Arme (= heutiges Verhalten,
// schichtweise dupliziert).
line = miterLine(project, a.wall, a.we.end, b.wall)
for arm in [a,b]:
cuts = arm.layers.map(_ => ({ line, kind:"miter" }))
writeEnd(result, arm.we, cuts)
```
So bleibt die **L-Ecke bit-genau wie heute** (Regressionssicherheit), und die
neue Logik greift nur bei ≥ 3 Armen.
---
## 7. Prioritäts-Semantik im Detail (das Putz/Beton-Beispiel)
### 7.1 Kanonisches Beispiel
Wandtyp „Aussenwand" (außen → innen):
| k | Component | thickness | joinPriority |
|---|------------|-----------|--------------|
| 0 | Putz aussen| 0.02 | 1 |
| 1 | Stahlbeton | 0.18 | 9 |
| 2 | Putz innen | 0.015 | 1 |
Drei solche Wände treffen in einem T-Knoten (zwei kollinear durchgehend „BB",
eine ankommend „A"):
1. **Beton (prio 9)** der durchgehenden Wände `BB` läuft durch — beide
Beton-Bänder sind kollinear gleicher Priorität → `hasCollinearSamePrio` =
true → `through`. Der Beton von `A` (prio 9) **stößt** an die **Betonfläche**
von `BB` (gegenüberliegende Fläche, gleiche höchste Priorität, aber kein
kollinearer Partner für `A`) → `butt` an Betonaußenfläche von `BB`.
2. **Putz (prio 1)** jeder Wand stößt an die **erste höherpriorisierte Fläche**
entlang `dirOut`. Für die Putzschichten von `A` ist das die **Betonfläche von
`BB`**: Putz **läuft auf jeder Seite bis zum Beton** und endet dort (`butt`).
3. Putz **überquert nie** den Beton, weil eine `butt`-Trimm-Linie immer die
**nächste** höhere Fläche ist — sie liegt vor dem Beton-Durchlauf.
Ergebnis exakt wie gefordert: *Beton durch, Putz schließt beidseitig an, Putz
kreuzt Beton nicht.*
### 7.2 Warum Priorität pro Component (nicht pro Layer)
Beton trifft Beton → gleiche Priorität → nahtlos. Würde Priorität pro Schicht
vergeben, müsste man sie pro Wandtyp neu pflegen. `joinPriority` am Component löst
das materialweise — Stahlbeton hat **immer** 9, egal in welchem Wandtyp.
### 7.3 Backbone-Erkennung für „through"
`hasCollinearSamePrio(arm, layer, junction)`:
```
for other in junction.arms where other != arm:
if collinear(arm.dirOut, other.dirOut) (antiparallel, gleiche Achse) and
other has a layer ol with ol.priority == layer.priority and
bandsAlign(layer, ol): // gleiche Offsets relativ zur gemeinsamen Achse
return true
return false
```
Nur wenn das Band auf der **gegenüberliegenden** Achse einen passenden Partner
gleicher Priorität und Lage hat, läuft es wirklich **durch**. Sonst (z. B. der
ankommende Beton von `A`) **stößt** es an — exakt das gewünschte Verhalten.
---
## 8. Layer-Wrapping (Umschlagen niederpriorisierter Schichten)
Ein subtiler, aber wichtiger Fall: An einem T-Stoß endet die **innere** Putzschicht
der ankommenden Wand `A` an der Betonfläche von `BB`. Damit der Putz **um die Ecke
herum sichtbar bleibt** (auf der Innenseite der durchgehenden Wand zieht der innere
Putz von `BB` durch), ist nichts Zusätzliches nötig — `BB`s innerer Putz läuft als
eigenes durchgehendes Band weiter.
Wo Wrapping aktiv nötig ist: wenn eine **niederpriorisierte Außenschicht** an einer
Ecke **um eine höhere Schicht herumgeführt** werden soll (z. B. Dämmung, die außen
um die Stütze läuft). Modellierung:
- Standard (MVP): kein Wrapping — jede Schicht endet an ihrer Trimm-Fläche
(`butt`). Das deckt T/X korrekt ab.
- Optional (Phase 4): Ein `wrap`-Flag pro Component (`Component.wrapAtEnds?:
boolean`). Ist es gesetzt, erzeugt der Generator an der Trimm-Fläche ein
**zusätzliches kurzes Stirnband** quer (Offset-Intervall der Schicht, Länge =
Tiefe bis zur nächsten Fläche), sodass die Schicht ihre eigene Stirnseite
„umschließt". Geometrisch ein weiteres `clippedBand` mit getauschten Achsen.
Wrapping ist bewusst **nachgelagert**; es ändert die Trimm-Logik (§5) nicht,
sondern fügt nur Zusatzpolygone hinzu.
---
## 9. Anbindung an den Plan-Generator (`generatePlan.addWallPoche`)
Heute ruft `addWallPoche` `clippedBand(p1, p2, off, off+thickness, startCut,
endCut)` mit **einer** `WallCuts` je Ende. Neu:
```ts
// joins: JoinMap (neu)
const join = joins.get(wall.id) ?? emptyJoin(wt.layers.length);
…
let off = -total / 2;
wt.layers.forEach((layer, k) => {
const startCut = (s <= 1e-6) ? join.start.layerCuts[k].line : null;
const endCut = (e >= axisLen - 1e-6) ? join.end.layerCuts[k].line : null;
out.push({ kind:"polygon",
pts: clippedBand(p1, p2, off, off + layer.thickness, startCut, endCut),
fill: comp.color, hatch: resolveHatch(...), … });
off += layer.thickness;
});
```
- Die **Umrisslinie** über die volle Dicke (`-T/2..+T/2`) nutzt für ihre beiden
Kanten die Cuts der **äußersten** bzw. **innersten** Schicht — oder wird, falls
Schichten unterschiedlich getrimmt sind, durch eine **abgeleitete Outline**
(Vereinigung der Schicht-Polygone, §11) ersetzt. MVP: weiterhin volle-Dicke-Band
mit den Cuts von Schicht 0 / letzter Schicht.
- `grob`-Detailgrad nutzt nur die **Backbone-Schicht** → ihr `through`/`miter`-Cut
für die ganze Sammelfläche (entspricht der bestehenden `backboneColor`-Logik).
**Keine Signaturänderung an `clippedBand`** — es bleibt der zentrale Trimmer; wir
füttern es nur schichtweise mit verschiedenen Linien.
---
## 10. 2D-analytisch jetzt vs. exakte 3D-Booleans später (OCCT)
### 10.1 Jetzt — 2D-analytisch (dieses Design)
- **Plan/Grundriss** ist eine reine 2D-Ableitung (vgl. `generatePlan.ts`-Kopf:
„nicht durch Zerschneiden eines 3D-Meshes, sondern direkt aus den Parametern").
- Trimmen = **Geraden-Schnitt** (`lineIntersect`) + Polygon-Kappen (`clippedBand`).
Kein Polygon-Boolean nötig, solange Schichten als **konvexe Bänder** mit
schrägen Stirnflächen modelliert werden. Das ist exakt, schnell (O(Arme² ·
Schichten) je Knoten), und deterministisch.
- **3D-Wand** im Viewport: Extrusion der getrimmten 2D-Schichtpolygone in `z`
(Höhe). Damit stimmen Plan und 3D ohne separaten Kernel überein.
Grenzen der 2D-Analytik: nicht-konvexe Stirnprofile, echte Materialdurchdringung in
`z` (z. B. wenn Wände unterschiedlicher Höhe / Sturz / Brüstung interagieren),
Verschneidung Wand × Decke × Stütze. Das braucht echte Volumen-Booleans.
### 10.2 Später — exakte 3D-Booleans (OCCT / opencascade.js)
- **Bibliothek:** `opencascade.js` (WASM-Port von OCCT) — `BRepAlgoAPI_Cut/Common`,
`BRepPrimAPI_MakePrism` für Extrusionen. Alternativ `manifold-3d` (schneller,
robuster für reine Mesh-Booleans, aber ohne B-Rep/Fillets).
- **Modell bleibt gleich:** Priorität (`joinPriority`) bestimmt die **Cut-
Reihenfolge**. Algorithmus: baue je Schicht einen über-langen Solid (Band ×
Höhe, über den Knoten hinaus verlängert); subtrahiere von jeder niederpriori-
sierten Schicht die Solids **aller höherpriorisierten** Schichten am Knoten
(`result = layerSolid − ⋃ higherPrioritySolids`). Gleiche Priorität: kein Cut
(Beton bleibt an Beton). Das ist die **3D-Verallgemeinerung exakt derselben
Prioritätsregel** — die 2D-`butt`/`through`-Klassifikation aus §5 ist die
2D-Projektion dieser Boolean-Reihenfolge.
- Die Datenstrukturen (§2) bleiben unverändert; nur der **Trimmer** wird
ausgetauscht (`clippedBand` → OCCT-Cut). Deshalb ist das 2D-Design bereits
„OCCT-ready".
---
## 11. Robuste Outline (optional, Phase 4)
Wenn Schichten an einem Knoten unterschiedlich getrimmt sind, ist die „volle
Dicke"-Umrisslinie nicht mehr ein einfaches Band. Saubere Lösung: die
**Wand-Outline** als **Vereinigung aller Schicht-Polygone** berechnen
(Polygon-Boolean in 2D). Empfohlene Library: **`polygon-clipping`** (Martinez-
Rueda, robust, klein) oder **`@flatten-js/boolean-op`**. Damit wird der Umriss
exakt die Außenkontur der getrimmten Bänder — auch bei X-Knoten. MVP verzichtet
darauf und nutzt die Schicht-0/N-Cuts (visuell für die meisten Fälle ausreichend).
---
## 12. Edge Cases
1. **Gleiche Priorität, nicht kollinear** (z. B. zwei Betonwände treffen im rechten
Winkel im T): kein „through" (kein kollinearer Partner), aber auch kein
`butt`-Blocker höherer Priorität → Fallback **`miter`** (gemeinsame Gehrung wie
im L-Fall). Bei drei gleichrangigen Armen: paarweise Gehrung pro Sektor; die
resultierenden Stirnflächen sind die Sektor-Halbierenden.
2. **Kollinear (180°)** — zwei Wände in einer Linie: `lineIntersect` liefert null
(parallel). Behandlung wie heute: **kein Schnitt**, Bänder laufen gerade durch
(bei gleichem Typ nahtlos). Bei ungleichem Typ: die schmalere Wand stößt an die
breitere; pro Schicht über §5 auflösbar.
3. **> 2 Schichten / asymmetrische Wandtypen**: Der Algorithmus ist in der Schicht-
anzahl generisch (`resolveLayerCut` läuft je Schicht-Index). Treffen Wände
verschiedener Typen (verschiedene Schichtzahl/-dicke), entscheidet **nur die
Priorität pro Fläche**, nicht der Index — deshalb arbeiten wir mit
`ArmLayer.faceA/faceB` (Welt-Flächen), nicht mit Schicht-Indizes über Wände
hinweg.
4. **Backbone fehlt** (alle Prioritäten gleich): degeneriert sauber zu reinen
Gehrungen (Fall 1).
5. **Sehr spitze Winkel**: Trimm-Schnittpunkte können weit vom Knoten wegwandern.
Begrenzung: `distanceAlong` auf `≤ maxReach` (z. B. `3 · total`) clampen; sonst
rechtwinklig kappen (`square`), um Artefakte zu vermeiden.
6. **Öffnungen am Wandende** (`addWallPoche`-Segmentierung): Cuts gelten nur für das
echte Wandende (`s≤ε` / `e≥len−ε`), wie heute. Türnahe Segmentenden bleiben
`square`.
7. **Numerische Knoten-Toleranz**: `roundKey`-Gitter (0.1 mm) und ε in
`pointOnSegment` müssen konsistent sein, sonst „flackernde" Knoten. Toleranz
zentral als Konstante (`JOIN_EPS = 1e-4` m).
8. **Mehr als 2 Wände gleicher Achse** (degenerierter X, alle kollinear): als ein
durchgehender Strang behandeln; Prioritätsregel pro Schicht greift normal.
---
## 13. Staged Build-Plan
**Phase 1 — Single-Layer T (Fundament).**
- `JoinMap`/`WallJoin`/`EndCuts`/`LayerCut` einführen; `WallCuts` darin als
Spezialfall abbilden. `clippedBand` unverändert.
- `buildJunctions` ohne `length!==2`-Abbruch; L-Fall ruft bestehende `miterLine`
(schichtweise dupliziert) → **Regressionsgleichheit** zu heute.
- T-Knoten **nur über koinzidente Endpunkte** (§3.3 MVP). Für **einschichtige**
Wände: die durchgehende Wand läuft durch, die ankommende stößt rechtwinklig an
deren nächste Fläche. Verifizieren: `npx tsc -b`, `npm run build`,
`node scripts/probe.mjs`, Screenshot prüfen.
**Phase 2 — Prioritäts-Auflösung mehrschichtig (T).**
- `ArmLayer`-Profil (§3.2), Sektor-Flächen (§4), `resolveLayerCut` (§5) inkl.
`through`/`butt`/`miter`-Klassifikation und `hasCollinearSamePrio`.
- `addWallPoche` schichtweise verkabeln (§9). Putz/Beton-Testszene (§7.1) anlegen
und visuell prüfen.
**Phase 3 — X-Knoten & echte Achs-T-Stöße.**
- `findTeeIncidences` (Achs-auf-Achs, §3.3): durchgehende Wand wird nicht geteilt.
- 4+-Arm-Sektorlogik vollständig; Edge Cases 1/5/8 absichern.
**Phase 4 — Politur.**
- Robuste Outline via Polygon-Boolean (§11); optionales Layer-Wrapping (§8,
`Component.wrapAtEnds`).
**Phase 5 — Exakte 3D-Booleans (OCCT).**
- `opencascade.js` integrieren; Trimmer-Interface so abstrahieren, dass 2D
(`clippedBand`) und 3D (OCCT-Cut) dieselbe Prioritäts-Reihenfolge nutzen (§10.2).
Plan bleibt 2D-analytisch; nur das 3D-Volumen nutzt Booleans.
---
## 14. Trimmer-Abstraktion (für §10.2-Migration)
Damit Phase 5 nicht den Aufrufer ändert, kapseln wir das Trimmen hinter einer
Schnittstelle. 2D nutzt `clippedBand`; 3D nutzt OCCT — beide konsumieren dieselbe
`JoinMap`.
```ts
interface LayerTrimmer {
/** 2D: getrimmtes Bandpolygon. 3D-Variante liefert stattdessen einen Solid. */
trimBand(p1: Vec2, p2: Vec2, offA: number, offB: number,
startCut: Line | null, endCut: Line | null): Vec2[];
}
```
Die Prioritätslogik (`computeJoins`) bleibt der **gemeinsame, kernel-unabhängige**
Kopf; nur der `LayerTrimmer` wird ausgetauscht. Das hält das Design dem
Architektur-Prinzip treu: ein semantisches Modell, viele abgeleitete Ansichten.
+581
View File
@@ -0,0 +1,581 @@
# WebGL2 GPU-Accelerated 2D Plan Renderer — Architecture
## Executive Summary
A WebGL2 canvas renderer for PlanView's heavy geometry (polygons, lines, hatches) with CPU fallback. Geometry tessellates once per plan and caches; pan/zoom only updates a transform-matrix uniform. Screen-space stroke width via vertex shader normal expansion. Thin SVG overlay handles text, grips, snap markers, tool preview.
**No npm dependencies** — raw WebGL2 + TypeScript.
---
## Current State (SVG Bottleneck)
**PlanView.tsx** renders `Primitive[]` (polygon/line/arc/text) → SVG DOM:
- ~2500 LOC: pan/zoom via viewBox, toScreen() scaling (1 meter = 90 viewBox units)
- `Primitive` types (generatePlan.ts:133):
- **polygon**: `pts: Vec2[]`, fill/stroke/strokeWidthMm, hatch (solid/insulation/diagonal/crosshatch)
- **line**: a/b endpoints, className, weightMm, dash[], optional color
- **arc**: center, from/to points, r, className, weightMm, dash[]
- **text**: anchor, RichTextDoc, roomStamp metadata
- Bottleneck: **pan/zoom re-renders entire SVG DOM** → Cairo rasterizes geometry at 144 Hz
**Key constants:**
- `PX_PER_M = 90` (viewBox units per meter; Modell-Y up → SVG-Y down via negation)
- `PAD = 60` (margin in viewBox units)
- `mmToPx(mm) = (mm / 25.4) * dpi()` (stroke width: constant screen-px via non-scaling-stroke)
- `ZOOM_MAX/MIN = 50/0.2` (pan/zoom bounds)
---
## Architecture: PlanRenderer (WebGL2 + Fallback SVG)
### Module Structure
```
src/plan/
├── PlanRenderer.ts (Main GPU/CPU dispatcher)
├── glPlan/
│ ├── glPlanCompile.ts (Tessellation & buffer upload)
│ ├── glPlanShaders.ts (Vertex/fragment sources + compilation)
│ ├── glPlanRender.ts (Draw loop: matrix uniform, state mgmt)
│ └── glPlanTypes.ts (TypeScript interfaces for GPU data)
└── PlanView.tsx (React wrapper, unchanged API)
```
### High-Level Flow
```
PlanView.tsx
↓ [receives plan: Plan]
↓
PlanRenderer (new abstraction)
↓
├─→ GPU path [if WebGL2 available && flag=true]
│ ├─ glPlanCompile() → upload tessellated geometry to VRAM
│ ├─ glPlanRender() → draw with pan/zoom matrix uniform
│ └─ [fast pan/zoom via matrix only]
│
└─→ Fallback: SVG [if WebGL fails || flag=false]
└─ existing PlanView render path (toScreen + DOM)
```
---
## GPU Path: Tessellation & Shaders
### 1. Tessellation Strategy
#### **Polygon** → Fans + Ear Clipping
- **Input**: Primitive.polygon = { pts: Vec2[], fill, stroke, strokeWidthMm, hatch }
- **Output**: Indexed triangle mesh
- **Algorithm**: Earcut2D (existing JS library logic, inlined to avoid npm)
- Convert polygon pts to 2D float32 array in **world space** (meters)
- Earcut → triangle indices
- Store: `{ vertices: Float32Array, indices: Uint32Array, color: vec4, hasHatch: bool }`
- **Hatch rendering**: Bake hatch as texture or re-implement in fragment shader (MVP: solid fill only; hatch deferred)
#### **Line** → Quad Expansion (Screen-Space Width)
- **Input**: Primitive.line = { a, b, weightMm, cls, dash?, color }
- **Output**: Degenerate quad (2 triangles) with screen-space normal offset
- **Strategy**:
1. Vertex shader receives `{ pos: vec2, side: float }` (side = ±1 for left/right edge)
2. Transform pos to clip space via matrix uniform
3. Compute screen-space perpendicular via `dFdx/dFdy` or pre-compute normal in CPU
4. Expand by `(weightMm / 25.4) * dpi * (screenPixelsPerClipUnit)` in clip space
5. Fragment shader: solid color (no dash MVP; dashing deferred or CPU pre-tessellation)
#### **Arc** → Line Segments (Polyline → Quads)
- **Input**: Primitive.arc = { center, from, to, r, weightMm, cls, dash }
- **Output**: Tessellate arc to ~30 line segments (adaptive based on radius/zoom), expand each as quad
- Fallback: SVG arc for MVP
#### **Text, Grips, Snap-Markers, Tool-Preview**
- **Stays in SVG overlay** (thin, non-bottleneck)
- Render above WebGL canvas at z-order 1
---
### 2. Shader Sources (GLSL 3.00 ES)
#### **Vertex Shader: Solid Fill (polygon)**
```glsl
#version 300 es
precision highp float;
uniform mat4 viewProjection; // pan/zoom as 2×3 affine (expand to mat4)
layout(location=0) in vec2 position; // world-space (meters)
layout(location=1) in vec4 color; // fill color
out VS_OUT {
flat vec4 vertexColor;
} vs_out;
void main() {
vec4 clipPos = viewProjection * vec4(position, 0.0, 1.0);
gl_Position = clipPos;
vs_out.vertexColor = color;
}
```
#### **Vertex Shader: Screen-Space Stroked Line**
```glsl
#version 300 es
precision highp float;
uniform mat4 viewProjection; // world → clip space
uniform vec2 screenSize; // canvas (width, height) in pixels
uniform float strokeWidthMm; // millimeters
uniform float dpi; // 96 * devicePixelRatio
layout(location=0) in vec2 position; // world-space endpoint
layout(location=1) in float sideFlag; // ±1.0 (left/right edge)
layout(location=2) in vec4 lineColor; // stroke color
out VS_OUT {
flat vec4 vertexColor;
} vs_out;
void main() {
vec4 clipPos = viewProjection * vec4(position, 0.0, 1.0);
// Convert stroke width (mm) → screen pixels
float strokePx = (strokeWidthMm / 25.4) * dpi;
// Convert screen pixels → normalized device coords (NDC)
// NDC ∈ [-1,1]²; screen (0,screenSize) → NDC [-1,1]
float strokeNdc = (strokePx / screenSize.x) * 2.0;
// Expand in clip space (simple; assumes aspect ≈ 1)
vec4 expanded = clipPos + vec4(sideFlag * strokeNdc, 0.0, 0.0, 0.0);
gl_Position = expanded;
vs_out.vertexColor = lineColor;
}
```
#### **Fragment Shader (both)**
```glsl
#version 300 es
precision highp float;
in VS_OUT {
flat vec4 vertexColor;
} fs_in;
out vec4 fragColor;
void main() {
fragColor = fs_in.vertexColor;
}
```
---
### 3. GPU Data Structures (TypeScript)
**glPlanTypes.ts:**
```typescript
export interface GLGeometryBatch {
/** Vertex buffer: interleaved (x, y, [z if 3D], ...) in world space. */
vertexBuffer: WebGLBuffer;
vertexCount: number;
/** Index buffer (triangles for fill, degenerate quads for strokes). */
indexBuffer: WebGLBuffer;
indexCount: number;
/** Vertex Array Object (VAO) binds VBO + IBO. */
vao: WebGLVertexArrayObject;
/** Per-batch metadata. */
batches: Array<{
kind: "polygon" | "line" | "arc";
indexStart: number;
indexCount: number;
color: [r: number, g: number, b: number, a: number]; // RGBA [0,1]
hasHatch: boolean;
hatchPattern?: "solid" | "insulation" | "diagonal" | "crosshatch";
strokeWidthMm?: number;
}>;
}
export interface GLPlanRenderState {
// Pan/zoom transform: world (meters) → clip space
viewMatrix: Matrix3 | Matrix4; // 2×3 affine
projMatrix: Matrix4; // orthographic
// Viewport size & DPI for screen-space stroke width
screenWidth: number;
screenHeight: number;
dpi: number;
// Compiled shaders
solidFillProgram: WebGLProgram;
strokeProgram: WebGLProgram;
// Geometry cache (tessellated once per plan)
geometryBatch: GLGeometryBatch | null;
}
```
---
## MVP API: PlanRenderer Class
### Interface
```typescript
export class PlanRenderer {
/**
* Create renderer with WebGL2 context + fallback config.
*/
constructor(
canvas: HTMLCanvasElement,
options?: {
enableGpu?: boolean; // default: true
enableGpuFallback?: boolean; // SVG fallback if GL fails
}
);
/**
* Compile and cache geometry from primitives.
* Call once per plan change.
*/
compilePlan(plan: Plan): Promise<void>;
/**
* Set pan/zoom transform matrix.
* Call on every view change (pan, zoom, fit).
*/
setViewMatrix(viewBox: { x, y, w, h }, canvasSize: { w, h }): void;
/**
* Render one frame: clear, draw batches, composite.
* Called from requestAnimationFrame loop.
*/
render(): void;
/**
* Release WebGL resources.
*/
dispose(): void;
/**
* Query GPU availability / fallback state.
*/
isGpuReady(): boolean;
isFallbackActive(): boolean;
}
```
### Usage in PlanView
**Before** (SVG only):
```tsx
function PlanView({ plan, ... }) {
return (
<svg ref={svgRef}>
<defs>{hatches}</defs>
{plan.primitives.map((p, i) => <PrimitiveShape ... />)}
</svg>
);
}
```
**After** (GPU + SVG fallback):
```tsx
function PlanView({ plan, ... }) {
const rendererRef = useRef<PlanRenderer | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
rendererRef.current = new PlanRenderer(canvas, { enableGpu: true });
rendererRef.current.compilePlan(plan);
}, [plan]);
useEffect(() => {
rendererRef.current?.setViewMatrix(view, { w: canvasWidth, h: canvasHeight });
}, [view, canvasWidth, canvasHeight]);
useEffect(() => {
const frame = () => {
rendererRef.current?.render();
rafId = requestAnimationFrame(frame);
};
rafId = requestAnimationFrame(frame);
return () => cancelAnimationFrame(rafId);
}, []);
return (
<div style={{ position: "relative" }}>
{/* GPU canvas (or SVG fallback if GL unavailable) */}
<canvas ref={canvasRef} style={{ position: "absolute" }} />
{/* Thin SVG overlay: text, grips, snap-markers, tool preview */}
<svg ref={svgRef} style={{ position: "absolute", zIndex: 1 }}>
{/* text, grips, snaps only; geometry stays in WebGL */}
</svg>
</div>
);
}
```
---
## Data Flow: From Primitives → GPU
### 1. **Compile Phase** (glPlanCompile.ts)
```typescript
export function compilePlan(gl: WebGL2RenderingContext, plan: Plan): GLGeometryBatch {
const batches: BatchInfo[] = [];
const vertices: number[] = [];
const indices: number[] = [];
let indexOffset = 0;
for (const prim of plan.primitives) {
if (prim.kind === "polygon") {
const { verts, inds } = tessellatePolygon(prim.pts);
const color = parseColor(prim.fill);
batches.push({
kind: "polygon",
indexStart: indexOffset,
indexCount: inds.length,
color,
hasHatch: prim.hatch.pattern !== "none",
hatchPattern: prim.hatch.pattern,
});
vertices.push(...verts);
indices.push(...inds.map((i) => i + indexOffset));
indexOffset += verts.length / 2;
} else if (prim.kind === "line") {
const { verts, inds } = tessellateLineQuad(prim.a, prim.b);
const color = parseColor(prim.color || "black");
batches.push({
kind: "line",
indexStart: indexOffset,
indexCount: inds.length,
color,
strokeWidthMm: prim.weightMm,
});
vertices.push(...verts);
indices.push(...inds.map((i) => i + indexOffset));
indexOffset += verts.length / 2;
}
// arc → polyline → quads (deferred for MVP)
}
const vbo = gl.createBuffer()!;
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
const ibo = gl.createBuffer()!;
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indices), gl.STATIC_DRAW);
const vao = gl.createVertexArray()!;
gl.bindVertexArray(vao);
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 8, 0); // position
gl.enableVertexAttribArray(0);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
return { vertexBuffer: vbo, indexBuffer: ibo, vao, batches, vertexCount: vertices.length, indexCount: indices.length };
}
```
### 2. **Render Phase** (glPlanRender.ts)
```typescript
export function renderPlan(
gl: WebGL2RenderingContext,
state: GLPlanRenderState,
batch: GLGeometryBatch
): void {
gl.clearColor(1, 1, 1, 1); // white background
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(state.solidFillProgram);
const mvpLoc = gl.getUniformLocation(state.solidFillProgram, "viewProjection");
const mvp = mat4.multiply(state.projMatrix, state.viewMatrix);
gl.uniformMatrix4fv(mvpLoc, false, mvp);
gl.bindVertexArray(batch.vao);
for (const b of batch.batches) {
const colorLoc = gl.getUniformLocation(state.solidFillProgram, "vertexColor");
gl.uniform4f(colorLoc, b.color[0], b.color[1], b.color[2], b.color[3]);
gl.drawElements(gl.TRIANGLES, b.indexCount, gl.UNSIGNED_INT, b.indexStart * 4);
}
}
```
---
## Tessellation Details
### Earcut (Polygon Triangulation)
**Inlined earcut logic (no npm):**
```typescript
function tessellatePolygon(pts: Vec2[]): { verts: number[]; inds: number[] } {
// Convert Vec2[] → flat float array
const coords = pts.flatMap((p) => [p.x, p.y]);
// Earcut2D: robust polygon triangulation
// → Returns index array (triplets = triangles)
const triangles = earcut(coords);
// Vertex buffer: just positions (x, y) in world space (meters)
const verts = coords;
return { verts, inds: triangles };
}
// Simplified earcut (full version ~200 LOC; reference libtess2 or earcut.js)
function earcut(data: number[], hole?: number[], dim?: number): number[] {
// ... iterative ear clipping, complexity O(n²) worst-case
// Returns Uint32Array of triangle indices
}
```
### Line Quad Expansion
```typescript
function tessellateLineQuad(
a: Vec2, b: Vec2,
widthMm: number = 0.5
): { verts: number[]; inds: number[] } {
// World-space endpoints; width (mm) will be expanded in vertex shader
// Create a degenerate quad: 2 triangles
// Vertices: [a_left, a_right, b_left, b_right]
// (normal expansion happens in VS)
const verts = [
a.x, a.y, 0.0, // vertex 0: a, left flag
a.x, a.y, 1.0, // vertex 1: a, right flag
b.x, b.y, 0.0, // vertex 2: b, left flag
b.x, b.y, 1.0, // vertex 3: b, right flag
];
// Two triangles: (0, 1, 2) and (1, 3, 2)
const inds = [0, 1, 2, 1, 3, 2];
return { verts, inds };
}
```
---
## Pan/Zoom Matrix Transform
### View Box → Clip Space
```typescript
function buildViewMatrix(
viewBox: { x, y, w, h },
canvasSize: { w, h }
): Matrix4 {
// 1. World space (meters, origin at model 0,0) → viewBox units (PX_PER_M=90)
const scale = PX_PER_M; // 1 meter → 90 viewBox units
// 2. ViewBox viewport: x,y,w,h in viewBox units → NDC [-1,+1]²
// Orthographic projection (no perspective).
const ortho = mat4.ortho(
viewBox.x,
viewBox.x + viewBox.w,
viewBox.y,
viewBox.y + viewBox.h,
-1, 1
);
// 3. Scale from viewBox units → world (invert PX_PER_M)
const scaleMatrix = mat4.scale(mat4.identity(), [1/scale, 1/scale, 1]);
return mat4.multiply(ortho, scaleMatrix);
}
```
Whenever PlanView calls `setView(viewBox)` or `onWheel()` → call `setViewMatrix()` → GPU re-renders with new matrix uniform (no tessellation).
---
## Fallback Strategy: SVG Renderer Flag
**Global flag** in PlanView or app state:
```typescript
const [useGpuRenderer, setUseGpuRenderer] = useState(true);
```
**Render path branching:**
```typescript
return useGpuRenderer && rendererRef.current?.isGpuReady()
? <canvas ref={canvasRef} />
: <svg ref={svgRef}>{/* existing SVG rendering */}</svg>;
```
**When GL fails** (e.g., no WebGL2 support, Out-Of-Memory):
1. Renderer catches error in `compilePlan()`
2. Sets internal `fallbackActive = true`
3. Returns gracefully (app renders SVG path instead)
4. User sees same plan, slower but functional
---
## Implementation Order (MVP → Iteration)
### Phase 1: Core (Week 1)
1. **glPlanTypes.ts** — TypeScript interfaces for GPU state
2. **glPlanShaders.ts** — Compile vertex/fragment shaders, handle GL errors
3. **glPlanCompile.ts** — Tessellation (earcut inlined), buffer upload
4. **glPlanRender.ts** — Draw loop, matrix uniform, clear/present
5. **PlanRenderer.ts** — Main class, dispatcher (GPU vs SVG fallback)
6. **PlanView.tsx** — Wire renderer, canvas overlay, canvas lifecycle
### Phase 2: Hatches & Lines (Week 2)
- Improve line tessellation: proper screen-space width (dFdx/dFdy or pre-computed normals)
- Hatch patterns: texture-based or procedural fragment shader (diagonal/insulation)
- Arc tessellation: polyline → quads
### Phase 3: Polish (Week 3)
- Stroke dashing via geometry or fragment shader
- Greyed opacity blending
- Hit testing integration (point-in-triangle for GPU)
- Performance profiling, batch merging
---
## Performance Targets
| Operation | SVG (Current) | GPU (Target) | Notes |
|-----------|---------------|--------------|-------|
| **Tessellation** | — | 10–50 ms | Once per plan |
| **Pan/Zoom 60 Hz** | 16 ms (re-render SVG) | <1 ms (matrix uniform) | Matrix upload negligible |
| **Pan/Zoom 144 Hz** | 7 ms (bottleneck) | <0.5 ms | 28× speedup expected |
| **Geometry: 1000 polygons** | 50–100 ms SVG render | 1–5 ms GPU draw | CPU tessellation pipelined |
User: AMD RX 7800 XT → easily capable of 4K+ geometry at 144 Hz.
---
## Known Deferred Items (Post-MVP)
- **Hatches**: Solid fill only MVP; insulation/diagonal/crosshatch in Phase 2 via texture or procedural shader
- **Dashing**: Not in MVP (complex with screen-space strokes); either CPU pre-tessellation or fragment shader alpha-discard
- **Arcs**: Fallback to SVG for MVP; GPU polyline expansion in Phase 2
- **Text, Grips, Snaps**: Stay in SVG overlay indefinitely (no GPU benefit; text rendering nontrivial)
- **Hit Testing**: Keep in CPU/SVG for MVP; GPU pick-buffer deferred
- **Color/Opacity Blending**: Basic for MVP; advanced (multiply, screen, dodge) deferred
---
## References
- **Earcut.js**: https://github.com/mapbox/earcut — polygon triangulation (logic to inline)
- **three.js line expansion**: https://github.com/mrdoob/three.js/blob/master/src/renderers/webgl/WebGLGeometries.js
- **OpenGL Perspective Division**: https://en.wikibooks.org/wiki/OpenGL_Programming/Modern_OpenGL_Tutorial_Polygon_offset
- **Screen-Space Stroke Width**: https://forum.libcinder.org/topic/smooth-line-rendering-using-geometry-shaders
- PlanView source: `/home/karim/cad/src/plan/PlanView.tsx` (2500 LOC)
- Primitive types: `/home/karim/cad/src/plan/generatePlan.ts:133`
+81
View File
@@ -0,0 +1,81 @@
# Briefing — Nativer wgpu-2D-Renderer (parallele Instanz)
> Für die Instanz/den Agenten, der den nativen GPU-Renderer baut. Isoliert vom
> Web-Renderer; koordiniert über dieses Dokument.
## COMMIT-REGEL (verbindlich)
Dieses Repo darf **keinerlei Hinweise auf KI-Werkzeuge** enthalten — nicht im Code,
in Kommentaren oder in der Git-Historie. Keine `Co-Authored-By`-Trailer, keine
„Generated with"-Zeilen, keine Modell-/Agenten-Namen. Kommentare deutsch,
Identifier englisch. **Kein Commit ohne Ansage der Hauptinstanz.**
## Warum
Der 2D-Plan wird aktuell in einem WebGL-Canvas gerendert (Web-Stack). In Chromium
ist das 144-Hz-flüssig, aber **Tauris Linux-Webview = WebKitGTK bremst** (langsamer
Compositor, vermutlich 60-Hz-rAF-Cap). Bewiesen: dieselbe App im `chromium --app`-
Fenster = butterweich, im Tauri-Fenster = zäh — trotz imperativem Pan, CSS-transform-
Overlay, DMABUF-Tweaks. Siehe Memo `webkitgtk-bottleneck`.
**Lösung:** die schwere Grafik **nativ mit wgpu** rendern (Rust), die Webview macht
nur noch UI-Chrome (Panels/Buttons). So bleibt Tauri (natives Rust-Backend, winziges
Bundle) UND wir umgehen den Webview-Compositor komplett.
## Referenz-Implementierung (NICHT wegwerfen — 1:1 portierbar)
Der WebGL-Renderer unter `src/plan/glPlan/` ist die **arbeitende Referenz**. Die
harte Arbeit ist dort schon gelöst und portiert konzeptuell direkt nach wgpu:
- `glPlanCompile.ts` — **Earcut-Triangulierung** (konkav-fähig), Linien→Quads mit
Normale+Seiten-Flag, Batch-Merging, Farb-Parsing. Unit-Tests: `glPlanCompile.test.ts`.
- `glPlanShaders.ts` — Vertex/Fragment (GLSL 300 es). **WGSL ≈ GLSL** — direkt übersetzbar.
- Füll-VS: Bildschirm-Position → Clip via `viewProj`-Matrix.
- Linien-VS: bildschirmkonstante bzw. papier-mm-Breite via Normalen-Offset im Clip
(siehe `strokeScale`/`strokePx`-Logik — Papier-mm × Massstab N × meet-Skala).
- `glPlanRender.ts` — Ortho-Matrix (aspekt-korrekt = SVG `xMidYMid meet`), Fill-/Line-
Pass, `computeOrthoMatrix`.
- Koordinaten-Konvention: BILDSCHIRM-Raum `sx = mx·PX_PER_M, sy = -my·PX_PER_M`
(`PX_PER_M=90`), Modell-Y hoch → Bildschirm-Y runter.
- Strichbreite = **echte Papier-mm im Massstab**: `Breite_px = mm · N/1000 · PX_PER_M ·
meetSkala` (repliziert SVG `printStrokeVb`). Am Beispiel 0.35 mm @ 1:100 = 0.35 mm.
Die **Daten** kommen aus `src/plan/generatePlan.ts` → `Plan.primitives` (Union-Typ
`Primitive`: polygon/line/arc/text). Text bleibt Overlay (nicht wgpu).
## Die EINE harte Frage (zuerst spiken/recherchieren)
**Wie rendert wgpu in das Tauri-Fenster neben der Webview?** Optionen recherchieren:
1. Natives Child-Surface unter der Webview (raw-window-handle), Webview transparent
drüber für UI-Chrome. Vermutlich der Zielweg.
2. Eigenes wgpu-Fenster (entkoppelt) — nur für den Spike, um Rendering von der
Fenster-Integration zu trennen.
3. wgpu→Textur→Webview — verwirft den Zweck (Copy-Overhead), nur Notnagel.
**Empfehlung:** Rendering-Spike ZUERST entkoppelt (Option 2, standalone `winit`+wgpu-
Fenster), das die `Primitive` als gefüllte Polygone + Striche zeichnet und Pan/Zoom
per Matrix-Uniform macht. Fenster-Integration in Tauri ist der zweite, separate Schritt.
## Meilensteine
- **M0 (Research):** kurzer Bericht `docs/design/wgpu-integration-findings.md` — wie
wgpu-Surface + Tauri/webview koexistieren (raw-window-handle, Transparenz, Z-Order,
Input-Routing). Quellen verlinken.
- **M1 (Rendering-Spike, standalone):** neue Crate `src-tauri/render2d/` (serde-Input
= geflachte Primitive), wgpu + WGSL:
- Earcut-Port (oder `lyon`/`earcutr` Crate evaluieren) → Dreiecke.
- Füll-Pipeline (gefüllte Polygone, Ortho-Matrix-Uniform).
- Linien-Pipeline (Quad-Expansion, echte Papier-mm-Breite).
- `cargo test` für die Tessellierung (Muster: `src-tauri/geometry` hat 4/4 Tests —
z. B. konkaves L flächentreu, wie `glPlanCompile.test.ts`).
- `cargo build` grün. (Visuelle Fenster-Verifikation braucht Display → an Mensch/
Display-Session übergeben; im Bericht vermerken.)
- **M2 (Tauri-Integration):** Surface unter die Webview, Pan/Zoom-Input aus der
Webview an den Renderer, Parität mit dem WebGL-Pfad messen (144 Hz).
- **M3:** Schraffur, Bögen, Auswahl-Highlight; danach 3D (three.js→wgpu).
## Koordination (WICHTIG)
- **Eigener Branch** (z. B. `feature/wgpu-renderer`), NICHT auf `master`/
`feature/parametric-walls` committen. Isolierter Worktree.
- **Nur** `src-tauri/` + neue Rust-Module + `docs/`. **NICHT** `src/App.tsx`,
`src/plan/*`, `types.ts` anfassen (die Hauptinstanz arbeitet dort an Features).
- Der Web-WebGL-Renderer bleibt bestehen (Browser-Lauffähigkeit + Referenz).
- Ergebnisse/Diffs zurückliefern; die Hauptinstanz committet und pflegt das Memory.
## Gates
`cargo check`/`cargo test`/`cargo build` grün. Trace-Scan sauber (COMMIT-REGEL).
`npx tsc -b` + `npm run build` müssen unberührt grün bleiben (keine Web-Änderungen).
+265
View File
@@ -0,0 +1,265 @@
# Briefing — Nativer wgpu-3D-Renderer (M0 Design + M1 Spike)
> Schwester-Dokument zu `wgpu-2d-renderer-briefing.md`. Beschreibt den Weg vom
> jetzigen three.js-3D-View (WebGL im WebKitGTK-Webview) zu einer nativen
> wgpu-Engine (Rust). Dieses Dokument ist der ANFANG: M0 (Bestandsaufnahme +
> Port-Plan) und M1 (entkoppelter Standalone-Spike). Noch NICHT die Migration.
## Commit-/Spuren-Regel
Wie im ganzen Repo: keine Hinweise auf KI-Werkzeuge — nicht im Code, in Kommentaren
oder in der Historie. Kommentare deutsch, Identifier englisch. Kein Commit ohne
Ansage der Hauptinstanz.
## Warum
Der 3D-View laeuft heute als three.js/WebGL im Tauri-Webview (WebKitGTK). Wie beim
2D-Plan bremst dieser Compositor unter Linux (siehe `wgpu-2d-renderer-briefing.md`
und Memo `webkitgtk-bottleneck`). Die schwere 3D-Grafik soll daher **nativ mit wgpu**
gerendert werden (Rust), die Webview macht nur noch UI-Chrome. Das umgeht den
Webview-Compositor komplett und teilt sich die Toolchain mit dem 2D-Renderer
(`src-tauri/render2d/`, gleiche Feature-Stufung, gleiche Test-Muster).
---
## 1. Bestandsaufnahme — was der three.js-View rendert
Referenz: `src/viewport/Viewport3D.tsx` (analysiert), plus die Geometrie-Grundlage
in `src/model/geometry.ts`, `src/model/wall.ts`, `src/model/joins.ts`,
`src/geometry/opening.ts`, `src/geometry/stair.ts`. Zeilenangaben beziehen sich auf
den Stand der Analyse.
### Koordinaten-Konvention (verbindlich)
Das Modell ist 2D in Metern (`x`, `y`) plus Hoehe `z`. Die 3D-Welt ist **Y-up**:
```
world.x = model.x
world.y = Hoehe (z)
world.z = model.y
```
D.h. **der Grundriss liegt in der XZ-Ebene, die Extrusion laeuft entlang +Y.**
Belegt u.a. in `Viewport3D.tsx`:
- Kommentar (~Z. 473): „Modell (x,y,z) → Three (x, z, y) (Z = Hoehe nach oben)".
- Kontext-Mesh (~Z. 1679–1684): `verts[i]=pos.x; verts[i+1]=pos.z (Hoehe); verts[i+2]=pos.y`.
- Wand-Griffe (~Z. 764): `new THREE.Vector3(wall.start.x, zBottom, wall.start.y)`.
- Workplane-Raycast (~Z. 627): `{ x: hit.x, y: hit.z }` (Three → Modell).
Diese Konvention ist in `render3d` 1:1 uebernommen (`types.rs`, `mesh.rs`).
### Waende (der Kern)
- Funktion `addWallMeshes()` / `addLayerPrism()` (~Z. 1939–2039, 2237–2271).
- **Mesh-Weg:** `THREE.ExtrudeGeometry` (~Z. 2248) ueber die 2D-Bandform, die
`clippedBand(p1, p2, offA, offB, startCut, endCut)` liefert (~Z. 2237; Funktion in
`src/model/geometry.ts:87`). Extrudiert wird um `depth = zTop - zBottom`.
- Die Bandform kommt aus Achse + Dicke: `wallBand`/`wallCorners`
(`geometry.ts:53`/`:70`) versetzen die Achse um `thickness/2` entlang der
**Links-Normale** `leftNormal(u) = (-u.y, u.x)` (`geometry.ts:17`), CCW-Umlauf.
- `ExtrudeGeometry` liegt in der XY-Ebene und waechst entlang +Z; three.js dreht das
Prisma daher um +90 Grad um X und setzt es auf `topY` (~Z. 2266–2271). In wgpu
extrudieren wir direkt in world (XZ-Grundriss, +Y-Hoehe) und sparen die Drehung.
- **Hoehe/Basis:** `wallVerticalExtent(project, wall)` (`wall.ts:56`) liefert
absolute `zBottom`/`zTop` (aus `wall.bottom`/`wall.top`-Ankern bzw. Geschoss-
`baseElevation + wall.height`).
- **Mehrschichtig:** je `wt.layers`-Schicht ein eigenes Prisma mit Dicken-Offset
(~Z. 1991–2015). M1 extrudiert vereinfacht EINE Schicht (Gesamtdicke).
- **Ecken/Gehrung:** `computeJoins()` (`joins.ts:45`) berechnet Schnittlinien
(`startCut`/`endCut`), die `clippedBand` an L-Ecken auf Gehrung zieht
(`miterLine`, `joins.ts:101`). M1 laesst das noch weg (stumpfe Enden).
### Oeffnungen (Fenster/Tueren)
- `addOpeningMeshes()` (~Z. 2056–2169) + Segmentierung in `addWallMeshes` (~Z.
1968–2035). **Kein CSG/Boolean:** die Wand wird entlang der Achse in Segmente
zerlegt (`openingInterval`, `geometry/opening.ts:31`), und je Oeffnung entstehen
bis zu drei Prismen: Wand DAVOR, **Bruestung** unter dem Fenster (`sillRel`),
**Sturz** ueber der Oeffnung (`headRel`). Rahmen/Fluegel als `BoxGeometry`;
Glas semitransparent, Tuerfluegel um `swingAngle` gedreht.
### Treppen
- `addStairMeshes()` (~Z. 2357–2409). `stairGeometry()` (`geometry/stair.ts`)
liefert Trittflaechen (Footprint + Steig-Hoehe) + optionalen Podest-Umriss; jede
Stufe als extrudierter Block (`ExtrudeGeometry`), Hoehe = `stairVerticalExtent`.
### Decken/Platten
- `addCeilingMesh()` (~Z. 2290–2347). `ceiling.outline` als `ExtrudeGeometry`, Tiefe
= Deckenstaerke, waechst nach unten von `zTop` (`ceilingVerticalExtent`, `wall.ts:80`).
### Raeume
- Nicht als eigenstaendige 3D-Koerper gerendert (2D-Grundriss-Repraesentation).
### Kontext/Gelaende
- `buildContext()` (~Z. 1651–1718). Terrain/importierte Meshes als rohe
`BufferGeometry` (Positions/Indices, Koordinaten-Swap wie oben); Hoehenlinien als
`LineSegments`. Dazu ein `GridHelper` (~Z. 421) auf OKFF-Hoehe.
### Materialien
- `MeshLambertMaterial` (Waende/Oeffnungen/Treppen, per Komponente eingefaerbt,
~Z. 2176–2206), `MeshStandardMaterial` (Weiss-/Textur-Modus + Terrain, PBR:
`roughness`/`metalness`/`aoMap`, ~Z. 445–491, 2001–2015), `MeshBasicMaterial`
(Hidden-Line-Flaechen + immer-oben-Marker), `LineBasicMaterial` (Kanten/2D-
Zeichnungen). Render-Modi: shaded / white / textured / wireframe / hidden-line.
### Beleuchtung
- `AmbientLight(0xffffff, 0.6)` (~Z. 416) + `DirectionalLight(0xffffff, 1.1)` bei
`(6, 12, 4)` (~Z. 417). **Keine Schatten** konfiguriert. Keine Hemisphere/Point-
Lights.
### Kamera + Presets
- Zwei Kameras: `PerspectiveCamera(fov, 1, 0.1, 1000)` (~Z. 367) und
`OrthographicCamera(-1,1,1,-1, 0.1, 5000)` (~Z. 374). `applyView3d()` (~Z.
1566–1633) setzt fuenf Presets:
- **front** — Richtung `(0,0,1)`, orthografisch.
- **side** — Richtung `(1,0,0)`, orthografisch.
- **top** — Richtung `(0,1,~0)`, orthografisch (Rotation gesperrt).
- **iso** — Richtung `(1,1,1)` normiert, orthografisch.
- **perspective** — Richtung `(0.62,0.5,0.7)` normiert, perspektivisch.
- Umschalten perspektiv/ortho ueber `active = perspective ? camera : orthoCamera`
(~Z. 1602); Ortho-Frustum aus den Modell-Bounds (`updateOrthoFrustum`, ~Z. 1522).
- **OrbitControls** (~Z. 391–413): Mitteltaste orbit, Shift+Mitte pan, Rad zoom
(linke/rechte Taste fuer Auswahl/Kontextmenue umgewidmet).
### Griffe / Gizmos
- Editier-Griffe (`SphereGeometry`, ~Z. 711–814): Endpunkt (orange), Hoehe (blau),
Verschieben (gruen); `depthTest:false` (immer sichtbar). Drag ueber Workplane-
Raycast. Fuer den nativen Renderer spaeter relevant (eigener Overlay-Pass).
### Schnittebene
- **Nicht implementiert:** keine `renderer.clippingPlanes` / `localClippingEnabled`.
Schnitte laufen aktuell 2D. Fuer wgpu ein eigenständiger spaeterer Milestone
(Clip-Distances im Shader oder Stencil-Capping).
### Tiefe / Culling
- Tiefentest three.js-Standard aktiv. Backface-Culling per Default (Ausnahme:
Terrain/Import `DoubleSide`). Diverse Overlays mit `depthTest:false`.
---
## 2. Port-Plan nach wgpu
### Datenfluss
Web-Modell → **geflachte Eingabe** (`WallInput`, spaeter Oeffnungen/Treppen/Decken)
→ `render3d`-Mesh-Erzeugung → GPU-Buffers → Draw. Analog zum 2D-Pfad (`Scene` →
Tessellierung → Buffers). Die Eingabe ist bewusst serde-only und GPU-frei, damit die
Mesh-Logik headless testbar bleibt.
### Mesh-Erzeugung (Waende extrudieren)
- Band aus Achse + Dicke ueber die Links-Normale (`(-u.y, u.x) * thickness/2`,
CCW), exakt wie `wallCorners`. Extrusion in world: XZ-Grundriss, +Y von
`base_elevation` bis `+height`.
- Ein Quader = 6 Seiten, je eigene Vertices mit Flaechen-Normale (flaches Shading,
korrektes Backface-Culling). 24 Vertices / 36 Indizes je Wand.
- Spaeter: mehrschichtige Waende (je Schicht ein Prisma), Gehrung
(`computeJoins`/`clippedBand`-Port), Oeffnungs-Segmentierung (Bruestung/Sturz).
### Kamera (View/Projektion, Presets)
- `look_at` (right-handed, Kamera blickt entlang -Z im View-Raum), `perspective`
und `orthographic` — beide auf **Clip-Z in [0,1]** (wgpu-Konvention, NICHT [-1,1]).
- Fuenf Presets (`preset_camera`): front/top/side orthografisch achsparallel,
iso/persp perspektivisch. `top` mit up=-Z, damit Modell-Y im Bild nach unten
zeigt (wie die 2D-Sicht).
- Orbit-Kamera aus Yaw/Pitch/Distanz (`orbit_eye`, Pitch geklemmt gegen Pol-Flip).
### Beleuchtung
- Zunaechst EIN Directional-Light (Richtung ZUM Licht) + ambienter Sockel im
Fragment-Shader (WGSL) — das GPU-Aequivalent zu `AmbientLight(0.6)` +
`DirectionalLight(1.1)@(6,12,4)`. Diffuses Lambert. PBR (Rauheit/Metallik/
Texturen/AO) spaeter.
### Tiefenpuffer + Culling
- `Depth32Float`-Attachment, `depth_compare = Less`, `depth_write = true`.
- `front_face = Ccw`, `cull_mode = Back` (die Extrusion liefert konsistent nach
aussen zeigende CCW-Flaechen).
### Matrix-Mathematik
- Handgerechnet (kein `glam`) in der serde-only Schicht — begruendet in `math.rs`:
die Standard-Schicht soll wie in render2d ohne Zusatz-Crates headless test-/baubar
bleiben; der Umfang (perspective/ortho/look_at + Orbit) ist klein und exakt
testbar. Ein spaeterer Wechsel zu `glam` (nur in der GPU-Schicht) bleibt moeglich,
ohne die Kamera-Tests anzufassen. Alles spalten-major, direkt als Uniform ladbar.
### Milestones M2..Mn
- **M2 — Mehrschichtige Waende + Gehrung:** Port von `computeJoins`/`miterLine` +
`clippedBand` → gehrte Bandformen je Schicht; Farben/Materialien je Komponente.
- **M3 — Oeffnungen:** Achsen-Segmentierung (Bruestung/Sturz) + Rahmen/Glas/Fluegel
als eigene Meshes; Tuerschwenk-Winkel.
- **M4 — Treppen + Decken:** Port von `stairGeometry`/`ceilingVerticalExtent`.
- **M5 — Kontext/Gelaende:** rohe Terrain-/Import-Meshes + Hoehenlinien + Grid.
- **M6 — Materialien (PBR):** `MeshStandardMaterial`-Aequivalent (roughness/
metalness/albedo/AO-Textur); Render-Modi shaded/white/textured/wireframe/hidden.
- **M7 — Schnittebene:** Clip-Distances im Shader oder Stencil-Capping (Feature, das
der three.js-View gar nicht hat — echter Mehrwert).
- **M8 — Griffe/Gizmos + Picking:** Overlay-Pass (immer-oben) + GPU-/Ray-Picking.
- **M9 — Tauri-Integration:** Surface unter der Webview (raw-window-handle,
Z-Order, Input-Routing) — siehe `wgpu-2d-renderer-briefing.md` M2 (gleiches
Integrations-Problem; einmal loesen, fuer 2D+3D nutzen). Kamera-Presets/Orbit-
Input aus der Webview an den Renderer.
---
## 3. Was in `src-tauri/render3d/` steht (M1)
Neue, eigenstaendige Crate (eigener leerer `[workspace]`-Block, wie render2d), damit
`cargo test`/`build` unabhaengig vom Tauri-Workspace laufen. Feature-Stufung 1:1 wie
render2d:
- `Cargo.toml` — Features `default` (serde-only) / `render` (wgpu) / `window`
(winit-Spike). `[[bin]] spike3d` mit `required-features = ["window"]`.
- `src/types.rs` — serde-only Eingabe: `WallInput { start, end, thickness, height,
base_elevation, color }`, `Camera` (+ `Projection`), `CameraPreset`; Ausgabe
`Mesh` (interleaved `[pos.xyz, normal.xyz, color.rgb]` + Indizes) mit
`vertex_count`/`triangle_count`/`bounds`. Koordinaten-Konvention dokumentiert.
- `src/mesh.rs` — Wand-Extrusion: `extrude_wall`/`build_walls_mesh`. Band ueber
Links-Normale, Quader mit sechs eigenen Seiten, nach aussen zeigende Normalen.
- `src/math.rs` — `Mat4` (spalten-major), `perspective`/`orthographic` (Clip-Z
[0,1]), `look_at`, `view_projection`, `orbit_eye`, `preset_camera` (fuenf Presets).
- `src/shaders.rs` — WGSL (`MESH_WGSL`): View-Projektion-Uniform + Directional-
Light + ambienter Sockel im Fragment-Shader.
- `src/gpu.rs` (Feature `render`) — `Renderer`: eine Pipeline mit Tiefenpuffer,
View-Projektions-Uniform, Backface-Culling. `upload_walls` → GPU-Buffers,
`render(camera, viewport)`.
- `src/bin/spike3d.rs` (Feature `window`) — winit-Fenster mit Demo-Raum (5
extrudierte Waende) + **Orbit-Kamera** (linke Maustaste dreht Yaw/Pitch, Rad
zoomt Abstand). Matrix-getrieben, kein Re-Meshing beim Kamera-Wechsel.
- `src/lib.rs` — Modul-Deklarationen, Re-Exports, Tests.
### Tests (`cargo test`, default-Feature)
Muster wie render2d/`glPlanCompile.test.ts`:
- Quader-Zaehlung (eine Wand → 24 Vertices / 36 Indizes / 12 Dreiecke).
- Mehrere Waende addieren sich.
- Bounding-Box deckt Laenge/Dicke/Hoehe ab; `base_elevation` verschiebt in Y.
- Deckel-Normale = +Y; **alle Mantel-Normalen zeigen nach aussen** (Dot mit
„Vertex − Zentrum" ≥ 0 → Backface-Culling korrekt).
- Diagonale Wand; degenerierte Wand (Start==Ende) erzeugt nichts (kein Absturz).
- Kamera: `look_at` setzt Ziel auf view-z=-dist; Perspektive klemmt z in [0,1];
`orbit_eye` haelt den Abstand; Presets setzen die richtige Projektionsart.
- Mit `--features render`: WGSL headless via `naga` (Parser + Validator) validiert.
---
## 4. Build-/Test-Ergebnis
Alle Gates gruen (Toolchain: cargo 1.96, wgpu 22, winit 0.30):
- `cargo test` (default) — **12/12** gruen (Mesh + Kamera).
- `cargo test --features render` — **13/13** gruen (inkl. WGSL-naga-Validierung).
- `cargo build` (default), `--features render`, `--features window` — je gruen,
**keine Warnungen**.
- Trace-Scan sauber (keine KI-Spuren).
- Web-Gates unberuehrt (nur `src-tauri/` + `docs/` angefasst; `src/` nur gelesen).
**Visuelle Fenster-Verifikation** ist headless NICHT moeglich. Auf einer aktiven
Display-Session pruefbar mit:
```
cargo run --features window --bin spike3d
```
Erwartet: ein Raum aus extrudierten Waenden mit diffuser Beleuchtung; linke
Maustaste dreht die Orbit-Kamera, das Rad zoomt.
---
## 5. Naechste Schritte
1. **M2** starten: `computeJoins`/`clippedBand`-Port für gehrte, mehrschichtige
Waende (die Bandmath ist im Web bereits verifiziert — gleiche Tests portieren).
2. Oeffnungs-Segmentierung (M3) auf demselben Extrusions-Kern.
3. Die **Tauri-Integration (M9)** gemeinsam mit dem 2D-Renderer loesen (ein Surface-
Unterbau, ein Input-Routing) — das ist der eigentliche Engpass, nicht das
Rendering. Erst standalone spiken (dieser Stand), dann unter die Webview.
+566
View File
@@ -0,0 +1,566 @@
# Swisstopo-Geodaten & SIA-Flächenstandards im Browser-BIM
> Stand: 2026-06-29 · Recherche für das Standalone-Browser-BIM (React + TS + Three.js),
> Port von **DOSSIER** (Rhino-Plugin). Ziel: (A) Schweizer Geodaten (Höhenmodell,
> Orthofoto, 3D-Gebäude, Parzellen) direkt im Browser laden, (B) Standort-Kontext
> (Gelände + Parzelle + Nachbargebäude) für ein Projekt importieren, (C) SIA-416-
> Flächen/Volumen + Raumschemata berechnen wie in DOSSIER.
>
> Bezug zur ROADMAP: Swisstopo/Terrain/OSM = **Phase 4** (Kontext/Daten); SIA-416-
> Räume + Bilanz-CSV = **Phase 2**. Beide sind dort bereits als ⭐-Features gelistet.
**Alle in diesem Dokument genannten geo.admin.ch-Endpunkte wurden am 2026-06-29 live
gegen die echte API getestet** (curl + CORS-Header-Check). Wo „verifiziert" steht,
liegt eine echte Antwort vor.
---
## Teil A — Swisstopo-APIs & Dienste aus dem Browser
### A.0 Das Wichtigste vorweg: CORS & Lizenz
Zwei Fragen entscheiden, ob ein Dienst *ohne Backend-Proxy* aus einer reinen
Browser-App nutzbar ist: CORS und Lizenz. Beide sind hier günstig.
**CORS (live verifiziert):** Alle relevanten Hosts senden `access-control-allow-origin: *`:
| Host | Dienst | CORS | Range-Requests |
|---|---|---|---|
| `api3.geo.admin.ch` | REST (height, profile, identify, find, search) | ✅ `*` | — |
| `data.geo.admin.ch` | STAC-API + Daten-Assets (COG-GeoTIFF, XYZ.zip) | ✅ `*` | ✅ `206 Partial Content`, `accept-ranges`/`content-range` vorhanden |
| `wmts.geo.admin.ch` | WMTS-Kacheln | ✅ `*` | — |
| `3d.geo.admin.ch` | 3D-Tiles (`tileset.json` + glTF) | ✅ `*` | — |
→ **Konsequenz:** Höhenabfrage, Geocoding, Parzellen-Identify, Karten-/Orthofoto-
Kacheln, **COG-GeoTIFF-Höhenmodell per Range-Request** und 3D-Tiles sind **direkt aus
dem Browser ohne eigenen Proxy** abrufbar. Das ist ein großer Vorteil gegenüber vielen
anderen nationalen Geodiensten.
**Lizenz:** swisstopo/geo.admin.ch ist **Open Government Data**: „The acquisition and
use of data or services is free of charge, subject to the provisions on fair use."
Kommerzielle Nutzung ist erlaubt, Einbindung in (auch kommerzielle) Web-Apps explizit
gedeckt. Pflicht-Attribution: **`© swisstopo`** (bzw. „© Data: swisstopo"). „Fair use"
= z.B. Web-App mit Ø 20'000 Nutzern/Tag ok; aggressives Bot-Scraping vermeiden. Haftung
ausgeschlossen, ~98% Verfügbarkeit. [Terms of use FSDI](https://www.geo.admin.ch/en/general-terms-of-use-fsdi)
> ⚠️ **Korrektur zu DOSSIER & zur Doku:** Die offizielle REST-Doku notiert beim
> *Height*-Service „This service is not freely accessible (fee required)". Das ist
> **in der Praxis falsch / veraltet**: Der Endpunkt antwortet anonym, ohne Key, mit
> `200` und CORS `*` (verifiziert, siehe A.1). DOSSIERs Aussage „alle APIs offen, ohne
> Auth, ohne Key" deckt sich mit der gemessenen Realität. Wir verlassen uns aber nicht
> blind darauf, sondern behandeln 402/429 defensiv (Retry/Backoff, Cache).
### A.1 Höhenabfrage — Height-Service (Einzelpunkt)
Punkt-Höhe (DTM) aus swissALTI3D/DTM. **Verifiziert:**
```
GET https://api3.geo.admin.ch/rest/services/height?easting=2600000&northing=1200000&sr=2056
→ {"height":"555.5"}
```
Parameter:
- `easting`, `northing` — LV95 (`sr=2056`) oder LV03 (`sr=21781`). **Pflicht.**
- `sr` — `2056` (LV95) angeben, sonst Default `21781`.
- `elevation_model` — `DTM2` (= swissALTI3D, 2 m), `DTM25` (Default), `COMB`.
(Im Tal lieferten DTM2/DTM25/COMB denselben Wert; im Steilgelände kann DTM2 genauer sein.)
- `callback` — JSONP (brauchen wir wegen CORS nicht).
Nutzung im Tool: **Projekt-Nullpunkt-Z** bzw. „Gebäude auf Gelände setzen" — eine
einzelne Höhe an der Projekt-Koordinate. Antwortzeit ~50–150 ms. Quelle:
[GeoAdmin REST – Height](https://geoadmin.readthedocs.io/en/latest/services/sdiservices.html)
### A.2 Höhenprofil — Profile-Service (Schnittlinie)
Höhen entlang einer Polylinie — ideal für **Geländeschnitt** unter einem
Gebäude-Schnitt. **Verifiziert** (echte Werte zurück):
```
GET https://api3.geo.admin.ch/rest/services/profile.json
?geom={"type":"LineString","coordinates":[[2600000,1200000],[2600200,1200000]]}
&sr=2056&nb_points=3
→ [{"alts":{"COMB":555.5,"DTM2":555.5,"DTM25":555.5},"dist":0,"easting":2600000,"northing":1200000},
{"alts":{...},"dist":100,...}, {"dist":200,...}]
```
Parameter: `geom` (GeoJSON-LineString, max 6'000 Punkte), `sr`, `nb_points` (Anzahl
Stützpunkte, Default 200), `elevation_models`, `offset` (Glättung). Auch als
`profile.csv`. → Für 2D-Geländeschnitte **ohne** Mesh-Download. Quelle:
[GeoAdmin REST – Profile](https://geoadmin.readthedocs.io/en/latest/services/sdiservices.html)
### A.3 swissALTI3D — Höhenmodell als COG-GeoTIFF (Mesh-Quelle) ⭐
Das ist der **Schlüssel für das Gelände-Mesh im Browser**. swissALTI3D ist das präzise
DTM der Schweiz (ohne Vegetation/Bebauung), Auflösung 0.5 m / 2 m, alle 6 Jahre
aktualisiert ([swissALTI3D](https://www.swisstopo.admin.ch/en/height-model-swissalti3d)).
Bezug über die **STAC-API** (verifiziert — Tile `swissalti3d_2019_2599-1198`):
```
GET https://data.geo.admin.ch/api/stac/v1/collections/ch.swisstopo.swissalti3d/items
?bbox=<lonMin,latMin,lonMax,latMax>&limit=...
```
Jedes 1×1-km-Tile liefert pro Auflösung **zwei** Asset-Typen:
| Asset | Typ | Browser-tauglich? |
|---|---|---|
| `..._0.5_2056_5728.tif` / `..._2_2056_5728.tif` | **Cloud-Optimized GeoTIFF**, **EPSG:2056** | ✅ **direkt** via `geotiff.js` + Range |
| `..._0.5_2056_5728.xyz.zip` / `..._2_..._xyz.zip` | ASCII-XYZ (E N Z) in ZIP | ✅ via `fflate` entpacken (so macht es DOSSIER) |
**Verifiziert:** `data.geo.admin.ch` liefert auf das `.tif` ein `206 Partial Content`
mit `content-range` bei `Range:`-Header und CORS `*`. Das bedeutet: **`geotiff.js`
liest nur den benötigten Ausschnitt eines COG per HTTP-Range, ohne das ganze File zu
laden** — perfekt für eine Browser-App. Bbox in WGS84 für STAC, Tile-Daten dann in
LV95-Metern (kein Reprojizieren der Z-Werte nötig). Quelle:
[STAC tech docs](https://docs.geo.admin.ch/) · COG-Tile live geprüft.
### A.4 SWISSIMAGE / Karten — WMTS-Kacheln
Orthofoto (10 cm) und Landeskarten als Kacheln. RESTful-URL-Template:
```
https://wmts.geo.admin.ch/1.0.0/<Layer>/default/<Time>/<TileMatrixSet>/<z>/<TileCol>/<TileRow>.<ext>
```
Beispiel-Layer:
- `ch.swisstopo.swissimage` — Orthofoto, `.jpeg`
- `ch.swisstopo.pixelkarte-farbe` — Landeskarte farbig, `.jpeg`
- `ch.kantone.cadastralwebmap-farbe` — **Katasterplan (AV)**, `.png`
TileMatrixSets: **`2056` (LV95)**, `21781`, `3857` (Web-Mercator), `4326`. Zoom 0–28
(4000 m → 0.1 m); Zoom 27/28 nur für wenige Layer (swissimage, Kataster). Für 3D in
Three.js am einfachsten **`3857`** (Standard-Slippy-Map-Schema, z/x/y), z.B.
```
https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swissimage/default/current/3857/{z}/{x}/{y}.jpeg
```
Für planimetrisch exakte 2D-Arbeit besser **`2056`**. CORS `*` (verifiziert). Quellen:
[WMTS docs](https://docs.geo.admin.ch/visualize-data/wmts.html) ·
[WMTS service](https://wmts.geo.admin.ch/) ·
[WMTS EPSG:2056 CodePen](https://codepen.io/geoadmin/pen/GZKEam).
> Es gibt zusätzlich klassisches **WMS** (`https://wms.geo.admin.ch/`, GetMap mit
> beliebiger BBox/Größe, ebenfalls EPSG:2056). Für ein einzelnes georeferenziertes
> Orthofoto-Rechteck unter dem Modell ist ein WMS-GetMap manchmal praktischer als
> WMTS-Kacheln zu stitchen. [WMS docs](https://docs.geo.admin.ch/visualize-data/wms.html)
### A.5 swissBUILDINGS3D — Nachbargebäude (3D)
Zwei Wege:
**(a) 3D-Tiles (Streaming, Cesium-Format)** — `glTF`/`tileset.json`, für große Gebiete:
```
https://3d.geo.admin.ch/<Layer>/<Version>/<Time>/tileset.json
```
Layer u.a. `ch.swisstopo.swissbuildings3d.3d`, `ch.swisstopo.swisstlm3d.3d`,
`ch.swisstopo.swissnames3d.3d`, `ch.swisstopo.vegetation.3d`. `Version` = `v1`, `Time`
optional (ISO `YYYYMMDD`, weglassen = aktuellste). CORS `*` (verifiziert auf
`tileset.json`). Direkt für CesiumJS gedacht; in **reinem Three.js** über
`@loaders.gl/3d-tiles` oder den `3DTilesRendererJS` (NASA-AMMOS/`three.js`-Community)
ladbar. [3D-Tiles docs](https://docs.geo.admin.ch/visualize-data/3d-tiles.html) ·
[Switzerland in 3D](https://www.swisstopo.admin.ch/en/switzerland-in-3d)
**(b) STAC-Tiles als CAD/Mesh-Datei (Download pro Tile)** — so macht es DOSSIER:
Collections `ch.swisstopo.swissbuildings3d_3_0` (neu; in Städten z.T. >700 MB Tiles)
und `ch.swisstopo.swissbuildings3d_2` (1-km-Tiles, ~50 MB, stabil). Assets in
`.dxf/.dwg/.obj/.ifc` (+ `.zip`), Varianten `solid`/`separated`. Für den **Import als
echte, editierbare Massen** ins eigene Modell ist der OBJ/IFC-Tile-Weg besser als
3D-Tiles (die sind read-only Visualisierung). swissBUILDINGS3D: >3 Mio Gebäude,
Lage-/Höhengenauigkeit 30–50 cm.
→ **Empfehlung:** Für „Nachbarschaft als Kontext anzeigen" (Phase 4 Start) **3D-Tiles
streamen** (kein Download, kein Parsing). Wenn der Nutzer Nachbargebäude als Geometrie
*braucht* (Verschattung, Abstand), **STAC-OBJ-Tile** laden und als Mesh importieren.
### A.6 Parzelle / Kataster (AV) — Identify-Service ⭐
**Verifiziert** — Parzellen-Polygon aus einer Koordinate, in LV95:
```
GET https://api3.geo.admin.ch/rest/services/api/MapServer/identify
?geometry=2600423,1199521&geometryType=esriGeometryPoint
&imageDisplay=100,100,96&mapExtent=2600323,1199421,2600523,1199621
&tolerance=2&layers=all:ch.kantone.cadastralwebmap-farbe
&returnGeometry=true&geometryFormat=geojson&sr=2056
→ {"results":[{"type":"Feature","bbox":[...],
"geometry":{"type":"Polygon","coordinates":[[ [2600377.1,1199523.7], ... ]]},
"attributes":{"number":"698","egris_egrid":"CH507635214670","ak":"BE", ...}}]}
```
- Parzellen-Layer: **`ch.kantone.cadastralwebmap-farbe`** (liefert `number`,
`egris_egrid` = EGRID, Kanton; mit `returnGeometry=true&geometryFormat=geojson` das
**Parzellen-Polygon in LV95-Metern** → direkt als Grundstücksgrenze importierbar).
- Gebäudeadressen: `ch.swisstopo.amtliches-gebaeudeadressverzeichnis`; Gebäude-/
Wohnungsregister `ch.bfs.gebaeude_wohnungs_register` (EGID).
- Pflichtparameter: `geometry`, `geometryType` (`esriGeometryPoint|...Polygon|...Envelope`),
`mapExtent`, `imageDisplay`, `tolerance`; `sr=2056`; max 50 Features/Request.
Quellen: [Identify features](https://docs.geo.admin.ch/access-data/identify-features.html) ·
[GeoAdmin REST – Identify/Find](https://geoadmin.readthedocs.io/en/latest/services/sdiservices.html) ·
Live-Antwort oben.
### A.7 Geocoding — SearchServer (Adresse → LV95)
**Verifiziert** (Adresse → Koordinate, deckt sich mit DOSSIERs `geocode()`):
```
GET https://api3.geo.admin.ch/rest/services/api/SearchServer
?searchText=Bundesplatz 3 Bern&type=locations&origins=address&sr=2056&limit=1
→ results[0].attrs: { label:"Bundesplatz 3 <b>3011 Bern</b>", lat:46.94677, lon:7.44419,
geom_st_box2d:"BOX(2600423.26 1199521.11, ...)", origin:"address", ... }
```
- `type=locations`, `origins` aus `{address, parcel, gg25, gazetteer, zipcode, district,
kantone}`, `sr=2056`. **Im LV95-Modus liefert die Geo-Admin-Konvention `y`=East,
`x`=North** (DOSSIER liest genau so: `e=attrs.y`, `n=attrs.x`). Labels enthalten
`<b>`-Tags (strippen).
- `type=featuresearch` + `features=<layer>` durchsucht Attribute (z.B. Parzellennummer).
Quelle: [Search](https://docs.geo.admin.ch/access-data/search.html) · Live-Antwort oben.
### A.8 Koordinatensystem LV95 / EPSG:2056 & Transformationen
Intern rechnet das BIM-Tool in **Metern** (ROADMAP-Konvention) und verschiebt den
Projekt-Ursprung nahe (0,0,0); LV95-Koordinaten sind ~2.6 Mio / 1.2 Mio Meter groß und
würden bei `float32` (Three.js) zu **Jitter** führen → **Origin-Shift Pflicht** (siehe B.3).
DOSSIER macht genau das (`origin_shift`/`shift_lv95`, typ. bbox-Center → 0/0/0).
Transformations-Optionen:
1. **`proj4` (npm `proj4@2.20.9`)** — universell, exakt. EPSG:2056-Definition:
```js
proj4.defs("EPSG:2056",
"+proj=somerc +lat_0=46.9524055555556 +lon_0=7.43958333333333 +k_0=1 "+
"+x_0=2600000 +y_0=1200000 +ellps=bessel "+
"+towgs84=674.374,15.056,405.346,0,0,0,0 +units=m +no_defs +type=crs");
const [e,n] = proj4("EPSG:4326","EPSG:2056",[lon,lat]); // WGS84→LV95
```
Genauigkeit mit dieser 3-Parameter-`towgs84` ~1 m (für Kontext-Import völlig
ausreichend). Types: `@types/proj4`. Quellen:
[epsg.io/2056](https://epsg.io/2056) · [proj4js](https://github.com/proj4js/proj4js).
2. **Näherungsformeln (CH1903→WGS84, swisstopo)** — DOSSIERs Ansatz, ~1 m genau,
**0 Dependencies** (zwei kleine Funktionen `lv95_to_wgs84`/`wgs84_to_lv95`).
1:1 nach TS portierbar; gut, wenn man `proj4` nicht ziehen will. Reicht, weil
STAC-Queries ohnehin nur eine grobe WGS84-Bbox brauchen und alle *Daten* schon in
LV95 kommen.
3. **swisstopo REFRAME Web-API** — cm-genaue offizielle Umrechnung (LV95↔WGS84,
LN02↔Bessel). Nur nötig, wenn Vermessungs-Genauigkeit verlangt wird. REST, online.
[REFRAME Web](https://www.swisstopo.admin.ch/en/rest-api-geoservices-reframe-web)
**Empfehlung:** `proj4` mit fester EPSG:2056-Def (eine Abhängigkeit, exakt genug,
wartungsarm) — oder, wenn Dependency-Geiz, DOSSIERs Formeln portieren. REFRAME nur bei
Bedarf nachrüsten.
### A.9 Endpoint-Übersicht (Spickzettel)
| Zweck | Endpoint | Frei/CORS | Format |
|---|---|---|---|
| Punkt-Höhe | `api3…/rest/services/height` | ✅ ✅ | JSON |
| Höhenprofil (Schnitt) | `api3…/rest/services/profile.json` | ✅ ✅ | JSON/CSV |
| Gelände-Mesh (COG) | STAC `…/swissalti3d/items` → `.tif` (COG, 2056) | ✅ ✅ Range | GeoTIFF |
| Gelände (ASCII) | STAC `…/swissalti3d` → `.xyz.zip` | ✅ ✅ | XYZ in ZIP |
| Orthofoto/Karte | `wmts…/1.0.0/<layer>/…/{z}/{x}/{y}.jpeg` | ✅ ✅ | Kacheln |
| 3D-Nachbargebäude (stream) | `3d…/ch.swisstopo.swissbuildings3d.3d/v1/tileset.json` | ✅ ✅ | 3D-Tiles/glTF |
| 3D-Gebäude (Datei) | STAC `…/swissbuildings3d_2` → `.obj/.ifc` | ✅ ✅ | OBJ/IFC |
| Parzelle/Kataster | `api3…/MapServer/identify` `layers=all:ch.kantone.cadastralwebmap-farbe` | ✅ ✅ | GeoJSON |
| Geocoding | `api3…/SearchServer?type=locations` | ✅ ✅ | JSON |
---
## Teil B — Standort-Kontext importieren (Terrain + Parzelle + Nachbargebäude)
So bekommt ein Projekt seinen realen Kontext „auf Knopfdruck". Der Ablauf folgt
DOSSIER (`rhino/swisstopo.py`), übersetzt auf Browser-Libs.
### B.1 Pipeline (End-to-End)
```
Adresse/Parzelle ──SearchServer──▶ Zentrum (E,N) in LV95
│
├─ radius r ──▶ bbox_LV95 (E±r, N±r) ──proj4/Formeln──▶ bbox_WGS84
│
├─[Parzelle] identify(cadastralwebmap, point) ─▶ Polygon (LV95) ─▶ Grundstücksgrenze (Ebene 01 Vermessung)
│
├─[Gelände] STAC(swissalti3d, bbox_WGS84) ─▶ COG .tif(2056)
│ └─ geotiff.js readRasters(window) ─▶ Höhen-Grid (E,N,Z, m)
│ └─ Three.js BufferGeometry (Grid→Mesh) [optional: TIN, Höhenlinien, Volumen]
│
├─[Orthofoto] WMTS swissimage ─▶ Textur auf Gelände-Mesh ODER georef. Plane
│
└─[Nachbarn] 3D-Tiles streamen (Anzeige) ODER STAC swissbuildings3d_2 .obj ─▶ Mesh-Import
(Weltweit/ausserhalb CH: OSM-Overpass als Fallback, siehe B.5)
──▶ alle Geometrien um origin_shift (bbox-Center→0/0/0) verschoben, Z aus ALTI3D
```
### B.2 Gelände-Mesh aus swissALTI3D (Kern, Phase 4)
**Empfohlener Browser-Weg (COG + geotiff.js):**
1. STAC-Query mit `bbox_WGS84` → Liste der überlappenden Tiles; pro Tile das
gewünschte COG-Asset (`_2_2056_` für 2 m, `_0.5_2056_` für 0.5 m).
2. `geotiff.js`: `const tiff = await fromUrl(href)` → COG; `image.readRasters({window})`
liest **nur den Ausschnitt** (Range-Requests, da CORS+Range bestätigt). Ergebnis ist
ein reguläres Z-Raster mit bekanntem Origin/PixelScale (LV95-Meter) aus den
GeoKeys/`image.getOrigin()`/`image.getResolution()`.
3. Raster → **`THREE.BufferGeometry`**: ein Vertex pro Rasterpunkt
`(E−shiftE, N−shiftN, Z−shiftZ)`, Faces als zwei Dreiecke pro Zelle (DOSSIER:
`mesh_from_grid`, gleiche Logik), `computeVertexNormals()`. Bei 0.5 m wird das Mesh
groß → bei Bedarf raumräumlich sub-samplen (DOSSIER macht ganzzahliges Sub-Sampling
auf dem **globalen** LV95-Raster, damit Nachbar-Tiles nahtlos zusammenpassen).
4. Mehrere Tiles: erst zu **einem** Grid mergen (gemeinsamer Origin/Step), dann meshen —
sonst entstehen Nähte (DOSSIER: `merge_grids`).
**Alternativweg (XYZ, exakt wie DOSSIER):** `.xyz.zip` laden → mit **`fflate`**
(npm, schnellster Inflate im Browser) entpacken → ASCII `E N Z` parsen → gleiches Grid.
Robust, aber überträgt mehr Bytes als der COG-Range-Weg. Für den Port empfehle ich
**COG primär, XYZ als Fallback**.
Bibliotheken: `geotiff@3.0.5`, `fflate` (XYZ-Variante), `three@0.185.0`.
[geotiff.js](https://github.com/geotiffjs/geotiff.js/) ·
[3D-Terrain aus GeoTIFF mit Three.js](https://spatial-dev.guru/2024/11/30/creating-3d-terrain-maps-from-geotiff-files-with-three-js/).
**Ableitungen wie in DOSSIER** (alle aus dem Grid, in Phase 4 portierbar):
- **Höhenlinien** (Marching-Squares auf dem Grid; npm `d3-contour` oder
`marchingsquares`) — für 2D-Plan.
- **TIN / Patch** (Delaunay aus den Punkten; npm `delaunator`) — alternatives Mesh.
- **Geschlossenes Gelände-Volumen** (Boden N m unter tiefstem Punkt) → gefüllte
Querschnitte beim Schnitt-Cut (DOSSIER `terrainVolume`/`terrainVolumeDepth`).
### B.3 Origin-Shift (Pflicht)
LV95-Koordinaten (~2.6e6) sprengen `float32`. Beim Import einmal
`shift = (eCenter, nCenter, zRef)` festlegen, **alle** Geometrien `−shift` rechnen, und
`shift` am Projekt persistieren (für Re-Import / Geo-Referenz / Norden). DOSSIER:
`origin_shift`/`shift_lv95`, plus „Auto-Zoom auf Import" (ROADMAP §11). Damit bleibt das
Modell metergenau und der Rückweg in echte LV95-Koordinaten (Export, weitere
swisstopo-Abfragen) ist `+ shift`.
### B.4 Parzelle + Orthofoto
- **Parzelle:** `identify(...cadastralwebmap..., returnGeometry=true, geometryFormat=geojson)`
→ Polygon (LV95) → `−shift` → als geschlossene Polylinie auf Ebene **`01 Vermessung`**.
Attribute `number`/`egris_egrid` am Objekt/Projekt speichern.
- **Orthofoto:** WMTS `swissimage`-Kacheln über die Modell-Bbox stitchen → eine Textur,
als Material auf das Gelände-Mesh **oder** auf eine georeferenzierte Plane (DOSSIER:
`add_ortho_plane`, mit UV-Shift gegen Tile-Nähte). Für 3D ist `3857` einfacher, für
exakte 2D-Lage `2056`.
### B.5 OSM-Overpass als weltweiter Fallback (Phase 4)
Ausserhalb der Schweiz (oder wenn nur 2D-Footprints/Straßen reichen): DOSSIER hat einen
**Overpass-Importer** (`https://overpass-api.de/api/interpreter`, POST) mit 7 Kategorien
(Straßen/Gebäude/Wasser/Wasserläufe/Grün/Wege). Liefert OSM-Ways → Polylinien. Im
Browser identisch nutzbar (`fetch` POST). Overpass koordiniert in WGS84 → mit
`proj4`→LV95→`−shift`. Hinweis: Overpass-CORS ist beim Haupt-Server meist offen, kann
aber je nach Mirror variieren; ggf. anderen Mirror wählen.
[Overpass API](https://overpass-api.de/).
### B.6 Was sich von DOSSIER **nicht** 1:1 portieren lässt
- **Rhino-`_-Import`** für DXF/DWG/OBJ und **`_-MeshPatch`/Delaunay**-Commands gibt es im
Browser nicht → ersetzen durch JS-Parser/Algorithmen (OBJ: `three`-`OBJLoader`;
Delaunay: `delaunator`; Contours: `d3-contour`).
- **Filesystem-Cache neben der `.3dm`** → Browser: **IndexedDB**-Cache (Cache-API für
Kacheln). Passt zur ROADMAP-Phase 5 (IndexedDB-Persistenz).
- IFC-Import von swissBUILDINGS3D 3.0 → über **web-ifc** (ohnehin im Stack, Phase 4).
---
## Teil C — SIA 416 (Flächen/Volumen) & SIA 421 + DOSSIER-Logik
### C.1 SIA 416 — Flächen- und Volumenhierarchie
**SIA 416:2003** „Flächen und Volumen von Gebäuden" ist die in der CH gültige Norm und
Berechnungsbasis für Kostenplanung/Flächennachweise. Sie kennt vier Bereiche:
**GSF** (Grundstück), **GF** (Geschossflächen), **AGF** (Aussengeschossflächen), **GV**
(Volumen). Maßgeblich ist die effektive Geometrie (keine fiktiven Zuschläge mehr).
Quellen: [SIA 416 Übersicht (siworks/DBV)](https://diebauherrenvertretung.ch/sia146/) ·
[SIA-Shop 416/2003](https://shop.sia.ch/normenwerk/architekt/sia%20416/dfi/D/Product) ·
[Flächenkennzahlen SIA 416 (Ginesta, PDF)](https://www.ginesta.ch/resources/public/lava3/media/kcfinder/files/Fl%C3%A4chenkennzahlen%20SIA%20416%20Norm.pdf).
**Hierarchie & Formeln (verifiziert):**
```
GSF Grundstücksfläche
GF Geschossfläche = KF + NGF
├─ KF Konstruktionsfläche (Wände/Stützen; tragend KFT + nicht tragend KFN)
└─ NGF Nettogeschossfläche = NF + VF + FF
├─ NF Nutzfläche = HNF + NNF
│ ├─ HNF Hauptnutzfläche (zweckbestimmte Hauptnutzung: Wohnen, Büro …)
│ └─ NNF Nebennutzfläche (Lager, Bad/WC, Abstell-, Nebenräume)
├─ VF Verkehrsfläche (Erschließung: Flure, Treppen, Lifte)
└─ FF Funktionsfläche (Gebäudetechnik: Heizung, Lüftung, Technik)
AGF Aussengeschossfläche (Balkone, Terrassen, gedeckte Aussenflächen)
GV Gebäudevolumen [m³]
```
| Abk. | Deutsch | Inhalt (Kurz) |
|---|---|---|
| GSF | Grundstücksfläche | Parzellenfläche (aus Kataster, Teil A.6) |
| GF | Geschossfläche | allseits umschlossene + überdeckte Grundrissflächen, geschossweise |
| KF | Konstruktionsfläche | Bauteile (Wände/Stützen), nicht begehbar; KFT tragend / KFN nicht tragend |
| NGF | Nettogeschossfläche | begehbare Fläche innerhalb der Umschließung = NF+VF+FF |
| NF | Nutzfläche | tatsächlich nutzbar = HNF+NNF |
| HNF | Hauptnutzfläche | zweckbestimmte Hauptnutzung |
| NNF | Nebennutzfläche | dienende Nebenräume (Lager, Bad, WC) |
| VF | Verkehrsfläche | horizontale/vertikale Erschließung |
| FF | Funktionsfläche | Gebäudetechnik |
| AGF | Aussengeschossfläche | Balkone/Terrassen u.ä. |
| GV | Gebäudevolumen | umbauter Raum [m³] |
> **Versionshinweis:** Es kursiert eine Revision **SIA 416:2017** mit präzisierten
> Begriffen, in der Praxis wird aber breit weiter **416:2003** referenziert. Für unser
> Tool reicht die Klassifikation **HNF/NNF/VF/FF/GF/AGF + Bilanz**; die genaue Auflage
> nur als Label dokumentieren. Verbindliche Definitionen stehen im kostenpflichtigen
> Normtext (SIA-Shop) — die hier zitierten freien Quellen stimmen in der Struktur überein.
### C.2 SIA 421 — Flächengliederung für Bewirtschaftung/Vermietung
**SIA 421:2006** „Flächengliederung und Mengenangaben" (Korrigenda C1:2014) baut auf der
SIA-416-Systematik auf und **gliedert Flächen für Immobilien-Bewirtschaftung und
Vermietung** (Mietflächen, Nutzungseinheiten, Zuordnung von VF/FF zu Mietern). Relevant,
sobald wir **Mietflächen-/Bewirtschaftungs-Auswertungen** wollen (über den reinen
Architektur-Nachweis hinaus). Für den ersten Wurf **nicht zwingend** — SIA 416 reicht
für Flächennachweis und Raumschema. SIA 421 ist die natürliche Erweiterung, wenn
Property-Management-Features kommen. Quellen:
[SIA 421:2006 (PDF Inhalt)](https://shop.sia.ch/d475e3f9-10c9-48aa-9be8-657784ea519b/F/DownloadAnhang) ·
[Korrigenda C1:2014 (PDF)](https://www.sia.ch/fileadmin/content/download/sia-norm/korrigenda_sn/421-C1_2014_d.pdf).
### C.3 Wie DOSSIER SIA macht (Vorlage für den Port)
Quellcode: `rhino/elemente.py` (Räume) + `rhino/elemente_uebersicht.py` (Bilanz/CSV).
Kernpunkte, die wir 1:1 übernehmen:
**Datenmodell pro Raum.** Ein Raum ist eine **geschlossene Outline-Curve** + ein
**Text-Stempel**. Klassifikation über das Feld `dossier_raum_sia` mit Werten
`{"", hnf, nnf, vf, ff, gf, agf}` (`_RAUM_SIA_KINDS`). Weitere Felder: `name`, `nummer`,
`funktion` (`wohnen|schlafen|bad|kueche|essen|flur|…`), `personen` (für
Personenbelegung/Brandschutz), Rundung, Stempel-Layout.
**Flächen-/Umfangsberechnung** (`_raum_amp`): Fläche aus `AreaMassProperties` (≙ in JS
**Shoelace-Formel** über das Polygon), Umfang = Kurvenlänge, plus Zentroid für den
Stempel. Im Browser: Polygon-Fläche selbst rechnen (Shoelace), kein Mesh nötig — exakt
und schnell. Rundungsstufen (`_format_area`): `exakt|0.01|0.1|0.5|1` (z.B. `0.5` =
`round(a*2)/2`).
**SIA-Bilanz** (`compute_sia_bilanz`, `scope = total | geschoss:<id>`): summiert
Raumflächen je Klasse, dann:
```
NF = HNF + NNF
NGF = NF + VF + FF (GF/AGF separat aggregiert, zählen nicht in NGF)
```
(genau die Formeln aus C.1). Ergebnis je Geschoss + Total.
**Farb-/Darstellungs-Konvention** (`_SIA_COLORS_HEX`, Pastell): HNF rot `#e8a8a8`,
NNF orange `#e8c498`, VF gelb `#e8d878`, FF hellblau `#a8c8e0`, GF grau `#d0d0d0`,
AGF hellgrün `#c0d8c0`. Umgesetzt als **regelbasierte Overrides** (`_build_sia_preset_rules`,
Preset „SIA-Raeume"): Bedingung `user_string == code` → Outline-Farbe + Solid-Hatch.
→ Passt 1:1 zur geplanten **Overrides-Engine** (ROADMAP §2c/§11).
**Export** (`_cmd_export_raeume`, `_export_bilanz`): CSV, **Semikolon + UTF-8-BOM**
(CH/DE-Excel), Dezimal-Komma. Raumliste: Nummer; Name; Geschoss; Funktion; SIA; Fläche;
Fläche gerundet; Umfang. Bilanz: eine Spalte je Geschoss + Total, Zeilen je Kategorie.
→ Im Browser: Blob + Download (kein SaveFileDialog), gleiche CSV-Struktur. Optional
direkt `.xlsx` via `sheetjs`/`exceljs`.
**Layer-Routing:** GF→`61_GF`, AGF→`62_AGF`, Rest→`60_RAEUME` (`_layer_path_for_raum_sia`)
— damit Geschossflächen-Outlines getrennt schalt-/exportierbar sind. Übersetzt sich auf
unsere Ebenen-Codes (`60 Räume`).
**Was wir im Port besser/anders machen:**
- Fläche per **Shoelace** statt Rhino-Mass-Props (0 Deps).
- Bilanz **reaktiv** aus dem semantischen Modell (Zustand-Store) statt Doc-Scan.
- **Space = Slab-/Raum-Polygon mit `siaClass`** im Datenmodell (ROADMAP:
`Space { boundary, name }`), Bilanz als abgeleitete Sicht.
---
## Teil D — Umsetzungsplan (Endpunkte · Libs · Phase)
Reihenfolge orientiert sich an der ROADMAP (SIA = Phase 2, Geo = Phase 4) und an „größter
Nutzen zuerst, geringste Abhängigkeit zuerst".
### Phase 2 — SIA-Räume (kein Netz, reine Logik) ⭐
**Endpunkte:** keine. **Libs:** keine (Shoelace selbst), optional `exceljs`/`sheetjs` für
.xlsx.
1. `Space`-Modell: `{ boundary[], geschossId, name, nummer, funktion, siaClass∈{hnf,nnf,vf,ff,gf,agf}, personen }`.
2. `computeArea` (Shoelace) + Umfang; Rundungsstufen (`exakt|0.01|0.1|0.5|1`) wie DOSSIER `_format_area`.
3. `computeSiaBilanz(scope)` → `{hnf,nnf,nf,vf,ff,ngf,gf,agf,count,personen}` mit
`nf=hnf+nnf`, `ngf=nf+vf+ff`.
4. SIA-Farbpalette + Stil-Override (Outline-Farbe/Solid-Fill) in der Overrides-Engine.
5. Raumstempel-Renderer (Felder-Layout) + **CSV-Export** (Semikolon, UTF-8-BOM, Komma)
für Raumliste **und** Bilanz.
*Ergebnis:* SIA-416-Flächennachweis + Raumschema, Excel-kompatibel — vor jeder Geo-Arbeit nutzbar.
### Phase 4a — Geo-Grundlage: Koordinaten + Standortabfrage
**Endpunkte:** `SearchServer` (Geocoding), `height` (Punkt-Z), `identify`
(Parzelle, `cadastralwebmap-farbe`). **Libs:** `proj4@2.20.9` (+ `@types/proj4`).
1. `proj4`-EPSG:2056-Def + Helfer `lv95↔wgs84`, `bboxLv95→bboxWgs84` (DOSSIER-Logik).
2. **Origin-Shift**-Mechanik + Persistenz am Projekt (`shift = bbox-Center`), Auto-Zoom.
3. Adresssuche → Zentrum; „Gelände-Höhe holen" (height); „Parzelle holen" → Polygon auf
Ebene `01 Vermessung` (+ EGRID/Nummer am Projekt).
*Ergebnis:* Projekt ist georeferenziert; Parzelle + Adresse + Geländehöhe vorhanden.
### Phase 4b — Gelände-Mesh + Orthofoto
**Endpunkte:** STAC `swissalti3d` (COG `.tif`, 2056) + `profile.json`; WMTS `swissimage`.
**Libs:** `geotiff@3.0.5`, `three@0.185.0`, optional `fflate` (XYZ-Fallback),
`d3-contour`/`marchingsquares` (Höhenlinien), `delaunator` (TIN).
1. STAC-Query (bbox) → COG-Tiles; `geotiff.js` Range-Read → Grid; Tiles mergen.
2. Grid → `THREE.BufferGeometry` (DOSSIER `mesh_from_grid`/`merge_grids`); Sub-Sampling
auf globalem LV95-Raster; Normalen.
3. Optional: Höhenlinien (2D-Plan), TIN, geschlossenes Volumen (Schnitt-Füllung).
4. WMTS-`swissimage`-Kacheln → Textur auf Mesh/Plane (DOSSIER `add_ortho_plane`).
5. Geländeschnitt im 2D-Plan via `profile.json` entlang der Schnittlinie.
*Ergebnis:* echtes Gelände mit Orthofoto unter dem Gebäude; Geländeschnitte.
### Phase 4c — Nachbargebäude + weltweiter Fallback
**Endpunkte:** 3D-Tiles `ch.swisstopo.swissbuildings3d.3d/v1/tileset.json` (Anzeige)
**oder** STAC `swissbuildings3d_2` `.obj/.ifc` (Import); OSM `overpass-api.de`.
**Libs:** `@loaders.gl/3d-tiles@4.4.3` **oder** `3DTilesRendererJS`; `three`-`OBJLoader`;
`web-ifc` (für 3.0-IFC); `proj4`.
1. Kontext-Anzeige: 3D-Tiles in den Three-Scenegraph streamen (kein Download).
2. Bedarf an echter Geometrie (Verschattung/Abstand): STAC-OBJ-Tile → Mesh-Import → `−shift`.
3. Ausserhalb CH / nur 2D: Overpass-POST → Ways → Polylinien (Ebene `70 OSM`).
*Ergebnis:* Nachbarschaftskontext (CH 3D, weltweit OSM).
### Querschnitt (alle Geo-Phasen)
- **Caching:** IndexedDB für STAC-Antworten + COG-Bytes + Kacheln (ersetzt DOSSIERs
Disk-Cache); Cache-Schlüssel = Tile-ID/URL.
- **Defensive HTTP:** Timeouts, Retry/Backoff, 402/429 abfangen, Größen-Limit pro Tile
(DOSSIER: 200-MB-Guard).
- **Attribution:** „© swisstopo" sichtbar einblenden, „© OpenStreetMap-Mitwirkende" bei OSM.
- **Worker:** GeoTIFF-Parsing + Mesh-Bau im Web-Worker (Comlink, ROADMAP-Stack), UI bleibt flüssig.
### Empfohlener Library-Satz (npm, aktuell)
`proj4@2.20.9` · `geotiff@3.0.5` · `three@0.185.0` · `fflate` (XYZ) ·
`@loaders.gl/3d-tiles@4.4.3` *oder* `3DTilesRendererJS` · `delaunator` ·
`d3-contour` · `web-ifc` (im Stack) · `exceljs`/`sheetjs` (optional, .xlsx).
---
## Quellen
- GeoAdmin REST (height/profile/identify/find/search): https://geoadmin.readthedocs.io/en/latest/services/sdiservices.html
- GeoAdmin Tech-Docs (Hub): https://docs.geo.admin.ch/
- Identify Features: https://docs.geo.admin.ch/access-data/identify-features.html
- Search: https://docs.geo.admin.ch/access-data/search.html
- WMTS: https://docs.geo.admin.ch/visualize-data/wmts.html · https://wmts.geo.admin.ch/
- WMS: https://docs.geo.admin.ch/visualize-data/wms.html
- 3D-Tiles: https://docs.geo.admin.ch/visualize-data/3d-tiles.html
- swissALTI3D: https://www.swisstopo.admin.ch/en/height-model-swissalti3d
- Switzerland in 3D / swissBUILDINGS3D: https://www.swisstopo.admin.ch/en/switzerland-in-3d
- Terms of use (FSDI / OGD): https://www.geo.admin.ch/en/general-terms-of-use-fsdi
- REFRAME Web-API: https://www.swisstopo.admin.ch/en/rest-api-geoservices-reframe-web
- EPSG:2056 Definition: https://epsg.io/2056
- proj4js: https://github.com/proj4js/proj4js
- geotiff.js: https://github.com/geotiffjs/geotiff.js/
- Three.js-Terrain aus GeoTIFF: https://spatial-dev.guru/2024/11/30/creating-3d-terrain-maps-from-geotiff-files-with-three-js/
- WMTS EPSG:2056 Beispiel: https://codepen.io/geoadmin/pen/GZKEam
- Overpass API: https://overpass-api.de/
- SIA 416 (Übersicht): https://diebauherrenvertretung.ch/sia146/ · https://shop.sia.ch/normenwerk/architekt/sia%20416/dfi/D/Product
- SIA 416 Flächenkennzahlen (PDF): https://www.ginesta.ch/resources/public/lava3/media/kcfinder/files/Fl%C3%A4chenkennzahlen%20SIA%20416%20Norm.pdf
- SIA 421:2006 (PDF) + Korrigenda C1:2014: https://shop.sia.ch/d475e3f9-10c9-48aa-9be8-657784ea519b/F/DownloadAnhang · https://www.sia.ch/fileadmin/content/download/sia-norm/korrigenda_sn/421-C1_2014_d.pdf
- DOSSIER-Quellcode (Vorlage): `rhino/swisstopo.py`, `rhino/osm.py`, `rhino/elemente.py` (Räume/SIA), `rhino/elemente_uebersicht.py` (Bilanz/CSV)
+197
View File
@@ -0,0 +1,197 @@
# Technologie-Auswahl: Browser-basiertes BIM-Tool (DOSSIER-Port)
> **Kontext:** Standalone, browser-basiertes BIM-Werkzeug (React + TypeScript + Three.js) als Port des DOSSIER Rhino-Plugins. Keine Server-Abhaengigkeit gewuenscht (alles client-side). Recherchestand: Juni 2026.
>
> **Leitprinzip:** Wo immer moeglich auf einem echten B-Rep-Geometriekernel (OCCT) aufbauen, weil ein BIM-Werkzeug exakte 2D-Ableitungen (Schnitte, verdeckte Kanten, Bemassung) braucht — das ist mit reinen Dreiecksnetzen nicht sauber loesbar. Mesh-Booleans (Manifold) als schnelle Ergaenzung fuer Importgeometrie und Vorschau.
---
## 1. Geometriekernel im Browser (Solids + Booleans)
Das ist das schwierigste und zugleich wichtigste Problem. Drei ernsthafte Optionen, die alle im Browser (WASM) laufen.
### Optionen
**A) opencascade.js (OCCT als WASM)**
Port des vollstaendigen OpenCASCADE-Kernels (OCCT) nach WebAssembly via Emscripten. Voller B-Rep-Kernel: NURBS-Flaechen, exakte boolesche Operationen, Fillets/Chamfers, STEP/IGES-Import/-Export, Meshing. TypeScript-Bindings vorhanden. Die neueren Versionen (V3-Linie) zielen explizit auf moderne Bundler.
- Repo: <https://github.com/donalffons/opencascade.js/>
- Doku: <https://opencascade-js.vercel.app/>
- npm: <https://www.npmjs.com/package/opencascade.js>
- **Lizenz:** OCCT steht unter **LGPL-2.1 mit OCCT-Exception** (seit 6.7.0). Kommerzielle Nutzung ohne Lizenzgebuehren/Royalties erlaubt, sofern man (a) sichtbar darauf hinweist, dass die Software OCCT nutzt, und (b) eine Kopie der OCCT-Lizenz mitliefert. Die Exception entschaerft das statische-Linking-Problem fuer Header/Templates. Quellen: <https://dev.opencascade.org/resources/licensing>, <https://spdx.org/licenses/OCCT-exception-1.0.html>
- **Trade-offs:** Sehr grosse WASM-Binaries (zweistelliger MB-Bereich je nach Custom-Build), steile Lern- und API-Kurve (rohe OCCT-C++-API durchgereicht), Build-Pflege aufwendig.
**B) replicad (Abstraktion ueber opencascade.js)** — *empfohlene Basis*
replicad ist eine schlanke, idiomatische TypeScript-Schicht ueber opencascade.js. Es liefert genau die High-Level-Bausteine, die ein BIM-Tool braucht: Sketches/Blueprints, Extrude/Revolve/Loft, Booleans, Fillet/Chamfer — und entscheidend: **HLR-Projektionen** (`drawProjection`) und 2D-Drawings mit SVG-Export. Laeuft per Design im **Web Worker** und gibt Dreiecksnetze an den Main-Thread fuer Three.js zurueck.
- Doku/Library-Guide: <https://replicad.xyz/docs/use-as-a-library/>
- API: <https://replicad.xyz/docs/api/>
- **Lizenz:** **MIT** (eigene Schicht) — der OCCT/LGPL-Hinweis gilt weiterhin fuer das eingebettete WASM. Repo: <https://github.com/sgenoud/replicad>
- **Trade-offs:** Erbt OCCT-WASM-Groesse und -Robustheitsgrenzen; kleineres Team/Bus-Faktor als OCCT selbst. Man kann jederzeit „unter die Haube" auf rohes opencascade.js durchgreifen, wenn die Abstraktion nicht ausreicht.
**C) Manifold (manifold-3d, WASM)** — *empfohlen als schnelle Mesh-Ergaenzung*
Geometrie-Bibliothek fuer topologisch robuste **Dreiecksnetze**. Bietet den (laut Autor) ersten garantiert mannigfaltigen Mesh-Boolean-Algorithmus — extrem schnell und robust gegen Randfaelle. Stark parallelisiert.
- Repo: <https://github.com/elalish/manifold>
- npm: <https://www.npmjs.com/package/manifold-3d> (aktuell v3.5.x, Juni 2026)
- **Lizenz:** **Apache-2.0** (sehr permissiv, ideal). Bestaetigt: <https://github.com/elalish/manifold>
- **Trade-offs:** **Nur Meshes, kein B-Rep** — keine exakten NURBS-Flaechen, keine echten Fillets auf Krümmungen, kein STEP. Fuer ein BIM-Tool, das exakte Plaene/Schnitte ableiten will, alleine nicht ausreichend, aber unschlagbar fuer schnelle Booleans auf importierter Mesh-Geometrie und Live-Vorschau. **Wichtig:** unterstuetzt `slice(z)` und `project()` (siehe Abschnitt 3).
**D) three-bvh-csg (nur erwähnt, nicht empfohlen als Kernel)**
Sehr schnelle CSG direkt auf Three.js-BufferGeometry (auf three-mesh-bvh). >100x schneller als BSP-basierte Three.js-CSG-Libs. Aber: erklaert selbst, dass Resultate „aufgrund numerischer Praezision nicht garantiert 2-mannigfaltig" sind und verweist fuer CAD-Robustheit ausdruecklich auf Manifold.
- Repo: <https://github.com/gkjohnson/three-bvh-csg>
- Forum: <https://discourse.threejs.org/t/three-bvh-csg-a-library-for-performing-fast-csg-operations/42713>
- **Trade-offs:** Gut fuer Live-Vorschau/visuelles Schneiden, ungeeignet als verlaesslicher Modellierkernel.
### Empfehlung (Kernel)
**replicad (= opencascade.js) als primaerer B-Rep-Kernel im Web Worker; Manifold als schneller Mesh-Boolean-Pfad fuer Import-/Vorschaugeometrie.** Diese Zweiteilung deckt sowohl „exakte BIM-Geometrie + 2D-Ableitung" (OCCT) als auch „schnell + robust auf beliebigen Meshes" (Manifold) ab. three-bvh-csg nur, falls man interaktives Echtzeit-Schneiden visuell braucht.
---
## 2. 2D-Ableitung aus 3D: verdeckte Kanten (HLR) + Schnittgenerierung
Kernfrage des DOSSIER-Ports: aus 3D-Solids saubere 2D-Zeichnungen (sichtbare/verdeckte Kanten, Schnitte) erzeugen — im Browser.
### Optionen
**A) OCC HLRBRep via replicad `drawProjection` — empfohlen**
OCCT enthaelt zwei HLR-Algorithmen: `HLRBRep_Algo` (exakt, auf der echten B-Rep) und `HLRBRep_PolyAlgo` (auf polyederisierter Naeherung, schneller, aber polygonal). Quellen: <https://dev.opencascade.org/doc/refman/html/class_h_l_r_b_rep.html>, <https://dev.opencascade.org/doc/occt-7.7.0/refman/html/class_h_l_r_b_rep___poly_algo.html>
replicad macht genau das im Browser nutzbar: `drawProjection(shape, camera)` liefert ein Objekt mit **`{ visible, hidden }`** — getrennte sichtbare und verdeckte Kantenzuege, die man unterschiedlich stylen kann (z.B. verdeckt = gestrichelt). Konkretes Beispiel aus der Doku:
```js
const { drawProjection, ProjectionCamera } = replicad;
const camera = new ProjectionCamera(corner).lookAt(center);
const { visible, hidden } = drawProjection(shape, camera);
// visible/hidden sind Drawings -> .toSVG()
```
- Beispiel: <https://replicad.xyz/docs/examples/projections/>
- Verwandte API: `makeProjectedEdges`, `ProjectionCamera`, `Drawing.toSVG()` / `toSVGPaths()` (<https://replicad.xyz/docs/api/classes/Drawing/>)
- **Das ist der entscheidende Grund, replicad/OCCT zu nehmen:** exakte verdeckte-Kanten-Berechnung auf echtem B-Rep ist mit Mesh-Tools nicht serioes machbar.
- **Trade-offs:** HLR ist rechenintensiv (deshalb Worker + Caching pro Ansicht/Kamera); `HLRBRep_Algo` exakt aber langsam, `PolyAlgo` schneller aber genaehert.
**B) Schnitte (Sections) via OCCT**
Echte Schnitte ueber Schnitt mit einer Ebene/Halbraum (`BRepAlgoAPI_Section` bzw. Boolean mit Schnittkoerper) ergeben exakte Schnittkanten als B-Rep-Edges, die wiederum nach SVG/DXF gehen. In replicad ueber Booleans + Projektion abbildbar.
**C) Manifold `slice()` / `project()` — schnelle Mesh-Variante**
`Manifold.slice(z)` gibt den Querschnitt parallel zur X-Y-Ebene auf Hoehe `z` als `CrossSection` (2D-Polygone, intern Clipper2); `Manifold.project()` die projizierte Aussenkontur. Quellen: <https://manifoldcad.org/docs/jsapi/>, <https://manifoldcad.org/docs/html/classmanifold_1_1_cross_section.html>
- **Trade-offs:** liefert **keine** verdeckte/sichtbare-Kanten-Trennung und keine Innenkanten-Semantik wie HLR — nur die geometrische Schnitt-/Projektionskontur des Meshes. Gut fuer Plan-Schnittkonturen (siehe Abschnitt 3), ungenuegend fuer vollwertige Ansichts-Zeichnungen mit verdeckten Kanten.
### Empfehlung (2D-Ableitung)
**HLR und Ansichts-Zeichnungen ueber replicad `drawProjection` (OCC HLRBRep) im Worker, mit Caching pro Kamera/Ansicht. Echte Schnitte ueber OCCT-Section-Boolean. Manifold `slice()` als schneller Pfad nur fuer reine Schnittkonturen (z.B. Plan-Cut auf Mesh-Importen).**
---
## 3. Plan-Ansicht: Clipping bei `cutHeight`
Verhalten: horizontaler Schnitt auf einstellbarer Hoehe (Grundriss), darüber Abschneiden, Schnittflaechen markieren.
### Optionen
**A) Three.js Clipping Planes (`clippingPlanes` / `localClippingEnabled`)**
Three.js bietet globale (`renderer.clippingPlanes`) und material-lokale (`material.clippingPlanes`) Schnittebenen; `renderer.localClippingEnabled` ist standardmaessig aus (Null-Kosten, bis aktiviert). Quellen: <https://threejs.org/docs/#api/en/materials/Material.clippingPlanes>, <https://threejs.org/examples/webgl_clipping.html>
- **Pro:** GPU-seitig, dynamisch (Slider auf `cutHeight` = `plane.constant` aendern, kein Geometrie-Rebuild), sehr fluessig.
- **Contra:** Clipping schneidet nur visuell — es entstehen **offene** Querschnitte (keine Deckflaeche). Fuer „Schnittflaeche fuellen/markieren" braucht man entweder einen Stencil-Cap-Trick oder eine echte Schnittkontur (Abschnitt 2). `clipIntersection = true` kann Material-Reinitialisierung pro Frame und FPS-Einbrueche verursachen — moeglichst vermeiden. Quelle: <https://github.com/mrdoob/three.js/issues/18675>
**B) Object Culling / Sichtbarkeit nach Hoehe**
Elemente oberhalb `cutHeight` per Bounding-Box/Etagen-Metadaten ausblenden (`object.visible = false`).
- **Pro:** trivial, keine Shader-Kosten, nutzt BIM-Etagensemantik.
- **Contra:** grobkoernig (ganze Objekte, kein praeziser Schnitt mitten durch ein Bauteil).
**C) Echte Schnittgeometrie pro Etage (OCCT/Manifold) fuer den 2D-Plan**
Fuer die exportierbare 2D-Grundriss-Zeichnung den echten Schnitt auf `cutHeight` rechnen (OCCT-Section bzw. `Manifold.slice(z)`), Schnittflaechen schraffieren (Abschnitt 6).
### Empfehlung (Plan-Clipping)
**Hybrid:** Im 3D-Viewport **Three.js Clipping Planes** fuer das interaktive Abschneiden (Slider direkt auf `plane.constant`), kombiniert mit **Object-Culling** ueber Etagen-Metadaten fuer Grob-Performance. Schnittflaechen-Caps via Stencil-Technik. Fuer die **exportierbare** 2D-Grundriss-Zeichnung die **echte** Schnittkontur ueber OCCT/Manifold berechnen statt nur GPU-Clipping. `clipIntersection` meiden.
---
## 4. Import: DWG/DXF, IFC, STL, OBJ, XYZ-Punktwolken
### DXF / DWG
- **DXF lesen:** `dxf-parser` (gdsestimating) — robust, weit verbreitet, parst DXF-Strings zu JS-Objekten. Repo: <https://github.com/gdsestimating/dxf-parser>. Zum reinen 2D-Anzeigen: `dxf-viewer` (vagran). <https://github.com/vagran/dxf-viewer>
- **DWG lesen (binaer!):** `@mlightcad/libredwg-web` — LibreDWG nach WASM, parst **DWG** (und DXF) direkt im Browser/Node ohne Backend. Aktuell v3.x. Repo: <https://github.com/mlightcad/libredwg-web>, npm: <https://www.npmjs.com/package/@mlightcad/libredwg-web>
- **Achtung Qualitaet/Limits:** DWG-Parsing ist speicherintensiv (kann >2 GB RAM ziehen); libredwg-web erzwingt WASM-Heap-Limits, sehr grosse DWGs koennen scheitern. Im Default-Build sind DXF-Parsing und DWG-Schreiben deaktiviert, um die WASM-Groesse zu senken — DXF also lieber mit `dxf-parser` lesen. Quelle: <https://medium.com/@mlightcad/parsing-autocad-dwg-files-in-the-browser-without-relying-on-the-backend-9067c5d9abf0>
- **Lizenz:** LibreDWG ist **GPL-3.0** — das ist fuer ein proprietaeres Produkt heikel. Wenn das Produkt nicht GPL sein soll, DWG-Import entweder ueber einen separaten Out-of-Process-Konverter kapseln oder Nutzer bitten, vorab nach DXF zu exportieren. **DWG-Lizenzfrage vor Integration klaeren.**
- **Referenz-Implementierung:** `cad-viewer` (mlightcad) zeigt vollstaendigen browser-only DXF/DWG-Viewer/Editor. <https://github.com/mlightcad/cad-viewer>
### IFC (BIM-Kern)
- **web-ifc (ThatOpen/engine_web-ifc):** IFC lesen/schreiben in JS „at native speeds" via WASM. De-facto-Standard fuer Open-BIM im Browser. Repo: <https://github.com/ThatOpen/engine_web-ifc>, Doku: <https://thatopen.github.io/engine_web-ifc/docs/>. **Lizenz: MPL-2.0** (datei-basiertes Copyleft, fuer proprietaere Apps i.d.R. unkritisch). Quelle: <https://spdx.org/licenses/MPL-2.0.html>
- **ThatOpen Components + Fragments:** Hoehere Ebene — `components` (Tools für BIM-Apps), `fragments` (kompaktes Binaerformat auf Google FlatBuffers). Typisch: ~100 MB IFC -> ~10 MB Fragments, >10x schnelleres Laden; Konvertierung lauft worker-basiert. Doku: <https://docs.thatopen.com/Tutorials/Fragments/Fragments/IfcImporter/>, Repo: <https://github.com/ThatOpen/engine_fragment>. IfcImporter setzt web-ifc (>=0.0.72) voraus.
- **Empfehlung:** IFC einmal mit web-ifc parsen, in **Fragments** cachen, danach aus Fragments laden.
### STL / OBJ / XYZ-Punktwolken
- **Standard-Three.js-Loader** decken alles ab: `STLLoader` (ASCII+Binaer), `OBJLoader`, `XYZLoader` (XYZ/XYZRGB -> BufferGeometry), `PCDLoader`. Quellen: <https://threejs.org/docs/#examples/en/loaders/PCDLoader>, <https://deepwiki.com/mrdoob/three.js/4.2-model-format-loaders>. Three.js ist **MIT**.
- **Punktwolken-Performance:** Naive Darstellung skaliert nicht — ~17 Mio. Punkte ruckeln deutlich. Strategien: Downsampling, **LOD/Culling**, Hintergrund-/Streaming-Laden. Fuer sehr grosse Wolken Out-of-Core-Octree-Renderer (z.B. Potree-Ansatz) erwaegen statt eines einzelnen `Points`-Objekts. Quellen: <https://discourse.threejs.org/t/render-large-point-cloud-data-in-threejs/57331>, <https://discourse.threejs.org/t/performance-issues-rendering-large-ply-point-cloud-in-three-js-downsampling-and-background-loading/69135>
### Empfehlung (Import)
**IFC: web-ifc + Fragments (ThatOpen).** **DXF: dxf-parser.** **DWG: libredwg-web — aber GPL-Lizenz vorab klaeren / kapseln.** **STL/OBJ/XYZ/PCD: native Three.js-Loader, mit LOD/Downsampling fuer grosse Punktwolken (Potree-Pattern bei Bedarf).**
---
## 5. Vektor-Export: SVG -> PDF (Print) und DXF
### SVG -> PDF
- **svg2pdf.js (yWorks) + jsPDF — empfohlen.** Reine JS-Loesung, laeuft im Browser, erhaelt **echte Vektoren** (kein Rasterisieren via html2canvas!), was fuer druckfaehige Plaene entscheidend ist. Integriert sich ueber `doc.svg(element, ...)`. Kompatibel mit jsPDF v2/v3/v4. Repo: <https://github.com/yWorks/svg2pdf.js/>. Lizenz: svg2pdf.js **MIT**, jsPDF **MIT**.
- **Trade-off:** SVG-Feature-Abdeckung ist sehr gut, aber nicht 100% — exotische Filter/Pattern koennen abweichen; Schraffuren als explizite Linien (statt CSS-Filter) exportieren erhoeht Treffsicherheit.
- **Alternative/ergaenzend:** `pdf-lib` (MIT) fuer Seitenmontage, Mehrseitigkeit, Metadaten, Zusammenfuehren — kann mit jsPDF-Output kombiniert werden. (`html2canvas`+jsPDF bewusst **vermeiden**, da Raster statt Vektor.)
### DXF-Export
- **@tarikjabiri/dxf (dxfjs/writer) — empfohlen.** Moderner, in TypeScript geschriebener DXF-Generator fuer Node + Browser. Unterstuetzt u.a. **Blocks, Hatches, Insert, Image** — d.h. Schraffuren lassen sich als echte DXF-Hatches exportieren (wichtig fuer CAD-Weiterverarbeitung). npm: <https://www.npmjs.com/package/@tarikjabiri/dxf>, Doku: <https://dxf.vercel.app/>, Repo: <https://github.com/tarikjabiri/js-dxf>. Lizenz: MIT.
- Einfachere Alternative: `dxf-writer` (Vorlaeufer, weniger Features).
### Empfehlung (Export)
**SVG -> PDF: svg2pdf.js + jsPDF (Vektor, nicht Raster), optional pdf-lib fuer Seitenmontage. DXF-Export: @tarikjabiri/dxf mit echten Hatch-Entities.** Interner Zwischenschritt: 2D-Geometrie als SVG-Paths halten (replicad `Drawing.toSVGPaths()`), daraus sowohl PDF als auch DXF erzeugen.
---
## 6. Schraffuren / Muster in SVG/Canvas bei Massstab
### Optionen & Erkenntnisse
- **SVG `<pattern>` mit `patternUnits="userSpaceOnUse"`** ist der richtige Weg fuer massstaebliche Schraffuren: das Muster skaliert NICHT mit der Form (im Gegensatz zu `objectBoundingBox`), sondern bleibt im Weltkoordinaten-Raster — exakt, was man fuer „mm pro Linie im Plan" braucht. Dichte/Winkel ueber `patternTransform`. Quellen: <https://www.w3.org/TR/2015/WD-SVG2-20150915/pservers.html>, <https://www.codegenes.net/blog/simple-fill-pattern-in-svg-diagonal-hatching/>
- **Performance:** Pattern-Layer werden bei jedem Layout-/Zoom-Schritt neu gerechnet; `userSpaceOnUse` vermeidet teures Rescaling und ist hier zugleich der schnellere und der korrekte Weg. Quelle: <https://oreillymedia.github.io/Using_SVG/extras/ch19-performance.html>
- **SVG vs. Canvas:** Fuer technische Zeichnungen ist SVG qualitativ klar ueberlegen (Vektor, exporttauglich nach PDF/DXF). Canvas wird erst bei sehr vielen einfachen Elementen schneller. Quellen: <https://felt.com/blog/from-svg-to-canvas-part-1-making-felt-faster>, <https://www.yworks.com/blog/svg-canvas-webgl>
- **Skalierungs-Strategie:** Bei sehr dichten Schraffuren ueber grosse Flaechen kann die Linienzahl explodieren -> entweder als Pattern-Kachel rendern (eine Definition, vielfach referenziert, statt tausende Einzellinien) **oder** beim Export Schraffuren in echte Linien/Hatch-Entities aufloesen (DXF-Hatch, Abschnitt 5).
### Empfehlung (Schraffuren)
**SVG `<pattern>` mit `patternUnits="userSpaceOnUse"` als primaerer Renderpfad** (massstabskorrekt, exportierbar, performant durch Kachel-Referenzierung). Bei extrem grossen/dichten Flaechen Canvas-Overlay nur fuer die reine Bildschirm-Vorschau erwaegen; fuer Export immer SVG -> svg2pdf.js bzw. echte DXF-Hatches.
---
## 7. WebGPU vs. WebGL + Worker-Offloading der WASM-Geometrie
### Rendering: WebGPU vs. WebGL
- **Reifegrad:** Seit Three.js r171 (Sept. 2025) ist der **WebGPURenderer produktionsreif** mit `import * as THREE from 'three/webgpu'` und **automatischem WebGL2-Fallback** — kein eigener Fallback-Code noetig. Quelle: <https://www.utsubo.com/blog/webgpu-threejs-migration-guide>
- **Browser-Abdeckung:** Chrome/Edge 113 (Mai 2023), Safari 26.0 (Sept. 2025), Firefox 141 (Juli 2025). ~95% der Nutzer WebGPU-faehig, restliche ~5% bekommen WebGL2-Fallback. Quelle: <https://vr.org/articles/webgpu-baseline-2026-three-js-webxr-default>
- **Performance (nuanciert):** Bei draw-call-lastigen Szenen (viele Bauteile/Etagen) gewinnt WebGPU deutlich (bei ~10'000 Draw-Calls ~50 FPS WebGPU vs. ~30 FPS WebGL); bei wenigen grossen Meshes kann WebGL noch gleichauf oder schneller sein. Compute-Shader (Punktwolken, Culling) sind ein WebGPU-Alleinstellungsmerkmal. Quellen: <https://medium.com/@sudenurcevik/upgrading-performance-moving-from-webgl-to-webgpu-in-three-js-4356e84e4702>, <https://altersquare.io/three-js-vs-webgpu-2026-large-scale-construction-viewers/>
- **Vorsicht:** Es gibt weiterhin Szenarien, in denen WebGPU langsamer ist als WebGL — daher messen, nicht blind migrieren. Quelle: <https://github.com/mrdoob/three.js/issues/31055>
### Worker-Offloading der WASM-Geometrie
- **Pflicht, nicht optional:** OCCT/replicad-Berechnungen (Booleans, HLR, Section) gehoeren in einen **Web Worker**, sonst blockiert die UI. replicad ist genau dafuer gebaut (WASM im Worker, Mesh zurueck an den Main-Thread). Quelle: <https://replicad.xyz/docs/use-as-a-library/>
- **Muster:** Worker laedt das (grosse) WASM einmal; Kommunikation via Comlink o.ae.; Geometrie als Transferable (ArrayBuffer) zuruecksenden, um Kopierkosten zu sparen. Manifold (WASM) ebenso im Worker betreiben; Manifold ist intern stark parallelisiert.
### Empfehlung (Performance)
**Three.js `three/webgpu`-Renderer mit automatischem WebGL2-Fallback** (gratis Abwaertskompatibilitaet, Vorteil bei vielen Draw-Calls/Etagen). **Alle WASM-Geometrie (OCCT/replicad + Manifold) konsequent in Web Worker(n)**, Ergebnis als Transferables. WebGPU-Compute fuer Punktwolken-/Culling-Beschleunigung als spaeteres Optimierungs-Upside. Vor groesserer WebGPU-Optimierung mit der echten Szene benchmarken.
---
## Empfehlung (Zusammenfassung)
| Thema | Wahl | Warum |
|---|---|---|
| **Geometriekernel (Solids/Booleans)** | **replicad** (= opencascade.js/OCCT) im Worker; **Manifold** als Mesh-Boolean-Ergaenzung | Echter B-Rep-Kernel noetig fuer exakte BIM-Geometrie + 2D-Ableitung; Manifold (Apache-2.0) schnell+robust fuer Mesh-Importe/Vorschau |
| **2D-Ableitung (HLR/Schnitt)** | **replicad `drawProjection`** (OCC HLRBRep, liefert `{visible, hidden}`); OCCT-Section fuer Schnitte | Einziger seriöser Weg fuer verdeckte/sichtbare Kanten auf echtem B-Rep im Browser; Manifold `slice()` nur fuer reine Konturen |
| **Plan-Clipping (`cutHeight`)** | **Three.js Clipping Planes** + **Object-Culling** (Etagen); echte Schnittkontur (OCCT/Manifold `slice`) fuer Export | GPU-Clipping fluessig & dynamisch fuer Viewport; Culling fuer Grob-Performance; exakte Kontur nur fuer druckbaren Plan |
| **Import IFC** | **web-ifc + Fragments** (ThatOpen) | De-facto Open-BIM-Standard, native Speed, ~10x kleineres/schnelleres Fragments-Caching; MPL-2.0 |
| **Import DXF / DWG** | **dxf-parser** (DXF) / **libredwg-web** (DWG) | Bewaehrt & browser-only; **DWG = GPL-3.0 -> Lizenz vorab klaeren/kapseln**, RAM-Limits bei grossen Dateien |
| **Import STL/OBJ/XYZ/PCD** | **Native Three.js-Loader** + LOD/Downsampling | Out of the box (MIT); grosse Punktwolken brauchen Octree/Potree-Pattern |
| **SVG -> PDF (Print)** | **svg2pdf.js + jsPDF** (+ pdf-lib optional) | Echte Vektoren statt Raster -> druckfaehig; MIT-Lizenzen |
| **DXF-Export** | **@tarikjabiri/dxf** | TS, Browser-faehig, echte Hatch-/Block-Entities fuer CAD-Weiterverarbeitung; MIT |
| **Schraffuren/Muster** | **SVG `<pattern>` mit `userSpaceOnUse`** | Massstabskorrekt (skaliert nicht mit Form), exportierbar, performant via Kachel-Referenz |
| **Rendering** | **Three.js `three/webgpu`** mit WebGL2-Fallback | Produktionsreif seit r171, ~95% Abdeckung, Vorteil bei vielen Draw-Calls; gratis Fallback |
| **WASM-Offloading** | **Web Worker** fuer OCCT/replicad + Manifold, Transferables | UI bleibt reaktiv; replicad ist dafuer gebaut; spart Kopierkosten |
---
### Wichtigste Risiken / offene Punkte
1. **DWG-Lizenz (LibreDWG = GPL-3.0):** Vor Integration klaeren — sonst proprietaeres Produkt gefaehrdet. Option: separater Konvertierungs-Service oder DXF-Pflicht beim Import.
2. **OCCT-WASM-Groesse & Build-Pflege:** zweistellige MB; Custom-Build/Tree-Shaking und Lazy-Loading im Worker einplanen.
3. **HLR-Kosten:** `drawProjection` pro Ansicht cachen; ggf. `PolyAlgo` fuer schnelle Vorschau, `HLRBRep_Algo` fuer den finalen Plan.
4. **WebGPU nicht blind:** mit echter Szene benchmarken — es gibt Faelle, in denen WebGL noch fuehrt.
+590
View File
@@ -0,0 +1,590 @@
# UX/UI-Patterns moderner Browser-CAD/BIM-Tools — Research & Leitplanken
> Recherche für unser Browser-BIM (React + TS + Three.js, DOSSIER-Port, Wohnbau,
> CH/SIA-Kontext). Ziel: konkrete, **übernehmbare** Interaktions- und UI-Muster.
> Stand: 2026-06-29.
>
> Untersucht: **Arcol**, **Snaptrude**, **TestFit**, **Onshape** (Browser-CAD),
> **Vectorworks** (Resource/Navigation-Modell), **Figma** (Canvas-Interaktion,
> Inspector). Querschnitt: Snapping/Inferencing, Grip-Editing, perceived
> performance, Command-Palette, Onboarding.
>
> Jeder externe Claim ist mit Quelle verlinkt. Am Ende:
> **„Leitplanken für unsere UI"** — priorisierte Empfehlungen.
---
## 0. Kurzfazit (TL;DR)
Die ganze Klasse moderner Browser-CAD/BIM-Tools konvergiert auf ein **gemeinsames
Set von Mustern**, das wir fast 1:1 übernehmen sollten:
1. **Ein Modell, viele synchrone Sichten** (2D-Plan ⇄ 3D ⇄ Daten/Sheets), Änderung
in einer Sicht propagiert sofort in alle anderen. (Arcol, Snaptrude)
2. **Kontextuelle UI** statt voller Werkzeugleisten: Buttons/Felder erscheinen nur,
wenn die aktuelle Auswahl sie erlaubt. (Arcol, Onshape, Figma)
3. **Drei-Zonen-Layout**: links Navigator/Layer-Baum, Mitte Canvas + schwebende
Tool-Palette, rechts Inspector. (Figma, Vectorworks, Onshape)
4. **Aggressives Snapping/Inferencing** mit Live-Glyphen + Modifier zum
Unterdrücken. (Onshape) — für Maus-im-Browser unverzichtbar.
5. **Direkte Manipulation per Grips** statt Dialogen. (Figma, DOSSIER-Backlog)
6. **Perceived performance** über Skeletons, optimistic UI und gescopte
Ladezustände — im Browser-3D-Kontext ein Differenzierungs-Hebel.
7. **Command-Palette (Ctrl/Cmd-K)** als Discovery- und Speed-Layer.
8. **Onboarding via „learn by doing"** an einem mitgelieferten Sample-Projekt +
progressive disclosure.
Unsere bestehende Architektur (semantisches Modell als Single Source of Truth,
abgeleitete Sichten, Vectorworks-Terminologie) ist exakt der richtige Unterbau für
diese Muster — die meiste Arbeit liegt in der **UI-Schicht**, nicht im Datenmodell.
---
## 1. Gesamtlayout & Navigator-/Layer-Panels
### 1.1 Was die Tools machen
**Figma** strukturiert die Fläche in vier Zonen: eine Toolbar, **zwei Panels** und
einen scrollbaren Canvas. Links das **Navigation-Panel** mit Layern und Pages,
rechts das **Properties-Panel**; der Layer-Baum „enthält und organisiert alle
Elemente auf dem Canvas … und zeigt, wie Elemente verbunden sind"
([Figma: left sidebar](https://help.figma.com/hc/en-us/articles/360039831974-View-layers-and-pages-in-the-left-sidebar),
[Figma: Interface](https://www.inthepocket.design/course/figma-for-everyone/the-interface)).
**Vectorworks** trennt sauber zwei Konzepte, die für uns 1:1 relevant sind:
- Die **Navigation Palette** gibt Zugriff auf *Classes, Design Layers, Sheet
Layers, Viewports, Saved Views, References* — jeweils als eigener Tab mit Liste.
Sichtbarkeit wird per Klick in einer **Visibility-Spalte** gesetzt; Doppelklick
**aktiviert** einen Layer/eine Class. Die Zeichenfläche bleibt nutzbar, während
die Palette offen ist
([VW Navigation Palette](https://app-help.vectorworks.net/2022/eng/VW2022_Guide/Structure/The_Navigation_palette.htm)).
- Paletten sind **andockbar/ein-/ausblendbar pro Workspace**
([VW Palettes & Tool Sets](https://app-help.vectorworks.net/2018/eng/VW2018_Guide/Start/Palettes_and_Tool_Sets.htm)).
**Arcol** baut beim Modellieren einen **Model Tree** im Menü auf — pro
BIM-Komponente wächst der Baum mit
([AEC Magazine: Arcol BIM in browser](https://aecmag.com/bim/arcol-bim-cloud-browser/)).
**Onshape** zeigt links den **Feature-/Assembly-Baum** (parametrische Historie:
Sketches, Features, Mates) — die Bauhistorie ist die primäre Navigation
([Onshape UI Basics](https://cad.onshape.com/help/Content/ui-basics.htm)).
### 1.2 Übernahme für uns
Unser Dokumentmodell hat **zwei unabhängige Achsen** (siehe ROADMAP §2c):
**Zeichnungsebenen** (Geschosse + Schnitte/Ansichten) und **Ebenen**
(Grafik-Kategorien-Baum `00 Raster … 80 Plangrafik`). Das mappt fast wörtlich auf
das Vectorworks-Navigations-Modell:
- **Linke Sidebar, getabbt** wie die VW-Navigation-Palette:
- Tab **„Geschosse / Ansichten"** (= unsere Zeichnungsebenen): EG/1OG/…,
Schnitte, Ansichten. Mit **Visibility-Toggle**, **Lock**, und **Doppelklick =
aktiv setzen** (welches Geschoss editiert wird).
- Tab **„Ebenen"** (= Grafik-Kategorien-Baum, in *jedem* Geschoss gleich): Baum
mit Code, Name, Farb-Swatch, Linienstärke, Visibility, Hatch. Pro Ansicht
schaltbar (das ist genau VWs „Sichtbarkeit pro Viewport/Saved View").
- Tab **„BIM-Tree / Elemente"** (aus DOSSIER-Backlog §11: *Element-Übersicht
Geschoss→Kind→Element, Suche, Shift-Klick = Zoom*). Das ist unser Pendant zu
Arcols Model Tree + Onshapes Feature-Baum.
- **Visibility-Spalte als Erstklass-Interaktion** (VW-Muster): Auge-Icon je Zeile,
Klick togglet sofort, kein Dialog. (Wir haben bereits `EyeIcon.tsx` — das ist die
Keimzelle.)
- **Wichtig:** Geschoss wird **im 3D *oder* Plan** betrachtet, *keine* getrennten
Daten — der View-Umschalter (siehe §2) gehört in die Geschoss-Auswahl, nicht in
separate Dokumente.
> **Anti-Pattern vermeiden:** Vectorworks selbst leidet unter
> **Paletten-Wildwuchs** (viele frei schwebende Fenster). Für ein fokussiertes
> Wohnbau-Tool: **feste 3-Zonen-Shell** (links Navigator, rechts Inspector), nicht
> N frei schwebende Palettenfenster. Figmas Striktheit schlägt VWs Flexibilität für
> unsere Zielgruppe (Architekt:innen, die schnell ein EFH zeichnen wollen).
---
## 2. 3D ⇄ Plan-View-Umschaltung (das Herzstück)
### 2.1 Was die Tools machen
- **Snaptrude:** Nutzer arbeiten **in 2D *und* 3D gleichzeitig**; Änderung in einer
Sicht spiegelt sich automatisch in der anderen. Objekte sind **nach Geschossen
klassifiziert**, was es erlaubt, „3D-Geometrie zu zeichnen, während man in einer
2D-Grundriss-Ansicht arbeitet". Push-an-einer-Fläche „fühlt sich an wie eine
Linie ziehen", berechnet aber sofort Flächen/BIM-Daten neu
([ArchDaily: Snaptrude](https://www.archdaily.com/1009121/snaptrude-the-browser-based-bim-tool-thats-changing-the-way-architects-work),
[Snaptrude](https://www.snaptrude.com/)).
- **Arcol:** „Every view is 3D in Arcol" — und **Boards** (Präsentations-Layouts)
sind **live-synced**: ändert sich das Gebäude, aktualisieren sich die Layouts
automatisch (kein statischer PDF-Export)
([AEC Magazine: Arcol BIM 2.0](https://aecmag.com/bim/arcol-unleashed-bim-2-0/)).
- **TestFit:** explizites **Expand/Collapse von Optionen** — man klappt Varianten
auf zum Vergleichen und wieder zu, um sich aufs Detail-Editieren im Canvas zu
konzentrieren („smoother flow between setting up a site, reviewing options, and
refining a design")
([TestFit 5.19](https://www.testfit.io/blog/testfit-5-19-a-new-generative-design-workflow)).
### 2.2 Übernahme für uns
Das ist **genau unsere Kern-Architektur** (ROADMAP §2c: „Ansichtstyp = Kamera-
Projektion + optionaler Schnitt"). Konkrete UI-Muster:
- **View-Switcher als segmented control** direkt am Canvas (oben links oder
oben mittig): `3D | Grundriss | Schnitt | Ansicht`. Plan-View = orthogonale
Top-Kamera + Clipping-Ebene auf `okff + schnitthöhe` (steht bereits so im Modell).
- **Kein** Moduswechsel der *Daten*, nur der **Kamera + Schnitt** — visuell als
weicher Übergang (Kamera-Animation) kommunizieren, damit Nutzer Orientierung
behalten (Snaptrude/Arcol-Gefühl: „dieselbe Sache aus anderem Winkel").
- **Optional, stark differenzierend:** **Split-View** (3D links, Plan rechts) wie
Snaptrudes „2D + 3D simultan". Da unsere Sichten ohnehin reaktiv aus *einem*
Modell abgeleitet werden (Zustand-Store → SVG-Plan + Three-Scene), ist Split-View
technisch billig und ein Wow-Moment im Onboarding.
- **Kamera-Presets** (aus DOSSIER-Backlog): Kardinal N/O/S/W, Iso-Oktanten,
**Norden-Rotation** (CH/Swisstopo-Georeferenz) — als kleine Würfel-/Kompass-Gizmo
oben rechts im 3D (ViewCube-Muster, bekannt aus Onshape/CAD allgemein
([Onshape UI Basics](https://cad.onshape.com/help/Content/ui-basics.htm))).
- **Drawing Layers (Schnitte/Ansichten) sind „Saved Views"**: Auswahl in der linken
Sidebar = Kamera + Schnitt + Maßstab + Layer-Kombination springt an (VW „Saved
Views" / DOSSIER „Ausschnitte"). Das ersetzt Ordner-Wildwuchs bei 50+ Ansichten.
---
## 3. Zeichnen & Editieren: Snapping, Inferencing, Grips, Tool-Paletten
### 3.1 Snapping / Inferencing — das wichtigste Detail im Browser
**Onshape** ist hier die Referenz (sehr konkret dokumentiert,
[Onshape Automatic Inferencing](https://cad.onshape.com/help/Content/Sketch/automatic_inferencing.htm)):
- **Inference-Typen:** horizontal, vertikal, **midpoint**, parallel, coincident,
Ausrichtung zum Origin/zu anderen Entities, Tangente, Perpendikular.
- **Visuelles Feedback:**
- **gelbe Highlights** auf Vertices/Mittelpunkten beim Hovern,
- **orange gestrichelte Linie** zeigt eine vorgeschlagene H/V-Ausrichtung,
- **orange Highlight** verwandter Geometrie beim Ziehen (relationales Feedback).
- **Steuerung:** Linksklick **akzeptiert** die vorgeschlagene Bedingung; **Shift
gedrückt halten unterdrückt** Inferencing temporär (loslassen = wieder an).
- **„Wake-up"-Inferences:** kurzes Verweilen über einer Geometrie „weckt" deren
Bezugslinien (z. B. erst Mittelpunkt antippen, dann woanders zeichnen → bekommt
Ausrichtung zu diesem Mittelpunkt).
- **Post-hoc:** vorhandene Geometrie ziehen triggert erneut Inferencing (Center
eines Kreises vertikal zum Origin ziehen → fügt automatisch vertikale Constraint).
**Constraint-Sichtbarkeit:** Constraint-Icons sind farbcodiert (blau =
externe/Referenz, weiß = intern) und ein-/ausblendbar
([Onshape Working with Constraints](https://cad.onshape.com/help/Content/Sketch/working_with_constraints.htm)).
### 3.2 Übernahme für uns (Snapping)
Für ein Maus-bedientes Browser-Tool ist **gutes Snapping der Unterschied zwischen
„Spielzeug" und „Werkzeug".** Konkret:
- **Snap-Targets** (Wohnbau-relevant): Wand-Endpunkte/Achsen, Wand-Mittelpunkte,
Rechtwinklig/Parallel zu bestehender Wand, **Raster** (Achsraster `00 Raster`),
Öffnungs-Achsen, vorhandene 2D-Linien-Endpunkte, Schnittpunkte.
- **Feedback exakt wie Onshape übernehmen:** Snap-Glyph am Cursor (●
Endpunkt, △ Mitte, ⟂ rechtwinklig), **gestrichelte Hilfslinie** für
Achsen-Alignment, Hover-Highlight des Snap-Ziels.
- **Modifier:** **Shift unterdrückt Snapping** (Onshape-Konvention) — Nutzer
erwarten das bereits aus anderen Tools. Zusätzlich **Ortho-Modus** (z. B. Shift
für 0/45/90° beim Linienziehen — Figma-Konvention) sauber davon trennen oder per
Toggle.
- **Live-Maßeingabe beim Zeichnen** (CAD-Standard, auch Onshape): während des
Ziehens Länge/Winkel tippbar (Tab zwischen Feldern). Das ersetzt nachträgliches
Dimensionieren und ist für Architekt:innen Pflicht.
- Wir brauchen **kein** volles Constraint-Solver-System wie Onshape (mechanisches
parametrisches CAD). BIM-Wände sind achs-basiert; **Inferencing beim Setzen**
genügt, persistente geometrische Constraints sind Overkill für Wohnbau.
### 3.3 Grip-Editing / direkte Manipulation
- **Figma:** Auswahl zeigt Bounding-Box mit **Resize-Handles**; ziehen
manipuliert direkt; Smart-Guides/Maße erscheinen relativ zu Nachbarn beim Bewegen
([Figma right sidebar](https://help.figma.com/hc/en-us/articles/360039832014-Design-prototype-and-explore-layer-properties-in-the-right-sidebar)).
- **Snaptrude:** Push/Pull an Flächen als primäre 3D-Edit-Geste
([ArchDaily](https://www.archdaily.com/1009121/snaptrude-the-browser-based-bim-tool-thats-changing-the-way-architects-work)).
- **DOSSIER-Backlog (§11)** listet **Grip-Editing** (Wand-Endpunkte,
Schnitt-Symbole im Plan) explizit als Aufwand L, Phase 3–4 — und das **9-Punkt-
Objekt-Info** (lesen + verschieben/skalieren/rotieren direkt).
**Übernahme:** Grips sind der wichtigste „pro feel"-Hebel.
- **Wand-Endpunkt-Grips** im Plan *und* 3D, mit Snapping (s. o.) und Live-Maß.
- **Öffnungs-Grips** (Position entlang Wand, Breite) — Host-Beziehung bleibt erhalten.
- **Schnittlinien-Grips im Plan** (Schnittlinie ziehen → Schnitt-Ansicht
re-deriviert) — das ist Grip-Editing über die Sicht-Grenze hinweg, ein starkes
Differenzierungsmerkmal.
- Selektion → **bounding handles** (Figma-Muster) für 2D-Plangrafik (Linien,
Rechtecke, Text).
### 3.4 Tool-Palette & kontextuelle Werkzeuge
**Kontextuelle UI ist das durchgehende Muster:**
- **Arcol:** „buttons appear only when they can be used" — Loft/Sweep erscheinen bei
Auswahl zweier Sketches, Boolean bei zwei Extrusions
([AEC: Arcol sneak peek](https://aecmag.com/bim/arcol-a-sneak-peek/)).
- **Onshape:** Sketch-Toolbar erscheint beim Betreten des Sketch-Modus; Tools in
Gruppen (vertikale Trennlinien), Dropdown-Pfeile für Varianten; Dialoge mit
**blau hinterlegtem Feld**, das eine Auswahl im Graphics-Bereich verlangt
([Onshape sketch toolbar](https://cad.onshape.com/help/Content/Sketch/sketch_basics.htm),
[Onshape Constraints](https://cad.onshape.com/help/Content/Sketch/working_with_constraints.htm)).
- **Figma:** schmale Bottom-/Top-Toolbar mit den Kern-Tools; alles Weitere
kontextuell rechts.
**Übernahme:**
- **Schlanke Tool-Palette** (Figma-artig), gruppiert nach unserer Domäne:
*Wand · Tür/Fenster · Decke · Treppe · Dach · Raum* (BIM) und *Linie · Polylinie
· Rechteck · Kreis · Bogen · Text · Bemaßung* (2D auf `80 Plangrafik`).
- **Modus-bewusste Tools:** im Grundriss andere Defaults als im 3D; im
Schnitt/Ansicht nur Annotation/2D-Tools.
- **Contextual action bar bei Auswahl** (Arcol-Muster): selektiere eine Wand →
schwebende Mini-Toolbar „Tür einsetzen / Fenster / Wandtyp / verschneiden".
Selektiere zwei Wände → „verschneiden / verlängern".
- **Aktives Tool sticky + ESC bricht ab**, Leertaste = Pan, Scroll = Zoom
(Figma/CAD-Konventionen — Nutzer bringen Muskelgedächtnis mit).
- **LoD-bewusste Tool-/Stil-UI** (DOSSIER-Backlog: *zeigt nur passende Controls je
Geometrie-Typ*) — keine Füll-Optionen bei 3D-Auswahl. Deckt sich mit Figmas
„controls appear based on layer type".
---
## 4. Property-/Inspector-Panel
### 4.1 Was die Tools machen
**Figma — rechte Sidebar** (für uns das Vorbild,
[Figma right sidebar](https://help.figma.com/hc/en-us/articles/360039832014-Design-prototype-and-explore-layer-properties-in-the-right-sidebar)):
- Tabs **Design / Prototype** (bei Edit), **Inspect / Properties** (bei View-only).
- Kategorien: **Alignment/Rotation/Position → Dimensions → Constraints/Layout →
Appearance (Fill, Stroke, Effects) → Export**.
- **Controls erscheinen je Layer-Typ** (kontextuell).
- **Dev-Mode/Inspect** liefert konkrete Werte + Abstände zwischen Objekten + Code.
**Arcol — rechtes Panel** zeigt **Gebäude-Metriken** kontextuell: Geschossfläche,
Anzahl Geschosse, GFZ/FAR, Unit-Count, Standortfläche, Kostenschätzung
([Arcol BIM 2.0](https://aecmag.com/bim/arcol-unleashed-bim-2-0/)).
**TestFit — ein zentrales Parameter-Panel:** „alle Schlüsselparameter an einem
Ort" (Unit-Counts, Parkplatz-Ziele, Gebäudegrößen-Limits) → sofortige Wirkung auf
generierte Optionen
([TestFit 5.19](https://www.testfit.io/blog/testfit-5-19-a-new-generative-design-workflow)).
**Onshape — Feature-Dialoge:** Erstellen/Editieren über Dialoge mit Pflicht-
Selektionsfeldern (blau)
([Onshape UI Basics](https://cad.onshape.com/help/Content/ui-basics.htm)).
### 4.2 Übernahme für uns
- **Rechter Inspector, kontextuell nach Element-Typ** (Figma-Muster):
- **Wand** → Wandtyp (→ WallType-Bibliothek), Referenzlage mid/left/right
(DOSSIER-Backlog), Höhe, Achs-Endpunkte (9-Punkt/Maße), Stil-Overrides.
- **Tür/Fenster** → Breite/Höhe, Brüstung, Schwenkbogen/Anschlag, Detailgrad,
Symbol.
- **Decke/Slab** → SlabType, UK/OK-Override, Aussparungen.
- **Raum** → SIA-416-Kategorie (HNF/NNF/VF/FF/GF/AGF), Fläche (read-only,
berechnet), Stempel-Felder.
- **2D-Element** → Linienstil (→ Line Manager), Hatch (→ Hatch Manager), Farbe.
- **Sektionen kollabierbar** (Figma) — Wohnbau-Inspector kann lang werden;
Default-Sektionen offen, Fortgeschrittenes (Overrides) zugeklappt
(= progressive disclosure, s. §7).
- **Mixed-value-Handling bei Mehrfachauswahl** (Figma): bei Mehrfachauswahl
abweichende Werte als „Mixed/—" zeigen, gemeinsames Editieren erlauben. Wichtig
z. B. „alle EG-Wände auf Wandtyp X".
- **Live-Metriken-Block** (Arcol-Muster) — selbst im Wohnbau wertvoll:
Bruttogeschossfläche, **SIA-416-Bilanz**, Raumzahl, Volumen. Im Inspector wenn
nichts selektiert ist = „Projekt-Übersicht" (vgl. Figmas Canvas-Level-Optionen
bei leerer Auswahl).
- **Read-only-Felder klar markieren** (berechnete Flächen/Volumen) vs. editierbar.
---
## 5. Resource-Manager (Bibliotheken)
### 5.1 Was Vectorworks macht — unser direktes Vorbild
Der **Resource Manager** ist „ein zentraler Ort für Assets" (Symbole, Linientypen,
Texturen, Materialien …)
([VW Resource Manager](https://app-help.vectorworks.net/2026/eng/VW2026_Guide/ResourceManager/Resource%20Manager.htm)):
- **Zwei-/Drei-Pane-Modell:** **File-Browser** (offene Dateien, Favoriten,
VW-Libraries, User-/Workgroup-Libraries) → **Resource-Viewer** (Ressourcen der
gewählten Datei) → optional **Preview/Metadaten**
([VW File browser pane](https://app-help.vectorworks.net/2023/eng/VW2023_Guide/ResourceManager/Resource_Manager_File_browser_pane.htm),
[VW Resource viewer pane](https://app-help.vectorworks.net/2026/eng/VW2026_Guide/ResourceManager/Resource_Manager_Resource_viewer_pane.htm)).
- **Organisation mehrdimensional:** nach Quelle, **nach Typ** (Dropdown-Filter),
nach Ordnerstruktur.
- **Ansichten:** Thumbnails / List / Thumbnails-List; **Suchfeld** mit Filtern.
- **Resource Selector**: dieselbe Bibliothek erscheint **in Dialogen** und zeigt
dort nur **kontextuell passende** Ressourcen.
### 5.2 Übernahme für uns
Unsere ROADMAP §2d definiert bereits **Line Manager / Hatch Manager / Component
Manager**, mit Verweis-per-id-Architektur (Components → Hatches → LineStyles). Das
Vectorworks-Modell passt perfekt:
- **Ein gemeinsames Resource-Browser-Pattern** für alle Bibliotheken (Linienstile,
Schraffuren, Components/Baustoffe, später Wand-/Öffnungs-Stil-Kataloge,
Material-PBR, Raumstempel-Layouts). Eine wiederverwendbare React-Komponente,
parametrisiert nach Ressourcentyp.
- **Zwei Erscheinungsformen** (wie VW):
1. **Manager-Ansicht** (großes Panel/Modal) zum Anlegen/Editieren/Duplizieren.
2. **Inline-Resource-Selector** im Inspector — beim Setzen eines Wandtyps/Hatch
öffnet sich ein kompakter Picker mit Thumbnails, gefiltert auf den passenden
Typ. (Figma macht das analog mit „Styles/Variables".)
- **Thumbnails sind im CAD-Kontext kritisch**: Hatch-Vorschau, Component-
Schichtaufbau, Linienstil-Strich als gerenderte Mini-Previews.
- **Zentrale Änderung propagiert** (unsere id-Referenz-Architektur): Component-Farbe
ändern → alle Wände mit diesem Component aktualisieren live. Das ist Arcols
„single source of truth" auf Ressourcen-Ebene.
- **Cross-Projekt-Presets/Favoriten** (DOSSIER-Backlog, LocalStorage; VW-Favoriten):
„einmal speichern, überall nutzen".
---
## 6. Perceived Performance (gefühlte Geschwindigkeit)
Browser-3D + WASM-Geometrie (web-ifc, OpenCascade) + HLR-Projektion = **echte
Latenz** an mehreren Stellen. Gefühlte Performance ist hier ein
Differenzierungs-Hebel gegenüber schwerfälligem Revit/ArchiCAD.
### 6.1 Belegte Muster
- **Skeleton-Screens** lassen Apps **20–30 % schneller** wirken als Spinner bei
identischer realer Ladezeit
([LogRocket: skeleton screens](https://blog.logrocket.com/ux-design/skeleton-loading-screen-design/),
[UI Deploy](https://ui-deploy.com/blog/skeleton-screens-vs-spinners-optimizing-perceived-performance)).
- **Indikator zur Situation passen:** Spinner für kurze Waits, Skeleton für
Content, Progress-Bar für messbare Operationen, **optimistic UI** für
„instant-feeling" Aktionen
([Onething: skeleton vs spinner](https://www.onething.design/post/skeleton-screens-vs-loading-spinners)).
- **Optimistic UI**: UI sofort aktualisieren, Server-Bestätigung abwarten, nur bei
Fehler zurückrollen — ideal für häufige, risikoarme Aktionen
([Smart Interface Design Patterns](https://smart-interface-design-patterns.com/articles/designing-better-loading-progress-ux/)).
- **Verzögerung vor Indikator (100–200 ms):** schließt die Operation vorher ab,
gar kein Indikator → kein Flackern
([Onething](https://www.onething.design/post/skeleton-screens-vs-loading-spinners)).
- **Gescopte Ladezustände** (React Suspense / Next loading.tsx): nur der betroffene
Bereich lädt, der Rest bleibt interaktiv; `aria-busy`, Live-Regions,
reduced-motion respektieren
([LogRocket](https://blog.logrocket.com/ux-design/skeleton-loading-screen-design/)).
### 6.2 Übernahme für uns (konkret)
- **Optimistic Model-Edits:** Geometrie-Mutation (Wand ziehen, Tür setzen) **sofort**
im Zustand-Store + 3D anzeigen; **schwere Booleans/HLR im Web Worker** (Comlink,
bereits geplant) nachziehen. Wand erscheint sofort, die *exakte* verschnittene
Öffnung/Schnittlinie „schärft nach". UI bleibt flüssig.
- **Progressive Plan-Generierung:** SVG-Grundriss zuerst grob (Achsen/Linien),
Schraffuren/Symbole nachladen — Skeleton/„low-detail first" statt Spinner.
- **HLR-Schnitte (Risiko #4):** Worker + Caching (ROADMAP §6). UI: **Skeleton der
Schnitt-Ansicht** + „berechne verdeckte Kanten…" mit Progress, restliche App
bleibt nutzbar (gescopter Ladezustand).
- **Delay-then-show** für alle Worker-Tasks (150 ms-Schwelle), sonst Flicker beim
schnellen Editieren.
- **Three.js-Disziplin:** stabile 60 fps beim Orbit/Pan ist selbst „perceived
performance" — instanziertes Rendering, Frustum-Culling, LoD für ferne Geometrie
(deckt sich mit ROADMAP-Phase-7-Performance-Härtung). Lieber 60 fps bei grober
Geometrie als ruckelnde Präzision.
- **Auto-Save-Status klar, unaufdringlich** kommunizieren („Gespeichert"/„Speichern…"),
optimistic — nie blockierend (Figma-Muster).
---
## 7. Onboarding & Discoverability
### 7.1 Belegte Muster
- **Progressive Disclosure:** zuerst nur Essenzielles zeigen, Komplexität schrittweise
enthüllen — reduziert kognitive Last; drei Typen: step-by-step, conditional,
contextual
([IxDF: Progressive Disclosure](https://ixdf.org/literature/topics/progressive-disclosure),
[UXPin](https://www.uxpin.com/studio/blog/what-is-progressive-disclosure/)).
- **„Learn by doing" an Sample-Dokument:** Grammarly startet Nutzer mit einem
Beispiel-Dokument mit Fehlern; Hotspots/Tooltips führen durch Features
([Userpilot: onboarding examples](https://userpilot.com/blog/user-onboarding-examples/)).
- **Stufenweises Aufdecken fortgeschrittener Features** (Asana: erst Projekt
anlegen, später Dependencies/Kanban/Gantt)
([Userpilot: progressive disclosure](https://userpilot.com/blog/progressive-disclosure-examples/)).
- **Command-Palette als Discovery-Layer:** durchsuchbare Befehlsliste hilft, Features
zu entdecken — „incredible effect on exploration and feature discoverability",
besonders für neue/seltene Nutzer
([Mobbin: command palette](https://mobbin.com/glossary/command-palette),
[Untitled UI: command menus](https://www.untitledui.com/components/command-menus)).
- **Arcol** wirbt explizit mit „low barrier to entry, gentle learning curve … clean,
intuitive, requires minimal training"
([AEC: Arcol BIM 2.0](https://aecmag.com/bim/arcol-unleashed-bim-2-0/)).
### 7.2 Übernahme für uns
- **Mitgeliefertes Sample-Projekt** (wir haben bereits `sampleProject.ts`!) als
Onboarding-Bühne: ein kleines EFH, fertig modelliert. Nutzer **manipuliert echtes
Modell** statt leerem Canvas (Grammarly-Muster). Erste Geste: „zieh diese Wand"
→ sieht 3D + Plan live mitlaufen (unser Kern-Wow).
- **Progressive Disclosure im Inspector & Tool-Palette:** Default zeigt
Wohnbau-Essenz (Wand/Tür/Fenster/Decke/Raum). Fortgeschrittenes (Prioritäts-
Verschneidung, Overrides, Detailgrade, Section-Styles) **zugeklappt / hinter
„Erweitert"**. Das passt zu unserem radikalen Wohnbau-Fokus.
- **Contextual coachmarks** statt langem Tutorial: beim ersten Selektieren einer
Wand ein kleiner Tooltip „Endpunkt ziehen zum Verlängern, Doppelklick für Wandtyp".
- **Command-Palette (Ctrl/Cmd-K)** — siehe §8 — doppelt als Onboarding: alle
Werkzeuge/Befehle durchsuchbar = lebende Feature-Liste.
- **Tastatur-Kürzel sichtbar machen** (in Tooltips, in der Palette) — schult
beiläufig pro Workflows.
---
## 8. Command-Palette & Tastatur
### 8.1 Belegte Muster
- **Ctrl/Cmd-K** ist die De-facto-Konvention (Linear, Figma [Cmd-P], Notion,
Vercel, Raycast, Slack, Superhuman); VS Code nutzt Cmd-Shift-P
([Mobbin](https://mobbin.com/glossary/command-palette),
[Superhuman: command palette](https://blog.superhuman.com/how-to-build-a-remarkable-command-palette/)).
- Zwei Haupt-Use-Cases: **Navigation/Suche** und **Shortcuts/Quick Actions**
([Outdraw Academy: command palette](https://outdraw-academy.gitbook.io/ux-patterns/command-palette)).
- Trigger kann **sichtbar** (Button/Suchleiste) oder nur per Shortcut sein —
für Discovery besser **auch sichtbar**
([Mobbin](https://mobbin.com/glossary/command-palette)).
### 8.2 Übernahme für uns
- **Cmd/Ctrl-K-Palette** für: Werkzeug aktivieren („Wand", „Tür"), Ansicht
springen („Grundriss EG", „Schnitt A-A"), Ressource öffnen („Hatch Manager"),
globale Aktionen („Norden rotieren", „PDF exportieren", „SIA-Bilanz").
- **Sichtbarer Trigger** (Such-/Befehlsfeld in der Top-Bar) für Entdeckung +
Shortcut für Speed.
- **Konsistente, dokumentierte Shortcuts** (Figma-Disziplin): W=Wand, T=Tür,
L=Linie, Space=Pan, Scroll=Zoom, Shift=Snap aus, Esc=Abbrechen, G=Grundriss-Toggle.
In Tooltips + Palette anzeigen.
---
## 9. Leitplanken für unsere UI (priorisierte Empfehlungen)
> Sortiert nach **Hebel × Aufwand**. „P#" = grobe Phasen-Zuordnung zur ROADMAP.
### A. Sofort / Fundament (Phase 0–1) — billig, prägt alles
1. **Feste 3-Zonen-Shell.** Links **Navigator** (Tabs: *Geschosse/Ansichten · Ebenen
· BIM-Tree*, je mit Visibility-Toggle wie VW Navigation Palette). Mitte **Canvas**
mit schwebender Tool-Palette + View-Switcher. Rechts **Inspector** (kontextuell).
*Keine* frei schwebenden Palettenfenster (Anti-VW-Wildwuchs).
2. **View-Switcher als segmented control** `3D | Grundriss | Schnitt | Ansicht`
direkt am Canvas; weiche Kamera-Übergänge; *eine* Datenquelle (kein Daten-
Moduswechsel). Nutzt unsere bestehende „abgeleitete Sichten"-Architektur.
3. **Visibility/Lock pro Zeile** als Erstklass-Interaktion (1 Klick, kein Dialog) —
`EyeIcon.tsx` ausbauen.
4. **Kontextueller Inspector**: Controls nur für den selektierten Element-Typ
(Figma/Arcol); berechnete Felder read-only markiert; Sektionen kollabierbar.
5. **Tastatur-Grundlagen + Konventionen festnageln**: Space=Pan, Scroll=Zoom,
Esc=Abbrechen, Shift=Snap aus. Früh festlegen → Muskelgedächtnis.
### B. Kern-„Pro-Feel" (Phase 1–2) — der eigentliche Wert
6. **Snapping/Inferencing nach Onshape-Vorbild**: Snap-Glyphen am Cursor,
gestrichelte Achsen-Hilfslinien, Hover-Highlight, **Shift = unterdrücken**,
Wake-up-Inferences. Targets: Wand-Enden/Achsen/Mitten, Raster, Rechtwinklig/
Parallel, Öffnungs-Achsen. **Höchste Priorität für „Werkzeug-Gefühl".**
7. **Live-Maßeingabe beim Zeichnen** (Länge/Winkel tippbar, Tab zwischen Feldern).
8. **Kontextuelle Action-Bar bei Auswahl** (Arcol): Wand selektiert → „Tür/Fenster
einsetzen / Wandtyp / verschneiden". Buttons erscheinen nur, wenn anwendbar.
9. **Schlanke domänen-gruppierte Tool-Palette** (BIM-Bauteile + 2D-Zeichnen),
modus-bewusst je Ansicht.
10. **Optimistic Edits + Worker-Nachzug**: Mutation sofort sichtbar, schwere
Booleans/HLR im Worker (Comlink); Delay-then-show (150 ms) statt Spinner-Flicker.
### C. Differenzierung (Phase 3) — hier gewinnen wir
11. **Grip-Editing** (Wand-Endpunkte, Öffnungs-Position/-Breite, **Schnittlinie im
Plan**) mit Snapping + Live-Maß, in 2D *und* 3D. (DOSSIER-Backlog, Aufwand L —
aber Kern-Differenzierer.)
12. **Saved Views / Ausschnitte** in der linken Sidebar = Kamera + Schnitt + Maßstab
+ Layer-Kombination per Klick (VW „Saved Views" / DOSSIER). Skaliert auf 50+
Ansichten.
13. **Wiederverwendbares Resource-Browser-Pattern** für Line/Hatch/Component-Manager:
Zwei-Pane (File-Browser → Viewer mit Thumbnails) als Manager *und* als inline
Picker im Inspector (VW-Modell). Zentrale Änderung propagiert (id-Referenzen).
14. **Perceived-performance-Politik festschreiben**: Skeletons für Plan-/Schnitt-
Generierung, gescopte Ladezustände (restliche App bleibt nutzbar),
reduced-motion/`aria-busy` respektieren.
15. **Split-View 3D|Plan** (Snaptrude-Muster) — billig dank reaktiver Sichten,
starker Wow-Effekt; auch fürs Onboarding.
### D. Adoption & Politur (Phase 1 fortlaufend → 7)
16. **Onboarding via Sample-Projekt** (`sampleProject.ts` als fertiges EFH);
„learn by doing", erste Geste zeigt 3D⇄Plan-Live-Sync. Progressive Disclosure:
Wohnbau-Essenz default, Fortgeschrittenes (Prioritäts-Verschneidung, Overrides,
Detailgrade) zugeklappt.
17. **Command-Palette (Cmd/Ctrl-K)** + sichtbarer Trigger: Werkzeuge/Ansichten/
Ressourcen/Aktionen durchsuchbar; doppelt als Discovery-Layer.
18. **Live-Metriken-Block** im Inspector (Arcol): BGF, **SIA-416-Bilanz**, Räume,
Volumen — bei leerer Auswahl als Projekt-Übersicht.
19. **CH-Spezifika UI-seitig vorsehen**: **Norden-Rotation**-Gizmo (ViewCube/Kompass),
SIA-Raumkategorien im Raum-Inspector, später Swisstopo-Import-Flow mit Auto-Zoom
+ Nullpunkt-Verschiebung.
### Übergreifende Designprinzipien (gelten immer)
- **Kontextualität vor Vollständigkeit** — zeige nur, was jetzt anwendbar ist
(Arcol/Onshape/Figma). Direkt verzahnt mit unserem „LoD-bewusste Stil-UI"-Backlog.
- **Direkte Manipulation vor Dialogen** — Grips/Drag/Inline-Edit schlägt
Properties-Dialog (Figma/Snaptrude/DOSSIER).
- **Eine Wahrheit, viele Sichten** — niemals Sicht-spezifische Daten; alles aus dem
semantischen Modell ableiten (deckt sich exakt mit unserem Architektur-Prinzip).
- **Konventionen respektieren** — Pan/Zoom/Snap/Esc/Cmd-K wie die etablierten Tools;
Nutzer bringen Muskelgedächtnis aus Figma/CAD mit.
- **Gefühlte > tatsächliche Geschwindigkeit** — optimistic + Skeletons + 60 fps;
im schweren Browser-3D-Geometrie-Kontext ein echter Wettbewerbsvorteil.
---
## Quellen
**Arcol**
- [Arcol unleashed – BIM 2.0 (AEC Magazine)](https://aecmag.com/bim/arcol-unleashed-bim-2-0/)
- [Arcol – BIM in a browser (AEC Magazine)](https://aecmag.com/bim/arcol-bim-cloud-browser/)
- [Arcol: a sneak peek (AEC Magazine)](https://aecmag.com/bim/arcol-a-sneak-peek/)
- [The Arcol Manifesto](https://arcol.io/blog/the-arcol-manifesto)
**Snaptrude**
- [Snaptrude – browser-based BIM tool (ArchDaily)](https://www.archdaily.com/1009121/snaptrude-the-browser-based-bim-tool-thats-changing-the-way-architects-work)
- [Snaptrude (offiziell)](https://www.snaptrude.com/)
**TestFit**
- [TestFit 5.19: A New Generative Design Workflow](https://www.testfit.io/blog/testfit-5-19-a-new-generative-design-workflow)
- [TestFit (offiziell)](https://www.testfit.io/)
**Onshape**
- [Onshape: User Interface Basics](https://cad.onshape.com/help/Content/ui-basics.htm)
- [Onshape: Automatic Inferencing](https://cad.onshape.com/help/Content/Sketch/automatic_inferencing.htm)
- [Onshape: Working with Constraints](https://cad.onshape.com/help/Content/Sketch/working_with_constraints.htm)
- [Onshape: Sketch Basics](https://cad.onshape.com/help/Content/Sketch/sketch_basics.htm)
**Vectorworks**
- [VW Resource Manager](https://app-help.vectorworks.net/2026/eng/VW2026_Guide/ResourceManager/Resource%20Manager.htm)
- [VW Resource Manager: File browser pane](https://app-help.vectorworks.net/2023/eng/VW2023_Guide/ResourceManager/Resource_Manager_File_browser_pane.htm)
- [VW Resource Manager: Resource viewer pane](https://app-help.vectorworks.net/2026/eng/VW2026_Guide/ResourceManager/Resource_Manager_Resource_viewer_pane.htm)
- [VW Navigation Palette](https://app-help.vectorworks.net/2022/eng/VW2022_Guide/Structure/The_Navigation_palette.htm)
- [VW Palettes and Tool Sets](https://app-help.vectorworks.net/2018/eng/VW2018_Guide/Start/Palettes_and_Tool_Sets.htm)
**Figma**
- [Figma: right sidebar / layer properties](https://help.figma.com/hc/en-us/articles/360039832014-Design-prototype-and-explore-layer-properties-in-the-right-sidebar)
- [Figma: left sidebar (layers & pages)](https://help.figma.com/hc/en-us/articles/360039831974-View-layers-and-pages-in-the-left-sidebar)
- [Figma for Everyone: The Interface](https://www.inthepocket.design/course/figma-for-everyone/the-interface)
**Perceived Performance**
- [LogRocket: Skeleton loading screen design](https://blog.logrocket.com/ux-design/skeleton-loading-screen-design/)
- [UI Deploy: Skeleton Screens vs. Spinners](https://ui-deploy.com/blog/skeleton-screens-vs-spinners-optimizing-perceived-performance)
- [Onething: Skeleton Screens vs Loading Spinners](https://www.onething.design/post/skeleton-screens-vs-loading-spinners)
- [Smart Interface Design Patterns: Loading & Progress UX](https://smart-interface-design-patterns.com/articles/designing-better-loading-progress-ux/)
**Command Palette**
- [Mobbin: Command Palette](https://mobbin.com/glossary/command-palette)
- [Untitled UI: Command menus (Cmd-K)](https://www.untitledui.com/components/command-menus)
- [Superhuman: How to build a remarkable command palette](https://blog.superhuman.com/how-to-build-a-remarkable-command-palette/)
- [Outdraw Academy: Command Palette UX pattern](https://outdraw-academy.gitbook.io/ux-patterns/command-palette)
**Onboarding / Progressive Disclosure**
- [IxDF: Progressive Disclosure](https://ixdf.org/literature/topics/progressive-disclosure)
- [UXPin: What Is Progressive Disclosure](https://www.uxpin.com/studio/blog/what-is-progressive-disclosure/)
- [Userpilot: User onboarding examples](https://userpilot.com/blog/user-onboarding-examples/)
- [Userpilot: Progressive disclosure examples](https://userpilot.com/blog/progressive-disclosure-examples/)
+124
View File
@@ -0,0 +1,124 @@
# Welle C — HLR-Spike: Feasibility-Report
> 2D-Vektor-Schnitte/-Ansichten aus dem 3D-Modell via Hidden-Line-Removal (HLR)
> mit opencascade.js (OCCT-WASM). Isolierter De-Risking-Spike, NICHT in die App
> verdrahtet. Einheiten: Meter.
## Ergebnis (kurz)
**HLR funktioniert in diesem Browser-Projekt.** opencascade.js initialisiert im
Vite-Build, und der HLR-Lauf liefert korrekte, getrennte sichtbare/verdeckte
2D-Kanten. Verifiziert im echten Browser (Chromium via Vite-Dev-Server) an einer
L-Wand + Bodenplatte, Front-Ansicht:
| Messwert | Wert |
|---|---|
| Sichtbare Kanten (sharp) | **9** |
| Verdeckte Kanten | **16** |
| Reine HLR-Rechenzeit | **~21 ms** |
| WASM-Größe (unkomprimiert) | **62.8 MB** (65 864 037 Bytes) |
| WASM-Größe (gzip) | **~19.6 MB** |
| WASM-Fetch (lokal, Dev) | ~113 ms |
| Modul-Init (Emscripten instanziieren) | ~450 ms (Browser) / ~680 ms (Node) |
Beweis-Artefakte: `hlr-elevation-proof.svg` / `.png` in diesem Ordner — sichtbare
Kanten durchgezogen, verdeckte gestrichelt. Die Zeichnung liest sich als korrekte
Hidden-Line-Ansicht (Silhouette + sichtbare Front-Kanten voll, verdeckte hinten
gestrichelt).
## Der genutzte API-Pfad (wichtig!)
Der geplante klassische Pfad (`HLRBRep_Algo`/`HLRBRep_PolyAlgo` +
`HLRBRep_HLRToShape`/`HLRBRep_PolyHLRToShape`) ist im **prebuilt Vollbuild
1.1.1 NICHT verfügbar**: diese Klassen sind nur als `Handle_…`-Smart-Pointer
gebunden, ohne konstruierbare Roh-Klasse und ohne den `HLRToShape`-Extraktor.
Ein direkter Aufbau darüber ist damit nicht möglich.
**Verwendeter, funktionierender Pfad:** `HLRAppli_ReflectLines` — der High-Level-
OCCT-Wrapper, der intern GENAU den exakten HLR-Algorithmus (`HLRBRep_Algo`)
fährt. Er ist als Klasse gebunden und liefert getrennt sichtbare/verdeckte
Kanten nach Kanten-Typ:
```
const rl = new oc.HLRAppli_ReflectLines(shape);
rl.SetAxes(dirX, dirY, dirZ, atX, atY, atZ, upX, upY, upZ); // Ortho-Projektion
rl.Perform();
const T = oc.HLRBRep_TypeOfResultingEdge;
// (typ, visible, in3d) → in3d=false liefert 2D-projizierte Kanten (Z≈0)
const visSharp = rl.GetCompoundOf3dEdges(T.HLRBRep_Sharp, true, false);
const visOutline = rl.GetCompoundOf3dEdges(T.HLRBRep_OutLine, true, false);
const hidSharp = rl.GetCompoundOf3dEdges(T.HLRBRep_Sharp, false, false);
```
Kanten-Extraktion: `TopExp_Explorer_2(compound, TopAbs_EDGE, TopAbs_SHAPE)` →
`TopoDS.Edge_1(current)` → `new BRepAdaptor_Curve_2(edge)` →
`FirstParameter/LastParameter/Value(u)` (`gp_Pnt` mit `.X() .Y() .Z()`).
**embind-Überladungen sind versioniert** (`_1`, `_2`, …) und die Nummerierung im
Vollbuild folgt NICHT der Argumentzahl. Empirisch verifiziert:
`BRepPrimAPI_MakeBox_1(dx,dy,dz)`, `BRepPrimAPI_MakeBox_3(gp_Pnt, gp_Pnt)`,
`BRepAlgoAPI_Fuse_3(a, b)`, `gp_Pnt_3(x,y,z)`. Bei einem Versionswechsel des
Pakets müssen diese Suffixe neu geprüft werden (Fehlermeldung nennt die
erwartete Parameterzahl).
## Vite-/package.json-Änderungen (genau)
- `package.json`: Dependency `opencascade.js@^1.1.1` (Vollbuild).
- `vite.config.ts` (additiv, analog zum bestehenden LibreDWG-Muster):
- Zwei Aliase → virtuelle Module auf die echten Paket-Dateien:
`virtual:occt-glue` → `dist/opencascade.wasm.js`,
`virtual:occt-wasm-url` → `dist/opencascade.wasm.wasm?url`.
- `optimizeDeps.exclude: ["opencascade.js"]` (esbuild kommt mit dem
UMD-Wrapper + Node-Shims der Glue nicht klar → Vor-Bündeln ausschließen).
- `src/section/occt-wasm.d.ts`: Ambient-Stubs für die zwei virtuellen Module.
- Geladen wird per **dynamischem Import** in `src/section/occt.ts` → die 62-MB-
WASM landet NICHT im Haupt-Bundle, sondern als separater Lazy-Chunk + Asset.
Verifiziert: `vite build` der App bleibt grün und unverändert groß (kein
OCCT-Chunk, da der Spike nicht verdrahtet ist). Ein Lib-Build, der `hlr.ts`
referenziert, splittet OCCT sauber in einen eigenen Glue-Chunk + WASM-Asset ab.
`tsc` ist für die neuen Dateien grün. (`npm run build` schlägt aktuell in
`tsc -b` fehl — ausschließlich wegen fehlender i18n-Keys in den parallel
bearbeiteten Dateien `commands/cmds/stair.ts` / `state/*`, NICHT wegen dieses
Spikes.)
## Dateien dieses Spikes
- `src/section/hlr.ts` — Kernmodul: Box-Specs → Fuse → HLR → 2D-Polylinien
(`hlrFromBoxes`, `hlrShape`, `VIEWS`, `hlrToSvg`).
- `src/section/occt.ts` — Lazy-Loader + schmale Typ-Fassade + Lade-Metriken.
- `src/section/occt-wasm.d.ts` — Ambient-Deklarationen der virtuellen Module.
- `docs/welle-c-hlr-spike/` — dieser Report + Proof-SVG/-PNG.
## Bewertung für Welle C
**Viabel.** Der exakte HLR liefert saubere, normgerechte Vektor-Kanten mit
korrekter Sichtbarkeit — genau das, was Schnitt/Ansicht brauchen und was reines
three.js-Kanten-Projizieren nicht robust liefert (dort fehlt echte
Flächen-Verdeckung). Die 2D-Polylinien passen direkt in die bestehende
SVG-Plan-Pipeline (`Pt2`/`Vec2`-kompatibel).
**Der Kostenpunkt ist die WASM-Größe (62.8 MB / ~19.6 MB gzip).** Für einen
Spike akzeptabel; für Produktion zu groß, um sie eager zu laden.
### Empfohlener Integrations- + Größenreduktions-Plan
1. **Lazy laden** (bereits so gebaut): OCCT erst bei erster Schnitt-/Ansichts-
Erzeugung dynamisch importieren; Ladezustand in der UI anzeigen. Der
Grundriss läuft weiter ohne OCCT (parametrisch, wie bisher).
2. **Web-Worker**: HLR im Worker fahren, damit der UI-Thread frei bleibt (die
WASM ist groß, aber der HLR-Lauf selbst ist mit ~20 ms günstig).
3. **Größe reduzieren — lohnt sich klar**: ein **Custom-Build** von
opencascade.js (das Paket unterstützt `make.py`/Docker-Build mit einer
Symbol-Whitelist). Für Welle C reicht ein minimaler Satz:
`BRepPrimAPI_*` (bzw. der spätere Solid-Erzeuger), `BRepAlgoAPI_Fuse/Common`,
`HLRAppli_ReflectLines` + `HLRBRep_TypeOfResultingEdge`, `TopExp_Explorer`,
`TopoDS`, `BRepAdaptor_Curve`, `gp_*`. Erfahrungswerte solcher Minimal-Builds
liegen bei **~5–15 MB WASM** statt 62 MB — Aufwand: ein reproduzierbarer
Docker-Build in CI, der die `.wasm`/`.js` als Projekt-Asset eincheckt.
Alternativ die **beta 2.0** prüfen (moderneres Build-System, evtl. bereits
schlankere Module + die klassischen HLR-Klassen konstruierbar).
4. **Fallback, falls die Größe untragbar bleibt**: three.js-basierte
Kanten-Extraktion (`EdgesGeometry`) + eigene Sichtbarkeit via Depth-Peeling /
GPU-Occlusion. Deutlich mehr Eigenaufwand, weniger robust bei Verschneidungen
— daher nur zweite Wahl; OCCT-HLR bleibt der empfohlene Weg.
+90
View File
@@ -0,0 +1,90 @@
# M2 — nativer wgpu-2D-Renderer im Tauri-Fenster: Ansatzwahl
> Ziel: den bereits fluessig laufenden standalone `render2d`-Renderer aus dem
> ECHTEN Tauri-Prozess heraus mit nativer GPU-Performance anzeigen — nicht in der
> WebKitGTK-Webview (Perf-Flaschenhals), nicht in einem Chromium-Workaround.
> Maschine: Linux, Wayland, AMD.
## Gewaehlter Ansatz: **B — separates natives Fenster (winit) im Tauri-Prozess**
Der Tauri-Hauptthread haelt weiterhin die GTK-Hauptschleife + das Webview-Fenster
(HTML-Chrome). Aus dem Tauri-`setup`-Hook startet der Prozess auf einem
Hintergrund-Thread ein **eigenes natives winit-Fenster** mit eigener wgpu-Surface,
das die `render2d`-Demo-Szene (konkaves L + Raum + Papier-mm-Linien) rendert,
pan-/zoombar. Renderer/Szene werden NICHT reimplementiert — es ist exakt der Code
des verifizierten standalone `spike`-Bins (`render2d::gpu::Renderer` +
`render2d::demo_scene`).
### Warum B (und was gegen A/C spricht)
- **Kein geteilter Wayland-Surface → keine Contention/Flicker.** winit spricht auf
Linux DIREKT Wayland/X11 (Crates `wayland-client`/`x11rb`), es benutzt **kein
GTK**. Das native Fenster bekommt eine eigene, von WebKitGTK voellig getrennte
Wayland-Surface. Genau das umgeht das dokumentierte Problem aus der Vorrecherche
(Roh-wgpu-Surface UNTER der Webview im selben Fenster → Flicker/Schwarzbild auf
Wayland).
- **Zwei Fenster-Stacks, aber KEIN Event-Loop-Konflikt.** tao (Tauri) fuehrt eine
GTK-Hauptschleife auf dem Hauptthread; winit fuehrt eine eigene Schleife.
winit 0.30 erlaubt die Event-Loop explizit auf einem Nicht-Haupt-Thread via
`EventLoopBuilderExtWayland::with_any_thread(true)` (X11-Pendant analog). Der
native Renderer laeuft daher auf `std::thread` „cad-native2d", der Tauri-/GTK-
Hauptthread bleibt frei. Da winit nicht an GTK gebunden ist, kollidieren die
beiden Schleifen nicht (getrennte Stacks, getrennte fds).
- **Ansatz A (wgpu-Surface im SELBEN Tauri-Fenster, Unter-/Overlay):** verworfen.
taos `raw_window_handle_rwh_06()` liefert auf Wayland den `WaylandWindowHandle`
der GTK-`ApplicationWindow` — also DIESELBE Surface, in die WebKitGTK
komponiert. Eine zweite wgpu-Surface darauf ist genau die Contention, die die
Vorrecherche als flackernd/schwarz auf diesem Setup markiert hat. Hoechstes
Wayland-Risiko, fuer den Spike ungeeignet.
- **Ansatz C (wgpu als Haupt-Surface, HTML nur duennes Overlay):** verworfen fuer
den Spike. Sauberste Perf-Story, aber groesster Umbau: die ganze App muesste auf
eine winit/tao-getriebene Haupt-Schleife umziehen und die Tauri-Webview zur
Nebenrolle degradiert werden. Kein „minimaler realer Spike" mehr. Bleibt die
Ziel-Endarchitektur-Option, aber nach B.
## Crates / Versionen (verifiziert aus der Lock-Datei)
| Rolle | Crate | Version |
|---|---|---|
| Tauri | `tauri` / `tauri-runtime-wry` | 2.11.5 / 2.11.4 |
| Fenster (Webview-Seite) | `tao` | 0.35.3 |
| Webview | `wry` | 0.55.1 |
| Natives Renderfenster | `winit` | 0.30.13 |
| GPU | `wgpu` / `wgpu-hal` | 22.1.0 / 22.0.0 |
| Handle-Bruecke | `raw-window-handle` | **0.6.2 (identisch fuer tao UND wgpu/winit)** |
| Renderer | `render2d` (Pfad) | 0.1.0, Feature `window` |
Der entscheidende Kompatibilitaets-Glueckstreffer: tao 0.35 und wgpu 22/winit 0.30
teilen sich `raw-window-handle 0.6.2` — keine rwh-Versionsbruecke noetig. (Fuer
Ansatz B wird der tao-Handle gar nicht gebraucht, weil das native Fenster eine
eigene winit-Surface hat; die Versions-Parität ist aber die Voraussetzung fuer ein
spaeteres A/Compositing, falls je gewuenscht.)
## Wayland-Caveats (fuer den Start auf dieser Maschine)
- **`WEBKIT_DISABLE_DMABUF_RENDERER=1`** bleibt fuer die Webview-Sichtbarkeit auf
diesem Wayland/AMD-Setup noetig (Vorrecherche). Betrifft die WebKitGTK-Seite,
nicht das wgpu-Fenster.
- Das native winit-Fenster laeuft ueber Vulkan; GLES/EGL-Fallback wurde standalone
ebenfalls funktionierend gesehen. `WGPU_BACKEND=gl` erzwingt bei Vulkan-Zicken
den GL-Pfad.
- `with_any_thread(true)` ist zwingend — ohne die Freigabe panict winit, weil es
die Event-Loop sonst nur auf dem Hauptthread zulaesst (den haelt hier GTK).
## Bau-Isolierung
Alles hinter Cargo-Feature **`native2d`** in `cad-tauri` (opt-in). Der normale
Build (`cargo build`, `npm run tauri:dev`) zieht weder winit noch wgpu und bleibt
unveraendert. `render2d` bleibt ohne Tauri-/GTK-Abhaengigkeiten headless baubar
und testbar.
## Naechster Schritt Richtung Vollbild-App (2D-Viewport + HTML-Chrome)
1. Szene aus dem echten Modell speisen: `Plan.primitives` (TS) → serde-`Scene` →
ueber einen Tauri-`emit`/Command an den native2d-Thread (statt `demo_scene`).
2. Pan/Zoom-State zwischen Webview-Chrome und native2d-Fenster synchronisieren
(Tauri-Events beidseitig).
3. Fenster-Kopplung: das native Fenster als Kind/als angedockten Bereich neben der
Webview positionieren (Layout), spaeter ggf. Umstieg auf Ansatz C, wenn ein
einziges Fenster gefordert ist.
+1 -1
View File
@@ -15,7 +15,7 @@
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%232f6df6'/%3E%3Cg fill='white'%3E%3Crect x='3' y='3' width='4' height='4'/%3E%3Crect x='9' y='3' width='4' height='4'/%3E%3Crect x='3' y='9' width='4' height='4'/%3E%3Crect x='9' y='9' width='4' height='4'/%3E%3C/g%3E%3C/svg%3E"
/>
<title>Dossier</title>
<title>cad — Phase 0 Spike</title>
</head>
<body>
<div id="root"></div>
+9 -341
View File
@@ -1,27 +1,20 @@
{
"name": "dossier",
"name": "cad",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dossier",
"name": "cad",
"version": "0.0.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@mlightcad/libredwg-web": "^0.7.7",
"@tauri-apps/api": "^2.11.1",
"@tauri-apps/plugin-dialog": "^2.7.1",
"@tauri-apps/plugin-fs": "^2.5.1",
"@tauri-apps/plugin-http": "^2.5.9",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-shell": "^2.3.5",
"@tauri-apps/plugin-updater": "^2.10.1",
"delaunator": "^5.1.0",
"dxf-parser": "^1.1.2",
"jspdf": "^4.2.1",
"jszip": "^3.10.1",
"pdfjs-dist": "^6.2.108",
"opencascade.js": "^1.1.1",
"polygon-clipping": "^0.15.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -875,271 +868,6 @@
"node": ">=20"
}
},
"node_modules/@napi-rs/canvas": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.3.tgz",
"integrity": "sha512-OlI657a5XXvKGFX7kNeIzJ8rO7IXt87Mqu2H8rXE46viAuOfum/JA7ysX7+eBhxNKznT+RCZh418mndlcFX3+w==",
"license": "MIT",
"optional": true,
"workspaces": [
"e2e/*"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"optionalDependencies": {
"@napi-rs/canvas-android-arm64": "1.0.3",
"@napi-rs/canvas-darwin-arm64": "1.0.3",
"@napi-rs/canvas-darwin-x64": "1.0.3",
"@napi-rs/canvas-linux-arm-gnueabihf": "1.0.3",
"@napi-rs/canvas-linux-arm64-gnu": "1.0.3",
"@napi-rs/canvas-linux-arm64-musl": "1.0.3",
"@napi-rs/canvas-linux-riscv64-gnu": "1.0.3",
"@napi-rs/canvas-linux-x64-gnu": "1.0.3",
"@napi-rs/canvas-linux-x64-musl": "1.0.3",
"@napi-rs/canvas-win32-arm64-msvc": "1.0.3",
"@napi-rs/canvas-win32-x64-msvc": "1.0.3"
}
},
"node_modules/@napi-rs/canvas-android-arm64": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.3.tgz",
"integrity": "sha512-7kSCdUhoXiO+AaIMXdBGdtp6EctZNkmF62Rea/BmVQlwKaM3bBhOzyGUzxyxz9dv5vdBfpyAaxhSRSJF4kqK4A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-darwin-arm64": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.3.tgz",
"integrity": "sha512-ds14V1BPagLszQyaDTeggny5fNeTCqsUQ5QhFj9VDxSEfzrVxXtdbR0LoFyKa0Siaaw8KvqSk4t7k/WoZJwvbg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-darwin-x64": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.3.tgz",
"integrity": "sha512-qof3LRAAycmkV2I1izZo9RoSHF8kCQr5O05sFwv0jK8rSdYV6KHVwimo6Qb7RxZj40WHKbLHm5JDaUF0o5XUAA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.3.tgz",
"integrity": "sha512-FU2kKZLmolHA9+KcUA+l1+xH3WTLUUTQDU/kLv9SEUr2TrRPu94aytOeizFJDHPs/QBcw4QL1mCQhetQXYBbag==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.3.tgz",
"integrity": "sha512-GVSjntxKeA+/y/ZKf1F+cmUw1WeIkE5aMRPqnZUlBTBvBcrvgWccJAWuYCKPX4QJQwZILIIwhgdAbl51yj6fpA==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.3.tgz",
"integrity": "sha512-J51oK/axyZ13kxycumSMfLiDZMdWdOVvqDFI28BpuViZHE3A0bQfr8B5vg8YnPEnqLD3BSn1hkdlh2buspEcNQ==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.3.tgz",
"integrity": "sha512-CtQgQjoVTX67jS9XuCTtJ40Sl7wRLMguoFnnGnfDmCWf7kzKFZVwj5ynqUOIGKFMSB61ZCuQlwPvVNxYTTseaw==",
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.3.tgz",
"integrity": "sha512-jtfzAHFp+FRaR7zGT4jyCe6wUgAG/dVb5A4Apd8FY9jKarntDfUAlJXscugiH7ZF5kKnu7/lHFk9LaDPcrGEVQ==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-x64-musl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.3.tgz",
"integrity": "sha512-xTzaUCKUHTY4bCGadeeRZggbRVbGUT1petg7Z8r9AJR2+D9Bqu6nQAgqBGC6D47tA70LjaaaLTrJ7wNY1T74dg==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-win32-arm64-msvc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.3.tgz",
"integrity": "sha512-ktVLuBkI6QVOm5BwO/WbdGwxgeetAMJa7TTmR8qBarXF0OU2NKjvjUtPJAl2y8t+zBRczJl/1VOl9gua6WcK2g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.3.tgz",
"integrity": "sha512-SGhlQ8bDjL1Cz2KnsKMasr/5sTcwG/SZkB6WCJxLsmSm/3aS2C+3p39bA7iZ2/94+NkVDySZfbiGoaSZSFHYxA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
@@ -2114,60 +1842,6 @@
"node": ">= 10"
}
},
"node_modules/@tauri-apps/plugin-dialog": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz",
"integrity": "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.11.0"
}
},
"node_modules/@tauri-apps/plugin-fs": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.5.1.tgz",
"integrity": "sha512-9Lz+Jopp6QyeEWhlpkMx4R/+P9HgR+AVAI4vOZhlT8Xaymtz8iVI/Ov984/XTqgJz/5gz5NretqPB/XEMS3NhQ==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.11.0"
}
},
"node_modules/@tauri-apps/plugin-http": {
"version": "2.5.9",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-http/-/plugin-http-2.5.9.tgz",
"integrity": "sha512-lCiY0+vs4HvIUSvZrBs8TC3TiCB0MOPRmiUjTq4prW7SlcJE2jdLeT6KBsJrT9Tlplufl7W1pY6SFAO3gCWxDA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.11.0"
}
},
"node_modules/@tauri-apps/plugin-process": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.1.tgz",
"integrity": "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.8.0"
}
},
"node_modules/@tauri-apps/plugin-shell": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz",
"integrity": "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.10.1"
}
},
"node_modules/@tauri-apps/plugin-updater": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.10.1.tgz",
"integrity": "sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.10.1"
}
},
"node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
@@ -3520,6 +3194,12 @@
"node": ">=12.20.0"
}
},
"node_modules/opencascade.js": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/opencascade.js/-/opencascade.js-1.1.1.tgz",
"integrity": "sha512-lw6/vOl86+CkJ8d3V01mlbGAC0A49gc1HbwGcqGeKjk5SGRLiF15jyUuA8aYEvizcPNTu4Ta4A+Ut2DJgsa7AQ==",
"license": "LGPL-2.1-only"
},
"node_modules/pako": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz",
@@ -3543,18 +3223,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/pdfjs-dist": {
"version": "6.2.108",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-6.2.108.tgz",
"integrity": "sha512-YxFb+SQcodN2rnX9Tn3dHYlqfb7NjlzzfONPpJd+AKoKtUjEdevTfbC07d5TcczzOK6261auRkP/M8OBHs9vFQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=22.13.0 || >=24"
},
"optionalDependencies": {
"@napi-rs/canvas": "^1.0.0"
}
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+3 -21
View File
@@ -1,10 +1,8 @@
{
"name": "dossier",
"name": "cad",
"private": true,
"version": "0.0.0",
"type": "module",
"license": "AGPL-3.0-or-later",
"author": "Karim Gabriele Varano",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
@@ -18,25 +16,16 @@
"shell": "scripts/chromium-shell.sh",
"electron": "scripts/electron-shell.sh",
"build:engine": "wasm-pack build src-tauri/render2d --release --target web --out-dir ../../src/engine/pkg --out-name render2d --no-default-features --features web",
"build:engine3d": "wasm-pack build src-tauri/render3d --release --target web --out-dir ../../src/engine/pkg3d --out-name render3d --no-default-features --features web",
"build:kernel2d": "wasm-pack build src-tauri/kernel2d --release --target web --out-dir ../../src/engine/pkgKernel2d --out-name kernel2d --no-default-features --features web",
"build:dwgimport": "wasm-pack build src-tauri/dwgimport --release --target web --out-dir ../../src/engine/pkgDwgImport --out-name dwgimport --no-default-features --features web",
"build:truck": "wasm-pack build src-tauri/trucksolid --release --target web --out-dir ../../src/engine/pkgTruck --out-name trucksolid --no-default-features --features web"
"build:engine3d": "wasm-pack build src-tauri/render3d --release --target web --out-dir ../../src/engine/pkg3d --out-name render3d --no-default-features --features web"
},
"dependencies": {
"@mlightcad/libredwg-web": "^0.7.7",
"@tauri-apps/api": "^2.11.1",
"@tauri-apps/plugin-dialog": "^2.7.1",
"@tauri-apps/plugin-fs": "^2.5.1",
"@tauri-apps/plugin-http": "^2.5.9",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-shell": "^2.3.5",
"@tauri-apps/plugin-updater": "^2.10.1",
"delaunator": "^5.1.0",
"dxf-parser": "^1.1.2",
"jspdf": "^4.2.1",
"jszip": "^3.10.1",
"pdfjs-dist": "^6.2.108",
"opencascade.js": "^1.1.1",
"polygon-clipping": "^0.15.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -56,12 +45,5 @@
"vite": "^5.4.8",
"vitest": "^4.1.9",
"wasm-pack": "^0.15.0"
},
"allowScripts": {
"core-js@3.49.0": true,
"esbuild@0.21.5": true,
"fsevents@2.3.3": true,
"fsevents@2.3.2": true,
"wasm-pack@0.15.0": true
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 KiB

+2 -14
View File
@@ -1,7 +1,7 @@
{
"source": "ambientCG (ambientcg.com)",
"license": "CC0 1.0 Universal (Public Domain)",
"resolution": "2K",
"resolution": "1K",
"materials": [
{
"id": "Concrete048",
@@ -61,18 +61,6 @@
"ao": "/assets/materials/Bricks104/ao.jpg"
}
},
{
"id": "RoofingTiles013A",
"name": "Dachziegel",
"category": "Roofing Tiles",
"maps": {
"color": "/assets/materials/RoofingTiles013A/color.jpg",
"normal": "/assets/materials/RoofingTiles013A/normal.jpg",
"roughness": "/assets/materials/RoofingTiles013A/roughness.jpg",
"displacement": "/assets/materials/RoofingTiles013A/displacement.jpg",
"ao": "/assets/materials/RoofingTiles013A/ao.jpg"
}
},
{
"id": "Tiles141",
"name": "Bodenfliesen",
@@ -156,4 +144,4 @@
}
}
]
}
}
-93
View File
@@ -1,93 +0,0 @@
Copyright 2020 The Archivo Project Authors (https://github.com/Omnibus-Type/Archivo)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-93
View File
@@ -1,93 +0,0 @@
Copyright 2014 The DM Sans Project Authors (https://github.com/googlefonts/dm-fonts)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-93
View File
@@ -1,93 +0,0 @@
Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
-93
View File
@@ -1,93 +0,0 @@
Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
-93
View File
@@ -1,93 +0,0 @@
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-93
View File
@@ -1,93 +0,0 @@
Copyright 2020 The Jost Project Authors (https://github.com/indestructible-type)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-93
View File
@@ -1,93 +0,0 @@
Copyright 2019 The Karla Project Authors (https://github.com/googlefonts/karla)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-93
View File
@@ -1,93 +0,0 @@
Copyright 2018 The Manrope Project Authors (https://github.com/sharanda/manrope)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
-93
View File
@@ -1,93 +0,0 @@
Copyright 2021 The Outfit Project Authors (https://github.com/Outfitio/Outfit-Fonts)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
-93
View File
@@ -1,93 +0,0 @@
Copyright 2015 The Public Sans Project Authors (https://github.com/uswds/public-sans)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
-93
View File
@@ -1,93 +0,0 @@
Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-93
View File
@@ -1,93 +0,0 @@
Copyright 2010-2020 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
-93
View File
@@ -1,93 +0,0 @@
Copyright 2014 The Source Serif 4 Project Authors (https://github.com/adobe-fonts/source-serif)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Some files were not shown because too many files have changed in this diff Show More