127 lines
5.1 KiB
C++
127 lines
5.1 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
|
|
// Proportional shift: same linear mapping as keyAtPoint/keyEdgeToX so click and drag
|
|
// agree across the full strip, even on non-divisible-by-128 widths. The proportional
|
|
// 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;
|
|
int shift;
|
|
if (dxPixels > 0) {
|
|
shift = (dxPixels * kStripKeyCount + half) / bandWidth;
|
|
} else {
|
|
shift = -(((-dxPixels) * kStripKeyCount + half) / bandWidth);
|
|
}
|
|
return clampNote(startNote + shift);
|
|
}
|
|
|
|
} // namespace reasampler::vst
|