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.
19 KiB
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 pergenerate*()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 viaExtrudeGeometry.
1.3 Wand-Verschneidung (Joins) — Risiko #1
Status: L-Ecken-Gehrung ✅ (joins.computeJoins → miterLine, 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:
- Knoten finden: Endpunkte auf Gitter runden (
roundKey, existiert), gruppieren.==1freies Ende,==2L-Ecke (Gehrung, ✅),>2T/X. - 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 - Schicht-Durchdringung (Backbone): nur das Material mit der höchsten
gemeinsamen
joinPriorityin 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 3–4)
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:
pointerdownauf Marker → Live-Preview-Linien zu Nachbar-Vertices → Snap (Endpunkt/Ortho/Raster) →pointerup→store.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 0–180 (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 von21 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:
boundaryalsTHREE.Shape, Aussparungen alsshape.holes(THREE.Path),ExtrudeGeometryüber die Schichten (≙_make_decke_volume(outline, holes)). - Plan: im Schnitt unter
cutHeightmeist 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.openingsdirekt 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;treadaus Lauflinienlänge / (steps−1). SIA-Komfort:2·riser + tread ∈ [0.60, 0.65](DOSSIER_TREPPE_SOLL_DEFAULT). Lock: istlockRisergesetzt, wirdstepsneu berechnet stattriserzu ä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).modesteuert massiv vs. Lauf-Platte. - Geschossübergreifend (Risiko #6): Höhe =
(baseElevation[floorEndId] - baseElevation[floorId])fallsfloorEndIdgesetzt; sonst Geschosshöhe. Treppe taucht dann in beiden Geschoss-Grundrissen auf (mit Schnitt ancutHeight). - Plan-Symbol (normgerecht): Lauflinie mit Auf-/Abpfeil (
arrowStyle), Stufenkanten, Bruchlinie ancutHeight(untere durchgezogen, obere gestrichelt viaupperDashed), Außenkante. ≙ DOSSIERs 2D-Treppensymbol; liegt auf Ebene40 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 M–L (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:
zTopdefault = OK der darüberliegenden Decke → Unterzug folgt automatisch (weniger Update-Fehler, ROADMAP §11). - Stützen liegen auf
25 Stützen, Träger auf35 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 nachareaRounding(_resolve_raum_rundung). Umfang analog. - Stempel: als SVG-Text-Block aus
layout-Zeilen am Centroid +offset(User kann verschieben; Offset persistiert wie DOSSIERstamp_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_ruleserzeugt 4 RegelnuserString sia == hnf|nnf|vf|ff→ Farbe + Solid-Hatch. Bei uns: ein Override-Preset „SIA-416" (resources-graphics.md), das aufspace.siamatcht. Toggle = Preset aktivieren. - SIA-Bilanz + CSV:
panels/SiaBalance.tsxsummiert 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)
- 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).
- Phase 2: Treppe (§4), Dach (§5), Tragwerk (§6), SIA-Räume + Stempel (§7), Stil-Kataloge.
- Phase 3–4: Grip-Editing (§1.4/§4.3), exakte B-Rep-Booleans im Worker.