1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
37use ogeom_geom::{Curve, Curve2d, Curve3d, PlanarCurve};
38use ogeom_math::{Point, Point2, solve};
39
40#[derive(Debug, Clone, Copy, PartialEq)]
42pub struct Crossing<P> {
43 pub on_a: f64,
45 pub on_b: f64,
47 pub point: P,
49 pub gap: f64,
55 pub reach: f64,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq)]
65pub struct Overlap {
66 pub on_a: (f64, f64),
68 pub on_b: (f64, f64),
70}
71
72#[derive(Debug, Clone, PartialEq)]
74pub struct CurveIntersection<P> {
75 pub crossings: Vec<Crossing<P>>,
77 pub overlaps: Vec<Overlap>,
89}
90
91impl<P> CurveIntersection<P> {
92 #[must_use]
94 pub fn is_empty(&self) -> bool {
95 self.crossings.is_empty() && self.overlaps.is_empty()
96 }
97
98 const fn empty() -> Self {
99 Self {
100 crossings: Vec::new(),
101 overlaps: Vec::new(),
102 }
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq)]
108pub struct CurveCurveOptions {
109 pub samples: usize,
113 pub gap: f64,
118}
119
120impl Default for CurveCurveOptions {
121 fn default() -> Self {
122 Self {
123 samples: 128,
124 gap: 1e-7,
125 }
126 }
127}
128
129pub fn intersect_curves_2d(
139 a: &PlanarCurve,
140 b: &PlanarCurve,
141 options: CurveCurveOptions,
142 tol: Tolerances,
143) -> OgeomResult<CurveIntersection<Point2>> {
144 check(options)?;
145 let (basis_a, window_a) = through_trim_2d(a);
148 let (basis_b, window_b) = through_trim_2d(b);
149 let found = match (basis_a, basis_b) {
150 (PlanarCurve::Line(x), PlanarCurve::Line(y)) => line_line_2d(x, y, tol),
151 (PlanarCurve::Line(x), PlanarCurve::Circle(y)) => line_circle_2d(x, y, false, tol),
152 (PlanarCurve::Circle(x), PlanarCurve::Line(y)) => line_circle_2d(y, x, true, tol),
153 (PlanarCurve::Circle(x), PlanarCurve::Circle(y)) => circle_circle_2d(x, y, tol),
154 _ => return general_2d(a, b, options, tol),
155 };
156 let side = |curve: &PlanarCurve, window: Option<(f64, f64)>| Side {
157 period: {
158 let (lo, hi) = curve.domain();
159 (curve.is_periodic() && hi > lo).then_some(hi - lo)
160 },
161 domain: curve.domain(),
162 window,
163 };
164 Ok(clipped_to_windows(
165 found,
166 side(basis_a, window_a),
167 side(basis_b, window_b),
168 tol,
169 ))
170}
171
172fn through_trim_2d(curve: &PlanarCurve) -> (&PlanarCurve, Option<(f64, f64)>) {
175 match curve {
176 PlanarCurve::Trimmed(t) if !t.is_reversed() => (t.basis(), Some(t.domain())),
177 other => (other, None),
178 }
179}
180
181#[derive(Debug, Clone, Copy)]
184struct Side {
185 period: Option<f64>,
186 domain: (f64, f64),
187 window: Option<(f64, f64)>,
188}
189
190impl Side {
191 fn of(curve: &Curve, window: Option<(f64, f64)>) -> Self {
192 let (lo, hi) = curve.domain();
193 Self {
194 period: (curve.is_periodic() && hi > lo).then_some(hi - lo),
195 domain: (lo, hi),
196 window,
197 }
198 }
199}
200
201pub fn intersect_curves(
207 a: &Curve,
208 b: &Curve,
209 options: CurveCurveOptions,
210 tol: Tolerances,
211) -> OgeomResult<CurveIntersection<Point>> {
212 check(options)?;
213 let (basis_a, window_a) = through_trim(a);
214 let (basis_b, window_b) = through_trim(b);
215 if let Some(found) = same_curve_3d(basis_a, basis_b) {
216 return Ok(clipped_to_windows(
217 found,
218 Side::of(basis_a, window_a),
219 Side::of(basis_b, window_b),
220 tol,
221 ));
222 }
223 if let Some(found) = analytic_3d(basis_a, basis_b, options, tol) {
224 return Ok(clipped_to_windows(
225 found,
226 Side::of(basis_a, window_a),
227 Side::of(basis_b, window_b),
228 tol,
229 ));
230 }
231 general_3d(a, b, options, tol)
232}
233
234fn through_trim(curve: &Curve) -> (&Curve, Option<(f64, f64)>) {
242 match curve {
243 Curve::Trimmed(t) if !t.is_reversed() => (t.basis(), Some(Curve3d::domain(&**t))),
244 other => (other, None),
245 }
246}
247
248fn analytic_3d(
251 a: &Curve,
252 b: &Curve,
253 options: CurveCurveOptions,
254 tol: Tolerances,
255) -> Option<CurveIntersection<Point>> {
256 match (a, b) {
257 (Curve::Line(x), Curve::Line(y)) => Some(line_line_3d(x, y, options, tol)),
258 (Curve::Circle(x), Curve::Circle(y)) => same_circle_3d(x, y, tol)
259 .or_else(|| coplanar_circles_3d(x, y, options, tol))
260 .or_else(|| skew_conics_3d(a, b, options, tol)),
261 (Curve::Ellipse(x), Curve::Ellipse(y)) => {
262 same_ellipse_3d(x, y, tol).or_else(|| skew_conics_3d(a, b, options, tol))
263 }
264 (Curve::Circle(_), Curve::Ellipse(_)) | (Curve::Ellipse(_), Curve::Circle(_)) => {
265 skew_conics_3d(a, b, options, tol)
266 }
267 (Curve::Line(x), Curve::Circle(_) | Curve::Ellipse(_)) => line_conic_3d(x, b, options, tol)
268 .map(|mut found| {
269 for c in &mut found.crossings {
270 core::mem::swap(&mut c.on_a, &mut c.on_b);
271 }
272 found.crossings.sort_by(|x, y| x.on_a.total_cmp(&y.on_a));
273 found
274 }),
275 (Curve::Circle(_) | Curve::Ellipse(_), Curve::Line(y)) => line_conic_3d(y, a, options, tol),
276 _ => None,
277 }
278}
279
280fn conic_of(
284 curve: &Curve,
285) -> Option<(
286 Point,
287 ogeom_math::Vector,
288 ogeom_math::Vector,
289 ogeom_math::Vector,
290)> {
291 match curve {
292 Curve::Circle(c) if !c.is_reversed() => {
293 let circle = c.circle();
294 let f = circle.frame();
295 Some((
296 f.origin(),
297 f.x().vector() * circle.radius(),
298 f.y().vector() * circle.radius(),
299 f.z().vector(),
300 ))
301 }
302 Curve::Ellipse(e) if !e.is_reversed() => {
303 let ellipse = e.ellipse();
304 let f = ellipse.frame();
305 Some((
306 f.origin(),
307 f.x().vector() * ellipse.major_radius(),
308 f.y().vector() * ellipse.minor_radius(),
309 f.z().vector(),
310 ))
311 }
312 _ => None,
313 }
314}
315
316fn skew_conics_3d(
322 a: &Curve,
323 b: &Curve,
324 options: CurveCurveOptions,
325 tol: Tolerances,
326) -> Option<CurveIntersection<Point>> {
327 let (ca, ua, va, na) = conic_of(a)?;
328 let (cb, _, _, nb) = conic_of(b)?;
329 if na.cross(nb).magnitude() <= tol.angular() {
330 return ((ca - cb).dot(nb).abs() > options.gap.max(tol.confusion()))
331 .then(CurveIntersection::empty);
332 }
333 let (alpha, beta, gamma) = (nb.dot(ua), nb.dot(va), nb.dot(ca - cb));
334 let size = alpha.hypot(beta);
335 if options.gap.max(tol.confusion()) > size * 1e-3 {
342 return None;
343 }
344 let mut crossings: Vec<Crossing<Point>> = Vec::new();
345 if size > 0.0 && gamma.abs() <= size * (1.0 + 1e-12) {
346 let phase = beta.atan2(alpha);
347 let turn = (-gamma / size).clamp(-1.0, 1.0).acos();
348 let (a_lo, a_hi) = a.domain();
349 let (b_lo, b_hi) = b.domain();
350 let tau = core::f64::consts::TAU;
351 let into = |t: f64, lo: f64| lo + (t - lo).rem_euclid(tau);
352 let roots = if turn <= 1e-12 {
353 vec![phase]
354 } else {
355 vec![phase - turn, phase + turn]
356 };
357 for root in roots {
358 let t = into(root, a_lo);
359 if t > a_hi + tol.parametric() {
360 continue;
361 }
362 let point = a.point_at(t, tol).ok()?;
363 if (point - cb).dot(nb).abs() > tol.confusion() * 10.0 {
367 return None;
368 }
369 let s = match b {
370 Curve::Circle(c) => {
371 ogeom_math::elementary::circle_parameter(&c.circle(), point, tol)
372 }
373 Curve::Ellipse(e) => {
374 ogeom_math::elementary::ellipse_parameter(&e.ellipse(), point, tol)
375 }
376 _ => return None,
377 };
378 let Ok(s) = s else {
379 continue;
380 };
381 let s = into(s, b_lo);
382 if s > b_hi + tol.parametric() {
383 continue;
384 }
385 let gap = b.point_at(s, tol).ok()?.distance(point);
386 if gap > options.gap {
387 continue;
388 }
389 crossings.push(Crossing {
390 on_a: t,
391 on_b: s,
392 point,
393 gap,
394 reach: 0.0,
395 });
396 }
397 }
398 crossings.sort_by(|x, y| x.on_a.total_cmp(&y.on_a));
399 Some(CurveIntersection {
400 crossings,
401 overlaps: Vec::new(),
402 })
403}
404
405fn line_conic_3d(
414 line: &ogeom_geom::LineCurve,
415 conic: &Curve,
416 options: CurveCurveOptions,
417 tol: Tolerances,
418) -> Option<CurveIntersection<Point>> {
419 let (centre, u, v, normal) = conic_of(conic)?;
420 let (origin, along) = (line.axis().location, line.axis().direction.vector());
421 let (a_len, b_len) = (u.magnitude(), v.magnitude());
422 if a_len <= tol.confusion() || b_len <= tol.confusion() {
423 return None;
424 }
425 let (ux, vy) = (u / a_len, v / b_len);
426 let lean = along.dot(normal);
427 let height = (origin - centre).dot(normal);
428 let mut ts: Vec<f64> = Vec::new();
429 if lean.abs() <= tol.angular() {
430 if height.abs() > options.gap.max(tol.confusion()) {
431 return Some(CurveIntersection::empty());
432 }
433 let (x0, y0) = ((origin - centre).dot(ux), (origin - centre).dot(vy));
435 let (dx, dy) = (along.dot(ux), along.dot(vy));
436 let qa = dx * dx / (a_len * a_len) + dy * dy / (b_len * b_len);
437 let qb = 2.0 * (x0 * dx / (a_len * a_len) + y0 * dy / (b_len * b_len));
438 let qc = x0 * x0 / (a_len * a_len) + y0 * y0 / (b_len * b_len) - 1.0;
439 let disc = qb.mul_add(qb, -4.0 * qa * qc);
440 if qa <= 0.0 {
441 return None;
442 }
443 if disc < 0.0 {
444 let t = -qb / (2.0 * qa);
445 let p = origin + along * t;
446 let foot = conic_parameter(conic, p, tol)?;
447 let gap = conic.point_at(foot, tol).ok()?.distance(p);
448 return (gap > options.gap).then(CurveIntersection::empty);
449 }
450 let root = disc.sqrt();
451 ts.push((-qb - root) / (2.0 * qa));
452 ts.push((-qb + root) / (2.0 * qa));
453 } else if lean.abs() >= 0.1 {
454 ts.push(-height / lean);
455 } else {
456 return None;
457 }
458 let (lo, hi) = line.domain();
459 let (c_lo, c_hi) = conic.domain();
460 let tau = core::f64::consts::TAU;
461 let mut crossings: Vec<Crossing<Point>> = Vec::new();
462 for t in ts {
463 if t < lo - tol.parametric() || t > hi + tol.parametric() {
464 continue;
465 }
466 let point = origin + along * t;
467 let s = conic_parameter(conic, point, tol)?;
468 let s = c_lo + (s - c_lo).rem_euclid(tau);
469 if s > c_hi + tol.parametric() {
470 continue;
471 }
472 let on_conic = conic.point_at(s, tol).ok()?;
473 let gap = on_conic.distance(point);
474 if gap > options.gap {
475 continue;
476 }
477 let tangent = conic.d1_at(s, tol).ok()?;
478 if tangent.cross(along).magnitude() <= 1e-3 * tangent.magnitude() {
479 return None;
480 }
481 crossings.push(Crossing {
482 on_a: s,
483 on_b: t,
484 point: on_conic,
485 gap,
486 reach: 0.0,
487 });
488 }
489 crossings.sort_by(|x, y| x.on_a.total_cmp(&y.on_a));
490 crossings.dedup_by(|x, y| (x.on_a - y.on_a).abs() <= tol.parametric());
491 Some(CurveIntersection {
492 crossings,
493 overlaps: Vec::new(),
494 })
495}
496
497fn conic_parameter(conic: &Curve, point: Point, tol: Tolerances) -> Option<f64> {
499 match conic {
500 Curve::Circle(c) => ogeom_math::elementary::circle_parameter(&c.circle(), point, tol).ok(),
501 Curve::Ellipse(e) => {
502 ogeom_math::elementary::ellipse_parameter(&e.ellipse(), point, tol).ok()
503 }
504 _ => None,
505 }
506}
507
508fn clipped_to_windows<P>(
517 found: CurveIntersection<P>,
518 a: Side,
519 b: Side,
520 tol: Tolerances,
521) -> CurveIntersection<P> {
522 let (window_a, window_b) = (a.window, b.window);
523 if window_a.is_none() && window_b.is_none() {
524 return found;
525 }
526 let (pa, pb) = (a.period, b.period);
527 let slack = tol.parametric();
528 let placed = |t: f64, window: Option<(f64, f64)>, period: Option<f64>| -> Option<f64> {
529 let Some((lo, hi)) = window else {
530 return Some(t);
531 };
532 for k in [0.0, 1.0, -1.0, 2.0, -2.0] {
533 let shifted = period.map_or(t, |p| p.mul_add(k, t));
534 if shifted >= lo - slack && shifted <= hi + slack {
535 return Some(shifted);
536 }
537 if period.is_none() {
538 break;
539 }
540 }
541 None
542 };
543
544 let mut crossings = Vec::with_capacity(found.crossings.len());
545 for crossing in found.crossings {
546 let (Some(on_a), Some(on_b)) = (
547 placed(crossing.on_a, window_a, pa),
548 placed(crossing.on_b, window_b, pb),
549 ) else {
550 continue;
551 };
552 crossings.push(Crossing {
553 on_a,
554 on_b,
555 ..crossing
556 });
557 }
558
559 let mut overlaps: Vec<Overlap> = Vec::with_capacity(found.overlaps.len());
560 let ordered = |r: (f64, f64)| if r.0 <= r.1 { r } else { (r.1, r.0) };
561 let meet = |x: (f64, f64), y: (f64, f64)| -> Option<(f64, f64)> {
562 let both = (x.0.max(y.0), x.1.min(y.1));
563 (both.1 - both.0 > slack).then_some(both)
564 };
565 let (domain_a, domain_b) = (a.domain, b.domain);
566 let shifts = |period: Option<f64>| -> Vec<f64> {
567 period.map_or_else(
568 || vec![0.0],
569 |p| [0.0, 1.0, -1.0, 2.0, -2.0].iter().map(|k| k * p).collect(),
570 )
571 };
572 for overlap in found.overlaps {
573 let span_a = overlap.on_a.1 - overlap.on_a.0;
574 let span_b = overlap.on_b.1 - overlap.on_b.0;
575 if span_a.abs() <= f64::MIN_POSITIVE || span_b.abs() <= f64::MIN_POSITIVE {
576 continue;
577 }
578 let rate = span_b / span_a;
579 let to_b = |t: f64| overlap.on_b.0 + rate * (t - overlap.on_a.0);
580 let to_a = |t: f64| overlap.on_a.0 + (t - overlap.on_b.0) / rate;
581 let whole = pa.is_some_and(|p| span_a.abs() >= p - slack)
586 && pb.is_some_and(|p| span_b.abs() >= p - slack);
587 let window_a = window_a.map_or(domain_a, ordered);
588 let window_b = window_b.map_or(domain_b, ordered);
589 let mut pieces_a: Vec<(f64, f64, f64)> = Vec::new();
590 if whole {
591 pieces_a.push((window_a.0, window_a.1, 0.0));
592 } else {
593 for shift in shifts(pa) {
594 let stretch = ordered(overlap.on_a);
595 if let Some(piece) = meet((stretch.0 + shift, stretch.1 + shift), window_a) {
596 pieces_a.push((piece.0, piece.1, shift));
597 }
598 }
599 }
600 for (lo, hi, shift_a) in pieces_a {
601 let image = ordered((to_b(lo - shift_a), to_b(hi - shift_a)));
603 for shift_b in shifts(pb) {
604 let Some(on_b) = meet((image.0 + shift_b, image.1 + shift_b), window_b) else {
605 continue;
606 };
607 let back = |t: f64| to_a(t - shift_b) + shift_a;
608 let on_a = ordered((back(on_b.0), back(on_b.1)));
609 if on_a.1 - on_a.0 <= slack {
610 continue;
611 }
612 let forward = |t: f64| to_b(t - shift_a) + shift_b;
614 let (on_a, on_b) = if span_a >= 0.0 {
615 (on_a, (forward(on_a.0), forward(on_a.1)))
616 } else {
617 ((on_a.1, on_a.0), (forward(on_a.1), forward(on_a.0)))
618 };
619 let repeated = overlaps.iter().any(|o| {
620 (o.on_a.0 - on_a.0).abs() <= slack && (o.on_a.1 - on_a.1).abs() <= slack
621 });
622 if !repeated {
623 overlaps.push(Overlap { on_a, on_b });
624 }
625 }
626 }
627 }
628 CurveIntersection {
629 crossings,
630 overlaps,
631 }
632}
633
634fn coplanar_circles_3d(
642 a: &ogeom_geom::CircleCurve,
643 b: &ogeom_geom::CircleCurve,
644 options: CurveCurveOptions,
645 tol: Tolerances,
646) -> Option<CurveIntersection<Point>> {
647 let (ca, cb) = (a.circle(), b.circle());
648 let normal = ca.frame().z().vector();
649 if normal.cross(cb.frame().z().vector()).magnitude() > tol.angular() {
650 return None;
651 }
652 let between = cb.centre() - ca.centre();
653 if between.dot(normal).abs() > tol.confusion() {
654 return None;
655 }
656 let in_plane = between - normal * between.dot(normal);
657 let distance = in_plane.magnitude();
658 let (ra, rb) = (ca.radius(), cb.radius());
659 if distance <= tol.confusion() {
660 return None;
661 }
662 if distance > ra + rb + options.gap || distance < (ra - rb).abs() - options.gap {
663 return Some(CurveIntersection::empty());
664 }
665 let weld = tol.confusion() * 50.0;
669 if (distance - (ra + rb)).abs() <= weld || (distance - (ra - rb).abs()).abs() <= weld {
670 return None;
671 }
672 let along = distance.mul_add(distance, ra.mul_add(ra, -(rb * rb))) / (2.0 * distance);
673 let squared = ra.mul_add(ra, -(along * along));
674 if squared <= tol.confusion() * tol.confusion() {
675 return None;
676 }
677 let half = squared.sqrt();
678 let ux = in_plane / distance;
679 let uy = normal.cross(ux);
680 let parameter = |curve: &ogeom_geom::CircleCurve, p: Point| -> f64 {
681 let local = curve.circle().frame().to_local(p);
682 let angle = local.y.atan2(local.x);
683 let angle = if curve.is_reversed() { -angle } else { angle };
684 let (lo, _) = Curve3d::domain(curve);
685 lo + (angle - lo).rem_euclid(core::f64::consts::TAU)
686 };
687 let mut crossings: Vec<Crossing<Point>> = [half, -half]
688 .into_iter()
689 .map(|h| {
690 let point = ca.centre() + ux * along + uy * h;
691 Crossing {
692 on_a: parameter(a, point),
693 on_b: parameter(b, point),
694 point,
695 gap: 0.0,
696 reach: 0.0,
697 }
698 })
699 .collect();
700 sort_crossings(&mut crossings);
701 Some(CurveIntersection {
702 crossings,
703 overlaps: Vec::new(),
704 })
705}
706
707fn same_circle_3d(
713 a: &ogeom_geom::CircleCurve,
714 b: &ogeom_geom::CircleCurve,
715 tol: Tolerances,
716) -> Option<CurveIntersection<Point>> {
717 let (ca, cb) = (a.circle(), b.circle());
718 if ca.centre().distance(cb.centre()) > tol.confusion() {
719 return None;
720 }
721 if (ca.radius() - cb.radius()).abs() > tol.confusion() {
722 return None;
723 }
724 let (za, zb) = (ca.frame().z().vector(), cb.frame().z().vector());
727 if za.cross(zb).magnitude() > tol.angular() {
728 return None;
729 }
730 let (lo, hi) = Curve3d::domain(a);
737 let start = a.point_at(lo, tol).ok()?;
738 let local = cb.frame().to_local(start);
739 let angle = local.y.atan2(local.x);
740 let phase = if b.is_reversed() { -angle } else { angle }.rem_euclid(core::f64::consts::TAU);
741 let along_a = a.d1_at(lo, tol).ok()?;
742 let along_b = b.d1_at(phase, tol).ok()?;
743 let winding: f64 = if along_a.dot(along_b) >= 0.0 {
744 1.0
745 } else {
746 -1.0
747 };
748 Some(CurveIntersection {
749 crossings: Vec::new(),
750 overlaps: vec![Overlap {
751 on_a: (lo, hi),
752 on_b: (phase, winding.mul_add(hi - lo, phase)),
753 }],
754 })
755}
756
757fn same_curve_3d(a: &Curve, b: &Curve) -> Option<CurveIntersection<Point>> {
764 let (lo, hi) = Curve3d::domain(a);
765 if a == b {
766 return Some(CurveIntersection {
767 crossings: Vec::new(),
768 overlaps: vec![Overlap {
769 on_a: (lo, hi),
770 on_b: (lo, hi),
771 }],
772 });
773 }
774 use ogeom_geom::Reversible as _;
775 if *a == b.clone().reversed() {
776 let (blo, bhi) = Curve3d::domain(b);
777 return Some(CurveIntersection {
778 crossings: Vec::new(),
779 overlaps: vec![Overlap {
780 on_a: (lo, hi),
781 on_b: (bhi, blo),
782 }],
783 });
784 }
785 None
786}
787
788fn same_ellipse_3d(
795 a: &ogeom_geom::EllipseCurve,
796 b: &ogeom_geom::EllipseCurve,
797 tol: Tolerances,
798) -> Option<CurveIntersection<Point>> {
799 let (ea, eb) = (a.ellipse(), b.ellipse());
800 if ea.frame().origin().distance(eb.frame().origin()) > tol.confusion() {
801 return None;
802 }
803 if (ea.major_radius() - eb.major_radius()).abs() > tol.confusion()
804 || (ea.minor_radius() - eb.minor_radius()).abs() > tol.confusion()
805 {
806 return None;
807 }
808 let (za, zb) = (ea.frame().z().vector(), eb.frame().z().vector());
809 if za.cross(zb).magnitude() > tol.angular() {
810 return None;
811 }
812 let (xa, xb) = (ea.frame().x().vector(), eb.frame().x().vector());
813 if xa.cross(xb).magnitude() > tol.angular() {
814 return None;
815 }
816 let (lo, hi) = Curve3d::domain(a);
820 let start = a.point_at(lo, tol).ok()?;
821 let angle = ogeom_math::elementary::ellipse_parameter(&eb, start, tol).ok()?;
822 let phase = if b.is_reversed() { -angle } else { angle }.rem_euclid(core::f64::consts::TAU);
823 let along_a = a.d1_at(lo, tol).ok()?;
824 let along_b = b.d1_at(phase, tol).ok()?;
825 let winding: f64 = if along_a.dot(along_b) >= 0.0 {
826 1.0
827 } else {
828 -1.0
829 };
830 Some(CurveIntersection {
831 crossings: Vec::new(),
832 overlaps: vec![Overlap {
833 on_a: (lo, hi),
834 on_b: (phase, winding.mul_add(hi - lo, phase)),
835 }],
836 })
837}
838
839fn check(options: CurveCurveOptions) -> OgeomResult<()> {
840 if options.samples < 2 {
841 ogeom_bail!(Construction, "seeding needs at least two segments");
842 }
843 if !options.gap.is_finite() || options.gap <= 0.0 {
844 ogeom_bail!(Construction, "a gap of {} is not a distance", options.gap);
845 }
846 Ok(())
847}
848
849fn line_line_2d(
852 a: &ogeom_geom::Line2d,
853 b: &ogeom_geom::Line2d,
854 tol: Tolerances,
855) -> CurveIntersection<Point2> {
856 let (oa, da) = (a.axis().location, a.axis().direction.vector());
857 let (ob, db) = (b.axis().location, b.axis().direction.vector());
858 let cross = da.cross(db);
859
860 if cross.abs() <= tol.angular() {
861 let between = ob - oa;
863 if between.cross(da).abs() > tol.confusion() {
864 return CurveIntersection::empty();
865 }
866 let (a_lo, a_hi) = a.domain();
868 let (b_lo, b_hi) = b.domain();
869 let project = |p: Point2| (p - oa).dot(da);
871 let (s0, s1) = (project(ob + db * b_lo), project(ob + db * b_hi));
872 let (lo, hi) = (s0.min(s1).max(a_lo), s0.max(s1).min(a_hi));
873 let back = |t: f64| (oa + da * t - ob).dot(db);
875 if hi - lo <= tol.confusion() {
876 if lo - hi > tol.confusion() {
878 return CurveIntersection::empty();
879 }
880 let t = f64::midpoint(lo, hi).clamp(a_lo, a_hi);
881 return CurveIntersection {
882 crossings: vec![Crossing {
883 on_a: t,
884 on_b: back(t).clamp(b_lo, b_hi),
885 point: oa + da * t,
886 gap: 0.0,
887 reach: 0.0,
888 }],
889 overlaps: Vec::new(),
890 };
891 }
892 return CurveIntersection {
893 crossings: Vec::new(),
894 overlaps: vec![Overlap {
895 on_a: (lo, hi),
896 on_b: (back(lo), back(hi)),
902 }],
903 };
904 }
905
906 let between = ob - oa;
907 let t = between.cross(db) / cross;
908 let s = between.cross(da) / cross;
909 let (a_lo, a_hi) = a.domain();
910 let (b_lo, b_hi) = b.domain();
911 if t < a_lo - tol.parametric()
912 || t > a_hi + tol.parametric()
913 || s < b_lo - tol.parametric()
914 || s > b_hi + tol.parametric()
915 {
916 return CurveIntersection::empty();
917 }
918 CurveIntersection {
919 crossings: vec![Crossing {
920 on_a: t,
921 on_b: s,
922 point: oa + da * t,
923 gap: 0.0,
924 reach: 0.0,
925 }],
926 overlaps: Vec::new(),
927 }
928}
929
930fn line_circle_2d(
931 line: &ogeom_geom::Line2d,
932 circle: &ogeom_geom::Circle2d,
933 swapped: bool,
934 tol: Tolerances,
935) -> CurveIntersection<Point2> {
936 let (o, d) = (line.axis().location, line.axis().direction.vector());
937 let c = circle.circle();
938 let centre = c.centre();
939 let radius = c.radius();
940
941 let along = (centre - o).dot(d);
943 let foot = o + d * along;
944 let gap = foot.distance(centre);
945 if gap > radius + tol.confusion() {
946 return CurveIntersection::empty();
947 }
948 let half = radius.mul_add(radius, -(gap * gap)).max(0.0).sqrt();
949 let candidates = if half <= tol.confusion() {
950 vec![along]
951 } else {
952 vec![along - half, along + half]
953 };
954
955 let (l_lo, l_hi) = line.domain();
956 let mut crossings = Vec::new();
957 for t in candidates {
958 if t < l_lo - tol.parametric() || t > l_hi + tol.parametric() {
959 continue;
960 }
961 let p = o + d * t;
962 let Some(s) = circle_parameter(circle, p, tol) else {
963 continue;
964 };
965 let (on_a, on_b) = if swapped { (s, t) } else { (t, s) };
966 crossings.push(Crossing {
967 on_a,
968 on_b,
969 point: p,
970 gap: 0.0,
971 reach: 0.0,
972 });
973 }
974 sort_crossings(&mut crossings);
975 CurveIntersection {
976 crossings,
977 overlaps: Vec::new(),
978 }
979}
980
981fn circle_circle_2d(
982 a: &ogeom_geom::Circle2d,
983 b: &ogeom_geom::Circle2d,
984 tol: Tolerances,
985) -> CurveIntersection<Point2> {
986 let (ca, cb) = (a.circle(), b.circle());
987 let between = cb.centre() - ca.centre();
988 let distance = between.magnitude();
989 let (ra, rb) = (ca.radius(), cb.radius());
990
991 if distance <= tol.confusion() {
992 if (ra - rb).abs() <= tol.confusion() {
993 use ogeom_geom::Curve2d as _;
998 let (lo, hi) = a.domain();
999 let correspondence = (|| {
1000 let start = a.point_at(lo, tol).ok()?;
1001 let phase = circle_parameter(b, start, tol)?;
1002 let along_a = a.d1_at(lo, tol).ok()?;
1003 let along_b = b.d1_at(phase, tol).ok()?;
1004 let winding: f64 = if along_a.dot(along_b) >= 0.0 {
1005 1.0
1006 } else {
1007 -1.0
1008 };
1009 Some((phase, winding.mul_add(hi - lo, phase)))
1010 })();
1011 return CurveIntersection {
1012 crossings: Vec::new(),
1013 overlaps: vec![Overlap {
1014 on_a: (lo, hi),
1015 on_b: correspondence.unwrap_or_else(|| b.domain()),
1016 }],
1017 };
1018 }
1019 return CurveIntersection::empty();
1020 }
1021 if distance > ra + rb + tol.confusion() || distance < (ra - rb).abs() - tol.confusion() {
1022 return CurveIntersection::empty();
1023 }
1024
1025 let along = distance.mul_add(distance, ra.mul_add(ra, -(rb * rb))) / (2.0 * distance);
1027 let squared = ra.mul_add(ra, -(along * along));
1028 let direction = between * (1.0 / distance);
1029 let foot = ca.centre() + direction * along;
1030 let mut crossings = Vec::new();
1031 let mut push = |p: Point2| {
1032 if let (Some(s), Some(t)) = (circle_parameter(a, p, tol), circle_parameter(b, p, tol)) {
1033 crossings.push(Crossing {
1034 on_a: s,
1035 on_b: t,
1036 point: p,
1037 gap: 0.0,
1038 reach: 0.0,
1039 });
1040 }
1041 };
1042 if squared <= tol.confusion() * tol.confusion() {
1043 push(foot);
1044 } else {
1045 let offset = ogeom_math::Vector2::new(-direction.y, direction.x) * squared.max(0.0).sqrt();
1046 push(foot + offset);
1047 push(foot - offset);
1048 }
1049 sort_crossings(&mut crossings);
1050 CurveIntersection {
1051 crossings,
1052 overlaps: Vec::new(),
1053 }
1054}
1055
1056fn circle_parameter(curve: &ogeom_geom::Circle2d, p: Point2, tol: Tolerances) -> Option<f64> {
1058 let c = curve.circle();
1059 let local = p - c.centre();
1060 let x = local.dot(c.frame().x().vector());
1061 let y = local.dot(c.frame().y().vector());
1062 let mut angle = y.atan2(x);
1063 if curve.is_reversed() {
1064 angle = -angle;
1065 }
1066 let angle = angle.rem_euclid(core::f64::consts::TAU);
1067 let (lo, hi) = curve.domain();
1068 if angle >= lo - tol.parametric() && angle <= hi + tol.parametric() {
1070 return Some(angle.clamp(lo, hi));
1071 }
1072 let shifted = angle - core::f64::consts::TAU;
1073 if shifted >= lo - tol.parametric() && shifted <= hi + tol.parametric() {
1074 return Some(shifted.clamp(lo, hi));
1075 }
1076 None
1077}
1078
1079fn line_line_3d(
1082 a: &ogeom_geom::LineCurve,
1083 b: &ogeom_geom::LineCurve,
1084 options: CurveCurveOptions,
1085 tol: Tolerances,
1086) -> CurveIntersection<Point> {
1087 let (oa, da) = (a.axis().location, a.axis().direction.vector());
1088 let (ob, db) = (b.axis().location, b.axis().direction.vector());
1089 let cross = da.cross(db);
1090 let denominator = cross.square_magnitude();
1091
1092 if denominator <= tol.angular() * tol.angular() {
1093 let between = ob - oa;
1095 if between.cross(da).magnitude() > tol.confusion() {
1096 return CurveIntersection::empty();
1097 }
1098 let (a_lo, a_hi) = a.domain();
1099 let (b_lo, b_hi) = b.domain();
1100 let project = |p: Point| (p - oa).dot(da);
1101 let (s0, s1) = (project(ob + db * b_lo), project(ob + db * b_hi));
1102 let (lo, hi) = (s0.min(s1).max(a_lo), s0.max(s1).min(a_hi));
1103 let back = |t: f64| (oa + da * t - ob).dot(db);
1104 if hi - lo <= tol.confusion() {
1105 if lo - hi > tol.confusion() {
1107 return CurveIntersection::empty();
1108 }
1109 let t = f64::midpoint(lo, hi).clamp(a_lo, a_hi);
1110 let s = back(t).clamp(b_lo, b_hi);
1111 let point = oa + da * t;
1112 return CurveIntersection {
1113 crossings: vec![Crossing {
1114 on_a: t,
1115 on_b: s,
1116 point,
1117 gap: point.distance(ob + db * s),
1118 reach: 0.0,
1119 }],
1120 overlaps: Vec::new(),
1121 };
1122 }
1123 return CurveIntersection {
1124 crossings: Vec::new(),
1125 overlaps: vec![Overlap {
1126 on_a: (lo, hi),
1127 on_b: (back(lo), back(hi)),
1133 }],
1134 };
1135 }
1136
1137 let between = ob - oa;
1139 let t = between.cross(db).dot(cross) / denominator;
1140 let s = between.cross(da).dot(cross) / denominator;
1141 let pa = oa + da * t;
1142 let pb = ob + db * s;
1143 let gap = pa.distance(pb);
1144 let (a_lo, a_hi) = a.domain();
1145 let (b_lo, b_hi) = b.domain();
1146 if gap > options.gap
1147 || t < a_lo - tol.parametric()
1148 || t > a_hi + tol.parametric()
1149 || s < b_lo - tol.parametric()
1150 || s > b_hi + tol.parametric()
1151 {
1152 return CurveIntersection::empty();
1153 }
1154 CurveIntersection {
1155 crossings: vec![Crossing {
1156 on_a: t,
1157 on_b: s,
1158 point: pa,
1159 gap,
1160 reach: 0.0,
1161 }],
1162 overlaps: Vec::new(),
1163 }
1164}
1165
1166struct Sampled<P> {
1170 points: Vec<P>,
1171 parameters: Vec<f64>,
1172}
1173
1174fn sample_2d(curve: &PlanarCurve, n: usize, tol: Tolerances) -> Sampled<Point2> {
1175 let (lo, hi) = curve.domain();
1176 let mut points = Vec::with_capacity(n + 1);
1177 let mut parameters = Vec::with_capacity(n + 1);
1178 for i in 0..=n {
1179 #[allow(clippy::cast_precision_loss)]
1180 let t = lo + (hi - lo) * i as f64 / n as f64;
1181 if let Ok(p) = curve.point_at(t, tol) {
1182 points.push(p);
1183 parameters.push(t);
1184 }
1185 }
1186 Sampled { points, parameters }
1187}
1188
1189fn sample_3d(curve: &Curve, n: usize, tol: Tolerances) -> Sampled<Point> {
1190 let (lo, hi) = curve.domain();
1191 let mut points = Vec::with_capacity(n + 1);
1192 let mut parameters = Vec::with_capacity(n + 1);
1193 for i in 0..=n {
1194 #[allow(clippy::cast_precision_loss)]
1195 let t = lo + (hi - lo) * i as f64 / n as f64;
1196 if let Ok(p) = curve.point_at(t, tol) {
1197 points.push(p);
1198 parameters.push(t);
1199 }
1200 }
1201 Sampled { points, parameters }
1202}
1203
1204fn general_2d(
1205 a: &PlanarCurve,
1206 b: &PlanarCurve,
1207 options: CurveCurveOptions,
1208 tol: Tolerances,
1209) -> OgeomResult<CurveIntersection<Point2>> {
1210 let sa = sample_2d(a, options.samples, tol);
1211 let sb = sample_2d(b, options.samples, tol);
1212
1213 let mut crossings: Vec<Crossing<Point2>> = Vec::new();
1214 for i in 1..sa.points.len() {
1215 for j in 1..sb.points.len() {
1216 let Some((ta, tb)) = segments_cross_2d(
1217 (sa.points[i - 1], sa.points[i]),
1218 (sb.points[j - 1], sb.points[j]),
1219 ) else {
1220 continue;
1221 };
1222 let seed_a = sa.parameters[i - 1] + (sa.parameters[i] - sa.parameters[i - 1]) * ta;
1223 let seed_b = sb.parameters[j - 1] + (sb.parameters[j] - sb.parameters[j - 1]) * tb;
1224 if let Some(found) = polish_2d(a, b, seed_a, seed_b, options, tol) {
1225 push_unique_2d(&mut crossings, found, tol);
1226 }
1227 }
1228 }
1229 sort_crossings(&mut crossings);
1230 Ok(CurveIntersection {
1231 crossings,
1232 overlaps: Vec::new(),
1233 })
1234}
1235
1236fn general_3d(
1237 a: &Curve,
1238 b: &Curve,
1239 options: CurveCurveOptions,
1240 tol: Tolerances,
1241) -> OgeomResult<CurveIntersection<Point>> {
1242 let sa = sample_3d(a, options.samples, tol);
1243 let sb = sample_3d(b, options.samples, tol);
1244
1245 let mut reach = options.gap;
1249 for s in [&sa, &sb] {
1250 let longest = s
1251 .points
1252 .windows(2)
1253 .map(|w| w[0].distance(w[1]))
1254 .fold(0.0_f64, f64::max);
1255 reach += longest;
1256 }
1257
1258 let feet: Vec<Option<(f64, f64)>> = sa
1266 .points
1267 .iter()
1268 .map(|p| foot_via_samples(b, &sb, *p, tol))
1269 .collect();
1270 let overlaps = if hugging_runs(&feet, options.gap).contains(&true) {
1271 shared_support_3d(a, b, &sa, &sb, &feet, options, tol)
1272 } else {
1273 Vec::new()
1274 };
1275 let shared = |lo: f64, hi: f64| {
1276 overlaps.iter().any(|o| {
1277 let (from, to) = order(o.on_a.0, o.on_a.1);
1278 lo >= from && hi <= to
1279 })
1280 };
1281
1282 let (na, nb) = (sa.points.len(), sb.points.len());
1289 let boxes = |s: &Sampled<Point>| -> Vec<(Point, Point)> {
1294 s.points
1295 .windows(2)
1296 .map(|w| {
1297 (
1298 Point::new(w[0].x.min(w[1].x), w[0].y.min(w[1].y), w[0].z.min(w[1].z)),
1299 Point::new(w[0].x.max(w[1].x), w[0].y.max(w[1].y), w[0].z.max(w[1].z)),
1300 )
1301 })
1302 .collect()
1303 };
1304 let (boxes_a, boxes_b) = (boxes(&sa), boxes(&sb));
1305 let apart = |x: &(Point, Point), y: &(Point, Point)| {
1306 x.0.x - y.1.x > reach
1307 || y.0.x - x.1.x > reach
1308 || x.0.y - y.1.y > reach
1309 || y.0.y - x.1.y > reach
1310 || x.0.z - y.1.z > reach
1311 || y.0.z - x.1.z > reach
1312 };
1313 let mut approach: Vec<(f64, f64, f64)> =
1314 Vec::with_capacity(na.saturating_sub(1) * nb.saturating_sub(1));
1315 for i in 1..na {
1316 for j in 1..nb {
1317 approach.push(if apart(&boxes_a[i - 1], &boxes_b[j - 1]) {
1318 (0.0, 0.0, f64::INFINITY)
1319 } else {
1320 segments_approach_3d(
1321 (sa.points[i - 1], sa.points[i]),
1322 (sb.points[j - 1], sb.points[j]),
1323 )
1324 });
1325 }
1326 }
1327 let cols = nb.saturating_sub(1);
1328 let gap_at = |i: usize, j: usize| approach[(i - 1) * cols + (j - 1)].2;
1329 let basin = |i: usize, j: usize| {
1332 let here = gap_at(i, j);
1333 let along_a = (i.saturating_sub(1).max(1)..=(i + 1).min(na - 1))
1334 .all(|p| p == i || here <= gap_at(p, j));
1335 let along_b = (j.saturating_sub(1).max(1)..=(j + 1).min(nb - 1))
1336 .all(|q| q == j || here <= gap_at(i, q));
1337 along_a || along_b
1338 };
1339 let mut crossings: Vec<Crossing<Point>> = Vec::new();
1340 for i in 1..na {
1341 let (lo, hi) = order(sa.parameters[i - 1], sa.parameters[i]);
1342 if shared(lo, hi) {
1343 continue;
1344 }
1345 for j in 1..nb {
1346 let (ta, tb, gap) = approach[(i - 1) * cols + (j - 1)];
1347 if gap > reach || !basin(i, j) {
1348 continue;
1349 }
1350 let seed_a = sa.parameters[i - 1] + (sa.parameters[i] - sa.parameters[i - 1]) * ta;
1351 let seed_b = sb.parameters[j - 1] + (sb.parameters[j] - sb.parameters[j - 1]) * tb;
1352 if let Some(found) = polish_3d(a, b, seed_a, seed_b, options, tol) {
1353 push_unique_3d(&mut crossings, found, tol);
1354 }
1355 }
1356 }
1357 sort_crossings(&mut crossings);
1358
1359 if crossings.len() > 1 {
1368 let mut merged: Vec<Crossing<Point>> = Vec::with_capacity(crossings.len());
1369 let mut run_start: Option<Point> = None;
1370 for c in crossings {
1371 if let Some(last) = merged.last_mut()
1372 && contact_between_3d(a, b, last, &c, options, tol)
1373 {
1374 let start = run_start.get_or_insert(last.point);
1375 let reach = start.distance(c.point).max(last.reach);
1376 if c.gap < last.gap {
1377 *last = c;
1378 }
1379 last.reach = reach;
1380 continue;
1381 }
1382 run_start = None;
1383 merged.push(c);
1384 }
1385 if merged.len() > 1 && a.is_periodic() {
1389 let (lo, hi) = a.domain();
1390 let (first, last) = (merged[0], merged[merged.len() - 1]);
1391 let wrapped = Crossing {
1392 on_a: first.on_a + (hi - lo),
1393 ..first
1394 };
1395 if contact_between_3d(a, b, &last, &wrapped, options, tol) {
1396 let reach = last
1397 .reach
1398 .max(first.reach)
1399 .max(last.point.distance(first.point));
1400 let keep = if first.gap <= last.gap {
1401 0
1402 } else {
1403 merged.len() - 1
1404 };
1405 merged[keep].reach = reach;
1406 if keep == 0 {
1407 merged.pop();
1408 } else {
1409 merged.remove(0);
1410 }
1411 }
1412 }
1413 crossings = merged;
1414 }
1415
1416 if !overlaps.is_empty() {
1426 crossings.retain(|c| {
1427 !overlaps.iter().any(|o| {
1428 let (lo, hi) = order(o.on_a.0, o.on_a.1);
1429 c.on_a >= lo - tol.parametric() && c.on_a <= hi + tol.parametric()
1430 })
1431 });
1432 }
1433 let gap = options.gap.max(tol.confusion());
1445 let extent = {
1446 let along = |s: &Sampled<Point>| {
1447 s.points
1448 .windows(2)
1449 .map(|w| w[0].distance(w[1]))
1450 .sum::<f64>()
1451 };
1452 along(&sa).min(along(&sb))
1453 };
1454 let cap = 4.0 * (gap * extent).sqrt();
1455 for c in &mut crossings {
1456 let valley = valley_extent_3d(a, b, &sb, c, gap, tol);
1457 if valley > gap * 8.0 && valley <= cap {
1458 c.reach = c.reach.max(valley);
1459 }
1460 }
1461 Ok(CurveIntersection {
1462 crossings,
1463 overlaps,
1464 })
1465}
1466
1467fn contact_between_3d(
1471 a: &Curve,
1472 b: &Curve,
1473 from: &Crossing<Point>,
1474 to: &Crossing<Point>,
1475 options: CurveCurveOptions,
1476 tol: Tolerances,
1477) -> bool {
1478 if (to.on_a - from.on_a).abs() <= tol.parametric() {
1479 return true;
1480 }
1481 (1..=3).all(|k| {
1482 let f = f64::from(k) / 4.0;
1483 let t = from.on_a + (to.on_a - from.on_a) * f;
1484 let seed = from.on_b + (to.on_b - from.on_b) * f;
1485 a.point_at(t, tol)
1486 .ok()
1487 .and_then(|p| foot_on_3d(b, p, seed, tol))
1488 .is_some_and(|(_, gap)| gap <= options.gap)
1489 })
1490}
1491
1492fn foot_via_samples(
1495 b: &Curve,
1496 sb: &Sampled<Point>,
1497 p: Point,
1498 tol: Tolerances,
1499) -> Option<(f64, f64)> {
1500 let mut seed = (f64::INFINITY, 0.0);
1501 for j in 1..sb.points.len() {
1502 let (_, tb, gap) = segments_approach_3d((p, p), (sb.points[j - 1], sb.points[j]));
1503 if gap < seed.0 {
1504 seed = (
1505 gap,
1506 sb.parameters[j - 1] + (sb.parameters[j] - sb.parameters[j - 1]) * tb,
1507 );
1508 }
1509 }
1510 if !seed.0.is_finite() {
1511 return None;
1512 }
1513 foot_on_3d(b, p, seed.1, tol)
1514}
1515
1516fn hugging_runs(feet: &[Option<(f64, f64)>], gap: f64) -> Vec<bool> {
1520 let within = |i: usize| feet[i].is_some_and(|(_, g)| g <= gap);
1521 let mut out = vec![false; feet.len()];
1522 let mut i = 0;
1523 while i < feet.len() {
1524 if !within(i) {
1525 i += 1;
1526 continue;
1527 }
1528 let start = i;
1529 while i + 1 < feet.len() && within(i + 1) {
1530 i += 1;
1531 }
1532 if i > start {
1533 out[start..=i].fill(true);
1534 }
1535 i += 1;
1536 }
1537 out
1538}
1539
1540fn shared_support_3d(
1543 a: &Curve,
1544 b: &Curve,
1545 sa: &Sampled<Point>,
1546 sb: &Sampled<Point>,
1547 feet: &[Option<(f64, f64)>],
1548 options: CurveCurveOptions,
1549 tol: Tolerances,
1550) -> Vec<Overlap> {
1551 let hugs = |t: f64| -> Option<(f64, f64)> {
1552 let p = a.point_at(t, tol).ok()?;
1553 let (s, gap) = foot_via_samples(b, sb, p, tol)?;
1554 (gap <= options.gap).then_some((s, gap))
1555 };
1556 let within = |i: usize| feet[i].is_some_and(|(_, gap)| gap <= options.gap);
1557
1558 let mut overlaps = Vec::new();
1559 let mut i = 0;
1560 while i < sa.points.len() {
1561 if !within(i) {
1562 i += 1;
1563 continue;
1564 }
1565 let start = i;
1566 while i + 1 < sa.points.len() && within(i + 1) {
1567 i += 1;
1568 }
1569 let end = i;
1570 i += 1;
1571 if end == start {
1572 continue;
1573 }
1574 let refine = |inside: usize, outside: Option<usize>| -> (f64, f64) {
1577 let (mut t_in, s_in) = (sa.parameters[inside], feet[inside].map_or(0.0, |f| f.0));
1578 let Some(out) = outside else {
1579 return (t_in, s_in);
1580 };
1581 let mut s_at = s_in;
1582 let mut t_out = sa.parameters[out];
1583 for _ in 0..48 {
1584 if (t_out - t_in).abs() <= tol.parametric() {
1585 break;
1586 }
1587 let mid = f64::midpoint(t_in, t_out);
1588 match hugs(mid) {
1589 Some((s, _)) => {
1590 t_in = mid;
1591 s_at = s;
1592 }
1593 None => t_out = mid,
1594 }
1595 }
1596 (t_in, s_at)
1597 };
1598 let (lo_a, lo_b) = refine(start, start.checked_sub(1));
1599 let (hi_a, hi_b) = refine(end, (end + 1 < sa.points.len()).then_some(end + 1));
1600 if hi_a - lo_a <= tol.parametric() || (hi_b - lo_b).abs() <= tol.parametric() {
1601 continue;
1602 }
1603 overlaps.push(Overlap {
1604 on_a: (lo_a, hi_a),
1605 on_b: (lo_b, hi_b),
1606 });
1607 }
1608 overlaps
1609}
1610
1611fn foot_on_3d(curve: &Curve, p: Point, seed: f64, tol: Tolerances) -> Option<(f64, f64)> {
1614 let mut s = clamp_3d(curve, seed);
1615 let mut best = (s, curve.point_at(s, tol).ok()?.distance(p));
1616 for _ in 0..30 {
1617 let d = curve.derivatives_at(s, 2, tol).ok()?;
1618 let zero = ogeom_math::Vector::ZERO;
1619 let (c, d1, d2) = (
1620 d.first().copied().unwrap_or(zero),
1621 d.get(1).copied().unwrap_or(zero),
1622 d.get(2).copied().unwrap_or(zero),
1623 );
1624 let gap = c - (p - Point::ORIGIN);
1625 let g = gap.dot(d1);
1626 let dg = d1.dot(d1) + gap.dot(d2);
1627 if dg.abs() <= f64::MIN_POSITIVE {
1628 break;
1629 }
1630 let next = clamp_3d(curve, s - g / dg);
1631 let dist = curve.point_at(next, tol).ok()?.distance(p);
1632 let moved = (next - s).abs();
1633 s = next;
1634 if dist < best.1 {
1635 best = (s, dist);
1636 }
1637 if moved <= tol.parametric() {
1638 break;
1639 }
1640 }
1641 Some(best)
1642}
1643
1644fn polish_2d(
1646 a: &PlanarCurve,
1647 b: &PlanarCurve,
1648 seed_a: f64,
1649 seed_b: f64,
1650 options: CurveCurveOptions,
1651 tol: Tolerances,
1652) -> Option<Crossing<Point2>> {
1653 let system = |x: &[f64; 2]| {
1654 let (t, s) = (clamp_2d(a, x[0]), clamp_2d(b, x[1]));
1655 let pa = a.point_at(t, tol).unwrap_or(Point2::ORIGIN);
1656 let pb = b.point_at(s, tol).unwrap_or(Point2::ORIGIN);
1657 let da = a
1658 .d1_at(t, tol)
1659 .unwrap_or(ogeom_math::Vector2::new(0.0, 0.0));
1660 let db = b
1661 .d1_at(s, tol)
1662 .unwrap_or(ogeom_math::Vector2::new(0.0, 0.0));
1663 ([pa.x - pb.x, pa.y - pb.y], [[da.x, -db.x], [da.y, -db.y]])
1664 };
1665 let criteria = solve::Criteria {
1666 residual: tol.confusion() * 0.01,
1667 step: tol.parametric(),
1668 max_iterations: 40,
1669 };
1670 let found = solve::newton_system_fixed(system, [seed_a, seed_b], criteria).ok()?;
1671 let (t, s) = (clamp_2d(a, found.0[0]), clamp_2d(b, found.0[1]));
1672 let pa = a.point_at(t, tol).ok()?;
1673 let pb = b.point_at(s, tol).ok()?;
1674 let gap = pa.distance(pb);
1675 if gap > options.gap {
1676 return None;
1677 }
1678 Some(Crossing {
1679 on_a: t,
1680 on_b: s,
1681 point: pa,
1682 gap,
1683 reach: 0.0,
1684 })
1685}
1686
1687fn polish_3d(
1694 a: &Curve,
1695 b: &Curve,
1696 seed_a: f64,
1697 seed_b: f64,
1698 options: CurveCurveOptions,
1699 tol: Tolerances,
1700) -> Option<Crossing<Point>> {
1701 let system = |x: [f64; 2]| {
1704 let (t, s) = (clamp_3d(a, x[0]), clamp_3d(b, x[1]));
1705 let (Ok(da), Ok(db)) = (a.derivatives_at(t, 2, tol), b.derivatives_at(s, 2, tol)) else {
1706 return ([f64::INFINITY; 2], [[0.0; 2]; 2]);
1709 };
1710 let zero = ogeom_math::Vector::ZERO;
1711 let at = |d: &[ogeom_math::Vector], k: usize| d.get(k).copied().unwrap_or(zero);
1712 let (pa, d1a, d2a) = (at(&da, 0), at(&da, 1), at(&da, 2));
1713 let (pb, d1b, d2b) = (at(&db, 0), at(&db, 1), at(&db, 2));
1714 let gap = pa - pb;
1715 (
1716 [gap.dot(d1a), -gap.dot(d1b)],
1717 [
1718 [d1a.dot(d1a) + gap.dot(d2a), -d1a.dot(d1b)],
1719 [-d1a.dot(d1b), d1b.dot(d1b) - gap.dot(d2b)],
1720 ],
1721 )
1722 };
1723 let criteria = solve::Criteria {
1724 residual: tol.confusion() * 0.01,
1725 step: tol.parametric(),
1726 max_iterations: 40,
1727 };
1728 let ([t, s], ..) = solve::newton_system_2(system, [seed_a, seed_b], criteria).ok()?;
1729 let (t, s) = (clamp_3d(a, t), clamp_3d(b, s));
1730 let pa = a.point_at(t, tol).ok()?;
1731 let pb = b.point_at(s, tol).ok()?;
1732 let gap = pa.distance(pb);
1733 if gap > options.gap {
1734 return None;
1735 }
1736 Some(Crossing {
1737 on_a: t,
1738 on_b: s,
1739 point: pa,
1740 gap,
1741 reach: 0.0,
1742 })
1743}
1744
1745fn clamp_2d(curve: &PlanarCurve, t: f64) -> f64 {
1748 let (lo, hi) = curve.domain();
1749 if curve.is_periodic() {
1750 let span = hi - lo;
1751 if span > 0.0 {
1752 return lo + (t - lo).rem_euclid(span);
1753 }
1754 }
1755 t.clamp(lo, hi)
1756}
1757
1758fn clamp_3d(curve: &Curve, t: f64) -> f64 {
1759 let (lo, hi) = curve.domain();
1760 if curve.is_periodic() {
1761 let span = hi - lo;
1762 if span > 0.0 {
1763 return lo + (t - lo).rem_euclid(span);
1764 }
1765 }
1766 t.clamp(lo, hi)
1767}
1768
1769fn segments_cross_2d(a: (Point2, Point2), b: (Point2, Point2)) -> Option<(f64, f64)> {
1771 let da = a.1 - a.0;
1772 let db = b.1 - b.0;
1773 let cross = da.cross(db);
1774 if cross.abs() <= f64::MIN_POSITIVE {
1775 return None;
1776 }
1777 let between = b.0 - a.0;
1778 let t = between.cross(db) / cross;
1779 let s = between.cross(da) / cross;
1780 if !(0.0..=1.0).contains(&t) || !(0.0..=1.0).contains(&s) {
1781 return None;
1782 }
1783 Some((t, s))
1784}
1785
1786fn distance_to_curve_3d(b: &Curve, sb: &Sampled<Point>, p: Point, tol: Tolerances) -> f64 {
1789 let mut best = (0_usize, f64::INFINITY);
1790 for i in 1..sb.points.len() {
1791 let (q0, q1) = (sb.points[i - 1], sb.points[i]);
1792 let d = q1 - q0;
1793 let len2 = d.dot(d);
1794 let f = if len2 <= f64::MIN_POSITIVE {
1795 0.0
1796 } else {
1797 ((p - q0).dot(d) / len2).clamp(0.0, 1.0)
1798 };
1799 let dist = p.distance(q0 + d * f);
1800 if dist < best.1 {
1801 best = (i, dist);
1802 }
1803 }
1804 if best.0 == 0 {
1805 return best.1;
1806 }
1807 let (mut lo, mut hi) = (sb.parameters[best.0 - 1], sb.parameters[best.0]);
1808 let at = |t: f64| -> f64 { b.point_at(t, tol).map_or(f64::INFINITY, |q| q.distance(p)) };
1809 let phi = 0.5 * (3.0 - 5.0_f64.sqrt());
1812 let (mut x1, mut x2) = (lo + phi * (hi - lo), hi - phi * (hi - lo));
1813 let (mut f1, mut f2) = (at(x1), at(x2));
1814 for _ in 0..48 {
1815 if f1 < f2 {
1816 hi = x2;
1817 x2 = x1;
1818 f2 = f1;
1819 x1 = lo + phi * (hi - lo);
1820 f1 = at(x1);
1821 } else {
1822 lo = x1;
1823 x1 = x2;
1824 f1 = f2;
1825 x2 = hi - phi * (hi - lo);
1826 f2 = at(x2);
1827 }
1828 }
1829 f1.min(f2).min(best.1)
1830}
1831
1832fn valley_extent_3d(
1835 a: &Curve,
1836 b: &Curve,
1837 sb: &Sampled<Point>,
1838 crossing: &Crossing<Point>,
1839 gap: f64,
1840 tol: Tolerances,
1841) -> f64 {
1842 let (lo, hi) = a.domain();
1843 let span = hi - lo;
1844 if span <= 0.0 {
1845 return 0.0;
1846 }
1847 let mut extent = 0.0_f64;
1848 for direction in [-1.0, 1.0] {
1849 let inside = |t: f64| -> bool {
1850 if t < lo || t > hi {
1851 return false;
1852 }
1853 a.point_at(t, tol)
1854 .is_ok_and(|q| distance_to_curve_3d(b, sb, q, tol) <= gap)
1855 };
1856 let mut step = span * 1e-6;
1857 let mut last_in = crossing.on_a;
1858 let mut first_out: Option<f64> = None;
1859 while step <= span {
1860 let t = crossing.on_a + direction * step;
1861 if inside(t) {
1862 last_in = t;
1863 step *= 2.0;
1864 } else {
1865 first_out = Some(t);
1866 break;
1867 }
1868 }
1869 let edge = match first_out {
1870 Some(mut out) => {
1871 let mut r#in = last_in;
1872 for _ in 0..30 {
1873 let mid = f64::midpoint(r#in, out);
1874 if inside(mid) {
1875 r#in = mid;
1876 } else {
1877 out = mid;
1878 }
1879 }
1880 r#in
1881 }
1882 None => last_in,
1883 };
1884 if let Ok(q) = a.point_at(edge, tol) {
1885 extent = extent.max(q.distance(crossing.point));
1886 }
1887 }
1888 extent
1889}
1890
1891fn segments_approach_3d(a: (Point, Point), b: (Point, Point)) -> (f64, f64, f64) {
1893 let da = a.1 - a.0;
1894 let db = b.1 - b.0;
1895 let between = a.0 - b.0;
1896 let (aa, bb, ab) = (da.dot(da), db.dot(db), da.dot(db));
1897 let (ad, bd) = (da.dot(between), db.dot(between));
1898 let denominator = ab.mul_add(-ab, aa * bb);
1899
1900 let (mut t, mut s) = if denominator.abs() <= f64::MIN_POSITIVE {
1901 (
1902 0.0,
1903 if bb > 0.0 {
1904 (bd / bb).clamp(0.0, 1.0)
1905 } else {
1906 0.0
1907 },
1908 )
1909 } else {
1910 (
1911 (ab.mul_add(bd, -(bb * ad)) / denominator).clamp(0.0, 1.0),
1912 (aa.mul_add(bd, -(ab * ad)) / denominator).clamp(0.0, 1.0),
1913 )
1914 };
1915 if bb > 0.0 {
1917 s = ((da.dot(between) + t * aa - 0.0).mul_add(0.0, db.dot(between + da * t)) / bb)
1918 .clamp(0.0, 1.0);
1919 }
1920 if aa > 0.0 {
1921 t = (da.dot(db * s - between) / aa).clamp(0.0, 1.0);
1922 }
1923 let pa = a.0 + da * t;
1924 let pb = b.0 + db * s;
1925 (t, s, pa.distance(pb))
1926}
1927
1928fn order(a: f64, b: f64) -> (f64, f64) {
1929 if a <= b { (a, b) } else { (b, a) }
1930}
1931
1932fn sort_crossings<P>(crossings: &mut [Crossing<P>]) {
1933 crossings.sort_by(|x, y| {
1934 x.on_a
1935 .partial_cmp(&y.on_a)
1936 .unwrap_or(core::cmp::Ordering::Equal)
1937 });
1938}
1939
1940fn push_unique_2d(crossings: &mut Vec<Crossing<Point2>>, found: Crossing<Point2>, tol: Tolerances) {
1941 let reach = tol.confusion() * 100.0;
1942 if crossings
1943 .iter()
1944 .any(|c| c.point.distance(found.point) <= reach)
1945 {
1946 return;
1947 }
1948 crossings.push(found);
1949}
1950
1951fn push_unique_3d(crossings: &mut Vec<Crossing<Point>>, found: Crossing<Point>, tol: Tolerances) {
1952 let reach = tol.confusion() * 100.0;
1953 if crossings
1954 .iter()
1955 .any(|c| c.point.distance(found.point) <= reach)
1956 {
1957 return;
1958 }
1959 crossings.push(found);
1960}
1961
1962#[cfg(test)]
1963#[allow(clippy::unwrap_used)]
1964mod tests {
1965 use super::*;
1966 use ogeom_geom::{BSpline2d, Circle2d, CircleCurve, Line2d, LineCurve};
1967 use ogeom_math::{Circle, Circle2, Direction2, Frame, Frame2, KnotVector, Vector2};
1968
1969 const T: Tolerances = Tolerances::millimetres();
1970
1971 fn line2(from: Point2, to: Point2) -> PlanarCurve {
1972 Line2d::segment(from, to, T).unwrap().into()
1973 }
1974
1975 fn circle2(centre: Point2, radius: f64) -> PlanarCurve {
1976 Circle2d::new(
1977 Circle2::new(
1978 Frame2::new(centre, Direction2::new(Vector2::new(1.0, 0.0), T).unwrap()),
1979 radius,
1980 T,
1981 )
1982 .unwrap(),
1983 )
1984 .into()
1985 }
1986
1987 #[test]
1988 fn two_lines_cross_where_algebra_says() {
1989 let a = line2(Point2::new(0.0, 0.0), Point2::new(4.0, 4.0));
1990 let b = line2(Point2::new(0.0, 4.0), Point2::new(4.0, 0.0));
1991 let found = intersect_curves_2d(&a, &b, CurveCurveOptions::default(), T).unwrap();
1992 assert_eq!(found.crossings.len(), 1);
1993 let hit = &found.crossings[0];
1994 assert!(hit.point.is_equal(Point2::new(2.0, 2.0), T));
1995 approx::assert_relative_eq!(hit.on_a, 8.0_f64.sqrt(), epsilon = 1e-9);
1997
1998 let short = line2(Point2::new(0.0, 4.0), Point2::new(1.0, 3.0));
2000 assert!(
2001 intersect_curves_2d(&a, &short, CurveCurveOptions::default(), T)
2002 .unwrap()
2003 .is_empty()
2004 );
2005 }
2006
2007 #[test]
2008 fn collinear_lines_overlap_rather_than_crossing_everywhere() {
2009 let a = line2(Point2::new(0.0, 0.0), Point2::new(10.0, 0.0));
2010 let b = line2(Point2::new(4.0, 0.0), Point2::new(20.0, 0.0));
2011 let found = intersect_curves_2d(&a, &b, CurveCurveOptions::default(), T).unwrap();
2012 assert!(found.crossings.is_empty());
2013 assert_eq!(found.overlaps.len(), 1);
2014 let overlap = &found.overlaps[0];
2015 approx::assert_relative_eq!(overlap.on_a.0, 4.0, epsilon = 1e-9);
2016 approx::assert_relative_eq!(overlap.on_a.1, 10.0, epsilon = 1e-9);
2017 approx::assert_relative_eq!(overlap.on_b.0, 0.0, epsilon = 1e-9);
2018 approx::assert_relative_eq!(overlap.on_b.1, 6.0, epsilon = 1e-9);
2019
2020 let above = line2(Point2::new(0.0, 1.0), Point2::new(10.0, 1.0));
2022 assert!(
2023 intersect_curves_2d(&a, &above, CurveCurveOptions::default(), T)
2024 .unwrap()
2025 .is_empty()
2026 );
2027 }
2028
2029 #[test]
2030 fn a_line_meets_a_circle_in_two_points_one_or_none() {
2031 let circle = circle2(Point2::new(0.0, 0.0), 2.0);
2032 let through = line2(Point2::new(-5.0, 0.0), Point2::new(5.0, 0.0));
2033 let found =
2034 intersect_curves_2d(&through, &circle, CurveCurveOptions::default(), T).unwrap();
2035 assert_eq!(found.crossings.len(), 2);
2036 for hit in &found.crossings {
2037 approx::assert_relative_eq!(
2038 hit.point.distance(Point2::new(0.0, 0.0)),
2039 2.0,
2040 epsilon = 1e-9
2041 );
2042 let PlanarCurve::Circle(_) = &circle else {
2044 unreachable!()
2045 };
2046 let on_circle = circle.point_at(hit.on_b, T).unwrap();
2047 assert!(on_circle.is_equal(hit.point, T));
2048 }
2049
2050 let tangent = line2(Point2::new(-5.0, 2.0), Point2::new(5.0, 2.0));
2051 assert_eq!(
2052 intersect_curves_2d(&tangent, &circle, CurveCurveOptions::default(), T)
2053 .unwrap()
2054 .crossings
2055 .len(),
2056 1
2057 );
2058 let missing = line2(Point2::new(-5.0, 3.0), Point2::new(5.0, 3.0));
2059 assert!(
2060 intersect_curves_2d(&missing, &circle, CurveCurveOptions::default(), T)
2061 .unwrap()
2062 .is_empty()
2063 );
2064 }
2065
2066 #[test]
2067 fn two_circles_cross_touch_coincide_or_miss() {
2068 let a = circle2(Point2::new(0.0, 0.0), 2.0);
2069
2070 let crossing = circle2(Point2::new(3.0, 0.0), 2.0);
2071 let found = intersect_curves_2d(&a, &crossing, CurveCurveOptions::default(), T).unwrap();
2072 assert_eq!(found.crossings.len(), 2);
2073 for hit in &found.crossings {
2074 let on_a = a.point_at(hit.on_a, T).unwrap();
2075 let on_b = crossing.point_at(hit.on_b, T).unwrap();
2076 assert!(on_a.is_equal(hit.point, T));
2077 assert!(on_b.is_equal(hit.point, T));
2078 }
2079
2080 let touching = circle2(Point2::new(4.0, 0.0), 2.0);
2081 assert_eq!(
2082 intersect_curves_2d(&a, &touching, CurveCurveOptions::default(), T)
2083 .unwrap()
2084 .crossings
2085 .len(),
2086 1
2087 );
2088
2089 let same = circle2(Point2::new(0.0, 0.0), 2.0);
2090 let coincident = intersect_curves_2d(&a, &same, CurveCurveOptions::default(), T).unwrap();
2091 assert!(coincident.crossings.is_empty());
2092 assert_eq!(coincident.overlaps.len(), 1);
2093
2094 let apart = circle2(Point2::new(10.0, 0.0), 2.0);
2095 assert!(
2096 intersect_curves_2d(&a, &apart, CurveCurveOptions::default(), T)
2097 .unwrap()
2098 .is_empty()
2099 );
2100 }
2101
2102 #[test]
2103 fn the_general_path_handles_what_has_no_closed_form() {
2104 let wave: PlanarCurve = BSpline2d::new(
2107 KnotVector::new(vec![0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0], 3).unwrap(),
2108 vec![
2109 Point2::new(0.0, -1.0),
2110 Point2::new(1.0, 3.0),
2111 Point2::new(2.0, -3.0),
2112 Point2::new(3.0, 3.0),
2113 Point2::new(4.0, -1.0),
2114 ],
2115 T,
2116 )
2117 .unwrap()
2118 .into();
2119 let axis = line2(Point2::new(-1.0, 0.0), Point2::new(5.0, 0.0));
2120 let found = intersect_curves_2d(&wave, &axis, CurveCurveOptions::default(), T).unwrap();
2121 assert_eq!(found.crossings.len(), 3, "a wave crosses its axis thrice");
2122 for hit in &found.crossings {
2123 assert!(hit.gap < 1e-9);
2124 assert!(hit.point.y.abs() < 1e-9);
2125 let on_wave = wave.point_at(hit.on_a, T).unwrap();
2126 assert!(on_wave.is_equal(hit.point, T));
2127 }
2128 }
2129
2130 #[test]
2133 fn one_planar_circle_written_twice_states_the_correspondence() {
2134 let a = circle2(Point2::new(1.0, 2.0), 3.0);
2135 let quarter = Frame2::new(
2136 Point2::new(1.0, 2.0),
2137 Direction2::new(Vector2::new(0.0, 1.0), T).unwrap(),
2138 );
2139 let b: PlanarCurve = Circle2d::new(Circle2::new(quarter, 3.0, T).unwrap()).into();
2140 let flipped: PlanarCurve = ogeom_geom::Reversible::reversed(&b);
2141 for other in [b, flipped] {
2142 let found = intersect_curves_2d(&a, &other, CurveCurveOptions::default(), T).unwrap();
2143 assert_eq!(found.overlaps.len(), 1);
2144 let overlap = found.overlaps[0];
2145 for i in 0..=8 {
2146 let t = f64::from(i) / 8.0;
2147 let ta = (overlap.on_a.1 - overlap.on_a.0).mul_add(t, overlap.on_a.0);
2148 let tb = (overlap.on_b.1 - overlap.on_b.0).mul_add(t, overlap.on_b.0);
2149 let pa = a.point_at(ta, T).unwrap();
2150 let pb = other
2151 .point_at(tb.rem_euclid(core::f64::consts::TAU), T)
2152 .unwrap();
2153 assert!(pa.distance(pb) < 1e-9, "at {t}: {pa:?} against {pb:?}");
2154 }
2155 }
2156 }
2157
2158 #[test]
2161 fn collinear_segments_meeting_end_to_end_share_a_point() {
2162 let a = line2(Point2::new(0.0, 0.0), Point2::new(5.0, 0.0));
2163 let b = line2(Point2::new(5.0, 0.0), Point2::new(9.0, 0.0));
2164 let found = intersect_curves_2d(&a, &b, CurveCurveOptions::default(), T).unwrap();
2165 assert!(found.overlaps.is_empty());
2166 assert_eq!(found.crossings.len(), 1);
2167 assert!(found.crossings[0].point.distance(Point2::new(5.0, 0.0)) < 1e-12);
2168
2169 let a: Curve = LineCurve::segment(Point::ORIGIN, Point::new(0.0, 0.0, 5.0), T)
2170 .unwrap()
2171 .into();
2172 let b: Curve = LineCurve::segment(Point::new(0.0, 0.0, 5.0), Point::new(0.0, 0.0, 7.0), T)
2173 .unwrap()
2174 .into();
2175 let found = intersect_curves(&a, &b, CurveCurveOptions::default(), T).unwrap();
2176 assert!(found.overlaps.is_empty());
2177 assert_eq!(found.crossings.len(), 1);
2178 assert!(found.crossings[0].point.distance(Point::new(0.0, 0.0, 5.0)) < 1e-12);
2179 }
2180
2181 #[test]
2188 fn one_circle_written_twice_states_the_correspondence_between_them() {
2189 use ogeom_math::{Direction, Vector};
2190 let a: Curve = CircleCurve::new(Circle::new(Frame::WORLD, 3.0, T).unwrap()).into();
2191 let third = 2.0 * core::f64::consts::PI / 3.0;
2194 let flipped = Frame::new(
2195 Point::ORIGIN,
2196 -Direction::Z,
2197 Direction::new(Vector::new(third.cos(), third.sin(), 0.0), T).unwrap(),
2198 T,
2199 )
2200 .unwrap();
2201 let b: Curve = CircleCurve::new(Circle::new(flipped, 3.0, T).unwrap()).into();
2202 for pair in [(&a, &b), (&b, &a)] {
2203 let found = intersect_curves(pair.0, pair.1, CurveCurveOptions::default(), T).unwrap();
2204 assert!(
2205 found.crossings.is_empty(),
2206 "every point is a hit, so none is"
2207 );
2208 assert_eq!(found.overlaps.len(), 1);
2209 let overlap = &found.overlaps[0];
2210 let span = overlap.on_a.1 - overlap.on_a.0;
2211 for i in 0..=8 {
2212 let t = f64::from(i) / 8.0;
2213 let ta = span.mul_add(t, overlap.on_a.0);
2214 let tb = (overlap.on_b.1 - overlap.on_b.0).mul_add(t, overlap.on_b.0);
2215 let pa = pair.0.point_at(ta, T).unwrap();
2216 let pb = pair
2217 .1
2218 .point_at(tb.rem_euclid(core::f64::consts::TAU), T)
2219 .unwrap();
2220 assert!(
2221 pa.distance(pb) < 1e-9,
2222 "at {t}: {pa:?} against {pb:?} (on_a {:?} on_b {:?})",
2223 overlap.on_a,
2224 overlap.on_b
2225 );
2226 }
2227 }
2228 }
2229
2230 #[test]
2231 fn space_curves_cross_within_a_gap_and_report_it() {
2232 let a: Curve = CircleCurve::new(Circle::new(Frame::WORLD, 2.0, T).unwrap()).into();
2239 let lifted = Frame::new(
2240 Point::new(3.0, 0.0, 0.001),
2241 ogeom_math::Direction::Z,
2242 ogeom_math::Direction::X,
2243 T,
2244 )
2245 .unwrap();
2246 let b: Curve = CircleCurve::new(Circle::new(lifted, 2.0, T).unwrap()).into();
2247
2248 let options = CurveCurveOptions {
2249 gap: 1e-2,
2250 ..CurveCurveOptions::default()
2251 };
2252 let found = intersect_curves(&a, &b, options, T).unwrap();
2253 assert_eq!(found.crossings.len(), 2, "two near-crossings");
2254 for hit in &found.crossings {
2255 assert!(hit.gap > 1e-4, "the gap is real and must not be zeroed");
2256 assert!(hit.gap < 2e-3, "but small: {}", hit.gap);
2257 }
2258
2259 let strict = CurveCurveOptions {
2261 gap: 1e-5,
2262 ..CurveCurveOptions::default()
2263 };
2264 assert!(intersect_curves(&a, &b, strict, T).unwrap().is_empty());
2265 }
2266
2267 #[test]
2268 fn skew_lines_in_space_miss_and_close_ones_meet() {
2269 let a: Curve = LineCurve::segment(Point::ORIGIN, Point::new(10.0, 0.0, 0.0), T)
2270 .unwrap()
2271 .into();
2272 let skew: Curve =
2273 LineCurve::segment(Point::new(0.0, -5.0, 1.0), Point::new(0.0, 5.0, 1.0), T)
2274 .unwrap()
2275 .into();
2276 assert!(
2277 intersect_curves(&a, &skew, CurveCurveOptions::default(), T)
2278 .unwrap()
2279 .is_empty(),
2280 "a unit apart is not a crossing"
2281 );
2282
2283 let meeting: Curve =
2284 LineCurve::segment(Point::new(5.0, -5.0, 0.0), Point::new(5.0, 5.0, 0.0), T)
2285 .unwrap()
2286 .into();
2287 let found = intersect_curves(&a, &meeting, CurveCurveOptions::default(), T).unwrap();
2288 assert_eq!(found.crossings.len(), 1);
2289 assert!(
2290 found.crossings[0]
2291 .point
2292 .is_equal(Point::new(5.0, 0.0, 0.0), T)
2293 );
2294 assert!(found.crossings[0].gap < 1e-12);
2295
2296 let collinear: Curve =
2298 LineCurve::segment(Point::new(4.0, 0.0, 0.0), Point::new(20.0, 0.0, 0.0), T)
2299 .unwrap()
2300 .into();
2301 let shared = intersect_curves(&a, &collinear, CurveCurveOptions::default(), T).unwrap();
2302 assert_eq!(shared.overlaps.len(), 1);
2303 }
2304
2305 #[test]
2306 fn a_fitted_curve_tracing_an_arc_is_one_overlap_not_a_row_of_crossings() {
2307 let circle: Curve = CircleCurve::new(Circle::new(Frame::WORLD, 4.0, T).unwrap()).into();
2313 let points: Vec<Point> = (0..=40)
2314 .map(|i| {
2315 let a = 0.2 + 1.0 * f64::from(i) / 40.0;
2316 Point::new(4.0 * a.cos(), 4.0 * a.sin(), 0.0)
2317 })
2318 .collect();
2319 let fitted: Curve = ogeom_geom::fit::fit_points(&points, 3, 1e-7, T)
2320 .unwrap()
2321 .curve
2322 .into();
2323 let options = CurveCurveOptions {
2324 gap: 1e-5,
2325 ..CurveCurveOptions::default()
2326 };
2327 let found = intersect_curves(&fitted, &circle, options, T).unwrap();
2328 assert_eq!(found.overlaps.len(), 1, "one shared stretch: {found:?}");
2329 let (lo, hi) = found.overlaps[0].on_a;
2330 let (fa, fb) = fitted.domain();
2331 assert!(
2332 lo - fa < 1e-3 && fb - hi < 1e-3,
2333 "the whole fit runs along the circle"
2334 );
2335 assert!(
2336 found.crossings.is_empty(),
2337 "no crossing survives inside the overlap: {:?}",
2338 found.crossings
2339 );
2340 }
2341
2342 #[test]
2343 fn an_arc_ending_tangent_to_a_line_is_one_crossing_with_its_reach() {
2344 let circle: Curve = CircleCurve::new(Circle::new(Frame::WORLD, 4.0, T).unwrap()).into();
2349 let line: Curve =
2350 LineCurve::segment(Point::new(4.0, -3.0, 0.0), Point::new(4.0, 3.0, 0.0), T)
2351 .unwrap()
2352 .into();
2353 let options = CurveCurveOptions {
2354 gap: 1e-5,
2355 ..CurveCurveOptions::default()
2356 };
2357 let found = intersect_curves(&circle, &line, options, T).unwrap();
2358 assert_eq!(found.crossings.len(), 1, "one touch: {:?}", found.crossings);
2359 let touch = found.crossings[0];
2360 assert!(
2361 touch.point.distance(Point::new(4.0, 0.0, 0.0)) < 2e-2,
2362 "{touch:?}"
2363 );
2364 assert!(touch.reach < 5e-2, "the valley is short: {touch:?}");
2365 assert!(found.overlaps.is_empty());
2366 }
2367
2368 #[test]
2369 fn unusable_options_are_refused() {
2370 let a = line2(Point2::new(0.0, 0.0), Point2::new(1.0, 0.0));
2371 for options in [
2372 CurveCurveOptions {
2373 samples: 1,
2374 ..CurveCurveOptions::default()
2375 },
2376 CurveCurveOptions {
2377 gap: 0.0,
2378 ..CurveCurveOptions::default()
2379 },
2380 CurveCurveOptions {
2381 gap: f64::NAN,
2382 ..CurveCurveOptions::default()
2383 },
2384 ] {
2385 assert!(intersect_curves_2d(&a, &a.clone(), options, T).is_err());
2386 }
2387 }
2388
2389 #[test]
2394 fn circles_and_ellipses_in_different_planes_meet_where_the_planes_do() {
2395 use ogeom_geom::EllipseCurve;
2396 use ogeom_math::{Direction, Ellipse, Vector};
2397 let radius = 2.0;
2398 let tilted = |normal: Vector, major: Vector, lean: f64| -> Curve {
2399 let frame = Frame::new(
2400 Point::ORIGIN,
2401 Direction::new(normal, T).unwrap(),
2402 Direction::new(major, T).unwrap(),
2403 T,
2404 )
2405 .unwrap();
2406 EllipseCurve::new(Ellipse::new(frame, radius / lean.cos(), radius, T).unwrap()).into()
2407 };
2408 let (p, q) = (0.4_f64, 0.7_f64);
2409 let about_x = tilted(
2410 Vector::new(0.0, -p.sin(), p.cos()),
2411 Vector::new(0.0, p.cos(), p.sin()),
2412 p,
2413 );
2414 let about_y = tilted(
2415 Vector::new(-q.sin(), 0.0, q.cos()),
2416 Vector::new(q.cos(), 0.0, q.sin()),
2417 q,
2418 );
2419 let circle: Curve = CircleCurve::new(Circle::new(Frame::WORLD, radius, T).unwrap()).into();
2420 for (a, b) in [
2421 (&about_x, &about_y),
2422 (&about_y, &about_x),
2423 (&circle, &about_x),
2424 (&about_y, &circle),
2425 ] {
2426 let found = intersect_curves(a, b, CurveCurveOptions::default(), T).unwrap();
2427 assert_eq!(found.crossings.len(), 2, "{found:?}");
2428 for hit in &found.crossings {
2429 let on_a = a.point_at(hit.on_a, T).unwrap();
2430 let on_b = b.point_at(hit.on_b, T).unwrap();
2431 assert!(on_a.distance(on_b) < 1e-9, "{on_a:?} against {on_b:?}");
2432 assert!((on_a.x.hypot(on_a.y) - radius).abs() < 1e-9);
2433 }
2434 }
2435 let lifted = Frame::new(Point::new(0.0, 0.0, 1.0), Direction::Z, Direction::X, T).unwrap();
2436 let above: Curve = CircleCurve::new(Circle::new(lifted, radius, T).unwrap()).into();
2437 assert!(
2438 intersect_curves(&circle, &above, CurveCurveOptions::default(), T)
2439 .unwrap()
2440 .is_empty()
2441 );
2442 }
2443
2444 #[test]
2449 fn circles_in_all_but_one_plane_meet_where_they_pass() {
2450 use ogeom_math::{Direction, Vector};
2451 let flat: Curve = CircleCurve::new(Circle::new(Frame::WORLD, 2.0, T).unwrap()).into();
2452 let lean = 1e-6;
2453 let normal = Direction::new(Vector::new(lean, 0.0, 1.0), T).unwrap();
2454 let centre = Point::new(1.0, 0.0, lean.mul_add(-0.5, 5e-8));
2455 let tilted_frame = Frame::new(
2456 centre,
2457 normal,
2458 Direction::new(Vector::new(1.0, 0.0, -lean), T).unwrap(),
2459 T,
2460 )
2461 .unwrap();
2462 let tilted: Curve = CircleCurve::new(Circle::new(tilted_frame, 2.0, T).unwrap()).into();
2463 let found = intersect_curves(&flat, &tilted, CurveCurveOptions::default(), T).unwrap();
2464 assert_eq!(found.crossings.len(), 2, "{found:?}");
2465 for hit in &found.crossings {
2466 assert!(hit.gap < 1e-7, "{hit:?}");
2467 let p = flat.point_at(hit.on_a, T).unwrap();
2468 assert!((p.x - 0.5).abs() < 1e-4, "{p:?}");
2469 }
2470 }
2471
2472 #[test]
2477 fn lines_meet_circles_and_ellipses_in_closed_form() {
2478 use ogeom_geom::EllipseCurve;
2479 use ogeom_math::Ellipse;
2480 let ellipse: Curve =
2481 EllipseCurve::new(Ellipse::new(Frame::WORLD, 3.0, 2.0, T).unwrap()).into();
2482 let across: Curve =
2483 LineCurve::segment(Point::new(-5.0, 1.0, 0.0), Point::new(5.0, 1.0, 0.0), T)
2484 .unwrap()
2485 .into();
2486 for (a, b, line_first) in [(&ellipse, &across, false), (&across, &ellipse, true)] {
2487 let found = intersect_curves(a, b, CurveCurveOptions::default(), T).unwrap();
2488 assert_eq!(found.crossings.len(), 2, "{found:?}");
2489 for hit in &found.crossings {
2490 let p = a.point_at(hit.on_a, T).unwrap();
2491 let q = b.point_at(hit.on_b, T).unwrap();
2492 assert!(p.distance(q) < 1e-9, "{p:?} against {q:?}");
2493 let on_line = if line_first { p } else { q };
2494 assert!((on_line.y - 1.0).abs() < 1e-12);
2495 }
2496 }
2497 let circle: Curve = CircleCurve::new(Circle::new(Frame::WORLD, 2.0, T).unwrap()).into();
2498 let through: Curve =
2499 LineCurve::segment(Point::new(2.0, 0.0, -1.0), Point::new(2.0, 0.0, 1.0), T)
2500 .unwrap()
2501 .into();
2502 let found = intersect_curves(&circle, &through, CurveCurveOptions::default(), T).unwrap();
2503 assert_eq!(found.crossings.len(), 1);
2504 assert!(found.crossings[0].on_a.abs() < 1e-9);
2505 assert!((found.crossings[0].on_b - 1.0).abs() < 1e-9);
2506 let inside: Curve =
2507 LineCurve::segment(Point::new(1.0, 0.0, -1.0), Point::new(1.0, 0.0, 1.0), T)
2508 .unwrap()
2509 .into();
2510 assert!(
2511 intersect_curves(&circle, &inside, CurveCurveOptions::default(), T)
2512 .unwrap()
2513 .is_empty()
2514 );
2515 }
2516
2517 #[test]
2522 fn arcs_of_one_circle_overlap_across_the_seam() {
2523 use ogeom_geom::{Curve3d as _, TrimmedCurve};
2524 let circle = |x: ogeom_math::Direction, z: ogeom_math::Direction| -> Curve {
2525 let frame = Frame::new(Point::new(10.0, 20.0, 30.0), z, x, T).unwrap();
2526 CircleCurve::new(Circle::new(frame, 50.0, T).unwrap()).into()
2527 };
2528 let trim = |c: &Curve, a: f64, b: f64| -> Curve {
2529 TrimmedCurve::new(c.clone(), a, b, T).unwrap().into()
2530 };
2531 let (x, y) = (ogeom_math::Direction::X, ogeom_math::Direction::Y);
2532 let (up, down) = (ogeom_math::Direction::Z, -ogeom_math::Direction::Z);
2533 let plain = circle(x, up);
2534 let turned = circle(y, up);
2535 let backwards = circle(x, down);
2536 for (a, b, share) in [
2538 (trim(&plain, 5.5, 7.0), trim(&plain, 0.2, 1.0), 0.517 / 1.5),
2539 (trim(&plain, 0.2, 1.0), trim(&plain, 5.5, 7.0), 0.517 / 0.8),
2540 (
2541 trim(&plain, 5.8, 6.8),
2542 trim(&backwards, 5.0, 6.2),
2543 0.4336 / 1.0,
2544 ),
2545 (trim(&plain, 5.0, 7.5), backwards.clone(), 1.0),
2546 (trim(&plain, 0.5, 6.0), trim(&turned, 4.0, 6.0), 1.218 / 5.5),
2547 ] {
2548 let found = intersect_curves(&a, &b, CurveCurveOptions::default(), T).unwrap();
2549 let (wa, wb) = (a.domain(), b.domain());
2550 let mut covered = 0.0;
2551 for o in &found.overlaps {
2552 for (t, w) in [
2553 (o.on_a.0, wa),
2554 (o.on_a.1, wa),
2555 (o.on_b.0, wb),
2556 (o.on_b.1, wb),
2557 ] {
2558 assert!(t >= w.0 - 1e-9 && t <= w.1 + 1e-9, "{t} outside {w:?}");
2559 }
2560 for (ta, tb) in [(o.on_a.0, o.on_b.0), (o.on_a.1, o.on_b.1)] {
2561 let gap = a
2562 .point_at(ta, T)
2563 .unwrap()
2564 .distance(b.point_at(tb, T).unwrap());
2565 assert!(gap < 1e-9, "ends {gap} apart");
2566 }
2567 covered += (o.on_a.1 - o.on_a.0).abs();
2568 }
2569 let want = share * (wa.1 - wa.0);
2570 assert!((covered - want).abs() < 2e-3, "{covered} against {want}");
2571 }
2572 }
2573
2574 #[test]
2578 fn a_trimmed_arc_meets_tangents_as_its_circle_does() {
2579 use ogeom_geom::Trimmed2d;
2580 let options = CurveCurveOptions::default();
2581 let circle = circle2(Point2::new(0.0, 0.0), 10.0);
2582 let arc: PlanarCurve = Trimmed2d::new(circle, 0.3, 2.9, T).unwrap().into();
2583 let tangent = line2(Point2::new(-20.0, 10.0), Point2::new(20.0, 10.0));
2584 let found = intersect_curves_2d(&arc, &tangent, options, T).unwrap();
2585 assert_eq!(found.crossings.len(), 1);
2586 assert!(found.crossings[0].point.is_equal(Point2::new(0.0, 10.0), T));
2587 let grazing = line2(
2588 Point2::new(-20.0, 10.0 - 1e-4),
2589 Point2::new(20.0, 10.0 - 1e-4),
2590 );
2591 let found = intersect_curves_2d(&arc, &grazing, options, T).unwrap();
2592 assert_eq!(found.crossings.len(), 2);
2593 let beside: PlanarCurve =
2594 Trimmed2d::new(circle2(Point2::new(20.0, 0.0), 10.0), 2.0, 4.5, T)
2595 .unwrap()
2596 .into();
2597 let whole = circle2(Point2::new(0.0, 0.0), 10.0);
2598 let found = intersect_curves_2d(&whole, &beside, options, T).unwrap();
2599 assert_eq!(found.crossings.len(), 1);
2600 assert!(found.crossings[0].point.is_equal(Point2::new(10.0, 0.0), T));
2601 let below = line2(Point2::new(-20.0, -10.0), Point2::new(20.0, -10.0));
2603 assert!(
2604 intersect_curves_2d(&arc, &below, options, T)
2605 .unwrap()
2606 .is_empty()
2607 );
2608 }
2609}