diff --git a/C-progress.md b/C-progress.md index 4f1bce3..43970b1 100644 --- a/C-progress.md +++ b/C-progress.md @@ -3,7 +3,34 @@ *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 3) · owner: PROCITY-C · reviewer: Fable +Last updated: 2026-07-14 (round 4) · owner: PROCITY-C · reviewer: Fable + +--- + +## Update 2026-07-14 (round 4) — hero props into rooms + +Round-4 task (ROUND4_INSTRUCTIONS §Lane C): map the 4 new hero props + cash_register (manifest now 14 +fittings), sweep/soak with them on, screenshot each, delete the stale `_local_glb/`, measure a hero +room. **All done.** Full report for Lane E: [LANE_C_GLB_VALIDATION.md](docs/LANES/LANE_C_GLB_VALIDATION.md); +contact sheet [docs/shots/laneC/glb_heroprops_r4.jpg](docs/shots/laneC/glb_heroprops_r4.jpg). + +- **New mappings** (`glb.js`): `arcade_cabinet`→new `arcadeCabinet` primitive (video focal prop), + `listening_booth`→`listeningCorner` (record), `drinks_fridge`→`fridge` (milkbar). All load, `glb_law` + compliant, yaw 0 (no correction). A new **counter-top attach** (`attachCounterTop`) puts `cash_register` + on every counter's till slot (hides the primitive till) and `milkshake_mixer` on the milk-bar benchtop — + the counter primitive + keeper stand are untouched. +- **Focal-prop reliability fix**: the door-counter path loop was pulling priority-4 focal props (arcade + appeared in only 5/24 video rooms). Raised the 3 focal props to priority 6 + placed-first → + **24/24 appearance**, still 0 carves / 0 path-fails. +- **Validation**: primitive sweep 810 builds `{throws 0, pathFail 0, detFail 0}`; GLB-on soak + `{throws 0, path 0, det 0, leakGeo 0, leakTex 0, worst 7.3ms}`; all 5 new props confirmed in-room. +- **Room tris measured** (task 4): milk-bar GLB-on ≈ **468k tris** (over budget) driven by `drinks_fridge` + (77k) + `milkshake_mixer` (73k) — **reported to Lane E** as the #1 bake-pass target (not fixed, per + integrator decision #2; interiors render one-at-a-time so gate 3 / the street budget is unaffected). +- **Cleanup**: deleted `web/assets/_local_glb/` (R3 validation copy); test page now loads GLBs via the + live `depot:` path (`?localdepot=1` still works for offline). `localBase` hook kept in `glb.js`. +- **Standby (C task 5)**: when Lane E re-exports `record_crate` (broken) + a ~2 m counter-with-till, I + re-map both. Not landed at commit time. --- diff --git a/docs/LANES/LANE_C_GLB_VALIDATION.md b/docs/LANES/LANE_C_GLB_VALIDATION.md index 096631b..cc84a0e 100644 --- a/docs/LANES/LANE_C_GLB_VALIDATION.md +++ b/docs/LANES/LANE_C_GLB_VALIDATION.md @@ -1,4 +1,59 @@ -# LANE C — GLB validation report (round 3) → for Lane E + Fable +# LANE C — GLB validation report → for Lane E + Fable + +## ROUND 4 update (2026-07-14) — the 4 new hero props + cash_register + +Mapped and validated the 5 round-4 hero GLBs (manifest now 14 fittings). Loaded via the **live +`depot:` path** (all published, HTTP 200); the R3 `_local_glb/` copy is deleted. Screenshot evidence: +[`docs/shots/laneC/glb_heroprops_r4.jpg`](../shots/laneC/glb_heroprops_r4.jpg). + +**All 5 load cleanly** (unlike the still-broken `record_crate`), follow `glb_law` (minY≈0, real metres), +and match their manifest footprints — **no per-kind yaw correction needed (yaw 0)**: + +| id / file | native x·y·z (m) | tris | maps to | placement | +|---|---|---|---|---| +| `arcade_cabinet` | 0.75·1.70·0.85 | 25,142 | **new** `arcadeCabinet` primitive (video recipe) | floor, aisle zone, focal | +| `listening_booth` | 0.88·1.60·0.71 | 53,502 | `listeningCorner` (record) | floor, window zone, focal | +| `drinks_fridge` | 0.85·1.60·0.96 | 77,047 | `fridge` (milkbar) | floor, against wall | +| `milkshake_mixer` | 0.38·0.50·0.27 | 73,492 | counter-top (milkbar) | ON counter benchtop (left slot) | +| `cash_register` | 0.42·0.35·0.34 | 5,999 | counter-top (all types) | ON counter benchtop (till slot); hides primitive till | + +New mechanism: **counter-top GLB attach** (`glb.js attachCounterTop`) — cash_register replaces the +primitive till on every counter; milkshake_mixer sits on the milk-bar benchtop. The counter primitive +(correct height + keeper stand) is kept; only the till mesh is hidden. Focal props made reliable +(priority 6 + placed first): **24/24 appearance** across seeds, 0 carves, 0 path-fails. + +**Sweep + soak with all R4 mappings on**: `throws 0 · pathFail 0 · determinismFail 0 · leakGeo 0 · +leakTex 0 · worst build 7.3 ms`. GLB clones share cached geometry (SkeletonUtils.clone) → per-room +dispose is leak-free (baseline taken after a full 9-type warmup). + +### ⚠ Interior tris — one room over budget (task 4 report, NOT fixed per integrator decision #2) + +Per-room triangles, single interior rendered alone (primitives vs GLB-on), seed 1990: + +| room | prims tris | GLB-on tris | GLB-on draws | note | +|---|---|---|---|---| +| pawn | 812 | 7,019 | 161 | fine | +| opshop | 2,476 | 58,230 | 706 | fine | +| video | 5,712 | 61,669 | 461 | arcade 25k dominates — fine | +| book | 6,372 | 102,998 | 1,249 | high **draws** (≈600 primitive spines) | +| record | 2,530 | 115,867 | 337 | listening_booth 53k — fine | +| **milkbar** | 1,662 | **468,106** | 578 | **OVER** — `drinks_fridge` (77k) ×up-to-3 + `milkshake_mixer` (73k) | + +**→ Lane E bake-pass priority (future, not v1):** `drinks_fridge` (77k) and `milkshake_mixer` (73k) are +grossly over-tessellated for their visual role (a fridge and a bench appliance). Baking these two to +~5–10k would drop the milk-bar room from ~468k to well under 150k. `cash_register` (6k) and +`arcade_cabinet` (25k) are already reasonable. Interiors render one-at-a-time (Lane F swaps scenes, the +street is not drawn behind them), so this does **not** affect gate 3 (street budget) or the v1 tag — but +it's the #1 candidate for the bake pass. Not blocking; reporting per task 4. + +### Still open (Lane E) +- `record_crate` — STILL fails to load (`Cannot read ...'uri'`); record bins/crates stay primitive. Re-export. +- `counter` — still the 4 m till-less asset; interiors keep the primitive counter. A ~2 m counter-with-till + would let me drop cash_register's separate attach and map `counter` directly. When both land I re-map (C task 5). + +--- + +## ROUND 3 report (superseded above where they overlap) → for Lane E + Fable *PROCITY-C, 2026-07-14. Round-3 task "GLB path visual validation" (ROUND3_INSTRUCTIONS §Lane C). Validated Lane E's 9 fitting GLBs from `pipeline/_normalized/` in real interior rooms via the diff --git a/docs/shots/laneC/glb_heroprops_r4.jpg b/docs/shots/laneC/glb_heroprops_r4.jpg new file mode 100644 index 0000000..41564d8 Binary files /dev/null and b/docs/shots/laneC/glb_heroprops_r4.jpg differ diff --git a/web/interior_test.html b/web/interior_test.html index 482cc37..27db6d3 100644 --- a/web/interior_test.html +++ b/web/interior_test.html @@ -101,9 +101,9 @@ for (const a of ARCHETYPE_KEYS) archSel.appendChild(new Option(a, a)); let current = null; // the live interior let gridHelper = null, pathHelper = null; let wire = false, showGrid = false, showPath = false, useGLB = false; -let glbManifest = null; // Lane E manifest + localBase → validate GLB props vs locally-served copies -fetch('assets/manifest.json').then(r => r.ok ? r.json() : null) - .then(m => { if (m) { m.localBase = 'assets/_local_glb/'; glbManifest = m; } }).catch(() => {}); +let glbManifest = null; // Lane E manifest → GLB props load via the live depot: path (all 22 GLBs published). +fetch('assets/manifest.json').then(r => r.ok ? r.json() : null) // for local-only testing add ?localdepot=1 to the URL + .then(m => { if (m) glbManifest = m; }).catch(() => {}); function parseLot(v) { const m = String(v).match(/(\d+(?:\.\d+)?)\s*[x×,\s]\s*(\d+(?:\.\d+)?)/); diff --git a/web/js/interiors/fittings.js b/web/js/interiors/fittings.js index 381bef8..b0d9845 100644 --- a/web/js/interiors/fittings.js +++ b/web/js/interiors/fittings.js @@ -167,6 +167,27 @@ function vhsAisle(ctx, o = {}, r) { return { group: g, footprint: { w, d }, height: h, slots, places: [] }; } +// Upright arcade cabinet — the video-store focal prop (GLB `arcade_cabinet` upgrades it). +// Built facing +Z (screen toward the room), footprint/height matched to the manifest so the GLB drops in. +function arcadeCabinet(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + const w = o.w || 0.72, h = o.h || 1.7, d = o.d || 0.82; + const body = ctx.mat(['#7a2a3a', '#2a3a7a', '#2a6a4a', '#6a3a2a'][(r() * 4) | 0], 0.6); + const black = MAT.black(ctx); + ctx.box(w, h, d * 0.55, body, 0, h / 2, -d * 0.2, g); // tall body (set back) + ctx.box(w, h * 0.5, d * 0.45, body, 0, h * 0.25, d * 0.05, g); // lower front (kickplate) + ctx.box(w + 0.04, 0.2, 0.14, ctx.mat('#f0e070', 0.4, { emissive: new ctx.THREE.Color('#403a10') }), 0, h - 0.1, 0.02, g); // lit marquee + const scr = ctx.box(w - 0.14, 0.52, 0.04, ctx.mat('#0a0e14', 0.25, { emissive: new ctx.THREE.Color('#0c1626') }), 0, h * 0.7, 0.16, g); + scr.rotation.x = 0.22; // slanted dark screen, faces +Z + const cp = ctx.box(w - 0.06, 0.34, 0.05, black, 0, h * 0.46, d * 0.22, g); cp.rotation.x = -1.0; // control panel + ctx.cyl(0.018, 0.13, MAT.chrome(ctx), -0.15, h * 0.5, d * 0.24, g); + ctx.cyl(0.018, 0.13, MAT.chrome(ctx), 0.15, h * 0.5, d * 0.24, g); + [['#e04a3a', -0.02], ['#f0b428', 0.05], ['#4ac04a', 0.12]].forEach(([c, bx]) => ctx.cyl(0.02, 0.03, ctx.mat(c, 0.4), bx, h * 0.51, d * 0.27, g)); + ctx.box(0.22, 0.16, 0.02, ctx.mat('#c8a13a', 0.4), 0, h * 0.14, d * 0.28, g); // coin door + g.userData = { kind: 'prop', interactable: true }; + return { group: g, footprint: { w, d }, height: h, slots: [], places: [g] }; +} + // Spinner rack — rotating wire stand (paperbacks / postcards). function spinnerRack(ctx, o = {}, r) { const g = new ctx.THREE.Group(); @@ -222,12 +243,17 @@ function counter(ctx, o = {}, r) { const wood = MAT.wood(ctx), dark = MAT.darkwood(ctx); const body = ctx.box(w, h, d, wood, 0, h / 2, 0, g); ctx.box(w + 0.1, 0.06, d + 0.1, dark, 0, h, 0, g); // benchtop - ctx.box(0.34, 0.22, 0.28, MAT.black(ctx), w / 2 - 0.35, h + 0.15, 0, g); // till + const till = ctx.box(0.34, 0.22, 0.28, MAT.black(ctx), w / 2 - 0.35, h + 0.15, 0, g); // till const scr = ctx.plane(0.14, 0.1, ctx.mat('#0a1a0a', 0.4, { emissive: new ctx.THREE.Color('#123') }), g); scr.position.set(w / 2 - 0.35, h + 0.28, 0.14); scr.rotation.x = -0.5; + till.userData = { tillPrimitive: true }; // glb.js hides these when cash_register lands + scr.userData = { tillPrimitive: true }; body.userData = { kind: 'counter', interactable: true }; g.userData = { kind: 'counter', interactable: true }; - return { group: g, footprint: { w: w + 0.1, d: d + 0.1 }, height: h + 0.3, slots: [], places: [g] }; + // Counter-top attach points (local space, on the benchtop top surface) for GLB counter-top props + // (cash_register on the till spot; milkshake_mixer on the free left spot). y = benchtop top. + const counterTop = { y: h + 0.03, till: { x: w / 2 - 0.35, z: 0 }, appliance: { x: -w / 2 + 0.4, z: -0.02 } }; + return { group: g, footprint: { w: w + 0.1, d: d + 0.1 }, height: h + 0.3, slots: [], places: [g], counterTop }; } // Glass display case — timber base + tinted glass top, a few "treasures" inside. @@ -352,7 +378,7 @@ function artFrame(ctx, o = {}, r) { export const FITTINGS = { recordBin, crate, listeningCorner, - wallShelf, metalShelf, cubeShelf, bookshelf, vhsAisle, spinnerRack, + wallShelf, metalShelf, cubeShelf, bookshelf, vhsAisle, spinnerRack, arcadeCabinet, clothesRack, trestleTable, counter, glassCase, pegboard, fridge, magazineRack, armchair, barredWindow, returnsSlot, escalator, artFrame, diff --git a/web/js/interiors/glb.js b/web/js/interiors/glb.js index 419f208..c5660d7 100644 --- a/web/js/interiors/glb.js +++ b/web/js/interiors/glb.js @@ -24,6 +24,10 @@ const KIND_TO_GLB = { bookshelf: 'bookshelf', cubeShelf: 'cube_shelf_wide', trestleTable: 'work_table', + // Round-4 hero props (manifest now 14 fittings): + listeningCorner: 'listening_booth', // record store focal prop + fridge: 'drinks_fridge', // milk bar drinks fridge + arcadeCabinet: 'arcade_cabinet', // video store focal prop }; // Per-kind facing correction (radians, about +Y). The GLBs follow glb_law (metres, +Y up, base at origin) @@ -34,8 +38,20 @@ const KIND_TO_YAW = { bookshelf: Math.PI / 2, cubeShelf: Math.PI / 2, trestleTable: Math.PI / 2, + // new hero props — tuned on-screen in R4 (see LANE_C_GLB_VALIDATION.md) + listeningCorner: 0, + fridge: 0, + arcadeCabinet: 0, }; +// Counter-top GLB props: sit ON the primitive counter's benchtop (not a floor swap). Each targets a +// counterTop attach slot exposed by the counter fitting. cash_register replaces the primitive till on +// every counter; milkshake_mixer only on milk-bar counters. +const COUNTERTOP = [ + { id: 'cash_register', slot: 'till', hideTill: true, types: null, yaw: 0, maxFoot: 0.42 }, + { id: 'milkshake_mixer', slot: 'appliance', hideTill: false, types: ['milkbar'], yaw: 0, maxFoot: 0.42 }, +]; + let _manifestP = null; // Fetch + cache the manifest. Returns null (not throw) if absent/unreachable → primitives everywhere. export function preloadManifest(url = 'assets/manifest.json') { @@ -81,9 +97,48 @@ export function upgradeFitting(ctx, fitting, kind, manifest) { }); } -// Upgrade every placed fitting in a room. Called from buildInterior when GLB mode is on. -// Returns a promise resolving once all in-room fittings' GLB swaps have settled. -export function upgradeRoom(ctx, placed, manifest) { - if (!manifest) return Promise.resolve(); - return Promise.all(placed.filter(p => !p.removed).map(p => upgradeFitting(ctx, p.fitting, p.kind, manifest))); +// Place counter-top GLB props (cash_register, milkshake_mixer) onto a counter fitting's benchtop. +// Additive: does NOT hide the counter primitive (correct height + keeper stand stay); cash_register +// hides just the primitive till it replaces. Fail-soft: unreachable/missing GLB → primitive stays. +export function attachCounterTop(ctx, fitting, manifest, shopType) { + const ct = fitting.counterTop; + if (!ct || !manifest || !manifest.fittings) return Promise.resolve(); + const jobs = []; + for (const spec of COUNTERTOP) { + if (spec.types && !spec.types.includes(shopType)) continue; + const entry = manifest.fittings[spec.id]; + if (!entry || !entry.file) continue; + const ref = manifest.localBase ? `${manifest.localBase}${entry.file}` : `depot:${entry.file}`; + jobs.push(loadGLB(ref).then(gltf => { + if (!gltf || ctx._disposed || !fitting.group.parent) return; + const THREE = ctx.THREE; + const inst = skeletonClone(gltf.scene); + inst.rotation.y = spec.yaw || 0; + const bb = new THREE.Box3().setFromObject(inst); + const size = new THREE.Vector3(); bb.getSize(size); + const glbW = Math.max(size.x, size.z) || 1; + const s = Math.min(1, spec.maxFoot / glbW); // clamp to the benchtop slot; never inflate + inst.scale.setScalar(s); + const at = ct[spec.slot]; + inst.position.set(at.x, ct.y - bb.min.y * s, at.z); // plant base on the benchtop surface + if (spec.hideTill) for (const c of fitting.group.children) if (c.userData && c.userData.tillPrimitive) c.visible = false; + inst.userData = { glbUpgrade: true, kind: spec.id, counterTop: true }; + fitting.group.add(inst); + })); + } + return Promise.all(jobs); +} + +// Upgrade every placed fitting in a room. Called from buildInterior when GLB mode is on. +// `shopType` gates counter-top props (e.g. milkshake_mixer → milk bar only). +// Returns a promise resolving once all in-room fittings' GLB swaps have settled. +export function upgradeRoom(ctx, placed, manifest, shopType) { + if (!manifest) return Promise.resolve(); + const jobs = []; + for (const p of placed) { + if (p.removed) continue; + jobs.push(upgradeFitting(ctx, p.fitting, p.kind, manifest)); + if (p.fitting.counterTop) jobs.push(attachCounterTop(ctx, p.fitting, manifest, shopType)); + } + return Promise.all(jobs); } diff --git a/web/js/interiors/interiors.js b/web/js/interiors/interiors.js index cd6b6ae..89e34ce 100644 --- a/web/js/interiors/interiors.js +++ b/web/js/interiors/interiors.js @@ -85,9 +85,9 @@ export function buildInterior(shop, THREE, opts) { let glbReady = Promise.resolve(); // resolves when GLB upgrades settle (immediately if off) if (opts.useGLB) { const manifest = opts.manifest || (glbManifest && glbManifest.__resolved) || null; - if (manifest) glbReady = upgradeRoom(ctx, lay.placed, manifest); + if (manifest) glbReady = upgradeRoom(ctx, lay.placed, manifest, recipe.key); else glbReady = preloadManifest(opts.manifestUrl).then(m => { - if (m && !ctx._disposed) { glbManifest = { __resolved: m }; return upgradeRoom(ctx, lay.placed, m); } + if (m && !ctx._disposed) { glbManifest = { __resolved: m }; return upgradeRoom(ctx, lay.placed, m, recipe.key); } }); } diff --git a/web/js/interiors/theme.js b/web/js/interiors/theme.js index 14d54f0..f088d7d 100644 --- a/web/js/interiors/theme.js +++ b/web/js/interiors/theme.js @@ -34,10 +34,10 @@ const RECIPES = { clutter: 0.85, counterPos: 'corner', fittings: [ + { kind: 'listeningCorner', zone: 'window', min: 1, max: 1, priority: 6 }, // focal hero prop — placed first + high priority { kind: 'recordBin', zone: 'aisle', min: 2, max: 5, priority: 5 }, { kind: 'crate', zone: 'centre', min: 1, max: 3, priority: 3 }, { kind: 'wallShelf', zone: 'wall', min: 1, max: 3, priority: 3 }, - { kind: 'listeningCorner', zone: 'window', min: 1, max: 1, priority: 4 }, { kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 }, ], stockKind: 'sleeves', @@ -110,6 +110,7 @@ const RECIPES = { clutter: 0.85, counterPos: 'door', fittings: [ + { kind: 'arcadeCabinet', zone: 'aisle', min: 1, max: 1, priority: 6 }, // focal hero prop — placed first + high priority so the door-counter path loop won't pull it { kind: 'vhsAisle', zone: 'aisle', min: 2, max: 4, priority: 5 }, { kind: 'vhsAisle', zone: 'wall', min: 1, max: 2, priority: 4 }, { kind: 'returnsSlot', zone: 'counter', min: 1, max: 1, priority: 4 },