S-VIEW-10: velocity-curve editor UI — draggable spline box beside the Sample hero + in the Zone panel, all math through the pure velocity_curve module

This commit is contained in:
2026-07-27 15:51:36 -04:00
parent c92d82df3a
commit 0e8b1b72cf
5 changed files with 356 additions and 15 deletions
+69
View File
@@ -274,6 +274,70 @@ static void testFromPointsSubTwoFallsBackToFlat() {
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() {
VelocityCurve orig = VelocityCurve::linear();
orig.addPoint(40.0, 0.2);
@@ -302,6 +366,11 @@ int main() {
testResolveDragDegenerateBoxNoMotion();
testFromPointsSortsClampsAndForcesEndpoints();
testFromPointsSubTwoFallsBackToFlat();
testPixelFromPointMapsCornersAndMidpoint();
testPointFromPixelInvertsAndClamps();
testPixelMapsRoundTripWithinOnePixelQuantum();
testPixelFromPointAgreesWithPointAtPixel();
testPointFromPixelDegenerateBox();
testFromPointsRoundTripsAValidCurve();
if (g_fail == 0) std::printf("velocity_curve: all tests passed\n");