// PROCITY Lane C — real record sleeves for `?stock=real` (round 7). Feeds Lane E's GODVERSE stock pack // (parody-transformed Discogs metadata + cover atlases) into the interior stock via the stockAdapter // seam built in v1. Read-only, no economy (buying stays a stub — that's a later round). // // Pack contract (agreed in LANE_C_NOTES): stock__index.json → // { version, atlas_px, cell, atlases:["…webp"], items:[{ id,title,artist,price,price_band,atlas,uv }] } // uv = [u0,v0,u1,v1] cover rect, ORIGIN TOP-LEFT (image-natural) — this module flips V for WebGL. // // Batching: every sleeve samples ONE shared atlas material with a per-item UV baked into its geometry, // so all real sleeves in a room merge (batch.js) to one draw per atlas — the ≤350 law holds. // Leak-free: the atlas texture + material are shared/cached in the pack (NOT per-room), like depot GLBs; // per-sleeve geometries are ctx-tracked and disposed with the room. // Fail-soft: index/atlas missing → getStockPack returns null → callers fall back to parody canvas. import * as THREE_NS from 'three'; const _texLoader = new THREE_NS.TextureLoader(); const _packs = new Map(); // type → Promise const _resolved = new Map(); // type → pack|null (sync accessor for buildInterior) // Preload a pack (fetch index + atlas textures). Cached. Call once before building with ?stock=real. export function preloadStockPack(type, { base = 'assets/models/', THREE = THREE_NS } = {}) { if (_packs.has(type)) return _packs.get(type); const p = fetch(`${base}stock_${type}_index.json`) .then(r => (r.ok ? r.json() : null)) .then(idx => (idx && idx.items && idx.items.length ? buildPack(type, idx, base, THREE) : null)) .catch(() => null) .then(pack => { _resolved.set(type, pack); return pack; }); _packs.set(type, p); return p; } // Synchronous accessor — the resolved pack if preloaded, else null (buildInterior uses this). export function getStockPack(type) { return _resolved.get(type) || null; } async function buildPack(type, idx, base, THREE) { const names = idx.atlases || []; const materials = {}; await Promise.all(names.map(async (name) => { const url = /^depot:|^https?:/.test(name) ? name : base + name; let tex; try { tex = await _texLoader.loadAsync(url); } catch (e) { tex = null; } if (tex) { tex.colorSpace = THREE.SRGBColorSpace; tex.anisotropy = 4; } materials[name] = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.72, map: tex || null }); })); const first = names[0]; return { type, items: idx.items, // shared material for an item's atlas (batching groups sleeves by this) material(item) { return materials[item.atlas] || materials[first]; }, // WebGL-flipped cover rect [u0, v0, u1, v1] (origin bottom-left) for a plane's UV uvRect(item) { const u = item.uv; return [u[0], 1 - u[3], u[2], 1 - u[1]]; }, }; } // The stockAdapter for ?stock=real. Returns the pack POOL for a sleeve slot; consumers (stock.binFan / // dig.js) pick a seeded subset per bin. Non-sleeve kinds → null (fall through to procedural). export function makeStockAdapter(pack) { if (!pack || !pack.items || !pack.items.length) return null; return (shop, slotKind) => (slotKind === 'sleeve' ? { real: true, pack } : null); } // Build a leaning cover PLANE for one pack item, UV-remapped to its atlas rect, tagged isStock so // batch.js merges it. Shared material (per atlas) → the whole bin batches to one draw. export function realSleeveMesh(ctx, pack, item, w, h) { const THREE = ctx.THREE; const g = ctx.geom(new THREE.PlaneGeometry(w, h)); const [u0, v0, u1, v1] = pack.uvRect(item); // already WebGL-flipped const a = g.attributes.uv; a.setXY(0, u0, v1); a.setXY(1, u1, v1); a.setXY(2, u0, v0); a.setXY(3, u1, v0); // TL,TR,BL,BR a.needsUpdate = true; const mesh = new THREE.Mesh(g, pack.material(item)); mesh.userData.isStock = true; return mesh; }