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 let apart = span(a).min(span(b)) / f64::from(u32::try_from(options.grid).unwrap_or(1));
174 let near = CellBins::over(&mesh_b, options.chord);
175
176 let mut found: Vec<Contact> = Vec::new();
177 let mut candidates: Vec<usize> = Vec::new();
178 for cell_a in &mesh_a {
179 near.candidates(cell_a, &mut candidates);
184 for &j in &candidates {
185 let cell_b = &mesh_b[j];
186 if !overlap(cell_a, cell_b, options.chord) {
187 continue;
188 }
189 let Some(guess) = triangles_cross(cell_a, cell_b) else {
190 continue;
191 };
192 let start = [cell_a.at.0, cell_a.at.1, cell_b.at.0, cell_b.at.1];
193 let Some(contact) = correct(a, b, start, guess, None, tol) else {
194 continue;
195 };
196 if found
204 .iter()
205 .any(|c| c.point.distance(contact.point) <= apart)
206 {
207 continue;
208 }
209 found.push(contact);
210 }
211 }
212 for (from_a, border_of, other) in [(true, a, b), (false, b, a)] {
218 for (border, at) in spline_borders(border_of, tol) {
219 let Ok(met) = crate::intersect_curve_surface(
220 &border,
221 other,
222 crate::CurveSurfaceOptions::default(),
223 tol,
224 ) else {
225 continue;
226 };
227 for piercing in met.crossings {
228 let on_border = at(piercing.on_curve);
229 let start = if from_a {
230 [
231 on_border.0,
232 on_border.1,
233 piercing.on_surface.0,
234 piercing.on_surface.1,
235 ]
236 } else {
237 [
238 piercing.on_surface.0,
239 piercing.on_surface.1,
240 on_border.0,
241 on_border.1,
242 ]
243 };
244 let Some(contact) = correct(a, b, start, piercing.point, None, tol) else {
245 continue;
246 };
247 if found
248 .iter()
249 .any(|c| c.point.distance(contact.point) <= apart)
250 {
251 continue;
252 }
253 found.push(contact);
254 }
255 }
256 }
257 Ok(found)
258}
259
260type Border = (ogeom_geom::Curve, Box<dyn Fn(f64) -> (f64, f64)>);
263
264fn spline_borders(surface: &SurfaceGeometry, tol: Tolerances) -> Vec<Border> {
266 let SurfaceGeometry::BSpline(spline) = surface else {
267 return Vec::new();
268 };
269 let ((u0, u1), (v0, v1)) = surface.domain();
270 let mut out: Vec<Border> = Vec::new();
271 if !surface.is_closed_u(tol) {
272 for u in [u0, u1] {
273 if let Ok(c) = spline.iso_u_curve(u, tol) {
274 out.push((ogeom_geom::Curve::BSpline(c), Box::new(move |t| (u, t))));
275 }
276 }
277 }
278 if !surface.is_closed_v(tol) {
279 for v in [v0, v1] {
280 if let Ok(c) = spline.iso_v_curve(v, tol) {
281 out.push((ogeom_geom::Curve::BSpline(c), Box::new(move |t| (t, v))));
282 }
283 }
284 }
285 out
286}
287
288pub fn branches(
302 a: &SurfaceGeometry,
303 b: &SurfaceGeometry,
304 options: Marching,
305 tol: Tolerances,
306) -> OgeomResult<Vec<Traced>> {
307 let found = seeds(a, b, options, tol)?;
308 let mut out: Vec<Traced> = Vec::new();
309 for seed in found {
310 let reach = options.chord.max(tol.confusion()) * 8.0;
312 if out
313 .iter()
314 .any(|branch| passes_near(branch, seed.point, reach))
315 {
316 continue;
317 }
318 if let Ok(branch) = trace(a, b, seed, options, tol)
322 && branch.points.len() >= 2
323 && !is_fragment(&branch, options)
324 {
325 let middle = branch.points[branch.points.len() / 2];
328 if out.iter().any(|other| passes_near(other, middle, reach)) {
329 continue;
330 }
331 out.push(branch);
332 }
333 }
334 Ok(stitch_stalled(out, a, b, options, tol))
335}
336
337const BRANCH_POINT_SINE: f64 = 0.05;
341
342fn crossing_sine(
344 a: &SurfaceGeometry,
345 b: &SurfaceGeometry,
346 on_a: (f64, f64),
347 on_b: (f64, f64),
348 tol: Tolerances,
349) -> f64 {
350 let Ok(na) = a.normal_at(on_a.0, on_a.1, tol) else {
351 return 0.0;
352 };
353 let Ok(nb) = b.normal_at(on_b.0, on_b.1, tol) else {
354 return 0.0;
355 };
356 na.vector().cross(nb.vector()).magnitude()
357}
358
359fn is_fragment(branch: &Traced, options: Marching) -> bool {
376 if branch.stopped != Stopped::Stalled {
377 return false;
378 }
379 let length: f64 = branch
380 .points
381 .windows(2)
382 .map(|pair| pair[0].distance(pair[1]))
383 .sum();
384 length < options.chord * 10.0
385}
386
387fn passes_near(branch: &Traced, p: Point, reach: f64) -> bool {
394 branch
395 .points
396 .windows(2)
397 .any(|pair| distance_to_segment(p, pair[0], pair[1]) <= reach)
398}
399
400fn distance_to_segment(p: Point, a: Point, b: Point) -> f64 {
402 let along = b - a;
403 let length = along.square_magnitude();
404 if length <= f64::MIN_POSITIVE {
405 return p.distance(a);
406 }
407 let t = ((p - a).dot(along) / length).clamp(0.0, 1.0);
408 p.distance(a + along * t)
409}
410
411pub fn trace(
419 a: &SurfaceGeometry,
420 b: &SurfaceGeometry,
421 from: Contact,
422 options: Marching,
423 tol: Tolerances,
424) -> OgeomResult<Traced> {
425 options.validate()?;
426 if tangent_at(a, b, from, tol).is_none() {
427 ogeom_bail!(
428 NotDone,
429 "the surfaces are tangent here, so the intersection has no single \
430 direction to follow; that is a branch point and needs the seed \
431 moved off it"
432 );
433 }
434
435 let ahead = walk(a, b, from, 1.0, options, tol)?;
438 if ahead.stopped == Stopped::Closed {
439 return Ok(ahead);
440 }
441 let behind = walk(a, b, from, -1.0, options, tol)?;
442 let last_step = |walked: &[Point]| -> f64 {
443 walked
444 .windows(2)
445 .last()
446 .map_or(0.0, |w| w[0].distance(w[1]))
447 };
448 let steps = last_step(&ahead.points).max(last_step(&behind.points));
449
450 let mut points = behind.points;
452 let mut on_a = behind.on_a;
453 let mut on_b = behind.on_b;
454 points.reverse();
455 on_a.reverse();
456 on_b.reverse();
457 points.pop();
458 on_a.pop();
459 on_b.pop();
460 points.extend(ahead.points);
461 on_a.extend(ahead.on_a);
462 on_b.extend(ahead.on_b);
463
464 let mut stopped = if ahead.stopped == Stopped::RanOut || behind.stopped == Stopped::RanOut {
467 Stopped::RanOut
468 } else if ahead.stopped == Stopped::Stalled || behind.stopped == Stopped::Stalled {
469 Stopped::Stalled
470 } else {
471 Stopped::LeftTheDomain
472 };
473 if stopped == Stopped::LeftTheDomain && points.len() > 3 {
483 let gap = points[0].distance(points[points.len() - 1]);
484 if gap <= (steps * 2.0).max(tol.confusion() * 10.0) {
485 points.push(points[0]);
486 on_a.push(on_a[0]);
487 on_b.push(on_b[0]);
488 stopped = Stopped::Closed;
489 }
490 }
491 Ok(Traced {
492 points,
493 on_a,
494 on_b,
495 stopped,
496 })
497}
498
499struct SurfacePair<'s> {
508 a: &'s SurfaceGeometry,
509 b: &'s SurfaceGeometry,
510}
511
512impl crate::walk::Condition for SurfacePair<'_> {
513 fn unknowns(&self) -> usize {
514 4
515 }
516
517 fn position(&self, x: &[f64], tol: Tolerances) -> Option<Point> {
518 self.a.point_at(x[0], x[1], tol).ok()
519 }
520
521 fn position_gradient(&self, x: &[f64], tol: Tolerances) -> Option<Vec<Vector>> {
522 let (au, av) = self.a.d1_at(x[0], x[1], tol).ok()?;
523 Some(vec![au, av, Vector::ZERO, Vector::ZERO])
526 }
527
528 fn system(&self, x: &[f64], tol: Tolerances) -> Option<(Vec<f64>, Vec<Vec<f64>>)> {
529 Some(self.system_at(x, tol)?.0)
530 }
531
532 fn system_at(
533 &self,
534 x: &[f64],
535 tol: Tolerances,
536 ) -> Option<((Vec<f64>, Vec<Vec<f64>>), Point, Vec<Vector>)> {
537 let pa = self.a.point_at(x[0], x[1], tol).ok()?;
538 let pb = self.b.point_at(x[2], x[3], tol).ok()?;
539 let (au, av) = self.a.d1_at(x[0], x[1], tol).ok()?;
540 let (bu, bv) = self.b.d1_at(x[2], x[3], tol).ok()?;
541 let gap = pa - pb;
542 Some((
543 (
544 vec![gap.x, gap.y, gap.z],
545 vec![
546 vec![au.x, av.x, -bu.x, -bv.x],
547 vec![au.y, av.y, -bu.y, -bv.y],
548 vec![au.z, av.z, -bu.z, -bv.z],
549 ],
550 ),
551 pa,
552 vec![au, av, Vector::ZERO, Vector::ZERO],
553 ))
554 }
555
556 fn clamp(&self, x: &mut [f64]) {
557 let (ua, va) = clamp(self.a, x[0], x[1]);
558 let (ub, vb) = clamp(self.b, x[2], x[3]);
559 x[0] = ua;
560 x[1] = va;
561 x[2] = ub;
562 x[3] = vb;
563 }
564
565 fn outside(&self, x: &[f64], tol: Tolerances) -> bool {
566 outside(self.a, (x[0], x[1]), tol) || outside(self.b, (x[2], x[3]), tol)
567 }
568
569 fn near_edge(&self, x: &[f64]) -> bool {
570 near_edge(self.a, (x[0], x[1])) || near_edge(self.b, (x[2], x[3]))
571 }
572
573 fn extent(&self) -> f64 {
574 span(self.a).max(span(self.b))
575 }
576
577 fn tangent_is_oriented(&self) -> bool {
578 true
581 }
582
583 fn tangent(&self, x: &[f64], tol: Tolerances) -> Option<Vector> {
584 tangent_at(
585 self.a,
586 self.b,
587 Contact {
588 on_a: (x[0], x[1]),
589 on_b: (x[2], x[3]),
590 point: Point::ORIGIN,
591 },
592 tol,
593 )
594 }
595}
596
597fn walk(
599 a: &SurfaceGeometry,
600 b: &SurfaceGeometry,
601 from: Contact,
602 sense: f64,
603 options: Marching,
604 tol: Tolerances,
605) -> OgeomResult<Traced> {
606 let pair = SurfacePair { a, b };
607 let start = [from.on_a.0, from.on_a.1, from.on_b.0, from.on_b.1];
608 let walked = crate::walk::walk_one_way(&pair, &start, sense, options, tol)?;
609 Ok(Traced {
610 on_a: walked.states.iter().map(|x| (x[0], x[1])).collect(),
611 on_b: walked.states.iter().map(|x| (x[2], x[3])).collect(),
612 points: walked.points,
613 stopped: walked.stopped,
614 })
615}
616
617const SHALLOWEST: f64 = 1e-6;
635
636fn tangent_at(
643 a: &SurfaceGeometry,
644 b: &SurfaceGeometry,
645 at: Contact,
646 tol: Tolerances,
647) -> Option<Vector> {
648 let na = normal_at(a, at.on_a, tol)?;
649 let nb = normal_at(b, at.on_b, tol)?;
650 let cross = na.cross(nb);
651 let length = cross.magnitude();
652 let floor = tol.angular().max(SHALLOWEST);
660 let widen = |value: f64| ogeom_math::Interval::about(value, tol.confusion());
661 let (ax, ay, az) = (widen(na.x), widen(na.y), widen(na.z));
662 let (bx, by, bz) = (widen(nb.x), widen(nb.y), widen(nb.z));
663 let cx = ay.mul(&bz).sub(&az.mul(&by));
664 let cy = az.mul(&bx).sub(&ax.mul(&bz));
665 let cz = ax.mul(&by).sub(&ay.mul(&bx));
666 let magnitude2 = cx.square().add(&cy.square()).add(&cz.square());
667 let above = magnitude2.sub(&ogeom_math::Interval::point(floor * floor));
668 if above.certain_sign() != Some(ogeom_core::Sign::Positive) || length <= f64::MIN_POSITIVE {
669 return None;
670 }
671 Some(cross * (1.0 / length))
672}
673
674fn normal_at(surface: &SurfaceGeometry, at: (f64, f64), tol: Tolerances) -> Option<Vector> {
676 let (du, dv) = surface.d1_at(at.0, at.1, tol).ok()?;
677 let cross = du.cross(dv);
678 let length = cross.magnitude();
679 if length <= tol.confusion() {
680 return None;
681 }
682 Some(cross * (1.0 / length))
683}
684
685fn correct(
696 a: &SurfaceGeometry,
697 b: &SurfaceGeometry,
698 start: [f64; 4],
699 guess: Point,
700 constraint: Option<(Point, Vector, f64)>,
701 tol: Tolerances,
702) -> Option<Contact> {
703 let (anchor, along, reach) = match constraint {
704 Some(given) => given,
705 None => {
706 let at = Contact {
710 on_a: (start[0], start[1]),
711 on_b: (start[2], start[3]),
712 point: guess,
713 };
714 (guess, tangent_at(a, b, at, tol).unwrap_or(Vector::X), 0.0)
715 }
716 };
717
718 let system = |x: &[f64; 4]| {
719 let (ua, va) = clamp(a, x[0], x[1]);
720 let (ub, vb) = clamp(b, x[2], x[3]);
721 let (Ok(pa), Ok(pb), Ok((au, av)), Ok((bu, bv))) = (
725 a.point_at(ua, va, tol),
726 b.point_at(ub, vb, tol),
727 a.d1_at(ua, va, tol),
728 b.d1_at(ub, vb, tol),
729 ) else {
730 return ([f64::INFINITY; 4], [[0.0; 4]; 4]);
731 };
732
733 let gap = pa - pb;
734 let residual = [gap.x, gap.y, gap.z, (pa - anchor).dot(along) - reach];
735 let jacobian = [
736 [au.x, av.x, -bu.x, -bv.x],
737 [au.y, av.y, -bu.y, -bv.y],
738 [au.z, av.z, -bu.z, -bv.z],
739 [au.dot(along), av.dot(along), 0.0, 0.0],
740 ];
741 (residual, jacobian)
742 };
743
744 let criteria = solve::Criteria {
745 residual: tol.confusion() * 0.01,
746 step: tol.parametric(),
747 max_iterations: 40,
748 };
749 let found = solve::newton_system_fixed(system, start, criteria).ok()?;
750 if found.1 > tol.confusion() {
751 return None;
752 }
753 let (ua, va) = clamp(a, found.0[0], found.0[1]);
754 let (ub, vb) = clamp(b, found.0[2], found.0[3]);
755 Some(Contact {
756 on_a: (ua, va),
757 on_b: (ub, vb),
758 point: a.point_at(ua, va, tol).ok()?,
759 })
760}
761
762fn clamp(surface: &SurfaceGeometry, u: f64, v: f64) -> (f64, f64) {
767 let ((ua, ub), (va, vb)) = surface.domain();
768 let fold = |x: f64, lo: f64, hi: f64, periodic: bool| {
769 if !periodic {
770 return x.clamp(lo, hi);
771 }
772 let span = hi - lo;
773 if span <= 0.0 {
774 return x;
775 }
776 lo + (x - lo).rem_euclid(span)
777 };
778 (
779 fold(u, ua, ub, surface.is_periodic_u()),
780 fold(v, va, vb, surface.is_periodic_v()),
781 )
782}
783
784fn near_edge(surface: &SurfaceGeometry, at: (f64, f64)) -> bool {
791 let ((ua, ub), (va, vb)) = surface.domain();
792 let close = |x: f64, lo: f64, hi: f64, periodic: bool| {
793 !periodic && {
794 let band = (hi - lo).abs() * 1e-4;
795 x <= lo + band || x >= hi - band
796 }
797 };
798 close(at.0, ua, ub, surface.is_periodic_u()) || close(at.1, va, vb, surface.is_periodic_v())
799}
800
801fn outside(surface: &SurfaceGeometry, at: (f64, f64), tol: Tolerances) -> bool {
803 let ((ua, ub), (va, vb)) = surface.domain();
804 let past = |x: f64, lo: f64, hi: f64, periodic: bool| {
805 !periodic && (x <= lo + tol.parametric() || x >= hi - tol.parametric())
806 };
807 past(at.0, ua, ub, surface.is_periodic_u()) || past(at.1, va, vb, surface.is_periodic_v())
808}
809
810fn span(surface: &SurfaceGeometry) -> f64 {
812 let ((ua, ub), (va, vb)) = surface.domain();
813 let tol = Tolerances::millimetres();
814 let corners = [(ua, va), (ub, va), (ua, vb), (ub, vb)];
815 let mut low = Point::new(f64::MAX, f64::MAX, f64::MAX);
816 let mut high = Point::new(f64::MIN, f64::MIN, f64::MIN);
817 for (u, v) in corners {
818 if let Ok(p) = surface.point_at(u, v, tol) {
819 low = Point::new(low.x.min(p.x), low.y.min(p.y), low.z.min(p.z));
820 high = Point::new(high.x.max(p.x), high.y.max(p.y), high.z.max(p.z));
821 }
822 }
823 let size = (high - low).magnitude();
824 if size.is_finite() && size > 0.0 {
825 size
826 } else {
827 1.0
828 }
829}
830
831pub(crate) struct Cell {
833 pub(crate) corners: [Point; 3],
834 pub(crate) at: (f64, f64),
835 pub(crate) low: Point,
836 pub(crate) high: Point,
837 pub(crate) sag: f64,
840 pub(crate) params: [(f64, f64); 3],
842}
843
844pub(crate) fn sample(surface: &SurfaceGeometry, grid: usize, tol: Tolerances) -> Vec<Cell> {
846 sample_by(surface, (grid, grid), tol)
847}
848
849pub(crate) fn sample_by(
851 surface: &SurfaceGeometry,
852 counts: (usize, usize),
853 tol: Tolerances,
854) -> Vec<Cell> {
855 let ((ua, ub), (va, vb)) = surface.domain();
856 let limit = 1.0e6;
859 let (ua, ub) = (ua.max(-limit), ub.min(limit));
860 let (va, vb) = (va.max(-limit), vb.min(limit));
861
862 let mut out = Vec::new();
863 #[allow(clippy::cast_precision_loss)]
864 let (nu, nv) = (counts.0 as f64, counts.1 as f64);
865 for i in 0..counts.0 {
866 for j in 0..counts.1 {
867 #[allow(clippy::cast_precision_loss)]
868 let (s0, s1) = (i as f64 / nu, (i + 1) as f64 / nu);
869 #[allow(clippy::cast_precision_loss)]
870 let (t0, t1) = (j as f64 / nv, (j + 1) as f64 / nv);
871 let at = |s: f64, t: f64| {
872 let (u, v) = (ua + (ub - ua) * s, va + (vb - va) * t);
873 surface.point_at(u, v, tol).map(|p| ((u, v), p))
874 };
875 let (Ok((p00, a00)), Ok((p10, a10)), Ok((p01, a01)), Ok((p11, a11))) =
876 (at(s0, t0), at(s1, t0), at(s0, t1), at(s1, t1))
877 else {
878 continue;
879 };
880 let sag = at(f64::midpoint(s0, s1), f64::midpoint(t0, t1))
881 .map_or(0.0, |(_, middle)| middle.distance(a00.midpoint(a11)));
882 for (corners, params) in [
883 ([a00, a10, a11], [p00, p10, p11]),
884 ([a00, a11, a01], [p00, p11, p01]),
885 ] {
886 let low = Point::new(
887 corners.iter().map(|p| p.x).fold(f64::MAX, f64::min),
888 corners.iter().map(|p| p.y).fold(f64::MAX, f64::min),
889 corners.iter().map(|p| p.z).fold(f64::MAX, f64::min),
890 );
891 let high = Point::new(
892 corners.iter().map(|p| p.x).fold(f64::MIN, f64::max),
893 corners.iter().map(|p| p.y).fold(f64::MIN, f64::max),
894 corners.iter().map(|p| p.z).fold(f64::MIN, f64::max),
895 );
896 out.push(Cell {
897 corners,
898 at: p00,
899 low,
900 high,
901 sag,
902 params,
903 });
904 }
905 }
906 }
907 out
908}
909
910struct CellBins {
914 low: Point,
915 size: f64,
916 counts: [usize; 3],
917 bins: Vec<Vec<usize>>,
918 margin: f64,
919}
920
921impl CellBins {
922 const MOST: usize = 48;
925
926 fn over(cells: &[Cell], margin: f64) -> Self {
927 let mut low = Point::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
928 let mut high = Point::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
929 for c in cells {
930 low = Point::new(low.x.min(c.low.x), low.y.min(c.low.y), low.z.min(c.low.z));
931 high = Point::new(
932 high.x.max(c.high.x),
933 high.y.max(c.high.y),
934 high.z.max(c.high.z),
935 );
936 }
937 let extent = (high - low).magnitude();
938 #[allow(clippy::cast_precision_loss)]
939 let size = if extent.is_finite() && extent > 0.0 {
940 (extent / Self::MOST as f64).max(margin)
941 } else {
942 1.0
943 };
944 let count = |lo: f64, hi: f64| -> usize {
945 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
946 let n = ((hi - lo) / size).floor() as usize + 1;
947 n.clamp(1, Self::MOST + 1)
948 };
949 let counts = if extent.is_finite() {
950 [
951 count(low.x, high.x),
952 count(low.y, high.y),
953 count(low.z, high.z),
954 ]
955 } else {
956 [1, 1, 1]
957 };
958 let mut bins = vec![Vec::new(); counts[0] * counts[1] * counts[2]];
959 let mut this = Self {
960 low,
961 size,
962 counts,
963 bins: Vec::new(),
964 margin,
965 };
966 for (i, c) in cells.iter().enumerate() {
967 this.each_bin(c.low, c.high, |k| bins[k].push(i));
968 }
969 this.bins = bins;
970 this
971 }
972
973 fn each_bin(&self, low: Point, high: Point, mut visit: impl FnMut(usize)) {
975 let index = |x: f64, lo: f64, n: usize| -> usize {
976 if !x.is_finite() {
977 return if x > 0.0 { n - 1 } else { 0 };
978 }
979 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
980 let k = ((x - lo) / self.size).floor().max(0.0) as usize;
981 k.min(n - 1)
982 };
983 let m = self.margin;
984 let [nx, ny, nz] = self.counts;
985 let (x0, x1) = (
986 index(low.x - m, self.low.x, nx),
987 index(high.x + m, self.low.x, nx),
988 );
989 let (y0, y1) = (
990 index(low.y - m, self.low.y, ny),
991 index(high.y + m, self.low.y, ny),
992 );
993 let (z0, z1) = (
994 index(low.z - m, self.low.z, nz),
995 index(high.z + m, self.low.z, nz),
996 );
997 for x in x0..=x1 {
998 for y in y0..=y1 {
999 for z in z0..=z1 {
1000 visit((x * ny + y) * nz + z);
1001 }
1002 }
1003 }
1004 }
1005
1006 fn candidates(&self, cell: &Cell, out: &mut Vec<usize>) {
1008 out.clear();
1009 self.each_bin(cell.low, cell.high, |k| {
1010 out.extend_from_slice(&self.bins[k])
1011 });
1012 out.sort_unstable();
1013 out.dedup();
1014 }
1015}
1016
1017fn overlap(a: &Cell, b: &Cell, margin: f64) -> bool {
1018 a.low.x <= b.high.x + margin
1019 && b.low.x <= a.high.x + margin
1020 && a.low.y <= b.high.y + margin
1021 && b.low.y <= a.high.y + margin
1022 && a.low.z <= b.high.z + margin
1023 && b.low.z <= a.high.z + margin
1024}
1025
1026fn triangles_cross(a: &Cell, b: &Cell) -> Option<Point> {
1032 for (edges, target) in [(a, b), (b, a)] {
1033 for k in 0..3 {
1034 let (from, to) = (edges.corners[k], edges.corners[(k + 1) % 3]);
1035 if let Some(hit) = segment_meets_triangle(from, to, target.corners) {
1036 return Some(hit);
1037 }
1038 }
1039 }
1040 None
1041}
1042
1043pub(crate) fn segment_meets_triangle(from: Point, to: Point, t: [Point; 3]) -> Option<Point> {
1045 let direction = to - from;
1046 let (e1, e2) = (t[1] - t[0], t[2] - t[0]);
1047 let h = direction.cross(e2);
1048 let determinant = e1.dot(h);
1049 if determinant.abs() <= f64::MIN_POSITIVE {
1050 return None;
1051 }
1052 let inverse = 1.0 / determinant;
1053 let s = from - t[0];
1054 let u = inverse * s.dot(h);
1055 if !(0.0..=1.0).contains(&u) {
1056 return None;
1057 }
1058 let q = s.cross(e1);
1059 let v = inverse * direction.dot(q);
1060 if v < 0.0 || u + v > 1.0 {
1061 return None;
1062 }
1063 let along = inverse * e2.dot(q);
1064 if !(0.0..=1.0).contains(&along) {
1065 return None;
1066 }
1067 Some(from + direction * along)
1068}
1069
1070struct Arc {
1072 points: Vec<Point>,
1073 on_a: Vec<(f64, f64)>,
1074 on_b: Vec<(f64, f64)>,
1075 head_bp: Option<usize>,
1077 tail_bp: Option<usize>,
1078}
1079
1080impl Arc {
1081 fn length(&self) -> f64 {
1082 self.points
1083 .windows(2)
1084 .map(|pair| pair[0].distance(pair[1]))
1085 .sum()
1086 }
1087
1088 fn outgoing(&self, tail: bool) -> Option<Vector> {
1091 let n = self.points.len();
1092 if n < 2 {
1093 return None;
1094 }
1095 let window = (n - 1).min(24);
1096 let (at, back) = if tail {
1097 (n - 1, n - 1 - window)
1098 } else {
1099 (0, window)
1100 };
1101 let out = self.points[at] - self.points[back];
1102 let m = out.magnitude();
1103 (m > f64::MIN_POSITIVE).then(|| out / m)
1104 }
1105}
1106
1107fn interior_is_transversal(
1113 branch: &Traced,
1114 a: &SurfaceGeometry,
1115 b: &SurfaceGeometry,
1116 tol: Tolerances,
1117) -> bool {
1118 let n = branch.points.len();
1119 if n < 5 {
1120 return false;
1121 }
1122 [n / 4, n / 2, 3 * n / 4]
1123 .into_iter()
1124 .any(|i| crossing_sine(a, b, branch.on_a[i], branch.on_b[i], tol) > BRANCH_POINT_SINE)
1125}
1126
1127fn stitch_stalled(
1146 found: Vec<Traced>,
1147 a: &SurfaceGeometry,
1148 b: &SurfaceGeometry,
1149 options: Marching,
1150 tol: Tolerances,
1151) -> Vec<Traced> {
1152 let reach = options.chord.max(tol.confusion()) * 60.0;
1153 const CONTINUES: f64 = 0.5;
1154
1155 let (candidates, mut out): (Vec<Traced>, Vec<Traced>) = found.into_iter().partition(|branch| {
1156 branch.stopped == Stopped::Stalled && interior_is_transversal(branch, a, b, tol)
1157 });
1158 if candidates.is_empty() {
1159 return out;
1160 }
1161
1162 let mut bps: Vec<Point> = Vec::new();
1164 for branch in &candidates {
1165 let n = branch.points.len();
1166 for at in [0, n - 1] {
1167 if crossing_sine(a, b, branch.on_a[at], branch.on_b[at], tol) < BRANCH_POINT_SINE {
1168 let p = branch.points[at];
1169 if !bps.iter().any(|held| held.distance(p) <= reach) {
1170 bps.push(p);
1171 }
1172 }
1173 }
1174 }
1175 if bps.is_empty() {
1176 out.extend(candidates);
1177 return out;
1178 }
1179 let bp_of =
1180 |p: Point| -> Option<usize> { bps.iter().position(|held| held.distance(p) <= reach) };
1181
1182 let mut arcs: Vec<Arc> = Vec::new();
1186 for branch in &candidates {
1187 let n = branch.points.len();
1188 let mut run_start: Option<usize> = None;
1189 for i in 0..=n {
1190 let near = i < n && bp_of(branch.points[i]).is_some();
1191 match (run_start, near, i == n) {
1192 (None, false, false) => run_start = Some(i),
1193 (Some(s), true, _) | (Some(s), _, true) => {
1194 let e = i;
1195 if e > s + 1 {
1196 let head_bp = if s > 0 {
1197 bp_of(branch.points[s - 1])
1198 } else {
1199 None
1200 };
1201 let tail_bp = if e < n { bp_of(branch.points[e]) } else { None };
1202 arcs.push(Arc {
1203 points: branch.points[s..e].to_vec(),
1204 on_a: branch.on_a[s..e].to_vec(),
1205 on_b: branch.on_b[s..e].to_vec(),
1206 head_bp,
1207 tail_bp,
1208 });
1209 }
1210 run_start = None;
1211 }
1212 _ => {}
1213 }
1214 }
1215 }
1216
1217 arcs.retain(|arc| arc.length() > options.chord * 10.0 && arc.points.len() >= 4);
1220 arcs.sort_by(|x, y| {
1221 y.length()
1222 .partial_cmp(&x.length())
1223 .unwrap_or(core::cmp::Ordering::Equal)
1224 });
1225 let mut kept: Vec<Arc> = Vec::new();
1226 'candidate: for arc in arcs {
1227 let n = arc.points.len();
1228 for probe in [n / 4, n / 2, 3 * n / 4] {
1229 let p = arc.points[probe];
1230 if kept.iter().any(|held| {
1231 held.points
1232 .windows(2)
1233 .any(|pair| distance_to_segment(p, pair[0], pair[1]) <= reach)
1234 }) {
1235 continue 'candidate;
1236 }
1237 }
1238 kept.push(arc);
1239 }
1240
1241 let ends: Vec<(usize, bool, usize, Vector)> = kept
1244 .iter()
1245 .enumerate()
1246 .flat_map(|(i, arc)| {
1247 [(false, arc.head_bp), (true, arc.tail_bp)]
1248 .into_iter()
1249 .filter_map(move |(tail, bp)| Some((i, tail, bp?, arc.outgoing(tail)?)))
1250 })
1251 .collect();
1252 let mut partner: Vec<Option<usize>> = vec![None; ends.len()];
1253 for bp in 0..bps.len() {
1254 loop {
1255 let mut best: Option<(usize, usize, f64)> = None;
1256 for x in 0..ends.len() {
1257 if partner[x].is_some() || ends[x].2 != bp {
1258 continue;
1259 }
1260 for y in (x + 1)..ends.len() {
1261 if partner[y].is_some() || ends[y].2 != bp {
1262 continue;
1263 }
1264 let score = -ends[x].3.dot(ends[y].3);
1265 if score > CONTINUES && best.is_none_or(|(_, _, held)| score > held) {
1266 best = Some((x, y, score));
1267 }
1268 }
1269 }
1270 let Some((x, y, _)) = best else { break };
1271 partner[x] = Some(y);
1272 partner[y] = Some(x);
1273 }
1274 }
1275
1276 let end_index = |arc: usize, tail: bool| -> Option<usize> {
1278 ends.iter().position(|e| e.0 == arc && e.1 == tail)
1279 };
1280 let mut used = vec![false; kept.len()];
1281 for start in 0..kept.len() {
1282 if used[start] {
1283 continue;
1284 }
1285 let mut first = start;
1287 let mut first_reversed = false;
1288 let mut seen_back = vec![false; kept.len()];
1289 loop {
1290 seen_back[first] = true;
1291 let Some(entry) = end_index(first, first_reversed) else {
1294 break;
1295 };
1296 let Some(p) = partner[entry] else { break };
1297 let (prev, prev_tail, _, _) = ends[p];
1298 if seen_back[prev] {
1299 break; }
1301 first = prev;
1302 first_reversed = !prev_tail;
1305 }
1306
1307 let mut points: Vec<Point> = Vec::new();
1309 let mut on_a: Vec<(f64, f64)> = Vec::new();
1310 let mut on_b: Vec<(f64, f64)> = Vec::new();
1311 let mut current = first;
1312 let mut reversed = first_reversed;
1313 let mut closed = false;
1314 loop {
1315 used[current] = true;
1316 let arc = &kept[current];
1317 type Run = (Vec<Point>, Vec<(f64, f64)>, Vec<(f64, f64)>);
1318 let (pts, pa, pb): Run = if reversed {
1319 (
1320 arc.points.iter().rev().copied().collect(),
1321 arc.on_a.iter().rev().copied().collect(),
1322 arc.on_b.iter().rev().copied().collect(),
1323 )
1324 } else {
1325 (arc.points.clone(), arc.on_a.clone(), arc.on_b.clone())
1326 };
1327 if !points.is_empty() {
1329 let joint_bp = if reversed { arc.tail_bp } else { arc.head_bp };
1330 if let Some(bp) = joint_bp {
1331 points.push(bps[bp]);
1332 on_a.push(pa[0]);
1333 on_b.push(pb[0]);
1334 }
1335 }
1336 points.extend(pts);
1337 on_a.extend(pa);
1338 on_b.extend(pb);
1339
1340 let leaving = end_index(current, !reversed);
1341 let Some(l) = leaving else { break };
1342 let Some(p) = partner[l] else { break };
1343 let (next, next_tail, _, _) = ends[p];
1344 if used[next] {
1345 closed = next == first;
1346 break;
1347 }
1348 current = next;
1349 reversed = next_tail;
1350 }
1351 if closed && points.len() > 3 {
1352 let bridge = points[0];
1353 let ba = on_a[0];
1354 let bb = on_b[0];
1355 points.push(bridge);
1356 on_a.push(ba);
1357 on_b.push(bb);
1358 }
1359 out.push(Traced {
1360 points,
1361 on_a,
1362 on_b,
1363 stopped: if closed {
1364 Stopped::Closed
1365 } else {
1366 Stopped::Stalled
1367 },
1368 });
1369 }
1370 out
1371}
1372
1373fn nearest_on(
1376 surface: &SurfaceGeometry,
1377 seed: (f64, f64),
1378 target: Point,
1379 tol: Tolerances,
1380) -> Option<((f64, f64), Point)> {
1381 let (mut u, mut v) = seed;
1382 for _ in 0..16 {
1383 let (u_ok, v_ok) = surface.normalize_parameters(u, v, tol).ok()?;
1384 u = u_ok;
1385 v = v_ok;
1386 let p = surface.point_at(u, v, tol).ok()?;
1387 let (su, sv) = surface.d1_at(u, v, tol).ok()?;
1388 let r = p - target;
1389 let (a11, a12, a22) = (su.dot(su), su.dot(sv), sv.dot(sv));
1390 let det = a11.mul_add(a22, -(a12 * a12));
1391 if det.abs() <= f64::MIN_POSITIVE {
1392 break;
1393 }
1394 let (b1, b2) = (-su.dot(r), -sv.dot(r));
1395 let du = b1.mul_add(a22, -(b2 * a12)) / det;
1396 let dv = a11.mul_add(b2, -(a12 * b1)) / det;
1397 u += du;
1398 v += dv;
1399 if du.hypot(dv) < 1e-14 {
1400 break;
1401 }
1402 }
1403 let (u, v) = surface.normalize_parameters(u, v, tol).ok()?;
1404 Some(((u, v), surface.point_at(u, v, tol).ok()?))
1405}
1406
1407pub fn trace_tangential(
1429 a: &SurfaceGeometry,
1430 b: &SurfaceGeometry,
1431 from: Contact,
1432 options: Marching,
1433 tol: Tolerances,
1434) -> OgeomResult<Traced> {
1435 options.validate()?;
1436 let accept = tol.confusion() * 100.0;
1437 let sine = crossing_sine(a, b, from.on_a, from.on_b, tol);
1438 if sine > BRANCH_POINT_SINE {
1439 ogeom_bail!(
1440 Construction,
1441 "the surfaces cross here at sine {sine}; tangential tracing wants a contact"
1442 );
1443 }
1444 let reach = span(a).max(span(b));
1445 let step = (options.chord * reach)
1446 .sqrt()
1447 .clamp(tol.confusion(), reach / 16.0);
1448
1449 type Walked = (Vec<Point>, Vec<(f64, f64)>, Vec<(f64, f64)>, Stopped);
1450 let walk_one = |sense: f64| -> OgeomResult<Walked> {
1451 let mut points = vec![from.point];
1452 let mut on_a = vec![from.on_a];
1453 let mut on_b = vec![from.on_b];
1454 let mut at = from;
1455 let mut previous: Option<Vector> = None;
1456 let mut stopped = Stopped::RanOut;
1457 while points.len() < options.max_points {
1458 ogeom_core::progress::checkpoint()?;
1459 let Some(normal) = normal_at(a, at.on_a, tol) else {
1464 stopped = Stopped::Stalled;
1465 break;
1466 };
1467 let direction = match previous {
1468 Some(d) => {
1469 let flat = d - normal * d.dot(normal);
1470 let m = flat.magnitude();
1471 if m <= f64::MIN_POSITIVE {
1472 stopped = Stopped::Stalled;
1473 break;
1474 }
1475 flat / m
1476 }
1477 None => {
1478 let (su, _) = a.d1_at(at.on_a.0, at.on_a.1, tol).map_err(|_| {
1481 ogeom_core::ogeom_err!(Construction, "the seed cannot be evaluated")
1482 })?;
1483 let t1 = {
1484 let flat = su - normal * su.dot(normal);
1485 let m = flat.magnitude();
1486 if m <= f64::MIN_POSITIVE {
1487 stopped = Stopped::Stalled;
1488 break;
1489 }
1490 flat / m
1491 };
1492 let t2 = normal.cross(t1);
1493 let mut best = (f64::INFINITY, t1);
1494 for k in 0..16 {
1495 let angle = core::f64::consts::TAU * f64::from(k) / 16.0;
1496 let dir = t1 * angle.cos() + t2 * angle.sin();
1497 let probe = at.point + dir * step;
1498 let Some((_, qa)) = nearest_on(a, at.on_a, probe, tol) else {
1499 continue;
1500 };
1501 let Some((_, qb)) = nearest_on(b, at.on_b, qa, tol) else {
1502 continue;
1503 };
1504 let gap = qa.distance(qb);
1505 if gap < best.0 {
1506 best = (gap, dir);
1507 }
1508 }
1509 best.1 * sense
1510 }
1511 };
1512
1513 let mut candidate = at.point + direction * step;
1516 let mut pa = at.on_a;
1517 let mut pb = at.on_b;
1518 let mut gap = f64::INFINITY;
1519 for _ in 0..8 {
1520 let Some((ua, qa)) = nearest_on(a, pa, candidate, tol) else {
1521 break;
1522 };
1523 let Some((ub, qb)) = nearest_on(b, pb, qa, tol) else {
1524 break;
1525 };
1526 pa = ua;
1527 pb = ub;
1528 gap = qa.distance(qb);
1529 if gap <= tol.confusion() {
1530 candidate = qa;
1531 break;
1532 }
1533 candidate = qa + (qb - qa) * 0.5;
1535 }
1536 if gap > accept {
1537 stopped = Stopped::Stalled;
1538 break;
1539 }
1540 let next = Contact {
1541 on_a: pa,
1542 on_b: pb,
1543 point: candidate,
1544 };
1545 if points.len() > 3 && next.point.distance(from.point) <= step {
1546 points.push(from.point);
1547 on_a.push(from.on_a);
1548 on_b.push(from.on_b);
1549 stopped = Stopped::Closed;
1550 break;
1551 }
1552 if next.point.distance(at.point) <= step * 1e-3 {
1553 stopped = Stopped::Stalled;
1554 break;
1555 }
1556 previous = Some(next.point - at.point);
1557 points.push(next.point);
1558 on_a.push(next.on_a);
1559 on_b.push(next.on_b);
1560 at = next;
1561 }
1562 Ok((points, on_a, on_b, stopped))
1563 };
1564
1565 let (points, on_a, on_b, stopped) = walk_one(1.0)?;
1566 if stopped == Stopped::Closed {
1567 return Ok(Traced {
1568 points,
1569 on_a,
1570 on_b,
1571 stopped,
1572 });
1573 }
1574 let (mut back_points, mut back_a, mut back_b, back_stopped) = walk_one(-1.0)?;
1575 back_points.reverse();
1576 back_a.reverse();
1577 back_b.reverse();
1578 back_points.pop();
1579 back_a.pop();
1580 back_b.pop();
1581 back_points.extend(points);
1582 back_a.extend(on_a);
1583 back_b.extend(on_b);
1584 let stopped = if stopped == Stopped::RanOut || back_stopped == Stopped::RanOut {
1585 Stopped::RanOut
1586 } else if stopped == Stopped::Stalled || back_stopped == Stopped::Stalled {
1587 Stopped::Stalled
1588 } else {
1589 Stopped::LeftTheDomain
1590 };
1591 Ok(Traced {
1592 points: back_points,
1593 on_a: back_a,
1594 on_b: back_b,
1595 stopped,
1596 })
1597}
1598
1599#[cfg(test)]
1600#[allow(clippy::unwrap_used, clippy::print_stdout)]
1601mod tests {
1602 use super::*;
1603 use ogeom_geom::{CylinderSurface, PlaneSurface, SphereSurface};
1604 use ogeom_math::{Cylinder, Direction, Frame, Plane, Sphere};
1605
1606 const T: Tolerances = Tolerances::millimetres();
1607
1608 fn cylinder(origin: Point, axis: Vector, radius: f64, height: (f64, f64)) -> SurfaceGeometry {
1609 let frame = Frame::new(
1610 origin,
1611 Direction::new(axis, T).unwrap(),
1612 Direction::from_cross(axis, Vector::new(0.3, 0.5, 0.9), T).unwrap(),
1613 T,
1614 )
1615 .unwrap();
1616 CylinderSurface::new(Cylinder::new(frame, radius, T).unwrap(), height)
1617 .unwrap()
1618 .into()
1619 }
1620
1621 fn sphere(centre: Point, radius: f64) -> SurfaceGeometry {
1622 SphereSurface::new(Sphere::centred(centre, radius, T).unwrap()).into()
1623 }
1624
1625 fn plane(origin: Point, normal: Vector) -> SurfaceGeometry {
1626 PlaneSurface::over(
1627 Plane::through(origin, Direction::new(normal, T).unwrap()),
1628 (-8.0, 8.0),
1629 (-8.0, 8.0),
1630 )
1631 .unwrap()
1632 .into()
1633 }
1634
1635 fn off(surface: &SurfaceGeometry, p: Point) -> f64 {
1637 match surface {
1638 SurfaceGeometry::Plane(x) => x.plane().distance_to(p),
1639 SurfaceGeometry::Sphere(x) => x.sphere().distance_to(p),
1640 SurfaceGeometry::Cylinder(x) => x.cylinder().distance_to(p),
1641 _ => 0.0,
1642 }
1643 }
1644
1645 fn deviation(a: &SurfaceGeometry, b: &SurfaceGeometry, traced: &Traced) -> f64 {
1647 traced
1648 .points
1649 .iter()
1650 .map(|p| off(a, *p).abs().max(off(b, *p).abs()))
1651 .fold(0.0_f64, f64::max)
1652 }
1653
1654 #[test]
1655 fn a_plane_through_a_bent_strip_seeds_both_branches() {
1656 use ogeom_geom::BSplineSurface;
1662 use ogeom_math::{ControlGrid, KnotVector};
1663 let mut points = Vec::new();
1664 for i in 0..7 {
1665 let a = core::f64::consts::PI * f64::from(i) / 6.0;
1666 for j in 0..2 {
1667 points.push(Point::new(2.0 * a.cos(), f64::from(j), 2.0 * a.sin()));
1668 }
1669 }
1670 let grid = ControlGrid::new(points, 7, 2).unwrap();
1671 let strip: SurfaceGeometry = BSplineSurface::new(
1672 KnotVector::clamped_uniform(3, 7).unwrap(),
1673 KnotVector::clamped_uniform(1, 2).unwrap(),
1674 &grid,
1675 T,
1676 )
1677 .unwrap()
1678 .into();
1679 let level: SurfaceGeometry = PlaneSurface::over(
1680 Plane::through(Point::new(0.0, 0.0, 1.0), Direction::Z),
1681 (-1.0e9, 1.0e9),
1682 (-1.0e9, 1.0e9),
1683 )
1684 .unwrap()
1685 .into();
1686 let options = Marching {
1687 chord: 1e-5,
1688 ..Marching::default()
1689 };
1690 let found = branches(&strip, &level, options, T).unwrap();
1691 assert_eq!(
1692 found.len(),
1693 2,
1694 "the arch crosses the level twice: {}",
1695 found.len()
1696 );
1697 for branch in &found {
1698 assert!(!branch.closed());
1699 for p in &branch.points {
1700 assert!((p.z - 1.0).abs() < 1e-4, "on the level: {p:?}");
1701 }
1702 }
1703 }
1704
1705 #[test]
1706 fn two_crossed_cylinders_are_traced_onto_both_of_them() {
1707 let a = cylinder(Point::ORIGIN, Vector::Z, 1.0, (-4.0, 4.0));
1711 let b = cylinder(Point::ORIGIN, Vector::X, 1.0, (-4.0, 4.0));
1712 let options = Marching {
1713 chord: 1e-5,
1714 ..Marching::default()
1715 };
1716
1717 let found = branches(&a, &b, options, T).unwrap();
1718 assert_eq!(
1719 found.len(),
1720 2,
1721 "two equal cylinders crossing at right angles meet in two closed \
1722 curves: the Steinmetz solid's seams"
1723 );
1724
1725 let mut worst = 0.0_f64;
1726 for branch in &found {
1727 assert!(branch.closed(), "each seam is a closed loop");
1728 assert!(
1729 branch.points.len() > 100,
1730 "a branch of only {} points",
1731 branch.points.len()
1732 );
1733 worst = worst.max(deviation(&a, &b, branch));
1734 }
1735 println!(
1736 "crossed cylinders: {} branches, worst deviation {worst:e}",
1737 found.len()
1738 );
1739 assert!(worst < 1e-7, "traced off the surfaces by {worst:e}");
1740 }
1741
1742 #[test]
1743 fn unequal_crossed_cylinders_meet_in_two_curves_as_well() {
1744 let a = cylinder(Point::ORIGIN, Vector::Z, 1.0, (-4.0, 4.0));
1749 let b = cylinder(Point::ORIGIN, Vector::X, 1.6, (-4.0, 4.0));
1750 let options = Marching {
1751 chord: 1e-5,
1752 ..Marching::default()
1753 };
1754
1755 let found = branches(&a, &b, options, T).unwrap();
1756 assert_eq!(found.len(), 2);
1757 for branch in &found {
1758 assert!(branch.closed());
1759 assert!(deviation(&a, &b, branch) < 1e-7);
1760 }
1761 }
1762
1763 #[test]
1764 fn a_traced_circle_agrees_with_the_circle_it_should_be() {
1765 let s = sphere(Point::ORIGIN, 3.0);
1770 let cut = plane(Point::ORIGIN, Vector::Z);
1771 let options = Marching {
1772 chord: 1e-6,
1773 ..Marching::default()
1774 };
1775
1776 let found = seeds(&s, &cut, options, T).unwrap();
1777 assert!(!found.is_empty());
1778 let branch = trace(&s, &cut, found[0], options, T).unwrap();
1779
1780 assert!(
1781 branch.closed(),
1782 "a plane through a sphere gives a closed loop"
1783 );
1784 for p in &branch.points {
1785 let radius = (p.x * p.x + p.y * p.y).sqrt();
1786 assert!(
1787 (radius - 3.0).abs() < 1e-7,
1788 "a point at radius {radius} on a circle of 3"
1789 );
1790 assert!(p.z.abs() < 1e-7, "off the cutting plane by {}", p.z);
1791 }
1792 }
1793
1794 #[test]
1795 fn a_branch_that_leaves_the_surface_says_so_rather_than_stopping_quietly() {
1796 let s = sphere(Point::ORIGIN, 3.0);
1799 let cut = plane(Point::new(0.0, 0.0, 0.0), Vector::Z);
1800 let options = Marching {
1801 chord: 1e-4,
1802 max_points: 8,
1803 ..Marching::default()
1804 };
1805 let found = seeds(&s, &cut, options, T).unwrap();
1806 let branch = trace(&s, &cut, found[0], options, T).unwrap();
1807 assert_eq!(branch.stopped, Stopped::RanOut);
1808 assert!(!branch.complete(), "a truncated branch is not complete");
1809 }
1810
1811 #[test]
1812 fn tangent_surfaces_are_refused_rather_than_followed_onto_a_guess() {
1813 let s = sphere(Point::new(0.0, 0.0, 3.0), 3.0);
1817 let ground = plane(Point::ORIGIN, Vector::Z);
1818 let touch = Contact {
1819 on_a: (0.0, -core::f64::consts::FRAC_PI_2),
1820 on_b: (0.0, 0.0),
1821 point: Point::ORIGIN,
1822 };
1823 let err = trace(&s, &ground, touch, Marching::default(), T).unwrap_err();
1824 assert!(err.to_string().contains("tangent"), "unexpected: {err}");
1825 }
1826
1827 #[test]
1828 fn the_number_of_branches_is_the_number_there_are() {
1829 let options = Marching {
1833 chord: 1e-5,
1834 ..Marching::default()
1835 };
1836
1837 let one = branches(
1839 &sphere(Point::ORIGIN, 3.0),
1840 &plane(Point::new(0.0, 0.0, 1.0), Vector::Z),
1841 options,
1842 T,
1843 )
1844 .unwrap();
1845 assert_eq!(one.len(), 1, "one plane through a sphere cuts one circle");
1846 assert!(one[0].closed());
1847
1848 let two = branches(
1851 &sphere(Point::ORIGIN, 3.0),
1852 &cylinder(Point::ORIGIN, Vector::Z, 1.5, (-4.0, 4.0)),
1853 options,
1854 T,
1855 )
1856 .unwrap();
1857 assert_eq!(two.len(), 2, "a coaxial cylinder cuts a sphere twice");
1858 for branch in &two {
1859 assert!(branch.closed(), "each is a closed circle");
1860 }
1861 let heights: Vec<f64> = two.iter().map(|b| b.points[0].z).collect();
1863 assert!(
1864 heights[0] * heights[1] < 0.0,
1865 "both branches came back on the same side: {heights:?}"
1866 );
1867 }
1868
1869 #[test]
1870 fn a_branch_thinner_than_the_sampling_is_missed_and_the_knob_finds_it() {
1871 let a = sphere(Point::ORIGIN, 3.0);
1875 let b = sphere(Point::new(5.98, 0.0, 0.0), 3.0);
1876
1877 let coarse = seeds(
1878 &a,
1879 &b,
1880 Marching {
1881 grid: 6,
1882 ..Marching::default()
1883 },
1884 T,
1885 )
1886 .unwrap();
1887 let fine = seeds(
1888 &a,
1889 &b,
1890 Marching {
1891 grid: 120,
1892 ..Marching::default()
1893 },
1894 T,
1895 )
1896 .unwrap();
1897 assert!(
1898 coarse.len() < fine.len(),
1899 "a finer grid should find what a coarse one steps over: {} against {}",
1900 coarse.len(),
1901 fine.len()
1902 );
1903 assert!(!fine.is_empty(), "the branch is there to be found");
1904 }
1905
1906 #[test]
1907 fn settings_that_could_not_work_are_refused() {
1908 let a = sphere(Point::ORIGIN, 1.0);
1909 let b = plane(Point::ORIGIN, Vector::Z);
1910 for options in [
1911 Marching {
1912 chord: 0.0,
1913 ..Marching::default()
1914 },
1915 Marching {
1916 grid: 1,
1917 ..Marching::default()
1918 },
1919 Marching {
1920 max_points: 1,
1921 ..Marching::default()
1922 },
1923 ] {
1924 assert!(seeds(&a, &b, options, T).is_err());
1925 }
1926 }
1927}