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
This commit is contained in:
byrongamatos
2026-09-05 06:50:52 +02:00
co-authored by Claude Sonnet 4.6
parent d5f622f31b
commit 84d33769b8
7 changed files with 266 additions and 96 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "highway_3d", "id": "highway_3d",
"name": "3D Highway", "name": "3D Highway",
"version": "3.35.0", "version": "3.36.0",
"type": "visualization", "type": "visualization",
"scriptType": "module", "scriptType": "module",
"bundled": true, "bundled": true,
+8 -85
View File
@@ -6,6 +6,8 @@
// main player and per-panel in splitscreen without any architectural // main player and per-panel in splitscreen without any architectural
// changes. // changes.
import { geoFretX, dZ, slideTrailEnd, camBaseDistU, camLowFretPullbackU, computeBPM, _makeGaussTex } from './src/geometry.js'; // h3d-carve-1
(function () { (function () {
'use strict'; 'use strict';
@@ -1510,22 +1512,11 @@
* Pure helpers * Pure helpers
* ====================================================================== */ * ====================================================================== */
// Logarithmic spacing — mirrors real guitar fret geometry (12th root of 2). // _fretXLog, _fretXUniStep, _fretXUni — moved to src/geometry.js (h3d-carve-1).
const _fretXLog = f => {
if (f <= 0) return 0;
const raw = FRET_SCALE - FRET_SCALE / Math.pow(2, f / 12);
if (f <= FRET_SPACING_ANCHOR_F) return raw;
const rawAnchor = FRET_SCALE - FRET_SCALE / Math.pow(2, FRET_SPACING_ANCHOR_F / 12);
return rawAnchor + (raw - rawAnchor) * FRET_SPACING_STRETCH_ABOVE12;
};
// Uniform spacing — same column width per fret (chart-format style).
// Total board width equals the logarithmic NFRETS position for consistency.
const _fretXUniStep = _fretXLog(NFRETS) / NFRETS;
const _fretXUni = f => f <= 0 ? 0 : f * _fretXUniStep;
let _h3dFretUniform = true; let _h3dFretUniform = true;
try { _h3dFretUniform = localStorage.getItem('highway_3d.fretSpacing') !== 'logarithmic'; } catch (_) {} try { _h3dFretUniform = localStorage.getItem('highway_3d.fretSpacing') !== 'logarithmic'; } catch (_) {}
const fretX = f => _h3dFretUniform ? _fretXUni(f) : _fretXLog(f); const fretX = f => geoFretX(f, _h3dFretUniform); // h3d-carve-1: delegator (1 beyond-subst)
window.h3dSetFretSpacing = mode => { window.h3dSetFretSpacing = mode => {
// Validate against the two supported modes before persisting so an // Validate against the two supported modes before persisting so an
@@ -1568,24 +1559,9 @@
const m = w / _fretLabelScaleRefW; const m = w / _fretLabelScaleRefW;
return Math.max(0.32, Math.min(1.45, m)); return Math.max(0.32, Math.min(1.45, m));
} }
const dZ = dt => -dt * TS; // dZ — moved to src/geometry.js (h3d-carve-1).
/** // slideTrailEnd — moved to src/geometry.js (h3d-carve-1).
* Pitched slide uses `sl`, unpitched uses `slu` (slide-to vs unpitched slide fields).
* Prefer `sl` when both are present matches RS wire.
* @returns {{ endFret: number, unpitched: boolean } | null}
*/
function slideTrailEnd(n) {
const sl = n.sl;
const slu = n.slu;
if (Number.isFinite(sl) && sl >= 0) {
return { endFret: sl | 0, unpitched: false };
}
if (Number.isFinite(slu) && slu >= 0) {
return { endFret: slu | 0, unpitched: true };
}
return null;
}
/** /**
* Lateral slide offset along the fretboard during sustain easing * Lateral slide offset along the fretboard during sustain easing
@@ -1605,15 +1581,7 @@
return (endX - startX) * w; return (endX - startX) * w;
} }
// Camera tgtDist building blocks. Both the dynamic (camera-follow) // camBaseDistU, camLowFretPullbackU — moved to src/geometry.js (h3d-carve-1).
// and locked (frets 1-12) branches compose tgtDist from these, so
// any future tuning of the base zoom curve or low-fret pullback
// lands in both branches without drift.
// span — camDistMax - camDistMin in fret-span units
// minFret — lowest fretted note in the camera window (or 1 for
// the locked branch, which assumes nut chords)
const camBaseDistU = span => 65 + Math.max(span, 4) * 3;
const camLowFretPullbackU = minFret => Math.max(0, 5 - minFret) * 4;
// World-units-per-fret near mid-neck. Used by the camera-X hysteresis // World-units-per-fret near mid-neck. Used by the camera-X hysteresis
// gate (issue #34) to convert a fret-equivalent dead zone into world // gate (issue #34) to convert a fret-equivalent dead zone into world
@@ -1632,52 +1600,7 @@
FRET_WIDTH_MID = fretX(7) - fretX(6); FRET_WIDTH_MID = fretX(7) - fretX(6);
} }
function computeBPM(beats, t) { // computeBPM, _makeGaussTex — moved to src/geometry.js (h3d-carve-1).
if (!beats || beats.length < 2) return 120;
let lo = 0, hi = beats.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (beats[mid].time < t) lo = mid + 1; else hi = mid;
}
let closest = lo;
if (lo === beats.length) closest = beats.length - 1;
else if (lo > 0 && Math.abs(beats[lo - 1].time - t) < Math.abs(beats[lo].time - t)) closest = lo - 1;
const start = Math.max(0, closest - 2);
const end = Math.min(beats.length - 1, closest + 2);
let sum = 0, count = 0;
for (let i = start; i < end; i++) {
const dt = beats[i + 1].time - beats[i].time;
if (dt > 0) { sum += dt; count++; }
}
return count > 0 && sum > 0 ? 60 / (sum / count) : 120;
}
// Build a horizontal gaussian DataTexture for the sustain-rail bloom effect.
// Returns a W×1 RGBA texture where alpha follows exp(-0.5*(u0.5)²/σ²),
// peaking at 1.0 in the centre. With the default σ=0.28 the edges retain
// ~0.20 alpha (not fully transparent) — a deliberately soft, wide falloff
// so the additive bloom fades gradually rather than cutting off sharply.
// Power-of-two width keeps WebGL mipmapping happy.
function _makeGaussTex(ThreeLib, w = 128, sigma = 0.28) {
const data = new Uint8Array(w * 4);
for (let i = 0; i < w; i++) {
const u = i / (w - 1);
const d = (u - 0.5) / sigma;
const v = Math.exp(-0.5 * d * d);
const a = Math.round(v * 255);
data[i * 4] = 255;
data[i * 4 + 1] = 255;
data[i * 4 + 2] = 255;
data[i * 4 + 3] = a;
}
const tex = new ThreeLib.DataTexture(data, w, 1, ThreeLib.RGBAFormat);
// LinearFilter on both axes so the bloom plane interpolates smoothly
// when scaled — the default NearestFilter causes visible banding.
tex.magFilter = ThreeLib.LinearFilter;
tex.minFilter = ThreeLib.LinearFilter;
tex.needsUpdate = true;
return tex;
}
/* ====================================================================== /* ======================================================================
* Three.js module lazily loaded, memoized * Three.js module lazily loaded, memoized
+137
View File
@@ -0,0 +1,137 @@
/**
* Pure geometry helpers — h3d-carve-1.
*
* All exports are stateless; they depend only on the compile-time constants
* below (which mirror their factory-scope counterparts in screen.js verbatim
* and never vary at runtime). No DOM, no Three.js imports, no side-effects.
*
* screen.js keeps a 1-arg delegator:
* const fretX = f => geoFretX(f, _h3dFretUniform);
* so no call site in screen.js changes.
*/
// ── Compile-time constants (mirror screen.js; never vary at runtime) ─────────
const SCALE = 2.25;
const K = SCALE / 300;
// Horizontal stretch factor for fret X positions.
const FRET_SCALE = SCALE * 1.1;
const NFRETS = 24;
/**
* Pure 12-semitone spacing compresses toward the bridge; multiply each
* segment above this fret by the factor so high positions stay
* slightly more playable/readable in 3D.
*/
const FRET_SPACING_STRETCH_ABOVE12 = 1.1;
const FRET_SPACING_ANCHOR_F = 12;
/** Note travel speed. */
const TS = 230 * K;
// ── Fret X ───────────────────────────────────────────────────────────────────
// Logarithmic spacing — mirrors real guitar fret geometry (12th root of 2).
const _fretXLog = f => {
if (f <= 0) return 0;
const raw = FRET_SCALE - FRET_SCALE / Math.pow(2, f / 12);
if (f <= FRET_SPACING_ANCHOR_F) return raw;
const rawAnchor = FRET_SCALE - FRET_SCALE / Math.pow(2, FRET_SPACING_ANCHOR_F / 12);
return rawAnchor + (raw - rawAnchor) * FRET_SPACING_STRETCH_ABOVE12;
};
// Uniform spacing — same column width per fret (chart-format style).
// Total board width equals the logarithmic NFRETS position for consistency.
const _fretXUniStep = _fretXLog(NFRETS) / NFRETS;
const _fretXUni = f => f <= 0 ? 0 : f * _fretXUniStep;
/**
* World-space X position for fret `f`.
* @param {number} f fret number
* @param {boolean} uniform true → uniform (chart-format) spacing; false → logarithmic
*/
export const geoFretX = (f, uniform) => uniform ? _fretXUni(f) : _fretXLog(f);
// ── Time → Z ─────────────────────────────────────────────────────────────────
/** Convert a time delta (seconds) to a world-space Z offset (notes travel toward Z). */
export const dZ = dt => -dt * TS;
// ── Slide trail ───────────────────────────────────────────────────────────────
/**
* Pitched slide uses `sl`, unpitched uses `slu` (slide-to vs unpitched slide fields).
* Prefer `sl` when both are present — matches RS wire.
* @returns {{ endFret: number, unpitched: boolean } | null}
*/
export function slideTrailEnd(n) {
const sl = n.sl;
const slu = n.slu;
if (Number.isFinite(sl) && sl >= 0) {
return { endFret: sl | 0, unpitched: false };
}
if (Number.isFinite(slu) && slu >= 0) {
return { endFret: slu | 0, unpitched: true };
}
return null;
}
// ── Camera distance building blocks ──────────────────────────────────────────
// Camera tgtDist building blocks. Both the dynamic (camera-follow)
// and locked (frets 1-12) branches compose tgtDist from these, so
// any future tuning of the base zoom curve or low-fret pullback
// lands in both branches without drift.
// span — camDistMax - camDistMin in fret-span units
// minFret — lowest fretted note in the camera window (or 1 for
// the locked branch, which assumes nut chords)
export const camBaseDistU = span => 65 + Math.max(span, 4) * 3;
export const camLowFretPullbackU = minFret => Math.max(0, 5 - minFret) * 4;
// ── BPM estimation ────────────────────────────────────────────────────────────
export function computeBPM(beats, t) {
if (!beats || beats.length < 2) return 120;
let lo = 0, hi = beats.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (beats[mid].time < t) lo = mid + 1; else hi = mid;
}
let closest = lo;
if (lo === beats.length) closest = beats.length - 1;
else if (lo > 0 && Math.abs(beats[lo - 1].time - t) < Math.abs(beats[lo].time - t)) closest = lo - 1;
const start = Math.max(0, closest - 2);
const end = Math.min(beats.length - 1, closest + 2);
let sum = 0, count = 0;
for (let i = start; i < end; i++) {
const dt = beats[i + 1].time - beats[i].time;
if (dt > 0) { sum += dt; count++; }
}
return count > 0 && sum > 0 ? 60 / (sum / count) : 120;
}
// ── Gaussian bloom texture ────────────────────────────────────────────────────
// Build a horizontal gaussian DataTexture for the sustain-rail bloom effect.
// Returns a W×1 RGBA texture where alpha follows exp(-0.5*(u0.5)²/σ²),
// peaking at 1.0 in the centre. With the default σ=0.28 the edges retain
// ~0.20 alpha (not fully transparent) — a deliberately soft, wide falloff
// so the additive bloom fades gradually rather than cutting off sharply.
// Power-of-two width keeps WebGL mipmapping happy.
export function _makeGaussTex(ThreeLib, w = 128, sigma = 0.28) {
const data = new Uint8Array(w * 4);
for (let i = 0; i < w; i++) {
const u = i / (w - 1);
const d = (u - 0.5) / sigma;
const v = Math.exp(-0.5 * d * d);
const a = Math.round(v * 255);
data[i * 4] = 255;
data[i * 4 + 1] = 255;
data[i * 4 + 2] = 255;
data[i * 4 + 3] = a;
}
const tex = new ThreeLib.DataTexture(data, w, 1, ThreeLib.RGBAFormat);
// LinearFilter on both axes so the bloom plane interpolates smoothly
// when scaled — the default NearestFilter causes visible banding.
tex.magFilter = ThreeLib.LinearFilter;
tex.minFilter = ThreeLib.LinearFilter;
tex.needsUpdate = true;
return tex;
}
+16 -5
View File
@@ -2,8 +2,12 @@
// The board can render fret columns either Uniform (equal width, the chart // The board can render fret columns either Uniform (equal width, the chart
// Remastered style) or Logarithmic (real instrument geometry), switchable at // Remastered style) or Logarithmic (real instrument geometry), switchable at
// runtime via window.h3dSetFretSpacing and persisted in localStorage. A // runtime via window.h3dSetFretSpacing and persisted in localStorage. A
// refactor that renames the storage key, drops the uniform/log branch in // refactor that renames the storage key, drops the delegator in fretX, or
// fretX, or stops validating the mode would silently regress the setting. // stops validating the mode would silently regress the setting.
//
// Since h3d-carve-1, the uniform/log branch lives in src/geometry.js
// (geoFretX); screen.js keeps a 1-arg delegator:
// const fretX = f => geoFretX(f, _h3dFretUniform);
// //
// Source-level only — same strategy as the other tests/js/ files. // Source-level only — same strategy as the other tests/js/ files.
@@ -13,6 +17,7 @@ const fs = require('node:fs');
const path = require('node:path'); const path = require('node:path');
const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js'); const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js');
const GEOMETRY_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'geometry.js');
test('fret-spacing mode is read from the highway_3d.fretSpacing localStorage key', () => { test('fret-spacing mode is read from the highway_3d.fretSpacing localStorage key', () => {
const src = fs.readFileSync(SCREEN_JS, 'utf8'); const src = fs.readFileSync(SCREEN_JS, 'utf8');
@@ -23,12 +28,18 @@ test('fret-spacing mode is read from the highway_3d.fretSpacing localStorage key
); );
}); });
test('fretX switches between the uniform and logarithmic implementations', () => { test('fretX is a 1-arg delegator to geoFretX in screen.js (h3d-carve-1)', () => {
const src = fs.readFileSync(SCREEN_JS, 'utf8'); const src = fs.readFileSync(SCREEN_JS, 'utf8');
assert.match( assert.match(
src, src,
/const\s+fretX\s*=\s*f\s*=>\s*_h3dFretUniform\s*\?\s*_fretXUni\(f\)\s*:\s*_fretXLog\(f\)/, /const\s+fretX\s*=\s*f\s*=>\s*geoFretX\(\s*f\s*,\s*_h3dFretUniform\s*\)/,
'fretX must pick _fretXUni when _h3dFretUniform else _fretXLog', 'screen.js fretX must delegate to geoFretX(f, _h3dFretUniform) from geometry.js',
);
const geo = fs.readFileSync(GEOMETRY_JS, 'utf8');
assert.match(
geo,
/export\s+const\s+geoFretX\s*=\s*\(\s*f\s*,\s*uniform\s*\)\s*=>/,
'geometry.js must export geoFretX as a 2-arg function (fret, uniform)',
); );
}); });
+80
View File
@@ -0,0 +1,80 @@
// 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}`);
});
+14 -1
View File
@@ -28,7 +28,11 @@ function loadHighway3dStatics() {
1, 1,
'expected exactly one factory-registration anchor in screen.js', 'expected exactly one factory-registration anchor in screen.js',
); );
const instrumented = src.replace( // Since h3d-carve-1 screen.js starts with an ES module import statement.
// vm.runInContext does not support static import — strip the import line
// and provide stub implementations of the geometry exports in the sandbox.
const stripped = src.replace(/^import\s+\{[^}]+\}\s+from\s+['"][^'"]+['"];\s*\/\/[^\n]*\n/m, '');
const instrumented = stripped.replace(
ANCHOR, ANCHOR,
`${ANCHOR}\n window.__h3dTestExports = { BG_DEFAULTS };`, `${ANCHOR}\n window.__h3dTestExports = { BG_DEFAULTS };`,
); );
@@ -50,6 +54,15 @@ function loadHighway3dStatics() {
register() {}, register() {},
}, },
}, },
// Geometry stubs — panel-controls test only reads factory statics;
// it never invokes the render path where these are called (h3d-carve-1).
geoFretX: (f, _uniform) => f * 0.1,
dZ: dt => -dt,
slideTrailEnd: () => null,
camBaseDistU: span => span,
camLowFretPullbackU: () => 0,
computeBPM: () => 120,
_makeGaussTex: () => ({}),
}; };
vm.createContext(sandbox); vm.createContext(sandbox);
vm.runInContext(instrumented, sandbox, { filename: SCREEN_JS }); vm.runInContext(instrumented, sandbox, { filename: SCREEN_JS });
+10 -4
View File
@@ -5,6 +5,10 @@
// stops using additive blending, or bumps the bloom renderOrder above the // stops using additive blending, or bumps the bloom renderOrder above the
// core rail (16) would silently regress or invert the effect. // core rail (16) would silently regress or invert the effect.
// //
// Since h3d-carve-1, _makeGaussTex is defined in src/geometry.js and
// imported into screen.js; the call site (_bloomGaussTex = _makeGaussTex(...))
// remains in screen.js.
//
// Source-level only — same strategy as the other tests/js/ files. // Source-level only — same strategy as the other tests/js/ files.
const { test } = require('node:test'); const { test } = require('node:test');
@@ -13,14 +17,16 @@ const fs = require('node:fs');
const path = require('node:path'); const path = require('node:path');
const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js'); const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js');
const GEOMETRY_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'geometry.js');
test('a gaussian DataTexture helper (_makeGaussTex) drives the bloom falloff', () => { test('a gaussian DataTexture helper (_makeGaussTex) drives the bloom falloff', () => {
const src = fs.readFileSync(SCREEN_JS, 'utf8'); const geo = fs.readFileSync(GEOMETRY_JS, 'utf8');
assert.match( assert.match(
src, geo,
/function\s+_makeGaussTex\s*\(/, /export\s+function\s+_makeGaussTex\s*\(/,
'_makeGaussTex must exist to build the bloom gaussian texture', '_makeGaussTex must be exported from geometry.js to build the bloom gaussian texture',
); );
const src = fs.readFileSync(SCREEN_JS, 'utf8');
assert.match( assert.match(
src, src,
/_bloomGaussTex\s*=\s*_makeGaussTex\(/, /_bloomGaussTex\s*=\s*_makeGaussTex\(/,