guts/docs/LANES/LANE_A_NOTES.md
jing ecf43940ec [lane A] Round 2 close-out: colorspace re-eyeball, stomach shape read, ruling #5
Finishes the lane A round-2 list that f0982e7 (the rescued first half) left open:

- Biome-tint re-eyeball under the colorspace law (#4 second half). Verdict: all
  six ART_BIBLE tints stand, biomes.js untouched — the wash was wall_material's
  round-1 constants, tuned on the raw framebuffer. Retuned: rim pow 2.2->3.0,
  rim gain 0.9->0.35, textured curve (0.35+d*.95)*.9 -> 0.12+d*0.60, procedural
  d*.8 -> d^2*.65, sheen .10->.04, matcap .22->.14. Rationale documented
  in-shader. Verified on a new six-biome contact sheet (?lvl=biomes, SIX_BIOMES
  fixture — kept as permanent kit), the real L2 hiatus, and an arena interior.
  Evidence: round2_tint_*.png x6 + two *_retuned frames. qa GREEN.

- Stomach-arena shape read for C (#7): recommend the spline-swept room (arena =
  "open" mode over an s-span; fundus dome stays a welded icosphere; chained
  spheres rejected — crease rings + union collision for a worse look). Full
  argument NOTES §-> Lane C + round2_stomach_arena_sketch.svg. Triplanar
  deliberately deferred: the swept room obsoletes it where it mattered.

- Ruling #5: harness -> web/dev/laneA_world.html (+DBG.post to the house shot
  sink); _fixture.js stays — the spline selfcheck asserts against it, now
  documented in-file. launch.json: guts-a on 8145.

- NOTES + progress written for both halves; ROUND2 mid-round box updated: lane
  A's round-2 list is closed, only round-3 items remain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:14:30 +10:00

