Lane B round-6: place street_bin + bus_shelter GLBs (on the street graph, collision-verified)

Consumes the two orphaned street GLBs via the use-if-ready furniture path (fail-soft; ?noassets →
primitives, 0 network).

- Bins: genuinely sparse (~1/block, s+=78m, seeded side), decoupled from benches — country town not
  a mall. GLB on its 0.57x0.68 footprint.
- Bus shelters ON THE STREET GRAPH (future tram/bus stops): deterministic rule = midpoint of every
  MAIN edge with hashEdge(id)%3===0, road-side footpath by the kerb, long axis along the edge, open
  front to road -> 1-3/town (2 in seed 20261990). Solid oriented-rect collider (furniture now returns
  colliders; chunks.js merges them into getColliders) -> walk-into-it is blocked. Exported
  busShelterStops(plan) -> [{edgeId,x,z,yaw}] as the stop-list contract for the vehicle loop.
- Perf: worst view WITH rig-fleet citizens 94k tris / 148 draws (<=200k / <=300, big margin);
  deterministic; all-flags-on (winmap+dig+roster) composes clean, 0 errors. street_bin GLB is 3k tris
  -> flagged to Lane E as a decimation candidate (bounded by the sparse cadence for now).

qa.sh --strict GREEN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-14 23:04:14 +10:00
parent 35981ade5f
commit 24cef5a7bb
5 changed files with 99 additions and 18 deletions

View File

@ -4,6 +4,21 @@
plus the game shell. It runs on my hand-written fixture **and** auto-integrates with Lane A's
`generatePlan` (which landed mid-session). Everything below was verified live in a browser.
## Round 6 (Fable's ROUND6 → Lane B) — placed the orphaned street props · qa GREEN
Wired the two published-but-unconsumed street GLBs (`street_bin_01`, `bus_shelter_01`) via the
use-if-ready furniture path (fail-soft; `?noassets` → primitives, 0 network). Files: `furniture.js`,
`chunks.js` (collider merge).
- **Bins**: sparse (~1/block, `s += 78 m`, seeded), decoupled from benches — country town not a mall.
- **Bus shelters on the street graph** (future tram/bus stops): deterministic rule = midpoint of every
MAIN edge with `hashEdge(id)%3===0`, road-side footpath, long axis along the edge, open front to road
**13/town** (2 in seed 20261990). **Solid collider** (furniture now returns colliders → `chunks`
merges into `getColliders`); walk-into-it is blocked. **Exposed `busShelterStops(plan)` → the stop
list contract** for whoever builds the vehicle loop.
- **Perf**: worst view WITH rig-fleet citizens **94k tris / 148 draws** (≤200k / ≤300, big margin);
deterministic; all-flags-on composes clean (0 errors). Flagged the 3k-tri `street_bin` GLB to Lane E as
a decimation candidate (bounded by the sparse cadence for now). Shot: `docs/shots/laneB/bus_shelter.png`.
## Round 5 (Fable's ROUND5 → Lane B) — v2 window trick, behind `?winmap=1` · qa GREEN (4/4)
- **Parallax interior-mapping window glass** (V2_IDEAS "Vuntra window trick"), behind **`?winmap=1`

View File

@ -1,5 +1,33 @@
# LANE B — NOTES (measured)
## Round 6 (Fable's ROUND6 → Lane B) — place the orphaned street props
Placed the two published-but-unconsumed street GLBs (`procity_street_bin_01`, `procity_street_bus_shelter_01`)
via the existing use-if-ready furniture GLB path (fail-soft to primitives; `?noassets` → primitives,
**0 network** verified). All in `furniture.js` (+ a 3-line collider merge in `chunks.js`).
- **Bins** — genuinely sparse now (one loop, ~1 per block: `s += 78 m`, seeded side per edge), a country
town not a mall. Decoupled from benches. GLB (3.0k tris) on its 0.57×0.68 footprint, primitive fallback.
- **Bus shelters — on the street graph, and this is the future tram/bus-stop contract.** Deterministic
rule: **the midpoint of every MAIN edge whose `hashEdge(id) % 3 === 0`, on the road-side footpath by the
kerb, long axis along the edge, open front to the road** — yields **13 per town** (2 in seed 20261990).
The GLB (2.33×2.4×1.41, 8.0k tris) gets a **solid oriented-rect collider** so the player rounds it
(furniture now returns `colliders`; `chunks.js` merges them into `getColliders`). Collision-verified: a
straight walk into it is blocked.
- **→ whoever builds the V2 tram/bus loop:** call **`busShelterStops(plan)`** (exported from
`furniture.js`) → `[{ edgeId, x, z, yaw }]`. It re-derives the stops from the exact same rule the
shelters are placed by — pure function of the plan, zero side effects. That is the stop list; don't
re-invent it. To move/space stops, change the one rule + `busShelterStops` together.
- **Perf (measured, worst view WITH the rig-fleet citizens, midday):** **94k tris · 148 draws** — both
well under the ≤200k / ≤300 gates (~100k tri and ~150 draw margin). Determinism holds (seed → same
props). All-flags-on (`?winmap=1&dig=1&roster=stream`) composes clean, 0 console errors. Shot:
`docs/shots/laneB/bus_shelter.png`. `qa.sh --strict` GREEN.
- **→ Lane E (decimation candidate, not blocking):** the `street_bin` GLB is **3.0k tris** — heavy for
a prop placed ~15×; I bounded it with the sparse cadence, but a decimate to ~500 tris would give back
tri headroom if the combined-lane budget tightens. (Same class as the deferred `novelty_record`.)
---
## Round 5 (Fable's ROUND5 → Lane B) — v2: the Vuntra window trick, behind `?winmap=1`
- **Parallax interior-mapping glass** (V2_IDEAS "window trick"). Behind **`?winmap=1`, default-off.**

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 KiB

