PROCITY/C-progress.md
m3ultra aa9c140eca Lane C R5 C1: v1.1 re-map — record_crate (fixed) + counter_till
Round-5 §Lane C task 1 (the v1.1 gate). Map Lane E's R4-late re-exports in
glb.js:

- record_crate FIXED (E stripped sourceless AO/roughness slots) → crate/
  recordBin upgrade to plastic crates with the procedural sleeves inside;
  removed the stale "fails to load" comment.
- counter → 'counter_till' (the real ~1.6m timber counter WITH a modelled
  till; NOT the 4m balcao). Footprint-fit + floor-plant + keeper-stand pose
  unchanged.
- Double-till resolved (documented): counter_till carries its own till, so
  the separate cash_register attach is dropped whenever the counter GLB is
  live (falls back to cash_register on the primitive counter otherwise);
  milkshake_mixer stays on the milk-bar bench, lifted +0.10m onto the taller
  GLB counter.

Validated (GLB on, via depot): sweep+soak {throws 0, path 0, det 0, worst
10.4ms}, 0 rooms with a visible primitive till, leak-free vs a saturated
baseline (the transient 2geo/4tex first-sight is record_crate + counter_till
cache-fill). Primitive path byte-unchanged (glb.js-only edit). qa.sh --strict
GREEN. Contact sheet docs/shots/laneC/glb_v1_1_r5.jpg. → F can tag v1.1
(LANE_C_NOTES.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 20:56:00 +10:00

267 lines
19 KiB
Markdown
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.

# LANE C — INTERIORS · progress (PROCITY-C)
*Status: **v1 complete & verified**. Standalone interiors library + test page. Every shop door opens
into a unique, seeded, themed interior, generated on demand in ~4ms, byte-identical every revisit.*
Last updated: 2026-07-14 (round 5) · owner: PROCITY-C · reviewer: Fable
---
## Update 2026-07-14 (round 5) — C1 v1.1 re-map (record_crate + counter_till) · **F can tag v1.1**
Round-5 §Lane C task 1 (the v1.1 gate). Lane E's re-exports mapped + validated in `glb.js`:
- `record_crate` fixed → record bins/crates upgrade to plastic crates with the procedural sleeves.
- `counter → 'counter_till'` (the real ~1.6 m counter-with-till). Keeper-stand pose unaffected.
- **Double-till resolved**: cash_register attach dropped when counter_till is live (it has its own till);
falls back on the primitive; milkshake_mixer stays on the milk-bar bench (lifted to the GLB height).
- Sweep + soak (GLB on): `0 throws / 0 path / 0 det / leak-free / worst 10.4ms`; **0 double tills**.
Primitive path unchanged (glb.js-only edit). Contact sheet: [glb_v1_1_r5.jpg](docs/shots/laneC/glb_v1_1_r5.jpg).
- Report + decision detail: [LANE_C_GLB_VALIDATION.md](docs/LANES/LANE_C_GLB_VALIDATION.md); F note in
[LANE_C_NOTES.md](docs/LANES/LANE_C_NOTES.md). **→ F: C1 committed, v1.1 can be tagged.**
Still in flight this round: C2 `?dig=1` crate-riffle (v2, behind flag), C3 record re-measure (after E's bake).
---
## Update 2026-07-14 (round 4) — hero props into rooms
Round-4 task (ROUND4_INSTRUCTIONS §Lane C): map the 4 new hero props + cash_register (manifest now 14
fittings), sweep/soak with them on, screenshot each, delete the stale `_local_glb/`, measure a hero
room. **All done.** Full report for Lane E: [LANE_C_GLB_VALIDATION.md](docs/LANES/LANE_C_GLB_VALIDATION.md);
contact sheet [docs/shots/laneC/glb_heroprops_r4.jpg](docs/shots/laneC/glb_heroprops_r4.jpg).
- **New mappings** (`glb.js`): `arcade_cabinet`→new `arcadeCabinet` primitive (video focal prop),
`listening_booth`→`listeningCorner` (record), `drinks_fridge`→`fridge` (milkbar). All load, `glb_law`
compliant, yaw 0 (no correction). A new **counter-top attach** (`attachCounterTop`) puts `cash_register`
on every counter's till slot (hides the primitive till) and `milkshake_mixer` on the milk-bar benchtop —
the counter primitive + keeper stand are untouched.
- **Focal-prop reliability fix**: the door-counter path loop was pulling priority-4 focal props (arcade
appeared in only 5/24 video rooms). Raised the 3 focal props to priority 6 + placed-first →
**24/24 appearance**, still 0 carves / 0 path-fails.
- **Validation**: primitive sweep 810 builds `{throws 0, pathFail 0, detFail 0}`; GLB-on soak
`{throws 0, path 0, det 0, leakGeo 0, leakTex 0, worst 7.3ms}`; all 5 new props confirmed in-room.
- **Room tris measured** (task 4): milk-bar GLB-on ≈ **468k tris** (over budget) driven by `drinks_fridge`
(77k) + `milkshake_mixer` (73k) — **reported to Lane E** as the #1 bake-pass target (not fixed, per
integrator decision #2; interiors render one-at-a-time so gate 3 / the street budget is unaffected).
- **Cleanup**: deleted `web/assets/_local_glb/` (R3 validation copy); test page now loads GLBs via the
live `depot:` path (`?localdepot=1` still works for offline). `localBase` hook kept in `glb.js`.
- **Standby (C task 5)**: when Lane E re-exports `record_crate` (broken) + a ~2 m counter-with-till, I
re-map both. Not landed at commit time.
---
## Update 2026-07-14 (round 3) — GLB hero-prop visual validation
Round-3 task (ROUND3_INSTRUCTIONS §Lane C): flip `useGLB` on and validate Lane E's fitting GLBs in real
rooms. Done — full report for Lane E in [docs/LANES/LANE_C_GLB_VALIDATION.md](docs/LANES/LANE_C_GLB_VALIDATION.md).
**Verdict:** 5 of 9 fitting GLBs are good and now live behind `useGLB` (wire_shelf, clothes_rack,
bookshelf, cube_shelf_wide, work_table); 2 fall back to primitive with a report filed
(`record_crate` **broken — won't load**; `counter` **too long/no till** → primitive keeps the till +
keeper stand). record bins stay primitive until Lane E re-exports the crate.
**Soak with `useGLB` on** (50 rooms, all types): `throws 0 · pathFail 0 · determinismFail 0 · leakGeo 0 ·
leakTex 0 · worst build 10.5ms · 256 upgrades applied` — GLB resources dispose leak-free on room exit.
**glb.js changes (my file):**
- `manifest.localBase` — load GLBs from a served local dir for offline validation (production still uses
`depot:<file>`; forward-compatible with Lane E's local-depot mode).
- **Real-metre scale, clamped** to the reserved footprint (was fit-to-width, which squashed tall props);
matches `glb_law` (metres). Per-kind **yaw correction** (`KIND_TO_YAW`) for the Z-major assets
(bookshelf/cube/work_table need +90°) per the manifest `facing` convention ("Lane B/C rotate directional props").
- `room.glbReady` (interiors.js) — a promise that resolves when GLB swaps settle, so callers can await
before screenshot/leak-check (Lane F can use it in the §3.4 wiring).
- Test page (`interior_test.html`) gains a **GLB props** toggle + a `GLB props: N upgraded` HUD line.
Local validation needs the GLBs copied in: `cp pipeline/_normalized/procity_fit_*.glb web/assets/_local_glb/`
(untracked; real GLBs live on the depot). Screenshot grid: [docs/shots/laneC/glb_validation_grid.jpg](docs/shots/laneC/glb_validation_grid.jpg).
---
## Update 2026-07-14 (b) — keeper stand pose (resolves Lane F §8 · unblocks §3.2/§3.3)
Lane F's runbook asked (LANE_F_NOTES §8): *"does `places` tag the counter slot with a stable
`kind:'counter'` so keepers find it?"* **Yes** — the counter interactable has always carried
`mesh.userData.kind === 'counter'` (+ `interactable:true`). But Lane D's keeper needs more than a
tag: `keepers.spawn(group,{x,z,ry})` needs *where the shopkeeper stands and which way it faces*
(keepers.js:28 — "`ry` = the counter's outward facing"). I computed the customer-facing side
internally but never surfaced it — so Lane D would have read the bench's raw transform and stood a
keeper embedded in the counter, possibly facing a wall. Closed that gap (my files only, purely additive):
- **New: `room.counter = { mesh, pose:{x,z,ry}, stand:{x,z,ry} }`** (interiors.js). `pose` is the
bench; **`stand` is the keeper pose** — behind the bench on the service side, facing the customer,
clamped inside the room, in **room-local space** (the same space Lane D adds the keeper into via
`interior.group`). `ry` uses the rig-front = local Z convention keepers.js expects.
- **The same `{x,z,ry}` also rides the counter interactable**: `place.userData.keeperStand` — for the
`places`-iteration path Lane D/F described. Deep-equal to `room.counter.stand`.
**Lane D/F wiring (drop-in for §3.2/§3.3):**
```js
const room = buildInterior(shop, THREE);
const s = room.counter.stand; // or: room.places.find(p=>p.userData.kind==='counter').userData.keeperStand
keepers.spawn(room.group, { ...s, shopId: shop.id, type: shop.type });
// on exit: keepers.remove(handle) / keepers.disposeAll(), then room.dispose()
```
Verified in-browser (9 types × 4 seeds = 36 builds, + 50-room soak):
`stand in-bounds ✓ · faces-customer ✓ (dot>0 for all) · places-carries-keeperStand ✓ (deep-equal) ·
stand deterministic ✓ · placement determinism unchanged ✓ · soak avg 3.6ms / worst 7.7ms · leak geo0 tex0 ·
pathFail 0 · every room has a counter+stand`. No change to layout/placement of existing rooms.
**Minor known cosmetic (corner counters):** a corner counter sits ~0.55 m off the east wall, so the
service gap (~0.25 m) is narrower than a person — the keeper stands *at* the till with a small hidden
overlap into the solid waist-high bench (torso reads above the counter, legs behind it). Facing and
side are correct; it reads as "keeper at the till." If Lane D/Fable want real standing clearance I can
nudge corner counters ~0.2 m off the wall — but that changes those rooms' committed placement +
screenshots, so it's a decision, not a bug.
**Two contract-drift corrections for the F runbook** (§2's Lane-C line was written against an earlier
draft of my API): (1) there is **no `opts.registry`** — registry override is the one-time global
`mergeRegistry(registry)` (a per-build registry mutation broke same-seed determinism; removed in the
review pass). (2) `recipe.counterPos` is a **placement-strategy string** (`door|forward|back|corner`),
**not** a world position — use `room.counter.stand` for the keeper pose.
---
## What shipped
All files under my lane's ownership (`web/js/interiors/*`, `web/interior_test.html`) — no other lane's
files touched. Lane A's `core/registry.js` hadn't landed, so the lane is **fully standalone** (its 9
type recipes live in `theme.js`) with a `mergeRegistry()` hook for Lane F to wire the registry later.
| file | role |
|---|---|
| [interiors.js](web/js/interiors/interiors.js) | **public API**`buildInterior(shop, THREE, opts)` |
| [theme.js](web/js/interiors/theme.js) | 9 shop-type recipes (archetype/wallpaper/floor bias, clutter, counter pos, fittings mix, stock kind, signage) + aliasing + registry-override hook |
| [shell.js](web/js/interiors/shell.js) | room shell: floor/walls/ceiling from lot×archetype, glazed shopfront + street backdrop, blocked back doorway, interior lighting |
| [fittings.js](web/js/interiors/fittings.js) | parametric fittings kit (ported from 90sDJsim + extended): bins, crates, racks, 4 shelf types, VHS aisle, glass case, counter+till, fridge, magazine/spinner racks, armchair, escalator, pegboard, barred window, returns slot, art frames |
| [layout.js](web/js/interiors/layout.js) | the placer: per-archetype zones, shuffled wall-slot system, occupancy grid, **guaranteed door→counter flood-fill path** |
| [stock.js](web/js/interiors/stock.js) | v1 visual stock: pooled canvas-texture sleeves/spines/boxes/garments/snacks with price stickers (dig.js trick) + `stockAdapter` hook for BaseGod data |
| [context.js](web/js/interiors/context.js) | build/dispose/seed backbone: independent seeded sub-streams, shared-geometry cache, leak-free `disposeAll()` |
| [glb.js](web/js/interiors/glb.js) | **optional** GLB hero-prop upgrade — reads Lane E's `manifest.json`, swaps primitives for depot GLBs, placeholder-persists. OFF by default; primitive fallback always |
| [interior_test.html](web/interior_test.html) | standalone test page (below) |
## Public API (the contract Lane B/F build against)
```js
import { buildInterior } from './js/interiors/interiors.js';
import * as THREE from 'three';
const room = buildInterior(shop, THREE, opts?);
scene.add(room.group);
// … player walks; on hitting room.exits[i], fire procity:exitShop …
room.dispose(); // frees ALL per-room GPU resources; removes group from parent
// room = {
// group, // THREE.Group, self-contained (lights included)
// spawn: { x, z, ry }, // enter just inside the door, facing Z (into the shop)
// exits: [ { x, z, w, toStreet } ], // door back to street
// places: [ …meshes w/ userData ], // interactables: counter, bin, case, fridge, returns, exit
// counter: { mesh, pose, stand }, // pay point. stand:{x,z,ry} = keeper (Lane D) spawn pose
// dims: { W, D, H, archetype, type },
// placement, // deterministic summary (deep-equal per seed)
// pathOK, buildMs, counts(), dispose()
// }
```
`shop` is tolerant — recognized fields `{ id, type, name, seed, storeys, lot:{w,d} }`:
- **seed** (uint32) is the ONLY randomness source; falls back to a hash of id/name/type if absent.
- **type** — any CITY_SPEC type (`record opshop toy book video pawn milkbar dept stall`) **or** a
thriftgod/Overpass alias (`music charity pawnbroker market video_games …`); defaults to `opshop`.
- **lot** `{w,d}` metres — the room adapts to it (never exceeds it); absent → archetype ranges decide.
- **storeys** drives ceiling height 3.24.5m.
- `opts`: `{ archetype?: 'cosy'|'gallery'|'wide'|'hall'|'pokey', stockAdapter?, useGLB?, manifest?, manifestUrl? }`.
Registry override is a **one-time global**`import { mergeRegistry } from interiors.js; mergeRegistry(registry)`
at init (NOT a per-build opt — a per-call mutation of the shared recipe table would break same-seed determinism).
**Coordinate convention** (shared with a future Lane B wiring): `+Z` = door/street side, `Z` = back,
origin = room centre, floor `y = 0`. Everything is in the returned `group`'s local space.
### For Lane B (door wiring, later)
`buildInterior` is a pure function — call it on `procity:enterShop`, add `room.group` to the scene,
teleport the player to `room.spawn`. On `procity:exitShop` (player reaches `room.exits[0]`) call
`room.dispose()` and restore the street. The street window fake is Lane B's; I own the inside view
(a bright canvas street backdrop plane is already behind the glazing).
### For content phase (BaseGod / GODVERSE)
Pass `opts.stockAdapter = (shop, slotKind) => ({ texture }) | ({ mesh }) | null`. Returning a texture
skins that stock item's face; a mesh replaces the item; null → procedural placeholder. No layout/
fittings changes needed. `slotKind ∈ { sleeve, spine, box, snack, magazine, garment, treasure }`.
## Test page — `web/interior_test.html`
Serve: `cd web && python3 -m http.server 8130` → open `/interior_test.html`.
- seed input · type dropdown (9) · archetype override (auto + 5) · lot `w×d`/auto · re-roll · seed ±
- first-person walk (PointerLock + WASD) with occupancy-grid collision + room clamp
- debug toggles: **wireframe** (F), **occupancy grid** (G), **path** (spawn/counter markers)
- **▶ 50-room soak test**: builds+disposes 50 seeded rooms, asserts ms/room, leaked geometries vs
baseline, and determinism (same seed → identical placement)
## Acceptance — all met
| requirement | result |
|---|---|
| Same seed twice → identical room (deep-equal placement) | ✅ 0 mismatches / 810 builds |
| All 9 types × 5 archetypes render sensibly | ✅ contact sheet `docs/shots/laneC/_grid_9types_5archetypes.jpg` |
| Build < 50ms/room | steady-state ~4ms, clean-soak worst ~8ms (first-ever build ~60ms one-time cold-JIT; occasional ~60ms GC pauses only under back-to-back torture loops not a real per-room cost) |
| Dispose leak-free (info.memory baseline) | soak leak: geo 0, tex 0 |
| Flood-fill doorcounter always exists | 0 path-fails, 0 carves / 810 builds |
| Runs with zero assets & zero network | seeded flat-colour fallback under every texture; map only assigned on load |
**Full sweep** (9 types × {auto+5 archetypes} × 3 seeds × {null,4×4,5×14,20×20,3×3} lots = 810 builds):
`throws 0 · pathFail 0 · determinismFail 0 · carved 0`. Warm perf: all < 50ms (worst 33.2ms).
**Soak** (50 rooms): `avg 4.0ms · worst 9.6ms · leakGeo 0 · leakTex 0 · detFail 0 · pathFail 0`.
## Key decisions & notes for review
- **90sDJsim `fittings.js` ported** (it was written to be lifted): `clothesRack`, `shelfUnit`,
`recordBin`, `table`, `counter`, `pegboard`, garment-silhouette canvas trick. Extended with the
fittings CITY_SPEC's registry needs (cube/metal/book/VHS shelving, glass case, fridge, spinner,
magazine rack, armchair, escalator prop, returns slot, barred window, listening corner).
- **thriftgod `buildShop` ported**: the 5 archetypes, the shuffled wall-slot system, bins with fanned
sleeves wearing covers, corner counter+till, crooked wall art, blocked back doorway.
- **Determinism** via independent seeded sub-streams keyed by a salt string (`ctx.stream('wallpaper')`
etc.) so adding a fitting in one subsystem never shifts another subsystem's picks.
- **Leak-free** via a tracked build context: per-room geometry/material/CanvasTexture/tiled-file-texture
are freed in `dispose()`; globally-cached file textures (`core/loaders.loadTex`) are shared and not
disposed (bounded cache). Dispose-before-texture-load race is guarded (`_disposed`).
- **Perf**: shared-geometry cache (`ctx.boxGeo/planeGeo/cylGeo`) collapses the hundreds of identical
stock items in a room down to ~80110 geometries; spine density tuned so a book barn is ~600 meshes.
- **GLB upgrades**: `web/assets/manifest.json` (Lane E) is read if present, primitive fallback always.
Not present yet 100% primitives, which is why the test page runs asset-free.
## Integration with sibling lanes (landed in parallel)
- **Lane A `core/registry.js`** its `SHOP_TYPE_IDS` match my 9 recipe keys exactly. I kept the
interior recipes local (the registry doesn't carry archetype-bias/clutter/counter-pos/stock detail),
with `mergeRegistry()` as the one-time override seam for Lane F.
- **Lane E `web/assets/manifest.json`** its `fittings` section (9 depot GLBs with footprints) is now
wired through `glb.js` (opt-in `useGLB`, primitive fallback). Depot GLBs may not be uploaded yet;
verified the fallback (unreachable depot primitives persist, no crash, no leak).
- **Lane D `citizens/keepers.js`** its `keepers.spawn(group,{x,z,ry,shopId,type})` now has a clean
source: `room.counter.stand` (also on `place.userData.keeperStand`). See the *Update (b)* section at
the top. Verified the pose is on the service side, faces the customer, and is deterministic.
## Adversarial review — 5 findings, all fixed
Ran a 5-dimension adversarial multi-agent review (determinism / disposal-leaks / path-guarantee /
three-api / edge-robustness) with a per-finding verify pass. 5 real defects surfaced; all fixed and
re-verified (830-build sweep + 50-room soak green after each fix):
| # | sev | file | defect | fix |
|---|---|---|---|---|
| 1 | med | interiors.js | per-build `opts.registry` permanently mutated the shared recipe table latent same-seed determinism break across differing registry states | removed per-build option; registry override is now a one-time global `mergeRegistry()` (Lane F setup) |
| 2 | low | interiors.js | explicit `null` opts threw (`opts = {}` default only covers `undefined`) | normalize `opts = opts \|\| {}` |
| 3 | low | shell.js | empty `archetypeBias` array `bias[0][0]` on `[]` throws | fall back to uniform bias when missing **or** empty |
| 4 | med | layout.js | fittings pulled by the path loop left phantom interactables in `places` (dangling after dispose) | collect `places` from survivors at the end, not incrementally at placement |
| 5 | low | layout.js | `rebuildOcc` re-stamped wall-mounted fittings that `placeAtWall` never stamped phantom floor obstacles after a blocker-pull | mark wall-mounted `noStamp`; skip in `rebuildOcc` **and** exclude from the removable pool |
Verify evidence (post-fix): `nullOpts ok · phantomPlaces 0/332 · determinismAcrossBuilds identical ·
emptyBias ok · sweep 810 {throws 0, pathFail 0, detFail 0} · soak {avg 4.2ms, worst 8ms, leakGeo 0,
leakTex 0}`. (Reviewers also confirmed no disposal/leak or three.js-contract defects.)
## Not done (out of scope for v1 / depends on other lanes)
- Real item data / economy (content phase `stockAdapter` hook is ready).
- GLB hero props are **wired** (`glb.js`, opt-in) but not visually validated end-to-end needs the
depot GLBs actually uploaded (Lane E) + `useGLB` flipped on by Lane B/F. Primitive path is the tested baseline.
- Interior-mapping shader for street windows (Lane B stretch goal).
- Back-room beyond the blocked doorway (v2).