Lane B R37 (v8 wave 0 §0.1 + §0.4): THE KERB IS FOOTPATH — +0 draws, tris DOWN; the chunk seam is subscribable at last
§0.1 — ground.js adopts Lane A's corridor law. The road quad narrows from the whole
corridor to the carriageway and the EXISTING merged footGeos class extends inward to
meet it, consuming `vergeBand(e)[0]` (not `roadWidth/2` — per A's mid-round correction,
so the geometry and A's corrected gate read the same number).
MEASURED, fresh boots, port-isolated no-store servers (:8178 = HEAD control, :8177 =
treatment), 2560x1440:
synthetic ground 5 draws / 224 tris -> 5 draws / 216 tris
walked spine, 102 steps @ 8 m: worst 276 draws BOTH; sum of draws
20,081 BOTH (identical at every step); 8 bookmarks identical draws
katoomba_real ground 4 draws / 9,932 tris -> 4 draws / 9,644 tris
walk 120 steps: worst 100 draws BOTH; sum 7,703 BOTH
+0 draws is proven, not asserted. Tris go DOWN (-8 / -288): the charter's "~2k tris" was
an estimate against a class that already existed. Honest number, stated loudly.
Surfaces verified by RAYCAST, not by reading the code (offset from centreline, topmost hit):
main@28 road 0-4.75 | kerb 5 | footpath 5.25-17.5 (band [5,14])
main@24 road 0-5 | footpath 5.25-15.25 (band [5,12], real)
side@12 road 0-3 | footpath 3.25-9.5 (band [3,6] — 72% of the corpus)
lane@8 road 0-4 | footpath 4.25-9 DEGENERATE band [4,4] => pre-ruling geometry
lane@4 road 0-1.75 | kerb 2 | footpath 2.25-5.25 (synthetic, also unchanged)
arcade footpath 0-6.5, NO road quad, NO kerbs (1,102 corpus edges)
RESIDENTIAL side@14 road 0-2.75 | kerb 3 | GRASS 3.25-6.75 | footpath 7-10.5
The lane case branches on `outer <= inner`, never on a band literal.
Posters: 14 of 14 stood inside painted bitumen before; 0 of 14 after. Not one poster moved
— the ground did.
ONE DEFECT FOUND AND FIXED IN THE ROUND. The first cut painted every intersection shut:
a 12.5 m footpath crosses the perpendicular CARRIAGEWAY where a 3.5 m one only ever
clipped a far corner. Screenshotted, rejected, and fixed by putting the carriageway on
top (ROAD_Y 0 -> 0.04 under the flag): roads run through junctions, footpaths stop at the
kerb they meet. Verified on the real graph's worst 4.3-degree junction.
?verge=0 ships permanently — the gate's falsifiability control. Proven EXACT, not merely
similar: ground vertex+uv hashes identical to the HEAD build (c4c72181 / c111d968 /
1a80ebcd / 12c1241b / fd0d3211), and the same detector goes RED on the default boot.
CLASSIC IS FENCED, NOT AMENDED (the R31 dig-flip pattern). ?classic=1 forces verge off, so
the covenanted town's render does not move on a render-side ruling. Proven twice: identical
ground geometry hashes AND identical ground-only rendered pixels at three poses
(79df268c / 91564001 / a09d1e52), while the default boot differs at all three. If Fable
wants classic to take the footpath too, that is an amendment ruling and a one-token change.
§0.4 — the chunk lifecycle seam. The producer has fired since R3 and Lane D's consumer has
existed since R3, but nothing could connect them: createChunkManager never handed `ctx`
back, so a consumer holding only PROCITY.chunks had nowhere to attach (measured on HEAD:
`!!chunks.ctx === false`). Now `chunks.onChunkBuilt(fn) -> unsubscribe()` for many
consumers plus `chunks.ctx` for the original single slot. Proven live: 136 build / 144
dispose events over a 40-step walk; payload {cx,cz,buildings,furniture,data} with
key === `${cx},${cz}`; a second consumer does not clobber the first; the ctx slot fires;
unsubscribe silences; a THROWING subscriber does not take the streamer down.
ZERO behaviour change when nobody subscribes: a 102-step walk recording
[draws, tris, chunks] per step hashes ceb1ec66 on BOTH builds.
Files: web/js/world/ground.js, web/js/world/chunks.js, docs/shots/laneB_r37_kerb/*,
docs/LANES/LANE_B_NOTES.md, B-progress.md. sim.js untouched (Lane D's).
This commit is contained in:
parent
e4d77629c5
commit
47db478a51
BIN
docs/shots/laneB_r37_kerb/kerb_after_junction.jpg
Normal file
BIN
docs/shots/laneB_r37_kerb/kerb_after_junction.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 362 KiB |
BIN
docs/shots/laneB_r37_kerb/kerb_after_katoomba_acute_4deg.jpg
Normal file
BIN
docs/shots/laneB_r37_kerb/kerb_after_katoomba_acute_4deg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 535 KiB |
BIN
docs/shots/laneB_r37_kerb/kerb_after_poster.jpg
Normal file
BIN
docs/shots/laneB_r37_kerb/kerb_after_poster.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 250 KiB |
BIN
docs/shots/laneB_r37_kerb/kerb_after_streetlevel.jpg
Normal file
BIN
docs/shots/laneB_r37_kerb/kerb_after_streetlevel.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 241 KiB |
BIN
docs/shots/laneB_r37_kerb/kerb_before_junction.jpg
Normal file
BIN
docs/shots/laneB_r37_kerb/kerb_before_junction.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 327 KiB |
BIN
docs/shots/laneB_r37_kerb/kerb_before_poster.jpg
Normal file
BIN
docs/shots/laneB_r37_kerb/kerb_before_poster.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 221 KiB |
@ -25,6 +25,34 @@ export function createChunkManager(plan, scene, ctx) {
|
|||||||
|
|
||||||
const _colliders = []; // scratch, refilled each getColliders()
|
const _colliders = []; // scratch, refilled each getColliders()
|
||||||
|
|
||||||
|
// ── the per-chunk lifecycle seam (ROUND37 §0.4) ──────────────────────────────────────────────
|
||||||
|
// The producer below has fired `ctx.onChunkBuilt` / `ctx.onChunkDisposed` since R3 and Lane D's
|
||||||
|
// sim has carried the matching `onChunkBuilt(key)` / `onChunkDisposed(key)` methods just as long —
|
||||||
|
// but nothing could ever connect them: `ctx` is the shell's local object and the manager never
|
||||||
|
// handed it back, so a consumer holding only the manager (which is all `PROCITY.chunks` is) had
|
||||||
|
// nowhere to attach. Two ways in now, both inert until someone uses them:
|
||||||
|
// • `chunks.onChunkBuilt(fn)` → returns an unsubscribe. MANY consumers, none clobbering another
|
||||||
|
// (the single `ctx` slot is last-writer-wins, and the v8 slate has several per-chunk systems).
|
||||||
|
// • `chunks.ctx.onChunkBuilt = fn` — the original single slot, still first to fire, and now
|
||||||
|
// reachable exactly as LANE_D_NOTES §35 documents it.
|
||||||
|
// Zero cost when nobody listens: the detail object is only allocated inside the guard, so a
|
||||||
|
// subscriber-free boot does the same two truthiness tests it has done since R3 and nothing else.
|
||||||
|
const builtSubs = [], disposedSubs = [];
|
||||||
|
const subscribe = (list, fn) => {
|
||||||
|
if (typeof fn !== 'function') return () => {};
|
||||||
|
list.push(fn);
|
||||||
|
return () => { const i = list.indexOf(fn); if (i >= 0) list.splice(i, 1); };
|
||||||
|
};
|
||||||
|
// A throwing subscriber must never take the streamer down with it — a half-built chunk leaks its
|
||||||
|
// groups and the player walks into a void. Report and carry on.
|
||||||
|
const fire = (fn, key, detail, what) => {
|
||||||
|
try { fn(key, detail); } catch (err) { console.warn(`[procity] ${what} subscriber threw for ${key}`, err); }
|
||||||
|
};
|
||||||
|
const emit = (slot, list, key, detail, what) => {
|
||||||
|
if (slot) fire(slot, key, detail, what);
|
||||||
|
for (let i = 0; i < list.length; i++) fire(list[i], key, detail, what);
|
||||||
|
};
|
||||||
|
|
||||||
function buildChunk(key) {
|
function buildChunk(key) {
|
||||||
if (live.has(key)) return;
|
if (live.has(key)) return;
|
||||||
const data = index.get(key);
|
const data = index.get(key);
|
||||||
@ -41,14 +69,19 @@ export function createChunkManager(plan, scene, ctx) {
|
|||||||
? buildings.colliders.concat(furniture.colliders) : buildings.colliders;
|
? buildings.colliders.concat(furniture.colliders) : buildings.colliders;
|
||||||
live.set(key, { buildings, furniture, colliders, cx, cz });
|
live.set(key, { buildings, furniture, colliders, cx, cz });
|
||||||
// optional lifecycle hook (Lane D/F per-chunk spawning, ambient, LOD — LANE_F_NOTES §8).
|
// 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 || builtSubs.length) {
|
||||||
if (ctx.onChunkBuilt) ctx.onChunkBuilt(key, { cx, cz, buildings: buildings.group, furniture: furniture.group, data });
|
emit(ctx.onChunkBuilt, builtSubs, key,
|
||||||
|
{ cx, cz, buildings: buildings.group, furniture: furniture.group, data }, 'onChunkBuilt');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function disposeChunk(key) {
|
function disposeChunk(key) {
|
||||||
const b = live.get(key);
|
const b = live.get(key);
|
||||||
if (!b) return;
|
if (!b) return;
|
||||||
if (ctx.onChunkDisposed) ctx.onChunkDisposed(key, { cx: b.cx, cz: b.cz }); // before the groups are freed
|
// fires BEFORE the groups are freed, so a consumer can still read what it is losing
|
||||||
|
if (ctx.onChunkDisposed || disposedSubs.length) {
|
||||||
|
emit(ctx.onChunkDisposed, disposedSubs, key, { cx: b.cx, cz: b.cz }, 'onChunkDisposed');
|
||||||
|
}
|
||||||
scene.remove(b.buildings.group, b.furniture.group);
|
scene.remove(b.buildings.group, b.furniture.group);
|
||||||
if (b.buildings.doorMesh) {
|
if (b.buildings.doorMesh) {
|
||||||
const i = doorMeshes.indexOf(b.buildings.doorMesh);
|
const i = doorMeshes.indexOf(b.buildings.doorMesh);
|
||||||
@ -128,6 +161,13 @@ export function createChunkManager(plan, scene, ctx) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
update, warmup, getColliders, setNight, dispose,
|
update, warmup, getColliders, setNight, dispose,
|
||||||
|
// §0.4 — the lifecycle seam, subscribable. `onChunkBuilt(fn)` → unsubscribe(); `ctx` is handed
|
||||||
|
// back so the original single-slot form works too. Keys are planutil chunk keys ("cx,cz"),
|
||||||
|
// which sim.js derives identically (sim.js:52).
|
||||||
|
onChunkBuilt: (fn) => subscribe(builtSubs, fn),
|
||||||
|
onChunkDisposed: (fn) => subscribe(disposedSubs, fn),
|
||||||
|
get subscriberCount() { return builtSubs.length + disposedSubs.length; },
|
||||||
|
ctx,
|
||||||
getDoorMeshes: () => doorMeshes,
|
getDoorMeshes: () => doorMeshes,
|
||||||
get count() { return live.size; },
|
get count() { return live.size; },
|
||||||
get pending() { return queue.length; },
|
get pending() { return queue.length; },
|
||||||
|
|||||||
@ -8,11 +8,62 @@
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
|
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
|
||||||
import { resolveEdges } from './planutil.js';
|
import { resolveEdges } from './planutil.js';
|
||||||
|
import { vergeBand } from '../core/registry.js';
|
||||||
|
|
||||||
const FOOT = 3.5; // footpath width
|
const FOOT = 3.5; // footpath width OUTBOARD of the corridor (unchanged since v1)
|
||||||
const TILE = 5; // metres per texture tile
|
const TILE = 5; // metres per texture tile
|
||||||
const OVERLAP = 1.5; // road segments overrun their nodes so intersections fill
|
const OVERLAP = 1.5; // road segments overrun their nodes so intersections fill
|
||||||
|
|
||||||
|
// ── THE KERB IS FOOTPATH (v8 ruling 1, ROUND37 §0.1) ───────────────────────────────
|
||||||
|
// `edge.width` is the whole CORRIDOR — carriageway + verge — and this file painted all of it as
|
||||||
|
// bitumen from v1 to v7, which put the band registry.js calls "verandah'd footpath" (and that
|
||||||
|
// buildings.js already builds an awning over) under road texture: 14 gig posters, every bench and
|
||||||
|
// every ped lane stood in the road on the boot every player gets. Lane A's published corridor law
|
||||||
|
// (core/registry.js) is now the single source of truth for where the bitumen stops: the road quad
|
||||||
|
// narrows to the carriageway and the EXISTING merged `footGeos` class extends inward to meet it.
|
||||||
|
// Same merged meshes, same quad count ⇒ measured +0 draws (identical at all 102 steps of the walked
|
||||||
|
// spine) and tris that go DOWN, not up: −8 on the synthetic town, −288 on katoomba_real, because the
|
||||||
|
// only quads this adds are the ones the arcade case removes. The charter's "~2 k tris" was an
|
||||||
|
// estimate against a class that already existed; the honest number is a re-parameterisation.
|
||||||
|
//
|
||||||
|
// We consume `vergeBand(e)[0]`, NOT `roadWidth(e)/2`. Both `roadWidth` and `edge.width` are full
|
||||||
|
// widths — one of the carriageway, one of the corridor — and A names that collision as the reason
|
||||||
|
// this bug existed at all; `vergeBand`'s inner bound is the number A's own corrected gate reads, so
|
||||||
|
// the gate and the geometry cannot drift apart (Lane A, R37, verified over 31,039 corpus edges).
|
||||||
|
// The band is NOT one number: 9 m is synthetic-only (main @28 m). A real main is 24 m ⇒ 7 m, and the
|
||||||
|
// corpus's dominant edge is `side` @12 m — 22,415 of 31,039 edges (72%) — ⇒ 3 m. Never branch on a
|
||||||
|
// literal band; branch on the degenerate test (`outer <= inner`), which is exactly the class of bug
|
||||||
|
// this whole item exists to retire.
|
||||||
|
//
|
||||||
|
// `?verge=0` reverts to the pre-ruling geometry, permanently — it is Lane F's falsifiability
|
||||||
|
// control for the poster-clearance gate (the pre-change failure is known and counted at exactly 14).
|
||||||
|
// `?classic=1` forces it off (the R31 dig-flip pattern): the covenanted town's render does not move
|
||||||
|
// on a render-side ruling without an amendment ruling from Fable.
|
||||||
|
const VERGE = (() => {
|
||||||
|
try {
|
||||||
|
const p = new URLSearchParams(location.search);
|
||||||
|
const classic = p.has('classic') && p.get('classic') !== '0';
|
||||||
|
return !classic && p.get('verge') !== '0';
|
||||||
|
} catch (e) { return false; } // unparseable search ⇒ pre-ruling geometry, never a throw
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Edges that keep a GRASS VERGE instead of pavement: the nature strip is period-correct in front of
|
||||||
|
// houses and nowhere else. An edge is residential if a house/yard lot fronts it, or if its fronting
|
||||||
|
// lots sit in a `residential` district. Real towns are 100% shop/mainstreet today ⇒ empty set ⇒ they
|
||||||
|
// pave, which is the intent.
|
||||||
|
function residentialEdges(plan) {
|
||||||
|
const set = new Set();
|
||||||
|
const districtOfBlock = new Map((plan.blocks || []).map((b) => [b.id, b.district]));
|
||||||
|
const kindOfDistrict = new Map((plan.districts || []).map((d) => [d.id, d.kind]));
|
||||||
|
for (const l of plan.lots || []) {
|
||||||
|
if (l.frontEdge == null) continue;
|
||||||
|
if (l.use === 'house' || l.use === 'yard' || kindOfDistrict.get(districtOfBlock.get(l.block)) === 'residential') {
|
||||||
|
set.add(l.frontEdge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
// Horizontal quad in XZ centred at (cx,cz), aligned to unit dir/perp, size len×wid, at height y.
|
// Horizontal quad in XZ centred at (cx,cz), aligned to unit dir/perp, size len×wid, at height y.
|
||||||
// UVs baked to tile every TILE metres.
|
// UVs baked to tile every TILE metres.
|
||||||
function hQuad(cx, cz, dir, perp, len, wid, y) {
|
function hQuad(cx, cz, dir, perp, len, wid, y) {
|
||||||
@ -46,6 +97,18 @@ export function buildGround(plan, skins) {
|
|||||||
const roadGeos = [], footGeos = [], kerbGeos = [];
|
const roadGeos = [], footGeos = [], kerbGeos = [];
|
||||||
const dir = new THREE.Vector3(), perp = new THREE.Vector3();
|
const dir = new THREE.Vector3(), perp = new THREE.Vector3();
|
||||||
|
|
||||||
|
const grassy = VERGE ? residentialEdges(plan) : null;
|
||||||
|
// Surface heights are a painter's order, not elevations — the whole stack is 9 cm thick and every
|
||||||
|
// class is a flat quad, so what these numbers decide is who wins where two strips CROSS.
|
||||||
|
// Pre-ruling the footpath (0.02) sat above the road (0.0) and that was invisible: a 3.5 m path
|
||||||
|
// outboard of the corridor only ever crossed the far corner of a junction. At 12.5 m it crosses
|
||||||
|
// the whole of the perpendicular CARRIAGEWAY, and the first cut of this change painted every
|
||||||
|
// intersection shut — four roads arriving at a pedestrian plaza, screenshotted and rejected.
|
||||||
|
// So under the ruling the carriageway goes on top: roads run through junctions, footpaths stop at
|
||||||
|
// the kerb they meet, which is what an intersection actually looks like. Road-on-road overlap in
|
||||||
|
// the junction box is unchanged and still invisible (one skin, one y, uniform asphalt — R20).
|
||||||
|
const ROAD_Y = VERGE ? 0.04 : 0.0;
|
||||||
|
|
||||||
for (const e of edges) {
|
for (const e of edges) {
|
||||||
const dx = e.bx - e.ax, dz = e.bz - e.az;
|
const dx = e.bx - e.ax, dz = e.bz - e.az;
|
||||||
const len = Math.hypot(dx, dz) || 1;
|
const len = Math.hypot(dx, dz) || 1;
|
||||||
@ -53,13 +116,37 @@ export function buildGround(plan, skins) {
|
|||||||
perp.set(-dir.z, 0, dir.x);
|
perp.set(-dir.z, 0, dir.x);
|
||||||
const cx = (e.ax + e.bx) / 2, cz = (e.az + e.bz) / 2;
|
const cx = (e.ax + e.bx) / 2, cz = (e.az + e.bz) / 2;
|
||||||
const roadLen = len + OVERLAP * 2;
|
const roadLen = len + OVERLAP * 2;
|
||||||
roadGeos.push(hQuad(cx, cz, dir, perp, roadLen, e.width, 0.0));
|
const half = e.width / 2; // corridor half-width (the v1 kerb line)
|
||||||
// footpaths flank the road
|
// A's law, as A publishes it: [kerb, corridor edge] measured from the centreline.
|
||||||
const off = e.width / 2 + FOOT / 2;
|
const [bandInner, bandOuter] = vergeBand(e);
|
||||||
footGeos.push(hQuad(cx + perp.x * off, cz + perp.z * off, dir, perp, roadLen, FOOT, 0.02));
|
// Where the kerb goes. `min(…, half)` is the inversion guard: no edge in the 31,039-edge corpus
|
||||||
footGeos.push(hQuad(cx - perp.x * off, cz - perp.z * off, dir, perp, roadLen, FOOT, 0.02));
|
// has a carriageway wider than its corridor, but a `side` under 6 m would, and an inverted band
|
||||||
|
// would flip a footpath quad inside out. A DEGENERATE band (`bandOuter <= bandInner` — all
|
||||||
|
// carriageway, no verge: 56 real `lane` edges, 2 synthetic ones, and NOT the [2,2] literal the
|
||||||
|
// brief carried) then lands on `bandInner === half`, i.e. the pre-ruling geometry, by arithmetic
|
||||||
|
// rather than by a special case.
|
||||||
|
const kerbHalf = VERGE ? Math.min(bandInner, half) : half;
|
||||||
|
|
||||||
|
// ARCADE (1,102 corpus edges): a covered pedestrian lane has no carriageway at all, so a road
|
||||||
|
// quad here is zero-area and the two kerbs land on top of each other at the centreline. Emit
|
||||||
|
// neither, and pave the whole corridor as ONE footpath quad (no centreline seam).
|
||||||
|
if (VERGE && kerbHalf <= 0) {
|
||||||
|
footGeos.push(hQuad(cx, cz, dir, perp, roadLen, e.width + FOOT * 2, 0.02));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
roadGeos.push(hQuad(cx, cz, dir, perp, roadLen, kerbHalf * 2, ROAD_Y));
|
||||||
|
// Footpaths flank the road: from the kerb line out to the corridor edge + FOOT. Two bands keep
|
||||||
|
// their v1 outboard-only pavement — a DEGENERATE band (no verge exists to pave: the laneway
|
||||||
|
// case, tested, never assumed from a width) and a RESIDENTIAL edge, where the verge stays grass
|
||||||
|
// because a nature strip is what is period-correct in front of a house.
|
||||||
|
const inner = (bandOuter <= bandInner || (grassy && grassy.has(e.id))) ? half : kerbHalf;
|
||||||
|
const fw = half + FOOT - inner;
|
||||||
|
const off = inner + fw / 2;
|
||||||
|
footGeos.push(hQuad(cx + perp.x * off, cz + perp.z * off, dir, perp, roadLen, fw, 0.02));
|
||||||
|
footGeos.push(hQuad(cx - perp.x * off, cz - perp.z * off, dir, perp, roadLen, fw, 0.02));
|
||||||
// low kerbs between road and footpath
|
// low kerbs between road and footpath
|
||||||
const koff = e.width / 2 + 0.12;
|
const koff = kerbHalf + 0.12;
|
||||||
kerbGeos.push(hQuad(cx + perp.x * koff, cz + perp.z * koff, dir, perp, roadLen, 0.24, 0.06));
|
kerbGeos.push(hQuad(cx + perp.x * koff, cz + perp.z * koff, dir, perp, roadLen, 0.24, 0.06));
|
||||||
kerbGeos.push(hQuad(cx - perp.x * koff, cz - perp.z * koff, dir, perp, roadLen, 0.24, 0.06));
|
kerbGeos.push(hQuad(cx - perp.x * koff, cz - perp.z * koff, dir, perp, roadLen, 0.24, 0.06));
|
||||||
}
|
}
|
||||||
@ -100,5 +187,5 @@ export function buildGround(plan, skins) {
|
|||||||
kerbMat.dispose();
|
kerbMat.dispose();
|
||||||
group.clear();
|
group.clear();
|
||||||
}
|
}
|
||||||
return { group, dispose };
|
return { group, dispose, verge: VERGE };
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user