Add new chart-transform plugin capability (#1000)

* Chart-transform plugin capability

* PR comments

* Cleanup

* Fix markdown

* CodeRabbit feedback

Signed-off-by: Joe <jphinspace@gmail.com>

---------

Signed-off-by: Joe <jphinspace@gmail.com>
Co-authored-by: Byron Gamatos <xasiklas@gmail.com>
This commit is contained in:
Joe Hallahan
2026-07-19 11:27:52 +02:00
committed by GitHub
co-authored by Byron Gamatos
parent f7942f3689
commit 05be9ebdbe
18 changed files with 1345 additions and 54 deletions
+3 -2
View File
@@ -25,8 +25,8 @@
Object.freeze({
id: 'player-audio',
label: 'Player and Audio Runtime',
summary: 'Playback, renderer, mixer, monitoring, effects, and note-detection surfaces.',
domains: Object.freeze(['playback', 'visualization', 'audio-mix', 'audio-input', 'audio-monitoring', 'audio-effects', 'stems', 'note-detection']),
summary: 'Playback, renderer, chart-transform, mixer, monitoring, effects, and note-detection surfaces.',
domains: Object.freeze(['playback', 'visualization', 'chart-transform', 'audio-mix', 'audio-input', 'audio-monitoring', 'audio-effects', 'stems', 'note-detection']),
}),
Object.freeze({
id: 'plugin-defined',
@@ -50,6 +50,7 @@
'audio-monitoring': 'headphones',
stems: 'sliders',
'note-detection': 'activity',
'chart-transform': 'box',
diagnostics: 'fileSearch',
pipeline: 'activity',
'ui.navigation': 'list',
+1 -1
View File
@@ -155,7 +155,7 @@ Every per-frame renderer call receives a `bundle` from feedBack core. Fields use
- `lefty` — display flag consumed by this renderer from `bundle.lefty`. Captured into `_leftyCached` before each frame so `xFret()`, `xFretMid()`, `boardSpanX()`, board geometry, note placement, and the camera shoulder offset mirror the fret axis for left-handed mode. A runtime lefty flip rebuilds board state and mirrors `curX`/`tgtX` plus the lookahead camera X cache so the camera does not drift across the neck.
- `getNoteState(note, chartTime)` — feedBack#254; per-note judgment from a scorer (note_detect). Captured each frame into `_ndGetNoteState` at the top of `update()` and consulted in `drawNote()` AFTER the event-driven `_ndHitMarks`/`_ndMissMarks` lookup AND over the proximity-based `hit` heuristic, both of which it overrides when it has a verdict: `'hit'`/`'active'``mGlow[s]` outline (bright string-tinted, *not* green) + `mGlow[s]` body + `mGlow[s]` sustain trail + a queue entry for `drawNotedetectSizzle` (so a held sustain keeps glowing/sparkling as long as the provider keeps returning `'active'`); `'miss'``mMissOutline` and `_showHit = false` (suppresses the bright body even if the note is near the line). Called with the note's chart time (`n.t`), which is how note_detect keys its `noteResults` map — *not* `now`. Returns null on cores without the API or songs with no scorer — then the event path / `hit` heuristic drive feedback for older note_detect builds. **notedetect ≥1.13 object verdicts additionally carry `{ points, mult, popKey }`** (game-scoring layer): `points` is the note's awarded score, `mult` the multiplier tier it landed at, and `popKey` a dedup key — chord members all return the chord-level judgment's key so a chord pops once, not once per gem. Consumed by the score-pop spawn in `drawNote()` (see Score FX below); all three are absent on older notedetect builds, so guard with `!== undefined`.
`tuning` and `capo` aren't consumed by this plugin.
`tuning` and `capo` feed only the nut's open-string pitch labels. They prefer the bundle's effective values; `songInfo` remains the original metadata fallback. Note placement never reads them.
Core reuses the bundle OBJECT across frames (mutated in place); never cache it or compare its identity between frames — field values are only valid for the current draw call. Field-identity caches on ARRAY fields (this plugin's `_mergeCacheChordsRef === bundle.chords` etc.) remain valid: arrays still swap reference when chart data changes. Core also exposes `bundle.lowerBoundT(arr, time)` (lower-bound on `.t`, notes/chords) and `bundle.lowerBoundTime(arr, time)` (on `.time`, beats/anchors/sections) — prefer these over the local `lowerBoundT` helper when a downlevel-host fallback isn't needed.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "highway_3d",
"name": "3D Highway",
"version": "3.32.0",
"version": "3.33.0",
"type": "visualization",
"bundled": true,
"script": "screen.js",
+16 -10
View File
@@ -858,9 +858,13 @@
*/
function _openStringPitchLabelsForTuning(bundle, songInfo, nEffective) {
const n = Number.isFinite(nEffective) ? Math.min(Math.max(1, Math.trunc(nEffective)), MAX_RENDER_STRINGS) : resolveStringCount(bundle);
let tuning = (songInfo && songInfo.tuning) || bundle.tuning;
let cap = songInfo && songInfo.capo;
cap = Number.isFinite(cap) ? cap : (Number.isFinite(bundle.capo) ? bundle.capo : 0);
// bundle first: chart-transform substitutes tuning/capo there, while
// songInfo keeps the chart's originals by contract. A malformed
// (non-array) bundle.tuning falls back to songInfo instead of
// blanking the labels.
let tuning = Array.isArray(bundle.tuning) ? bundle.tuning : (songInfo && songInfo.tuning);
let cap = bundle.capo;
cap = Number.isFinite(cap) ? cap : (songInfo && Number.isFinite(songInfo.capo) ? songInfo.capo : 0);
if (!Array.isArray(tuning)) tuning = [];
const base = _baseOpenStringMidis(n, songInfo?.arrangement);
@@ -5604,13 +5608,15 @@
function _openStringLabelSignature(bundle, labels) {
const si = bundle && bundle.songInfo;
const tun = si && si.tuning;
// Same bundle-first preference as _openStringPitchLabelsForTuning.
let tStr = '';
if (Array.isArray(tun)) tStr = tun.slice(0, labels.length).join(',');
else if (bundle && Array.isArray(bundle.tuning)) tStr = bundle.tuning.slice(0, labels.length).join(',');
if (bundle && Array.isArray(bundle.tuning)) tStr = bundle.tuning.slice(0, labels.length).join(',');
else if (si && Array.isArray(si.tuning)) tStr = si.tuning.slice(0, labels.length).join(',');
// Fallback 0 matches _openStringPitchLabelsForTuning, so the
// signature reflects exactly what was rendered.
const capo =
si && Number.isFinite(si.capo) ? si.capo
: (bundle && Number.isFinite(bundle.capo) ? bundle.capo : '');
bundle && Number.isFinite(bundle.capo) ? bundle.capo
: (si && Number.isFinite(si.capo) ? si.capo : 0);
const arrIdx = si && si.arrangement_index != null ? si.arrangement_index : '';
let palSig = '';
const nLab = labels.length;
@@ -5645,8 +5651,8 @@
const tunRef = (si && Array.isArray(si.tuning)) ? si.tuning : null;
const bundleTunRef = Array.isArray(bundle.tuning) ? bundle.tuning : null;
const capo =
si && Number.isFinite(si.capo) ? si.capo
: (Number.isFinite(bundle.capo) ? bundle.capo : NaN);
Number.isFinite(bundle.capo) ? bundle.capo
: (si && Number.isFinite(si.capo) ? si.capo : 0);
const arrIdx = si && si.arrangement_index != null ? si.arrangement_index : undefined;
if (
_tuningLabelSprites.length === nStr &&