7b9ea8bd07
- 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.
212 lines
6.8 KiB
TypeScript
212 lines
6.8 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|
||
});
|