7151e19432
Reverse S4 auto-select (empty=silence+empty state), add peak-thumbnail capture browser + bank filter + keyboard-strip drag machine, v3 component state (selection + zones), and the S-NAME-1 filename/display rename (UID locked). MSVC min/max macro collisions fixed post-implementation; 26/26 tests green.
119 lines
4.7 KiB
C++
119 lines
4.7 KiB
C++
// keyboard_strip.cpp — see keyboard_strip.h. Pure math; no host types.
|
|
|
|
#include "keyboard_strip.h"
|
|
|
|
#include <algorithm>
|
|
|
|
namespace reasampler::vst {
|
|
|
|
namespace {
|
|
|
|
int clampNote(int n) {
|
|
if (n < 0) return 0;
|
|
if (n > kStripKeyCount - 1) return kStripKeyCount - 1;
|
|
return n;
|
|
}
|
|
|
|
// Map a key BOUNDARY in [0, kStripKeyCount] to an x pixel inside a band of the given
|
|
// left/width. keyEdge is a boundary (0..128): 0 -> band left, 128 -> band right. Integer
|
|
// 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) {
|
|
if (keyEdge <= 0) return bandLeft;
|
|
if (keyEdge >= kStripKeyCount) return bandLeft + bandWidth;
|
|
return bandLeft + (keyEdge * bandWidth) / kStripKeyCount;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
StripLayout layoutStrip(int w, int h) {
|
|
const int cw = std::max(0, w);
|
|
const int ch = std::max(0, h);
|
|
StripLayout out;
|
|
out.keys = Rect{0, 0, cw, ch};
|
|
return out;
|
|
}
|
|
|
|
int keyLeftX(const StripLayout& layout, int note) {
|
|
const Rect& band = layout.keys;
|
|
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
|
|
// get a key's right edge, and 128 maps to the band right.
|
|
const int edge = note < 0 ? 0 : (note > kStripKeyCount ? kStripKeyCount : note);
|
|
return keyEdgeToX(band.left, bandWidth, edge);
|
|
}
|
|
|
|
Rect keyRect(const StripLayout& layout, int note) {
|
|
const int n = clampNote(note);
|
|
const int leftX = keyLeftX(layout, n);
|
|
const int rightX = keyLeftX(layout, n + 1);
|
|
return Rect{leftX, layout.keys.top, std::max(leftX, rightX), layout.keys.bottom};
|
|
}
|
|
|
|
Rect rootMarkerRect(const StripLayout& layout, int rootNote) {
|
|
return keyRect(layout, rootNote);
|
|
}
|
|
|
|
int keyAtPoint(const StripLayout& layout, int x, int y) {
|
|
const Rect& band = layout.keys;
|
|
if (!contains(band, x, y)) return -1;
|
|
const int bandWidth = std::max(0, band.width());
|
|
if (bandWidth <= 0) return -1;
|
|
// Invert keyEdgeToX: the key whose half-open [leftX, rightX) contains x. Floor-divide
|
|
// the pixel offset back to a key; clamp defensively (a point on band.right-1 maps to 127).
|
|
const int offset = x - band.left;
|
|
int note = (offset * kStripKeyCount) / bandWidth;
|
|
return clampNote(note);
|
|
}
|
|
|
|
Rect zoneBarRect(const StripLayout& layout, int lowNote, int highNote) {
|
|
int lo = clampNote(lowNote);
|
|
int hi = clampNote(highNote);
|
|
if (lo > hi) lo = hi; // defensive: a malformed zone collapses rather than inverts
|
|
const int leftX = keyLeftX(layout, lo);
|
|
const int rightX = keyLeftX(layout, hi + 1);
|
|
return Rect{leftX, layout.keys.top, std::max(leftX, rightX), layout.keys.bottom};
|
|
}
|
|
|
|
ZoneGrab zoneGrabAt(const StripLayout& layout, int lowNote, int highNote, int x, int y) {
|
|
const Rect bar = zoneBarRect(layout, lowNote, highNote);
|
|
if (!contains(bar, x, y)) return ZoneGrab::kNone;
|
|
|
|
const int barW = bar.width();
|
|
// A narrow bar (< 2*edge) has no body: split at the midpoint, LOW edge wins the tie so
|
|
// a click exactly on the midpoint resizes low (deterministic).
|
|
if (barW < 2 * kStripEdgeGrabWidth) {
|
|
const int mid = bar.left + barW / 2;
|
|
return x <= mid ? ZoneGrab::kLowEdge : ZoneGrab::kHighEdge;
|
|
}
|
|
if (x < bar.left + kStripEdgeGrabWidth) return ZoneGrab::kLowEdge;
|
|
if (x >= bar.right - kStripEdgeGrabWidth) return ZoneGrab::kHighEdge;
|
|
return ZoneGrab::kBody;
|
|
}
|
|
|
|
ZoneBarHit zoneBarAtPoint(const StripLayout& layout, const int* lows, const int* highs,
|
|
int count, int x, int y) {
|
|
if (count <= 0 || lows == nullptr || highs == nullptr) return ZoneBarHit{};
|
|
if (!contains(layout.keys, x, y)) return ZoneBarHit{};
|
|
for (int i = 0; i < count; ++i) {
|
|
const ZoneGrab g = zoneGrabAt(layout, lows[i], highs[i], x, y);
|
|
if (g != ZoneGrab::kNone) return ZoneBarHit{i, g};
|
|
}
|
|
return ZoneBarHit{}; // on the band but on no bar
|
|
}
|
|
|
|
int resolveDragNote(const StripLayout& layout, int startNote, int dxPixels) {
|
|
if (dxPixels == 0) return clampNote(startNote);
|
|
const int bandWidth = std::max(0, layout.keys.width());
|
|
if (bandWidth <= 0) return clampNote(startNote); // zero-width -> no motion
|
|
// Key width in pixels (>= 1 via the max). Round the delta to the nearest key so a
|
|
// half-key drag flips at the key centre: add/subtract half a key before the divide.
|
|
const int keyW = std::max(1, bandWidth / kStripKeyCount);
|
|
const int half = keyW / 2;
|
|
const int shift = dxPixels >= 0 ? (dxPixels + half) / keyW
|
|
: -((-dxPixels + half) / keyW);
|
|
return clampNote(startNote + shift);
|
|
}
|
|
|
|
} // namespace reasampler::vst
|