use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
use ogeom_geom::{Surface, SurfaceGeometry};
use ogeom_math::Point;
use ogeom_intersect::Traced;
#[derive(Debug, Clone, PartialEq)]
pub struct Coverage {
pub crossings: usize,
pub covered: usize,
pub missed: Vec<Point>,
pub grid: usize,
}
impl Coverage {
#[must_use]
pub const fn complete(&self) -> bool {
self.missed.is_empty()
}
#[must_use]
pub fn fraction(&self) -> f64 {
if self.crossings == 0 {
return 1.0;
}
#[allow(clippy::cast_precision_loss)]
{
self.covered as f64 / self.crossings as f64
}
}
}
pub fn coverage(
a: &SurfaceGeometry,
b: &SurfaceGeometry,
branches: &[Traced],
grid: usize,
tol: Tolerances,
) -> OgeomResult<Coverage> {
if grid < 2 {
ogeom_bail!(Construction, "a coverage grid needs at least two steps");
}
if signed_distance(b, Point::ORIGIN).is_none() {
ogeom_bail!(
NotDone,
"the second surface has no signed distance, so there is no sign to \
change and this cannot measure completeness for the pair. That is \
a limit of the instrument, not a finding about the intersector"
);
}
let ((ua, ub), (va, vb)) = bounded(a);
#[allow(clippy::cast_precision_loss)]
let n = grid as f64;
let mut crossings = 0;
let mut covered = 0;
let mut missed = Vec::new();
for i in 0..grid {
for j in 0..grid {
#[allow(clippy::cast_precision_loss)]
let (s0, s1) = (i as f64 / n, (i + 1) as f64 / n);
#[allow(clippy::cast_precision_loss)]
let (t0, t1) = (j as f64 / n, (j + 1) as f64 / n);
let at = |s: f64, t: f64| a.point_at(ua + (ub - ua) * s, va + (vb - va) * t, tol).ok();
let corners: Vec<Point> = [(s0, t0), (s1, t0), (s1, t1), (s0, t1)]
.iter()
.filter_map(|(s, t)| at(*s, *t))
.collect();
if corners.len() < 4 {
continue;
}
let signs: Vec<f64> = corners
.iter()
.filter_map(|p| signed_distance(b, *p))
.collect();
if signs.len() < 4 {
continue;
}
let positive = signs.iter().any(|d| *d > 0.0);
let negative = signs.iter().any(|d| *d < 0.0);
if !(positive && negative) {
continue;
}
crossings += 1;
let centre = Point::from_vector(
corners
.iter()
.fold(ogeom_math::Vector::ZERO, |acc, p| acc + p.to_vector())
* 0.25,
);
let reach = corners[0]
.distance(corners[2])
.max(corners[1].distance(corners[3]));
let reached = branches.iter().any(|branch| {
branch
.points
.windows(2)
.any(|pair| segment_distance(centre, pair[0], pair[1]) <= reach)
|| branch
.points
.first()
.is_some_and(|p| p.distance(centre) <= reach)
});
if reached {
covered += 1;
} else {
missed.push(centre);
}
}
}
Ok(Coverage {
crossings,
covered,
missed,
grid,
})
}
fn segment_distance(p: Point, a: Point, b: Point) -> f64 {
let along = b - a;
let length = along.square_magnitude();
if length <= f64::MIN_POSITIVE {
return p.distance(a);
}
let t = ((p - a).dot(along) / length).clamp(0.0, 1.0);
p.distance(a + along * t)
}
fn bounded(surface: &SurfaceGeometry) -> ((f64, f64), (f64, f64)) {
let ((ua, ub), (va, vb)) = surface.domain();
let limit = 1.0e6;
(
(ua.max(-limit), ub.min(limit)),
(va.max(-limit), vb.min(limit)),
)
}
fn signed_distance(surface: &SurfaceGeometry, p: Point) -> Option<f64> {
use SurfaceGeometry as S;
match surface {
S::Plane(x) => Some(x.plane().signed_distance_to(p)),
S::Sphere(x) => Some(x.sphere().signed_distance_to(p)),
S::Cylinder(x) => Some(x.cylinder().signed_distance_to(p)),
_ => None,
}
}