# 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.
