Lane B round 22 (v4.0): #3 per-town bookmarks — anchor on the retail heart (Lane G's filed bug) + F's primitive

ROOT CAUSE: every "nearest" resolver in dbg.js sorted by distance from the WORLD ORIGIN. That's the town
centre only on a synthetic plan -- a real OSM town is projected about its cache's bbox centre while its shops
cluster hundreds of metres away, so bookmarks teleported into arbitrary far-flung geometry, exactly as G
reported. Measured origin-pick distance from the retail heart: katoomba 408m, newtown 626m, fremantle 371m.

FIX: resolve bookmarks about the town's DENSEST SHOP CLUSTER (grid-bin shop lots at 120m; heaviest bin's
centroid = the retail heart) instead of the origin -- byDist, lotsByDist, streetViewPose, stopPose, and
crossroadsPose's tie-break.

GATED on the REAL-ROADS signal (edges > 200) -- the same primitive as the R20 tram fence -- so synthetic and
the marched fixtures keep their exact origin-centred behaviour and F's release-tour control shot cannot move.
The gate is load-bearing, not cosmetic: synthetic's anchor is only 117m out but it DOES re-pick (Anglibarn Op
Shop -> Dusty Pages), so an unconditional switch would have silently moved the control. Same reason the new
crossroadsPose tie-break is gated (a degree tie would otherwise resolve differently on synthetic).

Verified across the town set: synthetic(22 edges)/melbourne(15)/katoomba-fixture(15)/silverton(1) -> origin,
UNCHANGED; newtown_godverse(1626) -> anchor(98,-629); katoomba_real(993) -> anchor(-227,-625). G's own town
is fixed.

