libosu 0.0.28

General-purpose osu! library.
Documentation
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;

/// Represents a spline, a set of points that represents the actual shape of a slider, generated
/// from the control points.
#[derive(Clone, Debug)]
pub struct Spline {
    /// The actual points
    pub spline_points: Vec<P>,

    /// The cumulative lengths over the points. The indices correspond to the spline_points field
    pub cumulative_lengths: Vec<NotNan<f64>>,
}

impl Spline {
    /// Create a new spline from the control points of a slider.
    ///
    /// Pixel length gives the length in osu!pixels that the slider should be. If it's not given,
    /// the full slider will be rendered.
    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();

        // no matter what, if there's 2 control points, it's linear
        if control_points.len() == 2 {
            kind = SliderSplineKind::Linear;
        }

        // if there's 3 points but the 3 points occur on the same line then it's also linear
        // we can also safely remove the middle one
        // TODO: if the 3 points coincide then this gets fucked
        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);

                // find the t-values of the start and end of the slider
                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);

                // make sure t0 is less than t1
                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);

                // circumference is 2 * pi * r, slider length over circumference is length/(2 * pi * r)
                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
                };

                // TODO: hack to allow breaks
                #[allow(clippy::never_loop)]
                'outer: loop {
                    // split the curve by red-anchors
                    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]);

                            // check if it's equal to the last thing that was added to whole
                            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;
                            }

                            // add points, making sure no 2 are the same
                            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;
        // using NotNan here because these need to be binary-searched over
        // and f64 isn't Ord
        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,
        }
    }

    /// Truncate the length of the spline irreversibly
    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);
    }

    /// Return the pixel length of this spline
    pub fn pixel_length(&self) -> f64 {
        self.cumulative_lengths.last().unwrap().into_inner()
    }

    /// Return the endpoint of this spline
    pub fn end_point(&self) -> P {
        self.spline_points.last().cloned().unwrap()
    }

    /// Calculate the angle at the given length on the slider
    pub fn angle_at_length(&self, length: f64) -> f64 {
        // dumb way of calculating
        // TODO: is it worth it using a different algorithm?
        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"),
        }
    }

    /// Calculate the point at which the slider ball would be after it has traveled a distance of
    /// `length` into the slider.
    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) => {
                // if it's out of bounds, just return the bounds
                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];
                }

                // there's no point at this length, give us an approximation based on the two points
                // around it instead
                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);
        }
    }
}