// Standalone tests for reasampler::instrument::ui::spline_edit — no VST3, no REAPER, no // framework. Asserts the ONE point-editing grammar both spline consumers route through: // left-click grabs a node and adds in empty space, right-click deletes, control-click toggles // hard/smooth, and a click outside the mapping box resolves to nothing unless it lands on a // node's pick radius (so the popup's inset ring can grab but never add). Also // resolveWaveformClaim, the waveform overlay's node/tab/marker cross-affordance arbitration — // waveform_view is a test-only link so those tests can build the real geometry // editor_input_waveform.cpp's mouseDownWaveform composes. #include "../src/core/instrument/ui/spline_edit.h" #include "../src/core/instrument/ui/waveform_view.h" #include "../src/core/instrument/ui/sample_bands.h" // kWaveformMinHeight #include using namespace reasampler::instrument::ui; using namespace reasampler::instrument::engine; static OverlayArea overlayOf(const Rect& r) { return OverlayArea{r}; } static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) static const VelocityCurve::Box kBox{100, 50, 127, 101}; // A three-point curve whose interior node sits well away from both endpoints. static VelocityCurve threePoint() { VelocityCurve c = VelocityCurve::rampDown(); c.addPoint(64.0, 0.5); return c; } static void testLeftClickOnANodeGrabsIt() { const VelocityCurve c = threePoint(); const auto px = c.pixelFromPoint(kBox, c.points()[1]); const SplineEdit e = resolveSplineEdit(c, kBox, SplineGesture::kLeft, px.x, px.y); CHECK(e.kind == SplineEditKind::kGrab); CHECK(e.index == 1); } static void testLeftClickInEmptySpaceAdds() { const VelocityCurve c = VelocityCurve::rampDown(); // Well away from either endpoint's drawn position and from the traced line's nodes. const SplineEdit e = resolveSplineEdit(c, kBox, SplineGesture::kLeft, 160, 60); CHECK(e.kind == SplineEditKind::kAdd); CHECK(e.index == -1); // no point yet — the caller creates it } static void testRightClickOnANodeDeletesAndElsewhereDoesNothing() { const VelocityCurve c = threePoint(); const auto px = c.pixelFromPoint(kBox, c.points()[1]); const SplineEdit hit = resolveSplineEdit(c, kBox, SplineGesture::kRight, px.x, px.y); CHECK(hit.kind == SplineEditKind::kDelete); CHECK(hit.index == 1); // Right-click on empty canvas must NOT add — delete is the only thing the gesture means. const SplineEdit miss = resolveSplineEdit(c, kBox, SplineGesture::kRight, 160, 60); CHECK(miss.kind == SplineEditKind::kNone); } static void testControlClickOnANodeTogglesHardAndElsewhereDoesNothing() { const VelocityCurve c = threePoint(); const auto px = c.pixelFromPoint(kBox, c.points()[1]); const SplineEdit hit = resolveSplineEdit(c, kBox, SplineGesture::kControlLeft, px.x, px.y); CHECK(hit.kind == SplineEditKind::kToggleHard); CHECK(hit.index == 1); const SplineEdit miss = resolveSplineEdit(c, kBox, SplineGesture::kControlLeft, 160, 60); CHECK(miss.kind == SplineEditKind::kNone); } // The popup draws its box inset inside a border; a click in that ring must be able to GRAB an // endpoint handle (which is drawn on the box edge, within the pick radius of the ring) but must // never ADD — an added point there clamps onto an endpoint's x and stacks an undeletable // duplicate. static void testOutsideTheBoxGrabsButNeverAdds() { const VelocityCurve c = VelocityCurve::rampDown(); const auto first = c.pixelFromPoint(kBox, c.points()[0]); const SplineEdit ring = resolveSplineEdit(c, kBox, SplineGesture::kLeft, first.x - 3, first.y - 3); CHECK(ring.kind == SplineEditKind::kGrab); CHECK(ring.index == 0); // Far outside, on no node at all. const SplineEdit away = resolveSplineEdit(c, kBox, SplineGesture::kLeft, kBox.left - 60, kBox.top - 40); CHECK(away.kind == SplineEditKind::kNone); } static void testDegenerateBoxResolvesToNothing() { const VelocityCurve c = threePoint(); CHECK(resolveSplineEdit(c, VelocityCurve::Box{0, 0, 0, 40}, SplineGesture::kLeft, 0, 0) .kind == SplineEditKind::kNone); CHECK(resolveSplineEdit(c, VelocityCurve::Box{0, 0, 40, 1}, SplineGesture::kLeft, 0, 0) .kind == SplineEditKind::kNone); } // The overlay's box is the FULL area — no inset — so the contour spans the sample's whole // drawn width 1:1 with its time axis. static void testOverlayBoxIsTheWholeArea() { const OverlayArea area{Rect::ltrb(12, 40, 812, 240)}; const VelocityCurve::Box box = splineOverlayBox(area); CHECK(box.left == 12); CHECK(box.top == 40); CHECK(box.width == 800); CHECK(box.height == 200); } // --- Smallest-target-first: resolveWaveformClaim, the shell's own comparison chain ----- // // editor_input_waveform.cpp's mouseDownWaveform resolves a click among a contour node (a fixed // pick box), a mark's CAP, and a mark's full-height column by calling // resolveWaveformClaim with each candidate's own target area; the smallest hit wins. These // tests build the real geometry over the pure primitives the shell composes, then feed it into // resolveWaveformClaim itself, so a reverted node-first/marker-first/tab-first ordering fails // them — pinning the mechanism, not just the input geometry it acts on. A realistic band height // (kWaveformMinHeight, the product's own floor) is used throughout so these numbers are the // worst case for the node, not a favourable one. static VelocityCurve::Box boxOf(const Rect& r) { return VelocityCurve::Box{r.x, r.y, r.width, r.height}; } constexpr std::int64_t kNodeSide = 2 * kCurveNodeGrabRadius + 1; constexpr std::int64_t kNodeArea = kNodeSide * kNodeSide; // 169, fixed // Case (a): a fresh Spline default (rampDown) puts its endpoint 0 at (box.left, box.top) — the // exact pixel the start marker draws at frame 0. The node's fixed 169px pick box is far smaller // than a kWaveformMinHeight-tall marker column, so the endpoint stays reachable. static void testFreshRampDownEndpointBeatsTheStartMarkerAtFrameZero() { const Rect a = Rect{20, 10, 1000, kWaveformMinHeight}; const OverlayArea overlay = overlayOf(a); const std::int64_t frames = 100000; const VelocityCurve contour = VelocityCurve::rampDown(); const VelocityCurve::Box box = boxOf(a); CHECK(contour.pointAtPixel(box, a.x, a.y) == 0); // endpoint 0 sits at (box.left, box.top) const std::int64_t markers[1] = {0}; CHECK(markerAtPoint(overlay, frames, markers, 1, a.x, a.y) == 0); // the coincidence const std::int64_t markerArea = static_cast(2 * kMarkerGrabWidth + 1) * a.height; // 11 * band height CHECK(kNodeArea < markerArea); // the node wins: the endpoint stays a genuine grab target const WaveformClaim node{true, kNodeArea}; const WaveformClaim marker{true, markerArea}; CHECK(resolveWaveformClaim(node, WaveformClaim{}, marker, SplineGesture::kLeft) == WaveformClaimant::kNode); } // Case (b): the crossfade tab at zero crossfade sits on loopStart's own pixel column; a node // dragged to value ~0.98 lands a couple of rows below the box top — inside the tab's own // top-strip band, where the review found the tab fully shadowed by a node-first pass. static void testCrossfadeTabBeatsAContourNodeNearItsTopStrip() { const Rect a = Rect{20, 10, 1000, kWaveformMinHeight}; const OverlayArea overlay = overlayOf(a); const std::int64_t frames = 100000; const std::int64_t loopStart = 40000, crossfade = 0; // zero crossfade -> tab sits on loopStart const int mx = frameToX(overlay, frames, loopStart - crossfade); const Rect tabRect = markerHandleRect(overlay, frames, loopStart - crossfade); CHECK(!tabRect.empty()); const VelocityCurve::Box box = boxOf(a); const int ny = a.y + 3; // ~0.98 up a kWaveformMinHeight-tall box; inside the tab's top strip VelocityCurve c = VelocityCurve::flat(); const VelocityPoint p = c.pointFromPixel(box, mx, ny); c.addPoint(p.velocity, p.value); CHECK(c.pointAtPixel(box, mx, ny) >= 0); CHECK(contains(tabRect, mx, ny)); // the coincidence: both claim the same pixel const std::int64_t tabArea = static_cast(tabRect.width) * tabRect.height; // <= 110 CHECK(tabArea < kNodeArea); // the tab wins: it stays the only affordance at zero crossfade const WaveformClaim node{true, kNodeArea}; const WaveformClaim tab{true, tabArea}; CHECK(resolveWaveformClaim(node, tab, WaveformClaim{}, SplineGesture::kLeft) == WaveformClaimant::kTab); // The residual the review names: the node keeps its OUTER columns, one pixel past the tab's // clipped edge but still inside its own pick radius. const int outerX = mx + kMarkerHandleHalfWidth + 1; CHECK(!contains(tabRect, outerX, ny)); CHECK(c.pointAtPixel(box, outerX, ny) >= 0); } // Case (c): a contour node coincident with a loop marker. At kWaveformMinHeight (the product's // own floor) the column is already an order of magnitude larger than the node's fixed pick box, // so the node wins the shared pixel while the column stays reachable everywhere the node isn't. static void testContourNodeBeatsALoopMarkerAtTheirSharedPixelButNotElsewhere() { const Rect a = Rect{20, 10, 1000, kWaveformMinHeight}; const OverlayArea overlay = overlayOf(a); const std::int64_t frames = 100000; const std::int64_t loopEnd = 70000; const int mx = frameToX(overlay, frames, loopEnd); const std::int64_t markers[1] = {loopEnd}; const VelocityCurve::Box box = boxOf(a); const int ny = a.y + a.height / 2; // mid-height, well clear of any tab VelocityCurve c = VelocityCurve::flat(); const VelocityPoint p = c.pointFromPixel(box, mx, ny); c.addPoint(p.velocity, p.value); CHECK(c.pointAtPixel(box, mx, ny) >= 0); CHECK(markerAtPoint(overlay, frames, markers, 1, mx, ny) == 0); // the coincidence const std::int64_t markerArea = static_cast(2 * kMarkerGrabWidth + 1) * a.height; // 11 * band height CHECK(kNodeArea < markerArea); // the node wins the shared pixel const WaveformClaim node{true, kNodeArea}; const WaveformClaim marker{true, markerArea}; CHECK(resolveWaveformClaim(node, WaveformClaim{}, marker, SplineGesture::kLeft) == WaveformClaimant::kNode); // A few rows clear of the node (outside its 13px pick box, still on the marker's column) // the marker alone claims the click. const int farY = ny + kCurveNodeGrabRadius + 4; CHECK(c.pointAtPixel(box, mx, farY) < 0); CHECK(markerAtPoint(overlay, frames, markers, 1, mx, farY) == 0); CHECK(resolveWaveformClaim(WaveformClaim{}, WaveformClaim{}, marker, SplineGesture::kLeft) == WaveformClaimant::kMarker); } // Case (d): every mark now carries a cap, which RESOLVES the long-open "staged-envelope-node // shadow at zero-attack" wart. A zero-attack AttackEnd node sits at the canvas's top-left — the // same pixel a START marker at frame 0 draws at — and used to win the click outright, because // the marker's only target there was its full-height column. START's cap is the same 11x10 tab // the crossfade always had, so the node no longer shadows it. The cap slot's nominal area is // unchanged by the change (every mark's cap is one markerHandleRect), which is why the // arbitration itself needed no re-tuning: cap < node < column still holds. static void testAMarkCapOutranksACoincidentEnvelopeNodeInTheTopStrip() { const Rect a = Rect{20, 10, 1000, kWaveformMinHeight}; const OverlayArea overlay = overlayOf(a); const std::int64_t frames = 100000; // START at frame 0: its cap clips against the band's left edge, and a zero-attack node is // drawn on that same corner. const Rect cap = markerHandleRect(overlay, frames, 0); CHECK(!cap.empty()); CHECK(contains(cap, a.x, a.y)); // NOMINAL, matching what the shell feeds the arbitration — the clipped tab at frame 0 is // the worst case for the cap, and it still wins on the nominal number the shell uses. const std::int64_t capArea = static_cast(2 * kMarkerHandleHalfWidth + 1) * kMarkerHandleHeight; const std::int64_t columnArea = static_cast(2 * kMarkerGrabWidth + 1) * a.height; CHECK(capArea < kNodeArea); CHECK(kNodeArea < columnArea); const WaveformClaim node{true, kNodeArea}; const WaveformClaim capClaim{true, capArea}; const WaveformClaim column{true, columnArea}; CHECK(resolveWaveformClaim(node, capClaim, column, SplineGesture::kLeft) == WaveformClaimant::kTab); // And the node keeps everything below the cap strip, which is where it is actually drawn // for any non-degenerate envelope. CHECK(!contains(cap, a.x, a.y + kMarkerHandleHeight)); CHECK(resolveWaveformClaim(node, WaveformClaim{}, column, SplineGesture::kLeft) == WaveformClaimant::kNode); } // The only live tie: a mark's cap (<=110) can equal the node (169) only off-geometry, but // tab-vs-marker ties at overlay height 10 (kMarkerHandleHeight), where the tab's 11x10 strip // (110) equals a marker column's 11 * 10 (110) — the tab wins, matching check order. static void testTabWinsAGenuineTabVersusMarkerTie() { CHECK(resolveWaveformClaim(WaveformClaim{}, WaveformClaim{true, 110}, WaveformClaim{true, 110}, SplineGesture::kLeft) == WaveformClaimant::kTab); } // No claimant hit at all falls through to kNone — the caller's cue to let the drawn contour take // empty space (addOnEmptySpace) rather than starting any drag. static void testNoHitAnywhereFallsThroughToNone() { CHECK(resolveWaveformClaim(WaveformClaim{}, WaveformClaim{}, WaveformClaim{}, SplineGesture::kLeft) == WaveformClaimant::kNone); } // A candidate that reports hit == false must never win merely because its (unused, default) // area of 0 looks "smallest" — hit gates a candidate out before its area is ever compared. Real // call sites never produce hit == false with area != 0, but the arbitration still owes one // well-defined answer to every input, not just the ones live geometry happens to produce. static void testAMissedCandidateNeverWinsOnADegenerateZeroArea() { const WaveformClaim missedNode{false, 0}; const WaveformClaim tab{true, 50}; const WaveformClaim marker{true, 100}; CHECK(resolveWaveformClaim(missedNode, tab, marker, SplineGesture::kLeft) == 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() { 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); CHECK(resolveWaveformClaim(node, smallerTab, 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{}, SplineGesture::kControlLeft) == WaveformClaimant::kTab); } int main() { testLeftClickOnANodeGrabsIt(); testLeftClickInEmptySpaceAdds(); testRightClickOnANodeDeletesAndElsewhereDoesNothing(); testControlClickOnANodeTogglesHardAndElsewhereDoesNothing(); testOutsideTheBoxGrabsButNeverAdds(); testDegenerateBoxResolvesToNothing(); testOverlayBoxIsTheWholeArea(); testFreshRampDownEndpointBeatsTheStartMarkerAtFrameZero(); testCrossfadeTabBeatsAContourNodeNearItsTopStrip(); testContourNodeBeatsALoopMarkerAtTheirSharedPixelButNotElsewhere(); testAMarkCapOutranksACoincidentEnvelopeNodeInTheTopStrip(); testTabWinsAGenuineTabVersusMarkerTie(); testNoHitAnywhereFallsThroughToNone(); testAMissedCandidateNeverWinsOnADegenerateZeroArea(); testControlClickAlwaysTakesTheNodeOverASmallerTabOrMarker(); if (g_fail == 0) std::printf("spline_edit: all tests passed\n"); return g_fail == 0 ? 0 : 1; }