Lane F R15 (v3.0): the release money-shot tour (ledger #4) + settled measurements (ledger #5)

tools/qa/tour_shots.py: one deterministic run capturing the release set into docs/shots/release_v3/
(+ contact.html), grouped by the flags each shot needs, all seed 20261990:
- venue_night, district_posters, queue_night (John's HERO — now with A's flipped frontage poster VISIBLE
  by the door), gig_interior (the 4-piece + real instruments + a roster crowd — 2 punters carried through),
  rain_verandah, tram_stop
- settled discipline (ledger #5): every shot measured after the async instrument GLBs / poster textures /
  chunk stream land (shoot-twice), from a clean per-group load; the queue is driven LIVE (explicit admits,
  not rAF auto-drain which automation throttles)

The hero confirms ledger #1: the frontage poster reads from the street for the first time. Interior settles
35k->182k tris (draws 56 <= 350 gate). All street shots <=300 draws / <=200k tris. 0 page errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-16 11:35:27 +10:00
parent edc0c479c1
commit 6a35b29352
8 changed files with 177 additions and 0 deletions

View File

@ -0,0 +1 @@
<!doctype html><meta charset="utf-8"><title>PROCITY v3.0 — 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}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><h1>PROCITY v3.0 — release money-shot tour <small>· seed 20261990 · settled measurements</small></h1><div class="grid"><figure><img src="venue_night.png" alt="venue_night" loading="lazy"><figcaption><b>venue_night</b><br>121 draws · 51,899 tris · queue 2 · 14 posters</figcaption></figure><figure><img src="district_posters.png" alt="district_posters" loading="lazy"><figcaption><b>district_posters</b><br>186 draws · 134,068 tris · queue 2 · 14 posters</figcaption></figure><figure><img src="queue_night.png" alt="queue_night" loading="lazy"><figcaption><b>queue_night</b><br>121 draws · 61,738 tris · queue 2 · 14 posters</figcaption></figure><figure><img src="gig_interior.png" alt="gig_interior" loading="lazy"><figcaption><b>gig_interior</b><br>The Exchange Hotel · Screaming Utes · 4-piece · crowd 8 (2 from roster) · 56 draws · 35,216→182,657 tris (pre-load→settled)</figcaption></figure><figure><img src="rain_verandah.png" alt="rain_verandah" loading="lazy"><figcaption><b>rain_verandah</b><br>116 draws · 51,643 tris</figcaption></figure><figure><img src="tram_stop.png" alt="tram_stop" loading="lazy"><figcaption><b>tram_stop</b><br>105 draws · 172,187 tris</figcaption></figure></div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 946 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

176
tools/qa/tour_shots.py Normal file
View File

@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""PROCITY Lane F — the v3.0 release money-shot tour (round 15 · ?gigs=1). 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)
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).
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.
"""
import sys, time, socket, subprocess, pathlib
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'
if '--seed' in sys.argv:
SEED = int(sys.argv[sys.argv.index('--seed') + 1])
# shot groups — each is one clean page load with the flags that shot needs (deterministic, same seed).
GROUPS = [
{'flags': 'gigs=1', 'street': ['venue_night', 'district_posters', 'queue_night'], 'interior': True},
{'flags': 'weather=rain&tram=1&winmap=1', 'street': ['rain_verandah', 'tram_stop'], 'interior': False},
]
# 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) => {
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) };
}
"""
def port_up(port):
with socket.socket() as s:
s.settimeout(0.4)
return s.connect_ex(('127.0.0.1', port)) == 0
def ensure_server():
if port_up(PORT):
return None
proc = subprocess.Popen(['python3', '-m', 'http.server', str(PORT)], cwd=str(ROOT / 'web'),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
for _ in range(50):
if port_up(PORT): return proc
time.sleep(0.1)
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>')
return (
'<!doctype html><meta charset="utf-8"><title>PROCITY v3.0 — 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}'
'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.0 — release money-shot tour <small>· seed {SEED} · settled measurements</small></h1>'
f'<div class="grid">{"".join(cards)}</div>')
def main():
try:
from playwright.sync_api import sync_playwright
except ImportError:
sys.exit('playwright not installed — tools/.venv/bin/python -m playwright install chromium')
print(f"\033[1mv3.0 RELEASE MONEY-SHOT TOUR\033[0m (seed {SEED} → docs/shots/release_v3/)")
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:
pg = b.new_page(viewport={'width': 1280, 'height': 720})
errs = []
pg.on('pageerror', lambda e: errs.append(str(e)))
boot(pg, grp['flags'])
for name in grp['street']:
stat = pg.evaluate(STREET_JS, name)
out = OUTDIR / f'{name}.png'
pg.screenshot(path=str(out))
cap = f"{stat['draws']} draws · {stat['tris']:,} tris"
if stat.get('queue') is not None: cap += f" · queue {stat['queue']}"
if name in ('district_posters', 'venue_night', 'queue_night'): cap += f" · {stat['posters']} posters"
shots.append({'name': name, 'file': f'{name}.png', 'caption': cap})
print(f" \033[32m●\033[0m {name}: {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")
errs_total += len(errs)
if errs: print(f" ! {len(errs)} page error(s) in [{grp['flags']}]: {errs[0][:100]}")
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/ (+ contact.html) · {errs_total} page errors")
sys.exit(1 if errs_total or len(shots) < 6 else 0)
if __name__ == '__main__':
main()