Files
reasampler/tests/test_component_geometry.cpp
T
daniel 1e645adcef fix(draw_kit): reclaim 4x waveform oversample; relocate waveformColumnCount to component_geometry
kWaveformOversample set to 1 (was 4) — overbinning produced byte-identical pixels because columnMinMax exact partition already makes the draw gap-free. Comments credit columnMinMax, not oversampling.
2026-07-27 19:37:53 -04:00

215 lines
9.6 KiB
C++

// Standalone tests for reasampler::component_geometry — no REAPER, no LICE, no framework.
// Same fast assert loop as the sibling pure tests (prune_button / mode_switch).
//
// Covers (brief §L1 point 2 + §test cases): button box inset + graceful suppression; slider
// track/filled/handle geometry for representative values incl. endpoints, value->px inverse,
// too-small/degenerate suppression; list-row rect for representative indices, partial last
// row, hover hit-test returns the right row and "no hit" outside/past the last row; and the
// shared half-open box hit-test agrees with layout (no double-claimed pixel).
#include "../src/component_geometry.h"
#include <cstdio>
using namespace reasampler;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
// --- hitTestBox (the shared primitive) ---------------------------------------
static void testHitTestBoxHalfOpen() {
KitBox b{10, 20, 30, 40};
CHECK(hitTestBox(10, 20, b)); // top-left inclusive
CHECK(hitTestBox(39, 59, b)); // bottom-right inclusive (x+w-1, y+h-1)
CHECK(!hitTestBox(40, 20, b)); // right edge excluded
CHECK(!hitTestBox(10, 60, b)); // bottom edge excluded
CHECK(!hitTestBox(9, 20, b)); // just left
CHECK(!hitTestBox(10, 19, b)); // just above
KitBox empty{};
CHECK(!hitTestBox(0, 0, empty)); // empty claims nothing
}
// --- Button box --------------------------------------------------------------
static void testButtonBoxInset() {
KitBox cell{0, 0, 100, 40};
const KitButtonBox b = computeButtonBox(cell, 4);
CHECK(!b.box.empty());
CHECK((b.box == KitBox{4, 4, 92, 32}));
}
static void testButtonBoxZeroPadding() {
KitBox cell{5, 6, 20, 10};
const KitButtonBox b = computeButtonBox(cell, 0);
CHECK((b.box == cell)); // no inset -> the whole cell
}
static void testButtonBoxNegativePaddingTreatedAsZero() {
KitBox cell{5, 6, 20, 10};
CHECK((computeButtonBox(cell, -8).box == cell));
}
static void testButtonBoxSuppressedWhenPaddingCollapses() {
KitBox cell{0, 0, 10, 10};
CHECK(computeButtonBox(cell, 5).box.empty()); // 10 - 2*5 = 0 width -> suppressed
CHECK(computeButtonBox(cell, 6).box.empty()); // negative -> suppressed
CHECK(computeButtonBox(KitBox{}, 2).box.empty()); // degenerate cell -> suppressed
}
// --- Slider ------------------------------------------------------------------
// control 200x20, handle 12, track thickness 4. half = 6. track.x = 6, track.width =
// 200-12 = 188, track.y = (20-4)/2 = 8. At value 0 the handle centre is track.x=6 ->
// handle.x = 0; filled.width = 0. At value 1 the centre is 6+188=194 -> handle.x=188.
static void testSliderEndpoints() {
KitBox ctrl{0, 0, 200, 20};
const SliderGeometry lo = computeSlider(ctrl, 0.0, 12, 4);
CHECK(!lo.track.empty());
CHECK((lo.track == KitBox{6, 8, 188, 4}));
CHECK(lo.handle.x == 0); // flush left
CHECK(lo.filled.width == 0); // nothing filled at 0
const SliderGeometry hi = computeSlider(ctrl, 1.0, 12, 4);
CHECK(hi.handle.x == 188); // flush right (200-12)
CHECK(hi.filled.width == 188); // fully filled at 1
}
static void testSliderMidpoint() {
KitBox ctrl{0, 0, 200, 20};
const SliderGeometry m = computeSlider(ctrl, 0.5, 12, 4);
// centre = 6 + round(0.5*188) = 6 + 94 = 100; handle.x = 100 - 6 = 94.
CHECK(m.handle.x == 94);
CHECK(m.filled.width == 94);
}
static void testSliderClampsValue() {
KitBox ctrl{0, 0, 200, 20};
CHECK((computeSlider(ctrl, -1.0, 12, 4).handle.x == computeSlider(ctrl, 0.0, 12, 4).handle.x));
CHECK((computeSlider(ctrl, 5.0, 12, 4).handle.x == computeSlider(ctrl, 1.0, 12, 4).handle.x));
}
static void testSliderSuppressedWhenTooSmallOrDegenerate() {
CHECK(computeSlider(KitBox{0, 0, 8, 20}, 0.5, 12, 4).track.empty()); // width < handle
CHECK(computeSlider(KitBox{0, 0, 200, 8}, 0.5, 12, 4).track.empty()); // height < handle
CHECK(computeSlider(KitBox{}, 0.5, 12, 4).track.empty()); // degenerate
CHECK(computeSlider(KitBox{0, 0, 200, 20}, 0.5, 0, 4).track.empty()); // no handle
CHECK(computeSlider(KitBox{0, 0, 200, 20}, 0.5, 12, 0).track.empty()); // no track
}
// value->px is the inverse of the handle placement (a track jump).
static void testSliderValueAtInverse() {
KitBox ctrl{0, 0, 200, 20};
CHECK(sliderValueAt(6, ctrl, 12) == 0.0); // at/left of track start
CHECK(sliderValueAt(0, ctrl, 12) == 0.0); // left of the control -> 0
CHECK(sliderValueAt(194, ctrl, 12) == 1.0); // at track end -> 1
CHECK(sliderValueAt(300, ctrl, 12) == 1.0); // past the end -> clamped
const double mid = sliderValueAt(100, ctrl, 12); // (100-6)/188
CHECK(mid > 0.49 && mid < 0.51);
CHECK(sliderValueAt(50, KitBox{0, 0, 8, 20}, 12) == 0.0); // too small -> 0
}
// --- List row ----------------------------------------------------------------
static void testListRowStacking() {
KitBox list{0, 100, 220, 90}; // 3 full rows of 30 fit
CHECK((computeListRow(list, 0, 30).box == KitBox{0, 100, 220, 30}));
CHECK((computeListRow(list, 1, 30).box == KitBox{0, 130, 220, 30}));
CHECK((computeListRow(list, 2, 30).box == KitBox{0, 160, 220, 30}));
CHECK(computeListRow(list, 0, 30).index == 0);
CHECK(computeListRow(list, 2, 30).index == 2);
}
// A row that starts inside the list but overhangs the bottom keeps full height (caller
// clips the draw); a row starting AT/BELOW the bottom is suppressed.
static void testListRowPartialAndClipped() {
KitBox list{0, 0, 100, 50}; // rows of 30: row 0 [0,30), row 1 [30,60) overhangs
const ListRowBox partial = computeListRow(list, 1, 30);
CHECK(!partial.box.empty());
CHECK(partial.box.y == 30);
CHECK(partial.box.height == 30); // full height; caller clips
CHECK(computeListRow(list, 2, 30).box.empty()); // starts at y=60 >= bottom -> none
}
static void testListRowDegenerate() {
CHECK(computeListRow(KitBox{}, 0, 30).box.empty());
CHECK(computeListRow(KitBox{0, 0, 100, 90}, -1, 30).box.empty());
CHECK(computeListRow(KitBox{0, 0, 100, 90}, 0, 0).box.empty());
}
static void testListRowHitTest() {
KitBox list{0, 100, 220, 90}; // 3 rows of 30, rowCount = 3
CHECK(hitTestListRow(10, 100, list, 30, 3) == 0); // top of row 0
CHECK(hitTestListRow(10, 129, list, 30, 3) == 0); // bottom of row 0 (inclusive)
CHECK(hitTestListRow(10, 130, list, 30, 3) == 1); // top of row 1
CHECK(hitTestListRow(10, 189, list, 30, 3) == 2); // last pixel of row 2
// Misses.
CHECK(hitTestListRow(10, 99, list, 30, 3) == -1); // above the list
CHECK(hitTestListRow(10, 190, list, 30, 3) == -1); // below the list band
CHECK(hitTestListRow(-1, 100, list, 30, 3) == -1); // left of the list
CHECK(hitTestListRow(220, 100, list, 30, 3) == -1); // right edge excluded
}
// rowCount bounds the hit: a taller list with fewer rows than fit reports the empty tail
// as a miss (no phantom row past the data).
static void testListRowHitTestBoundedByCount() {
KitBox list{0, 0, 100, 200}; // room for 6 rows of 30, but only 2 exist
CHECK(hitTestListRow(10, 10, list, 30, 2) == 0);
CHECK(hitTestListRow(10, 40, list, 30, 2) == 1);
CHECK(hitTestListRow(10, 70, list, 30, 2) == -1); // row 2 is empty tail -> miss
CHECK(hitTestListRow(10, 10, list, 30, 0) == -1); // zero rows -> always miss
}
// Draw/hit-test agreement: every pixel inside a computed row hit-tests to that row.
static void testListRowLayoutHitAgreement() {
KitBox list{3, 7, 97, 120};
const int rh = 24, rowCount = 4;
for (int idx = 0; idx < rowCount; ++idx) {
const ListRowBox r = computeListRow(list, idx, rh);
if (r.box.empty()) continue;
for (int py = r.box.y; py < r.box.y + r.box.height &&
py < list.y + list.height; ++py)
CHECK(hitTestListRow(list.x + 1, py, list, rh, rowCount) == idx);
}
}
// --- waveformColumnCount -----------------------------------------------------
static void testWaveformColumnCount() {
// Normal box: width minus the two 2px side insets.
CHECK(waveformColumnCount(KitBox{0, 0, 100, 40}) == 96);
CHECK(waveformColumnCount(KitBox{10, 5, 50, 20}) == 46);
// Minimum: a 5-wide box yields exactly 1 drawable column.
CHECK(waveformColumnCount(KitBox{0, 0, 5, 10}) == 1);
// Too narrow to have any drawable columns: 4 or fewer columns returns 0.
CHECK(waveformColumnCount(KitBox{0, 0, 4, 10}) == 0);
CHECK(waveformColumnCount(KitBox{0, 0, 1, 10}) == 0);
// Degenerate (empty) box returns 0 — graceful suppression.
CHECK(waveformColumnCount(KitBox{}) == 0);
CHECK(waveformColumnCount(KitBox{0, 0, 0, 40}) == 0);
}
int main() {
testHitTestBoxHalfOpen();
testButtonBoxInset();
testButtonBoxZeroPadding();
testButtonBoxNegativePaddingTreatedAsZero();
testButtonBoxSuppressedWhenPaddingCollapses();
testSliderEndpoints();
testSliderMidpoint();
testSliderClampsValue();
testSliderSuppressedWhenTooSmallOrDegenerate();
testSliderValueAtInverse();
testListRowStacking();
testListRowPartialAndClipped();
testListRowDegenerate();
testListRowHitTest();
testListRowHitTestBoundedByCount();
testListRowLayoutHitAgreement();
testWaveformColumnCount();
if (g_fail == 0) std::printf("component_geometry: all tests passed\n");
else std::printf("component_geometry: %d CHECK(s) FAILED\n", g_fail);
return g_fail == 0 ? 0 : 1;
}