glytch/fktry/lanes/LANE-RENDER.md
type-two 30deed10dc [render] round 2 NOTES
Verification numbers, the MachineDef.color conflict written up in full, and
3 contract requests (settle color's meaning, EntityState.scrammed, BeltItem.side).

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

261 lines
17 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 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/<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).
### Round 2 — 2026-07-17 — Opus 4.8
**READ THIS FIRST — I did not follow order 2's letter, deliberately.**
`MachineDef.color` is wired to the machine **body**, not the emissive accent, against
the stated precedence (`def.color` > derived > kind). Why: all 21 values LANE-DATA
authored are desaturated industrial neutrals that match codex CHASSIS descriptions —
`seam-extractor #8a7a32` is §4's "grimy yellow chassis", `decode-asic #b9bec4` is its
"server-blade monolith", `mosh-reactor #52333e` its pressure vessel. Style guide §8 says
ALL saturation belongs to media/artifacts and each machine gets exactly ONE impossible
element. Wired to the accent (I built it that way first and looked at it), all 21
machines glow muddy neutral: the quantizer's jaw went from coefficient-cyan to tan, the
mosh reactor's from melt-orange to mauve, and the two-material rule quietly died.
Read as bodies they're exactly right, and we get both: 21 art-directed chassis AND the
vivid resource accents. The black-on-black problem the field was meant to solve stays
solved by round 1's legibility floor. **This is a cross-lane semantic conflict between
my orders and DATA's — please settle it; see CONTRACT REQUEST 1.** If you want the
literal reading, it's a two-line revert in `registry.ts` (`bodyFor`/`accentFor`).
**SHIPPED** — all 6 orders, each verified in the running build:
1. **Belt topology** (`topology.ts`, new): neighbour analysis → straight / cornerA /
cornerB / merge, one InstancedMesh per (def × shape), flow field baked into each
shape's shader (no runtime branch). Corners sweep a quarter-arc; merges add inlet
arrows. Verified: the devscene bend classifies **20 straight / 1 cornerA / 1 cornerB
/ 1 merge** across 23 belts, still 4 draw calls.
- **Cargo now follows the corner arc too.** Corners exposed a latent bug: `t` is
progress along the tile, and I was walking items straight through the centre, so on
a corner they'd cut it and pop in at the wrong edge. Items now enter at the feeding
edge and sweep the arc. Verified numerically: items on both corners sit 0.0890.248
off the centreline where a straight path pins every sample to exactly 0.000.
2. **Heat** (`heat.ts`, new + `entities.ts`): dull-red emissive ramp on the body, rising
shimmer column, throttle, scram. Verified end-to-end at forced heat:
- 0.50 → body emissive `#bc280a` @ 1.35 (= `1 + 0.5²×1.4`), haze 0.5
- 0.85 → idle clock advances **0.0053/frame vs 0.0083 full speed** = the 0.625×
throttle factor exactly; body emissive 2.01
- 1.00 → **scram**: every emissive 0 (dead-dark), haze off, idle clock frozen at
43.613, amber blinking
- back to 0.20 → **restart recovers**: body emissive 1.06 (= `1 + 0.2²×1.4`), latch
cleared, idle moving, haze back
3. **Buffer tank**: fill column driven by the tank's share of `bandwidth.stored` over
summed `bufferCap` (DATA shipped `buffer-tank: 240`). Verified oscillating with the
pool rather than sitting at its 0.001 floor.
4. **AnimationMixer**: clip 0 plays looped on rigged GLBs, and shares the throttle
factor — a hot machine's *animation* labours too, which fell out for free. Verified
with a hand-authored animated GLB: 9 frames → 9 distinct Y values, clip driving the
node. Rigged GLBs clone via `SkeletonUtils.clone`, not `Object3D.clone`, or every
instance would animate off the first one's bones.
5. **Exact cargo interpolation**: keys off `BeltItem.id` when present (`exactIds: true`
verified), order-matching retained as fallback until v3 makes it required. The
one-tick approximation is gone whenever the sim stamps ids.
6. **Multi-mesh belt GLBs**: merged with `mergeGeometries(..., useGroups)` so a fancy
belt asset can't half-render. Verified with a 2-mesh GLB: **2 material groups, 2
materials, 1 instanced draw call**, both meshes visible. If attributes ever mismatch
it keeps the first mesh and `console.warn`s the count rather than silently dropping.
**VERIFIED** — perf standing rule still met with everything above added:
`?devscene=stress` → 500 entities + 2,000 items at **3.6ms/frame avg (4.6× headroom)**,
5 instanced draw calls. Round 1 was 3.2ms, so topology + heat + mixers cost ~nothing.
Dev scaffold still DEV-gated and absent from `dist/` (verified 0 hits).
**DECISIONS**
- **Scram is derived from `heat >= 1`**, not from the `scram` event — the `Renderer`
interface only receives snapshots, never events, so the edge isn't reachable from
here. Works as long as SIM holds heat at 1.0 while latched off. See request 2.
- GLB instances get **cloned materials**; heat/scram is per-entity state and the
template's materials are shared, so one hot machine would otherwise tint every copy.
Placeholders own geo+mats; GLB clones own only mats. Disposal follows that split.
- Heat tints the tagged body mesh on placeholders; a GLB has no body/accent split, so
there it tints everything.
- Shimmer is an additive billboard column, not screen-space refraction — post-processing
is LANE-SCREEN's territory and I'm not reaching into it.
- Kept the `camera.ts` non-left-button `stopPropagation` as belt-and-braces even though
main.ts now guards; it's two lines and costs nothing.
**BLOCKED/BROKEN** — nothing blocking. Honest limitations:
- **Merge pieces don't route cargo per-inlet**: items on a merge belt still ride the
straight path, because `BeltItem` doesn't say which side it entered from. Reads fine
(a merge's main flow *is* straight) but it's not truthful for the side feed.
- **`reference.ts` doesn't exist** — SIM hadn't landed it when I built, so the DoD's
"reference factory looks ALIVE" was checked against my own devscene bend + showroom
row instead. Happy to re-verify against theirs next round.
- SIM doesn't emit heat/splitters/ids yet, so orders 2/4/5 are verified against the
devscene, not live sim data. All of it is contract-shaped and will light up as SIM
lands; nothing in my lane needs to change.
- Corner arc assumes a 1×1 belt. Fine per contract (`beltSpeed` belts are 1×1).
**CONTRACT REQUEST**
1. **Settle `MachineDef.color`'s meaning** (see the top of this entry). My proposal:
keep DATA's values as-authored and re-comment the field as chassis/body colour, then
add `accent?: string` if art-directing the emissive is ever wanted. Right now the
contract comment says "accent", DATA authored bodies, and I've followed the data.
2. **`EntityState.scrammed?: boolean`** — I derive scram from `heat >= 1`. If SIM ever
cools a machine while it's still latched off, the visual un-scrams early and lies.
A flag makes it unambiguous and costs one bool.
3. Not urgent: `BeltItem.side?: Dir` (which edge cargo entered from) would let merge
pieces route their side feed honestly. Only worth it if merges get common.
**PROPOSAL**
- MODELBEAST hot-swap now also covers **animated** and **multi-mesh** GLBs, so hero
assets can arrive rigged and kit-bashed with no code change. Asset keys are whatever
`MachineDef.asset` says — all 21 are live targets today.
- The showroom row in the devscene (`?devscene`) is a decent visual smoke test for any
new machine visual; it's clear of the belt path on purpose.
**NEXT (round 3 candidates, not self-assigned)**
- Per-era chassis skinning (§8 era skins) once tech/eras land; splitter output lamp
driven by real round-robin state rather than a timer; bloom-loop belt tell; HF dust
piles; parity-mite wildlife; LOD/culling if a real megabase ever drags the frame.