Files
DOSSIER-STANDALONE/src/geometry/stairOutline.test.ts
T
karim 7b9ea8bd07 Bauteile Gruppe A (4-6): Treppe-Referenz, Fenster-Fluegel, Tuer-wandoeffnung
- Treppe referenz links/mitte/rechts (Stair.referenz, Default mitte): Perp-Versatz
  referenzMidShift auf start/center, Lauflinie/Pfeil bleiben visuell zentriert.
- Fenster Fluegel-Mittelpfosten (Opening.wingCount 1-4, Default 1): wingCount-1
  Querlinien im Rahmen (window-mullion), nur mittel/fein.
- Tuer-Typ wandoeffnung (Opening.doorType, Default normal): ueberspringt
  Tuerblatt + Schwenkbogen, Laibung/Sturz bleiben.
- Alle Modellfelder optional/rueckwaertskompatibel. 17 neue Tests.
  vitest 292/292, tsc sauber.
2026-07-05 02:33:48 +02:00

212 lines
6.8 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.
/**
* Unit-Tests für `stairOutline` (Plan-Aussenlinie).
*
* Prüft:
* • Gerade Treppe → 4-Punkte-Rechteck (polygon, keine spiral).
* • L-Treppe → 6-Punkte-L-Polygon via Linien-Schnittpunkten.
* • Wendeltreppe → SpiralOutlineSegments (2 Bögen, 2 radiale Linien, kein polygon).
*
* Die genauen Koordinaten-Werte werden nur grob geprüft (Vorzeichen, Länge,
* Schliessen); die Geometrie-Logik wird via Abstand-Invarianten verifiziert.
*/
import { describe, it, expect } from "vitest";
import { stairOutline, stairGeometry } from "./stair";
import type { Stair, Vec2 } from "../model/types";
/** Einfache Distanz-Funktion. */
const dist = (a: Vec2, b: Vec2) => Math.hypot(b.x - a.x, b.y - a.y);
/** Minimale gerade Treppe nach rechts (+X). */
const geradeStair: Stair = {
id: "g1",
type: "stair",
floorId: "eg",
categoryCode: "40",
shape: "straight",
start: { x: 0, y: 0 },
dir: { x: 1, y: 0 },
runLength: 3.0,
width: 1.2,
stepCount: 16,
};
/** L-Treppe: erster Lauf +X, zweiter Lauf +Y (turn = +1). */
const lStair: Stair = {
id: "l1",
type: "stair",
floorId: "eg",
categoryCode: "40",
shape: "L",
start: { x: 0, y: 0 },
dir: { x: 1, y: 0 },
runLength: 2.0,
run2Length: 2.0,
turn: 1,
width: 1.2,
stepCount: 14,
};
/** Wendeltreppe: 270° im Gegenuhrzeigersinn, Radius 1.5 m. */
const wendelStair: Stair = {
id: "w1",
type: "stair",
floorId: "eg",
categoryCode: "40",
shape: "spiral",
start: { x: 1.5, y: 0 },
dir: { x: 1, y: 0 },
runLength: 3.0, // bei Wendel: Pflichtfeld, inhaltlich nicht genutzt
center: { x: 0, y: 0 },
radius: 1.5,
sweep: 270,
width: 1.2,
stepCount: 12,
};
describe("stairOutline — gerade Treppe", () => {
const totalRise = 3.0;
const result = stairOutline(geradeStair, totalRise);
it("liefert polygon, keine spiral", () => {
expect(result.polygon).not.toBeNull();
expect(result.spiral).toBeNull();
});
it("polygon hat genau 4 Ecken", () => {
expect(result.polygon!.length).toBe(4);
});
it("Lauflaenge entspricht runLength", () => {
// Start-Kante (x=0) vs End-Kante (x=3): Abstand der gleichseitigen Kanten = 3.0 m.
const pts = result.polygon!;
// pts[0] und pts[1] liegen bei x≈0, pts[2] und pts[3] bei x≈runLength.
const startX = (pts[0].x + pts[1].x) / 2;
const endX = (pts[2].x + pts[3].x) / 2;
expect(Math.abs(endX - startX)).toBeCloseTo(geradeStair.runLength, 5);
});
it("Breite entspricht stair.width", () => {
const pts = result.polygon!;
// Seitenkante 0→3 und 1→2 haben die Breite = stair.width.
const w = dist(pts[0], pts[1]);
expect(w).toBeCloseTo(geradeStair.width, 5);
});
});
describe("stairOutline — L-Treppe", () => {
const totalRise = 3.0;
const result = stairOutline(lStair, totalRise);
it("liefert polygon, keine spiral", () => {
expect(result.polygon).not.toBeNull();
expect(result.spiral).toBeNull();
});
it("polygon hat 6 Ecken (L-Form)", () => {
// 4 Aussen-Eck + 2 Innen-Eck via Linien-Schnittpunkten = 6 Punkte.
expect(result.polygon!.length).toBe(6);
});
it("keine NaN-Koordinaten", () => {
for (const p of result.polygon!) {
expect(isFinite(p.x)).toBe(true);
expect(isFinite(p.y)).toBe(true);
}
});
});
describe("stairOutline — Wendeltreppe", () => {
const totalRise = 3.0;
const result = stairOutline(wendelStair, totalRise);
it("liefert spiral, kein polygon", () => {
expect(result.spiral).not.toBeNull();
expect(result.polygon).toBeNull();
});
it("zwei radiale Schliesslinien (Start + Ende)", () => {
expect(result.spiral!.lines.length).toBe(2);
});
it("zwei Kreisboegen (Innen + Aussen)", () => {
expect(result.spiral!.arcs.length).toBe(2);
});
it("Innenradius < Aussenradius", () => {
const [arcIn, arcOut] = result.spiral!.arcs;
expect(arcIn.r).toBeLessThan(arcOut.r);
});
it("Startlinie Innen-Punkt liegt auf Innenkreis", () => {
const arcIn = result.spiral!.arcs[0];
const [lineStart] = result.spiral!.lines;
const [innerPt] = lineStart;
const d = dist({ x: arcIn.cx, y: arcIn.cy }, innerPt);
expect(d).toBeCloseTo(arcIn.r, 4);
});
});
describe("stairOutline — Referenz links/mitte/rechts (Item 4)", () => {
const totalRise = 3.0;
// Gerade Treppe entlang +X, Breite 1.2 m. Perp (linke Normale) = (0, +1).
const base: Stair = { ...geradeStair, referenz: "mitte" };
const mitte = stairOutline(base, totalRise).polygon!;
const links = stairOutline({ ...base, referenz: "links" }, totalRise).polygon!;
const rechts = stairOutline({ ...base, referenz: "rechts" }, totalRise).polygon!;
/** Mittlere Y-Höhe eines Rechteck-Polygons. */
const midY = (pts: Vec2[]) => pts.reduce((s, p) => s + p.y, 0) / pts.length;
it("Breite bleibt bei allen Referenzen gleich", () => {
for (const poly of [mitte, links, rechts]) {
expect(dist(poly[0], poly[1])).toBeCloseTo(geradeStair.width, 5);
}
});
it("referenz=links: Treppe liegt RECHTS der Achse (−n) → Mitte um width/2 nach −Y", () => {
// Achse (start/dir) = linke Kante; die Fläche versetzt sich in −n = (0,−1).
expect(midY(links) - midY(mitte)).toBeCloseTo(-geradeStair.width / 2, 5);
});
it("referenz=rechts: Treppe liegt LINKS der Achse (+n) → Mitte um width/2 nach +Y", () => {
expect(midY(rechts) - midY(mitte)).toBeCloseTo(+geradeStair.width / 2, 5);
});
it("bei referenz=links liegt die Achse (y=0) auf der linken Kante der Treppe", () => {
// Linke Kante = max-Y-Kante der Fläche (n = +Y). Sie soll bei y=0 liegen.
const maxY = Math.max(...links.map((p) => p.y));
expect(maxY).toBeCloseTo(0, 5);
});
it("mitte entspricht dem Default (kein referenz-Feld gesetzt)", () => {
const noRef = stairOutline({ ...geradeStair }, totalRise).polygon!;
for (let i = 0; i < noRef.length; i++) {
expect(noRef[i].x).toBeCloseTo(mitte[i].x, 6);
expect(noRef[i].y).toBeCloseTo(mitte[i].y, 6);
}
});
});
describe("stairGeometry — Lauflinie bleibt visuell zentriert (Item 4)", () => {
const totalRise = 3.0;
const links = stairGeometry({ ...geradeStair, referenz: "links" }, totalRise);
const mitte = stairGeometry({ ...geradeStair, referenz: "mitte" }, totalRise);
it("Lauflinie liegt auf der Treppen-Mitte, nicht auf der Referenzkante (y=0)", () => {
// Bei referenz=links liegt die Mitte bei y = −width/2 = −0.6.
for (const p of links.runLine) {
expect(p.y).toBeCloseTo(-geradeStair.width / 2, 5);
}
// Bei mitte liegt die Lauflinie auf y=0 (Achse = Mitte).
for (const p of mitte.runLine) {
expect(p.y).toBeCloseTo(0, 5);
}
});
it("Tritte sind bei referenz=links um −width/2 versetzt", () => {
const dy = links.treads[0].pts[0].y - mitte.treads[0].pts[0].y;
expect(dy).toBeCloseTo(-geradeStair.width / 2, 5);
});
});