fix(world): Laternen vom Fahrbahnrand versetzt, ungenutzte Requisiten entfernt
Ruling 60: vergeLampPoints sucht von 6 bis 16 m nach außen und verwirft eine Laterne ganz, wenn keiner der sechs Abstände die Straßenpolygone des Chunks verlässt (Punkt-in-Polygon mit Löchern, pointInPolygon). Ruling 61: die vier nie platzierten Requisiten (Bank, Mülleimer, Hydrant, Absperrung) fliegen aus props.mjs und aus dem Repository, ~18 MB auf 1,9 MB im Arbeitsverzeichnis. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9173400caa
commit
d6a2f6d269
7 files changed
+120
-23
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+59
-19
@@ -6,6 +6,7 @@ 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)
|
||||||
|
|
||||||
/** Kleiner, wiederholbarer Zufall aus einem Schlüssel. */
|
/** Kleiner, wiederholbarer Zufall aus einem Schlüssel. */
|
||||||
function hashUnit(seed, key) {
|
function hashUnit(seed, key) {
|
||||||
@@ -39,6 +40,58 @@ export function lampPoints(lines, spacing = 35) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Halbstrahl-Verfahren (Ray Casting): liegt (x, z) im Ring? Ring offen oder geschlossen. */
|
||||||
|
function inRing(x, z, ring) {
|
||||||
|
let inside = false;
|
||||||
|
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
||||||
|
const [xi, zi] = ring[i], [xj, zj] = ring[j];
|
||||||
|
if ((zi > z) !== (zj > z) && x < (xj - xi) * (z - zi) / (zj - zi) + xi) inside = !inside;
|
||||||
|
}
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* keinem der Löcher.
|
||||||
|
*/
|
||||||
|
export function pointInPolygon(x, z, rings) {
|
||||||
|
return rings.length > 0 && inRing(x, z, rings[0]) && !rings.slice(1).some((hole) => inRing(x, z, hole));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Versetzt lampPoints()-Punkte seitlich von der Fahrbahn (Ruling 60). lampPoints kennt nur
|
||||||
|
* 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
|
||||||
|
* dieses einen Chunks, nicht die des Nachbarn — nahe einer Chunk-Grenze kann eine Laterne
|
||||||
|
* deshalb auf der Fahrbahn stehen bleiben, obwohl eine Straße im Nachbarchunk sie deckt.
|
||||||
|
* Bekannter, hingenommener Rest (siehe Bericht für Zahlen an echten Daten).
|
||||||
|
*/
|
||||||
|
export function vergeLampPoints(points, roads) {
|
||||||
|
const out = [];
|
||||||
|
points.forEach((p, i) => {
|
||||||
|
const side = i % 2 === 0 ? 1 : -1;
|
||||||
|
const nx = -Math.sin(p.angle) * side, nz = Math.cos(p.angle) * side;
|
||||||
|
for (const verge of LAMP_VERGE) {
|
||||||
|
const x = p.x + nx * verge, z = p.z + nz * verge;
|
||||||
|
if (!roads.some((r) => pointInPolygon(x, z, r.poly))) {
|
||||||
|
out.push({ x, z, angle: p.angle });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
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)}`;
|
||||||
@@ -126,27 +179,14 @@ export class PropLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Laternen entlang der Mittellinien (lampPoints, oben).
|
* Laternen entlang der Mittellinien (lampPoints), seitlich zur Fahrbahnkante versetzt
|
||||||
*
|
* (vergeLampPoints, Ruling 60 — Fix-Runde 1; siehe dort für die Begründung). Das Modell
|
||||||
* Ohne diese Methode wäre lampPoints() eine getestete, aber nirgends aufgerufene Funktion,
|
* ist bei 1,7 m laut Poly-Haven-Angabe bereits maßstäblich, anders als die Palme also
|
||||||
* und this.lamp ein geladenes, nie benutztes Modell — im gesamten Umsetzungsplan ruft keine
|
* keine Zufallsskalierung.
|
||||||
* andere Aufgabe lampPoints() auf. Die Verdrahtung hier verwendet ausschließlich bereits
|
|
||||||
* spezifizierte, getestete Bausteine (lampPoints exakt wie oben, this.lamp aus load()).
|
|
||||||
*
|
|
||||||
* Platzierung exakt auf der Mittellinie, ohne seitlichen Versatz zur Fahrbahnkante: anders
|
|
||||||
* als bei den Palmen (Python-seitige Suche vom Fahrbahnrand nach außen gegen Straßen- und
|
|
||||||
* Gebäudepolygone, Ruling 22) kennen die Chunk-Daten keine separate Gehwegspur — nur
|
|
||||||
* Fahrbahn-, Sand-, Park- und Stadtfläche — und lampPoints(lines, spacing) bekommt bewusst
|
|
||||||
* keine Straßen- oder Gebäudepolygone übergeben. Ein erfundener fester Versatz ließe sich
|
|
||||||
* an keiner echten Geometrie prüfen und wäre nach Ruling 22 vermutlich ebenso falsch wie der
|
|
||||||
* ursprüngliche 4-m-Versatz der Palmen (60 % auf der Fahrbahn). Ruling 18 nimmt "mitten auf
|
|
||||||
* der Fahrbahn" für Laternen bereits ausdrücklich hin ("an einer Kreuzung ... stehen vier
|
|
||||||
* Laternen im selben Punkt, mitten auf der Fahrbahn") und behebt dort nur das Stapeln an
|
|
||||||
* Kreuzungen — genau dafür ist lampPoints() da. Das Modell ist bei 1,7 m laut Poly-Haven-
|
|
||||||
* Angabe bereits maßstäblich, anders als die Palme also keine Zufallsskalierung.
|
|
||||||
*/
|
*/
|
||||||
#buildLamps(chunk, visual) {
|
#buildLamps(chunk, visual) {
|
||||||
const points = lampPoints(chunk.centerlines ?? []);
|
const centerlinePoints = lampPoints(chunk.centerlines ?? []);
|
||||||
|
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();
|
||||||
|
|||||||
+51
-1
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { lampPoints, palmTransforms } from "../src/world/props.js";
|
import { lampPoints, palmTransforms, pointInPolygon, vergeLampPoints } 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", () => {
|
||||||
@@ -32,3 +32,53 @@ describe("props", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fix-Runde 1, Befund A / Ruling 60: Laternen standen exakt auf der Mittellinie.
|
||||||
|
describe("pointInPolygon", () => {
|
||||||
|
// Quadrat 0..20 mit einem 4x4-Loch in der Mitte (8..12) — offene Ringe, wie im Rest
|
||||||
|
// dieser Datei; die Laufzeitdaten schließen ihre Ringe (letzter Punkt = erster), das
|
||||||
|
// Halbstrahl-Verfahren behandelt beide Formen gleich (siehe Kommentar in props.js).
|
||||||
|
const outer = [[0, 0], [20, 0], [20, 20], [0, 20]];
|
||||||
|
const hole = [[8, 8], [12, 8], [12, 12], [8, 12]];
|
||||||
|
|
||||||
|
it("Punkt innen (im Außenring, außerhalb jedes Lochs)", () => {
|
||||||
|
expect(pointInPolygon(2, 2, [outer, hole])).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Punkt außen (außerhalb des Außenrings)", () => {
|
||||||
|
expect(pointInPolygon(25, 25, [outer, hole])).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Punkt im Loch (im Außenring, aber auch im Loch)", () => {
|
||||||
|
expect(pointInPolygon(10, 10, [outer, hole])).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("vergeLampPoints", () => {
|
||||||
|
it("versetzt eine Laterne von der Fahrbahn zum ersten freien Abstand, senkrecht zur Linie", () => {
|
||||||
|
// angle 0 -> Blickrichtung (1, 0), Normale (0, 1). Straße 10 m breit (Halbbreite 5 m):
|
||||||
|
// der erste Versatz aus LAMP_VERGE (6 m) reicht bereits über den Fahrbahnrand hinaus.
|
||||||
|
const points = [{ x: 0, z: 0, angle: 0 }];
|
||||||
|
const roads = [{ poly: [[[-50, -5], [50, -5], [50, 5], [-50, 5]]], bridge: false }];
|
||||||
|
const out = vergeLampPoints(points, roads);
|
||||||
|
expect(out).toHaveLength(1);
|
||||||
|
expect(out[0].x).toBeCloseTo(0);
|
||||||
|
expect(out[0].z).toBeCloseTo(6);
|
||||||
|
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", () => {
|
||||||
|
// Halbbreite 20 m > größter Versatz (16 m) -> jede Probe bleibt auf der Fahrbahn.
|
||||||
|
const points = [{ x: 0, z: 0, angle: 0 }];
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
+10
-3
@@ -1,16 +1,23 @@
|
|||||||
// Lädt die fünf Poly-Haven-Requisiten in 1k (gltf + zugehörige Texturen/`.bin`) nach
|
// Lädt die Poly-Haven-Straßenlaterne in 1k (gltf + zugehörige Texturen/`.bin`) nach
|
||||||
// public/models/props/raw/ und vereinfacht sie danach mit @gltf-transform/cli zu je einer
|
// public/models/props/raw/ und vereinfacht sie danach mit @gltf-transform/cli zu einer
|
||||||
// kompakten .glb unter public/models/props/. Gleiches Muster wie textures.mjs, aber Modelle
|
// kompakten .glb unter public/models/props/. Gleiches Muster wie textures.mjs, aber Modelle
|
||||||
// stehen unter dem Schlüssel "gltf" statt unter den Map-Namen ("Diffuse", "nor_gl", "arm"),
|
// stehen unter dem Schlüssel "gltf" statt unter den Map-Namen ("Diffuse", "nor_gl", "arm"),
|
||||||
// und die 1k-Auflösung liefert neben der .gltf-Datei selbst noch eine `include`-Liste mit den
|
// und die 1k-Auflösung liefert neben der .gltf-Datei selbst noch eine `include`-Liste mit den
|
||||||
// referenzierten Texturen und der gemeinsamen `.bin`-Geometriedatei — ohne die kann
|
// referenzierten Texturen und der gemeinsamen `.bin`-Geometriedatei — ohne die kann
|
||||||
// gltf-transform die .gltf-Datei nicht öffnen.
|
// gltf-transform die .gltf-Datei nicht öffnen.
|
||||||
|
//
|
||||||
|
// Ruling 61 (Fix-Runde 1): ursprünglich fünf Requisiten (Laterne, Bank, Mülleimer, Hydrant,
|
||||||
|
// Absperrung), aber nur street_lamp_02 hat in den Chunk-Daten eine Platzierungsquelle
|
||||||
|
// (chunk.centerlines) — für die anderen vier gibt es weder ein Feld im Chunk-Schema noch
|
||||||
|
// einen Aufrufer irgendwo im Plan. Sie lagen ungenutzt mit rund 18 MB im Arbeitsverzeichnis
|
||||||
|
// (1,2 MB davon versioniert); erfundene Platzierungsdaten wären reines Raten gewesen (siehe
|
||||||
|
// Bericht). Wieder aufnehmen, sobald eine echte Platzierungsquelle für sie entsteht.
|
||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync } from "node:child_process";
|
||||||
import { mkdir, writeFile } from "node:fs/promises";
|
import { mkdir, writeFile } from "node:fs/promises";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
|
|
||||||
const UA = { "User-Agent": "leonida-assets/0.1 (local hobby project)" };
|
const UA = { "User-Agent": "leonida-assets/0.1 (local hobby project)" };
|
||||||
const SLUGS = ["street_lamp_02", "painted_wooden_bench", "metal_trash_can", "fire_hydrant", "concrete_road_barrier_02"];
|
const SLUGS = ["street_lamp_02"];
|
||||||
const RAW_DIR = "public/models/props/raw";
|
const RAW_DIR = "public/models/props/raw";
|
||||||
const OUT_DIR = "public/models/props";
|
const OUT_DIR = "public/models/props";
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user