import { describe, it, expect } from "vitest"; import { roofGeometry, roofBBox } from "./roof"; import type { Roof, Vec2 } from "../model/types"; // Rechteck 6×4 (x:0..6, y:0..4), Neigung 45° (tan=1), Traufhöhe 10, kein Überstand. const RECT: Vec2[] = [ { x: 0, y: 0 }, { x: 6, y: 0 }, { x: 6, y: 4 }, { x: 0, y: 4 }, ]; const E = 10; function roof(over: Partial): Roof { return { id: "R1", type: "roof", floorId: "eg", categoryCode: "35", outline: RECT, shape: "sattel", pitchDeg: 45, overhang: 0, ridgeAxis: "x", thickness: 0.2, ...over, }; } /** Alle 3D-Punkte einer Geometrie (Flächen + Giebel). */ function allZ(g: ReturnType): number[] { const zs: number[] = []; for (const pl of g.planes) for (const p of pl.pts) zs.push(p[2]); for (const gp of g.gables) for (const p of gp) zs.push(p[2]); return zs; } describe("roofBBox", () => { it("liefert die Bounding-Box", () => { expect(roofBBox(RECT)).toEqual({ x0: 0, y0: 0, x1: 6, y1: 4 }); }); }); describe("roofGeometry — Formen (6×4, 45°, First entlang X)", () => { it("flach: eine waagrechte Fläche auf Traufhöhe, kein First", () => { const g = roofGeometry(roof({ shape: "flach" }), E); expect(g.planes).toHaveLength(1); expect(g.ridges).toHaveLength(0); expect(g.ridgeHeight).toBe(0); expect(allZ(g).every((z) => z === E)).toBe(true); }); it("sattel: Firsthöhe = halbe Tiefe · tan, First mittig, 2 Flächen + 2 Giebel", () => { const g = roofGeometry(roof({ shape: "sattel" }), E); expect(g.ridgeHeight).toBeCloseTo(2, 6); // halfD 2 · tan45 1 expect(g.planes).toHaveLength(2); expect(g.gables).toHaveLength(2); expect(g.ridges).toHaveLength(1); // First auf yc=2, z=E+2. expect(g.ridges[0][0]).toEqual({ x: 0, y: 2 }); expect(g.ridges[0][1]).toEqual({ x: 6, y: 2 }); expect(Math.max(...allZ(g))).toBeCloseTo(E + 2, 6); expect(Math.min(...allZ(g))).toBeCloseTo(E, 6); }); it("walm: First um halbe Tiefe verkürzt, 4 Flächen, 4 Grate, keine Giebel", () => { const g = roofGeometry(roof({ shape: "walm" }), E); expect(g.planes).toHaveLength(4); expect(g.hips).toHaveLength(4); expect(g.gables).toHaveLength(0); // ra=2, rb=4. expect(g.ridges[0][0]).toEqual({ x: 2, y: 2 }); expect(g.ridges[0][1]).toEqual({ x: 4, y: 2 }); expect(g.ridgeHeight).toBeCloseTo(2, 6); }); it("zelt: Spitze über der Mitte, 4 Flächen, 4 Grate zur Mitte", () => { const g = roofGeometry(roof({ shape: "zelt" }), E); expect(g.planes).toHaveLength(4); expect(g.hips).toHaveLength(4); expect(g.ridges).toHaveLength(0); // hz = min(halfW 3, halfD 2)=2. expect(g.ridgeHeight).toBeCloseTo(2, 6); // Alle Grate laufen zum Mittelpunkt (3,2). for (const hip of g.hips) expect(hip[1]).toEqual({ x: 3, y: 2 }); }); it("pult: eine Fläche, hohe Kante = Breite·tan über der Traufe", () => { const g = roofGeometry(roof({ shape: "pult" }), E); expect(g.planes).toHaveLength(1); expect(g.ridgeHeight).toBeCloseTo(4, 6); // Tiefe 4 · tan45 expect(Math.max(...allZ(g))).toBeCloseTo(E + 4, 6); }); it("mansarde: 4 Flächen, 2 Knicklinien, First mittig, Knick tiefer als First", () => { const g = roofGeometry(roof({ shape: "mansarde", pitchDeg: 70, pitchUpperDeg: 30 }), E); expect(g.planes).toHaveLength(4); expect(g.breaks).toHaveLength(2); expect(g.ridges).toHaveLength(1); expect(g.gables).toHaveLength(2); // Pentagon-Giebel // First höher als 0, Knicklinien innerhalb der Tiefe. expect(g.ridgeHeight).toBeGreaterThan(0); expect(g.breaks[0][0].y).toBeCloseTo(0 + 2 * 0.4, 6); // yf = y0 + halfD·0.4 }); it("mansarde giebel: Knick-Ratio parametriert die Knicklinien-Lage", () => { const g = roofGeometry( roof({ shape: "mansarde", mansardType: "giebel", mansardKneeRatio: 0.25 }), E, ); // yf = y0 + halfD·ratio = 0 + 2·0.25 = 0.5. expect(g.breaks[0][0].y).toBeCloseTo(0.5, 6); }); it("mansarde walm: allseitig geknickt -> 8 Flächen, 4 Knicklinien (Rechteck), 8 Grate, keine Giebel", () => { const g = roofGeometry(roof({ shape: "mansarde", mansardType: "walm" }), E); expect(g.planes).toHaveLength(8); // 4 Sockel + 2 Walm-Trapeze + 2 Walm-Dreiecke expect(g.breaks).toHaveLength(4); // inneres Knick-Rechteck expect(g.hips).toHaveLength(8); // 4 Sockel-Grate + 4 obere Walm-Grate expect(g.ridges).toHaveLength(1); expect(g.gables).toHaveLength(0); // Walm hat keine Giebel expect(g.ridgeHeight).toBeGreaterThan(0); // Knick-Höhe < First-Höhe (Sockel endet unter dem First). const kneeZ = g.planes[0].pts[2][2] - E; // z1 - e am inneren Punkt expect(kneeZ).toBeGreaterThan(0); expect(kneeZ).toBeLessThan(g.ridgeHeight); }); it("mansarde zelt: allseitig geknickt zur Spitze -> 8 Flächen, kein First, 8 Grate", () => { const g = roofGeometry(roof({ shape: "mansarde", mansardType: "zelt" }), E); expect(g.planes).toHaveLength(8); // 4 Sockel + 4 Spitzen-Dreiecke expect(g.breaks).toHaveLength(4); expect(g.hips).toHaveLength(8); expect(g.ridges).toHaveLength(0); // Zelt hat keinen First expect(g.gables).toHaveLength(0); expect(g.ridgeHeight).toBeGreaterThan(0); }); }); describe("roofGeometry — First entlang Y (Transponierung)", () => { it("dreht die Sattel-First-Linie auf die X-Mitte", () => { const g = roofGeometry(roof({ shape: "sattel", ridgeAxis: "y" }), E); // Firstrichtung Y → First liegt bei x = xc = 3, läuft in Y. expect(g.ridges[0][0]).toEqual({ x: 3, y: 0 }); expect(g.ridges[0][1]).toEqual({ x: 3, y: 4 }); // Firsthöhe nun aus der halben BREITE (3) · tan = 3. expect(g.ridgeHeight).toBeCloseTo(3, 6); }); }); describe("roofGeometry — Überstand", () => { it("weitet die Traufe um overhang nach aussen", () => { const g = roofGeometry(roof({ shape: "flach", overhang: 0.5 }), E); const bb = roofBBox(g.eaves); expect(bb).toEqual({ x0: -0.5, y0: -0.5, x1: 6.5, y1: 4.5 }); }); it("getrennter Traufe/Ortgang-Überstand: First entlang X -> Ortgang weitet X, Traufe Y", () => { // ridgeAxis "x": First entlang X -> Ortgang (Giebel) an den X-Enden, // Traufe an den Y-Seiten. overhang(Traufe)=0.3, overhangGable(Ortgang)=0.8. const g = roofGeometry( roof({ shape: "sattel", ridgeAxis: "x", overhang: 0.3, overhangGable: 0.8 }), E, ); const bb = roofBBox(g.eaves); expect(bb).toEqual({ x0: -0.8, y0: -0.3, x1: 6.8, y1: 4.3 }); }); it("getrennter Überstand: First entlang Y -> Ortgang weitet Y, Traufe X", () => { const g = roofGeometry( roof({ shape: "sattel", ridgeAxis: "y", overhang: 0.3, overhangGable: 0.8 }), E, ); const bb = roofBBox(g.eaves); expect(bb).toEqual({ x0: -0.3, y0: -0.8, x1: 6.3, y1: 4.8 }); }); it("overhangGable fehlt -> ringsum gleich overhang (rückwärtskompatibel)", () => { const g = roofGeometry(roof({ shape: "sattel", overhang: 0.4 }), E); const bb = roofBBox(g.eaves); expect(bb).toEqual({ x0: -0.4, y0: -0.4, x1: 6.4, y1: 4.4 }); }); });