use glam::{Vec2, Vec3};
#[inline(always)]
fn bezier_eval(p0: Vec2, p1: Vec2, p2: Vec2, t: f32) -> Vec2 {
let omt = 1.0 - t;
p0 * (omt * omt) + p1 * (2.0 * omt * t) + p2 * (t * t)
}
#[inline(always)]
fn bezier_tangent(p0: Vec2, p1: Vec2, p2: Vec2, t: f32) -> Vec2 {
(p1 - p0) * (2.0 * (1.0 - t)) + (p2 - p1) * (2.0 * t)
}
#[inline]
fn closest_t_on_bezier(q: Vec2, p0: Vec2, p1: Vec2, p2: Vec2) -> f32 {
let mut best_t = 0.0f32;
let mut best_d2 = f32::MAX;
for i in 0..5 {
let t = i as f32 * 0.25;
let b = bezier_eval(p0, p1, p2, t);
let d2 = (q - b).length_squared();
if d2 < best_d2 {
best_d2 = d2;
best_t = t;
}
}
let b_dd = 2.0 * (p0 - 2.0 * p1 + p2);
let mut t = best_t;
for _ in 0..5 {
let bt = bezier_eval(p0, p1, p2, t);
let bt_d = bezier_tangent(p0, p1, p2, t);
let diff = bt - q;
let numerator = diff.dot(bt_d);
let denominator = bt_d.dot(bt_d) + diff.dot(b_dd);
if denominator.abs() < 1e-10 {
break;
}
t -= numerator / denominator;
t = t.clamp(0.0, 1.0);
}
t
}
#[inline]
pub fn modifier_sweep_bezier(p: Vec3, p0: Vec2, p1: Vec2, p2: Vec2) -> Vec3 {
let q = Vec2::new(p.x, p.z);
let t = closest_t_on_bezier(q, p0, p1, p2);
let closest = bezier_eval(p0, p1, p2, t);
let d = (q - closest).length();
Vec3::new(d, p.y, 0.0)
}
#[allow(clippy::too_many_arguments)]
#[inline]
pub fn sweep_bezier_dist_y(
px: f32,
py: f32,
pz: f32,
p0x: f32,
p0z: f32,
p1x: f32,
p1z: f32,
p2x: f32,
p2z: f32,
) -> (f32, f32) {
let p0 = Vec2::new(p0x, p0z);
let p1 = Vec2::new(p1x, p1z);
let p2 = Vec2::new(p2x, p2z);
let q = Vec2::new(px, pz);
let t = closest_t_on_bezier(q, p0, p1, p2);
let closest = bezier_eval(p0, p1, p2, t);
let d = (q - closest).length();
(d, py)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bezier_eval_endpoints() {
let p0 = Vec2::new(0.0, 0.0);
let p1 = Vec2::new(0.5, 1.0);
let p2 = Vec2::new(1.0, 0.0);
let b0 = bezier_eval(p0, p1, p2, 0.0);
let b1 = bezier_eval(p0, p1, p2, 1.0);
assert!((b0 - p0).length() < 1e-6);
assert!((b1 - p2).length() < 1e-6);
}
#[test]
fn test_closest_t_at_control_points() {
let p0 = Vec2::new(0.0, 0.0);
let p1 = Vec2::new(0.5, 1.0);
let p2 = Vec2::new(1.0, 0.0);
let t = closest_t_on_bezier(p0, p0, p1, p2);
assert!(t < 0.05, "t at start should be ~0, got {}", t);
let t = closest_t_on_bezier(p2, p0, p1, p2);
assert!(t > 0.95, "t at end should be ~1, got {}", t);
}
#[test]
fn test_sweep_straight_line() {
let p0 = Vec2::new(-1.0, 0.0);
let p1 = Vec2::new(0.0, 0.0);
let p2 = Vec2::new(1.0, 0.0);
let result = modifier_sweep_bezier(Vec3::new(0.0, 2.0, 0.5), p0, p1, p2);
assert!(
(result.x - 0.5).abs() < 0.05,
"perp dist should be ~0.5, got {}",
result.x
);
assert!((result.y - 2.0).abs() < 1e-6, "y should be preserved");
}
#[test]
fn test_sweep_on_curve() {
let p0 = Vec2::new(0.0, 0.0);
let p1 = Vec2::new(0.5, 1.0);
let p2 = Vec2::new(1.0, 0.0);
let mid = bezier_eval(p0, p1, p2, 0.5);
let result = modifier_sweep_bezier(Vec3::new(mid.x, 3.0, mid.y), p0, p1, p2);
assert!(
result.x < 0.01,
"perp dist on curve should be ~0, got {}",
result.x
);
}
}