ca859c4aa4
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.
339 lines
16 KiB
Markdown
339 lines
16 KiB
Markdown
# 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.
|