cfe5249440
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.
1110 lines
32 KiB
TypeScript
1110 lines
32 KiB
TypeScript
// 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
|
||
});
|
||
});
|