mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 19:29:33 +00:00
feat(highway): render per-note bend curve (bnv) on 2D + 3D (#532)
PR-B of the bend-shape feature (feedpak §6.2.1). Both highways drew a bend
from the scalar `bn` only; now they trace the authoritative `bnv` curve
([{t, v}]) when present and fall back to the `bn` arc/envelope otherwise.
2D (static/highway.js drawNote): when a note carries `bnv`, draw the real
shape as a contour above the gem (round-trip rises then falls, pre-bend
starts high, release descends — `bt` is implicit in the point shape), with
an arrowhead only when the gesture ends rising. `bnvNormalizedPoints` maps
{t,v} to a 0..1 x span. The scalar-arrow path is preserved unchanged as the
fallback; the peak label is unchanged.
3D (plugins/highway_3d/screen.js): `bnvSampleAt` linearly interpolates the
curve (clamped to its endpoints) and `bendSemisAtTime` samples it when
present, else keeps the synthetic rise→hold→release envelope from `bn`. The
chevron count still comes from the peak. Fixed a stale-scratch hazard: the
reused `_scrChordNote` now resets `bnv`/`bt` (omit-when-default) after
Object.assign, mirroring the existing `fhm` reset, so a chord note without a
curve can't inherit the previous note's contour.
Render-only — no wire/schema change. Pure helpers covered by
tests/js/highway_bend_curve.test.js (interp, clamping, round-trip,
degenerate/empty); node --check passes on both files; full tests/js green.
Part of got-feedback/feedback#334
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e33df9a720
commit
351b273ab5
@@ -0,0 +1,77 @@
|
||||
// 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 span', () => {
|
||||
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 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
|
||||
});
|
||||
Reference in New Issue
Block a user