feat: legible ReaSampler 9000 editor — bigger knobs, ms time constants, per-ring double-click reset, and an antialiased draw pass
This commit is contained in:
+63
-11
@@ -9,6 +9,8 @@
|
||||
// always places; deckHeight consistency with deckRowCount.
|
||||
// * hit-test — knob cell hit (whole cell), toggle segment 0/1 boundaries, fence padding
|
||||
// misses, outside-deck misses.
|
||||
// * knob-FACE hit-test — the reset resolve against the drawn circles: inner disc, outer ring,
|
||||
// both exclusive boundaries, and the points where it deliberately disagrees with the cell.
|
||||
|
||||
#include "../src/core/instrument/ui/knob_deck.h"
|
||||
|
||||
@@ -183,7 +185,7 @@ static void testHitTest() {
|
||||
// not cover is smaller than one pixel per cell.
|
||||
static void testReservedCellWidthGoesToTheCellsPresent() {
|
||||
const DeckGroupDesc full{0, 78, {}, {100, 44}, {}, {20, 21, 22, 23, 24}, {}};
|
||||
// Three, four, and a lone cell against the same five-slot reserve — 240/3, 240/4, 240/1.
|
||||
// Three, four, and a lone cell against the same five-slot reserve.
|
||||
const std::vector<std::vector<int>> faces = {
|
||||
{20, 21, 22, -1, -1}, {20, 21, 22, 23, -1}, {20, -1, -1, -1, -1}};
|
||||
for (const std::vector<int>& ids : faces) {
|
||||
@@ -202,7 +204,13 @@ static void testReservedCellWidthGoesToTheCellsPresent() {
|
||||
const DeckCellLayout& c = lay.cells[static_cast<std::size_t>(i)];
|
||||
CHECK(c.cell.width == lay.cells[0].cell.width); // uniform
|
||||
CHECK(c.knob.width == kDeckKnobSize); // the dial itself is fixed
|
||||
CHECK(c.knob.x - c.cell.x == c.cell.right() - c.knob.right());
|
||||
// Centred as exactly as integers allow: a cell whose spare width is odd cannot
|
||||
// split it evenly, and the layout's integer division gives the odd pixel to the
|
||||
// RIGHT margin. Pinned as a directional identity rather than a tolerance, so a
|
||||
// future off-by-one on the other side would still fail here.
|
||||
const int leftGap = c.knob.x - c.cell.x;
|
||||
const int rightGap = c.cell.right() - c.knob.right();
|
||||
CHECK(rightGap - leftGap == (c.cell.width - kDeckKnobSize) % 2);
|
||||
if (i > 0) CHECK(c.cell.x == lay.cells[static_cast<std::size_t>(i - 1)].cell.right());
|
||||
}
|
||||
// Uncovered run is the indivisible residue only, split evenly at the two ends.
|
||||
@@ -222,21 +230,21 @@ static void testReservedCellWidthGoesToTheCellsPresent() {
|
||||
layoutDeck(b, 0, 0, 824).groups[0].rowToggle.seg0);
|
||||
}
|
||||
|
||||
// The three faces above (240/3, 240/4, 240/1) all divide their run evenly, so none of them
|
||||
// actually exercises "residue in symmetric end margins". An 8-slot reserve with 5 present
|
||||
// (384/5 = 76 r4) does: residue 4 is the smallest case that can tell a symmetric split (2/2)
|
||||
// apart from a trailing-only one (0/4) — a residue of 1 (0/1 vs 1/0... i.e. 0/1) can't, since
|
||||
// leadPad = residue/2 rounds to 0 either way, which is exactly why this seam's earlier test
|
||||
// passed without pinning the rule it was named for.
|
||||
// The three faces above all divide their run evenly, so none of them actually exercises
|
||||
// "residue in symmetric end margins". An 8-slot reserve with 7 present (480/7 = 68 r4) does:
|
||||
// residue 4 is the smallest case that can tell a symmetric split (2/2) apart from a
|
||||
// trailing-only one (0/4) — a residue of 1 can't, since leadPad = residue/2 rounds to 0 either
|
||||
// way, which is exactly why this seam's earlier test passed without pinning the rule it was
|
||||
// named for.
|
||||
static void testIndivisibleResidueSplitsSymmetricallyAcrossBothEnds() {
|
||||
const DeckGroupDesc g{0, 78, {}, {100, 44}, {}, {20, 21, 22, 23, 24, -1, -1, -1}, {}};
|
||||
const DeckGroupDesc g{0, 78, {}, {100, 44}, {}, {20, 21, 22, 23, 24, 25, 26, -1}, {}};
|
||||
std::vector<DeckGroupDesc> gs{g};
|
||||
const DeckLayout dl = layoutDeck(gs, 0, 0, 824);
|
||||
const DeckGroupLayout& lay = dl.groups[0];
|
||||
CHECK(lay.cells.size() == 5);
|
||||
CHECK(lay.cells.size() == 7);
|
||||
|
||||
const int run = 8 * kDeckCellW;
|
||||
const int present = 5;
|
||||
const int present = 7;
|
||||
const int cellW = run / present; // 76: the same integer division the layout uses
|
||||
const int expectedResidue = run - cellW * present; // 4
|
||||
CHECK(expectedResidue == 4);
|
||||
@@ -331,6 +339,49 @@ static void testCaptionToggle2() {
|
||||
CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 302 && h.segment == 1);
|
||||
}
|
||||
|
||||
// The double-click RESET resolve. Unlike hitTestDeck's whole-cell grab, this one answers the
|
||||
// drawn circles: inner disc -> inner target, outer ring -> outer target, anything off the dial
|
||||
// (the label band, the cell margin, outside the deck) -> neither. Both boundaries are exclusive.
|
||||
static void testKnobFaceResolvesInnerRingOuterRingAndMisses() {
|
||||
const std::vector<DeckGroupDesc> g = shellLikeDeck();
|
||||
const DeckLayout dl = layoutDeck(g, 0, 0, 900);
|
||||
const DeckCellLayout& c = dl.groups[0].cells[0];
|
||||
const int cx = c.knob.x + c.knob.width / 2;
|
||||
const int cy = c.knob.y + c.knob.height / 2;
|
||||
const int rOuter = c.knob.width / 2;
|
||||
const int rInner = c.inner.width / 2;
|
||||
CHECK(rInner > 0 && rInner < rOuter);
|
||||
|
||||
// Dead centre is the inner target; just inside the inner radius still is.
|
||||
DeckFaceHit h = hitTestKnobFace(dl, cx, cy);
|
||||
CHECK(h.id == c.id && h.inner);
|
||||
h = hitTestKnobFace(dl, cx + rInner - 1, cy);
|
||||
CHECK(h.id == c.id && h.inner);
|
||||
// EXACTLY on the inner radius is the outer ring — the boundary belongs to neither disc.
|
||||
h = hitTestKnobFace(dl, cx + rInner, cy);
|
||||
CHECK(h.id == c.id && !h.inner);
|
||||
// Just inside the rim is still the outer ring...
|
||||
h = hitTestKnobFace(dl, cx + rOuter - 1, cy);
|
||||
CHECK(h.id == c.id && !h.inner);
|
||||
// ...and EXACTLY on the rim is a miss, by the same exclusive rule.
|
||||
h = hitTestKnobFace(dl, cx + rOuter, cy);
|
||||
CHECK(h.id == -1 && !h.inner);
|
||||
|
||||
// The cell corner is inside the CELL (hitTestDeck resolves it as a grab) but outside the
|
||||
// circle — the two resolves deliberately disagree there.
|
||||
CHECK(hitTestDeck(dl, c.cell.x + 1, c.cell.y + 1).kind == DeckHitKind::Knob);
|
||||
CHECK(hitTestKnobFace(dl, c.cell.x + 1, c.cell.y + 1).id == -1);
|
||||
// The label band under the knob: a grab anchor, never a reset target.
|
||||
CHECK(hitTestDeck(dl, c.label.x + 2, c.label.y + 2).kind == DeckHitKind::Knob);
|
||||
CHECK(hitTestKnobFace(dl, c.label.x + 2, c.label.y + 2).id == -1);
|
||||
// Off the deck entirely.
|
||||
CHECK(hitTestKnobFace(dl, -50, -50).id == -1);
|
||||
// A diagonal at 45 degrees inside the rim: proves the resolve is radial, not the inscribed
|
||||
// square a rect test would accept — this point is inside the knob RECT but outside the disc.
|
||||
const int diag = static_cast<int>(rOuter * 0.75) + 1; // dist ~ 1.06 * rOuter
|
||||
CHECK(hitTestKnobFace(dl, cx + diag, cy + diag).id == -1);
|
||||
}
|
||||
|
||||
static void testEmptyDeck() {
|
||||
const std::vector<DeckGroupDesc> none;
|
||||
CHECK(deckRowCount(none, 800) == 0);
|
||||
@@ -349,6 +400,7 @@ int main() {
|
||||
testIndivisibleResidueSplitsSymmetricallyAcrossBothEnds();
|
||||
testCaptionRadioGeometryAndHit();
|
||||
testInnerDialHit();
|
||||
testKnobFaceResolvesInnerRingOuterRingAndMisses();
|
||||
testCaptionToggle2();
|
||||
testEmptyDeck();
|
||||
if (g_fail) {
|
||||
|
||||
Reference in New Issue
Block a user