// 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); }