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:
Byron Gamatos
2026-06-21 11:58:00 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 4195b73877
commit e518910baa
3 changed files with 72 additions and 26 deletions
+23 -7
View File
@@ -511,13 +511,18 @@ function createHighway() {
/** Harmony annotations (§6.3.1 / §6.6): display labels for a chord's
* harmonic function (the instance `fn.rn` Roman numeral) and its template
* `voicing` string. Returns '' for each when absent or malformed. Pure;
* node-tested and shared by both highways. Display/teaching only — MUST
* NEVER feed a grader (honesty rule). */
function chordHarmonyLabels(fn, voicing) {
* `voicing`, `caged` shape, and `guideTones`. Returns '' for each when
* absent or malformed; `caged`/`guideTones` come back pre-formatted
* ("CAGED: E" / "gt 4,10"). Pure; node-tested and shared by both highways.
* 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 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`.
@@ -2217,8 +2222,9 @@ function createHighway() {
// class as sd/ch) so they don't clutter the default highway.
// Display only — never grading.
if (_showTeachingMarks && !ch.hd && p.scale > 0.15 && sorted.length > 0) {
const { rn, voicing } = chordHarmonyLabels(ch.fn, tmpl && tmpl.voicing);
if (rn || voicing) {
const { rn, voicing, caged, guideTones } = chordHarmonyLabels(
ch.fn, tmpl && tmpl.voicing, tmpl && tmpl.caged, tmpl && tmpl.guideTones);
if (rn || voicing || caged || guideTones) {
const hx = hasNonZero
? (xMin + xMax) / 2
: (sorted.length >= 2
@@ -2240,6 +2246,16 @@ function createHighway() {
if (voicing) {
ctx.fillStyle = '#7fd1ff'; // matches the fg teaching color
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);
}
}
}