use num_rational::BigRational;
use num_traits::One;
use crate::linalg::{Vec2, Vec3};
use super::filtered::{self, incircle, orient2d, orient3d};
use super::predicates::{
incircle_r, line_line_intersect_2d, line_plane_intersect, orient2d_r, orient3d_r,
point_in_tri_2d, segment_param, tri_normal_r, TriLoc,
};
use super::rational::{rat, rat_to_f64, R2, R3};
use super::Sign;
struct Lcg(u64);
impl Lcg {
fn new(seed: u64) -> Self {
Lcg(seed)
}
fn next_u64(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.0
}
fn next_f64(&mut self, scale: f64) -> f64 {
let u = (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64; (u * 2.0 - 1.0) * scale
}
}
fn r2(x: f64, y: f64) -> R2 {
R2::from_vec2(Vec2::new(x, y))
}
fn r3(x: f64, y: f64, z: f64) -> R3 {
R3::from_vec3(Vec3::new(x, y, z))
}
#[test]
fn round_trip_is_bit_exact() {
let values = [
0.0,
1.0,
-1.0,
0.1,
-0.1,
std::f64::consts::PI,
1e300,
-1e300,
1e-300,
f64::MAX,
f64::MIN,
f64::MIN_POSITIVE, f64::MIN_POSITIVE / 4.0, 5e-324, -5e-324,
123456789.123456789,
];
for &v in &values {
let back = rat_to_f64(&rat(v));
assert_eq!(back.to_bits(), v.to_bits(), "round trip failed for {v:e}");
}
assert_eq!(rat_to_f64(&rat(-0.0)), 0.0);
let mut rng = Lcg::new(0x9E3779B97F4A7C15);
for i in 0..2000 {
let scale = 10f64.powi((i % 61) - 30);
let v = rng.next_f64(scale);
let back = rat_to_f64(&rat(v));
assert_eq!(back.to_bits(), v.to_bits(), "round trip failed for {v:e}");
}
}
#[test]
fn rounding_is_nearest_ties_even() {
let half_ulp = BigRational::new(1.into(), num_bigint::BigInt::one() << 53);
let tie_down = rat(1.0) + &half_ulp;
assert_eq!(rat_to_f64(&tie_down), 1.0);
let tie_up = rat(1.0) + &half_ulp * BigRational::from_integer(3.into());
assert_eq!(rat_to_f64(&tie_up), 1.0 + 2.0 * 2f64.powi(-52));
let quarter_ulp = &half_ulp / BigRational::from_integer(2.into());
assert_eq!(rat_to_f64(&(rat(1.0) + &quarter_ulp)), 1.0);
assert_eq!(
rat_to_f64(&(rat(1.0) + &half_ulp + &quarter_ulp)),
1.0 + 2f64.powi(-52)
);
}
#[test]
fn rounding_handles_overflow_and_subnormals() {
let two_max = rat(f64::MAX) * BigRational::from_integer(2.into());
assert_eq!(rat_to_f64(&two_max), f64::INFINITY);
assert_eq!(rat_to_f64(&(-two_max)), f64::NEG_INFINITY);
let min_sub = rat(5e-324);
let half_min = &min_sub / BigRational::from_integer(2.into());
assert_eq!(rat_to_f64(&half_min), 0.0);
let three_q = &min_sub * BigRational::new(3.into(), 4.into());
assert_eq!(rat_to_f64(&three_q).to_bits(), 5e-324f64.to_bits());
let sub = f64::MIN_POSITIVE / 8.0; let next = f64::from_bits(sub.to_bits() + 1);
let mid_plus = (rat(sub) + rat(next)) * BigRational::new(1.into(), 2.into())
+ BigRational::new(1.into(), num_bigint::BigInt::one() << 2000);
assert_eq!(rat_to_f64(&mid_plus).to_bits(), next.to_bits());
}
#[test]
fn orient2d_matches_exact_on_random_input() {
let mut rng = Lcg::new(42);
for _ in 0..5000 {
let (a, b, c) = (
Vec2::new(rng.next_f64(100.0), rng.next_f64(100.0)),
Vec2::new(rng.next_f64(100.0), rng.next_f64(100.0)),
Vec2::new(rng.next_f64(100.0), rng.next_f64(100.0)),
);
let exact = orient2d_r(&R2::from_vec2(a), &R2::from_vec2(b), &R2::from_vec2(c));
assert_eq!(orient2d(a, b, c), exact, "a={a:?} b={b:?} c={c:?}");
}
}
#[test]
fn orient2d_adversarial_near_collinear() {
let base = 12.0;
for i in 0..32 {
for j in 0..32 {
let a = Vec2::new(base + (i as f64) * 2f64.powi(-52), base + (j as f64) * 2f64.powi(-52));
let b = Vec2::new(24.0, 24.0);
let c = Vec2::new(48.0, 48.0);
let exact = orient2d_r(&R2::from_vec2(a), &R2::from_vec2(b), &R2::from_vec2(c));
assert_eq!(orient2d(a, b, c), exact, "i={i} j={j}");
}
}
assert_eq!(
orient2d(Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0), Vec2::new(3.0, 3.0)),
Sign::Zero
);
}
#[test]
fn orient3d_matches_exact_on_random_input() {
let mut rng = Lcg::new(7);
for _ in 0..5000 {
let p = |rng: &mut Lcg| {
Vec3::new(rng.next_f64(50.0), rng.next_f64(50.0), rng.next_f64(50.0))
};
let (a, b, c, d) = (p(&mut rng), p(&mut rng), p(&mut rng), p(&mut rng));
let exact = orient3d_r(
&R3::from_vec3(a),
&R3::from_vec3(b),
&R3::from_vec3(c),
&R3::from_vec3(d),
);
assert_eq!(orient3d(a, b, c, d), exact);
}
}
#[test]
fn orient3d_adversarial_near_coplanar() {
let a = Vec3::new(0.0, 0.0, 0.0);
let b = Vec3::new(1.0, 0.0, 1.0);
let c = Vec3::new(0.0, 1.0, 1.0);
for i in -16i32..=16 {
let z = 7.0 + (i as f64) * 2f64.powi(-50);
let d = Vec3::new(3.0, 4.0, z);
let exact = orient3d_r(
&R3::from_vec3(a),
&R3::from_vec3(b),
&R3::from_vec3(c),
&R3::from_vec3(d),
);
assert_eq!(orient3d(a, b, c, d), exact, "i={i}");
if i == 0 {
assert_eq!(exact, Sign::Zero);
}
}
}
#[test]
fn orient3d_sign_convention() {
let a = Vec3::new(0.0, 0.0, 0.0);
let b = Vec3::new(1.0, 0.0, 0.0);
let c = Vec3::new(0.0, 1.0, 0.0);
assert_eq!(orient3d(a, b, c, Vec3::new(0.2, 0.2, 1.0)), Sign::Pos);
assert_eq!(orient3d(a, b, c, Vec3::new(0.2, 0.2, -1.0)), Sign::Neg);
}
#[test]
fn incircle_matches_exact_and_handles_cocircular() {
let mut rng = Lcg::new(1234);
for _ in 0..3000 {
let p = |rng: &mut Lcg| Vec2::new(rng.next_f64(10.0), rng.next_f64(10.0));
let (a, b, c, d) = (p(&mut rng), p(&mut rng), p(&mut rng), p(&mut rng));
let exact = incircle_r(
&R2::from_vec2(a),
&R2::from_vec2(b),
&R2::from_vec2(c),
&R2::from_vec2(d),
);
assert_eq!(incircle(a, b, c, d), exact);
}
let a = Vec2::new(3.0, 4.0);
let b = Vec2::new(5.0, 0.0);
let c = Vec2::new(-3.0, -4.0);
let d = Vec2::new(-5.0, 0.0);
assert_eq!(incircle(a, b, c, d), Sign::Zero);
assert_eq!(incircle(a, b, c, Vec2::new(0.0, 0.0)), Sign::Neg); let (a2, b2, c2) = (b, a, c); assert_eq!(orient2d(a2, b2, c2), Sign::Pos);
assert_eq!(incircle(a2, b2, c2, Vec2::new(0.0, 0.0)), Sign::Pos);
assert_eq!(incircle(a2, b2, c2, Vec2::new(100.0, 0.0)), Sign::Neg);
}
#[test]
fn filter_hit_rate_is_high_on_generic_input() {
filtered::stats::reset();
let mut rng = Lcg::new(99);
for _ in 0..10000 {
let p = |rng: &mut Lcg| {
Vec3::new(rng.next_f64(50.0), rng.next_f64(50.0), rng.next_f64(50.0))
};
let (a, b, c, d) = (p(&mut rng), p(&mut rng), p(&mut rng), p(&mut rng));
let _ = orient3d(a, b, c, d);
let _ = orient2d(
Vec2::new(a.x, a.y),
Vec2::new(b.x, b.y),
Vec2::new(c.x, c.y),
);
}
let (fast, exact) = filtered::stats::snapshot();
let rate = fast as f64 / (fast + exact) as f64;
assert!(
rate > 0.99,
"float filter resolved only {rate:.4} of generic predicates (fast={fast}, exact={exact})"
);
}
#[test]
fn line_plane_intersect_lands_on_plane_and_segment() {
let a = r3(0.0, 0.0, 1.0);
let b = r3(4.0, 0.0, 1.0);
let c = r3(0.0, 4.0, 1.0);
let p = r3(1.0, 1.0, 0.0);
let q = r3(1.0, 1.0, 3.0);
let x = line_plane_intersect(&p, &q, &a, &b, &c).expect("not parallel");
assert_eq!(x, r3(1.0, 1.0, 1.0));
assert_eq!(orient3d_r(&a, &b, &c, &x), Sign::Zero);
assert_eq!(
segment_param(&p, &q, &x),
BigRational::new(1.into(), 3.into())
);
let p2 = r3(0.0, 0.0, 2.0);
let q2 = r3(1.0, 1.0, 2.0);
assert!(line_plane_intersect(&p2, &q2, &a, &b, &c).is_none());
}
#[test]
fn line_plane_intersect_is_exact_on_awkward_fractions() {
let a = r3(0.1, 0.2, 0.3);
let b = r3(1.7, -0.4, 0.9);
let c = r3(-0.6, 1.1, 2.2);
let p = r3(0.3, 0.3, -5.0);
let q = r3(0.4, 0.5, 7.0);
let x = line_plane_intersect(&p, &q, &a, &b, &c).expect("not parallel");
assert_eq!(orient3d_r(&a, &b, &c, &x), Sign::Zero);
let n = x.sub(&p).cross(&q.sub(&p));
assert!(n.is_zero());
}
#[test]
fn line_line_intersect_2d_crossing_diagonals() {
let x = line_line_intersect_2d(&r2(0.0, 0.0), &r2(1.0, 1.0), &r2(1.0, 0.0), &r2(0.0, 1.0))
.expect("not parallel");
assert_eq!(x, r2(0.5, 0.5));
assert!(
line_line_intersect_2d(&r2(0.0, 0.0), &r2(1.0, 0.0), &r2(0.0, 1.0), &r2(1.0, 1.0))
.is_none()
);
assert!(
line_line_intersect_2d(&r2(0.0, 0.0), &r2(1.0, 0.0), &r2(2.0, 0.0), &r2(3.0, 0.0))
.is_none()
);
}
#[test]
fn point_in_tri_2d_classifies_all_regions() {
let a = r2(0.0, 0.0);
let b = r2(4.0, 0.0);
let c = r2(0.0, 4.0);
assert_eq!(point_in_tri_2d(&r2(1.0, 1.0), &a, &b, &c), TriLoc::Inside);
assert_eq!(point_in_tri_2d(&r2(2.0, 0.0), &a, &b, &c), TriLoc::OnEdge(0));
assert_eq!(point_in_tri_2d(&r2(2.0, 2.0), &a, &b, &c), TriLoc::OnEdge(1));
assert_eq!(point_in_tri_2d(&r2(0.0, 1.0), &a, &b, &c), TriLoc::OnEdge(2));
assert_eq!(point_in_tri_2d(&a, &a, &b, &c), TriLoc::OnVertex(0));
assert_eq!(point_in_tri_2d(&b, &a, &b, &c), TriLoc::OnVertex(1));
assert_eq!(point_in_tri_2d(&c, &a, &b, &c), TriLoc::OnVertex(2));
assert_eq!(point_in_tri_2d(&r2(3.0, 3.0), &a, &b, &c), TriLoc::Outside);
assert_eq!(point_in_tri_2d(&r2(-0.1, 1.0), &a, &b, &c), TriLoc::Outside);
assert_eq!(point_in_tri_2d(&r2(1.0, 1.0), &a, &c, &b), TriLoc::Inside);
assert_eq!(point_in_tri_2d(&r2(2.0, 0.0), &a, &c, &b), TriLoc::OnEdge(2));
let d = r2(8.0, 0.0);
assert_eq!(point_in_tri_2d(&r2(1.0, 0.0), &a, &b, &d), TriLoc::Outside);
}
#[test]
fn tri_normal_r_matches_orientation() {
let n = tri_normal_r(&r3(0.0, 0.0, 0.0), &r3(1.0, 0.0, 0.0), &r3(0.0, 1.0, 0.0));
assert_eq!(n, r3(0.0, 0.0, 1.0));
let z = tri_normal_r(&r3(0.0, 0.0, 0.0), &r3(1.0, 1.0, 1.0), &r3(2.0, 2.0, 2.0));
assert!(z.is_zero());
}