Files
DOSSIER-STANDALONE/docs/design/plans-output.md
T
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

16 KiB
Raw Blame History

Design — Pläne & Output

Teil der Standalone-Architektur — siehe ../../ARCHITECTURE.md. Bauteile: elements.md. Ressourcen/Stile: 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
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:

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.Planes 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.
// 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.

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

// 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)

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

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

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

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):

// 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:

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; styleIdstroke/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.