PROCITY/tools/qa/r42_tour.py
m3ultra 293a82c5e8 R42 THE LOOK: five lane-runs — the pub is dressed, the decks read, and the town is cast
John looked at R41's proof shots and named three faults. All three are fixed, at zero draw
cost, and every lane corrected somebody's numbers on the way — including its own.

E-LOOK (§42.1+§42.2a) — THE PREMISE WAS WRONG. "The pub props have no texture" (my brief,
quoting E's own R41 note) is false: all 37 carry COLOR_0 and three.js renders it. The colour
was BLEACHED — jukebox at saturation 0.02, 15 assets under 0.05; what the source library
called colour was baked AO. So the fix was a palette imposed on colour already present, in
place: 46 assets, draw delta +0 and tri delta +0 on every one, proved the honest way — the
concatenated POSITION/NORMAL/TEXCOORD/index bytes are sha1-identical to R41, so footprints
and origins provably did not move. (A first-pass bbox check ignored node rotations and
"found" 3 phantom violations; thrown out for the byte comparison.) The banquette was 1990s
office teal and went burgundy; the beer umbrella was white and went brewery green.
THE DECK, diagnosed before being touched: 7 greys, chassis 0.05 luminance on a 0.033 bench =
1.21:1, and the one high-contrast feature was INSIDE-OUT (white ring around a dark disc; a
turntable is a dark record on a light platter). Now Technics silver at 6.74:1, black vinyl,
red start/stop. Node contract re-read from the shipped GLB: 4/4 + 1/1 PASS. Filed to R43,
not fixed: deck_1200_rigged has NO PLINTH (bottom 43mm of 119mm lost to R41 join damage).

E-CAST (§42.3) — 19 of 24 ship. Rejected: comical_luchador_01 (Mixamo Ch43, purple mask
reading EL CHUPACABRA, identified from the file's own material name), a 3-heads-tall cartoon
child, a robed wuxia elder, a WW2 infantryman — and dj_phrtt_01, AN UNCLOTHED BODY whose only
texture is a bare-skin atlas, which was in the DEFAULT crowd and live on the depot. It is the
R41 deny-list's failure class through a different door: an innocent name from another
pipeline. Only rendering all 24 and LOOKING found it.
Ruling 2 (prefer CC0) was REFUSED ON MEASUREMENT: Quaternius is 25 nodes / 23 joints and ZERO
of 64 canonical tracks bind — it would have loaded silently and never animated. Replacements
came from Mixamo's Ch## pool, filtered to the contract-clean 65-bone family (candidates ran
65..99 bones; named characters carry facial/hair bones).
THE LICENCE GAP WAS UNREAD, NOT MISSING: every source FBX carries ApplicationVendor "Mixamo,
Inc." internally. Adobe royalty-free, no attribution owed. B's amber rows are now real.

B (§42.5) — MY ASSIGNED FIX WAS USELESS AND B PROVED IT. document.fonts.ready shipped, then
measured: the atlas canvases are already byte-identical across boots, the game ships NO
webfonts (document.fonts.size === 0), and ready fires 50ms BEFORE the first fillText. The
real cause of unreproducible bookmarks is the wall-clock CROWD — 983 changed px of 921,600,
ALL in the pedestrian band; 0 px with the crowd out. New DBG.freeze() takes stable bookmarks
2/5 -> 5/5. Also found: ?pop=0 boots 140 citizens (0 is falsy against the default).
THE BENCH: D's filing correct on both counts. Front-vs-road 90.0deg -> 0.0deg, min=med=max
over 60/60 instances; the streetlight arm now reaches 1.05m over the road where it reached
0.00. Both used a base yaw that pointed +Z down the street while their own comments claimed
otherwise — the side term was already right, which is why it survived 41 rounds of reading.
Also: on any box with CDN access 9 of 60 benches are the depot GLB, 90deg out from the house
law — normalised at the geometry, once, at load. benchStops(plan) exported as one truth.

C (§42.2b) — 122 vs 123 WAS THE CAMERA ASPECT, and nobody was wrong. Draw counts are
post-frustum-cull and the sweep camera inherits the window aspect: 1.25->116, 1.60->122 (C's
harness), 1.778->123 (F's), 2.37->124. Pinned MEASURE_ASPECT=16/9 with a 1.25 control.
E's "keep the bench dark" advice was measured and declined with reason (deck-vs-bench was
already 4.73:1, over threshold). "Reads as a black box" is a claim about pixels, so C measured
pixels: near-black in the booth crop 57.5% -> 26.7%, distinct colours 228 -> 389. Bench
rebuilt 3->7 boxes with an open record bay; headphones on the mixer; faceDoor aims booths at
the player's entry — alignment within 45deg went 49% -> 92%. Two bugs found by looking: a
fitting could be planted INSIDE A WALL (0.77m of a 0.86m half-depth in plaster), and every
booth part sat 25mm sunk into its own worktop.

D (§42.4) — five swaps IN PLACE at the same index (pickRig indexes these arrays and pedIndex
is in the identity signature and the impostor atlas; removal would renumber the town).
Identity signature pre- vs post-swap: 0 differing lines of 150 — who each citizen is did not
move, only which body they wear. dj_phrtt_01 verified gone AT THE WIRE, not from source: a
fresh default boot fetches 32 ped GLBs, none retired.
THE CONTROL ARM FOUND A 41-ROUND-OLD BUG: the ped being RETIRED, comical_boy_01, binds only
3,132 of 3,460 tracks — six thumb bones lack the mixamorig prefix. It had been silently
dropping six tracks per clip since it shipped. Testing the thing being thrown away found it.
Imported B's benchStops mid-round: sitters now 0.0deg to the road, signatures byte-identical
across the rewire. Corrected F's filing: the browse smoke was flaky-by-construction (a 16-25s
race against a live sim), not deterministically red.

F (§42.6) — qa.sh --strict 24 passed / 0 failed / 0 warn / 0 skipped. New Gate 11: period law
(0/5 banned; CONTROL: R41's roster trips all five) and roster licence (24/24, 0 unverified;
CONTROL: an amber row turns 20 red). r41_shots.py promoted warn -> strict.
R41'S BYTE-HASH BAN IS REVERSED AND ITS STATED REASON WITHDRAWN AS WRONG. A gate may pin a
hash under five in-run assertions (classic+flags boot, freeze().stillLive all-false, a second
freeze removing 0 draws, settle until DBG.info() stops changing, hash with the dbg panel
hidden): 5/5 bookmarks now pin to one sha256 over 3 fresh boots. Two findings got it there —
the chunk streamer (a fixed settle is a bet on load) and the fps readout, which with
everything else frozen was the last thing moving at 53-62 px.
?pop=0 fixed (Number.isFinite) — a no-op for 35 rounds. Three red flags_check gates fixed.

Street: noon 282 · NIGHT 291/300 — the nine-draw margin INTACT, every R41 row reproducing to
the digit. Interior at pinned 16:9: 123 @ dept/auto, margin 227. selfcheck 157,647/157,647,
0x5f76e76 unmoved. No tag — John's playtest rules the epoch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 23:49:49 +10:00

