use super::pos::{CurvePos, quadratic_bezier};
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct BezierCurve {
pub(crate) start: CurvePos,
pub(crate) bezier: CurvePos,
pub(crate) end: CurvePos,
}
impl Hash for BezierCurve {
fn hash<H: Hasher>(&self, state: &mut H) {
self.start.x.to_be_bytes().hash(state);
self.start.y.to_be_bytes().hash(state);
self.bezier.x.to_be_bytes().hash(state);
self.bezier.y.to_be_bytes().hash(state);
self.end.x.to_be_bytes().hash(state);
self.end.y.to_be_bytes().hash(state);
}
}
impl BezierCurve {
pub(crate) fn new(start: CurvePos, bezier: CurvePos, end: CurvePos) -> Self {
Self { start, bezier, end }
}
pub(crate) fn value(&self, x: f32) -> f32 {
#[cfg(not(target_arch = "wasm32"))]
if let Some(value) = fitted::value(self, x) {
return value;
}
self.sample(x)
}
pub(crate) fn sample(&self, x: f32) -> f32 {
let points = [self.start, self.bezier, self.end];
let mut n = 2;
let mut t = 0.5;
loop {
let sample = quadratic_bezier(points, t);
let e = sample.x - x;
if e.abs() < 0.005 {
break sample.y;
}
let step = 1.0 / 2.0f32.powi(n);
if e < 0.0 {
t += step;
} else {
t -= step;
}
n += 1;
}
}
}
#[cfg(not(target_arch = "wasm32"))]
pub use fitted::{Polynomial, polynomials_fitting};
#[cfg(not(target_arch = "wasm32"))]
mod fitted {
use super::BezierCurve;
use crate::asset::curve::pos::quadratic_bezier;
use kanal::{Sender, unbounded};
use ndarray::arr1;
use once_cell::sync::Lazy;
use polyfit_residuals::{PolyFit, poly::NewtonPolynomial, try_fit_poly_with_residual};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use std::{
collections::HashMap,
sync::{
Mutex, MutexGuard, PoisonError,
atomic::{AtomicUsize, Ordering::Relaxed},
},
thread::spawn,
};
pub type Polynomial = NewtonPolynomial<f64, f64, Vec<f64>, Vec<f64>>;
static POLYNOMIALS_FITTING: AtomicUsize = AtomicUsize::new(0);
static POLYNOMIALS: Lazy<Mutex<HashMap<BezierCurve, Option<Polynomial>>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
static POLYNOMIAL_QUEUE: Lazy<Sender<BezierCurve>> = Lazy::new(|| {
let (sender, receiver) = unbounded::<BezierCurve>();
spawn(move || {
for bezier_curve in receiver {
if let Some(polynomial) = fit(&bezier_curve) {
polynomials().insert(bezier_curve, Some(polynomial));
}
POLYNOMIALS_FITTING.fetch_sub(1, Relaxed);
}
});
sender
});
fn polynomials() -> MutexGuard<'static, HashMap<BezierCurve, Option<Polynomial>>> {
POLYNOMIALS.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(super) fn value(curve: &BezierCurve, x: f32) -> Option<f32> {
let mut polynomials = polynomials();
match polynomials.get(curve) {
Some(Some(polynomial)) => return Some(polynomial.eval(x as f64) as f32),
None => {
POLYNOMIALS_FITTING.fetch_add(1, Relaxed);
POLYNOMIAL_QUEUE.send(*curve).ok();
polynomials.insert(*curve, None);
}
Some(None) => (),
}
None
}
pub fn polynomials_fitting() -> usize {
POLYNOMIALS_FITTING.load(Relaxed)
}
fn fit(curve: &BezierCurve) -> Option<Polynomial> {
tracing::debug!("Fitting polynomial to curve: {curve:?}");
let points = [curve.start, curve.bezier, curve.end];
let (x_values, y_values): (Vec<_>, Vec<_>) = (0..1_000_000)
.map(|t| t as f32 / 1_000_000.0)
.map(|t| {
let pos = quadratic_bezier(points, t);
(pos.x as f64, pos.y as f64)
})
.unzip();
let x_values = arr1(&x_values);
let y_values = arr1(&y_values);
let best = (1..6)
.into_par_iter()
.filter_map(|degree| {
try_fit_poly_with_residual(x_values.view(), y_values.view(), degree)
.map_err(|err| {
tracing::warn!("Could not fit a degree {degree} polynomial: {err:?}")
})
.ok()
})
.collect::<Vec<_>>()
.into_iter()
.min_by(|a: &PolyFit<f64, f64>, b| a.residual.total_cmp(&b.residual))
.map(|fit| fit.polynomial);
tracing::debug!("Done fitting polynomial to curve: {curve:?}");
best
}
}