# Design — Bauteile (Elements)

> Teil der Standalone-Architektur — siehe [../../ARCHITECTURE.md](../../ARCHITECTURE.md).
> Output/Pläne: [plans-output.md](plans-output.md). Ressourcen/Stile:
> [resources-graphics.md](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

```ts
// 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`):
```ts
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):
```ts
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
```ts
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.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:

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.

```ts
// 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:** `pointerdown` auf 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
```ts
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 von `21 Türen/Fenster`.

---

## 3. Decke / Boden (Slab) — mit Aussparungen

### 3.1 Daten
```ts
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
```ts
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 /
  (steps−1). 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
```ts
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
```ts
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
```ts
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.

```ts
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 3–4:** Grip-Editing (§1.4/§4.3), exakte B-Rep-Booleans im Worker.
