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
+36 -1
View File
@@ -4,7 +4,8 @@
// pixel delta produces exactly the param a knob would have.
//
// Covers: nodeAtPoint (every drawn handle grabbable, the anchored ReleaseEnd and the Origin
// never grabbed, other-kind nodes rejected, misses outside the radius); resolveNodeDrag
// never grabbed, other-kind nodes rejected, misses outside the radius, a dead coincident AHD
// DecayEnd excluded while a functional one stays grabbable); resolveNodeDrag
// (AHDSR stage times at the schematic scale, the sustain level on Y, the release dragged from
// its START with the inverted sign, the caller's clamp domain, AHD stage times at the 1:1
// scale, the hold FRACTION); curve-knot drags (the exponent domain, its endpoints, and the
@@ -47,6 +48,18 @@ static StageEnvelope ahdsrEnv() {
return e;
}
// F1's coincidence cases need attack/decay/fraction/span combinations ahdEnv() doesn't cover.
static StageEnvelope ahd(double a, double d, double frac, double origin, double span) {
StageEnvelope e;
e.kind = EnvKind::Ahd;
e.attackSeconds = a;
e.decaySeconds = d;
e.holdFraction = frac;
e.originSeconds = origin;
e.spanSeconds = span;
return e;
}
static StageEnvelope ahdEnv() {
StageEnvelope e;
e.kind = EnvKind::Ahd;
@@ -117,6 +130,26 @@ static void testAhdHasNoSustainNodes() {
CHECK(out.attackSeconds == e.attackSeconds);
}
// F1: at the Trigger default (decay 0, holdFraction 1.0) DecayEnd sits on HoldEnd's own instant
// AND cannot move there (resolveNodeDrag's decay branch has derivative 0 — see the denom guard).
// A grab at its true (now un-nudged) position must miss rather than resolve to a dead handle;
// HoldEnd, the live node underneath, stays fully grabbable.
static void testDeadCoincidentDecayEndIsNotGrabbable() {
const StageEnvelope e = ahd(0.5, 0.0, 1.0, 0.0, 3.0);
CHECK(!grabAt(e, EnvNode::DecayEnd).hit);
CHECK(grabAt(e, EnvNode::HoldEnd).hit);
CHECK(grabAt(e, EnvNode::HoldEnd).node == EnvNode::HoldEnd);
}
// Coincidence alone does not drop DecayEnd — only holdFraction == 1.0 makes it truly dead. With
// holdFraction < 1 the X-drag still moves decaySeconds (denom > 0), so it stays grabbable even
// when decay is 0 and it starts out coincident with HoldEnd.
static void testFunctionalCoincidentDecayEndStaysGrabbable() {
const StageEnvelope e = ahd(0.5, 0.0, 0.5, 0.0, 3.0);
CHECK(grabAt(e, EnvNode::DecayEnd).hit);
CHECK(grabAt(e, EnvNode::DecayEnd).node == EnvNode::DecayEnd);
}
static void testMissOutsideTheRadius() {
const StageEnvelope e = ahdsrEnv();
const Rect a = wideArea();
@@ -310,6 +343,8 @@ int main() {
testEveryDrawnHandleIsGrabbable();
testAnchoredEndAndOriginAreNotGrabbable();
testAhdHasNoSustainNodes();
testDeadCoincidentDecayEndIsNotGrabbable();
testFunctionalCoincidentDecayEndStaysGrabbable();
testMissOutsideTheRadius();
testAhdsrStageTimesTrackTheSchematicScale();