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
+175
View File
@@ -0,0 +1,175 @@
// envelope_overlay.cpp — see envelope_overlay.h. Pure geometry; no host types.
#include "core/instrument/ui/envelope_overlay.h"
#include "core/util/clamp01.h"
#include <algorithm>
namespace reasampler::instrument::ui {
using util::clamp01; // the ONE unit-interval clamp (Q-W1, T4-24)
int timeToX(const Rect& area, double totalSeconds, double t) {
const int w = std::max(0, area.width);
if (w <= 0 || totalSeconds <= 0.0) return area.x;
if (t < 0.0) t = 0.0;
// Linear map, clamped on BOTH sides (FA2 bounds invariant): t past totalSeconds pins to the
// last in-bounds column area.right()-1. Clamp in DOUBLE space BEFORE the integer cast — a huge
// t would overflow a 32-bit long (Windows) and wrap to the WRONG edge — then round.
double px = (t / totalSeconds) * static_cast<double>(w);
if (px > static_cast<double>(w - 1)) px = static_cast<double>(w - 1);
return area.x + static_cast<int>(px + 0.5);
}
int gateTimedWidth(const Rect& area) {
const int w = std::max(0, area.width);
if (w <= 0) return 0;
const int sustainPx =
static_cast<int>(kGateSustainDisplayFraction * static_cast<double>(w) + 0.5);
return std::max(1, w - sustainPx);
}
double gatePxPerSecond(const Rect& area) {
const int timedW = gateTimedWidth(area);
if (timedW <= 0) return 0.0;
// Usable width = timed region minus the four per-segment separation bases and the last
// in-bounds column, floored at 1 px so the scale never degenerates; the domain is the four
// stages end-to-end at their schematic maxima (param-domain scale — sample-length-free).
const double usable =
std::max(1.0, static_cast<double>(timedW - 1 - 4 * kGateNodeSepPx));
return usable / (4.0 * kGateStageMaxSeconds);
}
int levelToY(const Rect& area, double level) {
const int h = std::max(0, area.height);
if (h <= 0) return area.y;
if (level < 0.0) level = 0.0;
if (level > 1.0) level = 1.0;
// Level 1 -> top row, level 0 -> bottom row (bottom-1 under the half-open convention). The
// range spans (h-1) pixels so both endpoints land ON a drawable row.
const int span = h - 1;
const long dy = static_cast<long>((1.0 - level) * static_cast<double>(span) + 0.5);
return area.y + static_cast<int>(dy);
}
namespace {
EnvVertex vtx(EnvNode node, const Rect& area, double totalSeconds, double t, double level) {
EnvVertex v;
v.node = node;
v.x = timeToX(area, totalSeconds, t);
v.y = levelToY(area, level);
v.level = level;
return v;
}
// One Gate vertex from a pixel offset inside the area (the Gate schematic works in px space —
// timed px + the fixed sustain-plateau reserve — not through the plain timeToX map). Clamps x in
// DOUBLE space to the last in-bounds column BEFORE the integer cast (FA2 bounds invariant; a
// huge px would overflow a 32-bit long on Windows and wrap to the WRONG edge).
EnvVertex gateVtx(EnvNode node, const Rect& area, double px, double level) {
const int w = std::max(1, area.width);
if (px < 0.0) px = 0.0;
if (px > static_cast<double>(w - 1)) px = static_cast<double>(w - 1);
EnvVertex v;
v.node = node;
v.x = area.x + static_cast<int>(px + 0.5);
v.y = levelToY(area, level);
v.level = level;
return v;
}
std::vector<EnvVertex> gatePolyline(const AmpEnvelope& env, const Rect& area) {
// Non-negative segment durations (a stored negative would be an upstream bug; clamp defensively).
const double a = std::max(0.0, env.attackSeconds);
const double h = std::max(0.0, env.holdSeconds);
const double d = std::max(0.0, env.decaySeconds);
const double r = std::max(0.0, env.releaseSeconds);
const double sus = clamp01(env.sustainLevel);
// BOUNDED SCHEMATIC (FA2): A/H/D and R map onto the TIMED region (canvas minus the reserved
// sustain-plateau width) at the PARAM-DOMAIN scale — sample-length-free — and every segment
// gets a kGateNodeSepPx base so consecutive nodes never coincide (every node individually
// grabbable at any params, incl. the tier-0 zero-hold/zero-decay defaults). The sustain
// plateau is the fixed reserve between DecayEnd and ReleaseStart.
const int W = std::max(1, area.width);
const double sustainPx = static_cast<double>(W - gateTimedWidth(area));
const double sep = static_cast<double>(kGateNodeSepPx);
const double pps = gatePxPerSecond(area);
double xAttack = sep + a * pps; // AttackEnd
double xHold = xAttack + sep + h * pps; // HoldEnd
double xDecay = xHold + sep + d * pps; // DecayEnd (sustain node)
double xPlateau = xDecay + sustainPx; // ReleaseStart (schematic note-off)
double xRelease = xPlateau + sep + r * pps; // ReleaseEnd
// Right-edge overrun (a stored stage beyond the schematic domain): compress from the RIGHT
// preserving the minimum gaps, so trailing nodes stay individually separated instead of
// piling on the last column. The re-floor pass only bites when the canvas is too narrow to
// hold the minimum gaps at all — then gateVtx's [0, W-1] clamp wins (in-bounds > separation).
const double xMax = static_cast<double>(W - 1);
if (xRelease > xMax) {
xRelease = xMax;
xPlateau = std::min(xPlateau, xRelease - sep);
xDecay = std::min(xDecay, xPlateau - sustainPx);
xHold = std::min(xHold, xDecay - sep);
xAttack = std::min(xAttack, xHold - sep);
xAttack = std::max(xAttack, sep);
xHold = std::max(xHold, xAttack + sep);
xDecay = std::max(xDecay, xHold + sep);
xPlateau = std::max(xPlateau, xDecay + sustainPx);
xRelease = std::max(xRelease, xPlateau + sep);
}
std::vector<EnvVertex> pts;
pts.reserve(6);
pts.push_back(gateVtx(EnvNode::Origin, area, 0.0, 0.0));
pts.push_back(gateVtx(EnvNode::AttackEnd, area, xAttack, 1.0));
pts.push_back(gateVtx(EnvNode::HoldEnd, area, xHold, 1.0));
pts.push_back(gateVtx(EnvNode::DecayEnd, area, xDecay, sus)); // sustain node
pts.push_back(gateVtx(EnvNode::ReleaseStart, area, xPlateau, sus)); // plateau end
pts.push_back(gateVtx(EnvNode::ReleaseEnd, area, xRelease, 0.0));
return pts;
}
std::vector<EnvVertex> triggerPolyline(const AmpEnvelope& env, const Rect& area,
double totalSeconds) {
// The played span is lengthFraction of the whole sample; fades are fractions OF that span.
const double len = clamp01(env.lengthFraction);
double fadeIn = clamp01(env.fadeInFraction);
double fadeOut = clamp01(env.fadeOutFraction);
// Fades cannot overlap: clamp so fadeIn + fadeOut <= 1 (of the played span), mirroring the
// engine's TriggerParams clamp. Trim the LATER fade (fade-out) first, matching the engine.
if (fadeIn + fadeOut > 1.0) fadeOut = std::max(0.0, 1.0 - fadeIn);
const double playSeconds = len * totalSeconds;
const double tFadeInEnd = fadeIn * playSeconds;
const double tFadeOutStart = playSeconds - fadeOut * playSeconds; // where fade-out begins
std::vector<EnvVertex> pts;
pts.reserve(4);
pts.push_back(vtx(EnvNode::Origin, area, totalSeconds, 0.0, 0.0));
pts.push_back(vtx(EnvNode::FadeInEnd, area, totalSeconds, tFadeInEnd, 1.0));
pts.push_back(vtx(EnvNode::FadeOutStart, area, totalSeconds, tFadeOutStart, 1.0)); // unity end
pts.push_back(vtx(EnvNode::LengthEnd, area, totalSeconds, playSeconds, 0.0)); // playEnd
return pts;
}
} // namespace
std::vector<EnvVertex> buildEnvelopePolyline(const AmpEnvelope& env, const Rect& area,
double totalSeconds) {
if (area.width <= 0 || area.height <= 0 || totalSeconds <= 0.0) {
// Degenerate surface: a two-point flat baseline at level 0 so the shell always has a line.
return {vtx(EnvNode::Origin, area, 1.0, 0.0, 0.0),
vtx(EnvNode::ReleaseEnd, area, 1.0, 1.0, 0.0)};
}
// Gate is a param-domain schematic — totalSeconds only gates the degenerate branch above
// (no loaded duration -> baseline); Trigger is PCM-aligned and consumes it.
return env.mode == EnvMode::Gate ? gatePolyline(env, area)
: triggerPolyline(env, area, totalSeconds);
}
} // namespace reasampler::instrument::ui