fix: close round-3 review findings — smallest-target-first, residue test fix, extraction

Waveform overlay now resolves node/tab/marker click collisions by target area instead of check order; residue test now uses a distinguishing fixture; Gate-unavailable-while-drawn logic extracted to one pure helper shared by resolvePlay and applyControl.
This commit is contained in:
2026-07-31 23:44:05 -04:00
parent c3d67bc3da
commit 757e1585d6
11 changed files with 255 additions and 87 deletions
+21 -12
View File
@@ -148,8 +148,11 @@ static void testHitTest() {
h = hitTestDeck(dl, voice.rowToggle.seg1.x + 1, voice.rowToggle.seg1.y + 1);
CHECK(h.kind == DeckHitKind::RowToggle && h.id == 104 && h.segment == 1);
// A reserve (id -1) yields no cell of its own, so every point of the knob row lands on a
// real control: no dead rect survives for a grab to fall into.
// A reserve (id -1) yields no cell of its own. This fixture's reserve divides its present
// cells evenly (5 slots / 3 present -> 240/3, no residue), so every point of the knob row
// lands on a real control: no dead rect survives for a grab to fall into. That does NOT
// generalize to an indivisible reserve — a residue leaves a few uncovered margin pixels by
// design (testIndivisibleResidueSplitsSymmetricallyAcrossBothEnds, below).
std::vector<DeckGroupDesc> trig;
trig.push_back({0, 78, {}, {100, 44}, {}, {20, 21, 22, -1, -1}, {}});
const DeckLayout tl = layoutDeck(trig, 0, 0, 824);
@@ -220,26 +223,32 @@ static void testReservedCellWidthGoesToTheCellsPresent() {
}
// 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" — a 7-slot reserve with 5 present
// (336/5, remainder 1) does, and pins the residue split across BOTH ends rather than only
// the leading one.
static void testIndivisibleResidueLandsInSymmetricEndMargins() {
const DeckGroupDesc g{0, 78, {}, {100, 44}, {}, {20, 21, 22, 23, 24, -1, -1}, {}};
// 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.
static void testIndivisibleResidueSplitsSymmetricallyAcrossBothEnds() {
const DeckGroupDesc g{0, 78, {}, {100, 44}, {}, {20, 21, 22, 23, 24, -1, -1, -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);
const int run = 7 * kDeckCellW;
const int run = 8 * kDeckCellW;
const int present = 5;
const int cellW = run / present; // 67: the same integer division the layout uses
const int expectedResidue = run - cellW * present; // 1: the case the even-dividing faces can't reach
CHECK(expectedResidue > 0);
const int cellW = run / present; // 76: the same integer division the layout uses
const int expectedResidue = run - cellW * present; // 4
CHECK(expectedResidue == 4);
const int covered = lay.cells.back().cell.right() - lay.cells.front().cell.x;
CHECK(run - covered == expectedResidue);
const int leadPad = lay.cells.front().cell.x - (lay.box.x + kDeckGroupPadX);
const int trailPad = (lay.box.right() - kDeckGroupPadX) - lay.cells.back().cell.right();
// Hard literals, not just the formula: this is the case that actually distinguishes
// symmetric (2/2) from trailing-only (0/4) — see the comment above.
CHECK(leadPad == 2);
CHECK(trailPad == 2);
CHECK(leadPad == expectedResidue / 2);
CHECK(trailPad == expectedResidue - leadPad); // both ends share it, not one absorbing it
}
@@ -337,7 +346,7 @@ int main() {
testGroupInnerGeometry();
testHitTest();
testReservedCellWidthGoesToTheCellsPresent();
testIndivisibleResidueLandsInSymmetricEndMargins();
testIndivisibleResidueSplitsSymmetricallyAcrossBothEnds();
testCaptionRadioGeometryAndHit();
testInnerDialHit();
testCaptionToggle2();