feat(world): Fahrbahnmulde, Bordsteinwand und Markierungen bauen
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3d39a9bd66
commit
ca4f7161af
2 files changed
+255
-16
No files matched your search
+112
-12
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Box3, BufferGeometry } from "three";
|
||||
import { buildChunkGeometry, extrudePolygons, flatPolygons } from "../src/world/geometry.js";
|
||||
import { buildChunkGeometry, CARRIAGEWAY_DEPTH, extrudePolygons, flatPolygons, KERB_HEIGHT, KERB_INSET,
|
||||
KERB_TOP, kerbGeometry, MARKING_COLORS, MARKING_OFFSET, markingGeometry } 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
|
||||
@@ -71,8 +72,8 @@ describe("geometry", () => {
|
||||
const chunk = {
|
||||
chunk: [0, 0], origin: [0, 0],
|
||||
land: [PLOT], surfaces: [],
|
||||
roads: [{ poly: PLOT, bridge: true }],
|
||||
centerlines: [], palms: [], buildings: [],
|
||||
bridges: [{ poly: PLOT }],
|
||||
palms: [], buildings: [],
|
||||
};
|
||||
const out = buildChunkGeometry(chunk);
|
||||
const groundTop = new Box3().setFromBufferAttribute(out.ground.getAttribute("position")).max.y;
|
||||
@@ -90,12 +91,13 @@ describe("geometry", () => {
|
||||
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: [],
|
||||
carriageway: [{ poly: PLOT, klasse: "wohnstrasse" }],
|
||||
bridges: [{ poly: PLOT }],
|
||||
palms: [],
|
||||
buildings: [{ poly: PLOT, height: 12, source: "district", speculative: false }],
|
||||
};
|
||||
const out = buildChunkGeometry(chunk);
|
||||
for (const key of ["ground", "sand", "park", "roads", "bridges", "buildings"]) {
|
||||
for (const key of ["ground", "sand", "park", "carriageway", "bridges", "buildings"]) {
|
||||
expect(out[key]).toBeInstanceOf(BufferGeometry);
|
||||
}
|
||||
expect(out.urban).toBeNull();
|
||||
@@ -103,7 +105,7 @@ describe("geometry", () => {
|
||||
|
||||
it("baut Parkwege flach auf Straßenhöhe und die neuen Flächenarten wie Beläge", () => {
|
||||
const chunk = {
|
||||
chunk: [0, 0], origin: [0, 0], land: [], roads: [], centerlines: [], palms: [], buildings: [],
|
||||
chunk: [0, 0], origin: [0, 0], land: [], palms: [], buildings: [],
|
||||
surfaces: ["field", "court", "pool", "parking"].map((kind) => ({ poly: PLOT, kind })),
|
||||
paths: [{ poly: COURTYARD }],
|
||||
};
|
||||
@@ -118,18 +120,18 @@ describe("geometry", () => {
|
||||
});
|
||||
|
||||
it("liefert ohne Parkwege null statt einer leeren Geometrie", () => {
|
||||
const chunk = { chunk: [0, 0], origin: [0, 0], land: [PLOT], surfaces: [], roads: [], centerlines: [], buildings: [], palms: [] };
|
||||
const chunk = { chunk: [0, 0], origin: [0, 0], land: [PLOT], surfaces: [], buildings: [], palms: [] };
|
||||
expect(buildChunkGeometry(chunk).paths).toBeNull();
|
||||
});
|
||||
|
||||
it("gibt Gebäuden eine Farbe je Haus", () => {
|
||||
const chunk = { chunk: [0, 0], origin: [0, 0], land: [], surfaces: [], roads: [], centerlines: [], palms: [],
|
||||
const chunk = { chunk: [0, 0], origin: [0, 0], land: [], surfaces: [], 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 chunk = { chunk: [0, 0], origin: [0, 0], land: [PLOT], surfaces: [], 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);
|
||||
@@ -214,7 +216,7 @@ describe("geometry", () => {
|
||||
|
||||
it("baut eine Platte mit 100 Löchern in unter 50 ms", () => {
|
||||
const chunk = { chunk: [0, 0], origin: [0, 0], land: plateWithHoles(100),
|
||||
roads: [], centerlines: [], buildings: [], palms: [] };
|
||||
buildings: [], palms: [] };
|
||||
const start = performance.now();
|
||||
const geo = buildChunkGeometry(chunk).ground;
|
||||
const ms = performance.now() - start;
|
||||
@@ -226,7 +228,7 @@ describe("geometry", () => {
|
||||
|
||||
it("lässt kein Loch verschwinden", () => {
|
||||
const chunk = { chunk: [0, 0], origin: [0, 0], land: plateWithHoles(25),
|
||||
roads: [], centerlines: [], buildings: [], palms: [] };
|
||||
buildings: [], palms: [] };
|
||||
const geo = buildChunkGeometry(chunk).ground;
|
||||
// Ohne Löcher hätte die Platte 4 Ringpunkte für die Wände plus 4 für die Decke.
|
||||
expect(geo.getAttribute("position").count).toBeGreaterThan(25 * 16);
|
||||
@@ -237,3 +239,101 @@ describe("geometry", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Straßen aus Teil 2", () => {
|
||||
const chunk = (extra) => ({
|
||||
chunk: [0, 0], origin: [0, 0], land: [], surfaces: [], buildings: [], palms: [],
|
||||
carriageway: [], curb: [], sidewalk: [], markings: [], lots: [], bridges: [], edges: [],
|
||||
...extra,
|
||||
});
|
||||
// Eine Fahrbahn nördlich der Bordsteinlinie z = 10, die Linie in beiden Laufrichtungen: emit.py
|
||||
// legt die Richtung jeder Linie fest, sie sagt also nichts darüber, wo die Fahrbahn liegt.
|
||||
const FAHRBAHN = [[[10, 10], [20, 10], [20, 20], [10, 20]]];
|
||||
const kante = (umgedreht) => (umgedreht ? [[20, 10], [10, 10]] : [[10, 10], [20, 10]]);
|
||||
const bounds = (geo) => new Box3().setFromBufferAttribute(geo.getAttribute("position"));
|
||||
|
||||
it("legt die Fahrbahn 15 cm unter den Boden", () => {
|
||||
const geo = buildChunkGeometry(chunk({
|
||||
carriageway: [{ poly: FAHRBAHN, klasse: "wohnstrasse" }],
|
||||
})).carriageway;
|
||||
// toBeCloseTo, nicht toEqual: Die Positionen liegen als Float32 vor, und -0,15 ist dort
|
||||
// -0,15000000596.
|
||||
expect(bounds(geo).min.y).toBeCloseTo(-CARRIAGEWAY_DEPTH);
|
||||
expect(bounds(geo).max.y).toBeCloseTo(-CARRIAGEWAY_DEPTH);
|
||||
});
|
||||
|
||||
it("baut aus einer Bordsteinlinie eine Wand aus der Mulde und eine Oberkante knapp über den Belägen", () => {
|
||||
const geo = kerbGeometry([kante(false)], [FAHRBAHN]);
|
||||
expect(bounds(geo).min.y).toBeCloseTo(-CARRIAGEWAY_DEPTH);
|
||||
expect(bounds(geo).max.y).toBeCloseTo(KERB_HEIGHT);
|
||||
// Wand (4 Punkte) und Oberkante (4 Punkte) je Abschnitt.
|
||||
expect(geo.getAttribute("position").count).toBe(8);
|
||||
});
|
||||
|
||||
it("legt die Oberkante auf die Gehwegseite und die Wand 1 cm in die Fahrbahn, in beiden Laufrichtungen", () => {
|
||||
for (const umgedreht of [false, true]) {
|
||||
const geo = kerbGeometry([kante(umgedreht)], [FAHRBAHN]);
|
||||
const pos = geo.getAttribute("position");
|
||||
expect(bounds(geo).min.z, `umgedreht=${umgedreht}`).toBeCloseTo(10 - KERB_TOP);
|
||||
expect(bounds(geo).max.z, `umgedreht=${umgedreht}`).toBeCloseTo(10 + KERB_INSET);
|
||||
for (let i = 0; i < pos.count; i++) {
|
||||
if (pos.getY(i) < 0) expect(pos.getZ(i), `Wandfuß, umgedreht=${umgedreht}`).toBeCloseTo(10 + KERB_INSET);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("zeigt die Oberkante nach oben, in beiden Laufrichtungen", () => {
|
||||
for (const umgedreht of [false, true]) {
|
||||
const geo = kerbGeometry([kante(umgedreht)], [FAHRBAHN]);
|
||||
const pos = geo.getAttribute("position");
|
||||
const normal = geo.getAttribute("normal");
|
||||
const aussen = [];
|
||||
for (let i = 0; i < pos.count; i++) {
|
||||
if (pos.getZ(i) < 10 - KERB_TOP / 2) aussen.push(normal.getY(i)); // Außenkante der Oberkante
|
||||
}
|
||||
expect(aussen, `umgedreht=${umgedreht}`).toHaveLength(2);
|
||||
for (const ny of aussen) expect(ny, `umgedreht=${umgedreht}`).toBeGreaterThan(0.99);
|
||||
}
|
||||
});
|
||||
|
||||
it("baut nur die Wand, wenn keine Fahrbahn neben der Linie liegt", () => {
|
||||
// Ohne Fahrbahn lässt sich die Seite nicht bestimmen, und eine Oberkante auf der falschen
|
||||
// Seite hinge als Sims über der Mulde.
|
||||
const geo = kerbGeometry([kante(false)], []);
|
||||
expect(geo.getAttribute("position").count).toBe(4);
|
||||
expect(bounds(geo).min.z).toBeCloseTo(10);
|
||||
expect(bounds(geo).max.z).toBeCloseTo(10);
|
||||
});
|
||||
|
||||
it("gibt Markierungen ihre Farbe als Vertexfarbe und legt sie 1 cm über die Fahrbahn", () => {
|
||||
const geo = markingGeometry([
|
||||
{ poly: [[[0, 0], [3, 0], [3, -0.1], [0, -0.1]]], art: "gelb_doppelt" },
|
||||
{ poly: [[[5, 0], [8, 0], [8, -0.6], [5, -0.6]]], art: "zebra" },
|
||||
]);
|
||||
const color = geo.getAttribute("color").array;
|
||||
expect(color.slice(0, 3)).toEqual(new Float32Array(MARKING_COLORS.gelb_doppelt));
|
||||
expect(color.slice(12, 15)).toEqual(new Float32Array(MARKING_COLORS.zebra));
|
||||
expect(bounds(geo).min.y).toBeCloseTo(-CARRIAGEWAY_DEPTH + MARKING_OFFSET);
|
||||
expect(bounds(geo).max.y).toBeCloseTo(-CARRIAGEWAY_DEPTH + MARKING_OFFSET);
|
||||
});
|
||||
|
||||
it("legt Markierungen, die ganz auf einer Brücke liegen, auf das Deck und alle anderen in die Mulde", () => {
|
||||
const strich = (x) => ({ poly: [[[x, 5], [x + 3, 5], [x + 3, 5.1], [x, 5.1]]], art: "gelb_gestrichelt" });
|
||||
const out = buildChunkGeometry(chunk({
|
||||
bridges: [{ poly: [[[0, 0], [20, 0], [20, 20], [0, 20]]] }],
|
||||
markings: [strich(5), strich(18), strich(30)], // ganz auf der Brücke, halb, daneben
|
||||
}));
|
||||
const deck = bounds(out.bridges).max.y;
|
||||
const pos = out.markings.getAttribute("position");
|
||||
expect(pos.count).toBe(12);
|
||||
for (let i = 0; i < 4; i++) expect(pos.getY(i), "ganz auf der Brücke").toBeCloseTo(deck + MARKING_OFFSET);
|
||||
for (let i = 4; i < 12; i++) expect(pos.getY(i), "halb oder daneben").toBeCloseTo(-CARRIAGEWAY_DEPTH + MARKING_OFFSET);
|
||||
});
|
||||
|
||||
it("lädt alte Chunks ohne die neuen Schlüssel weiter", () => {
|
||||
const alt = { chunk: [0, 0], origin: [0, 0], land: [], surfaces: [],
|
||||
buildings: [], palms: [] };
|
||||
expect(() => buildChunkGeometry(alt)).not.toThrow();
|
||||
expect(buildChunkGeometry(alt).carriageway).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user