aedcc6976c
Extracts resolveWaveformClaim (core/instrument/ui/spline_edit) so the shell's node/tab/marker click resolution is unit-tested directly, not just its input geometry; folds the staged-envelope node into it; fixes comment accuracy, a cost regression, and test fidelity issues.
81 lines
4.6 KiB
C++
81 lines
4.6 KiB
C++
// spline_edit.h — the point-editing grammar's CLICK resolution: add/grab/delete/toggle from a
|
|
// single (x, y), plus resolveWaveformClaim, the waveform overlay's cross-affordance arbitration
|
|
// (node vs. crossfade tab vs. marker). Both spline consumers — the velocity-curve popup and the
|
|
// spline EG overlay — route their mouse-down through the click grammar, so the two cannot drift
|
|
// apart. Decision logic only, no host types, no drawing; mirror of envelope_edit otherwise.
|
|
|
|
#pragma once
|
|
|
|
#include <cstdint>
|
|
|
|
#include "core/instrument/engine/velocity_curve.h"
|
|
#include "core/instrument/ui/editor_geometry.h" // Rect / OverlayArea
|
|
|
|
namespace reasampler::instrument::ui {
|
|
|
|
using engine::VelocityCurve;
|
|
|
|
// The gesture, in the pure module's own vocabulary (the shell maps its modifier state onto it).
|
|
enum class SplineGesture { kLeft, kRight, kControlLeft };
|
|
|
|
// What the gesture resolves to. Left-click adds a point in empty space and grabs an existing
|
|
// one; right-click deletes; control-click toggles hard/smooth. Points are smooth by default.
|
|
enum class SplineEditKind { kNone, kGrab, kAdd, kDelete, kToggleHard };
|
|
|
|
struct SplineEdit {
|
|
SplineEditKind kind = SplineEditKind::kNone;
|
|
int index = -1; // the point the action targets; -1 for kAdd (it has none yet) and kNone
|
|
};
|
|
|
|
// Resolves a click at (x, y) over `box` into an edit. The endpoint and point-count rules are
|
|
// NOT re-stated here — kDelete on an endpoint and kAdd at the ceiling are refused by
|
|
// VelocityCurve::deletePoint / addPoint, which the caller applies, so there is exactly one home
|
|
// for each. A click outside the mapping box resolves to kNone unless it lands on a node's pick
|
|
// radius: the drawn inset ring must not ADD (the new point would clamp onto an endpoint's x and
|
|
// stack an undeletable duplicate) but must still be able to grab.
|
|
SplineEdit resolveSplineEdit(const VelocityCurve& curve, const VelocityCurve::Box& box,
|
|
SplineGesture gesture, int x, int y);
|
|
|
|
// The contour's mapping box inside the waveform overlay: the FULL area, so the drawn contour
|
|
// spans the whole sample width 1:1 with its time axis. No inset — unlike the popup's box, which
|
|
// insets to keep endpoint handles clear of the sheet border, this one must stay 1:1 with the
|
|
// waveform beneath it. Takes the overlay (not a lane) — see waveform_view.h's overlay contract.
|
|
VelocityCurve::Box splineOverlayBox(const OverlayArea& area);
|
|
|
|
// Two more rules complete the grammar. Both are enforced in the shell — mouse-tracking / drag
|
|
// state has no home in a pure module — and recorded here as their one home rather than restated
|
|
// at each call site:
|
|
// - DRAG-OFF DELETE: releasing a grabbed node well outside its box deletes it (endpoints exempt,
|
|
// per deletePoint's own refusal) — editor_input.cpp's onMouseUp, shared verbatim by the popup
|
|
// and the overlay.
|
|
// - THE OVERLAY'S OUTSIDE-BOX EXCEPTION: resolveSplineEdit's own outside-box grab/toggle/delete
|
|
// allowance (above) is meant for the popup's inset ring; the overlay narrows it back to
|
|
// strictly in-box, since splineOverlayBox has no inset — editor_input_waveform.cpp's
|
|
// splineOverlayClick.
|
|
|
|
// One arbitration candidate: whether the affordance was hit under the cursor, and its own
|
|
// pick-target area (nominal, per its own module's constants — not the actual clipped pixel
|
|
// count; see resolveWaveformClaim).
|
|
struct WaveformClaim {
|
|
bool hit = false;
|
|
std::int64_t area = 0;
|
|
};
|
|
|
|
// Which affordance a waveform-overlay click claims.
|
|
enum class WaveformClaimant { kNone, kNode, kTab, kMarker };
|
|
|
|
// The overlay's cross-affordance arbitration: a contour node (or, mutually exclusively, a
|
|
// staged envelope's drag node — both feed the same `node` slot), the loop crossfade tab, and a
|
|
// marker's full-height column can all claim the same pixel. Hit gates a candidate out
|
|
// entirely; among the ones that hit, the SMALLEST nominal area wins — the marker column is the
|
|
// odd one out (its target is the whole overlay height), so it only wins where nothing narrower
|
|
// also claims the click. Ties go to whichever is checked first: node, then tab, then marker —
|
|
// no live geometry produces a tie except tab-vs-marker, which the tab correctly wins (see
|
|
// editor_input_waveform.cpp's mouseDownWaveform for the live constants). A control-click has no
|
|
// tab/marker meaning (they answer plain grabs only), so it resolves to the node whenever the
|
|
// node is in the running, regardless of area.
|
|
WaveformClaimant resolveWaveformClaim(const WaveformClaim& node, const WaveformClaim& tab,
|
|
const WaveformClaim& marker, SplineGesture gesture);
|
|
|
|
} // namespace reasampler::instrument::ui
|