mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-11 03:04:30 +00:00
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:
co-authored by
Claude Sonnet 4.6
parent
a1f7ad5e68
commit
5e401afe87
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "highway_3d",
|
||||
"name": "3D Highway",
|
||||
"version": "3.36.0",
|
||||
"version": "3.37.0",
|
||||
"type": "visualization",
|
||||
"scriptType": "module",
|
||||
"bundled": true,
|
||||
|
||||
@@ -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 0–600 s → 0–6,000,000; * 10 + s(0–7) = 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 anchor’s
|
||||
@@ -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 f−1 .. f); used to scale row markers past ~12. */
|
||||
function fretColumnWorldW(f) {
|
||||
const fi = Math.round(Number(f));
|
||||
|
||||
@@ -107,6 +107,150 @@ export function computeBPM(beats, t) {
|
||||
return count > 0 && sum > 0 ? 60 / (sum / count) : 120;
|
||||
}
|
||||
|
||||
// ── Render-order layer stack — h3d-carve-1b ───────────────────────────────────
|
||||
|
||||
export 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',
|
||||
]);
|
||||
export const RENDER_ORDER_LAYER_INDEX = Object.freeze(RENDER_ORDER_LAYER_STACK.reduce(
|
||||
(indexByLayer, layerName, layerIndex) => {
|
||||
indexByLayer[layerName] = layerIndex;
|
||||
return indexByLayer;
|
||||
},
|
||||
Object.create(null)
|
||||
));
|
||||
|
||||
export const RENDER_ORDER_AT_Z_ZERO = 700;
|
||||
export 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.
|
||||
*/
|
||||
export 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;
|
||||
}
|
||||
|
||||
// ── Note key and binary search — h3d-carve-1b ─────────────────────────────────
|
||||
|
||||
// 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 0–600 s → 0–6,000,000; * 10 + s(0–7) = 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).
|
||||
export 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.
|
||||
export 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.
|
||||
*/
|
||||
export 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;
|
||||
}
|
||||
|
||||
// ── Fret mid — h3d-carve-1b ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* World-space X of the midpoint of fret column f.
|
||||
* f <= 0 → nut-side sentinel (−2K).
|
||||
* screen.js keeps `const fretMid = f => geoFretMid(f, _h3dFretUniform);`
|
||||
* so no call-site changes in screen.js.
|
||||
* @param {number} f fret number
|
||||
* @param {boolean} uniform true → uniform spacing; false → logarithmic
|
||||
*/
|
||||
export const geoFretMid = (f, uniform) => f <= 0 ? -2 * K : (geoFretX(f - 1, uniform) + geoFretX(f, uniform)) / 2;
|
||||
|
||||
// ── Gaussian bloom texture ────────────────────────────────────────────────────
|
||||
|
||||
// Build a horizontal gaussian DataTexture for the sustain-rail bloom effect.
|
||||
|
||||
@@ -13,6 +13,9 @@ const path = require('node:path');
|
||||
|
||||
const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js');
|
||||
const src = fs.readFileSync(SCREEN_JS, 'utf8');
|
||||
// Since h3d-carve-1b, hwyFirstRelevantFrettedTime lives in geometry.js.
|
||||
const GEOMETRY_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'geometry.js');
|
||||
const geoSrc = fs.readFileSync(GEOMETRY_JS, 'utf8');
|
||||
|
||||
function extractFn(source, name) {
|
||||
const start = source.indexOf('function ' + name);
|
||||
@@ -36,7 +39,7 @@ function sourceBetween(startText, endText) {
|
||||
|
||||
const hwyFirstRelevantFrettedTime = new Function(
|
||||
'"use strict";'
|
||||
+ extractFn(src, 'hwyFirstRelevantFrettedTime')
|
||||
+ extractFn(geoSrc, 'hwyFirstRelevantFrettedTime')
|
||||
+ '\nreturn hwyFirstRelevantFrettedTime;',
|
||||
)();
|
||||
|
||||
|
||||
@@ -99,6 +99,67 @@ test('camLowFretPullbackU is clamped to zero — high fret gives 0 not negative'
|
||||
assert.strictEqual(camLowFretPullbackU(10), 0, 'fret 10: clamped to 0, not -20');
|
||||
});
|
||||
|
||||
// ── Cut 1b class-killers ───────────────────────────────────────────────────────
|
||||
|
||||
test('RENDER_ORDER_LAYER_STACK has 17 layers with CHORD_FILL first and CHORD_FRET_LABEL last', async () => {
|
||||
const { RENDER_ORDER_LAYER_STACK } = await import(GEOMETRY_JS);
|
||||
assert.strictEqual(RENDER_ORDER_LAYER_STACK.length, 17, 'stack must have exactly 17 layers');
|
||||
assert.strictEqual(RENDER_ORDER_LAYER_STACK[0], 'CHORD_FILL', 'first layer must be CHORD_FILL');
|
||||
assert.strictEqual(RENDER_ORDER_LAYER_STACK[RENDER_ORDER_LAYER_STACK.length - 1], 'CHORD_FRET_LABEL', 'last layer must be CHORD_FRET_LABEL');
|
||||
});
|
||||
|
||||
test('RENDER_ORDER_LAYER_INDEX maps CHORD_FILL to 0 and NOTE_CORE to 10', async () => {
|
||||
// Mutation: wrong layer order → NOTE_CORE would not map to 10.
|
||||
const { RENDER_ORDER_LAYER_INDEX } = await import(GEOMETRY_JS);
|
||||
assert.strictEqual(RENDER_ORDER_LAYER_INDEX['CHORD_FILL'], 0, 'CHORD_FILL must be index 0 (bottom of stack)');
|
||||
assert.strictEqual(RENDER_ORDER_LAYER_INDEX['NOTE_CORE'], 10, 'NOTE_CORE must be index 10');
|
||||
});
|
||||
|
||||
test('renderOrderForLayerAtZ applies the far clamp — worldZ=-5 gives 50 not 33', async () => {
|
||||
// Mutation: remove Math.max(RENDER_ORDER_FAR_CLAMP, ...) clamp.
|
||||
// K=2.25/300=0.0075; Math.round(700+(-5)/0.0075)=Math.round(33.33)=33; max(50,33)=50.
|
||||
// Without clamp: 33 + 0/17 ≈ 33. Test pins the clamped value.
|
||||
const { renderOrderForLayerAtZ } = await import(GEOMETRY_JS);
|
||||
assert.strictEqual(renderOrderForLayerAtZ(-5, 'CHORD_FILL'), 50, 'far objects must be clamped to RENDER_ORDER_FAR_CLAMP=50');
|
||||
});
|
||||
|
||||
test('renderOrderForLayerAtZ throws for unknown layer names', async () => {
|
||||
const { renderOrderForLayerAtZ } = await import(GEOMETRY_JS);
|
||||
assert.throws(() => renderOrderForLayerAtZ(0, 'NONEXISTENT'), /Unknown 3D highway depth layer/);
|
||||
});
|
||||
|
||||
test('_noteKey integer-truncates float times — _noteKey(1.5, 3) is 150003 not 150008', async () => {
|
||||
// Mutation: drop |0 → (15000.5)*10+3 = 150008.
|
||||
const { _noteKey } = await import(GEOMETRY_JS);
|
||||
assert.strictEqual(_noteKey(1.5, 3), 150003, '|0 truncation must give 150003, not float-derived 150008');
|
||||
assert.strictEqual(_noteKey(0, 0), 0);
|
||||
});
|
||||
|
||||
test('lowerBoundT returns first index where arr[i].t >= t (strict lower bound)', async () => {
|
||||
// Mutation: < → <= causes lowerBoundT([{t:1},{t:3},{t:5}], 3) → 2 instead of 1.
|
||||
const { lowerBoundT } = await import(GEOMETRY_JS);
|
||||
const arr = [{ t: 1 }, { t: 3 }, { t: 5 }];
|
||||
assert.strictEqual(lowerBoundT(arr, 3), 1, 'strict lower-bound: first index where .t >= 3 is 1 (not 2)');
|
||||
assert.strictEqual(lowerBoundT(arr, 0), 0, 'value before all: must return 0');
|
||||
assert.strictEqual(lowerBoundT(arr, 6), 3, 'value after all: must return length');
|
||||
});
|
||||
|
||||
test('hwyFirstRelevantFrettedTime returns null for empty/all-open input', async () => {
|
||||
const { hwyFirstRelevantFrettedTime } = await import(GEOMETRY_JS);
|
||||
assert.strictEqual(hwyFirstRelevantFrettedTime([], [], 0, 0.2, 6), null);
|
||||
});
|
||||
|
||||
test('geoFretMid returns -2K sentinel for f<=0, positive for f=1', async () => {
|
||||
// Mutation: drop f<=0 guard → geoFretMid(0, true) returns (0+0)/2=0, not -0.015.
|
||||
const { geoFretMid } = await import(GEOMETRY_JS);
|
||||
const K = 2.25 / 300;
|
||||
assert.ok(Math.abs(geoFretMid(0, true) - (-2 * K)) < 1e-10, 'f=0 must return -2K sentinel (≈-0.015)');
|
||||
assert.ok(geoFretMid(1, true) > 0, 'f=1 must return positive X');
|
||||
// In uniform mode: geoFretX(0,true)=0, geoFretX(1,true)=step, so mid=step/2.
|
||||
// geoFretMid(2,true) = (step+2step)/2 = 1.5step. Ratio 3 catches wrong f offset.
|
||||
assert.ok(Math.abs(geoFretMid(2, true) / geoFretMid(1, true) - 3) < 1e-9, 'uniform mid(2)/mid(1) must equal 3');
|
||||
});
|
||||
|
||||
test('_makeGaussTex peak alpha is 255 at the centre pixel', async () => {
|
||||
// Mutation: default sigma changed to 0 → (u-0.5)/0 = NaN chain → all Uint8Array writes
|
||||
// become 0 (TypedArray coerces NaN to 0). Test calls without explicit sigma so the
|
||||
|
||||
@@ -55,7 +55,7 @@ function loadHighway3dStatics() {
|
||||
},
|
||||
},
|
||||
// Geometry stubs — panel-controls test only reads factory statics;
|
||||
// it never invokes the render path where these are called (h3d-carve-1).
|
||||
// it never invokes the render path where these are called (h3d-carve-1b).
|
||||
geoFretX: (f, _uniform) => f * 0.1,
|
||||
dZ: dt => -dt,
|
||||
slideTrailEnd: () => null,
|
||||
@@ -63,6 +63,15 @@ function loadHighway3dStatics() {
|
||||
camLowFretPullbackU: () => 0,
|
||||
computeBPM: () => 120,
|
||||
_makeGaussTex: () => ({}),
|
||||
RENDER_ORDER_LAYER_STACK: Object.freeze([]),
|
||||
RENDER_ORDER_LAYER_INDEX: Object.freeze(Object.create(null)),
|
||||
RENDER_ORDER_AT_Z_ZERO: 700,
|
||||
RENDER_ORDER_FAR_CLAMP: 50,
|
||||
renderOrderForLayerAtZ: () => 0,
|
||||
_noteKey: () => 0,
|
||||
lowerBoundT: () => 0,
|
||||
hwyFirstRelevantFrettedTime: () => null,
|
||||
geoFretMid: (f, _uniform) => f * 0.1,
|
||||
};
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(instrumented, sandbox, { filename: SCREEN_JS });
|
||||
|
||||
@@ -41,6 +41,9 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js');
|
||||
// Since h3d-carve-1b, RENDER_ORDER_* constants and renderOrderForLayerAtZ
|
||||
// live in geometry.js; screen.js imports them.
|
||||
const GEOMETRY_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'geometry.js');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -53,9 +56,16 @@ function src() {
|
||||
return _src;
|
||||
}
|
||||
|
||||
/** Parses the declared render-order layer stack from screen.js. */
|
||||
let _geo;
|
||||
/** Returns the cached geometry source (render-order constants + renderOrderForLayerAtZ). */
|
||||
function geo() {
|
||||
if (!_geo) _geo = fs.readFileSync(GEOMETRY_JS, 'utf8');
|
||||
return _geo;
|
||||
}
|
||||
|
||||
/** Parses the declared render-order layer stack from geometry.js. */
|
||||
function layers() {
|
||||
const match = src().match(/const\s+RENDER_ORDER_LAYER_STACK\s*=\s*Object\.freeze\(\s*\[([\s\S]*?)\]\s*\)/);
|
||||
const match = geo().match(/const\s+RENDER_ORDER_LAYER_STACK\s*=\s*Object\.freeze\(\s*\[([\s\S]*?)\]\s*\)/);
|
||||
assert.ok(match, 'RENDER_ORDER_LAYER_STACK must be declared');
|
||||
return Array.from(match[1].matchAll(/'([^']+)'/g), m => m[1]);
|
||||
}
|
||||
@@ -70,7 +80,7 @@ function layerIndex(name) {
|
||||
|
||||
/** Reads the render-order base used for objects at z = 0. */
|
||||
function zZeroRenderOrder() {
|
||||
const match = src().match(/const\s+RENDER_ORDER_AT_Z_ZERO\s*=\s*(-?\d+(?:\.\d+)?)\s*;/);
|
||||
const match = geo().match(/const\s+RENDER_ORDER_AT_Z_ZERO\s*=\s*(-?\d+(?:\.\d+)?)\s*;/);
|
||||
assert.ok(match, 'RENDER_ORDER_AT_Z_ZERO must be declared');
|
||||
return Number(match[1]);
|
||||
}
|
||||
@@ -301,14 +311,15 @@ test('chordFrameRenderOrder uses renderOrderForLayerAtZ(z, CHORD_FRAME)', () =>
|
||||
/const\s+chordFrameRenderOrder\s*=\s*renderOrderForLayerAtZ\(\s*z\s*,\s*'CHORD_FRAME'\s*\)\s*;/,
|
||||
'chordFrameRenderOrder must use renderOrderForLayerAtZ(z, CHORD_FRAME)',
|
||||
);
|
||||
assert.match(src(), /const\s+RENDER_ORDER_LAYER_INDEX\s*=\s*Object\.freeze\(\s*RENDER_ORDER_LAYER_STACK\.reduce\(/);
|
||||
assert.match(src(), /const\s+layerIndex\s*=\s*RENDER_ORDER_LAYER_INDEX\[layerName\]\s*;/);
|
||||
assert.match(src(), /if\s*\(\s*layerIndex\s*===\s*undefined\s*\)\s*throw\s+new\s+Error\(`Unknown 3D highway depth layer: \$\{layerName\}`\)\s*;/);
|
||||
assert.match(src(), /const\s+depthRenderOrder\s*=\s*Math\.max\(\s*RENDER_ORDER_FAR_CLAMP\s*,\s*Math\.round\(\s*RENDER_ORDER_AT_Z_ZERO\s*\+\s*worldZ\s*\/\s*K\s*\)\s*\)\s*;/);
|
||||
// renderOrderForLayerAtZ implementation lives in geometry.js since h3d-carve-1b.
|
||||
assert.match(geo(), /const\s+RENDER_ORDER_LAYER_INDEX\s*=\s*Object\.freeze\(\s*RENDER_ORDER_LAYER_STACK\.reduce\(/);
|
||||
assert.match(geo(), /const\s+layerIndex\s*=\s*RENDER_ORDER_LAYER_INDEX\[layerName\]\s*;/);
|
||||
assert.match(geo(), /if\s*\(\s*layerIndex\s*===\s*undefined\s*\)\s*throw\s+new\s+Error\(`Unknown 3D highway depth layer: \$\{layerName\}`\)\s*;/);
|
||||
assert.match(geo(), /const\s+depthRenderOrder\s*=\s*Math\.max\(\s*RENDER_ORDER_FAR_CLAMP\s*,\s*Math\.round\(\s*RENDER_ORDER_AT_Z_ZERO\s*\+\s*worldZ\s*\/\s*K\s*\)\s*\)\s*;/);
|
||||
// Layer is a sub-unit fraction so the integer depth bucket strictly
|
||||
// dominates (a farther object can't outrank a nearer one via a higher
|
||||
// layer); the layer only breaks ties within the same depth bucket.
|
||||
assert.match(src(), /return\s+depthRenderOrder\s*\+\s*layerIndex\s*\/\s*RENDER_ORDER_LAYER_STACK\.length\s*;/);
|
||||
assert.match(geo(), /return\s+depthRenderOrder\s*\+\s*layerIndex\s*\/\s*RENDER_ORDER_LAYER_STACK\.length\s*;/);
|
||||
assert.ok(layerIndex('CHORD_FRAME') < layerIndex('NOTE_OUTLINE'));
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user