Compare commits

..

24 Commits

Author SHA1 Message Date
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
253 changed files with 4604 additions and 41256 deletions
+1 -1
View File
@@ -57,7 +57,7 @@ Rendern angewandt, nie in die Geometrie eingebacken.
## Arbeitsweise (für Beiträge) ## Arbeitsweise (für Beiträge)
- Substanzielle, mehrstufige Arbeit schrittweise in isolierten Schritten angehen. - Substanzielle, mehrstufige Arbeit an **Subagenten** delegieren, wo möglich.
- Änderungen verifizieren: `npx tsc -b`, `npm run build`, und Screenshot via - Änderungen verifizieren: `npx tsc -b`, `npm run build`, und Screenshot via
`node scripts/probe.mjs` (schreibt `scripts/probe.png`) bzw. `probe-ff*.mjs` für `node scripts/probe.mjs` (schreibt `scripts/probe.png`) bzw. `probe-ff*.mjs` für
Firefox-Fälle. Screenshot ansehen und Geometrie visuell prüfen. Firefox-Fälle. Screenshot ansehen und Geometrie visuell prüfen.
+794 -65
View File
@@ -1,91 +1,820 @@
# HANDOVER — Stand 2026-07-04 (Schraffur/Linien-Epic + Folgethemen) # HANDOVER — Browser-BIM (cad), Standalone-Port von DOSSIER
Übergabe an eine frische Instanz. Alles unten Gelistete ist committet, sofern nicht anders vermerkt. Ältere Handover-Stände (< 2026-07-04) siehe git-Historie. Zuerst `CONVENTIONS.md` lesen. > Für die nächste Instanz. Stand: 2026-06-29. Lies zuerst
> `CONVENTIONS.md`, `ROADMAP.md` und die Projektnotizen (siehe unten).
> **➡️ Aufgaben-Queue steht in `PENDENZEN.md`** (Single Source of Truth, priorisierte Checkliste + Arbeitsprotokoll). HANDOVER = nur noch Kontext/Konventionen/Environment/Zielmodelle, KEIN Backlog mehr. Neue Pendenzen/Rückfragen dort eintragen, nicht hier. ## >>> COMMIT-REGEL (verbindlich, IMMER beachten) <<<
## ⚠️ NACHTRAG 2026-07-04 abends — Unterbruch, 3 Slices verloren Dieses Repo darf **keinerlei Hinweise auf KI-Werkzeuge** enthalten — weder im
Code/Doku noch in der Git-Historie.
> **STAND-UPDATE (Session 4, neues Gerät):** **Environment behoben** — Toolchain vorhanden (`node`/`npm`/`cargo` unter `/usr/bin`, `wasm-pack` via `npx` 0.15.0; es war die Flatpak-Sandbox); fehlendes Rust-Target per `sudo dnf install rust-std-static-wasm32-unknown-unknown` (+ lld) nachinstalliert; beide WASM-Engines gebaut → App läuft. **Slice 1 (Locked-Iso `cb8fae5`) + Slice 2 (Joins Phase 1c `c5a344d`) committet.** **Slice 3 (Öffnungen als Boolean-Löcher + Deckentrim-nur-3D) ✅ committet `1407c68`:** Fenster/Türen = 1 Wandkörper pro Schicht-Band mit rechteckigen `holes` (render3d stanzt sie per achsparalleler Rechteck-Gitter-Zerlegung + 4 Laibungsquads/Loch), Segment-Boxen weg; `holes`/`openings` schliessen sich im Emitter aus; Schnitt-Pfad (layered=false) segmentiert unverändert weiter. Deckentrim `trimWallTopForCeilings` gilt nur noch im 3D-Pfad, Schnitt-Pfad bekommt volle Wandhöhe. Alle Datei-Edits waren gelandet — unabhängig verifiziert: cargo test 56 (inkl. beider Pflicht-Tests versetzt-überlappende Fenster + Tür-Loch-bis-Boden), vitest 230, build:engine3d + tsc sauber, Trace clean. **Damit sind alle drei verlorenen 3b-Slices rekonstruiert.** **Memory-Direktiven `priority-based-joins.md` + `openings-as-booleans.md` wiederhergestellt.** - **Commit-Messages:** sachlich, in der Sprache des Projekts. **NIEMALS**
> `Co-Authored-By:`-Trailer, „Generated with …"-Zeilen, Tool-/Modellnamen oder
> **⚠️ PUSH BLOCKIERT:** keine Git-Credentials auf diesem Gerät (kein Helper, keine `~/.git-credentials`, kein SSH-Key; Remote HTTPS `git.openbureau.ch/karim/DOSSIER-STANDALONE`). `cb8fae5` + `c5a344d` (+ folgender Öffnungen-Commit) sind **lokal committet, NICHT gepusht** (`ahead of origin/master`). Sobald Gitea-PAT (repo-write) oder SSH-Key da ist: `git push origin master`. Bis dahin holt das andere Gerät die Arbeit nicht. sonstige Urheber-Hinweise auf einen Assistenten.
> - **Dateien/Kommentare:** keine Erwähnung von Assistenten, Modellen oder Agenten.
> Der folgende Block bleibt als historischer Kontext/Vorlage für den offenen Slice 3 stehen. 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.
Die Arbeit wurde unerwartet unterbrochen — mehrere Tasks liefen parallel. Diese Übergabe wurde nachträglich rekonstruiert. **Der tatsächliche HEAD-Commit ist `03f0c40`** (Nordstern-3D Live-Schnittebene) — alles danach existierte nur im Arbeitsbaum der alten Maschine und ist auf **diesem** Checkout (neues Gerät, `git status` sauber bis auf `package-lock.json`) **nicht vorhanden**. Diese drei Slices waren in echter Arbeit (nicht nur geplant) und gelten als verloren — neu beauftragen, nicht nur „prüfen ob gelandet": ## >>> KOORDINATION & MEMORY (bei mehreren Instanzen/Agents) <<<
1. **Locked-Iso-Fix** (`src/viewport/Wasm3DViewport.tsx`, `OrbitState` um `ortho: boolean` + `orthoHalfHeight: number` erweitern, `orbitCamera()`/Preset-Effect/Pan/Zoom entsprechend anpassen) — Bug: Iso-Ansicht kippt bei der ersten Kamerabewegung sofort in Perspektive statt orthografisch (parallel) zu bleiben. War fertig gebaut und quantitativ verifiziert (Parallelogramm-Kantenvergleich vor/nach Orbit, 225/225 Tests grün), aber **nicht mehr committet**, bevor der Unterbruch kam. Kleinster, unabhängigster der drei Fixes — zuerst neu machen. **Mehrere Hauptinstanzen gleichzeitig:**
2. **Joins Phase 1c — Durchgangswand echt aufbrechen** (`spanCutouts` in `src/model/joins.ts` + `src/plan/generatePlan.ts` + Rust-Parität `src-tauri/geometry/src/lib.rs`): Nutzer-Befund nach Phase 1b war, dass der Innenputz der DURCHGANGSWAND am T-Stoss nur überdeckt wird (Zeichenreihenfolge), nicht geometrisch ausgeschnitten — Fugen-/Nahflächenlinien laufen noch durch den Backstein-Durchstoss, es „liest" noch nicht wie ein einziger Join. Das war die **explizite Priorität des Nutzers** für diese Session. Die Umsetzung war weit fortgeschritten (schrieb bereits in `joins.ts`), dann abgebrochen. **Nicht committet, nicht im Baum.** Vollständige Aufgabenbeschreibung mit allen Datei:Zeile-Ankern steht im Transkript bei „Agent:Phase 1c: Durchgangswand aufbrechen" — 1:1 als Vorlage für den Neu-Auftrag wiederverwenden. - Git: nur **eine** Instanz committet/pusht auf `master`, ODER jede arbeitet auf
3. **Öffnungen als echte Boolean-Löcher** (`RWall.holes`, `src/plan/toWalls3d.ts`, `src-tauri/render3d/src/mesh.rs`, additiv `types.rs`): EIN Wandkörper mit rechteckigen Löchern statt der heutigen Pfeiler/Brüstung/Sturz-Ersatzkörper (sichtbare Segment-Nähte, keine echten Löcher). Pflicht-Testfall (Nutzer-Direktive, Kernmotivation des Slices): zwei Fenster in derselben Wand mit überlappendem Achsen-Intervall, aber unterschiedlicher Höhe (z. B. u=[1.0,2.0]/z=[0.9,2.1] und u=[1.5,2.5]/z=[0.3,0.7]) — die alte Segment-Zerlegung kann das nicht abbilden, echte Löcher schon. Zusatz-Auftrag im selben Slice: `trimWallTopForCeilings` (Phase-3-Deckentrim, Commit `e4b8df6`) darf **nur** im 3D-Viewer-Pfad (`layeredWalls:true`) gelten, **nicht** im Schnitt-Pfad (`layeredWalls:false`) — der Schnitt braucht die volle, ungetrimmte Wandhöhe für seine eigene schichtweise Prioritäts-Subtraktion (`subtractDominantBands`), sonst zeigt der Schnitt die Wände fälschlich abgeschnitten. Fortschritt bei Abbruch **unklar** (letztes Status-Update deutete auf „noch in der Kartierungsphase" hin) — vermutlich am wenigsten weit von den dreien; komplett neu beauftragen. Vollständiger Auftragstext im Transkript bei „Agent:Oeffnungen als Boolean-Loecher" + die zwei Zusatz-Direktiven (Decken-Trim-Fix, Pflicht-Testfall versetzte Fenster) kurz danach. 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.
**Zwei Engine-Direktiven vom alten Stand fehlen** (`priority-based-joins.md`, `openings-as-booleans.md`): die in Punkt 2/3 beschriebenen Zielverhalten waren gesichert (u. a. „Trennlinien an Merge-Kontakten verschwinden immer", „Öffnungen dürfen nie wieder zu Segmenten vereinfacht werden"). Beim Neu-Beauftragen diese Direktiven wieder explizit festhalten. **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.
**Environment-Lücke auf diesem Gerät:** `node`/`npm`/`cargo`/`wasm-pack` waren zum Zeitpunkt dieser Übergabe **nicht auf dem PATH** (geprüft: kein Treffer, auch nicht unter der VSCodium-Flatpak-Datenlage). **Vermutliche Ursache: VSCodium lief als Flatpak** — Flatpak-Sandboxing blendet system-installierte Toolchains (Node/Rust unter `/usr/...` oder `~/.cargo`) typischerweise aus, auch wenn sie auf dem Host tatsächlich installiert sind. Der Nutzer wechselt gerade auf **natives VSCodium** (kein Flatpak mehr) — das könnte die Lücke von selbst beheben, MUSS aber nach dem Wechsel neu geprüft werden (`which node npm cargo wasm-pack`), bevor man von "Toolchain fehlt" auf "Toolchain neu installieren" schließt. Erst wenn nach dem Wechsel auf natives VSCodium immer noch nichts gefunden wird, wirklich neu installieren (Node LTS + Rust via rustup + `wasm-pack`, `npm install`, einmal `cargo build --manifest-path src-tauri/render3d/Cargo.toml` durchlaufen lassen). Ohne Toolchain ist die in `CONVENTIONS.md` vorgeschriebene Verifikation (`tsc`/`vitest`/`cargo test`/`wasm-pack`) nicht möglich — das zuerst klären, bevor einer der drei Slices neu beauftragt wird, sonst baut ein Agent blind ohne Verifikation. **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).
**Aufräum-Hinweis:** `Edit MEMORY md Added 1.txt` (lag unversioniert im Repo-Root) und die lokale `package-lock.json`-Änderung waren dirty/untracked. Die txt-Datei gehört nicht ins Repo — außerhalb verschieben oder löschen. `package-lock.json` prüfen (`git diff package-lock.json`) und nur committen, falls die Dependency-Änderung beabsichtigt ist. ## >>> AUFGABE FÜR NEUE INSTANZ: 2D-Plan auf WebGL-GPU-Renderer (ZUERST) <<<
### Empfohlene Reihenfolge zum Wiederaufsetzen **Warum:** Tauri-Linux-Webview = **WebKitGTK**, das SVG/2D auf der **CPU (Cairo)** rastert.
1. Nach dem Wechsel auf natives VSCodium zuerst `which node npm cargo wasm-pack` prüfen — evtl. war es nur die Flatpak-Sandbox, die die Toolchain versteckt hat. Nur falls immer noch nichts gefunden wird: Node + Rust + wasm-pack installieren + `npm install`. Blocker für alles Weitere. User-GPU (AMD RX 7800 XT) langweilt sich; bei **144 Hz** (~6,9 ms/Frame) ruckelt Pan/Zoom/
2. Locked-Iso-Fix neu bauen (klein, unabhängig, Spezifikation im Transkript vollständig vorhanden). **Objekt-Bewegen** sichtbar. three.js/WebGL (3D) läuft GPU-beschleunigt → NUR der 2D-SVG-Pfad
3. Joins Phase 1c (Nutzer-Priorität dieser Session) — Direktive zuerst wieder ins Memory schreiben, dann Agent mit dem Auftragstext aus dem Transkript beauftragen. ist CPU-gebunden. Entscheidung mit User: 2D-Plan **von SVG auf WebGL2 (GPU)** umbauen.
4. Öffnungen als Boolean-Löcher + Deckentrim-nur-3D-Zusatz (dieselbe Rust-Crate wie Phase 1c — nacheinander, nicht parallel, wegen Datei-Overlap in `mesh.rs`/`toWalls3d.ts`).
5. Repo aufräumen (txt-Datei raus, `package-lock.json` klären), danach diesen Abschnitt auf „erledigt" aktualisieren.
## NACHTRAG Session 3 (2026-07-04 mittags) — Prioritaets-Joins + Schnitt-Sanierung **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.
**Prioritäts-Join-Epic (Kern-Direktive, siehe unten „Verschneidungs-Zielmodell"):** Joins werden nach `Component.joinPriority` materialbewusst aufgelöst — konsistent in Grundriss, Schnitt UND 3D. Gelandet: **Architektur (mit User abgestimmt):**
- `e1698b7` **Wand-T-Verbindungen** (T-Knoten 3 Enden + Mittelspannen-Stoss) in `computeJoins`; generatePlan wendet die Cuts unverändert an. `0ddf99d` Rust-Parität (geometry-Crate). `d74dda1` Sample: Querwand **W9** (neuer Wandtyp `iw` Innenputz/Backstein/Innenputz) als T-Demo, Tür/Fenster beiseite gerückt. - **Hybrid:** WebGL2-Canvas rendert die schwere Geometrie (Poché-Polygone, Linien, Schraffuren,
- `fc0a76f` **Phase 1 materialbewusst**: `resolveJoinPriority` (merge/coexist/trim), `WallCuts.layerCuts` (per-Schicht-Cuts + L-Seitenlinien), Backstein-Kern verschmilzt, Putz bricht als L. `bf97d50` **Phase 1b**: Kern sticht nur durch den Nah-Putz und stoppt an der Rückgrat-Nahfläche (mergeCut bei `offT + sign*(tT/2 nearPlaster)`), KEINE Achs-Überlappung; Naht entfällt über stroke:none/noStrokeEdges/Zeichenreihenfolge. Rust jeweils synchron. Kreise/Bögen als Segmente). **Dünne SVG-Ebene DARÜBER** behält Text (Raumstempel/Labels),
- `e4b8df6` **Phase 3 (3D)**: Wand wird an der Deckenunterkante getrimmt, wenn eine Decke höherer joinPriority bündig aufliegt (`trimWallTopForCeilings` in toWalls3d, BBox-Footprint-Näherung dokumentiert) → kein Z-Fighting Wand/Decke. 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.
**Schnitt-Sanierung (Regressionen aus der 3D-Schicht-Umstellung `de7a26b` behoben):** **Fixpunkte im Code:**
- `da39f0c` per-Schicht-3D-Bänder nutzten die RECHTE Normale statt `leftNormal` → Schichten in 3D+Schnitt gespiegelt (Backstein aussen). Eine Zeile in `pushSegment` gedreht. - `src/plan/PlanView.tsx` (~2500 Z.): `toScreen()` = Modell-m → viewBox-Einheiten, FIXER Ursprung,
- `6c47355` Schnitt bekam durch die per-Schicht-Boxen segments×layers Cut-Polygone → Owner-Index-Versatz → einheitliches Diagonal-Muster. Fix: `projectToModel3d(project, { layeredWalls:false })` für `computeSection` (Voll-Box pro Wand); 3D-Viewer behält Default true. `PX_PER_M=90`; `view={x,y,w,h}` treibt `<svg viewBox>`; Pan = setView, Zoom = setView(onWheel).
- `5ed3549` **Alle Schraffuren monochrom**: HATCH_INK `#0f0f0f` / HATCH_PAPER `#f0f0f0` (resolveHatch erzwingt Ink; Albedo-Fallback im Schnitt entfernt). Print-Mono separat/unangetastet. BEREITS optimiert: `primitiveEls`/`drawingRunEls` `useMemo` (Pan/Zoom 312→0 Re-Renders) +
- `c36a7e0` Wandrelative Muster drehen im Schnitt mit der Wand (`SECTION_WALL_AXIS_ANGLE_DEG=90` an resolveHatch) → Dämmung horizontal quer zur Dicke. 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.
**Sonstiges:** `1855717` Renderer-Umschalter aus der Statusleiste in die **Settings**, Default **Nordstern** (WebGL2 nur explizit/Fallback). `32497bc` TopBar: Zoom-Cluster von Grid auf Flex (Phantom-Zeile +6px weg, jetzt bündig mit Zweizeilern), Bar 56→60px. **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).
**IN FLIGHT bei Checkpoint:** (1) **3D-Live-Schnitt** (Opus-Agent): Clip-Plane-Uniform + discard in MESH/GRID_WGSL, `section_fill.rs` Cut-Caps aus `cut_section` mit prozeduraler 45°-Schraffur (CAP_WGSL), `set_section_plane` (web.rs cached walls/slabs), Viewport-Toggle; WASM-Rebuild nötig. (2) **Joins Phase 2** (Opus-Agent): Merge-Regel (gleiche Prio+Komponente) in der Schnitt-Dominanz `subtractDominantBands`. — `git status` prüfen, verifizieren, committen. **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.
**NÄCHSTER Engine-Slice (Nutzer-Direktive, queued):** **Öffnungen als echte Boolean-Löcher** — Fenster/Türen sind heute Pfeiler/Brüstung/Sturz-Ersatzkörper (keine CSG, Triangulator lochfrei) → sichtbare Segment-Nähte. Ziel: EIN Wandkörper, Löcher via earcut-with-holes + Laibungsflächen; behebt auch die dokumentierte mesh↔cut_section-Öffnungs-Inkonsistenz. Läuft im selben Crate wie der 3D-Schnitt → erst danach starten. ## >>> STAND 2026-07-01 (Tauri-PoC — App-frei, gelandet) <<<
**Verschneidungs-Zielmodell (Nutzer, verbindlich):** gleiche Komponente+Priorität = verschmelzen (Naht weg); schwächere Schicht bricht auf und schliesst als L; höhere Priorität schneidet bauteilübergreifend (Decke>Wand); Joins IMMER in allen drei Sichten identisch. Schraffur-Modell: Schnitt-Schraffur vs. Oberflächen-Schraffur, aktuell ALLE monochrom fg #0f0f0f auf bg #f0f0f0. **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`.
## NACHTRAG Session 2 (2026-07-04 nachmittags/abends) — TopBar-Redesign + 3D-Editierbarkeit - **`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.
**TopBar/UI gelandet:** DOSSIER-Wortmarke + Ressourcen-Icon links (Rhino-Stil), Massstab/Zoom-Cluster bereinigt (Duplikat weg, Zoom-Aktionen unter Massstab-Dropdown gestapelt, gleiche Breite, Ring bei „am Massstab" statt Flächen-Aufhellung), PDF/DXF raus → **Datei-Burger-Menü** rechts (Speichern/Öffnen als echter Projekt-JSON-Download/-Upload, Import, Export). **Einstellungs-Fenster** (`SettingsDialog.tsx`): Accent-Palette (`src/theme/accents.ts`), Auswahlrahmen-Farbe (Default Yuyake #CEB188) + Snap-Farbe (vorläufig **Sora #5FA1C9** gewählt — „aki"-Rückfrage bleibt offen, per Picker änderbar), Projekt-MüM-Feld (`project.referenceElevationMasl`, nur gespeichert, Draping folgt). **Custom Fenstersteuerung** (_/□/X) für Electron (`electron-preload.cjs` + IPC, `WindowControls.tsx`, TopBar = Drag-Region). **Dark-Theme ist jetzt fester Standard** (vorher an OS-`prefers-color-scheme` gekoppelt → zeigte fälschlich Light; Light bleibt Opt-in via `:root[data-theme="light"]`). Alle TopBar-Pillen auf dieselbe stets-dunkle Kontextmenü-Fläche vereinheitlicht; Panel-Köpfe flach (kein hellerer Balken) + feste 40px-Höhe. **Undo/Redo** (`historySlice.ts`, Ctrl+Z/Y, Coalescing für Drags). **15 gebündelte OFL-Schriften** (`public/fonts/`, `src/text/fonts.css`) statt Systemfonts. **Farbfelder**: Hex separat editierbar (`ColorHexField.tsx`). **Text-Zeilenhöhe-Regler** ersetzt „+ Text" (Text wird eigenes Werkzeug, AUDIT B1). **Linienstil** im Attribute-Panel editierbar. 2D-Zoom-Grenzen stark erweitert (20000×/0.005×). **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`.
**3D-Editierbarkeit gelandet (Nordstern/render3d, NICHT three.js — Free=three.js-Viewer, Premium=editierbarer Nordstern, siehe Memory):** ## >>> STAND 2026-07-01 (Parametric Walls Phase A — ISOLIERT) <<<
- **R1** `85a721f` Boden-Referenzraster auf y=baseElevation des aktiven Geschosses, ein-/ausschaltbar (Overlay-Button, `viewSlice.grid3dVisible`; eigene LineList-Pipeline `grid.rs`, `set_ground_grid`). Kamera: Links=Orbit, **Mitte/Rechts=Pan** (vorher Pan nur auf Shift+Mitte versteckt), Rad=Zoom-to-Cursor.
- **R2** `6ef57b6` View-Styles **wireframe** + **hidden-line** + Fill-Light (`set_render_style`, `edges.rs` = Feature-Edge-Extraktion mit Crease-Dedup, Hidden-Line via Depth-Bias-Flächen + Kanten obenauf). `textured` → vorerst wie shaded (keine Textur-Pipeline).
- **R3** `ac0d9ef` **Klick-Auswahl** im 3D (`raycast3d.ts`: OBB-Wand-/Prisma-Slab-Schnitt → Store-Selektion → Attribute+Objekt-Info-Panel). `toWalls3d.pickGeometry()` mit wallId/ceilingId.
- **R4** `fabf97a` **Auswahl-Highlight**: orange Outline, immer sichtbar (No-Depth-Linien-Pipeline `set_highlight_lines`, `selectionHighlightLines()`).
- **R5** `02bd261` **Materialfarben** im 3D: Wände/Decken in Farbe der dicksten Schicht statt Einheitsgrau.
- **weiter:** `8bc472a` 2D **z-Anordnen** (Kontextmenü nach vorn/hinten, `reorderDrawings`); `de7a26b` **per-Schicht Wand-Bänder** (jede Materiallage als eigener farbiger Quader, wallId bleibt → Pick/Highlight ganze Wand); `e42c336` **AUDIT A6 Bauteil-Schedule-CSV** (Datei-Menü, `exportSchedule.ts`); `7dd324d` **Glasscheiben in Fenstern** (A5-Teil, RMesh in jeder Fensteröffnung).
- Alle Runden **visuell im Browser verifiziert** (Playwright + WebGPU-Flags: `--enable-unsafe-webgpu --enable-features=Vulkan --use-gl=angle --use-angle=vulkan --no-sandbox`, URL `?engine=wasm`, Iso-View klicken).
**3D-REST (engine-schwer, bewusst NICHT blind gemacht — mit Nutzer angehen):** **Neue Instanz (Opus) — Parallelisierung mit Agents (Sonnet):**
- **Schnitt-Schraffuren** (2D-Poché/Schraffur auf 3D-Schnittflächen; `section.rs`, ~66KB, komplex). - **Branch:** `feature/parametric-walls` (isoliert, keine App.tsx-Änderungen)
- **Textur-/PBR-Pipeline** (Sampler/Bindings/UV in wgpu; dann `textured`-Style + `Component.texture3d`/Material echt rendern). - **Phase A FERTIG (alle Agents grün):**
- **Wand-Schicht-Bänder** in 3D (Option B aus R5: jede Materiallage als eigener extrudierter Teilquader statt nur repräsentativer Farbe — pro-Schicht-Farben liegen in `RWall.layers` schon vor). - **Agent 1:** Types (`ParametricWall`, 5 `ParametricRule` Varianten) + Engine
- **Ortho-Ray-Picking** (aktuell perspektivischer Pick-Strahl; für Front/Top/Side-Presets vor der ersten Navigation leicht ungenau). (`resolveParametricWall()`, 5 rule handlers, helpers) in `src/model/parametricWalls.ts`
- **3D-Griffe/Editieren** (Wand-Endpunkte im 3D ziehen etc. — bisher nur Auswahl + Panel-Edit). - **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`).
WASM-Workflow: `src/engine/pkg3d/` ist gitignore't → nach Rust-Änderung `npm run build:engine3d`, nur die `.rs` committen. DOSSIER-Audit-Details jetzt in `docs/design/dossier-feature-audit.md` (A1A6/B1B4/C/D/E) + `ROADMAP.md` §11. **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.
## Konventionen (ZWINGEND — zuerst lesen) - **Backlog #1 ERLEDIGT (Befehlsleiste-Transform vereinheitlicht):** Die laufende
- **Keine Fremd-Tool-Spuren** im Repo: vor jedem Commit Trace-Scan (`git diff | grep -iE "co-authored|generated with"`) → muss leer sein. Commits deutsch, sachlich, KEINE Co-Authored-By-Trailer. Auswahl-Transformation (nur noch `rotate`/Taste `d` mit U/I/O/P; move/mirror
- **NICHT ANFASSEN (fremde WIP):** `.gitignore`, `public/assets/materials/manifest.json`, `src/materials/library.ts`, `scripts/fetch-materials.mjs` — bleiben dauerhaft dirty, nie stagen. laufen längst über die Engine) speist jetzt die `CommandLine` statt der
- **Verifikation:** `tsc --noEmit` clean + `vitest run` + trace-scan vor jedem Commit. Nur Task-Dateien stagen (Kollisionen: parallele Tasks disjunkte Datei-Lanes geben; i18n exklusiv einem Task). schwebenden `TransformBar`. Dritter Zweig in der `<CommandLine>`-Ternäre parallel
- **Engine = „Nordstern"** (render2d/render3d/WASM). Referenz DOSSIER-Rhino: `https://git.openbureau.ch/karim/dossier` (Alias git.kgva.ch), geklont `/tmp/dossier-ref`. 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).
## In dieser Session gelandet (Auswahl Commits) ## >>> STAND 2026-07-01 (Abend) — Raum + Topbar-Umbau + Backlog — ZUERST lesen <<<
Schraffur/Linien-Epic komplett: Typmodell (vector/image Hatch, dash/zigzag/custom Line, Component Vordergrund/Hintergrund), ResourceManager **Master-Detail** (Schraffuren/Linien/Wandstile/Deckenstile, `9695748`), Rendering neuer Typen (Bild-`<pattern>`, random modellraum-verankert, Zickzack, Custom-Motiv-Editor `src/ui/MotifEditor.tsx`), modulares Linien-Segment-System (Strich/Punkt/Lücke), Attribut Vordergrund/Hintergrund + **By-Layer/By-Object-Quellen** (Nach Ebene/Bauteil/eigener Wert für Vordergrund/Hintergrund/Strichstärke/Schraffur, `d02781e`).
Weiter: Gehrung spitze Winkel (2D+Schnitt+Rust), TopBar/Footer-Reorg, Zoom-% am Massstab, Tool-Shortcuts 1..0, Verschiebe-Dreieck flacher + Snap, Statusleiste „Nordstern", ResourceManager als **floating nicht-modales Fenster**, **swisstopo** Gebäude(extrudiert)+Terrain-Mesh in 3D (`f22970f`), Schnitt-Wandschicht-Orientierung (`7569524`), **Schnitt-Boolean-Dominanz** nach joinPriority (`23d81be`).
## Backlog & Rückfragen → `PENDENZEN.md` Alle unten genannten Agenten GRÜN gelandet und gegatet (`tsc -b` 0 Fehler,
Der frühere „Offener Backlog"/„Offene Rückfragen"/„IN FLIGHT"-Abschnitt ist vollständig nach `PENDENZEN.md` migriert (Single Source of Truth). Dort steht auch das Arbeitsprotokoll. `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).
## Verifikation **Diese Session gelandet:**
`npx tsc --noEmit` + `npx vitest run` (zuletzt 152 grün) + trace-scan vor jedem Commit. - **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 ~15431548) 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` ~416449).
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 1314px, 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 1623,
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 #13 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).
-142
View File
@@ -1,142 +0,0 @@
# PENDENZEN — Aufgaben-Queue (Single Source of Truth)
> **Diese Datei ist die einzige verbindliche Aufgabenliste.** HANDOVER.md enthält nur
> noch Kontext/Konventionen/Environment/Zielmodelle — keinen Backlog mehr.
## Arbeitsprotokoll (ZWINGEND)
**Bearbeiter:**
1. **Zuerst diese Datei lesen**, dann `CONVENTIONS.md` + verlinkte Detailquellen des Items.
2. Oberstes nicht-blockiertes Item aus **🔧 In Arbeit** bzw. sonst **⏭️ Als Nächstes** nehmen, **fertig** machen, verifizieren (`npx tsc --noEmit` + `npx vitest run` + trace-scan), abhaken, eine Ergebniszeile nach **✅ Erledigt** schreiben (mit Commit-Hash, sobald committet).
3. **Rückfrage-Recht:** Bei Unklarheit/Design-Entscheid NICHT raten — Item mit `❓` markieren, kurze Frage darunter notieren, nächstes freies Item nehmen. Fragen sammeln sich unter **❓ Offene Rückfragen**.
4. **Kein Arbeitsschritt endet ohne aktualisierte Liste.** Status hier ist immer aktuell, sonst driftet es wieder.
5. Bearbeiter **committet nicht selbst** und delegiert Unteraufgaben nicht weiter.
**Planer** (mit dem Nutzer, kuratiert):
- Nimmt Nutzer-Feedback, pflegt daraus die Liste (rein/umpriorisieren/aufspalten). Führt selbst keine Queue-Items aus.
- Prüft **❓**-Zeilen mit dem Nutzer, wandelt sie in konkrete Items.
---
## 📅 Geplant
- [ ] **truck-Integration (Profil-Extrusion / B-Rep)****Start: 2026-07-07 (Dienstag, nach Reset).** Nutzer zeichnet 2D-Querschnitt (L-Profil, T-Träger, Freiform) → truck-Extrusion → 3D-Körper + Boolean gegen Wand/Decke. Voraussetzung: truck-Booleans stabil + WASM-Integration. Umfang: ~46 Wochen. Mit Nutzer Scope/MVP klären vor Start.
## ⛔ Blocker (zuerst klären)
- [x] ~~**Toolchain prüfen**~~**ERLEDIGT 2026-07-04 (macOS-Gerät):** `node`/`npm`/`cargo` (1.96.0)/`wasm-pack` (0.15.0) alle vorhanden. `npm install` + `npm approve-scripts esbuild wasm-pack …` (npm gated Install-Scripts → `allowScripts`-Block in `package.json`, ungetrackt gelassen), `rustup target add wasm32-unknown-unknown`, beide WASM-Engines gebaut, Frontend gebaut, **`npm run tauri:build` → Mac-App (`cad.app` + `cad_0.1.0_aarch64.dmg`, arm64) läuft.** Verifikations-Baseline grün: `tsc --noEmit` sauber, `vitest run` 230/230, `cargo test render3d` 56/56. **Kein Blocker mehr für Engine-Slices auf diesem Gerät.**
## 🔧 In Arbeit
- [ ] **kernel2d-Port nach Rust/WASM** (Plan: [PORT_PLAN.md](PORT_PLAN.md), Crate `src-tauri/kernel2d`, TS-Referenz bleibt `kernel2d.ts`, Differential-Harness `src/geometry/kernel2d.parity.test.ts`). Fortschritt:
- [x] Phase 1: Crate-Skelett + WASM-Fassade + `build:kernel2d``a78e7c7`
- [x] Phase 2: Primitive/Schnitt/Fläche/Kreis + Batch-Fassaden + Diff-Harness (Zufall+Golden) — `9e2c521`
- [x] Phase 3: Offset (Miter + 1e-9-Fallback) + Fillet — `c8ea2bf`
- [x] Phase 4: Trim/Split/Join (`trimPolyline`, `splitAtIntersections`, `joinChains` — Löwenanteil) — `28471c1`
- [x] Phase 5: `roomArea`-Flächen/`ceiling`/`stair`/`roomBoundary` portiert (vitest 263, 33 Parity) — `a608a59`. **Offen in Phase 5:** `opening` (mit Aufrufstellen-Änderung) — noch nicht portiert.
- [ ] Phase 6: TS-Fassade umstellen (alt → `kernel2d.legacy.ts`), Suite grün, `npm run build` + WASM sauber. **Achtung Laufzeit-Entscheid:** macht die Live-App synchron WASM-abhängig (Init + Per-Call-Marshalling) — heute nutzt die App KEINE WASM-Geometrie zur Laufzeit; vor Umstellung mit Nutzer klären.
- **Join-Durchstich verworfen (2026-07-05, gemessen):** `computeJoins` live auf WASM zu legen lohnt NICHT. Benchmark TS vs. WASM inkl. JSON-Marshalling (Median ms/Aufruf, 200 Iter): 6 W → TS 0.018/WASM 0.022 · 20 → 0.027/0.043 · 50 → 0.082/0.102 · 120 → 0.331/0.246 · 300 → 1.68/0.67. Crossover erst ~100+ Wände; realistische Plan-/Geschossmengen liegen darunter → TS schneller, und selbst 300 Wände sind mit TS 1,7 ms (nicht wahrnehmbar). Marshalling frisst den Rust-Vorteil, plus dauerhafte Rust↔TS-Paritätspflicht. **Fazit:** reines TS behalten; WASM für Joins nicht weiterverfolgen. WASM lohnt erst bei echten Rechen-Hotspots (Booleans/Tessellierung).
## ⚠️ Zu prüfen (evtl. schon erledigt / Status unklar)
- [x] ~~**Snap an Wand-Schichttrennlinien**~~**gelandet `339202b`** (`wallLayerBoundarySegments` in `src/tools/snapping.ts`), verifiziert vorhanden.
## ⏭️ Als Nächstes
- [ ] **Ribbon-UI + modulare Bars** (Nutzer-Vision 2026-07-05, bestätigt: Tab-Schema **2D·3D·BIM·Ansichten**). Voller Plan + datengetriebene Architektur: **[docs/design/ribbon-ui-plan.md](docs/design/ribbon-ui-plan.md)**. Ribbon-Oberleiste mit Tabs ersetzt die Werkzeug-Sidebar; datengetriebene Registry (RibbonItem = tool|command|action) ermöglicht auch eine **modulare Custom-Bar** (gleiche Items, vom Nutzer gewählt). Attribute bekommen volle Höhe, Objektinfo darunter gemergt; XYZ-Box oben rechts bleibt. **Phasen:** ~~(1) Gerüst + 2D/BIM-Tab~~ ✅ (`9d6e86d`), ~~(2) TopBar → Ansichten-Tab mergen~~ ✅ (`85011cb`), ~~(2b) Tabs in die TopBar-Zeile~~ ✅ (`456ebc8`), ~~(2c) OCS-Chrome (kleine Wortmarke + Quick-Access-Icons statt Burger, Zeile 26px)~~ ✅ (`4e3b074`), ~~(2d) Text + Ansichten auf eine Leiste, „Ansichten" als Standard-Tab zuerst~~ ✅ (`fe22cbf`), (3) **teilweise** ✅: Werkzeug-Sidebar aus Default-Layout raus + Attribute volle linke Höhe + Wandtyp-/Deckentyp-Picker ins Attribute-Panel verschoben (Nutzer-Entscheid), `LAYOUT_VERSION`→8; ~~**offen:** Objektinfo unter Attribute mergen~~ ✅ (`3a2cef3`): element-spezifische Abschnitte (Wand/Decke/Öffnung/Treppe/Raum) + Wand-Referenzlinie ins Attribute-Panel verschoben; ObjectInfo trägt nur noch Bezugspunkt + Masse (Nutzer-Wunsch). ~~(4) modulare Custom-Bar~~ ✅ (Tab „Eigene" + -Picker, localStorage-persistiert). **Offen:** 3D-Tab füllen (aktuell leer; ggf. 3D-spezifisch statt Doppelung mit Ansichten), Band-Höhe/Abstände + 26px-Zeile visuell im Tauri abnehmen, Dauer-Zoom-Anzeige (Statusleiste?) klären. ✅ Vorarbeit: Eigenschaften-Grid im OCS-Stil (`00733d8`), Kreis+Bogen-Werkzeuge (`e454eab`/`bd2b12b`). **Nächster Schritt: im Tauri visuell prüfen (Band-Höhe, Icons, Aktiv-Highlight), dann Phase 2/3.**
- [x] ~~**SPIKE — Bild-Texturen in `render3d`**~~**erledigt `0ca3b1d`** (verifiziert 2026-07-05): `RenderStyle::Textured` real, prozedurales 256×256-Schachbrett (kein Asset/`image`-Crate), UVs planar in Metern, Textur-Bind-Group group 1, `MESH_TEXTURED_WGSL` (gleiche Beleuchtung, Albedo aus `textureSample`), `spike3d` per `T` umschaltbar. Alt-Vertexpfad `[pos,normal,color]` bitgleich (Regressionstest). `cargo test` 58 grün (59 mit `--features render`, inkl. naga-Test); `spike3d`-Build sauber; keine neuen Deps, Default-Build unverändert. Auftrag: [SPIKE_TEXTUR_render3d.md](SPIKE_TEXTUR_render3d.md). **Lücken bis „richtig gutes" Texturing → siehe 3D-REST unten.**
## 📋 Backlog (Priorität grob absteigend)
- [x] ~~**Zeichenwerkzeuge ergänzen: Kreis + Bogen.**~~**komplett erledigt (2026-07-05):** beide Werkzeuge + Center-/Quadrant-Snaps + WebGL-Sichtbarkeitsfix (`2c8ad8f`). Details in den Unterpunkten:
- [x] ~~**Kreis-Toolbar** (trivial)~~**erledigt `e454eab` (2026-07-05):** `ToolId`+`"circle"`, Platzhalter `circleTool` (nicht floorOnly), `TOOL_COMMAND`+`TOOL_ORDER`, Kreis-Icon in `ToolsPanel`, i18n `tool.circle`/`tool.circle.hint`. tsc + Suite 331 grün. (Kreise rendern seit `4ac99d3` glatt als `<circle>`.)
- [x] ~~**Bogen-Werkzeug** (mittel)~~**erledigt (2026-07-05):** `arcCommand` in `src/commands/cmds/arc.ts` (3-Klick: Mittelpunkt → Start/Radius → Endwinkel, CCW; Vorschau via `arcPts`/`circlePts`), registriert in `registry.ts` (Alias `a`/`bogen`), `"arc"` als ToolId + Toolbar-Eintrag (Bogen-Icon) + i18n. tsc + Suite 331 grün. ✅ **Center-/Quadrant-Snaps ergänzt (2026-07-05):** `collectCircles` + Snap-Block in `snapping.ts` — Mittelpunkt + Quadranten (Kreis: alle 4; Bogen: nur im Spannbereich) unter der `center`-Einstellung, Bogen-Endpunkte unter `endpoint`; +3 Tests (Suite 334).
- [ ] **BAUTEILE aufs Rhino-Niveau heben (Treppe/Fenster/Tür).** Vergleich Rhino-Plugin ↔ TS + priorisierte Ansätze: **[RESEARCH_BAUTEILE_RHINO.md](RESEARCH_BAUTEILE_RHINO.md)**. ✅ **Gruppe A (2D, Items 16) komplett — verifiziert 2026-07-05:** (1) Treppe-Outline (`stairOutline`, gerade/L/Wendel, `generatePlan.ts:2186`), (2) Fenster-Brüstungslinie (`window-sill`, gepunktet, `sillHeight>0`, `:1810`), (3) Tür-Sturzlinien (`door-lintel` + `lintelLines` keine/innen/aussen/beide, gestrichelt, `:1711`), (4) Treppe-Referenz links/mitte/rechts, (5) Fenster-Flügel-Mittelpfosten, (6) Tür `wandoeffnung`. **Offen:** Gruppe B (2D mittel: Tür-Schwung am Rahmen, Treppen-Pfeil-Style `filled`, Fenster/Tür-Presets, `swing_invert`) — Feinpolish. ✅ **Fenster-Anschlag-Striche erledigt** (`8d688b9`, Laibungsstriche quer zur Wand bei „fein", analog Tür; +2 Tests). Gruppe C (3D: Rahmen/Blatt/Glas/Sims als Mesh — wartet auf Mesh-/B-Rep-Pipeline). **Der große Rest ist „Schnitt- vs. Ansichts-Darstellung" (eigenes Item unten).**
- [ ] **DWG/DXF-Import via `acadrust` (weiterbauen).** ✅ Spike `763a558`: `acadrust` 0.4 (MPL-2.0, pure Rust) **baut zu wasm32** (Crate `src-tauri/dwgimport`, 839 KB), parst DXF aus Byte-Buffer (`DxfReader::from_reader`+`Cursor`), headless getestet. **Offen:** (1) Entity→DOSSIER-Modell-Mapping (LINE/ARC/… → Wand/Öffnung — die eigentliche Domainarbeit, Wochen), (2) Datei-Upload-Glue im Browser (`<input type=file>`→Uint8Array→`parse_dxf_summary_json`, trivial), (3) DWG-binär (`DwgReader::from_reader` analog, aber R13R2018-Korrektheit unverifiziert), (4) WASM-Größe (nalgebra Haupttreiber). **Klarstellung:** der TS-DXF/DWG-Import (`parseDxf`/`parseDwg`/`dxfToDrawings`) + Upload-UI (`App.tsx`, `ImportDialog.tsx`) existieren längst und funktionieren — der acadrust-Weg wäre eine Rust-Neuimplementierung des Lesens (nur DWG-**Schreiben** ist eine echte Lücke). ✅ **2887794 (2026-07-05): Kurven-Abdeckungslücke geschlossen**`parseDxf` deckt jetzt ARC/CIRCLE/ELLIPSE (tesselliert zu Konturen, Winkel Radiant, voller Umlauf geschlossen) zusätzlich zu LINE/LWPOLYLINE/POLYLINE/MESH ab; 5 Tests, volle Suite 307 grün. ✅ **c481373 (2026-07-05): SPLINE + INSERT ergänzt**`parseDxf` wertet SPLINE als echte B-Spline (De Boor, Grad/Knoten; Fallback fitPoints/Kontrollpolygon) aus und expandiert INSERT-Block-Referenzen (2D-Transform Scale/Rotation/Basispunkt + MINSERT-Array + verschachtelte Blöcke, Tiefe ≤8) zu transformierten Konturen; Kontur-Dispatch in gemeinsamen `collectContours` refaktoriert; +7 Tests, volle Suite 314 grün. **Bekannte Grenzen:** rationale SPLINE-Gewichte ignoriert (dxf-parser liefert sie nicht); Block-interne MESH/3DFACE-Entities werden im 2D-Import nicht expandiert. ✅ **c29f27e (2026-07-05): HATCH ergänzt** — dxf-parser hat KEINEN HATCH-Handler (verwarf HATCH stumm); Lösung via `registerEntityHandler` + eigenem `HatchHandler` (sammelt rohe Gruppencodes) + testbarer `hatchContours`-Auswertung: Randpfade (Polyline-Pfade + Linien-/Bogen-Kanten, Bögen über vorhandene Tessellierung) → geschlossene Konturen mit `Contour.filled`; `contoursToDrawings` macht daraus gefüllte `polyline`-Drawing2D (fillColor-Default, restylebar). +6 Tests, Suite 320 grün. ✅ **05bc5aa (2026-07-05): HATCH-Ellipse/Spline-Kanten** ergänzt (Kantentyp 3/4 tesselliert; B-Spline-Sampling in `sampleBSpline` extrahiert). ✅ **4b93ac9 (2026-07-05): TEXT/MTEXT**`parseDxf` liefert `DxfImportResult.texts` (`ImportedText`: Position/Höhe-in-Metern/Winkel-Radiant; MTEXT-Formatcodes grob gesäubert); `textsToDrawings``{shape:"text"}`-Drawing2D; **Darstellung neu**: `addDrawing2D` emittiert ein schlankes `kind:"drawingText"`-Primitiv, PlanView rendert es rein per SVG (modellverankert, Rotation; GPU-Guard so, dass es in ALLEN Renderer-Modi im SVG bleibt); `toRenderScene` überspringt es; ImportDialog zählt/importiert Texte. +5 Tests, Suite 327 grün. **Bekannte Grenzen HATCH:** Bulges an Polyline-Rändern als Sehne; Insel-Loops = eigene Ringe (keine echten Löcher). **Bekannte Grenzen TEXT:** importierte Texte (pointerEvents:none) noch nicht per Canvas-Klick selektierbar; MTEXT-Feinformatierung flachgeklopft; Block-interne TEXT/MTEXT nicht expandiert. **Text im Tauri visuell abgenommen (Nutzer 2026-07-05)** — auch gedreht korrekt. ✅ **4ac99d3: CIRCLE/ARC als echte glatte Formen**`Contour.curve` trägt die wahre Kreis-/Bogen-Geometrie (pts bleiben für Kontext/3D); `contoursToDrawings` baut `{shape:"circle"|"arc"}`; neue Primitive `drawingCircle` (SVG `<circle>`) + `drawingArc` (SVG-Bogenpfad), `toRenderScene` tesselliert sie für den nativen Pfad; +4 Tests, Suite 331. Ellipse bleibt tesselliert (kein Ellipsen-Primitiv). **Weiter offen:** Entity→Wand-Semantik (die dicke Domainarbeit); DWG-Schreiben (einzige echte Export-Lücke).
- [ ] **STRATEGIE — „von BIM-Tool zu echtem CAD".** Direkt am Quellcode studierte Referenzen (OpenCADStudio/truck/acadrust) + Web-Import-Landkarte → konkrete, priorisierte Ansätze in **[RESEARCH_CAD_APPROACHES.md](RESEARCH_CAD_APPROACHES.md)**. Kern: (1) generisches Entity-Modell + Trait-Dispatch, (2) modeless Command-System (`StepInput`-Funnel + Kommandozeile), (3) DWG/DXF-Round-Trip via `acadrust` (MPL-2.0, pure Rust, WASM-tauglich), (4) volle Object-Snap-Schicht, (5) 3D-B-Rep später selektiv via `truck` (Apache-2.0, Geometrie-Crates WASM-fähig, Booleans/Fillets noch instabil). Der `kernel2d`-Rust/WASM-Kurs ist damit bestätigt. **Mit Nutzer priorisieren, welcher Ansatz zuerst.**
- [ ] **Schnitt- vs. Ansichts-Darstellung nach Schnitthöhe (BIM-Standard).****Phase 1 (Wand unter Schnittebene) erledigt (2026-07-05):** `generatePlan.addWallPoche` hat einen `viewOnly`-Zweig — erreicht eine Wand die Grundriss-Schnitthöhe nicht (`wall.height < floor.cutHeight`, z. B. 0.3-m-Brüstung bei 1 m), wird sie nur als Ansichts-Umriss (Haarlinie, `fill:"none"`, keine Schraffur) gezeichnet statt als Schnitt-Poché; normale Wände (≥ Schnitthöhe) unberührt. Segment-/Gehrungs-/Öffnungs-Logik geteilt. +2 Tests (`generatePlan.viewwall.test.ts`, Suite 336). ✅ **Phase 2 (Decke über Ebene = gestrichelte Überkopf-Linie) erledigt (2026-07-05, `39ddd9b`):** der freie Decken-Umriss (Überstände/Balkone, wo keine Wand verdeckt) ist jetzt gestrichelte Haarlinie (`OVERHEAD_DASH`) statt kräftiger Volllinie — BIM-Konvention „Aufsicht auf Bauteil über einem". Decken-FLÄCHE war schon Ansicht (viewHatchId, weiß). +1 Test. **Offen (Phase 3):** per-Component View-/Cut-Weight + eigene View-Schraffur (Slot-Trennung); cut/Aufsicht/Untersicht (Deckenspiegel/Reflected Ceiling Plan); Unterzüge; Feinheiten. Kern-Idee (Nutzer): jedes Bauteil bekommt getrennt eine **Schnittlinie** (kräftig, z. B. 0.250.35 mm, + Schnitt-Poché) und eine **Ansichtslinie** (Haarlinie); WELCHE gilt, entscheidet die z-Ausdehnung des Bauteils vs. die **Schnitthöhe** des Grundrisses (Default ~1 m):
- Bauteil wird von der Schnittebene GESCHNITTEN → Schnittlinie + Schnitt-Poché (heutiges Wandverhalten).
- Bauteil liegt ganz UNTER der Ebene (z. B. 30-cm-Wand bei 1 m Schnitthöhe, Brüstung, Podest) → nur „von oben gesehen" = **Ansichtslinie/Haarlinie, KEINE Schnitt-Poché**. ← genau der vom Nutzer genannte Fall.
- Bauteil liegt ganz ÜBER der Ebene (Decke/Slab, Unterzug) → Ansicht, üblicherweise **gestrichelte** Überkopf-Haarlinie.
- **Passt konsistent zum bereits existierenden `viewHatchId` (Ansichts-Schraffur) vs. Schnitt-Schraffur** — die Linienstärke-Dualität (View-/Cut-Weight je Component) ist die natürliche Erweiterung derselben Logik.
- **Nicht nur cut/view, sondern cut / AUFSICHT / UNTERSICHT (Nutzer):** ein Bauteil sieht von oben anders aus als von unten. Der Grundriss (Blick nach unten) zeigt Bauteile UNTER der Ebene in **Aufsicht** (Oberseite); ein **Deckenspiegel/Reflected Ceiling Plan** (Blick nach oben) zeigt Bauteile ÜBER der Ebene in **Untersicht** (Unterseite — z. B. Kassettendecke, Leuchten). Also je Component potenziell **drei** Darstellungs-Slots (Schnitt / Aufsicht / Untersicht) × {Schraffur + Linienstärke}. Das heutige `viewHatchId` ist faktisch EIN View-Slot und vermischt Auf-/Untersicht; sauber wäre die Trennung. WELCHER Slot gilt, entscheidet die **Blickrichtung der Sicht** (Grundriss ↓ / Deckenspiegel ↑) UND die z-Lage relativ zur Schnittebene.
- **Verallgemeinert den Decken-Footprint-Clip** (`a2f6923`): „Decke unter Wand verdeckt" ist ein Spezialfall von „Ansichtsbauteil vs. schneidende/überdeckende Bauteile".
- Aufwand: mittelgroß, phasenweise machbar (1: z-Extent-vs-Schnitthöhe-Klassifikation cut/above/below; 2: Ansichtslinie-Weight je Component + Haarlinie/gestrichelt; 3: 30-cm-Wände & Slabs verdrahten). Nicht zu komplex im Konzept — es ist der reguläre CAD/BIM-Weg (ArchiCAD/Vectorworks/Revit). Mit Nutzer Detailgrade/Defaults festlegen.
- [x] ~~**Ebene-Schraffur editierbar**~~**gelandet `dcb6ed5`**: Kategorie-Dialog (`App.tsx`, `editor.hatch`-Feld) hat `<select>` auf `cat.hatch` + `HatchSwatch`-Vorschau. Verifiziert vorhanden.
- [ ] **GEO-BLOCK** (gemeinsame Dateien io/geoContext/swissTopo/terrain/ContextImportDialog/Viewport3D/siteSlice):
- Reale Höhen + **Projekt-MüM** (EG-Referenzhöhe): Terrain georeferenziert bei realem z relativ dazu, Gebäude auf Terrain drapiert (heute alles z=0).
- **Luftbild/SWISSIMAGE**-Orthofoto als Textur aufs Terrain-Mesh.
- Importierte Geo-Elemente auf **aktives Geschoss** (`viewSlice.activeLevelId`) + Gelände-Ebene.
- **Nordstern-Geo-Rendering:** importierte Meshes (heute nur three.js `importedMesh`/`terrainMesh`) auch in `projectToModel3d` einspeisen.
- **3D-Mesh-DXF/DWG-Import** (heute DXF nur 2D); Building-Draping; höhere DTM-Auflösung.
- [x] ~~**ResourceManager Bauteile-Tab** auf Master-Detail~~**bereits Master-Detail** (`ComponentsTab`/`ComponentDetail` in `src/ui/ResourceManager.tsx`, Liste links `res-md-list` / Detail rechts). Verifiziert vorhanden.
- [ ] **Einstellungs-Fenster (Rest):** Verdrahtung ist **da** (`viewSlice.snapColor`/`marqueeColor` → PlanView `SnapMarker`/Marquee, Defaults aus `theme/accents.ts`, Projekt-MüM-Feld `referenceElevationMasl`). **Offen bleibt nur:** Snap/Endpunkt-Default = ❓„aki" (s.u., derzeit Sora #5FA1C9) — reine Farbentscheidung des Nutzers.
- [ ] **Bildschraffur:** ambientCG/CGI-Colorfiles als Quelle (Material-Lib WIP — erst nach Freigabe); Bild-Filter Sättigung/Helligkeit/Kontrast/SW (`image.filters`).
- [ ] **E2b Schraffur-Kachel-Motiv** (MotifEditor für HatchStyle-Tile wiederverwenden).
- [x] ~~**Linienstile aufräumen**~~**erledigt (2026-07-05):** die drei funktional identischen 0.13-Volllinien (`thin`/`hatch-line`/`joint-massive` — alle weight 0.13, `dash:null`, gleiche Farbe) auf EINE kanonische „Volllinie 0.13" (`thin`) zusammengeführt; alle weight-only-Stile klar als „Volllinie X.XX" benannt; Schraffur-Referenzen (`hatch-line``thin`, inkl. `ResourceManager.patToHatches`) + Schichtfugen (`joint-massive``thin`) remappt; `hatch-dash` (einzige gestrichelte) bleibt. Ids stabil gelassen (geladene Projekte + interne Refs bleiben heil; Rendering-Gewichte unverändert). 7→5 Stile. tsc + Suite 331 grün.
- [ ] 2D-Plan z-Anordnen (Kombo-Schraffuren); Bild-Schraffur GL/DXF (heute Fallback).
- [ ] **AUDIT (DOSSIER-Studie):** A1 Override-Regel-Engine; A2→A3 View-Snapshots → Print-Layout-Blätter (PDF pro Blatt); A5 reichere Öffnungen; B1 Text-Werkzeug (+ Kreis-Tool → Shortcuts 1&3); A4 Tragwerk; B2 Object-Info numerisch. (A6 Bauteil-Schedule-CSV erledigt.) Belege in `/tmp/dossier-ref/rhino/*.py`.
- [ ] Elemente im Schnitt anwählbar; render3d 2D-Schraffur auf 3D-Flächen.
- [ ] **TEAMWORK — kollaboratives Bearbeiten (Supabase self-hosted).** Projekte auf einem self-hosted Supabase-Stack speichern; mehrere Nutzer bearbeiten dasselbe Projekt gleichzeitig. Kernkonzept: **pessimistisches Object-Locking** (wie Revit Worksharing / ArchiCAD Teamwork) — alle Objekte sind zunächst gesperrt; ein Nutzer *reserviert* die Elemente, die er bearbeiten möchte (exklusiver Schreibzugriff), und *gibt sie frei*, sobald er fertig ist. Freigabe → sofort für alle anderen sichtbar via Supabase Realtime (WebSocket-Kanal). Kein Merge-Konflikt nötig, weil niemals zwei Nutzer dasselbe Objekt gleichzeitig schreiben. Grobe Schichten:
- **Auth + Projekt-Liste:** Supabase Auth (Email/Magic-Link), Projektübersicht, Öffnen/Schliessen.
- **Lock-Service:** Tabelle `object_locks (project_id, object_id, user_id, locked_at)` mit Row-Level Security; `reserveObjects(ids[])` / `releaseObjects(ids[])` als Supabase-RPC; Optimistisches Check-in via DB-Constraint (doppelte Reserve → Fehler → UI-Feedback).
- **Realtime-Sync:** Supabase Realtime-Channel pro Projekt; bei Freigabe werden die veränderten Objekte (JSON-Patch oder ganzes Objekt-Payload, TBD) gepusht; lokaler Store merged incoming changes sofort.
- **Presence:** Wer ist online, wer hat welche Objekte reserviert (farbige User-Badges an reservierten Elementen im Plan).
- **Offline-Guard:** Beim Verbindungsabbruch Locks automatisch nach Timeout freigeben (DB-seitig: `locked_at + interval` prüfen).
- **Umfang/Granularität TBD mit Nutzer:** Object = Wand/Raum/Drawing2D-Element? Geschoss? Layer? Feinere Granularität = mehr Parallelarbeit, aber komplexeres UI.
- **Aufwand:** gross (24 Wochen echte Arbeit). Erst sinnvoll, wenn Kern-CAD-Features stabil. Mit Nutzer Granularität + Hosting-Setup klären bevor Implementierung startet.
### 3D-REST (engine-schwer, bewusst NICHT blind — mit Nutzer angehen)
- [ ] **Textur-/PBR-Pipeline** (Sampler/Bindings/UV in wgpu; dann `textured`-Style + `Component.texture3d`/Material echt rendern). ✅ **Erster Durchstich erledigt** (`0ca3b1d`, Schachbrett-Textur auf Wänden, group-1-Bind-Group, `MESH_TEXTURED_WGSL`, planare Meter-UVs). **Verbleibende Lücken bis „richtig gut" (Grobschätzung ~1829 PT):** Bild-Datei-Laden (`image`-Crate, PNG/JPG→GPU) ~12 · Material→Textur-Zuordnung (Wandtyp/Layer→Material→Textur-Set, Cache/Atlas oder Bind-Group je Material) ~35 · Normal-/Roughness-/Metallic-Maps + Cook-Torrance-BRDF + Tangentenraum ~58 · Mipmaps (Blit-Pass) + anisotropes Filtern ~12 · Web-Pfad (`web.rs`/WebGPU, Bild-Upload via ImageBitmap) ~34 · UI zum Zuweisen + Persistenz im Dokumentmodell ~58.
- [x] ~~**Wand-Schicht-Bänder in 3D** Option B~~**bereits umgesetzt** in `resolveWallBands(layered=true)` (`src/plan/toWalls3d.ts:236-241`): 3D-Viewer-Pfad liefert je Materiallage ein eigenes `WallBand` (Dicke + Component-Albedo + Normalen-Versatz), `pushSegment` emittiert jede Lage als eigene Voll-Box. `dominantLayerColor` ist nur noch Fallback für den Schnitt-Einkörper-Pfad (`layered=false`). Verifiziert per Code-Lesung.
- [ ] **Ortho-Ray-Picking** (aktuell perspektivischer Pick-Strahl; Front/Top/Side-Presets vor erster Navigation leicht ungenau).
- [ ] **3D-Griffe/Editieren** (Wand-Endpunkte im 3D ziehen etc. — bisher nur Auswahl + Panel-Edit).
## ❓ Offene Rückfragen (an den Nutzer)
- [x] ~~**„aki"** Snap/Endpunkt-Farbe~~**entschieden 2026-07-04: Sora #5FA1C9** als endgültiger Default festgeschrieben (`src/theme/accents.ts` `DEFAULT_SNAP_COLOR`, Doc aktualisiert). Frage geschlossen.
- [ ] Floating-ResourceManager „headless" = ganz ohne Titelleiste? (aktuell MIT)
- [ ] Geo-Block: Projekt-MüM zuerst oder Nordstern-Geo-Rendering?
- [ ] Verifizieren/klären: Isometrie „echte" orthographische Iso (teilw. durch Locked-Iso `cb8fae5` adressiert)? · TopBar-Detailgrad (zoom% ganz aus Footer)? · DOSSIER-Audit welche Features konkret übernommen?
---
## ✅ Erledigt (Verlauf, neueste oben)
_Nur jüngste Session; ältere Historie siehe `git log` und HANDOVER-Narrative._
- [x] 2026-07-05 **Schnitt/Ansicht Phase 2: Decke über Schnittebene = gestrichelte Überkopf-Linie** (`39ddd9b`) — freier Decken-Umriss (Balkon-/Vordach-Überstände) jetzt gestrichelte Haarlinie (`OVERHEAD_DASH` in `generatePlan.ts`, `addCeilingPoche`) statt Volllinie; Decken-Fläche war schon Ansicht (viewHatchId). `lwMm`-Param entfernt (feste Haarlinie). +1 Test, Suite 337.
- [x] 2026-07-05 **Schnitt/Ansicht Phase 1: Wand unter Schnittebene = Ansichtslinie**`addWallPoche(viewOnly)` in `generatePlan.ts`: `wall.height < floor.cutHeight` (Brüstung/Podestrand) → Umriss-Haarlinie (`fill:"none"`, NO_HATCH) statt Schnitt-Poché; normale Wände unberührt (Segment-/Gehrungs-/Öffnungslogik geteilt). +2 Tests (`generatePlan.viewwall.test.ts`), Suite 336. Erster Baustein des BIM-Standard-Items „Schnitt vs. Ansicht nach Schnitthöhe".
- [x] 2026-07-05 **Ribbon Phase 4: modulare Custom-Bar** (Tab „Eigene") — datengetrieben auf der bestehenden Registry: neuer `custom`-Tab, `CustomBar` in `RibbonBar.tsx` rendert die vom Nutzer gewählten Items + einen „+ Hinzufügen"-Picker (Dropdown mit Häkchen, listet ALLE Werkzeuge/Befehle aus `ALL_RIBBON_ITEMS`, Klick = an/abwählen). `itemKey`/`itemFromKey` für stabile Persistenz; State in App (`customItems`) via localStorage (`dossier.ribbon.customItems`). Gleiche `RibbonButton`-Aktivierung wie normale Tabs (kein Sonderweg). tsc + Suite 334 grün.
- [x] 2026-07-05 **Ribbon Phase 3 (Teil): Werkzeug-Sidebar raus**`DEFAULT_LEFT_GROUPS` nur noch `attributes` (volle linke Höhe), `LAYOUT_VERSION` 7→8 (gespeicherte Layouts fallen auf neuen Default zurück → sichtbar). Wandtyp-/Deckentyp-Picker von `ToolsPanel` ins `AttributesPanel` verschoben (Nutzer-Entscheid „ins Attribute-Panel") — erscheint bei aktivem Wand-/Decken-Werkzeug (Sektion „Neues Bauteil", auch ohne Auswahl), Host-State unverändert. `ToolsPanel` bleibt registriert (per Fenster-Menü andockbar), `Dropdown`-Import dort entfernt. tsc + Suite 334 grün. **Offen:** Objektinfo unter Attribute mergen.
- [x] 2026-07-05 **Fix: gezeichnete/importierte Kreise+Bögen im WebGL unsichtbar** — der WebGL-Compiler (`glPlan/glPlanCompile.ts`, Default-Renderer) dispatchte `polygon`/`line`/`arc`, aber NICHT `drawingCircle`/`drawingArc` → seit `4ac99d3` (Kreise als echtes `drawingCircle`-Primitiv statt 64-Eck-Polygon) fielen sie im GL still raus (SVG-/WASM-Pfad hatten sie, GL nicht). Zweig ergänzt: bildschirm-adaptive Tessellierung wie beim `arc`-Primitiv (Kreis geschlossen + optionale Vollton-Füllung, Bogen a0..a1 offen). tsc + Suite 334 grün. **Nutzer-Report** („zeichne Kreis, bleibt nicht sichtbar").
- [x] 2026-07-05 **Textur-Spike verifiziert erledigt** (`0ca3b1d`) — bei der Backlog-Abarbeitung festgestellt, dass die render3d-Textur-Spike (`RenderStyle::Textured`, Schachbrett, planare Meter-UVs, `MESH_TEXTURED_WGSL` group 1, `spike3d`-`T`-Toggle) bereits vollständig umgesetzt + committet war; `cargo test` 58/59 grün, keine neuen Deps, Alt-Pfad bitgleich. „Als Nächstes"-Eintrag war veraltet → abgehakt; PBR-Restlückenliste (~1829 PT) in 3D-REST übernommen.
- [x] 2026-07-05 **Ribbon-Feinschliff (OCS-Zeile)** — Tabs in die TopBar-Zeile verlegt (`456ebc8`, eine Leiste Chrome+Tabs, Inhalt darunter; Tab-State in App); OCS-Chrome: kleine Wortmarke „dossier." + Quick-Access-Icons für ALLE Datei-/Export-Aktionen statt Burger-Menü, Zeile auf 26px (`4e3b074`); Text-/Font-Formatierung mit der Ansichts-/Zoom-Steuerung auf EINE Leiste gelegt, „Ansichten" als Standard-Tab zuerst + initial aktiv (`fe22cbf`). Alle tsc + 331 Tests grün. Visuelle Abnahme (26px, Icon-Sitz) noch offen.
- [x] 2026-07-05 **Ribbon Phase 2: TopBar → „Ansichten"-Tab gemergt** (`85011cb`, Nutzer-Entscheid „voll mergen") — die Ansichts-/Zoom-/Darstellungs-Cluster der TopBar (View-Grid+Kamera, Ebenen-/Zeichnungs-Kombis, Detailgrad, Massstab/Zoom, Darstellungsart) als `ViewRibbonTab` (in `TopBar.tsx`) in den Ansichten-Tab verschoben; App reicht sie als `viewsContent`-Node an `RibbonBar` (analog Layout-Menü). TopBar ist jetzt schmale globale Leiste (Marke/Ressourcen · Text · Datei/Export/Einstellungen · Fensterknöpfe). `TopBarProps` entsprechend verschlankt. Visuelle Feinabstimmung offen.
- [x] 2026-07-05 **Ribbon-UI Phase 1** (`9d6e86d`) — datengetriebene Tab-Leiste (`src/ui/ribbon/`: `ribbonItems.ts` Registry, `RibbonBar.tsx`, `CommandIcon.tsx`) unter der TopBar, additiv (Sidebar bleibt). Tabs 2D·3D·BIM·Ansichten; 2D=Zeichnen(select/line/polyline/rect/circle/arc)+Ändern(move/copy/mirror/offset/trim/join), BIM=Bauteile(wall/window/door/stair/ceiling/room); 3D/Ansichten noch leer. Ein Aktivierungs-Pfad: tool→`onSelectTool`, command→`onRunCommand`(engine.start); Aktiv-Highlight über `activeTool`/`engineView.commandName`. `ToolIcon` aus ToolsPanel exportiert (wiederverwendet).
- [x] 2026-07-05 **Attribut-Panel: ein Grid, volle Breite** (`ace0dc6`) — drei getrennte Grids zu EINEM durchgehenden Zwei-Spalten-Grid gemerged (Wertspalte über alle Sektionen bündig), Dropdown-Pills füllen die Wertspalte (fixe Breiten raus), doppeltes Eigen-Padding + zweiter „Attribute"-Titel entfernt. **Nutzer-Report.**
- [x] 2026-07-05 **Eigenschaften-Grid OCS-Stil** (`00733d8`) — `.attr-*` als klares Grid: Sektion-Balken, Zeilentrenner, füllende linksbündige Wertfelder (Texte/Zahlen/Dropdowns einheitlich). Erste Stufe der Ribbon-UI-Vision.
- [x] 2026-07-05 **Zoom-Scroll-Fix** (`077e774`) — Dokument-Overscroll/Rubberband gesperrt (`html,body,#root { overflow:hidden; overscroll-behavior:none }`); Viewport-Zoom zieht die UI nicht mehr mit (macOS). **Nutzer-Report.**
- [x] 2026-07-05 **Kreis- + Bogen-Werkzeug** (`e454eab`, `bd2b12b`) — Kreis-Toolbar (circleCommand gekoppelt) + neues `arcCommand` (3-Klick CCW), beide mit Toolbar-Icon/i18n; rendern glatt via `drawingCircle`/`drawingArc`.
- [x] 2026-07-05 **DXF-Import: Platzierungsoption** (`dd76ec8`) — ImportDialog fragt „relativ zum Nullpunkt" ODER „in die Mitte der aktuellen Ansicht"; `PlanViewHandle.viewCenterModel()` neu; `runImport` verschiebt den Gesamt-Umriss (bbox-Mitte → Ansichtsmitte). tsc + Suite 331 grün.
- [x] 2026-07-05 **Zeichnungen Copy/Paste über Geschosse** (`376aa67`) — Ctrl/Cmd+C kopiert gewählte Drawing2D tief; Ctrl/Cmd+V fügt Klone (neue IDs) auf dem AKTIVEN Geschoss ein und wählt sie. Für z. B. importiertes Mobiliar. Textfeld-Fokus bleibt natives Copy/Paste.
- [x] 2026-07-05 **Tauri Drag&Drop + Import-Dialog-Fix** (`c5b5ca6`, `45e19b7`) — `import`-Befehl als `autoRun` (Datei-Dialog öffnet synchron in der Geste, HMR-Falle via Config-Relaunch behoben); `dragDropEnabled:false` (Tauris natives Drag-Drop fing HTML-Drops ab). **Nutzer-bestätigt: Dialog + Text gehen.**
- [x] 2026-07-05 **Layout: Befehlszeile in Mitte-Spalte** (`c794fee`) — nur Viewport-breit, Docks gewinnen Höhe. **Nutzer-bestätigt „sieht clean aus".**
- [x] 2026-07-05 **Textur-Spike render3d** (`RenderStyle::Textured` real) — zweiter Vertex-Pfad `[pos,normal,uv]` aus dem Mesh abgeleitet (Alt-Pfad bitgleich, per Test belegt), prozedurales 256×256-Schachbrett (kein Asset/`image`-Crate), Textur-Bind-Group group 1, `MESH_TEXTURED_WGSL` (gleiche Beleuchtung, Albedo aus Sampler), Pipeline bitidentisch zur Haupt-Pipeline, `spike3d` per `T` umschaltbar. Verifiziert: cargo test 58/59 (inkl. naga-Test) grün, alle 4 Builds sauber. **Visuelle Fenster-Abnahme durch Nutzer bestanden 2026-07-05** (Schachbrett korrekt, `T`-Umschaltung ok). Erster Durchstich der Textur-/PBR-Pipeline; ehrliche Lückenliste im Bericht — `8556037`
- [x] 2026-07-04 **Grundriss: Decke drückt nicht mehr durch die Wände** — Decken-Umriss an Wand-Footprints (OBB) geclippt; Füllfläche strokelos, Umriss nur über unverdeckte Teilstücke (Überstände). Deckungsgleiche Decke ⇒ Umriss entfällt. vitest 230, tsc sauber — `a2f6923`
- [x] 2026-07-04 **T-Stoss-Putznaht entfernt** — L-Seitenlinie nur noch bei materialFREMDEM Nah-Putz; materialgleicher Putz verschmilzt nahtlos (Nutzer-Direktive „Naht entfernen"). TS+Rust synchron, 3 Tests gezogen, cargo 8/8 — `a5ebfa7`
- [x] 2026-07-04 **Snap-Farbe entschieden: Sora #5FA1C9** als endgültiger Default festgeschrieben (`DEFAULT_SNAP_COLOR`, Doc), „aki"-❓ geschlossen.
- [x] 2026-07-04 **Mac-Build via Tauri + Queue-Abgleich** — Toolchain auf macOS-Gerät verifiziert, `cad.app`/`cad_0.1.0_aarch64.dmg` gebaut & gestartet. Verifikations-Baseline grün (tsc/vitest 230/cargo 56). Queue-Reconciliation: Toolchain-Blocker, Snap-Slice (`339202b`), Ebene-Schraffur (`dcb6ed5`), Bauteile-Tab-Master-Detail, Einstellungs-Verdrahtung UND Wand-Schicht-Bänder 3D (Option B, `toWalls3d.ts`) waren allesamt **bereits gelandet**, aber in PENDENZEN noch als offen gelistet → abgehakt. (kein Code-Commit)
- [x] 2026-07-04 **Öffnungen als echte Boolean-Löcher** (+ Deckentrim-nur-3D) — Fenster/Türen = 1 Wandkörper mit rechteckigen `holes` (Rechteck-Gitter-Zerlegung + Laibungsquads), Segment-Boxen weg; Deckentrim nur noch im 3D-Pfad, Schnitt volle Höhe. Pflicht-Testfall versetzt-überlappende Fenster grün. cargo test 56, vitest 230, build:engine3d + tsc sauber — `1407c68`
- [x] 2026-07-04 **Joins Phase 1c** Durchgangswand am T-Stoss echt aufbrechen (spanCutouts) — `c5a344d`
- [x] 2026-07-04 **Locked-Iso-Fix** freie Kamera bleibt nach Ortho-Preset orthografisch — `cb8fae5`
- [x] 2026-07-04 **3D-Live-Schnittebene** mit schraffierten Schnittflächen — `03f0c40`
- [x] 2026-07-04 **Joins Phase 2** Merge-Regel im Schnitt (gleiche Komponente verschmilzt) — `82d354f`
-124
View File
@@ -1,124 +0,0 @@
# PORT_PLAN — kernel2d nach Rust/WASM (hinter identischem TS-Interface)
Stand: 2026-07-04. **Dieser Plan ist der geforderte erste Schritt — noch kein Code.**
Ziel: `src/geometry/kernel2d.ts` (+ reine Geometrie aus room/ceiling/opening/stair) in
ein Rust-Crate portieren, zu WASM bauen, hinter einer TS-Fassade mit *exakt gleichen*
Signaturen einhängen. TS-Kernel bleibt als Referenz (`kernel2d.legacy.ts`).
Differential-Test: Rust-WASM == TS-Legacy auf identischen Eingaben (epsilon je Funktion).
---
## 1. Scope-Inventar (was wirklich portiert wird)
**Externe JS-Geometrie-Libs: KEINE.** Der Kernel ist handgeschriebene f64-Mathematik,
einzige Abhängigkeit sind die Vektor-Helfer aus `src/model/geometry.ts`
(`add/sub/scale/len/normalize/leftNormal/cross/dot/lineIntersect`) — trivial mit zu
portieren. `polygon-clipping`/`delaunator` werden im Kernel **nicht** genutzt.
### Voll im Scope (reine Geometrie)
- **`kernel2d.ts`** — komplett: Primitive, Schnitt, Offset, Trim/Split/Join, Kreis, Fläche/Orientierung, Fillet.
- **`roomBoundary.ts`** — komplett: `detectRooms`, `roomFromPointInside(Faces)`, `pointInPolygon`, `WallSegment`/`WallFace`/`DetectRoomsOptions` (generische Geometrie-Typen, `thickness: number`).
- **`ceiling.ts`** — komplett: `normalizeOutline`, `isValidOutline`, `ceilingArea`, `outlineBBox`, `outlineCentroid`, `pointInOutline` (generische Polygon-Utilities; Name irreführend, keine Decken-Semantik).
### Teilweise im Scope
- **`roomArea.ts`** — NUR `signedArea`, `polygonArea`, `perimeter`, `centroid`. Alles ab `SiaCategory` (`siaLabel`, `evaluateRoom`, `balance`, `roomsToCsv`) ist SIA-416-Domänenlogik/CSV → **bleibt TS**.
- **`stair.ts`** — portierbar, aber `stairGeometry` nimmt `Stair`. „Scheinkopplung": es werden nur geometrische Felder gelesen (`shape/start/dir/runLength/width/stepCount/…`), `totalRise` kommt bereits aufgelöst vom Aufrufer. Port via Rust-Struct `StairParams` (strukturgleich, null Model-Semantik).
### ⚠️ Scope-Spannung: `opening.ts` (im Auftrag genannt, aber stark model-gekoppelt)
7 von 9 Exporten binden `Wall`/`Opening` direkt ein; `getWallType`/`wallTypeThickness`/`wallReferenceOffset`/`wallVerticalExtent` lösen gegen `Project` auf.
- **Portierbar mit abgeflachter Signatur** (Vec2/number statt Wall/Opening): `wallAxisLength`, `openingInterval`, `wallAxisFrame`, `openingJambs`, `openingCenter`, `doorSymbol`, `openingGapQuad`/`windowSymbol` (letztere brauchen `thickness`/`refOff` als `number`-Parameter).
- **Bleibt TS** (braucht `Project`/Geschoss-Auflösung): `openingVerticalExtent` (via `wallVerticalExtent``Project.drawingLevels`).
- **→ Entscheidung nötig** (siehe §7): Der Auftrag verbietet Änderungen an Aufrufstellen außer dem Import-Pfad. Ein Port von `opening` würde die *Signaturen* ändern (Wall→Vec2/number) und damit die Aufrufstellen brechen — das widerspricht „keine Änderung an Aufrufstellen". Empfehlung: **opening in Phase 1 ausklammern**, nur den echt reinen Kern (kernel2d/roomBoundary/ceiling/roomArea-Flächen/stair) portieren.
---
## 2. Crate-vs-Portieren — pro Operation
Akzeptanzkriterium ist **Differential-Parität gegen die naive TS-Routine**, nicht „geometrisch besser". Jedes Fremd-Crate mit anderem Algorithmus bricht die Parität per Konstruktion → Default = **PORTIEREN** (TS-Routinen sind 540 Zeilen f64, 1:1 übersetzbar).
| Operation | geprüftes Crate | Entscheidung | Grund |
|---|---|---|---|
| `offsetPolyline`/`offsetSegment` | cavalier_contours 0.6 | **PORTIEREN** | Crate liefert **Arcs (bulge)** statt `Vec2[]`, **heilt Selbstschnitte**, gibt mehrere Polylinien — TS heilt bewusst NICHT. Semantik nicht angleichbar. |
| Segment/Line-Schnitt | geo / robust | **PORTIEREN** | Cramer-Formel; jede andere denom-/Epsilon-Politik driftet in Parallel-Grenzfällen. |
| Trim/Split/`segmentPolylineHits` | geo `Relate` | **PORTIEREN** | Projektspezifisch (t-Dedup `1e-6`, Pick-nächster-Bogen, Wrap-Logik). |
| Kreis-Schnitte | — | **PORTIEREN** | Quadratik mit projekt-EPS-Disc-Klemmung. |
| `signedArea`/`isCCW` | geo `Area` | **PORTIEREN** | Shoelace-Summierung in **identischer Vertex-Reihenfolge** (f64 nicht assoziativ). |
| `filletCorner` | — | **PORTIEREN** | `acos/atan2/tan(θ/2)`-Kette + projekt-Cutoffs; liefert projekt-spezifische `Fillet`-Struktur. |
| `detectRooms` / `joinChains` | geo / i_overlay | **PORTIEREN** | Topologie-/reihenfolgeabhängig; andere Kantendurchlauf-Reihenfolge → andere (gleich gültige) Ringe → Parität bricht. |
| point-in-polygon | robust | **PORTIEREN** (robust nur intern, optional) | Siehe §3. |
Fremd-Crates (cavalier_contours/geo/i_overlay) wären für einen späteren *Tier-2-Rewrite* (echter Arc-Offset, Boolean-Ops) wertvoll — das ist ein **anderes Produkt**, nicht dieser paritätserhaltende Port.
---
## 3. `robust`-Prädikate vs. Differential-Parität (Spannung auflösen)
TS testet Orientierung überall via naives `cross()` gegen `EPS`. `robust::orient2d` liefert das **exakte** Vorzeichen — weicht von naiv **nur in der nahe-degenerierten Zone** ab (fast-parallele Segmente, Null-Fläche-Polygone, Punkt-auf-Kante). Genau dort schlägt der Diff-Test am ehesten an. Man kann nicht gleichzeitig „bit-Parität gegen naiv" und „robuste Prädikate" im *selben* Vergleich haben.
**Entscheidung:**
1. **v1 portiert die naiven `cross`-Vergleiche 1:1** (KEIN `robust`) → Zufalls-Diff-Test wird bit-nah grün. Robustheit kommt aus denselben f64-Formeln + demselben `EPS` wie TS.
2. **Additiv, getrennt:** `robust` nur *intern* in `detectRooms`/point-in-polygon hinter optionalem Feature `robust-predicates`, flankiert von **Golden-Cases, die die KORREKTE (robuste) Antwort asserten** (nicht TS-Parität). Diese Fälle sind aus dem Zufalls-Diff-Test ausgenommen.
3. Zwei Testklassen, nie gemischt: **Zufalls-Parität = naiv**, **Golden-Korrektheit = robust**.
---
## 4. Crate-Setup & Vite (exakter Klon von `src-tauri/geometry`)
Ort: **`src-tauri/kernel2d`** (analog render2d/render3d/geometry; das gesamte Tooling ist auf `src-tauri/<crate>` + `../../src/engine/pkg<X>` verdrahtet). *Namens-Hinweis:* Auftrag sagt `crates/kernel2d` — siehe §7.
- `Cargo.toml`: `crate-type = ["cdylib","rlib"]`; Features `default=[]`, `web=[wasm-bindgen, serde_json, console_error_panic_hook]`, `robust-predicates=[robust]` (additiv, nicht in web-Default).
- `src-tauri/Cargo.toml`: `exclude = [..., "kernel2d"]` erweitern (sonst „multiple workspace roots").
- `src/lib.rs`: reiner f64-Rechenkern feature-frei (`cargo test`-bar, headless) + `#[cfg(feature="web")]` JSON-**Batch**-Fassade pro Operation (`offset_polylines_json`, `intersect_batch_json`, `fillet_batch_json`, `circle_intersect_batch_json`, `detect_rooms_json`), Muster `compute_joins_json`.
- `examples/parity.rs`: Klon von `geometry/examples/parity.rs` (Batch-JSON stdin→stdout) — nativer Diff-Kanal.
- `package.json`: `"build:kernel2d": "wasm-pack build src-tauri/kernel2d --release --target web --out-dir ../../src/engine/pkgKernel2d --out-name kernel2d --no-default-features --features web"`.
- **Vite: kein Config-Eintrag nötig** — `--target web`-Pakete werden als normales ES-Modul importiert, Vite bündelt `kernel2d_bg.wasm` automatisch (`new URL(..., import.meta.url)`). `pkgKernel2d/` ist git-ignoriert (self-`.gitignore = *`) → vor `vitest`/`build` muss `build:kernel2d` laufen (CI-Schritt).
- **TS-Fassade** `src/geometry/kernel2d.ts` wird dünner Wrapper (init-WASM, JSON-Marshalling, gleiche Exports); Alt-Impl → `src/geometry/kernel2d.legacy.ts` (nicht löschen, ist die Diff-Referenz).
---
## 5. Differential-Test-Harness
- **Grobkörnige WASM-Grenze:** eine Batch-Funktion je Operation (N Polylinien rein, N Ergebnisse raus). Keine Per-Punkt-Calls (jeder Call marshallt einen String = O(n)-Kopie).
- **Diff-Kanal:** Der Auftrag verlangt **Rust-WASM** vs. TS-Legacy → primär WASM (via `initSync`). Zusätzlich `cargo run --example parity` (nativ) als schneller Sekundär-Kanal (bit-identisch zu WASM für `+ - * / sqrt`; **Ausnahme** `atan2/acos/tan` in Fillet: libm nativ ≠ wasm um letzte ULP → Winkel-Epsilon).
- **vitest-Init synchron:** `initSync({ module: readFileSync(pkgKernel2d/kernel2d_bg.wasm) })` (kein `fetch`), einmal in `beforeAll`.
- **Zufallsgeneratoren:** seed-basiert (Seed im Testnamen), Polylinien 320 Vertices, Koordinaten `[-100,100] m`, `closed`/`d` zufällig; zusätzlich Cluster nahe `0` und `1e-6..1e-3`, um Toleranzschwellen zu treffen.
- **Vergleichsreihenfolge:** zuerst **Struktur exakt** (Array-Längen, closed-Flags, Punktzahl, null/nicht-null), dann Werte mit op-Epsilon. Struktur ist der schärfste Paritäts-Wächter.
### Epsilon pro Funktion
| Größe | Toleranz | Grund |
|---|---|---|
| Punktkoordinaten (Offset/Trim/Split/Kreis) | `abs 1e-9` | Bestehender Paritätstest nutzt `1e-9` und besteht bit-nah. |
| Fläche (`signedArea`) | `rel 1e-9·max(1,\|A\|)` | Shoelace ∝ coord² → absolute ULP wächst mit Flächengröße; relativ skaliert korrekt. |
| Winkel (`filletCorner`) | `abs 1e-7 rad` | `atan2/acos` libm-abhängig (nativ↔wasm ULP-Drift); 1e-7 rad ≈ 5.7e-6°, weit unter Zeichenrelevanz. |
| Parameter t/s | `abs 1e-9` | TS dedupliziert erst ab `1e-6` → kleinere Diffs ändern nie die Struktur. |
| Struktur | **exakt** | Kein Epsilon. Hier bricht ein Fremd-Crate. |
### Golden-Cases (explizit, aus Zufallstest teils ausgenommen)
kollineare Tripel · Null-Länge-Segmente (Dublett-Vertex) · Offset-Selbstschnitt (enges U, großes d — hier bräche cavalier_contours) · spitze Fillet-Winkel (θ→0) · fast-paralleler Schnitt (denom knapp <>EPS → **Korrektheits-Golden/robust**) · konzentrische/tangentiale Kreise · Punkt exakt auf Polygonkante (**Korrektheits-Golden**).
---
## 6. Kritische Paritäts-Details (MÜSSEN exakt repliziert werden)
- `len` = `Math.hypot`**`f64::hypot`** (nicht `(x²+y²).sqrt()`).
- `normalize` Null-Guard: `len || 1``if l==0.0 {1.0} else {l}` (Ergebnis `{0,0}`, kein NaN).
- **Zwei Epsilons:** `EPS=1e-7` (kernel2d) UND hartkodiert **`1e-9`** in `lineIntersect` (Offset-Miter-Fallback hängt daran — geometrischer Sprung, nicht epsilon).
- Dedup-Schwelle `1e-6`, Fillet-Kollinearität `1e-4` (nicht EPS).
- **Stabile Sortierung** (JS `Array.sort` ist stabil): Rust `sort_by`, nicht `sort_unstable_by`.
- Term-Reihenfolge in `cross`, `signedArea`-Summierung, Kreis-Diskriminante `B*B4*A*C` exakt beibehalten (f64 nicht assoziativ; kein Kahan/Reorder).
- Modulo-Indizierung `(i+len-1)%len` (usize-Unterlauf vermeiden).
- `joinChains`: greedy `i<j`-erster-Treffer-dann-Neustart exakt nachbilden (reihenfolgeabhängiges Ergebnis).
---
## 7. Offene Entscheidungen (vor Coding klären)
1. **`opening` im Scope?** Portieren würde Signaturen (Wall→Vec2/number) und damit Aufrufstellen ändern — widerspricht „keine Änderung an Aufrufstellen außer Import-Pfad". **Empfehlung: opening in Phase 1 ausklammern.**
2. **Crate-Ort:** Auftrag `crates/kernel2d` vs. Repo-Konvention `src-tauri/kernel2d` (analog render2d/render3d). **Empfehlung: `src-tauri/kernel2d`** (Tooling passt out-of-the-box).
3. **Diff-Kanal:** Auftrag verlangt Rust-**WASM**; nativer `parity`-Kanal ist schneller (kein wasm-pack in CI) und bit-identisch außer Fillet-Transzendente. **Empfehlung: WASM primär (Auftragskonform) + nativ sekundär.**
## 8. Phasen (Reihenfolge)
1. Crate-Skelett `src-tauri/kernel2d` + Build-Script + Workspace-exclude + leere WASM-Fassade → `build:kernel2d` grün.
2. Primitive + Schnitt + Fläche + Kreis portieren (trivialmittel) + Batch-Fassade + Diff-Test-Harness (Zufall+Golden) → grün.
3. Offset (Miter+1e-9-Fallback) + Fillet portieren → Golden für Selbstschnitt/spitze Winkel grün.
4. Trim/Split/Join (`trimPolyline`, `splitAtIntersections`, `joinChains` — der Löwenanteil) → Struktur-Golden grün.
5. `roomBoundary` (`detectRooms`) + `ceiling` + `roomArea`-Flächen + `stair` (StairParams).
6. TS-Fassade umstellen (Alt → `.legacy.ts`), Import-Pfade der Aufrufstellen unverändert lassen, bestehende Suite grün, `npm run build` + WASM-Build sauber.
+26 -61
View File
@@ -1,75 +1,54 @@
# DOSSIER Standalone # DOSSIER Standalone
Eigenständiges BIM-Werkzeug für Wohnbau. Modelliert ein Wohnhaus aus Browser-BIM für Wohnbau. Ein Werkzeug, um ein Wohnhaus aus semantischen
semantischen Bauteilen und zieht daraus saubere, normgerechte 2D-Pläne — Bauteilen zu modellieren und daraus saubere, normgerechte 2D-Pläne zu ziehen
ohne Revit. ohne Revit, ohne Installation, im Browser.
Das ist die eigenständige Standalone-Variante des Rhino-Plugins Das ist die eigenständige Browser-Variante des Rhino-Plugins
[DOSSIER](https://git.kgva.ch/karim/DOSSIER): dieselbe Denkweise (Geschosse, [DOSSIER](https://git.kgva.ch/karim/DOSSIER): dieselbe Denkweise (Geschosse,
Kategorie-Ebenen, mehrschichtige Bauteile, Prioritäts-Verschneidung), aber als Kategorie-Ebenen, mehrschichtige Bauteile, Prioritäts-Verschneidung), aber als
React/Three.js-App in einer Electron-Desktop-Shell statt als Rhino-Aufsatz. React/Three.js-App 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 ## Grundgedanke
Es gibt **ein semantisches Modell** als einzige Wahrheit. Jede Ansicht — 3D, Es gibt **ein semantisches Modell** als einzige Wahrheit. Jede Ansicht — 3D,
Grundriss, Schnitt, Ansicht, PDF — wird daraus **abgeleitet**. Darstellung Grundriss, Schnitt, Ansicht — wird daraus **abgeleitet**. Darstellung
(Detailgrad, Linienstärken, Schraffuren) wird erst beim Rendern angewandt, nie (Detailgrad, Linienstärken, Schraffuren) wird erst beim Rendern angewandt, nie
in die Geometrie eingebacken. Wer eine Wand verschiebt, verschiebt sie überall. in die Geometrie eingebacken. Wer eine Wand verschiebt, verschiebt sie überall.
Konkret heißt das auch: Der Grundriss entsteht nicht aus einem zerschnittenen Konkret heißt das auch: Der Grundriss entsteht nicht aus einem zerschnittenen
3D-Mesh, sondern **analytisch aus den Bauteil-Parametern** (Wandachsen + Dicken → 3D-Mesh, sondern **analytisch aus den Bauteil-Parametern** (Wandachsen + Dicken →
Linien, Öffnungen → Lücken + Symbol). PDF-Export ist derselbe Plan, nur mit Linien, Öffnungen → Lücken + Symbol). Schnitte und Ansichten brauchen später den
echten mm-Stiftstärken statt Bildschirm-Hairlines — kein zweites Rendering. zweiten Weg — echte 3D-Projektion mit verdeckten Kanten.
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 ## Stand heute
Ehrlich eingeordnet: aus dem Risiko-Spike ist ein **benutzbares 2D-CAD mit Ehrlich eingeordnet: aus dem Risiko-Spike ist ein **benutzbares 2D-CAD mit
abgeleiteter 3D-Sicht, Vektor-PDF/DXF-Export und einer parametrischen abgeleiteter 3D-Sicht** geworden. Der einfachere Teil steht und ist per
Wand-Engine** geworden. Der einfachere Teil steht und ist per Screenshot/Probe Screenshot verifiziert; die schwierigen BIM-Kernrisiken sind bewusst noch offen.
verifiziert; die schwierigen BIM-Kernrisiken sind bewusst noch offen.
**Funktioniert:** **Funktioniert:**
- Semantisches Modell mit **mehrschichtigen Wänden** und automatischer - Semantisches Modell mit **mehrschichtigen Wänden** und automatischer
**L-Ecken-Gehrung** (`computeJoins`); dieselbe Logik speist Plan *und* 3D. **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/ - **Dokumentmodell wie in DOSSIER:** Zeichnungsebenen (Geschosse + Schnitte/
Ansichten/Zeichnungen) × Kategorie-Ebenen (Code-Baum 1:1, `20 Wände`, `30 Decken` …). Ansichten/Zeichnungen) × Kategorie-Ebenen (Code-Baum 1:1, `20 Wände`, `30 Decken` …).
- **Zeichnen & Editieren** im Grundriss: Wand, Linie, Polylinie, Rechteck, Kreis, - **Zeichnen & Editieren** im Grundriss: Wand, Linie, Polylinie, Rechteck, Kreis;
Öffnungen, Treppen, Decken, Raumstempel; Snapping (Endpunkt/Mittelpunkt/ Snapping (Endpunkt/Mittelpunkt/Schnittpunkt/Lot/Raster/Ortho), Grips, Move/Copy/
Schnittpunkt/Lot/Raster/Ortho), Grips, Move/Copy/Offset, Spiegeln/Drehen/Array, Offset, Spiegeln/Drehen/Array.
Trim/Split/Join.
- **Rhino-artiges Befehlssystem** mit getippten Koordinaten (`5,3` · `r5,3` · - **Rhino-artiges Befehlssystem** mit getippten Koordinaten (`5,3` · `r5,3` ·
`5<45`) und Tab-Feld-Zyklus (Länge → Winkel …). `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, - **Resource Manager** (Component / Hatch / Line) — alles per id referenziert,
zentral änderbar. zentral änderbar.
- **Panel-System** (dockbar, stapelbar, floatend), Top-Bar + Statusleiste - **Panel-System** (dockbar, stapelbar, floatend), Top-Bar + Statusleiste
(echter Maßstab 1:N, Cursor X/Y), eigenes Kontextmenü, **i18n** (de/en). (echter Maßstab 1:N, Cursor X/Y), eigenes Kontextmenü, **i18n** (de/en).
- **Import:** DXF/DWG (Konturen/Mesh) → Terrain-TIN, Swisstopo/LV95-Geokontext, - **Import:** DXF (Konturen/Mesh) → Terrain-TIN; `.lin`/`.pat` für Linien/Schraffuren.
OSM-Kontextimport; `.lin`/`.pat` für Linien/Schraffuren.
**Bewusst noch offen** (die eigentlich harten Teile): **Bewusst noch offen** (die eigentlich harten Teile):
- Echte **3D-Booleans** für Öffnungen — Türen/Fenster sind im Plan eine Lücke, - Echte **3D-Booleans** für Öffnungen — Türen/Fenster sind im Plan eine Lücke,
noch kein geschnittenes Volumen. noch kein geschnittenes Volumen.
- **HLR** (verdeckte Kanten) für Schnitte und Ansichten: Machbarkeit per - **HLR** (verdeckte Kanten) für Schnitte und Ansichten — diese Views sind Stubs.
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 - **Prioritäts-T-/X-Stöße** mehrschichtiger Wände (Beton läuft durch, Putz
verbindet seitlich) — das berüchtigte Risiko #1. 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 Details und die Begründungen stehen im
[HANDOVER](HANDOVER.md) (laufendes Arbeitsprotokoll) und in der [HANDOVER](HANDOVER.md) (laufendes Arbeitsprotokoll) und in der
@@ -82,57 +61,43 @@ Geometrie-Kernel verfrüht eingezogen.
| | | | | |
|---|---| |---|---|
| Shell | Electron (eigenes randloses App-Fenster, kein Browser-Tab) |
| Frontend | React + TypeScript + Vite | | Frontend | React + TypeScript + Vite |
| 3D | Three.js, plus eigener nativer Rust/WASM/WebGPU-Renderer (`render3d`) | | 3D | Three.js |
| 2D-Plan | eigener SVG-Renderer, plus eigener nativer Rust/WASM/WebGPU-Renderer (`render2d`) | | 2D-Plan | eigener SVG-Renderer |
| Geometrie | eigener 2D-Kernel (`src/geometry/kernel2d.ts`), `delaunator` fürs Terrain | | Geometrie | eigener 2D-Kernel (`src/geometry/kernel2d.ts`), `delaunator` fürs Terrain |
| Export | `jspdf`/`svg2pdf.js` (Vektor-PDF), eigener DXF-Writer | | Import | `dxf-parser`, eigene `.lin`/`.pat`-Parser |
| Import | `dxf-parser`, `@mlightcad/libredwg-web` (DWG), eigene `.lin`/`.pat`-Parser |
| Schnitt/HLR-Spike | `opencascade.js` (OCCT-WASM) — isoliert, noch nicht verdrahtet |
Geplant, aber noch nicht eingezogen: `rhino3dm` (NURBS / `.3dm`), web-ifc (IFC), 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.), OpenCascade/Manifold (exakte Booleans + HLR). Siehe ROADMAP §4.
aber noch nicht an Dokumentmodell/UI angebunden. Siehe ROADMAP §4.
## Entwicklung ## Entwicklung
```bash ```bash
npm install npm install
npm run dev # Vite, http://localhost:5187 npm run dev # Vite, http://localhost:5173
npm run electron # Electron-Fenster (Dev-Server + randlose App-Shell)
npx tsc -b # Typecheck npx tsc -b # Typecheck
npm run build # tsc -b && vite build npm run build # tsc -b && vite build
npm test # vitest run
``` ```
Verifiziert wird visuell: `node scripts/probe.mjs` rendert die App headless und Verifiziert wird visuell: `node scripts/probe.mjs` rendert die App headless und
schreibt `scripts/probe.png` — Screenshot ansehen, Geometrie prüfen. Weitere schreibt `scripts/probe.png` — Screenshot ansehen, Geometrie prüfen. Für
`scripts/probe-*.mjs` decken einzelne Features ab (PDF, Trim, Materialien, Firefox-Fälle gibt es `scripts/probe-ff*.mjs` (Playwright).
Theme, Boolean-Ops …); für Firefox-Fälle gibt es `scripts/probe-ff*.mjs`
(Playwright).
## Aufbau ## Aufbau
``` ```
src/ src/
model/ semantisches Modell + Ableitungen (types, parametricWalls, roomStamp, joins, terrain) model/ semantisches Modell + Ableitungen (types, geometry, joins, terrain)
geometry/ 2D-Kernel (offset/trim/fillet/intersect, ceiling, opening, roomArea/-Boundary, stair) geometry/ 2D-Kernel (offset/trim/fillet/intersect)
commands/ Rhino-artiges Befehlssystem (engine, parser, registry, cmds/) commands/ Rhino-artiges Befehlssystem (engine, parser, registry, cmds/)
tools/ interaktive Zeichenwerkzeuge + Snapping + Transformationen tools/ interaktive Zeichenwerkzeuge + Snapping + Transformationen
plan/ Grundriss-Ableitung (generatePlan) + PlanView (SVG) + glPlan (WebGL2-Renderer) plan/ Grundriss-Ableitung (generatePlan) + PlanView (SVG)
viewport/ Viewport3D (Three.js) 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 panels/ dockbares Panel-System + die einzelnen Paletten
state/ Store + Slices (project/selection/view/layout) state/ Store + Slices (project/selection/view/layout)
ui/ Top-Bar, Statusleiste, Kontextmenü, Resource Manager, Command-Line ui/ Top-Bar, Statusleiste, Kontextmenü, Resource Manager, Command-Line
io/ Import/Export (DXF, DWG, .lin, .pat), Swisstopo/LV95, OSM-Kontext io/ Import/Export (DXF, .lin, .pat)
i18n/ Wörterbücher de/en i18n/ Wörterbücher de/en
src-tauri/ Rust-Crates (render2d/render3d) — headless per wasm-pack zu WASM
gebaut (npm run build:engine), Tauri-Host selbst ist ausrangiert
``` ```
## Konventionen ## Konventionen
-43
View File
@@ -1,43 +0,0 @@
# RESEARCH — Bauteile Treppe/Fenster/Tür: Rhino-Plugin als Vorbild
Stand: 2026-07-05. Vergleich der authoritativen Rhino-Plugin-Implementierung
(`/Users/karim/PROJECTS/DOSSIER/rhino/`, C#/Python + RhinoCommon-B-Rep) mit
DOSSIER-STANDALONEs vereinfachter TS-Umsetzung. Ziel: Bauteile aufs Rhino-Niveau
heben. Belege = Rhino-Funktionsnamen.
**Grundsatz:** Rhino hat echten B-Rep-Kernel (3D). DOSSIER hat extrudierte Meshes +
`kernel2d` (2D). Darum getrennt: **2D-Symbol-Logik** (leicht in TS/kernel2d
übernehmbar) vs. **3D** (braucht Mesh-Pipeline bzw. später truck).
## Treppe (Rhino `treppe.py`, 1784 Z. — massiv reicher als `stair.ts`)
- **Typen:** gerade / L (3- und 4-Punkt mit Podest) / Wendel (inkl. Spindel-Cone bei r_in<0.05). TS hat alle drei, aber simpler.
- **3D-Querschnitt-Modi** `massiv`/`flach`/`plattenrand` (`_treppe_profile_2d`) — TS: nur Stufenboxen. **fehlt.**
- **2D-Plansymbol** (`_make_treppe_2d_symbol`): Tritte, Lauflinie (4 Pfeil-Styles: klassisch/filled/breit/voll, visuell zentriert via `mid_off`), **Aussenlinie/Outline** (alle 3 Typen), Bruchlinie, Podest-Hexagon. TS: Tritte+Bruch+Lauflinie(1 Style) da; **Aussenlinie fehlt komplett**, Referenz links/mitte/rechts fehlt.
- **Soll-Schrittmass** (2S+A, Lock), Show-Flags, Grips — fehlen in TS.
## Fenster (Rhino `elements.py` `_make_oeffnung_pieces`)
- **3D:** Rahmen (BooleanDiff), Mittelpfosten je Flügel (14), Glas (einfach 12mm / Doppel 2×6+16), Sims aussen (4 Styles), Rahmen-Offset innen/mitte/aussen. TS: nur 2D-Rahmen+Glaslinien, Wand-Boolean-Loch (`1407c68`); kein Rahmen-3D.
- **2D:** TS hat Rahmen + 12 Glaslinien (grob/mittel/fein). **Fehlt:** Brüstungslinie im Plan, Flügel-Mittelpfosten, Anschlag-Striche (nur Tür hat sie), Flügelanzahl.
## Tür (Rhino `elements.py`)
- **Typen:** `normal` / `wandoeffnung` (reiner Durchbruch, kein Blatt). **Rahmen:** `zarge` (3-seitig, im Wandquerschnitt) / `block` (+5cm Überhang).
- **2D-Schwung** (`_make_tuer_swing_curves`): Blatt+Arc, hinge_side/open_angle/aussenseite/swing_invert; Bogen-Anlage am Rahmen (standard/detail). **2D-Sturzlinien** (`_make_tuer_sturz_curves`, gestrichelt, Modi keine/innen/aussen/beide — SIA: Sturz über Schnittebene → gestrichelt). TS: Blatt+Arc+Anschlag (fein) da; **Sturzlinien fehlen komplett**, `wandoeffnung`-Typ + swing_invert fehlen.
---
## Gesamtreihenfolge „was zuerst" (Agenten-Empfehlung)
**Gruppe A — 2D-leicht, hoher Plan-Wert, kernel2d-kompatibel:**
1. **Treppe Aussenlinie/Outline** (alle 3 Typen; gerade=4 Linien, L via Linienschnitt `_line_intersect_xy`, Wendel=2 Bögen+radiale). Ohne sie wirkt der Plan unfertig.
2. **Fenster Brüstungslinie** im Plan (1 gepunktete Linie bei sillHeight>0). XS.
3. **Tür Sturzlinien** (gestrichelt, SIA) — Schulfall der geplanten Schnitt/Ansichts-Logik.
4. **Treppe Referenz links/mitte/rechts** + Lauflinie visuell zentriert.
5. **Fenster Flügel-Mittelpfosten** (2D, `wingCount` ins Modell).
6. **Tür `wandoeffnung`-Typ** (reine Öffnung ohne Blatt/Symbol).
7. Treppe Show-Flags + `obere_dashed`; 8. Fenster Anschlag-Striche.
**Gruppe B — 2D mittel:** Tür-Schwung am Rahmen; Treppe Pfeil-Style `filled`; Fenster/Tür Styles/Presets; swing_invert.
**Gruppe C — 3D (nach Mesh-/B-Rep-Pipeline):** Treppe `flach`-Querschnitt; Rahmen als Mesh (Fenster/Tür); Türblatt+Glas; Sims; L-Podest-Hexagon; Wendel helikoidale Unterseite (truck).
**Bezug PENDENZEN:** Gruppe-A-Items 13 (Aussenlinie/Brüstung/Sturz) fallen in das Backlog-Item „Schnitt- vs. Ansichts-Darstellung nach Schnitthöhe" — Sturzlinie = Bauteil unter Schnittebene → gestrichelte Überkopf-Projektion. Gruppe C wartet auf den Textur-/Mesh-Pipeline-Nachfolger bzw. truck.
-162
View File
@@ -1,162 +0,0 @@
# RESEARCH — Von BIM-Tool zu „echtem CAD": Ansätze aus fortgeschrittenen Programmen
Stand: 2026-07-05. Direkt am Quellcode studiert (Repos nach `/tmp/cad-study` geklont,
liegen NICHT im Projekt). Ziel: konkrete, verfolgbare Ansätze, um DOSSIER von einem
spezialisierten BIM/Architektur-Werkzeug zu einem allgemeineren CAD zu erweitern.
## Studierte Programme
| Repo | Was | Lizenz | Stack | Für uns |
|---|---|---|---|---|
| [OpenCADStudio](https://github.com/HakanSeven12/OpenCADStudio) | Eigenständige 2D/3D-CAD-App, ~200k LOC Rust | GPL-3.0 | Iced (GUI) + wgpu + WASM-Web-Build | **App-Architektur-Blaupause** (Entity-Modell, Command-System, Snap, Grips). Fast unser Stack. |
| [truck](https://github.com/ricosjp/truck) | Reiner Rust-CAD-**Kernel** (NURBS/B-Rep/Mesh/Boolean/STEP) | Apache-2.0 | 16 Crates, wgpu-Rendering + `truck-js` (WASM) | **3D-Kernel-Blaupause**. OpenCADStudio nutzt ihn als 3D-Backend. |
| [acadrust](https://github.com/HakanSeven12/acadrust) | Pure-Rust **DWG+DXF** R13R2018 read+write, 41 Entity-Typen | **MPL-2.0** | nom/byteorder/flate2/nalgebra/encoding_rs (alles pure Rust) | **DWG/DXF-Rückgrat**, MPL-2.0 nutzbar, sehr wahrscheinlich WASM-baubar. |
**Lizenz-Klarstellung:** `acadrust` ist **MPL-2.0** (file-level copyleft — für uns nutzbar,
keine GPL-Ansteckung), NICHT GPL. `truck` ist Apache-2.0 (frei nutzbar). Nur OpenCADStudio
selbst ist GPL-3.0 — wir lesen es als **Architektur-Referenz**, kopieren keinen Code.
---
## Die zentrale Lektion: was ein „echtes CAD" architektonisch ausmacht
DOSSIER ist heute **domänenzentriert** (Wand/Öffnung/Component/Plan-Pipeline, TS-Modell).
Ein echtes CAD (Rhino/AutoCAD-artig) hat stattdessen einen **generischen Kern**. Aus
OpenCADStudios `src/` destilliert:
### 1. Generisches Entity-Modell mit Traits (statt Wand-Spezialfall)
`src/entities/` = 41 Entity-Typen (line, arc, circle, lwpolyline, spline, ellipse, hatch,
text, mtext, dimension, leader, insert/block, table, viewport, mesh, solid3d, …). Jeder
Typ implementiert ein **Trait-Set** statt einer Sonderbehandlung:
- `TruckConvertible` → 3D-Kernel-Geometrie
- `FallbackTess` → Display-Geometrie (Punkte/Snap-Vertices)
- `Grippable` → editierbare Griffe + Hover-Menü (Add Vertex / Convert to Arc / Reverse / Lengthen…)
- `PropertyEditable` → Eigenschaften-Panel
- `Transformable` → Move/Rotate/Scale/Mirror
- `MassProps` → Fläche/Umfang für Abfragen
**DOSSIER-Lehre:** ein generisches `Entity`-Modell mit Trait-Dispatch neben das BIM-Modell
stellen. Wir haben Ansätze (`Drawing2D`, splitJoin-Editoren, kernel2d), aber keine einheitliche
Entity-Taxonomie mit Traits.
### 2. Modeless Command-System — der eine `StepInput`-Step-Machine
`src/command.rs`: EIN Enum `StepInput { Point / Text / EntityPick / StructurePick /
SelectionComplete }`. **Jede** Eingabequelle (Kommandozeile, Viewport-Klick, Pick, Selektion,
Dynamic Input, Plugin-API, Headless-Script) übersetzt in `StepInput` und läuft durch **eine**
`feed_command`-Funktion → treibt die aktive `CadCommand`-Step-Machine, egal woher der Schritt kam.
**DOSSIER-Lehre:** Das ist der größte UX-Unterschied. Echtes CAD = „Befehl tippen → Punkte
picken → Optionen". DOSSIER ist heute Tool-Button + Direktmanipulation. Ein `StepInput`-Funnel +
Kommandozeile wäre der Hebel, um beliebige Werkzeuge einheitlich, scriptbar und headless-testbar
zu machen.
### 3. Object-Snap-Engine (eigenständig, groß)
`src/snap.rs` = 70 KB nur für OSNAP (endpoint/midpoint/center/intersection/tangent/
perpendicular/…). Wir haben Teil-Snapping; ein echtes CAD hat eine dedizierte, vollständige
Snap-Schicht.
### 4. Universelle Grip-Editierung
Jede Entity liefert Griffe + kontextuelle Griff-Menüs mit teils numerischem Follow-up
(„Lengthen" fragt Wert an der Kommandozeile ab). Einheitlich über das `Grippable`-Trait.
### 5. `CadDocument` als Dokumentmodell = das DWG/DXF-Objektmodell
OpenCADStudio nutzt `acadrust::CadDocument` DIREKT als sein Datenmodell (Entities + Objects +
Tables: Layer/Linetype/Style/Blocks + XData). D. h. das native CAD-Austauschformat IST das
interne Modell → verlustfreier Round-Trip „for free".
---
## truck — 3D-Kernel-Blaupause (+ harte WASM-Realität)
Saubere Schichtung (Ship-of-Theseus, kleine ersetzbare Crates):
```
truck-base Basistraits/Toleranz (cgmath)
truck-geotrait ParametricCurve / ParametricSurface Traits
truck-geometry Knot-Vektor, B-Spline, NURBS ← Kurven/Flächen
truck-topology vertex/edge/wire/face/shell/solid ← B-Rep-Topologie
truck-modeling Geometrie + Topologie integriert ← Solids bauen
truck-polymesh Polygon-Datenstruktur + Meshing
truck-meshalgo Tessellation der Shapes
truck-shapeops Boolean-Ops auf Solids (v0.4 — früh)
truck-stepio STEP read/write (v0.3)
truck-platform wgpu-Grafik-Utility ┐ Rendering — brauchen wir NICHT
truck-rendimpl Shape/Mesh-Visualisierung ┘ (wir haben eigenen wgpu-Renderer „Nordstern")
truck-js WASM-Wrapper (v0.2)
```
**WASM-Realität (zwei Befunde abgeglichen):**
- OpenCADStudio pinnt `truck-meshalgo` 0.4 und schaltet den `solid3d`-Feature **im WASM-Build ab**,
weil dessen ACIS-/vtkio-Pfad über `xz2 → lzma-sys` (C-Bibliothek) nicht nach wasm32 kommt →
**im Web ist OpenCADStudio nur 2D.**
- Neuere `truck-meshalgo` (0.6) gated `rayon`/`vtkio` selbst per `cfg(not(target_arch="wasm32"))`
aus → **baut zu wasm32, aber single-threaded**. Der reine Geometrie-Stack
(base → geotrait → geometry → topology → polymesh → meshalgo → modeling → shapeops) hat keine
native-only-Deps und ist **sauber vom wgpu-Rendering trennbar** (truck-platform/-rendimpl weglassen).
**Reifegrad (Apache-2.0, aktiv, ~1 Hauptentwickler; ausdrücklich KEIN Produktionskern):**
- NURBS/B-Spline + B-Rep-Topologie: **stabil** (v0.5/0.6).
- Booleans (`truck-shapeops`): `and`/`or` vorhanden, aber **instabil bei coincident geometry**
(Issue #114); **`difference` fehlt** (#85); **Fillets fehlen komplett**. Der Fork
[`monstertruck`](https://github.com/virtualritz/monstertruck) hat difference + rolling-ball-Fillet.
→ Für unsere Wandöffnungen (boolean holes) haben wir das ohnehin schon selbst gelöst (`spanCutouts`).
- `truck-stepio`: STEP **schreiben** (aber NICHT für boolean-operierte Shapes) + **lesen** (v0.6 beta,
neues `truck_stepio::in`).
- `truck-js`: fertiger wasm-bindgen-Wrapper (kein npm-Paket → selbst per `wasm-pack` bauen), bündelt
modeling/shapeops/meshalgo/stepio; `IntoWasm`-Muster.
---
## acadrust — DWG/DXF-Rückgrat
Pure Rust, **MPL-2.0**, `CadDocument`-Objektmodell (entities/objects/tables/xdata/classes),
41 Entity-Typen, DWG „208/208 roundtrip-perfect", serde optional, failsafe-Parsing, ~40 Codepages.
**Alle Deps pure Rust** (nom/byteorder/flate2-miniz/nalgebra/indexmap/ahash/encoding_rs) → sehr
wahrscheinlich zu wasm32 baubar (Datei-I/O-API müsste auf Bytes/Reader statt `std::fs` umgestellt
werden — vermutlich schon vorhanden). Heute nutzen wir JS `dxf-parser` (nur 2D) + `libredwg-web`.
---
## Web-Import-Landkarte (für den GEO-BLOCK / Import-Backlog)
Pro Format die aktive, web-/WASM-taugliche Lib (Stand Mitte 2026):
| Format | Empfohlene Lib | Lizenz | Anmerkung |
|---|---|---|---|
| DWG/DXF (browser) | `@mlightcad/cad-viewer` / `libredwg-web` (nutzen wir) **oder** `acadrust` (Rust) | MIT / GPL-3.0 / **MPL-2.0** | acadrust = Rust-Weg, MPL-2.0 |
| STEP/IGES/BREP | `occt-wasm` (OCCT V8, ~4.5 MB) oder unser `opencascade.js` | LGPL-2.1 | occt-wasm = moderne TS-API |
| **IFC** (BIM!) | `web-ifc` (ThatOpen) | MPL-2.0 | De-facto-Standard, read+write, sehr aktiv |
| SHP/GIS | `shpjs` + `proj4` | MIT | direkt für GEO-BLOCK (SWISSIMAGE/Terrain) |
| Punktwolken LAS/LAZ | `@loaders.gl/las` (+ `laz-perf`) | MIT/Apache | Rendering: `potree-core` oder deck.gl |
| E57 | `e57-js` (Emscripten/libE57Format) | MIT | neu, funktional, sehr obskur |
| glTF/OBJ/STL | three.js-Loader (haben wir via `three`) | MIT | |
| 3MF | `lib3mf` (WASM) oder `THREE.3MFLoader` | BSD-2 / MIT | |
| PDF-Vektor | `pdfjs-dist` `getOperatorList()` | Apache-2.0 | kein fertiges PDF→DXF; roher Pfad-Stream |
---
## Priorisierte Ansätze für DOSSIER (die eigentliche Antwort)
Reihenfolge = Nutzen × Machbarkeit, ohne die BIM-Stärke zu opfern.
1. **Generisches Entity-Modell + Trait-Dispatch** (Fundament). Neben das BIM-Modell eine
generische Entity-Schicht (line/arc/circle/polyline/spline/text/dimension/hatch/block) mit
Traits à la OpenCADStudio (Display/Grips/Props/Transform). Unser `kernel2d` liefert schon die
Geometrie-Operationen dafür. **Größter struktureller Hebel.**
2. **Modeless Command-System** (`StepInput`-Funnel + Kommandozeile). Ein Eingabe-Enum, eine
`feed_command`-Schleife, `CadCommand`-Step-Machines. Macht Werkzeuge einheitlich, scriptbar,
headless-testbar. **Größter UX-Hebel Richtung „echtes CAD".**
3. **DWG/DXF-Round-Trip über `acadrust`** (Rust/WASM). MPL-2.0, pure Rust → passt in unseren
neuen `src-tauri`-Kern. Ersetzt langfristig die JS-Parser, bringt echten Import/Export.
Erst: WASM-Baubarkeit + Bytes-API verifizieren (Spike wie beim Textur-Spike).
4. **Vollständige Object-Snap-Schicht** als eigenes Modul (endpoint/mid/center/intersection/
tangent/perp/…), aufbauend auf `kernel2d`.
5. **3D-B-Rep später, selektiv** (truck-geometry/-topology/-modeling als reine Crates, OHNE
truck-Rendering — wir haben Nordstern). NUR wenn wir echte NURBS/Solids/Booleans brauchen; der
Meshing-/Boolean-Pfad ist WASM-problematisch (native C-Deps) und Booleans sind noch früh.
Bis dahin: truck als **Referenz** lesen, nicht einbinden.
**Kernaussage:** DOSSIERs Rust-nach-WASM-Kurs (den wir mit `kernel2d` gerade gehen) ist genau
richtig — OpenCADStudio bestätigt ihn. Der Weg zu „echtem CAD" führt über (1) generisches
Entity-Modell und (2) modeless Command-System; (3) `acadrust` bringt echten DWG/DXF-Austausch.
3D-B-Rep (truck) ist ein späterer, selektiver Schritt mit klaren WASM-Vorbehalten.
-137
View File
@@ -1,137 +0,0 @@
# SPIKE — Bild-Texturen in `render3d` (`RenderStyle::Textured` real machen)
Stand: 2026-07-05. **Auftrag/Übergabe für einen Agenten. Kleinster ehrlicher
Durchstich — kein Produktfeature, keine Integration.**
Ziel: Beweisen, dass der bestehende wgpu-3D-Renderer echte **Bild-Texturen** auf
Wandflächen darstellen kann, sichtbar im `spike3d`-Fenster. Am Ende steht eine
belastbare Aussage, wie viel Arbeit „richtig gutes texturiertes 3D" wirklich ist —
statt Spekulation.
---
## 0. Ausgangslage (verifiziert am 2026-07-05)
- `render3d` ist **kein** Three.js-Wrapper, sondern ein eigenständiger wgpu-Renderer
(~6400 LOC): echte GPU-Pipeline (wgpu 29), Tiefenpuffer, MSAA, WGSL-Shader.
Läuft nativ (winit-Spike), headless (naga-validiert) und im Browser (WebGPU).
- **`RenderStyle::Textured` existiert bereits als Stub** (`gpu.rs:34` Enum-Variante,
`gpu.rs:47` Parse aus `"textured"`) — es gibt aber **kein echtes Texturing**:
kein Sampler, keine Textur-Bind-Group, keine Bilddaten.
- Die `cap_pipeline` mit Layout `[pos vec3, uv vec2]` + `CAP_WGSL` ist **nicht** für
Bildtexturen, sondern für die **Schnittflächen-Kappen** (prozedurale Schraffur);
die UVs steuern dort den Schraffur-Abstand. **Nicht damit verwechseln.**
- **Günstig für uns:** Die Muster, die der Spike braucht, existieren schon —
ein UV-tragendes Vertex-Layout und eine zweite/dritte Pipeline, die sich die
`Globals`-Bind-Group teilt (`grid`, `cap`). Der texturierte Mesh-Pfad reiht sich
1:1 in dieses Muster ein.
---
## 1. Echte Symbole — vor dem Coding lesen
| Was | Ort |
|---|---|
| Vertex heute interleaved `[pos.xyz, normal.xyz, color.rgb]`, `FLOATS_PER_VERTEX`, `Mesh`-Struct | `src-tauri/render3d/src/types.rs:322` ff. |
| Quad-Emitter (Normale + Farbe je Vertex, Reihenfolge `(0,1,2)+(0,2,3)`) | `src-tauri/render3d/src/mesh.rs:886` |
| Wand-Extrusion / Mesh-Bau | `src-tauri/render3d/src/mesh.rs``extrude_wall` (`:91`), `build_walls_mesh` (`:911`) |
| Haupt-Pipeline + `Globals`-Bind-Group (group 0) | `src-tauri/render3d/src/gpu.rs:232``:320` |
| Vorlage „zweite Pipeline teilt sich Globals" — Grid | `src-tauri/render3d/src/gpu.rs:329` |
| Vorlage „Pipeline mit UV-Layout `[pos vec3, uv vec2]`" — Cap | `src-tauri/render3d/src/gpu.rs:408` |
| `RenderStyle`-Enum + Stub `Textured` | `src-tauri/render3d/src/gpu.rs:34`, `:47` |
| Pipeline-Bindung im Render-Pass (Muster für Stil-Umschaltung) | `src-tauri/render3d/src/gpu.rs:847` |
| Shader als WGSL-Konstanten (`MESH_WGSL`, `CAP_WGSL`) | `src-tauri/render3d/src/shaders.rs` |
| Beleuchtungsmodell (hemisphärisch + Directional + Fill) — Doku | `src-tauri/render3d/src/shaders.rs:1``40` |
| naga-WGSL-Validierung headless (Test-Vorlage) | `src-tauri/render3d/src/lib.rs:924` (`cap_module`) |
| Fenster-Spike mit Orbit-Kamera | `src-tauri/render3d/src/bin/spike3d.rs` |
---
## 2. Umfang — exakt das, nicht mehr
### 2.1 UVs auf Wandflächen
- In der Quad-Emitter-Funktion (`mesh.rs:886`) je Vertex eine **UV** berechnen:
planare Projektion in **Metern**`u` = Distanz entlang der Wandachse,
`v` = Höhe (z). Textur-Raster damit weltmassstäblich (z. B. 1 Kachel = 1 m).
- Den bestehenden `[pos, normal, color]`-Pfad **bitgleich unangetastet** lassen.
Zwei zulässige Wege (Agent wählt begründet):
1. **Separates additives UV-Array** in `Mesh` (Default leer/None), oder
2. **Paralleler `build_walls_mesh_textured`** → interleaved
`[pos.xyz, normal.xyz, uv.xy]`.
- Regressionstests für den Alt-Pfad müssen grün bleiben (siehe §4).
### 2.2 Test-Textur prozedural (kein Asset, keine `image`-Crate)
- Ein **256×256 RGBA-Schachbrett/Grid im Code** generieren (`Vec<u8>`).
- `device.create_texture` + `queue.write_texture` + `Sampler`
(`FilterMode::Linear`, `AddressMode::Repeat`). Mipmaps optional (nice-to-have für
flache Blickwinkel; kein Muss für den Spike).
- Selbstständig, damit der Spike ohne Dateipfade/Asset-Pipeline läuft.
### 2.3 Textur-Bind-Group (group 1)
- Neue Bind-Group-Layout mit `texture_view` (`TextureSampleType::Float`) +
`sampler`. **`Globals` bleibt group 0** und unverändert.
### 2.4 Textured-Pipeline + WGSL (`MESH_TEXTURED_WGSL`)
- Vertex-Layout `[pos vec3, normal vec3, uv vec2]`, `TriangleList`.
- **Dieselbe Beleuchtung wie `MESH_WGSL`** (hemisphärisches Ambient + Directional +
Fill) — nur **Albedo = `textureSample(tex, samp, uv)`** statt Vertex-Farbe.
- Depth-Format, MSAA (`SAMPLE_COUNT`) und Color-Target **identisch** zur
Haupt-Pipeline (sonst inkompatibler Render-Pass).
- Pipeline-Layout bindet group 0 (Globals) **und** group 1 (Textur).
### 2.5 Verdrahten
- Bei `RenderStyle::Textured` im Render-Pass die neue Pipeline + beide Bind-Groups
setzen (Muster: `cap_pipeline`-Bindung bei `gpu.rs:847`).
### 2.6 Spike sichtbar machen
- `spike3d.rs` so erweitern, dass der Stil auf `Textured` schaltbar ist
(Tastendruck, z. B. `T`, **oder** Startkonstante). Die Demo-Wände sollen
texturiert im Orbit erscheinen.
---
## 3. Randbedingungen (hart)
- **Nur** die `render3d`-Crate. `src/web.rs` und die Tauri-/`native3d`-Oberfläche
**nicht** anfassen.
- Feature-gegatet unter dem bestehenden `render`/`window`-Feature.
**Default-Build und Default-Darstellung bleiben unverändert.**
- **Keine neuen Dependencies** (insbesondere **kein `image`-Crate**) für den Spike.
- Term-/Reihenfolge-sensible Geometrie (Parität) wird **nicht** berührt — es kommt
nur additiv ein UV-Kanal + ein zweiter Render-Pfad dazu.
- Kommentar-Stil und Sprache (Deutsch, ausführliche Begründungs-Kommentare) wie im
umgebenden Code beibehalten.
---
## 4. Akzeptanz / Verifikation
1. `cargo test` (im Crate-Verzeichnis `src-tauri/render3d`) **grün**, inklusive:
- bestehende Mesh-Regression (Alt-Pfad `[pos,normal,color]` unverändert),
- **neuer naga-Validierungstest** für `MESH_TEXTURED_WGSL` (Vorlage:
`lib.rs:924`).
2. `cargo run --features window --bin spike3d` zeigt die Demo-Wände mit
**erkennbarer, korrekt gemappter** Schachbrett-Textur:
- Raster weltmassstäblich (in Metern), keine Verzerrung an Gehrungen/Ecken,
- beleuchtet wie im Shaded-Modus (Volumen bleibt ablesbar).
3. Umschalten Shaded ↔ Textured zur Laufzeit (oder per Startkonstante) funktioniert
ohne Re-Meshing-Crash.
---
## 5. Abschlussbericht (vom Agenten am Ende zu liefern)
- Welche Dateien geändert/hinzugefügt wurden und warum.
- Wie die UVs projiziert werden (Achswahl, Massstab, Verhalten an Gehrungen).
- Welcher der beiden UV-Wege (§2.1) gewählt wurde und weshalb.
- **Ehrliche Lückenliste für „richtig gutes" Texturing:** Asset-/Bild-Datei-Laden,
Material→Textur-Zuordnung (Wandtyp/Layer → Material), Normal-/Roughness-Maps
(PBR), anisotropes Filtern + Mipmaps, Web-Pfad (`web.rs`/WebGPU), UI zum
Zuweisen. Grobschätzung Aufwand je Punkt.
---
## 6. Nicht im Scope
Asset-/Bild-Datei-Laden · Material-System · mehrere Texturen gleichzeitig ·
PBR/Normal-Maps · Web-Pfad (`web.rs`) · jegliche UI · Anbindung unter die Webview.
+2 -2
View File
@@ -1,4 +1,4 @@
# ARCHITEKTUR-BRIEFING: Tauri + wgpu (Korrigiert) # HAUPTINSTANZ-BRIEFING: Tauri + wgpu (Korrigiert)
**Stand:** 2026-07-01 — **KORREKTUR** (vorherige Dokumente waren unvollständig) **Stand:** 2026-07-01 — **KORREKTUR** (vorherige Dokumente waren unvollständig)
@@ -158,7 +158,7 @@ npm run tauri:build # → Windows .exe / macOS .app / Linux .deb
--- ---
## Nächste Schritte ## Nächste Schritte (für Hauptinstanz)
1. **Lesen:** `docs/design/tauri-migration-plan.md` (technisch, konkret) 1. **Lesen:** `docs/design/tauri-migration-plan.md` (technisch, konkret)
2. **Spawn:** vier Agents (parallel, unabhängig) 2. **Spawn:** vier Agents (parallel, unabhängig)
-110
View File
@@ -1,110 +0,0 @@
# DOSSIER-Feature-Audit (A1A6, B1B4, C1C3, D1D3, E)
Ausführliche Fassung des Audits, auf das HANDOVER.md Backlog-Punkt 9 nur noch mit
Einzeilern verweist. Abgleich DOSSIER-Rhino-Referenzrepo (`/tmp/dossier-ref`) gegen
den aktuellen Browser-Port. Belege (Datei:Zeile) beziehen sich auf `/tmp/dossier-ref/rhino/*.py`.
Status-Symbole: ❌ nicht übernommen · 🟡 teilweise übernommen.
## A — Grösserer Funktionsumfang
**A1 · ❌ Grafische Overrides — Regel-Engine** (`overrides.py:7-40`)
ArchiCAD/Vectorworks-Stil: Regeln der Form `condition{type: layer_name|user_string|
object_name, operator: equals|contains|starts_with|not_equals, value, key} →
actions{color, lineweight, linetype}`, additiv angewendet, oberste Regel gewinnt,
reversibel. Cross-Doc-Presets + Rule-Templates (`list_presets:147`,
`list_rule_templates:218`). Der Port hat nur manuelle Per-Instanz-Farbe (By-Layer/
By-Object/eigener Wert, kein Regelwerk). Wert: enorm für Architektur-Pläne
(Bestand grau, Brandabschnitte, Bauphasen farblich markieren). Aufwand: M-L.
**A2 · ❌ Ausschnitte / View-Snapshots** (`ausschnitte.py:466-531`)
Benannte, gespeicherte Ansichten, die Kamera-Zustand + Layer-Sichtbarkeit +
Massstab + Detailgrad einfangen und wiederherstellen; organisiert in Ordnern +
Presets (`_capture_camera:78`, `_capture_layers:157`, `_capture:466`). Wert:
zentral für Wiederverwendbarkeit und Voraussetzung für A3. Aufwand: L.
**A3 · ❌ Print-Layout-Blätter** (`layouts.py:7-8`)
Layout-Seiten mit mehreren Details, jedes Detail an einen Ausschnitt-Snapshot
gebunden (`_BIND_KEY:28`), Papierformate A4/A3, Ordnerstruktur, PDF-Export pro
Layout-Blatt. Der Port exportiert aktuell nur eine einzelne Zeichnung nach PDF.
Aufwand: L, hängt an A2.
**A4 · ❌ Tragwerk-Elemente** (`elemente.py`)
Stütze, Träger, Unterzug, I-Profil als eigene Elementtypen. Der Port kennt nur
Wand/Decke/Öffnung/Treppe/Raum. Wert: vervollständigt den BIM-Elementsatz.
Aufwand: M je Typ.
**A5 · 🟡 Öffnungen viel reicher** (`elemente.py:59-68`)
Mehrere Flügel (`OEFF_FLUEGEL`), Sims innen+aussen mit eigenen Stilen
(`SIMS_AUS`/`SIMS_IN`), Glas-Toggle (`OEFF_GLAS`), Rahmen-Lage aussen/mittig/innen
(`RAHMEN_POS`), Rahmen-Profilbreite/-tiefe, Detailgrad einfach/standard/detail =
SIA-400-Darstellung (`OEFF_DARSTELLUNG:68`). Der Port hat nur swing/hinge/
frameDepth/frameThickness. Aufwand: M.
**A6 · 🟡 Element-Schedule / Bauteilliste** (`elemente_uebersicht.py:45,214`)
Voller Element-Überblick + SIA-Flächenbilanz + CSV-Export
(`_export_bilanz:214`). Der Port hat nur einen Raum-CSV-Export. Aufwand: M.
## B — Werkzeuge & Objekt-Info
**B1 · ❌ Text-Platzierungs-Werkzeug** (`text_create.py:972,847,105`)
On-Canvas-Text-Objekte als eigenständiges Zeichenwerkzeug (nicht nur
Raumstempel): Textstile, Fonts, Rich-Text fett/kursiv/unterstrichen,
Ausrichtung, Symbol-Einfügung, „auf Selektion anwenden". Der Port hat den
Rich-Text-Editor (`src/text/RichTextEditor.tsx`) bereits, aber kein
Platzierungs-Werkzeug, um damit ein freistehendes Textobjekt zu zeichnen
(Shortcut „1" ist bewusst noch unbelegt). Aufwand: M.
**B2 · 🟡 Object-Info numerisch erweitern** (`dimensions.py:342-350,240,267`)
Position, Rotation um Z-Achse (`_rotate_around_axis:240`), Kreis-Radius,
Linien-Länge (`_set_line_length:267`), Rechteck Breite/Höhe,
Koordinatensystem World/CPlane, 9-Punkt-Referenz. Der Port kann nur per
Anker-Griff skalieren. Aufwand: S-M.
**B3 · ❌ Kamera-Presets + Nordwinkel** (`kamera.py:28,36-45,87`)
Nicht in HANDOVER.md gelistet, aber im Audit gefunden: Kardinal-Presets
(N/O/S/W), Iso-Oktanten, Rotation um einen einstellbaren Nordwinkel
(georeferenziert, relevant für Swisstopo-Kontext).
**B4 · ❌ Massstab-Toolbar-Funktionen** (nicht separat referenziert, Teil der
Toolbar-Logik) — Zoom 1:1/auf Selektion, Linienstärken-Set, Grid/Ortho/
Referenzlinien-Toggles direkt aus der Symbolleiste.
## C — Zusätzlich gefunden, nicht in HANDOVER.md
**C1 · LoD pro Ansicht** — Darstellung einfach/standard/detail je Ansicht
umschaltbar (SIA-400-Detailgrad-Konvention), nicht nur global.
**C2 · Override-Presets + Rule-Templates als Bibliothek** — vertieft A1: die
Regeln selbst sind wiederverwendbare, benannte Presets/Templates, nicht nur
Ad-hoc-Zustand pro Dokument.
**C3 · Mass-Style-Presets** — Dezimalstellen/Rundung als benannte,
wiederverwendbare Bemassungs-Stile.
## D — Weitere Backlog-Ergänzungen aus dem Audit
**D1** Per-Layout-PDF-Export (vertieft A3).
**D2** Bauteil-CSV mit vollem Element-Set (vertieft A6).
**D3** Komponenten-Thumbnails (visuelle Vorschau im Component-Manager).
## E — Bewusst NICHT zu portieren
Rhino-/Windows-gebundene Implementierungsdetails ohne Browser-Äquivalent:
Window-Layout-XML-Persistierung, Auto-DPI via CoreGraphics, Custom-Grips-Code
(Rhino-SDK-spezifisch), nativer `.3dm`-Geometrie-Import.
## Priorisierung (aus dem Original-Audit)
1. A1 (Override-Regel-Engine)
2. A2 (View-Snapshots)
3. A3 (Print-Layout-Blätter)
4. A5 (reichere Öffnungen)
5. B1 (Text-Platzierungs-Werkzeug)
6. A4 (Tragwerk)
7. B2 (Object-Info numerisch)
8. A6 (Bauteil-Schedule)
Siehe auch `ROADMAP.md` §11 für eine noch breitere, unabhängig entstandene
Backlog-Liste (4-Wege-Survey über Bauteile/Darstellung/Pläne/Kontext) mit
teilweiser Überschneidung.
-137
View File
@@ -1,137 +0,0 @@
# 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.
-511
View File
@@ -1,511 +0,0 @@
# Engine-Nordstern 1 — Strichbreiten-Audit (Papier-mm end-to-end)
> Bezug: HANDOVER.md, Abschnitt ENGINE-NORDSTERN Punkt 1 ("Papier-mm-exakte
> Strichbreiten überall — Bildschirm bei jedem Massstab/Zoom = Druck"). Dieses
> Dokument ist ein Prüfbericht, kein Umbau — es fasst zusammen, wie die
> Strichbreite heute vom Modell bis zum Papier läuft, wo sie divergieren kann,
> und was konkret zu tun wäre. Nichts hieraus wurde umgesetzt.
Betroffene Pfade: `src/plan/PlanView.tsx` (SVG-Default + Print-Vorschau),
`src/plan/glPlan/*` (WebGL2, aktueller Default-GPU-Pfad), `src-tauri/render2d`
(WASM/WebGPU, `?engine=wasm`), `src/export/sceneToPrintSvg.ts` +
`src/export/exportPdf.ts` (Vektor-PDF). Referenz-Baseline für die
2D-Engine-Parität ist laut Commit `ce6bd26` **`?gl=0`** (reiner SVG-Pfad) —
NICHT der App-Default (der ist WebGL2, siehe Finding 1).
## 1. Modell der Wahrheit
So sollte eine Strichbreite in diesem Code korrekt fliessen:
1. **Quelle**: `generatePlan.ts` legt jede Strichbreite als `weightMm` /
`strokeWidthMm` in **echten Papier-Millimetern** ab (Kommentarkopf,
`generatePlan.ts:76-79`: "Alle Stricharten sind in mm Papier definiert").
Diese Zahl ist unabhängig von Zoom, Massstab und Render-Pfad — eine 0.18 mm
Wand-Umrisslinie bleibt 0.18 mm, ganz gleich wo sie später landet.
2. **Bildschirm bei Massstab 1:N**: 1 Modell-Meter entspricht auf Papier
`1000/N` mm. Der Bildschirm zeigt `PX_PER_M = 90` viewBox-Einheiten je
Modell-Meter (`PlanView.tsx:23`). Eine `mm`-Breite belegt daher
`mm · N/1000 · PX_PER_M` viewBox-Einheiten — das ist exakt
`printStrokeVb()` (`PlanView.tsx:2598-2600`). Multipliziert mit der
Geräte-px-je-viewBox-Einheit-Skala (`meet`, `PlanView.tsx:2074-2078`) ergibt
das die tatsächliche Bildschirmbreite in Geräte-Pixeln. Reinzoomen (kleinere
viewBox-Breite, grösseres `meet`) macht die Linie dicker — genau wie beim
Herausvergrössern eines gedruckten Plans mit der Lupe. Das gilt für JEDE
Zoomstufe gleichermassen: 250 % Zoom bei 1:50 zeigt exakt die 5-fache
Pixelbreite von 100 % Zoom bei 1:50, und bei gegebenem Zoom ist 1:50 exakt
doppelt so dick wie 1:100 (halber Nenner → doppelt so viele Weltmeter je
Papiermm).
3. **Druck/PDF**: dieselbe Formel, nur ohne Geräte-px-Zwischenschritt — direkt
`mm` bleibt `mm` — die Seite ist direkt in echten
Papiermillimetern aufgespannt (`sceneToPrintSvg.ts:99-125`). Zusätzlich wird
auf ISO-nahe Stiftstufen gerundet (`PEN_STEPS`,
`sceneToPrintSvg.ts:44`), weil ein reales Zeichengerät/Plotter nur endlich
viele Stiftbreiten kennt.
4. **Eine Wahrheit**: Seit `382771b` bauen sowohl der Viewport
(`useWasmPlanRenderer.ts`/`nativeSync.ts`) als auch der PDF-Export
(`exportPdf.ts:73`) dieselbe `planToRenderScene(plan)`-Szene — der PDF-Pfad
ist nur ein anderes *Ziel* derselben Szene, kein zweiter Interpret.
Korrekt hiesse also: **jede** der vier Anzeige-/Exportarten (SVG-Default,
SVG-Print-Vorschau, GPU-Viewport, PDF) muss aus **derselben** `weightMm`, für
**denselben** `N`, exakt dieselbe Papier-mm-Breite ergeben — bis auf die
bewusste PEN_STEPS-Rundung im Druckpfad, die dokumentiert und überall
gleichermassen sichtbar sein sollte (ist sie nicht, siehe Finding 2).
Ausdrücklich **kein** Teil dieses Modells: der Haarlinien-Modus
(`lineMode: "display"`, App-Default, `viewSlice.ts:38,74`). Er ist als
bewusster Papier-mm-*Ausstieg* gedacht ("Display: all lines as constant
hairlines (calm editing)", `en.ts:173`) — 1 Geräte-px, konstant, unabhängig
von Zoom/Massstab. Er muss also NICHT der Papier-mm-Formel folgen, aber er
muss in JEDEM Render-Pfad *gleichermassen* als Ausstieg wirken. Tut er nicht
(Finding 1).
## 2. Pfad-für-Pfad-Trace
### 2a. SVG-Default (App-Start ohne `?gl=0`, `lineMode:"display"`)
Nur aktiv, wenn WebGL2 fehlschlägt (`wantGl` ist sonst `true`, s. Finding 1) —
de facto der reine Fallback-Pfad.
- `PlanView.tsx:2613`: `print = !hairline && paperScale != null && paperScale > 0`
— mit `hairline=true` (Default) ist `print` immer `false`.
- `PlanView.tsx:2617-2618`:
```ts
const weight = (mm: number): number =>
hairline ? HAIRLINE_PX : print ? printStrokeVb(mm, paperScale!) : mmToPx(mm);
```
`HAIRLINE_PX = 1` (`PlanView.tsx:2583`), Einheit: Geräte-unabhängiger
CSS-Pixel, gezeichnet mit `vector-effect="non-scaling-stroke"`
(`PlanView.tsx:2621`, `vfx`) → bleibt beim Pan/Zoom optisch exakt 1 px, egal
wie stark reingezoomt wird. Papier-mm spielt hier explizit KEINE Rolle.
### 2b. SVG-Print-Vorschau (`lineMode:"print"`, weiterhin `?gl=0` oder
WebGL2-Fallback)
- `print = true`, `weight(mm) = printStrokeVb(mm, paperScale!)`
(`PlanView.tsx:2598-2600`):
```ts
function printStrokeVb(mm: number, n: number): number {
return Math.max(1e-4, (mm * n) / 1000) * PX_PER_M;
}
```
`vectorEffect` entfällt (`vfx = undefined`, `PlanView.tsx:2621`) — die Linie
skaliert MIT der Geometrie beim Zoomen, wie physisches Papier unter der
Lupe. `paperScale` selbst ist der **stabile, gemessene** 1:N-Nenner
(`PlanView.tsx:458-460`), nicht der live aus jedem Radzoom abgeleitete Wert
— sonst würde Reinzoomen die Linien nicht dicker, sondern konstant halten
(das wäre wieder Haarlinien-Verhalten). Keine PEN_STEPS-Rundung — die rohe
`mm`-Zahl wird 1:1 in viewBox-Einheiten übersetzt.
### 2c. GPU-Viewport (WebGL2 `useGlPlanRenderer.ts` — App-DEFAULT; WASM
`useWasmPlanRenderer.ts` bei `?engine=wasm`)
- Beide Hooks reichen `paperScaleN` unverändert an die Engine durch:
`useWasmPlanRenderer.ts:131-149` (`render(viewBox, paperScaleN=100,
textScaleN=100)` → `r.set_paper_scale(paperScaleN)`); WebGL2 analog
(`useGlPlanRenderer.ts:102-116`, kein `textScaleN`-Parameter überhaupt, s.
Finding 3).
- `PlanView.tsx` ruft in JEDEM Render-Aufruf
(`PlanView.tsx:494`,`502`,`1080`) `renderGl(view, paperScaleForGl(view),
textScaleForGl())`. `paperScaleForGl` (`PlanView.tsx:475-476`):
```ts
const paperScaleForGl = (v: ViewBox): number =>
paperScaleRef.current ?? scaleFromView(v, svgRef.current) ?? 100;
```
**Kein `hairline`-Zweig.** Egal ob `lineMode` "display" oder "print" ist,
hier kommt immer ein echter 1:N-Papier-Nenner heraus.
- Rust-Seite (`gpu.rs:606-616`):
```rust
let mm_px = mm_to_device_px(view_box, vw, vh, self.paper_scale_n);
```
`mm_to_device_px` (`ortho.rs:96-99`) reproduziert exakt `printStrokeVb`:
`(paper_scale_n/1000.0) * PX_PER_M * meet` — `PX_PER_M = 90.0`
(`tessellate.rs:22`, identisch zu TS). Die reine Mathematik ist also
deckungsgleich zum SVG-Print-Pfad (2b) — aber sie läuft **immer**, auch wenn
der Nutzer "Display: Haarlinien" gewählt hat. Siehe Finding 1.
- Stiftbreite pro Batch im Shader (`shaders.rs:91`, `LINE_WGSL`):
`width_px = max(0.6, stroke_px * stroke_scale) * miter` — harte Untergrenze
0.6 Geräte-px, die weder die SVG- noch die PDF-Seite kennt (Finding 6).
Identische Formel für Bögen (`shaders.rs:209`, `ARC_WGSL`) und im
WebGL2-Pfad (`glPlanRender.ts:243-249`, Kommentar "klemmt bei ~0.6 px").
- Schraffur-Musterlinien laufen NICHT über `mm_px`, sondern über
`width_screen`/`px_per_screen` (`gpu.rs:661-665`): Geräte-px = Breite ×
`meet`-Skala statt × mm→px — bewusst zoom-skalierend wie das SVG-`<pattern>`,
nicht papierkonstant (s. `toRenderScene.ts:371-375`, Finding 4).
### 2d. Vektor-PDF (`exportPdf.ts` → `sceneToPrintSvg.ts`)
- `exportPdf.ts:73`: `const scene = planToRenderScene(plan);` — dieselbe
Szene wie 2c, unabhängig vom aktuell gewählten `lineMode` der Ansicht.
- `sceneToPrintSvg.ts:243-244`:
```ts
const effectiveMm = widthScreen ? (widthMm / PX_PER_M) * mmPerM : widthMm;
const strokeMm = quantizePen(effectiveMm);
```
`PEN_STEPS = [0.13, 0.18, 0.25, 0.35, 0.5, 0.7, 1.0]`, `MIN_PEN_MM = 0.13`
(`sceneToPrintSvg.ts:44,47`). JEDE Linie/Umriss/Bogen/Schraffur wird auf die
nächsthöhere Stufe gerundet, bevor sie ins SVG/PDF geht
(`sceneToPrintSvg.ts:251,274,301`). Das ist der EINZIGE der vier Pfade, der
überhaupt quantisiert.
- `widthScreen`-Konvertierung: `widthMm` (hier eigentlich viewBox-Einheiten,
siehe 2c) wird über `/PX_PER_M * mmPerM` zurück in Weltmeter und dann in
Papier-mm beim GEWÄHLTEN `opts.scaleDenominator` übersetzt — nicht beim
Massstab, den die Live-Ansicht gerade zeigt (Finding 4).
- Text: `sizeMm` kommt unverändert aus `RText.sizeMm`
(`sceneToPrintSvg.ts:325`, `t.sizeMm`, keine Nachskalierung) — aber die
VERTIKALE Zeilenposition dieser Texte wurde in `toRenderScene.ts` mit einem
festen `STAMP_REF_N = 100` in Weltmeter umgerechnet (`toRenderScene.ts:163-165,
481-483`), unabhängig vom tatsächlich gewählten Export-`N` (Finding 3).
## 3. Findings (nach Schwere geordnet)
### 1. Haarlinien-Modus (App-Default) wird vom GPU-Renderer komplett ignoriert — betrifft den Standard-Zustand der App
`lineMode` startet auf `"display"` (`viewSlice.ts:74`), und WebGL2 ist der
Default-Renderer (`wantGl` ist `true`, ausser `?gl=0`,
`PlanView.tsx:433-437`) — **d. h. im frisch geladenen, unkonfigurierten App-
Zustand rendert bereits der GPU-Pfad, und die "Display: Haarlinien"-Option
tut nichts.** `paperScaleForGl` (`PlanView.tsx:475-476`) liest nur
`paperScaleRef.current ?? scaleFromView(...) ?? 100` — der `hairline`-Boolean
aus den Props (`PlanView.tsx:2571`) wird an dieser Stelle nie geprüft, obwohl
das benachbarte `textScaleForGl` (`PlanView.tsx:485-488`) exakt diesen Zweig
korrekt hat:
```ts
const textScaleForGl = (): number => {
const print = !hairline && paperScale != null && paperScale > 0;
return print ? paperScale! : 100;
};
```
Ergebnis: Text (Raumstempel) fällt im Haarlinien-Modus korrekt auf die
Referenzskala 100 zurück, aber jede Wand-/Tür-/2D-Zeichenlinie wird trotzdem
mit dem echten (gemessenen oder geschätzten) `paperScale` gezeichnet — sie
wird beim Reinzoomen dicker statt konstant zu bleiben, exakt das Gegenteil
dessen, was der Menüpunkt verspricht ("calm editing", `en.ts:173`). Der Bug
betrifft sowohl `useGlPlanRenderer.ts` (Default) als auch
`useWasmPlanRenderer.ts` (`?engine=wasm`) — keiner der beiden `render()`-
Aufrufe kennt einen `hairline`-Parameter überhaupt.
### 2. PEN_STEPS-Quantisierung existiert NUR im PDF-Export — beide Live-Vorschauen (SVG-Print und GPU) zeigen unquantisierte Rohwerte
Konkret, mit den tatsächlichen Konstanten aus `generatePlan.ts`:
- `LAYER_LINE_MM = 0.13` (`generatePlan.ts:82`), `LAYER_DETAIL_FACTOR.fein =
0.7` (`generatePlan.ts:90-93`) → Schichtfuge im Detailgrad "fein":
`0.13 · 0.7 = 0.091 mm` (`generatePlan.ts:1022`,
`strokeWidthMm: LAYER_LINE_MM * LAYER_DETAIL_FACTOR[detail]`). Die
Bildschirm-Print-Vorschau zeigt genau diese 0.091 mm (`printStrokeVb(0.091,
N)`); der PDF-Export klemmt via `quantizePen` auf `MIN_PEN_MM = 0.13` mm —
**+43 % dicker im Druck als in der Vorschau.**
- Türschwenkbogen: `doorLwMm * 0.6` (`generatePlan.ts:902`) mit dem üblichen
Fallback `doorLwMm = LAYER_LINE_MM = 0.13` (`generatePlan.ts:354`) ergibt
`0.078 mm`. Screen zeigt 0.078 mm (nahezu unsichtbar dünn bei kleinen
Massstäben), PDF klemmt auf 0.13 mm — **+67 %.**
- Wand-Umriss im Detailgrad "grob": `outlineMm = wallLwMm ·
OUTLINE_DETAIL_FACTOR.grob (1.6)` (`generatePlan.ts:84-88, 648, 796`); beim
häufigen Fallback `WALL_FALLBACK_MM = 0.18` (`generatePlan.ts:97`) ergibt das
`0.288 mm`. `quantizePen(0.288)` rundet auf die nächste Stufe `0.35`
(`PEN_STEPS`, da `0.25 < 0.288 ≤ 0.35`) — **+21.5 % im Druck.**
Das systematische Muster: der Detailgrad "fein" existiert explizit, um
Nebenlinien DÜNNER zu machen (`generatePlan.ts:52-58`,
`LAYER_DETAIL_FACTOR.fein = 0.7`) — aber sobald der berechnete Wert unter
`MIN_PEN_MM` fällt, hebt die PDF-Quantisierung ihn wieder auf die Untergrenze
an, ohne dass die Bildschirm-Vorschau (weder SVG-Print noch GPU) davon
irgendetwas zeigt. Wer die Druckvorschau am Bildschirm beurteilt, sieht NICHT,
was tatsächlich gedruckt wird.
### 3. Stempel-Zeilenabstand ist nur bei Massstab 1:100 exakt — SVG- und RenderScene-Pfad nutzen zwei verschiedene Formeln für dieselbe Grösse
Bereits als bekannte Alt-Lücke in HANDOVER.md (Commit `382771b`, Punkt 6)
vermerkt; hier präzise verortet:
- SVG (`PlanView.tsx:2738-2739`):
```ts
const nRef = print ? paperScale! : 100;
const unitPerPt = (1 / 72) * 0.0254 * nRef * PX_PER_M;
```
`nRef` folgt im Druckmodus dem TATSÄCHLICH aktiven `paperScale` — der
Zeilenabstand (`lineGap = baseFs * 1.3`, `PlanView.tsx:2741`) ist für jedes
`N` korrekt in viewBox-Einheiten, weil er direkt aus `unitPerPt` (das `N`
enthält) abgeleitet wird.
- RenderScene (`toRenderScene.ts:163-165`):
```ts
const STAMP_REF_N = 100;
const MM_TO_M = STAMP_REF_N / 1000;
```
und (`toRenderScene.ts:481-483`):
```ts
const baseH = baseMm * MM_TO_M; // Basisgröße in Modell-Metern
const lineGap = baseH * 1.3;
```
Hier ist der Umrechnungsfaktor `MM_TO_M` FEST auf `N = 100` verdrahtet,
unabhängig davon, mit welchem `N` der WASM-Viewport oder der PDF-Export
tatsächlich rendert (`paper_scale_n` in `gpu.rs`, `opts.scaleDenominator` in
`sceneToPrintSvg.ts`). Bei `N = 100` sind beide Formeln identisch (das war
offenbar der Verifikationsfall in `382771b`); bei jedem anderen `N` — z. B.
1:50 — driftet der vertikale Zeilenabstand der Stempel-Mehrzeiler
proportional zum Verhältnis `N_wahr/100` auseinander, während die einzelne
Zeilenhöhe (`sizeMm`) selbst korrekt bleibt. Betroffen: WASM-Viewport (2c)
UND PDF-Export (2d) gleichermassen (beide beziehen `RText` aus derselben
`toRenderScene`-Funktion); die WebGL2-Ansicht ist NICHT betroffen, weil sie
Text grundsätzlich nicht selbst zeichnet, sondern die SVG-Overlay-Ebene
weiterverwendet (`PlanView.tsx:1592`,
`useGpuRenderer ? wantWasm || p.kind !== "text" : ...` — bei WebGL2
(`wantWasm=false`) werden Text-Primitive NIE aus dem SVG-Rendering
herausgefiltert).
### 4. Schraffur-Strichbreite im PDF hängt vom GEWÄHLTEN Export-Massstab ab, nicht vom Massstab der Live-Vorschau
`hatchPx` wird in `toRenderScene.ts:374-375` genau einmal, massstabsunabhängig
berechnet:
```ts
const hatchMm = p.hatch.lineWeight > 0 ? p.hatch.lineWeight : 0.13;
const hatchPx = Math.max(0.6, hatchMm * (1 / 0.13));
```
— eine reine viewBox-Grösse (analog zum SVG-`<pattern>`-Strich, der
absichtlich mit dem Zoom mitskaliert, damit die Schraffur bei jedem Zoom
gleich dicht aussieht: Kommentar `toRenderScene.ts:371-373`). Erst beim
Export wird daraus in `sceneToPrintSvg.ts:243`
`effectiveMm = (widthMm / PX_PER_M) * mmPerM` — und `mmPerM = 1000 /
opts.scaleDenominator` (`sceneToPrintSvg.ts:101`) verwendet den vom
Export-Dialog gewählten Massstab, NICHT den `paperScale`, den der Nutzer
gerade in der "Print"-Live-Vorschau sieht (`exportPdf.ts` ruft
`sceneToPrintSvg` mit `opts.scaleDenominator` aus den Export-Optionen,
`exportPdf.ts:61-79`, völlig unabhängig vom `PlanView`-State).
Beispiel: `lineWeight = 0.13` mm (Standard-Fuge, `getHatch`-Default) →
`hatchPx = max(0.6, 0.13 · (1/0.13)) = 1.0` viewBox-Einheiten.
- Export bei 1:50 (`mmPerM = 20`): `effectiveMm = (1.0/90)·20 = 0.222 mm` →
`quantizePen` → **0.25 mm**.
- Export bei 1:200 (`mmPerM = 5`): `effectiveMm = (1.0/90)·5 = 0.056 mm` →
auf `MIN_PEN_MM` geklemmt → **0.13 mm**.
Fast die doppelte Strichstärke allein durch die Wahl des Export-Massstabs,
bei UNVERÄNDERTER Quell-`lineWeight` — und ohne dass die Bildschirm-
"Print"-Vorschau (die ja mit dem live gemessenen `paperScale` arbeitet,
nicht mit `opts.scaleDenominator`) das anzeigen könnte, solange beide Werte
nicht zufällig übereinstimmen.
### 5. Dieselbe Formel existiert vierfach, nur durch Kommentare (nicht durch Typen/Code) synchron gehalten
`PX_PER_M = 90` ist unabhängig deklariert in: `PlanView.tsx:23` (SVG),
`glPlan/glPlanRender.ts:12` (WebGL2), `tessellate.rs:22` (Rust, geteilt
zwischen WASM-Viewport und Headless/Golden-Test) und `sceneToPrintSvg.ts:64`
(PDF-Serializer, mit explizitem Kommentar "MUSS mit dem dortigen
uebereinstimmen, sonst driftet die Schraffur-Dichte des PDFs vom Viewport
ab", `sceneToPrintSvg.ts:61-62`). Die Hatch-Dichte-Formel
`Math.max(0.6, weightMm · (1/0.13))` ist separat dupliziert in
`PlanView.tsx:2344-2345` (`hatchStrokePx`, SVG-`<pattern>`) und
`toRenderScene.ts:375` (`hatchPx`, GPU + PDF) — beide mit demselben
"magischen" Faktor `1/0.13`, ohne gemeinsame Konstante. Aktuell sind alle vier
Kopien konsistent; das Risiko ist rein prospektiv (nächste Tuning-Änderung an
einer Stelle vergisst die anderen drei) — aber genau das ist der
Mechanismus, über den Findings wie 1-4 überhaupt erst entstehen können, ohne
dass ein Typfehler oder Test anschlägt.
### 6. Drei unabhängige, nicht aufeinander abgestimmte Mindestbreiten-Politiken
- SVG-Print: `Math.max(1e-4, ...)` (`PlanView.tsx:2599`) — praktisch keine
Untergrenze, verlässt sich auf das Antialiasing des Browsers.
- GPU (WebGL2 UND WASM, Linien UND Bögen): harter Floor von **0.6
Geräte-Pixel** (`shaders.rs:91,209`; `glPlanRender.ts:243-249`,
Kommentar "klemmt bei ~0.6 px").
- PDF: **`MIN_PEN_MM = 0.13` mm** (`sceneToPrintSvg.ts:38` in
`planToPrintSvg.ts`, äquivalent `sceneToPrintSvg.ts:47`) — eine
Papier-mm-Grösse, kein Pixelwert, konzeptionell etwas anderes als die
GPU-Pixel-Untergrenze.
Auswirkung: bei sehr kleinem Massstab (weit rausgezoomt oder grosses `N`,
z. B. Übersichtsplan 1:500) hält der GPU-Pfad sehr dünne Linien künstlich bei
0.6 px sichtbar, während dieselbe Linie im SVG-Pfad fast verschwindet und im
PDF auf eine ganz andere (mm-basierte, massstabsabhängige) Grösse geklemmt
wird. Kein Pfad kennt die Politik der anderen beiden. Niedrigere Priorität
als 1-4, weil hier keine der drei Politiken die *exportierte* Papier-mm-Wahrheit
verändert (die bleibt PDF-exklusiv über `MIN_PEN_MM`) — es geht nur um
Bildschirm-Konsistenz zwischen SVG/GPU bei Extremzoom.
### 7. Toter Zweitpfad `planToPrintSvg.ts` dupliziert PEN_STEPS/mm-Logik komplett, unbenutzt aber vorhanden
`src/export/planToPrintSvg.ts` trägt seit `382771b` einen Kopfkommentar
"ERSETZT durch sceneToPrintSvg.ts … wird vom PDF-Export NICHT MEHR
verwendet" (`planToPrintSvg.ts:1-6`) und ist tatsächlich nirgends mehr
importiert (verifiziert per Grep über `src/`). Er enthält jedoch weiterhin
eine eigene, unabhängige Kopie von `PEN_STEPS`/`MIN_PEN_MM`/`quantizePen`
(`planToPrintSvg.ts:35,38,40-45`) und einer kompletten Plan→SVG-Serialisierung
inkl. eigener `PX_PER_M`-Handhabung (`planToPrintSvg.ts:326`). Kein aktiver
Bug, aber eine Falle: Copy-Paste-Wiederverwendung dieses Altpfads (z. B. für
einen zukünftigen DXF/PNG-Export) würde eine dritte, potenziell abweichende
Quantisierungs-Tabelle in den Baum ziehen.
## 4. Vorgeschlagene Fixes
### zu Finding 1 (Haarlinien-Modus im GPU-Pfad)
Kleinste, korrekte Lösung: den `hairline`-Zustand als eigenen Parameter bis
in den Shader durchreichen, analog zu `paper_scale_n`/`text_scale_n`.
- **Rust (`src-tauri/render2d/src/gpu.rs`)**: neues Feld
`pub hairline: bool` auf `Renderer` (Default `false`, neben `paper_scale_n`
bei `gpu.rs:422`). In `Renderer::render` (`gpu.rs:606-616`): wenn
`self.hairline`, `mm_px` NICHT aus `mm_to_device_px(...)` berechnen, sondern
auf einen konstanten Geräte-px-Wert setzen (z. B. `1.0`), UND dafür sorgen,
dass der Shader `width_mm` in diesem Fall ignoriert (sonst bleibt die
RELATIVE Differenz zwischen z. B. 0.13 mm und 0.35 mm bestehen, nur global
skaliert — nicht das gewünschte "alle Linien exakt 1 px"). Sauberster Weg:
ein neues `hairline: u32`-Feld in die pro-Frame-Uniform (`Globals`,
`shaders.rs:20-23` und `ArcGlobals`), im Fragment-/Vertex-Shader
(`shaders.rs:91`, `LINE_WGSL`; `shaders.rs:209`, `ARC_WGSL`) per
`select(...)` auf einen festen `1.0`-px-Wert umschalten statt
`max(0.6, stroke_px * stroke_scale)`.
- **`src-tauri/render2d/src/web.rs`**: neue Methode `set_hairline(&mut self,
on: bool)` neben `set_paper_scale`/`set_text_scale`
(`web.rs:146-155`-Nachbarschaft).
- **`src/plan/useWasmPlanRenderer.ts`**: `render()`
(`useWasmPlanRenderer.ts:131-155`) um einen `hairline: boolean`-Parameter
erweitern, `r.set_hairline(hairline)` vor `r.render()` aufrufen.
- **`src/plan/useGlPlanRenderer.ts`**: analog — `render()`
(Signatur aktuell `(viewBox, paperScaleN=100)`) um `hairline` erweitern;
im WebGL2-Shader-Uniform-Pfad (`glPlanRender.ts:159-165,213-215`)
`mmToDevicePx` bei `hairline===true` durch einen konstanten Wert ersetzen
und im Fragment-Shader denselben `select`-Trick wie oben anwenden
(`glPlanShaders.ts`, dort wo "~0.6 px"-Klemmung passiert, siehe Finding 6).
- **`src/plan/PlanView.tsx`**: `paperScaleForGl` (`PlanView.tsx:475-476`)
bleibt wie sie ist (wird weiter für den Massstab-Nenner gebraucht, sobald
der Nutzer zurück auf "Print" schaltet); stattdessen an allen drei
`renderGl(...)`-Aufrufstellen (`PlanView.tsx:494,502,1080`) zusätzlich
`hairline` durchreichen: `renderGl(view, paperScaleForGl(view),
textScaleForGl(), hairline)`.
### zu Finding 2 (PEN_STEPS nur im PDF)
Zwei mögliche Richtungen, je nach gewünschtem Produktverhalten:
- **Option A (Vorschau matcht Druck)**: `quantizePen`
(`sceneToPrintSvg.ts:50-54`) in ein gemeinsames Modul auslagern (z. B.
`src/plan/penSteps.ts`, exportiert `PEN_STEPS`, `MIN_PEN_MM`,
`quantizePen`), von `sceneToPrintSvg.ts` UND von `PlanView.tsx`
(`printStrokeVb`, `PlanView.tsx:2598-2600`) importieren und dort ebenfalls
anwenden: `printStrokeVb(mm, n) = Math.max(1e-4, (quantizePen(mm) * n) /
1000) * PX_PER_M`. Für den GPU-Pfad müsste die Quantisierung dann VOR dem
Scene-Bau passieren (in `toRenderScene.ts`, auf `widthMm` jeder
`RLine`/`ROutline`/`RPolyline`/`RArc`, ausgenommen `widthScreen`-Einträge),
damit WASM/WebGL2-Viewport dieselben Stufen zeigen wie SVG und PDF.
- **Option B (Vorschau bleibt Rohgrösse, aber sichtbar gemacht)**: falls die
feinkörnige Vorschau bewusst erhalten bleiben soll, zumindest den
Stiftstufen-Sprung an der Stelle sichtbar machen, an der die Detailgrad-
Faktoren definiert werden (`generatePlan.ts:84-93`) — z. B. ein
Entwickler-/Lint-Test, der bei jedem `OUTLINE_DETAIL_FACTOR`/
`LAYER_DETAIL_FACTOR`-Wert prüft, ob das Produkt mit den üblichen
Fallback-`weightMm`-Werten (`WALL_FALLBACK_MM`, `LAYER_LINE_MM`) exakt auf
einer `PEN_STEPS`-Stufe landet, und sonst warnt.
Empfehlung: Option A — sie erfüllt den Nordstern wörtlich ("Bildschirm bei
jedem Massstab = Druck").
### zu Finding 3 (STAMP_REF_N)
In `toRenderScene.ts` `STAMP_REF_N = 100` (`toRenderScene.ts:163`) durch den
tatsächlichen Ziel-Massstab ersetzen. `planToRenderScene(plan)` kennt aktuell
keinen `N`-Parameter (`exportPdf.ts:73` ruft es ohne Massstabsangabe auf) —
die Funktion müsste ein optionales `paperScaleN`-Argument bekommen (Default
100, um den Viewport-Aufruf ohne Massstabskontext — `nativeSync.ts` — nicht
zu brechen, dort ist 100 ohnehin die richtige Referenz für den WASM-Viewport
im Anzeigemodus, s. `gpu.rs:422`), und `exportPdf.ts:73` müsste
`planToRenderScene(plan, opts.scaleDenominator)` aufrufen. `MM_TO_M`
(`toRenderScene.ts:165`) dann aus diesem Parameter statt aus der Konstante
ableiten.
### zu Finding 4 (Hatch-mm im PDF folgt dem Export-N, nicht dem Vorschau-N)
Kein Bug im engeren Sinn (die Formel ist in sich konsistent — die Schraffur
soll ja pro *gewähltem Blatt-Massstab* eine sinnvolle Dichte haben), aber die
Bildschirm-„Print"-Vorschau sollte denselben `N` verwenden, den der
Export-Dialog tatsächlich benutzen wird, sonst lügt die Vorschau. Fix:
`paperScale` in `PlanView.tsx` beim Öffnen des Export-Dialogs (bzw. der
Export-Dialog selbst) mit `opts.scaleDenominator` vorbelegen/synchronisieren,
statt zwei unabhängige State-Quellen zu pflegen — Ort: dort, wo der
Export-Dialog `scaleDenominator` initialisiert (App.tsx, PDF-Export-UI) einen
Default aus `paperScale`/`liveScale` (`App.tsx`, `onScale`-Callback,
`PlanView.tsx:794`) übernehmen.
### zu Finding 5 (vierfache Formel-Duplikation)
Eine gemeinsame TS-Konstantendatei `src/plan/renderConstants.ts` mit
`PX_PER_M = 90`, `HATCH_DENSITY_FACTOR = 1/0.13`, `HATCH_MIN_PX = 0.6`
anlegen; `PlanView.tsx`, `glPlan/glPlanRender.ts`, `toRenderScene.ts`,
`sceneToPrintSvg.ts` importieren daraus statt eigener Literale. Für die
Rust-Seite (`tessellate.rs:22`, `shaders.rs:142`) ist echtes Teilen über die
Sprachgrenze hinweg nicht trivial — dort bleibt nur ein Kommentar-Link auf die
TS-Konstante plus ein Parity-Test (siehe Abschnitt 5), der bei Abweichung
fehlschlägt statt bei stillem Drift.
### zu Finding 6 (drei Mindestbreiten-Politiken)
Niedrige Priorität, aber falls angegangen: den 0.6-px-Floor aus den Shadern
(`shaders.rs:91,209`, `glPlanShaders.ts`) als benannte Konstante
exportieren/dokumentieren und explizit von `1e-4` (SVG) und `MIN_PEN_MM=0.13`
(PDF) abgrenzen — mindestens per Kommentar klarstellen, dass die drei
UNTERSCHIEDLICHE Zwecke haben (GPU: Pixel-Sichtbarkeits-Floor;
PDF: Papier-mm-Stiftgrenze) und NICHT synchronisiert werden müssen, damit
niemand versehentlich versucht, sie anzugleichen und dabei die jeweils
andere Semantik bricht.
### zu Finding 7 (toter Pfad)
`src/export/planToPrintSvg.ts` entfernen (nach kurzer Rücksprache, ob der
Kopfkommentar-Vermerk "bleibt nur als Referenz stehen" noch gewollt ist) oder,
falls er als Referenz bleiben soll, seine `PEN_STEPS`/`quantizePen`-Kopie
durch einen Import aus der in Finding 2 vorgeschlagenen gemeinsamen
`penSteps.ts` ersetzen.
## 5. Verifikations-Rezept
Was schon existiert:
- `scripts/probe-engine-parity.mjs` vergleicht `?gl=0` gegen `?engine=wasm`
rein visuell (Screenshot-Diff von Auge, `probe-parity-svg.png` vs.
`probe-parity-wasm.png`) — prüft NICHT quantitativ, ob Strichbreiten in
Pixeln übereinstimmen, und deckt weder den Haarlinien-Modus noch den
WebGL2-Default-Pfad noch den PDF-Export ab.
- `src-tauri/render2d/tests/golden.rs` vergleicht die Demo-Szene
Pixel-für-Pixel gegen `tests/golden/demo.png` (Toleranz: Kanal-Delta > 2 auf
< 0.5 % der Pixel, `docs/design/engine-headless.md`) — ein reiner
GPU-Regressionstest, ohne SVG- oder PDF-Vergleich.
- Die einzige bisher dokumentierte mm-genaue Messung ist manuell:
HANDOVER.md, Commit `382771b` — `pdftoppm`-Messung eines exportierten PDFs
(53.51×43.69 mm gegen erwartete 53.45×43.45 mm bei 1:100), einmalig, nicht
als wiederholbares Skript hinterlegt.
Um den Nordstern ("Bildschirm bei jedem Zoom/Massstab = Druck") tatsächlich
beweisbar zu machen, fehlt ein quantitativer End-to-End-Test, der:
1. Für eine feste Test-Szene (idealerweise die bestehende `demo::demo_scene`
aus `src-tauri/render2d/src/demo.rs`, die bereits sowohl einen `width_mm`-
als auch einen `width_screen`-Strich enthält, `demo.rs:34,49-50`) bei
mehreren `N` (1:10, 1:50, 1:100) UND mehreren Zoomstufen:
- den PDF-Export erzeugt und via `pdftoppm -r <dpi>` in ein PNG rendert,
dort die Strichbreite in Pixeln misst und in mm zurückrechnet (bekannte
DPI ⇒ bekannte mm/px);
- denselben Frame headless über `HeadlessRenderer::render_to_image`
(`src-tauri/render2d/src/headless.rs`) mit identischem `paper_scale_n`
rendert und dieselbe Pixel-Breite misst;
- beide mm-Werte gegen den erwarteten `mm · N/1000`-Sollwert UND
gegeneinander vergleicht, mit einer Toleranz, die die PEN_STEPS-Rundung
(Finding 2, sobald behoben: siehe Option A) einbezieht.
2. Den Haarlinien-Modus separat abdeckt: ein Screenshot-Vergleich bei zwei
verschiedenen Zoomstufen im `lineMode:"display"` — die gemessene
Pixelbreite MUSS bei beiden Zoomstufen identisch sein (Beweis, dass
Finding 1 behoben ist), sowohl für WebGL2 (`?gl` ohne `=0`, App-Default)
als auch für WASM (`?engine=wasm`).
3. `probe-engine-parity.mjs` um eine tatsächliche Pixel-Differenz-Metrik
erweitert (statt nur Kindanzahl/Warnungen zu loggen wie aktuell
`probe-engine-parity.mjs:36-44`) — z. B. eine bekannte Referenzlinie im
Testmodell an fester Bildschirmposition, deren Strichbreite in beiden
Screenshots per Pixel-Sampling gemessen und verglichen wird.
Bis dieser Test existiert, bleibt jede Aussage über "Bildschirm = Druck" eine
Behauptung, die nur durch manuelles `pdftoppm`-Nachmessen einzelner
Stichproben gedeckt ist — die in diesem Audit gefundenen Divergenzen (v. a.
Finding 1 und 2) hätten mit den heutigen Probes nicht auffallen können, weil
keiner von ihnen den Default-Zustand der App (`lineMode:"display"`, WebGL2)
gegen den Druckpfad misst.
-186
View File
@@ -1,186 +0,0 @@
# 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.
-74
View File
@@ -1,74 +0,0 @@
# Ribbon-UI + modulare Bars — Plan
Nutzer-Vision (2026-07-05, mit OCS/AutoCAD-Ribbon als Referenz). Ziel: klare,
einheitliche Werkzeug-/Eigenschaften-Darstellung; Werkzeug-Sidebar entfällt.
## Zielbild
- **Ribbon-Oberleiste mit Tabs: 2D · 3D · BIM · Ansichten.** Jeder Tab zeigt
gruppierte Werkzeug-/Aktions-Icons (wie OCS Draw/Model/Insert/Annotate/View).
- **2D**: Zeichnen (Select/Linie/Polylinie/Rechteck/Kreis/Bogen/Text) · Ändern
(Move/Copy/Mirror/Offset/Trim/Join/Boolean).
- **3D**: Volumen/Boolean (vorhandene 3D-Aktionen), Kamera-Presets.
- **BIM**: Wand/Fenster/Tür/Treppe/Decke/Raum (die Bauteile).
- **Ansichten**: Geschoss/Schnitt/Ansicht-Wechsel, Zoom/Einpassen, Render-Modi.
- **Werkzeug-Sidebar entfällt** → rechtes Panel (Attribute) bekommt volle Höhe.
- **Objektinfo unter die Attribute** mergen (Wandstil-Definition etc. zusammen).
- **XYZ-Referenzpunkt-Box oben rechts bleibt** (vom Nutzer ausdrücklich gewünscht).
- **Eigenschaften-Grid im OCS-Stil** — ✅ bereits erledigt (`00733d8`): Sektion-
Balken, Zeilentrenner, füllende linksbündige Wertfelder (`.attr-*`).
## Architektur — DATENGETRIEBEN (ermöglicht modulare Custom-Bar)
Kern: EINE Registry beschreibt alle Ribbon-Elemente; Ribbon-Tabs UND eine
benutzerdefinierte Custom-Bar sind bloß zwei Ansichten über dieselbe Registry.
```
RibbonItem =
| { kind: "tool"; id: ToolId } // aktiviert ein Werkzeug (onSelectTool)
| { kind: "command"; name: string } // startet einen Engine-Befehl (engine.start)
| { kind: "action"; id: string; run: () => void } // freie App-Aktion (Zoom, Render-Modus …)
RibbonGroup = { titleKey: string; items: RibbonItem[] }
RibbonTab = { id: "2d"|"3d"|"bim"|"views"; labelKey: string; groups: RibbonGroup[] }
RIBBON: RibbonTab[]
```
- Icons: `ToolIcon` (vorhanden) für `tool`-Items; für `command`/`action` eine
kleine Icon-Map (SVG) analog `ToolsPanel`.
- Aktivierung: `tool``onSelectTool(id)` · `command``engine.start(name)` ·
`action``run()`. Aktiv-Highlight über `activeTool` bzw. laufenden Befehl.
- **Custom-Bar (modular)**: der Nutzer wählt beliebige `RibbonItem`s in eine
persistierte Liste (`viewSlice`/Projekt); eine `CustomBar`-Komponente rendert
genau diese. Gleiche Item-Typen, gleiche Aktivierung — kein Sonderweg.
## Phasen
1.**Gerüst + 2D/BIM-Tab** (`9d6e86d`). `RibbonBar` unter der TopBar (additiv).
Datengetriebene Registry (`src/ui/ribbon/ribbonItems.ts`), 2D-Tab (Zeichnen +
Ändern) und BIM-Tab (Bauteile) gefüllt, Tab-State lokal, Modify-Icons ergänzt.
2.**TopBar → „Ansichten"-Tab gemergt** (`85011cb`, Nutzer-Entscheid „voll
mergen"). Die Ansichts-/Zoom-/Darstellungs-Cluster der TopBar (View-Grid +
Kamera, Ebenen-/Zeichnungs-Kombis, Detailgrad, Massstab/Zoom, Darstellungsart)
sind als `ViewRibbonTab` (in `TopBar.tsx`) in den „Ansichten"-Tab gewandert;
App reicht sie als `viewsContent`-Node an `RibbonBar` (wie das Layout-Menü).
Die **Tab-Reiter sitzen in der TopBar-Zeile** (`RibbonTabs`, in TopBar
gerendert; Tab-State in App), der Ribbon-Inhalt (`RibbonBar`) folgt darunter →
nur ZWEI Bänder. Die TopBar ist eine **schmale Leiste** (Höhe 40px): Marke/
Ressourcen · Tabs · Datei/Export/Einstellungen · Fensterknöpfe. Die Text-/
Font-Formatierung (`TextGroup`) liegt im eigenen **„Text"-Tab** (Reihenfolge
2D·3D·BIM·Text·Ansichten; `RibbonBar.tabContent` trägt „text" + „views").
- **Offen (visuell iterieren):** ob Zoom/Massstab zusätzlich als Dauer-Anzeige
(Statusleiste) sichtbar sein soll; Default-Tab (aktuell 2D); 3D-Tab noch
leer (Kamera-Presets/3D-Aktionen füllen).
3. **Sidebar raus** (ToolsPanel aus Default-Layout) + rechtes Panel volle Höhe;
Objektinfo unter Attribute mergen.
4. **Modulare Custom-Bar**: Item-Picker + persistierte Custom-Bar-Ansicht.
## Hinweise / Randbedingungen
- Additiv bauen: bestehende Kommandozeile + Nummern-Shortcuts + `ToolsPanel`
bleiben funktionsfähig, bis Phase 3 die Sidebar bewusst entfernt.
- Werkzeug-Kopplung Tool↔Befehl existiert bereits (`TOOL_COMMAND`) — Ribbon nutzt
denselben `onSelectTool`/`engine.start`-Pfad (EIN Pfad, keine Duplikate).
- Empfehlung: Phase 1+ in fokussierter Session mit visueller Iteration im Tauri.
+1 -1
View File
@@ -146,6 +146,6 @@ Neue Keys in de.ts UND en.ts: `text.style`, `text.font`, `text.size`,
## 7. Gate (Pflicht) ## 7. Gate (Pflicht)
`rm -f tsconfig.tsbuildinfo && npx tsc -b` grün, `npm run build` grün, keine `rm -f tsconfig.tsbuildinfo && npx tsc -b` grün, `npm run build` grün, keine
Trace-Scan (grep auf Co-Authored/Generated), Boot-Probe AI-Spuren (grep auf Claude/Anthropic/AI/Generated/Co-Authored), Boot-Probe
(`node scripts/probe.mjs`) ohne Konsolenfehler, Screenshot der Oberleiste. (`node scripts/probe.mjs`) ohne Konsolenfehler, Screenshot der Oberleiste.
KEIN Commit. KEIN Commit.
-403
View File
@@ -1,403 +0,0 @@
# truck-Integration — Profil-Extrusion (B-Rep → 3D-Mesh)
> Übergabe-Dokument für die Implementierung. Lies zuerst CONVENTIONS.md.
## Ziel
Nutzer zeichnen im Grundriss ein geschlossenes Polygon (z. B. L-Profil einer Stütze)
und können es als 3D-Körper auf eine Höhe extrudieren. Das Ergebnis erscheint sofort
im 3D-Viewport neben Wänden und Decken.
**MVP-Scope (dieser Auftrag):**
- Neue Rust/WASM-Crate `trucksolid` mit zwei Funktionen: `extrude_polygon` + `extrude_circle`
- Neue TS-Wrapper-Datei `src/engine/truckSolid.ts` (WASM laden + typisierte API)
- KEIN neues Werkzeug, KEIN UI — nur die Geometrie-Schicht. UI kommt in einem
separaten Folgeauftrag.
---
## Architektur-Kontext
```
src-tauri/
render3d/ ← bestehend: wgpu-Renderer (wasm-pack → pkg3d/)
kernel2d/ ← bestehend: 2D-Geometrie-Kern
trucksolid/ ← NEU (dieser Auftrag)
Cargo.toml
src/
lib.rs
src/engine/
pkg3d/ ← render3d WASM-Output
pkgTruck/ ← NEU: trucksolid WASM-Output (wasm-pack Ziel)
truckSolid.ts ← NEU: TS-Wrapper
src/plan/
toWalls3d.ts ← bestehend: baut RenderScene; emitMeshes() muss erweitert werden
```
Das Koordinatensystem in DOSSIER ist **Modell-Meter: X = rechts, Y = oben (Grundriss),
Z = Höhe**. render3d erwartet `positions` im gleichen System — `mesh.rs` macht intern
`(x, y, z) → (x, z, y)` für wgpu (Y-up world). Truck arbeitet in 3D; wir bauen
das Polygon in der XY-Ebene (z=0) und extrudieren nach +Z.
---
## 1 — Rust-Crate `src-tauri/trucksolid/`
### 1.1 Cargo.toml
Exakt dasselbe Muster wie `src-tauri/kernel2d/Cargo.toml`:
```toml
[workspace] # entkoppelt vom cad-tauri-Workspace (Pflicht)
[package]
name = "trucksolid"
version = "0.1.0"
edition = "2021"
description = "Profil-Extrusion via truck (B-Rep → tesselliertes Mesh für render3d)"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = []
web = [
"dep:wasm-bindgen",
"dep:serde_json",
"dep:console_error_panic_hook",
]
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", optional = true }
wasm-bindgen = { version = "0.2", optional = true }
console_error_panic_hook = { version = "0.1", optional = true }
# truck: nur die stabilen Crates (KEIN truck-modeling — Booleans instabil)
truck-geometry = "0.3"
truck-topology = "0.3"
truck-rendimesh = "0.3"
[dev-dependencies]
serde_json = "1"
```
**Wichtig:** `truck-modeling` (die Crate mit Boolean-Operatoren) wird NICHT eingebunden.
Nur `truck-geometry`, `truck-topology` und `truck-rendimesh` — die sind stabil.
### 1.2 src/lib.rs — Kern-Logik
#### Eingabe/Ausgabe-Typen
```rust
use serde::{Deserialize, Serialize};
/// Input: flaches Array [x0,y0, x1,y1, ...] in Modell-Metern (Grundriss-XY).
/// Muss ≥ 3 Punkte enthalten; Wiederholung des ersten Punkts am Ende optional.
/// Reihenfolge CCW oder CW — truck normalisiert selbst.
#[derive(Deserialize)]
pub struct ExtrudePolyInput {
pub points: Vec<f64>, // flat: [x0,y0, x1,y1, ...]
pub height: f64, // Extrusionshöhe in Metern (> 0)
}
/// Output: trianguliertes Mesh, kompatibel mit render3d::types::MeshInput.
/// positions: flat [x0,y0,z0, x1,y1,z1, ...] in Modell-Metern
/// indices: Dreiecks-Indizes (je 3 = 1 Dreieck), 0-basiert
#[derive(Serialize)]
pub struct MeshOutput {
pub positions: Vec<f32>,
pub indices: Vec<u32>,
}
```
#### Polygon-Extrusion (truck-API)
```rust
use truck_geometry::prelude::*;
use truck_topology::*;
pub fn extrude_polygon_core(pts: &[(f64, f64)], height: f64) -> Result<MeshOutput, String> {
if pts.len() < 3 { return Err("min 3 Punkte".into()); }
if height <= 0.0 { return Err("height muss > 0 sein".into()); }
// 1. Punkte in der XY-Ebene (z=0) als truck-Vertices
let verts: Vec<Vertex<Point3>> = pts.iter()
.map(|(x, y)| Vertex::new(Point3::new(*x, *y, 0.0)))
.collect();
// 2. Kanten: je zwei aufeinanderfolgende Vertices verbinden, Ring schliessen
let edges: Vec<Edge<_, Line<Point3>>> = verts.windows(2)
.chain(std::iter::once([verts.last().unwrap(), &verts[0]].as_slice()))
.map(|w| {
let p0 = *w[0].point();
let p1 = *w[1].point();
Edge::new(&w[0], &w[1], Line(p0, p1))
})
.collect();
// 3. Wire (geschlossener Kantenzug)
let wire = Wire::from_iter(edges);
// 4. Planare Face aus dem Wire
// truck-topology::Face::new braucht die äussere Boundary + optional Holes
let face = Face::new(vec![wire]);
// 5. Lineare Extrusion: tsweep entlang +Z um `height`
let solid = face.tsweep(&Vector3::new(0.0, 0.0, height));
// 6. Tessellieren (chord-tolerance in Metern — 0.005 = 5 mm)
use truck_rendimesh::MeshedShape;
let mesh = solid.triangulation(0.005).to_polygon();
// 7. positions + indices extrahieren
let positions: Vec<f32> = mesh.positions().iter()
.flat_map(|p| [p.x as f32, p.y as f32, p.z as f32])
.collect();
let indices: Vec<u32> = mesh.tri_faces().iter()
.flat_map(|tri| tri.iter().map(|idx| idx.pos as u32))
.collect();
Ok(MeshOutput { positions, indices })
}
```
> **Achtung:** Die exakte truck-API (Methoden-Namen, Trait-Imports, tsweep-Signatur)
> kann je nach veröffentlichter Version leicht abweichen. Prüfe die Docs von
> `truck-topology 0.3` und `truck-rendimesh 0.3` auf docs.rs. Die Struktur oben
> ist das Ziel-Pattern — passe Methoden-Signaturen an, wenn der Compiler es verlangt.
> Ändere NICHT die Eingabe/Ausgabe-JSON-Struktur.
#### Kreis-Extrusion (Zylinder)
```rust
pub fn extrude_circle_core(cx: f64, cy: f64, r: f64, height: f64) -> Result<MeshOutput, String> {
// Kreis in XY-Ebene tessellieren (N Segmente je nach r),
// dann wie extrude_polygon_core aufrufen.
// Alternativ: truck-geometry::Ellipse/Circle direkt nutzen, falls vorhanden.
let n = (2.0 * std::f64::consts::PI * r / 0.02).ceil().max(16.0) as usize;
let pts: Vec<(f64, f64)> = (0..n)
.map(|i| {
let a = 2.0 * std::f64::consts::PI * i as f64 / n as f64;
(cx + r * a.cos(), cy + r * a.sin())
})
.collect();
extrude_polygon_core(&pts, height)
}
```
#### WASM-Bindings (Feature "web")
```rust
#[cfg(feature = "web")]
mod web {
use super::*;
use wasm_bindgen::prelude::*;
#[wasm_bindgen(start)]
pub fn init() {
console_error_panic_hook::set_once();
}
/// Extrudiert ein Polygon-Profil.
/// `input_json`: `{ "points": [x0,y0,…], "height": 2.5 }`
/// Rückgabe: `{ "positions": […], "indices": […] }` oder throws JsError.
#[wasm_bindgen]
pub fn extrude_polygon(input_json: &str) -> Result<String, JsError> {
let input: ExtrudePolyInput = serde_json::from_str(input_json)
.map_err(|e| JsError::new(&e.to_string()))?;
let pts: Vec<(f64, f64)> = input.points.chunks(2)
.map(|c| (c[0], c[1]))
.collect();
let mesh = extrude_polygon_core(&pts, input.height)
.map_err(|e| JsError::new(&e))?;
serde_json::to_string(&mesh).map_err(|e| JsError::new(&e.to_string()))
}
/// Extrudiert einen Kreis-Querschnitt (Zylinder).
/// `input_json`: `{ "cx": 0, "cy": 0, "r": 0.15, "height": 3.0 }`
#[wasm_bindgen]
pub fn extrude_circle(input_json: &str) -> Result<String, JsError> {
#[derive(serde::Deserialize)]
struct In { cx: f64, cy: f64, r: f64, height: f64 }
let i: In = serde_json::from_str(input_json)
.map_err(|e| JsError::new(&e.to_string()))?;
let mesh = extrude_circle_core(i.cx, i.cy, i.r, i.height)
.map_err(|e| JsError::new(&e))?;
serde_json::to_string(&mesh).map_err(|e| JsError::new(&e.to_string()))
}
}
```
### 1.3 Tests (headless, ohne Feature "web")
Mindestens diese drei Tests müssen mit `cargo test` grün sein:
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quad_extrusion_vertex_count() {
// 1m × 1m Quadrat, 2m hoch → 8 Eckpunkte minimum (6 Flächen × 2 Dreiecke)
let pts = vec![(0.0,0.0),(1.0,0.0),(1.0,1.0),(0.0,1.0)];
let m = extrude_polygon_core(&pts, 2.0).unwrap();
assert!(m.positions.len() >= 8 * 3); // ≥ 8 Vertices × 3 floats
assert_eq!(m.indices.len() % 3, 0); // vollständige Dreiecke
assert!(m.indices.len() >= 12 * 3); // ≥ 12 Dreiecke (Quader)
}
#[test]
fn l_profile_extrusion() {
// L-Profil: 6 Punkte
let pts = vec![
(0.0,0.0),(0.3,0.0),(0.3,0.1),(0.1,0.1),(0.1,0.3),(0.0,0.3),
];
let m = extrude_polygon_core(&pts, 3.0).unwrap();
assert_eq!(m.indices.len() % 3, 0);
assert!(m.positions.len() > 0);
}
#[test]
fn cylinder_extrusion() {
let m = extrude_circle_core(0.0, 0.0, 0.15, 3.0).unwrap();
assert_eq!(m.indices.len() % 3, 0);
assert!(m.positions.len() > 0);
}
#[test]
fn rejects_too_few_points() {
assert!(extrude_polygon_core(&[(0.0,0.0),(1.0,0.0)], 1.0).is_err());
}
#[test]
fn rejects_zero_height() {
let pts = vec![(0.0,0.0),(1.0,0.0),(0.5,1.0)];
assert!(extrude_polygon_core(&pts, 0.0).is_err());
}
}
```
---
## 2 — Build-Script (package.json)
Füge in `package.json` unter `"scripts"` hinzu:
```json
"build:truck": "wasm-pack build src-tauri/trucksolid --release --target web --out-dir ../../src/engine/pkgTruck --out-name trucksolid --no-default-features --features web"
```
Und in `src-tauri/Cargo.toml` unter `exclude`:
```toml
exclude = ["render2d", "render3d", "geometry", "kernel2d", "dwgimport", "trucksolid"]
```
---
## 3 — TS-Wrapper `src/engine/truckSolid.ts`
Gleiche Lade-Pattern wie `src/plan/useWasmPlanRenderer.ts` (render2d) und
`src/viewport/useWasm3dRenderer.ts` (render3d):
```typescript
// Lazy-Singleton: WASM einmalig laden, dann gecacht.
let modulePromise: Promise<typeof import("../../engine/pkgTruck/trucksolid")> | null = null;
async function getModule() {
if (!modulePromise) {
modulePromise = import("../../engine/pkgTruck/trucksolid").then(async (m) => {
await m.default(); // WASM-Binary initialisieren
return m;
});
}
return modulePromise;
}
export interface ExtrudedMesh {
positions: number[];
indices: number[];
}
/** Extrudiert ein geschlossenes Polygon-Profil (Modell-Meter XY) um `height` m. */
export async function extrudePolygon(
points: number[], // flat [x0,y0, x1,y1, ...]
height: number,
): Promise<ExtrudedMesh> {
const m = await getModule();
const json = m.extrude_polygon(JSON.stringify({ points, height }));
return JSON.parse(json) as ExtrudedMesh;
}
/** Extrudiert einen Kreis-Querschnitt (Zylinder). */
export async function extrudeCircle(
cx: number, cy: number, r: number, height: number,
): Promise<ExtrudedMesh> {
const m = await getModule();
const json = m.extrude_circle(JSON.stringify({ cx, cy, r, height }));
return JSON.parse(json) as ExtrudedMesh;
}
```
---
## 4 — Integration in toWalls3d.ts (Vorbereitung)
In `src/plan/toWalls3d.ts` ist `RMeshKind` bereits definiert:
```typescript
export type RMeshKind = "terrain" | "imported";
```
Erweitere auf `"extrusion"` (damit der Renderer später eine eigene Farbe/Darstellung
wählen kann, auch wenn heute noch kein Unterschied besteht):
```typescript
export type RMeshKind = "terrain" | "imported" | "extrusion";
```
Die `emitMeshes()`-Funktion liest bereits `project.drawings2d` und ähnliche Arrays.
Für extrudierte Körper wird es später ein `project.extrudedSolids`-Array geben
(oder ähnlich — das ist Teil des UI-Folgeauftrags). Die `emitMeshes()`-Erweiterung
kommt dann.
---
## 5 — Verifikations-Checkliste
Bevor du als fertig meldest, müssen alle Punkte grün sein:
- [ ] `cd src-tauri/trucksolid && cargo test` → alle 5 Tests grün, keine Warnings
- [ ] `npm run build:truck``src/engine/pkgTruck/trucksolid.js` + `.wasm` erzeugt
- [ ] `npx tsc --noEmit` (aus Root) → 0 Fehler
- [ ] `npx vitest run` → alle bestehenden Tests weiterhin grün (keine Regression)
- [ ] `src-tauri/Cargo.toml` hat `"trucksolid"` im `exclude`-Array
- [ ] `package.json` hat `"build:truck"` im `"scripts"`-Block
---
## 6 — Was NICHT in diesem Auftrag
- Kein neues UI/Werkzeug (kommt später)
- Kein `truck-modeling` (Boolean-Operationen — instabil upstream)
- Kein STEP-Export (kommt in Folgeauftrag wenn Basisschicht steht)
- Kein Eintrag in `project.extrudedSolids` oder Store (UI-Auftrag)
- Keine Änderungen an render3d, generatePlan, App.tsx
---
## 7 — Koordinatensystem-Reminder
| DOSSIER-Modell | truck (im Code) | render3d (wgpu) |
|---|---|---|
| X = rechts | X = rechts | X = rechts |
| Y = oben (Grundriss) | Y = oben (Grundriss) | Z = oben (Y-up swapped) |
| Z = Höhe | Z = Höhe | Y = Höhe |
`mesh.rs` in render3d macht bereits `(model.x, model.y, model.z) → (x, z, y)` beim
Hochladen. Du musst also in truck DOSSIER-Koordinaten verwenden (XY-Ebene = Grundriss,
Z = Höhe) — das ist konsistent mit der bestehenden `MeshInput.positions`-Konvention.
+7 -6
View File
@@ -1,12 +1,13 @@
# Briefing — Nativer wgpu-2D-Renderer (parallele Instanz) # Briefing — Nativer wgpu-2D-Renderer (parallele Instanz)
> Für den Entwickler, der den nativen GPU-Renderer baut. Isoliert vom > Für die Instanz/den Agenten, der den nativen GPU-Renderer baut. Isoliert vom
> Web-Renderer; koordiniert über dieses Dokument. > Web-Renderer; koordiniert über dieses Dokument.
## COMMIT-REGEL (verbindlich) ## COMMIT-REGEL (verbindlich)
Dieses Repo darf **keinerlei Fremd-Tool-Hinweise** enthalten — nicht im Code, Dieses Repo darf **keinerlei Hinweise auf KI-Werkzeuge** enthalten — nicht im Code,
in Kommentaren oder in der Git-Historie. Kommentare deutsch, Identifier englisch. in Kommentaren oder in der Git-Historie. Keine `Co-Authored-By`-Trailer, keine
**Kein Commit ohne Absprache.** „Generated with"-Zeilen, keine Modell-/Agenten-Namen. Kommentare deutsch,
Identifier englisch. **Kein Commit ohne Ansage der Hauptinstanz.**
## Warum ## Warum
Der 2D-Plan wird aktuell in einem WebGL-Canvas gerendert (Web-Stack). In Chromium Der 2D-Plan wird aktuell in einem WebGL-Canvas gerendert (Web-Stack). In Chromium
@@ -71,9 +72,9 @@ per Matrix-Uniform macht. Fenster-Integration in Tauri ist der zweite, separate
- **Eigener Branch** (z. B. `feature/wgpu-renderer`), NICHT auf `master`/ - **Eigener Branch** (z. B. `feature/wgpu-renderer`), NICHT auf `master`/
`feature/parametric-walls` committen. Isolierter Worktree. `feature/parametric-walls` committen. Isolierter Worktree.
- **Nur** `src-tauri/` + neue Rust-Module + `docs/`. **NICHT** `src/App.tsx`, - **Nur** `src-tauri/` + neue Rust-Module + `docs/`. **NICHT** `src/App.tsx`,
`src/plan/*`, `types.ts` anfassen (dort laufen parallel Features). `src/plan/*`, `types.ts` anfassen (die Hauptinstanz arbeitet dort an Features).
- Der Web-WebGL-Renderer bleibt bestehen (Browser-Lauffähigkeit + Referenz). - Der Web-WebGL-Renderer bleibt bestehen (Browser-Lauffähigkeit + Referenz).
- Ergebnisse/Diffs zurückliefern; Projektleitung committet. - Ergebnisse/Diffs zurückliefern; die Hauptinstanz committet und pflegt das Memory.
## Gates ## Gates
`cargo check`/`cargo test`/`cargo build` grün. Trace-Scan sauber (COMMIT-REGEL). `cargo check`/`cargo test`/`cargo build` grün. Trace-Scan sauber (COMMIT-REGEL).
+3 -1
View File
@@ -6,7 +6,9 @@
> Port-Plan) und M1 (entkoppelter Standalone-Spike). Noch NICHT die Migration. > Port-Plan) und M1 (entkoppelter Standalone-Spike). Noch NICHT die Migration.
## Commit-/Spuren-Regel ## Commit-/Spuren-Regel
Wie im ganzen Repo: keine Fremd-Tool-Hinweise im Code, in Kommentaren oder der Historie. Kommentare deutsch, Identifier englisch. Kein Commit ohne Absprache. 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 ## Warum
Der 3D-View laeuft heute als three.js/WebGL im Tauri-Webview (WebKitGTK). Wie beim Der 3D-View laeuft heute als three.js/WebGL im Tauri-Webview (WebKitGTK). Wie beim
@@ -1,42 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="576" height="420" viewBox="0 0 576.0 420.0">
<rect x="0" y="0" width="576.0" height="420.0" fill="#ffffff"/>
<polygon points="48.00,372.00 528.00,372.00 528.00,348.00 48.00,348.00" fill="#c9d2d6" stroke="#2b2b2b" stroke-width="1.2"/>
<line x1="108.00" y1="348.00" x2="108.00" y2="48.00" stroke="#111111" stroke-width="1.6"/>
<line x1="468.00" y1="348.00" x2="468.00" y2="48.00" stroke="#111111" stroke-width="1.6"/>
<line x1="108.00" y1="348.00" x2="468.00" y2="348.00" stroke="#111111" stroke-width="1.6"/>
<line x1="108.00" y1="48.00" x2="468.00" y2="48.00" stroke="#111111" stroke-width="1.6"/>
<line x1="228.00" y1="252.00" x2="228.00" y2="132.00" stroke="#111111" stroke-width="1.6"/>
<line x1="348.00" y1="252.00" x2="348.00" y2="132.00" stroke="#111111" stroke-width="1.6"/>
<line x1="228.00" y1="252.00" x2="348.00" y2="252.00" stroke="#111111" stroke-width="1.6"/>
<line x1="228.00" y1="132.00" x2="348.00" y2="132.00" stroke="#111111" stroke-width="1.6"/>
<line x1="480.00" y1="348.00" x2="480.00" y2="48.00" stroke="#111111" stroke-width="1.6"/>
<line x1="468.00" y1="348.00" x2="480.00" y2="348.00" stroke="#111111" stroke-width="1.6"/>
<line x1="468.00" y1="48.00" x2="480.00" y2="48.00" stroke="#111111" stroke-width="1.6"/>
<line x1="252.00" y1="252.00" x2="252.00" y2="132.00" stroke="#111111" stroke-width="1.6"/>
<line x1="324.00" y1="252.00" x2="324.00" y2="132.00" stroke="#111111" stroke-width="1.6"/>
<line x1="468.00" y1="348.00" x2="468.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="108.00" y1="348.00" x2="108.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="468.00" y1="348.00" x2="108.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="468.00" y1="48.00" x2="108.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="480.00" y1="348.00" x2="480.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="456.00" y1="348.00" x2="456.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="456.00" y1="348.00" x2="456.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="480.00" y1="348.00" x2="456.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="480.00" y1="48.00" x2="456.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="456.00" y1="348.00" x2="468.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="456.00" y1="48.00" x2="468.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="252.00" y1="348.00" x2="252.00" y2="252.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="252.00" y1="132.00" x2="252.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="324.00" y1="348.00" x2="324.00" y2="252.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="324.00" y1="132.00" x2="324.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="324.00" y1="348.00" x2="324.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="252.00" y1="348.00" x2="252.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="252.00" y1="348.00" x2="324.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="252.00" y1="48.00" x2="324.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="324.00" y1="348.00" x2="252.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="324.00" y1="48.00" x2="252.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="48.00" y1="372.00" x2="48.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="528.00" y1="372.00" x2="528.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="48.00" y1="372.00" x2="528.00" y2="372.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
<line x1="48.00" y1="348.00" x2="528.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

+1 -227
View File
@@ -27,13 +27,11 @@
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@types/three": "^0.169.0", "@types/three": "^0.169.0",
"@vitejs/plugin-react": "^4.3.2", "@vitejs/plugin-react": "^4.3.2",
"electron": "^43.0.0",
"playwright": "^1.61.1", "playwright": "^1.61.1",
"puppeteer": "^25.2.1", "puppeteer": "^25.2.1",
"typescript": "^5.6.2", "typescript": "^5.6.2",
"vite": "^5.4.8", "vite": "^5.4.8",
"vitest": "^4.1.9", "vitest": "^4.1.9"
"wasm-pack": "^0.15.0"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
@@ -327,50 +325,6 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@electron-internal/extract-zip": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.4.tgz",
"integrity": "sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/@electron/get": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz",
"integrity": "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"env-paths": "^3.0.0",
"graceful-fs": "^4.2.11",
"progress": "^2.0.3",
"semver": "^7.6.3",
"sumchecker": "^3.0.1"
},
"engines": {
"node": ">=22.12.0"
},
"optionalDependencies": {
"undici": "^7.24.4"
}
},
"node_modules/@electron/get/node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@emnapi/core": { "node_modules/@emnapi/core": {
"version": "1.11.1", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
@@ -796,19 +750,6 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"minipass": "^7.0.4"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13", "version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -1930,16 +1871,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": {
"version": "24.13.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
"integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/@types/pako": { "node_modules/@types/pako": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
@@ -2275,16 +2206,6 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/chromium-bidi": { "node_modules/chromium-bidi": {
"version": "16.0.1", "version": "16.0.1",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz",
@@ -2434,25 +2355,6 @@
"loglevel": "^1.7.1" "loglevel": "^1.7.1"
} }
}, },
"node_modules/electron": {
"version": "43.0.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-43.0.0.tgz",
"integrity": "sha512-PV60GsWU6qufhuOhw3n+Yix3WPDcqDtBqE8orbEQGQGHEkgp9o/JCPgb7L4vIL0r1HnfPdqSRtboOTqbDkcFDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-internal/extract-zip": "^1.0.1",
"@electron/get": "^5.0.0",
"@types/node": "^24.9.0"
},
"bin": {
"electron": "cli.js",
"install-electron": "install.js"
},
"engines": {
"node": ">= 22.12.0"
}
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.380", "version": "1.5.380",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.380.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.380.tgz",
@@ -2467,19 +2369,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/env-paths": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
"integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/es-module-lexer": { "node_modules/es-module-lexer": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.2.0.tgz", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.2.0.tgz",
@@ -2645,13 +2534,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/html2canvas": { "node_modules/html2canvas": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
@@ -3104,29 +2986,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/minipass": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/minizlib": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"minipass": "^7.1.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/mitt": { "node_modules/mitt": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
@@ -3342,16 +3201,6 @@
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/puppeteer": { "node_modules/puppeteer": {
"version": "25.2.1", "version": "25.2.1",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.2.1.tgz", "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.2.1.tgz",
@@ -3694,19 +3543,6 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1" "url": "https://github.com/chalk/strip-ansi?sponsor=1"
} }
}, },
"node_modules/sumchecker": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.1.0"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/svg-pathdata": { "node_modules/svg-pathdata": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
@@ -3741,33 +3577,6 @@
"url": "https://github.com/fontello/svg2ttf?sponsor=1" "url": "https://github.com/fontello/svg2ttf?sponsor=1"
} }
}, },
"node_modules/tar": {
"version": "7.5.19",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz",
"integrity": "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minizlib": "^3.1.0",
"yallist": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/tar/node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/text-segmentation": { "node_modules/text-segmentation": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
@@ -3857,24 +3666,6 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/undici": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
"integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -4177,23 +3968,6 @@
} }
} }
}, },
"node_modules/wasm-pack": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/wasm-pack/-/wasm-pack-0.15.0.tgz",
"integrity": "sha512-DdqtGWc3+iFx+7lL7QU5LBWs7qMnwQSWxF0htSfE15sNa3roVwHjAkTm2JXgueU2GGfSwNqbq2EzyC2b/biKDA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT OR Apache-2.0",
"dependencies": {
"tar": "^7.5.3"
},
"bin": {
"wasm-pack": "run.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/webdriver-bidi-protocol": { "node_modules/webdriver-bidi-protocol": {
"version": "0.4.2", "version": "0.4.2",
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz", "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz",
+2 -10
View File
@@ -13,13 +13,7 @@
"tauri:dev": "tauri dev", "tauri:dev": "tauri dev",
"tauri:build": "tauri build", "tauri:build": "tauri build",
"dump:native": "node scripts/dump-native-scene.mjs", "dump:native": "node scripts/dump-native-scene.mjs",
"shell": "scripts/chromium-shell.sh", "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:geometry": "wasm-pack build src-tauri/geometry --release --target web --out-dir ../../src/engine/pkgGeometry --out-name geometry --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"
}, },
"dependencies": { "dependencies": {
"@mlightcad/libredwg-web": "^0.7.7", "@mlightcad/libredwg-web": "^0.7.7",
@@ -41,12 +35,10 @@
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@types/three": "^0.169.0", "@types/three": "^0.169.0",
"@vitejs/plugin-react": "^4.3.2", "@vitejs/plugin-react": "^4.3.2",
"electron": "^43.0.0",
"playwright": "^1.61.1", "playwright": "^1.61.1",
"puppeteer": "^25.2.1", "puppeteer": "^25.2.1",
"typescript": "^5.6.2", "typescript": "^5.6.2",
"vite": "^5.4.8", "vite": "^5.4.8",
"vitest": "^4.1.9", "vitest": "^4.1.9"
"wasm-pack": "^0.15.0"
} }
} }
-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.
-93
View File
@@ -1,93 +0,0 @@
Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk)
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 2019 The Work Sans Project Authors (https://github.com/weiweihuanghuang/Work-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:
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.
+1 -9
View File
@@ -77,19 +77,11 @@ trap cleanup EXIT
mkdir -p "${PROFILE_DIR}" mkdir -p "${PROFILE_DIR}"
# WebGPU: Auf Linux liegt WebGPU in Chromium noch hinter Flags. Für den echo "chromium-shell: öffne ${URL} in ${BROWSER_BIN} (App-Modus) …"
# render2d-WASM-Viewport (`?engine=wasm`, useWasmPlanRenderer) muss `navigator.gpu`
# vorhanden sein — daher `--enable-unsafe-webgpu` (aktiviert die API auch ohne
# volle Konformität) plus `--enable-features=Vulkan` (WebGPU nutzt unter Linux das
# Vulkan-Backend). Ohne `?engine=wasm` sind die Flags folgenlos (der Standard-
# WebGL2-Pfad läuft unverändert weiter).
echo "chromium-shell: öffne ${URL} in ${BROWSER_BIN} (App-Modus, WebGPU aktiv) …"
"${BROWSER_BIN}" \ "${BROWSER_BIN}" \
--app="${URL}" \ --app="${URL}" \
--user-data-dir="${PROFILE_DIR}" \ --user-data-dir="${PROFILE_DIR}" \
--window-size=1600,1000 \ --window-size=1600,1000 \
--enable-unsafe-webgpu \
--enable-features=Vulkan \
>/tmp/cad-chromium-shell-browser.log 2>&1 >/tmp/cad-chromium-shell-browser.log 2>&1
# Läuft weiter, bis der Browserprozess (das App-Fenster) beendet wird; dann # Läuft weiter, bis der Browserprozess (das App-Fenster) beendet wird; dann
-77
View File
@@ -1,77 +0,0 @@
// electron-main.js — Electron-Prototyp-Shell für die CAD-App.
//
// Ersetzt WebKitGTK (Tauri-Linux-Webview) durch Chromium, damit WebGPU
// (render2d-WASM-Engine, ?engine=wasm) zuverlässig läuft. Kein Rust-Backend
// nötig: compute_joins hat einen TS-Fallback (src/compute/index.ts).
const { app, BrowserWindow, Menu, ipcMain } = require("electron");
const path = require("node:path");
// Randloses App-Fenster wie chromium-shell.sh (--app=…): keine native
// Menüleiste (File/Edit/View/Window), kein Fensterrahmen. Minimieren/
// Maximieren/Schliessen laufen darum über eine eigene Fenstersteuerung im
// Renderer (WindowControls.tsx) + IPC hier unten — ohne frame gibt es sonst
// gar keine Möglichkeit, das Fenster zu steuern.
Menu.setApplicationMenu(null);
// Gleiche Flags wie scripts/chromium-shell.sh — WebGPU liegt unter Linux
// hinter --enable-unsafe-webgpu, und WebGPU braucht das Vulkan-Backend.
app.commandLine.appendSwitch("enable-unsafe-webgpu");
app.commandLine.appendSwitch("enable-features", "Vulkan");
const DEV_URL = "http://localhost:5187";
const isDev = !app.isPackaged;
function createWindow() {
const win = new BrowserWindow({
width: 1600,
height: 1000,
frame: false,
autoHideMenuBar: true,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.join(__dirname, "electron-preload.cjs"),
},
});
const notifyMaximized = () => {
win.webContents.send("window:maximized-changed", win.isMaximized());
};
win.on("maximize", notifyMaximized);
win.on("unmaximize", notifyMaximized);
if (isDev) {
win.loadURL(DEV_URL);
} else {
win.loadFile(require("node:path").join(__dirname, "..", "dist", "index.html"));
}
}
// Fenstersteuerung vom Renderer (WindowControls.tsx über electron-preload.cjs).
// Wirkt auf das Fenster, aus dem das Event kam (mehrfachfenster-sicher).
ipcMain.on("window:minimize", (event) => {
BrowserWindow.fromWebContents(event.sender)?.minimize();
});
ipcMain.on("window:toggle-maximize", (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (!win) return;
if (win.isMaximized()) win.unmaximize();
else win.maximize();
});
ipcMain.on("window:close", (event) => {
BrowserWindow.fromWebContents(event.sender)?.close();
});
ipcMain.handle("window:is-maximized", (event) => {
return BrowserWindow.fromWebContents(event.sender)?.isMaximized() ?? false;
});
app.whenReady().then(createWindow);
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
-19
View File
@@ -1,19 +0,0 @@
// electron-preload.cjs — Bruecke zwischen dem isolierten Renderer (contextIsolation)
// und dem Electron-Hauptprozess. Braucht es NUR fuer die custom Fenstersteuerung
// (Minimieren/Maximieren/Schliessen) im randlosen Fenster (frame:false, siehe
// electron-main.cjs) — sonst gaebe es keine Moeglichkeit, das Fenster ueberhaupt
// zu steuern.
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("dossierWindow", {
minimize: () => ipcRenderer.send("window:minimize"),
toggleMaximize: () => ipcRenderer.send("window:toggle-maximize"),
close: () => ipcRenderer.send("window:close"),
isMaximized: () => ipcRenderer.invoke("window:is-maximized"),
onMaximizedChange: (callback) => {
const listener = (_event, isMaximized) => callback(isMaximized);
ipcRenderer.on("window:maximized-changed", listener);
return () => ipcRenderer.removeListener("window:maximized-changed", listener);
},
});
-49
View File
@@ -1,49 +0,0 @@
#!/usr/bin/env bash
# electron-shell.sh — startet die CAD-Oberfläche im Electron-Prototyp-Fenster.
#
# Wie chromium-shell.sh: stellt sicher, dass der Vite-Dev-Server läuft, startet
# ihn sonst selbst und beendet ihn wieder, sobald das Electron-Fenster schliesst.
set -u
PORT=5187
URL="http://localhost:${PORT}"
server_is_up() {
curl --silent --fail --max-time 1 --output /dev/null "${URL}"
}
DEV_SERVER_PID=""
if server_is_up; then
echo "electron-shell: Dev-Server läuft bereits auf ${URL}."
else
echo "electron-shell: starte Vite-Dev-Server (Port ${PORT}) …"
npm run dev >/tmp/cad-electron-shell-dev.log 2>&1 &
DEV_SERVER_PID=$!
ATTEMPTS=0
MAX_ATTEMPTS=60
until server_is_up; do
ATTEMPTS=$((ATTEMPTS + 1))
if [ "${ATTEMPTS}" -ge "${MAX_ATTEMPTS}" ]; then
echo "electron-shell: Dev-Server antwortet nach ${MAX_ATTEMPTS}s nicht, Abbruch." >&2
kill "${DEV_SERVER_PID}" 2>/dev/null
exit 1
fi
sleep 1
done
echo "electron-shell: Dev-Server bereit."
fi
cleanup() {
if [ -n "${DEV_SERVER_PID}" ]; then
echo "electron-shell: beende selbst gestarteten Dev-Server (PID ${DEV_SERVER_PID}) …"
kill "${DEV_SERVER_PID}" 2>/dev/null
wait "${DEV_SERVER_PID}" 2>/dev/null
fi
}
trap cleanup EXIT
echo "electron-shell: öffne Electron-Fenster …"
npx electron "$(dirname "$0")/electron-main.cjs"
-60
View File
@@ -1,60 +0,0 @@
// Vergleicht den SVG-Referenzpfad gegen den nativen render2d-WASM/WebGPU-Pfad
// (?engine=wasm): lädt die App einmal ohne Engine-Param (SVG) und einmal mit
// ?engine=wasm, gleicher Viewport/Zoom, schießt je einen Screenshot. Prüft
// zusätzlich, ob WebGPU verfügbar ist und der native Renderer tatsächlich
// initialisiert hat (statt still auf SVG zurückzufallen).
import puppeteer from "puppeteer";
const BASE = process.env.PROBE_URL || "http://localhost:5187/";
const browser = await puppeteer.launch({
headless: "new",
args: [
"--no-sandbox",
"--enable-unsafe-webgpu",
"--enable-features=Vulkan",
"--use-angle=vulkan",
"--ignore-gpu-blocklist",
],
});
async function shoot(url, path, tag) {
const page = await browser.newPage();
await page.setViewport({ width: 1400, height: 900, deviceScaleFactor: 1 });
const logs = [];
page.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`));
page.on("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
try {
await page.goto(url, { waitUntil: "networkidle0", timeout: 30000 });
} catch (e) {
logs.push(`[GOTO] ${e.message}`);
}
// Warten, bis Plan + evtl. WASM/WebGPU-Renderer initialisiert und gezeichnet hat.
await new Promise((r) => setTimeout(r, 2500));
const info = await page.evaluate(() => {
const svg = document.querySelector(".plan-svg");
const canvases = document.querySelectorAll("canvas");
return {
hasGpu: typeof navigator !== "undefined" && "gpu" in navigator,
hasSvg: !!svg,
svgChildren: svg ? svg.querySelectorAll("path,polygon,polyline,line,rect,text").length : 0,
canvasCount: canvases.length,
};
});
await page.screenshot({ path });
console.log(`\n=== ${tag} (${url}) ===`);
console.log(JSON.stringify(info, null, 2));
const warns = logs.filter((l) => /warn|error|PAGEERROR/i.test(l));
console.log("Warnungen/Fehler:", warns.length ? "\n" + warns.join("\n") : "(keine)");
await page.close();
return info;
}
await shoot(BASE + "?gl=0", "scripts/probe-parity-svg.png", "SVG-Referenzpfad");
await shoot(BASE + "?engine=wasm", "scripts/probe-parity-wasm.png", "WASM/WebGPU-Engine");
await browser.close();
console.log("\nScreenshots: scripts/probe-parity-svg.png vs scripts/probe-parity-wasm.png");
-129
View File
@@ -1,129 +0,0 @@
// Probe: belegt den nativen render3d-Renderer als WASM/WebGPU-Viewport in der
// 3D-Ansicht (`?engine=wasm`, siehe src/viewport/Wasm3DViewport.tsx).
//
// Ablauf: App mit ?engine=wasm laden, in die Perspektive wechseln (Ansichts-
// Icon-Grid der Oberleiste), auf die Engine-Initialisierung warten, Screenshot
// scripts/probe-engine3d.png schiessen und dann PRÜFEN, dass das Engine-Canvas
// (canvas[data-engine="render3d"]) vorhanden ist UND Geometrie zeigt.
//
// PIXEL-SAMPLING über den Screenshot (nicht drawImage): ein WebGPU-Canvas
// liefert nach dem Present über drawImage/getImageData nur transparente Pixel
// (der Frame ist bereits verbraucht). Der CDP-Screenshot enthält dagegen die
// komposittierte Ausgabe — er wird als Data-URL zurück in die Seite geladen und
// im Rechteck des Engine-Canvas gegen die Löschfarbe (#f5f5f5) ausgezählt.
//
// WebGPU liegt in Chromium/Linux hinter Flags (siehe scripts/chromium-shell.sh):
// --enable-unsafe-webgpu --enable-features=Vulkan. HEADLESS initialisiert die
// API zwar (Adapter/Device ok, Init-Logs erscheinen), PRÄSENTIERT aber keine
// Frames — das gilt genauso für den etablierten render2d-Viewport (gegen-
// geprüft). Verlässlich verifiziert daher nur headful:
// HEADFUL=1 [CHROMIUM_BIN=/usr/bin/chromium] node scripts/probe-engine3d.mjs
import puppeteer from "puppeteer";
const BASE = process.env.PROBE_URL || "http://localhost:5187/";
const URL = BASE.replace(/\/?$/, "/") + "?engine=wasm";
const HEADFUL = !!process.env.HEADFUL;
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const browser = await puppeteer.launch({
headless: HEADFUL ? false : "new",
// Headful: das System-Chromium nutzen (Puppeteers gebündeltes Chrome kommt
// mit der Wayland/niri-Session hier nicht hoch).
executablePath: HEADFUL ? process.env.CHROMIUM_BIN || "/usr/bin/chromium" : undefined,
args: ["--no-sandbox", "--enable-unsafe-webgpu", "--enable-features=Vulkan"],
});
const page = await browser.newPage();
await page.setViewport({ width: 1400, height: 900, deviceScaleFactor: 1 });
const logs = [];
page.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`));
page.on("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
await page.goto(URL, { waitUntil: "networkidle0", timeout: 25000 });
const hasGpu = await page.evaluate(() => "gpu" in navigator);
// In die Perspektive wechseln: die 3D-Blicke im Ansichts-Icon-Grid tragen ihr
// Label als aria-label (TopBar.tsx).
const clicked = await page.evaluate(() => {
const btn = document.querySelector('button.view-icon[aria-label="Perspektive"]');
if (!btn) return false;
btn.click();
return true;
});
// Engine-Init (WASM-Load + Adapter/Device) + erster Frame brauchen einen Moment.
await wait(3500);
// Position/Größe des Engine-Canvas im Viewport (CSS-Pixel, dsf=1).
const rect = await page.evaluate(() => {
const c = document.querySelector('canvas[data-engine="render3d"]');
if (!c) return null;
const r = c.getBoundingClientRect();
return { x: r.x, y: r.y, w: r.width, h: r.height };
});
await page.screenshot({ path: "scripts/probe-engine3d.png" });
let verdict = { present: !!rect };
if (rect && rect.w > 4 && rect.h > 4) {
// Screenshot zurück in die Seite laden und im Canvas-Rechteck auszählen.
const b64 = await page.screenshot({ encoding: "base64" });
verdict = await page.evaluate(
async (b64Png, r) => {
const img = new Image();
img.src = "data:image/png;base64," + b64Png;
await img.decode();
const s = document.createElement("canvas");
s.width = img.width;
s.height = img.height;
const ctx = s.getContext("2d");
ctx.drawImage(img, 0, 0);
const d = ctx.getImageData(
Math.round(r.x),
Math.round(r.y),
Math.round(r.w),
Math.round(r.h),
).data;
let nonBackground = 0;
const total = d.length / 4;
for (let i = 0; i < d.length; i += 4) {
if (
Math.abs(d[i] - 245) > 12 ||
Math.abs(d[i + 1] - 245) > 12 ||
Math.abs(d[i + 2] - 245) > 12
) {
nonBackground++;
}
}
return { present: true, rect: r, total, nonBackground };
},
b64,
rect,
);
}
console.log(`URL: ${URL}`);
console.log("navigator.gpu:", hasGpu, "| Perspektive-Button:", clicked);
console.log(JSON.stringify(verdict, null, 2));
const relevant = logs.filter((l) => /render3d|render2d|webgpu|wasm|error|warn/i.test(l));
console.log("Logs:", relevant.length ? "\n" + relevant.join("\n") : "(keine)");
await browser.close();
// Erfolg: Canvas vorhanden und deutlich mehr als nur die Löschfarbe sichtbar
// (>3 % der Pixel abseits von #f5f5f5 — das Gebäude deckt real ~40 % ab; das
// Ansichts-Badge allein läge unter 1 %).
const ok = verdict.present && (verdict.nonBackground ?? 0) > (verdict.total ?? 1) * 0.03;
if (ok) {
console.log(
`\nOK: Engine-Canvas zeigt Geometrie (${verdict.nonBackground}/${verdict.total} Pixel abseits der Löschfarbe). Screenshot: scripts/probe-engine3d.png`,
);
} else {
console.error(
`\nFEHLSCHLAG: Engine-Canvas ${verdict.present ? "ohne sichtbare Geometrie" : "fehlt"} (navigator.gpu=${hasGpu}). ` +
"Headless präsentiert keine WebGPU-Frames — mit HEADFUL=1 erneut ausführen.",
);
process.exit(1);
}
-109
View File
@@ -1,109 +0,0 @@
// Probe (Materialien-Tab — PBR-Kugel-Vorschau + Filter): öffnet den Ressourcen-
// Manager, wechselt in den neuen „Materialien"-Tab, prüft dass Kacheln
// erscheinen, mindestens eine Kugel-Vorschau (Bild) gerendert wurde, und dass
// Such-/Kategorie-Filter die Trefferzahl verändern.
//
// Aufruf: PROBE_URL=http://localhost:5187/ node scripts/probe-material-tiles.mjs
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5187/";
const OUT = "scripts";
const browser = await puppeteer.launch({
headless: "new",
args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"],
});
const page = await browser.newPage();
await page.setViewport({ width: 1360, height: 900, deviceScaleFactor: 2 });
const logs = [];
page.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`));
page.on("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const shot = (n) => page.screenshot({ path: `${OUT}/${n}` });
async function clickByText(selector, text) {
const handle = await page.evaluateHandle(
(sel, t) =>
[...document.querySelectorAll(sel)].find((el) => el.textContent.includes(t)),
selector,
text,
);
const el = handle.asElement();
if (!el) throw new Error(`nicht gefunden: ${selector} ~ "${text}"`);
await el.click();
return el;
}
await page.goto(URL, { waitUntil: "domcontentloaded", timeout: 20000 });
await page.waitForSelector(".topbar", { timeout: 20000 });
await sleep(600);
// 1) Ressourcen öffnen → in den Materialien-Tab wechseln.
await page.waitForSelector('button[aria-label="Ressourcen"]', { timeout: 8000 });
await page.click('button[aria-label="Ressourcen"]');
await page.waitForSelector(".res-drawer", { timeout: 8000 });
await clickByText(".res-tab-btn", "Materialien");
await page.waitForSelector(".mat-lib-grid .mat-tile", { timeout: 8000 });
const tileCount = await page.$$eval(".mat-lib-grid .mat-tile", (els) => els.length);
console.log("Material-Kacheln (ungefiltert):", tileCount);
// 2) Warten, bis mindestens eine Kugel-Vorschau gerendert ist (Bild im Tile).
await page.waitForFunction(
() => document.querySelectorAll(".mat-sphere img").length > 0,
{ timeout: 10000 },
);
// Sichtbare Kacheln lazy nachladen lassen (Warteschlange rendert seriell).
await sleep(2500);
const renderedCount = await page.$$eval(".mat-sphere img", (els) => els.length);
console.log("Gerenderte Kugel-Vorschauen (sichtbarer Bereich):", renderedCount);
// Erstes Vorschaubild darf kein leeres/transparentes 1x1-Pixelbild sein.
const firstSrc = await page.$eval(".mat-sphere img", (img) => img.src);
console.log("Erste Vorschau-DataURL-Länge:", firstSrc.length);
await shot("probe-material-tiles-1-grid.png");
// 3) Kategorie-Filter: erste konkrete Kategorie-Chip anklicken (nicht "Alle").
const chipLabels = await page.$$eval(".mat-chip", (els) => els.map((e) => e.textContent));
console.log("Kategorie-Chips:", chipLabels);
const firstCategoryChip = chipLabels[1]; // Index 0 = "Alle".
await clickByText(".mat-chip", firstCategoryChip);
await sleep(300);
const filteredByCategory = await page.$$eval(".mat-lib-grid .mat-tile", (els) => els.length);
console.log(`Kacheln nach Kategorie-Filter ("${firstCategoryChip}"):`, filteredByCategory);
await shot("probe-material-tiles-2-category.png");
// Filter zurücksetzen ("Alle").
await clickByText(".mat-chip", "Alle");
await sleep(200);
// 4) Textsuche: nach "Holz" suchen (mehrere Bibliotheks-Einträge betroffen).
await page.type(".mat-search", "Holz");
await sleep(400);
const filteredByText = await page.$$eval(".mat-lib-grid .mat-tile", (els) => els.length);
console.log('Kacheln nach Textsuche ("Holz"):', filteredByText);
await shot("probe-material-tiles-3-search.png");
// 5) Kachel auswählen → aktiv-Zustand + Statuszeile prüfen.
await page.click(".mat-lib-grid .mat-tile");
await sleep(200);
const hasActive = await page.$(".mat-lib-grid .mat-tile.active");
const selectedLine = await page.$eval(".mat-lib-selected", (el) => el.textContent).catch(() => null);
console.log("Aktive Kachel vorhanden:", !!hasActive, "| Auswahl-Zeile:", selectedLine);
await shot("probe-material-tiles-4-selected.png");
console.log("--- console ---");
for (const l of logs.slice(-30)) console.log(l);
if (tileCount === 0) throw new Error("Keine Material-Kacheln gefunden");
if (renderedCount === 0) throw new Error("Keine Kugel-Vorschau gerendert");
if (firstSrc.length < 200) throw new Error("Vorschau-DataURL wirkt leer");
if (filteredByCategory >= tileCount) throw new Error("Kategorie-Filter hat nichts gefiltert");
if (filteredByText >= tileCount) throw new Error("Textsuche hat nichts gefiltert");
if (!hasActive) throw new Error("Kachel-Auswahl hat keinen aktiven Zustand gesetzt");
await browser.close();
console.log("done");
+10 -51
View File
@@ -12,7 +12,6 @@ import { join } from "node:path";
const URL = process.env.PROBE_URL || "http://localhost:5187/"; const URL = process.env.PROBE_URL || "http://localhost:5187/";
const OUT = process.env.SCRATCH || "/tmp/pdf-probe"; const OUT = process.env.SCRATCH || "/tmp/pdf-probe";
const PEN_STEPS_LOG = "0.13, 0.18, 0.25, 0.35, 0.5, 0.7, 1.0";
if (!existsSync(OUT)) mkdirSync(OUT, { recursive: true }); if (!existsSync(OUT)) mkdirSync(OUT, { recursive: true });
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
@@ -70,12 +69,10 @@ const info = await page.evaluate(() => {
}); });
console.log("Store:", JSON.stringify(info)); console.log("Store:", JSON.stringify(info));
// PDF-Button in der Oberleiste klicken (Icon-Button, aria-label „PDF" — die // PDF-Button in der Oberleiste klicken (Text „PDF"). Öffnet den Export-Dialog.
// TopBar zeigt seit dem Icon-Redesign nur noch ein Material-Symbol, kein
// Klartext mehr). Öffnet den Export-Dialog.
const clickedBtn = await page.evaluate(() => { const clickedBtn = await page.evaluate(() => {
const btns = Array.from(document.querySelectorAll(".tb-iconbtn")); const btns = Array.from(document.querySelectorAll(".tb-btn"));
const b = btns.find((x) => x.getAttribute("aria-label")?.trim() === "PDF"); const b = btns.find((x) => x.textContent.trim() === "PDF");
if (b) { if (b) {
b.click(); b.click();
return true; return true;
@@ -121,15 +118,13 @@ const pdfBytes = Buffer.from(captured.b64, "base64");
const pdfPath = join(OUT, captured.name.endsWith(".pdf") ? captured.name : captured.name + ".pdf"); const pdfPath = join(OUT, captured.name.endsWith(".pdf") ? captured.name : captured.name + ".pdf");
writeFileSync(pdfPath, pdfBytes); writeFileSync(pdfPath, pdfBytes);
// Visuelle Prüfung: das DRUCK-SVG (exakt die Vektor-Quelle des PDF — jetzt aus // Visuelle Prüfung: das DRUCK-SVG (exakt die Vektor-Quelle des PDF) im Browser
// der RENDER-SZENE, derselben Quelle wie der Viewport) im Browser erzeugen und // erzeugen und nach PNG rastern. Zeigt scharfe Linien, Schraffuren, weisses Blatt.
// nach PNG rastern. Zeigt scharfe Linien, Schraffuren, weisses Blatt.
try { try {
const result = await page.evaluate(async () => { const svgPng = await page.evaluate(async () => {
const gp = await import("/src/plan/generatePlan.ts"); const gp = await import("/src/plan/generatePlan.ts");
const sp = await import("/src/model/sampleProject.ts"); const sp = await import("/src/model/sampleProject.ts");
const trs = await import("/src/plan/toRenderScene.ts"); const ptp = await import("/src/export/planToPrintSvg.ts");
const stp = await import("/src/export/sceneToPrintSvg.ts");
const proj = sp.sampleProject; const proj = sp.sampleProject;
const codes = new Set(); const codes = new Set();
const walk = (cs) => const walk = (cs) =>
@@ -141,29 +136,12 @@ try {
}); });
walk(proj.layers); walk(proj.layers);
const plan = gp.generatePlan(proj, "eg", codes, undefined, "mittel", false, false); const plan = gp.generatePlan(proj, "eg", codes, undefined, "mittel", false, false);
const scene = trs.planToRenderScene(plan); const print = ptp.planToPrintSvg(plan, {
const print = stp.sceneToPrintSvg(scene, {
scaleDenominator: 100, scaleDenominator: 100,
pageWidthMm: 297, pageWidthMm: 297,
pageHeightMm: 210, pageHeightMm: 210,
marginMm: 10, marginMm: 10,
}); });
// Schraffur-Nachweis: widthScreen-Polylinien der Szene sind die Schraffur-
// Musterlinien (siehe toRenderScene.ts); im Print-SVG muessen sie als
// <polyline> mit ISO-Stiftstufen-Strichbreite auftauchen.
const hatchRunCount = scene.polylines.filter((p) => p.widthScreen).length;
const PEN_STEPS = [0.13, 0.18, 0.25, 0.35, 0.5, 0.7, 1.0];
const strokeWidths = Array.from(
print.svg.querySelectorAll("polyline, polygon, line, path"),
)
.map((el) => el.getAttribute("stroke-width"))
.filter((w) => w !== null)
.map(Number);
const offPenStep = strokeWidths.filter(
(w) => !PEN_STEPS.some((s) => Math.abs(s - w) < 1e-6),
);
const xml = new XMLSerializer().serializeToString(print.svg); const xml = new XMLSerializer().serializeToString(print.svg);
const blob = new Blob([xml], { type: "image/svg+xml" }); const blob = new Blob([xml], { type: "image/svg+xml" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
@@ -181,30 +159,11 @@ try {
ctx.fillStyle = "#ffffff"; ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
return { return canvas.toDataURL("image/png");
png: canvas.toDataURL("image/png"),
hatchRunCount,
strokedElementCount: strokeWidths.length,
offPenStepCount: offPenStep.length,
offPenStepSample: offPenStep.slice(0, 5),
};
}); });
const b64 = result.png.split(",")[1]; const b64 = svgPng.split(",")[1];
writeFileSync(join(OUT, "print-svg.png"), Buffer.from(b64, "base64")); writeFileSync(join(OUT, "print-svg.png"), Buffer.from(b64, "base64"));
console.log("Druck-SVG → PNG:", join(OUT, "print-svg.png")); console.log("Druck-SVG → PNG:", join(OUT, "print-svg.png"));
console.log("\n=== SCHRAFFUR / STIFTSTUFEN (Print-SVG) ===");
console.log(`Schraffur-Musterlinien (widthScreen-Polylinien) in der Szene: ${result.hatchRunCount}`);
console.log(`Gestrichelte/-gezeichnete Elemente mit stroke-width im Print-SVG: ${result.strokedElementCount}`);
console.log(
`Strichbreiten ausserhalb der ISO-Stiftstufen (${PEN_STEPS_LOG}): ${result.offPenStepCount}` +
(result.offPenStepCount ? ` (Beispiele: ${result.offPenStepSample.join(", ")})` : ""),
);
console.log(
`→ Schraffur vorhanden UND alle Strichbreiten auf Stiftstufen? ${
result.hatchRunCount > 0 && result.offPenStepCount === 0 ? "JA" : "NEIN"
}`,
);
} catch (e) { } catch (e) {
console.log("(SVG-Render übersprungen:", e.message + ")"); console.log("(SVG-Render übersprungen:", e.message + ")");
} }
+367 -407
View File
File diff suppressed because it is too large Load Diff
+8 -12
View File
@@ -1,14 +1,10 @@
[workspace] [workspace]
members = ["."] members = [".", "geometry"]
# render2d/render3d/geometry sind eigenstaendige Crates, damit sie headless ohne # render2d/render3d sind eigenstaendige Crates (jeweils eigener [workspace]), damit
# Tauri-Toolchain UND per wasm-pack (Feature "web") zu WASM baubar bleiben. Sie # sie headless ohne Tauri-Toolchain baubar/testbar bleiben. Sie liegen zwar im
# liegen zwar im src-tauri-Baum, gehoeren aber NICHT zum cad-tauri-Workspace — # src-tauri-Baum, gehoeren aber NICHT zum cad-tauri-Workspace — hier ausschliessen,
# hier ausschliessen, damit `cad-tauri` sie dennoch per Pfad als Abhaengigkeit # damit `cad-tauri` sie dennoch per Pfad als (optionale) Abhaengigkeit nutzen kann.
# nutzen kann. geometry: neu ausgeschlossen (war Member), seit es zu WASM gebaut exclude = ["render2d", "render3d"]
# wird und das TS-Frontend die Joins direkt per WASM aufruft (statt TS-Duplikat).
# kernel2d: analog — eigenstaendiger 2D-Geometrie-Kern (Port von kernel2d.ts),
# per wasm-pack (Feature "web") zu WASM gebaut, ausserhalb des cad-tauri-Workspace.
exclude = ["render2d", "render3d", "geometry", "kernel2d", "dwgimport"]
[package] [package]
name = "cad-tauri" name = "cad-tauri"
@@ -51,5 +47,5 @@ render2d = { path = "render2d", features = ["window"], optional = true }
render3d = { path = "render3d", features = ["window"], optional = true } render3d = { path = "render3d", features = ["window"], optional = true }
# Versionen an render2d gekoppelt (identische raw-window-handle 0.6-Kette). # Versionen an render2d gekoppelt (identische raw-window-handle 0.6-Kette).
winit = { version = "0.30", optional = true } winit = { version = "0.30", optional = true }
wgpu = { version = "29", optional = true } wgpu = { version = "22", optional = true }
pollster = { version = "0.4", optional = true } pollster = { version = "0.3", optional = true }

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