Merge pS-w3-velcurve-ui: velocity-curve editor UI (S-VIEW-10) — draggable spline points in Sample + Zone views
This commit is contained in:
@@ -693,11 +693,22 @@ tresult PLUGIN_API ReaSamplerEditor::onSize(ViewRect* newSize) {
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr int kPad = 8;
|
constexpr int kPad = 8;
|
||||||
|
|
||||||
|
// The S-VIEW-10 velocity-curve editor box: a compact bordered box beside the hero waveform
|
||||||
|
// (amp-over-time beside amp-over-velocity — the two share the drawn-curve-with-handles grammar).
|
||||||
|
// Suppressed gracefully (empty rect) when the window is too narrow for both, mirroring
|
||||||
|
// prune_button's suppression rule. The INSET keeps node handles + the pick radius inside the
|
||||||
|
// border so an endpoint at amp 0/1 stays grabbable.
|
||||||
|
constexpr int kVelCurveBoxW = 168; // the curve box width (incl. border)
|
||||||
|
constexpr int kVelCurveMinHeroW = 240; // hero must keep at least this much width beside the box
|
||||||
|
constexpr int kVelCurveInset = 14; // border -> mapping-box inset: caption band (~12px) + 2px gap
|
||||||
|
constexpr int kCurveDragOffMargin = 24; // release beyond box+margin -> drag-off delete
|
||||||
|
|
||||||
struct SampleBands {
|
struct SampleBands {
|
||||||
Rect title; // top: name + Browse/Zone nav buttons
|
Rect title; // top: name + Browse/Zone nav buttons
|
||||||
Rect navBrowse; // the "Browse" title-band button
|
Rect navBrowse; // the "Browse" title-band button
|
||||||
Rect navZone; // the "Zone" title-band button
|
Rect navZone; // the "Zone" title-band button
|
||||||
Rect hero; // the hero waveform + S-VIEW-3 envelope overlay
|
Rect hero; // the hero waveform + S-VIEW-3 envelope overlay
|
||||||
|
Rect velCurve; // the S-VIEW-10 velocity-curve editor box (empty when suppressed)
|
||||||
Rect cluster; // root strip + preview-trigger + velocity knob + channel toggle
|
Rect cluster; // root strip + preview-trigger + velocity knob + channel toggle
|
||||||
Rect control; // the param control strip (Mode / Pitch / AHDSR|Trigger / AD pitch / keyTrack)
|
Rect control; // the param control strip (Mode / Pitch / AHDSR|Trigger / AD pitch / keyTrack)
|
||||||
};
|
};
|
||||||
@@ -715,7 +726,17 @@ SampleBands computeSampleBands(int w, int h) {
|
|||||||
|
|
||||||
int y = titleH;
|
int y = titleH;
|
||||||
const int heroH = (std::min)(kHeroWaveformHeight, (std::max)(0, h - titleH));
|
const int heroH = (std::min)(kHeroWaveformHeight, (std::max)(0, h - titleH));
|
||||||
b.hero = Rect{kPad, y, w - kPad, y + heroH};
|
const Rect fullHero{kPad, y, w - kPad, y + heroH};
|
||||||
|
// Carve the velocity-curve editor from the hero band's right when there is room for both;
|
||||||
|
// otherwise the hero keeps the full width and the curve editor is suppressed (empty rect).
|
||||||
|
if (fullHero.width() >= kVelCurveBoxW + kPad + kVelCurveMinHeroW) {
|
||||||
|
b.velCurve = Rect{fullHero.right - kVelCurveBoxW, fullHero.top, fullHero.right,
|
||||||
|
fullHero.bottom};
|
||||||
|
b.hero = Rect{fullHero.left, fullHero.top, b.velCurve.left - kPad, fullHero.bottom};
|
||||||
|
} else {
|
||||||
|
b.hero = fullHero;
|
||||||
|
b.velCurve = Rect{};
|
||||||
|
}
|
||||||
y += heroH;
|
y += heroH;
|
||||||
const int clusterH = (std::min)(kClusterHeight, (std::max)(0, h - y));
|
const int clusterH = (std::min)(kClusterHeight, (std::max)(0, h - y));
|
||||||
b.cluster = Rect{0, y, w, y + clusterH};
|
b.cluster = Rect{0, y, w, y + clusterH};
|
||||||
@@ -784,6 +805,36 @@ Rect zonesControlPanel(const Rect& content) {
|
|||||||
content.bottom - 4};
|
content.bottom - 4};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The S-VIEW-10 velocity-curve editor inside the Zone param panel: right-anchored, top-aligned,
|
||||||
|
// with the control rows keeping the panel's left. Suppressed (empty rect) when the panel is too
|
||||||
|
// narrow/short for both — the controls then keep the full panel (the pre-S-VIEW-10 layout).
|
||||||
|
// Both draw + hit-test derive from these two formulas so they never drift.
|
||||||
|
constexpr int kVelCurveBoxH = 140; // the Zone-panel curve box height (Sample uses the hero's)
|
||||||
|
constexpr int kVelCurveMinPanelW = 220; // controls keep at least this much width beside the box
|
||||||
|
Rect zonesCurveBox(const Rect& content) {
|
||||||
|
const Rect panel = zonesControlPanel(content);
|
||||||
|
if (panel.width() < kVelCurveBoxW + kPad + kVelCurveMinPanelW || panel.height() < 80) {
|
||||||
|
return Rect{};
|
||||||
|
}
|
||||||
|
const int boxH = (std::min)(kVelCurveBoxH, panel.height());
|
||||||
|
return Rect{panel.right - kVelCurveBoxW, panel.top, panel.right, panel.top + boxH};
|
||||||
|
}
|
||||||
|
Rect zonesControlsArea(const Rect& content) {
|
||||||
|
const Rect panel = zonesControlPanel(content);
|
||||||
|
const Rect curve = zonesCurveBox(content);
|
||||||
|
if (curve.width() <= 0) return panel;
|
||||||
|
return Rect{panel.left, panel.top, curve.left - kPad, panel.bottom};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The pure-module mapping Box for a drawn curve rect: inset from the border so node handles and
|
||||||
|
// the pick radius stay inside the box. Every consumer (paint, hit-test, add, drag) derives the
|
||||||
|
// Box through this ONE formula, so drawn nodes and grabs can never drift apart.
|
||||||
|
VelocityCurve::Box curveBoxFromRect(const Rect& r) {
|
||||||
|
return VelocityCurve::Box{r.left + kVelCurveInset, r.top + kVelCurveInset,
|
||||||
|
(std::max)(0, r.width() - 2 * kVelCurveInset),
|
||||||
|
(std::max)(0, r.height() - 2 * kVelCurveInset)};
|
||||||
|
}
|
||||||
|
|
||||||
// The S7 mono/stereo toggle (S-VIEW-2: moved here from Browse to the Sample cluster band — it is
|
// The S7 mono/stereo toggle (S-VIEW-2: moved here from Browse to the Sample cluster band — it is
|
||||||
// a per-capture output-mode concern, not a choosing concern). A two-segment control right-anchored
|
// a per-capture output-mode concern, not a choosing concern). A two-segment control right-anchored
|
||||||
// in `area` and vertically centered. Returns {mono-segment, stereo-segment}, each kChanSegW wide,
|
// in `area` and vertically centered. Returns {mono-segment, stereo-segment}, each kChanSegW wide,
|
||||||
@@ -983,6 +1034,10 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) {
|
|||||||
kitTextCentered(bmp, waveArea, "(decoding...)", Font::Label, Role::TextDim);
|
kitTextCentered(bmp, waveArea, "(decoding...)", Font::Label, Role::TextDim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// S-VIEW-10: the velocity->amp transfer-curve editor beside the hero (amp-over-time beside
|
||||||
|
// amp-over-velocity). Independent of the decoded PCM — drawn even while (decoding...).
|
||||||
|
paintVelocityCurve(bmp, bands.velCurve, zone);
|
||||||
|
|
||||||
// --- Root + preview cluster (fenced root strip, preview button, velocity knob, channel) ---
|
// --- Root + preview cluster (fenced root strip, preview button, velocity knob, channel) ---
|
||||||
fillSurface(bmp, toKitBox(bands.cluster), Role::BgPanel, InteractionState::Rest);
|
fillSurface(bmp, toKitBox(bands.cluster), Role::BgPanel, InteractionState::Rest);
|
||||||
int root = effectiveRoot();
|
int root = effectiveRoot();
|
||||||
@@ -1070,6 +1125,114 @@ void ReaSamplerEditor::paintEnvelopeOverlay(LICE_IBitmap* bmp, const Rect& waveA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ReaSamplerEditor::paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r,
|
||||||
|
const PerformanceZone& zone) {
|
||||||
|
if (r.width() <= 0 || r.height() <= 0) return; // suppressed (window too narrow)
|
||||||
|
|
||||||
|
// The bordered box: a panel surface + hairline border, drawn by palette role.
|
||||||
|
fillSurface(bmp, toKitBox(r), Role::BgPanel, InteractionState::Rest);
|
||||||
|
LICE_DrawRect(bmp, r.left, r.top, r.width() - 1, r.height() - 1,
|
||||||
|
toLice(roleColor(Role::LineHairline)), 1.0f, 0);
|
||||||
|
// A corner caption (decorative — the axes are velocity -> amp).
|
||||||
|
kitText(bmp, Rect{r.left + 4, r.top + 1, r.right - 4, r.top + 12}, "Vel curve",
|
||||||
|
Font::Micro, Role::TextDim);
|
||||||
|
|
||||||
|
const VelocityCurve::Box box = curveBoxFromRect(r);
|
||||||
|
if (box.width <= 0 || box.height <= 1) return;
|
||||||
|
const VelocityCurve& curve = zone.velocityCurve;
|
||||||
|
|
||||||
|
// Trace the monotone spline — ONE eval per x column over the mapping box, in the categorical
|
||||||
|
// secondary accent (the same grammar as the envelope trace over the hero). The x -> velocity
|
||||||
|
// and amp -> y mappings both go through the pure module so the trace, the node handles, and
|
||||||
|
// the hit-test all share one coordinate system.
|
||||||
|
const LICE_pixel line = toLice(roleColor(Role::AccentSecondary));
|
||||||
|
int prevX = 0, prevY = 0;
|
||||||
|
for (int px = 0; px <= box.width; ++px) {
|
||||||
|
const int cx = box.left + px;
|
||||||
|
const double vel = VelocityCurve::pointFromPixel(box, cx, box.top).velocity;
|
||||||
|
const int cy = VelocityCurve::pixelFromPoint(box, {vel, curve.eval(vel)}).y;
|
||||||
|
if (px > 0) LICE_Line(bmp, prevX, prevY, cx, cy, line, 1.0f, 0, true);
|
||||||
|
prevX = cx;
|
||||||
|
prevY = cy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draggable node handles (mirror of the envelope overlay's): accent-primary squares lifted
|
||||||
|
// to accent-hot when grabbed or hovered, or warn when a drag-off delete is armed (cursor
|
||||||
|
// has passed kCurveDragOffMargin outside the box — release will delete the node).
|
||||||
|
const LICE_pixel handle = toLice(roleColor(Role::AccentPrimary));
|
||||||
|
const LICE_pixel handleHot = toLice(roleColor(Role::AccentHot));
|
||||||
|
const LICE_pixel handleWarn = toLice(roleColor(Role::Warn));
|
||||||
|
// Drag-off check: during a kCurveNode drag on THIS box, is the live cursor beyond the margin?
|
||||||
|
const bool dragOffArmed = (drag_ == DragKind::kCurveNode && dragCurveRect_.left == r.left &&
|
||||||
|
dragCurveRect_.top == r.top) &&
|
||||||
|
(dragCurX_ < r.left - kCurveDragOffMargin ||
|
||||||
|
dragCurX_ > r.right + kCurveDragOffMargin ||
|
||||||
|
dragCurY_ < r.top - kCurveDragOffMargin ||
|
||||||
|
dragCurY_ > r.bottom + kCurveDragOffMargin);
|
||||||
|
for (std::size_t i = 0; i < curve.points().size(); ++i) {
|
||||||
|
const auto np = VelocityCurve::pixelFromPoint(box, curve.points()[i]);
|
||||||
|
const bool grabbed = (drag_ == DragKind::kCurveNode &&
|
||||||
|
curvePointIndex_ == static_cast<int>(i));
|
||||||
|
const bool hot = grabbed || isHovered(HoverKind::kCurveNode, static_cast<int>(i));
|
||||||
|
// A grabbed node in drag-off territory draws warn to signal "release will delete."
|
||||||
|
const LICE_pixel col = (grabbed && dragOffArmed) ? handleWarn
|
||||||
|
: (hot ? handleHot : handle);
|
||||||
|
const int nr = 3;
|
||||||
|
LICE_FillRect(bmp, np.x - nr, np.y - nr, 2 * nr, 2 * nr, col, 1.0f, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReaSamplerEditor::handleCurveMouseDown(const Rect& r, int zoneIndex, int x, int y) {
|
||||||
|
if (zoneIndex < 0 || zoneIndex >= static_cast<int>(map_.zones.size())) return;
|
||||||
|
const VelocityCurve::Box box = curveBoxFromRect(r);
|
||||||
|
if (box.width <= 0 || box.height <= 1) return;
|
||||||
|
PerformanceZone& z = map_.zones[static_cast<std::size_t>(zoneIndex)];
|
||||||
|
|
||||||
|
int idx = z.velocityCurve.pointAtPixel(box, x, y);
|
||||||
|
|
||||||
|
// Modifier-click (Alt) deletes an interior node — a discrete, final edit committed at once
|
||||||
|
// (deletePoint refuses the two endpoints, so an Alt-click on them is a safe no-op).
|
||||||
|
if (idx >= 0 && (GetKeyState(VK_MENU) & 0x8000) != 0) {
|
||||||
|
if (z.velocityCurve.deletePoint(static_cast<std::size_t>(idx))) {
|
||||||
|
selectedZone_ = zoneIndex;
|
||||||
|
commitAndReload();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot the map BEFORE any mutation so a capture-loss rollback also cancels an in-flight
|
||||||
|
// ADD (mirror of the other map-editing drags' dragStartMap_ contract).
|
||||||
|
dragStartMap_ = map_;
|
||||||
|
|
||||||
|
// Empty-space click inside the MAPPING BOX: add a control point at the cursor via the pure
|
||||||
|
// inverse map, then grab it — the click flows straight into a placing drag. Guard: the caller
|
||||||
|
// gates on contains(r, x, y) (the full border rect), but the 6+px inset ring — including the
|
||||||
|
// caption band — must not add a point; a click there would clamp to velocity 0/127 and
|
||||||
|
// produce an undeletable duplicate stacked on an endpoint. Clicks in the ring may still grab
|
||||||
|
// an existing node (pointAtPixel's pick radius legitimately extends into the ring), which is
|
||||||
|
// handled above; only the add path is box-gated here.
|
||||||
|
if (idx < 0) {
|
||||||
|
const bool inBox = (x >= box.left && x < box.left + box.width &&
|
||||||
|
y >= box.top && y < box.top + box.height);
|
||||||
|
if (inBox) {
|
||||||
|
const VelocityPoint p = VelocityCurve::pointFromPixel(box, x, y);
|
||||||
|
idx = static_cast<int>(z.velocityCurve.addPoint(p.velocity, p.amp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idx < 0) return; // ring click with no node hit — nothing to grab
|
||||||
|
|
||||||
|
drag_ = DragKind::kCurveNode;
|
||||||
|
curvePointIndex_ = idx;
|
||||||
|
dragStartCurve_ = z.velocityCurve; // AFTER the add — resolvePointDrag's absolute-delta base
|
||||||
|
dragCurveRect_ = r;
|
||||||
|
dragCurveZone_ = zoneIndex;
|
||||||
|
dragStartX_ = x;
|
||||||
|
dragStartY_ = y;
|
||||||
|
selectedZone_ = zoneIndex;
|
||||||
|
invalidate(); // live feedback; the commit lands on WM_LBUTTONUP
|
||||||
|
}
|
||||||
|
|
||||||
void ReaSamplerEditor::paintEmptyState(LICE_IBitmap* bmp, const Rect& area) {
|
void ReaSamplerEditor::paintEmptyState(LICE_IBitmap* bmp, const Rect& area) {
|
||||||
// Shown when no card is drawn (nothing to pick): distinguish a genuinely empty bank from
|
// Shown when no card is drawn (nothing to pick): distinguish a genuinely empty bank from
|
||||||
// a bank filter that hides everything. Either way it is the "pick a capture" empty state.
|
// a bank filter that hides everything. Either way it is the "pick a capture" empty state.
|
||||||
@@ -1362,8 +1525,11 @@ void ReaSamplerEditor::paintZone(LICE_IBitmap* bmp, int w, int h) {
|
|||||||
// single-capture face when the map is empty but a capture is picked (S15-F2 lean: the
|
// single-capture face when the map is empty but a capture is picked (S15-F2 lean: the
|
||||||
// single capture is already a one-zone map — one storage site serves both).
|
// single capture is already a one-zone map — one storage site serves both).
|
||||||
if (selectedZone_ >= 0 && selectedZone_ < static_cast<int>(map_.zones.size())) {
|
if (selectedZone_ >= 0 && selectedZone_ < static_cast<int>(map_.zones.size())) {
|
||||||
paintControls(bmp, zonesControlPanel(content),
|
const PerformanceZone& z = map_.zones[static_cast<std::size_t>(selectedZone_)];
|
||||||
map_.zones[static_cast<std::size_t>(selectedZone_)]);
|
paintControls(bmp, zonesControlsArea(content), z);
|
||||||
|
// S-VIEW-10: the same velocity-curve editor, right-anchored in the per-zone param panel
|
||||||
|
// (one curve per zone). Suppressed (empty rect) when the panel is too narrow.
|
||||||
|
paintVelocityCurve(bmp, zonesCurveBox(content), z);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1499,13 +1665,22 @@ void ReaSamplerEditor::resolveHover(int x, int y) {
|
|||||||
} else if (selectedZone_ >= 0 && contains(delR, x, y)) {
|
} else if (selectedZone_ >= 0 && contains(delR, x, y)) {
|
||||||
h = {HoverKind::kDeleteZone, -1};
|
h = {HoverKind::kDeleteZone, -1};
|
||||||
} else if (selectedZone_ >= 0 && selectedZone_ < static_cast<int>(map_.zones.size())) {
|
} else if (selectedZone_ >= 0 && selectedZone_ < static_cast<int>(map_.zones.size())) {
|
||||||
const ZonePlaySeconds& play = map_.zones[static_cast<std::size_t>(selectedZone_)].play;
|
const Rect cb = zonesCurveBox(content);
|
||||||
const Rect panel = zonesControlPanel(content);
|
if (cb.width() > 0 && contains(cb, x, y)) {
|
||||||
|
// S-VIEW-10: a curve node under the pointer lights accent-hot.
|
||||||
|
const PerformanceZone& z = map_.zones[static_cast<std::size_t>(selectedZone_)];
|
||||||
|
const int idx = z.velocityCurve.pointAtPixel(curveBoxFromRect(cb), x, y);
|
||||||
|
if (idx >= 0) h = {HoverKind::kCurveNode, idx};
|
||||||
|
} else {
|
||||||
|
const ZonePlaySeconds& play =
|
||||||
|
map_.zones[static_cast<std::size_t>(selectedZone_)].play;
|
||||||
|
const Rect panel = zonesControlsArea(content);
|
||||||
const std::vector<ControlDesc> descs = controlDescs(play);
|
const std::vector<ControlDesc> descs = controlDescs(play);
|
||||||
const std::vector<ControlRow> rows = layoutControls(panel, descs);
|
const std::vector<ControlRow> rows = layoutControls(panel, descs);
|
||||||
const int id = controlAtPoint(rows, x, y);
|
const int id = controlAtPoint(rows, x, y);
|
||||||
if (id >= 0) h = {HoverKind::kControl, id};
|
if (id >= 0) h = {HoverKind::kControl, id};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else { // Sample view (home)
|
} else { // Sample view (home)
|
||||||
const SampleBands bands = computeSampleBands(w, hgt);
|
const SampleBands bands = computeSampleBands(w, hgt);
|
||||||
if (contains(bands.navBrowse, x, y)) {
|
if (contains(bands.navBrowse, x, y)) {
|
||||||
@@ -1519,7 +1694,13 @@ void ReaSamplerEditor::resolveHover(int x, int y) {
|
|||||||
if (contains(clusterPreviewButton(bands.cluster), x, y)) h = {HoverKind::kPreview, -1};
|
if (contains(clusterPreviewButton(bands.cluster), x, y)) h = {HoverKind::kPreview, -1};
|
||||||
else if (contains(chan.mono, x, y)) h = {HoverKind::kChanMono, -1};
|
else if (contains(chan.mono, x, y)) h = {HoverKind::kChanMono, -1};
|
||||||
else if (contains(chan.stereo, x, y)) h = {HoverKind::kChanStereo, -1};
|
else if (contains(chan.stereo, x, y)) h = {HoverKind::kChanStereo, -1};
|
||||||
else {
|
else if (bands.velCurve.width() > 0 && contains(bands.velCurve, x, y)) {
|
||||||
|
// S-VIEW-10: a curve node under the pointer lights accent-hot.
|
||||||
|
const PerformanceZone zone = effectiveSampleZone();
|
||||||
|
const int idx = zone.velocityCurve.pointAtPixel(
|
||||||
|
curveBoxFromRect(bands.velCurve), x, y);
|
||||||
|
if (idx >= 0) h = {HoverKind::kCurveNode, idx};
|
||||||
|
} else {
|
||||||
const PerformanceZone zone = effectiveSampleZone();
|
const PerformanceZone zone = effectiveSampleZone();
|
||||||
const std::vector<ControlDesc> descs = controlDescs(zone.play);
|
const std::vector<ControlDesc> descs = controlDescs(zone.play);
|
||||||
const std::vector<ControlRow> rows = layoutControls(bands.control, descs);
|
const std::vector<ControlRow> rows = layoutControls(bands.control, descs);
|
||||||
@@ -1669,6 +1850,15 @@ void ReaSamplerEditor::onMouseDown(int x, int y) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// S-VIEW-10: the velocity-curve editor beside the hero. Every in-box click is an edit
|
||||||
|
// (grab / Alt-delete / add-at-cursor), so materialize the one-zone site first (the
|
||||||
|
// mirror of the control strip's ensureSampleZone path).
|
||||||
|
if (bands.velCurve.width() > 0 && contains(bands.velCurve, x, y)) {
|
||||||
|
const int zi = ensureSampleZone();
|
||||||
|
if (zi >= 0) handleCurveMouseDown(bands.velCurve, zi, x, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Hero waveform: envelope nodes (S-VIEW-3) first, then the S11 markers.
|
// Hero waveform: envelope nodes (S-VIEW-3) first, then the S11 markers.
|
||||||
const std::vector<AudioSample>& pcm = monoPcmFor(selectedId_);
|
const std::vector<AudioSample>& pcm = monoPcmFor(selectedId_);
|
||||||
const std::int64_t frames = static_cast<std::int64_t>(pcm.size());
|
const std::int64_t frames = static_cast<std::int64_t>(pcm.size());
|
||||||
@@ -1833,9 +2023,15 @@ void ReaSamplerEditor::onMouseDown(int x, int y) {
|
|||||||
|
|
||||||
// The param panel: a toggle segment flips at once (commit); a slider grab starts a live drag.
|
// The param panel: a toggle segment flips at once (commit); a slider grab starts a live drag.
|
||||||
// Only when a zone is selected (the Zone surface has no single-capture fallback — that lives
|
// Only when a zone is selected (the Zone surface has no single-capture fallback — that lives
|
||||||
// on the Sample face now).
|
// on the Sample face now). The S-VIEW-10 curve box is checked first — it sits inside the
|
||||||
|
// panel's right column (zonesControlsArea is the panel minus that column).
|
||||||
if (selectedZone_ >= 0 && selectedZone_ < static_cast<int>(map_.zones.size())) {
|
if (selectedZone_ >= 0 && selectedZone_ < static_cast<int>(map_.zones.size())) {
|
||||||
handleControlClick(selectedZone_, zonesControlPanel(content), x, y);
|
const Rect cb = zonesCurveBox(content);
|
||||||
|
if (cb.width() > 0 && contains(cb, x, y)) {
|
||||||
|
handleCurveMouseDown(cb, selectedZone_, x, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleControlClick(selectedZone_, zonesControlsArea(content), x, y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1882,6 +2078,8 @@ bool ReaSamplerEditor::handleControlClick(int zoneIndex, const Rect& panel, int
|
|||||||
|
|
||||||
void ReaSamplerEditor::onMouseMove(int x, int y) {
|
void ReaSamplerEditor::onMouseMove(int x, int y) {
|
||||||
if (drag_ == DragKind::kNone) return;
|
if (drag_ == DragKind::kNone) return;
|
||||||
|
dragCurX_ = x; // keep the live cursor position for drag-state draw cues (e.g. drag-off warn)
|
||||||
|
dragCurY_ = y;
|
||||||
RECT cr{};
|
RECT cr{};
|
||||||
GetClientRect(childHwnd_, &cr);
|
GetClientRect(childHwnd_, &cr);
|
||||||
const int w = cr.right - cr.left;
|
const int w = cr.right - cr.left;
|
||||||
@@ -1941,6 +2139,22 @@ void ReaSamplerEditor::onMouseMove(int x, int y) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (drag_ == DragKind::kCurveNode) {
|
||||||
|
// S-VIEW-10: resolve the grabbed control point from the pixel delta through the pure
|
||||||
|
// inverse map (box + neighbour-X + endpoint-pin clamps), against the grab-time curve +
|
||||||
|
// box (absolute delta — the mirror of the envelope-node drag). Live feedback only; the
|
||||||
|
// commit lands on WM_LBUTTONUP.
|
||||||
|
if (dragCurveZone_ < 0 || dragCurveZone_ >= static_cast<int>(map_.zones.size())) return;
|
||||||
|
if (curvePointIndex_ < 0) return;
|
||||||
|
const int dy = y - dragStartY_;
|
||||||
|
map_.zones[static_cast<std::size_t>(dragCurveZone_)].velocityCurve =
|
||||||
|
VelocityCurve::resolvePointDrag(dragStartCurve_,
|
||||||
|
static_cast<std::size_t>(curvePointIndex_),
|
||||||
|
curveBoxFromRect(dragCurveRect_), dx, dy);
|
||||||
|
invalidate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (drag_ == DragKind::kWaveMarker) {
|
if (drag_ == DragKind::kWaveMarker) {
|
||||||
// S11: resolve the grabbed marker's new frame from the pixel delta, zero-crossing-snap
|
// S11: resolve the grabbed marker's new frame from the pixel delta, zero-crossing-snap
|
||||||
// it against the decoded PCM, apply the inter-marker clamps, and write the override live.
|
// it against the decoded PCM, apply the inter-marker clamps, and write the override live.
|
||||||
@@ -2048,7 +2262,7 @@ void ReaSamplerEditor::onMouseMove(int x, int y) {
|
|||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReaSamplerEditor::onMouseUp(int /*x*/, int /*y*/) {
|
void ReaSamplerEditor::onMouseUp(int x, int y) {
|
||||||
// Release a held preview note first (the preview button is a momentary key: note-off on up).
|
// Release a held preview note first (the preview button is a momentary key: note-off on up).
|
||||||
// This runs regardless of drag state — the preview press does not start a drag.
|
// This runs regardless of drag state — the preview press does not start a drag.
|
||||||
if (previewingNote_ >= 0) {
|
if (previewingNote_ >= 0) {
|
||||||
@@ -2059,9 +2273,14 @@ void ReaSamplerEditor::onMouseUp(int /*x*/, int /*y*/) {
|
|||||||
if (drag_ == DragKind::kNone) return;
|
if (drag_ == DragKind::kNone) return;
|
||||||
const DragKind kind = drag_;
|
const DragKind kind = drag_;
|
||||||
const int paramId = dragParamId_;
|
const int paramId = dragParamId_;
|
||||||
|
const int curveIdx = curvePointIndex_;
|
||||||
|
const int curveZone = dragCurveZone_;
|
||||||
|
const Rect curveRect = dragCurveRect_;
|
||||||
drag_ = DragKind::kNone;
|
drag_ = DragKind::kNone;
|
||||||
dragParamId_ = -1;
|
dragParamId_ = -1;
|
||||||
dragParamZone_ = -1;
|
dragParamZone_ = -1;
|
||||||
|
curvePointIndex_ = -1;
|
||||||
|
dragCurveZone_ = -1;
|
||||||
// A scrollbar drag is transient UI (no map change), and the preview-velocity knob (id==-2) is a
|
// A scrollbar drag is transient UI (no map change), and the preview-velocity knob (id==-2) is a
|
||||||
// processor-side per-instance setting already applied live — neither reloads the instrument.
|
// processor-side per-instance setting already applied live — neither reloads the instrument.
|
||||||
// Every other drag is a coherent map edit: publish the in-flight map + reload off-thread.
|
// Every other drag is a coherent map edit: publish the in-flight map + reload off-thread.
|
||||||
@@ -2069,6 +2288,21 @@ void ReaSamplerEditor::onMouseUp(int /*x*/, int /*y*/) {
|
|||||||
invalidate();
|
invalidate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// S-VIEW-10 drag-off delete: releasing a curve-node drag well OUTSIDE the box removes the
|
||||||
|
// dragged point (deletePoint refuses the two endpoints, so an endpoint drag-off is a plain
|
||||||
|
// move — its amp keeps the last clamped drag value).
|
||||||
|
if (kind == DragKind::kCurveNode && curveIdx >= 0 && curveZone >= 0 &&
|
||||||
|
curveZone < static_cast<int>(map_.zones.size())) {
|
||||||
|
const bool off = x < curveRect.left - kCurveDragOffMargin ||
|
||||||
|
x > curveRect.right + kCurveDragOffMargin ||
|
||||||
|
y < curveRect.top - kCurveDragOffMargin ||
|
||||||
|
y > curveRect.bottom + kCurveDragOffMargin;
|
||||||
|
if (off) {
|
||||||
|
map_.zones[static_cast<std::size_t>(curveZone)].velocityCurve.deletePoint(
|
||||||
|
static_cast<std::size_t>(curveIdx));
|
||||||
|
hover_ = HoverTarget{}; // stale kCurveNode index would light a shifted node on next paint
|
||||||
|
}
|
||||||
|
}
|
||||||
commitAndReload();
|
commitAndReload();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2237,6 +2471,8 @@ LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam,
|
|||||||
self->drag_ = DragKind::kNone;
|
self->drag_ = DragKind::kNone;
|
||||||
self->dragParamId_ = -1;
|
self->dragParamId_ = -1;
|
||||||
self->dragParamZone_ = -1;
|
self->dragParamZone_ = -1;
|
||||||
|
self->curvePointIndex_ = -1; // S-VIEW-10 curve-node drag state (peer reset)
|
||||||
|
self->dragCurveZone_ = -1;
|
||||||
self->invalidate();
|
self->invalidate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
#include "param_slider.h" // ControlRow (the S12/S15/S16 control-surface geometry)
|
#include "param_slider.h" // ControlRow (the S12/S15/S16 control-surface geometry)
|
||||||
#include "peaks.h" // Envelope (the cached peak thumbnail)
|
#include "peaks.h" // Envelope (the cached peak thumbnail)
|
||||||
#include "sample_map.h" // SampleChoice, BankChoice, PerformanceMap (the shell's snapshot)
|
#include "sample_map.h" // SampleChoice, BankChoice, PerformanceMap (the shell's snapshot)
|
||||||
|
#include "velocity_curve.h" // VelocityCurve (S-VIEW-10 transfer-curve editor state)
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
@@ -78,9 +79,10 @@ private:
|
|||||||
// single-capture root drag on the setup strip; kWaveMarker is a draggable start/loop
|
// single-capture root drag on the setup strip; kWaveMarker is a draggable start/loop
|
||||||
// marker on the S11 waveform surface (which marker is in waveMarker_); kEnvNode is a
|
// marker on the S11 waveform surface (which marker is in waveMarker_); kEnvNode is a
|
||||||
// draggable envelope breakpoint on the Sample-view hero overlay (S-VIEW-3, which node in
|
// draggable envelope breakpoint on the Sample-view hero overlay (S-VIEW-3, which node in
|
||||||
// envNode_).
|
// envNode_); kCurveNode is a draggable velocity-curve control point in the S-VIEW-10
|
||||||
|
// transfer-curve editor (which point in curvePointIndex_).
|
||||||
enum class DragKind { kNone, kRootMarker, kZoneLow, kZoneHigh, kZoneBody, kWaveMarker,
|
enum class DragKind { kNone, kRootMarker, kZoneLow, kZoneHigh, kZoneBody, kWaveMarker,
|
||||||
kScrollThumb, kParamSlider, kEnvNode };
|
kScrollThumb, kParamSlider, kEnvNode, kCurveNode };
|
||||||
|
|
||||||
// The parameter controls on the setup surface (S12 AHDSR + the S15/S16 control surfaces).
|
// The parameter controls on the setup surface (S12 AHDSR + the S15/S16 control surfaces).
|
||||||
// The int value is the ControlDesc id the pure param_slider hit-test returns; the shell
|
// The int value is the ControlDesc id the pure param_slider hit-test returns; the shell
|
||||||
@@ -133,6 +135,7 @@ private:
|
|||||||
kAddZone, // the "+ Add Zone" button
|
kAddZone, // the "+ Add Zone" button
|
||||||
kDeleteZone, // the "Delete" zone button
|
kDeleteZone, // the "Delete" zone button
|
||||||
kControl, // a param-panel control row (index = ControlDesc id)
|
kControl, // a param-panel control row (index = ControlDesc id)
|
||||||
|
kCurveNode, // a velocity-curve control point (index = point index, S-VIEW-10)
|
||||||
};
|
};
|
||||||
struct HoverTarget {
|
struct HoverTarget {
|
||||||
HoverKind kind = HoverKind::kNone;
|
HoverKind kind = HoverKind::kNone;
|
||||||
@@ -152,6 +155,17 @@ private:
|
|||||||
// `zone`'s play params, at the sample's wall-clock duration. Shared by the Sample hero band.
|
// `zone`'s play params, at the sample's wall-clock duration. Shared by the Sample hero band.
|
||||||
void paintEnvelopeOverlay(LICE_IBitmap* bmp, const Rect& waveArea, const PerformanceZone& zone,
|
void paintEnvelopeOverlay(LICE_IBitmap* bmp, const Rect& waveArea, const PerformanceZone& zone,
|
||||||
std::int64_t frames);
|
std::int64_t frames);
|
||||||
|
// S-VIEW-10: the velocity->amp transfer-curve editor — a bordered box (X = velocity 0-127,
|
||||||
|
// Y = amp 0-1), the monotone spline traced by eval, one draggable node handle per control
|
||||||
|
// point. Shared by the Sample face (beside the hero) and the Zone param panel; all mapping /
|
||||||
|
// hit-test / clamp math lives in the pure velocity_curve module. `r` empty -> draws nothing.
|
||||||
|
void paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r, const PerformanceZone& zone);
|
||||||
|
|
||||||
|
// Route a mouse-down inside curve-editor box `r` editing map_.zones[zoneIndex]: a node grab
|
||||||
|
// starts a kCurveNode drag; Alt-click on an interior node deletes it (committed at once);
|
||||||
|
// an empty-space click ADDS a point at the cursor and grabs it for an immediate drag.
|
||||||
|
// `zoneIndex` must be a valid index into map_.zones (callers materialize first).
|
||||||
|
void handleCurveMouseDown(const Rect& r, int zoneIndex, int x, int y);
|
||||||
|
|
||||||
void onMouseDown(int x, int y);
|
void onMouseDown(int x, int y);
|
||||||
void onMouseMove(int x, int y);
|
void onMouseMove(int x, int y);
|
||||||
@@ -378,6 +392,8 @@ private:
|
|||||||
DragKind drag_ = DragKind::kNone;
|
DragKind drag_ = DragKind::kNone;
|
||||||
int dragStartX_ = 0; // grab x (px), for the pixel-delta resolver
|
int dragStartX_ = 0; // grab x (px), for the pixel-delta resolver
|
||||||
int dragStartY_ = 0; // grab y (px), for the vertical scrollbar-thumb drag
|
int dragStartY_ = 0; // grab y (px), for the vertical scrollbar-thumb drag
|
||||||
|
int dragCurX_ = 0; // live cursor x (px) during a drag — updated in onMouseMove
|
||||||
|
int dragCurY_ = 0; // live cursor y (px) during a drag — updated in onMouseMove
|
||||||
int dragStartLow_ = 0; // the grabbed field's note at grab time
|
int dragStartLow_ = 0; // the grabbed field's note at grab time
|
||||||
int dragStartHigh_ = 0;
|
int dragStartHigh_ = 0;
|
||||||
int dragStartRoot_ = 60;
|
int dragStartRoot_ = 60;
|
||||||
@@ -405,6 +421,15 @@ private:
|
|||||||
EnvNode envNode_ = EnvNode::Origin;
|
EnvNode envNode_ = EnvNode::Origin;
|
||||||
AmpEnvelope dragStartEnv_{};
|
AmpEnvelope dragStartEnv_{};
|
||||||
|
|
||||||
|
// S-VIEW-10 velocity-curve node drag: which point is grabbed, the curve snapshotted at grab
|
||||||
|
// (resolvePointDrag's absolute-delta contract), the box rect the grab happened in (the Sample
|
||||||
|
// and Zone views place the editor differently — the drag resolves against the grab-time box),
|
||||||
|
// and which zone the edit lands on. Mirror of the envelope-node drag state.
|
||||||
|
int curvePointIndex_ = -1;
|
||||||
|
VelocityCurve dragStartCurve_ = VelocityCurve::flat();
|
||||||
|
Rect dragCurveRect_{};
|
||||||
|
int dragCurveZone_ = -1;
|
||||||
|
|
||||||
// --- Peak-thumbnail cache (mirror of bank_panel; id -> envelope at a bin width) ------
|
// --- Peak-thumbnail cache (mirror of bank_panel; id -> envelope at a bin width) ------
|
||||||
// Keyed by "id|binCount" so a resize recomputes at the new width. Cleared on refresh so
|
// Keyed by "id|binCount" so a resize recomputes at the new width. Cleared on refresh so
|
||||||
// a bank edit (a re-captured or deleted sample) does not show a stale thumbnail.
|
// a bank edit (a re-captured or deleted sample) does not show a stale thumbnail.
|
||||||
|
|||||||
@@ -216,6 +216,28 @@ bool VelocityCurve::deletePoint(std::size_t index) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VelocityCurve::CurvePixel VelocityCurve::pixelFromPoint(const Box& box, const VelocityPoint& p) {
|
||||||
|
return CurvePixel{velToX(box, p.velocity), ampToY(box, p.amp)};
|
||||||
|
}
|
||||||
|
|
||||||
|
VelocityPoint VelocityCurve::pointFromPixel(const Box& box, int x, int y) {
|
||||||
|
// The exact inverse of velToX/ampToY (within the one-pixel rounding quantum). Degenerate
|
||||||
|
// dimensions collapse the same way the forward map does: velToX pins to box.left (velocity 0),
|
||||||
|
// ampToY pins to box.top (amp 1).
|
||||||
|
VelocityPoint p;
|
||||||
|
const int w = std::max(0, box.width);
|
||||||
|
const int h = std::max(0, box.height);
|
||||||
|
p.velocity = (w <= 0)
|
||||||
|
? kVelMin
|
||||||
|
: clampVelocity(kVelMin + static_cast<double>(x - box.left) / static_cast<double>(w) *
|
||||||
|
(kVelMax - kVelMin));
|
||||||
|
p.amp = (h <= 1)
|
||||||
|
? kAmpMax
|
||||||
|
: clampAmp(kAmpMax - static_cast<double>(y - box.top) / static_cast<double>(h - 1) *
|
||||||
|
(kAmpMax - kAmpMin));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
int VelocityCurve::pointAtPixel(const Box& box, int x, int y) const {
|
int VelocityCurve::pointAtPixel(const Box& box, int x, int y) const {
|
||||||
for (std::size_t i = 0; i < points_.size(); ++i) {
|
for (std::size_t i = 0; i < points_.size(); ++i) {
|
||||||
const int px = velToX(box, points_[i].velocity);
|
const int px = velToX(box, points_[i].velocity);
|
||||||
|
|||||||
@@ -133,6 +133,22 @@ public:
|
|||||||
// for determinism (mirror of nodeAtPoint).
|
// for determinism (mirror of nodeAtPoint).
|
||||||
int pointAtPixel(const Box& box, int x, int y) const;
|
int pointAtPixel(const Box& box, int x, int y) const;
|
||||||
|
|
||||||
|
// A node's drawn pixel position (S-VIEW-10). The ONE point->pixel mapping — the same mapping
|
||||||
|
// pointAtPixel hit-tests against — exposed so the editor shell draws the trace + node handles
|
||||||
|
// at exactly the coordinates the hit-test expects (draw and grab can never drift).
|
||||||
|
struct CurvePixel {
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
};
|
||||||
|
static CurvePixel pixelFromPoint(const Box& box, const VelocityPoint& p);
|
||||||
|
|
||||||
|
// The absolute pixel -> (velocity, amp) inverse (S-VIEW-10): where an empty-space click lands
|
||||||
|
// as a NEW control point, box-clamped. The exact inverse of pixelFromPoint's mapping (within
|
||||||
|
// the one-pixel quantum), so an added point appears under the cursor. Degenerate box: a
|
||||||
|
// zero-width box reads velocity 0; a height <= 1 box reads amp 1 (the top row), mirroring
|
||||||
|
// pixelFromPoint's degenerate collapse.
|
||||||
|
static VelocityPoint pointFromPixel(const Box& box, int x, int y);
|
||||||
|
|
||||||
// Resolve a drag of point `index` by a pixel delta since grab, given the curve AS OF GRAB TIME
|
// Resolve a drag of point `index` by a pixel delta since grab, given the curve AS OF GRAB TIME
|
||||||
// (`grabCurve` — the shell snapshots it on mouse-down so the delta is absolute) and the box.
|
// (`grabCurve` — the shell snapshots it on mouse-down so the delta is absolute) and the box.
|
||||||
// Maps the pixel delta to a (velocity, amp) delta over the box, then applies movePoint's clamp
|
// Maps the pixel delta to a (velocity, amp) delta over the box, then applies movePoint's clamp
|
||||||
|
|||||||
@@ -274,6 +274,70 @@ static void testFromPointsSubTwoFallsBackToFlat() {
|
|||||||
CHECK(c1.equals(VelocityCurve::flat()));
|
CHECK(c1.equals(VelocityCurve::flat()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- S-VIEW-10 pixel maps (the editor draw/add seam) -----------------------------
|
||||||
|
|
||||||
|
static void testPixelFromPointMapsCornersAndMidpoint() {
|
||||||
|
// Box 100 px wide, 51 px tall at (10, 20). velToX spans the WIDTH (frac * w); ampToY spans
|
||||||
|
// (h - 1) rows with amp 1 at the top — assert the drawn corners land where the module's own
|
||||||
|
// hit-test mapping puts them.
|
||||||
|
const Box box{10, 20, 100, 51};
|
||||||
|
const auto tl = VelocityCurve::pixelFromPoint(box, {0.0, 1.0});
|
||||||
|
CHECK(tl.x == 10 && tl.y == 20);
|
||||||
|
const auto br = VelocityCurve::pixelFromPoint(box, {127.0, 0.0});
|
||||||
|
CHECK(br.x == 110 && br.y == 70);
|
||||||
|
const auto mid = VelocityCurve::pixelFromPoint(box, {63.5, 0.5});
|
||||||
|
CHECK(mid.x == 60 && mid.y == 45);
|
||||||
|
// Out-of-box values are clamped by the mapping (velocity 200 draws at the right edge).
|
||||||
|
const auto clamped = VelocityCurve::pixelFromPoint(box, {200.0, 2.0});
|
||||||
|
CHECK(clamped.x == 110 && clamped.y == 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testPointFromPixelInvertsAndClamps() {
|
||||||
|
const Box box{10, 20, 100, 51};
|
||||||
|
// Exact corners invert exactly.
|
||||||
|
const VelocityPoint tl = VelocityCurve::pointFromPixel(box, 10, 20);
|
||||||
|
CHECK(near(tl.velocity, 0.0) && near(tl.amp, 1.0));
|
||||||
|
const VelocityPoint br = VelocityCurve::pointFromPixel(box, 110, 70);
|
||||||
|
CHECK(near(br.velocity, 127.0) && near(br.amp, 0.0));
|
||||||
|
// A pixel OUTSIDE the box clamps into the domain (never an invariant-violating point).
|
||||||
|
const VelocityPoint out = VelocityCurve::pointFromPixel(box, -50, 500);
|
||||||
|
CHECK(near(out.velocity, 0.0) && near(out.amp, 0.0));
|
||||||
|
const VelocityPoint out2 = VelocityCurve::pointFromPixel(box, 500, -50);
|
||||||
|
CHECK(near(out2.velocity, 127.0) && near(out2.amp, 1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testPixelMapsRoundTripWithinOnePixelQuantum() {
|
||||||
|
// Forward-then-inverse must agree within one pixel's worth of value (the rounding quantum) —
|
||||||
|
// this is what keeps an added point under the cursor and a drawn node grabbable where drawn.
|
||||||
|
const Box box{3, 7, 160, 120};
|
||||||
|
const double velQuantum = 127.0 / 160.0;
|
||||||
|
const double ampQuantum = 1.0 / 119.0;
|
||||||
|
const VelocityPoint pts[] = {{0.0, 1.0}, {127.0, 0.0}, {40.0, 0.25}, {90.5, 0.66}, {63.5, 0.5}};
|
||||||
|
for (const VelocityPoint& p : pts) {
|
||||||
|
const auto px = VelocityCurve::pixelFromPoint(box, p);
|
||||||
|
const VelocityPoint back = VelocityCurve::pointFromPixel(box, px.x, px.y);
|
||||||
|
CHECK(std::fabs(back.velocity - p.velocity) <= velQuantum);
|
||||||
|
CHECK(std::fabs(back.amp - p.amp) <= ampQuantum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testPixelFromPointAgreesWithPointAtPixel() {
|
||||||
|
// A node drawn at pixelFromPoint's coordinates must hit-test back to that same node — the
|
||||||
|
// draw/grab no-drift contract the two helpers exist to guarantee.
|
||||||
|
VelocityCurve c = VelocityCurve::linear();
|
||||||
|
const std::size_t idx = c.addPoint(70.0, 0.3);
|
||||||
|
const Box box{0, 0, 200, 100};
|
||||||
|
const auto px = VelocityCurve::pixelFromPoint(box, c.points()[idx]);
|
||||||
|
CHECK(c.pointAtPixel(box, px.x, px.y) == static_cast<int>(idx));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testPointFromPixelDegenerateBox() {
|
||||||
|
// Zero width -> velocity 0; height <= 1 -> amp 1 (mirrors the forward map's degenerate pins).
|
||||||
|
const Box flat{5, 5, 0, 0};
|
||||||
|
const VelocityPoint p = VelocityCurve::pointFromPixel(flat, 50, 50);
|
||||||
|
CHECK(near(p.velocity, 0.0) && near(p.amp, 1.0));
|
||||||
|
}
|
||||||
|
|
||||||
static void testFromPointsRoundTripsAValidCurve() {
|
static void testFromPointsRoundTripsAValidCurve() {
|
||||||
VelocityCurve orig = VelocityCurve::linear();
|
VelocityCurve orig = VelocityCurve::linear();
|
||||||
orig.addPoint(40.0, 0.2);
|
orig.addPoint(40.0, 0.2);
|
||||||
@@ -302,6 +366,11 @@ int main() {
|
|||||||
testResolveDragDegenerateBoxNoMotion();
|
testResolveDragDegenerateBoxNoMotion();
|
||||||
testFromPointsSortsClampsAndForcesEndpoints();
|
testFromPointsSortsClampsAndForcesEndpoints();
|
||||||
testFromPointsSubTwoFallsBackToFlat();
|
testFromPointsSubTwoFallsBackToFlat();
|
||||||
|
testPixelFromPointMapsCornersAndMidpoint();
|
||||||
|
testPointFromPixelInvertsAndClamps();
|
||||||
|
testPixelMapsRoundTripWithinOnePixelQuantum();
|
||||||
|
testPixelFromPointAgreesWithPointAtPixel();
|
||||||
|
testPointFromPixelDegenerateBox();
|
||||||
testFromPointsRoundTripsAValidCurve();
|
testFromPointsRoundTripsAValidCurve();
|
||||||
|
|
||||||
if (g_fail == 0) std::printf("velocity_curve: all tests passed\n");
|
if (g_fail == 0) std::printf("velocity_curve: all tests passed\n");
|
||||||
|
|||||||
Reference in New Issue
Block a user