# Design — Prioritätsbasierte mehrschichtige Wand-Verschneidung (T/X)

> Teil der Standalone-Architektur — siehe [../../ARCHITECTURE.md](../../ARCHITECTURE.md).
> Aufbauend auf der bestehenden **L-Ecken-Gehrung** in `src/model/joins.ts`
> (`computeJoins`, `miterLine`) und `src/model/geometry.ts` (`clippedBand`,
> `lineIntersect`). Plan-Verbraucher: `src/plan/generatePlan.ts` (`addWallPoche`).
> Bezeichner **englisch**, Prosa deutsch, Einheiten **Meter**.

> **Hinweis Referenz:** Die im Auftrag genannte DOSSIER-Datei
> `rhino/wand_grips.py` ist in dieser Umgebung **nicht vorhanden** (das einzige
> auffindbare `dossier` ist ein Typst-Portfolio, kein Rhino-Plugin). Das Design
> stützt sich daher auf (a) den bestehenden Code, (b) die in CONVENTIONS.md/elements.md
> festgehaltenen Geometrie-Konventionen und (c) die etablierte Bau-Semantik der
> prioritätsbasierten Schichtverschneidung (Vectorworks „Komponenten-Verbindung",
> Revit „Layer Priority", ArchiCAD „Composite Priority"). Sobald `wand_grips.py`
> verfügbar ist, sollte Abschnitt 7 (Priorität/Backbone) gegen DOSSIERs konkrete
> Rangregel abgeglichen werden.

---

## 0. Problem & Leitbild

Heute (`computeJoins`) wird **nur die L-Ecke** behandelt: genau zwei Wandenden
treffen sich in einem Knoten, eine gemeinsame **Gehrungslinie** (`miterLine`)
schneidet beide Wände sauber. T-/X-Stöße (≥ 3 Enden) bleiben rechtwinklig
gekappt (`if (ends.length !== 2) continue;`) — die durchgehende Wand wird von der
ankommenden Wand nicht durchdrungen, und die Schichten überlappen sich oder
klaffen.

**Ziel:** An jedem Knoten — L (2 Enden), T (3), X/Kreuz (4+) — soll **jede
Schicht jeder Wand** genau bis zu der Fläche reichen, die ihre Verschneidungs-
Semantik vorgibt:

- Die **gemeinsame, höchstpriorisierte Schicht** (Backbone, z. B. Stahlbeton)
  läuft **durch** den Stoß.
- **Niederpriorisierte Schichten** (z. B. Putz, Dämmung) **stoßen** an die nächst-
  höhere Schicht des Nachbarn und **enden** dort.
- Putz **läuft auf jeder Seite** bis zur Betonfläche und endet dort; Putz
  **überquert nie** den Beton (kanonisches Beispiel, siehe §7.1).

**Architektur-Prinzip (CONVENTIONS.md):** Das semantische Modell ist die einzige
Wahrheit. Die Verschneidung ist eine **reine Ableitung** (`Project + Wall[]` →
Trimm-Linien je Schicht-Band), nichts wird in die Geometrie eingebacken. Sie wird
beim Generieren des Plans angewandt und ist **deterministisch** und **idempotent**.

---

## 1. Geometrie-Konventionen (Wiederholung, verbindlich)

Aus CONVENTIONS.md und `geometry.ts`:

- Achsrichtung `u = normalize(end - start)`.
- Wand-Normale `n = leftNormal(u) = (-u.y, u.x)`.
- Schichten werden **außen → innen** gestapelt: Offset entlang `n` läuft von
  `-T/2` (linke/„außen"-Kante) nach `+T/2` (rechte/„innen"-Kante). Eine Schicht `k`
  belegt das Intervall `[off_k, off_k + thickness_k]` mit `off_0 = -T/2`.
- Eine **unendliche Gerade** ist `Line { point, dir }`; Schnittpunkt zweier
  Geraden via `lineIntersect(a, da, b, db)` (null bei parallel).
- Ein Schicht-**Band** zwischen Offsets `offA, offB` wird über `clippedBand(start,
  end, offA, offB, startCut, endCut)` gebildet; jede Bandlängskante wird mit einer
  optionalen Schnittlinie verschnitten statt rechtwinklig gekappt.

---

## 2. Datenmodell — was schon da ist, was neu kommt

### 2.1 Vorhanden (`types.ts`)

