instrument: a deck group's reserved cell width goes to the cells present
A Trigger face dropping Sustain and Release now gets wider cells instead of 144 px of dead slots. Group widths, row packing and Gate are untouched.
This commit is contained in:
+121
-3
@@ -3,8 +3,9 @@
|
||||
// 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 hit-test reaching the new filter controls, the bipolar knob
|
||||
// floor width and its fit inside the floor window, the pinned Gate widths and row assignment,
|
||||
// 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
|
||||
// the live tier — and the overlay-selection state machine (exclusivity, the none resting state,
|
||||
// and which selections are inert).
|
||||
@@ -89,7 +90,7 @@ static void testCurveTargetNamesEachCellsOwnDestination() {
|
||||
CHECK(curveTargetFor(cell(DeckParam::kFilterVelCurve)) == CurveTarget::kFilter);
|
||||
CHECK(curveTargetFor(cell(DeckParam::kFilterCutoff)) == CurveTarget::kNone);
|
||||
CHECK(curveTargetFor(cell(DeckParam::kMasterGain)) == CurveTarget::kNone);
|
||||
CHECK(curveTargetFor(-1) == CurveTarget::kNone); // a blank reserved cell
|
||||
CHECK(curveTargetFor(-1) == CurveTarget::kNone); // a width reserve, not a control
|
||||
CHECK(curveTargetFor(9999) == CurveTarget::kNone); // out of the id space
|
||||
}
|
||||
|
||||
@@ -264,6 +265,7 @@ 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(b.decks.height == h);
|
||||
// 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
|
||||
@@ -529,6 +531,119 @@ static void testTheModeTogglesCostNoGroupWidth() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
||||
const struct { int id; int width; int row; } want[] = {
|
||||
{kGroupPitch, 150, 0}, {kGroupPitchEnv, 204, 0}, {kGroupFilter, 440, 0},
|
||||
{kGroupFilterEnv, 252, 1}, {kGroupAmpEnv, 252, 1}, {kGroupVelocity, 156, 1},
|
||||
{kGroupVoice, 152, 2}, {kGroupMaster, 60, 2},
|
||||
};
|
||||
CHECK(g.size() == sizeof(want) / sizeof(want[0]));
|
||||
const DeckLayout dl = layoutDeck(g, kPad, 0, kAvailAtMinWidth);
|
||||
for (std::size_t i = 0; i < dl.groups.size(); ++i) {
|
||||
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));
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
static bool sameToggle(const DeckToggleLayout& a, const DeckToggleLayout& b) {
|
||||
return a.id == b.id && a.seg0 == b.seg0 && a.seg1 == b.seg1;
|
||||
}
|
||||
|
||||
static bool sameLayout(const DeckLayout& a, const DeckLayout& b) {
|
||||
if (a.rowCount != b.rowCount || a.height != b.height ||
|
||||
a.groups.size() != b.groups.size()) return false;
|
||||
for (std::size_t i = 0; i < a.groups.size(); ++i) {
|
||||
const DeckGroupLayout& x = a.groups[i];
|
||||
const DeckGroupLayout& y = b.groups[i];
|
||||
if (x.id != y.id || !(x.box == y.box) || !(x.caption == y.caption)) return false;
|
||||
if (x.captionRadio.id != y.captionRadio.id || !(x.captionRadio.box == y.captionRadio.box))
|
||||
return false;
|
||||
if (!sameToggle(x.captionToggle, y.captionToggle) ||
|
||||
!sameToggle(x.captionToggle2, y.captionToggle2) ||
|
||||
!sameToggle(x.rowToggle, y.rowToggle)) return false;
|
||||
if (x.cells.size() != y.cells.size()) return false;
|
||||
for (std::size_t k = 0; k < x.cells.size(); ++k) {
|
||||
const DeckCellLayout& c = x.cells[k];
|
||||
const DeckCellLayout& d = y.cells[k];
|
||||
if (c.id != d.id || !(c.cell == d.cell) || !(c.knob == d.knob) ||
|
||||
!(c.inner == d.inner) || !(c.label == d.label)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// A Spline excursion is fully reversible at the layout level: the mode forcing swaps the amp
|
||||
// and filter faces onto their wider cells and back, leaving no residue in the geometry. Driven
|
||||
// through splineActive and the editor's own forcing rule, so the deck cannot agree with a
|
||||
// forcing rule the shell does not use.
|
||||
static void testGateSplineGateRoundTripsToTheSameLayout() {
|
||||
PlayParams p; // Gate, all three envelopes staged
|
||||
const DeckLayout before = layoutDeck(sampleDeckGroups(p.playMode), kPad, 0, kAvailAtMinWidth);
|
||||
|
||||
p.ampSpline.mode = EnvMode::Spline;
|
||||
if (splineActive(p)) p.playMode = PlayMode::Trigger; // editor_controls' forcing, verbatim
|
||||
CHECK(p.playMode == PlayMode::Trigger);
|
||||
const DeckLayout drawn = layoutDeck(sampleDeckGroups(p.playMode), kPad, 0, kAvailAtMinWidth);
|
||||
// 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),
|
||||
kGroupAmpEnv))];
|
||||
const DeckGroupLayout& trigAmp =
|
||||
drawn.groups[static_cast<std::size_t>(indexOfGroup(sampleDeckGroups(PlayMode::Trigger),
|
||||
kGroupAmpEnv))];
|
||||
CHECK(trigAmp.cells.size() < gateAmp.cells.size());
|
||||
CHECK(trigAmp.cells[0].cell.width > gateAmp.cells[0].cell.width);
|
||||
CHECK(!sameLayout(before, drawn));
|
||||
|
||||
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);
|
||||
CHECK(sameLayout(before, after));
|
||||
}
|
||||
|
||||
int main() {
|
||||
testOverlaySelectionIsExclusiveAcrossTheThreeEnvelopeDecks();
|
||||
testClickingTheActiveOverlayRadioClearsToNone();
|
||||
@@ -550,6 +665,9 @@ int main() {
|
||||
testAmpGroupWidthSurvivesAGateTriggerFlip();
|
||||
testWrappedDeckHeightAtTheEditorFloorWidth();
|
||||
testDeckFitsInsideTheEnforcedMinimumWindow();
|
||||
testNoFaceLeavesSlackWhereItsDroppedControlsWere();
|
||||
testGateModeWidthsAndRowAssignmentAreUnchanged();
|
||||
testGateSplineGateRoundTripsToTheSameLayout();
|
||||
testHitTestResolvesTheNewFilterControls();
|
||||
testBipolarKnobLawRoundTripsAndIsExactAtCentre();
|
||||
if (g_fail == 0) std::printf("deck_groups: all tests passed\n");
|
||||
|
||||
Reference in New Issue
Block a user