Files
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

19 KiB
Raw Permalink Blame History

Design — Bauteile (Elements)

Teil der Standalone-Architektur — siehe ../../ARCHITECTURE.md. Output/Pläne: plans-output.md. Ressourcen/Stile: resources-graphics.md.

Dieses Dokument legt die Daten, die Generierung (3D-Geometrie + Plan- Symbolik) und das Grip-Editing je Bauteil fest und übersetzt DOSSIERs elemente.py (7244 LOC, Monolith) in kleine Bauteil-Module (src/model/ elements/wall.ts, opening.ts, …). Bezeichner englisch, Prosa deutsch, Meter.

DOSSIERs Architektur dort: pro Element eine Achse/Outline-Source (editierbar)

  • ein auto-generiertes Volumen (wand_axis+wand_volume, Outline+Brep). Browser-Äquivalent: das semantische Element ist die Source; Geometrie wird per generate*() abgeleitet (nie persistiert). Das ist sauberer als DOSSIERs zwei-Objekt-Modell und kennt kein Cache-Stale.

0. Gemeinsames Fundament

// src/model/elements/base.ts
interface ElementBase {
  id: string;
  type: ElementType;          // "wall" | "window" | "door" | "slab" | "stair" | "roof"
                              //   | "column" | "beam" | "space" | "draw2d"
  floorId: string;            // Zeichnungsebene (Geschoss); bei gehosteten via Host
  categoryCode: string;       // Ebene (Grafik-Kategorie), z.B. "20"
  styleId?: string;           // optionaler Element-Override-Stil (resources-graphics.md)
  name?: string;
}

Geometrie-Konvention (aus CONVENTIONS.md, im Spike etabliert): Wand-Normale n = leftNormal(u) = (-u.y, u.x); bei CCW-Wicklung zeigt +n nach innen. Schichten werden außen (-T/2) → innen (+T/2) gestapelt (generatePlan.addWallPoche, Viewport3D.addWallMeshes).

Detailgrad (LoD) — DOSSIERs darstellung (auto|einfach|standard|detail):

type DetailLevel = "coarse" | "medium" | "fine";   // einfach | standard | detail
// Auflösung: Element-Wert "auto" → Dokument-/Snapshot-Wert; sonst Element-Wert.
function resolveDetail(el: ElementBase, doc: { detailLevel: DetailLevel }): DetailLevel

≙ DOSSIER _resolve_oeff_darstellung + get_aktive_darstellung. Steuert, wie viel Symbolik gezeichnet wird (1:500 Rechteck → 1:50 Glas/Sims/Schwenkbogen).

Generierungs-Signaturen (jedes Modul exportiert beides):

function build3d(project, el, ctx): THREE.Object3D        // Volumen (Schichten/Brep)
function generatePlan(project, el, ctx, lod): Primitive[] // Schnittflächen + Symbol
// ctx trägt baseElevation, joins, sichtbare Codes, resolver für Components/Styles

1. Wand (Wall) — mehrschichtig

1.1 Daten

interface Wall extends ElementBase {
  type: "wall";
  start: Vec2; end: Vec2;          // Achse (Centerline) im Grundriss        [im Spike]
  wallTypeId: string;              // → WallType.layers (außen→innen)
  height: number;
  reference: "mid" | "left" | "right";   // Referenzlage der Achse (DOSSIER _wand_referenz)
  baseOffset?: number;             // UK relativ zu OKFF (default 0)
  topOffset?: number;              // OK-Override (default = floorHeight)
  jointRole?: "auto" | "through" | "butt";  // T-Stoss-Rolle (DOSSIER wand_joint_rolle)
  // Mehrsegment-Wände (Polyline): optional axisPoints statt start/end
  axisPoints?: Vec2[];
}

reference verschiebt die Achse auf Außenkante/Mitte (DOSSIER _wall_offsets_from_referenz): hilft beim Modellieren und beim Import fremder Pläne (ROADMAP §11). Offsets: mid → [+T/2, -T/2], left → [0, -T], right → [+T, 0].

