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:
@@ -62,9 +62,18 @@ VelocityCurve VelocityCurve::zero() {
|
||||
return c;
|
||||
}
|
||||
|
||||
VelocityCurve VelocityCurve::rampDown() {
|
||||
VelocityCurve c;
|
||||
c.points_ = {{kVelMin, kCurveYMax, false}, {kVelMax, 0.0, false}};
|
||||
return c;
|
||||
}
|
||||
|
||||
VelocityCurve VelocityCurve::fromPoints(std::vector<VelocityPoint> pts, CurveDomain domain) {
|
||||
// Stable sort so coincident-X points keep their wire order (eval stays well-defined for
|
||||
// duplicate-X knots).
|
||||
// Trim before the endpoint synthesis below can add up to two more, then again after, so a
|
||||
// corrupt over-long blob lands at exactly the ceiling with its two endpoints intact.
|
||||
if (pts.size() > kMaxCurvePoints) pts.resize(kMaxCurvePoints);
|
||||
for (VelocityPoint& p : pts) {
|
||||
p.velocity = clampVelocity(p.velocity);
|
||||
p.value = clampValue(p.value, domain);
|
||||
@@ -77,42 +86,34 @@ VelocityCurve VelocityCurve::fromPoints(std::vector<VelocityPoint> pts, CurveDom
|
||||
return domain == CurveDomain::Bipolar ? zero() : flat();
|
||||
}
|
||||
if (pts.front().velocity > kVelMin) {
|
||||
pts.insert(pts.begin(), VelocityPoint{kVelMin, pts.front().value});
|
||||
pts.insert(pts.begin(), VelocityPoint{kVelMin, pts.front().value, pts.front().hard});
|
||||
} else {
|
||||
pts.front().velocity = kVelMin;
|
||||
}
|
||||
if (pts.back().velocity < kVelMax) {
|
||||
pts.push_back(VelocityPoint{kVelMax, pts.back().value});
|
||||
pts.push_back(VelocityPoint{kVelMax, pts.back().value, pts.back().hard});
|
||||
} else {
|
||||
pts.back().velocity = kVelMax;
|
||||
}
|
||||
if (pts.size() > kMaxCurvePoints) {
|
||||
// Drop the interior points nearest the end, never an endpoint.
|
||||
pts.erase(pts.begin() + static_cast<std::ptrdiff_t>(kMaxCurvePoints) - 1,
|
||||
pts.end() - 1);
|
||||
}
|
||||
VelocityCurve c;
|
||||
c.domain_ = domain;
|
||||
c.points_ = std::move(pts);
|
||||
return c;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Fritsch-Carlson monotone-cubic tangent: a sign change (or flat) neighbour is a local extremum,
|
||||
// so the tangent pins to 0 to avoid overshoot; otherwise the weighted-harmonic-mean tangent,
|
||||
// which for collinear knots (dPrev==dNext) reduces exactly to the shared secant — this is what
|
||||
// makes the spline reproduce a straight line for linear()-style input.
|
||||
double fritschCarlsonTangent(double dPrev, double dNext, double spanPrev, double spanNext) {
|
||||
if (dPrev * dNext <= 0.0) return 0.0;
|
||||
const double w1 = 2.0 * spanNext + spanPrev;
|
||||
const double w2 = spanNext + 2.0 * spanPrev;
|
||||
return (w1 + w2) / (w1 / dPrev + w2 / dNext);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
double VelocityCurve::eval(double velocity) const {
|
||||
if (points_.empty()) return curveNeutral(domain_);
|
||||
if (points_.size() == 1) return clampValue(points_[0].value, domain_);
|
||||
const double v = clampVelocity(velocity);
|
||||
if (v <= points_.front().velocity) return clampValue(points_.front().value, domain_);
|
||||
if (v >= points_.back().velocity) return clampValue(points_.back().value, domain_);
|
||||
// Linear walk: this overload is the COLD one (a note-on, a paint column). The per-sample
|
||||
// reader is SplineCursor, which shares the same tangent + Hermite functions.
|
||||
for (std::size_t i = 0; i + 1 < points_.size(); ++i) {
|
||||
const VelocityPoint& a = points_[i];
|
||||
const VelocityPoint& b = points_[i + 1];
|
||||
@@ -120,55 +121,38 @@ double VelocityCurve::eval(double velocity) const {
|
||||
const double span = b.velocity - a.velocity;
|
||||
// Coincident-X neighbours (a step): zero-width segment, no interior to blend.
|
||||
if (span <= 0.0) return clampValue(b.value, domain_);
|
||||
|
||||
// Monotone cubic Hermite (Fritsch-Carlson): provably stays within [a.value, b.value]
|
||||
// between the two knots (no overshoot), reproducing a straight line for collinear input.
|
||||
const double d = (b.value - a.value) / span;
|
||||
|
||||
double mA = d;
|
||||
if (i > 0) {
|
||||
const VelocityPoint& prev = points_[i - 1];
|
||||
const double spanPrev = a.velocity - prev.velocity;
|
||||
if (spanPrev > 0.0) {
|
||||
const double dPrev = (a.value - prev.value) / spanPrev;
|
||||
mA = fritschCarlsonTangent(dPrev, d, spanPrev, span);
|
||||
} else {
|
||||
mA = 0.0;
|
||||
}
|
||||
}
|
||||
double mB = d;
|
||||
if (i + 2 < points_.size()) {
|
||||
const VelocityPoint& next = points_[i + 2];
|
||||
const double spanNext = next.velocity - b.velocity;
|
||||
if (spanNext > 0.0) {
|
||||
const double dNext = (next.value - b.value) / spanNext;
|
||||
mB = fritschCarlsonTangent(d, dNext, span, spanNext);
|
||||
} else {
|
||||
mB = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
const double t = (v - a.velocity) / span;
|
||||
const double t2 = t * t;
|
||||
const double t3 = t2 * t;
|
||||
const double h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
|
||||
const double h10 = t3 - 2.0 * t2 + t;
|
||||
const double h01 = -2.0 * t3 + 3.0 * t2;
|
||||
const double h11 = t3 - t2;
|
||||
const double y = h00 * a.value + h10 * span * mA + h01 * b.value + h11 * span * mB;
|
||||
const SegmentTangents m = segmentTangents(points_.data(), points_.size(), i, d, span);
|
||||
const double y = hermiteAt(a.value, b.value, span, m.mA, m.mB,
|
||||
(v - a.velocity) / span);
|
||||
return clampValue(y, domain_);
|
||||
}
|
||||
}
|
||||
return clampValue(points_.back().value, domain_); // unreachable (v is between the endpoints)
|
||||
}
|
||||
|
||||
std::size_t VelocityCurve::addPoint(double velocity, double value) {
|
||||
const VelocityPoint p{clampVelocity(velocity), clampValue(value, domain_)};
|
||||
int VelocityCurve::addPoint(double velocity, double value) {
|
||||
// At the ceiling the add is REFUSED outright rather than trading a point away — the existing
|
||||
// contour must come through an over-add bit-identical.
|
||||
if (points_.size() >= kMaxCurvePoints) return -1;
|
||||
const VelocityPoint p{clampVelocity(velocity), clampValue(value, domain_), false};
|
||||
// First index strictly greater, so a duplicate-X point lands immediately after the existing one.
|
||||
std::size_t i = 0;
|
||||
while (i < points_.size() && points_[i].velocity <= p.velocity) ++i;
|
||||
points_.insert(points_.begin() + static_cast<std::ptrdiff_t>(i), p);
|
||||
return i;
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
|
||||
bool VelocityCurve::toggleHard(std::size_t index) {
|
||||
if (index >= points_.size()) return false;
|
||||
points_[index].hard = !points_[index].hard;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VelocityCurve::setHard(std::size_t index, bool hard) {
|
||||
if (index >= points_.size()) return false;
|
||||
points_[index].hard = hard;
|
||||
return true;
|
||||
}
|
||||
|
||||
VelocityPoint VelocityCurve::movePoint(std::size_t index, double velocity, double value) {
|
||||
@@ -187,7 +171,7 @@ VelocityPoint VelocityCurve::movePoint(std::size_t index, double velocity, doubl
|
||||
const double hi = points_[index + 1].velocity;
|
||||
newVel = std::clamp(clampVelocity(velocity), lo, hi);
|
||||
}
|
||||
points_[index] = VelocityPoint{newVel, newValue};
|
||||
points_[index] = VelocityPoint{newVel, newValue, points_[index].hard};
|
||||
return points_[index];
|
||||
}
|
||||
|
||||
@@ -255,6 +239,7 @@ bool VelocityCurve::equals(const VelocityCurve& other, double eps) const {
|
||||
for (std::size_t i = 0; i < points_.size(); ++i) {
|
||||
if (std::fabs(points_[i].velocity - other.points_[i].velocity) > eps) return false;
|
||||
if (std::fabs(points_[i].value - other.points_[i].value) > eps) return false;
|
||||
if (points_[i].hard != other.points_[i].hard) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user