```ts
interface Component { …; joinPriority: number; }   // höher = läuft am Stoß durch
interface Layer     { componentId: string; thickness: number; }
interface WallType  { id; name; layers: Layer[]; } // layers: außen → innen
interface Wall      { start: Vec2; end: Vec2; wallTypeId; height; … }
```

`joinPriority` ist bereits **pro Bauteil (Component)** definiert — genau richtig:
Priorität ist eine **Material-Eigenschaft**, nicht eine Schicht-Eigenschaft. Damit
verschneidet sich Beton mit Beton unabhängig vom Wandtyp.

### 2.2 Neue, abgeleitete Strukturen (keine neuen persistenten Felder)

Die heutige `WallCuts`-Struktur (eine Schnittlinie je **Wandende**) reicht für L
(Gehrung) — aber **nicht** für T/X, weil dort verschiedene Schichten **verschiedene**
Trimm-Linien brauchen (Beton läuft durch, Putz stoppt früher). Wir erweitern auf
**pro Schicht, pro Ende** eine eigene Trimm-Linie:

```ts
// src/model/joins.ts (erweitert)

/** Eine gerichtete Halbkante eines Wandendes an einem Knoten. */
interface WallEnd {
  wallId: string;
  end: "start" | "end";
}

/** Trimm-Linie + Klassifikation für GENAU EIN Schicht-Band an EINEM Ende. */
interface LayerCut {
  /** Schnittgerade in Weltkoordinaten; null = rechtwinklig kappen (Default). */
  line: Line | null;
  /**
   * "miter"    – Gehrung (L): geteilte Diagonale mit dem Nachbarn.
   * "butt"     – Band stößt stumpf an eine Nachbarfläche (niedrigere Priorität).
   * "through"  – Band läuft durch den Knoten (höchste Priorität / Backbone).
   * "square"   – freies Ende, rechtwinklig (line === null).
   */
  kind: "miter" | "butt" | "through" | "square";
}

/** Pro Wandende: eine Trimm-Linie je Schicht-Index (parallel zu WallType.layers). */
interface EndCuts {
  /** layerCuts[k] gilt für layers[k]; Länge === wt.layers.length. */
  layerCuts: LayerCut[];
}

/** Ersetzt die alte WallCuts: jetzt schichtweise an beiden Enden. */
interface WallJoin {
  start: EndCuts;
  end: EndCuts;
}

export type JoinMap = Map<string /*wallId*/, WallJoin>;
```

**Abwärtskompatibilität:** Die alte L-Gehrung ist der Spezialfall „alle
`layerCuts[k].line` an einem Ende sind dieselbe `miter`-Linie". `clippedBand`
bleibt unverändert; der Plan-Generator ruft es jetzt **je Schicht mit der
schichtspezifischen Linie** auf (siehe §8).

### 2.3 Knoten-Repräsentation

```ts
/** Ein Verschneidungsknoten: alle Wandenden, die sich (gerundet) berühren. */
interface Junction {
  key: string;        // roundKey(p)
  p: Vec2;            // Knotenposition (Mittel der Enden)
  arms: Arm[];        // sortiert nach Außenwinkel (CCW)
}

/** Ein „Arm" = ein Wandende, gesehen als Strahl, der vom Knoten WEGzeigt. */
interface Arm {
  we: WallEnd;
  wall: Wall;
  /** Richtung VOM Knoten weg in die Wand hinein (immer normiert). */
  dirOut: Vec2;       // = end==="start" ? +u : -u
  /** Außenwinkel atan2(dirOut.y, dirOut.x) für Sortierung. */
  angle: number;
  total: number;      // Gesamtdicke der Wand
  /** Schicht-Profil, vom Knoten aus gesehen (siehe §3.2). */
  layers: ArmLayer[];
}

/** Eine Schicht eines Arms, mit ihren beiden Längs-Flächengeraden am Knoten. */
interface ArmLayer {
  index: number;           // Index in wt.layers
  componentId: string;
  priority: number;        // Component.joinPriority
  /** Offsets entlang n der Wand: [innerEdge..outerEdge] des Bandes. */
  off0: number; off1: number;
  /** Die zwei Längsflächen als Geraden (point am Knoten, dir = u der Wand). */
  faceA: Line; faceB: Line;
}
```

---

## 3. Knotenerkennung (Junction Detection)

### 3.1 Knoten clustern

Identisch zur heutigen Logik, nur ohne die `length !== 2`-Abbruchbedingung:

