Fix envelope-overlay knot/trace disagreement on odd pixel spans

Generalize curveMidLevel/curveFromMidLevel to curveLevelAt/curveFromLevelAt at
arbitrary phi; knotVtx and its drag inverse now read the phi a knot's truncated
x actually implies, not always 0.5.
This commit is contained in:
2026-08-01 21:31:40 -04:00
parent ee8a956fbd
commit a1b42ed1a8
7 changed files with 252 additions and 32 deletions
+37
View File
@@ -14,6 +14,7 @@
#include <cmath>
#include <cstdio>
#include <initializer_list>
using namespace reasampler::util;
@@ -107,6 +108,39 @@ static void testMidLevelInverseSaturates() {
CHECK(std::fabs(curveFromMidLevel(0.5) - kCurveNeutral) < 1e-12);
}
// curveLevelAt/curveFromLevelAt is the general form a knot's own (possibly off-centre) phi
// needs — curveMidLevel/curveFromMidLevel is the phi = 0.5 case, not a second law.
static void testMidLevelIsThePhiHalfSpecialCase() {
for (double e : {kCurveMin, 0.3, kCurveNeutral, 2.0, kCurveMax}) {
CHECK(curveLevelAt(0.5, e) == curveMidLevel(e));
}
for (double u : {0.0, 0.2, 0.5, 0.8, 1.0}) {
CHECK(curveFromLevelAt(0.5, u) == curveFromMidLevel(u));
}
}
// The round trip must hold at an arbitrary phi, not only 0.5 — this is what a knot whose
// integer x lands off its segment's true midpoint (an odd pixel span) actually exercises.
static void testLevelAtRoundTripsAtArbitraryPhi() {
for (double phi : {0.1, 0.3, 0.42, 0.5, 0.63, 0.9}) {
for (int i = 0; i <= 50; ++i) {
const double e = kCurveMin + (kCurveMax - kCurveMin) * (i / 50.0);
const double level = curveLevelAt(phi, e);
CHECK(level > 0.0 && level < 1.0);
CHECK(std::fabs(curveFromLevelAt(phi, level) - e) < 1e-9);
}
}
}
// Saturation holds at an arbitrary phi too, not only the mid-level special case.
static void testLevelAtInverseSaturatesAtArbitraryPhi() {
for (double phi : {0.2, 0.5, 0.8}) {
CHECK(curveFromLevelAt(phi, 0.0) == kCurveMax);
CHECK(curveFromLevelAt(phi, 1.0) == kCurveMin);
CHECK(curveFromLevelAt(phi, std::nan("")) == kCurveMax);
}
}
// --- The inner dial's travel ---------------------------------------------------
// The knob drag delivers `start - dy/kKnobDragRangePixels`. param_slider owns that constant and
@@ -184,6 +218,9 @@ int main() {
testClampCurveHoldsTheDomain();
testMidLevelRoundTripsAgainstTheExponent();
testMidLevelInverseSaturates();
testMidLevelIsThePhiHalfSpecialCase();
testLevelAtRoundTripsAtArbitraryPhi();
testLevelAtInverseSaturatesAtArbitraryPhi();
testKnobLawIsExactAtTheNeutralCentre();
testADialSweptThroughNeutralLandsOnTheIdentity();
testKnobLawRoundTripsOutsideTheDetent();