feat(highway): render chord harmony fn.rn + voicing on 2D + 3D (§6.3.1, §6.6) (#541)

* feat(core): carry chord harmony fn + template voicing on the wire (§6.3.1, §6.6)

Add two OPTIONAL per-chord harmony annotations (feedpak 1.7.0), mirroring the
teaching-marks (fg/ch/sd) wire work:

- Chord.fn (instance): {rn, q, deg} harmonic-function object, key-dependent.
  Validated by _validate_fn on BOTH decode and emit so a partial / out-of-range
  fn (which would fail the schema's required-keys rule) never rides the wire.
  Default-omitted, mirroring bend bnv.
- ChordTemplate.voicing (template): key-independent voicing-type string
  ("open", "triad", "shell", "drop2", "barre", ...). Emitted only when
  non-empty; non-string wire values fall back to "".

Display/teaching only — never fed to a grader (honesty rule). fn auto-derivation
is DEFERRED (carry-only): a complete rn/q needs chord-quality analysis, and a
deg-only fn would be schema-invalid, so server.py carries author-provided fn
unchanged. GP import unchanged (no reliable per-chord function/voicing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(highway): render chord harmony fn.rn + voicing on 2D + 3D (§6.3.1, §6.6)

Draw the chord's harmonic-function Roman numeral (instance fn.rn) and its
template voicing string, stacked above the chord name on both highways. A shared
pure helper chordHarmonyLabels(fn, voicing) formats the two labels (empty when
absent/malformed) and is node-tested against both files.

Both labels are gated behind the EXISTING teaching-marks opt-in
(_showTeachingMarks / teachingMarksVisible bundle flag) — they're chord-level
teaching overlays, same class as sd/ch, so they stay off the default highway.
2D guards the empty-note-chord case; 3D reuses the gold chord-label sprite style.

Render only — no scoring / NoteVerifier path is touched (honesty rule).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Byron Gamatos
2026-06-21 11:03:15 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent ea22791984
commit 3fc077cbc1
3 changed files with 133 additions and 0 deletions
+40
View File
@@ -10296,6 +10296,37 @@
lbl.scale.set(lblWS, lblHS, 1);
}
// Harmony annotations (§6.3.1 / §6.6) — the chord's
// function (fn.rn Roman numeral) and template voicing,
// stacked above the chord name. Gated by the
// teaching-marks opt-in (mirrors the 2D overlay). Display
// only — never grading.
if (_drawTeachingMarks && firstInShapeRun && !chordWireHighDensity(ch)) {
const _h = chordHarmonyLabels(ch.fn, bundle.chordTemplates?.[ch.id]?.voicing);
if (_h.rn || _h.voicing) {
const hlW = 24 * K * _textSizeMul;
const hlH = 9 * K * _textSizeMul;
const frameLeft = cx - width / 2;
const baseX = frameLeft - hlW / 2 + NW * 0.94;
const opacity = Math.min(1, 0.3 + fade * 0.7) * chordTailMul;
// Start one chord-name-height above the name and
// stack upward so labels never overlap the gems.
let hy = yMaxF + hlH * 1.6;
const _drawHarmony = (text, colorHex) => {
if (!text) return;
const s = pChordLbl.get();
const m = txtMat(text, colorHex, true, 'chord');
if (s.material.map !== m.map) { s.material.map = m.map; s.material.needsUpdate = true; }
s.material.opacity = opacity;
s.position.set(baseX, hy, z);
s.scale.set(hlW, hlH, 1);
hy += hlH;
};
_drawHarmony(_h.rn, '#ffcc66'); // sd teaching color
_drawHarmony(_h.voicing, '#7fd1ff'); // fg teaching color
}
}
// Shape-based barre detection for the 3D indicator.
// Drives off chord notes alone — independent of label
// availability, so charts whose chordTemplates lack a
@@ -11358,6 +11389,15 @@
if (!Number.isInteger(sd) || sd < 0 || sd > 11) return '';
return String(sd);
}
/** Harmony annotations (§6.3.1 / §6.6): display labels for a chord's
* function (instance `fn.rn` Roman numeral) and template `voicing`
* string. '' for each when absent/malformed. Pure; shared with the 2D
* highway and node-tested. Display only never grading. */
function chordHarmonyLabels(fn, voicing) {
const rn = (fn && typeof fn.rn === 'string') ? fn.rn.trim() : '';
const vc = (typeof voicing === 'string') ? voicing.trim() : '';
return { rn, voicing: vc };
}
function bnvSampleAt(bnv, t) {
// Linear interpolation of a bend curve [{t, v}] (§6.2.1; t is