```
function buildJunctions(walls): Junction[]
  map = Map<key, WallEnd[]>
  for w in walls:
    map.push(roundKey(w.start), {wallId:w.id, end:"start"})
    map.push(roundKey(w.end),   {wallId:w.id, end:"end"})
  out = []
  for (key, ends) in map:
    if ends.length < 2: continue          // freies Ende → alle Schichten "square"
    arms = ends.map(buildArm)
    arms.sort(by angle)                   // CCW um den Knoten
    out.push({ key, p: avgEndPoint(ends), arms })
  return out
```

`roundKey` (vorhanden) gruppiert Endpunkte auf ein 0.1-mm-Gitter. **Wichtig für
T-Stöße:** Bei einem echten T endet die ankommende Wand **auf der Achse** der
durchgehenden Wand, nicht an deren Endpunkt. Solche Knoten werden über die
Endpunkt-Gruppierung **nicht** gefunden, wenn die durchgehende Wand dort kein Ende
hat. Daher zusätzlich (siehe §3.3) eine **Achs-Auf-Achs-Inzidenz**.

### 3.2 Arm-Schichtprofil (kanonische Orientierung)

Damit Schichten zweier Arme vergleichbar sind, muss jeder Arm seine Schichten in
**konsistenter Welt-Orientierung** kennen. Wir speichern je Schicht ihre beiden
**Längsflächen** als Geraden mit Stützpunkt am Knoten und Richtung `u`:

```
function buildArm(we): Arm
  wall = byId(we.wallId);  u = dirOf(wall);  n = leftNormal(u)
  dirOut = we.end==="start" ? u : negate(u)   // vom Knoten in die Wand
  j = we.end==="start" ? wall.start : wall.end
  total = wallTypeThickness(wt)
  off = -total/2
  layers = []
  for (k, layer) in wt.layers:
    off0 = off; off1 = off + layer.thickness
    faceA = { point: j + n*off0, dir: u }
    faceB = { point: j + n*off1, dir: u }
    layers.push({ index:k, componentId, priority, off0, off1, faceA, faceB })
    off = off1
  return { we, wall, dirOut, angle: atan2(dirOut), total, layers }
```

### 3.3 T-Stoß-Erkennung (Achs-auf-Achs)

Zusätzlich zu Endpunkt-Clustern: ein Wandende `e` (Punkt `P`) bildet einen
**T-Stoß** mit Wand `B`, wenn `P` (innerhalb Toleranz) auf der **Strecke** `B.start
→ B.end` liegt, aber **nicht** auf deren Endpunkten:

```
function findTeeIncidences(walls):
  for endpoint P of each wall A (as WallEnd e):
    for each wall B != A:
      if pointOnSegment(P, B.start, B.end, tol) and not nearEndpoint(P, B):
        // virtueller Knoten: A endet, B läuft durch.
        registerTee(P, armOf(e), passThroughWall=B)
```

`B` wird hier als **durchgehender Strang** behandelt (kein Ende am Knoten); im
Junction-Modell taucht `B` als zwei kollineare „Arme" auf (Richtung `+u` und
`−u`), die der Prioritätsalgorithmus (§5) automatisch als „durchlaufend" erkennt
(zwei kollineare Arme gleicher Wand → ihre Schichten enden nie gegeneinander).

**MVP-Vereinfachung (Phase 1, §10):** T-Stöße zunächst NUR über koinzidente
Endpunkte (B hat dort tatsächlich einen Eckpunkt, z. B. weil die Wand dort geteilt
wurde). Echte Achs-auf-Achs-T-Stöße (B durchgehend) folgen in Phase 3.

---

## 4. Winkel-Sektoren & Nachbar-Flächen

Für die Prioritätsauflösung muss man wissen, **welche Fläche eines Arms welcher
Fläche des Nachbarn gegenübersteht**. Die Arme sind CCW nach `angle` sortiert.
Zwischen zwei aufeinanderfolgenden Armen `arms[i]` und `arms[i+1]` (zyklisch) liegt
ein **Sektor** (Keil). Jeder Sektor wird von **einer Längsfläche jedes der beiden
Arme** begrenzt:

```
        arm[i+1]
           \   Sektor S_i
            \ /
   faceR(i+1)\ ___ faceL(i)
              X  (Knoten)
             /
            /
        arm[i]
```

