mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-11 05:04:30 +00:00
feat(h3d-carve-14): extract V-section (note renderer) into src/note-renderer.js
Carve cut 14 of the h3d-carve epic. Moves the full V-section (note renderer):
drawNote, drawArpBrackets, drawNotedetectLabels, chordHarmonyLabels
and 14 private helpers (~1,433 lines)
from screen.js into plugins/highway_3d/src/note-renderer.js
as a factory-DI ES module: createNoteRenderer({137 DI params}).
Beyond-subst (3 sites):
_ndVerdictSawAlpha = true → setNdVerdictSawAlpha(true)
_ndVerdictMaxAlpha = v → setNdVerdictMaxAlpha(v)
_streakHits = 0/++ → setStreakHits(0/getStreakHits()+1)
Tests:
- New: tests/js/highway_3d_note_renderer.test.js (22 assertions)
wiring guard (137 params), export contract, chordHarmonyLabels
behavioral kills, beyond-subst sentinel, tombstone checks
- Updated 8 existing test files to search note-renderer.js
alongside screen.js for moved patterns (h3d-carve-14 retarget)
Bumps plugin.json 3.49.0 → 3.50.0
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
66aa829362
commit
c58c40f1ed
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "highway_3d",
|
"id": "highway_3d",
|
||||||
"name": "3D Highway",
|
"name": "3D Highway",
|
||||||
"version": "3.49.0",
|
"version": "3.50.0",
|
||||||
"type": "visualization",
|
"type": "visualization",
|
||||||
"scriptType": "module",
|
"scriptType": "module",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
|
|||||||
+111
-1436
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -23,11 +23,17 @@ 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');
|
||||||
|
// 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');
|
||||||
|
|
||||||
let _src;
|
let _src;
|
||||||
/** Returns the cached 3D highway screen source under test. */
|
/** Returns screen.js + note-renderer.js concatenated for pattern matching. */
|
||||||
function src() {
|
function src() {
|
||||||
if (!_src) _src = fs.readFileSync(SCREEN_JS, 'utf8');
|
if (!_src) {
|
||||||
|
_src = fs.readFileSync(SCREEN_JS, 'utf8')
|
||||||
|
+ '\n' + fs.readFileSync(NOTE_RENDERER_JS, 'utf8');
|
||||||
|
}
|
||||||
return _src;
|
return _src;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ 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');
|
||||||
|
// 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 _noteRendererSrc = fs.readFileSync(NOTE_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');
|
||||||
@@ -57,7 +61,8 @@ test('exactly one element is gated behind the lean flag, and it is the rail bloo
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('the trail + ribbon outline always draw and use the hit/miss-aware material', () => {
|
test('the trail + ribbon outline always draw and use the hit/miss-aware material', () => {
|
||||||
const src = fs.readFileSync(SCREEN_JS, 'utf8');
|
// h3d-carve-14: sustain trail body now in note-renderer.js
|
||||||
|
const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + _noteRendererSrc;
|
||||||
// Outline material is hit/miss aware: miss -> mMissOutline, confirmed hit
|
// Outline material is hit/miss aware: miss -> mMissOutline, confirmed hit
|
||||||
// -> bright, otherwise the default mSusOutline white border.
|
// -> bright, otherwise the default mSusOutline white border.
|
||||||
assert.match(
|
assert.match(
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
// h3d-carve-14: V-section (note renderer) pin tests.
|
||||||
|
//
|
||||||
|
// Guards:
|
||||||
|
// 1. Wiring: createNoteRenderer factory exists in note-renderer.js and the
|
||||||
|
// wiring in screen.js contains the exact expected DI param count (136).
|
||||||
|
// 2. Behavioral kill: chordHarmonyLabels is directly testable (pure fn);
|
||||||
|
// we gut and restore to prove the kill fires RED.
|
||||||
|
// 3. Export contract: all 4 exports exist and are functions.
|
||||||
|
// 4. Getter-aliasing: private helpers used by drawNote reference DI names.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const NOTE_RENDERER_JS = path.join(
|
||||||
|
__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'note-renderer.js'
|
||||||
|
);
|
||||||
|
const SCREEN_JS = path.join(
|
||||||
|
__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
const src = fs.readFileSync(NOTE_RENDERER_JS, 'utf8');
|
||||||
|
const screenSrc = fs.readFileSync(SCREEN_JS, 'utf8');
|
||||||
|
|
||||||
|
// ── 1. Wiring guard ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test('createNoteRenderer is exported from note-renderer.js', () => {
|
||||||
|
assert.match(src, /export function createNoteRenderer/,
|
||||||
|
'note-renderer.js must export createNoteRenderer');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('screen.js imports createNoteRenderer from note-renderer.js', () => {
|
||||||
|
assert.match(screenSrc, /import.*createNoteRenderer.*from.*note-renderer\.js/,
|
||||||
|
'screen.js must import createNoteRenderer');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('screen.js wiring block contains all 136 DI params', () => {
|
||||||
|
// Locate the wiring call; count getter arrows, setter arrows, and
|
||||||
|
// shorthand entries. Each property in the object literal is one entry.
|
||||||
|
// Strategy: extract the createNoteRenderer({...}) call text and count.
|
||||||
|
const wiringMatch = screenSrc.match(
|
||||||
|
/createNoteRenderer\(\{([\s\S]*?)\}\)/
|
||||||
|
);
|
||||||
|
assert.ok(wiringMatch, 'screen.js must contain createNoteRenderer({...}) call');
|
||||||
|
const wiringBody = wiringMatch[1];
|
||||||
|
|
||||||
|
// Count getter arrows getX: () => _x,
|
||||||
|
const getterCount = (wiringBody.match(/\bget[A-Z]\w+\s*:/g) || []).length;
|
||||||
|
// Count setter arrows setX: (v) => { ... },
|
||||||
|
const setterCount = (wiringBody.match(/\bset[A-Z]\w+\s*:/g) || []).length;
|
||||||
|
// Count shorthand identifiers: lines without '=>' and without a leading '//'
|
||||||
|
// can have multiple shorthands per line (e.g. "K, NFRETS, NW, NH, AHEAD,").
|
||||||
|
// Match each identifier followed by a comma or closing paren on such lines.
|
||||||
|
const shorthandCount = wiringBody.split('\n').reduce((acc, line) => {
|
||||||
|
const t = line.trim();
|
||||||
|
if (!t || t.startsWith('//') || t.includes('=>')) return acc;
|
||||||
|
const ids = t.match(/\b[A-Za-z_][A-Za-z0-9_]*\b(?=\s*,)/g) || [];
|
||||||
|
return acc + ids.length;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const total = getterCount + setterCount + shorthandCount;
|
||||||
|
// 137 = 27 constants + 31 fn-refs + 7 stable-refs (shorthands:65) + 69 getters + 3 setters
|
||||||
|
assert.strictEqual(total, 137,
|
||||||
|
`DI param count must be exactly 137 (got getters:${getterCount} setters:${setterCount} shorthands:${shorthandCount} = ${total})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 2. Factory returns all 4 exports ────────────────────────────────────────
|
||||||
|
|
||||||
|
test('createNoteRenderer returns drawNote', () => {
|
||||||
|
assert.match(src, /return\s*\{[\s\S]*?\bdrawNote\b[\s\S]*?\}/,
|
||||||
|
'factory must return drawNote');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createNoteRenderer returns drawArpBrackets', () => {
|
||||||
|
assert.match(src, /return\s*\{[\s\S]*?\bdrawArpBrackets\b[\s\S]*?\}/,
|
||||||
|
'factory must return drawArpBrackets');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createNoteRenderer returns drawNotedetectLabels', () => {
|
||||||
|
assert.match(src, /return\s*\{[\s\S]*?\bdrawNotedetectLabels\b[\s\S]*?\}/,
|
||||||
|
'factory must return drawNotedetectLabels');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createNoteRenderer returns chordHarmonyLabels', () => {
|
||||||
|
assert.match(src, /return\s*\{[\s\S]*?\bchordHarmonyLabels\b[\s\S]*?\}/,
|
||||||
|
'factory must return chordHarmonyLabels');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 3. Behavioral kill — chordHarmonyLabels (pure fn, testable directly) ────
|
||||||
|
|
||||||
|
// Extract and eval chordHarmonyLabels from the source for node testing.
|
||||||
|
// The function is defined inside createNoteRenderer; we pull it out as-is.
|
||||||
|
function extractChordHarmonyLabels(moduleSrc) {
|
||||||
|
// The function is declared as: function chordHarmonyLabels(fn, voicing, caged, guideTones) { ... }
|
||||||
|
// Find the opening and use bracket-depth to find closing.
|
||||||
|
const start = moduleSrc.indexOf('function chordHarmonyLabels(');
|
||||||
|
if (start === -1) return null;
|
||||||
|
let depth = 0;
|
||||||
|
let i = moduleSrc.indexOf('{', start);
|
||||||
|
const open = i;
|
||||||
|
for (; i < moduleSrc.length; i++) {
|
||||||
|
if (moduleSrc[i] === '{') depth++;
|
||||||
|
else if (moduleSrc[i] === '}') {
|
||||||
|
depth--;
|
||||||
|
if (depth === 0) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const fnSrc = moduleSrc.slice(start, i + 1);
|
||||||
|
// Wrap in a closure to evaluate
|
||||||
|
// eslint-disable-next-line no-new-func
|
||||||
|
return new Function(`return (${fnSrc})`)();
|
||||||
|
}
|
||||||
|
|
||||||
|
const chordHarmonyLabels = extractChordHarmonyLabels(src);
|
||||||
|
|
||||||
|
test('chordHarmonyLabels extracted from source is a function', () => {
|
||||||
|
assert.strictEqual(typeof chordHarmonyLabels, 'function',
|
||||||
|
'chordHarmonyLabels must be extractable and be a function');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('chordHarmonyLabels — valid RN + voicing', () => {
|
||||||
|
const fn = { rn: 'IV' };
|
||||||
|
const r = chordHarmonyLabels(fn, 'drop2', null, null);
|
||||||
|
assert.strictEqual(r.rn, 'IV');
|
||||||
|
assert.strictEqual(r.voicing, 'drop2');
|
||||||
|
assert.strictEqual(r.caged, '');
|
||||||
|
assert.strictEqual(r.guideTones, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('chordHarmonyLabels — valid CAGED shape', () => {
|
||||||
|
const r = chordHarmonyLabels(null, null, 'E', null);
|
||||||
|
assert.strictEqual(r.caged, 'CAGED: E');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('chordHarmonyLabels — invalid CAGED shape rejected', () => {
|
||||||
|
const r = chordHarmonyLabels(null, null, 'X', null);
|
||||||
|
assert.strictEqual(r.caged, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('chordHarmonyLabels — guideTones array', () => {
|
||||||
|
const r = chordHarmonyLabels(null, null, null, [4, 10]);
|
||||||
|
assert.strictEqual(r.guideTones, 'gt 4,10');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('chordHarmonyLabels — out-of-range guideTone filtered', () => {
|
||||||
|
const r = chordHarmonyLabels(null, null, null, [4, 12]);
|
||||||
|
assert.strictEqual(r.guideTones, 'gt 4');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('chordHarmonyLabels — all null → all empty', () => {
|
||||||
|
const r = chordHarmonyLabels(null, null, null, null);
|
||||||
|
assert.strictEqual(r.rn, '');
|
||||||
|
assert.strictEqual(r.voicing, '');
|
||||||
|
assert.strictEqual(r.caged, '');
|
||||||
|
assert.strictEqual(r.guideTones, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 4. Getter-aliasing discipline ────────────────────────────────────────────
|
||||||
|
|
||||||
|
test('drawNote aliases getLeftyCached at function entry', () => {
|
||||||
|
assert.match(src,
|
||||||
|
/function drawNote[\s\S]*?const _leftyCached\s*=\s*getLeftyCached\(\)/,
|
||||||
|
'drawNote must alias getLeftyCached() once at entry');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('drawNote aliases getPNote pool at entry', () => {
|
||||||
|
assert.match(src,
|
||||||
|
/function drawNote[\s\S]*?const pNote\s*=\s*getPNote\(\)/,
|
||||||
|
'drawNote must alias getPNote() pool getter once at entry');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('drawNote aliases getMStr material at entry', () => {
|
||||||
|
assert.match(src,
|
||||||
|
/function drawNote[\s\S]*?const mStr\s*=\s*getMStr\(\)/,
|
||||||
|
'drawNote must alias getMStr() material getter once at entry');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 5. Beyond-subst rewires present ──────────────────────────────────────────
|
||||||
|
|
||||||
|
test('setNdVerdictSawAlpha beyond-subst: setter called, not direct assignment', () => {
|
||||||
|
// Strip single-line comments so comment-docs don't trigger the check
|
||||||
|
const codeOnly = src.replace(/\/\/[^\n]*/g, '');
|
||||||
|
assert.doesNotMatch(codeOnly, /_ndVerdictSawAlpha\s*=\s*(true|false)/,
|
||||||
|
'V-section code must not directly assign _ndVerdictSawAlpha (beyond-subst: use setter)');
|
||||||
|
assert.match(src, /setNdVerdictSawAlpha\(true\)/,
|
||||||
|
'V-section must call setNdVerdictSawAlpha(true)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setStreakHits beyond-subst: setter called, not direct assignment', () => {
|
||||||
|
const codeOnly = src.replace(/\/\/[^\n]*/g, '');
|
||||||
|
assert.doesNotMatch(codeOnly, /_streakHits\s*=\s*0/,
|
||||||
|
'V-section code must not directly assign _streakHits = 0 (beyond-subst: use setStreakHits)');
|
||||||
|
assert.match(src, /setStreakHits\(0\)/,
|
||||||
|
'V-section must call setStreakHits(0) instead of _streakHits = 0');
|
||||||
|
assert.match(src, /setStreakHits\(getStreakHits\(\)\s*\+\s*1\)/,
|
||||||
|
'V-section must call setStreakHits(getStreakHits() + 1) for increment');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 6. Tombstone present in screen.js ────────────────────────────────────────
|
||||||
|
|
||||||
|
test('screen.js V-section tombstone is present', () => {
|
||||||
|
assert.match(screenSrc,
|
||||||
|
/h3d-carve-14.*V-section.*note-renderer/,
|
||||||
|
'screen.js must have the h3d-carve-14 tombstone comment');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('screen.js no longer contains slideRibbonUpdatePositions body', () => {
|
||||||
|
// After carve-14, only the module import/wrapper level should contain the
|
||||||
|
// function name (in the tombstone or import comments); the function body
|
||||||
|
// (with its internal `const pa =` assignment) must be gone.
|
||||||
|
assert.doesNotMatch(screenSrc, /function slideRibbonUpdatePositions/,
|
||||||
|
'screen.js must not contain the original slideRibbonUpdatePositions body after carve-14');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('screen.js no longer contains raw drawNote function body', () => {
|
||||||
|
// The function definition moved to note-renderer.js; screen.js must only
|
||||||
|
// destructure the export — not declare the function body itself.
|
||||||
|
const drawNoteBodyMatches = [
|
||||||
|
...screenSrc.matchAll(/function drawNote\b/g)
|
||||||
|
];
|
||||||
|
assert.strictEqual(drawNoteBodyMatches.length, 0,
|
||||||
|
'screen.js must not declare function drawNote after carve-14');
|
||||||
|
});
|
||||||
@@ -44,15 +44,21 @@ const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'scr
|
|||||||
// Since h3d-carve-1b, RENDER_ORDER_* constants and renderOrderForLayerAtZ
|
// Since h3d-carve-1b, RENDER_ORDER_* constants and renderOrderForLayerAtZ
|
||||||
// live in geometry.js; screen.js imports them.
|
// live in geometry.js; screen.js imports them.
|
||||||
const GEOMETRY_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'geometry.js');
|
const GEOMETRY_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'geometry.js');
|
||||||
|
// 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');
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
let _src;
|
let _src;
|
||||||
/** Returns the cached 3D highway screen source under test. */
|
/** Returns screen.js + note-renderer.js concatenated for pattern matching. */
|
||||||
function src() {
|
function src() {
|
||||||
if (!_src) _src = fs.readFileSync(SCREEN_JS, 'utf8');
|
if (!_src) {
|
||||||
|
_src = fs.readFileSync(SCREEN_JS, 'utf8')
|
||||||
|
+ '\n' + fs.readFileSync(NOTE_RENDERER_JS, 'utf8');
|
||||||
|
}
|
||||||
return _src;
|
return _src;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ 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');
|
||||||
|
// 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 _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');
|
const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + _noteSrc;
|
||||||
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\(/,
|
||||||
@@ -31,7 +34,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');
|
const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + _noteSrc;
|
||||||
assert.match(
|
assert.match(
|
||||||
src,
|
src,
|
||||||
/_isSlideTgt\s*=\s*!!\(\s*_slideTargetSet\s*&&\s*_slideTargetSet\.has\(/,
|
/_isSlideTgt\s*=\s*!!\(\s*_slideTargetSet\s*&&\s*_slideTargetSet\.has\(/,
|
||||||
@@ -42,7 +45,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');
|
const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + _noteSrc;
|
||||||
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*,/,
|
||||||
@@ -53,7 +56,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');
|
const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + _noteSrc;
|
||||||
assert.match(
|
assert.match(
|
||||||
src,
|
src,
|
||||||
/Rendered for ALL notes with sustain, including skipBody=true/,
|
/Rendered for ALL notes with sustain, including skipBody=true/,
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ function loadFn(file, name) {
|
|||||||
// R3c: the PURE geometry/label primitives were carved out of highway.js into
|
// R3c: the PURE geometry/label primitives were carved out of highway.js into
|
||||||
// static/js/highway-geometry.js. Same bodies, byte-for-byte — only the file moved.
|
// static/js/highway-geometry.js. Same bodies, byte-for-byte — only the file moved.
|
||||||
const bnvNormalizedPoints = loadFn('static/js/highway-geometry.js', 'bnvNormalizedPoints');
|
const bnvNormalizedPoints = loadFn('static/js/highway-geometry.js', 'bnvNormalizedPoints');
|
||||||
const bnvSampleAt = loadFn('plugins/highway_3d/screen.js', 'bnvSampleAt');
|
// h3d-carve-14: bnvSampleAt moved to note-renderer.js (private helper inside createNoteRenderer)
|
||||||
|
const bnvSampleAt = loadFn('plugins/highway_3d/src/note-renderer.js', 'bnvSampleAt');
|
||||||
|
|
||||||
// ── bnvNormalizedPoints (2D) ─────────────────────────────────────────────────
|
// ── bnvNormalizedPoints (2D) ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ function loadFn(file, name) {
|
|||||||
// R3c: the PURE geometry/label primitives were carved out of highway.js into
|
// R3c: the PURE geometry/label primitives were carved out of highway.js into
|
||||||
// static/js/highway-geometry.js. Same bodies, byte-for-byte — only the file moved.
|
// static/js/highway-geometry.js. Same bodies, byte-for-byte — only the file moved.
|
||||||
const labels2D = loadFn('static/js/highway-geometry.js', 'chordHarmonyLabels');
|
const labels2D = loadFn('static/js/highway-geometry.js', 'chordHarmonyLabels');
|
||||||
const labels3D = loadFn('plugins/highway_3d/screen.js', 'chordHarmonyLabels');
|
// h3d-carve-14: chordHarmonyLabels moved to note-renderer.js
|
||||||
|
const labels3D = loadFn('plugins/highway_3d/src/note-renderer.js', 'chordHarmonyLabels');
|
||||||
|
|
||||||
for (const [name, fn] of [['2D', labels2D], ['3D', labels3D]]) {
|
for (const [name, fn] of [['2D', labels2D], ['3D', labels3D]]) {
|
||||||
test(`chordHarmonyLabels (${name}) surfaces rn + voicing + caged + guideTones`, () => {
|
test(`chordHarmonyLabels (${name}) surfaces rn + voicing + caged + guideTones`, () => {
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ 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');
|
||||||
|
// 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');
|
||||||
|
const _h3dNoteRendererSrc = fs.readFileSync(_h3dNoteRendererJs, 'utf8');
|
||||||
|
|
||||||
// Brace-balanced extraction (same helper shape as highway_visibility.test.js).
|
// Brace-balanced extraction (same helper shape as highway_visibility.test.js).
|
||||||
function extractBlock(src, signature) {
|
function extractBlock(src, signature) {
|
||||||
@@ -138,7 +142,9 @@ 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', () => {
|
||||||
const src = fs.readFileSync(highway3dJs, 'utf8');
|
// 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;
|
||||||
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.
|
||||||
@@ -146,7 +152,7 @@ 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');
|
const src = fs.readFileSync(highway3dJs, 'utf8') + '\n' + _h3dNoteRendererSrc;
|
||||||
// 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.
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ function loadFn(file, name) {
|
|||||||
// static/js/highway-geometry.js. Same bodies, byte-for-byte — only the file moved.
|
// static/js/highway-geometry.js. Same bodies, byte-for-byte — only the file moved.
|
||||||
const fingerLabel2D = loadFn('static/js/highway-geometry.js', 'teachingFingerLabel');
|
const fingerLabel2D = loadFn('static/js/highway-geometry.js', 'teachingFingerLabel');
|
||||||
const degreeLabel2D = loadFn('static/js/highway-geometry.js', 'teachingDegreeLabel');
|
const degreeLabel2D = loadFn('static/js/highway-geometry.js', 'teachingDegreeLabel');
|
||||||
const fingerLabel3D = loadFn('plugins/highway_3d/screen.js', 'teachingFingerLabel');
|
// h3d-carve-14: teachingFingerLabel/Degree moved to note-renderer.js
|
||||||
const degreeLabel3D = loadFn('plugins/highway_3d/screen.js', 'teachingDegreeLabel');
|
const fingerLabel3D = loadFn('plugins/highway_3d/src/note-renderer.js', 'teachingFingerLabel');
|
||||||
|
const degreeLabel3D = loadFn('plugins/highway_3d/src/note-renderer.js', 'teachingDegreeLabel');
|
||||||
const strumGroupBuckets = loadFn('static/js/highway-draw.js', 'strumGroupBuckets');
|
const strumGroupBuckets = loadFn('static/js/highway-draw.js', 'strumGroupBuckets');
|
||||||
|
|
||||||
// ── teachingFingerLabel (fg) ─────────────────────────────────────────────────
|
// ── teachingFingerLabel (fg) ─────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user