328 lines
22 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 A — WORLD — NOTES
## Round 2 (2026-07-16) — the law-compliant look, and what shape a stomach is
Context for the reader: the original round-2 session was cut off 15:08 before NOTES; F
committed the orphaned work as `f0982e7` (its commit message is the NOTES for that half) and
a resuming session finished the lane. This section covers both halves; trust the commit
message for the mechanics of the first.
### What landed
First half (`f0982e7`, per its message): `crestSpeed(s)` + CREST_FACTOR 1.6 with selfcheck
asserts (ruling #1) · radius blend widened ±12 → ±25 + no-cliff selfcheck (C's #4 dependency —
the taper C assumed now exists) · per-segment `wave.amp` override baked as per-vertex `aWaveA`
(ruling #8) · colorspace law in the wall shader (ruling #2) · TBN normal maps + matcap + dual
detail layers, D's `perturb()` with the trap documented in-shader · `sample(s, out)` v1.2 ·
slug map shrunk to the two real mismatches.
Second half (this commit):
1. **Biome-tint re-eyeball under the colorspace law (task #4, second half). Verdict: all six
ART_BIBLE tints stand — `biomes.js` is untouched.** The wash was never in the palette; it
was the shader's tuned constants, all eyeballed round 1 on the *raw* framebuffer. Once
`<colorspace_fragment>` encodes the output, every midtone gains roughly a stop, and the
fresnel rim — which an inside-a-tube camera sees at grazing angles almost everywhere —
flooded the frame (the esophagus shot was effectively rim colour wall-to-wall; the oral
fallback was near white-out). Retuned in `wall_material.js`, where the round-1 numbers were
authored: `uRimPow` 2.2 → 3.0 (confines the rim to fold edges, where the TBN normals bend),
`uRimGain` 0.9 → 0.35, textured base `(0.35 + d·0.95)·0.9``0.12 + d·0.60`, procedural
base `d·0.8``d²·0.65` (the sine bands only span ~0.4..1, and post-law that reads flat —
squaring restores the dark troughs), crest sheen 0.10 → 0.04, matcap gain 0.22 → 0.14.
2. **The six-biome contact sheet** that the eyeball ran on, kept as infrastructure:
`?lvl=biomes` in the harness loads `SIX_BIOMES` (`_fixture.js`) — one straight segment per
registry biome. Whenever tints, textures, colorspace or TBN change again, one fly-through
re-checks everything. Evidence: `docs/shots/laneA/round2_tint_<biome>.png` ×6, plus
`round2_L2_hiatus_retuned.png` (C's calm-wave override, real L2) and
`round2_arena_interior_retuned.png` (the BackSide/arena material path).
3. **Ruling #5 executed**: the harness moved to `web/dev/laneA_world.html` (it stays — it is
the lane's instrument, not a boot workaround: `pose`/`poseAt` deterministic evidence frames,
the leak `sweep`, the contact sheet). It gained `DBG.post(name)` which POSTs the canvas to
D's `shot_sink` — the house evidence tool — instead of the round-1 private sink.
`_fixture.js` stays too, and now says why: the spline selfcheck (a qa gate) asserts against
FIXTURE's exact shape, headless. It is not waiting on C's levels after all.
4. **Stomach-arena shape read for C (task #7)** — recommendation + sketch, § → Lane C below.
### Measured / verified this half
qa.sh GREEN end-to-end (ESM, determinism, levels 3/3, world selfcheck, manifest). Spline
selfcheck green after every edit — the `_fixture.js` changes are additive (new export), and
FIXTURE itself is untouched. Contact sheet: 6 draws / 61k tris per posed frame at 1280×720,
consistent with round 1's budget headroom. fps still not claimed, same reason as round 1.
### Deliberately not done
- **Triplanar arena shells** ("if time", task #7's tail): skipped, and not only for time — if
C takes the swept-room proposal below, the stomach stops being an icosphere and triplanar
stops being needed for the game's biggest room. The remaining icospheres (fundus dome, boss
lairs, r ≲ 70) have modest pole pinch. Decide the shape first, then spend the shader work
where it still matters. Revisit in round 3 with the L3 build.
- **Slug-map deletion** (ruling #3): waiting on D's rename ping, as sequenced. The map is
already down to the two real mismatches (`smallint`, `colon`).
## → Lane C (round 2): what shape is the acid sea?
**Recommendation: don't grow the sphere — sweep the room along the spline you already
authored.** Your L3 `segments[]` (fundus r 40, body r 45, antrum 25, + wobble, + curviness)
already *describe* the stomach's bag: my proposal is that the "arena" becomes a **mode over an
s-span**, not a separate geometry — the tube pipeline extrudes those segments at room radii
exactly as it does corridors, and gameplay flags the span "open" (B's 6DOF, no disc clamp).
The J-bend comes from `curviness`, the lobes from `wobble` (plus a low-frequency bulge term I
add to the radius law if you want asymmetry — greater vs lesser curvature), and the two ends
cap with hemispheres at the cardia and pylorus sphincters. What this buys, concretely:
collision is the *existing* `project()`/`wallRho` path (varying radius is already supported
and selfchecked — no union-of-spheres code); UVs stay `(theta, s)` so D's gastric pits tile
like every other wall with **no pole pinch** (the round-1 starburst limitation dies rather
than getting a triplanar bandage); the acid plane is a horizontal chord of the swept section
(height driven by your events, `#c8ff3a`, round 3); and your mucus shallows are an `(s, theta)`
band hugging the greater-curvature side — expressible in level data. The **fundus dome stays a
welded icosphere** (it's a dead-end bulge *off* the centreline, which a sweep can't express) —
the existing primitive, existing `arenaAt`. Chained overlapping spheres were the alternative
and I sketched why not: every intersection of two displaced shells is a visible crease ring,
and union `collide()`/`project()` is new math for a worse look. Cost on my side, stated
honestly: radial resolution must scale with radius (64 segments at r 45 is an 8-unit facet;
same ~3u solve the arena already does), cap geometry at the span ends, and modest streaming
cost since room chunks are bigger. Schema ask, if you take it: `segments[].mode: "open"` (or
an `arenas: [{from, to}]` span form — your call which reads better as data), spec'd by you in
schema v3 early round 3, implemented by me the same round, before you author the acid sea
proper. Sketch: `docs/shots/laneA/round2_stomach_arena_sketch.svg`. `arenaAt`/`modeAt` keep
their contract for B either way (bounding `{center, radius}` per open span).
## → Lane D (round 2)
- **Your measured detail curve changed — deliberately, and it's the law's fault, not your
measurement's.** `(0.35 + d·0.95)·0.9` was right on the raw framebuffer; under
`<colorspace_fragment>` its pedestal alone displays as ~60% grey. Now `0.12 + d·0.60`
(rationale in-shader). Your mean-0.50/std-0.19 normalisation is exactly what made retuning a
one-liner — the pack didn't move, the curve did. `normalScale` 0.6 unchanged and still right.
- When your mouth + colon walls land, the eyeball harness is
`web/dev/laneA_world.html?dbg=1&assets=1&lvl=biomes` — all six biomes, one URL. I'll re-run
the contact sheet then; expect no curve change (your normalisation again).
- Slug rename: still on, map deletion is a same-day promise once your NOTES ping lands.
## → Lane B (round 2)
- **Nothing gameplay-moving changed this half.** The retune is fragment-shader only: `wallRho`,
wave amplitudes, `crestSpeed`, collision — all untouched since `f0982e7`. Your speed-lock
and envelope numbers stand.
- The crest sheen is dimmer (0.10 → 0.04) but the crest still reads — it keeps its geometry,
normal tilt, and rim pop. If playtest says the boost tell got too subtle, the knob is one
constant in `wall_material.js` and I'll trade sheen against rim gain, not against the law.
## → Lane E (round 2)
- `biomeAt(s).palette` hexes are unchanged (and now display *as written* end-to-end, which
they never quite did before the law) — safe to key HUD accents off `palette.rim`.
## → Lane F (round 2)
- Ruling #5 executed as above; `web/dev/` now holds D's texview and my world harness.
- Housekeeping: `launch.json` gained `guts-a` on **8145** (8140/8141 were other sessions').
Shot sink on **8144** was reused as the instructions suggested and left running.
- The two pre-retune shots `f0982e7` committed (`round2_L2_hiatus.png`, `round2_L2_tbn_wall.png`)
show the washed-out pre-eyeball state — superseded by the `*_retuned` set, kept as the
before/after record of what ruling #2 predicted.
### Round 3 (mine)
Swept-room implementation if C specs it (radial-res solve + caps + open-span mode) · acid
plane with C's height events · villi band when a small-intestine level exists · sphincter
joint geometry at biome seams · slug-map deletion on D's ping · stomach-asymmetry bulge term
if C wants authored greater/lesser curvature.
---
# Round 1 NOTES (kept verbatim below)
World v0 is in and **the real game boots it**: `http://localhost:8140/?dbg=1` runs Lane A's
canal on C's `L2_esophagus` (3600 units, hash `cefc4f83`), zero console errors. No boot.js
change was needed — C's `levels/index.js` landing is what unblocked `pickWorld()`.
## What landed
`web/js/world/` — nothing outside it was touched.
| file | what |
|---|---|
| `spline.js` | the math: centreline, arclength LUTs, parallel-transport frames, radius law, wave phase, `project()`, `hash()`, `stats()`. **Imports no three.js**`node web/js/world/spline.js --selfcheck` runs headless (14 assertions). |
| `biomes.js` | palette/param registry (ART_BIBLE values). C ratified this as the biome-id source of truth. |
| `wall_material.js` | the synthetic-scanner wall: tint × detail + fresnel rim + fog, peristalsis in the vertex shader. |
| `tube.js` | chunked extrusion + streaming window. |
| `arena.js` | displaced-icosphere shells (the stretch goal), v0. |
| `flycam.js` | `?fly=1` noclip. **Not wired** — snippet for F below. |
| `index.js` | `createWorld()` — THE WORLD CONTRACT, drop-in for the stub. |
| `dev.html` | my harness. Dev-only; delete when boot covers it. |
| `_fixture.js` | dev fixture level. Delete when C's levels cover every case (arenas: L3 does now). |
Contract is implemented as written — same surface, units and semantics as the stub, so B's
stub-built work should port unchanged. Extras beyond it: `flowPulse(s,t)`, `hash()`, `stats()`,
`dispose()`, `spline`, and an **optional 2nd arg on `project(pos, hint)`** (see → Lane B).
## Measured (say how you measured it — so: how)
All from `dev.html` on C's real `L2_esophagus` with D's real pack, 1920×1080, via
`renderer.info` (`DBG.pose()` / the sweep in `DBG.sweep()`):
| thing | measured | budget |
|---|---|---|
| draws, worst frame over a full-canal sweep | **8** | ≤150 (charter), ≤300 (TECH) |
| tris, worst frame | **74,420** | ≤500k |
| live chunks | 58 (+1 per arena) | — |
| geometries after full sweep + return to s=0 | **6 → 6, no leak** | dispose-clean |
| geometries after `dispose()` | **0** (textures left alone — D owns them) | — |
| level load, C's 3600-unit L2 | **<120 ms** | <2 s |
| pinch ratio (min turn radius ÷ max tube radius) | L2 **3.32**, L3 **1.89**, fixture 2.37 | >1.5 |
**fps is NOT measured and I'm not claiming it.** Screenshot tooling drives the tab hidden,
which pauses rAF; `gl.finish()` didn't sync either (it reported 0.12 ms/frame = 8219 fps,
which is obviously junk). Draws/tris are reliable and are 20× under budget, so I expect fps to
be fine — but someone with a real window has to confirm. → Lane F, at round end.
Determinism: same seed ⇒ same hash, **and node and the browser agree** (`50a5b0ec` for the
fixture in both). Different seed ⇒ different hash. Both asserted in the selfcheck.
## Decisions (mine to make; flagging the ones that touch you)
1. **`curviness` = fraction of the tightest bend this pipe can survive**, not an amplitude.
Octave amplitudes are solved from a curvature budget `kappa = curviness / (2.2 × baseRadius)`.
Why: authoring amplitudes in units let a wide, curvy segment fold the tube through itself —
the selfcheck's pinch guard caught exactly that on its first run (turn radius 9.9 vs tube
radius 17.2). Now a wide pipe straightens itself and **C never has to think about curvature**.
C's 0.150.95 range works as documented; nothing to change.
2. **Peristalsis: global `OMEGA = 3.08 rad/s`, wavenumber `k(s) = OMEGA / flow(s)`, phase
`K(s) = ∫k ds`** (baked per-vertex as `aPhase`). Consequence worth knowing: **a wave crest
travels at exactly the local flow speed**, so "ride the crest for boost" is physically the
same thing as "ride the current". Plain `k·s ω·t` tears the wave at every flow change;
`K(s)` doesn't. For a flow-14 esophagus this reproduces the stub's wave exactly (k = 0.22).
3. **Esophagus `wave.amp` 0.9 → 1.4** (`biomes.js`). The stub's 0.9 is a barely-legible ripple;
GDD §2 makes riding a crest the level's signature, and B can't surf what the player can't
see. 1.4 reads as distinct rings and still leaves ~78% of a radius-10 tube flyable. Measured
in the harness against 0.9 and 3.2. **This moves `wallRho`** → Lane B.
4. **Chunk seams align to biome joins**; an arena owns its whole s-span and the tube is skipped
there (otherwise the corridor renders *inside* the room and `collide()` disagrees with what
you can see). Seam is chunk-quantised ±20u — fine until sphincter geometry exists.
5. **Arena fog is sized to the room**, not the biome: `min(biome.fog, 1.09/radius)`. Biome fog
is tuned for ~100u corridor sightlines; reused in C's 360u-wide acid sea it renders a black
rectangle (that's what my first arena shot literally was). Small lairs keep the murk.
6. **One `uv` attribute = (θ/2π, s in world units)**, no uv2 — tiling is derived in-shader from
a `uTile` uniform, so a second UV set would be dead weight. Charter said uv2; this is the
simplification, flagged rather than done silently.
7. **Colorspace: matched to the stub** — the wall writes its colour with no
`<colorspace_fragment>` conversion. That's a whole-game decision (it moves every colour at
once) so I'm not making it unilaterally → Lane F.
## Known limitations (v0, stated plainly)
- **Arena pole pinch.** Arena UVs are spherical, so the fold pattern converges into a starburst
at the poles — visible top-right in `round1_L3_arena_acid_sea.png`. A real texture will pinch
there too. Round 2: triplanar projection for arena shells.
- **Arena shape.** A sphere is the wrong shape for a stomach and C says so in their own
`L3` note. Round 2 should discuss a capsule/lobed form; the `arenaAt` contract (centre+radius)
survives either way for B's clamp.
- **No acid plane** (GDD's animated `#c8ff3a` level) — round 2, needs C's event to drive height.
- **No villi** (small-intestine InstancedMesh band) — round 2, no level needs it yet.
- **No matcap / normal map.** D ships `normal` maps already; using them needs a TBN in the wall
shader. Round 2. Untested code paths are worse than absent ones.
- **`project()` in arena mode** is step-clamped rather than properly solved; it's accurate in
tube mode (recovers s to <0.25, θ to <0.02 rad asserted). Arena collision doesn't use it
(sphere test), so this only matters if B calls `project` on a point way off the centreline.
- Segment param blending lags the march by one 0.5u step. Deterministic, sub-step, harmless.
---
## → Lane F
1. **Pass `assets` into `createWorld`.** The game currently boots my world but **D's textures
are loaded and then unused** I verified every material comes up `USE_DETAIL`-less under
`boot.js`. In `pickWorld()`:
```js
const mod = await import('./world/index.js');
const level = await loadLevel(flags.lvl);
const assets = await (await import('./core/assets.js')).createAssets({ flags, renderer });
return mod.createWorld(level, { rng: createRng(flags.seed ?? level.seed), assets });
```
(`createWorld` already ignores a missing/failed `assets` the fallback law holds either way.)
2. **Add the world selfcheck to the qa gate** it has caught two real defects already:
```bash
# 4b. Lane A world math (headless; no three.js needed)
node web/js/world/spline.js --selfcheck >/dev/null 2>&1 && green "world selfcheck" || red "world selfcheck failed"
```
3. **Screenshot tooling traps**, which will bite you at round-end boot-matrix time. The pane
drives tabs **hidden**: rAF never fires (so the loop isn't running when you shoot) and
`innerWidth` is **0** (so the canvas is 0×0 and `toDataURL()` returns `"data:,"`). The pane
screenshot still looks right because grabbing it fronts the tab. `DBG.shot()`'s anchor-click
download also silently no-ops here. My harness works around all three in `DBG.size/pose/poseAt`
— worth lifting into `boot.js`'s DBG. I got PNGs out by having the page `fetch(PUT)` them to
a 30-line local sink (in my scratchpad, happy to promote it to `tools/` if you want it).
4. **Dev port**: 8140 was already held by another session's server, and `launch.json` is yours,
so I left it alone and reused the running one. If lanes are meant to run concurrently on one
box this needs a call.
5. Contract freeze: I'd like `project(pos, hint)` and `flowPulse/hash/stats/dispose` folded into
TECH.md's world contract, and `assets.texture()` added to the asset contract (see → Lane D).
## → Lane B
- **`wallRho` shrank**: it subtracts `wave.amp + wave.breathe + 0.6` and esophagus `wave.amp`
is now 1.4 (was 0.9). Playable radius in a base-10 tube ≈ **7.85**. Re-check your graze
distances. It's time-independent on purpose — a collision surface that breathed would have
you fighting the wall.
- **`project(pos, hint)`** — pass your previous `s` as `hint` and it's near-free and exact. With
no hint it seeds from `-pos.z` and still converges (6 fixed-point steps), but the hint is
strictly better in your per-frame path. Both are asserted in the selfcheck.
- **Crest speed == `biomeAt(s).flow`**, exactly, by construction. `world.flowPulse(s, t)` is
the *same* function the vertex shader runs (0 = trough, 1 = crest), so pulse ≈ 1 means the
wall is at its narrowest AND the crest is moving at flow speed right there. Boost window.
- `biomeAt(s).flow` is **blended across segment joins**, not stepped — safe to read per-frame.
- Arena mode: `modeAt(s)` / `arenaAt(s)``{center, radius}` for your 6DOF clamp. `collide()`
branches automatically (sphere test inside a room, radius test in a tube); you don't have to.
- `sample()` allocates fresh Vector3s per call, same as the stub. If that shows up in your
profile, ask and I'll add `sampleInto(s, out)`.
## → Lane C
- Your schema v1 reads clean here — v1 is additive and my reader consumes L1/L2/L3 unchanged.
Your ratified arena semantics (sphere spanning `[at-radius, at+radius]`) are exactly what's
implemented, and `modeAt`/`arenaAt` agree with your notes on all three L3 arenas.
- **Biome ids are the registry in `world/biomes.js`**: `oral · esophagus · stomach ·
small_intestine · large_intestine · appendix`. Unknown id ⇒ neutral fallback + one warning,
never a crash. Thanks for writing the "biome = tissue family, not anatomy" rule into TECH —
that's exactly right and it's why `name` carries the anatomy.
- `curviness` behaves as you documented (0 straight … 1 writhing). Under the hood 1.0 now means
"as curvy as this radius can safely be", so **you can't author a pinch** — L2 measures 3.32×
clear, L3 1.89×. Author freely.
- Free lever if you want it: `flow` is per-segment and it now sets the peristalsis wavelength
(`k = 3.08/flow`). Low flow ⇒ tighter, slower rings. A "sluggish" segment reads sluggish.
- Your L3 note that a sphere is the wrong shape for the stomach body: agreed, see limitations.
## → Lane D
- **Naming mismatch, silent by construction.** Your keys are `wall_smallint_a` / (future)
colon; my biome ids are `small_intestine` / `large_intestine`. `wall_${biomeId}_a` therefore
misses, and because assets are optional it falls back to procedural **forever with no error**.
I've added an explicit slug map in `index.js` so it works today. Proposal for round 2:
standardise on the biome ids (`wall_small_intestine_a`) and delete the map — one name, one
place. Your call, just let's not leave it implicit.
- **Your `tile` semantics and mine converged independently** — `[repeats around theta, units of
s per repeat]`, both read off the stub's uv. Ratified from my side; `[4,16]` looks right in
engine (`round1_L2_textured.png`).
- **`texture()` is the useful entry point, not `get()`.** TECH names `get(category,name)` as THE
contract, but it returns raw JSON a shader can't eat. I prefer `assets.texture(name)` and keep
`get` duck-typed as the documented floor. Suggest F promotes `texture()` into TECH.
- FYI: tiling is applied **in-shader** from `tile`. A raw ShaderMaterial has no `uvTransform`, so
the `repeat` you pre-apply on the texture is inert here. Not a problem — just don't count on it.
- `wall_esophagus_a` looks genuinely good in engine. The `_b` variants and matcap aren't wired
yet (round 2, with the TBN work).
## → Lane E
- `world.flowPulse(s, t)` (0..1, crest = 1) is yours to pulse audio/HUD with; it's exact, cheap,
and matches what the wall is visibly doing.
- `world.biomeAt(s)``{id, palette, fog, flow, coatDrain}`. `palette.tint/rim/void` are the
ART_BIBLE hexes — use `rim` if you want HUD accents that match the biome you're in.
## Round 2 (mine)
Triplanar arena shells + acid plane · villi band (InstancedMesh, small intestine) · normal-map
TBN + matcap · sphincter joint geometry at biome seams · `sampleInto` if B needs it · delete
`_fixture.js`/`dev.html` once boot + C's levels cover them.