use core::cmp::Ordering;
use crate::exact_int::{
RationalCoordInt, RationalEqInt, RationalOrderInt, RationalOrientInt, f64_to_rational_int,
};
use crate::round::F64Bracket;
use crate::{NonFinite, Sign};
const NUM_BITS: usize = 6300;
const DEN_BITS: usize = 4200;
const CROSS_BITS: usize = 10500;
const ORDER_BITS: usize = 12600;
const LIFT_BITS: usize = 6301;
const ORIENT_BITS: usize = 8401;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RationalCoord2d {
num_x: RationalCoordInt,
num_y: RationalCoordInt,
den: RationalCoordInt,
}
impl RationalCoord2d {
pub fn from_f64(p: [f64; 2]) -> Result<Self, NonFinite> {
if !p[0].is_finite() || !p[1].is_finite() {
return Err(NonFinite::new());
}
Ok(Self {
num_x: f64_to_rational_int(p[0]),
num_y: f64_to_rational_int(p[1]),
den: RationalCoordInt::one(),
})
}
pub fn from_segment_intersection(
a0: [f64; 2],
a1: [f64; 2],
b0: [f64; 2],
b1: [f64; 2],
) -> Result<Option<Self>, NonFinite> {
for p in [a0, a1, b0, b1] {
if !p[0].is_finite() || !p[1].is_finite() {
return Err(NonFinite::new());
}
}
let (a0x, a0y) = (f64_to_rational_int(a0[0]), f64_to_rational_int(a0[1]));
let (a1x, a1y) = (f64_to_rational_int(a1[0]), f64_to_rational_int(a1[1]));
let (b0x, b0y) = (f64_to_rational_int(b0[0]), f64_to_rational_int(b0[1]));
let (b1x, b1y) = (f64_to_rational_int(b1[0]), f64_to_rational_int(b1[1]));
let rx = a1x - a0x;
let ry = a1y - a0y;
let sx = b1x - b0x;
let sy = b1y - b0y;
let den = rx * sy - ry * sx;
debug_assert!(
den.fits_in_signed_bits(DEN_BITS),
"RationalCoord2d denominator overflow: width analysis violated"
);
if den.is_zero() {
return Ok(None);
}
let wx = b0x - a0x;
let wy = b0y - a0y;
let num_t = wx * sy - wy * sx;
debug_assert!(
num_t.fits_in_signed_bits(DEN_BITS),
"RationalCoord2d parameter numerator overflow: width analysis violated"
);
let mut num_x = a0x * den + num_t * rx;
let mut num_y = a0y * den + num_t * ry;
let mut den = den;
debug_assert!(
num_x.fits_in_signed_bits(NUM_BITS) && num_y.fits_in_signed_bits(NUM_BITS),
"RationalCoord2d numerator overflow: width analysis violated"
);
if den.is_negative() {
num_x = -num_x;
num_y = -num_y;
den = -den;
}
debug_assert!(
den.is_positive(),
"RationalCoord2d sign normalization failed to make the denominator positive"
);
Ok(Some(Self { num_x, num_y, den }))
}
#[must_use]
pub fn f64_bracket(&self) -> [F64Bracket; 2] {
[
crate::round::rational_to_bracket(&self.num_x, &self.den),
crate::round::rational_to_bracket(&self.num_y, &self.den),
]
}
}
fn cross(
an: &RationalCoordInt,
ad: &RationalCoordInt,
bn: &RationalCoordInt,
bd: &RationalCoordInt,
) -> RationalEqInt {
let (an, ad): (RationalEqInt, RationalEqInt) = (an.widen(), ad.widen());
let (bn, bd): (RationalEqInt, RationalEqInt) = (bn.widen(), bd.widen());
let diff = an * bd - bn * ad;
debug_assert!(
diff.fits_in_signed_bits(CROSS_BITS),
"rational cross-multiplication overflow: width analysis violated; \
result sign may be incorrect"
);
diff
}
#[must_use]
pub fn rational_coords_equal(a: &RationalCoord2d, b: &RationalCoord2d) -> bool {
cross(&a.num_x, &a.den, &b.num_x, &b.den).is_zero()
&& cross(&a.num_y, &a.den, &b.num_y, &b.den).is_zero()
}
pub fn rational_compare_along(
seg_start: [f64; 2],
seg_end: [f64; 2],
a: &RationalCoord2d,
b: &RationalCoord2d,
) -> Result<Ordering, NonFinite> {
for p in [seg_start, seg_end] {
if !p[0].is_finite() || !p[1].is_finite() {
return Err(NonFinite::new());
}
}
let dx: RationalOrderInt =
(f64_to_rational_int(seg_end[0]) - f64_to_rational_int(seg_start[0])).widen();
let dy: RationalOrderInt =
(f64_to_rational_int(seg_end[1]) - f64_to_rational_int(seg_start[1])).widen();
let ux: RationalOrderInt = cross(&a.num_x, &a.den, &b.num_x, &b.den).widen();
let uy: RationalOrderInt = cross(&a.num_y, &a.den, &b.num_y, &b.den).widen();
let det = ux * dx + uy * dy;
debug_assert!(
det.fits_in_signed_bits(ORDER_BITS),
"rational_compare_along overflow: width analysis violated; \
ordering may be incorrect"
);
Ok(if det.is_zero() {
Ordering::Equal
} else if det.is_negative() {
Ordering::Less
} else {
Ordering::Greater
})
}
pub fn orient2d_rational(
pa: [f64; 2],
pb: [f64; 2],
c: &RationalCoord2d,
) -> Result<Sign, NonFinite> {
for p in [pa, pb] {
if !p[0].is_finite() || !p[1].is_finite() {
return Err(NonFinite::new());
}
}
let pax: RationalOrientInt = f64_to_rational_int(pa[0]).widen();
let pay: RationalOrientInt = f64_to_rational_int(pa[1]).widen();
let pbx: RationalOrientInt = f64_to_rational_int(pb[0]).widen();
let pby: RationalOrientInt = f64_to_rational_int(pb[1]).widen();
let cx: RationalOrientInt = c.num_x.widen();
let cy: RationalOrientInt = c.num_y.widen();
let dc: RationalOrientInt = c.den.widen();
let lift_x = cx - pax * dc;
let lift_y = cy - pay * dc;
debug_assert!(
lift_x.fits_in_signed_bits(LIFT_BITS) && lift_y.fits_in_signed_bits(LIFT_BITS),
"orient2d_rational lift overflow: width analysis violated"
);
let det = (pbx - pax) * lift_y - (pby - pay) * lift_x;
debug_assert!(
det.fits_in_signed_bits(ORIENT_BITS),
"orient2d_rational overflow: width analysis violated; \
result sign may be incorrect"
);
Ok(if det.is_zero() {
Sign::Zero
} else if det.is_negative() {
Sign::Negative
} else {
Sign::Positive
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(
clippy::float_cmp,
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_possible_wrap,
clippy::range_plus_one,
clippy::no_effect_underscore_binding
)]
mod tests {
use super::*;
fn isect(a0: [f64; 2], a1: [f64; 2], b0: [f64; 2], b1: [f64; 2]) -> RationalCoord2d {
RationalCoord2d::from_segment_intersection(a0, a1, b0, b1)
.unwrap()
.unwrap()
}
#[test]
fn denominator_is_normalized_positive_in_both_argument_orders() {
let p = isect([0.0, 0.0], [4.0, 4.0], [0.0, 4.0], [4.0, 0.0]);
let q = isect([0.0, 4.0], [4.0, 0.0], [0.0, 0.0], [4.0, 4.0]);
assert!(p.den.is_positive(), "den must be normalized positive");
assert!(q.den.is_positive(), "den must be normalized positive");
assert!(
rational_coords_equal(&p, &q),
"the crossing point does not depend on which segment is named first"
);
}
#[test]
fn exact_lattice_crossing_equals_the_f64_point_exactly() {
let p = isect([0.0, 0.0], [4.0, 4.0], [0.0, 4.0], [4.0, 0.0]);
let two_two = RationalCoord2d::from_f64([2.0, 2.0]).unwrap();
assert!(rational_coords_equal(&p, &two_two));
}
#[test]
fn non_representable_crossing_is_not_equal_to_its_f64_rounding() {
let p = isect([0.0, 0.0], [1.0, 1.0], [1.0, 0.0], [0.0, 2.0]);
let rounded = RationalCoord2d::from_f64([2.0 / 3.0, 2.0 / 3.0]).unwrap();
assert!(
!rational_coords_equal(&p, &rounded),
"the exact point and its f64 rounding are different points; \
reporting them equal would mean the representation is not exact"
);
}
#[test]
fn parallel_lines_report_no_intersection() {
assert_eq!(
RationalCoord2d::from_segment_intersection(
[0.0, 0.0],
[1.0, 0.0],
[0.0, 1.0],
[1.0, 1.0]
),
Ok(None)
);
}
#[test]
fn collinear_lines_report_no_intersection() {
assert_eq!(
RationalCoord2d::from_segment_intersection(
[0.0, 0.0],
[2.0, 2.0],
[1.0, 1.0],
[3.0, 3.0]
),
Ok(None)
);
}
#[test]
fn non_finite_inputs_are_rejected() {
assert_eq!(
RationalCoord2d::from_f64([f64::NAN, 0.0]),
Err(NonFinite::new())
);
assert_eq!(
RationalCoord2d::from_f64([0.0, f64::INFINITY]),
Err(NonFinite::new())
);
assert_eq!(
RationalCoord2d::from_segment_intersection(
[0.0, 0.0],
[1.0, 1.0],
[f64::NEG_INFINITY, 0.0],
[1.0, 0.0]
),
Err(NonFinite::new())
);
let p = RationalCoord2d::from_f64([0.0, 0.0]).unwrap();
assert_eq!(
rational_compare_along([0.0, 0.0], [f64::NAN, 1.0], &p, &p),
Err(NonFinite::new())
);
assert_eq!(
orient2d_rational([f64::NAN, 0.0], [1.0, 0.0], &p),
Err(NonFinite::new())
);
}
#[test]
fn equal_is_reflexive_and_distinguishes_distinct_points() {
let p = isect([0.0, 0.0], [1.0, 1.0], [1.0, 0.0], [0.0, 2.0]);
let q = isect([0.0, 0.0], [1.0, 1.0], [1.0, 0.0], [0.0, 3.0]);
assert!(rational_coords_equal(&p, &p));
assert!(!rational_coords_equal(&p, &q));
}
#[test]
fn equal_sees_through_different_denominators() {
let diag = isect([0.0, 0.0], [4.0, 4.0], [0.0, 4.0], [4.0, 0.0]);
let mids = isect([2.0, 0.0], [2.0, 9.0], [0.0, 2.0], [7.0, 2.0]);
assert!(
rational_coords_equal(&diag, &mids),
"equality must be a statement about the point, not about the \
segment pair it was derived from"
);
}
#[test]
fn compare_along_orders_three_crossings_on_one_edge() {
let e0 = [0.0, 0.0];
let e1 = [10.0, 0.0];
let at = |x: f64| isect(e0, e1, [x, -1.0], [x, 1.0]);
let (p1, p2, p3) = (at(1.0), at(5.0), at(9.0));
assert_eq!(rational_compare_along(e0, e1, &p1, &p2), Ok(Ordering::Less));
assert_eq!(
rational_compare_along(e0, e1, &p3, &p2),
Ok(Ordering::Greater)
);
assert_eq!(
rational_compare_along(e0, e1, &p2, &p2),
Ok(Ordering::Equal)
);
}
#[test]
fn compare_along_reverses_with_the_direction() {
let e0 = [0.0, 0.0];
let e1 = [10.0, 0.0];
let at = |x: f64| isect(e0, e1, [x, -1.0], [x, 1.0]);
let (p1, p2) = (at(1.0), at(9.0));
assert_eq!(rational_compare_along(e0, e1, &p1, &p2), Ok(Ordering::Less));
assert_eq!(
rational_compare_along(e1, e0, &p1, &p2),
Ok(Ordering::Greater),
"reversing the directed segment must reverse the order"
);
}
#[test]
fn compare_along_equal_does_not_imply_same_point() {
let a = RationalCoord2d::from_f64([3.0, 1.0]).unwrap();
let b = RationalCoord2d::from_f64([3.0, 7.0]).unwrap();
assert_eq!(
rational_compare_along([0.0, 0.0], [1.0, 0.0], &a, &b),
Ok(Ordering::Equal)
);
assert!(
!rational_coords_equal(&a, &b),
"Equal order and equal point are different questions"
);
}
#[test]
fn compare_along_zero_direction_is_all_equal() {
let a = RationalCoord2d::from_f64([1.0, 2.0]).unwrap();
let b = RationalCoord2d::from_f64([9.0, 9.0]).unwrap();
assert_eq!(
rational_compare_along([4.0, 4.0], [4.0, 4.0], &a, &b),
Ok(Ordering::Equal)
);
}
#[test]
fn orient_rational_matches_orient2d_on_an_exact_point() {
let pa = [0.0, 0.0];
let pb = [1.0, 0.0];
for c in [[0.5, 1.0], [0.5, -1.0], [0.5, 0.0], [2.0, 0.0]] {
let rp = RationalCoord2d::from_f64(c).unwrap();
assert_eq!(
orient2d_rational(pa, pb, &rp).unwrap(),
crate::orient2d(pa, pb, c).unwrap(),
"orient2d_rational must share orient2d's convention for c = {c:?}"
);
}
}
#[test]
fn orient_rational_is_zero_on_the_generating_line() {
let a0 = [0.0, 0.0];
let a1 = [7.0, 3.0];
let b0 = [1.0, 5.0];
let b1 = [6.0, -2.0];
let p = isect(a0, a1, b0, b1);
assert_eq!(orient2d_rational(a0, a1, &p), Ok(Sign::Zero));
assert_eq!(orient2d_rational(b0, b1, &p), Ok(Sign::Zero));
assert_eq!(
orient2d_rational(a1, a0, &p),
Ok(Sign::Zero),
"reversing the line must not disturb an exact zero"
);
}
#[test]
fn orient_rational_reverses_with_the_line() {
let a0 = [0.0, 0.0];
let a1 = [1.0, 0.0];
let p = isect([0.0, 1.0], [2.0, 1.0], [1.0, 0.0], [1.0, 5.0]); let left = orient2d_rational(a0, a1, &p).unwrap();
let right = orient2d_rational(a1, a0, &p).unwrap();
assert_eq!(left, Sign::Positive);
assert_eq!(right, Sign::Negative);
assert_eq!(right, -left);
}
#[test]
fn extreme_magnitude_endpoints_stay_within_the_proven_widths() {
let big = f64::MAX;
let p = isect([-big, -big], [big, big], [-big, big], [big, -big]);
let origin = RationalCoord2d::from_f64([0.0, 0.0]).unwrap();
assert!(rational_coords_equal(&p, &origin));
}
#[test]
fn subnormal_endpoints_are_handled_exactly() {
let s = f64::from_bits(1); let p = isect(
[0.0, 0.0],
[4.0 * s, 4.0 * s],
[0.0, 4.0 * s],
[4.0 * s, 0.0],
);
let mid = RationalCoord2d::from_f64([2.0 * s, 2.0 * s]).unwrap();
assert!(rational_coords_equal(&p, &mid));
}
}