132 lines
7.7 KiB
C++
132 lines
7.7 KiB
C++
// keyboard_strip.h — PURE layout + hit-test + drag math for the S10 capture-first
|
|
// editor's keyboard strip. NO VST3, NO REAPER, NO SWELL/LICE types at the boundary.
|
|
// The mirror of editor_geometry / embed_strip / mode_switch: the fiddly rectangle +
|
|
// 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
|
|
// idiom embed_strip uses). It serves TWO faces of the S10 editor:
|
|
// * the SINGLE-CAPTURE fast path (default): one loaded capture with a ROOT MARKER on
|
|
// the strip, click-a-key (or drag the marker) sets the capture's root note; and
|
|
// * 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
|
|
|
|
#include "editor_geometry.h" // Rect, contains — one shared geometry idiom
|
|
|
|
namespace reasampler::vst {
|
|
|
|
// The full MIDI key span the strip maps across its width: 128 keys (0..127). Named
|
|
// distinctly from embed_strip's kEmbedKeyCount (same value) so the two strips stay
|
|
// independent — the editor strip may grow octave labels/metrics the embed strip never does.
|
|
inline constexpr int kStripKeyCount = 128;
|
|
|
|
// The width (px) of an edge-grab hit region at each end of a zone bar: a drag started
|
|
// within this many pixels of the bar's left/right edge resizes that edge; a drag started
|
|
// 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;
|
|
|
|
// The strip's regions, derived from the (w x h) band the shell allots it. The keys band
|
|
// takes the whole area today (a future octave-label lane can carve a sub-band here without
|
|
// changing callers). Clamped so a degenerate (tiny/zero) size never yields an inverted rect.
|
|
struct StripLayout {
|
|
Rect keys; // the key band: the 128-key span maps linearly across keys.width()
|
|
};
|
|
|
|
// Divide a (w x h) strip area into its regions. Pure: same inputs -> same layout. A zero or
|
|
// negative size yields empty rects (no inversion).
|
|
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
|
|
// span maps linearly across keys.width(); key N occupies the half-open pixel range
|
|
// [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);
|
|
|
|
// The half-open pixel rect of a single key `note` (0..127): [keyLeftX(note),
|
|
// 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);
|
|
|
|
// The rect of the ROOT MARKER for the single-capture fast path: the key cell of `rootNote`,
|
|
// drawn as a highlighted key. Equivalent to keyRect(layout, rootNote) — a named entry point
|
|
// 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);
|
|
|
|
// The MIDI note a point (x, y) lands on, or -1 for a point outside the keys band. Backs
|
|
// click-to-set-root (single capture) and click-a-key-sets-zone-root (zones). Pure.
|
|
int keyAtPoint(const StripLayout& layout, int x, int y);
|
|
|
|
// The horizontal sub-rect of the keys band for a zone spanning [lowNote, highNote]
|
|
// (inclusive): [keyLeftX(low), keyLeftX(high+1)) horizontally, the full band height. Notes
|
|
// 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);
|
|
|
|
// Which part of a zone bar a grab landed on. The shell uses this to decide what a drag
|
|
// edits: an edge resizes that boundary; the body moves the whole span; none means the grab
|
|
// missed the bar entirely (the shell may treat that as a key-click to set the root, or as a
|
|
// deselect).
|
|
enum class ZoneGrab {
|
|
kNone, // the point is not on this zone's bar
|
|
kLowEdge, // within kStripEdgeGrabWidth of the bar's LEFT edge -> resize low
|
|
kHighEdge, // within kStripEdgeGrabWidth of the bar's RIGHT edge -> resize high
|
|
kBody, // on the bar but not an edge -> move the whole span
|
|
};
|
|
|
|
// Classify a grab at (x, y) against ONE zone's bar (low..high). Returns kNone when the
|
|
// point is off the bar (or off the keys band). On the bar: kLowEdge/kHighEdge when within
|
|
// kStripEdgeGrabWidth of that edge, else kBody. A narrow bar (< 2*kStripEdgeGrabWidth)
|
|
// 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);
|
|
|
|
// The zone (index into `lows`/`highs`, draw order) whose bar a grab at (x, y) lands on,
|
|
// plus which part of it, or {-1, kNone} for a point off every bar. First covering zone in
|
|
// draw order wins (first-match, mirroring the core's Keymap::resolve + embed_strip). The
|
|
// 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 {
|
|
int zoneIndex = -1;
|
|
ZoneGrab grab = ZoneGrab::kNone;
|
|
};
|
|
ZoneBarHit zoneBarAtPoint(const StripLayout& layout, const int* lows, const int* highs,
|
|
int count, int x, int y);
|
|
|
|
// Resolve a drag to a new MIDI note. Given the note the grabbed field held at grab time
|
|
// (`startNote`) and the horizontal pixel delta since grab (`dxPixels`), returns the note
|
|
// the field should now hold: startNote shifted by round(dxPixels / keyWidth), clamped to
|
|
// [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);
|
|
|
|
// Returns true when `note` (0..127) is a NATURAL (white) key in standard 12-tone equal
|
|
// temperament; false when it is an ACCIDENTAL (black) key. Notes out of the [0,127]
|
|
// 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);
|
|
|
|
} // namespace reasampler::vst
|