// envelope_edit.h — node hit-test + pixel-delta -> clamped-param inverse map for the draggable // envelope nodes and their mid-segment curve knots. The inverse of envelope_overlay's forward // (draw) map; both read/write the same StageEnvelope fields, so node drag / knot drag / knob // edit are one model — see core/instrument/CLAUDE.md's "envelope overlay" section for why. // Mirror of card_drag/waveform_view: drag arithmetic lives here, unit-tested outside the DAW. // // Time-only nodes drag on X; DecayEnd in an AHDSR drags on both axes (X = decay time, Y = // sustain level); a curve knot drags on Y alone (curve_law.h owns the exponent domain). Origin // is never draggable, and neither is an AHDSR's ReleaseEnd — anchored to the right edge, with // release dragged from ReleaseStart instead. A node is only editable in its own kind. #pragma once #include #include #include "core/instrument/ui/editor_geometry.h" // Rect #include "core/instrument/ui/envelope_overlay.h" // EnvNode, EnvKind, StageEnvelope, EnvVertex namespace reasampler::instrument::ui { // Pick radius (px) around a node's drawn point, in both x and y. Mirrors waveform_view's // kMarkerGrabWidth. inline constexpr int kNodeGrabRadius = 6; // Per-param clamp bounds the shell supplies — the same maxima its knobs map [0,1] onto. // Lower bound is always 0. Defaults are placeholders; the shell overrides with its live domain. struct EnvClampBounds { double maxAttackSeconds = 4.0; double maxHoldSeconds = 4.0; double maxDecaySeconds = 4.0; double maxReleaseSeconds = 4.0; // sustainLevel is always [0,1] and the hold FRACTION is always [0,1] — no shell knob needed. }; // Which node a grab at (x, y) lands on, given the current envelope/rect/duration (the same // inputs buildEnvelopePolyline drew from). `hit` is false for a point off every draggable node. // Nearest node within the radius wins (Chebyshev distance); an exact tie goes to the earlier // draw-order node, and since knots are appended last, a coincident endpoint handle wins over a // knot rather than the drag silently becoming a curve edit. An AHD's DecayEnd is excluded // entirely when it coincides with HoldEnd AND holdFraction == 1.0 — there it cannot move // (resolveNodeDrag's derivative is 0), so it is dropped rather than won by draw order. struct NodeHit { bool hit = false; EnvNode node = EnvNode::Origin; // meaningful only when hit == true }; // Takes the waveform overlay (not a lane) — see waveform_view.h's overlay contract. NodeHit nodeAtPoint(const StageEnvelope& env, const OverlayArea& area, double totalSeconds, int x, int y); // Resolves a drag of `node` to a new StageEnvelope. `grabEnv` is the envelope as of grab time // (the shell snapshots it on button-down so the delta is absolute, not accumulated); // `dxPixels`/`dyPixels` is the pixel delta since grab. // * X delta -> the node's time param, through the same map the forward draw used — the tapered // slot on an AHDSR, 1:1 wall clock on an AHD — clamped to [0, per-param max]. // * Y delta -> the level param (AHDSR DecayEnd's sustain) or, on a knot, the segment's curve // exponent. Ignored for time-only nodes. // * `mods` carries the shared interaction law (param_taper.h): Ctrl scales the pixel delta, // Shift snaps the resolved param to a whole unit of its own category before the clamp. The // shell RE-ANCHORS on every modifier transition, so `mods` is constant across one delta. // * A non-draggable node, an other-kind node, a zero-size area, or totalSeconds <= 0 returns // `grabEnv` unchanged. // Only the dragged node's param(s) change. Pure. StageEnvelope resolveNodeDrag(const StageEnvelope& grabEnv, EnvNode node, const OverlayArea& area, double totalSeconds, const EnvClampBounds& bounds, int dxPixels, int dyPixels, const DragModifiers& mods = {}); } // namespace reasampler::instrument::ui