Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,24 @@ function sanitizeBendCurve(raw) {
out.sort((a, b) => a.t - b.t);
return out;
}

// Rescale a bend curve so its peak == `peak` (preserves shape). Returns null
// when the curve is empty/invalid, `peak <= 0`, or the curve is all-zero
// (unscalable) — callers then drop the curve so the scalar `bn` and `bnv` can
// never contradict each other.
function rescaleBendCurveToPeak(raw, peak) {
const clean = sanitizeBendCurve(raw);
if (!clean || !(peak > 0)) return null;
const oldPeak = clean.reduce((m, p) => Math.max(m, p.v), 0);
if (!(oldPeak > 0)) return null;
const k = peak / oldPeak;
const out = clean.map(p => ({ t: p.t, v: Math.round(p.v * k * 10) / 10 }));
// A target peak below bnv's 0.1 precision (e.g. 0.04) rounds every point to
// 0 — the curve can't carry the peak. Report unscalable so the caller drops
// it and keeps the scalar bn, rather than deriving a contradictory 0.
if (!(out.reduce((m, p) => Math.max(m, p.v), 0) > 0)) return null;
return out;
}
/* @pure:bend-shape:end */

// Reconstruct chords from notes at the same time before saving
Expand Down Expand Up @@ -2752,6 +2770,11 @@ function _editorBendModal({ bn = 0, bt = 0, bnv = null, sustain = 0 } = {}) {
bnInput.addEventListener('change', () => {
const v = Number(bnInput.value);
curBn = Number.isFinite(v) ? Math.max(0, Math.min(3, v)) : 0;
// Keep the curve consistent with the Peak input: rescale to the new
// peak (preserves shape), or clear it — when Peak is 0 (= no bend),
// or when the curve is empty/all-zero so it can't carry the peak
// (else OK would derive bn=0 and silently discard the Peak edit).
pts = curBn > 0 ? (rescaleBendCurveToPeak(pts, curBn) || []) : [];
bnInput.value = String(curBn);
redraw();
});
Expand All @@ -2762,9 +2785,16 @@ function _editorBendModal({ bn = 0, bt = 0, bnv = null, sustain = 0 } = {}) {
};
inner.querySelector('#bend-clear').onclick = () => { pts = []; redraw(); };
inner.querySelector('#bend-cancel').onclick = () => done(null);
inner.querySelector('#bend-ok').onclick = () => done({
bn: curBn, bt: curBt, bnv: sanitizeBendCurve(pts),
});
inner.querySelector('#bend-ok').onclick = () => {
const cleanBnv = sanitizeBendCurve(pts);
// `bn` is the PEAK; when a curve exists it MUST equal the curve's
// peak (renderers/graders treat bnv as authoritative). Reconcile so
// a saved `bn` can never contradict `bnv`.
const finalBn = (cleanBnv && cleanBnv.length)
? Math.max(0, ...cleanBnv.map(p => p.v))
: curBn;
done({ bn: finalBn, bt: curBt, bnv: cleanBnv });
};

_installModalKeyboard(modal, inner, () => done(null));
bnInput.focus();
Expand Down Expand Up @@ -3845,6 +3875,18 @@ window.editorInspectorSetTech = (key, raw) => {
for (const n of sel) {
if (!n.techniques) n.techniques = {};
n.techniques[key] = v;
// Editing the scalar peak must keep any authored curve consistent
// (renderers/graders read bnv as authoritative): rescale the curve to
// the new peak, or drop it when the peak is 0 / the curve is unscalable.
if (key === 'bend' && sanitizeBendCurve(n.techniques.bend_values)) {
const scaled = v > 0
? rescaleBendCurveToPeak(n.techniques.bend_values, v)
: null;
n.techniques.bend_values = scaled;
// bnv rounds points to 0.1, so a non-0.1 `v` (e.g. 0.25) would leave
// bn disagreeing with the curve's real peak. Snap bn to the curve.
if (scaled) n.techniques.bend = scaled.reduce((m, p) => Math.max(m, p.v), 0);
}
}
draw();
updateStatus();
Expand Down
46 changes: 42 additions & 4 deletions tests/bend_shape.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ if (!m) {
console.error('FAIL: @pure:bend-shape block not found in screen.js');
process.exit(1);
}
const { bendPresetCurve, sanitizeBendCurve, BEND_INTENTS } = new Function(
'"use strict";' + m[0] +
'\nreturn { bendPresetCurve, sanitizeBendCurve, BEND_INTENTS };'
)();
const { bendPresetCurve, sanitizeBendCurve, rescaleBendCurveToPeak, BEND_INTENTS } =
new Function(
'"use strict";' + m[0] +
'\nreturn { bendPresetCurve, sanitizeBendCurve, rescaleBendCurveToPeak,' +
' BEND_INTENTS };'
)();

let pass = 0, fail = 0;
function t(name, fn) {
Expand Down Expand Up @@ -76,5 +78,41 @@ t('empty / non-list / all-invalid -> null (never [])', () => {
assert.strictEqual(sanitizeBendCurve([{ t: 'a', v: 'b' }, 42]), null);
});

// ── rescaleBendCurveToPeak ───────────────────────────────────────────────────
// Keeps the scalar `bn` and the `bnv` curve from contradicting: editing Peak
// rescales the curve's shape to the new peak (or drops it when unscalable).
t('rescales a curve so its peak matches the target (shape preserved)', () => {
assert.deepStrictEqual(
rescaleBendCurveToPeak([{ t: 0, v: 0 }, { t: 0.5, v: 1 }, { t: 1, v: 0 }], 2),
[{ t: 0, v: 0 }, { t: 0.5, v: 2 }, { t: 1, v: 0 }]);
});
t('scaling down halves a peak-2 curve to peak 1', () => {
assert.deepStrictEqual(
rescaleBendCurveToPeak([{ t: 0, v: 2 }, { t: 1, v: 0 }], 1),
[{ t: 0, v: 1 }, { t: 1, v: 0 }]);
});
t('rounds rescaled v to 1 decimal (matches sanitize)', () => {
assert.deepStrictEqual(
rescaleBendCurveToPeak([{ t: 0, v: 0 }, { t: 1, v: 3 }], 1),
[{ t: 0, v: 0 }, { t: 1, v: 1 }]); // 3 -> *(1/3) = 1.0
});
t('peak<=0, empty, or all-zero (unscalable) curve -> null', () => {
assert.strictEqual(rescaleBendCurveToPeak([{ t: 0, v: 1 }], 0), null);
assert.strictEqual(rescaleBendCurveToPeak([{ t: 0, v: 1 }], -1), null);
assert.strictEqual(rescaleBendCurveToPeak([], 2), null);
assert.strictEqual(rescaleBendCurveToPeak(null, 2), null);
assert.strictEqual(rescaleBendCurveToPeak([{ t: 0, v: 0 }, { t: 1, v: 0 }], 2), null);
});
t('sub-0.1 target peak that rounds every point to 0 -> null (not a 0 curve)', () => {
// 0.04 is below bnv's 0.1 precision; the curve can't carry it, so the
// caller drops it and keeps the scalar bn instead of deriving bn=0.
assert.strictEqual(
rescaleBendCurveToPeak([{ t: 0, v: 0 }, { t: 1, v: 1 }], 0.04), null);
// 0.05 rounds up to a real 0.1 peak, so it survives.
assert.deepStrictEqual(
rescaleBendCurveToPeak([{ t: 0, v: 0 }, { t: 1, v: 1 }], 0.05),
[{ t: 0, v: 0 }, { t: 1, v: 0.1 }]);
});

console.log(`\n${pass} passed, ${fail} failed`);
if (fail) process.exit(1);