# LANE-RENDER — three.js world renderer You are an Opus 4.8 executor on the RENDER lane of FKTRY. Read `../MASTERPLAN.md` and `../CONTRACTS.md` first; the world bible + visual style guide (§8) is `../../docs/FKTRY_LORE.md` — §8 is YOUR section, follow it. **Mission:** make the Bitstream visible. Isometric factory view, machines, belts with smoothly interpolated items, ghosts, selection — and the hot-swap pipeline that lets MODELBEAST GLB assets land mid-development without a code change. **Owned paths:** `src/render/**`, `public/assets/**` (manifest + loading; the files themselves arrive from MODELBEAST). **Never touch:** contracts, main.ts, other lanes. Sim is truth: you render snapshots, you never simulate. If motion looks wrong, interpolate better or note it — don't "fix" positions locally. **Standing rules (all rounds):** - 60fps with 500 entities + 2,000 belt items on an M-series MacBook. Instanced meshes for belts/items from day one — you will not get to retrofit this later. - Placeholder aesthetic is a feature: crisp box-primitive machines tinted per codex `color`, NOT gray cubes. Two-material rule even for placeholders: dark body + one emissive accent. - Every entity mesh keys off `MachineDef.asset` / `ItemDef.id` through the registry — zero hard-coded ids. --- ## CURRENT ORDERS — Round 2 (goal: finish M1 polish, open M2 "PRESSURE") Round 1 review: excellent — the hot-swap verification and the fog/CAM_DIST honesty note were exactly right. Verdicts: req 1 GRANTED (`BeltItem.id?` in contracts v2 — use when present, keep order-matching as fallback until v3 hardens it). Req 2 GRANTED (`MachineDef.color?` — precedence: `def.color` > your derived-from-outputs > kind accent; your derivation stays as the fallback, it was the right instinct). Req 3 DONE (main.ts ignores non-left buttons; drop your camera.ts workaround or keep it as belt-and-braces, your call). 1. **Belt topology visuals**: corner/junction pieces chosen from neighbor analysis — pure presentation, sim truth unchanged. Splitters (mechanics land this round in SIM) get a visible output-cycling tell. 2. **Heat**: shimmer + dull-red emissive ramp driven by `EntityState.heat`; throttled = idle animation visibly slowed; scram = dead-dark + blinking amber until restart. 3. **Buffer tank**: visible fill level (shader window or scale trick) proportional to the tank's share of `snapshot.bandwidth.stored`. 4. **AnimationMixer**: play clip 0 on rigged GLBs with an idle loop — MODELBEAST assets are imminent; verify with any animated GLB you can find or make. 5. **Exact cargo interpolation** via `BeltItem.id` — delete the one-tick approximation. 6. **Multi-mesh belt GLBs**: merge-or-group fix (your call, note it) so a fancy belt asset can't half-render. **Definition of done:** reference factory (SIM ships it this round — coordinate via their `src/sim/reference.ts` export in a dev page or test harness) looks ALIVE: corners read as corners, heat reads as heat, tanks read as tanks; perf standing rule still met under `?devscene=stress`. --- ## NOTES (append-only log, newest at bottom) ``` ### Round N — YYYY-MM-DD — [your model] SHIPPED: / DECISIONS: / BLOCKED/BROKEN: / CONTRACT REQUEST: / PROPOSAL: / NEXT: ``` ### Round 1 — 2026-07-17 — Opus 4.8 **SHIPPED** — all 7 orders, each verified in the running dev build (not "should work"): - **Camera/controls** (`camera.ts`): ortho iso, wheel zoom clamped 8..40, middle/right-drag pan, WASD+arrows, resize via ResizeObserver. - **Ground** (`ground.ts`): shader grid (antialiased via fwidth, major lines every 8, crust grain) + world-edge dissolve, over `#0c0a15`. Screen-space vignette as a DOM overlay — kept post-processing out of LANE-SCREEN's way. - **Registry + hot-swap** (`registry.ts`): placeholder factories per kind; probes `assets/models/.glb` on init + every 10s in dev. **Verified live**: dropped a GLB into `public/assets/models/quantizer.glb` mid-session → quantizer swapped from placeholder to the GLB mesh with no reload (tick continuous, other 5 machines untouched), console logged the swap. Probe file was removed afterwards so MODELBEAST's real asset lands on a clean path. GLBs are auto-normalised (scaled to footprint, sat on ground), so any export scale just works. - **Entities** (`entities.ts`), **belts** (`belts.ts`, instanced + chevron scroll), **belt items** (`beltitems.ts`, InstancedMesh per type, interpolated), **ghost + picking** (`ghost.ts`), jam blink + brownout dimming. **VERIFIED (numbers, not vibes)** - **Perf standing rule met with 5x headroom**: `?devscene=stress` → 500 entities (20 machines + 480 belts) + 2,000 belt items renders at **3.2ms/frame avg** (budget 16.6ms). The 2,480 belts+items cost **5 instanced draw calls** total. - **Smooth motion**: sampled instance matrices over 9 consecutive frames → 9 distinct positions, steady ~0.0166 tiles/frame (2 tiles/s on a 120Hz panel). Interpolating continuously, not stepping at 30tps. - **Ghost**: hovering an occupied tile (mosh reactor) → picked tile [6,1], tint `#ff3b30` red; empty ground → `#3fd4ff` cyan. Driven through main.ts's real glue. - **Brownout**: exposure 1.35 → 0.644 with flicker. **Jam**: amber `#ffa726`, blinking. **DECISIONS** (flagging these because they're judgement calls, not obvious readings) - **Machine accent colour is derived from data**: `MachineDef` has no `color` field, so a machine's one emissive accent = the colour of the item it makes (recipes → outputs → `ItemDef.color`). Quantizer glows coefficient-cyan, mosh reactor melt-orange; a new machine in `data/` lights up with zero code. Matches §8's "colour = value = literally the resource". See CONTRACT REQUEST below. - **Legibility floor on data colours.** Raw products are near-black by design (MDAT ORE `#1a1a22`). Taken literally that made the seam extractor's CRT head a black panel on a black chassis, and ore items invisible on the belt. Derived accents below L=0.3 now fall back to the per-kind accent (extractor → CRT blue, per codex §4); item base colours are lifted to a lightness floor for rendering only (`palette.readable`). Data is untouched — ore still reads as the darkest thing on the line, just not as a hole. - **Brownout drives tone-mapping exposure**, not light intensity, so emissives dim too — a brownout that left the glow at full reads as a lighting bug, not a power failure. - **Fog is banded around the camera distance**, not absolute. Called out because it bit me: an ortho iso rig parks the camera CAM_DIST(90) from everything, so `Fog(bg,26,96)` fogged the entire world ~91% into the background. Anything touching `CAM_DIST` must keep `index.ts`'s fog band in step. - Dev scaffold (`devscene.ts`) is gated behind `import.meta.env.DEV` **and** `?devscene`, and is tree-shaken out of the prod bundle (verified: 0 hits in `dist/`). It yields automatically the moment the real sim reports any entity — it never fabricates over live sim data. `?devscene=stress` is the perf rig. **BLOCKED/BROKEN** — nothing blocking. Honest limitations: - **Belt GLB instancing takes only the FIRST mesh** of a multi-mesh GLB. Fine for a kit-bashed single-mesh belt segment (codex §9 says belts are modular kit pieces); a multi-mesh belt GLB would render only part of itself. Machines are unaffected. - **GLB animation clips are not played** (no AnimationMixer yet). Idle motion currently only exists on placeholders, so a rigged GLB will land static. Wanted for M4. - **Belt-item interpolation is approximate for exactly one tick** when the lead item leaves a belt and the queue behind it shifts index — bounded at one tick of travel (~0.07 tiles), imperceptible. Root cause is the missing stable id; see below. - Below 30fps the interpolation stretches (prev-tick snapshot may be >1 tick back). Not observed; noting it rather than hiding it. **CONTRACT REQUEST** 1. `BeltItem.id` (stable, per-item) — would make cargo interpolation exact instead of order-matched. Low priority; current error is imperceptible. 2. `MachineDef.color?: string` (optional) — right now I derive the accent from recipe outputs, which is elegant but gives LANE-DATA no way to art-direct a machine that makes something black, or a machine with no recipes. Optional field, derived fallback. 3. **`main.ts` places on ANY pointerdown**, including right/middle button — so a middle-drag pan would also drop a building. I worked around it inside my lane (`camera.ts` stops non-left buttons from bubbling to `#game`), but the clean fix is `if (e.button !== 0) return;` in main.ts's handler. Flagging since it's your file. **PROPOSAL** - MODELBEAST asset keys are live now — the registry will hot-swap any of these the moment a GLB lands, no code change: `seam-extractor`, `belt`, `demuxer`, `quantizer`, `mosh-reactor`, `decode-asic`, `shipper` (+ whatever LANE-DATA adds; the key is `MachineDef.asset`). Hero assets (mosh reactor, muxer) are the ones worth `fal_trellis2`. - Shadows are on (2048 map, tight ortho shadow camera). Cheap at current load; if a big factory drags, that's the first dial to turn. **NEXT (round 2 candidates, not self-assigned)** - AnimationMixer for rigged GLBs; per-era skinning hooks; splitter/lane visuals; heat shimmer + throttle read-out on the software decoder; belt corner/junction pieces (currently every belt is a straight plate regardless of neighbours).