Q-W1 pt2: core/shell/app relocation + sub-namespaces; one concrete ui::Rect (LTRB fork retired); slot_map split from bank_book; BankIndex→BankModel; 59/59 green

This commit is contained in:
2026-07-28 20:48:56 -04:00
parent 67a41728f3
commit 847936f813
222 changed files with 2247 additions and 2079 deletions
+108
View File
@@ -0,0 +1,108 @@
// envelope_edit.h — PURE node hit-test + pixel-delta→clamped-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.
//
// TWO SURFACES, ONE MODEL. envelope_overlay owns the params→polyline 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.
//
// 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.
//
// 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.
#pragma once
#include <cstdint>
#include <vector>
#include "core/instrument/ui/editor_geometry.h" // Rect
#include "core/instrument/ui/envelope_overlay.h" // EnvNode, EnvMode, AmpEnvelope, EnvVertex, timeToX/levelToY
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.
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.
struct EnvClampBounds {
double maxAttackSeconds = 4.0; // upper bound of the attack slider
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.
};
// 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.
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).
AmpEnvelope resolveNodeDrag(const AmpEnvelope& grabEnv, EnvNode node, const Rect& area,
double totalSeconds, const EnvClampBounds& bounds,
int dxPixels, int dyPixels);
} // namespace reasampler::instrument::ui