Merge Γ-W1-T4: the editor floor, the width budget, and the deck row law
This commit is contained in:
@@ -304,7 +304,7 @@ anything for a trigger shape.
|
|||||||
### `ui/`
|
### `ui/`
|
||||||
|
|
||||||
- `editor_geometry` (`core/instrument/ui`) — the shared geometry VOCABULARY every instrument UI module speaks: the `core::ui::Rect` alias, `contains()`, and `OverlayArea` (a one-field `Rect` wrapper, no implicit conversion from `Rect`). Header-only (an INTERFACE CMake target), so it carries no layout of its own.
|
- `editor_geometry` (`core/instrument/ui`) — the shared geometry VOCABULARY every instrument UI module speaks: the `core::ui::Rect` alias, `contains()`, and `OverlayArea` (a one-field `Rect` wrapper, no implicit conversion from `Rect`). Header-only (an INTERFACE CMake target), so it carries no layout of its own.
|
||||||
- `sample_bands` — **THE band-stack allocator**, and the only module that owns the Sample face's vertical inventory — including `kEditorMinWidth`/`kEditorMinHeight`, the editor's client-area floor, which IS its default size (the shell's `checkSizeConstraint` and opening `ViewRect` both read it; the face grows, never shrinks below what the stack is laid out for). Three bands top-to-bottom (CHROME toolbar+control row / WAVEFORM elastic, floored at two stacked lanes / DECKS bottom-anchored at the knob deck's own wrapped height), plus the waveform band's lane split (`waveformLanes` takes a resolved `LaneSplit`, not a raw bool — only `waveformSurface` folds the source-channel-count decision in). A shared READ-ONLY surface for every band owner — a band's interior module lays out inside the rect it is handed and never re-allocates the stack.
|
- `sample_bands` — **THE band-stack allocator**, and the only module that owns the Sample face's vertical inventory — including `kEditorMinWidth`/`kEditorMinHeight`, the editor's client-area floor, which IS its default size (the shell's `checkSizeConstraint` and opening `ViewRect` both read it; the face grows, never shrinks below what the stack is laid out for), and `kEditorCeilingWidth`, the floor's sibling window fact (the hard cap the floor may not exceed) — moved here from `knob_deck.h` since it is a window fact, not a deck one; the derivation identity against the deck's width budget stays in `test_deck_groups.cpp`, the one place that already includes both headers. Three bands top-to-bottom (CHROME toolbar+control row / WAVEFORM elastic, floored at two stacked lanes / DECKS bottom-anchored at the knob deck's own wrapped height), plus the waveform band's lane split (`waveformLanes` takes a resolved `LaneSplit`, not a raw bool — only `waveformSurface` folds the source-channel-count decision in). A shared READ-ONLY surface for every band owner — a band's interior module lays out inside the rect it is handed and never re-allocates the stack.
|
||||||
- `sample_chrome` — the CHROME band's interior: the toolbar row (title + the whole right-anchored control run — bake Hold cell, bake, preview, velocity knob cell, channel toggle, Browse) over the strip row, which the piano strip owns outright. The title takes what the run leaves; the strip takes its whole row, inset only by the shared band pad so it lines up with the waveform band beneath. Every run member's width is RESERVED unconditionally, the Hold cell included — the only conditionally-drawn one, and the leftmost, so what its reservation buys is a title slot that does not re-measure when a loop is dialled in or out (`sample_chrome.h` records the cost). Also `previewGlyph`, the preview button's play triangle — three vertices for one filled-triangle draw, so the button's label needs no font metric and no image asset.
|
- `sample_chrome` — the CHROME band's interior: the toolbar row (title + the whole right-anchored control run — bake Hold cell, bake, preview, velocity knob cell, channel toggle, Browse) over the strip row, which the piano strip owns outright. The title takes what the run leaves; the strip takes its whole row, inset only by the shared band pad so it lines up with the waveform band beneath. Every run member's width is RESERVED unconditionally, the Hold cell included — the only conditionally-drawn one, and the leftmost, so what its reservation buys is a title slot that does not re-measure when a loop is dialled in or out (`sample_chrome.h` records the cost). Also `previewGlyph`, the preview button's play triangle — three vertices for one filled-triangle draw, so the button's label needs no font metric and no image asset.
|
||||||
- `bake_hold` — the Hold knob's value domain and nothing else: the knob's normalized [0,1] mapped onto the note-length ladder and back, ordered by LENGTH rather than by the ladder's presentation order. Split from `sample_chrome` on the same axis `deck_values` was split from `knob_deck` — that says where the cell is, this says what its position means.
|
- `bake_hold` — the Hold knob's value domain and nothing else: the knob's normalized [0,1] mapped onto the note-length ladder and back, ordered by LENGTH rather than by the ladder's presentation order. Split from `sample_chrome` on the same axis `deck_values` was split from `knob_deck` — that says where the cell is, this says what its position means.
|
||||||
- `keyboard_strip` — piano-keyboard strip: true white/black key geometry (whites tiled at one width, blacks overlaid at one width and height, straddling their boundary), hit-test resolving black-over-white by zone, root-marker rect, the absolute-position drag resolver, and MIDI note naming under the C4 convention. **Same-class keys are one integer width by construction; the residue of an indivisible band width (`w % 75`, up to 74 px) lands in symmetric end margins, never in a key** — uniform widths and gap-free edge-to-edge tiling cannot both hold, and uniformity wins.
|
- `keyboard_strip` — piano-keyboard strip: true white/black key geometry (whites tiled at one width, blacks overlaid at one width and height, straddling their boundary), hit-test resolving black-over-white by zone, root-marker rect, the absolute-position drag resolver, and MIDI note naming under the C4 convention. **Same-class keys are one integer width by construction; the residue of an indivisible band width (`w % 75`, up to 74 px) lands in symmetric end margins, never in a key** — uniform widths and gap-free edge-to-edge tiling cannot both hold, and uniformity wins.
|
||||||
@@ -314,7 +314,7 @@ anything for a trigger shape.
|
|||||||
- `browser_scroll` — scroll + type-to-filter layered over `capture_browser`: vertical scroll offset, scrollbar thumb, thumb-drag mapping, and name-substring search.
|
- `browser_scroll` — scroll + type-to-filter layered over `capture_browser`: vertical scroll offset, scrollbar thumb, thumb-drag mapping, and name-substring search.
|
||||||
- `param_slider` — parameter control-panel: vertical stack of TOGGLE (two-segment selector) and SLIDER (horizontal track) rows; maps normalized value to/from handle pixel.
|
- `param_slider` — parameter control-panel: vertical stack of TOGGLE (two-segment selector) and SLIDER (horizontal track) rows; maps normalized value to/from handle pixel.
|
||||||
- `embed_strip` — compact single-row control layout for embed mode in the track FX chain.
|
- `embed_strip` — compact single-row control layout for embed mode in the track FX chain.
|
||||||
- `knob_deck` — pure knob-deck layout + hit-test (FB1): group-box / caption-row / compact-toggle / knob-cell geometry, deterministic whole-group wrap, `DeckLayout` / `DeckHit`. Mirror of `action_bar`/`param_slider`; no LICE or REAPER types. Carries a SECOND hit-test, `hitTestKnobFace`, resolved against the drawn CIRCLES rather than the cell: a double-click reset is aimed at a dial, so the label band and the cell margins must miss where a drag grab deliberately does not, and only a radial resolve can tell the inner curve dial from the outer ring it sits inside. **The cell/knob/label sizes and `sample_bands`' editor floor move as a pair** — wider cells need a wider floor width or the deck wraps to a fourth row. A group carries TWO caption-toggle slots, laid right-to-left: the second exists because a group whose knob row is wider than its caption row has caption slack a toggle can occupy for free, and the deck has fourteen pixels of headroom on its first row at the editor's floor width — a `rowToggle` would widen the GROUP and wrap the deck to a fourth row, past what the minimum window holds. **A group's cell run is a RESERVED WIDTH, not a fixed cell size**: a `-1` id reserves one cell's width without a cell, and the cells present divide the whole run between them at one uniform integer width (residue in symmetric end margins). That is what lets a mode flip drop controls from a face — Trigger's AMP and FILTER ENV lose their Sustain/Release stages — without either reflowing the deck or leaving dead slots in the box; a face with fewer controls simply gets roomier cells. Do not reintroduce fixed-width cells with blank slots.
|
- `knob_deck` — pure knob-deck layout + hit-test (FB1): group-box / caption-row / compact-toggle / knob-cell geometry, deterministic whole-group wrap, `DeckLayout` / `DeckHit`. Mirror of `action_bar`/`param_slider`; no LICE or REAPER types. Carries a SECOND hit-test, `hitTestKnobFace`, resolved against the drawn CIRCLES rather than the cell: a double-click reset is aimed at a dial, so the label band and the cell margins must miss where a drag grab deliberately does not, and only a radial resolve can tell the inner curve dial from the outer ring it sits inside. The deck's width budget at the editor's floor — the row block, the spanning deck's reserve, and what drives the floor — is declared and reasoned at the constants themselves (`knob_deck.h`; the ceiling itself now lives in `sample_bands.h` as a window fact); every group's categorical row is `deck_groups`' `deckRowFor`. A group carries TWO caption-toggle slots, laid right-to-left: the second exists because a group whose knob row is wider than its caption row has caption slack a toggle can occupy for free, where a `rowToggle` widens the GROUP and is charged against that budget — which is why the env decks' mode toggles ride the caption row. **A group's cell run is a RESERVED WIDTH, not a fixed cell size**: a `-1` id reserves one cell's width without a cell, and the cells present divide the whole run between them at one uniform integer width (residue in symmetric end margins). That is what lets a mode flip drop controls from a face — Trigger's AMP and FILTER ENV lose their Sustain/Release stages — without either reflowing the deck or leaving dead slots in the box; a face with fewer controls simply gets roomier cells. Do not reintroduce fixed-width cells with blank slots.
|
||||||
- `deck_values` — the deck's control-id ↔ parameter-set BINDING and its display units, split
|
- `deck_values` — the deck's control-id ↔ parameter-set BINDING and its display units, split
|
||||||
from the editor shell on the same axis `deck_groups` was split from `knob_deck`: `deck_groups`
|
from the editor shell on the same axis `deck_groups` was split from `knob_deck`: `deck_groups`
|
||||||
says which controls exist, this says what each one's value MEANS. Holds `deckParamNorm` /
|
says which controls exist, this says what each one's value MEANS. Holds `deckParamNorm` /
|
||||||
|
|||||||
@@ -130,6 +130,29 @@ std::vector<DeckGroupDesc> sampleDeckGroups(PlayMode playMode) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DeckRow deckRowFor(DeckGroupId group) {
|
||||||
|
// Every enumerator listed and no default, on the same gate isLiveDeckParam below relies on.
|
||||||
|
switch (group) {
|
||||||
|
case kGroupPitch:
|
||||||
|
case kGroupFilter:
|
||||||
|
case kGroupVelocity:
|
||||||
|
case kGroupVoice:
|
||||||
|
return DeckRow::Sound;
|
||||||
|
case kGroupPitchEnv:
|
||||||
|
case kGroupFilterEnv:
|
||||||
|
case kGroupAmpEnv:
|
||||||
|
return DeckRow::Contour;
|
||||||
|
case kGroupMaster:
|
||||||
|
return DeckRow::Spanning;
|
||||||
|
}
|
||||||
|
// Unreachable for a valid enumerator, and Spanning rather than Sound ON PURPOSE: the
|
||||||
|
// -Wswitch gate is compiler-dependent, so on a toolchain that does not raise it a dropped
|
||||||
|
// case arm falls here instead. Sound is what a new group most plausibly IS, which would
|
||||||
|
// make the fall-through invisible; Spanning is the one row nothing may silently join, so
|
||||||
|
// the tests' partition count catches it.
|
||||||
|
return DeckRow::Spanning;
|
||||||
|
}
|
||||||
|
|
||||||
CurveTarget curveTargetFor(int controlId) {
|
CurveTarget curveTargetFor(int controlId) {
|
||||||
switch (static_cast<DeckParam>(controlId)) {
|
switch (static_cast<DeckParam>(controlId)) {
|
||||||
case DeckParam::kAmpVelCurve: return CurveTarget::kAmp;
|
case DeckParam::kAmpVelCurve: return CurveTarget::kAmp;
|
||||||
|
|||||||
@@ -101,6 +101,16 @@ enum DeckGroupId {
|
|||||||
kGroupMaster,
|
kGroupMaster,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The deck's two categorical rows, plus the row-spanning bus deck. Sound is what the voice
|
||||||
|
// IS, Contour is how it moves over time, Spanning is what happens after the mixer.
|
||||||
|
enum class DeckRow { Sound, Contour, Spanning };
|
||||||
|
|
||||||
|
// Which row a group belongs to. Membership is a property of the GROUP; width is a property of
|
||||||
|
// its descriptor — separating them is what lets the row law be settled while the descriptors
|
||||||
|
// are still moving. Total over DeckGroupId by an exhaustive switch with no default, so a group
|
||||||
|
// added without a row cannot silently become Sound.
|
||||||
|
DeckRow deckRowFor(DeckGroupId group);
|
||||||
|
|
||||||
// Which velocity curve a deck cell edits, or kNone when the control is an ordinary knob. THE
|
// Which velocity curve a deck cell edits, or kNone when the control is an ordinary knob. THE
|
||||||
// one place a control id resolves to a curve target — paint (draw a curve thumbnail, not a
|
// one place a control id resolves to a curve target — paint (draw a curve thumbnail, not a
|
||||||
// dial) and hit-test (open a popup, not start a drag) both read this predicate rather than
|
// dial) and hit-test (open a popup, not start a drag) both read this predicate rather than
|
||||||
|
|||||||
@@ -23,8 +23,9 @@
|
|||||||
namespace reasampler::instrument::ui {
|
namespace reasampler::instrument::ui {
|
||||||
|
|
||||||
// Fixed deck metrics, exposed so the shell and tests agree. The cell/knob/label sizes were
|
// Fixed deck metrics, exposed so the shell and tests agree. The cell/knob/label sizes were
|
||||||
// raised together for legibility at high pixel densities; the editor's floor width
|
// raised together for legibility at high pixel densities. The deck's cell metrics AND its
|
||||||
// (sample_bands) is what absorbs the wider cells, so the two move as a pair.
|
// group/row composition BOTH drive sample_bands' kEditorMinWidth; none of the three may move
|
||||||
|
// alone.
|
||||||
inline constexpr int kDeckCellW = 60; // one knob cell
|
inline constexpr int kDeckCellW = 60; // one knob cell
|
||||||
inline constexpr int kDeckCellH = 74;
|
inline constexpr int kDeckCellH = 74;
|
||||||
inline constexpr int kDeckKnobSize = 40; // knob diameter inside the cell
|
inline constexpr int kDeckKnobSize = 40; // knob diameter inside the cell
|
||||||
@@ -46,6 +47,19 @@ inline constexpr int kDeckInnerDialSize = 20;
|
|||||||
inline constexpr int kDeckGroupH =
|
inline constexpr int kDeckGroupH =
|
||||||
kDeckGroupPadY + kDeckCaptionH + kDeckCaptionGap + kDeckCellH + kDeckGroupPadY;
|
kDeckGroupPadY + kDeckCaptionH + kDeckCaptionGap + kDeckCellH + kDeckGroupPadY;
|
||||||
|
|
||||||
|
// --- The deck's width budget at the editor's floor ------------------------------------
|
||||||
|
// DECLARATIONS of budget, not measurements: nothing here is computed from a descriptor, and a
|
||||||
|
// group inventory that overruns one is what fails. sample_bands' kEditorMinWidth is derived
|
||||||
|
// from the first two — kDeckRowBlockW + kDeckGroupGap + kDeckSpanningW + 2*kPad — and the
|
||||||
|
// identity is asserted in test_deck_groups.cpp rather than coded, so the allocator keeps no
|
||||||
|
// include edge to this header.
|
||||||
|
inline constexpr int kDeckRowBlockW = 1020; // the block both categorical rows justify inside
|
||||||
|
inline constexpr int kDeckSpanningW = 142; // the right-anchored spanning deck, outside the block
|
||||||
|
// The hard ceiling the floor may not exceed lives beside the floor itself, in sample_bands.h's
|
||||||
|
// kEditorCeilingWidth — a window fact, not a deck one. Today's gap between the two is 90px,
|
||||||
|
// the whole width budget for the life of this layout (asserted in test_deck_groups.cpp) — see
|
||||||
|
// instrument-control-surface.md §1.6 before spending any of it.
|
||||||
|
|
||||||
// A two-segment compact toggle (always 2 segments — the Mono/Stereo grammar). id -1 = absent.
|
// A two-segment compact toggle (always 2 segments — the Mono/Stereo grammar). id -1 = absent.
|
||||||
struct DeckToggleDesc {
|
struct DeckToggleDesc {
|
||||||
int id = -1; // shell control id returned by the hit-test; -1 = no toggle
|
int id = -1; // shell control id returned by the hit-test; -1 = no toggle
|
||||||
|
|||||||
@@ -16,9 +16,21 @@ inline constexpr int kPad = 8;
|
|||||||
// exactly this, and there is no scroll, so anything smaller pushes the deck band off the
|
// exactly this, and there is no scroll, so anything smaller pushes the deck band off the
|
||||||
// window bottom (computeSampleBands' waveform-floor-wins degrade). Growing is fine — the
|
// window bottom (computeSampleBands' waveform-floor-wins degrade). Growing is fine — the
|
||||||
// waveform band is the elastic one. Both the enforced minimum and the opening rect read this.
|
// waveform band is the elastic one. Both the enforced minimum and the opening rect read this.
|
||||||
inline constexpr int kEditorMinWidth = 980;
|
//
|
||||||
|
// The width is a LITERAL here on purpose, though it is derived from knob_deck's width budget:
|
||||||
|
// this allocator is deliberately independent of the deck (it takes deckHeight as a parameter
|
||||||
|
// for exactly that reason), so the derivation is asserted in test_deck_groups.cpp — the one
|
||||||
|
// place that already includes both headers — rather than coded as an include edge.
|
||||||
|
inline constexpr int kEditorMinWidth = 1190;
|
||||||
inline constexpr int kEditorMinHeight = 680;
|
inline constexpr int kEditorMinHeight = 680;
|
||||||
|
|
||||||
|
// The hard ceiling the floor above may not exceed; the window itself still grows freely above
|
||||||
|
// it. A window fact, sibling of kEditorMinWidth/kEditorMinHeight, not a deck one — moved here
|
||||||
|
// from knob_deck.h for that reason. The gap to the floor (today: 90px) is the deck's whole
|
||||||
|
// width budget, spent once; the identity is asserted in test_deck_groups.cpp, the one place
|
||||||
|
// that already includes both this header and knob_deck.h.
|
||||||
|
inline constexpr int kEditorCeilingWidth = 1280;
|
||||||
|
|
||||||
// Chrome band: the toolbar row (title + nav) stacked over the control row (piano strip,
|
// Chrome band: the toolbar row (title + nav) stacked over the control row (piano strip,
|
||||||
// preview, velocity knob, channel toggle). sample_chrome partitions it.
|
// preview, velocity knob, channel toggle). sample_chrome partitions it.
|
||||||
inline constexpr int kTitleHeight = 26;
|
inline constexpr int kTitleHeight = 26;
|
||||||
|
|||||||
+103
-19
@@ -3,7 +3,8 @@
|
|||||||
// descriptors the Sample face carries: the signal-flow group order (pitch -> filter -> amp),
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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() {
|
static void testWrappedDeckHeightAtTheEditorFloorWidth() {
|
||||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
||||||
// At the floor (== default) 980 the deck takes three rows: PITCH + PITCH ENV + FILTER fill
|
// An UPPER BOUND, not an equality. The greedy whole-group wrap is still what decides row
|
||||||
// the first (950 of the 964 available — fourteen px of headroom, so one more FILTER cell
|
// membership until the reflow replaces it with the categorical partition, and at this width
|
||||||
// would wrap the group and reflow everything under it), FILTER ENV + AMP + VELOCITY the
|
// it happens to pack two ragged rows with the wrong composition. Bounding it is a real
|
||||||
// second, VOICE + MASTER the third. Two rows cannot hold the eight groups in ANY order at
|
// regression canary — a third row would cost the waveform 112 px again — without turning a
|
||||||
// this width: 1978 px of group plus 72 px of gaps against a 1928 px two-row capacity.
|
// wrap outcome into a claim.
|
||||||
CHECK(deckRowCount(g, kAvailAtMinWidth) == 3);
|
const int rows = deckRowCount(g, kAvailAtMinWidth);
|
||||||
CHECK(deckHeight(g, kAvailAtMinWidth) == 3 * kDeckGroupH + 2 * kDeckRowGap);
|
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
|
// Whole groups only, never split: every group's box lies inside the available width or is
|
||||||
// the first of its row.
|
// the first of its row.
|
||||||
@@ -265,8 +267,13 @@ static void testDeckFitsInsideTheEnforcedMinimumWindow() {
|
|||||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(mode);
|
const std::vector<DeckGroupDesc> g = sampleDeckGroups(mode);
|
||||||
const int h = deckHeight(g, kAvailAtMinWidth);
|
const int h = deckHeight(g, kAvailAtMinWidth);
|
||||||
const SampleBands b = computeSampleBands(kEditorMinWidth, kEditorMinHeight, h);
|
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);
|
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
|
// 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
|
// 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
|
// 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() {
|
static void testHitTestResolvesTheNewFilterControls() {
|
||||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
||||||
const DeckLayout dl = layoutDeck(g, kPad, 40, kAvailAtMinWidth);
|
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
|
// 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.
|
// 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
|
// Gate is the common face and its group widths are what the width budget is spent against:
|
||||||
// assignment at the floor so a later edit anywhere in the deck cannot reflow it silently.
|
// 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.)
|
// (Measured from the shipped descriptors, not copied out of a failing run.) The WRAP row a
|
||||||
static void testGateModeWidthsAndRowAssignmentAreUnchanged() {
|
// 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 std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
||||||
const struct { int id; int width; int row; } want[] = {
|
const struct { int id; int width; } want[] = {
|
||||||
{kGroupPitch, 150, 0}, {kGroupPitchEnv, 252, 0}, {kGroupFilter, 524, 0},
|
{kGroupPitch, 150}, {kGroupPitchEnv, 252}, {kGroupFilter, 524},
|
||||||
{kGroupFilterEnv, 312, 1}, {kGroupAmpEnv, 312, 1}, {kGroupVelocity, 192, 1},
|
{kGroupFilterEnv, 312}, {kGroupAmpEnv, 312}, {kGroupVelocity, 192},
|
||||||
{kGroupVoice, 164, 2}, {kGroupMaster, 72, 2},
|
{kGroupVoice, 164}, {kGroupMaster, 72},
|
||||||
};
|
};
|
||||||
CHECK(g.size() == sizeof(want) / sizeof(want[0]));
|
CHECK(g.size() == sizeof(want) / sizeof(want[0]));
|
||||||
const DeckLayout dl = layoutDeck(g, kPad, 0, kAvailAtMinWidth);
|
const DeckLayout dl = layoutDeck(g, kPad, 0, kAvailAtMinWidth);
|
||||||
@@ -584,7 +662,10 @@ static void testGateModeWidthsAndRowAssignmentAreUnchanged() {
|
|||||||
CHECK(dl.groups[i].id == want[i].id);
|
CHECK(dl.groups[i].id == want[i].id);
|
||||||
CHECK(deckGroupWidth(g[i]) == want[i].width);
|
CHECK(deckGroupWidth(g[i]) == want[i].width);
|
||||||
CHECK(dl.groups[i].box.width == 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.
|
// 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);
|
for (const DeckCellLayout& c : dl.groups[i].cells) CHECK(c.cell.width == kDeckCellW);
|
||||||
}
|
}
|
||||||
@@ -669,8 +750,11 @@ int main() {
|
|||||||
testWrappedDeckHeightAtTheEditorFloorWidth();
|
testWrappedDeckHeightAtTheEditorFloorWidth();
|
||||||
testDeckFitsInsideTheEnforcedMinimumWindow();
|
testDeckFitsInsideTheEnforcedMinimumWindow();
|
||||||
testNoFaceLeavesSlackWhereItsDroppedControlsWere();
|
testNoFaceLeavesSlackWhereItsDroppedControlsWere();
|
||||||
testGateModeWidthsAndRowAssignmentAreUnchanged();
|
testGateModeGroupWidthsAreUnchanged();
|
||||||
testGateSplineGateRoundTripsToTheSameLayout();
|
testGateSplineGateRoundTripsToTheSameLayout();
|
||||||
|
testTheEditorFloorIsDerivedFromTheDeckWidthBudget();
|
||||||
|
testEveryDeckGroupBelongsToExactlyOneRow();
|
||||||
|
testTheContourRowAndTheSpanningDeckFitTheBudget();
|
||||||
testHitTestResolvesTheNewFilterControls();
|
testHitTestResolvesTheNewFilterControls();
|
||||||
testBipolarKnobLawRoundTripsAndIsExactAtCentre();
|
testBipolarKnobLawRoundTripsAndIsExactAtCentre();
|
||||||
if (g_fail == 0) std::printf("deck_groups: all tests passed\n");
|
if (g_fail == 0) std::printf("deck_groups: all tests passed\n");
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
// octave boundaries and the 0..127 extremes; keyRect tiling and black-over-white overlap;
|
// octave boundaries and the 0..127 extremes; keyRect tiling and black-over-white overlap;
|
||||||
// keyAtPoint resolving black-over-white by zone and missing off-band; the root affordance's
|
// keyAtPoint resolving black-over-white by zone and missing off-band; the root affordance's
|
||||||
// hit-to-marker round trip; resolveDragNote clamping a wandering pointer; noteName under the
|
// hit-to-marker round trip; resolveDragNote clamping a wandering pointer; noteName under the
|
||||||
// C4 (MIDI 60) DAW convention; and the gutter pinned at the shipped default window size.
|
// C4 (MIDI 60) DAW convention; the gutter at the sawtooth's maximum residue; and the gutter
|
||||||
|
// pinned at the shipped default window size (derived from kEditorMinWidth/kEditorMinHeight).
|
||||||
//
|
//
|
||||||
// Client-pixel-only guarantee: every width swept below is a CLIENT-pixel width. Nothing in
|
// Client-pixel-only guarantee: every width swept below is a CLIENT-pixel width. Nothing in
|
||||||
// the instrument implements IPlugViewContentScaleSupport, so if a host scales the plugin
|
// the instrument implements IPlugViewContentScaleSupport, so if a host scales the plugin
|
||||||
@@ -280,28 +281,18 @@ static void testNoteNamesFollowTheC4Convention() {
|
|||||||
CHECK(noteName(500) == "G9");
|
CHECK(noteName(500) == "G9");
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- the gutter at the shipped default window size -----------------------------
|
// --- the gutter at the sawtooth's maximum residue -------------------------------
|
||||||
|
|
||||||
// The rule-based sweep above pins `margins == w % 75` and `leftMargin == margins/2` at every
|
// The rule-based sweep above pins `margins == w % 75` and `leftMargin == margins/2` at every
|
||||||
// width, but pins no concrete number — Daniel is making a visual call on the specific gutter
|
// width, but pins no concrete number. The strip is a sawtooth with period kStripWhiteKeyCount
|
||||||
// at the shipped default, and neither kPad nor the 840 default is covered by another test
|
// (75) px of window width; 840 is an arbitrary but independently useful sample because it
|
||||||
// firing if either ever changes. The strip is a sawtooth with period kStripWhiteKeyCount (75)
|
// lands on residue 74 — the cycle's maximum: one pixel of resize (840->841) collapses both
|
||||||
// px of window width, and the shipped 840 default lands on residue 74 — the cycle's maximum:
|
// gutters to zero and grows every white key from 10px to 11px. Kept as a synthetic worst-case
|
||||||
// one pixel of resize (840->841) collapses both gutters to zero and grows every white key
|
// sample; NOT tied to any shipped window size (see testGutterAtTheShippedDefaultWindowSize
|
||||||
// from 10px to 11px.
|
// below for that).
|
||||||
static void testGutterAtTheShippedDefaultWindowSize() {
|
static void testGutterAtTheSawtoothMaximumResidueWidth() {
|
||||||
constexpr int kShippedDefaultWindowW = 840; // editor_session.cpp's ViewRect default
|
constexpr int kSawtoothMaxResidueStripW = 824; // an arbitrary width landing on residue 74
|
||||||
constexpr int kShippedDefaultWindowH = 620; // editor_session.cpp's ViewRect default
|
const StripLayout L = layoutStrip(kSawtoothMaxResidueStripW, 30);
|
||||||
// Derive rootStrip's width the same way the shell does, through the real allocator +
|
|
||||||
// chrome layout, rather than re-deriving the inset formula — so a change to either one
|
|
||||||
// fails this test instead of silently moving the shipped gutter out from under it.
|
|
||||||
const SampleBands bands =
|
|
||||||
computeSampleBands(kShippedDefaultWindowW, kShippedDefaultWindowH, 0);
|
|
||||||
const ChromeRects chrome = chromeRects(bands.chrome, /*knobSize=*/24);
|
|
||||||
const int stripW = chrome.rootStrip.width;
|
|
||||||
CHECK(stripW == 824);
|
|
||||||
|
|
||||||
const StripLayout L = layoutStrip(stripW, 30);
|
|
||||||
CHECK(L.whiteWidth == 10);
|
CHECK(L.whiteWidth == 10);
|
||||||
const int margins = L.band.width - L.keys.width;
|
const int margins = L.band.width - L.keys.width;
|
||||||
CHECK(margins == 74);
|
CHECK(margins == 74);
|
||||||
@@ -309,6 +300,31 @@ static void testGutterAtTheShippedDefaultWindowSize() {
|
|||||||
CHECK(leftMargin == 37);
|
CHECK(leftMargin == 37);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- the gutter at the shipped default window size -----------------------------
|
||||||
|
|
||||||
|
// Daniel is making a visual call on the specific gutter at the shipped default; neither kPad
|
||||||
|
// nor the floor is covered by another test firing if either ever changes. Derived from
|
||||||
|
// kEditorMinWidth/kEditorMinHeight (editor_session.cpp's ViewRect default IS the floor) rather
|
||||||
|
// than a hardcoded window size, so a floor change fails HERE instead of silently moving the
|
||||||
|
// shipped gutter out from under it. The numbers below are today's floor (1190x680); re-derive
|
||||||
|
// them by hand if the floor ever moves.
|
||||||
|
static void testGutterAtTheShippedDefaultWindowSize() {
|
||||||
|
// Derive rootStrip's width the same way the shell does, through the real allocator +
|
||||||
|
// chrome layout, rather than re-deriving the inset formula.
|
||||||
|
const SampleBands bands = computeSampleBands(kEditorMinWidth, kEditorMinHeight, 0);
|
||||||
|
const ChromeRects chrome = chromeRects(bands.chrome, /*knobSize=*/24);
|
||||||
|
const int stripW = chrome.rootStrip.width;
|
||||||
|
CHECK(stripW == kEditorMinWidth - 2 * kPad);
|
||||||
|
CHECK(stripW == 1174);
|
||||||
|
|
||||||
|
const StripLayout L = layoutStrip(stripW, 30);
|
||||||
|
CHECK(L.whiteWidth == 15);
|
||||||
|
const int margins = L.band.width - L.keys.width;
|
||||||
|
CHECK(margins == 49);
|
||||||
|
const int leftMargin = L.keys.x - L.band.x;
|
||||||
|
CHECK(leftMargin == 24);
|
||||||
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
testLayoutFillsTheBandAndCentresTheKeys();
|
testLayoutFillsTheBandAndCentresTheKeys();
|
||||||
testDegenerateSizesYieldNoKeys();
|
testDegenerateSizesYieldNoKeys();
|
||||||
@@ -325,6 +341,7 @@ int main() {
|
|||||||
testHitTestingAKeyMarksThatSameKey();
|
testHitTestingAKeyMarksThatSameKey();
|
||||||
testDragTracksThePointerAndClampsWhenItWanders();
|
testDragTracksThePointerAndClampsWhenItWanders();
|
||||||
testNoteNamesFollowTheC4Convention();
|
testNoteNamesFollowTheC4Convention();
|
||||||
|
testGutterAtTheSawtoothMaximumResidueWidth();
|
||||||
testGutterAtTheShippedDefaultWindowSize();
|
testGutterAtTheShippedDefaultWindowSize();
|
||||||
|
|
||||||
if (g_fail == 0) {
|
if (g_fail == 0) {
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
// three-band vertical inventory (chrome over waveform over decks, no overlap, no
|
// three-band vertical inventory (chrome over waveform over decks, no overlap, no
|
||||||
// inversion) asserted as pure geometry with no paint call; the waveform band's two-lane
|
// inversion) asserted as pure geometry with no paint call; the waveform band's two-lane
|
||||||
// floor and the bands-clip-rather-than-squeeze rule on a short window; the deck band's
|
// floor and the bands-clip-rather-than-squeeze rule on a short window; the deck band's
|
||||||
// bottom anchor and its exact requested height; and the lane split (mono = one full-band
|
// bottom anchor and its exact requested height; the same stack/anchor/degrade properties
|
||||||
// lane, stereo = two lanes with the seam gap between them).
|
// re-anchored AT the allocator's own kEditorMinWidth/kEditorMinHeight floor rather than only
|
||||||
|
// below it; and the lane split (mono = one full-band lane, stereo = two lanes with the seam
|
||||||
|
// gap between them).
|
||||||
|
|
||||||
#include "../src/core/instrument/ui/sample_bands.h"
|
#include "../src/core/instrument/ui/sample_bands.h"
|
||||||
|
|
||||||
@@ -104,6 +106,56 @@ static void testDegenerateWindowYieldsNoInvertedRects() {
|
|||||||
CHECK(tiny.decks.right() >= tiny.decks.x);
|
CHECK(tiny.decks.right() >= tiny.decks.x);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- the allocator at its own floor (kEditorMinWidth x kEditorMinHeight) -------
|
||||||
|
|
||||||
|
// The allocator's own client-area floor is a fact IT owns (kEditorMinWidth/kEditorMinHeight
|
||||||
|
// above); the fixtures above validate the general shape entirely below that floor (840x620,
|
||||||
|
// 840x160). These anchor the same properties AT the floor itself, so a floor move that broke
|
||||||
|
// the stack there would have nothing else in this file to catch it.
|
||||||
|
|
||||||
|
static void testBandsStackWithoutOverlapAtTheEditorFloor() {
|
||||||
|
const SampleBands b = computeSampleBands(kEditorMinWidth, kEditorMinHeight, 120);
|
||||||
|
CHECK(b.chrome.y == 0);
|
||||||
|
CHECK(b.chrome.width == kEditorMinWidth);
|
||||||
|
CHECK(b.chrome.height == kTitleHeight + kChromeRowHeight);
|
||||||
|
CHECK(b.waveform.y >= b.chrome.bottom());
|
||||||
|
CHECK(b.decks.y >= b.waveform.bottom());
|
||||||
|
CHECK(b.waveform.x == kPad && b.waveform.right() == kEditorMinWidth - kPad);
|
||||||
|
CHECK(b.decks.x == kPad && b.decks.right() == kEditorMinWidth - kPad);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testDeckBandIsBottomAnchoredAtTheEditorFloor() {
|
||||||
|
constexpr int deckH = 120;
|
||||||
|
const SampleBands b = computeSampleBands(kEditorMinWidth, kEditorMinHeight, deckH);
|
||||||
|
CHECK(b.decks.height == deckH);
|
||||||
|
CHECK(b.decks.bottom() == kEditorMinHeight - kPad);
|
||||||
|
}
|
||||||
|
|
||||||
|
// At a representative two-row deck height (216px — the ceiling test_deck_groups.cpp bounds
|
||||||
|
// the wrapped deck to), the waveform gets exactly what the floor's own height leaves it: an
|
||||||
|
// equality, not a bound, so a floor-height change that quietly ate into the waveform's slack
|
||||||
|
// would fail here rather than only widen/narrow a `>=`.
|
||||||
|
static void testWaveformGetsExactlyTheFloorsRemainingHeightAtATwoRowDeck() {
|
||||||
|
constexpr int twoRowDeckH = 216;
|
||||||
|
const SampleBands b = computeSampleBands(kEditorMinWidth, kEditorMinHeight, twoRowDeckH);
|
||||||
|
const int expected = kEditorMinHeight - kPad - twoRowDeckH - 2 * kBandGap -
|
||||||
|
(kTitleHeight + kChromeRowHeight);
|
||||||
|
CHECK(b.waveform.height == expected);
|
||||||
|
CHECK(b.waveform.height == 358);
|
||||||
|
CHECK(b.waveform.height >= kWaveformMinHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The floor is not immune to the degrade path: a deck grown too tall for the floor's OWN
|
||||||
|
// height still hits the floor-wins rule instead of squeezing the waveform below its usable
|
||||||
|
// minimum — the sub-floor fixtures above cover the general rule; this is the same rule
|
||||||
|
// exercised at the allocator's own minimum width.
|
||||||
|
static void testAnOversizedDeckAtTheFloorStillDegradesGracefully() {
|
||||||
|
const SampleBands b = computeSampleBands(kEditorMinWidth, kEditorMinHeight, 500);
|
||||||
|
CHECK(b.waveform.height == kWaveformMinHeight);
|
||||||
|
CHECK(b.decks.y >= b.waveform.bottom());
|
||||||
|
CHECK(b.decks.bottom() > kEditorMinHeight); // clipped below the floor's own window
|
||||||
|
}
|
||||||
|
|
||||||
// --- the waveform band's lanes ------------------------------------------------
|
// --- the waveform band's lanes ------------------------------------------------
|
||||||
|
|
||||||
static void testMonoUsesOneFullBandLane() {
|
static void testMonoUsesOneFullBandLane() {
|
||||||
@@ -149,6 +201,10 @@ int main() {
|
|||||||
testWaveformNeverShrinksBelowTheTwoLaneFloor();
|
testWaveformNeverShrinksBelowTheTwoLaneFloor();
|
||||||
testTwoLaneFloorHoldsTwoUsableLanes();
|
testTwoLaneFloorHoldsTwoUsableLanes();
|
||||||
testDegenerateWindowYieldsNoInvertedRects();
|
testDegenerateWindowYieldsNoInvertedRects();
|
||||||
|
testBandsStackWithoutOverlapAtTheEditorFloor();
|
||||||
|
testDeckBandIsBottomAnchoredAtTheEditorFloor();
|
||||||
|
testWaveformGetsExactlyTheFloorsRemainingHeightAtATwoRowDeck();
|
||||||
|
testAnOversizedDeckAtTheFloorStillDegradesGracefully();
|
||||||
testMonoUsesOneFullBandLane();
|
testMonoUsesOneFullBandLane();
|
||||||
testStereoSplitsIntoTwoLanesWithTheSeamGap();
|
testStereoSplitsIntoTwoLanesWithTheSeamGap();
|
||||||
testStereoOddRemainderGoesToTheUpperLane();
|
testStereoOddRemainderGoesToTheUpperLane();
|
||||||
|
|||||||
Reference in New Issue
Block a user