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
+44 -34
View File
@@ -1,6 +1,7 @@
// editor_input_curve.cpp — the velocity-curve popup's input: the modal click routing,
// node grab/add/Alt-delete inside the curve box, the live node drag, the right-click
// delete, and the popup's hover. Band-independent (the sheet floats over the whole face).
// node grab/add/toggle/delete inside the curve box, the live node drag, the right-click
// delete on EITHER spline surface, and the popup's hover. Band-independent (the sheet floats
// over the whole face).
// Windows-only.
#include "shell/instrument/reasampler_editor.h"
@@ -8,6 +9,8 @@
#ifdef _WIN32
#include "core/instrument/ui/curve_popup.h" // computeCurvePopup / popupOutsideSheet
#include "core/instrument/ui/spline_edit.h" // the shared point-editing grammar
#include "core/instrument/ui/waveform_view.h" // waveformOverlayArea (the overlay right-click)
#include "shell/instrument/editor_internal.h" // curveBoxFromRect
#include "shell/instrument/reasampler_processor.h"
@@ -42,37 +45,37 @@ void ReaSamplerEditor::handleCurveMouseDown(const Rect& r, int x, int y) {
const VelocityCurve::Box box = curveBoxFromRect(r);
if (box.width <= 0 || box.height <= 1) return;
int idx = editedCurve().pointAtPixel(box, x, y);
// Modifier-click (Alt) deletes an interior node — a discrete, final edit committed at once
// (deletePoint refuses the two endpoints, so an Alt-click on them is a safe no-op).
if (idx >= 0 && (GetKeyState(VK_MENU) & 0x8000) != 0) {
if (editedCurve().deletePoint(static_cast<std::size_t>(idx))) {
// 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;
}
const bool ctrl = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
const SplineEdit edit = resolveSplineEdit(
editedCurve(), box, ctrl ? SplineGesture::kControlLeft : SplineGesture::kLeft, x, y);
if (edit.kind == SplineEditKind::kToggleHard) {
if (editedCurve().toggleHard(static_cast<std::size_t>(edit.index))) commitAndReload();
return;
}
if (edit.kind == SplineEditKind::kNone) return; // ring click with no node hit
// Snapshot BEFORE any mutation so a capture-loss rollback also cancels an in-flight ADD
// (mirror of the other parameter-editing drags' dragStartParams_ contract).
dragStartParams_ = params_;
// Empty-space click inside the mapping box: add a control point via the pure inverse map,
// then grab it. Box-gated (not just contains(r,x,y)) because the inset ring must not add a
// point — it would clamp to velocity 0/127, stacking an undeletable duplicate on an
// endpoint. A ring click can still grab an existing node (handled above).
if (idx < 0) {
const bool inBox = (x >= box.left && x < box.left + box.width &&
y >= box.top && y < box.top + box.height);
if (inBox) {
const VelocityPoint p = editedCurve().pointFromPixel(box, x, y);
idx = static_cast<int>(editedCurve().addPoint(p.velocity, p.value));
}
int idx = edit.index;
if (edit.kind == SplineEditKind::kAdd) {
const VelocityPoint p = editedCurve().pointFromPixel(box, x, y);
idx = editedCurve().addPoint(p.velocity, p.value);
if (idx < 0) return; // at the point ceiling: refused, curve untouched
}
if (idx < 0) return; // ring click with no node hit — nothing to grab
drag_ = DragKind::kCurveNode;
curvePointIndex_ = idx;
dragStartCurve_ = editedCurve(); // AFTER the add — resolvePointDrag's delta base
@@ -94,23 +97,30 @@ void ReaSamplerEditor::dragCurve(int x, int y) {
}
void ReaSamplerEditor::onMouseRDown(int x, int y) {
// Right-click on a popup curve node deletes it — the primary delete affordance; Alt-click
// and drag-off remain as landed alternates. Commits immediately through the same path as
// Alt-click; deletePoint's endpoint guard makes an endpoint right-click a safe no-op.
// Right-clicks act only while the popup is open, and never during an in-flight left drag.
if (!processor_ || view_ == View::kBrowse || curvePopup_ == CurveTarget::kNone) return;
// Right-click deletes a node on EITHER spline surface — the primary delete affordance;
// Alt-click and drag-off remain as landed alternates in the popup. deletePoint's endpoint
// guard makes an endpoint right-click a safe no-op. Never acts during an in-flight left
// drag; the modal popup wins when it is open.
if (!processor_ || view_ == View::kBrowse) return;
if (drag_ != DragKind::kNone) return;
RECT rc{};
GetClientRect(childHwnd_, &rc);
const CurvePopupLayout pl = computeCurvePopup(rc.right - rc.left, rc.bottom - rc.top);
if (!contains(pl.curveBox, x, y)) return;
const VelocityCurve::Box box = curveBoxFromRect(pl.curveBox);
const int idx = editedCurve().pointAtPixel(box, x, y);
if (idx < 0) return;
if (editedCurve().deletePoint(static_cast<std::size_t>(idx))) {
hover_ = HoverTarget{}; // a stale kCurveNode index would light a shifted node
commitAndReload();
if (curvePopup_ != CurveTarget::kNone) {
const CurvePopupLayout pl = computeCurvePopup(rc.right - rc.left, rc.bottom - rc.top);
if (!contains(pl.curveBox, x, y)) return;
const SplineEdit edit = resolveSplineEdit(editedCurve(), curveBoxFromRect(pl.curveBox),
SplineGesture::kRight, x, y);
if (edit.kind != SplineEditKind::kDelete) return;
if (editedCurve().deletePoint(static_cast<std::size_t>(edit.index))) {
hover_ = HoverTarget{}; // a stale kCurveNode index would light a shifted node
commitAndReload();
}
return;
}
if (!overlayIsSpline() || !overlayEnvEnabled(overlayEnv_, deckEnableState())) return;
const FaceLayout fl = faceLayout(rc.right - rc.left, rc.bottom - rc.top);
splineOverlayClick(waveformOverlayArea(fl.bands.waveform), x, y, SplineGesture::kRight,
/*addOnEmptySpace=*/false);
}
ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverCurvePopup(int w, int h, int x,