1.2 Generierung — 3D + Plan (Status: im Spike, einschichtig→mehrschichtig )

Beide Sichten extrudieren/füllen dasselbe gehrte Band-Polygon pro Schicht. Heute schon vorhanden:

  • geometry.clippedBand(start, end, offA, offB, startCut, endCut) — Band mit Gehrungsschnitt.
  • generatePlan.addWallPoche — pro Schicht ein gefülltes Polygon (Component-Fill + Schraffur), Öffnungen ausgespart.
  • Viewport3D.addLayerPrism — dasselbe Polygon via ExtrudeGeometry.

1.3 Wand-Verschneidung (Joins) — Risiko #1

Status: L-Ecken-Gehrung (joins.computeJoinsmiterLine, robust gegen Wicklung + ungleiche Dicken). Offen: Prioritäts-T-/X-Stöße bei mehrschichtigen Wänden.

DOSSIERs gelöste Logik (elemente._t_junction_layer_overrides, _wand_should_apply_t_miter), die wir portieren:

  1. Knoten finden: Endpunkte auf Gitter runden (roundKey, existiert), gruppieren. ==1 freies Ende, ==2 L-Ecke (Gehrung, ), >2 T/X.
  2. Through-Wand bestimmen: an einem T-Stoß läuft genau eine Wand durch. Auswahl nach jointRole (DOSSIER-Regel), sonst nach Component-joinPriority:
    my.role="through" → ich laufe durch (kein Miter)
    my.role="butt"    → ich stoße an (Miter)
    beide "auto"      → höhere joinPriority = Through-Wand
    
  3. Schicht-Durchdringung (Backbone): nur das Material mit der höchsten gemeinsamen joinPriority in beiden Wänden läuft durch und unioniert (T-Form). Beispiel ROADMAP §2d: Beton (800) läuft mittig durch; Putze (100) verbinden sich seitlich, gehen aber nirgends durch den Beton. Alle Nicht- Backbone-Schichten der anstoßenden Wand mitern an der Through-Außenkante (standard_miter). Ergebnis: gleichfarbige Außenlagen bilden automatisch saubere L-Stöße.
// joins.ts — Erweiterung der bestehenden API
interface WallCuts { startCut: Line|null; endCut: Line|null;
  // neu: pro-Schicht Overrides am T-Stoss
  layerExtensions?: number[];   // wie weit jede Schicht in Through-Body drillt
  layerMiters?: (Line|null)[];  // pro-Schicht Mitre (null = Backbone, läuft durch)
}
function computeJoins(project, walls): Map<string, WallCuts>   // erweitert

