feat(world): Tribüne im Park J6 statt des alten Gebäudekastens
tribuene.glb mit places.mjs aufbereitet (6,3 MB -> 1,7 MB, 69 557 Dreiecke, 14 Primitive, 49 MB Grafikspeicher). PLACES-Eintrag bei (1409,9 | 1051,1), 45° gedreht, lift 0, replaces auf den 18,6 m hohen Kasten. Der Lage-Test übergeht ersetzte Grundrisse und prüft, dass jeder replaces-Punkt genau ein Gebäude unter dem Ort trifft. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ae0f6106bb
commit
190fe9c13c
5 files changed
+51
-8
No files matched your search
@@ -98,9 +98,12 @@ Die positive Drehung bringt +x nach Nordosten und die Front (+z) nach Südosten.
|
|||||||
|
|
||||||
## Budget
|
## Budget
|
||||||
|
|
||||||
- **Erwartet:** rund 70 000 Dreiecke, 14 Primitive (Draw Calls), unter 53 MB Grafikspeicher, Datei deutlich unter 30 MB.
|
- **Gemessen an der Aufbereitung (2026-09-14):**
|
||||||
|
- 1,7 MB, 69 557 Dreiecke, 14 Primitive (Draw Calls)
|
||||||
|
- 39 WebP-Texturen: 4 × 768, 23 × 512, 12 × 256 px
|
||||||
|
- geschätzt 49 MB Grafikspeicher, Außenmaße unverändert
|
||||||
|
- Die sechs Kleinteil-Materialien haben 15 939 Dreiecke statt 64 298.
|
||||||
- **Zum Vergleich der Hood Yard:** 657 653 Dreiecke, 43 Primitive, 231 MB.
|
- **Zum Vergleich der Hood Yard:** 657 653 Dreiecke, 43 Primitive, 231 MB.
|
||||||
- Nach der Aufbereitung kommen die Werte aus dem Bericht von `places.mjs` hier hinein.
|
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
+17
-1
@@ -19,7 +19,9 @@ export const PLACE_HIDE_RADIUS = 450;
|
|||||||
* at: Welt-x und Welt-y in Metern, wie auf der Minimap — nie three-z. rotation: Grad gegen den
|
* at: Welt-x und Welt-y in Metern, wie auf der Minimap — nie three-z. rotation: Grad gegen den
|
||||||
* Uhrzeigersinn, von oben mit Norden oben (wie add_building in corrections.toml). lift: Meter auf die
|
* Uhrzeigersinn, von oben mit Norden oben (wie add_building in corrections.toml). lift: Meter auf die
|
||||||
* Modellwurzel. bounds: Außenmaße in Modellkoordinaten (x, z); das Spiel braucht sie nicht, die Tests
|
* Modellwurzel. bounds: Außenmaße in Modellkoordinaten (x, z); das Spiel braucht sie nicht, die Tests
|
||||||
* prüfen damit Datei (placesAsset.test.js) und Lage (placesSite.test.js) gegeneinander.
|
* prüfen damit Datei (placesAsset.test.js) und Lage (placesSite.test.js) gegeneinander. replaces
|
||||||
|
* (optional): Weltpunkte [x, y]; jeder Gebäudegrundriss aus der Extraktion, in dem einer liegt, wird beim
|
||||||
|
* Chunk-Aufbau weggelassen (withoutReplacedBuildings in streaming.js).
|
||||||
*/
|
*/
|
||||||
export const PLACES = [
|
export const PLACES = [
|
||||||
{
|
{
|
||||||
@@ -32,6 +34,20 @@ export const PLACES = [
|
|||||||
lift: -0.28,
|
lift: -0.28,
|
||||||
bounds: { minX: -17.15, maxX: 17, minZ: -13.4, maxZ: 8.6 },
|
bounds: { minX: -17.15, maxX: 17, minZ: -13.4, maxZ: 8.6 },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "tribuene",
|
||||||
|
url: "./models/places/tribuene.glb",
|
||||||
|
// Längs mittig auf dem alten Gebäudekasten, Front 1 m vor dessen Feldseite (Vergleichsbild des Nutzers);
|
||||||
|
// längs bleiben bis zur Fahrbahn nur 1,25 m nach Nordosten und 1,8 m nach Südwesten.
|
||||||
|
at: [1409.9, 1051.1],
|
||||||
|
// Die Sitzreihen schauen wie in echt nach Südosten auf die Home Plate.
|
||||||
|
rotation: 45,
|
||||||
|
// Keine Bodenplatte: der Betonkörper steht mit seiner Unterseite auf 0, flach auf 0 liegt nichts.
|
||||||
|
lift: 0,
|
||||||
|
bounds: { minX: -20.8, maxX: 20.8, minZ: -14.82, maxZ: 0.8 },
|
||||||
|
// Der 18,6 m hohe Gebäudekasten aus der Extraktion an dieser Stelle (J6).
|
||||||
|
replaces: [[1403.25, 1057.75]],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Materialien, die kollidieren. Alles andere — Gras, Blätter, Büsche, Sitzschalen, Kleinteile — läuft man durch. */
|
/** Materialien, die kollidieren. Alles andere — Gras, Blätter, Büsche, Sitzschalen, Kleinteile — läuft man durch. */
|
||||||
|
|||||||
@@ -181,14 +181,15 @@ describe("prepareModel", () => {
|
|||||||
|
|
||||||
describe("PLACES", () => {
|
describe("PLACES", () => {
|
||||||
// Bruch: ein Eintrag ohne Pflichtfeld — placeTransform rechnete mit undefined, die Tests mit bounds
|
// Bruch: ein Eintrag ohne Pflichtfeld — placeTransform rechnete mit undefined, die Tests mit bounds
|
||||||
// prüften nichts.
|
// prüften nichts; oder replaces ist keine Liste von Punkten [x, y], und der Streamer ließe nichts weg.
|
||||||
it("hat für jeden Ort id, url, at, rotation, lift und bounds", () => {
|
it("hat für jeden Ort id, url, at, rotation, lift und bounds, dazu optional replaces mit Weltpunkten", () => {
|
||||||
for (const entry of PLACES) {
|
for (const { replaces = [], ...entry } of PLACES) {
|
||||||
expect(entry).toEqual({
|
expect(entry).toEqual({
|
||||||
id: expect.any(String), url: expect.stringMatching(/^\.\/models\/places\/.+\.glb$/),
|
id: expect.any(String), url: expect.stringMatching(/^\.\/models\/places\/.+\.glb$/),
|
||||||
at: [expect.any(Number), expect.any(Number)], rotation: expect.any(Number), lift: expect.any(Number),
|
at: [expect.any(Number), expect.any(Number)], rotation: expect.any(Number), lift: expect.any(Number),
|
||||||
bounds: { minX: expect.any(Number), maxX: expect.any(Number), minZ: expect.any(Number), maxZ: expect.any(Number) },
|
bounds: { minX: expect.any(Number), maxX: expect.any(Number), minZ: expect.any(Number), maxZ: expect.any(Number) },
|
||||||
});
|
});
|
||||||
|
for (const point of replaces) expect(point).toEqual([expect.any(Number), expect.any(Number)]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { CHUNKS_PER_SIDE, CHUNK_SIZE, SLICE, chunkForWorld } from "../src/world/coords.js";
|
import { CHUNKS_PER_SIDE, CHUNK_SIZE, SLICE, chunkForWorld } from "../src/world/coords.js";
|
||||||
import { PLACES, PLACE_HIDE_RADIUS, PLACE_SHOW_RADIUS, placeTransform } from "../src/world/places.js";
|
import { PLACES, PLACE_HIDE_RADIUS, PLACE_SHOW_RADIUS, placeTransform } from "../src/world/places.js";
|
||||||
import { lampPoints, pointInPolygon, vergeLampPoints } from "../src/world/props.js";
|
import { lampPoints, pointInPolygon, vergeLampPoints } from "../src/world/props.js";
|
||||||
|
import { withoutReplacedBuildings } from "../src/world/streaming.js";
|
||||||
|
|
||||||
// Radien aus ChunkStreamer.update (src/world/streaming.js), gemessen zum Chunk-Mittelpunkt.
|
// Radien aus ChunkStreamer.update (src/world/streaming.js), gemessen zum Chunk-Mittelpunkt.
|
||||||
const CHUNK_LOAD_RADIUS = 400;
|
const CHUNK_LOAD_RADIUS = 400;
|
||||||
@@ -18,6 +19,16 @@ function chunk(i, j) {
|
|||||||
return cache.get(key);
|
return cache.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Grundrisse, die ein Ort ersetzt, baut der Streamer nicht (withoutReplacedBuildings, verdrahtet in
|
||||||
|
// src/main.js) — für die Prüfung auf Gebäude zählen sie deshalb nicht.
|
||||||
|
const REPLACED = PLACES.flatMap((place) => place.replaces ?? []);
|
||||||
|
const standing = new Map();
|
||||||
|
function standingBuildings(i, j) {
|
||||||
|
const key = `${i}_${j}`;
|
||||||
|
if (!standing.has(key)) standing.set(key, withoutReplacedBuildings(chunk(i, j), REPLACED).buildings);
|
||||||
|
return standing.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
/** Modellpunkt (x, z) -> Welt (X, Y), mit derselben Drehung wie three (rotation.y). */
|
/** Modellpunkt (x, z) -> Welt (X, Y), mit derselben Drehung wie three (rotation.y). */
|
||||||
function modelToWorld(place, x, z) {
|
function modelToWorld(place, x, z) {
|
||||||
const { position, rotationY } = placeTransform(place);
|
const { position, rotationY } = placeTransform(place);
|
||||||
@@ -51,7 +62,7 @@ describe.each(PLACES.map((place) => [place.id, place]))("Ort %s auf der Karte",
|
|||||||
const under = keys.map((key) => key.split("_").map(Number));
|
const under = keys.map((key) => key.split("_").map(Number));
|
||||||
|
|
||||||
// Bruch: der Ort steht auf einer Straße, in einem Gebäude oder über Wasser — nach einem Verschieben
|
// Bruch: der Ort steht auf einer Straße, in einem Gebäude oder über Wasser — nach einem Verschieben
|
||||||
// in PLACES oder nach einem neuen Lauf der Extraktion.
|
// in PLACES oder nach einem neuen Lauf der Extraktion. Gebäude, die ein Ort ersetzt, zählen nicht.
|
||||||
it("liegt ganz auf Land, auf keiner Straße und in keinem Gebäude", () => {
|
it("liegt ganz auf Land, auf keiner Straße und in keinem Gebäude", () => {
|
||||||
const bad = points.filter(({ X, Y }) => {
|
const bad = points.filter(({ X, Y }) => {
|
||||||
const { i, j } = chunkForWorld(X, Y);
|
const { i, j } = chunkForWorld(X, Y);
|
||||||
@@ -59,12 +70,24 @@ describe.each(PLACES.map((place) => [place.id, place]))("Ort %s auf der Karte",
|
|||||||
const u = X - c.origin[0], v = -Y - c.origin[1];
|
const u = X - c.origin[0], v = -Y - c.origin[1];
|
||||||
const onLand = c.land.some((rings) => pointInPolygon(u, v, rings));
|
const onLand = c.land.some((rings) => pointInPolygon(u, v, rings));
|
||||||
const onRoad = c.roads.some((road) => pointInPolygon(u, v, road.poly));
|
const onRoad = c.roads.some((road) => pointInPolygon(u, v, road.poly));
|
||||||
const inBuilding = c.buildings.some((building) => pointInPolygon(u, v, building.poly));
|
const inBuilding = standingBuildings(i, j).some((building) => pointInPolygon(u, v, building.poly));
|
||||||
return !onLand || onRoad || inBuilding;
|
return !onLand || onRoad || inBuilding;
|
||||||
});
|
});
|
||||||
expect(bad.slice(0, 5)).toEqual([]);
|
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.
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
expect(bad).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
// Bruch: eine Palme oder Laterne mitten im Ort. Beide entstehen je Chunk und wissen nichts von Orten;
|
// Bruch: eine Palme oder Laterne mitten im Ort. Beide entstehen je Chunk und wissen nichts von Orten;
|
||||||
// Laternen stehen bis 16 m neben ihrer Mittellinie, deshalb zählen auch die Nachbarchunks.
|
// Laternen stehen bis 16 m neben ihrer Mittellinie, deshalb zählen auch die Nachbarchunks.
|
||||||
it("enthält keine Palme und keine Laterne", () => {
|
it("enthält keine Palme und keine Laterne", () => {
|
||||||
|
|||||||
Reference in new issue
Block a user