Fix stale post-widen deck-reflow figures (90/144/1190px) across docs and tests, and split test_deck_groups.cpp's width-budget fixtures into a new file.
This commit is contained in:
+58
-416
@@ -1,18 +1,16 @@
|
||||
// Standalone tests for reasampler::instrument::ui::deck_groups — no VST3, no REAPER, no
|
||||
// framework. knob_deck's own tests pin how a descriptor list LAYS OUT; these pin WHICH
|
||||
// descriptors the Sample face carries: the signal-flow group order (pitch -> filter -> amp),
|
||||
// the Filter group's contents, the VELOCITY group's exclusive ownership of the three curve
|
||||
// cells and its placement immediately left of VOICE, the wrapped deck height at the editor's
|
||||
// floor width and its fit inside the floor window, the pinned Gate group widths, the editor
|
||||
// floor derived from the deck's width budget and each group's categorical row,
|
||||
// that no face leaves slack where its dropped controls were and that a Gate/Spline/Gate round
|
||||
// trip restores the layout exactly, the hit-test reaching the new filter controls, and the
|
||||
// bipolar knob law's inverse pair. The commit-tier routing and the overlay-selection state
|
||||
// machine live in test_deck_groups_state.cpp — they touch no layout at all.
|
||||
// framework. Pins WHICH descriptors the Sample face carries and how they resolve to a layout:
|
||||
// the signal-flow group order (pitch -> filter -> amp), the Filter group's contents, the
|
||||
// VELOCITY group's exclusive ownership of the three curve cells and its placement immediately
|
||||
// left of VOICE, row membership, that no face leaves slack where its dropped controls were,
|
||||
// that a Gate/Spline/Gate round trip restores the layout exactly, the hit-test reaching the new
|
||||
// filter controls, and the bipolar knob law's inverse pair. The width-BUDGET fixtures (the
|
||||
// editor floor's derivation, the row/gutter arithmetic at the floor, MASTER's interior) live in
|
||||
// test_deck_groups_measured.cpp, which needs sample_bands/master_meter and this file does not.
|
||||
// The commit-tier routing and the overlay-selection state machine live in
|
||||
// test_deck_groups_state.cpp — they touch no layout at all.
|
||||
|
||||
#include "../src/core/instrument/ui/deck_groups.h"
|
||||
#include "../src/core/instrument/ui/master_meter.h" // kMeterColumnW: MASTER's reserve IS this
|
||||
#include "../src/core/instrument/ui/sample_bands.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
@@ -25,9 +23,14 @@ static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
// The editor's floor width, which is also its default (checkSizeConstraint clamps to it), less
|
||||
// the band allocator's kPad inset on each side.
|
||||
static constexpr int kAvailAtMinWidth = kEditorMinWidth - 2 * kPad;
|
||||
// A pad and an available width for exercising layoutDeck, kept independent of sample_bands.h —
|
||||
// this file pins what the deck IS, not the window-floor budget. kSampleAvail equals the real
|
||||
// floor's available width because it is derived the same way (block + gap + spanning deck);
|
||||
// that identity, and the window-fact constants (kPad, kEditorMinWidth) it derives from, are
|
||||
// test_deck_groups_measured.cpp's to own.
|
||||
static constexpr int kSamplePad = 8;
|
||||
static constexpr int kSampleAvail = kDeckRowBlockW + kDeckGroupGap + kDeckSpanningW;
|
||||
static constexpr int kSampleAvailWide = kSampleAvail + 200; // comfortably above the block
|
||||
|
||||
static int indexOfGroup(const std::vector<DeckGroupDesc>& g, int id) {
|
||||
for (std::size_t i = 0; i < g.size(); ++i) {
|
||||
@@ -99,7 +102,7 @@ static void testCurveTargetNamesEachCellsOwnDestination() {
|
||||
// treats them as knob cells, so the popup routing rides an ordinary Knob hit.
|
||||
static void testVelocityCellsHitTestWithinTheirGroup() {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
||||
const DeckLayout dl = layoutDeck(g, kPad, 40, kAvailAtMinWidth);
|
||||
const DeckLayout dl = layoutDeck(g, kSamplePad, 40, kSampleAvail);
|
||||
const DeckGroupLayout& v =
|
||||
dl.groups[static_cast<std::size_t>(indexOfGroup(g, kGroupVelocity))];
|
||||
CHECK(v.cells.size() == 3);
|
||||
@@ -257,8 +260,8 @@ static void testTheDeckIsTwoRowsPlusTheSpanningDeckByConstruction() {
|
||||
CHECK(deckHeight(g) == 2 * kDeckGroupH + kDeckRowGap);
|
||||
CHECK(deckHeight(g) == 216);
|
||||
|
||||
for (int avail : {kAvailAtMinWidth, kAvailAtMinWidth + 200, 4000}) {
|
||||
const DeckLayout dl = layoutDeck(g, kPad, 0, avail);
|
||||
for (int avail : {kSampleAvail, kSampleAvail + 200, 4000}) {
|
||||
const DeckLayout dl = layoutDeck(g, kSamplePad, 0, avail);
|
||||
CHECK(dl.rowCount == 2);
|
||||
CHECK(dl.height == 216);
|
||||
CHECK(dl.groups.size() == g.size());
|
||||
@@ -268,7 +271,7 @@ static void testTheDeckIsTwoRowsPlusTheSpanningDeckByConstruction() {
|
||||
if (row == DeckRow::Spanning) {
|
||||
CHECK(gl.box.y == 0);
|
||||
CHECK(gl.box.height == kDeckSpanningH);
|
||||
CHECK(gl.box.right() == kPad + avail); // right-anchored at every width
|
||||
CHECK(gl.box.right() == kSamplePad + avail); // right-anchored at every width
|
||||
} else {
|
||||
CHECK(gl.box.y == rowTops[row == DeckRow::Contour ? 1 : 0]);
|
||||
CHECK(gl.box.height == kDeckGroupH);
|
||||
@@ -278,54 +281,6 @@ static void testTheDeckIsTwoRowsPlusTheSpanningDeckByConstruction() {
|
||||
}
|
||||
}
|
||||
|
||||
// The guard the raised floor exists to provide: at the smallest window the host can produce,
|
||||
// the deck band still lands inside the client area AND the waveform still gets its two-lane
|
||||
// floor. Growing the deck past what the floor height can hold fails HERE instead of silently pushing
|
||||
// FILTER ENV / AMP / VOICE / MASTER off-screen, where there is no scroll to reach them.
|
||||
static void testDeckFitsInsideTheEnforcedMinimumWindow() {
|
||||
for (PlayMode mode : {PlayMode::Gate, PlayMode::Trigger}) {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(mode);
|
||||
const int h = deckHeight(g);
|
||||
const SampleBands b = computeSampleBands(kEditorMinWidth, kEditorMinHeight, h);
|
||||
CHECK(deckRowCount(g) == 2); // either face
|
||||
CHECK(b.decks.height == h);
|
||||
// The reflow's 112 px land in the waveform: at two rows the deck band is 216 and the
|
||||
// waveform 358, against 328/246 before. Pinned now that both are reached by
|
||||
// construction rather than by a pack outcome.
|
||||
CHECK(b.decks.height == 2 * kDeckGroupH + kDeckRowGap);
|
||||
CHECK(b.waveform.height == 358);
|
||||
// Bottom-anchored INSIDE the pad is the whole assertion: the degrade path pushes the
|
||||
// deck down until the waveform hits its floor, so any deck too tall to fit stops
|
||||
// landing on this exact line. A `<= kEditorMinHeight` bound would not catch it — the
|
||||
// degrade can still leave the deck ending at the window edge.
|
||||
CHECK(b.decks.bottom() == kEditorMinHeight - kPad);
|
||||
CHECK(b.waveform.height >= kWaveformMinHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// The floor is a DERIVED number, and this is the one place the derivation is written down —
|
||||
// sample_bands stays independent of knob_deck, so neither header can hold it. This fixture is
|
||||
// the only one that includes both.
|
||||
static void testTheEditorFloorIsDerivedFromTheDeckWidthBudget() {
|
||||
CHECK(kDeckRowBlockW + kDeckGroupGap + kDeckSpanningW + 2 * kPad == kEditorMinWidth);
|
||||
// The budget: what is left between the derived floor and the hard ceiling, and it is spent
|
||||
// once. A cell costs 60 of it.
|
||||
CHECK(kEditorCeilingWidth - kEditorMinWidth == 82);
|
||||
// 82 still buys one more deck cell (60), which is the only purchase the ledger promises —
|
||||
// the widen below spent 8 px of slack, not the layout's purchasing power.
|
||||
CHECK(kEditorCeilingWidth - kEditorMinWidth >= kDeckCellW);
|
||||
// The reflow's 112 px goes entirely to the waveform, so the height does not move.
|
||||
CHECK(kEditorMinHeight == 680);
|
||||
// 1190 + 8: the row block was widened 1020 -> 1028 to put the two rows' filter edges on
|
||||
// one pixel, which is the only reason the floor moved off Γ-W1-T4's number.
|
||||
CHECK(kEditorMinWidth == 1198);
|
||||
CHECK(kEditorMinWidth <= kEditorCeilingWidth);
|
||||
CHECK(kEditorMinHeight <= 720);
|
||||
// And the row block really is what the two rows justify inside — derived from the floor
|
||||
// and the spanning reserve, not restated.
|
||||
CHECK(kEditorMinWidth - 2 * kPad - kDeckSpanningW - kDeckGroupGap == kDeckRowBlockW);
|
||||
}
|
||||
|
||||
static void testEveryDeckGroupBelongsToExactlyOneRow() {
|
||||
CHECK(deckRowFor(kGroupPitch) == DeckRow::Sound);
|
||||
CHECK(deckRowFor(kGroupFilter) == DeckRow::Sound);
|
||||
@@ -351,210 +306,46 @@ static void testEveryDeckGroupBelongsToExactlyOneRow() {
|
||||
}
|
||||
}
|
||||
|
||||
// Both rows now fit their block, in BOTH play modes. Row 1's fit is the one this track closes:
|
||||
// it was 1030, +42 from PITCH/RATE's third cell and −92 from FILTER's Band|Notch caption move
|
||||
// take it to 980. Row 2's 876 is mode-stable because FILTER ENV's and AMP's reserve slots hold
|
||||
// them at 312 in Trigger too — asserted here rather than assumed.
|
||||
static void testBothRowsAndTheSpanningDeckFitTheBudget() {
|
||||
for (PlayMode mode : {PlayMode::Gate, PlayMode::Trigger}) {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(mode);
|
||||
int width[3] = {0, 0, 0};
|
||||
int count[3] = {0, 0, 0};
|
||||
for (const DeckGroupDesc& d : g) {
|
||||
const int r = static_cast<int>(deckRowFor(static_cast<DeckGroupId>(d.id)));
|
||||
width[r] += deckGroupWidth(d);
|
||||
++count[r];
|
||||
}
|
||||
const int sound = static_cast<int>(DeckRow::Sound);
|
||||
const int contour = static_cast<int>(DeckRow::Contour);
|
||||
const int spanning = static_cast<int>(DeckRow::Spanning);
|
||||
|
||||
CHECK(count[sound] == 4);
|
||||
CHECK(width[sound] == 980); // 192 + 432 + 192 + 164
|
||||
CHECK(count[contour] == 3);
|
||||
CHECK(width[contour] == 876); // 252 + 312 + 312
|
||||
CHECK(count[spanning] == 1);
|
||||
CHECK(width[spanning] == kDeckSpanningW); // 142 exactly — the reserve is now spent
|
||||
|
||||
for (int r : {sound, contour}) {
|
||||
CHECK(width[r] <= kDeckRowBlockW);
|
||||
// Slack enough that no gutter in the row falls under the minimum.
|
||||
CHECK(kDeckRowBlockW - width[r] >= (count[r] - 1) * kDeckGroupGap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The gutters the justification law produces at the floor, and the alignment they buy. The
|
||||
// At the 1028 block the justification law makes the tie-line exact by arithmetic rather than
|
||||
// by a special rule: row 1's slack is 48 over three gutters (16 each, no residue) and row 2's
|
||||
// is 152 over two (76 each), which lands both filter edges on 640. Only two of the three
|
||||
// properties §1.3 once claimed can hold at once — a smallest gutter of exactly kDeckGroupGap
|
||||
// needs a 1016 block — and 12 is a floor, not a target, so 16 satisfies the real rule.
|
||||
static void testGutterArithmeticAndTheFilterTieLineAtTheFloor() {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
||||
const DeckLayout dl = layoutDeck(g, kPad, 0, kAvailAtMinWidth);
|
||||
|
||||
const auto box = [&](int id) {
|
||||
return dl.groups[static_cast<std::size_t>(indexOfGroup(g, id))].box;
|
||||
};
|
||||
// Row 1: flush left, flush right on the block, and three EQUAL gutters — 48 divides by 3
|
||||
// with no residue, so no gutter carries a leftover pixel.
|
||||
CHECK(box(kGroupPitch).x == kPad);
|
||||
CHECK(box(kGroupFilter).x - box(kGroupPitch).right() == 16);
|
||||
CHECK(box(kGroupVelocity).x - box(kGroupFilter).right() == 16);
|
||||
CHECK(box(kGroupVoice).x - box(kGroupVelocity).right() == 16);
|
||||
CHECK(box(kGroupVoice).right() == kPad + kDeckRowBlockW);
|
||||
|
||||
// Row 2: flush left, flush right, two gutters exactly equal.
|
||||
CHECK(box(kGroupPitchEnv).x == kPad);
|
||||
CHECK(box(kGroupFilterEnv).x - box(kGroupPitchEnv).right() == 76);
|
||||
CHECK(box(kGroupAmpEnv).x - box(kGroupFilterEnv).right() == 76);
|
||||
CHECK(box(kGroupAmpEnv).right() == kPad + kDeckRowBlockW);
|
||||
|
||||
// The tie-line, block-relative: both filter edges on ONE pixel, which is what the widen
|
||||
// bought. Pinned as an identity too, so a group-width change cannot pass by moving both.
|
||||
CHECK(box(kGroupFilterEnv).right() - kPad == 640);
|
||||
CHECK(box(kGroupFilter).right() - kPad == 640);
|
||||
CHECK(box(kGroupFilter).right() == box(kGroupFilterEnv).right());
|
||||
|
||||
// MASTER is right-anchored outside the block, one kDeckGroupGap clear of it.
|
||||
CHECK(box(kGroupMaster).x - box(kGroupVoice).right() == kDeckGroupGap);
|
||||
CHECK(box(kGroupMaster).right() == kPad + kAvailAtMinWidth);
|
||||
}
|
||||
|
||||
// No gutter is ever narrower than kDeckGroupGap at or above the floor, and both rows stay
|
||||
// flush at every width — the property the exact-at-the-floor numbers above are one point of.
|
||||
// Above the floor the tie-line DRIFTS, which is accepted and deliberate (§1.3): row 1 divides
|
||||
// its slack over three gutters and row 2 over two, so row 2's filter edge pulls right past
|
||||
// row 1's and the gap widens monotonically. Encoded as EXPECTED, not as a failure.
|
||||
//
|
||||
// Checked per ROW (tracking the last-seen box in each of the two categorical rows while
|
||||
// walking dl.groups in deck order), not just deck-order neighbours: two same-row groups can
|
||||
// sit apart in deck order with a different-row group between them, and a deck-order-only
|
||||
// check would silently skip that gutter.
|
||||
static void testGuttersHoldTheirMinimumAndTheTieLineDriftsAboveTheFloor() {
|
||||
for (PlayMode mode : {PlayMode::Gate, PlayMode::Trigger}) {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(mode);
|
||||
int lastDrift = 1 << 20; // sentinel above any real drift
|
||||
for (int avail = kAvailAtMinWidth; avail <= kAvailAtMinWidth + 600; avail += 37) {
|
||||
const DeckLayout dl = layoutDeck(g, kPad, 0, avail);
|
||||
const DeckGroupLayout* prevInRow[2] = {nullptr, nullptr};
|
||||
for (const DeckGroupLayout& gl : dl.groups) {
|
||||
const DeckRow row = deckRowFor(static_cast<DeckGroupId>(gl.id));
|
||||
if (row == DeckRow::Spanning) continue;
|
||||
const int r = row == DeckRow::Contour ? 1 : 0;
|
||||
if (prevInRow[r]) {
|
||||
CHECK(gl.box.x - prevInRow[r]->box.right() >= kDeckGroupGap);
|
||||
// The gap fix as a property of the shipped descriptors, not a picture: whichever face a
|
||||
// mode-dependent group shows, its knob row still spans the group's whole reserved run. The
|
||||
// Trigger faces drop Sustain and Release and get wider cells for it — never a hole where the
|
||||
// dropped control was. What the run does not cover is the indivisible residue alone, strictly
|
||||
// under one pixel per cell. Checked at both a tight and a genuinely wider width.
|
||||
static void testNoFaceLeavesSlackWhereItsDroppedControlsWere() {
|
||||
for (int avail : {kSampleAvail, kSampleAvailWide}) {
|
||||
for (PlayMode mode : {PlayMode::Gate, PlayMode::Trigger}) {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(mode);
|
||||
const DeckLayout dl = layoutDeck(g, kSamplePad, 0, avail);
|
||||
CHECK(dl.groups.size() == g.size());
|
||||
for (std::size_t i = 0; i < dl.groups.size(); ++i) {
|
||||
// The spanning deck's slots STACK — the run-division law this pins is the
|
||||
// horizontal one, and its vertical guard is its own test.
|
||||
if (g[i].row == DeckRow::Spanning) continue;
|
||||
const DeckGroupLayout& lay = dl.groups[i];
|
||||
const int reserved = static_cast<int>(g[i].cellIds.size()) * kDeckCellW;
|
||||
const std::size_t present = lay.cells.size();
|
||||
CHECK(present > 0);
|
||||
for (std::size_t k = 0; k < present; ++k) {
|
||||
const DeckCellLayout& c = lay.cells[k];
|
||||
CHECK(c.id >= 0); // a reserve yields width, never a dead rect
|
||||
CHECK(c.cell.width == lay.cells[0].cell.width);
|
||||
if (k > 0) CHECK(c.cell.x == lay.cells[k - 1].cell.right());
|
||||
}
|
||||
prevInRow[r] = ≷
|
||||
const int covered = lay.cells.back().cell.right() - lay.cells.front().cell.x;
|
||||
CHECK(reserved - covered < static_cast<int>(present));
|
||||
CHECK(lay.cells.front().cell.x >= lay.box.x + kDeckGroupPadX);
|
||||
CHECK(lay.cells.back().cell.right() <= lay.box.right() - kDeckGroupPadX);
|
||||
}
|
||||
const auto right = [&](int id) {
|
||||
return dl.groups[static_cast<std::size_t>(indexOfGroup(g, id))].box.right();
|
||||
};
|
||||
// Flush right on the block at every width, both rows.
|
||||
CHECK(right(kGroupVoice) == right(kGroupAmpEnv));
|
||||
// Monotone in width rather than oscillating: row 2's two gutters absorb slack
|
||||
// faster than row 1's three, so the gap only ever opens.
|
||||
const int drift = right(kGroupFilter) - right(kGroupFilterEnv);
|
||||
CHECK(drift <= lastDrift);
|
||||
lastDrift = drift;
|
||||
}
|
||||
// It really does open up: the tie-line is exact AT the floor and separates above it,
|
||||
// which is the accepted outcome rather than a near-miss to be pinned back.
|
||||
CHECK(lastDrift < -50);
|
||||
}
|
||||
}
|
||||
|
||||
// MASTER's interior, exact to the pixel (§1.4). The two left slots sit on the two rows' own
|
||||
// knob baselines — that is what "stitched to both rows" means — and the meter is ONE rect
|
||||
// across both, never a readout per row.
|
||||
static void testTheMasterDeckInteriorLandsOnBothRowBaselines() {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
||||
const DeckLayout dl = layoutDeck(g, kPad, 0, kAvailAtMinWidth);
|
||||
const DeckGroupLayout& m =
|
||||
dl.groups[static_cast<std::size_t>(indexOfGroup(g, kGroupMaster))];
|
||||
|
||||
CHECK(m.box.width == 142);
|
||||
CHECK(m.box.height == 216);
|
||||
// 6 + 60 + 8 + 62 + 6 — the decomposition, not just the total, and the 62 is the meter
|
||||
// module's own kMeterColumnW rather than a copy of it. That link is the whole point: the
|
||||
// column is banked to GROW (§1.2), and a reserve that did not track it would leave the
|
||||
// interior underfilling or overrunning with every test still green.
|
||||
CHECK(kDeckGroupPadX + kDeckCellW + kDeckColumnGap + kMeterColumnW + kDeckGroupPadX == 142);
|
||||
CHECK(m.column.id == cell(DeckParam::kMasterMeter));
|
||||
CHECK(m.column.box.width == kMeterColumnW);
|
||||
|
||||
// One cell drawn (gain) and one slot RESERVED below it: the reserve is height at a fixed
|
||||
// position and draws nothing.
|
||||
CHECK(m.cells.size() == 1);
|
||||
CHECK(m.cells[0].id == cell(DeckParam::kMasterGain));
|
||||
CHECK(m.cells[0].cell.y - m.box.y == 26);
|
||||
const int reserveTop = m.cells[0].cell.y + kDeckGroupH + kDeckRowGap;
|
||||
CHECK(reserveTop - m.box.y == 138);
|
||||
|
||||
// The two baselines are row 1's and row 2's own.
|
||||
const DeckGroupLayout& filter =
|
||||
dl.groups[static_cast<std::size_t>(indexOfGroup(g, kGroupFilter))];
|
||||
const DeckGroupLayout& amp =
|
||||
dl.groups[static_cast<std::size_t>(indexOfGroup(g, kGroupAmpEnv))];
|
||||
CHECK(m.cells[0].cell.y == filter.cells[0].cell.y);
|
||||
CHECK(reserveTop == amp.cells[0].cell.y);
|
||||
|
||||
// The meter: one rect spanning both baselines, 62 x 186.
|
||||
CHECK(m.column.id == cell(DeckParam::kMasterMeter));
|
||||
CHECK(m.column.box.width == 62);
|
||||
CHECK(m.column.box.height == 186);
|
||||
CHECK(m.column.box.y == m.cells[0].cell.y);
|
||||
CHECK(m.column.box.bottom() - m.box.y == 212);
|
||||
}
|
||||
|
||||
// The regression guard for the rule most likely to be "generalised" wrongly: MASTER's left
|
||||
// column is FIXED slots at the two baselines, NOT knob_deck's horizontal run-division law
|
||||
// applied vertically — which would stretch the one gain knob over the whole 186 px.
|
||||
static void testTheMasterColumnDoesNotDivideItsRunVertically() {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
||||
const DeckLayout dl = layoutDeck(g, kPad, 0, kAvailAtMinWidth);
|
||||
const DeckGroupLayout& m =
|
||||
dl.groups[static_cast<std::size_t>(indexOfGroup(g, kGroupMaster))];
|
||||
CHECK(m.cells[0].cell.height == kDeckCellH);
|
||||
CHECK(m.cells[0].cell.width == kDeckCellW);
|
||||
// Under the run-division law the lone present cell would take the whole two-slot run;
|
||||
// here it takes exactly one slot and leaves the rest empty.
|
||||
CHECK(m.cells[0].cell.height < m.column.box.height);
|
||||
CHECK(m.cells[0].cell.bottom() < m.column.box.bottom());
|
||||
CHECK(m.cells[0].knob.width == kDeckKnobSize && m.cells[0].knob.height == kDeckKnobSize);
|
||||
// And dropping the reserve does not move the gain knob or the meter — the slot below it is
|
||||
// reserved height, so nothing above it depends on whether it is there.
|
||||
std::vector<DeckGroupDesc> noReserve = g;
|
||||
for (DeckGroupDesc& d : noReserve) {
|
||||
if (d.id == kGroupMaster) d.cellIds = {cell(DeckParam::kMasterGain)};
|
||||
}
|
||||
const DeckLayout dl2 = layoutDeck(noReserve, kPad, 0, kAvailAtMinWidth);
|
||||
const DeckGroupLayout& m2 =
|
||||
dl2.groups[static_cast<std::size_t>(indexOfGroup(noReserve, kGroupMaster))];
|
||||
CHECK(m2.cells[0].cell == m.cells[0].cell);
|
||||
CHECK(m2.column.box == m.column.box);
|
||||
}
|
||||
|
||||
// MASTER's caption row and knob row measure exactly equal (130 == 130) today, so a column
|
||||
// derived from either edge lands in the same place — that balance is what let a left-derived
|
||||
// offset masquerade as right-anchored. Widen the caption reserve alone (as a wider caption or
|
||||
// a limiter-toggle change would) and the column must still land flush against the group's own
|
||||
// right padding, derived from innerRight rather than measured past the cell slots.
|
||||
static void testMasterColumnStaysRightAnchoredWhenCaptionRowOutgrowsTheKnobRow() {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
||||
DeckGroupDesc probe = g[static_cast<std::size_t>(indexOfGroup(g, kGroupMaster))];
|
||||
probe.captionWidth += 40; // unbalances it: the caption row now measures past the knob row
|
||||
const std::vector<DeckGroupDesc> one = {probe};
|
||||
const DeckLayout dl = layoutDeck(one, kPad, 0, kAvailAtMinWidth);
|
||||
const DeckGroupLayout& m = dl.groups[0];
|
||||
CHECK(m.box.width > 142); // the widen is real, not absorbed elsewhere
|
||||
CHECK(m.column.box.right() == m.box.right() - kDeckGroupPadX);
|
||||
}
|
||||
// The "residue lands in symmetric end margins" rule is knob_deck's own (layoutGroup), pinned
|
||||
// once by its synthetic residue>=2 fixture in test_knob_deck.cpp rather than restated here.
|
||||
|
||||
static void testHitTestResolvesTheNewFilterControls() {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
||||
const DeckLayout dl = layoutDeck(g, kPad, 40, kAvailAtMinWidth);
|
||||
const DeckLayout dl = layoutDeck(g, kSamplePad, 40, kSampleAvail);
|
||||
const DeckGroupLayout& f =
|
||||
dl.groups[static_cast<std::size_t>(indexOfGroup(g, kGroupFilter))];
|
||||
|
||||
@@ -617,143 +408,6 @@ static void testBipolarKnobLawRoundTripsAndIsExactAtCentre() {
|
||||
CHECK(deckNormFromBipolar(3.0) == 1.0);
|
||||
}
|
||||
|
||||
// The three mode toggles ride each env group's caption slack, so the deck's wrapped geometry
|
||||
// is unchanged by them: raising their segment width past the caption headroom would reflow the
|
||||
// first row and push the deck to a fourth one (see testDeckFitsInsideTheEnforcedMinimumWindow).
|
||||
static void testTheModeTogglesCostNoGroupWidth() {
|
||||
for (PlayMode mode : {PlayMode::Gate, PlayMode::Trigger}) {
|
||||
for (const DeckGroupDesc& g : sampleDeckGroups(mode)) {
|
||||
if (g.captionToggle2.id < 0) continue;
|
||||
DeckGroupDesc without = g;
|
||||
without.captionToggle2 = DeckToggleDesc{};
|
||||
CHECK(deckGroupWidth(g) == deckGroupWidth(without));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A typical larger window, to check the same properties once the deck has re-wrapped.
|
||||
static constexpr int kAvailAtLargerWidth = 1100 - 2 * kPad;
|
||||
|
||||
// The gap fix as a property of the shipped descriptors, not a picture: whichever face a
|
||||
// mode-dependent group shows, its knob row still spans the group's whole reserved run. The
|
||||
// Trigger faces drop Sustain and Release and get wider cells for it — never a hole where the
|
||||
// dropped control was. What the run does not cover is the indivisible residue alone, strictly
|
||||
// under one pixel per cell.
|
||||
static void testNoFaceLeavesSlackWhereItsDroppedControlsWere() {
|
||||
for (int avail : {kAvailAtMinWidth, kAvailAtLargerWidth}) {
|
||||
for (PlayMode mode : {PlayMode::Gate, PlayMode::Trigger}) {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(mode);
|
||||
const DeckLayout dl = layoutDeck(g, kPad, 0, avail);
|
||||
CHECK(dl.groups.size() == g.size());
|
||||
for (std::size_t i = 0; i < dl.groups.size(); ++i) {
|
||||
// The spanning deck's slots STACK — the run-division law this pins is the
|
||||
// horizontal one, and its vertical guard is its own test.
|
||||
if (g[i].row == DeckRow::Spanning) continue;
|
||||
const DeckGroupLayout& lay = dl.groups[i];
|
||||
const int reserved = static_cast<int>(g[i].cellIds.size()) * kDeckCellW;
|
||||
const std::size_t present = lay.cells.size();
|
||||
CHECK(present > 0);
|
||||
for (std::size_t k = 0; k < present; ++k) {
|
||||
const DeckCellLayout& c = lay.cells[k];
|
||||
CHECK(c.id >= 0); // a reserve yields width, never a dead rect
|
||||
CHECK(c.cell.width == lay.cells[0].cell.width);
|
||||
if (k > 0) CHECK(c.cell.x == lay.cells[k - 1].cell.right());
|
||||
}
|
||||
const int covered = lay.cells.back().cell.right() - lay.cells.front().cell.x;
|
||||
CHECK(reserved - covered < static_cast<int>(present));
|
||||
CHECK(lay.cells.front().cell.x >= lay.box.x + kDeckGroupPadX);
|
||||
CHECK(lay.cells.back().cell.right() <= lay.box.right() - kDeckGroupPadX);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The "residue lands in symmetric end margins" rule is knob_deck's own (layoutGroup), pinned
|
||||
// once by its synthetic residue>=2 fixture in test_knob_deck.cpp rather than restated here.
|
||||
|
||||
// PITCH/RATE carries three cells and measures exactly 192 — the KNOB row (3 x kDeckCellW plus
|
||||
// padding) is what it measures from, and the caption row must stay under that. The ceiling is
|
||||
// asserted by construction rather than as a comment: at a caption reserve of 80 the group is
|
||||
// still 192, and at 81 it is not, which is the whole content of "hard ceiling 80". Widening the
|
||||
// group is not the remedy if the caption text ever outgrows it — narrowing the mode toggle is.
|
||||
static void testThePitchRateGroupIsKnobRowDrivenAtExactlyOneNinetyTwo() {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
||||
const DeckGroupDesc* pitch = nullptr;
|
||||
for (const DeckGroupDesc& d : g) if (d.id == kGroupPitch) pitch = &d;
|
||||
CHECK(pitch != nullptr);
|
||||
if (!pitch) return;
|
||||
CHECK(pitch->cellIds.size() == 3);
|
||||
CHECK(pitch->cellIds[0] == static_cast<int>(DeckParam::kKeyTrack));
|
||||
CHECK(pitch->cellIds[1] == static_cast<int>(DeckParam::kRate));
|
||||
CHECK(pitch->cellIds[2] == static_cast<int>(DeckParam::kPitch));
|
||||
CHECK(deckGroupWidth(*pitch) == 192);
|
||||
CHECK(3 * kDeckCellW + 2 * kDeckGroupPadX == 192); // the knob row IS the measurement
|
||||
|
||||
DeckGroupDesc probe = *pitch;
|
||||
probe.captionWidth = 80;
|
||||
CHECK(deckGroupWidth(probe) == 192); // at the ceiling the caption row still fits under it
|
||||
probe.captionWidth = 81;
|
||||
CHECK(deckGroupWidth(probe) > 192); // one past it, the caption row takes over
|
||||
}
|
||||
|
||||
// The kEnvModeSegW ceilings recorded in deck_groups.cpp's own comment (PITCH ENV binds at 47,
|
||||
// AMP at 55) pinned against the descriptors they derive from, the same way the Pitch/Rate
|
||||
// caption ceiling above is: a change to either group's caption width or its enable toggle
|
||||
// would otherwise invalidate the recorded numbers with nothing failing.
|
||||
static void testEnvModeSegWCeilingsArePinnedForPitchEnvAndAmp() {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
||||
const DeckGroupDesc& penv = g[static_cast<std::size_t>(indexOfGroup(g, kGroupPitchEnv))];
|
||||
const DeckGroupDesc& amp = g[static_cast<std::size_t>(indexOfGroup(g, kGroupAmpEnv))];
|
||||
CHECK(deckGroupWidth(penv) == 252);
|
||||
CHECK(deckGroupWidth(amp) == 312);
|
||||
|
||||
DeckGroupDesc penvProbe = penv;
|
||||
penvProbe.captionToggle2.segWidth = 47;
|
||||
CHECK(deckGroupWidth(penvProbe) == 252); // at the ceiling, still knob-row-driven
|
||||
penvProbe.captionToggle2.segWidth = 48;
|
||||
CHECK(deckGroupWidth(penvProbe) > 252); // one past it, the caption row takes over
|
||||
|
||||
DeckGroupDesc ampProbe = amp;
|
||||
ampProbe.captionToggle2.segWidth = 55;
|
||||
CHECK(deckGroupWidth(ampProbe) == 312);
|
||||
ampProbe.captionToggle2.segWidth = 56;
|
||||
CHECK(deckGroupWidth(ampProbe) > 312);
|
||||
}
|
||||
|
||||
// Every group's width, in BOTH play modes, against the measured layout table
|
||||
// (instrument-control-surface.md §1.2). Mode-independence is the second half of the claim: the
|
||||
// reserve slots hold the two mode-dependent groups at 312 either way, which is what makes the
|
||||
// contour row's 876 a constant rather than a Gate-only fact.
|
||||
static void testEveryGroupWidthMatchesTheMeasuredLayout() {
|
||||
const struct { int id; int width; } want[] = {
|
||||
{kGroupPitch, 192}, {kGroupPitchEnv, 252}, {kGroupFilter, 432},
|
||||
{kGroupFilterEnv, 312}, {kGroupAmpEnv, 312}, {kGroupVelocity, 192},
|
||||
{kGroupVoice, 164}, {kGroupMaster, 142},
|
||||
};
|
||||
for (PlayMode mode : {PlayMode::Gate, PlayMode::Trigger}) {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(mode);
|
||||
CHECK(g.size() == sizeof(want) / sizeof(want[0]));
|
||||
const DeckLayout dl = layoutDeck(g, kPad, 0, kAvailAtMinWidth);
|
||||
for (const auto& w : want) {
|
||||
const int i = indexOfGroup(g, w.id);
|
||||
CHECK(i >= 0);
|
||||
if (i < 0) continue;
|
||||
CHECK(deckGroupWidth(g[static_cast<std::size_t>(i)]) == w.width);
|
||||
const DeckGroupLayout& lay =
|
||||
dl.groups[static_cast<std::size_t>(indexOfGroup(g, w.id))];
|
||||
CHECK(lay.box.width == w.width);
|
||||
}
|
||||
// Gate carries no reserves, so its cells are the deck's base size; Trigger's two
|
||||
// reduced faces divide the same reserved run between fewer cells and get wider ones.
|
||||
for (const DeckGroupLayout& lay : dl.groups) {
|
||||
for (const DeckCellLayout& c : lay.cells) {
|
||||
CHECK(c.cell.width >= kDeckCellW);
|
||||
if (mode == PlayMode::Gate) CHECK(c.cell.width == kDeckCellW);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool sameToggle(const DeckToggleLayout& a, const DeckToggleLayout& b) {
|
||||
return a.id == b.id && a.seg0 == b.seg0 && a.seg1 == b.seg1;
|
||||
}
|
||||
@@ -789,12 +443,12 @@ static bool sameLayout(const DeckLayout& a, const DeckLayout& b) {
|
||||
// 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);
|
||||
const DeckLayout before = layoutDeck(sampleDeckGroups(p.playMode), kSamplePad, 0, kSampleAvail);
|
||||
|
||||
p.ampSpline.mode = EnvMode::Spline;
|
||||
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);
|
||||
const DeckLayout drawn = layoutDeck(sampleDeckGroups(p.playMode), kSamplePad, 0, kSampleAvail);
|
||||
// The excursion is real: the amp face's cells are strictly wider than Gate's.
|
||||
const DeckGroupLayout& gateAmp =
|
||||
before.groups[static_cast<std::size_t>(indexOfGroup(sampleDeckGroups(PlayMode::Gate),
|
||||
@@ -809,12 +463,11 @@ static void testGateSplineGateRoundTripsToTheSameLayout() {
|
||||
p.ampSpline.mode = EnvMode::Staged;
|
||||
CHECK(!splineActive(p));
|
||||
p.playMode = PlayMode::Gate; // Gate is selectable again once nothing is drawn
|
||||
const DeckLayout after = layoutDeck(sampleDeckGroups(p.playMode), kPad, 0, kAvailAtMinWidth);
|
||||
const DeckLayout after = layoutDeck(sampleDeckGroups(p.playMode), kSamplePad, 0, kSampleAvail);
|
||||
CHECK(sameLayout(before, after));
|
||||
}
|
||||
|
||||
int main() {
|
||||
testTheModeTogglesCostNoGroupWidth();
|
||||
testDeckReadsPitchThenFilterThenAmpLeftToRight();
|
||||
testVelocityGroupOwnsTheThreeCurvesExclusively();
|
||||
testCurveTargetNamesEachCellsOwnDestination();
|
||||
@@ -825,22 +478,11 @@ int main() {
|
||||
testOnlySlopedStageKnobsCarryAnInnerCurveDial();
|
||||
testAmpGroupWidthSurvivesAGateTriggerFlip();
|
||||
testTheDeckIsTwoRowsPlusTheSpanningDeckByConstruction();
|
||||
testDeckFitsInsideTheEnforcedMinimumWindow();
|
||||
testNoFaceLeavesSlackWhereItsDroppedControlsWere();
|
||||
testThePitchRateGroupIsKnobRowDrivenAtExactlyOneNinetyTwo();
|
||||
testEnvModeSegWCeilingsArePinnedForPitchEnvAndAmp();
|
||||
testEveryGroupWidthMatchesTheMeasuredLayout();
|
||||
testGateSplineGateRoundTripsToTheSameLayout();
|
||||
testTheEditorFloorIsDerivedFromTheDeckWidthBudget();
|
||||
testEveryDeckGroupBelongsToExactlyOneRow();
|
||||
testBothRowsAndTheSpanningDeckFitTheBudget();
|
||||
testGutterArithmeticAndTheFilterTieLineAtTheFloor();
|
||||
testGuttersHoldTheirMinimumAndTheTieLineDriftsAboveTheFloor();
|
||||
testTheMasterDeckInteriorLandsOnBothRowBaselines();
|
||||
testTheMasterColumnDoesNotDivideItsRunVertically();
|
||||
testMasterColumnStaysRightAnchoredWhenCaptionRowOutgrowsTheKnobRow();
|
||||
testNoFaceLeavesSlackWhereItsDroppedControlsWere();
|
||||
testHitTestResolvesTheNewFilterControls();
|
||||
testBipolarKnobLawRoundTripsAndIsExactAtCentre();
|
||||
testGateSplineGateRoundTripsToTheSameLayout();
|
||||
if (g_fail == 0) std::printf("deck_groups: all tests passed\n");
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user