Konvention: pro Arm hat die **äußere** Schicht (Index 0, Offset `-T/2`) die Fläche,
die in den **CCW-vorausgehenden** Sektor zeigt; die **innere** Schicht den
**nachfolgenden**. Konkret bestimmen wir die „dem Sektor zugewandte" Fläche jeder
Schicht über das Vorzeichen von `cross(dirOut, sektorrichtung)` — analog zur
`lbCloser`-Heuristik in `miterLine`, aber pro Sektor statt global.

```
function sectorFaces(armLeft, armRight):
  // armRight ist CCW vor armLeft (Sektor liegt zwischen ihnen).
  // Wähle für jeden Arm die Schicht-Flächen, die in den Sektor zeigen.
  faceOf(arm, towards): pick faceA or faceB of each layer by sign of
                        cross(arm.dirOut, towards - knoten)
```

Diese Sektor-Sicht verallgemeinert die bestehende `miterLine`: bei genau zwei
Armen (L) gibt es zwei Sektoren (innen/außen), und die Mittel-Gehrungslinie ergibt
sich wie bisher aus dem Schnitt der gegenüberliegenden Außen- bzw. Innenflächen.

---

## 5. Prioritätsauflösung — das Kernstück

### 5.1 Idee

An einem Knoten konkurrieren Schichten verschiedener Arme um denselben Raum. Regel:

> Eine Schicht **läuft durch** (`through`), wenn sie zur **höchsten am Knoten
> präsenten Priorität** gehört **und** auf der „gegenüberliegenden" Seite eine
> Schicht **gleichen Materials** (oder ≥ gleicher Priorität) existiert, an die sie
> nahtlos anschließt. Andernfalls **stößt** sie (`butt`) an die nächsthöher-
> priorisierte Nachbarfläche und endet dort.

Praktisch lösen wir das **fläche-gegen-fläche** je Schicht: Für jede Schicht `L`
eines Arms suchen wir die **Trimm-Fläche**, die ihr Band beendet. Das ist die
**erste** (vom Knoten aus, entlang `dirOut`) der gegenüberliegenden Flächen mit
**echt höherer Priorität**; existiert keine, läuft die Schicht bis zur
Knoten-Mittelachse durch.

### 5.2 Pro Schicht: Trimm-Fläche finden

```
function resolveLayerCut(arm, layer, junction): LayerCut
  // Kandidaten: alle Schichten ALLER ANDEREN Arme, deren Band den
  // Halbraum dieses Layers überlappt (Offset-Überlapp im gemeinsamen
  // Sektor) UND deren Priorität die Trimm-Entscheidung bestimmt.
  candidates = []
  for other in junction.arms where other.wall.id-end != arm.id-end:
    for ol in other.layers:
      if overlapsInSector(layer, ol, arm, other):
        candidates.push({ other, ol, face: facingFace(ol, towards arm) })

  // Höchste am Knoten präsente Priorität (global, für "through").
  maxPrio = max over all candidate.ol.priority and layer.priority

  if layer.priority == maxPrio and hasCollinearSamePrio(arm, layer, junction):
     // Backbone: läuft durch bis zur Mittel-/Gehrungslinie.
     return { line: miterMidline(arm, layer, junction), kind: "through" }

  // Sonst: stoße an die NÄCHSTE Fläche höherer Priorität entlang dirOut.
  blockers = candidates.filter(c => c.ol.priority > layer.priority)
  if blockers.empty:
     // niemand höher → Gehrung mit dem Nachbarn gleicher Stufe (L-Fall)
     return { line: miterMidline(arm, layer, junction), kind: "miter" }

  nearest = argmin over blockers of distanceAlong(dirOut, face)
  return { line: nearest.face, kind: "butt" }
```

Hilfsbegriffe:

- `overlapsInSector(layer, ol, …)` — projiziert beide Bänder auf die **Normale des
  Sektors** und prüft Intervall-Überlapp `[off0,off1] ∩ [off0',off1'] ≠ ∅`. Nur
  überlappende Bänder können sich gegenseitig trimmen.
- `facingFace(ol, towards arm)` — die der `arm`-Seite zugewandte Längsfläche von
  `ol` (die `faceA`/`faceB` von §3.2), als `Line`.
- `distanceAlong(dirOut, face)` — Abstand des Schnittpunkts `faceLine ∩ layerAxis`
  vom Knoten, gemessen entlang `dirOut`. Negative bzw. hinter dem Knoten liegende
  Treffer werden verworfen (eine Trimm-Fläche muss **vor** dem Band liegen).
