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(a, &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(
1702 a: &Curve,
1703 b: &Curve,
1704 seed_a: f64,
1705 seed_b: f64,
1706 options: CurveCurveOptions,
1707 tol: Tolerances,
1708) -> Option<Crossing<Point>> {
1709 let (t, s, converged) = stationary_3d(a, b, seed_a, seed_b, tol)?;
1710 let mut found = crossing_at_3d(a, b, t, s, tol)?;
1711 if found.gap > options.gap && !converged {
1712 let (t, s) = alternating_feet_3d(a, b, t, s, found.gap, tol)?;
1713 let walked = crossing_at_3d(a, b, t, s, tol)?;
1714 let polished = stationary_3d(a, b, t, s, tol)
1715 .and_then(|(t, s, _)| crossing_at_3d(a, b, t, s, tol))
1716 .filter(|p| p.gap < walked.gap);
1717 found = polished.unwrap_or(walked);
1718 }
1719 (found.gap <= options.gap).then_some(found)
1720}
1721
1722fn crossing_at_3d(
1724 a: &Curve,
1725 b: &Curve,
1726 t: f64,
1727 s: f64,
1728 tol: Tolerances,
1729) -> Option<Crossing<Point>> {
1730 let pa = a.point_at(t, tol).ok()?;
1731 let pb = b.point_at(s, tol).ok()?;
1732 Some(Crossing {
1733 on_a: t,
1734 on_b: s,
1735 point: pa,
1736 gap: pa.distance(pb),
1737 reach: 0.0,
1738 })
1739}
1740
1741fn alternating_feet_3d(
1747 a: &Curve,
1748 b: &Curve,
1749 mut t: f64,
1750 mut s: f64,
1751 mut gap: f64,
1752 tol: Tolerances,
1753) -> Option<(f64, f64)> {
1754 for _ in 0..64 {
1755 let before = gap;
1756 let pa = a.point_at(t, tol).ok()?;
1757 if let Some((next, d)) = foot_on_3d(b, pa, s, tol)
1758 && d < gap
1759 {
1760 (s, gap) = (next, d);
1761 }
1762 let pb = b.point_at(s, tol).ok()?;
1763 if let Some((next, d)) = foot_on_3d(a, pb, t, tol)
1764 && d < gap
1765 {
1766 (t, gap) = (next, d);
1767 }
1768 if before - gap <= tol.confusion() * 0.01 {
1769 break;
1770 }
1771 }
1772 Some((t, s))
1773}
1774
1775fn stationary_3d(
1779 a: &Curve,
1780 b: &Curve,
1781 seed_a: f64,
1782 seed_b: f64,
1783 tol: Tolerances,
1784) -> Option<(f64, f64, bool)> {
1785 let system = |x: [f64; 2]| {
1788 let (t, s) = (clamp_3d(a, x[0]), clamp_3d(b, x[1]));
1789 let (Ok(da), Ok(db)) = (a.derivatives_at(t, 2, tol), b.derivatives_at(s, 2, tol)) else {
1790 return ([f64::INFINITY; 2], [[0.0; 2]; 2]);
1793 };
1794 let zero = ogeom_math::Vector::ZERO;
1795 let at = |d: &[ogeom_math::Vector], k: usize| d.get(k).copied().unwrap_or(zero);
1796 let (pa, d1a, d2a) = (at(&da, 0), at(&da, 1), at(&da, 2));
1797 let (pb, d1b, d2b) = (at(&db, 0), at(&db, 1), at(&db, 2));
1798 let gap = pa - pb;
1799 (
1800 [gap.dot(d1a), -gap.dot(d1b)],
1801 [
1802 [d1a.dot(d1a) + gap.dot(d2a), -d1a.dot(d1b)],
1803 [-d1a.dot(d1b), d1b.dot(d1b) - gap.dot(d2b)],
1804 ],
1805 )
1806 };
1807 let criteria = solve::Criteria {
1808 residual: tol.confusion() * 0.01,
1809 step: tol.parametric(),
1810 max_iterations: 40,
1811 };
1812 let ([t, s], _, convergence, _) =
1813 solve::newton_system_2(system, [seed_a, seed_b], criteria).ok()?;
1814 Some((
1815 clamp_3d(a, t),
1816 clamp_3d(b, s),
1817 convergence == solve::Convergence::Residual,
1818 ))
1819}
1820
1821fn clamp_2d(curve: &PlanarCurve, t: f64) -> f64 {
1824 let (lo, hi) = curve.domain();
1825 if curve.is_periodic() {
1826 let span = hi - lo;
1827 if span > 0.0 {
1828 return lo + (t - lo).rem_euclid(span);
1829 }
1830 }
1831 t.clamp(lo, hi)
1832}
1833
1834fn clamp_3d(curve: &Curve, t: f64) -> f64 {
1835 let (lo, hi) = curve.domain();
1836 if curve.is_periodic() {
1837 let span = hi - lo;
1838 if span > 0.0 {
1839 return lo + (t - lo).rem_euclid(span);
1840 }
1841 }
1842 t.clamp(lo, hi)
1843}
1844
1845fn segments_cross_2d(a: (Point2, Point2), b: (Point2, Point2)) -> Option<(f64, f64)> {
1847 let da = a.1 - a.0;
1848 let db = b.1 - b.0;
1849 let cross = da.cross(db);
1850 if cross.abs() <= f64::MIN_POSITIVE {
1851 return None;
1852 }
1853 let between = b.0 - a.0;
1854 let t = between.cross(db) / cross;
1855 let s = between.cross(da) / cross;
1856 if !(0.0..=1.0).contains(&t) || !(0.0..=1.0).contains(&s) {
1857 return None;
1858 }
1859 Some((t, s))
1860}
1861
1862fn distance_to_curve_3d(b: &Curve, sb: &Sampled<Point>, p: Point, tol: Tolerances) -> f64 {
1865 let mut best = (0_usize, f64::INFINITY);
1866 for i in 1..sb.points.len() {
1867 let (q0, q1) = (sb.points[i - 1], sb.points[i]);
1868 let d = q1 - q0;
1869 let len2 = d.dot(d);
1870 let f = if len2 <= f64::MIN_POSITIVE {
1871 0.0
1872 } else {
1873 ((p - q0).dot(d) / len2).clamp(0.0, 1.0)
1874 };
1875 let dist = p.distance(q0 + d * f);
1876 if dist < best.1 {
1877 best = (i, dist);
1878 }
1879 }
1880 if best.0 == 0 {
1881 return best.1;
1882 }
1883 let (mut lo, mut hi) = (sb.parameters[best.0 - 1], sb.parameters[best.0]);
1884 let at = |t: f64| -> f64 { b.point_at(t, tol).map_or(f64::INFINITY, |q| q.distance(p)) };
1885 let phi = 0.5 * (3.0 - 5.0_f64.sqrt());
1888 let (mut x1, mut x2) = (lo + phi * (hi - lo), hi - phi * (hi - lo));
1889 let (mut f1, mut f2) = (at(x1), at(x2));
1890 for _ in 0..48 {
1891 if f1 < f2 {
1892 hi = x2;
1893 x2 = x1;
1894 f2 = f1;
1895 x1 = lo + phi * (hi - lo);
1896 f1 = at(x1);
1897 } else {
1898 lo = x1;
1899 x1 = x2;
1900 f1 = f2;
1901 x2 = hi - phi * (hi - lo);
1902 f2 = at(x2);
1903 }
1904 }
1905 f1.min(f2).min(best.1)
1906}
1907
1908fn valley_extent_3d(
1911 a: &Curve,
1912 b: &Curve,
1913 sb: &Sampled<Point>,
1914 crossing: &Crossing<Point>,
1915 gap: f64,
1916 tol: Tolerances,
1917) -> f64 {
1918 let (lo, hi) = a.domain();
1919 let span = hi - lo;
1920 if span <= 0.0 {
1921 return 0.0;
1922 }
1923 let mut extent = 0.0_f64;
1924 for direction in [-1.0, 1.0] {
1925 let inside = |t: f64| -> bool {
1926 if t < lo || t > hi {
1927 return false;
1928 }
1929 a.point_at(t, tol)
1930 .is_ok_and(|q| distance_to_curve_3d(b, sb, q, tol) <= gap)
1931 };
1932 let mut step = span * 1e-6;
1933 let mut last_in = crossing.on_a;
1934 let mut first_out: Option<f64> = None;
1935 while step <= span {
1936 let t = crossing.on_a + direction * step;
1937 if inside(t) {
1938 last_in = t;
1939 step *= 2.0;
1940 } else {
1941 first_out = Some(t);
1942 break;
1943 }
1944 }
1945 let edge = match first_out {
1946 Some(mut out) => {
1947 let mut r#in = last_in;
1948 for _ in 0..30 {
1949 let mid = f64::midpoint(r#in, out);
1950 if inside(mid) {
1951 r#in = mid;
1952 } else {
1953 out = mid;
1954 }
1955 }
1956 r#in
1957 }
1958 None => last_in,
1959 };
1960 if let Ok(q) = a.point_at(edge, tol) {
1961 extent = extent.max(q.distance(crossing.point));
1962 }
1963 }
1964 extent
1965}
1966
1967fn segments_approach_3d(a: (Point, Point), b: (Point, Point)) -> (f64, f64, f64) {
1969 let da = a.1 - a.0;
1970 let db = b.1 - b.0;
1971 let between = a.0 - b.0;
1972 let (aa, bb, ab) = (da.dot(da), db.dot(db), da.dot(db));
1973 let (ad, bd) = (da.dot(between), db.dot(between));
1974 let denominator = ab.mul_add(-ab, aa * bb);
1975
1976 let (mut t, mut s) = if denominator.abs() <= f64::MIN_POSITIVE {
1977 (
1978 0.0,
1979 if bb > 0.0 {
1980 (bd / bb).clamp(0.0, 1.0)
1981 } else {
1982 0.0
1983 },
1984 )
1985 } else {
1986 (
1987 (ab.mul_add(bd, -(bb * ad)) / denominator).clamp(0.0, 1.0),
1988 (aa.mul_add(bd, -(ab * ad)) / denominator).clamp(0.0, 1.0),
1989 )
1990 };
1991 if bb > 0.0 {
1993 s = ((da.dot(between) + t * aa - 0.0).mul_add(0.0, db.dot(between + da * t)) / bb)
1994 .clamp(0.0, 1.0);
1995 }
1996 if aa > 0.0 {
1997 t = (da.dot(db * s - between) / aa).clamp(0.0, 1.0);
1998 }
1999 let pa = a.0 + da * t;
2000 let pb = b.0 + db * s;
2001 (t, s, pa.distance(pb))
2002}
2003
2004fn order(a: f64, b: f64) -> (f64, f64) {
2005 if a <= b { (a, b) } else { (b, a) }
2006}
2007
2008fn sort_crossings<P>(crossings: &mut [Crossing<P>]) {
2009 crossings.sort_by(|x, y| {
2010 x.on_a
2011 .partial_cmp(&y.on_a)
2012 .unwrap_or(core::cmp::Ordering::Equal)
2013 });
2014}
2015
2016fn push_unique_2d(crossings: &mut Vec<Crossing<Point2>>, found: Crossing<Point2>, tol: Tolerances) {
2017 let reach = tol.confusion() * 100.0;
2018 if crossings
2019 .iter()
2020 .any(|c| c.point.distance(found.point) <= reach)
2021 {
2022 return;
2023 }
2024 crossings.push(found);
2025}
2026
2027fn push_unique_3d(
2034 a: &Curve,
2035 crossings: &mut Vec<Crossing<Point>>,
2036 found: Crossing<Point>,
2037 tol: Tolerances,
2038) {
2039 let reach = tol.confusion() * 100.0;
2040 let (lo, hi) = a.domain();
2041 let period = hi - lo;
2042 let closed = a.is_periodic()
2043 || (period.is_finite()
2044 && a.point_at(lo, tol)
2045 .ok()
2046 .zip(a.point_at(hi, tol).ok())
2047 .is_some_and(|(p, q)| p.distance(q) <= reach));
2048 let same = |c: &Crossing<Point>| {
2049 if c.point.distance(found.point) > reach {
2050 return false;
2051 }
2052 let mut mid = f64::midpoint(c.on_a, found.on_a);
2053 if closed && (c.on_a - found.on_a).abs() > period * 0.5 {
2054 mid += period * 0.5;
2055 if mid > hi {
2056 mid -= period;
2057 }
2058 }
2059 a.point_at(mid, tol)
2060 .is_ok_and(|p| p.distance(found.point) <= reach)
2061 };
2062 if crossings.iter().any(same) {
2063 return;
2064 }
2065 crossings.push(found);
2066}
2067
2068#[cfg(test)]
2069#[allow(clippy::unwrap_used)]
2070mod tests {
2071 use super::*;
2072 use ogeom_geom::{BSpline2d, Circle2d, CircleCurve, Line2d, LineCurve};
2073 use ogeom_math::{Circle, Circle2, Direction2, Frame, Frame2, KnotVector, Vector2};
2074
2075 const T: Tolerances = Tolerances::millimetres();
2076
2077 fn line2(from: Point2, to: Point2) -> PlanarCurve {
2078 Line2d::segment(from, to, T).unwrap().into()
2079 }
2080
2081 fn circle2(centre: Point2, radius: f64) -> PlanarCurve {
2082 Circle2d::new(
2083 Circle2::new(
2084 Frame2::new(centre, Direction2::new(Vector2::new(1.0, 0.0), T).unwrap()),
2085 radius,
2086 T,
2087 )
2088 .unwrap(),
2089 )
2090 .into()
2091 }
2092
2093 #[test]
2094 fn two_lines_cross_where_algebra_says() {
2095 let a = line2(Point2::new(0.0, 0.0), Point2::new(4.0, 4.0));
2096 let b = line2(Point2::new(0.0, 4.0), Point2::new(4.0, 0.0));
2097 let found = intersect_curves_2d(&a, &b, CurveCurveOptions::default(), T).unwrap();
2098 assert_eq!(found.crossings.len(), 1);
2099 let hit = &found.crossings[0];
2100 assert!(hit.point.is_equal(Point2::new(2.0, 2.0), T));
2101 approx::assert_relative_eq!(hit.on_a, 8.0_f64.sqrt(), epsilon = 1e-9);
2103
2104 let short = line2(Point2::new(0.0, 4.0), Point2::new(1.0, 3.0));
2106 assert!(
2107 intersect_curves_2d(&a, &short, CurveCurveOptions::default(), T)
2108 .unwrap()
2109 .is_empty()
2110 );
2111 }
2112
2113 #[test]
2114 fn collinear_lines_overlap_rather_than_crossing_everywhere() {
2115 let a = line2(Point2::new(0.0, 0.0), Point2::new(10.0, 0.0));
2116 let b = line2(Point2::new(4.0, 0.0), Point2::new(20.0, 0.0));
2117 let found = intersect_curves_2d(&a, &b, CurveCurveOptions::default(), T).unwrap();
2118 assert!(found.crossings.is_empty());
2119 assert_eq!(found.overlaps.len(), 1);
2120 let overlap = &found.overlaps[0];
2121 approx::assert_relative_eq!(overlap.on_a.0, 4.0, epsilon = 1e-9);
2122 approx::assert_relative_eq!(overlap.on_a.1, 10.0, epsilon = 1e-9);
2123 approx::assert_relative_eq!(overlap.on_b.0, 0.0, epsilon = 1e-9);
2124 approx::assert_relative_eq!(overlap.on_b.1, 6.0, epsilon = 1e-9);
2125
2126 let above = line2(Point2::new(0.0, 1.0), Point2::new(10.0, 1.0));
2128 assert!(
2129 intersect_curves_2d(&a, &above, CurveCurveOptions::default(), T)
2130 .unwrap()
2131 .is_empty()
2132 );
2133 }
2134
2135 #[test]
2136 fn a_line_meets_a_circle_in_two_points_one_or_none() {
2137 let circle = circle2(Point2::new(0.0, 0.0), 2.0);
2138 let through = line2(Point2::new(-5.0, 0.0), Point2::new(5.0, 0.0));
2139 let found =
2140 intersect_curves_2d(&through, &circle, CurveCurveOptions::default(), T).unwrap();
2141 assert_eq!(found.crossings.len(), 2);
2142 for hit in &found.crossings {
2143 approx::assert_relative_eq!(
2144 hit.point.distance(Point2::new(0.0, 0.0)),
2145 2.0,
2146 epsilon = 1e-9
2147 );
2148 let PlanarCurve::Circle(_) = &circle else {
2150 unreachable!()
2151 };
2152 let on_circle = circle.point_at(hit.on_b, T).unwrap();
2153 assert!(on_circle.is_equal(hit.point, T));
2154 }
2155
2156 let tangent = line2(Point2::new(-5.0, 2.0), Point2::new(5.0, 2.0));
2157 assert_eq!(
2158 intersect_curves_2d(&tangent, &circle, CurveCurveOptions::default(), T)
2159 .unwrap()
2160 .crossings
2161 .len(),
2162 1
2163 );
2164 let missing = line2(Point2::new(-5.0, 3.0), Point2::new(5.0, 3.0));
2165 assert!(
2166 intersect_curves_2d(&missing, &circle, CurveCurveOptions::default(), T)
2167 .unwrap()
2168 .is_empty()
2169 );
2170 }
2171
2172 #[test]
2173 fn two_circles_cross_touch_coincide_or_miss() {
2174 let a = circle2(Point2::new(0.0, 0.0), 2.0);
2175
2176 let crossing = circle2(Point2::new(3.0, 0.0), 2.0);
2177 let found = intersect_curves_2d(&a, &crossing, CurveCurveOptions::default(), T).unwrap();
2178 assert_eq!(found.crossings.len(), 2);
2179 for hit in &found.crossings {
2180 let on_a = a.point_at(hit.on_a, T).unwrap();
2181 let on_b = crossing.point_at(hit.on_b, T).unwrap();
2182 assert!(on_a.is_equal(hit.point, T));
2183 assert!(on_b.is_equal(hit.point, T));
2184 }
2185
2186 let touching = circle2(Point2::new(4.0, 0.0), 2.0);
2187 assert_eq!(
2188 intersect_curves_2d(&a, &touching, CurveCurveOptions::default(), T)
2189 .unwrap()
2190 .crossings
2191 .len(),
2192 1
2193 );
2194
2195 let same = circle2(Point2::new(0.0, 0.0), 2.0);
2196 let coincident = intersect_curves_2d(&a, &same, CurveCurveOptions::default(), T).unwrap();
2197 assert!(coincident.crossings.is_empty());
2198 assert_eq!(coincident.overlaps.len(), 1);
2199
2200 let apart = circle2(Point2::new(10.0, 0.0), 2.0);
2201 assert!(
2202 intersect_curves_2d(&a, &apart, CurveCurveOptions::default(), T)
2203 .unwrap()
2204 .is_empty()
2205 );
2206 }
2207
2208 #[test]
2209 fn the_general_path_handles_what_has_no_closed_form() {
2210 let wave: PlanarCurve = BSpline2d::new(
2213 KnotVector::new(vec![0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0], 3).unwrap(),
2214 vec![
2215 Point2::new(0.0, -1.0),
2216 Point2::new(1.0, 3.0),
2217 Point2::new(2.0, -3.0),
2218 Point2::new(3.0, 3.0),
2219 Point2::new(4.0, -1.0),
2220 ],
2221 T,
2222 )
2223 .unwrap()
2224 .into();
2225 let axis = line2(Point2::new(-1.0, 0.0), Point2::new(5.0, 0.0));
2226 let found = intersect_curves_2d(&wave, &axis, CurveCurveOptions::default(), T).unwrap();
2227 assert_eq!(found.crossings.len(), 3, "a wave crosses its axis thrice");
2228 for hit in &found.crossings {
2229 assert!(hit.gap < 1e-9);
2230 assert!(hit.point.y.abs() < 1e-9);
2231 let on_wave = wave.point_at(hit.on_a, T).unwrap();
2232 assert!(on_wave.is_equal(hit.point, T));
2233 }
2234 }
2235
2236 #[test]
2239 fn one_planar_circle_written_twice_states_the_correspondence() {
2240 let a = circle2(Point2::new(1.0, 2.0), 3.0);
2241 let quarter = Frame2::new(
2242 Point2::new(1.0, 2.0),
2243 Direction2::new(Vector2::new(0.0, 1.0), T).unwrap(),
2244 );
2245 let b: PlanarCurve = Circle2d::new(Circle2::new(quarter, 3.0, T).unwrap()).into();
2246 let flipped: PlanarCurve = ogeom_geom::Reversible::reversed(&b);
2247 for other in [b, flipped] {
2248 let found = intersect_curves_2d(&a, &other, CurveCurveOptions::default(), T).unwrap();
2249 assert_eq!(found.overlaps.len(), 1);
2250 let overlap = found.overlaps[0];
2251 for i in 0..=8 {
2252 let t = f64::from(i) / 8.0;
2253 let ta = (overlap.on_a.1 - overlap.on_a.0).mul_add(t, overlap.on_a.0);
2254 let tb = (overlap.on_b.1 - overlap.on_b.0).mul_add(t, overlap.on_b.0);
2255 let pa = a.point_at(ta, T).unwrap();
2256 let pb = other
2257 .point_at(tb.rem_euclid(core::f64::consts::TAU), T)
2258 .unwrap();
2259 assert!(pa.distance(pb) < 1e-9, "at {t}: {pa:?} against {pb:?}");
2260 }
2261 }
2262 }
2263
2264 #[test]
2267 fn collinear_segments_meeting_end_to_end_share_a_point() {
2268 let a = line2(Point2::new(0.0, 0.0), Point2::new(5.0, 0.0));
2269 let b = line2(Point2::new(5.0, 0.0), Point2::new(9.0, 0.0));
2270 let found = intersect_curves_2d(&a, &b, CurveCurveOptions::default(), T).unwrap();
2271 assert!(found.overlaps.is_empty());
2272 assert_eq!(found.crossings.len(), 1);
2273 assert!(found.crossings[0].point.distance(Point2::new(5.0, 0.0)) < 1e-12);
2274
2275 let a: Curve = LineCurve::segment(Point::ORIGIN, Point::new(0.0, 0.0, 5.0), T)
2276 .unwrap()
2277 .into();
2278 let b: Curve = LineCurve::segment(Point::new(0.0, 0.0, 5.0), Point::new(0.0, 0.0, 7.0), T)
2279 .unwrap()
2280 .into();
2281 let found = intersect_curves(&a, &b, CurveCurveOptions::default(), T).unwrap();
2282 assert!(found.overlaps.is_empty());
2283 assert_eq!(found.crossings.len(), 1);
2284 assert!(found.crossings[0].point.distance(Point::new(0.0, 0.0, 5.0)) < 1e-12);
2285 }
2286
2287 #[test]
2294 fn one_circle_written_twice_states_the_correspondence_between_them() {
2295 use ogeom_math::{Direction, Vector};
2296 let a: Curve = CircleCurve::new(Circle::new(Frame::WORLD, 3.0, T).unwrap()).into();
2297 let third = 2.0 * core::f64::consts::PI / 3.0;
2300 let flipped = Frame::new(
2301 Point::ORIGIN,
2302 -Direction::Z,
2303 Direction::new(Vector::new(third.cos(), third.sin(), 0.0), T).unwrap(),
2304 T,
2305 )
2306 .unwrap();
2307 let b: Curve = CircleCurve::new(Circle::new(flipped, 3.0, T).unwrap()).into();
2308 for pair in [(&a, &b), (&b, &a)] {
2309 let found = intersect_curves(pair.0, pair.1, CurveCurveOptions::default(), T).unwrap();
2310 assert!(
2311 found.crossings.is_empty(),
2312 "every point is a hit, so none is"
2313 );
2314 assert_eq!(found.overlaps.len(), 1);
2315 let overlap = &found.overlaps[0];
2316 let span = overlap.on_a.1 - overlap.on_a.0;
2317 for i in 0..=8 {
2318 let t = f64::from(i) / 8.0;
2319 let ta = span.mul_add(t, overlap.on_a.0);
2320 let tb = (overlap.on_b.1 - overlap.on_b.0).mul_add(t, overlap.on_b.0);
2321 let pa = pair.0.point_at(ta, T).unwrap();
2322 let pb = pair
2323 .1
2324 .point_at(tb.rem_euclid(core::f64::consts::TAU), T)
2325 .unwrap();
2326 assert!(
2327 pa.distance(pb) < 1e-9,
2328 "at {t}: {pa:?} against {pb:?} (on_a {:?} on_b {:?})",
2329 overlap.on_a,
2330 overlap.on_b
2331 );
2332 }
2333 }
2334 }
2335
2336 #[test]
2337 fn space_curves_cross_within_a_gap_and_report_it() {
2338 let a: Curve = CircleCurve::new(Circle::new(Frame::WORLD, 2.0, T).unwrap()).into();
2345 let lifted = Frame::new(
2346 Point::new(3.0, 0.0, 0.001),
2347 ogeom_math::Direction::Z,
2348 ogeom_math::Direction::X,
2349 T,
2350 )
2351 .unwrap();
2352 let b: Curve = CircleCurve::new(Circle::new(lifted, 2.0, T).unwrap()).into();
2353
2354 let options = CurveCurveOptions {
2355 gap: 1e-2,
2356 ..CurveCurveOptions::default()
2357 };
2358 let found = intersect_curves(&a, &b, options, T).unwrap();
2359 assert_eq!(found.crossings.len(), 2, "two near-crossings");
2360 for hit in &found.crossings {
2361 assert!(hit.gap > 1e-4, "the gap is real and must not be zeroed");
2362 assert!(hit.gap < 2e-3, "but small: {}", hit.gap);
2363 }
2364
2365 let strict = CurveCurveOptions {
2367 gap: 1e-5,
2368 ..CurveCurveOptions::default()
2369 };
2370 assert!(intersect_curves(&a, &b, strict, T).unwrap().is_empty());
2371 }
2372
2373 #[test]
2379 fn a_line_through_a_figure_eight_s_double_point_is_crossed_twice() {
2380 let ring = [
2381 Point::new(2.0, 0.0, 0.0),
2382 Point::new(1.0, 1.0, 0.0),
2383 Point::new(-1.0, -1.0, 0.0),
2384 Point::new(-2.0, 0.0, 0.0),
2385 Point::new(-1.0, 1.0, 0.0),
2386 Point::new(1.0, -1.0, 0.0),
2387 ];
2388 let a: Curve = ogeom_geom::BSplineCurve::periodic(&ring, 3, T)
2389 .unwrap()
2390 .into();
2391 let b: Curve = LineCurve::segment(Point::new(0.0, 0.0, -1.0), Point::new(0.0, 0.0, 1.0), T)
2392 .unwrap()
2393 .into();
2394 let found = intersect_curves(&a, &b, CurveCurveOptions::default(), T).unwrap();
2395 assert_eq!(found.crossings.len(), 2, "{:?}", found.crossings);
2396 let (lo, hi) = a.domain();
2397 let apart = (found.crossings[0].on_a - found.crossings[1].on_a).abs();
2398 assert!(
2399 (apart - (hi - lo) / 2.0).abs() < 1e-6,
2400 "half a turn apart: {apart}"
2401 );
2402 for hit in &found.crossings {
2403 assert!(hit.point.distance(Point::ORIGIN) < 1e-9, "{:?}", hit.point);
2404 }
2405 }
2406
2407 #[test]
2408 fn skew_lines_in_space_miss_and_close_ones_meet() {
2409 let a: Curve = LineCurve::segment(Point::ORIGIN, Point::new(10.0, 0.0, 0.0), T)
2410 .unwrap()
2411 .into();
2412 let skew: Curve =
2413 LineCurve::segment(Point::new(0.0, -5.0, 1.0), Point::new(0.0, 5.0, 1.0), T)
2414 .unwrap()
2415 .into();
2416 assert!(
2417 intersect_curves(&a, &skew, CurveCurveOptions::default(), T)
2418 .unwrap()
2419 .is_empty(),
2420 "a unit apart is not a crossing"
2421 );
2422
2423 let meeting: Curve =
2424 LineCurve::segment(Point::new(5.0, -5.0, 0.0), Point::new(5.0, 5.0, 0.0), T)
2425 .unwrap()
2426 .into();
2427 let found = intersect_curves(&a, &meeting, CurveCurveOptions::default(), T).unwrap();
2428 assert_eq!(found.crossings.len(), 1);
2429 assert!(
2430 found.crossings[0]
2431 .point
2432 .is_equal(Point::new(5.0, 0.0, 0.0), T)
2433 );
2434 assert!(found.crossings[0].gap < 1e-12);
2435
2436 let collinear: Curve =
2438 LineCurve::segment(Point::new(4.0, 0.0, 0.0), Point::new(20.0, 0.0, 0.0), T)
2439 .unwrap()
2440 .into();
2441 let shared = intersect_curves(&a, &collinear, CurveCurveOptions::default(), T).unwrap();
2442 assert_eq!(shared.overlaps.len(), 1);
2443 }
2444
2445 #[test]
2446 fn a_fitted_curve_tracing_an_arc_is_one_overlap_not_a_row_of_crossings() {
2447 let circle: Curve = CircleCurve::new(Circle::new(Frame::WORLD, 4.0, T).unwrap()).into();
2453 let points: Vec<Point> = (0..=40)
2454 .map(|i| {
2455 let a = 0.2 + 1.0 * f64::from(i) / 40.0;
2456 Point::new(4.0 * a.cos(), 4.0 * a.sin(), 0.0)
2457 })
2458 .collect();
2459 let fitted: Curve = ogeom_geom::fit::fit_points(&points, 3, 1e-7, T)
2460 .unwrap()
2461 .curve
2462 .into();
2463 let options = CurveCurveOptions {
2464 gap: 1e-5,
2465 ..CurveCurveOptions::default()
2466 };
2467 let found = intersect_curves(&fitted, &circle, options, T).unwrap();
2468 assert_eq!(found.overlaps.len(), 1, "one shared stretch: {found:?}");
2469 let (lo, hi) = found.overlaps[0].on_a;
2470 let (fa, fb) = fitted.domain();
2471 assert!(
2472 lo - fa < 1e-3 && fb - hi < 1e-3,
2473 "the whole fit runs along the circle"
2474 );
2475 assert!(
2476 found.crossings.is_empty(),
2477 "no crossing survives inside the overlap: {:?}",
2478 found.crossings
2479 );
2480 }
2481
2482 #[test]
2483 fn an_arc_ending_tangent_to_a_line_is_one_crossing_with_its_reach() {
2484 let circle: Curve = CircleCurve::new(Circle::new(Frame::WORLD, 4.0, T).unwrap()).into();
2489 let line: Curve =
2490 LineCurve::segment(Point::new(4.0, -3.0, 0.0), Point::new(4.0, 3.0, 0.0), T)
2491 .unwrap()
2492 .into();
2493 let options = CurveCurveOptions {
2494 gap: 1e-5,
2495 ..CurveCurveOptions::default()
2496 };
2497 let found = intersect_curves(&circle, &line, options, T).unwrap();
2498 assert_eq!(found.crossings.len(), 1, "one touch: {:?}", found.crossings);
2499 let touch = found.crossings[0];
2500 assert!(
2501 touch.point.distance(Point::new(4.0, 0.0, 0.0)) < 2e-2,
2502 "{touch:?}"
2503 );
2504 assert!(touch.reach < 5e-2, "the valley is short: {touch:?}");
2505 assert!(found.overlaps.is_empty());
2506 }
2507
2508 #[test]
2509 fn unusable_options_are_refused() {
2510 let a = line2(Point2::new(0.0, 0.0), Point2::new(1.0, 0.0));
2511 for options in [
2512 CurveCurveOptions {
2513 samples: 1,
2514 ..CurveCurveOptions::default()
2515 },
2516 CurveCurveOptions {
2517 gap: 0.0,
2518 ..CurveCurveOptions::default()
2519 },
2520 CurveCurveOptions {
2521 gap: f64::NAN,
2522 ..CurveCurveOptions::default()
2523 },
2524 ] {
2525 assert!(intersect_curves_2d(&a, &a.clone(), options, T).is_err());
2526 }
2527 }
2528
2529 #[test]
2534 fn circles_and_ellipses_in_different_planes_meet_where_the_planes_do() {
2535 use ogeom_geom::EllipseCurve;
2536 use ogeom_math::{Direction, Ellipse, Vector};
2537 let radius = 2.0;
2538 let tilted = |normal: Vector, major: Vector, lean: f64| -> Curve {
2539 let frame = Frame::new(
2540 Point::ORIGIN,
2541 Direction::new(normal, T).unwrap(),
2542 Direction::new(major, T).unwrap(),
2543 T,
2544 )
2545 .unwrap();
2546 EllipseCurve::new(Ellipse::new(frame, radius / lean.cos(), radius, T).unwrap()).into()
2547 };
2548 let (p, q) = (0.4_f64, 0.7_f64);
2549 let about_x = tilted(
2550 Vector::new(0.0, -p.sin(), p.cos()),
2551 Vector::new(0.0, p.cos(), p.sin()),
2552 p,
2553 );
2554 let about_y = tilted(
2555 Vector::new(-q.sin(), 0.0, q.cos()),
2556 Vector::new(q.cos(), 0.0, q.sin()),
2557 q,
2558 );
2559 let circle: Curve = CircleCurve::new(Circle::new(Frame::WORLD, radius, T).unwrap()).into();
2560 for (a, b) in [
2561 (&about_x, &about_y),
2562 (&about_y, &about_x),
2563 (&circle, &about_x),
2564 (&about_y, &circle),
2565 ] {
2566 let found = intersect_curves(a, b, CurveCurveOptions::default(), T).unwrap();
2567 assert_eq!(found.crossings.len(), 2, "{found:?}");
2568 for hit in &found.crossings {
2569 let on_a = a.point_at(hit.on_a, T).unwrap();
2570 let on_b = b.point_at(hit.on_b, T).unwrap();
2571 assert!(on_a.distance(on_b) < 1e-9, "{on_a:?} against {on_b:?}");
2572 assert!((on_a.x.hypot(on_a.y) - radius).abs() < 1e-9);
2573 }
2574 }
2575 let lifted = Frame::new(Point::new(0.0, 0.0, 1.0), Direction::Z, Direction::X, T).unwrap();
2576 let above: Curve = CircleCurve::new(Circle::new(lifted, radius, T).unwrap()).into();
2577 assert!(
2578 intersect_curves(&circle, &above, CurveCurveOptions::default(), T)
2579 .unwrap()
2580 .is_empty()
2581 );
2582 }
2583
2584 #[test]
2589 fn circles_in_all_but_one_plane_meet_where_they_pass() {
2590 use ogeom_math::{Direction, Vector};
2591 let flat: Curve = CircleCurve::new(Circle::new(Frame::WORLD, 2.0, T).unwrap()).into();
2592 let lean = 1e-6;
2593 let normal = Direction::new(Vector::new(lean, 0.0, 1.0), T).unwrap();
2594 let centre = Point::new(1.0, 0.0, lean.mul_add(-0.5, 5e-8));
2595 let tilted_frame = Frame::new(
2596 centre,
2597 normal,
2598 Direction::new(Vector::new(1.0, 0.0, -lean), T).unwrap(),
2599 T,
2600 )
2601 .unwrap();
2602 let tilted: Curve = CircleCurve::new(Circle::new(tilted_frame, 2.0, T).unwrap()).into();
2603 let found = intersect_curves(&flat, &tilted, CurveCurveOptions::default(), T).unwrap();
2604 assert_eq!(found.crossings.len(), 2, "{found:?}");
2605 for hit in &found.crossings {
2606 assert!(hit.gap < 1e-7, "{hit:?}");
2607 let p = flat.point_at(hit.on_a, T).unwrap();
2608 assert!((p.x - 0.5).abs() < 1e-4, "{p:?}");
2609 }
2610 }
2611
2612 #[test]
2617 fn lines_meet_circles_and_ellipses_in_closed_form() {
2618 use ogeom_geom::EllipseCurve;
2619 use ogeom_math::Ellipse;
2620 let ellipse: Curve =
2621 EllipseCurve::new(Ellipse::new(Frame::WORLD, 3.0, 2.0, T).unwrap()).into();
2622 let across: Curve =
2623 LineCurve::segment(Point::new(-5.0, 1.0, 0.0), Point::new(5.0, 1.0, 0.0), T)
2624 .unwrap()
2625 .into();
2626 for (a, b, line_first) in [(&ellipse, &across, false), (&across, &ellipse, true)] {
2627 let found = intersect_curves(a, b, CurveCurveOptions::default(), T).unwrap();
2628 assert_eq!(found.crossings.len(), 2, "{found:?}");
2629 for hit in &found.crossings {
2630 let p = a.point_at(hit.on_a, T).unwrap();
2631 let q = b.point_at(hit.on_b, T).unwrap();
2632 assert!(p.distance(q) < 1e-9, "{p:?} against {q:?}");
2633 let on_line = if line_first { p } else { q };
2634 assert!((on_line.y - 1.0).abs() < 1e-12);
2635 }
2636 }
2637 let circle: Curve = CircleCurve::new(Circle::new(Frame::WORLD, 2.0, T).unwrap()).into();
2638 let through: Curve =
2639 LineCurve::segment(Point::new(2.0, 0.0, -1.0), Point::new(2.0, 0.0, 1.0), T)
2640 .unwrap()
2641 .into();
2642 let found = intersect_curves(&circle, &through, CurveCurveOptions::default(), T).unwrap();
2643 assert_eq!(found.crossings.len(), 1);
2644 assert!(found.crossings[0].on_a.abs() < 1e-9);
2645 assert!((found.crossings[0].on_b - 1.0).abs() < 1e-9);
2646 let inside: Curve =
2647 LineCurve::segment(Point::new(1.0, 0.0, -1.0), Point::new(1.0, 0.0, 1.0), T)
2648 .unwrap()
2649 .into();
2650 assert!(
2651 intersect_curves(&circle, &inside, CurveCurveOptions::default(), T)
2652 .unwrap()
2653 .is_empty()
2654 );
2655 }
2656
2657 #[test]
2662 fn arcs_of_one_circle_overlap_across_the_seam() {
2663 use ogeom_geom::{Curve3d as _, TrimmedCurve};
2664 let circle = |x: ogeom_math::Direction, z: ogeom_math::Direction| -> Curve {
2665 let frame = Frame::new(Point::new(10.0, 20.0, 30.0), z, x, T).unwrap();
2666 CircleCurve::new(Circle::new(frame, 50.0, T).unwrap()).into()
2667 };
2668 let trim = |c: &Curve, a: f64, b: f64| -> Curve {
2669 TrimmedCurve::new(c.clone(), a, b, T).unwrap().into()
2670 };
2671 let (x, y) = (ogeom_math::Direction::X, ogeom_math::Direction::Y);
2672 let (up, down) = (ogeom_math::Direction::Z, -ogeom_math::Direction::Z);
2673 let plain = circle(x, up);
2674 let turned = circle(y, up);
2675 let backwards = circle(x, down);
2676 for (a, b, share) in [
2678 (trim(&plain, 5.5, 7.0), trim(&plain, 0.2, 1.0), 0.517 / 1.5),
2679 (trim(&plain, 0.2, 1.0), trim(&plain, 5.5, 7.0), 0.517 / 0.8),
2680 (
2681 trim(&plain, 5.8, 6.8),
2682 trim(&backwards, 5.0, 6.2),
2683 0.4336 / 1.0,
2684 ),
2685 (trim(&plain, 5.0, 7.5), backwards.clone(), 1.0),
2686 (trim(&plain, 0.5, 6.0), trim(&turned, 4.0, 6.0), 1.218 / 5.5),
2687 ] {
2688 let found = intersect_curves(&a, &b, CurveCurveOptions::default(), T).unwrap();
2689 let (wa, wb) = (a.domain(), b.domain());
2690 let mut covered = 0.0;
2691 for o in &found.overlaps {
2692 for (t, w) in [
2693 (o.on_a.0, wa),
2694 (o.on_a.1, wa),
2695 (o.on_b.0, wb),
2696 (o.on_b.1, wb),
2697 ] {
2698 assert!(t >= w.0 - 1e-9 && t <= w.1 + 1e-9, "{t} outside {w:?}");
2699 }
2700 for (ta, tb) in [(o.on_a.0, o.on_b.0), (o.on_a.1, o.on_b.1)] {
2701 let gap = a
2702 .point_at(ta, T)
2703 .unwrap()
2704 .distance(b.point_at(tb, T).unwrap());
2705 assert!(gap < 1e-9, "ends {gap} apart");
2706 }
2707 covered += (o.on_a.1 - o.on_a.0).abs();
2708 }
2709 let want = share * (wa.1 - wa.0);
2710 assert!((covered - want).abs() < 2e-3, "{covered} against {want}");
2711 }
2712 }
2713
2714 #[test]
2718 fn a_trimmed_arc_meets_tangents_as_its_circle_does() {
2719 use ogeom_geom::Trimmed2d;
2720 let options = CurveCurveOptions::default();
2721 let circle = circle2(Point2::new(0.0, 0.0), 10.0);
2722 let arc: PlanarCurve = Trimmed2d::new(circle, 0.3, 2.9, T).unwrap().into();
2723 let tangent = line2(Point2::new(-20.0, 10.0), Point2::new(20.0, 10.0));
2724 let found = intersect_curves_2d(&arc, &tangent, options, T).unwrap();
2725 assert_eq!(found.crossings.len(), 1);
2726 assert!(found.crossings[0].point.is_equal(Point2::new(0.0, 10.0), T));
2727 let grazing = line2(
2728 Point2::new(-20.0, 10.0 - 1e-4),
2729 Point2::new(20.0, 10.0 - 1e-4),
2730 );
2731 let found = intersect_curves_2d(&arc, &grazing, options, T).unwrap();
2732 assert_eq!(found.crossings.len(), 2);
2733 let beside: PlanarCurve =
2734 Trimmed2d::new(circle2(Point2::new(20.0, 0.0), 10.0), 2.0, 4.5, T)
2735 .unwrap()
2736 .into();
2737 let whole = circle2(Point2::new(0.0, 0.0), 10.0);
2738 let found = intersect_curves_2d(&whole, &beside, options, T).unwrap();
2739 assert_eq!(found.crossings.len(), 1);
2740 assert!(found.crossings[0].point.is_equal(Point2::new(10.0, 0.0), T));
2741 let below = line2(Point2::new(-20.0, -10.0), Point2::new(20.0, -10.0));
2743 assert!(
2744 intersect_curves_2d(&arc, &below, options, T)
2745 .unwrap()
2746 .is_empty()
2747 );
2748 }
2749}