From 962bfef778582f79d3dfe6aa1a73e8677610ebf5 Mon Sep 17 00:00:00 2001 From: KNSO Date: Sat, 12 Sep 2026 19:43:46 +0200 Subject: [PATCH] =?UTF-8?q?fix(world):=20openRing()=20st=C3=BCrzt=20bei=20?= =?UTF-8?q?leerem=20oder=20fehlendem=20Au=C3=9Fenring=20nicht=20mehr=20ab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/world/geometry.js | 17 +++++++++++++--- tests/geometry.test.js | 44 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/world/geometry.js b/src/world/geometry.js index af69df5..35a8d6d 100644 --- a/src/world/geometry.js +++ b/src/world/geometry.js @@ -20,10 +20,17 @@ function buildingTint(poly) { // ab dem ersten Loch um eins gegenüber dieser internen Nummerierung, und Dreiecke zeigen auf // falsche Punkte. Deshalb hier einmal vorab entfernen, bevor irgendetwas anderes mit dem Ring // passiert — Ringe ohne Schlusspunkt (wie in den Tests) bleiben dabei unverändert. +// +// Ruling 47: Länge zuerst prüfen, dann destrukturieren. Ein Ring mit 0 oder 1 Punkten hat +// keinen von "erstem" verschiedenen "letzten" Punkt, den man entdoppeln könnte — vorher +// stand die Längenprüfung erst im return, sodass ring[0] bei einem leeren Ring bereits +// undefined war und die Destrukturierung warf, statt wie der Rest dieses Moduls zu einem +// harmlosen leeren Ergebnis zu degradieren. const openRing = (ring) => { + if (ring.length <= 1) return ring; const [x0, z0] = ring[0]; const [xn, zn] = ring[ring.length - 1]; - return ring.length > 1 && x0 === xn && z0 === zn ? ring.slice(0, -1) : ring; + return x0 === xn && z0 === zn ? ring.slice(0, -1) : ring; }; const signedArea = (ring) => ring.reduce((sum, [x0, z0], i) => { @@ -47,7 +54,9 @@ function toGeometry(pos, idx, uv, color) { export function flatPolygons(polygons, { y = 0, uvScale = 8 } = {}) { const pos = [], idx = [], uv = []; for (const raw of polygons) { - const outer = clockwise(openRing(raw[0])); + // raw[0] fehlt, wenn die Fläche ganz ohne Ringe übergeben wird (raw = []) — dann + // wie einen leeren Außenring behandeln, statt in openRing() auf undefined zuzugreifen. + const outer = clockwise(openRing(raw[0] ?? [])); // clockwise() gibt denselben Array zurueck, wenn der Ring schon passt — deshalb // vor dem Umdrehen kopieren, sonst wird die Eingabe des Aufrufers veraendert. const holes = raw.slice(1).map((hole) => [...clockwise(openRing(hole))].reverse()); @@ -71,7 +80,9 @@ export function extrudePolygons(polygons, { base = 0, top = 10, uvScale = 4, tin for (const raw of polygons) { // Jede Fläche ist eine Liste von Ringen: Außenring zuerst, danach die Löcher. // triangulateShape erwartet die Löcher gegenläufig zum Außenring. - const outer = clockwise(openRing(raw[0])); + // raw[0] fehlt, wenn die Fläche ganz ohne Ringe übergeben wird (raw = []) — dann + // wie einen leeren Außenring behandeln, statt in openRing() auf undefined zuzugreifen. + const outer = clockwise(openRing(raw[0] ?? [])); // clockwise() gibt denselben Array zurueck, wenn der Ring schon passt — deshalb // vor dem Umdrehen kopieren, sonst wird die Eingabe des Aufrufers veraendert. const holes = raw.slice(1).map((hole) => [...clockwise(openRing(hole))].reverse()); diff --git a/tests/geometry.test.js b/tests/geometry.test.js index d560342..23ac1ea 100644 --- a/tests/geometry.test.js +++ b/tests/geometry.test.js @@ -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]); + }); });