mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
feat(highway): render teaching marks fg/ch/sd on 2D + 3D (§6.2.2) (#538)
Render the three per-note teaching marks on both highways, mirroring the bend-curve render (#532). Display only — no scoring / NoteVerifier coupling. - 2D static/highway.js: fg renders by default as a small finger numeral hugging the gem (T = thumb, 1..4); sd (degree label) and ch (strum bracket connecting notes that share a ch key, arrow direction from pkd) are opt-in behind a new `showTeachingMarks` toggle (exposed via toggle/get/set + the bundle's `teachingMarksVisible` flag). Pure helpers teachingFingerLabel / teachingDegreeLabel / strumGroupBuckets drive the glyphs. ch bracket is note-stream-only (chord notes already read as one gesture). - 3D plugins/highway_3d/screen.js: fg (default) + sd (opt-in, mirrors the 2D toggle via bundle.teachingMarksVisible) render next to the per-note fret label via a new pooled sprite (pTeachMarkLbl); _scrChordNote resets fg/sd so chord notes don't inherit stale marks. ch strum brackets are deferred in 3D (no cross-note batch pass in the per-note render); 2D covers ch. Tests: tests/js/highway_teaching_marks.test.js extracts the pure helpers from both files (extract-and-eval) and asserts label mapping + strum-group bucketing. Part of got-feedback/feedback#334 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
6ee5da3d8b
commit
0f1006972b
@@ -2534,6 +2534,9 @@
|
||||
let _drawRecentByString = null;
|
||||
/** Snapshotted in update() — drawNote() is a sibling of update(), not nested in its closure. */
|
||||
let _drawChordTemplates = null;
|
||||
/** Teaching marks sd/ch overlay pref (§6.2.2), mirrored from the 2D
|
||||
* highway's `teachingMarksVisible` bundle flag. fg renders regardless. */
|
||||
let _drawTeachingMarks = false;
|
||||
let _laneTargetColor = null;
|
||||
let _renderScale = 1;
|
||||
let lyricsCanvas = null, lyricsCtx = null;
|
||||
@@ -3017,6 +3020,7 @@
|
||||
let gPMXLines = null, pMuteXLines = null; // PM X lines combined geometry (8 segs as quads)
|
||||
let gFHXLines = null, pFHXLines = null; // FH X lines combined geometry
|
||||
let pNoteFretLabel, pConnectorLine, pDropLine, pTapChevron, pAccentHalo;
|
||||
let pTeachMarkLbl; // teaching marks fg/sd label sprites (§6.2.2)
|
||||
let pHaloBar = null, gHaloBar = null; // gradient halo bar geometry — replaces per-shell pChordAccentHalo
|
||||
let gArpBracket = null; // shared 1×1×1 box geometry for pArpBracket; built once, disposed in teardown
|
||||
let pSusRibbon = null, pSusRibbonOl = null;
|
||||
@@ -6095,6 +6099,14 @@
|
||||
_nfl.material.depthTest = false;
|
||||
return _nfl;
|
||||
});
|
||||
// Teaching marks fg/sd labels (§6.2.2). One pool, two get()s per note
|
||||
// (finger + degree); the texture is swapped per draw via material.map.
|
||||
pTeachMarkLbl = pool(lblG, () => {
|
||||
const _tml = new T.Sprite(txtMat('0', '#7fd1ff', false, 'teachMark').clone());
|
||||
_tml.material.fog = false;
|
||||
_tml.material.depthTest = false;
|
||||
return _tml;
|
||||
});
|
||||
pConnectorLine = pool(noteG, () => new T.Line(
|
||||
new T.BufferGeometry().setFromPoints([new T.Vector3(0, 0, 0), new T.Vector3(0, 1, 0)]),
|
||||
new T.LineBasicMaterial({ color: 0xaaaaaa, transparent: true, opacity: 0.5, depthTest: false }),
|
||||
@@ -6153,6 +6165,7 @@
|
||||
pSusRailBloom.warm(_WARM_CHORD);
|
||||
pTechPlane.warm(_WARM_CHORD);
|
||||
pNoteFretLabel.warm(_WARM_NOTE);
|
||||
pTeachMarkLbl.warm(_WARM_NOTE);
|
||||
pChordFrameFill.warm(_WARM_CHORD);
|
||||
pChordBox.warm(_WARM_CHORD);
|
||||
pChordLbl.warm(_WARM_CHORD);
|
||||
@@ -8353,6 +8366,7 @@
|
||||
if (pMuteXLines) pMuteXLines.reset();
|
||||
if (pFHXLines) pFHXLines.reset();
|
||||
pNoteFretLabel.reset(); pConnectorLine.reset(); pDropLine.reset();
|
||||
pTeachMarkLbl.reset();
|
||||
pFretColMarker.reset(); pSusRail.reset(); pSusRailBloom.reset(); pTechPlane.reset();
|
||||
// Clear per-frame queues in-place (avoid reallocating the array object).
|
||||
_ndLabels.length = 0;
|
||||
@@ -8816,6 +8830,7 @@
|
||||
|
||||
_drawNextByString = nextNoteByString;
|
||||
_drawChordTemplates = bundle.chordTemplates ?? null;
|
||||
_drawTeachingMarks = !!bundle.teachingMarksVisible;
|
||||
|
||||
// ── Recent-past event per string (for _nextAnyT deadline) ─────
|
||||
// Once a note/chord passes `now` it leaves _drawNextByString,
|
||||
@@ -9830,6 +9845,12 @@
|
||||
// apply the wrong contour). Reset explicitly.
|
||||
_scrChordNote.bnv = Array.isArray(cn.bnv) ? cn.bnv : undefined;
|
||||
_scrChordNote.bt = cn.bt || 0;
|
||||
// Same stale-scratch hazard for the teaching marks
|
||||
// (§6.2.2): fg/sd are omit-when-default on the wire,
|
||||
// so a chord note without them must reset to -1 or it
|
||||
// inherits the previous note's finger/degree label.
|
||||
_scrChordNote.fg = Number.isInteger(cn.fg) ? cn.fg : -1;
|
||||
_scrChordNote.sd = Number.isInteger(cn.sd) ? cn.sd : -1;
|
||||
drawNote(
|
||||
_scrChordNote,
|
||||
now,
|
||||
@@ -11316,6 +11337,22 @@
|
||||
return visualIdx >= (nStr - 1) * 0.5 ? -1 : 1;
|
||||
}
|
||||
|
||||
// Teaching marks (§6.2.2) — display only, never grading. Pure label
|
||||
// helpers, mirroring static/highway.js so the two highways agree;
|
||||
// node-tested via tests/js/highway_teaching_marks.test.js.
|
||||
function teachingFingerLabel(fg) {
|
||||
// fret-hand finger: '' when unset/out of range; 0 -> 'T' (thumb),
|
||||
// 1..4 -> '1'..'4'.
|
||||
if (!Number.isInteger(fg) || fg < 0 || fg > 4) return '';
|
||||
return fg === 0 ? 'T' : String(fg);
|
||||
}
|
||||
function teachingDegreeLabel(sd) {
|
||||
// scale degree: chromatic 0..11 above the active key tonic; '' when
|
||||
// unset/out of range.
|
||||
if (!Number.isInteger(sd) || sd < 0 || sd > 11) return '';
|
||||
return String(sd);
|
||||
}
|
||||
|
||||
function bnvSampleAt(bnv, t) {
|
||||
// Linear interpolation of a bend curve [{t, v}] (§6.2.1; t is
|
||||
// seconds from the note onset) at elapsed time t. Clamps to the
|
||||
@@ -12260,6 +12297,32 @@
|
||||
fretLabel.scale.set(flS, flS, 1);
|
||||
fretLabel.material.opacity = alpha;
|
||||
}
|
||||
|
||||
// Teaching marks (§6.2.2) — display only, never grading. The
|
||||
// fret-hand finger (fg) renders by default to the right of the
|
||||
// fret label; the scale degree (sd) is opt-in (mirrors the 2D
|
||||
// `teachingMarksVisible` toggle) and renders to the left.
|
||||
if (alpha > 0 && n.f > 0) {
|
||||
const _tmS = 5.0 * K * _textSizeMul * fretLabelScaleForFret(n.f);
|
||||
const _drawTeachMark = (text, colorHex, dx, cacheKey) => {
|
||||
if (!text) return;
|
||||
const spr = pTeachMarkLbl.get();
|
||||
const m = txtMat(text, colorHex, false, cacheKey);
|
||||
if (spr.material.map !== m.map) {
|
||||
spr.material.map = m.map;
|
||||
spr.material.needsUpdate = true;
|
||||
}
|
||||
spr.position.set(x + dx, labelY, noteZ);
|
||||
spr.renderOrder = renderOrderForLayerAtZ(noteZ,
|
||||
_isArpNote ? 'ARP_NOTE_FRET_LABEL' : 'NOTE_FRET_LABEL');
|
||||
spr.scale.set(_tmS, _tmS, 1);
|
||||
spr.material.opacity = alpha;
|
||||
};
|
||||
_drawTeachMark(teachingFingerLabel(n.fg), '#7fd1ff', NW * 0.95, 'teachFg');
|
||||
if (_drawTeachingMarks) {
|
||||
_drawTeachMark(teachingDegreeLabel(n.sd), '#ffcc66', -NW * 0.95, 'teachSd');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12973,7 +13036,7 @@
|
||||
_renderScale = 1;
|
||||
mBeatM = mBeatQ = null;
|
||||
pNote = pNoteEdge = pSus = pSusOutline = pSusRibbon = pSusRibbonOl = pLbl = pBeat = pSec = null;
|
||||
pFretLbl = pLane = pLaneDivider = pGhostFretLbl = pChordBox = pChordFrameFill = pChordLbl = pBarreLine = pArpBracket = pNoteFretLabel = pConnectorLine = pDropLine = pTapChevron = pAccentHalo = pHaloBar = pPMXFill = pFHXFill = pMuteXLines = pFHXLines = null;
|
||||
pFretLbl = pLane = pLaneDivider = pGhostFretLbl = pChordBox = pChordFrameFill = pChordLbl = pBarreLine = pArpBracket = pNoteFretLabel = pConnectorLine = pDropLine = pTapChevron = pAccentHalo = pHaloBar = pPMXFill = pFHXFill = pMuteXLines = pFHXLines = pTeachMarkLbl = null;
|
||||
if (gPMXFill) { gPMXFill.dispose(); gPMXFill = null; }
|
||||
if (gFHXFill) { gFHXFill.dispose(); gFHXFill = null; }
|
||||
if (gPMXLines) { gPMXLines.dispose(); gPMXLines = null; }
|
||||
|
||||
Reference in New Issue
Block a user