From d982d14760d75414863a56ccbcb3cd4676a0fa8d Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Mon, 27 Jul 2026 16:05:05 -0400 Subject: [PATCH] fix(velcurve-ui): caption overlap, add-in-ring data corruption, drag-off warn affordance --- src/vst/reasampler_editor.cpp | 41 ++++++++++++++++++++++++++++------- src/vst/reasampler_editor.h | 2 ++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/vst/reasampler_editor.cpp b/src/vst/reasampler_editor.cpp index 4dbb8ab..e068f1f 100644 --- a/src/vst/reasampler_editor.cpp +++ b/src/vst/reasampler_editor.cpp @@ -700,7 +700,7 @@ constexpr int kPad = 8; // border so an endpoint at amp 0/1 stays grabbable. constexpr int kVelCurveBoxW = 168; // the curve box width (incl. border) constexpr int kVelCurveMinHeroW = 240; // hero must keep at least this much width beside the box -constexpr int kVelCurveInset = 6; // border -> mapping-box inset (>= node half-size + 3) +constexpr int kVelCurveInset = 14; // border -> mapping-box inset: caption band (~12px) + 2px gap constexpr int kCurveDragOffMargin = 24; // release beyond box+margin -> drag-off delete struct SampleBands { @@ -1157,17 +1157,28 @@ void ReaSamplerEditor::paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r, } // Draggable node handles (mirror of the envelope overlay's): accent-primary squares lifted - // to accent-hot when grabbed or hovered. + // to accent-hot when grabbed or hovered, or warn when a drag-off delete is armed (cursor + // has passed kCurveDragOffMargin outside the box — release will delete the node). const LICE_pixel handle = toLice(roleColor(Role::AccentPrimary)); const LICE_pixel handleHot = toLice(roleColor(Role::AccentHot)); + const LICE_pixel handleWarn = toLice(roleColor(Role::Warn)); + // Drag-off check: during a kCurveNode drag on THIS box, is the live cursor beyond the margin? + const bool dragOffArmed = (drag_ == DragKind::kCurveNode && dragCurveRect_.left == r.left && + dragCurveRect_.top == r.top) && + (dragCurX_ < r.left - kCurveDragOffMargin || + dragCurX_ > r.right + kCurveDragOffMargin || + dragCurY_ < r.top - kCurveDragOffMargin || + dragCurY_ > r.bottom + kCurveDragOffMargin); for (std::size_t i = 0; i < curve.points().size(); ++i) { const auto np = VelocityCurve::pixelFromPoint(box, curve.points()[i]); const bool grabbed = (drag_ == DragKind::kCurveNode && curvePointIndex_ == static_cast(i)); const bool hot = grabbed || isHovered(HoverKind::kCurveNode, static_cast(i)); + // A grabbed node in drag-off territory draws warn to signal "release will delete." + const LICE_pixel col = (grabbed && dragOffArmed) ? handleWarn + : (hot ? handleHot : handle); const int nr = 3; - LICE_FillRect(bmp, np.x - nr, np.y - nr, 2 * nr, 2 * nr, hot ? handleHot : handle, - 1.0f, 0); + LICE_FillRect(bmp, np.x - nr, np.y - nr, 2 * nr, 2 * nr, col, 1.0f, 0); } } @@ -1193,13 +1204,24 @@ void ReaSamplerEditor::handleCurveMouseDown(const Rect& r, int zoneIndex, int x, // ADD (mirror of the other map-editing drags' dragStartMap_ contract). dragStartMap_ = map_; - // Empty-space click: add a control point at the cursor via the pure inverse map, then grab - // it — the click flows straight into a placing drag. + // Empty-space click inside the MAPPING BOX: add a control point at the cursor via the pure + // inverse map, then grab it — the click flows straight into a placing drag. Guard: the caller + // gates on contains(r, x, y) (the full border rect), but the 6+px inset ring — including the + // caption band — must not add a point; a click there would clamp to velocity 0/127 and + // produce an undeletable duplicate stacked on an endpoint. Clicks in the ring may still grab + // an existing node (pointAtPixel's pick radius legitimately extends into the ring), which is + // handled above; only the add path is box-gated here. if (idx < 0) { - const VelocityPoint p = VelocityCurve::pointFromPixel(box, x, y); - idx = static_cast(z.velocityCurve.addPoint(p.velocity, p.amp)); + const bool inBox = (x >= box.left && x < box.left + box.width && + y >= box.top && y < box.top + box.height); + if (inBox) { + const VelocityPoint p = VelocityCurve::pointFromPixel(box, x, y); + idx = static_cast(z.velocityCurve.addPoint(p.velocity, p.amp)); + } } + if (idx < 0) return; // ring click with no node hit — nothing to grab + drag_ = DragKind::kCurveNode; curvePointIndex_ = idx; dragStartCurve_ = z.velocityCurve; // AFTER the add — resolvePointDrag's absolute-delta base @@ -2056,6 +2078,8 @@ bool ReaSamplerEditor::handleControlClick(int zoneIndex, const Rect& panel, int void ReaSamplerEditor::onMouseMove(int x, int y) { if (drag_ == DragKind::kNone) return; + dragCurX_ = x; // keep the live cursor position for drag-state draw cues (e.g. drag-off warn) + dragCurY_ = y; RECT cr{}; GetClientRect(childHwnd_, &cr); const int w = cr.right - cr.left; @@ -2276,6 +2300,7 @@ void ReaSamplerEditor::onMouseUp(int x, int y) { if (off) { map_.zones[static_cast(curveZone)].velocityCurve.deletePoint( static_cast(curveIdx)); + hover_ = HoverTarget{}; // stale kCurveNode index would light a shifted node on next paint } } commitAndReload(); diff --git a/src/vst/reasampler_editor.h b/src/vst/reasampler_editor.h index 4b2dd78..b18897e 100644 --- a/src/vst/reasampler_editor.h +++ b/src/vst/reasampler_editor.h @@ -392,6 +392,8 @@ private: DragKind drag_ = DragKind::kNone; int dragStartX_ = 0; // grab x (px), for the pixel-delta resolver int dragStartY_ = 0; // grab y (px), for the vertical scrollbar-thumb drag + int dragCurX_ = 0; // live cursor x (px) during a drag — updated in onMouseMove + int dragCurY_ = 0; // live cursor y (px) during a drag — updated in onMouseMove int dragStartLow_ = 0; // the grabbed field's note at grab time int dragStartHigh_ = 0; int dragStartRoot_ = 60;