[render] fix GLB body-scale regression: pop settles to baseScale, not 1

The placement scale-pop hardcoded the settled scale to 1, which stomped a
normalised GLB's fit-to-footprint root scale (s~2-3.5) down to 1 on the first
sync, collapsing the body to raw geometry size (a speck) while the beacon/jam
child meshes kept their own local scale. Store baseScale per entity (1 for
placeholders, the normalisation scale for GLBs) and make the pop a multiplier
on it. Verified full-size bodies on fresh loads in ?showroom and ?uidemo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-20 19:33:48 +10:00
parent 0d491f8a2f
commit 3fc043e504
2 changed files with 54 additions and 4 deletions

View File

@ -760,3 +760,45 @@ was added. Round 5's measured **4.4ms/frame** at full load stands as the referen
`firewall.glb` when it lands; per-mite orientation toward its `target` belt; heat-shimmer
*relief* inside a cooler rectangle; bloom-loop belt tell; save/load camera state;
LOD/culling if a real megabase ever drags the frame.
### Round 6 ADDENDUM — 2026-07-20 — Opus 4.8 — GLB body-scale regression fix
**Orchestrator caught a deploy-blocking regression at 0d491f8: GLB machine BODIES collapsed
to specks (beacons + jam lights + labels still correct). Fixed in `entities.ts` — commit
below. My round-6 verification missed it because I checked on an HMR'd session and wrongly
attributed the small bodies to "asset normalisation"; on a FRESH load the specks are stark.**
**ROOT CAUSE — the placement-pop hardcoded the settled scale to 1, which is only true for
placeholders.** A normalised GLB roots at its fit-to-footprint scale `s` (measured live:
2.03.5 for the current wave). The pop's settle branch was
`else if (rec.obj.scale.x !== 1) rec.obj.scale.setScalar(1)` — so on the very first sync it
saw `2.08 !== 1` and stomped the root to 1, shrinking the body to raw geometry size (~0.63
vs the correct 1.84 for a 2×2). The beacon and jam meshes carry their own local scale on
child nodes, so they were untouched — which is exactly the "bodies vanished, beacons fine"
signature the orchestrator reported.
**Why it surfaced now, honestly:** the pop code is UNCHANGED by me and the bug is latent for
ANY non-placeholder settled scale ≠ 1. It bit this round because the live GLB wave normalises
to s≈23.5 (verified: fresh `entry.create()` returns root scale 2.08 + a correct 1.84-wide
body; the live post-pop root was scale 1 + a 0.63 speck). The pop only ever writes
`rec.obj.scale`, so it was provably the resetter. It is not the beacon — a fresh create is
correct; the pop destroys it one frame later.
**FIX:** the pop is now a MULTIPLIER on a stored `rec.baseScale` (captured from `obj.scale.x`
at build = 1 for placeholders, the normalisation scale for GLBs), never an absolute. Settle
compares against `baseScale`, not 1. Placeholders: identical behaviour (baseScale 1). GLBs:
body stays full-size, and the placement scale-pop still plays (relative overshoot).
**VERIFIED on FRESH loads (hard navigate, no HMR), before/after described:**
- `?showroom`: BEFORE = every GLB a speck on a full plinth; AFTER = all 27 render as full
sculpted bodies filling their footprints, lifted + beaconed. Scene-graph confirms live
roots now carry scale 2.03.5 and body world sizes match footprints (1.84 for 2×2, 2.76
for 3×3), where they read 1.0 / 0.63 before the fix.
- `?uidemo` (reference factory, default game zoom): BEFORE = specks; AFTER = full-size
machines throughout, belts + cargo flowing. No console errors. `npm run check` clean,
`npm test` 481/481.
**LESSON (for me and the next lane):** verify visual regressions on a cold load, not an
HMR'd session — an HMR'd renderer can hold meshes normalised in a prior module state and
mask a fresh-boot break. And a hardcoded `scale = 1` is a placeholder assumption that any
scaled asset (GLB, future LOD) will trip; base-relative is the correct shape.

View File

@ -60,6 +60,11 @@ interface Rec {
wasScrammed: boolean;
/** Seconds into the placement scale-pop; >= POP_TIME means settled. */
pop: number;
/** The object's settled scale. 1 for placeholders; the GLB fit-to-footprint scale for a
* hot-swapped asset. The pop multiplies THIS, never resets to 1 a normalised GLB roots
* at s1 and hardcoding 1 collapsed the body to a speck (the beacon/jam sit on their own
* local scale so they were unaffected, which is why only bodies vanished). */
baseScale: number;
}
const JAM_GEO = new THREE.SphereGeometry(0.09, 8, 6);
@ -159,13 +164,15 @@ export class EntityLayer {
if (alarm) anyAlarm = true;
// Placement thunk: overshoot then settle. Reads as weight landing on the floor.
// The pop is a MULTIPLIER on the settled scale, never an absolute — a normalised GLB
// settles at baseScale (s≠1), so resetting to 1 would collapse its body to a speck.
if (rec.pop < POP_TIME) {
rec.pop += dt;
const k = Math.min(1, rec.pop / POP_TIME);
const s = k < 1 ? 1 + Math.sin(k * Math.PI) * 0.22 : 1;
rec.obj.scale.setScalar(k < 0.12 ? k / 0.12 : s);
} else if (rec.obj.scale.x !== 1) {
rec.obj.scale.setScalar(1);
const mult = k < 1 ? 1 + Math.sin(k * Math.PI) * 0.22 : 1;
rec.obj.scale.setScalar(rec.baseScale * (k < 0.12 ? k / 0.12 : mult));
} else if (rec.obj.scale.x !== rec.baseScale) {
rec.obj.scale.setScalar(rec.baseScale);
}
// Harassed by a swarm: the machine stutters. Wrongness of motion is the tell (§8).
@ -292,6 +299,7 @@ export class EntityLayer {
all,
wasScrammed: false,
pop: POP_TIME, // settled by default; a real placement rewinds it to 0
baseScale: obj.scale.x, // 1 for placeholders; the GLB normalisation scale otherwise
};
this.live.set(id, rec);
return rec;