import { describe, expect, it } from "vitest"; import { lampPoints, palmTransforms, pointInPolygon, vergeLampPoints } from "../src/world/props.js"; describe("props", () => { it("setzt Laternen im gewünschten Abstand entlang der Linie", () => { const pts = lampPoints([[[0, 0], [100, 0]]], 35); expect(pts.length).toBe(3); // 17,5 / 52,5 / 87,5 Meter expect(pts[0].x).toBeCloseTo(17.5); // nicht auf der Kreuzung expect(pts[1].x).toBeCloseTo(52.5); }); it("setzt auf einer kurzen Linie eine Laterne in die Mitte", () => { const pts = lampPoints([[[0, 0], [10, 0]]], 35); expect(pts).toHaveLength(1); expect(pts[0].x).toBeCloseTo(5); }); it("stapelt an einer Kreuzung keine Laternen", () => { // Zwei gegenüberliegende Äste, wie centerlines() sie seit Task 4 liefert. const pts = lampPoints([[[0, 0], [60, 0]], [[0, 0], [-60, 0]]], 35); const keys = new Set(pts.map((p) => `${p.x.toFixed(2)},${p.z.toFixed(2)}`)); expect(keys.size).toBe(pts.length); }); it("skaliert Palmen reproduzierbar zwischen 10 und 18 Metern", () => { const a = palmTransforms([[5, 5], [9, 9]], 7); const b = palmTransforms([[5, 5], [9, 9]], 7); expect(a).toEqual(b); for (const t of a) { expect(t.height).toBeGreaterThanOrEqual(10); expect(t.height).toBeLessThanOrEqual(18); } }); }); // Fix-Runde 1, Befund A / Ruling 60: Laternen standen exakt auf der Mittellinie. describe("pointInPolygon", () => { // Quadrat 0..20 mit einem 4x4-Loch in der Mitte (8..12) — offene Ringe, wie im Rest // dieser Datei; die Laufzeitdaten schließen ihre Ringe (letzter Punkt = erster), das // Halbstrahl-Verfahren behandelt beide Formen gleich (siehe Kommentar in props.js). const outer = [[0, 0], [20, 0], [20, 20], [0, 20]]; const hole = [[8, 8], [12, 8], [12, 12], [8, 12]]; it("Punkt innen (im Außenring, außerhalb jedes Lochs)", () => { expect(pointInPolygon(2, 2, [outer, hole])).toBe(true); }); it("Punkt außen (außerhalb des Außenrings)", () => { expect(pointInPolygon(25, 25, [outer, hole])).toBe(false); }); it("Punkt im Loch (im Außenring, aber auch im Loch)", () => { expect(pointInPolygon(10, 10, [outer, hole])).toBe(false); }); }); describe("vergeLampPoints", () => { it("versetzt eine Laterne von der Fahrbahn zum ersten freien Abstand, senkrecht zur Linie", () => { // angle 0 -> Blickrichtung (1, 0), Normale (0, 1). Straße 10 m breit (Halbbreite 5 m): // der erste Versatz aus LAMP_VERGE (6 m) reicht bereits über den Fahrbahnrand hinaus. const points = [{ x: 0, z: 0, angle: 0 }]; const roads = [{ poly: [[[-50, -5], [50, -5], [50, 5], [-50, 5]]], bridge: false }]; const out = vergeLampPoints(points, roads); expect(out).toHaveLength(1); expect(out[0].x).toBeCloseTo(0); expect(out[0].z).toBeCloseTo(6); expect(pointInPolygon(out[0].x, out[0].z, roads[0].poly)).toBe(false); }); it("lässt eine Laterne entfallen, wenn keiner der sechs Abstände die Fahrbahn verlässt", () => { // Halbbreite 20 m > größter Versatz (16 m) -> jede Probe bleibt auf der Fahrbahn. const points = [{ x: 0, z: 0, angle: 0 }]; const roads = [{ poly: [[[-50, -20], [50, -20], [50, 20], [-50, 20]]], bridge: false }]; expect(vergeLampPoints(points, roads)).toHaveLength(0); }); it("wechselt die Versatzseite von Punkt zu Punkt ab", () => { // Ohne Straßen gewinnt sofort der erste (kleinste) Versatz bei jedem Punkt. const points = [{ x: 0, z: 0, angle: 0 }, { x: 10, z: 0, angle: 0 }]; const out = vergeLampPoints(points, []); expect(out[0].z).toBeCloseTo(6); expect(out[1].z).toBeCloseTo(-6); }); });