Skip to main content

brepkit_math/
filtered.rs

1//! Filtered exact arithmetic for geometric predicates.
2//!
3//! Provides fast-path floating-point computation with automatic fallback
4//! to exact arithmetic when the result is ambiguous. In practice, 95%+
5//! of predicate evaluations resolve in the fast path.
6//!
7//! Based on Shewchuk's adaptive precision arithmetic (1997).
8//!
9//! CDT (`cdt.rs`) and mesh booleans (`mesh_boolean.rs` in brepkit-operations)
10//! can switch their `orient2d`/`in_circle` calls to these filtered versions
11//! for a significant performance improvement without sacrificing robustness.
12
13#![allow(
14    clippy::suboptimal_flops,
15    clippy::many_single_char_names,
16    clippy::similar_names
17)]
18
19use crate::vec::{Point2, Point3};
20
21/// Filtered orient2d: fast f64 path with exact fallback.
22///
23/// Returns a positive value if `(a, b, c)` are in counter-clockwise order,
24/// negative if clockwise, and zero if collinear.
25///
26/// Uses error-free transformations to bound the rounding error. If the
27/// computed result is larger than the error bound, the sign is guaranteed
28/// correct. Otherwise, falls back to exact arithmetic.
29#[must_use]
30pub fn filtered_orient2d(a: Point2, b: Point2, c: Point2) -> f64 {
31    let acx = a.x() - c.x();
32    let bcx = b.x() - c.x();
33    let acy = a.y() - c.y();
34    let bcy = b.y() - c.y();
35
36    let det = acx * bcy - acy * bcx;
37
38    // Compute error bound using Shewchuk's method
39    let det_sum = (acx * bcy).abs() + (acy * bcx).abs();
40
41    // The error bound for orient2d: (3 + 16*eps) * eps * |detsum|
42    let eps = f64::EPSILON;
43    let err_bound = (3.0 + 16.0 * eps) * eps * det_sum;
44
45    if det.abs() > err_bound {
46        det
47    } else {
48        // Fall back to exact arithmetic
49        crate::predicates::orient2d(a, b, c)
50    }
51}
52
53/// Filtered orient3d: fast f64 path with exact fallback.
54///
55/// Returns a positive value if `d` is below the plane defined by `(a, b, c)`
56/// (with CCW orientation), negative if above, zero if coplanar.
57#[must_use]
58pub fn filtered_orient3d(a: Point3, b: Point3, c: Point3, d: Point3) -> f64 {
59    let adx = a.x() - d.x();
60    let bdx = b.x() - d.x();
61    let cdx = c.x() - d.x();
62    let ady = a.y() - d.y();
63    let bdy = b.y() - d.y();
64    let cdy = c.y() - d.y();
65    let adz = a.z() - d.z();
66    let bdz = b.z() - d.z();
67    let cdz = c.z() - d.z();
68
69    let det = adx * (bdy * cdz - bdz * cdy) - bdx * (ady * cdz - adz * cdy)
70        + cdx * (ady * bdz - adz * bdy);
71
72    // Error bound for orient3d
73    let permanent = (adx.abs() * ((bdy * cdz).abs() + (bdz * cdy).abs()))
74        + (bdx.abs() * ((ady * cdz).abs() + (adz * cdy).abs()))
75        + (cdx.abs() * ((ady * bdz).abs() + (adz * bdy).abs()));
76
77    let eps = f64::EPSILON;
78    let err_bound = (7.0 + 56.0 * eps) * eps * permanent;
79
80    if det.abs() > err_bound {
81        det
82    } else {
83        crate::predicates::orient3d(a, b, c, d)
84    }
85}
86
87/// Filtered in-circle: fast f64 path with exact fallback.
88///
89/// Returns a positive value if `d` is inside the circumcircle of `(a, b, c)`,
90/// negative if outside, zero if exactly on the circle.
91/// Assumes `(a, b, c)` are in counter-clockwise order.
92#[must_use]
93pub fn filtered_in_circle(a: Point2, b: Point2, c: Point2, d: Point2) -> f64 {
94    let adx = a.x() - d.x();
95    let ady = a.y() - d.y();
96    let bdx = b.x() - d.x();
97    let bdy = b.y() - d.y();
98    let cdx = c.x() - d.x();
99    let cdy = c.y() - d.y();
100
101    let abdet = adx * bdy - bdx * ady;
102    let bcdet = bdx * cdy - cdx * bdy;
103    let cadet = cdx * ady - adx * cdy;
104    let alift = adx * adx + ady * ady;
105    let blift = bdx * bdx + bdy * bdy;
106    let clift = cdx * cdx + cdy * cdy;
107
108    let det = alift * bcdet + blift * cadet + clift * abdet;
109
110    // Error bound for in_circle
111    let permanent = alift * ((bcdet).abs() + (bdx * cdy).abs() + (cdx * bdy).abs())
112        + blift * ((cadet).abs() + (cdx * ady).abs() + (adx * cdy).abs())
113        + clift * ((abdet).abs() + (adx * bdy).abs() + (bdx * ady).abs());
114
115    let eps = f64::EPSILON;
116    let err_bound = (10.0 + 96.0 * eps) * eps * permanent;
117
118    if det.abs() > err_bound {
119        det
120    } else {
121        crate::predicates::in_circle(a, b, c, d)
122    }
123}
124
125// ---------------------------------------------------------------------------
126// Segment intersection
127// ---------------------------------------------------------------------------
128
129/// Result of a 2D segment-segment intersection test.
130#[derive(Debug, Clone, Copy, PartialEq)]
131pub enum SegmentIntersection {
132    /// No intersection.
133    None,
134    /// Segments intersect at a single point.
135    Point {
136        /// The intersection point.
137        point: Point2,
138        /// Parameter on the first segment (0..1).
139        t1: f64,
140        /// Parameter on the second segment (0..1).
141        t2: f64,
142    },
143    /// Segments overlap (collinear) along a range.
144    Overlap {
145        /// Start of the overlap.
146        start: Point2,
147        /// End of the overlap.
148        end: Point2,
149    },
150}
151
152/// Compute the intersection of two 2D line segments using filtered predicates.
153///
154/// Handles all degeneracies: T-intersections, endpoint-on-segment,
155/// collinear overlap, and parallel/disjoint segments.
156///
157/// Uses [`filtered_orient2d`] for robust classification.
158#[must_use]
159#[allow(clippy::too_many_lines)]
160pub fn segment_intersection(a1: Point2, a2: Point2, b1: Point2, b2: Point2) -> SegmentIntersection {
161    // Orient2d tests for segment classification
162    let d1 = filtered_orient2d(a1, a2, b1);
163    let d2 = filtered_orient2d(a1, a2, b2);
164    let d3 = filtered_orient2d(b1, b2, a1);
165    let d4 = filtered_orient2d(b1, b2, a2);
166
167    // Standard crossing test: opposite signs
168    if ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0))
169        && ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0))
170    {
171        // Proper crossing: compute intersection point
172        let denom = (a2.x() - a1.x()) * (b2.y() - b1.y()) - (a2.y() - a1.y()) * (b2.x() - b1.x());
173
174        if denom.abs() < f64::EPSILON * 1e3 {
175            return SegmentIntersection::None; // Degenerate
176        }
177
178        let t =
179            ((b1.x() - a1.x()) * (b2.y() - b1.y()) - (b1.y() - a1.y()) * (b2.x() - b1.x())) / denom;
180
181        let u =
182            ((b1.x() - a1.x()) * (a2.y() - a1.y()) - (b1.y() - a1.y()) * (a2.x() - a1.x())) / denom;
183
184        let px = (a2.x() - a1.x()).mul_add(t, a1.x());
185        let py = (a2.y() - a1.y()).mul_add(t, a1.y());
186
187        return SegmentIntersection::Point {
188            point: Point2::new(px, py),
189            t1: t,
190            t2: u,
191        };
192    }
193
194    // Check collinear overlap first (all four orientations are zero)
195    if d1 == 0.0 && d2 == 0.0 && d3 == 0.0 && d4 == 0.0 {
196        return collinear_overlap(a1, a2, b1, b2);
197    }
198
199    // Check endpoint-on-segment cases (T-intersections)
200    if d1 == 0.0 && on_segment(a1, a2, b1) {
201        let t = segment_param(a1, a2, b1);
202        return SegmentIntersection::Point {
203            point: b1,
204            t1: t,
205            t2: 0.0,
206        };
207    }
208    if d2 == 0.0 && on_segment(a1, a2, b2) {
209        let t = segment_param(a1, a2, b2);
210        return SegmentIntersection::Point {
211            point: b2,
212            t1: t,
213            t2: 1.0,
214        };
215    }
216    if d3 == 0.0 && on_segment(b1, b2, a1) {
217        let u = segment_param(b1, b2, a1);
218        return SegmentIntersection::Point {
219            point: a1,
220            t1: 0.0,
221            t2: u,
222        };
223    }
224    if d4 == 0.0 && on_segment(b1, b2, a2) {
225        let u = segment_param(b1, b2, a2);
226        return SegmentIntersection::Point {
227            point: a2,
228            t1: 1.0,
229            t2: u,
230        };
231    }
232
233    SegmentIntersection::None
234}
235
236/// Check if point `p` lies on segment `(a, b)` (assuming collinearity).
237fn on_segment(a: Point2, b: Point2, p: Point2) -> bool {
238    let min_x = a.x().min(b.x());
239    let max_x = a.x().max(b.x());
240    let min_y = a.y().min(b.y());
241    let max_y = a.y().max(b.y());
242
243    p.x() >= min_x - f64::EPSILON
244        && p.x() <= max_x + f64::EPSILON
245        && p.y() >= min_y - f64::EPSILON
246        && p.y() <= max_y + f64::EPSILON
247}
248
249/// Compute the parameter of point `p` on segment `(a, b)`.
250fn segment_param(a: Point2, b: Point2, p: Point2) -> f64 {
251    let dx = b.x() - a.x();
252    let dy = b.y() - a.y();
253
254    if dx.abs() > dy.abs() {
255        (p.x() - a.x()) / dx
256    } else if dy.abs() > f64::EPSILON {
257        (p.y() - a.y()) / dy
258    } else {
259        0.0
260    }
261}
262
263/// Handle collinear overlap of two segments.
264fn collinear_overlap(a1: Point2, a2: Point2, b1: Point2, b2: Point2) -> SegmentIntersection {
265    // Project onto the axis with greater extent
266    let dx = (a2.x() - a1.x()).abs().max((b2.x() - b1.x()).abs());
267    let dy = (a2.y() - a1.y()).abs().max((b2.y() - b1.y()).abs());
268
269    let (_ta1, _ta2, tb1_param, tb2_param) = if dx >= dy {
270        let dir = a2.x() - a1.x();
271        if dir.abs() < f64::EPSILON {
272            return SegmentIntersection::None;
273        }
274        (0.0, 1.0, (b1.x() - a1.x()) / dir, (b2.x() - a1.x()) / dir)
275    } else {
276        let dir = a2.y() - a1.y();
277        if dir.abs() < f64::EPSILON {
278            return SegmentIntersection::None;
279        }
280        (0.0, 1.0, (b1.y() - a1.y()) / dir, (b2.y() - a1.y()) / dir)
281    };
282
283    let (tb_lo, tb_hi) = if tb1_param < tb2_param {
284        (tb1_param, tb2_param)
285    } else {
286        (tb2_param, tb1_param)
287    };
288    let lo = 0.0_f64.max(tb_lo);
289    let hi = 1.0_f64.min(tb_hi);
290
291    if lo > hi + f64::EPSILON {
292        SegmentIntersection::None
293    } else if (hi - lo).abs() < f64::EPSILON {
294        // Single point overlap
295        let px = (a2.x() - a1.x()).mul_add(lo, a1.x());
296        let py = (a2.y() - a1.y()).mul_add(lo, a1.y());
297        let pt = Point2::new(px, py);
298        SegmentIntersection::Point {
299            point: pt,
300            t1: lo,
301            t2: segment_param(b1, b2, pt),
302        }
303    } else {
304        let sx = (a2.x() - a1.x()).mul_add(lo, a1.x());
305        let sy = (a2.y() - a1.y()).mul_add(lo, a1.y());
306        let ex = (a2.x() - a1.x()).mul_add(hi, a1.x());
307        let ey = (a2.y() - a1.y()).mul_add(hi, a1.y());
308        SegmentIntersection::Overlap {
309            start: Point2::new(sx, sy),
310            end: Point2::new(ex, ey),
311        }
312    }
313}
314
315#[cfg(test)]
316#[allow(
317    clippy::unwrap_used,
318    clippy::expect_used,
319    clippy::float_cmp,
320    clippy::suboptimal_flops,
321    clippy::panic,
322    clippy::cast_lossless
323)]
324mod tests {
325
326    use super::*;
327    use crate::vec::{Point2, Point3};
328
329    // -- filtered_orient2d -------------------------------------------------
330
331    #[test]
332    fn filtered_orient2d_ccw() {
333        let a = Point2::new(0.0, 0.0);
334        let b = Point2::new(1.0, 0.0);
335        let c = Point2::new(0.0, 1.0);
336        assert!(filtered_orient2d(a, b, c) > 0.0);
337    }
338
339    #[test]
340    fn filtered_orient2d_cw() {
341        let a = Point2::new(0.0, 0.0);
342        let b = Point2::new(0.0, 1.0);
343        let c = Point2::new(1.0, 0.0);
344        assert!(filtered_orient2d(a, b, c) < 0.0);
345    }
346
347    #[test]
348    fn filtered_orient2d_collinear() {
349        let a = Point2::new(0.0, 0.0);
350        let b = Point2::new(1.0, 1.0);
351        let c = Point2::new(2.0, 2.0);
352        assert_eq!(filtered_orient2d(a, b, c), 0.0);
353    }
354
355    #[test]
356    fn filtered_orient2d_near_collinear() {
357        // Points very close to collinear -- should still give correct answer
358        let a = Point2::new(0.0, 0.0);
359        let b = Point2::new(1.0, 1.0);
360        let c = Point2::new(2.0, 2.0 + 1e-15);
361        // The exact predicate should resolve this
362        let result = filtered_orient2d(a, b, c);
363        // c is slightly above the line, so should be positive (CCW)
364        assert!(result >= 0.0);
365    }
366
367    // -- filtered_in_circle ------------------------------------------------
368
369    #[test]
370    fn filtered_in_circle_inside() {
371        let a = Point2::new(0.0, 0.0);
372        let b = Point2::new(1.0, 0.0);
373        let c = Point2::new(0.0, 1.0);
374        let d = Point2::new(0.25, 0.25);
375        assert!(filtered_in_circle(a, b, c, d) > 0.0);
376    }
377
378    #[test]
379    fn filtered_in_circle_outside() {
380        let a = Point2::new(0.0, 0.0);
381        let b = Point2::new(1.0, 0.0);
382        let c = Point2::new(0.0, 1.0);
383        let d = Point2::new(3.0, 3.0);
384        assert!(filtered_in_circle(a, b, c, d) < 0.0);
385    }
386
387    // -- filtered_orient3d -------------------------------------------------
388
389    #[test]
390    fn filtered_orient3d_basic() {
391        let a = Point3::new(0.0, 0.0, 0.0);
392        let b = Point3::new(1.0, 0.0, 0.0);
393        let c = Point3::new(0.0, 1.0, 0.0);
394        // d above the plane => negative (matches robust crate convention)
395        let above = Point3::new(0.0, 0.0, 1.0);
396        assert!(filtered_orient3d(a, b, c, above) < 0.0);
397        // d below the plane => positive
398        let below = Point3::new(0.0, 0.0, -1.0);
399        assert!(filtered_orient3d(a, b, c, below) > 0.0);
400        // d on the plane => zero
401        let on = Point3::new(0.5, 0.5, 0.0);
402        assert_eq!(filtered_orient3d(a, b, c, on), 0.0);
403    }
404
405    // -- segment_intersection ----------------------------------------------
406
407    #[test]
408    fn segment_intersection_crossing() {
409        let a1 = Point2::new(0.0, 0.0);
410        let a2 = Point2::new(1.0, 1.0);
411        let b1 = Point2::new(0.0, 1.0);
412        let b2 = Point2::new(1.0, 0.0);
413
414        match segment_intersection(a1, a2, b1, b2) {
415            SegmentIntersection::Point { point, t1, t2 } => {
416                assert!((point.x() - 0.5).abs() < 1e-10);
417                assert!((point.y() - 0.5).abs() < 1e-10);
418                assert!((t1 - 0.5).abs() < 1e-10);
419                assert!((t2 - 0.5).abs() < 1e-10);
420            }
421            other => panic!("expected Point, got {other:?}"),
422        }
423    }
424
425    #[test]
426    fn segment_intersection_parallel() {
427        let a1 = Point2::new(0.0, 0.0);
428        let a2 = Point2::new(1.0, 0.0);
429        let b1 = Point2::new(0.0, 1.0);
430        let b2 = Point2::new(1.0, 1.0);
431
432        assert_eq!(
433            segment_intersection(a1, a2, b1, b2),
434            SegmentIntersection::None
435        );
436    }
437
438    #[test]
439    fn segment_intersection_t_junction() {
440        let a1 = Point2::new(0.0, 0.0);
441        let a2 = Point2::new(1.0, 0.0);
442        let b1 = Point2::new(0.5, -1.0);
443        let b2 = Point2::new(0.5, 0.0); // endpoint on segment a
444
445        match segment_intersection(a1, a2, b1, b2) {
446            SegmentIntersection::Point { point, .. } => {
447                assert!((point.x() - 0.5).abs() < 1e-10);
448                assert!(point.y().abs() < 1e-10);
449            }
450            other => panic!("expected Point, got {other:?}"),
451        }
452    }
453
454    #[test]
455    fn segment_intersection_collinear_overlap() {
456        let a1 = Point2::new(0.0, 0.0);
457        let a2 = Point2::new(2.0, 0.0);
458        let b1 = Point2::new(1.0, 0.0);
459        let b2 = Point2::new(3.0, 0.0);
460
461        match segment_intersection(a1, a2, b1, b2) {
462            SegmentIntersection::Overlap { start, end } => {
463                assert!((start.x() - 1.0).abs() < 1e-10);
464                assert!((end.x() - 2.0).abs() < 1e-10);
465            }
466            other => panic!("expected Overlap, got {other:?}"),
467        }
468    }
469
470    #[test]
471    fn segment_intersection_disjoint_collinear() {
472        let a1 = Point2::new(0.0, 0.0);
473        let a2 = Point2::new(1.0, 0.0);
474        let b1 = Point2::new(2.0, 0.0);
475        let b2 = Point2::new(3.0, 0.0);
476
477        assert_eq!(
478            segment_intersection(a1, a2, b1, b2),
479            SegmentIntersection::None
480        );
481    }
482
483    #[test]
484    fn segment_intersection_no_intersection() {
485        let a1 = Point2::new(0.0, 0.0);
486        let a2 = Point2::new(1.0, 0.0);
487        let b1 = Point2::new(2.0, 2.0);
488        let b2 = Point2::new(3.0, 3.0);
489
490        assert_eq!(
491            segment_intersection(a1, a2, b1, b2),
492            SegmentIntersection::None
493        );
494    }
495}