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.
16 KiB
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):
view_dir= senkrecht zur Linie in XY, Richtung =directionSign.- 3D-Vorschau:
THREE.Planes setzen —- Cut (nur
cutAtLine): auf der Linie, Normale+view_dir. - Back (immer): um
depthBackin+view_dirversetzt, Normale−view_dir. - via
renderer.localClippingEnabled = true,material.clippingPlanes.
- Cut (nur
- Kamera:
OrthographicCamera, Positionmid − view_dir·dist, Targetmid, Up+Z; Zoom auf BBox (linePoints+ Höhenbereich +depthBack). Beiperspective:PerspectiveCamera+ FOV. - 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 (onDoubleClickauf 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; beiTHREE.OrthographicCameracamera.zoom = canvasWidthCssPx / (frustumWidth_world / metersPerPixelAtZoom1)bzw. direktleft/rightsetzen.
// 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.patternUnitsso wählen, dass das Muster die gewünschte Paper-Dichte hat. Formel 1:1 übernehmen. - Linetype-Dash:
stroke-dasharrayin 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 auscamera. 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
northAnglerotiert (Portset_cardinal_view,_set_iso,set_top_view).northAngleliegt imProject(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>mitbaseline-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(oderpdf-libmit 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 viamm·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)
- Phase 1 (MVP): Grundriss-Generator ✅ ausbauen (Schraffuren, LoD), Live- Grundriss neben 3D, Basis-Bemaßung; Massstab pro Viewport (§3).
- Phase 3 ⭐: Schnitt/Ansicht via HLR (§4, Worker), Auto-Bemaßung (§7), Ausschnitte + Layer-Kombinationen (§5), Kamera-Presets + Norden (§6), Sheets + Vektor-PDF (§8).
- Phase 4: DXF-Export (§9), Detail↔Ausschnitt-Sync-Politur.