main #1
No files matched your search
@@ -113,7 +113,7 @@ Die positive Drehung bringt +x nach Nordosten und die Front (+z) nach Südosten.
|
||||
- Ein `ChunkStreamer` mit `replacedBuildings` baut aus einem Chunk mit nur diesem Gebäude keine Gebäudegeometrie.
|
||||
- **`tests/placesSite.test.js`:**
|
||||
- „in keinem Gebäude“ übergeht die Grundrisse, die ein Ort ersetzt.
|
||||
- Neu: Jeder `replaces`-Punkt liegt in genau einem Grundriss seines Chunks und innerhalb der Außenmaße seines Orts. Das fängt einen Lauf der Extraktion ab, der das Gebäude verschiebt.
|
||||
- Neu: Der Punkt trifft genau einen Grundriss, liegt in den Außenmaßen, und alle Ecken dieses Grundrisses liegen höchstens 5 m außerhalb. Das fängt einen Lauf der Extraktion ab, der das Gebäude verschiebt, teilt oder mit einem Nachbarn zusammenlegt.
|
||||
- **`tests/placesAsset.test.js`** wird auf `describe.each(PLACES)` umgebaut:
|
||||
- Je Ort geprüft werden Dateigröße, meshopt und WebP, fehlende Instanzen, Texturen bis 1024 px und `bounds` auf 5 cm.
|
||||
- Die festen Materialien je Ort kommen aus einer Tabelle im Test, deren Vereinigung `SOLID_MATERIALS` ergeben muss.
|
||||
@@ -135,7 +135,7 @@ Die positive Drehung bringt +x nach Nordosten und die Front (+z) nach Südosten.
|
||||
- **Alter Kasten:** Er ist weg. Ein Strahl im Streifen hinter der Tribüne trifft den Boden (y 0), in der alten Mitte das Tribünendach (y 10,1) statt 18,6 m. Ein Nachbargebäude in J6 steht unverändert.
|
||||
- **Kollision:**
|
||||
- Genau die acht festen Materialien haben Float32-Positionen und kommen in den Collider, die sechs durchlässigen nicht.
|
||||
- Die Kapsel (Radius 0,15 m) steht auf dem Boden bei y 0,75 und bleibt an der Rückwand genau am Sockel stehen, Modell-z −14,39.
|
||||
- Die Kapsel (Radius 0,15 m) steht auf dem Boden bei y 0,75. An der Rückwand bleibt ihre Mitte bei Modell-z −14,54 stehen; die Sockelfläche (Sichtbeton, y 0–1,15) liegt bei −14,39 — die Kapsel berührt die Wand also, ohne einzudringen.
|
||||
- Sie steigt über die 25-cm-Kante in den Tunnel, läuft die Treppe hinauf auf die Sitzstufe (y 3,6) und im Gang hinunter bis ans vordere Geländer.
|
||||
- Auf der Sitzstufe in 6 m Höhe läuft sie ohne Halt 5,7 m durch eine Sitzreihe.
|
||||
- **Kosten:** Median aus 60 Bildern mit `gl.finish`:
|
||||
|
||||
@@ -37,6 +37,10 @@ function dist(k, position) {
|
||||
* jeder Grundriss, in dem einer der Weltpunkte [x, y] liegt, entfällt — und mit ihm Kasten, Schatten und
|
||||
* Kollision. Wirkt wie remove_building der Feinkalibrierung, nur zur Laufzeit. Trifft kein Punkt, kommt
|
||||
* derselbe Chunk zurück.
|
||||
*
|
||||
* Ein Gebäude an einer Chunkgrenze schreibt die Extraktion ungeschnitten in jeden berührten Chunk. Deshalb
|
||||
* wird hier jeder Punkt gegen jeden Chunk geprüft, statt ihn vorab einem Chunk zuzuordnen — sonst bliebe
|
||||
* die Kopie im Nachbarchunk stehen.
|
||||
*/
|
||||
export function withoutReplacedBuildings(chunk, points) {
|
||||
if (points.length === 0) return chunk;
|
||||
|
||||
@@ -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