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:
1 parent
4c82b14a57
commit
962bfef778
2 files changed
+58
-3
No files matched your search
+14
-3
@@ -20,10 +20,17 @@ function buildingTint(poly) {
|
|||||||
// ab dem ersten Loch um eins gegenüber dieser internen Nummerierung, und Dreiecke zeigen auf
|
// 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
|
// 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.
|
// 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) => {
|
const openRing = (ring) => {
|
||||||
|
if (ring.length <= 1) return ring;
|
||||||
const [x0, z0] = ring[0];
|
const [x0, z0] = ring[0];
|
||||||
const [xn, zn] = ring[ring.length - 1];
|
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) => {
|
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 } = {}) {
|
export function flatPolygons(polygons, { y = 0, uvScale = 8 } = {}) {
|
||||||
const pos = [], idx = [], uv = [];
|
const pos = [], idx = [], uv = [];
|
||||||
for (const raw of polygons) {
|
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
|
// clockwise() gibt denselben Array zurueck, wenn der Ring schon passt — deshalb
|
||||||
// vor dem Umdrehen kopieren, sonst wird die Eingabe des Aufrufers veraendert.
|
// vor dem Umdrehen kopieren, sonst wird die Eingabe des Aufrufers veraendert.
|
||||||
const holes = raw.slice(1).map((hole) => [...clockwise(openRing(hole))].reverse());
|
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) {
|
for (const raw of polygons) {
|
||||||
// Jede Fläche ist eine Liste von Ringen: Außenring zuerst, danach die Löcher.
|
// 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.
|
// 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
|
// clockwise() gibt denselben Array zurueck, wenn der Ring schon passt — deshalb
|
||||||
// vor dem Umdrehen kopieren, sonst wird die Eingabe des Aufrufers veraendert.
|
// vor dem Umdrehen kopieren, sonst wird die Eingabe des Aufrufers veraendert.
|
||||||
const holes = raw.slice(1).map((hole) => [...clockwise(openRing(hole))].reverse());
|
const holes = raw.slice(1).map((hole) => [...clockwise(openRing(hole))].reverse());
|
||||||
|
|||||||
@@ -108,4 +108,48 @@ describe("geometry", () => {
|
|||||||
const geo = flatPolygons([COURTYARD_CLOSED], { y: 0, uvScale: 4 });
|
const geo = flatPolygons([COURTYARD_CLOSED], { y: 0, uvScale: 4 });
|
||||||
expect(footprintArea(geo)).toBeCloseTo(800, 6);
|
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]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user