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 let apart = span(a).min(span(b)) / f64::from(u32::try_from(options.grid).unwrap_or(1));
212 for (from_a, border_of, other) in [(true, a, b), (false, b, a)] {
213 for (border, at) in spline_borders(border_of, tol) {
214 let Ok(met) = crate::intersect_curve_surface(
215 &border,
216 other,
217 crate::CurveSurfaceOptions::default(),
218 tol,
219 ) else {
220 continue;
221 };
222 for piercing in met.crossings {
223 let on_border = at(piercing.on_curve);
224 let start = if from_a {
225 [
226 on_border.0,
227 on_border.1,
228 piercing.on_surface.0,
229 piercing.on_surface.1,
230 ]
231 } else {
232 [
233 piercing.on_surface.0,
234 piercing.on_surface.1,
235 on_border.0,
236 on_border.1,
237 ]
238 };
239 let Some(contact) = correct(a, b, start, piercing.point, None, tol) else {
240 continue;
241 };
242 if found
243 .iter()
244 .any(|c| c.point.distance(contact.point) <= apart)
245 {
246 continue;
247 }
248 found.push(contact);
249 }
250 }
251 }
252 Ok(found)
253}
254
255type Border = (ogeom_geom::Curve, Box<dyn Fn(f64) -> (f64, f64)>);
258
259fn spline_borders(surface: &SurfaceGeometry, tol: Tolerances) -> Vec<Border> {
261 let SurfaceGeometry::BSpline(spline) = surface else {
262 return Vec::new();
263 };
264 let ((u0, u1), (v0, v1)) = surface.domain();
265 let mut out: Vec<Border> = Vec::new();
266 if !surface.is_closed_u(tol) {
267 for u in [u0, u1] {
268 if let Ok(c) = spline.iso_u_curve(u, tol) {
269 out.push((ogeom_geom::Curve::BSpline(c), Box::new(move |t| (u, t))));
270 }
271 }
272 }
273 if !surface.is_closed_v(tol) {
274 for v in [v0, v1] {
275 if let Ok(c) = spline.iso_v_curve(v, tol) {
276 out.push((ogeom_geom::Curve::BSpline(c), Box::new(move |t| (t, v))));
277 }
278 }
279 }
280 out
281}
282
283pub fn branches(
297 a: &SurfaceGeometry,
298 b: &SurfaceGeometry,
299 options: Marching,
300 tol: Tolerances,
301) -> OgeomResult<Vec<Traced>> {
302 let found = seeds(a, b, options, tol)?;
303 let mut out: Vec<Traced> = Vec::new();
304 for seed in found {
305 let reach = options.chord.max(tol.confusion()) * 8.0;
307 if out
308 .iter()
309 .any(|branch| passes_near(branch, seed.point, reach))
310 {
311 continue;
312 }
313 if let Ok(branch) = trace(a, b, seed, options, tol)
317 && branch.points.len() >= 2
318 && !is_fragment(&branch, options)
319 {
320 let middle = branch.points[branch.points.len() / 2];
323 if out.iter().any(|other| passes_near(other, middle, reach)) {
324 continue;
325 }
326 out.push(branch);
327 }
328 }
329 Ok(stitch_stalled(out, a, b, options, tol))
330}
331
332const BRANCH_POINT_SINE: f64 = 0.05;
336
337fn crossing_sine(
339 a: &SurfaceGeometry,
340 b: &SurfaceGeometry,
341 on_a: (f64, f64),
342 on_b: (f64, f64),
343 tol: Tolerances,
344) -> f64 {
345 let Ok(na) = a.normal_at(on_a.0, on_a.1, tol) else {
346 return 0.0;
347 };
348 let Ok(nb) = b.normal_at(on_b.0, on_b.1, tol) else {
349 return 0.0;
350 };
351 na.vector().cross(nb.vector()).magnitude()
352}
353
354fn is_fragment(branch: &Traced, options: Marching) -> bool {
371 if branch.stopped != Stopped::Stalled {
372 return false;
373 }
374 let length: f64 = branch
375 .points
376 .windows(2)
377 .map(|pair| pair[0].distance(pair[1]))
378 .sum();
379 length < options.chord * 10.0
380}
381
382fn passes_near(branch: &Traced, p: Point, reach: f64) -> bool {
389 branch
390 .points
391 .windows(2)
392 .any(|pair| distance_to_segment(p, pair[0], pair[1]) <= reach)
393}
394
395fn distance_to_segment(p: Point, a: Point, b: Point) -> f64 {
397 let along = b - a;
398 let length = along.square_magnitude();
399 if length <= f64::MIN_POSITIVE {
400 return p.distance(a);
401 }
402 let t = ((p - a).dot(along) / length).clamp(0.0, 1.0);
403 p.distance(a + along * t)
404}
405
406pub fn trace(
414 a: &SurfaceGeometry,
415 b: &SurfaceGeometry,
416 from: Contact,
417 options: Marching,
418 tol: Tolerances,
419) -> OgeomResult<Traced> {
420 options.validate()?;
421 if tangent_at(a, b, from, tol).is_none() {
422 ogeom_bail!(
423 NotDone,
424 "the surfaces are tangent here, so the intersection has no single \
425 direction to follow; that is a branch point and needs the seed \
426 moved off it"
427 );
428 }
429
430 let ahead = walk(a, b, from, 1.0, options, tol)?;
433 if ahead.stopped == Stopped::Closed {
434 return Ok(ahead);
435 }
436 let behind = walk(a, b, from, -1.0, options, tol)?;
437 let last_step = |walked: &[Point]| -> f64 {
438 walked
439 .windows(2)
440 .last()
441 .map_or(0.0, |w| w[0].distance(w[1]))
442 };
443 let steps = last_step(&ahead.points).max(last_step(&behind.points));
444
445 let mut points = behind.points;
447 let mut on_a = behind.on_a;
448 let mut on_b = behind.on_b;
449 points.reverse();
450 on_a.reverse();
451 on_b.reverse();
452 points.pop();
453 on_a.pop();
454 on_b.pop();
455 points.extend(ahead.points);
456 on_a.extend(ahead.on_a);
457 on_b.extend(ahead.on_b);
458
459 let mut stopped = if ahead.stopped == Stopped::RanOut || behind.stopped == Stopped::RanOut {
462 Stopped::RanOut
463 } else if ahead.stopped == Stopped::Stalled || behind.stopped == Stopped::Stalled {
464 Stopped::Stalled
465 } else {
466 Stopped::LeftTheDomain
467 };
468 if stopped == Stopped::LeftTheDomain && points.len() > 3 {
478 let gap = points[0].distance(points[points.len() - 1]);
479 if gap <= (steps * 2.0).max(tol.confusion() * 10.0) {
480 points.push(points[0]);
481 on_a.push(on_a[0]);
482 on_b.push(on_b[0]);
483 stopped = Stopped::Closed;
484 }
485 }
486 Ok(Traced {
487 points,
488 on_a,
489 on_b,
490 stopped,
491 })
492}
493
494struct SurfacePair<'s> {
503 a: &'s SurfaceGeometry,
504 b: &'s SurfaceGeometry,
505}
506
507impl crate::walk::Condition for SurfacePair<'_> {
508 fn unknowns(&self) -> usize {
509 4
510 }
511
512 fn position(&self, x: &[f64], tol: Tolerances) -> Option<Point> {
513 self.a.point_at(x[0], x[1], tol).ok()
514 }
515
516 fn position_gradient(&self, x: &[f64], tol: Tolerances) -> Option<Vec<Vector>> {
517 let (au, av) = self.a.d1_at(x[0], x[1], tol).ok()?;
518 Some(vec![au, av, Vector::ZERO, Vector::ZERO])
521 }
522
523 fn system(&self, x: &[f64], tol: Tolerances) -> Option<(Vec<f64>, Vec<Vec<f64>>)> {
524 let pa = self.a.point_at(x[0], x[1], tol).ok()?;
525 let pb = self.b.point_at(x[2], x[3], tol).ok()?;
526 let (au, av) = self.a.d1_at(x[0], x[1], tol).ok()?;
527 let (bu, bv) = self.b.d1_at(x[2], x[3], tol).ok()?;
528 let gap = pa - pb;
529 Some((
530 vec![gap.x, gap.y, gap.z],
531 vec![
532 vec![au.x, av.x, -bu.x, -bv.x],
533 vec![au.y, av.y, -bu.y, -bv.y],
534 vec![au.z, av.z, -bu.z, -bv.z],
535 ],
536 ))
537 }
538
539 fn clamp(&self, x: &mut [f64]) {
540 let (ua, va) = clamp(self.a, x[0], x[1]);
541 let (ub, vb) = clamp(self.b, x[2], x[3]);
542 x[0] = ua;
543 x[1] = va;
544 x[2] = ub;
545 x[3] = vb;
546 }
547
548 fn outside(&self, x: &[f64], tol: Tolerances) -> bool {
549 outside(self.a, (x[0], x[1]), tol) || outside(self.b, (x[2], x[3]), tol)
550 }
551
552 fn near_edge(&self, x: &[f64]) -> bool {
553 near_edge(self.a, (x[0], x[1])) || near_edge(self.b, (x[2], x[3]))
554 }
555
556 fn extent(&self) -> f64 {
557 span(self.a).max(span(self.b))
558 }
559
560 fn tangent_is_oriented(&self) -> bool {
561 true
564 }
565
566 fn tangent(&self, x: &[f64], tol: Tolerances) -> Option<Vector> {
567 tangent_at(
568 self.a,
569 self.b,
570 Contact {
571 on_a: (x[0], x[1]),
572 on_b: (x[2], x[3]),
573 point: Point::ORIGIN,
574 },
575 tol,
576 )
577 }
578}
579
580fn walk(
582 a: &SurfaceGeometry,
583 b: &SurfaceGeometry,
584 from: Contact,
585 sense: f64,
586 options: Marching,
587 tol: Tolerances,
588) -> OgeomResult<Traced> {
589 let pair = SurfacePair { a, b };
590 let start = [from.on_a.0, from.on_a.1, from.on_b.0, from.on_b.1];
591 let walked = crate::walk::walk_one_way(&pair, &start, sense, options, tol)?;
592 Ok(Traced {
593 on_a: walked.states.iter().map(|x| (x[0], x[1])).collect(),
594 on_b: walked.states.iter().map(|x| (x[2], x[3])).collect(),
595 points: walked.points,
596 stopped: walked.stopped,
597 })
598}
599
600const SHALLOWEST: f64 = 1e-6;
618
619fn tangent_at(
626 a: &SurfaceGeometry,
627 b: &SurfaceGeometry,
628 at: Contact,
629 tol: Tolerances,
630) -> Option<Vector> {
631 let na = normal_at(a, at.on_a, tol)?;
632 let nb = normal_at(b, at.on_b, tol)?;
633 let cross = na.cross(nb);
634 let length = cross.magnitude();
635 let floor = tol.angular().max(SHALLOWEST);
643 let widen = |value: f64| ogeom_math::Interval::about(value, tol.confusion());
644 let (ax, ay, az) = (widen(na.x), widen(na.y), widen(na.z));
645 let (bx, by, bz) = (widen(nb.x), widen(nb.y), widen(nb.z));
646 let cx = ay.mul(&bz).sub(&az.mul(&by));
647 let cy = az.mul(&bx).sub(&ax.mul(&bz));
648 let cz = ax.mul(&by).sub(&ay.mul(&bx));
649 let magnitude2 = cx.square().add(&cy.square()).add(&cz.square());
650 let above = magnitude2.sub(&ogeom_math::Interval::point(floor * floor));
651 if above.certain_sign() != Some(ogeom_core::Sign::Positive) || length <= f64::MIN_POSITIVE {
652 return None;
653 }
654 Some(cross * (1.0 / length))
655}
656
657fn normal_at(surface: &SurfaceGeometry, at: (f64, f64), tol: Tolerances) -> Option<Vector> {
659 let (du, dv) = surface.d1_at(at.0, at.1, tol).ok()?;
660 let cross = du.cross(dv);
661 let length = cross.magnitude();
662 if length <= tol.confusion() {
663 return None;
664 }
665 Some(cross * (1.0 / length))
666}
667
668fn correct(
679 a: &SurfaceGeometry,
680 b: &SurfaceGeometry,
681 start: [f64; 4],
682 guess: Point,
683 constraint: Option<(Point, Vector, f64)>,
684 tol: Tolerances,
685) -> Option<Contact> {
686 let (anchor, along, reach) = match constraint {
687 Some(given) => given,
688 None => {
689 let at = Contact {
693 on_a: (start[0], start[1]),
694 on_b: (start[2], start[3]),
695 point: guess,
696 };
697 (guess, tangent_at(a, b, at, tol).unwrap_or(Vector::X), 0.0)
698 }
699 };
700
701 let system = |x: &[f64]| {
702 let (ua, va) = clamp(a, x[0], x[1]);
703 let (ub, vb) = clamp(b, x[2], x[3]);
704 let pa = a.point_at(ua, va, tol).unwrap_or(Point::ORIGIN);
705 let pb = b.point_at(ub, vb, tol).unwrap_or(Point::ORIGIN);
706 let (au, av) = a.d1_at(ua, va, tol).unwrap_or((Vector::ZERO, Vector::ZERO));
707 let (bu, bv) = b.d1_at(ub, vb, tol).unwrap_or((Vector::ZERO, Vector::ZERO));
708
709 let gap = pa - pb;
710 let residual = vec![gap.x, gap.y, gap.z, (pa - anchor).dot(along) - reach];
711 let jacobian = vec![
712 vec![au.x, av.x, -bu.x, -bv.x],
713 vec![au.y, av.y, -bu.y, -bv.y],
714 vec![au.z, av.z, -bu.z, -bv.z],
715 vec![au.dot(along), av.dot(along), 0.0, 0.0],
716 ];
717 (residual, jacobian)
718 };
719
720 let criteria = solve::Criteria {
721 residual: tol.confusion() * 0.01,
722 step: tol.parametric(),
723 max_iterations: 40,
724 };
725 let found = solve::newton_system(system, &start, criteria).ok()?;
726 if found.residual > tol.confusion() {
727 return None;
728 }
729 let (ua, va) = clamp(a, found.value[0], found.value[1]);
730 let (ub, vb) = clamp(b, found.value[2], found.value[3]);
731 Some(Contact {
732 on_a: (ua, va),
733 on_b: (ub, vb),
734 point: a.point_at(ua, va, tol).ok()?,
735 })
736}
737
738fn clamp(surface: &SurfaceGeometry, u: f64, v: f64) -> (f64, f64) {
743 let ((ua, ub), (va, vb)) = surface.domain();
744 let fold = |x: f64, lo: f64, hi: f64, periodic: bool| {
745 if !periodic {
746 return x.clamp(lo, hi);
747 }
748 let span = hi - lo;
749 if span <= 0.0 {
750 return x;
751 }
752 lo + (x - lo).rem_euclid(span)
753 };
754 (
755 fold(u, ua, ub, surface.is_periodic_u()),
756 fold(v, va, vb, surface.is_periodic_v()),
757 )
758}
759
760fn near_edge(surface: &SurfaceGeometry, at: (f64, f64)) -> bool {
767 let ((ua, ub), (va, vb)) = surface.domain();
768 let close = |x: f64, lo: f64, hi: f64, periodic: bool| {
769 !periodic && {
770 let band = (hi - lo).abs() * 1e-4;
771 x <= lo + band || x >= hi - band
772 }
773 };
774 close(at.0, ua, ub, surface.is_periodic_u()) || close(at.1, va, vb, surface.is_periodic_v())
775}
776
777fn outside(surface: &SurfaceGeometry, at: (f64, f64), tol: Tolerances) -> bool {
779 let ((ua, ub), (va, vb)) = surface.domain();
780 let past = |x: f64, lo: f64, hi: f64, periodic: bool| {
781 !periodic && (x <= lo + tol.parametric() || x >= hi - tol.parametric())
782 };
783 past(at.0, ua, ub, surface.is_periodic_u()) || past(at.1, va, vb, surface.is_periodic_v())
784}
785
786fn span(surface: &SurfaceGeometry) -> f64 {
788 let ((ua, ub), (va, vb)) = surface.domain();
789 let tol = Tolerances::millimetres();
790 let corners = [(ua, va), (ub, va), (ua, vb), (ub, vb)];
791 let mut low = Point::new(f64::MAX, f64::MAX, f64::MAX);
792 let mut high = Point::new(f64::MIN, f64::MIN, f64::MIN);
793 for (u, v) in corners {
794 if let Ok(p) = surface.point_at(u, v, tol) {
795 low = Point::new(low.x.min(p.x), low.y.min(p.y), low.z.min(p.z));
796 high = Point::new(high.x.max(p.x), high.y.max(p.y), high.z.max(p.z));
797 }
798 }
799 let size = (high - low).magnitude();
800 if size.is_finite() && size > 0.0 {
801 size
802 } else {
803 1.0
804 }
805}
806
807pub(crate) struct Cell {
809 pub(crate) corners: [Point; 3],
810 pub(crate) at: (f64, f64),
811 pub(crate) low: Point,
812 pub(crate) high: Point,
813 pub(crate) sag: f64,
816 pub(crate) params: [(f64, f64); 3],
818}
819
820pub(crate) fn sample(surface: &SurfaceGeometry, grid: usize, tol: Tolerances) -> Vec<Cell> {
822 sample_by(surface, (grid, grid), tol)
823}
824
825pub(crate) fn sample_by(
827 surface: &SurfaceGeometry,
828 counts: (usize, usize),
829 tol: Tolerances,
830) -> Vec<Cell> {
831 let ((ua, ub), (va, vb)) = surface.domain();
832 let limit = 1.0e6;
835 let (ua, ub) = (ua.max(-limit), ub.min(limit));
836 let (va, vb) = (va.max(-limit), vb.min(limit));
837
838 let mut out = Vec::new();
839 #[allow(clippy::cast_precision_loss)]
840 let (nu, nv) = (counts.0 as f64, counts.1 as f64);
841 for i in 0..counts.0 {
842 for j in 0..counts.1 {
843 #[allow(clippy::cast_precision_loss)]
844 let (s0, s1) = (i as f64 / nu, (i + 1) as f64 / nu);
845 #[allow(clippy::cast_precision_loss)]
846 let (t0, t1) = (j as f64 / nv, (j + 1) as f64 / nv);
847 let at = |s: f64, t: f64| {
848 let (u, v) = (ua + (ub - ua) * s, va + (vb - va) * t);
849 surface.point_at(u, v, tol).map(|p| ((u, v), p))
850 };
851 let (Ok((p00, a00)), Ok((p10, a10)), Ok((p01, a01)), Ok((p11, a11))) =
852 (at(s0, t0), at(s1, t0), at(s0, t1), at(s1, t1))
853 else {
854 continue;
855 };
856 let sag = at(f64::midpoint(s0, s1), f64::midpoint(t0, t1))
857 .map_or(0.0, |(_, middle)| middle.distance(a00.midpoint(a11)));
858 for (corners, params) in [
859 ([a00, a10, a11], [p00, p10, p11]),
860 ([a00, a11, a01], [p00, p11, p01]),
861 ] {
862 let low = Point::new(
863 corners.iter().map(|p| p.x).fold(f64::MAX, f64::min),
864 corners.iter().map(|p| p.y).fold(f64::MAX, f64::min),
865 corners.iter().map(|p| p.z).fold(f64::MAX, f64::min),
866 );
867 let high = Point::new(
868 corners.iter().map(|p| p.x).fold(f64::MIN, f64::max),
869 corners.iter().map(|p| p.y).fold(f64::MIN, f64::max),
870 corners.iter().map(|p| p.z).fold(f64::MIN, f64::max),
871 );
872 out.push(Cell {
873 corners,
874 at: p00,
875 low,
876 high,
877 sag,
878 params,
879 });
880 }
881 }
882 }
883 out
884}
885
886fn overlap(a: &Cell, b: &Cell, margin: f64) -> bool {
888 a.low.x <= b.high.x + margin
889 && b.low.x <= a.high.x + margin
890 && a.low.y <= b.high.y + margin
891 && b.low.y <= a.high.y + margin
892 && a.low.z <= b.high.z + margin
893 && b.low.z <= a.high.z + margin
894}
895
896fn triangles_cross(a: &Cell, b: &Cell) -> Option<Point> {
902 for (edges, target) in [(a, b), (b, a)] {
903 for k in 0..3 {
904 let (from, to) = (edges.corners[k], edges.corners[(k + 1) % 3]);
905 if let Some(hit) = segment_meets_triangle(from, to, target.corners) {
906 return Some(hit);
907 }
908 }
909 }
910 None
911}
912
913pub(crate) fn segment_meets_triangle(from: Point, to: Point, t: [Point; 3]) -> Option<Point> {
915 let direction = to - from;
916 let (e1, e2) = (t[1] - t[0], t[2] - t[0]);
917 let h = direction.cross(e2);
918 let determinant = e1.dot(h);
919 if determinant.abs() <= f64::MIN_POSITIVE {
920 return None;
921 }
922 let inverse = 1.0 / determinant;
923 let s = from - t[0];
924 let u = inverse * s.dot(h);
925 if !(0.0..=1.0).contains(&u) {
926 return None;
927 }
928 let q = s.cross(e1);
929 let v = inverse * direction.dot(q);
930 if v < 0.0 || u + v > 1.0 {
931 return None;
932 }
933 let along = inverse * e2.dot(q);
934 if !(0.0..=1.0).contains(&along) {
935 return None;
936 }
937 Some(from + direction * along)
938}
939
940struct Arc {
942 points: Vec<Point>,
943 on_a: Vec<(f64, f64)>,
944 on_b: Vec<(f64, f64)>,
945 head_bp: Option<usize>,
947 tail_bp: Option<usize>,
948}
949
950impl Arc {
951 fn length(&self) -> f64 {
952 self.points
953 .windows(2)
954 .map(|pair| pair[0].distance(pair[1]))
955 .sum()
956 }
957
958 fn outgoing(&self, tail: bool) -> Option<Vector> {
961 let n = self.points.len();
962 if n < 2 {
963 return None;
964 }
965 let window = (n - 1).min(24);
966 let (at, back) = if tail {
967 (n - 1, n - 1 - window)
968 } else {
969 (0, window)
970 };
971 let out = self.points[at] - self.points[back];
972 let m = out.magnitude();
973 (m > f64::MIN_POSITIVE).then(|| out / m)
974 }
975}
976
977fn interior_is_transversal(
983 branch: &Traced,
984 a: &SurfaceGeometry,
985 b: &SurfaceGeometry,
986 tol: Tolerances,
987) -> bool {
988 let n = branch.points.len();
989 if n < 5 {
990 return false;
991 }
992 [n / 4, n / 2, 3 * n / 4]
993 .into_iter()
994 .any(|i| crossing_sine(a, b, branch.on_a[i], branch.on_b[i], tol) > BRANCH_POINT_SINE)
995}
996
997fn stitch_stalled(
1016 found: Vec<Traced>,
1017 a: &SurfaceGeometry,
1018 b: &SurfaceGeometry,
1019 options: Marching,
1020 tol: Tolerances,
1021) -> Vec<Traced> {
1022 let reach = options.chord.max(tol.confusion()) * 60.0;
1023 const CONTINUES: f64 = 0.5;
1024
1025 let (candidates, mut out): (Vec<Traced>, Vec<Traced>) = found.into_iter().partition(|branch| {
1026 branch.stopped == Stopped::Stalled && interior_is_transversal(branch, a, b, tol)
1027 });
1028 if candidates.is_empty() {
1029 return out;
1030 }
1031
1032 let mut bps: Vec<Point> = Vec::new();
1034 for branch in &candidates {
1035 let n = branch.points.len();
1036 for at in [0, n - 1] {
1037 if crossing_sine(a, b, branch.on_a[at], branch.on_b[at], tol) < BRANCH_POINT_SINE {
1038 let p = branch.points[at];
1039 if !bps.iter().any(|held| held.distance(p) <= reach) {
1040 bps.push(p);
1041 }
1042 }
1043 }
1044 }
1045 if bps.is_empty() {
1046 out.extend(candidates);
1047 return out;
1048 }
1049 let bp_of =
1050 |p: Point| -> Option<usize> { bps.iter().position(|held| held.distance(p) <= reach) };
1051
1052 let mut arcs: Vec<Arc> = Vec::new();
1056 for branch in &candidates {
1057 let n = branch.points.len();
1058 let mut run_start: Option<usize> = None;
1059 for i in 0..=n {
1060 let near = i < n && bp_of(branch.points[i]).is_some();
1061 match (run_start, near, i == n) {
1062 (None, false, false) => run_start = Some(i),
1063 (Some(s), true, _) | (Some(s), _, true) => {
1064 let e = i;
1065 if e > s + 1 {
1066 let head_bp = if s > 0 {
1067 bp_of(branch.points[s - 1])
1068 } else {
1069 None
1070 };
1071 let tail_bp = if e < n { bp_of(branch.points[e]) } else { None };
1072 arcs.push(Arc {
1073 points: branch.points[s..e].to_vec(),
1074 on_a: branch.on_a[s..e].to_vec(),
1075 on_b: branch.on_b[s..e].to_vec(),
1076 head_bp,
1077 tail_bp,
1078 });
1079 }
1080 run_start = None;
1081 }
1082 _ => {}
1083 }
1084 }
1085 }
1086
1087 arcs.retain(|arc| arc.length() > options.chord * 10.0 && arc.points.len() >= 4);
1090 arcs.sort_by(|x, y| {
1091 y.length()
1092 .partial_cmp(&x.length())
1093 .unwrap_or(core::cmp::Ordering::Equal)
1094 });
1095 let mut kept: Vec<Arc> = Vec::new();
1096 'candidate: for arc in arcs {
1097 let n = arc.points.len();
1098 for probe in [n / 4, n / 2, 3 * n / 4] {
1099 let p = arc.points[probe];
1100 if kept.iter().any(|held| {
1101 held.points
1102 .windows(2)
1103 .any(|pair| distance_to_segment(p, pair[0], pair[1]) <= reach)
1104 }) {
1105 continue 'candidate;
1106 }
1107 }
1108 kept.push(arc);
1109 }
1110
1111 let ends: Vec<(usize, bool, usize, Vector)> = kept
1114 .iter()
1115 .enumerate()
1116 .flat_map(|(i, arc)| {
1117 [(false, arc.head_bp), (true, arc.tail_bp)]
1118 .into_iter()
1119 .filter_map(move |(tail, bp)| Some((i, tail, bp?, arc.outgoing(tail)?)))
1120 })
1121 .collect();
1122 let mut partner: Vec<Option<usize>> = vec![None; ends.len()];
1123 for bp in 0..bps.len() {
1124 loop {
1125 let mut best: Option<(usize, usize, f64)> = None;
1126 for x in 0..ends.len() {
1127 if partner[x].is_some() || ends[x].2 != bp {
1128 continue;
1129 }
1130 for y in (x + 1)..ends.len() {
1131 if partner[y].is_some() || ends[y].2 != bp {
1132 continue;
1133 }
1134 let score = -ends[x].3.dot(ends[y].3);
1135 if score > CONTINUES && best.is_none_or(|(_, _, held)| score > held) {
1136 best = Some((x, y, score));
1137 }
1138 }
1139 }
1140 let Some((x, y, _)) = best else { break };
1141 partner[x] = Some(y);
1142 partner[y] = Some(x);
1143 }
1144 }
1145
1146 let end_index = |arc: usize, tail: bool| -> Option<usize> {
1148 ends.iter().position(|e| e.0 == arc && e.1 == tail)
1149 };
1150 let mut used = vec![false; kept.len()];
1151 for start in 0..kept.len() {
1152 if used[start] {
1153 continue;
1154 }
1155 let mut first = start;
1157 let mut first_reversed = false;
1158 let mut seen_back = vec![false; kept.len()];
1159 loop {
1160 seen_back[first] = true;
1161 let Some(entry) = end_index(first, first_reversed) else {
1164 break;
1165 };
1166 let Some(p) = partner[entry] else { break };
1167 let (prev, prev_tail, _, _) = ends[p];
1168 if seen_back[prev] {
1169 break; }
1171 first = prev;
1172 first_reversed = !prev_tail;
1175 }
1176
1177 let mut points: Vec<Point> = Vec::new();
1179 let mut on_a: Vec<(f64, f64)> = Vec::new();
1180 let mut on_b: Vec<(f64, f64)> = Vec::new();
1181 let mut current = first;
1182 let mut reversed = first_reversed;
1183 let mut closed = false;
1184 loop {
1185 used[current] = true;
1186 let arc = &kept[current];
1187 type Run = (Vec<Point>, Vec<(f64, f64)>, Vec<(f64, f64)>);
1188 let (pts, pa, pb): Run = if reversed {
1189 (
1190 arc.points.iter().rev().copied().collect(),
1191 arc.on_a.iter().rev().copied().collect(),
1192 arc.on_b.iter().rev().copied().collect(),
1193 )
1194 } else {
1195 (arc.points.clone(), arc.on_a.clone(), arc.on_b.clone())
1196 };
1197 if !points.is_empty() {
1199 let joint_bp = if reversed { arc.tail_bp } else { arc.head_bp };
1200 if let Some(bp) = joint_bp {
1201 points.push(bps[bp]);
1202 on_a.push(pa[0]);
1203 on_b.push(pb[0]);
1204 }
1205 }
1206 points.extend(pts);
1207 on_a.extend(pa);
1208 on_b.extend(pb);
1209
1210 let leaving = end_index(current, !reversed);
1211 let Some(l) = leaving else { break };
1212 let Some(p) = partner[l] else { break };
1213 let (next, next_tail, _, _) = ends[p];
1214 if used[next] {
1215 closed = next == first;
1216 break;
1217 }
1218 current = next;
1219 reversed = next_tail;
1220 }
1221 if closed && points.len() > 3 {
1222 let bridge = points[0];
1223 let ba = on_a[0];
1224 let bb = on_b[0];
1225 points.push(bridge);
1226 on_a.push(ba);
1227 on_b.push(bb);
1228 }
1229 out.push(Traced {
1230 points,
1231 on_a,
1232 on_b,
1233 stopped: if closed {
1234 Stopped::Closed
1235 } else {
1236 Stopped::Stalled
1237 },
1238 });
1239 }
1240 out
1241}
1242
1243fn nearest_on(
1246 surface: &SurfaceGeometry,
1247 seed: (f64, f64),
1248 target: Point,
1249 tol: Tolerances,
1250) -> Option<((f64, f64), Point)> {
1251 let (mut u, mut v) = seed;
1252 for _ in 0..16 {
1253 let (u_ok, v_ok) = surface.normalize_parameters(u, v, tol).ok()?;
1254 u = u_ok;
1255 v = v_ok;
1256 let p = surface.point_at(u, v, tol).ok()?;
1257 let (su, sv) = surface.d1_at(u, v, tol).ok()?;
1258 let r = p - target;
1259 let (a11, a12, a22) = (su.dot(su), su.dot(sv), sv.dot(sv));
1260 let det = a11.mul_add(a22, -(a12 * a12));
1261 if det.abs() <= f64::MIN_POSITIVE {
1262 break;
1263 }
1264 let (b1, b2) = (-su.dot(r), -sv.dot(r));
1265 let du = b1.mul_add(a22, -(b2 * a12)) / det;
1266 let dv = a11.mul_add(b2, -(a12 * b1)) / det;
1267 u += du;
1268 v += dv;
1269 if du.hypot(dv) < 1e-14 {
1270 break;
1271 }
1272 }
1273 let (u, v) = surface.normalize_parameters(u, v, tol).ok()?;
1274 Some(((u, v), surface.point_at(u, v, tol).ok()?))
1275}
1276
1277pub fn trace_tangential(
1299 a: &SurfaceGeometry,
1300 b: &SurfaceGeometry,
1301 from: Contact,
1302 options: Marching,
1303 tol: Tolerances,
1304) -> OgeomResult<Traced> {
1305 options.validate()?;
1306 let accept = tol.confusion() * 100.0;
1307 let sine = crossing_sine(a, b, from.on_a, from.on_b, tol);
1308 if sine > BRANCH_POINT_SINE {
1309 ogeom_bail!(
1310 Construction,
1311 "the surfaces cross here at sine {sine}; tangential tracing wants a contact"
1312 );
1313 }
1314 let reach = span(a).max(span(b));
1315 let step = (options.chord * reach)
1316 .sqrt()
1317 .clamp(tol.confusion(), reach / 16.0);
1318
1319 type Walked = (Vec<Point>, Vec<(f64, f64)>, Vec<(f64, f64)>, Stopped);
1320 let walk_one = |sense: f64| -> OgeomResult<Walked> {
1321 let mut points = vec![from.point];
1322 let mut on_a = vec![from.on_a];
1323 let mut on_b = vec![from.on_b];
1324 let mut at = from;
1325 let mut previous: Option<Vector> = None;
1326 let mut stopped = Stopped::RanOut;
1327 while points.len() < options.max_points {
1328 ogeom_core::progress::checkpoint()?;
1329 let Some(normal) = normal_at(a, at.on_a, tol) else {
1334 stopped = Stopped::Stalled;
1335 break;
1336 };
1337 let direction = match previous {
1338 Some(d) => {
1339 let flat = d - normal * d.dot(normal);
1340 let m = flat.magnitude();
1341 if m <= f64::MIN_POSITIVE {
1342 stopped = Stopped::Stalled;
1343 break;
1344 }
1345 flat / m
1346 }
1347 None => {
1348 let (su, _) = a.d1_at(at.on_a.0, at.on_a.1, tol).map_err(|_| {
1351 ogeom_core::ogeom_err!(Construction, "the seed cannot be evaluated")
1352 })?;
1353 let t1 = {
1354 let flat = su - normal * su.dot(normal);
1355 let m = flat.magnitude();
1356 if m <= f64::MIN_POSITIVE {
1357 stopped = Stopped::Stalled;
1358 break;
1359 }
1360 flat / m
1361 };
1362 let t2 = normal.cross(t1);
1363 let mut best = (f64::INFINITY, t1);
1364 for k in 0..16 {
1365 let angle = core::f64::consts::TAU * f64::from(k) / 16.0;
1366 let dir = t1 * angle.cos() + t2 * angle.sin();
1367 let probe = at.point + dir * step;
1368 let Some((_, qa)) = nearest_on(a, at.on_a, probe, tol) else {
1369 continue;
1370 };
1371 let Some((_, qb)) = nearest_on(b, at.on_b, qa, tol) else {
1372 continue;
1373 };
1374 let gap = qa.distance(qb);
1375 if gap < best.0 {
1376 best = (gap, dir);
1377 }
1378 }
1379 best.1 * sense
1380 }
1381 };
1382
1383 let mut candidate = at.point + direction * step;
1386 let mut pa = at.on_a;
1387 let mut pb = at.on_b;
1388 let mut gap = f64::INFINITY;
1389 for _ in 0..8 {
1390 let Some((ua, qa)) = nearest_on(a, pa, candidate, tol) else {
1391 break;
1392 };
1393 let Some((ub, qb)) = nearest_on(b, pb, qa, tol) else {
1394 break;
1395 };
1396 pa = ua;
1397 pb = ub;
1398 gap = qa.distance(qb);
1399 if gap <= tol.confusion() {
1400 candidate = qa;
1401 break;
1402 }
1403 candidate = qa + (qb - qa) * 0.5;
1405 }
1406 if gap > accept {
1407 stopped = Stopped::Stalled;
1408 break;
1409 }
1410 let next = Contact {
1411 on_a: pa,
1412 on_b: pb,
1413 point: candidate,
1414 };
1415 if points.len() > 3 && next.point.distance(from.point) <= step {
1416 points.push(from.point);
1417 on_a.push(from.on_a);
1418 on_b.push(from.on_b);
1419 stopped = Stopped::Closed;
1420 break;
1421 }
1422 if next.point.distance(at.point) <= step * 1e-3 {
1423 stopped = Stopped::Stalled;
1424 break;
1425 }
1426 previous = Some(next.point - at.point);
1427 points.push(next.point);
1428 on_a.push(next.on_a);
1429 on_b.push(next.on_b);
1430 at = next;
1431 }
1432 Ok((points, on_a, on_b, stopped))
1433 };
1434
1435 let (points, on_a, on_b, stopped) = walk_one(1.0)?;
1436 if stopped == Stopped::Closed {
1437 return Ok(Traced {
1438 points,
1439 on_a,
1440 on_b,
1441 stopped,
1442 });
1443 }
1444 let (mut back_points, mut back_a, mut back_b, back_stopped) = walk_one(-1.0)?;
1445 back_points.reverse();
1446 back_a.reverse();
1447 back_b.reverse();
1448 back_points.pop();
1449 back_a.pop();
1450 back_b.pop();
1451 back_points.extend(points);
1452 back_a.extend(on_a);
1453 back_b.extend(on_b);
1454 let stopped = if stopped == Stopped::RanOut || back_stopped == Stopped::RanOut {
1455 Stopped::RanOut
1456 } else if stopped == Stopped::Stalled || back_stopped == Stopped::Stalled {
1457 Stopped::Stalled
1458 } else {
1459 Stopped::LeftTheDomain
1460 };
1461 Ok(Traced {
1462 points: back_points,
1463 on_a: back_a,
1464 on_b: back_b,
1465 stopped,
1466 })
1467}
1468
1469#[cfg(test)]
1470#[allow(clippy::unwrap_used, clippy::print_stdout)]
1471mod tests {
1472 use super::*;
1473 use ogeom_geom::{CylinderSurface, PlaneSurface, SphereSurface};
1474 use ogeom_math::{Cylinder, Direction, Frame, Plane, Sphere};
1475
1476 const T: Tolerances = Tolerances::millimetres();
1477
1478 fn cylinder(origin: Point, axis: Vector, radius: f64, height: (f64, f64)) -> SurfaceGeometry {
1479 let frame = Frame::new(
1480 origin,
1481 Direction::new(axis, T).unwrap(),
1482 Direction::from_cross(axis, Vector::new(0.3, 0.5, 0.9), T).unwrap(),
1483 T,
1484 )
1485 .unwrap();
1486 CylinderSurface::new(Cylinder::new(frame, radius, T).unwrap(), height)
1487 .unwrap()
1488 .into()
1489 }
1490
1491 fn sphere(centre: Point, radius: f64) -> SurfaceGeometry {
1492 SphereSurface::new(Sphere::centred(centre, radius, T).unwrap()).into()
1493 }
1494
1495 fn plane(origin: Point, normal: Vector) -> SurfaceGeometry {
1496 PlaneSurface::over(
1497 Plane::through(origin, Direction::new(normal, T).unwrap()),
1498 (-8.0, 8.0),
1499 (-8.0, 8.0),
1500 )
1501 .unwrap()
1502 .into()
1503 }
1504
1505 fn off(surface: &SurfaceGeometry, p: Point) -> f64 {
1507 match surface {
1508 SurfaceGeometry::Plane(x) => x.plane().distance_to(p),
1509 SurfaceGeometry::Sphere(x) => x.sphere().distance_to(p),
1510 SurfaceGeometry::Cylinder(x) => x.cylinder().distance_to(p),
1511 _ => 0.0,
1512 }
1513 }
1514
1515 fn deviation(a: &SurfaceGeometry, b: &SurfaceGeometry, traced: &Traced) -> f64 {
1517 traced
1518 .points
1519 .iter()
1520 .map(|p| off(a, *p).abs().max(off(b, *p).abs()))
1521 .fold(0.0_f64, f64::max)
1522 }
1523
1524 #[test]
1525 fn a_plane_through_a_bent_strip_seeds_both_branches() {
1526 use ogeom_geom::BSplineSurface;
1532 use ogeom_math::{ControlGrid, KnotVector};
1533 let mut points = Vec::new();
1534 for i in 0..7 {
1535 let a = core::f64::consts::PI * f64::from(i) / 6.0;
1536 for j in 0..2 {
1537 points.push(Point::new(2.0 * a.cos(), f64::from(j), 2.0 * a.sin()));
1538 }
1539 }
1540 let grid = ControlGrid::new(points, 7, 2).unwrap();
1541 let strip: SurfaceGeometry = BSplineSurface::new(
1542 KnotVector::clamped_uniform(3, 7).unwrap(),
1543 KnotVector::clamped_uniform(1, 2).unwrap(),
1544 &grid,
1545 T,
1546 )
1547 .unwrap()
1548 .into();
1549 let level: SurfaceGeometry = PlaneSurface::over(
1550 Plane::through(Point::new(0.0, 0.0, 1.0), Direction::Z),
1551 (-1.0e9, 1.0e9),
1552 (-1.0e9, 1.0e9),
1553 )
1554 .unwrap()
1555 .into();
1556 let options = Marching {
1557 chord: 1e-5,
1558 ..Marching::default()
1559 };
1560 let found = branches(&strip, &level, options, T).unwrap();
1561 assert_eq!(
1562 found.len(),
1563 2,
1564 "the arch crosses the level twice: {}",
1565 found.len()
1566 );
1567 for branch in &found {
1568 assert!(!branch.closed());
1569 for p in &branch.points {
1570 assert!((p.z - 1.0).abs() < 1e-4, "on the level: {p:?}");
1571 }
1572 }
1573 }
1574
1575 #[test]
1576 fn two_crossed_cylinders_are_traced_onto_both_of_them() {
1577 let a = cylinder(Point::ORIGIN, Vector::Z, 1.0, (-4.0, 4.0));
1581 let b = cylinder(Point::ORIGIN, Vector::X, 1.0, (-4.0, 4.0));
1582 let options = Marching {
1583 chord: 1e-5,
1584 ..Marching::default()
1585 };
1586
1587 let found = branches(&a, &b, options, T).unwrap();
1588 assert_eq!(
1589 found.len(),
1590 2,
1591 "two equal cylinders crossing at right angles meet in two closed \
1592 curves: the Steinmetz solid's seams"
1593 );
1594
1595 let mut worst = 0.0_f64;
1596 for branch in &found {
1597 assert!(branch.closed(), "each seam is a closed loop");
1598 assert!(
1599 branch.points.len() > 100,
1600 "a branch of only {} points",
1601 branch.points.len()
1602 );
1603 worst = worst.max(deviation(&a, &b, branch));
1604 }
1605 println!(
1606 "crossed cylinders: {} branches, worst deviation {worst:e}",
1607 found.len()
1608 );
1609 assert!(worst < 1e-7, "traced off the surfaces by {worst:e}");
1610 }
1611
1612 #[test]
1613 fn unequal_crossed_cylinders_meet_in_two_curves_as_well() {
1614 let a = cylinder(Point::ORIGIN, Vector::Z, 1.0, (-4.0, 4.0));
1619 let b = cylinder(Point::ORIGIN, Vector::X, 1.6, (-4.0, 4.0));
1620 let options = Marching {
1621 chord: 1e-5,
1622 ..Marching::default()
1623 };
1624
1625 let found = branches(&a, &b, options, T).unwrap();
1626 assert_eq!(found.len(), 2);
1627 for branch in &found {
1628 assert!(branch.closed());
1629 assert!(deviation(&a, &b, branch) < 1e-7);
1630 }
1631 }
1632
1633 #[test]
1634 fn a_traced_circle_agrees_with_the_circle_it_should_be() {
1635 let s = sphere(Point::ORIGIN, 3.0);
1640 let cut = plane(Point::ORIGIN, Vector::Z);
1641 let options = Marching {
1642 chord: 1e-6,
1643 ..Marching::default()
1644 };
1645
1646 let found = seeds(&s, &cut, options, T).unwrap();
1647 assert!(!found.is_empty());
1648 let branch = trace(&s, &cut, found[0], options, T).unwrap();
1649
1650 assert!(
1651 branch.closed(),
1652 "a plane through a sphere gives a closed loop"
1653 );
1654 for p in &branch.points {
1655 let radius = (p.x * p.x + p.y * p.y).sqrt();
1656 assert!(
1657 (radius - 3.0).abs() < 1e-7,
1658 "a point at radius {radius} on a circle of 3"
1659 );
1660 assert!(p.z.abs() < 1e-7, "off the cutting plane by {}", p.z);
1661 }
1662 }
1663
1664 #[test]
1665 fn a_branch_that_leaves_the_surface_says_so_rather_than_stopping_quietly() {
1666 let s = sphere(Point::ORIGIN, 3.0);
1669 let cut = plane(Point::new(0.0, 0.0, 0.0), Vector::Z);
1670 let options = Marching {
1671 chord: 1e-4,
1672 max_points: 8,
1673 ..Marching::default()
1674 };
1675 let found = seeds(&s, &cut, options, T).unwrap();
1676 let branch = trace(&s, &cut, found[0], options, T).unwrap();
1677 assert_eq!(branch.stopped, Stopped::RanOut);
1678 assert!(!branch.complete(), "a truncated branch is not complete");
1679 }
1680
1681 #[test]
1682 fn tangent_surfaces_are_refused_rather_than_followed_onto_a_guess() {
1683 let s = sphere(Point::new(0.0, 0.0, 3.0), 3.0);
1687 let ground = plane(Point::ORIGIN, Vector::Z);
1688 let touch = Contact {
1689 on_a: (0.0, -core::f64::consts::FRAC_PI_2),
1690 on_b: (0.0, 0.0),
1691 point: Point::ORIGIN,
1692 };
1693 let err = trace(&s, &ground, touch, Marching::default(), T).unwrap_err();
1694 assert!(err.to_string().contains("tangent"), "unexpected: {err}");
1695 }
1696
1697 #[test]
1698 fn the_number_of_branches_is_the_number_there_are() {
1699 let options = Marching {
1703 chord: 1e-5,
1704 ..Marching::default()
1705 };
1706
1707 let one = branches(
1709 &sphere(Point::ORIGIN, 3.0),
1710 &plane(Point::new(0.0, 0.0, 1.0), Vector::Z),
1711 options,
1712 T,
1713 )
1714 .unwrap();
1715 assert_eq!(one.len(), 1, "one plane through a sphere cuts one circle");
1716 assert!(one[0].closed());
1717
1718 let two = branches(
1721 &sphere(Point::ORIGIN, 3.0),
1722 &cylinder(Point::ORIGIN, Vector::Z, 1.5, (-4.0, 4.0)),
1723 options,
1724 T,
1725 )
1726 .unwrap();
1727 assert_eq!(two.len(), 2, "a coaxial cylinder cuts a sphere twice");
1728 for branch in &two {
1729 assert!(branch.closed(), "each is a closed circle");
1730 }
1731 let heights: Vec<f64> = two.iter().map(|b| b.points[0].z).collect();
1733 assert!(
1734 heights[0] * heights[1] < 0.0,
1735 "both branches came back on the same side: {heights:?}"
1736 );
1737 }
1738
1739 #[test]
1740 fn a_branch_thinner_than_the_sampling_is_missed_and_the_knob_finds_it() {
1741 let a = sphere(Point::ORIGIN, 3.0);
1745 let b = sphere(Point::new(5.98, 0.0, 0.0), 3.0);
1746
1747 let coarse = seeds(
1748 &a,
1749 &b,
1750 Marching {
1751 grid: 6,
1752 ..Marching::default()
1753 },
1754 T,
1755 )
1756 .unwrap();
1757 let fine = seeds(
1758 &a,
1759 &b,
1760 Marching {
1761 grid: 120,
1762 ..Marching::default()
1763 },
1764 T,
1765 )
1766 .unwrap();
1767 assert!(
1768 coarse.len() < fine.len(),
1769 "a finer grid should find what a coarse one steps over: {} against {}",
1770 coarse.len(),
1771 fine.len()
1772 );
1773 assert!(!fine.is_empty(), "the branch is there to be found");
1774 }
1775
1776 #[test]
1777 fn settings_that_could_not_work_are_refused() {
1778 let a = sphere(Point::ORIGIN, 1.0);
1779 let b = plane(Point::ORIGIN, Vector::Z);
1780 for options in [
1781 Marching {
1782 chord: 0.0,
1783 ..Marching::default()
1784 },
1785 Marching {
1786 grid: 1,
1787 ..Marching::default()
1788 },
1789 Marching {
1790 max_points: 1,
1791 ..Marching::default()
1792 },
1793 ] {
1794 assert!(seeds(&a, &b, options, T).is_err());
1795 }
1796 }
1797}