fix(highway_3d): hoist sY before createScoreFx to resolve TDZ

Root cause: const sY was declared 371 lines after createScoreFx({..., sY})
inside createFactory(). The shorthand {sY} reads the binding immediately
(not a closure), so every factory call threw ReferenceError: Cannot access
'sY' before initialization. highway.js caught it, reverted to 2D, emitted
viz:reverted. THREE.js was never requested.

Fix: hoist sY declaration to just before createScoreFx. Also hoist
_invertedCached, nStr, curX, and highwayCanvas above their first lexical
reference (all were closure false positives, but hoisting makes the code
unambiguously safe and keeps the new ESLint gate clean with 0 errors).
Hoist _bcPanel in bc-panel.js for the same reason.

Regression gate: eslint no-use-before-define (variables:true, functions:false)
scoped over plugins/highway_3d/ (screen.js + src/). Statically catches any
const/let used before its declaration in the factory — the whole class, not
just this pair. RED at broken tip (sY flagged): GREEN after fix.
Pre-existing violations surfaced (all closure false positives, none true TDZ
runtime bugs): highwayCanvas in _v3TopRightChromeBottom body, _invertedCached
and nStr in sY arrow body and DI getters, curX in getCurX DI getter, _bcPanel
in bc-panel.js function bodies — all resolved by hoisting; no silenced errors.

Bump plugin.json 3.53.0→3.54.0 (viz factory change per standing rule).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
This commit is contained in:
byrongamatos
2026-09-06 12:58:19 +02:00
co-authored by Claude Sonnet 4.6
parent a6efd4ee5e
commit ffbbf3fb94
5 changed files with 95 additions and 11 deletions
+26 -9
View File
@@ -3221,6 +3221,10 @@ import { createSceneInit } from './src/scene-init.js'; // h3d-carve-16
// — never a per-frame querySelector (see CLAUDE.md "never run DOM queries
// on a per-frame path") — and re-resolved only when a node detaches.
let _v3HudEls = null;
// Hoisted above _v3TopRightChromeBottom so the function body reference
// is not flagged by no-use-before-define (closure false positive — read
// only at call time; original declaration was near the lifecycle flags).
let highwayCanvas = null;
// Returns the bottom edge (in overlay-canvas px, which are 1:1 CSS px on
// this overlay) of the lowest visible top-right v3 chrome element, or 0
// when none apply (classic v2 UI, or all hidden). Only called while the
@@ -3607,6 +3611,25 @@ import { createSceneInit } from './src/scene-init.js'; // h3d-carve-16
// silent — see the live-latch handling in the per-gem loop below).
let _susVerdictLatch = new Map();
// ── String-to-Y (respects invert) ─────────────────────────────────
// Declared here so createScoreFx (carve-10) can receive a direct
// sY reference; the original declaration at carve-6 was after the
// carve-10 block, putting sY in TDZ when createScoreFx read it.
//
// _invertedCached and nStr are hoisted here from their original
// positions (lifecycle flags and per-frame state blocks below) so
// the sY arrow body and the DI getter arrows in createScoreFx are
// not flagged by no-use-before-define. Both read these bindings
// only at call time (closure), never at factory-init time.
let _invertedCached = false;
// Active string count for the current arrangement (resolved each
// frame from bundle.stringCount and clamped to MAX_RENDER_STRINGS).
let nStr = NSTR;
// curX hoisted for the getCurX DI getter arrow below (closure, read
// at call time). Initial value assigned later at xFretMid init.
let curX;
const sY = s => S_BASE + (_invertedCached ? s : (nStr - 1 - s)) * S_GAP;
/* ── h3d-carve-10: K-section (score FX) → src/score-fx.js ──────── */
const { fxInit, fxTeardown, fxSpawnPop: _fxSpawnPop, drawScoreFx, fxClearSeen } = createScoreFx({
getHighwayCanvas: () => highwayCanvas,
@@ -3699,9 +3722,6 @@ import { createSceneInit } from './src/scene-init.js'; // h3d-carve-16
// Per-fret last-active timestamp for lane persistence
let fretLastActiveTime = new Array(NFRETS + 1).fill(0);
// Active string count for the current arrangement (resolved each
// frame from bundle.stringCount and clamped to MAX_RENDER_STRINGS).
let nStr = NSTR;
// Set true once a chart with out-of-range s indices has triggered
// its warning. Reset only on teardown or when nStr changes (e.g.
// arrangement switch from guitar to bass) — same-nStr songs share
@@ -3866,7 +3886,7 @@ import { createSceneInit } from './src/scene-init.js'; // h3d-carve-16
};
};
let tgtX = xFretMid(CAM_LOCK_CENTER_FRET), curX = xFretMid(CAM_LOCK_CENTER_FRET);
let tgtX = xFretMid(CAM_LOCK_CENTER_FRET); curX = xFretMid(CAM_LOCK_CENTER_FRET); // curX declared above (DI hoisting)
let tgtDist = CAM_DIST_BASE, curDist = CAM_DIST_BASE;
// Dolly-back multiplier applied to the curDist lerp target by camUpdate's
// fret-row fit guard. 1 = no extra pull-back (the common case); rises
@@ -3958,11 +3978,11 @@ import { createSceneInit } from './src/scene-init.js'; // h3d-carve-16
// Lifecycle flags
let _isReady = false;
let _destroyed = false;
let _invertedCached = false;
// _invertedCached hoisted to before sY — see comment there.
let _invertedForBoard = false;
let _leftyForBoard = false;
let _initToken = 0;
let highwayCanvas = null;
// highwayCanvas hoisted to before _v3TopRightChromeBottom — see comment there.
// ── Focus state (splitscreen dim) ─────────────────────────────────
let _focusSubscribed = false;
@@ -3985,9 +4005,6 @@ import { createSceneInit } from './src/scene-init.js'; // h3d-carve-16
if (dirLight) dirLight.intensity = focused ? 0.8 : 0.35;
}
// ── String-to-Y (respects invert) ─────────────────────────────────
const sY = s => S_BASE + (_invertedCached ? s : (nStr - 1 - s)) * S_GAP;
// ── h3d-carve-6: material builders ───────────────────────────────────
// TXT_STYLES, txtMat, pinchHarmonicMat, naturalHarmonicMat,
// palmMuteXSpriteMat, fretHandMuteXSpriteMat, muteXMat,