deck: filter mod moves to FILTER ENV, cell runs centre in their reserves, two-segment toggles become single buttons, deck focuses its overlay

This commit is contained in:
2026-08-03 13:12:28 -04:00
parent 0eb2c67875
commit 450559f155
22 changed files with 885 additions and 504 deletions
+53 -45
View File
@@ -120,47 +120,58 @@ static void testOnlyALiveControlsDragTakesTheLiveTier() {
LiveCommit::Reload);
}
// --- The overlay selection state machine ---------------------------------------
// --- The overlay focus state machine -------------------------------------------
static int radio(DeckParam p) { return static_cast<int>(p); }
// EXCLUSIVITY: picking another deck's radio switches to it outright — two envelopes can never
// be overlay-active at once, whatever the previous selection was.
static void testOverlaySelectionIsExclusiveAcrossTheThreeEnvelopeDecks() {
const OverlayEnv states[] = {OverlayEnv::kNone, OverlayEnv::kAmp, OverlayEnv::kPitch,
OverlayEnv::kFilter};
for (OverlayEnv from : states) {
if (from != OverlayEnv::kAmp) {
CHECK(nextOverlaySelection(from, radio(DeckParam::kAmpEnvSelect)) == OverlayEnv::kAmp);
}
if (from != OverlayEnv::kPitch) {
CHECK(nextOverlaySelection(from, radio(DeckParam::kPitchEnvSelect)) ==
OverlayEnv::kPitch);
}
if (from != OverlayEnv::kFilter) {
CHECK(nextOverlaySelection(from, radio(DeckParam::kFilterEnvSelect)) ==
OverlayEnv::kFilter);
// EXCLUSIVITY, and the whole of it: the focus is a function of the clicked GROUP alone, so
// wherever it was before, clicking an envelope deck lands on that deck's envelope. Two
// envelopes can never be overlay-active at once, and no previous state can change the answer.
static void testOverlayFocusIsExclusiveAndIndependentOfThePreviousSelection() {
const struct { int group; OverlayEnv env; } decks[] = {
{kGroupAmpEnv, OverlayEnv::kAmp},
{kGroupPitchEnv, OverlayEnv::kPitch},
{kGroupFilterEnv, OverlayEnv::kFilter},
};
for (const auto& d : decks) CHECK(overlayEnvForGroup(d.group) == d.env);
// Distinct answers, so no two decks can select the same overlay.
CHECK(overlayEnvForGroup(kGroupAmpEnv) != overlayEnvForGroup(kGroupPitchEnv));
CHECK(overlayEnvForGroup(kGroupPitchEnv) != overlayEnvForGroup(kGroupFilterEnv));
CHECK(overlayEnvForGroup(kGroupAmpEnv) != overlayEnvForGroup(kGroupFilterEnv));
}
// Focus SETS; it does not toggle. Driven as the SHELL drives it — `focus = f(group)` over a
// click sequence starting from every prior focus — because that composition is the thing the
// retired re-click-clears branch broke: a second click on the focused deck (which is every
// knob tweak on it) landed back on kNone. The map taking no current focus is what makes that
// unreachable; this pins the sequence a reader would otherwise have to reconstruct.
static void testAClickSequenceOnOneDeckNeverLeavesIt() {
for (OverlayEnv prior : {OverlayEnv::kNone, OverlayEnv::kAmp, OverlayEnv::kPitch,
OverlayEnv::kFilter}) {
OverlayEnv focus = prior;
// Panel, then knob, then button — all three land in the same group, so all three are
// the same assignment, whatever the click before them was.
for (int i = 0; i < 3; ++i) {
focus = overlayEnvForGroup(kGroupFilterEnv);
CHECK(focus == OverlayEnv::kFilter);
}
// And leaving is a click ELSEWHERE, never a repeat of the one that got here.
focus = overlayEnvForGroup(kGroupVoice);
CHECK(focus == OverlayEnv::kNone);
}
}
// kNone is a RESTING STATE the user can get back to: clicking the active radio clears it.
static void testClickingTheActiveOverlayRadioClearsToNone() {
CHECK(nextOverlaySelection(OverlayEnv::kAmp, radio(DeckParam::kAmpEnvSelect)) ==
OverlayEnv::kNone);
CHECK(nextOverlaySelection(OverlayEnv::kPitch, radio(DeckParam::kPitchEnvSelect)) ==
OverlayEnv::kNone);
CHECK(nextOverlaySelection(OverlayEnv::kFilter, radio(DeckParam::kFilterEnvSelect)) ==
OverlayEnv::kNone);
}
// A control that is not one of the three radios selects nothing and clears nothing.
static void testANonRadioIdLeavesTheOverlaySelectionAlone() {
CHECK(overlayEnvForRadio(radio(DeckParam::kFilterCutoff)) == OverlayEnv::kNone);
CHECK(overlayEnvForRadio(-1) == OverlayEnv::kNone);
CHECK(nextOverlaySelection(OverlayEnv::kFilter, radio(DeckParam::kFilterCutoff)) ==
OverlayEnv::kFilter);
CHECK(nextOverlaySelection(OverlayEnv::kAmp, 9999) == OverlayEnv::kAmp);
// kNone is still a reachable resting state — reached by clicking a control surface OUTSIDE the
// envelope decks rather than by clicking the active one again.
static void testClickingAnyNonEnvelopeDeckClearsTheFocus() {
for (int id : {kGroupPitch, kGroupFilter, kGroupVelocity, kGroupVoice, kGroupMaster}) {
CHECK(overlayEnvForGroup(id) == OverlayEnv::kNone);
}
CHECK(overlayEnvForGroup(-1) == OverlayEnv::kNone); // off the deck entirely
// A CONTROL id is not a group id: the map keys on groups now, and a stray control id must
// never light an overlay by numeric coincidence.
CHECK(overlayEnvForGroup(radio(DeckParam::kAmpEnvSelect)) == OverlayEnv::kNone);
CHECK(overlayEnvForGroup(radio(DeckParam::kFilterCutoff)) == OverlayEnv::kNone);
}
// The two group gates, spelled the way the predicates read them. Spline flags default off, so
@@ -215,25 +226,22 @@ static void testDeckKnobIsInertExactlyWithItsGroupsEnableToggle() {
// A drawn envelope's STAGED segment knobs go inert; the mode toggle itself and the depth knobs
// that scale either shape stay live. (Which segment knobs, per envelope, is pinned in
// spline_egs_tests alongside the rest of the spline rules.)
static void testAModeToggleIsNeitherLiveNorAnOverlayRadio() {
static void testAModeToggleIsNotALiveControl() {
CHECK(deckParamCommit(DeckParam::kAmpEnvMode) == LiveCommit::Reload);
CHECK(deckParamCommit(DeckParam::kPitchEnvMode) == LiveCommit::Reload);
CHECK(deckParamCommit(DeckParam::kFilterEnvMode) == LiveCommit::Reload);
CHECK(overlayEnvForModeToggle(radio(DeckParam::kAmpEnvMode)) == OverlayEnv::kAmp);
CHECK(overlayEnvForModeToggle(radio(DeckParam::kPitchEnvMode)) == OverlayEnv::kPitch);
CHECK(overlayEnvForModeToggle(radio(DeckParam::kFilterEnvMode)) == OverlayEnv::kFilter);
// A mode toggle must not be mistaken for the overlay-select radio beside it.
CHECK(overlayEnvForRadio(radio(DeckParam::kAmpEnvMode)) == OverlayEnv::kNone);
CHECK(overlayEnvForModeToggle(radio(DeckParam::kAmpEnvSelect)) == OverlayEnv::kNone);
// It needs no overlay map of its own: the toggle sits INSIDE its envelope's deck, so the
// click that flips it already focuses that envelope through the group map.
CHECK(overlayEnvForGroup(kGroupAmpEnv) == OverlayEnv::kAmp);
}
int main() {
testOverlaySelectionIsExclusiveAcrossTheThreeEnvelopeDecks();
testClickingTheActiveOverlayRadioClearsToNone();
testANonRadioIdLeavesTheOverlaySelectionAlone();
testOverlayFocusIsExclusiveAndIndependentOfThePreviousSelection();
testAClickSequenceOnOneDeckNeverLeavesIt();
testClickingAnyNonEnvelopeDeckClearsTheFocus();
testOverlayIsInertExactlyWhenItsGroupToggleIsOff();
testDeckKnobIsInertExactlyWithItsGroupsEnableToggle();
testAModeToggleIsNeitherLiveNorAnOverlayRadio();
testAModeToggleIsNotALiveControl();
testEveryDeckControlIsClassifiedIntoOneOfTheThreeCommitTiers();
testOnlyALiveControlsDragTakesTheLiveTier();
if (g_fail == 0) std::printf("deck_groups_state: all tests passed\n");