fix(instrument): stop Ctrl-before-click stealing a waveform mark grab from a coincident node
Ctrl held before mouse-down forced the node/toggle win over a smaller cap or column regardless of area; now it defers to the ordinary smallest-area arbitration like a plain click. Also amends product docs, VERIFICATION.md, and adds sparse-material/narrow-overlay test fixtures.
This commit is contained in:
@@ -280,15 +280,27 @@ static void testAMissedCandidateNeverWinsOnADegenerateZeroArea() {
|
||||
WaveformClaimant::kTab);
|
||||
}
|
||||
|
||||
// A control-click has no tab/marker meaning (only the node's hard/smooth toggle answers it), so
|
||||
// it resolves to the node whenever the node is in the running, even where a plain left-click at
|
||||
// the same pixel would hand the tab or marker the win on area alone.
|
||||
static void testControlClickAlwaysTakesTheNodeOverASmallerTabOrMarker() {
|
||||
// A control-click has no tab/marker meaning (only the node's hard/smooth toggle answers it), but
|
||||
// that must not let Ctrl steal a mark grab out from under the cursor: when a cap or column is
|
||||
// ALSO in the running, control-click defers to the ordinary smallest-area arbitration exactly
|
||||
// like a plain left-click would, so pressing Ctrl before or after the button gives the same
|
||||
// answer. Only with no cap/column in the running at all does control-click claim the node
|
||||
// outright regardless of area.
|
||||
static void testControlClickDefersToACoincidentCapOrColumnLikeAPlainClick() {
|
||||
const WaveformClaim node{true, kNodeArea};
|
||||
const WaveformClaim smallerTab{true, 50}; // would beat the node on a plain left-click
|
||||
CHECK(resolveWaveformClaim(node, smallerTab, WaveformClaim{}, SplineGesture::kLeft) ==
|
||||
WaveformClaimant::kTab);
|
||||
// Ctrl pressed before the click must not out-rank the cap that a plain click already gives
|
||||
// the win — the exact regression this pins.
|
||||
CHECK(resolveWaveformClaim(node, smallerTab, WaveformClaim{}, SplineGesture::kControlLeft) ==
|
||||
WaveformClaimant::kTab);
|
||||
const WaveformClaim smallerMarker{true, 80}; // still smaller than the node, no tab present
|
||||
CHECK(resolveWaveformClaim(node, WaveformClaim{}, smallerMarker,
|
||||
SplineGesture::kControlLeft) == WaveformClaimant::kMarker);
|
||||
// No cap or column at all: control-click still claims the node outright, regardless of area
|
||||
// — there is nothing else for it to defer to.
|
||||
CHECK(resolveWaveformClaim(node, WaveformClaim{}, WaveformClaim{}, SplineGesture::kControlLeft) ==
|
||||
WaveformClaimant::kNode);
|
||||
// No node in the running: control-click has nothing to fall back to, so the tab still wins.
|
||||
CHECK(resolveWaveformClaim(WaveformClaim{}, smallerTab, WaveformClaim{},
|
||||
@@ -311,7 +323,7 @@ int main() {
|
||||
testTabWinsAGenuineTabVersusMarkerTie();
|
||||
testNoHitAnywhereFallsThroughToNone();
|
||||
testAMissedCandidateNeverWinsOnADegenerateZeroArea();
|
||||
testControlClickAlwaysTakesTheNodeOverASmallerTabOrMarker();
|
||||
testControlClickDefersToACoincidentCapOrColumnLikeAPlainClick();
|
||||
|
||||
if (g_fail == 0) std::printf("spline_edit: all tests passed\n");
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
|
||||
@@ -394,6 +394,45 @@ static void testSnapRadiusIsThePixelBandsOwnFrameSpan() {
|
||||
CHECK(zeroCrossingSnapFrames(overlayOf(Rect::ltrb(0, 0, 0, 60)), 1000) == 0); // zero width
|
||||
}
|
||||
|
||||
// A narrower-than-kZeroCrossingSnapPx overlay pushes `area.x + kZeroCrossingSnapPx` past
|
||||
// area.right(), so xToFrame answers frameCount (its own past-the-edge clamp) and the "radius"
|
||||
// becomes the WHOLE buffer — the original unbounded-snap defect, on a width the band-stack
|
||||
// allocator's kEditorMinWidth floor never actually produces in the shipped editor. Documented as
|
||||
// a fixture rather than left implicit, since this is a public pure API and the width sweep
|
||||
// elsewhere in this file jumps straight from 0 to 1000.
|
||||
static void testNarrowOverlayLosesTheBoundBelowTheSnapWidth() {
|
||||
const Rect a = Rect::ltrb(0, 0, 3, 60); // narrower than kZeroCrossingSnapPx (5)
|
||||
CHECK(zeroCrossingSnapFrames(overlayOf(a), 100000) == 100000);
|
||||
}
|
||||
|
||||
// Long SPARSE material: a 1 s / 48 kHz 40 Hz square wave, crossings ~600 frames apart, drawn
|
||||
// 1000 px wide (r = 240). The dense sweep above holds every crossing well inside the radius by
|
||||
// construction, so it can never observe the bound; this is the only fixture where the radius
|
||||
// sits strictly BETWEEN two crossings on genuinely long material, so the bounded and unbounded
|
||||
// searches can actually disagree.
|
||||
static void testSnapBoundsALongSparseCaptureBetweenCrossings() {
|
||||
constexpr std::int64_t n = 48000, kHalfPeriod = 600; // 40 Hz square wave at 48 kHz
|
||||
std::vector<AudioSample> pcm(static_cast<std::size_t>(n));
|
||||
for (std::int64_t i = 0; i < n; ++i) {
|
||||
pcm[static_cast<std::size_t>(i)] = ((i / kHalfPeriod) % 2 == 0) ? 1.0f : -1.0f;
|
||||
}
|
||||
const Rect a = wideArea(); // width 1000 -> 48 frames per px
|
||||
const std::int64_t r = zeroCrossingSnapFrames(overlayOf(a), n);
|
||||
CHECK(r == kZeroCrossingSnapPx * 48); // 240
|
||||
CHECK(r < kHalfPeriod); // strictly between two crossings, not covering either
|
||||
|
||||
// Equidistant midpoint between the crossings at 600 and 1200: the unbounded search still
|
||||
// finds one (the tie rule picks the lower, 600), while the bounded snap correctly leaves the
|
||||
// mark where it was dropped — this pair IS the observable difference on long material.
|
||||
const std::int64_t crossing = kHalfPeriod, midpoint = crossing + kHalfPeriod / 2;
|
||||
CHECK(nearestZeroCrossing(pcm.data(), n, midpoint) == crossing);
|
||||
CHECK(snapToZeroCrossing(pcm.data(), n, midpoint, r) == midpoint);
|
||||
|
||||
// Inside the radius the snap still reaches its crossing, same as ever.
|
||||
CHECK(snapToZeroCrossing(pcm.data(), n, crossing + r, r) == crossing);
|
||||
CHECK(snapToZeroCrossing(pcm.data(), n, crossing - r, r) == crossing);
|
||||
}
|
||||
|
||||
// One cycle of a 60 Hz sine at 48 kHz — 800 frames, and exactly ONE interior sign change, at the
|
||||
// midpoint (frame 0 is on zero, which is not a crossing, and the up-crossing is the wrap). That
|
||||
// single crossing IS the reported defect: an unbounded search resolves every drop in the buffer
|
||||
@@ -944,8 +983,10 @@ int main() {
|
||||
testZeroCrossingClampsTarget();
|
||||
testZeroCrossingDegenerate();
|
||||
testSnapRadiusIsThePixelBandsOwnFrameSpan();
|
||||
testNarrowOverlayLosesTheBoundBelowTheSnapWidth();
|
||||
testSnapLeavesASingleCycleMarkWhereItWasDropped();
|
||||
testSnapIsUnchangedOnDenseMaterial();
|
||||
testSnapBoundsALongSparseCaptureBetweenCrossings();
|
||||
testSnapTakesACrossingAtTheRadiusAndRefusesOnePastIt();
|
||||
testSnapKeepsTheTieRuleInsideTheRadius();
|
||||
testSnapAtZeroRadiusMovesNothingButAnExactHit();
|
||||
|
||||
Reference in New Issue
Block a user