Ersetzt den zu schwachen Leerlisten-Test für sidewalkLampPoints() durch einen Fall mit echter, aber für LAMP_VERGE zu weit entfernter Gehwegfläche, und belegt in materials.test.js, dass applyPolygonOffsets() den eigenen markings-Versatz tatsächlich in ein Materialobjekt durchreicht. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
171 lines
8.6 KiB
JavaScript
171 lines
8.6 KiB
JavaScript
import { describe, expect, it } from "vitest";
|
|
import { STREET_LAMP_HEIGHT, chunkLampPoints, lampFit, lampPoints, palmTransforms, pointInPolygon,
|
|
sidewalkLampPoints } 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);
|
|
});
|
|
|
|
// Fix-Runde 2, Befund B / Ruling 63: bis hierher benutzte jede Vorrichtung offene Ringe;
|
|
// die echten Chunk-Daten schließen ihre Ringe ausnahmslos (letzter Punkt = erster, siehe
|
|
// Kommentar oben und in props.js) — dieser Fall stand bisher in keinem Test. Ein künftiger
|
|
// Umbau, der das Schließen kaputt macht, ginge sonst durch alle Tests.
|
|
it("behandelt geschlossene Ringe genauso wie offene (Laufzeitdaten schließen ihre Ringe)", () => {
|
|
const closedOuter = [...outer, outer[0]];
|
|
const closedHole = [...hole, hole[0]];
|
|
expect(pointInPolygon(2, 2, [closedOuter, closedHole])).toBe(true);
|
|
expect(pointInPolygon(25, 25, [closedOuter, closedHole])).toBe(false);
|
|
expect(pointInPolygon(10, 10, [closedOuter, closedHole])).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("Laternen auf dem Gehweg", () => {
|
|
// Straße entlang der x-Achse, angle 0: Die Normale ist (0, 1). Der Gehweg liegt nur auf der Seite
|
|
// z > 0, 3,5 bis 6 m neben der Mittellinie — wie bei einer Wohnstraße mit 7 m Fahrbahn.
|
|
const gehweg = [{ poly: [[[-20, 3.5], [20, 3.5], [20, 6], [-20, 6]]] }];
|
|
|
|
it("stellt die Laterne auf den Gehweg, so dicht an die Bordsteinkante wie möglich", () => {
|
|
const out = sidewalkLampPoints([{ x: 0, z: 0, angle: 0 }], gehweg);
|
|
expect(out).toHaveLength(1);
|
|
expect(out[0].x).toBeCloseTo(0);
|
|
expect(out[0].z).toBeCloseTo(4); // der erste Abstand aus LAMP_VERGE, der auf dem Gehweg liegt
|
|
});
|
|
|
|
it("sucht auch auf der anderen Straßenseite", () => {
|
|
const gegenueber = [{ poly: [[[-20, -6], [20, -6], [20, -3.5], [-20, -3.5]]] }];
|
|
const out = sidewalkLampPoints([{ x: 0, z: 0, angle: 0 }], gegenueber);
|
|
expect(out).toHaveLength(1);
|
|
expect(out[0].z).toBeCloseTo(-4);
|
|
});
|
|
|
|
it("lässt eine Laterne weg, für die es keinen Gehweg gibt", () => {
|
|
expect(sidewalkLampPoints([{ x: 0, z: 0, angle: 0 }], [])).toEqual([]);
|
|
});
|
|
|
|
it("lässt eine Laterne entfallen, wenn der Gehweg außerhalb aller Abstände liegt", () => {
|
|
// Der Gehweg beginnt erst bei 30 m, LAMP_VERGE deckt aber höchstens 12 m ab.
|
|
const fern = [{ poly: [[[-20, 30], [20, 30], [20, 33], [-20, 33]]] }];
|
|
expect(sidewalkLampPoints([{ x: 0, z: 0, angle: 0 }], fern)).toEqual([]);
|
|
});
|
|
|
|
it("hängt nicht an der Listenposition", () => {
|
|
// Die alte Fassung wählte die Seite nach dem Index: Der zweite Punkt hätte auf der Seite ohne
|
|
// Gehweg gesucht und wäre entfallen.
|
|
const punkte = [{ x: -10, z: 0, angle: 0 }, { x: 10, z: 0, angle: 0 }];
|
|
const vorwaerts = sidewalkLampPoints(punkte, gehweg);
|
|
const rueckwaerts = sidewalkLampPoints([...punkte].reverse(), gehweg);
|
|
expect(vorwaerts).toHaveLength(2);
|
|
const xs = (out) => out.map((p) => p.x).sort((a, b) => a - b);
|
|
expect(xs(vorwaerts)).toEqual(xs(rueckwaerts));
|
|
});
|
|
|
|
it("findet den Gehweg auch, wenn sein Ring geschlossen ist (so liegen die Chunk-Dateien vor)", () => {
|
|
const ring = [[-20, 3.5], [20, 3.5], [20, 6], [-20, 6]];
|
|
expect(sidewalkLampPoints([{ x: 0, z: 0, angle: 0 }], [{ poly: [[...ring, ring[0]]] }])).toHaveLength(1);
|
|
});
|
|
|
|
it("setzt aus einem Chunk im Format von Teil 2 Laternen auf den Gehweg", () => {
|
|
// Wohnstraße entlang z = 50, Gehweg auf beiden Seiten; lampPoints() setzt auf 100 m drei Punkte.
|
|
const chunk = {
|
|
chunk: [0, 0], origin: [0, 0], land: [], surfaces: [], buildings: [], palms: [],
|
|
carriageway: [], curb: [], markings: [], lots: [], bridges: [],
|
|
edges: [{ line: [[0, 50], [100, 50]], klasse: "wohnstrasse", breite: 12, fahrbahn: 7 }],
|
|
sidewalk: [{ poly: [[[0, 53.5], [100, 53.5], [100, 56], [0, 56]]] },
|
|
{ poly: [[[0, 44], [100, 44], [100, 46.5], [0, 46.5]]] }],
|
|
};
|
|
const out = chunkLampPoints(chunk);
|
|
expect(out).toHaveLength(3);
|
|
for (const p of out) expect(chunk.sidewalk.some((s) => pointInPolygon(p.x, p.z, s.poly))).toBe(true);
|
|
});
|
|
});
|
|
|
|
// Fix-Runde 3, Befund B: street_lamp_02.glb ist mit `npx @gltf-transform/cli@4.5.0 inspect`
|
|
// vermessen bboxMin.y=-0.39258 / bboxMax.y=1.26478 (Modellhöhe 1,65736 m) — der Fuß liegt
|
|
// unter dem Ursprung, #buildLamps setzte das Modell bisher unskaliert auf y=0, der Fuß steckt
|
|
// also im Boden, und 1,66 m ist ohnehin zu klein für eine Straßenlaterne (s. Bericht).
|
|
// lampFit() ist rein: sie bekommt die gemessene bbox und die Zielhöhe hinein, keine
|
|
// Seiteneffekte, kein GLTFLoader — PropLayer.load() ist der einzige Aufrufer.
|
|
describe("lampFit", () => {
|
|
const BBOX_MIN_Y = -0.39258;
|
|
const BBOX_MAX_Y = 1.26478;
|
|
|
|
it("skaliert die gemessene Modellhöhe exakt auf die Zielhöhe", () => {
|
|
const { scale } = lampFit(BBOX_MIN_Y, BBOX_MAX_Y, STREET_LAMP_HEIGHT);
|
|
const modelHeight = BBOX_MAX_Y - BBOX_MIN_Y;
|
|
expect(scale * modelHeight).toBeCloseTo(STREET_LAMP_HEIGHT, 5);
|
|
});
|
|
|
|
it("hebt das skalierte Modell so an, dass sein Fuß (bboxMin.y) auf y = 0 zu stehen kommt", () => {
|
|
const { scale, liftY } = lampFit(BBOX_MIN_Y, BBOX_MAX_Y, STREET_LAMP_HEIGHT);
|
|
expect(BBOX_MIN_Y * scale + liftY).toBeCloseTo(0, 5);
|
|
});
|
|
|
|
it("STREET_LAMP_HEIGHT ist eine glaubwürdige Straßenlaternenhöhe, keine 1,66 m", () => {
|
|
// 3-6 m ist der in Quellen genannte Bereich für dekorative/ornamentale Straßen- und
|
|
// Parklaternen (s. Bericht); das Modell selbst ist mit 1,66 m unstrittig zu klein.
|
|
expect(STREET_LAMP_HEIGHT).toBeGreaterThanOrEqual(3);
|
|
expect(STREET_LAMP_HEIGHT).toBeLessThanOrEqual(6);
|
|
});
|
|
|
|
it("leitet den Skalierungsfaktor aus der übergebenen Zielhöhe her, statt eine Zahl zu raten", () => {
|
|
expect(lampFit(0, 2, 4).scale).toBeCloseTo(2);
|
|
expect(lampFit(0, 4, 4).scale).toBeCloseTo(1);
|
|
expect(lampFit(0, 1, 4).scale).toBeCloseTo(4);
|
|
});
|
|
|
|
it("liefert scale 1 und hebt nur um |bboxMin.y| an, wenn keine Zielhöhe übergeben wird und die Modellhöhe 0 ist (Randfall)", () => {
|
|
// Entartete bbox (maxY === minY) darf nicht durch 0 teilen.
|
|
const { scale, liftY } = lampFit(2, 2, STREET_LAMP_HEIGHT);
|
|
expect(scale).toBe(1);
|
|
expect(liftY).toBeCloseTo(-2);
|
|
});
|
|
});
|