Skip to main content

brepkit_math/
region2d.rs

1//! Whether a point lies in a plane region bounded by segments and conic arcs.
2//!
3//! The curves themselves are read, not a chord polygon, which misreads every
4//! point between an arc and its chords.
5
6use std::f64::consts::TAU;
7
8use crate::vec::{Point2, Vec2};
9
10/// A piece of a region's boundary in the plane's 2D frame.
11#[derive(Debug, Clone, Copy)]
12pub enum Boundary2 {
13    /// A straight segment between two points.
14    Segment(Point2, Point2),
15    /// The arc of the ellipse `center + a cos t u + b sin t v` for `t` from
16    /// `t0` to `t1` (`t0 < t1 <= t0 + 2 pi`); a circle when `a == b`. `u` and
17    /// `v` are orthonormal.
18    Arc {
19        /// The ellipse's center.
20        center: Point2,
21        /// The direction of the `a` axis.
22        u: Vec2,
23        /// The direction of the `b` axis.
24        v: Vec2,
25        /// The semi-axis along `u`.
26        a: f64,
27        /// The semi-axis along `v`.
28        b: f64,
29        /// The arc's first angle.
30        t0: f64,
31        /// The arc's last angle.
32        t1: f64,
33    },
34}
35
36/// Ray directions tried in turn, none along an axis or a common diagonal.
37const DIRECTIONS: [f64; 6] = [0.613, 1.931, 2.871, 4.127, 5.369, 0.229];
38
39/// Whether `p` lies inside the region `pieces` enclose, holes included.
40///
41/// Read by the parity of a ray's crossings of every loop's pieces together.
42/// `None` when `p` lies within `tol` of the boundary, or every trial ray
43/// passes within `tol` of a piece's end or touches an arc without crossing.
44#[must_use]
45pub fn point_in_region(pieces: &[Boundary2], p: Point2, tol: f64) -> Option<bool> {
46    'direction: for angle in DIRECTIONS {
47        let d = Vec2::new(angle.cos(), angle.sin());
48        let mut odd = false;
49        for piece in pieces {
50            match crossings(piece, p, d, tol) {
51                Crossing::Count(n) => odd ^= n % 2 == 1,
52                Crossing::Grazes => continue 'direction,
53                Crossing::OnBoundary => return None,
54            }
55        }
56        return Some(odd);
57    }
58    None
59}
60
61/// Whether `p` lies within `tol` of one of the region's boundary pieces.
62#[must_use]
63pub fn on_boundary(pieces: &[Boundary2], p: Point2, tol: f64) -> bool {
64    let along = Vec2::new(1.0, 0.0);
65    pieces
66        .iter()
67        .any(|piece| matches!(crossings(piece, p, along, tol), Crossing::OnBoundary))
68}
69
70/// How a ray from a point meets one boundary piece.
71enum Crossing {
72    /// The point itself lies on the piece.
73    OnBoundary,
74    /// The ray passes a piece's end or touches it without crossing: another
75    /// ray is needed.
76    Grazes,
77    /// The ray crosses the piece this many times.
78    Count(u32),
79}
80
81/// How the ray from `p` along `d` meets `piece`.
82fn crossings(piece: &Boundary2, p: Point2, d: Vec2, tol: f64) -> Crossing {
83    let cross = |x: Vec2, y: Vec2| x.x().mul_add(y.y(), -(x.y() * y.x()));
84    match *piece {
85        Boundary2::Segment(a, b) => {
86            let (ab, ap) = (b - a, p - a);
87            let len = ab.length();
88            if len <= tol {
89                return Crossing::Count(0);
90            }
91            // Within `tol` of the closed segment: a point at a vertex rounds
92            // a hair past the end of both segments meeting there.
93            let along = (ap.dot(ab) / (len * len)).clamp(0.0, 1.0);
94            if (ap - ab * along).length() <= tol {
95                return Crossing::OnBoundary;
96            }
97            let det = cross(d, ab);
98            if det.abs() <= 1e-12 * len {
99                // Parallel: a collinear segment ahead grazes the ray.
100                return if cross(d, ap).abs() <= tol && ap.dot(d) < 0.0 {
101                    Crossing::Grazes
102                } else {
103                    Crossing::Count(0)
104                };
105            }
106            let s = cross(ap * -1.0, ab) / det;
107            let w = cross(ap * -1.0, d) / det;
108            if s <= 0.0 || w < -tol / len || w > 1.0 + tol / len {
109                return Crossing::Count(0);
110            }
111            if w * len <= tol || (1.0 - w) * len <= tol {
112                return Crossing::Grazes;
113            }
114            Crossing::Count(1)
115        }
116        Boundary2::Arc {
117            center,
118            u,
119            v,
120            a,
121            b,
122            t0,
123            t1,
124        } => {
125            let q = p - center;
126            let (x0, y0) = (q.dot(u) / a, q.dot(v) / b);
127            let (dx, dy) = (d.dot(u) / a, d.dot(v) / b);
128            let qa = dx.mul_add(dx, dy * dy);
129            let qb = 2.0 * x0.mul_add(dx, y0 * dy);
130            let qc = x0.mul_add(x0, y0 * y0) - 1.0;
131            let size = a.max(b);
132            let on_arc = |t: f64| t0 + (t - t0).rem_euclid(TAU) <= t1;
133            // On the ellipse (within tol) and on the arc: on the boundary. The
134            // offset is read along the ray from the center, where the ellipse
135            // lies at `1 / rho` of the point's distance.
136            let rho = x0.hypot(y0);
137            let radial = if rho > 0.0 {
138                q.length() * (rho - 1.0).abs() / rho
139            } else {
140                a.min(b)
141            };
142            let end = |t: f64| center + u * (a * t.cos()) + v * (b * t.sin());
143            if (radial <= tol && on_arc(y0.atan2(x0)))
144                || (p - end(t0)).length() <= tol
145                || (p - end(t1)).length() <= tol
146            {
147                return Crossing::OnBoundary;
148            }
149            let disc = qb.mul_add(qb, -4.0 * qa * qc);
150            if disc < 0.0 {
151                return Crossing::Count(0);
152            }
153            let half = disc.sqrt() / (2.0 * qa);
154            if half <= tol {
155                return Crossing::Grazes;
156            }
157            let mut n = 0;
158            for s in [-qb / (2.0 * qa) - half, -qb / (2.0 * qa) + half] {
159                if s <= 0.0 {
160                    continue;
161                }
162                let t = (y0 + s * dy).atan2(x0 + s * dx);
163                let t = t0 + (t - t0).rem_euclid(TAU);
164                let (from_start, to_end) = (t - t0, t1 - t);
165                if (from_start * size <= tol || to_end.abs() * size <= tol)
166                    || (t > t1 && (t - TAU - t0).abs() * size <= tol)
167                {
168                    return Crossing::Grazes;
169                }
170                if t <= t1 {
171                    n += 1;
172                }
173            }
174            Crossing::Count(n)
175        }
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    #![allow(clippy::unwrap_used)]
182    use std::f64::consts::PI;
183
184    use super::*;
185
186    fn circle(center: (f64, f64), r: f64, t0: f64, t1: f64) -> Boundary2 {
187        Boundary2::Arc {
188            center: Point2::new(center.0, center.1),
189            u: Vec2::new(1.0, 0.0),
190            v: Vec2::new(0.0, 1.0),
191            a: r,
192            b: r,
193            t0,
194            t1,
195        }
196    }
197
198    /// A unit disc bounded by one whole circle: a point a sagitta inside its
199    /// rim is inside, however few chords a polygon would give it.
200    #[test]
201    fn a_disc_holds_points_up_to_its_rim() {
202        let disc = [circle((0.0, 0.0), 1.0, 0.0, TAU)];
203        for r in [0.0, 0.5, 0.999, 0.9999] {
204            for k in 0..12 {
205                let t = f64::from(k) * PI / 6.0 + 0.1;
206                let p = Point2::new(r * t.cos(), r * t.sin());
207                assert_eq!(point_in_region(&disc, p, 1e-9), Some(true), "r {r} t {t}");
208            }
209        }
210        assert_eq!(
211            point_in_region(&disc, Point2::new(1.001, 0.0), 1e-9),
212            Some(false)
213        );
214        assert_eq!(point_in_region(&disc, Point2::new(1.0, 0.0), 1e-9), None);
215    }
216
217    /// A triangle's corners, nudged by rounding past the ends of both edges
218    /// meeting there, and an arc's ends, read on the boundary.
219    #[test]
220    fn points_at_corners_read_on_the_boundary() {
221        let (a, b, c) = (
222            Point2::new(-18.75, -16.75),
223            Point2::new(-20.673_141_121_612_92, -17.530_361_288_064_512),
224            Point2::new(-20.75, 4.0),
225        );
226        let triangle = [
227            Boundary2::Segment(a, b),
228            Boundary2::Segment(b, c),
229            Boundary2::Segment(c, a),
230        ];
231        for corner in [a, b, c] {
232            for (dx, dy) in [(0.0, 0.0), (3e-15, -2e-15), (-4e-15, 1e-15)] {
233                let p = Point2::new(corner.x() + dx, corner.y() + dy);
234                assert_eq!(point_in_region(&triangle, p, 1e-9), None, "{p:?}");
235            }
236        }
237        let half = [
238            circle((0.0, 0.0), 2.0, 0.3, 2.9),
239            Boundary2::Segment(
240                Point2::new(2.0 * 2.9_f64.cos(), 2.0 * 2.9_f64.sin()),
241                Point2::new(2.0 * 0.3_f64.cos(), 2.0 * 0.3_f64.sin()),
242            ),
243        ];
244        for t in [0.3_f64, 2.9] {
245            let p = Point2::new(2.0 * t.cos() + 1e-15, 2.0 * t.sin());
246            assert_eq!(point_in_region(&half, p, 1e-9), None, "t {t}");
247        }
248    }
249
250    /// Points on a side or an arc read on the boundary, and points a ray
251    /// from which grazes a corner do not.
252    #[test]
253    fn on_boundary_reads_distance_not_ray_parity() {
254        let half = [
255            circle((0.0, 0.0), 2.0, 0.0, PI),
256            Boundary2::Segment(Point2::new(-2.0, 0.0), Point2::new(2.0, 0.0)),
257        ];
258        assert!(on_boundary(&half, Point2::new(0.5, 0.0), 1e-9));
259        assert!(on_boundary(&half, Point2::new(0.0, 2.0), 1e-9));
260        assert!(on_boundary(&half, Point2::new(2.0, 1e-15), 1e-9));
261        assert!(!on_boundary(&half, Point2::new(0.5, 0.5), 1e-9));
262        assert!(!on_boundary(&half, Point2::new(0.0, 2.1), 1e-9));
263    }
264
265    /// A half disc: its diameter and a half circle. Points just inside the
266    /// arc read inside, points past the diameter outside.
267    #[test]
268    fn a_half_disc_of_a_segment_and_an_arc() {
269        let half = [
270            circle((0.0, 0.0), 2.0, 0.0, PI),
271            Boundary2::Segment(Point2::new(-2.0, 0.0), Point2::new(2.0, 0.0)),
272        ];
273        assert_eq!(
274            point_in_region(&half, Point2::new(0.0, 1.999), 1e-9),
275            Some(true)
276        );
277        assert_eq!(
278            point_in_region(&half, Point2::new(1.4, 1.4), 1e-9),
279            Some(true)
280        );
281        assert_eq!(
282            point_in_region(&half, Point2::new(0.0, -0.001), 1e-9),
283            Some(false)
284        );
285        assert_eq!(
286            point_in_region(&half, Point2::new(1.42, 1.42), 1e-9),
287            Some(false)
288        );
289    }
290
291    /// A square with a round hole: the hole's inside reads outside, the ring
292    /// inside.
293    #[test]
294    fn a_square_with_a_round_hole() {
295        let (lo, hi) = (Point2::new(-3.0, -3.0), Point2::new(3.0, 3.0));
296        let region = [
297            Boundary2::Segment(lo, Point2::new(hi.x(), lo.y())),
298            Boundary2::Segment(Point2::new(hi.x(), lo.y()), hi),
299            Boundary2::Segment(hi, Point2::new(lo.x(), hi.y())),
300            Boundary2::Segment(Point2::new(lo.x(), hi.y()), lo),
301            circle((0.0, 0.0), 1.0, 0.0, TAU),
302        ];
303        assert_eq!(
304            point_in_region(&region, Point2::new(0.0, 0.0), 1e-9),
305            Some(false)
306        );
307        assert_eq!(
308            point_in_region(&region, Point2::new(0.0, 1.0005), 1e-9),
309            Some(true)
310        );
311        assert_eq!(
312            point_in_region(&region, Point2::new(0.0, 0.9995), 1e-9),
313            Some(false)
314        );
315        assert_eq!(
316            point_in_region(&region, Point2::new(2.9, -2.9), 1e-9),
317            Some(true)
318        );
319        assert_eq!(
320            point_in_region(&region, Point2::new(3.1, 0.0), 1e-9),
321            Some(false)
322        );
323    }
324
325    /// An ellipse 3 by 1, turned: a point just inside its end reads inside.
326    #[test]
327    fn a_turned_ellipse() {
328        let (c, s) = (0.5_f64.cos(), 0.5_f64.sin());
329        let ellipse = [Boundary2::Arc {
330            center: Point2::new(1.0, 2.0),
331            u: Vec2::new(c, s),
332            v: Vec2::new(-s, c),
333            a: 3.0,
334            b: 1.0,
335            t0: 0.3,
336            t1: 0.3 + TAU,
337        }];
338        let at = |x: f64, y: f64| Point2::new(1.0 + x * c - y * s, 2.0 + x * s + y * c);
339        assert_eq!(point_in_region(&ellipse, at(2.999, 0.0), 1e-9), Some(true));
340        assert_eq!(point_in_region(&ellipse, at(3.001, 0.0), 1e-9), Some(false));
341        assert_eq!(point_in_region(&ellipse, at(0.0, 0.999), 1e-9), Some(true));
342    }
343
344    /// The first trial ray from a point passes through a triangle's vertex:
345    /// it grazes, the next ray decides, inside and outside alike.
346    #[test]
347    fn a_ray_through_a_vertex_tries_another() {
348        let d = Vec2::new(DIRECTIONS[0].cos(), DIRECTIONS[0].sin());
349        let apex = Point2::new(0.0, 0.0) + d * 2.0;
350        let (b, c) = (Point2::new(-2.0, 0.5), Point2::new(0.5, -2.0));
351        let triangle = [
352            Boundary2::Segment(apex, b),
353            Boundary2::Segment(b, c),
354            Boundary2::Segment(c, apex),
355        ];
356        assert_eq!(
357            point_in_region(&triangle, Point2::new(0.0, 0.0), 1e-9),
358            Some(true)
359        );
360        let behind = Point2::new(0.0, 0.0) + d * -3.0;
361        assert_eq!(point_in_region(&triangle, behind, 1e-9), Some(false));
362    }
363
364    /// A side lying along the first trial ray: the ray grazes it, and the
365    /// next ray reads the point outside.
366    #[test]
367    fn a_side_along_the_ray_tries_another() {
368        let d = Vec2::new(DIRECTIONS[0].cos(), DIRECTIONS[0].sin());
369        let n = Vec2::new(-d.y(), d.x());
370        let p = Point2::new(0.0, 0.0);
371        let (q1, q2) = (p + d, p + d * 2.0);
372        let q3 = p + d * 1.5 + n;
373        let triangle = [
374            Boundary2::Segment(q1, q2),
375            Boundary2::Segment(q2, q3),
376            Boundary2::Segment(q3, q1),
377        ];
378        assert_eq!(point_in_region(&triangle, p, 1e-9), Some(false));
379    }
380
381    /// The segment of a unit circle between angles 2.5 and 4.0, across the
382    /// angle's wrap at pi, closed by its chord.
383    #[test]
384    fn an_arc_across_the_wrap() {
385        let (t0, t1) = (2.5_f64, 4.0_f64);
386        let segment = [
387            circle((0.0, 0.0), 1.0, t0, t1),
388            Boundary2::Segment(
389                Point2::new(t1.cos(), t1.sin()),
390                Point2::new(t0.cos(), t0.sin()),
391            ),
392        ];
393        assert_eq!(
394            point_in_region(&segment, Point2::new(-0.9, 0.0), 1e-9),
395            Some(true)
396        );
397        assert_eq!(
398            point_in_region(&segment, Point2::new(-0.7, 0.0), 1e-9),
399            Some(false)
400        );
401        assert_eq!(
402            point_in_region(&segment, Point2::new(-1.01, 0.0), 1e-9),
403            Some(false)
404        );
405    }
406
407    /// The upper half of an ellipse 3 by 1, closed by its major axis.
408    #[test]
409    fn half_an_ellipse() {
410        let half = [
411            Boundary2::Arc {
412                center: Point2::new(0.0, 0.0),
413                u: Vec2::new(1.0, 0.0),
414                v: Vec2::new(0.0, 1.0),
415                a: 3.0,
416                b: 1.0,
417                t0: 0.0,
418                t1: PI,
419            },
420            Boundary2::Segment(Point2::new(-3.0, 0.0), Point2::new(3.0, 0.0)),
421        ];
422        assert_eq!(
423            point_in_region(&half, Point2::new(0.0, 0.99), 1e-9),
424            Some(true)
425        );
426        assert_eq!(
427            point_in_region(&half, Point2::new(0.0, 1.01), 1e-9),
428            Some(false)
429        );
430        assert_eq!(
431            point_in_region(&half, Point2::new(2.9, 0.1), 1e-9),
432            Some(true)
433        );
434        assert_eq!(
435            point_in_region(&half, Point2::new(2.9, 0.3), 1e-9),
436            Some(false)
437        );
438        assert_eq!(
439            point_in_region(&half, Point2::new(0.0, -0.1), 1e-9),
440            Some(false)
441        );
442    }
443
444    /// A long ellipse's end: a point 5e-7 past it is off the boundary.
445    #[test]
446    fn a_long_ellipse_reads_its_end_by_distance() {
447        let ellipse = [Boundary2::Arc {
448            center: Point2::new(0.0, 0.0),
449            u: Vec2::new(1.0, 0.0),
450            v: Vec2::new(0.0, 1.0),
451            a: 100.0,
452            b: 1.0,
453            t0: 0.0,
454            t1: TAU,
455        }];
456        let past = Point2::new(100.000_000_5, 0.0);
457        assert_eq!(point_in_region(&ellipse, past, 1e-7), Some(false));
458    }
459}