mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-11 04:34:30 +00:00
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:
co-authored by
Claude Sonnet 4.6
parent
d475899c5a
commit
06e4fe335a
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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 ────────────────────────────────
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user