Cut core/instrument/ui comment bloat ~49% (comments only, zero code change)

This commit is contained in:
2026-07-29 20:48:39 -04:00
parent 1f24c4b095
commit ccd9968be1
21 changed files with 539 additions and 1043 deletions
+35 -72
View File
@@ -1,41 +1,18 @@
// envelope_edit.h — PURE node hit-test + pixel-deltaclamped-param inverse map for the S-VIEW-3
// draggable envelope nodes. NO VST3, NO REAPER, NO SWELL/LICE types at the boundary. The mirror
// of card_drag / waveform_view: the drag arithmetic + clamp/monotonic constraints live here,
// unit-tested at the boundaries outside the DAW, while the editor shell (reasampler_editor.cpp)
// draws the handles, captures the grab on WM_LBUTTONDOWN, feeds each move's pixel delta back
// through here, and commits the resulting params to the zone through the same off-audio-thread
// path a slider edit uses.
// envelope_edit.h — node hit-test + pixel-delta -> clamped-param inverse map for the draggable
// envelope nodes. Mirror of card_drag/waveform_view: drag arithmetic lives here, unit-tested
// outside the DAW; the shell draws handles, captures the grab, and feeds pixel deltas back in.
//
// TWO SURFACES, ONE MODEL. envelope_overlay owns the paramspolyline FORWARD map (draw); this
// module owns the pixel→params INVERSE map (edit) + node hit-test. Both read/write the SAME
// AmpEnvelope fields (the shell re-reads the zone every paint — no listener chain), so a node
// drag and a slider edit are two views on one source of truth and can never diverge.
// envelope_overlay owns the params->polyline forward (draw) map; this module owns the inverse
// (edit) map + hit-test. Both read/write the same AmpEnvelope fields (shell re-reads the zone
// every paint), so a node drag and a slider edit are two views on one source of truth.
//
// THE INVARIANT (S-VIEW-F2). A drag can NEVER produce a param a slider couldn't:
// * MONOTONIC IN TIME — a node clamps between its time predecessor and successor, so attack-end
// can't pass hold-end, decay can't pass release, etc. Each segment stays >= 0.
// * RANGE-CLAMPED — times clamp to the SAME per-param [min,max] the slider enforces; levels
// clamp to [0,1]. Because the concrete second/fraction maxima live SHELL-SIDE (param_slider
// is deliberately engine-free — the shell owns the 0..1↔domain mapping), the clamp bounds are
// CALLER-SUPPLIED here (EnvClampBounds): the shell passes the same maxima it feeds the slider,
// so the two surfaces share one clamp by construction.
// A drag can never produce a param a slider couldn't: nodes are monotonic in time (clamped
// between time predecessor/successor) and range-clamped to the same per-param [min,max] the
// slider uses (EnvClampBounds, caller-supplied since those maxima live shell-side).
//
// WHICH AXES. Time-only nodes (AttackEnd, HoldEnd, ReleaseEnd; FadeInEnd, FadeOutStart,
// LengthEnd) drag on X only. The sustain node (DecayEnd) drags on BOTH axes — its X sets the
// decay time, its Y sets the sustain level (the standard ADSR-editor grammar). Origin and the
// drawing-only ReleaseStart vertex are NOT draggable.
//
// GATE DRAG SCALE (FA2). Gate time nodes convert px->seconds via the reciprocal of the
// schematic's PARAM-DOMAIN scale (envelope_overlay's gatePxPerSecond — sample-length-free), so
// a dragged handle tracks the cursor exactly 1:1 for stages within the schematic domain (each
// node's x is affine in its own segment duration). Trigger nodes keep the full-canvas
// PCM-aligned scale. Both match the forward map in envelope_overlay. A node is only editable in
// its OWN mode: Gate nodes ignore drags while the envelope is in Trigger mode and vice versa
// (guards the degenerate baseline's cross-mode ReleaseEnd vertex from writing releaseSeconds).
//
// Reuses editor_geometry's Rect + the EnvNode / AmpEnvelope / EnvMode types from
// envelope_overlay (one shared node vocabulary across draw + edit), and the shared timeToX /
// levelToY maps so the handle the overlay drew and the grab region here agree pixel-for-pixel.
// Time-only nodes drag on X; DecayEnd (the sustain node) drags on both axes (X = decay time,
// Y = sustain level). Origin and the drawing-only ReleaseStart are not draggable. A node is only
// editable in its own mode (Gate nodes ignore drags in Trigger mode and vice versa).
#pragma once
@@ -47,60 +24,46 @@
namespace reasampler::instrument::ui {
// The pick radius (px) around a node's drawn point: a grab within this many pixels (in BOTH x and
// y) of a node handle grabs it. Mirrors waveform_view's kMarkerGrabWidth — wide enough to grab a
// small handle comfortably, narrow enough that adjacent nodes stay distinguishable.
// Pick radius (px) around a node's drawn point, in both x and y. Mirrors waveform_view's
// kMarkerGrabWidth.
inline constexpr int kNodeGrabRadius = 6;
// The per-param clamp bounds the shell supplies (the SAME maxima its sliders map 0..1 onto). All
// are upper bounds in the param's own domain; the lower bound is 0 (each stage >= 0), and the
// monotonic-in-time constraint tightens these further at edit time. Defaults are conservative
// placeholders; the shell OVERRIDES them with its live slider domain so the clamp matches exactly.
// Per-param clamp bounds the shell supplies the same maxima its sliders map [0,1] onto.
// Lower bound is always 0; the monotonic-in-time constraint tightens further at edit time.
// Defaults are placeholders; the shell overrides with its live slider domain.
struct EnvClampBounds {
double maxAttackSeconds = 4.0; // upper bound of the attack slider
double maxAttackSeconds = 4.0;
double maxHoldSeconds = 4.0;
double maxDecaySeconds = 4.0;
double maxReleaseSeconds = 4.0;
// Trigger fades + length are fractions; their natural upper bound is 1.0. Exposed so a shell
// that caps a fade below the full span (e.g. 0.5) shares that cap with its slider.
double maxFadeInFraction = 1.0;
double maxFadeOutFraction = 1.0;
double maxLengthFraction = 1.0;
// sustainLevel is always [0,1] — no shell knob needed, kept implicit.
// sustainLevel is always [0,1] — no shell knob needed.
};
// Which node a grab at (x, y) lands on, given the CURRENT envelope + overlay rect + sample
// duration (the same inputs buildEnvelopePolyline drew from, so the grab tests the drawn handles).
// Returns EnvNode::Origin's NON-membership as a miss via the bool return: `hit` is false for a
// point off every DRAGGABLE node. Origin and ReleaseStart are never returned (not draggable),
// and a node from the OTHER mode is never returned (the degenerate baseline's ReleaseEnd vertex
// is not grabbable in Trigger mode). The NEAREST node within the radius wins (Chebyshev
// distance); an exact tie goes to the earlier draw-order node (FA2 — deterministic). Gate nodes
// never coincide (the forward map enforces kGateNodeSepPx separation, so every Gate handle is
// individually grabbable in every state); the tie-break matters only for Trigger's zero-fade-out
// coincidence, where FadeOutStart overlays LengthEnd, wins the tie, and can be dragged inward
// from the right edge. Pure.
// 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;
// Origin/ReleaseStart and nodes from the other mode never hit. Nearest node within the radius
// wins (Chebyshev distance); an exact tie goes to the earlier draw-order node — this only matters
// for Trigger's zero-fade-out coincidence (FadeOutStart overlaps LengthEnd and wins, so the fade
// can be dragged open from zero). Gate nodes never coincide (forward map enforces
// kGateNodeSepPx), so every Gate handle is independently grabbable.
struct NodeHit {
bool hit = false;
EnvNode node = EnvNode::Origin; // meaningful only when hit == true
};
NodeHit nodeAtPoint(const AmpEnvelope& env, const Rect& area, double totalSeconds, int x, int y);
// Resolve a drag of `node` to a new AmpEnvelope. Given the envelope AS OF GRAB TIME (`grabEnv` —
// the shell snapshots it on WM_LBUTTONDOWN so the delta is absolute, not accumulated), the overlay
// rect + sample duration (the pixel↔param maps), the caller's clamp bounds, and the pixel delta
// since grab (`dxPixels`, `dyPixels`), returns the envelope the node should now describe:
// * X delta -> the node's TIME param, shifted proportionally (same linear map as timeToX),
// clamped to [0, per-param max] AND to its monotonic-in-time neighbours (>= predecessor time,
// <= successor time). For a cumulative-time node the shift lands on that node's OWN segment
// duration (e.g. dragging HoldEnd changes holdSeconds, not attack).
// * Y delta -> the LEVEL param, but ONLY for the sustain node (DecayEnd); clamped to [0,1].
// dyPixels is IGNORED for every time-only node.
// * Non-draggable node (Origin / ReleaseStart), a node from the OTHER mode (a Gate node while
// grabEnv.mode is Trigger, or vice versa), a zero-width/zero-height area, or
// totalSeconds <= 0 -> `grabEnv` returned unchanged (no motion).
// Only the dragged node's param(s) change; every other field carries through from `grabEnv`. Pure
// — rounding is to the param's continuous value (no snapping, matching the sliders' resolution).
// Resolves a drag of `node` to a new AmpEnvelope. `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, shifted via the same linear map as timeToX, clamped to
// [0, per-param max] and to its monotonic-in-time neighbours.
// * Y delta -> the level param, only for DecayEnd; clamped to [0,1]. Ignored for time-only nodes.
// * A non-draggable node, an other-mode node, a zero-size area, or totalSeconds <= 0 returns
// `grabEnv` unchanged.
// Only the dragged node's param(s) change. Pure.
AmpEnvelope resolveNodeDrag(const AmpEnvelope& grabEnv, EnvNode node, const Rect& area,
double totalSeconds, const EnvClampBounds& bounds,
int dxPixels, int dyPixels);