feat(h3d-carve-15): extract U-section (per-frame renderer) into src/renderer.js

- createRenderer factory DI: 241 params (consts, fn-refs, 33 pool getters,
  material/settings/camera/ND getters, 35 setters)
- screen.js: tombstone + createRenderer wiring after createNoteRenderer +
  createCamera wirings (§1 ruling: createNoteRenderer called from screen.js)
- Restore createArp wiring + _resetStringDependentCaches to screen.js
  (accidentally dropped during carve; both needed in IIFE scope)
- smoothNow: correction 3 — setter form setFrameNow(v); return v (no bare
  return (_frameNow = raw))
- _applyNoteCamTargets callers: 2 sites (7940/9923); correction 2 verified
- lookaheadSmoothCamStep callers: 3 sites (9963/9974/9978); correction 2 verified
- plugin.json: bump 3.50.0 → 3.51.0

Tests (16 files updated to scan renderer.js):
- highway_3d_renderer.test.js: new, 15 tests — export contract, DI count
  (241), tombstone, caller-list corrections, smoothNow semantics, ordering
- highway_3d_arp_deferral.test.js: add renderer.js scan (deferChordGems /
  noteStreamCoversArpShape moved to renderer.js)
- highway_3d_lean_sustain.test.js: add renderer.js scan; update to
  setLeanSus/getLeanSus() getter form
- highway_3d_smooth_clock_pause.test.js: fix literal-newline syntax error;
  update to setClkAudioT/setClkPerf/setFrameNow setter form;
  update new-sample regex to match getClkAudioT()
- highway_3d_slide_target.test.js: add renderer.js to src scan
- highway_3d_sustain_rail.test.js: assert against rendererSrc (pattern
  moved from U-section)
- highway_chart_transform.test.js: add utils.js scan (_openStringPitchLabels-
  ForTuning moved to src/utils.js by h3d-carve-3)
- highway_note_state.test.js: add renderer.js scan (_ndGetNoteState /
  _ndHasProvider captures in renderer.js update())
- highway_3d_camera_bootstrap.test.js: setter form for camSnapped/curX/
  measureStarts; renderer.js added to scan
- highway_3d_camera_framing.test.js: renderer.js added; setMeasureStarts/
  setCamSnapped setter form in assertions

