use glam::{Vec3, Vec3Swizzles};
use crate::{
prelude::Interpolatable,
utils::math::{cross2d, intersection},
};
#[derive(Default)]
pub struct PathBuilder {
start_point: Option<Vec3>,
points: Vec<Vec3>,
}
impl PathBuilder {
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.points.len()
}
pub fn move_to(&mut self, point: Vec3) -> &mut Self {
self.start_point = Some(point);
if let Some(end) = self.points.last() {
self.points.extend_from_slice(&[*end, point]);
} else {
self.points.push(point);
}
self
}
fn assert_started(&self) {
assert!(
self.start_point.is_some() || self.points.is_empty(),
"A path have to start with move_to"
);
}
pub fn line_to(&mut self, p: Vec3) -> &mut Self {
self.assert_started();
let mid = (self.points.last().unwrap() + p) / 2.0;
self.points.extend_from_slice(&[mid, p]);
self
}
pub fn quad_to(&mut self, h: Vec3, p: Vec3) -> &mut Self {
self.assert_started();
let cur = self.points.last().unwrap();
if cur.distance_squared(h) < f32::EPSILON || h.distance_squared(p) < f32::EPSILON {
return self.line_to(p);
}
self.points.extend_from_slice(&[h, p]);
self
}
pub fn cubic_to(&mut self, h1: Vec3, h2: Vec3, p: Vec3) -> &mut Self {
self.assert_started();
let cur = self.points.last().unwrap();
if cur.distance_squared(h1) < f32::EPSILON || h1.distance_squared(h2) < f32::EPSILON {
return self.quad_to(h2, p);
}
if h2.distance_squared(p) < f32::EPSILON {
return self.quad_to(h1, p);
}
let quads = approx_cubic_with_quadratic([*cur, h1, h2, p]);
for quad in quads {
self.quad_to(quad[1], quad[2]);
}
self
}
pub fn close_path(&mut self) -> &mut Self {
self.assert_started();
if self.points.last() == self.start_point.as_ref() {
return self;
}
self.line_to(self.start_point.unwrap());
self
}
pub fn vpoints(&self) -> &[Vec3] {
&self.points
}
}
pub fn split_cubic_bezier(bezier: &[Vec3; 4], t: f32) -> ([Vec3; 4], [Vec3; 4]) {
let [p0, h0, h1, p1] = bezier;
let split_point = &cubic_bezier_eval(bezier, t);
let h00 = p0.lerp(*h0, t);
let h01 = p0.lerp(*h0, t).lerp(h0.lerp(*h1, t), t);
let h10 = h0.lerp(*h1, t).lerp(h1.lerp(*p1, t), t);
let h11 = h1.lerp(*p1, t);
([*p0, h00, h01, *split_point], [*split_point, h10, h11, *p1])
}
pub fn split_quad_bezier(bezier: &[Vec3; 3], t: f32) -> ([Vec3; 3], [Vec3; 3]) {
let [p0, h, p1] = bezier;
let split_point = &quad_bezier_eval(bezier, t);
let h0 = p0.lerp(*h, t);
let h1 = h.lerp(*p1, t);
([*p0, h0, *split_point], [*split_point, h1, *p1])
}
pub fn trim_quad_bezier(bezier: &[Vec3; 3], a: f32, b: f32) -> [Vec3; 3] {
let (a, b) = if a > b { (b, a) } else { (a, b) };
let end_on_b = split_quad_bezier(bezier, b).0;
let a = a / b;
split_quad_bezier(&end_on_b, a).1
}
pub fn trim_cubic_bezier(bezier: &[Vec3; 4], a: f32, b: f32) -> [Vec3; 4] {
let (a, b) = if a > b { (b, a) } else { (a, b) };
let end_on_b = split_cubic_bezier(bezier, b).0;
let a = a / b;
split_cubic_bezier(&end_on_b, a).1
}
pub fn point_on_quadratic_bezier<T: Interpolatable>(points: &[T; 3], t: f32) -> T {
let t = t.clamp(0.0, 1.0);
let p1 = points[0].lerp(&points[1], t);
let p2 = points[1].lerp(&points[2], t);
p1.lerp(&p2, t)
}
pub fn partial_quadratic_bezier<T: Interpolatable>(points: &[T; 3], a: f32, b: f32) -> [T; 3] {
let a = a.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
let h0 = point_on_quadratic_bezier(points, a);
let h2 = point_on_quadratic_bezier(points, b);
let h1_prime = points[1].lerp(&points[2], a);
let end_prop = (b - a) / (1.0 - a);
let h1 = h0.lerp(&h1_prime, end_prop);
[h0, h1, h2]
}
pub fn cubic_bezier_eval(bezier: &[Vec3; 4], t: f32) -> Vec3 {
let t = t.clamp(0.0, 1.0);
let p0 = bezier[0].lerp(bezier[1], t);
let p1 = bezier[1].lerp(bezier[2], t);
let p2 = bezier[2].lerp(bezier[3], t);
let p0 = p0.lerp(p1, t);
let p1 = p1.lerp(p2, t);
p0.lerp(p1, t)
}
pub fn quad_bezier_eval(bezier: &[Vec3; 3], t: f32) -> Vec3 {
let t = t.clamp(0.0, 1.0);
let p0 = bezier[0].lerp(bezier[1], t);
let p1 = bezier[1].lerp(bezier[2], t);
p0.lerp(p1, t)
}
pub fn approx_cubic_with_quadratic(cubic: [Vec3; 4]) -> Vec<[Vec3; 3]> {
let [p1, h1, h2, p2] = cubic;
let p = h1 - p1;
let q = h2 - 2. * h1 + p1;
let r = p2 - 3. * h2 + 3. * h1 - p1;
let a = cross2d(q.xy(), r.xy());
let b = cross2d(p.xy(), r.xy());
let c = cross2d(p.xy(), q.xy());
let disc = b * b - 4. * a * c;
let sqrt_disc = disc.sqrt();
let mut root = if a == 0.0 && b == 0.0 || a != 0.0 && disc < 0.0 {
0.5
} else if a == 0.0 {
-c / b
} else {
let mut root = (-b + sqrt_disc) / (2. * a);
if root <= 0.0 || root >= 1.0 {
root = (b + sqrt_disc) / (-2. * a);
}
if root <= 0.0 || root >= 1.0 {
root = 0.5
}
root
};
if root == 0.0 || root == 1.0 {
root = 0.5;
}
let cut_point = cubic_bezier_eval(&cubic, root);
let cut_tangent = quad_bezier_eval(&[h1 - p1, h2 - h1, p2 - h2], root);
let p1_tangent = h1 - p1;
let p2_tangent = p2 - h2;
let i1 = intersection(p1, p1_tangent, cut_point, cut_tangent);
let i2 = intersection(p2, p2_tangent, cut_point, cut_tangent);
let (Some(i1), Some(i2)) = (i1, i2) else {
let root = if root > 0.5 {
root / 2.0
} else {
0.5 + (1.0 - root) / 2.0
};
let cut_point = cubic_bezier_eval(&cubic, root);
let cut_tangent = quad_bezier_eval(&[h1 - p1, h2 - h1, p2 - h2], root);
let p1_tangent = h1 - p1;
let p2_tangent = p2 - h2;
let i1 = intersection(p1, p1_tangent, cut_point, cut_tangent).unwrap();
let i2 = intersection(p2, p2_tangent, cut_point, cut_tangent).unwrap();
return vec![[p1, i1, cut_point], [cut_point, i2, p2]];
};
vec![[p1, i1, cut_point], [cut_point, i2, p2]]
}
#[cfg(test)]
mod test {
use super::*;
use glam::vec3;
#[test]
fn test_trim_quad_bezier() {
let bezier = [
vec3(0.0, 0.0, 0.0),
vec3(1.0, 2.0, 0.0),
vec3(2.0, 0.0, 0.0),
];
let trimmed = trim_quad_bezier(&bezier, 0.25, 0.75);
let start_point = quad_bezier_eval(&bezier, 0.25);
let end_point = quad_bezier_eval(&bezier, 0.75);
assert!((trimmed[0] - start_point).length() < f32::EPSILON);
assert!((trimmed[2] - end_point).length() < f32::EPSILON);
let trimmed_reversed = trim_quad_bezier(&bezier, 0.75, 0.25);
assert!((trimmed_reversed[0] - start_point).length() < f32::EPSILON);
assert!((trimmed_reversed[2] - end_point).length() < f32::EPSILON);
let full_curve = trim_quad_bezier(&bezier, 0.0, 1.0);
assert!((full_curve[0] - bezier[0]).length() < f32::EPSILON);
assert!((full_curve[2] - bezier[2]).length() < f32::EPSILON);
let zero_length = trim_quad_bezier(&bezier, 0.5, 0.5);
let mid_point = quad_bezier_eval(&bezier, 0.5);
assert!((zero_length[0] - mid_point).length() < f32::EPSILON);
assert!((zero_length[2] - mid_point).length() < f32::EPSILON);
let complex_bezier = [
vec3(-1.0, 0.0, 1.0),
vec3(1.0, 3.0, 0.0),
vec3(3.0, 0.0, -1.0),
];
let trimmed_complex = trim_quad_bezier(&complex_bezier, 0.2, 0.8);
let complex_start = quad_bezier_eval(&complex_bezier, 0.2);
let complex_end = quad_bezier_eval(&complex_bezier, 0.8);
assert!((trimmed_complex[0] - complex_start).length() < f32::EPSILON);
assert!((trimmed_complex[2] - complex_end).length() < f32::EPSILON);
}
}