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
+35
View File
@@ -373,6 +373,40 @@ static void testKnotOnANearLevelSegmentIsANoOp() {
CHECK(out.decayCurve == 2.5);
}
// The knot drag must read the SAME phi the draw used even off the segment midpoint (an odd
// pixel span), not the fixed phi = 0.5 wideArea()'s AttackCurve span happens to land on above.
// Checked two ways: a zero-delta grab reproduces the stored exponent, and a real one-pixel drag
// moves the knot's own drawn y by the same one pixel every other node axis tracks 1:1.
static void testKnotDragTracksTheDrawOnAnOddPixelSpan() {
bool found = false;
for (int width = 24; width <= 260 && !found; ++width) {
const Rect a = Rect::ltrb(0, 0, width, 100);
StageEnvelope e = ahdsrEnv();
e.attackCurve = 3.0;
EnvVertex origin, attackEnd, knot;
const std::vector<EnvVertex> poly = buildEnvelopePolyline(e, overlayOf(a), kTotal);
if (!findNode(poly, EnvNode::Origin, origin)) continue;
if (!findNode(poly, EnvNode::AttackEnd, attackEnd)) continue;
if (!findNode(poly, EnvNode::AttackCurve, knot)) continue;
const int span = attackEnd.x - origin.x;
if (span <= 0 || span % 2 == 0) continue;
found = true;
const StageEnvelope same =
resolveNodeDrag(e, EnvNode::AttackCurve, overlayOf(a), kTotal, bounds(), 0, 0);
CHECK(std::fabs(same.attackCurve - e.attackCurve) < 1e-9);
const StageEnvelope dragged =
resolveNodeDrag(e, EnvNode::AttackCurve, overlayOf(a), kTotal, bounds(), 0, 1);
EnvVertex knotAfter;
CHECK(findNode(buildEnvelopePolyline(dragged, overlayOf(a), kTotal), EnvNode::AttackCurve,
knotAfter));
CHECK(knotAfter.x == knot.x); // a curve drag never moves the knot's x
CHECK(std::abs(knotAfter.y - (knot.y + 1)) <= 1);
}
CHECK(found); // the sweep must actually land on an odd span
}
// --- the interaction law on the overlay ----------------------------------------
// Ctrl scales the PIXEL delta, so it composes with every axis — the tapered schematic, the 1:1
@@ -476,6 +510,7 @@ int main() {
testKnotAndModelCannotDiverge();
testKnotOnALevelSegmentIsANoOp();
testKnotOnANearLevelSegmentIsANoOp();
testKnotDragTracksTheDrawOnAnOddPixelSpan();
testDegenerateInputsAreNoOps();