instrument: spline EGs — hard points on the one shared spline, a drawn contour per envelope beside its staged state, payload v13

This commit is contained in:
2026-07-31 21:33:59 -04:00
parent f115904e4f
commit e44bd42dd9
33 changed files with 1739 additions and 364 deletions
@@ -13,6 +13,7 @@
#include <vector>
#include "core/audio/peaks.h" // computeEnvelope (waveform binning)
#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
#include "shell/instrument/reasampler_processor.h"
@@ -131,11 +132,59 @@ void ReaSamplerEditor::paintWaveform(LICE_IBitmap* bmp, const Rect& band) {
paintEnvelopeOverlay(bmp, overlay, frames);
}
void ReaSamplerEditor::paintSplineOverlay(LICE_IBitmap* bmp, const OverlayArea& waveArea) {
const VelocityCurve::Box box = splineOverlayBox(waveArea);
if (box.width <= 0 || box.height <= 1) return;
const VelocityCurve& curve = splineFor(overlayEnv_);
// One eval per drawn column, through the curve's own pixel maps, so the trace and the
// handles share the coordinate system the hit-test resolves against.
const LICE_pixel line = toLice(roleColor(Role::OverlayTrace));
int prevX = 0, prevY = 0;
for (int px = 0; px <= box.width; ++px) {
const int cx = box.left + px;
const double t = curve.pointFromPixel(box, cx, box.top).velocity;
const int cy = curve.pixelFromPoint(box, {t, curve.eval(t)}).y;
if (px > 0) LICE_Line(bmp, prevX, prevY, cx, cy, line, 1.0f, 0, true);
prevX = cx;
prevY = cy;
}
// Handles carry two independent states on the same mark, so they use two independent
// channels: SIZE is the grab (the staged painter's grammar — a hotter hue reads as lower
// contrast over the lime, see its note), and HOLLOW is hard. The corner itself is the
// primary hard cue, but a corner between two near-collinear segments has none to show.
const LICE_pixel handle = toLice(roleColor(Role::OverlayTrace));
const LICE_pixel core = toLice(roleColor(Role::BgBase));
const std::vector<instrument::engine::VelocityPoint>& pts = curve.points();
for (std::size_t i = 0; i < pts.size(); ++i) {
const auto np = curve.pixelFromPoint(box, pts[i]);
const bool grabbed =
(drag_ == DragKind::kSplineNode && curvePointIndex_ == static_cast<int>(i));
const int r = grabbed ? kEnvHandleGrabbedRadius : kEnvHandleRadius;
const int ir = r - kEnvHandleRingPx;
const int hx = (std::max)(box.left + r,
(std::min)(box.left + box.width - 1 - r, np.x));
const int hy = (std::max)(box.top + r,
(std::min)(box.top + box.height - 1 - r, np.y));
LICE_FillRect(bmp, hx - r, hy - r, 2 * r, 2 * r, handle, 1.0f, 0);
if (pts[i].hard && ir > 0) {
LICE_FillRect(bmp, hx - ir, hy - ir, 2 * ir, 2 * ir, core, 1.0f, 0);
}
}
}
void ReaSamplerEditor::paintEnvelopeOverlay(LICE_IBitmap* bmp, const OverlayArea& waveArea,
std::int64_t frames) {
if (overlayEnv_ == OverlayEnv::kNone) return; // no envelope selected is a resting state
const Rect& area = waveArea.rect;
if (frames <= 0 || area.width <= 0 || area.height <= 0) return;
if (overlayIsSpline()) {
// The contour is a pure function of normalized position, so it needs neither the frame
// count nor the live rate the staged path below resolves its seconds against.
paintSplineOverlay(bmp, waveArea);
return;
}
const double rate = liveSampleRate();
if (rate <= 0.0) return;
const double totalSeconds = static_cast<double>(frames) / rate;