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
|
*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.*
|
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 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)
|
## → 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
|
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">
|
<script type="module">
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
|
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';
|
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 ──────────────────────────────────────────────
|
// ── renderer / scene / camera ──────────────────────────────────────────────
|
||||||
const app = document.getElementById('app');
|
const app = document.getElementById('app');
|
||||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
@ -121,7 +126,7 @@ function rebuild() {
|
|||||||
const arch = archSel.value === 'auto' ? undefined : archSel.value;
|
const arch = archSel.value === 'auto' ? undefined : archSel.value;
|
||||||
const lot = parseLot($('lot').value);
|
const lot = parseLot($('lot').value);
|
||||||
const shop = { id: 's' + seed, type, name: type.toUpperCase() + ' ' + seed, seed, storeys: 1, lot };
|
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);
|
scene.add(current.group);
|
||||||
// place the player at the spawn
|
// place the player at the spawn
|
||||||
camera.position.set(current.spawn.x, 1.6, current.spawn.z);
|
camera.position.set(current.spawn.x, 1.6, current.spawn.z);
|
||||||
@ -342,8 +347,8 @@ function openDigOn(bin) {
|
|||||||
if (controls.isLocked) controls.unlock();
|
if (controls.isLocked) controls.unlock();
|
||||||
const p = new THREE.Vector3(); bin.getWorldPosition(p);
|
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)
|
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,
|
dig.open({ seed: binSeed(curSeed, key), count: 16, shopName: current.recipe.label, shop: { type: current.dims.type },
|
||||||
getCash: () => 50, onBuy: () => true, onClose: () => {} });
|
stockAdapter: realAdapterFor(current.dims.type), getCash: () => 50, onBuy: () => true, onClose: () => {} });
|
||||||
}
|
}
|
||||||
if (DIG_ON) {
|
if (DIG_ON) {
|
||||||
addEventListener('keydown', e => { if (e.code === 'KeyE' && current && (!dig || !dig.active)) { const b = binUnderAim(); if (b) openDigOn(b); } });
|
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;
|
let z = -0.05;
|
||||||
list.forEach((o, i) => {
|
list.forEach((o, i) => {
|
||||||
const grp = new THREE.Group(); grp.position.set(0, 0, z);
|
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);
|
let mesh;
|
||||||
const mesh = new THREE.Mesh(sleeveGeo, [sideMat, sideMat, sideMat, sideMat, face, sideMat]); // shared sleeve geo
|
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;
|
mesh.position.y = h / 2;
|
||||||
grp.add(mesh); grp.rotation.x = BACK;
|
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)
|
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);
|
renderer.domElement.addEventListener('pointerup', onUp);
|
||||||
document.addEventListener('keydown', onKey, true);
|
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 })
|
// open({ seed, count, shopName, shop, stockAdapter, onClose, onBuy, getCash })
|
||||||
function open(opts = {}) {
|
function open(opts = {}) {
|
||||||
active = true;
|
active = true;
|
||||||
onCloseCb = opts.onClose || null; onBuyCb = opts.onBuy || null; getCashCb = opts.getCash || null;
|
onCloseCb = opts.onClose || null; onBuyCb = opts.onBuy || null; getCashCb = opts.getCash || null;
|
||||||
const count = opts.count || 16;
|
const count = opts.count || 16;
|
||||||
// riffle items: the stockAdapter seam (real records later) or the seeded procedural sleeves now.
|
// riffle items: real pack (?stock=real) via the stockAdapter seam, else seeded procedural sleeves.
|
||||||
offers = (opts.stockAdapter && opts.stockAdapter(opts.shop, 'sleeve')) || proceduralSleeves(opts.seed ?? 1, count);
|
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 + ' — ' : '');
|
curLbl.textContent = (opts.shopName ? opts.shopName + ' — ' : '');
|
||||||
[hint, curLbl, outBtn].forEach(e => e.style.display = 'block');
|
[hint, curLbl, outBtn].forEach(e => e.style.display = 'block');
|
||||||
buildStack(offers);
|
buildStack(offers);
|
||||||
|
|||||||
@ -40,12 +40,16 @@ import { ARCHETYPE_KEYS, chooseArchetype, computeDims, buildShell } from './shel
|
|||||||
import { layout } from './layout.js';
|
import { layout } from './layout.js';
|
||||||
import { batchRoom } from './batch.js';
|
import { batchRoom } from './batch.js';
|
||||||
import { preloadManifest, upgradeRoom } from './glb.js';
|
import { preloadManifest, upgradeRoom } from './glb.js';
|
||||||
|
import { getStockPack, makeStockAdapter } from './stockpack.js';
|
||||||
import { xmur3 } from '../core/prng.js';
|
import { xmur3 } from '../core/prng.js';
|
||||||
|
|
||||||
export { SHOP_TYPES, ARCHETYPE_KEYS, mergeRegistry };
|
export { SHOP_TYPES, ARCHETYPE_KEYS, mergeRegistry };
|
||||||
export { preloadManifest } from './glb.js'; // Lane F: preload once so GLB upgrades apply synchronously
|
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)
|
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) {
|
function normalizeShop(shop) {
|
||||||
const s = 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 dims = computeDims(ctx, recipe, archetype, norm.lot, norm.storeys);
|
||||||
const shell = buildShell(ctx, { recipe, archetype, dims });
|
const shell = buildShell(ctx, { recipe, archetype, dims });
|
||||||
|
|
||||||
// fittings + wall decor + stock + walkable-path guarantee
|
// fittings + wall decor + stock + walkable-path guarantee. ?stock=real feeds Lane E's GODVERSE pack
|
||||||
const lay = layout(ctx, { recipe, dims, shell, adapter: opts.stockAdapter || null, shop: norm });
|
// 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
|
// 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
|
// 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.
|
// items) → few CanvasTextures to build and dispose, fast builds, soak returns to baseline.
|
||||||
|
|
||||||
import { pick } from './context.js';
|
import { pick } from './context.js';
|
||||||
|
import { realSleeveMesh } from './stockpack.js';
|
||||||
|
|
||||||
// Muted 90s stock palette (cardboard, plastic, faded card).
|
// Muted 90s stock palette (cardboard, plastic, faded card).
|
||||||
const CARD = ['#b49b72', '#a6b472', '#72a6b4', '#b47298', '#8a72b4', '#b48a72', '#72b48a', '#b4a672'];
|
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));
|
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) {
|
function binFan(ctx, parent, slot, opts, r) {
|
||||||
const n = slot.count || 12;
|
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 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 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++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const z = -run / 2 + (i + 0.5) * step;
|
const z = -run / 2 + (i + 0.5) * step;
|
||||||
const front = i >= n - 4; // front sleeves show a cover
|
let sl;
|
||||||
const faceMat = front ? ctx.mat('#ffffff', 0.7) : ctx.mat(pick(r, CARD), 0.85);
|
if (pack) {
|
||||||
if (front) { faceMat.map = pick(r, pool); faceMat.color.set('#ffffff'); faceMat.needsUpdate = true; }
|
sl = realSleeveMesh(ctx, pack, pick(r, pack.items), fw, h); // isStock + shared atlas material
|
||||||
const sl = ctx.box(fw, h, 0.012, faceMat, slot.x + (r() - 0.5) * 0.02, slot.y + h / 2, slot.z + z, parent);
|
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.x = tilt + (r() - 0.5) * 0.05;
|
||||||
sl.rotation.z = (r() - 0.5) * 0.04;
|
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