instrument: fix AHD DecayEnd overlay/grab defect, pin flaky curve test, close comment/doc findings

This commit is contained in:
2026-07-31 10:19:45 -04:00
parent 2fa1405b06
commit 03fb471c92
10 changed files with 117 additions and 28 deletions
+35 -3
View File
@@ -102,9 +102,10 @@ static void testDegenerateAreaAndDuration() {
// This is what makes a dragged handle track the cursor 1:1 (envelope_edit's own inverse reads
// this same function) — a scale regression here is exactly what a relational-only check misses.
static void testGatePxPerSecond() {
const double expected =
(1000.0 - 1.0 - 4.0 * kGateNodeSepPx) / (4.0 * kGateStageMaxSeconds); // 967/8 px/s
CHECK(gatePxPerSecond(wideArea()) == expected);
// 967 / 8 px/s, pinned as a literal — restating the formula with the same named constants
// would let a change to kGateNodeSepPx or kGateStageMaxSeconds move both sides and pass
// silently.
CHECK(gatePxPerSecond(wideArea()) == 120.875);
CHECK(gatePxPerSecond(Rect::ltrb(5, 5, 5, 45)) == 0.0); // zero-width area -> 0
CHECK(gatePxPerSecond(Rect::ltrb(0, 0, 10, 10)) > 0.0); // tiny area: usable floors at 1px, > 0
}
@@ -304,6 +305,36 @@ static void testAhdIsOneToOneWithTheTimeAxis() {
CHECK(!hasNode(poly, EnvNode::ReleaseCurve));
}
// F1 regression: DecayEnd stays at its true wall-clock instant even when that instant coincides
// with HoldEnd's (decay ~ 0) — the 1:1 AHD axis promises N seconds -> N seconds, and a nudge
// away from that instant lies about the shape, including the Trigger default's abrupt cutoff.
// Fails against the prior nudge, which moved DecayEnd right whenever the gap was under
// kGateNodeSepPx.
static void testDecayEndStaysAtItsTrueInstantEvenWhenCoincidentWithHoldEnd() {
const Rect a = wideArea();
const double total = 4.0;
// Short but nonzero decay: the true gap to HoldEnd is a few px, under kGateNodeSepPx, so
// the retired nudge would have fired here too.
const StageEnvelope shortDecay = ahd(0.5, 0.02, 1.0, 0.0, 3.0);
const AhdSplit sShort = splitAhdSeconds(shortDecay);
EnvVertex decayShort;
CHECK(findNode(buildEnvelopePolyline(shortDecay, overlayOf(a), total), EnvNode::DecayEnd,
decayShort));
CHECK(decayShort.x == timeToX(a, total, sShort.total));
// Zero decay (the Trigger AHD default's shape): DecayEnd and HoldEnd share the exact same
// instant — the abrupt cutoff — and DecayEnd must not be nudged off it.
const StageEnvelope zeroDecay = ahd(0.5, 0.0, 1.0, 0.0, 3.0);
const AhdSplit sZero = splitAhdSeconds(zeroDecay);
const std::vector<EnvVertex> polyZero = buildEnvelopePolyline(zeroDecay, overlayOf(a), total);
EnvVertex decayZero, holdZero;
CHECK(findNode(polyZero, EnvNode::DecayEnd, decayZero));
CHECK(findNode(polyZero, EnvNode::HoldEnd, holdZero));
CHECK(decayZero.x == timeToX(a, total, sZero.total));
CHECK(decayZero.x == holdZero.x); // truly coincident, not nudged apart
}
// --- curve knots --------------------------------------------------------------
// A knot rides every sloped stage that has a duration, and none that does not — a zero-length
@@ -392,6 +423,7 @@ int main() {
testAhdSplitNeverExceedsTheSpan();
testHoldFractionEndpoints();
testAhdIsOneToOneWithTheTimeAxis();
testDecayEndStaysAtItsTrueInstantEvenWhenCoincidentWithHoldEnd();
testKnotsRideOnlySlopedNonZeroSegments();
testKnotHeightTracksTheExponent();