fix(param_slider): 7->5 o'clock arc (50% straight up), seam contract single-sourced, boundary/normDeg/wrap-boundary fixes

This commit is contained in:
2026-07-27 19:09:51 -04:00
parent 6c0bb4c204
commit cf93d4dc50
3 changed files with 58 additions and 32 deletions
+5 -1
View File
@@ -89,6 +89,9 @@ constexpr double kPi = 3.14159265358979323846;
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;
}
@@ -109,7 +112,8 @@ 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;
return dx * dx + dy * dy <= knob.radius * knob.radius;
// Boundary exclusive: matches the module's half-open Rect convention.
return dx * dx + dy * dy < knob.radius * knob.radius;
}
double knobSweepDeg(const KnobArc& arc) {
+15 -11
View File
@@ -105,17 +105,18 @@ double valueAtPoint(const Rect& control, int x);
// (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 encodes Daniel's "6 to 4 o'clock" spec: min at 6 o'clock (180°) sweeping
// clockwise 300° around to max at 4 o'clock (120°), leaving a minimal 60° dead arc at the
// bottom-right. The angles are PARAMETERS, not hardcoded — the shell sets the final sweep
// when the parallel layout spec lands.
inline constexpr double kKnobArcStartDeg = 180.0; // value 0 — 6 o'clock
inline constexpr double kKnobArcEndDeg = 120.0; // value 14 o'clock (clockwise wrap)
// 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 07 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 6->4 o'clock reading above.
// The configurable value arc of a knob. Defaults to the 7->5 o'clock reading above.
struct KnobArc {
double startDeg = kKnobArcStartDeg;
double endDeg = kKnobArcEndDeg;
@@ -136,12 +137,15 @@ struct KnobPoint {
};
// The knob circle inscribed in `cell`, centered, radius = half the smaller dimension. A
// degenerate cell yields radius 0. The shell passes whatever cell it wants the knob in
// (the whole control column, or a square sub-cell from its own layout). Pure.
// 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 inside the knob circle (boundary inclusive). A degenerate knob
// (radius <= 0) hits nothing. Pure.
// 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