glytch/fktry/lanes/LANE-RENDER.md
type-two 5ee0fb9e78 [render] round 1 NOTES
Decisions, verification evidence, honest limitations, and 3 contract
requests (BeltItem stable id, optional MachineDef.color, main.ts placing
on any pointer button).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 16:31:10 +10:00

157 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 1 (goal: milestone M1 "FLOW")
Replace the stub in `src/render/index.ts`:
1. **Camera + controls.** Orthographic isometric-style view. Pan: middle-drag or WASD.
Zoom: wheel (clamped ~8..40 world units). Window resize handled.
2. **Ground.** Tile grid over the 64×64 world; subtle Stream-Crust styling (dark
`#0a0812` base, faint `#181425` lines) — see style guide §8. A soft vignette or
fog so the world doesn't just end.
3. **Asset registry + hot-swap.** `registry.ts`: maps asset key → THREE.Object3D
factory. Default: procedural placeholder (footprint-sized beveled box, body color
from data, one emissive accent, tiny idle animation — e.g. quantizer jaw bob).
On init, attempt `GLTFLoader` for `public/assets/models/<key>.glb`; if present it
replaces the placeholder factory. Also poll or re-check on a 10s timer in dev so
assets dropped in mid-session appear without reload (note what you implement).
4. **Entities.** Sync scene to `snap.entities` (add/remove/rotate). Belts get a flat
directional piece with an animated arrow/chevron texture (shader or UV scroll).
5. **Belt items.** InstancedMesh per item type (colored octahedron/box placeholder),
positioned from `BeltItem.t` + belt entity pos/dir, interpolated with `alpha` for
butter-smooth motion at 30 tps sim.
6. **Picking + ghost.** `pickTile` via raycast to the ground plane. `setGhost` shows
the selected machine's placeholder at the hovered tile, tinted cyan; red tint if
tiles occupied (you can read occupancy from `snap.entities` footprints).
7. **Feedback dressing (cheap but load-bearing):** jammed machines get a small
blinking amber light; brownout dims the whole scene ~30% and flickers lights
(listen to snapshot.bandwidth.brownout).
**Definition of done:** dev server shows the factory; placing a line from the UI (or
console dispatch) shows machines + items gliding down belts smoothly; a GLB dropped
into `public/assets/models/quantizer.glb` replaces the placeholder. `npm run check`
clean. NOTES written.
---
## 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/<key>.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).