Files
feedBack/tests/js/highway_3d_geometry.test.js
T
byrongamatosandClaude Sonnet 4.6 84d33769b8 refactor(h3d-carve-1): extract pure geometry helpers to src/geometry.js
Moves 7 exports from screen.js to a new ES module src/geometry.js:
  geoFretX (was fretX, now 2-arg), dZ, slideTrailEnd, camBaseDistU,
  camLowFretPullbackU, computeBPM, _makeGaussTex.

Internal helpers _fretXLog / _fretXUniStep / _fretXUni are module-private
inside geometry.js. Module-level constants (SCALE, K, FRET_SCALE, NFRETS,
FRET_SPACING_*, TS) are duplicated at geometry.js module scope — values
are compile-time and never vary at runtime.

screen.js keeps a 1-arg delegator (1 beyond-subst):
  const fretX = f => geoFretX(f, _h3dFretUniform);
No call site changes; fretMid / fretColumnWorldW / slideOffsetWorldX /
_recomputeFretSpacingDerived stay in screen.js and use the delegator.

Tests:
- highway_3d_geometry.test.js: class-killer via import() with 9
  known-answer assertions (geoFretX uniform/log invariants, dZ linearity,
  slideTrailEnd, computeBPM BPM estimate).
- highway_3d_fret_spacing: test updated to match delegator pattern + adds
  GEOMETRY_JS read to verify geoFretX export.
- highway_3d_sustain_bloom: retargeted _makeGaussTex check to GEOMETRY_JS
  (definition moved); call-site check stays on SCREEN_JS.
- highway_3d_panel_controls: vm loader strips ES import line and injects
  geometry stubs; all factory-statics tests unaffected.

Suite: 169/169 pass
  node --test tests/js/highway_3d*.test.js plugins/highway_3d/tests/*.test.js
Version bump: 3.35.0 -> 3.36.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
2026-09-05 06:50:52 +02:00

81 lines
3.9 KiB
JavaScript
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.
// Class-killer for src/geometry.js — h3d-carve-1.
//
// Uses dynamic import() (not the vm source-scan pattern) so Node actually
// evaluates the ES module and its exports are the real runtime values.
// A refactor that renames geoFretX, changes the uniform/logarithmic
// decision, removes slideTrailEnd, or breaks computeBPM's BPM estimate
// would be caught here before any other test.
const { test } = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const GEOMETRY_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'geometry.js');
test('geoFretX returns 0 for fret 0 in both modes', async () => {
const { geoFretX } = await import(GEOMETRY_JS);
assert.strictEqual(geoFretX(0, true), 0, 'uniform: fret 0 must be 0');
assert.strictEqual(geoFretX(0, false), 0, 'logarithmic: fret 0 must be 0');
});
test('geoFretX uniform spacing is linear — fret N is N × fret 1', async () => {
const { geoFretX } = await import(GEOMETRY_JS);
const step = geoFretX(1, true);
assert.ok(step > 0, 'uniform step must be positive');
assert.ok(Math.abs(geoFretX(5, true) - 5 * step) < 1e-9, 'fret 5 must be 5 × step');
assert.ok(Math.abs(geoFretX(12, true) - 12 * step) < 1e-9, 'fret 12 must be 12 × step');
});
test('geoFretX logarithmic spacing is non-linear — frets compress toward the bridge', async () => {
const { geoFretX } = await import(GEOMETRY_JS);
const d1 = geoFretX(1, false);
const d2 = geoFretX(2, false) - geoFretX(1, false);
const d3 = geoFretX(3, false) - geoFretX(2, false);
assert.ok(d1 > d2, 'fret 1 gap must be wider than fret 2 gap (compression toward bridge)');
assert.ok(d2 > d3, 'fret 2 gap must be wider than fret 3 gap');
});
test('geoFretX uniform and logarithmic agree at fret 24 (total board width)', async () => {
const { geoFretX } = await import(GEOMETRY_JS);
// By construction: _fretXUniStep = _fretXLog(24) / 24, so geoFretX(24, uniform)
// equals geoFretX(24, logarithmic). This is the board-width invariant.
const uniWidth = geoFretX(24, true);
const logWidth = geoFretX(24, false);
assert.ok(Math.abs(uniWidth - logWidth) < 1e-9, 'board width must be identical in both modes');
});
test('dZ converts positive dt to a negative Z delta', async () => {
const { dZ } = await import(GEOMETRY_JS);
assert.ok(dZ(1) < 0, 'positive time delta must produce negative Z (notes travel toward camera)');
assert.ok(dZ(0) === 0, 'zero dt must produce zero dZ');
assert.ok(Math.abs(dZ(2) / dZ(1) - 2) < 1e-9, 'dZ must be linear in dt');
});
test('slideTrailEnd returns null for notes with no slide fields', async () => {
const { slideTrailEnd } = await import(GEOMETRY_JS);
assert.strictEqual(slideTrailEnd({}), null);
assert.strictEqual(slideTrailEnd({ sl: -1 }), null, 'negative sl must be ignored');
});
test('slideTrailEnd prefers sl over slu and marks pitched/unpitched correctly', async () => {
const { slideTrailEnd } = await import(GEOMETRY_JS);
assert.deepStrictEqual(slideTrailEnd({ sl: 7 }), { endFret: 7, unpitched: false });
assert.deepStrictEqual(slideTrailEnd({ slu: 5 }), { endFret: 5, unpitched: true });
assert.deepStrictEqual(slideTrailEnd({ sl: 7, slu: 5 }), { endFret: 7, unpitched: false });
});
test('computeBPM returns 120 for degenerate inputs', async () => {
const { computeBPM } = await import(GEOMETRY_JS);
assert.strictEqual(computeBPM(null, 0), 120);
assert.strictEqual(computeBPM([], 0), 120);
assert.strictEqual(computeBPM([{ time: 0 }], 0), 120, 'single beat has no interval');
});
test('computeBPM estimates 120 BPM from evenly-spaced beats', async () => {
const { computeBPM } = await import(GEOMETRY_JS);
// 120 BPM = 0.5 s per beat
const beats = [0, 0.5, 1.0, 1.5, 2.0].map(time => ({ time }));
const bpm = computeBPM(beats, 1.0);
assert.ok(Math.abs(bpm - 120) < 0.01, `expected ~120 BPM, got ${bpm}`);
});