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/// The distance from `p` to the nearest of `pieces`; infinite with none.
71#[must_use]
72pub fn distance_to_boundary(pieces: &[Boundary2], p: Point2) -> f64 {
73    pieces
74        .iter()
75        .map(|piece| distance_to_piece(piece, p))
76        .fold(f64::INFINITY, f64::min)
77}
78
79/// The distance from `p` to `piece`.
80///
81/// Exact for a segment and a circular arc. For an elliptical arc, a branch
82/// and bound over its angles that never reads more than the true distance
83/// and falls short of it by a millionth or so.
84#[must_use]
85pub fn distance_to_piece(piece: &Boundary2, p: Point2) -> f64 {
86    match *piece {
87        Boundary2::Segment(a, b) => {
88            let (ab, ap) = (b - a, p - a);
89            let len2 = ab.dot(ab);
90            let along = if len2 > 0.0 {
91                (ap.dot(ab) / len2).clamp(0.0, 1.0)
92            } else {
93                0.0
94            };
95            (ap - ab * along).length()
96        }
97        Boundary2::Arc {
98            center,
99            u,
100            v,
101            a,
102            b,
103            t0,
104            t1,
105        } => {
106            let q = p - center;
107            let (x0, y0) = (q.dot(u), q.dot(v));
108            #[allow(clippy::float_cmp)]
109            let circle = a == b;
110            if !circle {
111                return ellipse_arc_distance(x0, y0, a, b, t0, t1);
112            }
113            if t0 + (y0.atan2(x0) - t0).rem_euclid(TAU) <= t1 {
114                return (x0.hypot(y0) - a).abs();
115            }
116            let end = |t: f64| (a.mul_add(-t.cos(), x0)).hypot(b.mul_add(-t.sin(), y0));
117            end(t0).min(end(t1))
118        }
119    }
120}
121
122/// The most halvings of an elliptical arc's angles a distance reads.
123const ELLIPSE_DEPTH: u32 = 60;
124
125/// The distance from `(x0, y0)` to the arc `(a cos t, b sin t)` for `t` from
126/// `t0` to `t1`, by branch and bound on the squared distance `F`.
127///
128/// On an angle interval of half-width `r` about `m`, `F` is at least
129/// `F(m) - |F'(m)| r - K r^2 / 2`, with `K = 2 (|a^2 - b^2| + a |x0| + b |y0|)`
130/// bounding `|F''|`. An interval whose bound cannot undercut the least `F`
131/// seen, less a millionth of it, is dropped; the rest are halved.
132fn ellipse_arc_distance(x0: f64, y0: f64, a: f64, b: f64, t0: f64, t1: f64) -> f64 {
133    // Bounds that cannot be compared prune nothing, and the halving would
134    // run to its full depth everywhere: such an arc reads as touching.
135    const MAX_INTERVALS: usize = 1 << 20;
136    if ![x0, y0, a, b, t0, t1].iter().all(|v| v.is_finite()) {
137        return 0.0;
138    }
139    let squared = |t: f64| {
140        let (s, c) = t.sin_cos();
141        a.mul_add(c, -x0).powi(2) + b.mul_add(s, -y0).powi(2)
142    };
143    let slope = |t: f64| {
144        let (s, c) = t.sin_cos();
145        2.0 * (a.mul_add(c, -x0) * (-a * s) + b.mul_add(s, -y0) * (b * c))
146    };
147    let k = 2.0 * (a.mul_add(a, -b * b).abs() + a * x0.abs() + b * y0.abs());
148    let noise = (1e-15 * a.max(b).max(x0.hypot(y0))).powi(2);
149    let slack = |best: f64| 1e-6_f64.mul_add(best, noise);
150    let mut best = squared(t0).min(squared(t1));
151    let mut unsettled = f64::INFINITY;
152    let mut pending = vec![(t0, t1, 0_u32)];
153    let mut visited = 0_usize;
154    while let Some((lo, hi, depth)) = pending.pop() {
155        visited += 1;
156        let (m, r) = (0.5 * (lo + hi), 0.5 * (hi - lo));
157        let at_m = squared(m);
158        best = best.min(at_m);
159        let low = at_m - slope(m).abs() * r - 0.5 * k * r * r;
160        if low.is_nan() || visited > MAX_INTERVALS {
161            return 0.0;
162        }
163        if low >= best - slack(best) {
164            continue;
165        }
166        if depth == ELLIPSE_DEPTH {
167            unsettled = unsettled.min(low);
168            continue;
169        }
170        pending.push((lo, m, depth + 1));
171        pending.push((m, hi, depth + 1));
172    }
173    (best - slack(best)).min(unsettled).max(0.0).sqrt()
174}
175
176/// How a ray from a point meets one boundary piece.
177enum Crossing {
178    /// The point itself lies on the piece.
179    OnBoundary,
180    /// The ray passes a piece's end or touches it without crossing: another
181    /// ray is needed.
182    Grazes,
183    /// The ray crosses the piece this many times.
184    Count(u32),
185}
186
187/// How the ray from `p` along `d` meets `piece`.
188fn crossings(piece: &Boundary2, p: Point2, d: Vec2, tol: f64) -> Crossing {
189    let cross = |x: Vec2, y: Vec2| x.x().mul_add(y.y(), -(x.y() * y.x()));
190    match *piece {
191        Boundary2::Segment(a, b) => {
192            let (ab, ap) = (b - a, p - a);
193            let len = ab.length();
194            if len <= tol {
195                return Crossing::Count(0);
196            }
197            // Within `tol` of the closed segment: a point at a vertex rounds
198            // a hair past the end of both segments meeting there.
199            let along = (ap.dot(ab) / (len * len)).clamp(0.0, 1.0);
200            if (ap - ab * along).length() <= tol {
201                return Crossing::OnBoundary;
202            }
203            let det = cross(d, ab);
204            if det.abs() <= 1e-12 * len {
205                // Parallel: a collinear segment ahead grazes the ray.
206                return if cross(d, ap).abs() <= tol && ap.dot(d) < 0.0 {
207                    Crossing::Grazes
208                } else {
209                    Crossing::Count(0)
210                };
211            }
212            let s = cross(ap * -1.0, ab) / det;
213            let w = cross(ap * -1.0, d) / det;
214            if s <= 0.0 || w < -tol / len || w > 1.0 + tol / len {
215                return Crossing::Count(0);
216            }
217            if w * len <= tol || (1.0 - w) * len <= tol {
218                return Crossing::Grazes;
219            }
220            Crossing::Count(1)
221        }
222        Boundary2::Arc {
223            center,
224            u,
225            v,
226            a,
227            b,
228            t0,
229            t1,
230        } => {
231            let q = p - center;
232            let (x0, y0) = (q.dot(u) / a, q.dot(v) / b);
233            let (dx, dy) = (d.dot(u) / a, d.dot(v) / b);
234            let qa = dx.mul_add(dx, dy * dy);
235            let qb = 2.0 * x0.mul_add(dx, y0 * dy);
236            let qc = x0.mul_add(x0, y0 * y0) - 1.0;
237            let size = a.max(b);
238            let on_arc = |t: f64| t0 + (t - t0).rem_euclid(TAU) <= t1;
239            // On the ellipse (within tol) and on the arc: on the boundary. The
240            // offset is read along the ray from the center, where the ellipse
241            // lies at `1 / rho` of the point's distance.
242            let rho = x0.hypot(y0);
243            let radial = if rho > 0.0 {
244                q.length() * (rho - 1.0).abs() / rho
245            } else {
246                a.min(b)
247            };
248            let end = |t: f64| center + u * (a * t.cos()) + v * (b * t.sin());
249            if (radial <= tol && on_arc(y0.atan2(x0)))
250                || (p - end(t0)).length() <= tol
251                || (p - end(t1)).length() <= tol
252            {
253                return Crossing::OnBoundary;
254            }
255            let disc = qb.mul_add(qb, -4.0 * qa * qc);
256            if disc < 0.0 {
257                return Crossing::Count(0);
258            }
259            let half = disc.sqrt() / (2.0 * qa);
260            if half <= tol {
261                return Crossing::Grazes;
262            }
263            let mut n = 0;
264            for s in [-qb / (2.0 * qa) - half, -qb / (2.0 * qa) + half] {
265                if s <= 0.0 {
266                    continue;
267                }
268                let t = (y0 + s * dy).atan2(x0 + s * dx);
269                let t = t0 + (t - t0).rem_euclid(TAU);
270                let (from_start, to_end) = (t - t0, t1 - t);
271                if (from_start * size <= tol || to_end.abs() * size <= tol)
272                    || (t > t1 && (t - TAU - t0).abs() * size <= tol)
273                {
274                    return Crossing::Grazes;
275                }
276                if t <= t1 {
277                    n += 1;
278                }
279            }
280            Crossing::Count(n)
281        }
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    #![allow(clippy::unwrap_used)]
288    use std::f64::consts::PI;
289
290    use super::*;
291
292    /// An arc given a non-finite value reads as touching the point at once
293    /// rather than halving without end.
294    #[test]
295    fn a_non_finite_ellipse_arc_reads_as_touching() {
296        for bad in [f64::NAN, f64::INFINITY] {
297            let off_centre = super::ellipse_arc_distance(bad, 0.0, 2.0, 1.0, 0.0, 1.0);
298            let off_range = super::ellipse_arc_distance(0.5, 0.0, 2.0, 1.0, 0.0, bad);
299            assert!(off_centre.abs() < f64::EPSILON && off_range.abs() < f64::EPSILON);
300        }
301    }
302
303    fn circle(center: (f64, f64), r: f64, t0: f64, t1: f64) -> Boundary2 {
304        Boundary2::Arc {
305            center: Point2::new(center.0, center.1),
306            u: Vec2::new(1.0, 0.0),
307            v: Vec2::new(0.0, 1.0),
308            a: r,
309            b: r,
310            t0,
311            t1,
312        }
313    }
314
315    /// A unit disc bounded by one whole circle: a point a sagitta inside its
316    /// rim is inside, however few chords a polygon would give it.
317    #[test]
318    fn a_disc_holds_points_up_to_its_rim() {
319        let disc = [circle((0.0, 0.0), 1.0, 0.0, TAU)];
320        for r in [0.0, 0.5, 0.999, 0.9999] {
321            for k in 0..12 {
322                let t = f64::from(k) * PI / 6.0 + 0.1;
323                let p = Point2::new(r * t.cos(), r * t.sin());
324                assert_eq!(point_in_region(&disc, p, 1e-9), Some(true), "r {r} t {t}");
325            }
326        }
327        assert_eq!(
328            point_in_region(&disc, Point2::new(1.001, 0.0), 1e-9),
329            Some(false)
330        );
331        assert_eq!(point_in_region(&disc, Point2::new(1.0, 0.0), 1e-9), None);
332    }
333
334    /// A triangle's corners, nudged by rounding past the ends of both edges
335    /// meeting there, and an arc's ends, read on the boundary.
336    #[test]
337    fn points_at_corners_read_on_the_boundary() {
338        let (a, b, c) = (
339            Point2::new(-18.75, -16.75),
340            Point2::new(-20.673_141_121_612_92, -17.530_361_288_064_512),
341            Point2::new(-20.75, 4.0),
342        );
343        let triangle = [
344            Boundary2::Segment(a, b),
345            Boundary2::Segment(b, c),
346            Boundary2::Segment(c, a),
347        ];
348        for corner in [a, b, c] {
349            for (dx, dy) in [(0.0, 0.0), (3e-15, -2e-15), (-4e-15, 1e-15)] {
350                let p = Point2::new(corner.x() + dx, corner.y() + dy);
351                assert_eq!(point_in_region(&triangle, p, 1e-9), None, "{p:?}");
352            }
353        }
354        let half = [
355            circle((0.0, 0.0), 2.0, 0.3, 2.9),
356            Boundary2::Segment(
357                Point2::new(2.0 * 2.9_f64.cos(), 2.0 * 2.9_f64.sin()),
358                Point2::new(2.0 * 0.3_f64.cos(), 2.0 * 0.3_f64.sin()),
359            ),
360        ];
361        for t in [0.3_f64, 2.9] {
362            let p = Point2::new(2.0 * t.cos() + 1e-15, 2.0 * t.sin());
363            assert_eq!(point_in_region(&half, p, 1e-9), None, "t {t}");
364        }
365    }
366
367    /// Points on a side or an arc read on the boundary, and points a ray
368    /// from which grazes a corner do not.
369    #[test]
370    fn on_boundary_reads_distance_not_ray_parity() {
371        let half = [
372            circle((0.0, 0.0), 2.0, 0.0, PI),
373            Boundary2::Segment(Point2::new(-2.0, 0.0), Point2::new(2.0, 0.0)),
374        ];
375        assert!(on_boundary(&half, Point2::new(0.5, 0.0), 1e-9));
376        assert!(on_boundary(&half, Point2::new(0.0, 2.0), 1e-9));
377        assert!(on_boundary(&half, Point2::new(2.0, 1e-15), 1e-9));
378        assert!(!on_boundary(&half, Point2::new(0.5, 0.5), 1e-9));
379        assert!(!on_boundary(&half, Point2::new(0.0, 2.1), 1e-9));
380    }
381
382    /// A half disc: its diameter and a half circle. Points just inside the
383    /// arc read inside, points past the diameter outside.
384    #[test]
385    fn a_half_disc_of_a_segment_and_an_arc() {
386        let half = [
387            circle((0.0, 0.0), 2.0, 0.0, PI),
388            Boundary2::Segment(Point2::new(-2.0, 0.0), Point2::new(2.0, 0.0)),
389        ];
390        assert_eq!(
391            point_in_region(&half, Point2::new(0.0, 1.999), 1e-9),
392            Some(true)
393        );
394        assert_eq!(
395            point_in_region(&half, Point2::new(1.4, 1.4), 1e-9),
396            Some(true)
397        );
398        assert_eq!(
399            point_in_region(&half, Point2::new(0.0, -0.001), 1e-9),
400            Some(false)
401        );
402        assert_eq!(
403            point_in_region(&half, Point2::new(1.42, 1.42), 1e-9),
404            Some(false)
405        );
406    }
407
408    /// A square with a round hole: the hole's inside reads outside, the ring
409    /// inside.
410    #[test]
411    fn a_square_with_a_round_hole() {
412        let (lo, hi) = (Point2::new(-3.0, -3.0), Point2::new(3.0, 3.0));
413        let region = [
414            Boundary2::Segment(lo, Point2::new(hi.x(), lo.y())),
415            Boundary2::Segment(Point2::new(hi.x(), lo.y()), hi),
416            Boundary2::Segment(hi, Point2::new(lo.x(), hi.y())),
417            Boundary2::Segment(Point2::new(lo.x(), hi.y()), lo),
418            circle((0.0, 0.0), 1.0, 0.0, TAU),
419        ];
420        assert_eq!(
421            point_in_region(&region, Point2::new(0.0, 0.0), 1e-9),
422            Some(false)
423        );
424        assert_eq!(
425            point_in_region(&region, Point2::new(0.0, 1.0005), 1e-9),
426            Some(true)
427        );
428        assert_eq!(
429            point_in_region(&region, Point2::new(0.0, 0.9995), 1e-9),
430            Some(false)
431        );
432        assert_eq!(
433            point_in_region(&region, Point2::new(2.9, -2.9), 1e-9),
434            Some(true)
435        );
436        assert_eq!(
437            point_in_region(&region, Point2::new(3.1, 0.0), 1e-9),
438            Some(false)
439        );
440    }
441
442    /// An ellipse 3 by 1, turned: a point just inside its end reads inside.
443    #[test]
444    fn a_turned_ellipse() {
445        let (c, s) = (0.5_f64.cos(), 0.5_f64.sin());
446        let ellipse = [Boundary2::Arc {
447            center: Point2::new(1.0, 2.0),
448            u: Vec2::new(c, s),
449            v: Vec2::new(-s, c),
450            a: 3.0,
451            b: 1.0,
452            t0: 0.3,
453            t1: 0.3 + TAU,
454        }];
455        let at = |x: f64, y: f64| Point2::new(1.0 + x * c - y * s, 2.0 + x * s + y * c);
456        assert_eq!(point_in_region(&ellipse, at(2.999, 0.0), 1e-9), Some(true));
457        assert_eq!(point_in_region(&ellipse, at(3.001, 0.0), 1e-9), Some(false));
458        assert_eq!(point_in_region(&ellipse, at(0.0, 0.999), 1e-9), Some(true));
459    }
460
461    /// The first trial ray from a point passes through a triangle's vertex:
462    /// it grazes, the next ray decides, inside and outside alike.
463    #[test]
464    fn a_ray_through_a_vertex_tries_another() {
465        let d = Vec2::new(DIRECTIONS[0].cos(), DIRECTIONS[0].sin());
466        let apex = Point2::new(0.0, 0.0) + d * 2.0;
467        let (b, c) = (Point2::new(-2.0, 0.5), Point2::new(0.5, -2.0));
468        let triangle = [
469            Boundary2::Segment(apex, b),
470            Boundary2::Segment(b, c),
471            Boundary2::Segment(c, apex),
472        ];
473        assert_eq!(
474            point_in_region(&triangle, Point2::new(0.0, 0.0), 1e-9),
475            Some(true)
476        );
477        let behind = Point2::new(0.0, 0.0) + d * -3.0;
478        assert_eq!(point_in_region(&triangle, behind, 1e-9), Some(false));
479    }
480
481    /// A side lying along the first trial ray: the ray grazes it, and the
482    /// next ray reads the point outside.
483    #[test]
484    fn a_side_along_the_ray_tries_another() {
485        let d = Vec2::new(DIRECTIONS[0].cos(), DIRECTIONS[0].sin());
486        let n = Vec2::new(-d.y(), d.x());
487        let p = Point2::new(0.0, 0.0);
488        let (q1, q2) = (p + d, p + d * 2.0);
489        let q3 = p + d * 1.5 + n;
490        let triangle = [
491            Boundary2::Segment(q1, q2),
492            Boundary2::Segment(q2, q3),
493            Boundary2::Segment(q3, q1),
494        ];
495        assert_eq!(point_in_region(&triangle, p, 1e-9), Some(false));
496    }
497
498    /// The segment of a unit circle between angles 2.5 and 4.0, across the
499    /// angle's wrap at pi, closed by its chord.
500    #[test]
501    fn an_arc_across_the_wrap() {
502        let (t0, t1) = (2.5_f64, 4.0_f64);
503        let segment = [
504            circle((0.0, 0.0), 1.0, t0, t1),
505            Boundary2::Segment(
506                Point2::new(t1.cos(), t1.sin()),
507                Point2::new(t0.cos(), t0.sin()),
508            ),
509        ];
510        assert_eq!(
511            point_in_region(&segment, Point2::new(-0.9, 0.0), 1e-9),
512            Some(true)
513        );
514        assert_eq!(
515            point_in_region(&segment, Point2::new(-0.7, 0.0), 1e-9),
516            Some(false)
517        );
518        assert_eq!(
519            point_in_region(&segment, Point2::new(-1.01, 0.0), 1e-9),
520            Some(false)
521        );
522    }
523
524    /// The upper half of an ellipse 3 by 1, closed by its major axis.
525    #[test]
526    fn half_an_ellipse() {
527        let half = [
528            Boundary2::Arc {
529                center: Point2::new(0.0, 0.0),
530                u: Vec2::new(1.0, 0.0),
531                v: Vec2::new(0.0, 1.0),
532                a: 3.0,
533                b: 1.0,
534                t0: 0.0,
535                t1: PI,
536            },
537            Boundary2::Segment(Point2::new(-3.0, 0.0), Point2::new(3.0, 0.0)),
538        ];
539        assert_eq!(
540            point_in_region(&half, Point2::new(0.0, 0.99), 1e-9),
541            Some(true)
542        );
543        assert_eq!(
544            point_in_region(&half, Point2::new(0.0, 1.01), 1e-9),
545            Some(false)
546        );
547        assert_eq!(
548            point_in_region(&half, Point2::new(2.9, 0.1), 1e-9),
549            Some(true)
550        );
551        assert_eq!(
552            point_in_region(&half, Point2::new(2.9, 0.3), 1e-9),
553            Some(false)
554        );
555        assert_eq!(
556            point_in_region(&half, Point2::new(0.0, -0.1), 1e-9),
557            Some(false)
558        );
559    }
560
561    /// A long ellipse's end: a point 5e-7 past it is off the boundary.
562    #[test]
563    fn a_long_ellipse_reads_its_end_by_distance() {
564        let ellipse = [Boundary2::Arc {
565            center: Point2::new(0.0, 0.0),
566            u: Vec2::new(1.0, 0.0),
567            v: Vec2::new(0.0, 1.0),
568            a: 100.0,
569            b: 1.0,
570            t0: 0.0,
571            t1: TAU,
572        }];
573        let past = Point2::new(100.000_000_5, 0.0);
574        assert_eq!(point_in_region(&ellipse, past, 1e-7), Some(false));
575    }
576
577    /// A segment's distance runs to its nearest point, its ends included,
578    /// and a zero-length one is a point.
579    #[test]
580    fn distance_to_a_segment() {
581        let seg = Boundary2::Segment(Point2::new(0.0, 0.0), Point2::new(2.0, 0.0));
582        let at = |x: f64, y: f64| distance_to_piece(&seg, Point2::new(x, y));
583        assert!((at(1.0, 0.5) - 0.5).abs() < 1e-15);
584        assert!((at(3.0, 0.0) - 1.0).abs() < 1e-15);
585        assert!((at(-3.0, 4.0) - 5.0).abs() < 1e-15);
586        let dot = Boundary2::Segment(Point2::new(1.0, 1.0), Point2::new(1.0, 1.0));
587        assert!((distance_to_piece(&dot, Point2::new(4.0, 5.0)) - 5.0).abs() < 1e-15);
588    }
589
590    /// A circular arc's distance is radial where the point's angle lies on
591    /// the arc, and to the nearer end where it does not.
592    #[test]
593    fn distance_to_a_circular_arc() {
594        let arc = circle((1.0, 1.0), 2.0, 0.0, PI / 2.0);
595        let at = |x: f64, y: f64| distance_to_piece(&arc, Point2::new(x, y));
596        assert!((at(1.0 + 3.0 * 0.6, 1.0 + 3.0 * 0.8) - 1.0).abs() < 1e-14);
597        assert!((at(1.0 + 0.6, 1.0 + 0.8) - 1.0).abs() < 1e-14);
598        assert!((at(1.0, 1.0) - 2.0).abs() < 1e-14);
599        assert!((at(1.0, -1.0) - 2.0_f64.hypot(2.0)).abs() < 1e-14);
600        assert!((at(-2.0, 1.0) - 3.0_f64.hypot(2.0)).abs() < 1e-14);
601    }
602
603    /// The least distance from `p` to the ellipse arc, by dense sampling
604    /// refined by golden section: a value the curve reaches.
605    fn sampled_ellipse_distance(arc: &Boundary2, p: Point2) -> f64 {
606        let Boundary2::Arc {
607            center,
608            u,
609            v,
610            a,
611            b,
612            t0,
613            t1,
614        } = *arc
615        else {
616            return f64::NAN;
617        };
618        let at = |t: f64| (center + u * (a * t.cos()) + v * (b * t.sin()) - p).length();
619        let n = 20_000;
620        let step = (t1 - t0) / f64::from(n);
621        let mut best = (at(t0), t0);
622        for k in 1..=n {
623            let t = step.mul_add(f64::from(k), t0);
624            if at(t) < best.0 {
625                best = (at(t), t);
626            }
627        }
628        let (mut lo, mut hi) = ((best.1 - step).max(t0), (best.1 + step).min(t1));
629        for _ in 0..100 {
630            let (m1, m2) = (lo + 0.382 * (hi - lo), lo + 0.618 * (hi - lo));
631            if at(m1) < at(m2) {
632                hi = m2;
633            } else {
634                lo = m1;
635            }
636        }
637        best.0.min(at(0.5 * (lo + hi)))
638    }
639
640    /// An ellipse's distance never exceeds what the curve reaches and falls
641    /// short of it by no more than a millionth: from outside, from near the
642    /// centre where two angles are nearest, from on and beside the curve,
643    /// and from past an arc's end.
644    #[test]
645    fn distance_to_an_elliptical_arc() {
646        let (c, s) = (0.5_f64.cos(), 0.5_f64.sin());
647        let turned = |t0: f64, t1: f64| Boundary2::Arc {
648            center: Point2::new(1.0, 2.0),
649            u: Vec2::new(c, s),
650            v: Vec2::new(-s, c),
651            a: 3.0,
652            b: 1.0,
653            t0,
654            t1,
655        };
656        let local = |x: f64, y: f64| Point2::new(1.0 + x * c - y * s, 2.0 + x * s + y * c);
657        let whole = turned(0.3, 0.3 + TAU);
658        let half = turned(0.0, PI);
659        let cases = [
660            (&whole, local(5.0, 4.0)),
661            (&whole, local(0.1, 0.05)),
662            (&whole, local(2.0, 0.0)),
663            (&whole, local(3.0 * 0.8_f64.cos(), 0.8_f64.sin())),
664            (
665                &whole,
666                local(3.0 * 0.8_f64.cos(), 1.000_001 * 0.8_f64.sin()),
667            ),
668            (&half, local(0.5, -2.0)),
669            (&half, local(-3.5, -0.5)),
670        ];
671        for (arc, p) in cases {
672            let read = distance_to_piece(arc, p);
673            let reached = sampled_ellipse_distance(arc, p);
674            assert!(read <= reached + 1e-12, "{p:?}: {read} above {reached}");
675            assert!(
676                read >= reached * (1.0 - 1e-6) - 1e-12,
677                "{p:?}: {read} below {reached}"
678            );
679        }
680    }
681
682    /// The boundary's distance is its nearest piece's, and infinite with
683    /// none.
684    #[test]
685    fn distance_to_the_nearest_piece() {
686        let pieces = [
687            Boundary2::Segment(Point2::new(0.0, 0.0), Point2::new(4.0, 0.0)),
688            circle((2.0, 0.0), 1.0, 0.0, PI),
689        ];
690        let p = Point2::new(2.0, 0.5);
691        assert!((distance_to_boundary(&pieces, p) - 0.5).abs() < 1e-15);
692        assert!(distance_to_boundary(&[], p).is_infinite());
693    }
694}