fix: pin the waveform arbitration in a testable predicate, close round-4 review minors
Extracts resolveWaveformClaim (core/instrument/ui/spline_edit) so the shell's node/tab/marker click resolution is unit-tested directly, not just its input geometry; folds the staged-envelope node into it; fixes comment accuracy, a cost regression, and test fidelity issues.
This commit is contained in:
@@ -619,14 +619,14 @@ static bool sameLayout(const DeckLayout& a, const DeckLayout& b) {
|
||||
|
||||
// A Spline excursion is fully reversible at the layout level: the mode forcing swaps the amp
|
||||
// and filter faces onto their wider cells and back, leaving no residue in the geometry. Driven
|
||||
// through splineActive and the editor's own forcing rule, so the deck cannot agree with a
|
||||
// forcing rule the shell does not use.
|
||||
// through the shared enforceGateUnavailableWhileDrawn helper, so the deck cannot agree with a
|
||||
// forcing rule the real callers do not use.
|
||||
static void testGateSplineGateRoundTripsToTheSameLayout() {
|
||||
PlayParams p; // Gate, all three envelopes staged
|
||||
const DeckLayout before = layoutDeck(sampleDeckGroups(p.playMode), kPad, 0, kAvailAtMinWidth);
|
||||
|
||||
p.ampSpline.mode = EnvMode::Spline;
|
||||
if (splineActive(p)) p.playMode = PlayMode::Trigger; // editor_controls' forcing, verbatim
|
||||
enforceGateUnavailableWhileDrawn(p); // the shared helper both real callers route through
|
||||
CHECK(p.playMode == PlayMode::Trigger);
|
||||
const DeckLayout drawn = layoutDeck(sampleDeckGroups(p.playMode), kPad, 0, kAvailAtMinWidth);
|
||||
// The excursion is real: the amp face's cells are strictly wider than Gate's.
|
||||
|
||||
+170
-1
@@ -2,15 +2,22 @@
|
||||
// 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).
|
||||
// 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 <cstdio>
|
||||
|
||||
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)
|
||||
@@ -97,6 +104,159 @@ static void testOverlayBoxIsTheWholeArea() {
|
||||
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), the crossfade tab, and a marker'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<std::int64_t>(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<std::int64_t>(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<std::int64_t>(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);
|
||||
}
|
||||
|
||||
// The only live tie: the crossfade tab (<=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();
|
||||
@@ -105,6 +265,15 @@ int main() {
|
||||
testOutsideTheBoxGrabsButNeverAdds();
|
||||
testDegenerateBoxResolvesToNothing();
|
||||
testOverlayBoxIsTheWholeArea();
|
||||
|
||||
testFreshRampDownEndpointBeatsTheStartMarkerAtFrameZero();
|
||||
testCrossfadeTabBeatsAContourNodeNearItsTopStrip();
|
||||
testContourNodeBeatsALoopMarkerAtTheirSharedPixelButNotElsewhere();
|
||||
testTabWinsAGenuineTabVersusMarkerTie();
|
||||
testNoHitAnywhereFallsThroughToNone();
|
||||
testAMissedCandidateNeverWinsOnADegenerateZeroArea();
|
||||
testControlClickAlwaysTakesTheNodeOverASmallerTabOrMarker();
|
||||
|
||||
if (g_fail == 0) std::printf("spline_edit: all tests passed\n");
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
@@ -363,6 +363,33 @@ static void testFlatZeroFinalSegmentStillFreesTheVoiceEarly() {
|
||||
CHECK(!v.soundingNote()); // a genuine permanent terminus, not a mid-contour dip
|
||||
}
|
||||
|
||||
// The timing, not just the fact: the case above is trivially "early" (a wholly-flat contour
|
||||
// frees on frame 0), which can't distinguish "frees early" from "frees at the right frame." This
|
||||
// fixture's final segment starts MID-sample, so the free must land there, not at frame 0 and not
|
||||
// at the sample's natural end. The breakpoint (phase 0.5495) is deliberately off every sampled
|
||||
// frame's exact phase (k/1000), so no sampled frame lands on the segment boundary itself and
|
||||
// which segment "owns" that frame is never ambiguous.
|
||||
static void testFlatZeroFinalSegmentFreesTheVoiceWhereItBeginsNotAtFrameZero() {
|
||||
const double breakpointPhase = 0.5495;
|
||||
const VelocityCurve contour = VelocityCurve::fromPoints(
|
||||
{{xAt(0.0), 1.0}, {xAt(breakpointPhase), 0.0}, {xAt(1.0), 0.0}}, CurveDomain::Unipolar);
|
||||
const std::size_t frames = 1000;
|
||||
const SampleData s = splineAmpSample(frames, contour);
|
||||
|
||||
Voice v;
|
||||
v.start(60, 100, s);
|
||||
// Frames 0..549 (phase < breakpoint) sit on the declining first segment: still sounding.
|
||||
for (std::size_t i = 0; i < 550; ++i) {
|
||||
v.renderFrame();
|
||||
CHECK(v.soundingNote());
|
||||
}
|
||||
// Frame 550 (phase 0.55) is the first sampled frame past the breakpoint, on the flat-zero
|
||||
// final segment — this is where the early-free fires.
|
||||
const double y550 = v.renderFrame();
|
||||
CHECK(near(y550, 0.0, 1e-9));
|
||||
CHECK(!v.soundingNote());
|
||||
}
|
||||
|
||||
// --- 9. A fresh spline EG opens on the smooth y = 1 - x ----------------------
|
||||
|
||||
static void testAFreshSplineEgDefaultsToTheSmoothDownwardSlope() {
|
||||
@@ -532,6 +559,7 @@ int main() {
|
||||
testAContourReplaysProportionallyOnADifferentLengthSample();
|
||||
testTwoPointContourRisingFromZeroSoundsForItsFullSpan();
|
||||
testFlatZeroFinalSegmentStillFreesTheVoiceEarly();
|
||||
testFlatZeroFinalSegmentFreesTheVoiceWhereItBeginsNotAtFrameZero();
|
||||
testAFreshSplineEgDefaultsToTheSmoothDownwardSlope();
|
||||
testGateIsUnavailableWhileASplineEgIsActiveAndReturnsAfterwards();
|
||||
testEnforceGateUnavailableWhileDrawnForcesTriggerOnBothRepresentations();
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// stacked height, grabs reaching the lower lane); laneEnvelope (per-lane channel split).
|
||||
|
||||
#include "../src/core/instrument/ui/waveform_view.h"
|
||||
#include "../src/core/instrument/engine/velocity_curve.h" // kCurveNodeGrabRadius, VelocityCurve::pointAtPixel
|
||||
#include "../src/core/instrument/ui/sample_bands.h" // kWaveformMinHeight, kLaneGap
|
||||
|
||||
#include <cstddef>
|
||||
@@ -22,7 +21,6 @@
|
||||
|
||||
using namespace reasampler;
|
||||
using namespace reasampler::instrument::ui;
|
||||
using namespace reasampler::instrument::engine;
|
||||
using reasampler::audio::AudioSample;
|
||||
|
||||
static int g_fail = 0;
|
||||
@@ -386,102 +384,6 @@ static void testStartMarkerSharesTheHandleStripWhenItSitsAtTheFadeEdge() {
|
||||
CHECK(contains(markerHandleRect(overlayOf(a), 1000, fadeEdge), mx, topY));
|
||||
}
|
||||
|
||||
// --- Smallest-target-first: the three-way coincidence with a spline contour node -------
|
||||
//
|
||||
// editor_input_waveform.cpp's mouseDownWaveform resolves a click among a contour node (a fixed
|
||||
// pick box), the crossfade tab, and a marker's full-height column by measuring each candidate's
|
||||
// own target area and letting the smallest win — this module can't exercise the shell's
|
||||
// arbitration itself (no shell test target wraps the editor), but it can pin the geometric facts
|
||||
// that arbitration depends on, over the pure primitives it composes. 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}; }
|
||||
|
||||
// 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
|
||||
|
||||
constexpr std::int64_t nodeSide = 2 * kCurveNodeGrabRadius + 1;
|
||||
constexpr std::int64_t nodeArea = nodeSide * nodeSide; // 169, fixed
|
||||
const std::int64_t markerArea =
|
||||
static_cast<std::int64_t>(2 * kMarkerGrabWidth + 1) * a.height; // 11 * band height
|
||||
CHECK(nodeArea < markerArea); // the node wins: the endpoint stays a genuine grab target
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
constexpr std::int64_t nodeSide = 2 * kCurveNodeGrabRadius + 1;
|
||||
constexpr std::int64_t nodeArea = nodeSide * nodeSide; // 169, fixed
|
||||
const std::int64_t tabArea = static_cast<std::int64_t>(tabRect.width) * tabRect.height; // <= 110
|
||||
CHECK(tabArea < nodeArea); // the tab wins: it stays the only affordance at zero crossfade
|
||||
|
||||
// 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
|
||||
|
||||
constexpr std::int64_t nodeSide = 2 * kCurveNodeGrabRadius + 1;
|
||||
constexpr std::int64_t nodeArea = nodeSide * nodeSide; // 169, fixed
|
||||
const std::int64_t markerArea =
|
||||
static_cast<std::int64_t>(2 * kMarkerGrabWidth + 1) * a.height; // 11 * band height
|
||||
CHECK(nodeArea < markerArea); // the node wins the shared pixel
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// --- Per-lane envelope content -------------------------------------------------
|
||||
|
||||
static void testAsymmetricStereoLanesCarryDifferentContent() {
|
||||
@@ -557,9 +459,6 @@ int main() {
|
||||
testMarkerHandleClipsIntoTheArea();
|
||||
testMarkerHandleOnDegenerateAreas();
|
||||
testStartMarkerSharesTheHandleStripWhenItSitsAtTheFadeEdge();
|
||||
testFreshRampDownEndpointBeatsTheStartMarkerAtFrameZero();
|
||||
testCrossfadeTabBeatsAContourNodeNearItsTopStrip();
|
||||
testContourNodeBeatsALoopMarkerAtTheirSharedPixelButNotElsewhere();
|
||||
|
||||
testAsymmetricStereoLanesCarryDifferentContent();
|
||||
testLaneEnvelopeRejectsOutOfRangeLane();
|
||||
|
||||
Reference in New Issue
Block a user