Result on katoomba: shopfront bookmarks went from ~408m adrift to <=42m of the retail heart (arcade 7m,
milkbar_dusk/patronage_door/shopfront_detail 24m, market_square 26m, dig_real 42m); strip views ~195m on the
high street; venue_night/queue_night ~345m (A's venue placement, not the bookmark). All 19 resolve, 0 errors;
synthetic 19/19 unchanged. warehouse_fringe sits 3,074m out -- correct by design (it's the far:true fringe
shot; on a 5km town the fringe genuinely is 3km away).

-> F (ledger #4, the shared posing primitive): published on the harness as DBG.townAnchor {x,z,n,anchored,ref}
   (the retail heart + whether it's in use) and DBG.clusterPose(kinds) (stand on the road at the densest
   cluster, look along it). Your cluster-pose finding as a dbg.js-consumable -- the tour and the bookmarks now
   resolve through the SAME primitive, so a tour shot and its bookmark can't drift apart.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-16 22:01:41 +10:00
parent 9c9ad730a3
commit af8a6db020
2 changed files with 85 additions and 6 deletions

View File

@ -67,6 +67,45 @@ contained Lane B change I can land quickly. I'm **not** doing it unilaterally: p
(asset tris), and source decimation is the right fix (it helps every town and every view, not just this
one). Say the word if you want the B-side gate as belt-and-braces.
## Round 22 — #3 per-town bookmarks (Lane G's filed bug) — FIXED
**Root cause:** every "nearest" resolver in `dbg.js` sorted by distance from the **world origin**. That's
the town centre only on a *synthetic* plan. A real OSM town is projected about its cache's bbox centre
while its shops cluster hundreds of metres away — so bookmarks teleported into arbitrary far-flung
geometry, exactly as G reported. Measured origin-pick distance from the retail heart: **katoomba 408 m ·
newtown 626 m · fremantle 371 m**.
**Fix:** resolve every bookmark about the town's **densest shop cluster** (grid-bin the shop lots at 120 m,
heaviest bin's centroid = the retail heart) instead of the origin — `byDist`, `lotsByDist`,
`streetViewPose`, `stopPose`, and `crossroadsPose`'s tie-break.
**Gated on the REAL-ROADS signal (`edges > 200`)** — the same primitive the R20 tram fence uses — so
**synthetic and the marched fixtures keep their exact origin-centred behaviour** and F's release-tour
control shot cannot move. This gate is *load-bearing, not cosmetic*: synthetic's anchor is only 117 m out
but it **does** re-pick (Anglibarn Op Shop → Dusty Pages), so an unconditional switch would have silently
moved the control. Same reason `crossroadsPose`'s new tie-break is gated — a degree tie would otherwise
resolve differently on synthetic.
| town | edges | ref used |
|---|---|---|
| synthetic · melbourne · katoomba-fixture · silverton | 22 · 15 · 15 · 1 | **origin — unchanged** ✅ |
| **newtown_godverse** (G's town) | 1,626 | anchor (98, 629) ✅ |
| katoomba_real | 993 | anchor (227, 625) ✅ |
**Result on katoomba** — shopfront bookmarks went from ~408 m adrift to **≤42 m** of the retail heart:
`arcade` 7 m · `milkbar_dusk`/`patronage_door`/`shopfront_detail` 24 m · `market_square` 26 m · `dig_real`
42 m; strip views (`street_noon`, `katoomba_main`, `district_posters`) ~195 m — on the high street;
`venue_night`/`queue_night` ~345 m (that's A's venue placement, not the bookmark). All 19 resolve, 0
errors, synthetic 19/19 unchanged.
- `warehouse_fringe` sits **3,074 m** out — **correct by design**: it's the `far:true` fringe shot, and on
a 5 km town the fringe genuinely is 3 km away. Left alone deliberately.
**→ Lane F (ledger #4, the shared posing primitive):** published on the harness as
**`DBG.townAnchor`** (`{x, z, n, anchored, ref}` — the retail heart + whether it's in use) and
**`DBG.clusterPose(kinds)`** (stand on the road at the densest cluster, look along it). That's your
cluster-pose finding as a `dbg.js`-consumable — the tour and the bookmarks now resolve through the same
primitive, so a tour shot and its bookmark can't drift apart.
### Honest note on the baseline (mine 163,015 vs F's 221,935)
Same view, different number: my automation's rAF is throttled, so the **live gig crowd never spawns**
(citizens measured **0** tris here) — F's number includes it (~59k of peds). This doesn't move the verdict:

View File

@ -59,9 +59,33 @@ export function installDBG(deps) {
look: [frontX, 2.2, frontZ],
};
};
// ── ROUND22 (Lane G's bug): per-town bookmark anchoring ────────────────────────────────────────
// Every "nearest" resolver below used to sort by distance from the WORLD ORIGIN, which is only the
// town centre on a SYNTHETIC plan. A real OSM town is projected about its cache's bbox centre while
// its shops cluster hundreds of metres away, so origin-sorted bookmarks teleported to arbitrary
// far-flung geometry (measured: the origin pick sits 408 m from the retail heart on katoomba, 626 m
// on newtown, 371 m on fremantle). Fix: sort about the town's DENSEST SHOP CLUSTER instead.
//
// Gated on the REAL-ROADS signal (`edges > 200`, the same primitive the R20 tram fence uses) so
// SYNTHETIC + the marched fixtures keep the exact origin-centred behaviour they've always had —
// their bookmarks (and F's release-tour control shot) do not move. Verified: synthetic's anchor is
// only 117 m out but DOES re-pick, hence the gate rather than an unconditional switch.
const townAnchor = (() => {
const CELL = 120; // ~a block; the heaviest bin is the retail heart
const pts = (plan.shops || []).map(lotOf).filter(Boolean);
if (!pts.length) return { x: 0, z: 0, n: 0 };
const bins = new Map();
for (const p of pts) { const k = `${Math.round(p.x / CELL)},${Math.round(p.z / CELL)}`; if (!bins.has(k)) bins.set(k, []); bins.get(k).push(p); }
let best = null; for (const arr of bins.values()) if (!best || arr.length > best.length) best = arr;
return { x: best.reduce((a, p) => a + p.x, 0) / best.length, z: best.reduce((a, p) => a + p.z, 0) / best.length, n: best.length };
})();
const REAL_ROADS = (plan.streets?.edges?.length || 0) > 200;
const REF = REAL_ROADS ? townAnchor : { x: 0, z: 0 }; // synthetic/fixtures → origin, exactly as before
const dRef = (x, z) => Math.hypot((x || 0) - REF.x, (z || 0) - REF.z);
const byDist = () => [...(plan.shops || [])].sort((a, b) => {
const la = lotOf(a), lb = lotOf(b);
return Math.hypot(la?.x || 0, la?.z || 0) - Math.hypot(lb?.x || 0, lb?.z || 0);
return dRef(la?.x, la?.z) - dRef(lb?.x, lb?.z);
});
// nearest shop matching pred, else nearest shop overall → a pose is always produced
const pickShop = (pred) => { const s = byDist(); return poseForShop(s.find(pred) || s[0]); };
@ -75,7 +99,7 @@ export function installDBG(deps) {
return { pos: [frontX + fx * dist + rx * off, 1.9, frontZ + fz * dist + rz * off], look: [frontX, 2.0, frontZ] };
};
const blockKindOfLot = (lot) => plan.blocks?.find((b) => b.id === lot.block)?.kind;
const lotsByDist = () => [...(plan.lots || [])].sort((a, b) => Math.hypot(a.x, a.z) - Math.hypot(b.x, b.z));
const lotsByDist = () => [...(plan.lots || [])].sort((a, b) => dRef(a.x, a.z) - dRef(b.x, b.z));
// nearest (or farthest = "fringe") lot matching pred, else nearest lot overall
const pickLot = (pred, farthest = false) => {
const L = lotsByDist(); const hit = L.filter(pred);
@ -89,11 +113,11 @@ export function installDBG(deps) {
const nn = new Map((plan.streets?.nodes || []).map((n) => [n.id, n]));
const es = (plan.streets?.edges || []).filter((e) => kinds.includes(e.kind) && nn.get(e.a) && nn.get(e.b));
if (!es.length) return crossroadsPose();
let best = null, bd = 1e18; // long edge, near the origin (or far, for the fringe)
let best = null, bd = 1e18; // long edge, near REF (or far, for the fringe)
for (const e of es) {
const a = nn.get(e.a), b = nn.get(e.b);
const len = Math.hypot(b.x - a.x, b.z - a.z) || 1;
const dist = Math.hypot((a.x + b.x) / 2, (a.z + b.z) / 2);
const dist = dRef((a.x + b.x) / 2, (a.z + b.z) / 2); // R22: about the town anchor, not the origin
const score = (far ? -dist : dist) - len * 0.8; // favour long open runs; `far` picks the outer ring
if (score < bd) { bd = score; best = { a, b, len, width: e.width || 8 }; }
}
@ -111,7 +135,15 @@ export function installDBG(deps) {
const crossroadsPose = () => {
const edges = plan.streets?.edges || [], nodes = plan.streets?.nodes || [];
const deg = new Map(); for (const e of edges) { deg.set(e.a, (deg.get(e.a) || 0) + 1); deg.set(e.b, (deg.get(e.b) || 0) + 1); }
let node = null, bd = -1; for (const n of nodes) { const d = deg.get(n.id) || 0; if (d > bd) { bd = d; node = n; } }
// busiest node — R22: on a real town, tie-break toward the anchor so we frame the town's own
// crossroads, not a highway junction kilometres away. Synthetic REF is the origin ⇒ unchanged.
let node = null, bd = -1, bdist = 1e18;
for (const n of nodes) {
const d = deg.get(n.id) || 0, dist = dRef(n.x, n.z);
// strict `>` alone on synthetic/fixtures = the pre-R22 "first max-degree node wins" (identical);
// the anchor tie-break only engages on real-roads towns.
if (d > bd || (REAL_ROADS && d === bd && dist < bdist)) { bd = d; bdist = dist; node = n; }
}
if (!node) return pickShop(() => true);
const e = edges.find((ed) => ed.a === node.id || ed.b === node.id);
const o = e && nodes.find((n) => n.id === (e.a === node.id ? e.b : e.a));
@ -123,7 +155,7 @@ export function installDBG(deps) {
const stopPose = () => {
const stops = busShelterStops(plan);
if (!stops.length) return streetViewPose(['main']);
const s = [...stops].sort((a, b) => Math.hypot(a.x, a.z) - Math.hypot(b.x, b.z))[0]; // nearest origin = most open
const s = [...stops].sort((a, b) => dRef(a.x, a.z) - dRef(b.x, b.z))[0]; // nearest the town anchor (origin on synthetic)
const px = Math.sin(s.yaw), pz = Math.cos(s.yaw), ex = Math.cos(s.yaw), ez = -Math.sin(s.yaw);
return { pos: [s.x - px * 6 + ex * 4, 2.0, s.z - pz * 6 + ez * 4], look: [s.x, 1.3, s.z] };
};
@ -192,6 +224,14 @@ export function installDBG(deps) {
bookmarks: Object.keys(BOOKMARKS),
info,
// ── R22: the town-anchoring primitive, published for Lane F's release tour (ledger #4) ──
// `townAnchor` = the centroid of the densest shop cluster (the retail heart). `anchored` says
// whether the bookmarks are using it (real-roads towns) or the origin (synthetic/fixtures —
// unchanged behaviour). `clusterPose()` is the shared poser: stand on the road at the densest
// cluster and look along it — the same primitive B's bookmarks now resolve through.
townAnchor: { ...townAnchor, anchored: REAL_ROADS, ref: { x: REF.x, z: REF.z } },
clusterPose: (kinds = ['main']) => streetViewPose(kinds),
// time-of-day (hands the day cycle to the harness so captures are deterministic)
setSegment(seg) { lighting.setPaused(true); lighting.setSegment(seg | 0); return lighting.getClock(); },