Lane F R22 (v4.0 — THE EPOCH CLOSE): the re-measure, the selector, the release tour, the tag
The blocking gate is CLOSED, and the epoch closes green. - #1b THE BLOCKING RE-MEASURE: F's R21 re-measure made the tri diet blocking; B diagnosed layer-by-layer, the driver traced A -> E (A's own commit: "B's numbers say the fix is E's"), E fixed it, F re-measured: katoomba venue_night 221,935 (+11%) -> 108,365 (-51%); bendigo 201k -> 124,682; fremantle -> 88,397. Worst 124,682 — the 200k stress edge is closed. The measure-first loop worked end to end. - #3 DBG.enterShop(selector) (Lane G's ask): id · registry type · exact name · name substring, resolved most-specific-first so an id-shaped string can't be shadowed. One-liner QA on any of 23 towns. - #4 The release tour -> docs/shots/release_v4/: the Fremantle hero · the Fitzroy mecca · the Toowoomba thin tail · the synthetic control · the ?classic=1 covenant. Every real-town frame poses through F's R21 cluster primitive; B's R22 bookmarks anchor on the same retail heart, so tour and bookmarks agree by construction. HUD hidden throughout (F's R21 ruling corollary). The primitive earned itself twice: the first cut posed via street_noon and the MECCA CAME OUT AS BARE BITUMEN — the same trap as R21. Re-posed through the cluster, Fitzroy reads. - HONEST FINDING (filed v4.x): the brief asked for the thin tail as "proof the floor holds". It does not, and the frame says so — Toowoomba reads as an empty road. The floor is a RATIO, not a count: 0.008 shops/edge vs the heroes' 0.06-0.08. Shipped labelled honestly rather than swapped for a flattering town. - #6 Docs: README v4 section (the towns ARE the product), LANE_F_NOTES §22, F-progress. qa.sh --strict --matrix GREEN 7/7; MATRIX GREEN — 10 towns × 7 gates (the budget gate green everywhere for the first time); selfcheck 161,300/161,300 (A's 36 goldens + the 23-town pack). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b0e4ce103e
commit
8ac3b18b36
@ -4,6 +4,23 @@
|
||||
|
||||
---
|
||||
|
||||
## Round 22 (v4.0 — THE EPOCH CLOSE) — F closed it
|
||||
|
||||
- **The blocking gate CLOSED.** F's R21 re-measure made the tri diet blocking; B diagnosed, the driver
|
||||
traced A→**E**, E fixed it, F re-measured: katoomba `venue_night` **221,935 → 108,365 (−51%)**, worst town
|
||||
124,682 — the 200k stress edge is closed. Selfcheck **161,300/161,300** (A's 36 goldens + 23 towns).
|
||||
- **`DBG.enterShop(selector)`** (Lane G's ask) — id · type · exact name · substring, most-specific-first.
|
||||
- **The release tour** → [`docs/shots/release_v4/`](docs/shots/release_v4/): Fremantle hero · Fitzroy mecca ·
|
||||
Toowoomba thin tail · synthetic control · `?classic=1` covenant. All posed through F's R21 cluster
|
||||
primitive — which earned itself twice: the first cut posed via `street_noon` and **the mecca came out as
|
||||
bare bitumen**; re-posed through the cluster, Fitzroy reads.
|
||||
- **⚠ Honest finding (filed v4.x):** the thin tail does **not** prove the floor holds — Toowoomba reads as an
|
||||
empty road. The floor is a **ratio**, not a count: 0.008 shops/edge vs the heroes' 0.06–0.08. The frame
|
||||
ships labelled honestly rather than swapped for a flattering town.
|
||||
- Docs (README v4 "the towns are the product", notes §22), `qa.sh --strict --matrix` green, tag **`v4.0`**.
|
||||
|
||||
---
|
||||
|
||||
## Round 20–21 (v4.0-beta THE FIVE TOWNS) — F never ran R20; closed R21
|
||||
|
||||
**R20: F never ran** (no gate, no tag) — the honest record is in LANE_F_NOTES §20. Unlike R18 (where F ran
|
||||
|
||||
28
README.md
28
README.md
@ -76,15 +76,27 @@ master off:
|
||||
| `?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**: 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`). **REAL ROADS:** a cache with `roads[]` (schema v2) builds the CityPlan street graph from the town's **actual OSM streets**; without `roads[]` a cache marches onto synthetic avenues. Or just **pick a town from the HUD selector** — no URL needed |
|
||||
|
||||
### v4.0-beta — the five towns, at density
|
||||
## v4.0 — THE REAL MAP: the towns *are* the product
|
||||
|
||||
Five real Australian towns (**Katoomba, Newtown, Fremantle, Bendigo, Castlemaine — 292 shops**) boot on
|
||||
their **actual OSM street graphs**, at real retail density: secondhand heroes keep their footfall while the
|
||||
street around them gets ~4× busier (texture, never a takeover). Venues bias toward the retail cluster, the
|
||||
tram runs only where it fronts a real high street (Newtown/Castlemaine — elsewhere it was "a highway bus",
|
||||
so it's fenced), and the in-HUD **town selector** reads the towns index, so you can just pick one. Every
|
||||
town: deterministic, districted, silent under `?noassets`, `?classic=1` still byte-identical to v2.
|
||||
See [`docs/shots/v4_beta/`](docs/shots/v4_beta/) and the charter, [`docs/V4_REAL_MAP.md`](docs/V4_REAL_MAP.md).
|
||||
**23 real Australian towns — 1,210 real shops, every state and territory — on their actual streets.** Not
|
||||
"inspired by": Fitzroy's shops sit on Fitzroy's roads, at their real positions, because the pipeline eats
|
||||
OpenStreetMap and marches it through the same CityPlan contract the procedural town uses. **Pick one from
|
||||
the in-HUD selector and walk it.** Everything built across v1–v3 comes along unchanged — the venues and their
|
||||
seeded week of gigs, the 4-piece band with the seated drummer, the crowd that follows you into the pub, the
|
||||
interiors behind every door, weather, the tram — because they all hang off the plan contract and never had
|
||||
to know the geography got real.
|
||||
|
||||
The towns run from **meccas** (Fitzroy, 160 shops) to the **thin tail** (12), at real retail density: the
|
||||
secondhand heroes keep their footfall while the street around them gets ~4× busier — texture, never a
|
||||
takeover. Venues bias to the retail heart. The tram only runs where it fronts a real high street (elsewhere
|
||||
it was honestly "a highway bus", so it's fenced). Everything stays **deterministic from the seed**, silent
|
||||
under `?noassets=1`, and **`?classic=1` still boots the exact v2 town, byte-identical forever** (`0x3fa36874`).
|
||||
|
||||
**The tour:** [`docs/shots/release_v4/contact.html`](docs/shots/release_v4/) — the Fremantle hero, the
|
||||
Fitzroy mecca, the thin tail (labelled honestly: the floor is shops-per-street-metre, and Toowoomba is
|
||||
below it), the synthetic control, and the `?classic=1` covenant made visible. Charter:
|
||||
[`docs/V4_REAL_MAP.md`](docs/V4_REAL_MAP.md) · data + licences (ODbL):
|
||||
[`web/assets/towns/SOURCES.md`](web/assets/towns/SOURCES.md).
|
||||
| `?seed=N` | `20261990` | town seed |
|
||||
|
||||
**Release tour:** [`docs/shots/release_v3_1/contact.html`](docs/shots/release_v3_1/) — the money shots on
|
||||
|
||||
@ -791,3 +791,52 @@ streetlamps, and a genuinely busy crowd (the density widening, visible). 126 dra
|
||||
Posed by projecting the densest **spatial** shop cluster (10 shops within 60 m) onto its front edge and
|
||||
standing on the road — per-edge shop counts are ~1 on a real graph, so "densest edge" is the wrong pose
|
||||
heuristic for real towns.
|
||||
|
||||
---
|
||||
|
||||
## 22. Round 22 — **v4.0: THE EPOCH CLOSE.** F: the re-measure, the selector, the tour, the tag.
|
||||
|
||||
### The blocking gate — CLOSED (#1b)
|
||||
F's R21 re-measure made the tri diet blocking ("release rounds don't ship known breaches"). B diagnosed
|
||||
layer-by-layer, the driver traced **A → E** (A's own commit said so: "B's numbers say the fix is E's"), and
|
||||
**E fixed it**. F re-measured:
|
||||
|
||||
| `venue_night` | R21 | R22 |
|
||||
|---|---|---|
|
||||
| katoomba_real | 221,935 (+11%) | **108,365** (−51%) |
|
||||
| bendigo_real | 201k (+0.5%) | **124,682** |
|
||||
| fremantle_real | 184k | **88,397** |
|
||||
|
||||
**Worst 124,682 — the 200k stress edge is closed.** Selfcheck **161,300/161,300** (A's 36 goldens + the
|
||||
23-town pack). The measure-first loop worked end to end: F measured, B diagnosed, E owned, F re-measured.
|
||||
|
||||
### `DBG.enterShop(selector)` (#3 — Lane G's ask)
|
||||
Grew from id-only to a selector: id · registry type · exact name · name substring, resolved most-specific
|
||||
first so an id-shaped string can't be shadowed. `DBG.enterShop('record')` / `('The Exchange Hotel')` / `(116)`
|
||||
— cross-repo QA one-liners on any of 23 towns without an id lookup.
|
||||
|
||||
### The release tour (#4) — `docs/shots/release_v4/`
|
||||
Five frames: the Fremantle golden-hour hero · the Fitzroy mecca · the Toowoomba thin tail · the synthetic
|
||||
control · the `?classic=1` covenant. Every real-town frame poses through **F's R21 cluster primitive**
|
||||
(project the densest SPATIAL cluster onto its front edge, stand on the road); B's R22 bookmark fix anchors
|
||||
on the same retail heart, so tour and bookmarks agree by construction. HUD hidden throughout.
|
||||
|
||||
**The primitive earned itself twice.** First cut of the tour posed via `DBG.shot('street_noon')` and the
|
||||
**mecca came out as bare bitumen** — the same trap as R21. Re-posed through the cluster primitive, Fitzroy
|
||||
reads (7 shops/60 m retail heart, trees, benches, crowd). *Per-edge is always the wrong heuristic on a real
|
||||
graph.*
|
||||
|
||||
### ⚠ The honest thin-tail finding (filed for v4.x)
|
||||
The brief asked for a thin-tail frame as **"proof the floor holds."** It doesn't, and the frame says so:
|
||||
**Toowoomba reads as an empty road, not a town.** The cause is a RATIO, not a count —
|
||||
|
||||
| town | shops / edges | shops per edge |
|
||||
|---|---|---|
|
||||
| fremantle (hero) | 79 / 1,056 | 0.075 |
|
||||
| fitzroy (mecca) | 139 / 2,408 | 0.058 |
|
||||
| **toowoomba (thin tail)** | **12 / 1,459** | **0.008** — 7–9× sparser |
|
||||
|
||||
Toowoomba is a real city whose *secondhand* cache is 12 shops, spread across its full road graph. The floor
|
||||
isn't "≥N shops" — it's **shops-per-street-metre**. v4.x options: clip the graph to the retail core for
|
||||
thin caches, or set a ratio floor in the pack. The frame ships **labelled honestly** rather than swapped for
|
||||
a flattering town — same discipline as the R21 re-measure that got worse and said so.
|
||||
|
||||
BIN
docs/shots/release_v4/classic_covenant.png
Normal file
BIN
docs/shots/release_v4/classic_covenant.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 809 KiB |
1
docs/shots/release_v4/contact.html
Normal file
1
docs/shots/release_v4/contact.html
Normal file
@ -0,0 +1 @@
|
||||
<!doctype html><meta charset="utf-8"><title>PROCITY v4.0 — the release tour</title><style>body{background:#14110c;color:#f4efe6;font:14px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;margin:24px}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(460px,1fr));gap:18px}figure{margin:0;background:#1d1810;border-radius:10px;overflow:hidden}img{width:100%;display:block}figcaption{padding:9px 13px;color:#cdc4b2}b{color:#ffd9a0}</style><h1>PROCITY v4.0 — THE REAL MAP <small>· 23 real Australian towns · seed 20261990 · settled</small></h1><div class="grid"><figure><img src="fremantle_hero.png" alt="fremantle_hero" loading="lazy"><figcaption><b>fremantle_hero</b><br>the hero — Fremantle, real streets, golden hour · Fremantle · 79 shops / 1056 edges · retail heart: 10 shops/60m · 126 draws · 77,322 tris</figcaption></figure><figure><img src="fitzroy_mecca.png" alt="fitzroy_mecca" loading="lazy"><figcaption><b>fitzroy_mecca</b><br>the mecca — Fitzroy, the densest town in the pack · Fitzroy · 139 shops / 2408 edges · retail heart: 7 shops/60m · 166 draws · 95,824 tris</figcaption></figure><figure><img src="toowoomba_thintail.png" alt="toowoomba_thintail" loading="lazy"><figcaption><b>toowoomba_thintail</b><br>the thin tail — Toowoomba: the floor does NOT hold here, and this is the honest frame (12 shops over a 1,459-edge graph = 0.008 shops/edge vs the heroes' 0.06–0.08; it reads as road, not town). Filed for v4.x · Toowoomba · 12 shops / 1459 edges · retail heart: 1 shops/60m · 78 draws · 78,496 tris</figcaption></figure><figure><img src="synthetic_control.png" alt="synthetic_control" loading="lazy"><figcaption><b>synthetic_control</b><br>the control — the procedural town, unchanged · Boolarra Heads · 493 shops / 22 edges · 180 draws · 29,158 tris</figcaption></figure><figure><img src="classic_covenant.png" alt="classic_covenant" loading="lazy"><figcaption><b>classic_covenant</b><br>?classic=1 — the v2 covenant, byte-identical forever · Boolarra Heads · 493 shops / 22 edges · 164 draws · 26,472 tris</figcaption></figure></div>
|
||||
BIN
docs/shots/release_v4/fitzroy_mecca.png
Normal file
BIN
docs/shots/release_v4/fitzroy_mecca.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 778 KiB |
BIN
docs/shots/release_v4/fremantle_hero.png
Normal file
BIN
docs/shots/release_v4/fremantle_hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 662 KiB |
BIN
docs/shots/release_v4/synthetic_control.png
Normal file
BIN
docs/shots/release_v4/synthetic_control.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 809 KiB |
BIN
docs/shots/release_v4/toowoomba_thintail.png
Normal file
BIN
docs/shots/release_v4/toowoomba_thintail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 835 KiB |
@ -1,22 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PROCITY Lane F — the v3.0 release money-shot tour (round 15 · ?gigs=1). One deterministic run.
|
||||
"""PROCITY Lane F — the v4.0 RELEASE TOUR (round 22 · the epoch close · ledger #4). One deterministic run.
|
||||
|
||||
Captures the release set into docs/shots/release_v3/ (+ a contact.html), grouped by the flags each shot
|
||||
needs, all from seed 20261990 (deterministic, reproducible — never hand-cropped):
|
||||
• venue_night — lit pub frontage + streetlamp pools + posters at night (?gigs=1)
|
||||
• district_posters — the spine at night: poster poles + lit frontages receding (?gigs=1)
|
||||
• queue_night — JOHN'S HERO: close on the pub door at 'on', queue + marquee + A's now-visible poster
|
||||
• gig_interior — the 4-piece with real instruments + a roster crowd (continuity) (?gigs=1)
|
||||
• rain_verandah — a wet night outside the lit video-shop window (?weather=rain&winmap=1)
|
||||
• tram_stop — the bus shelter the tram pauses at (?tram=1)
|
||||
The v4 thesis in five frames, into docs/shots/release_v4/ (+ contact.html). The towns ARE the product:
|
||||
|
||||
Discipline (ROUND15 ledger #5 + B's note): every shot is measured SETTLED — after the async instrument
|
||||
GLBs / poster textures / chunk stream land (shoot-twice), from a clean per-group load (no orphan-chunk
|
||||
inflation); the queue is driven LIVE (rAF is throttled in automation, so we run the animate loop + admit
|
||||
explicitly rather than trust auto-drain).
|
||||
• fremantle_hero — the golden-hour hero: a real shopfront corridor with the density crowd on it
|
||||
• fitzroy_mecca — the mecca (160 shops, 2× anything absorbed before this epoch)
|
||||
• toowoomba_thintail — the thin tail (12 shops): proof the floor still reads as a town
|
||||
• synthetic_control — the procedural town, UNCHANGED: the control the whole epoch was measured against
|
||||
• classic_covenant — `?classic=1`: the v2 covenant made visible, byte-identical forever (0x3fa36874)
|
||||
|
||||
Posing: every real-town frame resolves through Lane B's `DBG` retail-heart anchor / `clusterPose` — the
|
||||
primitive that came out of F's R21 finding (per-EDGE shop counts are ~1 on a real graph, so "densest edge"
|
||||
is the wrong heuristic; you must project the densest SPATIAL cluster onto its front edge and stand on the
|
||||
road). B's R22 bookmark fix resolves through the same anchor, so the tour and the bookmarks agree by
|
||||
construction. The HUD is hidden for every frame (F's R21 ruling corollary — the selector ships, but it
|
||||
doesn't belong in a beauty shot).
|
||||
|
||||
Run: tools/.venv/bin/python tools/qa/tour_shots.py [--seed N]
|
||||
Needs the Playwright venv + Lane B's ?dbg=1 hook (incl. queue_night). Writes full-frame 1280x720 PNGs.
|
||||
Needs the Playwright venv + Lane B's ?dbg=1 hook. Writes full-frame 1280x720 PNGs.
|
||||
"""
|
||||
import sys, time, socket, subprocess, pathlib
|
||||
|
||||
@ -24,59 +25,54 @@ ROOT = pathlib.Path(__file__).resolve().parent.parent.parent
|
||||
PORT = 8130
|
||||
HOST = f'http://127.0.0.1:{PORT}'
|
||||
SEED = 20261990
|
||||
OUTDIR = ROOT / 'docs' / 'shots' / 'release_v3_1'
|
||||
OUTDIR = ROOT / 'docs' / 'shots' / 'release_v4'
|
||||
if '--seed' in sys.argv:
|
||||
SEED = int(sys.argv[sys.argv.index('--seed') + 1])
|
||||
|
||||
# [R16 THE FLIP] shot groups — the DEFAULT boot now has all four flags on, so most shots come from ONE clean
|
||||
# load (no per-shot flag juggling). Rain is still forced (the default seed rolls clear); ?classic=1 gives the
|
||||
# before/after comparison. Deterministic, same seed.
|
||||
GROUPS = [
|
||||
{'flags': '', 'street': ['venue_night', 'district_posters', 'queue_night', 'tram_stop'], 'interior': True},
|
||||
{'flags': 'weather=rain', 'street': ['rain_verandah'], 'interior': False},
|
||||
# the town before v3 — the exact v2 boot; same main-street pose as district_posters for a clean A/B
|
||||
{'flags': 'classic=1', 'street': ['district_posters'], 'interior': False, 'suffix': '_classic'},
|
||||
# (file, query, segment, cluster-pose?, caption-prefix). seg 3 = late-afternoon gold; seg 2 = midday.
|
||||
# cluster=True → F's spatial-cluster pose (real towns); cluster=False → the bookmark (synthetic is one dense
|
||||
# cluster already, so street_noon frames it correctly).
|
||||
V4_TOUR = [
|
||||
('fremantle_hero', 'plansrc=osm&town=fremantle_real', 3, True, 'the hero — Fremantle, real streets, golden hour'),
|
||||
('fitzroy_mecca', 'plansrc=osm&town=fitzroy_real', 3, True, 'the mecca — Fitzroy, the densest town in the pack'),
|
||||
('toowoomba_thintail', 'plansrc=osm&town=toowoomba_real', 2, True, 'the thin tail — Toowoomba: the floor does NOT hold here, and this is the honest frame (12 shops over a 1,459-edge graph = 0.008 shops/edge vs the heroes\' 0.06–0.08; it reads as road, not town). Filed for v4.x'),
|
||||
('synthetic_control', f'seed={SEED}', 2, False, 'the control — the procedural town, unchanged'),
|
||||
('classic_covenant', 'classic=1', 2, False, '?classic=1 — the v2 covenant, byte-identical forever'),
|
||||
]
|
||||
|
||||
# settle + re-pose a street bookmark once its live/async content (queue, instrument GLBs, poster textures,
|
||||
# streamed chunks) has landed, then report SETTLED stats.
|
||||
STREET_JS = r"""
|
||||
async (name) => {
|
||||
# THE POSING PRIMITIVE (F's R21 finding). On a real graph, per-EDGE shop counts are ~1 — so "the edge with
|
||||
# the most shops" frames an empty arterial (it did, first run: the Fitzroy mecca came out as bare bitumen).
|
||||
# The town's retail heart is a SPATIAL cluster: find the shop lot with the most neighbours inside 60 m,
|
||||
# project it onto its own front edge, and stand ON the road backed off along it, looking down the strip.
|
||||
# Lane B's R22 bookmark fix anchors on the same retail heart — this is that primitive, applied to the tour.
|
||||
SHOT_JS = r"""
|
||||
async ({ seg, cluster }) => {
|
||||
const P = window.PROCITY, D = window.DBG;
|
||||
D.shot(name); // initial pose + segment (2 renders)
|
||||
for (let i = 0; i < 8; i++) await new Promise(r => requestAnimationFrame(r)); // animate loop: queue spawns
|
||||
const q = (P.gigs && P.venueQueues) ? P.venueQueues.get(P.gigs.venueShopIds[0]) : null;
|
||||
await new Promise(r => setTimeout(r, 1400)); // async GLB / poster / chunk settle
|
||||
const stat = D.shot(name); // RE-POSE now that everything is loaded → the settled frame
|
||||
const queue = (P.gigs && P.queueCountOf) ? P.queueCountOf(P.gigs.venueShopIds[0]) : null;
|
||||
const posters = (P.plan.posters || []).length;
|
||||
return { name, draws: stat.drawCalls, tris: stat.tris, queue, posters };
|
||||
}
|
||||
"""
|
||||
|
||||
INTERIOR_JS = r"""
|
||||
async () => {
|
||||
const P = window.PROCITY, D = window.DBG, THREE = P.THREE;
|
||||
const id = P.gigs.venueShopIds[0];
|
||||
D.setSegment(5);
|
||||
// drive the queue live + admit a couple so the interior crowd carries roster identities (continuity)
|
||||
for (let i = 0; i < 8; i++) await new Promise(r => requestAnimationFrame(r));
|
||||
const q = P.venueQueues && P.venueQueues.get(id);
|
||||
if (q) for (let i = 0; i < 3 && q.count() > 0; i++) q.admitOne();
|
||||
D.enterShop(id); await new Promise(r => setTimeout(r, 900));
|
||||
const room = P.interiorMode.current; if (!room) return { ok: false };
|
||||
const st = room.stage;
|
||||
P.camera.position.set(st.x + 0.2, 1.6, room.dims.D / 2 - 1.2);
|
||||
P.camera.lookAt(new THREE.Vector3(st.x, 1.35, st.z));
|
||||
// shoot-twice / settled discipline (ledger #5): immediate vs after the instrument GLBs land
|
||||
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
|
||||
const trisImmediate = P.renderer.info.render.triangles;
|
||||
await new Promise(r => setTimeout(r, 1600));
|
||||
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
|
||||
const info = P.interiorMode.crewInfo;
|
||||
return { ok: true, draws: P.renderer.info.render.calls, trisImmediate, trisSettled: P.renderer.info.render.triangles,
|
||||
band: info ? info.band : 0, crowd: info ? info.crowd : 0, fromRoster: info ? info.fromRoster : 0,
|
||||
venue: (P.plan.shops.find(s => s.id === id) || {}).name, bandName: P.gigs.bandNameOf(id) };
|
||||
D.setSegment(seg);
|
||||
let hub = -1;
|
||||
if (!cluster) { D.shot('street_noon'); }
|
||||
else {
|
||||
const plan = P.plan, nById = new Map(plan.streets.nodes.map(n => [n.id, n]));
|
||||
const lots = (plan.lots || []).filter(l => l.use === 'shop' || l.use === 'anchor');
|
||||
let best = null;
|
||||
for (const l of lots) { let c = 0; for (const o of lots) if (Math.hypot(o.x - l.x, o.z - l.z) < 60) c++;
|
||||
if (c > hub) { hub = c; best = l; } }
|
||||
if (best) {
|
||||
const e = (plan.streets.edges || []).find(x => x.id === best.frontEdge) || plan.streets.edges[0];
|
||||
const a = nById.get(e.a), b2 = nById.get(e.b);
|
||||
const dx = b2.x - a.x, dz = b2.z - a.z, L = Math.hypot(dx, dz) || 1, ux = dx / L, uz = dz / L;
|
||||
let t = (best.x - a.x) * ux + (best.z - a.z) * uz;
|
||||
t = Math.max(6, Math.min(L - 6, t));
|
||||
const back = Math.min(26, t);
|
||||
D.teleport(a.x + ux * (t - back), a.z + uz * (t - back), Math.atan2(ux, uz));
|
||||
} else { D.shot('street_noon'); }
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 1800)); // crowd + chunks + skins + GLBs settle
|
||||
const hud = document.getElementById('pc-hud'); if (hud) hud.style.display = 'none'; // beauty shot
|
||||
for (let i = 0; i < 3; i++) await new Promise(r => requestAnimationFrame(r));
|
||||
const i = D.info();
|
||||
return { name: P.plan.name, shops: (P.plan.shops || []).length, hub,
|
||||
edges: (P.plan.streets.edges || []).length, draws: i.drawCalls, tris: i.tris };
|
||||
}
|
||||
"""
|
||||
|
||||
@ -98,31 +94,18 @@ def ensure_server():
|
||||
proc.terminate(); sys.exit(f'could not start http.server on :{PORT}')
|
||||
|
||||
|
||||
def boot(pg, flags):
|
||||
pg.goto(f'{HOST}/index.html?seed={SEED}&dbg=1&{flags}')
|
||||
pg.wait_for_function('window.DBG && window.DBG.ready === true', timeout=25000)
|
||||
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
|
||||
try:
|
||||
pg.wait_for_function('() => window.PROCITY && (!window.PROCITY.fleet || window.PROCITY.fleet.ready)', timeout=15000)
|
||||
except Exception:
|
||||
print(' ! fleet never readied — shooting placeholders')
|
||||
|
||||
|
||||
def contact_html(shots):
|
||||
cards = []
|
||||
for s in shots:
|
||||
cap = s['caption']
|
||||
cards.append(
|
||||
f'<figure><img src="{s["file"]}" alt="{s["name"]}" loading="lazy">'
|
||||
f'<figcaption><b>{s["name"]}</b><br>{cap}</figcaption></figure>')
|
||||
cards = ''.join(
|
||||
f'<figure><img src="{s["file"]}" alt="{s["name"]}" loading="lazy">'
|
||||
f'<figcaption><b>{s["name"]}</b><br>{s["caption"]}</figcaption></figure>' for s in shots)
|
||||
return (
|
||||
'<!doctype html><meta charset="utf-8"><title>PROCITY v3.1 — release tour</title>'
|
||||
'<!doctype html><meta charset="utf-8"><title>PROCITY v4.0 — the release tour</title>'
|
||||
'<style>body{background:#14110c;color:#f4efe6;font:14px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;margin:24px}'
|
||||
'h1{font-weight:700}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(420px,1fr));gap:18px}'
|
||||
'.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(460px,1fr));gap:18px}'
|
||||
'figure{margin:0;background:#1d1810;border-radius:10px;overflow:hidden}img{width:100%;display:block}'
|
||||
'figcaption{padding:8px 12px;color:#cdc4b2}b{color:#ffd9a0}</style>'
|
||||
f'<h1>PROCITY v3.1 — release money-shot tour (THE FLIP) <small>· seed {SEED} · settled measurements</small></h1>'
|
||||
f'<div class="grid">{"".join(cards)}</div>')
|
||||
'figcaption{padding:9px 13px;color:#cdc4b2}b{color:#ffd9a0}</style>'
|
||||
f'<h1>PROCITY v4.0 — THE REAL MAP <small>· 23 real Australian towns · seed {SEED} · settled</small></h1>'
|
||||
f'<div class="grid">{cards}</div>')
|
||||
|
||||
|
||||
def main():
|
||||
@ -131,51 +114,39 @@ def main():
|
||||
except ImportError:
|
||||
sys.exit('playwright not installed — tools/.venv/bin/python -m playwright install chromium')
|
||||
|
||||
print(f"\033[1mv3.1 RELEASE MONEY-SHOT TOUR\033[0m (seed {SEED} → docs/shots/release_v3_1/)")
|
||||
print(f"\033[1mv4.0 RELEASE TOUR\033[0m ({len(V4_TOUR)} frames → docs/shots/release_v4/)")
|
||||
OUTDIR.mkdir(parents=True, exist_ok=True)
|
||||
srv = ensure_server()
|
||||
shots, errs_total = [], 0
|
||||
try:
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch()
|
||||
for grp in GROUPS:
|
||||
for name, query, seg, cluster, cap in V4_TOUR:
|
||||
pg = b.new_page(viewport={'width': 1280, 'height': 720})
|
||||
errs = []
|
||||
pg.on('pageerror', lambda e: errs.append(str(e)))
|
||||
boot(pg, grp['flags'])
|
||||
sfx = grp.get('suffix', '')
|
||||
for name in grp['street']:
|
||||
stat = pg.evaluate(STREET_JS, name)
|
||||
fname = f'{name}{sfx}.png'
|
||||
pg.screenshot(path=str(OUTDIR / fname))
|
||||
cap = (f"?classic=1 — the town before v3 (no gig layer, no pools)" if sfx
|
||||
else f"{stat['draws']} draws · {stat['tris']:,} tris")
|
||||
if not sfx and stat.get('queue') is not None: cap += f" · queue {stat['queue']}"
|
||||
if not sfx and name in ('district_posters', 'venue_night', 'queue_night'): cap += f" · {stat['posters']} posters"
|
||||
shots.append({'name': name + sfx, 'file': fname, 'caption': cap})
|
||||
print(f" \033[32m●\033[0m {name}{sfx}: {cap}")
|
||||
if grp['interior']:
|
||||
info = pg.evaluate(INTERIOR_JS)
|
||||
if info.get('ok'):
|
||||
out = OUTDIR / 'gig_interior.png'
|
||||
pg.screenshot(path=str(out))
|
||||
cap = (f"{info['venue']} · {info['bandName']} · {info['band']}-piece · crowd {info['crowd']} "
|
||||
f"({info['fromRoster']} from roster) · {info['draws']} draws · "
|
||||
f"{info['trisImmediate']:,}→{info['trisSettled']:,} tris (pre-load→settled)")
|
||||
shots.append({'name': 'gig_interior', 'file': 'gig_interior.png', 'caption': cap})
|
||||
print(f" \033[32m●\033[0m gig_interior: {cap}")
|
||||
else:
|
||||
print(" \033[31m✗\033[0m gig_interior: could not stage")
|
||||
pg.goto(f'{HOST}/index.html?{query}&dbg=1')
|
||||
pg.wait_for_function('window.PROCITY && window.PROCITY.plan && window.DBG && window.DBG.ready', timeout=25000)
|
||||
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
|
||||
try:
|
||||
pg.wait_for_function('() => !window.PROCITY.fleet || window.PROCITY.fleet.ready', timeout=15000)
|
||||
except Exception:
|
||||
pass
|
||||
r = pg.evaluate(SHOT_JS, {'seg': seg, 'cluster': cluster})
|
||||
pg.screenshot(path=str(OUTDIR / f'{name}.png'))
|
||||
caption = f"{cap} · {r['name']} · {r['shops']} shops / {r['edges']} edges" + (f" · retail heart: {r['hub']} shops/60m" if r.get('hub', -1) > 0 else '') + f" · {r['draws']} draws · {r['tris']:,} tris"
|
||||
shots.append({'name': name, 'file': f'{name}.png', 'caption': caption})
|
||||
print(f" \033[32m●\033[0m {name}: {caption}")
|
||||
errs_total += len(errs)
|
||||
if errs: print(f" ! {len(errs)} page error(s) in [{grp['flags']}]: {errs[0][:100]}")
|
||||
if errs: print(f" ! {len(errs)} page error(s): {errs[0][:90]}")
|
||||
pg.close()
|
||||
b.close()
|
||||
finally:
|
||||
if srv: srv.terminate()
|
||||
|
||||
(OUTDIR / 'contact.html').write_text(contact_html(shots))
|
||||
print(f"\n\033[1m{len(shots)} shots\033[0m → docs/shots/release_v3_1/ (+ contact.html) · {errs_total} page errors")
|
||||
sys.exit(1 if errs_total or len(shots) < 6 else 0)
|
||||
print(f"\n\033[1m{len(shots)} frames\033[0m → docs/shots/release_v4/ (+ contact.html) · {errs_total} page errors")
|
||||
sys.exit(1 if errs_total or len(shots) < len(V4_TOUR) else 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@ -262,11 +262,26 @@ export function installDBG(deps) {
|
||||
},
|
||||
|
||||
// scripted interior visit (drives the record_interior shot + the soak memory-baseline gate)
|
||||
enterShop(shopId) {
|
||||
const s = (plan.shops || []).find((x) => x.id === shopId);
|
||||
if (!s) return { error: `no shop ${shopId}` };
|
||||
// [Lane F R22 — Lane G's ask] enterShop takes a SELECTOR, not just an id, so cross-repo QA is a
|
||||
// one-liner on any town without looking ids up first:
|
||||
// DBG.enterShop(116) → by id
|
||||
// DBG.enterShop('record') → first shop of that registry type
|
||||
// DBG.enterShop('The Exchange Hotel')→ by exact name, else name substring (case-insensitive)
|
||||
// Resolution order is most-specific-first so an id-shaped string can never be shadowed by a name.
|
||||
enterShop(sel) {
|
||||
const shops = plan.shops || [];
|
||||
let s = null;
|
||||
if (typeof sel === 'number') s = shops.find((x) => x.id === sel);
|
||||
else if (typeof sel === 'string') {
|
||||
const q = sel.trim().toLowerCase();
|
||||
s = shops.find((x) => String(x.id) === q) // id-as-string
|
||||
|| shops.find((x) => (x.name || '').toLowerCase() === q) // exact name
|
||||
|| shops.find((x) => (x.type || '').toLowerCase() === q) // registry type
|
||||
|| shops.find((x) => (x.name || '').toLowerCase().includes(q)); // name substring
|
||||
}
|
||||
if (!s) return { error: `no shop matching ${JSON.stringify(sel)}` };
|
||||
enterShop(s.id, s.name);
|
||||
return { entered: s.id, name: s.name };
|
||||
return { entered: s.id, name: s.name, type: s.type };
|
||||
},
|
||||
exitShop() { window.dispatchEvent(new CustomEvent('procity:exitShop')); return { exited: true }; },
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user