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 (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. 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) ### 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** 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: (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], 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 byDist = () => [...(plan.shops || [])].sort((a, b) => {
const la = lotOf(a), lb = lotOf(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 // 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]); }; 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] }; 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 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 // nearest (or farthest = "fringe") lot matching pred, else nearest lot overall
const pickLot = (pred, farthest = false) => { const pickLot = (pred, farthest = false) => {
const L = lotsByDist(); const hit = L.filter(pred); 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 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)); const es = (plan.streets?.edges || []).filter((e) => kinds.includes(e.kind) && nn.get(e.a) && nn.get(e.b));
if (!es.length) return crossroadsPose(); 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) { for (const e of es) {
const a = nn.get(e.a), b = nn.get(e.b); 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 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 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 }; } if (score < bd) { bd = score; best = { a, b, len, width: e.width || 8 }; }
} }
@ -111,7 +135,15 @@ export function installDBG(deps) {
const crossroadsPose = () => { const crossroadsPose = () => {
const edges = plan.streets?.edges || [], nodes = plan.streets?.nodes || []; 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); } 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); if (!node) return pickShop(() => true);
const e = edges.find((ed) => ed.a === node.id || ed.b === node.id); 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)); 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 stopPose = () => {
const stops = busShelterStops(plan); const stops = busShelterStops(plan);
if (!stops.length) return streetViewPose(['main']); 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); 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] }; 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), bookmarks: Object.keys(BOOKMARKS),
info, 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) // 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(); }, setSegment(seg) { lighting.setPaused(true); lighting.setSegment(seg | 0); return lighting.getClock(); },