refactor(h3d-carve-1b): extract render-order, note-key, camera-bootstrap, fretMid to src/geometry.js

Move 9 symbols verbatim from screen.js factory scope to geometry.js:
- RENDER_ORDER_LAYER_STACK, RENDER_ORDER_LAYER_INDEX, RENDER_ORDER_AT_Z_ZERO,
  RENDER_ORDER_FAR_CLAMP — compile-time constants, now exported from geometry.js
- renderOrderForLayerAtZ — pure fn; reads K + RENDER_ORDER_* from geometry scope
- _noteKey, lowerBoundT — hot-path helpers; no external deps
- hwyFirstRelevantFrettedTime — camera bootstrap scan; no external deps
- fretMid → geoFretMid(f, uniform) — same fretX delegator pattern as Cut 1;
  screen.js keeps: const fretMid = f => geoFretMid(f, _h3dFretUniform); (1 beyond-subst)

screen.js import line updated to import all 9 new exports; tombstones replace
each original definition.

Source-scan retargets:
- highway_3d_render_order.test.js: layers()/zZeroRenderOrder() now read
  GEOMETRY_JS; the 5 renderOrderForLayerAtZ-internals asserts in
  chordFrameRenderOrder test retargeted to geo() (call-site assert stays on src()).
- highway_3d_camera_bootstrap.test.js: extractFn now reads geoSrc (GEOMETRY_JS);
  sourceBetween wiring tests remain on SCREEN_JS.
- highway_3d_panel_controls.test.js: sandbox stubs extended with 9 new names.

Class-killer tests added to highway_3d_geometry.test.js (8 new tests, 180 total):
- RENDER_ORDER_LAYER_STACK length + first/last entries
- RENDER_ORDER_LAYER_INDEX spot-checks (CHORD_FILL=0, NOTE_CORE=10)
- renderOrderForLayerAtZ far-clamp (worldZ=-5 gives 50, not 33 without max)
- renderOrderForLayerAtZ unknown-layer throws
- _noteKey |0 truncation (1.5,3)=150003 not float-derived 150008
- lowerBoundT strict lower-bound (3 in [{t:1},{t:3},{t:5}] gives 1, not 2)
- hwyFirstRelevantFrettedTime smoke (empty → null)
- geoFretMid sentinel (f=0 gives -2K≈-0.015, not 0) + ratio invariant

Mutation analysis confirmed all 8 tests fail under their named mutation before
committing (per Toby r1 lesson).