Implementierungsplan (stufenweise, Risiko #1):

  • (a) L-Gehrung bleibt.
  • (b) T-Stoß ohne Schichten: Backbone = ganze Wand; Through union, Stem mitert.
  • (c) T-Stoß mit Schichten: Backbone-Material-Logik wie oben (Port von _t_junction_layer_overrides).
  • (d) X-Stoß: paarweise als zwei T behandeln.
  • Booleans: Union/Extension der Backbone-Säule via OpenCascade.js/Manifold im Worker (workers/geometry.worker.ts), nur für 3D + exakten B-Rep-Export; der 2D-Plan bleibt rein analytisch (Polygon-Clipping, kein Kernel) — schnell.
  • Validierung: Screenshot-Probe der T-Ecke (Beton durch, Putz seitlich).

1.4 Grip-Editing (Risiko #L, Phase 34)

DOSSIER: Display-Conduit zeichnet dicke Marker an Achs-Endpunkten, MouseCallback fängt Klick → GetPoint mit Snap → _replace_axis_vertex → Volumen regeneriert (wand_grips.py). Browser-Port:

  • Marker: SVG-Kreise (r ≈ 7 px) an Endpunkten/Knicks der selektierten Wand, als Overlay über dem Plan (unabhängig von Ebenen-Sichtbarkeit) — exakt DOSSIERs Conduit-Idee.
  • Hit-Test: Pointer-Distanz < 14 px (DOSSIER _HIT_RADIUS_PX).
  • Drag: pointerdown auf Marker → Live-Preview-Linien zu Nachbar-Vertices → Snap (Endpunkt/Ortho/Raster) → pointerupstore.apply(p => wall.start = newPt). Abgeleitete Sichten (Plan + 3D) re-derivieren reaktiv — kein manuelles Regen.
  • Funktioniert für Line (2 Grips) und Polyline (jeder Knick ein Grip), wie DOSSIER.

2. Öffnungen (Window / Door) — gehostet, LoD

2.1 Daten

interface OpeningBase extends ElementBase {
  hostWallId: string;          // Host-Wand (Geschoss ergibt sich daraus)  [im Spike]
  position: number;            // Abstand entlang Wandachse vom Wand-Start (m)
  width: number; height: number;
  reference: "mid" | "left" | "right";   // Lage des Klickpunkts in der Öffnung
  detailLevel: DetailLevel | "auto";
  frame?: { width: number; depth: number; pos: "outer"|"mid"|"inner"; offset: number };
  outerSide: "left" | "right";  // welche Wandseite ist außen
}
interface Window extends OpeningBase {
  type: "window";
  sill: number;                 // Brüstungshöhe
  sashes: 1|2|3|4;              // Flügelzahl
  sillProfileOut?: "none"|"narrow"|"standard"|"wide";   // Sims außen (DOSSIER _OEFF_SIMS_STYLES)
  sillProfileIn?:  "none"|"narrow"|"standard"|"wide";
  glass: boolean;
}
interface Door extends OpeningBase {
  type: "door";
  swing: "left" | "right";      // Anschlagseite                            [im Spike]
  hinge: "start" | "end";       // Scharnierpfosten                         [im Spike]
  openAngle: number;            // Plan-Öffnungswinkel 0180 (default 90)
  doorType: "normal" | "wall-opening";   // Wandöffnung = ohne Blatt
  frameType: "casing" | "block";         // Zarge | Blockrahmen
  lintel?: "none"|"inner"|"outer"|"both";// Sturzlinien-Anzeige (DOSSIER _OEFF_STURZ)
}

Felder 1:1 aus DOSSIERs _OEFF_*-Keys + _OEFF_STYLE_FIELDS. Presets (Fenster Standard/Gross/Bandlage, Tür Innen/Eingang/Verglast, Wandöffnung) als Style-Katalog (resources-graphics.md), seed wie _OEFF_DEFAULT_STYLES.

2.2 Host-Beziehung (Risiko #2)

Die Öffnung kennt ihre Wand (hostWallId); ihre Geometrie wird relativ zur Wandachse berechnet (opening.axisFrame(wall, position) → Punkt + Tangente + Normale, ≙ DOSSIER _oeff_axis_frame). Verschiebt sich die Wand, folgt die Öffnung automatisch (sie hält keinen absoluten Punkt). Beim Plan/3D wird die Wand an [position, position+width] ausgespart — steht im Spike (addWallPoche Segmentierung, addWallMeshes Sturz).

2.3 Generierung nach LoD

LoD Plan-Symbol 3D
coarse (1:200/500) Öffnung als Lücke + dünne Linie Aussparung, kein Rahmen
medium (1:100) + Rahmenlinien, Tür-Schwenkbogen (addDoorSymbol ), Sturz gestrichelt Aussparung + einfacher Rahmen-Quader
fine (1:50) + Glas-Doppellinie, Sims, Flügel-Teilung, Anschlag Rahmen + Blatt + Glas (transparent) + Sims (DOSSIER _OEFF_PIECE_DEFS)
  • Tür-Schwenkbogen: im Spike (generatePlan.addDoorSymbol — Blatt + Arc). Ausbau: openAngle, lichte vs. volle Breite je LoD (Port von _make_tuer_swing_curves).
  • 3D-Stücke (Rahmen/Glas/Flügel/Sims/Sturz) ≙ DOSSIER _make_oeffnung_pieces / _OEFF_PIECE_DEFS — jeweils eigene Component (Farbe + Transparenz: Glas α≈0.88, IOR 1.5). Pieces landen auf Unter-Ebenen von 21 Türen/Fenster.

3. Decke / Boden (Slab) — mit Aussparungen

3.1 Daten

interface Slab extends ElementBase {
  type: "slab";
  boundary: Vec2[];            // geschlossener Umriss (CCW)
  slabTypeId: string;          // mehrschichtig (analog WallType)
  openings?: Vec2[][];         // Aussparungen: Treppenauge, Schacht, Kamin (DOSSIER aussp)
  ukOverride?: number; okOverride?: number;  // UK/OK statt auto (Abhängung, schräge Brüstung)
}

3.2 Generierung

  • Z-Auflösung: okOverride ?? (baseElevation_oberes_Geschoss), ukOverride ?? (ok - thickness) — Port von _resolve_decke_z. Decke sitzt standardmäßig zwischen zwei Geschossen.
  • 3D: boundary als THREE.Shape, Aussparungen als shape.holes (THREE.Path), ExtrudeGeometry über die Schichten (≙ _make_decke_volume(outline, holes)).
  • Plan: im Schnitt unter cutHeight meist nur Kante; Aussparungs-Ränder als Linien; geschnittene Decke (in Schnitt-Ansicht) bekommt Schraffur.
  • Aussparung↔Decke: Aussparung als geschlossene Curve, die räumlich in der Decke liegt (_find_decke_containing_point / _find_aussparungen_for_decke). Bei uns: Slab.openings direkt im Slab — keine separate Source nötig (einfacher als DOSSIERs Parent-Child).

4. Treppe (Stair) — Typen, Lauflinie, geschossübergreifend

4.1 Daten

interface Stair extends ElementBase {
  type: "stair";
  kind: "straight" | "l-shaped" | "spiral";    // gerade | L | Wendel (DOSSIER _TREPPE_ARTEN)
  run: Vec2[];                 // Lauflinien-Stützpunkte (gerade: 2; L: 3; Wendel: Zentrum+Start)
  width: number;
  reference: "mid" | "left" | "right";          // Lage der Lauflinie zur Treppe
  steps: number;               // Anzahl Steigungen
  mode: "solid" | "flat" | "slab-edge";         // massiv | flach | Plattenrand
  runSlabThickness?: number;   // Lauf-Plattendicke
  floorEndId?: string;         // Zielgeschoss (geschossübergreifend, Risiko #6)
  heightOverride?: number; ukOverride?: number;
  rules?: { riser:[lo,hi,on]; tread:[lo,hi,on]; stepGo:[lo,hi,on] };  // SIA-Komfortregeln
  lockRiser?: { value: number };   // Schrittmass-Lock (S fix, N passt sich an)
  // Plan-Symbol-Flags (DOSSIER _KEY_TREPPE_SHOW_*)
  show?: { treads; runLine; outline; breakLine };
  upperDashed?: boolean;       // obere Stufen gestrichelt (über Schnitthöhe)
  arrowStyle?: "classic"|"filled"|"double"|"line";
}

4.2 Generierung

  • Steigung/Auftritt: riser = height/steps; tread aus Lauflinienlänge / (steps1). SIA-Komfort: 2·riser + tread ∈ [0.60, 0.65] (DOSSIER _TREPPE_SOLL_DEFAULT). Lock: ist lockRiser gesetzt, wird steps neu berechnet statt riser zu ändern.
  • 3D je kind: gerade → Stapel von Tritt-Quadern oder massive Rampe; L → zwei Läufe + Podest (podestMin); Wendel → um Zentrum rotierte Tritte (Port _make_treppe_*_preview / Volume-Funktionen). mode steuert massiv vs. Lauf-Platte.
  • Geschossübergreifend (Risiko #6): Höhe = (baseElevation[floorEndId] - baseElevation[floorId]) falls floorEndId gesetzt; sonst Geschosshöhe. Treppe taucht dann in beiden Geschoss-Grundrissen auf (mit Schnitt an cutHeight).
  • Plan-Symbol (normgerecht): Lauflinie mit Auf-/Abpfeil (arrowStyle), Stufenkanten, Bruchlinie an cutHeight (untere durchgezogen, obere gestrichelt via upperDashed), Außenkante. ≙ DOSSIERs 2D-Treppensymbol; liegt auf Ebene 40 Treppen/41 Treppen-2D.

4.3 Grip-Editing

Lauflinien-Stützpunkte als Grips (wie Wand-Vertices, §1.4); Ziehen ändert Geometrie + Stufenzahl reaktiv.


5. Dach (Roof)

5.1 Daten

interface Roof extends ElementBase {
  type: "roof";
  outline: Vec2[];             // Grundriss-Umriss
  roofType: "mono" | "gable" | "hip" | "mansard";  // Pult|Sattel|Walm|Mansarde
  thickness: number;
  slope: number;               // Grad (Hauptneigung)
  eaveIndex?: number;          // Index der Traufkante (Pult)
  ridge?: "long" | "short";    // Firstrichtung (Sattel)
  // Mansarde:
  slopeLower?: number; kinkHeight?: number;
  mansardVariant?: "hip" | "gable" | "hip-gable";
}

5.2 Generierung

Port von DOSSIERs _make_pultdach/_satteldach/_walmdach/_mansardendach* + _thicken_roof_inward. Aufwand ML (Mansarde später). Reihenfolge: Pult → Sattel → Walm → Mansarde. 3D als Brep/Mesh über OpenCascade.js (Worker), da Schräg-Verschneidung Booleans braucht. Plan: Firstlinien + Traufe + ggf. Höhenkoten.


6. Tragwerk (Column / Beam)

6.1 Daten

interface ProfileDef {
  shape: "square"|"rect"|"round"|"i-beam"|"tube";  // DOSSIER _TRAG_PROFILE
  b?: number; h?: number; d?: number; t?: number;  // Breite/Höhe/Durchm./Wanddicke
  angle: number;               // Rotation um Z
}
interface Column extends ElementBase { type:"column"; point: Vec2; profile: ProfileDef;
  uk?: number; ok?: number; }
interface Beam extends ElementBase { type:"beam"; axis:[Vec2,Vec2]; profile: ProfileDef;
  zTop?: number;               // hängt unter Decken-OK (zTop = ok der Decke)
}

6.2 Generierung

  • Querschnitt: profileCurve(shape, b,h,d,t, angle) (Port _trag_profile_curve) → für Stütze entlang Z extrudieren (_make_stuetze_volume), für Träger entlang der Achse (_make_traeger_volume, Profil in der Schnitt-Ebene).
  • Träger achs-basiert unter Decke: zTop default = OK der darüberliegenden Decke → Unterzug folgt automatisch (weniger Update-Fehler, ROADMAP §11).
  • Stützen liegen auf 25 Stützen, Träger auf 35 Träger.

7. Raum (Space) — SIA-416 + Stempel

7.1 Daten

interface Space extends ElementBase {
  type: "space";
  boundary: Vec2[];            // geschlossener Umriss
  number?: string; spaceName?: string; function?: string;
  sia?: "" | "HNF"|"NNF"|"VF"|"FF"|"GF"|"AGF";   // SIA-416-Klasse
  persons?: number;            // Personenbelegung (Brandschutz)
  areaRounding: "exact"|"0.01"|"0.1"|"0.5"|"1";
  stamp: StampConfig;          // Raumstempel-Layout (s.u.)
  fill?: string;               // Füll-Hatch-Id (optional)
}
interface StampConfig {        // ≙ DOSSIER Stempel-Builder
  layout: FieldId[][];         // Zeilen × Felder, z.B. [["number","name"],["function"],["area"]]
  font; bold; italic; textHeight; textMode:"fixed"|"scale"; align:"left"|"mid"|"right";
  offset: Vec2;                // Stempel-Position relativ zum Centroid (User-Move)
}
type FieldId = "number"|"name"|"function"|"area"|"sia";

7.2 Generierung & Bilanz

  • Fläche: Shoelace-Formel über boundary, gerundet nach areaRounding (_resolve_raum_rundung). Umfang analog.
  • Stempel: als SVG-Text-Block aus layout-Zeilen am Centroid + offset (User kann verschieben; Offset persistiert wie DOSSIER stamp_dx/dy). textMode:"scale" → Texthöhe in Paper-mm × Massstab (plans-output.md).
  • SIA-Färbung: über die Overrides-Engine (regelbasiert), nicht hartcodiert — DOSSIER _build_sia_preset_rules erzeugt 4 Regeln userString sia == hnf|nnf|vf|ff → Farbe + Solid-Hatch. Bei uns: ein Override-Preset „SIA-416" (resources-graphics.md), das auf space.sia matcht. Toggle = Preset aktivieren.
  • SIA-Bilanz + CSV: panels/SiaBalance.tsx summiert Flächen je Klasse je Geschoss → Tabelle + CSV-Export (HNF/NNF/VF/FF/GF/AGF). Pflicht für CH-Flächennachweis (ROADMAP ).

8. Werkzeuge (Tools) — ersetzt Rhino-Command-Aliases

DOSSIER hat pro Bauteil ein Command-Alias (rhino/aliases/cmd/wand.py, tuer.py, treppe.py, …) das GetPoint-Interaktionen fährt. Browser: ein Tool-Interface mit Pointer-Handlern + Snap.

interface Tool {
  id: ToolId;
  onPointerDown(pt: Vec2, snap: SnapResult, state): void;
  onPointerMove(pt: Vec2, snap: SnapResult, state): Primitive[];   // Live-Preview
  onPointerUp(pt: Vec2, snap: SnapResult, state): void;
  commit(store): void;        // ruft store.apply()
}
Tool DOSSIER-Alias Kurzbeschrieb
wall cmd/wand Achse zeichnen (Linie/Polyline), Dicke/Referenz/Typ aus „last used"
door/window cmd/tuer,fenster Punkt auf Wandachse → hosten (Snap an Wand)
slab cmd/decke Umriss klicken; Aussparung als Loch
stair cmd/treppe Lauflinie + Breite + Stufen
roof cmd/dach Umriss + Typ + Neigung
column/beam cmd/stuetze,traeger Punkt / Achse + Profil
space cmd/raum Umriss → Fläche auto, Stempel
draw2d cmd/symbol,stempel Linie/Polyline/Rect/Kreis/Bogen/Text auf 60 Plangrafik
pipette cmd/pipette Stil/Typ von Element übernehmen

Snap-Engine (tools/snap.ts): Endpunkt, Mitte, Schnitt, senkrecht, Raster, Ortho — ersetzt Rhinos OSnap. T-Snap an andere Wandachsen (Port _t_snap_to_wand_axis, _snap_endpoint_to_other_wand_axis) sorgt für saubere Knoten.


9. Element-Übersicht (BIM-Tree)

panels/ElementTree.tsx: Baum Geschoss → Bauteiltyp → Element, mit Suche und Shift-Klick = Zoom (DOSSIER ELEMENTE-ÜBERSICHT). Inhaltsverzeichnis bei 100+ Elementen — reine Ableitung aus project.elements.


10. Reihenfolge der Umsetzung (verweist auf ROADMAP-Phasen)

  1. Phase 1: Wand mehrschichtig + L-Gehrung Prio-T-Stoß (§1.3); Tür/Fenster gehostet (§2); Decke + Aussparung (§3); Wand-Referenzlage (§1.1); Element-Übersicht (§9).
  2. Phase 2: Treppe (§4), Dach (§5), Tragwerk (§6), SIA-Räume + Stempel (§7), Stil-Kataloge.
  3. Phase 34: Grip-Editing (§1.4/§4.3), exakte B-Rep-Booleans im Worker.