Files
DOSSIER-STANDALONE/src/plan/generatePlan.windowFields.test.ts
T
karim 13cc6a0d6a Fenster im Grundriss: grob/mittel/fein nach SIA 400 klar unterscheiden
grob (1:100) zeigt nur eine schematische Glaslinie, mittel (1:50) einen
Blendrahmen mit Flügel-Trennlinien und Stulp-Quadrat je Flügelstoss, fein
(1:20) zusätzlich verschachtelte Flügelrahmen, Glas als Doppellinie
(Isolierverglasung) und zwei Stulp-Quadrate je Stoss — vorher waren mittel
und fein praktisch identisch. Referenz: SIA 400 Anhang B.9.1, Fig. 36–38
(docs/research/sia400-fenster-tueren.md).
2026-07-11 18:18:37 +02:00

276 lines
11 KiB
TypeScript

/**
* Unit-Tests für die NEUEN Fenster-Modellfelder im Grundriss (generatePlan +
* geometry/opening.windowSymbol):
* • `WindowType.sashes` (Flügeltabelle) — ersetzt die gleichmäßige
* `wingCount`-Ableitung durch echte Flügel-/Pfostenbreiten, sobald ein
* Fenstertyp referenziert ist. Alt-Fall (keine `sashes`/kein `typeId`)
* bleibt UNVERÄNDERT (reines `Opening.wingCount`, siehe
* generatePlan.openings.test.ts).
* • `WindowType.glazingPanes` — steuert die Anzahl paralleler Glaslinien.
* • `WindowType.shading` — zeichnet eine zusätzliche, gestrichelte
* Kastenkontur über der Öffnung.
* • Öffnungsandeutung je Flügel (`SashDef.opening`/`hingeSide`).
*/
import { describe, it, expect } from "vitest";
import { generatePlan } from "./generatePlan";
import type { Opening, Project, Wall, WindowType } from "../model/types";
/** Minimalprojekt: eine Wand (Dicke 0.3 m, Achse mittig, horizontal entlang x)
* + eine Fenster-Öffnung + optionale Fenstertyp-Bibliothek. */
function project(opening: Opening, 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 }] }],
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: [opening],
ceilings: [],
stairs: [],
rooms: [],
drawings2d: [],
context: [],
};
}
const visible = new Set(["20", "21"]);
/** Basis-Fenster (Brüstung 0, damit keine Sill-Linie mitzählt), position 1.0, width 1.2. */
const baseWindow: Opening = {
id: "F1",
type: "opening",
hostWallId: "W1",
categoryCode: "21",
kind: "window",
position: 1.0,
width: 1.2,
height: 1.2,
sillHeight: 0,
};
/** Minimaler Fenstertyp (Grunddaten, Felder je Test überschrieben/ergänzt). */
const baseWindowType: WindowType = {
id: "wt1",
name: "Test-Fenster",
kind: "dreh",
wingCount: 1,
glazing: "einfach",
frameThickness: 0.06,
defaultSillHeight: 0.8,
defaultWidth: 1.2,
defaultHeight: 1.2,
};
function linesOfClass(p: Project, cls: string, detail: "grob" | "mittel" | "fein" = "mittel") {
return generatePlan(p, "eg", visible, undefined, detail).primitives.filter(
(pr) => pr.kind === "line" && pr.cls === cls,
);
}
describe("generatePlan — Flügeleinteilung aus sashes (Item 1)", () => {
it("Alt-Fall (kein typeId/sashes, nur wingCount=3) erzeugt genau 2 Pfostenlinien — Regression", () => {
const p = project({ ...baseWindow, wingCount: 3 });
expect(linesOfClass(p, "window-mullion").length).toBe(2);
});
it("WindowType MIT sashes (3 feste Flügelbreiten 0.4 m) erzeugt Pfostenlinien exakt an den Flügelgrenzen", () => {
// Öffnung 1.2 m breit, jambStart bei x=1.0 (position 1.0) -> jambEnd x=2.2.
// 3 Flügel à 0.4 m (autoWidth:false) -> Grenzen bei x=1.4 und x=1.8.
const wt: WindowType = {
...baseWindowType,
sashes: [
{ kind: "fluegel", autoWidth: false, width: 0.4, opening: "dreh", hingeSide: "left" },
{ kind: "fluegel", autoWidth: false, width: 0.4, opening: "kipp" },
{ kind: "fluegel", autoWidth: false, width: 0.4, opening: "dreh", hingeSide: "right" },
],
};
const p = project({ ...baseWindow, typeId: "wt1" }, [wt]);
const mullions = linesOfClass(p, "window-mullion");
expect(mullions.length).toBe(2);
const xs = mullions
.map((pr) => (pr.kind === "line" ? pr.a.x : NaN))
.sort((a, b) => a - b);
expect(xs[0]).toBeCloseTo(1.4, 6);
expect(xs[1]).toBeCloseTo(1.8, 6);
});
it("WindowType MIT sashes, aber alle autoWidth (kein fixes width/pfosten) -> identische Grenzen wie die alte wingCount-Gleichverteilung", () => {
// 3 gleichmässige Flügel über sashesOfWindowType (kein `sashes` am Typ,
// nur `wingCount: 3`) müssen exakt dieselben Pfosten liefern wie der
// Alt-Fall ohne Typ (Regressionssicherheit der Ableitung).
const wt: WindowType = { ...baseWindowType, wingCount: 3 };
const withType = project({ ...baseWindow, typeId: "wt1" }, [wt]);
const withoutType = project({ ...baseWindow, wingCount: 3 });
const linesOf = (p: Project) =>
linesOfClass(p, "window-mullion")
.map((pr) => (pr.kind === "line" ? pr.a.x : NaN))
.sort((a, b) => a - b);
const a = linesOf(withType);
const b = linesOf(withoutType);
expect(a.length).toBe(b.length);
a.forEach((v, i) => expect(v).toBeCloseTo(b[i], 9));
});
it("öffenbarer Flügel bekommt einen Flügelrahmen (window-sash, 4 Kanten) bei 'fein'; 'fest' keinen — SIA fig. 38 (verschachtelter Flügelrahmen nur bei fein, s. sia400-fenster-tueren.md)", () => {
// Ein öffenbarer + ein fester Flügel: genau EIN Flügelrahmen-Rechteck = 4
// Kanten-Linien (der feste Flügel bekommt keinen; Glas sitzt direkt im
// Blendrahmen). So liest man fest vs. öffenbar direkt im Grundriss.
// Nur bei „fein" (SIA fig. 38) — „mittel" (fig. 37) bleibt beim reinen
// Blendrahmen-Band ohne verschachtelten Flügelrahmen.
const wt: WindowType = {
...baseWindowType,
sashes: [
{ kind: "fluegel", autoWidth: true, opening: "dreh", hingeSide: "left" },
{ kind: "fluegel", autoWidth: true, opening: "fest" },
],
};
const p = project({ ...baseWindow, typeId: "wt1" }, [wt]);
expect(linesOfClass(p, "window-sash", "fein").length).toBe(4);
expect(linesOfClass(p, "window-sash", "mittel").length).toBe(0);
// Nur feste Flügel -> gar kein Flügelrahmen.
const wtFixed: WindowType = {
...baseWindowType,
sashes: [{ kind: "fluegel", autoWidth: true, opening: "fest" }],
};
const pFixed = project({ ...baseWindow, typeId: "wt1" }, [wtFixed]);
expect(linesOfClass(pFixed, "window-sash", "fein").length).toBe(0);
});
it("Alt-Fall (kein typeId) erzeugt KEINEN Flügelrahmen", () => {
const p = project({ ...baseWindow });
expect(linesOfClass(p, "window-sash", "fein").length).toBe(0);
});
});
describe("generatePlan — Verglasung glazingPanes (Item 2)", () => {
const glassLineCount = (p: Project, detail: "grob" | "mittel" | "fein" = "mittel") =>
linesOfClass(p, "window-glass", detail).length;
it("glazingPanes beeinflusst die 2D-Glaslinienzahl NICHT mehr — SIA-Detailgrad entscheidet (Doppellinie/IV nur bei 'fein', unabhängig von der Scheibenzahl)", () => {
const wt3: WindowType = { ...baseWindowType, id: "wt3", glazingPanes: 3 };
const wt1: WindowType = { ...baseWindowType, id: "wt1", glazingPanes: 1 };
const p3 = project({ ...baseWindow, typeId: "wt3" }, [wt3]);
const p1 = project({ ...baseWindow, typeId: "wt1" }, [wt1]);
expect(glassLineCount(p3, "mittel")).toBe(1);
expect(glassLineCount(p1, "mittel")).toBe(1);
expect(glassLineCount(p3, "fein")).toBe(2);
expect(glassLineCount(p1, "fein")).toBe(2);
});
it("ohne typeId bleibt die alte Detailgrad-Regel (grob/mittel=1, fein=2) unverändert — Regression", () => {
const p = project({ ...baseWindow });
expect(glassLineCount(p, "grob")).toBe(1);
expect(glassLineCount(p, "mittel")).toBe(1);
expect(glassLineCount(p, "fein")).toBe(2);
});
it("glazingPanes wirkt bei 'grob' NICHT (bleibt bei 1 Glaslinie)", () => {
const wt: WindowType = { ...baseWindowType, glazingPanes: 3 };
const p = project({ ...baseWindow, typeId: "wt1" }, [wt]);
expect(glassLineCount(p, "grob")).toBe(1);
});
});
describe("generatePlan — Stulp-/Flügelstoss-Marken (SIA fig. 37/38, window-stulp)", () => {
const stulpLineCount = (p: Project, detail: "grob" | "mittel" | "fein") =>
linesOfClass(p, "window-stulp", detail).length;
const twoSashType: WindowType = {
...baseWindowType,
sashes: [
{ kind: "fluegel", autoWidth: true, opening: "dreh", hingeSide: "left" },
{ kind: "fluegel", autoWidth: true, opening: "dreh", hingeSide: "right" },
],
};
it("mittel: EIN Stulp-Quadrat (4 Linien) je Flügel-Junction", () => {
const p = project({ ...baseWindow, typeId: "wt1" }, [twoSashType]);
expect(stulpLineCount(p, "mittel")).toBe(4);
});
it("fein: ZWEI Stulp-Quadrate (8 Linien) je Flügel-Junction", () => {
const p = project({ ...baseWindow, typeId: "wt1" }, [twoSashType]);
expect(stulpLineCount(p, "fein")).toBe(8);
});
it("grob: KEINE Stulp-Marken", () => {
const p = project({ ...baseWindow, typeId: "wt1" }, [twoSashType]);
expect(stulpLineCount(p, "grob")).toBe(0);
});
it("Alt-Fall (kein typeId, nur wingCount) erzeugt KEINE Stulp-Marken — keine echte Flügeltabelle", () => {
const p = project({ ...baseWindow, wingCount: 3 });
expect(stulpLineCount(p, "mittel")).toBe(0);
expect(stulpLineCount(p, "fein")).toBe(0);
});
it("einflügliges Fenster (keine Junction) erzeugt KEINE Stulp-Marken", () => {
const p = project({ ...baseWindow, typeId: "wt1" }, [baseWindowType]);
expect(stulpLineCount(p, "mittel")).toBe(0);
expect(stulpLineCount(p, "fein")).toBe(0);
});
});
describe("generatePlan — Rollladenkasten shading (Item 3)", () => {
const shadingLineCount = (p: Project) => linesOfClass(p, "window-shading").length;
it("shading.create: true erzeugt eine zusätzliche Kastenkontur (4 Linien)", () => {
const wt: WindowType = {
...baseWindowType,
shading: { create: true, kind: "rollladen", box: { width: 1.2, height: 0.2, depth: 0.15 } },
};
const p = project({ ...baseWindow, typeId: "wt1" }, [wt]);
expect(shadingLineCount(p)).toBe(4);
});
it("shading.create: false erzeugt KEINE Kastenkontur", () => {
const wt: WindowType = {
...baseWindowType,
shading: { create: false, kind: "rollladen", box: { width: 1.2, height: 0.2, depth: 0.15 } },
};
const p = project({ ...baseWindow, typeId: "wt1" }, [wt]);
expect(shadingLineCount(p)).toBe(0);
});
it("kein shading-Feld am Typ erzeugt KEINE Kastenkontur", () => {
const p = project({ ...baseWindow, typeId: "wt1" }, [baseWindowType]);
expect(shadingLineCount(p)).toBe(0);
});
it("ohne typeId (Alt-Fall) erzeugt KEINE Kastenkontur", () => {
const p = project({ ...baseWindow });
expect(shadingLineCount(p)).toBe(0);
});
});