Compare commits
2 Commits
8bd4676593
...
9b1c54ea81
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b1c54ea81 | ||
|
|
3432fccd1f |
@ -1,5 +1,39 @@
|
||||
# PROCITY-D — progress (Lane D · Citizens)
|
||||
|
||||
## Round 10 (blocker fix) — interior giants → human-sized. qa --strict GREEN. (2026-07-15)
|
||||
|
||||
Fixed the R9 ship-blocker: interior rig figures were ~2× too tall (keeper crown at 3.83 m in a
|
||||
3.4 m room, clipping the ceiling). Measured live, not from notes.
|
||||
|
||||
- **Root cause (rigs.js `buildFigure`)**: it normalised scale by `height / headY`, where `headY` is
|
||||
the head bone's world Y in bind pose. But the Mixamo skeleton's ORIGIN is the hips (~mid-body), so
|
||||
`headY` is head-**above-hips** (~half the standing height) — not the feet→crown height. Result:
|
||||
every rig ~1.83–2.0× too tall. (NB Fable's "bind-pose 82 vs posed 180" hypothesis was close on the
|
||||
numbers but the head does NOT move when posed — bind crown 3.209 m vs idle-posed 3.205 m. The 82 is
|
||||
head-above-hips; the 175 is the true feet→crown span. So it's a hips-relative measure, not animation.)
|
||||
- **Fix (one logic line)**: normalise by the feet→crown **span** = `headY - minY` (both already
|
||||
computed). Now `buildFigure(rig, h)` lands the crown at exactly `h` above planted feet. Verified all
|
||||
19 fleet rigs → crown == 1.75 m (feet 0); live keeper in "Second Time Redfern" 3.83 m → **1.82 m**.
|
||||
This one change fixes keepers, browser rigs, near-tier street rigs AND the impostor bake (all ride
|
||||
`buildFigure`) — street peds looked fine only because they're mostly impostors, which are sized by
|
||||
requested height and so masked the bug.
|
||||
- **Regression guard (required)** — new `tools/qa/interior_scale_check.py` (Playwright): enters a
|
||||
sample of open shops, injects browser rigs at every browse point, and asserts EVERY interior fig's
|
||||
crown ∈ [1.4, 2.0] m AND < room `dims.H` AND feet planted. Wired into `tools/qa.sh --strict` (gate 6,
|
||||
`run_gate` ⇒ a giant fails qa). Proven **bidirectional**: PASS on the fix (24 figs / 6 shops), FAIL
|
||||
on a reintroduced giant (crowns 3.4–4.05 m, all 6 shops red). `qa.sh --strict` = **6 passed, 0 failed**.
|
||||
- **Determinism + leak re-verified**: same seed → same keeper height + ped (shop 18 → 1.823 m, idx 10,
|
||||
identical twice; buildFigure crown stable & == requested); 15 spawn/dispose cycles → geo/tex delta 0.
|
||||
- **Re-shot** `laneD/r9_browsers_in_shop.jpg` → `laneD/r10_browsers_fixed.jpg` (kept r9 as the
|
||||
documented before-state).
|
||||
|
||||
**Figures human-sized relative to doors/fittings: yes** — verified live (measured crowns 1.6–1.86 m
|
||||
across 6 shop types under 3.4–4.0 m ceilings) and visible in `r10_browsers_fixed.jpg` (a hi-vis worker
|
||||
and a hoodie browser both well under the ceiling, in proportion to the shelves and door).
|
||||
|
||||
Files touched (mine only): `web/js/citizens/rigs.js` (fix), `tools/qa/interior_scale_check.py` (new
|
||||
gate), `tools/qa.sh` (+gate block, authorized by ROUND10 §Lane D), `docs/shots/laneD/r10_*.jpg`, docs.
|
||||
|
||||
## Round 9 (v2.0 ship) — interior presence: occupancy truth + browser rigs. qa GREEN.
|
||||
|
||||
Built the D side of the one cross-lane seam (C browse points → D occupancy+browsers → F handoff).
|
||||
|
||||
@ -4,7 +4,34 @@
|
||||
|
||||
---
|
||||
|
||||
## Round 9 (the v2.0 ship round) — DONE ✅ (v2.0 tagged)
|
||||
## Round 10 (fix the R9 giant-rig blocker, re-shoot, re-tag) — verify + ship
|
||||
|
||||
R9 shipped a blocking regression: interior rig figures came out ~2× room scale (keeper head 3.83 m in
|
||||
a 3.4 m room) because `rigs.js buildFigure` normalized off the head bone's **bind-pose** Y. **Lane D
|
||||
fixed it** (R10, commit `3432fcc`): normalize by the feet→crown **span**, plus a new `qa.sh --strict`
|
||||
no-giants gate (`interior_scale_check.py`). Lane F verify + re-shoot + soak + re-tag:
|
||||
|
||||
| task | result |
|
||||
|---|---|
|
||||
| **F1 — verify the fix** | ✅ `qa.sh --strict` **GREEN 6/6** incl. the new scale gate: **24 interior figs / 6 shops all crown ∈ [1.4,2.0] m and under-ceiling** (Second Time Redfern, the giant scene: 1.66–1.86 m). Live-measured too — the 3.83 m I first saw was a **stale ES-module cache in a day-old browser tab**, not a regression (served + fresh-chromium code both correct). |
|
||||
| **F2 — re-shoot** | ✅ `browsers.png` re-shot: browser at the rack + keeper at the counter, **human-sized** (*figures human-sized vs door/fittings: yes*). `night_crowd.png` re-framed to a readable "lit VIDEO REGAL amid CLOSED town" — **see the honest note below.** |
|
||||
| **F3 — soak** | ✅ full-default soak **PASS**: 37 chunks, 20 shops, **heap 54 MB stable (no perf drift** from the keeper fix — it's a build-time scale calc), draws 248, **0 errors**. Tri budget still ~327k > 200k (pre-existing rig-model item, D/E decimation — not an R10 change, not the blocker). |
|
||||
| **F4/6 — docs** | ✅ tour README updated with stable v2 filenames + captions; this addendum is the R10 release note. |
|
||||
| **F5 — tag** | ⏸ `v2.0` annotated tag staged on the fixed commit; **holding the push for John** given the night_crowd finding (a public release + his flagged money shot). |
|
||||
|
||||
**Honest finding — `night_crowd` has no crowd, and it can't without new work.** John flagged it as
|
||||
near-black/empty. Re-framed it readable (clean night, tight on the neon). But a visible *street* crowd
|
||||
does not form: Lane D's night patronage parks ducked-in patrons **hidden inside** the shop (they are
|
||||
the interior browsers in `browsers.png`), and seed 20261990's video shop is a **low-traffic corner**
|
||||
(tested default streamed roster + patronage-on at night: **0 patronage, 0 visible peds** at the shop).
|
||||
This is not a framing bug and not (clearly) a regression — it's that the "night crowd" was always the
|
||||
*interior* presence. A visible outdoor queue is a **v3 Lane-D mechanic** (loiter-outside-the-openLate-
|
||||
shop). The ~20 fps in night captures is a **headless software-GL artifact** (128 draws / 65k tris is light).
|
||||
→ Flagged for D: confirm night patronage at the openLate shop still behaves as the v2.0-rc notes claim.
|
||||
|
||||
---
|
||||
|
||||
## Round 9 (the v2.0 ship round) — DONE (v2.0 tag pulled, re-tagged in R10)
|
||||
|
||||
All lanes landed (C browse points + buy-anywhere, D browser rigs, A CITY_SPEC freeze + closing-time
|
||||
ruling, E VRAM audit + half-res book/toy atlases, B tram + tour bookmarks). F wired the one cross-lane
|
||||
|
||||
@ -4,31 +4,44 @@ Owned by Lane F (each lane also drops its own acceptance screenshots here).
|
||||
|
||||
```
|
||||
docs/shots/
|
||||
laneA/ … A: 5 seeds rendered in map.html (spine, arcade, market, milkbars)
|
||||
laneB/ … B: 3 fixed street cameras (P-key shot harness)
|
||||
laneC/ … C: 9 types × 5 archetypes interior grid
|
||||
laneD/ … D: near / mid tiers + busy main street
|
||||
laneE/ … E: skin contact sheets (new skins vs originals)
|
||||
v1_tour/ … F: the 10 beauty shots (see tools/shots.py TOUR) — the devlog set
|
||||
before/ … F: last known-good capture, for side-by-side regression
|
||||
references/ … real-world reference photos named per shot (street_noon.jpg, arcade.png, …)
|
||||
contact.html … generated by tools/shots.py — this-run | before | reference
|
||||
laneA/ … laneE/ … per-lane acceptance screenshots (each lane owns its dir)
|
||||
v1_tour/ … F: the v1 beauty shots (tools/shots.py) — frozen devlog set
|
||||
v2_tour/ … F: the v2 tour (tools/v2_tour.py) — the CURRENT canonical set (see below)
|
||||
before/ … F: last known-good capture, for side-by-side regression
|
||||
references/ … real-world reference photos named per shot
|
||||
v2_contact.html … generated contact sheet for the v2 tour
|
||||
```
|
||||
|
||||
## Capture (Lane F, once Lane B's shell + `window.DBG` hook land)
|
||||
## v2 tour — stable filenames (do not rename; the v3 site links these)
|
||||
|
||||
```
|
||||
cd web && python3 -m http.server 8130 # terminal 1
|
||||
python3 -m venv tools/.venv # once
|
||||
tools/.venv/bin/pip install playwright
|
||||
tools/.venv/bin/python -m playwright install chromium
|
||||
tools/.venv/bin/python tools/shots.py v1_tour --seed 20261990 # terminal 2
|
||||
open docs/shots/contact.html
|
||||
```
|
||||
`cd web && python3 -m http.server 8130` then `tools/.venv/bin/python tools/v2_tour.py`
|
||||
→ 1280×720 PNGs in `v2_tour/` + `docs/shots/v2_contact.html`.
|
||||
|
||||
Before a risky change, snapshot the good state into `before/`
|
||||
(`tools/.venv/bin/python tools/shots.py before`), then re-run and eyeball `contact.html`.
|
||||
| file | what it documents |
|
||||
|---|---|
|
||||
| `street_noon.png` | main strip at midday |
|
||||
| `crossroads_busy.png` | busiest intersection, peds about |
|
||||
| `shopfront_detail.png` | verandah + sign + window up close |
|
||||
| `patronage_door.png` | streamed peds at an open shop's door (Lane D patronage) |
|
||||
| `rain_verandah.png` | rain outside a lit `?winmap` video-shop window |
|
||||
| `rain_street.png` | wet strip, rain down the street |
|
||||
| `tram_stop.png` | the v2 tram paused at a shelter (`?tram=1`) |
|
||||
| `night_crowd.png` | **VIDEO REGAL glowing amid CLOSED neighbours at night** — the §3.5 hours story (clean-night boot). **NB:** no visible street crowd — see the note below. |
|
||||
| `browsers.png` | **an occupied shop's interior life** — a browser at the rack + the keeper at the counter, both human-sized (R10 giant-rig fix). *figures human-sized vs door/fittings: yes.* |
|
||||
| `dig_real.png` | riffling a real-cover record bin (`?dig=1&stock=real`) |
|
||||
| `katoomba_main.png` | plan-agnostic main strip (`?plansrc=osm&town=katoomba`) |
|
||||
|
||||
The v1 tour list (LANE_F_INTEGRATION §Shots): main-street noon, arcade, market square,
|
||||
milkbar dusk, night neon, warehouse fringe, residential collar, record interior, opshop
|
||||
interior, busy crossroads. Adjust `TOUR` in `tools/shots.py` as the town evolves.
|
||||
### Note on `night_crowd` (Lane F, R10)
|
||||
Re-framed to the tighter, readable "one lit shop in a closed town" composition. It does **not** show a
|
||||
standing street crowd, and that is not a framing bug: Lane D's night patronage parks ducked-in patrons
|
||||
*hidden inside* the shop (they become the interior browsers seen in `browsers.png`), and seed 20261990's
|
||||
video shop sits on a low-traffic corner, so none gather visibly outside (tested: 0 patronage / 0 visible
|
||||
at night). The town's night life is the interior presence + the neon-vs-CLOSED mood. A visible outdoor
|
||||
night queue would need a new Lane-D mechanic (loiter-outside-the-openLate-shop) — parked for v3. The
|
||||
~20 fps in night captures is a **headless-chromium software-GL artifact** (the scene is light, ~128
|
||||
draws / 65k tris); on real hardware the night scene is not a perf concern.
|
||||
|
||||
## Regression workflow
|
||||
Before a risky change, snapshot the good state into `before/`, then re-run `v2_tour.py` and eyeball
|
||||
`v2_contact.html`. **Process rule (R10): any shot containing a humanoid gets an explicit
|
||||
"figures human-sized relative to doors/fittings: yes" line in the progress note — actually look.**
|
||||
|
||||
BIN
docs/shots/laneD/r10_browsers_fixed.jpg
Normal file
|
After Width: | Height: | Size: 779 KiB |
@ -1 +1 @@
|
||||
<!doctype html><meta charset="utf-8"><title>PROCITY v2.0 tour</title><style>body{background:#111;color:#ddd;font:14px monospace;padding:16px}.g{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}figure{margin:0}img{width:100%}figcaption{color:#8f8;padding:3px 0}</style><h2>PROCITY v2.0 — 11 shots</h2><div class="g"><figure><img src="rain_verandah.png" loading="lazy"><figcaption>rain_verandah</figcaption></figure><figure><img src="night_crowd.png" loading="lazy"><figcaption>night_crowd</figcaption></figure><figure><img src="tram_stop.png" loading="lazy"><figcaption>tram_stop</figcaption></figure><figure><img src="rain_street.png" loading="lazy"><figcaption>rain_street</figcaption></figure><figure><img src="patronage_door.png" loading="lazy"><figcaption>patronage_door</figcaption></figure><figure><img src="shopfront_detail.png" loading="lazy"><figcaption>shopfront_detail</figcaption></figure><figure><img src="street_noon.png" loading="lazy"><figcaption>street_noon</figcaption></figure><figure><img src="crossroads_busy.png" loading="lazy"><figcaption>crossroads_busy</figcaption></figure><figure><img src="katoomba_main.png" loading="lazy"><figcaption>katoomba_main</figcaption></figure><figure><img src="dig_real.png" loading="lazy"><figcaption>dig_real</figcaption></figure><figure><img src="browsers.png" loading="lazy"><figcaption>browsers</figcaption></figure></div>
|
||||
<!doctype html><meta charset="utf-8"><title>PROCITY v2.0 tour</title><style>body{background:#111;color:#ddd;font:14px monospace;padding:16px}.g{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}figure{margin:0}img{width:100%}figcaption{color:#8f8;padding:3px 0}</style><h2>PROCITY v2.0 — 11 shots</h2><div class="g"><figure><img src="rain_verandah.png" loading="lazy"><figcaption>rain_verandah</figcaption></figure><figure><img src="tram_stop.png" loading="lazy"><figcaption>tram_stop</figcaption></figure><figure><img src="rain_street.png" loading="lazy"><figcaption>rain_street</figcaption></figure><figure><img src="patronage_door.png" loading="lazy"><figcaption>patronage_door</figcaption></figure><figure><img src="shopfront_detail.png" loading="lazy"><figcaption>shopfront_detail</figcaption></figure><figure><img src="street_noon.png" loading="lazy"><figcaption>street_noon</figcaption></figure><figure><img src="crossroads_busy.png" loading="lazy"><figcaption>crossroads_busy</figcaption></figure><figure><img src="night_crowd.png" loading="lazy"><figcaption>night_crowd</figcaption></figure><figure><img src="katoomba_main.png" loading="lazy"><figcaption>katoomba_main</figcaption></figure><figure><img src="dig_real.png" loading="lazy"><figcaption>dig_real</figcaption></figure><figure><img src="browsers.png" loading="lazy"><figcaption>browsers</figcaption></figure></div>
|
||||
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 778 KiB After Width: | Height: | Size: 776 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 849 KiB After Width: | Height: | Size: 846 KiB |
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 478 KiB After Width: | Height: | Size: 478 KiB |
|
Before Width: | Height: | Size: 806 KiB After Width: | Height: | Size: 808 KiB |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 437 KiB After Width: | Height: | Size: 437 KiB |
|
Before Width: | Height: | Size: 810 KiB After Width: | Height: | Size: 812 KiB |
|
Before Width: | Height: | Size: 874 KiB After Width: | Height: | Size: 861 KiB |
15
tools/qa.sh
@ -96,6 +96,21 @@ else
|
||||
soft_skip "v2 flags harness" "Playwright venv absent (python3 -m venv tools/.venv && …/pip install playwright)"
|
||||
fi
|
||||
|
||||
# ── Gate 5 (Lane D, round 10): interior figure-scale regression guard ────────
|
||||
# Enters a sample of open shops and asserts every interior rig figure's crown is human-sized
|
||||
# [1.4,2.0] m AND under the room ceiling — so the R9 giant blocker (buildFigure normalised off
|
||||
# head-above-hips → ~2× too tall) can never pass a gate again. run_gate ⇒ a giant FAILS qa.
|
||||
# Needs the Playwright venv (auto-skips where absent, like the flags harness). Wired per ROUND10 §D.
|
||||
SCALE_SKIP=0; for a in "$@"; do [ "$a" = "--no-scale" ] && SCALE_SKIP=1; done
|
||||
if [ "$SCALE_SKIP" = 1 ]; then
|
||||
soft_skip "interior figure-scale gate" "--no-scale"
|
||||
elif [ -x tools/.venv/bin/python ] && tools/.venv/bin/python -c "import playwright" 2>/dev/null; then
|
||||
run_gate "interior figure-scale (no giants: crown ∈ [1.4,2.0] m, under ceiling · Lane D R10)" \
|
||||
tools/.venv/bin/python tools/qa/interior_scale_check.py
|
||||
else
|
||||
soft_skip "interior figure-scale gate" "Playwright venv absent (python3 -m venv tools/.venv && …/pip install playwright)"
|
||||
fi
|
||||
|
||||
# ── Summary ──────────────────────────────────────────────────────────────────
|
||||
hr
|
||||
printf '%sSUMMARY%s %s%d passed%s · %s%d failed%s · %s%d warn%s · %s%d skipped%s\n' \
|
||||
|
||||
144
tools/qa/interior_scale_check.py
Normal file
@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PROCITY Lane D — interior figure-scale gate (Round 10 regression guard).
|
||||
|
||||
Guards the R9 blocker: interior rig figures came out ~2x too tall (keeper head at 3.83 m in a
|
||||
3.4 m room) because rigs.js `buildFigure` normalised by the head bone's world Y in BIND POSE —
|
||||
which is head-above-hips (the skeleton origin), ~half the standing height — instead of the
|
||||
feet->crown span. Fixed in R10 (normalise by `headY - minY`).
|
||||
|
||||
This gate boots the real game, enters a sample of open shops, spawns browser rigs at every browse
|
||||
point (occupancy is 0 right after boot, so we inject it to exercise the browse spawn path
|
||||
deterministically), and asserts for EVERY spawned interior figure:
|
||||
1.4 m <= crown(head-bone world Y) <= 2.0 m (human-sized)
|
||||
crown < room dims.H (never clips the ceiling)
|
||||
|feet(min-bone world Y)| < 0.05 m (planted on the floor)
|
||||
Exits non-zero if any figure fails, so tools/qa.sh --strict fails a giant before it can ship.
|
||||
|
||||
Run: tools/.venv/bin/python tools/qa/interior_scale_check.py [--seed N]
|
||||
Setup: the same Playwright venv as tools/soak.py / tools/flags_check.py.
|
||||
Needs Lane B's window.DBG hook (?dbg=1) + window.PROCITY.interiorMode (Lane C/F).
|
||||
"""
|
||||
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
|
||||
if '--seed' in sys.argv:
|
||||
SEED = int(sys.argv[sys.argv.index('--seed') + 1])
|
||||
|
||||
LO, HI = 1.4, 2.0 # human crown band (Round-10 gate)
|
||||
FOOT_EPS = 0.05 # feet must plant at floor y=0
|
||||
SAMPLE_TYPES = ['opshop', 'toy', 'video', 'record', 'book', 'milkbar', 'clothes', 'hardware']
|
||||
|
||||
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 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}")
|
||||
|
||||
# JS: enter is done via DBG; this measures every interior fig after injecting browsers at browse points.
|
||||
MEASURE_JS = r"""
|
||||
(shopId) => {
|
||||
const P = window.PROCITY, THREE = P.THREE, im = P.interiorMode, v = new THREE.Vector3();
|
||||
const cur = im.current;
|
||||
if (!cur || P.mode !== 'interior') return { err: 'not in interior' };
|
||||
// occupancy is 0 right after boot; inject a browser rig at each browse point to exercise the
|
||||
// browse spawn path (same spawnRig->buildFigure the shell uses when patronage fills a shop).
|
||||
const bps = cur.browsePoints || [];
|
||||
bps.slice(0, 3).forEach((pt, i) => {
|
||||
try { im.keepers.spawn(cur.group, { x: pt.x, z: pt.z, ry: pt.ry, shopId: shopId, browse: true,
|
||||
seedKey: 'qa#' + i, pedIndex: (i * 7) % 19 }); } catch (e) {}
|
||||
});
|
||||
const H = +cur.dims.H;
|
||||
const figs = im.keepers.keepers.map(k => {
|
||||
const a = k.actor; let crown = 0, foot = Infinity;
|
||||
a.fig.updateWorldMatrix(true, true);
|
||||
(a.inner || a.fig).traverse(o => { if (o.isBone) { o.getWorldPosition(v);
|
||||
if (/head/i.test(o.name)) crown = Math.max(crown, v.y); foot = Math.min(foot, v.y); } });
|
||||
return { kind: k.kind, browse: !!k.browse, crown: +crown.toFixed(3), foot: +foot.toFixed(3) };
|
||||
});
|
||||
return { H: +H.toFixed(2), figs };
|
||||
}
|
||||
"""
|
||||
|
||||
def main():
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
except ImportError:
|
||||
sys.exit("playwright not installed — python3 -m venv tools/.venv && tools/.venv/bin/pip install playwright && tools/.venv/bin/playwright install chromium")
|
||||
|
||||
print(f"\033[1mINTERIOR FIGURE-SCALE GATE\033[0m (seed {SEED}, band [{LO},{HI}] m, no giants)")
|
||||
srv = ensure_server()
|
||||
total_figs = 0
|
||||
shops_checked = 0
|
||||
try:
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch()
|
||||
pg = b.new_page(viewport={'width': 1280, 'height': 720})
|
||||
pg.goto(f'{HOST}/index.html?seed={SEED}&dbg=1')
|
||||
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:
|
||||
sys.exit("fleet never became ready — cannot test rig scale")
|
||||
pg.evaluate("() => window.DBG.setSegment(2)") # midday: shops open
|
||||
|
||||
for t in SAMPLE_TYPES:
|
||||
sid = pg.evaluate("""(t) => {
|
||||
const P=window.PROCITY;
|
||||
const s=(P.plan.shops||[]).find(x=>x.type===t && P.isOpen(x));
|
||||
if(!s) return null; window.DBG.enterShop(s.id); return {id:s.id, name:s.name};
|
||||
}""", t)
|
||||
if not sid:
|
||||
continue
|
||||
pg.wait_for_timeout(400) # interior build + keeper/browser rigs settle
|
||||
res = pg.evaluate(MEASURE_JS, sid['id'])
|
||||
if isinstance(res, dict) and res.get('err'):
|
||||
FAIL(f"{sid['name']} ({t}): {res['err']}")
|
||||
pg.evaluate("() => window.DBG.exitShop && window.DBG.exitShop()"); pg.wait_for_timeout(150)
|
||||
continue
|
||||
H, figs = res['H'], res['figs']
|
||||
shops_checked += 1
|
||||
bad = []
|
||||
for f in figs:
|
||||
total_figs += 1
|
||||
if not (LO <= f['crown'] <= HI): bad.append(f"crown {f['crown']}m out of [{LO},{HI}]")
|
||||
elif not (f['crown'] < H): bad.append(f"crown {f['crown']}m >= ceiling {H}m")
|
||||
elif abs(f['foot']) >= FOOT_EPS: bad.append(f"feet at {f['foot']}m (not planted)")
|
||||
if bad:
|
||||
FAIL(f"{sid['name']} ({t}, H={H}m): " + "; ".join(bad))
|
||||
else:
|
||||
crowns = ", ".join(f"{f['crown']:.2f}" for f in figs)
|
||||
OK(f"{sid['name']} ({t}, H={H}m): {len(figs)} figs crowns=[{crowns}] all human-sized")
|
||||
pg.evaluate("() => window.DBG.exitShop && window.DBG.exitShop()")
|
||||
pg.wait_for_timeout(150)
|
||||
b.close()
|
||||
finally:
|
||||
if srv: srv.terminate()
|
||||
|
||||
print()
|
||||
if shops_checked == 0:
|
||||
sys.exit("\033[31m✗ no shops could be entered — gate inconclusive\033[0m")
|
||||
if fails:
|
||||
print(f"\033[31m● {len(fails)} FAIL\033[0m — interior giant(s) detected across {shops_checked} shop(s), {total_figs} figs.")
|
||||
sys.exit(1)
|
||||
print(f"\033[32m● PASS\033[0m — {total_figs} interior figs across {shops_checked} shops all human-sized [{LO},{HI}] m and under-ceiling.")
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -14,8 +14,11 @@ HOST = 'http://127.0.0.1:8130'
|
||||
# flag-combo -> [street bookmarks] (each dbg.js bookmark sets its own pose + day segment)
|
||||
GROUPS = [
|
||||
('winmap=1&weather=rain&tram=1&stock=real&pop=200',
|
||||
['rain_verandah', 'night_crowd', 'tram_stop', 'rain_street', 'patronage_door', 'shopfront_detail',
|
||||
['rain_verandah', 'tram_stop', 'rain_street', 'patronage_door', 'shopfront_detail',
|
||||
'street_noon', 'crossroads_busy']),
|
||||
# [Lane F R10] night_crowd on a CLEAN night boot (no rain → the neon reads brighter + fps recovers;
|
||||
# rain variants already exist as rain_verandah/rain_street). High pop for any passers-by.
|
||||
('pop=300', ['night_crowd']),
|
||||
('plansrc=osm&town=katoomba&pop=180', ['katoomba_main']),
|
||||
]
|
||||
# interior shots: flags, then enter + optional dig
|
||||
@ -43,9 +46,14 @@ def main():
|
||||
boot(pg, flags)
|
||||
for name in marks:
|
||||
try:
|
||||
pg.evaluate("(n) => window.DBG.shot(n)", name)
|
||||
# [Lane F R10] night_crowd's dist-16 bookmark reads dark + empty (the openLate video
|
||||
# shop sits at a dead-end corner and nothing routes patrons to it at night — a real
|
||||
# engine gap, noted for D). Reuse night_neon's tighter dist-12 pose so the stable
|
||||
# night_crowd.png filename carries the readable "one lit shop amid a CLOSED town" shot.
|
||||
pose = 'night_neon' if name == 'night_crowd' else name
|
||||
pg.evaluate("(n) => window.DBG.shot(n)", pose)
|
||||
pg.wait_for_timeout(1100)
|
||||
pg.evaluate("(n) => window.DBG.shot(n)", name) # settle bloom/segment
|
||||
pg.evaluate("(n) => window.DBG.shot(n)", pose) # settle bloom/segment
|
||||
pg.wait_for_timeout(300)
|
||||
pg.screenshot(path=str(OUT / f'{name}.png'))
|
||||
shot_names.append(name); print('shot', name)
|
||||
|
||||
@ -104,17 +104,22 @@ export function buildFigure(rig, height = 1.75) {
|
||||
fig.add(inner);
|
||||
fig.updateWorldMatrix(true, true);
|
||||
|
||||
// height-normalise off the head bone (Mixamo export scale is unreliable), then plant feet.
|
||||
// height-normalise so the crown sits at `height` above the feet, then plant feet.
|
||||
// NB the Mixamo skeleton's ORIGIN is the hips (~mid-body), so the head bone's world Y in bind pose
|
||||
// is head-above-hips (~half the standing height), NOT the standing height. Normalising by that alone
|
||||
// makes every figure ~2× too tall (the R9 interior-giant blocker). Normalise by the feet→crown SPAN
|
||||
// (top head bone minus lowest bone) so a requested 1.75 m rig is 1.75 m from sole to crown.
|
||||
let headY = 0, minY = Infinity;
|
||||
inner.traverse(o => {
|
||||
if (o.isBone) {
|
||||
o.getWorldPosition(_wp);
|
||||
if (/head/i.test(o.name)) headY = Math.max(headY, _wp.y);
|
||||
minY = Math.min(minY, _wp.y);
|
||||
if (/head/i.test(o.name)) headY = Math.max(headY, _wp.y); // crown (HeadTop_End is highest)
|
||||
minY = Math.min(minY, _wp.y); // sole (lowest foot/toe bone)
|
||||
}
|
||||
});
|
||||
if (headY > 1e-4) {
|
||||
inner.scale.setScalar(height / headY);
|
||||
const span = headY - minY; // feet→crown standing height in the rig's native units
|
||||
if (headY > 1e-4 && span > 1e-4) {
|
||||
inner.scale.setScalar(height / span);
|
||||
} else {
|
||||
// no head bone found — fall back to bounding-box height so we never ship a giant/ant
|
||||
_bb.setFromObject(inner); _bb.getSize(_bs);
|
||||
|
||||