THE ROUND IN ONE MEASUREMENT (12 samples x 146 active, ?clips=0 vs default — a new flag that turns off the library and nothing else): walking 99.3% -> 78.1% · bench-sit 0 -> 9.5% · lean 0 -> 7.1% · stopped in own idle 0.4% -> 5.1% · DISTINCT CLIPS ACROSS THE CROWD 4 -> 20. The town was 99.3% people walking because standing still had nowhere to happen. Wiring: new postures.js + clipbank.js. idles.glb (10/10) drives a per-citizen deterministic idle on every near-tier actor plus the seeded shopkeeper. locomotion gives 33.7% of walkers a shopping bag. sitlean (8/8, lazy) puts 4 sits on Lane B's ACTUAL benches and 4 leans on shopfront walls. browse (5/8, lazy) is a real BROWSE state at C's browse points, seeded per (shopId, slot). venue (5/6, lazy) widens the gig crowd, plus a publican pouring and a record keeper in headphones. social (0/8) is never fetched — two-person conversation needs a paired state machine, filed to R42. Cost: boot = 4 requests, 1.24 MB / 16 clips resident; the rest lazy on first need; heap delta +3.34 MB; mixer median 0.1 ms both arms. ?clips=0 / ?classic=1 / ?noassets=1 fetch ZERO clips — not even clipbank.js (dynamic import). No shell edit needed. DRAWS: +0 on every bookmark (street_noon 193, crossroads 108, night_crowd 128, market_square 94, night_neon 111, interior 110 — identical both arms). Ruling 4 respected exactly. DETERMINISM: 150 citizens, two fresh contexts, byte-equal posture signature. Controls: seed+1 differs; EVERY clip GLB delayed 2 s -> identical signature (posture is a pure function of (citySeed, id), never of residency). 6 new streams collide with none of the 12 pre-R41 keys. TWO FINDINGS THAT CHANGED THE DESIGN: the idle pool was INVISIBLE — wired only to the R17/R29 node loiter, so only 0.8% of citizens were ever stopped; and the lean never fired at all (0 in a 9 s run). Both moved to the patronage stride check. Bench stations are GATED not trusted: 14/14 derived stations coincide with real instanced geometry within 2 cm, and the control (same stations offset 2 m) matches 0/14. Filed to B: one benchStops(plan) export retires the mirror, and furniture.js puts the bench's front ALONG the street rather than facing the road, contradicting its own comment. Leak: +0 geometries, +1 texture over 6 enter/exit cycles. Goldens 157,647/157,647, 0x5f76e76. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
411 lines
23 KiB
Python
411 lines
23 KiB
Python
#!/usr/bin/env python3
|
||
"""PROCITY Lane D — R41 §41.3 acceptance shots: THE STREET STOPS BEING ONE PERSON COPY-PASTED.
|
||
|
||
tools/.venv/bin/python tools/qa/r41_shots.py [--seed N] [--outdir DIR] [--soak MS]
|
||
|
||
Two reproducible frames, both chosen by MEASUREMENT rather than by hand:
|
||
|
||
1. r41_street_postures.jpg the camera pose comes from geometry, not taste: the bench station with
|
||
the most patronage door points around it (the one block where BOTH R41 street states can fire in
|
||
a single frame), stood off on the carriageway and occlusion-raycast before it is accepted. Then
|
||
it HOLDS and waits — postures run on 6-20 s dwells, so the honest way to shoot the busy moment is
|
||
to stand still until it happens. Every candidate figure is ray-tested from the lens too: "inside
|
||
the frustum" is not "in the picture", and three earlier runs reported 3-6 rigs in shot while the
|
||
JPEG showed footpath and a gum-tree billboard.
|
||
2. r41_browse_interior.jpg the shop with the most patronage occupants that actually has browse
|
||
points, entered through the shell's own enterShop and framed from Lane C's room.spawn — the view
|
||
the game gives you walking in. The browser rigs are the peds who ducked in off the street, now
|
||
playing browse.glb instead of the shopkeeper's idle.
|
||
|
||
Every figure in each frame is measured for STATURE (feet->crown span off the posed skeleton) and
|
||
reported as the R10 no-giants line, written into a sidecar .txt beside each shot. The check is
|
||
height-RELATIVE on purpose (see human_line): fails on a giant (>2.0 m) or a fold (<55% of that
|
||
citizen's own nominal height), not on an absolute seated band that a wall lean legitimately breaks.
|
||
|
||
Fresh headless context, own no-store server (this project's documented ES-module cache burn).
|
||
"""
|
||
import sys, os, time, json, socket, subprocess, pathlib
|
||
|
||
ROOT = pathlib.Path(__file__).resolve().parent.parent.parent
|
||
PORT = int(os.environ.get('PROCITY_R41_SHOT_PORT', '8983'))
|
||
HOST = f'http://127.0.0.1:{PORT}'
|
||
SEED = 20261990
|
||
SOAK = 60000
|
||
OUTDIR = ROOT / 'docs' / 'shots' / 'laneD'
|
||
if '--seed' in sys.argv: SEED = int(sys.argv[sys.argv.index('--seed') + 1])
|
||
if '--soak' in sys.argv: SOAK = int(sys.argv[sys.argv.index('--soak') + 1])
|
||
if '--outdir' in sys.argv: OUTDIR = pathlib.Path(sys.argv[sys.argv.index('--outdir') + 1])
|
||
|
||
LO, HI = 1.4, 2.0 # R10 no-giants standing band
|
||
SEAT_LO, SEAT_HI = 0.9, 1.5 # R16 seated band
|
||
|
||
NOSTORE = r'''
|
||
import sys, http.server, functools
|
||
class H(http.server.SimpleHTTPRequestHandler):
|
||
def end_headers(self):
|
||
self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate')
|
||
super().end_headers()
|
||
def log_message(self, *a): pass
|
||
http.server.HTTPServer(('127.0.0.1', int(sys.argv[1])), functools.partial(H, directory=sys.argv[2])).serve_forever()
|
||
'''
|
||
|
||
# ── PLACE: stand on the road opposite the bench nearest the retail heart ──────────────────────────
|
||
# Deterministic and re-findable: the town anchor (DBG's densest-shop-cluster centroid) picks the
|
||
# block, the sim's OWN benchStationsFor picks the bench, and the camera stands out on the carriageway
|
||
# so the footpath reads unobstructed. Standing ON the footpath was tried first and framed a shopfront
|
||
# wall — the peds are at the back of a 3.5 m verge, so there is no room to back off beside them.
|
||
PLACE = r"""
|
||
async () => {
|
||
const P = window.PROCITY, C = P.citizens;
|
||
const THREE = await import('three');
|
||
const mod = await import('./js/citizens/sim.js');
|
||
const V3 = P.scene.position.constructor;
|
||
const ray = new THREE.Raycaster(); ray.far = 40;
|
||
// Score every bench station by how many patronage door points sit within 14 m: that is the block
|
||
// where BOTH R41 street states fire (bench sits at the station, shopfront leans off those doors),
|
||
// so it is the one place a single frame can hold both. Camera 7 m out on the carriageway at a 45°
|
||
// three-quarter, looking a little further down the footpath. Occlusion-raycast before accepting —
|
||
// an unchecked pose put the camera inside a building twice, high score and a grey wall.
|
||
const doors = [];
|
||
if (C.shopsByChunk) for (const list of C.shopsByChunk.values()) for (const d of list) doors.push(d);
|
||
const clear = (px, pz, tx, tz) => {
|
||
const d = Math.hypot(tx - px, tz - pz);
|
||
ray.set(new V3(px, 1.6, pz), new V3((tx - px) / d, 0, (tz - pz) / d));
|
||
let h = ray.intersectObject(P.scene, true).filter(x => !x.object.isSkinnedMesh && !x.object.isSprite);
|
||
if (h.length && h[0].distance < d - 0.8) return false;
|
||
for (const [ax, az] of [[1,0],[-1,0],[0,1],[0,-1]]) {
|
||
ray.set(new V3(px, 1.6, pz), new V3(ax, 0, az));
|
||
h = ray.intersectObject(P.scene, true).filter(x => !x.object.isSkinnedMesh);
|
||
if (h.length && h[0].distance < 1.3) return false;
|
||
}
|
||
return true;
|
||
};
|
||
const out = [];
|
||
for (let i = 0; i < C.edges.length; i++) {
|
||
const e = C.edges[i];
|
||
if ((e.width || 4) < 10) continue;
|
||
const L = e.len, ux = e.ux, uz = e.uz, nxE = -uz, nzE = ux;
|
||
for (const st of mod.benchStationsFor(e)) {
|
||
let nd = 0;
|
||
for (const d of doors) if (Math.hypot(d.x - st.x, d.z - st.z) < 14) nd++;
|
||
if (!nd) continue;
|
||
// which side of the centreline the bench is on → put the camera on the road, same side
|
||
const bd = Math.hypot(st.x - (e.A.x + ux * st.s), st.z - (e.A.z + uz * st.s)) || 1;
|
||
const nx = (st.x - (e.A.x + ux * st.s)) / bd, nz = (st.z - (e.A.z + uz * st.s)) / bd;
|
||
// ON THE CARRIAGEWAY at a quarter of the half-width from the centreline, 8 m back along the
|
||
// street, aimed square at the bench. This is the geometry every readable PROCITY street shot
|
||
// has: the footpath fills the mid-ground at 12-15 m, the verandah is above the heads, and
|
||
// nothing can get between the lens and the subject. Every closer variant tried (4 m, 7 m off
|
||
// the ped) put the camera under an awning or inside a shopfront window box.
|
||
const back = Math.min(bd * 0.55, 9); // out toward the road, but stay on the deck
|
||
for (const sgn of [1, -1]) {
|
||
const px = st.x - nx * back - ux * 5 * sgn, pz = st.z - nz * back - uz * 5 * sgn;
|
||
out.push({ nd, px, pz, yaw: Math.atan2(-(st.x - px), -(st.z - pz)), bx: st.x, bz: st.z });
|
||
}
|
||
}
|
||
}
|
||
out.sort((p, q) => q.nd - p.nd);
|
||
const keep = [];
|
||
for (const c of out) { if (keep.length >= 6) break; if (clear(c.px, c.pz, c.bx, c.bz)) keep.push(c); }
|
||
return { ok: keep.length > 0, cands: keep, stations: out.length / 2 | 0 };
|
||
}
|
||
"""
|
||
|
||
# ── FRAME: how good is the view from RIGHT HERE, right now — states and clips inside a tight box ───
|
||
FRAME = r"""
|
||
async () => {
|
||
const P = window.PROCITY, C = P.citizens, V3 = P.scene.position.constructor;
|
||
const THREE = await import('three');
|
||
const cam = P.camera; cam.updateMatrixWorld(true);
|
||
// Per-ped VISIBILITY raycast. "Inside the frustum" is not "in the picture": three frames running
|
||
// measured 3-6 rigs in shot while the JPEG showed footpath and a gum-tree billboard. A ped hidden
|
||
// behind a verandah post, a tree or a shopfront is not evidence of anything, so each one is
|
||
// ray-tested from the lens and only the ones that can actually be SEEN are counted.
|
||
const ray = new THREE.Raycaster(); ray.far = 30;
|
||
const visible = (c) => {
|
||
const dx = c.x - cam.position.x, dz = c.z - cam.position.z, d = Math.hypot(dx, dz);
|
||
ray.set(new V3(cam.position.x, 1.35, cam.position.z), new V3(dx / d, 0, dz / d));
|
||
const h = ray.intersectObject(P.scene, true)
|
||
.filter(x => !x.object.isSkinnedMesh && !x.object.isSprite && !x.object.isPoints);
|
||
return !(h.length && h[0].distance < d - 0.5);
|
||
};
|
||
const v = new V3(); const rows = [];
|
||
for (const c of C.activeCitizens()) {
|
||
if (!c.actor || c.actorKind !== 'rig') continue;
|
||
const d = Math.hypot(c.x - cam.position.x, c.z - cam.position.z);
|
||
if (d < 3.0 || d > 20) continue; // close enough to READ
|
||
v.set(c.x, 1.0, c.z).project(cam);
|
||
if (v.z > 1 || Math.abs(v.x) > 0.62 || Math.abs(v.y) > 0.55) continue; // comfortably in shot
|
||
if (!visible(c)) continue; // and not behind a post
|
||
const posedNow = c.loiter > 0;
|
||
rows.push({ id: c.id, d: +d.toFixed(1),
|
||
state: c.bench ? 'bench-sit' : c.lean ? 'lean' : (posedNow && c.sit) ? 'sit'
|
||
: (posedNow && c.glance) ? 'glance' : (posedNow ? 'idle' : 'walk'),
|
||
clip: c.bench ? c.posture.sit : c.lean ? c.posture.lean : (posedNow && c.glance) ? '@look'
|
||
: (posedNow ? c.posture.idle : c.posture.walk) });
|
||
}
|
||
const states = new Set(rows.map(r => r.state)), clips = new Set(rows.map(r => r.clip));
|
||
const posed = rows.filter(r => r.state === 'bench-sit' || r.state === 'lean').length;
|
||
return { n: rows.length, states: [...states], clips: clips.size, posed,
|
||
score: clips.size * 100 + rows.length * 25 + posed * 350 };
|
||
}
|
||
"""
|
||
|
||
# ── measure every figure in the frame (the human-sized line) ──────────────────────────────────────
|
||
MEASURE = r"""
|
||
() => {
|
||
const P = window.PROCITY, C = P.citizens;
|
||
const V3 = P.scene.position.constructor;
|
||
const cam = P.camera; cam.updateMatrixWorld(true);
|
||
const v = new V3(); const rows = [];
|
||
for (const c of C.activeCitizens()) {
|
||
if (!c.actor || c.actorKind !== 'rig') continue;
|
||
const d = Math.hypot(c.x - cam.position.x, c.z - cam.position.z);
|
||
if (d < 2.2 || d > 26) continue;
|
||
v.set(c.x, 1.0, c.z).project(cam);
|
||
if (v.z > 1 || Math.abs(v.x) > 0.95 || Math.abs(v.y) > 0.95) continue;
|
||
let lo = 1e9, hi = -1e9;
|
||
c.actor.inner.updateWorldMatrix(true, true);
|
||
c.actor.inner.traverse(o => { if (o.isBone) { const w = new V3(); o.getWorldPosition(w); if (w.y < lo) lo = w.y; if (w.y > hi) hi = w.y; } });
|
||
// NB `c.sit` / `c.glance` are LATCHED flags (R17/R29 set them at a node and never clear them);
|
||
// the ped is only actually posed while `c.loiter > 0`. Reading them raw mislabels a walker as
|
||
// seated — which is exactly how this harness first reported a 1.63 m "seated" figure.
|
||
const posedNow = c.loiter > 0;
|
||
rows.push({ id: c.id, d: +d.toFixed(1),
|
||
state: c.bench ? 'bench-sit' : c.lean ? 'lean' : (posedNow && c.sit) ? 'sit'
|
||
: (posedNow && c.glance) ? 'glance' : (posedNow ? 'idle' : 'walk'),
|
||
clip: c.bench ? c.posture.sit : c.lean ? c.posture.lean : (posedNow && c.glance) ? '@look'
|
||
: (posedNow ? c.posture.idle : c.posture.walk),
|
||
seated: !!c.bench || !!(posedNow && c.sit), h: +c.height.toFixed(3),
|
||
stature: +(hi - lo).toFixed(3), footY: +lo.toFixed(3) });
|
||
}
|
||
rows.sort((a, b) => a.d - b.d);
|
||
return { rows, info: window.DBG.info(), clips: C.clipStats() };
|
||
}
|
||
"""
|
||
|
||
INTERIOR = r"""
|
||
async () => {
|
||
const P = window.PROCITY, D = window.DBG, C = P.citizens;
|
||
// Shops that currently hold patronage occupants — occupancy is what puts BROWSERS on the floor (F
|
||
// stands one per browse point per occupant). Not every room type HAS browse points (a market stall
|
||
// has one, an op-shop three), so try several and keep the BEST rather than the first that works:
|
||
// taking the first gave a one-browser stall on one run and a three-browser op-shop on the next.
|
||
const cands = (P.plan.shops || [])
|
||
.map((s) => ({ id: s.id, name: s.name, type: s.type, count: C.occupancyOf(s.id).count }))
|
||
.filter((s) => s.count > 0).sort((a, b) => b.count - a.count);
|
||
if (!cands.length) return { ok: false, why: 'no shop has occupants yet' };
|
||
D.setSegment(2);
|
||
let best = null;
|
||
for (const s of cands.slice(0, 8)) {
|
||
D.enterShop(s.id);
|
||
await new Promise(r => setTimeout(r, 2600)); // room build + browser spawn + lazy browse.glb
|
||
const room = P.interiorMode.current;
|
||
const n = room ? (room.browsePoints || []).length : 0;
|
||
const browsers = ((P.interiorMode.keepers || {}).keepers || []).filter((k) => k.browse).length;
|
||
if (room && n && browsers && (!best || browsers > best.browsers)) best = { shop: s, browsePoints: n, browsers };
|
||
if (best && best.browsers >= 3) break;
|
||
D.exitShop();
|
||
await new Promise(r => setTimeout(r, 500));
|
||
}
|
||
if (!best) return { ok: false, why: 'no occupied shop had browse points with browsers', cands: cands.slice(0, 6) };
|
||
D.enterShop(best.shop.id); // re-enter the winner
|
||
await new Promise(r => setTimeout(r, 2600));
|
||
return { ok: true, ...best, tried: cands.length };
|
||
}
|
||
"""
|
||
|
||
INT_FRAME = r"""
|
||
() => {
|
||
const P = window.PROCITY, V3 = P.scene.position.constructor;
|
||
const room = P.interiorMode.current;
|
||
const km = P.interiorMode.keepers;
|
||
const figs = (km ? km.keepers : []).map(k => k.actor.fig);
|
||
if (!figs.length) return { ok: false, why: 'no keeper/browser figures' };
|
||
// Stand where the PLAYER stands walking in (Lane C's room.spawn) and look at the centroid of the
|
||
// shop's people — so the frame is the view the game actually gives you, not a staged angle. Backed
|
||
// off toward the spawn wall if that puts the camera on top of somebody.
|
||
let mx = 0, mz = 0; for (const f of figs) { mx += f.position.x; mz += f.position.z; } mx /= figs.length; mz /= figs.length;
|
||
const sp = room.spawn || { x: 0, z: 0 };
|
||
let px = sp.x, pz = sp.z;
|
||
let d = Math.hypot(mx - px, mz - pz);
|
||
if (d < 2.6) { const k = 2.6 / (d || 1); px = mx - (mx - px) * k; pz = mz - (mz - pz) * k; d = 2.6; }
|
||
P.camera.position.set(px, 1.62, pz);
|
||
P.camera.lookAt(new V3(mx, 1.15, mz));
|
||
P.camera.updateMatrixWorld(true);
|
||
P.renderer.info.reset();
|
||
P.renderer.render(P.interiorMode.scene, P.camera);
|
||
// who is actually inside the frame
|
||
const v = new V3(); let seen = 0;
|
||
for (const f of figs) { v.set(f.position.x, 1.0, f.position.z).project(P.camera);
|
||
if (v.z <= 1 && Math.abs(v.x) < 0.9 && Math.abs(v.y) < 0.9) seen++; }
|
||
return { ok: true, cam: [+px.toFixed(2), +pz.toFixed(2)], target: [+mx.toFixed(2), +mz.toFixed(2)],
|
||
figures: figs.length, inFrame: seen,
|
||
draws: P.renderer.info.render.calls, tris: P.renderer.info.render.triangles };
|
||
}
|
||
"""
|
||
|
||
INT_MEASURE = r"""
|
||
() => {
|
||
const P = window.PROCITY, V3 = P.scene.position.constructor;
|
||
const km = P.interiorMode.keepers;
|
||
const rows = [];
|
||
for (const k of (km ? km.keepers : [])) {
|
||
const a = k.actor; if (!a.inner) { rows.push({ kind: k.browse ? 'browser' : 'keeper', placeholder: true }); continue; }
|
||
let lo = 1e9, hi = -1e9;
|
||
a.inner.updateWorldMatrix(true, true);
|
||
a.inner.traverse(o => { if (o.isBone) { const w = new V3(); o.getWorldPosition(w); if (w.y < lo) lo = w.y; if (w.y > hi) hi = w.y; } });
|
||
rows.push({ kind: k.browse ? 'browser' : 'keeper', shopId: k.shopId, type: k.type, h: +(a.height || 0).toFixed(3),
|
||
clip: (a.mixer && a.mixer._actions || []).filter(x => x.getEffectiveWeight() > 0.5).map(x => x.getClip().name)[0] || null,
|
||
pending: k.want || null,
|
||
stature: +(hi - lo).toFixed(3), footY: +lo.toFixed(3),
|
||
pos: [+a.fig.position.x.toFixed(2), +a.fig.position.z.toFixed(2)] });
|
||
}
|
||
return { rows, info: window.DBG.info() };
|
||
}
|
||
"""
|
||
|
||
|
||
def port_up(port):
|
||
with socket.socket() as s:
|
||
s.settimeout(0.4); return s.connect_ex(('127.0.0.1', port)) == 0
|
||
|
||
|
||
def serve():
|
||
p = subprocess.Popen([sys.executable, '-c', NOSTORE, str(PORT), str(ROOT / 'web')],
|
||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||
for _ in range(80):
|
||
if port_up(PORT): return p
|
||
time.sleep(0.1)
|
||
p.terminate(); raise SystemExit(f'could not serve on :{PORT}')
|
||
|
||
|
||
def human_line(rows):
|
||
"""The R10 no-giants line — measured, and measured against the law it is actually about.
|
||
|
||
An absolute seated band was tried first and it was the WRONG instrument for R41: the library's
|
||
four sits run from bolt-upright-in-a-chair to slumped, and its four wall leans take 15-25% off a
|
||
standing crown by construction (you are leaning BACK). A 1.32 m leaner is not a defect, and a
|
||
fixed [0.9,1.5] seated band called it one. What the R10 law forbids is a GIANT, and what R16
|
||
forbids is a FOLD (Hips.quaternion laying the body flat, head down at hip height). So: stature is
|
||
reported for every figure with its ratio to that citizen's own nominal height, and the gate fails
|
||
on stature > 2.0 m (giant), stature < 0.55 x height (folded), or a nominal height outside the
|
||
seeded [1.4, 2.0] range."""
|
||
out, bad = [], []
|
||
for r in rows:
|
||
if r.get('placeholder'):
|
||
continue
|
||
h = r.get('h')
|
||
ratio = (r['stature'] / h) if h else None
|
||
if r['stature'] > HI: bad.append(f"{r.get('id', r.get('kind'))} GIANT {r['stature']}m")
|
||
elif h and r['stature'] < 0.55 * h: bad.append(f"{r.get('id', r.get('kind'))} FOLDED {r['stature']}m of {h}m")
|
||
elif h and not (LO <= h <= HI): bad.append(f"{r.get('id', r.get('kind'))} nominal height {h}m out of [{LO},{HI}]")
|
||
out.append(f"{r.get('state', r.get('kind'))} {r['stature']}m"
|
||
+ (f" ({ratio:.0%} of its {h}m nominal)" if ratio else ""))
|
||
verdict = 'ALL HUMAN-SIZED — no giant (>2.0 m), no fold (<55% of nominal)' if not bad else 'FAIL: ' + ', '.join(bad)
|
||
return (f"HUMAN-SIZED LINE — {len(out)} figures, stature (feet\u2192crown) measured off the posed "
|
||
f"skeleton: {' \u00b7 '.join(out)}. {verdict}."), not bad
|
||
|
||
|
||
def main():
|
||
OUTDIR.mkdir(parents=True, exist_ok=True)
|
||
from playwright.sync_api import sync_playwright
|
||
srv = serve()
|
||
rc = 0
|
||
try:
|
||
with sync_playwright() as p:
|
||
b = p.chromium.launch()
|
||
pg = b.new_page(viewport={'width': 1280, 'height': 720})
|
||
errs = []
|
||
pg.on('console', lambda m: errs.append(m.text) if m.type == 'error' else None)
|
||
pg.on('pageerror', lambda e: errs.append(str(e)))
|
||
pg.goto(f'{HOST}/index.html?seed={SEED}&dbg=1')
|
||
pg.wait_for_function('window.DBG && window.DBG.ready === true', timeout=40000)
|
||
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
|
||
pg.evaluate("() => { window.DBG.setSegment(2); window.DBG.shot('crossroads_busy'); }")
|
||
print(f'soaking {SOAK} ms so the crowd reaches its posture steady state…')
|
||
pg.wait_for_timeout(SOAK)
|
||
|
||
# ── shot 1: the street ────────────────────────────────────────────────────────────────
|
||
# The camera pose is chosen ONCE, from geometry (the bench station with the most shop
|
||
# doors around it — the one block where both R41 street states can fire in one frame) and
|
||
# occlusion-raycast before it is accepted. Then it HOLDS and waits: postures come and go
|
||
# on 6–20 s dwells, so the honest way to shoot the busy moment is to stand still until it
|
||
# happens. Chasing subjects with a moving camera was tried first and lost them to
|
||
# rig-reacquisition every time.
|
||
place = pg.evaluate(PLACE)
|
||
print(f"place: {place.get('stations', 0)} bench stations near shop doors; "
|
||
f"{len(place.get('cands', []))} clear camera poses")
|
||
best, m = None, None
|
||
for cand in place.get('cands', [])[:4]:
|
||
for _ in range(2):
|
||
pg.evaluate("([x,z,y]) => window.DBG.teleport(x,z,y)", [cand['px'], cand['pz'], cand['yaw']])
|
||
pg.wait_for_timeout(2500)
|
||
for _ in range(14): # ~35 s of holding this pose
|
||
fr = pg.evaluate(FRAME)
|
||
if fr['n'] and (not best or fr['score'] > best['score']):
|
||
mm = pg.evaluate(MEASURE)
|
||
pg.screenshot(path=str(OUTDIR / 'r41_street_postures.jpg'), type='jpeg', quality=90)
|
||
best, m = dict(fr, **{k: cand[k] for k in ('px', 'pz', 'yaw')}), mm
|
||
print(f" ({cand['px']:.0f},{cand['pz']:.0f}) score {fr['score']} n={fr['n']} "
|
||
f"clips={fr['clips']} posed={fr['posed']} states={fr['states']}")
|
||
if best and best['posed'] >= 2 and best['clips'] >= 4: break
|
||
pg.wait_for_timeout(2500)
|
||
pg.evaluate("([x,z,y]) => window.DBG.teleport(x,z,y)", [cand['px'], cand['pz'], cand['yaw']])
|
||
if best and best['posed'] >= 2 and best['clips'] >= 4: break
|
||
if not best:
|
||
print(' never framed anybody'); rc = 1
|
||
else:
|
||
line, ok = human_line(m['rows'])
|
||
name = 'r41_street_postures.jpg'
|
||
states = {}
|
||
for r in m['rows']: states[r['state']] = states.get(r['state'], 0) + 1
|
||
cap = (f"PROCITY R41 §41.3 — {name}\n"
|
||
f"seed {SEED} · synthetic · MIDDAY · camera ({best['px']:.1f}, {best['pz']:.1f}) yaw {best['yaw']:.3f}\n"
|
||
f"{len(m['rows'])} near-tier rigs in frame · states {states} · "
|
||
f"{len(set(r['clip'] for r in m['rows']))} distinct clips: "
|
||
f"{', '.join(sorted(set(r['clip'] for r in m['rows'])))}\n"
|
||
f"draws {m['info']['drawCalls']} / budget {m['info']['budget']['draws']} · tris {m['info']['tris']}\n"
|
||
f"clips resident: {m['clips']['groups']} groups / {m['clips']['clips']} clips / {m['clips']['bytes']} B\n"
|
||
f"{line}\n")
|
||
(OUTDIR / name.replace('.jpg', '.txt')).write_text(cap)
|
||
print(cap)
|
||
if not ok: rc = 1
|
||
|
||
# ── shot 2: a browsing interior ───────────────────────────────────────────────────────
|
||
info = pg.evaluate(INTERIOR)
|
||
print('interior:', json.dumps(info))
|
||
if not info.get('ok'):
|
||
print(' skipped interior shot'); rc = 1
|
||
else:
|
||
fr = pg.evaluate(INT_FRAME)
|
||
print('frame:', json.dumps(fr))
|
||
if fr.get('ok'):
|
||
im = pg.evaluate(INT_MEASURE)
|
||
browsers = [r for r in im['rows'] if r['kind'] == 'browser']
|
||
line, ok = human_line(im['rows'])
|
||
name = 'r41_browse_interior.jpg'
|
||
pg.screenshot(path=str(OUTDIR / name), type='jpeg', quality=90)
|
||
cap = (f"PROCITY R41 §41.3 — {name}\n"
|
||
f"seed {SEED} · {info['shop']['name']} ({info['shop']['type']}) · "
|
||
f"{info['shop']['count']} patronage occupants · {info['browsePoints']} browse points\n"
|
||
f"figures: {json.dumps(im['rows'])}\n"
|
||
f"draws {fr['draws']} / interior budget 350 · tris {fr['tris']}\n"
|
||
f"{line}\n")
|
||
(OUTDIR / name.replace('.jpg', '.txt')).write_text(cap)
|
||
print(cap)
|
||
if not ok or not browsers: rc = 1
|
||
else:
|
||
rc = 1
|
||
if errs:
|
||
print('CONSOLE ERRORS:', errs[:6]); rc = 1
|
||
b.close()
|
||
finally:
|
||
srv.terminate()
|
||
print('\033[32m● shots written\033[0m' if rc == 0 else '\033[31m● problems — see above\033[0m')
|
||
return rc
|
||
|
||
|
||
if __name__ == '__main__':
|
||
sys.exit(main())
|