Skip to main content

manifold_rust/robust/exact/
rational.rs

1// robust/exact/rational.rs — Exact rational points and correctly rounded
2// rational→f64 conversion for the robust boolean engine.
3//
4// Input mesh vertices are finite f64 and convert to Rational exactly
5// (`rat`, `R3::from_vec3`). Constructed points — plane/segment intersections
6// built in robust/exact/predicates.rs — stay rational through the whole
7// pipeline; only output assembly rounds them back, via `rat_to_f64`, which
8// rounds to the *nearest* f64 (ties to even, subnormals and overflow
9// handled). That single-rounding guarantee is what lets the robust engine's
10// output vertices agree with the exact engine's to the last ulp on
11// intersection points, and bit-for-bit on pass-through input vertices.
12
13use super::backend::{
14    self, denom, numer_mag, rat_from_f64, rat_is_negative, rat_is_zero, Rational, ToPrimitive,
15    Uint,
16};
17
18use crate::linalg::{Vec2, Vec3};
19
20/// Exact conversion of a finite f64. Every finite f64 is a dyadic rational,
21/// so this never loses information.
22///
23/// Panics on NaN/infinity — mesh import rejects non-finite vertices
24/// (`Error::NonFiniteVertex`) long before the robust engine runs, so a
25/// non-finite value here is an internal logic error, not bad user input.
26#[inline]
27pub fn rat(v: f64) -> Rational {
28    rat_from_f64(v).expect("robust engine: coordinate must be finite")
29}
30
31/// 2^e as f64, exact for the full representable range -1074..=1023
32/// (subnormal powers of two included). Built by bit manipulation so no
33/// intermediate rounding can occur.
34#[inline]
35fn pow2(e: i64) -> f64 {
36    debug_assert!((-1074..=1023).contains(&e), "pow2 exponent out of range: {e}");
37    if e >= -1022 {
38        f64::from_bits(((e + 1023) as u64) << 52)
39    } else {
40        f64::from_bits(1u64 << (e + 1074))
41    }
42}
43
44/// Round a rational to the nearest f64, ties to even — the correctly rounded
45/// result, identical to rounding the exact real value once. Values beyond
46/// f64 range become ±infinity; values below half the smallest subnormal
47/// become (signed) zero.
48pub fn rat_to_f64(r: &Rational) -> f64 {
49    if rat_is_zero(r) {
50        return 0.0;
51    }
52    let neg = rat_is_negative(r);
53    let n_mag = numer_mag(r);
54    let n: &Uint = n_mag.as_ref();
55    let d: &Uint = denom(r);
56
57    // Exact floor exponent e: 2^e <= n/d < 2^(e+1).
58    let mut e = backend::uint_bits(n) as i64 - backend::uint_bits(d) as i64;
59    let ge = if e >= 0 {
60        *n >= (d << e as usize)
61    } else {
62        (n << (-e) as usize) >= *d
63    };
64    if !ge {
65        e -= 1;
66    }
67    if e > 1023 {
68        return if neg { f64::NEG_INFINITY } else { f64::INFINITY };
69    }
70
71    // Position of the result's least significant bit. Normal numbers carry
72    // 53 bits ending at e-52; subnormals are cut off at 2^-1074.
73    let lsb = (e - 52).max(-1074);
74
75    // m = round_nearest_even((n/d) / 2^lsb), computed with one exact
76    // integer division plus a remainder comparison.
77    let (num, den) = if lsb >= 0 {
78        (n.clone(), d << lsb as usize)
79    } else {
80        (n << (-lsb) as usize, d.clone())
81    };
82    let q = &num / &den;
83    let rem = &num - &q * &den;
84    let mut m = q;
85    let twice_rem = &rem << 1usize;
86    match twice_rem.cmp(&den) {
87        std::cmp::Ordering::Greater => m += 1u32,
88        std::cmp::Ordering::Equal => {
89            if backend::uint_bit(&m, 0) {
90                m += 1u32;
91            }
92        }
93        std::cmp::Ordering::Less => {}
94    }
95
96    if m.is_zero() {
97        return if neg { -0.0 } else { 0.0 };
98    }
99    // Rounding up may have crossed into the next binade (m = 2^53) or past
100    // the largest finite value (2^1024 -> infinity).
101    if backend::uint_bits(&m) as i64 - 1 + lsb > 1023 {
102        return if neg { f64::NEG_INFINITY } else { f64::INFINITY };
103    }
104    // m <= 2^53, so both the u64 and the f64 conversion are exact, and
105    // m * 2^lsb is representable by construction — the multiply is exact.
106    let val = m.to_u64().expect("mantissa fits in u64") as f64 * pow2(lsb);
107    if neg {
108        -val
109    } else {
110        val
111    }
112}
113
114// ─── R2 — exact 2D point/vector ─────────────────────────────────────────────
115
116/// Exact 2D point (or vector). Derived `Ord` is lexicographic, which the
117/// arrangement code uses for exact point dedup in BTree maps.
118#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
119pub struct R2 {
120    pub x: Rational,
121    pub y: Rational,
122}
123
124impl R2 {
125    #[inline]
126    pub fn new(x: Rational, y: Rational) -> Self {
127        Self { x, y }
128    }
129
130    #[inline]
131    pub fn from_vec2(v: Vec2) -> Self {
132        Self::new(rat(v.x), rat(v.y))
133    }
134
135    pub fn to_vec2_rounded(&self) -> Vec2 {
136        Vec2::new(rat_to_f64(&self.x), rat_to_f64(&self.y))
137    }
138
139    pub fn sub(&self, o: &R2) -> R2 {
140        R2::new(&self.x - &o.x, &self.y - &o.y)
141    }
142
143    pub fn add(&self, o: &R2) -> R2 {
144        R2::new(&self.x + &o.x, &self.y + &o.y)
145    }
146
147    pub fn scale(&self, s: &Rational) -> R2 {
148        R2::new(&self.x * s, &self.y * s)
149    }
150
151    pub fn dot(&self, o: &R2) -> Rational {
152        &self.x * &o.x + &self.y * &o.y
153    }
154
155    /// 2D cross product (z of the 3D cross of the embedded vectors).
156    pub fn cross(&self, o: &R2) -> Rational {
157        &self.x * &o.y - &self.y * &o.x
158    }
159
160    pub fn is_zero(&self) -> bool {
161        rat_is_zero(&self.x) && rat_is_zero(&self.y)
162    }
163}
164
165// ─── R3 — exact 3D point/vector ─────────────────────────────────────────────
166
167/// Exact 3D point (or vector). Derived `Ord` is lexicographic (x, y, z) for
168/// exact vertex welding in output assembly.
169#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
170pub struct R3 {
171    pub x: Rational,
172    pub y: Rational,
173    pub z: Rational,
174}
175
176impl R3 {
177    #[inline]
178    pub fn new(x: Rational, y: Rational, z: Rational) -> Self {
179        Self { x, y, z }
180    }
181
182    #[inline]
183    pub fn from_vec3(v: Vec3) -> Self {
184        Self::new(rat(v.x), rat(v.y), rat(v.z))
185    }
186
187    pub fn to_vec3_rounded(&self) -> Vec3 {
188        Vec3::new(rat_to_f64(&self.x), rat_to_f64(&self.y), rat_to_f64(&self.z))
189    }
190
191    pub fn sub(&self, o: &R3) -> R3 {
192        R3::new(&self.x - &o.x, &self.y - &o.y, &self.z - &o.z)
193    }
194
195    pub fn add(&self, o: &R3) -> R3 {
196        R3::new(&self.x + &o.x, &self.y + &o.y, &self.z + &o.z)
197    }
198
199    pub fn scale(&self, s: &Rational) -> R3 {
200        R3::new(&self.x * s, &self.y * s, &self.z * s)
201    }
202
203    pub fn dot(&self, o: &R3) -> Rational {
204        &self.x * &o.x + &self.y * &o.y + &self.z * &o.z
205    }
206
207    pub fn cross(&self, o: &R3) -> R3 {
208        R3::new(
209            &self.y * &o.z - &self.z * &o.y,
210            &self.z * &o.x - &self.x * &o.z,
211            &self.x * &o.y - &self.y * &o.x,
212        )
213    }
214
215    pub fn is_zero(&self) -> bool {
216        rat_is_zero(&self.x) && rat_is_zero(&self.y) && rat_is_zero(&self.z)
217    }
218
219    /// Drop the coordinate at `axis` (0=x, 1=y, 2=z), keeping the other two
220    /// in cyclic order — the paper's bijective dominant-axis projection used
221    /// to embed per-triangle 2D arrangements.
222    pub fn project_drop(&self, axis: usize) -> R2 {
223        match axis {
224            0 => R2::new(self.y.clone(), self.z.clone()),
225            1 => R2::new(self.z.clone(), self.x.clone()),
226            2 => R2::new(self.x.clone(), self.y.clone()),
227            _ => unreachable!("axis must be 0, 1, or 2"),
228        }
229    }
230}
231
232// ─── Cheap exact hash keys ───────────────────────────────────────────────────
233//
234// A general-purpose rational Hash/Eq must stay consistent for UNREDUCED
235// ratios, which costs at least a cross-multiplication (and, in some
236// libraries, a Euclidean recursion). Every rational this pipeline stores is canonical —
237// built by rat_new, the arithmetic operators, or rat_from_f64, all of which
238// reduce — so field identity IS value identity, and hashing the raw sign/limb
239// data is both exact and division-free. The wrappers below are drop-in
240// hash-map keys that are 1–2 orders of magnitude cheaper than hashing R2/R3
241// directly. See backend.rs items (1) and (2).
242
243use backend::{hash_rational as hash_rat, rat_eq as rat_fields_eq};
244
245/// Hash-map key wrapper around a canonical R2.
246#[derive(Clone, Debug)]
247pub struct R2Key(pub R2);
248
249impl PartialEq for R2Key {
250    #[inline]
251    fn eq(&self, other: &Self) -> bool {
252        rat_fields_eq(&self.0.x, &other.0.x) && rat_fields_eq(&self.0.y, &other.0.y)
253    }
254}
255impl Eq for R2Key {}
256impl std::hash::Hash for R2Key {
257    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
258        hash_rat(&self.0.x, state);
259        hash_rat(&self.0.y, state);
260    }
261}
262
263/// Hash-map key wrapper around a canonical R3.
264#[derive(Clone, Debug)]
265pub struct R3Key(pub R3);
266
267impl PartialEq for R3Key {
268    #[inline]
269    fn eq(&self, other: &Self) -> bool {
270        rat_fields_eq(&self.0.x, &other.0.x)
271            && rat_fields_eq(&self.0.y, &other.0.y)
272            && rat_fields_eq(&self.0.z, &other.0.z)
273    }
274}
275impl Eq for R3Key {}
276impl std::hash::Hash for R3Key {
277    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
278        hash_rat(&self.0.x, state);
279        hash_rat(&self.0.y, state);
280        hash_rat(&self.0.z, state);
281    }
282}
283
284/// Field-wise equality of canonical R2 values — see [`r3_eq`].
285#[inline]
286pub fn r2_eq(a: &R2, b: &R2) -> bool {
287    rat_fields_eq(&a.x, &b.x) && rat_fields_eq(&a.y, &b.y)
288}
289
290/// Field-wise equality of canonical R3 values — value equality without
291/// the backend's general comparison (which is most expensive exactly when
292/// the values ARE equal).
293#[inline]
294pub fn r3_eq(a: &R3, b: &R3) -> bool {
295    rat_fields_eq(&a.x, &b.x) && rat_fields_eq(&a.y, &b.y) && rat_fields_eq(&a.z, &b.z)
296}