feat(world): Geometrie-Baustein für Land, Straßen, Brücken und Gebäude
Normalisiert Ringe vor der Triangulierung: ShapeUtils.triangulateShape
verlangt echte Vector2 (nicht {x,y}-Objekte) wegen des internen
Vector2#equals-Aufrufs, und jeder extrahierte Ring schließt sich selbst
(letzter Punkt == erster). Ohne vorheriges Entfernen dieses Schlusspunkts
verschieben sich ab dem ersten Loch alle Dreiecksindizes um eins gegenüber
der von ShapeUtils intern neu vergebenen Nummerierung — Häuser und Straßen
mit Innenhof bekämen an genau der Stelle falsch verdrahtete Dreiecke, ohne
dass die Dreieckszahl sich ändert. Beide Fehler traten mit den Testquadraten
aus dem Task-Brief nicht auf, weil deren Ringe offen sind; die echten
Chunk-Dateien schließen aber ausnahmslos jeden Ring.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
70c35faf86
commit
dce90a2263
3 files changed
+265
No files matched your search
@@ -0,0 +1,111 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Box3, BufferGeometry } from "three";
|
||||
import { buildChunkGeometry, extrudePolygons, flatPolygons } from "../src/world/geometry.js";
|
||||
|
||||
const SQUARE = [[0, 0], [10, 0], [10, 10], [0, 10]];
|
||||
const PLOT = [SQUARE]; // Fläche ohne Loch: nur der Außenring
|
||||
const COURTYARD = [[[0, 0], [30, 0], [30, 30], [0, 30]],
|
||||
[[10, 10], [20, 10], [20, 20], [10, 20]]]; // Block mit Innenhof
|
||||
|
||||
// Die echten Chunk-Dateien (public/data/vice-a/*.json) schließen jeden Ring selbst: der
|
||||
// letzte Punkt wiederholt den ersten. PLOT/COURTYARD oben tun das nicht. Beide Schreibweisen
|
||||
// beschreiben dasselbe Polygon und müssen dieselbe Geometrie ergeben.
|
||||
const PLOT_CLOSED = [[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]];
|
||||
const COURTYARD_CLOSED = [[[0, 0], [30, 0], [30, 30], [0, 30], [0, 0]],
|
||||
[[10, 10], [20, 10], [20, 20], [10, 20], [10, 10]]];
|
||||
|
||||
/** Fläche der (x,z)-Projektion aller Dreiecke einer Geometrie. Deckt Fehler auf, die die
|
||||
* reine Dreieckszahl nicht zeigt: ein Dreieck kann auf den falschen Punkt zeigen und die
|
||||
* Zahl der Dreiecke bleibt trotzdem gleich. */
|
||||
function footprintArea(geo) {
|
||||
const pos = geo.getAttribute("position");
|
||||
const index = geo.getIndex().array;
|
||||
let area = 0;
|
||||
for (let i = 0; i < index.length; i += 3) {
|
||||
const a = index[i], b = index[i + 1], c = index[i + 2];
|
||||
const x0 = pos.getX(a), z0 = pos.getZ(a);
|
||||
const x1 = pos.getX(b), z1 = pos.getZ(b);
|
||||
const x2 = pos.getX(c), z2 = pos.getZ(c);
|
||||
area += Math.abs((x1 - x0) * (z2 - z0) - (x2 - x0) * (z1 - z0)) / 2;
|
||||
}
|
||||
return area;
|
||||
}
|
||||
|
||||
describe("geometry", () => {
|
||||
it("extrudiert ein Quadrat zu Wänden und Dach", () => {
|
||||
const geo = extrudePolygons([PLOT], { base: 0, top: 20, uvScale: 4 });
|
||||
// 4 Wände à 2 Dreiecke + Dach aus 2 Dreiecken
|
||||
expect(geo.getIndex().count / 3).toBe(10);
|
||||
const box = new Box3().setFromBufferAttribute(geo.getAttribute("position"));
|
||||
expect(box.max.y).toBeCloseTo(20);
|
||||
expect(box.min.y).toBeCloseTo(0);
|
||||
});
|
||||
|
||||
it("richtet Dachflächen nach oben aus", () => {
|
||||
const geo = extrudePolygons([PLOT], { base: 0, top: 5, uvScale: 4 });
|
||||
geo.computeVertexNormals();
|
||||
const n = geo.getAttribute("normal");
|
||||
const up = [...Array(n.count).keys()].filter((i) => n.getY(i) > 0.9);
|
||||
expect(up.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("lässt den Innenhof eines Blocks offen", () => {
|
||||
// Außenring 30x30, Loch 10x10: 8 Wände à 2 Dreiecke, das Dach ist ein Ring aus
|
||||
// 8 Dreiecken. Ohne Lochbehandlung wäre der Hof zugedeckt und das Dach hätte 2.
|
||||
const geo = extrudePolygons([COURTYARD], { base: 0, top: 10, uvScale: 4 });
|
||||
expect(geo.getIndex().count / 3).toBe(24);
|
||||
});
|
||||
|
||||
it("legt flache Flächen auf die gewünschte Höhe", () => {
|
||||
const geo = flatPolygons([PLOT], { y: 0.03, uvScale: 8 });
|
||||
const box = new Box3().setFromBufferAttribute(geo.getAttribute("position"));
|
||||
expect(box.min.y).toBeCloseTo(0.03);
|
||||
expect(box.max.y).toBeCloseTo(0.03);
|
||||
});
|
||||
|
||||
it("baut alle Teile eines Chunks", () => {
|
||||
const chunk = {
|
||||
chunk: [0, 0], origin: [0, 0],
|
||||
land: [PLOT],
|
||||
surfaces: [{ poly: PLOT, kind: "sand" }, { poly: PLOT, kind: "park" }],
|
||||
roads: [{ poly: PLOT, bridge: false }, { poly: PLOT, bridge: true }],
|
||||
centerlines: [], palms: [],
|
||||
buildings: [{ poly: PLOT, height: 12, source: "district", speculative: false }],
|
||||
};
|
||||
const out = buildChunkGeometry(chunk);
|
||||
for (const key of ["ground", "sand", "park", "roads", "bridges", "buildings"]) {
|
||||
expect(out[key]).toBeInstanceOf(BufferGeometry);
|
||||
}
|
||||
expect(out.urban).toBeNull();
|
||||
});
|
||||
|
||||
it("gibt Gebäuden eine Farbe je Haus", () => {
|
||||
const chunk = { chunk: [0, 0], origin: [0, 0], land: [], surfaces: [], roads: [], centerlines: [], palms: [],
|
||||
buildings: [{ poly: PLOT, height: 9, source: "district", speculative: false }] };
|
||||
expect(buildChunkGeometry(chunk).buildings.getAttribute("color")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("hält alle Koordinaten im Chunk", () => {
|
||||
const chunk = { chunk: [0, 0], origin: [0, 0], land: [PLOT], surfaces: [], roads: [], centerlines: [], buildings: [], palms: [] };
|
||||
const box = new Box3().setFromBufferAttribute(buildChunkGeometry(chunk).ground.getAttribute("position"));
|
||||
expect(box.min.x).toBeGreaterThanOrEqual(-0.01);
|
||||
expect(box.max.x).toBeLessThanOrEqual(250.01);
|
||||
});
|
||||
|
||||
// Die folgenden zwei Tests stehen nicht im Brief. Die echten Chunk-Dateien schließen
|
||||
// jeden Ring selbst (erster Punkt == letzter Punkt); PLOT/COURTYARD oben nicht. Beide
|
||||
// Formen beschreiben dasselbe Polygon und müssen dieselbe Geometrie ergeben — sonst
|
||||
// weichen alle echten Flächen, Straßen und Gebäude von den hier geprüften Werten ab.
|
||||
it("behandelt einen sich selbst schließenden Ring wie seinen offenen Zwilling", () => {
|
||||
const geo = extrudePolygons([PLOT_CLOSED], { base: 0, top: 20, uvScale: 4 });
|
||||
expect(geo.getIndex().count / 3).toBe(10);
|
||||
});
|
||||
|
||||
it("verschiebt bei geschlossenen Ringen keine Lochpunkte", () => {
|
||||
// Dachfläche eines Blocks mit Innenhof: 30x30 Außenring minus 10x10 Loch = 800.
|
||||
// Ohne Entdopplung des Schlusspunkts landet der erste Lochpunkt auf dem Schlusspunkt
|
||||
// des Außenrings, und mindestens ein Dreieck bekommt einen falschen Eckpunkt.
|
||||
const geo = flatPolygons([COURTYARD_CLOSED], { y: 0, uvScale: 4 });
|
||||
expect(footprintArea(geo)).toBeCloseTo(800, 6);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user