View File

@ -35,7 +35,10 @@ export function createChunkManager(plan, scene, ctx) {
lastBuildMs = performance.now() - t0;
scene.add(buildings.group, furniture.group);
if (buildings.doorMesh) doorMeshes.push(buildings.doorMesh);
live.set(key, { buildings, furniture, colliders: buildings.colliders, cx, cz });
// player colliders = building/yard rects + any furniture keep-outs (bus shelters)
const colliders = furniture.colliders && furniture.colliders.length
? buildings.colliders.concat(furniture.colliders) : buildings.colliders;
live.set(key, { buildings, furniture, colliders, cx, cz });
// optional lifecycle hook (Lane D/F per-chunk spawning, ambient, LOD — LANE_F_NOTES §8).
// Inert unless a consumer sets ctx.onChunkBuilt; citizens currently drive off plan.streets instead.
if (ctx.onChunkBuilt) ctx.onChunkBuilt(key, { cx, cz, buildings: buildings.group, furniture: furniture.group, data });

View File

@ -15,19 +15,24 @@ import { rng, frange } from '../core/prng.js';
import { chunkOf, resolveEdges } from './planutil.js';
const FOOT = 3.5;
// bus-shelter footprint (from manifest: 2.33 × 1.41 m) + a facing tweak applied to the along-edge
// yaw so the open front reads toward the road (verified in-browser; π flips it if the GLB is mirrored).
const SHELTER_W = 2.33, SHELTER_D = 1.41, SHELTER_YAW = 0;
// ── 3GOD-depot furniture GLB upgrade (fail-soft; ?noassets → primitives only, zero network) ──────
// Each mapped type loads ONCE at module init. buildChunkFurniture checks `_glbReady[type]`
// SYNCHRONOUSLY at build time — so a chunk built before the GLB lands uses its primitive, and every
// chunk built after uses the GLB (streaming rebuilds pick it up). No async write into a live chunk.
const NOASSETS = (() => { try { const p = new URLSearchParams(location.search); return p.has('noassets') && p.get('noassets') !== '0'; } catch { return false; } })();
// Only LIGHT GLBs (≤5k tris/glb_law) are enabled — a heavy prop × many instances blows the ≤200k
// tri gate. Measured: bench 1.9k ✓, food_cart 5.0k ✓, streetlight 5.0k ✓, but novelty_record is
// **26.5k tris** (5× over) → it stays on the low-poly PRIMITIVE until Lane E decimates it (same
// treatment as the ped rigs — see LANE_B_NOTES). Placement (record-store adjacency) is unchanged.
// Only LIGHT GLBs (≤~8k tris) are enabled — a heavy prop × many instances blows the ≤200k tri gate.
// Measured: bench 1.9k ✓, food_cart 5.0k ✓, bin 3.0k ✓, bus_shelter 8.0k ✓ (only 13 per town), but
// novelty_record is **26.5k tris** (5× over) → it stays on the low-poly PRIMITIVE until Lane E
// decimates it (same treatment as the ped rigs — see LANE_B_NOTES).
const GLB_FILES = {
bench: 'procity_street_bench_01.glb',
food_cart: 'procity_street_food_cart_01.glb',
bench: 'procity_street_bench_01.glb',
food_cart: 'procity_street_food_cart_01.glb',
bin: 'procity_street_bin_01.glb',
bus_shelter: 'procity_street_bus_shelter_01.glb',
};
const _glbReady = {}; // type → { geo, mat } once (and if) the GLB loads and merges to one mesh
@ -161,6 +166,7 @@ export function buildChunkFurniture(chunkData, ctx, cx, cz) {
const edges = ctx._edges || (ctx._edges = resolveEdges(ctx.plan));
const lists = { pole: [], lamp: [], bench: [], bin: [], busstop: [], tree: [], novelty: [], foodcart: [] };
const colliders = []; // oriented-rect keep-outs (bus shelters) → player collision (via chunks.js)
const here = (x, z) => { const c = chunkOf(x, z); return c.cx === cx && c.cz === cz; };
const pushYaw = (list, x, y, z, yaw, sx = 1, sy = 1, sz = 1) => {
@ -189,14 +195,21 @@ export function buildChunkFurniture(chunkData, ctx, cx, cz) {
}
side = -side;
}
// benches + bins every ~26m on the building side of the footpath, facing the road
// benches every ~26m on the building side of the footpath, facing the road
for (let s = 14; s < len - 6; s += 26) {
const bside = (Math.floor(s / 26) % 2) ? 1 : -1;
const off = (halfRoad + FOOT - 0.8) * bside;
const bx = e.ax + ux * s + px * off, bz = e.az + uz * s + pz * off;
if (here(bx, bz)) pushYaw(lists.bench, bx, 0, bz, yaw + (bside > 0 ? Math.PI : 0));
const nx = bx + ux * 1.4, nz = bz + uz * 1.4;
if (here(nx, nz)) pushYaw(lists.bin, nx, 0, nz, 0);
}
// bins: genuinely sparse (~1 per block), seeded, one side of the footpath — a country town, not a
// mall. Wide cadence also bounds the 3k-tri bin GLB's contribution to the ≤200k tri gate.
const binH = hashEdge(e.id);
for (let s = 30; s < len - 8; s += 78) {
const bside = ((binH + Math.floor(s / 78)) % 2) ? 1 : -1;
const off = (halfRoad + FOOT - 0.6) * bside;
const x = e.ax + ux * s + px * off, z = e.az + uz * s + pz * off;
if (here(x, z)) pushYaw(lists.bin, x, 0, z, 0);
}
// gum trees only along side/lane verges (main street has no verge room)
if (e.kind === 'side' || e.kind === 'lane') {
@ -208,11 +221,18 @@ export function buildChunkFurniture(chunkData, ctx, cx, cz) {
}
}
}
// one bus stop near each main-street edge's near end
if (e.kind === 'main') {
const s = 10, off = (halfRoad + FOOT - 0.9);
const x = e.ax + ux * s + px * off, z = e.az + uz * s + pz * off;
if (here(x, z)) pushYaw(lists.busstop, x, 0, z, yaw + Math.PI);
// Bus shelters — 13 per town, ON THE STREET GRAPH (they are the future tram/bus stops).
// Deterministic rule (see busShelterStops() + LANE_B_NOTES): at the MIDPOINT of every MAIN edge
// whose id hashes to 0 mod 3, on the road-side footpath near the kerb, long axis (2.33 m) ALONG
// the edge, open front to the road. A solid collider so the player rounds it (collision-verified).
if (e.kind === 'main' && hashEdge(e.id) % 3 === 0) {
const off = halfRoad + 1.7; // on the footpath, hard by the kerb
const sx = e.ax + ux * (len * 0.5) + px * off, sz = e.az + uz * (len * 0.5) + pz * off;
const syaw = Math.atan2(-uz, ux) + SHELTER_YAW; // long axis along the edge (+ facing tweak)
if (here(sx, sz)) {
pushYaw(lists.busstop, sx, 0, sz, syaw);
colliders.push({ x: sx, z: sz, ry: syaw, hw: SHELTER_W / 2, hd: SHELTER_D / 2, use: 'shelter' });
}
}
}
@ -247,8 +267,8 @@ export function buildChunkFurniture(chunkData, ctx, cx, cz) {
emit(t.pole, mat.dark, lists.pole);
emit(t.lamp, mat.lamp, lists.lamp, false);
emit(...glb('bench', t.bench, mat.wood), lists.bench);
emit(t.bin, mat.bin, lists.bin);
emit(t.busstop, mat.shelter, lists.busstop);
emit(...glb('bin', t.bin, mat.bin), lists.bin);
emit(...glb('bus_shelter', t.busstop, mat.shelter), lists.busstop);
emit(t.treeGeo, mat.tree, lists.tree, false);
emit(...glb('novelty_record', t.novelty, mat.novelty), lists.novelty);
emit(...glb('food_cart', t.foodcart, mat.foodcart), lists.foodcart);
@ -261,7 +281,22 @@ export function buildChunkFurniture(chunkData, ctx, cx, cz) {
// shared templates/materials intentionally NOT disposed (reused by other chunks)
group.clear();
}
return { group, applyNight, dispose };
return { group, applyNight, dispose, colliders };
}
function hashEdge(id) { const s = String(id); let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; return h >>> 0; } // String(): generatePlan uses numeric edge ids
// The town's bus/tram stops, derived from the SAME deterministic rule buildChunkFurniture places the
// shelters by, so the future vehicle loop can enumerate stops without touching furniture internals.
// Rule: midpoint of every MAIN edge whose id hashes to 0 mod 3, on the road-side footpath (kerb).
// Returns [{ edgeId, x, z, yaw }] in plan order. Zero side effects; pure function of the plan.
export function busShelterStops(plan) {
const stops = [];
for (const e of resolveEdges(plan)) {
if (e.kind !== 'main' || hashEdge(e.id) % 3 !== 0) continue;
const dx = e.bx - e.ax, dz = e.bz - e.az, len = Math.hypot(dx, dz) || 1;
const ux = dx / len, uz = dz / len, px = -uz, pz = ux, off = e.width / 2 + 1.7;
stops.push({ edgeId: e.id, x: e.ax + ux * (len * 0.5) + px * off, z: e.az + uz * (len * 0.5) + pz * off, yaw: Math.atan2(-uz, ux) + SHELTER_YAW });
}
return stops;
}