mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-11 04:44:31 +00:00
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:
co-authored by
Claude Sonnet 4.6
parent
b02c760aec
commit
7623ad85e3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "highway_3d",
|
||||
"name": "3D Highway",
|
||||
"version": "3.50.0",
|
||||
"version": "3.51.0",
|
||||
"type": "visualization",
|
||||
"scriptType": "module",
|
||||
"bundled": true,
|
||||
|
||||
+252
-3538
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,8 @@ const path = require('node:path');
|
||||
const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js');
|
||||
// h3d-carve-12: chordShapeCoveredByStandaloneNotes moved to 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', () => {
|
||||
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
|
||||
// regression PR #262 fixed. Pin both predicates so a refactor that drops
|
||||
// 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(
|
||||
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*\)/,
|
||||
@@ -39,14 +41,14 @@ test('noteStreamCoversArpShape is computed lazily (called, not eagerly bound)',
|
||||
// Eager allocation regressed perf on dense charts (Copilot review on PR
|
||||
// #262). The shape must be a callable so short-circuit evaluation skips
|
||||
// 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(
|
||||
src,
|
||||
/const\s+noteStreamCoversArpShape\s*=\s*(?:\(\s*\)\s*=>|function(?:\s+\w+)?\s*\(\s*\))/,
|
||||
'noteStreamCoversArpShape must be an arrow/function so the scan is lazy',
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
src,
|
||||
fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + fs.readFileSync(RENDERER_JS, 'utf8'),
|
||||
/const\s+noteStreamCoversArpShape\s*=\s*chordShapeCoveredByStandaloneNotes\(/,
|
||||
'noteStreamCoversArpShape must not eagerly invoke the coverage helper',
|
||||
);
|
||||
|
||||
@@ -12,7 +12,9 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
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.
|
||||
const GEOMETRY_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'geometry.js');
|
||||
const geoSrc = fs.readFileSync(GEOMETRY_JS, 'utf8');
|
||||
@@ -121,7 +123,7 @@ test('bootstrap runs once when complete chart arrays arrive', () => {
|
||||
);
|
||||
assert.match(
|
||||
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',
|
||||
);
|
||||
assert.match(
|
||||
@@ -131,7 +133,7 @@ test('bootstrap runs once when complete chart arrays arrive', () => {
|
||||
);
|
||||
assert.match(
|
||||
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',
|
||||
);
|
||||
});
|
||||
@@ -163,7 +165,7 @@ test('steady and lookahead modes initialize immediately from future chart data',
|
||||
);
|
||||
assert.match(
|
||||
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',
|
||||
);
|
||||
});
|
||||
@@ -180,24 +182,31 @@ test('silent-intro hold hands off only when live framing is ready', () => {
|
||||
);
|
||||
assert.match(
|
||||
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',
|
||||
);
|
||||
assert.match(
|
||||
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',
|
||||
);
|
||||
});
|
||||
|
||||
test('song changes and teardown reset every bootstrap state field', () => {
|
||||
const resetAssignments = src.match(
|
||||
/_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,
|
||||
// h3d-carve-15: song-change path uses setter calls (renderer.js);
|
||||
// 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(
|
||||
resetAssignments.length,
|
||||
totalResets,
|
||||
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})`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
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.
|
||||
const CAMERA_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'camera.js');
|
||||
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', () => {
|
||||
// Intra-measure beats carry measure === -1 and must be skipped.
|
||||
// h3d-carve-15: bare _measureStarts = _ms → setMeasureStarts(_ms) in renderer.js
|
||||
assert.match(
|
||||
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',
|
||||
);
|
||||
});
|
||||
@@ -130,9 +133,10 @@ test('measure-start cache is invalidated on song change', () => {
|
||||
// The song-change reset (reconnect path) resets _camSnapped; it must also
|
||||
// drop the measure-start cache, otherwise lookaheadEndTime sizes the window
|
||||
// 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(
|
||||
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',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
// patterns must now search both files.
|
||||
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;
|
||||
/** Returns screen.js + note-renderer.js concatenated for pattern matching. */
|
||||
function src() {
|
||||
if (!_src) {
|
||||
_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;
|
||||
}
|
||||
|
||||
@@ -17,10 +17,11 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
// must now search both files.
|
||||
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 _rendererSrc = fs.readFileSync(RENDERER_JS, 'utf8');
|
||||
|
||||
test('lean sustain rendering is the default (_leanSus starts true)', () => {
|
||||
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', () => {
|
||||
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(
|
||||
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",
|
||||
);
|
||||
});
|
||||
|
||||
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
|
||||
// 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.
|
||||
const gates = src.match(/if\s*\(\s*!_leanSus\s*\)/g) || [];
|
||||
const gates = src.match(/if\s*\(\s*!getLeanSus\s*\(\s*\)\s*\)/g) || [];
|
||||
assert.equal(
|
||||
gates.length,
|
||||
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(
|
||||
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()',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
// search both files.
|
||||
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
|
||||
@@ -57,7 +58,8 @@ let _src;
|
||||
function src() {
|
||||
if (!_src) {
|
||||
_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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
@@ -17,10 +17,11 @@ const path = require('node:path');
|
||||
const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.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 RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'renderer.js');
|
||||
const _noteSrc = fs.readFileSync(NOTE_RENDERER_JS, 'utf8');
|
||||
|
||||
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(
|
||||
src,
|
||||
/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', () => {
|
||||
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(
|
||||
src,
|
||||
/_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', () => {
|
||||
// drawNote(n, now, openX, skipLabel, skipBody, ...) — _isSlideTgt sits in
|
||||
// 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(
|
||||
src,
|
||||
/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', () => {
|
||||
// The trail block must stay outside the !skipBody gem gate so suppressed
|
||||
// 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(
|
||||
src,
|
||||
/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 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).
|
||||
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', () => {
|
||||
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)');
|
||||
// Strict === false so downlevel hosts (isPlaying undefined) fall through
|
||||
// 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
|
||||
// (no forward extrapolation).
|
||||
// h3d-carve-15: bare assignments → DI setter calls in renderer.js
|
||||
const branch = fn.slice(guardIdx);
|
||||
assert.match(branch, /_clkAudioT\s*=\s*raw/, '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, /return\s*\(\s*_frameNow\s*=\s*raw\s*\)/, 'pause branch must return raw');
|
||||
assert.match(branch, /setClkAudioT\s*\(\s*raw\s*\)/, 'pause branch must re-anchor _clkAudioT to raw');
|
||||
assert.match(branch, /setClkPerf\s*\(\s*p\s*\)/, 'pause branch must re-anchor _clkPerf to now');
|
||||
assert.match(branch, /setFrameNow\s*\([^)]+\)/, 'pause branch must call setFrameNow (return raw)');
|
||||
|
||||
// The pause gate must come before the new-sample re-anchor / interpolation
|
||||
// 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(guardIdx < newSampleIdx, 'isPlaying pause gate must precede the interpolation path');
|
||||
});
|
||||
|
||||
@@ -13,23 +13,26 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
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', () => {
|
||||
// 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
|
||||
// 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(
|
||||
src,
|
||||
rendererSrc,
|
||||
/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',
|
||||
);
|
||||
});
|
||||
|
||||
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(
|
||||
src,
|
||||
rendererSrc,
|
||||
/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',
|
||||
);
|
||||
|
||||
@@ -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', () => {
|
||||
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\)/,
|
||||
'label derivation reads a well-formed bundle.tuning first, songInfo otherwise');
|
||||
assert.match(src, /let cap = bundle\.capo;/,
|
||||
|
||||
@@ -17,6 +17,7 @@ const highwayJs = path.join(__dirname, '..', '..', 'static', 'highway.js');
|
||||
// cannot import per-instance state without two panels sharing it.
|
||||
const primitivesJs = path.join(__dirname, '..', '..', 'static', 'js', 'highway-state-primitives.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
|
||||
// pin its patterns must now also search 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', () => {
|
||||
// h3d-carve-14: _ndGetNoteState captured in update() (screen.js); _showHit
|
||||
// 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');
|
||||
// Provider verdict wins: miss => not _showHit; otherwise provider state
|
||||
// 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)', () => {
|
||||
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
|
||||
// hold floor, and the smart drawNote cull — must be gated on a real
|
||||
// provider being registered, not on the always-present bundle.
|
||||
|
||||
Reference in New Issue
Block a user