Lane C R6 C2: map the 3 orphaned fittings (glass_case/magazine_rack/crate_stack)

Published R5, unconsumed until now:
- glb.js KIND_TO_GLB: glassCase->glass_case, magazineRack->magazine_rack,
  and a NEW crateStack primitive (fittings.js — a stack of crates) ->
  crate_stack. yaw 0 (validated on-screen).
- theme.js: crateStack in record + opshop (back-room dressing); glassCase
  added to milkbar; magazineRack added to pawn; milkbar magazineRack moved
  window->wall (dodges the door-counter congestion).
- layout.js: fixed placeNearCounter — it picked one random side and gave up,
  so counter-zone fittings (the glass case) rarely landed. Now it tries both
  sides + offsets, then falls back to a free aisle spot, so a min>=1 fitting
  reliably lands.

Validated (GLB on, baked depot): all 3 load + render planted/scaled/facing
right. Appearance glassCase 12/12 (toy/pawn/milkbar), magazineRack 12/12
(milkbar/pawn), crateStack ~11/12 (record/opshop). drawSweep still <=350
with them on (worst 177 GLB-on / 157 off). Determinism 0 fails, leak-free,
dig (?dig=1) still riffles batched bins (C3 re-verify). Contact sheet
docs/shots/laneC/glb_orphans_r6.jpg. qa.sh --strict GREEN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-14 23:19:48 +10:00
parent 3b86b0c5b3
commit c88d8906a2
6 changed files with 58 additions and 13 deletions

View File

