Type-enforce the waveform overlay contract, narrow waveformLanes to LaneSplit, fix laneCount/cache/path-fallback bugs
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
// stacked height, grabs reaching the lower lane); laneEnvelope (per-lane channel split).
|
||||
|
||||
#include "../src/core/instrument/ui/waveform_view.h"
|
||||
#include "../src/core/instrument/ui/sample_bands.h" // kWaveformMinHeight, kLaneGap
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
@@ -25,6 +26,10 @@ static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
// frameToX/markerAtPoint/resolveDragFrame take the overlay type, not a bare Rect (the
|
||||
// distinct-type enforcement in editor_geometry.h) — this wraps a plain test Rect for them.
|
||||
static OverlayArea overlayOf(const Rect& r) { return OverlayArea{r}; }
|
||||
|
||||
// A comfortable waveform area: 1000px wide, offset so left != 0 (catches origin bugs).
|
||||
static Rect wideArea() { return Rect::ltrb(20, 10, 1020, 90); } // width 1000
|
||||
|
||||
@@ -32,22 +37,22 @@ static Rect wideArea() { return Rect::ltrb(20, 10, 1020, 90); } // width 1000
|
||||
|
||||
static void testFrameToXEndpoints() {
|
||||
const Rect a = wideArea();
|
||||
CHECK(frameToX(a, 1000, 0) == a.x); // frame 0 -> left edge
|
||||
CHECK(frameToX(a, 1000, 1000) == a.right()); // frameCount -> right edge
|
||||
CHECK(frameToX(a, 1000, 500) == a.x + 500); // midpoint (1:1 here)
|
||||
CHECK(frameToX(overlayOf(a), 1000, 0) == a.x); // frame 0 -> left edge
|
||||
CHECK(frameToX(overlayOf(a), 1000, 1000) == a.right()); // frameCount -> right edge
|
||||
CHECK(frameToX(overlayOf(a), 1000, 500) == a.x + 500); // midpoint (1:1 here)
|
||||
}
|
||||
|
||||
static void testFrameToXClampsOutOfRange() {
|
||||
const Rect a = wideArea();
|
||||
CHECK(frameToX(a, 1000, -50) == a.x); // below 0 pins left
|
||||
CHECK(frameToX(a, 1000, 5000) == a.right()); // above count pins right
|
||||
CHECK(frameToX(overlayOf(a), 1000, -50) == a.x); // below 0 pins left
|
||||
CHECK(frameToX(overlayOf(a), 1000, 5000) == a.right()); // above count pins right
|
||||
}
|
||||
|
||||
static void testFrameToXDegenerate() {
|
||||
const Rect a = wideArea();
|
||||
CHECK(frameToX(a, 0, 100) == a.x); // no frames -> left
|
||||
CHECK(frameToX(overlayOf(a), 0, 100) == a.x); // no frames -> left
|
||||
const Rect z = Rect::ltrb(5, 5, 5, 45); // zero width
|
||||
CHECK(frameToX(z, 1000, 500) == z.x);
|
||||
CHECK(frameToX(overlayOf(z), 1000, 500) == z.x);
|
||||
}
|
||||
|
||||
static void testXToFrameInverse() {
|
||||
@@ -69,7 +74,7 @@ static void testFrameToXRoundTrip() {
|
||||
// xToFrame should land within a couple frames (rounding both directions).
|
||||
const Rect a = Rect::ltrb(0, 0, 800, 60);
|
||||
for (std::int64_t f = 0; f <= 2000; f += 137) {
|
||||
const int x = frameToX(a, 2000, f);
|
||||
const int x = frameToX(overlayOf(a), 2000, f);
|
||||
const std::int64_t back = xToFrame(a, 2000, x);
|
||||
CHECK(back >= f - 3 && back <= f + 3);
|
||||
}
|
||||
@@ -79,75 +84,82 @@ static void testFrameToXRoundTrip() {
|
||||
|
||||
static void testMarkerAtPointGrabsWithinBand() {
|
||||
const Rect a = wideArea();
|
||||
const OverlayArea ov = overlayOf(a);
|
||||
// Markers at frames 100, 500, 900 -> x = left+100, left+500, left+900.
|
||||
const std::int64_t frames[3] = {100, 500, 900};
|
||||
const int midY = a.y + a.height / 2;
|
||||
CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 100, midY) == 0);
|
||||
CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 500, midY) == 1);
|
||||
CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 900, midY) == 2);
|
||||
CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 100, midY) == 0);
|
||||
CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 500, midY) == 1);
|
||||
CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 900, midY) == 2);
|
||||
// Within the grab band on either side of the line.
|
||||
CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 500 + kMarkerGrabWidth, midY) == 1);
|
||||
CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 500 - kMarkerGrabWidth, midY) == 1);
|
||||
CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 500 + kMarkerGrabWidth, midY) == 1);
|
||||
CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 500 - kMarkerGrabWidth, midY) == 1);
|
||||
}
|
||||
|
||||
static void testMarkerAtPointMissesBetween() {
|
||||
const Rect a = wideArea();
|
||||
const OverlayArea ov = overlayOf(a);
|
||||
const std::int64_t frames[3] = {100, 500, 900};
|
||||
const int midY = a.y + a.height / 2;
|
||||
// Well away from any marker line.
|
||||
CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 300, midY) == -1);
|
||||
CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 300, midY) == -1);
|
||||
// Off the area vertically.
|
||||
CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 500, a.y - 5) == -1);
|
||||
CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 500, a.y - 5) == -1);
|
||||
}
|
||||
|
||||
static void testMarkerAtPointFirstMatchOnOverlap() {
|
||||
const Rect a = wideArea();
|
||||
const OverlayArea ov = overlayOf(a);
|
||||
// Two markers at the same frame -> first in order wins.
|
||||
const std::int64_t frames[2] = {400, 400};
|
||||
const int midY = a.y + a.height / 2;
|
||||
CHECK(markerAtPoint(a, 1000, frames, 2, a.x + 400, midY) == 0);
|
||||
CHECK(markerAtPoint(ov, 1000, frames, 2, a.x + 400, midY) == 0);
|
||||
}
|
||||
|
||||
static void testMarkerAtPointRejectsNullEmpty() {
|
||||
const Rect a = wideArea();
|
||||
const OverlayArea ov = overlayOf(a);
|
||||
const int midY = a.y + a.height / 2;
|
||||
CHECK(markerAtPoint(a, 1000, nullptr, 3, a.x + 100, midY) == -1);
|
||||
CHECK(markerAtPoint(ov, 1000, nullptr, 3, a.x + 100, midY) == -1);
|
||||
const std::int64_t frames[1] = {100};
|
||||
CHECK(markerAtPoint(a, 1000, frames, 0, a.x + 100, midY) == -1);
|
||||
CHECK(markerAtPoint(ov, 1000, frames, 0, a.x + 100, midY) == -1);
|
||||
}
|
||||
|
||||
// --- resolveDragFrame ---------------------------------------------------------
|
||||
|
||||
static void testResolveDragFrameShift() {
|
||||
const Rect a = wideArea(); // 1:1 (1000px / 1000 frames)
|
||||
CHECK(resolveDragFrame(a, 1000, 300, 0) == 300); // zero delta -> unchanged
|
||||
CHECK(resolveDragFrame(a, 1000, 300, 100) == 400); // +100px -> +100 frames
|
||||
CHECK(resolveDragFrame(a, 1000, 300, -50) == 250); // -50px -> -50 frames
|
||||
const OverlayArea ov = overlayOf(a);
|
||||
CHECK(resolveDragFrame(ov, 1000, 300, 0) == 300); // zero delta -> unchanged
|
||||
CHECK(resolveDragFrame(ov, 1000, 300, 100) == 400); // +100px -> +100 frames
|
||||
CHECK(resolveDragFrame(ov, 1000, 300, -50) == 250); // -50px -> -50 frames
|
||||
}
|
||||
|
||||
static void testResolveDragFrameClamps() {
|
||||
const Rect a = wideArea();
|
||||
CHECK(resolveDragFrame(a, 1000, 50, -500) == 0); // clamp low
|
||||
CHECK(resolveDragFrame(a, 1000, 950, 500) == 1000); // clamp high (== frameCount)
|
||||
const OverlayArea ov = overlayOf(a);
|
||||
CHECK(resolveDragFrame(ov, 1000, 50, -500) == 0); // clamp low
|
||||
CHECK(resolveDragFrame(ov, 1000, 950, 500) == 1000); // clamp high (== frameCount)
|
||||
}
|
||||
|
||||
static void testResolveDragFrameRounds() {
|
||||
// 500px area over 1000 frames -> 2 frames/px. A +3px drag -> round(6.0)=6; the rounding is
|
||||
// at the frame centre. Use a scale where a fractional result appears.
|
||||
const Rect a = Rect::ltrb(0, 0, 300, 60); // 1000 frames / 300px = 3.33 frames/px
|
||||
const OverlayArea ov = overlayOf(Rect::ltrb(0, 0, 300, 60)); // 1000 frames / 300px = 3.33 frames/px
|
||||
// +3px -> 3*1000/300 = 10.0 -> 10 frames.
|
||||
CHECK(resolveDragFrame(a, 1000, 100, 3) == 110);
|
||||
CHECK(resolveDragFrame(ov, 1000, 100, 3) == 110);
|
||||
// +1px -> 1000/300 = 3.33 -> rounds to 3.
|
||||
CHECK(resolveDragFrame(a, 1000, 100, 1) == 103);
|
||||
CHECK(resolveDragFrame(ov, 1000, 100, 1) == 103);
|
||||
}
|
||||
|
||||
static void testResolveDragFrameDegenerate() {
|
||||
const Rect z = Rect::ltrb(0, 0, 0, 60); // zero width
|
||||
CHECK(resolveDragFrame(z, 1000, 300, 100) == 300); // pinned to start
|
||||
CHECK(resolveDragFrame(overlayOf(z), 1000, 300, 100) == 300); // pinned to start
|
||||
const Rect a = wideArea();
|
||||
CHECK(resolveDragFrame(a, 0, 300, 100) == 0); // no frames -> clamp(start)=0
|
||||
const OverlayArea ov = overlayOf(a);
|
||||
CHECK(resolveDragFrame(ov, 0, 300, 100) == 0); // no frames -> clamp(start)=0
|
||||
// startFrame out of range is clamped first.
|
||||
CHECK(resolveDragFrame(a, 1000, 5000, 0) == 1000);
|
||||
CHECK(resolveDragFrame(ov, 1000, 5000, 0) == 1000);
|
||||
}
|
||||
|
||||
// --- nearestZeroCrossing ------------------------------------------------------
|
||||
@@ -239,12 +251,12 @@ static void testSurfaceOverlayIsFullStackedHeightInBothModes() {
|
||||
const WaveformSurface st = waveformSurface(b, /*stereoMode=*/true, 2);
|
||||
const WaveformSurface mo = waveformSurface(b, /*stereoMode=*/false, 2);
|
||||
// Stereo: ONE overlay rect spanning both lanes, not either lane.
|
||||
CHECK(st.overlay == b);
|
||||
CHECK(st.overlay.height == st.upper.height + kLaneGap + st.lower.height);
|
||||
CHECK(st.overlay != st.upper && st.overlay != st.lower);
|
||||
CHECK(st.overlay.rect == b);
|
||||
CHECK(st.overlay.rect.height == st.upper.height + kLaneGap + st.lower.height);
|
||||
CHECK(st.overlay.rect != st.upper && st.overlay.rect != st.lower);
|
||||
// Mono: the same rect, which is also the single lane.
|
||||
CHECK(mo.overlay == b);
|
||||
CHECK(mo.overlay == mo.upper);
|
||||
CHECK(mo.overlay.rect == b);
|
||||
CHECK(mo.overlay.rect == mo.upper);
|
||||
// The standalone accessor the hit-test paths use agrees with the resolved surface.
|
||||
CHECK(waveformOverlayArea(b) == st.overlay);
|
||||
CHECK(waveformOverlayArea(b) == mo.overlay);
|
||||
@@ -253,8 +265,8 @@ static void testSurfaceOverlayIsFullStackedHeightInBothModes() {
|
||||
static void testSurfaceDegenerateBandDrawsNothing() {
|
||||
const WaveformSurface s = waveformSurface(Rect{10, 10, 0, 0}, true, 2);
|
||||
CHECK(s.laneCount == 0);
|
||||
CHECK(s.upper.empty() && s.lower.empty() && s.overlay.empty());
|
||||
CHECK(waveformOverlayArea(Rect{10, 10, 0, 0}).empty());
|
||||
CHECK(s.upper.empty() && s.lower.empty() && s.overlay.rect.empty());
|
||||
CHECK(waveformOverlayArea(Rect{10, 10, 0, 0}).rect.empty());
|
||||
}
|
||||
|
||||
// --- Hit-testing across the stacked lanes -------------------------------------
|
||||
@@ -271,8 +283,10 @@ static void testMarkerGrabReachesTheLowerStereoLane() {
|
||||
CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx, upperY) == 0);
|
||||
CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx, lowerY) == 0);
|
||||
// A lower-lane grab hit-tested against the UPPER LANE would be lost — the miss this
|
||||
// contract exists to prevent.
|
||||
CHECK(markerAtPoint(s.upper, frames, markers, 1, mx, lowerY) == -1);
|
||||
// contract exists to prevent. (Explicit OverlayArea{} wrap: production code can't do
|
||||
// this by accident — markerAtPoint won't accept a bare lane Rect — but the geometry
|
||||
// claim still needs proving.)
|
||||
CHECK(markerAtPoint(overlayOf(s.upper), frames, markers, 1, mx, lowerY) == -1);
|
||||
// Off the marker's x is still a miss at either height.
|
||||
CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx + 40, lowerY) == -1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user