Bauteile Gruppe A: Treppe-Aussenlinie, Fenster-Bruestung, Tuer-Sturz (Rhino-Vorbild)
- Treppe Aussenlinie/Outline (stair.ts stairOutline): gerade=4-Punkt-Rechteck, L=6-Punkt-Polygon via lineIntersect2d, Wendel=2 Boegen+2 Radiallinien; in addStairSymbol als durchgezogene Umrisslinie. (Rhino _aussen_gerade/_l/_wendel) - Fenster Bruestungslinie: bei sillHeight>0 gepunktete Linie auf der Wandachse zwischen den Pfosten (Klasse window-sill). - Tuer Sturzlinien (SIA): gestrichelte Linien an Wand-Innen/-Aussenkante, neues optionales Opening-Feld lintelLines (keine/innen/aussen/beide, Default beide), rueckwaertskompatibel. - 12 neue Tests (stairOutline.test.ts). vitest 275/275, tsc sauber.
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 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 } 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user