use crate::class::Eps;
use core::f64::consts::TAU;
pub const MAX_SETTLE: f32 = 120.0;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Spring {
duration: f32,
bounce: f32,
}
impl Default for Spring {
fn default() -> Self {
Spring::DEFAULT
}
}
#[derive(Clone, Copy, Debug)]
struct Params {
alpha: f64,
w0: f64,
kind: Kind,
}
#[derive(Clone, Copy, Debug)]
enum Kind {
Under { wd: f64 },
Critical,
Over { s: f64 },
}
impl Spring {
pub const MIN_DURATION: f32 = 0.01;
pub const MAX_DURATION: f32 = 60.0;
pub const MAX_BOUNCE: f32 = 0.95;
pub const DEFAULT: Spring = Spring { duration: 0.3, bounce: 0.0 };
pub const NONE: Spring = Spring { duration: 0.0, bounce: 0.0 };
pub const fn new(duration: f32, bounce: f32) -> Spring {
let duration = if duration.is_nan() {
Self::DEFAULT.duration
} else if duration <= 0.0 {
0.0
} else {
duration.clamp(Self::MIN_DURATION, Self::MAX_DURATION)
};
let bounce = if bounce.is_nan() { 0.0 } else { bounce.clamp(-Self::MAX_BOUNCE, Self::MAX_BOUNCE) };
Spring { duration, bounce }
}
pub const fn from_response(response: f32, damping_ratio: f32) -> Spring {
let zeta = if damping_ratio.is_nan() { 1.0 } else { damping_ratio };
let bounce = if zeta <= 0.0 {
Self::MAX_BOUNCE
} else if zeta <= 1.0 {
1.0 - zeta
} else {
1.0 / zeta - 1.0
};
Spring::new(response, bounce)
}
pub const fn duration(self) -> f32 {
self.duration
}
pub const fn bounce(self) -> f32 {
self.bounce
}
pub const fn is_none(self) -> bool {
self.duration <= 0.0
}
pub fn damping_ratio(self) -> f32 {
zeta(self.bounce) as f32
}
pub fn stiffness(self) -> f32 {
self.params().map_or(0.0, |p| (p.w0 * p.w0) as f32)
}
pub fn damping(self) -> f32 {
self.params().map_or(0.0, |p| (2.0 * p.alpha) as f32)
}
fn params(self) -> Option<Params> {
if self.is_none() {
return None;
}
let w0 = TAU / f64::from(self.duration);
let zeta = zeta(self.bounce);
let alpha = zeta * w0;
let kind = if zeta < 1.0 {
let wd = w0 * (1.0 - zeta * zeta).sqrt();
if wd > 0.0 { Kind::Under { wd } } else { Kind::Critical }
} else if zeta > 1.0 {
let s = w0 * (zeta * zeta - 1.0).sqrt();
if s > 0.0 { Kind::Over { s } } else { Kind::Critical }
} else {
Kind::Critical
};
Some(Params { alpha, w0, kind })
}
pub fn sample(self, x0: f32, v0: f32, target: f32, t: f32) -> (f32, f32) {
let (x0, v0, target) = sane_state(x0, v0, target);
if t.is_nan() || t <= 0.0 {
return (x0, v0);
}
let Some(p) = self.params() else { return (target, 0.0) };
let (x, v) = p.sample(f64::from(x0), f64::from(v0), f64::from(target), f64::from(t));
(finite_f32(x, target), finite_f32(v, 0.0))
}
pub fn settle_time(self, from: f32, v0: f32, target: f32, eps: Eps) -> f32 {
self.settle_time_lanes([(from, v0, target)], eps)
}
pub fn settle_time_lanes(self, lanes: impl IntoIterator<Item = (f32, f32, f32)>, eps: Eps) -> f32 {
let Some(p) = self.params() else { return 0.0 };
let mut bounds = Bounds::default();
for (from, v0, target) in lanes {
let (from, v0, target) = sane_state(from, v0, target);
bounds = bounds.max(p.bounds(f64::from(from) - f64::from(target), f64::from(v0)));
}
let t = p.solve(&bounds, eps.sanitized());
let t = t * (1.0 + 1e-4) + 1e-4;
if t.is_finite() { (t as f32).clamp(0.0, MAX_SETTLE) } else { MAX_SETTLE }
}
}
#[derive(Clone, Copy, Debug, Default)]
struct Bounds {
y_a: f64,
y_b: f64,
v_a: f64,
v_b: f64,
y_e: f64,
y_f: f64,
v_e: f64,
v_f: f64,
}
impl Bounds {
fn max(self, o: Bounds) -> Bounds {
Bounds {
y_a: self.y_a.max(o.y_a),
y_b: self.y_b.max(o.y_b),
v_a: self.v_a.max(o.v_a),
v_b: self.v_b.max(o.v_b),
y_e: self.y_e.max(o.y_e),
y_f: self.y_f.max(o.y_f),
v_e: self.v_e.max(o.v_e),
v_f: self.v_f.max(o.v_f),
}
}
}
impl Params {
fn decayed_c_s(&self, t: f64) -> (f64, f64) {
match self.kind {
Kind::Under { wd } => {
let e = (-self.alpha * t).exp();
let (sin, cos) = (wd * t).sin_cos();
(e * cos, e * sin / wd)
}
Kind::Critical => {
let e = (-self.w0 * t).exp();
(e, e * t)
}
Kind::Over { s } => {
let e1 = ((s - self.alpha) * t).exp();
let e2 = (-(s + self.alpha) * t).exp();
let x = s * t;
let es = if x < 1e-3 {
(-self.alpha * t).exp() * t * (1.0 + x * x / 6.0)
} else {
(e1 - e2) / (2.0 * s)
};
((e1 + e2) * 0.5, es)
}
}
}
fn sample(&self, x0: f64, v0: f64, target: f64, t: f64) -> (f64, f64) {
let y0 = x0 - target;
let (ec, es) = self.decayed_c_s(t);
let y = y0 * ec + (v0 + self.alpha * y0) * es;
let v = v0 * ec - (self.alpha * v0 + self.w0 * self.w0 * y0) * es;
(target + y, v)
}
fn bounds(&self, y0: f64, v0: f64) -> Bounds {
let w2 = self.w0 * self.w0;
let by = v0 + self.alpha * y0;
let bv = self.alpha * v0 + w2 * y0;
let (y_e, y_f, v_e, v_f) = match self.kind {
Kind::Under { wd } => ((y0 * y0 + (by / wd) * (by / wd)).sqrt(), 0.0, (v0 * v0 + (bv / wd) * (bv / wd)).sqrt(), 0.0),
Kind::Critical => (0.0, 0.0, 0.0, 0.0),
Kind::Over { s } => {
let (r1, r2) = (s - self.alpha, -s - self.alpha);
let c2 = (v0 - r1 * y0) / (r2 - r1);
let c1 = y0 - c2;
(c1.abs(), c2.abs(), (r1 * c1).abs(), (r2 * c2).abs())
}
};
Bounds { y_a: y0.abs(), y_b: by.abs(), v_a: v0.abs(), v_b: bv.abs(), y_e, y_f, v_e, v_f }
}
fn solve(&self, b: &Bounds, eps: Eps) -> f64 {
let (ep, ev) = (f64::from(eps.pos), f64::from(eps.vel));
let (ty, tv) = match self.kind {
Kind::Under { .. } => (
linear_exp_root(b.y_a, b.y_b, self.alpha, ep).min(exp_root(b.y_e, self.alpha, ep)),
linear_exp_root(b.v_a, b.v_b, self.alpha, ev).min(exp_root(b.v_e, self.alpha, ev)),
),
Kind::Critical => (linear_exp_root(b.y_a, b.y_b, self.w0, ep), linear_exp_root(b.v_a, b.v_b, self.w0, ev)),
Kind::Over { s } => {
let (slow, fast) = (self.alpha - s, self.alpha + s);
(
linear_exp_root(b.y_a, b.y_b, slow, ep).min(two_exp_root(b.y_e, slow, b.y_f, fast, ep)),
linear_exp_root(b.v_a, b.v_b, slow, ev).min(two_exp_root(b.v_e, slow, b.v_f, fast, ev)),
)
}
};
let t = ty.max(tv);
if t.is_nan() { f64::from(MAX_SETTLE) } else { t.max(0.0) }
}
}
fn zeta(bounce: f32) -> f64 {
let b = f64::from(bounce);
if b >= 0.0 { 1.0 - b } else { 1.0 / (1.0 + b) }
}
fn exp_root(a: f64, r: f64, eps: f64) -> f64 {
if a.is_nan() || a <= eps {
return 0.0;
}
if r.is_nan() || r <= 0.0 || !a.is_finite() {
return f64::INFINITY;
}
(a / eps).ln() / r
}
fn two_exp_root(a: f64, ra: f64, b: f64, rb: f64, eps: f64) -> f64 {
let f = |t: f64| a * (-ra * t).exp() + b * (-rb * t).exp() - eps;
let (mut lo, mut hi) = (exp_root(a, ra, eps), exp_root(a + b, ra, eps));
if !hi.is_finite() || hi <= lo {
return hi;
}
let (mut flo, mut fhi) = (f(lo), f(hi));
if flo <= 0.0 {
return lo;
}
let mut side = 0i8;
for _ in 0..24 {
if flo.is_nan() || fhi.is_nan() || fhi > 0.0 || hi - lo < 1e-6 * (1.0 + hi) {
break;
}
let t = (lo * fhi - hi * flo) / (fhi - flo);
let t = if t > lo && t < hi { t } else { (lo + hi) * 0.5 };
let ft = f(t);
if ft > 0.0 {
lo = t;
flo = ft;
if side == -1 {
fhi *= 0.5;
}
side = -1;
} else {
hi = t;
fhi = ft;
if side == 1 {
flo *= 0.5;
}
side = 1;
}
}
hi
}
fn linear_exp_root(a: f64, b: f64, r: f64, eps: f64) -> f64 {
if b.is_nan() || b <= 0.0 {
return exp_root(a, r, eps);
}
if r.is_nan() || r <= 0.0 || !a.is_finite() || !b.is_finite() {
return f64::INFINITY;
}
let big = a + 2.0 * b / r;
if big.is_nan() || big <= eps {
return 0.0;
}
let mut t = 2.0 * (big / eps).ln() / r;
let target = eps.ln();
for _ in 0..16 {
let h = (a + b * t).ln() - r * t - target;
let dh = b / (a + b * t) - r;
if dh.is_nan() || dh >= 0.0 {
return if a <= eps { 0.0 } else { t };
}
let next = t - h / dh;
if !next.is_finite() || next >= t {
break;
}
let step = t - next;
t = next;
if step < 1e-9 * (1.0 + t) {
break;
}
}
t.max(0.0)
}
pub(crate) fn sane_state(x0: f32, v0: f32, target: f32) -> (f32, f32, f32) {
let target = if target.is_finite() {
target
} else if x0.is_finite() {
x0
} else {
0.0
};
let x0 = if x0.is_finite() { x0 } else { target };
let v0 = if v0.is_finite() { v0 } else { 0.0 };
(x0, v0, target)
}
pub(crate) fn finite_f32(x: f64, fallback: f32) -> f32 {
let y = x as f32;
if y.is_finite() { y } else { fallback }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constructor_clamps() {
assert_eq!(Spring::new(f32::NAN, f32::NAN), Spring::DEFAULT);
assert!(Spring::new(-1.0, 0.0).is_none());
assert!(Spring::new(f32::NEG_INFINITY, 0.0).is_none());
assert_eq!(Spring::new(f32::INFINITY, 2.0).duration(), Spring::MAX_DURATION);
assert_eq!(Spring::new(1e-9, -2.0).duration(), Spring::MIN_DURATION);
assert_eq!(Spring::new(1.0, -2.0).bounce(), -Spring::MAX_BOUNCE);
assert_eq!(Spring::from_response(0.5, 1.0), Spring::new(0.5, 0.0));
assert!((Spring::from_response(0.5, 0.7).bounce() - 0.3).abs() < 1e-6);
assert!((Spring::from_response(0.5, 2.0).damping_ratio() - 2.0).abs() < 1e-5);
}
#[test]
fn critical_has_no_overshoot() {
let s = Spring::new(0.4, 0.0);
let mut t = 0.0;
while t < 2.0 {
let (x, _) = s.sample(0.0, 0.0, 1.0, t);
assert!(x <= 1.0 + 1e-6, "{x} at {t}");
t += 0.001;
}
}
#[test]
fn linear_exp_root_is_last_crossing() {
for &(a, b, r) in &[(1.0, 10.0, 3.0), (0.0, 5.0, 1.0), (2.0, 0.0, 2.0), (1e-6, 1.0, 50.0)] {
let eps = 1e-3;
let t = linear_exp_root(a, b, r, eps);
let f = |t: f64| (a + b * t) * (-r * t).exp();
assert!(f(t) <= eps * (1.0 + 1e-9), "{a} {b} {r}");
let mut u = t;
while u < t + 20.0 {
assert!(f(u) <= eps * (1.0 + 1e-9));
u += 0.01;
}
assert!(f(t - 1e-3) > eps * 0.99 || t < 1e-3);
}
}
}