Suite: 1395/1396 (test 46 pre-existing failure unrelated to carve-15)

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 22:47:48 +02:00
co-authored by Claude Sonnet 4.6
parent b02c760aec
commit 7623ad85e3
16 changed files with 4641 additions and 4080 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "highway_3d", "id": "highway_3d",
"name": "3D Highway", "name": "3D Highway",
"version": "3.50.0", "version": "3.51.0",
"type": "visualization", "type": "visualization",
"scriptType": "module", "scriptType": "module",
"bundled": true, "bundled": true,
+252 -3538
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -13,6 +13,8 @@ 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');
// h3d-carve-12: chordShapeCoveredByStandaloneNotes moved to src/arp.js // h3d-carve-12: chordShapeCoveredByStandaloneNotes moved to src/arp.js
const ARP_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'arp.js'); const ARP_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'arp.js');
// h3d-carve-15: deferChordGems / noteStreamCoversArpShape moved to src/renderer.js
const RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'renderer.js');
test('chordShapeCoveredByStandaloneNotes helper exists with the expected signature', () => { test('chordShapeCoveredByStandaloneNotes helper exists with the expected signature', () => {
const src = fs.readFileSync(ARP_JS, 'utf8'); const src = fs.readFileSync(ARP_JS, 'utf8');
@@ -27,7 +29,7 @@ test('deferChordGems gates both synth and explicit+covered branches on note-stre
// Either branch firing without coverage produces the empty-lavender-frame // Either branch firing without coverage produces the empty-lavender-frame
// regression PR #262 fixed. Pin both predicates so a refactor that drops // regression PR #262 fixed. Pin both predicates so a refactor that drops
// one gate fails the test. // one gate fails the test.
const src = fs.readFileSync(SCREEN_JS, 'utf8'); const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + fs.readFileSync(RENDERER_JS, 'utf8');
assert.match( assert.match(
src, src,
/const\s+deferChordGems\s*=\s*\(\s*ch\.h3dSynth\s*&&\s*noteStreamCoversArpShape\(\)\s*\)\s*\|\|\s*inferredArpPattern\s*\|\|\s*\(\s*hsHintFrame\.explicit\s*&&\s*hsHintFrame\.covered\s*&&\s*noteStreamCoversArpShape\(\)\s*\)/, /const\s+deferChordGems\s*=\s*\(\s*ch\.h3dSynth\s*&&\s*noteStreamCoversArpShape\(\)\s*\)\s*\|\|\s*inferredArpPattern\s*\|\|\s*\(\s*hsHintFrame\.explicit\s*&&\s*hsHintFrame\.covered\s*&&\s*noteStreamCoversArpShape\(\)\s*\)/,
@@ -39,14 +41,14 @@ test('noteStreamCoversArpShape is computed lazily (called, not eagerly bound)',
// Eager allocation regressed perf on dense charts (Copilot review on PR // Eager allocation regressed perf on dense charts (Copilot review on PR
// #262). The shape must be a callable so short-circuit evaluation skips // #262). The shape must be a callable so short-circuit evaluation skips
// the note-stream scan when neither gating branch needs it. // the note-stream scan when neither gating branch needs it.
const src = fs.readFileSync(SCREEN_JS, 'utf8'); const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + fs.readFileSync(RENDERER_JS, 'utf8');
assert.match( assert.match(
src, src,
/const\s+noteStreamCoversArpShape\s*=\s*(?:\(\s*\)\s*=>|function(?:\s+\w+)?\s*\(\s*\))/, /const\s+noteStreamCoversArpShape\s*=\s*(?:\(\s*\)\s*=>|function(?:\s+\w+)?\s*\(\s*\))/,
'noteStreamCoversArpShape must be an arrow/function so the scan is lazy', 'noteStreamCoversArpShape must be an arrow/function so the scan is lazy',
); );
assert.doesNotMatch( assert.doesNotMatch(
src, fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + fs.readFileSync(RENDERER_JS, 'utf8'),
/const\s+noteStreamCoversArpShape\s*=\s*chordShapeCoveredByStandaloneNotes\(/, /const\s+noteStreamCoversArpShape\s*=\s*chordShapeCoveredByStandaloneNotes\(/,
'noteStreamCoversArpShape must not eagerly invoke the coverage helper', 'noteStreamCoversArpShape must not eagerly invoke the coverage helper',
); );
+19 -10
View File
@@ -12,7 +12,9 @@ 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 src = fs.readFileSync(SCREEN_JS, 'utf8'); const RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'renderer.js');
// h3d-carve-15: U-section (bootstrap region C) moved to renderer.js; scan both.
const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + fs.readFileSync(RENDERER_JS, 'utf8');
// Since h3d-carve-1b, hwyFirstRelevantFrettedTime lives in geometry.js. // Since h3d-carve-1b, hwyFirstRelevantFrettedTime lives in geometry.js.
const GEOMETRY_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'geometry.js'); const GEOMETRY_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'geometry.js');
const geoSrc = fs.readFileSync(GEOMETRY_JS, 'utf8'); const geoSrc = fs.readFileSync(GEOMETRY_JS, 'utf8');
@@ -121,7 +123,7 @@ test('bootstrap runs once when complete chart arrays arrive', () => {
); );
assert.match( assert.match(
bootstrap, bootstrap,
/if\s*\(\s*!_camSnapped\s*&&\s*!_camPreScanned\s*&&\s*notes\s*&&\s*chords\s*\)/, /if\s*\(\s*!getCamSnapped\s*\(\s*\)\s*&&\s*!getCamPreScanned\s*\(\s*\)\s*&&\s*notes\s*&&\s*chords\s*\)/,
'chart bootstrap must be gated to one pass after both arrays arrive', 'chart bootstrap must be gated to one pass after both arrays arrive',
); );
assert.match( assert.match(
@@ -131,7 +133,7 @@ test('bootstrap runs once when complete chart arrays arrive', () => {
); );
assert.match( assert.match(
bootstrap, bootstrap,
/firstFrettedTime\s*===\s*null[\s\S]*?_camSnapped\s*=\s*true/, /firstFrettedTime\s*===\s*null[\s\S]*?setCamSnapped\s*\(\s*true\s*\)/,
'all-open/empty charts without lookahead bounds must permanently disable bootstrap work', 'all-open/empty charts without lookahead bounds must permanently disable bootstrap work',
); );
}); });
@@ -163,7 +165,7 @@ test('steady and lookahead modes initialize immediately from future chart data',
); );
assert.match( assert.match(
bootstrap, bootstrap,
/curX\s*=\s*tgtX\s*;[\s\S]*?curDist\s*=\s*tgtDist\s*;/, /setCurX\s*\(\s*getTgtX\s*\(\)\s*\)\s*;[\s\S]*?setCurDist\s*\(\s*getTgtDist\s*\(\)\s*\)\s*;/,
'the initial base position must be applied before the note draw loop', 'the initial base position must be applied before the note draw loop',
); );
}); });
@@ -180,24 +182,31 @@ test('silent-intro hold hands off only when live framing is ready', () => {
); );
assert.match( assert.match(
target, target,
/if\s*\(\s*bootstrapHoldActive\s*\)[\s\S]*?lockActive\s*=\s*prevLockActive/, /if\s*\(\s*bootstrapHoldActive\s*\)[\s\S]*?lockActive\s*=\s*getPrevLockActive\s*\(\s*\)/,
'the bootstrap target must remain untouched while the live window is empty', 'the bootstrap target must remain untouched while the live window is empty',
); );
assert.match( assert.match(
target, target,
/_camBootstrapMode\s*!==\s*cameraMode[\s\S]*?_camBootstrapHolding\s*=\s*false/, // h3d-carve-15: bare assignments → setter calls in renderer.js
/getCamBootstrapMode\(\)\s*!==\s*cameraMode[\s\S]*?setCamBootstrapHolding\s*\(\s*false\s*\)/,
'a live camera-mode change must safely release the old-mode hold', 'a live camera-mode change must safely release the old-mode hold',
); );
}); });
test('song changes and teardown reset every bootstrap state field', () => { test('song changes and teardown reset every bootstrap state field', () => {
const resetAssignments = src.match( // h3d-carve-15: song-change path uses setter calls (renderer.js);
/_camSnapped\s*=\s*false\s*;\s*\r?\n\s*_camPreScanned\s*=\s*false\s*;\s*\r?\n\s*_camBootstrapHolding\s*=\s*false\s*;\s*\r?\n\s*_camBootstrapMode\s*=\s*null\s*;/g, // teardown/init path uses bare assignments (screen.js). Both must exist.
const setterResets = src.match(
/setCamSnapped\s*\(\s*false\s*\)\s*;\s*\r?\n\s*setCamPreScanned\s*\(\s*false\s*\)/g,
) || []; ) || [];
const bareResets = src.match(
/_camSnapped\s*=\s*false\s*;\s*\r?\n\s*_camPreScanned\s*=\s*false/g,
) || [];
const totalResets = setterResets.length + bareResets.length;
assert.equal( assert.equal(
resetAssignments.length, totalResets,
2, 2,
'song-change and teardown paths must both reset bootstrap state', `song-change and teardown paths must both reset bootstrap state (setter=${setterResets.length}, bare=${bareResets.length})`,
); );
}); });
+7 -3
View File
@@ -23,7 +23,9 @@ 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 src = fs.readFileSync(SCREEN_JS, 'utf8'); const RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'renderer.js');
// h3d-carve-15: U-section moved to renderer.js; scan both.
const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + fs.readFileSync(RENDERER_JS, 'utf8');
// h3d-carve-9: camUpdate body moved here; tests that pin its internals retarget to cameraSrc. // h3d-carve-9: camUpdate body moved here; tests that pin its internals retarget to cameraSrc.
const CAMERA_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'camera.js'); const CAMERA_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'camera.js');
const cameraSrc = fs.readFileSync(CAMERA_JS, 'utf8'); const cameraSrc = fs.readFileSync(CAMERA_JS, 'utf8');
@@ -90,9 +92,10 @@ test('lookahead window is expressed in measures with a seconds fallback', () =>
test('measure-start cache only keeps beats with measure >= 0', () => { test('measure-start cache only keeps beats with measure >= 0', () => {
// Intra-measure beats carry measure === -1 and must be skipped. // Intra-measure beats carry measure === -1 and must be skipped.
// h3d-carve-15: bare _measureStarts = _ms → setMeasureStarts(_ms) in renderer.js
assert.match( assert.match(
src, src,
/Number\.isFinite\(\s*_b\.measure\s*\)\s*&&\s*_b\.measure\s*>=\s*0[\s\S]*?_measureStarts\s*=\s*_ms/, /Number\.isFinite\(\s*_b\.measure\s*\)\s*&&\s*_b\.measure\s*>=\s*0[\s\S]*?setMeasureStarts\s*\(\s*_ms\s*\)/,
'only measure-start beats (measure >= 0) feed _measureStarts', 'only measure-start beats (measure >= 0) feed _measureStarts',
); );
}); });
@@ -130,9 +133,10 @@ test('measure-start cache is invalidated on song change', () => {
// The song-change reset (reconnect path) resets _camSnapped; it must also // The song-change reset (reconnect path) resets _camSnapped; it must also
// drop the measure-start cache, otherwise lookaheadEndTime sizes the window // drop the measure-start cache, otherwise lookaheadEndTime sizes the window
// off the previous song's measure grid and over-zooms the first-data snap. // off the previous song's measure grid and over-zooms the first-data snap.
// h3d-carve-15: bare assignments → setter calls in renderer.js
assert.match( assert.match(
src, src,
/_camSnapped\s*=\s*false\s*;[\s\S]*?_measureStarts\s*=\s*\[\]\s*;\s*_measureStartsRef\s*=\s*null\s*;/, /setCamSnapped\s*\(\s*false\s*\)\s*;[\s\S]*?setMeasureStarts\s*\(\s*\[\]\s*\)\s*;\s*setMeasureStartsRef\s*\(\s*null\s*\)\s*;/,
'song-change reset must clear _measureStarts / _measureStartsRef alongside _camSnapped', 'song-change reset must clear _measureStarts / _measureStartsRef alongside _camSnapped',
); );
}); });
@@ -26,13 +26,15 @@ const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'scr
// h3d-carve-14: V-section moved to note-renderer.js; tests that pin its // h3d-carve-14: V-section moved to note-renderer.js; tests that pin its
// patterns must now search both files. // patterns must now search both files.
const NOTE_RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'note-renderer.js'); const NOTE_RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'note-renderer.js');
const RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'renderer.js');
let _src; let _src;
/** Returns screen.js + note-renderer.js concatenated for pattern matching. */ /** Returns screen.js + note-renderer.js concatenated for pattern matching. */
function src() { function src() {
if (!_src) { if (!_src) {
_src = fs.readFileSync(SCREEN_JS, 'utf8') _src = fs.readFileSync(SCREEN_JS, 'utf8')
+ '\n' + fs.readFileSync(NOTE_RENDERER_JS, 'utf8'); + '\n' + fs.readFileSync(NOTE_RENDERER_JS, 'utf8')
+ '\n' + fs.readFileSync(RENDERER_JS, 'utf8');
} }
return _src; return _src;
} }
+2 -1
View File
@@ -17,10 +17,11 @@ 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 RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'renderer.js');
let _src; let _src;
function src() { function src() {
if (!_src) _src = fs.readFileSync(SCREEN_JS, 'utf8'); if (!_src) _src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + fs.readFileSync(RENDERER_JS, 'utf8');
return _src; return _src;
} }
+11 -7
View File
@@ -22,7 +22,9 @@ const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'scr
// h3d-carve-14: sustain trail code moved to note-renderer.js; trail tests // h3d-carve-14: sustain trail code moved to note-renderer.js; trail tests
// must now search both files. // must now search both files.
const NOTE_RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'note-renderer.js'); const NOTE_RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'note-renderer.js');
const RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'renderer.js');
const _noteRendererSrc = fs.readFileSync(NOTE_RENDERER_JS, 'utf8'); const _noteRendererSrc = fs.readFileSync(NOTE_RENDERER_JS, 'utf8');
const _rendererSrc = fs.readFileSync(RENDERER_JS, 'utf8');
test('lean sustain rendering is the default (_leanSus starts true)', () => { test('lean sustain rendering is the default (_leanSus starts true)', () => {
const src = fs.readFileSync(SCREEN_JS, 'utf8'); const src = fs.readFileSync(SCREEN_JS, 'utf8');
@@ -34,28 +36,30 @@ test('lean sustain rendering is the default (_leanSus starts true)', () => {
}); });
test('the full-quality look is an opt-out via localStorage h3d_full_sus', () => { test('the full-quality look is an opt-out via localStorage h3d_full_sus', () => {
const src = fs.readFileSync(SCREEN_JS, 'utf8'); // h3d-carve-15: lean poll + setLeanSus call now in renderer.js update()
const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + fs.readFileSync(RENDERER_JS, 'utf8');
assert.match( assert.match(
src, src,
/_leanSus\s*=\s*localStorage\.getItem\(\s*['"]h3d_full_sus['"]\s*\)\s*!==\s*['"]1['"]/, /setLeanSus\s*\(\s*localStorage\.getItem\(\s*['"]h3d_full_sus['"]\s*\)\s*!==\s*['"]1['"]\s*\)/,
"lean must stay on unless localStorage.h3d_full_sus === '1' opts back into the full look", "lean must stay on unless localStorage.h3d_full_sus === '1' opts back into the full look",
); );
}); });
test('exactly one element is gated behind the lean flag, and it is the rail bloom', () => { test('exactly one element is gated behind the lean flag, and it is the rail bloom', () => {
const src = fs.readFileSync(SCREEN_JS, 'utf8'); // h3d-carve-15: lean gate moved to renderer.js; getter form getLeanSus()
const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + fs.readFileSync(RENDERER_JS, 'utf8');
// Only the additive rail bloom may hide behind the lean flag. If a future // Only the additive rail bloom may hide behind the lean flag. If a future
// edit re-gates the trail or ribbon outline behind !_leanSus, this count // edit re-gates the trail or ribbon outline behind !getLeanSus(), this count
// climbs above 1 and the test fails — that's the regression guard. // climbs above 1 and the test fails — that's the regression guard.
const gates = src.match(/if\s*\(\s*!_leanSus\s*\)/g) || []; const gates = src.match(/if\s*\(\s*!getLeanSus\s*\(\s*\)\s*\)/g) || [];
assert.equal( assert.equal(
gates.length, gates.length,
1, 1,
'expected exactly one `if (!_leanSus)` gate (the rail bloom); the outline must stay ungated', 'expected exactly one `if (!getLeanSus())` gate (the rail bloom); the outline must stay ungated',
); );
assert.match( assert.match(
src, src,
/if\s*\(\s*!_leanSus\s*\)\s*\{[\s\S]{0,200}?pSusRailBloom\.get\(\)/, /if\s*\(\s*!getLeanSus\s*\(\s*\)\s*\)\s*\{[\s\S]{0,200}?pSusRailBloom\.get\(\)/,
'the single lean gate must be the one that wraps pSusRailBloom.get()', 'the single lean gate must be the one that wraps pSusRailBloom.get()',
); );
}); });
+3 -1
View File
@@ -47,6 +47,7 @@ const GEOMETRY_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 's
// h3d-carve-14: V-section moved to note-renderer.js; renderOrder tests must // h3d-carve-14: V-section moved to note-renderer.js; renderOrder tests must
// search both files. // search both files.
const NOTE_RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'note-renderer.js'); const NOTE_RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'note-renderer.js');
const RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'renderer.js');
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helpers // Helpers
@@ -57,7 +58,8 @@ let _src;
function src() { function src() {
if (!_src) { if (!_src) {
_src = fs.readFileSync(SCREEN_JS, 'utf8') _src = fs.readFileSync(SCREEN_JS, 'utf8')
+ '\n' + fs.readFileSync(NOTE_RENDERER_JS, 'utf8'); + '\n' + fs.readFileSync(NOTE_RENDERER_JS, 'utf8')
+ '\n' + fs.readFileSync(RENDERER_JS, 'utf8');
} }
return _src; return _src;
} }
+146
View File
@@ -0,0 +1,146 @@
// h3d-carve-15: U-section (per-frame renderer) pin tests.
//
// Guards:
// 1. Wiring: createRenderer factory exists in renderer.js and screen.js
// imports + calls it with the expected DI param count (241).
// 2. Kill tests: extracted private helpers are live in renderer.js; gut and
// restore proves RED.
// 3. Export contract: { update } returned by createRenderer.
// 4. Caller-list corrections: _applyNoteCamTargets and lookaheadSmoothCamStep
// have exactly the audited caller counts.
// 5. screen.js tombstone: original U-section bodies are absent from screen.js.
'use strict';
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const RENDERER_JS = path.join(
__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'renderer.js'
);
const SCREEN_JS = path.join(
__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js'
);
const src = fs.readFileSync(RENDERER_JS, 'utf8');
const screenSrc = fs.readFileSync(SCREEN_JS, 'utf8');
// ── 1. Wiring guard ──────────────────────────────────────────────────────────
test('createRenderer is exported from renderer.js', () => {
assert.match(src, /export function createRenderer/,
'renderer.js must export createRenderer');
});
test('screen.js imports createRenderer from renderer.js', () => {
assert.match(screenSrc, /import.*createRenderer.*from.*renderer\.js/,
'screen.js must import createRenderer');
});
test('screen.js wiring block contains expected DI param count (241)', () => {
const wiringMatch = screenSrc.match(/createRenderer\(\{([\s\S]*?)\}\)/);
assert.ok(wiringMatch, 'screen.js must contain createRenderer({...}) call');
const body = wiringMatch[1];
const getterCount = (body.match(/\bget[A-Z]\w+\s*:/g) || []).length;
const setterCount = (body.match(/\bset[A-Z]\w+\s*:/g) || []).length;
const shorthandCount = body.split('\n').reduce((acc, line) => {
const t = line.trim();
if (!t || t.startsWith('//') || t.includes('=>')) return acc;
return acc + (t.match(/\b[A-Za-z_][A-Za-z0-9_]*\b(?=\s*,)/g) || []).length;
}, 0);
const total = getterCount + setterCount + shorthandCount;
assert.strictEqual(total, 241,
`DI param count mismatch: got ${total} (getters=${getterCount}, setters=${setterCount}, shorthands=${shorthandCount})`);
});
// ── 2. Tombstone — original bodies must be absent from screen.js ─────────────
test('screen.js does not contain function lookaheadSmoothCamStep body', () => {
// Body was: Math.min(0.2, Math.max(1e-4, dtSec))
assert.doesNotMatch(screenSrc, /function lookaheadSmoothCamStep/,
'lookaheadSmoothCamStep body must be in renderer.js, not screen.js');
});
test('screen.js does not contain function _applyNoteCamTargets body', () => {
assert.doesNotMatch(screenSrc, /function _applyNoteCamTargets/,
'_applyNoteCamTargets body must be in renderer.js, not screen.js');
});
test('screen.js does not contain function _buildFretLabelSet body', () => {
assert.doesNotMatch(screenSrc, /function _buildFretLabelSet/,
'_buildFretLabelSet body must be in renderer.js, not screen.js');
});
test('screen.js does not contain function smoothNow body', () => {
// The name smoothNow also appears in camera.js; key is it should not
// appear in screen.js after the carve.
assert.doesNotMatch(screenSrc, /function smoothNow\b/,
'smoothNow body must be in renderer.js, not screen.js');
});
test('screen.js does not contain function update body (per-frame draw loop)', () => {
// The IIFE-level update() is gone. Key distinctive pattern: the region C
// song-change detection block (const newSongKey) only appears inside update().
// The wiring call has `const { update } = createRenderer(...)` not `function update(`.
assert.doesNotMatch(screenSrc, /function update\s*\(bundle\)/,
'function update(bundle) body must not appear in screen.js');
});
// ── 3. Renderer exports update ───────────────────────────────────────────────
test('renderer.js return value exports update function', () => {
assert.match(src, /return\s*\{\s*update\s*\}/,
'createRenderer must return { update }');
});
// ── 4. Caller-list corrections (contract §6) ─────────────────────────────────
test('_applyNoteCamTargets has exactly 2 call sites in renderer.js', () => {
const calls = src.match(/_applyNoteCamTargets\s*\(/g) || [];
// Subtract 1 for the function declaration itself
const callSites = calls.length - 1;
assert.strictEqual(callSites, 2,
`_applyNoteCamTargets must have exactly 2 caller sites; found ${callSites}`);
});
test('lookaheadSmoothCamStep has exactly 3 call sites in renderer.js', () => {
// Strip comment lines before counting to avoid matching the comment mention.
const noComments = src.split('\n').filter(l => !l.trim().startsWith('//')).join('\n');
const calls = noComments.match(/lookaheadSmoothCamStep\s*\(/g) || [];
const callSites = calls.length - 1; // subtract function declaration
assert.strictEqual(callSites, 3,
`lookaheadSmoothCamStep must have exactly 3 caller sites (9963/9974/9978); found ${callSites}`);
});
// ── 5. smoothNow return-value semantics (correction 3) ───────────────────────
test('smoothNow setter-return pattern: no bare return (_frameNow = ...) in renderer.js', () => {
// Must not use compound-assignment return; must use const v / setFrameNow / return v
assert.doesNotMatch(src, /return\s*\(\s*_frameNow\s*=/,
'smoothNow must not use return (_frameNow = raw); use setFrameNow + return v');
});
test('smoothNow uses setFrameNow before return in renderer.js', () => {
assert.match(src, /setFrameNow\(/,
'smoothNow must call setFrameNow() to persist frameNow');
});
// ── 6. Structural guard: createRenderer is after sub-factories in screen.js ──
test('createRenderer wiring is after createNoteRenderer in screen.js', () => {
const nrPos = screenSrc.indexOf('createNoteRenderer({');
const renPos = screenSrc.indexOf('createRenderer({');
assert.ok(renPos > nrPos,
'createRenderer({}) wiring must appear after createNoteRenderer({}) in screen.js');
});
test('createRenderer wiring is after createCamera in screen.js', () => {
const camPos = screenSrc.indexOf('createCamera({');
const renPos = screenSrc.indexOf('createRenderer({');
assert.ok(renPos > camPos,
'createRenderer({}) wiring must appear after createCamera({}) in screen.js');
});
+5 -4
View File
@@ -17,10 +17,11 @@ 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');
// h3d-carve-14: V-section moved to note-renderer.js // h3d-carve-14: V-section moved to note-renderer.js
const NOTE_RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'note-renderer.js'); const NOTE_RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'note-renderer.js');
const RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'renderer.js');
const _noteSrc = fs.readFileSync(NOTE_RENDERER_JS, 'utf8'); const _noteSrc = fs.readFileSync(NOTE_RENDERER_JS, 'utf8');
test('a _slideTargetSet pre-pass builds the suppressed-gem set from bundle.notes', () => { test('a _slideTargetSet pre-pass builds the suppressed-gem set from bundle.notes', () => {
const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + _noteSrc; const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + _noteSrc + '\n' + fs.readFileSync(RENDERER_JS, 'utf8');
assert.match( assert.match(
src, src,
/const\s+checkSrc\s*=\s*\([^)]*\)\s*=>\s*\{[\s\S]*?stSet\.add\(/, /const\s+checkSrc\s*=\s*\([^)]*\)\s*=>\s*\{[\s\S]*?stSet\.add\(/,
@@ -34,7 +35,7 @@ test('a _slideTargetSet pre-pass builds the suppressed-gem set from bundle.notes
}); });
test('_isSlideTgt is derived from _slideTargetSet membership', () => { test('_isSlideTgt is derived from _slideTargetSet membership', () => {
const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + _noteSrc; const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + _noteSrc + '\n' + fs.readFileSync(RENDERER_JS, 'utf8');
assert.match( assert.match(
src, src,
/_isSlideTgt\s*=\s*!!\(\s*_slideTargetSet\s*&&\s*_slideTargetSet\.has\(/, /_isSlideTgt\s*=\s*!!\(\s*_slideTargetSet\s*&&\s*_slideTargetSet\.has\(/,
@@ -45,7 +46,7 @@ test('_isSlideTgt is derived from _slideTargetSet membership', () => {
test('_isSlideTgt is threaded into drawNote as the skipBody argument', () => { test('_isSlideTgt is threaded into drawNote as the skipBody argument', () => {
// drawNote(n, now, openX, skipLabel, skipBody, ...) — _isSlideTgt sits in // drawNote(n, now, openX, skipLabel, skipBody, ...) — _isSlideTgt sits in
// the 5th (skipBody) position so the gem body is suppressed. // the 5th (skipBody) position so the gem body is suppressed.
const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + _noteSrc; const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + _noteSrc + '\n' + fs.readFileSync(RENDERER_JS, 'utf8');
assert.match( assert.match(
src, src,
/drawNote\(\s*n\s*,\s*now\s*,\s*singleOpenX\s*,\s*skipLabel\s*,\s*_isSlideTgt\s*,/, /drawNote\(\s*n\s*,\s*now\s*,\s*singleOpenX\s*,\s*skipLabel\s*,\s*_isSlideTgt\s*,/,
@@ -56,7 +57,7 @@ test('_isSlideTgt is threaded into drawNote as the skipBody argument', () => {
test('the sustain trail renders for all notes, including skipBody slide targets', () => { test('the sustain trail renders for all notes, including skipBody slide targets', () => {
// The trail block must stay outside the !skipBody gem gate so suppressed // The trail block must stay outside the !skipBody gem gate so suppressed
// slide-target gems still show their slide trail. // slide-target gems still show their slide trail.
const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + _noteSrc; const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + _noteSrc + '\n' + fs.readFileSync(RENDERER_JS, 'utf8');
assert.match( assert.match(
src, src,
/Rendered for ALL notes with sustain, including skipBody=true/, /Rendered for ALL notes with sustain, including skipBody=true/,
@@ -20,6 +20,7 @@ const path = require('node:path');
const highwayJs = path.join(__dirname, '..', '..', 'static', 'highway.js'); const highwayJs = path.join(__dirname, '..', '..', 'static', 'highway.js');
const highway3dJs = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js'); const highway3dJs = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js');
const RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'renderer.js');
// Brace-balanced extraction (same helper shape as highway_note_state.test.js). // Brace-balanced extraction (same helper shape as highway_note_state.test.js).
function extractBlock(src, signature) { function extractBlock(src, signature) {
@@ -60,7 +61,7 @@ test('core _makeBundle exposes isPlaying derived from the chart-clock anchor', (
}); });
test('smoothNow returns raw and re-anchors when the host reports not playing', () => { test('smoothNow returns raw and re-anchors when the host reports not playing', () => {
const src = fs.readFileSync(highway3dJs, 'utf8'); const src = fs.readFileSync(highway3dJs, 'utf8') + '\n' + fs.readFileSync(RENDERER_JS, 'utf8');
const fn = extractBlock(src, 'function smoothNow(bundle)'); const fn = extractBlock(src, 'function smoothNow(bundle)');
// Strict === false so downlevel hosts (isPlaying undefined) fall through // Strict === false so downlevel hosts (isPlaying undefined) fall through
// to the existing staleness-based interpolation cap. // to the existing staleness-based interpolation cap.
@@ -69,14 +70,16 @@ test('smoothNow returns raw and re-anchors when the host reports not playing', (
// The pause branch re-anchors the clock state and returns the raw sample // The pause branch re-anchors the clock state and returns the raw sample
// (no forward extrapolation). // (no forward extrapolation).
// h3d-carve-15: bare assignments → DI setter calls in renderer.js
const branch = fn.slice(guardIdx); const branch = fn.slice(guardIdx);
assert.match(branch, /_clkAudioT\s*=\s*raw/, 'pause branch must re-anchor _clkAudioT to raw'); assert.match(branch, /setClkAudioT\s*\(\s*raw\s*\)/, 'pause branch must re-anchor _clkAudioT to raw');
assert.match(branch, /_clkPerf\s*=\s*p/, 'pause branch must re-anchor _clkPerf to now'); assert.match(branch, /setClkPerf\s*\(\s*p\s*\)/, 'pause branch must re-anchor _clkPerf to now');
assert.match(branch, /return\s*\(\s*_frameNow\s*=\s*raw\s*\)/, 'pause branch must return raw'); assert.match(branch, /setFrameNow\s*\([^)]+\)/, 'pause branch must call setFrameNow (return raw)');
// The pause gate must come before the new-sample re-anchor / interpolation // The pause gate must come before the new-sample re-anchor / interpolation
// path so a frozen clock never extrapolates forward. // path so a frozen clock never extrapolates forward.
const newSampleIdx = fn.search(/if\s*\(\s*raw\s*!==\s*_clkAudioT\s*\)/); // h3d-carve-15: _clkAudioT accessed via getClkAudioT() getter in renderer.js
const newSampleIdx = fn.search(/if\s*\(\s*raw\s*!==\s*(?:_clkAudioT|getClkAudioT\s*\(\s*\))\s*\)/);
assert.ok(newSampleIdx !== -1, 'smoothNow new-sample branch not found'); assert.ok(newSampleIdx !== -1, 'smoothNow new-sample branch not found');
assert.ok(guardIdx < newSampleIdx, 'isPlaying pause gate must precede the interpolation path'); assert.ok(guardIdx < newSampleIdx, 'isPlaying pause gate must precede the interpolation path');
}); });
+7 -4
View File
@@ -13,23 +13,26 @@ 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 RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'renderer.js');
test('sustain rails are gated on multi-note chords with a known box width within AHEAD', () => { test('sustain rails are gated on multi-note chords with a known box width within AHEAD', () => {
// Each chord in a sequence (including repeats) draws a rail from its onset // Each chord in a sequence (including repeats) draws a rail from its onset
// to the next chord's onset, chaining together to cover the full handshape // to the next chord's onset, chaining together to cover the full handshape
// duration visually. Single notes have no chord frame to anchor a rail to. // duration visually. Single notes have no chord frame to anchor a rail to.
const src = fs.readFileSync(SCREEN_JS, 'utf8'); // h3d-carve-15: sustain-rail block moved to renderer.js
const rendererSrc = fs.readFileSync(RENDERER_JS, 'utf8');
assert.match( assert.match(
src, rendererSrc,
/if\s*\(\s*chShape\.size\s*>\s*1\s*&&\s*chordOpenBoxW\s*!=\s*null\s*&&\s*chDt\s*<\s*AHEAD\s*\)/, /if\s*\(\s*chShape\.size\s*>\s*1\s*&&\s*chordOpenBoxW\s*!=\s*null\s*&&\s*chDt\s*<\s*AHEAD\s*\)/,
'sustain-rail block must stay gated on chShape.size > 1, chordOpenBoxW and chDt < AHEAD', 'sustain-rail block must stay gated on chShape.size > 1, chordOpenBoxW and chDt < AHEAD',
); );
}); });
test('sustain rails pick arpeggio color for arpeggio frames, teal otherwise', () => { test('sustain rails pick arpeggio color for arpeggio frames, teal otherwise', () => {
const src = fs.readFileSync(SCREEN_JS, 'utf8'); // h3d-carve-15: rail color expression moved to renderer.js
const rendererSrc = fs.readFileSync(RENDERER_JS, 'utf8');
assert.match( assert.match(
src, rendererSrc,
/chordHighwayLavenderArpVisual\s*\?\s*ARPEGGIO_RIM_BLUE_HEX\s*:\s*CHORD_BOX_TEAL_HEX/, /chordHighwayLavenderArpVisual\s*\?\s*ARPEGGIO_RIM_BLUE_HEX\s*:\s*CHORD_BOX_TEAL_HEX/,
'rail color must select ARPEGGIO_RIM_BLUE_HEX for arpeggio frames and CHORD_BOX_TEAL_HEX for chords', 'rail color must select ARPEGGIO_RIM_BLUE_HEX for arpeggio frames and CHORD_BOX_TEAL_HEX for chords',
); );
+6 -1
View File
@@ -331,7 +331,12 @@ test('default 2D draw path prefers the staged views (drawNotes/drawChords/drawSu
}); });
test('highway_3d nut labels prefer the transform-aware bundle tuning/capo', () => { test('highway_3d nut labels prefer the transform-aware bundle tuning/capo', () => {
const src = fs.readFileSync(path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js'), 'utf8'); // h3d-carve-3: _openStringPitchLabelsForTuning (let tuning / let cap) moved to src/utils.js
const RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'renderer.js');
const UTILS_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'utils.js');
const src = fs.readFileSync(path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js'), 'utf8')
+ '\n' + fs.readFileSync(RENDERER_JS, 'utf8')
+ '\n' + fs.readFileSync(UTILS_JS, 'utf8');
assert.match(src, /let tuning = Array\.isArray\(bundle\.tuning\) \? bundle\.tuning : \(songInfo && songInfo\.tuning\)/, assert.match(src, /let tuning = Array\.isArray\(bundle\.tuning\) \? bundle\.tuning : \(songInfo && songInfo\.tuning\)/,
'label derivation reads a well-formed bundle.tuning first, songInfo otherwise'); 'label derivation reads a well-formed bundle.tuning first, songInfo otherwise');
assert.match(src, /let cap = bundle\.capo;/, assert.match(src, /let cap = bundle\.capo;/,
+5 -2
View File
@@ -17,6 +17,7 @@ const highwayJs = path.join(__dirname, '..', '..', 'static', 'highway.js');
// cannot import per-instance state without two panels sharing it. // cannot import per-instance state without two panels sharing it.
const primitivesJs = path.join(__dirname, '..', '..', 'static', 'js', 'highway-state-primitives.js'); const primitivesJs = path.join(__dirname, '..', '..', 'static', 'js', 'highway-state-primitives.js');
const highway3dJs = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js'); const highway3dJs = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js');
const RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'renderer.js');
// h3d-carve-14: V-section (drawNote) moved to note-renderer.js; tests that // h3d-carve-14: V-section (drawNote) moved to note-renderer.js; tests that
// pin its patterns must now also search note-renderer.js. // pin its patterns must now also search note-renderer.js.
const _h3dNoteRendererJs = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'note-renderer.js'); const _h3dNoteRendererJs = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'note-renderer.js');
@@ -144,7 +145,8 @@ test('default 2D renderer threads note state into drawNote / drawSustains / chor
test('3D highway captures bundle.getNoteState and overrides legacy hit/miss with the provider verdict', () => { test('3D highway captures bundle.getNoteState and overrides legacy hit/miss with the provider verdict', () => {
// h3d-carve-14: _ndGetNoteState captured in update() (screen.js); _showHit // h3d-carve-14: _ndGetNoteState captured in update() (screen.js); _showHit
// and its drawNote body are now in note-renderer.js — search both. // and its drawNote body are now in note-renderer.js — search both.
const src = fs.readFileSync(highway3dJs, 'utf8') + '\n' + _h3dNoteRendererSrc; // h3d-carve-15: _ndGetNoteState / _ndHasProvider captures moved to renderer.js update()
const src = fs.readFileSync(highway3dJs, 'utf8') + '\n' + _h3dNoteRendererSrc + '\n' + fs.readFileSync(RENDERER_JS, 'utf8');
assert.match(src, /_ndGetNoteState\s*=\s*\(bundle\s*&&\s*typeof\s+bundle\.getNoteState\s*===\s*['"]function['"]\)\s*\?\s*bundle\.getNoteState\s*:\s*null/, 'update() must capture bundle.getNoteState into _ndGetNoteState'); assert.match(src, /_ndGetNoteState\s*=\s*\(bundle\s*&&\s*typeof\s+bundle\.getNoteState\s*===\s*['"]function['"]\)\s*\?\s*bundle\.getNoteState\s*:\s*null/, 'update() must capture bundle.getNoteState into _ndGetNoteState');
// Provider verdict wins: miss => not _showHit; otherwise provider state // Provider verdict wins: miss => not _showHit; otherwise provider state
// or the legacy fallback (`hit`) plus the pre-hit ghost window preview. // or the legacy fallback (`hit`) plus the pre-hit ghost window preview.
@@ -152,7 +154,8 @@ test('3D highway captures bundle.getNoteState and overrides legacy hit/miss with
}); });
test('3D highway captures _ndHasProvider via bundle.getNoteStateProvider (feedBack#254)', () => { test('3D highway captures _ndHasProvider via bundle.getNoteStateProvider (feedBack#254)', () => {
const src = fs.readFileSync(highway3dJs, 'utf8') + '\n' + _h3dNoteRendererSrc; // h3d-carve-15: _ndGetNoteState / _ndHasProvider captures moved to renderer.js update()
const src = fs.readFileSync(highway3dJs, 'utf8') + '\n' + _h3dNoteRendererSrc + '\n' + fs.readFileSync(RENDERER_JS, 'utf8');
// Detect-mode behavior — verdict-window cull extension, chord-frame // Detect-mode behavior — verdict-window cull extension, chord-frame
// hold floor, and the smart drawNote cull — must be gated on a real // hold floor, and the smart drawNote cull — must be gated on a real
// provider being registered, not on the always-present bundle. // provider being registered, not on the always-present bundle.