Browser-BIM (cad): semantisches Modell, abgeleitete 2D/3D-Sichten, Zeichenwerkzeuge
Standalone-Browser-Port von DOSSIER. Enthaelt das semantische Modell mit Plan-/3D-Ableitung, Zeichen- und Editierwerkzeuge, Rhino-artiges Befehlssystem, dockbares Panel-System, Resource-Manager, DXF/.lin/.pat-Import, i18n (de/en) sowie Projektdokumentation und Probe-Harness.
This commit is contained in:
+152
@@ -0,0 +1,152 @@
|
||||
# Dokumentation — Standalone Browser-BIM (cad)
|
||||
|
||||
> Stand: 2026-06-29 · Übergeordnet: [ROADMAP.md](../ROADMAP.md) (Vision & Phasen) ·
|
||||
> [CONVENTIONS.md](../CONVENTIONS.md) (Konventionen) · [ARCHITECTURE.md](../ARCHITECTURE.md).
|
||||
|
||||
Dieses Verzeichnis bündelt die Recherche- und Design-Dokumente für `cad`, die
|
||||
eigenständige Browser-Variante des DOSSIER-Rhino-Plugins (React + TypeScript +
|
||||
Three.js + SVG, alles client-side). **Leitprinzip aller Dokumente:** ein
|
||||
semantisches Modell ist die einzige Wahrheit; jede Sicht (3D, Grundriss, Schnitt)
|
||||
wird **abgeleitet**, Darstellung erst beim Rendern angewandt. Bezeichner im Code
|
||||
englisch (Vectorworks-Terminologie), Prosa deutsch, Einheiten intern in Metern.
|
||||
|
||||
Die Dokumente sind in vier Gruppen geordnet: **Tech** (Bibliotheken/Kernel),
|
||||
**Architektur/Design** (Aufbau & Bauteile), **UX** (Oberfläche & Interaktion),
|
||||
**Swisstopo/SIA** (CH-Geodaten & Flächenstandards).
|
||||
|
||||
---
|
||||
|
||||
## Tech — Technologie- & Bibliotheksauswahl
|
||||
|
||||
### [research/tech-selection.md](research/tech-selection.md)
|
||||
Evaluiert den kompletten Client-Stack für ein serverloses BIM-Werkzeug und
|
||||
empfiehlt **`replicad`** (idiomatische TS-Schicht über `opencascade.js`/OCCT, MIT)
|
||||
als primären B-Rep-Kernel im Web Worker, ergänzt durch **`Manifold`** (Apache-2.0)
|
||||
für schnelle, robuste Mesh-Booleans auf Importgeometrie — denn nur ein echter
|
||||
B-Rep-Kernel liefert exakte 2D-Ableitungen, und genau das löst replicads
|
||||
`drawProjection` (OCC-HLR, `{visible, hidden}`-Kanten direkt im Browser). Weitere
|
||||
Wahl: Import via **web-ifc + Fragments** (IFC), `dxf-parser` (DXF) und
|
||||
`libredwg-web` (DWG, aber **GPL-3.0 → vorab klären/kapseln**); Vektor-Export über
|
||||
**`svg2pdf.js` + `jsPDF`** (PDF) und **`@tarikjabiri/dxf`** (echte Hatch-Entities);
|
||||
Schraffuren als SVG-`<pattern>` mit `userSpaceOnUse` (maßstabskorrekt); Rendering
|
||||
über **`three/webgpu`** mit automatischem WebGL2-Fallback. Top-Risiken: DWG-Lizenz,
|
||||
OCCT-WASM-Größe, HLR-Kosten (pro Ansicht cachen), WebGPU vor Migration benchmarken.
|
||||
|
||||
---
|
||||
|
||||
## Architektur/Design — Aufbau, Datenmodell & Bauteile
|
||||
|
||||
### [../ARCHITECTURE.md](../ARCHITECTURE.md)
|
||||
Die übergreifende Standalone-Architektur und die systematische Übersetzung jedes
|
||||
DOSSIER-Konzepts in ein Browser-Äquivalent (30-zeilige **Rhino→Browser-Mapping-
|
||||
Tabelle**). Kern: das semantische `Project` (JSON) als einzige Wahrheit mit pure
|
||||
`derive()` zu Scene3D/Plan/Section; ein **Zwei-Achsen-Datenmodell**
|
||||
(`drawingLevels` × `layers`) plus `Resources`/`WallType`/`Element`/`Sheet`; ein
|
||||
**Zustand-Store** ersetzt DOSSIERs `sc.sticky`-Bus, **`.cad.json`** (File System
|
||||
Access API) + IndexedDB-Autosave ersetzen `doc.Strings`, und ein **Immer-Patch-
|
||||
Undo/Redo** eliminiert die Cache-Stale-Bugs strukturell. Ziel-Repo-Struktur mit
|
||||
**kleinen Bauteil-Modulen** statt des 7244-LOC-`elemente.py`-Monolithen; Rendering
|
||||
über einen `THREE.Group`-Baum, der den Ebenen-Baum spiegelt.
|
||||
|
||||
### [design/elements.md](design/elements.md)
|
||||
Legt **Daten, Generierung (3D + Plan) und Grip-Editing pro Bauteil** fest. Wichtigste
|
||||
Empfehlung: die **Prioritäts-T-/X-Verschneidung mehrschichtiger Wände** (Backbone-
|
||||
Algorithmus, Port von `_t_junction_layer_overrides`) — das höchstpriorisierte
|
||||
gemeinsame Material läuft durch, der Rest mitert an; Priorität sitzt am **Component**
|
||||
(`joinPriority` als Daten, nicht Hardcode). Deckt zudem gehostete Öffnungen mit
|
||||
LoD-Stufen (`_OEFF_PIECE_DEFS`), Decken mit Aussparungen, Treppen (gerade/L/Wendel,
|
||||
geschossübergreifend, normgerechtes 2D-Symbol), Dächer, Tragwerk und **SIA-416-Räume**
|
||||
(Shoelace-Fläche, Stempel, Färbung über Override-Preset) ab; das `Tool`-Interface +
|
||||
Snap-Engine ersetzt DOSSIERs Rhino-Command-Aliases.
|
||||
|
||||
### [design/plans-output.md](design/plans-output.md)
|
||||
Der Weg zu **schönen, normgerechten, druckfertigen 2D-Plänen** (Vektor-PDF). Zentrale
|
||||
Erkenntnis: Ansichten = Kamera + optionaler Schnitt, und es gibt **zwei Plan-Pfade**
|
||||
(symbolischer Grundriss aus Parametern vs. Schnitt/Ansicht via **HLR im Worker**,
|
||||
gecacht). Empfiehlt SVG/Paper-Space als Maßstabsmodell — Strichstärke/Schraffur sind
|
||||
direkt in mm definiert (`dpi = 96·devicePixelRatio`, Hatch-Faktor `sqrt(N)/10`), was
|
||||
DOSSIERs fragiles Plotweight-Rescaling überflüssig macht. Behandelt außerdem
|
||||
Ausschnitte/View-Snapshots, Layer-Kombinationen, Kamera-Presets + Norden-Rotation,
|
||||
Bemaßung sowie Sheets + Vektor-PDF-Export (`svg2pdf.js`/`jsPDF`, `PAPER_MM`).
|
||||
|
||||
### [design/resources-graphics.md](design/resources-graphics.md)
|
||||
Die **Stil-Schicht**: verwaltete Ressourcen-Bibliotheken (Component-/Hatch-/Line-
|
||||
Manager, alles per id referenziert), die `resolveStyle`-Kette
|
||||
(ByLayer → Element-Style → Override) und die **regelbasierte Overrides-Engine**.
|
||||
Schlüssel-Empfehlung: Overrides als **reine Render-Reads** modellieren (kein
|
||||
Backup/Restore wie in DOSSIER, da nichts mutiert wird) — inklusive eines
|
||||
**SIA-416-Presets** statt hartcodierter Färbung. Ergänzt Symbol-Bibliothek,
|
||||
Rich-Text-Annotationen, den LoD-Resolver (`resolveDetail`) und den Section-Style für
|
||||
geschnittene Bauteile; eine Tabelle zeigt, was der Browser hier gegenüber DOSSIER
|
||||
vereinfacht.
|
||||
|
||||
---
|
||||
|
||||
## UX — Oberfläche, Interaktion & gefühlte Geschwindigkeit
|
||||
|
||||
### [research/ux-patterns.md](research/ux-patterns.md)
|
||||
Untersucht UX-Muster moderner Browser-CAD/BIM-Tools (Arcol, Snaptrude, TestFit,
|
||||
Onshape, Vectorworks, Figma) und leitet **priorisierte Leitplanken** ab. Empfehlung
|
||||
für die Grundstruktur: eine feste, Figma-artige **3-Zonen-Shell**
|
||||
(Navigator/Viewport/Inspector) — explizit gegen Paletten-Wildwuchs —, mit
|
||||
Vectorworks-Navigation-Tabs für unsere zwei Achsen und einem zwei/drei-spaltigen
|
||||
Resource-Manager als Vorbild. Größte Differenzierungs-Hebel laut Doku:
|
||||
**Snapping/Inferencing** im Onshape-Stil (Vertex-Highlights, Achsenlinien, Shift
|
||||
unterdrückt) und **Grip-Editing über Sicht-Grenzen** (Schnittlinie im Plan ziehen);
|
||||
dazu perceived-performance-Muster (Skeletons, optimistic UI, 150-ms-Delay-then-show),
|
||||
eine Command-Palette (Cmd/Ctrl-K) und learn-by-doing-Onboarding am Sample-Projekt.
|
||||
|
||||
---
|
||||
|
||||
## Swisstopo/SIA — Schweizer Geodaten & Flächenstandards
|
||||
|
||||
### [research/swisstopo-sia.md](research/swisstopo-sia.md)
|
||||
Dokumentiert die **live getesteten** geo.admin.ch-Dienste und die SIA-Flächenlogik.
|
||||
Überraschendster Befund: **alles ist ohne eigenen Backend-Proxy nutzbar** — alle vier
|
||||
Hosts senden `access-control-allow-origin: *`, und der Height-Service antwortet
|
||||
faktisch frei. Schlüssel fürs Browser-Gelände-Mesh ist **swissALTI3D als Cloud-
|
||||
Optimized GeoTIFF** (Range-Requests via `geotiff.js`, kein Full-Download); die
|
||||
**Parzelle** kommt direkt als LV95-Polygon + EGRID aus dem Identify-Service. Empfiehlt
|
||||
einen konkreten Library-Satz (`proj4`, `geotiff`, `3DTilesRendererJS`/`loaders.gl`)
|
||||
und ordnet die Umsetzung in ROADMAP-Phasen ein (Phase 2 SIA-Räume = reine Logik →
|
||||
Phase 4a Koordinaten → 4b Gelände/Orthofoto → 4c Nachbargebäude). SIA-Teil:
|
||||
verifizierte SIA-416-Formeln, DOSSIERs SIA-Logik 1:1 portierbar (Shoelace,
|
||||
`compute_sia_bilanz`, CSV mit BOM); Origin-Shift (LV95 → 0/0/0) ist Pflicht wegen
|
||||
float32-Jitter, Caching über IndexedDB.
|
||||
|
||||
---
|
||||
|
||||
## Top-5 Querschnitts-Empfehlungen für die ROADMAP
|
||||
|
||||
Diese fünf Punkte tauchen in mehreren Dokumenten auf und sollten die ROADMAP-Planung
|
||||
und Priorisierung leiten:
|
||||
|
||||
1. **Pure-Ableitungs-Architektur als unverhandelbares Fundament** — ein
|
||||
semantisches Modell, alle Sichten abgeleitet, Darstellung erst beim Rendern.
|
||||
Trägt ARCHITECTURE.md, beide Plan-/Stil-Designs und die UX-Doku (billiger
|
||||
Split-View, optimistic Edits, kein Cache-Stale-/Override-Restore-Aufwand). Muss
|
||||
früh stehen (Store + Undo, Phase 0–1), weil sie alles Spätere prägt.
|
||||
|
||||
2. **OCCT/replicad im Web Worker früh als Spike absichern** — der B-Rep-Kernel und
|
||||
sein `drawProjection`-HLR sind der kritische Pfad für Schnitt/Ansicht (Risiko #4)
|
||||
*und* für exakte Wand-Booleans (Risiko #1) *und* für IFC. WASM-Größe, HLR-Kosten
|
||||
(pro Ansicht cachen) und das Worker-Pattern sollten vor Phase 3 mit einer echten
|
||||
Szene validiert werden.
|
||||
|
||||
3. **Component-getriebene Prioritäts-Verschneidung (Backbone-T/X) als zentrales
|
||||
Geometrie-Risiko** — `joinPriority` als Daten am Component; höchstes gemeinsames
|
||||
Material läuft durch, Rest mitert. Verbindet elements.md + resources-graphics.md;
|
||||
2D-Plan rein analytisch, exakte 3D-Booleans im Worker. Stufenweise umsetzen
|
||||
(Risiko #1, Phase 1).
|
||||
|
||||
4. **SVG/Paper-Space-Maßstabsmodell + maßstabskorrekte Schraffuren durchgängig** —
|
||||
Strichstärke/Text/Hatch in mm, `dpi = 96·devicePixelRatio`, Hatch `sqrt(N)/10`,
|
||||
SVG-`<pattern>` mit `userSpaceOnUse`. Eliminiert DOSSIERs Plotweight-Rescaling und
|
||||
speist denselben Serializer für Bildschirm, PDF und DXF (tech-selection +
|
||||
plans-output + resources-graphics).
|
||||
|
||||
5. **Schweiz-Spezifika als Differenzierer ohne Backend-Last** — SIA-416-Bilanz
|
||||
(reine Logik, Phase 2, ⭐) und der serverlose Swisstopo-Flow (CORS-offen,
|
||||
COG-Terrain, Parzelle/EGRID, Norden-Rotation, Origin-Shift). Klein im Aufwand,
|
||||
groß im CH-Marktwert; SIA-Färbung läuft über das Override-Preset, nicht über
|
||||
Sonderpfade.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Backend & Kollaboration — Architekturentscheidung
|
||||
|
||||
> Stand: 2026-06-29 · Ziel: komplett self-hosted, kollaborations-offen
|
||||
|
||||
## Grundsatz
|
||||
So lange wie möglich **client-only** bleiben; das Backend additiv einführen, ohne
|
||||
den Kern umzubauen. Die Pure-Ableitungs-Architektur (ein serialisierbares Modell,
|
||||
alle Sichten abgeleitet) ist bereits kollaborations-freundlich.
|
||||
|
||||
## Phasen
|
||||
| Phase | Persistenz / Backend |
|
||||
|---|---|
|
||||
| **0–3** (Modellierer) | **Client-only**: IndexedDB + Datei-Export/Import (JSON). Offline-fähig (PWA möglich). Kein Server. |
|
||||
| **5** (Konten/Persistenz) | **Supabase self-hosted** (Docker Compose): Postgres + Auth + Storage. Projekte, Versionen, Dateien (IFC/Pläne/Assets). Row-Level-Security pro Nutzer/Projekt. |
|
||||
| **6** (Kollaboration) | **Yjs (CRDT)** + **Hocuspocus** Sync-Server (Container), persistiert Snapshots nach Postgres. Presence/Cursors. Optional Supabase-Realtime nur für leichte Broadcasts. |
|
||||
|
||||
## Warum Yjs/Hocuspocus statt reinem Supabase-Realtime
|
||||
Gleichzeitiges Editieren eines strukturierten Dokuments braucht Konfliktauflösung
|
||||
(CRDT). Yjs ist dafür Standard; Hocuspocus ist der self-hostbare Server dazu und
|
||||
kann nach Postgres (Supabase) persistieren. Supabase-Realtime allein wäre nur
|
||||
Pub/Sub ohne Merge-Semantik.
|
||||
|
||||
## Was wir JETZT schon richtig machen (damit Collab nicht blockiert)
|
||||
- Dokumentmodell rein **JSON-serialisierbar**, keine Zyklen, stabile IDs.
|
||||
- Edits immutable über `setProject` → später leicht auf Yjs-Doc abbildbar
|
||||
(`Y.Map`/`Y.Array` je Sammlung: drawingLevels, layers, components, walls …).
|
||||
- Kein Wahrheits-Zustand im Three.js-Scene-Graph oder im DOM — alles ableitbar.
|
||||
- Ressourcen (Components/Hatches/Lines) als referenzierte Bibliotheken (IDs) →
|
||||
gut mergebar.
|
||||
|
||||
## Self-hosted Stack (Skizze, Phase 5/6)
|
||||
```
|
||||
docker-compose:
|
||||
supabase (postgres, gotrue auth, storage, kong gateway, studio)
|
||||
hocuspocus (yjs websocket sync, persist -> postgres)
|
||||
web (vite build, statisch via nginx/caddy)
|
||||
```
|
||||
Alles auf eigener Infrastruktur lauffähig; keine externe Cloud nötig.
|
||||
|
||||
## Offene Punkte
|
||||
- Granularität der CRDT-Struktur (pro Sammlung vs. pro Element).
|
||||
- Datei-Storage (Supabase Storage vs. S3-kompatibel/MinIO im selben Stack).
|
||||
- Auth-Modell (E-Mail, OIDC/SSO fürs Büro).
|
||||
@@ -0,0 +1,54 @@
|
||||
# Kontextmenü & Anzeige-Modi — 1:1 wie DOSSIER
|
||||
|
||||
> Quelle: DOSSIER `src/components/ContextMenu.jsx` + `DrawingLevelsApp.jsx`/Ebenen-Panel.
|
||||
> Maus-Schema (unsere Festlegung): **Mitte = navigieren** (Plan Pan / 3D Orbit, Shift+Mitte Pan) ·
|
||||
> **Links = Auswahl** · **Rechts = Kontextmenü** · **Rad = Zoom**.
|
||||
|
||||
## ContextMenu-Komponente (generisch, wiederverwendbar)
|
||||
`ContextMenu({ x, y, items, onClose, title })`
|
||||
- **item**: `{ label, icon?, onClick, disabled?, danger?, shortcut?, divider? }`
|
||||
- Fixed-Position mit Rand-Clamp (4px); min-width 200px; Radius 13px; weicher Schatten;
|
||||
Mount-Animation `scale(.94) translateY(-5px)` 100ms; Item-Hover = `--accent-dim`;
|
||||
`danger` = rote Schrift; `divider` = 1px Trenner; Titel oben (caps, 10px, muted).
|
||||
- **Schließen:** Klick außerhalb · Escape · erneuter Rechtsklick · nach Item-Klick.
|
||||
- z-index ~300 (unter Modals).
|
||||
|
||||
## Ebenen-Kontextmenü (Rechtsklick auf Ebenen-Zeile)
|
||||
1. **Ebeneneinstellungen…** (`settings`) — Ebenen-Dialog *(Rhino-spez. → vorerst Stub)*
|
||||
2. — Trenner —
|
||||
3. **Sub-Ebene hinzufügen…** (`add`)
|
||||
4. **Selektion hierher übertragen** (`move_down`) *(braucht Auswahl → später)*
|
||||
5. — Trenner —
|
||||
6. **Duplizieren** (`content_copy`) — Klon mit Suffix „ KOPIE"
|
||||
7. **Eigenschaften kopieren** (`colorize`) — Farbe + Linienstärke
|
||||
8. **Eigenschaften einfügen** (`format_paint`) — disabled wenn Clipboard leer
|
||||
9. — Trenner —
|
||||
10. **Löschen** (`delete`, danger) — disabled wenn ≤1 Ebene
|
||||
Titel = Ebenenname/-code.
|
||||
|
||||
## Zeichnungsebenen-Kontextmenü (Rechtsklick auf Geschoss/Schnitt/Zeichnung)
|
||||
1. **Einstellungen…** (`settings`)
|
||||
2. — Trenner —
|
||||
3. **Duplizieren** (`content_copy`) — Klon mit Suffix „ Kopie"
|
||||
4. — Trenner —
|
||||
5. **Löschen** (`delete`, danger) — disabled wenn ≤1 Ebene
|
||||
Titel = Name.
|
||||
|
||||
## „+"-Menü (Zeichnungsebenen)
|
||||
**Geschoss** (`layers`) · **Schnitt / Ansicht** (`content_cut`) · — · **Zeichnung** (`edit_note`)
|
||||
|
||||
## Anzeige-Modi (Dropdown oben im Panel) — **5 Modi** (für Ebenen UND Zeichnungsebenen)
|
||||
| Wert | Label | Verhalten |
|
||||
|---|---|---|
|
||||
| `all_force` | **Alle anzeigen** | alle erzwungen sichtbar; Augen gedimmt; Klick aufs Auge → wechselt zu „Ausgewählte" |
|
||||
| `all` | **Ausgewählte** | sichtbar nach per-Zeile-Flag |
|
||||
| `active` | **Nur aktive** | nur aktive sichtbar; andere stark gedimmt |
|
||||
| `grey` | **Andere grau** | aktive normal, andere 45% (Sichtbarkeits-Flags gelten) |
|
||||
| `grey_locked` | **Andere grau & gesperrt** | wie grey + andere gesperrt |
|
||||
Regel: Klick aufs Auge in `all_force`/`active` schaltet automatisch auf `all`.
|
||||
*(Hinweis: Panel-System-Workflow hatte vorerst nur 3 Modi — beim Kontextmenü-Build auf diese 5 angleichen.)*
|
||||
|
||||
## Migration (was sofort geht / was Stub bleibt)
|
||||
- Sofort: ContextMenu-Komponente 1:1; Duplizieren, Eigenschaften kopieren/einfügen, Löschen,
|
||||
+Menü, 5 Anzeige-Modi (alles reine JSON-State-Operationen).
|
||||
- Stub/später: „Ebeneneinstellungen…"/„Einstellungen…" (Dialog), „Sub-Ebene hinzufügen", „Selektion hierher übertragen".
|
||||
@@ -0,0 +1,760 @@
|
||||
# Aktive Zeichen- und Bearbeitungs-Werkzeuge
|
||||
|
||||
Status: Entwurf. Dieses Dokument spezifiziert das **Tool-System** für das aktive
|
||||
Erzeugen von Modell-Elementen durch Zeichnen im Grundriss: Wände (Achs-Polylinie
|
||||
→ `Wall` eines `WallType`) sowie reine 2D-Geometrie (Linie, Polylinie, Rechteck,
|
||||
Kreis, Bogen, Text). Es definiert die Werkzeug-Zustandsmaschine, die Live-Vorschau
|
||||
(Rubber-Band), das **Snapping** mit Bildschirm-Markern, die Ebenen-/Kategorie-/
|
||||
Stil-Zuordnung neuer Elemente und das neue Element `Drawing2D` samt Ableitung in
|
||||
`generatePlan`.
|
||||
|
||||
Bezugsdokumente: [elements.md](elements.md) (Wand-/Tür-Modell),
|
||||
[resources-graphics.md](resources-graphics.md) (Stil-Auflösung),
|
||||
[plans-output.md](plans-output.md) (Papier-Maßstab, mm-Strichstärken),
|
||||
[context-menu.md](context-menu.md) (Maus-Schema).
|
||||
|
||||
## 0. Architektur-Prinzip (Bezug zum Repo)
|
||||
|
||||
Die App folgt der Regel **ein semantisches Modell ist die einzige Wahrheit; jede
|
||||
Ansicht ist abgeleitet** (CONVENTIONS.md, `App.tsx`). Werkzeuge greifen darum NUR über
|
||||
`setProject` immutabel auf das `Project`-Modell zu; sie schreiben NIE Geometrie
|
||||
direkt in den Plan. Der `PlanView` bleibt eine reine Darstellungs-/Eingabe-
|
||||
Schicht. Das Tool-System setzt genau an der bestehenden Naht in `PlanView` an:
|
||||
|
||||
- **Modell↔Screen.** `PlanView` rechnet bereits Cursor-Pixel → viewBox-Einheiten
|
||||
(`clientToView`) → Modell-Meter (`viewToModel`). Diese Umrechnung ist die
|
||||
Grundlage; Werkzeuge arbeiten ausschließlich in **Modell-Metern** (CONVENTIONS.md:
|
||||
intern alles in Metern). Für Snap-Marker brauchen Werkzeuge zusätzlich die
|
||||
Rückrichtung Modell → viewBox (`toScreen`, existiert bereits) bzw. Modell →
|
||||
Client-Pixel.
|
||||
- **Pointer-Handling.** `PlanView` besitzt heute drei Gesten an der linken Taste/
|
||||
Mitte/rechts: Auswahl/Marquee, Pan, Kontextmenü. Das Tool-System schiebt sich
|
||||
VOR diese Logik: ist ein aktives Zeichenwerkzeug gewählt (≠ `select`), übernimmt
|
||||
das Werkzeug `pointerdown/move/up`; das `select`-Werkzeug delegiert an die heute
|
||||
schon vorhandene Auswahl-/Marquee-Logik (kein Verhaltensbruch).
|
||||
- **Pan/Zoom bleiben immer aktiv.** Mittlere Maustaste (Pan) und Mausrad (Zoom)
|
||||
laufen unverändert weiter, auch während ein Zeichenwerkzeug aktiv ist — sonst
|
||||
kann man beim Zeichnen nicht navigieren.
|
||||
|
||||
## 1. Datenfluss-Überblick
|
||||
|
||||
```
|
||||
TopBar (Werkzeugleiste) --activeTool--> App-State
|
||||
│
|
||||
┌──── activeTool, wallTypeId, defaultCategoryCode ────┐
|
||||
▼ ▼
|
||||
PlanView ── pointerdown/move/up (Modellpunkt) ──> ToolController
|
||||
▲ │
|
||||
Snap-Marker + Rubber-Band-Overlay <── DraftState (Vorschau) ──┘
|
||||
│ │
|
||||
└──────────────── commit ──> onToolCommit(Element) ──> setProject
|
||||
```
|
||||
|
||||
`activeTool` und die Werkzeug-Parameter (aktiver `WallType`, Default-Kategorie)
|
||||
liegen als **View-State** in `App.tsx` — wie `viewType`, `detail`, `selectedWallIds`
|
||||
bereits dort liegen. Der `ToolController` ist **frameworkfrei** (reines TS, kein
|
||||
React-State pro Mausbewegung — analog zu `drag`/`marquee` als `useRef` in
|
||||
`PlanView`), damit die Live-Vorschau ohne Re-Render des ganzen Baums läuft. Nur
|
||||
beim **Commit** wird `setProject` (Re-Render) ausgelöst.
|
||||
|
||||
## 2. Koordinaten & Hilfsfunktionen
|
||||
|
||||
`PlanView` exportiert künftig zwei reine Konverter (heute intern vorhanden),
|
||||
plus die effektive Pixel-pro-Meter-Skala für die Snap-Toleranz:
|
||||
|
||||
```ts
|
||||
// PlanView-intern bereits da; wird als stabile Callbacks nach außen gereicht.
|
||||
type ToModel = (clientX: number, clientY: number) => Vec2; // Pixel → Meter
|
||||
type ToClient = (m: Vec2) => { x: number; y: number }; // Meter → Pixel
|
||||
type PxPerMeter = () => number; // aktuelle meet-Skala * PX_PER_M (Snap-Toleranz)
|
||||
```
|
||||
|
||||
`PxPerMeter` ergibt sich aus `meetScale(view) * PX_PER_M` (beides in `PlanView`
|
||||
vorhanden). Snap-Toleranzen werden in **Bildschirm-Pixeln** definiert (z. B. 10 px)
|
||||
und über `pxPerMeter` in Meter umgerechnet — so ist der Fangradius zoom-unabhängig
|
||||
konstant am Bildschirm.
|
||||
|
||||
## 3. Tool-System
|
||||
|
||||
### 3.1 Werkzeug-Identität und Registry
|
||||
|
||||
```ts
|
||||
export type ToolId =
|
||||
| "select" // Default: Auswahl/Marquee (heutiges Verhalten)
|
||||
| "wall" // Wand-Achs-Polylinie → Wall je Segment
|
||||
| "line" // einzelne 2D-Strecke
|
||||
| "polyline" // offene 2D-Polylinie
|
||||
| "rect" // 2D-Rechteck (zwei Ecken)
|
||||
| "circle" // 2D-Kreis (Zentrum + Radius)
|
||||
| "arc" // 2D-Bogen (3-Punkt oder Zentrum-Start-Ende)
|
||||
| "text"; // 2D-Textmarke
|
||||
|
||||
/** Live-Kontext, den ein Werkzeug bei jedem Schritt erhält. */
|
||||
export interface ToolContext {
|
||||
project: Project;
|
||||
/** Aktives Geschoss/Zeichnungsebene (Ziel der neuen Elemente). */
|
||||
level: DrawingLevel;
|
||||
/** Default-Kategorie-Code für neue Elemente (siehe §6). */
|
||||
defaultCategoryCode: string;
|
||||
/** Aktiver Wandtyp für das Wand-Werkzeug. */
|
||||
activeWallTypeId: string;
|
||||
/** Aktiver Linienstil-Code für 2D-Primitive (Line Manager). */
|
||||
activeLineStyleId: string;
|
||||
/** Snapping-Einstellungen (an/aus je Typ, ortho, grid). */
|
||||
snap: SnapSettings;
|
||||
/** Pixel pro Meter (für Snap-Toleranz in Metern). */
|
||||
pxPerMeter: number;
|
||||
}
|
||||
|
||||
/** Ein an einer Modellposition ausgelöstes Pointer-Ereignis. */
|
||||
export interface ToolPointer {
|
||||
/** Roher Modellpunkt (vor Snapping), in Metern. */
|
||||
raw: Vec2;
|
||||
/** Gesnappter Punkt + Marker-Info (siehe §5). null = kein Snap. */
|
||||
snap: SnapResult | null;
|
||||
/** Effektiver Punkt = snap?.point ?? raw. */
|
||||
point: Vec2;
|
||||
/** Modifikatoren (Shift = Ortho erzwingen, Ctrl = Snap aus, Alt = …). */
|
||||
shift: boolean;
|
||||
ctrl: boolean;
|
||||
alt: boolean;
|
||||
button: number; // 0 links, 2 rechts
|
||||
}
|
||||
|
||||
/** Was ein Werkzeug-Schritt nach außen meldet. */
|
||||
export interface ToolResult {
|
||||
/** Neuer Vorschau-Zustand (Rubber-Band-Geometrie); null = nichts zu zeigen. */
|
||||
draft: ToolDraft | null;
|
||||
/** Bei Abschluss: Mutation, die App über setProject anwendet. */
|
||||
commit?: (p: Project) => Project;
|
||||
/** true → Werkzeug ist fertig und kehrt in seinen Ruhezustand zurück. */
|
||||
done?: boolean;
|
||||
}
|
||||
|
||||
/** Die Werkzeug-Schnittstelle (reine Funktionen über einen internen State). */
|
||||
export interface Tool {
|
||||
id: ToolId;
|
||||
/** UI-Label-Key (i18n), z. B. "tool.wall". */
|
||||
labelKey: string;
|
||||
/** Material-Symbol-Name für die Werkzeugleiste. */
|
||||
icon: string;
|
||||
/** Statuszeilen-Hinweis-Key je Phase (z. B. "tool.wall.firstPoint"). */
|
||||
hintKey: (state: ToolState) => string;
|
||||
|
||||
/** Initialer Ruhezustand. */
|
||||
init(): ToolState;
|
||||
/** Klick/Tap (pointerdown→up ohne Drag, bzw. „setze Punkt"). */
|
||||
onClick(state: ToolState, p: ToolPointer, ctx: ToolContext): [ToolState, ToolResult];
|
||||
/** Bewegung (Hover/Drag): nur Vorschau, nie Commit. */
|
||||
onMove(state: ToolState, p: ToolPointer, ctx: ToolContext): [ToolState, ToolResult];
|
||||
/** Doppelklick/Enter: mehrteilige Werkzeuge abschließen (z. B. Polylinie). */
|
||||
onCommitGesture(state: ToolState, ctx: ToolContext): [ToolState, ToolResult];
|
||||
/** Esc: aktuellen Entwurf verwerfen, zurück in den Ruhezustand. */
|
||||
onCancel(state: ToolState): [ToolState, ToolResult];
|
||||
/** Backspace: letzten gesetzten Punkt zurücknehmen (mehrteilig). */
|
||||
onUndoPoint?(state: ToolState, ctx: ToolContext): [ToolState, ToolResult];
|
||||
}
|
||||
```
|
||||
|
||||
`ToolState` ist je Werkzeug ein Discriminated Union (Beispiel Wand in §4). Der
|
||||
`ToolController` hält genau eine aktive `Tool`-Instanz + deren `ToolState` in
|
||||
einem `useRef` und ist die einzige Stelle, die diese Methoden aufruft.
|
||||
|
||||
### 3.2 Vorschau-Geometrie (Rubber-Band)
|
||||
|
||||
```ts
|
||||
/** Darstellbare Vorschau — dieselben Primitive wie der Plan, plus Marker. */
|
||||
export interface ToolDraft {
|
||||
/** Vorschau-Primitive (gestrichelt/halbtransparent gezeichnet). */
|
||||
preview: Primitive[];
|
||||
/** Bereits gesetzte „feste" Stützpunkte (kleine Quadrate). */
|
||||
vertices: Vec2[];
|
||||
/** Optionaler Maß-/Winkel-Text am Cursor (z. B. "3.20 m, 90°"). */
|
||||
hud?: { at: Vec2; text: string };
|
||||
}
|
||||
```
|
||||
|
||||
Wichtig: Die Vorschau benutzt **dieselben `Primitive`-Typen** wie `generatePlan`
|
||||
(`polygon | line | arc`). Damit kann der Vorschau-Layer mit derselben
|
||||
`PrimitiveShape`-Renderlogik gezeichnet werden (DRY) — nur mit einer
|
||||
Vorschau-CSS-Klasse (gestrichelt, Akzentfarbe). Für die Wand-Vorschau kann das
|
||||
Werkzeug sogar `generatePlan` auf einem **temporären Projekt** (Original + die in
|
||||
Bau befindliche Wand) aufrufen, um echte gehrte Poché live zu zeigen; in der
|
||||
ersten Phase reicht eine einfache Bandvorschau (`wallCorners`).
|
||||
|
||||
### 3.3 Zustandsmaschine (allgemein)
|
||||
|
||||
Jedes Werkzeug ist eine kleine Maschine über `pointerdown → move → up`. Da
|
||||
`PlanView` Pointer-Capture nutzt, kommen `move`/`up` zuverlässig an. Generisches
|
||||
Muster:
|
||||
|
||||
```
|
||||
ruht ──pointerdown──> (Werkzeug setzt 1. Punkt / startet Drag)
|
||||
▲ │
|
||||
│ ├──move──> Vorschau (rubber-band), kein Commit
|
||||
│ │
|
||||
│ (mehrteilig) pointerdown──> Punkt anhängen, Vorschau weiter
|
||||
│ │
|
||||
└──Esc/Cancel─────────────┤
|
||||
▼
|
||||
Doppelklick/Enter/letzter Punkt ──> commit(project) ──> ruht
|
||||
```
|
||||
|
||||
- **Klick-vs-Drag.** Wie heute in `PlanView` (`MARQUEE_THRESHOLD_PX`): unter der
|
||||
Schwelle ist es ein „Punkt setzen" (Klick), darüber ein Drag. Rechteck/Kreis/
|
||||
Linie unterstützen BEIDE Bedienarten: Zwei-Klick (Punkt, Punkt) ODER Drücken-
|
||||
Ziehen-Loslassen. Polyline/Wall sind reine Klickfolgen mit Abschluss per
|
||||
Doppelklick/Enter.
|
||||
- **Esc** verwirft den Entwurf (`onCancel`) und bleibt im selben Werkzeug.
|
||||
Zweites Esc (im Ruhezustand) schaltet zurück auf `select`.
|
||||
- **Rechtsklick** während eines aktiven Entwurfs = „abschließen/abbrechen"
|
||||
(CAD-üblich), KEIN Kontextmenü; im Ruhezustand öffnet Rechtsklick wie bisher
|
||||
das Plan-Kontextmenü.
|
||||
|
||||
### 3.4 Einbettung in PlanView (Pointer-Routing)
|
||||
|
||||
`PlanView` bekommt zwei neue Props:
|
||||
|
||||
```ts
|
||||
interface PlanViewProps {
|
||||
// … bisherige Props …
|
||||
/** Aktives Werkzeug; "select" = bisheriges Verhalten. */
|
||||
activeTool?: ToolId;
|
||||
/**
|
||||
* Werkzeug-Treiber. PlanView ruft diese Callbacks mit fertig gesnappten
|
||||
* Modellpunkten auf und rendert den zurückgegebenen Draft als Overlay.
|
||||
*/
|
||||
toolHandlers?: {
|
||||
onToolDown(p: ToolPointer): void;
|
||||
onToolMove(p: ToolPointer): void;
|
||||
onToolUp(p: ToolPointer): void;
|
||||
onToolDoubleClick(): void;
|
||||
/** liefert die zu zeichnende Vorschau (von App/Controller gehalten). */
|
||||
draft: ToolDraft | null;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Routing in `onPointerDown` (Ergänzung der bestehenden Methode):
|
||||
|
||||
```
|
||||
onPointerDown(e):
|
||||
if e.button === 1: → bestehender Pan (unverändert)
|
||||
if e.button === 0:
|
||||
if activeTool === "select": → bestehende Auswahl-/Marquee-Geste
|
||||
else:
|
||||
setPointerCapture
|
||||
p = makeToolPointer(e) // raw → snap → point (§5)
|
||||
toolHandlers.onToolDown(p)
|
||||
if e.button === 2 (rechts):
|
||||
if activeTool !== "select" && entwurf aktiv: toolHandlers.onToolUp({button:2,…}) // abschließen
|
||||
else: bestehendes Kontextmenü
|
||||
```
|
||||
|
||||
`onPointerMove`/`onPointerUp` analog: bei aktivem Zeichenwerkzeug an
|
||||
`onToolMove`/`onToolUp` routen statt an Pan/Marquee. Der Cursor wird auf
|
||||
`crosshair` gesetzt. `makeToolPointer` führt das Snapping aus (§5) und liefert den
|
||||
fertigen `ToolPointer`.
|
||||
|
||||
Die **Snap-Marker** und der **Draft** werden als zusätzliche SVG-Gruppe NACH den
|
||||
Plan-Primitiven, aber vor der Auswahl-Hervorhebung gerendert (immer obenauf,
|
||||
`pointerEvents="none"`). Marker werden in viewBox-Einheiten über `toScreen`
|
||||
positioniert (existiert bereits).
|
||||
|
||||
## 4. Werkzeug: Wand (Wall)
|
||||
|
||||
Das Wand-Werkzeug zeichnet eine **Achs-Polylinie**; jedes Segment wird zu einem
|
||||
eigenständigen `Wall`-Element des aktiven `WallType` auf dem aktiven Geschoss.
|
||||
Aufeinanderfolgende Segmente teilen sich einen Knoten → die bestehende
|
||||
`computeJoins`-Verschneidung (in `generatePlan`) erzeugt automatisch saubere
|
||||
Gehrungen an den Ecken. Kein zusätzlicher Join-Code nötig.
|
||||
|
||||
### 4.1 Zustand
|
||||
|
||||
```ts
|
||||
type WallToolState =
|
||||
| { phase: "idle" }
|
||||
| {
|
||||
phase: "drawing";
|
||||
/** Bisher gesetzte Achs-Knoten (in Metern). */
|
||||
points: Vec2[];
|
||||
/** Aktuelle Cursor-Position (gesnappt) für die Rubber-Band-Vorschau. */
|
||||
cursor: Vec2 | null;
|
||||
};
|
||||
```
|
||||
|
||||
### 4.2 Pseudocode
|
||||
|
||||
```
|
||||
WallTool.onClick(state, p, ctx):
|
||||
if state.phase === "idle":
|
||||
return [{phase:"drawing", points:[p.point], cursor:p.point}, {draft: draftFor([p.point], p.point, ctx)}]
|
||||
else: // weiteren Knoten anhängen
|
||||
pts = [...state.points, p.point]
|
||||
# Ortho/Snap haben p.point bereits ausgerichtet (§5).
|
||||
return [{phase:"drawing", points: pts, cursor: p.point}, {draft: draftFor(pts, p.point, ctx)}]
|
||||
|
||||
WallTool.onMove(state, p, ctx):
|
||||
if state.phase !== "drawing": return [state, {draft:null}]
|
||||
return [{...state, cursor:p.point}, {draft: draftFor(state.points, p.point, ctx)}]
|
||||
|
||||
WallTool.onCommitGesture(state, ctx): // Doppelklick / Enter / Rechtsklick
|
||||
if state.phase !== "drawing" || state.points.length < 2:
|
||||
return [{phase:"idle"}, {draft:null, done:true}]
|
||||
pts = state.points
|
||||
return [{phase:"idle"}, {
|
||||
draft: null, done: true,
|
||||
commit: (proj) => appendWalls(proj, pts, ctx)
|
||||
}]
|
||||
|
||||
WallTool.onCancel(state):
|
||||
return [{phase:"idle"}, {draft:null, done:true}]
|
||||
|
||||
WallTool.onUndoPoint(state):
|
||||
if state.phase==="drawing" && state.points.length>1:
|
||||
return [{...state, points: state.points.slice(0,-1)}, {draft: …}]
|
||||
return [{phase:"idle"}, {draft:null}]
|
||||
```
|
||||
|
||||
`draftFor` baut die Vorschau: feste Segmente zwischen `points` + ein „lebendes"
|
||||
Segment `points[last] → cursor`. Pro Segment werden die vier Band-Eckpunkte über
|
||||
`wallCorners(a, b, thickness)` (vorhanden) berechnet und als Vorschau-`polygon`
|
||||
gezeichnet; zusätzlich ein HUD mit Länge `|b−a|` und Winkel. `thickness =
|
||||
wallTypeThickness(getWallType(...))`.
|
||||
|
||||
### 4.3 Commit ins Modell
|
||||
|
||||
```
|
||||
appendWalls(project, pts, ctx):
|
||||
newWalls = []
|
||||
for i in 0 .. pts.length-2:
|
||||
a = pts[i]; b = pts[i+1]
|
||||
if |b-a| < EPS: continue // Null-Segmente überspringen
|
||||
newWalls.push({
|
||||
id: uniqueId("W"), // siehe §8 (ID-Vergabe)
|
||||
type: "wall",
|
||||
floorId: ctx.level.id, // aktives Geschoss
|
||||
categoryCode: ctx.defaultCategoryCode, // §6
|
||||
start: a, end: b,
|
||||
wallTypeId: ctx.activeWallTypeId,
|
||||
height: ctx.level.floorHeight ?? 2.6, // Geschosshöhe als Default
|
||||
})
|
||||
return { ...project, walls: [...project.walls, ...newWalls] }
|
||||
```
|
||||
|
||||
Hinweise:
|
||||
- **Höhe** erbt die lichte Geschosshöhe (`DrawingLevel.floorHeight`), Fallback 2.6 m.
|
||||
- **Geschossbindung**: Das Wand-Werkzeug ist nur aktiv, wenn `level.kind === "floor"`
|
||||
(sonst gibt es keine Wände). In `drawing`-Ebenen ist das Wand-Werkzeug
|
||||
deaktiviert (nur 2D-Werkzeuge); siehe §6.
|
||||
- Die Wicklung wird NICHT erzwungen — `leftNormal`-Konvention (CONVENTIONS.md) und
|
||||
`computeJoins` arbeiten richtungsunabhängig pro Segment.
|
||||
|
||||
## 5. Snapping
|
||||
|
||||
Snapping läuft in `makeToolPointer` (PlanView) BEVOR der Punkt an das Werkzeug
|
||||
geht. Es prüft mehrere Snap-Quellen, wählt die nächstgelegene innerhalb der
|
||||
Toleranz und liefert sowohl den gefangenen Punkt als auch eine **Marker-Art** für
|
||||
die Bildschirmdarstellung.
|
||||
|
||||
### 5.1 Typen
|
||||
|
||||
```ts
|
||||
export type SnapKind =
|
||||
| "endpoint" // Wand-Achsenende, Polylinien-Knoten, Primitiv-Endpunkt
|
||||
| "midpoint" // Mitte einer Strecke/Wandachse
|
||||
| "intersection" // Schnittpunkt zweier Achsen/Linien
|
||||
| "center" // Kreis-/Bogenzentrum
|
||||
| "quadrant" // Kreis-Quadrantenpunkte (0/90/180/270°)
|
||||
| "onEdge" // nächster Punkt AUF einer Wandachse/Linie (Lot)
|
||||
| "grid" // Rasterpunkt
|
||||
| "ortho" // orthogonal/winkelrastriert zum vorigen Punkt
|
||||
| "extension"; // Verlängerung einer Achse (gestrichelte Hilfslinie)
|
||||
|
||||
export interface SnapResult {
|
||||
point: Vec2; // gefangener Punkt (Meter)
|
||||
kind: SnapKind;
|
||||
/** Quell-Element (für Marker/Hilfslinien), optional. */
|
||||
refA?: Vec2;
|
||||
refB?: Vec2;
|
||||
/** Bildschirm-Distanz Cursor→Snap (px) — für die Auswahl des Besten. */
|
||||
distPx: number;
|
||||
}
|
||||
|
||||
export interface SnapSettings {
|
||||
enabled: boolean; // Master-Schalter (Ctrl invertiert temporär)
|
||||
endpoint: boolean;
|
||||
midpoint: boolean;
|
||||
intersection: boolean;
|
||||
center: boolean;
|
||||
onEdge: boolean;
|
||||
grid: boolean;
|
||||
gridSize: number; // Rasterweite in Metern, z. B. 0.10
|
||||
ortho: boolean; // Shift erzwingt zusätzlich
|
||||
angleStep: number; // Winkelraster in Grad (z. B. 45)
|
||||
tolerancePx: number; // Fangradius am Bildschirm, z. B. 10
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Snap-Kandidaten sammeln
|
||||
|
||||
Quellen pro Geschoss (gefiltert auf sichtbare Kategorien, wie der Plan):
|
||||
|
||||
| Snap | Quelle |
|
||||
|------|--------|
|
||||
| endpoint | `wall.start`, `wall.end` aller sichtbaren Wände; Knoten bereits gesetzter Draft-Punkte; `Drawing2D`-Vertices |
|
||||
| midpoint | Mitte jeder Wandachse und jedes 2D-Segments |
|
||||
| intersection | paarweise `lineIntersect` der Wandachsen (nur Paare, deren Boxen sich am Cursor nähern) |
|
||||
| center/quadrant | Kreise/Bögen aus `Drawing2D` |
|
||||
| onEdge | Lotfußpunkt des Cursors auf jede nahe Wandachse/2D-Linie |
|
||||
| grid | Rundung des Cursors auf `gridSize` |
|
||||
| ortho | Ausrichtung relativ zum letzten Draft-Punkt (§5.4) |
|
||||
|
||||
Performance: Kandidaten werden je `move` neu erzeugt, aber **früh nach
|
||||
Bildschirm-Distanz gefiltert** (nur Punkte innerhalb ~`2·tolerancePx`). Bei
|
||||
großen Modellen kann eine grobe Bounding-Box-Vorauswahl je Wand vorgeschaltet
|
||||
werden; in den ersten Phasen genügt lineares Scannen (Wandzahl ist klein).
|
||||
|
||||
### 5.3 Auswahl-Pseudocode
|
||||
|
||||
```
|
||||
computeSnap(rawModel, ctx, draftPoints, lastPoint):
|
||||
if ctrl(): return null # Snap temporär aus
|
||||
s = ctx.snap
|
||||
tolM = s.tolerancePx / ctx.pxPerMeter # px-Toleranz → Meter
|
||||
cands: SnapResult[] = []
|
||||
|
||||
if s.endpoint: cands += endpoints(...) filtered to within tolM
|
||||
if s.midpoint: cands += midpoints(...)
|
||||
if s.intersection: cands += intersections(...)
|
||||
if s.center: cands += centers/quadrants(...)
|
||||
if s.onEdge: cands += perpendicularFeet(...) # niedrigere Priorität
|
||||
|
||||
# Punkt-Snaps haben Vorrang vor Linien-/Raster-Snaps:
|
||||
pick = argmin(cands, by distPx within tolM, tie-break by priority)
|
||||
if pick: rawModel = pick.point
|
||||
|
||||
# Ortho/Winkelraster wirkt RELATIV zum letzten Punkt und ÜBERLAGERT:
|
||||
if (s.ortho || shift()) && lastPoint:
|
||||
rawModel = applyAngleConstraint(lastPoint, rawModel, s.angleStep)
|
||||
# Wenn dabei auch ein Punkt-Snap nahe der Ortho-Linie liegt → bevorzugen.
|
||||
|
||||
if !pick && s.grid:
|
||||
g = snapToGrid(rawModel, s.gridSize)
|
||||
if dist(g, rawModel) within tolM: return {point:g, kind:"grid", …}
|
||||
|
||||
return pick ?? null
|
||||
```
|
||||
|
||||
Prioritätsreihenfolge bei gleichem Abstand: `endpoint > intersection > midpoint >
|
||||
center/quadrant > onEdge > grid`. Ortho/Winkelraster ist eine **Projektion**, kein
|
||||
Punkt-Kandidat: es verschiebt den (ggf. schon gesnappten) Punkt auf die nächste
|
||||
erlaubte Richtung vom letzten Knoten.
|
||||
|
||||
### 5.4 Ortho / Winkelraster
|
||||
|
||||
```
|
||||
applyAngleConstraint(from, to, stepDeg):
|
||||
d = to - from
|
||||
ang = atan2(d.y, d.x)
|
||||
k = round(ang / rad(stepDeg)) * rad(stepDeg)
|
||||
len = |d|
|
||||
return from + (cos(k), sin(k)) * len
|
||||
```
|
||||
|
||||
Mit `stepDeg = 90` ist das klassisches Ortho (H/V); `45` erlaubt Diagonalen.
|
||||
`Shift` erzwingt Ortho temporär unabhängig von der Einstellung.
|
||||
|
||||
### 5.5 Bildschirm-Marker
|
||||
|
||||
Pro aktivem Snap zeichnet `PlanView` ein Marker-Glyph an `toScreen(snap.point)`
|
||||
(`pointerEvents="none"`, eigene CSS-Klassen, papierkonstante Größe via
|
||||
non-scaling):
|
||||
|
||||
- `endpoint` → kleines Quadrat ▫
|
||||
- `midpoint` → Dreieck �△
|
||||
- `intersection` → ✕
|
||||
- `center` → ○, `quadrant` → ◇
|
||||
- `onEdge` → ⟂-Glyph
|
||||
- `grid` → feiner Punkt
|
||||
- `ortho`/`extension` → zusätzlich eine **gestrichelte Hilfslinie** von `refA`
|
||||
(Bezugspunkt) zum Cursor
|
||||
|
||||
Marker erscheinen NUR während ein Zeichenwerkzeug aktiv ist. i18n-Tooltips/Status
|
||||
(„Endpunkt", „Mittelpunkt", …) über `t('snap.endpoint')` etc.
|
||||
|
||||
## 6. Ebene, Kategorie und Stil neuer Elemente
|
||||
|
||||
Neue Elemente brauchen eine **Zeichnungsebene** (DrawingLevel) und eine
|
||||
**Kategorie** (LayerCategory `code`) sowie — bei 2D-Primitiven — einen Stift/
|
||||
Schraffur-Stil.
|
||||
|
||||
### 6.1 Zeichnungsebene (Ziel)
|
||||
|
||||
- Ziel ist **immer das aktive Geschoss/die aktive Zeichnungsebene** (`activeLevelId`
|
||||
in `App.tsx`). Wände nur auf `kind === "floor"`. 2D-Primitive (`Drawing2D`) auf
|
||||
jeder Ebene, also auch auf `kind === "drawing"` (freie 2D-Zeichnung).
|
||||
|
||||
### 6.2 Kategorie (categoryCode)
|
||||
|
||||
- Es gibt eine **aktive Kategorie** als View-State (`activeCategoryCode` in App,
|
||||
neu). Default beim Start: der Code der gewählten Wand-Kategorie (im Sample „20"
|
||||
Wände), bzw. die erste sichtbare Kategorie. Die Statusleiste zeigt heute schon
|
||||
die „aktive Ebene" (`activeLayerName`); diese wird künftig von `activeCategoryCode`
|
||||
gespeist statt nur aus der Auswahl abgeleitet.
|
||||
- Neue Wände: `categoryCode = activeCategoryCode` (z. B. „20").
|
||||
- Neue 2D-Primitive: ebenfalls `activeCategoryCode`. Sinnvoll ist eine eigene
|
||||
2D-/Hilfslinien-Kategorie (z. B. „90 Zeichnung"); diese wird über die
|
||||
Kategorie-Auswahl in der Statusleiste/Werkzeugleiste gesetzt.
|
||||
- Die Kategorie liefert Farbe + Strichstärke (`LayerCategory.color`, `.lw`), genau
|
||||
wie `generatePlan` es heute für Wände via `categoryLwMap` nutzt.
|
||||
|
||||
### 6.3 Stift/Schraffur
|
||||
|
||||
- **Wände** erhalten KEINEN eigenen Stift — ihr Erscheinungsbild kommt aus dem
|
||||
`WallType` (Component → Hatch → LineStyle) und der Kategorie-`lw` (bestehender
|
||||
Pfad in `generatePlan`).
|
||||
- **2D-Primitive** referenzieren optional einen `LineStyle` aus dem Line Manager
|
||||
(`activeLineStyleId`). Ohne expliziten Stil erben sie Farbe/Strichstärke aus der
|
||||
Kategorie (`color`, `lw`). Flächige 2D-Primitive (geschlossenes Rechteck/Kreis/
|
||||
Polyline) können optional eine Schraffur (`hatchId`) tragen.
|
||||
|
||||
## 7. Speicherung der 2D-Primitive: `Drawing2D`
|
||||
|
||||
2D-Geometrie wird als neues Modell-Element `Drawing2D` gespeichert — analog zu
|
||||
`Wall`/`Door` ein semantisches Element, das beim Rendern abgeleitet wird (KEINE
|
||||
vorab erzeugten Primitive im Modell). Damit bleibt die Architektur „Modell →
|
||||
abgeleitete Ansicht" intakt.
|
||||
|
||||
### 7.1 Typ
|
||||
|
||||
```ts
|
||||
/** Geometrie-Form eines 2D-Zeichenelements. */
|
||||
export type Drawing2DGeom =
|
||||
| { shape: "line"; a: Vec2; b: Vec2 }
|
||||
| { shape: "polyline"; pts: Vec2[]; closed: boolean }
|
||||
| { shape: "rect"; min: Vec2; max: Vec2 } // achsparallel
|
||||
| { shape: "circle"; center: Vec2; r: number }
|
||||
| {
|
||||
shape: "arc";
|
||||
center: Vec2;
|
||||
r: number;
|
||||
/** Start-/Endwinkel in Radiant (math. Konvention, CCW positiv). */
|
||||
a0: number;
|
||||
a1: number;
|
||||
}
|
||||
| { shape: "text"; at: Vec2; text: string; height: number; angle: number };
|
||||
|
||||
/** Ein freies 2D-Zeichenelement auf einer Zeichnungsebene. */
|
||||
export interface Drawing2D {
|
||||
id: string;
|
||||
type: "drawing2d";
|
||||
/** Zeichnungsebene (Geschoss ODER freie 2D-Ebene). */
|
||||
levelId: string;
|
||||
/** Grafik-Kategorie (Ebene) — liefert Farbe/Strichstärke als Default. */
|
||||
categoryCode: string;
|
||||
geom: Drawing2DGeom;
|
||||
/** Optionaler Linienstil (Line Manager); sonst Kategorie-Default. */
|
||||
lineStyleId?: string;
|
||||
/** Optionale Schraffur für geschlossene Formen (Hatch Manager). */
|
||||
hatchId?: string;
|
||||
/** Optionale explizite Strichfarbe; sonst Kategorie-Farbe. */
|
||||
color?: string;
|
||||
}
|
||||
```
|
||||
|
||||
Ergänzung am `Project`:
|
||||
|
||||
```ts
|
||||
export interface Project {
|
||||
// … bisher …
|
||||
drawings2d: Drawing2D[]; // NEU
|
||||
}
|
||||
export type Element = Wall | Door | Drawing2D; // erweitert
|
||||
```
|
||||
|
||||
`sampleProject` bekommt ein leeres `drawings2d: []`. Lösch-/Referenz-Regeln:
|
||||
beim Löschen einer Zeichnungsebene werden auch deren `Drawing2D` entfernt (analog
|
||||
zur bestehenden Wand-/Tür-Bereinigung in `deleteLevel`).
|
||||
|
||||
### 7.2 Ableitung in `generatePlan`
|
||||
|
||||
`generatePlan` rendert künftig zusätzlich die `Drawing2D` des Geschosses (gefiltert
|
||||
wie Wände auf sichtbare Kategorien + `categoryDisplay`). Neue Funktion
|
||||
`addDrawing2D(out, project, d, greyed, lwMm)`:
|
||||
|
||||
```
|
||||
addDrawing2D(out, project, d):
|
||||
color = d.color ?? categoryColor(d.categoryCode)
|
||||
weight = lineStyle(d.lineStyleId)?.weight ?? categoryLw(d.categoryCode)
|
||||
dash = lineStyle(d.lineStyleId)?.dash ?? null
|
||||
switch d.geom.shape:
|
||||
"line": out.push({kind:"line", a, b, cls:"draw2d", weightMm:weight, dash})
|
||||
"polyline": for each segment → line-Primitive (closed → Schluss-Segment)
|
||||
"rect": vier Kanten als line-Primitive (oder polygon, falls hatchId)
|
||||
"circle": → als zwei 180°-Bögen (arc-Primitive) ODER neues Primitiv (s. u.)
|
||||
"arc": → arc-Primitive (center/from/to/r aus a0,a1)
|
||||
"text": → neues text-Primitiv (s. u.)
|
||||
```
|
||||
|
||||
Dabei wird, wo möglich, der **vorhandene** `Primitive`-Vorrat (`line`, `arc`,
|
||||
`polygon`) wiederverwendet — die Strichstärke kommt in mm Papier (wie der Rest des
|
||||
Plans), Farbe über eine CSS-Klasse oder ein neues optionales `color`-Feld am
|
||||
`line`-Primitive.
|
||||
|
||||
Zwei `Primitive`-Erweiterungen sind nötig:
|
||||
|
||||
```ts
|
||||
// kreisförmige Vollkurve (Kreis) — sonst muss man sie in zwei Bögen zerlegen:
|
||||
| { kind: "circle"; center: Vec2; r: number; cls: string; weightMm: number;
|
||||
dash?: number[] | null; fill?: string; greyed?: boolean }
|
||||
// Textmarke:
|
||||
| { kind: "text"; at: Vec2; text: string; heightMm: number; angle: number;
|
||||
cls: string; color?: string; greyed?: boolean }
|
||||
```
|
||||
|
||||
`PlanView.renderPrimitive` bekommt entsprechende `case`-Zweige (`<circle>`,
|
||||
`<text>`). Text wird in **Papier-Millimetern** dimensioniert (Höhe → `mmToPx`,
|
||||
non-scaling), damit die Schrifthöhe beim Zoomen papierkonstant bleibt (analog zu
|
||||
Strichstärken in `plans-output.md`).
|
||||
|
||||
Das `arc`-Primitiv zeichnet heute nur Kurzbögen (≤180°, `large-arc=0`). Für
|
||||
beliebige 2D-Bögen wird es um ein `largeArc`-Flag erweitert (aus `|a1−a0|`
|
||||
berechnet); abwärtskompatibel (Default 0).
|
||||
|
||||
## 8. ID-Vergabe & Immutabilität
|
||||
|
||||
- Neue IDs über einen kleinen Helfer `uniqueId(prefix)` (z. B.
|
||||
`\`${prefix}-${Date.now()}-${counter++}\``), konsistent mit der bestehenden
|
||||
Praxis in `App.tsx` (`floor-${Date.now()}` usw.). Wand-Präfix „W", 2D-Präfix
|
||||
„dr2d".
|
||||
- Alle Mutationen laufen über `setProject` immutabel (CONVENTIONS.md / App-Konvention).
|
||||
Der `commit(project)` eines Werkzeugs ist eine reine Funktion `Project →
|
||||
Project`; App ruft `setProject(prev => result.commit(prev))`.
|
||||
|
||||
## 9. App- und PlanView-Verdrahtung (konkret)
|
||||
|
||||
Neuer View-State in `App.tsx`:
|
||||
|
||||
```ts
|
||||
const [activeTool, setActiveTool] = useState<ToolId>("select");
|
||||
const [activeCategoryCode, setActiveCategoryCode] = useState<string>(/* erste Wand-Kat */);
|
||||
const [activeWallTypeId, setActiveWallTypeId] = useState<string>(project.wallTypes[0].id);
|
||||
const [activeLineStyleId, setActiveLineStyleId] = useState<string>(project.lineStyles[0].id);
|
||||
const [snap, setSnap] = useState<SnapSettings>(DEFAULT_SNAP);
|
||||
const toolStateRef = useRef<ToolState>(getTool(activeTool).init());
|
||||
const [draft, setDraft] = useState<ToolDraft | null>(null);
|
||||
```
|
||||
|
||||
Der `ToolController` ist eine kleine Hook/Klasse, die `toolStateRef` hält und die
|
||||
`PlanView.toolHandlers` implementiert:
|
||||
|
||||
```
|
||||
onToolDown(p): [st, res] = tool.onClick(toolStateRef.current, p, ctx)
|
||||
toolStateRef.current = st; setDraft(res.draft)
|
||||
if res.commit: setProject(res.commit)
|
||||
if res.done: toolStateRef.current = tool.init()
|
||||
onToolMove(p): [st, res] = tool.onMove(...); toolStateRef.current=st; setDraft(res.draft)
|
||||
onToolDoubleClick(): [st,res]=tool.onCommitGesture(...); apply commit/done; setDraft(null)
|
||||
```
|
||||
|
||||
Keyboard (global, nur wenn ein Zeichenwerkzeug aktiv ist):
|
||||
`Esc → onCancel`, `Enter → onCommitGesture`, `Backspace → onUndoPoint`. Beim
|
||||
Wechsel von `activeLevelId`/`viewType` wird der laufende Entwurf verworfen (analog
|
||||
zur bestehenden Auswahl-Bereinigung in den `useEffect`s).
|
||||
|
||||
`ctx` (ToolContext) wird in App via `useMemo` aus Project + aktiven Selektionen
|
||||
gebaut und an PlanView/Controller gereicht.
|
||||
|
||||
### 9.1 Werkzeugleiste (TopBar)
|
||||
|
||||
Eine neue Werkzeug-Gruppe in der `TopBar` (links, vor den Ansichts-Toggles), als
|
||||
i18n-beschriftete Icon-Buttons (`t('tool.select')`, `t('tool.wall')`, …). Aktiv-
|
||||
Zustand hervorgehoben. Daneben: Auswahl des aktiven `WallType` (für Wand) und der
|
||||
aktiven Kategorie/des Linienstils (Dropdowns), sowie Snap-Toggles (kleines
|
||||
Snap-Menü mit Checkboxen je `SnapKind`, Grid-Größe, Winkelraster). Wand-/2D-
|
||||
Werkzeuge werden je nach `level.kind` aktiviert/deaktiviert (Tooltip nennt den
|
||||
Grund — wie die bestehenden disabled-Menüpunkte in `App.tsx`).
|
||||
|
||||
### 9.2 i18n-Keys (neu, Auszug)
|
||||
|
||||
```
|
||||
tool.select / tool.wall / tool.line / tool.polyline / tool.rect /
|
||||
tool.circle / tool.arc / tool.text
|
||||
tool.wall.firstPoint / tool.wall.nextPoint / tool.wall.finish
|
||||
snap.endpoint / snap.midpoint / snap.intersection / snap.center /
|
||||
snap.quadrant / snap.onEdge / snap.grid / snap.ortho
|
||||
snap.settings / snap.gridSize / snap.angleStep
|
||||
status.activeWallType / status.activeCategory / status.activeTool
|
||||
```
|
||||
|
||||
Alle sichtbaren Strings über `t(...)` (CONVENTIONS.md). Identifier bleiben englisch.
|
||||
|
||||
## 10. Übrige Werkzeuge (Kurzspezifikation)
|
||||
|
||||
| Werkzeug | Eingabe | Zustand | Commit |
|
||||
|----------|---------|---------|--------|
|
||||
| **Line** | 2 Punkte (Klick-Klick oder Drag) | `{a?}` | `Drawing2D{shape:"line"}` |
|
||||
| **Polyline** | n Punkte, Abschluss Doppelklick/Enter; `closed` per „C" oder Klick auf Start | `{pts}` | `Drawing2D{shape:"polyline"}` |
|
||||
| **Rectangle** | 2 Ecken (Drag oder Klick-Klick) | `{p0?}` | `Drawing2D{shape:"rect"}` (min/max sortiert) |
|
||||
| **Circle** | Zentrum + Radius-Punkt | `{center?}` | `Drawing2D{shape:"circle"}` |
|
||||
| **Arc** | 3 Punkte (Start, durch, Ende) ODER Zentrum-Start-Ende (Modus-Toggle) | `{p0?,p1?}` | `Drawing2D{shape:"arc"}` (a0/a1 aus Punkten) |
|
||||
| **Text** | 1 Punkt → Inline-Eingabefeld (wie `InlineEditor` in App) | `{at?}` | `Drawing2D{shape:"text"}` |
|
||||
|
||||
Alle nutzen dasselbe `Tool`-Interface, dasselbe Snapping und denselben Draft-/
|
||||
Commit-Pfad. Text öffnet beim Setzen des Ankerpunkts ein kleines Overlay-Eingabe-
|
||||
feld (an `toClient(at)` positioniert) und committet bei Enter/Blur.
|
||||
|
||||
3-Punkt-Bogen → Zentrum: Umkreismittelpunkt der drei Punkte (Schnitt der
|
||||
Mittelsenkrechten via `lineIntersect`), `r`, `a0/a1` aus Start-/Endwinkel; Drehsinn
|
||||
aus dem mittleren Punkt.
|
||||
|
||||
## 11. Phasenplan
|
||||
|
||||
**Phase 1 — Gerüst + Select + Wall + Line (MVP).**
|
||||
1. `ToolId`, `Tool`, `ToolContext`, `ToolPointer`, `ToolDraft`, `ToolResult`,
|
||||
`SnapResult`, `SnapSettings`, `Drawing2D`(+`Project.drawings2d`) als Typen.
|
||||
2. `PlanView`: `toScreen`/`viewToModel`/`pxPerMeter` als Callbacks nach außen;
|
||||
Pointer-Routing für `activeTool !== "select"`; Draft-/Marker-Overlay-Rendering;
|
||||
Crosshair-Cursor.
|
||||
3. `ToolController` + App-State (`activeTool`, `activeCategoryCode`,
|
||||
`activeWallTypeId`, `snap`) + Keyboard (Esc/Enter/Backspace).
|
||||
4. **WallTool** voll funktionsfähig (Polylinie → Wände, Live-Band-Vorschau, HUD,
|
||||
Commit via `appendWalls`). Verschneidung kommt automatisch aus `computeJoins`.
|
||||
5. **LineTool** als erstes 2D-Werkzeug; `generatePlan.addDrawing2D` für `line`;
|
||||
`Drawing2D`-Löschung beim Geschoss-Löschen.
|
||||
6. **Snapping Stufe 1**: endpoint + grid + ortho (Shift), mit Bildschirm-Markern.
|
||||
7. TopBar-Werkzeuggruppe (select/wall/line) + WallType-/Kategorie-Auswahl;
|
||||
i18n-Keys; Statusleiste zeigt aktives Werkzeug + Kategorie.
|
||||
8. Verifizieren: `npx tsc -b`, `npm run build`, Screenshot via `scripts/probe.mjs`
|
||||
(Wand zeichnen, Gehrung prüfen).
|
||||
|
||||
**Phase 2 — Snapping vervollständigen + 2D-Grundformen.**
|
||||
- Snap: midpoint, intersection, onEdge (Lot), extension-Hilfslinien, Winkelraster
|
||||
(45°), Snap-Einstellungsmenü in der TopBar.
|
||||
- Werkzeuge: Polyline, Rectangle (inkl. optionaler Schraffur für geschlossene
|
||||
Formen). `Primitive`-Erweiterung nur für tatsächlich gebrauchte Formen.
|
||||
|
||||
**Phase 3 — Kurven + Text.**
|
||||
- `Primitive` um `circle` (+ `arc` `largeArc`) und `text` erweitern; PlanView-
|
||||
Renderzweige; Text papierkonstant.
|
||||
- Werkzeuge: Circle, Arc (3-Punkt), Text (Inline-Eingabe). Snap: center/quadrant.
|
||||
|
||||
**Phase 4 — Bearbeitung (Folge-Doku).**
|
||||
- Grips/Editieren bestehender Elemente (Wand-Enden ziehen, 2D-Vertices verschieben),
|
||||
Verschieben/Kopieren/Rotieren der Auswahl, numerische Direkteingabe von
|
||||
Länge/Winkel im HUD. Baut auf demselben Snapping + Draft-Pfad auf. (Eigenes
|
||||
Design-Dokument; hier nur als Ausblick.)
|
||||
|
||||
## 12. Architektur-Garantien (Checkliste)
|
||||
|
||||
- Modell bleibt einzige Wahrheit; Werkzeuge schreiben nur `Project`, nie Plan-
|
||||
Primitive. Ansichten (Plan/3D) leiten ab.
|
||||
- Alle Bezeichner englisch; alle UI-Texte über `t(...)`; Einheiten in Metern,
|
||||
Anzeige via `formatM`; Strichstärken/Texthöhen in mm Papier (non-scaling).
|
||||
- Native-App-Verhalten: kein Browser-Kontextmenü während des Zeichnens; keine
|
||||
Textauswahl (außer Text-Eingabefeld); Pan/Zoom immer verfügbar.
|
||||
- DRY: Vorschau nutzt dieselben `Primitive` + Renderlogik wie der Plan; Snapping
|
||||
und Commit-Pfad sind werkzeugübergreifend geteilt.
|
||||
</content>
|
||||
</invoke>
|
||||
@@ -0,0 +1,424 @@
|
||||
# Design — Bauteile (Elements)
|
||||
|
||||
> Teil der Standalone-Architektur — siehe [../../ARCHITECTURE.md](../../ARCHITECTURE.md).
|
||||
> Output/Pläne: [plans-output.md](plans-output.md). Ressourcen/Stile:
|
||||
> [resources-graphics.md](resources-graphics.md).
|
||||
|
||||
Dieses Dokument legt die **Daten**, die **Generierung** (3D-Geometrie + Plan-
|
||||
Symbolik) und das **Grip-Editing** je Bauteil fest und übersetzt DOSSIERs
|
||||
`elemente.py` (7244 LOC, Monolith) in **kleine Bauteil-Module** (`src/model/
|
||||
elements/wall.ts`, `opening.ts`, …). Bezeichner englisch, Prosa deutsch, Meter.
|
||||
|
||||
DOSSIERs Architektur dort: pro Element eine **Achse/Outline-Source** (editierbar)
|
||||
+ ein **auto-generiertes Volumen** (`wand_axis`+`wand_volume`, Outline+Brep).
|
||||
Browser-Äquivalent: das **semantische Element ist die Source**; Geometrie wird per
|
||||
`generate*()` **abgeleitet** (nie persistiert). Das ist sauberer als DOSSIERs
|
||||
zwei-Objekt-Modell und kennt kein Cache-Stale.
|
||||
|
||||
---
|
||||
|
||||
## 0. Gemeinsames Fundament
|
||||
|
||||
```ts
|
||||
// src/model/elements/base.ts
|
||||
interface ElementBase {
|
||||
id: string;
|
||||
type: ElementType; // "wall" | "window" | "door" | "slab" | "stair" | "roof"
|
||||
// | "column" | "beam" | "space" | "draw2d"
|
||||
floorId: string; // Zeichnungsebene (Geschoss); bei gehosteten via Host
|
||||
categoryCode: string; // Ebene (Grafik-Kategorie), z.B. "20"
|
||||
styleId?: string; // optionaler Element-Override-Stil (resources-graphics.md)
|
||||
name?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Geometrie-Konvention** (aus CONVENTIONS.md, im Spike etabliert): Wand-Normale
|
||||
`n = leftNormal(u) = (-u.y, u.x)`; bei CCW-Wicklung zeigt `+n` nach innen.
|
||||
Schichten werden außen (`-T/2`) → innen (`+T/2`) gestapelt (`generatePlan.addWallPoche`,
|
||||
`Viewport3D.addWallMeshes`).
|
||||
|
||||
**Detailgrad** (LoD) — DOSSIERs `darstellung` (`auto|einfach|standard|detail`):
|
||||
```ts
|
||||
type DetailLevel = "coarse" | "medium" | "fine"; // einfach | standard | detail
|
||||
// Auflösung: Element-Wert "auto" → Dokument-/Snapshot-Wert; sonst Element-Wert.
|
||||
function resolveDetail(el: ElementBase, doc: { detailLevel: DetailLevel }): DetailLevel
|
||||
```
|
||||
≙ DOSSIER `_resolve_oeff_darstellung` + `get_aktive_darstellung`. Steuert, *wie
|
||||
viel* Symbolik gezeichnet wird (1:500 Rechteck → 1:50 Glas/Sims/Schwenkbogen).
|
||||
|
||||
**Generierungs-Signaturen** (jedes Modul exportiert beides):
|
||||
```ts
|
||||
function build3d(project, el, ctx): THREE.Object3D // Volumen (Schichten/Brep)
|
||||
function generatePlan(project, el, ctx, lod): Primitive[] // Schnittflächen + Symbol
|
||||
// ctx trägt baseElevation, joins, sichtbare Codes, resolver für Components/Styles
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Wand (Wall) — mehrschichtig
|
||||
|
||||
### 1.1 Daten
|
||||
```ts
|
||||
interface Wall extends ElementBase {
|
||||
type: "wall";
|
||||
start: Vec2; end: Vec2; // Achse (Centerline) im Grundriss [im Spike]
|
||||
wallTypeId: string; // → WallType.layers (außen→innen)
|
||||
height: number;
|
||||
reference: "mid" | "left" | "right"; // Referenzlage der Achse (DOSSIER _wand_referenz)
|
||||
baseOffset?: number; // UK relativ zu OKFF (default 0)
|
||||
topOffset?: number; // OK-Override (default = floorHeight)
|
||||
jointRole?: "auto" | "through" | "butt"; // T-Stoss-Rolle (DOSSIER wand_joint_rolle)
|
||||
// Mehrsegment-Wände (Polyline): optional axisPoints statt start/end
|
||||
axisPoints?: Vec2[];
|
||||
}
|
||||
```
|
||||
`reference` verschiebt die Achse auf Außenkante/Mitte (DOSSIER
|
||||
`_wall_offsets_from_referenz`): hilft beim Modellieren *und* beim Import fremder
|
||||
Pläne (ROADMAP §11). Offsets: `mid → [+T/2, -T/2]`, `left → [0, -T]`, `right → [+T, 0]`.
|
||||
|
||||
### 1.2 Generierung — 3D + Plan (Status: im Spike, einschichtig→mehrschichtig ✅)
|
||||
Beide Sichten extrudieren/füllen **dasselbe gehrte Band-Polygon** pro Schicht.
|
||||
Heute schon vorhanden:
|
||||
- `geometry.clippedBand(start, end, offA, offB, startCut, endCut)` — Band mit
|
||||
Gehrungsschnitt.
|
||||
- `generatePlan.addWallPoche` — pro Schicht ein gefülltes Polygon (Component-Fill +
|
||||
Schraffur), Öffnungen ausgespart.
|
||||
- `Viewport3D.addLayerPrism` — dasselbe Polygon via `ExtrudeGeometry`.
|
||||
|
||||
### 1.3 Wand-Verschneidung (Joins) — Risiko #1
|
||||
|
||||
**Status: L-Ecken-Gehrung ✅** (`joins.computeJoins` → `miterLine`, robust gegen
|
||||
Wicklung + ungleiche Dicken). **Offen: Prioritäts-T-/X-Stöße** bei mehrschichtigen
|
||||
Wänden.
|
||||
|
||||
DOSSIERs gelöste Logik (`elemente._t_junction_layer_overrides`, `_wand_should_apply_t_miter`),
|
||||
die wir portieren:
|
||||
|
||||
1. **Knoten finden:** Endpunkte auf Gitter runden (`roundKey`, existiert),
|
||||
gruppieren. `==1` freies Ende, `==2` L-Ecke (Gehrung, ✅), `>2` T/X.
|
||||
2. **Through-Wand bestimmen:** an einem T-Stoß läuft genau **eine** Wand durch.
|
||||
Auswahl nach `jointRole` (DOSSIER-Regel), sonst nach Component-`joinPriority`:
|
||||
```
|
||||
my.role="through" → ich laufe durch (kein Miter)
|
||||
my.role="butt" → ich stoße an (Miter)
|
||||
beide "auto" → höhere joinPriority = Through-Wand
|
||||
```
|
||||
3. **Schicht-Durchdringung (Backbone):** **nur das Material mit der höchsten
|
||||
gemeinsamen `joinPriority`** in *beiden* Wänden läuft durch und unioniert
|
||||
(T-Form). Beispiel ROADMAP §2d: Beton (800) läuft mittig durch; Putze (100)
|
||||
verbinden sich seitlich, gehen aber nirgends durch den Beton. Alle Nicht-
|
||||
Backbone-Schichten der anstoßenden Wand mitern an der Through-Außenkante
|
||||
(`standard_miter`). Ergebnis: gleichfarbige Außenlagen bilden automatisch
|
||||
saubere L-Stöße.
|
||||
|
||||
```ts
|
||||
// joins.ts — Erweiterung der bestehenden API
|
||||
interface WallCuts { startCut: Line|null; endCut: Line|null;
|
||||
// neu: pro-Schicht Overrides am T-Stoss
|
||||
layerExtensions?: number[]; // wie weit jede Schicht in Through-Body drillt
|
||||
layerMiters?: (Line|null)[]; // pro-Schicht Mitre (null = Backbone, läuft durch)
|
||||
}
|
||||
function computeJoins(project, walls): Map<string, WallCuts> // erweitert
|
||||
```
|
||||
|
||||
**Implementierungsplan (stufenweise, Risiko #1):**
|
||||
- (a) ✅ L-Gehrung bleibt.
|
||||
- (b) T-Stoß ohne Schichten: Backbone = ganze Wand; Through union, Stem mitert.
|
||||
- (c) T-Stoß mit Schichten: Backbone-Material-Logik wie oben (Port von
|
||||
`_t_junction_layer_overrides`).
|
||||
- (d) X-Stoß: paarweise als zwei T behandeln.
|
||||
- **Booleans:** Union/Extension der Backbone-Säule via **OpenCascade.js/Manifold**
|
||||
im Worker (`workers/geometry.worker.ts`), nur für 3D + exakten B-Rep-Export; der
|
||||
2D-Plan bleibt rein analytisch (Polygon-Clipping, kein Kernel) — schnell.
|
||||
- **Validierung:** Screenshot-Probe der T-Ecke (Beton durch, Putz seitlich).
|
||||
|
||||
### 1.4 Grip-Editing (Risiko #L, Phase 3–4)
|
||||
DOSSIER: Display-Conduit zeichnet dicke Marker an Achs-Endpunkten, MouseCallback
|
||||
fängt Klick → `GetPoint` mit Snap → `_replace_axis_vertex` → Volumen regeneriert
|
||||
(`wand_grips.py`). Browser-Port:
|
||||
- **Marker:** SVG-Kreise (r ≈ 7 px) an Endpunkten/Knicks der *selektierten* Wand,
|
||||
als Overlay über dem Plan (unabhängig von Ebenen-Sichtbarkeit) — exakt DOSSIERs
|
||||
Conduit-Idee.
|
||||
- **Hit-Test:** Pointer-Distanz < 14 px (DOSSIER `_HIT_RADIUS_PX`).
|
||||
- **Drag:** `pointerdown` auf Marker → Live-Preview-Linien zu Nachbar-Vertices →
|
||||
Snap (Endpunkt/Ortho/Raster) → `pointerup` → `store.apply(p => wall.start = newPt)`.
|
||||
Abgeleitete Sichten (Plan + 3D) re-derivieren reaktiv — kein manuelles Regen.
|
||||
- Funktioniert für Line (2 Grips) und Polyline (jeder Knick ein Grip), wie DOSSIER.
|
||||
|
||||
---
|
||||
|
||||
## 2. Öffnungen (Window / Door) — gehostet, LoD
|
||||
|
||||
### 2.1 Daten
|
||||
```ts
|
||||
interface OpeningBase extends ElementBase {
|
||||
hostWallId: string; // Host-Wand (Geschoss ergibt sich daraus) [im Spike]
|
||||
position: number; // Abstand entlang Wandachse vom Wand-Start (m)
|
||||
width: number; height: number;
|
||||
reference: "mid" | "left" | "right"; // Lage des Klickpunkts in der Öffnung
|
||||
detailLevel: DetailLevel | "auto";
|
||||
frame?: { width: number; depth: number; pos: "outer"|"mid"|"inner"; offset: number };
|
||||
outerSide: "left" | "right"; // welche Wandseite ist außen
|
||||
}
|
||||
interface Window extends OpeningBase {
|
||||
type: "window";
|
||||
sill: number; // Brüstungshöhe
|
||||
sashes: 1|2|3|4; // Flügelzahl
|
||||
sillProfileOut?: "none"|"narrow"|"standard"|"wide"; // Sims außen (DOSSIER _OEFF_SIMS_STYLES)
|
||||
sillProfileIn?: "none"|"narrow"|"standard"|"wide";
|
||||
glass: boolean;
|
||||
}
|
||||
interface Door extends OpeningBase {
|
||||
type: "door";
|
||||
swing: "left" | "right"; // Anschlagseite [im Spike]
|
||||
hinge: "start" | "end"; // Scharnierpfosten [im Spike]
|
||||
openAngle: number; // Plan-Öffnungswinkel 0–180 (default 90)
|
||||
doorType: "normal" | "wall-opening"; // Wandöffnung = ohne Blatt
|
||||
frameType: "casing" | "block"; // Zarge | Blockrahmen
|
||||
lintel?: "none"|"inner"|"outer"|"both";// Sturzlinien-Anzeige (DOSSIER _OEFF_STURZ)
|
||||
}
|
||||
```
|
||||
Felder 1:1 aus DOSSIERs `_OEFF_*`-Keys + `_OEFF_STYLE_FIELDS`. Presets (Fenster
|
||||
Standard/Gross/Bandlage, Tür Innen/Eingang/Verglast, Wandöffnung) als
|
||||
Style-Katalog (resources-graphics.md), seed wie `_OEFF_DEFAULT_STYLES`.
|
||||
|
||||
### 2.2 Host-Beziehung (Risiko #2)
|
||||
Die Öffnung kennt ihre Wand (`hostWallId`); ihre Geometrie wird **relativ zur
|
||||
Wandachse** berechnet (`opening.axisFrame(wall, position)` → Punkt + Tangente +
|
||||
Normale, ≙ DOSSIER `_oeff_axis_frame`). Verschiebt sich die Wand, folgt die
|
||||
Öffnung automatisch (sie hält keinen absoluten Punkt). Beim Plan/3D wird die
|
||||
Wand an `[position, position+width]` ausgespart — steht im Spike (`addWallPoche`
|
||||
Segmentierung, `addWallMeshes` Sturz).
|
||||
|
||||
### 2.3 Generierung nach LoD
|
||||
| LoD | Plan-Symbol | 3D |
|
||||
|---|---|---|
|
||||
| **coarse** (1:200/500) | Öffnung als Lücke + dünne Linie | Aussparung, kein Rahmen |
|
||||
| **medium** (1:100) | + Rahmenlinien, Tür-Schwenkbogen (`addDoorSymbol` ✅), Sturz gestrichelt | Aussparung + einfacher Rahmen-Quader |
|
||||
| **fine** (1:50) | + Glas-Doppellinie, Sims, Flügel-Teilung, Anschlag | Rahmen + Blatt + Glas (transparent) + Sims (DOSSIER `_OEFF_PIECE_DEFS`) |
|
||||
|
||||
- **Tür-Schwenkbogen:** im Spike (`generatePlan.addDoorSymbol` — Blatt + Arc).
|
||||
Ausbau: `openAngle`, lichte vs. volle Breite je LoD (Port von
|
||||
`_make_tuer_swing_curves`).
|
||||
- **3D-Stücke** (Rahmen/Glas/Flügel/Sims/Sturz) ≙ DOSSIER `_make_oeffnung_pieces`
|
||||
/ `_OEFF_PIECE_DEFS` — jeweils eigene Component (Farbe + Transparenz: Glas
|
||||
α≈0.88, IOR 1.5). Pieces landen auf Unter-Ebenen von `21 Türen/Fenster`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Decke / Boden (Slab) — mit Aussparungen
|
||||
|
||||
### 3.1 Daten
|
||||
```ts
|
||||
interface Slab extends ElementBase {
|
||||
type: "slab";
|
||||
boundary: Vec2[]; // geschlossener Umriss (CCW)
|
||||
slabTypeId: string; // mehrschichtig (analog WallType)
|
||||
openings?: Vec2[][]; // Aussparungen: Treppenauge, Schacht, Kamin (DOSSIER aussp)
|
||||
ukOverride?: number; okOverride?: number; // UK/OK statt auto (Abhängung, schräge Brüstung)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Generierung
|
||||
- **Z-Auflösung:** `okOverride ?? (baseElevation_oberes_Geschoss)`,
|
||||
`ukOverride ?? (ok - thickness)` — Port von `_resolve_decke_z`. Decke sitzt
|
||||
standardmäßig zwischen zwei Geschossen.
|
||||
- **3D:** `boundary` als `THREE.Shape`, Aussparungen als `shape.holes` (`THREE.Path`),
|
||||
`ExtrudeGeometry` über die Schichten (≙ `_make_decke_volume(outline, holes)`).
|
||||
- **Plan:** im Schnitt unter `cutHeight` meist nur Kante; Aussparungs-Ränder als
|
||||
Linien; geschnittene Decke (in Schnitt-Ansicht) bekommt Schraffur.
|
||||
- **Aussparung↔Decke:** Aussparung als geschlossene Curve, die räumlich in der
|
||||
Decke liegt (`_find_decke_containing_point` / `_find_aussparungen_for_decke`).
|
||||
Bei uns: `Slab.openings` direkt im Slab — keine separate Source nötig (einfacher
|
||||
als DOSSIERs Parent-Child).
|
||||
|
||||
---
|
||||
|
||||
## 4. Treppe (Stair) — Typen, Lauflinie, geschossübergreifend
|
||||
|
||||
### 4.1 Daten
|
||||
```ts
|
||||
interface Stair extends ElementBase {
|
||||
type: "stair";
|
||||
kind: "straight" | "l-shaped" | "spiral"; // gerade | L | Wendel (DOSSIER _TREPPE_ARTEN)
|
||||
run: Vec2[]; // Lauflinien-Stützpunkte (gerade: 2; L: 3; Wendel: Zentrum+Start)
|
||||
width: number;
|
||||
reference: "mid" | "left" | "right"; // Lage der Lauflinie zur Treppe
|
||||
steps: number; // Anzahl Steigungen
|
||||
mode: "solid" | "flat" | "slab-edge"; // massiv | flach | Plattenrand
|
||||
runSlabThickness?: number; // Lauf-Plattendicke
|
||||
floorEndId?: string; // Zielgeschoss (geschossübergreifend, Risiko #6)
|
||||
heightOverride?: number; ukOverride?: number;
|
||||
rules?: { riser:[lo,hi,on]; tread:[lo,hi,on]; stepGo:[lo,hi,on] }; // SIA-Komfortregeln
|
||||
lockRiser?: { value: number }; // Schrittmass-Lock (S fix, N passt sich an)
|
||||
// Plan-Symbol-Flags (DOSSIER _KEY_TREPPE_SHOW_*)
|
||||
show?: { treads; runLine; outline; breakLine };
|
||||
upperDashed?: boolean; // obere Stufen gestrichelt (über Schnitthöhe)
|
||||
arrowStyle?: "classic"|"filled"|"double"|"line";
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Generierung
|
||||
- **Steigung/Auftritt:** `riser = height/steps`; `tread` aus Lauflinienlänge /
|
||||
(steps−1). SIA-Komfort: `2·riser + tread ∈ [0.60, 0.65]` (DOSSIER
|
||||
`_TREPPE_SOLL_DEFAULT`). Lock: ist `lockRiser` gesetzt, wird `steps` neu
|
||||
berechnet statt `riser` zu ändern.
|
||||
- **3D je `kind`:** gerade → Stapel von Tritt-Quadern oder massive Rampe;
|
||||
L → zwei Läufe + Podest (`podestMin`); Wendel → um Zentrum rotierte Tritte
|
||||
(Port `_make_treppe_*_preview` / Volume-Funktionen). `mode` steuert massiv vs.
|
||||
Lauf-Platte.
|
||||
- **Geschossübergreifend (Risiko #6):** Höhe = `(baseElevation[floorEndId] -
|
||||
baseElevation[floorId])` falls `floorEndId` gesetzt; sonst Geschosshöhe. Treppe
|
||||
taucht dann in beiden Geschoss-Grundrissen auf (mit Schnitt an `cutHeight`).
|
||||
- **Plan-Symbol (normgerecht):** Lauflinie mit **Auf-/Abpfeil** (`arrowStyle`),
|
||||
Stufenkanten, Bruchlinie an `cutHeight` (untere durchgezogen, obere gestrichelt
|
||||
via `upperDashed`), Außenkante. ≙ DOSSIERs 2D-Treppensymbol; liegt auf Ebene
|
||||
`40 Treppen`/`41 Treppen-2D`.
|
||||
|
||||
### 4.3 Grip-Editing
|
||||
Lauflinien-Stützpunkte als Grips (wie Wand-Vertices, §1.4); Ziehen ändert
|
||||
Geometrie + Stufenzahl reaktiv.
|
||||
|
||||
---
|
||||
|
||||
## 5. Dach (Roof)
|
||||
|
||||
### 5.1 Daten
|
||||
```ts
|
||||
interface Roof extends ElementBase {
|
||||
type: "roof";
|
||||
outline: Vec2[]; // Grundriss-Umriss
|
||||
roofType: "mono" | "gable" | "hip" | "mansard"; // Pult|Sattel|Walm|Mansarde
|
||||
thickness: number;
|
||||
slope: number; // Grad (Hauptneigung)
|
||||
eaveIndex?: number; // Index der Traufkante (Pult)
|
||||
ridge?: "long" | "short"; // Firstrichtung (Sattel)
|
||||
// Mansarde:
|
||||
slopeLower?: number; kinkHeight?: number;
|
||||
mansardVariant?: "hip" | "gable" | "hip-gable";
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Generierung
|
||||
Port von DOSSIERs `_make_pultdach/_satteldach/_walmdach/_mansardendach*` +
|
||||
`_thicken_roof_inward`. Aufwand M–L (Mansarde später). Reihenfolge: Pult →
|
||||
Sattel → Walm → Mansarde. 3D als Brep/Mesh über OpenCascade.js (Worker), da
|
||||
Schräg-Verschneidung Booleans braucht. Plan: Firstlinien + Traufe + ggf.
|
||||
Höhenkoten.
|
||||
|
||||
---
|
||||
|
||||
## 6. Tragwerk (Column / Beam)
|
||||
|
||||
### 6.1 Daten
|
||||
```ts
|
||||
interface ProfileDef {
|
||||
shape: "square"|"rect"|"round"|"i-beam"|"tube"; // DOSSIER _TRAG_PROFILE
|
||||
b?: number; h?: number; d?: number; t?: number; // Breite/Höhe/Durchm./Wanddicke
|
||||
angle: number; // Rotation um Z
|
||||
}
|
||||
interface Column extends ElementBase { type:"column"; point: Vec2; profile: ProfileDef;
|
||||
uk?: number; ok?: number; }
|
||||
interface Beam extends ElementBase { type:"beam"; axis:[Vec2,Vec2]; profile: ProfileDef;
|
||||
zTop?: number; // hängt unter Decken-OK (zTop = ok der Decke)
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Generierung
|
||||
- **Querschnitt:** `profileCurve(shape, b,h,d,t, angle)` (Port `_trag_profile_curve`)
|
||||
→ für Stütze entlang Z extrudieren (`_make_stuetze_volume`), für Träger entlang
|
||||
der Achse (`_make_traeger_volume`, Profil in der Schnitt-Ebene).
|
||||
- **Träger achs-basiert unter Decke:** `zTop` default = OK der darüberliegenden
|
||||
Decke → Unterzug folgt automatisch (weniger Update-Fehler, ROADMAP §11).
|
||||
- Stützen liegen auf `25 Stützen`, Träger auf `35 Träger`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Raum (Space) — SIA-416 + Stempel
|
||||
|
||||
### 7.1 Daten
|
||||
```ts
|
||||
interface Space extends ElementBase {
|
||||
type: "space";
|
||||
boundary: Vec2[]; // geschlossener Umriss
|
||||
number?: string; spaceName?: string; function?: string;
|
||||
sia?: "" | "HNF"|"NNF"|"VF"|"FF"|"GF"|"AGF"; // SIA-416-Klasse
|
||||
persons?: number; // Personenbelegung (Brandschutz)
|
||||
areaRounding: "exact"|"0.01"|"0.1"|"0.5"|"1";
|
||||
stamp: StampConfig; // Raumstempel-Layout (s.u.)
|
||||
fill?: string; // Füll-Hatch-Id (optional)
|
||||
}
|
||||
interface StampConfig { // ≙ DOSSIER Stempel-Builder
|
||||
layout: FieldId[][]; // Zeilen × Felder, z.B. [["number","name"],["function"],["area"]]
|
||||
font; bold; italic; textHeight; textMode:"fixed"|"scale"; align:"left"|"mid"|"right";
|
||||
offset: Vec2; // Stempel-Position relativ zum Centroid (User-Move)
|
||||
}
|
||||
type FieldId = "number"|"name"|"function"|"area"|"sia";
|
||||
```
|
||||
|
||||
### 7.2 Generierung & Bilanz
|
||||
- **Fläche:** Shoelace-Formel über `boundary`, gerundet nach `areaRounding`
|
||||
(`_resolve_raum_rundung`). Umfang analog.
|
||||
- **Stempel:** als SVG-Text-Block aus `layout`-Zeilen am Centroid + `offset`
|
||||
(User kann verschieben; Offset persistiert wie DOSSIER `stamp_dx/dy`).
|
||||
`textMode:"scale"` → Texthöhe in Paper-mm × Massstab (plans-output.md).
|
||||
- **SIA-Färbung:** über die **Overrides-Engine** (regelbasiert), nicht hartcodiert
|
||||
— DOSSIER `_build_sia_preset_rules` erzeugt 4 Regeln `userString sia == hnf|nnf|vf|ff`
|
||||
→ Farbe + Solid-Hatch. Bei uns: ein Override-Preset „SIA-416" (resources-graphics.md),
|
||||
das auf `space.sia` matcht. Toggle = Preset aktivieren.
|
||||
- **SIA-Bilanz + CSV:** `panels/SiaBalance.tsx` summiert Flächen je Klasse je
|
||||
Geschoss → Tabelle + CSV-Export (`HNF/NNF/VF/FF/GF/AGF`). Pflicht für
|
||||
CH-Flächennachweis (ROADMAP ⭐).
|
||||
|
||||
---
|
||||
|
||||
## 8. Werkzeuge (Tools) — ersetzt Rhino-Command-Aliases
|
||||
|
||||
DOSSIER hat pro Bauteil ein Command-Alias (`rhino/aliases/cmd/wand.py`, `tuer.py`,
|
||||
`treppe.py`, …) das `GetPoint`-Interaktionen fährt. Browser: ein **Tool-Interface**
|
||||
mit Pointer-Handlern + Snap.
|
||||
|
||||
```ts
|
||||
interface Tool {
|
||||
id: ToolId;
|
||||
onPointerDown(pt: Vec2, snap: SnapResult, state): void;
|
||||
onPointerMove(pt: Vec2, snap: SnapResult, state): Primitive[]; // Live-Preview
|
||||
onPointerUp(pt: Vec2, snap: SnapResult, state): void;
|
||||
commit(store): void; // ruft store.apply()
|
||||
}
|
||||
```
|
||||
|
||||
| Tool | DOSSIER-Alias | Kurzbeschrieb |
|
||||
|---|---|---|
|
||||
| `wall` | `cmd/wand` | Achse zeichnen (Linie/Polyline), Dicke/Referenz/Typ aus „last used" |
|
||||
| `door`/`window` | `cmd/tuer`,`fenster` | Punkt auf Wandachse → hosten (Snap an Wand) |
|
||||
| `slab` | `cmd/decke` | Umriss klicken; Aussparung als Loch |
|
||||
| `stair` | `cmd/treppe` | Lauflinie + Breite + Stufen |
|
||||
| `roof` | `cmd/dach` | Umriss + Typ + Neigung |
|
||||
| `column`/`beam` | `cmd/stuetze`,`traeger` | Punkt / Achse + Profil |
|
||||
| `space` | `cmd/raum` | Umriss → Fläche auto, Stempel |
|
||||
| `draw2d` | `cmd/symbol`,`stempel` | Linie/Polyline/Rect/Kreis/Bogen/Text auf `60 Plangrafik` |
|
||||
| `pipette` | `cmd/pipette` | Stil/Typ von Element übernehmen |
|
||||
|
||||
**Snap-Engine** (`tools/snap.ts`): Endpunkt, Mitte, Schnitt, senkrecht, Raster,
|
||||
Ortho — ersetzt Rhinos OSnap. T-Snap an andere Wandachsen (Port
|
||||
`_t_snap_to_wand_axis`, `_snap_endpoint_to_other_wand_axis`) sorgt für saubere
|
||||
Knoten.
|
||||
|
||||
---
|
||||
|
||||
## 9. Element-Übersicht (BIM-Tree)
|
||||
`panels/ElementTree.tsx`: Baum Geschoss → Bauteiltyp → Element, mit Suche und
|
||||
Shift-Klick = Zoom (DOSSIER ELEMENTE-ÜBERSICHT). Inhaltsverzeichnis bei 100+
|
||||
Elementen — reine Ableitung aus `project.elements`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Reihenfolge der Umsetzung (verweist auf ROADMAP-Phasen)
|
||||
|
||||
1. **Phase 1:** Wand mehrschichtig ✅ + L-Gehrung ✅ → **Prio-T-Stoß** (§1.3);
|
||||
Tür/Fenster gehostet (§2); Decke + Aussparung (§3); Wand-Referenzlage (§1.1);
|
||||
Element-Übersicht (§9).
|
||||
2. **Phase 2:** Treppe (§4), Dach (§5), Tragwerk (§6), SIA-Räume + Stempel (§7),
|
||||
Stil-Kataloge.
|
||||
3. **Phase 3–4:** Grip-Editing (§1.4/§4.3), exakte B-Rep-Booleans im Worker.
|
||||
@@ -0,0 +1,498 @@
|
||||
# Design — Ebenen-Darstellung (Layer Display Settings)
|
||||
|
||||
> Teil der Standalone-Architektur — siehe [../../ARCHITECTURE.md](../../ARCHITECTURE.md).
|
||||
> Ressourcen/Stile: [resources-graphics.md](resources-graphics.md). Output/Pläne:
|
||||
> [plans-output.md](plans-output.md). Kontextmenü/Inline-Editor:
|
||||
> [context-menu.md](context-menu.md).
|
||||
|
||||
Dieses Dokument spezifiziert die **per-Ebene Darstellungseinstellungen** auf der
|
||||
`LayerCategory` (Grafik-Kategorie) und den dazugehörigen Editor
|
||||
„Ebeneneinstellungen…", der aus dem Ebenen-Kontextmenü geöffnet wird.
|
||||
|
||||
Heute trägt jede `LayerCategory` nur eine flache Strichstärke (`lw`), eine `color`
|
||||
und eine optionale `hatch` (ein freier String, der nirgends aufgelöst wird). Das
|
||||
reicht nicht: Eine Ebene soll — wie in Vectorworks/DOSSIER — einen vollständigen
|
||||
**Stift (PEN)** und eine vollständige **Standard-Schraffur (HATCH)** definieren,
|
||||
die beim Rendern angewandt werden. Bezeichner englisch, Prosa/UI-Text deutsch
|
||||
(CONVENTIONS.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. Zielbild
|
||||
|
||||
Jede Ebene definiert zwei Darstellungs-Aspekte, die in den Grundriss-Generator
|
||||
einfließen:
|
||||
|
||||
- **PEN** — Linienstil der Ebene: `type` (durchgezogen / gestrichelt / …),
|
||||
`color` und `lw` (Strichstärke in mm Papier). Steuert alle Umriss-/Symbol-Linien
|
||||
der Elemente dieser Ebene (Wand-Umriss, Tür-Symbol, Referenzlinie).
|
||||
- **HATCH** — Standard-Schraffur der Ebene: `pattern`, `scale`, `angle` und die
|
||||
`lineWeight` der Musterlinien. Wird angewandt, wo ein Element keine eigene
|
||||
Schraffur aus einem `Component` mitbringt (z. B. einschichtige/„grob"-Flächen,
|
||||
reine 2D-Zeichnungsobjekte einer Ebene).
|
||||
|
||||
Beides folgt dem Architektur-Prinzip: **Darstellung wird beim Rendern aufgelöst,
|
||||
nie in die Geometrie eingebacken.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Reference vs. Inline — Entscheidung
|
||||
|
||||
Es gibt drei Modelle, ein Datum für PEN/HATCH einer Ebene zu halten:
|
||||
|
||||
1. **Pure inline** — die Ebene trägt `{type,color,lw}` und `{pattern,scale,angle,
|
||||
lineWeight}` direkt. Einfach, aber: kein Wiederverwenden, kein zentrales
|
||||
Ändern; widerspricht der Ressourcen-Architektur (resources-graphics.md §1:
|
||||
„alles verweist per id, zentral änderbar").
|
||||
2. **Pure reference** — die Ebene trägt nur `lineStyleId` / `hatchId`. Konsistent,
|
||||
zentral, aber unflexibel: Eine Ebene kann z. B. nicht „den Stil X, aber in
|
||||
ihrer eigenen Farbe" wollen, ohne einen Klon-Stil anzulegen.
|
||||
3. **Reference + optionale per-Ebene Overrides** (EMPFOHLEN) — die Ebene
|
||||
**verweist** auf eine `LineStyle`- bzw. `HatchStyle`-Ressource und darf
|
||||
**einzelne Felder lokal überschreiben**. Das ist exakt das DOSSIER/Vectorworks-
|
||||
Muster: ein Stil als Basis, regelbasierte/lokale Overrides obendrauf
|
||||
(resources-graphics.md, `overrides.py`).
|
||||
|
||||
### Empfehlung: Reference + optionale Overrides
|
||||
|
||||
Begründung:
|
||||
|
||||
- **Zentrale Pflege bleibt erhalten:** Ändert man den Linienstil „Wand stark" im
|
||||
Line Manager, ziehen alle Ebenen nach, die ihn referenzieren und das jeweilige
|
||||
Feld nicht überschreiben.
|
||||
- **Lokale Freiheit ohne Stil-Wildwuchs:** Eine Ebene kann punktuell `color` oder
|
||||
`lw` anpassen (häufigster Fall: gleiche Strichart, andere Farbe), ohne einen
|
||||
fast identischen Stil zu duplizieren.
|
||||
- **Migrationsfähig:** Die heutige flache `{color, lw}` der Ebene wird zu reinen
|
||||
Overrides über einem neutralen Basis-Stil — verlustfrei (siehe §6).
|
||||
- **Konsistent mit der bestehenden Kette:** `Component → Hatch → LineStyle`
|
||||
verweist bereits per id; Ebenen reihen sich nahtlos ein.
|
||||
|
||||
Die Overrides sind **sparse**: nur gesetzte Felder überschreiben. Ein leeres
|
||||
Override-Objekt (oder `undefined`) bedeutet „komplett dem Stil folgen".
|
||||
|
||||
---
|
||||
|
||||
## 3. Datenmodell (TS)
|
||||
|
||||
### 3.1 LineStyle erweitern um `type`
|
||||
|
||||
`LineStyle` trägt heute schon `weight`, `color`, `dash`. Wir machen die
|
||||
Strichart explizit benennbar (statt nur via `dash`-Array), damit der Editor ein
|
||||
sauberes Dropdown anbietet und `dash` daraus ableiten kann.
|
||||
|
||||
```ts
|
||||
/** Benannte Strichart eines Stifts (für UI-Dropdown). */
|
||||
export type LineKind = "solid" | "dashed" | "dotted" | "dashdot";
|
||||
|
||||
/** mm-Strichmuster je Strichart (relativ zur Papier-mm). */
|
||||
export const LINE_DASH: Record<LineKind, number[] | null> = {
|
||||
solid: null,
|
||||
dashed: [0.6, 0.4],
|
||||
dotted: [0.1, 0.25],
|
||||
dashdot: [0.6, 0.25, 0.1, 0.25],
|
||||
};
|
||||
|
||||
export interface LineStyle {
|
||||
id: string;
|
||||
name: string;
|
||||
/** NEU: benannte Strichart; `dash` wird daraus abgeleitet, falls nicht gesetzt. */
|
||||
kind: LineKind;
|
||||
/** Strichstärke in Millimetern (≙ Rhino PlotWeight). */
|
||||
weight: number;
|
||||
color: string;
|
||||
/** Strichmuster in mm; `null` = durchgezogen. Optional — sonst aus `kind`. */
|
||||
dash: number[] | null;
|
||||
}
|
||||
```
|
||||
|
||||
> Hinweis: `kind` ist additiv; bestehende `LineStyle`-Daten setzen es per Migration
|
||||
> aus `dash` (§6).
|
||||
|
||||
### 3.2 PEN- und HATCH-Override-Typen
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Per-Ebene Stift (PEN). Verweist auf einen LineStyle; einzelne Felder dürfen
|
||||
* lokal überschrieben werden. Alle Override-Felder optional (sparse).
|
||||
*/
|
||||
export interface LayerPen {
|
||||
/** Basis-Linienstil (Line Manager). */
|
||||
lineStyleId: string;
|
||||
/** Lokale Overrides — nur gesetzte Felder gewinnen. */
|
||||
override?: {
|
||||
kind?: LineKind;
|
||||
color?: string;
|
||||
/** Strichstärke in mm Papier. */
|
||||
lw?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-Ebene Standard-Schraffur (HATCH). Verweist auf einen HatchStyle; einzelne
|
||||
* Felder dürfen lokal überschrieben werden. `enabled=false` = Ebene hat keine
|
||||
* Default-Schraffur (Umriss-only).
|
||||
*/
|
||||
export interface LayerHatch {
|
||||
/** Aktiv? false = keine Default-Schraffur dieser Ebene. */
|
||||
enabled: boolean;
|
||||
/** Basis-Schraffur (Hatch Manager). */
|
||||
hatchId: string;
|
||||
/** Lokale Overrides — nur gesetzte Felder gewinnen. */
|
||||
override?: {
|
||||
pattern?: HatchPattern;
|
||||
scale?: number;
|
||||
/** Drehung in Grad. */
|
||||
angle?: number;
|
||||
color?: string;
|
||||
/** Strichstärke der Musterlinien in mm Papier. */
|
||||
lineWeight?: number;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 LayerCategory erweitern
|
||||
|
||||
```ts
|
||||
export interface LayerCategory {
|
||||
code: string;
|
||||
name: string;
|
||||
visible: boolean;
|
||||
locked: boolean;
|
||||
|
||||
// ── NEU: vollständige Darstellung ──────────────────────────────────────────
|
||||
/** Stift der Ebene (PEN) — Linien aller Elemente dieser Ebene. */
|
||||
pen: LayerPen;
|
||||
/** Standard-Schraffur der Ebene (HATCH). */
|
||||
hatch: LayerHatch;
|
||||
|
||||
/** Unterkategorien (Baum). */
|
||||
children?: LayerCategory[];
|
||||
|
||||
// ── DEPRECATED (nur Übergang; siehe Migration §6) ──────────────────────────
|
||||
/** @deprecated → pen.override.color. */
|
||||
color?: string;
|
||||
/** @deprecated → pen.override.lw. */
|
||||
lw?: number;
|
||||
}
|
||||
```
|
||||
|
||||
`color` und `lw` bleiben als optionale, deprecatete Felder bestehen, bis alle
|
||||
Lesepfade auf den Resolver (§4) umgestellt sind, und werden dann entfernt. Die
|
||||
Panel-Swatch (`LayersPanel`) liest künftig die **aufgelöste** Stift-Farbe.
|
||||
|
||||
---
|
||||
|
||||
## 4. Resolver — vom Modell zur Render-Entscheidung
|
||||
|
||||
Der Resolver löst PEN/HATCH einer Ebene gegen die Ressourcen-Bibliotheken auf und
|
||||
wendet die Overrides an. Er ist die **einzige** Stelle, an der „Stil + Override"
|
||||
zusammenfließen; Generator und Panel rufen nur ihn.
|
||||
|
||||
### 4.1 Aufgelöste Render-Typen
|
||||
|
||||
`HatchRender` existiert bereits in `generatePlan.ts`. Wir ergänzen ein paralleles
|
||||
`PenRender` und exportieren beide Resolver aus einem neuen Modul
|
||||
`src/model/layerStyle.ts` (damit Panel und Generator teilen).
|
||||
|
||||
```ts
|
||||
/** Aufgelöster Stift einer Ebene — alles, was die Linie zu zeichnen braucht. */
|
||||
export interface PenRender {
|
||||
color: string;
|
||||
/** Strichstärke in mm Papier. */
|
||||
lw: number;
|
||||
/** Strichmuster in mm Papier; null = durchgezogen. */
|
||||
dash: number[] | null;
|
||||
}
|
||||
|
||||
// HatchRender: bereits in generatePlan.ts definiert (pattern, scale, angle,
|
||||
// color, lineWeight, dash). Wird nach layerStyle.ts gezogen und re-exportiert.
|
||||
```
|
||||
|
||||
### 4.2 Resolver-Funktionen (Pseudocode)
|
||||
|
||||
```ts
|
||||
function resolvePen(project: Project, layer: LayerCategory): PenRender {
|
||||
const ls = getLineStyle(project, layer.pen.lineStyleId); // wirft, falls fehlend
|
||||
const o = layer.pen.override ?? {};
|
||||
const kind = o.kind ?? ls.kind;
|
||||
return {
|
||||
color: o.color ?? ls.color,
|
||||
lw: o.lw ?? ls.weight,
|
||||
// Override-kind setzt das dash neu; sonst Stil-dash bzw. aus kind abgeleitet.
|
||||
dash: o.kind ? LINE_DASH[o.kind] : (ls.dash ?? LINE_DASH[ls.kind]),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveLayerHatch(project: Project, layer: LayerCategory): HatchRender | null {
|
||||
if (!layer.hatch.enabled) return null; // Ebene ohne Default-Schraffur
|
||||
const h = getHatch(project, layer.hatch.hatchId); // wirft, falls fehlend
|
||||
const o = layer.hatch.override ?? {};
|
||||
// Musterlinien-Stärke: Override > LineStyle der Schraffur > Default 0.13 mm.
|
||||
const baseLs = h.lineStyleId ? getLineStyle(project, h.lineStyleId) : null;
|
||||
return {
|
||||
pattern: o.pattern ?? h.pattern,
|
||||
scale: o.scale ?? h.scale,
|
||||
angle: o.angle ?? h.angle,
|
||||
color: o.color ?? h.color,
|
||||
lineWeight: o.lineWeight ?? baseLs?.weight ?? 0.13,
|
||||
dash: baseLs?.dash ?? null,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Beide bauen eine `Map<code, …>` über den ganzen Baum, analog zur heutigen
|
||||
`categoryLwMap`:
|
||||
|
||||
```ts
|
||||
export function penMap(project: Project): Map<string, PenRender> {
|
||||
const m = new Map<string, PenRender>();
|
||||
for (const c of flattenCategories(project.layers)) m.set(c.code, resolvePen(project, c));
|
||||
return m;
|
||||
}
|
||||
export function layerHatchMap(project: Project): Map<string, HatchRender | null> {
|
||||
const m = new Map<string, HatchRender | null>();
|
||||
for (const c of flattenCategories(project.layers))
|
||||
m.set(c.code, resolveLayerHatch(project, c));
|
||||
return m;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Einfluss auf `generatePlan`
|
||||
|
||||
Heute (generatePlan.ts):
|
||||
|
||||
- `categoryLwMap(project.layers)` liefert nur `lw` je Code; die Umriss-Strichstärke
|
||||
kommt daraus, **Farbe** der Umrisse ist fest `POCHE_STROKE`.
|
||||
- Schraffur kommt ausschließlich aus dem `Component` der jeweiligen Schicht
|
||||
(`resolveHatch(project, comp.hatchId)`); die Ebenen-`hatch` wird **nicht** genutzt.
|
||||
|
||||
Änderungen (minimal-invasiv, additiv):
|
||||
|
||||
### 5.1 Pens ersetzen `lwByCode`
|
||||
|
||||
```ts
|
||||
const pens = penMap(project); // statt categoryLwMap
|
||||
const layerHatches = layerHatchMap(project);
|
||||
…
|
||||
const pen = pens.get(wall.categoryCode) ?? FALLBACK_PEN; // {color, lw, dash}
|
||||
```
|
||||
|
||||
`addWallPoche` und `addDoorSymbol` bekommen statt `wallLwMm: number` /
|
||||
`doorLwMm: number` jeweils das ganze `pen: PenRender`:
|
||||
|
||||
- **Wand-Umrisslinie:** `stroke: pen.color` (statt fix `POCHE_STROKE`),
|
||||
`strokeWidthMm: pen.lw * OUTLINE_DETAIL_FACTOR[detail]`, `dash: pen.dash`.
|
||||
→ Das `Primitive` „polygon" braucht ein optionales `dash?: number[] | null`
|
||||
(Schichtfugen bleiben durchgezogen; nur die Umriss-Kontur nutzt `pen.dash`).
|
||||
- **Schichtfugen:** behalten `POCHE_STROKE` und ihre dünne `LAYER_LINE_MM`
|
||||
(interne Hilfslinien sind bewusst neutral, nicht stift-gefärbt).
|
||||
- **Tür-Symbol / Referenzlinie:** `cls` bleibt, aber `weightMm` aus `pen.lw`,
|
||||
und die PlanView darf die Stift-Farbe nutzen (`door-leaf` etc. erhalten optional
|
||||
ein `stroke`-Feld am line/arc-Primitive; ansonsten greift die CSS-Klasse wie
|
||||
bisher).
|
||||
|
||||
### 5.2 Default-Schraffur der Ebene
|
||||
|
||||
Die Ebenen-Schraffur greift dort, wo **keine Component-Schraffur** vorliegt:
|
||||
|
||||
- **`detail === "grob"`** (eine Sammelfläche, heute `NO_HATCH`): statt `NO_HATCH`
|
||||
nun `layerHatches.get(wall.categoryCode) ?? NO_HATCH`. So bekommt die grobe
|
||||
Poché die Standard-Schraffur der Ebene (z. B. ein leichtes Diagonalmuster),
|
||||
falls die Ebene eine definiert; sonst bleibt sie ungeschraffiert.
|
||||
- **mittel/fein, mehrschichtig:** unverändert — die Component-Schraffur je Schicht
|
||||
hat Vorrang (spezifischer als die Ebene). Die Ebenen-Schraffur ist der
|
||||
*Fallback*, nicht der Default-Override.
|
||||
- **Reine 2D-Zeichnungsobjekte** (künftige `drawing`-Ebenen-Elemente ohne
|
||||
Component): nutzen direkt `resolveLayerHatch` als ihre Füllschraffur.
|
||||
|
||||
Auflöse-Reihenfolge der Schraffur einer gezeichneten Fläche:
|
||||
|
||||
```
|
||||
Component.hatch > LayerCategory.hatch (enabled) > keine Schraffur
|
||||
```
|
||||
|
||||
### 5.3 Geänderte Signaturen (Zusammenfassung)
|
||||
|
||||
```ts
|
||||
// vorher: addWallPoche(out, project, wall, doors, cuts, greyed, detail, wallLwMm)
|
||||
function addWallPoche(out, project, wall, doors, cuts, greyed, detail,
|
||||
pen: PenRender, layerHatch: HatchRender | null): void
|
||||
|
||||
// vorher: addDoorSymbol(out, wall, door, greyed, detail, doorLwMm)
|
||||
function addDoorSymbol(out, wall, door, greyed, detail, pen: PenRender): void
|
||||
```
|
||||
|
||||
`Primitive` (polygon) erhält optional `dash?: number[] | null`; line/arc erhalten
|
||||
optional `stroke?: string`, damit Pen-Farbe durchschlagen kann (CSS-Klasse bleibt
|
||||
Default).
|
||||
|
||||
---
|
||||
|
||||
## 6. Editor „Ebeneneinstellungen…"
|
||||
|
||||
Geöffnet wie heute über `layerMenuItems → openLayerEditor(code)` →
|
||||
`setEditor({ kind: "layer", code, x, y })`. Der bestehende `InlineEditor`-Rahmen
|
||||
(dunkel, am Anker, Esc/Außenklick schließt) und die `EditorField`-Zeilen bleiben;
|
||||
der Inhalt wächst von 3 Feldern auf zwei kompakte Abschnitte **PEN** und **HATCH**.
|
||||
|
||||
Da der Editor jetzt mehr Felder trägt, wird er als **kompakte Sektions-Form**
|
||||
gestaltet (zwei Gruppen mit Trenn-Überschrift), gemäß CONVENTIONS.md UI-Konventionen
|
||||
(saubere Form, keine wiederholten Beschriftungen, DOSSIER-Stil, alles via `t()`).
|
||||
|
||||
### 6.1 Aufbau
|
||||
|
||||
```
|
||||
┌ Ebene 20 ───────────────── ×
|
||||
│ Name [ Wände ]
|
||||
│
|
||||
│ ── Stift (PEN) ──────────────
|
||||
│ Linienstil [ Wand stark ▾ ] ← Dropdown über project.lineStyles
|
||||
│ Strichart [ durchgezogen ▾ ] ← override.kind (leer = "vom Stil")
|
||||
│ Farbe [■] [↺] ← override.color; ↺ = Override entfernen
|
||||
│ Stärke [ 0.35 ] mm [↺] ← override.lw
|
||||
│
|
||||
│ ── Schraffur (HATCH) ────────
|
||||
│ [✓] aktiv
|
||||
│ Schraffur [ Beton ▾ ] ← Dropdown über project.hatches
|
||||
│ Muster [ vom Stil ▾ ] ← override.pattern
|
||||
│ Maßstab [ 1.00 ] [↺]
|
||||
│ Drehung [ 45 ] ° [↺]
|
||||
│ Farbe [■] [↺]
|
||||
│ Linienst. [ 0.13 ] mm [↺]
|
||||
└──────────────────────────────
|
||||
```
|
||||
|
||||
- **Override-Semantik im UI:** Jedes Override-Feld zeigt entweder „vom Stil"
|
||||
(Override leer → Platzhalter mit dem aufgelösten Stil-Wert als Hint) oder einen
|
||||
konkreten Wert. Ein kleiner **Reset-Knopf `↺`** je Override-Feld löscht das
|
||||
Override (setzt es zurück auf `undefined` → Feld folgt wieder dem Stil).
|
||||
- **Live, kein Bestätigen:** wie der heutige Editor — jede Änderung ruft sofort
|
||||
`patchCategory(code, patch)`.
|
||||
- **i18n:** alle Labels über `t()`. Neue Keys (Beispiele):
|
||||
`editor.pen`, `editor.lineStyle`, `editor.lineKind`, `editor.color`,
|
||||
`editor.lineWeight`, `editor.hatch`, `editor.hatchEnabled`, `editor.pattern`,
|
||||
`editor.scale`, `editor.rotation`, `editor.fromStyle`, `editor.resetOverride`.
|
||||
Strichart-/Muster-Werte: `lineKind.solid`, `lineKind.dashed`, …,
|
||||
`hatchPattern.solid`, `hatchPattern.diagonal`, … . Menü-Label bleibt
|
||||
`ctx.layerSettings`.
|
||||
|
||||
### 6.2 Patch-Helfer
|
||||
|
||||
`patchCategory(code, patch: Partial<LayerCategory>)` bleibt die Schnittstelle.
|
||||
Für die verschachtelten Overrides nutzt der Editor schmale Helfer (im App-Scope),
|
||||
die sparse mergen und leere Overrides auf `undefined` kollabieren:
|
||||
|
||||
```ts
|
||||
function setPenOverride(cat: LayerCategory, patch: Partial<LayerPen["override"]>) {
|
||||
const next = pruneEmpty({ ...cat.pen.override, ...patch });
|
||||
patchCategory(cat.code, { pen: { ...cat.pen, override: next } });
|
||||
}
|
||||
function setHatchOverride(cat, patch) { /* analog für cat.hatch.override */ }
|
||||
// pruneEmpty: entfernt undefined-Felder; gibt undefined zurück, wenn leer.
|
||||
```
|
||||
|
||||
`setLineStyleId` / `setHatchId` setzen nur die Referenz; `hatch.enabled` ist ein
|
||||
Checkbox-Patch.
|
||||
|
||||
### 6.3 „Eigenschaften kopieren / einfügen"
|
||||
|
||||
Der bestehende `layerClipboard` (heute `{ color, lw }`) wird auf die volle
|
||||
Darstellung erweitert: `{ pen, hatch }` (die Override-tragenden Strukturen, ohne
|
||||
`code/name/visible/locked`). „Kopieren" liest `{ pen, hatch }` der Quelle,
|
||||
„Einfügen" patcht sie auf das Ziel. So überträgt sich der komplette Stift +
|
||||
Schraffur einer Ebene auf eine andere.
|
||||
|
||||
---
|
||||
|
||||
## 7. Migration bestehender Beispieldaten
|
||||
|
||||
Bestehende Projekte/Sample-Daten haben `LayerCategory { color, lw, hatch?: string }`
|
||||
und `LineStyle { weight, color, dash }` (ohne `kind`). Eine reine Lese-Zeit-
|
||||
Migration (`migrateProject(project)`), idempotent, beim Laden:
|
||||
|
||||
1. **LineStyle.kind ableiten** — aus `dash`:
|
||||
```
|
||||
dash == null || dash.length === 0 → "solid"
|
||||
sonst, wenn min(dash) sehr klein → "dotted" (heuristisch)
|
||||
sonst → "dashed"
|
||||
```
|
||||
(Eine genaue Zuordnung ist nicht nötig; `dash` bleibt führend, `kind` ist nur
|
||||
für das Dropdown.)
|
||||
|
||||
2. **Neutralen Basis-Linienstil sicherstellen** — falls die Bibliothek noch keinen
|
||||
generischen „Standard"-Stift hat, einen `lineStyle` mit
|
||||
`{ id: "ls-default", name: "Standard", kind: "solid", weight: <Ebenen-lw>, color: "#000", dash: null }`
|
||||
anlegen. (Pro Ebene wird der Stift referenziert; die Ebenen-spezifischen
|
||||
`color`/`lw` wandern in das **Override**, nicht in den Stil — so bleibt der
|
||||
Stil wiederverwendbar.)
|
||||
|
||||
3. **Pro LayerCategory `pen` bauen:**
|
||||
```ts
|
||||
pen = {
|
||||
lineStyleId: "ls-default",
|
||||
override: pruneEmpty({ color: cat.color, lw: cat.lw }),
|
||||
}
|
||||
```
|
||||
Damit ist die Darstellung **pixelgenau wie vorher** (gleiche Farbe, gleiche lw),
|
||||
nur jetzt über die Resolver-Kette.
|
||||
|
||||
4. **Pro LayerCategory `hatch` bauen** — aus dem alten `hatch?: string`:
|
||||
- War `hatch` ein gültiger `HatchStyle.id` → `{ enabled: true, hatchId: hatch }`.
|
||||
- War es ein Pattern-Name oder leer/unbekannt → `{ enabled: false, hatchId:
|
||||
<erste Hatch-id der Bibliothek> }` (Referenz muss existieren, aber inaktiv).
|
||||
So entsteht **keine** unbeabsichtigte Schraffur (Default heute: keine).
|
||||
|
||||
5. **Deprecated-Felder belassen** für eine Übergangsphase; nach Umstellung aller
|
||||
Lesepfade (`generatePlan`, `LayersPanel`-Swatch, Clipboard) in einem zweiten
|
||||
Schritt `color`/`lw` aus `LayerCategory` und der alte `hatch: string` entfernen.
|
||||
|
||||
Migration ist **idempotent**: Liegt `pen`/`hatch` bereits vor, wird die Ebene
|
||||
unverändert durchgereicht.
|
||||
|
||||
---
|
||||
|
||||
## 8. Build-Plan (phasiert)
|
||||
|
||||
**Phase 1 — Datenmodell & Resolver (keine UI-Sichtbarkeit).**
|
||||
- `LineKind` + `LINE_DASH`, `LineStyle.kind`, `LayerPen`, `LayerHatch`,
|
||||
`LayerCategory.pen/hatch` in `types.ts`.
|
||||
- `src/model/layerStyle.ts`: `PenRender`, `resolvePen`, `resolveLayerHatch`,
|
||||
`penMap`, `layerHatchMap`; `HatchRender` hierher ziehen + re-exportieren.
|
||||
- `migrateProject()` (Schritte §7) + Aufruf beim Laden/Seed.
|
||||
- `npx tsc -b` grün.
|
||||
|
||||
**Phase 2 — Generator umstellen.**
|
||||
- `generatePlan` nutzt `penMap`/`layerHatchMap` statt `categoryLwMap`.
|
||||
- `Primitive`-polygon `dash?`, line/arc `stroke?` ergänzen; `addWallPoche`/
|
||||
`addDoorSymbol`-Signaturen auf `PenRender` + `HatchRender|null`.
|
||||
- Ebenen-Default-Schraffur in „grob" und für Schicht-lose Flächen verdrahten.
|
||||
- Visuell prüfen via `node scripts/probe.mjs` (Geometrie unverändert, Farben/lw
|
||||
identisch zur Migration).
|
||||
|
||||
**Phase 3 — Panel.**
|
||||
- `LayersPanel`-Swatch liest aufgelöste Stift-Farbe (`resolvePen(...).color`).
|
||||
|
||||
**Phase 4 — Editor.**
|
||||
- `InlineEditor`-Inhalt für `kind: "layer"` auf die PEN/HATCH-Sektionen erweitern
|
||||
(§6), mit Dropdowns über `project.lineStyles` / `project.hatches`, Reset-Knöpfen,
|
||||
neuen i18n-Keys.
|
||||
- `layerClipboard` auf `{ pen, hatch }` erweitern; Kopieren/Einfügen anpassen.
|
||||
|
||||
**Phase 5 — Aufräumen.**
|
||||
- Deprecatete `color`/`lw`/`hatch: string` aus `LayerCategory` entfernen, sobald
|
||||
kein Lesepfad sie mehr nutzt; Sample-Daten direkt im neuen Format ablegen.
|
||||
|
||||
---
|
||||
|
||||
## 9. Offene Punkte / bewusst nicht jetzt
|
||||
|
||||
- **Pro-Geschoss-Overrides der Ebene** (eine Ebene anders je `DrawingLevel`):
|
||||
nicht in dieser Iteration; das Schema gilt geschossübergreifend (types.ts).
|
||||
Falls später nötig, als zweite Override-Ebene über demselben Resolver.
|
||||
- **Regelbasierte Overrides** (resources-graphics.md, `overrides.py`): orthogonal;
|
||||
würden nach der Ebenen-Auflösung greifen.
|
||||
- **Linienstil-Endkappen/Joins** und feinere Dash-Skalierung: bleiben in der
|
||||
PlanView (Darstellung), nicht im Modell.
|
||||
@@ -0,0 +1,338 @@
|
||||
# Design — Pläne & Output
|
||||
|
||||
> Teil der Standalone-Architektur — siehe [../../ARCHITECTURE.md](../../ARCHITECTURE.md).
|
||||
> Bauteile: [elements.md](elements.md). Ressourcen/Stile: [resources-graphics.md](resources-graphics.md).
|
||||
|
||||
Hier gewinnen wir (ROADMAP §3, Phase 3 ⭐): **schöne, normgerechte 2D-Pläne**,
|
||||
automatisch aus dem Modell abgeleitet, druckfertig als Vektor-PDF. Dieses Dokument
|
||||
übersetzt DOSSIERs `schnitte.py`, `massstab.py`, `ausschnitte.py`, `kamera.py`,
|
||||
`dimensionen.py`, `layouts.py` in Browser-Module. Bezeichner englisch, Prosa
|
||||
deutsch, Meter.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ansichtstypen = Kamera-Projektion + optionaler Schnitt
|
||||
|
||||
Vereinheitlichtes Modell (ROADMAP §2c, im Spike als `DrawingLevelKind` angelegt):
|
||||
|
||||
| Typ | Projektion | Schnitt | Erzeugung |
|
||||
|---|---|---|---|
|
||||
| **Grundriss** | Ortho Top | horizontal auf `okff + cutHeight` | symbolisch aus Footprint (Pfad A) |
|
||||
| **Schnitt** | Ortho Front (Richtung) | vertikale Schnittebene + Tiefe | 3D-Projektion/HLR (Pfad B) |
|
||||
| **Ansicht** | Ortho Front (Richtung) | kein Schnitt (Fassade außen) | 3D-Projektion/HLR (Pfad B) |
|
||||
| **Perspektive** | 3D perspektivisch | — | Three.js direkt |
|
||||
|
||||
```ts
|
||||
type ViewType = "plan" | "section" | "elevation" | "perspective";
|
||||
interface DerivedView { // was der Viewport gerade zeigt
|
||||
type: ViewType;
|
||||
levelId?: string; // Geschoss (plan) bzw. Schnitt/Ansicht (DrawingLevel)
|
||||
camera: CameraState;
|
||||
cut?: CutSpec; // Clipping-Spezifikation (s.u.)
|
||||
detailLevel: DetailLevel;
|
||||
}
|
||||
interface CutSpec {
|
||||
planes: { point: Vec3; normal: Vec3 }[]; // 1 (plan/elevation) oder 2 (section: cut+back)
|
||||
}
|
||||
```
|
||||
|
||||
**Zwei Wege zum Plan** (zentrale Architektur-Erkenntnis, ROADMAP §3) — wir bauen
|
||||
**beide**:
|
||||
- **A) Grundriss = symbolisch** aus den Parametern (`plan/generatePlan.ts`, im
|
||||
Spike). Schnell, exakt, vektorbasiert. Kein Mesh-Schnitt.
|
||||
- **B) Schnitt & Ansicht = 3D-Projektion mit Hidden-Line-Removal** (`plan/
|
||||
generateSection.ts`, §4). Durch das zusammengebaute Gebäude.
|
||||
|
||||
---
|
||||
|
||||
## 2. Schnitt & Ansicht — Datenmodell & Aktivierung
|
||||
|
||||
DOSSIER speichert Schnitte als Zeichnungsebenen-Eintrag (`type:"schnitt"`) mit
|
||||
`linePts/dirSign/depthBack/cutAtLine/heightMin/heightMax/projection`
|
||||
(`schnitte.create_schnitt_entry`). Im Spike sind die Felder als `DrawingLevel`
|
||||
(`kind:"section"|"elevation"`, `linePoints`, `directionSign`) angelegt — wir
|
||||
ergänzen:
|
||||
|
||||
```ts
|
||||
interface SectionLevel extends DrawingLevel { // kind: "section" | "elevation"
|
||||
linePoints: [Vec2, Vec2];
|
||||
directionSign: 1 | -1; // Blickrichtung (Pfeil im Plan)
|
||||
depthBack: number; // Tiefe hinter der Schnittlinie (default 8)
|
||||
cutAtLine: boolean; // true=Schnitt (cut+back), false=Ansicht (nur back)
|
||||
heightMin: number; heightMax: number;
|
||||
projection: "parallel" | "perspective";
|
||||
}
|
||||
```
|
||||
|
||||
**Aktivierung** (Port `schnitte.activate_schnitt`):
|
||||
1. `view_dir` = senkrecht zur Linie in XY, Richtung = `directionSign`.
|
||||
2. **3D-Vorschau:** `THREE.Plane`s setzen —
|
||||
- Cut (nur `cutAtLine`): auf der Linie, Normale `+view_dir`.
|
||||
- Back (immer): um `depthBack` in `+view_dir` versetzt, Normale `−view_dir`.
|
||||
- via `renderer.localClippingEnabled = true`, `material.clippingPlanes`.
|
||||
3. **Kamera:** `OrthographicCamera`, Position `mid − view_dir·dist`, Target `mid`,
|
||||
Up `+Z`; Zoom auf BBox (`linePoints` + Höhenbereich + `depthBack`). Bei
|
||||
`perspective`: `PerspectiveCamera` + FOV.
|
||||
4. **Vektor-Ergebnis:** HLR (§4).
|
||||
|
||||
**2D-Plan-Symbol** (Schnittmarke im Grundriss, Port `make_schnitt_symbol`): Linie
|
||||
+ Endpfeile in `view_dir`, Beschriftung. Bleibt im Grundriss sichtbar (liegt auf
|
||||
einer eigenen Ebene, z.B. `18 Schnittlinien`). **Doppelklick** auf das Symbol
|
||||
aktiviert den Schnitt (`onDoubleClick` auf das SVG-Symbol → `setActiveLevel(id)`,
|
||||
≙ DOSSIER `_SchnittDoubleClickHandler`).
|
||||
|
||||
**Grip-Editing der Schnittlinie:** Endpunkte als Grips im Grundriss; Ziehen
|
||||
aktualisiert `linePoints` + Symbol + (falls aktiv) Clipping — ohne Re-Zoom der
|
||||
3D-View (DOSSIER `skip_view`-Flag-Äquivalent: Drag aktualisiert nur die Clip-
|
||||
Ebenen, nicht die Kamera).
|
||||
|
||||
---
|
||||
|
||||
## 3. Massstab (Scale) — pro Viewport, Auto-DPI
|
||||
|
||||
### 3.1 Mathematik (Port `massstab._compute_scale`, identisch im Browser)
|
||||
```
|
||||
frustumWidth_world = ortho-Kamera-Breite in Modell-Einheiten (Meter)
|
||||
frustumWidth_mm = frustumWidth_world * 1000 (Meter→mm)
|
||||
screenWidth_mm = canvasWidthCssPx * 25.4 / dpi
|
||||
N (1:N) = frustumWidth_mm / screenWidth_mm
|
||||
```
|
||||
- **Nur bei Orthografie** sinnvoll; in Perspektive zeigt die UI „—" (wie DOSSIER).
|
||||
- **DPI:** Browser kennt das nativ — `dpi = 96 * window.devicePixelRatio` (CSS
|
||||
definiert 1 px = 1/96 inch). Das ersetzt DOSSIERs CoreGraphics-JXA-Detection
|
||||
komplett und ist exakter. Optional manuell kalibrierbar (Eingabe in den
|
||||
Settings), persistiert pro Projekt.
|
||||
- **Massstab setzen** (1:N → Zoom): `frustumWidth_world = screenWidth_mm · N /
|
||||
1000`; bei `THREE.OrthographicCamera` `camera.zoom = canvasWidthCssPx /
|
||||
(frustumWidth_world / metersPerPixelAtZoom1)` bzw. direkt `left/right` setzen.
|
||||
|
||||
```ts
|
||||
// plan/scale.ts
|
||||
function computeScale(view: { frustumWidthWorld; canvasCssWidthPx; dpi }): number|null // 1:N
|
||||
function applyScale(camera: THREE.OrthographicCamera, n: number, canvasCssWidthPx, dpi): void
|
||||
const SCALE_PRESETS = [1,5,10,20,25,50,100,200,500,1000]; // 1:N Dropdown
|
||||
```
|
||||
|
||||
### 3.2 Massstabs-abhängige Skalierung (DOSSIER-Stärke)
|
||||
Bei 1:N müssen **Strichstärken** und **Schraffuren** lesbar bleiben:
|
||||
- **Plotweight → SVG stroke-width:** `strokeWidthPx = lwMm / 25.4 · dpi`
|
||||
(Welt-unabhängig; die Linie ist im Plan immer z.B. 0.25 mm dick). DOSSIER
|
||||
skaliert dafür die PlotWeights (`_apply_scaled_lineweights`); im SVG-Modell
|
||||
rechnen wir die mm-Strichstärke direkt in Pixel — **viel einfacher**, da SVG
|
||||
von Natur aus papierbezogen ist.
|
||||
- **Schraffur-Skalierung:** DOSSIER nutzt `factor = sqrt(N)/10` (1:100 ⇒ 1.0,
|
||||
1:50 ⇒ 0.71, 1:500 ⇒ 2.24; `apply_scaled_hatches`). Port: SVG `<pattern>`-
|
||||
`patternTransform="scale(factor)"` bzw. `patternUnits` so wählen, dass das Muster
|
||||
die gewünschte Paper-Dichte hat. Formel 1:1 übernehmen.
|
||||
- **Linetype-Dash:** `stroke-dasharray` in mm→px, ebenfalls papierbezogen.
|
||||
|
||||
> **Kernvorteil gegenüber DOSSIER:** Weil der Plan **SVG/Paper-Space** ist,
|
||||
> entfällt das fragile Welt↔Bildschirm-Plotweight-Rescaling (DOSSIER `write_plotweight`,
|
||||
> `read_plotweight`, Print-Display-Toggle). Strichstärke und Maßlinien sind direkt
|
||||
> in mm definiert und werden 1:1 gedruckt.
|
||||
|
||||
---
|
||||
|
||||
## 4. Schnitt/Ansicht-Projektion (HLR) — Risiko #4
|
||||
|
||||
Vertikale Schnitte/Ansichten brauchen **echte 3D-Projektion mit verdeckten
|
||||
Kanten** durch das zusammengebaute Gebäude.
|
||||
|
||||
```ts
|
||||
// plan/generateSection.ts (läuft im Web Worker via Comlink)
|
||||
interface SectionRequest { meshes: SerializedBrep[]; cut: CutSpec; camera: CameraState; }
|
||||
interface SectionResult {
|
||||
cutLines: Primitive[]; // Schnittkanten (dick) — geschnittene Bauteile
|
||||
cutFaces: Primitive[]; // Schnittflächen → Component-Schraffur (Poché)
|
||||
visibleLines: Primitive[]; // sichtbare Projektion (dünn)
|
||||
hiddenLines?: Primitive[]; // verdeckte (gestrichelt, optional)
|
||||
}
|
||||
function generateSection(req: SectionRequest): SectionResult
|
||||
```
|
||||
|
||||
- **Kernel:** **OpenCascade.js** `HLRBRep_Algo` / `HLRBRep_HLRToShape` (B-Rep →
|
||||
sichtbare/verdeckte Kanten). Eingabe = die Bauteil-Breps (Wände/Decken/Treppen…),
|
||||
Projektionsrichtung aus `camera`. Alternativ Mesh-basiert (langsamer, weniger
|
||||
sauber).
|
||||
- **Schnittflächen-Schraffur (Section-Style):** wo die Cut-Plane ein Bauteil
|
||||
durchschneidet, entsteht eine Fläche → mit der Component-Schraffur füllen
|
||||
(resources-graphics.md). ≙ DOSSIER `SectionStyle` (Hatch + Schnittkante +
|
||||
Silhouette), nur dass wir es als SVG-Fill rendern statt als Rhino-Layer-Property.
|
||||
- **Performance:** schwer → **Worker + Cache**. Cache-Key =
|
||||
hash(sichtbare Element-IDs + Geometrie-Hash + CutSpec + camera). Nur neu rechnen,
|
||||
wenn sich relevante Eingaben ändern (ROADMAP Risiko #4). Geschnittene vs. dahinter
|
||||
liegende Geometrie über die Back-Plane begrenzen (`depthBack`).
|
||||
- **Stufenweise:** (a) Ansicht ohne Verdeckung (einfache Projektion) → (b) HLR
|
||||
sichtbar → (c) verdeckte Kanten gestrichelt → (d) Schnittflächen-Poché.
|
||||
|
||||
---
|
||||
|
||||
## 5. Ausschnitte (View-Snapshots)
|
||||
|
||||
Navigation über 50+ Ansichten ohne Ordner-Wildwuchs (DOSSIER `ausschnitte.py`).
|
||||
Ein Snapshot speichert **Kamera + Sichtbarkeit + Massstab + Darstellung + Overrides**.
|
||||
|
||||
```ts
|
||||
// in Project: viewSnapshots: ViewSnapshot[]
|
||||
interface ViewSnapshot {
|
||||
id; name; folder?: string;
|
||||
camera: CameraState; // pos/target/up/parallel/fov + frustumWidth (Zoom!)
|
||||
scale: number; // 1:N (DOSSIER speichert "1:50"-String)
|
||||
detailLevel: DetailLevel; // LoD-Override (DOSSIER darstellung)
|
||||
visibility: VisibilityState; // pro Geschoss + pro Ebene visible/locked
|
||||
layerCombinationId?: string; // ODER Verweis auf Layer-Kombi (live) — s.u.
|
||||
overrides?: { presetId?: string; enabled: boolean };
|
||||
}
|
||||
interface CameraState { position; target; up; parallel; fov?; frustumWidth?; }
|
||||
```
|
||||
|
||||
- **Save:** aktuellen `ui`-Zustand einfrieren (Port `_capture`: Kamera inkl.
|
||||
Frustum-Breite für exakten Zoom-Restore, Layer-Sichtbarkeit, Massstab, LoD).
|
||||
- **Restore:** Snapshot → `ui` + ggf. `project`-Sichtbarkeit anwenden (Port
|
||||
`_restore`): Kamera, Sichtbarkeit (oder referenzierte Layer-Kombi), LoD,
|
||||
optional Overrides-Preset. Da alles im Store liegt, ist das ein einfacher
|
||||
State-Set — kein Multi-Panel-Force-Send-Tanz wie in DOSSIER.
|
||||
- **Ordner, Umbenennen, Duplizieren, Settings-Drawer** wie DOSSIER (`_duplicate`,
|
||||
`_set_field`, `_open_settings_window` → React-Drawer statt Eto-Form).
|
||||
|
||||
### 5.1 Layer-Kombinationen (Presets)
|
||||
```ts
|
||||
interface LayerCombination { id; name; visibility: VisibilityState; }
|
||||
```
|
||||
Bauphasen/Varianten/MEP per Klick (DOSSIER `_save_preset`/`apply_layer_preset_by_name`).
|
||||
Snapshot kann **live** auf eine Kombi verweisen (folgt Änderungen) **oder**
|
||||
eingefroren den `visibility`-Stand halten — genau DOSSIERs Wahl (`layerCombination`
|
||||
vs. `layers`).
|
||||
|
||||
---
|
||||
|
||||
## 6. Kamera-Presets & Norden-Rotation ⭐
|
||||
|
||||
Port `kamera.py`. Schnelle Ansichtswechsel + Georeferenzierung (Swisstopo, Phase 4).
|
||||
|
||||
```ts
|
||||
// viewport/camera.ts
|
||||
function setCardinal(cam, dir: "N"|"E"|"S"|"W", northAngle: number): void
|
||||
function setIso(cam, octant: "NE"|"SE"|"SW"|"NW"|..., northAngle: number): void
|
||||
function setTop(cam, northAngle: number): void // Plan-Norden zeigt nach oben
|
||||
// northAngle = Grad im Uhrzeigersinn von +Y (DOSSIER dossier_north_angle, default 0)
|
||||
const north = (deg) => ({ x: Math.sin(rad(deg)), y: Math.cos(rad(deg)) });
|
||||
interface CameraPreset { id; name; camera: CameraState; } // benutzerdefiniert, gespeichert
|
||||
```
|
||||
- **Norden-Rotation:** alle Kardinal-/Iso-Richtungen werden um `northAngle`
|
||||
rotiert (Port `set_cardinal_view`, `_set_iso`, `set_top_view`). `northAngle`
|
||||
liegt im `Project` (georeferenziert zu swissBUILDINGS).
|
||||
- **Benutzer-Presets:** speichern/laden wie DOSSIER (`_load_presets`/`_save_presets`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Bemaßung (Dimensions)
|
||||
|
||||
Port `dimensionen.py`. Maße werden **aus dem Modell abgeleitet** (Wand-Dicken,
|
||||
Geschoss-Höhen, Öffnungen) + manuelle Maßketten.
|
||||
|
||||
```ts
|
||||
interface Dimension {
|
||||
id; floorId; categoryCode; // liegt auf einer Ebene
|
||||
kind: "linear" | "chain" | "aligned" | "level"; // Einzel|Kette|ausgerichtet|Höhenkote
|
||||
refs: DimRef[]; // Bezugspunkte (frei ODER an Element gebunden)
|
||||
offset: number; // Abstand der Maßlinie vom Objekt
|
||||
style: DimStyleId; // Pfeile, Texthöhe, Einheiten
|
||||
}
|
||||
type DimRef = { point: Vec2 } | { elementId: string; anchor: "start"|"end"|"jamb"|... };
|
||||
```
|
||||
- **Auto-Bemaßung** (Phase 3): Außenketten (Gebäude-Hülle), Achsketten (Achsraster),
|
||||
Öffnungs-Ketten — aus der Geometrie generiert, dann editierbar.
|
||||
- **9-Punkt-Objekt-Info** (DOSSIER ROADMAP §11): Bounding-Box-Maße lesen +
|
||||
Element via Greifen verschieben/skalieren/rotieren — direkt im Plan.
|
||||
- **Rich-Text-Indizes** (Bold/Hoch-/Tiefstellung) für Maßzahlen — als SVG
|
||||
`<tspan>` mit `baseline-shift` (resources-graphics.md §Rich-Text).
|
||||
- **Massstabsbezug:** Texthöhe/Pfeilgröße in **Paper-mm**, rendern × Massstab —
|
||||
konsistent mit §3.2.
|
||||
|
||||
---
|
||||
|
||||
## 8. Plansätze (Sheets) & PDF-Export
|
||||
|
||||
DOSSIER nutzt Rhinos `RhinoPageView` + `Detail`-Viewports + `FilePdf`
|
||||
(`layouts.py`). Browser-Äquivalent: eigenes Sheet-Modell + SVG → PDF.
|
||||
|
||||
### 8.1 Datenmodell
|
||||
```ts
|
||||
interface Sheet {
|
||||
id; name; folder?;
|
||||
paper: "A0"|"A1"|"A2"|"A3"|"A4"|"Letter"; landscape: boolean;
|
||||
viewports: SheetViewport[];
|
||||
titleBlock?: TitleBlock; // Titelblock (Projekt/Plan/Massstab/Datum)
|
||||
}
|
||||
interface SheetViewport { // ≙ DOSSIER Detail + gebundener Ausschnitt
|
||||
id; rect: { x; y; w; h }; // Position auf dem Blatt (mm)
|
||||
source: { kind: "level"; levelId } | { kind: "snapshot"; snapshotId };
|
||||
scale: number; // 1:N
|
||||
clipToRect: boolean;
|
||||
}
|
||||
const PAPER_MM = { A0:[841,1189], A1:[594,841], A2:[420,594], A3:[297,420],
|
||||
A4:[210,297], Letter:[216,279] }; // Port PAPER_SIZES_MM
|
||||
```
|
||||
|
||||
### 8.2 Sheet-Editor
|
||||
`sheets/SheetEditor.tsx`: Blatt als SVG in mm, Viewports per Drag platzieren/
|
||||
skalieren, Quelle (Geschoss/Snapshot) + Massstab zuweisen. Ein Viewport rendert
|
||||
den abgeleiteten Plan/Schnitt **bei seinem Massstab** in sein `rect` (≙ DOSSIER
|
||||
`apply_snapshot_to_detail`). Bei Änderung der Quelle re-derivieren (live), kein
|
||||
manuelles Re-Sync nötig (DOSSIER war Snapshot-Mode).
|
||||
|
||||
### 8.3 Detail↔Ausschnitt-Bindung
|
||||
`SheetViewport.source.snapshotId` ist die Bindung (DOSSIER `_BIND_KEY`). „Alle
|
||||
aktualisieren" = alle Viewports neu rendern; weil rein abgeleitet, ist das
|
||||
automatisch. Umbenennen synchronisiert Titelblock + Schnitt-Symbol (DOSSIER
|
||||
Detail↔Ausschnitt-Sync).
|
||||
|
||||
### 8.4 PDF-Export (Vektor, Multi-Page, @DPI)
|
||||
Port `layouts._export_pdf`, aber **vektorbasiert** (DOSSIER rasterte via
|
||||
`ViewCaptureToFile` @DPI — wir bleiben Vektor → schärfer, kleiner):
|
||||
|
||||
```ts
|
||||
// sheets/exportPdf.ts
|
||||
async function exportSheetsPdf(sheets: Sheet[], opts: { vector: boolean }): Promise<Blob>
|
||||
```
|
||||
- **Vektor-Pfad (bevorzugt):** jeder Sheet-Viewport rendert seinen Plan als SVG;
|
||||
SVG → PDF via **`svg2pdf.js` + `jsPDF`** (oder `pdf-lib` mit eigenem Pfad-
|
||||
Emit). Eine PDF-Seite pro Sheet, Größe = `PAPER_MM`. Strichstärken/Schraffuren
|
||||
sind bereits in mm (§3.2) → 1:1 druckbar.
|
||||
- **Raster-Fallback** (Perspektiven/3D-Inhalte): Three.js `renderer` → Canvas →
|
||||
PNG @DPI → in PDF-Seite (`px = mm/25.4·dpi`, Port der DOSSIER-Pixelrechnung).
|
||||
- **Speichern:** Blob → File System Access API (`showSaveFilePicker`) / Download.
|
||||
|
||||
---
|
||||
|
||||
## 9. Primitive & SVG-Serializer (gemeinsame Basis)
|
||||
|
||||
Alle Pläne (Grundriss, Schnitt, Ansicht, Sheet-Viewport) sprechen dieselbe
|
||||
`Primitive`-Sprache (heute in `generatePlan.ts`), erweitert um Schraffur/Text:
|
||||
|
||||
```ts
|
||||
type Primitive =
|
||||
| { kind:"polygon"; pts:Vec2[]; fill:string; stroke:string; strokeWidthMm:number; hatchId?:string }
|
||||
| { kind:"line"; a:Vec2; b:Vec2; styleId:string } // styleId → LineStyle (mm, dash)
|
||||
| { kind:"arc"; center:Vec2; from:Vec2; to:Vec2; r:number; styleId:string }
|
||||
| { kind:"text"; at:Vec2; text:string; heightMm:number; align; font; rich?:RichRun[] }
|
||||
| { kind:"symbol"; at:Vec2; symbolId:string; scale:number; angle:number }; // Symbol-Bibliothek
|
||||
interface Plan { primitives: Primitive[]; bounds: Rect; }
|
||||
```
|
||||
- **SVG-Serializer** (`plan/primitives.ts`): Primitive → SVG-Elemente.
|
||||
`strokeWidthMm` → px via `mm·dpi/25.4`; `hatchId` → `<pattern>`-Referenz;
|
||||
`styleId` → `stroke`/`stroke-dasharray`. Derselbe Serializer für Bildschirm
|
||||
*und* PDF.
|
||||
- **DXF-Export** (Phase 4): dieselben Primitive → DXF-Entities (`dxf`-Writer-lib).
|
||||
|
||||
---
|
||||
|
||||
## 10. Umsetzungs-Reihenfolge (verweist auf ROADMAP-Phasen)
|
||||
|
||||
1. **Phase 1 (MVP):** Grundriss-Generator ✅ ausbauen (Schraffuren, LoD), Live-
|
||||
Grundriss neben 3D, Basis-Bemaßung; Massstab pro Viewport (§3).
|
||||
2. **Phase 3 ⭐:** Schnitt/Ansicht via HLR (§4, Worker), Auto-Bemaßung (§7),
|
||||
Ausschnitte + Layer-Kombinationen (§5), Kamera-Presets + Norden (§6),
|
||||
Sheets + Vektor-PDF (§8).
|
||||
3. **Phase 4:** DXF-Export (§9), Detail↔Ausschnitt-Sync-Politur.
|
||||
@@ -0,0 +1,293 @@
|
||||
# Design — Ressourcen & Grafik
|
||||
|
||||
> Teil der Standalone-Architektur — siehe [../../ARCHITECTURE.md](../../ARCHITECTURE.md).
|
||||
> Bauteile: [elements.md](elements.md). Output/Pläne: [plans-output.md](plans-output.md).
|
||||
|
||||
Die **Stil-Schicht**: verwaltete Ressourcen-Bibliotheken (Vectorworks-Stil),
|
||||
regelbasierte Overrides, Symbol-/Text-Bibliotheken und Detailgrad-Steuerung. Sie
|
||||
wird **beim Rendern angewandt, nie in die Geometrie eingebacken** (ROADMAP §2b).
|
||||
Dieses Dokument übersetzt DOSSIERs `styles.py`/`gestaltung.py`, `mass_style.py`,
|
||||
`overrides.py`, `library.py`, `text_create.py`. Bezeichner englisch, Prosa
|
||||
deutsch.
|
||||
|
||||
---
|
||||
|
||||
## 1. Resource Manager — verwaltete Bibliotheken
|
||||
|
||||
Drei Bibliotheken im `Project.resources`-Block; **alles verweist per id**, zentral
|
||||
änderbar (ROADMAP §2d). Verweis-Kette: 2D-Objekte/Ebenen → LineStyle/Hatch;
|
||||
Hatch → LineStyle; Component → Hatch (+3D-Material).
|
||||
|
||||
```ts
|
||||
interface Resources {
|
||||
lineStyles: LineStyle[];
|
||||
hatches: Hatch[];
|
||||
components: Component[];
|
||||
}
|
||||
|
||||
interface LineStyle { // Line Manager
|
||||
id; name;
|
||||
weight: number; // Strichstärke in mm (≙ Rhino PlotWeight)
|
||||
color: string; // hex
|
||||
dash: number[]; // Strichmuster in mm ([] = durchgezogen)
|
||||
}
|
||||
|
||||
interface Hatch { // Hatch Manager
|
||||
id; name;
|
||||
pattern: PatternId; // "solid" | "diagonal" | "insulation" | "concrete" | ...
|
||||
scale: number; // Grundmaßstab des Musters
|
||||
angle: number; // Grad
|
||||
lineStyleId: string; // Linien der Schraffur → Line Manager
|
||||
}
|
||||
|
||||
interface Component { // Component Manager (= DOSSIER-Material, erweitert)
|
||||
id; name;
|
||||
hatchId: string; // Schnitt-Schraffur → Hatch Manager
|
||||
color3d: string; // 3D-Diffusfarbe
|
||||
texture3d?: TextureRef; // optionale PBR-Textur (Phase 3)
|
||||
pbr?: { roughness; metalness; opacity; ior }; // Material-Bibliothek (ROADMAP §11)
|
||||
joinPriority: number; // Verschneidungs-Rang (DOSSIER _MATERIAL_PRIO als Daten)
|
||||
}
|
||||
```
|
||||
|
||||
**Migration vom heutigen Stand:** Der Spike hat `Material { color, planFill, hatch }`
|
||||
und `Layer { materialId, thickness, priority }`. Ziel: `Material → Component`
|
||||
(`color→color3d`, `planFill→` Fill aus `hatch.pattern==solid`+Farbe, `hatch`-Enum
|
||||
→ `hatchId`), `Layer.priority → Component.joinPriority` (Priorität wandert vom
|
||||
Layer zum Component, damit man sie nur einmal pflegt — siehe elements.md §1.3).
|
||||
|
||||
### 1.1 Manager-UI
|
||||
`managers/ComponentManager.tsx`, `HatchManager.tsx`, `LineManager.tsx` — je eine
|
||||
Liste mit CRUD + Vorschau (Three-Sphere für Component-3D, SVG-Swatch für
|
||||
Hatch/Line). **Seeds** beim ersten Projekt (DOSSIER-Defaults):
|
||||
- LineStyles: 0.13 / 0.18 / 0.25 / 0.35 / 0.50 mm (aus `DEFAULT_LAYER_SCHEMA`-lw).
|
||||
- Hatches: `solid`, `diagonal`, `concrete`, `insulation` (Dämmung).
|
||||
- Components: Stahlbeton (prio 800), Beton (800), Mauerwerk (600), Ziegel (550),
|
||||
Holzständer (400), Dämmung (200), Putz (100) — exakt DOSSIER `_MATERIAL_PRIO`
|
||||
(elements.md §1.3). Plus Glas (transparent), Holz-Türblatt (DOSSIER
|
||||
`_OEFF_PIECE_DEFS`).
|
||||
|
||||
### 1.2 Render-Anwendung
|
||||
- **3D:** Component → `MeshStandardMaterial` (`color3d`/`pbr`), pro `componentId`
|
||||
gecacht (`viewport/scene.ts`).
|
||||
- **Plan/Schnitt:** geschnittene Schicht → Polygon mit `fill` (Component-Farbe) +
|
||||
`<pattern>` aus `hatchId`. Pattern als SVG `<pattern>` mit `patternTransform`
|
||||
für Massstab (plans-output.md §3.2). Der Hatch nutzt seinen `lineStyleId` für
|
||||
die Musterlinien.
|
||||
|
||||
---
|
||||
|
||||
## 2. Mehrschichtige Aufbauten ↔ Ressourcen
|
||||
`WallType.layers[].componentId` / `SlabType.layers[].componentId` verweisen auf
|
||||
Components. 3D und Plan lesen dieselben Schichten (elements.md §1). Die
|
||||
**Prioritäts-Verschneidung** (Risiko #1) liest `Component.joinPriority`:
|
||||
höhere Priorität läuft am Stoß durch (Backbone), niedrigere stößt seitlich an —
|
||||
Algorithmus in elements.md §1.3 (Port DOSSIER `_t_junction_layer_overrides`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Stile & Element-Override (Selektions-Attribute)
|
||||
|
||||
DOSSIERs GESTALTUNG-Panel (`styles.py`) setzt Farbe/Lineweight/Linetype/Hatch auf
|
||||
die *Selektion*. Browser-Äquivalent — zwei Ebenen, in Render-Reihenfolge:
|
||||
|
||||
```
|
||||
ByLayer (Ebenen-Default) → Element-Style (styleId) → Override-Regeln → gerendert
|
||||
```
|
||||
|
||||
```ts
|
||||
// Effektiver Stil eines Elements (resolve beim Rendern, nie persistiert)
|
||||
interface EffectiveStyle { color; lineStyleId; hatchId?; }
|
||||
function resolveStyle(project, el, doc): EffectiveStyle {
|
||||
// 1) Default aus LayerCategory(categoryCode)
|
||||
// 2) überschrieben durch el.styleId (Element-Override, optional)
|
||||
// 3) überschrieben durch passende Override-Regeln (§4)
|
||||
}
|
||||
```
|
||||
|
||||
- **Wall-/Opening-Stil-Kataloge** (Presets, ROADMAP §11): benannte Sätze von
|
||||
Default-Werten (Wandtyp + Farbe + lw; Öffnung mit Rahmen/Sims/…). 1-Klick-
|
||||
Anwendung, globaler Stilwechsel. Speicherung: pro Projekt + cross-Projekt
|
||||
(LocalStorage), Seed wie DOSSIER `_OEFF_DEFAULT_STYLES` (elements.md §2.1).
|
||||
- **LoD-bewusste Stil-UI** (DOSSIER ROADMAP §11): das Stil-Panel zeigt nur
|
||||
passende Controls je Geometrietyp (keine Füll-Optionen bei einer 3D-/Linien-
|
||||
Auswahl). `panels/StylePanel.tsx` schaltet Felder nach `selection`-Typ.
|
||||
- **Pipette:** Stil/Typ von einem Element auf ein anderes übernehmen (DOSSIER
|
||||
`cmd/pipette`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Regelbasierte Overrides (Engine)
|
||||
|
||||
Port `overrides.py` (ArchiCAD Graphical Overrides / Vectorworks
|
||||
Datenvisualisierung). Im Browser **viel einfacher**, weil Overrides reine
|
||||
**Render-Transformationen** sind — kein UserString-Backup/Restore nötig (DOSSIER
|
||||
musste Originalwerte sichern, weil es echte Rhino-Objekte mutierte; wir mutieren
|
||||
nichts).
|
||||
|
||||
```ts
|
||||
interface OverrideConfig { enabled: boolean; rules: OverrideRule[]; activePresetId?: string; }
|
||||
interface OverrideRule {
|
||||
id; name; enabled: boolean;
|
||||
conditions: Condition[]; conditionsLogic: "and" | "or";
|
||||
actions: { color?: string; lineWeight?: number; lineStyleId?: string;
|
||||
hatchId?: string; hatchScale?: number };
|
||||
}
|
||||
interface Condition {
|
||||
type: "category" | "userField" | "name" | "elementType"; // ≙ layer_name/user_string/object_name
|
||||
operator: "equals"|"notEquals"|"contains"|"startsWith"|"endsWith";
|
||||
value: string;
|
||||
key?: string; // nur für userField (z.B. "sia")
|
||||
}
|
||||
```
|
||||
|
||||
**Auswertung** (Port `_compose_overrides`):
|
||||
```ts
|
||||
function composeOverrides(el, project, cfg): Partial<Actions> {
|
||||
// additive: Actions aller matchenden, aktiven Regeln kombinieren;
|
||||
// bei Konflikt für dieselbe Property gewinnt die Regel WEITER OBEN (kleinerer Index).
|
||||
}
|
||||
```
|
||||
- **Anwendung:** `resolveStyle` (§3) ruft `composeOverrides` — Override liegt
|
||||
über Element-Style. Reines Read beim Rendern → kein `apply_all`/`restore_all`,
|
||||
kein Backup, **keine reversibilität nötig**. Toggle `enabled` rendert neu.
|
||||
- **Live:** Da abgeleitet, schlägt jede Modell-/Regel-Änderung sofort durch (kein
|
||||
`install_listeners`/`AddRhinoObject`-Hook wie DOSSIER).
|
||||
- **Presets & Templates** (cross-Projekt): Preset = Satz Regeln, Template =
|
||||
einzelne Regel; LocalStorage statt `~/Library/.../override_presets.json`
|
||||
(`save_preset`/`load_preset`/`list_rule_templates` → `resources/overridePresets.ts`).
|
||||
- **SIA-416-Preset** (elements.md §7): vier Regeln `userField sia == HNF|NNF|VF|FF`
|
||||
→ Farbe + Solid-Hatch, Port `_build_sia_preset_rules`. Aktivieren = Preset
|
||||
`activePresetId` setzen.
|
||||
|
||||
```ts
|
||||
// resources/overrides.ts
|
||||
function composeOverrides(el, project, cfg): Partial<OverrideAction>
|
||||
function setActivePreset(project, presetId): Project // immutabel
|
||||
const PRESETS_NS = "cad.presets.overrides"; // LocalStorage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Symbol-Bibliothek
|
||||
|
||||
Port `library.py` + `cmd/symbol`. Wiederverwendbare 2D-Symbole (Möbel, Sanitär,
|
||||
Bäume, Nordpfeil, Pflanzen) für den Plan.
|
||||
|
||||
```ts
|
||||
interface Symbol {
|
||||
id; name; category: string; // "furniture" | "sanitary" | "vegetation" | "annotation"
|
||||
svgPath: string; // Pfad-/Gruppen-Markup im Symbol-Koordinatensystem (m)
|
||||
defaultScale: number;
|
||||
}
|
||||
interface SymbolInstance extends ElementBase { // type:"draw2d", subtype:"symbol"
|
||||
symbolId: string; at: Vec2; scale: number; angle: number;
|
||||
}
|
||||
```
|
||||
- **Speicherung:** mitgelieferte Symbole als statische Assets (SVG); Nutzer-
|
||||
Symbole im Projekt + cross-Projekt (LocalStorage). DOSSIER nutzt Block-
|
||||
Definitionen; bei uns SVG-Definition + Instanz-Transform (`<use>`-artig).
|
||||
- **Picker:** `panels/SymbolPicker.tsx` (≙ DOSSIER `SymbolPicker.jsx`) — Grid mit
|
||||
Vorschau, Drag in den Plan; Instanz auf Ebene `60 Plangrafik` (bzw. `22 Möbel`).
|
||||
- **Render:** `Primitive{ kind:"symbol", ... }` → SVG `<g transform>` mit dem
|
||||
Symbol-Markup (plans-output.md §9).
|
||||
|
||||
---
|
||||
|
||||
## 6. Text & Rich-Text-Annotationen
|
||||
|
||||
Port `text_create.py` + `text_editor.py`. Formatierte Beschriftungen auf Canvas.
|
||||
|
||||
```ts
|
||||
interface TextElement extends ElementBase { // type:"draw2d", subtype:"text"
|
||||
at: Vec2; runs: RichRun[];
|
||||
heightMm: number; // Paper-mm (rendern × Massstab) ODER Modell-m
|
||||
heightMode: "paper" | "model"; // DOSSIER raum_txt_modus fix|masstab
|
||||
font: string; align: "left"|"mid"|"right"; angle: number;
|
||||
mask?: boolean; // Hintergrund-Maskierung (verdeckt Linien darunter)
|
||||
frame?: boolean; // Rahmen um den Text
|
||||
}
|
||||
interface RichRun {
|
||||
text: string;
|
||||
bold?; italic?; super?; sub?; // Hoch-/Tiefstellung (Maß-Indizes)
|
||||
}
|
||||
interface TextStyle { id; name; font; heightMm; bold; italic; } // Text-Presets
|
||||
```
|
||||
- **Render:** `<text>` mit `<tspan>` pro Run; `super/sub` via `baseline-shift` +
|
||||
kleinerer `font-size`; `mask` via weißem `<rect>` darunter; `frame` via `<rect>`.
|
||||
- **Editor:** Inline-Rich-Text-Editor (contentEditable oder leichter Custom-Editor)
|
||||
→ `RichRun[]`. Fonts aus einer kuratierten Web-Font-Liste + System-Fonts
|
||||
(DOSSIER `_list_system_fonts` mit Preferred-Liste DM Mono/Krungthep/…); im
|
||||
Browser via `document.fonts` / `queryLocalFonts()` (wo verfügbar) + gebündelte
|
||||
Web-Fonts.
|
||||
- **Massstabsbezug:** `heightMode:"paper"` → Texthöhe in mm, gerendert × Massstab
|
||||
(plans-output.md §3) — Beschriftung bleibt bei jedem Massstab lesbar.
|
||||
|
||||
---
|
||||
|
||||
## 7. Detailgrad (Level of Detail)
|
||||
|
||||
Querschnittsthema (ROADMAP §2b „Modelldarstellungen"). Drei Stufen, Dokument-/
|
||||
Snapshot-weiter Override mit Per-Element-Ausnahme — DOSSIER
|
||||
`darstellung`/`aktive_darstellung`.
|
||||
|
||||
```ts
|
||||
type DetailLevel = "coarse" | "medium" | "fine"; // einfach | standard | detail
|
||||
// Auflösung (Port _resolve_oeff_darstellung):
|
||||
function resolveDetail(el, doc): DetailLevel {
|
||||
const v = el.detailLevel ?? "auto";
|
||||
return v === "auto" ? doc.detailLevel : v; // doc-Level hat IMMER konkreten Wert
|
||||
}
|
||||
```
|
||||
- **Dokument-Ebene:** `Project.detailLevel` (Default `coarse`/einfach = 1:100,
|
||||
DOSSIER `_DARSTELLUNG_DEFAULT_GLOBAL`). In der TopBar global umschaltbar; ein
|
||||
ViewSnapshot kann ihn pro Ansicht überschreiben (plans-output.md §5).
|
||||
- **Wirkung:** jedes Bauteil-`generatePlan`/`build3d` liest den aufgelösten LoD und
|
||||
zeichnet entsprechend (elements.md: Tür coarse=Lücke, fine=Glas/Schwenkbogen/
|
||||
Sims). Kritisch für Mixed-Scale-Pläne (1:50 Detail neben 1:200 Übersicht).
|
||||
- **Schnitt-Schraffur** koppelt an LoD: grob ggf. nur Umriss, fein voll schraffiert.
|
||||
|
||||
---
|
||||
|
||||
## 8. Section-Style (3D-Schnittflächen)
|
||||
Port DOSSIER `_apply_section_style` (`layer_builder.py`). Wo die Schnittebene ein
|
||||
Bauteil durchschneidet: Schnittfläche bekommt die **Component-Schraffur**, die
|
||||
Schnittkante einen dicken Rand, optional eine Silhouette.
|
||||
|
||||
```ts
|
||||
interface SectionStyle { // pro Component (oder Ebene) ableitbar
|
||||
hatchId?: string; hatchScale; hatchAngle;
|
||||
boundaryShow: boolean; boundaryLineStyleId; boundaryWidthScale;
|
||||
fillBackground: boolean;
|
||||
}
|
||||
```
|
||||
- **3D-Viewport:** Three.js hat keinen nativen „Schnittflächen-Cap". Cap-Geometrie
|
||||
selbst erzeugen: Schnittpolygon der Cut-Plane mit den Breps → Fläche mit
|
||||
Hatch-Material (oder Stencil-Cap-Technik). Phase 4.
|
||||
- **2D-Schnitt (SVG):** `generateSection` (plans-output.md §4) liefert `cutFaces`
|
||||
→ mit `SectionStyle.hatchId` füllen. Das ist der Hauptweg; der 3D-Cap ist Bonus.
|
||||
|
||||
---
|
||||
|
||||
## 9. Was Browser hier einfacher macht (vs. DOSSIER)
|
||||
|
||||
| DOSSIER-Aufwand | entfällt im Browser, weil … |
|
||||
|---|---|
|
||||
| UserString-Backup/Restore bei Overrides (`_backup_original`/`_restore_original`) | Overrides sind reine Render-Reads — nichts wird mutiert |
|
||||
| Hatch-Curve-Link über Sticky (`gestaltung_curve_hatch`, Pending-TTL) | Schraffur ist eine Eigenschaft des Polygons, kein separates Objekt |
|
||||
| Plotweight-Welt↔Bildschirm-Rescaling (`write/read_plotweight`) | Strichstärke ist in mm im SVG-Paper-Space (plans-output.md §3.2) |
|
||||
| `install_listeners` für Live-Override-Reapply | reaktiver Store re-rendert automatisch |
|
||||
| SectionStyle-API-Reflection über Rhino-Versionen | wir definieren das Rendering selbst (SVG/Three) |
|
||||
| Cross-doc Presets als Dateien im User-Home | LocalStorage + Export/Import |
|
||||
|
||||
---
|
||||
|
||||
## 10. Umsetzungs-Reihenfolge (verweist auf ROADMAP-Phasen)
|
||||
|
||||
1. **Phase 1:** Component/Hatch/Line-Manager (§1) + `resolveStyle` (§3) +
|
||||
LoD-Grundgerüst (§7) + LoD-bewusste Stil-UI.
|
||||
2. **Phase 2:** Stil-Kataloge (Wände/Öffnungen), Material-Seeds mit `joinPriority`.
|
||||
3. **Phase 3:** Overrides-Engine + SIA-Preset (§4), Symbol-Bibliothek (§5),
|
||||
Rich-Text (§6), PBR-Material-Bibliothek; massstabsabhängige Hatch-/Linetype-
|
||||
Skalierung (plans-output.md §3.2).
|
||||
4. **Phase 4:** Section-Style 3D-Cap (§8).
|
||||
@@ -0,0 +1,370 @@
|
||||
# Handoff — Rhino-artiges Befehlssystem + Modellier-Werkzeuge
|
||||
|
||||
> Für die Instanz, die das Befehlssystem (Tab-getriggert) und die Rhino-artigen
|
||||
> Modellierfunktionen baut. Stand: 2026-06-30. Zuerst lesen: `CONVENTIONS.md`,
|
||||
> `ROADMAP.md`, `HANDOVER.md`, `docs/design/drawing-tools.md`. Volle Autonomie,
|
||||
> selbst bestätigen (Memory `proceed-autonomously`, `wire-dont-stub`, `prefer-agents`).
|
||||
|
||||
Dieses Dokument hat drei Teile:
|
||||
1. **Was schon steht** (worauf du aufbaust — exakte Dateien/Typen/Actions).
|
||||
2. **Rhino-Referenz** (Interaktionsmodell, das nachzubilden ist).
|
||||
3. **Konkreter Bauplan für DIESE Codebase** (Architektur, Dateien, Reihenfolge).
|
||||
|
||||
---
|
||||
|
||||
## TL;DR — die Kernidee
|
||||
|
||||
Es gibt **kein** Befehlssystem, keine Command-Line, keinen Tab-Handler. Das baust du
|
||||
greenfield. **Aber:** Das Werkzeug-System ist bereits eine saubere Pure-Function-Registry
|
||||
(`src/tools/`) mit generischem Controller in `App.tsx`, und die Mutations-Schicht
|
||||
(`projectSlice`) ist umfassend. Ein neues Modellier-Tool steckst du durch Hinzufügen eines
|
||||
`Tool`-Objekts ein — **PlanView muss dafür nicht angefasst werden**.
|
||||
|
||||
Das Befehlssystem ist im Kern eine **State-Machine-Engine über prompt → pick/type →
|
||||
options**, plus eine **Command-Line-UI** (Statusleiste), plus ein **Koordinaten-Parser**.
|
||||
Befehle dispatchen auf bestehende Store-Actions + `setActiveTool`/`setProject`.
|
||||
|
||||
**Wichtigster konzeptioneller Sprung:** Die heutigen Tools haben je eine *eigene*
|
||||
ad-hoc-Phasenlogik (`onClick`/`onMove`). Rhino-Feel verlangt eine **gemeinsame
|
||||
Prompt/Option/Numerik-Engine**, die alle Befehle teilen. Plane das als Verallgemeinerung
|
||||
des bestehenden `Tool`-Interfaces, nicht als Parallelwelt daneben (sonst zwei Eingabe-Pfade,
|
||||
die divergieren).
|
||||
|
||||
---
|
||||
|
||||
# TEIL 1 — Was schon steht (Baufundament)
|
||||
|
||||
Stack: React 18 + TS + Vite, `three` 0.169 (nur 3D-Display). Einheiten intern **Meter**.
|
||||
Eigener winziger Store (`useSyncExternalStore`, kein Redux/Zustand). Identifier englisch,
|
||||
UI-Text deutsch via `t()`. Strict tsc (`noUnusedLocals` → ungenutzte Vars brechen den Build).
|
||||
|
||||
## 1.1 Werkzeug-System — `src/tools/`
|
||||
- **`Tool`-Interface** `src/tools/types.ts:139` — reine Funktionen über internen `ToolState`
|
||||
(Discriminated Union je Tool). Handler geben `[nextState, ToolResult]` zurück. Tools
|
||||
schreiben NIE Plan-Primitive; sie geben `commit(project) => project` zurück.
|
||||
- `ToolId` `types.ts:11`: `"select" | "wall" | "line" | "polyline" | "rect"`.
|
||||
- `ToolContext` `types.ts:72`: `{ project, level, defaultCategoryCode, activeWallTypeId, activeLineStyleId }`.
|
||||
- `ToolPointer` `types.ts:85`: `{ raw, snap, point (=snap?.point ?? raw), shift, ctrl, alt, button }`.
|
||||
- `ToolResult` `types.ts:121`: `{ draft, commit?, done? }`. `ToolDraft` `types.ts:109`:
|
||||
`{ preview: DraftShape[], vertices: Vec2[], snap?, hud?: {at,text} }`.
|
||||
- **Registry** `src/tools/tools.ts:375`: `TOOLS: Record<ToolId,Tool>`, `getTool(id)` `:384`,
|
||||
`TOOL_ORDER` `:389`. Implementiert: select (Platzhalter), wall, line, polyline, rect.
|
||||
- `uniqueId(prefix)` `types.ts:188` — ID-Generator.
|
||||
|
||||
## 1.2 Controller / Verdrahtung — alles in `App.tsx` (NICHT in den Tool-Dateien)
|
||||
- Aktives Tool: `const [activeTool,setActiveTool]=useState<ToolId>("select")` `App.tsx:197`.
|
||||
- Laufender Zustand in **Ref** `toolStateRef` `App.tsx:205` (kein Re-Render je Mausschritt).
|
||||
Live-Vorschau `const [draft,setDraft]` `App.tsx:206`.
|
||||
- **Controller** `runToolStep(kind,raw,pxPerMeter,mods)` `App.tsx:350`: baut `ToolContext`
|
||||
(`toolCtx` `:294`), snappt via `snapFor` `:322` (→ `computeSnap`), baut `ToolPointer`,
|
||||
ruft `tool.onClick/onMove`, speichert State in Ref, `applyToolResult` `:339` wendet
|
||||
draft/commit/done an. `toolHandlers` `App.tsx:419` verbindet PlanView↔Controller.
|
||||
- Tool-Tasten `App.tsx:448`: Esc=Abbruch/zurück-zu-select, Enter=Commit-Geste, Backspace=Punkt zurück.
|
||||
|
||||
## 1.3 Semantisches Modell — `src/model/types.ts`
|
||||
- `type Element = Wall | Door | Drawing2D` `:251`. `Vec2={x,y}`. **Kein Slab/Stair-Typ.**
|
||||
- `Project` `:254`: `{ ..., wallTypes[], drawingLevels[], layers[], walls[], doors[], drawings2d[] }`.
|
||||
- `Wall` `:150`: `{ id,type:"wall", floorId, categoryCode, start, end, wallTypeId, height, color? }`
|
||||
(Mittellinie + mehrschichtiger `WallType`).
|
||||
- **Plan-Primitive `Drawing2DGeom`** `:200` — die Geom-Typen existieren bereits ALLE:
|
||||
`line | polyline | rect | circle | arc | text`. Aber Tools erzeugen heute nur line/polyline/rect,
|
||||
und `drawingVertices` (Grips) kennt nur diese drei. **circle/arc/text sind im Typ da, aber
|
||||
nicht durchgängig gerendert/editierbar** — Lücke, kein Neubau nötig.
|
||||
- `Drawing2D` `:209`: `{ id,type:"drawing2d", levelId, categoryCode, geom, lineStyleId?, hatchId?, color?, fillColor?, weightMm? }`.
|
||||
|
||||
## 1.4 Geometrie — `src/model/geometry.ts`
|
||||
`sub,add,scale,len,normalize`; `leftNormal(a)={x:-a.y,y:a.x}` `:17` (Wand-Normale-Konvention);
|
||||
`cross`, `lineIntersect(a,da,b,db)`, `along`, `wallBand`, `wallCorners` `:70`, `clippedBand` `:87`.
|
||||
`src/model/joins.ts`: `computeJoins(project,walls)` `:44` (nur L-Ecken gehrt; T/X eckig).
|
||||
**Für Offset/Trim/Fillet (Rhino-Kern) gibt es NOCH KEIN 2D-Geometrie-Kernel** — Kurven/Kurven-
|
||||
Schnitt, Polylinien-Offset usw. musst du ergänzen (siehe Bauplan §3.4).
|
||||
|
||||
## 1.5 Store — `src/state/`
|
||||
- `createStore` `store.ts:51` über `useSyncExternalStore`. **Actions leben IM State**
|
||||
(`useStore(s=>s.action)`, referenzstabil). `RootState = Project & Selection & View & Layout`
|
||||
`appStore.ts:21`. Exports `useStore`, `getState`, `setState`.
|
||||
- **projectSlice**: `project` + `setProject(next|(p)=>p)`. Mutationen u.a. `addFloor`,
|
||||
`addCategory`, `setElementColor/Weight/Fill`, `resizeElement`, `moveGripOf`, `moveElementByOf`,
|
||||
`moveEdgeOf`, `commitTransformOn`. **Es gibt keine generische „addWall/addDrawing2d"-Action** —
|
||||
Tools committen via `setProject`. (Beim Befehlssystem ggf. saubere Actions ergänzen.)
|
||||
- **Aktive Zeichenebene + aktive Kategorie liegen im viewSlice**, NICHT in selection:
|
||||
`activeLevelId` `viewSlice.ts:46`/`setActiveLevelId`, `activeCategoryCode` `:42`/`setActiveCategoryCode`.
|
||||
- selectionSlice: `selectedWallIds[]`, `selectedDrawingId` + Setter/`clearSelection`.
|
||||
|
||||
## 1.6 Views, Eingabe, Koordinaten — `src/plan/PlanView.tsx` (SVG-Vektor)
|
||||
- Modell → `Plan`-Primitive via `generatePlan` `src/plan/generatePlan.ts:203`. `Primitive` =
|
||||
`polygon|line|arc` (polygons tragen `wallId`/`drawingId` für Hit-Test).
|
||||
- **Transform (entscheidend):** `PX_PER_M=90` `:20`; `toScreen(p)={x:p.x*90,y:-p.y*90}` `:31`
|
||||
(fixer Welt-Ursprung 0,0; Y flippt). Invers `viewToModel` `:440`. SVG `viewBox`=State `view`;
|
||||
Pan/Zoom ändern nur `view`, nie das Modell↔Screen-Mapping.
|
||||
- **`rawModelAt(clientX,clientY)`** `:446` = aktuelle Mauswelt-Position in Meter (der Eine-Aufruf,
|
||||
den ein Tool/Befehl braucht). `currentPxPerMeter()` `:488`.
|
||||
- **Pointer-Events** alle am `<svg>` `:910`: down `:553`, move `:625`, up `:715`, wheel `:820`,
|
||||
dblclick `:847`, contextmenu `:857`. Schema: Mitte=Pan, Links=Select/Marquee/Tool, Rechts=Menü.
|
||||
Bei `toolActive` `:268` routen Links-Events zu `toolHandlers`. **PlanView meldet bereits
|
||||
`(rawModelAt, currentPxPerMeter, toolMods)` nach oben** — neue Tools brauchen hier NICHTS.
|
||||
- **Snapping** `src/tools/snapping.ts`: `computeSnap(input)` `:88` — endpoint/midpoint/intersection/
|
||||
onEdge/grid/ortho mit Prioritätstabelle. Wird in App (`snapFor`) konsumiert, nicht in PlanView.
|
||||
`applyAngleConstraint` für Ortho. `SnapSettings`/`DEFAULT_SNAP` in `tools/types.ts:36/55`.
|
||||
- 3D `src/viewport/Viewport3D.tsx` (three.js, Raycaster): nur Anzeige+Auswahl, **keine
|
||||
Zeichenwerkzeuge**. 3D-Authoring = eigene spätere Phase (Raycast auf Arbeitsebene).
|
||||
|
||||
## 1.7 Tastatur / globale Eingabe — **kein Dispatch-System**
|
||||
- `main.tsx:38` globaler `contextmenu`→preventDefault; `:42` blockt Ctrl/Cmd+A außerhalb Inputs;
|
||||
`isTextEntry(el)` `:24`.
|
||||
- App-useEffects mit `window.addEventListener("keydown")`: Tool-Tasten `:448`, Delete `:604`,
|
||||
Transform-Shortcuts m/s/d + u/i/o/p `:637`. **Jeder Guard wiederholt inline den
|
||||
INPUT/TEXTAREA/contentEditable-Check** — es gibt keine geteilte Keymap. Dein Tab-Handler +
|
||||
Command-Input kommt als neuer globaler `keydown` dazu (siehe §3.2).
|
||||
|
||||
## 1.8 UI-Shell + i18n
|
||||
- `App.tsx` (~2200 Z., enthält noch ToolController/Grips/Transform). JSX `:1043`: TopBar → body
|
||||
(Dock links, Content-View-Router, TransformBar, Dock rechts, Floating) → StatusBar →
|
||||
ResourceManager → ContextMenu → InlineEditor. Panel-Daten via `PanelHostContext` (`baseHost`
|
||||
`App.tsx:729`, Typ `host.ts`).
|
||||
- `StatusBar.tsx` — Footer: links `hint` (Tool-Hinweis), rechts X/Y, Einheit, Massstab 1:N, Zoom,
|
||||
aktives Geschoss, aktive Ebene. **Bester Ort für die Command-Line** (Rhino hat sie klassisch unten).
|
||||
- **i18n** `src/i18n/`: `t(key,params?)` `index.ts:67`, `useT()` `:84`. Flaches `as const`-Dict,
|
||||
Punkt-Namespaces (`tool.*`,`snap.*`,`transform.*`,`status.*`…). `de.ts` (Quelle, ~309 Keys) +
|
||||
`en.ts`; `TranslationKey=keyof typeof de` erzwingt Parität. **Neue Keys IMMER in beide Dateien.**
|
||||
Keine hartcodierten JSX-Strings.
|
||||
|
||||
## 1.9 Verifizieren
|
||||
- `npx tsc -b` · `npm run build` · Dev `npm run dev` (Vite 5173, `host:true`).
|
||||
- Screenshot `node scripts/probe.mjs` → `scripts/probe.png` (Puppeteer headless, `deviceScaleFactor:2`,
|
||||
URL via `PROBE_URL`). Viele task-Probes existieren (`probe-tools.mjs`, `probe-line.mjs`,
|
||||
`probe-transform.mjs` …) — gute Vorlagen, um Tools/Befehle programmatisch zu treiben.
|
||||
**Screenshot ansehen + Geometrie prüfen**, nicht nur „kompiliert".
|
||||
|
||||
---
|
||||
|
||||
# TEIL 2 — Rhino-Referenz (das Interaktionsmodell)
|
||||
|
||||
## 2.1 Die Command-Line ist das Rückgrat
|
||||
**Alles ist ein Befehl**, und die Command-Line **hört immer zu**: Tastenanschläge gehen an die
|
||||
Command-Line, wenn sie nicht von einem Feld konsumiert werden. Kein „Tool aktiv vs. Eingabe aktiv".
|
||||
Die Zeile hat gleichzeitig drei Rollen: **Eingabe** (Befehl/Wert tippen), **Prompt**
|
||||
(„Start of line", „Next point"), **Optionen** (eckige, klickbare Inline-Optionen).
|
||||
|
||||
## 2.2 Befehl aufrufen
|
||||
- Namen tippen, z. B. `Line`. **Präfix-Autocomplete** (case-insensitiv): `L`→`Li`→`Lin` zeigt
|
||||
Kandidatenliste mit Best-Match. **Tab/Pfeile** akzeptieren Vorschlag, **Enter/Leertaste** führt aus.
|
||||
- **Aliase**: nutzerdefinierte Kürzel → Makro (z. B. `L`→`!_Line`, `cp`→`!_Copy`). Werden VOR
|
||||
Autocomplete gematcht. (Minimal: Einzelbuchstabe→Befehl.)
|
||||
|
||||
## 2.3 Enter / Leertaste / Rechtsklick (leicht falsch gemacht)
|
||||
- **Enter = Leertaste** in der Command-Line. Beide: Befehl ausführen / Default akzeptieren /
|
||||
mehrteiligen Befehl **beenden** / bei **leerer** Zeile **letzten Befehl wiederholen**.
|
||||
- **Rechtsklick im Viewport = Enter.** Also: Rechtsklick beendet Polyline UND wiederholt bei
|
||||
leerer Zeile den letzten Befehl. → `lastCommand` speichern, bei Leer-Enter/Rechtsklick neu starten.
|
||||
|
||||
## 2.4 Inline-Optionen (klickbare Klammern)
|
||||
```
|
||||
Start of line ( BothSides=No Chamfer Mode=Distance ):
|
||||
```
|
||||
- Jede Option **klickbar UND tippbar** (genug Buchstaben zur Eindeutigkeit + Enter).
|
||||
- **Toggle** `Name=Value` flippt beim Klick. **Value**-Option fragt Unterwert ab. **Action**-Option
|
||||
(ohne `=`) verzweigt sofort.
|
||||
- Optionen sind **innerhalb des Befehls persistent**, viele **über Aufrufe hinweg** (letzte
|
||||
Offset-Distanz, Array-Anzahl, Fillet-Radius merken). **Zuletzt benutzte Optionswerte je Befehl
|
||||
persistieren** — Nutzer erwarten das.
|
||||
|
||||
## 2.5 Sub-Prompts = State-Machine
|
||||
Befehle laufen Prompts ab. `Line`: „Start of line:" → Punkt → „End of line:" → Punkt → fertig.
|
||||
`Polyline`: „Start" → „Next point ( Close Undo ):" → … → **Enter** beendet. Prompt-Text ist
|
||||
sichtbar und lehrreich („Next point. Press Enter when done") — literal nachbilden.
|
||||
|
||||
## 2.6 Transparente/verschachtelbare Befehle
|
||||
Manche Befehle (Zoom/Pan, Osnap-Toggle, alles mit `'`-Präfix) laufen **innerhalb** eines anderen,
|
||||
ohne ihn abzubrechen, und kehren zum Original-Prompt zurück. → Command-Runner braucht einen **Stack**.
|
||||
|
||||
## 2.7 Koordinaten- & Numerik-Eingabe (Herz der Präzision)
|
||||
| Eingabe | Bedeutung |
|
||||
|---|---|
|
||||
| `5,3` / `5,3,2` | absolut X,Y(,Z) |
|
||||
| `r5,3` | **relativ** zum letzten Punkt (das `r`-Idiom) |
|
||||
| `<45` | Winkel-Constraint auf 45°, dann Maus/Distanz |
|
||||
| `5<45` | **polar**: Distanz 5 unter 45° vom letzten Punkt |
|
||||
| Zahl tippen während Drag | **Distanz-Lock**: Richtung per Maus, Länge per Zahl+Enter (meistgenutzte Geste) |
|
||||
| Zahl + **Tab** | Lock umschalten (Länge fix → Winkel folgt Maus, oder umgekehrt) |
|
||||
Das Feld parst **kontextabhängig**: Befehlsname / Optionsbuchstabe / Koordinate / nackte Zahl —
|
||||
je nach Befehlszustand. Das Live-Tool muss **einen primären Skalar** (Länge/Radius/Distanz)
|
||||
exponieren, an den eine getippte Zahl bindet.
|
||||
|
||||
## 2.8 Osnaps + Ortho + Gumball
|
||||
- **Osnaps** (persistente Toggles): End, Mid, Cen, Int, Perp, Near, Quad, Tan, Point. Pro Mausschritt
|
||||
gegen nahe Geometrie geprüft (Pixel-Toleranz), Marker+Label am Cursor; liefert **exakte
|
||||
Modellkoordinate** (nie Roh-Maus, wenn Snap aktiv). One-Shot-Osnap überschreibt für den nächsten Pick.
|
||||
- **Ortho** (F8): Winkelraster (90°/konfigurierbar), **Shift** togglet temporär. **Grid Snap** (F9).
|
||||
**SmartTrack**: temporäre Hilfslinien aus zuletzt gehoverten Punkten.
|
||||
- **Gumball**: On-Object-Widget (Pfeile=Move, Bögen=Rotate, Handles=Scale); Handle klicken →
|
||||
Zahl tippen für exakten Transform. Direkt-Manipulations-Gegenstück zu getippten Befehlen.
|
||||
|
||||
> Präzisionsmodell = **(Snap ODER getippte Koordinate) × (Ortho/Winkel-Constraint) ×
|
||||
> (Distanz-Constraint)**, in EINEM Pick komponierbar.
|
||||
|
||||
## 2.9 Auswahl-Modell (links/rechts-Regel exakt)
|
||||
- Klick = wählen; Shift+Klick add; Ctrl+Klick remove.
|
||||
- **Links→rechts = Window** (nur voll umschlossene; **durchgezogenes** Rechteck).
|
||||
- **Rechts→links = Crossing** (auch berührte; **gestricheltes** Rechteck). Richtung bestimmt
|
||||
Modus — starke Konvention, exakt nachbilden.
|
||||
- **SelLast** (zuletzt erzeugte/gewählte erneut wählen) ist enorm nützlich („erzeugen, dann sofort
|
||||
bewegen"). Min. `SelLast`, `SelAll`, `SelNone`, `Invert`.
|
||||
|
||||
## 2.10 Befehls-Prompt-Sequenzen (Kurz)
|
||||
2D: **Line** (2 Pkt) · **Polyline** (Close/Undo, Enter beendet) · **Rectangle** (Ecke+Ecke, oder
|
||||
Breite/Höhe tippen; 3Point/Center) · **Circle** (Center+Radius; 2P/3P/Tan) · **Arc** (Center-Start-End /
|
||||
3Point) · **Offset** (Kurve wählen → Seite klicken/Distanz tippen; Distanz persistent) ·
|
||||
**Fillet/Chamfer** (Kurve1→Kurve2, Radius/Distances persistent) · **Trim** (Schneider wählen→Enter→
|
||||
wegzuschneidendes Stück klicken) · **Split** · **Extend** · **Join** · **Explode** ·
|
||||
**Move/Copy/Rotate/Scale/Mirror** (Auswahl→Basispunkt→Ziel; Copy-Option) · **ArrayRect/ArrayPolar** ·
|
||||
**Group/Ungroup**.
|
||||
3D (braucht CSG, später): **ExtrudeCrv** (geschlossene Kurve→Solid, Cap) · **Box** · **Boolean
|
||||
Union/Difference/Intersection** · **Cap** · **Gumball-Face-Drag = PushPull** · Loft/Sweep/Revolve.
|
||||
|
||||
---
|
||||
|
||||
# TEIL 3 — Bauplan für DIESE Codebase
|
||||
|
||||
> Ziel: nutzbarer 2D-Architektur-Drafter mit Rhino-Feel, dann einfaches Massing. Halte das
|
||||
> ROADMAP-Prinzip: **ein semantisches Modell → Sichten abgeleitet**; Extrusionshöhe ist eine
|
||||
> Eigenschaft, nie eingebackene Geometrie.
|
||||
|
||||
## 3.0 Kuratierungs-Prinzip (WICHTIG — Nutzer-Vorgabe)
|
||||
**NICHT den ganzen Rhino-Katalog stumpf portieren.** Wir bauen ein **Wohnbau-BIM**, keinen
|
||||
NURBS-Allzweck-Modeller. Nimm nur, was dem Wohnbau-Workflow dient; lass den Rest weg, bis er
|
||||
konkret gebraucht wird. Faustregel: *Brauche ich das, um ein Einfamilienhaus zu zeichnen und
|
||||
daraus Pläne zu ziehen?* Wenn nein → weglassen.
|
||||
|
||||
**Bewusst WEGLASSEN (vorerst):** Loft / Sweep1+2 / Revolve (Sonderformen, kaum Wohnbau) ·
|
||||
freie NURBS-Kurven (`Curve`/`InterpCrv` Grad>1, Deformable, FromFoci) · Ellipse · Tangent/
|
||||
Bisector/4Point-Linienvarianten · SmartTrack (nett, nicht kritisch) · der volle `Sel*`-Zoo
|
||||
(nur SelLast/SelAll/SelNone/Invert) · Knot/Vertex/Tan-Osnaps. Alle leicht später additiv
|
||||
nachrüstbar — kein Grund, sie jetzt mitzuschleppen.
|
||||
|
||||
**Booleans sind KEIN „nice to have später"** — sie werden gebraucht, **sobald Tür/Fenster als
|
||||
echte 3D-Öffnung** kommen (heute schneidet `Door` nur eine Plan-Lücke, kein 3D-Boolean, siehe
|
||||
HANDOVER). Darum: CSG/Booleans an die **Tür/Fenster-Phase koppeln** und dann reinnehmen — nicht
|
||||
ans Ende schieben. ABER (das ist der „nicht stumpf"-Teil):
|
||||
> Für **rechteckige** Öffnungen in extrudierten Wänden braucht es **keinen allgemeinen
|
||||
> Boolean-Kernel**. Eine analytische **Wand-minus-Box-Subtraktion** (Öffnung als parametrische
|
||||
> Aussparung im Wand-Solid) ist einfacher, robuster und für 95 % Wohnbau ausreichend. Den
|
||||
> allgemeinen CSG-Boolean (`rhino3dm`) erst ziehen, wenn schräge/runde/verschnittene Fälle
|
||||
> wirklich auftreten. Also: **Öffnungen zuerst analytisch, allgemeine Booleans erst bei Bedarf.**
|
||||
|
||||
## 3.1 Leitentscheidung: Engine verallgemeinern, nicht parallel bauen
|
||||
Baue eine gemeinsame **Command-Engine**, die das bestehende `Tool`-Interface erweitert/ablöst,
|
||||
sodass es **einen** Eingabepfad gibt (Maus + Tastatur + Command-Line speisen dieselbe Maschine).
|
||||
Konkret: ein `Command`-Modell, das je Schritt einen **Prompt** (Text), erwartete **Eingabearten**
|
||||
(Punkt | Zahl | Option | Auswahl) und **Optionen** beschreibt. Die heutigen Tools werden zu
|
||||
Befehlen dieser Engine (wall/line/polyline/rect lassen sich 1:1 portieren — ihre Phasenlogik ist
|
||||
schon eine Mini-State-Machine).
|
||||
|
||||
**Warum nicht das alte Tool-Interface unangetastet lassen und Command-Line nur draufsetzen?**
|
||||
Weil die Command-Line getippte Koordinaten/Optionen in denselben Schritt einspeisen muss, in dem
|
||||
die Maus pickt. Zwei getrennte Pfade divergieren garantiert (Snapping, Constraints, HUD doppelt).
|
||||
|
||||
## 3.2 Neue Dateien (Vorschlag)
|
||||
- `src/commands/engine.ts` — Command-Runner: aktiver Befehl, Prompt-Stack (für transparente
|
||||
Befehle §2.6), `lastCommand`-Wiederholung, Routing von Maus-Pick / getippter Eingabe / Option-Klick
|
||||
in den aktuellen Schritt. Hält `CommandState`.
|
||||
- `src/commands/types.ts` — `Command`-Interface (Verallgemeinerung von `Tool`): Schritte mit
|
||||
`prompt: TranslationKey`, `accepts: ("point"|"number"|"option"|"selection")[]`, `options: CmdOption[]`,
|
||||
`onInput(state,input,ctx): [state, CommandResult]`. `CommandResult` wie `ToolResult` (+`commit`).
|
||||
- `src/commands/parseInput.ts` — Koordinaten-Parser (§2.7): `5,3` · `r5,3` · `5<45` · `<45` ·
|
||||
nackte Zahl (Distanz-Lock) · Optionsbuchstabe. Liefert eine Discriminated Union, die die Engine
|
||||
in einen Modellpunkt/Constraint auflöst (mit `lastPoint` für `r`/polar).
|
||||
- `src/commands/registry.ts` — `COMMANDS: Record<string,Command>` + Aliase + Autocomplete (Präfix).
|
||||
- `src/ui/CommandLine.tsx` — die Command-Line-UI **in/über der Statusleiste** (`StatusBar.tsx`):
|
||||
zeigt Prompt + klickbare Optionen + Texteingabe; Autocomplete-Dropdown. Tab fokussiert sie.
|
||||
- (später) `src/geometry/kernel2d.ts` — 2D-Kernel für Offset/Trim/Fillet/Schnitt (§3.4).
|
||||
- (viel später) `src/geometry/solid3d.ts` o. `rhino3dm`-Anbindung für Massing/Booleans (§3.5).
|
||||
|
||||
## 3.3 Verdrahtung (minimal-invasiv)
|
||||
- **Globaler Tab-Handler**: neuer `window.keydown` in App (gleicher Guard wie `App.tsx:448` —
|
||||
INPUT/TEXTAREA/contentEditable überspringen). Tab → Command-Line fokussieren/öffnen. Jeder
|
||||
getippte Buchstabe ohne aktives Tool startet den Befehlsmodus (Rhino „hört immer zu" — optional
|
||||
in Phase 2; Phase 1 reicht Tab).
|
||||
- **Command-Line → Engine → Store**: Befehle dispatchen auf `setActiveTool` (für tool-artige) bzw.
|
||||
direkt auf Store-Actions / `setProject`. Nutze `getState()/setState()` (referenzstabil) aus
|
||||
`appStore.ts`.
|
||||
- **Pick-Eingabe**: die Engine konsumiert dieselben `(rawModelAt, currentPxPerMeter, toolMods)`,
|
||||
die PlanView schon hochmeldet (`ToolHandlers`). `computeSnap` für Punktfang wiederverwenden.
|
||||
→ PlanView braucht im Idealfall **keine Änderung** (höchstens: Window/Crossing-Marquee-Visual
|
||||
durchgezogen vs. gestrichelt nach Drag-Richtung, §2.9 — heute evtl. nur ein Modus).
|
||||
- **Prompt/HUD**: Prompt-Text in die Statusleiste (`StatusBar` `hint` existiert schon). Distanz/
|
||||
Winkel-HUD am Cursor existiert in `ToolDraft.hud`.
|
||||
|
||||
## 3.4 Reihenfolge (Tiers — strikt 2D zuerst)
|
||||
**Tier 0 — Substrat (VOR jedem Befehl; das ist der „Feel"):**
|
||||
1. Command-Runner + Command-Line-UI (Prompt → pick/type → Optionen; Enter/Space/Rechtsklick =
|
||||
bestätigen/beenden/wiederholen; `lastCommand`).
|
||||
2. Koordinaten-Parser (`x,y` · `rdx,dy` · `dist<angle` · nackte-Zahl-Lock).
|
||||
3. Osnaps (End/Mid/Cen/Int/Perp/Near) — `computeSnap` ist da, ggf. Cen/Perp/Near ergänzen.
|
||||
4. Ortho (90°/45°, Shift-Toggle) + Grid-Snap — teils vorhanden (`applyAngleConstraint`).
|
||||
5. Auswahl: Klick, Shift/Ctrl add/remove, **Window vs. Crossing** (durchgezogen/gestrichelt,
|
||||
links/rechts-Regel).
|
||||
|
||||
**Tier 1 — 2D-Pflicht (reines SVG/2D), grobe Baufolge:**
|
||||
6. **Line** (validiert die ganze pick/snap/constrain-Schleife) → 7. **Polyline** (Close/Undo) →
|
||||
8. **Rectangle** (Ecke + Center/3Point) → 9. **Circle** (Center+Radius). Diese vier portieren die
|
||||
heutigen Tools auf die Engine + numerische Eingabe.
|
||||
10. **Move** → 11. **Copy** (wiederholend) → 12. **Offset** (persistente Distanz — DAS Architektur-
|
||||
Primitiv) → 13. **Trim** + **Split** → 14. **Join** + **Explode**. **Undo/Redo** durchgängig
|
||||
annehmen (heute? — prüfen; ggf. Command-History/Undo-Stack im Store ergänzen).
|
||||
|
||||
**Tier 2 — 2D stark nützlich:** Rotate/Scale/Mirror (Copy-Option) · Fillet/Chamfer · Arc ·
|
||||
Extend · ArrayRect/ArrayPolar · Group/Ungroup · Gumball(2D) · Sel*-Helfer (min. SelLast).
|
||||
|
||||
**Tier 3 — Massing + Öffnungen (an Tür/Fenster-Phase gekoppelt):**
|
||||
- **Öffnungen zuerst analytisch:** Tür/Fenster als parametrische Aussparung im Wand-Solid
|
||||
(Wand-Extrude minus Öffnungs-Box) — KEIN allgemeiner Boolean-Kernel nötig (§3.0). Das ist der
|
||||
kritische, roadmap-markierte 🔴-Teil (echte 3D-Öffnung statt nur Plan-Lücke) und kommt MIT
|
||||
Tür/Fenster, nicht danach.
|
||||
- **Massing-Befehle:** ExtrudeCrv (geschlossene Plan-Kurve → gecapptes Solid) → Box →
|
||||
Gumball-Face-Drag-PushPull.
|
||||
- **Allgemeine Booleans** (Union/Difference/Intersection) **erst bei Bedarf** (schräge/runde/
|
||||
verschnittene Fälle): **kein eigener Kernel — `rhino3dm` (WASM-openNURBS)** als `src/io/`-Schicht
|
||||
(Roadmap-Entscheid, HANDOVER). Bis dahin reicht die analytische Subtraktion.
|
||||
- **Weggelassen:** Loft/Sweep/Revolve/OffsetSrf (§3.0 — Sonderformen, kaum Wohnbau).
|
||||
|
||||
## 3.5 Was sauber 2D ist vs. was hart ist
|
||||
- **Sauber SVG/2D:** Line, Polyline, Rect, Circle, Arc, Move/Copy/Rotate/Scale/Mirror, Array, Group,
|
||||
Control-Point-Edit, Gumball(2D). Affine Transforms + Kurven-Schnitt.
|
||||
- **Echte Arbeit (2D-Kernel nötig):** **Offset, Trim, Fillet** brauchen kompetenten Kurven-Schnitt
|
||||
und Polylinien-Offset — dafür Zeit einplanen (`src/geometry/kernel2d.ts`).
|
||||
- **Braucht 3D/CSG:** Extrude, Box, Boolean*, Cap, OffsetSrf, Loft/Sweep/Revolve, Face-Drag. Booleans
|
||||
sind das Korrektheits-Zentrum → `rhino3dm`.
|
||||
|
||||
## 3.6 Gotchas (aus Rhino-Verhalten + dieser Codebase)
|
||||
- **Command-Line hört immer zu** — Tasten global routen, aber die `isTextEntry`-Disziplin
|
||||
(`main.tsx:24`) + die Native-App-Regeln (kein Ctrl+A/keine Textauswahl, CONVENTIONS.md) wahren.
|
||||
- **Zuletzt benutzte Optionswerte je Befehl persistieren** (Offset-Distanz, Array-Anzahl, Fillet-Radius).
|
||||
- **Enter = Rechtsklick = Wiederholen/Bestätigen/Mehrteiliges-Beenden** — alle drei auf EIN Signal.
|
||||
- **Distanz-Lock:** Live-Befehl muss EINEN primären Skalar exponieren, an den eine getippte Zahl bindet.
|
||||
- **Window vs. Crossing** über Drag-Richtung + durchgezogen/gestrichelt — nicht global ein Modus.
|
||||
- **Osnap liefert exakte Modellkoordinate** — nie Roh-Maus, wenn Snap aktiv (`ToolPointer.point`).
|
||||
- **Strict tsc** (`noUnusedLocals`) — ungenutzte Vars/Parameter brechen `npm run build`.
|
||||
- **i18n**: jeder sichtbare String über `t('key')`, Keys in `de.ts` UND `en.ts` (Parität erzwungen).
|
||||
- **Keine generische addWall/addDrawing2d-Action** — entweder via `setProject` committen (wie heute)
|
||||
oder beim Refactor saubere Actions im `projectSlice` ergänzen (besser für Undo/Redo).
|
||||
- **App.tsx ist bereits ~2200 Z.** (God-Component-Kritik in HANDOVER). Lege Command-Engine in
|
||||
`src/commands/`, halte App-Verdrahtung dünn (nur Tab-Handler + CommandLine-Mount + Dispatch-Brücke).
|
||||
|
||||
## 3.7 Verifikations-Drehbuch
|
||||
Pro Tier eine Probe (Vorlage: `scripts/probe-tools.mjs`/`probe-transform.mjs`): Befehl per Command-
|
||||
Line tippen → Punkte/Werte tippen → Screenshot → Geometrie visuell prüfen. Tier 0 zuerst headless
|
||||
treiben (Tab → „line" → „0,0" Enter → „r3,0" Enter → Linie im PNG sichtbar). `npx tsc -b` +
|
||||
`npm run build` grün halten. **Screenshot ansehen**, nicht nur Kompilat vertrauen (Memory `wire-dont-stub`).
|
||||
|
||||
---
|
||||
|
||||
## Anhang — Minimaler erster Meilenstein (konkret)
|
||||
1. `src/commands/types.ts` + `engine.ts` + `parseInput.ts` (Tier 0.1/0.2).
|
||||
2. `src/ui/CommandLine.tsx`, in `StatusBar` gemountet; Tab-Handler in App.
|
||||
3. `Line` als erster Command (portiert `lineTool`), inkl. getippter `0,0` / `r3,0` / `3<45`.
|
||||
4. Probe `scripts/probe-command-line.mjs`: Tab→line→zwei getippte Koordinaten→PNG prüfen.
|
||||
5. Dann Polyline/Rect/Circle, danach Move/Copy/Offset.
|
||||
|
||||
Damit steht der Rhino-Feel-Kern, und jeder weitere Befehl ist additiv (neues `Command`-Objekt in
|
||||
`registry.ts`, keine PlanView-/App-Änderung).
|
||||
@@ -0,0 +1,52 @@
|
||||
# Zustands-Architektur & Code-Aufteilung (App.tsx entschlacken)
|
||||
|
||||
> Ziel: `App.tsx` von „God-Component" zu dünnem Shell. Globaler Zustand in einen
|
||||
> Store, Features in eigene Module → **wartbar + parallel bearbeitbar**.
|
||||
|
||||
## Problem
|
||||
`App.tsx` hält aktuell: Projekt-State, Auswahl, View-State (viewType/detailLevel/
|
||||
renderMode/referenceLines/scale/activeLevel), Layout, ALLE Mutations-Handler
|
||||
(floors/layers/components/hatches/lineStyles/walls/doors), Kontextmenü-Builder,
|
||||
Inline-Editoren, Layout-Menü, View-Routing. → Risiko + Flaschenhals (jedes Feature
|
||||
fasst App.tsx an → kein paralleles Arbeiten).
|
||||
|
||||
## Zielstruktur
|
||||
```
|
||||
src/state/
|
||||
store.ts // Store (Zustand) — kombiniert die Slices, ein useStore-Hook
|
||||
projectSlice.ts // project + alle Mutationen (floors, layers, components,
|
||||
// hatches, lineStyles, walls, doors) inkl. recompute/refs-aware delete
|
||||
selectionSlice.ts // selectedWallIds (+ marquee-Ergebnis)
|
||||
viewSlice.ts // viewType, detailLevel, renderMode, referenceLines, activeLevelId, scale
|
||||
layoutSlice.ts // wraps src/panels/layout.ts (docks + floating)
|
||||
src/views/ // LevelPlanView, PerspectiveView, SectionStub, DrawingView (+ ViewRouter)
|
||||
src/editors/ // FloorSettingsEditor, LayerSettingsEditor, … (heute inline in App)
|
||||
src/menus/ // layerContextMenu(), levelContextMenu(), planContextMenu() — bauen ContextMenu-Items
|
||||
src/ui/ // TopBar, StatusBar, ResourceManager, ContextMenu (bestehen)
|
||||
src/panels/ // Docks/Panels (bestehen)
|
||||
App.tsx // DÜNN: Store-Provider · TopBar · (Docks + ViewRouter) · StatusBar
|
||||
// · Floating-Panels · Ressourcen-Overlay
|
||||
```
|
||||
|
||||
## Store-Wahl: Zustand (empfohlen)
|
||||
- Winzige Lib, kein Boilerplate, **Slices** gut teilbar, Selektoren verhindern
|
||||
Re-Render-Sturm, kein Prop-Drilling. Passt zu „verschiedene Features = verschiedene
|
||||
Slice-Dateien" → Parallelität.
|
||||
- Alternative ohne Dependency: Context + useReducer oder `useSyncExternalStore`
|
||||
(wie i18n). Mehr Boilerplate; bei der State-Menge ist Zustand ergonomischer.
|
||||
- Komponenten: `const walls = useStore(s => s.walls)` / `useStore(s => s.addFloor)`.
|
||||
PanelHostContext entfällt (Panels lesen direkt aus dem Store).
|
||||
|
||||
## Vorgehen (reiner Refactor — Verhalten MUSS identisch bleiben)
|
||||
1. Store + Slices anlegen, Projekt-State + Mutationen aus App.tsx hierher ziehen
|
||||
(1:1, gleiche Logik inkl. recomputeFloorElevations, refs-aware delete).
|
||||
2. View-/Selection-/Layout-State in ihre Slices.
|
||||
3. Inline-Editoren, Kontextmenü-Builder, View-Routing in `src/editors/`,`src/menus/`,`src/views/` extrahieren; sie lesen den Store.
|
||||
4. App.tsx auf den Shell reduzieren.
|
||||
5. Verifizieren: tsc + build + Screenshots — **pixel-/funktionsgleich** zu vorher
|
||||
(Auswahl, Massstab, Kontextmenü, Panels, 3D/Plan, i18n). Reiner Umbau, kein
|
||||
Feature-Wechsel.
|
||||
|
||||
## Auszahlung
|
||||
- Danach editiert ein Wand-Feature `projectSlice`/`views`, ein Panel-Feature `panels`,
|
||||
ein Editor `editors` — **disjunkte Dateien → mehrere Code-Workflows parallel** möglich.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Top-Bar (Oberleiste) & Footer/Status-Leiste — Design
|
||||
|
||||
> Referenz: DOSSIER `rhino/toolbar.py` + `src/ToolbarApp.jsx`. Hier auf unseren
|
||||
> Standalone-Stack (React+TS, eigenes Modell) übersetzt. DOSSIER hat KEINEN Footer
|
||||
> (delegiert an Rhinos eigene Leiste) — den Footer ergänzen wir neu (Vectorworks-Stil).
|
||||
|
||||
## Top-Bar — Gruppen (links → rechts)
|
||||
|
||||
1. **Marke/Logo** + Settings-Icons (Projekt-Einstellungen, App-Einstellungen).
|
||||
2. **Ansicht** — Umschalter: Grundriss · Perspektive · Schnitt · Ansicht.
|
||||
Später: 3D-Views Top/Iso + Himmelsrichtungen N/O/S/W (mit Nordwinkel-Rotation).
|
||||
3. **Darstellung** — Render-/Anzeigemodus (Wireframe/Shaded/…) + **Detailgrad**
|
||||
(grob/mittel/fein, ≙ DOSSIER „Darstellung" Einfach/Standard/Detail).
|
||||
4. **Massstab & Zoom** — Live-Anzeige „1:N" + Dropdown (1:1,1:5,…,1:1000, frei) +
|
||||
**Plan-Ansicht-Toggle** (Linienstärken für Druck) + Zoom-Buttons: 100% · Einpassen ·
|
||||
Auswahl. Quelle: viewBox-Skala / `dpi = 96·devicePixelRatio` (siehe docs/design/plans-output.md).
|
||||
5. **Overrides** (regelbasiert, Toggle + Preset) · **Masse** (Bemaßungs-Preset) — später.
|
||||
6. **Anordnen (Z-Order)** — nach vorne/hinten (für 2D-Plangrafik) — später.
|
||||
7. **Snapping** — Master-Osnap + Modi (End/Mitte/Schnittpunkt/Lot/Zentrum/Nah) +
|
||||
**Raster** an/aus + **Referenzlinien** (Wandachsen) an/aus — wenn Zeichenwerkzeuge da sind.
|
||||
8. **Text** — Stil/Font/Größe + B/I/U + Ausrichtung + „+" — mit den 2D-Werkzeugen.
|
||||
|
||||
### MVP jetzt (zu vorhandenem Modell)
|
||||
- Ansichts-Umschalter (haben wir, ausbauen) · Detailgrad-Dropdown · **Massstab 1:N +
|
||||
Zoom: Einpassen/Auswahl/100%** · Render-Modus · Referenzlinien-Toggle · Ressourcen.
|
||||
|
||||
## Footer / Status-Leiste (neu, unten, ~22 px)
|
||||
|
||||
`[Werkzeug-Hinweis] … [Cursor X/Y/Z] · [Einheit] · [Massstab 1:N] · [Zoom %] · [Geschoss] · [Ebene] · [Snap] … [Auswahl: n]`
|
||||
|
||||
- **Cursor X/Y/Z** — live aus der Plan-/3D-Position (Plan: aus viewBox-Inverse der Maus).
|
||||
- **Einheit** — m (aus Projekt). **Massstab** 1:N + **Zoom %** — aus der View-Transform.
|
||||
- **Aktives Geschoss** + **aktive Ebene** — aus Selection/State.
|
||||
- **Snap-Status** — aktive Fänge (später). **Auswahl: n** — Anzahl selektierter Objekte.
|
||||
- **Werkzeug-Hinweis** links — kontextueller Text des aktiven Werkzeugs.
|
||||
|
||||
### MVP jetzt
|
||||
- Cursor X/Y (im Grundriss), Einheit, Massstab 1:N, Zoom %, aktives Geschoss + Ebene.
|
||||
Snap/Werkzeug/Auswahl kommen mit den Zeichenwerkzeugen.
|
||||
|
||||
## Anbindung
|
||||
Beide Leisten docken an das Panel-/Dock-Layout an (Top über den Docks, Footer darunter),
|
||||
Inhalte rein aus dem Modell + View-State abgeleitet (keine Sonderzustände).
|
||||
@@ -0,0 +1,636 @@
|
||||
# Design — Prioritätsbasierte mehrschichtige Wand-Verschneidung (T/X)
|
||||
|
||||
> Teil der Standalone-Architektur — siehe [../../ARCHITECTURE.md](../../ARCHITECTURE.md).
|
||||
> Aufbauend auf der bestehenden **L-Ecken-Gehrung** in `src/model/joins.ts`
|
||||
> (`computeJoins`, `miterLine`) und `src/model/geometry.ts` (`clippedBand`,
|
||||
> `lineIntersect`). Plan-Verbraucher: `src/plan/generatePlan.ts` (`addWallPoche`).
|
||||
> Bezeichner **englisch**, Prosa deutsch, Einheiten **Meter**.
|
||||
|
||||
> **Hinweis Referenz:** Die im Auftrag genannte DOSSIER-Datei
|
||||
> `rhino/wand_grips.py` ist in dieser Umgebung **nicht vorhanden** (das einzige
|
||||
> auffindbare `dossier` ist ein Typst-Portfolio, kein Rhino-Plugin). Das Design
|
||||
> stützt sich daher auf (a) den bestehenden Code, (b) die in CONVENTIONS.md/elements.md
|
||||
> festgehaltenen Geometrie-Konventionen und (c) die etablierte Bau-Semantik der
|
||||
> prioritätsbasierten Schichtverschneidung (Vectorworks „Komponenten-Verbindung",
|
||||
> Revit „Layer Priority", ArchiCAD „Composite Priority"). Sobald `wand_grips.py`
|
||||
> verfügbar ist, sollte Abschnitt 7 (Priorität/Backbone) gegen DOSSIERs konkrete
|
||||
> Rangregel abgeglichen werden.
|
||||
|
||||
---
|
||||
|
||||
## 0. Problem & Leitbild
|
||||
|
||||
Heute (`computeJoins`) wird **nur die L-Ecke** behandelt: genau zwei Wandenden
|
||||
treffen sich in einem Knoten, eine gemeinsame **Gehrungslinie** (`miterLine`)
|
||||
schneidet beide Wände sauber. T-/X-Stöße (≥ 3 Enden) bleiben rechtwinklig
|
||||
gekappt (`if (ends.length !== 2) continue;`) — die durchgehende Wand wird von der
|
||||
ankommenden Wand nicht durchdrungen, und die Schichten überlappen sich oder
|
||||
klaffen.
|
||||
|
||||
**Ziel:** An jedem Knoten — L (2 Enden), T (3), X/Kreuz (4+) — soll **jede
|
||||
Schicht jeder Wand** genau bis zu der Fläche reichen, die ihre Verschneidungs-
|
||||
Semantik vorgibt:
|
||||
|
||||
- Die **gemeinsame, höchstpriorisierte Schicht** (Backbone, z. B. Stahlbeton)
|
||||
läuft **durch** den Stoß.
|
||||
- **Niederpriorisierte Schichten** (z. B. Putz, Dämmung) **stoßen** an die nächst-
|
||||
höhere Schicht des Nachbarn und **enden** dort.
|
||||
- Putz **läuft auf jeder Seite** bis zur Betonfläche und endet dort; Putz
|
||||
**überquert nie** den Beton (kanonisches Beispiel, siehe §7.1).
|
||||
|
||||
**Architektur-Prinzip (CONVENTIONS.md):** Das semantische Modell ist die einzige
|
||||
Wahrheit. Die Verschneidung ist eine **reine Ableitung** (`Project + Wall[]` →
|
||||
Trimm-Linien je Schicht-Band), nichts wird in die Geometrie eingebacken. Sie wird
|
||||
beim Generieren des Plans angewandt und ist **deterministisch** und **idempotent**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Geometrie-Konventionen (Wiederholung, verbindlich)
|
||||
|
||||
Aus CONVENTIONS.md und `geometry.ts`:
|
||||
|
||||
- Achsrichtung `u = normalize(end - start)`.
|
||||
- Wand-Normale `n = leftNormal(u) = (-u.y, u.x)`.
|
||||
- Schichten werden **außen → innen** gestapelt: Offset entlang `n` läuft von
|
||||
`-T/2` (linke/„außen"-Kante) nach `+T/2` (rechte/„innen"-Kante). Eine Schicht `k`
|
||||
belegt das Intervall `[off_k, off_k + thickness_k]` mit `off_0 = -T/2`.
|
||||
- Eine **unendliche Gerade** ist `Line { point, dir }`; Schnittpunkt zweier
|
||||
Geraden via `lineIntersect(a, da, b, db)` (null bei parallel).
|
||||
- Ein Schicht-**Band** zwischen Offsets `offA, offB` wird über `clippedBand(start,
|
||||
end, offA, offB, startCut, endCut)` gebildet; jede Bandlängskante wird mit einer
|
||||
optionalen Schnittlinie verschnitten statt rechtwinklig gekappt.
|
||||
|
||||
---
|
||||
|
||||
## 2. Datenmodell — was schon da ist, was neu kommt
|
||||
|
||||
### 2.1 Vorhanden (`types.ts`)
|
||||
|
||||
```ts
|
||||
interface Component { …; joinPriority: number; } // höher = läuft am Stoß durch
|
||||
interface Layer { componentId: string; thickness: number; }
|
||||
interface WallType { id; name; layers: Layer[]; } // layers: außen → innen
|
||||
interface Wall { start: Vec2; end: Vec2; wallTypeId; height; … }
|
||||
```
|
||||
|
||||
`joinPriority` ist bereits **pro Bauteil (Component)** definiert — genau richtig:
|
||||
Priorität ist eine **Material-Eigenschaft**, nicht eine Schicht-Eigenschaft. Damit
|
||||
verschneidet sich Beton mit Beton unabhängig vom Wandtyp.
|
||||
|
||||
### 2.2 Neue, abgeleitete Strukturen (keine neuen persistenten Felder)
|
||||
|
||||
Die heutige `WallCuts`-Struktur (eine Schnittlinie je **Wandende**) reicht für L
|
||||
(Gehrung) — aber **nicht** für T/X, weil dort verschiedene Schichten **verschiedene**
|
||||
Trimm-Linien brauchen (Beton läuft durch, Putz stoppt früher). Wir erweitern auf
|
||||
**pro Schicht, pro Ende** eine eigene Trimm-Linie:
|
||||
|
||||
```ts
|
||||
// src/model/joins.ts (erweitert)
|
||||
|
||||
/** Eine gerichtete Halbkante eines Wandendes an einem Knoten. */
|
||||
interface WallEnd {
|
||||
wallId: string;
|
||||
end: "start" | "end";
|
||||
}
|
||||
|
||||
/** Trimm-Linie + Klassifikation für GENAU EIN Schicht-Band an EINEM Ende. */
|
||||
interface LayerCut {
|
||||
/** Schnittgerade in Weltkoordinaten; null = rechtwinklig kappen (Default). */
|
||||
line: Line | null;
|
||||
/**
|
||||
* "miter" – Gehrung (L): geteilte Diagonale mit dem Nachbarn.
|
||||
* "butt" – Band stößt stumpf an eine Nachbarfläche (niedrigere Priorität).
|
||||
* "through" – Band läuft durch den Knoten (höchste Priorität / Backbone).
|
||||
* "square" – freies Ende, rechtwinklig (line === null).
|
||||
*/
|
||||
kind: "miter" | "butt" | "through" | "square";
|
||||
}
|
||||
|
||||
/** Pro Wandende: eine Trimm-Linie je Schicht-Index (parallel zu WallType.layers). */
|
||||
interface EndCuts {
|
||||
/** layerCuts[k] gilt für layers[k]; Länge === wt.layers.length. */
|
||||
layerCuts: LayerCut[];
|
||||
}
|
||||
|
||||
/** Ersetzt die alte WallCuts: jetzt schichtweise an beiden Enden. */
|
||||
interface WallJoin {
|
||||
start: EndCuts;
|
||||
end: EndCuts;
|
||||
}
|
||||
|
||||
export type JoinMap = Map<string /*wallId*/, WallJoin>;
|
||||
```
|
||||
|
||||
**Abwärtskompatibilität:** Die alte L-Gehrung ist der Spezialfall „alle
|
||||
`layerCuts[k].line` an einem Ende sind dieselbe `miter`-Linie". `clippedBand`
|
||||
bleibt unverändert; der Plan-Generator ruft es jetzt **je Schicht mit der
|
||||
schichtspezifischen Linie** auf (siehe §8).
|
||||
|
||||
### 2.3 Knoten-Repräsentation
|
||||
|
||||
```ts
|
||||
/** Ein Verschneidungsknoten: alle Wandenden, die sich (gerundet) berühren. */
|
||||
interface Junction {
|
||||
key: string; // roundKey(p)
|
||||
p: Vec2; // Knotenposition (Mittel der Enden)
|
||||
arms: Arm[]; // sortiert nach Außenwinkel (CCW)
|
||||
}
|
||||
|
||||
/** Ein „Arm" = ein Wandende, gesehen als Strahl, der vom Knoten WEGzeigt. */
|
||||
interface Arm {
|
||||
we: WallEnd;
|
||||
wall: Wall;
|
||||
/** Richtung VOM Knoten weg in die Wand hinein (immer normiert). */
|
||||
dirOut: Vec2; // = end==="start" ? +u : -u
|
||||
/** Außenwinkel atan2(dirOut.y, dirOut.x) für Sortierung. */
|
||||
angle: number;
|
||||
total: number; // Gesamtdicke der Wand
|
||||
/** Schicht-Profil, vom Knoten aus gesehen (siehe §3.2). */
|
||||
layers: ArmLayer[];
|
||||
}
|
||||
|
||||
/** Eine Schicht eines Arms, mit ihren beiden Längs-Flächengeraden am Knoten. */
|
||||
interface ArmLayer {
|
||||
index: number; // Index in wt.layers
|
||||
componentId: string;
|
||||
priority: number; // Component.joinPriority
|
||||
/** Offsets entlang n der Wand: [innerEdge..outerEdge] des Bandes. */
|
||||
off0: number; off1: number;
|
||||
/** Die zwei Längsflächen als Geraden (point am Knoten, dir = u der Wand). */
|
||||
faceA: Line; faceB: Line;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Knotenerkennung (Junction Detection)
|
||||
|
||||
### 3.1 Knoten clustern
|
||||
|
||||
Identisch zur heutigen Logik, nur ohne die `length !== 2`-Abbruchbedingung:
|
||||
|
||||
```
|
||||
function buildJunctions(walls): Junction[]
|
||||
map = Map<key, WallEnd[]>
|
||||
for w in walls:
|
||||
map.push(roundKey(w.start), {wallId:w.id, end:"start"})
|
||||
map.push(roundKey(w.end), {wallId:w.id, end:"end"})
|
||||
out = []
|
||||
for (key, ends) in map:
|
||||
if ends.length < 2: continue // freies Ende → alle Schichten "square"
|
||||
arms = ends.map(buildArm)
|
||||
arms.sort(by angle) // CCW um den Knoten
|
||||
out.push({ key, p: avgEndPoint(ends), arms })
|
||||
return out
|
||||
```
|
||||
|
||||
`roundKey` (vorhanden) gruppiert Endpunkte auf ein 0.1-mm-Gitter. **Wichtig für
|
||||
T-Stöße:** Bei einem echten T endet die ankommende Wand **auf der Achse** der
|
||||
durchgehenden Wand, nicht an deren Endpunkt. Solche Knoten werden über die
|
||||
Endpunkt-Gruppierung **nicht** gefunden, wenn die durchgehende Wand dort kein Ende
|
||||
hat. Daher zusätzlich (siehe §3.3) eine **Achs-Auf-Achs-Inzidenz**.
|
||||
|
||||
### 3.2 Arm-Schichtprofil (kanonische Orientierung)
|
||||
|
||||
Damit Schichten zweier Arme vergleichbar sind, muss jeder Arm seine Schichten in
|
||||
**konsistenter Welt-Orientierung** kennen. Wir speichern je Schicht ihre beiden
|
||||
**Längsflächen** als Geraden mit Stützpunkt am Knoten und Richtung `u`:
|
||||
|
||||
```
|
||||
function buildArm(we): Arm
|
||||
wall = byId(we.wallId); u = dirOf(wall); n = leftNormal(u)
|
||||
dirOut = we.end==="start" ? u : negate(u) // vom Knoten in die Wand
|
||||
j = we.end==="start" ? wall.start : wall.end
|
||||
total = wallTypeThickness(wt)
|
||||
off = -total/2
|
||||
layers = []
|
||||
for (k, layer) in wt.layers:
|
||||
off0 = off; off1 = off + layer.thickness
|
||||
faceA = { point: j + n*off0, dir: u }
|
||||
faceB = { point: j + n*off1, dir: u }
|
||||
layers.push({ index:k, componentId, priority, off0, off1, faceA, faceB })
|
||||
off = off1
|
||||
return { we, wall, dirOut, angle: atan2(dirOut), total, layers }
|
||||
```
|
||||
|
||||
### 3.3 T-Stoß-Erkennung (Achs-auf-Achs)
|
||||
|
||||
Zusätzlich zu Endpunkt-Clustern: ein Wandende `e` (Punkt `P`) bildet einen
|
||||
**T-Stoß** mit Wand `B`, wenn `P` (innerhalb Toleranz) auf der **Strecke** `B.start
|
||||
→ B.end` liegt, aber **nicht** auf deren Endpunkten:
|
||||
|
||||
```
|
||||
function findTeeIncidences(walls):
|
||||
for endpoint P of each wall A (as WallEnd e):
|
||||
for each wall B != A:
|
||||
if pointOnSegment(P, B.start, B.end, tol) and not nearEndpoint(P, B):
|
||||
// virtueller Knoten: A endet, B läuft durch.
|
||||
registerTee(P, armOf(e), passThroughWall=B)
|
||||
```
|
||||
|
||||
`B` wird hier als **durchgehender Strang** behandelt (kein Ende am Knoten); im
|
||||
Junction-Modell taucht `B` als zwei kollineare „Arme" auf (Richtung `+u` und
|
||||
`−u`), die der Prioritätsalgorithmus (§5) automatisch als „durchlaufend" erkennt
|
||||
(zwei kollineare Arme gleicher Wand → ihre Schichten enden nie gegeneinander).
|
||||
|
||||
**MVP-Vereinfachung (Phase 1, §10):** T-Stöße zunächst NUR über koinzidente
|
||||
Endpunkte (B hat dort tatsächlich einen Eckpunkt, z. B. weil die Wand dort geteilt
|
||||
wurde). Echte Achs-auf-Achs-T-Stöße (B durchgehend) folgen in Phase 3.
|
||||
|
||||
---
|
||||
|
||||
## 4. Winkel-Sektoren & Nachbar-Flächen
|
||||
|
||||
Für die Prioritätsauflösung muss man wissen, **welche Fläche eines Arms welcher
|
||||
Fläche des Nachbarn gegenübersteht**. Die Arme sind CCW nach `angle` sortiert.
|
||||
Zwischen zwei aufeinanderfolgenden Armen `arms[i]` und `arms[i+1]` (zyklisch) liegt
|
||||
ein **Sektor** (Keil). Jeder Sektor wird von **einer Längsfläche jedes der beiden
|
||||
Arme** begrenzt:
|
||||
|
||||
```
|
||||
arm[i+1]
|
||||
\ Sektor S_i
|
||||
\ /
|
||||
faceR(i+1)\ ___ faceL(i)
|
||||
X (Knoten)
|
||||
/
|
||||
/
|
||||
arm[i]
|
||||
```
|
||||
|
||||
Konvention: pro Arm hat die **äußere** Schicht (Index 0, Offset `-T/2`) die Fläche,
|
||||
die in den **CCW-vorausgehenden** Sektor zeigt; die **innere** Schicht den
|
||||
**nachfolgenden**. Konkret bestimmen wir die „dem Sektor zugewandte" Fläche jeder
|
||||
Schicht über das Vorzeichen von `cross(dirOut, sektorrichtung)` — analog zur
|
||||
`lbCloser`-Heuristik in `miterLine`, aber pro Sektor statt global.
|
||||
|
||||
```
|
||||
function sectorFaces(armLeft, armRight):
|
||||
// armRight ist CCW vor armLeft (Sektor liegt zwischen ihnen).
|
||||
// Wähle für jeden Arm die Schicht-Flächen, die in den Sektor zeigen.
|
||||
faceOf(arm, towards): pick faceA or faceB of each layer by sign of
|
||||
cross(arm.dirOut, towards - knoten)
|
||||
```
|
||||
|
||||
Diese Sektor-Sicht verallgemeinert die bestehende `miterLine`: bei genau zwei
|
||||
Armen (L) gibt es zwei Sektoren (innen/außen), und die Mittel-Gehrungslinie ergibt
|
||||
sich wie bisher aus dem Schnitt der gegenüberliegenden Außen- bzw. Innenflächen.
|
||||
|
||||
---
|
||||
|
||||
## 5. Prioritätsauflösung — das Kernstück
|
||||
|
||||
### 5.1 Idee
|
||||
|
||||
An einem Knoten konkurrieren Schichten verschiedener Arme um denselben Raum. Regel:
|
||||
|
||||
> Eine Schicht **läuft durch** (`through`), wenn sie zur **höchsten am Knoten
|
||||
> präsenten Priorität** gehört **und** auf der „gegenüberliegenden" Seite eine
|
||||
> Schicht **gleichen Materials** (oder ≥ gleicher Priorität) existiert, an die sie
|
||||
> nahtlos anschließt. Andernfalls **stößt** sie (`butt`) an die nächsthöher-
|
||||
> priorisierte Nachbarfläche und endet dort.
|
||||
|
||||
Praktisch lösen wir das **fläche-gegen-fläche** je Schicht: Für jede Schicht `L`
|
||||
eines Arms suchen wir die **Trimm-Fläche**, die ihr Band beendet. Das ist die
|
||||
**erste** (vom Knoten aus, entlang `dirOut`) der gegenüberliegenden Flächen mit
|
||||
**echt höherer Priorität**; existiert keine, läuft die Schicht bis zur
|
||||
Knoten-Mittelachse durch.
|
||||
|
||||
### 5.2 Pro Schicht: Trimm-Fläche finden
|
||||
|
||||
```
|
||||
function resolveLayerCut(arm, layer, junction): LayerCut
|
||||
// Kandidaten: alle Schichten ALLER ANDEREN Arme, deren Band den
|
||||
// Halbraum dieses Layers überlappt (Offset-Überlapp im gemeinsamen
|
||||
// Sektor) UND deren Priorität die Trimm-Entscheidung bestimmt.
|
||||
candidates = []
|
||||
for other in junction.arms where other.wall.id-end != arm.id-end:
|
||||
for ol in other.layers:
|
||||
if overlapsInSector(layer, ol, arm, other):
|
||||
candidates.push({ other, ol, face: facingFace(ol, towards arm) })
|
||||
|
||||
// Höchste am Knoten präsente Priorität (global, für "through").
|
||||
maxPrio = max over all candidate.ol.priority and layer.priority
|
||||
|
||||
if layer.priority == maxPrio and hasCollinearSamePrio(arm, layer, junction):
|
||||
// Backbone: läuft durch bis zur Mittel-/Gehrungslinie.
|
||||
return { line: miterMidline(arm, layer, junction), kind: "through" }
|
||||
|
||||
// Sonst: stoße an die NÄCHSTE Fläche höherer Priorität entlang dirOut.
|
||||
blockers = candidates.filter(c => c.ol.priority > layer.priority)
|
||||
if blockers.empty:
|
||||
// niemand höher → Gehrung mit dem Nachbarn gleicher Stufe (L-Fall)
|
||||
return { line: miterMidline(arm, layer, junction), kind: "miter" }
|
||||
|
||||
nearest = argmin over blockers of distanceAlong(dirOut, face)
|
||||
return { line: nearest.face, kind: "butt" }
|
||||
```
|
||||
|
||||
Hilfsbegriffe:
|
||||
|
||||
- `overlapsInSector(layer, ol, …)` — projiziert beide Bänder auf die **Normale des
|
||||
Sektors** und prüft Intervall-Überlapp `[off0,off1] ∩ [off0',off1'] ≠ ∅`. Nur
|
||||
überlappende Bänder können sich gegenseitig trimmen.
|
||||
- `facingFace(ol, towards arm)` — die der `arm`-Seite zugewandte Längsfläche von
|
||||
`ol` (die `faceA`/`faceB` von §3.2), als `Line`.
|
||||
- `distanceAlong(dirOut, face)` — Abstand des Schnittpunkts `faceLine ∩ layerAxis`
|
||||
vom Knoten, gemessen entlang `dirOut`. Negative bzw. hinter dem Knoten liegende
|
||||
Treffer werden verworfen (eine Trimm-Fläche muss **vor** dem Band liegen).
|
||||
- `miterMidline(...)` — die gemeinsame Diagonale für gleichrangige Begegnung; für
|
||||
zwei Arme exakt die heutige `miterLine`.
|
||||
- `hasCollinearSamePrio(...)` — true, wenn auf der „anderen Seite" des Knotens eine
|
||||
Schicht **gleichen Materials/Priorität** existiert, in die das Band nahtlos
|
||||
übergeht (Backbone-Kontinuität, §7).
|
||||
|
||||
### 5.3 Resultat in `LayerCut.line` schreiben
|
||||
|
||||
`resolveLayerCut` liefert für `layers[k]` eine `Line | null`. Diese wird in
|
||||
`WallJoin[end].layerCuts[k]` abgelegt. `clippedBand` kappt damit **dieses eine
|
||||
Band** an genau dieser Linie — alle anderen Schichten derselben Wand können andere
|
||||
Linien (oder `null`) haben. Das ist die ganze Verkabelung zum Renderer.
|
||||
|
||||
---
|
||||
|
||||
## 6. Master-Pseudocode `computeJoins` (neu)
|
||||
|
||||
```
|
||||
function computeJoins(project, walls): JoinMap
|
||||
result = init each wall → { start:{layerCuts:[square…]}, end:{layerCuts:[square…]} }
|
||||
junctions = buildJunctions(walls) // §3.1
|
||||
// (Phase 3: junctions += findTeeIncidences(walls)) // §3.3
|
||||
|
||||
for J in junctions:
|
||||
if J.arms.length == 1: continue // freies Ende: alles "square"
|
||||
if J.arms.length == 2:
|
||||
resolveL(J, project, result) // bestehende Gehrung, schichtweise
|
||||
else:
|
||||
resolveTorX(J, project, result) // §5, pro Arm pro Schicht
|
||||
return result
|
||||
|
||||
function resolveTorX(J, project, result):
|
||||
for arm in J.arms:
|
||||
cuts = []
|
||||
for layer in arm.layers:
|
||||
cuts[layer.index] = resolveLayerCut(arm, layer, J) // §5.2
|
||||
writeEnd(result, arm.we, cuts) // start oder end
|
||||
|
||||
function resolveL(J, project, result):
|
||||
[a, b] = J.arms
|
||||
// Wie heute: eine gemeinsame Gehrungslinie pro Sektor; aber wenn die
|
||||
// Dicken/Schichten ungleich sind, kann pro Schicht über §5.2 verfeinert werden.
|
||||
// MVP: eine miterLine für alle Schichten beider Arme (= heutiges Verhalten,
|
||||
// schichtweise dupliziert).
|
||||
line = miterLine(project, a.wall, a.we.end, b.wall)
|
||||
for arm in [a,b]:
|
||||
cuts = arm.layers.map(_ => ({ line, kind:"miter" }))
|
||||
writeEnd(result, arm.we, cuts)
|
||||
```
|
||||
|
||||
So bleibt die **L-Ecke bit-genau wie heute** (Regressionssicherheit), und die
|
||||
neue Logik greift nur bei ≥ 3 Armen.
|
||||
|
||||
---
|
||||
|
||||
## 7. Prioritäts-Semantik im Detail (das Putz/Beton-Beispiel)
|
||||
|
||||
### 7.1 Kanonisches Beispiel
|
||||
|
||||
Wandtyp „Aussenwand" (außen → innen):
|
||||
|
||||
| k | Component | thickness | joinPriority |
|
||||
|---|------------|-----------|--------------|
|
||||
| 0 | Putz aussen| 0.02 | 1 |
|
||||
| 1 | Stahlbeton | 0.18 | 9 |
|
||||
| 2 | Putz innen | 0.015 | 1 |
|
||||
|
||||
Drei solche Wände treffen in einem T-Knoten (zwei kollinear durchgehend „BB",
|
||||
eine ankommend „A"):
|
||||
|
||||
1. **Beton (prio 9)** der durchgehenden Wände `BB` läuft durch — beide
|
||||
Beton-Bänder sind kollinear gleicher Priorität → `hasCollinearSamePrio` =
|
||||
true → `through`. Der Beton von `A` (prio 9) **stößt** an die **Betonfläche**
|
||||
von `BB` (gegenüberliegende Fläche, gleiche höchste Priorität, aber kein
|
||||
kollinearer Partner für `A`) → `butt` an Betonaußenfläche von `BB`.
|
||||
2. **Putz (prio 1)** jeder Wand stößt an die **erste höherpriorisierte Fläche**
|
||||
entlang `dirOut`. Für die Putzschichten von `A` ist das die **Betonfläche von
|
||||
`BB`**: Putz **läuft auf jeder Seite bis zum Beton** und endet dort (`butt`).
|
||||
3. Putz **überquert nie** den Beton, weil eine `butt`-Trimm-Linie immer die
|
||||
**nächste** höhere Fläche ist — sie liegt vor dem Beton-Durchlauf.
|
||||
|
||||
Ergebnis exakt wie gefordert: *Beton durch, Putz schließt beidseitig an, Putz
|
||||
kreuzt Beton nicht.*
|
||||
|
||||
### 7.2 Warum Priorität pro Component (nicht pro Layer)
|
||||
|
||||
Beton trifft Beton → gleiche Priorität → nahtlos. Würde Priorität pro Schicht
|
||||
vergeben, müsste man sie pro Wandtyp neu pflegen. `joinPriority` am Component löst
|
||||
das materialweise — Stahlbeton hat **immer** 9, egal in welchem Wandtyp.
|
||||
|
||||
### 7.3 Backbone-Erkennung für „through"
|
||||
|
||||
`hasCollinearSamePrio(arm, layer, junction)`:
|
||||
|
||||
```
|
||||
for other in junction.arms where other != arm:
|
||||
if collinear(arm.dirOut, other.dirOut) (antiparallel, gleiche Achse) and
|
||||
other has a layer ol with ol.priority == layer.priority and
|
||||
bandsAlign(layer, ol): // gleiche Offsets relativ zur gemeinsamen Achse
|
||||
return true
|
||||
return false
|
||||
```
|
||||
|
||||
Nur wenn das Band auf der **gegenüberliegenden** Achse einen passenden Partner
|
||||
gleicher Priorität und Lage hat, läuft es wirklich **durch**. Sonst (z. B. der
|
||||
ankommende Beton von `A`) **stößt** es an — exakt das gewünschte Verhalten.
|
||||
|
||||
---
|
||||
|
||||
## 8. Layer-Wrapping (Umschlagen niederpriorisierter Schichten)
|
||||
|
||||
Ein subtiler, aber wichtiger Fall: An einem T-Stoß endet die **innere** Putzschicht
|
||||
der ankommenden Wand `A` an der Betonfläche von `BB`. Damit der Putz **um die Ecke
|
||||
herum sichtbar bleibt** (auf der Innenseite der durchgehenden Wand zieht der innere
|
||||
Putz von `BB` durch), ist nichts Zusätzliches nötig — `BB`s innerer Putz läuft als
|
||||
eigenes durchgehendes Band weiter.
|
||||
|
||||
Wo Wrapping aktiv nötig ist: wenn eine **niederpriorisierte Außenschicht** an einer
|
||||
Ecke **um eine höhere Schicht herumgeführt** werden soll (z. B. Dämmung, die außen
|
||||
um die Stütze läuft). Modellierung:
|
||||
|
||||
- Standard (MVP): kein Wrapping — jede Schicht endet an ihrer Trimm-Fläche
|
||||
(`butt`). Das deckt T/X korrekt ab.
|
||||
- Optional (Phase 4): Ein `wrap`-Flag pro Component (`Component.wrapAtEnds?:
|
||||
boolean`). Ist es gesetzt, erzeugt der Generator an der Trimm-Fläche ein
|
||||
**zusätzliches kurzes Stirnband** quer (Offset-Intervall der Schicht, Länge =
|
||||
Tiefe bis zur nächsten Fläche), sodass die Schicht ihre eigene Stirnseite
|
||||
„umschließt". Geometrisch ein weiteres `clippedBand` mit getauschten Achsen.
|
||||
|
||||
Wrapping ist bewusst **nachgelagert**; es ändert die Trimm-Logik (§5) nicht,
|
||||
sondern fügt nur Zusatzpolygone hinzu.
|
||||
|
||||
---
|
||||
|
||||
## 9. Anbindung an den Plan-Generator (`generatePlan.addWallPoche`)
|
||||
|
||||
Heute ruft `addWallPoche` `clippedBand(p1, p2, off, off+thickness, startCut,
|
||||
endCut)` mit **einer** `WallCuts` je Ende. Neu:
|
||||
|
||||
```ts
|
||||
// joins: JoinMap (neu)
|
||||
const join = joins.get(wall.id) ?? emptyJoin(wt.layers.length);
|
||||
…
|
||||
let off = -total / 2;
|
||||
wt.layers.forEach((layer, k) => {
|
||||
const startCut = (s <= 1e-6) ? join.start.layerCuts[k].line : null;
|
||||
const endCut = (e >= axisLen - 1e-6) ? join.end.layerCuts[k].line : null;
|
||||
out.push({ kind:"polygon",
|
||||
pts: clippedBand(p1, p2, off, off + layer.thickness, startCut, endCut),
|
||||
fill: comp.color, hatch: resolveHatch(...), … });
|
||||
off += layer.thickness;
|
||||
});
|
||||
```
|
||||
|
||||
- Die **Umrisslinie** über die volle Dicke (`-T/2..+T/2`) nutzt für ihre beiden
|
||||
Kanten die Cuts der **äußersten** bzw. **innersten** Schicht — oder wird, falls
|
||||
Schichten unterschiedlich getrimmt sind, durch eine **abgeleitete Outline**
|
||||
(Vereinigung der Schicht-Polygone, §11) ersetzt. MVP: weiterhin volle-Dicke-Band
|
||||
mit den Cuts von Schicht 0 / letzter Schicht.
|
||||
- `grob`-Detailgrad nutzt nur die **Backbone-Schicht** → ihr `through`/`miter`-Cut
|
||||
für die ganze Sammelfläche (entspricht der bestehenden `backboneColor`-Logik).
|
||||
|
||||
**Keine Signaturänderung an `clippedBand`** — es bleibt der zentrale Trimmer; wir
|
||||
füttern es nur schichtweise mit verschiedenen Linien.
|
||||
|
||||
---
|
||||
|
||||
## 10. 2D-analytisch jetzt vs. exakte 3D-Booleans später (OCCT)
|
||||
|
||||
### 10.1 Jetzt — 2D-analytisch (dieses Design)
|
||||
|
||||
- **Plan/Grundriss** ist eine reine 2D-Ableitung (vgl. `generatePlan.ts`-Kopf:
|
||||
„nicht durch Zerschneiden eines 3D-Meshes, sondern direkt aus den Parametern").
|
||||
- Trimmen = **Geraden-Schnitt** (`lineIntersect`) + Polygon-Kappen (`clippedBand`).
|
||||
Kein Polygon-Boolean nötig, solange Schichten als **konvexe Bänder** mit
|
||||
schrägen Stirnflächen modelliert werden. Das ist exakt, schnell (O(Arme² ·
|
||||
Schichten) je Knoten), und deterministisch.
|
||||
- **3D-Wand** im Viewport: Extrusion der getrimmten 2D-Schichtpolygone in `z`
|
||||
(Höhe). Damit stimmen Plan und 3D ohne separaten Kernel überein.
|
||||
|
||||
Grenzen der 2D-Analytik: nicht-konvexe Stirnprofile, echte Materialdurchdringung in
|
||||
`z` (z. B. wenn Wände unterschiedlicher Höhe / Sturz / Brüstung interagieren),
|
||||
Verschneidung Wand × Decke × Stütze. Das braucht echte Volumen-Booleans.
|
||||
|
||||
### 10.2 Später — exakte 3D-Booleans (OCCT / opencascade.js)
|
||||
|
||||
- **Bibliothek:** `opencascade.js` (WASM-Port von OCCT) — `BRepAlgoAPI_Cut/Common`,
|
||||
`BRepPrimAPI_MakePrism` für Extrusionen. Alternativ `manifold-3d` (schneller,
|
||||
robuster für reine Mesh-Booleans, aber ohne B-Rep/Fillets).
|
||||
- **Modell bleibt gleich:** Priorität (`joinPriority`) bestimmt die **Cut-
|
||||
Reihenfolge**. Algorithmus: baue je Schicht einen über-langen Solid (Band ×
|
||||
Höhe, über den Knoten hinaus verlängert); subtrahiere von jeder niederpriori-
|
||||
sierten Schicht die Solids **aller höherpriorisierten** Schichten am Knoten
|
||||
(`result = layerSolid − ⋃ higherPrioritySolids`). Gleiche Priorität: kein Cut
|
||||
(Beton bleibt an Beton). Das ist die **3D-Verallgemeinerung exakt derselben
|
||||
Prioritätsregel** — die 2D-`butt`/`through`-Klassifikation aus §5 ist die
|
||||
2D-Projektion dieser Boolean-Reihenfolge.
|
||||
- Die Datenstrukturen (§2) bleiben unverändert; nur der **Trimmer** wird
|
||||
ausgetauscht (`clippedBand` → OCCT-Cut). Deshalb ist das 2D-Design bereits
|
||||
„OCCT-ready".
|
||||
|
||||
---
|
||||
|
||||
## 11. Robuste Outline (optional, Phase 4)
|
||||
|
||||
Wenn Schichten an einem Knoten unterschiedlich getrimmt sind, ist die „volle
|
||||
Dicke"-Umrisslinie nicht mehr ein einfaches Band. Saubere Lösung: die
|
||||
**Wand-Outline** als **Vereinigung aller Schicht-Polygone** berechnen
|
||||
(Polygon-Boolean in 2D). Empfohlene Library: **`polygon-clipping`** (Martinez-
|
||||
Rueda, robust, klein) oder **`@flatten-js/boolean-op`**. Damit wird der Umriss
|
||||
exakt die Außenkontur der getrimmten Bänder — auch bei X-Knoten. MVP verzichtet
|
||||
darauf und nutzt die Schicht-0/N-Cuts (visuell für die meisten Fälle ausreichend).
|
||||
|
||||
---
|
||||
|
||||
## 12. Edge Cases
|
||||
|
||||
1. **Gleiche Priorität, nicht kollinear** (z. B. zwei Betonwände treffen im rechten
|
||||
Winkel im T): kein „through" (kein kollinearer Partner), aber auch kein
|
||||
`butt`-Blocker höherer Priorität → Fallback **`miter`** (gemeinsame Gehrung wie
|
||||
im L-Fall). Bei drei gleichrangigen Armen: paarweise Gehrung pro Sektor; die
|
||||
resultierenden Stirnflächen sind die Sektor-Halbierenden.
|
||||
2. **Kollinear (180°)** — zwei Wände in einer Linie: `lineIntersect` liefert null
|
||||
(parallel). Behandlung wie heute: **kein Schnitt**, Bänder laufen gerade durch
|
||||
(bei gleichem Typ nahtlos). Bei ungleichem Typ: die schmalere Wand stößt an die
|
||||
breitere; pro Schicht über §5 auflösbar.
|
||||
3. **> 2 Schichten / asymmetrische Wandtypen**: Der Algorithmus ist in der Schicht-
|
||||
anzahl generisch (`resolveLayerCut` läuft je Schicht-Index). Treffen Wände
|
||||
verschiedener Typen (verschiedene Schichtzahl/-dicke), entscheidet **nur die
|
||||
Priorität pro Fläche**, nicht der Index — deshalb arbeiten wir mit
|
||||
`ArmLayer.faceA/faceB` (Welt-Flächen), nicht mit Schicht-Indizes über Wände
|
||||
hinweg.
|
||||
4. **Backbone fehlt** (alle Prioritäten gleich): degeneriert sauber zu reinen
|
||||
Gehrungen (Fall 1).
|
||||
5. **Sehr spitze Winkel**: Trimm-Schnittpunkte können weit vom Knoten wegwandern.
|
||||
Begrenzung: `distanceAlong` auf `≤ maxReach` (z. B. `3 · total`) clampen; sonst
|
||||
rechtwinklig kappen (`square`), um Artefakte zu vermeiden.
|
||||
6. **Öffnungen am Wandende** (`addWallPoche`-Segmentierung): Cuts gelten nur für das
|
||||
echte Wandende (`s≤ε` / `e≥len−ε`), wie heute. Türnahe Segmentenden bleiben
|
||||
`square`.
|
||||
7. **Numerische Knoten-Toleranz**: `roundKey`-Gitter (0.1 mm) und ε in
|
||||
`pointOnSegment` müssen konsistent sein, sonst „flackernde" Knoten. Toleranz
|
||||
zentral als Konstante (`JOIN_EPS = 1e-4` m).
|
||||
8. **Mehr als 2 Wände gleicher Achse** (degenerierter X, alle kollinear): als ein
|
||||
durchgehender Strang behandeln; Prioritätsregel pro Schicht greift normal.
|
||||
|
||||
---
|
||||
|
||||
## 13. Staged Build-Plan
|
||||
|
||||
**Phase 1 — Single-Layer T (Fundament).**
|
||||
- `JoinMap`/`WallJoin`/`EndCuts`/`LayerCut` einführen; `WallCuts` darin als
|
||||
Spezialfall abbilden. `clippedBand` unverändert.
|
||||
- `buildJunctions` ohne `length!==2`-Abbruch; L-Fall ruft bestehende `miterLine`
|
||||
(schichtweise dupliziert) → **Regressionsgleichheit** zu heute.
|
||||
- T-Knoten **nur über koinzidente Endpunkte** (§3.3 MVP). Für **einschichtige**
|
||||
Wände: die durchgehende Wand läuft durch, die ankommende stößt rechtwinklig an
|
||||
deren nächste Fläche. Verifizieren: `npx tsc -b`, `npm run build`,
|
||||
`node scripts/probe.mjs`, Screenshot prüfen.
|
||||
|
||||
**Phase 2 — Prioritäts-Auflösung mehrschichtig (T).**
|
||||
- `ArmLayer`-Profil (§3.2), Sektor-Flächen (§4), `resolveLayerCut` (§5) inkl.
|
||||
`through`/`butt`/`miter`-Klassifikation und `hasCollinearSamePrio`.
|
||||
- `addWallPoche` schichtweise verkabeln (§9). Putz/Beton-Testszene (§7.1) anlegen
|
||||
und visuell prüfen.
|
||||
|
||||
**Phase 3 — X-Knoten & echte Achs-T-Stöße.**
|
||||
- `findTeeIncidences` (Achs-auf-Achs, §3.3): durchgehende Wand wird nicht geteilt.
|
||||
- 4+-Arm-Sektorlogik vollständig; Edge Cases 1/5/8 absichern.
|
||||
|
||||
**Phase 4 — Politur.**
|
||||
- Robuste Outline via Polygon-Boolean (§11); optionales Layer-Wrapping (§8,
|
||||
`Component.wrapAtEnds`).
|
||||
|
||||
**Phase 5 — Exakte 3D-Booleans (OCCT).**
|
||||
- `opencascade.js` integrieren; Trimmer-Interface so abstrahieren, dass 2D
|
||||
(`clippedBand`) und 3D (OCCT-Cut) dieselbe Prioritäts-Reihenfolge nutzen (§10.2).
|
||||
Plan bleibt 2D-analytisch; nur das 3D-Volumen nutzt Booleans.
|
||||
|
||||
---
|
||||
|
||||
## 14. Trimmer-Abstraktion (für §10.2-Migration)
|
||||
|
||||
Damit Phase 5 nicht den Aufrufer ändert, kapseln wir das Trimmen hinter einer
|
||||
Schnittstelle. 2D nutzt `clippedBand`; 3D nutzt OCCT — beide konsumieren dieselbe
|
||||
`JoinMap`.
|
||||
|
||||
```ts
|
||||
interface LayerTrimmer {
|
||||
/** 2D: getrimmtes Bandpolygon. 3D-Variante liefert stattdessen einen Solid. */
|
||||
trimBand(p1: Vec2, p2: Vec2, offA: number, offB: number,
|
||||
startCut: Line | null, endCut: Line | null): Vec2[];
|
||||
}
|
||||
```
|
||||
|
||||
Die Prioritätslogik (`computeJoins`) bleibt der **gemeinsame, kernel-unabhängige**
|
||||
Kopf; nur der `LayerTrimmer` wird ausgetauscht. Das hält das Design dem
|
||||
Architektur-Prinzip treu: ein semantisches Modell, viele abgeleitete Ansichten.
|
||||
@@ -0,0 +1,566 @@
|
||||
# Swisstopo-Geodaten & SIA-Flächenstandards im Browser-BIM
|
||||
|
||||
> Stand: 2026-06-29 · Recherche für das Standalone-Browser-BIM (React + TS + Three.js),
|
||||
> Port von **DOSSIER** (Rhino-Plugin). Ziel: (A) Schweizer Geodaten (Höhenmodell,
|
||||
> Orthofoto, 3D-Gebäude, Parzellen) direkt im Browser laden, (B) Standort-Kontext
|
||||
> (Gelände + Parzelle + Nachbargebäude) für ein Projekt importieren, (C) SIA-416-
|
||||
> Flächen/Volumen + Raumschemata berechnen wie in DOSSIER.
|
||||
>
|
||||
> Bezug zur ROADMAP: Swisstopo/Terrain/OSM = **Phase 4** (Kontext/Daten); SIA-416-
|
||||
> Räume + Bilanz-CSV = **Phase 2**. Beide sind dort bereits als ⭐-Features gelistet.
|
||||
|
||||
**Alle in diesem Dokument genannten geo.admin.ch-Endpunkte wurden am 2026-06-29 live
|
||||
gegen die echte API getestet** (curl + CORS-Header-Check). Wo „verifiziert" steht,
|
||||
liegt eine echte Antwort vor.
|
||||
|
||||
---
|
||||
|
||||
## Teil A — Swisstopo-APIs & Dienste aus dem Browser
|
||||
|
||||
### A.0 Das Wichtigste vorweg: CORS & Lizenz
|
||||
|
||||
Zwei Fragen entscheiden, ob ein Dienst *ohne Backend-Proxy* aus einer reinen
|
||||
Browser-App nutzbar ist: CORS und Lizenz. Beide sind hier günstig.
|
||||
|
||||
**CORS (live verifiziert):** Alle relevanten Hosts senden `access-control-allow-origin: *`:
|
||||
|
||||
| Host | Dienst | CORS | Range-Requests |
|
||||
|---|---|---|---|
|
||||
| `api3.geo.admin.ch` | REST (height, profile, identify, find, search) | ✅ `*` | — |
|
||||
| `data.geo.admin.ch` | STAC-API + Daten-Assets (COG-GeoTIFF, XYZ.zip) | ✅ `*` | ✅ `206 Partial Content`, `accept-ranges`/`content-range` vorhanden |
|
||||
| `wmts.geo.admin.ch` | WMTS-Kacheln | ✅ `*` | — |
|
||||
| `3d.geo.admin.ch` | 3D-Tiles (`tileset.json` + glTF) | ✅ `*` | — |
|
||||
|
||||
→ **Konsequenz:** Höhenabfrage, Geocoding, Parzellen-Identify, Karten-/Orthofoto-
|
||||
Kacheln, **COG-GeoTIFF-Höhenmodell per Range-Request** und 3D-Tiles sind **direkt aus
|
||||
dem Browser ohne eigenen Proxy** abrufbar. Das ist ein großer Vorteil gegenüber vielen
|
||||
anderen nationalen Geodiensten.
|
||||
|
||||
**Lizenz:** swisstopo/geo.admin.ch ist **Open Government Data**: „The acquisition and
|
||||
use of data or services is free of charge, subject to the provisions on fair use."
|
||||
Kommerzielle Nutzung ist erlaubt, Einbindung in (auch kommerzielle) Web-Apps explizit
|
||||
gedeckt. Pflicht-Attribution: **`© swisstopo`** (bzw. „© Data: swisstopo"). „Fair use"
|
||||
= z.B. Web-App mit Ø 20'000 Nutzern/Tag ok; aggressives Bot-Scraping vermeiden. Haftung
|
||||
ausgeschlossen, ~98% Verfügbarkeit. [Terms of use FSDI](https://www.geo.admin.ch/en/general-terms-of-use-fsdi)
|
||||
|
||||
> ⚠️ **Korrektur zu DOSSIER & zur Doku:** Die offizielle REST-Doku notiert beim
|
||||
> *Height*-Service „This service is not freely accessible (fee required)". Das ist
|
||||
> **in der Praxis falsch / veraltet**: Der Endpunkt antwortet anonym, ohne Key, mit
|
||||
> `200` und CORS `*` (verifiziert, siehe A.1). DOSSIERs Aussage „alle APIs offen, ohne
|
||||
> Auth, ohne Key" deckt sich mit der gemessenen Realität. Wir verlassen uns aber nicht
|
||||
> blind darauf, sondern behandeln 402/429 defensiv (Retry/Backoff, Cache).
|
||||
|
||||
### A.1 Höhenabfrage — Height-Service (Einzelpunkt)
|
||||
|
||||
Punkt-Höhe (DTM) aus swissALTI3D/DTM. **Verifiziert:**
|
||||
|
||||
```
|
||||
GET https://api3.geo.admin.ch/rest/services/height?easting=2600000&northing=1200000&sr=2056
|
||||
→ {"height":"555.5"}
|
||||
```
|
||||
|
||||
Parameter:
|
||||
- `easting`, `northing` — LV95 (`sr=2056`) oder LV03 (`sr=21781`). **Pflicht.**
|
||||
- `sr` — `2056` (LV95) angeben, sonst Default `21781`.
|
||||
- `elevation_model` — `DTM2` (= swissALTI3D, 2 m), `DTM25` (Default), `COMB`.
|
||||
(Im Tal lieferten DTM2/DTM25/COMB denselben Wert; im Steilgelände kann DTM2 genauer sein.)
|
||||
- `callback` — JSONP (brauchen wir wegen CORS nicht).
|
||||
|
||||
Nutzung im Tool: **Projekt-Nullpunkt-Z** bzw. „Gebäude auf Gelände setzen" — eine
|
||||
einzelne Höhe an der Projekt-Koordinate. Antwortzeit ~50–150 ms. Quelle:
|
||||
[GeoAdmin REST – Height](https://geoadmin.readthedocs.io/en/latest/services/sdiservices.html)
|
||||
|
||||
### A.2 Höhenprofil — Profile-Service (Schnittlinie)
|
||||
|
||||
Höhen entlang einer Polylinie — ideal für **Geländeschnitt** unter einem
|
||||
Gebäude-Schnitt. **Verifiziert** (echte Werte zurück):
|
||||
|
||||
```
|
||||
GET https://api3.geo.admin.ch/rest/services/profile.json
|
||||
?geom={"type":"LineString","coordinates":[[2600000,1200000],[2600200,1200000]]}
|
||||
&sr=2056&nb_points=3
|
||||
→ [{"alts":{"COMB":555.5,"DTM2":555.5,"DTM25":555.5},"dist":0,"easting":2600000,"northing":1200000},
|
||||
{"alts":{...},"dist":100,...}, {"dist":200,...}]
|
||||
```
|
||||
|
||||
Parameter: `geom` (GeoJSON-LineString, max 6'000 Punkte), `sr`, `nb_points` (Anzahl
|
||||
Stützpunkte, Default 200), `elevation_models`, `offset` (Glättung). Auch als
|
||||
`profile.csv`. → Für 2D-Geländeschnitte **ohne** Mesh-Download. Quelle:
|
||||
[GeoAdmin REST – Profile](https://geoadmin.readthedocs.io/en/latest/services/sdiservices.html)
|
||||
|
||||
### A.3 swissALTI3D — Höhenmodell als COG-GeoTIFF (Mesh-Quelle) ⭐
|
||||
|
||||
Das ist der **Schlüssel für das Gelände-Mesh im Browser**. swissALTI3D ist das präzise
|
||||
DTM der Schweiz (ohne Vegetation/Bebauung), Auflösung 0.5 m / 2 m, alle 6 Jahre
|
||||
aktualisiert ([swissALTI3D](https://www.swisstopo.admin.ch/en/height-model-swissalti3d)).
|
||||
Bezug über die **STAC-API** (verifiziert — Tile `swissalti3d_2019_2599-1198`):
|
||||
|
||||
```
|
||||
GET https://data.geo.admin.ch/api/stac/v1/collections/ch.swisstopo.swissalti3d/items
|
||||
?bbox=<lonMin,latMin,lonMax,latMax>&limit=...
|
||||
```
|
||||
|
||||
Jedes 1×1-km-Tile liefert pro Auflösung **zwei** Asset-Typen:
|
||||
|
||||
| Asset | Typ | Browser-tauglich? |
|
||||
|---|---|---|
|
||||
| `..._0.5_2056_5728.tif` / `..._2_2056_5728.tif` | **Cloud-Optimized GeoTIFF**, **EPSG:2056** | ✅ **direkt** via `geotiff.js` + Range |
|
||||
| `..._0.5_2056_5728.xyz.zip` / `..._2_..._xyz.zip` | ASCII-XYZ (E N Z) in ZIP | ✅ via `fflate` entpacken (so macht es DOSSIER) |
|
||||
|
||||
**Verifiziert:** `data.geo.admin.ch` liefert auf das `.tif` ein `206 Partial Content`
|
||||
mit `content-range` bei `Range:`-Header und CORS `*`. Das bedeutet: **`geotiff.js`
|
||||
liest nur den benötigten Ausschnitt eines COG per HTTP-Range, ohne das ganze File zu
|
||||
laden** — perfekt für eine Browser-App. Bbox in WGS84 für STAC, Tile-Daten dann in
|
||||
LV95-Metern (kein Reprojizieren der Z-Werte nötig). Quelle:
|
||||
[STAC tech docs](https://docs.geo.admin.ch/) · COG-Tile live geprüft.
|
||||
|
||||
### A.4 SWISSIMAGE / Karten — WMTS-Kacheln
|
||||
|
||||
Orthofoto (10 cm) und Landeskarten als Kacheln. RESTful-URL-Template:
|
||||
|
||||
```
|
||||
https://wmts.geo.admin.ch/1.0.0/<Layer>/default/<Time>/<TileMatrixSet>/<z>/<TileCol>/<TileRow>.<ext>
|
||||
```
|
||||
|
||||
Beispiel-Layer:
|
||||
- `ch.swisstopo.swissimage` — Orthofoto, `.jpeg`
|
||||
- `ch.swisstopo.pixelkarte-farbe` — Landeskarte farbig, `.jpeg`
|
||||
- `ch.kantone.cadastralwebmap-farbe` — **Katasterplan (AV)**, `.png`
|
||||
|
||||
TileMatrixSets: **`2056` (LV95)**, `21781`, `3857` (Web-Mercator), `4326`. Zoom 0–28
|
||||
(4000 m → 0.1 m); Zoom 27/28 nur für wenige Layer (swissimage, Kataster). Für 3D in
|
||||
Three.js am einfachsten **`3857`** (Standard-Slippy-Map-Schema, z/x/y), z.B.
|
||||
|
||||
```
|
||||
https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swissimage/default/current/3857/{z}/{x}/{y}.jpeg
|
||||
```
|
||||
|
||||
Für planimetrisch exakte 2D-Arbeit besser **`2056`**. CORS `*` (verifiziert). Quellen:
|
||||
[WMTS docs](https://docs.geo.admin.ch/visualize-data/wmts.html) ·
|
||||
[WMTS service](https://wmts.geo.admin.ch/) ·
|
||||
[WMTS EPSG:2056 CodePen](https://codepen.io/geoadmin/pen/GZKEam).
|
||||
|
||||
> Es gibt zusätzlich klassisches **WMS** (`https://wms.geo.admin.ch/`, GetMap mit
|
||||
> beliebiger BBox/Größe, ebenfalls EPSG:2056). Für ein einzelnes georeferenziertes
|
||||
> Orthofoto-Rechteck unter dem Modell ist ein WMS-GetMap manchmal praktischer als
|
||||
> WMTS-Kacheln zu stitchen. [WMS docs](https://docs.geo.admin.ch/visualize-data/wms.html)
|
||||
|
||||
### A.5 swissBUILDINGS3D — Nachbargebäude (3D)
|
||||
|
||||
Zwei Wege:
|
||||
|
||||
**(a) 3D-Tiles (Streaming, Cesium-Format)** — `glTF`/`tileset.json`, für große Gebiete:
|
||||
```
|
||||
https://3d.geo.admin.ch/<Layer>/<Version>/<Time>/tileset.json
|
||||
```
|
||||
Layer u.a. `ch.swisstopo.swissbuildings3d.3d`, `ch.swisstopo.swisstlm3d.3d`,
|
||||
`ch.swisstopo.swissnames3d.3d`, `ch.swisstopo.vegetation.3d`. `Version` = `v1`, `Time`
|
||||
optional (ISO `YYYYMMDD`, weglassen = aktuellste). CORS `*` (verifiziert auf
|
||||
`tileset.json`). Direkt für CesiumJS gedacht; in **reinem Three.js** über
|
||||
`@loaders.gl/3d-tiles` oder den `3DTilesRendererJS` (NASA-AMMOS/`three.js`-Community)
|
||||
ladbar. [3D-Tiles docs](https://docs.geo.admin.ch/visualize-data/3d-tiles.html) ·
|
||||
[Switzerland in 3D](https://www.swisstopo.admin.ch/en/switzerland-in-3d)
|
||||
|
||||
**(b) STAC-Tiles als CAD/Mesh-Datei (Download pro Tile)** — so macht es DOSSIER:
|
||||
Collections `ch.swisstopo.swissbuildings3d_3_0` (neu; in Städten z.T. >700 MB Tiles)
|
||||
und `ch.swisstopo.swissbuildings3d_2` (1-km-Tiles, ~50 MB, stabil). Assets in
|
||||
`.dxf/.dwg/.obj/.ifc` (+ `.zip`), Varianten `solid`/`separated`. Für den **Import als
|
||||
echte, editierbare Massen** ins eigene Modell ist der OBJ/IFC-Tile-Weg besser als
|
||||
3D-Tiles (die sind read-only Visualisierung). swissBUILDINGS3D: >3 Mio Gebäude,
|
||||
Lage-/Höhengenauigkeit 30–50 cm.
|
||||
|
||||
→ **Empfehlung:** Für „Nachbarschaft als Kontext anzeigen" (Phase 4 Start) **3D-Tiles
|
||||
streamen** (kein Download, kein Parsing). Wenn der Nutzer Nachbargebäude als Geometrie
|
||||
*braucht* (Verschattung, Abstand), **STAC-OBJ-Tile** laden und als Mesh importieren.
|
||||
|
||||
### A.6 Parzelle / Kataster (AV) — Identify-Service ⭐
|
||||
|
||||
**Verifiziert** — Parzellen-Polygon aus einer Koordinate, in LV95:
|
||||
|
||||
```
|
||||
GET https://api3.geo.admin.ch/rest/services/api/MapServer/identify
|
||||
?geometry=2600423,1199521&geometryType=esriGeometryPoint
|
||||
&imageDisplay=100,100,96&mapExtent=2600323,1199421,2600523,1199621
|
||||
&tolerance=2&layers=all:ch.kantone.cadastralwebmap-farbe
|
||||
&returnGeometry=true&geometryFormat=geojson&sr=2056
|
||||
→ {"results":[{"type":"Feature","bbox":[...],
|
||||
"geometry":{"type":"Polygon","coordinates":[[ [2600377.1,1199523.7], ... ]]},
|
||||
"attributes":{"number":"698","egris_egrid":"CH507635214670","ak":"BE", ...}}]}
|
||||
```
|
||||
|
||||
- Parzellen-Layer: **`ch.kantone.cadastralwebmap-farbe`** (liefert `number`,
|
||||
`egris_egrid` = EGRID, Kanton; mit `returnGeometry=true&geometryFormat=geojson` das
|
||||
**Parzellen-Polygon in LV95-Metern** → direkt als Grundstücksgrenze importierbar).
|
||||
- Gebäudeadressen: `ch.swisstopo.amtliches-gebaeudeadressverzeichnis`; Gebäude-/
|
||||
Wohnungsregister `ch.bfs.gebaeude_wohnungs_register` (EGID).
|
||||
- Pflichtparameter: `geometry`, `geometryType` (`esriGeometryPoint|...Polygon|...Envelope`),
|
||||
`mapExtent`, `imageDisplay`, `tolerance`; `sr=2056`; max 50 Features/Request.
|
||||
|
||||
Quellen: [Identify features](https://docs.geo.admin.ch/access-data/identify-features.html) ·
|
||||
[GeoAdmin REST – Identify/Find](https://geoadmin.readthedocs.io/en/latest/services/sdiservices.html) ·
|
||||
Live-Antwort oben.
|
||||
|
||||
### A.7 Geocoding — SearchServer (Adresse → LV95)
|
||||
|
||||
**Verifiziert** (Adresse → Koordinate, deckt sich mit DOSSIERs `geocode()`):
|
||||
|
||||
```
|
||||
GET https://api3.geo.admin.ch/rest/services/api/SearchServer
|
||||
?searchText=Bundesplatz 3 Bern&type=locations&origins=address&sr=2056&limit=1
|
||||
→ results[0].attrs: { label:"Bundesplatz 3 <b>3011 Bern</b>", lat:46.94677, lon:7.44419,
|
||||
geom_st_box2d:"BOX(2600423.26 1199521.11, ...)", origin:"address", ... }
|
||||
```
|
||||
|
||||
- `type=locations`, `origins` aus `{address, parcel, gg25, gazetteer, zipcode, district,
|
||||
kantone}`, `sr=2056`. **Im LV95-Modus liefert die Geo-Admin-Konvention `y`=East,
|
||||
`x`=North** (DOSSIER liest genau so: `e=attrs.y`, `n=attrs.x`). Labels enthalten
|
||||
`<b>`-Tags (strippen).
|
||||
- `type=featuresearch` + `features=<layer>` durchsucht Attribute (z.B. Parzellennummer).
|
||||
|
||||
Quelle: [Search](https://docs.geo.admin.ch/access-data/search.html) · Live-Antwort oben.
|
||||
|
||||
### A.8 Koordinatensystem LV95 / EPSG:2056 & Transformationen
|
||||
|
||||
Intern rechnet das BIM-Tool in **Metern** (ROADMAP-Konvention) und verschiebt den
|
||||
Projekt-Ursprung nahe (0,0,0); LV95-Koordinaten sind ~2.6 Mio / 1.2 Mio Meter groß und
|
||||
würden bei `float32` (Three.js) zu **Jitter** führen → **Origin-Shift Pflicht** (siehe B.3).
|
||||
DOSSIER macht genau das (`origin_shift`/`shift_lv95`, typ. bbox-Center → 0/0/0).
|
||||
|
||||
Transformations-Optionen:
|
||||
|
||||
1. **`proj4` (npm `proj4@2.20.9`)** — universell, exakt. EPSG:2056-Definition:
|
||||
```js
|
||||
proj4.defs("EPSG:2056",
|
||||
"+proj=somerc +lat_0=46.9524055555556 +lon_0=7.43958333333333 +k_0=1 "+
|
||||
"+x_0=2600000 +y_0=1200000 +ellps=bessel "+
|
||||
"+towgs84=674.374,15.056,405.346,0,0,0,0 +units=m +no_defs +type=crs");
|
||||
const [e,n] = proj4("EPSG:4326","EPSG:2056",[lon,lat]); // WGS84→LV95
|
||||
```
|
||||
Genauigkeit mit dieser 3-Parameter-`towgs84` ~1 m (für Kontext-Import völlig
|
||||
ausreichend). Types: `@types/proj4`. Quellen:
|
||||
[epsg.io/2056](https://epsg.io/2056) · [proj4js](https://github.com/proj4js/proj4js).
|
||||
|
||||
2. **Näherungsformeln (CH1903→WGS84, swisstopo)** — DOSSIERs Ansatz, ~1 m genau,
|
||||
**0 Dependencies** (zwei kleine Funktionen `lv95_to_wgs84`/`wgs84_to_lv95`).
|
||||
1:1 nach TS portierbar; gut, wenn man `proj4` nicht ziehen will. Reicht, weil
|
||||
STAC-Queries ohnehin nur eine grobe WGS84-Bbox brauchen und alle *Daten* schon in
|
||||
LV95 kommen.
|
||||
|
||||
3. **swisstopo REFRAME Web-API** — cm-genaue offizielle Umrechnung (LV95↔WGS84,
|
||||
LN02↔Bessel). Nur nötig, wenn Vermessungs-Genauigkeit verlangt wird. REST, online.
|
||||
[REFRAME Web](https://www.swisstopo.admin.ch/en/rest-api-geoservices-reframe-web)
|
||||
|
||||
**Empfehlung:** `proj4` mit fester EPSG:2056-Def (eine Abhängigkeit, exakt genug,
|
||||
wartungsarm) — oder, wenn Dependency-Geiz, DOSSIERs Formeln portieren. REFRAME nur bei
|
||||
Bedarf nachrüsten.
|
||||
|
||||
### A.9 Endpoint-Übersicht (Spickzettel)
|
||||
|
||||
| Zweck | Endpoint | Frei/CORS | Format |
|
||||
|---|---|---|---|
|
||||
| Punkt-Höhe | `api3…/rest/services/height` | ✅ ✅ | JSON |
|
||||
| Höhenprofil (Schnitt) | `api3…/rest/services/profile.json` | ✅ ✅ | JSON/CSV |
|
||||
| Gelände-Mesh (COG) | STAC `…/swissalti3d/items` → `.tif` (COG, 2056) | ✅ ✅ Range | GeoTIFF |
|
||||
| Gelände (ASCII) | STAC `…/swissalti3d` → `.xyz.zip` | ✅ ✅ | XYZ in ZIP |
|
||||
| Orthofoto/Karte | `wmts…/1.0.0/<layer>/…/{z}/{x}/{y}.jpeg` | ✅ ✅ | Kacheln |
|
||||
| 3D-Nachbargebäude (stream) | `3d…/ch.swisstopo.swissbuildings3d.3d/v1/tileset.json` | ✅ ✅ | 3D-Tiles/glTF |
|
||||
| 3D-Gebäude (Datei) | STAC `…/swissbuildings3d_2` → `.obj/.ifc` | ✅ ✅ | OBJ/IFC |
|
||||
| Parzelle/Kataster | `api3…/MapServer/identify` `layers=all:ch.kantone.cadastralwebmap-farbe` | ✅ ✅ | GeoJSON |
|
||||
| Geocoding | `api3…/SearchServer?type=locations` | ✅ ✅ | JSON |
|
||||
|
||||
---
|
||||
|
||||
## Teil B — Standort-Kontext importieren (Terrain + Parzelle + Nachbargebäude)
|
||||
|
||||
So bekommt ein Projekt seinen realen Kontext „auf Knopfdruck". Der Ablauf folgt
|
||||
DOSSIER (`rhino/swisstopo.py`), übersetzt auf Browser-Libs.
|
||||
|
||||
### B.1 Pipeline (End-to-End)
|
||||
|
||||
```
|
||||
Adresse/Parzelle ──SearchServer──▶ Zentrum (E,N) in LV95
|
||||
│
|
||||
├─ radius r ──▶ bbox_LV95 (E±r, N±r) ──proj4/Formeln──▶ bbox_WGS84
|
||||
│
|
||||
├─[Parzelle] identify(cadastralwebmap, point) ─▶ Polygon (LV95) ─▶ Grundstücksgrenze (Ebene 01 Vermessung)
|
||||
│
|
||||
├─[Gelände] STAC(swissalti3d, bbox_WGS84) ─▶ COG .tif(2056)
|
||||
│ └─ geotiff.js readRasters(window) ─▶ Höhen-Grid (E,N,Z, m)
|
||||
│ └─ Three.js BufferGeometry (Grid→Mesh) [optional: TIN, Höhenlinien, Volumen]
|
||||
│
|
||||
├─[Orthofoto] WMTS swissimage ─▶ Textur auf Gelände-Mesh ODER georef. Plane
|
||||
│
|
||||
└─[Nachbarn] 3D-Tiles streamen (Anzeige) ODER STAC swissbuildings3d_2 .obj ─▶ Mesh-Import
|
||||
(Weltweit/ausserhalb CH: OSM-Overpass als Fallback, siehe B.5)
|
||||
──▶ alle Geometrien um origin_shift (bbox-Center→0/0/0) verschoben, Z aus ALTI3D
|
||||
```
|
||||
|
||||
### B.2 Gelände-Mesh aus swissALTI3D (Kern, Phase 4)
|
||||
|
||||
**Empfohlener Browser-Weg (COG + geotiff.js):**
|
||||
|
||||
1. STAC-Query mit `bbox_WGS84` → Liste der überlappenden Tiles; pro Tile das
|
||||
gewünschte COG-Asset (`_2_2056_` für 2 m, `_0.5_2056_` für 0.5 m).
|
||||
2. `geotiff.js`: `const tiff = await fromUrl(href)` → COG; `image.readRasters({window})`
|
||||
liest **nur den Ausschnitt** (Range-Requests, da CORS+Range bestätigt). Ergebnis ist
|
||||
ein reguläres Z-Raster mit bekanntem Origin/PixelScale (LV95-Meter) aus den
|
||||
GeoKeys/`image.getOrigin()`/`image.getResolution()`.
|
||||
3. Raster → **`THREE.BufferGeometry`**: ein Vertex pro Rasterpunkt
|
||||
`(E−shiftE, N−shiftN, Z−shiftZ)`, Faces als zwei Dreiecke pro Zelle (DOSSIER:
|
||||
`mesh_from_grid`, gleiche Logik), `computeVertexNormals()`. Bei 0.5 m wird das Mesh
|
||||
groß → bei Bedarf raumräumlich sub-samplen (DOSSIER macht ganzzahliges Sub-Sampling
|
||||
auf dem **globalen** LV95-Raster, damit Nachbar-Tiles nahtlos zusammenpassen).
|
||||
4. Mehrere Tiles: erst zu **einem** Grid mergen (gemeinsamer Origin/Step), dann meshen —
|
||||
sonst entstehen Nähte (DOSSIER: `merge_grids`).
|
||||
|
||||
**Alternativweg (XYZ, exakt wie DOSSIER):** `.xyz.zip` laden → mit **`fflate`**
|
||||
(npm, schnellster Inflate im Browser) entpacken → ASCII `E N Z` parsen → gleiches Grid.
|
||||
Robust, aber überträgt mehr Bytes als der COG-Range-Weg. Für den Port empfehle ich
|
||||
**COG primär, XYZ als Fallback**.
|
||||
|
||||
Bibliotheken: `geotiff@3.0.5`, `fflate` (XYZ-Variante), `three@0.185.0`.
|
||||
[geotiff.js](https://github.com/geotiffjs/geotiff.js/) ·
|
||||
[3D-Terrain aus GeoTIFF mit Three.js](https://spatial-dev.guru/2024/11/30/creating-3d-terrain-maps-from-geotiff-files-with-three-js/).
|
||||
|
||||
**Ableitungen wie in DOSSIER** (alle aus dem Grid, in Phase 4 portierbar):
|
||||
- **Höhenlinien** (Marching-Squares auf dem Grid; npm `d3-contour` oder
|
||||
`marchingsquares`) — für 2D-Plan.
|
||||
- **TIN / Patch** (Delaunay aus den Punkten; npm `delaunator`) — alternatives Mesh.
|
||||
- **Geschlossenes Gelände-Volumen** (Boden N m unter tiefstem Punkt) → gefüllte
|
||||
Querschnitte beim Schnitt-Cut (DOSSIER `terrainVolume`/`terrainVolumeDepth`).
|
||||
|
||||
### B.3 Origin-Shift (Pflicht)
|
||||
|
||||
LV95-Koordinaten (~2.6e6) sprengen `float32`. Beim Import einmal
|
||||
`shift = (eCenter, nCenter, zRef)` festlegen, **alle** Geometrien `−shift` rechnen, und
|
||||
`shift` am Projekt persistieren (für Re-Import / Geo-Referenz / Norden). DOSSIER:
|
||||
`origin_shift`/`shift_lv95`, plus „Auto-Zoom auf Import" (ROADMAP §11). Damit bleibt das
|
||||
Modell metergenau und der Rückweg in echte LV95-Koordinaten (Export, weitere
|
||||
swisstopo-Abfragen) ist `+ shift`.
|
||||
|
||||
### B.4 Parzelle + Orthofoto
|
||||
|
||||
- **Parzelle:** `identify(...cadastralwebmap..., returnGeometry=true, geometryFormat=geojson)`
|
||||
→ Polygon (LV95) → `−shift` → als geschlossene Polylinie auf Ebene **`01 Vermessung`**.
|
||||
Attribute `number`/`egris_egrid` am Objekt/Projekt speichern.
|
||||
- **Orthofoto:** WMTS `swissimage`-Kacheln über die Modell-Bbox stitchen → eine Textur,
|
||||
als Material auf das Gelände-Mesh **oder** auf eine georeferenzierte Plane (DOSSIER:
|
||||
`add_ortho_plane`, mit UV-Shift gegen Tile-Nähte). Für 3D ist `3857` einfacher, für
|
||||
exakte 2D-Lage `2056`.
|
||||
|
||||
### B.5 OSM-Overpass als weltweiter Fallback (Phase 4)
|
||||
|
||||
Ausserhalb der Schweiz (oder wenn nur 2D-Footprints/Straßen reichen): DOSSIER hat einen
|
||||
**Overpass-Importer** (`https://overpass-api.de/api/interpreter`, POST) mit 7 Kategorien
|
||||
(Straßen/Gebäude/Wasser/Wasserläufe/Grün/Wege). Liefert OSM-Ways → Polylinien. Im
|
||||
Browser identisch nutzbar (`fetch` POST). Overpass koordiniert in WGS84 → mit
|
||||
`proj4`→LV95→`−shift`. Hinweis: Overpass-CORS ist beim Haupt-Server meist offen, kann
|
||||
aber je nach Mirror variieren; ggf. anderen Mirror wählen.
|
||||
[Overpass API](https://overpass-api.de/).
|
||||
|
||||
### B.6 Was sich von DOSSIER **nicht** 1:1 portieren lässt
|
||||
|
||||
- **Rhino-`_-Import`** für DXF/DWG/OBJ und **`_-MeshPatch`/Delaunay**-Commands gibt es im
|
||||
Browser nicht → ersetzen durch JS-Parser/Algorithmen (OBJ: `three`-`OBJLoader`;
|
||||
Delaunay: `delaunator`; Contours: `d3-contour`).
|
||||
- **Filesystem-Cache neben der `.3dm`** → Browser: **IndexedDB**-Cache (Cache-API für
|
||||
Kacheln). Passt zur ROADMAP-Phase 5 (IndexedDB-Persistenz).
|
||||
- IFC-Import von swissBUILDINGS3D 3.0 → über **web-ifc** (ohnehin im Stack, Phase 4).
|
||||
|
||||
---
|
||||
|
||||
## Teil C — SIA 416 (Flächen/Volumen) & SIA 421 + DOSSIER-Logik
|
||||
|
||||
### C.1 SIA 416 — Flächen- und Volumenhierarchie
|
||||
|
||||
**SIA 416:2003** „Flächen und Volumen von Gebäuden" ist die in der CH gültige Norm und
|
||||
Berechnungsbasis für Kostenplanung/Flächennachweise. Sie kennt vier Bereiche:
|
||||
**GSF** (Grundstück), **GF** (Geschossflächen), **AGF** (Aussengeschossflächen), **GV**
|
||||
(Volumen). Maßgeblich ist die effektive Geometrie (keine fiktiven Zuschläge mehr).
|
||||
Quellen: [SIA 416 Übersicht (siworks/DBV)](https://diebauherrenvertretung.ch/sia146/) ·
|
||||
[SIA-Shop 416/2003](https://shop.sia.ch/normenwerk/architekt/sia%20416/dfi/D/Product) ·
|
||||
[Flächenkennzahlen SIA 416 (Ginesta, PDF)](https://www.ginesta.ch/resources/public/lava3/media/kcfinder/files/Fl%C3%A4chenkennzahlen%20SIA%20416%20Norm.pdf).
|
||||
|
||||
**Hierarchie & Formeln (verifiziert):**
|
||||
|
||||
```
|
||||
GSF Grundstücksfläche
|
||||
GF Geschossfläche = KF + NGF
|
||||
├─ KF Konstruktionsfläche (Wände/Stützen; tragend KFT + nicht tragend KFN)
|
||||
└─ NGF Nettogeschossfläche = NF + VF + FF
|
||||
├─ NF Nutzfläche = HNF + NNF
|
||||
│ ├─ HNF Hauptnutzfläche (zweckbestimmte Hauptnutzung: Wohnen, Büro …)
|
||||
│ └─ NNF Nebennutzfläche (Lager, Bad/WC, Abstell-, Nebenräume)
|
||||
├─ VF Verkehrsfläche (Erschließung: Flure, Treppen, Lifte)
|
||||
└─ FF Funktionsfläche (Gebäudetechnik: Heizung, Lüftung, Technik)
|
||||
AGF Aussengeschossfläche (Balkone, Terrassen, gedeckte Aussenflächen)
|
||||
GV Gebäudevolumen [m³]
|
||||
```
|
||||
|
||||
| Abk. | Deutsch | Inhalt (Kurz) |
|
||||
|---|---|---|
|
||||
| GSF | Grundstücksfläche | Parzellenfläche (aus Kataster, Teil A.6) |
|
||||
| GF | Geschossfläche | allseits umschlossene + überdeckte Grundrissflächen, geschossweise |
|
||||
| KF | Konstruktionsfläche | Bauteile (Wände/Stützen), nicht begehbar; KFT tragend / KFN nicht tragend |
|
||||
| NGF | Nettogeschossfläche | begehbare Fläche innerhalb der Umschließung = NF+VF+FF |
|
||||
| NF | Nutzfläche | tatsächlich nutzbar = HNF+NNF |
|
||||
| HNF | Hauptnutzfläche | zweckbestimmte Hauptnutzung |
|
||||
| NNF | Nebennutzfläche | dienende Nebenräume (Lager, Bad, WC) |
|
||||
| VF | Verkehrsfläche | horizontale/vertikale Erschließung |
|
||||
| FF | Funktionsfläche | Gebäudetechnik |
|
||||
| AGF | Aussengeschossfläche | Balkone/Terrassen u.ä. |
|
||||
| GV | Gebäudevolumen | umbauter Raum [m³] |
|
||||
|
||||
> **Versionshinweis:** Es kursiert eine Revision **SIA 416:2017** mit präzisierten
|
||||
> Begriffen, in der Praxis wird aber breit weiter **416:2003** referenziert. Für unser
|
||||
> Tool reicht die Klassifikation **HNF/NNF/VF/FF/GF/AGF + Bilanz**; die genaue Auflage
|
||||
> nur als Label dokumentieren. Verbindliche Definitionen stehen im kostenpflichtigen
|
||||
> Normtext (SIA-Shop) — die hier zitierten freien Quellen stimmen in der Struktur überein.
|
||||
|
||||
### C.2 SIA 421 — Flächengliederung für Bewirtschaftung/Vermietung
|
||||
|
||||
**SIA 421:2006** „Flächengliederung und Mengenangaben" (Korrigenda C1:2014) baut auf der
|
||||
SIA-416-Systematik auf und **gliedert Flächen für Immobilien-Bewirtschaftung und
|
||||
Vermietung** (Mietflächen, Nutzungseinheiten, Zuordnung von VF/FF zu Mietern). Relevant,
|
||||
sobald wir **Mietflächen-/Bewirtschaftungs-Auswertungen** wollen (über den reinen
|
||||
Architektur-Nachweis hinaus). Für den ersten Wurf **nicht zwingend** — SIA 416 reicht
|
||||
für Flächennachweis und Raumschema. SIA 421 ist die natürliche Erweiterung, wenn
|
||||
Property-Management-Features kommen. Quellen:
|
||||
[SIA 421:2006 (PDF Inhalt)](https://shop.sia.ch/d475e3f9-10c9-48aa-9be8-657784ea519b/F/DownloadAnhang) ·
|
||||
[Korrigenda C1:2014 (PDF)](https://www.sia.ch/fileadmin/content/download/sia-norm/korrigenda_sn/421-C1_2014_d.pdf).
|
||||
|
||||
### C.3 Wie DOSSIER SIA macht (Vorlage für den Port)
|
||||
|
||||
Quellcode: `rhino/elemente.py` (Räume) + `rhino/elemente_uebersicht.py` (Bilanz/CSV).
|
||||
Kernpunkte, die wir 1:1 übernehmen:
|
||||
|
||||
**Datenmodell pro Raum.** Ein Raum ist eine **geschlossene Outline-Curve** + ein
|
||||
**Text-Stempel**. Klassifikation über das Feld `dossier_raum_sia` mit Werten
|
||||
`{"", hnf, nnf, vf, ff, gf, agf}` (`_RAUM_SIA_KINDS`). Weitere Felder: `name`, `nummer`,
|
||||
`funktion` (`wohnen|schlafen|bad|kueche|essen|flur|…`), `personen` (für
|
||||
Personenbelegung/Brandschutz), Rundung, Stempel-Layout.
|
||||
|
||||
**Flächen-/Umfangsberechnung** (`_raum_amp`): Fläche aus `AreaMassProperties` (≙ in JS
|
||||
**Shoelace-Formel** über das Polygon), Umfang = Kurvenlänge, plus Zentroid für den
|
||||
Stempel. Im Browser: Polygon-Fläche selbst rechnen (Shoelace), kein Mesh nötig — exakt
|
||||
und schnell. Rundungsstufen (`_format_area`): `exakt|0.01|0.1|0.5|1` (z.B. `0.5` =
|
||||
`round(a*2)/2`).
|
||||
|
||||
**SIA-Bilanz** (`compute_sia_bilanz`, `scope = total | geschoss:<id>`): summiert
|
||||
Raumflächen je Klasse, dann:
|
||||
```
|
||||
NF = HNF + NNF
|
||||
NGF = NF + VF + FF (GF/AGF separat aggregiert, zählen nicht in NGF)
|
||||
```
|
||||
(genau die Formeln aus C.1). Ergebnis je Geschoss + Total.
|
||||
|
||||
**Farb-/Darstellungs-Konvention** (`_SIA_COLORS_HEX`, Pastell): HNF rot `#e8a8a8`,
|
||||
NNF orange `#e8c498`, VF gelb `#e8d878`, FF hellblau `#a8c8e0`, GF grau `#d0d0d0`,
|
||||
AGF hellgrün `#c0d8c0`. Umgesetzt als **regelbasierte Overrides** (`_build_sia_preset_rules`,
|
||||
Preset „SIA-Raeume"): Bedingung `user_string == code` → Outline-Farbe + Solid-Hatch.
|
||||
→ Passt 1:1 zur geplanten **Overrides-Engine** (ROADMAP §2c/§11).
|
||||
|
||||
**Export** (`_cmd_export_raeume`, `_export_bilanz`): CSV, **Semikolon + UTF-8-BOM**
|
||||
(CH/DE-Excel), Dezimal-Komma. Raumliste: Nummer; Name; Geschoss; Funktion; SIA; Fläche;
|
||||
Fläche gerundet; Umfang. Bilanz: eine Spalte je Geschoss + Total, Zeilen je Kategorie.
|
||||
→ Im Browser: Blob + Download (kein SaveFileDialog), gleiche CSV-Struktur. Optional
|
||||
direkt `.xlsx` via `sheetjs`/`exceljs`.
|
||||
|
||||
**Layer-Routing:** GF→`61_GF`, AGF→`62_AGF`, Rest→`60_RAEUME` (`_layer_path_for_raum_sia`)
|
||||
— damit Geschossflächen-Outlines getrennt schalt-/exportierbar sind. Übersetzt sich auf
|
||||
unsere Ebenen-Codes (`60 Räume`).
|
||||
|
||||
**Was wir im Port besser/anders machen:**
|
||||
- Fläche per **Shoelace** statt Rhino-Mass-Props (0 Deps).
|
||||
- Bilanz **reaktiv** aus dem semantischen Modell (Zustand-Store) statt Doc-Scan.
|
||||
- **Space = Slab-/Raum-Polygon mit `siaClass`** im Datenmodell (ROADMAP:
|
||||
`Space { boundary, name }`), Bilanz als abgeleitete Sicht.
|
||||
|
||||
---
|
||||
|
||||
## Teil D — Umsetzungsplan (Endpunkte · Libs · Phase)
|
||||
|
||||
Reihenfolge orientiert sich an der ROADMAP (SIA = Phase 2, Geo = Phase 4) und an „größter
|
||||
Nutzen zuerst, geringste Abhängigkeit zuerst".
|
||||
|
||||
### Phase 2 — SIA-Räume (kein Netz, reine Logik) ⭐
|
||||
**Endpunkte:** keine. **Libs:** keine (Shoelace selbst), optional `exceljs`/`sheetjs` für
|
||||
.xlsx.
|
||||
1. `Space`-Modell: `{ boundary[], geschossId, name, nummer, funktion, siaClass∈{hnf,nnf,vf,ff,gf,agf}, personen }`.
|
||||
2. `computeArea` (Shoelace) + Umfang; Rundungsstufen (`exakt|0.01|0.1|0.5|1`) wie DOSSIER `_format_area`.
|
||||
3. `computeSiaBilanz(scope)` → `{hnf,nnf,nf,vf,ff,ngf,gf,agf,count,personen}` mit
|
||||
`nf=hnf+nnf`, `ngf=nf+vf+ff`.
|
||||
4. SIA-Farbpalette + Stil-Override (Outline-Farbe/Solid-Fill) in der Overrides-Engine.
|
||||
5. Raumstempel-Renderer (Felder-Layout) + **CSV-Export** (Semikolon, UTF-8-BOM, Komma)
|
||||
für Raumliste **und** Bilanz.
|
||||
*Ergebnis:* SIA-416-Flächennachweis + Raumschema, Excel-kompatibel — vor jeder Geo-Arbeit nutzbar.
|
||||
|
||||
### Phase 4a — Geo-Grundlage: Koordinaten + Standortabfrage
|
||||
**Endpunkte:** `SearchServer` (Geocoding), `height` (Punkt-Z), `identify`
|
||||
(Parzelle, `cadastralwebmap-farbe`). **Libs:** `proj4@2.20.9` (+ `@types/proj4`).
|
||||
1. `proj4`-EPSG:2056-Def + Helfer `lv95↔wgs84`, `bboxLv95→bboxWgs84` (DOSSIER-Logik).
|
||||
2. **Origin-Shift**-Mechanik + Persistenz am Projekt (`shift = bbox-Center`), Auto-Zoom.
|
||||
3. Adresssuche → Zentrum; „Gelände-Höhe holen" (height); „Parzelle holen" → Polygon auf
|
||||
Ebene `01 Vermessung` (+ EGRID/Nummer am Projekt).
|
||||
*Ergebnis:* Projekt ist georeferenziert; Parzelle + Adresse + Geländehöhe vorhanden.
|
||||
|
||||
### Phase 4b — Gelände-Mesh + Orthofoto
|
||||
**Endpunkte:** STAC `swissalti3d` (COG `.tif`, 2056) + `profile.json`; WMTS `swissimage`.
|
||||
**Libs:** `geotiff@3.0.5`, `three@0.185.0`, optional `fflate` (XYZ-Fallback),
|
||||
`d3-contour`/`marchingsquares` (Höhenlinien), `delaunator` (TIN).
|
||||
1. STAC-Query (bbox) → COG-Tiles; `geotiff.js` Range-Read → Grid; Tiles mergen.
|
||||
2. Grid → `THREE.BufferGeometry` (DOSSIER `mesh_from_grid`/`merge_grids`); Sub-Sampling
|
||||
auf globalem LV95-Raster; Normalen.
|
||||
3. Optional: Höhenlinien (2D-Plan), TIN, geschlossenes Volumen (Schnitt-Füllung).
|
||||
4. WMTS-`swissimage`-Kacheln → Textur auf Mesh/Plane (DOSSIER `add_ortho_plane`).
|
||||
5. Geländeschnitt im 2D-Plan via `profile.json` entlang der Schnittlinie.
|
||||
*Ergebnis:* echtes Gelände mit Orthofoto unter dem Gebäude; Geländeschnitte.
|
||||
|
||||
### Phase 4c — Nachbargebäude + weltweiter Fallback
|
||||
**Endpunkte:** 3D-Tiles `ch.swisstopo.swissbuildings3d.3d/v1/tileset.json` (Anzeige)
|
||||
**oder** STAC `swissbuildings3d_2` `.obj/.ifc` (Import); OSM `overpass-api.de`.
|
||||
**Libs:** `@loaders.gl/3d-tiles@4.4.3` **oder** `3DTilesRendererJS`; `three`-`OBJLoader`;
|
||||
`web-ifc` (für 3.0-IFC); `proj4`.
|
||||
1. Kontext-Anzeige: 3D-Tiles in den Three-Scenegraph streamen (kein Download).
|
||||
2. Bedarf an echter Geometrie (Verschattung/Abstand): STAC-OBJ-Tile → Mesh-Import → `−shift`.
|
||||
3. Ausserhalb CH / nur 2D: Overpass-POST → Ways → Polylinien (Ebene `70 OSM`).
|
||||
*Ergebnis:* Nachbarschaftskontext (CH 3D, weltweit OSM).
|
||||
|
||||
### Querschnitt (alle Geo-Phasen)
|
||||
- **Caching:** IndexedDB für STAC-Antworten + COG-Bytes + Kacheln (ersetzt DOSSIERs
|
||||
Disk-Cache); Cache-Schlüssel = Tile-ID/URL.
|
||||
- **Defensive HTTP:** Timeouts, Retry/Backoff, 402/429 abfangen, Größen-Limit pro Tile
|
||||
(DOSSIER: 200-MB-Guard).
|
||||
- **Attribution:** „© swisstopo" sichtbar einblenden, „© OpenStreetMap-Mitwirkende" bei OSM.
|
||||
- **Worker:** GeoTIFF-Parsing + Mesh-Bau im Web-Worker (Comlink, ROADMAP-Stack), UI bleibt flüssig.
|
||||
|
||||
### Empfohlener Library-Satz (npm, aktuell)
|
||||
`proj4@2.20.9` · `geotiff@3.0.5` · `three@0.185.0` · `fflate` (XYZ) ·
|
||||
`@loaders.gl/3d-tiles@4.4.3` *oder* `3DTilesRendererJS` · `delaunator` ·
|
||||
`d3-contour` · `web-ifc` (im Stack) · `exceljs`/`sheetjs` (optional, .xlsx).
|
||||
|
||||
---
|
||||
|
||||
## Quellen
|
||||
|
||||
- GeoAdmin REST (height/profile/identify/find/search): https://geoadmin.readthedocs.io/en/latest/services/sdiservices.html
|
||||
- GeoAdmin Tech-Docs (Hub): https://docs.geo.admin.ch/
|
||||
- Identify Features: https://docs.geo.admin.ch/access-data/identify-features.html
|
||||
- Search: https://docs.geo.admin.ch/access-data/search.html
|
||||
- WMTS: https://docs.geo.admin.ch/visualize-data/wmts.html · https://wmts.geo.admin.ch/
|
||||
- WMS: https://docs.geo.admin.ch/visualize-data/wms.html
|
||||
- 3D-Tiles: https://docs.geo.admin.ch/visualize-data/3d-tiles.html
|
||||
- swissALTI3D: https://www.swisstopo.admin.ch/en/height-model-swissalti3d
|
||||
- Switzerland in 3D / swissBUILDINGS3D: https://www.swisstopo.admin.ch/en/switzerland-in-3d
|
||||
- Terms of use (FSDI / OGD): https://www.geo.admin.ch/en/general-terms-of-use-fsdi
|
||||
- REFRAME Web-API: https://www.swisstopo.admin.ch/en/rest-api-geoservices-reframe-web
|
||||
- EPSG:2056 Definition: https://epsg.io/2056
|
||||
- proj4js: https://github.com/proj4js/proj4js
|
||||
- geotiff.js: https://github.com/geotiffjs/geotiff.js/
|
||||
- Three.js-Terrain aus GeoTIFF: https://spatial-dev.guru/2024/11/30/creating-3d-terrain-maps-from-geotiff-files-with-three-js/
|
||||
- WMTS EPSG:2056 Beispiel: https://codepen.io/geoadmin/pen/GZKEam
|
||||
- Overpass API: https://overpass-api.de/
|
||||
- SIA 416 (Übersicht): https://diebauherrenvertretung.ch/sia146/ · https://shop.sia.ch/normenwerk/architekt/sia%20416/dfi/D/Product
|
||||
- SIA 416 Flächenkennzahlen (PDF): https://www.ginesta.ch/resources/public/lava3/media/kcfinder/files/Fl%C3%A4chenkennzahlen%20SIA%20416%20Norm.pdf
|
||||
- SIA 421:2006 (PDF) + Korrigenda C1:2014: https://shop.sia.ch/d475e3f9-10c9-48aa-9be8-657784ea519b/F/DownloadAnhang · https://www.sia.ch/fileadmin/content/download/sia-norm/korrigenda_sn/421-C1_2014_d.pdf
|
||||
- DOSSIER-Quellcode (Vorlage): `rhino/swisstopo.py`, `rhino/osm.py`, `rhino/elemente.py` (Räume/SIA), `rhino/elemente_uebersicht.py` (Bilanz/CSV)
|
||||
@@ -0,0 +1,197 @@
|
||||
# Technologie-Auswahl: Browser-basiertes BIM-Tool (DOSSIER-Port)
|
||||
|
||||
> **Kontext:** Standalone, browser-basiertes BIM-Werkzeug (React + TypeScript + Three.js) als Port des DOSSIER Rhino-Plugins. Keine Server-Abhaengigkeit gewuenscht (alles client-side). Recherchestand: Juni 2026.
|
||||
>
|
||||
> **Leitprinzip:** Wo immer moeglich auf einem echten B-Rep-Geometriekernel (OCCT) aufbauen, weil ein BIM-Werkzeug exakte 2D-Ableitungen (Schnitte, verdeckte Kanten, Bemassung) braucht — das ist mit reinen Dreiecksnetzen nicht sauber loesbar. Mesh-Booleans (Manifold) als schnelle Ergaenzung fuer Importgeometrie und Vorschau.
|
||||
|
||||
---
|
||||
|
||||
## 1. Geometriekernel im Browser (Solids + Booleans)
|
||||
|
||||
Das ist das schwierigste und zugleich wichtigste Problem. Drei ernsthafte Optionen, die alle im Browser (WASM) laufen.
|
||||
|
||||
### Optionen
|
||||
|
||||
**A) opencascade.js (OCCT als WASM)**
|
||||
Port des vollstaendigen OpenCASCADE-Kernels (OCCT) nach WebAssembly via Emscripten. Voller B-Rep-Kernel: NURBS-Flaechen, exakte boolesche Operationen, Fillets/Chamfers, STEP/IGES-Import/-Export, Meshing. TypeScript-Bindings vorhanden. Die neueren Versionen (V3-Linie) zielen explizit auf moderne Bundler.
|
||||
- Repo: <https://github.com/donalffons/opencascade.js/>
|
||||
- Doku: <https://opencascade-js.vercel.app/>
|
||||
- npm: <https://www.npmjs.com/package/opencascade.js>
|
||||
- **Lizenz:** OCCT steht unter **LGPL-2.1 mit OCCT-Exception** (seit 6.7.0). Kommerzielle Nutzung ohne Lizenzgebuehren/Royalties erlaubt, sofern man (a) sichtbar darauf hinweist, dass die Software OCCT nutzt, und (b) eine Kopie der OCCT-Lizenz mitliefert. Die Exception entschaerft das statische-Linking-Problem fuer Header/Templates. Quellen: <https://dev.opencascade.org/resources/licensing>, <https://spdx.org/licenses/OCCT-exception-1.0.html>
|
||||
- **Trade-offs:** Sehr grosse WASM-Binaries (zweistelliger MB-Bereich je nach Custom-Build), steile Lern- und API-Kurve (rohe OCCT-C++-API durchgereicht), Build-Pflege aufwendig.
|
||||
|
||||
**B) replicad (Abstraktion ueber opencascade.js)** — *empfohlene Basis*
|
||||
replicad ist eine schlanke, idiomatische TypeScript-Schicht ueber opencascade.js. Es liefert genau die High-Level-Bausteine, die ein BIM-Tool braucht: Sketches/Blueprints, Extrude/Revolve/Loft, Booleans, Fillet/Chamfer — und entscheidend: **HLR-Projektionen** (`drawProjection`) und 2D-Drawings mit SVG-Export. Laeuft per Design im **Web Worker** und gibt Dreiecksnetze an den Main-Thread fuer Three.js zurueck.
|
||||
- Doku/Library-Guide: <https://replicad.xyz/docs/use-as-a-library/>
|
||||
- API: <https://replicad.xyz/docs/api/>
|
||||
- **Lizenz:** **MIT** (eigene Schicht) — der OCCT/LGPL-Hinweis gilt weiterhin fuer das eingebettete WASM. Repo: <https://github.com/sgenoud/replicad>
|
||||
- **Trade-offs:** Erbt OCCT-WASM-Groesse und -Robustheitsgrenzen; kleineres Team/Bus-Faktor als OCCT selbst. Man kann jederzeit „unter die Haube" auf rohes opencascade.js durchgreifen, wenn die Abstraktion nicht ausreicht.
|
||||
|
||||
**C) Manifold (manifold-3d, WASM)** — *empfohlen als schnelle Mesh-Ergaenzung*
|
||||
Geometrie-Bibliothek fuer topologisch robuste **Dreiecksnetze**. Bietet den (laut Autor) ersten garantiert mannigfaltigen Mesh-Boolean-Algorithmus — extrem schnell und robust gegen Randfaelle. Stark parallelisiert.
|
||||
- Repo: <https://github.com/elalish/manifold>
|
||||
- npm: <https://www.npmjs.com/package/manifold-3d> (aktuell v3.5.x, Juni 2026)
|
||||
- **Lizenz:** **Apache-2.0** (sehr permissiv, ideal). Bestaetigt: <https://github.com/elalish/manifold>
|
||||
- **Trade-offs:** **Nur Meshes, kein B-Rep** — keine exakten NURBS-Flaechen, keine echten Fillets auf Krümmungen, kein STEP. Fuer ein BIM-Tool, das exakte Plaene/Schnitte ableiten will, alleine nicht ausreichend, aber unschlagbar fuer schnelle Booleans auf importierter Mesh-Geometrie und Live-Vorschau. **Wichtig:** unterstuetzt `slice(z)` und `project()` (siehe Abschnitt 3).
|
||||
|
||||
**D) three-bvh-csg (nur erwähnt, nicht empfohlen als Kernel)**
|
||||
Sehr schnelle CSG direkt auf Three.js-BufferGeometry (auf three-mesh-bvh). >100x schneller als BSP-basierte Three.js-CSG-Libs. Aber: erklaert selbst, dass Resultate „aufgrund numerischer Praezision nicht garantiert 2-mannigfaltig" sind und verweist fuer CAD-Robustheit ausdruecklich auf Manifold.
|
||||
- Repo: <https://github.com/gkjohnson/three-bvh-csg>
|
||||
- Forum: <https://discourse.threejs.org/t/three-bvh-csg-a-library-for-performing-fast-csg-operations/42713>
|
||||
- **Trade-offs:** Gut fuer Live-Vorschau/visuelles Schneiden, ungeeignet als verlaesslicher Modellierkernel.
|
||||
|
||||
### Empfehlung (Kernel)
|
||||
**replicad (= opencascade.js) als primaerer B-Rep-Kernel im Web Worker; Manifold als schneller Mesh-Boolean-Pfad fuer Import-/Vorschaugeometrie.** Diese Zweiteilung deckt sowohl „exakte BIM-Geometrie + 2D-Ableitung" (OCCT) als auch „schnell + robust auf beliebigen Meshes" (Manifold) ab. three-bvh-csg nur, falls man interaktives Echtzeit-Schneiden visuell braucht.
|
||||
|
||||
---
|
||||
|
||||
## 2. 2D-Ableitung aus 3D: verdeckte Kanten (HLR) + Schnittgenerierung
|
||||
|
||||
Kernfrage des DOSSIER-Ports: aus 3D-Solids saubere 2D-Zeichnungen (sichtbare/verdeckte Kanten, Schnitte) erzeugen — im Browser.
|
||||
|
||||
### Optionen
|
||||
|
||||
**A) OCC HLRBRep via replicad `drawProjection` — empfohlen**
|
||||
OCCT enthaelt zwei HLR-Algorithmen: `HLRBRep_Algo` (exakt, auf der echten B-Rep) und `HLRBRep_PolyAlgo` (auf polyederisierter Naeherung, schneller, aber polygonal). Quellen: <https://dev.opencascade.org/doc/refman/html/class_h_l_r_b_rep.html>, <https://dev.opencascade.org/doc/occt-7.7.0/refman/html/class_h_l_r_b_rep___poly_algo.html>
|
||||
|
||||
replicad macht genau das im Browser nutzbar: `drawProjection(shape, camera)` liefert ein Objekt mit **`{ visible, hidden }`** — getrennte sichtbare und verdeckte Kantenzuege, die man unterschiedlich stylen kann (z.B. verdeckt = gestrichelt). Konkretes Beispiel aus der Doku:
|
||||
|
||||
```js
|
||||
const { drawProjection, ProjectionCamera } = replicad;
|
||||
const camera = new ProjectionCamera(corner).lookAt(center);
|
||||
const { visible, hidden } = drawProjection(shape, camera);
|
||||
// visible/hidden sind Drawings -> .toSVG()
|
||||
```
|
||||
- Beispiel: <https://replicad.xyz/docs/examples/projections/>
|
||||
- Verwandte API: `makeProjectedEdges`, `ProjectionCamera`, `Drawing.toSVG()` / `toSVGPaths()` (<https://replicad.xyz/docs/api/classes/Drawing/>)
|
||||
- **Das ist der entscheidende Grund, replicad/OCCT zu nehmen:** exakte verdeckte-Kanten-Berechnung auf echtem B-Rep ist mit Mesh-Tools nicht serioes machbar.
|
||||
- **Trade-offs:** HLR ist rechenintensiv (deshalb Worker + Caching pro Ansicht/Kamera); `HLRBRep_Algo` exakt aber langsam, `PolyAlgo` schneller aber genaehert.
|
||||
|
||||
**B) Schnitte (Sections) via OCCT**
|
||||
Echte Schnitte ueber Schnitt mit einer Ebene/Halbraum (`BRepAlgoAPI_Section` bzw. Boolean mit Schnittkoerper) ergeben exakte Schnittkanten als B-Rep-Edges, die wiederum nach SVG/DXF gehen. In replicad ueber Booleans + Projektion abbildbar.
|
||||
|
||||
**C) Manifold `slice()` / `project()` — schnelle Mesh-Variante**
|
||||
`Manifold.slice(z)` gibt den Querschnitt parallel zur X-Y-Ebene auf Hoehe `z` als `CrossSection` (2D-Polygone, intern Clipper2); `Manifold.project()` die projizierte Aussenkontur. Quellen: <https://manifoldcad.org/docs/jsapi/>, <https://manifoldcad.org/docs/html/classmanifold_1_1_cross_section.html>
|
||||
- **Trade-offs:** liefert **keine** verdeckte/sichtbare-Kanten-Trennung und keine Innenkanten-Semantik wie HLR — nur die geometrische Schnitt-/Projektionskontur des Meshes. Gut fuer Plan-Schnittkonturen (siehe Abschnitt 3), ungenuegend fuer vollwertige Ansichts-Zeichnungen mit verdeckten Kanten.
|
||||
|
||||
### Empfehlung (2D-Ableitung)
|
||||
**HLR und Ansichts-Zeichnungen ueber replicad `drawProjection` (OCC HLRBRep) im Worker, mit Caching pro Kamera/Ansicht. Echte Schnitte ueber OCCT-Section-Boolean. Manifold `slice()` als schneller Pfad nur fuer reine Schnittkonturen (z.B. Plan-Cut auf Mesh-Importen).**
|
||||
|
||||
---
|
||||
|
||||
## 3. Plan-Ansicht: Clipping bei `cutHeight`
|
||||
|
||||
Verhalten: horizontaler Schnitt auf einstellbarer Hoehe (Grundriss), darüber Abschneiden, Schnittflaechen markieren.
|
||||
|
||||
### Optionen
|
||||
|
||||
**A) Three.js Clipping Planes (`clippingPlanes` / `localClippingEnabled`)**
|
||||
Three.js bietet globale (`renderer.clippingPlanes`) und material-lokale (`material.clippingPlanes`) Schnittebenen; `renderer.localClippingEnabled` ist standardmaessig aus (Null-Kosten, bis aktiviert). Quellen: <https://threejs.org/docs/#api/en/materials/Material.clippingPlanes>, <https://threejs.org/examples/webgl_clipping.html>
|
||||
- **Pro:** GPU-seitig, dynamisch (Slider auf `cutHeight` = `plane.constant` aendern, kein Geometrie-Rebuild), sehr fluessig.
|
||||
- **Contra:** Clipping schneidet nur visuell — es entstehen **offene** Querschnitte (keine Deckflaeche). Fuer „Schnittflaeche fuellen/markieren" braucht man entweder einen Stencil-Cap-Trick oder eine echte Schnittkontur (Abschnitt 2). `clipIntersection = true` kann Material-Reinitialisierung pro Frame und FPS-Einbrueche verursachen — moeglichst vermeiden. Quelle: <https://github.com/mrdoob/three.js/issues/18675>
|
||||
|
||||
**B) Object Culling / Sichtbarkeit nach Hoehe**
|
||||
Elemente oberhalb `cutHeight` per Bounding-Box/Etagen-Metadaten ausblenden (`object.visible = false`).
|
||||
- **Pro:** trivial, keine Shader-Kosten, nutzt BIM-Etagensemantik.
|
||||
- **Contra:** grobkoernig (ganze Objekte, kein praeziser Schnitt mitten durch ein Bauteil).
|
||||
|
||||
**C) Echte Schnittgeometrie pro Etage (OCCT/Manifold) fuer den 2D-Plan**
|
||||
Fuer die exportierbare 2D-Grundriss-Zeichnung den echten Schnitt auf `cutHeight` rechnen (OCCT-Section bzw. `Manifold.slice(z)`), Schnittflaechen schraffieren (Abschnitt 6).
|
||||
|
||||
### Empfehlung (Plan-Clipping)
|
||||
**Hybrid:** Im 3D-Viewport **Three.js Clipping Planes** fuer das interaktive Abschneiden (Slider direkt auf `plane.constant`), kombiniert mit **Object-Culling** ueber Etagen-Metadaten fuer Grob-Performance. Schnittflaechen-Caps via Stencil-Technik. Fuer die **exportierbare** 2D-Grundriss-Zeichnung die **echte** Schnittkontur ueber OCCT/Manifold berechnen statt nur GPU-Clipping. `clipIntersection` meiden.
|
||||
|
||||
---
|
||||
|
||||
## 4. Import: DWG/DXF, IFC, STL, OBJ, XYZ-Punktwolken
|
||||
|
||||
### DXF / DWG
|
||||
- **DXF lesen:** `dxf-parser` (gdsestimating) — robust, weit verbreitet, parst DXF-Strings zu JS-Objekten. Repo: <https://github.com/gdsestimating/dxf-parser>. Zum reinen 2D-Anzeigen: `dxf-viewer` (vagran). <https://github.com/vagran/dxf-viewer>
|
||||
- **DWG lesen (binaer!):** `@mlightcad/libredwg-web` — LibreDWG nach WASM, parst **DWG** (und DXF) direkt im Browser/Node ohne Backend. Aktuell v3.x. Repo: <https://github.com/mlightcad/libredwg-web>, npm: <https://www.npmjs.com/package/@mlightcad/libredwg-web>
|
||||
- **Achtung Qualitaet/Limits:** DWG-Parsing ist speicherintensiv (kann >2 GB RAM ziehen); libredwg-web erzwingt WASM-Heap-Limits, sehr grosse DWGs koennen scheitern. Im Default-Build sind DXF-Parsing und DWG-Schreiben deaktiviert, um die WASM-Groesse zu senken — DXF also lieber mit `dxf-parser` lesen. Quelle: <https://medium.com/@mlightcad/parsing-autocad-dwg-files-in-the-browser-without-relying-on-the-backend-9067c5d9abf0>
|
||||
- **Lizenz:** LibreDWG ist **GPL-3.0** — das ist fuer ein proprietaeres Produkt heikel. Wenn das Produkt nicht GPL sein soll, DWG-Import entweder ueber einen separaten Out-of-Process-Konverter kapseln oder Nutzer bitten, vorab nach DXF zu exportieren. **DWG-Lizenzfrage vor Integration klaeren.**
|
||||
- **Referenz-Implementierung:** `cad-viewer` (mlightcad) zeigt vollstaendigen browser-only DXF/DWG-Viewer/Editor. <https://github.com/mlightcad/cad-viewer>
|
||||
|
||||
### IFC (BIM-Kern)
|
||||
- **web-ifc (ThatOpen/engine_web-ifc):** IFC lesen/schreiben in JS „at native speeds" via WASM. De-facto-Standard fuer Open-BIM im Browser. Repo: <https://github.com/ThatOpen/engine_web-ifc>, Doku: <https://thatopen.github.io/engine_web-ifc/docs/>. **Lizenz: MPL-2.0** (datei-basiertes Copyleft, fuer proprietaere Apps i.d.R. unkritisch). Quelle: <https://spdx.org/licenses/MPL-2.0.html>
|
||||
- **ThatOpen Components + Fragments:** Hoehere Ebene — `components` (Tools für BIM-Apps), `fragments` (kompaktes Binaerformat auf Google FlatBuffers). Typisch: ~100 MB IFC -> ~10 MB Fragments, >10x schnelleres Laden; Konvertierung lauft worker-basiert. Doku: <https://docs.thatopen.com/Tutorials/Fragments/Fragments/IfcImporter/>, Repo: <https://github.com/ThatOpen/engine_fragment>. IfcImporter setzt web-ifc (>=0.0.72) voraus.
|
||||
- **Empfehlung:** IFC einmal mit web-ifc parsen, in **Fragments** cachen, danach aus Fragments laden.
|
||||
|
||||
### STL / OBJ / XYZ-Punktwolken
|
||||
- **Standard-Three.js-Loader** decken alles ab: `STLLoader` (ASCII+Binaer), `OBJLoader`, `XYZLoader` (XYZ/XYZRGB -> BufferGeometry), `PCDLoader`. Quellen: <https://threejs.org/docs/#examples/en/loaders/PCDLoader>, <https://deepwiki.com/mrdoob/three.js/4.2-model-format-loaders>. Three.js ist **MIT**.
|
||||
- **Punktwolken-Performance:** Naive Darstellung skaliert nicht — ~17 Mio. Punkte ruckeln deutlich. Strategien: Downsampling, **LOD/Culling**, Hintergrund-/Streaming-Laden. Fuer sehr grosse Wolken Out-of-Core-Octree-Renderer (z.B. Potree-Ansatz) erwaegen statt eines einzelnen `Points`-Objekts. Quellen: <https://discourse.threejs.org/t/render-large-point-cloud-data-in-threejs/57331>, <https://discourse.threejs.org/t/performance-issues-rendering-large-ply-point-cloud-in-three-js-downsampling-and-background-loading/69135>
|
||||
|
||||
### Empfehlung (Import)
|
||||
**IFC: web-ifc + Fragments (ThatOpen).** **DXF: dxf-parser.** **DWG: libredwg-web — aber GPL-Lizenz vorab klaeren / kapseln.** **STL/OBJ/XYZ/PCD: native Three.js-Loader, mit LOD/Downsampling fuer grosse Punktwolken (Potree-Pattern bei Bedarf).**
|
||||
|
||||
---
|
||||
|
||||
## 5. Vektor-Export: SVG -> PDF (Print) und DXF
|
||||
|
||||
### SVG -> PDF
|
||||
- **svg2pdf.js (yWorks) + jsPDF — empfohlen.** Reine JS-Loesung, laeuft im Browser, erhaelt **echte Vektoren** (kein Rasterisieren via html2canvas!), was fuer druckfaehige Plaene entscheidend ist. Integriert sich ueber `doc.svg(element, ...)`. Kompatibel mit jsPDF v2/v3/v4. Repo: <https://github.com/yWorks/svg2pdf.js/>. Lizenz: svg2pdf.js **MIT**, jsPDF **MIT**.
|
||||
- **Trade-off:** SVG-Feature-Abdeckung ist sehr gut, aber nicht 100% — exotische Filter/Pattern koennen abweichen; Schraffuren als explizite Linien (statt CSS-Filter) exportieren erhoeht Treffsicherheit.
|
||||
- **Alternative/ergaenzend:** `pdf-lib` (MIT) fuer Seitenmontage, Mehrseitigkeit, Metadaten, Zusammenfuehren — kann mit jsPDF-Output kombiniert werden. (`html2canvas`+jsPDF bewusst **vermeiden**, da Raster statt Vektor.)
|
||||
|
||||
### DXF-Export
|
||||
- **@tarikjabiri/dxf (dxfjs/writer) — empfohlen.** Moderner, in TypeScript geschriebener DXF-Generator fuer Node + Browser. Unterstuetzt u.a. **Blocks, Hatches, Insert, Image** — d.h. Schraffuren lassen sich als echte DXF-Hatches exportieren (wichtig fuer CAD-Weiterverarbeitung). npm: <https://www.npmjs.com/package/@tarikjabiri/dxf>, Doku: <https://dxf.vercel.app/>, Repo: <https://github.com/tarikjabiri/js-dxf>. Lizenz: MIT.
|
||||
- Einfachere Alternative: `dxf-writer` (Vorlaeufer, weniger Features).
|
||||
|
||||
### Empfehlung (Export)
|
||||
**SVG -> PDF: svg2pdf.js + jsPDF (Vektor, nicht Raster), optional pdf-lib fuer Seitenmontage. DXF-Export: @tarikjabiri/dxf mit echten Hatch-Entities.** Interner Zwischenschritt: 2D-Geometrie als SVG-Paths halten (replicad `Drawing.toSVGPaths()`), daraus sowohl PDF als auch DXF erzeugen.
|
||||
|
||||
---
|
||||
|
||||
## 6. Schraffuren / Muster in SVG/Canvas bei Massstab
|
||||
|
||||
### Optionen & Erkenntnisse
|
||||
- **SVG `<pattern>` mit `patternUnits="userSpaceOnUse"`** ist der richtige Weg fuer massstaebliche Schraffuren: das Muster skaliert NICHT mit der Form (im Gegensatz zu `objectBoundingBox`), sondern bleibt im Weltkoordinaten-Raster — exakt, was man fuer „mm pro Linie im Plan" braucht. Dichte/Winkel ueber `patternTransform`. Quellen: <https://www.w3.org/TR/2015/WD-SVG2-20150915/pservers.html>, <https://www.codegenes.net/blog/simple-fill-pattern-in-svg-diagonal-hatching/>
|
||||
- **Performance:** Pattern-Layer werden bei jedem Layout-/Zoom-Schritt neu gerechnet; `userSpaceOnUse` vermeidet teures Rescaling und ist hier zugleich der schnellere und der korrekte Weg. Quelle: <https://oreillymedia.github.io/Using_SVG/extras/ch19-performance.html>
|
||||
- **SVG vs. Canvas:** Fuer technische Zeichnungen ist SVG qualitativ klar ueberlegen (Vektor, exporttauglich nach PDF/DXF). Canvas wird erst bei sehr vielen einfachen Elementen schneller. Quellen: <https://felt.com/blog/from-svg-to-canvas-part-1-making-felt-faster>, <https://www.yworks.com/blog/svg-canvas-webgl>
|
||||
- **Skalierungs-Strategie:** Bei sehr dichten Schraffuren ueber grosse Flaechen kann die Linienzahl explodieren -> entweder als Pattern-Kachel rendern (eine Definition, vielfach referenziert, statt tausende Einzellinien) **oder** beim Export Schraffuren in echte Linien/Hatch-Entities aufloesen (DXF-Hatch, Abschnitt 5).
|
||||
|
||||
### Empfehlung (Schraffuren)
|
||||
**SVG `<pattern>` mit `patternUnits="userSpaceOnUse"` als primaerer Renderpfad** (massstabskorrekt, exportierbar, performant durch Kachel-Referenzierung). Bei extrem grossen/dichten Flaechen Canvas-Overlay nur fuer die reine Bildschirm-Vorschau erwaegen; fuer Export immer SVG -> svg2pdf.js bzw. echte DXF-Hatches.
|
||||
|
||||
---
|
||||
|
||||
## 7. WebGPU vs. WebGL + Worker-Offloading der WASM-Geometrie
|
||||
|
||||
### Rendering: WebGPU vs. WebGL
|
||||
- **Reifegrad:** Seit Three.js r171 (Sept. 2025) ist der **WebGPURenderer produktionsreif** mit `import * as THREE from 'three/webgpu'` und **automatischem WebGL2-Fallback** — kein eigener Fallback-Code noetig. Quelle: <https://www.utsubo.com/blog/webgpu-threejs-migration-guide>
|
||||
- **Browser-Abdeckung:** Chrome/Edge 113 (Mai 2023), Safari 26.0 (Sept. 2025), Firefox 141 (Juli 2025). ~95% der Nutzer WebGPU-faehig, restliche ~5% bekommen WebGL2-Fallback. Quelle: <https://vr.org/articles/webgpu-baseline-2026-three-js-webxr-default>
|
||||
- **Performance (nuanciert):** Bei draw-call-lastigen Szenen (viele Bauteile/Etagen) gewinnt WebGPU deutlich (bei ~10'000 Draw-Calls ~50 FPS WebGPU vs. ~30 FPS WebGL); bei wenigen grossen Meshes kann WebGL noch gleichauf oder schneller sein. Compute-Shader (Punktwolken, Culling) sind ein WebGPU-Alleinstellungsmerkmal. Quellen: <https://medium.com/@sudenurcevik/upgrading-performance-moving-from-webgl-to-webgpu-in-three-js-4356e84e4702>, <https://altersquare.io/three-js-vs-webgpu-2026-large-scale-construction-viewers/>
|
||||
- **Vorsicht:** Es gibt weiterhin Szenarien, in denen WebGPU langsamer ist als WebGL — daher messen, nicht blind migrieren. Quelle: <https://github.com/mrdoob/three.js/issues/31055>
|
||||
|
||||
### Worker-Offloading der WASM-Geometrie
|
||||
- **Pflicht, nicht optional:** OCCT/replicad-Berechnungen (Booleans, HLR, Section) gehoeren in einen **Web Worker**, sonst blockiert die UI. replicad ist genau dafuer gebaut (WASM im Worker, Mesh zurueck an den Main-Thread). Quelle: <https://replicad.xyz/docs/use-as-a-library/>
|
||||
- **Muster:** Worker laedt das (grosse) WASM einmal; Kommunikation via Comlink o.ae.; Geometrie als Transferable (ArrayBuffer) zuruecksenden, um Kopierkosten zu sparen. Manifold (WASM) ebenso im Worker betreiben; Manifold ist intern stark parallelisiert.
|
||||
|
||||
### Empfehlung (Performance)
|
||||
**Three.js `three/webgpu`-Renderer mit automatischem WebGL2-Fallback** (gratis Abwaertskompatibilitaet, Vorteil bei vielen Draw-Calls/Etagen). **Alle WASM-Geometrie (OCCT/replicad + Manifold) konsequent in Web Worker(n)**, Ergebnis als Transferables. WebGPU-Compute fuer Punktwolken-/Culling-Beschleunigung als spaeteres Optimierungs-Upside. Vor groesserer WebGPU-Optimierung mit der echten Szene benchmarken.
|
||||
|
||||
---
|
||||
|
||||
## Empfehlung (Zusammenfassung)
|
||||
|
||||
| Thema | Wahl | Warum |
|
||||
|---|---|---|
|
||||
| **Geometriekernel (Solids/Booleans)** | **replicad** (= opencascade.js/OCCT) im Worker; **Manifold** als Mesh-Boolean-Ergaenzung | Echter B-Rep-Kernel noetig fuer exakte BIM-Geometrie + 2D-Ableitung; Manifold (Apache-2.0) schnell+robust fuer Mesh-Importe/Vorschau |
|
||||
| **2D-Ableitung (HLR/Schnitt)** | **replicad `drawProjection`** (OCC HLRBRep, liefert `{visible, hidden}`); OCCT-Section fuer Schnitte | Einziger seriöser Weg fuer verdeckte/sichtbare Kanten auf echtem B-Rep im Browser; Manifold `slice()` nur fuer reine Konturen |
|
||||
| **Plan-Clipping (`cutHeight`)** | **Three.js Clipping Planes** + **Object-Culling** (Etagen); echte Schnittkontur (OCCT/Manifold `slice`) fuer Export | GPU-Clipping fluessig & dynamisch fuer Viewport; Culling fuer Grob-Performance; exakte Kontur nur fuer druckbaren Plan |
|
||||
| **Import IFC** | **web-ifc + Fragments** (ThatOpen) | De-facto Open-BIM-Standard, native Speed, ~10x kleineres/schnelleres Fragments-Caching; MPL-2.0 |
|
||||
| **Import DXF / DWG** | **dxf-parser** (DXF) / **libredwg-web** (DWG) | Bewaehrt & browser-only; **DWG = GPL-3.0 -> Lizenz vorab klaeren/kapseln**, RAM-Limits bei grossen Dateien |
|
||||
| **Import STL/OBJ/XYZ/PCD** | **Native Three.js-Loader** + LOD/Downsampling | Out of the box (MIT); grosse Punktwolken brauchen Octree/Potree-Pattern |
|
||||
| **SVG -> PDF (Print)** | **svg2pdf.js + jsPDF** (+ pdf-lib optional) | Echte Vektoren statt Raster -> druckfaehig; MIT-Lizenzen |
|
||||
| **DXF-Export** | **@tarikjabiri/dxf** | TS, Browser-faehig, echte Hatch-/Block-Entities fuer CAD-Weiterverarbeitung; MIT |
|
||||
| **Schraffuren/Muster** | **SVG `<pattern>` mit `userSpaceOnUse`** | Massstabskorrekt (skaliert nicht mit Form), exportierbar, performant via Kachel-Referenz |
|
||||
| **Rendering** | **Three.js `three/webgpu`** mit WebGL2-Fallback | Produktionsreif seit r171, ~95% Abdeckung, Vorteil bei vielen Draw-Calls; gratis Fallback |
|
||||
| **WASM-Offloading** | **Web Worker** fuer OCCT/replicad + Manifold, Transferables | UI bleibt reaktiv; replicad ist dafuer gebaut; spart Kopierkosten |
|
||||
|
||||
---
|
||||
|
||||
### Wichtigste Risiken / offene Punkte
|
||||
1. **DWG-Lizenz (LibreDWG = GPL-3.0):** Vor Integration klaeren — sonst proprietaeres Produkt gefaehrdet. Option: separater Konvertierungs-Service oder DXF-Pflicht beim Import.
|
||||
2. **OCCT-WASM-Groesse & Build-Pflege:** zweistellige MB; Custom-Build/Tree-Shaking und Lazy-Loading im Worker einplanen.
|
||||
3. **HLR-Kosten:** `drawProjection` pro Ansicht cachen; ggf. `PolyAlgo` fuer schnelle Vorschau, `HLRBRep_Algo` fuer den finalen Plan.
|
||||
4. **WebGPU nicht blind:** mit echter Szene benchmarken — es gibt Faelle, in denen WebGL noch fuehrt.
|
||||
@@ -0,0 +1,590 @@
|
||||
# UX/UI-Patterns moderner Browser-CAD/BIM-Tools — Research & Leitplanken
|
||||
|
||||
> Recherche für unser Browser-BIM (React + TS + Three.js, DOSSIER-Port, Wohnbau,
|
||||
> CH/SIA-Kontext). Ziel: konkrete, **übernehmbare** Interaktions- und UI-Muster.
|
||||
> Stand: 2026-06-29.
|
||||
>
|
||||
> Untersucht: **Arcol**, **Snaptrude**, **TestFit**, **Onshape** (Browser-CAD),
|
||||
> **Vectorworks** (Resource/Navigation-Modell), **Figma** (Canvas-Interaktion,
|
||||
> Inspector). Querschnitt: Snapping/Inferencing, Grip-Editing, perceived
|
||||
> performance, Command-Palette, Onboarding.
|
||||
>
|
||||
> Jeder externe Claim ist mit Quelle verlinkt. Am Ende:
|
||||
> **„Leitplanken für unsere UI"** — priorisierte Empfehlungen.
|
||||
|
||||
---
|
||||
|
||||
## 0. Kurzfazit (TL;DR)
|
||||
|
||||
Die ganze Klasse moderner Browser-CAD/BIM-Tools konvergiert auf ein **gemeinsames
|
||||
Set von Mustern**, das wir fast 1:1 übernehmen sollten:
|
||||
|
||||
1. **Ein Modell, viele synchrone Sichten** (2D-Plan ⇄ 3D ⇄ Daten/Sheets), Änderung
|
||||
in einer Sicht propagiert sofort in alle anderen. (Arcol, Snaptrude)
|
||||
2. **Kontextuelle UI** statt voller Werkzeugleisten: Buttons/Felder erscheinen nur,
|
||||
wenn die aktuelle Auswahl sie erlaubt. (Arcol, Onshape, Figma)
|
||||
3. **Drei-Zonen-Layout**: links Navigator/Layer-Baum, Mitte Canvas + schwebende
|
||||
Tool-Palette, rechts Inspector. (Figma, Vectorworks, Onshape)
|
||||
4. **Aggressives Snapping/Inferencing** mit Live-Glyphen + Modifier zum
|
||||
Unterdrücken. (Onshape) — für Maus-im-Browser unverzichtbar.
|
||||
5. **Direkte Manipulation per Grips** statt Dialogen. (Figma, DOSSIER-Backlog)
|
||||
6. **Perceived performance** über Skeletons, optimistic UI und gescopte
|
||||
Ladezustände — im Browser-3D-Kontext ein Differenzierungs-Hebel.
|
||||
7. **Command-Palette (Ctrl/Cmd-K)** als Discovery- und Speed-Layer.
|
||||
8. **Onboarding via „learn by doing"** an einem mitgelieferten Sample-Projekt +
|
||||
progressive disclosure.
|
||||
|
||||
Unsere bestehende Architektur (semantisches Modell als Single Source of Truth,
|
||||
abgeleitete Sichten, Vectorworks-Terminologie) ist exakt der richtige Unterbau für
|
||||
diese Muster — die meiste Arbeit liegt in der **UI-Schicht**, nicht im Datenmodell.
|
||||
|
||||
---
|
||||
|
||||
## 1. Gesamtlayout & Navigator-/Layer-Panels
|
||||
|
||||
### 1.1 Was die Tools machen
|
||||
|
||||
**Figma** strukturiert die Fläche in vier Zonen: eine Toolbar, **zwei Panels** und
|
||||
einen scrollbaren Canvas. Links das **Navigation-Panel** mit Layern und Pages,
|
||||
rechts das **Properties-Panel**; der Layer-Baum „enthält und organisiert alle
|
||||
Elemente auf dem Canvas … und zeigt, wie Elemente verbunden sind"
|
||||
([Figma: left sidebar](https://help.figma.com/hc/en-us/articles/360039831974-View-layers-and-pages-in-the-left-sidebar),
|
||||
[Figma: Interface](https://www.inthepocket.design/course/figma-for-everyone/the-interface)).
|
||||
|
||||
**Vectorworks** trennt sauber zwei Konzepte, die für uns 1:1 relevant sind:
|
||||
- Die **Navigation Palette** gibt Zugriff auf *Classes, Design Layers, Sheet
|
||||
Layers, Viewports, Saved Views, References* — jeweils als eigener Tab mit Liste.
|
||||
Sichtbarkeit wird per Klick in einer **Visibility-Spalte** gesetzt; Doppelklick
|
||||
**aktiviert** einen Layer/eine Class. Die Zeichenfläche bleibt nutzbar, während
|
||||
die Palette offen ist
|
||||
([VW Navigation Palette](https://app-help.vectorworks.net/2022/eng/VW2022_Guide/Structure/The_Navigation_palette.htm)).
|
||||
- Paletten sind **andockbar/ein-/ausblendbar pro Workspace**
|
||||
([VW Palettes & Tool Sets](https://app-help.vectorworks.net/2018/eng/VW2018_Guide/Start/Palettes_and_Tool_Sets.htm)).
|
||||
|
||||
**Arcol** baut beim Modellieren einen **Model Tree** im Menü auf — pro
|
||||
BIM-Komponente wächst der Baum mit
|
||||
([AEC Magazine: Arcol BIM in browser](https://aecmag.com/bim/arcol-bim-cloud-browser/)).
|
||||
|
||||
**Onshape** zeigt links den **Feature-/Assembly-Baum** (parametrische Historie:
|
||||
Sketches, Features, Mates) — die Bauhistorie ist die primäre Navigation
|
||||
([Onshape UI Basics](https://cad.onshape.com/help/Content/ui-basics.htm)).
|
||||
|
||||
### 1.2 Übernahme für uns
|
||||
|
||||
Unser Dokumentmodell hat **zwei unabhängige Achsen** (siehe ROADMAP §2c):
|
||||
**Zeichnungsebenen** (Geschosse + Schnitte/Ansichten) und **Ebenen**
|
||||
(Grafik-Kategorien-Baum `00 Raster … 80 Plangrafik`). Das mappt fast wörtlich auf
|
||||
das Vectorworks-Navigations-Modell:
|
||||
|
||||
- **Linke Sidebar, getabbt** wie die VW-Navigation-Palette:
|
||||
- Tab **„Geschosse / Ansichten"** (= unsere Zeichnungsebenen): EG/1OG/…,
|
||||
Schnitte, Ansichten. Mit **Visibility-Toggle**, **Lock**, und **Doppelklick =
|
||||
aktiv setzen** (welches Geschoss editiert wird).
|
||||
- Tab **„Ebenen"** (= Grafik-Kategorien-Baum, in *jedem* Geschoss gleich): Baum
|
||||
mit Code, Name, Farb-Swatch, Linienstärke, Visibility, Hatch. Pro Ansicht
|
||||
schaltbar (das ist genau VWs „Sichtbarkeit pro Viewport/Saved View").
|
||||
- Tab **„BIM-Tree / Elemente"** (aus DOSSIER-Backlog §11: *Element-Übersicht
|
||||
Geschoss→Kind→Element, Suche, Shift-Klick = Zoom*). Das ist unser Pendant zu
|
||||
Arcols Model Tree + Onshapes Feature-Baum.
|
||||
- **Visibility-Spalte als Erstklass-Interaktion** (VW-Muster): Auge-Icon je Zeile,
|
||||
Klick togglet sofort, kein Dialog. (Wir haben bereits `EyeIcon.tsx` — das ist die
|
||||
Keimzelle.)
|
||||
- **Wichtig:** Geschoss wird **im 3D *oder* Plan** betrachtet, *keine* getrennten
|
||||
Daten — der View-Umschalter (siehe §2) gehört in die Geschoss-Auswahl, nicht in
|
||||
separate Dokumente.
|
||||
|
||||
> **Anti-Pattern vermeiden:** Vectorworks selbst leidet unter
|
||||
> **Paletten-Wildwuchs** (viele frei schwebende Fenster). Für ein fokussiertes
|
||||
> Wohnbau-Tool: **feste 3-Zonen-Shell** (links Navigator, rechts Inspector), nicht
|
||||
> N frei schwebende Palettenfenster. Figmas Striktheit schlägt VWs Flexibilität für
|
||||
> unsere Zielgruppe (Architekt:innen, die schnell ein EFH zeichnen wollen).
|
||||
|
||||
---
|
||||
|
||||
## 2. 3D ⇄ Plan-View-Umschaltung (das Herzstück)
|
||||
|
||||
### 2.1 Was die Tools machen
|
||||
|
||||
- **Snaptrude:** Nutzer arbeiten **in 2D *und* 3D gleichzeitig**; Änderung in einer
|
||||
Sicht spiegelt sich automatisch in der anderen. Objekte sind **nach Geschossen
|
||||
klassifiziert**, was es erlaubt, „3D-Geometrie zu zeichnen, während man in einer
|
||||
2D-Grundriss-Ansicht arbeitet". Push-an-einer-Fläche „fühlt sich an wie eine
|
||||
Linie ziehen", berechnet aber sofort Flächen/BIM-Daten neu
|
||||
([ArchDaily: Snaptrude](https://www.archdaily.com/1009121/snaptrude-the-browser-based-bim-tool-thats-changing-the-way-architects-work),
|
||||
[Snaptrude](https://www.snaptrude.com/)).
|
||||
- **Arcol:** „Every view is 3D in Arcol" — und **Boards** (Präsentations-Layouts)
|
||||
sind **live-synced**: ändert sich das Gebäude, aktualisieren sich die Layouts
|
||||
automatisch (kein statischer PDF-Export)
|
||||
([AEC Magazine: Arcol BIM 2.0](https://aecmag.com/bim/arcol-unleashed-bim-2-0/)).
|
||||
- **TestFit:** explizites **Expand/Collapse von Optionen** — man klappt Varianten
|
||||
auf zum Vergleichen und wieder zu, um sich aufs Detail-Editieren im Canvas zu
|
||||
konzentrieren („smoother flow between setting up a site, reviewing options, and
|
||||
refining a design")
|
||||
([TestFit 5.19](https://www.testfit.io/blog/testfit-5-19-a-new-generative-design-workflow)).
|
||||
|
||||
### 2.2 Übernahme für uns
|
||||
|
||||
Das ist **genau unsere Kern-Architektur** (ROADMAP §2c: „Ansichtstyp = Kamera-
|
||||
Projektion + optionaler Schnitt"). Konkrete UI-Muster:
|
||||
|
||||
- **View-Switcher als segmented control** direkt am Canvas (oben links oder
|
||||
oben mittig): `3D | Grundriss | Schnitt | Ansicht`. Plan-View = orthogonale
|
||||
Top-Kamera + Clipping-Ebene auf `okff + schnitthöhe` (steht bereits so im Modell).
|
||||
- **Kein** Moduswechsel der *Daten*, nur der **Kamera + Schnitt** — visuell als
|
||||
weicher Übergang (Kamera-Animation) kommunizieren, damit Nutzer Orientierung
|
||||
behalten (Snaptrude/Arcol-Gefühl: „dieselbe Sache aus anderem Winkel").
|
||||
- **Optional, stark differenzierend:** **Split-View** (3D links, Plan rechts) wie
|
||||
Snaptrudes „2D + 3D simultan". Da unsere Sichten ohnehin reaktiv aus *einem*
|
||||
Modell abgeleitet werden (Zustand-Store → SVG-Plan + Three-Scene), ist Split-View
|
||||
technisch billig und ein Wow-Moment im Onboarding.
|
||||
- **Kamera-Presets** (aus DOSSIER-Backlog): Kardinal N/O/S/W, Iso-Oktanten,
|
||||
**Norden-Rotation** (CH/Swisstopo-Georeferenz) — als kleine Würfel-/Kompass-Gizmo
|
||||
oben rechts im 3D (ViewCube-Muster, bekannt aus Onshape/CAD allgemein
|
||||
([Onshape UI Basics](https://cad.onshape.com/help/Content/ui-basics.htm))).
|
||||
- **Drawing Layers (Schnitte/Ansichten) sind „Saved Views"**: Auswahl in der linken
|
||||
Sidebar = Kamera + Schnitt + Maßstab + Layer-Kombination springt an (VW „Saved
|
||||
Views" / DOSSIER „Ausschnitte"). Das ersetzt Ordner-Wildwuchs bei 50+ Ansichten.
|
||||
|
||||
---
|
||||
|
||||
## 3. Zeichnen & Editieren: Snapping, Inferencing, Grips, Tool-Paletten
|
||||
|
||||
### 3.1 Snapping / Inferencing — das wichtigste Detail im Browser
|
||||
|
||||
**Onshape** ist hier die Referenz (sehr konkret dokumentiert,
|
||||
[Onshape Automatic Inferencing](https://cad.onshape.com/help/Content/Sketch/automatic_inferencing.htm)):
|
||||
|
||||
- **Inference-Typen:** horizontal, vertikal, **midpoint**, parallel, coincident,
|
||||
Ausrichtung zum Origin/zu anderen Entities, Tangente, Perpendikular.
|
||||
- **Visuelles Feedback:**
|
||||
- **gelbe Highlights** auf Vertices/Mittelpunkten beim Hovern,
|
||||
- **orange gestrichelte Linie** zeigt eine vorgeschlagene H/V-Ausrichtung,
|
||||
- **orange Highlight** verwandter Geometrie beim Ziehen (relationales Feedback).
|
||||
- **Steuerung:** Linksklick **akzeptiert** die vorgeschlagene Bedingung; **Shift
|
||||
gedrückt halten unterdrückt** Inferencing temporär (loslassen = wieder an).
|
||||
- **„Wake-up"-Inferences:** kurzes Verweilen über einer Geometrie „weckt" deren
|
||||
Bezugslinien (z. B. erst Mittelpunkt antippen, dann woanders zeichnen → bekommt
|
||||
Ausrichtung zu diesem Mittelpunkt).
|
||||
- **Post-hoc:** vorhandene Geometrie ziehen triggert erneut Inferencing (Center
|
||||
eines Kreises vertikal zum Origin ziehen → fügt automatisch vertikale Constraint).
|
||||
|
||||
**Constraint-Sichtbarkeit:** Constraint-Icons sind farbcodiert (blau =
|
||||
externe/Referenz, weiß = intern) und ein-/ausblendbar
|
||||
([Onshape Working with Constraints](https://cad.onshape.com/help/Content/Sketch/working_with_constraints.htm)).
|
||||
|
||||
### 3.2 Übernahme für uns (Snapping)
|
||||
|
||||
Für ein Maus-bedientes Browser-Tool ist **gutes Snapping der Unterschied zwischen
|
||||
„Spielzeug" und „Werkzeug".** Konkret:
|
||||
|
||||
- **Snap-Targets** (Wohnbau-relevant): Wand-Endpunkte/Achsen, Wand-Mittelpunkte,
|
||||
Rechtwinklig/Parallel zu bestehender Wand, **Raster** (Achsraster `00 Raster`),
|
||||
Öffnungs-Achsen, vorhandene 2D-Linien-Endpunkte, Schnittpunkte.
|
||||
- **Feedback exakt wie Onshape übernehmen:** Snap-Glyph am Cursor (●
|
||||
Endpunkt, △ Mitte, ⟂ rechtwinklig), **gestrichelte Hilfslinie** für
|
||||
Achsen-Alignment, Hover-Highlight des Snap-Ziels.
|
||||
- **Modifier:** **Shift unterdrückt Snapping** (Onshape-Konvention) — Nutzer
|
||||
erwarten das bereits aus anderen Tools. Zusätzlich **Ortho-Modus** (z. B. Shift
|
||||
für 0/45/90° beim Linienziehen — Figma-Konvention) sauber davon trennen oder per
|
||||
Toggle.
|
||||
- **Live-Maßeingabe beim Zeichnen** (CAD-Standard, auch Onshape): während des
|
||||
Ziehens Länge/Winkel tippbar (Tab zwischen Feldern). Das ersetzt nachträgliches
|
||||
Dimensionieren und ist für Architekt:innen Pflicht.
|
||||
- Wir brauchen **kein** volles Constraint-Solver-System wie Onshape (mechanisches
|
||||
parametrisches CAD). BIM-Wände sind achs-basiert; **Inferencing beim Setzen**
|
||||
genügt, persistente geometrische Constraints sind Overkill für Wohnbau.
|
||||
|
||||
### 3.3 Grip-Editing / direkte Manipulation
|
||||
|
||||
- **Figma:** Auswahl zeigt Bounding-Box mit **Resize-Handles**; ziehen
|
||||
manipuliert direkt; Smart-Guides/Maße erscheinen relativ zu Nachbarn beim Bewegen
|
||||
([Figma right sidebar](https://help.figma.com/hc/en-us/articles/360039832014-Design-prototype-and-explore-layer-properties-in-the-right-sidebar)).
|
||||
- **Snaptrude:** Push/Pull an Flächen als primäre 3D-Edit-Geste
|
||||
([ArchDaily](https://www.archdaily.com/1009121/snaptrude-the-browser-based-bim-tool-thats-changing-the-way-architects-work)).
|
||||
- **DOSSIER-Backlog (§11)** listet **Grip-Editing** (Wand-Endpunkte,
|
||||
Schnitt-Symbole im Plan) explizit als Aufwand L, Phase 3–4 — und das **9-Punkt-
|
||||
Objekt-Info** (lesen + verschieben/skalieren/rotieren direkt).
|
||||
|
||||
**Übernahme:** Grips sind der wichtigste „pro feel"-Hebel.
|
||||
- **Wand-Endpunkt-Grips** im Plan *und* 3D, mit Snapping (s. o.) und Live-Maß.
|
||||
- **Öffnungs-Grips** (Position entlang Wand, Breite) — Host-Beziehung bleibt erhalten.
|
||||
- **Schnittlinien-Grips im Plan** (Schnittlinie ziehen → Schnitt-Ansicht
|
||||
re-deriviert) — das ist Grip-Editing über die Sicht-Grenze hinweg, ein starkes
|
||||
Differenzierungsmerkmal.
|
||||
- Selektion → **bounding handles** (Figma-Muster) für 2D-Plangrafik (Linien,
|
||||
Rechtecke, Text).
|
||||
|
||||
### 3.4 Tool-Palette & kontextuelle Werkzeuge
|
||||
|
||||
**Kontextuelle UI ist das durchgehende Muster:**
|
||||
- **Arcol:** „buttons appear only when they can be used" — Loft/Sweep erscheinen bei
|
||||
Auswahl zweier Sketches, Boolean bei zwei Extrusions
|
||||
([AEC: Arcol sneak peek](https://aecmag.com/bim/arcol-a-sneak-peek/)).
|
||||
- **Onshape:** Sketch-Toolbar erscheint beim Betreten des Sketch-Modus; Tools in
|
||||
Gruppen (vertikale Trennlinien), Dropdown-Pfeile für Varianten; Dialoge mit
|
||||
**blau hinterlegtem Feld**, das eine Auswahl im Graphics-Bereich verlangt
|
||||
([Onshape sketch toolbar](https://cad.onshape.com/help/Content/Sketch/sketch_basics.htm),
|
||||
[Onshape Constraints](https://cad.onshape.com/help/Content/Sketch/working_with_constraints.htm)).
|
||||
- **Figma:** schmale Bottom-/Top-Toolbar mit den Kern-Tools; alles Weitere
|
||||
kontextuell rechts.
|
||||
|
||||
**Übernahme:**
|
||||
- **Schlanke Tool-Palette** (Figma-artig), gruppiert nach unserer Domäne:
|
||||
*Wand · Tür/Fenster · Decke · Treppe · Dach · Raum* (BIM) und *Linie · Polylinie
|
||||
· Rechteck · Kreis · Bogen · Text · Bemaßung* (2D auf `80 Plangrafik`).
|
||||
- **Modus-bewusste Tools:** im Grundriss andere Defaults als im 3D; im
|
||||
Schnitt/Ansicht nur Annotation/2D-Tools.
|
||||
- **Contextual action bar bei Auswahl** (Arcol-Muster): selektiere eine Wand →
|
||||
schwebende Mini-Toolbar „Tür einsetzen / Fenster / Wandtyp / verschneiden".
|
||||
Selektiere zwei Wände → „verschneiden / verlängern".
|
||||
- **Aktives Tool sticky + ESC bricht ab**, Leertaste = Pan, Scroll = Zoom
|
||||
(Figma/CAD-Konventionen — Nutzer bringen Muskelgedächtnis mit).
|
||||
- **LoD-bewusste Tool-/Stil-UI** (DOSSIER-Backlog: *zeigt nur passende Controls je
|
||||
Geometrie-Typ*) — keine Füll-Optionen bei 3D-Auswahl. Deckt sich mit Figmas
|
||||
„controls appear based on layer type".
|
||||
|
||||
---
|
||||
|
||||
## 4. Property-/Inspector-Panel
|
||||
|
||||
### 4.1 Was die Tools machen
|
||||
|
||||
**Figma — rechte Sidebar** (für uns das Vorbild,
|
||||
[Figma right sidebar](https://help.figma.com/hc/en-us/articles/360039832014-Design-prototype-and-explore-layer-properties-in-the-right-sidebar)):
|
||||
- Tabs **Design / Prototype** (bei Edit), **Inspect / Properties** (bei View-only).
|
||||
- Kategorien: **Alignment/Rotation/Position → Dimensions → Constraints/Layout →
|
||||
Appearance (Fill, Stroke, Effects) → Export**.
|
||||
- **Controls erscheinen je Layer-Typ** (kontextuell).
|
||||
- **Dev-Mode/Inspect** liefert konkrete Werte + Abstände zwischen Objekten + Code.
|
||||
|
||||
**Arcol — rechtes Panel** zeigt **Gebäude-Metriken** kontextuell: Geschossfläche,
|
||||
Anzahl Geschosse, GFZ/FAR, Unit-Count, Standortfläche, Kostenschätzung
|
||||
([Arcol BIM 2.0](https://aecmag.com/bim/arcol-unleashed-bim-2-0/)).
|
||||
|
||||
**TestFit — ein zentrales Parameter-Panel:** „alle Schlüsselparameter an einem
|
||||
Ort" (Unit-Counts, Parkplatz-Ziele, Gebäudegrößen-Limits) → sofortige Wirkung auf
|
||||
generierte Optionen
|
||||
([TestFit 5.19](https://www.testfit.io/blog/testfit-5-19-a-new-generative-design-workflow)).
|
||||
|
||||
**Onshape — Feature-Dialoge:** Erstellen/Editieren über Dialoge mit Pflicht-
|
||||
Selektionsfeldern (blau)
|
||||
([Onshape UI Basics](https://cad.onshape.com/help/Content/ui-basics.htm)).
|
||||
|
||||
### 4.2 Übernahme für uns
|
||||
|
||||
- **Rechter Inspector, kontextuell nach Element-Typ** (Figma-Muster):
|
||||
- **Wand** → Wandtyp (→ WallType-Bibliothek), Referenzlage mid/left/right
|
||||
(DOSSIER-Backlog), Höhe, Achs-Endpunkte (9-Punkt/Maße), Stil-Overrides.
|
||||
- **Tür/Fenster** → Breite/Höhe, Brüstung, Schwenkbogen/Anschlag, Detailgrad,
|
||||
Symbol.
|
||||
- **Decke/Slab** → SlabType, UK/OK-Override, Aussparungen.
|
||||
- **Raum** → SIA-416-Kategorie (HNF/NNF/VF/FF/GF/AGF), Fläche (read-only,
|
||||
berechnet), Stempel-Felder.
|
||||
- **2D-Element** → Linienstil (→ Line Manager), Hatch (→ Hatch Manager), Farbe.
|
||||
- **Sektionen kollabierbar** (Figma) — Wohnbau-Inspector kann lang werden;
|
||||
Default-Sektionen offen, Fortgeschrittenes (Overrides) zugeklappt
|
||||
(= progressive disclosure, s. §7).
|
||||
- **Mixed-value-Handling bei Mehrfachauswahl** (Figma): bei Mehrfachauswahl
|
||||
abweichende Werte als „Mixed/—" zeigen, gemeinsames Editieren erlauben. Wichtig
|
||||
z. B. „alle EG-Wände auf Wandtyp X".
|
||||
- **Live-Metriken-Block** (Arcol-Muster) — selbst im Wohnbau wertvoll:
|
||||
Bruttogeschossfläche, **SIA-416-Bilanz**, Raumzahl, Volumen. Im Inspector wenn
|
||||
nichts selektiert ist = „Projekt-Übersicht" (vgl. Figmas Canvas-Level-Optionen
|
||||
bei leerer Auswahl).
|
||||
- **Read-only-Felder klar markieren** (berechnete Flächen/Volumen) vs. editierbar.
|
||||
|
||||
---
|
||||
|
||||
## 5. Resource-Manager (Bibliotheken)
|
||||
|
||||
### 5.1 Was Vectorworks macht — unser direktes Vorbild
|
||||
|
||||
Der **Resource Manager** ist „ein zentraler Ort für Assets" (Symbole, Linientypen,
|
||||
Texturen, Materialien …)
|
||||
([VW Resource Manager](https://app-help.vectorworks.net/2026/eng/VW2026_Guide/ResourceManager/Resource%20Manager.htm)):
|
||||
- **Zwei-/Drei-Pane-Modell:** **File-Browser** (offene Dateien, Favoriten,
|
||||
VW-Libraries, User-/Workgroup-Libraries) → **Resource-Viewer** (Ressourcen der
|
||||
gewählten Datei) → optional **Preview/Metadaten**
|
||||
([VW File browser pane](https://app-help.vectorworks.net/2023/eng/VW2023_Guide/ResourceManager/Resource_Manager_File_browser_pane.htm),
|
||||
[VW Resource viewer pane](https://app-help.vectorworks.net/2026/eng/VW2026_Guide/ResourceManager/Resource_Manager_Resource_viewer_pane.htm)).
|
||||
- **Organisation mehrdimensional:** nach Quelle, **nach Typ** (Dropdown-Filter),
|
||||
nach Ordnerstruktur.
|
||||
- **Ansichten:** Thumbnails / List / Thumbnails-List; **Suchfeld** mit Filtern.
|
||||
- **Resource Selector**: dieselbe Bibliothek erscheint **in Dialogen** und zeigt
|
||||
dort nur **kontextuell passende** Ressourcen.
|
||||
|
||||
### 5.2 Übernahme für uns
|
||||
|
||||
Unsere ROADMAP §2d definiert bereits **Line Manager / Hatch Manager / Component
|
||||
Manager**, mit Verweis-per-id-Architektur (Components → Hatches → LineStyles). Das
|
||||
Vectorworks-Modell passt perfekt:
|
||||
|
||||
- **Ein gemeinsames Resource-Browser-Pattern** für alle Bibliotheken (Linienstile,
|
||||
Schraffuren, Components/Baustoffe, später Wand-/Öffnungs-Stil-Kataloge,
|
||||
Material-PBR, Raumstempel-Layouts). Eine wiederverwendbare React-Komponente,
|
||||
parametrisiert nach Ressourcentyp.
|
||||
- **Zwei Erscheinungsformen** (wie VW):
|
||||
1. **Manager-Ansicht** (großes Panel/Modal) zum Anlegen/Editieren/Duplizieren.
|
||||
2. **Inline-Resource-Selector** im Inspector — beim Setzen eines Wandtyps/Hatch
|
||||
öffnet sich ein kompakter Picker mit Thumbnails, gefiltert auf den passenden
|
||||
Typ. (Figma macht das analog mit „Styles/Variables".)
|
||||
- **Thumbnails sind im CAD-Kontext kritisch**: Hatch-Vorschau, Component-
|
||||
Schichtaufbau, Linienstil-Strich als gerenderte Mini-Previews.
|
||||
- **Zentrale Änderung propagiert** (unsere id-Referenz-Architektur): Component-Farbe
|
||||
ändern → alle Wände mit diesem Component aktualisieren live. Das ist Arcols
|
||||
„single source of truth" auf Ressourcen-Ebene.
|
||||
- **Cross-Projekt-Presets/Favoriten** (DOSSIER-Backlog, LocalStorage; VW-Favoriten):
|
||||
„einmal speichern, überall nutzen".
|
||||
|
||||
---
|
||||
|
||||
## 6. Perceived Performance (gefühlte Geschwindigkeit)
|
||||
|
||||
Browser-3D + WASM-Geometrie (web-ifc, OpenCascade) + HLR-Projektion = **echte
|
||||
Latenz** an mehreren Stellen. Gefühlte Performance ist hier ein
|
||||
Differenzierungs-Hebel gegenüber schwerfälligem Revit/ArchiCAD.
|
||||
|
||||
### 6.1 Belegte Muster
|
||||
|
||||
- **Skeleton-Screens** lassen Apps **20–30 % schneller** wirken als Spinner bei
|
||||
identischer realer Ladezeit
|
||||
([LogRocket: skeleton screens](https://blog.logrocket.com/ux-design/skeleton-loading-screen-design/),
|
||||
[UI Deploy](https://ui-deploy.com/blog/skeleton-screens-vs-spinners-optimizing-perceived-performance)).
|
||||
- **Indikator zur Situation passen:** Spinner für kurze Waits, Skeleton für
|
||||
Content, Progress-Bar für messbare Operationen, **optimistic UI** für
|
||||
„instant-feeling" Aktionen
|
||||
([Onething: skeleton vs spinner](https://www.onething.design/post/skeleton-screens-vs-loading-spinners)).
|
||||
- **Optimistic UI**: UI sofort aktualisieren, Server-Bestätigung abwarten, nur bei
|
||||
Fehler zurückrollen — ideal für häufige, risikoarme Aktionen
|
||||
([Smart Interface Design Patterns](https://smart-interface-design-patterns.com/articles/designing-better-loading-progress-ux/)).
|
||||
- **Verzögerung vor Indikator (100–200 ms):** schließt die Operation vorher ab,
|
||||
gar kein Indikator → kein Flackern
|
||||
([Onething](https://www.onething.design/post/skeleton-screens-vs-loading-spinners)).
|
||||
- **Gescopte Ladezustände** (React Suspense / Next loading.tsx): nur der betroffene
|
||||
Bereich lädt, der Rest bleibt interaktiv; `aria-busy`, Live-Regions,
|
||||
reduced-motion respektieren
|
||||
([LogRocket](https://blog.logrocket.com/ux-design/skeleton-loading-screen-design/)).
|
||||
|
||||
### 6.2 Übernahme für uns (konkret)
|
||||
|
||||
- **Optimistic Model-Edits:** Geometrie-Mutation (Wand ziehen, Tür setzen) **sofort**
|
||||
im Zustand-Store + 3D anzeigen; **schwere Booleans/HLR im Web Worker** (Comlink,
|
||||
bereits geplant) nachziehen. Wand erscheint sofort, die *exakte* verschnittene
|
||||
Öffnung/Schnittlinie „schärft nach". UI bleibt flüssig.
|
||||
- **Progressive Plan-Generierung:** SVG-Grundriss zuerst grob (Achsen/Linien),
|
||||
Schraffuren/Symbole nachladen — Skeleton/„low-detail first" statt Spinner.
|
||||
- **HLR-Schnitte (Risiko #4):** Worker + Caching (ROADMAP §6). UI: **Skeleton der
|
||||
Schnitt-Ansicht** + „berechne verdeckte Kanten…" mit Progress, restliche App
|
||||
bleibt nutzbar (gescopter Ladezustand).
|
||||
- **Delay-then-show** für alle Worker-Tasks (150 ms-Schwelle), sonst Flicker beim
|
||||
schnellen Editieren.
|
||||
- **Three.js-Disziplin:** stabile 60 fps beim Orbit/Pan ist selbst „perceived
|
||||
performance" — instanziertes Rendering, Frustum-Culling, LoD für ferne Geometrie
|
||||
(deckt sich mit ROADMAP-Phase-7-Performance-Härtung). Lieber 60 fps bei grober
|
||||
Geometrie als ruckelnde Präzision.
|
||||
- **Auto-Save-Status klar, unaufdringlich** kommunizieren („Gespeichert"/„Speichern…"),
|
||||
optimistic — nie blockierend (Figma-Muster).
|
||||
|
||||
---
|
||||
|
||||
## 7. Onboarding & Discoverability
|
||||
|
||||
### 7.1 Belegte Muster
|
||||
|
||||
- **Progressive Disclosure:** zuerst nur Essenzielles zeigen, Komplexität schrittweise
|
||||
enthüllen — reduziert kognitive Last; drei Typen: step-by-step, conditional,
|
||||
contextual
|
||||
([IxDF: Progressive Disclosure](https://ixdf.org/literature/topics/progressive-disclosure),
|
||||
[UXPin](https://www.uxpin.com/studio/blog/what-is-progressive-disclosure/)).
|
||||
- **„Learn by doing" an Sample-Dokument:** Grammarly startet Nutzer mit einem
|
||||
Beispiel-Dokument mit Fehlern; Hotspots/Tooltips führen durch Features
|
||||
([Userpilot: onboarding examples](https://userpilot.com/blog/user-onboarding-examples/)).
|
||||
- **Stufenweises Aufdecken fortgeschrittener Features** (Asana: erst Projekt
|
||||
anlegen, später Dependencies/Kanban/Gantt)
|
||||
([Userpilot: progressive disclosure](https://userpilot.com/blog/progressive-disclosure-examples/)).
|
||||
- **Command-Palette als Discovery-Layer:** durchsuchbare Befehlsliste hilft, Features
|
||||
zu entdecken — „incredible effect on exploration and feature discoverability",
|
||||
besonders für neue/seltene Nutzer
|
||||
([Mobbin: command palette](https://mobbin.com/glossary/command-palette),
|
||||
[Untitled UI: command menus](https://www.untitledui.com/components/command-menus)).
|
||||
- **Arcol** wirbt explizit mit „low barrier to entry, gentle learning curve … clean,
|
||||
intuitive, requires minimal training"
|
||||
([AEC: Arcol BIM 2.0](https://aecmag.com/bim/arcol-unleashed-bim-2-0/)).
|
||||
|
||||
### 7.2 Übernahme für uns
|
||||
|
||||
- **Mitgeliefertes Sample-Projekt** (wir haben bereits `sampleProject.ts`!) als
|
||||
Onboarding-Bühne: ein kleines EFH, fertig modelliert. Nutzer **manipuliert echtes
|
||||
Modell** statt leerem Canvas (Grammarly-Muster). Erste Geste: „zieh diese Wand"
|
||||
→ sieht 3D + Plan live mitlaufen (unser Kern-Wow).
|
||||
- **Progressive Disclosure im Inspector & Tool-Palette:** Default zeigt
|
||||
Wohnbau-Essenz (Wand/Tür/Fenster/Decke/Raum). Fortgeschrittenes (Prioritäts-
|
||||
Verschneidung, Overrides, Detailgrade, Section-Styles) **zugeklappt / hinter
|
||||
„Erweitert"**. Das passt zu unserem radikalen Wohnbau-Fokus.
|
||||
- **Contextual coachmarks** statt langem Tutorial: beim ersten Selektieren einer
|
||||
Wand ein kleiner Tooltip „Endpunkt ziehen zum Verlängern, Doppelklick für Wandtyp".
|
||||
- **Command-Palette (Ctrl/Cmd-K)** — siehe §8 — doppelt als Onboarding: alle
|
||||
Werkzeuge/Befehle durchsuchbar = lebende Feature-Liste.
|
||||
- **Tastatur-Kürzel sichtbar machen** (in Tooltips, in der Palette) — schult
|
||||
beiläufig pro Workflows.
|
||||
|
||||
---
|
||||
|
||||
## 8. Command-Palette & Tastatur
|
||||
|
||||
### 8.1 Belegte Muster
|
||||
|
||||
- **Ctrl/Cmd-K** ist die De-facto-Konvention (Linear, Figma [Cmd-P], Notion,
|
||||
Vercel, Raycast, Slack, Superhuman); VS Code nutzt Cmd-Shift-P
|
||||
([Mobbin](https://mobbin.com/glossary/command-palette),
|
||||
[Superhuman: command palette](https://blog.superhuman.com/how-to-build-a-remarkable-command-palette/)).
|
||||
- Zwei Haupt-Use-Cases: **Navigation/Suche** und **Shortcuts/Quick Actions**
|
||||
([Outdraw Academy: command palette](https://outdraw-academy.gitbook.io/ux-patterns/command-palette)).
|
||||
- Trigger kann **sichtbar** (Button/Suchleiste) oder nur per Shortcut sein —
|
||||
für Discovery besser **auch sichtbar**
|
||||
([Mobbin](https://mobbin.com/glossary/command-palette)).
|
||||
|
||||
### 8.2 Übernahme für uns
|
||||
|
||||
- **Cmd/Ctrl-K-Palette** für: Werkzeug aktivieren („Wand", „Tür"), Ansicht
|
||||
springen („Grundriss EG", „Schnitt A-A"), Ressource öffnen („Hatch Manager"),
|
||||
globale Aktionen („Norden rotieren", „PDF exportieren", „SIA-Bilanz").
|
||||
- **Sichtbarer Trigger** (Such-/Befehlsfeld in der Top-Bar) für Entdeckung +
|
||||
Shortcut für Speed.
|
||||
- **Konsistente, dokumentierte Shortcuts** (Figma-Disziplin): W=Wand, T=Tür,
|
||||
L=Linie, Space=Pan, Scroll=Zoom, Shift=Snap aus, Esc=Abbrechen, G=Grundriss-Toggle.
|
||||
In Tooltips + Palette anzeigen.
|
||||
|
||||
---
|
||||
|
||||
## 9. Leitplanken für unsere UI (priorisierte Empfehlungen)
|
||||
|
||||
> Sortiert nach **Hebel × Aufwand**. „P#" = grobe Phasen-Zuordnung zur ROADMAP.
|
||||
|
||||
### A. Sofort / Fundament (Phase 0–1) — billig, prägt alles
|
||||
|
||||
1. **Feste 3-Zonen-Shell.** Links **Navigator** (Tabs: *Geschosse/Ansichten · Ebenen
|
||||
· BIM-Tree*, je mit Visibility-Toggle wie VW Navigation Palette). Mitte **Canvas**
|
||||
mit schwebender Tool-Palette + View-Switcher. Rechts **Inspector** (kontextuell).
|
||||
*Keine* frei schwebenden Palettenfenster (Anti-VW-Wildwuchs).
|
||||
2. **View-Switcher als segmented control** `3D | Grundriss | Schnitt | Ansicht`
|
||||
direkt am Canvas; weiche Kamera-Übergänge; *eine* Datenquelle (kein Daten-
|
||||
Moduswechsel). Nutzt unsere bestehende „abgeleitete Sichten"-Architektur.
|
||||
3. **Visibility/Lock pro Zeile** als Erstklass-Interaktion (1 Klick, kein Dialog) —
|
||||
`EyeIcon.tsx` ausbauen.
|
||||
4. **Kontextueller Inspector**: Controls nur für den selektierten Element-Typ
|
||||
(Figma/Arcol); berechnete Felder read-only markiert; Sektionen kollabierbar.
|
||||
5. **Tastatur-Grundlagen + Konventionen festnageln**: Space=Pan, Scroll=Zoom,
|
||||
Esc=Abbrechen, Shift=Snap aus. Früh festlegen → Muskelgedächtnis.
|
||||
|
||||
### B. Kern-„Pro-Feel" (Phase 1–2) — der eigentliche Wert
|
||||
|
||||
6. **Snapping/Inferencing nach Onshape-Vorbild**: Snap-Glyphen am Cursor,
|
||||
gestrichelte Achsen-Hilfslinien, Hover-Highlight, **Shift = unterdrücken**,
|
||||
Wake-up-Inferences. Targets: Wand-Enden/Achsen/Mitten, Raster, Rechtwinklig/
|
||||
Parallel, Öffnungs-Achsen. **Höchste Priorität für „Werkzeug-Gefühl".**
|
||||
7. **Live-Maßeingabe beim Zeichnen** (Länge/Winkel tippbar, Tab zwischen Feldern).
|
||||
8. **Kontextuelle Action-Bar bei Auswahl** (Arcol): Wand selektiert → „Tür/Fenster
|
||||
einsetzen / Wandtyp / verschneiden". Buttons erscheinen nur, wenn anwendbar.
|
||||
9. **Schlanke domänen-gruppierte Tool-Palette** (BIM-Bauteile + 2D-Zeichnen),
|
||||
modus-bewusst je Ansicht.
|
||||
10. **Optimistic Edits + Worker-Nachzug**: Mutation sofort sichtbar, schwere
|
||||
Booleans/HLR im Worker (Comlink); Delay-then-show (150 ms) statt Spinner-Flicker.
|
||||
|
||||
### C. Differenzierung (Phase 3) — hier gewinnen wir
|
||||
|
||||
11. **Grip-Editing** (Wand-Endpunkte, Öffnungs-Position/-Breite, **Schnittlinie im
|
||||
Plan**) mit Snapping + Live-Maß, in 2D *und* 3D. (DOSSIER-Backlog, Aufwand L —
|
||||
aber Kern-Differenzierer.)
|
||||
12. **Saved Views / Ausschnitte** in der linken Sidebar = Kamera + Schnitt + Maßstab
|
||||
+ Layer-Kombination per Klick (VW „Saved Views" / DOSSIER). Skaliert auf 50+
|
||||
Ansichten.
|
||||
13. **Wiederverwendbares Resource-Browser-Pattern** für Line/Hatch/Component-Manager:
|
||||
Zwei-Pane (File-Browser → Viewer mit Thumbnails) als Manager *und* als inline
|
||||
Picker im Inspector (VW-Modell). Zentrale Änderung propagiert (id-Referenzen).
|
||||
14. **Perceived-performance-Politik festschreiben**: Skeletons für Plan-/Schnitt-
|
||||
Generierung, gescopte Ladezustände (restliche App bleibt nutzbar),
|
||||
reduced-motion/`aria-busy` respektieren.
|
||||
15. **Split-View 3D|Plan** (Snaptrude-Muster) — billig dank reaktiver Sichten,
|
||||
starker Wow-Effekt; auch fürs Onboarding.
|
||||
|
||||
### D. Adoption & Politur (Phase 1 fortlaufend → 7)
|
||||
|
||||
16. **Onboarding via Sample-Projekt** (`sampleProject.ts` als fertiges EFH);
|
||||
„learn by doing", erste Geste zeigt 3D⇄Plan-Live-Sync. Progressive Disclosure:
|
||||
Wohnbau-Essenz default, Fortgeschrittenes (Prioritäts-Verschneidung, Overrides,
|
||||
Detailgrade) zugeklappt.
|
||||
17. **Command-Palette (Cmd/Ctrl-K)** + sichtbarer Trigger: Werkzeuge/Ansichten/
|
||||
Ressourcen/Aktionen durchsuchbar; doppelt als Discovery-Layer.
|
||||
18. **Live-Metriken-Block** im Inspector (Arcol): BGF, **SIA-416-Bilanz**, Räume,
|
||||
Volumen — bei leerer Auswahl als Projekt-Übersicht.
|
||||
19. **CH-Spezifika UI-seitig vorsehen**: **Norden-Rotation**-Gizmo (ViewCube/Kompass),
|
||||
SIA-Raumkategorien im Raum-Inspector, später Swisstopo-Import-Flow mit Auto-Zoom
|
||||
+ Nullpunkt-Verschiebung.
|
||||
|
||||
### Übergreifende Designprinzipien (gelten immer)
|
||||
|
||||
- **Kontextualität vor Vollständigkeit** — zeige nur, was jetzt anwendbar ist
|
||||
(Arcol/Onshape/Figma). Direkt verzahnt mit unserem „LoD-bewusste Stil-UI"-Backlog.
|
||||
- **Direkte Manipulation vor Dialogen** — Grips/Drag/Inline-Edit schlägt
|
||||
Properties-Dialog (Figma/Snaptrude/DOSSIER).
|
||||
- **Eine Wahrheit, viele Sichten** — niemals Sicht-spezifische Daten; alles aus dem
|
||||
semantischen Modell ableiten (deckt sich exakt mit unserem Architektur-Prinzip).
|
||||
- **Konventionen respektieren** — Pan/Zoom/Snap/Esc/Cmd-K wie die etablierten Tools;
|
||||
Nutzer bringen Muskelgedächtnis aus Figma/CAD mit.
|
||||
- **Gefühlte > tatsächliche Geschwindigkeit** — optimistic + Skeletons + 60 fps;
|
||||
im schweren Browser-3D-Geometrie-Kontext ein echter Wettbewerbsvorteil.
|
||||
|
||||
---
|
||||
|
||||
## Quellen
|
||||
|
||||
**Arcol**
|
||||
- [Arcol unleashed – BIM 2.0 (AEC Magazine)](https://aecmag.com/bim/arcol-unleashed-bim-2-0/)
|
||||
- [Arcol – BIM in a browser (AEC Magazine)](https://aecmag.com/bim/arcol-bim-cloud-browser/)
|
||||
- [Arcol: a sneak peek (AEC Magazine)](https://aecmag.com/bim/arcol-a-sneak-peek/)
|
||||
- [The Arcol Manifesto](https://arcol.io/blog/the-arcol-manifesto)
|
||||
|
||||
**Snaptrude**
|
||||
- [Snaptrude – browser-based BIM tool (ArchDaily)](https://www.archdaily.com/1009121/snaptrude-the-browser-based-bim-tool-thats-changing-the-way-architects-work)
|
||||
- [Snaptrude (offiziell)](https://www.snaptrude.com/)
|
||||
|
||||
**TestFit**
|
||||
- [TestFit 5.19: A New Generative Design Workflow](https://www.testfit.io/blog/testfit-5-19-a-new-generative-design-workflow)
|
||||
- [TestFit (offiziell)](https://www.testfit.io/)
|
||||
|
||||
**Onshape**
|
||||
- [Onshape: User Interface Basics](https://cad.onshape.com/help/Content/ui-basics.htm)
|
||||
- [Onshape: Automatic Inferencing](https://cad.onshape.com/help/Content/Sketch/automatic_inferencing.htm)
|
||||
- [Onshape: Working with Constraints](https://cad.onshape.com/help/Content/Sketch/working_with_constraints.htm)
|
||||
- [Onshape: Sketch Basics](https://cad.onshape.com/help/Content/Sketch/sketch_basics.htm)
|
||||
|
||||
**Vectorworks**
|
||||
- [VW Resource Manager](https://app-help.vectorworks.net/2026/eng/VW2026_Guide/ResourceManager/Resource%20Manager.htm)
|
||||
- [VW Resource Manager: File browser pane](https://app-help.vectorworks.net/2023/eng/VW2023_Guide/ResourceManager/Resource_Manager_File_browser_pane.htm)
|
||||
- [VW Resource Manager: Resource viewer pane](https://app-help.vectorworks.net/2026/eng/VW2026_Guide/ResourceManager/Resource_Manager_Resource_viewer_pane.htm)
|
||||
- [VW Navigation Palette](https://app-help.vectorworks.net/2022/eng/VW2022_Guide/Structure/The_Navigation_palette.htm)
|
||||
- [VW Palettes and Tool Sets](https://app-help.vectorworks.net/2018/eng/VW2018_Guide/Start/Palettes_and_Tool_Sets.htm)
|
||||
|
||||
**Figma**
|
||||
- [Figma: right sidebar / layer properties](https://help.figma.com/hc/en-us/articles/360039832014-Design-prototype-and-explore-layer-properties-in-the-right-sidebar)
|
||||
- [Figma: left sidebar (layers & pages)](https://help.figma.com/hc/en-us/articles/360039831974-View-layers-and-pages-in-the-left-sidebar)
|
||||
- [Figma for Everyone: The Interface](https://www.inthepocket.design/course/figma-for-everyone/the-interface)
|
||||
|
||||
**Perceived Performance**
|
||||
- [LogRocket: Skeleton loading screen design](https://blog.logrocket.com/ux-design/skeleton-loading-screen-design/)
|
||||
- [UI Deploy: Skeleton Screens vs. Spinners](https://ui-deploy.com/blog/skeleton-screens-vs-spinners-optimizing-perceived-performance)
|
||||
- [Onething: Skeleton Screens vs Loading Spinners](https://www.onething.design/post/skeleton-screens-vs-loading-spinners)
|
||||
- [Smart Interface Design Patterns: Loading & Progress UX](https://smart-interface-design-patterns.com/articles/designing-better-loading-progress-ux/)
|
||||
|
||||
**Command Palette**
|
||||
- [Mobbin: Command Palette](https://mobbin.com/glossary/command-palette)
|
||||
- [Untitled UI: Command menus (Cmd-K)](https://www.untitledui.com/components/command-menus)
|
||||
- [Superhuman: How to build a remarkable command palette](https://blog.superhuman.com/how-to-build-a-remarkable-command-palette/)
|
||||
- [Outdraw Academy: Command Palette UX pattern](https://outdraw-academy.gitbook.io/ux-patterns/command-palette)
|
||||
|
||||
**Onboarding / Progressive Disclosure**
|
||||
- [IxDF: Progressive Disclosure](https://ixdf.org/literature/topics/progressive-disclosure)
|
||||
- [UXPin: What Is Progressive Disclosure](https://www.uxpin.com/studio/blog/what-is-progressive-disclosure/)
|
||||
- [Userpilot: User onboarding examples](https://userpilot.com/blog/user-onboarding-examples/)
|
||||
- [Userpilot: Progressive disclosure examples](https://userpilot.com/blog/progressive-disclosure-examples/)
|
||||
Reference in New Issue
Block a user