fix: close review findings on spline EGs — engine, codec, and popup/overlay UI grammar
Live pitch depth, Gate/Spline enable-rule agreement, inert kTrigLength, NaN wire guards, hard-flag-tail corruption no longer wipes the record, RT/cold spline tie-break, retired alt-click, marker-shadow fix, plus new test coverage.
This commit is contained in:
@@ -11,12 +11,10 @@
|
||||
#include <cstdint>
|
||||
#include <cstdio> // snprintf (deck value labels)
|
||||
#include <string>
|
||||
#include <utility> // std::as_const (the const/non-const editedCurve pair)
|
||||
#include <vector>
|
||||
|
||||
#include "core/instrument/engine/filter/filter_params.h" // the filter's own control laws
|
||||
#include "core/instrument/engine/master_gain.h" // master-gain dB<->linear<->knob taper
|
||||
#include "core/instrument/map/trigger_seam.h" // triggerPlayLength (the Trigger play span)
|
||||
#include "core/instrument/ui/deck_groups.h" // sampleDeckGroups (the deck's composition)
|
||||
#include "core/instrument/ui/knob_deck.h" // deckHeight / kDeckKnobSize (the band's own height)
|
||||
#include "core/util/clamp01.h"
|
||||
@@ -26,7 +24,7 @@
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
using namespace reasampler::instrument::map; // PlaySeconds vocabulary + trigger_seam
|
||||
using namespace reasampler::instrument::map; // PlaySeconds vocabulary
|
||||
using instrument::ui::computeSampleBands;
|
||||
using instrument::ui::chromeRects;
|
||||
using instrument::ui::deckHeight;
|
||||
@@ -299,6 +297,15 @@ double ReaSamplerEditor::deckControlNorm(int id) const {
|
||||
}
|
||||
}
|
||||
|
||||
instrument::ui::DeckEnableState ReaSamplerEditor::deckEnableState() const {
|
||||
const PlaySeconds& play = params_.play;
|
||||
return instrument::ui::DeckEnableState{
|
||||
play.pitchEnv.enabled, play.filter.enabled,
|
||||
play.ampSpline.mode == EnvMode::Spline,
|
||||
play.pitchSpline.mode == EnvMode::Spline,
|
||||
play.filterSpline.mode == EnvMode::Spline};
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::applyDeckKnob(int id, double norm) {
|
||||
if (!processor_) return;
|
||||
norm = clamp01(norm);
|
||||
|
||||
@@ -112,17 +112,22 @@ void ReaSamplerEditor::onMouseUp(int x, int y) {
|
||||
invalidate();
|
||||
return;
|
||||
}
|
||||
// Drag-off delete: releasing a curve-node drag well outside the box removes the dragged
|
||||
// point (deletePoint refuses the two endpoints, so an endpoint drag-off is a plain move —
|
||||
// its amp keeps the last clamped drag value).
|
||||
if (kind == DragKind::kCurveNode && curveIdx >= 0) {
|
||||
// Drag-off delete: releasing a curve-node drag well outside its box removes the dragged
|
||||
// point on EITHER spline surface — the popup's kCurveNode and the overlay's kSplineNode
|
||||
// share this grammar, not just their click grammar (deletePoint refuses the two endpoints,
|
||||
// so an endpoint drag-off is a plain move — its amp keeps the last clamped drag value).
|
||||
if ((kind == DragKind::kCurveNode || kind == DragKind::kSplineNode) && curveIdx >= 0) {
|
||||
const bool off = x < curveRect.x - kCurveDragOffMargin ||
|
||||
x > curveRect.right() + kCurveDragOffMargin ||
|
||||
y < curveRect.y - kCurveDragOffMargin ||
|
||||
y > curveRect.bottom() + kCurveDragOffMargin;
|
||||
if (off) {
|
||||
editedCurve().deletePoint(static_cast<std::size_t>(curveIdx));
|
||||
hover_ = HoverTarget{}; // stale kCurveNode index would light a shifted node
|
||||
if (kind == DragKind::kCurveNode) {
|
||||
editedCurve().deletePoint(static_cast<std::size_t>(curveIdx));
|
||||
} else {
|
||||
splineFor(overlayEnv_).deletePoint(static_cast<std::size_t>(curveIdx));
|
||||
}
|
||||
hover_ = HoverTarget{}; // stale node index would light a shifted node
|
||||
}
|
||||
}
|
||||
commitAndReload();
|
||||
|
||||
@@ -45,17 +45,8 @@ void ReaSamplerEditor::handleCurveMouseDown(const Rect& r, int x, int y) {
|
||||
const VelocityCurve::Box box = curveBoxFromRect(r);
|
||||
if (box.width <= 0 || box.height <= 1) return;
|
||||
|
||||
// Alt-click delete predates the converged grammar and is kept as a landed alternate; every
|
||||
// other gesture routes through the shared resolver so both spline consumers stay one
|
||||
// grammar.
|
||||
if ((GetKeyState(VK_MENU) & 0x8000) != 0) {
|
||||
const int alt = editedCurve().pointAtPixel(box, x, y);
|
||||
if (alt >= 0 && editedCurve().deletePoint(static_cast<std::size_t>(alt))) {
|
||||
hover_ = HoverTarget{}; // a stale kCurveNode index would light a shifted node
|
||||
commitAndReload();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Alt-click delete is retired (the spec's right-click supersedes it — one grammar, no
|
||||
// migration on either side): every gesture here routes through the shared resolver.
|
||||
const bool ctrl = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
|
||||
const SplineEdit edit = resolveSplineEdit(
|
||||
editedCurve(), box, ctrl ? SplineGesture::kControlLeft : SplineGesture::kLeft, x, y);
|
||||
|
||||
@@ -63,11 +63,19 @@ bool ReaSamplerEditor::mouseDownDeck(const FaceLayout& fl, int x, int y) {
|
||||
applyParamControl(hit.id, 0.0, hit.segment);
|
||||
commitAndReload();
|
||||
break;
|
||||
default:
|
||||
// Parameter-set toggles (play mode / pitch engine / pitch-env + filter enable).
|
||||
default: {
|
||||
// Parameter-set toggles (play mode / pitch engine / pitch-env + filter enable,
|
||||
// and the three env-mode toggles).
|
||||
applyParamControl(hit.id, 0.0, hit.segment);
|
||||
commitAndReload();
|
||||
// Flipping an EG's Staged|Spline toggle makes THAT envelope's overlay active,
|
||||
// so the contour (or the staged shape you just returned to) is what's drawn.
|
||||
// overlayEnvForModeToggle answers kNone for every other toggle this default
|
||||
// case handles, which is why the assignment is conditional.
|
||||
const OverlayEnv modeEnv = overlayEnvForModeToggle(hit.id);
|
||||
if (modeEnv != OverlayEnv::kNone) overlayEnv_ = modeEnv;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -31,17 +31,16 @@ bool ReaSamplerEditor::mouseDownWaveform(const FaceLayout& fl, int x, int y) {
|
||||
if (frames <= 0) return false;
|
||||
const OverlayArea overlay = waveformOverlayArea(fl.bands.waveform);
|
||||
|
||||
// A drawn EG's own point actions come first, for the same reason the staged nodes do —
|
||||
// they sit on top of the markers. Its ADD, which any empty-space click satisfies, is held
|
||||
// back until after the markers below, or a spline overlay would make them unreachable.
|
||||
// A drawn EG's contour is evaluated below, AFTER the markers: pointAtPixel's pick radius
|
||||
// has no box check of its own, so a coincident contour node would otherwise shadow a
|
||||
// marker's own dedicated grab rect (the loop-crossfade tab most sharply, since it is the
|
||||
// ONLY affordance at zero crossfade) — marker reachability wins on any pixel overlap. The
|
||||
// staged envelope-node pass just below is unaffected (its own, longer-standing ordering).
|
||||
const DeckEnableState gates = deckEnableState();
|
||||
const bool splineLive = overlayIsSpline() && overlayEnvEnabled(overlayEnv_, gates);
|
||||
const SplineGesture gesture = (GetKeyState(VK_CONTROL) & 0x8000) != 0
|
||||
? SplineGesture::kControlLeft
|
||||
: SplineGesture::kLeft;
|
||||
if (splineLive && splineOverlayClick(overlay, x, y, gesture, /*addOnEmptySpace=*/false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Envelope nodes first (they sit on top of the markers), then the wave markers. With no
|
||||
// envelope overlay-active — or with its deck group's enable toggle off, which makes the
|
||||
@@ -83,6 +82,11 @@ bool ReaSamplerEditor::mouseDownWaveform(const FaceLayout& fl, int x, int y) {
|
||||
beginMarkerDrag(static_cast<WaveMarker>(hit), m, frames, x);
|
||||
return true;
|
||||
}
|
||||
// No marker wanted the click: a drawn EG's own node grab/toggle (never add — empty space is
|
||||
// tried last, below).
|
||||
if (splineLive && splineOverlayClick(overlay, x, y, gesture, /*addOnEmptySpace=*/false)) {
|
||||
return true;
|
||||
}
|
||||
// Nothing else wanted the click: now the drawn contour may take the empty space.
|
||||
if (splineLive) return splineOverlayClick(overlay, x, y, gesture, /*addOnEmptySpace=*/true);
|
||||
return false;
|
||||
@@ -92,7 +96,15 @@ bool ReaSamplerEditor::splineOverlayClick(const OverlayArea& waveArea, int x, in
|
||||
SplineGesture gesture, bool addOnEmptySpace) {
|
||||
const VelocityCurve::Box box = splineOverlayBox(waveArea);
|
||||
VelocityCurve& contour = splineFor(overlayEnv_);
|
||||
const SplineEdit edit = resolveSplineEdit(contour, box, gesture, x, y);
|
||||
SplineEdit edit = resolveSplineEdit(contour, box, gesture, x, y);
|
||||
// resolveSplineEdit's outside-box grab/delete/toggle allowance (pointAtPixel's radius has
|
||||
// no box check of its own) was designed for the popup's inset ring; the overlay box has NO
|
||||
// inset (splineOverlayBox), so honoring it here would extend the grab halo 6px into the
|
||||
// inter-band pad. kAdd already requires in-box (resolveSplineEdit's own check).
|
||||
if (edit.kind != SplineEditKind::kNone && edit.kind != SplineEditKind::kAdd &&
|
||||
!(x >= box.left && x < box.left + box.width && y >= box.top && y < box.top + box.height)) {
|
||||
edit = SplineEdit{};
|
||||
}
|
||||
if (edit.kind == SplineEditKind::kAdd && !addOnEmptySpace) return false;
|
||||
switch (edit.kind) {
|
||||
case SplineEditKind::kNone:
|
||||
@@ -125,6 +137,10 @@ bool ReaSamplerEditor::splineOverlayClick(const OverlayArea& waveArea, int x, in
|
||||
drag_ = DragKind::kSplineNode;
|
||||
curvePointIndex_ = index;
|
||||
dragStartCurve_ = contour; // AFTER the add — resolvePointDrag's delta base
|
||||
// The release-time drag-off-delete bound (onMouseUp), matching the popup's kCurveNode use
|
||||
// of the same field — the two spline surfaces share one drag-off grammar, not just the
|
||||
// click grammar.
|
||||
dragCurveRect_ = waveArea.rect;
|
||||
dragStartX_ = x;
|
||||
dragStartY_ = y;
|
||||
invalidate(); // live feedback; the commit lands on WM_LBUTTONUP
|
||||
|
||||
@@ -112,15 +112,6 @@ void ReaSamplerEditor::unpackEnvelope(OverlayEnv which, const StageEnvelope& env
|
||||
}
|
||||
}
|
||||
|
||||
instrument::ui::DeckEnableState ReaSamplerEditor::deckEnableState() const {
|
||||
const PlaySeconds& play = params_.play;
|
||||
return instrument::ui::DeckEnableState{
|
||||
play.pitchEnv.enabled, play.filter.enabled,
|
||||
play.ampSpline.mode == EnvMode::Spline,
|
||||
play.pitchSpline.mode == EnvMode::Spline,
|
||||
play.filterSpline.mode == EnvMode::Spline};
|
||||
}
|
||||
|
||||
const VelocityCurve& ReaSamplerEditor::splineFor(OverlayEnv which) const {
|
||||
switch (which) {
|
||||
case OverlayEnv::kPitch: return params_.play.pitchSpline.contour;
|
||||
@@ -177,12 +168,4 @@ VelocityCurve& ReaSamplerEditor::editedCurve() {
|
||||
return curveFor(curvePopup_);
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::closeCurvePopup() {
|
||||
curvePopup_ = CurveTarget::kNone;
|
||||
if (drag_ == DragKind::kCurveNode) {
|
||||
drag_ = DragKind::kNone;
|
||||
curvePointIndex_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace reasampler::vst
|
||||
|
||||
@@ -159,9 +159,13 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
|
||||
drawToggle(t, "Off", "On", play.filter.enabled, false);
|
||||
break;
|
||||
case ParamControl::kAmpEnvMode:
|
||||
drawToggle(t, "Stg", "Spl", play.ampSpline.mode == EnvMode::Spline, false);
|
||||
break;
|
||||
case ParamControl::kPitchEnvMode:
|
||||
drawToggle(t, "Stg", "Spl", play.pitchSpline.mode == EnvMode::Spline, false);
|
||||
break;
|
||||
case ParamControl::kFilterEnvMode:
|
||||
drawToggle(t, "Stg", "Spl", deckControlNorm(t.id) > 0.5, false);
|
||||
drawToggle(t, "Stg", "Spl", play.filterSpline.mode == EnvMode::Spline, false);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
@@ -141,7 +141,10 @@ void ReaSamplerEditor::paintSplineOverlay(LICE_IBitmap* bmp, const OverlayArea&
|
||||
// handles share the coordinate system the hit-test resolves against.
|
||||
const LICE_pixel line = toLice(roleColor(Role::OverlayTrace));
|
||||
int prevX = 0, prevY = 0;
|
||||
for (int px = 0; px <= box.width; ++px) {
|
||||
// < not <=: box.left + box.width is the overlay's own EXCLUSIVE right edge (the box has no
|
||||
// inset, unlike the popup's), so a <= column paints one pixel into the next band's pad —
|
||||
// and it is redundant with the clamped endpoint handle below anyway.
|
||||
for (int px = 0; px < box.width; ++px) {
|
||||
const int cx = box.left + px;
|
||||
const double t = curve.pointFromPixel(box, cx, box.top).velocity;
|
||||
const int cy = curve.pixelFromPoint(box, {t, curve.eval(t)}).y;
|
||||
@@ -153,9 +156,18 @@ void ReaSamplerEditor::paintSplineOverlay(LICE_IBitmap* bmp, const OverlayArea&
|
||||
// Handles carry two independent states on the same mark, so they use two independent
|
||||
// channels: SIZE is the grab (the staged painter's grammar — a hotter hue reads as lower
|
||||
// contrast over the lime, see its note), and HOLLOW is hard. The corner itself is the
|
||||
// primary hard cue, but a corner between two near-collinear segments has none to show.
|
||||
// primary hard cue, but a corner between two near-collinear segments has none to show. A
|
||||
// grabbed node past the drag-off margin draws WARN — the popup's same "release will
|
||||
// delete" cue (editor_paint_curve.cpp), since the two spline surfaces share the drag-off
|
||||
// grammar, not just the click grammar.
|
||||
const LICE_pixel handle = toLice(roleColor(Role::OverlayTrace));
|
||||
const LICE_pixel handleWarn = toLice(roleColor(Role::Warn));
|
||||
const LICE_pixel core = toLice(roleColor(Role::BgBase));
|
||||
const bool dragOffArmed = drag_ == DragKind::kSplineNode &&
|
||||
(dragCurX_ < box.left - kCurveDragOffMargin ||
|
||||
dragCurX_ > box.left + box.width + kCurveDragOffMargin ||
|
||||
dragCurY_ < box.top - kCurveDragOffMargin ||
|
||||
dragCurY_ > box.top + box.height + kCurveDragOffMargin);
|
||||
const std::vector<instrument::engine::VelocityPoint>& pts = curve.points();
|
||||
for (std::size_t i = 0; i < pts.size(); ++i) {
|
||||
const auto np = curve.pixelFromPoint(box, pts[i]);
|
||||
@@ -167,7 +179,8 @@ void ReaSamplerEditor::paintSplineOverlay(LICE_IBitmap* bmp, const OverlayArea&
|
||||
(std::min)(box.left + box.width - 1 - r, np.x));
|
||||
const int hy = (std::max)(box.top + r,
|
||||
(std::min)(box.top + box.height - 1 - r, np.y));
|
||||
LICE_FillRect(bmp, hx - r, hy - r, 2 * r, 2 * r, handle, 1.0f, 0);
|
||||
const LICE_pixel fill = (grabbed && dragOffArmed) ? handleWarn : handle;
|
||||
LICE_FillRect(bmp, hx - r, hy - r, 2 * r, 2 * r, fill, 1.0f, 0);
|
||||
if (pts[i].hard && ir > 0) {
|
||||
LICE_FillRect(bmp, hx - ir, hy - ir, 2 * ir, 2 * ir, core, 1.0f, 0);
|
||||
}
|
||||
|
||||
@@ -157,6 +157,14 @@ bool ReaSamplerEditor::dragCommitsLive(DragKind kind, int paramId) const {
|
||||
return instrument::ui::liveCommitFor(k, paramId);
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::closeCurvePopup() {
|
||||
curvePopup_ = CurveTarget::kNone;
|
||||
if (drag_ == DragKind::kCurveNode) {
|
||||
drag_ = DragKind::kNone;
|
||||
curvePointIndex_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::loadSelection(const std::string& id) {
|
||||
// A load REPLACES the loaded sound. The shaping parameters (play mode, envelopes, pitch
|
||||
// engine, key-track, velocity curve) are NOT reset — the one set governs whatever is
|
||||
|
||||
@@ -216,8 +216,7 @@ private:
|
||||
|
||||
// Mouse-down inside curve-editor box `r`, resolved through the shared point-editing
|
||||
// grammar (spline_edit): a node grab starts a kCurveNode drag, an empty-space click adds a
|
||||
// point and grabs it, control-click toggles a node hard/smooth. Alt-click delete predates
|
||||
// that grammar and is kept as a landed alternate.
|
||||
// point and grabs it, control-click toggles a node hard/smooth.
|
||||
void handleCurveMouseDown(const Rect& r, int x, int y);
|
||||
|
||||
// Left-click while the curve popup is open (modal over the Sample face): Close /
|
||||
@@ -270,8 +269,8 @@ private:
|
||||
void commitLive();
|
||||
|
||||
// Whether an in-flight drag commits live rather than through a reload. A deck knob is
|
||||
// live per isLiveDeckParam; an envelope-node drag is live only in Gate, where it edits the
|
||||
// AHDSR — in Trigger the same drag rewrites the play span, which is not a live control.
|
||||
// live per isLiveDeckParam; an envelope-node drag is live in EITHER mode — see
|
||||
// liveCommitFor (deck_groups.h) for why.
|
||||
bool dragCommitsLive(DragKind kind, int paramId = -1) const;
|
||||
|
||||
// Commits `id` as the loaded capture. The one parameter set carries over — it governs
|
||||
|
||||
Reference in New Issue
Block a user