// Parametrische Wand-Engine — Unit-Tests. // // Testet die reine Datenschicht (kein React, kein three.js, kein Store). // Alle Tests laufen gegen die öffentliche API: resolveParametricWall(), // deduplicateWalls(), matchesCondition(), matchesTarget(). import { describe, it, expect, beforeEach } from "vitest"; import { resolveParametricWall, deduplicateWalls, matchesCondition, matchesTarget, applyGridRule, applyModuleRule, applyConditionalThicknessRule, applyReferenceLineRule, applySequenceRule, type ParametricContext, } from "./parametricWalls"; import type { DrawingLevel, GridRule, ModuleRule, ConditionalThicknessRule, ReferenceLineRule, SequenceRule, ParametricWall, Wall, WallType, } from "./types"; // ── Gemeinsame Test-Fixtures ────────────────────────────────────────────── function makeFloor(overrides?: Partial): DrawingLevel { return { id: "f1", name: "Erdgeschoss", kind: "floor", visible: true, locked: false, floorHeight: 2.6, cutHeight: 1.0, baseElevation: 0, ...overrides, }; } function makeWallType(id = "wt1", overrides?: Partial): WallType { return { id, name: "Standard", layers: [{ componentId: "c1", thickness: 0.2 }], ...overrides, }; } function makeContext(overrides?: Partial): ParametricContext { return { floor: makeFloor(), ...overrides, }; } /** Baut einen internen RuleCtx für direkte Regel-Funktionen. */ function makeRuleCtx( floorId: string, defaultWallType: WallType, context: ParametricContext, existingWalls: Wall[] = [], ) { // RuleCtx ist ein internes Interface; wir übergeben als unknown → any. return { floorId, defaultWallType, context, existingWalls } as Parameters< typeof applyGridRule >[1]; } function makeWall(overrides: Partial = {}): Wall { return { id: "w-test", type: "wall", floorId: "f1", categoryCode: "20", start: { x: 0, y: 0 }, end: { x: 3, y: 0 }, wallTypeId: "wt1", height: 2.6, ...overrides, }; } // ── GridRule ───────────────────────────────────────────────────────────────── describe("GridRule", () => { const defaultWt = makeWallType("wt-default"); const ctx = makeContext(); it("generiert Wände auf einem 3m-Raster in X-Richtung", () => { const rule: GridRule = { type: "grid", spacing: 3, directions: "x", }; const rCtx = makeRuleCtx("f1", defaultWt, ctx); const walls = applyGridRule(rule, rCtx); // X-Richtung: Wände senkrecht zur X-Achse (bei x=0,3,6,…,30) expect(walls.length).toBeGreaterThan(0); // Jede Wand läuft parallel zur Y-Achse (konstantes x) for (const w of walls) { expect(w.start.x).toBeCloseTo(w.end.x, 5); } }); it("generiert Wände in Y-Richtung", () => { const rule: GridRule = { type: "grid", spacing: 3, directions: "y", }; const rCtx = makeRuleCtx("f1", defaultWt, ctx); const walls = applyGridRule(rule, rCtx); expect(walls.length).toBeGreaterThan(0); // Jede Wand läuft parallel zur X-Achse (konstantes y) for (const w of walls) { expect(w.start.y).toBeCloseTo(w.end.y, 5); } }); it("generiert Wände in beiden Richtungen bei 'both'", () => { const rule: GridRule = { type: "grid", spacing: 3, directions: "both", }; const rCtx = makeRuleCtx("f1", defaultWt, ctx); const walls = applyGridRule(rule, rCtx); const xWalls = walls.filter((w) => Math.abs(w.start.x - w.end.x) < 1e-6); const yWalls = walls.filter((w) => Math.abs(w.start.y - w.end.y) < 1e-6); expect(xWalls.length).toBeGreaterThan(0); expect(yWalls.length).toBeGreaterThan(0); }); it("verwendet wallTypeId aus der Regel", () => { const rule: GridRule = { type: "grid", spacing: 3, directions: "x", wallTypeId: "wt-exterior", }; const rCtx = makeRuleCtx("f1", defaultWt, ctx); const walls = applyGridRule(rule, rCtx); expect(walls.length).toBeGreaterThan(0); for (const w of walls) { expect(w.wallTypeId).toBe("wt-exterior"); } }); it("fällt auf defaultWallType zurück wenn kein wallTypeId", () => { const rule: GridRule = { type: "grid", spacing: 3, directions: "x", }; const rCtx = makeRuleCtx("f1", defaultWt, ctx); const walls = applyGridRule(rule, rCtx); for (const w of walls) { expect(w.wallTypeId).toBe(defaultWt.id); } }); it("setzt referenceLine wenn angegeben", () => { const rule: GridRule = { type: "grid", spacing: 3, directions: "x", referenceLine: "left", }; const rCtx = makeRuleCtx("f1", defaultWt, ctx); const walls = applyGridRule(rule, rCtx); expect(walls.length).toBeGreaterThan(0); for (const w of walls) { expect(w.referenceLine).toBe("left"); } }); it("clips Raster auf boundaryGeometry-BBox", () => { const rule: GridRule = { type: "grid", spacing: 3, directions: "x", }; const ctxWithBoundary = makeContext({ boundaryGeometry: { boundary: [ { x: 0, y: 0 }, { x: 6, y: 0 }, { x: 6, y: 9 }, { x: 0, y: 9 }, ], }, }); const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary); const walls = applyGridRule(rule, rCtx); // Im 6×9m-Bereich mit 3m-Raster: 3 X-Achsen (0, 3, 6) → 3 Wände expect(walls.length).toBe(3); // Keine Wand soll außerhalb von [0..9] in Y liegen for (const w of walls) { expect(Math.min(w.start.y, w.end.y)).toBeGreaterThanOrEqual(0 - 1e-6); expect(Math.max(w.start.y, w.end.y)).toBeLessThanOrEqual(9 + 1e-6); } }); it("verwendet gridAxes aus dem Kontext wenn gridId gesetzt ist", () => { const rule: GridRule = { type: "grid", spacing: 3, directions: "x", gridId: "grid-1", }; const ctxWithAxes = makeContext({ gridAxes: { x: [0, 5, 10], y: [0, 4, 8] }, }); const rCtx = makeRuleCtx("f1", defaultWt, ctxWithAxes); const walls = applyGridRule(rule, rCtx); // 3 Achsen → 3 Wände in X-Richtung expect(walls.length).toBe(3); const xs = walls.map((w) => w.start.x).sort((a, b) => a - b); expect(xs[0]).toBeCloseTo(0); expect(xs[1]).toBeCloseTo(5); expect(xs[2]).toBeCloseTo(10); }); it("setzt alle Wände auf das korrekte Geschoss", () => { const rule: GridRule = { type: "grid", spacing: 3, directions: "both", }; const rCtx = makeRuleCtx("f1", defaultWt, ctx); const walls = applyGridRule(rule, rCtx); for (const w of walls) { expect(w.floorId).toBe("f1"); } }); }); // ── ModuleRule ──────────────────────────────────────────────────────────────── describe("ModuleRule", () => { const defaultWt = makeWallType("wt-module"); const ctx = makeContext({ boundaryGeometry: { boundary: [ { x: 0, y: 0 }, { x: 18, y: 0 }, { x: 18, y: 9 }, { x: 0, y: 9 }, ], }, }); it("teilt eine 18m-Spanne in 6m-Joche (X-Richtung)", () => { const rule: ModuleRule = { type: "module", moduleSize: 6, direction: "x", }; const rCtx = makeRuleCtx("f1", defaultWt, ctx); const walls = applyModuleRule(rule, rCtx); // Bei 18m Breite und 6m-Modulen: 2 innere Trennwände (bei x=6, x=12) expect(walls.length).toBe(2); const xs = walls.map((w) => w.start.x).sort((a, b) => a - b); expect(xs[0]).toBeCloseTo(6); expect(xs[1]).toBeCloseTo(12); }); it("teilt eine Spanne in Y-Richtung", () => { const rule: ModuleRule = { type: "module", moduleSize: 3, direction: "y", }; const rCtx = makeRuleCtx("f1", defaultWt, ctx); const walls = applyModuleRule(rule, rCtx); // 9m / 3m = 3 Joche → 2 innere Trennwände (bei y=3, y=6) expect(walls.length).toBe(2); const ys = walls.map((w) => w.start.y).sort((a, b) => a - b); expect(ys[0]).toBeCloseTo(3); expect(ys[1]).toBeCloseTo(6); }); it("snappt an eine Referenzwand", () => { const refWall: Wall = makeWall({ id: "ref-wall", start: { x: 0, y: 0 }, end: { x: 12, y: 0 }, }); const rule: ModuleRule = { type: "module", moduleSize: 6, direction: "x", referenceWallId: "ref-wall", }; const ctxWithBound = makeContext({ boundaryGeometry: { boundary: [ { x: 0, y: 0 }, { x: 18, y: 0 }, { x: 18, y: 6 }, { x: 0, y: 6 }, ], }, }); const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBound, [refWall]); const walls = applyModuleRule(rule, rCtx); // Referenzwand 12m → 1 innere Trennwand bei x=6 expect(walls.length).toBe(1); expect(walls[0].start.x).toBeCloseTo(6); }); it("verwendet Geschoss-Ausdehnung wenn keine Referenzwand angegeben", () => { const rule: ModuleRule = { type: "module", moduleSize: 6, direction: "x", }; const rCtx = makeRuleCtx("f1", defaultWt, ctx); const walls = applyModuleRule(rule, rCtx); // 18m Breite → 2 innere Trennwände expect(walls.length).toBeGreaterThan(0); }); }); // ── ConditionalThicknessRule ────────────────────────────────────────────────── describe("ConditionalThicknessRule", () => { const defaultWt = makeWallType("wt-inner"); const exteriorTypeId = "wt-exterior"; const ctxWithBoundary = makeContext({ boundaryGeometry: { boundary: [ { x: 0, y: 0 }, { x: 12, y: 0 }, { x: 12, y: 9 }, { x: 0, y: 9 }, ], }, }); it("ändert wallTypeId für Außenwände (exterior)", () => { // Wände auf dem Rand (y=0) — exterior const exteriorWall: Wall = makeWall({ id: "w-ext", start: { x: 0, y: 0 }, end: { x: 12, y: 0 }, wallTypeId: "wt-inner", }); // Innenwand const interiorWall: Wall = makeWall({ id: "w-int", start: { x: 3, y: 3 }, end: { x: 9, y: 3 }, wallTypeId: "wt-inner", }); const rule: ConditionalThicknessRule = { type: "conditional-thickness", condition: "exterior", wallTypeId: exteriorTypeId, }; const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary, [exteriorWall, interiorWall]); const result = applyConditionalThicknessRule(rule, rCtx); // Nur die Außenwand wird geändert expect(result.length).toBe(1); expect(result[0].wallTypeId).toBe(exteriorTypeId); }); it("ändert wallTypeId für Innenwände (interior)", () => { const exteriorWall: Wall = makeWall({ id: "w-ext", start: { x: 0, y: 0 }, end: { x: 12, y: 0 }, wallTypeId: "wt-standard", }); const interiorWall: Wall = makeWall({ id: "w-int", start: { x: 3, y: 3 }, end: { x: 9, y: 3 }, wallTypeId: "wt-standard", }); const rule: ConditionalThicknessRule = { type: "conditional-thickness", condition: "interior", wallTypeId: "wt-partition", }; const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary, [exteriorWall, interiorWall]); const result = applyConditionalThicknessRule(rule, rCtx); expect(result.length).toBe(1); expect(result[0].wallTypeId).toBe("wt-partition"); }); it("gibt keine Wand zurück wenn keine Bedingung passt", () => { const interiorWall: Wall = makeWall({ id: "w-int", start: { x: 3, y: 3 }, end: { x: 9, y: 3 }, wallTypeId: "wt-inner", }); const rule: ConditionalThicknessRule = { type: "conditional-thickness", condition: "exterior", wallTypeId: exteriorTypeId, }; const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary, [interiorWall]); const result = applyConditionalThicknessRule(rule, rCtx); expect(result.length).toBe(0); }); it("erkennt tragende Wände (bearing) nach Achsenrichtung", () => { // Wand fast exakt in X-Richtung const bearingWall: Wall = makeWall({ id: "w-bear", start: { x: 0, y: 0 }, end: { x: 10, y: 0.05 }, // nahezu horizontal wallTypeId: "wt-std", }); // Diagonale Wand const diagWall: Wall = makeWall({ id: "w-diag", start: { x: 0, y: 0 }, end: { x: 5, y: 5 }, // 45° wallTypeId: "wt-std", }); const rule: ConditionalThicknessRule = { type: "conditional-thickness", condition: "bearing", wallTypeId: "wt-bearing", }; const simpleCtx = makeContext(); const rCtx = makeRuleCtx("f1", defaultWt, simpleCtx, [bearingWall, diagWall]); const result = applyConditionalThicknessRule(rule, rCtx); // Nur die Achsen-Wand gilt als tragend expect(result.length).toBe(1); expect(result[0].id).not.toBe(diagWall.id); // neue ID per makeWallId expect(result[0].wallTypeId).toBe("wt-bearing"); }); it("mutiert keine Originalwände", () => { const original: Wall = makeWall({ id: "w-orig", start: { x: 0, y: 0 }, end: { x: 12, y: 0 }, wallTypeId: "wt-orig", }); const originalTypeBefore = original.wallTypeId; const rule: ConditionalThicknessRule = { type: "conditional-thickness", condition: "exterior", wallTypeId: "wt-new", }; const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary, [original]); applyConditionalThicknessRule(rule, rCtx); expect(original.wallTypeId).toBe(originalTypeBefore); }); }); // ── ReferenceLineRule ───────────────────────────────────────────────────────── describe("ReferenceLineRule", () => { const defaultWt = makeWallType(); const ctxWithBoundary = makeContext({ boundaryGeometry: { boundary: [ { x: 0, y: 0 }, { x: 9, y: 0 }, { x: 9, y: 6 }, { x: 0, y: 6 }, ], }, }); it("setzt referenceLine='left' für alle Wände (target='all')", () => { const walls: Wall[] = [ makeWall({ id: "w1", start: { x: 0, y: 0 }, end: { x: 9, y: 0 } }), makeWall({ id: "w2", start: { x: 3, y: 2 }, end: { x: 6, y: 2 } }), ]; const rule: ReferenceLineRule = { type: "reference-line", referenceLine: "left", target: "all", }; const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary, walls); const result = applyReferenceLineRule(rule, rCtx); expect(result.length).toBe(2); for (const w of result) { expect(w.referenceLine).toBe("left"); } }); it("setzt referenceLine='right' nur für Außenwände (target='exterior')", () => { // Außenwand am Rand const extWall: Wall = makeWall({ id: "w-ext", start: { x: 0, y: 0 }, end: { x: 9, y: 0 }, }); // Innenwand const intWall: Wall = makeWall({ id: "w-int", start: { x: 3, y: 3 }, end: { x: 6, y: 3 }, }); const rule: ReferenceLineRule = { type: "reference-line", referenceLine: "right", target: "exterior", }; const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary, [extWall, intWall]); const result = applyReferenceLineRule(rule, rCtx); // Nur die Außenwand erhält referenceLine expect(result.length).toBe(1); expect(result[0].referenceLine).toBe("right"); }); it("setzt referenceLine='center' für alle Wände", () => { const walls: Wall[] = [ makeWall({ id: "w1" }), makeWall({ id: "w2", referenceLine: "left" }), ]; const rule: ReferenceLineRule = { type: "reference-line", referenceLine: "center", target: "all", }; const rCtx = makeRuleCtx("f1", defaultWt, makeContext(), walls); const result = applyReferenceLineRule(rule, rCtx); expect(result.length).toBe(2); for (const w of result) { expect(w.referenceLine).toBe("center"); } }); it("gibt leere Liste zurück wenn keine existingWalls", () => { const rule: ReferenceLineRule = { type: "reference-line", referenceLine: "left", target: "all", }; const rCtx = makeRuleCtx("f1", defaultWt, makeContext(), []); const result = applyReferenceLineRule(rule, rCtx); expect(result).toHaveLength(0); }); }); // ── SequenceRule ────────────────────────────────────────────────────────────── describe("SequenceRule", () => { const defaultWt = makeWallType("wt-seq"); const ctx = makeContext({ boundaryGeometry: { boundary: [ { x: 0, y: 0 }, { x: 9, y: 0 }, { x: 9, y: 6 }, { x: 0, y: 6 }, ], }, }); it("wendet Regeln in Reihenfolge an", () => { const sequence: SequenceRule = { type: "sequence", rules: [ { type: "grid", spacing: 3, directions: "x", wallTypeId: "wt-base", } as GridRule, { type: "conditional-thickness", condition: "exterior", wallTypeId: "wt-exterior", } as ConditionalThicknessRule, ], }; const rCtx = makeRuleCtx("f1", defaultWt, ctx); const result = applySequenceRule(sequence, rCtx); // Schritt 1: Raster erzeugt Wände; Schritt 2: Außenwände erhalten neuen Typ. // Beide Schritte liefern Wände zurück → Summe expect(result.length).toBeGreaterThan(0); }); it("spätere Regeln sehen Wände früherer Regeln", () => { // Schritt 1: Raster (X) erzeugt senkrechte Wände // Schritt 2: ConditionalThickness sieht diese Wände und kann sie verfeinern const sequence: SequenceRule = { type: "sequence", rules: [ { type: "grid", spacing: 3, directions: "x", wallTypeId: "wt-step1", } as GridRule, { type: "conditional-thickness", condition: "exterior", wallTypeId: "wt-step2", } as ConditionalThicknessRule, ], }; const rCtx = makeRuleCtx("f1", defaultWt, ctx); const result = applySequenceRule(sequence, rCtx); // Schritt 2 sieht die Ausgabe von Schritt 1 (existingWalls enthält sie) const step2Walls = result.filter((w) => w.wallTypeId === "wt-step2"); expect(step2Walls.length).toBeGreaterThan(0); }); it("stopOnMatch bricht nach dem ersten produktiven Schritt ab", () => { const sequence: SequenceRule = { type: "sequence", stopOnMatch: true, rules: [ { type: "grid", spacing: 3, directions: "x", wallTypeId: "wt-first", } as GridRule, { type: "grid", spacing: 3, directions: "y", wallTypeId: "wt-second", } as GridRule, ], }; const rCtx = makeRuleCtx("f1", defaultWt, ctx); const result = applySequenceRule(sequence, rCtx); // Nur Schritt 1 (X-Wände), Schritt 2 soll nicht mehr laufen expect(result.length).toBeGreaterThan(0); for (const w of result) { expect(w.wallTypeId).toBe("wt-first"); } }); it("stopOnMatch=false führt alle Regeln aus", () => { const sequence: SequenceRule = { type: "sequence", stopOnMatch: false, rules: [ { type: "grid", spacing: 3, directions: "x", wallTypeId: "wt-x", } as GridRule, { type: "grid", spacing: 3, directions: "y", wallTypeId: "wt-y", } as GridRule, ], }; const rCtx = makeRuleCtx("f1", defaultWt, ctx); const result = applySequenceRule(sequence, rCtx); const xWalls = result.filter((w) => w.wallTypeId === "wt-x"); const yWalls = result.filter((w) => w.wallTypeId === "wt-y"); expect(xWalls.length).toBeGreaterThan(0); expect(yWalls.length).toBeGreaterThan(0); }); it("leere Sequenz liefert leeres Array", () => { const sequence: SequenceRule = { type: "sequence", rules: [], }; const rCtx = makeRuleCtx("f1", defaultWt, ctx); const result = applySequenceRule(sequence, rCtx); expect(result).toHaveLength(0); }); }); // ── deduplicateWalls ────────────────────────────────────────────────────────── describe("deduplicateWalls", () => { it("entfernt doppelte Wände mit identischen Endpunkten", () => { const wall1 = makeWall({ id: "a", start: { x: 0, y: 0 }, end: { x: 3, y: 0 } }); const wall2 = makeWall({ id: "b", start: { x: 0, y: 0 }, end: { x: 3, y: 0 } }); const result = deduplicateWalls([wall1, wall2]); expect(result).toHaveLength(1); }); it("entfernt Duplikate mit umgekehrter Endpunkt-Reihenfolge", () => { const wall1 = makeWall({ id: "a", start: { x: 0, y: 0 }, end: { x: 3, y: 0 } }); const wall2 = makeWall({ id: "b", start: { x: 3, y: 0 }, end: { x: 0, y: 0 } }); const result = deduplicateWalls([wall1, wall2]); expect(result).toHaveLength(1); }); it("behält eindeutige Wände", () => { const wall1 = makeWall({ id: "a", start: { x: 0, y: 0 }, end: { x: 3, y: 0 } }); const wall2 = makeWall({ id: "b", start: { x: 0, y: 3 }, end: { x: 3, y: 3 } }); const result = deduplicateWalls([wall1, wall2]); expect(result).toHaveLength(2); }); it("behält die erste Instanz (nicht die letzte)", () => { const wall1 = makeWall({ id: "first", start: { x: 0, y: 0 }, end: { x: 3, y: 0 } }); const wall2 = makeWall({ id: "second", start: { x: 0, y: 0 }, end: { x: 3, y: 0 } }); const result = deduplicateWalls([wall1, wall2]); expect(result[0].id).toBe("first"); }); it("respektiert die Toleranz", () => { // Fast gleiche Wände (< 1cm) const wall1 = makeWall({ id: "a", start: { x: 0, y: 0 }, end: { x: 3, y: 0 } }); const wall2 = makeWall({ id: "b", start: { x: 0.005, y: 0.005 }, end: { x: 3.005, y: 0.005 } }); const resultStrict = deduplicateWalls([wall1, wall2], 0.001); // strenger Schwellwert expect(resultStrict).toHaveLength(2); const resultLoose = deduplicateWalls([wall1, wall2], 0.02); // lockerer Schwellwert expect(resultLoose).toHaveLength(1); }); it("gibt leere Liste zurück bei leerer Eingabe", () => { expect(deduplicateWalls([])).toHaveLength(0); }); }); // ── matchesCondition ────────────────────────────────────────────────────────── describe("matchesCondition", () => { const defaultWt = makeWallType(); const ctxWithBoundary = makeContext({ boundaryGeometry: { boundary: [ { x: 0, y: 0 }, { x: 12, y: 0 }, { x: 12, y: 8 }, { x: 0, y: 8 }, ], }, }); it("erkennt Außenwand am BBox-Rand (exterior)", () => { const extWall = makeWall({ start: { x: 0, y: 0 }, end: { x: 12, y: 0 }, }); const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary); expect(matchesCondition(extWall, "exterior", rCtx)).toBe(true); }); it("erkennt Innenwand nicht als exterior", () => { const intWall = makeWall({ start: { x: 3, y: 3 }, end: { x: 9, y: 3 }, }); const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary); expect(matchesCondition(intWall, "exterior", rCtx)).toBe(false); }); it("erkennt interior korrekt (Negation von exterior)", () => { const intWall = makeWall({ start: { x: 3, y: 3 }, end: { x: 9, y: 3 }, }); const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary); expect(matchesCondition(intWall, "interior", rCtx)).toBe(true); }); it("bearing = true für achsenparallele Wand", () => { const bearWall = makeWall({ start: { x: 0, y: 0 }, end: { x: 10, y: 0 }, }); const rCtx = makeRuleCtx("f1", defaultWt, makeContext()); expect(matchesCondition(bearWall, "bearing", rCtx)).toBe(true); }); it("bearing = false für 45°-Wand", () => { const diagWall = makeWall({ start: { x: 0, y: 0 }, end: { x: 5, y: 5 }, }); const rCtx = makeRuleCtx("f1", defaultWt, makeContext()); expect(matchesCondition(diagWall, "bearing", rCtx)).toBe(false); }); it("unbekannte Bedingung gibt false zurück", () => { const wall = makeWall(); const rCtx = makeRuleCtx("f1", defaultWt, makeContext()); expect(matchesCondition(wall, "unknown-condition-xyz", rCtx)).toBe(false); }); }); // ── matchesTarget ───────────────────────────────────────────────────────────── describe("matchesTarget", () => { const defaultWt = makeWallType(); const ctxWithBoundary = makeContext({ boundaryGeometry: { boundary: [ { x: 0, y: 0 }, { x: 9, y: 0 }, { x: 9, y: 6 }, { x: 0, y: 6 }, ], }, }); it("'all' passt auf jede Wand", () => { const anyWall = makeWall({ start: { x: 3, y: 3 }, end: { x: 6, y: 3 } }); const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary); expect(matchesTarget(anyWall, "all", rCtx)).toBe(true); }); it("'exterior' passt auf Außenwand", () => { const extWall = makeWall({ start: { x: 0, y: 0 }, end: { x: 9, y: 0 } }); const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary); expect(matchesTarget(extWall, "exterior", rCtx)).toBe(true); }); it("'exterior' passt nicht auf Innenwand", () => { const intWall = makeWall({ start: { x: 3, y: 3 }, end: { x: 6, y: 3 } }); const rCtx = makeRuleCtx("f1", defaultWt, ctxWithBoundary); expect(matchesTarget(intWall, "exterior", rCtx)).toBe(false); }); it("unbekanntes Ziel gibt false zurück", () => { const wall = makeWall(); const rCtx = makeRuleCtx("f1", defaultWt, makeContext()); expect(matchesTarget(wall, "unknown-target-xyz", rCtx)).toBe(false); }); }); // ── resolveParametricWall (Integration) ────────────────────────────────────── describe("resolveParametricWall (Integration)", () => { let floor: DrawingLevel; let defaultWt: WallType; let ctx: ParametricContext; beforeEach(() => { floor = makeFloor({ id: "f-integ" }); defaultWt = makeWallType("wt-integ-default"); ctx = makeContext({ floor, boundaryGeometry: { boundary: [ { x: 0, y: 0 }, { x: 9, y: 0 }, { x: 9, y: 6 }, { x: 0, y: 6 }, ], }, }); }); it("erzeugt Wall[]-Array aus einem GridRule-Regelwerk", () => { const pw: ParametricWall = { id: "pw-1", name: "Test-Raster", defaultWallTypeId: defaultWt.id, rules: [ { type: "grid", spacing: 3, directions: "both", }, ], }; const result = resolveParametricWall(pw, "f-integ", ctx, defaultWt); expect(result.length).toBeGreaterThan(0); for (const w of result) { expect(w.type).toBe("wall"); expect(w.floorId).toBe("f-integ"); } }); it("kombiniert GridRule + ConditionalThicknessRule (mehrere Regeln)", () => { // ConditionalThicknessRule erzeugt modifizierte Kopien, die jedoch nach der // Deduplication (gleiche Koordinaten, erste Instanz gewinnt) entfernt werden. // Das korrekte Verhalten: die Ausgabe enthält alle Grid-Wände unverändert; // die Conditional-Thickness-Kopien sind Duplikate und werden entfernt. const pw: ParametricWall = { id: "pw-2", name: "Raster + Dicke", defaultWallTypeId: defaultWt.id, rules: [ { type: "grid", spacing: 3, directions: "x", wallTypeId: "wt-integ-default", }, { type: "conditional-thickness", condition: "exterior", wallTypeId: "wt-integ-exterior", }, ], }; const result = resolveParametricWall(pw, "f-integ", ctx, defaultWt); expect(result.length).toBeGreaterThan(0); // Alle erhaltenen Wände stammen vom Grid (erste Instanz gewinnt bei Dedup) const baseWalls = result.filter((w) => w.wallTypeId === "wt-integ-default"); expect(baseWalls.length).toBeGreaterThan(0); // ConditionalThickness erzeugt zusätzliche Wände, die bei applyRule zurückgegeben werden // (testbar via applyConditionalThicknessRule direkt — dort bereits getestet) }); it("entfernt überlappende Duplikate", () => { // Zwei Regeln mit identischen Achsen (gleiche Wände) const pw: ParametricWall = { id: "pw-3", name: "Duplikat-Test", defaultWallTypeId: defaultWt.id, rules: [ { type: "grid", spacing: 3, directions: "x", wallTypeId: "wt-integ-default", }, { type: "grid", spacing: 3, directions: "x", wallTypeId: "wt-integ-default", }, ], }; const result = resolveParametricWall(pw, "f-integ", ctx, defaultWt); const singleRun = resolveParametricWall( { ...pw, rules: [pw.rules[0]] }, "f-integ", ctx, defaultWt, ); // Mit Dedup sollen keine doppelten Wände enthalten sein expect(result.length).toBe(singleRun.length); }); it("leeres Regelwerk liefert leeres Array", () => { const pw: ParametricWall = { id: "pw-empty", name: "Leer", defaultWallTypeId: defaultWt.id, rules: [], }; const result = resolveParametricWall(pw, "f-integ", ctx, defaultWt); expect(result).toHaveLength(0); }); it("Wall-Objekte haben korrekte Felder (type, floorId, categoryCode)", () => { const pw: ParametricWall = { id: "pw-fields", name: "Felder-Test", defaultWallTypeId: defaultWt.id, rules: [ { type: "grid", spacing: 3, directions: "x", }, ], }; const result = resolveParametricWall(pw, "f-integ", ctx, defaultWt); for (const w of result) { expect(w.type).toBe("wall"); expect(w.floorId).toBe("f-integ"); expect(w.categoryCode).toBe("20"); expect(typeof w.height).toBe("number"); expect(w.height).toBeGreaterThan(0); } }); it("verwendet Geschosshöhe als Wall-Höhe", () => { const floorWith3m = makeFloor({ id: "f-height", floorHeight: 3.0 }); const ctxWith3m = makeContext({ floor: floorWith3m, boundaryGeometry: { boundary: [ { x: 0, y: 0 }, { x: 6, y: 0 }, { x: 6, y: 6 }, { x: 0, y: 6 }, ], }, }); const pw: ParametricWall = { id: "pw-height", name: "Höhen-Test", defaultWallTypeId: defaultWt.id, rules: [ { type: "grid", spacing: 3, directions: "x", }, ], }; const result = resolveParametricWall(pw, "f-height", ctxWith3m, defaultWt); for (const w of result) { expect(w.height).toBeCloseTo(3.0); } }); it("Sequenzregel in resolveParametricWall: Raster → RefLinie", () => { // ReferenceLineRule erzeugt modifizierte Kopien der Grid-Wände (gleiche Koordinaten, // referenceLine='left'). Nach Deduplication gewinnt die erste Instanz (Grid-Wand ohne // referenceLine), daher liefert das Endergebnis Grid-Wände ohne referenceLine. // Das erwartete Verhalten: alle Wände vom Grid kommen durch, Sequenz läuft fehlerlos. const pw: ParametricWall = { id: "pw-seq", name: "Sequenz", defaultWallTypeId: defaultWt.id, rules: [ { type: "sequence", rules: [ { type: "grid", spacing: 3, directions: "x", wallTypeId: "wt-integ-default", }, { type: "reference-line", referenceLine: "left", target: "all", }, ], }, ], }; const result = resolveParametricWall(pw, "f-integ", ctx, defaultWt); expect(result.length).toBeGreaterThan(0); // Grid-Wände sind enthalten (erste Instanz gewinnt nach Dedup) const gridWalls = result.filter((w) => w.wallTypeId === "wt-integ-default"); expect(gridWalls.length).toBeGreaterThan(0); // ReferenceLineRule als isolierte Regel getestet in applyReferenceLineRule-Suite }); });