mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-12 11:49:28 +00:00
feat(highway): render caged + guideTones teaching labels (§6.6) (#545)
Mirror the voicing/fn.rn teaching-mark render for the two new chord-template
fields, in both the 2D and 3D highways:
- Extend the shared pure chordHarmonyLabels() helper (identical in static/highway.js
and plugins/highway_3d/screen.js) to also surface caged ("CAGED: E") and
guideTones ("gt 4,10"), pre-formatted and node-testable. Invalid caged enum and
out-of-range / non-int guide tones are filtered out.
- Draw both, stacked above the existing rn/voicing labels, in distinct colors.
- Gated behind the SAME teaching-marks toggle (_showTeachingMarks 2D /
teachingMarksVisible 3D) — no clutter on the default highway.
Render only — no scoring / NoteVerifier coupling.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4195b73877
commit
e518910baa
@@ -10302,8 +10302,9 @@
|
|||||||
// teaching-marks opt-in (mirrors the 2D overlay). Display
|
// teaching-marks opt-in (mirrors the 2D overlay). Display
|
||||||
// only — never grading.
|
// only — never grading.
|
||||||
if (_drawTeachingMarks && firstInShapeRun && !chordWireHighDensity(ch)) {
|
if (_drawTeachingMarks && firstInShapeRun && !chordWireHighDensity(ch)) {
|
||||||
const _h = chordHarmonyLabels(ch.fn, bundle.chordTemplates?.[ch.id]?.voicing);
|
const _tmpl = bundle.chordTemplates?.[ch.id];
|
||||||
if (_h.rn || _h.voicing) {
|
const _h = chordHarmonyLabels(ch.fn, _tmpl?.voicing, _tmpl?.caged, _tmpl?.guideTones);
|
||||||
|
if (_h.rn || _h.voicing || _h.caged || _h.guideTones) {
|
||||||
const hlW = 24 * K * _textSizeMul;
|
const hlW = 24 * K * _textSizeMul;
|
||||||
const hlH = 9 * K * _textSizeMul;
|
const hlH = 9 * K * _textSizeMul;
|
||||||
const frameLeft = cx - width / 2;
|
const frameLeft = cx - width / 2;
|
||||||
@@ -10322,8 +10323,10 @@
|
|||||||
s.scale.set(hlW, hlH, 1);
|
s.scale.set(hlW, hlH, 1);
|
||||||
hy += hlH;
|
hy += hlH;
|
||||||
};
|
};
|
||||||
_drawHarmony(_h.rn, '#ffcc66'); // sd teaching color
|
_drawHarmony(_h.rn, '#ffcc66'); // sd teaching color
|
||||||
_drawHarmony(_h.voicing, '#7fd1ff'); // fg teaching color
|
_drawHarmony(_h.voicing, '#7fd1ff'); // fg teaching color
|
||||||
|
_drawHarmony(_h.caged, '#a0ffa0'); // CAGED shape teaching color
|
||||||
|
_drawHarmony(_h.guideTones, '#d0a0ff'); // guide-tone teaching color
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11390,13 +11393,19 @@
|
|||||||
return String(sd);
|
return String(sd);
|
||||||
}
|
}
|
||||||
/** Harmony annotations (§6.3.1 / §6.6): display labels for a chord's
|
/** Harmony annotations (§6.3.1 / §6.6): display labels for a chord's
|
||||||
* function (instance `fn.rn` Roman numeral) and template `voicing`
|
* function (instance `fn.rn` Roman numeral) and template `voicing`,
|
||||||
* string. '' for each when absent/malformed. Pure; shared with the 2D
|
* `caged` shape, and `guideTones`. '' for each when absent/malformed;
|
||||||
* highway and node-tested. Display only — never grading. */
|
* `caged`/`guideTones` come back pre-formatted ("CAGED: E" / "gt 4,10").
|
||||||
function chordHarmonyLabels(fn, voicing) {
|
* Pure; shared with the 2D highway and node-tested. Display only — never
|
||||||
|
* grading. */
|
||||||
|
function chordHarmonyLabels(fn, voicing, caged, guideTones) {
|
||||||
const rn = (fn && typeof fn.rn === 'string') ? fn.rn.trim() : '';
|
const rn = (fn && typeof fn.rn === 'string') ? fn.rn.trim() : '';
|
||||||
const vc = (typeof voicing === 'string') ? voicing.trim() : '';
|
const vc = (typeof voicing === 'string') ? voicing.trim() : '';
|
||||||
return { rn, voicing: vc };
|
const cg = (typeof caged === 'string' && /^[CAGED]$/.test(caged.trim()))
|
||||||
|
? 'CAGED: ' + caged.trim() : '';
|
||||||
|
const gt = Array.isArray(guideTones)
|
||||||
|
? guideTones.filter(n => Number.isInteger(n) && n >= 0 && n <= 11) : [];
|
||||||
|
return { rn, voicing: vc, caged: cg, guideTones: gt.length ? 'gt ' + gt.join(',') : '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
function bnvSampleAt(bnv, t) {
|
function bnvSampleAt(bnv, t) {
|
||||||
|
|||||||
+23
-7
@@ -511,13 +511,18 @@ function createHighway() {
|
|||||||
|
|
||||||
/** Harmony annotations (§6.3.1 / §6.6): display labels for a chord's
|
/** Harmony annotations (§6.3.1 / §6.6): display labels for a chord's
|
||||||
* harmonic function (the instance `fn.rn` Roman numeral) and its template
|
* harmonic function (the instance `fn.rn` Roman numeral) and its template
|
||||||
* `voicing` string. Returns '' for each when absent or malformed. Pure;
|
* `voicing`, `caged` shape, and `guideTones`. Returns '' for each when
|
||||||
* node-tested and shared by both highways. Display/teaching only — MUST
|
* absent or malformed; `caged`/`guideTones` come back pre-formatted
|
||||||
* NEVER feed a grader (honesty rule). */
|
* ("CAGED: E" / "gt 4,10"). Pure; node-tested and shared by both highways.
|
||||||
function chordHarmonyLabels(fn, voicing) {
|
* Display/teaching only — MUST NEVER feed a grader (honesty rule). */
|
||||||
|
function chordHarmonyLabels(fn, voicing, caged, guideTones) {
|
||||||
const rn = (fn && typeof fn.rn === 'string') ? fn.rn.trim() : '';
|
const rn = (fn && typeof fn.rn === 'string') ? fn.rn.trim() : '';
|
||||||
const vc = (typeof voicing === 'string') ? voicing.trim() : '';
|
const vc = (typeof voicing === 'string') ? voicing.trim() : '';
|
||||||
return { rn, voicing: vc };
|
const cg = (typeof caged === 'string' && /^[CAGED]$/.test(caged.trim()))
|
||||||
|
? 'CAGED: ' + caged.trim() : '';
|
||||||
|
const gt = Array.isArray(guideTones)
|
||||||
|
? guideTones.filter(n => Number.isInteger(n) && n >= 0 && n <= 11) : [];
|
||||||
|
return { rn, voicing: vc, caged: cg, guideTones: gt.length ? 'gt ' + gt.join(',') : '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Teaching mark (§6.2.2): bucket drawn notes by their strum-group key `ch`.
|
/** Teaching mark (§6.2.2): bucket drawn notes by their strum-group key `ch`.
|
||||||
@@ -2217,8 +2222,9 @@ function createHighway() {
|
|||||||
// class as sd/ch) so they don't clutter the default highway.
|
// class as sd/ch) so they don't clutter the default highway.
|
||||||
// Display only — never grading.
|
// Display only — never grading.
|
||||||
if (_showTeachingMarks && !ch.hd && p.scale > 0.15 && sorted.length > 0) {
|
if (_showTeachingMarks && !ch.hd && p.scale > 0.15 && sorted.length > 0) {
|
||||||
const { rn, voicing } = chordHarmonyLabels(ch.fn, tmpl && tmpl.voicing);
|
const { rn, voicing, caged, guideTones } = chordHarmonyLabels(
|
||||||
if (rn || voicing) {
|
ch.fn, tmpl && tmpl.voicing, tmpl && tmpl.caged, tmpl && tmpl.guideTones);
|
||||||
|
if (rn || voicing || caged || guideTones) {
|
||||||
const hx = hasNonZero
|
const hx = hasNonZero
|
||||||
? (xMin + xMax) / 2
|
? (xMin + xMax) / 2
|
||||||
: (sorted.length >= 2
|
: (sorted.length >= 2
|
||||||
@@ -2240,6 +2246,16 @@ function createHighway() {
|
|||||||
if (voicing) {
|
if (voicing) {
|
||||||
ctx.fillStyle = '#7fd1ff'; // matches the fg teaching color
|
ctx.fillStyle = '#7fd1ff'; // matches the fg teaching color
|
||||||
fillTextReadable(voicing, hx, stackY);
|
fillTextReadable(voicing, hx, stackY);
|
||||||
|
stackY -= sz * 0.45;
|
||||||
|
}
|
||||||
|
if (caged) {
|
||||||
|
ctx.fillStyle = '#a0ffa0'; // CAGED shape teaching color
|
||||||
|
fillTextReadable(caged, hx, stackY);
|
||||||
|
stackY -= sz * 0.45;
|
||||||
|
}
|
||||||
|
if (guideTones) {
|
||||||
|
ctx.fillStyle = '#d0a0ff'; // guide-tone teaching color
|
||||||
|
fillTextReadable(guideTones, hx, stackY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,21 +29,42 @@ const labels2D = loadFn('static/highway.js', 'chordHarmonyLabels');
|
|||||||
const labels3D = loadFn('plugins/highway_3d/screen.js', 'chordHarmonyLabels');
|
const labels3D = loadFn('plugins/highway_3d/screen.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`, () => {
|
test(`chordHarmonyLabels (${name}) surfaces rn + voicing + caged + guideTones`, () => {
|
||||||
assert.deepEqual(fn({ rn: 'ii7', q: 'm7', deg: 2 }, 'open'),
|
assert.deepEqual(fn({ rn: 'ii7', q: 'm7', deg: 2 }, 'open', 'E', [4, 10]),
|
||||||
{ rn: 'ii7', voicing: 'open' });
|
{ rn: 'ii7', voicing: 'open', caged: 'CAGED: E', guideTones: 'gt 4,10' });
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`chordHarmonyLabels (${name}) trims whitespace`, () => {
|
test(`chordHarmonyLabels (${name}) trims whitespace`, () => {
|
||||||
assert.deepEqual(fn({ rn: ' V7 ' }, ' drop2 '),
|
assert.deepEqual(fn({ rn: ' V7 ' }, ' drop2 ', ' G ', []),
|
||||||
{ rn: 'V7', voicing: 'drop2' });
|
{ rn: 'V7', voicing: 'drop2', caged: 'CAGED: G', guideTones: '' });
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`chordHarmonyLabels (${name}) empties absent / malformed inputs`, () => {
|
test(`chordHarmonyLabels (${name}) empties absent / malformed inputs`, () => {
|
||||||
assert.deepEqual(fn(null, undefined), { rn: '', voicing: '' });
|
assert.deepEqual(fn(null, undefined),
|
||||||
assert.deepEqual(fn({}, ''), { rn: '', voicing: '' });
|
{ rn: '', voicing: '', caged: '', guideTones: '' });
|
||||||
assert.deepEqual(fn({ rn: 7 }, 7), { rn: '', voicing: '' }); // non-string
|
assert.deepEqual(fn({}, ''),
|
||||||
assert.deepEqual(fn(undefined, 'shell'), { rn: '', voicing: 'shell' });
|
{ rn: '', voicing: '', caged: '', guideTones: '' });
|
||||||
assert.deepEqual(fn({ rn: 'vi' }, null), { rn: 'vi', voicing: '' });
|
assert.deepEqual(fn({ rn: 7 }, 7), // non-string
|
||||||
|
{ rn: '', voicing: '', caged: '', guideTones: '' });
|
||||||
|
assert.deepEqual(fn(undefined, 'shell'),
|
||||||
|
{ rn: '', voicing: 'shell', caged: '', guideTones: '' });
|
||||||
|
assert.deepEqual(fn({ rn: 'vi' }, null),
|
||||||
|
{ rn: 'vi', voicing: '', caged: '', guideTones: '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`chordHarmonyLabels (${name}) rejects invalid caged enum`, () => {
|
||||||
|
assert.equal(fn(null, null, 'X').caged, ''); // not a CAGED letter
|
||||||
|
assert.equal(fn(null, null, 'e').caged, ''); // lower-case rejected
|
||||||
|
assert.equal(fn(null, null, 7).caged, ''); // non-string
|
||||||
|
assert.equal(fn(null, null, ['E']).caged, ''); // non-string
|
||||||
|
assert.equal(fn(null, null, 'C').caged, 'CAGED: C');
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`chordHarmonyLabels (${name}) filters out-of-range / non-int guide tones`, () => {
|
||||||
|
assert.equal(fn(null, null, '', [12, -1, 3, 'x', 10]).guideTones, 'gt 3,10');
|
||||||
|
assert.equal(fn(null, null, '', [0, 11]).guideTones, 'gt 0,11'); // boundaries kept
|
||||||
|
assert.equal(fn(null, null, '', []).guideTones, '');
|
||||||
|
assert.equal(fn(null, null, '', '4,10').guideTones, ''); // non-array
|
||||||
|
assert.equal(fn(null, null, '', [12, -1]).guideTones, ''); // all dropped
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user