839 lines
51 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""PROCITY Lane F — R42 §42.6 THE PROOF SHOTS. This round is judged by eye, so these ARE the round.
tools/.venv/bin/python tools/qa/r42_tour.py [--only NAME[,NAME…]] [--outdir DIR]
R42 is a LOOK round: three faults were visible in R41's own committed proof shots, and the deliverable
is the same frames taken again on the integrated tree, **paired against those committed originals**.
The R41 files in `docs/shots/laneF_r41/` are the BEFORE — they are in git, nobody can re-shoot them,
and that is exactly what makes them evidence.
pub_furnished R41's hero. The before holds TWO purple `EL CHUPACABRA` luchadors and an
UNCLOTHED body at the stage edge, in a room of white plastic-looking props.
Same recipe, same seed, same room, same segment. → pub_furnished_ab.jpg
record_dj_booth R41's booth: "a plain black box". Same recipe. → record_dj_booth_ab.jpg
record_dj_booth_door …and the frame R41 never took: the booth from where a player actually stands,
at Lane C's own `room.spawn` — the door. A composition claim is a claim about
the entry view, so it has to be shot from the entry.
street_postures R41's street, from R41's OWN published camera — parsed out of its committed
sidecar, because re-running R41's live-crowd cluster search lands somewhere
different every time. → street_postures_ab.jpg
street_bench the bench claim on its own, shot SIDE-ON (a person facing the camera looks the
same whichever way the bench under them is turned), with the yaw MEASURED in
the frame from the plan's road geometry and the scene's instance matrices —
not read back off B's placement rule.
credits_panel the F2 surface with the ped rows filled: ruling 3 on screen, no `unverified`.
TWO LINES ON EVERY HUMANOID FRAME
· the R10 human-sized line (stature feet→crown off the POSED skeleton, as a ratio of that figure's
own nominal height — a giant >2.0 m or a fold <55% fails).
· the CAST line (Lane D's §42.4 idea, adopted): every figure resolved back to the ped GLB it is
wearing via `fleet.all[pedIndex].pedName`, with an explicit assertion that none of the five
retired bodies is in the picture. "The luchador is gone" is a claim about the cast, and a JPEG
cannot carry it alone.
Fresh headless context per frame, own no-store server, seed 20261990, 1280x720.
Exit 0 = every frame captured, every human-sized line clean, and no retired body in any cast.
"""
import sys, os, re, json, time, socket, subprocess, pathlib
ROOT = pathlib.Path(__file__).resolve().parent.parent.parent
PORT = int(os.environ.get('PROCITY_R42_TOUR_PORT', '8984'))
HOST = f'http://127.0.0.1:{PORT}'
SEED = 20261990
OUTDIR = ROOT / 'docs' / 'shots' / 'laneF_r42'
BEFORE = ROOT / 'docs' / 'shots' / 'laneF_r41' # the committed R41 originals — the BEFORE arm
LO, HI = 1.4, 2.0
if '--outdir' in sys.argv: OUTDIR = pathlib.Path(sys.argv[sys.argv.index('--outdir') + 1])
ONLY = None
if '--only' in sys.argv: ONLY = set(sys.argv[sys.argv.index('--only') + 1].split(','))
# Lane E §42.3 / Lane D §42.4 — the five bodies that must never be in a shipped frame again.
RETIRED = ['comical_luchador_01', 'comical_boy_01', 'man_elder_01', 'man_soldier_ww2_01', 'dj_phrtt_01']
fails = []
def FAIL(m): fails.append(m); print(f" \033[31m✗ FAIL\033[0m {m}")
def OK(m): print(f" \033[32m✓\033[0m {m}")
def head(m): print(f"\n\033[1m{m}\033[0m")
def note(m): print(f" \033[33m·\033[0m {m}")
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()
'''
# ── the street: R41's own cluster search, plus the CAST term ──────────────────────────────────────
# Unchanged in method from r41_tour.py on purpose — a before/after pair whose AFTER was framed by a
# different algorithm is not a pair. The one addition is `ped`: the frame this round is about WHO is
# in it, so the scorer also rewards distinct BODIES, and the sidecar can name them.
STREET_LOOK = r"""
() => {
const P = window.PROCITY, C = P.citizens, V3 = P.scene.position.constructor;
const cam = P.camera;
const names = (P.fleet.all || []).map(r => r.pedName);
const people = [];
for (const c of C.activeCitizens()) {
if (!c.actor || c.actorKind !== 'rig') continue;
const posedNow = c.loiter > 0;
people.push({ id: c.id, x: c.x, z: c.z, ped: names[c.pedIndex] || '?',
state: c.bench ? 'bench-sit' : c.lean ? 'lean' : (posedNow && c.sit) ? 'sit'
: (posedNow && c.glance) ? 'glance' : (posedNow ? 'idle' : 'walk') });
}
const anchors = people.filter(p => p.state !== 'walk');
if (!anchors.length) return { ok: false, why: 'no non-walking rig on the street yet', n: people.length };
const targets = anchors.map(a => ({ x: a.x, z: a.z, state: a.state }));
for (let i = 0; i < anchors.length; i++)
for (let j = i + 1; j < anchors.length; j++)
if (anchors[i].state !== anchors[j].state && Math.hypot(anchors[i].x - anchors[j].x, anchors[i].z - anchors[j].z) < 34)
targets.push({ x: (anchors[i].x + anchors[j].x) / 2, z: (anchors[i].z + anchors[j].z) / 2,
state: anchors[i].state + '+' + anchors[j].state });
let best = null;
for (const a of targets) {
for (let i = 0; i < 12; i++) {
const th = i * Math.PI / 6;
for (const r of [7, 9.5, 12, 15, 18]) {
const px = a.x + Math.cos(th) * r, pz = a.z + Math.sin(th) * r;
cam.position.set(px, 1.62, pz);
cam.lookAt(new V3(a.x, 1.25, a.z));
cam.updateMatrixWorld(true);
const v = new V3(); const seen = [];
for (const p of people) {
const d = Math.hypot(p.x - px, p.z - pz);
if (d < 2.4 || d > 26) continue;
v.set(p.x, 1.0, p.z).project(cam);
if (v.z > 1 || Math.abs(v.x) > 0.9 || Math.abs(v.y) > 0.9) continue;
seen.push(p);
}
const states = new Set(seen.map(s => s.state));
const bodies = new Set(seen.map(s => s.ped));
const score = states.size * 100 + bodies.size * 40 + seen.length * 10 - r * 4;
if (!best || score > best.score) best = { score, px, pz, ax: a.x, az: a.z,
states: [...states], bodies: [...bodies], inFrame: seen.length, anchor: a.state };
}
}
}
cam.position.set(best.px, 1.62, best.pz);
cam.lookAt(new V3(best.ax, 1.25, best.az));
cam.updateMatrixWorld(true);
// MEASURE IN THE SAME BEAT — searching in one evaluate and measuring in the next lets the dwell
// clock run between them, and a bench somebody has just got up from is the frame you shoot.
const v2 = 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;
v2.set(c.x, 1.0, c.z).project(cam);
if (v2.z > 1 || Math.abs(v2.x) > 0.92 || Math.abs(v2.y) > 0.92) 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; } });
const posedNow = c.loiter > 0;
// the clip is read off the MIXER, not off `c.posture` — posture is the ASSIGNMENT and exists on a
// ?clips=0 boot that has fetched nothing (R41's lesson, kept).
const playing = ((c.actor.mixer && c.actor.mixer._actions) || [])
.filter(x => x.getEffectiveWeight() > 0.5).map(x => x.getClip().name);
rows.push({ id: c.id, d: +d.toFixed(1), ped: names[c.pedIndex] || '?',
state: c.bench ? 'bench-sit' : c.lean ? 'lean' : (posedNow && c.sit) ? 'sit'
: (posedNow && c.glance) ? 'glance' : (posedNow ? 'idle' : 'walk'),
clip: playing[0] || null, h: +c.height.toFixed(3), stature: +(hi - lo).toFixed(3) });
}
rows.sort((a, b) => a.d - b.d);
const states = [...new Set(rows.map(r => r.state))].sort();
const clips = [...new Set(rows.map(r => r.clip).filter(Boolean))].sort();
const bodies = [...new Set(rows.map(r => r.ped))].sort();
// …and a BENCH-SIT BONUS, stated rather than hidden: R41's committed `street_postures.jpg` is a
// bench-sit frame, and a pair whose halves show different states is not a pair. The bonus biases
// the AFTER toward the comparable frame; it does not manufacture one, because a bench-sit anchor
// only exists if a citizen is actually sitting on a Lane B bench in this town at this moment.
const benchBonus = rows.some(r => r.state === 'bench-sit') ? 150 : 0;
return { ok: true, ...best, rows, states, clips, bodies, inFrame: rows.length, benchBonus,
score2: states.length * 100 + bodies.length * 50 + clips.length * 40 + rows.length * 10 + benchBonus,
info: window.DBG.info(), clipStats: C.clipStats() };
}
"""
# ── THE BENCH, measured in the frame and NOT from Lane B's rule ───────────────────────────────────
# `benchStops(plan)` is used for POSITIONS ONLY — to say "this instance is a bench" — which is the
# same non-circularity B used. The yaw verdict is then computed from two things B's placer never
# touches: the PLAN's road centreline (Lane A's graph) and the instance's own world matrix.
BENCH_MEASURE = r"""
async () => {
const P = window.PROCITY, V3 = P.scene.position.constructor;
const THREE = await import('three');
const F = await import('./js/world/furniture.js');
const stops = F.benchStops(P.plan);
// road geometry, straight from Lane A's graph — not from the furniture module
const nodes = new Map(P.plan.streets.nodes.map(n => [n.id, n]));
const edges = P.plan.streets.edges.map(e => { const a = nodes.get(e.a), b = nodes.get(e.b);
return { id: e.id, ax: a.x, az: a.z, bx: b.x, bz: b.z, kind: e.kind, width: e.width }; });
const byId = new Map(edges.map(e => [e.id, e]));
const foot = (e, x, z) => {
const dx = e.bx - e.ax, dz = e.bz - e.az, L2 = dx * dx + dz * dz || 1, L = Math.sqrt(L2);
let t = ((x - e.ax) * dx + (z - e.az) * dz) / L2; t = Math.max(0, Math.min(1, t));
const cx = e.ax + dx * t, cz = e.az + dz * t;
return { d: Math.hypot(x - cx, z - cz), cx, cz, ux: dx / L, uz: dz / L, e };
};
// THE EDGE A BENCH BELONGS TO IS THE ONE ITS STATION NAMES, not the geometrically nearest one.
// Measured the hard way: nearest-edge attribution reported one bench at 174.4 degrees off "the
// road" — a bench standing near a corner is often closer to the CROSS street's centreline than to
// its own, so it correctly faces its own street and the instrument calls that a 180-degree error.
// `edgeId` is metadata of the station this instance was matched to BY POSITION; the yaw verdict is
// still computed from Lane A's road geometry and the instance's own matrix, so it stays
// non-circular. Both readings are returned so the disagreement is visible rather than tuned away.
const nearestEdgePoint = (x, z) => {
let best = null;
for (const e of edges) { const f = foot(e, x, z); if (!best || f.d < best.d) best = f; }
return best;
};
const cam = P.camera; cam.updateMatrixWorld(true);
const m = new THREE.Matrix4(), pos = new V3(), q = new THREE.Quaternion(), sc = new V3();
const fwd = new V3(), v = new V3();
const rows = [];
P.scene.traverse(o => {
if (!o.isInstancedMesh) return;
o.updateWorldMatrix(true, false);
for (let i = 0; i < o.count; i++) {
o.getMatrixAt(i, m); m.premultiply(o.matrixWorld); m.decompose(pos, q, sc);
// POSITION-ONLY identification: is there a bench station under this instance?
let hit = null;
for (const s of stops) { const d = Math.hypot(s.x - pos.x, s.z - pos.z); if (d < 0.02) { hit = s; break; } }
if (!hit) continue;
fwd.set(0, 0, 1).applyQuaternion(q).normalize(); // the bench FRONT in world space
const own = byId.has(hit.edgeId) ? foot(byId.get(hit.edgeId), pos.x, pos.z) : nearestEdgePoint(pos.x, pos.z);
const ne = nearestEdgePoint(pos.x, pos.z);
const ang = (f) => {
const toRoad = new V3(f.cx - pos.x, 0, f.cz - pos.z).normalize();
return Math.acos(Math.max(-1, Math.min(1, fwd.dot(toRoad)))) * 180 / Math.PI;
};
const along = new V3(own.ux, 0, own.uz);
const degTo = ang(own), degNear = ang(ne);
const degAlong = Math.acos(Math.max(-1, Math.min(1, Math.abs(fwd.dot(along))))) * 180 / Math.PI;
v.copy(pos); v.y = 1.0; v.project(cam);
rows.push({ x: +pos.x.toFixed(2), z: +pos.z.toFixed(2), edgeId: hit.edgeId, side: hit.side,
offset: +own.d.toFixed(2), nearOffset: +ne.d.toFixed(2), nearIsOwn: ne.e.id === hit.edgeId,
toRoadDeg: +degTo.toFixed(1), nearDeg: +degNear.toFixed(1), alongDeg: +degAlong.toFixed(1),
camDist: +Math.hypot(pos.x - cam.position.x, pos.z - cam.position.z).toFixed(1),
inFrame: !(v.z > 1 || Math.abs(v.x) > 0.95 || Math.abs(v.y) > 0.95) });
}
});
// THE CONTROL, in the same evaluate: shift every station 3 m and re-identify. "coincides" has to
// be a measurement, or the 2 cm match above is just "there was an instanced mesh somewhere".
let ctrl = 0;
P.scene.traverse(o => {
if (!o.isInstancedMesh) return;
o.updateWorldMatrix(true, false);
for (let i = 0; i < o.count; i++) {
o.getMatrixAt(i, m); m.premultiply(o.matrixWorld); m.decompose(pos, q, sc);
for (const s of stops) if (Math.hypot(s.x + 3 - pos.x, s.z + 3 - pos.z) < 0.02) { ctrl++; break; }
}
});
// …and choose the camera for the DEDICATED bench frame while the geometry is still in hand.
// A SIDE-ON view is the one that reads. Standing on the bench's own front line (which is what the
// street cluster search happens to pick) shows a person facing the camera, and a person facing the
// camera looks identical whichever way the bench underneath them is turned. In profile, with the
// road on one side and the shopfront on the other, "the sitter faces the road" is visible.
let pick = null;
for (const c of P.citizens.activeCitizens()) {
if (!c.bench || !c.actor || c.actorKind !== 'rig') continue;
for (const r of rows) {
const d = Math.hypot(r.x - c.x, r.z - c.z);
if (d < 1.2 && (!pick || d < pick.d)) pick = { d, r };
}
}
let shot = null;
if (pick) {
const e = byId.get(pick.r.edgeId);
const f = e ? foot(e, pick.r.x, pick.r.z) : null;
const toRoad = f ? new V3(f.cx - pick.r.x, 0, f.cz - pick.r.z).normalize() : new V3(0, 0, 1);
const side = new V3(-toRoad.z, 0, toRoad.x); // 90 deg to the bench front
shot = { px: +(pick.r.x + side.x * 5.0 + toRoad.x * 2.4).toFixed(3),
pz: +(pick.r.z + side.z * 5.0 + toRoad.z * 2.4).toFixed(3),
ax: +(pick.r.x + toRoad.x * 0.3).toFixed(3),
az: +(pick.r.z + toRoad.z * 0.3).toFixed(3),
bench: pick.r, sitterDist: +pick.d.toFixed(2) };
}
return { stations: stops.length, matched: rows.length, control: ctrl, rows, shot };
}
"""
# Place the camera the BENCH_MEASURE pick chose, then measure the figures that land in it — in the
# same beat, for the same reason the street search does: a bench somebody has just got up from is the
# frame you actually shoot otherwise.
POSE = r"""
({ px, pz, ax, az, eye, ly }) => {
const P = window.PROCITY, V3 = P.scene.position.constructor;
P.player.teleport(px, pz, 0);
P.camera.position.set(px, eye == null ? 1.55 : eye, pz);
P.camera.lookAt(new V3(ax, ly == null ? 1.05 : ly, az));
P.chunks.warmup(P.camera.position);
P.camera.updateMatrixWorld(true);
const names = (P.fleet.all || []).map(r => r.pedName);
const cam = P.camera, v = new V3(), rows = [];
for (const c of P.citizens.activeCitizens()) {
if (!c.actor || c.actorKind !== 'rig') continue;
const d = Math.hypot(c.x - cam.position.x, c.z - cam.position.z);
if (d < 1.0 || d > 26) continue;
v.set(c.x, 1.0, c.z).project(cam);
if (v.z > 1 || Math.abs(v.x) > 0.92 || Math.abs(v.y) > 0.92) 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; } });
const posedNow = c.loiter > 0;
rows.push({ id: c.id, d: +d.toFixed(1), ped: names[c.pedIndex] || '?',
state: c.bench ? 'bench-sit' : c.lean ? 'lean' : (posedNow ? 'idle' : 'walk'),
clip: ((c.actor.mixer && c.actor.mixer._actions) || []).filter(x => x.getEffectiveWeight() > 0.5)
.map(x => x.getClip().name)[0] || null,
h: +c.height.toFixed(3), stature: +(hi - lo).toFixed(3) });
}
rows.sort((a, b) => a.d - b.d);
return { rows, info: window.DBG.info(), sitters: rows.filter(r => r.state === 'bench-sit').length };
}
"""
# ── interiors ────────────────────────────────────────────────────────────────────────────────────
# Same as R41's INT_FRAME, plus a `from: 'door'` arm: stand at Lane C's own `room.spawn` — the point
# the player is put down on when the door opens — and look at the target from there. R41 shot the
# booth from 2.5 m at eye 1.45 (a product photo); the fault John reported was about the READ from
# where you walk in, so the after has to answer that question in the frame that asks it.
INT_FRAME = r"""
async ({ target, back, eye, from }) => {
const P = window.PROCITY, V3 = P.scene.position.constructor;
const room = P.interiorMode.current;
if (!room) return { ok: false, why: 'no room open' };
const sp = room.spawn || { x: 0, z: 0 };
let tx, tz, ty = 1.15;
if (target === 'people') {
const figs = ((P.interiorMode.keepers || {}).keepers || []).map(k => k.actor.fig);
if (!figs.length) return { ok: false, why: 'no figures in the room' };
tx = 0; tz = 0; for (const f of figs) { tx += f.position.x; tz += f.position.z; }
tx /= figs.length; tz /= figs.length;
} else if (target === 'centre') {
tx = 0; tz = -room.dims.D * 0.12; ty = 1.05;
} else {
let hit = null;
room.group.traverse(o => {
if (hit) return;
const u = o.userData || {};
if (target === 'djBooth' && u.djBooth) hit = o;
});
if (!hit) return { ok: false, why: `no ${target} in this room` };
const THREE = await import('three');
const c = new THREE.Box3().setFromObject(hit).getCenter(new V3());
tx = c.x; tz = c.z; ty = Math.max(0.95, c.y);
}
let px, pz;
if (from === 'door') { px = sp.x; pz = sp.z; } // the player's real view
else {
let dx = sp.x - tx, dz = sp.z - tz; const L = Math.hypot(dx, dz) || 1;
dx /= L; dz /= L;
const d = Math.min(Math.max(back, 1.9), Math.max(L, back));
px = tx + dx * d; pz = tz + dz * d;
}
P.camera.position.set(px, eye, pz);
P.camera.lookAt(new V3(tx, ty, tz));
P.camera.updateMatrixWorld(true);
P.renderer.info.reset();
P.renderer.render(P.interiorMode.scene, P.camera);
return { ok: true, cam: [+px.toFixed(2), +pz.toFixed(2)], target: [+tx.toFixed(2), +tz.toFixed(2)],
spawn: [+sp.x.toFixed(2), +sp.z.toFixed(2)],
standoff: +Math.hypot(px - tx, pz - tz).toFixed(2),
draws: P.renderer.info.render.calls, tris: P.renderer.info.render.triangles,
shop: P.interiorMode.shop, dims: room.dims };
}
"""
# Lane D's §42.4 measure, adopted whole: the gig crew AND the keepers, each resolved to its body.
ROOM_MEASURE = r"""
() => {
const P = window.PROCITY, V3 = P.scene.position.constructor;
const names = (P.fleet.all || []).map(r => r.pedName);
const cam = P.camera; cam.updateMatrixWorld(true);
const v = new V3(); const rows = [];
const add = (a, kind, role, pedIndex) => {
if (!a || !a.inner) { rows.push({ state: kind, role, ped: 'placeholder', h: 0, stature: 0, inFrame: false }); return; }
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; } });
v.set(a.fig.position.x, 1.0, a.fig.position.z).project(cam);
rows.push({ state: kind, role, ped: (pedIndex != null && names[pedIndex]) || '?',
h: +(a.height || a.nominalHeight || 0).toFixed(3), stature: +(hi - lo).toFixed(3),
clip: ((a.mixer && a.mixer._actions) || []).filter(x => x.getEffectiveWeight() > 0.5)
.map(x => x.getClip().name)[0] || null,
inFrame: !(v.z > 1 || Math.abs(v.x) > 0.92 || Math.abs(v.y) > 0.92) });
};
const crew = P.interiorMode.crew;
if (crew) for (const m of crew.members) add(m.actor, m.part === 'band' ? 'band' : 'crowd', m.role, m.pedIndex);
for (const k of ((P.interiorMode.keepers || {}).keepers || []))
add(k.actor, k.browse ? 'browser' : 'keeper', k.type, k.pedIndex);
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 line — height-RELATIVE (D's R41 finding: a 1.32 m wall-leaner is not a defect, a 2.4 m
anything is, and a body folded to hip height is the R16 failure this exists to catch)."""
out, bad = [], []
for r in rows:
if r.get('ped') == 'placeholder': continue
h = r.get('h') or 0
ratio = (r['stature'] / h) if h else None
who = r.get('id', r.get('state'))
if r['stature'] > HI: bad.append(f'{who} GIANT {r["stature"]}m')
elif h and r['stature'] < 0.55 * h: bad.append(f'{who} FOLDED {r["stature"]}m of {h}m')
elif h and not (LO <= h <= HI): bad.append(f'{who} nominal {h}m out of [{LO},{HI}]')
out.append(f"{r['state']}" + (f"[{r.get('clip') or r.get('ped')}]" if (r.get('clip') or r.get('ped')) else '')
+ f" {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 (R10) — {len(out)} figure(s), stature (feet→crown) measured off the "
f"posed skeleton: {' · '.join(out)}. {verdict}."), not bad
def cast_line(rows, label):
"""Lane D's §42.4 idea, adopted. A JPEG cannot say 'the luchador is gone'; this can."""
peds = [r['ped'] for r in rows if r.get('ped') and r['ped'] != 'placeholder']
seen = {}
for p in peds: seen[p] = seen.get(p, 0) + 1
banned = sorted(set(peds) & set(RETIRED))
return (f"CAST ({label}) — {len(peds)} figure(s) wearing {len(seen)} distinct bodies: "
+ ', '.join(f'{k}x{v}' for k, v in sorted(seen.items())) + '. '
+ ('PERIOD LAW (ruling 1): none of the five retired bodies '
f'({", ".join(RETIRED)}) is in this frame.'
if not banned else f'*** RETIRED BODY IN FRAME: {banned} ***')), not banned
def write_shot(pg, name, caption, rows=None, cast_label=None, capture=True):
OUTDIR.mkdir(parents=True, exist_ok=True)
if capture:
pg.screenshot(path=str(OUTDIR / f'{name}.jpg'), type='jpeg', quality=92)
txt = caption
if rows is not None:
line, ok = human_line(rows)
txt += '\n\n' + line
if not ok: FAIL(f'{name}: {line}')
cl, cok = cast_line(rows, cast_label or name)
txt += '\n\n' + cl
if not cok: FAIL(f'{name}: {cl}')
(OUTDIR / f'{name}.txt').write_text(txt + '\n')
OK(f'{name}.jpg + .txt')
def pair(name, before_name, title, before_cap, after_cap):
"""Composite the committed R41 original beside today's frame. `pipeline/montage.py` is Lane E's
tool and this is exactly what it was written for (§42 notes) — no second copy of it here."""
b = BEFORE / f'{before_name}.jpg'
a = OUTDIR / f'{name}.jpg'
if not b.exists(): FAIL(f'{name}_ab: the committed R41 before is missing at {b}'); return
if not a.exists(): FAIL(f'{name}_ab: no after frame to pair'); return
out = OUTDIR / f'{name}_ab.jpg'
r = subprocess.run([sys.executable, str(ROOT / 'pipeline' / 'montage.py'), str(out),
'--cols', '2', '--width', '640', '--title', title,
f'{b}:{before_cap}', f'{a}:{after_cap}'], capture_output=True, text=True)
if r.returncode or not out.exists():
FAIL(f'{name}_ab: montage failed — {r.stdout.strip()} {r.stderr.strip()[:200]}')
else:
OK(f'{name}_ab.jpg (R41 committed original | R42 same recipe)')
def new_page(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)))
return b, pg, errs
def boot(pg, query):
pg.goto(f'{HOST}/index.html?seed={SEED}&dbg=1' + (('&' + query) if query else ''))
pg.wait_for_function('window.DBG && window.DBG.ready === true', timeout=45000)
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
pg.wait_for_function('() => window.PROCITY.fleet && window.PROCITY.fleet.ready', timeout=25000)
def enter_best(pg, want_type=None, need=None, tries=10):
js = r"""
async ({ want, need, tries }) => {
const P = window.PROCITY, D = window.DBG, C = P.citizens;
const PREF = ['record', 'opshop', 'book', 'video', 'stall', 'pawn'];
const shops = (P.plan.shops || []).filter(s => !want || s.type === want)
.map(s => ({ id: s.id, name: s.name, type: s.type, occ: (C.occupancyOf(s.id) || {}).count || 0 }))
.sort((a, b) => (b.occ - a.occ) || ((PREF.indexOf(a.type) + 9) % 9) - ((PREF.indexOf(b.type) + 9) % 9));
const has = (room) => {
if (!room) return false;
if (need === 'djBooth') { let f = false; room.group.traverse(o => { if ((o.userData || {}).djBooth) f = true; }); return f; }
return true;
};
let best = null;
for (const s of shops.slice(0, tries)) {
D.enterShop(s.id);
await new Promise(r => setTimeout(r, 2600));
const room = P.interiorMode.current;
const ks = ((P.interiorMode.keepers || {}).keepers) || [];
if (room && has(room)) {
best = { ok: true, shop: s, tried: shops.length, keepers: ks.length,
fittings: (room.placement || []).length };
return best;
}
D.exitShop(); await new Promise(r => setTimeout(r, 350));
}
return { ok: false, why: `no ${want || 'shop'} satisfied "${need}"`, tried: shops.length };
}
"""
return pg.evaluate(js, dict(want=want_type, need=need, tries=tries))
# ── 1. THE PUB ────────────────────────────────────────────────────────────────────────────────────
def shot_pub(p):
head('1. pub_furnished — R41\'s hero, re-shot: dressed props, and a cast with no luchador and no nude')
b, pg, errs = new_page(p)
try:
boot(pg, 'gigs=1&weather=0&magpie=0&washing=0')
pg.evaluate("() => window.DBG.setSegment(5)")
pg.wait_for_timeout(1500)
r = enter_best(pg, 'pub', None)
if not r.get('ok'):
FAIL(f"pub_furnished: {r.get('why')}"); return
pg.wait_for_timeout(1800) # let the gig crew resolve its bodies before the cast is read
fr = pg.evaluate(INT_FRAME, {'target': 'centre', 'back': 5.2, 'eye': 1.62, 'from': 'standoff'})
if not fr.get('ok'): FAIL(f"pub_furnished: {fr.get('why')}"); return
m = pg.evaluate(ROOM_MEASURE)
inframe = [x for x in m['rows'] if x.get('inFrame')]
note(f"{r['shop']['name']} · {r['fittings']} fittings · {fr['draws']} draws / {fr['tris']} tris · "
f"{len(m['rows'])} figures ({len(inframe)} in frame)")
cap = (f"PROCITY R42 §42.6 — pub_furnished (AFTER)\n"
f"seed {SEED}, {r['shop']['name']} (pub), segment 5, ?gigs=1 — the SAME recipe as "
f"docs/shots/laneF_r41/pub_furnished.jpg (Lane C's room.spawn line, back 5.2 m, eye 1.62 m), "
f"so the pair differs only by what landed this round.\n"
f"WHAT CHANGED: Lane E §42.1 repainted 46 props in place via a COLOR_0 rewrite (draw delta "
f"+0, tri delta +0, every POSITION/NORMAL/index accessor sha1-identical to R41) — the white "
f"blobs are brown vinyl and beer-carpet red because they always carried vertex colour and it "
f"was bleached to 0.02 saturation, not missing. Lane E §42.3 + Lane D §42.4 swapped five "
f"bodies at their own index: the purple EL CHUPACABRA luchador and the UNCLOTHED dj_phrtt_01 "
f"that stood at the stage edge in the before are both gone from the fleet.\n"
f"{r['fittings']} fittings · {fr['draws']} draws of the ≤350 interior law ({fr['tris']} tris) "
f"· camera {fr['cam']}{fr['target']}.")
write_shot(pg, 'pub_furnished', cap, m['rows'], cast_label='the whole room')
if errs: FAIL(f'pub_furnished: {len(errs)} console error(s): {errs[:2]}')
finally:
b.close()
pair('pub_furnished', 'pub_furnished',
'PROCITY R42 §42.6 — THE PUB: R41 as committed, and the same frame today',
'BEFORE (R41, committed): white plastic-looking props; TWO purple EL CHUPACABRA luchadors; '
'an unclothed body at the stage edge.',
'AFTER (R42): the props are painted (E §42.1, +0 draws), and every body in the room is a '
'person who could stand on a 1996 Sydney street (E §42.3 + D §42.4).')
# ── 2. THE BOOTH ──────────────────────────────────────────────────────────────────────────────────
def shot_booth(p):
head('2. record_dj_booth — R41\'s recipe re-shot, AND the frame R41 never took (from the door)')
b, pg, errs = new_page(p)
try:
boot(pg, 'weather=0&magpie=0&washing=0')
pg.evaluate("() => window.DBG.setSegment(2)")
pg.wait_for_timeout(1200)
r = enter_best(pg, 'record', 'djBooth')
if not r.get('ok'):
FAIL(f"record_dj_booth: {r.get('why')}"); return
# a) the PAIR arm — R41's exact camera recipe, so the before/after is a pair
fr = pg.evaluate(INT_FRAME, {'target': 'djBooth', 'back': 2.5, 'eye': 1.45, 'from': 'standoff'})
if not fr.get('ok'): FAIL(f"record_dj_booth: {fr.get('why')}"); return
m = pg.evaluate(ROOM_MEASURE)
note(f"pair arm: {r['shop']['name']} · {fr['draws']} draws / {fr['tris']} tris · "
f"camera {fr['cam']}{fr['target']} (standoff {fr['standoff']} m)")
write_shot(pg, 'record_dj_booth',
f"PROCITY R42 §42.6 — record_dj_booth (AFTER, R41's own camera recipe)\n"
f"seed {SEED}, {r['shop']['name']} (record), midday. Camera derived exactly as in R41: "
f"2.5 m off the booth group's bounding-box centre along Lane C's room.spawn line, "
f"eye 1.45 m — so this is a PAIR with docs/shots/laneF_r41/record_dj_booth.jpg.\n"
f"WHAT CHANGED: Lane E §42.2a repainted the deck chassis Technics silver (#b9bdc2) — "
f"contrast against the booth bench went 1.21:1 → 6.74:1 — and Lane C §42.2b composed the "
f"booth (bench 3 → 7 boxes, a record bay, headphones, and the booth turned to face the "
f"door: entry-line alignment 49% → 92%). Near-black pixels inside the booth's own screen "
f"rect: 57.5% → 26.7%.\n"
f"{fr['draws']} draws of the ≤350 interior law ({fr['tris']} tris) · camera {fr['cam']} "
f"{fr['target']}.", m['rows'] if m['rows'] else None, cast_label='the record shop')
# b) the DOOR arm — the frame the fault was actually reported about
fr2 = pg.evaluate(INT_FRAME, {'target': 'djBooth', 'back': 0, 'eye': 1.62, 'from': 'door'})
if not fr2.get('ok'): FAIL(f"record_dj_booth_door: {fr2.get('why')}"); return
m2 = pg.evaluate(ROOM_MEASURE)
note(f"door arm: spawn {fr2['spawn']} → booth {fr2['target']} ({fr2['standoff']} m) · "
f"{fr2['draws']} draws / {fr2['tris']} tris")
write_shot(pg, 'record_dj_booth_door',
f"PROCITY R42 §42.6 — record_dj_booth_door (THE PLAYER'S REAL VIEW)\n"
f"seed {SEED}, {r['shop']['name']} (record), midday. The camera stands at Lane C's own "
f"`room.spawn` — the exact spot the player is put down on when the door opens — at eye "
f"height 1.62 m, looking at the booth: {fr2['standoff']} m away, spawn {fr2['spawn']}"
f"booth {fr2['target']}.\n"
f"R41 shot this booth from 2.5 m at eye 1.45 m, which is a product photograph. The fault "
f"John reported — 'it reads as a plain black box' — is a claim about the ENTRY view, so "
f"this is the frame that answers it. C §42.2b moved the booth to face this line "
f"(alignment 49% → 92%).\n"
f"{fr2['draws']} draws of the ≤350 interior law ({fr2['tris']} tris).",
m2['rows'] if m2['rows'] else None, cast_label='the record shop')
if errs: FAIL(f'record_dj_booth: {len(errs)} console error(s): {errs[:2]}')
finally:
b.close()
pair('record_dj_booth', 'record_dj_booth',
'PROCITY R42 §42.6 — THE DJ BOOTH: R41 as committed, and the same camera today',
'BEFORE (R41, committed): the rigged 1200 and the TTM-54i are in there, and at normal '
'viewing distance you cannot tell it is a turntable setup.',
'AFTER (R42): silver chassis (E §42.2a, 1.21:1 → 6.74:1) in a composed booth (C §42.2b) — '
'near-black pixels in the booth crop 57.5% → 26.7%.')
# ── 3. THE STREET ─────────────────────────────────────────────────────────────────────────────────
def shot_street(p):
head('3. street_postures — the recast crowd, and Lane B\'s benches now facing the road')
b, pg, errs = new_page(p)
try:
boot(pg, 'weather=0&magpie=0&washing=0')
pg.evaluate("() => window.DBG.setSegment(2)")
pg.evaluate("() => window.DBG.shot('street_noon')")
# ── THE PAIR FRAME: stand on the BEFORE's own published camera ──────────────────────────────
# The first cut re-ran R41's cluster search and got a different spot each time, because the
# search scores a LIVE crowd — one run landed within 40 cm of R41's camera and the next landed
# 30 m away behind a wall. Re-rolling until it looks right is not a method. The pose is parsed
# out of the committed R41 sidecar (a magic number I typed would be a different claim), so the
# AFTER stands exactly where the BEFORE stood and the only variable in the pair is the tree.
m41 = re.search(r'camera \(([-\d.]+), ([-\d.]+)\) looking at \(([-\d.]+), ([-\d.]+)\)',
(BEFORE / 'street_postures.txt').read_text())
if not m41:
FAIL('street_postures: could not parse the R41 camera out of its committed sidecar'); return
px, pz, ax, az = (float(g) for g in m41.groups())
note(f'R41 published camera, parsed from its sidecar: ({px}, {pz}) → ({ax}, {az})')
sp = pg.evaluate(POSE, dict(px=px, pz=pz, ax=ax, az=az, eye=1.62, ly=1.25))
pg.wait_for_timeout(1500)
sp = pg.evaluate(POSE, dict(px=px, pz=pz, ax=ax, az=az, eye=1.62, ly=1.25))
OUTDIR.mkdir(parents=True, exist_ok=True)
pg.screenshot(path=str(OUTDIR / 'street_postures.jpg'), type='jpeg', quality=92)
states = sorted({r['state'] for r in sp['rows']})
clips = sorted({r['clip'] for r in sp['rows'] if r['clip']})
bodies = sorted({r['ped'] for r in sp['rows']})
cs = pg.evaluate("() => window.PROCITY.citizens.clipStats()")
note(f"{len(sp['rows'])} rigs in frame · states {states} · clips {clips} · bodies {bodies} · "
f"{sp['info']['drawCalls']} draws / {sp['info']['tris']} tris")
write_shot(pg, 'street_postures',
f"PROCITY R42 §42.6 — street_postures (AFTER)\n"
f"seed {SEED}, midday (seg 2). Camera ({px}, {pz}) → ({ax}, {az}) — **R41's own published "
f"pose**, parsed out of docs/shots/laneF_r41/street_postures.txt rather than re-derived. "
f"R41 chose it with a live-crowd cluster search; re-running that search on a live crowd "
f"lands somewhere different every time, so the AFTER stands where the BEFORE stood and the "
f"only variable in the pair is the tree.\n"
f"{len(sp['rows'])} rigs in frame · posture states {states} · {len(clips)} distinct clips "
f"({', '.join(clips)}) · {len(bodies)} distinct bodies · {sp['info']['drawCalls']} draws of "
f"the ≤300 street law ({sp['info']['tris']} tris) · clip bank {cs}.\n"
f"R41 read 173 draws / 43,562 tris at this pose. The recast is draw-neutral by "
f"construction (one primitive, one material per ped — Lane D §42.4); the 802-station pin "
f"reads 0/0 and a single bookmark moves ±1 run to run.\n"
f"NB the rows below count NEAR-TIER RIGS only (R41's convention, kept). A citizen far "
f"enough out to be drawn as a mid-tier billboard has no skeleton and no posture to read, "
f"so a seated figure visible in the picture can be absent from the list — that is what "
f"street_bench.jpg is for.",
sp['rows'], cast_label='the street', capture=False)
# ── THE BENCH FRAME: found by polling, shot in the same beat ────────────────────────────────
STOPS = ['crossroads_busy', 'street_noon', 'crossroads_busy', 'patronage_door', 'night_crowd']
best, bench_shot = None, None
for i in range(72):
if i % 12 == 0:
pg.evaluate(f"() => window.DBG.shot('{STOPS[(i // 12) % len(STOPS)]}')")
pg.evaluate("() => window.DBG.setSegment(2)")
pg.wait_for_timeout(900)
r = pg.evaluate(STREET_LOOK)
if not r.get('ok'):
pg.wait_for_timeout(600); continue
if not best or r['score2'] > best['score2']: best = r
# THE BENCH FRAME, TAKEN IN THE SAME BEAT IT IS FOUND. The first cut computed the pose
# here and shot it after the loop, and came back with 0 figures in frame: the dwell clock
# runs, the sitter gets up, and you photograph an empty bench with a caption about a
# sitter. Measure, pose, capture, re-measure — then move on. (This is the R41 tour's own
# lesson applied one level down; it cost that harness two frames before it was learned.)
if not bench_shot and r['anchor'].startswith('bench-sit'):
bm = pg.evaluate(BENCH_MEASURE)
if bm.get('shot'):
bp = pg.evaluate(POSE, dict(bm['shot'], eye=1.55, ly=1.05))
pg.wait_for_timeout(350)
if bp['sitters']:
pg.screenshot(path=str(OUTDIR / 'street_bench.jpg'), type='jpeg', quality=92)
bench_shot = dict(bm=bm, bp=bp, sh=bm['shot'])
# whether or not it worked, the pose moved the player — put the search back where
# it was so the next poll is not measuring a different street.
pg.evaluate(f"() => window.DBG.shot('{STOPS[(i // 12) % len(STOPS)]}')")
pg.evaluate("() => window.DBG.setSegment(2)")
pg.wait_for_timeout(700)
if len(r['states']) >= 3 and len(r['clips']) >= 3 and r['inFrame'] >= 4 and bench_shot: break
pg.wait_for_timeout(450)
if not bench_shot:
FAIL('street_bench: no citizen sat on a real Lane B bench in 72 polls — no bench frame')
else:
bm, bp, sh = bench_shot['bm'], bench_shot['bp'], bench_shot['sh']
inf = [x for x in bm['rows'] if x['inFrame']]
deg = sorted(x['toRoadDeg'] for x in bm['rows'])
along = sorted(x['alongDeg'] for x in bm['rows'])
if not bm['rows']:
FAIL('street_bench: no bench instance matched a benchStops() station within 2 cm')
elif bm['control'] != 0:
FAIL(f"street_bench: the 3 m-offset control matched {bm['control']} instance(s) — "
f"the 2 cm identification is not a measurement")
else:
mid = deg[len(deg) // 2]
corners = [x for x in bm['rows'] if not x['nearIsOwn']]
note(f"bench yaw: {len(bm['rows'])} of {bm['stations']} stations matched · front-vs-OWN-road "
f"min {deg[0]}° / med {mid}° / max {deg[-1]}° · front-vs-street med "
f"{along[len(along) // 2]}° · control 0 · {len(corners)} bench(es) sit nearer a CROSS "
f"street than their own (max nearest-edge reading {max((x['nearDeg'] for x in corners), default=0)}°)")
if deg[-1] > 5.0:
FAIL(f'street_bench: a bench front is {deg[-1]}° off its own road — B\'s §42.5-A fix is '
f'not what this scene is drawing')
note(f"bench frame: camera ({sh['px']}, {sh['pz']}) → ({sh['ax']}, {sh['az']}) — side-on, "
f"{bp['sitters']} sitter(s) and {len(bp['rows'])} figure(s) in frame · "
f"{bp['info']['drawCalls']} draws")
write_shot(pg, 'street_bench',
f"PROCITY R42 §42.6 — street_bench (Lane B §42.5-A, measured in this frame)\n"
f"seed {SEED}, midday. Camera ({sh['px']}, {sh['pz']}) → ({sh['ax']}, {sh['az']}) — "
f"5 m to the SIDE of the bench and 2.4 m out into the carriageway, deliberately NOT the "
f"cluster search's pose: that one stands on the bench's own front line, and a person "
f"facing the camera looks identical whichever way the bench under them is turned. In "
f"profile, 'the sitter faces the road' is something you can see.\n"
f"THE MEASUREMENT, and it is not B's arithmetic read back: bench instances are "
f"identified by POSITION ONLY (a `benchStops(plan)` station within 2 cm — CONTROL: the "
f"same stations offset 3 m match {bm['control']} of {bm['matched']}), and then the "
f"angle is computed between the instance's own world +Z and the foot of the "
f"perpendicular onto Lane A's road centreline for the edge that station names. "
f"Neither input is the placement rule.\n"
f"{bm['matched']} of {bm['stations']} town-wide stations are standing in the streamed "
f"window ({len(inf)} in this frame). Bench FRONT vs the direction to the road: "
f"min {deg[0]}° · median {deg[len(deg) // 2]}° · max {deg[-1]}°. Front vs the street's "
f"own direction: median {along[len(along) // 2]}°. In R41 those two were 90° and 0°.\n"
f"ONE INSTRUMENT NOTE, recorded rather than tuned away: {len([x for x in bm['rows'] if not x['nearIsOwn']])} "
f"of {bm['matched']} benches stand nearer a CROSS street's centreline than their own, "
f"because a station 14 m from a node is close to the corner. Attributing those to the "
f"geometrically nearest edge reads up to "
f"{max((x['nearDeg'] for x in bm['rows'] if not x['nearIsOwn']), default=0)}° and is "
f"WRONG — the bench correctly faces the street it belongs to. This is the same trap "
f"Lane B named in §42.5-A when they matched instances by expected perpendicular offset.\n"
f"IN THIS FRAME: {bp['sitters']} citizen(s) on a bench, {len(bp['rows'])} figure(s) "
f"total, {bp['info']['drawCalls']} draws of the ≤300 street law ({bp['info']['tris']} tris).",
bp['rows'], cast_label='the bench frame', capture=False)
if errs: FAIL(f'street: {len(errs)} console error(s): {errs[:2]}')
finally:
b.close()
pair('street_postures', 'street_postures',
"PROCITY R42 §42.6 — THE STREET: R41 as committed, and its OWN camera today",
'BEFORE (R41, committed): the R41 roster, and benches standing side-on to the road.',
"AFTER (R42): the same pose, parsed out of the R41 sidecar. Five bodies recast (E §42.3 / "
"D §42.4), bench + streetlight yaw fixed (B §42.5-A, 60/60). See street_bench.jpg for the "
"yaw in profile.")
# ── 4. THE CREDITS ────────────────────────────────────────────────────────────────────────────────
def shot_credits(p):
head('4. credits_panel — ruling 3 on screen: the ped rows filled, no `unverified` left on a body')
b, pg, errs = new_page(p)
try:
boot(pg, 'plansrc=osm&town=katoomba&weather=0&magpie=0&washing=0')
pg.evaluate("() => window.DBG.shot('street_noon')")
pg.wait_for_timeout(600)
link = pg.evaluate("() => { const el = document.getElementById('pc-creditlink'); return el ? el.textContent.trim() : null; }")
pg.keyboard.press('F2')
pg.wait_for_timeout(1200)
# scroll to the ped rows — the panel is long and the rows this round filled are the point
pg.evaluate(r"""() => {
const el = document.getElementById('pc-credits'); if (!el) return;
const box = el.querySelector('*') && el;
const walk = (n) => { for (const c of n.querySelectorAll('*'))
if (/character roster/i.test(c.textContent || '') && c.children.length < 6) return c; return null; };
const t = walk(el); if (t) t.scrollIntoView({ block: 'center' });
}""")
pg.wait_for_timeout(400)
st = pg.evaluate(r"""() => {
const el = document.getElementById('pc-credits');
const t = el ? el.innerText : '';
return { open: !!(el && getComputedStyle(el).display !== 'none'),
entries: (window.PROCITY.credits && window.PROCITY.credits.count) || null,
chars: t.length,
osm: /OpenStreetMap contributors/.test(t), odbl: /ODbL/.test(t),
roster: /character roster/i.test(t),
withdrawn: /withdrawn from the roster/i.test(t),
chIds: /Ch01|Ch49|Ch23|Ch31/.test(t),
unverifiedCount: (t.match(/unverified/gi) || []).length,
pedUnverified: /(character roster|DJ five)[^]{0,600}?unverified/i.test(t) }; }""")
note(f"HUD link {link!r} · panel open {st['open']} · roster row {st['roster']} · withdrawn row "
f"{st['withdrawn']} · Mixamo Ch ids {st['chIds']} · 'unverified' appears {st['unverifiedCount']}× "
f"· {st['chars']} chars")
cap = (f"PROCITY R42 §42.6 — credits_panel (RULING 3, on screen)\n"
f"seed {SEED}, ?plansrc=osm&town=katoomba — an OSM boot, so the ODbL obligation is live and "
f"the HUD link carries it: {link!r}. F2 opens Lane B's §41.5 surface.\n"
f"WHAT CHANGED THIS ROUND: R41 shipped the ped roster as amber `unverified` — B could not "
f"establish the bodies' provenance from anything in this repo. Lane E §42.3-A answered it "
f"from INSIDE the assets: all 24 source FBX carry Original|ApplicationVendor = 'Mixamo, Inc.' "
f"in their own headers, and the four unmerged peds kept Mixamo's numbered character ids in "
f"their material names (Ch43_Body = the luchador), which recovers the id for the other 20. "
f"So the row now reads a real licence with per-asset evidence, `required: false` is correct, "
f"and no attribution is owed. A second row records the four bodies WITHDRAWN on ruling 1 — "
f"'why is that gone' is a question a credits panel should be able to answer.\n"
f"On screen: the roster row ({st['roster']}), the withdrawn row ({st['withdrawn']}), the "
f"Mixamo Ch ids ({st['chIds']}), the © OpenStreetMap contributors credit ({st['osm']}) and "
f"ODbL 1.0 ({st['odbl']}). 'unverified' still appears {st['unverifiedCount']}x in the panel — "
f"on the PROPS row (props-house-library, 18 house props), which is a different, still-open "
f"debt and is deliberately not hidden. NO ped row is unverified: {not st['pedUnverified']}.\n"
f"Zero draws by construction (DOM); credits.json is fetched on first open, not at boot.")
write_shot(pg, 'credits_panel', cap)
if not st['open']: FAIL('credits_panel: the panel did not open on F2')
if not st['roster']: FAIL('credits_panel: the character-roster row is not on screen')
if not st['withdrawn']: FAIL('credits_panel: the withdrawn-bodies row is not on screen')
if st['pedUnverified']: FAIL('credits_panel: a ped row still reads `unverified` — ruling 3 is not met')
if errs: FAIL(f'credits_panel: {len(errs)} console error(s): {errs[:2]}')
finally:
b.close()
def main():
srv = serve()
try:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
if not ONLY or 'pub' in ONLY: shot_pub(p)
if not ONLY or 'booth' in ONLY: shot_booth(p)
if not ONLY or 'street' in ONLY: shot_street(p)
if not ONLY or 'credits' in ONLY: shot_credits(p)
finally:
srv.terminate()
print('')
if fails:
print(f'\033[31m● FAIL\033[0m — {len(fails)} problem(s):')
for f in fails: print(f' · {f}')
return 1
print(f'\033[32m● PASS\033[0m — the tour is in {OUTDIR}, every humanoid frame carrying its R10 line '
f'and its CAST line, every pair composited against the committed R41 original')
return 0
if __name__ == '__main__':
sys.exit(main())