Files
DOSSIER-STANDALONE/src/model/parametricWalls.test.ts
T
karim cfe5249440 Add parametric walls unit tests
50 Vitest-Tests für die parametrische Wand-Engine (src/model/parametricWalls.ts):
GridRule, ModuleRule, ConditionalThicknessRule, ReferenceLineRule, SequenceRule,
deduplicateWalls, matchesCondition/matchesTarget, Integration via resolveParametricWall.
Vitest als devDependency + Testskript in package.json + vitest.config.ts ergänzt.
2026-07-01 20:33:38 +02:00

1110 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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>): 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>): WallType {
return {
id,
name: "Standard",
layers: [{ componentId: "c1", thickness: 0.2 }],
...overrides,
};
}
function makeContext(overrides?: Partial<ParametricContext>): 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> = {}): 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
});
});