use ogeom_core::{OgeomResult, Tolerances};
use ogeom_geom::{Curve, Curve3d, Surface, SurfaceGeometry};
use ogeom_math::Point;
use ogeom_intersect::{Meeting, surface_surface};
#[derive(Debug, Clone, PartialEq)]
pub struct Measured {
pub meeting: Meeting,
pub deviation: Option<f64>,
pub samples: usize,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Report {
pub cases: usize,
pub solved: usize,
pub deferred: usize,
pub worst: f64,
pub worst_case: Option<String>,
}
impl Report {
#[must_use]
pub fn within(&self, bar: f64) -> bool {
self.worst <= bar
}
}
pub fn measure(a: &SurfaceGeometry, b: &SurfaceGeometry, tol: Tolerances) -> OgeomResult<Measured> {
const SAMPLES: usize = 64;
let meeting = surface_surface(a, b, tol)?;
let mut worst: Option<f64> = None;
let mut samples = 0;
let mut check = |p: Point| {
let off = distance_to(a, p, tol).max(distance_to(b, p, tol));
worst = Some(worst.map_or(off, |w: f64| w.max(off)));
samples += 1;
};
match &meeting {
Meeting::Along(curves) => {
for curve in curves {
let (lo, hi) = sampling_range(curve);
for i in 0..=SAMPLES {
#[allow(clippy::cast_precision_loss)]
let u = lo + (hi - lo) * i as f64 / SAMPLES as f64;
if let Ok(p) = curve.point_at(u, tol) {
check(p);
}
}
}
}
Meeting::Touching(points) => {
for p in points {
check(*p);
}
}
Meeting::Apart | Meeting::Same => {}
}
Ok(Measured {
meeting,
deviation: worst,
samples,
})
}
pub fn measure_all(
cases: &[(String, SurfaceGeometry, SurfaceGeometry)],
tol: Tolerances,
) -> Report {
let mut report = Report {
cases: cases.len(),
..Report::default()
};
for (name, a, b) in cases {
match measure(a, b, tol) {
Ok(found) => {
report.solved += 1;
if let Some(off) = found.deviation
&& off > report.worst
{
report.worst = off;
report.worst_case = Some(name.clone());
}
}
Err(_) => report.deferred += 1,
}
}
report
}
fn sampling_range(curve: &Curve) -> (f64, f64) {
let (lo, hi) = curve.domain();
if matches!(curve, Curve::Line(_)) {
return (-100.0, 100.0);
}
(lo, hi)
}
fn distance_to(surface: &SurfaceGeometry, p: Point, tol: Tolerances) -> f64 {
use SurfaceGeometry as S;
match surface {
S::Plane(x) => x.plane().distance_to(p),
S::Sphere(x) => x.sphere().distance_to(p),
S::Cylinder(x) => x.cylinder().distance_to(p),
S::Cone(x) => x.cone().distance_to(p),
S::Torus(x) => x.torus().distance_to(p),
other => nearest_on(other, p, tol),
}
}
fn nearest_on(surface: &SurfaceGeometry, p: Point, tol: Tolerances) -> f64 {
const STEPS: usize = 128;
let ((ua, ub), (va, vb)) = surface.domain();
let mut best = f64::MAX;
for i in 0..=STEPS {
for j in 0..=STEPS {
#[allow(clippy::cast_precision_loss)]
let (s, t) = (i as f64 / STEPS as f64, j as f64 / STEPS as f64);
if let Ok(q) = surface.point_at(ua + (ub - ua) * s, va + (vb - va) * t, tol) {
best = best.min(p.distance(q));
}
}
}
best
}