fix(h3d): carve-15 full DI — ESLint no-undef=0 on renderer.js

Resolve all 160 undeclared names that were latent ReferenceErrors in the
ES-module-scoped renderer.js. Module scope never chains into screen.js's
IIFE scope; every name was a crash on first execution path.

Changes:
- renderer.js: move 44 private names into createRenderer closure (Category E
  no-screen-use); DI 13 shared mutable names as getter/setter pairs; DI 43
  Category-B consts, 37 Category-C fn-refs, 3 Category-D getters (ren/scene/cam),
  11 Category-F stable refs + getter/setter, 5 Category-G lane-material getters;
  2 extra getters for chordFrameGradTex/Arp.
- score-fx.js: add fxClearSeen() to exports so renderer can clear _fxSeen
  without a direct reference.
- screen.js: remove 44 declarations moved to closure; wire all 129 new DI
  params in createRenderer call; destructure fxClearSeen from createScoreFx.
- tests: update DI count pin 184→313; fix 4 test regexes for new API surface;
  add 3 actual execution smoke tests (new Function pattern, no-ReferenceError
  assertion, documents first crash at d475899).

ESLint no-undef: 0 errors on renderer.js.
Suite: 1405/1406 (test 46 pre-existing, unchanged since cut-8).
DI count: 313 (getters=105, setters=48, shorthands=160).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
This commit is contained in:
byrongamatos
2026-09-06 01:00:02 +02:00
co-authored by Claude Sonnet 4.6
parent d475899c5a
commit 06e4fe335a
6 changed files with 546 additions and 131 deletions
+79 -45
View File
@@ -3301,14 +3301,9 @@ import { createRenderer } from './src/renderer.js'; // h3d-carve-15
// Per-frame booleans: handShapes[i] passes inferArpeggioFromNotePattern
// once (see fillArpeggioGhostInferFlags) so the note loop skips O(hs×notes)
// rescans — ref fillArpeggioGhostInferFlags in update().
let _arpGhostHsInferScratch = [];
// Handshape start-times where ghost fret numbers show but [ ] brackets are suppressed
// (synth-chord onset-match cases — not genuine arpeggios).
let _arpSynthOnsetHsSet = new Set();
/** Per-frame: ``handShapeIsArpeggioForLaneRail`` baked once — lane slices were O(96 × hs × infer). */
let _arpLaneRailHsScratch = [];
let _arpRailBoundLoScratch = [];
let _arpRailBoundHiScratch = [];
// ── Cross-frame caches for chart-static derivations ──────────────
// The merge + arp-flag fills below depend only on chart-static
@@ -3319,9 +3314,6 @@ import { createRenderer } from './src/renderer.js'; // h3d-carve-15
// arrangements this avoids per-frame Set construction, nested
// O(hs × notes) scans, and a sort — significant FPS recovery.
let _mergeCacheResult = null;
let _mergeCacheChordsRef = null;
let _mergeCacheHsRef = null;
let _mergeCacheTplRef = null;
// Fret connector-label visibility cache: tracks which (time, fret)
// pairs may show their indicator number per the measure-skip rule
@@ -3339,9 +3331,6 @@ import { createRenderer } from './src/renderer.js'; // h3d-carve-15
// (arpeggio chords, synthetic chords) never produce stacked duplicate labels.
const _frameLabeledKeys = new Set();
let _arpGhostInferRefHs = null;
let _arpGhostInferRefNotes = null;
let _arpGhostInferRefTpl = null;
// Slide-target gem suppression. A Set of "t_s" keys for notes in
// bundle.notes that are the linkNext destination of a preceding note
@@ -3351,13 +3340,7 @@ import { createRenderer } from './src/renderer.js'; // h3d-carve-15
let _slideTargetNotesRef = null;
let _slideTargetChordsRef = null;
let _laneRailFlagsRefHs = null;
let _laneRailFlagsRefTpl = null;
let _laneRailBoundsRefHs = null;
let _laneRailBoundsRefChords = null;
let _laneRailBoundsRefTpl = null;
let _laneRailBoundsRefNotes = null;
let _lastHwW = 0, _lastHwH = 0;
// Frame counter for throttling the CSS-box drift check in draw()
// (getBoundingClientRect is a forced layout read; see the comment
@@ -3587,8 +3570,6 @@ import { createRenderer } from './src/renderer.js'; // h3d-carve-15
// ``time * 1e4`` keeps a 0.1 ms resolution — more than enough
// to disambiguate distinct chord onsets — and stays under the
// safe-integer limit for any realistic song length.
const _CV_KEY_TIME_MUL = 1e4;
const _CV_KEY_TIME_SLOT = 1e6;
function _encodeChordVerdictKey(ch) {
const tSlot = Math.round(ch.t * _CV_KEY_TIME_MUL) * _CV_KEY_TIME_SLOT;
const idSlot = ch.id != null ? ((Number(ch.id) | 0) + 1) : 0;
@@ -3622,7 +3603,7 @@ import { createRenderer } from './src/renderer.js'; // h3d-carve-15
let _susVerdictLatch = new Map();
/* ── h3d-carve-10: K-section (score FX) → src/score-fx.js ──────── */
const { fxInit, fxTeardown, fxSpawnPop: _fxSpawnPop, drawScoreFx } = createScoreFx({
const { fxInit, fxTeardown, fxSpawnPop: _fxSpawnPop, drawScoreFx, fxClearSeen } = createScoreFx({
getHighwayCanvas: () => highwayCanvas,
getNdFrameNowMs: () => _ndFrameNowMs,
getCam: () => cam,
@@ -3647,15 +3628,7 @@ import { createRenderer } from './src/renderer.js'; // h3d-carve-15
// an anchor (the common case) collapse into the same entry. Held as
// four parallel arrays so the per-frame work allocates nothing once
// the buffers reach their steady-state size.
const _laneSegDMin = [];
const _laneSegDMax = [];
const _laneSegZ0 = [];
const _laneSegZ1 = [];
/** Chart-time span per merged lane segment (for per-slice arpeggio rail tint). */
const _laneSegTLo = [];
const _laneSegTHi = [];
const _laneSegArp = [];
let _laneSegLen = 0;
let pChordBox, pChordFrameFill, pChordLbl, pBarreLine, pArpBracket, pPMXFill, pFHXFill;
let gPMXFill = null; // shared geometry for PM X fill — disposed in teardown
let gFHXFill = null; // shared geometry for FH X fill — disposed in teardown
@@ -3805,9 +3778,6 @@ import { createRenderer } from './src/renderer.js'; // h3d-carve-15
// Holding these at closure scope keeps them in a GC root; the engine can
// keep them hot in L1/L2 across frames, and no allocation pressure from
// update() itself.
const _scrStringSustain = new Array(MAX_RENDER_STRINGS).fill(false);
const _scrStringAnticipation = new Array(MAX_RENDER_STRINGS).fill(0);
const _scrFretHeat = new Array(NFRETS + 1).fill(0);
// Fret-wire hit flash. _fwHitIn is per-frame (cleared with the rest of
// the frame state, written by drawNote when a provider confirms a note);
// _fwHitGlow persists across frames so the flash can decay smoothly
@@ -3823,20 +3793,13 @@ import { createRenderer } from './src/renderer.js'; // h3d-carve-15
let _fwHitPrevTime = -Infinity; // chart time of the last decay step
let _fwHitColor = null; // T.Color scratch (built in initScene)
let _fwHitEmissive = null;
const _scrStrGlow = new Array(MAX_RENDER_STRINGS).fill(0.5);
const _scrAccentFillBoost = new Array(MAX_RENDER_STRINGS).fill(0);
const _scrNextNoteByString = new Array(MAX_RENDER_STRINGS).fill(null);
const _scrLastFretForString = new Array(MAX_RENDER_STRINGS).fill(undefined);
// Scratch buffer for the recent-past-event prepass (~0.6 s back) — avoids
// re-allocating a per-string Array every frame. Re-filled with -Infinity
// at the top of each prepass run.
const _scrRecentByString = new Array(MAX_RENDER_STRINGS).fill(-Infinity);
// Scratch buffers for the ghost-preview gap prepass — refilled each
// frame to avoid the `new Array(nStr)` + `Object.create(null)` churn.
// The Map is cleared at the top of the prepass; live entries are
// consumed by drawNote() reads later in the same frame.
const _scrGhostLastT = new Array(MAX_RENDER_STRINGS).fill(-Infinity);
const _scrGhostPrevBuf = new Map();
// Per-string count of upcoming-ghost slots (1/2) claimed so far this
// frame (board ghost — up to 3 simultaneous previews per string).
// Reset to 0 each frame alongside the other pool .reset() calls.
@@ -3847,25 +3810,18 @@ import { createRenderer } from './src/renderer.js'; // h3d-carve-15
// already drawn brackets in the AHEAD note-stream pass. Cleared at the
// top of every chord pass so the Set objects (and the outer Map) are
// reused across frames instead of reallocated.
const _scrNoteStreamBracketStrings = new Map();
// Scratch object reused for chord-note drawNote calls so `{ ...cn, t: ch.t }`
// doesn't allocate a new object per chord note per frame.
const _scrChordNote = {};
// Scratch objects for the nextNoteByString prepass — chord notes need
// a merged `{ ...cn, t: ch.t }` object, but spread allocates every frame.
// One scratch object per string (max MAX_RENDER_STRINGS) is safe because:
// (a) the prepass writes each string's entry at most once per frame,
// (b) drawNote() reads nxFrame.t before the next frame's prepass can overwrite.
const _scrNextNoteByStringData = Array.from({ length: MAX_RENDER_STRINGS }, () => ({}));
// Reusable Set for arpeggio persistence key lookup — cleared each frame
// instead of reallocating a new Set.
const _scrArpPersistKeys = new Set();
// Reusable Set for active-fret cooldown tracking — cleared each frame.
const _scrActiveFrets = new Set();
// Reusable scratch for barre atMinFretStrings computation — avoids the
// [...chShape].filter().map().sort() chain (3 allocations per chord per frame).
const _scrAtMinFretArr = new Array(MAX_RENDER_STRINGS).fill(0);
let _scrAtMinFretLen = 0;
// Sorted scalar view of "next event time per string recent event
// time per string" — populated once per frame in update() after
// _drawNextByString and _drawRecentByString are set. drawNote() and
@@ -7004,6 +6960,84 @@ import { createRenderer } from './src/renderer.js'; // h3d-carve-15
setClkPerf: (v) => { _clkPerf = v; },
setClkRate: (v) => { _clkRate = v; },
setFrameNow: (v) => { _frameNow = v; },
// ── Category B — plain consts ──────────────────────────────────────
ACCENT_NOTE_FILL_BOOST, ACCENT_NOTE_LINGER_EPS, ACCENT_NOTE_STR_GLOW,
ARPEGGIO_RIM_BLUE_HEX, ARP_FRAME_ONSET_CLUSTER_S, ARP_FRAME_ONSET_PAD_S,
ARP_INFER_MIN_HAND_SHAPE_SPAN_S,
CAM_DIST_HYST_C, CAM_DIST_HYST_T, CAM_TGT_AHEAD_C, CAM_TGT_AHEAD_T,
CAM_TGT_HYST_C, CAM_TGT_HYST_T, CAM_TGT_TAU_C, CAM_TGT_TAU_T,
CHORD_BOX_EDGE_ALPHA, CHORD_BOX_HIT_BRIGHT_HEX, CHORD_BOX_MISS_DARK_HEX, CHORD_BOX_TEAL_HEX,
CHORD_FRAME_RIM_Z_MIN, CHORD_FRAME_RIM_Z_SCAL,
CHORD_HWY_FADE_S, CHORD_HWY_LINGER_S,
DIAG_CROSSFADE_S, DIAG_ENTRANCE_S, DIAG_LINGER_S, DOTS,
FRET_COOLDOWN, FRET_EMISSIVE,
FRET_WIRE_ACTIVE_HEX, FRET_WIRE_ACTIVE_OP, FRET_WIRE_HIT_DECAY, FRET_WIRE_HIT_INTENSITY,
FRET_WIRE_HIT_OP, FRET_WIRE_IDLE_HEX, FRET_WIRE_IDLE_OP,
HWY_LANE_STRIPE_OP_BASE, HWY_LANE_STRIPE_OP_INT, HWY_LANE_TIME_SLICES,
NEXT_ON_STRING_T_EPS, _ND_UNMATCHED_LATCH_AFTER, VENUE_LANE_OP_BOOST,
MAX_RENDER_STRINGS,
// ── Category C — fn-refs / let-vars ────────────────────────────────
activePalette, anchorLaneBoundsAt, anchorPlayedFretSpanAt,
boardSpanX, chordShapeSignature,
_drawAnchors, _drawChordTemplates, _drawNextByString, _drawRecentByString, _drawTeachingMarks,
_encodeChordVerdictKey, _firstEventTimeGreaterThan,
fretColumnMarkerCadence, fretColumnMarkersForAnchor, fretDividersVisible,
fretLastActiveTime, _fretMarkerWaveCache, fretWireMats, fretX,
getChartAnchorAt, hwyPostHitTailFadeMul,
imFHTech, imFHXFill, imFHXLines, imPMTech, imPMXFill, imPMXLines,
laneBoundsFromAnchor, sectionLabelsOnHighway, _showFingerHints, updateStringHighlights,
_noteKey,
bendChevronMat, darkenHex, slideArrowMat, triMat,
palmMuteXSpriteMat, fretHandMuteXSpriteMat,
fxClearSeen,
// ── Category D — Three.js render objects (reassigned) ───────────────
getRen: () => ren,
getScene: () => scene,
getCam: () => cam,
// ── Category E — shared mutable state (getter/setter) ───────────────
getDiagChord: () => _diagChord,
setDiagChord: (v) => { _diagChord = v; },
getDiagEntranceT: () => _diagEntranceT,
setDiagEntranceT: (v) => { _diagEntranceT = v; },
getDiagLastKey: () => _diagLastKey,
setDiagLastKey: (v) => { _diagLastKey = v; },
getDiagPrev: () => _diagPrev,
setDiagPrev: (v) => { _diagPrev = v; },
getDiagPrevOpacity: () => _diagPrevOpacity,
setDiagPrevOpacity: (v) => { _diagPrevOpacity = v; },
getDiagPrevStartOpacity: () => _diagPrevStartOpacity,
setDiagPrevStartOpacity: (v) => { _diagPrevStartOpacity = v; },
getDiagPrevStartT: () => _diagPrevStartT,
setDiagPrevStartT: (v) => { _diagPrevStartT = v; },
getMergeCacheResult: () => _mergeCacheResult,
setMergeCacheResult: (v) => { _mergeCacheResult = v; },
_scrEventTimes,
getScrEventTimesLen: () => _scrEventTimesLen,
setScrEventTimesLen: (v) => { _scrEventTimesLen = v; },
getSlideTargetChordsRef: () => _slideTargetChordsRef,
setSlideTargetChordsRef: (v) => { _slideTargetChordsRef = v; },
getSlideTargetNotesRef: () => _slideTargetNotesRef,
setSlideTargetNotesRef: (v) => { _slideTargetNotesRef = v; },
getSlideTargetSet: () => _slideTargetSet,
setSlideTargetSet: (v) => { _slideTargetSet = v; },
// ── Category F — shared mutable (stable refs + getters) ─────────────
_fwChordAcc, _fwHitGlow, _fwHitIn, _rimFlashIn, _susVerdictLatch,
getFwHitColor: () => _fwHitColor,
getFwHitEmissive: () => _fwHitEmissive,
getFwHitPrevTime: () => _fwHitPrevTime,
setFwHitPrevTime: (v) => { _fwHitPrevTime = v; },
getMBeatM: () => mBeatM,
getMBeatQ: () => mBeatQ,
getMRimFlash: () => mRimFlash,
// ── Category G — lane materials (reassigned) ─────────────────────────
getMLaneDivider: () => mLaneDivider,
getMLaneDividerArp: () => mLaneDividerArp,
getMLaneDividerExt: () => mLaneDividerExt,
getMLaneEven: () => mLaneEven,
getMLaneOdd: () => mLaneOdd,
// ── Extra ──────────────────────────────────────────────────────────
getChordFrameGradTex: () => chordFrameGradTex,
getChordFrameGradTexArp: () => chordFrameGradTexArp,
});
+177 -77
View File
@@ -92,12 +92,111 @@ export function createRenderer({
setCamBootstrapHolding, setCamBootstrapMode,
setMeasureStarts, setMeasureStartsRef,
setClkAudioT, setClkPerf, setClkRate, setFrameNow,
// ── Category B — plain consts ─────────────────────────────────────────
ACCENT_NOTE_FILL_BOOST, ACCENT_NOTE_LINGER_EPS, ACCENT_NOTE_STR_GLOW,
ARPEGGIO_RIM_BLUE_HEX, ARP_FRAME_ONSET_CLUSTER_S, ARP_FRAME_ONSET_PAD_S,
ARP_INFER_MIN_HAND_SHAPE_SPAN_S,
CAM_DIST_HYST_C, CAM_DIST_HYST_T, CAM_TGT_AHEAD_C, CAM_TGT_AHEAD_T,
CAM_TGT_HYST_C, CAM_TGT_HYST_T, CAM_TGT_TAU_C, CAM_TGT_TAU_T,
CHORD_BOX_EDGE_ALPHA, CHORD_BOX_HIT_BRIGHT_HEX, CHORD_BOX_MISS_DARK_HEX, CHORD_BOX_TEAL_HEX,
CHORD_FRAME_RIM_Z_MIN, CHORD_FRAME_RIM_Z_SCAL,
CHORD_HWY_FADE_S, CHORD_HWY_LINGER_S,
DIAG_CROSSFADE_S, DIAG_ENTRANCE_S, DIAG_LINGER_S, DOTS,
FRET_COOLDOWN, FRET_EMISSIVE,
FRET_WIRE_ACTIVE_HEX, FRET_WIRE_ACTIVE_OP, FRET_WIRE_HIT_DECAY, FRET_WIRE_HIT_INTENSITY,
FRET_WIRE_HIT_OP, FRET_WIRE_IDLE_HEX, FRET_WIRE_IDLE_OP,
HWY_LANE_STRIPE_OP_BASE, HWY_LANE_STRIPE_OP_INT, HWY_LANE_TIME_SLICES,
NEXT_ON_STRING_T_EPS, _ND_UNMATCHED_LATCH_AFTER, VENUE_LANE_OP_BOOST,
MAX_RENDER_STRINGS,
// ── Category C — fn-refs / let-vars ──────────────────────────────────
activePalette, anchorLaneBoundsAt, anchorPlayedFretSpanAt,
boardSpanX, chordShapeSignature,
_drawAnchors, _drawChordTemplates, _drawNextByString, _drawRecentByString, _drawTeachingMarks,
_encodeChordVerdictKey, _firstEventTimeGreaterThan,
fretColumnMarkerCadence, fretColumnMarkersForAnchor, fretDividersVisible,
fretLastActiveTime, _fretMarkerWaveCache, fretWireMats, fretX,
getChartAnchorAt, hwyPostHitTailFadeMul,
imFHTech, imFHXFill, imFHXLines, imPMTech, imPMXFill, imPMXLines,
laneBoundsFromAnchor, sectionLabelsOnHighway, _showFingerHints, updateStringHighlights,
_noteKey,
bendChevronMat, darkenHex, slideArrowMat, triMat,
palmMuteXSpriteMat, fretHandMuteXSpriteMat,
fxClearSeen,
// ── Category D — Three.js render objects (reassigned) ─────────────────
getRen, getScene, getCam,
// ── Category E — shared mutable state (getter/setter) ─────────────────
getDiagChord, setDiagChord,
getDiagEntranceT, setDiagEntranceT,
getDiagLastKey, setDiagLastKey,
getDiagPrev, setDiagPrev,
getDiagPrevOpacity, setDiagPrevOpacity,
getDiagPrevStartOpacity, setDiagPrevStartOpacity,
getDiagPrevStartT, setDiagPrevStartT,
getMergeCacheResult, setMergeCacheResult,
_scrEventTimes,
getScrEventTimesLen, setScrEventTimesLen,
getSlideTargetChordsRef, setSlideTargetChordsRef,
getSlideTargetNotesRef, setSlideTargetNotesRef,
getSlideTargetSet, setSlideTargetSet,
// ── Category F — shared mutable (stable refs + getters) ───────────────
_fwChordAcc, _fwHitGlow, _fwHitIn, _rimFlashIn, _susVerdictLatch,
getFwHitColor, getFwHitEmissive,
getFwHitPrevTime, setFwHitPrevTime,
getMBeatM, getMBeatQ, getMRimFlash,
// ── Category G — lane materials (reassigned) ──────────────────────────
getMLaneDivider, getMLaneDividerArp, getMLaneDividerExt, getMLaneEven, getMLaneOdd,
// ── Extra ─────────────────────────────────────────────────────────────
getChordFrameGradTex, getChordFrameGradTexArp,
}) {
// Per-renderer mutable state (not DI — persists across update() calls):
let _chordVerdicts = new Map();
let _chordVerdictsLastNow = null;
let _fretLabelNotesRef = null;
let _fretLabelAllowed = null;
let _arpGhostHsInferScratch = [];
let _arpGhostInferRefHs = null;
let _arpGhostInferRefNotes = null;
let _arpGhostInferRefTpl = null;
let _arpLaneRailHsScratch = [];
let _arpRailBoundHiScratch = [];
let _arpRailBoundLoScratch = [];
let _arpSynthOnsetHsSet = new Set();
let _laneRailBoundsRefChords = null;
let _laneRailBoundsRefHs = null;
let _laneRailBoundsRefNotes = null;
let _laneRailBoundsRefTpl = null;
let _laneRailFlagsRefHs = null;
let _laneRailFlagsRefTpl = null;
const _laneSegArp = [];
const _laneSegDMax = [];
const _laneSegDMin = [];
let _laneSegLen = 0;
const _laneSegTHi = [];
const _laneSegTLo = [];
const _laneSegZ0 = [];
const _laneSegZ1 = [];
let _mergeCacheChordsRef = null;
let _mergeCacheHsRef = null;
let _mergeCacheTplRef = null;
const _scrAccentFillBoost = new Array(MAX_RENDER_STRINGS).fill(0);
const _scrActiveFrets = new Set();
const _scrArpPersistKeys = new Set();
const _scrAtMinFretArr = new Array(MAX_RENDER_STRINGS).fill(0);
let _scrAtMinFretLen = 0;
const _scrChordNote = {};
const _scrFretHeat = new Array(NFRETS + 1).fill(0);
const _scrGhostLastT = new Array(MAX_RENDER_STRINGS).fill(-Infinity);
const _scrGhostPrevBuf = new Map();
const _scrLastFretForString = new Array(MAX_RENDER_STRINGS).fill(undefined);
const _scrNextNoteByString = new Array(MAX_RENDER_STRINGS).fill(null);
const _scrNextNoteByStringData = Array.from({ length: MAX_RENDER_STRINGS }, () => ({}));
const _scrNoteStreamBracketStrings = new Map();
const _scrRecentByString = new Array(MAX_RENDER_STRINGS).fill(-Infinity);
const _scrStrGlow = new Array(MAX_RENDER_STRINGS).fill(0.5);
const _scrStringAnticipation = new Array(MAX_RENDER_STRINGS).fill(0);
const _scrStringSustain = new Array(MAX_RENDER_STRINGS).fill(false);
const _CV_KEY_TIME_MUL = 1e4;
const _CV_KEY_TIME_SLOT = 1e6;
function lookaheadSmoothCamStep(dtSec, tgtXWorld, tgtSpanInt) {
const d = Math.min(0.2, Math.max(1e-4, dtSec));
@@ -320,9 +419,10 @@ function smoothNow(bundle) {
let _chartPrewarmed = false;
function _prewarmTex(mat) {
if (mat && mat.map && ren) ren.initTexture(mat.map);
if (mat && mat.map && getRen()) getRen().initTexture(mat.map);
}
function _prewarmStatic() {
const nStr = getNStr();
// MAINTENANCE NOTE: this list must cover every deterministic
// (chart-independent) material/texture the per-frame paths can
// request lazily. Adding a new label style or sprite factory to
@@ -331,7 +431,7 @@ function _prewarmStatic() {
// mid-song. Chart-dependent labels (chord names, section names)
// live in _prewarmChart.
try {
if (ren && scene && cam) ren.compile(scene, cam);
if (getRen() && getScene() && getCam()) getRen().compile(getScene(), getCam());
} catch (e) { console.warn('[3D-Hwy] prewarm compile:', e); }
try {
// Fret-number labels in the per-frame style/colour combos.
@@ -586,7 +686,7 @@ function update(bundle) {
// Score-pop dedup too: a practice loop / rewind re-judges
// the same popKeys, and the wall-time TTL alone would
// suppress their fresh "+N" pops for up to 4 s.
_fxSeen.clear();
fxClearSeen();
}
if (_ndHasProvider && _chordVerdicts.size > 0) {
if (_chordVerdictsLastNow !== null && now < _chordVerdictsLastNow - 0.25) {
@@ -605,18 +705,18 @@ function update(bundle) {
// Skip the merge when inputs are identity-equal to the last
// frame's; mergeHandShapeSynthChords is chart-static.
let chords;
if (_mergeCacheResult !== null
if (getMergeCacheResult() !== null
&& _mergeCacheChordsRef === bundle.chords
&& _mergeCacheHsRef === bundle.handShapes
&& _mergeCacheTplRef === bundle.chordTemplates) {
chords = _mergeCacheResult;
chords = getMergeCacheResult();
} else {
chords = mergeHandShapeSynthChords(
bundle.chords,
bundle.handShapes,
bundle.chordTemplates,
);
_mergeCacheResult = chords;
setMergeCacheResult(chords);
_mergeCacheChordsRef = bundle.chords;
_mergeCacheHsRef = bundle.handShapes;
_mergeCacheTplRef = bundle.chordTemplates;
@@ -687,8 +787,8 @@ function update(bundle) {
// Case 2 — same fret (hold), destination has sl/slu (hold→slide)
//
// Sources can be single notes OR chord notes (bundle.chords).
if (notes !== _slideTargetNotesRef || bundle.chords !== _slideTargetChordsRef) {
_slideTargetSet = null;
if (notes !== getSlideTargetNotesRef() || bundle.chords !== getSlideTargetChordsRef()) {
setSlideTargetSet(null);
if (notes && notes.length) {
const stSet = new Set();
const checkSrc = (srcT, srcS, srcF, srcSus, srcSl) => {
@@ -725,10 +825,10 @@ function update(bundle) {
}
}
}
if (stSet.size > 0) _slideTargetSet = stSet;
if (stSet.size > 0) setSlideTargetSet(stSet);
}
_slideTargetNotesRef = notes;
_slideTargetChordsRef = bundle.chords;
setSlideTargetNotesRef(notes);
setSlideTargetChordsRef(bundle.chords);
}
/** Arpeggio lane purple rails — authored-marker cache + bounds cache. */
@@ -1037,18 +1137,18 @@ function update(bundle) {
// Pulls directly from _drawNextByString / _drawRecentByString
// (closure-scoped, populated just above) so we're independent of
// the recent-event prepass's inner-block ``_recArr`` alias.
_scrEventTimesLen = 0;
setScrEventTimesLen(0);
for (let s = 0; s < nStr; s++) {
const nf = _drawNextByString[s];
if (nf) {
const tn = nf.t;
if (Number.isFinite(tn)) _scrEventTimes[_scrEventTimesLen++] = tn;
if (Number.isFinite(tn)) { const _etl = getScrEventTimesLen(); _scrEventTimes[_etl] = tn; setScrEventTimesLen(_etl + 1); }
}
const rt = _drawRecentByString[s];
if (Number.isFinite(rt)) _scrEventTimes[_scrEventTimesLen++] = rt;
if (Number.isFinite(rt)) { const _etl = getScrEventTimesLen(); _scrEventTimes[_etl] = rt; setScrEventTimesLen(_etl + 1); }
}
if (_scrEventTimesLen > 1) {
_scrEventTimes.subarray(0, _scrEventTimesLen).sort();
if (getScrEventTimesLen() > 1) {
_scrEventTimes.subarray(0, getScrEventTimesLen()).sort();
}
// ── Ghost preview gap prepass ──────────────────────────────────
@@ -1445,7 +1545,7 @@ function update(bundle) {
// Suppress the gem for linkNext slide-target notes (skipBody=true).
// The sustain/slide trail still renders because it now lives outside
// the !skipBody gate in drawNote().
const _isSlideTgt = !!(_slideTargetSet && _slideTargetSet.has(_noteKey(n.t, n.s)));
const _isSlideTgt = !!(getSlideTargetSet() && getSlideTargetSet().has(_noteKey(n.t, n.s)));
// Always show the fret label — suppressing it for repeated frets on the same
// string caused the label to be invisible throughout the note's flight and
// only appear moments before being played (when the previous note's linger
@@ -2330,7 +2430,7 @@ function update(bundle) {
// Swapping `map` between two non-null gradient textures
// doesn't change shader-defining state, so no needsUpdate
// — that flag would otherwise force a recompile per frame.
fill.material.map = isArpeggioFrame ? chordFrameGradTexArp : chordFrameGradTex;
fill.material.map = isArpeggioFrame ? getChordFrameGradTexArp() : getChordFrameGradTex();
fill.material.color.setRGB(1, 1, 1);
const withTopFrame = !isRepeat;
@@ -2792,7 +2892,7 @@ function update(bundle) {
// frame-rate independent and honours playback speed. Seeking
// backward resets it — otherwise a flash from a hit we jumped away
// from would linger on the wire.
if (fretWireMats.length && _fwHitColor) {
if (fretWireMats.length && getFwHitColor()) {
// Resolve accumulated chord hits: a chord's flash frames the
// LANE, not its own shape. The lit lane strip spans the anchor's
// width (min ~4 frets), which can run a fret past the chord's
@@ -2819,12 +2919,12 @@ function update(bundle) {
if (_fwA > _fwHitIn[_w1]) _fwHitIn[_w1] = _fwA;
}
const _fwDt = now - _fwHitPrevTime;
const _fwDt = now - getFwHitPrevTime();
if (!(_fwDt >= 0) || _fwDt > 1) _fwHitGlow.fill(0); // first frame, seek, or long stall
const _fwDecay = (_fwDt > 0 && _fwDt <= 1)
? Math.exp(-_fwDt / FRET_WIRE_HIT_DECAY)
: 0;
_fwHitPrevTime = now;
setFwHitPrevTime(now);
// Decay EVERY wire's glow state, but flash only the OUTERMOST
// pair of lit wires. Fast passages overlap their decay tails, so
// without this a run of consecutive notes lights a picket fence
@@ -2849,8 +2949,8 @@ function update(bundle) {
const _g = _fwHitGlow[_f];
const _m = fretWireMats[_f];
if (!_m) continue;
_m.color.lerp(_fwHitColor, _g);
_m.emissive.lerp(_fwHitEmissive, _g);
_m.color.lerp(getFwHitColor(), _g);
_m.emissive.lerp(getFwHitEmissive(), _g);
_m.emissiveIntensity = 1 + (FRET_WIRE_HIT_INTENSITY - 1) * _g;
_m.opacity += (FRET_WIRE_HIT_OP - _m.opacity) * _g;
}
@@ -2860,8 +2960,8 @@ function update(bundle) {
// is only ever ASSIGNED while the provider confirms the note,
// and the provider's alpha already fades; when it goes silent
// the outline reverts and idle intensity is irrelevant.
for (let _s = 0; _s < mRimFlash.length; _s++) {
const _m = mRimFlash[_s];
for (let _s = 0; _s < getMRimFlash().length; _s++) {
const _m = getMRimFlash()[_s];
if (_m) _m.emissiveIntensity = 1 + (FRET_WIRE_HIT_INTENSITY - 1) * _rimFlashIn[_s];
}
}
@@ -2952,8 +3052,8 @@ function update(bundle) {
* (_venueSceneOverride ? VENUE_LANE_OP_BOOST : 1);
// 2 shared materials (odd/even); opacity travels via the
// material so set it once per frame, not per mesh.
mLaneOdd.opacity = laneOp;
mLaneEven.opacity = laneOp;
getMLaneOdd().opacity = laneOp;
getMLaneEven().opacity = laneOp;
for (let s = 0; s < _laneSegLen; s++) {
const segZ0 = _laneSegZ0[s];
const segZ1 = _laneSegZ1[s];
@@ -2969,7 +3069,7 @@ function update(bundle) {
lane.rotation.x = -Math.PI / 2;
lane.scale.set(laneW, stripLen, 1);
const odd = ((f - fLow) & 1) === 0;
lane.material = odd ? mLaneOdd : mLaneEven;
lane.material = odd ? getMLaneOdd() : getMLaneEven();
lane.renderOrder = 1;
}
}
@@ -2978,8 +3078,8 @@ function update(bundle) {
{
const yPos = boardY + 0.03 * K;
const divOpArp = Math.min(0.92, 0.16 + highwayIntensity * 0.42);
if (mLaneDividerArp) {
mLaneDividerArp.opacity = divOpArp;
if (getMLaneDividerArp()) {
getMLaneDividerArp().opacity = divOpArp;
}
for (let s = 0; s < _laneSegLen; s++) {
@@ -2995,7 +3095,7 @@ function update(bundle) {
if (_laneSegArp[s] && (f === fDiv0 || f === fDiv1)) continue;
const div = pLaneDivider.get();
div.position.set(xFret(f), yPos, zMid);
div.material = mLaneDivider;
div.material = getMLaneDivider();
div.scale.set(1, 1, dz);
div.renderOrder = 2;
}
@@ -3021,7 +3121,7 @@ function update(bundle) {
for (const xf of [fL, fR]) {
const div = pLaneDivider.get();
div.position.set(xFret(xf), yPos, zArpMid);
div.material = mLaneDividerArp;
div.material = getMLaneDividerArp();
div.scale.set(arpSSeg, arpSSeg, arpRailLen);
div.renderOrder = 2;
}
@@ -3069,8 +3169,8 @@ function update(bundle) {
const zLane = -laneLen / 2;
const laneOp = (HWY_LANE_STRIPE_OP_BASE + highwayIntensity * HWY_LANE_STRIPE_OP_INT)
* (_venueSceneOverride ? VENUE_LANE_OP_BOOST : 1);
mLaneOdd.opacity = laneOp;
mLaneEven.opacity = laneOp;
getMLaneOdd().opacity = laneOp;
getMLaneEven().opacity = laneOp;
const fLow = dMin + 1;
const fHi = dMax;
for (let f = fLow; f <= fHi; f++) {
@@ -3081,7 +3181,7 @@ function update(bundle) {
lane.rotation.x = -Math.PI / 2;
lane.scale.set(laneWStrip, laneLen, 1);
const odd = ((f - fLow) & 1) === 0;
lane.material = odd ? mLaneOdd : mLaneEven;
lane.material = odd ? getMLaneOdd() : getMLaneEven();
lane.renderOrder = 1;
}
@@ -3091,9 +3191,9 @@ function update(bundle) {
const yPos = boardY + 0.03 * K;
const divOp2 = 0.02 + highwayIntensity * 0.1;
const divOpArp2 = Math.min(0.92, 0.16 + highwayIntensity * 0.42);
if (mLaneDivider && mLaneDividerArp) {
mLaneDivider.opacity = divOp2;
mLaneDividerArp.opacity = divOpArp2;
if (getMLaneDivider() && getMLaneDividerArp()) {
getMLaneDivider().opacity = divOp2;
getMLaneDividerArp().opacity = divOpArp2;
}
const fDivA = Math.floor(divMin);
const fDivB = Math.ceil(divMax);
@@ -3101,7 +3201,7 @@ function update(bundle) {
if (hwyLaneArpOuterDividers && (f === fDivA || f === fDivB)) continue;
const div = pLaneDivider.get();
div.position.set(xFret(f), yPos, -divLen * 0.5);
div.material = mLaneDivider;
div.material = getMLaneDivider();
div.scale.set(1, 1, divLen);
div.renderOrder = 2;
}
@@ -3109,7 +3209,7 @@ function update(bundle) {
for (const xf of [fDivA, fDivB]) {
const div = pLaneDivider.get();
div.position.set(xFret(xf), yPos, zLane);
div.material = mLaneDividerArp;
div.material = getMLaneDividerArp();
div.scale.set(arpLaneS, arpLaneS, laneLen);
div.renderOrder = 2;
}
@@ -3118,17 +3218,17 @@ function update(bundle) {
}
// ── Fret boundary extension lines ─────────────────────────
if (mLaneDividerExt && fretDividersVisible) {
if (getMLaneDividerExt() && fretDividersVisible) {
// Same hit-line stop as the lane (#991) — otherwise these lines
// would be the only floor geometry still running past it.
const extLaneLen = TS * AHEAD;
const extZMid = -extLaneLen / 2;
const extYPos = boardY + 0.03 * K;
mLaneDividerExt.opacity = Math.max(0.3, 0.3 + highwayIntensity * 0.15);
getMLaneDividerExt().opacity = Math.max(0.3, 0.3 + highwayIntensity * 0.15);
for (let f = 0; f <= NFRETS; f++) {
const div = pLaneDivider.get();
div.position.set(xFret(f), extYPos, extZMid);
div.material = mLaneDividerExt;
div.material = getMLaneDividerExt();
div.scale.set(1, 1, extLaneLen);
div.renderOrder = 2;
}
@@ -3188,7 +3288,7 @@ function update(bundle) {
const meas = b.measure !== lastM; lastM = b.measure;
if (b.time < t0 || b.time > t1) continue;
const bl2 = pBeat.get();
bl2.material = meas ? mBeatM : mBeatQ;
bl2.material = meas ? getMBeatM() : getMBeatQ();
bl2.scale.set(bw2, 1, 1);
bl2.position.set(board.min - 2 * K, S_BASE - NH / 2 - 1.5 * K, dZ(b.time - now));
}
@@ -3458,12 +3558,12 @@ function update(bundle) {
// Include frets in the key so two templates sharing a display name but
// differing in fingering each trigger a fresh crossfade/entrance.
const newKey = newChord ? newChord.name + '|' + newChord.frets.join(',') : null;
if (newKey !== _diagLastKey) {
if (_diagChord && newKey !== null) {
if (newKey !== getDiagLastKey()) {
if (getDiagChord() && newKey !== null) {
// Recompute outgoing alpha from stored event time rather than the
// stale per-frame chDt; after dropped frames or seeks this prevents
// the overlay jumping to a stale brightness before the crossfade.
const freshChDt = _diagChord.t !== undefined ? _diagChord.t - now : _diagChord.chDt;
const freshChDt = getDiagChord().t !== undefined ? getDiagChord().t - now : getDiagChord().chDt;
const prevOpacity = Math.max(0, Math.min(1, 1 + freshChDt / DIAG_LINGER_S));
// Only crossfade when the outgoing chord is actually visible at now.
// freshChDt > 0 means the old chord is in the future (backward seek
@@ -3476,10 +3576,10 @@ function update(bundle) {
// Use the string count the outgoing chord was captured with, not the
// current nStr — an arrangement switch during a 150 ms crossfade
// must not remap the outgoing diagram onto the new layout.
_diagPrev = { name: _diagChord.name, frets: _diagChord.frets, nStr: _diagChord.nStr ?? nStr, t: _diagChord.t0 ?? _diagChord.t ?? now };
_diagPrevStartOpacity = prevOpacity;
_diagPrevOpacity = prevOpacity;
_diagPrevStartT = now;
setDiagPrev({ name: getDiagChord().name, frets: getDiagChord().frets, nStr: getDiagChord().nStr ?? nStr, t: getDiagChord().t0 ?? getDiagChord().t ?? now });
setDiagPrevStartOpacity(prevOpacity);
setDiagPrevOpacity(prevOpacity);
setDiagPrevStartT(now);
// entranceT for the outgoing diagram is computed live from _diagPrev.t
// each frame (see draw path), so it rewinds correctly on backward seeks
// within the crossfade window — no separate snapped state needed here.
@@ -3518,41 +3618,41 @@ function update(bundle) {
// a chord that was mostly faded from appearing brighter on a seek.
const histStartOpacity = Math.max(0, Math.min(1,
1 - (newChord.t - histPrev.t) / DIAG_LINGER_S));
_diagPrev = histPrev;
_diagPrevStartOpacity = histStartOpacity;
_diagPrevOpacity = Math.max(0, histStartOpacity * (1 - elapsed / DIAG_CROSSFADE_S));
_diagPrevStartT = newChord.t;
setDiagPrev(histPrev);
setDiagPrevStartOpacity(histStartOpacity);
setDiagPrevOpacity(Math.max(0, getDiagPrevStartOpacity() * (1 - elapsed / DIAG_CROSSFADE_S)));
setDiagPrevStartT(newChord.t);
} else {
_diagPrev = null; _diagPrevOpacity = 0; _diagPrevStartOpacity = 0;
_diagPrevStartT = null;
setDiagPrev(null); setDiagPrevOpacity(0); setDiagPrevStartOpacity(0);
setDiagPrevStartT(null);
}
} else {
// prevOpacity <= 0: old chord already fully faded, no crossfade needed.
_diagPrev = null; _diagPrevOpacity = 0; _diagPrevStartOpacity = 0;
_diagPrevStartT = null;
setDiagPrev(null); setDiagPrevOpacity(0); setDiagPrevStartOpacity(0);
setDiagPrevStartT(null);
}
} else {
_diagPrev = null; _diagPrevOpacity = 0; _diagPrevStartOpacity = 0;
_diagPrevStartT = null;
setDiagPrev(null); setDiagPrevOpacity(0); setDiagPrevStartOpacity(0);
setDiagPrevStartT(null);
}
_diagLastKey = newKey;
setDiagLastKey(newKey);
// Only update _diagChord when the chord key actually changes so that a
// lingering chord's original nStr is preserved on subsequent frames.
// (newChord is rebuilt every frame with the live nStr; unconditionally
// assigning here would stomp the captured nStr if the arrangement switches
// while the same chord is still in its linger window.)
_diagChord = newChord;
} else if (newKey !== null && newChord && _diagChord) {
setDiagChord(newChord);
} else if (newKey !== null && newChord && getDiagChord()) {
// Same chord re-seen. Update linger expiry (t) when the event time changes.
// Forward restrum (newChord.t > _diagChord.t): extend the linger window
// but preserve t0 so the entrance animation is NOT replayed — avoids the
// overlay jumping back to its 0.85× scale on every strum of the same chord.
// Backward seek to earlier occurrence (newChord.t < _diagChord.t): update
// both t and t0 to restart the entrance animation from the earlier position.
if (newChord.t !== _diagChord.t) {
_diagChord = newChord.t < _diagChord.t
? { ..._diagChord, t: newChord.t, t0: newChord.t } // backward seek
: { ..._diagChord, t: newChord.t }; // forward restrum
if (newChord.t !== getDiagChord().t) {
setDiagChord(newChord.t < getDiagChord().t
? { ...getDiagChord(), t: newChord.t, t0: newChord.t } // backward seek
: { ...getDiagChord(), t: newChord.t }); // forward restrum
}
}
@@ -3560,28 +3660,28 @@ function update(bundle) {
// If _diagPrevStartT is in the future relative to now, the crossfade was set up
// during a later playback position that has since been seeked past. Clear it so
// the stale outgoing diagram does not stay fully visible at the seek target.
if (_diagPrev && _diagPrevStartT !== null && _diagPrevStartT > now) {
_diagPrev = null; _diagPrevOpacity = 0; _diagPrevStartOpacity = 0;
_diagPrevStartT = null;
if (getDiagPrev() && getDiagPrevStartT() !== null && getDiagPrevStartT() > now) {
setDiagPrev(null); setDiagPrevOpacity(0); setDiagPrevStartOpacity(0);
setDiagPrevStartT(null);
}
// Entrance: derived from t0 (the original appearance time, not updated on
// forward restrums) so repeated hits of the same chord do not replay the
// 0.85→1.0 scale animation. On backward seeks t0 is updated alongside t,
// so the animation still rewinds correctly to the earlier position.
const _entranceAnchor = _diagChord && (_diagChord.t0 ?? _diagChord.t);
_diagEntranceT = (_diagChord && _entranceAnchor !== undefined)
const _entranceAnchor = getDiagChord() && (getDiagChord().t0 ?? getDiagChord().t);
setDiagEntranceT(getDiagChord() && _entranceAnchor !== undefined
? Math.min(1.0, Math.max(0, (now - _entranceAnchor) / DIAG_ENTRANCE_S))
: 1.0;
: 1.0);
// Crossfade: derived from absolute start time so backward seeks within the
// crossfade window correctly rewind the fade. _diagPrev is kept alive (at
// opacity 0) until the next key change rather than destroyed here, so that a
// backward seek that re-enters the crossfade window can recompute a positive
// opacity. Seeks before _diagPrevStartT are handled by the guard above.
if (_diagPrev && _diagPrevStartT !== null) {
const fadedT = Math.max(0, now - _diagPrevStartT);
_diagPrevOpacity = Math.max(0, _diagPrevStartOpacity * (1 - fadedT / DIAG_CROSSFADE_S));
if (getDiagPrev() && getDiagPrevStartT() !== null) {
const fadedT = Math.max(0, now - getDiagPrevStartT());
setDiagPrevOpacity(Math.max(0, getDiagPrevStartOpacity() * (1 - fadedT / DIAG_CROSSFADE_S)));
}
}
// ── Finalise InstancedMesh batches ────────────────────────────────
+2 -1
View File
@@ -292,5 +292,6 @@ export function createScoreFx({ getHighwayCanvas, getNdFrameNowMs, getCam, getPr
_fxRingMs = _fxBreakMs = -1e9;
}
return { fxInit, fxTeardown, fxSpawnPop: _fxSpawnPop, drawScoreFx };
function fxClearSeen() { _fxSeen.clear(); }
return { fxInit, fxTeardown, fxSpawnPop: _fxSpawnPop, drawScoreFx, fxClearSeen };
}
+283 -3
View File
@@ -42,8 +42,13 @@ test('screen.js imports createRenderer from renderer.js', () => {
'screen.js must import createRenderer');
});
test('screen.js wiring block contains expected DI param count (184)', () => {
// 184 = 77 getters + 35 setters + 72 shorthands
test('screen.js wiring block contains expected DI param count (313)', () => {
// 313 = 105 getters + 48 setters + 160 shorthands
// 184→313: +129 carve-15 full completion:
// +43 Category B consts, +37 Category C fn-refs, +3 Category D getters,
// +27 Category E getter/setter pairs + 1 stable ref,
// +11 Category F (5 stable + 6 getter/setter), +5 Category G getters,
// +2 extra (chordFrameGradTex/Arp getters).
// 177→184: +7 Creed r1 F3 fixes: TS, S_BASE, FRET_LABEL_GOLD_HEX, FRET_LABEL_IDLE_HEX,
// lookaheadBootstrapTime, lookaheadComputeFretBounds, lookaheadTargetWorldX.
// 241→177: removed 44 phantom consts, 2 undefined fn-refs, 16 dead params,
@@ -61,7 +66,7 @@ test('screen.js wiring block contains expected DI param count (184)', () => {
}, 0);
const total = getterCount + setterCount + shorthandCount;
assert.strictEqual(total, 184,
assert.strictEqual(total, 313,
`DI param count mismatch: got ${total} (getters=${getterCount}, setters=${setterCount}, shorthands=${shorthandCount})`);
});
@@ -306,3 +311,278 @@ test('F1: screen.js destructures _prewarmStatic and _prewarmChart from createRen
assert.match(screenSrc, /const\s*\{\s*update\s*,\s*_prewarmStatic\s*,\s*_prewarmChart\s*\}/,
'screen.js must destructure _prewarmStatic and _prewarmChart from createRenderer return');
});
// ── 23. ACTUAL EXECUTION SMOKE TEST ─────────────────────────────────────────
// Loads createRenderer via new Function (strips ESM import/export) so it runs
// in a CJS test context with fully-stub DI. Proves update() does not throw.
//
// RED at d475899: first execution would crash with
// ReferenceError: ACCENT_NOTE_FILL_BOOST is not defined
// because Category-B consts were read from renderer.js scope but were never
// declared inside it (they lived only in screen.js's IIFE and ES-module scope
// never chains into an IIFE). GREEN at this commit: all 313 DI params wired.
{
// Stub window for Node (renderer.js reads window.feedBack, guarded by &&)
if (typeof global.window === 'undefined') global.window = {};
// ── Geometry stubs (replace the geometry.js import) ──────────────────────
const _geo = {
lowerBoundT(arr, t) {
let lo = 0, hi = arr.length;
while (lo < hi) { const m = (lo + hi) >> 1; if (arr[m].t < t) lo = m + 1; else hi = m; }
return lo;
},
camBaseDistU: () => 0,
camLowFretPullbackU: () => 0,
dZ: () => 0,
renderOrderForLayerAtZ: () => 0,
};
// Strip ESM: remove import lines, rename export function
const _stripped = src
.replace(/^import\s+\{[^}]+\}\s+from\s+['"][^'"]+['"]\s*;?[^\n]*/mg, '')
.replace('export function createRenderer', 'function createRenderer');
// Wrap in a function that closes over geometry helpers and returns the factory
const _getFactory = new Function(
'lowerBoundT', 'camBaseDistU', 'camLowFretPullbackU', 'dZ', 'renderOrderForLayerAtZ',
_stripped + '\nreturn createRenderer;',
);
const _createRenderer = _getFactory(
_geo.lowerBoundT, _geo.camBaseDistU, _geo.camLowFretPullbackU,
_geo.dZ, _geo.renderOrderForLayerAtZ,
);
// ── Build a minimal-stub DI covering all 313 params ──────────────────────
const N = () => {};
const NAR = new Float32Array(0);
const NSTR = 6, NFRETS = 24;
function _makeDI() {
return {
// B — consts
K: 1, NFRETS, NW: 1, NH: 0.1, AHEAD: 1.5, BEHIND: 0.2, S_GAP: 1,
CAM_FOCUS_BLEND_RATE: 0.1, CAM_LOCK_ZOOM_MIN: 0.5, CAM_LOCK_ZOOM_MAX: 2,
CAM_LOCK_CENTER_FRET: 7, LOOKAHEAD_LOCK_ENGAGE_MAXF: 3, LOOKAHEAD_LOCK_RELEASE_MAXF: 5,
DEFAULT_LOOKAHEAD_FRET_SPAN: 8, FRET_WIDTH_MID: 0.05, CAM_TGT_BEHIND: 0.2,
CAM_DIST_BASE: 5, VENUE_GEM_EMISSIVE_MUL: 1.5, NOTEDETECT_GEM_VERDICT_WINDOW: 0.3,
INLAY_LABEL_FRETS: [3,5,7,9,12], GHOST_HOLD_AFTER_ONSET: 0.1,
CHORD_FRAME_RIM_MIN: 0.01, CHORD_FRAME_RIM_FRAC_H: 0.1,
TS: 1, S_BASE: 0.1, FRET_LABEL_GOLD_HEX: '#e8c040', FRET_LABEL_IDLE_HEX: '#9ab8cc',
ACCENT_NOTE_FILL_BOOST: 0.3, ACCENT_NOTE_LINGER_EPS: 0.05, ACCENT_NOTE_STR_GLOW: 0.5,
ARPEGGIO_RIM_BLUE_HEX: '#4080ff', ARP_FRAME_ONSET_CLUSTER_S: 0.1,
ARP_FRAME_ONSET_PAD_S: 0.05, ARP_INFER_MIN_HAND_SHAPE_SPAN_S: 0.2,
CAM_DIST_HYST_C: 0.1, CAM_DIST_HYST_T: 0.1, CAM_TGT_AHEAD_C: 0.1,
CAM_TGT_AHEAD_T: 0.1, CAM_TGT_HYST_C: 0.05, CAM_TGT_HYST_T: 0.05,
CAM_TGT_TAU_C: 0.2, CAM_TGT_TAU_T: 0.2, CHORD_BOX_EDGE_ALPHA: 0.7,
CHORD_BOX_HIT_BRIGHT_HEX: '#fff', CHORD_BOX_MISS_DARK_HEX: '#333',
CHORD_BOX_TEAL_HEX: '#00ac', CHORD_FRAME_RIM_Z_MIN: 0.1,
CHORD_FRAME_RIM_Z_SCAL: 1, CHORD_HWY_FADE_S: 0.3, CHORD_HWY_LINGER_S: 2,
DIAG_CROSSFADE_S: 0.15, DIAG_ENTRANCE_S: 0.2, DIAG_LINGER_S: 1.5,
DOTS: [3,5,7,9,12,15,17,19,21], FRET_COOLDOWN: 0.15, FRET_EMISSIVE: 2,
FRET_WIRE_ACTIVE_HEX: '#80c0ff', FRET_WIRE_ACTIVE_OP: 0.9,
FRET_WIRE_HIT_DECAY: 0.9, FRET_WIRE_HIT_INTENSITY: 3, FRET_WIRE_HIT_OP: 1,
FRET_WIRE_IDLE_HEX: '#aaa', FRET_WIRE_IDLE_OP: 0.3,
HWY_LANE_STRIPE_OP_BASE: 0.3, HWY_LANE_STRIPE_OP_INT: 0.15,
HWY_LANE_TIME_SLICES: 8, NEXT_ON_STRING_T_EPS: 0.01,
_ND_UNMATCHED_LATCH_AFTER: 0.2, VENUE_LANE_OP_BOOST: 0.5,
MAX_RENDER_STRINGS: 8,
// C — fn-refs
sY: (s) => s * 0.1, xFret: (f) => f * 0.05, xFretMid: (f) => f * 0.05,
fretLabelScaleForFret: () => 1, pbBeg: N, pbEnd: N, pbReportTick: N,
hwyFirstRelevantFrettedTime: () => Infinity, _syncOpenStringPitchLabels: N,
txtMat: () => ({ opacity: 1, map: null, color: { lerp: N }, emissive: { lerp: N }, emissiveIntensity: 1 }),
_setLabelMap: N, drawNote: N, drawArpBrackets: N, chordHarmonyLabels: N,
camUpdate: N, lookaheadBootstrapTime: N,
lookaheadComputeFretBounds: () => ({ lo: 0, hi: 12 }),
lookaheadTargetWorldX: () => 0, chordWireHighDensity: () => false,
chordTemplateLabel: () => null, chordTemplateMarkedArpeggio: () => false,
chordHandShapeArpeggioHint: () => false,
mergeHandShapeSynthChords: () => [], mergeChordShape: () => null,
inferArpeggioFromNotePattern: N, chordShapeCoveredByStandaloneNotes: () => false,
hsStart: () => 0, hsEnd: () => 0, handShapeChartSpanSec: () => 0.5,
fillArpeggioGhostInferFlags: N, arpeggioChordIdForNoteWithInferCache: () => -1,
arpHsBoundsForNote: () => null, fillLaneRailHandShapeFlags: N,
fillArpeggioRailShapeBoundsCaches: N,
arpeggioLaneOuterRailLaneSlice: () => null,
arpeggioLaneOuterRailAtChartTime: () => null,
arpeggioLaneDividerFrameAccentMul: () => 1,
arpeggioLaneDividerXYScaleMatchFrameRim: () => 1,
validString: (s) => s >= 0 && s < NSTR, filterValidNotes: (n) => n,
activePalette: new Array(NSTR).fill(0xffffff),
anchorLaneBoundsAt: () => null, anchorPlayedFretSpanAt: () => null,
boardSpanX: 1, chordShapeSignature: () => '',
_drawAnchors: [], _drawChordTemplates: [], _drawNextByString: new Array(NSTR).fill(null),
_drawRecentByString: new Array(NSTR).fill(null), _drawTeachingMarks: [],
_encodeChordVerdictKey: (t, s, f) => `${t}_${s}_${f}`,
_firstEventTimeGreaterThan: () => Infinity,
fretColumnMarkerCadence: 0, fretColumnMarkersForAnchor: () => [],
fretDividersVisible: true, fretLastActiveTime: new Float32Array(NFRETS + 1),
_fretMarkerWaveCache: {}, fretWireMats: [],
fretX: (f) => f * 0.05,
getChartAnchorAt: () => ({ fret: 0, width: 12 }), hwyPostHitTailFadeMul: () => 1,
imFHTech: null, imFHXFill: null, imFHXLines: null,
imPMTech: null, imPMXFill: null, imPMXLines: null,
laneBoundsFromAnchor: () => ({ lo: 0, hi: 12 }), sectionLabelsOnHighway: false,
_showFingerHints: () => false, updateStringHighlights: N,
_noteKey: (t, s) => `${t}_${s}`,
bendChevronMat: () => null, darkenHex: (h) => h, slideArrowMat: () => null,
triMat: () => null, palmMuteXSpriteMat: () => null,
fretHandMuteXSpriteMat: () => null, fxClearSeen: N,
// D — ren/scene/cam getters
getRen: () => null, getScene: () => null, getCam: () => null,
// E — shared mutable (getter/setter)
getDiagChord: () => null, setDiagChord: N,
getDiagEntranceT: () => 1, setDiagEntranceT: N,
getDiagLastKey: () => null, setDiagLastKey: N,
getDiagPrev: () => null, setDiagPrev: N,
getDiagPrevOpacity: () => 0, setDiagPrevOpacity: N,
getDiagPrevStartOpacity: () => 0, setDiagPrevStartOpacity: N,
getDiagPrevStartT: () => null, setDiagPrevStartT: N,
getMergeCacheResult: () => null, setMergeCacheResult: N,
_scrEventTimes: new Float64Array(256),
getScrEventTimesLen: () => 0, setScrEventTimesLen: N,
getSlideTargetChordsRef: () => null, setSlideTargetChordsRef: N,
getSlideTargetNotesRef: () => null, setSlideTargetNotesRef: N,
getSlideTargetSet: () => null, setSlideTargetSet: N,
// F — stable refs + getter/setter
_fwChordAcc: new Map(), _fwHitGlow: new Float32Array(NFRETS + 1),
_fwHitIn: new Float32Array(NFRETS + 1), _rimFlashIn: new Float32Array(NSTR),
_susVerdictLatch: new Map(),
getFwHitColor: () => null, getFwHitEmissive: () => null,
getFwHitPrevTime: () => -Infinity, setFwHitPrevTime: N,
getMBeatM: () => null, getMBeatQ: () => null, getMRimFlash: () => [],
// G — lane materials
getMLaneDivider: () => ({ opacity: 1, color: { lerp: N }, emissive: { lerp: N } }),
getMLaneDividerArp: () => ({ opacity: 1 }),
getMLaneDividerExt: () => ({ opacity: 1 }),
getMLaneEven: () => ({ opacity: 1 }),
getMLaneOdd: () => ({ opacity: 1 }),
// Extra
getChordFrameGradTex: () => null, getChordFrameGradTexArp: () => null,
// Pool getters (33)
getPNote: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPNoteEdge: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPSus: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPSusOutline: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPSusRibbon: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPSusRibbonOl: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPTapChevron: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPAccentHalo: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPLbl: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPArpBracket: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPBeat: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPSec: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPFretLbl: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPLane: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPLaneDivider: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPGhostFretLbl: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPChordBox: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPChordFrameFill: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPChordLbl: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPBarreLine: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPHaloBar: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPPMXFill: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPFHXFill: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPMuteXLines: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPFHXLines: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPNoteFretLabel: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPConnectorLine: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPDropLine: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPTeachMarkLbl: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPFretColMarker: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPSusRail: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPSusRailBloom: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
getPTechPlane: () => ({ get: () => ({}), release: N, resetCount: N, reset: N }),
// Material/scene getters
getMHitBright: () => [], getMHitSusOutline: () => null,
getGlowMul: () => 1, getVenueSceneOverride: () => false, getProjMeshArr: () => [],
// Render settings
getTextSize: () => 12, getCameraMode: () => 'smooth',
getCameraSmoothing: () => 0.5, getZoomSmoothing: () => 0.5,
getCameraLockLow: () => false, getCameraLockZoom: () => 0,
getNStr: () => NSTR, getLeftyCached: () => false, getInverted: () => false,
// Camera state getters
getTgtX: () => 0.3, getTgtDist: () => 5, getCurX: () => 0.3,
getPrevLowFretBonus: () => 0, getPrevLockActive: () => false,
getLookaheadCamX: () => 0.3, getLookaheadFretSpan: () => 8,
getLookaheadLowBonusU: () => 0, getLookaheadHiNeckLatch: () => false,
getLookaheadCamPrevNow: () => 0,
getFrameNow: () => 0, getClkAudioT: () => 0, getClkPerf: () => 0,
getClkRate: () => 1, getCamSnapped: () => true, getCamPreScanned: () => true,
getCamBootstrapHolding: () => false, getCamBootstrapMode: () => 'snap',
getSongKey: () => 'smoke-test', getNdVerdictSawAlpha: () => false,
getNdVerdictMaxAlpha: () => 0, getNdFrameNowMs: () => 0,
getInlayLabels: () => [], getLeanSusPollCounter: () => 0, getLeanSus: () => true,
getTextSizeMul: () => 1, getTextSizeMulApplied: () => 1,
getImPMTechCount: () => 0, getImFHTechCount: () => 0,
getMeasureStartsRef: () => [],
// Stable object refs
_frameLabeledKeys: new Set(), _ndLabels: [],
_scrGhostUpcomingCount: new Int32Array(NSTR),
_ndHitMarks: [], _ndMissMarks: [],
// Setters
setNdVerdictSawAlpha: N, setNdVerdictMaxAlpha: N, setNdFrameNowMs: N,
setLeanSus: N, setLeanSusPollCounter: N,
setTextSizeMul: N, setTextSizeMulApplied: N,
setImPMTechCount: N, setImFHTechCount: N,
setImPMXFillCount: N, setImPMXLinesCount: N,
setImFHXFillCount: N, setImFHXLinesCount: N,
setLookaheadCamX: N, setLookaheadFretSpan: N,
setLookaheadCamPrevNow: N, setLookaheadHiNeckLatch: N,
setLookaheadLowBonusU: N, setTgtX: N, setTgtDist: N,
setPrevLowFretBonus: N, setPrevLockActive: N,
setCurX: N, setCurDist: N, setSongKey: N, setCamSnapped: N,
setCamPreScanned: N, setCamBootstrapHolding: N, setCamBootstrapMode: N,
setMeasureStarts: N, setMeasureStartsRef: N,
setClkAudioT: N, setClkPerf: N, setClkRate: N, setFrameNow: N,
};
}
function _makeBundle(o) {
return Object.assign({
currentTime: 1.0, notes: [], chords: [], beats: [], sections: [],
anchors: [{ time: 0, fret: 0, width: 12 }], chordTemplates: [],
stringCount: NSTR, lyricsVisible: false, toneChanges: [], phrases: null,
isReady: true, mastery: 1, hasPhraseData: false,
songInfo: { arrangement: 'lead', tuning: [0,0,0,0,0,0], capo: 0, centOffset: 0 },
lowerBoundT: _geo.lowerBoundT,
lowerBoundTime: (arr, t) => _geo.lowerBoundT(arr, t),
project: () => ({ x: 0, y: 0 }), fretX: (f) => f * 0.05,
getNoteState: () => null,
}, o);
}
test('smoke: createRendererFn constructs without throw', () => {
assert.doesNotThrow(() => _createRenderer(_makeDI()),
'createRenderer(stub-DI) must not throw — all names must be provided');
});
test('smoke: update() with empty bundle throws no ReferenceError (proves no undeclared names)', () => {
// RED at d475899: ACCENT_NOTE_FILL_BOOST is not defined (Category B, not DI'd).
// GREEN at this commit: all 313 DI params wired in createRenderer signature.
// TypeErrors from stub DI (incomplete Three.js objects) are expected and accepted;
// what must NOT happen is a ReferenceError for an undeclared name.
const renderer = _createRenderer(_makeDI());
try {
renderer.update(_makeBundle({}));
} catch (e) {
assert.notStrictEqual(e.constructor, ReferenceError,
`update() must not throw ReferenceError — undeclared name: ${e.message}`);
}
});
test('smoke: update() backward seek (region C) throws no ReferenceError', () => {
// Backward seek triggers _susVerdictLatch.clear() and slide-target reset.
// TypeErrors from stub DI are expected; ReferenceError proves an undeclared name.
const di = _makeDI();
di.getFrameNow = () => 2.0;
const renderer = _createRenderer(di);
try { renderer.update(_makeBundle({ currentTime: 2.0 })); } catch (_) {}
try {
renderer.update(_makeBundle({ currentTime: 0.5 })); // backward seek
} catch (e) {
assert.notStrictEqual(e.constructor, ReferenceError,
`update() backward seek must not throw ReferenceError: ${e.message}`);
}
});
}
+3 -3
View File
@@ -28,8 +28,8 @@ test('score-fx.js exports createScoreFx', () => {
test('createScoreFx returns all four expected exports', () => {
assert.match(
scoreFxSrc,
/return\s*\{\s*fxInit\s*,\s*fxTeardown\s*,\s*fxSpawnPop\s*:\s*_fxSpawnPop\s*,\s*drawScoreFx\s*\}/,
'factory must return { fxInit, fxTeardown, fxSpawnPop: _fxSpawnPop, drawScoreFx }',
/return\s*\{[^}]*fxInit[^}]*fxTeardown[^}]*fxSpawnPop\s*:\s*_fxSpawnPop[^}]*drawScoreFx[^}]*\}/,
'factory must return { fxInit, fxTeardown, fxSpawnPop: _fxSpawnPop, drawScoreFx, ... }',
);
});
@@ -380,7 +380,7 @@ test('screen.js teardown callsite replaced with fxTeardown()', () => {
test('createScoreFx({...}) wiring has correct naming correspondence (no param swaps)', () => {
const PINNED_RENAMES = {};
const ANCHOR = 'const { fxInit, fxTeardown, fxSpawnPop: _fxSpawnPop, drawScoreFx } = createScoreFx({';
const ANCHOR = 'const { fxInit, fxTeardown, fxSpawnPop: _fxSpawnPop, drawScoreFx, fxClearSeen } = createScoreFx({';
const callStart = src.indexOf(ANCHOR);
assert.ok(callStart >= 0, 'createScoreFx call must be findable in screen.js');
const blockStart = callStart + ANCHOR.length - 1;
+2 -2
View File
@@ -29,7 +29,7 @@ test('a _slideTargetSet pre-pass builds the suppressed-gem set from bundle.notes
);
assert.match(
src,
/if\s*\(\s*stSet\.size\s*>\s*0\s*\)\s*_slideTargetSet\s*=\s*stSet/,
/if\s*\(\s*stSet\.size\s*>\s*0\s*\)\s*(?:_slideTargetSet\s*=\s*stSet|setSlideTargetSet\s*\(\s*stSet\s*\))/,
'_slideTargetSet must be assigned from the pre-pass result',
);
});
@@ -38,7 +38,7 @@ test('_isSlideTgt is derived from _slideTargetSet membership', () => {
const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + _noteSrc + '\n' + fs.readFileSync(RENDERER_JS, 'utf8');
assert.match(
src,
/_isSlideTgt\s*=\s*!!\(\s*_slideTargetSet\s*&&\s*_slideTargetSet\.has\(/,
/_isSlideTgt\s*=\s*!!\(\s*(?:_slideTargetSet|getSlideTargetSet\(\))\s*&&\s*(?:_slideTargetSet|getSlideTargetSet\(\))\.has\(/,
'_isSlideTgt must test _slideTargetSet membership',
);
});