- `miterMidline(...)` — die gemeinsame Diagonale für gleichrangige Begegnung; für
  zwei Arme exakt die heutige `miterLine`.
- `hasCollinearSamePrio(...)` — true, wenn auf der „anderen Seite" des Knotens eine
  Schicht **gleichen Materials/Priorität** existiert, in die das Band nahtlos
  übergeht (Backbone-Kontinuität, §7).

### 5.3 Resultat in `LayerCut.line` schreiben

`resolveLayerCut` liefert für `layers[k]` eine `Line | null`. Diese wird in
`WallJoin[end].layerCuts[k]` abgelegt. `clippedBand` kappt damit **dieses eine
Band** an genau dieser Linie — alle anderen Schichten derselben Wand können andere
Linien (oder `null`) haben. Das ist die ganze Verkabelung zum Renderer.

---

## 6. Master-Pseudocode `computeJoins` (neu)

```
function computeJoins(project, walls): JoinMap
  result = init each wall → { start:{layerCuts:[square…]}, end:{layerCuts:[square…]} }
  junctions = buildJunctions(walls)            // §3.1
  // (Phase 3: junctions += findTeeIncidences(walls))  // §3.3

  for J in junctions:
    if J.arms.length == 1: continue            // freies Ende: alles "square"
    if J.arms.length == 2:
       resolveL(J, project, result)            // bestehende Gehrung, schichtweise
    else:
       resolveTorX(J, project, result)         // §5, pro Arm pro Schicht
  return result

function resolveTorX(J, project, result):
  for arm in J.arms:
    cuts = []
    for layer in arm.layers:
      cuts[layer.index] = resolveLayerCut(arm, layer, J)   // §5.2
    writeEnd(result, arm.we, cuts)             // start oder end

function resolveL(J, project, result):
  [a, b] = J.arms
  // Wie heute: eine gemeinsame Gehrungslinie pro Sektor; aber wenn die
  // Dicken/Schichten ungleich sind, kann pro Schicht über §5.2 verfeinert werden.
  // MVP: eine miterLine für alle Schichten beider Arme (= heutiges Verhalten,
  // schichtweise dupliziert).
  line = miterLine(project, a.wall, a.we.end, b.wall)
  for arm in [a,b]:
    cuts = arm.layers.map(_ => ({ line, kind:"miter" }))
    writeEnd(result, arm.we, cuts)
```

So bleibt die **L-Ecke bit-genau wie heute** (Regressionssicherheit), und die
neue Logik greift nur bei ≥ 3 Armen.

---

## 7. Prioritäts-Semantik im Detail (das Putz/Beton-Beispiel)

### 7.1 Kanonisches Beispiel

Wandtyp „Aussenwand" (außen → innen):

| k | Component  | thickness | joinPriority |
|---|------------|-----------|--------------|
| 0 | Putz aussen| 0.02      | 1            |
| 1 | Stahlbeton | 0.18      | 9            |
| 2 | Putz innen | 0.015     | 1            |

