h3d-carve-11: extract R-section (string glow) into src/string-glow.js

VERBATIM-MOVE of updateStringHighlights from screen.js into a new
createStringGlow() factory. 7 DI-rewires at function entry (aliased
locals), plus 1 plain const shorthand. 8 DI params total, 0 setter
pairs, single export { updateStringHighlights }.

screen.js changes:
- Function definition (old 6610–6649) replaced with createStringGlow({…})
  factory destructure
- mStr confirmed absent from updateStringHighlights (plan row 11 stale;
  declared surprise in contract, accepted by god)
- VENUE_GEM_EMISSIVE_MUL passed as plain const shorthand (not a getter)

Tests (highway_3d_string_glow.test.js, 11 new, all green):
- Module shape + DI rewire source-scans
- Wiring-correspondence guard (createStringGlow, empty PINNED_RENAMES)
- 3 behavioral tests: emissive/opacity writes, venue multiplier,
  null-mesh slot safety — behavioral kill: gut loop → assert RED

Suite: 1317/1319 pass. Pre-existing failures #46 (legacy analyser)
and #639 (nut-labels) unchanged.

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-05 17:51:54 +02:00
co-authored by Claude Sonnet 4.6
parent 4b2e4172c5
commit 930b492fa5
4 changed files with 384 additions and 41 deletions
+12 -40
View File
@@ -16,6 +16,7 @@ import { createOverlay } from './src/overlay.js'; // h3d-carve-7
import { createFx } from './src/fx.js'; // h3d-carve-8
import { createCamera } from './src/camera.js'; // h3d-carve-9
import { createScoreFx } from './src/score-fx.js'; // h3d-carve-10
import { createStringGlow } from './src/string-glow.js'; // h3d-carve-11
(function () {
'use strict';
@@ -6607,46 +6608,17 @@ import { createScoreFx } from './src/score-fx.js'; // h3d-carve-10
}
/* ── String glow (called each frame) ────────────────────────────── */
function updateStringHighlights(noteState) {
// Glow slider scales both the idle floor and anticipation peak,
// so glowMul=0 fully silences the per-string emissive pulse.
// Vibrancy controls the idle opacity floor — anticipation
// still rides on top regardless of vibrancy so play-feedback
// through the opacity channel survives even at glowMul=0.
//
// Folded with the post-noteState mGlow / mAccentCore writes
// (was a separate `for (s = 0; s < nStr)` loop in update()),
// so the per-string scratch arrays stay hot in L1 across all
// material writes for a given string.
const BASE_GLOW = 0.02 * glowMul;
const MAX_GLOW = 3.5 * glowMul;
const IDLE_OP = _vibrancyIdleOp;
const g = glowMul;
const venueGemMul = _venueSceneOverride ? VENUE_GEM_EMISSIVE_MUL : 1;
for (let s = 0; s < nStr; s++) {
const mesh = stringLines[s];
if (mesh) {
const intensity = Math.max(
noteState.stringSustain[s] ? 1 : 0,
noteState.stringAnticipation[s] || 0,
);
mesh.material.emissiveIntensity = BASE_GLOW + intensity * MAX_GLOW;
mesh.material.opacity = IDLE_OP + intensity * (1 - IDLE_OP);
mesh.scale.set(1, 1 + intensity * 0.3, 1 + intensity * 0.3);
}
// Hit-note emissive — same write pattern as the standalone
// loop that previously lived at update()'s post-call site.
// The glow slider scales it here since this assignment
// stomps anything _applyGlow() set statically.
const bg = noteState.strGlow[s] * g;
if (mGlow[s]) mGlow[s].emissiveIntensity = bg * venueGemMul;
if (mAccentCore[s]) {
mAccentCore[s].emissiveIntensity =
(bg + noteState.accentFillBoost[s] * g) * venueGemMul;
}
}
}
/* ── h3d-carve-11: R-section (string glow) → src/string-glow.js ── */
const { updateStringHighlights } = createStringGlow({
VENUE_GEM_EMISSIVE_MUL,
getGlowMul: () => glowMul,
getVibrancyIdleOp: () => _vibrancyIdleOp,
getVenueSceneOverride: () => _venueSceneOverride,
getNStr: () => nStr,
getStringLines: () => stringLines,
getMGlow: () => mGlow,
getMAccentCore: () => mAccentCore,
});
/* ── Lookahead fret bounds + smooth camera ───────────────────────── */
// End time of the lookahead window = start of the measure that is