Γ-W1-T3: staged contour traces draw the curve their exponent defines

New pure curve_tessellate joins the overlay's node vertices through curveMap,
one sample per pixel column; the knot no longer floats off its own trace.
This commit is contained in:
2026-08-01 18:43:55 -04:00
parent ab3548dced
commit f39fb1b145
7 changed files with 507 additions and 9 deletions
+1
View File
@@ -337,6 +337,7 @@ anything for a trigger shape.
- `spline_edit` — THE point-editing grammar, and the one place it is written down: left-click grabs a node and adds one in empty space, right-click deletes, control-click toggles hard/smooth. Both spline consumers — the velocity-curve popup and the spline EG overlay — route their mouse-down through `resolveSplineEdit`, so the two cannot drift into two grammars. The endpoint and point-count rules are NOT restated here: `deletePoint` and `addPoint` own them, and the caller applies the resolved action to the curve. Also home to `splineOverlayBox`, the contour's mapping box inside the waveform overlay — the FULL area, no inset, so the drawn contour stays 1:1 with the sample's time axis. Spline points are excluded from `param_taper`'s Shift/Ctrl modifier law like waveform markers are: a point is a normalized position with no displayed unit, and control-click there is already claimed by the hard/smooth toggle above.
- `curve_popup` — pure curve-popup geometry + dismissal test (FB1): centered sheet over the Sample face — width/height clamps, title row, Close button rect, curve-box rect, outside-sheet dismissal test. Mirror of `overflow_menu`; no LICE or REAPER types.
- `envelope_overlay` — pure staged-envelope→polyline geometry for the Sample-view overlay (read from `envelope_overlay.h`): maps a `StageEnvelope` to a polyline inside a rect under whichever of TWO layout policies its `EnvKind` selects — an AHDSR draws a bounded param-domain schematic with its release RIGHT-ANCHORED to the canvas edge, an AHD draws 1:1 over the waveform's own time axis — plus a round mid-segment knot on every sloped stage that has a duration. Every vertex clamped in-canvas. Shares the `EnvNode`/`StageEnvelope`/`timeToX`/`levelToY` vocabulary with `envelope_edit` so the drawn handle and its grab region agree pixel-for-pixel. No VST3/REAPER/LICE types at the boundary.
- `curve_tessellate` — the staged envelope's TRACE, split from `envelope_overlay` on the axis those two already have: that module decides where a node LANDS, this strokes the span BETWEEN two of them. Joins the non-knot vertices with the curve each stage's exponent defines, sampled one point per pixel column, at `start + (end - start) * curveMap(phi)` — the composition `envelopes.h`'s four evaluators use, so a drawn stage and the sound it makes cannot diverge. Node vertices keep their exact integer coordinates (the handles are drawn on them); only the interior samples are sub-pixel. A neutral exponent or a zero level span emits the two endpoints and nothing between, which is the straight stroke drawn before curves existed, vertex for vertex.
- `envelope_edit` — pure node hit-test + pixel-delta→clamped-param inverse map for the draggable envelope nodes and their curve knots (read from `envelope_edit.h`): `nodeAtPoint` resolves a grab to the nearest node within a pick radius (Chebyshev distance, draw-order tie-break, knots appended last so a coincident endpoint handle wins); `resolveNodeDrag` maps a pixel delta since grab to a new `StageEnvelope` under the same caller-supplied per-param clamp bounds the knobs use — a drag can never produce a param a knob couldn't. Mirror of `card_drag`/`waveform_view`; the inverse of `envelope_overlay`'s params→polyline forward map, so node-drag, knot-drag and knob-edit read/write one shared model and can never diverge.
## Gotchas
+9
View File
@@ -104,3 +104,12 @@ reasampler_pure_library(param_taper SOURCES param_taper.cpp LINK PUBLIC curve_la
# is judged against the envelope node drag at the editor's own floor width (the sharper of the
# taper's two consumers), read from the allocator/overlay rather than copied as a number.
reasampler_test(param_taper LINK param_taper envelope_overlay sample_bands)
# The staged trace, split from envelope_overlay's vertex model. stroke_aa is the trace-point
# vocabulary, filled in place so the shell's scratch buffer is reused rather than a fresh
# vector returned per paint; curve_law arrives through envelope_overlay but is named here
# because this module evaluates the law rather than merely carrying its exponents.
reasampler_pure_library(curve_tessellate
SOURCES curve_tessellate.cpp
LINK PUBLIC envelope_overlay stroke_aa curve_law)
reasampler_test(curve_tessellate LINK curve_tessellate)
@@ -0,0 +1,65 @@
// curve_tessellate.cpp — see curve_tessellate.h. Pure geometry; no host types.
#include "core/instrument/ui/curve_tessellate.h"
#include <algorithm>
#include <cstdlib>
#include "core/util/curve_law.h"
namespace reasampler::instrument::ui {
using StrokePoint = reasampler::ui::StrokePoint;
namespace {
StrokePoint pt(double x, double y) {
return StrokePoint{static_cast<float>(x), static_cast<float>(y)};
}
// The samples strictly BETWEEN two nodes. The endpoints are the caller's, so a shared node is
// emitted once and the polyline carries no zero-length joint.
void appendInterior(int x0, int y0, int x1, int y1, double exponent,
std::vector<StrokePoint>& out) {
const int span = std::abs(x1 - x0);
if (span < 2 || y1 == y0 || exponent == util::kCurveNeutral) return;
const double dx = static_cast<double>(x1 - x0);
const double dy = static_cast<double>(y1 - y0);
for (int i = 1; i < span; ++i) {
// phi is exact at every column, so x lands on the integer column and the last interior
// sample is one column short of the end node.
const double phi = static_cast<double>(i) / static_cast<double>(span);
out.push_back(pt(static_cast<double>(x0) + dx * phi,
static_cast<double>(y0) + dy * util::curveMap(phi, exponent)));
}
}
} // namespace
double segmentCurve(const StageEnvelope& env, EnvNode endNode) {
switch (endNode) {
case EnvNode::AttackEnd: return env.attackCurve;
case EnvNode::DecayEnd: return env.decayCurve;
case EnvNode::ReleaseEnd: return env.releaseCurve;
default: return util::kCurveNeutral;
}
}
void buildEnvelopeTrace(const std::vector<EnvVertex>& poly, const StageEnvelope& env, int xLo,
int xHi, std::vector<StrokePoint>& out) {
out.clear();
bool started = false;
int px = 0;
int py = 0;
for (const EnvVertex& v : poly) {
if (v.knot) continue;
const int x = std::max(xLo, std::min(xHi, v.x));
if (started) appendInterior(px, py, x, v.y, segmentCurve(env, v.node), out);
out.push_back(pt(x, v.y));
started = true;
px = x;
py = v.y;
}
}
} // namespace reasampler::instrument::ui
+34
View File
@@ -0,0 +1,34 @@
// curve_tessellate.h — the staged envelope's TRACE: envelope_overlay's node vertices joined by
// the curve each stage's exponent defines. Split from that module on the axis the two already
// have — envelope_overlay decides where a node LANDS, this strokes the span BETWEEN two of
// them over phi, so a re-scaled time axis changes nothing here.
#pragma once
#include <vector>
#include "core/instrument/ui/envelope_overlay.h"
#include "core/ui/stroke_aa.h" // StrokePoint — the stroker's own vertex type
namespace reasampler::instrument::ui {
// The exponent governing the segment that ENDS at `node`: attack, decay and release are the
// three sloped stages. Every other node ends a plateau, whose straightness comes from its own
// zero level span rather than from an exponent, so neutral is returned and no caller needs a
// second rule to recognize one.
double segmentCurve(const StageEnvelope& env, EnvNode endNode);
// Replaces `out` with the polyline the shell strokes. Knot vertices are handles, not line
// vertices, and are skipped; node vertices keep their exact INTEGER coordinates (clamped to
// [xLo, xHi]) because those are the positions their draggable handles are drawn at. Only the
// interior samples are sub-pixel, one per pixel column, which is what makes the density follow
// the canvas width instead of a fixed count.
//
// A segment's level runs start + (end - start) * curveMap(phi, exponent) — the composition
// envelopes.h's four evaluators use, so the trace cannot diverge from the sound. A neutral
// exponent or a zero level span emits the two endpoints and nothing between: the straight
// stroke, vertex for vertex.
void buildEnvelopeTrace(const std::vector<EnvVertex>& poly, const StageEnvelope& env, int xLo,
int xHi, std::vector<reasampler::ui::StrokePoint>& out);
} // namespace reasampler::instrument::ui
+1
View File
@@ -91,6 +91,7 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
knob_deck deck_groups deck_values curve_popup spline_edit master_gain sample_usage
bake_hold
file_bytes curve_law stroke_aa
curve_tessellate
bake_plan bake_render bake_reset bake_wire wav_codec)
# SDK_INC gives the REAPER VST3 interfaces + API header for the bridge; WDL_INC gives
# LICE for the editor. The VST3 SDK headers arrive via vst3_sdk PUBLIC.
@@ -13,6 +13,7 @@
#include <vector>
#include "core/audio/peaks.h" // computeEnvelope (waveform binning)
#include "core/instrument/ui/curve_tessellate.h" // buildEnvelopeTrace (the staged trace)
#include "core/instrument/ui/spline_edit.h" // splineOverlayBox (the contour's mapping box)
#include "core/instrument/ui/waveform_view.h" // waveformSurface / laneEnvelope / frameToX
#include "shell/instrument/editor_internal.h" // kit adapters
@@ -212,17 +213,9 @@ void ReaSamplerEditor::paintEnvelopeOverlay(LICE_IBitmap* bmp, const OverlayArea
const StageEnvelope env = packEnvelope(overlayEnv_, params_.play, frames, startFrame);
const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, waveArea, totalSeconds);
// Clip x to the wave rect. Knots are handles, not line vertices. Vertices stay INTEGER here
// — unlike the spline traces above — because they are the same positions the draggable
// handles are drawn at, and a sub-pixel trace would sit off its own handles.
const LICE_pixel line = toLice(roleColor(Role::OverlayTrace));
std::vector<ui::StrokePoint>& trace = scratchPoints();
trace.clear();
for (const EnvVertex& v : poly) {
if (v.knot) continue;
const int vx = (std::max)(area.x, (std::min)(area.right() - 1, v.x));
trace.push_back(ui::StrokePoint{static_cast<float>(vx), static_cast<float>(v.y)});
}
buildEnvelopeTrace(poly, env, area.x, area.right() - 1, trace);
// A degenerate envelope (every stage collapsed to zero span) can reduce this to ONE vertex.
// strokePolylineAA's round-cap zero-length case then draws a dot at it, marking the sole
// point rather than drawing nothing — kept deliberately as more legible than a blank trace.