# 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 · owner: PROCITY-C · reviewer: Fable --- ## 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 // 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.2–4.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 door→counter 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 ~80–110 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). ## 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).