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:
parent
9c9ad730a3
commit
af8a6db020
@ -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:
|
||||
|
||||
@ -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(); },
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user