feat(world): Materialien, Kollision und Laternen für die neuen Straßen
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ca4f7161af
commit
25248b3929
8 files changed
+205
-110
No files matched your search
+38
-19
@@ -1,30 +1,35 @@
|
|||||||
import { DataTexture, MeshStandardMaterial, RepeatWrapping, RGBAFormat, SRGBColorSpace, TextureLoader,
|
import { DataTexture, DoubleSide, MeshStandardMaterial, RepeatWrapping, RGBAFormat, SRGBColorSpace, TextureLoader,
|
||||||
Vector2 } from "three";
|
Vector2 } from "three";
|
||||||
|
|
||||||
export const MATERIAL_KEYS = ["ground", "sand", "park", "urban", "field", "court", "pool", "parking", "paths",
|
export const MATERIAL_KEYS = ["ground", "sand", "park", "urban", "field", "court", "pool", "parking",
|
||||||
"roads", "bridges", "buildings"];
|
"paths", "carriageway", "curb", "sidewalk", "markings", "lots",
|
||||||
|
"bridges", "buildings"];
|
||||||
export const TEXTURES = {
|
export const TEXTURES = {
|
||||||
ground: "grass_path_2", // unbebautes Land
|
ground: "grass_path_2", // unbebautes Land
|
||||||
sand: "sand_01", // Strand
|
sand: "sand_01", // Strand
|
||||||
park: "leafy_grass", // Grünflächen
|
park: "leafy_grass", // Grünflächen
|
||||||
urban: "concrete_pavement_03", // versiegelte Stadtfläche
|
urban: "concrete_pavement_03", // versiegelte Stadtfläche
|
||||||
// Flamingo Park (docs/superpowers/specs/2026-09-15-flamingo-park-design.md): Felder im Gras des Parks, der
|
// Flamingo Park (docs/superpowers/specs/2026-09-15-flamingo-park-design.md): Felder im Gras des Parks, der
|
||||||
// Parkplatz im Asphalt der Straßen. Beide lädt createMaterials nicht neu, sondern klont park und roads.
|
// Parkplatz im Asphalt der Straßen. Beide lädt createMaterials nicht neu, sondern klont park und carriageway.
|
||||||
field: "leafy_grass",
|
field: "leafy_grass",
|
||||||
parking: "asphalt_02",
|
parking: "asphalt_02",
|
||||||
paths: "concrete_pavement", // helle Betonplatten, auch für die Gehwege aus Teil 2 gedacht
|
paths: "concrete_pavement", // helle Betonplatten, auch für die Gehwege aus Teil 2 gedacht
|
||||||
roads: "asphalt_02",
|
carriageway: "asphalt_02",
|
||||||
// Ruling 56: eine Brücke ist in den Daten dasselbe Straßenstück, nur über Wasser
|
lots: "asphalt_02", // Parkplätze und Vorplätze: derselbe Asphalt wie die Fahrbahn
|
||||||
// (geometry.js trennt sie einzig über roads.filter(r => r.bridge)) — dieselbe
|
curb: "curb", // modular_sidewalk_curb_kit (Just8, CC BY 4.0), s. tools/assets/textures.mjs
|
||||||
// Textur wie roads, sonst zeigt jeder Straße-Brücke-Übergang eine Farbnaht.
|
sidewalk: "sidewalk",
|
||||||
|
// Ruling 56: eine Brücke ist in den Daten dasselbe Straßenstück, nur über Wasser — dieselbe
|
||||||
|
// Textur wie die Fahrbahn, sonst zeigt jeder Straße-Brücke-Übergang eine Farbnaht.
|
||||||
bridges: "asphalt_02",
|
bridges: "asphalt_02",
|
||||||
buildings: "white_stucco",
|
buildings: "white_stucco",
|
||||||
};
|
};
|
||||||
// Plätze und Pool sind einfarbig: Sandplätze, Hartplätze und Poolwasser hat Poly Haven nicht.
|
// Plätze und Pool sind einfarbig: Sandplätze, Hartplätze und Poolwasser hat Poly Haven nicht.
|
||||||
export const SURFACE_COLORS = { court: 0x6f8c79, pool: 0x3f9fd8 };
|
// Die Markierung trägt ihre Farbe je Art als Vertexfarbe in der Geometrie; das Material ist nur der Träger.
|
||||||
|
export const SURFACE_COLORS = { court: 0x6f8c79, pool: 0x3f9fd8, markings: 0xffffff };
|
||||||
const FALLBACK_COLORS = {
|
const FALLBACK_COLORS = {
|
||||||
ground: 0xa8b878, sand: 0xe8d888, park: 0x8bc34a, urban: 0xd8d8d8, paths: 0xd8d2c4,
|
ground: 0xa8b878, sand: 0xe8d888, park: 0x8bc34a, urban: 0xd8d8d8, paths: 0xd8d2c4,
|
||||||
roads: 0x505050, bridges: 0xb0b0b0, buildings: 0xe8e4dc,
|
carriageway: 0x505050, lots: 0x505050, curb: 0xbdbdbd, sidewalk: 0xd8d2c4,
|
||||||
|
bridges: 0xb0b0b0, buildings: 0xe8e4dc,
|
||||||
};
|
};
|
||||||
// Ruling 55: asphalt_02 misst roh heller (Ø 89.5) als concrete_pavement_03 (Ø 65.9) —
|
// Ruling 55: asphalt_02 misst roh heller (Ø 89.5) als concrete_pavement_03 (Ø 65.9) —
|
||||||
// Stadtflächen wirkten dadurch wie dunkle Flecken auf der Straße, das Straßennetz
|
// Stadtflächen wirkten dadurch wie dunkle Flecken auf der Straße, das Straßennetz
|
||||||
@@ -41,7 +46,7 @@ const PARK_TINT = 0xb4dc94;
|
|||||||
// im Rasen. material.color kann nur abdunkeln, also Rot und Blau stärker als Grün.
|
// im Rasen. material.color kann nur abdunkeln, also Rot und Blau stärker als Grün.
|
||||||
const FIELD_TINT = 0xc8f0b0;
|
const FIELD_TINT = 0xc8f0b0;
|
||||||
// Klone statt zweiter Ladevorgang: dieselben Texturen, eigene Tönung und eigener polygonOffset.
|
// Klone statt zweiter Ladevorgang: dieselben Texturen, eigene Tönung und eigener polygonOffset.
|
||||||
const CLONES = { field: { from: "park", tint: FIELD_TINT }, parking: { from: "roads" } };
|
const CLONES = { field: { from: "park", tint: FIELD_TINT }, parking: { from: "carriageway" } };
|
||||||
|
|
||||||
/** Kachelbare Wellen-Normalmap, weil Poly Haven keine Wassertextur hat. */
|
/** Kachelbare Wellen-Normalmap, weil Poly Haven keine Wassertextur hat. */
|
||||||
export function waterNormalData(size = 128) {
|
export function waterNormalData(size = 128) {
|
||||||
@@ -138,14 +143,26 @@ async function loadSet(loader, slug, fallback, { tint, maxAnisotropy } = {}) {
|
|||||||
//
|
//
|
||||||
// Flamingo Park: Felder, Plätze, Pool und Parkplatz liegen über dem Rasen, die Wege über allen Flächen
|
// Flamingo Park: Felder, Plätze, Pool und Parkplatz liegen über dem Rasen, die Wege über allen Flächen
|
||||||
// und unter den Straßen, die sie an der Parkkante berühren.
|
// und unter den Straßen, die sie an der Parkkante berühren.
|
||||||
export const POLYGON_OFFSET_ORDER = ["ground", "urban", "park", "sand", "field", "court", "pool", "parking", "paths", "roads"];
|
//
|
||||||
|
// Teil 2: Gehweg und Restflächen liegen als Beläge auf SURFACE_OFFSET und reihen sich unter den
|
||||||
|
// Wegen ein. Fahrbahn und Bordstein brauchen keinen Versatz, weil sie 15 cm tiefer als echte
|
||||||
|
// Geometrie liegen und mit nichts koplanar sind. Die Markierung steht nicht in der Rangfolge: Sie
|
||||||
|
// liegt 1 cm über der Fahrbahn und berührt keinen Belag, ihr einziger Nachbar hat Versatz 0. Die
|
||||||
|
// oberste Stufe zöge sie beim streifenden Blick am weitesten nach vorn, ohne dass es etwas nützte,
|
||||||
|
// deshalb ein eigener Versatz von -1. Die Rangfolge wächst damit nur um eine Stufe; die Wege
|
||||||
|
// stehen jetzt auf -10.
|
||||||
|
export const POLYGON_OFFSET_ORDER = ["ground", "urban", "park", "sand", "field", "court", "pool",
|
||||||
|
"parking", "lots", "sidewalk", "paths"];
|
||||||
|
|
||||||
export const POLYGON_OFFSETS = Object.fromEntries(
|
export const POLYGON_OFFSETS = {
|
||||||
POLYGON_OFFSET_ORDER.map((key, i) => [
|
...Object.fromEntries(
|
||||||
key,
|
POLYGON_OFFSET_ORDER.map((key, i) => [
|
||||||
{ polygonOffset: true, polygonOffsetFactor: -i, polygonOffsetUnits: -i },
|
key,
|
||||||
]),
|
{ polygonOffset: true, polygonOffsetFactor: -i, polygonOffsetUnits: -i },
|
||||||
);
|
]),
|
||||||
|
),
|
||||||
|
markings: { polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 },
|
||||||
|
};
|
||||||
|
|
||||||
/** Trägt POLYGON_OFFSETS in die passenden Materialien ein. Nur die benannten Schichten —
|
/** Trägt POLYGON_OFFSETS in die passenden Materialien ein. Nur die benannten Schichten —
|
||||||
* Brücken (eigene, geometrische Lösung, s. geometry.js BRIDGE-Kommentar), Gebäude, Wasser und
|
* Brücken (eigene, geometrische Lösung, s. geometry.js BRIDGE-Kommentar), Gebäude, Wasser und
|
||||||
@@ -159,7 +176,7 @@ export function applyPolygonOffsets(materials) {
|
|||||||
|
|
||||||
export async function createMaterials(maxAnisotropy) {
|
export async function createMaterials(maxAnisotropy) {
|
||||||
const loader = new TextureLoader();
|
const loader = new TextureLoader();
|
||||||
const tints = { roads: ROAD_TINT, bridges: ROAD_TINT, park: PARK_TINT };
|
const tints = { carriageway: ROAD_TINT, lots: ROAD_TINT, bridges: ROAD_TINT, park: PARK_TINT };
|
||||||
const textured = MATERIAL_KEYS.filter((key) => !CLONES[key] && !SURFACE_COLORS[key]);
|
const textured = MATERIAL_KEYS.filter((key) => !CLONES[key] && !SURFACE_COLORS[key]);
|
||||||
const entries = await Promise.all(
|
const entries = await Promise.all(
|
||||||
textured.map(async (key) =>
|
textured.map(async (key) =>
|
||||||
@@ -174,6 +191,8 @@ export async function createMaterials(maxAnisotropy) {
|
|||||||
materials[key] = new MeshStandardMaterial({ color, roughness: key === "pool" ? 0.25 : 0.9, metalness: 0 });
|
materials[key] = new MeshStandardMaterial({ color, roughness: key === "pool" ? 0.25 : 0.9, metalness: 0 });
|
||||||
}
|
}
|
||||||
materials.buildings.vertexColors = true; // pastellige Tönung je Gebäude
|
materials.buildings.vertexColors = true; // pastellige Tönung je Gebäude
|
||||||
|
materials.markings.vertexColors = true; // Farbe je Markierungsart, s. geometry.js MARKING_COLORS
|
||||||
|
materials.curb.side = DoubleSide; // welche Seite der Wand außen liegt, weiß die Linie nicht
|
||||||
applyPolygonOffsets(materials);
|
applyPolygonOffsets(materials);
|
||||||
|
|
||||||
const normal = new DataTexture(waterNormalData(128), 128, 128, RGBAFormat);
|
const normal = new DataTexture(waterNormalData(128), 128, 128, RGBAFormat);
|
||||||
|
|||||||
+38
-30
@@ -6,7 +6,9 @@ import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js";
|
|||||||
|
|
||||||
const PALM_MODEL_HEIGHT = 439.74; // Einheiten im OBJ, gemessen
|
const PALM_MODEL_HEIGHT = 439.74; // Einheiten im OBJ, gemessen
|
||||||
const TRUNK_RADIUS = 0.22;
|
const TRUNK_RADIUS = 0.22;
|
||||||
const LAMP_VERGE = [6, 8, 10, 12, 14, 16]; // Meter, von innen nach außen (Ruling 60)
|
// Abstände zur Mittellinie, in denen ein Platz auf dem Gehweg gesucht wird. Von innen nach
|
||||||
|
// außen, damit die Laterne so dicht wie möglich an der Bordsteinkante steht.
|
||||||
|
export const LAMP_VERGE = [4, 5, 6, 7, 8, 9, 10, 11, 12];
|
||||||
|
|
||||||
// Befund B (Fix-Runde 3): street_lamp_02.glb ist mit `npx @gltf-transform/cli@4.5.0 inspect`
|
// Befund B (Fix-Runde 3): street_lamp_02.glb ist mit `npx @gltf-transform/cli@4.5.0 inspect`
|
||||||
// vermessen nur 1,65736 m hoch (bboxMax.y 1.26478 - bboxMin.y -0.39258) — Poly Haven selbst
|
// vermessen nur 1,65736 m hoch (bboxMax.y 1.26478 - bboxMin.y -0.39258) — Poly Haven selbst
|
||||||
@@ -77,7 +79,7 @@ function inRing(x, z, ring) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Liegt (x, z) auf der Fläche, die dieses Ringpaket beschreibt — Außenring zuerst, danach
|
* Liegt (x, z) auf der Fläche, die dieses Ringpaket beschreibt — Außenring zuerst, danach
|
||||||
* Löcher (wie chunk.roads[i].poly, Task 6)? Auf der Fläche heißt: im Außenring und in
|
* Löcher (wie chunk.sidewalk[i].poly)? Auf der Fläche heißt: im Außenring und in
|
||||||
* keinem der Löcher.
|
* keinem der Löcher.
|
||||||
*/
|
*/
|
||||||
export function pointInPolygon(x, z, rings) {
|
export function pointInPolygon(x, z, rings) {
|
||||||
@@ -85,38 +87,46 @@ export function pointInPolygon(x, z, rings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Versetzt lampPoints()-Punkte seitlich von der Fahrbahn (Ruling 60). lampPoints kennt nur
|
* Laternen auf den Gehweg.
|
||||||
* die Mittellinie und setzt jeden Punkt genau darauf — ohne diesen Schritt stünde jede
|
|
||||||
* Laterne mitten auf der Fahrbahn. Für jeden Punkt wird senkrecht zur Blickrichtung
|
|
||||||
* (Normale zu (cos angle, sin angle) ist (-sin angle, cos angle)) von innen nach außen
|
|
||||||
* durch LAMP_VERGE probiert; die Seite wechselt von Punkt zu Punkt (gerader Index links,
|
|
||||||
* ungerader rechts), das halbiert die Zahl je Straßenseite und wirkt weniger wie eine
|
|
||||||
* durchgehende Reihe. Der erste Abstand, dessen Punkt außerhalb aller Straßenpolygone des
|
|
||||||
* Chunks liegt, gewinnt; findet keiner der sechs einen freien Punkt, entfällt die Laterne
|
|
||||||
* ganz — wie bei den Palmen (Ruling 22) kein Sonderfall, sondern die vorgesehene Reaktion
|
|
||||||
* auf eine Straße, die breiter ist als der Suchbereich.
|
|
||||||
*
|
*
|
||||||
* Bewusst nicht in der Python-Pipeline (Ruling 60): chunk.roads kennt nur die Straßen
|
* Vorher wählte vergeLampPoints() die Straßenseite über den Listenindex (gerade links, ungerade
|
||||||
* dieses einen Chunks, nicht die des Nachbarn — nahe einer Chunk-Grenze kann eine Laterne
|
* rechts) und verwarf, was auf der Fahrbahn landete. Das hing an der Reihenfolge der Mittellinien in
|
||||||
* deshalb auf der Fahrbahn stehen bleiben, obwohl eine Straße im Nachbarchunk sie deckt.
|
* der Datei — beim Sortieren der Ausgabe wechselten 1.848 von 3.921 Laternen die Seite, weshalb die
|
||||||
* Bekannter, hingenommener Rest (siehe Bericht für Zahlen an echten Daten).
|
* Mittellinien als einzige Schicht unsortiert bleiben mussten. Jetzt entscheidet die Gehwegfläche:
|
||||||
|
* Senkrecht zur Blickrichtung (Normale zu (cos angle, sin angle) ist (-sin angle, cos angle)) wird
|
||||||
|
* erst links, dann rechts von innen nach außen durch LAMP_VERGE probiert, und wo kein Gehweg ist,
|
||||||
|
* steht keine Laterne. Das Ergebnis hängt an der Geometrie statt an der Listenposition.
|
||||||
*/
|
*/
|
||||||
export function vergeLampPoints(points, roads) {
|
export function sidewalkLampPoints(points, sidewalk) {
|
||||||
const out = [];
|
const out = [];
|
||||||
points.forEach((p, i) => {
|
for (const p of points) {
|
||||||
const side = i % 2 === 0 ? 1 : -1;
|
let placed = null;
|
||||||
const nx = -Math.sin(p.angle) * side, nz = Math.cos(p.angle) * side;
|
for (const side of [1, -1]) {
|
||||||
for (const verge of LAMP_VERGE) {
|
const nx = -Math.sin(p.angle) * side, nz = Math.cos(p.angle) * side;
|
||||||
const x = p.x + nx * verge, z = p.z + nz * verge;
|
for (const verge of LAMP_VERGE) {
|
||||||
if (!roads.some((r) => pointInPolygon(x, z, r.poly))) {
|
const x = p.x + nx * verge, z = p.z + nz * verge;
|
||||||
out.push({ x, z, angle: p.angle });
|
if (sidewalk.some((s) => pointInPolygon(x, z, s.poly))) {
|
||||||
return;
|
placed = { x, z, angle: p.angle };
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (placed) break;
|
||||||
}
|
}
|
||||||
});
|
if (placed) out.push(placed);
|
||||||
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Laternen eines Chunks: im Abstand von lampPoints() entlang der Straßenkanten, jede auf den
|
||||||
|
* Gehweg daneben. Eine eigene Funktion, damit ein Test prüfen kann, dass aus einem Chunk im Format
|
||||||
|
* von Teil 2 wirklich Laternen entstehen — nach dem Formatwechsel las #buildLamps Schlüssel, die es
|
||||||
|
* nicht mehr gab, und setzte ohne jeden Fehler keine einzige Laterne.
|
||||||
|
*/
|
||||||
|
export function chunkLampPoints(chunk) {
|
||||||
|
return sidewalkLampPoints(lampPoints((chunk.edges ?? []).map((e) => e.line)), chunk.sidewalk ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
export function palmTransforms(points, seed = 1) {
|
export function palmTransforms(points, seed = 1) {
|
||||||
return points.map(([x, z]) => {
|
return points.map(([x, z]) => {
|
||||||
const key = `${x.toFixed(1)},${z.toFixed(1)}`;
|
const key = `${x.toFixed(1)},${z.toFixed(1)}`;
|
||||||
@@ -220,16 +230,14 @@ export class PropLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Laternen entlang der Mittellinien (lampPoints), seitlich zur Fahrbahnkante versetzt
|
* Laternen auf dem Gehweg entlang der Straßenkanten (chunkLampPoints). Skalierung
|
||||||
* (vergeLampPoints, Ruling 60 — Fix-Runde 1; siehe dort für die Begründung). Skalierung
|
|
||||||
* auf STREET_LAMP_HEIGHT und die Fuß-Anhebung stecken bereits in this.lampParts[*].matrix
|
* auf STREET_LAMP_HEIGHT und die Fuß-Anhebung stecken bereits in this.lampParts[*].matrix
|
||||||
* (s. load(), Befund B / Fix-Runde 3) — hier wird jede Laterne nur noch bei y = 0 auf den
|
* (s. load(), Befund B / Fix-Runde 3) — hier wird jede Laterne nur noch bei y = 0 auf den
|
||||||
* Boden gesetzt. Anders als bei der Palme keine Zufallsskalierung je Instanz: jede Laterne
|
* Boden gesetzt. Anders als bei der Palme keine Zufallsskalierung je Instanz: jede Laterne
|
||||||
* ist gleich hoch, wie ein echtes Serienprodukt.
|
* ist gleich hoch, wie ein echtes Serienprodukt.
|
||||||
*/
|
*/
|
||||||
#buildLamps(chunk, visual) {
|
#buildLamps(chunk, visual) {
|
||||||
const centerlinePoints = lampPoints(chunk.centerlines ?? []);
|
const points = chunkLampPoints(chunk);
|
||||||
const points = vergeLampPoints(centerlinePoints, chunk.roads ?? []);
|
|
||||||
if (!points.length || !this.lampParts?.length) return;
|
if (!points.length || !this.lampParts?.length) return;
|
||||||
const dummy = new Object3D();
|
const dummy = new Object3D();
|
||||||
const local = new Matrix4();
|
const local = new Matrix4();
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import { buildChunkGeometry } from "./geometry.js";
|
|||||||
import { pointInPolygon } from "./props.js";
|
import { pointInPolygon } from "./props.js";
|
||||||
|
|
||||||
const key = (i, j) => `${i}_${j}`;
|
const key = (i, j) => `${i}_${j}`;
|
||||||
const DECALS = new Set(["roads", "sand", "park", "urban", "field", "court", "pool", "parking", "paths"]);
|
// Fahrbahn und Bordstein sind seit Teil 2 Kollisionsgeometrie — die Fahrbahn, weil man in der Mulde
|
||||||
|
// fährt, der Bordstein, weil er die Stufe zum Gehweg ist. Gemessen wurde, dass die Kapsel 15 und
|
||||||
|
// 25 cm ohne Halt erklimmt und erst bei 40 cm blockiert.
|
||||||
|
const DECALS = new Set(["sand", "park", "urban", "field", "court", "pool", "parking",
|
||||||
|
"paths", "sidewalk", "markings", "lots"]);
|
||||||
|
|
||||||
/** Mittelpunkt eines Chunks in three-Koordinaten. */
|
/** Mittelpunkt eines Chunks in three-Koordinaten. */
|
||||||
function chunkCenter(i, j) {
|
function chunkCenter(i, j) {
|
||||||
@@ -89,7 +93,8 @@ export class ChunkStreamer {
|
|||||||
const mesh = new Mesh(geo, this.materials[name]);
|
const mesh = new Mesh(geo, this.materials[name]);
|
||||||
mesh.castShadow = name === "buildings";
|
mesh.castShadow = name === "buildings";
|
||||||
mesh.receiveShadow = true;
|
mesh.receiveShadow = true;
|
||||||
// Straßen und Bodenflächen sind reine Auflagen; Kollision trägt die Landplatte.
|
// Auflagen sind Beläge, Wege, Gehweg, Markierung und Restflächen; Kollision tragen
|
||||||
|
// Landplatte, Fahrbahn, Bordstein, Brücken und Gebäude.
|
||||||
(DECALS.has(name) ? group : collide).add(mesh);
|
(DECALS.has(name) ? group : collide).add(mesh);
|
||||||
}
|
}
|
||||||
// this.props ist seit Task 8 ein optionales Konstruktorfeld (siehe dortiger
|
// this.props ist seit Task 8 ein optionales Konstruktorfeld (siehe dortiger
|
||||||
|
|||||||
+41
-10
@@ -15,9 +15,9 @@ describe("materials", () => {
|
|||||||
|
|
||||||
it("gibt Feldern das Gras des Parks, dem Parkplatz den Asphalt der Straßen und Wegen Betonplatten", () => {
|
it("gibt Feldern das Gras des Parks, dem Parkplatz den Asphalt der Straßen und Wegen Betonplatten", () => {
|
||||||
expect(TEXTURES.field).toBe(TEXTURES.park);
|
expect(TEXTURES.field).toBe(TEXTURES.park);
|
||||||
expect(TEXTURES.parking).toBe(TEXTURES.roads);
|
expect(TEXTURES.parking).toBe(TEXTURES.carriageway);
|
||||||
expect(TEXTURES.paths).toBe("concrete_pavement");
|
expect(TEXTURES.paths).toBe("concrete_pavement");
|
||||||
expect(Object.keys(SURFACE_COLORS).sort()).toEqual(["court", "pool"]);
|
expect(Object.keys(SURFACE_COLORS).sort()).toEqual(["court", "markings", "pool"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("deckt Strand und Park mit eigenen Materialien ab", () => {
|
it("deckt Strand und Park mit eigenen Materialien ab", () => {
|
||||||
@@ -26,9 +26,8 @@ describe("materials", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("gibt Brücken dieselbe Textur wie Straßen, damit an der Übergangsnaht kein Farbsprung entsteht (Ruling 56)", () => {
|
it("gibt Brücken dieselbe Textur wie Straßen, damit an der Übergangsnaht kein Farbsprung entsteht (Ruling 56)", () => {
|
||||||
// Brücken sind in den Daten dasselbe Straßenstück, nur über Wasser
|
// Brücken sind in den Daten dasselbe Straßenstück, nur über Wasser.
|
||||||
// (geometry.js trennt sie einzig über roads.filter(r => r.bridge)).
|
expect(TEXTURES.bridges).toBe(TEXTURES.carriageway);
|
||||||
expect(TEXTURES.bridges).toBe(TEXTURES.roads);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("erzeugt eine Wellen-Normalmap mit neutraler Grundfarbe", () => {
|
it("erzeugt eine Wellen-Normalmap mit neutraler Grundfarbe", () => {
|
||||||
@@ -47,8 +46,9 @@ describe("materials", () => {
|
|||||||
// Datenstruktur, applyPolygonOffsets() eine reine Funktion auf Platzhalterobjekten — beide
|
// Datenstruktur, applyPolygonOffsets() eine reine Funktion auf Platzhalterobjekten — beide
|
||||||
// brauchen weder createMaterials() noch echtes Texturladen.
|
// brauchen weder createMaterials() noch echtes Texturladen.
|
||||||
describe("Tiefenpuffer-Versatz je Schicht (gegen Flackern zwischen Boden/Stadt/Park/Strand/Straße)", () => {
|
describe("Tiefenpuffer-Versatz je Schicht (gegen Flackern zwischen Boden/Stadt/Park/Strand/Straße)", () => {
|
||||||
it("ordnet die Schichten Boden < Stadtfläche < Park < Strand < Feld < Platz < Pool < Parkplatz < Weg < Straße", () => {
|
it("ordnet die Schichten Boden < Stadtfläche < Park < Strand < Feld < Platz < Pool < Parkplatz < Restfläche < Gehweg < Weg", () => {
|
||||||
expect(POLYGON_OFFSET_ORDER).toEqual(["ground", "urban", "park", "sand", "field", "court", "pool", "parking", "paths", "roads"]);
|
expect(POLYGON_OFFSET_ORDER).toEqual(["ground", "urban", "park", "sand", "field", "court", "pool",
|
||||||
|
"parking", "lots", "sidewalk", "paths"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("aktiviert polygonOffset für jede der fünf Schichten", () => {
|
it("aktiviert polygonOffset für jede der fünf Schichten", () => {
|
||||||
@@ -79,16 +79,47 @@ describe("Tiefenpuffer-Versatz je Schicht (gegen Flackern zwischen Boden/Stadt/P
|
|||||||
it("trägt den Versatz nur in Boden/Stadtfläche/Park/Strand/Straße ein — Brücken, Gebäude, "
|
it("trägt den Versatz nur in Boden/Stadtfläche/Park/Strand/Straße ein — Brücken, Gebäude, "
|
||||||
+ "Wasser und Meeresboden bleiben unberührt (eigene Materialobjekte, s. Auftrag)", () => {
|
+ "Wasser und Meeresboden bleiben unberührt (eigene Materialobjekte, s. Auftrag)", () => {
|
||||||
const fake = Object.fromEntries(
|
const fake = Object.fromEntries(
|
||||||
["ground", "urban", "park", "sand", "field", "court", "pool", "parking", "paths", "roads",
|
["ground", "urban", "park", "sand", "field", "court", "pool", "parking", "lots", "sidewalk",
|
||||||
"bridges", "buildings", "water", "seafloor"]
|
"paths", "markings", "carriageway", "curb", "bridges", "buildings", "water", "seafloor"]
|
||||||
.map((k) => [k, {}]),
|
.map((k) => [k, {}]),
|
||||||
);
|
);
|
||||||
applyPolygonOffsets(fake);
|
applyPolygonOffsets(fake);
|
||||||
for (const key of POLYGON_OFFSET_ORDER) {
|
for (const key of POLYGON_OFFSET_ORDER) {
|
||||||
expect(fake[key]).toMatchObject(POLYGON_OFFSETS[key]);
|
expect(fake[key]).toMatchObject(POLYGON_OFFSETS[key]);
|
||||||
}
|
}
|
||||||
for (const key of ["bridges", "buildings", "water", "seafloor"]) {
|
for (const key of ["carriageway", "curb", "bridges", "buildings", "water", "seafloor"]) {
|
||||||
expect(fake[key]).toEqual({});
|
expect(fake[key]).toEqual({});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Schichten aus Teil 2", () => {
|
||||||
|
it("hat für jede neue Schicht ein Material", () => {
|
||||||
|
for (const key of ["carriageway", "curb", "sidewalk", "markings", "lots"]) {
|
||||||
|
expect(MATERIAL_KEYS).toContain(key);
|
||||||
|
}
|
||||||
|
expect(MATERIAL_KEYS).not.toContain("roads");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("nimmt Bordstein und Gehweg aus dem Kit", () => {
|
||||||
|
expect(TEXTURES.curb).toBe("curb");
|
||||||
|
expect(TEXTURES.sidewalk).toBe("sidewalk");
|
||||||
|
expect(TEXTURES.carriageway).toBe("asphalt_02");
|
||||||
|
expect(TEXTURES.lots).toBe("asphalt_02");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gibt der Markierung eine Farbe statt einer Textur", () => {
|
||||||
|
expect(SURFACE_COLORS.markings).toBeDefined();
|
||||||
|
expect(TEXTURES.markings).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gibt der Markierung einen eigenen kleinen Versatz statt der obersten Stufe der Beläge", () => {
|
||||||
|
// Ihr einziger Nachbar ist die Fahrbahn 1 cm darunter, und die hat keinen Versatz. Die oberste
|
||||||
|
// Stufe zöge sie beim streifenden Blick am weitesten nach vorn, ohne dass es etwas nützte.
|
||||||
|
expect(POLYGON_OFFSET_ORDER).not.toContain("markings");
|
||||||
|
expect(POLYGON_OFFSETS.markings).toEqual({ polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 });
|
||||||
|
// Fahrbahn und Bordstein liegen 15 cm tiefer und sind echte Geometrie — kein Versatz nötig.
|
||||||
|
expect(POLYGON_OFFSETS.carriageway).toBeUndefined();
|
||||||
|
expect(POLYGON_OFFSETS.curb).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
|
|||||||
import { describe, expect, it } from "vitest";
|
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 { chunkLampPoints, pointInPolygon } from "../src/world/props.js";
|
||||||
import { withoutReplacedBuildings } from "../src/world/streaming.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.
|
||||||
@@ -72,7 +72,9 @@ describe.each(PLACES.map((place) => [place.id, place]))("Ort %s auf der Karte",
|
|||||||
const c = chunk(i, j);
|
const c = chunk(i, j);
|
||||||
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));
|
// Straße ist seit Teil 2 alles, was früher der Korridor war: Fahrbahn, Gehweg und Restflächen.
|
||||||
|
const onRoad = [...(c.carriageway ?? []), ...(c.sidewalk ?? []), ...(c.lots ?? [])]
|
||||||
|
.some((road) => pointInPolygon(u, v, road.poly));
|
||||||
const inBuilding = standingBuildings(i, j).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;
|
||||||
});
|
});
|
||||||
@@ -106,7 +108,7 @@ describe.each(PLACES.map((place) => [place.id, place]))("Ort %s auf der Karte",
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 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 12 m neben ihrer Straßenkante (LAMP_VERGE), deshalb zählen auch die Nachbarchunks.
|
||||||
it("enthält keine Palme und keine Laterne", () => {
|
it("enthält keine Palme und keine Laterne", () => {
|
||||||
const nearby = new Set();
|
const nearby = new Set();
|
||||||
for (const [i, j] of under) {
|
for (const [i, j] of under) {
|
||||||
@@ -120,7 +122,7 @@ describe.each(PLACES.map((place) => [place.id, place]))("Ort %s auf der Karte",
|
|||||||
const c = chunk(...key.split("_").map(Number));
|
const c = chunk(...key.split("_").map(Number));
|
||||||
const toWorld = (u, v) => ({ X: c.origin[0] + u, Y: -(c.origin[1] + v) });
|
const toWorld = (u, v) => ({ X: c.origin[0] + u, Y: -(c.origin[1] + v) });
|
||||||
const palms = (c.palms ?? []).map(([u, v]) => toWorld(u, v));
|
const palms = (c.palms ?? []).map(([u, v]) => toWorld(u, v));
|
||||||
const lamps = vergeLampPoints(lampPoints(c.centerlines ?? []), c.roads ?? []).map((p) => toWorld(p.x, p.z));
|
const lamps = chunkLampPoints(c).map((p) => toWorld(p.x, p.z));
|
||||||
return [...palms, ...lamps];
|
return [...palms, ...lamps];
|
||||||
});
|
});
|
||||||
expect(props.filter(({ X, Y }) => insideBounds(place, X, Y))).toEqual([]);
|
expect(props.filter(({ X, Y }) => insideBounds(place, X, Y))).toEqual([]);
|
||||||
|
|||||||
+48
-33
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { STREET_LAMP_HEIGHT, lampFit, lampPoints, palmTransforms, pointInPolygon, vergeLampPoints } from "../src/world/props.js";
|
import { STREET_LAMP_HEIGHT, chunkLampPoints, lampFit, lampPoints, palmTransforms, pointInPolygon,
|
||||||
|
sidewalkLampPoints } from "../src/world/props.js";
|
||||||
|
|
||||||
describe("props", () => {
|
describe("props", () => {
|
||||||
it("setzt Laternen im gewünschten Abstand entlang der Linie", () => {
|
it("setzt Laternen im gewünschten Abstand entlang der Linie", () => {
|
||||||
@@ -66,43 +67,57 @@ describe("pointInPolygon", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("vergeLampPoints", () => {
|
describe("Laternen auf dem Gehweg", () => {
|
||||||
it("versetzt eine Laterne von der Fahrbahn zum ersten freien Abstand, senkrecht zur Linie", () => {
|
// Straße entlang der x-Achse, angle 0: Die Normale ist (0, 1). Der Gehweg liegt nur auf der Seite
|
||||||
// angle 0 -> Blickrichtung (1, 0), Normale (0, 1). Straße 10 m breit (Halbbreite 5 m):
|
// z > 0, 3,5 bis 6 m neben der Mittellinie — wie bei einer Wohnstraße mit 7 m Fahrbahn.
|
||||||
// der erste Versatz aus LAMP_VERGE (6 m) reicht bereits über den Fahrbahnrand hinaus.
|
const gehweg = [{ poly: [[[-20, 3.5], [20, 3.5], [20, 6], [-20, 6]]] }];
|
||||||
const points = [{ x: 0, z: 0, angle: 0 }];
|
|
||||||
const roads = [{ poly: [[[-50, -5], [50, -5], [50, 5], [-50, 5]]], bridge: false }];
|
it("stellt die Laterne auf den Gehweg, so dicht an die Bordsteinkante wie möglich", () => {
|
||||||
const out = vergeLampPoints(points, roads);
|
const out = sidewalkLampPoints([{ x: 0, z: 0, angle: 0 }], gehweg);
|
||||||
expect(out).toHaveLength(1);
|
expect(out).toHaveLength(1);
|
||||||
expect(out[0].x).toBeCloseTo(0);
|
expect(out[0].x).toBeCloseTo(0);
|
||||||
expect(out[0].z).toBeCloseTo(6);
|
expect(out[0].z).toBeCloseTo(4); // der erste Abstand aus LAMP_VERGE, der auf dem Gehweg liegt
|
||||||
expect(pointInPolygon(out[0].x, out[0].z, roads[0].poly)).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("lässt eine Laterne entfallen, wenn keiner der sechs Abstände die Fahrbahn verlässt", () => {
|
it("sucht auch auf der anderen Straßenseite", () => {
|
||||||
// Halbbreite 20 m > größter Versatz (16 m) -> jede Probe bleibt auf der Fahrbahn.
|
const gegenueber = [{ poly: [[[-20, -6], [20, -6], [20, -3.5], [-20, -3.5]]] }];
|
||||||
const points = [{ x: 0, z: 0, angle: 0 }];
|
const out = sidewalkLampPoints([{ x: 0, z: 0, angle: 0 }], gegenueber);
|
||||||
const roads = [{ poly: [[[-50, -20], [50, -20], [50, 20], [-50, 20]]], bridge: false }];
|
|
||||||
expect(vergeLampPoints(points, roads)).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("wechselt die Versatzseite von Punkt zu Punkt ab", () => {
|
|
||||||
// Ohne Straßen gewinnt sofort der erste (kleinste) Versatz bei jedem Punkt.
|
|
||||||
const points = [{ x: 0, z: 0, angle: 0 }, { x: 10, z: 0, angle: 0 }];
|
|
||||||
const out = vergeLampPoints(points, []);
|
|
||||||
expect(out[0].z).toBeCloseTo(6);
|
|
||||||
expect(out[1].z).toBeCloseTo(-6);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fix-Runde 2, Befund B / Ruling 63: siehe Kommentar bei pointInPolygon — derselbe
|
|
||||||
// ungeprüfte Fall (offene statt geschlossene Ringe) steckte auch hier in jeder Vorrichtung.
|
|
||||||
it("versetzt eine Laterne auch dann, wenn der Straßenring geschlossen ist", () => {
|
|
||||||
const points = [{ x: 0, z: 0, angle: 0 }];
|
|
||||||
const ring = [[-50, -5], [50, -5], [50, 5], [-50, 5]];
|
|
||||||
const roads = [{ poly: [[...ring, ring[0]]], bridge: false }];
|
|
||||||
const out = vergeLampPoints(points, roads);
|
|
||||||
expect(out).toHaveLength(1);
|
expect(out).toHaveLength(1);
|
||||||
expect(out[0].z).toBeCloseTo(6);
|
expect(out[0].z).toBeCloseTo(-4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lässt eine Laterne weg, für die es keinen Gehweg gibt", () => {
|
||||||
|
expect(sidewalkLampPoints([{ x: 0, z: 0, angle: 0 }], [])).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hängt nicht an der Listenposition", () => {
|
||||||
|
// Die alte Fassung wählte die Seite nach dem Index: Der zweite Punkt hätte auf der Seite ohne
|
||||||
|
// Gehweg gesucht und wäre entfallen.
|
||||||
|
const punkte = [{ x: -10, z: 0, angle: 0 }, { x: 10, z: 0, angle: 0 }];
|
||||||
|
const vorwaerts = sidewalkLampPoints(punkte, gehweg);
|
||||||
|
const rueckwaerts = sidewalkLampPoints([...punkte].reverse(), gehweg);
|
||||||
|
expect(vorwaerts).toHaveLength(2);
|
||||||
|
const xs = (out) => out.map((p) => p.x).sort((a, b) => a - b);
|
||||||
|
expect(xs(vorwaerts)).toEqual(xs(rueckwaerts));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("findet den Gehweg auch, wenn sein Ring geschlossen ist (so liegen die Chunk-Dateien vor)", () => {
|
||||||
|
const ring = [[-20, 3.5], [20, 3.5], [20, 6], [-20, 6]];
|
||||||
|
expect(sidewalkLampPoints([{ x: 0, z: 0, angle: 0 }], [{ poly: [[...ring, ring[0]]] }])).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("setzt aus einem Chunk im Format von Teil 2 Laternen auf den Gehweg", () => {
|
||||||
|
// Wohnstraße entlang z = 50, Gehweg auf beiden Seiten; lampPoints() setzt auf 100 m drei Punkte.
|
||||||
|
const chunk = {
|
||||||
|
chunk: [0, 0], origin: [0, 0], land: [], surfaces: [], buildings: [], palms: [],
|
||||||
|
carriageway: [], curb: [], markings: [], lots: [], bridges: [],
|
||||||
|
edges: [{ line: [[0, 50], [100, 50]], klasse: "wohnstrasse", breite: 12, fahrbahn: 7 }],
|
||||||
|
sidewalk: [{ poly: [[[0, 53.5], [100, 53.5], [100, 56], [0, 56]]] },
|
||||||
|
{ poly: [[[0, 44], [100, 44], [100, 46.5], [0, 46.5]]] }],
|
||||||
|
};
|
||||||
|
const out = chunkLampPoints(chunk);
|
||||||
|
expect(out).toHaveLength(3);
|
||||||
|
for (const p of out) expect(chunk.sidewalk.some((s) => pointInPolygon(p.x, p.z, s.poly))).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -239,6 +239,27 @@ describe("ChunkStreamer", () => {
|
|||||||
const group = scene.add.mock.calls[0][0];
|
const group = scene.add.mock.calls[0][0];
|
||||||
expect(group.children.filter((child) => child.isMesh)).toHaveLength(5);
|
expect(group.children.filter((child) => child.isMesh)).toHaveLength(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Teil 2: Fahrbahn und Bordstein sind Kollision — man fährt in der Mulde, und der Bordstein ist die
|
||||||
|
// Stufe zum Gehweg. Gehweg, Markierung und Restflächen bleiben Auflagen.
|
||||||
|
it("legt Fahrbahn und Bordstein in die Kollision, Gehweg, Markierung und Restfläche als Auflagen in die Szene", async () => {
|
||||||
|
const square = [[[0, 0], [20, 0], [20, 20], [0, 20]]];
|
||||||
|
const chunk = {
|
||||||
|
chunk: [4, 6], origin: [0, -1000], land: [square], surfaces: [], buildings: [],
|
||||||
|
carriageway: [{ poly: [[[0, 20], [20, 20], [20, 30], [0, 30]]], klasse: "wohnstrasse" }],
|
||||||
|
curb: [[[0, 20], [20, 20]]],
|
||||||
|
sidewalk: [{ poly: [[[0, 17], [20, 17], [20, 20], [0, 20]]] }],
|
||||||
|
markings: [{ poly: [[[2, 25], [5, 25], [5, 25.1], [2, 25.1]]], art: "gelb_gestrichelt" }],
|
||||||
|
lots: [{ poly: [[[0, 0], [5, 0], [5, 5], [0, 5]]] }],
|
||||||
|
};
|
||||||
|
const { streamer, player, scene } = makeStreamer(chunk);
|
||||||
|
streamer.update(emptyChunkCenter);
|
||||||
|
await flush();
|
||||||
|
|
||||||
|
expect(player.addCollider.mock.calls[0][0].shape.source.children).toHaveLength(3); // Landplatte, Fahrbahn, Bordstein
|
||||||
|
const group = scene.add.mock.calls[0][0];
|
||||||
|
expect(group.children.filter((child) => child.isMesh)).toHaveLength(3); // Gehweg, Markierung, Restfläche
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fix-Runde 2, Befund C / Ruling 64: der Zweig `if (this.props)` in #load hatte keine
|
// Fix-Runde 2, Befund C / Ruling 64: der Zweig `if (this.props)` in #load hatte keine
|
||||||
|
|||||||
@@ -74,18 +74,12 @@ def _sorted_surfaces(entries: list[dict]) -> list[dict]:
|
|||||||
def _line(line, i: int, j: int) -> list[list[float]]:
|
def _line(line, i: int, j: int) -> list[list[float]]:
|
||||||
"""Ein Linienzug in Chunk-Koordinaten, richtungsunabhängig festgelegt.
|
"""Ein Linienzug in Chunk-Koordinaten, richtungsunabhängig festgelegt.
|
||||||
|
|
||||||
normalize() dreht eine Linie in eine feste Laufrichtung. Normalisieren und Sortieren ist für
|
normalize() dreht eine Linie in eine feste Laufrichtung, damit dieselbe Linie unabhängig vom
|
||||||
curb und edges nur zulässig, solange keine Laterne ihre Straßenseite aus der Listenposition
|
Rechenweg gleich in der Datei steht. Zulässig ist das, weil kein Verbraucher aus Richtung oder
|
||||||
wählt, sondern aus der Geometrie - Kantenrichtung und eine Prüfung gegen die Gehwegfläche.
|
Listenposition etwas ableitet: Die Laternen (sidewalkLampPoints() in props.js) suchen ihre
|
||||||
Bei den früheren centerlines war das nicht so, deshalb blieben sie unsortiert und
|
Straßenseite an der Gehwegfläche, und die Oberkante des Bordsteins (kerbGeometry() in
|
||||||
unnormalisiert.
|
geometry.js) findet ihre Seite an der Fahrbahnfläche. Bei den früheren centerlines war das
|
||||||
|
nicht so, deshalb blieben sie unsortiert und unnormalisiert.
|
||||||
Eingelöst wird die Voraussetzung erst mit einer geometriebasierten Nachfolgefunktion in
|
|
||||||
props.js (vorgesehen als sidewalkLampPoints(), als Ablösung von vergeLampPoints()). Heute
|
|
||||||
liest dort noch niemand curb oder edges: vergeLampPoints() wählt die Seite weiterhin über den
|
|
||||||
geraden/ungeraden Index (Zeile 107) und läuft auf chunk.centerlines/chunk.roads. Bis zur
|
|
||||||
Ablösung ist das hier deshalb folgenlos - nicht weil die Bedingung gälte, sondern weil noch
|
|
||||||
kein Verbraucher zuhört.
|
|
||||||
"""
|
"""
|
||||||
ox = SLICE["x0"] + i * CHUNK_SIZE
|
ox = SLICE["x0"] + i * CHUNK_SIZE
|
||||||
oy = SLICE["y1"] - j * CHUNK_SIZE
|
oy = SLICE["y1"] - j * CHUNK_SIZE
|
||||||
|
|||||||
Reference in new issue
Block a user