Files
karim ca859c4aa4 Browser-BIM (cad): semantisches Modell, abgeleitete 2D/3D-Sichten, Zeichenwerkzeuge
Standalone-Browser-Port von DOSSIER. Enthaelt das semantische Modell mit
Plan-/3D-Ableitung, Zeichen- und Editierwerkzeuge, Rhino-artiges Befehlssystem,
dockbares Panel-System, Resource-Manager, DXF/.lin/.pat-Import, i18n (de/en)
sowie Projektdokumentation und Probe-Harness.
2026-06-30 20:52:27 +02:00

339 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.