use crate::vec::{Point2, Point3};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Orientation {
CounterClockwise,
Clockwise,
Collinear,
}
const fn to_coord(p: Point2) -> robust::Coord<f64> {
robust::Coord { x: p.x(), y: p.y() }
}
#[inline]
#[must_use]
pub fn orient2d(a: Point2, b: Point2, c: Point2) -> f64 {
robust::orient2d(to_coord(a), to_coord(b), to_coord(c))
}
#[must_use]
pub fn orientation2d(a: Point2, b: Point2, c: Point2) -> Orientation {
let det = orient2d(a, b, c);
if det > 0.0 {
Orientation::CounterClockwise
} else if det < 0.0 {
Orientation::Clockwise
} else {
Orientation::Collinear
}
}
#[must_use]
pub fn in_circle(a: Point2, b: Point2, c: Point2, d: Point2) -> f64 {
robust::incircle(to_coord(a), to_coord(b), to_coord(c), to_coord(d))
}
#[must_use]
pub fn winding_number(point: Point2, polygon: &[Point2]) -> i32 {
let n = polygon.len();
if n < 3 {
return 0;
}
let mut wn = 0i32;
for i in 0..n {
let j = (i + 1) % n;
let vi = polygon[i];
let vj = polygon[j];
if vi.y() <= point.y() {
if vj.y() > point.y() {
if orient2d(vi, vj, point) > 0.0 {
wn += 1;
}
}
} else if vj.y() <= point.y() {
if orient2d(vi, vj, point) < 0.0 {
wn -= 1;
}
}
}
wn
}
#[must_use]
pub fn point_in_polygon(point: Point2, polygon: &[Point2]) -> bool {
winding_number(point, polygon) != 0
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Orientation3D {
Above,
Below,
Coplanar,
}
const fn to_coord3d(p: Point3) -> robust::Coord3D<f64> {
robust::Coord3D {
x: p.x(),
y: p.y(),
z: p.z(),
}
}
#[inline]
#[must_use]
pub fn orient3d(a: Point3, b: Point3, c: Point3, d: Point3) -> f64 {
robust::orient3d(to_coord3d(a), to_coord3d(b), to_coord3d(c), to_coord3d(d))
}
#[must_use]
pub fn orientation3d(a: Point3, b: Point3, c: Point3, d: Point3) -> Orientation3D {
let det = orient3d(a, b, c, d);
if det > 0.0 {
Orientation3D::Below
} else if det < 0.0 {
Orientation3D::Above
} else {
Orientation3D::Coplanar
}
}
#[must_use]
#[allow(clippy::many_single_char_names)]
pub fn insphere(a: Point3, b: Point3, c: Point3, d: Point3, e: Point3) -> f64 {
robust::insphere(
to_coord3d(a),
to_coord3d(b),
to_coord3d(c),
to_coord3d(d),
to_coord3d(e),
)
}
#[allow(clippy::float_cmp)]
#[must_use]
pub fn orient2d_sos(a: Point2, b: Point2, c: Point2, ia: usize, ib: usize, ic: usize) -> f64 {
let det = orient2d(a, b, c);
if det != 0.0 {
return det;
}
let max_idx = ia.max(ib).max(ic);
if max_idx == ic {
1.0 } else if max_idx == ib {
-1.0 } else {
1.0 }
}
#[allow(clippy::float_cmp)]
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn orient3d_sos(
a: Point3,
b: Point3,
c: Point3,
d: Point3,
ia: usize,
ib: usize,
ic: usize,
id: usize,
) -> f64 {
let det = orient3d(a, b, c, d);
if det != 0.0 {
return det;
}
let max_idx = ia.max(ib).max(ic).max(id);
if max_idx == id {
1.0 } else if max_idx == ic {
-1.0 } else if max_idx == ib {
1.0 } else {
-1.0 }
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
#[test]
fn orient2d_ccw() {
let a = Point2::new(0.0, 0.0);
let b = Point2::new(1.0, 0.0);
let c = Point2::new(0.0, 1.0);
assert!(orient2d(a, b, c) > 0.0);
assert_eq!(orientation2d(a, b, c), Orientation::CounterClockwise);
}
#[test]
fn orient2d_cw() {
let a = Point2::new(0.0, 0.0);
let b = Point2::new(0.0, 1.0);
let c = Point2::new(1.0, 0.0);
assert!(orient2d(a, b, c) < 0.0);
assert_eq!(orientation2d(a, b, c), Orientation::Clockwise);
}
#[test]
fn orient2d_collinear() {
let a = Point2::new(0.0, 0.0);
let b = Point2::new(1.0, 1.0);
let c = Point2::new(2.0, 2.0);
assert_eq!(orient2d(a, b, c), 0.0);
assert_eq!(orientation2d(a, b, c), Orientation::Collinear);
}
#[test]
fn orient2d_swap_reverses_sign() {
let a = Point2::new(0.0, 0.0);
let b = Point2::new(1.0, 0.0);
let c = Point2::new(0.5, 1.0);
let d1 = orient2d(a, b, c);
let d2 = orient2d(b, a, c);
assert!((d1 + d2).abs() < 1e-15, "swap should reverse sign");
}
#[test]
fn orient3d_basic() {
let a = Point3::new(0.0, 0.0, 0.0);
let b = Point3::new(1.0, 0.0, 0.0);
let c = Point3::new(0.0, 1.0, 0.0);
let above = Point3::new(0.0, 0.0, 1.0);
let below = Point3::new(0.0, 0.0, -1.0);
let on = Point3::new(0.5, 0.5, 0.0);
assert!(orient3d(a, b, c, above) < 0.0); assert!(orient3d(a, b, c, below) > 0.0); assert_eq!(orient3d(a, b, c, on), 0.0);
assert_eq!(orientation3d(a, b, c, above), Orientation3D::Above);
assert_eq!(orientation3d(a, b, c, below), Orientation3D::Below);
assert_eq!(orientation3d(a, b, c, on), Orientation3D::Coplanar);
}
#[test]
fn insphere_inside() {
let a = Point3::new(1.0, 0.0, 0.0);
let b = Point3::new(0.0, 1.0, 0.0);
let c = Point3::new(0.0, 0.0, 1.0);
let d = Point3::new(0.0, 0.0, 0.0);
let center = Point3::new(0.25, 0.25, 0.25);
let result = insphere(a, b, c, d, center);
assert!(result.is_finite());
}
#[test]
fn winding_number_square() {
let square = vec![
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
Point2::new(1.0, 1.0),
Point2::new(0.0, 1.0),
];
assert_eq!(winding_number(Point2::new(0.5, 0.5), &square), 1);
assert!(point_in_polygon(Point2::new(0.5, 0.5), &square));
assert_eq!(winding_number(Point2::new(2.0, 2.0), &square), 0);
assert!(!point_in_polygon(Point2::new(2.0, 2.0), &square));
}
#[test]
fn winding_number_triangle() {
let tri = vec![
Point2::new(0.0, 0.0),
Point2::new(4.0, 0.0),
Point2::new(2.0, 3.0),
];
assert!(point_in_polygon(Point2::new(2.0, 1.0), &tri));
assert!(!point_in_polygon(Point2::new(5.0, 0.0), &tri));
}
#[test]
fn winding_number_degenerate() {
assert_eq!(winding_number(Point2::new(0.0, 0.0), &[]), 0);
assert_eq!(
winding_number(
Point2::new(0.0, 0.0),
&[Point2::new(0.0, 0.0), Point2::new(1.0, 1.0)]
),
0
);
}
use proptest::prelude::*;
proptest! {
#[test]
fn prop_orient2d_swap_sign(
ax in -10.0f64..10.0, ay in -10.0f64..10.0,
bx in -10.0f64..10.0, by in -10.0f64..10.0,
cx in -10.0f64..10.0, cy in -10.0f64..10.0,
) {
let a = Point2::new(ax, ay);
let b = Point2::new(bx, by);
let c = Point2::new(cx, cy);
let d1 = orient2d(a, b, c);
let d2 = orient2d(b, a, c);
prop_assert!((d1 + d2).abs() < 1e-10, "d1={}, d2={}", d1, d2);
}
}
#[test]
fn orient2d_sos_never_zero() {
let a = Point2::new(0.0, 0.0);
let b = Point2::new(1.0, 1.0);
let c = Point2::new(2.0, 2.0);
assert_eq!(orient2d(a, b, c), 0.0);
assert_ne!(orient2d_sos(a, b, c, 0, 1, 2), 0.0);
}
#[test]
fn orient2d_sos_consistent() {
let a = Point2::new(0.0, 0.0);
let b = Point2::new(1.0, 1.0);
let c = Point2::new(2.0, 2.0);
let s1 = orient2d_sos(a, b, c, 0, 1, 2);
let s2 = orient2d_sos(a, b, c, 0, 1, 2);
assert_eq!(s1.signum(), s2.signum());
}
#[test]
fn orient3d_sos_never_zero() {
let a = Point3::new(0.0, 0.0, 0.0);
let b = Point3::new(1.0, 0.0, 0.0);
let c = Point3::new(0.0, 1.0, 0.0);
let d = Point3::new(0.5, 0.5, 0.0);
assert_eq!(orient3d(a, b, c, d), 0.0);
assert_ne!(orient3d_sos(a, b, c, d, 0, 1, 2, 3), 0.0);
}
#[test]
fn orient3d_sos_passes_through_nonzero() {
let a = Point3::new(0.0, 0.0, 0.0);
let b = Point3::new(1.0, 0.0, 0.0);
let c = Point3::new(0.0, 1.0, 0.0);
let d = Point3::new(0.0, 0.0, 1.0);
let exact = orient3d(a, b, c, d);
let sos = orient3d_sos(a, b, c, d, 0, 1, 2, 3);
assert_eq!(exact.signum(), sos.signum());
}
}