# Parametrische Wände Status: Implementiert (Phase A — Typ-System und Resolver in `src/model/`, kein UI). Dieses Dokument spezifiziert die **Parametrischen Wände**: regelbasierte Definitionen, die beim Auflösen eine Liste von `Wall`-Elementen erzeugen, anstatt sie einzeln vom Nutzer zeichnen zu lassen. Bezugsdokumente: [elements.md](elements.md) (Wand-/Türmodell), [drawing-tools.md](drawing-tools.md) (Werkzeugsystem, Direktzeichnen), [state-architecture.md](state-architecture.md) (Projekt-Store), [resources-graphics.md](resources-graphics.md) (WallType/Component-Auflösung). Implementierungsdateien: - `src/model/types.ts` — `ParametricWall`, `ParametricRule` und alle Regel-Varianten. - `src/model/parametricWalls.ts` — `resolveParametricWall()`, `applyRule()` und Hilfsfunktionen. --- ## 0. Überblick Eine **parametrische Wand** (`ParametricWall`) ist kein festes `Wall`-Element, sondern ein **Regelwerk**, das beim Auflösen (`resolveParametricWall`) eine Menge von `Wall[]`- Elementen generiert. Die erzeugten Wände sind gewöhnliche `Wall`-Objekte; sie unterscheiden sich lediglich in ihrer Herkunft. Das semantische Modell (`Project`) bleibt die einzige Wahrheit — parametrische Wände sind eine Ressource in der Ressourcen-Bibliothek, nicht eine separate Laufzeit-Geometrie-Schicht. ``` Project.parametricWalls: ParametricWall[] │ │ resolveParametricWall(pw, floorId, context, defaultWallType) ▼ Wall[] ──→ normales Rendering über generatePlan / Viewport3D ``` Erzeugte Wände können entweder **temporär** (zur Laufzeit, als Ergänzung zu `project.walls` im Rendering-Pfad) oder **eingebacken** (als `Wall[]` fest in `Project.walls` gespeichert) behandelt werden. Phase A legt nur den Auflöser fest; die Auswahl liegt bei der aufrufenden Komponente. --- ## 1. Motivation ### 1.1 Schnellere Modellierung von Regelgrundrissen Schweizer Wohnbauten folgen häufig einem 3-m-Achsraster (SIA-Norm, Modul-/ Skelettbauweise). Zwanzig Wände eines Rasters von Hand zu zeichnen ist fehleranfällig und verhindert spätere parametrische Änderungen (z. B. Geschossanzahl, Rasterweite, Wandtyp). Eine `GridRule` erzeugt dieses Muster aus wenigen Parametern (Achsabstand, Richtung, Bereich) und lässt sich mit einer einzigen Zahl auf „4-m-Büroraster" umstellen. ### 1.2 Kongruenz mit FreeCAD BIM / IFC FreeCAD BIM kennt **ParametricObjects**, die ihre Geometrie aus Regeln ableiten (z. B. `ArchWall` mit `Length`, `Width`, `Height`). Obwohl das Datenformat hier kein IFC ist, schafft ein ähnliches Abstraktionsniveau eine spätere Brücke: Beim IFC-Export können parametrische Wände als `IfcWallStandardCase` mit konstanten Attributen exportiert werden — kein Informationsverlust gegenüber manuell gezeichneten Wänden. ### 1.3 Bedingte Wandtypen ohne manuelle Klassifizierung Außenwände sind dicker als Innenwände; Trennwände zwischen Einheiten erfordern Schallschutz. Eine `ConditionalThicknessRule` (`condition: "exterior" → thickType`) weist den richtigen Wandtyp automatisch aus der geometrischen Lage zu — ohne dass der Nutzer jeden Wandabschnitt einzeln klassifizieren muss. --- ## 2. Architektur ### 2.1 Typen (`src/model/types.ts`) ```ts /** * Eine parametrische Wand-Regel — generiert automatisch Wall[]-Einträge für * ein gegebenes Geschoss. Lebt in Project.parametricWalls[]. */ export interface ParametricWall { id: string; name: string; description?: string; /** * Geordnete Liste der anzuwendenden Regeln. Spätere Regeln können die * Ausgabe früherer verfeinern (z. B. Dickenzuweisung nach Raster). */ rules: ParametricRule[]; /** * Rückfall-Wandtyp, falls eine Regel keinen eigenen `wallTypeId` nennt. */ defaultWallTypeId: string; } /** Diskriminierte Union aller Regel-Varianten. */ export type ParametricRule = | GridRule | ModuleRule | ConditionalThicknessRule | ReferenceLineRule | SequenceRule; ``` ### 2.2 Einbettung ins Projekt ```ts export interface Project { // … bestehende Felder … /** * Parametrische Wanddefinitionen (Ressourcen-Bibliothek). Optional, damit * bestehende Projekte/Tests ohne `parametricWalls` gültig bleiben. */ parametricWalls?: ParametricWall[]; } ``` ### 2.3 Resolver-Kontext (`src/model/parametricWalls.ts`) ```ts export interface ParametricContext { /** Das Ziel-Geschoss. */ floor: DrawingLevel; /** * Optionale Rasterachsen (Phase C: verlinkter Grid-Ressource). Fehlen sie, * berechnet die Engine die Achsen aus GridRule.spacing. */ gridAxes?: { x: number[]; y: number[] }; /** * Optionales Clipping-Polygon (Meter). Fehlt es, reicht das Raster über * einen Standardbereich (0 … spacing × 10). */ boundaryGeometry?: { boundary: Vec2[] }; /** * Bereits im Projekt vorhandene Wände des Geschosses. Werden von * refinierenden Regeln (ConditionalThicknessRule, ReferenceLineRule) genutzt. */ existingWalls?: Wall[]; } ``` --- ## 3. Regel-Varianten ### 3.1 GridRule — Achsraster Erzeugt parallele Wände auf einem gleichmäßigen Raster. Typischer Einsatz: Schweizer Wohnbau-Achsraster (3 m), Büro-Konstruktionsraster (6 m), strukturelle Raster mit fester Stützweite. ```ts export interface GridRule { type: "grid"; /** * Optionaler Verweis auf eine Grid-Ressource (Phase C). Für Phase A wird * stattdessen `spacing` genutzt. */ gridId?: string; /** Rasterabstand in Metern (Default: 3.0). */ spacing?: number; /** * Achsrichtungen: „x" = nur Wände entlang der Y-Achse, * „y" = nur Wände entlang der X-Achse, „both" = Vollraster. */ directions: "x" | "y" | "both"; /** Optionaler Verweis auf Clipping-Polygon. */ boundaryId?: string; /** Optionale Wandtyp-Übersteuerung; sonst defaultWallTypeId. */ wallTypeId?: string; /** Lage der Wandachse über die Dicke (Vectorworks-Stil). */ referenceLine?: WallReferenceLine; /** Optionale Höhenübersteuerung in Metern; sonst Geschosshöhe. */ height?: number; } ``` **Geometrieausgabe (top-down Grundriss):** ``` directions: "x", spacing: 3.0, Bereich 0…12 m: y │ 12 ────────────────────────── │ 9 ────────────────────────── │ 6 ────────────────────────── │ 3 ────────────────────────── │ 0 ────────────────────────── │ └──────────────────────────► x 0 12 ``` **Wann verwenden:** - Tragende Wände auf fester Stützweite (Wohnbau 3 m, Büro 6 m). - Vollraster (`"both"`) für strukturelle Rastersysteme. - In Kombination mit `ConditionalThicknessRule` zur automatischen Außen/Innen-Klassifizierung. **Beispiel: Schweizer 3-m-Wohnraster** ```ts const pw: ParametricWall = { id: "pw-eg-raster", name: "EG Längswände 3m-Raster", defaultWallTypeId: "wt-innen-15", rules: [ { type: "grid", spacing: 3.0, directions: "x", // Wände in X-Richtung (y = 0, 3, 6, 9, 12) wallTypeId: "wt-innen-15", }, ], }; // Auflösung: const walls = resolveParametricWall(pw, "floor-eg", { floor: egFloor, boundaryGeometry: { boundary: rectBoundary(0, 0, 12, 12) }, }, defaultWallType); // → 5 Wände bei y = 0, 3, 6, 9, 12, je 12 m lang ``` --- ### 3.2 ModuleRule — Bay-/Jochbauweise Unterteilt eine Referenzspanne in gleiche Module und erzeugt Querwände an jedem Teilungspunkt. Typisch für Bürogebäude (6-m-Joch) oder Reihenhäuser mit modularer Erschließung. ```ts export interface ModuleRule { type: "module"; /** Modulmaß in Metern (z. B. 6.0, 3.6). */ moduleSize: number; /** Ausrichtung der Trennwände: „x" = Querwände senkrecht zu X, „y" = zu Y. */ direction: "x" | "y"; /** * Optionaler Verweis auf eine Referenzwand, die die Spannweite definiert. * Fehlt er, wird die Geschoss-Ausdehnung (Bounding-Box) genutzt. */ referenceWallId?: string; /** Optionale Wandtyp-Übersteuerung; sonst defaultWallTypeId. */ wallTypeId?: string; referenceLine?: WallReferenceLine; height?: number; } ``` **Wann verwenden:** - Wenn sich Querwände aus einer Referenzspanne (Fassade, Achswand) ergeben. - Vorzug vor `GridRule`, wenn nur in eine Richtung unterteilt wird und eine Referenzwand die Spanne definiert. **Beispiel: 6-m-Bay-Bürogebäude** ```ts const pw: ParametricWall = { id: "pw-buero-joch", name: "Büro 6m-Joch", defaultWallTypeId: "wt-beton-20", rules: [ { type: "module", moduleSize: 6.0, direction: "x", // Querwände senkrecht zur X-Achse // referenceWallId: "W-sudfassade" → Spanne aus der Südwand ableiten }, ], }; // resolveParametricWall → Querwände bei x = 6, 12, 18, 24, 30 (bei 36-m-Fassade) ``` --- ### 3.3 ConditionalThicknessRule — Bedingte Wandtypen Weist bereits erzeugten Wänden (aus vorherigen Regeln in der Sequenz) einen anderen Wandtyp zu — abhängig von einer Bedingung. Gibt modifizierte **Kopien** zurück; die Eingabe-Wände werden nicht mutiert. ```ts export interface ConditionalThicknessRule { type: "conditional-thickness"; /** * Bedingung für den Treffer: * • „exterior" — Wand liegt am Außenrand (Bounding-Box des Kontexts). * • „interior" — Wand liegt im Inneren. * • „bearing" — tragende Wand (Heuristikum: Wand läuft ±10° zu X/Y-Achse). * • beliebiger String — benutzerdefiniertes Tag (Phase C: Wall.tags[]). */ condition: "exterior" | "interior" | "bearing" | string; /** Ziel-Wandtyp, der bei Treffer gesetzt wird. */ wallTypeId: string; } ``` **Wann verwenden:** - Immer in Kombination mit `GridRule` oder `ModuleRule` (als zweite Regel in `ParametricWall.rules`): Raster erzeugt, Dicke verfeinert. - Wenn Außen- und Innenwände denselben geometrischen Ursprung haben, aber verschiedene Aufbauten benötigen. **Beispiel: Außen dick, Innen dünn** ```ts const pw: ParametricWall = { id: "pw-eg-komplett", name: "EG Vollraster mit Außenwand-Differenzierung", defaultWallTypeId: "wt-innen-15", rules: [ { type: "grid", spacing: 3.0, directions: "both", wallTypeId: "wt-innen-15", }, { type: "conditional-thickness", condition: "exterior", wallTypeId: "wt-aussen-36", // Außenwände erhalten dicken Aufbau }, ], }; ``` --- ### 3.4 ReferenceLineRule — Wandachsen-Lage Setzt `referenceLine` bei passenden Wänden einheitlich (Vectorworks-Stil: Achse links/rechts/mittig). Gibt modifizierte Kopien zurück. ```ts export interface ReferenceLineRule { type: "reference-line"; /** Neue Lage der Wandachse, die einheitlich gesetzt wird. */ referenceLine: WallReferenceLine; // "left" | "center" | "right" /** * Filterziel: * • „all" — alle Wände im aktuellen Satz. * • „exterior" — nur Außenwände. * • beliebiger String — benutzerdefiniertes Tag (Phase C). */ target: "all" | "exterior" | string; } ``` **Wann verwenden:** - Außenwände auf `"left"` setzen (Achse liegt auf der Fassadenfläche). - Als abschließende Regel in einer `SequenceRule` nach Raster und Dickenzuweisung. --- ### 3.5 SequenceRule — Zusammenfassung von Unterregeln Fasst mehrere Regeln als atomare Einheit zusammen. Jede Unterregel erhält die Ausgabe der vorherigen als `existingWalls` — so können spätere Regeln frühere verfeinern. ```ts export interface SequenceRule { type: "sequence"; rules: ParametricRule[]; /** * Wenn true: Abbruch nach der ersten Unterregel, die mindestens eine Wand * generiert/verändert hat (Short-Circuit-Fallback). */ stopOnMatch?: boolean; } ``` **Wann verwenden:** - Um eine zusammengehörige Kombination (Raster → Dicke → Referenzlinie) als Untermodul wiederzuverwenden — z. B. in unterschiedlichen Geschossen mit leicht abweichenden Parametern. --- ## 4. Resolver-API (`src/model/parametricWalls.ts`) ```ts /** * Löst ein ParametricWall-Regelwerk zu einem Wall[]-Array für ein gegebenes * Geschoss auf. * * Ablauf: * 1. Regelwerk sequenziell ausführen; jede Regel erhält die Ausgabe der * vorherigen als existingWalls (ermöglicht Verfeinerung). * 2. Duplikate (gleicher Start-/Endpunkt innerhalb tolerance) entfernen. * 3. Bereinigte Wall[]-Liste zurückgeben. * * Die Ausgabe ist sofort bereit zur Einfügung in project.walls. Es werden * keine Seiteneffekte erzeugt — kein Store, kein Dispatch, kein React. * * @param parametricWall Das Regelwerk. * @param floorId ID des Ziel-Geschosses. * @param context Kontext (Geschoss-Objekt, Grid-Achsen, Grenzen, …). * @param defaultWallType Fallback-Wandtyp, wenn eine Regel keinen nennt. * @param tolerance Näherungstoleranz für Duplikat-Erkennung (Meter, Default 0.01). * @returns Wall[]-Array, bereit zur Einfügung. */ export function resolveParametricWall( parametricWall: ParametricWall, floorId: string, context: ParametricContext, defaultWallType: WallType, tolerance?: number, ): Wall[]; /** * Dispatcher: delegiert eine Regel an die passende Implementierung. * Exportiert für Unit-Tests und erweiterbare Regeltypen. */ export function applyRule(rule: ParametricRule, ctx: RuleCtx): Wall[]; /** * Entfernt doppelte Wände: zwei Wände gelten als Duplikat, wenn Start- und * Endpunkt jeweils innerhalb tolerance übereinstimmen (vorwärts und rückwärts). */ export function deduplicateWalls(walls: Wall[], tolerance?: number): Wall[]; ``` ### 4.1 Höhenauflösung Die Wandhöhe (`Wall.height`) ergibt sich nach folgender Priorität: 1. `rule.height`, falls an der einzelnen Regel gesetzt. 2. `context.floor.floorHeight` des Zielgeschosses. 3. Fallback: 2.6 m (globaler Default, CONVENTIONS.md). ### 4.2 ID-Schema ``` "pw--gx-" // GridRule, X-Achse "pw--gy-" // GridRule, Y-Achse "pw--mx-" // ModuleRule, X-Teilung "pw--ct-" // ConditionalThicknessRule "pw--rl-" // ReferenceLineRule ``` IDs sind sessionlokal (Zähler startet bei 0 je Modullade). Eingebrannte Wände erhalten beim Commit neue stabile IDs über `uniqueId("W")` — konsistent mit dem Wand-Werkzeug (vgl. [drawing-tools.md §8](drawing-tools.md#8-id-vergabe--immutabilität)). ### 4.3 Duplikat-Erkennung `deduplicateWalls` vergleicht Start-/Endpunkte beider Wände (vorwärts: A→B == A→B, und rückwärts: A→B == B→A) innerhalb einer Toleranz von 1 cm (0.01 m). Die **erste** Instanz wird behalten; spätere Duplikate werden verworfen. Dies ist wichtig bei Vollrastern (`"both"`), bei denen X- und Y-Wände exakt auf einem Rasterpunkt zusammentreffen könnten. ### 4.4 Verhalten bei ungültigen Eingaben | Situation | Verhalten | |-----------|-----------| | `spacing <= 0` oder `moduleSize <= 0` | `[]` | | `referenceWallId` nicht in `existingWalls` | Fallback auf Bounding-Box, kein Fehler | | Unbekannter `condition`-String | `matchesCondition` gibt `false` zurück (kein Treffer) | | Unbekannter `SequenceRule`-Untertyp | TypeScript exhaustiveness-Guard, `[]` | | Segment mit `|end - start| < 1e-6` m | Kann durch deduplicateWalls entfernt werden | --- ## 5. Integration ins Projekt ### 5.1 Ressourcen-Speicherung `ParametricWall`-Einträge leben unter `Project.parametricWalls` (optionales Array). Sie sind Teil des `.cad.json`-Dokuments und werden mit dem Rest des Projekts gespeichert. ```ts // sampleProject.ts — Beispieleintrag export const sampleProject: Project = { // … parametricWalls: [ { id: "pw-eg-raster", name: "EG Längswände 3m-Raster", defaultWallTypeId: "wt-innen-15", rules: [ { type: "grid", spacing: 3.0, directions: "x" }, { type: "conditional-thickness", condition: "exterior", wallTypeId: "wt-aussen-36" }, ], }, ], }; ``` ### 5.2 Rendering ohne UI (Phase A) In Phase A werden parametrische Wände **nicht** automatisch gerendert. Der Auflöser ist eine reine Funktion; Aufrufer müssen ihn explizit einbinden. Mögliche Verwendung in `generatePlan` oder `Viewport3D`: ```ts // generatePlan.ts (Ergänzung, Phase A) const defaultWallType = project.wallTypes[0]; const extraWalls = (project.parametricWalls ?? []).flatMap((pw) => resolveParametricWall(pw, activeLevelId, { floor: activeFloor, boundaryGeometry: projectBoundary, }, defaultWallType) ); const allWalls = [...project.walls, ...extraWalls]; // … allWalls statt project.walls in der Rendering-Pipeline verwenden ``` ### 5.3 Keine UI in Phase A Kein Command, kein Panel, kein Formular. `ParametricWall`-Einträge werden in Phase A ausschließlich **programmatisch** (Unit-Tests, `sampleProject`, direkte JSON-Bearbeitung des Projekts) erstellt. --- ## 6. Ausblick: Folge-Phasen ### Phase B — UI und Command-Schnittstelle - Neues Command (z. B. `PWWALL`) oder Ressourcen-Manager-Tab „Parametrische Wände" mit Formular-Editor je Regeltyp. - „Einbrennen" (Flatten): `ParametricWall` → feste `Wall[]` in `Project.walls` einfügen und den `ParametricWall`-Eintrag entfernen (unidirektional, Undo über Store). - Auswahl parametrischer Wände im Plan (als Gruppe); Grip-Editing der Raster-Parameter und Spannweiten. ### Phase C — Grid-Ressource und Schnittpunkt-Clipping - `GridResource`: ein projektweites, benanntes Koordinatenraster (LV95-Offset, Rasterweite, Drehung), auf das mehrere `GridRule`-Instanzen via `gridId` verweisen. - Präzises Clipping: erzeugte Wände werden am Gebäudeumriss getrimmt — exakte `lineIntersect`-Berechnung statt Bounding-Box-Approximation. - Benutzerdefinierte Tags (`Wall.tags[]`) für komplexe `ConditionalThicknessRule`- Bedingungen jenseits von „exterior/interior/bearing". - IFC-Export: `ParametricWall`-Gruppen → `IfcWallStandardCase` mit parametrischen Attributen und `IfcRelDefinesByType`. --- ## 7. Vollständige Anwendungsbeispiele ### 7.1 Schweizer Wohnbau: 3-m-Raster EG + 1.OG Zwei-Geschoss-Wohnhaus, typisches CH-Wohnbauraster. Die Längswände beider Geschosse entstehen aus zwei `ParametricWall`-Einträgen mit identischen Regeln, unterschieden nur durch `floorId` beim Auflösen: ``` Top-down (Grundriss): y=12 ──────────────────────────── (W5) y=9 ──────────────────────────── (W4) y=6 ──────────────────────────── (W3) y=3 ──────────────────────────── (W2) y=0 ──────────────────────────── (W1) ↑ x=0 x=12 ``` ```ts const rasterRegel: ParametricWall = { id: "pw-laengswand-raster", name: "Längswände 3m-Raster", defaultWallTypeId: "wt-innen-15", rules: [ { type: "grid", spacing: 3.0, directions: "x" }, // Außenwände (y=0 und y=12) erhalten den dicken Aufbau: { type: "conditional-thickness", condition: "exterior", wallTypeId: "wt-aussen-36" }, // Außenwände: Achse liegt auf der Fassadenfläche: { type: "reference-line", referenceLine: "left", target: "exterior" }, ], }; // EG auflösen: const wallsEG = resolveParametricWall(rasterRegel, "floor-eg", { floor: egFloor, boundaryGeometry: { boundary: rect(0,0,12,12) } }, project.wallTypes[0]); // 1.OG auflösen (gleiche Regel, anderes Geschoss): const wallsOG = resolveParametricWall(rasterRegel, "floor-og1", { floor: ogFloor, boundaryGeometry: { boundary: rect(0,0,12,12) } }, project.wallTypes[0]); // Änderung spacing: 3.5 → beide Geschosse sofort konsistent. ``` ### 7.2 Vollraster mit Außen/Innen-Differenzierung Gebäudeumriss als Rechteck; die Randwände erhalten automatisch den dicken Außenwand-Typ, alle anderen den dünnen Innenwand-Typ: ```ts const vollraster: ParametricWall = { id: "pw-eg-vollraster", name: "EG Vollraster mit Differenzierung", defaultWallTypeId: "wt-innen-15", rules: [ { type: "grid", spacing: 3.0, directions: "both" }, { type: "conditional-thickness", condition: "exterior", wallTypeId: "wt-aussen-36" }, { type: "conditional-thickness", condition: "interior", wallTypeId: "wt-innen-15" }, { type: "reference-line", referenceLine: "left", target: "exterior" }, ], }; ``` ``` ─────┬─────┬─────┬───── │ │ │ │ │ ─────┼─────┼─────┼───── │ │ │ │ │ ─────┴─────┴─────┴───── Rand-Segmente: wt-aussen-36 (dicker Aufbau) Innen-Segmente: wt-innen-15 (dünner Aufbau) ``` ### 7.3 Modulbauweise: 6-m-Joch, Bürogebäude Längliches Bürogebäude, 36 m × 12 m, 6-m-Joch. Querwände entstehen automatisch; Entwurfsänderung (z. B. auf 7.2-m-Joch) erfordert eine einzige Zahl: ```ts const joch: ParametricWall = { id: "pw-buero-joch", name: "Büro 6m-Joch", defaultWallTypeId: "wt-beton-20", rules: [ { type: "module", moduleSize: 6.0, direction: "x", // Querwände senkrecht zur X-Achse // referenceWallId: "W-sudfassade" → Spanne aus Referenzwand }, ], }; // resolveParametricWall → Querwände bei x = 6, 12, 18, 24, 30 // (bei Bounding-Box minX=0, maxX=36, Enden selbst ausgespart) // Änderung auf 7.2-m-Joch: moduleSize: 7.2 // → 4 Trennwände bei x ≈ 7.2, 14.4, 21.6, 28.8 — automatisch neu berechnet. ``` --- ## 8. Architektur-Garantien - **Modell bleibt einzige Wahrheit.** `ParametricWall`-Definitionen sind Daten in `Project.parametricWalls`; `resolveParametricWall` ist eine **reine Funktion** ohne Side-Effects. Keine globale Laufzeit-Geometrie-Schicht. - **Erzeugte Wände sind gewöhnliche `Wall`-Objekte.** Alle nachgelagerten Systeme (`generatePlan`, `Viewport3D`, `computeJoins`) arbeiten unverändert; sie müssen nicht zwischen „parametrisch erzeugten" und „direkt gezeichneten" Wänden unterscheiden. - **Fehlertoleranz statt Absturz.** Unbekannte Regeltypen liefern `[]`; der TypeScript- exhaustiveness-Guard fängt fehlende `case`-Zweige zur Compilezeit. Unbekannte Bedingungsstrings in `ConditionalThicknessRule` geben `false` (kein Treffer) statt zu werfen. - **Keine vorzeitige Generalisierung.** Phase A liefert fünf Regel-Varianten und einen Auflöser. UI, Command-Schnittstelle und Grid-Ressource folgen in Phase B/C. - **Immutabilität.** Verfeinerungsregeln (`ConditionalThicknessRule`, `ReferenceLineRule`) geben modifizierte **Kopien** zurück; `existingWalls` werden nie mutiert — konsistent mit der `setProject`-Konvention (CONVENTIONS.md).