mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-12 19:59:35 +00:00
Post-merge Codex review of the bend-curve PRs (#531/#532) surfaced edge cases: - GP8 (#531 P2): bnv timing used rn.sustain, which is zeroed for notes <= 0.2s, so short GP8 bends kept the scalar bn but lost bt/bnv. Use the beat duration `dur` (matching the GP5 path) so the curve survives. - 2D highway (#532 P2): bnvNormalizedPoints mapped x over the curve's own t-range [first,last] instead of the note span, so curves not starting at 0 / ending at sus were time-distorted. Now maps over [0, sus] (clamped), with a curve-span fallback when sus<=0 (existing no-sus callers unaffected). - 3D highway (#532 P3): the sustain ribbon + bend chevron were gated on bn>0, so a note carrying an authoritative bnv with bn==0 drew no ribbon/marker. Both now also fire on bnv presence; chevron steps derived from max(bn, bnv peak). Codex-reviewed: clean (no findings). +1 JS test (sus-relative mapping + fallback). JS 8/8, 250 core GP/song tests pass. NB: GP8's short-bend path still lacks a dedicated synthetic-GPIF fixture (same gap as the GP8 offset-prop-names P3) — _gpx_bend_shape units cover the function; the fix is the one-line caller change. Part of got-feedback/feedback#334. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
4.1 KiB
JavaScript
91 lines
4.1 KiB
JavaScript
// Behavioural tests for the per-note bend-curve (bnv, §6.2.1) render helpers:
|
|
// `bnvNormalizedPoints` (static/highway.js, 2D glyph) and `bnvSampleAt`
|
|
// (plugins/highway_3d/screen.js, 3D Y gesture). Both are pure, so we extract
|
|
// the function source by brace-matching and eval it in isolation.
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
function extractFn(src, name) {
|
|
const start = src.indexOf('function ' + name);
|
|
assert.ok(start >= 0, `function ${name} must exist`);
|
|
const open = src.indexOf('{', start);
|
|
let depth = 0;
|
|
for (let i = open; i < src.length; i++) {
|
|
if (src[i] === '{') depth++;
|
|
else if (src[i] === '}' && --depth === 0) return src.slice(start, i + 1);
|
|
}
|
|
throw new Error(`unbalanced braces extracting ${name}`);
|
|
}
|
|
|
|
function loadFn(file, name) {
|
|
const src = fs.readFileSync(path.join(__dirname, '..', '..', file), 'utf8');
|
|
return new Function('"use strict";' + extractFn(src, name) + `\nreturn ${name};`)();
|
|
}
|
|
|
|
const bnvNormalizedPoints = loadFn('static/highway.js', 'bnvNormalizedPoints');
|
|
const bnvSampleAt = loadFn('plugins/highway_3d/screen.js', 'bnvSampleAt');
|
|
|
|
// ── bnvNormalizedPoints (2D) ─────────────────────────────────────────────────
|
|
|
|
test('bnvNormalizedPoints normalizes t to 0..1 across the curve span (no sus)', () => {
|
|
const pts = bnvNormalizedPoints([
|
|
{ t: 0.5, v: 0 }, { t: 1.0, v: 2 }, { t: 1.5, v: 0 }]);
|
|
assert.deepEqual(pts, [
|
|
{ x: 0, v: 0 }, { x: 0.5, v: 2 }, { x: 1, v: 0 }]);
|
|
});
|
|
|
|
test('bnvNormalizedPoints maps t over the note sus span when given', () => {
|
|
// A bend that completes at t=0.4 of a 0.5s note draws to x=0.8, not x=1 —
|
|
// i.e. it stops short of the glyph's right edge (correct timing shape).
|
|
assert.deepEqual(
|
|
bnvNormalizedPoints([{ t: 0, v: 0 }, { t: 0.25, v: 1 }, { t: 0.4, v: 0 }], 0.5),
|
|
[{ x: 0, v: 0 }, { x: 0.5, v: 1 }, { x: 0.8, v: 0 }]);
|
|
// Points beyond sus clamp to 1; sus<=0 falls back to curve-span mapping.
|
|
assert.deepEqual(bnvNormalizedPoints([{ t: 0, v: 0 }, { t: 1, v: 2 }], 0.5),
|
|
[{ x: 0, v: 0 }, { x: 1, v: 2 }]);
|
|
assert.deepEqual(bnvNormalizedPoints([{ t: 0, v: 0 }, { t: 1, v: 2 }], 0),
|
|
[{ x: 0, v: 0 }, { x: 1, v: 2 }]);
|
|
});
|
|
|
|
test('bnvNormalizedPoints handles degenerate/empty input', () => {
|
|
assert.deepEqual(bnvNormalizedPoints([]), []);
|
|
assert.deepEqual(bnvNormalizedPoints(null), []);
|
|
// All-same-t span collapses x to 0 (no divide-by-zero).
|
|
assert.deepEqual(bnvNormalizedPoints([{ t: 1, v: 1 }, { t: 1, v: 2 }]),
|
|
[{ x: 0, v: 1 }, { x: 0, v: 2 }]);
|
|
});
|
|
|
|
// ── bnvSampleAt (3D) ─────────────────────────────────────────────────────────
|
|
|
|
test('bnvSampleAt linearly interpolates between points', () => {
|
|
const bnv = [{ t: 0, v: 0 }, { t: 1, v: 2 }];
|
|
assert.equal(bnvSampleAt(bnv, 0.5), 1); // midpoint
|
|
assert.equal(bnvSampleAt(bnv, 0.25), 0.5);
|
|
});
|
|
|
|
test('bnvSampleAt clamps to the endpoints', () => {
|
|
const bnv = [{ t: 0.2, v: 1 }, { t: 0.8, v: 3 }];
|
|
assert.equal(bnvSampleAt(bnv, 0), 1); // before first
|
|
assert.equal(bnvSampleAt(bnv, 5), 3); // after last
|
|
});
|
|
|
|
test('bnvSampleAt traces a round-trip curve up then back down', () => {
|
|
const bnv = [{ t: 0, v: 0 }, { t: 0.5, v: 2 }, { t: 1, v: 0 }];
|
|
assert.equal(bnvSampleAt(bnv, 0.25), 1); // rising
|
|
assert.equal(bnvSampleAt(bnv, 0.5), 2); // peak
|
|
assert.equal(bnvSampleAt(bnv, 0.75), 1); // falling
|
|
});
|
|
|
|
test('bnvSampleAt returns 0 for an empty/invalid curve', () => {
|
|
assert.equal(bnvSampleAt([], 0.5), 0);
|
|
assert.equal(bnvSampleAt(null, 0.5), 0);
|
|
});
|
|
|
|
test('bnvSampleAt tolerates a zero-width segment (duplicate t)', () => {
|
|
const bnv = [{ t: 0, v: 0 }, { t: 0.5, v: 1 }, { t: 0.5, v: 2 }, { t: 1, v: 2 }];
|
|
assert.equal(bnvSampleAt(bnv, 0.5), 1); // first matching segment wins
|
|
});
|