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>
261 lines
17 KiB
Markdown
261 lines
17 KiB
Markdown
# 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.089–0.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.
|