fix: close review findings on the velocity-curve deck and bipolar curves

Cancels the curve-node drag whenever the popup closes so Esc mid-drag can't alias the amp curve; generalizes CurveTarget routing to one switch; fixes stale/overstated comments; clamps a pre-v12 depth fold; adds deck-inertness and filter-fold test coverage.
This commit is contained in:
2026-07-31 19:46:37 -04:00
parent 9d38f87a2d
commit cfb53aade3
22 changed files with 223 additions and 99 deletions
+30 -6
View File
@@ -53,7 +53,9 @@ namespace {
// The ceiling is READ from the overlay's schematic scale rather than restated: the AHDSR
// schematic anchors a maxed knob at the canvas edge, which only holds while the two agree.
constexpr double kEnvTimeMaxSeconds = instrument::ui::kGateStageMaxSeconds;
constexpr double kPitchDepthMaxSemis = 24.0; // pitch depth throw: +/-24 st, centered
// Pitch depth throw: +/-kVelocityPitchRangeSemitones, centered. The one throw the pitch
// envelope's peak and the velocity->pitch curve's full scale both speak (play_params.h).
constexpr double kPitchDepthMaxSemis = kVelocityPitchRangeSemitones;
constexpr double kKeyTrackMax = 2.0; // key-track slider ceiling (0..200%)
// The raw stored curve exponent for a curve-dial control id, read DIRECTLY off the field —
@@ -510,21 +512,43 @@ void ReaSamplerEditor::unpackEnvelope(OverlayEnv which, const StageEnvelope& env
}
}
const VelocityCurve& ReaSamplerEditor::editedCurve() const {
switch (curvePopup_) {
const VelocityCurve& ReaSamplerEditor::curveFor(CurveTarget target) const {
switch (target) {
case CurveTarget::kPitch: return params_.play.pitchVelocityCurve;
case CurveTarget::kFilter: return params_.play.filter.velocityCurve;
case CurveTarget::kAmp:
case CurveTarget::kNone:
// kNone only reaches here from a paint/hover racing the close; the amp curve is a
// valid, harmless read rather than a branch every caller would have to repeat.
// kNone reads as amp — a valid, harmless choice for a target-agnostic caller (a
// disabled deck cell still needs SOME curve to draw). editedCurve()'s mutable
// overload below refuses kNone rather than relying on this fallback to protect amp.
return params_.velocityCurve;
}
return params_.velocityCurve;
}
VelocityCurve& ReaSamplerEditor::curveFor(CurveTarget target) {
return const_cast<VelocityCurve&>(std::as_const(*this).curveFor(target));
}
const VelocityCurve& ReaSamplerEditor::editedCurve() const { return curveFor(curvePopup_); }
VelocityCurve& ReaSamplerEditor::editedCurve() {
return const_cast<VelocityCurve&>(std::as_const(*this).editedCurve());
// Refuses kNone rather than aliasing amp (see curvePopup_'s declaration for why); should be
// unreachable now that every writer of kNone also cancels the drag, but this is the second,
// independent line of defense.
if (curvePopup_ == CurveTarget::kNone) {
static VelocityCurve sink = VelocityCurve::flat();
return sink;
}
return curveFor(curvePopup_);
}
void ReaSamplerEditor::closeCurvePopup() {
curvePopup_ = CurveTarget::kNone;
if (drag_ == DragKind::kCurveNode) {
drag_ = DragKind::kNone;
curvePointIndex_ = -1;
}
}
void ReaSamplerEditor::applyParamControl(int id, double value, int segment) {
+5 -2
View File
@@ -131,9 +131,12 @@ void ReaSamplerEditor::onMouseWheel(int delta) {
void ReaSamplerEditor::onSearchChar(unsigned int ch) {
// The curve popup: Esc dismisses (checked first — the popup is modal over the face, and
// the Browse search cannot hold focus under it).
// the Browse search cannot hold focus under it). Esc reaches here unconditionally
// (editor_platform's WM_CHAR routing), including mid-drag on a curve node — closeCurvePopup
// cancels that drag too, or a subsequent WM_MOUSEMOVE would resolve editedCurve() with the
// popup already closed.
if (curvePopup_ != CurveTarget::kNone && ch == 27) {
curvePopup_ = CurveTarget::kNone;
closeCurvePopup();
invalidate();
return;
}
+2 -2
View File
@@ -23,7 +23,7 @@ bool ReaSamplerEditor::handlePopupMouseDown(int w, int h, int x, int y) {
if (curvePopup_ == CurveTarget::kNone) return false;
const CurvePopupLayout pl = computeCurvePopup(w, h);
if (contains(pl.close, x, y)) {
curvePopup_ = CurveTarget::kNone;
closeCurvePopup();
invalidate();
return true;
}
@@ -32,7 +32,7 @@ bool ReaSamplerEditor::handlePopupMouseDown(int w, int h, int x, int y) {
return true;
}
if (popupOutsideSheet(pl, x, y) && drag_ == DragKind::kNone) {
curvePopup_ = CurveTarget::kNone;
closeCurvePopup();
invalidate();
}
return true;
+4 -27
View File
@@ -17,33 +17,10 @@ using namespace reasampler::ui;
using namespace reasampler::instrument::ui;
bool ReaSamplerEditor::deckKnobDisabled(int id) const {
switch (static_cast<ParamControl>(id)) {
case ParamControl::kPitchEnvAttack:
case ParamControl::kPitchEnvHold:
case ParamControl::kPitchEnvDecay:
case ParamControl::kPitchEnvDepth:
return !params_.play.pitchEnv.enabled;
case ParamControl::kFilterMorph:
case ParamControl::kFilterCutoff:
case ParamControl::kFilterQ:
case ParamControl::kFilterDrive:
case ParamControl::kFilterModAmt:
case ParamControl::kFilterKeyTrack:
// The filter's velocity curve sits in the VELOCITY group but is a filter parameter:
// it goes inert with every other one, so no surface can reach a param the knobs can't.
case ParamControl::kFilterVelCurve:
case ParamControl::kFilterEnvAttack:
case ParamControl::kFilterEnvHold:
case ParamControl::kFilterEnvDecay:
case ParamControl::kFilterEnvSustain:
case ParamControl::kFilterEnvRelease:
case ParamControl::kFilterTrigAttack:
case ParamControl::kFilterTrigHold:
case ParamControl::kFilterTrigDecay:
return !params_.play.filter.enabled;
default:
return false;
}
// Thin int-id wrapper over the pure, CTest-covered predicate — see deckKnobInert
// (deck_groups.h) for which cells go inert and why.
return deckKnobInert(static_cast<DeckParam>(id), params_.play.pitchEnv.enabled,
params_.play.filter.enabled);
}
bool ReaSamplerEditor::mouseDownDeck(const FaceLayout& fl, int x, int y) {
+6 -5
View File
@@ -1,7 +1,7 @@
// editor_paint_chrome.cpp — the CHROME band's painter: the toolbar row (product title +
// live readout, then the control run — preview, preview-velocity knob, curve button,
// Mono|Stereo, Browse) over the strip row, which the piano strip has to itself. Windows-only;
// all rects come from the pure sample_chrome interior and the pure keyboard_strip geometry.
// live readout, then the control run — preview, preview-velocity knob, Mono|Stereo, Browse)
// over the strip row, which the piano strip has to itself. Windows-only; all rects come from
// the pure sample_chrome interior and the pure keyboard_strip geometry.
#include "shell/instrument/reasampler_editor.h"
@@ -140,8 +140,9 @@ void ReaSamplerEditor::paintChrome(LICE_IBitmap* bmp, const FaceLayout& fl, bool
drawButton(bmp, box, nullptr, st, /*warn=*/false);
const PreviewGlyph g = previewGlyph(cr.preview);
if (!g.empty()) {
const LICE_pixel ink =
toLice(roleColor(active ? Role::BgBase : Role::TextPrimary));
// Same surface as the button label, so the glyph tracks drawButton's own rule
// (buttonLabelRole) rather than a second copy of it that could desync from the kit.
const LICE_pixel ink = toLice(roleColor(buttonLabelRole(st)));
LICE_FillTriangle(bmp, g.leftX, g.topY, g.leftX, g.bottomY, g.apexX, g.apexY,
ink, 1.0f, 0);
}
+1 -14
View File
@@ -17,19 +17,6 @@ using namespace reasampler::instrument::ui; // popup geometry
namespace {
// The curve a VELOCITY cell shows. Mirrors editedCurve's routing for the popup's own target;
// a cell draws whichever curve it opens, whether or not the popup is up.
const VelocityCurve& curveFor(const InstrumentParams& p, CurveTarget target) {
switch (target) {
case CurveTarget::kPitch: return p.play.pitchVelocityCurve;
case CurveTarget::kFilter: return p.play.filter.velocityCurve;
case CurveTarget::kAmp:
case CurveTarget::kNone:
return p.velocityCurve;
}
return p.velocityCurve;
}
const char* curveTitle(CurveTarget target) {
switch (target) {
case CurveTarget::kPitch: return "VELOCITY -> PITCH";
@@ -56,7 +43,7 @@ void ReaSamplerEditor::paintCurveButton(LICE_IBitmap* bmp, const Rect& r, CurveT
? roleColor(Role::AccentPrimary)
: roleColor(Role::LineHairline);
LICE_DrawRect(bmp, r.x, r.y, r.width - 1, r.height - 1, toLice(border), 1.0f, 0);
const VelocityCurve& curve = curveFor(params_, target);
const VelocityCurve& curve = curveFor(target);
const int inset = 3;
const VelocityCurve::Box mini{r.x + inset, r.y + inset, r.width - 2 * inset,
r.height - 2 * inset};
+1 -1
View File
@@ -179,7 +179,7 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
paintCurveButton(bmp, c.knob, curveCell, disabled,
!disabled && isHovered(HoverKind::kControl, c.id));
kitTextCentered(bmp, c.label, knobName(static_cast<ParamControl>(c.id)),
Font::Micro, disabled ? Role::LineHairline : Role::TextDim);
Font::Micro, Role::TextDim);
continue;
}
const bool dragging = (drag_ == DragKind::kDeckKnob && dragParamId_ == c.id);
+1 -1
View File
@@ -72,7 +72,7 @@ void ReaSamplerEditor::refreshFromBank() {
monoTrigger_ = processor_->monoTrigger();
// A refresh that emptied the selection closes the curve popup — an open-but-invisible
// modal would otherwise swallow clicks on the empty state.
if (selectedId_.empty()) curvePopup_ = CurveTarget::kNone;
if (selectedId_.empty()) closeCurvePopup();
// Drop a filter that names a bank no longer present.
if (!activeFilterBankId_.empty()) {
bool found = false;
+16 -5
View File
@@ -150,7 +150,7 @@ private:
// --- Band painters (one TU each, mirroring the input side) ---
// Chrome: title band + Browse nav + the control row (root strip, preview, velocity knob,
// curve button, channel toggle).
// channel toggle).
void paintChrome(LICE_IBitmap* bmp, const FaceLayout& fl, bool empty);
// The hovered piano key's note-name chip. Drawn after every band — it overhangs the
// chrome into whatever is below it.
@@ -459,11 +459,22 @@ private:
// delta from this anchor, so a grab never jumps the value.
double dragKnobStartValue_ = 0.0;
// Which velocity curve the popup is editing; kNone = closed. Never persisted.
// Which velocity curve the popup is editing; kNone = closed. Never persisted. Every writer
// of kNone must also cancel a live curve-node drag (closeCurvePopup does both) — an Esc
// mid-drag that closed the popup without cancelling the drag used to leave editedCurve()'s
// mutable overload aliasing the amp curve underneath an in-flight pitch/filter drag.
CurveTarget curvePopup_ = CurveTarget::kNone;
// The parameter-set curve `curvePopup_` names. Both overloads exist because every edit
// path needs the mutable one and paint needs the const one; routing through this ONE
// switch is what keeps the three curves on a single popup/draw/hit-test code path.
void closeCurvePopup();
// THE one switch from a CurveTarget to the parameter-set curve it names — paint (button
// thumbnails, for every target) and edit (editedCurve, for curvePopup_ specifically) both
// route through it, so a fourth curve or a moved field is a one-place edit.
const VelocityCurve& curveFor(CurveTarget target) const;
VelocityCurve& curveFor(CurveTarget target);
// The curve curvePopup_ names — curveFor(curvePopup_), typed as its own pair because every
// edit path needs the mutable overload and paint needs the const one. The mutable overload
// refuses kNone (a closed popup has nothing open to edit) rather than aliasing amp.
VelocityCurve& editedCurve();
const VelocityCurve& editedCurve() const;