mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-11 05:04:30 +00:00
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:
co-authored by
Claude Sonnet 4.6
parent
4b2e4172c5
commit
930b492fa5
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "highway_3d",
|
||||
"name": "3D Highway",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"type": "visualization",
|
||||
"scriptType": "module",
|
||||
"bundled": true,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// h3d-carve-11: R-section (string glow) extracted from screen.js.
|
||||
// VERBATIM-MOVE: updateStringHighlights body is byte-for-byte identical to
|
||||
// screen.js except for 7 DI-rewires at function entry (aliased locals).
|
||||
// No logic changes, no new guards.
|
||||
//
|
||||
// Beyond-subst changes (all mechanical DI rewires at function entry):
|
||||
// 1. glowMul → aliased: const glowMul = getGlowMul()
|
||||
// 2. _vibrancyIdleOp → aliased: const _vibrancyIdleOp = getVibrancyIdleOp()
|
||||
// 3. _venueSceneOverride → aliased: const _venueSceneOverride = getVenueSceneOverride()
|
||||
// 4. nStr → aliased: const nStr = getNStr()
|
||||
// 5. stringLines → aliased: const stringLines = getStringLines()
|
||||
// 6. mGlow → aliased: const mGlow = getMGlow()
|
||||
// 7. mAccentCore → aliased: const mAccentCore = getMAccentCore()
|
||||
// VENUE_GEM_EMISSIVE_MUL is a plain const shorthand (no aliasing needed).
|
||||
// Total DI params: 8 (1 plain const + 7 live getters, 0 setter pairs).
|
||||
|
||||
export function createStringGlow({
|
||||
VENUE_GEM_EMISSIVE_MUL,
|
||||
getGlowMul,
|
||||
getVibrancyIdleOp,
|
||||
getVenueSceneOverride,
|
||||
getNStr,
|
||||
getStringLines,
|
||||
getMGlow,
|
||||
getMAccentCore,
|
||||
}) {
|
||||
function updateStringHighlights(noteState) {
|
||||
// DI: all mutable IIFE-scope vars aliased here; body is verbatim.
|
||||
const glowMul = getGlowMul(); // DI: glowMul
|
||||
const _vibrancyIdleOp = getVibrancyIdleOp(); // DI: _vibrancyIdleOp
|
||||
const _venueSceneOverride = getVenueSceneOverride(); // DI: _venueSceneOverride
|
||||
const nStr = getNStr(); // DI: nStr
|
||||
const stringLines = getStringLines(); // DI: stringLines
|
||||
const mGlow = getMGlow(); // DI: mGlow
|
||||
const mAccentCore = getMAccentCore(); // DI: mAccentCore
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { updateStringHighlights };
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
// h3d-carve-11: Regression coverage for updateStringHighlights extracted into
|
||||
// plugins/highway_3d/src/string-glow.js.
|
||||
//
|
||||
// Two test classes:
|
||||
// - Source-level: module shape, DI wiring in screen.js, wiring-correspondence guard
|
||||
// - Behavioral: calls updateStringHighlights with fake mesh/material objects,
|
||||
// asserts actual emissive + opacity writes (RED when loop body is gutted)
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { pathToFileURL } = require('node:url');
|
||||
|
||||
const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js');
|
||||
const STRING_GLOW_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'string-glow.js');
|
||||
|
||||
const src = fs.readFileSync(SCREEN_JS, 'utf8');
|
||||
const stringGlowSrc = fs.readFileSync(STRING_GLOW_JS, 'utf8');
|
||||
|
||||
// ── Module shape ─────────────────────────────────────────────────────────────
|
||||
|
||||
test('string-glow.js exports createStringGlow', () => {
|
||||
assert.match(stringGlowSrc, /export\s+function\s+createStringGlow\s*\(/,
|
||||
'string-glow.js must export createStringGlow');
|
||||
});
|
||||
|
||||
test('createStringGlow returns { updateStringHighlights }', () => {
|
||||
assert.match(
|
||||
stringGlowSrc,
|
||||
/return\s*\{\s*updateStringHighlights\s*\}/,
|
||||
'factory must return { updateStringHighlights }',
|
||||
);
|
||||
});
|
||||
|
||||
// ── DI rewires (source-level) ────────────────────────────────────────────────
|
||||
|
||||
test('all 7 getter DI params aliased at updateStringHighlights entry', () => {
|
||||
for (const [alias, getter] of [
|
||||
['glowMul', 'getGlowMul'],
|
||||
['_vibrancyIdleOp', 'getVibrancyIdleOp'],
|
||||
['_venueSceneOverride','getVenueSceneOverride'],
|
||||
['nStr', 'getNStr'],
|
||||
['stringLines', 'getStringLines'],
|
||||
['mGlow', 'getMGlow'],
|
||||
['mAccentCore', 'getMAccentCore'],
|
||||
]) {
|
||||
assert.match(
|
||||
stringGlowSrc,
|
||||
new RegExp('const\\s+' + alias.replace('_', '\\_?') + '\\s*=\\s*' + getter + '\\(\\)'),
|
||||
`${getter}() must be aliased to ${alias} at function entry`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('VENUE_GEM_EMISSIVE_MUL is used as a plain const (no getter call)', () => {
|
||||
assert.match(
|
||||
stringGlowSrc,
|
||||
/VENUE_GEM_EMISSIVE_MUL/,
|
||||
'VENUE_GEM_EMISSIVE_MUL must appear in the module',
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
stringGlowSrc,
|
||||
/getVenueGemEmissiveMul/,
|
||||
'VENUE_GEM_EMISSIVE_MUL must not be wrapped in a getter',
|
||||
);
|
||||
});
|
||||
|
||||
// ── screen.js wiring ─────────────────────────────────────────────────────────
|
||||
|
||||
test('screen.js imports createStringGlow from src/string-glow.js', () => {
|
||||
assert.match(
|
||||
src,
|
||||
/import\s*\{\s*createStringGlow\s*\}\s*from\s*'\.\/src\/string-glow\.js'/,
|
||||
'screen.js must import createStringGlow',
|
||||
);
|
||||
});
|
||||
|
||||
test('screen.js original updateStringHighlights body is gone (no bare glowMul const inside)', () => {
|
||||
// After extraction the function definition no longer lives in screen.js.
|
||||
// The clearest signal: `const BASE_GLOW = 0.02 * glowMul` was inside the
|
||||
// function body and must not appear in screen.js post-extraction.
|
||||
assert.doesNotMatch(
|
||||
src,
|
||||
/const\s+BASE_GLOW\s*=\s*0\.02\s*\*\s*glowMul/,
|
||||
'BASE_GLOW constant must not remain in screen.js after extraction',
|
||||
);
|
||||
});
|
||||
|
||||
test('screen.js callsite uses createStringGlow factory destructure', () => {
|
||||
assert.match(
|
||||
src,
|
||||
/const\s*\{\s*updateStringHighlights\s*\}\s*=\s*createStringGlow\s*\(/,
|
||||
'screen.js must destructure updateStringHighlights from createStringGlow()',
|
||||
);
|
||||
});
|
||||
|
||||
// ── Wiring-correspondence guard (cut-9 pattern, empty PINNED_RENAMES) ────────
|
||||
// Verifies every entry in createStringGlow({…}) satisfies its naming class.
|
||||
// Kills swaps like getMGlow: () => mAccentCore.
|
||||
|
||||
test('createStringGlow({...}) wiring has correct naming correspondence (no param swaps)', () => {
|
||||
const PINNED_RENAMES = {}; // all entries are plain shorthand or standard get-arrows
|
||||
|
||||
const ANCHOR = 'const { updateStringHighlights } = createStringGlow({';
|
||||
const callStart = src.indexOf(ANCHOR);
|
||||
assert.ok(callStart >= 0, 'createStringGlow call must be findable in screen.js');
|
||||
const blockStart = callStart + ANCHOR.length - 1;
|
||||
assert.equal(src[blockStart], '{', 'expected { at computed blockStart');
|
||||
let depth = 0, blockEnd = -1;
|
||||
for (let i = blockStart; i < src.length; i++) {
|
||||
if (src[i] === '{') depth++;
|
||||
else if (src[i] === '}' && --depth === 0) { blockEnd = i; break; }
|
||||
}
|
||||
assert.ok(blockEnd > blockStart, 'createStringGlow argument block must have balanced braces');
|
||||
const inner = src.slice(blockStart + 1, blockEnd);
|
||||
|
||||
const rawEntries = [];
|
||||
let current = '', d = 0;
|
||||
for (let i = 0; i < inner.length; i++) {
|
||||
const ch = inner[i];
|
||||
if (ch === '{') d++;
|
||||
else if (ch === '}') d--;
|
||||
if (ch === ',' && d === 0) {
|
||||
const t = current.trim();
|
||||
if (t) rawEntries.push(t);
|
||||
current = '';
|
||||
} else {
|
||||
current += ch;
|
||||
}
|
||||
}
|
||||
if (current.trim()) rawEntries.push(current.trim());
|
||||
|
||||
const entries = rawEntries
|
||||
.map(e => e.replace(/\/\/[^\n]*/g, '').trim())
|
||||
.filter(Boolean);
|
||||
|
||||
assert.ok(entries.length >= 8, `expected at least 8 entries, got ${entries.length}`);
|
||||
|
||||
const violations = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.includes(':')) continue; // shorthand (VENUE_GEM_EMISSIVE_MUL, etc.)
|
||||
|
||||
const colonIdx = entry.indexOf(':');
|
||||
const key = entry.slice(0, colonIdx).trim();
|
||||
const value = entry.slice(colonIdx + 1).trim();
|
||||
|
||||
if (key in PINNED_RENAMES) {
|
||||
if (value !== PINNED_RENAMES[key]) {
|
||||
violations.push(`${key}: pinned to '${PINNED_RENAMES[key]}' but got '${value}'`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key.startsWith('get')) {
|
||||
const expectedStem = key[3].toLowerCase() + key.slice(4);
|
||||
const m = value.match(/^\(\)\s*=>\s*_?(\w+)$/);
|
||||
if (!m) {
|
||||
violations.push(`${key}: getter value '${value}' does not match () => [_]var`);
|
||||
continue;
|
||||
}
|
||||
if (m[1] !== expectedStem) {
|
||||
violations.push(`${key}: getter body references var stem '${m[1]}' but expected '${expectedStem}'`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
violations.push(`${key}: key:value entry not in PINNED_RENAMES and not a get/set arrow`);
|
||||
}
|
||||
|
||||
assert.deepEqual(violations, [], 'createStringGlow wiring violations found');
|
||||
});
|
||||
|
||||
// ── Behavioral kill test ─────────────────────────────────────────────────────
|
||||
// Calls updateStringHighlights with fake mesh/material objects and asserts
|
||||
// the emissive and opacity writes actually happened. RED when loop body is gutted.
|
||||
|
||||
test('updateStringHighlights writes emissive intensity and opacity to string meshes (behavioral kill: gut loop → RED)', async () => {
|
||||
const { createStringGlow } = await import(pathToFileURL(STRING_GLOW_JS).href);
|
||||
|
||||
// Fake material that records writes.
|
||||
const mat0 = { emissiveIntensity: 0, opacity: 0 };
|
||||
const mat1 = { emissiveIntensity: 0, opacity: 0 };
|
||||
const scaleSet = [];
|
||||
const stringLines = [
|
||||
{ material: mat0, scale: { set(...args) { scaleSet.push([0, ...args]); } } },
|
||||
{ material: mat1, scale: { set(...args) { scaleSet.push([1, ...args]); } } },
|
||||
];
|
||||
const mGlow = [{ emissiveIntensity: 0 }, { emissiveIntensity: 0 }];
|
||||
const mAccentCore = [{ emissiveIntensity: 0 }, { emissiveIntensity: 0 }];
|
||||
|
||||
const { updateStringHighlights } = createStringGlow({
|
||||
VENUE_GEM_EMISSIVE_MUL: 1.12,
|
||||
getGlowMul: () => 1,
|
||||
getVibrancyIdleOp: () => 0.4,
|
||||
getVenueSceneOverride: () => false,
|
||||
getNStr: () => 2,
|
||||
getStringLines: () => stringLines,
|
||||
getMGlow: () => mGlow,
|
||||
getMAccentCore: () => mAccentCore,
|
||||
});
|
||||
|
||||
// String 0: sustaining (stringSustain=true) + strGlow=0.8
|
||||
// String 1: anticipating (stringAnticipation=0.5) + strGlow=0.3
|
||||
const noteState = {
|
||||
stringSustain: [true, false],
|
||||
stringAnticipation: [0, 0.5],
|
||||
strGlow: [0.8, 0.3],
|
||||
accentFillBoost: [0, 0],
|
||||
};
|
||||
|
||||
updateStringHighlights(noteState);
|
||||
|
||||
// String 0 — sustain intensity=1: BASE_GLOW=0.02, MAX_GLOW=3.5
|
||||
const expectedEI0 = 0.02 + 1 * 3.5; // 3.52
|
||||
assert.strictEqual(
|
||||
mat0.emissiveIntensity,
|
||||
expectedEI0,
|
||||
`string 0 emissiveIntensity must be BASE_GLOW + MAX_GLOW = ${expectedEI0}`,
|
||||
);
|
||||
// IDLE_OP=0.4, intensity=1 → opacity = 0.4 + 1*(1-0.4) = 1.0
|
||||
assert.strictEqual(mat0.opacity, 1.0, 'string 0 opacity must be 1 when sustaining');
|
||||
|
||||
// String 1 — anticipation=0.5: emissive = 0.02 + 0.5*3.5 = 1.77
|
||||
const expectedEI1 = 0.02 + 0.5 * 3.5;
|
||||
assert.strictEqual(mat1.emissiveIntensity, expectedEI1,
|
||||
`string 1 emissiveIntensity must be BASE_GLOW + 0.5*MAX_GLOW = ${expectedEI1}`);
|
||||
|
||||
// mGlow writes: bg = strGlow * glowMul; venueGemMul = 1 (no venue override)
|
||||
assert.strictEqual(mGlow[0].emissiveIntensity, 0.8, 'mGlow[0] must receive strGlow[0] * glowMul');
|
||||
assert.strictEqual(mGlow[1].emissiveIntensity, 0.3, 'mGlow[1] must receive strGlow[1] * glowMul');
|
||||
|
||||
// scale.set was called for both strings (intensity > 0)
|
||||
assert.ok(scaleSet.length === 2, 'scale.set must be called for both strings');
|
||||
});
|
||||
|
||||
test('updateStringHighlights respects venueSceneOverride multiplier on mGlow (behavioral)', async () => {
|
||||
const { createStringGlow } = await import(pathToFileURL(STRING_GLOW_JS).href);
|
||||
|
||||
const mGlow = [{ emissiveIntensity: 0 }];
|
||||
const mAccentCore = [{ emissiveIntensity: 0 }];
|
||||
|
||||
const { updateStringHighlights } = createStringGlow({
|
||||
VENUE_GEM_EMISSIVE_MUL: 1.12,
|
||||
getGlowMul: () => 1,
|
||||
getVibrancyIdleOp: () => 0.4,
|
||||
getVenueSceneOverride: () => true, // venue override ON
|
||||
getNStr: () => 1,
|
||||
getStringLines: () => [null], // no mesh → only glow write
|
||||
getMGlow: () => mGlow,
|
||||
getMAccentCore: () => mAccentCore,
|
||||
});
|
||||
|
||||
updateStringHighlights({
|
||||
stringSustain: [false],
|
||||
stringAnticipation: [0],
|
||||
strGlow: [1.0],
|
||||
accentFillBoost: [0],
|
||||
});
|
||||
|
||||
// bg=1.0, venueGemMul=1.12 → mGlow[0].emissiveIntensity = 1.12
|
||||
assert.ok(
|
||||
Math.abs(mGlow[0].emissiveIntensity - 1.12) < 1e-9,
|
||||
`mGlow emissiveIntensity must be bg * VENUE_GEM_EMISSIVE_MUL = 1.12, got ${mGlow[0].emissiveIntensity}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('updateStringHighlights skips null stringLines entries without throwing (behavioral)', async () => {
|
||||
const { createStringGlow } = await import(pathToFileURL(STRING_GLOW_JS).href);
|
||||
|
||||
const mGlow = [{ emissiveIntensity: 0 }, { emissiveIntensity: 0 }];
|
||||
const mAccentCore = [{ emissiveIntensity: 0 }, { emissiveIntensity: 0 }];
|
||||
|
||||
const { updateStringHighlights } = createStringGlow({
|
||||
VENUE_GEM_EMISSIVE_MUL: 1.12,
|
||||
getGlowMul: () => 1,
|
||||
getVibrancyIdleOp: () => 0.4,
|
||||
getVenueSceneOverride: () => false,
|
||||
getNStr: () => 2,
|
||||
getStringLines: () => [null, null], // no meshes at all
|
||||
getMGlow: () => mGlow,
|
||||
getMAccentCore: () => mAccentCore,
|
||||
});
|
||||
|
||||
// Must not throw; mGlow writes still happen
|
||||
assert.doesNotThrow(() => updateStringHighlights({
|
||||
stringSustain: [true, true],
|
||||
stringAnticipation: [0, 0],
|
||||
strGlow: [0.5, 0.5],
|
||||
accentFillBoost: [0, 0],
|
||||
}));
|
||||
assert.strictEqual(mGlow[0].emissiveIntensity, 0.5, 'mGlow writes must still happen for null mesh slots');
|
||||
});
|
||||
Reference in New Issue
Block a user