From 3fc043e50419f33d1bcb15f9d48c2befc8c2bf27 Mon Sep 17 00:00:00 2001 From: type-two Date: Mon, 20 Jul 2026 19:33:48 +1000 Subject: [PATCH] [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 --- fktry/lanes/LANE-RENDER.md | 42 ++++++++++++++++++++++++++++++++++++ fktry/src/render/entities.ts | 16 ++++++++++---- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/fktry/lanes/LANE-RENDER.md b/fktry/lanes/LANE-RENDER.md index 33388d9..f932943 100644 --- a/fktry/lanes/LANE-RENDER.md +++ b/fktry/lanes/LANE-RENDER.md @@ -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.0–3.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≈2–3.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.0–3.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. diff --git a/fktry/src/render/entities.ts b/fktry/src/render/entities.ts index af959e9..913346f 100644 --- a/fktry/src/render/entities.ts +++ b/fktry/src/render/entities.ts @@ -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 s≠1 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;