Base run (a1f7ad5): 172/172 pass.
Post-cut run: 180/180 pass.
Command: node --test tests/js/highway_3d*.test.js plugins/highway_3d/tests/*.test.js

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
This commit is contained in:
byrongamatos
2026-09-05 07:19:58 +02:00
co-authored by Claude Sonnet 4.6
parent a1f7ad5e68
commit 5e401afe87
7 changed files with 244 additions and 139 deletions
+5 -128
View File
@@ -6,7 +6,7 @@
// main player and per-panel in splitscreen without any architectural
// changes.
import { geoFretX, dZ, slideTrailEnd, camBaseDistU, camLowFretPullbackU, computeBPM, _makeGaussTex } from './src/geometry.js'; // h3d-carve-1
import { geoFretX, dZ, slideTrailEnd, camBaseDistU, camLowFretPullbackU, computeBPM, _makeGaussTex, RENDER_ORDER_LAYER_STACK, RENDER_ORDER_LAYER_INDEX, RENDER_ORDER_AT_Z_ZERO, RENDER_ORDER_FAR_CLAMP, renderOrderForLayerAtZ, _noteKey, lowerBoundT, hwyFirstRelevantFrettedTime, geoFretMid } from './src/geometry.js'; // h3d-carve-1b
(function () {
'use strict';
@@ -971,58 +971,8 @@ import { geoFretX, dZ, slideTrailEnd, camBaseDistU, camLowFretPullbackU, compute
/** Note travel speed. */
const TS = 230 * K;
const RENDER_ORDER_LAYER_STACK = Object.freeze([
'CHORD_FILL',
'CHORD_STRUM_FILL',
'CHORD_STRUM_LINE',
'SUSTAIN_TRAIL',
'CHORD_FRAME',
'CHORD_EDGE_GLOW',
'CONNECTOR_LINE',
'FRET_COLUMN',
'ARP_CONNECTOR_LINE',
'NOTE_OUTLINE',
'NOTE_CORE',
'TECHNIQUE_MARKER',
'BOARD_STRING',
'BOARD_FRET_WIRE',
'NOTE_FRET_LABEL',
'ARP_NOTE_FRET_LABEL',
'CHORD_FRET_LABEL',
]);
const RENDER_ORDER_LAYER_INDEX = Object.freeze(RENDER_ORDER_LAYER_STACK.reduce(
(indexByLayer, layerName, layerIndex) => {
indexByLayer[layerName] = layerIndex;
return indexByLayer;
},
Object.create(null)
));
const RENDER_ORDER_AT_Z_ZERO = 700;
const RENDER_ORDER_FAR_CLAMP = 50;
/**
* Computes renderOrder from world depth plus a named layer.
* Closer objects receive larger values and paint over farther objects; the
* layer stack breaks ties at the same depth, keeping labels above note gems.
*
* The layer index is added as a sub-unit fraction (< 1) so the integer
* depth bucket STRICTLY dominates: a farther object can never outrank a
* nearer one merely because it sits on a higher layer. Adding the raw index
* (0..N-1) directly would let the ~N-wide layer span leak across depth
* buckets and re-introduce far-over-near bleed for notes within ~N draw
* units of each other. Fraction granularity (1/N 0.06) stays well above
* the 0.0001 intra-element sub-increments used at some call sites.
*/
function renderOrderForLayerAtZ(worldZ, layerName) {
const layerIndex = RENDER_ORDER_LAYER_INDEX[layerName];
if (layerIndex === undefined) throw new Error(`Unknown 3D highway depth layer: ${layerName}`);
const depthRenderOrder = Math.max(
RENDER_ORDER_FAR_CLAMP,
Math.round(RENDER_ORDER_AT_Z_ZERO + worldZ / K)
);
return depthRenderOrder + layerIndex / RENDER_ORDER_LAYER_STACK.length;
}
// RENDER_ORDER_LAYER_STACK, RENDER_ORDER_LAYER_INDEX, RENDER_ORDER_AT_Z_ZERO,
// RENDER_ORDER_FAR_CLAMP, renderOrderForLayerAtZ — moved to src/geometry.js (h3d-carve-1b).
/** Match `nextNoteByString` onset to this note (float + chart rounding; avoids ghost / glow flicker). */
const NEXT_ON_STRING_T_EPS = 0.06;
@@ -1285,80 +1235,7 @@ import { geoFretX, dZ, slideTrailEnd, camBaseDistU, camLowFretPullbackU, compute
return cadence.slice(i0, i1);
}
// Fast integer key for (t, s) pairs — avoids per-frame string allocation in
// hot-path Set lookups. Encodes chart time in 0.1 ms steps (sufficient for
// chart-format note precision) combined with the string index.
// t range 0600 s → 06,000,000; * 10 + s(07) = max 60,000,007 < 2^53 ✓.
// The |0 truncates to int32 but the outer multiply stays in float64, so the
// key is always a safe JS integer for songs ≤ 214,748 s (well above any song).
function _noteKey(t, s) { return ((t * 10000 + 0.5) | 0) * 10 + s; }
// Binary lower-bound: returns the first index i in arr where arr[i].t >= t.
// Assumes arr is sorted ascending by .t (bundle.notes / bundle.chords always are).
// Byte-identical to core's bundle.lowerBoundT — kept as a local because this
// plugin must run on downlevel hosts whose bundles don't carry the helper
// (it's called from ~30 sites incl. top-level helpers that don't receive a
// bundle). New code that already holds a bundle should prefer
// bundle.lowerBoundT / bundle.lowerBoundTime.
function lowerBoundT(arr, t) {
let lo = 0, hi = arr.length;
while (lo < hi) {
const mid = (lo + hi) >>> 1;
if (arr[mid].t < t) lo = mid + 1;
else hi = mid;
}
return lo;
}
/**
* Return the chart time of the first fretted event that can still affect
* the camera at `now`, or the next fretted onset after it.
*
* This is intentionally a one-time full-chart scan. It runs only when a
* new song/arrangement's arrays first arrive, allowing the camera to frame
* the opening phrase during a silent intro instead of waiting for that
* phrase to enter the live targeting window. Open strings do not define a
* horizontal fret target, and malformed/out-of-range strings are ignored.
*
* Events already inside the behind-window, plus older sustains that are
* still ringing at `now`, return `now` so bootstrap framing matches the
* ordinary live path. Future events return their onset time.
*/
function hwyFirstRelevantFrettedTime(notes, chords, now, behind, stringCount) {
const nStrings = Number.isFinite(stringCount) ? Math.max(0, Math.floor(stringCount)) : 0;
const cameraFloor = now - Math.max(0, Number(behind) || 0);
let first = Infinity;
const validFretted = n => n
&& n.f > 0
&& Number.isInteger(n.s)
&& n.s >= 0
&& n.s < nStrings;
const consider = (eventTime, sustain) => {
const t = Number(eventTime);
if (!Number.isFinite(t)) return;
const sus = Number(sustain);
const end = t + (Number.isFinite(sus) && sus > 0 ? sus : 0);
if (t < cameraFloor && end < now) return;
const relevantTime = t <= now ? now : t;
if (relevantTime < first) first = relevantTime;
};
if (notes) {
for (const n of notes) {
if (validFretted(n)) consider(n.t, n.sus);
}
}
if (chords) {
for (const ch of chords) {
if (!ch || !ch.notes) continue;
for (const cn of ch.notes) {
if (validFretted(cn)) consider(ch.t, cn.sus);
}
}
}
return Number.isFinite(first) ? first : null;
}
// _noteKey, lowerBoundT, hwyFirstRelevantFrettedTime — moved to src/geometry.js (h3d-carve-1b).
// Last arrangement <anchor> at or before chart time `t` (sorted by .time).
// Mirrors static/highway.js getAnchorAt — until t reaches the first anchors
@@ -1540,7 +1417,7 @@ import { geoFretX, dZ, slideTrailEnd, camBaseDistU, camLowFretPullbackU, compute
_bgEmitChange('fretSpacing');
};
const fretMid = f => (f <= 0 ? -2 * K : (fretX(f - 1) + fretX(f)) / 2);
const fretMid = f => geoFretMid(f, _h3dFretUniform); // h3d-carve-1b: delegator (1 beyond-subst)
/** World-space width of fret column (wires f1 .. f); used to scale row markers past ~12. */
function fretColumnWorldW(f) {
const fi = Math.round(Number(f));