use brepkit_math::traits::ParametricCurve;
use brepkit_math::vec::{Point3, Vec3};
const MAX_DEPTH: u32 = 20;
fn chord_deviation(p: Point3, a: Point3, b: Point3) -> f64 {
let ab: Vec3 = b - a;
let ab_len = ab.length();
if ab_len < f64::EPSILON {
return 0.0;
}
let ap: Vec3 = p - a;
ap.cross(ab).length() / ab_len
}
#[allow(clippy::too_many_arguments)]
fn subdivide<C: ParametricCurve>(
curve: &C,
t_a: f64,
p_a: Point3,
t_b: f64,
p_b: Point3,
max_deflection: f64,
depth: u32,
out: &mut Vec<(f64, Point3)>,
) {
if depth >= MAX_DEPTH {
return;
}
let t_m = 0.5 * (t_a + t_b);
let p_m = curve.evaluate(t_m);
if chord_deviation(p_m, p_a, p_b) <= max_deflection {
return;
}
subdivide(curve, t_a, p_a, t_m, p_m, max_deflection, depth + 1, out);
out.push((t_m, p_m));
subdivide(curve, t_m, p_m, t_b, p_b, max_deflection, depth + 1, out);
}
#[must_use]
pub fn sample_deflection<C: ParametricCurve>(
curve: &C,
t_start: f64,
t_end: f64,
max_deflection: f64,
) -> Vec<(f64, Point3)> {
let p_start = curve.evaluate(t_start);
let p_end = curve.evaluate(t_end);
let mut out = Vec::new();
out.push((t_start, p_start));
if max_deflection > 0.0 {
subdivide(
curve,
t_start,
p_start,
t_end,
p_end,
max_deflection,
0,
&mut out,
);
}
out.push((t_end, p_end));
out
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use brepkit_math::curves::Circle3D;
use brepkit_math::vec::{Point3, Vec3};
use std::f64::consts::TAU;
fn circle(radius: f64) -> Circle3D {
Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), radius).unwrap()
}
#[test]
fn deflection_circle_r10_many_points() {
let c = circle(10.0);
let max_dev = 0.01;
let pairs = sample_deflection(&c, 0.0, TAU, max_dev);
assert!(
pairs.len() > 10,
"expected many points, got {}",
pairs.len()
);
}
#[test]
fn deflection_every_midpoint_within_tolerance() {
let c = circle(10.0);
let max_dev = 0.01;
let pairs = sample_deflection(&c, 0.0, TAU, max_dev);
for window in pairs.windows(2) {
let (t_a, p_a) = window[0];
let (t_b, p_b) = window[1];
let t_m = 0.5 * (t_a + t_b);
let p_m = c.evaluate(t_m);
let dev = chord_deviation(p_m, p_a, p_b);
assert!(
dev <= max_dev + 1e-12,
"chord deviation {dev} exceeds max {max_dev} between t={t_a} and t={t_b}"
);
}
}
#[test]
fn deflection_endpoints_always_included() {
let c = circle(1.0);
let pairs = sample_deflection(&c, 0.0, TAU, 0.1);
assert!(!pairs.is_empty());
assert!((pairs.first().unwrap().0 - 0.0).abs() < 1e-12);
assert!((pairs.last().unwrap().0 - TAU).abs() < 1e-12);
}
#[test]
fn non_positive_deflection_returns_two_endpoints() {
let c = circle(1.0);
let pairs = sample_deflection(&c, 0.0, TAU, 0.0);
assert_eq!(pairs.len(), 2);
let pairs_neg = sample_deflection(&c, 0.0, TAU, -1.0);
assert_eq!(pairs_neg.len(), 2);
}
#[test]
fn points_sorted_by_parameter() {
let c = circle(5.0);
let pairs = sample_deflection(&c, 0.0, TAU, 0.05);
for w in pairs.windows(2) {
assert!(
w[0].0 < w[1].0,
"parameters not sorted: {} >= {}",
w[0].0,
w[1].0
);
}
}
}