Merge pS-fa4-knob: radial KNOB primitive (7->5 arc, needle, vertical-drag) in param_slider
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
#include "param_slider.h"
|
#include "param_slider.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
namespace reasampler::vst {
|
namespace reasampler::vst {
|
||||||
|
|
||||||
@@ -78,10 +79,74 @@ double valueAtPoint(const Rect& control, int x) {
|
|||||||
return static_cast<double>(x - track.left) / static_cast<double>(span);
|
return static_cast<double>(x - track.left) / static_cast<double>(span);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Radial knob (Wave A FA4) ---------------------------------------------------------
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr double kPi = 3.14159265358979323846;
|
||||||
|
|
||||||
|
// Normalize an angle in degrees to [0, 360).
|
||||||
|
double normDeg(double deg) {
|
||||||
|
deg = std::fmod(deg, 360.0);
|
||||||
|
if (deg < 0.0) deg += 360.0;
|
||||||
|
// Guard: fmod can return exactly 360.0 on some implementations due to floating-point
|
||||||
|
// rounding; fold it back to 0.
|
||||||
|
if (deg >= 360.0) deg -= 360.0;
|
||||||
|
return deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
double clamp01(double v) { return (std::min)(1.0, (std::max)(0.0, v)); }
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
KnobGeometry computeKnob(const Rect& cell) {
|
||||||
|
if (cell.width() <= 0 || cell.height() <= 0) return KnobGeometry{};
|
||||||
|
KnobGeometry g;
|
||||||
|
g.centerX = (cell.left + cell.right) / 2.0;
|
||||||
|
g.centerY = (cell.top + cell.bottom) / 2.0;
|
||||||
|
g.radius = (std::min)(cell.width(), cell.height()) / 2.0;
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool knobHitTest(const KnobGeometry& knob, int x, int y) {
|
||||||
|
if (knob.radius <= 0.0) return false;
|
||||||
|
const double dx = x - knob.centerX;
|
||||||
|
const double dy = y - knob.centerY;
|
||||||
|
// Boundary exclusive: matches the module's half-open Rect convention.
|
||||||
|
return dx * dx + dy * dy < knob.radius * knob.radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
double knobSweepDeg(const KnobArc& arc) {
|
||||||
|
const double sweep = normDeg(arc.endDeg) - normDeg(arc.startDeg);
|
||||||
|
// An end at-or-behind the start wraps clockwise past 12 o'clock; equal angles mean a
|
||||||
|
// full circle.
|
||||||
|
return sweep <= 0.0 ? sweep + 360.0 : sweep;
|
||||||
|
}
|
||||||
|
|
||||||
|
double knobValueAngleDeg(const KnobArc& arc, double value) {
|
||||||
|
return normDeg(normDeg(arc.startDeg) + knobSweepDeg(arc) * clamp01(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
KnobPoint knobNeedlePoint(const KnobGeometry& knob, const KnobArc& arc, double value) {
|
||||||
|
// Clock angle -> screen direction: 0° points up (-y), 90° points right (+x).
|
||||||
|
const double rad = knobValueAngleDeg(arc, value) * kPi / 180.0;
|
||||||
|
return KnobPoint{knob.centerX + knob.radius * std::sin(rad),
|
||||||
|
knob.centerY - knob.radius * std::cos(rad)};
|
||||||
|
}
|
||||||
|
|
||||||
|
double knobDragValue(double startValue, int dyPixels, int dragRangePixels) {
|
||||||
|
const double start = clamp01(startValue);
|
||||||
|
if (dragRangePixels <= 0) return start;
|
||||||
|
// Screen y grows downward: an upward drag (negative dy) increases the value.
|
||||||
|
return clamp01(start - static_cast<double>(dyPixels) / dragRangePixels);
|
||||||
|
}
|
||||||
|
|
||||||
int controlAtPoint(const std::vector<ControlRow>& rows, int x, int y) {
|
int controlAtPoint(const std::vector<ControlRow>& rows, int x, int y) {
|
||||||
for (const ControlRow& r : rows) {
|
for (const ControlRow& r : rows) {
|
||||||
if (r.kind == ControlKind::Toggle) {
|
if (r.kind == ControlKind::Toggle) {
|
||||||
if (contains(r.control, x, y)) return r.id;
|
if (contains(r.control, x, y)) return r.id;
|
||||||
|
} else if (r.kind == ControlKind::Knob) {
|
||||||
|
if (knobHitTest(computeKnob(r.control), x, y)) return r.id;
|
||||||
} else { // Slider — the interactive area is the track
|
} else { // Slider — the interactive area is the track
|
||||||
if (contains(sliderTrackRect(r.control), x, y)) return r.id;
|
if (contains(sliderTrackRect(r.control), x, y)) return r.id;
|
||||||
}
|
}
|
||||||
|
|||||||
+89
-13
@@ -10,12 +10,13 @@
|
|||||||
// grows a stack of parameter controls: the S15 play-mode toggle (Gate|Trigger), the AHDSR
|
// grows a stack of parameter controls: the S15 play-mode toggle (Gate|Trigger), the AHDSR
|
||||||
// amp-envelope sliders (attack/hold/decay/sustain/release), the Trigger %-length + fade
|
// amp-envelope sliders (attack/hold/decay/sustain/release), the Trigger %-length + fade
|
||||||
// controls, the S16 Varispeed|Preserve engine toggle, and the AD pitch-envelope
|
// controls, the S16 Varispeed|Preserve engine toggle, and the AD pitch-envelope
|
||||||
// enable/attack/decay/depth. They are two shapes only — a two-segment TOGGLE and a
|
// enable/attack/decay/depth. They are three shapes — a two-segment TOGGLE, a horizontal
|
||||||
// horizontal SLIDER — laid out as a vertical stack of fixed-height rows. This module lays out
|
// SLIDER, and (Wave A FA4) a radial KNOB with a needle indicator and vertical-drag value
|
||||||
// that stack and maps a slider's NORMALIZED value (0..1) to/from its handle pixel; the shell
|
// mapping — laid out as a vertical stack of fixed-height rows. This module lays out that
|
||||||
// converts each control's engine value (frames, seconds, a fraction, a signed semitone
|
// stack and maps a control's NORMALIZED value (0..1) to/from its handle pixel / needle
|
||||||
// depth) to/from that 0..1 with its own domain knowledge (this module stays engine-free so it
|
// angle; the shell converts each control's engine value (frames, seconds, a fraction, a
|
||||||
// tests without the audio core).
|
// signed semitone depth) to/from that 0..1 with its own domain knowledge (this module stays
|
||||||
|
// engine-free so it tests without the audio core).
|
||||||
//
|
//
|
||||||
// It reuses editor_geometry's Rect + contains() (one shared geometry idiom).
|
// It reuses editor_geometry's Rect + contains() (one shared geometry idiom).
|
||||||
|
|
||||||
@@ -34,9 +35,11 @@ inline constexpr int kControlLabelWidth = 92; // the label column at the row's
|
|||||||
inline constexpr int kSliderHandleWidth = 8; // the draggable slider handle width (px)
|
inline constexpr int kSliderHandleWidth = 8; // the draggable slider handle width (px)
|
||||||
inline constexpr int kToggleSegments = 2; // a toggle is always two segments
|
inline constexpr int kToggleSegments = 2; // a toggle is always two segments
|
||||||
|
|
||||||
// A control is one of two shapes. Toggle = a two-segment selector (the active segment
|
// A control is one of three shapes. Toggle = a two-segment selector (the active segment
|
||||||
// highlights); Slider = a horizontal track with a draggable handle over a 0..1 value.
|
// highlights); Slider = a horizontal track with a draggable handle over a 0..1 value;
|
||||||
enum class ControlKind { Toggle, Slider };
|
// Knob = a radial dial with a needle indicator over a 0..1 value, dragged VERTICALLY
|
||||||
|
// (up = increase).
|
||||||
|
enum class ControlKind { Toggle, Slider, Knob };
|
||||||
|
|
||||||
// One control the shell places in the panel, in stack order. `id` is the shell's own control
|
// One control the shell places in the panel, in stack order. `id` is the shell's own control
|
||||||
// identifier (an int the shell casts from its ControlId enum) returned by the hit-test so the
|
// identifier (an int the shell casts from its ControlId enum) returned by the hit-test so the
|
||||||
@@ -94,11 +97,84 @@ Rect sliderHandleRect(const Rect& control, double value);
|
|||||||
// shell converts the returned 0..1 into its engine domain (frames/seconds/fraction/semitones).
|
// shell converts the returned 0..1 into its engine domain (frames/seconds/fraction/semitones).
|
||||||
double valueAtPoint(const Rect& control, int x);
|
double valueAtPoint(const Rect& control, int x);
|
||||||
|
|
||||||
|
// --- Radial knob (Wave A FA4) --------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Angle convention: DEGREES CLOCKWISE FROM 12 O'CLOCK, matching a clock face in screen
|
||||||
|
// coordinates (y grows downward): 0 = 12 o'clock (up), 90 = 3 o'clock (right), 180 = 6
|
||||||
|
// o'clock (down), 270 = 9 o'clock (left). The value arc sweeps CLOCKWISE from startDeg
|
||||||
|
// (value 0) to endDeg (value 1); an endDeg at-or-behind startDeg wraps +360, so equal
|
||||||
|
// angles mean a full 360° sweep.
|
||||||
|
//
|
||||||
|
// The DEFAULT arc is the conventional 7→5 o'clock layout: min at 7 o'clock (210°) sweeping
|
||||||
|
// clockwise 300° around to max at 5 o'clock (150°), leaving a symmetric 60° dead arc at the
|
||||||
|
// bottom. The 50% (midpoint) value lands at 12 o'clock (0°/360°) — straight up. The angles
|
||||||
|
// are PARAMETERS, not hardcoded — the shell sets the final sweep when the parallel layout
|
||||||
|
// spec lands.
|
||||||
|
inline constexpr double kKnobArcStartDeg = 210.0; // value 0 — 7 o'clock
|
||||||
|
inline constexpr double kKnobArcEndDeg = 150.0; // value 1 — 5 o'clock (clockwise wrap)
|
||||||
|
|
||||||
|
// Default vertical-drag sensitivity: pixels of upward drag for one full 0->1 sweep.
|
||||||
|
inline constexpr int kKnobDragRangePixels = 128;
|
||||||
|
|
||||||
|
// The configurable value arc of a knob. Defaults to the 7->5 o'clock reading above.
|
||||||
|
struct KnobArc {
|
||||||
|
double startDeg = kKnobArcStartDeg;
|
||||||
|
double endDeg = kKnobArcEndDeg;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A knob's circle within its control cell: center + radius in pixel space (doubles so the
|
||||||
|
// shell rounds once, at draw time). radius == 0 marks a degenerate cell.
|
||||||
|
struct KnobGeometry {
|
||||||
|
double centerX = 0.0;
|
||||||
|
double centerY = 0.0;
|
||||||
|
double radius = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A pixel-space point (the needle endpoint the shell draws to).
|
||||||
|
struct KnobPoint {
|
||||||
|
double x = 0.0;
|
||||||
|
double y = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The knob circle inscribed in `cell`, centered, radius = half the smaller dimension. A
|
||||||
|
// degenerate cell yields radius 0. CONTRACT: the shell MUST pass `row.control` (the full
|
||||||
|
// control column) both when drawing and when hit-testing — `controlAtPoint` always uses
|
||||||
|
// `r.control` as the cell, so the draw cell and hit cell must be the same. If the shell
|
||||||
|
// wants to draw a smaller circle it must center it within `row.control` and accept that the
|
||||||
|
// hit area is the larger column-inscribed circle. Pure.
|
||||||
|
KnobGeometry computeKnob(const Rect& cell);
|
||||||
|
|
||||||
|
// True if (x, y) falls strictly inside the knob circle (boundary exclusive, matching the
|
||||||
|
// module's half-open Rect convention). A degenerate knob (radius <= 0) hits nothing. Pure.
|
||||||
|
bool knobHitTest(const KnobGeometry& knob, int x, int y);
|
||||||
|
|
||||||
|
// The clockwise sweep of `arc` in degrees, in (0, 360]: normalized end - start, wrapping
|
||||||
|
// +360 when the end is at-or-behind the start (default arc -> 300). Pure.
|
||||||
|
double knobSweepDeg(const KnobArc& arc);
|
||||||
|
|
||||||
|
// The needle angle for normalized `value` (clamped to [0,1]): startDeg at 0, endDeg at 1,
|
||||||
|
// linear between, returned normalized to [0, 360). Pure.
|
||||||
|
double knobValueAngleDeg(const KnobArc& arc, double value);
|
||||||
|
|
||||||
|
// The needle endpoint for normalized `value`: the point on the knob circle at the value's
|
||||||
|
// angle, from the center. The shell draws the needle from (centerX, centerY) to this point
|
||||||
|
// (or lerps toward the center for a shorter needle). Pure.
|
||||||
|
KnobPoint knobNeedlePoint(const KnobGeometry& knob, const KnobArc& arc, double value);
|
||||||
|
|
||||||
|
// Map a vertical drag onto a knob value: `startValue` is the value at drag start (clamped),
|
||||||
|
// `dyPixels` the pointer's y displacement in screen coordinates (down = positive). Dragging
|
||||||
|
// UP increases, DOWN decreases; `dragRangePixels` pixels of travel covers the full 0..1
|
||||||
|
// range. Result clamps to [0,1]; a non-positive drag range yields the clamped start value.
|
||||||
|
// Pure — the inverse map for the knob's drag interaction.
|
||||||
|
double knobDragValue(double startValue, int dyPixels,
|
||||||
|
int dragRangePixels = kKnobDragRangePixels);
|
||||||
|
|
||||||
// The control a point lands on, given the laid-out `rows`. Returns the control id (ControlDesc
|
// The control a point lands on, given the laid-out `rows`. Returns the control id (ControlDesc
|
||||||
// id) whose interactive area (a Slider's track, a Toggle's whole control area) contains the
|
// id) whose interactive area (a Slider's track, a Toggle's whole control area, a Knob's
|
||||||
// point, or -1 for a miss (a gap, the label column, or outside every row). The FIRST matching
|
// circle) contains the point, or -1 for a miss (a gap, the label column, or outside every
|
||||||
// row wins (rows never overlap, so at most one matches). Pure — the shell's routing entry
|
// row). The FIRST matching row wins (rows never overlap, so at most one matches). Pure — the
|
||||||
// point: on a hit it reads the value (valueAtPoint / toggleSegmentHitTest) and commits.
|
// shell's routing entry point: on a hit it reads the value (valueAtPoint /
|
||||||
|
// toggleSegmentHitTest / knobDragValue over the ensuing drag) and commits.
|
||||||
int controlAtPoint(const std::vector<ControlRow>& rows, int x, int y);
|
int controlAtPoint(const std::vector<ControlRow>& rows, int x, int y);
|
||||||
|
|
||||||
} // namespace reasampler::vst
|
} // namespace reasampler::vst
|
||||||
|
|||||||
+125
-1
@@ -9,10 +9,16 @@
|
|||||||
// sliderTrackRect insetting a half-handle at each end; sliderHandleRect at value 0/0.5/1 and
|
// sliderTrackRect insetting a half-handle at each end; sliderHandleRect at value 0/0.5/1 and
|
||||||
// out-of-range clamping; valueAtPoint mapping x back to 0..1 (endpoints saturate) as the inverse
|
// out-of-range clamping; valueAtPoint mapping x back to 0..1 (endpoints saturate) as the inverse
|
||||||
// of the handle position; controlAtPoint routing a point to the right control id (toggle whole
|
// of the handle position; controlAtPoint routing a point to the right control id (toggle whole
|
||||||
// area vs slider track) and MISSING in the label column, a row gap, and off-panel.
|
// area vs slider track vs knob circle) and MISSING in the label column, a row gap, and
|
||||||
|
// off-panel. FA4 adds the radial KNOB: computeKnob inscribing the circle in its cell, the
|
||||||
|
// circular hit-test (boundary exclusive), the arc angle<->value mapping (min at startDeg, max at
|
||||||
|
// endDeg, linear midpoint; default = the 7->5 o'clock 300-degree sweep with 50% landing at 12
|
||||||
|
// o'clock), wrap-boundary + un-normalized arc inputs, the needle endpoint on the circle, and
|
||||||
|
// the vertical-drag delta->value map (up = increase) with clamping at 0/1.
|
||||||
|
|
||||||
#include "../src/vst/param_slider.h"
|
#include "../src/vst/param_slider.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -152,6 +158,110 @@ static void testValueAtPointDegenerateTrack() {
|
|||||||
CHECK(approx(valueAtPoint(Rect{0, 0, kSliderHandleWidth - 1, 22}, 5), 0.0));
|
CHECK(approx(valueAtPoint(Rect{0, 0, kSliderHandleWidth - 1, 22}, 5), 0.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- knob (FA4) -----------------------------------------------------------------
|
||||||
|
|
||||||
|
static bool nearWithin(double a, double b, double tol) { return (a - b) < tol && (b - a) < tol; }
|
||||||
|
|
||||||
|
static void testKnobGeometryInscribesCell() {
|
||||||
|
// A 44x44 cell at (100,0): center (122,22), radius 22.
|
||||||
|
const KnobGeometry g = computeKnob(Rect{100, 0, 144, 44});
|
||||||
|
CHECK(approx(g.centerX, 122.0));
|
||||||
|
CHECK(approx(g.centerY, 22.0));
|
||||||
|
CHECK(approx(g.radius, 22.0));
|
||||||
|
// A wide cell inscribes on the smaller (vertical) dimension.
|
||||||
|
const KnobGeometry w = computeKnob(Rect{0, 0, 200, 22});
|
||||||
|
CHECK(approx(w.radius, 11.0));
|
||||||
|
CHECK(approx(w.centerX, 100.0));
|
||||||
|
// Degenerate cells yield radius 0.
|
||||||
|
CHECK(computeKnob(Rect{0, 0, 0, 22}).radius == 0.0);
|
||||||
|
CHECK(computeKnob(Rect{0, 0, 22, 0}).radius == 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testKnobHitTestCircle() {
|
||||||
|
const KnobGeometry g = computeKnob(Rect{100, 0, 144, 44}); // center (122,22), r 22
|
||||||
|
CHECK(knobHitTest(g, 122, 22)); // center — always hits
|
||||||
|
CHECK(!knobHitTest(g, 122 + 22, 22)); // exactly on the boundary — boundary exclusive
|
||||||
|
CHECK(!knobHitTest(g, 122 + 22, 44)); // cell corner: inside the rect, outside the circle
|
||||||
|
CHECK(!knobHitTest(g, 122, 45)); // just below the circle
|
||||||
|
CHECK(knobHitTest(g, 122 + 21, 22)); // one pixel inside the boundary — hits
|
||||||
|
CHECK(!knobHitTest(KnobGeometry{}, 0, 0)); // degenerate knob hits nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testKnobDefaultArcIsSevenToFiveOClock() {
|
||||||
|
const KnobArc arc; // default: 210 (7 o'clock) clockwise to 150 (5 o'clock)
|
||||||
|
CHECK(approx(knobSweepDeg(arc), 300.0));
|
||||||
|
CHECK(approx(knobValueAngleDeg(arc, 0.0), kKnobArcStartDeg)); // min at 7 o'clock (210°)
|
||||||
|
CHECK(approx(knobValueAngleDeg(arc, 1.0), kKnobArcEndDeg)); // max at 5 o'clock (150°)
|
||||||
|
// Midpoint: 210 + 150 = 360 -> normalized to 0 (12 o'clock, straight up).
|
||||||
|
CHECK(approx(knobValueAngleDeg(arc, 0.5), 0.0));
|
||||||
|
// Out-of-range values clamp to the arc ends.
|
||||||
|
CHECK(approx(knobValueAngleDeg(arc, -0.5), kKnobArcStartDeg));
|
||||||
|
CHECK(approx(knobValueAngleDeg(arc, 1.5), kKnobArcEndDeg));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testKnobArcIsParameterized() {
|
||||||
|
// A custom non-wrapping arc: 3 o'clock down to 9 o'clock through 6.
|
||||||
|
const KnobArc arc{90.0, 270.0};
|
||||||
|
CHECK(approx(knobSweepDeg(arc), 180.0));
|
||||||
|
CHECK(approx(knobValueAngleDeg(arc, 0.0), 90.0));
|
||||||
|
CHECK(approx(knobValueAngleDeg(arc, 0.5), 180.0));
|
||||||
|
CHECK(approx(knobValueAngleDeg(arc, 1.0), 270.0));
|
||||||
|
// Equal start/end means a full-circle sweep (end at-or-behind start wraps +360).
|
||||||
|
CHECK(approx(knobSweepDeg(KnobArc{0.0, 0.0}), 360.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testKnobArcWrapBoundary() {
|
||||||
|
// A 1-degree arc starting at 180: end 181, sweep must be 1, NOT 361.
|
||||||
|
const KnobArc tiny{180.0, 181.0};
|
||||||
|
CHECK(approx(knobSweepDeg(tiny), 1.0));
|
||||||
|
// Un-normalized inputs: start -180 (== 180) sweeping to end 120.
|
||||||
|
// normDeg(-180) = 180; normDeg(120) = 120; sweep = 120-180 = -60 <= 0 -> 300.
|
||||||
|
const KnobArc unnorm{-180.0, 120.0};
|
||||||
|
CHECK(approx(knobSweepDeg(unnorm), 300.0));
|
||||||
|
CHECK(approx(knobValueAngleDeg(unnorm, 0.0), 180.0)); // min at 6 o'clock
|
||||||
|
CHECK(approx(knobValueAngleDeg(unnorm, 1.0), 120.0)); // max at 4 o'clock
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testKnobNeedlePointOnCircle() {
|
||||||
|
const KnobGeometry g = computeKnob(Rect{100, 0, 144, 44}); // center (122,22), r 22
|
||||||
|
// Default arc, value 0 -> 7 o'clock -> needle points down-left from center.
|
||||||
|
const KnobPoint p7 = knobNeedlePoint(g, KnobArc{}, 0.0);
|
||||||
|
// 210° clockwise from 12: sin(210°)=-0.5, cos(210°)=-√3/2 -> x = cx - r/2, y = cy + r*√3/2
|
||||||
|
CHECK(nearWithin(p7.x, 122.0 + 22.0 * std::sin(210.0 * 3.14159265358979323846 / 180.0), 1e-6));
|
||||||
|
CHECK(nearWithin(p7.y, 22.0 - 22.0 * std::cos(210.0 * 3.14159265358979323846 / 180.0), 1e-6));
|
||||||
|
// A 12 o'clock needle points straight UP; 3 o'clock points RIGHT.
|
||||||
|
const KnobPoint p12 = knobNeedlePoint(g, KnobArc{0.0, 180.0}, 0.0);
|
||||||
|
CHECK(nearWithin(p12.x, 122.0, 1e-6) && nearWithin(p12.y, 0.0, 1e-6));
|
||||||
|
const KnobPoint p3 = knobNeedlePoint(g, KnobArc{0.0, 180.0}, 0.5);
|
||||||
|
CHECK(nearWithin(p3.x, 144.0, 1e-6) && nearWithin(p3.y, 22.0, 1e-6));
|
||||||
|
// Every needle endpoint sits ON the circle.
|
||||||
|
for (double v : {0.0, 0.25, 0.5, 0.75, 1.0}) {
|
||||||
|
const KnobPoint p = knobNeedlePoint(g, KnobArc{}, v);
|
||||||
|
const double dx = p.x - g.centerX, dy = p.y - g.centerY;
|
||||||
|
CHECK(nearWithin(dx * dx + dy * dy, g.radius * g.radius, 1e-6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testKnobDragUpIncreases() {
|
||||||
|
// Up (negative dy) increases, down decreases, scaled by the drag range.
|
||||||
|
CHECK(approx(knobDragValue(0.5, -32, 128), 0.75));
|
||||||
|
CHECK(approx(knobDragValue(0.5, +32, 128), 0.25));
|
||||||
|
// A full-range upward drag from 0 lands exactly at 1.
|
||||||
|
CHECK(approx(knobDragValue(0.0, -128, 128), 1.0));
|
||||||
|
// Default sensitivity applies when the range is omitted.
|
||||||
|
CHECK(approx(knobDragValue(0.0, -kKnobDragRangePixels), 1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testKnobDragClamps() {
|
||||||
|
CHECK(approx(knobDragValue(0.9, -64, 128), 1.0)); // over-drag up clamps at 1
|
||||||
|
CHECK(approx(knobDragValue(0.1, +64, 128), 0.0)); // over-drag down clamps at 0
|
||||||
|
// The start value itself is clamped before the delta applies.
|
||||||
|
CHECK(approx(knobDragValue(1.5, 0, 128), 1.0));
|
||||||
|
CHECK(approx(knobDragValue(-0.5, 0, 128), 0.0));
|
||||||
|
// A degenerate drag range yields the clamped start value.
|
||||||
|
CHECK(approx(knobDragValue(0.7, -50, 0), 0.7));
|
||||||
|
}
|
||||||
|
|
||||||
// --- controlAtPoint routing ---------------------------------------------------
|
// --- controlAtPoint routing ---------------------------------------------------
|
||||||
|
|
||||||
static void testControlAtPointRoutes() {
|
static void testControlAtPointRoutes() {
|
||||||
@@ -160,6 +270,7 @@ static void testControlAtPointRoutes() {
|
|||||||
{10, ControlKind::Toggle},
|
{10, ControlKind::Toggle},
|
||||||
{20, ControlKind::Slider},
|
{20, ControlKind::Slider},
|
||||||
};
|
};
|
||||||
|
ctl.push_back({30, ControlKind::Knob});
|
||||||
const std::vector<ControlRow> rows = layoutControls(panel, ctl);
|
const std::vector<ControlRow> rows = layoutControls(panel, ctl);
|
||||||
// A point in the toggle's control area routes to the toggle id.
|
// A point in the toggle's control area routes to the toggle id.
|
||||||
const Rect tctl = rows[0].control;
|
const Rect tctl = rows[0].control;
|
||||||
@@ -168,6 +279,11 @@ static void testControlAtPointRoutes() {
|
|||||||
const Rect strack = sliderTrackRect(rows[1].control);
|
const Rect strack = sliderTrackRect(rows[1].control);
|
||||||
CHECK(controlAtPoint(rows, (strack.left + strack.right) / 2,
|
CHECK(controlAtPoint(rows, (strack.left + strack.right) / 2,
|
||||||
(strack.top + strack.bottom) / 2) == 20);
|
(strack.top + strack.bottom) / 2) == 20);
|
||||||
|
// A point at the knob's center routes to the knob id; the control-rect corner (outside
|
||||||
|
// the circle) is a miss.
|
||||||
|
const KnobGeometry kg = computeKnob(rows[2].control);
|
||||||
|
CHECK(controlAtPoint(rows, static_cast<int>(kg.centerX), static_cast<int>(kg.centerY)) == 30);
|
||||||
|
CHECK(controlAtPoint(rows, rows[2].control.left + 1, rows[2].control.top + 1) == -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void testControlAtPointMisses() {
|
static void testControlAtPointMisses() {
|
||||||
@@ -196,6 +312,14 @@ int main() {
|
|||||||
testValueAtPointEndpointsSaturate();
|
testValueAtPointEndpointsSaturate();
|
||||||
testValueAtPointIsHandleInverse();
|
testValueAtPointIsHandleInverse();
|
||||||
testValueAtPointDegenerateTrack();
|
testValueAtPointDegenerateTrack();
|
||||||
|
testKnobGeometryInscribesCell();
|
||||||
|
testKnobHitTestCircle();
|
||||||
|
testKnobDefaultArcIsSevenToFiveOClock();
|
||||||
|
testKnobArcIsParameterized();
|
||||||
|
testKnobArcWrapBoundary();
|
||||||
|
testKnobNeedlePointOnCircle();
|
||||||
|
testKnobDragUpIncreases();
|
||||||
|
testKnobDragClamps();
|
||||||
testControlAtPointRoutes();
|
testControlAtPointRoutes();
|
||||||
testControlAtPointMisses();
|
testControlAtPointMisses();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user