Drei solche Wände treffen in einem T-Knoten (zwei kollinear durchgehend „BB",
eine ankommend „A"):

1. **Beton (prio 9)** der durchgehenden Wände `BB` läuft durch — beide
   Beton-Bänder sind kollinear gleicher Priorität → `hasCollinearSamePrio` =
   true → `through`. Der Beton von `A` (prio 9) **stößt** an die **Betonfläche**
   von `BB` (gegenüberliegende Fläche, gleiche höchste Priorität, aber kein
   kollinearer Partner für `A`) → `butt` an Betonaußenfläche von `BB`.
2. **Putz (prio 1)** jeder Wand stößt an die **erste höherpriorisierte Fläche**
   entlang `dirOut`. Für die Putzschichten von `A` ist das die **Betonfläche von
   `BB`**: Putz **läuft auf jeder Seite bis zum Beton** und endet dort (`butt`).
3. Putz **überquert nie** den Beton, weil eine `butt`-Trimm-Linie immer die
   **nächste** höhere Fläche ist — sie liegt vor dem Beton-Durchlauf.

Ergebnis exakt wie gefordert: *Beton durch, Putz schließt beidseitig an, Putz
kreuzt Beton nicht.*

### 7.2 Warum Priorität pro Component (nicht pro Layer)

Beton trifft Beton → gleiche Priorität → nahtlos. Würde Priorität pro Schicht
vergeben, müsste man sie pro Wandtyp neu pflegen. `joinPriority` am Component löst
das materialweise — Stahlbeton hat **immer** 9, egal in welchem Wandtyp.

### 7.3 Backbone-Erkennung für „through"

`hasCollinearSamePrio(arm, layer, junction)`:

```
for other in junction.arms where other != arm:
  if collinear(arm.dirOut, other.dirOut) (antiparallel, gleiche Achse) and
     other has a layer ol with ol.priority == layer.priority and
     bandsAlign(layer, ol):    // gleiche Offsets relativ zur gemeinsamen Achse
       return true
return false
```

Nur wenn das Band auf der **gegenüberliegenden** Achse einen passenden Partner
gleicher Priorität und Lage hat, läuft es wirklich **durch**. Sonst (z. B. der
ankommende Beton von `A`) **stößt** es an — exakt das gewünschte Verhalten.

---

## 8. Layer-Wrapping (Umschlagen niederpriorisierter Schichten)

Ein subtiler, aber wichtiger Fall: An einem T-Stoß endet die **innere** Putzschicht
der ankommenden Wand `A` an der Betonfläche von `BB`. Damit der Putz **um die Ecke
herum sichtbar bleibt** (auf der Innenseite der durchgehenden Wand zieht der innere
Putz von `BB` durch), ist nichts Zusätzliches nötig — `BB`s innerer Putz läuft als
eigenes durchgehendes Band weiter.

Wo Wrapping aktiv nötig ist: wenn eine **niederpriorisierte Außenschicht** an einer
Ecke **um eine höhere Schicht herumgeführt** werden soll (z. B. Dämmung, die außen
um die Stütze läuft). Modellierung:

- Standard (MVP): kein Wrapping — jede Schicht endet an ihrer Trimm-Fläche
  (`butt`). Das deckt T/X korrekt ab.
- Optional (Phase 4): Ein `wrap`-Flag pro Component (`Component.wrapAtEnds?:
  boolean`). Ist es gesetzt, erzeugt der Generator an der Trimm-Fläche ein
  **zusätzliches kurzes Stirnband** quer (Offset-Intervall der Schicht, Länge =
  Tiefe bis zur nächsten Fläche), sodass die Schicht ihre eigene Stirnseite
  „umschließt". Geometrisch ein weiteres `clippedBand` mit getauschten Achsen.

Wrapping ist bewusst **nachgelagert**; es ändert die Trimm-Logik (§5) nicht,
sondern fügt nur Zusatzpolygone hinzu.

---

## 9. Anbindung an den Plan-Generator (`generatePlan.addWallPoche`)

Heute ruft `addWallPoche` `clippedBand(p1, p2, off, off+thickness, startCut,
endCut)` mit **einer** `WallCuts` je Ende. Neu:

```ts
// joins: JoinMap (neu)
const join = joins.get(wall.id) ?? emptyJoin(wt.layers.length);
…
let off = -total / 2;
wt.layers.forEach((layer, k) => {
  const startCut = (s <= 1e-6) ? join.start.layerCuts[k].line : null;
  const endCut   = (e >= axisLen - 1e-6) ? join.end.layerCuts[k].line : null;
  out.push({ kind:"polygon",
    pts: clippedBand(p1, p2, off, off + layer.thickness, startCut, endCut),
    fill: comp.color, hatch: resolveHatch(...), … });
  off += layer.thickness;
});
```

- Die **Umrisslinie** über die volle Dicke (`-T/2..+T/2`) nutzt für ihre beiden
  Kanten die Cuts der **äußersten** bzw. **innersten** Schicht — oder wird, falls
  Schichten unterschiedlich getrimmt sind, durch eine **abgeleitete Outline**
  (Vereinigung der Schicht-Polygone, §11) ersetzt. MVP: weiterhin volle-Dicke-Band
  mit den Cuts von Schicht 0 / letzter Schicht.
- `grob`-Detailgrad nutzt nur die **Backbone-Schicht** → ihr `through`/`miter`-Cut
  für die ganze Sammelfläche (entspricht der bestehenden `backboneColor`-Logik).

**Keine Signaturänderung an `clippedBand`** — es bleibt der zentrale Trimmer; wir
füttern es nur schichtweise mit verschiedenen Linien.

---

## 10. 2D-analytisch jetzt vs. exakte 3D-Booleans später (OCCT)

### 10.1 Jetzt — 2D-analytisch (dieses Design)

- **Plan/Grundriss** ist eine reine 2D-Ableitung (vgl. `generatePlan.ts`-Kopf:
  „nicht durch Zerschneiden eines 3D-Meshes, sondern direkt aus den Parametern").
- Trimmen = **Geraden-Schnitt** (`lineIntersect`) + Polygon-Kappen (`clippedBand`).
  Kein Polygon-Boolean nötig, solange Schichten als **konvexe Bänder** mit
  schrägen Stirnflächen modelliert werden. Das ist exakt, schnell (O(Arme² ·
  Schichten) je Knoten), und deterministisch.
- **3D-Wand** im Viewport: Extrusion der getrimmten 2D-Schichtpolygone in `z`
  (Höhe). Damit stimmen Plan und 3D ohne separaten Kernel überein.

Grenzen der 2D-Analytik: nicht-konvexe Stirnprofile, echte Materialdurchdringung in
`z` (z. B. wenn Wände unterschiedlicher Höhe / Sturz / Brüstung interagieren),
Verschneidung Wand × Decke × Stütze. Das braucht echte Volumen-Booleans.

### 10.2 Später — exakte 3D-Booleans (OCCT / opencascade.js)

- **Bibliothek:** `opencascade.js` (WASM-Port von OCCT) — `BRepAlgoAPI_Cut/Common`,
  `BRepPrimAPI_MakePrism` für Extrusionen. Alternativ `manifold-3d` (schneller,
  robuster für reine Mesh-Booleans, aber ohne B-Rep/Fillets).
- **Modell bleibt gleich:** Priorität (`joinPriority`) bestimmt die **Cut-
  Reihenfolge**. Algorithmus: baue je Schicht einen über-langen Solid (Band ×
  Höhe, über den Knoten hinaus verlängert); subtrahiere von jeder niederpriori-
  sierten Schicht die Solids **aller höherpriorisierten** Schichten am Knoten
  (`result = layerSolid − ⋃ higherPrioritySolids`). Gleiche Priorität: kein Cut
  (Beton bleibt an Beton). Das ist die **3D-Verallgemeinerung exakt derselben
  Prioritätsregel** — die 2D-`butt`/`through`-Klassifikation aus §5 ist die
  2D-Projektion dieser Boolean-Reihenfolge.
- Die Datenstrukturen (§2) bleiben unverändert; nur der **Trimmer** wird
  ausgetauscht (`clippedBand` → OCCT-Cut). Deshalb ist das 2D-Design bereits
  „OCCT-ready".

---

## 11. Robuste Outline (optional, Phase 4)

Wenn Schichten an einem Knoten unterschiedlich getrimmt sind, ist die „volle
Dicke"-Umrisslinie nicht mehr ein einfaches Band. Saubere Lösung: die
**Wand-Outline** als **Vereinigung aller Schicht-Polygone** berechnen
(Polygon-Boolean in 2D). Empfohlene Library: **`polygon-clipping`** (Martinez-
Rueda, robust, klein) oder **`@flatten-js/boolean-op`**. Damit wird der Umriss
exakt die Außenkontur der getrimmten Bänder — auch bei X-Knoten. MVP verzichtet
darauf und nutzt die Schicht-0/N-Cuts (visuell für die meisten Fälle ausreichend).

---

## 12. Edge Cases

1. **Gleiche Priorität, nicht kollinear** (z. B. zwei Betonwände treffen im rechten
   Winkel im T): kein „through" (kein kollinearer Partner), aber auch kein
   `butt`-Blocker höherer Priorität → Fallback **`miter`** (gemeinsame Gehrung wie
   im L-Fall). Bei drei gleichrangigen Armen: paarweise Gehrung pro Sektor; die
   resultierenden Stirnflächen sind die Sektor-Halbierenden.
2. **Kollinear (180°)** — zwei Wände in einer Linie: `lineIntersect` liefert null
   (parallel). Behandlung wie heute: **kein Schnitt**, Bänder laufen gerade durch
   (bei gleichem Typ nahtlos). Bei ungleichem Typ: die schmalere Wand stößt an die
   breitere; pro Schicht über §5 auflösbar.
3. **> 2 Schichten / asymmetrische Wandtypen**: Der Algorithmus ist in der Schicht-
   anzahl generisch (`resolveLayerCut` läuft je Schicht-Index). Treffen Wände
   verschiedener Typen (verschiedene Schichtzahl/-dicke), entscheidet **nur die
   Priorität pro Fläche**, nicht der Index — deshalb arbeiten wir mit
   `ArmLayer.faceA/faceB` (Welt-Flächen), nicht mit Schicht-Indizes über Wände
   hinweg.
4. **Backbone fehlt** (alle Prioritäten gleich): degeneriert sauber zu reinen
   Gehrungen (Fall 1).
5. **Sehr spitze Winkel**: Trimm-Schnittpunkte können weit vom Knoten wegwandern.
   Begrenzung: `distanceAlong` auf `≤ maxReach` (z. B. `3 · total`) clampen; sonst
   rechtwinklig kappen (`square`), um Artefakte zu vermeiden.
6. **Öffnungen am Wandende** (`addWallPoche`-Segmentierung): Cuts gelten nur für das
   echte Wandende (`s≤ε` / `e≥len−ε`), wie heute. Türnahe Segmentenden bleiben
   `square`.
7. **Numerische Knoten-Toleranz**: `roundKey`-Gitter (0.1 mm) und ε in
   `pointOnSegment` müssen konsistent sein, sonst „flackernde" Knoten. Toleranz
   zentral als Konstante (`JOIN_EPS = 1e-4` m).
8. **Mehr als 2 Wände gleicher Achse** (degenerierter X, alle kollinear): als ein
   durchgehender Strang behandeln; Prioritätsregel pro Schicht greift normal.

---

## 13. Staged Build-Plan

**Phase 1 — Single-Layer T (Fundament).**
- `JoinMap`/`WallJoin`/`EndCuts`/`LayerCut` einführen; `WallCuts` darin als
  Spezialfall abbilden. `clippedBand` unverändert.
- `buildJunctions` ohne `length!==2`-Abbruch; L-Fall ruft bestehende `miterLine`
  (schichtweise dupliziert) → **Regressionsgleichheit** zu heute.
- T-Knoten **nur über koinzidente Endpunkte** (§3.3 MVP). Für **einschichtige**
  Wände: die durchgehende Wand läuft durch, die ankommende stößt rechtwinklig an
  deren nächste Fläche. Verifizieren: `npx tsc -b`, `npm run build`,
  `node scripts/probe.mjs`, Screenshot prüfen.

**Phase 2 — Prioritäts-Auflösung mehrschichtig (T).**
- `ArmLayer`-Profil (§3.2), Sektor-Flächen (§4), `resolveLayerCut` (§5) inkl.
  `through`/`butt`/`miter`-Klassifikation und `hasCollinearSamePrio`.
- `addWallPoche` schichtweise verkabeln (§9). Putz/Beton-Testszene (§7.1) anlegen
  und visuell prüfen.

**Phase 3 — X-Knoten & echte Achs-T-Stöße.**
- `findTeeIncidences` (Achs-auf-Achs, §3.3): durchgehende Wand wird nicht geteilt.
- 4+-Arm-Sektorlogik vollständig; Edge Cases 1/5/8 absichern.

**Phase 4 — Politur.**
- Robuste Outline via Polygon-Boolean (§11); optionales Layer-Wrapping (§8,
  `Component.wrapAtEnds`).

**Phase 5 — Exakte 3D-Booleans (OCCT).**
- `opencascade.js` integrieren; Trimmer-Interface so abstrahieren, dass 2D
  (`clippedBand`) und 3D (OCCT-Cut) dieselbe Prioritäts-Reihenfolge nutzen (§10.2).
  Plan bleibt 2D-analytisch; nur das 3D-Volumen nutzt Booleans.

---

## 14. Trimmer-Abstraktion (für §10.2-Migration)

Damit Phase 5 nicht den Aufrufer ändert, kapseln wir das Trimmen hinter einer
Schnittstelle. 2D nutzt `clippedBand`; 3D nutzt OCCT — beide konsumieren dieselbe
`JoinMap`.

```ts
interface LayerTrimmer {
  /** 2D: getrimmtes Bandpolygon. 3D-Variante liefert stattdessen einen Solid. */
  trimBand(p1: Vec2, p2: Vec2, offA: number, offB: number,
           startCut: Line | null, endCut: Line | null): Vec2[];
}
```

Die Prioritätslogik (`computeJoins`) bleibt der **gemeinsame, kernel-unabhängige**
Kopf; nur der `LayerTrimmer` wird ausgetauscht. Das hält das Design dem
Architektur-Prinzip treu: ein semantisches Modell, viele abgeleitete Ansichten.
