test(world): ersetzte Grundrisse und feste Materialien je Ort strenger prüfen
Behebt Befunde 2-6 aus dem Abschluss-Review: der replaces-Test prüft jetzt zusätzlich jede Ecke des getroffenen Grundrisses gegen die Außenmaße plus REPLACED_OVERHANG (5 m), der Verteilungstest vergleicht als Mengen und bekommt eine Gegenrichtung je Datei, BUMP_BY_PLACE erzwingt eine Zeile je Ort statt sie mit "?? []" zu verdecken, ein Gebäude an einer Chunkgrenze bekommt einen Test über zwei Chunks samt JSDoc-Hinweis, und der Rückwand-Messwert in der Spec nennt Kapselmitte, Wandfläche und Radius einzeln. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
14fd9cefde
commit
237a4ed389
5 files changed
+59
-12
No files matched your search
@@ -5,7 +5,7 @@ import { NodeIO } from "@gltf-transform/core";
|
||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||
import { getBounds } from "@gltf-transform/functions";
|
||||
import { MeshoptDecoder } from "meshoptimizer";
|
||||
import { PLACES, SOLID_MATERIALS } from "../src/world/places.js";
|
||||
import { isSolidMaterial, PLACES, SOLID_MATERIALS } from "../src/world/places.js";
|
||||
import { EXT_MATERIALS_BUMP, EXTMaterialsBump } from "../tools/assets/bumpExtension.mjs";
|
||||
|
||||
// Feste Materialien je Ort. SOLID_MATERIALS gilt für alle Orte zusammen, aber keine Datei enthält alle
|
||||
@@ -26,16 +26,20 @@ const SOLID_BY_PLACE = {
|
||||
// Materialien, deren Relief nur aus EXT_materials_bump kommt. Die Tribüne hat keine Bump-Maps.
|
||||
const BUMP_BY_PLACE = {
|
||||
"hood-yard": ["cont_a_side", "cont_a_end", "cont_b_side", "cont_b_end", "cont_c_side", "cont_c_end"],
|
||||
tribuene: [],
|
||||
};
|
||||
|
||||
// Bruch: ein festes Material steht in keiner Zeile — die Datei seines Orts würde nie darauf geprüft.
|
||||
// Bruch: ein festes Material steht in keiner Zeile — die Datei seines Orts würde nie darauf geprüft. Als
|
||||
// Mengen verglichen, damit ein Name, der in zwei Orten fest ist, keinen Fehlalarm auslöst.
|
||||
it("verteilt SOLID_MATERIALS vollständig auf die Orte", () => {
|
||||
expect(Object.values(SOLID_BY_PLACE).flat().sort()).toEqual([...SOLID_MATERIALS].sort());
|
||||
expect(new Set(Object.values(SOLID_BY_PLACE).flat())).toEqual(new Set(SOLID_MATERIALS));
|
||||
});
|
||||
|
||||
describe.each(PLACES.map((place) => [place.id, place]))("%s.glb", (id, place) => {
|
||||
const file = fileURLToPath(new URL(`../public/${place.url.slice(2)}`, import.meta.url));
|
||||
const bump = BUMP_BY_PLACE[id] ?? [];
|
||||
// Kein "?? []": eine fehlende Zeile soll die Prüfung darunter rot machen (Bruch), nicht still
|
||||
// durchlaufen. it.runIf gleich unten sichert nur seine eigene Bedingung gegen "undefined" ab.
|
||||
const bump = BUMP_BY_PLACE[id];
|
||||
let document;
|
||||
beforeAll(async () => {
|
||||
await MeshoptDecoder.ready;
|
||||
@@ -65,9 +69,15 @@ describe.each(PLACES.map((place) => [place.id, place]))("%s.glb", (id, place) =>
|
||||
}
|
||||
});
|
||||
|
||||
// Bruch: der Ort hat keine Zeile in BUMP_BY_PLACE — ohne diesen Test liefe die Bump-Prüfung darunter
|
||||
// per it.runIf(bump?.length > 0) einfach übersprungen durch, statt rot zu werden.
|
||||
it("hat eine Zeile in BUMP_BY_PLACE", () => {
|
||||
expect(bump).toBeDefined();
|
||||
});
|
||||
|
||||
// Bruch: die Aufbereitung verwirft EXT_materials_bump (wie die gltf-transform-CLI) — das Wellblech der
|
||||
// Container wäre flach.
|
||||
it.runIf(bump.length > 0)("gibt den Bump-Materialien ihre Bump-Textur", () => {
|
||||
it.runIf((bump ?? []).length > 0)("gibt den Bump-Materialien ihre Bump-Textur", () => {
|
||||
const used = document.getRoot().listExtensionsUsed().map((extension) => extension.extensionName);
|
||||
const materials = new Map(document.getRoot().listMaterials().map((material) => [material.getName(), material]));
|
||||
|
||||
@@ -86,6 +96,15 @@ describe.each(PLACES.map((place) => [place.id, place]))("%s.glb", (id, place) =>
|
||||
expect(SOLID_BY_PLACE[id].filter((name) => !names.includes(name))).toEqual([]);
|
||||
});
|
||||
|
||||
// Bruch: ein Material der Datei ist laut isSolidMaterial fest (weil sein Name anderswo fest ist), ohne
|
||||
// in der Zeile seines Orts zu stehen — es würde hier still mitkollidieren, ungeprüft von SOLID_BY_PLACE.
|
||||
it("hat kein fremdes festes Material ohne Zeile in SOLID_BY_PLACE", () => {
|
||||
const names = document.getRoot().listMaterials().map((material) => material.getName());
|
||||
const ownSolid = new Set(SOLID_BY_PLACE[id]);
|
||||
|
||||
expect(names.filter((name) => isSolidMaterial(name) && !ownSolid.has(name))).toEqual([]);
|
||||
});
|
||||
|
||||
// Bruch: bounds in PLACES passt nicht mehr zur Datei — der Datentest prüfte dann die falsche Fläche.
|
||||
it("passt auf 5 cm zu den Außenmaßen in PLACES", () => {
|
||||
const { min, max } = getBounds(document.getRoot().listScenes()[0]);
|
||||
|
||||
@@ -9,6 +9,9 @@ import { withoutReplacedBuildings } from "../src/world/streaming.js";
|
||||
const CHUNK_LOAD_RADIUS = 400;
|
||||
const CHUNK_UNLOAD_RADIUS = 600;
|
||||
const STEP = 0.5; // Meter zwischen zwei Prüfpunkten auf der Ortsfläche
|
||||
// Wie weit darf ein ersetzter Grundriss über die Außenmaße seines Orts hinausragen? Die Tribüne liegt
|
||||
// heute 3,78 m vor der Rückseite ihres alten Kastens, Modell-z −18,60 gegen −14,82.
|
||||
const REPLACED_OVERHANG = 5; // Meter
|
||||
|
||||
const cache = new Map();
|
||||
function chunk(i, j) {
|
||||
@@ -44,10 +47,10 @@ function worldToModel(place, X, Y) {
|
||||
return { x: dx * cos - dz * sin, z: dx * sin + dz * cos };
|
||||
}
|
||||
|
||||
function insideBounds(place, X, Y) {
|
||||
function insideBounds(place, X, Y, margin = 0) {
|
||||
const { x, z } = worldToModel(place, X, Y);
|
||||
const { minX, maxX, minZ, maxZ } = place.bounds;
|
||||
return x >= minX && x <= maxX && z >= minZ && z <= maxZ;
|
||||
return x >= minX - margin && x <= maxX + margin && z >= minZ - margin && z <= maxZ + margin;
|
||||
}
|
||||
|
||||
describe.each(PLACES.map((place) => [place.id, place]))("Ort %s auf der Karte", (_id, place) => {
|
||||
@@ -76,14 +79,18 @@ describe.each(PLACES.map((place) => [place.id, place]))("Ort %s auf der Karte",
|
||||
expect(bad.slice(0, 5)).toEqual([]);
|
||||
});
|
||||
|
||||
// Bruch: Nach einem neuen Lauf der Extraktion liegt das ersetzte Gebäude woanders oder ist geteilt. Der
|
||||
// Punkt träfe dann nichts (der alte Kasten stünde wieder im Ort), mehrere Grundrisse oder einen außerhalb.
|
||||
// Bruch: Nach einem neuen Lauf der Extraktion liegt das ersetzte Gebäude woanders oder ist geteilt, oder
|
||||
// der Punkt trifft ein fremdes oder mit einem Nachbarn zusammengelegtes Gebäude. Der Punkt träfe dann
|
||||
// nichts (der alte Kasten stünde wieder im Ort), mehrere Grundrisse, einen außerhalb, oder einen mit
|
||||
// Ecken weit jenseits der Außenmaße.
|
||||
it.runIf((place.replaces ?? []).length > 0)("ersetzt mit jedem replaces-Punkt genau ein Gebäude unter sich", () => {
|
||||
const bad = place.replaces.filter(([X, Y]) => {
|
||||
const { i, j } = chunkForWorld(X, Y);
|
||||
const c = chunk(i, j);
|
||||
const hits = c.buildings.filter((building) => pointInPolygon(X - c.origin[0], -Y - c.origin[1], building.poly));
|
||||
return hits.length !== 1 || !insideBounds(place, X, Y);
|
||||
if (hits.length !== 1 || !insideBounds(place, X, Y)) return true;
|
||||
const corners = hits[0].poly.flat(1);
|
||||
return corners.some(([u, v]) => !insideBounds(place, c.origin[0] + u, -(c.origin[1] + v), REPLACED_OVERHANG));
|
||||
});
|
||||
expect(bad).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -54,6 +54,23 @@ describe("withoutReplacedBuildings", () => {
|
||||
expect(withoutReplacedBuildings(chunk, [])).toBe(chunk);
|
||||
expect(withoutReplacedBuildings(chunk, [[1405, 1255]])).toBe(chunk);
|
||||
});
|
||||
|
||||
// Bruch: eine Zuordnung des Punkts per chunkForWorld ordnete ihn nur chunk_9_5 zu (i = 9) und ließe die
|
||||
// ungeschnittene Kopie desselben Grundrisses in chunk_10_5 stehen — genau das Gebäude, das der Ort ersetzt.
|
||||
it("lässt denselben, ungeschnittenen Grundriss in beiden berührten Chunks weg", () => {
|
||||
const chunkA = {
|
||||
chunk: [9, 5], origin: [1250, -1250], land: [], surfaces: [], roads: [],
|
||||
buildings: [{ poly: square(245, 190), height: 18.6 }], // Welt x 1495–1505, y 1050–1060
|
||||
};
|
||||
const chunkB = {
|
||||
chunk: [10, 5], origin: [1500, -1250], land: [], surfaces: [], roads: [],
|
||||
buildings: [{ poly: square(-5, 190), height: 18.6 }], // dieselbe Welt-Fläche, andere u
|
||||
};
|
||||
const points = [[1498, 1055]];
|
||||
|
||||
expect(withoutReplacedBuildings(chunkA, points).buildings).toEqual([]);
|
||||
expect(withoutReplacedBuildings(chunkB, points).buildings).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// Gemeinsame Vorrichtung für beide ChunkStreamer-describe-Blöcke unten (mit und ohne PropLayer).
|
||||
|
||||
Reference in new issue
Block a user