Raise the editor floor to 1190x680, derived from the deck's declared width budget, and make row membership a property of the group
This commit is contained in:
+103
-19
@@ -3,7 +3,8 @@
|
||||
// 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 widths and row assignment,
|
||||
// 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, the bipolar knob
|
||||
// law's inverse pair, the commit-tier routing — which controls are live, and which drags take
|
||||
@@ -238,13 +239,14 @@ static void testAmpGroupWidthSurvivesAGateTriggerFlip() {
|
||||
|
||||
static void testWrappedDeckHeightAtTheEditorFloorWidth() {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
||||
// At the floor (== default) 980 the deck takes three rows: PITCH + PITCH ENV + FILTER fill
|
||||
// the first (950 of the 964 available — fourteen px of headroom, so one more FILTER cell
|
||||
// would wrap the group and reflow everything under it), FILTER ENV + AMP + VELOCITY the
|
||||
// second, VOICE + MASTER the third. Two rows cannot hold the eight groups in ANY order at
|
||||
// this width: 1978 px of group plus 72 px of gaps against a 1928 px two-row capacity.
|
||||
CHECK(deckRowCount(g, kAvailAtMinWidth) == 3);
|
||||
CHECK(deckHeight(g, kAvailAtMinWidth) == 3 * kDeckGroupH + 2 * kDeckRowGap);
|
||||
// An UPPER BOUND, not an equality. The greedy whole-group wrap is still what decides row
|
||||
// membership until the reflow replaces it with the categorical partition, and at this width
|
||||
// it happens to pack two ragged rows with the wrong composition. Bounding it is a real
|
||||
// regression canary — a third row would cost the waveform 112 px again — without turning a
|
||||
// wrap outcome into a claim.
|
||||
const int rows = deckRowCount(g, kAvailAtMinWidth);
|
||||
CHECK(rows <= 2);
|
||||
CHECK(deckHeight(g, kAvailAtMinWidth) == rows * kDeckGroupH + (rows - 1) * kDeckRowGap);
|
||||
|
||||
// Whole groups only, never split: every group's box lies inside the available width or is
|
||||
// the first of its row.
|
||||
@@ -265,8 +267,13 @@ static void testDeckFitsInsideTheEnforcedMinimumWindow() {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(mode);
|
||||
const int h = deckHeight(g, kAvailAtMinWidth);
|
||||
const SampleBands b = computeSampleBands(kEditorMinWidth, kEditorMinHeight, h);
|
||||
CHECK(deckRowCount(g, kAvailAtMinWidth) == 3); // either face, three rows at the floor
|
||||
CHECK(deckRowCount(g, kAvailAtMinWidth) <= 2); // either face; see the bound above
|
||||
CHECK(b.decks.height == h);
|
||||
// The raised floor hands the waveform the reflow's 112 px two waves early: at two rows
|
||||
// the deck band is 216 and the waveform 358, against 328/246 before. Bounded rather
|
||||
// than pinned for the same reason the row count is.
|
||||
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
|
||||
@@ -276,6 +283,75 @@ static void testDeckFitsInsideTheEnforcedMinimumWindow() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 == 90);
|
||||
// The reflow's 112 px goes entirely to the waveform, so the height does not move.
|
||||
CHECK(kEditorMinHeight == 680);
|
||||
}
|
||||
|
||||
static void testEveryDeckGroupBelongsToExactlyOneRow() {
|
||||
CHECK(deckRowFor(kGroupPitch) == DeckRow::Sound);
|
||||
CHECK(deckRowFor(kGroupFilter) == DeckRow::Sound);
|
||||
CHECK(deckRowFor(kGroupVelocity) == DeckRow::Sound);
|
||||
CHECK(deckRowFor(kGroupVoice) == DeckRow::Sound);
|
||||
CHECK(deckRowFor(kGroupPitchEnv) == DeckRow::Contour);
|
||||
CHECK(deckRowFor(kGroupFilterEnv) == DeckRow::Contour);
|
||||
CHECK(deckRowFor(kGroupAmpEnv) == DeckRow::Contour);
|
||||
CHECK(deckRowFor(kGroupMaster) == DeckRow::Spanning);
|
||||
|
||||
// Totality against the descriptor list the deck actually carries, not just against the
|
||||
// enum: a group that shipped without a row would land here as a miscount.
|
||||
for (PlayMode mode : {PlayMode::Gate, PlayMode::Trigger}) {
|
||||
int sound = 0, contour = 0, spanning = 0;
|
||||
for (const DeckGroupDesc& d : sampleDeckGroups(mode)) {
|
||||
switch (deckRowFor(static_cast<DeckGroupId>(d.id))) {
|
||||
case DeckRow::Sound: ++sound; break;
|
||||
case DeckRow::Contour: ++contour; break;
|
||||
case DeckRow::Spanning: ++spanning; break;
|
||||
}
|
||||
}
|
||||
CHECK(sound == 4 && contour == 3 && spanning == 1);
|
||||
}
|
||||
}
|
||||
|
||||
// What the budget can already be measured against. The contour row fits today and MASTER has
|
||||
// not touched its reserve; the SOUND row does not fit yet and must not be forced to — it is
|
||||
// 1030 against the 1020 block, and the 50 px deficit is exactly what two later descriptor
|
||||
// changes buy: PITCH becoming PITCH/RATE (+42) and FILTER's Band|Notch moving from the knob
|
||||
// row to the caption corner (−92), netting 980. The fit is asserted when they land, not here.
|
||||
static void testTheContourRowAndTheSpanningDeckFitTheBudget() {
|
||||
for (PlayMode mode : {PlayMode::Gate, PlayMode::Trigger}) {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(mode);
|
||||
int contourWidth = 0, contourGroups = 0, spanningWidth = 0;
|
||||
for (const DeckGroupDesc& d : g) {
|
||||
const DeckRow row = deckRowFor(static_cast<DeckGroupId>(d.id));
|
||||
if (row == DeckRow::Contour) {
|
||||
contourWidth += deckGroupWidth(d);
|
||||
++contourGroups;
|
||||
} else if (row == DeckRow::Spanning) {
|
||||
spanningWidth += deckGroupWidth(d);
|
||||
}
|
||||
}
|
||||
// 252 + 312 + 312. Mode-stable because FILTER ENV's and AMP's reserve slots hold them
|
||||
// at 312 in Trigger as well as Gate.
|
||||
CHECK(contourGroups == 3);
|
||||
CHECK(contourWidth == 876);
|
||||
CHECK(contourWidth <= kDeckRowBlockW);
|
||||
// Slack enough that neither of the row's two gutters falls under the minimum.
|
||||
CHECK(kDeckRowBlockW - contourWidth >= (contourGroups - 1) * kDeckGroupGap);
|
||||
// MASTER is 72 today against a 142 reserve: the double-height interior it grows into is
|
||||
// budgeted for, not yet spent.
|
||||
CHECK(spanningWidth == 72);
|
||||
CHECK(spanningWidth <= kDeckSpanningW);
|
||||
}
|
||||
}
|
||||
|
||||
static void testHitTestResolvesTheNewFilterControls() {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
||||
const DeckLayout dl = layoutDeck(g, kPad, 40, kAvailAtMinWidth);
|
||||
@@ -568,15 +644,17 @@ static void testNoFaceLeavesSlackWhereItsDroppedControlsWere() {
|
||||
// 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.
|
||||
|
||||
// Gate is the common face and it already packs correctly: pin its group widths and row
|
||||
// assignment at the floor so a later edit anywhere in the deck cannot reflow it silently.
|
||||
// (Measured from the shipped descriptors, not copied out of a failing run.)
|
||||
static void testGateModeWidthsAndRowAssignmentAreUnchanged() {
|
||||
// Gate is the common face and its group widths are what the width budget is spent against:
|
||||
// pin them at the floor so a later edit anywhere in the deck cannot move one silently.
|
||||
// (Measured from the shipped descriptors, not copied out of a failing run.) The WRAP row a
|
||||
// group lands on is deliberately NOT pinned — that is the interim greedy pack the reflow
|
||||
// replaces, and deckRowFor is where row membership is asserted.
|
||||
static void testGateModeGroupWidthsAreUnchanged() {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
||||
const struct { int id; int width; int row; } want[] = {
|
||||
{kGroupPitch, 150, 0}, {kGroupPitchEnv, 252, 0}, {kGroupFilter, 524, 0},
|
||||
{kGroupFilterEnv, 312, 1}, {kGroupAmpEnv, 312, 1}, {kGroupVelocity, 192, 1},
|
||||
{kGroupVoice, 164, 2}, {kGroupMaster, 72, 2},
|
||||
const struct { int id; int width; } want[] = {
|
||||
{kGroupPitch, 150}, {kGroupPitchEnv, 252}, {kGroupFilter, 524},
|
||||
{kGroupFilterEnv, 312}, {kGroupAmpEnv, 312}, {kGroupVelocity, 192},
|
||||
{kGroupVoice, 164}, {kGroupMaster, 72},
|
||||
};
|
||||
CHECK(g.size() == sizeof(want) / sizeof(want[0]));
|
||||
const DeckLayout dl = layoutDeck(g, kPad, 0, kAvailAtMinWidth);
|
||||
@@ -584,7 +662,10 @@ static void testGateModeWidthsAndRowAssignmentAreUnchanged() {
|
||||
CHECK(dl.groups[i].id == want[i].id);
|
||||
CHECK(deckGroupWidth(g[i]) == want[i].width);
|
||||
CHECK(dl.groups[i].box.width == want[i].width);
|
||||
CHECK(dl.groups[i].box.y == want[i].row * (kDeckGroupH + kDeckRowGap));
|
||||
// Every box lands on a row line, and no lower than the second — the same two-row
|
||||
// bound the deck height carries.
|
||||
CHECK(dl.groups[i].box.y % (kDeckGroupH + kDeckRowGap) == 0);
|
||||
CHECK(dl.groups[i].box.y <= kDeckGroupH + kDeckRowGap);
|
||||
// Gate carries no reserves, so its cells are the deck's base size.
|
||||
for (const DeckCellLayout& c : dl.groups[i].cells) CHECK(c.cell.width == kDeckCellW);
|
||||
}
|
||||
@@ -669,8 +750,11 @@ int main() {
|
||||
testWrappedDeckHeightAtTheEditorFloorWidth();
|
||||
testDeckFitsInsideTheEnforcedMinimumWindow();
|
||||
testNoFaceLeavesSlackWhereItsDroppedControlsWere();
|
||||
testGateModeWidthsAndRowAssignmentAreUnchanged();
|
||||
testGateModeGroupWidthsAreUnchanged();
|
||||
testGateSplineGateRoundTripsToTheSameLayout();
|
||||
testTheEditorFloorIsDerivedFromTheDeckWidthBudget();
|
||||
testEveryDeckGroupBelongsToExactlyOneRow();
|
||||
testTheContourRowAndTheSpanningDeckFitTheBudget();
|
||||
testHitTestResolvesTheNewFilterControls();
|
||||
testBipolarKnobLawRoundTripsAndIsExactAtCentre();
|
||||
if (g_fail == 0) std::printf("deck_groups: all tests passed\n");
|
||||
|
||||
Reference in New Issue
Block a user