use std::{borrow::Cow, cmp::Ordering, convert::identity, f64::consts::PI, iter};
use crate::parse::{PathControlPoint, PathType, Pos2};
const BEZIER_TOLERANCE: f32 = 0.25;
const CATMULL_DETAIL: usize = 50;
const CIRCULAR_ARC_TOLERANCE: f32 = 0.1;
#[derive(Clone, Debug, Default)]
pub(crate) struct CurveBuffers {
path: Vec<Pos2>,
lengths: Vec<f64>,
vertices: Vec<Pos2>,
bezier: BezierBuffers,
}
#[derive(Clone, Debug, Default)]
struct BezierBuffers {
left: Vec<Pos2>,
right: Vec<Pos2>,
midpoints: Vec<Pos2>,
left_child: Vec<Pos2>,
}
impl BezierBuffers {
fn extend_exact(&mut self, len: usize) {
if len <= self.left.len() {
return;
}
let additional = len - self.left.len();
self.left
.extend(iter::repeat(Pos2::zero()).take(additional));
self.right
.extend(iter::repeat(Pos2::zero()).take(additional));
self.midpoints
.extend(iter::repeat(Pos2::zero()).take(additional));
self.left_child
.extend(iter::repeat(Pos2::zero()).take(additional));
}
}
struct CircularArcProperties {
theta_start: f64,
theta_range: f64,
direction: f64,
radius: f32,
centre: Pos2,
}
pub(crate) struct Curve<'bufs> {
path: &'bufs [Pos2],
lengths: &'bufs [f64],
}
impl<'bufs> Curve<'bufs> {
pub(crate) fn new(
points: &[PathControlPoint],
expected_len: Option<f64>,
bufs: &'bufs mut CurveBuffers,
) -> Self {
Self::calculate_path(points, bufs);
Self::calculate_length(points, bufs, expected_len);
Self {
path: &bufs.path,
lengths: &bufs.lengths,
}
}
pub(crate) fn position_at(&self, progress: f64) -> Pos2 {
let d = self.progress_to_dist(progress);
let i = self.idx_of_dist(d);
self.interpolate_vertices(i, d)
}
fn progress_to_dist(&self, progress: f64) -> f64 {
progress.clamp(0.0, 1.0) * self.dist()
}
pub(crate) fn dist(&self) -> f64 {
self.lengths.last().copied().unwrap_or(0.0)
}
fn idx_of_dist(&self, d: f64) -> usize {
self.lengths
.binary_search_by(|len| len.partial_cmp(&d).unwrap_or(Ordering::Equal))
.map_or_else(identity, identity)
}
fn interpolate_vertices(&self, i: usize, d: f64) -> Pos2 {
if self.path.is_empty() {
return Pos2::zero();
}
let p1 = if i == 0 {
return self.path[0];
} else if let Some(p) = self.path.get(i) {
*p
} else {
return self.path[self.path.len() - 1];
};
let p0 = self.path[i - 1];
let d0 = self.lengths[i - 1];
let d1 = self.lengths[i];
if (d0 - d1).abs() <= f64::EPSILON {
return p0;
}
let w = (d - d0) / (d1 - d0);
p0 + (p1 - p0) * w as f32
}
fn calculate_path(points: &[PathControlPoint], bufs: &mut CurveBuffers) {
bufs.path.clear();
if points.is_empty() {
return;
}
let CurveBuffers {
vertices,
bezier,
path,
..
} = bufs;
vertices.clear();
vertices.extend(points.iter().map(|p| p.pos));
let mut start = 0;
for i in 0..points.len() {
if points[i].kind.is_none() && i < points.len() - 1 {
continue;
}
let segment_vertices = &vertices[start..i + 1];
let segment_kind = points[start].kind.unwrap_or(PathType::Linear);
Self::calculate_subpath(path, segment_vertices, segment_kind, bezier);
start = i;
}
path.dedup();
}
fn calculate_length(
points: &[PathControlPoint],
bufs: &mut CurveBuffers,
expected_len: Option<f64>,
) {
let CurveBuffers {
path,
lengths: cumulative_len,
..
} = bufs;
cumulative_len.clear();
let mut calculated_len = 0.0;
cumulative_len.reserve(path.len());
cumulative_len.push(0.0);
let length_iter = path.iter().zip(path.iter().skip(1)).map(|(&curr, &next)| {
calculated_len += (next - curr).length() as f64;
calculated_len
});
cumulative_len.extend(length_iter);
if let Some(expected_len) = expected_len.filter(|&len| calculated_len != len) {
let condition_opt = points
.len()
.checked_sub(2)
.and_then(|i| points.get(i..))
.filter(|suffix| suffix[0].pos == suffix[1].pos && expected_len > calculated_len);
if condition_opt.is_some() {
cumulative_len.push(calculated_len);
return;
}
if cumulative_len.len() == 1 {
return;
}
cumulative_len.pop();
let last_valid = cumulative_len
.iter()
.rev()
.position(|l| *l < expected_len)
.map_or(0, |idx| cumulative_len.len() - idx);
if last_valid < cumulative_len.len() {
cumulative_len.truncate(last_valid);
path.truncate(last_valid + 1);
if cumulative_len.is_empty() {
cumulative_len.push(0.0);
return;
}
}
let end_idx = cumulative_len.len();
let prev_idx = end_idx - 1;
let dir = (path[end_idx] - path[prev_idx]).normalize();
path[end_idx] = path[prev_idx] + dir * (expected_len - cumulative_len[prev_idx]) as f32;
cumulative_len.push(expected_len);
}
}
fn calculate_subpath(
path: &mut Vec<Pos2>,
sub_points: &[Pos2],
kind: PathType,
bufs: &mut BezierBuffers,
) {
match kind {
PathType::Bezier => Self::approximate_bezier(path, sub_points, bufs),
PathType::Catmull => Self::approximate_catmull(path, sub_points),
PathType::Linear => Self::approximate_linear(path, sub_points),
PathType::PerfectCurve => {
if let [a, b, c] = sub_points {
if Self::approximate_circular_arc(path, *a, *b, *c) {
return;
}
}
Self::approximate_bezier(path, sub_points, bufs)
}
}
}
fn approximate_bezier(path: &mut Vec<Pos2>, points: &[Pos2], bufs: &mut BezierBuffers) {
bufs.extend_exact(points.len());
Self::approximate_bspline(path, points, bufs);
}
fn approximate_catmull(path: &mut Vec<Pos2>, points: &[Pos2]) {
if points.len() == 1 {
return;
}
path.reserve_exact((points.len() - 1) * CATMULL_DETAIL * 2);
let v1 = points[0];
let v2 = points[0];
let v3 = points.get(1).copied().unwrap_or(v2);
let v4 = points.get(2).copied().unwrap_or_else(|| v3 * 2.0 - v2);
Self::catmull_subpath(path, v1, v2, v3, v4);
for (i, (&v1, &v2)) in (2..points.len()).zip(points.iter().zip(points.iter().skip(1))) {
let v3 = points.get(i).copied().unwrap_or_else(|| v2 * 2.0 - v1);
let v4 = points.get(i + 1).copied().unwrap_or_else(|| v3 * 2.0 - v2);
Self::catmull_subpath(path, v1, v2, v3, v4);
}
}
fn approximate_linear(path: &mut Vec<Pos2>, points: &[Pos2]) {
path.extend(points)
}
fn approximate_circular_arc(path: &mut Vec<Pos2>, a: Pos2, b: Pos2, c: Pos2) -> bool {
let Some(pr) = Self::circular_arc_properties(a, b, c) else {
return false;
};
let amount_points = if 2.0 * pr.radius <= CIRCULAR_ARC_TOLERANCE {
2
} else {
let divisor = 2.0 * (1.0 - CIRCULAR_ARC_TOLERANCE / pr.radius).acos();
if divisor.abs() <= f32::EPSILON {
2
} else {
((pr.theta_range / divisor as f64).ceil() as usize).max(2)
}
};
path.reserve_exact(amount_points);
let divisor = (amount_points - 1) as f64;
let directed_range = pr.direction * pr.theta_range;
let subpath = (0..amount_points).map(|i| {
let fract = i as f64 / divisor;
let theta = pr.theta_start + fract * directed_range;
let (sin, cos) = theta.sin_cos();
let origin = Pos2 {
x: cos as f32,
y: sin as f32,
};
pr.centre + origin * pr.radius
});
path.extend(subpath);
true
}
fn approximate_bspline(path: &mut Vec<Pos2>, points: &[Pos2], bufs: &mut BezierBuffers) {
let p = points.len();
let mut to_flatten = Vec::new();
let mut free_bufs = Vec::new();
to_flatten.push(Cow::Borrowed(points));
let BezierBuffers {
left,
right,
midpoints,
left_child,
} = bufs;
while let Some(mut parent) = to_flatten.pop() {
if Self::bezier_is_flat_enough(&parent) {
Self::bezier_approximate(&parent, path, left, right, midpoints);
free_bufs.push(parent);
continue;
}
let mut right_child = free_bufs
.pop()
.unwrap_or_else(|| Cow::Owned(vec![Pos2::zero(); p]));
Self::bezier_subdivide(&parent, left_child, right_child.to_mut(), midpoints);
parent.to_mut().copy_from_slice(&left_child[..p]);
to_flatten.push(right_child);
to_flatten.push(parent);
}
path.push(points[p - 1]);
}
fn bezier_is_flat_enough(points: &[Pos2]) -> bool {
let limit = BEZIER_TOLERANCE * BEZIER_TOLERANCE * 4.0;
!points
.iter()
.zip(points.iter().skip(1))
.zip(points.iter().skip(2))
.any(|((&prev, &curr), &next)| (prev - curr * 2.0 + next).length_squared() > limit)
}
fn bezier_subdivide(points: &[Pos2], l: &mut [Pos2], r: &mut [Pos2], midpoints: &mut [Pos2]) {
let count = points.len();
midpoints[..count].copy_from_slice(&points[..count]);
for i in (1..count).rev() {
l[count - i - 1] = midpoints[0];
r[i] = midpoints[i];
for j in 0..i {
midpoints[j] = (midpoints[j] + midpoints[j + 1]) / 2.0;
}
}
l[count - 1] = midpoints[0];
r[0] = midpoints[0];
}
fn bezier_approximate(
points: &[Pos2],
path: &mut Vec<Pos2>,
l: &mut [Pos2],
r: &mut [Pos2],
midpoints: &mut [Pos2],
) {
let count = points.len();
Self::bezier_subdivide(points, l, r, midpoints);
path.push(points[0]);
let l = &l[..count];
let r = &r[1..count];
let subpath = l
.iter()
.chain(r)
.skip(1)
.zip(l.iter().chain(r).skip(2))
.zip(l.iter().chain(r).skip(3))
.step_by(2)
.map(|((&prev, &curr), &next)| (prev + curr * 2.0 + next) * 0.25);
path.extend(subpath);
}
fn catmull_subpath(path: &mut Vec<Pos2>, v1: Pos2, v2: Pos2, v3: Pos2, v4: Pos2) {
let x1 = 2.0 * v2.x;
let x2 = -v1.x + v3.x;
let x3 = 2.0 * v1.x - 5.0 * v2.x + 4.0 * v3.x - v4.x;
let x4 = -v1.x + 3.0 * (v2.x - v3.x) + v4.x;
let y1 = 2.0 * v2.y;
let y2 = -v1.y + v3.y;
let y3 = 2.0 * v1.y - 5.0 * v2.y + 4.0 * v3.y - v4.y;
let y4 = -v1.y + 3.0 * (v2.y - v3.y) + v4.y;
let catmull_detail = CATMULL_DETAIL as f32;
let subpath = (0..CATMULL_DETAIL).flat_map(|c| {
let c = c as f32;
let t1 = c / catmull_detail;
let t2 = t1 * t1;
let t3 = t2 * t1;
let pos1 = Pos2 {
x: 0.5 * (x1 + x2 * t1 + x3 * t2 + x4 * t3),
y: 0.5 * (y1 + y2 * t1 + y3 * t2 + y4 * t3),
};
let t1 = (c + 1.0) / catmull_detail;
let t2 = t1 * t1;
let t3 = t2 * t1;
let pos2 = Pos2 {
x: 0.5 * (x1 + x2 * t1 + x3 * t2 + x4 * t3),
y: 0.5 * (y1 + y2 * t1 + y3 * t2 + y4 * t3),
};
iter::once(pos1).chain(iter::once(pos2))
});
path.extend(subpath);
}
fn circular_arc_properties(a: Pos2, b: Pos2, c: Pos2) -> Option<CircularArcProperties> {
if ((b.y - a.y) * (c.x - a.x) - (b.x - a.x) * (c.y - a.y)).abs() <= f32::EPSILON {
return None;
}
let d = 2.0 * (a.x * (b - c).y + b.x * (c - a).y + c.x * (a - b).y);
let a_sq = a.length_squared();
let b_sq = b.length_squared();
let c_sq = c.length_squared();
let centre = Pos2 {
x: (a_sq * (b - c).y + b_sq * (c - a).y + c_sq * (a - b).y) / d,
y: (a_sq * (c - b).x + b_sq * (a - c).x + c_sq * (b - a).x) / d,
};
let d_a = a - centre;
let d_c = c - centre;
let radius = d_a.length();
let theta_start = (d_a.y as f64).atan2(d_a.x as f64);
let mut theta_end = (d_c.y as f64).atan2(d_c.x as f64);
while theta_end < theta_start {
theta_end += 2.0 * PI;
}
let mut direction = 1.0;
let mut theta_range = theta_end - theta_start;
let mut ortho_a_to_c = c - a;
ortho_a_to_c = Pos2 {
x: ortho_a_to_c.y,
y: -ortho_a_to_c.x,
};
if ortho_a_to_c.dot(b - a) < 0.0 {
direction = -direction;
theta_range = 2.0 * PI - theta_range;
}
Some(CircularArcProperties {
theta_start,
theta_range,
direction,
radius,
centre,
})
}
}