#![expect(
clippy::cast_possible_truncation,
reason = "axis index used as array subscript"
)]
use crate::r3::PreciseVector;
use crate::s1::Angle;
use crate::s2::Point;
use crate::s2::predicates;
pub fn intersection_error() -> Angle {
Angle::from_radians(8.0 * predicates::DBL_ERROR)
}
pub fn intersection_merge_radius() -> Angle {
Angle::from_radians(16.0 * predicates::DBL_ERROR)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Crossing {
Cross,
MaybeCross,
#[default]
DoNotCross,
}
impl std::fmt::Display for Crossing {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Crossing::Cross => write!(f, "Cross"),
Crossing::MaybeCross => write!(f, "MaybeCross"),
Crossing::DoNotCross => write!(f, "DoNotCross"),
}
}
}
#[inline]
pub fn crossing_sign(a: Point, b: Point, c: Point, d: Point) -> Crossing {
let mut crosser = super::edge_crosser::EdgeCrosser::new(a, b);
crosser.restart_at(c);
crosser.chain_crossing_sign(d)
}
#[inline]
pub fn vertex_crossing(a: Point, b: Point, c: Point, d: Point) -> bool {
if a == b || c == d {
return false;
}
if a == c {
return (b == d) || predicates::ordered_ccw(a.reference_dir(), d, b, a);
}
if b == d {
return predicates::ordered_ccw(b.reference_dir(), c, a, b);
}
if a == d {
return (b == c) || predicates::ordered_ccw(a.reference_dir(), c, b, a);
}
if b == c {
return predicates::ordered_ccw(b.reference_dir(), d, a, b);
}
false
}
#[inline]
pub fn edge_or_vertex_crossing(a: Point, b: Point, c: Point, d: Point) -> bool {
match crossing_sign(a, b, c, d) {
Crossing::DoNotCross => false,
Crossing::Cross => true,
Crossing::MaybeCross => vertex_crossing(a, b, c, d),
}
}
pub fn intersection(a0: Point, a1: Point, b0: Point, b1: Point) -> Point {
let (pt, ok) = intersection_stable(a0, a1, b0, b1);
let pt = if ok {
pt
} else {
intersection_exact(a0, a1, b0, b1)
};
if pt.0.dot((a0.0 + a1.0) + (b0.0 + b1.0)) < 0.0 {
Point(-pt.0)
} else {
pt
}
}
#[inline]
pub fn angle_contains_vertex(a: Point, b: Point, c: Point) -> bool {
debug_assert!(a != b && b != c);
!predicates::ordered_ccw(b.reference_dir(), c, a, b)
}
#[inline]
pub fn signed_vertex_crossing(a: Point, b: Point, c: Point, d: Point) -> i32 {
if a == b || c == d {
return 0;
}
if a == c {
return if (b == d) || predicates::ordered_ccw(a.reference_dir(), d, b, a) {
1
} else {
0
};
}
if b == d {
return if predicates::ordered_ccw(b.reference_dir(), c, a, b) {
1
} else {
0
};
}
if a == d {
return if (b == c) || predicates::ordered_ccw(a.reference_dir(), c, b, a) {
-1
} else {
0
};
}
if b == c {
return if predicates::ordered_ccw(b.reference_dir(), d, a, b) {
-1
} else {
0
};
}
0 }
pub const ROBUST_CROSS_PROD_ERROR: Angle = Angle::from_radians(6.0 * predicates::DBL_ERROR);
pub const EXACT_CROSS_PROD_ERROR: Angle = Angle::from_radians(predicates::DBL_ERROR);
pub fn robust_cross_prod(a: Point, b: Point) -> Point {
if let Some(result) = get_stable_cross_prod(a.0, b.0) {
return Point(result);
}
if a == b {
return super::point::ortho(a);
}
Point(exact_cross_prod(a, b))
}
fn get_stable_cross_prod(a: crate::r3::Vector, b: crate::r3::Vector) -> Option<crate::r3::Vector> {
const T_ERR: f64 = predicates::DBL_ERROR;
const ROBUST_ERR: f64 = 6.0 * predicates::DBL_ERROR; const MIN_NORM: f64 = (32.0 * predicates::SQRT3 * predicates::DBL_ERROR)
/ (ROBUST_ERR / T_ERR - (1.0 + 2.0 * predicates::SQRT3));
let result = (a - b).cross(a + b);
if result.norm2() >= MIN_NORM * MIN_NORM {
Some(result)
} else {
None
}
}
#[inline]
fn is_normalizable(p: crate::r3::Vector) -> bool {
let max_comp = p.x.abs().max(p.y.abs()).max(p.z.abs());
max_comp >= f64::from_bits(0x30D0_0000_0000_0000) }
fn ensure_normalizable(p: crate::r3::Vector) -> crate::r3::Vector {
if is_normalizable(p) {
return p;
}
let p_max = p.x.abs().max(p.y.abs()).max(p.z.abs());
debug_assert!(p_max > 0.0, "EnsureNormalizable: zero vector");
let ilog = if p_max == 0.0 {
0
} else {
(p_max.log2().floor()) as i32
};
let scale = (2.0f64).powi(-1 - ilog);
p * scale
}
fn normalizable_from_exact(pv: &PreciseVector) -> crate::r3::Vector {
use crate::r3::Vector;
let x = pv.to_vector();
if is_normalizable(x) {
return x;
}
let xf = [&pv.x, &pv.y, &pv.z];
let mut max_exp = i64::MIN;
for c in &xf {
if !c.is_zero() {
max_exp = max_exp.max(c.exp());
}
}
if max_exp == i64::MIN {
return Vector::new(0.0, 0.0, 0.0); }
Vector::new(
pv.x.to_f64_shifted(max_exp),
pv.y.to_f64_shifted(max_exp),
pv.z.to_f64_shifted(max_exp),
)
}
fn symbolic_cross_prod_sorted(a: Point, b: Point) -> crate::r3::Vector {
use crate::r3::Vector;
debug_assert!(a < b, "SymbolicCrossProdSorted: a must be < b");
if b.0.y != 0.0 || b.0.x != 0.0 {
return Vector::new(-b.0.y, b.0.x, 0.0);
}
if b.0.z != 0.0 {
return Vector::new(b.0.z, 0.0, 0.0);
}
if a.0.y != 0.0 || a.0.x != 0.0 {
return Vector::new(a.0.y, -a.0.x, 0.0);
}
Vector::new(1.0, 0.0, 0.0)
}
fn symbolic_cross_prod(a: Point, b: Point) -> crate::r3::Vector {
debug_assert!(a != b, "SymbolicCrossProd: a == b");
if a < b {
ensure_normalizable(symbolic_cross_prod_sorted(a, b))
} else {
-ensure_normalizable(symbolic_cross_prod_sorted(b, a))
}
}
pub(crate) fn exact_cross_prod(a: Point, b: Point) -> crate::r3::Vector {
debug_assert!(a != b, "ExactCrossProd: a == b");
let ap = PreciseVector::from_vector(a.0);
let bp = PreciseVector::from_vector(b.0);
let result = ap.cross(&bp);
if !result.is_zero() {
return normalizable_from_exact(&result);
}
symbolic_cross_prod(a, b)
}
fn robust_normal_with_length(
x: crate::r3::Vector,
y: crate::r3::Vector,
) -> (crate::r3::Vector, f64) {
let tmp = (x - y).cross(x + y);
let len = tmp.norm();
if len == 0.0 {
(tmp, 0.0)
} else {
(tmp * (1.0 / len), 0.5 * len)
}
}
fn projection(
x: crate::r3::Vector,
a_norm: crate::r3::Vector,
a_norm_len: f64,
a0: Point,
a1: Point,
) -> (f64, f64) {
let proj = x.dot(a_norm) / a_norm_len;
let bound = ((x.dot((a0.0 + a1.0) * 0.5)).abs()
+ x.norm() * a_norm_len * predicates::DBL_ERROR)
/ a_norm_len;
(proj, bound)
}
fn intersection_stable(a0: Point, a1: Point, b0: Point, b1: Point) -> (Point, bool) {
let (a_norm, a_norm_len) = robust_normal_with_length(a0.0, a1.0);
let (b_norm, b_norm_len) = robust_normal_with_length(b0.0, b1.0);
if a_norm_len == 0.0 || b_norm_len == 0.0 {
return (Point::from_coords(0.0, 0.0, 0.0), false);
}
if a_norm_len >= b_norm_len {
intersection_stable_sorted(b0, b1, a0, a1, &a_norm, a_norm_len)
} else {
intersection_stable_sorted(a0, a1, b0, b1, &b_norm, b_norm_len)
}
}
fn intersection_stable_sorted(
a0: Point,
a1: Point,
b0: Point,
b1: Point,
b_norm: &crate::r3::Vector,
b_norm_len: f64,
) -> (Point, bool) {
let (proj0, bound0) = projection(a0.0, *b_norm, b_norm_len, b0, b1);
let (proj1, bound1) = projection(a1.0, *b_norm, b_norm_len, b0, b1);
if proj0 * proj1 >= 0.0 {
return (Point::from_coords(0.0, 0.0, 0.0), false);
}
let t = proj0 / (proj0 - proj1);
let x = a0.0 * (1.0 - t) + a1.0 * t;
let err = bound0 + bound1;
if (proj0 - proj1).abs() < err {
return (Point(x.normalize()), false);
}
(Point(x.normalize()), true)
}
fn intersection_exact(a0: Point, a1: Point, b0: Point, b1: Point) -> Point {
let pa0 = PreciseVector::from_vector(a0.0);
let pa1 = PreciseVector::from_vector(a1.0);
let pb0 = PreciseVector::from_vector(b0.0);
let pb1 = PreciseVector::from_vector(b1.0);
let a_norm = pa0.cross(&pa1);
let b_norm = pb0.cross(&pb1);
let x = a_norm.cross(&b_norm);
if x.is_zero() {
let endpoints = [a0, a1, b0, b1];
let mut best = endpoints[0];
for &p in &endpoints[1..] {
if p.cmp_point(best) == std::cmp::Ordering::Less {
best = p;
}
}
return best.normalize();
}
let v = x.to_vector();
let sign = predicates::robust_sign(a0, a1, b1);
let s = f64::from(sign as i8);
Point((v * s).normalize())
}
#[cfg(test)]
mod tests {
use super::*;
fn is_send_sync<T: Sized + Send + Sync + Unpin>() {}
#[test]
fn crossing_is_send_sync() {
is_send_sync::<Crossing>();
}
#[test]
fn test_crossing_sign_basic() {
let a = Point::from_coords(1.0, 0.0, 1.0).normalize();
let b = Point::from_coords(-1.0, 0.0, 1.0).normalize();
let c = Point::from_coords(0.0, 1.0, 1.0).normalize();
let d = Point::from_coords(0.0, -1.0, 1.0).normalize();
assert_eq!(crossing_sign(a, b, c, d), Crossing::Cross);
}
#[test]
fn test_crossing_sign_no_cross() {
let a = Point::from_coords(1.0, 0.0, 1.0).normalize();
let b = Point::from_coords(0.0, 1.0, 1.0).normalize();
let c = Point::from_coords(-1.0, 0.0, 1.0).normalize();
let d = Point::from_coords(0.0, -1.0, 1.0).normalize();
assert_ne!(crossing_sign(a, b, c, d), Crossing::Cross);
}
#[test]
fn test_crossing_sign_shared_vertex() {
let a = Point::from_coords(1.0, 0.0, 0.0);
let b = Point::from_coords(0.0, 1.0, 0.0);
assert_eq!(crossing_sign(a, b, a, b), Crossing::MaybeCross);
}
#[test]
fn test_vertex_crossing() {
let a = Point::from_coords(1.0, 0.0, 0.0);
let b = Point::from_coords(0.0, 1.0, 0.0);
assert!(vertex_crossing(a, b, a, b));
assert!(vertex_crossing(a, b, b, a));
}
#[test]
fn test_vertex_crossing_degenerate() {
let a = Point::from_coords(1.0, 0.0, 0.0);
let b = Point::from_coords(0.0, 1.0, 0.0);
assert!(!vertex_crossing(a, a, a, b));
assert!(!vertex_crossing(a, b, a, a));
}
#[test]
fn test_edge_or_vertex_crossing() {
let a = Point::from_coords(1.0, 0.0, 1.0).normalize();
let b = Point::from_coords(-1.0, 0.0, 1.0).normalize();
let c = Point::from_coords(0.0, 1.0, 1.0).normalize();
let d = Point::from_coords(0.0, -1.0, 1.0).normalize();
assert!(edge_or_vertex_crossing(a, b, c, d));
}
#[test]
fn test_intersection() {
let a0 = Point::from_coords(1.0, 0.0, 1.0).normalize();
let a1 = Point::from_coords(-1.0, 0.0, 1.0).normalize();
let b0 = Point::from_coords(0.0, 1.0, 1.0).normalize();
let b1 = Point::from_coords(0.0, -1.0, 1.0).normalize();
let x = intersection(a0, a1, b0, b1);
assert!(x.0.z > 0.9, "intersection z = {} should be near 1.0", x.0.z);
assert!(
x.0.x.abs() < 0.1,
"intersection x = {} should be near 0.0",
x.0.x
);
assert!(
x.0.y.abs() < 0.1,
"intersection y = {} should be near 0.0",
x.0.y
);
}
#[test]
fn test_crossing_symmetry() {
let a = Point::from_coords(1.0, 0.0, 1.0).normalize();
let b = Point::from_coords(-1.0, 0.0, 1.0).normalize();
let c = Point::from_coords(0.0, 1.0, 1.0).normalize();
let d = Point::from_coords(0.0, -1.0, 1.0).normalize();
assert_eq!(crossing_sign(a, b, c, d), crossing_sign(c, d, a, b));
}
#[test]
fn test_signed_vertex_crossing_same_edge() {
let a = Point::from_coords(1.0, 0.0, 0.0);
let b = Point::from_coords(0.0, 1.0, 0.0);
assert_eq!(signed_vertex_crossing(a, b, a, b), 1);
}
#[test]
fn test_signed_vertex_crossing_reversed_edge() {
let a = Point::from_coords(1.0, 0.0, 0.0);
let b = Point::from_coords(0.0, 1.0, 0.0);
assert_eq!(signed_vertex_crossing(a, b, b, a), -1);
}
#[test]
fn test_signed_vertex_crossing_degenerate() {
let a = Point::from_coords(1.0, 0.0, 0.0);
let b = Point::from_coords(0.0, 1.0, 0.0);
assert_eq!(signed_vertex_crossing(a, a, a, b), 0);
assert_eq!(signed_vertex_crossing(a, b, a, a), 0);
}
#[test]
fn test_signed_vertex_crossing_no_shared_vertex() {
let a = Point::from_coords(1.0, 0.0, 0.0);
let b = Point::from_coords(0.0, 1.0, 0.0);
let c = Point::from_coords(0.0, 0.0, 1.0);
let d = Point::from_coords(-1.0, 0.0, 0.0);
assert_eq!(signed_vertex_crossing(a, b, c, d), 0);
}
#[test]
fn test_angle_contains_vertex_basic() {
let a = Point::from_coords(1.0, 0.0, 0.0);
let b = Point::from_coords(0.0, 1.0, 0.0);
let ref_b = b.reference_dir();
assert!(!angle_contains_vertex(a, b, a));
assert!(angle_contains_vertex(ref_b, b, a));
assert!(!angle_contains_vertex(a, b, ref_b));
}
#[test]
fn test_angle_contains_vertex_opposite() {
let a = Point::from_coords(1.0, 0.0, 0.0);
let b = Point::from_coords(0.0, 1.0, 0.0);
let c = Point::from_coords(0.0, 0.0, 1.0);
let fwd = angle_contains_vertex(a, b, c);
let rev = angle_contains_vertex(c, b, a);
assert_ne!(fwd, rev, "angle_contains_vertex should be antisymmetric");
}
#[test]
fn test_exact_intersection_underflow() {
let a0 = Point::from_coords(1.0, 0.0, 0.0);
let a1 = Point::from_coords(1.0, 2e-300, 0.0);
let b0 = Point::from_coords(1.0, 1e-300, 0.0);
let b1 = Point::from_coords(1.0, 3e-300, 0.0);
let x = intersection(a0, a1, b0, b1);
let expected = Point::from_coords(1.0, 1e-300, 0.0);
assert!(
(x.0.x - expected.0.x).abs() < 1e-10
&& (x.0.y - expected.0.y).abs() < 1e-280
&& x.0.z.abs() < 1e-280,
"intersection underflow: got {x:?}, expected near {expected:?}",
);
}
#[test]
fn test_intersection_antipodal_collinear() {
let a0 = Point::from_coords(-1.0, -1.6065916409055676e-10, 0.0);
let a1 = Point::from_coords(1.0, 0.0, 0.0);
let b0 = Point::from_coords(1.0, -4.7617930898495072e-13, 0.0);
let b1 = Point::from_coords(-1.0, 1.2678623820887328e-09, 0.0);
let x = intersection(a0, a1, b0, b1);
assert!(
x.0.x > 0.99,
"intersection should be near positive x axis, got x={}",
x.0.x,
);
}
#[test]
fn test_crossing_sign_collinear_non_overlapping() {
let a = Point::from_coords(1.0, 0.0, 0.0);
let d = Point::from_coords(0.0, 1.0, 0.0);
let ab = a.0 * 0.95 + d.0 * 0.05;
let b = Point(ab.normalize());
let cd = a.0 * 0.05 + d.0 * 0.95;
let c = Point(cd.normalize());
assert_ne!(crossing_sign(a, b, c, d), Crossing::Cross);
}
#[test]
fn test_crossing_sign_perpendicular_edges() {
let a = Point::from_coords(1.0, -1.0, 0.0).normalize();
let b = Point::from_coords(1.0, 1.0, 0.0).normalize();
let c = Point::from_coords(1.0, 0.0, -1.0).normalize();
let d = Point::from_coords(1.0, 0.0, 1.0).normalize();
assert_eq!(crossing_sign(a, b, c, d), Crossing::Cross);
assert_eq!(crossing_sign(c, d, a, b), Crossing::Cross);
}
#[test]
fn test_intersection_error_bound() {
let err = intersection_error();
assert!(err.radians() > 0.0);
assert!(err.radians() < 1e-10);
}
#[test]
fn test_intersection_merge_radius_positive() {
let r = intersection_merge_radius();
assert!(r.radians() > 0.0);
assert!(r.radians() > intersection_error().radians());
}
#[test]
fn test_exact_intersection_sign_antipodal_collinear() {
let a0 = Point::from_coords(-1.0, -1.6065916409055676e-10, 0.0);
let a1 = Point::from_coords(1.0, 0.0, 0.0);
let b0 = Point::from_coords(1.0, -4.7617930898495072e-13, 0.0);
let b1 = Point::from_coords(-1.0, 1.2678623820887328e-09, 0.0);
let x = intersection(a0, a1, b0, b1);
assert!(
x.0.x > 0.99,
"intersection should be near positive x-axis, got x={}",
x.0.x,
);
assert!(
(x.0.norm() - 1.0).abs() < 1e-10,
"intersection should be unit length, got norm={}",
x.0.norm(),
);
}
#[test]
fn test_collinear_intersection_selects_lexicographic_min() {
let a0 = Point::from_coords(1.0, 0.0, 0.0);
let a1 = Point::from_coords(1.0, 4e-300, 0.0);
let b0 = Point::from_coords(1.0, 1e-300, 0.0);
let b1 = Point::from_coords(1.0, 3e-300, 0.0);
let x = intersection(a0, a1, b0, b1);
let expected = Point::from_coords(1.0, 1e-300, 0.0).normalize();
let dist = (x.0 - expected.0).norm();
assert!(
dist < 1e-10,
"collinear best-selection: got {x:?}, expected {expected:?}, dist={dist}",
);
}
#[test]
fn test_intersection_collinear_same_direction() {
let a0 = Point::from_coords(1.0, 0.0, 0.0);
let a1 = Point::from_coords(1.0, 2e-300, 0.0);
let b0 = Point::from_coords(1.0, 1e-300, 0.0);
let b1 = Point::from_coords(1.0, 3e-300, 0.0);
let x = intersection(a0, a1, b0, b1);
assert!(x.0.norm() > 0.0, "intersection should be non-zero");
}
fn check_robust_cross_prod(a: Point, b: Point, expected_normal: Point) {
let result = robust_cross_prod(a, b);
assert!(
result.0.norm2() > 0.0,
"robust_cross_prod should be non-zero"
);
let result_n = result.normalize();
let expected_n = expected_normal.normalize();
let dist = (result_n.0 - expected_n.0).norm();
assert!(
dist < 1e-10,
"robust_cross_prod({a:?}, {b:?}) = {result_n:?}, expected ≈ {expected_n:?}, dist={dist}"
);
}
#[test]
fn test_robust_cross_prod_basic() {
check_robust_cross_prod(
Point::from_coords(1.0, 0.0, 0.0),
Point::from_coords(0.0, 1.0, 0.0),
Point::from_coords(0.0, 0.0, 1.0),
);
}
#[test]
fn test_robust_cross_prod_nearly_parallel() {
let dbl_err = predicates::DBL_ERROR;
check_robust_cross_prod(
Point::from_coords(20.0 * dbl_err, 1.0, 0.0).normalize(),
Point::from_coords(0.0, 1.0, 0.0),
Point::from_coords(0.0, 0.0, 1.0),
);
}
#[test]
fn test_robust_cross_prod_exact_path() {
let dbl_err = predicates::DBL_ERROR;
let a = Point::from_coords(4.0 * dbl_err * dbl_err, 1.0, 0.0).normalize();
let b = Point::from_coords(0.0, 1.0, 0.0);
let result = robust_cross_prod(a, b);
assert!(result.0.norm2() > 0.0);
assert!(result.normalize().0.z > 0.9);
}
#[test]
fn test_robust_cross_prod_exact_underflow() {
check_robust_cross_prod(
Point::from_coords(5e-324, 1.0, 0.0).normalize(),
Point::from_coords(0.0, 1.0, 0.0),
Point::from_coords(0.0, 0.0, 1.0),
);
}
#[test]
fn test_robust_cross_prod_exact_double_underflow() {
use crate::r3::Vector;
let dbl_err = predicates::DBL_ERROR;
let a = Point(Vector::new(5e-324, 1.0, 0.0));
let b = Point(Vector::new(5e-324, 1.0 - dbl_err, 0.0));
assert!(a != b);
let result = robust_cross_prod(a, b);
assert!(result.0.norm2() > 0.0);
assert!(
result.normalize().0.z < -0.9,
"expected -z direction, got {:?}",
result.normalize()
);
}
#[test]
fn test_robust_cross_prod_symbolic_proportional() {
use crate::r3::Vector;
let a = Point(Vector::new(1.0, 0.0, 0.0));
let b = Point(Vector::new(1.0 + f64::EPSILON, 0.0, 0.0));
assert!(a != b, "points must be different for symbolic path");
let result = robust_cross_prod(a, b);
assert!(result.0.norm2() > 0.0);
let sign = predicates::robust_sign(a, b, result.normalize());
assert_eq!(sign, predicates::Direction::CounterClockwise);
}
#[test]
fn test_robust_cross_prod_symbolic_negative_proportional() {
use crate::r3::Vector;
let a = Point(Vector::new(0.0, 1.0 + f64::EPSILON, 0.0));
let b = Point(Vector::new(0.0, 1.0, 0.0));
assert!(a != b);
let result = robust_cross_prod(a, b);
assert!(result.0.norm2() > 0.0);
let sign = predicates::robust_sign(a, b, result.normalize());
assert_eq!(sign, predicates::Direction::CounterClockwise);
}
#[test]
fn test_robust_cross_prod_symbolic_antipodal() {
use crate::r3::Vector;
let a = Point(Vector::new(0.0, 0.0, 1.0));
let b = Point(Vector::new(0.0, 0.0, -1.0));
let result = robust_cross_prod(a, b);
assert!(result.0.norm2() > 0.0);
let sign = predicates::robust_sign(a, b, result.normalize());
assert_eq!(sign, predicates::Direction::CounterClockwise);
}
#[test]
fn test_robust_cross_prod_equal_points() {
let a = Point::from_coords(1.0, 0.0, 0.0);
let result = robust_cross_prod(a, a);
assert!(
result.0.norm2() > 0.0,
"result should be non-zero for equal points"
);
assert!(
result.0.dot(a.0).abs() < 1e-14,
"result should be orthogonal to input"
);
}
#[test]
fn test_robust_cross_prod_antisymmetry() {
let a = Point::from_coords(1.0, 2.0, 3.0).normalize();
let b = Point::from_coords(4.0, 5.0, 6.0).normalize();
let ab = robust_cross_prod(a, b).normalize();
let ba = robust_cross_prod(b, a).normalize();
let diff = (ab.0 + ba.0).norm();
assert!(diff < 1e-14, "antisymmetry: diff = {diff}");
}
#[test]
fn test_exact_cross_prod_basic() {
let a = Point::from_coords(1.0, 1e-300, 0.0).normalize();
let b = Point::from_coords(1.0, 0.0, 0.0);
let result = exact_cross_prod(a, b);
assert!(result.norm2() > 0.0, "exact_cross_prod should be non-zero");
assert!(result.z < 0.0, "exact cross product should have negative z");
}
#[test]
fn test_symbolic_cross_prod_all_orderings() {
for &x in &[-1.0, 0.0, 1.0] {
for &y in &[-1.0, 0.0, 1.0] {
for &z in &[-1.0, 0.0, 1.0] {
let a = Point::from_coords(x, y, z).normalize();
if a.0.norm2() < 0.5 {
continue; }
let dbl_err = predicates::DBL_ERROR;
for &scale in &[-1.0, 1.0 - dbl_err, 1.0 + 2.0 * f64::EPSILON] {
let b = Point(a.0 * scale);
if a == b {
continue;
}
let cross = robust_cross_prod(a, b).normalize();
let sign = predicates::robust_sign(a, b, cross);
assert!(
sign == predicates::Direction::CounterClockwise,
"Sign(a={a:?}, b={b:?}, cross={cross:?}) = {sign:?}, expected CCW"
);
}
}
}
}
}
#[test]
fn test_symbolic_cross_prod_sorted_edge_cases() {
use crate::r3::Vector;
let a = Point(Vector::new(-1.0, -1.0, 0.0));
let b = Point(Vector::new(0.0, 1.0, 0.0));
assert!(a < b);
assert_eq!(
symbolic_cross_prod_sorted(a, b),
Vector::new(-1.0, 0.0, 0.0)
);
let a = Point(Vector::new(-1.0, 0.0, 0.0));
let b = Point(Vector::new(0.0, 0.0, 1.0));
assert!(a < b);
assert_eq!(symbolic_cross_prod_sorted(a, b), Vector::new(1.0, 0.0, 0.0));
let a = Point(Vector::new(-1.0, 0.0, 0.0));
let b = Point(Vector::new(0.0, 0.0, 0.0));
assert!(a < b);
assert_eq!(symbolic_cross_prod_sorted(a, b), Vector::new(0.0, 1.0, 0.0));
let a = Point(Vector::new(0.0, 0.0, -1.0));
let b = Point(Vector::new(0.0, 0.0, 0.0));
assert!(a < b);
assert_eq!(symbolic_cross_prod_sorted(a, b), Vector::new(1.0, 0.0, 0.0));
}
#[test]
fn test_robust_cross_prod_magnitude() {
use std::f64::consts::FRAC_PI_2;
let r1 = robust_cross_prod(
Point::from_coords(1.0, 0.0, 0.0),
Point::from_coords(1.0, 1e-100, 0.0),
);
let r2 = robust_cross_prod(
Point::from_coords(1.0, 0.0, 0.0),
Point::from_coords(1.0, 0.0, 1e-100),
);
let angle = r1.0.angle(r2.0);
assert!(
(angle - FRAC_PI_2).abs() < 1e-10,
"angle between cross products should be π/2, got {angle}"
);
}
#[test]
fn test_robust_cross_prod_magnitude_symbolic() {
use std::f64::consts::FRAC_PI_2;
let r1 = robust_cross_prod(
Point::from_coords(-1e-100, 0.0, 1.0),
Point::from_coords(1e-100, 0.0, -1.0),
);
let r2 = robust_cross_prod(
Point::from_coords(0.0, -1e-100, 1.0),
Point::from_coords(0.0, 1e-100, -1.0),
);
assert!(Point(r1.0).is_normalizable(), "r1 should be normalizable");
assert!(Point(r2.0).is_normalizable(), "r2 should be normalizable");
let angle = r1.0.angle(r2.0);
assert!(
(angle - FRAC_PI_2).abs() < 1e-10,
"angle between symbolic cross products should be π/2, got {angle}"
);
}
#[test]
fn test_normalizable_from_exact() {
let tiny = PreciseVector::new(5e-324, 0.0, 0.0);
let result = normalizable_from_exact(&tiny);
assert!(is_normalizable(result), "result should be normalizable");
assert!(result.x > 0.0);
assert_eq!(result.y, 0.0);
assert_eq!(result.z, 0.0);
}
#[test]
fn test_normalizable_from_exact_zero() {
let zero = PreciseVector::new(0.0, 0.0, 0.0);
let result = normalizable_from_exact(&zero);
assert_eq!(result.x, 0.0);
assert_eq!(result.y, 0.0);
assert_eq!(result.z, 0.0);
}
#[test]
fn test_ensure_normalizable_normal() {
use crate::r3::Vector;
let v = Vector::new(1.0, 0.0, 0.0);
assert_eq!(ensure_normalizable(v), v);
}
#[test]
fn test_ensure_normalizable_tiny() {
use crate::r3::Vector;
let v = Vector::new(1e-300, 0.0, 0.0);
let result = ensure_normalizable(v);
assert!(is_normalizable(result));
assert!(result.x > 0.0);
}
#[test]
fn test_get_stable_cross_prod_orthogonal() {
use crate::r3::Vector;
let a = Vector::new(1.0, 0.0, 0.0);
let b = Vector::new(0.0, 1.0, 0.0);
let result = get_stable_cross_prod(a, b);
assert!(result.is_some());
let r = result.unwrap();
assert!(r.z > 0.0);
}
#[test]
fn test_get_stable_cross_prod_nearly_equal() {
use crate::r3::Vector;
let a = Vector::new(1.0, 0.0, 0.0);
let b = Vector::new(1.0, 1e-20, 0.0).normalize();
let result = get_stable_cross_prod(a, b);
let _ = result;
}
#[test]
fn test_robust_cross_prod_constants() {
assert!(ROBUST_CROSS_PROD_ERROR.radians() > 0.0);
assert!(EXACT_CROSS_PROD_ERROR.radians() > 0.0);
assert!(ROBUST_CROSS_PROD_ERROR.radians() > EXACT_CROSS_PROD_ERROR.radians());
let expected = 6.0 * predicates::DBL_ERROR;
assert!(
(ROBUST_CROSS_PROD_ERROR.radians() - expected).abs() < 1e-30,
"ROBUST_CROSS_PROD_ERROR should be 6 * DBL_ERROR"
);
}
#[cfg(feature = "serde")]
#[test]
fn test_serde_crossing_roundtrip() {
for c in [Crossing::Cross, Crossing::MaybeCross, Crossing::DoNotCross] {
let json = serde_json::to_string(&c).unwrap();
let back: Crossing = serde_json::from_str(&json).unwrap();
assert_eq!(c, back);
}
}
}