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