Cut core/instrument/ui comment bloat ~49% (comments only, zero code change)
This commit is contained in:
@@ -1,9 +1,8 @@
|
|||||||
// browser_scroll.cpp — see browser_scroll.h. PURE scroll + search geometry over the S10
|
// browser_scroll.cpp — see browser_scroll.h. Pure scroll + search geometry; no host types.
|
||||||
// capture_browser. No host types; only the shared Rect + BrowserLayout.
|
|
||||||
|
|
||||||
#include "core/instrument/ui/browser_scroll.h"
|
#include "core/instrument/ui/browser_scroll.h"
|
||||||
|
|
||||||
#include "core/instrument/ui/editor_geometry.h" // kPad / kTitleHeight / kNavButtonWidth (Q-W2v hoist)
|
#include "core/instrument/ui/editor_geometry.h" // kPad / kTitleHeight / kNavButtonWidth
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
@@ -50,11 +49,10 @@ VisibleRange visibleCardRange(const BrowserLayout& layout, int cardCount, int of
|
|||||||
return vr;
|
return vr;
|
||||||
}
|
}
|
||||||
if (offset < 0) offset = 0;
|
if (offset < 0) offset = 0;
|
||||||
// First visible ROW: the topmost row whose bottom edge is below the offset. Floor so a row
|
// First row: floored so a row partially scrolled off the top still draws. Last row:
|
||||||
// partially scrolled off the top still draws (its lower part is visible).
|
// the row containing pixel (offset + gridH - 1), +1 for the exclusive end, so a row
|
||||||
|
// straddling the bottom edge still draws.
|
||||||
const int firstRow = offset / kBrowserCardHeight;
|
const int firstRow = offset / kBrowserCardHeight;
|
||||||
// Last visible ROW: the row containing the pixel (offset + gridH - 1), inclusive; +1 for
|
|
||||||
// the exclusive end. A row straddling the bottom edge still draws.
|
|
||||||
const int lastRow = (offset + gridH - 1) / kBrowserCardHeight + 1;
|
const int lastRow = (offset + gridH - 1) / kBrowserCardHeight + 1;
|
||||||
int first = firstRow * columns;
|
int first = firstRow * columns;
|
||||||
int last = lastRow * columns;
|
int last = lastRow * columns;
|
||||||
@@ -84,13 +82,11 @@ Rect scrollThumbRect(const BrowserLayout& layout, int cardCount, int offset) {
|
|||||||
const int trackLeft = trackRight - kScrollbarWidth;
|
const int trackLeft = trackRight - kScrollbarWidth;
|
||||||
const int trackTop = layout.grid.y;
|
const int trackTop = layout.grid.y;
|
||||||
|
|
||||||
// Thumb height proportional to the visible fraction, floored at a grabbable minimum but
|
// Thumb height proportional to the visible fraction, floored/capped to the track.
|
||||||
// never taller than the track.
|
|
||||||
int thumbH = static_cast<int>(static_cast<long long>(gridH) * gridH / content);
|
int thumbH = static_cast<int>(static_cast<long long>(gridH) * gridH / content);
|
||||||
thumbH = (std::max)(kMinThumbHeight, thumbH);
|
thumbH = (std::max)(kMinThumbHeight, thumbH);
|
||||||
thumbH = (std::min)(thumbH, gridH);
|
thumbH = (std::min)(thumbH, gridH);
|
||||||
|
|
||||||
// Thumb top proportional to the offset over the movable track span.
|
|
||||||
const int trackSpan = gridH - thumbH; // >= 0
|
const int trackSpan = gridH - thumbH; // >= 0
|
||||||
int thumbTop = trackTop;
|
int thumbTop = trackTop;
|
||||||
if (maxOff > 0 && trackSpan > 0) {
|
if (maxOff > 0 && trackSpan > 0) {
|
||||||
@@ -106,7 +102,7 @@ int thumbDragToOffset(const BrowserLayout& layout, int cardCount, int startOffse
|
|||||||
const int gridH = (std::max)(0, layout.grid.height);
|
const int gridH = (std::max)(0, layout.grid.height);
|
||||||
if (content <= gridH || gridH <= 0) return clampScrollOffset(layout, cardCount, startOffset);
|
if (content <= gridH || gridH <= 0) return clampScrollOffset(layout, cardCount, startOffset);
|
||||||
|
|
||||||
// Thumb height (same formula as scrollThumbRect) -> movable track span in thumb pixels.
|
// Same thumb-height formula as scrollThumbRect.
|
||||||
int thumbH = static_cast<int>(static_cast<long long>(gridH) * gridH / content);
|
int thumbH = static_cast<int>(static_cast<long long>(gridH) * gridH / content);
|
||||||
thumbH = (std::max)(kMinThumbHeight, thumbH);
|
thumbH = (std::max)(kMinThumbHeight, thumbH);
|
||||||
thumbH = (std::min)(thumbH, gridH);
|
thumbH = (std::min)(thumbH, gridH);
|
||||||
@@ -157,8 +153,6 @@ std::vector<int> filterNameIndices(const std::vector<std::string>& names,
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Browse-modal regions (hoisted from the editor shell, Q-W2v/T2-06 — body verbatim;
|
|
||||||
// the band metrics come from editor_geometry, the search height from searchBoxRect).
|
|
||||||
BrowseModal computeBrowseModal(int w, int h) {
|
BrowseModal computeBrowseModal(int w, int h) {
|
||||||
constexpr int kBrowseFooterH = 30;
|
constexpr int kBrowseFooterH = 30;
|
||||||
BrowseModal m;
|
BrowseModal m;
|
||||||
|
|||||||
@@ -1,23 +1,16 @@
|
|||||||
// browser_scroll.h — PURE scroll + type-to-filter geometry LAYERED over the S10
|
// browser_scroll.h — scroll + type-to-filter geometry layered over capture_browser. Mirror
|
||||||
// capture_browser. NO VST3, NO REAPER, NO SWELL/LICE types at the boundary. The mirror of
|
// of capture_browser/editor_geometry; the shell draws the clipped card window, scrollbar,
|
||||||
// capture_browser / editor_geometry: the fiddly scroll-window + scrollbar-thumb + search-box
|
// and search field, and routes wheel/drag/keystrokes into these functions.
|
||||||
// arithmetic lives here, unit-tested outside the DAW, while the editor shell draws the
|
|
||||||
// clipped card window + the scrollbar + the search field and routes wheel/drag/keystrokes
|
|
||||||
// into these functions.
|
|
||||||
//
|
//
|
||||||
// WHY IT EXISTS (S12). capture_browser (S10) lays out EVERY card top-down and the shell
|
// capture_browser lays out every card top-down and the shell clips at the browser bottom —
|
||||||
// clips at the browser bottom — a bank longer than the panel runs off with no way to reach
|
// a bank longer than the panel has no way to reach the rest. This module adds scroll (a
|
||||||
// it (the S12 gap). This module adds the two things S12 layers over that stable geometry:
|
// vertical pixel offset with max-offset clamp, visible-row window, scrollbar thumb, and
|
||||||
// * SCROLL — a vertical pixel offset into the card grid, with the max-offset clamp, the
|
// thumb-drag<->offset mapping) and search (a case-insensitive name-substring filter that
|
||||||
// visible-row window, a scrollbar thumb rect, and the thumb-drag<->offset mapping so a
|
// composes with capture_browser's bank filter — the shell applies the bank filter first,
|
||||||
// wheel tick or a thumb drag reaches every card; and
|
// then this search narrows within it).
|
||||||
// * SEARCH — a name-substring filter (case-insensitive) that narrows the drawn cards,
|
|
||||||
// COMPOSING with capture_browser's bank filter (the shell applies the bank filter first,
|
|
||||||
// then this search narrows within it) + the search-box rect the shell draws the field in.
|
|
||||||
//
|
//
|
||||||
// It holds NO card data and draws nothing — it knows only the browser layout (from
|
// Holds no card data and draws nothing — knows only the browser layout, counts, and the
|
||||||
// capture_browser), COUNTS, and the scroll OFFSET the shell owns as transient UI state. It
|
// scroll offset the shell owns as transient UI state.
|
||||||
// reuses capture_browser's BrowserLayout + the shared Rect (one geometry idiom).
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -28,96 +21,74 @@
|
|||||||
|
|
||||||
namespace reasampler::instrument::ui {
|
namespace reasampler::instrument::ui {
|
||||||
|
|
||||||
// The width (px) of the vertical scrollbar gutter at the right edge of the grid. The shell
|
// Width of the vertical scrollbar gutter at the grid's right edge. When content fits (no
|
||||||
// draws the track + thumb here and hit-tests thumb grabs against scrollThumbRect. Exposed so
|
// scroll needed), scrollThumbRect returns empty and the shell may reclaim the gutter.
|
||||||
// the shell and tests agree. When the content fits (no scroll needed) the scrollbar is
|
|
||||||
// suppressed (scrollThumbRect returns empty) and the shell may reclaim the gutter.
|
|
||||||
inline constexpr int kScrollbarWidth = 10;
|
inline constexpr int kScrollbarWidth = 10;
|
||||||
|
|
||||||
// The height (px) of the type-to-filter search box the shell draws ABOVE the tab strip (a
|
// Height of the type-to-filter search box the shell draws above the tab strip.
|
||||||
// thin band spanning the browser width). Exposed so the shell reserves the band and tests
|
// capture_browser's tab strip + grid sit below this band.
|
||||||
// agree. capture_browser's tab strip + grid sit BELOW this band (the shell offsets the
|
|
||||||
// BrowserLayout it feeds to capture_browser by kSearchBoxHeight).
|
|
||||||
inline constexpr int kSearchBoxHeight = 22;
|
inline constexpr int kSearchBoxHeight = 22;
|
||||||
|
|
||||||
// The total pixel HEIGHT the card grid needs to draw all `cardCount` cards at `layout`'s
|
// Total pixel height the card grid needs for `cardCount` cards at `layout`'s column
|
||||||
// column count: the number of ROWS (ceil(cardCount / columns)) times the fixed cell height.
|
// count: rows (ceil(cardCount / columns)) times the fixed cell height.
|
||||||
// Zero cards -> 0. Pure — the content extent the scroll offset ranges over.
|
|
||||||
int scrollContentHeight(const BrowserLayout& layout, int cardCount);
|
int scrollContentHeight(const BrowserLayout& layout, int cardCount);
|
||||||
|
|
||||||
// The maximum scroll offset (px): content height minus the visible grid height, floored at 0.
|
// Maximum scroll offset: content height minus visible grid height, floored at 0.
|
||||||
// When the content fits within the grid this is 0 (nothing to scroll). Pure — the clamp
|
|
||||||
// ceiling for every offset the shell tracks.
|
|
||||||
int scrollMaxOffset(const BrowserLayout& layout, int cardCount);
|
int scrollMaxOffset(const BrowserLayout& layout, int cardCount);
|
||||||
|
|
||||||
// Clamp a proposed scroll offset into [0, scrollMaxOffset]. The shell clamps after every wheel
|
// Clamps a proposed scroll offset into [0, scrollMaxOffset].
|
||||||
// tick / thumb drag so an over-scroll pins to an edge rather than showing past the last card
|
|
||||||
// or above the first. Pure.
|
|
||||||
int clampScrollOffset(const BrowserLayout& layout, int cardCount, int proposedOffset);
|
int clampScrollOffset(const BrowserLayout& layout, int cardCount, int proposedOffset);
|
||||||
|
|
||||||
// The half-open range of card INDICES [first, last) at least partially visible in the grid at
|
// Half-open range of card indices [first, last) at least partially visible at scroll
|
||||||
// scroll `offset`. The shell draws only these cards (the S12 clip window) rather than every
|
// `offset` (assumed pre-clamped). The shell draws only these cards.
|
||||||
// card. `offset` is assumed pre-clamped (the shell clamps on input); a first past the last row
|
|
||||||
// yields an empty range (first==last==cardCount). Pure.
|
|
||||||
struct VisibleRange {
|
struct VisibleRange {
|
||||||
int first = 0; // first card index drawn (inclusive)
|
int first = 0;
|
||||||
int last = 0; // one past the last card index drawn (exclusive)
|
int last = 0;
|
||||||
};
|
};
|
||||||
VisibleRange visibleCardRange(const BrowserLayout& layout, int cardCount, int offset);
|
VisibleRange visibleCardRange(const BrowserLayout& layout, int cardCount, int offset);
|
||||||
|
|
||||||
// The cell rect of card `index` SHIFTED UP by the scroll offset, ready to draw (the shell
|
// Cell rect of card `index` shifted up by the scroll offset (the shell still adds the
|
||||||
// still adds the browser sub-area origin). Equivalent to capture_browser::cardCellRect with
|
// browser sub-area origin).
|
||||||
// the offset subtracted from top/bottom. Pure — the one place the offset applies to a card.
|
|
||||||
Rect scrolledCardCellRect(const BrowserLayout& layout, int index, int offset);
|
Rect scrolledCardCellRect(const BrowserLayout& layout, int index, int offset);
|
||||||
|
|
||||||
// The vertical scrollbar THUMB rect within the grid's right-edge gutter, sized proportional to
|
// Vertical scrollbar thumb rect within the grid's right-edge gutter, sized proportional
|
||||||
// the visible fraction (grid height / content height) and positioned proportional to the
|
// to the visible fraction and positioned proportional to the scroll offset. Empty when
|
||||||
// scroll offset. Returns an EMPTY rect when the content fits (no scroll needed) — the shell
|
// the content fits. A minimum thumb height keeps a tiny thumb grabbable on a long bank.
|
||||||
// suppresses the scrollbar then. A minimum thumb height keeps a tiny thumb grabbable on a very
|
|
||||||
// long bank. Pure — the geometry the shell draws + hit-tests the thumb grab against.
|
|
||||||
Rect scrollThumbRect(const BrowserLayout& layout, int cardCount, int offset);
|
Rect scrollThumbRect(const BrowserLayout& layout, int cardCount, int offset);
|
||||||
|
|
||||||
// Map a thumb-drag to a scroll offset. Given the offset the thumb held at grab time
|
// Maps a thumb-drag to a new (clamped) scroll offset: `startOffset` shifted by the pixel
|
||||||
// (`startOffset`) and the vertical pixel delta since grab (`dyPixels`), returns the new
|
// delta scaled from thumb-track pixels to content pixels. A degenerate track or fitting
|
||||||
// (clamped) scroll offset: startOffset shifted by the delta scaled from thumb-track pixels to
|
// content pins to startOffset.
|
||||||
// content pixels (a 1px thumb move covers content/track px of content). A degenerate track /
|
|
||||||
// fitting content pins to startOffset. Pure — the inverse of scrollThumbRect's position map.
|
|
||||||
int thumbDragToOffset(const BrowserLayout& layout, int cardCount, int startOffset, int dyPixels);
|
int thumbDragToOffset(const BrowserLayout& layout, int cardCount, int startOffset, int dyPixels);
|
||||||
|
|
||||||
// The search-box rect: a full-width band of height kSearchBoxHeight at the TOP of the browser
|
// Search-box rect: full-width band of height kSearchBoxHeight at the top of the browser
|
||||||
// area (above where capture_browser's tab strip draws). `w` is the browser sub-area width;
|
// area. `w` is the browser sub-area width; the shell adds its origin.
|
||||||
// the shell adds its origin. A zero/negative width yields an empty rect. Pure.
|
|
||||||
Rect searchBoxRect(int w);
|
Rect searchBoxRect(int w);
|
||||||
|
|
||||||
// True iff `name` contains `query` as a case-insensitive ASCII substring. An EMPTY query
|
// True iff `name` contains `query` as a case-insensitive ASCII substring. An empty query
|
||||||
// matches everything (the no-filter identity). Matching is ASCII case-folded (the display
|
// matches everything.
|
||||||
// names are ASCII until the Phase L type kit lands, mirroring the editor's other ASCII-only
|
|
||||||
// text). Pure — the single match predicate the shell's search narrow is built from.
|
|
||||||
bool nameMatchesQuery(const std::string& name, const std::string& query);
|
bool nameMatchesQuery(const std::string& name, const std::string& query);
|
||||||
|
|
||||||
// Narrow a list of display `names` to the INDICES whose name matches `query`, preserving
|
// Narrows a list of display `names` to the indices whose name matches `query`, preserving
|
||||||
// order. An EMPTY query returns every index [0, names.size()) (the composition base so "bank
|
// order. An empty query returns every index. Kept name-only (indices, not card structs)
|
||||||
// filter, no search" == today's browser). Kept name-only (indices, not card structs) so this
|
// so this module stays free of the sample_map/bank_book chain — the shell applies the
|
||||||
// module stays free of the sample_map/bank_book chain — the shell owns the SampleChoice list
|
// bank filter first, then feeds the surviving display names here.
|
||||||
// and applies the bank filter FIRST, then feeds the surviving display names here (search
|
|
||||||
// narrows within the bank). Pure.
|
|
||||||
std::vector<int> filterNameIndices(const std::vector<std::string>& names,
|
std::vector<int> filterNameIndices(const std::vector<std::string>& names,
|
||||||
const std::string& query);
|
const std::string& query);
|
||||||
|
|
||||||
// --- The Browse-modal (S-VIEW-5) top-level regions (Q-W2v hoist, T2-06) -------
|
// --- Browse-modal top-level regions --------------------------------------------
|
||||||
//
|
//
|
||||||
// A title band with a Back button, the search box, the browser sub-area (tabs + card
|
// A title band with a Back button, the search box, the browser sub-area (tabs + card
|
||||||
// grid — layoutBrowser's origin), and a footer with Cancel / Load-confirm. The picker
|
// grid — layoutBrowser's origin), and a footer with Cancel / Load-confirm. The picker
|
||||||
// covers the full window (F3: full-window overlay). Draw + hit-test both derive from
|
// covers the full window. Homed here (not editor_geometry) because the search-box
|
||||||
// this single layout so they never drift. Homed here (not editor_geometry) because the
|
// height feeds it.
|
||||||
// search-box height feeds it — browser_scroll already owns the search/scroll geometry.
|
|
||||||
struct BrowseModal {
|
struct BrowseModal {
|
||||||
Rect title;
|
Rect title;
|
||||||
Rect back; // the "Back" title-band button
|
Rect back;
|
||||||
Rect search; // the type-to-filter box (absolute)
|
Rect search;
|
||||||
Rect content; // the browser sub-area (tabs + grid) — layoutBrowser's origin
|
Rect content; // browser sub-area (tabs + grid) — layoutBrowser's origin
|
||||||
Rect cancel; // footer Cancel
|
Rect cancel;
|
||||||
Rect confirm; // footer Load (confirm)
|
Rect confirm;
|
||||||
};
|
};
|
||||||
BrowseModal computeBrowseModal(int w, int h);
|
BrowseModal computeBrowseModal(int w, int h);
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ namespace reasampler::instrument::ui {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// The left edge of tab i in a strip of the given x-origin and width divided into `count`
|
// Left edge of tab i in a strip divided into `count` equal segments (mirror of
|
||||||
// equal segments (mirror of mode_switch::segmentEdge). Every boundary derives from the same
|
// mode_switch::segmentEdge). Same formula for every boundary so consecutive tabs share
|
||||||
// formula, so consecutive tabs share an exact edge and the last tab reaches x+width exactly.
|
// an exact edge.
|
||||||
int tabEdge(int x, int width, int i, int count) {
|
int tabEdge(int x, int width, int i, int count) {
|
||||||
return x + (i * width) / count;
|
return x + (i * width) / count;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,92 +1,67 @@
|
|||||||
// capture_browser.h — PURE layout + hit-test for the S10 capture-first editor's default
|
// capture_browser.h — layout + hit-test for the capture-first editor's default face: a
|
||||||
// face: a scannable grid of capture CARDS with a bank-FILTER tab strip above it. NO VST3,
|
// scannable grid of capture cards with a bank-filter tab strip above it. Mirror of
|
||||||
// NO REAPER, NO SWELL/LICE types at the boundary. The mirror of editor_geometry /
|
// editor_geometry/embed_strip/mode_switch; the shell draws thumbnails/names/badges and
|
||||||
// embed_strip / mode_switch: the fiddly card-grid + tab arithmetic lives here so it is
|
// routes clicks into these functions.
|
||||||
// unit-tested outside the DAW, while the editor shell draws each card's peak thumbnail +
|
|
||||||
// name + root/key badge and routes clicks into these functions.
|
|
||||||
//
|
//
|
||||||
// The browser replaces the old text item-list (the named anti-pattern). It lays out N
|
// This module knows only counts and rects — it draws nothing and holds no sample data;
|
||||||
// cards in a fixed-cell grid that wraps across the browser width, and a horizontal tab
|
// the shell owns the SampleChoice list, peak envelopes, and filter state.
|
||||||
// strip of bank filters (one tab per bank_book bank + an "All" tab) above the grid. This
|
|
||||||
// module knows only COUNTS and RECTS — it draws nothing and holds no sample data; the
|
|
||||||
// shell owns the SampleChoice list, the peak envelopes, and the filter state, and asks this
|
|
||||||
// module only "where does card i draw" / "what did the user click".
|
|
||||||
//
|
//
|
||||||
// Scroll is NOT here (S12 layers it over this module). The browser lays out every card
|
// Scroll is layered on top by browser_scroll — this module lays out every card top-down
|
||||||
// top-down; the shell clips at the browser's bottom until S12 adds a scroll offset. Keeping
|
// and the shell clips at the bottom until a scroll offset is applied.
|
||||||
// scroll out keeps this module the stable card/tab geometry S12 builds on.
|
|
||||||
//
|
|
||||||
// It reuses the same Rect + contains() as editor_geometry (one shared geometry idiom).
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "core/instrument/ui/editor_geometry.h" // Rect, contains — one shared geometry idiom
|
#include "core/instrument/ui/editor_geometry.h" // Rect, contains
|
||||||
|
|
||||||
namespace reasampler::instrument::ui {
|
namespace reasampler::instrument::ui {
|
||||||
|
|
||||||
// Fixed browser metrics, exposed so the shell and tests agree. The card is sized to show a
|
// Fixed browser metrics, exposed so the shell and tests agree.
|
||||||
// peak thumbnail with a name + badge line under it — scannable by eye, not a dense list.
|
inline constexpr int kBrowserTabHeight = 26;
|
||||||
inline constexpr int kBrowserTabHeight = 26; // the bank-filter tab strip band height
|
inline constexpr int kBrowserCardWidth = 132;
|
||||||
inline constexpr int kBrowserCardWidth = 132; // one card cell width (incl. gutter)
|
inline constexpr int kBrowserCardHeight = 84;
|
||||||
inline constexpr int kBrowserCardHeight = 84; // one card cell height (incl. gutter)
|
inline constexpr int kBrowserCardGutter = 8;
|
||||||
inline constexpr int kBrowserCardGutter = 8; // inset between the cell edge and the card
|
inline constexpr int kBrowserThumbHeight = 44;
|
||||||
inline constexpr int kBrowserThumbHeight = 44; // the peak-thumbnail band inside a card
|
|
||||||
|
|
||||||
// The browser's regions, derived from the (w x h) area the shell allots it. Both clamp to
|
// Clamped so a degenerate (tiny/zero) size never yields an inverted rect.
|
||||||
// the area so a degenerate (tiny/zero) size never yields an inverted rect.
|
|
||||||
struct BrowserLayout {
|
struct BrowserLayout {
|
||||||
Rect tabStrip; // top: the bank-filter tabs
|
Rect tabStrip;
|
||||||
Rect grid; // below the tabs: where the capture cards tile
|
Rect grid;
|
||||||
int columns = 1; // cards per row in `grid` (>= 1); derived from grid.width
|
int columns = 1; // cards per row in `grid` (>= 1)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Divide a (w x h) browser area into its regions and compute the column count. Pure: same
|
// Divide a (w x h) browser area into its regions and compute the column count. columns =
|
||||||
// inputs -> same layout. The tab strip takes a fixed height at the top (clamped so it never
|
// max(1, grid.width/cardWidth) so a browser narrower than one card still lays out a
|
||||||
// exceeds the area); the grid takes the rest. columns = max(1, grid.width/cardWidth) so a
|
// single column.
|
||||||
// browser narrower than one card still lays out a single column. A zero/negative size
|
|
||||||
// yields empty rects + columns==1.
|
|
||||||
BrowserLayout layoutBrowser(int w, int h);
|
BrowserLayout layoutBrowser(int w, int h);
|
||||||
|
|
||||||
// The cell rect of capture card `index` (0-based) in the grid, laid out left-to-right then
|
// Cell rect of capture card `index` (0-based), left-to-right then top-to-bottom across
|
||||||
// top-to-bottom across `columns`. This is the full CELL (card + gutter); cardContentRect
|
// `columns`. This is the full cell (card + gutter); cardContentRect insets it.
|
||||||
// insets it to the drawable card. Rows past the visible grid are still computed (the shell
|
|
||||||
// clips at paint time). A negative index yields an empty rect. Pure.
|
|
||||||
Rect cardCellRect(const BrowserLayout& layout, int index);
|
Rect cardCellRect(const BrowserLayout& layout, int index);
|
||||||
|
|
||||||
// The drawable card rect inside a cell: the cell inset by kBrowserCardGutter on all sides.
|
// Drawable card rect inside a cell: the cell inset by kBrowserCardGutter on all sides.
|
||||||
// The shell fills this (background + border) and draws the thumbnail/name/badge inside it. Pure.
|
|
||||||
Rect cardContentRect(const BrowserLayout& layout, int index);
|
Rect cardContentRect(const BrowserLayout& layout, int index);
|
||||||
|
|
||||||
// The peak-thumbnail sub-rect at the top of a card's content: full card width, the top
|
// Peak-thumbnail sub-rect at the top of a card's content: full card width, the top
|
||||||
// kBrowserThumbHeight (clamped to the card height). The shell draws the envelope here; the
|
// kBrowserThumbHeight (clamped to the card height).
|
||||||
// name + badge go in the remaining strip below. Pure.
|
|
||||||
Rect cardThumbnailRect(const BrowserLayout& layout, int index);
|
Rect cardThumbnailRect(const BrowserLayout& layout, int index);
|
||||||
|
|
||||||
// The name/badge sub-rect below the thumbnail: the card content minus the thumbnail band.
|
// Name/badge sub-rect below the thumbnail.
|
||||||
// The shell draws the display name + root/key badge here. Pure.
|
|
||||||
Rect cardLabelRect(const BrowserLayout& layout, int index);
|
Rect cardLabelRect(const BrowserLayout& layout, int index);
|
||||||
|
|
||||||
// The card a click at (x, y) lands on, given `cardCount` cards, or -1 for a click outside
|
// Card a click at (x, y) lands on, given `cardCount` cards, or -1 for a miss. Only the
|
||||||
// every card (in a gutter, past the last card, or on the tab strip). Only the card CONTENT
|
// card content rect counts as a hit — a click in the inter-card gutter is a miss.
|
||||||
// rect counts as a hit — a click in the inter-card gutter is a miss. Pure.
|
|
||||||
int cardHitTest(const BrowserLayout& layout, int cardCount, int x, int y);
|
int cardHitTest(const BrowserLayout& layout, int cardCount, int x, int y);
|
||||||
|
|
||||||
// --- Bank-filter tabs --------------------------------------------------------
|
// --- Bank-filter tabs ---------------------------------------------------------
|
||||||
//
|
//
|
||||||
// The tab strip divides tabStrip into `tabCount` equal segments (mirror of mode_switch):
|
// Divides tabStrip into `tabCount` equal segments: one tab per bank plus a leading "All"
|
||||||
// one tab per bank_book bank plus a leading "All" tab the shell prepends, so tabCount ==
|
// tab the shell prepends. This module only divides the strip + hit-tests; the shell
|
||||||
// bankCount + 1 in practice. This module only divides the strip + hit-tests; the shell
|
// supplies labels and tracks the active tab.
|
||||||
// supplies the labels and tracks which tab is active. A tab click narrows the card list to
|
|
||||||
// that bank (the shell filters its SampleChoice list before laying out cards).
|
|
||||||
|
|
||||||
// The rect of tab `index` (0-based) when the strip is divided into `tabCount` equal
|
// Rect of tab `index` when the strip is divided into `tabCount` equal segments. The last
|
||||||
// segments. The last tab absorbs any width remainder so the tabs tile the whole strip with
|
// tab absorbs any width remainder so the tabs tile the whole strip with no gap.
|
||||||
// no gap (mirror of mode_switch's segment split). A negative index or tabCount<=0 yields an
|
|
||||||
// empty rect. Pure.
|
|
||||||
Rect filterTabRect(const BrowserLayout& layout, int tabCount, int index);
|
Rect filterTabRect(const BrowserLayout& layout, int tabCount, int index);
|
||||||
|
|
||||||
// The tab a click at (x, y) lands on, given `tabCount` tabs, or -1 for a click outside the
|
|
||||||
// tab strip. Pure.
|
|
||||||
int filterTabHitTest(const BrowserLayout& layout, int tabCount, int x, int y);
|
int filterTabHitTest(const BrowserLayout& layout, int tabCount, int x, int y);
|
||||||
|
|
||||||
} // namespace reasampler::instrument::ui
|
} // namespace reasampler::instrument::ui
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
// curve_popup.h — PURE sheet geometry + dismissal test for the r11 velocity-curve popup
|
// curve_popup.h — sheet geometry + dismissal test for the velocity-curve popup editor.
|
||||||
// editor (Wave B, FB1). NO VST3, NO REAPER, NO SWELL/LICE types at the boundary. The mirror
|
// Mirror of overflow_menu; the shell draws through the L1 kit and routes clicks via
|
||||||
// of overflow_menu: the size-clamp / centering / title-row arithmetic lives here, unit-tested
|
// these rects.
|
||||||
// at the clamps outside the DAW, while the editor shell draws the wash + sheet through the
|
|
||||||
// L1 kit and routes clicks (close / curve box / outside-sheet dismiss) via these rects.
|
|
||||||
//
|
//
|
||||||
// THE POPUP (CONTEXT.md §S-VIEW r11). Summoned by the mini curve-preview button, a CENTERED
|
// A centered sheet over the Sample face (a lighter wash than Browse's, since this is a
|
||||||
// SHEET over the Sample face (a 0.50-alpha bg/base wash behind it — lighter than Browse's
|
// focused sub-editor, not a view change): width/height each clamp to a fraction of the
|
||||||
// 0.82; a focused sub-editor, not a view change): width clamp(60% of window, 360..520),
|
// window within min/max bounds. A title row sits over the curve box. The curve box rect
|
||||||
// height clamp(55% of window, 260..380). Inside: a ~22px title row ("VELOCITY -> AMP"
|
// here is the border rect — the shell derives the mapping box via its curveBoxFromRect
|
||||||
// micro-caps left, an 18x18 Close button right) over the full-size curve box filling the
|
// formula, so the popup editor and the Zone-panel inline editor share coordinates.
|
||||||
// remainder. The curve box rect here is the BORDER rect — the shell derives the mapping box
|
|
||||||
// through its ONE curveBoxFromRect formula (the landed inset grammar), so the popup editor
|
|
||||||
// and the Zone-panel inline editor share coordinates by construction.
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -19,7 +14,7 @@
|
|||||||
|
|
||||||
namespace reasampler::instrument::ui {
|
namespace reasampler::instrument::ui {
|
||||||
|
|
||||||
// Fixed popup metrics (spec r11), exposed so the shell and tests agree.
|
// Fixed popup metrics, exposed so the shell and tests agree.
|
||||||
inline constexpr int kCurvePopupMinW = 360;
|
inline constexpr int kCurvePopupMinW = 360;
|
||||||
inline constexpr int kCurvePopupMaxW = 520;
|
inline constexpr int kCurvePopupMaxW = 520;
|
||||||
inline constexpr int kCurvePopupMinH = 260;
|
inline constexpr int kCurvePopupMinH = 260;
|
||||||
@@ -29,20 +24,19 @@ inline constexpr int kCurvePopupCloseSize = 18;
|
|||||||
inline constexpr int kCurvePopupPad = 8; // sheet inner padding (title inset + box margins)
|
inline constexpr int kCurvePopupPad = 8; // sheet inner padding (title inset + box margins)
|
||||||
|
|
||||||
struct CurvePopupLayout {
|
struct CurvePopupLayout {
|
||||||
Rect sheet; // the bg/panel sheet, centered in the window
|
Rect sheet;
|
||||||
Rect title; // the caption text rect (left part of the title row)
|
Rect title;
|
||||||
Rect close; // the 18x18 Close (x) button, right-anchored in the title row
|
Rect close; // Close (x) button, right-anchored in the title row
|
||||||
Rect curveBox; // the full-size curve editor BORDER rect (shell insets via curveBoxFromRect)
|
Rect curveBox; // full-size curve editor border rect (shell insets via curveBoxFromRect)
|
||||||
};
|
};
|
||||||
|
|
||||||
// The popup geometry for a (w x h) window: sheet width clamp(60% w, 360..520) and height
|
// Popup geometry for a (w x h) window: sheet width clamp(60% w, min..max) and height
|
||||||
// clamp(55% h, 260..380) — each additionally capped at the window dimension so a degenerate
|
// clamp(55% h, min..max), each additionally capped at the window dimension so a
|
||||||
// window never yields an overhanging sheet — centered; title row + close button at the top;
|
// degenerate window never yields an overhanging sheet — centered.
|
||||||
// the curve box filling the remainder inside kCurvePopupPad margins. Pure.
|
|
||||||
CurvePopupLayout computeCurvePopup(int w, int h);
|
CurvePopupLayout computeCurvePopup(int w, int h);
|
||||||
|
|
||||||
// True when (x, y) lands OUTSIDE the sheet (on the wash) — the click-outside dismissal test.
|
// True when (x, y) lands outside the sheet (on the wash) — the click-outside dismissal
|
||||||
// The shell additionally gates on "no drag in flight" (spec). Pure.
|
// test. The shell additionally gates on "no drag in flight".
|
||||||
bool popupOutsideSheet(const CurvePopupLayout& layout, int x, int y);
|
bool popupOutsideSheet(const CurvePopupLayout& layout, int x, int y);
|
||||||
|
|
||||||
} // namespace reasampler::instrument::ui
|
} // namespace reasampler::instrument::ui
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ namespace reasampler::instrument::ui {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Spike editor layout constants. These are the editor's fixed metrics; the real
|
|
||||||
// editor (S4/S5) will parameterize as its content demands.
|
|
||||||
constexpr int kTitleBarHeight = 28;
|
constexpr int kTitleBarHeight = 28;
|
||||||
constexpr int kButtonMargin = 10;
|
constexpr int kButtonMargin = 10;
|
||||||
constexpr int kButtonWidth = 120;
|
constexpr int kButtonWidth = 120;
|
||||||
@@ -17,26 +15,18 @@ constexpr int kButtonHeight = 24;
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// contains() now lives with the shared ui::Rect (core/ui/rect.h) — same half-open
|
|
||||||
// semantics, re-exported through the header's using-declaration.
|
|
||||||
|
|
||||||
EditorLayout layoutEditor(int w, int h) {
|
EditorLayout layoutEditor(int w, int h) {
|
||||||
// Clamp the surface to non-negative extents so a degenerate view can't produce
|
// Clamp to non-negative extents so a degenerate view can't produce inverted rects.
|
||||||
// inverted rects.
|
|
||||||
const int cw = std::max(0, w);
|
const int cw = std::max(0, w);
|
||||||
const int ch = std::max(0, h);
|
const int ch = std::max(0, h);
|
||||||
|
|
||||||
EditorLayout out;
|
EditorLayout out;
|
||||||
|
|
||||||
// Title bar spans the top, clamped so it never exceeds the client height.
|
|
||||||
const int titleH = std::min(kTitleBarHeight, ch);
|
const int titleH = std::min(kTitleBarHeight, ch);
|
||||||
out.titleBar = Rect::ltrb(0, 0, cw, titleH);
|
out.titleBar = Rect::ltrb(0, 0, cw, titleH);
|
||||||
|
|
||||||
// Canvas is everything below the title bar.
|
|
||||||
out.canvas = Rect::ltrb(0, titleH, cw, ch);
|
out.canvas = Rect::ltrb(0, titleH, cw, ch);
|
||||||
|
|
||||||
// Button sits at the top-left of the canvas, inset by a margin, and is clamped to
|
// Button inset from the canvas top-left, clamped so it never overhangs a small view.
|
||||||
// fit inside the canvas so it never overhangs on a small view.
|
|
||||||
const int bx = out.canvas.x + kButtonMargin;
|
const int bx = out.canvas.x + kButtonMargin;
|
||||||
const int by = out.canvas.y + kButtonMargin;
|
const int by = out.canvas.y + kButtonMargin;
|
||||||
const int bRight = std::min(bx + kButtonWidth, out.canvas.right());
|
const int bRight = std::min(bx + kButtonWidth, out.canvas.right());
|
||||||
@@ -59,15 +49,11 @@ Rect sampleRowRect(const EditorLayout& layout, int index) {
|
|||||||
|
|
||||||
int sampleRowHitTest(const EditorLayout& layout, int rowCount, int x, int y) {
|
int sampleRowHitTest(const EditorLayout& layout, int rowCount, int x, int y) {
|
||||||
if (rowCount <= 0) return -1;
|
if (rowCount <= 0) return -1;
|
||||||
// Must be within the canvas horizontally and at/below its top.
|
|
||||||
if (x < layout.canvas.x || x >= layout.canvas.right()) return -1;
|
if (x < layout.canvas.x || x >= layout.canvas.right()) return -1;
|
||||||
if (y < layout.canvas.y) return -1;
|
if (y < layout.canvas.y) return -1;
|
||||||
// Clip at the canvas bottom: clicks in the canvas's dead-zone below the last
|
|
||||||
// visible row agree with sampleRowRect, which does not clamp rows to canvas.bottom().
|
|
||||||
if (y >= layout.canvas.bottom()) return -1;
|
if (y >= layout.canvas.bottom()) return -1;
|
||||||
const int index = (y - layout.canvas.y) / kSampleRowHeight;
|
const int index = (y - layout.canvas.y) / kSampleRowHeight;
|
||||||
if (index < 0 || index >= rowCount) return -1;
|
if (index < 0 || index >= rowCount) return -1;
|
||||||
// Guard the bottom edge: a click below the last row's bottom is outside.
|
|
||||||
const Rect r = sampleRowRect(layout, index);
|
const Rect r = sampleRowRect(layout, index);
|
||||||
if (y >= r.bottom()) return -1;
|
if (y >= r.bottom()) return -1;
|
||||||
return index;
|
return index;
|
||||||
@@ -80,9 +66,6 @@ KeymapEditorLayout layoutKeymapEditor(int w, int h) {
|
|||||||
out.base = layoutEditor(w, h);
|
out.base = layoutEditor(w, h);
|
||||||
const Rect& canvas = out.base.canvas;
|
const Rect& canvas = out.base.canvas;
|
||||||
|
|
||||||
// Split the canvas vertically: the left column is the bank-sample list, the right
|
|
||||||
// column (1/kZonePanelFraction of the width) is the zone panel. Guard tiny widths so
|
|
||||||
// the split point never crosses the canvas edges.
|
|
||||||
const int canvasW = std::max(0, canvas.width);
|
const int canvasW = std::max(0, canvas.width);
|
||||||
const int splitW = canvasW / kZonePanelFraction; // width of the zone panel
|
const int splitW = canvasW / kZonePanelFraction; // width of the zone panel
|
||||||
const int splitX = std::max(canvas.x, canvas.right() - splitW);
|
const int splitX = std::max(canvas.x, canvas.right() - splitW);
|
||||||
@@ -90,13 +73,11 @@ KeymapEditorLayout layoutKeymapEditor(int w, int h) {
|
|||||||
out.sampleList = Rect::ltrb(canvas.x, canvas.y, splitX, canvas.bottom());
|
out.sampleList = Rect::ltrb(canvas.x, canvas.y, splitX, canvas.bottom());
|
||||||
out.zonePanel = Rect::ltrb(splitX, canvas.y, canvas.right(), canvas.bottom());
|
out.zonePanel = Rect::ltrb(splitX, canvas.y, canvas.right(), canvas.bottom());
|
||||||
|
|
||||||
// "Add Zone" button spans the top of the zone panel, clamped to its height.
|
|
||||||
const int addH = std::min(kAddZoneHeight, std::max(0, out.zonePanel.height));
|
const int addH = std::min(kAddZoneHeight, std::max(0, out.zonePanel.height));
|
||||||
out.addZoneButton =
|
out.addZoneButton =
|
||||||
Rect::ltrb(out.zonePanel.x, out.zonePanel.y, out.zonePanel.right(),
|
Rect::ltrb(out.zonePanel.x, out.zonePanel.y, out.zonePanel.right(),
|
||||||
out.zonePanel.y + addH);
|
out.zonePanel.y + addH);
|
||||||
|
|
||||||
// Zone rows stack below the button.
|
|
||||||
out.zoneRowArea = Rect::ltrb(out.zonePanel.x, out.addZoneButton.bottom(),
|
out.zoneRowArea = Rect::ltrb(out.zonePanel.x, out.addZoneButton.bottom(),
|
||||||
out.zonePanel.right(), out.zonePanel.bottom());
|
out.zonePanel.right(), out.zonePanel.bottom());
|
||||||
return out;
|
return out;
|
||||||
@@ -138,10 +119,8 @@ ZoneHit zoneHitTest(const KeymapEditorLayout& layout, int zoneCount, int x, int
|
|||||||
const Rect row = zoneRowRect(layout, index);
|
const Rect row = zoneRowRect(layout, index);
|
||||||
if (y >= row.bottom()) return ZoneHit{};
|
if (y >= row.bottom()) return ZoneHit{};
|
||||||
|
|
||||||
// Seven mini-buttons pinned to the right edge, right-to-left:
|
// Seven mini-buttons pinned to the right edge, each kZoneCtrlWidth wide, in slot
|
||||||
// delete, root+, root-, high+, high-, low+, low-
|
// order 0..6; a click left of the leftmost is the label ("select").
|
||||||
// Each is kZoneCtrlWidth wide. A click left of the leftmost is the label ("select").
|
|
||||||
// The fields laid out LEFT-TO-RIGHT in slot order 0..6.
|
|
||||||
const ZoneField fields[7] = {
|
const ZoneField fields[7] = {
|
||||||
ZoneField::kLowDown, ZoneField::kLowUp, ZoneField::kHighDown,
|
ZoneField::kLowDown, ZoneField::kLowUp, ZoneField::kHighDown,
|
||||||
ZoneField::kHighUp, ZoneField::kRootDown, ZoneField::kRootUp,
|
ZoneField::kHighUp, ZoneField::kRootDown, ZoneField::kRootUp,
|
||||||
@@ -159,35 +138,25 @@ bool addZoneHitTest(const KeymapEditorLayout& layout, int x, int y) {
|
|||||||
return contains(layout.addZoneButton, x, y);
|
return contains(layout.addZoneButton, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- r11 Sample / Zone face layout (Q-W2v hoist, T2-06) ----------------------
|
|
||||||
// Bodies moved verbatim from the reasampler_editor shell (behavior-identical); the
|
|
||||||
// only signature change is clusterRects' `knobSize` parameter (formerly knob_deck's
|
|
||||||
// kDeckKnobSize read directly — passed in so this module stays knob_deck-free).
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Fixed band metrics (formerly the editor shell's anon-ns constants).
|
constexpr int kHeroMinHeight = 150; // elastic hero's floor
|
||||||
constexpr int kHeroMinHeight = 150; // the elastic hero's floor (r11)
|
|
||||||
constexpr int kClusterHeight = 52; // root strip + preview + vel knob + curve btn + channel toggle
|
constexpr int kClusterHeight = 52; // root strip + preview + vel knob + curve btn + channel toggle
|
||||||
constexpr int kStripBandHeight = 40; // the keyboard-strip band height (root strip + zone strip)
|
constexpr int kStripBandHeight = 40; // keyboard-strip band height (root strip + zone strip)
|
||||||
|
|
||||||
// The r11 cluster's fixed right-anchored run (left -> right: Preview button, the radial
|
// Cluster's fixed right-anchored run: Preview button, vel knob cell, curve button, Mono|Stereo.
|
||||||
// preview-velocity knob cell, the mini curve-preview button, Mono|Stereo).
|
|
||||||
constexpr int kPreviewBtnW = 64;
|
constexpr int kPreviewBtnW = 64;
|
||||||
constexpr int kVelCellW = 48; // the Vel knob cell (deck cell grammar)
|
constexpr int kVelCellW = 48;
|
||||||
constexpr int kCurveBtnSize = 28; // the square curve-preview button
|
constexpr int kCurveBtnSize = 28;
|
||||||
|
|
||||||
// The S7 mono/stereo toggle segments.
|
|
||||||
constexpr int kChanSegW = 52;
|
constexpr int kChanSegW = 52;
|
||||||
constexpr int kChanSegH = 18;
|
constexpr int kChanSegH = 18;
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// r11 band order: title (fixed) -> hero (ELASTIC: absorbs all height left after the fixed
|
// Band order: title (fixed) -> hero (elastic, absorbs remaining height, floor
|
||||||
// bands, floor kHeroMinHeight) -> cluster (fixed) -> deck (fixed height `deckH`, bottom-
|
// kHeroMinHeight) -> cluster (fixed) -> deck (fixed height `deckH`, bottom-anchored). A
|
||||||
// anchored). When the window is too short for the floor (below the checkSizeConstraint
|
// window too short for the floor keeps the hero at its floor and clips lower bands.
|
||||||
// minimum — a defensive case), the hero keeps its floor and the lower bands clip past the
|
|
||||||
// window bottom gracefully.
|
|
||||||
SampleBands computeSampleBands(int w, int h, int deckH) {
|
SampleBands computeSampleBands(int w, int h, int deckH) {
|
||||||
SampleBands b;
|
SampleBands b;
|
||||||
const int titleH = (std::min)(kTitleHeight, h);
|
const int titleH = (std::min)(kTitleHeight, h);
|
||||||
@@ -214,7 +183,6 @@ SampleBands computeSampleBands(int w, int h, int deckH) {
|
|||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw + hit-test both derive from this ONE formula.
|
|
||||||
ClusterRects clusterRects(const Rect& cluster, const Rect& chanMono, int knobSize) {
|
ClusterRects clusterRects(const Rect& cluster, const Rect& chanMono, int knobSize) {
|
||||||
ClusterRects r;
|
ClusterRects r;
|
||||||
const int stripTop = cluster.y + (cluster.height - kStripBandHeight) / 2;
|
const int stripTop = cluster.y + (cluster.height - kStripBandHeight) / 2;
|
||||||
@@ -261,16 +229,14 @@ Rect zoneDeleteRect(const Rect& addR) {
|
|||||||
return Rect::ltrb(addR.right() + 8, addR.y, addR.right() + 8 + 64, addR.bottom());
|
return Rect::ltrb(addR.right() + 8, addR.y, addR.right() + 8 + 64, addR.bottom());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zone content sits below the "+ Add Zone" affordance (top+4, height 20) with a 12px
|
// Sits below the "+ Add Zone" affordance (top+4, height 20) with a 12px gap.
|
||||||
// gap, padded kPad horizontally. All call sites use this formula.
|
|
||||||
Rect zonesStripArea(const Rect& content) {
|
Rect zonesStripArea(const Rect& content) {
|
||||||
const int stripTop = content.y + 4 + 20 + 12; // addR.bottom() + 12
|
const int stripTop = content.y + 4 + 20 + 12; // addR.bottom() + 12
|
||||||
return Rect::ltrb(content.x + kPad, stripTop, content.right() - kPad,
|
return Rect::ltrb(content.x + kPad, stripTop, content.right() - kPad,
|
||||||
stripTop + kStripBandHeight);
|
stripTop + kStripBandHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anchored off zonesStripArea.bottom() so the legend top tracks the strip bottom
|
// Anchored off zonesStripArea.bottom() so the legend top tracks the strip bottom.
|
||||||
// without re-inlining the strip arithmetic here.
|
|
||||||
Rect noteEntryFieldsArea(const Rect& content) {
|
Rect noteEntryFieldsArea(const Rect& content) {
|
||||||
const int stripBottom = zonesStripArea(content).bottom();
|
const int stripBottom = zonesStripArea(content).bottom();
|
||||||
const int top = stripBottom + 8; // legendTop (== zonesStripArea.bottom() + 8)
|
const int top = stripBottom + 8; // legendTop (== zonesStripArea.bottom() + 8)
|
||||||
@@ -292,9 +258,8 @@ Rect zonesControlPanel(const Rect& content) {
|
|||||||
content.bottom() - 4);
|
content.bottom() - 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FB2 (R11-F2 parity): the deck lays out from the panel top (top-anchored), with a
|
// Top-anchored; reserves a column at the panel's right for the curve-preview button so
|
||||||
// column at the panel's right reserved for the mini curve-preview button so no deck row
|
// no deck row starts inside it.
|
||||||
// starts inside it.
|
|
||||||
Rect zonesDeckArea(const Rect& content) {
|
Rect zonesDeckArea(const Rect& content) {
|
||||||
const Rect panel = zonesControlPanel(content);
|
const Rect panel = zonesControlPanel(content);
|
||||||
return Rect::ltrb(panel.x, panel.y, panel.right() - kCurveBtnSize - kPad, panel.bottom());
|
return Rect::ltrb(panel.x, panel.y, panel.right() - kCurveBtnSize - kPad, panel.bottom());
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
// editor_geometry.h — PURE view geometry + hit-test for the VST3 IPlugView LICE
|
// editor_geometry.h — view geometry + hit-test for the VST3 IPlugView LICE editor. The
|
||||||
// editor (Phase S1). NO VST3, NO REAPER, NO SWELL/LICE types at the boundary.
|
// IPlugView shell owns window/bitmap/SWELL plumbing; the rectangle math and hit-testing
|
||||||
//
|
// live here so they can be unit-tested outside the DAW.
|
||||||
// The IPlugView shell (reasampler_editor.cpp) owns the window/bitmap/SWELL plumbing
|
|
||||||
// and is DAW-verified; this module holds the fiddly rectangle math and hit-testing so
|
|
||||||
// it can be unit-tested outside the DAW — the mirror of how bank_grid / mode_switch /
|
|
||||||
// tab_strip split their layout math out of the panel shell.
|
|
||||||
//
|
|
||||||
// The spike's editor is deliberately trivial (a title band + one clickable button),
|
|
||||||
// enough to PROVE the host->draw/hit-test event routing works. As the real editor
|
|
||||||
// (S4/S5) grows, its layout math accretes here, not in the shell.
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -16,101 +8,80 @@
|
|||||||
|
|
||||||
namespace reasampler::instrument::ui {
|
namespace reasampler::instrument::ui {
|
||||||
|
|
||||||
// The shared pixel rectangle + containment test (Q-W1, T2-05 ≡ T4-21): the former
|
|
||||||
// LTRB Rect defined here is folded into the ONE concrete ui::Rect (XYWH storage,
|
|
||||||
// right()/bottom() accessors, Rect::ltrb() for edge-wise construction, same
|
|
||||||
// half-open convention). Aliased here so every instrument-ui call site keeps its
|
|
||||||
// established `Rect` / `contains` spelling.
|
|
||||||
using Rect = ::reasampler::ui::Rect;
|
using Rect = ::reasampler::ui::Rect;
|
||||||
using ::reasampler::ui::contains;
|
using ::reasampler::ui::contains;
|
||||||
|
|
||||||
// The regions the spike editor draws, derived from the current view size. All are
|
// Title band + one button + remaining canvas, clamped so a degenerate (too-small) view
|
||||||
// clamped to the client area so a degenerate (too-small) view never yields a region
|
// never yields a region spilling outside the surface.
|
||||||
// that spills outside the surface.
|
|
||||||
struct EditorLayout {
|
struct EditorLayout {
|
||||||
Rect titleBar; // top band: the plugin name + a live-state readout
|
Rect titleBar;
|
||||||
Rect button; // a single clickable button (proves hit-test routing)
|
Rect button;
|
||||||
Rect canvas; // the remaining surface below the title bar
|
Rect canvas;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Divide a (w x h) client area into the spike editor's regions. Pure: the same
|
// Divide a (w x h) client area into the editor's top-level regions. Pure.
|
||||||
// inputs always yield the same layout. Guards tiny sizes — every returned rect stays
|
|
||||||
// within [0,w] x [0,h], and the button never overhangs the canvas.
|
|
||||||
EditorLayout layoutEditor(int w, int h);
|
EditorLayout layoutEditor(int w, int h);
|
||||||
|
|
||||||
// The editor's hit-test targets. kNone means the point landed on inert surface.
|
|
||||||
enum class HitTarget {
|
enum class HitTarget {
|
||||||
kNone,
|
kNone,
|
||||||
kButton,
|
kButton,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Classify a click at (x, y) against a layout. The button wins only when the point is
|
// Classify a click at (x, y) against a layout.
|
||||||
// inside the button rect; everything else (including the title bar and empty canvas)
|
|
||||||
// is kNone in the spike.
|
|
||||||
HitTarget hitTest(const EditorLayout& layout, int x, int y);
|
HitTarget hitTest(const EditorLayout& layout, int x, int y);
|
||||||
|
|
||||||
// --- Sample-selection list (S4 Tier-0 UI) -----------------------------------
|
// --- Sample-selection list ---------------------------------------------------
|
||||||
//
|
//
|
||||||
// The Tier-0 editor lists the bank's samples as a vertical stack of fixed-height rows
|
// A vertical stack of fixed-height rows below the title bar; clicking a row selects that
|
||||||
// below the title bar; clicking a row selects that sample. This is the pure geometry:
|
// sample. Pure geometry only — the shell draws names and routes the click.
|
||||||
// the row rectangles and the point->row hit-test, unit-tested outside the DAW while the
|
|
||||||
// shell draws the names and routes the click into the processor's reloadInstrument.
|
|
||||||
|
|
||||||
// The fixed row height (px) for one sample entry. Exposed so the shell and tests agree.
|
|
||||||
inline constexpr int kSampleRowHeight = 22;
|
inline constexpr int kSampleRowHeight = 22;
|
||||||
|
|
||||||
// The rectangle for row `index` (0-based) of the sample list, laid out top-down inside
|
// Rect for row `index` (0-based), laid out top-down inside the layout's canvas. Rows
|
||||||
// the layout's canvas. Rows beyond what the canvas can show are still computed (the
|
// beyond what the canvas can show are still computed (the shell clips at paint time); a
|
||||||
// shell clips at paint time); a negative index yields an empty rect. Pure.
|
// negative index yields an empty rect.
|
||||||
Rect sampleRowRect(const EditorLayout& layout, int index);
|
Rect sampleRowRect(const EditorLayout& layout, int index);
|
||||||
|
|
||||||
// The row index a click at (x, y) lands on, given `rowCount` rows, or -1 for a click
|
// Row index a click at (x, y) lands on given `rowCount` rows, or -1 for a click outside
|
||||||
// outside the list (above the first row, past the last, or on the title bar). Pure.
|
// the list.
|
||||||
int sampleRowHitTest(const EditorLayout& layout, int rowCount, int x, int y);
|
int sampleRowHitTest(const EditorLayout& layout, int rowCount, int x, int y);
|
||||||
|
|
||||||
// --- Keymap editor (S5 Tier-1 UI) -------------------------------------------
|
// --- Keymap editor ------------------------------------------------------------
|
||||||
//
|
//
|
||||||
// The Tier-1 editor splits the canvas into a LEFT bank-sample list (the same rows as
|
// Splits the canvas into a LEFT bank-sample list (the sample-selection rows above, reused
|
||||||
// Tier 0, reused for the "sample to add / fallback pick") and a RIGHT zone panel listing
|
// as the "sample to add / fallback pick") and a RIGHT zone panel listing the performance
|
||||||
// the performance map's zones. An "Add Zone" button sits at the top of the zone panel;
|
// map's zones. An "Add Zone" button sits at the top of the zone panel; each zone row
|
||||||
// each zone row carries small nudge/delete controls so the user can set the range and
|
// carries nudge/delete mini-buttons (LICE has no native numeric entry field).
|
||||||
// root note without a text field (LICE has no native numeric entry). All rectangle math
|
|
||||||
// is here so the shell only draws + routes — the mirror of the sample-list split above.
|
|
||||||
|
|
||||||
// Fixed metrics for the zone panel, exposed so the shell and tests agree.
|
|
||||||
inline constexpr int kZoneRowHeight = 24;
|
inline constexpr int kZoneRowHeight = 24;
|
||||||
inline constexpr int kZonePanelFraction = 2; // zone panel gets the RIGHT 1/2 of the canvas
|
inline constexpr int kZonePanelFraction = 2; // zone panel gets the RIGHT 1/2 of the canvas
|
||||||
inline constexpr int kZoneCtrlWidth = 20; // width of one nudge/delete mini-button
|
inline constexpr int kZoneCtrlWidth = 20; // width of one nudge/delete mini-button
|
||||||
inline constexpr int kAddZoneHeight = 22; // the "Add Zone" button band height
|
inline constexpr int kAddZoneHeight = 22; // "Add Zone" button band height
|
||||||
|
|
||||||
// The keymap editor's regions, derived from the (w x h) client area. All clamp to the
|
// Clamps every rect to the canvas so a degenerate view still yields in-bounds rects.
|
||||||
// canvas so a degenerate view yields in-bounds rects.
|
|
||||||
struct KeymapEditorLayout {
|
struct KeymapEditorLayout {
|
||||||
EditorLayout base; // title bar + canvas (the sample list uses base.canvas.x half)
|
EditorLayout base;
|
||||||
Rect sampleList; // LEFT column: the bank-sample rows (sampleRowRect is relative here)
|
Rect sampleList; // LEFT column
|
||||||
Rect zonePanel; // RIGHT column: the "Add Zone" button + the zone rows
|
Rect zonePanel; // RIGHT column
|
||||||
Rect addZoneButton; // top of the zone panel
|
Rect addZoneButton; // top of the zone panel
|
||||||
Rect zoneRowArea; // below addZoneButton: where zone rows stack
|
Rect zoneRowArea; // below addZoneButton
|
||||||
};
|
};
|
||||||
|
|
||||||
KeymapEditorLayout layoutKeymapEditor(int w, int h);
|
KeymapEditorLayout layoutKeymapEditor(int w, int h);
|
||||||
|
|
||||||
// The rectangle for bank-sample row `index` inside the LEFT sample list column of a
|
// Rect for bank-sample row `index` inside the LEFT column. Negative index -> empty.
|
||||||
// keymap layout. Same fixed height as the Tier-0 list; laid out top-down inside
|
|
||||||
// sampleList. Negative index -> empty. Pure.
|
|
||||||
Rect keymapSampleRowRect(const KeymapEditorLayout& layout, int index);
|
Rect keymapSampleRowRect(const KeymapEditorLayout& layout, int index);
|
||||||
|
|
||||||
// The bank-sample row a click lands on inside the left list, or -1 outside it. Pure.
|
// Bank-sample row a click lands on inside the left list, or -1 outside it.
|
||||||
int keymapSampleRowHitTest(const KeymapEditorLayout& layout, int rowCount, int x, int y);
|
int keymapSampleRowHitTest(const KeymapEditorLayout& layout, int rowCount, int x, int y);
|
||||||
|
|
||||||
// The rectangle for zone row `index` inside the zone panel's zoneRowArea. Negative
|
// Rect for zone row `index` inside zoneRowArea. Negative index -> empty.
|
||||||
// index -> empty. Pure.
|
|
||||||
Rect zoneRowRect(const KeymapEditorLayout& layout, int index);
|
Rect zoneRowRect(const KeymapEditorLayout& layout, int index);
|
||||||
|
|
||||||
// A zone row's interactive fields. The row is a horizontal strip: a label on the left,
|
// A zone row's interactive fields: a label on the left, then seven fixed-width
|
||||||
// then seven fixed-width mini-buttons on the right (left-to-right: low-, low+, high-, high+,
|
// mini-buttons on the right (low-, low+, high-, high+, root-, root+, delete). kZoneNone
|
||||||
// root-, root+, delete). kZoneNone means the click missed a control
|
// means the click missed a control (e.g. the label) — the shell may still treat that as
|
||||||
// (e.g. on the label) — the shell may still treat that as "select this zone".
|
// "select this zone".
|
||||||
enum class ZoneField {
|
enum class ZoneField {
|
||||||
kZoneNone,
|
kZoneNone,
|
||||||
kLowDown,
|
kLowDown,
|
||||||
@@ -122,101 +93,90 @@ enum class ZoneField {
|
|||||||
kDelete,
|
kDelete,
|
||||||
};
|
};
|
||||||
|
|
||||||
// The result of hit-testing a click against the zone rows: which zone row (or -1) and
|
// Which zone row (or -1) and which field within it a click landed on. A click on
|
||||||
// which field within it. A click on the "Add Zone" button is reported separately by
|
// "Add Zone" is reported separately by addZoneHitTest.
|
||||||
// addZoneHitTest — this covers only the zone rows.
|
|
||||||
struct ZoneHit {
|
struct ZoneHit {
|
||||||
int zoneIndex = -1;
|
int zoneIndex = -1;
|
||||||
ZoneField field = ZoneField::kZoneNone;
|
ZoneField field = ZoneField::kZoneNone;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Classify a click at (x, y) against `zoneCount` zone rows. Returns {-1, kZoneNone} for a
|
// Classify a click at (x, y) against `zoneCount` zone rows. {-1, kZoneNone} for a miss.
|
||||||
// click outside every zone row. Within a row, the seven mini-buttons occupy fixed-width
|
// Within a row, the seven mini-buttons occupy fixed-width slots on the right edge; a
|
||||||
// slots on the right edge (left-to-right: low-, low+, high-, high+, root-, root+, delete);
|
// click left of those slots is {index, kZoneNone} (the label area — "select").
|
||||||
// a click left of those slots is {index, kZoneNone} (the label area — "select"). Pure.
|
|
||||||
ZoneHit zoneHitTest(const KeymapEditorLayout& layout, int zoneCount, int x, int y);
|
ZoneHit zoneHitTest(const KeymapEditorLayout& layout, int zoneCount, int x, int y);
|
||||||
|
|
||||||
// True if (x, y) lands on the "Add Zone" button. Pure.
|
|
||||||
bool addZoneHitTest(const KeymapEditorLayout& layout, int x, int y);
|
bool addZoneHitTest(const KeymapEditorLayout& layout, int x, int y);
|
||||||
|
|
||||||
// --- r11 Sample / Zone face layout (Q-W2v hoist, T2-06) ----------------------
|
// --- Sample / Zone face layout ------------------------------------------------
|
||||||
//
|
//
|
||||||
// The capture-first editor's band/cluster/zone-surface layout math, hoisted out of the
|
// The capture-first editor's band/cluster/zone-surface layout math. Draw and hit-test
|
||||||
// reasampler_editor shell where it had accreted untestable (the §2 scope gap). Draw and
|
// both derive every rect from these formulas so they can never drift; the shell only
|
||||||
// hit-test both derive every rect from these ONE formulas so they can never drift; the
|
// draws + routes. The Browse-modal layout lives in browser_scroll (its search box
|
||||||
// shell only draws + routes. The Browse-modal layout lives in browser_scroll (its search
|
// height feeds it).
|
||||||
// box height feeds it — dependency-clean placement beside its scroll/search siblings).
|
|
||||||
|
|
||||||
// Shared band metrics (the shell's remaining direct uses: horizontal padding + the
|
|
||||||
// title-band height; everything else is internal to the layout functions below).
|
|
||||||
inline constexpr int kPad = 8;
|
inline constexpr int kPad = 8;
|
||||||
inline constexpr int kTitleHeight = 26;
|
inline constexpr int kTitleHeight = 26;
|
||||||
inline constexpr int kNavButtonWidth = 62; // Browse / Zone / Back title-band buttons
|
inline constexpr int kNavButtonWidth = 62; // Browse / Zone / Back title-band buttons
|
||||||
|
|
||||||
// The r11 Sample-face bands (top->bottom): a TITLE band (name + Browse/Zone nav), the
|
// Sample-face bands (top->bottom): TITLE (name + Browse/Zone nav), a full-width elastic
|
||||||
// FULL-WIDTH ELASTIC HERO (absorbs all height left after the fixed bands, floor
|
// HERO (absorbs all height left after the fixed bands, floored), the root+preview
|
||||||
// kHeroMinHeight), the ROOT + PREVIEW CLUSTER, and the bottom-anchored KNOB DECK
|
// CLUSTER, and the bottom-anchored knob DECK (height `deckH` from knob_deck's wrap). A
|
||||||
// (height `deckH` from the pure knob_deck wrap). When the window is too short for the
|
// window shorter than the hero floor clips the lower bands past the window bottom.
|
||||||
// hero floor (below the checkSizeConstraint minimum — defensive), the hero keeps its
|
|
||||||
// floor and the lower bands clip past the window bottom gracefully.
|
|
||||||
struct SampleBands {
|
struct SampleBands {
|
||||||
Rect title; // top: name + Browse/Zone nav buttons
|
Rect title;
|
||||||
Rect navBrowse; // the "Browse" title-band button
|
Rect navBrowse;
|
||||||
Rect navZone; // the "Zone" title-band button
|
Rect navZone;
|
||||||
Rect hero; // the FULL-WIDTH ELASTIC hero waveform + S-VIEW-3 envelope overlay
|
Rect hero; // waveform + envelope overlay
|
||||||
Rect cluster; // root strip + preview + vel knob + curve button + channel toggle
|
Rect cluster; // root strip + preview + vel knob + curve button + channel toggle
|
||||||
Rect deck; // the bottom-anchored knob deck (height from the pure knob_deck wrap)
|
Rect deck;
|
||||||
};
|
};
|
||||||
SampleBands computeSampleBands(int w, int h, int deckH);
|
SampleBands computeSampleBands(int w, int h, int deckH);
|
||||||
|
|
||||||
// The r11 cluster sub-rects: the root strip keeps the left side at REMAINDER width; the
|
// Cluster sub-rects: the root strip keeps the left side at remainder width; the right
|
||||||
// right side is the fixed-width right-anchored run (Preview 64 · Vel knob cell 48 · curve
|
// side is the fixed-width right-anchored run (Preview · vel knob cell · curve button ·
|
||||||
// preview button 28 · Mono|Stereo). `knobSize` is the deck knob square (knob_deck's
|
// Mono|Stereo). `knobSize` is the deck knob square, passed in so this module does not
|
||||||
// kDeckKnobSize — passed in so this module does not depend on knob_deck).
|
// depend on knob_deck.
|
||||||
struct ClusterRects {
|
struct ClusterRects {
|
||||||
Rect rootStrip; // remainder-width fenced root strip
|
Rect rootStrip;
|
||||||
Rect preview; // the preview-trigger button
|
Rect preview;
|
||||||
Rect velCell; // the radial preview-velocity knob cell (knob + label band)
|
Rect velCell; // preview-velocity knob cell (knob + label band)
|
||||||
Rect velKnob; // the knob square at the cell's top
|
Rect velKnob;
|
||||||
Rect velLabel; // the label band beneath it
|
Rect velLabel;
|
||||||
Rect curveBtn; // the mini curve-preview button (opens the popup)
|
Rect curveBtn; // opens the curve-preview popup
|
||||||
};
|
};
|
||||||
ClusterRects clusterRects(const Rect& cluster, const Rect& chanMono, int knobSize);
|
ClusterRects clusterRects(const Rect& cluster, const Rect& chanMono, int knobSize);
|
||||||
|
|
||||||
// The S7 mono/stereo toggle: a two-segment control right-anchored in `area`, vertically
|
// Mono/stereo toggle: a two-segment control right-anchored in `area`, vertically centered.
|
||||||
// centered. Returns {mono-segment, stereo-segment}, side by side.
|
|
||||||
struct ChannelToggleRects {
|
struct ChannelToggleRects {
|
||||||
Rect mono;
|
Rect mono;
|
||||||
Rect stereo;
|
Rect stereo;
|
||||||
};
|
};
|
||||||
ChannelToggleRects channelToggleRects(const Rect& area);
|
ChannelToggleRects channelToggleRects(const Rect& area);
|
||||||
|
|
||||||
// The Zone-view (S-VIEW-8) content area: the whole window below the title band.
|
// Zone-view content area: the whole window below the title band.
|
||||||
Rect zoneContentArea(int w, int h);
|
Rect zoneContentArea(int w, int h);
|
||||||
|
|
||||||
// The Zone/Browse "Back" title-band button (right-anchored — the same slot the Sample
|
// Zone/Browse "Back" button — the same slot the Sample face's Zone nav button occupies.
|
||||||
// face's Zone nav button occupies).
|
|
||||||
Rect zoneBackRect(int w, int h);
|
Rect zoneBackRect(int w, int h);
|
||||||
|
|
||||||
// The "+ Add Zone" affordance at the top of the Zone content, and the "Delete" button
|
// "+ Add Zone" affordance and the "Delete" button beside it (Delete only draws/hits
|
||||||
// beside it (Delete only draws/hits when a zone is selected).
|
// when a zone is selected).
|
||||||
Rect zoneAddRect(const Rect& content);
|
Rect zoneAddRect(const Rect& content);
|
||||||
Rect zoneDeleteRect(const Rect& addR);
|
Rect zoneDeleteRect(const Rect& addR);
|
||||||
|
|
||||||
// The Zone-view keyboard strip rect: below the "+ Add Zone" affordance with a 12px gap,
|
// Zone-view keyboard strip rect: below "+ Add Zone" with a 12px gap, padded kPad
|
||||||
// padded kPad horizontally.
|
// horizontally.
|
||||||
Rect zonesStripArea(const Rect& content);
|
Rect zonesStripArea(const Rect& content);
|
||||||
|
|
||||||
// The S12 numeric-entry field ROW area inside the Zones legend (a band to the right of
|
// Numeric-entry field row area inside the Zones legend, and the rect of field `f`
|
||||||
// the sample label), and the rect of field `f` (0=low, 1=high, 2=root) within it —
|
// (0=low, 1=high, 2=root) within it — three equal segments left-to-right. Out-of-range
|
||||||
// three equal segments left-to-right. An out-of-range index yields an empty rect.
|
// index yields an empty rect.
|
||||||
Rect noteEntryFieldsArea(const Rect& content);
|
Rect noteEntryFieldsArea(const Rect& content);
|
||||||
Rect noteEntryFieldRect(const Rect& fields, int f);
|
Rect noteEntryFieldRect(const Rect& fields, int f);
|
||||||
|
|
||||||
// The per-zone parameter panel below the strip + the one-line legend, running to the
|
// Per-zone parameter panel below the strip + legend, running to the content bottom; the
|
||||||
// content bottom; the FB2 knob-deck area within it (a column at the right reserved for
|
// knob-deck area within it (a right column reserved for the curve-preview button); and
|
||||||
// the mini curve-preview button); and that button's rect (the cluster's 28px square,
|
// that button's rect (right-anchored at the panel top).
|
||||||
// right-anchored at the panel top).
|
|
||||||
Rect zonesControlPanel(const Rect& content);
|
Rect zonesControlPanel(const Rect& content);
|
||||||
Rect zonesDeckArea(const Rect& content);
|
Rect zonesDeckArea(const Rect& content);
|
||||||
Rect zonesCurveButton(const Rect& content);
|
Rect zonesCurveButton(const Rect& content);
|
||||||
|
|||||||
@@ -8,17 +8,15 @@ namespace reasampler::instrument::ui {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Clamp a MIDI note to [0, kEmbedKeyCount-1].
|
|
||||||
int clampNote(int n) {
|
int clampNote(int n) {
|
||||||
if (n < 0) return 0;
|
if (n < 0) return 0;
|
||||||
if (n > kEmbedKeyCount - 1) return kEmbedKeyCount - 1;
|
if (n > kEmbedKeyCount - 1) return kEmbedKeyCount - 1;
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map a key boundary in [0, kEmbedKeyCount] to an x pixel inside a band of the given
|
// Maps a key boundary (0..128) to an x pixel; keyEdge==128 maps to the band's right. A
|
||||||
// left/width. keyEdge is a boundary (0..128), so keyEdge==128 maps to the band's right.
|
// zone's left uses floor(low) and its right uses floor(high+1), tiling adjacent zones
|
||||||
// Integer math, floored — a zone's left uses floor(low) and its right uses floor(high+1),
|
// without a seam.
|
||||||
// which tiles adjacent zones without a seam.
|
|
||||||
int keyEdgeToX(int bandLeft, int bandWidth, int keyEdge) {
|
int keyEdgeToX(int bandLeft, int bandWidth, int keyEdge) {
|
||||||
if (keyEdge <= 0) return bandLeft;
|
if (keyEdge <= 0) return bandLeft;
|
||||||
if (keyEdge >= kEmbedKeyCount) return bandLeft + bandWidth;
|
if (keyEdge >= kEmbedKeyCount) return bandLeft + bandWidth;
|
||||||
@@ -33,9 +31,8 @@ EmbedLayout layoutEmbed(int w, int h) {
|
|||||||
|
|
||||||
EmbedLayout out;
|
EmbedLayout out;
|
||||||
|
|
||||||
// The level band takes a fixed height at the bottom, but never so much that the keymap
|
// Fixed height at the bottom, but never so much that the keymap falls below its
|
||||||
// above it falls below its minimum (or that the band exceeds the area). On a very short
|
// minimum; on a very short area the band yields to the keymap entirely.
|
||||||
// area the band yields to the keymap entirely.
|
|
||||||
int bandH = std::min(kEmbedLevelBandHeight, ch);
|
int bandH = std::min(kEmbedLevelBandHeight, ch);
|
||||||
if (ch - bandH < kEmbedKeymapMinHeight) {
|
if (ch - bandH < kEmbedKeymapMinHeight) {
|
||||||
bandH = std::max(0, ch - kEmbedKeymapMinHeight);
|
bandH = std::max(0, ch - kEmbedKeymapMinHeight);
|
||||||
|
|||||||
@@ -1,76 +1,57 @@
|
|||||||
// embed_strip.h — PURE layout + hit-test for the S6 embedded TCP/MCP strip. NO VST3,
|
// embed_strip.h — layout + hit-test for the embedded TCP/MCP strip. Mirror of
|
||||||
// NO REAPER, NO SWELL/LICE types at the boundary. The mirror of editor_geometry /
|
// editor_geometry/mode_switch; the embed shell marshals REAPER's embed messages (paint
|
||||||
// mode_switch: the fiddly rectangle math for the compact inline keymap/level strip lives
|
// bitmap + mouse coords) into these functions.
|
||||||
// here so it is unit-tested outside the DAW, while the embed shell (reasampler_embed.cpp)
|
|
||||||
// marshals REAPER's embed messages (paint bitmap + mouse coords) into these functions.
|
|
||||||
//
|
//
|
||||||
// The strip is a single compact band REAPER draws inline in the track/mixer control panel
|
// A single compact band REAPER draws inline in the track/mixer control panel via the
|
||||||
// (context TCP or MCP) via the Cockos embedded-UI surface. It shows:
|
// Cockos embedded-UI surface: each performance zone as a horizontal segment across the
|
||||||
// * the zone layout — each performance zone as a horizontal segment across the keyboard
|
// keyboard span (MIDI 0..127 mapped to the strip width), plus a thin activity level band
|
||||||
// span (MIDI 0..127 mapped to the strip width), so the keymap reads at a glance; and
|
// at the bottom. Interaction is zone selection only — no editing.
|
||||||
// * a thin level band at the bottom — a 0..1 activity indicator the shell fills.
|
|
||||||
// Interaction is zone SELECTION at most (S6 constraint: no new editing semantics) — a
|
|
||||||
// click maps to the zone whose key range covers that point, or -1.
|
|
||||||
//
|
|
||||||
// It reuses the same Rect + contains() as editor_geometry (the strip and the editor share
|
|
||||||
// one geometry idiom), so this header depends on editor_geometry.h rather than redefining
|
|
||||||
// a second rectangle type.
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "core/instrument/ui/editor_geometry.h" // Rect, contains — one shared geometry idiom
|
#include "core/instrument/ui/editor_geometry.h" // Rect, contains
|
||||||
|
|
||||||
namespace reasampler::instrument::ui {
|
namespace reasampler::instrument::ui {
|
||||||
|
|
||||||
// The full MIDI key span the strip maps across its width. 128 keys (0..127); the strip's
|
|
||||||
// horizontal axis is this range, so a zone [lowNote, highNote] becomes a sub-rectangle.
|
|
||||||
inline constexpr int kEmbedKeyCount = 128;
|
inline constexpr int kEmbedKeyCount = 128;
|
||||||
|
|
||||||
// Fixed metrics for the strip, exposed so the shell and tests agree.
|
// Fixed metrics for the strip, exposed so the shell and tests agree.
|
||||||
inline constexpr int kEmbedLevelBandHeight = 4; // the bottom activity band (px)
|
inline constexpr int kEmbedLevelBandHeight = 4;
|
||||||
inline constexpr int kEmbedKeymapMinHeight = 6; // keymap area collapses no smaller
|
inline constexpr int kEmbedKeymapMinHeight = 6;
|
||||||
|
|
||||||
// One zone rendered on the strip: its inclusive MIDI key range. This is the minimal
|
// One zone rendered on the strip: its inclusive MIDI key range — the minimal projection
|
||||||
// projection of a PerformanceZone the strip needs (it does not carry sample ids or PCM —
|
// of a PerformanceZone the strip needs (no sample ids or PCM). Expected in [0,127] with
|
||||||
// the shell resolves labels; the strip only lays out ranges). lowNote/highNote are
|
// low <= high; layout clamps defensively regardless.
|
||||||
// expected in [0,127] with low <= high, but the layout clamps defensively so a malformed
|
|
||||||
// zone never yields an out-of-strip rect.
|
|
||||||
struct EmbedZone {
|
struct EmbedZone {
|
||||||
int lowNote = 0;
|
int lowNote = 0;
|
||||||
int highNote = 127;
|
int highNote = 127;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The strip's regions, derived from the (w x h) embed area REAPER reports. Both clamp to
|
// Clamped to the area so a degenerate (tiny) size never yields a region spilling outside
|
||||||
// the area so a degenerate (tiny) size never yields a region spilling outside the surface.
|
// the surface.
|
||||||
struct EmbedLayout {
|
struct EmbedLayout {
|
||||||
Rect keymap; // top: the zone-segment band (the compact keymap)
|
Rect keymap; // top: zone-segment band
|
||||||
Rect levelBand; // bottom: the thin level/activity indicator
|
Rect levelBand; // bottom: level/activity indicator
|
||||||
};
|
};
|
||||||
|
|
||||||
// Divide a (w x h) embed area into the strip's regions. Pure: same inputs -> same layout.
|
// Divide a (w x h) embed area into the strip's regions. The level band takes a fixed
|
||||||
// The level band takes a fixed height at the bottom (clamped so it never exceeds the area
|
// height at the bottom (clamped so it never starves the keymap below
|
||||||
// or starves the keymap below kEmbedKeymapMinHeight); the keymap takes the rest. A zero or
|
// kEmbedKeymapMinHeight); the keymap takes the rest.
|
||||||
// negative size yields empty rects (no inversion).
|
|
||||||
EmbedLayout layoutEmbed(int w, int h);
|
EmbedLayout layoutEmbed(int w, int h);
|
||||||
|
|
||||||
// The horizontal sub-rectangle of the keymap band for a zone spanning [lowNote, highNote]
|
// Horizontal sub-rect of the keymap band for a zone spanning [lowNote, highNote]
|
||||||
// (inclusive). The 128-key span maps linearly across keymap.width; the returned rect
|
// (inclusive). Spans the half-open pixel range so adjacent zones tile without a gap or
|
||||||
// spans the half-open pixel range [x(lowNote), x(highNote+1)) so adjacent zones (e.g.
|
// overlap. Notes clamp to [0,127] and low clamps to <= high.
|
||||||
// 0..59 and 60..127) tile without a gap or overlap. Notes are clamped to [0,127] and low
|
|
||||||
// is clamped to <= high, so a malformed zone yields an in-band (possibly zero-width) rect,
|
|
||||||
// never an inverted one. Pure.
|
|
||||||
Rect zoneSegmentRect(const EmbedLayout& layout, int lowNote, int highNote);
|
Rect zoneSegmentRect(const EmbedLayout& layout, int lowNote, int highNote);
|
||||||
|
|
||||||
// The zone a click at (x, y) lands on, given the zones in draw order, or -1 for a click
|
// Zone a click at (x, y) lands on, given zones in draw order, or -1 for a miss. When
|
||||||
// outside the keymap band or on a key not covered by any zone. When zones overlap on a
|
// zones overlap on a key, the first covering zone in order wins — mirroring the sampler
|
||||||
// key, the FIRST covering zone in order wins — mirroring the sampler core's first-match
|
// core's first-match Keymap::resolve, so selection agrees with playback.
|
||||||
// Keymap::resolve and the editor's zone order, so selection agrees with playback. Pure.
|
|
||||||
int zoneAtPoint(const EmbedLayout& layout, const EmbedZone* zones, int zoneCount, int x,
|
int zoneAtPoint(const EmbedLayout& layout, const EmbedZone* zones, int zoneCount, int x,
|
||||||
int y);
|
int y);
|
||||||
|
|
||||||
// The filled portion of the level band for a 0..1 level. Clamps level to [0,1]; the
|
// Filled portion of the level band for a 0..1 level (clamped); left sub-rect of levelBand
|
||||||
// returned rect is the left sub-rectangle of levelBand whose width is level * band width
|
// whose width is level * band width.
|
||||||
// (rounded down). level <= 0 -> empty rect; level >= 1 -> the whole band. Pure.
|
|
||||||
Rect levelFillRect(const EmbedLayout& layout, double level);
|
Rect levelFillRect(const EmbedLayout& layout, double level);
|
||||||
|
|
||||||
} // namespace reasampler::instrument::ui
|
} // namespace reasampler::instrument::ui
|
||||||
|
|||||||
@@ -9,34 +9,28 @@ namespace reasampler::instrument::ui {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
// Matches envelope_overlay::timeToX. Zero when the area is degenerate (no motion).
|
||||||
// Seconds represented by one horizontal pixel under the overlay's linear time base. Zero when the
|
|
||||||
// area is degenerate (the caller then produces no motion). Matches envelope_overlay::timeToX.
|
|
||||||
double secondsPerPixel(const Rect& area, double totalSeconds) {
|
double secondsPerPixel(const Rect& area, double totalSeconds) {
|
||||||
const int w = std::max(0, area.width);
|
const int w = std::max(0, area.width);
|
||||||
if (w <= 0 || totalSeconds <= 0.0) return 0.0;
|
if (w <= 0 || totalSeconds <= 0.0) return 0.0;
|
||||||
return totalSeconds / static_cast<double>(w);
|
return totalSeconds / static_cast<double>(w);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seconds per pixel for a GATE time-node drag (FA2): the reciprocal of the overlay's
|
// Reciprocal of the overlay's gatePxPerSecond, matching gatePolyline's scale exactly so a
|
||||||
// param-domain gatePxPerSecond(area) scale — sample-length-free, matching
|
// dragged handle tracks the cursor 1:1.
|
||||||
// envelope_overlay::gatePolyline exactly so the dragged handle tracks the cursor 1:1 (each
|
|
||||||
// node's x is affine in its own segment duration with slope gatePxPerSecond). Zero when the
|
|
||||||
// area is degenerate.
|
|
||||||
double gateSecondsPerPixel(const Rect& area) {
|
double gateSecondsPerPixel(const Rect& area) {
|
||||||
const double pps = gatePxPerSecond(area);
|
const double pps = gatePxPerSecond(area);
|
||||||
return pps > 0.0 ? 1.0 / pps : 0.0;
|
return pps > 0.0 ? 1.0 / pps : 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Level (0..1) represented by one vertical pixel. levelToY spans (height-1) rows for [0,1], so one
|
// Matches envelope_overlay::levelToY (spans height-1 rows for [0,1]).
|
||||||
// pixel is 1/(height-1). Zero when degenerate. Matches envelope_overlay::levelToY.
|
|
||||||
double levelPerPixel(const Rect& area) {
|
double levelPerPixel(const Rect& area) {
|
||||||
const int h = std::max(0, area.height);
|
const int h = std::max(0, area.height);
|
||||||
if (h <= 1) return 0.0;
|
if (h <= 1) return 0.0;
|
||||||
return 1.0 / static_cast<double>(h - 1);
|
return 1.0 / static_cast<double>(h - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// True for the nodes the user can grab-and-drag (Origin + ReleaseStart are draw-only anchors).
|
// Origin + ReleaseStart are draw-only anchors, not grabbable.
|
||||||
bool isDraggable(EnvNode n) {
|
bool isDraggable(EnvNode n) {
|
||||||
switch (n) {
|
switch (n) {
|
||||||
case EnvNode::Origin:
|
case EnvNode::Origin:
|
||||||
@@ -47,10 +41,8 @@ bool isDraggable(EnvNode n) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// True when the node belongs to the envelope's active mode. Guards the degenerate cross-mode
|
// Guards the degenerate baseline's cross-mode ReleaseEnd vertex from writing releaseSeconds in
|
||||||
// write: the degenerate baseline polyline carries a ReleaseEnd vertex regardless of mode, so a
|
// Trigger mode (and vice versa). Applied by both the hit-test and the drag resolver.
|
||||||
// zero-height Trigger-mode grab of it must not write releaseSeconds (and vice versa for Gate
|
|
||||||
// nodes vs Trigger fields). Applied by BOTH the hit-test and the drag resolver so they agree.
|
|
||||||
bool nodeInMode(EnvNode n, EnvMode m) {
|
bool nodeInMode(EnvNode n, EnvMode m) {
|
||||||
switch (n) {
|
switch (n) {
|
||||||
case EnvNode::AttackEnd:
|
case EnvNode::AttackEnd:
|
||||||
@@ -64,7 +56,7 @@ bool nodeInMode(EnvNode n, EnvMode m) {
|
|||||||
return m == EnvMode::Trigger;
|
return m == EnvMode::Trigger;
|
||||||
case EnvNode::Origin:
|
case EnvNode::Origin:
|
||||||
case EnvNode::ReleaseStart:
|
case EnvNode::ReleaseStart:
|
||||||
return false; // never draggable in any mode (isDraggable filters these anyway)
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -73,18 +65,15 @@ bool nodeInMode(EnvNode n, EnvMode m) {
|
|||||||
|
|
||||||
NodeHit nodeAtPoint(const AmpEnvelope& env, const Rect& area, double totalSeconds, int x, int y) {
|
NodeHit nodeAtPoint(const AmpEnvelope& env, const Rect& area, double totalSeconds, int x, int y) {
|
||||||
const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, area, totalSeconds);
|
const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, area, totalSeconds);
|
||||||
// NEAREST draggable, mode-matching node within the pick radius wins (Chebyshev distance —
|
// Nearest draggable, mode-matching node within the pick radius wins (Chebyshev distance);
|
||||||
// the square grab box); ties break to the earlier draw-order node (FA2). Gate nodes never
|
// ties go to the earlier draw-order node. Only matters for Trigger's zero-fade-out
|
||||||
// coincide (the forward map enforces kGateNodeSepPx separation), so the tie-break only
|
// coincidence (FadeOutStart overlaps LengthEnd and wins).
|
||||||
// matters for Trigger's zero-fade-out coincidence: FadeOutStart overlays LengthEnd, WINS the
|
|
||||||
// tie, and can be dragged inward from the right edge. The mode filter keeps the degenerate
|
|
||||||
// baseline's ReleaseEnd vertex from registering as a grabbable node in Trigger mode.
|
|
||||||
NodeHit best;
|
NodeHit best;
|
||||||
int bestDist = kNodeGrabRadius + 1;
|
int bestDist = kNodeGrabRadius + 1;
|
||||||
for (const EnvVertex& v : poly) {
|
for (const EnvVertex& v : poly) {
|
||||||
if (!isDraggable(v.node) || !nodeInMode(v.node, env.mode)) continue;
|
if (!isDraggable(v.node) || !nodeInMode(v.node, env.mode)) continue;
|
||||||
const int dist = std::max(std::abs(x - v.x), std::abs(y - v.y));
|
const int dist = std::max(std::abs(x - v.x), std::abs(y - v.y));
|
||||||
if (dist < bestDist) { // strictly closer only: earlier draw order keeps ties
|
if (dist < bestDist) { // strict-less-than keeps ties at the earlier draw order
|
||||||
bestDist = dist;
|
bestDist = dist;
|
||||||
best = NodeHit{true, v.node};
|
best = NodeHit{true, v.node};
|
||||||
}
|
}
|
||||||
@@ -101,16 +90,12 @@ AmpEnvelope resolveNodeDrag(const AmpEnvelope& grabEnv, EnvNode node, const Rect
|
|||||||
const double secPerPx = secondsPerPixel(area, totalSeconds);
|
const double secPerPx = secondsPerPixel(area, totalSeconds);
|
||||||
if (secPerPx <= 0.0) return out; // degenerate area / duration — no motion
|
if (secPerPx <= 0.0) return out; // degenerate area / duration — no motion
|
||||||
const double dSec = static_cast<double>(dxPixels) * secPerPx;
|
const double dSec = static_cast<double>(dxPixels) * secPerPx;
|
||||||
// Gate time nodes use the schematic's PARAM-DOMAIN px->seconds scale (FA2) — the reciprocal
|
|
||||||
// of the overlay's gatePxPerSecond, sample-length-free — so the dragged handle tracks the
|
|
||||||
// cursor 1:1. gateTimedWidth >= 1 whenever the area is non-empty, so gateDSec is
|
|
||||||
// well-defined past the degenerate guard above.
|
|
||||||
const double gateDSec = static_cast<double>(dxPixels) * gateSecondsPerPixel(area);
|
const double gateDSec = static_cast<double>(dxPixels) * gateSecondsPerPixel(area);
|
||||||
|
|
||||||
switch (node) {
|
switch (node) {
|
||||||
// --- Gate: each cumulative-time node edits its OWN segment duration. Non-negative
|
// Gate: each cumulative-time node edits its own segment duration. Non-negative durations
|
||||||
// durations ARE the monotonic-in-time guarantee (a node can never cross a neighbour
|
// ARE the monotonic-in-time guarantee (a segment can never go negative, so a node can
|
||||||
// because every segment stays >= 0), so the [0, max] clamp is the whole constraint.
|
// never cross a neighbour) — the [0, max] clamp is the whole constraint.
|
||||||
case EnvNode::AttackEnd:
|
case EnvNode::AttackEnd:
|
||||||
out.attackSeconds =
|
out.attackSeconds =
|
||||||
std::clamp(grabEnv.attackSeconds + gateDSec, 0.0, bounds.maxAttackSeconds);
|
std::clamp(grabEnv.attackSeconds + gateDSec, 0.0, bounds.maxAttackSeconds);
|
||||||
@@ -119,8 +104,7 @@ AmpEnvelope resolveNodeDrag(const AmpEnvelope& grabEnv, EnvNode node, const Rect
|
|||||||
out.holdSeconds = std::clamp(grabEnv.holdSeconds + gateDSec, 0.0, bounds.maxHoldSeconds);
|
out.holdSeconds = std::clamp(grabEnv.holdSeconds + gateDSec, 0.0, bounds.maxHoldSeconds);
|
||||||
break;
|
break;
|
||||||
case EnvNode::DecayEnd: {
|
case EnvNode::DecayEnd: {
|
||||||
// Sustain node: X sets decay time, Y sets sustain level (drag DOWN = higher y = lower
|
// X sets decay time, Y sets sustain level (drag down = higher y = lower level).
|
||||||
// level, so subtract the level delta).
|
|
||||||
out.decaySeconds = std::clamp(grabEnv.decaySeconds + gateDSec, 0.0, bounds.maxDecaySeconds);
|
out.decaySeconds = std::clamp(grabEnv.decaySeconds + gateDSec, 0.0, bounds.maxDecaySeconds);
|
||||||
const double lvlPerPx = levelPerPixel(area);
|
const double lvlPerPx = levelPerPixel(area);
|
||||||
const double dLevel = -static_cast<double>(dyPixels) * lvlPerPx;
|
const double dLevel = -static_cast<double>(dyPixels) * lvlPerPx;
|
||||||
@@ -132,17 +116,9 @@ AmpEnvelope resolveNodeDrag(const AmpEnvelope& grabEnv, EnvNode node, const Rect
|
|||||||
std::clamp(grabEnv.releaseSeconds + gateDSec, 0.0, bounds.maxReleaseSeconds);
|
std::clamp(grabEnv.releaseSeconds + gateDSec, 0.0, bounds.maxReleaseSeconds);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// --- Trigger: fades + length are FRACTIONS. X pixels convert to a fraction of the PLAYED
|
// Trigger: fades + length are fractions. X pixels convert to a fraction of the played
|
||||||
// span (fades) or the whole sample (length). Monotonic: fadeIn + fadeOut <= 1 so the
|
// span (fades) or the whole sample (length). fadeIn + fadeOut <= 1 keeps the two fade
|
||||||
// two fade nodes never cross (each clamps against the other), and length in [0, max].
|
// nodes from crossing (each clamps against the other).
|
||||||
//
|
|
||||||
// TRIGGER SEAM — CONVERSION REQUIRED ON BOTH PATHS (Wave 2 shell author, read this):
|
|
||||||
// fadeInFraction/fadeOutFraction in AmpEnvelope are fractions of the played span.
|
|
||||||
// TriggerParams (sampler_core.h) stores the corresponding values as SOURCE FRAMES
|
|
||||||
// (fadeInFrames/fadeOutFrames, int64_t). The shell owes a converter on BOTH directions:
|
|
||||||
// pack (draw): fadeInFrames/fadeOutFrames -> fraction (needs frameCount + rate)
|
|
||||||
// unpack (commit): fraction -> fadeInFrames/fadeOutFrames (same inputs)
|
|
||||||
// See the TRIGGER SEAM note on AmpEnvelope in envelope_overlay.h for the formula.
|
|
||||||
case EnvNode::FadeInEnd: {
|
case EnvNode::FadeInEnd: {
|
||||||
if (dxPixels == 0) break; // zero-motion grab: no param change, no division
|
if (dxPixels == 0) break; // zero-motion grab: no param change, no division
|
||||||
const double playSeconds = std::max(0.0, grabEnv.lengthFraction) * totalSeconds;
|
const double playSeconds = std::max(0.0, grabEnv.lengthFraction) * totalSeconds;
|
||||||
|
|||||||
@@ -1,41 +1,18 @@
|
|||||||
// envelope_edit.h — PURE node hit-test + pixel-delta→clamped-param inverse map for the S-VIEW-3
|
// envelope_edit.h — node hit-test + pixel-delta -> clamped-param inverse map for the draggable
|
||||||
// draggable envelope nodes. NO VST3, NO REAPER, NO SWELL/LICE types at the boundary. The mirror
|
// envelope nodes. Mirror of card_drag/waveform_view: drag arithmetic lives here, unit-tested
|
||||||
// of card_drag / waveform_view: the drag arithmetic + clamp/monotonic constraints live here,
|
// outside the DAW; the shell draws handles, captures the grab, and feeds pixel deltas back in.
|
||||||
// unit-tested at the boundaries outside the DAW, while the editor shell (reasampler_editor.cpp)
|
|
||||||
// draws the handles, captures the grab on WM_LBUTTONDOWN, feeds each move's pixel delta back
|
|
||||||
// through here, and commits the resulting params to the zone through the same off-audio-thread
|
|
||||||
// path a slider edit uses.
|
|
||||||
//
|
//
|
||||||
// TWO SURFACES, ONE MODEL. envelope_overlay owns the params→polyline FORWARD map (draw); this
|
// envelope_overlay owns the params->polyline forward (draw) map; this module owns the inverse
|
||||||
// module owns the pixel→params INVERSE map (edit) + node hit-test. Both read/write the SAME
|
// (edit) map + hit-test. Both read/write the same AmpEnvelope fields (shell re-reads the zone
|
||||||
// AmpEnvelope fields (the shell re-reads the zone every paint — no listener chain), so a node
|
// every paint), so a node drag and a slider edit are two views on one source of truth.
|
||||||
// drag and a slider edit are two views on one source of truth and can never diverge.
|
|
||||||
//
|
//
|
||||||
// THE INVARIANT (S-VIEW-F2). A drag can NEVER produce a param a slider couldn't:
|
// A drag can never produce a param a slider couldn't: nodes are monotonic in time (clamped
|
||||||
// * MONOTONIC IN TIME — a node clamps between its time predecessor and successor, so attack-end
|
// between time predecessor/successor) and range-clamped to the same per-param [min,max] the
|
||||||
// can't pass hold-end, decay can't pass release, etc. Each segment stays >= 0.
|
// slider uses (EnvClampBounds, caller-supplied since those maxima live shell-side).
|
||||||
// * RANGE-CLAMPED — times clamp to the SAME per-param [min,max] the slider enforces; levels
|
|
||||||
// clamp to [0,1]. Because the concrete second/fraction maxima live SHELL-SIDE (param_slider
|
|
||||||
// is deliberately engine-free — the shell owns the 0..1↔domain mapping), the clamp bounds are
|
|
||||||
// CALLER-SUPPLIED here (EnvClampBounds): the shell passes the same maxima it feeds the slider,
|
|
||||||
// so the two surfaces share one clamp by construction.
|
|
||||||
//
|
//
|
||||||
// WHICH AXES. Time-only nodes (AttackEnd, HoldEnd, ReleaseEnd; FadeInEnd, FadeOutStart,
|
// Time-only nodes drag on X; DecayEnd (the sustain node) drags on both axes (X = decay time,
|
||||||
// LengthEnd) drag on X only. The sustain node (DecayEnd) drags on BOTH axes — its X sets the
|
// Y = sustain level). Origin and the drawing-only ReleaseStart are not draggable. A node is only
|
||||||
// decay time, its Y sets the sustain level (the standard ADSR-editor grammar). Origin and the
|
// editable in its own mode (Gate nodes ignore drags in Trigger mode and vice versa).
|
||||||
// drawing-only ReleaseStart vertex are NOT draggable.
|
|
||||||
//
|
|
||||||
// GATE DRAG SCALE (FA2). Gate time nodes convert px->seconds via the reciprocal of the
|
|
||||||
// schematic's PARAM-DOMAIN scale (envelope_overlay's gatePxPerSecond — sample-length-free), so
|
|
||||||
// a dragged handle tracks the cursor exactly 1:1 for stages within the schematic domain (each
|
|
||||||
// node's x is affine in its own segment duration). Trigger nodes keep the full-canvas
|
|
||||||
// PCM-aligned scale. Both match the forward map in envelope_overlay. A node is only editable in
|
|
||||||
// its OWN mode: Gate nodes ignore drags while the envelope is in Trigger mode and vice versa
|
|
||||||
// (guards the degenerate baseline's cross-mode ReleaseEnd vertex from writing releaseSeconds).
|
|
||||||
//
|
|
||||||
// Reuses editor_geometry's Rect + the EnvNode / AmpEnvelope / EnvMode types from
|
|
||||||
// envelope_overlay (one shared node vocabulary across draw + edit), and the shared timeToX /
|
|
||||||
// levelToY maps so the handle the overlay drew and the grab region here agree pixel-for-pixel.
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -47,60 +24,46 @@
|
|||||||
|
|
||||||
namespace reasampler::instrument::ui {
|
namespace reasampler::instrument::ui {
|
||||||
|
|
||||||
// The pick radius (px) around a node's drawn point: a grab within this many pixels (in BOTH x and
|
// Pick radius (px) around a node's drawn point, in both x and y. Mirrors waveform_view's
|
||||||
// y) of a node handle grabs it. Mirrors waveform_view's kMarkerGrabWidth — wide enough to grab a
|
// kMarkerGrabWidth.
|
||||||
// small handle comfortably, narrow enough that adjacent nodes stay distinguishable.
|
|
||||||
inline constexpr int kNodeGrabRadius = 6;
|
inline constexpr int kNodeGrabRadius = 6;
|
||||||
|
|
||||||
// The per-param clamp bounds the shell supplies (the SAME maxima its sliders map 0..1 onto). All
|
// Per-param clamp bounds the shell supplies — the same maxima its sliders map [0,1] onto.
|
||||||
// are upper bounds in the param's own domain; the lower bound is 0 (each stage >= 0), and the
|
// Lower bound is always 0; the monotonic-in-time constraint tightens further at edit time.
|
||||||
// monotonic-in-time constraint tightens these further at edit time. Defaults are conservative
|
// Defaults are placeholders; the shell overrides with its live slider domain.
|
||||||
// placeholders; the shell OVERRIDES them with its live slider domain so the clamp matches exactly.
|
|
||||||
struct EnvClampBounds {
|
struct EnvClampBounds {
|
||||||
double maxAttackSeconds = 4.0; // upper bound of the attack slider
|
double maxAttackSeconds = 4.0;
|
||||||
double maxHoldSeconds = 4.0;
|
double maxHoldSeconds = 4.0;
|
||||||
double maxDecaySeconds = 4.0;
|
double maxDecaySeconds = 4.0;
|
||||||
double maxReleaseSeconds = 4.0;
|
double maxReleaseSeconds = 4.0;
|
||||||
// Trigger fades + length are fractions; their natural upper bound is 1.0. Exposed so a shell
|
|
||||||
// that caps a fade below the full span (e.g. 0.5) shares that cap with its slider.
|
|
||||||
double maxFadeInFraction = 1.0;
|
double maxFadeInFraction = 1.0;
|
||||||
double maxFadeOutFraction = 1.0;
|
double maxFadeOutFraction = 1.0;
|
||||||
double maxLengthFraction = 1.0;
|
double maxLengthFraction = 1.0;
|
||||||
// sustainLevel is always [0,1] — no shell knob needed, kept implicit.
|
// sustainLevel is always [0,1] — no shell knob needed.
|
||||||
};
|
};
|
||||||
|
|
||||||
// Which node a grab at (x, y) lands on, given the CURRENT envelope + overlay rect + sample
|
// Which node a grab at (x, y) lands on, given the current envelope/rect/duration (the same
|
||||||
// duration (the same inputs buildEnvelopePolyline drew from, so the grab tests the drawn handles).
|
// inputs buildEnvelopePolyline drew from). `hit` is false for a point off every draggable node;
|
||||||
// Returns EnvNode::Origin's NON-membership as a miss via the bool return: `hit` is false for a
|
// Origin/ReleaseStart and nodes from the other mode never hit. Nearest node within the radius
|
||||||
// point off every DRAGGABLE node. Origin and ReleaseStart are never returned (not draggable),
|
// wins (Chebyshev distance); an exact tie goes to the earlier draw-order node — this only matters
|
||||||
// and a node from the OTHER mode is never returned (the degenerate baseline's ReleaseEnd vertex
|
// for Trigger's zero-fade-out coincidence (FadeOutStart overlaps LengthEnd and wins, so the fade
|
||||||
// is not grabbable in Trigger mode). The NEAREST node within the radius wins (Chebyshev
|
// can be dragged open from zero). Gate nodes never coincide (forward map enforces
|
||||||
// distance); an exact tie goes to the earlier draw-order node (FA2 — deterministic). Gate nodes
|
// kGateNodeSepPx), so every Gate handle is independently grabbable.
|
||||||
// never coincide (the forward map enforces kGateNodeSepPx separation, so every Gate handle is
|
|
||||||
// individually grabbable in every state); the tie-break matters only for Trigger's zero-fade-out
|
|
||||||
// coincidence, where FadeOutStart overlays LengthEnd, wins the tie, and can be dragged inward
|
|
||||||
// from the right edge. Pure.
|
|
||||||
struct NodeHit {
|
struct NodeHit {
|
||||||
bool hit = false;
|
bool hit = false;
|
||||||
EnvNode node = EnvNode::Origin; // meaningful only when hit == true
|
EnvNode node = EnvNode::Origin; // meaningful only when hit == true
|
||||||
};
|
};
|
||||||
NodeHit nodeAtPoint(const AmpEnvelope& env, const Rect& area, double totalSeconds, int x, int y);
|
NodeHit nodeAtPoint(const AmpEnvelope& env, const Rect& area, double totalSeconds, int x, int y);
|
||||||
|
|
||||||
// Resolve a drag of `node` to a new AmpEnvelope. Given the envelope AS OF GRAB TIME (`grabEnv` —
|
// Resolves a drag of `node` to a new AmpEnvelope. `grabEnv` is the envelope as of grab time (the
|
||||||
// the shell snapshots it on WM_LBUTTONDOWN so the delta is absolute, not accumulated), the overlay
|
// shell snapshots it on button-down so the delta is absolute, not accumulated); `dxPixels`/
|
||||||
// rect + sample duration (the pixel↔param maps), the caller's clamp bounds, and the pixel delta
|
// `dyPixels` is the pixel delta since grab.
|
||||||
// since grab (`dxPixels`, `dyPixels`), returns the envelope the node should now describe:
|
// * X delta -> the node's time param, shifted via the same linear map as timeToX, clamped to
|
||||||
// * X delta -> the node's TIME param, shifted proportionally (same linear map as timeToX),
|
// [0, per-param max] and to its monotonic-in-time neighbours.
|
||||||
// clamped to [0, per-param max] AND to its monotonic-in-time neighbours (>= predecessor time,
|
// * Y delta -> the level param, only for DecayEnd; clamped to [0,1]. Ignored for time-only nodes.
|
||||||
// <= successor time). For a cumulative-time node the shift lands on that node's OWN segment
|
// * A non-draggable node, an other-mode node, a zero-size area, or totalSeconds <= 0 returns
|
||||||
// duration (e.g. dragging HoldEnd changes holdSeconds, not attack).
|
// `grabEnv` unchanged.
|
||||||
// * Y delta -> the LEVEL param, but ONLY for the sustain node (DecayEnd); clamped to [0,1].
|
// Only the dragged node's param(s) change. Pure.
|
||||||
// dyPixels is IGNORED for every time-only node.
|
|
||||||
// * Non-draggable node (Origin / ReleaseStart), a node from the OTHER mode (a Gate node while
|
|
||||||
// grabEnv.mode is Trigger, or vice versa), a zero-width/zero-height area, or
|
|
||||||
// totalSeconds <= 0 -> `grabEnv` returned unchanged (no motion).
|
|
||||||
// Only the dragged node's param(s) change; every other field carries through from `grabEnv`. Pure
|
|
||||||
// — rounding is to the param's continuous value (no snapping, matching the sliders' resolution).
|
|
||||||
AmpEnvelope resolveNodeDrag(const AmpEnvelope& grabEnv, EnvNode node, const Rect& area,
|
AmpEnvelope resolveNodeDrag(const AmpEnvelope& grabEnv, EnvNode node, const Rect& area,
|
||||||
double totalSeconds, const EnvClampBounds& bounds,
|
double totalSeconds, const EnvClampBounds& bounds,
|
||||||
int dxPixels, int dyPixels);
|
int dxPixels, int dyPixels);
|
||||||
|
|||||||
@@ -8,15 +8,14 @@
|
|||||||
|
|
||||||
namespace reasampler::instrument::ui {
|
namespace reasampler::instrument::ui {
|
||||||
|
|
||||||
using util::clamp01; // the ONE unit-interval clamp (Q-W1, T4-24)
|
using util::clamp01;
|
||||||
|
|
||||||
int timeToX(const Rect& area, double totalSeconds, double t) {
|
int timeToX(const Rect& area, double totalSeconds, double t) {
|
||||||
const int w = std::max(0, area.width);
|
const int w = std::max(0, area.width);
|
||||||
if (w <= 0 || totalSeconds <= 0.0) return area.x;
|
if (w <= 0 || totalSeconds <= 0.0) return area.x;
|
||||||
if (t < 0.0) t = 0.0;
|
if (t < 0.0) t = 0.0;
|
||||||
// Linear map, clamped on BOTH sides (FA2 bounds invariant): t past totalSeconds pins to the
|
// Clamp in double space before the int cast — a huge t would overflow a 32-bit long
|
||||||
// last in-bounds column area.right()-1. Clamp in DOUBLE space BEFORE the integer cast — a huge
|
// (Windows) and wrap to the wrong edge.
|
||||||
// t would overflow a 32-bit long (Windows) and wrap to the WRONG edge — then round.
|
|
||||||
double px = (t / totalSeconds) * static_cast<double>(w);
|
double px = (t / totalSeconds) * static_cast<double>(w);
|
||||||
if (px > static_cast<double>(w - 1)) px = static_cast<double>(w - 1);
|
if (px > static_cast<double>(w - 1)) px = static_cast<double>(w - 1);
|
||||||
return area.x + static_cast<int>(px + 0.5);
|
return area.x + static_cast<int>(px + 0.5);
|
||||||
@@ -33,9 +32,7 @@ int gateTimedWidth(const Rect& area) {
|
|||||||
double gatePxPerSecond(const Rect& area) {
|
double gatePxPerSecond(const Rect& area) {
|
||||||
const int timedW = gateTimedWidth(area);
|
const int timedW = gateTimedWidth(area);
|
||||||
if (timedW <= 0) return 0.0;
|
if (timedW <= 0) return 0.0;
|
||||||
// Usable width = timed region minus the four per-segment separation bases and the last
|
// Minus the four per-segment separation bases and the last in-bounds column, floored at 1.
|
||||||
// in-bounds column, floored at 1 px so the scale never degenerates; the domain is the four
|
|
||||||
// stages end-to-end at their schematic maxima (param-domain scale — sample-length-free).
|
|
||||||
const double usable =
|
const double usable =
|
||||||
std::max(1.0, static_cast<double>(timedW - 1 - 4 * kGateNodeSepPx));
|
std::max(1.0, static_cast<double>(timedW - 1 - 4 * kGateNodeSepPx));
|
||||||
return usable / (4.0 * kGateStageMaxSeconds);
|
return usable / (4.0 * kGateStageMaxSeconds);
|
||||||
@@ -46,8 +43,8 @@ int levelToY(const Rect& area, double level) {
|
|||||||
if (h <= 0) return area.y;
|
if (h <= 0) return area.y;
|
||||||
if (level < 0.0) level = 0.0;
|
if (level < 0.0) level = 0.0;
|
||||||
if (level > 1.0) level = 1.0;
|
if (level > 1.0) level = 1.0;
|
||||||
// Level 1 -> top row, level 0 -> bottom row (bottom-1 under the half-open convention). The
|
// Level 1 -> top row, level 0 -> bottom row; spans (h-1) px so both endpoints land on a
|
||||||
// range spans (h-1) pixels so both endpoints land ON a drawable row.
|
// drawable row.
|
||||||
const int span = h - 1;
|
const int span = h - 1;
|
||||||
const long dy = static_cast<long>((1.0 - level) * static_cast<double>(span) + 0.5);
|
const long dy = static_cast<long>((1.0 - level) * static_cast<double>(span) + 0.5);
|
||||||
return area.y + static_cast<int>(dy);
|
return area.y + static_cast<int>(dy);
|
||||||
@@ -64,10 +61,8 @@ EnvVertex vtx(EnvNode node, const Rect& area, double totalSeconds, double t, dou
|
|||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
// One Gate vertex from a pixel offset inside the area (the Gate schematic works in px space —
|
// Gate works in px space (timed px + the fixed sustain-plateau reserve) rather than the plain
|
||||||
// timed px + the fixed sustain-plateau reserve — not through the plain timeToX map). Clamps x in
|
// timeToX map; clamps in double space before the int cast for the same overflow reason as above.
|
||||||
// DOUBLE space to the last in-bounds column BEFORE the integer cast (FA2 bounds invariant; a
|
|
||||||
// huge px would overflow a 32-bit long on Windows and wrap to the WRONG edge).
|
|
||||||
EnvVertex gateVtx(EnvNode node, const Rect& area, double px, double level) {
|
EnvVertex gateVtx(EnvNode node, const Rect& area, double px, double level) {
|
||||||
const int w = std::max(1, area.width);
|
const int w = std::max(1, area.width);
|
||||||
if (px < 0.0) px = 0.0;
|
if (px < 0.0) px = 0.0;
|
||||||
@@ -81,18 +76,16 @@ EnvVertex gateVtx(EnvNode node, const Rect& area, double px, double level) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<EnvVertex> gatePolyline(const AmpEnvelope& env, const Rect& area) {
|
std::vector<EnvVertex> gatePolyline(const AmpEnvelope& env, const Rect& area) {
|
||||||
// Non-negative segment durations (a stored negative would be an upstream bug; clamp defensively).
|
// Clamp defensively — a stored negative duration would be an upstream bug.
|
||||||
const double a = std::max(0.0, env.attackSeconds);
|
const double a = std::max(0.0, env.attackSeconds);
|
||||||
const double h = std::max(0.0, env.holdSeconds);
|
const double h = std::max(0.0, env.holdSeconds);
|
||||||
const double d = std::max(0.0, env.decaySeconds);
|
const double d = std::max(0.0, env.decaySeconds);
|
||||||
const double r = std::max(0.0, env.releaseSeconds);
|
const double r = std::max(0.0, env.releaseSeconds);
|
||||||
const double sus = clamp01(env.sustainLevel);
|
const double sus = clamp01(env.sustainLevel);
|
||||||
|
|
||||||
// BOUNDED SCHEMATIC (FA2): A/H/D and R map onto the TIMED region (canvas minus the reserved
|
// A/H/D/R map onto the timed region at the param-domain scale, each segment getting a
|
||||||
// sustain-plateau width) at the PARAM-DOMAIN scale — sample-length-free — and every segment
|
// kGateNodeSepPx base so nodes never coincide even at the tier-0 zero-hold/zero-decay
|
||||||
// gets a kGateNodeSepPx base so consecutive nodes never coincide (every node individually
|
// defaults. The sustain plateau is the fixed reserve between DecayEnd and ReleaseStart.
|
||||||
// grabbable at any params, incl. the tier-0 zero-hold/zero-decay defaults). The sustain
|
|
||||||
// plateau is the fixed reserve between DecayEnd and ReleaseStart.
|
|
||||||
const int W = std::max(1, area.width);
|
const int W = std::max(1, area.width);
|
||||||
const double sustainPx = static_cast<double>(W - gateTimedWidth(area));
|
const double sustainPx = static_cast<double>(W - gateTimedWidth(area));
|
||||||
const double sep = static_cast<double>(kGateNodeSepPx);
|
const double sep = static_cast<double>(kGateNodeSepPx);
|
||||||
@@ -104,10 +97,9 @@ std::vector<EnvVertex> gatePolyline(const AmpEnvelope& env, const Rect& area) {
|
|||||||
double xPlateau = xDecay + sustainPx; // ReleaseStart (schematic note-off)
|
double xPlateau = xDecay + sustainPx; // ReleaseStart (schematic note-off)
|
||||||
double xRelease = xPlateau + sep + r * pps; // ReleaseEnd
|
double xRelease = xPlateau + sep + r * pps; // ReleaseEnd
|
||||||
|
|
||||||
// Right-edge overrun (a stored stage beyond the schematic domain): compress from the RIGHT
|
// Overrun beyond the schematic domain compresses from the right, preserving minimum gaps so
|
||||||
// preserving the minimum gaps, so trailing nodes stay individually separated instead of
|
// trailing nodes stay separated instead of piling on the last column. This re-floor only
|
||||||
// piling on the last column. The re-floor pass only bites when the canvas is too narrow to
|
// bites when the canvas is too narrow to hold the gaps at all — gateVtx's clamp wins then.
|
||||||
// hold the minimum gaps at all — then gateVtx's [0, W-1] clamp wins (in-bounds > separation).
|
|
||||||
const double xMax = static_cast<double>(W - 1);
|
const double xMax = static_cast<double>(W - 1);
|
||||||
if (xRelease > xMax) {
|
if (xRelease > xMax) {
|
||||||
xRelease = xMax;
|
xRelease = xMax;
|
||||||
@@ -135,12 +127,11 @@ std::vector<EnvVertex> gatePolyline(const AmpEnvelope& env, const Rect& area) {
|
|||||||
|
|
||||||
std::vector<EnvVertex> triggerPolyline(const AmpEnvelope& env, const Rect& area,
|
std::vector<EnvVertex> triggerPolyline(const AmpEnvelope& env, const Rect& area,
|
||||||
double totalSeconds) {
|
double totalSeconds) {
|
||||||
// The played span is lengthFraction of the whole sample; fades are fractions OF that span.
|
// Played span is lengthFraction of the whole sample; fades are fractions of that span.
|
||||||
const double len = clamp01(env.lengthFraction);
|
const double len = clamp01(env.lengthFraction);
|
||||||
double fadeIn = clamp01(env.fadeInFraction);
|
double fadeIn = clamp01(env.fadeInFraction);
|
||||||
double fadeOut = clamp01(env.fadeOutFraction);
|
double fadeOut = clamp01(env.fadeOutFraction);
|
||||||
// Fades cannot overlap: clamp so fadeIn + fadeOut <= 1 (of the played span), mirroring the
|
// Fades cannot overlap; trim fade-out first, matching the engine's TriggerParams clamp.
|
||||||
// engine's TriggerParams clamp. Trim the LATER fade (fade-out) first, matching the engine.
|
|
||||||
if (fadeIn + fadeOut > 1.0) fadeOut = std::max(0.0, 1.0 - fadeIn);
|
if (fadeIn + fadeOut > 1.0) fadeOut = std::max(0.0, 1.0 - fadeIn);
|
||||||
|
|
||||||
const double playSeconds = len * totalSeconds;
|
const double playSeconds = len * totalSeconds;
|
||||||
@@ -161,12 +152,10 @@ std::vector<EnvVertex> triggerPolyline(const AmpEnvelope& env, const Rect& area,
|
|||||||
std::vector<EnvVertex> buildEnvelopePolyline(const AmpEnvelope& env, const Rect& area,
|
std::vector<EnvVertex> buildEnvelopePolyline(const AmpEnvelope& env, const Rect& area,
|
||||||
double totalSeconds) {
|
double totalSeconds) {
|
||||||
if (area.width <= 0 || area.height <= 0 || totalSeconds <= 0.0) {
|
if (area.width <= 0 || area.height <= 0 || totalSeconds <= 0.0) {
|
||||||
// Degenerate surface: a two-point flat baseline at level 0 so the shell always has a line.
|
// Degenerate surface: flat two-point baseline so the shell always has a line.
|
||||||
return {vtx(EnvNode::Origin, area, 1.0, 0.0, 0.0),
|
return {vtx(EnvNode::Origin, area, 1.0, 0.0, 0.0),
|
||||||
vtx(EnvNode::ReleaseEnd, area, 1.0, 1.0, 0.0)};
|
vtx(EnvNode::ReleaseEnd, area, 1.0, 1.0, 0.0)};
|
||||||
}
|
}
|
||||||
// Gate is a param-domain schematic — totalSeconds only gates the degenerate branch above
|
|
||||||
// (no loaded duration -> baseline); Trigger is PCM-aligned and consumes it.
|
|
||||||
return env.mode == EnvMode::Gate ? gatePolyline(env, area)
|
return env.mode == EnvMode::Gate ? gatePolyline(env, area)
|
||||||
: triggerPolyline(env, area, totalSeconds);
|
: triggerPolyline(env, area, totalSeconds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,7 @@
|
|||||||
// envelope_overlay.h — PURE amp-envelope → polyline geometry for the S-VIEW-3 Sample-view
|
// envelope_overlay.h — amp-envelope -> polyline geometry for the Sample-view envelope overlay.
|
||||||
// envelope overlay. NO VST3, NO REAPER, NO SWELL/LICE types at the boundary. The mirror of
|
// Engine-free by design (no sample_map/sampler_core dependency); mirror of waveform_view /
|
||||||
// waveform_view / param_slider: the params→pixel polyline math lives here, unit-tested outside
|
// param_slider. The shell packs the zone's AdsrSeconds/TriggerParams into AmpEnvelope and draws
|
||||||
// the DAW, while the editor shell (reasampler_editor.cpp) traces the polyline in an accent hue
|
// the polyline plus a handle at each node (envelope_edit does the hit-test).
|
||||||
// and draws the node handles (via envelope_edit's hit-test).
|
|
||||||
//
|
|
||||||
// WHAT IT DRAWS. The amp envelope over the Sample view's hero waveform (Simpler / Phase-Plant
|
|
||||||
// grammar):
|
|
||||||
// * Gate -> the AHDSR shape: attack ramp 0->1, hold plateau at 1, decay 1->sustain,
|
|
||||||
// sustain plateau, release sustain->0. Since there is no held note-off to draw
|
|
||||||
// against, Gate is a BOUNDED SCHEMATIC (FA2): a fixed fraction of the canvas
|
|
||||||
// width (kGateSustainDisplayFraction) is RESERVED for the sustain plateau, and
|
|
||||||
// the remaining "timed" width carries A/H/D AND the release at the PARAM-DOMAIN
|
|
||||||
// scale — the timed width represents 4 x kGateStageMaxSeconds (the four stage
|
|
||||||
// sliders end-to-end at their maxima), NOT the sample's duration, so the layout
|
|
||||||
// is identical for a 0.3s and a 10s capture. Each segment additionally gets a
|
|
||||||
// kGateNodeSepPx pixel base, so consecutive nodes NEVER coincide: every Gate
|
|
||||||
// node is individually grabbable at ANY param values, including the tier-0
|
|
||||||
// defaults (hold 0 / decay 0). A -> (H) -> D -> S-plateau -> R all render INSIDE
|
|
||||||
// the canvas and the release is a visible, draggable segment.
|
|
||||||
// * Trigger -> the fade/%-length shape: fade-in 0->1, unity plateau, fade-out 1->0 anchored
|
|
||||||
// to playEnd (= lengthFraction of the post-start span). Trigger keeps the
|
|
||||||
// waveform's exact time base so the shape lines up with the PCM under it.
|
|
||||||
// The horizontal axis is TIME (Gate: schematic, see above; Trigger: wall-clock across the rect);
|
|
||||||
// the vertical axis is LEVEL (0 at rect bottom, 1 at rect top).
|
|
||||||
//
|
|
||||||
// BOUNDS INVARIANT (FA2). EVERY vertex of EVERY polyline is clamped inside the canvas:
|
|
||||||
// x in [area.x, area.right()-1], y in [area.y, area.bottom()-1] (half-open rect convention).
|
|
||||||
// No node and no drawn segment ever exceeds the canvas — paint-time clipping of handles is no
|
|
||||||
// longer needed (and never fires) in the shell.
|
|
||||||
//
|
|
||||||
// FA2 CONTRACT CHANGE — WAVE B SHELL AUTHOR, READ THIS:
|
|
||||||
// * The EnvNode enum is UNCHANGED (same node set, same draggable set — Origin + ReleaseStart
|
|
||||||
// remain the only non-draggable anchors).
|
|
||||||
// * ALL vertices are now in-bounds (see above). The shell's previous "skip handle when
|
|
||||||
// v.x >= waveArea.right()" clip is dead code: ReleaseEnd (Gate) and FadeOutStart/LengthEnd
|
|
||||||
// (Trigger, at full length / zero fade-out) now land at area.right()-1 and MUST get handles.
|
|
||||||
// * Gate's x-axis is SCHEMATIC, not PCM-aligned: the timed region is scaled to the param
|
|
||||||
// domain (4 x kGateStageMaxSeconds), the sustain reserve is a fixed width, and every
|
|
||||||
// segment carries a kGateNodeSepPx pixel base. The Gate curve does NOT line up with the
|
|
||||||
// waveform under it — do not label it as if it did. Trigger's x-axis IS still PCM-aligned.
|
|
||||||
// * Gate nodes never coincide (min-separation, above), so every Gate handle is individually
|
|
||||||
// grabbable in every state. nodeAtPoint (envelope_edit) resolves to the NEAREST node within
|
|
||||||
// the grab radius with a draw-order tie-break; the tie-break only matters for the one
|
|
||||||
// remaining coincidence, Trigger's zero-fade-out (FadeOutStart overlays LengthEnd at the
|
|
||||||
// right edge and wins the tie, so the fade can be dragged open from zero).
|
|
||||||
//
|
|
||||||
// DELIBERATELY ENGINE-FREE (house pattern — param_slider does the same). It does NOT depend on
|
|
||||||
// sample_map / sampler_core (which would drag bank_book / wav_codec in). The shell reads the
|
|
||||||
// zone's AdsrSeconds / TriggerParams and packs them into the small AmpEnvelope view struct here.
|
|
||||||
// AHDSR times are wall-clock SECONDS (rate-free, matching the stored domain — Daniel's no-
|
|
||||||
// hardcoded-rate ruling); Trigger fades are FRACTIONS of the play span. The one rate-bound input
|
|
||||||
// is the total sample duration in seconds, which the shell resolves once from the live rate and
|
|
||||||
// the frame count and passes in — this module never sees a sample rate.
|
|
||||||
//
|
|
||||||
// It reuses editor_geometry's Rect + contains(), the one shared geometry idiom.
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -64,166 +12,96 @@
|
|||||||
|
|
||||||
namespace reasampler::instrument::ui {
|
namespace reasampler::instrument::ui {
|
||||||
|
|
||||||
// The play mode the overlay draws — a LOCAL mirror of sampler_core's PlayMode kept here so the
|
// Local mirror of sampler_core's PlayMode, kept here so this module stays engine-free.
|
||||||
// geometry module stays engine-free (the shell maps the zone's PlayMode to this). Same two cases.
|
|
||||||
enum class EnvMode { Gate, Trigger };
|
enum class EnvMode { Gate, Trigger };
|
||||||
|
|
||||||
// Which breakpoint a polyline vertex / node is. The shell draws a draggable handle at each of
|
// Gate nodes: Origin -> AttackEnd -> HoldEnd -> DecayEnd(sustain) -> ReleaseStart -> ReleaseEnd.
|
||||||
// these; envelope_edit hit-tests against them. Kept in one enum shared by overlay + edit so the
|
// Trigger nodes: Origin -> FadeInEnd -> FadeOutStart -> LengthEnd(playEnd).
|
||||||
// forward map (draw) and inverse map (edit) name the same nodes.
|
// Shared by envelope_overlay (forward/draw map) and envelope_edit (inverse/edit map).
|
||||||
//
|
|
||||||
// Gate nodes: Origin -> AttackEnd -> HoldEnd -> DecayEnd(=sustain corner) -> ReleaseStart
|
|
||||||
// -> ReleaseEnd. The sustain node is DecayEnd (its Y is the sustain level);
|
|
||||||
// ReleaseStart is a drawing-only plateau-end vertex (the schematic note-off);
|
|
||||||
// release is edited by dragging ReleaseEnd.
|
|
||||||
// Trigger nodes: Origin -> FadeInEnd -> FadeOutStart -> LengthEnd(playEnd, level 0). The fade-out
|
|
||||||
// ramp is the FadeOutStart->LengthEnd segment; LengthEnd is the playEnd terminal.
|
|
||||||
enum class EnvNode {
|
enum class EnvNode {
|
||||||
Origin, // t=0, level 0 (both modes) — not draggable (fixed anchor)
|
Origin, // t=0, level 0 — not draggable
|
||||||
AttackEnd, // Gate: top of the attack ramp (level 1) — X sets attackSeconds
|
AttackEnd, // Gate: attack ramp top — sets attackSeconds
|
||||||
HoldEnd, // Gate: end of the hold plateau (level 1) — X sets holdSeconds
|
HoldEnd, // Gate: hold plateau end — sets holdSeconds
|
||||||
DecayEnd, // Gate: decay settles to sustain — the SUSTAIN node (X sets decaySeconds,
|
DecayEnd, // Gate: decay settles to sustain — sets decaySeconds (X) and sustainLevel (Y)
|
||||||
// Y sets sustainLevel)
|
ReleaseStart, // Gate: sustain plateau end — drawing-only, not draggable
|
||||||
ReleaseStart, // Gate: end of the sustain plateau / start of the release (sustain level) —
|
ReleaseEnd, // Gate: release tail end — sets releaseSeconds
|
||||||
// a DRAWING vertex only, not a draggable handle (release is edited at
|
FadeInEnd, // Trigger: fade-in top — sets fadeInFraction
|
||||||
// ReleaseEnd; this vertex sits a fixed sustain-plateau width right of
|
FadeOutStart, // Trigger: fade-out start — sets fadeOutFraction
|
||||||
// DecayEnd — the schematic note-off — Y = sustain level)
|
LengthEnd, // Trigger: playEnd terminal — sets lengthFraction
|
||||||
ReleaseEnd, // Gate: end of the release tail (level 0) — X sets releaseSeconds
|
|
||||||
FadeInEnd, // Trigger: top of the fade-in (level 1) — X sets fadeInFraction
|
|
||||||
FadeOutStart, // Trigger: end of the unity plateau / start of the fade-out (level 1) —
|
|
||||||
// X sets fadeOutFraction
|
|
||||||
LengthEnd, // Trigger: the playEnd terminal / %-length (level 0) — X sets lengthFraction
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// The amp-envelope params the overlay draws — the small view struct the shell packs from the
|
// Amp-envelope params the overlay draws. Trigger's fadeIn/fadeOutFraction are derived from
|
||||||
// zone's stored AdsrSeconds / TriggerParams. Engine-free by design (no sampler_core include).
|
// TriggerParams' frame counts, not a direct field copy — see the trigger_seam gotcha in
|
||||||
//
|
// core/instrument/CLAUDE.md.
|
||||||
// Gate fields (SECONDS, wall-clock): attack / hold / decay / release; sustain is a LEVEL 0..1.
|
|
||||||
// These map 1-to-1 with the stored AdsrSeconds fields — no conversion required.
|
|
||||||
//
|
|
||||||
// Trigger fields (FRACTIONS of play): fadeIn / fadeOut as a fraction of the played span;
|
|
||||||
// lengthFraction is the played span as a fraction of the
|
|
||||||
// post-start sample length (matching TriggerParams).
|
|
||||||
//
|
|
||||||
// TRIGGER SEAM — CONVERSION REQUIRED ON BOTH PATHS (Wave 2 shell author, read this):
|
|
||||||
// TriggerParams (sampler_core.h) stores Trigger fades as SOURCE FRAMES:
|
|
||||||
// fadeInFrames (int64_t) — 0->1 ramp length in source frames
|
|
||||||
// fadeOutFrames (int64_t) — 1->0 ramp length in source frames
|
|
||||||
// AmpEnvelope stores them as FRACTIONS of the played span:
|
|
||||||
// fadeInFraction = fadeInFrames / playLengthFrames
|
|
||||||
// fadeOutFraction = fadeOutFrames / playLengthFrames
|
|
||||||
// where playLengthFrames = round(lengthFraction * (frameCount - startFrame)).
|
|
||||||
// This is a NON-TRIVIAL derived view — NOT a direct field copy. The shell owes a
|
|
||||||
// converter on BOTH directions:
|
|
||||||
// PACK (draw): frames -> fraction (TriggerParams -> AmpEnvelope, needs frameCount + rate)
|
|
||||||
// UNPACK (commit): fraction -> frames (AmpEnvelope -> TriggerParams, same inputs)
|
|
||||||
// lengthFraction maps 1-to-1 with TriggerParams::lengthFraction and needs no conversion.
|
|
||||||
//
|
|
||||||
// Unused fields for the active mode are ignored.
|
|
||||||
struct AmpEnvelope {
|
struct AmpEnvelope {
|
||||||
EnvMode mode = EnvMode::Gate;
|
EnvMode mode = EnvMode::Gate;
|
||||||
|
|
||||||
// Gate (AHDSR), seconds + a dimensionless sustain level.
|
// Gate (AHDSR): seconds, plus a dimensionless sustain level.
|
||||||
double attackSeconds = 0.003;
|
double attackSeconds = 0.003;
|
||||||
double holdSeconds = 0.0;
|
double holdSeconds = 0.0;
|
||||||
double decaySeconds = 0.0;
|
double decaySeconds = 0.0;
|
||||||
double sustainLevel = 1.0;
|
double sustainLevel = 1.0;
|
||||||
double releaseSeconds = 0.060;
|
double releaseSeconds = 0.060;
|
||||||
|
|
||||||
// Trigger, fractions of the play span (fadeIn/fadeOut) and of the post-start length.
|
// Trigger: fractions of the played span.
|
||||||
// NOTE: fadeInFraction/fadeOutFraction are DERIVED from TriggerParams::fadeInFrames/
|
double lengthFraction = 1.0;
|
||||||
// fadeOutFrames — see the TRIGGER SEAM note above. A converter is owed on both the
|
double fadeInFraction = 0.0;
|
||||||
// pack (draw) and unpack (commit) paths; these fields are NOT a direct TriggerParams copy.
|
double fadeOutFraction = 0.0;
|
||||||
double lengthFraction = 1.0; // (0,1] of the post-start span that plays (1-to-1 with TriggerParams)
|
|
||||||
double fadeInFraction = 0.0; // 0->1 ramp as a fraction of the played span (DERIVED — see above)
|
|
||||||
double fadeOutFraction = 0.0; // 1->0 ramp as a fraction of the played span (DERIVED — see above)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// One polyline vertex: a pixel point plus which node it is. The shell draws a line through the
|
// One polyline vertex: pixel point plus which node it is. level is redundant with y, carried for
|
||||||
// points in order (the amp curve) and a draggable handle at each vertex whose node is not Origin.
|
// inspection.
|
||||||
// Level is carried alongside (0..1) for callers that want to label/inspect; it is redundant with y.
|
|
||||||
struct EnvVertex {
|
struct EnvVertex {
|
||||||
EnvNode node = EnvNode::Origin;
|
EnvNode node = EnvNode::Origin;
|
||||||
int x = 0; // pixel x inside the overlay rect
|
int x = 0;
|
||||||
int y = 0; // pixel y inside the overlay rect (top = level 1, bottom = level 0)
|
int y = 0;
|
||||||
double level = 0.0; // 0..1, the vertex's amplitude (redundant with y; for inspection)
|
double level = 0.0;
|
||||||
|
|
||||||
bool operator==(const EnvVertex& o) const {
|
bool operator==(const EnvVertex& o) const {
|
||||||
return node == o.node && x == o.x && y == o.y && level == o.level;
|
return node == o.node && x == o.x && y == o.y && level == o.level;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// The fraction of the canvas width RESERVED for the Gate sustain-plateau display (FA2). The
|
// Fraction of canvas width reserved for the Gate sustain-plateau display; the remaining width
|
||||||
// plateau is a fixed-width schematic region between DecayEnd and ReleaseStart; the remaining
|
// carries A/H/D/R at the param-domain scale. Shared with envelope_edit.
|
||||||
// width is the "timed" region A/H/D/R map onto at the schematic param-domain scale. One
|
|
||||||
// constant shared by the forward map (here) and the inverse map (envelope_edit).
|
|
||||||
inline constexpr double kGateSustainDisplayFraction = 0.15;
|
inline constexpr double kGateSustainDisplayFraction = 0.15;
|
||||||
|
|
||||||
// The minimum pixel separation between consecutive Gate polyline nodes: every Gate segment gets
|
// Minimum pixel separation between consecutive Gate nodes, so zero-duration stages (tier-0
|
||||||
// this many px as a base, PLUS its time-proportional extent, so zero-duration stages (tier-0
|
// defaults) still render as distinct, grabbable handles. Larger than envelope_edit's grab
|
||||||
// defaults: hold 0, decay 0) still render as distinct, individually grabbable handles. Chosen
|
// radius (6) so a click can never tie between neighbours.
|
||||||
// larger than envelope_edit's kNodeGrabRadius (6) so a click dead-on a node can never tie with
|
|
||||||
// its neighbour. Shared by the forward map and the drag inverse.
|
|
||||||
inline constexpr int kGateNodeSepPx = 8;
|
inline constexpr int kGateNodeSepPx = 8;
|
||||||
|
|
||||||
// The Gate schematic's per-stage time domain (seconds): the timed region represents the four
|
// Gate schematic's per-stage time domain (seconds) — the timed region represents four stages
|
||||||
// stages end-to-end at this maximum each (4 x this total). MIRRORS the shell's stage-slider
|
// end-to-end at this max each. Must match the shell's stage-slider ceiling so a maxed slider
|
||||||
// ceiling (kEnvTimeMaxSeconds in reasampler_editor.cpp) — keep the two equal so a stage at its
|
// lands exactly at the canvas edge.
|
||||||
// slider max lands exactly at the canvas edge. Drag safety does NOT depend on this constant
|
|
||||||
// (param clamps are caller-supplied in envelope_edit); only layout does.
|
|
||||||
inline constexpr double kGateStageMaxSeconds = 2.0;
|
inline constexpr double kGateStageMaxSeconds = 2.0;
|
||||||
|
|
||||||
// The pixel width of the Gate timed region: area.width minus the sustain-plateau reserve,
|
// Pixel width of the Gate timed region (area width minus the sustain reserve), floored at 1 for
|
||||||
// floored at 1 px so the px<->seconds scale never degenerates for a non-empty area. Returns 0
|
// a non-empty area; 0 for a zero/negative-width area.
|
||||||
// for a zero/negative-width area. Shared by gatePolyline and envelope_edit's gate drag scale.
|
|
||||||
int gateTimedWidth(const Rect& area);
|
int gateTimedWidth(const Rect& area);
|
||||||
|
|
||||||
// Pixels per second of the Gate timed region under the PARAM-DOMAIN scale: the timed width,
|
// Pixels per second of the Gate timed region, independent of the sample's actual duration.
|
||||||
// minus the four per-segment kGateNodeSepPx bases and the last in-bounds column, spread over
|
// Shared by buildEnvelopePolyline and envelope_edit's drag inverse so a dragged handle tracks
|
||||||
// 4 x kGateStageMaxSeconds. Independent of the sample's duration. Returns 0 for a
|
// the cursor 1:1.
|
||||||
// zero/negative-width area; otherwise > 0 (the usable width floors at 1 px). The ONE px<->sec
|
|
||||||
// scale shared by the forward map (gatePolyline) and the drag inverse (envelope_edit), so a
|
|
||||||
// dragged handle tracks the cursor 1:1.
|
|
||||||
double gatePxPerSecond(const Rect& area);
|
double gatePxPerSecond(const Rect& area);
|
||||||
|
|
||||||
// Map an amp envelope to its polyline vertices inside `area`, over a sample of `totalSeconds`
|
// Maps an amp envelope to polyline vertices inside `area` over a sample of `totalSeconds`
|
||||||
// wall-clock duration. `area` is the waveform rect (left/top inclusive, right/bottom exclusive);
|
// duration. y maps level [0,1] across [area.bottom()-1, area.y] (level 1 at the top); vertices
|
||||||
// y maps level 0..1 across [area.bottom()-1 .. area.y] (level 1 at the TOP). The polyline reads
|
// are in draw order, Origin first.
|
||||||
// left-to-right in draw order, Origin first.
|
|
||||||
//
|
//
|
||||||
// TIME BASE (FA2).
|
// Gate's x-axis is a bounded schematic independent of totalSeconds (does NOT line up with the
|
||||||
// * Gate: a bounded schematic, INDEPENDENT of totalSeconds. The canvas splits into a TIMED
|
// waveform under it); Trigger's x-axis is PCM-aligned wall-clock. Every vertex is clamped inside
|
||||||
// region of gateTimedWidth(area) px — where attack/hold/decay run from t=0 and the release
|
// the canvas: x in [area.x, area.right()-1], y in [area.y, area.bottom()-1]. A degenerate area
|
||||||
// ramp runs after the plateau, at the gatePxPerSecond(area) PARAM-DOMAIN scale, each segment
|
// or totalSeconds <= 0 yields the flat two-point baseline [Origin, end at level 0].
|
||||||
// carrying a kGateNodeSepPx base so consecutive nodes never coincide — plus a FIXED sustain
|
|
||||||
// plateau of (width - timedWidth) px between DecayEnd and ReleaseStart (the schematic
|
|
||||||
// note-off). Stages beyond the schematic domain (a stored stage > kGateStageMaxSeconds)
|
|
||||||
// compress from the RIGHT preserving the minimum gaps, so trailing nodes stay individually
|
|
||||||
// separated instead of piling on the last column; only a canvas too narrow to hold the
|
|
||||||
// minimum gaps at all sacrifices separation (in-bounds wins).
|
|
||||||
// * Trigger: the waveform's exact time base (PCM-aligned). The played span is
|
|
||||||
// lengthFraction * totalSeconds; fade-in/out are fractions OF that played span. Nodes past
|
|
||||||
// the played span never appear (FadeOutStart/LengthEnd sit at the played span's right edge).
|
|
||||||
//
|
|
||||||
// BOUNDS: every vertex is inside the canvas — x in [area.x, area.right()-1], y in
|
|
||||||
// [area.y, area.bottom()-1]. Nothing maps past area.right() (the pre-FA2 release tail is gone). A
|
|
||||||
// degenerate area (zero width/height) or totalSeconds <= 0 yields the two-point flat baseline
|
|
||||||
// [Origin, end at level 0] so the shell always has a drawable line. Pure — same inputs, same
|
|
||||||
// polyline.
|
|
||||||
std::vector<EnvVertex> buildEnvelopePolyline(const AmpEnvelope& env, const Rect& area,
|
std::vector<EnvVertex> buildEnvelopePolyline(const AmpEnvelope& env, const Rect& area,
|
||||||
double totalSeconds);
|
double totalSeconds);
|
||||||
|
|
||||||
// Map a time (seconds) to a pixel x inside `area`: t=0 -> area.x, t=totalSeconds ->
|
// Maps a time (seconds) to a pixel x inside `area`, linear and clamped at both ends. Shared
|
||||||
// area.right()-1, linear, CLAMPED on both sides (t < 0 pins to area.x; t past totalSeconds pins
|
// with envelope_edit's node hit-test so the drawn handle and its grab region agree.
|
||||||
// to area.right()-1 — the in-bounds invariant, FA2). A zero-width area or totalSeconds <= 0 yields
|
|
||||||
// area.x. Pure — the shared time->x map the Trigger polyline and the node hit-test
|
|
||||||
// (envelope_edit) use, so the drawn handle and its grab region agree.
|
|
||||||
int timeToX(const Rect& area, double totalSeconds, double t);
|
int timeToX(const Rect& area, double totalSeconds, double t);
|
||||||
|
|
||||||
// Map a level (0..1) to a pixel y inside `area`: level 1 -> area.y, level 0 -> area.bottom()-1
|
// Maps a level [0,1] to a pixel y inside `area` (level 1 at the top, 0 at the bottom row),
|
||||||
// (so the full-amplitude line sits at the top edge and silence at the bottom pixel row). level is
|
// clamped. Shared with envelope_edit's node hit-test.
|
||||||
// clamped to [0,1]. A zero-height area yields area.y. Pure — the shared level->y map the polyline
|
|
||||||
// and the node hit-test share.
|
|
||||||
int levelToY(const Rect& area, double level);
|
int levelToY(const Rect& area, double level);
|
||||||
|
|
||||||
} // namespace reasampler::instrument::ui
|
} // namespace reasampler::instrument::ui
|
||||||
|
|||||||
@@ -14,10 +14,8 @@ int clampNote(int n) {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map a key BOUNDARY in [0, kStripKeyCount] to an x pixel inside a band of the given
|
// Maps a key boundary (0..128) to an x pixel. Key N's left is keyEdgeToX(N), right is
|
||||||
// left/width. keyEdge is a boundary (0..128): 0 -> band left, 128 -> band right. Integer
|
// keyEdgeToX(N+1) — tiles adjacent keys/zones without a seam. Mirrors embed_strip::keyEdgeToX.
|
||||||
// math, floored — key N's left is keyEdgeToX(N) and its right is keyEdgeToX(N+1), tiling
|
|
||||||
// adjacent keys/zones without a seam (mirror of embed_strip::keyEdgeToX).
|
|
||||||
int keyEdgeToX(int bandLeft, int bandWidth, int keyEdge) {
|
int keyEdgeToX(int bandLeft, int bandWidth, int keyEdge) {
|
||||||
if (keyEdge <= 0) return bandLeft;
|
if (keyEdge <= 0) return bandLeft;
|
||||||
if (keyEdge >= kStripKeyCount) return bandLeft + bandWidth;
|
if (keyEdge >= kStripKeyCount) return bandLeft + bandWidth;
|
||||||
@@ -37,8 +35,7 @@ StripLayout layoutStrip(int w, int h) {
|
|||||||
int keyLeftX(const StripLayout& layout, int note) {
|
int keyLeftX(const StripLayout& layout, int note) {
|
||||||
const Rect& band = layout.keys;
|
const Rect& band = layout.keys;
|
||||||
const int bandWidth = std::max(0, band.width);
|
const int bandWidth = std::max(0, band.width);
|
||||||
// note is a KEY here (0..127); its left edge is boundary `note`. Callers pass note+1 to
|
// note is a key (0..127); callers pass note+1 to get its right edge, 128 -> band right.
|
||||||
// get a key's right edge, and 128 maps to the band right.
|
|
||||||
const int edge = note < 0 ? 0 : (note > kStripKeyCount ? kStripKeyCount : note);
|
const int edge = note < 0 ? 0 : (note > kStripKeyCount ? kStripKeyCount : note);
|
||||||
return keyEdgeToX(band.x, bandWidth, edge);
|
return keyEdgeToX(band.x, bandWidth, edge);
|
||||||
}
|
}
|
||||||
@@ -59,8 +56,7 @@ int keyAtPoint(const StripLayout& layout, int x, int y) {
|
|||||||
if (!contains(band, x, y)) return -1;
|
if (!contains(band, x, y)) return -1;
|
||||||
const int bandWidth = std::max(0, band.width);
|
const int bandWidth = std::max(0, band.width);
|
||||||
if (bandWidth <= 0) return -1;
|
if (bandWidth <= 0) return -1;
|
||||||
// Invert keyEdgeToX: the key whose half-open [leftX, rightX) contains x. Floor-divide
|
// Inverts keyEdgeToX: the key whose half-open [leftX, rightX) contains x.
|
||||||
// the pixel offset back to a key; clamp defensively (a point on band.right()-1 maps to 127).
|
|
||||||
const int offset = x - band.x;
|
const int offset = x - band.x;
|
||||||
int note = (offset * kStripKeyCount) / bandWidth;
|
int note = (offset * kStripKeyCount) / bandWidth;
|
||||||
return clampNote(note);
|
return clampNote(note);
|
||||||
@@ -69,7 +65,7 @@ int keyAtPoint(const StripLayout& layout, int x, int y) {
|
|||||||
Rect zoneBarRect(const StripLayout& layout, int lowNote, int highNote) {
|
Rect zoneBarRect(const StripLayout& layout, int lowNote, int highNote) {
|
||||||
int lo = clampNote(lowNote);
|
int lo = clampNote(lowNote);
|
||||||
int hi = clampNote(highNote);
|
int hi = clampNote(highNote);
|
||||||
if (lo > hi) lo = hi; // defensive: a malformed zone collapses rather than inverts
|
if (lo > hi) lo = hi; // malformed zone collapses rather than inverts
|
||||||
const int leftX = keyLeftX(layout, lo);
|
const int leftX = keyLeftX(layout, lo);
|
||||||
const int rightX = keyLeftX(layout, hi + 1);
|
const int rightX = keyLeftX(layout, hi + 1);
|
||||||
return Rect::ltrb(leftX, layout.keys.y, std::max(leftX, rightX), layout.keys.bottom());
|
return Rect::ltrb(leftX, layout.keys.y, std::max(leftX, rightX), layout.keys.bottom());
|
||||||
@@ -80,8 +76,7 @@ ZoneGrab zoneGrabAt(const StripLayout& layout, int lowNote, int highNote, int x,
|
|||||||
if (!contains(bar, x, y)) return ZoneGrab::kNone;
|
if (!contains(bar, x, y)) return ZoneGrab::kNone;
|
||||||
|
|
||||||
const int barW = bar.width;
|
const int barW = bar.width;
|
||||||
// A narrow bar (< 2*edge) has no body: split at the midpoint, LOW edge wins the tie so
|
// A narrow bar has no body: split at the midpoint, low edge wins the tie.
|
||||||
// a click exactly on the midpoint resizes low (deterministic).
|
|
||||||
if (barW < 2 * kStripEdgeGrabWidth) {
|
if (barW < 2 * kStripEdgeGrabWidth) {
|
||||||
const int mid = bar.x + barW / 2;
|
const int mid = bar.x + barW / 2;
|
||||||
return x <= mid ? ZoneGrab::kLowEdge : ZoneGrab::kHighEdge;
|
return x <= mid ? ZoneGrab::kLowEdge : ZoneGrab::kHighEdge;
|
||||||
@@ -103,11 +98,7 @@ ZoneBarHit zoneBarAtPoint(const StripLayout& layout, const int* lows, const int*
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool isNaturalKey(int note) {
|
bool isNaturalKey(int note) {
|
||||||
// Clamp to the valid MIDI range before indexing.
|
|
||||||
const int n = note < 0 ? 0 : (note > kStripKeyCount - 1 ? kStripKeyCount - 1 : note);
|
const int n = note < 0 ? 0 : (note > kStripKeyCount - 1 ? kStripKeyCount - 1 : note);
|
||||||
// The 12-semitone pattern of natural (white) keys within an octave, starting at C:
|
|
||||||
// positions 0(C) 2(D) 4(E) 5(F) 7(G) 9(A) 11(B) are natural;
|
|
||||||
// positions 1(C#) 3(D#) 6(F#) 8(G#) 10(A#) are accidental.
|
|
||||||
static constexpr bool kNatural[12] = {
|
static constexpr bool kNatural[12] = {
|
||||||
true, // 0 C
|
true, // 0 C
|
||||||
false, // 1 C#
|
false, // 1 C#
|
||||||
@@ -129,13 +120,8 @@ int resolveDragNote(const StripLayout& layout, int startNote, int dxPixels) {
|
|||||||
if (dxPixels == 0) return clampNote(startNote);
|
if (dxPixels == 0) return clampNote(startNote);
|
||||||
const int bandWidth = std::max(0, layout.keys.width);
|
const int bandWidth = std::max(0, layout.keys.width);
|
||||||
if (bandWidth <= 0) return clampNote(startNote); // zero-width -> no motion
|
if (bandWidth <= 0) return clampNote(startNote); // zero-width -> no motion
|
||||||
// Proportional shift: same linear mapping as keyAtPoint/keyEdgeToX so click and drag
|
// Same linear mapping as keyAtPoint/keyEdgeToX (exact rational), not a truncated-integer
|
||||||
// agree across the full strip, even on non-divisible-by-128 widths. The proportional
|
// bandWidth/128 key width — that drifted at the far end of the strip.
|
||||||
// key width is (bandWidth / kStripKeyCount) in exact rational arithmetic; rounding to
|
|
||||||
// the nearest key (half-key drag flips at the key centre) is achieved by adding
|
|
||||||
// bandWidth/2 to the absolute pixel delta before dividing — identical to the old
|
|
||||||
// formula except keyWidth is now derived from the same linear map (exact rational)
|
|
||||||
// rather than the truncated-integer bandWidth/128 that caused drift at the far end.
|
|
||||||
const int half = bandWidth / 2;
|
const int half = bandWidth / 2;
|
||||||
int shift;
|
int shift;
|
||||||
if (dxPixels > 0) {
|
if (dxPixels > 0) {
|
||||||
|
|||||||
@@ -1,106 +1,70 @@
|
|||||||
// keyboard_strip.h — PURE layout + hit-test + drag math for the S10 capture-first
|
// keyboard_strip.h — layout + hit-test + drag math for the capture-first editor's
|
||||||
// editor's keyboard strip. NO VST3, NO REAPER, NO SWELL/LICE types at the boundary.
|
// keyboard strip. Mirror of editor_geometry/embed_strip/mode_switch; the shell draws
|
||||||
// The mirror of editor_geometry / embed_strip / mode_switch: the fiddly rectangle +
|
// and marshals mouse events into these functions.
|
||||||
// note-mapping arithmetic lives here so it is unit-tested outside the DAW, while the
|
|
||||||
// editor shell (reasampler_editor.cpp) draws the strip and marshals mouse events into
|
|
||||||
// these functions.
|
|
||||||
//
|
//
|
||||||
// The strip maps the full 128-key MIDI span across a horizontal band (the same key-span
|
// The strip maps the full 128-key MIDI span across a horizontal band (the same idiom
|
||||||
// idiom embed_strip uses). It serves TWO faces of the S10 editor:
|
// embed_strip uses) and serves two faces: the single-capture fast path (a root marker,
|
||||||
// * the SINGLE-CAPTURE fast path (default): one loaded capture with a ROOT MARKER on
|
// click-a-key or drag it to set root) and the opt-in zones panel (each zone drawn as a
|
||||||
// the strip, click-a-key (or drag the marker) sets the capture's root note; and
|
// bar with edge-grab resize handles + a body move-handle).
|
||||||
// * the opt-in ZONES panel (S10-Z, demoted): each performance zone drawn as a bar over
|
|
||||||
// the keys it covers, with edge-grab resize handles + a body move-handle so a drag
|
|
||||||
// sets low/high (edges) or moves the span (body), and a key-click sets the zone root.
|
|
||||||
//
|
|
||||||
// All interaction resolves through the pure DRAG-DELTA resolver here: the shell captures
|
|
||||||
// a grab on WM_LBUTTONDOWN, feeds each WM_MOUSEMOVE's pixel delta back through
|
|
||||||
// resolveDragNote, and commits the resolved note(s) on WM_LBUTTONUP. Live feedback is the
|
|
||||||
// shell re-drawing the in-flight note; one coherent edit lands on release.
|
|
||||||
//
|
|
||||||
// It reuses the same Rect + contains() as editor_geometry (one shared geometry idiom),
|
|
||||||
// so this header depends on editor_geometry.h rather than redefining a rectangle type.
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "core/instrument/ui/editor_geometry.h" // Rect, contains — one shared geometry idiom
|
#include "core/instrument/ui/editor_geometry.h" // Rect, contains
|
||||||
|
|
||||||
namespace reasampler::instrument::ui {
|
namespace reasampler::instrument::ui {
|
||||||
|
|
||||||
// The full MIDI key span the strip maps across its width: 128 keys (0..127). Named
|
// Named distinctly from embed_strip's kEmbedKeyCount (same value) so the two strips
|
||||||
// distinctly from embed_strip's kEmbedKeyCount (same value) so the two strips stay
|
// stay independent.
|
||||||
// independent — the editor strip may grow octave labels/metrics the embed strip never does.
|
|
||||||
inline constexpr int kStripKeyCount = 128;
|
inline constexpr int kStripKeyCount = 128;
|
||||||
|
|
||||||
// The width (px) of an edge-grab hit region at each end of a zone bar: a drag started
|
// Pixel width of a zone bar's edge-grab region. A zone narrower than 2x this has no
|
||||||
// within this many pixels of the bar's left/right edge resizes that edge; a drag started
|
// body move-handle (both edges win their halves).
|
||||||
// anywhere else on the bar moves the whole span. A zone narrower than 2*this has no body
|
|
||||||
// move-handle (both edges win their halves) — deliberate: a 1-key zone is all edges.
|
|
||||||
inline constexpr int kStripEdgeGrabWidth = 6;
|
inline constexpr int kStripEdgeGrabWidth = 6;
|
||||||
|
|
||||||
// The strip's regions, derived from the (w x h) band the shell allots it. The keys band
|
// The keys band takes the whole strip area today; clamped so a degenerate size never
|
||||||
// takes the whole area today (a future octave-label lane can carve a sub-band here without
|
// yields an inverted rect.
|
||||||
// changing callers). Clamped so a degenerate (tiny/zero) size never yields an inverted rect.
|
|
||||||
struct StripLayout {
|
struct StripLayout {
|
||||||
Rect keys; // the key band: the 128-key span maps linearly across keys.width
|
Rect keys;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Divide a (w x h) strip area into its regions. Pure: same inputs -> same layout. A zero or
|
// Divide a (w x h) strip area into its regions. Pure.
|
||||||
// negative size yields empty rects (no inversion).
|
|
||||||
StripLayout layoutStrip(int w, int h);
|
StripLayout layoutStrip(int w, int h);
|
||||||
|
|
||||||
// The x pixel (inside the keys band) of the LEFT edge of key `note` (0..127). The 128-key
|
// x pixel of the LEFT edge of key `note` (0..127) under the linear 128-key map; key N
|
||||||
// span maps linearly across keys.width; key N occupies the half-open pixel range
|
// occupies [keyLeftX(N), keyLeftX(N+1)). note==128 maps to the band's right edge.
|
||||||
// [keyLeftX(N), keyLeftX(N+1)). Notes are clamped to [0,127]; note==128 maps to the band's
|
|
||||||
// right edge (so a key's right edge is keyLeftX(note+1)). Pure.
|
|
||||||
int keyLeftX(const StripLayout& layout, int note);
|
int keyLeftX(const StripLayout& layout, int note);
|
||||||
|
|
||||||
// The half-open pixel rect of a single key `note` (0..127): [keyLeftX(note),
|
// Half-open rect of a single key `note`, clamped to [0,127].
|
||||||
// keyLeftX(note+1)) horizontally, the full keys-band height. A malformed (out-of-range)
|
|
||||||
// note clamps to [0,127]. Pure.
|
|
||||||
Rect keyRect(const StripLayout& layout, int note);
|
Rect keyRect(const StripLayout& layout, int note);
|
||||||
|
|
||||||
// The rect of the ROOT MARKER for the single-capture fast path: the key cell of `rootNote`,
|
// Root-marker rect for the single-capture fast path; equivalent to
|
||||||
// drawn as a highlighted key. Equivalent to keyRect(layout, rootNote) — a named entry point
|
// keyRect(layout, rootNote) but named so the intent reads at the call site.
|
||||||
// so the shell's intent (this is the root marker, not just any key) reads at the call site,
|
|
||||||
// and so a future marker shape (a triangle over the key) has one place to change. Pure.
|
|
||||||
Rect rootMarkerRect(const StripLayout& layout, int rootNote);
|
Rect rootMarkerRect(const StripLayout& layout, int rootNote);
|
||||||
|
|
||||||
// The MIDI note a point (x, y) lands on, or -1 for a point outside the keys band. Backs
|
// MIDI note a point (x, y) lands on, or -1 outside the keys band.
|
||||||
// click-to-set-root (single capture) and click-a-key-sets-zone-root (zones). Pure.
|
|
||||||
int keyAtPoint(const StripLayout& layout, int x, int y);
|
int keyAtPoint(const StripLayout& layout, int x, int y);
|
||||||
|
|
||||||
// The horizontal sub-rect of the keys band for a zone spanning [lowNote, highNote]
|
// Horizontal sub-rect for a zone spanning [lowNote, highNote] inclusive. Notes clamp to
|
||||||
// (inclusive): [keyLeftX(low), keyLeftX(high+1)) horizontally, the full band height. Notes
|
// [0,127] and low clamps to <= high, so a malformed zone never yields an inverted rect.
|
||||||
// clamp to [0,127] and low clamps to <= high, so a malformed zone yields an in-band
|
|
||||||
// (possibly zero-width) rect, never an inverted one. Mirrors embed_strip::zoneSegmentRect.
|
|
||||||
// Pure.
|
|
||||||
Rect zoneBarRect(const StripLayout& layout, int lowNote, int highNote);
|
Rect zoneBarRect(const StripLayout& layout, int lowNote, int highNote);
|
||||||
|
|
||||||
// Which part of a zone bar a grab landed on. The shell uses this to decide what a drag
|
// Which part of a zone bar a grab landed on: an edge resizes that boundary, the body
|
||||||
// edits: an edge resizes that boundary; the body moves the whole span; none means the grab
|
// moves the whole span, kNone means the grab missed the bar.
|
||||||
// missed the bar entirely (the shell may treat that as a key-click to set the root, or as a
|
|
||||||
// deselect).
|
|
||||||
enum class ZoneGrab {
|
enum class ZoneGrab {
|
||||||
kNone, // the point is not on this zone's bar
|
kNone,
|
||||||
kLowEdge, // within kStripEdgeGrabWidth of the bar's LEFT edge -> resize low
|
kLowEdge,
|
||||||
kHighEdge, // within kStripEdgeGrabWidth of the bar's RIGHT edge -> resize high
|
kHighEdge,
|
||||||
kBody, // on the bar but not an edge -> move the whole span
|
kBody,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Classify a grab at (x, y) against ONE zone's bar (low..high). Returns kNone when the
|
// Classify a grab at (x, y) against one zone's bar. A narrow bar (< 2*kStripEdgeGrabWidth)
|
||||||
// point is off the bar (or off the keys band). On the bar: kLowEdge/kHighEdge when within
|
// resolves the near half to each edge (no body); the low edge wins a tie at the exact
|
||||||
// kStripEdgeGrabWidth of that edge, else kBody. A narrow bar (< 2*kStripEdgeGrabWidth)
|
// midpoint.
|
||||||
// resolves the near half to each edge (no body). The LOW edge wins a tie at the exact
|
|
||||||
// midpoint of a narrow bar (deterministic). Pure.
|
|
||||||
ZoneGrab zoneGrabAt(const StripLayout& layout, int lowNote, int highNote, int x, int y);
|
ZoneGrab zoneGrabAt(const StripLayout& layout, int lowNote, int highNote, int x, int y);
|
||||||
|
|
||||||
// The zone (index into `lows`/`highs`, draw order) whose bar a grab at (x, y) lands on,
|
// Zone (index into the parallel `lows`/`highs` arrays, draw order) whose bar a grab
|
||||||
// plus which part of it, or {-1, kNone} for a point off every bar. First covering zone in
|
// lands on, plus which part, or {-1, kNone} for a miss. First covering zone in draw
|
||||||
// draw order wins (first-match, mirroring the core's Keymap::resolve + embed_strip). The
|
// order wins.
|
||||||
// arrays are parallel (lows[i]/highs[i] is zone i's inclusive range); `count` is their
|
|
||||||
// length. Pure — no host containers at the boundary (a raw pointer pair, like
|
|
||||||
// embed_strip::zoneAtPoint).
|
|
||||||
struct ZoneBarHit {
|
struct ZoneBarHit {
|
||||||
int zoneIndex = -1;
|
int zoneIndex = -1;
|
||||||
ZoneGrab grab = ZoneGrab::kNone;
|
ZoneGrab grab = ZoneGrab::kNone;
|
||||||
@@ -108,24 +72,13 @@ struct ZoneBarHit {
|
|||||||
ZoneBarHit zoneBarAtPoint(const StripLayout& layout, const int* lows, const int* highs,
|
ZoneBarHit zoneBarAtPoint(const StripLayout& layout, const int* lows, const int* highs,
|
||||||
int count, int x, int y);
|
int count, int x, int y);
|
||||||
|
|
||||||
// Resolve a drag to a new MIDI note. Given the note the grabbed field held at grab time
|
// Resolves a drag to a new MIDI note: `startNote` shifted by round(dxPixels / keyWidth),
|
||||||
// (`startNote`) and the horizontal pixel delta since grab (`dxPixels`), returns the note
|
// clamped to [0,127]. The one arithmetic behind edge-resize, body-move (apply to both
|
||||||
// the field should now hold: startNote shifted by round(dxPixels / keyWidth), clamped to
|
// edges with the same delta to preserve span), and root-marker drag.
|
||||||
// [0,127]. keyWidth is derived from the layout (band width / 128); a zero-width band pins
|
|
||||||
// the result to startNote (no motion). This is the single arithmetic behind edge-resize,
|
|
||||||
// body-move (apply to both edges with the SAME delta so the span is preserved), and
|
|
||||||
// root-marker drag. Pure — rounding is to the nearest key so a half-key drag flips at the
|
|
||||||
// key centre. Returns startNote unchanged for dxPixels==0.
|
|
||||||
int resolveDragNote(const StripLayout& layout, int startNote, int dxPixels);
|
int resolveDragNote(const StripLayout& layout, int startNote, int dxPixels);
|
||||||
|
|
||||||
// Returns true when `note` (0..127) is a NATURAL (white) key in standard 12-tone equal
|
// True when `note` (clamped to [0,127]) is a natural (white) key in 12-tone equal
|
||||||
// temperament; false when it is an ACCIDENTAL (black) key. Notes out of the [0,127]
|
// temperament; false for an accidental (black) key.
|
||||||
// range are clamped to [0,127] before classification (i.e. this never throws/UBs on a
|
|
||||||
// bad input). The 12 semitone positions within an octave:
|
|
||||||
// Natural (white): 0(C) 2(D) 4(E) 5(F) 7(G) 9(A) 11(B)
|
|
||||||
// Accidental (black): 1(C#) 3(D#) 6(F#) 8(G#) 10(A#)
|
|
||||||
// Used by the shell to overlay the two-tone bright/dark piano-key pattern over the
|
|
||||||
// pastel spectral fill (S-VIEW-7). Pure — no layout required, no host types.
|
|
||||||
bool isNaturalKey(int note);
|
bool isNaturalKey(int note);
|
||||||
|
|
||||||
} // namespace reasampler::instrument::ui
|
} // namespace reasampler::instrument::ui
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ DeckGroupLayout layoutGroup(const DeckGroupDesc& g, const Rect& box) {
|
|||||||
const int innerLeft = box.x + kDeckGroupPadX;
|
const int innerLeft = box.x + kDeckGroupPadX;
|
||||||
const int innerRight = box.right() - kDeckGroupPadX;
|
const int innerRight = box.right() - kDeckGroupPadX;
|
||||||
|
|
||||||
// Caption row: text left, compact toggle right-anchored (r11 — the not-full-width home).
|
// Caption row: text left, compact toggle right-anchored.
|
||||||
out.caption = Rect::ltrb(innerLeft, captionTop, innerRight, captionTop + kDeckCaptionH);
|
out.caption = Rect::ltrb(innerLeft, captionTop, innerRight, captionTop + kDeckCaptionH);
|
||||||
if (g.captionToggle.id >= 0) {
|
if (g.captionToggle.id >= 0) {
|
||||||
const int segW = g.captionToggle.segWidth;
|
const int segW = g.captionToggle.segWidth;
|
||||||
|
|||||||
@@ -1,27 +1,18 @@
|
|||||||
// knob_deck.h — PURE knob-deck layout + hit-test for the r11 Sample-face recomposition
|
// knob_deck.h — knob-deck layout + hit-test for the Sample-face knob deck. Engine-free
|
||||||
// (Wave B, FB1). NO VST3, NO REAPER, NO SWELL/LICE types at the boundary, and — like
|
// like param_slider: cells and toggles carry opaque shell-owned control ids. Mirror of
|
||||||
// param_slider — NO engine types: cells and toggles carry opaque shell-owned control ids.
|
// action_bar/param_slider; the knob primitive itself (value<->needle-angle, drag) is
|
||||||
// The mirror of action_bar / param_slider: the fiddly group-box / caption-row / cell-grid
|
// param_slider's — a knob cell here is just a rect the shell composes it into.
|
||||||
// arithmetic lives here, unit-tested outside the DAW, while the editor shell draws each
|
|
||||||
// group (fence, caption, compact toggles, knobs) through the L1 kit and routes clicks/drags
|
|
||||||
// via the hit-test. The KNOB PRIMITIVE itself (value<->needle-angle, vertical drag) is
|
|
||||||
// param_slider's (FA4); a knob cell here is just a rect — the shell composes the two.
|
|
||||||
//
|
//
|
||||||
// THE DECK (CONTEXT.md §S-VIEW r11). A horizontal run of FENCED GROUPS, left -> right, each
|
// The deck is a horizontal run of fenced groups, left->right, each a bordered box with a
|
||||||
// a hairline-bordered bg/panel box with a CAPTION ROW (micro-caps caption left; the group's
|
// caption row (caption left, the group's compact mode toggle right-anchored) over a knob
|
||||||
// compact mode toggle right-anchored IN the caption row — this is where the not-full-width
|
// row of fixed cells (knob centered, label band beneath). A group may also place one
|
||||||
// toggles live) over a KNOB ROW of fixed 48x58 cells (28px knob centered, 12px label band
|
// two-segment toggle in the knob row after its cells. Groups that must keep stable
|
||||||
// beneath). A group may additionally place one 18px-tall two-segment toggle IN the knob row
|
// geometry across a mode flip reserve blank cells (id -1) so a mode flip never reflows
|
||||||
// after its cells (the VOICE group's Retrig|Legato — same Mono/Stereo segment grammar,
|
// neighbouring groups.
|
||||||
// vertically centered). Groups that must keep stable geometry across a mode flip reserve
|
|
||||||
// blank cells (id -1): the AMP ENVELOPE group always spans 5 cells so Gate<->Trigger never
|
|
||||||
// reflows its neighbours.
|
|
||||||
//
|
//
|
||||||
// WRAP (deterministic): groups place left-to-right with kDeckGroupGap between; a group that
|
// Wrap is deterministic: groups place left-to-right with kDeckGroupGap between; a group
|
||||||
// does not fit the remaining width starts a new deck row (whole groups only, never split).
|
// that does not fit the remaining width starts a new row (whole groups only, never
|
||||||
// The first group of a row always places even if wider than the row (degenerate width).
|
// split); the first group of a row always places even if wider than the row.
|
||||||
// deckHeight() exposes the resulting height so the shell can bottom-anchor the deck band and
|
|
||||||
// give the ELASTIC HERO the rest (r11 band order).
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -31,7 +22,7 @@
|
|||||||
|
|
||||||
namespace reasampler::instrument::ui {
|
namespace reasampler::instrument::ui {
|
||||||
|
|
||||||
// Fixed deck metrics (spec r11), exposed so the shell and tests agree.
|
// Fixed deck metrics, exposed so the shell and tests agree.
|
||||||
inline constexpr int kDeckCellW = 48; // one knob cell
|
inline constexpr int kDeckCellW = 48; // one knob cell
|
||||||
inline constexpr int kDeckCellH = 58;
|
inline constexpr int kDeckCellH = 58;
|
||||||
inline constexpr int kDeckKnobSize = 28; // knob diameter inside the cell
|
inline constexpr int kDeckKnobSize = 28; // knob diameter inside the cell
|
||||||
@@ -55,9 +46,8 @@ struct DeckToggleDesc {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// One fenced group, in deck order. `cellIds` are the knob cells left-to-right; an id of -1
|
// One fenced group, in deck order. `cellIds` are the knob cells left-to-right; an id of -1
|
||||||
// is a RESERVED BLANK cell (geometry held, never hit — the AMP ENVELOPE Trigger face).
|
// is a reserved blank cell (geometry held, never hit). `captionWidth` is the px the shell
|
||||||
// `captionWidth` is the px the shell reserves for the caption text (this module does not
|
// reserves for the caption text (this module does not measure text).
|
||||||
// measure text — the house constant-metrics pattern).
|
|
||||||
struct DeckGroupDesc {
|
struct DeckGroupDesc {
|
||||||
int id = 0; // shell group id (opaque here)
|
int id = 0; // shell group id (opaque here)
|
||||||
int captionWidth = 60;
|
int captionWidth = 60;
|
||||||
@@ -96,21 +86,20 @@ struct DeckLayout {
|
|||||||
int height = 0; // rowCount * kDeckGroupH + (rowCount-1) * kDeckRowGap; 0 for no groups
|
int height = 0; // rowCount * kDeckGroupH + (rowCount-1) * kDeckRowGap; 0 for no groups
|
||||||
};
|
};
|
||||||
|
|
||||||
// The width of one group box: the wider of its caption row (caption + gap + toggle) and its
|
// Width of one group box: the wider of its caption row (caption + gap + toggle) and its
|
||||||
// knob row (cells + gap + row toggle), plus the horizontal padding. Pure.
|
// knob row (cells + gap + row toggle), plus horizontal padding.
|
||||||
int deckGroupWidth(const DeckGroupDesc& g);
|
int deckGroupWidth(const DeckGroupDesc& g);
|
||||||
|
|
||||||
// The number of deck rows the groups occupy at `availWidth` under the greedy whole-group
|
// Number of deck rows the groups occupy at `availWidth` under the greedy whole-group wrap.
|
||||||
// wrap (a group that does not fit the remaining row width starts a new row; the first group
|
// 0 for an empty list.
|
||||||
// of a row always places). 0 for an empty group list. Pure — the wrap is deterministic.
|
|
||||||
int deckRowCount(const std::vector<DeckGroupDesc>& groups, int availWidth);
|
int deckRowCount(const std::vector<DeckGroupDesc>& groups, int availWidth);
|
||||||
|
|
||||||
// The total deck height at `availWidth` (rows * kDeckGroupH + inter-row gaps). 0 for an
|
// Total deck height at `availWidth` (rows * kDeckGroupH + inter-row gaps). The shell
|
||||||
// empty list. The shell bottom-anchors a band of exactly this height. Pure.
|
// bottom-anchors a band of exactly this height.
|
||||||
int deckHeight(const std::vector<DeckGroupDesc>& groups, int availWidth);
|
int deckHeight(const std::vector<DeckGroupDesc>& groups, int availWidth);
|
||||||
|
|
||||||
// Lay the groups out from (left, top) within `availWidth`, wrapping per deckRowCount's rule.
|
// Lays the groups out from (left, top) within `availWidth`, wrapping per deckRowCount's
|
||||||
// Every rect is absolute. Pure — same inputs, same layout.
|
// rule. Every rect is absolute.
|
||||||
DeckLayout layoutDeck(const std::vector<DeckGroupDesc>& groups, int left, int top,
|
DeckLayout layoutDeck(const std::vector<DeckGroupDesc>& groups, int left, int top,
|
||||||
int availWidth);
|
int availWidth);
|
||||||
|
|
||||||
@@ -124,10 +113,9 @@ struct DeckHit {
|
|||||||
int segment = -1; // 0/1 for a toggle hit; -1 otherwise
|
int segment = -1; // 0/1 for a toggle hit; -1 otherwise
|
||||||
};
|
};
|
||||||
|
|
||||||
// The deck element a point lands on: a knob CELL (the whole 48x58 cell — friendlier than the
|
// The deck element a point lands on: a knob cell (the whole cell, not just the knob
|
||||||
// bare knob circle; the shell anchors the vertical drag wherever the grab lands), a caption-
|
// circle — the shell anchors the vertical drag wherever the grab lands), a caption-toggle
|
||||||
// toggle segment, or a row-toggle segment. Blank cells (id -1) and everything else miss.
|
// segment, or a row-toggle segment. Blank cells (id -1) and everything else miss.
|
||||||
// Pure — the shell's routing entry point.
|
|
||||||
DeckHit hitTestDeck(const DeckLayout& layout, int x, int y);
|
DeckHit hitTestDeck(const DeckLayout& layout, int x, int y);
|
||||||
|
|
||||||
} // namespace reasampler::instrument::ui
|
} // namespace reasampler::instrument::ui
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// param_slider.cpp — see param_slider.h. PURE control-surface geometry for the S12/S15/S16
|
// param_slider.cpp — see param_slider.h. Pure control-surface geometry; no host types.
|
||||||
// editor parameter panel. No host types; only the shared Rect + contains().
|
|
||||||
|
|
||||||
#include "core/instrument/ui/param_slider.h"
|
#include "core/instrument/ui/param_slider.h"
|
||||||
|
|
||||||
@@ -10,7 +9,7 @@
|
|||||||
|
|
||||||
namespace reasampler::instrument::ui {
|
namespace reasampler::instrument::ui {
|
||||||
|
|
||||||
using util::clamp01; // the ONE unit-interval clamp (Q-W1, T4-24)
|
using util::clamp01;
|
||||||
|
|
||||||
std::vector<ControlRow> layoutControls(const Rect& panel,
|
std::vector<ControlRow> layoutControls(const Rect& panel,
|
||||||
const std::vector<ControlDesc>& controls) {
|
const std::vector<ControlDesc>& controls) {
|
||||||
@@ -83,7 +82,7 @@ double valueAtPoint(const Rect& control, int x) {
|
|||||||
return static_cast<double>(x - track.x) / static_cast<double>(span);
|
return static_cast<double>(x - track.x) / static_cast<double>(span);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Radial knob (Wave A FA4) ---------------------------------------------------------
|
// --- Radial knob -----------------------------------------------------------------------
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
|||||||
@@ -1,180 +1,141 @@
|
|||||||
// param_slider.h — PURE control-surface layout + hit-test + value<->pixel mapping for the
|
// param_slider.h — control-surface layout + hit-test + value<->pixel mapping for the
|
||||||
// S12/S15/S16 editor parameter panel. NO VST3, NO REAPER, NO SWELL/LICE types at the
|
// editor parameter panel. Engine-free by design (no sampler_core/sample_map). Mirror of
|
||||||
// boundary, and — deliberately — NO sampler_core / sample_map engine types either. The
|
// keyboard_strip/waveform_view/mode_switch; the shell draws each row and routes
|
||||||
// mirror of keyboard_strip / waveform_view / mode_switch: the fiddly slider-track and
|
// clicks/drags into these functions, owning the control-id -> engine-param binding and
|
||||||
// toggle-segment arithmetic lives here, unit-tested outside the DAW, while the editor shell
|
// the value domain mapping.
|
||||||
// draws each row (label + track/segments + handle) and routes clicks/drags into these
|
|
||||||
// functions, owning the control-id -> engine-param binding + the value DOMAIN mapping.
|
|
||||||
//
|
//
|
||||||
// WHY IT EXISTS (S12 + the S15/S16 control surfaces deferred here). The setup / Zones surface
|
// Controls are one of three shapes — a two-segment Toggle, a horizontal Slider, or a
|
||||||
// grows a stack of parameter controls: the S15 play-mode toggle (Gate|Trigger), the AHDSR
|
// radial Knob with a needle and vertical-drag mapping — laid out as a vertical stack of
|
||||||
// amp-envelope sliders (attack/hold/decay/sustain/release), the Trigger %-length + fade
|
// fixed-height rows. This module maps a control's normalized value (0..1) to/from its
|
||||||
// controls, the S16 Varispeed|Preserve engine toggle, and the AD pitch-envelope
|
// handle pixel / needle angle; the shell converts each control's engine value (frames,
|
||||||
// enable/attack/decay/depth. They are three shapes — a two-segment TOGGLE, a horizontal
|
// seconds, a fraction, a signed semitone depth) to/from that 0..1.
|
||||||
// SLIDER, and (Wave A FA4) a radial KNOB with a needle indicator and vertical-drag value
|
|
||||||
// mapping — laid out as a vertical stack of fixed-height rows. This module lays out that
|
|
||||||
// stack and maps a control's NORMALIZED value (0..1) to/from its handle pixel / needle
|
|
||||||
// angle; the shell converts each control's engine value (frames, seconds, a fraction, a
|
|
||||||
// signed semitone depth) to/from that 0..1 with its own domain knowledge (this module stays
|
|
||||||
// engine-free so it tests without the audio core).
|
|
||||||
//
|
|
||||||
// It reuses editor_geometry's Rect + contains() (one shared geometry idiom).
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "core/instrument/ui/editor_geometry.h" // Rect, contains — one shared geometry idiom
|
#include "core/instrument/ui/editor_geometry.h" // Rect, contains
|
||||||
|
|
||||||
namespace reasampler::instrument::ui {
|
namespace reasampler::instrument::ui {
|
||||||
|
|
||||||
// Fixed control-panel metrics, exposed so the shell and tests agree.
|
// Fixed control-panel metrics, exposed so the shell and tests agree.
|
||||||
inline constexpr int kControlRowHeight = 22; // one control row (incl. its inter-row gap)
|
inline constexpr int kControlRowHeight = 22;
|
||||||
inline constexpr int kControlRowGap = 4; // vertical gap below each row
|
inline constexpr int kControlRowGap = 4;
|
||||||
inline constexpr int kControlLabelWidth = 92; // the label column at the row's left
|
inline constexpr int kControlLabelWidth = 92;
|
||||||
inline constexpr int kSliderHandleWidth = 8; // the draggable slider handle width (px)
|
inline constexpr int kSliderHandleWidth = 8;
|
||||||
inline constexpr int kToggleSegments = 2; // a toggle is always two segments
|
inline constexpr int kToggleSegments = 2;
|
||||||
|
|
||||||
// A control is one of three shapes. Toggle = a two-segment selector (the active segment
|
// Toggle = two-segment selector (active segment highlights); Slider = horizontal track
|
||||||
// highlights); Slider = a horizontal track with a draggable handle over a 0..1 value;
|
// with a draggable handle over a 0..1 value; Knob = radial dial with a needle, dragged
|
||||||
// Knob = a radial dial with a needle indicator over a 0..1 value, dragged VERTICALLY
|
// vertically (up = increase).
|
||||||
// (up = increase).
|
|
||||||
enum class ControlKind { Toggle, Slider, Knob };
|
enum class ControlKind { Toggle, Slider, Knob };
|
||||||
|
|
||||||
// One control the shell places in the panel, in stack order. `id` is the shell's own control
|
// One control the shell places in the panel, in stack order. `id` is the shell's own
|
||||||
// identifier (an int the shell casts from its ControlId enum) returned by the hit-test so the
|
// control identifier, returned by the hit-test so the shell routes to the right engine
|
||||||
// shell routes the interaction to the right engine param — this module never interprets it.
|
// param — this module never interprets it.
|
||||||
struct ControlDesc {
|
struct ControlDesc {
|
||||||
int id = 0;
|
int id = 0;
|
||||||
ControlKind kind = ControlKind::Slider;
|
ControlKind kind = ControlKind::Slider;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The laid-out geometry of one control row: its full row rect plus the interactive sub-rect
|
// Laid-out geometry of one control row: full row rect plus the interactive sub-rect (the
|
||||||
// (the track for a Slider, the whole control area for a Toggle — the shell splits a Toggle
|
// track for a Slider, the whole control area for a Toggle — the shell splits a Toggle
|
||||||
// into segments via toggleSegmentRect). `index` is the control's position in the stack.
|
// into segments via toggleSegmentRect).
|
||||||
struct ControlRow {
|
struct ControlRow {
|
||||||
int id = 0;
|
int id = 0;
|
||||||
ControlKind kind = ControlKind::Slider;
|
ControlKind kind = ControlKind::Slider;
|
||||||
Rect row; // the full row (label column + control column)
|
Rect row;
|
||||||
Rect label; // the label column at the left
|
Rect label;
|
||||||
Rect control; // the control column to the right of the label (track / toggle area)
|
Rect control;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Lay out `controls` as a vertical stack of fixed-height rows inside `panel`, top-down. Each
|
// Lays out `controls` as a vertical stack of fixed-height rows inside `panel`, top-down.
|
||||||
// row is kControlRowHeight tall with kControlRowGap below it; the label column takes the left
|
// Label column takes the left kControlLabelWidth (clamped to the panel), control column
|
||||||
// kControlLabelWidth (clamped so it never exceeds the panel), the control column the rest. A
|
// the rest. A row past the panel bottom is still returned (the shell clips/suppresses it)
|
||||||
// row whose top falls past the panel bottom is still returned (the shell clips at paint /
|
// so stack geometry is deterministic regardless of panel height.
|
||||||
// suppresses it) so the stack geometry is deterministic regardless of panel height. An empty
|
|
||||||
// control list or a degenerate panel yields an empty vector. Pure.
|
|
||||||
std::vector<ControlRow> layoutControls(const Rect& panel,
|
std::vector<ControlRow> layoutControls(const Rect& panel,
|
||||||
const std::vector<ControlDesc>& controls);
|
const std::vector<ControlDesc>& controls);
|
||||||
|
|
||||||
// The rect of segment `seg` (0..kToggleSegments-1) within a toggle control's `control` rect,
|
// Rect of segment `seg` within a toggle's `control` rect, splitting it into
|
||||||
// splitting it into kToggleSegments equal segments left-to-right (the last absorbs any width
|
// kToggleSegments equal segments left-to-right (last absorbs any width remainder).
|
||||||
// remainder, mirror of mode_switch's segment split). An out-of-range segment or a degenerate
|
|
||||||
// control rect yields an empty rect. Pure.
|
|
||||||
Rect toggleSegmentRect(const Rect& control, int seg);
|
Rect toggleSegmentRect(const Rect& control, int seg);
|
||||||
|
|
||||||
// The toggle segment a point lands on within a toggle control's `control` rect, or -1 for a
|
|
||||||
// miss (outside the control area). Pure.
|
|
||||||
int toggleSegmentHitTest(const Rect& control, int x, int y);
|
int toggleSegmentHitTest(const Rect& control, int x, int y);
|
||||||
|
|
||||||
// The slider track sub-rect inside a slider control's `control` rect: the control inset so the
|
// Slider track sub-rect inside `control`: inset so the handle stays fully within the
|
||||||
// handle (kSliderHandleWidth) stays fully within the control at value 0 and 1 (a half-handle
|
// control at value 0 and 1. The handle center ranges across [track.x, track.right()] as
|
||||||
// margin at each end). The handle CENTER ranges across [track.x, track.right()] as the value
|
// the value ranges [0,1].
|
||||||
// ranges [0,1]. The shell draws the track fill + handle here. A degenerate control yields an
|
|
||||||
// empty rect. Pure.
|
|
||||||
Rect sliderTrackRect(const Rect& control);
|
Rect sliderTrackRect(const Rect& control);
|
||||||
|
|
||||||
// The handle rect for a slider at normalized `value` (clamped to [0,1]) within `control`: a
|
// Handle rect for a slider at normalized `value` (clamped to [0,1]).
|
||||||
// kSliderHandleWidth-wide bar centered at the value's position along sliderTrackRect. A
|
|
||||||
// degenerate control yields an empty rect. Pure — the inverse of valueAtPoint.
|
|
||||||
Rect sliderHandleRect(const Rect& control, double value);
|
Rect sliderHandleRect(const Rect& control, double value);
|
||||||
|
|
||||||
// Map a point x to a normalized slider value [0,1] within `control` (the handle-center range).
|
// Maps a point x to a normalized slider value [0,1]: at/left of track start -> 0, at/right
|
||||||
// x at/left of the track start -> 0; at/right of the end -> 1; linear between. A degenerate
|
// of end -> 1, linear between. Inverse of sliderHandleRect's position map.
|
||||||
// track (zero movable span) -> 0. Pure — the inverse of sliderHandleRect's position map; the
|
|
||||||
// shell converts the returned 0..1 into its engine domain (frames/seconds/fraction/semitones).
|
|
||||||
double valueAtPoint(const Rect& control, int x);
|
double valueAtPoint(const Rect& control, int x);
|
||||||
|
|
||||||
// --- Radial knob (Wave A FA4) --------------------------------------------------------------
|
// --- Radial knob ---------------------------------------------------------------------
|
||||||
//
|
//
|
||||||
// Angle convention: DEGREES CLOCKWISE FROM 12 O'CLOCK, matching a clock face in screen
|
// Angle convention: degrees clockwise from 12 o'clock (screen coords, y grows downward).
|
||||||
// coordinates (y grows downward): 0 = 12 o'clock (up), 90 = 3 o'clock (right), 180 = 6
|
// The value arc sweeps clockwise from startDeg (value 0) to endDeg (value 1); an endDeg
|
||||||
// o'clock (down), 270 = 9 o'clock (left). The value arc sweeps CLOCKWISE from startDeg
|
// at-or-behind startDeg wraps +360.
|
||||||
// (value 0) to endDeg (value 1); an endDeg at-or-behind startDeg wraps +360, so equal
|
|
||||||
// angles mean a full 360° sweep.
|
|
||||||
//
|
//
|
||||||
// The DEFAULT arc is the conventional 7→5 o'clock layout: min at 7 o'clock (210°) sweeping
|
// Default arc: 7 o'clock (210°) sweeping clockwise 300° to 5 o'clock (150°), leaving a
|
||||||
// clockwise 300° around to max at 5 o'clock (150°), leaving a symmetric 60° dead arc at the
|
// symmetric 60° dead arc at the bottom; the 50% value lands at 12 o'clock. Angles are
|
||||||
// bottom. The 50% (midpoint) value lands at 12 o'clock (0°/360°) — straight up. The angles
|
// parameters, not hardcoded.
|
||||||
// are PARAMETERS, not hardcoded — the shell sets the final sweep when the parallel layout
|
inline constexpr double kKnobArcStartDeg = 210.0;
|
||||||
// spec lands.
|
inline constexpr double kKnobArcEndDeg = 150.0;
|
||||||
inline constexpr double kKnobArcStartDeg = 210.0; // value 0 — 7 o'clock
|
|
||||||
inline constexpr double kKnobArcEndDeg = 150.0; // value 1 — 5 o'clock (clockwise wrap)
|
|
||||||
|
|
||||||
// Default vertical-drag sensitivity: pixels of upward drag for one full 0->1 sweep.
|
// Pixels of upward drag for one full 0->1 sweep.
|
||||||
inline constexpr int kKnobDragRangePixels = 128;
|
inline constexpr int kKnobDragRangePixels = 128;
|
||||||
|
|
||||||
// The configurable value arc of a knob. Defaults to the 7->5 o'clock reading above.
|
|
||||||
struct KnobArc {
|
struct KnobArc {
|
||||||
double startDeg = kKnobArcStartDeg;
|
double startDeg = kKnobArcStartDeg;
|
||||||
double endDeg = kKnobArcEndDeg;
|
double endDeg = kKnobArcEndDeg;
|
||||||
};
|
};
|
||||||
|
|
||||||
// A knob's circle within its control cell: center + radius in pixel space (doubles so the
|
// A knob's circle within its control cell: center + radius (doubles so the shell rounds
|
||||||
// shell rounds once, at draw time). radius == 0 marks a degenerate cell.
|
// once, at draw time). radius == 0 marks a degenerate cell.
|
||||||
struct KnobGeometry {
|
struct KnobGeometry {
|
||||||
double centerX = 0.0;
|
double centerX = 0.0;
|
||||||
double centerY = 0.0;
|
double centerY = 0.0;
|
||||||
double radius = 0.0;
|
double radius = 0.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// A pixel-space point (the needle endpoint the shell draws to).
|
|
||||||
struct KnobPoint {
|
struct KnobPoint {
|
||||||
double x = 0.0;
|
double x = 0.0;
|
||||||
double y = 0.0;
|
double y = 0.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The knob circle inscribed in `cell`, centered, radius = half the smaller dimension. A
|
// Knob circle inscribed in `cell`, centered, radius = half the smaller dimension. The
|
||||||
// degenerate cell yields radius 0. CONTRACT: the shell MUST pass `row.control` (the full
|
// shell must pass `row.control` both when drawing and hit-testing — controlAtPoint always
|
||||||
// control column) both when drawing and when hit-testing — `controlAtPoint` always uses
|
// uses `r.control` as the cell, so draw cell and hit cell must agree.
|
||||||
// `r.control` as the cell, so the draw cell and hit cell must be the same. If the shell
|
|
||||||
// wants to draw a smaller circle it must center it within `row.control` and accept that the
|
|
||||||
// hit area is the larger column-inscribed circle. Pure.
|
|
||||||
KnobGeometry computeKnob(const Rect& cell);
|
KnobGeometry computeKnob(const Rect& cell);
|
||||||
|
|
||||||
// True if (x, y) falls strictly inside the knob circle (boundary exclusive, matching the
|
// True if (x, y) falls strictly inside the knob circle (boundary exclusive).
|
||||||
// module's half-open Rect convention). A degenerate knob (radius <= 0) hits nothing. Pure.
|
|
||||||
bool knobHitTest(const KnobGeometry& knob, int x, int y);
|
bool knobHitTest(const KnobGeometry& knob, int x, int y);
|
||||||
|
|
||||||
// The clockwise sweep of `arc` in degrees, in (0, 360]: normalized end - start, wrapping
|
// Clockwise sweep of `arc` in degrees, in (0, 360]: normalized end - start, wrapping +360
|
||||||
// +360 when the end is at-or-behind the start (default arc -> 300). Pure.
|
// when the end is at-or-behind the start (default arc -> 300).
|
||||||
double knobSweepDeg(const KnobArc& arc);
|
double knobSweepDeg(const KnobArc& arc);
|
||||||
|
|
||||||
// The needle angle for normalized `value` (clamped to [0,1]): startDeg at 0, endDeg at 1,
|
// Needle angle for normalized `value` (clamped to [0,1]): startDeg at 0, endDeg at 1,
|
||||||
// linear between, returned normalized to [0, 360). Pure.
|
// linear between, normalized to [0, 360).
|
||||||
double knobValueAngleDeg(const KnobArc& arc, double value);
|
double knobValueAngleDeg(const KnobArc& arc, double value);
|
||||||
|
|
||||||
// The needle endpoint for normalized `value`: the point on the knob circle at the value's
|
// Needle endpoint for normalized `value`: the point on the knob circle at the value's
|
||||||
// angle, from the center. The shell draws the needle from (centerX, centerY) to this point
|
// angle, from the center.
|
||||||
// (or lerps toward the center for a shorter needle). Pure.
|
|
||||||
KnobPoint knobNeedlePoint(const KnobGeometry& knob, const KnobArc& arc, double value);
|
KnobPoint knobNeedlePoint(const KnobGeometry& knob, const KnobArc& arc, double value);
|
||||||
|
|
||||||
// Map a vertical drag onto a knob value: `startValue` is the value at drag start (clamped),
|
// Maps a vertical drag onto a knob value: `startValue` is the value at drag start,
|
||||||
// `dyPixels` the pointer's y displacement in screen coordinates (down = positive). Dragging
|
// `dyPixels` the pointer's y displacement (down = positive). Up increases, down
|
||||||
// UP increases, DOWN decreases; `dragRangePixels` pixels of travel covers the full 0..1
|
// decreases; `dragRangePixels` pixels of travel covers the full 0..1 range.
|
||||||
// range. Result clamps to [0,1]; a non-positive drag range yields the clamped start value.
|
|
||||||
// Pure — the inverse map for the knob's drag interaction.
|
|
||||||
double knobDragValue(double startValue, int dyPixels,
|
double knobDragValue(double startValue, int dyPixels,
|
||||||
int dragRangePixels = kKnobDragRangePixels);
|
int dragRangePixels = kKnobDragRangePixels);
|
||||||
|
|
||||||
// The control a point lands on, given the laid-out `rows`. Returns the control id (ControlDesc
|
// Control a point lands on, given laid-out `rows`. Returns the control id whose
|
||||||
// id) whose interactive area (a Slider's track, a Toggle's whole control area, a Knob's
|
// interactive area contains the point, or -1 for a miss. First matching row wins (rows
|
||||||
// circle) contains the point, or -1 for a miss (a gap, the label column, or outside every
|
// never overlap).
|
||||||
// row). The FIRST matching row wins (rows never overlap, so at most one matches). Pure — the
|
|
||||||
// shell's routing entry point: on a hit it reads the value (valueAtPoint /
|
|
||||||
// toggleSegmentHitTest / knobDragValue over the ensuing drag) and commits.
|
|
||||||
int controlAtPoint(const std::vector<ControlRow>& rows, int x, int y);
|
int controlAtPoint(const std::vector<ControlRow>& rows, int x, int y);
|
||||||
|
|
||||||
} // namespace reasampler::instrument::ui
|
} // namespace reasampler::instrument::ui
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ int frameToX(const Rect& area, std::int64_t frameCount, std::int64_t frame) {
|
|||||||
const int w = std::max(0, area.width);
|
const int w = std::max(0, area.width);
|
||||||
if (frameCount <= 0 || w <= 0) return area.x;
|
if (frameCount <= 0 || w <= 0) return area.x;
|
||||||
const std::int64_t f = clampFrame(frame, frameCount);
|
const std::int64_t f = clampFrame(frame, frameCount);
|
||||||
// Linear map: x = left + round(f * w / frameCount). Rounding keeps the marker line
|
// x = left + round(f * w / frameCount); multiply before divide to keep this exact.
|
||||||
// visually centered on its frame; the divide is exact rational (multiply first).
|
|
||||||
const std::int64_t num = f * static_cast<std::int64_t>(w) + frameCount / 2;
|
const std::int64_t num = f * static_cast<std::int64_t>(w) + frameCount / 2;
|
||||||
return area.x + static_cast<int>(num / frameCount);
|
return area.x + static_cast<int>(num / frameCount);
|
||||||
}
|
}
|
||||||
@@ -33,8 +32,7 @@ std::int64_t xToFrame(const Rect& area, std::int64_t frameCount, int x) {
|
|||||||
if (x <= area.x) return 0;
|
if (x <= area.x) return 0;
|
||||||
if (x >= area.right()) return frameCount;
|
if (x >= area.right()) return frameCount;
|
||||||
const std::int64_t dx = static_cast<std::int64_t>(x - area.x);
|
const std::int64_t dx = static_cast<std::int64_t>(x - area.x);
|
||||||
// Inverse of frameToX: frame = round(dx * frameCount / w). Round so click and marker draw
|
// Inverse of frameToX: frame = round(dx * frameCount / w).
|
||||||
// agree at bin granularity.
|
|
||||||
const std::int64_t num = dx * frameCount + static_cast<std::int64_t>(w) / 2;
|
const std::int64_t num = dx * frameCount + static_cast<std::int64_t>(w) / 2;
|
||||||
return clampFrame(num / static_cast<std::int64_t>(w), frameCount);
|
return clampFrame(num / static_cast<std::int64_t>(w), frameCount);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,84 +1,52 @@
|
|||||||
// waveform_view.h — PURE waveform/marker geometry + zero-crossing snap for the S11
|
// waveform_view.h — waveform/marker geometry + zero-crossing snap. Mirror of keyboard_strip/
|
||||||
// waveform surface. NO VST3, NO REAPER, NO SWELL/LICE types at the boundary. The mirror
|
// editor_geometry: frame<->pixel + marker hit-test + snap arithmetic lives here, unit-tested
|
||||||
// of keyboard_strip / editor_geometry: the fiddly frame<->pixel + marker hit-test + snap
|
// outside the DAW; the shell draws and marshals mouse events into it.
|
||||||
// arithmetic lives here, unit-tested outside the DAW, while the editor shell
|
|
||||||
// (reasampler_editor.cpp) draws the envelope + markers and marshals mouse events into it.
|
|
||||||
//
|
//
|
||||||
// The surface maps a sample's full frame span [0, frameCount] linearly across a horizontal
|
// The surface maps a sample's full frame span [0, frameCount] linearly across a horizontal
|
||||||
// waveform rect. Draggable MARKERS mark frames of interest (S11: start point, loop start,
|
// waveform rect. Markers are a generic N-named-marker set (not hardcoded specials), so a
|
||||||
// loop end). The marker set is GENERIC — N named markers with drag + snap — deliberately
|
// different mode (e.g. start + %-length end + fades) can repurpose the same machinery.
|
||||||
// not three hardcoded specials, so S15 (Trigger/Gate) can repurpose this same surface with a
|
|
||||||
// different marker set (start + %-length end + fades) without reworking the machinery.
|
|
||||||
//
|
|
||||||
// Interaction resolves through the pure DRAG-DELTA resolver here: the shell captures a grab
|
|
||||||
// on WM_LBUTTONDOWN (markerAtPoint identifies the grabbed marker), feeds each WM_MOUSEMOVE's
|
|
||||||
// pixel delta back through resolveDragFrame (which clamps + optionally zero-crossing-snaps),
|
|
||||||
// and commits on WM_LBUTTONUP. Live feedback is the shell re-drawing the in-flight frame.
|
|
||||||
//
|
|
||||||
// It reuses the same Rect + contains() as editor_geometry (one shared geometry idiom), so
|
|
||||||
// this header depends on editor_geometry.h rather than redefining a rectangle type. Audio
|
|
||||||
// is the peaks AudioSample float alias (the one house precedent — sampler_core / wav_codec do
|
|
||||||
// the same), so the zero-crossing helper takes the same mono PCM the shell already decoded.
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
#include "core/instrument/ui/editor_geometry.h" // Rect, contains — one shared geometry idiom
|
#include "core/instrument/ui/editor_geometry.h" // Rect, contains
|
||||||
#include "core/audio/peaks.h" // AudioSample (float), the mono PCM the snap scans
|
#include "core/audio/peaks.h" // AudioSample (float)
|
||||||
|
|
||||||
namespace reasampler::instrument::ui {
|
namespace reasampler::instrument::ui {
|
||||||
|
|
||||||
using audio::AudioSample;
|
using audio::AudioSample;
|
||||||
|
|
||||||
// The width (px) of a marker's grab region either side of its x line: a grab within this many
|
// Pixel width of a marker's grab region either side of its x line. Mirrors keyboard_strip's
|
||||||
// pixels of a marker's drawn x is a grab OF that marker. Mirrors keyboard_strip's edge-grab
|
// edge-grab idiom.
|
||||||
// idiom — wide enough to grab a 1px line comfortably, narrow enough that adjacent markers stay
|
|
||||||
// distinguishable.
|
|
||||||
inline constexpr int kMarkerGrabWidth = 5;
|
inline constexpr int kMarkerGrabWidth = 5;
|
||||||
|
|
||||||
// The x pixel (inside `area`) of frame `frame` under the linear map: frame 0 -> area.x,
|
// x pixel of `frame` under the linear map: frame 0 -> area.x, frame frameCount -> area.right().
|
||||||
// frame frameCount -> area.right(). A frame is clamped to [0, frameCount] before mapping, so an
|
// Frame is clamped to [0, frameCount] before mapping. frameCount <= 0 or a zero-width area pins
|
||||||
// out-of-range frame pins to an edge rather than escaping the rect. frameCount <= 0 or a
|
// every frame to area.x.
|
||||||
// zero-width area pins every frame to area.x (a degenerate, non-inverting result). Pure.
|
|
||||||
int frameToX(const Rect& area, std::int64_t frameCount, std::int64_t frame);
|
int frameToX(const Rect& area, std::int64_t frameCount, std::int64_t frame);
|
||||||
|
|
||||||
// The frame a point x (inside `area`) maps to under the inverse linear map, clamped to
|
// Inverse of frameToX: the frame a point x maps to, clamped to [0, frameCount]. A point left of
|
||||||
// [0, frameCount]. A point left of area.x yields 0; right of area.right() yields frameCount.
|
// area.x yields 0; right of area.right() yields frameCount.
|
||||||
// frameCount <= 0 or a zero-width area yields 0. Pure — the inverse of frameToX (round-trips
|
|
||||||
// to the same frame at bin granularity).
|
|
||||||
std::int64_t xToFrame(const Rect& area, std::int64_t frameCount, int x);
|
std::int64_t xToFrame(const Rect& area, std::int64_t frameCount, int x);
|
||||||
|
|
||||||
// Which marker (index into a caller-supplied parallel `frames` array, in draw order) a grab at
|
// Which marker (index into the caller's parallel `frames` array, in draw order) a grab at
|
||||||
// (x, y) lands on, or -1 for a point off every marker (or off the waveform area). A marker is
|
// (x, y) lands on, or -1 for a miss. A marker is grabbed when x is within kMarkerGrabWidth of
|
||||||
// grabbed when x is within kMarkerGrabWidth of its drawn x AND y is inside `area`. First marker
|
// its drawn x and y is inside `area`. First marker in draw order wins an overlapping tie.
|
||||||
// in order wins a tie where two markers overlap within the grab band (deterministic, mirroring
|
|
||||||
// keyboard_strip's first-match). `frames` is `count` frame indices; a null/empty array or
|
|
||||||
// count <= 0 yields -1. Pure — a raw pointer at the boundary (no host container), like
|
|
||||||
// keyboard_strip::zoneBarAtPoint.
|
|
||||||
int markerAtPoint(const Rect& area, std::int64_t frameCount, const std::int64_t* frames,
|
int markerAtPoint(const Rect& area, std::int64_t frameCount, const std::int64_t* frames,
|
||||||
int count, int x, int y);
|
int count, int x, int y);
|
||||||
|
|
||||||
// Resolve a drag to a new frame. Given the frame the grabbed marker held at grab time
|
// Resolves a drag to a new frame: `startFrame` shifted by round(dxPixels * frameCount /
|
||||||
// (`startFrame`) and the horizontal pixel delta since grab (`dxPixels`), returns the frame the
|
// areaWidth), clamped to [0, frameCount]. The shell applies between-marker clamps (e.g.
|
||||||
// marker should now hold: startFrame shifted by round(dxPixels * frameCount / areaWidth),
|
// start <= loopEnd) after this per-marker resolve.
|
||||||
// clamped to [0, frameCount]. A zero-width area or non-positive frameCount pins the result to
|
|
||||||
// the clamped startFrame (no motion). This is the single arithmetic behind every marker drag;
|
|
||||||
// the shell applies clamps BETWEEN markers (start <= loopEnd, loopStart <= loopEnd) after this
|
|
||||||
// per-marker resolve. Pure — rounding is to the nearest frame. Returns the clamped startFrame
|
|
||||||
// for dxPixels == 0.
|
|
||||||
std::int64_t resolveDragFrame(const Rect& area, std::int64_t frameCount, std::int64_t startFrame,
|
std::int64_t resolveDragFrame(const Rect& area, std::int64_t frameCount, std::int64_t startFrame,
|
||||||
int dxPixels);
|
int dxPixels);
|
||||||
|
|
||||||
// The nearest zero-crossing frame to `target` in the mono PCM, for the loop/start snap (the
|
// Nearest zero-crossing frame to `target` in the mono PCM, for loop/start snap. A crossing is a
|
||||||
// S2 zero-crossing-aware requirement). A zero crossing is a frame index i (1 <= i < frames)
|
// frame i (1 <= i < frames) where pcm[i-1] and pcm[i] differ in sign (pcm[i] == 0 snaps to i).
|
||||||
// where the sign of pcm[i-1] and pcm[i] differ (a sample exactly 0 counts as its own crossing
|
// Search fans out symmetrically from the clamped target; an equidistant tie resolves to the
|
||||||
// — pcm[i] == 0 snaps to i). The search fans out symmetrically from the clamped target and
|
// lower frame. No sign change anywhere (or fewer than 2 frames) returns the clamped target
|
||||||
// returns the closest crossing frame; ties (equidistant crossings on both sides) resolve to
|
// unchanged.
|
||||||
// the LOWER frame (deterministic). When the PCM has NO sign change anywhere (all one sign, or
|
|
||||||
// fewer than 2 frames), returns the clamped target unchanged (nothing to snap to — the caller
|
|
||||||
// keeps the raw frame). `target` is clamped to [0, frames) before searching. Pure — scans the
|
|
||||||
// decoded PCM the shell already holds; no host types, no file I/O.
|
|
||||||
std::int64_t nearestZeroCrossing(const AudioSample* pcm, std::int64_t frames,
|
std::int64_t nearestZeroCrossing(const AudioSample* pcm, std::int64_t frames,
|
||||||
std::int64_t target);
|
std::int64_t target);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user