Compare commits
6 Commits
55d5c634d9
...
ddd1e6827e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddd1e6827e | ||
|
|
96b3dea70f | ||
|
|
73694c97aa | ||
|
|
6fc4b40632 | ||
|
|
f17babd8d7 | ||
|
|
e6fcb7b365 |
@ -3,7 +3,35 @@
|
||||
*Status: **v1 complete & verified**. Standalone interiors library + test page. Every shop door opens
|
||||
into a unique, seeded, themed interior, generated on demand in ~4ms, byte-identical every revisit.*
|
||||
|
||||
Last updated: 2026-07-16 (round 18) · owner: PROCITY-C · reviewer: Fable
|
||||
Last updated: 2026-07-16 (round 19) · owner: PROCITY-C · reviewer: Fable
|
||||
|
||||
---
|
||||
|
||||
## Update 2026-07-16 (round 19, v4.0-alpha CLOSE) — re-soak katoomba on REAL roads (against A's in-flight pass)
|
||||
|
||||
R19 §Lane C (on-call, ledger #5): E's roads landed (`dbb52c2` — katoomba_real is now **schema v2, 484 real
|
||||
roads**), so I re-ran the 20-door soak against the real street graph — closing the R18 "re-run when roads land"
|
||||
caveat. **Honest caveat: the working tree also carried Lane A's UNCOMMITTED R19 fragmentation pass** (plan_osm.js
|
||||
WIP — `JOIN_TOL`/`BRIDGE_MAX`, junction-protected Douglas–Peucker, shopless-island cull / shop-island bridge),
|
||||
so the dev server served A's *in-progress* finalization, not a frozen state. Read this as "interiors survive
|
||||
A's real-graph pipeline as it stands mid-round"; a clean confirm follows A's commit.
|
||||
|
||||
**Robust result (holds whatever A's WIP does):** every shop the plan produced builds a **valid interior** —
|
||||
0 throws · 0 carves · 0 path-fails · 0 over-budget · worst **147** draws (min 115); 3 gig venues
|
||||
(pub/band_room/rsl — **district placement holds on the real graph**); plan deterministic (byte-identical
|
||||
re-gen). Lot dims **unchanged** (7.5–8.5 × 14) → interiors are dim-identical to R18 (road-agnostic confirmed;
|
||||
my R18 degenerate-lot proof already spans anything the real graph can seat).
|
||||
|
||||
**Shop count observed = 19 (NOT filed as a defect).** The plan yielded 19 shops here, but against A's in-flight
|
||||
fragmentation code that's expected, not a bug — A's ruling explicitly culls shopless islands and bridges/
|
||||
drops-with-count shop-bearing ones, so the final count is A's to pin (could land at 20 connected or an explicit
|
||||
19 with a `norm.dropped` count). I'm not attributing it to A as a finding; I only confirm interiors build clean
|
||||
whichever way it resolves. (If A's committed pass surprises with a shop that *can't* build an interior, that
|
||||
would be the finding — none did in this run.)
|
||||
|
||||
**Final C spot-confirm pending A's COMMITTED finalization + golden pin** (per Fable's sequencing, C confirms
|
||||
after A). When A pins `katoomba_real`'s real-roads golden I'll re-run once against the frozen state. Docs-only
|
||||
round for C.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -4,6 +4,27 @@
|
||||
|
||||
---
|
||||
|
||||
## Round 18–19 (v4.0-alpha REAL ROADS) — F held R18 correctly, then closed R19
|
||||
|
||||
**R18: F did NOT run — the gate would have failed by design, so F held the tag** and filed the failure list
|
||||
(the alpha's charter deliverable). Real roads landed structurally but the selfcheck was RED (katoomba golden
|
||||
unpinned + poster-kerb breach on all 5 real towns) and the town was fragmented (20 shops over 799 edges = an
|
||||
empty field). Classic/default stayed frozen — the alpha changed nothing outside the cache path. Fable's
|
||||
verdict agreed: "F never ran — correctly."
|
||||
|
||||
**R19: the close.** A resolved the findings (fragmentation cull/join, poster clamp, goldens pinned), D
|
||||
re-measured, B shipped the town selector. F ran:
|
||||
- **B's `BIG_CITY` one-liner** (`e6fcb7b`, early): `shops>120 || edges>200` — real Katoomba's 799-edge graph
|
||||
now streams like a big city. **280k → ~12k tris.** B proved it; F owns index.html.
|
||||
- **The alpha gate** — `town_matrix.py` grew a `roads` dimension (a v2 cache must build the REAL graph,
|
||||
`mode='roads'` via the plan_osm report sink); classic/default proven untouched; budgets in the green.
|
||||
- **The money shot** — [Katoomba's real main street](docs/shots/v4_alpha/katoomba_mainstreet.png): asphalt +
|
||||
lane markings, footpaths, a pedestrian crossing, a walking crowd, gig posters, B's town selector in-frame.
|
||||
**PROCITY's look on real bones.** Tag **`v4.0-alpha`**. (Honest note: 19 real shops sit ~1/edge — a real
|
||||
*street*, not yet a dense strip; density is a v4.0-beta concern.)
|
||||
|
||||
---
|
||||
|
||||
## Round 17 (v3.2 THE TAIL + THE SCOUT) — F: the town matrix + the close
|
||||
|
||||
The tail drained (drummer lean + bench-sit, wind sway, backline verify, --verify fix) and E's real-map scout
|
||||
|
||||
@ -74,7 +74,7 @@ master off:
|
||||
| `?roster=v1` · `?pop=N` | stream | chunk-streamed citizen density is the baseline; `?roster=v1` restores the fixed roster |
|
||||
| `?dig=0` · `?stock=real` | dig on | crate-riffle on record bins · real GODVERSE sleeves in the stock |
|
||||
| `?noassets=1` · `?plansrc=osm` · `?mute=1` · `?dbg=1` | off | asset-free (zero network) · OSM plan source · silence · the `window.DBG` capture/soak harness |
|
||||
| `?plansrc=osm&town=<key>` | — | boot a **real Australian town** (v3.2 scout): fixtures `melbourne`/`katoomba`/`silverton`, or a real OSM cache `bendigo_real`/`castlemaine_real`/`fremantle_real`/`katoomba_real`/`newtown_real` (ODbL, `docs`/`web/assets/towns/SOURCES.md`) |
|
||||
| `?plansrc=osm&town=<key>` | — | boot a **real Australian town**: fixtures `melbourne`/`katoomba`/`silverton`, or a real OSM cache `bendigo_real`/`castlemaine_real`/`fremantle_real`/`katoomba_real`/`newtown_real` (ODbL, `web/assets/towns/SOURCES.md`). **v4.0-alpha REAL ROADS:** a cache with `roads[]` (schema v2) builds the CityPlan street graph from the town's **actual OSM streets** — walk Katoomba's real main street; without `roads[]` a cache marches onto synthetic avenues |
|
||||
| `?seed=N` | `20261990` | town seed |
|
||||
|
||||
**Release tour:** [`docs/shots/release_v3_1/contact.html`](docs/shots/release_v3_1/) — the money shots on
|
||||
|
||||
@ -1,11 +1,37 @@
|
||||
# LANE A — CITYGEN · progress (PROCITY-A)
|
||||
|
||||
*Status: **all deliverables landed; self-check all-green (16810/16810); rounds 2–18 closed** (see below).
|
||||
v4.0-alpha REAL ROADS: cache schema v2 (roads[]) published first + the real-graph lift (OSM ways →
|
||||
CityPlan street graph) implemented, proven on a synthetic irregular graph. All osm/classic/gig goldens
|
||||
frozen; katoomba_real's real-roads golden pins when E's roads land (A→E→A).*
|
||||
*Status: **all deliverables landed; self-check all-green (51670/51670); rounds 2–19 closed** (see below).
|
||||
v4.0-alpha alpha-close: fragmentation resolved (105→1 component on katoomba), E's spine-poster finding
|
||||
fixed, all 5 real towns pinned on real-roads goldens (katoomba `0xb7ffbca2`). classic/gig/marched-fixtures
|
||||
frozen.*
|
||||
*Date: 2026-07-16 · owner: PROCITY-A (Opus 4.8)*
|
||||
|
||||
## Round 19 (2026-07-16) — v4.0-alpha CLOSE: fragmentation ruling + findings hardened + goldens pinned
|
||||
|
||||
A was the round's spine (the A→E→A finalization deferred from R18). Every filed finding resolved; the tag is
|
||||
F's to land.
|
||||
|
||||
1. **Fragmentation (D's finding — Fable ruled, A owns details).** The naïve real graph fragmented
|
||||
(katoomba **105 components**, 58% street-metres on islands). Fixed in `plan_osm.buildRealRoads`:
|
||||
(a) **junction-protected Douglas–Peucker** — a point shared by ≥2 ways is a junction; simplify only
|
||||
between junctions, never through one (the collinear tee-off drop). Alone → 94–100% main-net.
|
||||
(b) **cull/bridge** — main component + near-fragments joined (`JOIN_TOL` 12 m; shop-island bridge ≤200 m);
|
||||
far **shopless** islands culled. **Result: 1 connected component on all five towns** (katoomba 100%
|
||||
main-net, 2 islands culled). Every dropped shop **counted** (`norm.dropped`) — katoomba 19/20 (1 crowd
|
||||
drop, the R18 overflow prediction measured), the other four kept all.
|
||||
2. **E's poster-clearance finding fixed** (`gigs.js`): a spine poster overhanging a real crossing carriageway
|
||||
is skipped — measured exactly as the selfcheck kerb floor, a **no-op on synthetic** (gig golden frozen).
|
||||
The 23 real-town clearance failures → 0.
|
||||
3. **Goldens pinned** (the finalization): all 5 real towns on their real-roads output
|
||||
(katoomba `0xb7ffbca2`, newtown `0x6f984c81`, fremantle `0xfb197000`, bendigo `0x21179b3b`,
|
||||
castlemaine `0x7c47f639`). synthetic/marched-fixtures/classic/default **frozen** — the alpha changed
|
||||
nothing outside the cache-schema path. selfcheck ALL GREEN **51670/51670**; scaffold + consistency GREEN.
|
||||
4. **New finding filed (beta, non-blocking):** spine-poster density scales with edge count (katoomba 438
|
||||
posters vs ~14 synthetic; draws stay low via instancing) — a beta posters-per-town cap.
|
||||
|
||||
Handshake fired: `katoomba_real` boots real roads, golden pinned, findings resolved → B/C/D/F. D re-measures
|
||||
(1 component / 100% / 0 stranded); F applies B's `BIG_CITY` one-liner + gates + tags `v4.0-alpha`.
|
||||
|
||||
## Round 18 (2026-07-16) — v4.0-alpha REAL ROADS: cache schema v2 + the real-graph lift (the epoch headline)
|
||||
|
||||
Lane A is the round's long pole (serial spine A→E→A). Two commits:
|
||||
|
||||
@ -387,8 +387,20 @@ OSM `highway=*` class (`validateTownCache` accepts schema `…/1` and `…/2`; a
|
||||
4. **seat** each shop on its **nearest real edge** at its real order + real side, marched with regularised
|
||||
spacing (real shops overlap — pixel-exact positions can't satisfy the no-overlap invariant), a
|
||||
frontage-strip block per edge, then a deterministic **overlap-resolve** pass (crowded intersections drop
|
||||
the later-id lot). **Same CityPlan schema out** — B/C/D/F consume it unchanged by contract; the golden
|
||||
re-pins per the amendment law (`katoomba_real`'s when E's roads land; synthetic + fixtures untouched).
|
||||
the later-id lot). **Same CityPlan schema out** — B/C/D/F consume it unchanged by contract.
|
||||
|
||||
**The fragmentation ruling (ROUND19, Fable — A owns the details).** Real geometry fragments the naïve graph
|
||||
(katoomba: **105 components**, 58% of street-metres on islands, D's finding). Two mechanisms, both handled:
|
||||
(a) **junction-protected simplification** — a point shared by ≥2 ways is a junction; Douglas–Peucker runs
|
||||
only *between* junctions, never through one (the collinear junction-drop that tee-off side streets hit). That
|
||||
alone reconnects to 94–100% main-net. (b) **cull/bridge** — the town is the **main component + joined
|
||||
near-fragments** (endpoints ≤ `JOIN_TOL` 12 m, or a shop-bearing island ≤ 200 m, get a bridge edge); a **far
|
||||
shopless island is culled** (bbox-clipped noise + dead streaming budget). A **stranded shop is always
|
||||
connected or dropped with a count** (`norm.dropped`). Result on all five towns: **one connected component**.
|
||||
A spine poster that would overhang a real crossing carriageway is **skipped** (E's finding; no-op on the
|
||||
synthetic town, so its gig golden is frozen). Golden re-pins per the amendment law (all five real towns pin
|
||||
on their real-roads output; synthetic + marched fixtures + classic/default untouched — the alpha changed
|
||||
nothing outside the cache-schema path).
|
||||
|
||||
## Shop types v1 (registry lives in `web/js/core/registry.js`, Lane A writes it, all lanes read)
|
||||
|
||||
|
||||
@ -2,6 +2,39 @@
|
||||
|
||||
Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`.
|
||||
|
||||
## Round 19 (2026-07-16) — the alpha close: fragmentation resolved + findings hardened + goldens pinned
|
||||
|
||||
### → B / C / D / F: **`katoomba_real` boots on real roads, golden pinned, findings resolved** (the handshake)
|
||||
The A→E→A finalization landed. `katoomba_real` golden = **`0xb7ffbca2`** (pinned); the other four real towns
|
||||
pinned too (newtown `0x6f984c81`, fremantle `0xfb197000`, bendigo `0x21179b3b`, castlemaine `0x7c47f639`).
|
||||
selfcheck **ALL GREEN 51670/51670**; classic/gig/marched-fixtures frozen (`0x3fa36874` / `0xb1d48ea1` / …).
|
||||
|
||||
### The fragmentation fix (D's finding — Fable's ruling, my details)
|
||||
**katoomba: 105 components → 1; 58% → 100% of street-metres in the main net.** Two mechanisms:
|
||||
1. **Junction-protected Douglas–Peucker** (the root cause). A point shared by ≥2 ways is a junction;
|
||||
simplify only *between* junctions, never through one — the collinear junction-drop where a side street
|
||||
tees into a straight main road. This alone took every town to 94–100% main-net.
|
||||
2. **Cull/bridge** (`JOIN_TOL` 12 m, shop-island bridge ≤ 200 m): main component + joined near-fragments;
|
||||
far **shopless** islands culled (bbox-clipped noise + dead streaming budget = half of B's tris problem).
|
||||
Per-town: katoomba 2 islands culled/0 bridges, newtown 3/4, fremantle 4/0, bendigo 3/1, castlemaine 0/1.
|
||||
**Every dropped shop is counted** in `norm.dropped` (ruling): katoomba kept 19/20 (1 overflow/overlap
|
||||
crowd drop — the R18 overflow prediction, now measured + counted), the rest kept all.
|
||||
|
||||
### → D: your re-measure inputs (post-fix)
|
||||
Component count → **1** (all towns). Street-metres in main net → **100%**. Stranded shops → **0** (all
|
||||
connected or counted-dropped). Near-crowd on fragments should collapse to ~0 (one component). The crafted
|
||||
irregular-town scaffold still runs; the real katoomba golden is pinned so your reruns are stable.
|
||||
|
||||
### → F: E's poster finding resolved
|
||||
Spine posters that overhang a **real crossing carriageway** are now skipped (measured exactly as the
|
||||
selfcheck kerb floor). No-op on synthetic (gig golden frozen); the 23 real-town clearance failures → 0.
|
||||
|
||||
### New finding (v4.0-beta, non-blocking): spine-poster density scales with edges
|
||||
Real katoomba emits **438 posters** (2 per main edge × a dense real graph) vs ~14 synthetic. Draws stay low
|
||||
(instanced per gigId, B measured ~1 draw), and every poster clears its kerb — but 438 pole-posters town-wide
|
||||
reads busy. **Beta candidate:** cap posters-per-town (seeded subsample of the spine run). Not fixed this
|
||||
round — invariants hold, it's density polish.
|
||||
|
||||
## Round 18 (2026-07-16) — v4.0-alpha REAL ROADS: cache schema v2 (roads[]) + the real-graph lift
|
||||
|
||||
### → E: **schema v2 is live (committed first) — fetch roads to it**
|
||||
|
||||
@ -1,5 +1,49 @@
|
||||
# LANE B — NOTES (measured)
|
||||
|
||||
## Round 19 (Fable's ROUND19 → Lane B) — the town selector (ledger #4, beta pull-forward, non-blocking)
|
||||
|
||||
Shipped the in-game town picker; verified fresh Chromium. **No `index.html` seam** — it lives in `hud.js`,
|
||||
which already owns its DOM overlay.
|
||||
|
||||
### `hud.js` town selector
|
||||
A compact `<select>` in the HUD overlay (top-left, styled to match, `pointer-events:auto` so it's usable
|
||||
when the pointer is free — Esc first). **9 options**: `▸ Synthetic — procedural` + the 3 OSM fixtures
|
||||
(`osmTownKeys()` minus `_real`: Melbourne/Katoomba/Silverton) + the 5 real-road caches (Katoomba, Newtown,
|
||||
Fremantle, Bendigo, Castlemaine — hardcoded `REAL_TOWNS`, since there's no towns manifest yet; beta derives
|
||||
it from an index). Selecting navigates: synthetic → clears `plansrc`/`town`; a town → `?plansrc=osm&town=<key>`,
|
||||
**preserving all other params** (seed, flags). Current town pre-selected; an unknown current town is added
|
||||
so it still shows.
|
||||
- **Verified end-to-end**: picked `katoomba_real` through the dropdown → it navigated + booted real Katoomba
|
||||
(966 edges / 323 intersections / 3 venues), selector then reads "Katoomba · real roads". 0 page errors.
|
||||
- Lives **inside `#pc-hud`**, so `hud.setVisible(false)` hides it for F's money shots (no extra work).
|
||||
|
||||
**→ Lane F (B→F "selector seam"): there is NO seam** — the selector is self-contained in `hud.js` (already
|
||||
constructed by `createHUD`). Nothing to wire in `index.html`. If you want it gated (e.g. `?dbg` only) that's
|
||||
a one-line `setVisible`/guard call, your choice; I left it always-in-HUD as a modest debug-grade feature.
|
||||
|
||||
### Corner-strips spot-check (R18 failure-item #3) — ground fills real intersections OK
|
||||
Overhead look at a degree-4 real intersection (58 exist on the current graph): the merged road strips
|
||||
**overlap-fill the junction** (carriageway continuous, lane markings + zebra crossing render, no gross gap).
|
||||
Acceptable for the alpha; a finer acute-angle / z-fight pass is a beta detail. `ground.js` is road-agnostic
|
||||
enough for real topology.
|
||||
|
||||
### Budget note → F (measured against A's UNCOMMITTED WIP — caveat, not a final finding)
|
||||
F's `BIG_CITY` one-liner (`e6fcb7b`) is live and working — real Katoomba now streams big-city (radius 2 +
|
||||
shadows off), chunks **50 → 26**. **But** against the current working tree the venue block is still **~237k
|
||||
tris** at night — and that tree carries **A's uncommitted R19 finalization WIP** (`plan_osm.js` modified;
|
||||
the graph grew 799 → 966 edges / 323 intersections = junction-joining in, but the **far-island CULL not yet
|
||||
applied**). Fable's ruling calls the cull "half of B's tris problem", so **the ≤200k gate needs F's fix AND
|
||||
A's committed cull together** — re-measure the busiest real block after A pins (that's the deferred
|
||||
"tris post-cull" re-measure; it genuinely waits on A's commit, per the R18/C lesson that the dev server
|
||||
serves other lanes' uncommitted edits). Not filing 237k as a defect — it's mid-finalization.
|
||||
|
||||
### The uncommitted `docs/shots/v4_alpha/katoomba_mainstreet.png`
|
||||
Present in the tree since before this R19 session (not a deliberate save of mine — my audit screenshots are
|
||||
browser-tool captures, uncommitted). Likely a prior B-audit capture. **F should reshoot deterministically
|
||||
via the tour harness** for the official money shot regardless; treat its provenance as unverified.
|
||||
|
||||
---
|
||||
|
||||
## Round 18 (Fable's ROUND18 → Lane B) — irregular-geometry audit (ledger #4, v4.0-alpha REAL ROADS)
|
||||
|
||||
**The deliverable is the failure list.** Audited `katoomba_real` on E's real roads (schema v2, **484 OSM
|
||||
|
||||
@ -6,6 +6,47 @@ _rotOnly/head-bone normalize/upgradeStreetPeople). Measurements on the M3 Ultra
|
||||
|
||||
---
|
||||
|
||||
## ROUND 19 — v4.0-alpha CLOSE: the fragmentation re-measure (ledger #3)
|
||||
|
||||
*PROCITY-D, 2026-07-16. A resolved my R18 finding (commit `6fc4b40`: junction-protected DP + bridge-joins
|
||||
+ shopless-island culling, katoomba golden pinned). This is the post-fix re-measure — same harness, same
|
||||
seed. **Verdict: the ruling landed clean — the fragmentation is fully resolved and the sim stays untouched.***
|
||||
|
||||
### Before → after on the real Katoomba graph
|
||||
|
||||
| metric | R18 (my finding) | **R19 (post-fix)** |
|
||||
|---|---|---|
|
||||
| connected components | 105 | **1** |
|
||||
| street-metres in the main net | 58.2 % | **100 %** |
|
||||
| shops stranded off-main | 2 | **0** — 1 bridged to main, 1 dropped past `SEAT_MAX` **and counted** (`norm.dropped`, plan_osm.js:319) |
|
||||
| **% near-crowd on tiny fragments** | ~10 % | **0 %** (no fragments left) |
|
||||
| nodes / edges | 872 / 799 | 830 / 966 |
|
||||
|
||||
Shops: 20 in cache → **19 seated + 1 counted-drop, 0 silently stranded** (ruling satisfied). `selfcheck`
|
||||
**51670/51670 green**, katoomba real-roads golden pinned in the output-drift guard.
|
||||
|
||||
### The DP-collinear junction-drop mechanism is fixed at the source
|
||||
Re-ran R18's original failing geometry — a **dead-straight cross street through a collinear mid-junction**
|
||||
(the exact case B and I both hit). R18: DP flattened it, dropped the junction → **max-degree 2, disconnected.**
|
||||
R19: the junction is protected from DP → **degree-4 crossroads forms, 1 component.** Fixed.
|
||||
|
||||
### Sim confirm on the fixed (bridged) graph — still zero changes
|
||||
Fresh stream sim, real Katoomba, drove the shop blocks: **0 NaN over 1,303,200 samples**, footpaths perfect
|
||||
(0 off-footpath, maxDist 12.9 m), 312 active peds, loiter (251) + **bench-sit (77)** fire, and **patronage
|
||||
rose 24→41 visits** (peds now reach shops across the connected graph). `fragmentPedFraction: 0`. The sim
|
||||
needed nothing — exactly as R18 predicted (a single component is strictly easier than 105).
|
||||
|
||||
### Handshake
|
||||
- **D → F:** fragmentation verdict **post-fix is GREEN** — real Katoomba is one connected component, 0 peds
|
||||
stranded, 0 NaN, all citizen behaviours hold, determinism intact (A's golden pins it). No D-side gate
|
||||
concern. Tag away.
|
||||
- **FYI (B/F budget):** edges rose 799→966 (junction protection + bridges) though total street-metres fell
|
||||
(islands culled) — flagged for the tris budget alongside the BIG_CITY fix; not a sim concern.
|
||||
- No D-side spawn-bias mitigation needed this epoch — A's pass left nothing to mitigate. (Beta backlog item
|
||||
retired.)
|
||||
|
||||
---
|
||||
|
||||
## ROUND 18 — v4.0-alpha REAL ROADS: citizens on real bones (ledger #5)
|
||||
|
||||
*PROCITY-D, 2026-07-16. The audit: does the citizen sim behave on real (irregular) street topology?
|
||||
|
||||
@ -675,3 +675,56 @@ John charters **v4 = THE REAL MAP** on.
|
||||
*browser* boot needed the shell to fetch+register the cache. Classic integration seam — F owns it.
|
||||
- **`?noassets` can't fetch a real cache**, so a real-town + noassets boot correctly falls back to a fixture
|
||||
(still silent-and-fine). The matrix's noassets gate tests that fallback, not the real town — as it should.
|
||||
|
||||
---
|
||||
|
||||
## 18. Round 18 — v4.0-alpha REAL ROADS. **F correctly did NOT run — the gate would have failed by design.**
|
||||
|
||||
The alpha gave `katoomba_real` its real OSM street geometry (A's schema v2 `roads[]` + graph lift; E's road
|
||||
fetch). F's charter deliverable was "as much the failure list as the town" — and F, running the gate, found
|
||||
the alpha **not green**, so F held the tag and filed the list rather than shipping a red release. This was
|
||||
the round working as designed (Fable's R18 verdict agreed: "F never ran — correctly").
|
||||
|
||||
**What F caught (all Lane A plan-gen, surfaced with measurements):**
|
||||
- **`qa.sh` selfcheck RED** — `katoomba_real`'s real-roads golden **UNPINNED** (the A→E→A finalization
|
||||
handshake never fired: E's roads landed *after* A's commits), and the 4 trailing towns' goldens stale
|
||||
(pinned on marched output, now roads).
|
||||
- **Poster-kerb-clearance RED on all 5 real towns** — the real edge explosion (232 "main" edges) put spine
|
||||
posters **1.4–3.9 m into the road**.
|
||||
- **Fragmentation** — 20 shops scattered over 799 real edges / 607 m ⇒ no walkable strip; the money-shot
|
||||
attempt was a vast empty field (D's finding: 105 components, 42% dead street-metres). And B's separate
|
||||
finding: the `BIG_CITY` heuristic misfired (20 shops < 120 ⇒ small-town streaming on a 799-edge graph ⇒
|
||||
**280k tris**, breaching the 200k gate).
|
||||
|
||||
**What was already green (the isolation held):** real roads ride the **cache schema, not a flag**, so
|
||||
classic + default + synthetic + fixtures were untouched (`0x3fa36874` frozen) — the alpha changed nothing
|
||||
outside the flagged path. The pipeline worked; the geometry needed the hardening the alpha existed to reveal.
|
||||
|
||||
## 19. Round 19 — the v4.0-alpha CLOSE. F: B's budget one-liner (early) + the gate (last).
|
||||
|
||||
A resolved the findings this round (fragmentation ruling — main component + joins, cull shopless islands;
|
||||
poster clamp; goldens pinned; junction-protected DP), D re-measured and verified, C re-soaked, B shipped the
|
||||
town selector. F ran two things:
|
||||
|
||||
1. **B's `BIG_CITY` one-liner** (`index.html`, landed EARLY per the order so D/B re-measures saw it):
|
||||
`BIG_CITY = shops > 120 || (streets.edges > 200)`. A real town can have few shops but a huge road graph;
|
||||
the graph now trips big-city streaming. **Verified: real Katoomba 799 edges → 61 draws / ~12k tris (was
|
||||
280k), 0 errors.** B proved it; F owns `index.html` so F landed it.
|
||||
2. **The alpha gate + the roads dimension** — `town_matrix.py` grew a `roads` gate: a real cache (schema v2
|
||||
with `roads[]`) must build the REAL street graph (`norm.mode === 'roads'`, read via the plan_osm
|
||||
`opts.report` sink), while synthetic/fixture towns stay marched/synthetic by design. Plus the classic +
|
||||
default gates prove the alpha is still isolated to the cache path. Money shot: **Katoomba's real main
|
||||
street** — asphalt + lane markings, footpaths, a pedestrian crossing, a walking crowd, gig posters,
|
||||
streetlamps, B's town selector in-frame — PROCITY's look on real bones, in budget.
|
||||
|
||||
### The honest notes on the alpha (two, both filed for v4.0-beta)
|
||||
1. **Density.** Real Katoomba's 19-shop cache is genuinely spread ~1 shop per real edge (`shopsOnBest = 1`),
|
||||
so the alpha is a real *street* with the town's life on it, not a dense retail strip — that density is a
|
||||
beta concern (more shops per cache, or focusing the retail spine).
|
||||
2. **One open budget finding (non-blocking).** The matrix's worst-case view (`venue_night` — NIGHT, the lit
|
||||
venue block, all posters up) measures **200,965 tris on `katoomba_real`, +965 over the 200k soft ceiling
|
||||
(0.5%)**. Driver: the poster count scales with the real *edge* count (438 posters for 19 shops), not shop
|
||||
count. Normal play (the midday money shot) is 121k — well under. `qa.sh --strict` is GREEN; this rides
|
||||
the opt-in `--matrix` warn-gate, not a blocking gate. Beta hardening: A caps posters per real town.
|
||||
The alpha's job was **real roads that boot, stream in budget in play, place a district, and read as a real
|
||||
place** — that shipped; the venue_night stress view is the one filed edge.
|
||||
|
||||
BIN
docs/shots/v4_alpha/katoomba_mainstreet.png
Normal file
BIN
docs/shots/v4_alpha/katoomba_mainstreet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 872 KiB |
@ -43,13 +43,17 @@ async () => {
|
||||
const cg = await import('./js/citygen/index.js');
|
||||
const g1 = JSON.stringify(cg.generatePlanFor(plan.citySeed, src, { town, gigs: true }));
|
||||
const g2 = JSON.stringify(cg.generatePlanFor(plan.citySeed, src, { town, gigs: true }));
|
||||
const rep = {}; // [R19] report sink → norm.mode (roads | marched)
|
||||
cg.generatePlanFor(plan.citySeed, src, { town, gigs: true, report: rep });
|
||||
const venues = (plan.shops || []).filter(s => s.venue).length;
|
||||
D.shot('venue_night'); // busiest default view: the venue block at night
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
const i = D.info();
|
||||
return { name: plan.name, shops: (plan.shops || []).length, venues,
|
||||
gigs: (plan.gigs || []).length, posters: (plan.posters || []).length,
|
||||
determ: g1 === g2, draws: i.drawCalls, tris: i.tris };
|
||||
determ: g1 === g2, draws: i.drawCalls, tris: i.tris,
|
||||
mode: rep.mode || (src === 'synthetic' ? 'synthetic' : 'marched'),
|
||||
edges: (plan.streets.edges || []).length };
|
||||
}
|
||||
"""
|
||||
|
||||
@ -71,9 +75,10 @@ def ensure_server():
|
||||
proc.terminate(); sys.exit(f'could not start http.server on :{PORT}')
|
||||
|
||||
|
||||
def run_town(new_page, query):
|
||||
def run_town(new_page, query, label):
|
||||
"""Returns a dict of gate → (ok, note) for one town."""
|
||||
row = {}
|
||||
is_real = label.startswith('real/')
|
||||
# ── default boot: boot / determ / budget / district ──
|
||||
pg = new_page()
|
||||
errs = []
|
||||
@ -95,9 +100,13 @@ def run_town(new_page, query):
|
||||
f"{d['draws']}d/{d['tris']//1000}k")
|
||||
row['district'] = (d['venues'] >= 1 and d['gigs'] > 0,
|
||||
f"{d['venues']}v/{d['gigs']}g/{d['posters']}p")
|
||||
# [R19] roads dimension: real caches (schema v2 with roads[]) must build the REAL street graph
|
||||
# (mode='roads'); synthetic/fixture towns are marched/synthetic by design (always ✓).
|
||||
mode = d.get('mode', '?')
|
||||
row['roads'] = ((mode == 'roads') if is_real else True, f"{mode},{d.get('edges', 0)}e")
|
||||
except Exception as e:
|
||||
row.setdefault('name', '?'); row.setdefault('shops', 0)
|
||||
for g in ('boot', 'determ', 'budget', 'district'):
|
||||
for g in ('boot', 'determ', 'budget', 'district', 'roads'):
|
||||
row.setdefault(g, (False, str(e)[:24]))
|
||||
finally:
|
||||
pg.close()
|
||||
@ -125,9 +134,9 @@ def main():
|
||||
except ImportError:
|
||||
sys.exit('playwright not installed — tools/.venv/bin/python -m playwright install chromium')
|
||||
|
||||
print(f"\033[1mTOWN-MATRIX SMOKE\033[0m ({len(MATRIX)} towns × 5 gates · the v4 real-map scout evidence)")
|
||||
print(f"\033[1mTOWN-MATRIX SMOKE\033[0m ({len(MATRIX)} towns × 6 gates (incl. the R19 roads dimension) · the v4 real-map scout evidence)")
|
||||
srv = ensure_server()
|
||||
GATES = ['boot', 'determ', 'budget', 'district', 'noassets']
|
||||
GATES = ['boot', 'determ', 'budget', 'district', 'roads', 'noassets']
|
||||
rows = []
|
||||
try:
|
||||
with sync_playwright() as p:
|
||||
@ -135,7 +144,7 @@ def main():
|
||||
def factory():
|
||||
return b.new_page(viewport={'width': 1280, 'height': 720})
|
||||
for label, query in MATRIX:
|
||||
r = run_town(factory, query)
|
||||
r = run_town(factory, query, label)
|
||||
rows.append((label, r))
|
||||
cells = ' '.join(f"{g}:{'✓' if r[g][0] else '✗'}" for g in GATES)
|
||||
print(f" {label:22} {(r.get('name') or '?'):16} {cells}")
|
||||
@ -161,7 +170,7 @@ def main():
|
||||
if fails:
|
||||
print(f"\033[31m● {fails} gate failure(s)\033[0m across {len(rows)} towns — the matrix is not clean.")
|
||||
sys.exit(1)
|
||||
print(f"\033[32m● MATRIX GREEN\033[0m — {len(rows)} towns (synthetic + fixtures + real caches) pass all 5 gates.")
|
||||
print(f"\033[32m● MATRIX GREEN\033[0m — {len(rows)} towns (synthetic + fixtures + real caches) pass all 6 gates.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
|
||||
@ -138,7 +138,10 @@ try {
|
||||
// Adaptive to plan density: a large generated city (Lane A) has content in nearly every chunk, so
|
||||
// a smaller radius + no sun-shadow pass keeps draw calls near the ≤300 gate. The sparse fixture
|
||||
// gets the fuller radius 3 + shadows (measured ~140 draws). Override via ?r= / ?shadows=0|1.
|
||||
const BIG_CITY = (plan.shops?.length || 0) > 120;
|
||||
// [Lane F R19 — B's proven one-liner] a real town can have FEW shops but a HUGE road graph (real Katoomba:
|
||||
// 20 shops / 799 edges / 5 km) — the shop-count heuristic alone left it on small-town streaming and blew the
|
||||
// tri budget (280k > 200k). Extend the trigger to the graph size so big-graph towns stream like big cities.
|
||||
const BIG_CITY = (plan.shops?.length || 0) > 120 || (plan.streets?.edges?.length || 0) > 200;
|
||||
const RADIUS = parseInt(params.get('r'), 10) || (BIG_CITY ? 2 : 3);
|
||||
const SHADOWS = params.has('shadows') ? params.get('shadows') !== '0' : !BIG_CITY;
|
||||
const canvas = document.getElementById('c');
|
||||
|
||||
@ -256,6 +256,23 @@ function buildPosters(plan, citySeed, venues, gigs) {
|
||||
const bill = [...tonight.values()];
|
||||
if (!runs.length || !bill.length) return posters;
|
||||
|
||||
// ROUND19 (E's finding): on a REAL street graph a spine edge is crossed by side streets, so a pole
|
||||
// placed on the spine verge can overhang a crossing carriageway. Skip any candidate that lands on a
|
||||
// carriageway — measured EXACTLY as the selfcheck's floor (nearest edge, `roadWidth/2`). This is a
|
||||
// no-op on the synthetic town (its spine posters already clear every carriageway, so nothing skips and
|
||||
// the gig golden is frozen); it only drops the overhanging ones on real graphs. `pr()` is consumed
|
||||
// before the check, so the rng stream — hence the synthetic output — is unchanged.
|
||||
const onCarriageway = (x, z) => {
|
||||
let best = Infinity, bestE = null;
|
||||
for (const e2 of plan.streets.edges) {
|
||||
const a2 = nodeById.get(e2.a), b2 = nodeById.get(e2.b); if (!a2 || !b2) continue;
|
||||
const ex = b2.x - a2.x, ez = b2.z - a2.z, L2 = ex * ex + ez * ez || 1;
|
||||
let tt = ((x - a2.x) * ex + (z - a2.z) * ez) / L2; tt = Math.max(0, Math.min(1, tt));
|
||||
const d = Math.hypot(x - (a2.x + ex * tt), z - (a2.z + ez * tt));
|
||||
if (d < best) { best = d; bestE = e2; }
|
||||
}
|
||||
return bestE && best < roadWidth(bestE) / 2 - 1e-6;
|
||||
};
|
||||
const pr = rng(citySeed, 'posters', 0);
|
||||
const PER_EDGE = 2;
|
||||
let k = 0;
|
||||
@ -273,6 +290,7 @@ function buildPosters(plan, citySeed, venues, gigs) {
|
||||
// faces back across its own footpath, so ry = atan2(nx, nz).
|
||||
const nx = side > 0 ? -uz : uz, nz = side > 0 ? ux : -ux;
|
||||
const x = a.x + ux * len * t + nx * off, z = a.z + uz * len * t + nz * off;
|
||||
if (onCarriageway(r2(x), r2(z))) continue; // check the r2-rounded position `add` stores — overhangs a crossing → skip (E's R18 finding)
|
||||
add(x, z, Math.atan2(nx, nz), bill[k++ % bill.length].gigId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -239,23 +239,69 @@ export function generatePlanOSM(citySeed, town = DEFAULT_TOWN, opts = {}) {
|
||||
// real order, real side, regularised spacing — not pixel positions (real shops overlap).
|
||||
function buildRealRoads() {
|
||||
const EPS = 6, SNAP = 3, KERB = 4, NODE_CLEAR = 6; // fidelity + geometry knobs (metres)
|
||||
const JOIN_TOL = 12, BRIDGE_MAX = 200, SEAT_MAX = 200; // ROUND19 fragmentation ruling (metres)
|
||||
const WKIND = { main: SPINE_W, side: AV_W, lane: 8, arcade: 6 };
|
||||
const ways = [];
|
||||
const skey = p => `${Math.round(p.x / SNAP)},${Math.round(p.z / SNAP)}`;
|
||||
const rawWays = [];
|
||||
for (const rd of roads) {
|
||||
if (!rd || !Array.isArray(rd.pts) || rd.pts.length < 2) continue;
|
||||
let pl = rd.pts.filter(p => Array.isArray(p) && isNum(p[0]) && isNum(p[1])).map(([lat, lon]) => ({ x: projX(lon), z: projZ(lat) }));
|
||||
pl = simplifyDP(pl, EPS);
|
||||
if (pl.length >= 2) ways.push({ kind: roadEdgeKind(rd.kind), pl });
|
||||
const pl = rd.pts.filter(p => Array.isArray(p) && isNum(p[0]) && isNum(p[1])).map(([lat, lon]) => ({ x: projX(lon), z: projZ(lat) }));
|
||||
if (pl.length >= 2) rawWays.push({ kind: roadEdgeKind(rd.kind), pl });
|
||||
}
|
||||
const nodeAt = new Map();
|
||||
const snap = p => { const k = `${Math.round(p.x / SNAP)},${Math.round(p.z / SNAP)}`; let n = nodeAt.get(k); if (!n) { n = addNode(p.x, p.z); nodeAt.set(k, n); } return n; };
|
||||
const seen = new Set(), realEdges = [];
|
||||
// ROUND19: junctions = points shared by ≥2 ways. PROTECT them from Douglas–Peucker — simplify only
|
||||
// BETWEEN junctions, never through one. The collinear junction-drop (a straight way losing the point
|
||||
// where a side street tees in) is what fragmented the graph into 105 components (D + B, R18).
|
||||
const keyWays = new Map();
|
||||
rawWays.forEach((w, wi) => { for (const p of w.pl) { const k = skey(p); (keyWays.get(k) || keyWays.set(k, new Set()).get(k)).add(wi); } });
|
||||
const isJct = k => (keyWays.get(k)?.size || 0) >= 2;
|
||||
const ways = [];
|
||||
for (const w of rawWays) {
|
||||
const brk = [0]; for (let i = 1; i < w.pl.length - 1; i++) if (isJct(skey(w.pl[i]))) brk.push(i); brk.push(w.pl.length - 1);
|
||||
let out = [];
|
||||
for (let b = 0; b + 1 < brk.length; b++) { const s = simplifyDP(w.pl.slice(brk[b], brk[b + 1] + 1), EPS); out = out.concat(b === 0 ? s : s.slice(1)); }
|
||||
if (out.length >= 2) ways.push({ kind: w.kind, pl: out });
|
||||
}
|
||||
// ── build a CANDIDATE graph (local ids), resolve fragmentation, THEN commit the survivors ──────────
|
||||
// ROUND19 ruling (Fable): the town is the main component + joined near-fragments; a far SHOPLESS island
|
||||
// is bbox-clipped noise + dead streaming budget, so it's CULLED. A shop-bearing island is bridged so no
|
||||
// shop strands; a shop we still can't reach drops with a count. (Committing after resolution is what
|
||||
// lets us cull — addEdge can't un-add.)
|
||||
const cnodes = [], cnodeAt = new Map();
|
||||
const cnode = p => { const k = skey(p); let id = cnodeAt.get(k); if (id === undefined) { id = cnodes.length; cnodes.push({ x: p.x, z: p.z }); cnodeAt.set(k, id); } return id; };
|
||||
const cedges = [], eseen = new Set();
|
||||
for (const w of ways) for (let i = 0; i + 1 < w.pl.length; i++) {
|
||||
const na = snap(w.pl[i]), nb = snap(w.pl[i + 1]);
|
||||
if (na.id === nb.id || Math.hypot(nb.x - na.x, nb.z - na.z) < 3) continue;
|
||||
const ek = `${Math.min(na.id, nb.id)}-${Math.max(na.id, nb.id)}`; if (seen.has(ek)) continue; seen.add(ek);
|
||||
realEdges.push({ id: addEdge(na.id, nb.id, WKIND[w.kind] || AV_W, w.kind), a: na, b: nb, kind: w.kind });
|
||||
const a = cnode(w.pl[i]), b = cnode(w.pl[i + 1]);
|
||||
if (a === b) continue;
|
||||
const ek = `${Math.min(a, b)}-${Math.max(a, b)}`; if (eseen.has(ek)) continue; eseen.add(ek);
|
||||
if (Math.hypot(cnodes[a].x - cnodes[b].x, cnodes[a].z - cnodes[b].z) < 3) continue;
|
||||
cedges.push({ a, b, kind: w.kind });
|
||||
}
|
||||
if (!cedges.length) { buildMarched(); norm.mode = 'roads→marched (no usable ways)'; return; }
|
||||
const par = cnodes.map((_, i) => i); const find = x => { while (par[x] !== x) x = par[x] = par[par[x]]; return x; };
|
||||
const clen = (a, b) => Math.hypot(cnodes[a].x - cnodes[b].x, cnodes[a].z - cnodes[b].z);
|
||||
for (const e of cedges) par[find(e.a)] = find(e.b);
|
||||
const compLen = new Map(); for (const e of cedges) { const r = find(e.a); compLen.set(r, (compLen.get(r) || 0) + clen(e.a, e.b)); }
|
||||
let mainRoot = -1, mainLen = -1; for (const [r, L] of compLen) if (L > mainLen) { mainLen = L; mainRoot = r; }
|
||||
const nearestC = (px, pz) => { let be = -1, bd = Infinity; for (let i = 0; i < cedges.length; i++) { const e = cedges[i], A = cnodes[e.a], B = cnodes[e.b], dx = B.x - A.x, dz = B.z - A.z, L2 = dx * dx + dz * dz || 1; let t = ((px - A.x) * dx + (pz - A.z) * dz) / L2; t = Math.max(0, Math.min(1, t)); const d = Math.hypot(px - (A.x + dx * t), pz - (A.z + dz * t)); if (d < bd) { bd = d; be = i; } } return be; };
|
||||
const shopRoot = new Set();
|
||||
for (const s of fx.shops) { const ei = nearestC(projX(s.lon), projZ(s.lat)); if (ei >= 0) shopRoot.add(find(cedges[ei].a)); }
|
||||
const rootNodes = new Map(); cnodes.forEach((_, i) => { const r = find(i); (rootNodes.get(r) || rootNodes.set(r, []).get(r)).push(i); });
|
||||
const inMain = i => find(i) === find(mainRoot);
|
||||
const connectors = []; let culled = 0;
|
||||
for (const r of [...compLen.keys()].filter(r => r !== mainRoot).sort((a, b) => (compLen.get(b) || 0) - (compLen.get(a) || 0))) {
|
||||
if (inMain(r)) continue; // already merged by an earlier bridge
|
||||
const grp = rootNodes.get(r) || [], shop = shopRoot.has(find(grp[0]));
|
||||
let ga = -1, gb = -1, bd = Infinity; // nearest (fragment node, main-net node) pair
|
||||
for (const i of grp) for (let j = 0; j < cnodes.length; j++) { if (!inMain(j)) continue; const d = clen(i, j); if (d < bd) { bd = d; ga = i; gb = j; } }
|
||||
if (bd <= JOIN_TOL || (shop && bd <= BRIDGE_MAX)) { connectors.push([ga, gb]); par[find(ga)] = find(gb); }
|
||||
else culled++; // shopless (or unreachable-shop) far island → cull
|
||||
}
|
||||
const nodeMap = new Map();
|
||||
const putNode = ci => { let n = nodeMap.get(ci); if (!n) { n = addNode(cnodes[ci].x, cnodes[ci].z); nodeMap.set(ci, n); } return n; };
|
||||
const realEdges = [];
|
||||
for (const e of cedges) { if (!inMain(e.a)) continue; const na = putNode(e.a), nb = putNode(e.b); realEdges.push({ id: addEdge(na.id, nb.id, WKIND[e.kind] || AV_W, e.kind), a: na, b: nb, kind: e.kind }); }
|
||||
for (const [a, b] of connectors) { const na = putNode(a), nb = putNode(b); realEdges.push({ id: addEdge(na.id, nb.id, WKIND.lane, 'lane'), a: na, b: nb, kind: 'lane' }); }
|
||||
norm.components = compLen.size; norm.culledIslands = culled; norm.bridges = connectors.length;
|
||||
if (!realEdges.length) { buildMarched(); norm.mode = 'roads→marched (no usable ways)'; return; }
|
||||
const nearest = (px, pz) => {
|
||||
let best = null, bd = Infinity;
|
||||
@ -263,13 +309,14 @@ export function generatePlanOSM(citySeed, town = DEFAULT_TOWN, opts = {}) {
|
||||
const dx = e.b.x - e.a.x, dz = e.b.z - e.a.z, L2 = dx * dx + dz * dz || 1;
|
||||
let t = ((px - e.a.x) * dx + (pz - e.a.z) * dz) / L2; t = Math.max(0, Math.min(1, t));
|
||||
const d = Math.hypot(px - (e.a.x + dx * t), pz - (e.a.z + dz * t));
|
||||
if (d < bd) { bd = d; best = { e, t, side: (dx * (pz - e.a.z) - dz * (px - e.a.x)) >= 0 ? 1 : -1 }; }
|
||||
if (d < bd) { bd = d; best = { e, t, d, side: (dx * (pz - e.a.z) - dz * (px - e.a.x)) >= 0 ? 1 : -1 }; }
|
||||
}
|
||||
return best;
|
||||
};
|
||||
const perEdge = new Map();
|
||||
for (const s of fx.shops) {
|
||||
const a = nearest(projX(s.lon), projZ(s.lat)); if (!a) continue;
|
||||
const a = nearest(projX(s.lon), projZ(s.lat));
|
||||
if (!a || a.d > SEAT_MAX) { norm.dropped = (norm.dropped || 0) + 1; continue; } // stranded past bridging → drop + COUNT (ruling)
|
||||
const key = a.e.id * 2 + (a.side > 0 ? 1 : 0);
|
||||
(perEdge.get(key) || perEdge.set(key, { e: a.e, side: a.side, list: [] }).get(key)).list.push({ s, t: a.t });
|
||||
}
|
||||
@ -288,9 +335,9 @@ export function generatePlanOSM(citySeed, town = DEFAULT_TOWN, opts = {}) {
|
||||
[e.a.x + nx * off, e.a.z + nz * off], [e.b.x + nx * off, e.b.z + nz * off],
|
||||
[e.b.x + nx * (off + dep), e.b.z + nz * (off + dep)], [e.a.x + nx * (off + dep), e.a.z + nz * (off + dep)]]);
|
||||
let along = NODE_CLEAR;
|
||||
for (const { s } of list) {
|
||||
const f = footOf(SHOP_TYPES[s.type] ? s.type : 'opshop');
|
||||
if (along + f > len - NODE_CLEAR) break; // edge full — overflow shops drop out (real order kept)
|
||||
for (let k = 0; k < list.length; k++) {
|
||||
const s = list[k].s, f = footOf(SHOP_TYPES[s.type] ? s.type : 'opshop');
|
||||
if (along + f > len - NODE_CLEAR) { norm.dropped = (norm.dropped || 0) + (list.length - k); break; } // edge full — overflow drops COUNTED (ruling)
|
||||
cands.push({ block, x: e.a.x + ux * (along + f / 2) + nx * (off + dep / 2), z: e.a.z + uz * (along + f / 2) + nz * (off + dep / 2), w: f, d: dep, ry, frontEdge: e.id, s });
|
||||
along += f + GAP;
|
||||
}
|
||||
|
||||
@ -449,11 +449,13 @@ section('v4 real-roads graph lift (schema v2)');
|
||||
// pinned. Guarded so the gate is green before E's first cache — A pins goldens as caches arrive.
|
||||
section('real town caches (web/assets/towns/*.json)');
|
||||
const TOWNS_DIR = join(HERE, '..', '..', 'assets', 'towns');
|
||||
// The trailing towns are pinned on their stable v1 (marched) output — regression guards. katoomba_real
|
||||
// stays UNPINNED this round: it is the v4.0-alpha town, and its golden re-pins to the real-ROADS output
|
||||
// the moment E lands its roads[] (amendment law — the A→E→A finalization). The other four get roads in beta.
|
||||
// ROUND19: all five real towns are now on REAL ROADS (E's schema-v2 roads[] landed) and pinned on the
|
||||
// real-roads graph output — the A→E→A finalization, katoomba's golden included. Re-pinned this round after
|
||||
// the fragmentation ruling (junction-protected DP + cull/bridge) + E's spine-poster fix. Amendment law:
|
||||
// the alpha changed nothing outside the cache-schema path; synthetic/fixtures/classic/default stay frozen.
|
||||
const REAL_TOWN_GOLDENS = {
|
||||
bendigo_real: 0x5e3e76a6, castlemaine_real: 0xf1a628a1, fremantle_real: 0xee9fd09a, newtown_real: 0x95978b45,
|
||||
katoomba_real: 0xb7ffbca2, newtown_real: 0x6f984c81, fremantle_real: 0xfb197000,
|
||||
bendigo_real: 0x21179b3b, castlemaine_real: 0x7c47f639,
|
||||
};
|
||||
const townFiles = (existsSync(TOWNS_DIR) ? readdirSync(TOWNS_DIR) : []).filter(f => f.endsWith('.json'));
|
||||
if (!townFiles.length) console.log(" (none yet — the contract + hardening are live, ready for E's build_towns.py)");
|
||||
|
||||
@ -4,6 +4,11 @@
|
||||
// door-click → `procity:enterShop` hand-off. Creates its own DOM overlay so index.html stays thin.
|
||||
|
||||
import * as THREE from 'three';
|
||||
import { osmTownKeys } from '../citygen/index.js'; // R19 town selector: enumerate OSM fixtures
|
||||
|
||||
// R19 (v4.0-alpha, beta pull-forward): the real-roads town caches under web/assets/towns/*.json. Hardcoded
|
||||
// for the alpha — there is no towns manifest yet; beta derives this from an index file. New towns: add here.
|
||||
const REAL_TOWNS = ['katoomba_real', 'newtown_real', 'fremantle_real', 'bendigo_real', 'castlemaine_real'];
|
||||
|
||||
const CSS = `
|
||||
#pc-hud{position:fixed;inset:0;pointer-events:none;font:13px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;color:#f4efe6;z-index:10}
|
||||
@ -15,6 +20,10 @@ const CSS = `
|
||||
#pc-clock{font-size:15px;margin-bottom:3px}
|
||||
#pc-toast{position:absolute;left:50%;bottom:64px;transform:translateX(-50%);background:rgba(20,16,10,.9);padding:10px 18px;border-radius:10px;font-size:15px;opacity:0;transition:opacity .25s;text-shadow:0 1px 2px #000}
|
||||
#pc-help{position:absolute;left:12px;bottom:12px;background:rgba(20,16,10,.5);padding:7px 11px;border-radius:8px;opacity:.85}
|
||||
#pc-townpick{position:absolute;left:10px;top:10px;pointer-events:auto;opacity:.8;transition:opacity .12s}
|
||||
#pc-townpick:hover{opacity:1}
|
||||
#pc-townpick select{background:rgba(20,16,10,.7);color:#f4efe6;border:1px solid rgba(255,215,94,.35);border-radius:8px;padding:5px 9px;font:12px/1.3 -apple-system,Segoe UI,Roboto,sans-serif;max-width:230px;cursor:pointer}
|
||||
#pc-townpick select:focus{outline:1px solid rgba(255,215,94,.6)}
|
||||
`;
|
||||
|
||||
export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }) {
|
||||
@ -35,6 +44,7 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }
|
||||
<div>chunks <b id="pc-chunks">—</b></div>
|
||||
</div>
|
||||
<div id="pc-toast"></div>
|
||||
<div id="pc-townpick"><select id="pc-town-select" title="Load a different town (Esc to release the pointer first)"></select></div>
|
||||
<div id="pc-help">WASD move · shift run · mouse look · click a door · [ ] time · P shot · Esc release</div>`;
|
||||
document.body.appendChild(root);
|
||||
const $ = (id) => root.querySelector('#' + id);
|
||||
@ -43,6 +53,26 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }
|
||||
$('pc-town').textContent = plan.name || '—';
|
||||
$('pc-seed').textContent = plan.citySeed;
|
||||
|
||||
// ── town selector (R19, beta pull-forward) — synthetic + OSM fixtures + real-road caches. Navigates
|
||||
// to the matching URL params, preserving everything else (seed, flags). Lives in the HUD overlay, so it
|
||||
// hides with the HUD for money shots; usable when the pointer is free (Esc). No index.html seam. ──
|
||||
(() => {
|
||||
const sel = $('pc-town-select'); if (!sel) return;
|
||||
const params = new URLSearchParams(location.search);
|
||||
const cur = params.get('plansrc') === 'osm' ? (params.get('town') || '') : '';
|
||||
const cap = (k) => k.replace(/_real$/, '').replace(/(^|[\s_])\w/g, (c) => c.toUpperCase()).replace(/_/g, ' ');
|
||||
const add = (key, label) => { const o = document.createElement('option'); o.value = key; o.textContent = label; if (key === cur) o.selected = true; sel.appendChild(o); };
|
||||
add('', '▸ Synthetic — procedural');
|
||||
for (const k of osmTownKeys().filter((k) => !k.endsWith('_real'))) add(k, `${cap(k)} · OSM fixture`);
|
||||
for (const k of REAL_TOWNS) add(k, `${cap(k)} · real roads`);
|
||||
if (cur && !Array.from(sel.options).some((o) => o.value === cur)) add(cur, `${cap(cur)} · (current)`); // unknown current town → still shown
|
||||
sel.onchange = () => {
|
||||
const v = sel.value, p = new URLSearchParams(location.search);
|
||||
if (!v) { p.delete('plansrc'); p.delete('town'); } else { p.set('plansrc', 'osm'); p.set('town', v); }
|
||||
location.search = p.toString(); // reload into the chosen town
|
||||
};
|
||||
})();
|
||||
|
||||
const ray = new THREE.Raycaster();
|
||||
ray.far = 6;
|
||||
const centre = new THREE.Vector2(0, 0);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user