1use std::f64::consts::TAU;
7
8use crate::vec::{Point2, Vec2};
9
10#[derive(Debug, Clone, Copy)]
12pub enum Boundary2 {
13 Segment(Point2, Point2),
15 Arc {
19 center: Point2,
21 u: Vec2,
23 v: Vec2,
25 a: f64,
27 b: f64,
29 t0: f64,
31 t1: f64,
33 },
34}
35
36const DIRECTIONS: [f64; 6] = [0.613, 1.931, 2.871, 4.127, 5.369, 0.229];
38
39#[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#[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#[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#[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
122const ELLIPSE_DEPTH: u32 = 60;
124
125fn ellipse_arc_distance(x0: f64, y0: f64, a: f64, b: f64, t0: f64, t1: f64) -> f64 {
133 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
176enum Crossing {
178 OnBoundary,
180 Grazes,
183 Count(u32),
185}
186
187fn 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 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 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 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 #[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 #[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 #[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 #[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 #[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 #[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(®ion, Point2::new(0.0, 0.0), 1e-9),
422 Some(false)
423 );
424 assert_eq!(
425 point_in_region(®ion, Point2::new(0.0, 1.0005), 1e-9),
426 Some(true)
427 );
428 assert_eq!(
429 point_in_region(®ion, Point2::new(0.0, 0.9995), 1e-9),
430 Some(false)
431 );
432 assert_eq!(
433 point_in_region(®ion, Point2::new(2.9, -2.9), 1e-9),
434 Some(true)
435 );
436 assert_eq!(
437 point_in_region(®ion, Point2::new(3.1, 0.0), 1e-9),
438 Some(false)
439 );
440 }
441
442 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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}