# 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 ``- `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 `` 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 ``` - **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` → ``-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.