mod bezier;
use ordered_float::NotNan;
use crate::float::compare_eq_f64;
use crate::hitobject::SliderSplineKind;
use crate::math::{Math, Point};
use self::bezier::{create_singlebezier, P};
const CATMULL_DETAIL: usize = 50;
#[derive(Clone, Debug)]
pub struct Spline {
pub spline_points: Vec<P>,
pub cumulative_lengths: Vec<NotNan<f64>>,
}
impl Spline {
pub fn from_control(
kind: SliderSplineKind,
control_points: &[Point<i32>],
pixel_length: Option<f64>,
) -> Self {
let mut kind = kind;
let mut control_points = control_points.to_vec();
if control_points.len() == 2 {
kind = SliderSplineKind::Linear;
}
if control_points.len() == 3
&& Math::is_line(
control_points[0].to_float::<f64>().unwrap(),
control_points[1].to_float::<f64>().unwrap(),
control_points[2].to_float::<f64>().unwrap(),
)
{
kind = SliderSplineKind::Linear;
control_points.remove(1);
}
let points = control_points
.iter()
.map(|p| Point::new(p.x as f64, p.y as f64))
.collect::<Vec<_>>();
let spline_points = match kind {
SliderSplineKind::Linear => {
let start = points[0];
let end = if let Some(pixel_length) = pixel_length {
Math::point_on_line(points[0], points[1], pixel_length)
} else {
points[1]
};
vec![start, end]
}
SliderSplineKind::Perfect => {
let (p1, p2, p3) = (points[0], points[1], points[2]);
let (center, radius) = Math::circumcircle(p1, p2, p3);
let t0 = (center.y - p1.y).atan2(p1.x - center.x);
let mut mid = (center.y - p2.y).atan2(p2.x - center.x);
let mut t1 = (center.y - p3.y).atan2(p3.x - center.x);
while mid < t0 {
mid += std::f64::consts::TAU;
}
while t1 < t0 {
t1 += std::f64::consts::TAU;
}
if mid > t1 {
t1 -= std::f64::consts::TAU;
}
let diff = (t1 - t0).abs();
let pixel_length = pixel_length.unwrap_or(radius * diff);
let direction_unit = (t1 - t0) / (t1 - t0).abs();
let new_t1 = t0 + direction_unit * (pixel_length / radius);
let mut t = t0;
let mut c = Vec::new();
loop {
if !((new_t1 >= t0 && t < new_t1) || (new_t1 < t0 && t > new_t1)) {
break;
}
let rel = Point::new(t.cos() * radius, -t.sin() * radius);
c.push(center + rel);
t += (new_t1 - t0) / pixel_length;
}
c
}
SliderSplineKind::Bezier => {
let mut idx = 0;
let mut whole = Vec::new();
let mut cumul_length = 0.0;
let mut last_circ: Option<P> = None;
let mut check_push = |whole: &mut Vec<P>, point: P| -> bool {
let result;
if let Some(circ) = last_circ {
let distance = circ.distance(point);
let total_len = cumul_length + distance;
if let Some(pixel_length) = pixel_length {
if total_len < pixel_length {
whole.push(point);
cumul_length += circ.distance(point);
last_circ = Some(point);
result = true;
} else {
let push_amt = pixel_length - cumul_length;
let new_end = Math::point_on_line(circ, point, push_amt);
whole.push(new_end);
last_circ = Some(new_end);
result = false;
}
} else {
whole.push(point);
cumul_length += circ.distance(point);
last_circ = Some(point);
result = true;
}
} else {
whole.push(point);
last_circ = Some(point);
result = true;
}
result
};
#[allow(clippy::never_loop)]
'outer: loop {
for i in 1..points.len() {
if compare_eq_f64(points[i].x, points[i - 1].x)
&& compare_eq_f64(points[i].y, points[i - 1].y)
{
let mut spline = Vec::new();
create_singlebezier(&mut spline, &points[idx..i]);
if let Some(last) = whole.last() {
if spline[0] != *last && !check_push(&mut whole, spline[0]) {
break 'outer;
}
} else if !check_push(&mut whole, spline[0]) {
break 'outer;
}
for points in spline.windows(2) {
if points[0] != points[1] && !check_push(&mut whole, points[1]) {
break 'outer;
}
}
idx = i;
continue;
}
}
let mut spline = Vec::new();
create_singlebezier(&mut spline, &points[idx..]);
if let Some(last) = whole.last() {
if spline[0] != *last && !check_push(&mut whole, spline[0]) {
break 'outer;
}
} else if !check_push(&mut whole, spline[0]) {
break 'outer;
}
for points in spline.windows(2) {
if points[0] != points[1] && !check_push(&mut whole, points[1]) {
break 'outer;
}
}
break;
}
whole
}
SliderSplineKind::Catmull => {
let mut path = Vec::with_capacity((points.len() - 1) * CATMULL_DETAIL * 2);
for j in 0..points.len() - 1 {
let v1 = match j {
n if n > 0 => points[j - 1],
_ => points[0],
};
let v2 = points[j];
let v3 = match j + 1 {
n if n < points.len() => points[j + 1],
_ => v2 + (v2 - v1),
};
let v4 = match j + 2 {
n if n < points.len() => points[j + 2],
_ => v3 + (v3 - v2),
};
for c in 0..CATMULL_DETAIL {
path.push(Math::catmull_find_point(
v1,
v2,
v3,
v4,
c as f64 / CATMULL_DETAIL as f64,
));
path.push(Math::catmull_find_point(
v1,
v2,
v3,
v4,
(c + 1) as f64 / CATMULL_DETAIL as f64,
));
}
}
path
}
};
let mut cumulative_lengths = Vec::with_capacity(spline_points.len());
let mut curr = 0.0;
cumulative_lengths.push(NotNan::new(curr).unwrap());
for points in spline_points.windows(2) {
let dist = points[0].distance(points[1]);
curr += dist;
cumulative_lengths.push(NotNan::new(curr).unwrap());
}
Spline {
spline_points,
cumulative_lengths,
}
}
pub fn truncate(&mut self, to_length: f64) {
debug!("truncating to {} pixels", to_length);
let mut limit_idx = None;
for (i, cumul_length) in self.cumulative_lengths.iter().enumerate() {
if cumul_length.into_inner() > to_length {
limit_idx = Some(i);
break;
}
}
let limit_idx = match limit_idx {
Some(v) if v > 0 => v,
_ => return,
};
let prev_idx = limit_idx - 1;
let a = self.spline_points[prev_idx];
let b = self.spline_points[limit_idx];
let a_len = self.cumulative_lengths[prev_idx];
debug!("a={:?} (a_len={}) b={:?}", a, b, a_len);
let remain = to_length - a_len.into_inner();
let mid = Math::point_on_line(a, b, remain);
debug!("remain={:?} mid={:?}", remain, mid);
self.spline_points[limit_idx] = mid;
self.cumulative_lengths[limit_idx] = NotNan::new(to_length).unwrap();
debug!("spline_points[{}] = {:?}", limit_idx, mid);
debug!("cumulative_lengths[{}] = {:?}", limit_idx, to_length);
self.spline_points.truncate(limit_idx + 1);
self.cumulative_lengths.truncate(limit_idx + 1);
debug!("truncated to len {}", limit_idx + 1);
}
pub fn pixel_length(&self) -> f64 {
self.cumulative_lengths.last().unwrap().into_inner()
}
pub fn end_point(&self) -> P {
self.spline_points.last().cloned().unwrap()
}
pub fn angle_at_length(&self, length: f64) -> f64 {
let mut cands = vec![];
const EPSILON: f64 = 0.001;
cands.push(self.point_at_length(length - EPSILON));
cands.push(self.point_at_length(length));
cands.push(self.point_at_length(length + EPSILON));
cands.dedup();
match cands.as_slice() {
&[a, b, ..] => (a.y - b.y).atan2(a.x - b.x),
_ => panic!("uhhhhh"),
}
}
pub fn point_at_length(&self, length: f64) -> P {
let length_notnan = NotNan::new(length).unwrap();
match self.cumulative_lengths.binary_search(&length_notnan) {
Ok(idx) => self.spline_points[idx],
Err(idx) => {
let n = self.spline_points.len();
if idx == 0 && self.spline_points.len() > 2 {
return self.spline_points[0];
} else if idx == n {
return self.spline_points[n - 1];
}
let (len1, len2) = (
self.cumulative_lengths[idx - 1].into_inner(),
self.cumulative_lengths[idx].into_inner(),
);
let proportion = (length - len1) / (len2 - len1);
let (p1, p2) = (self.spline_points[idx - 1], self.spline_points[idx]);
(p2 - p1) * P::new(proportion, proportion) + p1
}
}
}
}
#[cfg(test)]
mod tests {
use proptest::collection::vec;
use proptest::option;
use proptest::prelude::*;
use proptest::proptest;
use super::Spline;
use crate::hitobject::SliderSplineKind;
use crate::math::Point;
proptest! {
#![proptest_config(ProptestConfig {
cases: 2,
verbose: 2,
timeout: 3000,
..ProptestConfig::default()
})]
#[test]
fn doesnt_crash(
kind: SliderSplineKind,
pixel_length in option::of(any::<f64>()),
control in vec((0..512, 0..384), 10))
{
let control = control.into_iter().map(|(x, y)| Point::new(x, y)).collect::<Vec<_>>();
let spline = Spline::from_control(kind, &control, pixel_length);
}
}
}