@ -27,7 +27,19 @@ then batched (new `web/js/interiors/batch.js`):
- **Sweep now ASSERTS ≤350** (`drawSweep()` in the test page, folded into the soak; `window.PROCITY_C.
drawSweep` for F's harness — see LANE_C_NOTES for the hook).
Still in flight this round: C2 map glass_case/magazine_rack/crate_stack.
**C2 — map the 3 orphaned fittings** (published R5, unconsumed). In `glb.js`: `glassCase→glass_case`,
`magazineRack→magazine_rack`, and a NEW `crateStack` primitive (stack of crates) → `crate_stack`.
Recipe placements: crateStack in record + opshop (back-room dressing), glassCase added to milkbar,
magazineRack added to pawn (+ moved milkbar's magazineRack window→wall to dodge the door-counter).
Also **fixed `placeNearCounter`** — it picked one random side and gave up, so counter-zone fittings
(the glass case) rarely landed; now it tries both sides + offsets, then falls back to a free aisle spot.
All 3 load + render planted/scaled/facing right (yaw 0). Appearance: glassCase **12/12** (toy/pawn/milkbar),
magazineRack **12/12** (milkbar/pawn), crateStack ~11/12 (record/opshop). Contact sheet:
[glb_orphans_r6.jpg](docs/shots/laneC/glb_orphans_r6.jpg). drawSweep still ≤350 with them on (worst 177
GLB-on). **C3**: dig soak re-run after batching + new fittings — riffle works on batched bins, leak-free.
**Round 6 complete for Lane C**: C1 (batching, `35981ad`) + C2/C3 (orphans + dig re-verify) committed.
Draw law satisfied (≤313 was the tightest; ≤177 after C2), determinism/leak/dig all green, qa strict green.
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@ -76,6 +76,22 @@ function crate(ctx, o = {}, r) {
return { group: g, footprint: { w: w + 0.06, d: d + 0.06 }, height: h, slots, places: [g] };
}
// A stack of empty crates — back-room dressing for record/op shops (GLB `crate_stack` upgrades it).
function crateStack(ctx, o = {}, r) {
const g = new ctx.THREE.Group();
const w = o.w || 0.66, d = o.d || 0.66, ch = 0.3, th = 0.02;
const n = 2 + (r() * 2 | 0); // 23 crates
for (let i = 0; i < n; i++) {
const m = ctx.mat(i % 2 ? '#4a5a6a' : '#8a6a45', 0.8);
const ox = (r() - 0.5) * 0.06, oz = (r() - 0.5) * 0.06, y = i * ch;
ctx.box(w, th, d, m, ox, y + th / 2, oz, g); // floor
ctx.box(th, ch, d, m, ox - w / 2, y + ch / 2, oz, g); ctx.box(th, ch, d, m, ox + w / 2, y + ch / 2, oz, g);
ctx.box(w, ch, th, m, ox, y + ch / 2, oz - d / 2, g); ctx.box(w, ch, th, m, ox, y + ch / 2, oz + d / 2, g);
}
g.userData = { kind: 'prop' };
return { group: g, footprint: { w: w + 0.1, d: d + 0.1 }, height: n * ch, slots: [], places: [] };
}
// A listening corner: a low bench + a boxy hi-fi + headphones on a stand.
function listeningCorner(ctx, o = {}, r) {
const g = new ctx.THREE.Group();
@ -377,7 +393,7 @@ function artFrame(ctx, o = {}, r) {
}
export const FITTINGS = {
recordBin, crate, listeningCorner,
recordBin, crate, crateStack, listeningCorner,
wallShelf, metalShelf, cubeShelf, bookshelf, vhsAisle, spinnerRack, arcadeCabinet,
clothesRack,
trestleTable, counter, glassCase,

View File

@ -29,6 +29,10 @@ const KIND_TO_GLB = {
listeningCorner: 'listening_booth', // record store focal prop
fridge: 'drinks_fridge', // milk bar drinks fridge
arcadeCabinet: 'arcade_cabinet', // video store focal prop
// Round-6 orphans (published R5, mapped now):
glassCase: 'glass_case', // toy/pawn/dept/milkbar display case
magazineRack: 'magazine_rack', // milkbar/pawn/book magazine rack
crateStack: 'crate_stack', // record/opshop back-room dressing
};
// Per-kind facing correction (radians, about +Y). The GLBs follow glb_law (metres, +Y up, base at origin)
@ -44,6 +48,7 @@ const KIND_TO_YAW = {
fridge: 0,
arcadeCabinet: 0,
counter: 0, // R5 counter_till — tuned on-screen
glassCase: 0, magazineRack: 0, crateStack: 0, // R6 orphans — tuned on-screen
};
// Counter-top GLB props: sit ON the counter's benchtop (not a floor swap). Each targets a counterTop

View File

@ -198,7 +198,7 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop }) {
if (spec.zone === 'wall' || f.wallMounted) {
ok = placeAtWall(ctx, grid, f, wallSlots, rPlace, placed, spec);
} else if (spec.zone === 'counter') {
ok = placeNearCounter(ctx, grid, f, counter, rPlace, placed, spec);
ok = placeNearCounter(ctx, grid, f, counter, rPlace, placed, spec, W, D, doorCx);
} else {
ok = placeFree(ctx, grid, f, spec.zone, W, D, doorCx, rPlace, placed, spec);
}
@ -296,17 +296,25 @@ function placeAtWall(ctx, grid, f, wallSlots, r, placed, spec) {
return false;
}
function placeNearCounter(ctx, grid, f, counter, r, placed, spec) {
// treasures / cases beside the till, on the goods side of the counter
const side = counter.ry === 0 ? { x: counter.x + (r() < 0.5 ? -1.3 : 1.3), z: counter.z } : { x: counter.x, z: counter.z + (r() < 0.5 ? -1.3 : 1.3) };
function placeNearCounter(ctx, grid, f, counter, r, placed, spec, W, D, doorCx) {
// cases beside the till, on the goods side of the counter — try BOTH sides + a couple offsets, then
// fall back to a free-standing aisle spot so a min≥1 fitting (e.g. a glass case) reliably lands.
const ry = counter.ry;
const cands = counter.ry === 0
? [{ x: counter.x - 1.4, z: counter.z }, { x: counter.x + 1.4, z: counter.z }, { x: counter.x - 1.4, z: counter.z - 0.7 }, { x: counter.x + 1.4, z: counter.z - 0.7 }]
: [{ x: counter.x, z: counter.z - 1.4 }, { x: counter.x, z: counter.z + 1.4 }, { x: counter.x - 0.7, z: counter.z - 1.4 }, { x: counter.x - 0.7, z: counter.z + 1.4 }];
shuffle(r, cands);
const [hw, hd] = halfExtents(f.footprint.w, f.footprint.d, ry);
const rect = rectCells(grid, side.x, side.z, hw, hd);
if (!areaFree(grid, rect)) return false;
f.group.position.set(side.x, 0, side.z); f.group.rotation.y = ry;
stampRect(grid, rect);
placed.push({ fitting: f, x: side.x, z: side.z, ry, hw, hd, rect, priority: spec.priority, kind: spec.kind });
return true;
for (const side of cands) {
const rect = rectCells(grid, side.x, side.z, hw, hd);
if (!areaFree(grid, rect)) continue;
f.group.position.set(side.x, 0, side.z); f.group.rotation.y = ry;
stampRect(grid, rect);
placed.push({ fitting: f, x: side.x, z: side.z, ry, hw, hd, rect, priority: spec.priority, kind: spec.kind });
return true;
}
// counter is boxed in — put it in the aisle instead (still front-of-room-ish)
return placeFree(ctx, grid, f, 'aisle', W, D, doorCx, r, placed, spec);
}
// ── wall decor (art + signage) ─────────────────────────────────────────────────────────

View File

@ -38,6 +38,7 @@ const RECIPES = {
{ 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: 'crateStack', zone: 'wall', min: 1, max: 2, priority: 2 }, // back-room dressing (crate_stack GLB)
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
],
stockKind: 'sleeves',
@ -57,6 +58,7 @@ const RECIPES = {
{ kind: 'metalShelf', zone: 'wall', min: 1, max: 3, priority: 4 },
{ kind: 'bookshelf', zone: 'wall', min: 1, max: 2, priority: 3 },
{ kind: 'trestleTable', zone: 'aisle', min: 1, max: 2, priority: 3 },
{ kind: 'crateStack', zone: 'wall', min: 1, max: 2, priority: 2 }, // back-room dressing (crate_stack GLB)
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
],
stockKind: 'garments',
@ -133,6 +135,7 @@ const RECIPES = {
{ kind: 'glassCase', zone: 'counter', min: 1, max: 3, priority: 6 },
{ kind: 'pegboard', zone: 'wall', min: 1, max: 3, priority: 4 }, // wall hooks
{ kind: 'metalShelf', zone: 'wall', min: 1, max: 2, priority: 3 },
{ kind: 'magazineRack', zone: 'wall', min: 1, max: 1, priority: 2 }, // magazine_rack GLB
{ kind: 'barredWindow', zone: 'window', min: 1, max: 1, priority: 3 },
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
],
@ -151,7 +154,8 @@ const RECIPES = {
fittings: [
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
{ kind: 'fridge', zone: 'wall', min: 1, max: 3, priority: 6 },
{ kind: 'magazineRack', zone: 'window', min: 1, max: 2, priority: 4 },
{ kind: 'magazineRack', zone: 'wall', min: 1, max: 2, priority: 4 }, // wall (was window — conflicted with the door-counter)
{ kind: 'glassCase', zone: 'counter', min: 1, max: 1, priority: 3 }, // lolly/treat display case (glass_case GLB)
{ kind: 'metalShelf', zone: 'wall', min: 1, max: 3, priority: 4 },
{ kind: 'cubeShelf', zone: 'aisle', min: 0, max: 1, priority: 2 },
],