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>
150 lines
7.5 KiB
JavaScript
150 lines
7.5 KiB
JavaScript
import { BufferGeometry, Float32BufferAttribute, ShapeUtils, Vector2 } from "three";
|
|
|
|
const GROUND_THICKNESS = 1.0;
|
|
const SURFACE_OFFSET = 0.02; // Sand, Park und Stadtfläche liegen knapp über dem Boden
|
|
const ROAD_OFFSET = 0.03; // Straßen wiederum knapp über den Flächen
|
|
const BRIDGE_THICKNESS = 0.6;
|
|
export const SURFACE_KINDS = ["sand", "park", "urban"];
|
|
|
|
/** Pastellton je Gebäude, reproduzierbar aus seiner Lage. */
|
|
function buildingTint(poly) {
|
|
const [x, z] = poly[0][0];
|
|
const h = Math.abs(Math.sin(x * 12.9898 + z * 78.233) * 43758.5453) % 1;
|
|
return [0.82 + 0.18 * h, 0.80 + 0.20 * ((h * 3) % 1), 0.78 + 0.22 * ((h * 7) % 1)];
|
|
}
|
|
|
|
// Die extrahierten Ringe schließen sich selbst: der letzte Punkt wiederholt den ersten
|
|
// (geprüft über alle 144 Chunks — ausnahmslos). ShapeUtils.triangulateShape entfernt diesen
|
|
// Schlusspunkt intern über Vector2#equals und zählt seine Indizes danach neu durch. Bauen wir
|
|
// die Positionsliste dagegen aus dem rohen, geschlossenen Ring auf, verschiebt sich jeder Index
|
|
// 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 x0 === xn && z0 === zn ? ring.slice(0, -1) : ring;
|
|
};
|
|
|
|
const signedArea = (ring) => ring.reduce((sum, [x0, z0], i) => {
|
|
const [x1, z1] = ring[(i + 1) % ring.length];
|
|
return sum + x0 * z1 - x1 * z0;
|
|
}, 0) / 2;
|
|
|
|
/** Ringe im Uhrzeigersinn, damit Wände nach außen zeigen. */
|
|
const clockwise = (ring) => (signedArea(ring) > 0 ? [...ring].reverse() : ring);
|
|
|
|
function toGeometry(pos, idx, uv, color) {
|
|
const geo = new BufferGeometry();
|
|
geo.setAttribute("position", new Float32BufferAttribute(pos, 3));
|
|
geo.setAttribute("uv", new Float32BufferAttribute(uv, 2));
|
|
if (color) geo.setAttribute("color", new Float32BufferAttribute(color, 3));
|
|
geo.setIndex(idx);
|
|
geo.computeVertexNormals();
|
|
return geo;
|
|
}
|
|
|
|
export function flatPolygons(polygons, { y = 0, uvScale = 8 } = {}) {
|
|
const pos = [], idx = [], uv = [];
|
|
for (const raw of polygons) {
|
|
// 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());
|
|
const base = pos.length / 3;
|
|
for (const ring of [outer, ...holes]) {
|
|
for (const [x, z] of ring) {
|
|
pos.push(x, y, z);
|
|
uv.push(x / uvScale, z / uvScale);
|
|
}
|
|
}
|
|
const toPoints = (ring) => ring.map(([x, z]) => new Vector2(x, z));
|
|
for (const tri of ShapeUtils.triangulateShape(toPoints(outer), holes.map(toPoints))) {
|
|
idx.push(base + tri[0], base + tri[2], base + tri[1]); // nach oben gedreht
|
|
}
|
|
}
|
|
return pos.length ? toGeometry(pos, idx, uv) : null;
|
|
}
|
|
|
|
export function extrudePolygons(polygons, { base = 0, top = 10, uvScale = 4, tint = null } = {}) {
|
|
const pos = [], idx = [], uv = [], color = [];
|
|
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.
|
|
// 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());
|
|
for (const ring of [outer, ...holes]) {
|
|
const n = ring.length;
|
|
for (let i = 0; i < n; i++) {
|
|
const [x0, z0] = ring[i];
|
|
const [x1, z1] = ring[(i + 1) % n];
|
|
const len = Math.hypot(x1 - x0, z1 - z0);
|
|
const b = pos.length / 3;
|
|
pos.push(x0, base, z0, x1, base, z1, x1, top, z1, x0, top, z0);
|
|
uv.push(0, 0, len / uvScale, 0, len / uvScale, (top - base) / uvScale, 0, (top - base) / uvScale);
|
|
if (tint) for (let k = 0; k < 4; k++) color.push(...tint);
|
|
idx.push(b, b + 1, b + 2, b, b + 2, b + 3);
|
|
}
|
|
}
|
|
// Deckfläche: erst alle Punkte in derselben Reihenfolge, die triangulateShape indiziert.
|
|
const b = pos.length / 3;
|
|
for (const ring of [outer, ...holes]) {
|
|
for (const [x, z] of ring) {
|
|
pos.push(x, top, z);
|
|
uv.push(x / uvScale, z / uvScale);
|
|
if (tint) color.push(...tint);
|
|
}
|
|
}
|
|
const toPoints = (ring) => ring.map(([x, z]) => new Vector2(x, z));
|
|
for (const tri of ShapeUtils.triangulateShape(toPoints(outer), holes.map(toPoints))) {
|
|
idx.push(b + tri[0], b + tri[2], b + tri[1]);
|
|
}
|
|
}
|
|
return pos.length ? toGeometry(pos, idx, uv, tint ? color : null) : null;
|
|
}
|
|
|
|
export function buildChunkGeometry(chunk) {
|
|
const roads = chunk.roads.filter((r) => !r.bridge).map((r) => r.poly);
|
|
const bridges = chunk.roads.filter((r) => r.bridge).map((r) => r.poly);
|
|
const out = {
|
|
ground: extrudePolygons(chunk.land, { base: -GROUND_THICKNESS, top: 0, uvScale: 8 }),
|
|
roads: flatPolygons(roads, { y: ROAD_OFFSET, uvScale: 8 }),
|
|
bridges: extrudePolygons(bridges, { base: -BRIDGE_THICKNESS, top: 0, uvScale: 8 }),
|
|
buildings: buildBuildings(chunk.buildings),
|
|
};
|
|
for (const kind of SURFACE_KINDS) {
|
|
const rings = (chunk.surfaces ?? []).filter((s) => s.kind === kind).map((s) => s.poly);
|
|
out[kind] = flatPolygons(rings, { y: SURFACE_OFFSET, uvScale: 8 });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function buildBuildings(buildings) {
|
|
const parts = buildings.map((b) =>
|
|
extrudePolygons([b.poly], { base: 0, top: b.height, uvScale: 4, tint: buildingTint(b.poly) }));
|
|
return parts.filter(Boolean).reduce((acc, geo) => (acc ? mergeInto(acc, geo) : geo), null);
|
|
}
|
|
|
|
function mergeInto(a, b) {
|
|
const offset = a.getAttribute("position").count;
|
|
const pos = [...a.getAttribute("position").array, ...b.getAttribute("position").array];
|
|
const uv = [...a.getAttribute("uv").array, ...b.getAttribute("uv").array];
|
|
const ca = a.getAttribute("color"), cb = b.getAttribute("color");
|
|
const color = ca && cb ? [...ca.array, ...cb.array] : null;
|
|
const idx = [...a.getIndex().array, ...[...b.getIndex().array].map((i) => i + offset)];
|
|
return toGeometry(pos, idx, uv, color);
|
|
}
|