1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
45use ogeom_geom::{Surface, SurfaceGeometry};
46use ogeom_math::{Point, Vector, solve};
47
48#[derive(Debug, Clone, Copy, PartialEq)]
50pub struct Marching {
51 pub chord: f64,
53 pub grid: usize,
59 pub max_points: usize,
62}
63
64impl Default for Marching {
65 fn default() -> Self {
66 Self {
67 chord: 1e-4,
68 grid: 24,
69 max_points: 20_000,
70 }
71 }
72}
73
74impl Marching {
75 pub fn validate(&self) -> OgeomResult<()> {
83 if !self.chord.is_finite() || self.chord <= 0.0 {
84 ogeom_bail!(Construction, "a chord of {} is not a distance", self.chord);
85 }
86 if self.grid < 2 {
87 ogeom_bail!(Construction, "a sampling grid needs at least two steps");
88 }
89 if self.max_points < 2 {
90 ogeom_bail!(Construction, "a branch needs at least two points");
91 }
92 Ok(())
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq)]
98pub struct Contact {
99 pub on_a: (f64, f64),
101 pub on_b: (f64, f64),
103 pub point: Point,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum Stopped {
110 Closed,
112 LeftTheDomain,
114 Stalled,
120 RanOut,
125}
126
127#[derive(Debug, Clone, PartialEq)]
129pub struct Traced {
130 pub points: Vec<Point>,
132 pub on_a: Vec<(f64, f64)>,
134 pub on_b: Vec<(f64, f64)>,
136 pub stopped: Stopped,
138}
139
140impl Traced {
141 #[must_use]
143 pub const fn complete(&self) -> bool {
144 !matches!(self.stopped, Stopped::RanOut)
145 }
146
147 #[must_use]
149 pub const fn closed(&self) -> bool {
150 matches!(self.stopped, Stopped::Closed)
151 }
152}
153
154pub fn seeds(
166 a: &SurfaceGeometry,
167 b: &SurfaceGeometry,
168 options: Marching,
169 tol: Tolerances,
170) -> OgeomResult<Vec<Contact>> {
171 options.validate()?;
172 let (mesh_a, mesh_b) = (sample(a, options.grid, tol), sample(b, options.grid, tol));
173
174 let mut found: Vec<Contact> = Vec::new();
175 for cell_a in &mesh_a {
176 for cell_b in &mesh_b {
177 if !overlap(cell_a, cell_b, options.chord) {
180 continue;
181 }
182 let Some(guess) = triangles_cross(cell_a, cell_b) else {
183 continue;
184 };
185 let start = [cell_a.at.0, cell_a.at.1, cell_b.at.0, cell_b.at.1];
186 let Some(contact) = correct(a, b, start, guess, None, tol) else {
187 continue;
188 };
189 let apart = span(a).min(span(b)) / f64::from(u32::try_from(options.grid).unwrap_or(1));
197 if found
198 .iter()
199 .any(|c| c.point.distance(contact.point) <= apart)
200 {
201 continue;
202 }
203 found.push(contact);
204 }
205 }
206 Ok(found)
207}
208
209pub fn branches(
223 a: &SurfaceGeometry,
224 b: &SurfaceGeometry,
225 options: Marching,
226 tol: Tolerances,
227) -> OgeomResult<Vec<Traced>> {
228 let found = seeds(a, b, options, tol)?;
229 let mut out: Vec<Traced> = Vec::new();
230 for seed in found {
231 let reach = options.chord.max(tol.confusion()) * 8.0;
233 if out
234 .iter()
235 .any(|branch| passes_near(branch, seed.point, reach))
236 {
237 continue;
238 }
239 if let Ok(branch) = trace(a, b, seed, options, tol)
243 && branch.points.len() >= 2
244 && !is_fragment(&branch, options)
245 {
246 out.push(branch);
247 }
248 }
249 Ok(stitch_stalled(out, a, b, options, tol))
250}
251
252const BRANCH_POINT_SINE: f64 = 0.05;
256
257fn crossing_sine(
259 a: &SurfaceGeometry,
260 b: &SurfaceGeometry,
261 on_a: (f64, f64),
262 on_b: (f64, f64),
263 tol: Tolerances,
264) -> f64 {
265 let Ok(na) = a.normal_at(on_a.0, on_a.1, tol) else {
266 return 0.0;
267 };
268 let Ok(nb) = b.normal_at(on_b.0, on_b.1, tol) else {
269 return 0.0;
270 };
271 na.vector().cross(nb.vector()).magnitude()
272}
273
274fn is_fragment(branch: &Traced, options: Marching) -> bool {
291 if branch.stopped != Stopped::Stalled {
292 return false;
293 }
294 let length: f64 = branch
295 .points
296 .windows(2)
297 .map(|pair| pair[0].distance(pair[1]))
298 .sum();
299 length < options.chord * 10.0
300}
301
302fn passes_near(branch: &Traced, p: Point, reach: f64) -> bool {
309 branch
310 .points
311 .windows(2)
312 .any(|pair| distance_to_segment(p, pair[0], pair[1]) <= reach)
313}
314
315fn distance_to_segment(p: Point, a: Point, b: Point) -> f64 {
317 let along = b - a;
318 let length = along.square_magnitude();
319 if length <= f64::MIN_POSITIVE {
320 return p.distance(a);
321 }
322 let t = ((p - a).dot(along) / length).clamp(0.0, 1.0);
323 p.distance(a + along * t)
324}
325
326pub fn trace(
334 a: &SurfaceGeometry,
335 b: &SurfaceGeometry,
336 from: Contact,
337 options: Marching,
338 tol: Tolerances,
339) -> OgeomResult<Traced> {
340 options.validate()?;
341 if tangent_at(a, b, from, tol).is_none() {
342 ogeom_bail!(
343 NotDone,
344 "the surfaces are tangent here, so the intersection has no single \
345 direction to follow; that is a branch point and needs the seed \
346 moved off it"
347 );
348 }
349
350 let ahead = walk(a, b, from, 1.0, options, tol)?;
353 if ahead.stopped == Stopped::Closed {
354 return Ok(ahead);
355 }
356 let behind = walk(a, b, from, -1.0, options, tol)?;
357 let last_step = |walked: &[Point]| -> f64 {
358 walked
359 .windows(2)
360 .last()
361 .map_or(0.0, |w| w[0].distance(w[1]))
362 };
363 let steps = last_step(&ahead.points).max(last_step(&behind.points));
364
365 let mut points = behind.points;
367 let mut on_a = behind.on_a;
368 let mut on_b = behind.on_b;
369 points.reverse();
370 on_a.reverse();
371 on_b.reverse();
372 points.pop();
373 on_a.pop();
374 on_b.pop();
375 points.extend(ahead.points);
376 on_a.extend(ahead.on_a);
377 on_b.extend(ahead.on_b);
378
379 let mut stopped = if ahead.stopped == Stopped::RanOut || behind.stopped == Stopped::RanOut {
382 Stopped::RanOut
383 } else if ahead.stopped == Stopped::Stalled || behind.stopped == Stopped::Stalled {
384 Stopped::Stalled
385 } else {
386 Stopped::LeftTheDomain
387 };
388 if stopped == Stopped::LeftTheDomain && points.len() > 3 {
398 let gap = points[0].distance(points[points.len() - 1]);
399 if gap <= (steps * 2.0).max(tol.confusion() * 10.0) {
400 points.push(points[0]);
401 on_a.push(on_a[0]);
402 on_b.push(on_b[0]);
403 stopped = Stopped::Closed;
404 }
405 }
406 Ok(Traced {
407 points,
408 on_a,
409 on_b,
410 stopped,
411 })
412}
413
414struct SurfacePair<'s> {
423 a: &'s SurfaceGeometry,
424 b: &'s SurfaceGeometry,
425}
426
427impl crate::walk::Condition for SurfacePair<'_> {
428 fn unknowns(&self) -> usize {
429 4
430 }
431
432 fn position(&self, x: &[f64], tol: Tolerances) -> Option<Point> {
433 self.a.point_at(x[0], x[1], tol).ok()
434 }
435
436 fn position_gradient(&self, x: &[f64], tol: Tolerances) -> Option<Vec<Vector>> {
437 let (au, av) = self.a.d1_at(x[0], x[1], tol).ok()?;
438 Some(vec![au, av, Vector::ZERO, Vector::ZERO])
441 }
442
443 fn system(&self, x: &[f64], tol: Tolerances) -> Option<(Vec<f64>, Vec<Vec<f64>>)> {
444 let pa = self.a.point_at(x[0], x[1], tol).ok()?;
445 let pb = self.b.point_at(x[2], x[3], tol).ok()?;
446 let (au, av) = self.a.d1_at(x[0], x[1], tol).ok()?;
447 let (bu, bv) = self.b.d1_at(x[2], x[3], tol).ok()?;
448 let gap = pa - pb;
449 Some((
450 vec![gap.x, gap.y, gap.z],
451 vec![
452 vec![au.x, av.x, -bu.x, -bv.x],
453 vec![au.y, av.y, -bu.y, -bv.y],
454 vec![au.z, av.z, -bu.z, -bv.z],
455 ],
456 ))
457 }
458
459 fn clamp(&self, x: &mut [f64]) {
460 let (ua, va) = clamp(self.a, x[0], x[1]);
461 let (ub, vb) = clamp(self.b, x[2], x[3]);
462 x[0] = ua;
463 x[1] = va;
464 x[2] = ub;
465 x[3] = vb;
466 }
467
468 fn outside(&self, x: &[f64], tol: Tolerances) -> bool {
469 outside(self.a, (x[0], x[1]), tol) || outside(self.b, (x[2], x[3]), tol)
470 }
471
472 fn near_edge(&self, x: &[f64]) -> bool {
473 near_edge(self.a, (x[0], x[1])) || near_edge(self.b, (x[2], x[3]))
474 }
475
476 fn extent(&self) -> f64 {
477 span(self.a).max(span(self.b))
478 }
479
480 fn tangent_is_oriented(&self) -> bool {
481 true
484 }
485
486 fn tangent(&self, x: &[f64], tol: Tolerances) -> Option<Vector> {
487 tangent_at(
488 self.a,
489 self.b,
490 Contact {
491 on_a: (x[0], x[1]),
492 on_b: (x[2], x[3]),
493 point: Point::ORIGIN,
494 },
495 tol,
496 )
497 }
498}
499
500fn walk(
502 a: &SurfaceGeometry,
503 b: &SurfaceGeometry,
504 from: Contact,
505 sense: f64,
506 options: Marching,
507 tol: Tolerances,
508) -> OgeomResult<Traced> {
509 let pair = SurfacePair { a, b };
510 let start = [from.on_a.0, from.on_a.1, from.on_b.0, from.on_b.1];
511 let walked = crate::walk::walk_one_way(&pair, &start, sense, options, tol)?;
512 Ok(Traced {
513 on_a: walked.states.iter().map(|x| (x[0], x[1])).collect(),
514 on_b: walked.states.iter().map(|x| (x[2], x[3])).collect(),
515 points: walked.points,
516 stopped: walked.stopped,
517 })
518}
519
520const SHALLOWEST: f64 = 1e-6;
538
539fn tangent_at(
546 a: &SurfaceGeometry,
547 b: &SurfaceGeometry,
548 at: Contact,
549 tol: Tolerances,
550) -> Option<Vector> {
551 let na = normal_at(a, at.on_a, tol)?;
552 let nb = normal_at(b, at.on_b, tol)?;
553 let cross = na.cross(nb);
554 let length = cross.magnitude();
555 let floor = tol.angular().max(SHALLOWEST);
563 let widen = |value: f64| ogeom_math::Interval::about(value, tol.confusion());
564 let (ax, ay, az) = (widen(na.x), widen(na.y), widen(na.z));
565 let (bx, by, bz) = (widen(nb.x), widen(nb.y), widen(nb.z));
566 let cx = ay.mul(&bz).sub(&az.mul(&by));
567 let cy = az.mul(&bx).sub(&ax.mul(&bz));
568 let cz = ax.mul(&by).sub(&ay.mul(&bx));
569 let magnitude2 = cx.square().add(&cy.square()).add(&cz.square());
570 let above = magnitude2.sub(&ogeom_math::Interval::point(floor * floor));
571 if above.certain_sign() != Some(ogeom_core::Sign::Positive) || length <= f64::MIN_POSITIVE {
572 return None;
573 }
574 Some(cross * (1.0 / length))
575}
576
577fn normal_at(surface: &SurfaceGeometry, at: (f64, f64), tol: Tolerances) -> Option<Vector> {
579 let (du, dv) = surface.d1_at(at.0, at.1, tol).ok()?;
580 let cross = du.cross(dv);
581 let length = cross.magnitude();
582 if length <= tol.confusion() {
583 return None;
584 }
585 Some(cross * (1.0 / length))
586}
587
588fn correct(
599 a: &SurfaceGeometry,
600 b: &SurfaceGeometry,
601 start: [f64; 4],
602 guess: Point,
603 constraint: Option<(Point, Vector, f64)>,
604 tol: Tolerances,
605) -> Option<Contact> {
606 let (anchor, along, reach) = match constraint {
607 Some(given) => given,
608 None => {
609 let at = Contact {
613 on_a: (start[0], start[1]),
614 on_b: (start[2], start[3]),
615 point: guess,
616 };
617 (guess, tangent_at(a, b, at, tol).unwrap_or(Vector::X), 0.0)
618 }
619 };
620
621 let system = |x: &[f64]| {
622 let (ua, va) = clamp(a, x[0], x[1]);
623 let (ub, vb) = clamp(b, x[2], x[3]);
624 let pa = a.point_at(ua, va, tol).unwrap_or(Point::ORIGIN);
625 let pb = b.point_at(ub, vb, tol).unwrap_or(Point::ORIGIN);
626 let (au, av) = a.d1_at(ua, va, tol).unwrap_or((Vector::ZERO, Vector::ZERO));
627 let (bu, bv) = b.d1_at(ub, vb, tol).unwrap_or((Vector::ZERO, Vector::ZERO));
628
629 let gap = pa - pb;
630 let residual = vec![gap.x, gap.y, gap.z, (pa - anchor).dot(along) - reach];
631 let jacobian = vec![
632 vec![au.x, av.x, -bu.x, -bv.x],
633 vec![au.y, av.y, -bu.y, -bv.y],
634 vec![au.z, av.z, -bu.z, -bv.z],
635 vec![au.dot(along), av.dot(along), 0.0, 0.0],
636 ];
637 (residual, jacobian)
638 };
639
640 let criteria = solve::Criteria {
641 residual: tol.confusion() * 0.01,
642 step: tol.parametric(),
643 max_iterations: 40,
644 };
645 let found = solve::newton_system(system, &start, criteria).ok()?;
646 if found.residual > tol.confusion() {
647 return None;
648 }
649 let (ua, va) = clamp(a, found.value[0], found.value[1]);
650 let (ub, vb) = clamp(b, found.value[2], found.value[3]);
651 Some(Contact {
652 on_a: (ua, va),
653 on_b: (ub, vb),
654 point: a.point_at(ua, va, tol).ok()?,
655 })
656}
657
658fn clamp(surface: &SurfaceGeometry, u: f64, v: f64) -> (f64, f64) {
663 let ((ua, ub), (va, vb)) = surface.domain();
664 let fold = |x: f64, lo: f64, hi: f64, periodic: bool| {
665 if !periodic {
666 return x.clamp(lo, hi);
667 }
668 let span = hi - lo;
669 if span <= 0.0 {
670 return x;
671 }
672 lo + (x - lo).rem_euclid(span)
673 };
674 (
675 fold(u, ua, ub, surface.is_periodic_u()),
676 fold(v, va, vb, surface.is_periodic_v()),
677 )
678}
679
680fn near_edge(surface: &SurfaceGeometry, at: (f64, f64)) -> bool {
687 let ((ua, ub), (va, vb)) = surface.domain();
688 let close = |x: f64, lo: f64, hi: f64, periodic: bool| {
689 !periodic && {
690 let band = (hi - lo).abs() * 1e-4;
691 x <= lo + band || x >= hi - band
692 }
693 };
694 close(at.0, ua, ub, surface.is_periodic_u()) || close(at.1, va, vb, surface.is_periodic_v())
695}
696
697fn outside(surface: &SurfaceGeometry, at: (f64, f64), tol: Tolerances) -> bool {
699 let ((ua, ub), (va, vb)) = surface.domain();
700 let past = |x: f64, lo: f64, hi: f64, periodic: bool| {
701 !periodic && (x <= lo + tol.parametric() || x >= hi - tol.parametric())
702 };
703 past(at.0, ua, ub, surface.is_periodic_u()) || past(at.1, va, vb, surface.is_periodic_v())
704}
705
706fn span(surface: &SurfaceGeometry) -> f64 {
708 let ((ua, ub), (va, vb)) = surface.domain();
709 let tol = Tolerances::millimetres();
710 let corners = [(ua, va), (ub, va), (ua, vb), (ub, vb)];
711 let mut low = Point::new(f64::MAX, f64::MAX, f64::MAX);
712 let mut high = Point::new(f64::MIN, f64::MIN, f64::MIN);
713 for (u, v) in corners {
714 if let Ok(p) = surface.point_at(u, v, tol) {
715 low = Point::new(low.x.min(p.x), low.y.min(p.y), low.z.min(p.z));
716 high = Point::new(high.x.max(p.x), high.y.max(p.y), high.z.max(p.z));
717 }
718 }
719 let size = (high - low).magnitude();
720 if size.is_finite() && size > 0.0 {
721 size
722 } else {
723 1.0
724 }
725}
726
727pub(crate) struct Cell {
729 pub(crate) corners: [Point; 3],
730 pub(crate) at: (f64, f64),
731 pub(crate) low: Point,
732 pub(crate) high: Point,
733 pub(crate) sag: f64,
736 pub(crate) params: [(f64, f64); 3],
738}
739
740pub(crate) fn sample(surface: &SurfaceGeometry, grid: usize, tol: Tolerances) -> Vec<Cell> {
742 sample_by(surface, (grid, grid), tol)
743}
744
745pub(crate) fn sample_by(
747 surface: &SurfaceGeometry,
748 counts: (usize, usize),
749 tol: Tolerances,
750) -> Vec<Cell> {
751 let ((ua, ub), (va, vb)) = surface.domain();
752 let limit = 1.0e6;
755 let (ua, ub) = (ua.max(-limit), ub.min(limit));
756 let (va, vb) = (va.max(-limit), vb.min(limit));
757
758 let mut out = Vec::new();
759 #[allow(clippy::cast_precision_loss)]
760 let (nu, nv) = (counts.0 as f64, counts.1 as f64);
761 for i in 0..counts.0 {
762 for j in 0..counts.1 {
763 #[allow(clippy::cast_precision_loss)]
764 let (s0, s1) = (i as f64 / nu, (i + 1) as f64 / nu);
765 #[allow(clippy::cast_precision_loss)]
766 let (t0, t1) = (j as f64 / nv, (j + 1) as f64 / nv);
767 let at = |s: f64, t: f64| {
768 let (u, v) = (ua + (ub - ua) * s, va + (vb - va) * t);
769 surface.point_at(u, v, tol).map(|p| ((u, v), p))
770 };
771 let (Ok((p00, a00)), Ok((p10, a10)), Ok((p01, a01)), Ok((p11, a11))) =
772 (at(s0, t0), at(s1, t0), at(s0, t1), at(s1, t1))
773 else {
774 continue;
775 };
776 let sag = at(f64::midpoint(s0, s1), f64::midpoint(t0, t1))
777 .map_or(0.0, |(_, middle)| middle.distance(a00.midpoint(a11)));
778 for (corners, params) in [
779 ([a00, a10, a11], [p00, p10, p11]),
780 ([a00, a11, a01], [p00, p11, p01]),
781 ] {
782 let low = Point::new(
783 corners.iter().map(|p| p.x).fold(f64::MAX, f64::min),
784 corners.iter().map(|p| p.y).fold(f64::MAX, f64::min),
785 corners.iter().map(|p| p.z).fold(f64::MAX, f64::min),
786 );
787 let high = Point::new(
788 corners.iter().map(|p| p.x).fold(f64::MIN, f64::max),
789 corners.iter().map(|p| p.y).fold(f64::MIN, f64::max),
790 corners.iter().map(|p| p.z).fold(f64::MIN, f64::max),
791 );
792 out.push(Cell {
793 corners,
794 at: p00,
795 low,
796 high,
797 sag,
798 params,
799 });
800 }
801 }
802 }
803 out
804}
805
806fn overlap(a: &Cell, b: &Cell, margin: f64) -> bool {
808 a.low.x <= b.high.x + margin
809 && b.low.x <= a.high.x + margin
810 && a.low.y <= b.high.y + margin
811 && b.low.y <= a.high.y + margin
812 && a.low.z <= b.high.z + margin
813 && b.low.z <= a.high.z + margin
814}
815
816fn triangles_cross(a: &Cell, b: &Cell) -> Option<Point> {
822 for (edges, target) in [(a, b), (b, a)] {
823 for k in 0..3 {
824 let (from, to) = (edges.corners[k], edges.corners[(k + 1) % 3]);
825 if let Some(hit) = segment_meets_triangle(from, to, target.corners) {
826 return Some(hit);
827 }
828 }
829 }
830 None
831}
832
833pub(crate) fn segment_meets_triangle(from: Point, to: Point, t: [Point; 3]) -> Option<Point> {
835 let direction = to - from;
836 let (e1, e2) = (t[1] - t[0], t[2] - t[0]);
837 let h = direction.cross(e2);
838 let determinant = e1.dot(h);
839 if determinant.abs() <= f64::MIN_POSITIVE {
840 return None;
841 }
842 let inverse = 1.0 / determinant;
843 let s = from - t[0];
844 let u = inverse * s.dot(h);
845 if !(0.0..=1.0).contains(&u) {
846 return None;
847 }
848 let q = s.cross(e1);
849 let v = inverse * direction.dot(q);
850 if v < 0.0 || u + v > 1.0 {
851 return None;
852 }
853 let along = inverse * e2.dot(q);
854 if !(0.0..=1.0).contains(&along) {
855 return None;
856 }
857 Some(from + direction * along)
858}
859
860struct Arc {
862 points: Vec<Point>,
863 on_a: Vec<(f64, f64)>,
864 on_b: Vec<(f64, f64)>,
865 head_bp: Option<usize>,
867 tail_bp: Option<usize>,
868}
869
870impl Arc {
871 fn length(&self) -> f64 {
872 self.points
873 .windows(2)
874 .map(|pair| pair[0].distance(pair[1]))
875 .sum()
876 }
877
878 fn outgoing(&self, tail: bool) -> Option<Vector> {
881 let n = self.points.len();
882 if n < 2 {
883 return None;
884 }
885 let window = (n - 1).min(24);
886 let (at, back) = if tail {
887 (n - 1, n - 1 - window)
888 } else {
889 (0, window)
890 };
891 let out = self.points[at] - self.points[back];
892 let m = out.magnitude();
893 (m > f64::MIN_POSITIVE).then(|| out / m)
894 }
895}
896
897fn interior_is_transversal(
903 branch: &Traced,
904 a: &SurfaceGeometry,
905 b: &SurfaceGeometry,
906 tol: Tolerances,
907) -> bool {
908 let n = branch.points.len();
909 if n < 5 {
910 return false;
911 }
912 [n / 4, n / 2, 3 * n / 4]
913 .into_iter()
914 .any(|i| crossing_sine(a, b, branch.on_a[i], branch.on_b[i], tol) > BRANCH_POINT_SINE)
915}
916
917fn stitch_stalled(
936 found: Vec<Traced>,
937 a: &SurfaceGeometry,
938 b: &SurfaceGeometry,
939 options: Marching,
940 tol: Tolerances,
941) -> Vec<Traced> {
942 let reach = options.chord.max(tol.confusion()) * 60.0;
943 const CONTINUES: f64 = 0.5;
944
945 let (candidates, mut out): (Vec<Traced>, Vec<Traced>) = found.into_iter().partition(|branch| {
946 branch.stopped == Stopped::Stalled && interior_is_transversal(branch, a, b, tol)
947 });
948 if candidates.is_empty() {
949 return out;
950 }
951
952 let mut bps: Vec<Point> = Vec::new();
954 for branch in &candidates {
955 let n = branch.points.len();
956 for at in [0, n - 1] {
957 if crossing_sine(a, b, branch.on_a[at], branch.on_b[at], tol) < BRANCH_POINT_SINE {
958 let p = branch.points[at];
959 if !bps.iter().any(|held| held.distance(p) <= reach) {
960 bps.push(p);
961 }
962 }
963 }
964 }
965 if bps.is_empty() {
966 out.extend(candidates);
967 return out;
968 }
969 let bp_of =
970 |p: Point| -> Option<usize> { bps.iter().position(|held| held.distance(p) <= reach) };
971
972 let mut arcs: Vec<Arc> = Vec::new();
976 for branch in &candidates {
977 let n = branch.points.len();
978 let mut run_start: Option<usize> = None;
979 for i in 0..=n {
980 let near = i < n && bp_of(branch.points[i]).is_some();
981 match (run_start, near, i == n) {
982 (None, false, false) => run_start = Some(i),
983 (Some(s), true, _) | (Some(s), _, true) => {
984 let e = i;
985 if e > s + 1 {
986 let head_bp = if s > 0 {
987 bp_of(branch.points[s - 1])
988 } else {
989 None
990 };
991 let tail_bp = if e < n { bp_of(branch.points[e]) } else { None };
992 arcs.push(Arc {
993 points: branch.points[s..e].to_vec(),
994 on_a: branch.on_a[s..e].to_vec(),
995 on_b: branch.on_b[s..e].to_vec(),
996 head_bp,
997 tail_bp,
998 });
999 }
1000 run_start = None;
1001 }
1002 _ => {}
1003 }
1004 }
1005 }
1006
1007 arcs.retain(|arc| arc.length() > options.chord * 10.0 && arc.points.len() >= 4);
1010 arcs.sort_by(|x, y| {
1011 y.length()
1012 .partial_cmp(&x.length())
1013 .unwrap_or(core::cmp::Ordering::Equal)
1014 });
1015 let mut kept: Vec<Arc> = Vec::new();
1016 'candidate: for arc in arcs {
1017 let n = arc.points.len();
1018 for probe in [n / 4, n / 2, 3 * n / 4] {
1019 let p = arc.points[probe];
1020 if kept.iter().any(|held| {
1021 held.points
1022 .windows(2)
1023 .any(|pair| distance_to_segment(p, pair[0], pair[1]) <= reach)
1024 }) {
1025 continue 'candidate;
1026 }
1027 }
1028 kept.push(arc);
1029 }
1030
1031 let ends: Vec<(usize, bool, usize, Vector)> = kept
1034 .iter()
1035 .enumerate()
1036 .flat_map(|(i, arc)| {
1037 [(false, arc.head_bp), (true, arc.tail_bp)]
1038 .into_iter()
1039 .filter_map(move |(tail, bp)| Some((i, tail, bp?, arc.outgoing(tail)?)))
1040 })
1041 .collect();
1042 let mut partner: Vec<Option<usize>> = vec![None; ends.len()];
1043 for bp in 0..bps.len() {
1044 loop {
1045 let mut best: Option<(usize, usize, f64)> = None;
1046 for x in 0..ends.len() {
1047 if partner[x].is_some() || ends[x].2 != bp {
1048 continue;
1049 }
1050 for y in (x + 1)..ends.len() {
1051 if partner[y].is_some() || ends[y].2 != bp {
1052 continue;
1053 }
1054 let score = -ends[x].3.dot(ends[y].3);
1055 if score > CONTINUES && best.is_none_or(|(_, _, held)| score > held) {
1056 best = Some((x, y, score));
1057 }
1058 }
1059 }
1060 let Some((x, y, _)) = best else { break };
1061 partner[x] = Some(y);
1062 partner[y] = Some(x);
1063 }
1064 }
1065
1066 let end_index = |arc: usize, tail: bool| -> Option<usize> {
1068 ends.iter().position(|e| e.0 == arc && e.1 == tail)
1069 };
1070 let mut used = vec![false; kept.len()];
1071 for start in 0..kept.len() {
1072 if used[start] {
1073 continue;
1074 }
1075 let mut first = start;
1077 let mut first_reversed = false;
1078 let mut seen_back = vec![false; kept.len()];
1079 loop {
1080 seen_back[first] = true;
1081 let Some(entry) = end_index(first, first_reversed) else {
1084 break;
1085 };
1086 let Some(p) = partner[entry] else { break };
1087 let (prev, prev_tail, _, _) = ends[p];
1088 if seen_back[prev] {
1089 break; }
1091 first = prev;
1092 first_reversed = !prev_tail;
1095 }
1096
1097 let mut points: Vec<Point> = Vec::new();
1099 let mut on_a: Vec<(f64, f64)> = Vec::new();
1100 let mut on_b: Vec<(f64, f64)> = Vec::new();
1101 let mut current = first;
1102 let mut reversed = first_reversed;
1103 let mut closed = false;
1104 loop {
1105 used[current] = true;
1106 let arc = &kept[current];
1107 type Run = (Vec<Point>, Vec<(f64, f64)>, Vec<(f64, f64)>);
1108 let (pts, pa, pb): Run = if reversed {
1109 (
1110 arc.points.iter().rev().copied().collect(),
1111 arc.on_a.iter().rev().copied().collect(),
1112 arc.on_b.iter().rev().copied().collect(),
1113 )
1114 } else {
1115 (arc.points.clone(), arc.on_a.clone(), arc.on_b.clone())
1116 };
1117 if !points.is_empty() {
1119 let joint_bp = if reversed { arc.tail_bp } else { arc.head_bp };
1120 if let Some(bp) = joint_bp {
1121 points.push(bps[bp]);
1122 on_a.push(pa[0]);
1123 on_b.push(pb[0]);
1124 }
1125 }
1126 points.extend(pts);
1127 on_a.extend(pa);
1128 on_b.extend(pb);
1129
1130 let leaving = end_index(current, !reversed);
1131 let Some(l) = leaving else { break };
1132 let Some(p) = partner[l] else { break };
1133 let (next, next_tail, _, _) = ends[p];
1134 if used[next] {
1135 closed = next == first;
1136 break;
1137 }
1138 current = next;
1139 reversed = next_tail;
1140 }
1141 if closed && points.len() > 3 {
1142 let bridge = points[0];
1143 let ba = on_a[0];
1144 let bb = on_b[0];
1145 points.push(bridge);
1146 on_a.push(ba);
1147 on_b.push(bb);
1148 }
1149 out.push(Traced {
1150 points,
1151 on_a,
1152 on_b,
1153 stopped: if closed {
1154 Stopped::Closed
1155 } else {
1156 Stopped::Stalled
1157 },
1158 });
1159 }
1160 out
1161}
1162
1163fn nearest_on(
1166 surface: &SurfaceGeometry,
1167 seed: (f64, f64),
1168 target: Point,
1169 tol: Tolerances,
1170) -> Option<((f64, f64), Point)> {
1171 let (mut u, mut v) = seed;
1172 for _ in 0..16 {
1173 let (u_ok, v_ok) = surface.normalize_parameters(u, v, tol).ok()?;
1174 u = u_ok;
1175 v = v_ok;
1176 let p = surface.point_at(u, v, tol).ok()?;
1177 let (su, sv) = surface.d1_at(u, v, tol).ok()?;
1178 let r = p - target;
1179 let (a11, a12, a22) = (su.dot(su), su.dot(sv), sv.dot(sv));
1180 let det = a11.mul_add(a22, -(a12 * a12));
1181 if det.abs() <= f64::MIN_POSITIVE {
1182 break;
1183 }
1184 let (b1, b2) = (-su.dot(r), -sv.dot(r));
1185 let du = b1.mul_add(a22, -(b2 * a12)) / det;
1186 let dv = a11.mul_add(b2, -(a12 * b1)) / det;
1187 u += du;
1188 v += dv;
1189 if du.hypot(dv) < 1e-14 {
1190 break;
1191 }
1192 }
1193 let (u, v) = surface.normalize_parameters(u, v, tol).ok()?;
1194 Some(((u, v), surface.point_at(u, v, tol).ok()?))
1195}
1196
1197pub fn trace_tangential(
1219 a: &SurfaceGeometry,
1220 b: &SurfaceGeometry,
1221 from: Contact,
1222 options: Marching,
1223 tol: Tolerances,
1224) -> OgeomResult<Traced> {
1225 options.validate()?;
1226 let accept = tol.confusion() * 100.0;
1227 let sine = crossing_sine(a, b, from.on_a, from.on_b, tol);
1228 if sine > BRANCH_POINT_SINE {
1229 ogeom_bail!(
1230 Construction,
1231 "the surfaces cross here at sine {sine}; tangential tracing wants a contact"
1232 );
1233 }
1234 let reach = span(a).max(span(b));
1235 let step = (options.chord * reach)
1236 .sqrt()
1237 .clamp(tol.confusion(), reach / 16.0);
1238
1239 type Walked = (Vec<Point>, Vec<(f64, f64)>, Vec<(f64, f64)>, Stopped);
1240 let walk_one = |sense: f64| -> OgeomResult<Walked> {
1241 let mut points = vec![from.point];
1242 let mut on_a = vec![from.on_a];
1243 let mut on_b = vec![from.on_b];
1244 let mut at = from;
1245 let mut previous: Option<Vector> = None;
1246 let mut stopped = Stopped::RanOut;
1247 while points.len() < options.max_points {
1248 ogeom_core::progress::checkpoint()?;
1249 let Some(normal) = normal_at(a, at.on_a, tol) else {
1254 stopped = Stopped::Stalled;
1255 break;
1256 };
1257 let direction = match previous {
1258 Some(d) => {
1259 let flat = d - normal * d.dot(normal);
1260 let m = flat.magnitude();
1261 if m <= f64::MIN_POSITIVE {
1262 stopped = Stopped::Stalled;
1263 break;
1264 }
1265 flat / m
1266 }
1267 None => {
1268 let (su, _) = a.d1_at(at.on_a.0, at.on_a.1, tol).map_err(|_| {
1271 ogeom_core::ogeom_err!(Construction, "the seed cannot be evaluated")
1272 })?;
1273 let t1 = {
1274 let flat = su - normal * su.dot(normal);
1275 let m = flat.magnitude();
1276 if m <= f64::MIN_POSITIVE {
1277 stopped = Stopped::Stalled;
1278 break;
1279 }
1280 flat / m
1281 };
1282 let t2 = normal.cross(t1);
1283 let mut best = (f64::INFINITY, t1);
1284 for k in 0..16 {
1285 let angle = core::f64::consts::TAU * f64::from(k) / 16.0;
1286 let dir = t1 * angle.cos() + t2 * angle.sin();
1287 let probe = at.point + dir * step;
1288 let Some((_, qa)) = nearest_on(a, at.on_a, probe, tol) else {
1289 continue;
1290 };
1291 let Some((_, qb)) = nearest_on(b, at.on_b, qa, tol) else {
1292 continue;
1293 };
1294 let gap = qa.distance(qb);
1295 if gap < best.0 {
1296 best = (gap, dir);
1297 }
1298 }
1299 best.1 * sense
1300 }
1301 };
1302
1303 let mut candidate = at.point + direction * step;
1306 let mut pa = at.on_a;
1307 let mut pb = at.on_b;
1308 let mut gap = f64::INFINITY;
1309 for _ in 0..8 {
1310 let Some((ua, qa)) = nearest_on(a, pa, candidate, tol) else {
1311 break;
1312 };
1313 let Some((ub, qb)) = nearest_on(b, pb, qa, tol) else {
1314 break;
1315 };
1316 pa = ua;
1317 pb = ub;
1318 gap = qa.distance(qb);
1319 if gap <= tol.confusion() {
1320 candidate = qa;
1321 break;
1322 }
1323 candidate = qa + (qb - qa) * 0.5;
1325 }
1326 if gap > accept {
1327 stopped = Stopped::Stalled;
1328 break;
1329 }
1330 let next = Contact {
1331 on_a: pa,
1332 on_b: pb,
1333 point: candidate,
1334 };
1335 if points.len() > 3 && next.point.distance(from.point) <= step {
1336 points.push(from.point);
1337 on_a.push(from.on_a);
1338 on_b.push(from.on_b);
1339 stopped = Stopped::Closed;
1340 break;
1341 }
1342 if next.point.distance(at.point) <= step * 1e-3 {
1343 stopped = Stopped::Stalled;
1344 break;
1345 }
1346 previous = Some(next.point - at.point);
1347 points.push(next.point);
1348 on_a.push(next.on_a);
1349 on_b.push(next.on_b);
1350 at = next;
1351 }
1352 Ok((points, on_a, on_b, stopped))
1353 };
1354
1355 let (points, on_a, on_b, stopped) = walk_one(1.0)?;
1356 if stopped == Stopped::Closed {
1357 return Ok(Traced {
1358 points,
1359 on_a,
1360 on_b,
1361 stopped,
1362 });
1363 }
1364 let (mut back_points, mut back_a, mut back_b, back_stopped) = walk_one(-1.0)?;
1365 back_points.reverse();
1366 back_a.reverse();
1367 back_b.reverse();
1368 back_points.pop();
1369 back_a.pop();
1370 back_b.pop();
1371 back_points.extend(points);
1372 back_a.extend(on_a);
1373 back_b.extend(on_b);
1374 let stopped = if stopped == Stopped::RanOut || back_stopped == Stopped::RanOut {
1375 Stopped::RanOut
1376 } else if stopped == Stopped::Stalled || back_stopped == Stopped::Stalled {
1377 Stopped::Stalled
1378 } else {
1379 Stopped::LeftTheDomain
1380 };
1381 Ok(Traced {
1382 points: back_points,
1383 on_a: back_a,
1384 on_b: back_b,
1385 stopped,
1386 })
1387}
1388
1389#[cfg(test)]
1390#[allow(clippy::unwrap_used, clippy::print_stdout)]
1391mod tests {
1392 use super::*;
1393 use ogeom_geom::{CylinderSurface, PlaneSurface, SphereSurface};
1394 use ogeom_math::{Cylinder, Direction, Frame, Plane, Sphere};
1395
1396 const T: Tolerances = Tolerances::millimetres();
1397
1398 fn cylinder(origin: Point, axis: Vector, radius: f64, height: (f64, f64)) -> SurfaceGeometry {
1399 let frame = Frame::new(
1400 origin,
1401 Direction::new(axis, T).unwrap(),
1402 Direction::from_cross(axis, Vector::new(0.3, 0.5, 0.9), T).unwrap(),
1403 T,
1404 )
1405 .unwrap();
1406 CylinderSurface::new(Cylinder::new(frame, radius, T).unwrap(), height)
1407 .unwrap()
1408 .into()
1409 }
1410
1411 fn sphere(centre: Point, radius: f64) -> SurfaceGeometry {
1412 SphereSurface::new(Sphere::centred(centre, radius, T).unwrap()).into()
1413 }
1414
1415 fn plane(origin: Point, normal: Vector) -> SurfaceGeometry {
1416 PlaneSurface::over(
1417 Plane::through(origin, Direction::new(normal, T).unwrap()),
1418 (-8.0, 8.0),
1419 (-8.0, 8.0),
1420 )
1421 .unwrap()
1422 .into()
1423 }
1424
1425 fn off(surface: &SurfaceGeometry, p: Point) -> f64 {
1427 match surface {
1428 SurfaceGeometry::Plane(x) => x.plane().distance_to(p),
1429 SurfaceGeometry::Sphere(x) => x.sphere().distance_to(p),
1430 SurfaceGeometry::Cylinder(x) => x.cylinder().distance_to(p),
1431 _ => 0.0,
1432 }
1433 }
1434
1435 fn deviation(a: &SurfaceGeometry, b: &SurfaceGeometry, traced: &Traced) -> f64 {
1437 traced
1438 .points
1439 .iter()
1440 .map(|p| off(a, *p).abs().max(off(b, *p).abs()))
1441 .fold(0.0_f64, f64::max)
1442 }
1443
1444 #[test]
1445 fn a_plane_through_a_bent_strip_seeds_both_branches() {
1446 use ogeom_geom::BSplineSurface;
1452 use ogeom_math::{ControlGrid, KnotVector};
1453 let mut points = Vec::new();
1454 for i in 0..7 {
1455 let a = core::f64::consts::PI * f64::from(i) / 6.0;
1456 for j in 0..2 {
1457 points.push(Point::new(2.0 * a.cos(), f64::from(j), 2.0 * a.sin()));
1458 }
1459 }
1460 let grid = ControlGrid::new(points, 7, 2).unwrap();
1461 let strip: SurfaceGeometry = BSplineSurface::new(
1462 KnotVector::clamped_uniform(3, 7).unwrap(),
1463 KnotVector::clamped_uniform(1, 2).unwrap(),
1464 &grid,
1465 T,
1466 )
1467 .unwrap()
1468 .into();
1469 let level: SurfaceGeometry = PlaneSurface::over(
1470 Plane::through(Point::new(0.0, 0.0, 1.0), Direction::Z),
1471 (-1.0e9, 1.0e9),
1472 (-1.0e9, 1.0e9),
1473 )
1474 .unwrap()
1475 .into();
1476 let options = Marching {
1477 chord: 1e-5,
1478 ..Marching::default()
1479 };
1480 let found = branches(&strip, &level, options, T).unwrap();
1481 assert_eq!(
1482 found.len(),
1483 2,
1484 "the arch crosses the level twice: {}",
1485 found.len()
1486 );
1487 for branch in &found {
1488 assert!(!branch.closed());
1489 for p in &branch.points {
1490 assert!((p.z - 1.0).abs() < 1e-4, "on the level: {p:?}");
1491 }
1492 }
1493 }
1494
1495 #[test]
1496 fn two_crossed_cylinders_are_traced_onto_both_of_them() {
1497 let a = cylinder(Point::ORIGIN, Vector::Z, 1.0, (-4.0, 4.0));
1501 let b = cylinder(Point::ORIGIN, Vector::X, 1.0, (-4.0, 4.0));
1502 let options = Marching {
1503 chord: 1e-5,
1504 ..Marching::default()
1505 };
1506
1507 let found = branches(&a, &b, options, T).unwrap();
1508 assert_eq!(
1509 found.len(),
1510 2,
1511 "two equal cylinders crossing at right angles meet in two closed \
1512 curves: the Steinmetz solid's seams"
1513 );
1514
1515 let mut worst = 0.0_f64;
1516 for branch in &found {
1517 assert!(branch.closed(), "each seam is a closed loop");
1518 assert!(
1519 branch.points.len() > 100,
1520 "a branch of only {} points",
1521 branch.points.len()
1522 );
1523 worst = worst.max(deviation(&a, &b, branch));
1524 }
1525 println!(
1526 "crossed cylinders: {} branches, worst deviation {worst:e}",
1527 found.len()
1528 );
1529 assert!(worst < 1e-7, "traced off the surfaces by {worst:e}");
1530 }
1531
1532 #[test]
1533 fn unequal_crossed_cylinders_meet_in_two_curves_as_well() {
1534 let a = cylinder(Point::ORIGIN, Vector::Z, 1.0, (-4.0, 4.0));
1539 let b = cylinder(Point::ORIGIN, Vector::X, 1.6, (-4.0, 4.0));
1540 let options = Marching {
1541 chord: 1e-5,
1542 ..Marching::default()
1543 };
1544
1545 let found = branches(&a, &b, options, T).unwrap();
1546 assert_eq!(found.len(), 2);
1547 for branch in &found {
1548 assert!(branch.closed());
1549 assert!(deviation(&a, &b, branch) < 1e-7);
1550 }
1551 }
1552
1553 #[test]
1554 fn a_traced_circle_agrees_with_the_circle_it_should_be() {
1555 let s = sphere(Point::ORIGIN, 3.0);
1560 let cut = plane(Point::ORIGIN, Vector::Z);
1561 let options = Marching {
1562 chord: 1e-6,
1563 ..Marching::default()
1564 };
1565
1566 let found = seeds(&s, &cut, options, T).unwrap();
1567 assert!(!found.is_empty());
1568 let branch = trace(&s, &cut, found[0], options, T).unwrap();
1569
1570 assert!(
1571 branch.closed(),
1572 "a plane through a sphere gives a closed loop"
1573 );
1574 for p in &branch.points {
1575 let radius = (p.x * p.x + p.y * p.y).sqrt();
1576 assert!(
1577 (radius - 3.0).abs() < 1e-7,
1578 "a point at radius {radius} on a circle of 3"
1579 );
1580 assert!(p.z.abs() < 1e-7, "off the cutting plane by {}", p.z);
1581 }
1582 }
1583
1584 #[test]
1585 fn a_branch_that_leaves_the_surface_says_so_rather_than_stopping_quietly() {
1586 let s = sphere(Point::ORIGIN, 3.0);
1589 let cut = plane(Point::new(0.0, 0.0, 0.0), Vector::Z);
1590 let options = Marching {
1591 chord: 1e-4,
1592 max_points: 8,
1593 ..Marching::default()
1594 };
1595 let found = seeds(&s, &cut, options, T).unwrap();
1596 let branch = trace(&s, &cut, found[0], options, T).unwrap();
1597 assert_eq!(branch.stopped, Stopped::RanOut);
1598 assert!(!branch.complete(), "a truncated branch is not complete");
1599 }
1600
1601 #[test]
1602 fn tangent_surfaces_are_refused_rather_than_followed_onto_a_guess() {
1603 let s = sphere(Point::new(0.0, 0.0, 3.0), 3.0);
1607 let ground = plane(Point::ORIGIN, Vector::Z);
1608 let touch = Contact {
1609 on_a: (0.0, -core::f64::consts::FRAC_PI_2),
1610 on_b: (0.0, 0.0),
1611 point: Point::ORIGIN,
1612 };
1613 let err = trace(&s, &ground, touch, Marching::default(), T).unwrap_err();
1614 assert!(err.to_string().contains("tangent"), "unexpected: {err}");
1615 }
1616
1617 #[test]
1618 fn the_number_of_branches_is_the_number_there_are() {
1619 let options = Marching {
1623 chord: 1e-5,
1624 ..Marching::default()
1625 };
1626
1627 let one = branches(
1629 &sphere(Point::ORIGIN, 3.0),
1630 &plane(Point::new(0.0, 0.0, 1.0), Vector::Z),
1631 options,
1632 T,
1633 )
1634 .unwrap();
1635 assert_eq!(one.len(), 1, "one plane through a sphere cuts one circle");
1636 assert!(one[0].closed());
1637
1638 let two = branches(
1641 &sphere(Point::ORIGIN, 3.0),
1642 &cylinder(Point::ORIGIN, Vector::Z, 1.5, (-4.0, 4.0)),
1643 options,
1644 T,
1645 )
1646 .unwrap();
1647 assert_eq!(two.len(), 2, "a coaxial cylinder cuts a sphere twice");
1648 for branch in &two {
1649 assert!(branch.closed(), "each is a closed circle");
1650 }
1651 let heights: Vec<f64> = two.iter().map(|b| b.points[0].z).collect();
1653 assert!(
1654 heights[0] * heights[1] < 0.0,
1655 "both branches came back on the same side: {heights:?}"
1656 );
1657 }
1658
1659 #[test]
1660 fn a_branch_thinner_than_the_sampling_is_missed_and_the_knob_finds_it() {
1661 let a = sphere(Point::ORIGIN, 3.0);
1665 let b = sphere(Point::new(5.98, 0.0, 0.0), 3.0);
1666
1667 let coarse = seeds(
1668 &a,
1669 &b,
1670 Marching {
1671 grid: 6,
1672 ..Marching::default()
1673 },
1674 T,
1675 )
1676 .unwrap();
1677 let fine = seeds(
1678 &a,
1679 &b,
1680 Marching {
1681 grid: 120,
1682 ..Marching::default()
1683 },
1684 T,
1685 )
1686 .unwrap();
1687 assert!(
1688 coarse.len() < fine.len(),
1689 "a finer grid should find what a coarse one steps over: {} against {}",
1690 coarse.len(),
1691 fine.len()
1692 );
1693 assert!(!fine.is_empty(), "the branch is there to be found");
1694 }
1695
1696 #[test]
1697 fn settings_that_could_not_work_are_refused() {
1698 let a = sphere(Point::ORIGIN, 1.0);
1699 let b = plane(Point::ORIGIN, Vector::Z);
1700 for options in [
1701 Marching {
1702 chord: 0.0,
1703 ..Marching::default()
1704 },
1705 Marching {
1706 grid: 1,
1707 ..Marching::default()
1708 },
1709 Marching {
1710 max_points: 1,
1711 ..Marching::default()
1712 },
1713 ] {
1714 assert!(seeds(&a, &b, options, T).is_err());
1715 }
1716 }
1717}