use crate::{PlanRing, ProximityError};
const CONVEXITY: f64 = 1.0e-12;
#[derive(Clone, Debug, PartialEq)]
pub struct ConvexPlanRegion {
ring: PlanRing,
}
fn cross(o: [f64; 2], a: [f64; 2], b: [f64; 2]) -> f64 {
(a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
}
fn length(a: [f64; 2], b: [f64; 2]) -> f64 {
(b[0] - a[0]).hypot(b[1] - a[1])
}
fn segment_distance(point: [f64; 2], a: [f64; 2], b: [f64; 2]) -> f64 {
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
let squared = dx * dx + dy * dy;
let t = if squared == 0.0 {
0.0
} else {
(((point[0] - a[0]) * dx + (point[1] - a[1]) * dy) / squared).clamp(0.0, 1.0)
};
(point[0] - (a[0] + t * dx)).hypot(point[1] - (a[1] + t * dy))
}
impl ConvexPlanRegion {
pub fn try_new(ring: PlanRing) -> Result<Self, ProximityError> {
let count = ring.len();
if count < 3 || ring.iter().flatten().any(|c| !c.is_finite()) {
return Err(ProximityError::InvalidMeasurement);
}
let mut area = 0.0;
for index in 0..count {
let (a, b, c) = (
ring[index],
ring[(index + 1) % count],
ring[(index + 2) % count],
);
let scale = length(a, b) * length(b, c);
if cross(a, b, c) < -CONVEXITY * scale.max(f64::MIN_POSITIVE) {
return Err(ProximityError::InvalidMeasurement);
}
area += a[0] * b[1] - b[0] * a[1];
}
if area <= 0.0 {
return Err(ProximityError::InvalidMeasurement);
}
Ok(Self { ring })
}
#[must_use]
pub fn ring(&self) -> &[[f64; 2]] {
&self.ring
}
#[must_use]
pub fn bounds(&self) -> ([f64; 2], [f64; 2]) {
self.ring.iter().fold(
([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]),
|(low, high), [x, y]| {
(
[low[0].min(*x), low[1].min(*y)],
[high[0].max(*x), high[1].max(*y)],
)
},
)
}
#[must_use]
pub fn area_square_metres(&self) -> f64 {
let count = self.ring.len();
(0..count)
.map(|index| {
let (a, b) = (self.ring[index], self.ring[(index + 1) % count]);
a[0] * b[1] - b[0] * a[1]
})
.sum::<f64>()
/ 2.0
}
fn edges(&self) -> impl Iterator<Item = ([f64; 2], [f64; 2])> + '_ {
let count = self.ring.len();
(0..count).map(move |index| (self.ring[index], self.ring[(index + 1) % count]))
}
#[must_use]
pub fn separation(&self, other: &Self) -> f64 {
let mut gap = f64::NEG_INFINITY;
for (region, against) in [(self, other), (other, self)] {
for (a, b) in region.edges() {
let edge = length(a, b);
if edge == 0.0 {
continue;
}
let normal = [(b[1] - a[1]) / edge, (a[0] - b[0]) / edge];
let reach = |point: &[f64; 2]| {
(point[0] - a[0]) * normal[0] + (point[1] - a[1]) * normal[1]
};
let nearest = against.ring.iter().map(reach).fold(f64::INFINITY, f64::min);
gap = gap.max(nearest);
}
}
if gap < 0.0 {
return gap;
}
let mut distance = f64::INFINITY;
for (region, against) in [(self, other), (other, self)] {
for point in ®ion.ring {
for (a, b) in against.edges() {
distance = distance.min(segment_distance(*point, a, b));
}
}
}
distance
}
}
#[cfg(test)]
mod tests {
use super::*;
fn square(x: f64, y: f64, side: f64) -> ConvexPlanRegion {
ConvexPlanRegion::try_new(vec![
[x, y],
[x + side, y],
[x + side, y + side],
[x, y + side],
])
.unwrap()
}
#[test]
fn a_region_is_convex_and_anticlockwise() {
assert!(ConvexPlanRegion::try_new(vec![[0.0, 0.0], [1.0, 0.0]]).is_err());
assert!(
ConvexPlanRegion::try_new(vec![[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]])
.is_err()
);
assert!(
ConvexPlanRegion::try_new(vec![
[0.0, 0.0],
[2.0, 0.0],
[1.0, 0.5],
[2.0, 2.0],
[0.0, 2.0]
])
.is_err()
);
let collinear =
ConvexPlanRegion::try_new(vec![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [1.0, 1.0]])
.unwrap();
assert!((collinear.area_square_metres() - 1.0).abs() < 1e-12);
assert_eq!(square(1.0, 2.0, 1.0).bounds(), ([1.0, 2.0], [2.0, 3.0]));
}
#[test]
fn separation_is_the_distance_apart_or_the_depth_of_overlap() {
let a = square(0.0, 0.0, 1.0);
assert!((a.separation(&square(3.0, 0.0, 1.0)) - 2.0).abs() < 1e-12);
let diagonal = a.separation(&square(2.0, 2.0, 1.0));
assert!((diagonal - 2.0_f64.sqrt()).abs() < 1e-12);
assert!(a.separation(&square(1.0, 0.0, 1.0)).abs() < 1e-12);
assert!((a.separation(&square(0.75, 0.5, 1.0)) + 0.25).abs() < 1e-12);
}
}