diff --git a/docs/shots/release_v3/contact.html b/docs/shots/release_v3/contact.html new file mode 100644 index 0000000..f1ac630 --- /dev/null +++ b/docs/shots/release_v3/contact.html @@ -0,0 +1 @@ +PROCITY v3.0 — release tour

PROCITY v3.0 — release money-shot tour · seed 20261990 · settled measurements

venue_night
venue_night
121 draws · 51,899 tris · queue 2 · 14 posters
district_posters
district_posters
186 draws · 134,068 tris · queue 2 · 14 posters
queue_night
queue_night
121 draws · 61,738 tris · queue 2 · 14 posters
gig_interior
gig_interior
The Exchange Hotel · Screaming Utes · 4-piece · crowd 8 (2 from roster) · 56 draws · 35,216→182,657 tris (pre-load→settled)
rain_verandah
rain_verandah
116 draws · 51,643 tris
tram_stop
tram_stop
105 draws · 172,187 tris
\ No newline at end of file diff --git a/docs/shots/release_v3/district_posters.png b/docs/shots/release_v3/district_posters.png new file mode 100644 index 0000000..26137d6 Binary files /dev/null and b/docs/shots/release_v3/district_posters.png differ diff --git a/docs/shots/release_v3/gig_interior.png b/docs/shots/release_v3/gig_interior.png new file mode 100644 index 0000000..48bb271 Binary files /dev/null and b/docs/shots/release_v3/gig_interior.png differ diff --git a/docs/shots/release_v3/queue_night.png b/docs/shots/release_v3/queue_night.png new file mode 100644 index 0000000..fea0ed5 Binary files /dev/null and b/docs/shots/release_v3/queue_night.png differ diff --git a/docs/shots/release_v3/rain_verandah.png b/docs/shots/release_v3/rain_verandah.png new file mode 100644 index 0000000..7828775 Binary files /dev/null and b/docs/shots/release_v3/rain_verandah.png differ diff --git a/docs/shots/release_v3/tram_stop.png b/docs/shots/release_v3/tram_stop.png new file mode 100644 index 0000000..078ba6d Binary files /dev/null and b/docs/shots/release_v3/tram_stop.png differ diff --git a/docs/shots/release_v3/venue_night.png b/docs/shots/release_v3/venue_night.png new file mode 100644 index 0000000..f88e40f Binary files /dev/null and b/docs/shots/release_v3/venue_night.png differ diff --git a/tools/qa/tour_shots.py b/tools/qa/tour_shots.py new file mode 100644 index 0000000..2b8b8d1 --- /dev/null +++ b/tools/qa/tour_shots.py @@ -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'
{s[' + f'
{s["name"]}
{cap}
') + return ( + 'PROCITY v3.0 — release tour' + '' + f'

PROCITY v3.0 — release money-shot tour · seed {SEED} · settled measurements

' + f'
{"".join(cards)}
') + + +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()