PROCITY/C-progress.md
m3ultra 0e9d3fb0f2 Lane C: on-demand seeded shop interiors (library + test page)
Every shop door opens into a unique, believable, seeded interior — generated
in ~4ms, byte-identical every revisit (shop.seed), themed by shop.type.

- interiors.js: buildInterior(shop, THREE, opts) public API — pure fn of shop,
  returns {group, spawn, exits, places, dims, placement, dispose()}.
- theme.js: 9 CITY_SPEC type recipes (archetype/wallpaper/floor bias, clutter,
  counter pos, fittings mix, stock kind, signage) + type aliasing + one-time
  mergeRegistry() override seam for Lane F. Standalone (no hard registry dep).
- shell.js: room shell from lot x archetype (cosy/gallery/wide/hall/pokey),
  glazed shopfront + street backdrop, blocked back doorway, interior lighting.
- fittings.js: parametric kit ported from 90sDJsim + extended (bins, crates,
  4 shelf types, VHS aisle, glass case, counter+till, fridge, magazine/spinner
  racks, armchair, escalator, pegboard, barred window, returns slot, art).
- layout.js: per-archetype zones, thriftgod shuffled wall-slot system,
  occupancy grid, guaranteed door->counter flood-fill path (pull/carve).
- stock.js: v1 visual stock (pooled canvas sleeves/spines/boxes/garments/snacks
  with price stickers) + stockAdapter hook for BaseGod content later.
- context.js: seed sub-streams + shared-geometry cache + leak-free disposeAll().
- glb.js: optional GLB hero-prop upgrade via Lane E manifest (off by default,
  primitive fallback).
- interior_test.html: standalone page — seed/type/archetype, first-person walk,
  wireframe/occupancy/path debug, 50-room soak (perf + leak + determinism).

Acceptance (verified): same seed -> identical placement (0/810 mismatches);
9 types x 5 archetypes render sensibly (docs/shots/laneC grid); build <50ms
(steady ~4ms, soak worst 8ms); leak-free dispose (geo/tex delta 0); door->counter
path always exists (0 fails, 0 carves); runs with zero assets and zero network.
Adversarial multi-agent review: 5 findings, all fixed and re-verified.

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

11 KiB
Raw Blame History

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 public APIbuildInterior(shop, THREE, opts)
theme.js 9 shop-type recipes (archetype/wallpaper/floor bias, clutter, counter pos, fittings mix, stock kind, signage) + aliasing + registry-override hook
shell.js room shell: floor/walls/ceiling from lot×archetype, glazed shopfront + street backdrop, blocked back doorway, interior lighting
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 the placer: per-archetype zones, shuffled wall-slot system, occupancy grid, guaranteed door→counter flood-fill path
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 build/dispose/seed backbone: independent seeded sub-streams, shared-geometry cache, leak-free disposeAll()
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 standalone test page (below)

Public API (the contract Lane B/F build against)

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.24.5m.
  • opts: { archetype?: 'cosy'|'gallery'|'wide'|'hall'|'pokey', stockAdapter?, useGLB?, manifest?, manifestUrl? }. Registry override is a one-time globalimport { 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 ~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).

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).