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:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user