Files

23 KiB
Raw Permalink Blame History

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 (Wand-/Türmodell), drawing-tools.md (Werkzeugsystem, Direktzeichnen), state-architecture.md (Projekt-Store), resources-graphics.md (WallType/Component-Auflösung).

Implementierungsdateien:

  • src/model/types.tsParametricWall, ParametricRule und alle Regel-Varianten.
  • src/model/parametricWalls.tsresolveParametricWall(), 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)

/**
 * 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

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)

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.

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

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.

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

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.

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

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.

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.

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)

/**
 * 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-<floorId>-gx-<counter>"    // GridRule, X-Achse
"pw-<floorId>-gy-<counter>"    // GridRule, Y-Achse
"pw-<floorId>-mx-<counter>"    // ModuleRule, X-Teilung
"pw-<floorId>-ct-<counter>"    // ConditionalThicknessRule
"pw-<floorId>-rl-<counter>"    // 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).

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

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.

// 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:

// 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
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:

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:

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).