Lane C R7 C1: ?stock=real — real GODVERSE sleeves in the crates
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>
This commit is contained in:
parent
43edabfe56
commit
3724a65505
@ -3,7 +3,32 @@
|
||||
*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 (round 6) · owner: PROCITY-C · reviewer: Fable
|
||||
Last updated: 2026-07-14 (round 7) · owner: PROCITY-C · reviewer: Fable
|
||||
|
||||
---
|
||||
|
||||
## Update 2026-07-14 (round 7) — `?stock=real`: real GODVERSE sleeves in the crates
|
||||
|
||||
Round-7 §Lane C: the `stockAdapter` seam built in v1 finally eats. Lane E's GODVERSE stock pack (parody-
|
||||
transformed Discogs metadata + cover atlas) feeds record-shop bins + the dig riffle behind `?stock=real`.
|
||||
- **Contract handshake** (task 1, unblocked E): confirmed E's index schema as-is in
|
||||
[LANE_C_NOTES.md](docs/LANES/LANE_C_NOTES.md) — `{ items:[{id,title,artist,price,price_band,atlas,uv}] }`,
|
||||
UV origin top-left (C flips V for WebGL), fail-soft, per-type packs.
|
||||
- **New `stockpack.js`**: `preloadStockPack`/`getStockPack` (cached) + `makeStockAdapter`. Atlas texture +
|
||||
material are shared/cached (like depot GLBs); sleeves are cover PLANEs sampling the atlas at each item's
|
||||
baked UV → all real sleeves in a room **batch to one draw** (record: 59 draws).
|
||||
- **Consumers**: `stock.js binFan` builds real cover planes (else parody canvas); `dig.js` riffles real
|
||||
covers and the pull card shows **title / artist / $price / band**; `interiors.js` wires `opts.stock='real'`
|
||||
through the seam (warn-once + parody fallback if the pack is absent); test page reads `?stock=real`.
|
||||
- **Validated (sample 24-pack)**: determinism 0 fails/30, leak-free rooms AND real dig (0/0 over 25 cycles —
|
||||
atlas shared, sleeve geo disposed), draws ≤139 all rooms (record 59), fail-soft on no-pack types (opshop
|
||||
ok → parody), `?dig=1&stock=real` leak-free. Flag-off + `?noassets` untouched (procedural path byte-identical).
|
||||
- Built against E's **staged sample** (real index format, synthetic covers); real Discogs covers drop in
|
||||
unchanged when E gets the DB (external — John/Fable). Shot: [stock_real_r7.jpg](docs/shots/laneC/stock_real_r7.jpg).
|
||||
|
||||
**→ F**: new smoke for your strict harness — `?stock=real` enter record shop + open dig → assert real
|
||||
(non-parody) sleeves; the record bin's real sleeves are cover planes with `userData.isStock` on a shared
|
||||
atlas material (drawSweep stays ≤350). Hook + adapter in LANE_C_NOTES.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -1,5 +1,64 @@
|
||||
# LANE C — cross-lane notes (PROCITY-C)
|
||||
|
||||
## → Lane F: `?stock=real` smoke for the strict harness (round-7 C1 landed)
|
||||
|
||||
`?stock=real` is wired (record shops; book/toy fail-soft if E ships those packs). Smoke recipe:
|
||||
- Preload once: `import { preloadStockPack } from interiors.js; await preloadStockPack('record')`.
|
||||
- Boot `?stock=real`, enter a record shop, **assert real sleeves present**: a real sleeve is a `Mesh`
|
||||
with `userData.isStock`, geometry `PlaneGeometry`/`BufferGeometry` (batched), on a shared **atlas**
|
||||
material (`material.map` = the pack atlas, not a per-item canvas). Simplest assert: after build,
|
||||
`let n=0; room.group.traverse(o=>{ if(o.userData?.isStock && o.material?.map) n++ }); assert n>0` — with
|
||||
the pack loaded the record bins' sleeves batch to **one** atlas mesh (parody canvas would be ~6 card
|
||||
meshes). Or open the dig (`?dig=1&stock=real`) and assert a pulled sleeve's panel title is a pack title.
|
||||
- **Fail-soft**: pack missing → parody canvas + one `console.warn` (no throw). `?noassets` unaffected.
|
||||
- **drawSweep still ≤350** with `?stock=real` (record 59 draws — the atlas batches all sleeves).
|
||||
|
||||
## → Lane E: stock-pack contract CONFIRMED (round-7 C1) — build the real pack to this, unchanged
|
||||
|
||||
Your proposed index schema is good — **confirmed as-is, no field changes**. Building `?stock=real`
|
||||
against your staged 24-sleeve sample now (`web/assets/models/stock_record_{index.json,atlas_00.webp}`);
|
||||
the real covers drop in unchanged. The contract C consumes:
|
||||
|
||||
```jsonc
|
||||
// stock_<type>_index.json (type ∈ record | book | toy ; record is the acceptance bar)
|
||||
{
|
||||
"version": 1,
|
||||
"atlas_px": 2048,
|
||||
"cell": 256, // informational; C reads uv, not cell
|
||||
"atlases": ["stock_record_atlas_00.webp", ...], // 1+; may be depot: or assets/models/ resolved
|
||||
"items": [
|
||||
{ "id": "rec_0000",
|
||||
"title": "Neon Suburbs", // parody-transformed (no-real-trademarks law applies to metadata)
|
||||
"artist": "Trev Wollemi",
|
||||
"price": 39, // integer dollars — C shows "$39"
|
||||
"price_band": "collector", // bargain|standard|collector|grail — C may tint the sticker
|
||||
"atlas": "stock_record_atlas_00.webp", // which entry in `atlases`
|
||||
"uv": [0.0, 0.0, 0.125, 0.125] } // [u0,v0,u1,v1] cover rect, ORIGIN TOP-LEFT
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Agreements / who-does-what:
|
||||
- **UV origin is top-left (image-natural) — C flips V for WebGL** (`v_gl = 1 - v`). Keep your index
|
||||
image-natural; the consumer handles the flip. (Sample verified against this.)
|
||||
- **Resolution**: C loads `stock_<type>_index.json` from a base (default `assets/models/`, i.e. your
|
||||
staged path — same-origin, works offline). Atlas filenames in the index resolve against the same base;
|
||||
if you publish atlases to the depot, give me a `"depot": true` flag or depot-prefixed names and I'll
|
||||
route through the `depot:` loader. For now the sample loads local.
|
||||
- **Seeded pick per bin is mine** (same `shop.seed` → same crate contents). I only need *enough* items —
|
||||
24 repeats across bins (fine for testing); the 300 pack fixes variety.
|
||||
- **Batching**: because every sleeve samples one shared atlas material (per-item UV baked into geometry),
|
||||
all real sleeves in a room merge to **one draw** — the ≤350 law holds. Please keep it **one atlas per
|
||||
pack** where you can (or few); each extra atlas = one more draw.
|
||||
- **Fail-soft**: index/atlas missing or `?stock` off → parody canvas (warn once). `?noassets` untouched.
|
||||
- **Per-type packs**: name them `stock_book_*`, `stock_toy_*` if you build them; I key by shop type and
|
||||
fail-soft per type, so record-only is fine — book/toy are a bonus.
|
||||
|
||||
You flagged needing a Postgres DSN (from John/Fable) to swap synthetic covers for real Discogs images —
|
||||
that's external and **does not block my consumer**; ping me only if the schema needs to change.
|
||||
|
||||
|
||||
|
||||
## → Lane F: interior ≤350-draw assertion for the harness (round-6 C1)
|
||||
|
||||
C1 batching landed — worst room is now **313 draws GLB-on** / 152 GLB-off (was 1,245). The test page
|
||||
|
||||
BIN
docs/shots/laneC/stock_real_r7.jpg
Normal file
BIN
docs/shots/laneC/stock_real_r7.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@ -69,9 +69,14 @@
|
||||
<script type="module">
|
||||
import * as THREE from 'three';
|
||||
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
|
||||
import { buildInterior, SHOP_TYPES, ARCHETYPE_KEYS } from './js/interiors/interiors.js';
|
||||
import { buildInterior, SHOP_TYPES, ARCHETYPE_KEYS, preloadStockPack, getStockPack, makeStockAdapter } from './js/interiors/interiors.js';
|
||||
import { createDig, binSeed } from './js/interiors/dig.js';
|
||||
|
||||
// ?stock=real — feed Lane E's GODVERSE record-sleeve pack through the stockAdapter seam.
|
||||
const STOCK_REAL = new URLSearchParams(location.search).get('stock') === 'real';
|
||||
if (STOCK_REAL) for (const t of ['record', 'book', 'toy']) preloadStockPack(t); // fail-soft per type
|
||||
const realAdapterFor = (type) => (STOCK_REAL ? makeStockAdapter(getStockPack(type)) : null);
|
||||
|
||||
// ── renderer / scene / camera ──────────────────────────────────────────────
|
||||
const app = document.getElementById('app');
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
@ -121,7 +126,7 @@ function rebuild() {
|
||||
const arch = archSel.value === 'auto' ? undefined : archSel.value;
|
||||
const lot = parseLot($('lot').value);
|
||||
const shop = { id: 's' + seed, type, name: type.toUpperCase() + ' ' + seed, seed, storeys: 1, lot };
|
||||
current = buildInterior(shop, THREE, { archetype: arch, useGLB, manifest: useGLB ? glbManifest : undefined });
|
||||
current = buildInterior(shop, THREE, { archetype: arch, useGLB, manifest: useGLB ? glbManifest : undefined, stock: STOCK_REAL ? 'real' : undefined });
|
||||
scene.add(current.group);
|
||||
// place the player at the spawn
|
||||
camera.position.set(current.spawn.x, 1.6, current.spawn.z);
|
||||
@ -342,8 +347,8 @@ function openDigOn(bin) {
|
||||
if (controls.isLocked) controls.unlock();
|
||||
const p = new THREE.Vector3(); bin.getWorldPosition(p);
|
||||
const key = Math.round(p.x * 100) + '_' + Math.round(p.z * 100); // stable per-bin key (deterministic position)
|
||||
dig.open({ seed: binSeed(curSeed, key), count: 16, shopName: current.recipe.label,
|
||||
getCash: () => 50, onBuy: () => true, onClose: () => {} });
|
||||
dig.open({ seed: binSeed(curSeed, key), count: 16, shopName: current.recipe.label, shop: { type: current.dims.type },
|
||||
stockAdapter: realAdapterFor(current.dims.type), getCash: () => 50, onBuy: () => true, onClose: () => {} });
|
||||
}
|
||||
if (DIG_ON) {
|
||||
addEventListener('keydown', e => { if (e.code === 'KeyE' && current && (!dig || !dig.active)) { const b = binUnderAim(); if (b) openDigOn(b); } });
|
||||
|
||||
@ -172,8 +172,16 @@ export function createDig(THREE, renderer) {
|
||||
let z = -0.05;
|
||||
list.forEach((o, i) => {
|
||||
const grp = new THREE.Group(); grp.position.set(0, 0, z);
|
||||
const face = new THREE.MeshStandardMaterial({ map: sleeveTexture(o), roughness: 0.75 }); oMat.add(face);
|
||||
const mesh = new THREE.Mesh(sleeveGeo, [sideMat, sideMat, sideMat, sideMat, face, sideMat]); // shared sleeve geo
|
||||
let mesh;
|
||||
if (o.real) { // ?stock=real: a cover PLANE sampling the pack atlas
|
||||
const g = new THREE.PlaneGeometry(w, h); oGeo.add(g);
|
||||
const [u0, v0, u1, v1] = o.pack.uvRect(o.item);
|
||||
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); a.needsUpdate = true;
|
||||
mesh = new THREE.Mesh(g, o.pack.material(o.item)); // shared atlas material (not disposed here)
|
||||
} else {
|
||||
const face = new THREE.MeshStandardMaterial({ map: sleeveTexture(o), roughness: 0.75 }); oMat.add(face);
|
||||
mesh = new THREE.Mesh(sleeveGeo, [sideMat, sideMat, sideMat, sideMat, face, sideMat]); // shared sleeve geo
|
||||
}
|
||||
mesh.position.y = h / 2;
|
||||
grp.add(mesh); grp.rotation.x = BACK;
|
||||
grp.rotation.z = (((i * 2654435761) >>> 0) % 100 - 50) / 1800; // each sleeve leans its own way (seeded by index)
|
||||
@ -258,13 +266,25 @@ export function createDig(THREE, renderer) {
|
||||
renderer.domElement.addEventListener('pointerup', onUp);
|
||||
document.addEventListener('keydown', onKey, true);
|
||||
|
||||
// ?stock=real: seeded pick of real pack items (same bin seed → same crate contents).
|
||||
function pickRealOffers(pack, seed, n) {
|
||||
const rr = mulberry32(seed >>> 0), out = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const it = pack.items[(rr() * pack.items.length) | 0];
|
||||
out.push({ real: true, pack, item: it, a: it.artist, t: it.title, price: it.price,
|
||||
s: (it.price_band || '').toUpperCase(), y: '', cond: '', rare: it.price_band === 'grail' });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// open({ seed, count, shopName, shop, stockAdapter, onClose, onBuy, getCash })
|
||||
function open(opts = {}) {
|
||||
active = true;
|
||||
onCloseCb = opts.onClose || null; onBuyCb = opts.onBuy || null; getCashCb = opts.getCash || null;
|
||||
const count = opts.count || 16;
|
||||
// riffle items: the stockAdapter seam (real records later) or the seeded procedural sleeves now.
|
||||
offers = (opts.stockAdapter && opts.stockAdapter(opts.shop, 'sleeve')) || proceduralSleeves(opts.seed ?? 1, count);
|
||||
// riffle items: real pack (?stock=real) via the stockAdapter seam, else seeded procedural sleeves.
|
||||
const src = opts.stockAdapter && opts.stockAdapter(opts.shop, 'sleeve');
|
||||
offers = (src && src.real) ? pickRealOffers(src.pack, opts.seed ?? 1, count) : proceduralSleeves(opts.seed ?? 1, count);
|
||||
curLbl.textContent = (opts.shopName ? opts.shopName + ' — ' : '');
|
||||
[hint, curLbl, outBtn].forEach(e => e.style.display = 'block');
|
||||
buildStack(offers);
|
||||
|
||||
@ -40,12 +40,16 @@ import { ARCHETYPE_KEYS, chooseArchetype, computeDims, buildShell } from './shel
|
||||
import { layout } from './layout.js';
|
||||
import { batchRoom } from './batch.js';
|
||||
import { preloadManifest, upgradeRoom } from './glb.js';
|
||||
import { getStockPack, makeStockAdapter } from './stockpack.js';
|
||||
import { xmur3 } from '../core/prng.js';
|
||||
|
||||
export { SHOP_TYPES, ARCHETYPE_KEYS, mergeRegistry };
|
||||
export { preloadManifest } from './glb.js'; // Lane F: preload once so GLB upgrades apply synchronously
|
||||
export { preloadStockPack, getStockPack, makeStockAdapter } from './stockpack.js'; // ?stock=real
|
||||
|
||||
let glbManifest = null; // resolved manifest cache (once the depot answers)
|
||||
const _warned = new Set();
|
||||
function warnOnce(msg) { if (!_warned.has(msg)) { _warned.add(msg); console.warn('[interiors] ' + msg); } }
|
||||
|
||||
function normalizeShop(shop) {
|
||||
const s = shop || {};
|
||||
@ -77,8 +81,15 @@ export function buildInterior(shop, THREE, opts) {
|
||||
const dims = computeDims(ctx, recipe, archetype, norm.lot, norm.storeys);
|
||||
const shell = buildShell(ctx, { recipe, archetype, dims });
|
||||
|
||||
// fittings + wall decor + stock + walkable-path guarantee
|
||||
const lay = layout(ctx, { recipe, dims, shell, adapter: opts.stockAdapter || null, shop: norm });
|
||||
// fittings + wall decor + stock + walkable-path guarantee. ?stock=real feeds Lane E's GODVERSE pack
|
||||
// through the stockAdapter seam (preload it first); missing pack → parody canvas (warn once).
|
||||
let adapter = opts.stockAdapter || null;
|
||||
if (!adapter && opts.stock === 'real') {
|
||||
const pack = getStockPack(recipe.key);
|
||||
if (pack) adapter = makeStockAdapter(pack);
|
||||
else warnOnce(`stock=real: no pack for "${recipe.key}" (preload it) — using parody canvas`);
|
||||
}
|
||||
const lay = layout(ctx, { recipe, dims, shell, adapter, shop: norm });
|
||||
|
||||
// DRAW BATCHING (round 6, ≤350 draws/room law): merge the many same-material stock/fixture meshes
|
||||
// inside each fitting group into a few merged geometries. Fitting groups (and their userData) are
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
// items) → few CanvasTextures to build and dispose, fast builds, soak returns to baseline.
|
||||
|
||||
import { pick } from './context.js';
|
||||
import { realSleeveMesh } from './stockpack.js';
|
||||
|
||||
// Muted 90s stock palette (cardboard, plastic, faded card).
|
||||
const CARD = ['#b49b72', '#a6b472', '#72a6b4', '#b47298', '#8a72b4', '#b48a72', '#72b48a', '#b4a672'];
|
||||
@ -108,21 +109,32 @@ function faceTex(ctx, kind, opts, r) {
|
||||
return pick(r, facePool(ctx, kind === 'magazine' ? 'card' : kind, r));
|
||||
}
|
||||
|
||||
// Packed leaning sleeves in a bin/crate; front few wear covers (dig.js look).
|
||||
// Packed leaning sleeves in a bin/crate; front few wear covers (dig.js look). With ?stock=real the
|
||||
// adapter hands back a pack pool → every sleeve is a real cover PLANE sampling the shared atlas (batches
|
||||
// to one draw); otherwise procedural cardboard.
|
||||
function binFan(ctx, parent, slot, opts, r) {
|
||||
const n = slot.count || 12;
|
||||
const fw = slot.faceW || 0.3, h = slot.height || 0.3, run = slot.run || 0.5, tilt = slot.tilt ?? -0.4;
|
||||
const step = run / Math.max(1, n);
|
||||
const pool = facePool(ctx, 'card', r);
|
||||
const src = opts.adapter && opts.adapter(opts.shop, 'sleeve');
|
||||
const pack = src && src.real ? src.pack : null;
|
||||
const pool = pack ? null : facePool(ctx, 'card', r);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const z = -run / 2 + (i + 0.5) * step;
|
||||
const front = i >= n - 4; // front sleeves show a cover
|
||||
const faceMat = front ? ctx.mat('#ffffff', 0.7) : ctx.mat(pick(r, CARD), 0.85);
|
||||
if (front) { faceMat.map = pick(r, pool); faceMat.color.set('#ffffff'); faceMat.needsUpdate = true; }
|
||||
const sl = ctx.box(fw, h, 0.012, faceMat, slot.x + (r() - 0.5) * 0.02, slot.y + h / 2, slot.z + z, parent);
|
||||
let sl;
|
||||
if (pack) {
|
||||
sl = realSleeveMesh(ctx, pack, pick(r, pack.items), fw, h); // isStock + shared atlas material
|
||||
sl.position.set(slot.x + (r() - 0.5) * 0.02, slot.y + h / 2, slot.z + z);
|
||||
parent.add(sl);
|
||||
} else {
|
||||
const front = i >= n - 4; // front sleeves show a cover
|
||||
const faceMat = front ? ctx.mat('#ffffff', 0.7) : ctx.mat(pick(r, CARD), 0.85);
|
||||
if (front) { faceMat.map = pick(r, pool); faceMat.color.set('#ffffff'); faceMat.needsUpdate = true; }
|
||||
sl = ctx.box(fw, h, 0.012, faceMat, slot.x + (r() - 0.5) * 0.02, slot.y + h / 2, slot.z + z, parent);
|
||||
sl.userData.isStock = true;
|
||||
}
|
||||
sl.rotation.x = tilt + (r() - 0.5) * 0.05;
|
||||
sl.rotation.z = (r() - 0.5) * 0.04;
|
||||
sl.userData.isStock = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
76
web/js/interiors/stockpack.js
Normal file
76
web/js/interiors/stockpack.js
Normal file
@ -0,0 +1,76 @@
|
||||
// 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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user