instrument: spline EGs — hard points on the one shared spline, a drawn contour per envelope beside its staged state, payload v13

This commit is contained in:
2026-07-31 21:33:59 -04:00
parent f115904e4f
commit e44bd42dd9
33 changed files with 1739 additions and 364 deletions
+70 -2
View File
@@ -14,6 +14,7 @@
#include "core/instrument/engine/loop/loop_span.h" // maxCrossfade (the shared drag-clamp bound)
#include "core/instrument/ui/envelope_edit.h" // nodeAtPoint / resolveNodeDrag
#include "core/instrument/ui/spline_edit.h" // the shared point-editing grammar
#include "core/instrument/ui/waveform_view.h" // waveformOverlayArea / markerAtPoint / snap
#include "shell/instrument/editor_internal.h"
#include "shell/instrument/reasampler_processor.h"
@@ -30,13 +31,24 @@ 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.
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
// same params' knobs inert — there are no grabbable nodes and the markers take every grab.
const double rate = liveSampleRate();
const bool nodesLive =
overlayEnv_ != OverlayEnv::kNone &&
!overlayEnvInert(overlayEnv_, params_.play.pitchEnv.enabled, params_.play.filter.enabled);
overlayEnv_ != OverlayEnv::kNone && !overlayEnvInert(overlayEnv_, gates);
if (rate > 0.0 && nodesLive) {
const std::int64_t startFrame = params_.startPoint.value_or(0);
const StageEnvelope env = packEnvelope(overlayEnv_, params_.play, frames, startFrame);
@@ -71,9 +83,54 @@ bool ReaSamplerEditor::mouseDownWaveform(const FaceLayout& fl, int x, int y) {
beginMarkerDrag(static_cast<WaveMarker>(hit), m, frames, x);
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;
}
bool ReaSamplerEditor::splineOverlayClick(const OverlayArea& waveArea, int x, int y,
SplineGesture gesture, bool addOnEmptySpace) {
const VelocityCurve::Box box = splineOverlayBox(waveArea);
VelocityCurve& contour = splineFor(overlayEnv_);
const SplineEdit edit = resolveSplineEdit(contour, box, gesture, x, y);
if (edit.kind == SplineEditKind::kAdd && !addOnEmptySpace) return false;
switch (edit.kind) {
case SplineEditKind::kNone:
return false;
case SplineEditKind::kDelete:
// deletePoint refuses the two endpoints, so a right-click on one is a safe no-op.
if (!contour.deletePoint(static_cast<std::size_t>(edit.index))) return true;
hover_ = HoverTarget{}; // a stale index would light a shifted node
commitAndReload();
return true;
case SplineEditKind::kToggleHard:
if (!contour.toggleHard(static_cast<std::size_t>(edit.index))) return true;
commitAndReload();
return true;
case SplineEditKind::kAdd:
case SplineEditKind::kGrab:
break;
}
// Snapshot BEFORE the add so a capture-loss rollback cancels the in-flight point too
// (the same contract the other parameter-editing drags keep).
dragStartParams_ = params_;
int index = edit.index;
if (edit.kind == SplineEditKind::kAdd) {
const VelocityPoint p = contour.pointFromPixel(box, x, y);
index = contour.addPoint(p.velocity, p.value);
// At the ceiling: refused, contour untouched, nothing to roll back. Swallow the click
// rather than letting it fall through to a marker grab under the cursor.
if (index < 0) return true;
}
drag_ = DragKind::kSplineNode;
curvePointIndex_ = index;
dragStartCurve_ = contour; // AFTER the add — resolvePointDrag's delta base
dragStartX_ = x;
dragStartY_ = y;
invalidate(); // live feedback; the commit lands on WM_LBUTTONUP
return true;
}
void ReaSamplerEditor::beginMarkerDrag(WaveMarker which, const SetupMarkers& m,
std::int64_t frames, int x) {
drag_ = DragKind::kWaveMarker;
@@ -88,6 +145,17 @@ void ReaSamplerEditor::dragWaveform(const FaceLayout& fl, int x, int y) {
const OverlayArea overlay = waveformOverlayArea(fl.bands.waveform);
const int dx = x - dragStartX_;
if (drag_ == DragKind::kSplineNode) {
// Same absolute-delta contract as the popup's node drag, against the grab-time contour
// and the overlay's own box.
if (curvePointIndex_ < 0) return;
splineFor(overlayEnv_) = VelocityCurve::resolvePointDrag(
dragStartCurve_, static_cast<std::size_t>(curvePointIndex_),
splineOverlayBox(overlay), dx, y - dragStartY_);
invalidate(); // live feedback; commit on release
return;
}
if (drag_ == DragKind::kEnvNode) {
// Resolve the grabbed envelope node's new params from the pixel delta (through the
// pure envelope_edit inverse map, clamped), then unpack them back onto the parameter