PROCITY/web/js/interiors/glb.js
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

67 lines
3.6 KiB
JavaScript
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.

// PROCITY Lane C — optional GLB-upgrade layer. Reads Lane E's web/assets/manifest.json and swaps a
// primitive fitting for a detailed depot GLB where one exists ("reserve the detailed GLB hero props
// for where the camera gets close" — RESEARCH). Strictly ADDITIVE and OFF by default:
// • the whole lane runs 100% on primitives with no manifest and no network (LANE_C acceptance);
// • enable per build with opts.useGLB (interiors.js), or globally via preloadManifest().
//
// Pattern (house law): promise-cached loader, PLACEHOLDER-PERSISTS — the primitive shows immediately
// and stays until the GLB resolves; if the depot is unreachable the primitive stays forever (never a
// crash, never a blank). The GLB is footprint-fitted to the primitive it replaces, so occupancy/paths
// already computed by layout.js stay valid.
import { loadGLB } from '../core/loaders.js';
import { clone as skeletonClone } from 'three/addons/utils/SkeletonUtils.js';
// my fitting kind → manifest.fittings id (only kinds with a depot GLB; others stay primitive)
const KIND_TO_GLB = {
crate: 'record_crate', recordBin: 'record_crate',
metalShelf: 'wire_shelf', wallShelf: 'wire_shelf',
clothesRack: 'clothes_rack',
bookshelf: 'bookshelf',
cubeShelf: 'cube_shelf_wide',
counter: 'counter',
trestleTable: 'work_table',
};
let _manifestP = null;
// Fetch + cache the manifest. Returns null (not throw) if absent/unreachable → primitives everywhere.
export function preloadManifest(url = 'assets/manifest.json') {
return (_manifestP ||= fetch(url).then(r => (r.ok ? r.json() : null)).catch(() => null));
}
export function manifestReady() { return _manifestP && typeof _manifestP.then === 'function' ? _manifestP : Promise.resolve(null); }
// Upgrade one placed fitting in-place, if the manifest maps its kind. No-op when disabled/unavailable.
// `manifest` is the resolved object (or null). ctx is the room build context (for the _disposed guard).
export function upgradeFitting(ctx, fitting, kind, manifest) {
if (!manifest || !manifest.fittings) return;
const id = KIND_TO_GLB[kind];
const entry = id && manifest.fittings[id];
if (!entry || !entry.file) return;
loadGLB(`depot:${entry.file}`).then(gltf => {
if (!gltf || ctx._disposed || !fitting.group.parent) return; // room rebuilt / disposed mid-flight
const THREE = ctx.THREE;
const inst = skeletonClone(gltf.scene); // clone-safe even if skinned
// fit the GLB to the primitive's footprint (GLB convention: origin at base, facing Z, metres)
const bb = new THREE.Box3().setFromObject(inst);
const size = new THREE.Vector3(); bb.getSize(size);
const want = fitting.footprint, targetW = Math.max(want.w, want.d);
const glbW = Math.max(size.x, size.z) || 1;
const s = targetW / glbW;
inst.scale.setScalar(s);
inst.position.y -= bb.min.y * s; // plant on the floor
// hide the primitive FRAME (the first frameCount children) but keep procedural stock — which was
// added after the frame — visible on top of the detailed GLB shelf/counter.
const frameCount = fitting.frameCount ?? fitting.group.children.length;
for (let i = 0; i < frameCount && i < fitting.group.children.length; i++) fitting.group.children[i].visible = false;
inst.userData = { glbUpgrade: true };
fitting.group.add(inst);
});
}
// Upgrade every placed fitting in a room. Called from buildInterior when GLB mode is on.
export function upgradeRoom(ctx, placed, manifest) {
if (!manifest) return;
for (const p of placed) if (!p.removed) upgradeFitting(ctx, p.fitting, p.kind, manifest);
}