/** * Unit-Tests für die Tür-/Fenstertyp-Rahmenparameter im Grundriss * (generatePlan): Rahmenband (Zarge/Blockrahmen), Schichteinzug (insetFromFace/ * insetFace), Oberlicht (transomHeight). `mullionRows` bleibt bewusst ungetestet * — Kämpfer-Zeilen sind im Grundriss-Schnitt unsichtbar (3D-Sache). */ import { describe, it, expect } from "vitest"; import { generatePlan } from "./generatePlan"; import type { DoorType, Opening, Project, Wall, WindowType } from "../model/types"; /** Minimalprojekt: eine Wand (Dicke 0.3 m, Achse mittig) + Öffnungen und * optionale Tür-/Fenstertyp-Bibliotheken, konfigurierbar je Test. */ function project( openings: Opening[], opts: { doorTypes?: DoorType[]; windowTypes?: WindowType[] } = {}, ): Project { const wall: Wall = { id: "W1", type: "wall", floorId: "eg", categoryCode: "20", start: { x: 0, y: 0 }, end: { x: 4, y: 0 }, wallTypeId: "aw", height: 2.6, }; return { id: "t", name: "T", lineStyles: [{ id: "thin", name: "d", weight: 0.13, color: "#111", dash: null }], hatches: [{ id: "none", name: "Ohne", pattern: "none", scale: 1, angle: 0, color: "#111" }], components: [{ id: "a", name: "A", color: "#d8d2c7", hatchId: "none", joinPriority: 10 }], wallTypes: [{ id: "aw", name: "AW", layers: [{ componentId: "a", thickness: 0.3 }] }], doorTypes: opts.doorTypes, windowTypes: opts.windowTypes, drawingLevels: [ { id: "eg", name: "EG", kind: "floor", visible: true, locked: false, floorHeight: 2.6, cutHeight: 1.0, baseElevation: 0, }, ], layers: [ { code: "20", name: "Wände", color: "#0a0a0a", lw: 0.5, visible: true, locked: false }, { code: "21", name: "Öffnungen", color: "#0a0a0a", lw: 0.3, visible: true, locked: false }, ], walls: [wall], doors: [], openings, ceilings: [], stairs: [], rooms: [], drawings2d: [], context: [], }; } const visible = new Set(["20", "21"]); /** Basis-Fenster, jenseits der Tür auf derselben Wandachse. */ const baseWindow: Opening = { id: "F1", type: "opening", hostWallId: "W1", categoryCode: "21", kind: "window", position: 2.0, width: 1.2, height: 1.2, sillHeight: 0, }; /** Basis-Tür. */ const baseDoor: Opening = { id: "T1", type: "opening", hostWallId: "W1", categoryCode: "21", kind: "door", position: 0.2, width: 0.9, height: 2.0, sillHeight: 0, swing: "left", hinge: "start", }; /** Fenstertyp mit Rahmen, Schichteinzug (aussen) und Oberlicht. */ const windowType: WindowType = { id: "wt1", name: "Fenstertyp Test", kind: "dreh", wingCount: 1, glazing: "zweifach", frameThickness: 0.06, frameWidth: 0.05, insetFromFace: 0.05, insetFace: "aussen", transomHeight: 0.3, defaultSillHeight: 0.8, defaultWidth: 1.2, defaultHeight: 1.2, }; /** Türtyp mit Blockrahmen, Schichteinzug (innen) und Oberlicht. */ const doorType: DoorType = { id: "dt1", name: "Türtyp Test", kind: "dreh", leafCount: 1, leafStyle: "glatt", frameThickness: 0.06, frameKind: "blockrahmen", frameWidth: 0.06, insetFromFace: 0.04, insetFace: "innen", transomHeight: 0.4, defaultWidth: 0.9, defaultHeight: 2.0, }; type LinePrim = Extract["primitives"][number], { kind: "line" }>; function linesOfClass(p: Project, cls: string, openingId?: string): LinePrim[] { return generatePlan(p, "eg", visible, undefined, "mittel").primitives.filter( (pr): pr is LinePrim => pr.kind === "line" && pr.cls === cls && (!openingId || pr.openingId === openingId), ); } describe("generatePlan — Tür-/Fenstertyp-Rahmenband (opening-frame)", () => { it("erzeugt Rahmenband-Primitive für Tür UND Fenster mit referenziertem Typ", () => { const p = project( [ { ...baseWindow, typeId: "wt1" }, { ...baseDoor, typeId: "dt1" }, ], { doorTypes: [doorType], windowTypes: [windowType] }, ); expect(linesOfClass(p, "opening-frame").length).toBeGreaterThan(0); // Je Öffnung ein geschlossenes Rechteck aus 4 Kanten. expect(linesOfClass(p, "opening-frame", "F1").length).toBe(4); expect(linesOfClass(p, "opening-frame", "T1").length).toBe(4); }); it("Blockrahmen (Tür) zeichnet eine kräftigere Linie als die Zargen-Optik (Fenster)", () => { const p = project( [ { ...baseWindow, typeId: "wt1" }, { ...baseDoor, typeId: "dt1" }, ], { doorTypes: [doorType], windowTypes: [windowType] }, ); const doorWeight = linesOfClass(p, "opening-frame", "T1")[0].weightMm; const windowWeight = linesOfClass(p, "opening-frame", "F1")[0].weightMm; expect(doorWeight).toBeGreaterThan(windowWeight); }); }); describe("generatePlan — Oberlicht (opening-transom)", () => { it("erscheint genau dann, wenn transomHeight > 0", () => { const withTransom = project([{ ...baseWindow, typeId: "wt1" }], { windowTypes: [windowType] }); const transomLines = linesOfClass(withTransom, "opening-transom", "F1"); expect(transomLines.length).toBe(1); expect(Array.isArray(transomLines[0].dash) && transomLines[0].dash!.length).toBeGreaterThan(0); const noTransomType: WindowType = { ...windowType, id: "wt2", transomHeight: 0 }; const withoutTransom = project([{ ...baseWindow, typeId: "wt2" }], { windowTypes: [noTransomType] }); expect(linesOfClass(withoutTransom, "opening-transom", "F1").length).toBe(0); }); }); describe("generatePlan — Schichteinzug (insetFromFace/insetFace)", () => { it("verschiebt die Rahmen-Nahfläche um den Einzugsbetrag, die Gegenfläche bleibt unverändert", () => { const flushType: WindowType = { ...windowType, id: "wt-flush", insetFromFace: 0 }; const inset = project([{ ...baseWindow, typeId: "wt1" }], { windowTypes: [windowType] }); const flush = project([{ ...baseWindow, typeId: "wt-flush" }], { windowTypes: [flushType] }); // Die Wandachse ist horizontal (u=(1,0)) ⇒ Wandnormale n=(0,1); die // y-Koordinate der Rahmenband-Punkte entspricht direkt dem Offset quer zur // Wand. Die "aussen"-Fläche liegt bei +n — ihr Rahmenrand muss um genau // `insetFromFace` (0.05 m) nach innen (kleineres y) rücken; die // gegenüberliegende ("innen"-)Fläche bleibt unverändert. const insetYs = linesOfClass(inset, "opening-frame", "F1").flatMap((l) => [l.a.y, l.b.y]); const flushYs = linesOfClass(flush, "opening-frame", "F1").flatMap((l) => [l.a.y, l.b.y]); expect(Math.max(...flushYs) - Math.max(...insetYs)).toBeCloseTo(0.05, 9); expect(Math.min(...insetYs)).toBeCloseTo(Math.min(...flushYs), 9); }); }); describe("generatePlan — Rückwärtskompatibilität (Öffnung ohne typeId)", () => { it("liefert exakt dieselben Primitive, ob Typ-Bibliotheken im Projekt existieren oder nicht", () => { const withResources = project([{ ...baseWindow }, { ...baseDoor }], { doorTypes: [doorType], windowTypes: [windowType], }); const withoutResources = project([{ ...baseWindow }, { ...baseDoor }]); const a = generatePlan(withResources, "eg", visible, undefined, "mittel").primitives; const b = generatePlan(withoutResources, "eg", visible, undefined, "mittel").primitives; expect(a).toEqual(b); expect(a.some((pr) => pr.kind === "line" && pr.cls === "opening-frame")).toBe(false); expect(a.some((pr) => pr.kind === "line" && pr.cls === "opening-transom")).toBe(false); }); });