fix(world): openRing() stürzt bei leerem oder fehlendem Außenring nicht mehr ab

Ruling 47 (Fix-Runde 1, Befund A): Die Längenprüfung stand erst im return,
destrukturiert wurde davor. extrudePolygons/flatPolygons warfen dadurch bei
einem Polygon mit leerem Außenring ([[]]) oder ganz ohne Ringe ([]), statt wie
der Rest des Moduls zu null zu degradieren. Längenprüfung nach vorn gezogen
und zusätzlich raw[0] am Aufrufort auf [] defaultet, da ein Polygon ganz ohne
Ringe openRing() ein undefined statt eines leeren Rings übergibt — die
Prüfung allein in openRing() fängt diesen zweiten Fall nicht ab.

Dazu Ruling 49: Regressionstest für die Wickelrichtung (Außenwände nach
außen, Lochwände nach innen, Dach nach oben) anhand von COURTYARD. Bestand
beim ersten Lauf unverändert, keine Anpassung am Modul nötig.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-12 19:43:46 +02:00
1 parent 4c82b14a57
commit 962bfef778
2 files changed
+58 -3

No files matched your search

+44
View File
@@ -108,4 +108,48 @@ describe("geometry", () => {
const geo = flatPolygons([COURTYARD_CLOSED], { y: 0, uvScale: 4 });
expect(footprintArea(geo)).toBeCloseTo(800, 6);
});
// Fix-Runde 1, Befund A / Ruling 47: ein Polygon mit leerem Außenring oder ganz ohne
// Ringe muss wie eine leere Fläche zu null degradieren, nicht werfen.
it("gibt bei einem Polygon mit leerem Außenring oder ganz ohne Ringe null zurück, statt zu werfen", () => {
expect(extrudePolygons([[[]]], { base: 0, top: 10, uvScale: 4 })).toBeNull();
expect(extrudePolygons([[]], { base: 0, top: 10, uvScale: 4 })).toBeNull();
expect(flatPolygons([[[]]], { y: 0, uvScale: 4 })).toBeNull();
expect(flatPolygons([[]], { y: 0, uvScale: 4 })).toBeNull();
});
// Fix-Runde 1, Befund B / Ruling 49.
it("wendet Außenwände nach außen, Lochwände nach innen und Deckflächen nach oben", () => {
const geo = extrudePolygons([COURTYARD], { base: 0, top: 10, uvScale: 4 });
const pos = geo.getAttribute("position"), idx = geo.getIndex();
const MITTE = 15; // Außenring 30x30 und Loch 10x10 teilen den Mittelpunkt
let deckel = 0, aussen = 0, innen = 0;
for (let t = 0; t < idx.count; t += 3) {
const p = [0, 1, 2].map((k) => {
const i = idx.getX(t + k);
return [pos.getX(i), pos.getY(i), pos.getZ(i)];
});
const u = [0, 1, 2].map((a) => p[1][a] - p[0][a]);
const v = [0, 1, 2].map((a) => p[2][a] - p[0][a]);
const n = [u[1] * v[2] - u[2] * v[1],
u[2] * v[0] - u[0] * v[2],
u[0] * v[1] - u[1] * v[0]];
const mid = [0, 1, 2].map((a) => (p[0][a] + p[1][a] + p[2][a]) / 3);
if (Math.abs(n[0]) < 1e-6 && Math.abs(n[2]) < 1e-6) {
expect(n[1]).toBeGreaterThan(0); // Deckfläche nach oben
deckel++;
continue;
}
const radial = [mid[0] - MITTE, mid[2] - MITTE];
const dot = n[0] * radial[0] + n[2] * radial[1];
if (Math.max(Math.abs(radial[0]), Math.abs(radial[1])) > 10) {
expect(dot).toBeGreaterThan(0); // Außenwand zeigt vom Mittelpunkt weg
aussen++;
} else {
expect(dot).toBeLessThan(0); // Lochwand zeigt in den Hof hinein
innen++;
}
}
expect([deckel, aussen, innen]).toEqual([8, 8, 8]);
});
});