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:
@@ -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
|
||||
Reference in New Issue
Block a user