The stockAdapter seam built in v1 finally eats. Lane E's stock pack (parody-
transformed Discogs metadata + cover atlas) feeds record-shop bins + the dig
riffle behind ?stock=real. Economy stays a stub (buying = toast).
- Contract handshake (task 1, unblocked E): confirmed E's index schema as-is
in LANE_C_NOTES (items[{id,title,artist,price,price_band,atlas,uv}]; uv
origin top-left, C flips V for WebGL; fail-soft; per-type packs).
- stockpack.js (new): preloadStockPack/getStockPack (cached) + makeStockAdapter.
Atlas texture + material are shared/cached (like depot GLBs); each sleeve is
a cover PLANE sampling the atlas at its baked UV, so all real sleeves in a
room batch to ONE draw (record: 59 draws).
- stock.js binFan: real cover planes (else parody canvas). dig.js: riffles real
covers, pull card shows title/artist/$price/band. interiors.js: opts.stock=
'real' wires the adapter (warn-once + parody fallback if pack absent). Test
page reads ?stock=real, preloads record/book/toy, passes the adapter to the dig.
Validated (E's staged 24-sample; real covers drop in unchanged when E gets the
DB): determinism 0/30, leak-free rooms + real dig (0/0 over 25 cycles), draws
<=139 all rooms (record 59), fail-soft on no-pack types, ?dig=1&stock=real
leak-free. Flag-off + ?noassets untouched (procedural path byte-identical).
qa.sh --strict GREEN. F smoke hook + E contract in LANE_C_NOTES. Shot
docs/shots/laneC/stock_real_r7.jpg.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
77 lines
3.9 KiB
JavaScript
77 lines
3.9 KiB
JavaScript
// 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_<type>_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<pack|null>
|
|
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;
|
|
}
|