diff --git a/plugins/highway_3d/screen.js b/plugins/highway_3d/screen.js index 291569a..7f8ce51 100644 --- a/plugins/highway_3d/screen.js +++ b/plugins/highway_3d/screen.js @@ -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, }); diff --git a/plugins/highway_3d/src/renderer.js b/plugins/highway_3d/src/renderer.js index f913712..15f1152 100644 --- a/plugins/highway_3d/src/renderer.js +++ b/plugins/highway_3d/src/renderer.js @@ -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 ──────────────────────────────── diff --git a/plugins/highway_3d/src/score-fx.js b/plugins/highway_3d/src/score-fx.js index 5b85e5b..f8cf6a0 100644 --- a/plugins/highway_3d/src/score-fx.js +++ b/plugins/highway_3d/src/score-fx.js @@ -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 }; } diff --git a/tests/js/highway_3d_renderer.test.js b/tests/js/highway_3d_renderer.test.js index 7d5cd5d..4ec96ef 100644 --- a/tests/js/highway_3d_renderer.test.js +++ b/tests/js/highway_3d_renderer.test.js @@ -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}`); + } + }); +} diff --git a/tests/js/highway_3d_score_fx.test.js b/tests/js/highway_3d_score_fx.test.js index e3f8979..7796fd7 100644 --- a/tests/js/highway_3d_score_fx.test.js +++ b/tests/js/highway_3d_score_fx.test.js @@ -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; diff --git a/tests/js/highway_3d_slide_target.test.js b/tests/js/highway_3d_slide_target.test.js index 6e66636..85e173b 100644 --- a/tests/js/highway_3d_slide_target.test.js +++ b/tests/js/highway_3d_slide_target.test.js @@ -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', ); });