1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
28use ogeom_geom::{
29 Circle2d, Curve, Curve2d as _, Curve3d, Ellipse2d, Line2d, PlanarCurve, Surface,
30 SurfaceGeometry,
31};
32use ogeom_math::{Circle2, Ellipse2, Frame2, Point, Point2};
33
34use crate::approx::approximate_branch;
35use crate::contact::trace_tangential;
36use crate::march::{Marching, branches};
37use crate::surface::{Meeting, surface_surface};
38
39#[derive(Debug, Clone, Copy, PartialEq)]
41pub struct IntersectOptions {
42 pub tolerance: f64,
44 pub marching: Marching,
46}
47
48impl Default for IntersectOptions {
49 fn default() -> Self {
50 Self {
51 tolerance: 1e-6,
52 marching: Marching::default(),
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq)]
59pub struct SectionCurve {
60 pub curve: Curve,
62 pub on_a: Option<PlanarCurve>,
69 pub on_b: Option<PlanarCurve>,
71 pub tolerance: f64,
76 pub exact: bool,
78 pub closed: bool,
80 pub tangential: bool,
90}
91
92#[derive(Debug, Clone, PartialEq)]
94pub enum SurfaceIntersection {
95 Apart,
102 Touching(Vec<Point>),
104 Along(Vec<SectionCurve>),
106 Same,
108}
109
110pub fn intersect_surfaces(
128 a: &SurfaceGeometry,
129 b: &SurfaceGeometry,
130 options: IntersectOptions,
131 tol: Tolerances,
132) -> OgeomResult<SurfaceIntersection> {
133 if !options.tolerance.is_finite() || options.tolerance <= 0.0 {
134 ogeom_bail!(
135 Construction,
136 "a tolerance of {} is not a distance",
137 options.tolerance
138 );
139 }
140
141 if let Some(sections) = near_parallel_plane_drum(a, b, tol) {
146 return Ok(if sections.is_empty() {
147 SurfaceIntersection::Apart
148 } else {
149 SurfaceIntersection::Along(sections)
150 });
151 }
152 match surface_surface(a, b, tol) {
153 Ok(Meeting::Apart) => Ok(SurfaceIntersection::Apart),
154 Ok(Meeting::Same) => Ok(SurfaceIntersection::Same),
155 Ok(Meeting::Touching(points)) => Ok(SurfaceIntersection::Touching(points)),
156 Ok(Meeting::Along(curves)) => {
157 let sections: Vec<SectionCurve> = curves
158 .into_iter()
159 .filter_map(|curve| exact_section(curve, a, b, tol))
160 .collect();
161 Ok(if sections.is_empty() {
162 SurfaceIntersection::Apart
165 } else {
166 SurfaceIntersection::Along(sections)
167 })
168 }
169 Err(_) if surfaces_coincide(a, b, options.tolerance, tol) => Ok(SurfaceIntersection::Same),
174 Err(_) => match near_parallel_drums(a, b, tol)
177 .or_else(|| ball_through_drum(a, b, tol))
178 .or_else(|| axial_plane_revolution(a, b, tol))
179 .or_else(|| plane_along_spline_lines(a, b, tol))
180 {
181 Some(sections) if sections.is_empty() => Ok(SurfaceIntersection::Apart),
182 Some(sections) => Ok(SurfaceIntersection::Along(sections)),
183 None => marched(a, b, options, tol),
184 },
185 }
186}
187
188fn surfaces_coincide(
209 a: &SurfaceGeometry,
210 b: &SurfaceGeometry,
211 reach: f64,
212 tol: Tolerances,
213) -> bool {
214 const GRID: usize = 6;
217 const EVIDENCE: usize = 4;
218
219 let span = |s: &SurfaceGeometry| -> f64 {
220 let ((ua, ub), (va, vb)) = s.domain();
221 (ub - ua).abs().max((vb - va).abs())
222 };
223 let (sampled, against) = if span(a) <= span(b) { (a, b) } else { (b, a) };
224 let ((ua, ub), (va, vb)) = sampled.domain();
225 if !(ua.is_finite() && ub.is_finite() && va.is_finite() && vb.is_finite()) {
226 return false;
227 }
228 let ((wu0, wu1), (wv0, wv1)) = against.domain();
229 let (mu, mv) = ((wu1 - wu0) * 0.05, (wv1 - wv0) * 0.05);
232
233 let mut evidence = 0_usize;
234 for i in 0..=GRID {
235 for j in 0..=GRID {
236 #[allow(clippy::cast_precision_loss)]
237 let u = ua + (ub - ua) * (i as f64 / GRID as f64);
238 #[allow(clippy::cast_precision_loss)]
239 let v = va + (vb - va) * (j as f64 / GRID as f64);
240 let Ok(p) = sampled.point_at(u, v, tol) else {
241 return false;
242 };
243 let Ok(foot) = ogeom_geom::project_on_surface(against, p, 16, tol) else {
244 return false;
245 };
246 let (fu, fv) = foot.parameters;
247 if fu <= wu0 + mu || fu >= wu1 - mu || fv <= wv0 + mv || fv >= wv1 - mv {
248 continue;
249 }
250 if foot.distance > reach {
251 return false;
252 }
253 evidence += 1;
254 }
255 }
256 evidence >= EVIDENCE
257}
258
259fn axial_plane_revolution(
272 a: &SurfaceGeometry,
273 b: &SurfaceGeometry,
274 tol: Tolerances,
275) -> Option<Vec<SectionCurve>> {
276 const SAMPLES: u32 = 64;
277 let (plane, revolution, plane_first) = match (a, b) {
278 (SurfaceGeometry::Plane(p), SurfaceGeometry::Revolution(r)) => (p, r, true),
279 (SurfaceGeometry::Revolution(r), SurfaceGeometry::Plane(p)) => (p, r, false),
280 _ => return None,
281 };
282 let cut = plane.plane();
283 let axis = revolution.axis();
284 let normal = cut.normal();
285 if normal.dot(axis.direction).abs() > tol.angular()
286 || cut.signed_distance_to(axis.location).abs() > tol.confusion()
287 {
288 return None;
289 }
290 let profile = revolution.curve();
291 let (v0, v1) = profile.domain();
292 let at = |k: u32| v0 + (v1 - v0) * f64::from(k) / f64::from(SAMPLES);
293 let radial = |v: f64| {
294 let p = profile.point_at(v, tol).ok()?;
295 Some(p - axis.project(p))
296 };
297 let mut widest = ogeom_math::Vector::ZERO;
299 for k in 0..=SAMPLES {
300 let r = radial(at(k))?;
301 if r.magnitude() > widest.magnitude() {
302 widest = r;
303 }
304 }
305 let side = ogeom_math::Direction::new(widest, tol).ok()?;
306 let across = axis.direction.cross_with(side.vector());
307 let offset = |v: f64| radial(v).map(|r| (r.dot(side.vector()), r.dot(across)));
309 for k in 0..=SAMPLES {
310 let (_, off) = offset(at(k))?;
311 if off.abs() > tol.confusion() {
312 return None;
313 }
314 }
315 let mut cuts = vec![v0];
318 for k in 0..SAMPLES {
319 let (mut lo, mut hi) = (at(k), at(k + 1));
320 let (s_lo, s_hi) = (offset(lo)?.0, offset(hi)?.0);
321 if s_lo.abs() <= tol.confusion() || s_lo * s_hi >= 0.0 {
322 continue;
323 }
324 for _ in 0..80 {
325 let mid = f64::midpoint(lo, hi);
326 if offset(mid)?.0 * s_lo > 0.0 {
327 lo = mid;
328 } else {
329 hi = mid;
330 }
331 }
332 cuts.push(f64::midpoint(lo, hi));
333 }
334 cuts.push(v1);
335 let out = axis.direction.cross_with(normal.vector());
337 let first = across.dot(out).atan2(side.vector().dot(out));
338 let (u0, u1) = revolution.domain().0;
339 let turns: Vec<f64> = [first, first + core::f64::consts::PI]
340 .into_iter()
341 .filter_map(|u| {
342 let u = u0 + (u - u0).rem_euclid(core::f64::consts::TAU);
343 let u = if (u - u0 - core::f64::consts::TAU).abs() <= tol.angular() {
344 u0
345 } else {
346 u
347 };
348 (u <= u1 + tol.angular()).then_some(u.min(u1))
349 })
350 .collect();
351 let mut sections = Vec::new();
352 for &u in &turns {
353 let turned = ogeom_geom::Transformable::transformed(
354 profile,
355 &ogeom_math::Transform::rotation(axis, u),
356 tol,
357 )
358 .ok()?;
359 for piece in cuts.windows(2) {
360 let (va, vb) = (piece[0], piece[1]);
361 if vb - va <= tol.parametric() {
362 continue;
363 }
364 let curve: Curve = ogeom_geom::TrimmedCurve::new(turned.clone(), va, vb, tol)
365 .ok()?
366 .into();
367 let column: PlanarCurve = Line2d::over(
368 ogeom_math::Axis2::new(Point2::new(u, 0.0), ogeom_math::Direction2::Y),
369 va,
370 vb,
371 )
372 .ok()?
373 .into();
374 let flat = exact_pcurve(&curve, (va, vb), a_or_b(plane_first, a, b), tol);
375 let (on_a, on_b) = if plane_first {
376 (flat, Some(column))
377 } else {
378 (Some(column), flat)
379 };
380 sections.push(SectionCurve {
381 on_a,
382 on_b,
383 tolerance: 0.0,
384 exact: true,
385 closed: false,
386 tangential: false,
387 curve,
388 });
389 }
390 }
391 Some(sections)
392}
393
394fn plane_along_spline_lines(
407 a: &SurfaceGeometry,
408 b: &SurfaceGeometry,
409 tol: Tolerances,
410) -> Option<Vec<SectionCurve>> {
411 const SAMPLES: u32 = 96;
412 let (plane, spline, plane_first) = match (a, b) {
413 (SurfaceGeometry::Plane(p), SurfaceGeometry::BSpline(s)) => (p.plane(), s, true),
414 (SurfaceGeometry::BSpline(s), SurfaceGeometry::Plane(p)) => (p.plane(), s, false),
415 _ => return None,
416 };
417 let distance = |p: Point| plane.signed_distance_to(p);
418 let ((u0, u1), (v0, v1)) = spline.domain();
419 let line_at = |along_u: bool, t: f64| -> Option<ogeom_geom::BSplineCurve> {
422 if along_u {
423 spline.iso_u_curve(t, tol).ok()
424 } else {
425 spline.iso_v_curve(t, tol).ok()
426 }
427 };
428 let weighted = |curve: &ogeom_geom::BSplineCurve| -> Vec<f64> {
429 curve
430 .control_points()
431 .iter()
432 .map(|w| w.weight * distance(w.point()))
433 .collect()
434 };
435 let lies_in = |curve: &ogeom_geom::BSplineCurve| {
436 curve
437 .control_points()
438 .iter()
439 .all(|w| distance(w.point()).abs() <= tol.confusion())
440 };
441 let has_length = |curve: &ogeom_geom::BSplineCurve| {
442 let first = curve.control_points()[0].point();
443 curve
444 .control_points()
445 .iter()
446 .any(|w| w.point().distance(first) > tol.confusion())
447 };
448 let found = |along_u: bool| -> Option<Vec<f64>> {
451 let (lo, hi) = if along_u { (u0, u1) } else { (v0, v1) };
452 let at = |k: u32| lo + (hi - lo) * f64::from(k) / f64::from(SAMPLES);
453 let rows: Vec<Vec<f64>> = (0..=SAMPLES)
454 .map(|k| line_at(along_u, at(k)).map(|c| weighted(&c)))
455 .collect::<Option<_>>()?;
456 let count = rows[0].len();
457 if rows.iter().any(|r| r.len() != count) {
458 return None;
459 }
460 let widest = (0..count).max_by(|&i, &j| {
461 let spread = |i: usize| rows.iter().fold(0.0_f64, |m, r| m.max(r[i].abs()));
462 spread(i).total_cmp(&spread(j))
463 })?;
464 let mut roots = vec![lo, hi];
465 for k in 0..SAMPLES {
466 let (mut a, mut b) = (at(k), at(k + 1));
467 let (da, db) = (rows[k as usize][widest], rows[k as usize + 1][widest]);
468 if da == 0.0 {
469 roots.push(a);
470 continue;
471 }
472 if da * db > 0.0 {
473 continue;
474 }
475 let sign = da.signum();
476 for _ in 0..80 {
477 let mid = f64::midpoint(a, b);
478 let d = weighted(&line_at(along_u, mid)?)[widest];
479 if d * sign > 0.0 {
480 a = mid;
481 } else {
482 b = mid;
483 }
484 }
485 roots.push(f64::midpoint(a, b));
486 }
487 roots.sort_by(f64::total_cmp);
488 roots.dedup_by(|x, y| (*x - *y).abs() <= tol.parametric());
489 Some(
490 roots
491 .into_iter()
492 .filter(|&t| line_at(along_u, t).is_some_and(|c| lies_in(&c) && has_length(&c)))
493 .collect(),
494 )
495 };
496 let columns = found(true)?;
497 let rows = found(false)?;
498 if columns.is_empty() && rows.is_empty() {
499 return None;
500 }
501 let strip = |lines: &[f64], t: f64| lines.iter().filter(|&&x| x < t).count();
504 let near_line = |lines: &[f64], t: f64, span: f64| {
505 lines
506 .iter()
507 .any(|&x| (x - t).abs() <= span / f64::from(SAMPLES) * 0.25)
508 };
509 let mut sides: ogeom_core::FastMap<(usize, usize), f64> = ogeom_core::FastMap::default();
510 let band = tol.confusion() * 10.0;
511 for i in 0..=SAMPLES {
512 let u = u0 + (u1 - u0) * (f64::from(i) + 0.5) / f64::from(SAMPLES + 1);
513 if near_line(&columns, u, u1 - u0) {
514 continue;
515 }
516 for j in 0..=SAMPLES {
517 let v = v0 + (v1 - v0) * (f64::from(j) + 0.5) / f64::from(SAMPLES + 1);
518 if near_line(&rows, v, v1 - v0) {
519 continue;
520 }
521 let d = distance(spline.point_at(u, v, tol).ok()?);
522 if d.abs() <= band {
523 continue;
524 }
525 let cell = (strip(&columns, u), strip(&rows, v));
526 match sides.get(&cell) {
527 Some(side) if side * d < 0.0 => return None,
528 Some(_) => {}
529 None => {
530 sides.insert(cell, d.signum());
531 }
532 }
533 }
534 }
535 let closed_u = spline.is_closed_u(tol);
541 let closed_v = spline.is_closed_v(tol);
542 let side_of = |cu: Option<usize>, cv: Option<usize>| -> Option<f64> {
543 let mut found = sides
544 .iter()
545 .filter(|((u, v), _)| cu.is_none_or(|c| c == *u) && cv.is_none_or(|c| c == *v))
546 .map(|(_, s)| *s);
547 let first = found.next()?;
548 found.all(|s| s == first).then_some(first)
549 };
550 let crosses = |k: usize, count: usize, closed: bool, cell: &dyn Fn(usize) -> Option<f64>| {
551 let below = cell(k).or_else(|| closed.then(|| (0..=count).rev().find_map(cell)).flatten());
552 let above = cell(k + 1).or_else(|| closed.then(|| (0..=count).find_map(cell)).flatten());
553 matches!((below, above), (Some(x), Some(y)) if x * y < 0.0)
554 };
555 for k in 0..columns.len() {
556 if !crosses(k, columns.len(), closed_u, &|c| side_of(Some(c), None)) {
557 return None;
558 }
559 }
560 for k in 0..rows.len() {
561 if !crosses(k, rows.len(), closed_v, &|c| side_of(None, Some(c))) {
562 return None;
563 }
564 }
565 let mut sections = Vec::new();
566 let mut emit = |along_u: bool, t: f64| -> Option<()> {
567 let iso = line_at(along_u, t)?;
568 let curve: Curve = iso.into();
569 let range = curve.domain();
570 let chart: PlanarCurve = if along_u {
571 Line2d::over(
572 ogeom_math::Axis2::new(Point2::new(t, 0.0), ogeom_math::Direction2::Y),
573 range.0,
574 range.1,
575 )
576 } else {
577 Line2d::over(
578 ogeom_math::Axis2::new(Point2::new(0.0, t), ogeom_math::Direction2::X),
579 range.0,
580 range.1,
581 )
582 }
583 .ok()?
584 .into();
585 let flat = exact_pcurve(&curve, range, a_or_b(plane_first, a, b), tol)?;
586 let (on_a, on_b) = if plane_first {
587 (Some(flat), Some(chart))
588 } else {
589 (Some(chart), Some(flat))
590 };
591 sections.push(SectionCurve {
592 on_a,
593 on_b,
594 tolerance: 0.0,
595 exact: true,
596 closed: curve.is_closed(tol),
597 tangential: false,
598 curve,
599 });
600 Some(())
601 };
602 for (k, &u) in columns.iter().enumerate() {
605 if closed_u
606 && k + 1 == columns.len()
607 && k > 0
608 && columns[0] == u0
609 && (u - u1).abs() <= tol.parametric()
610 {
611 continue;
612 }
613 emit(true, u)?;
614 }
615 for (k, &v) in rows.iter().enumerate() {
616 if closed_v
617 && k + 1 == rows.len()
618 && k > 0
619 && rows[0] == v0
620 && (v - v1).abs() <= tol.parametric()
621 {
622 continue;
623 }
624 emit(false, v)?;
625 }
626 Some(sections)
627}
628
629fn a_or_b<'s>(first: bool, a: &'s SurfaceGeometry, b: &'s SurfaceGeometry) -> &'s SurfaceGeometry {
631 if first { a } else { b }
632}
633
634fn near_parallel_drums(
651 a: &SurfaceGeometry,
652 b: &SurfaceGeometry,
653 tol: Tolerances,
654) -> Option<Vec<SectionCurve>> {
655 const LEAN: f64 = 1e-3;
656 let (SurfaceGeometry::Cylinder(sa), SurfaceGeometry::Cylinder(sb)) = (a, b) else {
657 return None;
658 };
659 let (ca, cb) = (sa.cylinder(), sb.cylinder());
660 let (axis_a, axis_b) = (ca.axis(), cb.axis());
661 let (da, db) = (axis_a.direction.vector(), axis_b.direction.vector());
662 let (ra, rb) = (ca.radius(), cb.radius());
663 let cos = da.dot(db);
664 if da.cross(db).magnitude() > LEAN || cos.abs() < 0.5 {
665 return None;
666 }
667 let (pa, pb) = (axis_a.location, axis_b.location);
668 let (_, (a0, a1)) = a.domain();
670 let (_, (b0, b1)) = b.domain();
671 let along = |v: f64| (pb - pa).dot(da) + v * cos;
672 let (lo, hi) = (
673 a0.min(a1).max(along(b0).min(along(b1))),
674 a0.max(a1).min(along(b0).max(along(b1))),
675 );
676 if !(lo.is_finite() && hi.is_finite()) {
677 return None;
678 }
679 if hi - lo <= tol.confusion() {
680 return Some(Vec::new());
681 }
682 let meet = |z: f64| -> Option<[Point; 2]> {
686 let centre_a = pa + da * z;
687 let s = (centre_a - pb).dot(da) / cos;
688 let centre_b = pb + db * s;
689 let mut between = centre_b - centre_a;
690 between = between - da * between.dot(da);
691 let d = between.magnitude();
692 let margin = tol.confusion() * 50.0;
697 if d <= margin || d >= ra + rb - margin || d <= (ra - rb).abs() + margin {
698 return None;
699 }
700 let x = (d * d + ra * ra - rb * rb) / (2.0 * d);
701 let h = (ra * ra - x * x).max(0.0).sqrt();
702 let ex = between / d;
703 let ey = da.cross(ex);
704 Some([centre_a + ex * x + ey * h, centre_a + ex * x - ey * h])
705 };
706 lines_through_stations(lo, hi, meet, rb * (1.0 / cos.abs() - 1.0), tol)
707}
708
709const NEAR_PARALLEL_STRAY: f64 = 1e-5;
712
713fn lines_through_stations(
721 lo: f64,
722 hi: f64,
723 meet: impl Fn(f64) -> Option<[Point; 2]>,
724 stated: f64,
725 tol: Tolerances,
726) -> Option<Vec<SectionCurve>> {
727 const STATIONS: u32 = 32;
728 const STRAIGHT: f64 = 1e-6;
729 let at = |k: f64| (hi - lo).mul_add(k / f64::from(STATIONS), lo);
730 let heights: Vec<f64> = (0..=STATIONS).map(|k| at(f64::from(k))).collect();
731 let met: Vec<[Point; 2]> = heights.iter().map(|&z| meet(z)).collect::<Option<_>>()?;
732 let between: Vec<[Point; 2]> = (0..STATIONS)
733 .map(|k| meet(at(f64::from(k) + 0.5)))
734 .collect::<Option<_>>()?;
735 let mut out = Vec::with_capacity(2);
736 for side in 0..2 {
737 let (from, to) = (met[0][side], met[met.len() - 1][side]);
738 let span = to - from;
739 let length = span.magnitude();
740 if length <= tol.confusion() {
741 return None;
742 }
743 let off_line = |p: Point| {
744 let t = (p - from).dot(span) / (length * length);
745 p.distance(from + span * t)
746 };
747 let stray = met
748 .iter()
749 .chain(&between)
750 .map(|pair| off_line(pair[side]))
751 .fold(0.0_f64, f64::max);
752 let (curve, stray): (Curve, f64) = if stray <= STRAIGHT {
753 (
754 ogeom_geom::LineCurve::segment(from, to, tol).ok()?.into(),
755 stray,
756 )
757 } else {
758 let points: Vec<Point> = met.iter().map(|pair| pair[side]).collect();
759 let fitted =
760 ogeom_geom::fit::fit_points_at(&heights, &points, 3, tol.confusion(), tol).ok()?;
761 let curve: Curve = fitted.curve.into();
762 let mut worst = fitted.error;
763 for (k, pair) in (0..STATIONS).zip(&between) {
764 let p = curve.point_at(at(f64::from(k) + 0.5), tol).ok()?;
765 worst = worst.max(p.distance(pair[side]));
766 }
767 (curve, worst)
768 };
769 let tolerance = stray + stated + tol.confusion();
770 if tolerance > NEAR_PARALLEL_STRAY {
771 return None;
772 }
773 out.push(SectionCurve {
774 curve,
775 on_a: None,
776 on_b: None,
777 tolerance,
778 exact: false,
779 closed: false,
780 tangential: false,
781 });
782 }
783 Some(out)
784}
785
786fn ball_through_drum(
798 a: &SurfaceGeometry,
799 b: &SurfaceGeometry,
800 tol: Tolerances,
801) -> Option<Vec<SectionCurve>> {
802 const SAMPLES: u32 = 256;
803 const STRAY: f64 = 1e-5;
804 let (ball, drum, ball_first) = match (a, b) {
805 (SurfaceGeometry::Sphere(s), SurfaceGeometry::Cylinder(c)) => (s, c, true),
806 (SurfaceGeometry::Cylinder(c), SurfaceGeometry::Sphere(s)) => (s, c, false),
807 _ => return None,
808 };
809 let (sphere, cylinder) = (ball.sphere(), drum.cylinder());
810 let frame = cylinder.frame();
811 let (x, y, d) = (frame.x().vector(), frame.y().vector(), frame.z().vector());
812 let (origin, r) = (frame.origin(), cylinder.radius());
813 let (centre, big) = (sphere.centre(), sphere.radius());
814 let ball_frame = sphere.frame();
815 let (_, (h0, h1)) = drum.domain();
816 let margin = r * 0.1;
820 let heights = |angle: f64| -> Option<[f64; 2]> {
822 let foot = origin + (x * angle.cos() + y * angle.sin()) * r;
823 let w = foot - centre;
824 let half = d.dot(w);
825 let disc = half.mul_add(half, -(w.dot(w) - big * big));
826 if disc <= margin * margin {
827 return None;
828 }
829 let root = disc.sqrt();
830 let pair = [-half - root, -half + root];
831 pair.iter().all(|v| *v >= h0 && *v <= h1).then_some(pair)
832 };
833 let at = |angle: f64, v: f64| origin + (x * angle.cos() + y * angle.sin()) * r + d * v;
834 let on_ball = |p: Point, before: Option<Point2>| -> Point2 {
836 let local = ball_frame.to_local(p);
837 let lat = local.z.atan2(local.x.hypot(local.y));
838 let mut lon = local.y.atan2(local.x).rem_euclid(core::f64::consts::TAU);
839 if let Some(prev) = before {
840 while lon - prev.x > core::f64::consts::PI {
841 lon -= core::f64::consts::TAU;
842 }
843 while prev.x - lon > core::f64::consts::PI {
844 lon += core::f64::consts::TAU;
845 }
846 }
847 Point2::new(lon, lat)
848 };
849 let angle_of = |k: f64| core::f64::consts::TAU * k / f64::from(SAMPLES);
850 let params: Vec<f64> = (0..=SAMPLES).map(|k| angle_of(f64::from(k))).collect();
851 let mut sampled: Vec<[f64; 2]> = Vec::with_capacity(params.len());
852 for &angle in ¶ms {
853 sampled.push(heights(angle)?);
854 }
855 let mut out = Vec::with_capacity(2);
856 for side in 0..2 {
857 let points: Vec<Point> = params
858 .iter()
859 .zip(&sampled)
860 .map(|(&angle, pair)| at(angle, pair[side]))
861 .collect();
862 let on_drum: Vec<Point2> = params
863 .iter()
864 .zip(&sampled)
865 .map(|(&angle, pair)| Point2::new(angle, pair[side]))
866 .collect();
867 let mut on_sphere: Vec<Point2> = Vec::with_capacity(points.len());
868 for p in &points {
869 let q = on_ball(*p, on_sphere.last().copied());
870 on_sphere.push(q);
871 }
872 let target = tol.confusion() * 10.0;
873 let curve: Curve = ogeom_geom::fit::fit_points_at(¶ms, &points, 3, target, tol)
874 .ok()?
875 .curve
876 .into();
877 let drum_image: PlanarCurve =
878 ogeom_geom::fit::fit_points_2d_at(¶ms, &on_drum, 3, target, tol)
879 .ok()?
880 .curve
881 .into();
882 let ball_image: PlanarCurve =
883 ogeom_geom::fit::fit_points_2d_at(¶ms, &on_sphere, 3, target, tol)
884 .ok()?
885 .curve
886 .into();
887 let mut stray = 0.0_f64;
890 for k in 0..(2 * SAMPLES) {
891 let angle = angle_of(f64::from(k) / 2.0);
892 let truth = at(angle, heights(angle)?[side]);
893 let on_curve = curve.point_at(angle, tol).ok()?;
894 let uv = drum_image.point_at(angle, tol).ok()?;
895 let through_drum = drum.point_at(uv.x, uv.y, tol).ok()?;
896 let uv = ball_image.point_at(angle, tol).ok()?;
897 let through_ball = ball.point_at(uv.x, uv.y, tol).ok()?;
898 stray = stray
899 .max(truth.distance(on_curve))
900 .max(truth.distance(through_drum))
901 .max(truth.distance(through_ball));
902 }
903 let tolerance = stray.max(tol.confusion());
904 if tolerance > STRAY {
905 return None;
906 }
907 let (on_a, on_b) = if ball_first {
908 (ball_image, drum_image)
909 } else {
910 (drum_image, ball_image)
911 };
912 out.push(SectionCurve {
913 curve,
914 on_a: Some(on_a),
915 on_b: Some(on_b),
916 tolerance,
917 exact: false,
918 closed: true,
919 tangential: false,
920 });
921 }
922 Some(out)
923}
924
925fn near_parallel_plane_drum(
937 a: &SurfaceGeometry,
938 b: &SurfaceGeometry,
939 tol: Tolerances,
940) -> Option<Vec<SectionCurve>> {
941 const LEAN: f64 = 1e-3;
942 const SPAN: f64 = 3e4;
943 let (plane, drum, surface) = match (a, b) {
944 (SurfaceGeometry::Plane(p), SurfaceGeometry::Cylinder(c)) => (p.plane(), c.cylinder(), b),
945 (SurfaceGeometry::Cylinder(c), SurfaceGeometry::Plane(p)) => (p.plane(), c.cylinder(), a),
946 _ => return None,
947 };
948 let axis = drum.axis();
949 let (d, r) = (axis.direction.vector(), drum.radius());
950 let n = plane.normal().vector();
951 let lean = n.dot(d).abs();
952 if lean <= tol.angular() || lean > LEAN || r / lean < SPAN {
957 return None;
958 }
959 let across = n - d * n.dot(d);
960 let k = across.magnitude();
961 let e1 = across / k;
962 let e2 = d.cross(e1);
963 let (_, (lo, hi)) = surface.domain();
964 if !(lo.is_finite() && hi.is_finite()) || hi - lo <= tol.confusion() {
965 return None;
966 }
967 let meet = |z: f64| -> Option<[Point; 2]> {
968 let centre = axis.location + d * z;
969 let u = -plane.signed_distance_to(centre) / k;
970 let margin = tol.confusion() * 1e3;
971 if u.abs() >= r - margin {
972 return None;
973 }
974 let w = r.mul_add(r, -(u * u)).sqrt();
975 Some([centre + e1 * u + e2 * w, centre + e1 * u - e2 * w])
976 };
977 lines_through_stations(lo, hi, meet, 0.0, tol)
978}
979
980fn exact_section(
995 curve: Curve,
996 a: &SurfaceGeometry,
997 b: &SurfaceGeometry,
998 tol: Tolerances,
999) -> Option<SectionCurve> {
1000 let closed = match &curve {
1001 Curve::Circle(_) | Curve::Ellipse(_) => true,
1002 _ => curve.is_closed(tol),
1003 };
1004 let range = curve.domain();
1005 let on_a = exact_pcurve(&curve, range, a, tol);
1006 let on_b = exact_pcurve(&curve, range, b, tol);
1007
1008 if let Curve::Line(_) = &curve {
1009 let mut interval = curve.domain();
1012 if let Some(p) = &on_a {
1013 interval = intersect_intervals(interval, inside_box(p, a))?;
1014 }
1015 if let Some(p) = &on_b {
1016 interval = intersect_intervals(interval, inside_box(p, b))?;
1017 }
1018 let (lo, hi) = interval;
1019 let Curve::Line(line) = &curve else {
1020 unreachable!()
1021 };
1022 let clipped: Curve = ogeom_geom::LineCurve::over(line.axis(), lo, hi)
1023 .ok()?
1024 .into();
1025 let clip2 = |p: &PlanarCurve| -> Option<PlanarCurve> {
1026 let PlanarCurve::Line(l) = p else {
1027 return Some(p.clone());
1028 };
1029 Some(Line2d::over(l.axis(), lo, hi).ok()?.into())
1030 };
1031 let (ca, cb) = (on_a.as_ref().and_then(clip2), on_b.as_ref().and_then(clip2));
1032 let tangential = touching_along(&clipped, ca.as_ref(), cb.as_ref(), a, b, tol);
1033 return Some(SectionCurve {
1034 on_a: ca,
1035 on_b: cb,
1036 tolerance: 0.0,
1037 exact: true,
1038 closed: false,
1039 tangential,
1040 curve: clipped,
1041 });
1042 }
1043
1044 for (pcurve, surface) in [(&on_a, a), (&on_b, b)] {
1047 if let Some(p) = pcurve
1048 && !touches_box(p, surface, tol)
1049 {
1050 return None;
1051 }
1052 }
1053 let tangential = touching_along(&curve, on_a.as_ref(), on_b.as_ref(), a, b, tol);
1054 Some(SectionCurve {
1055 on_a,
1056 on_b,
1057 tolerance: 0.0,
1058 exact: true,
1059 closed,
1060 tangential,
1061 curve,
1062 })
1063}
1064
1065fn touching_along(
1074 curve: &Curve,
1075 on_a: Option<&PlanarCurve>,
1076 on_b: Option<&PlanarCurve>,
1077 a: &SurfaceGeometry,
1078 b: &SurfaceGeometry,
1079 tol: Tolerances,
1080) -> bool {
1081 let sample_uv = |pc: Option<&PlanarCurve>,
1089 surface: &SurfaceGeometry,
1090 t: f64|
1091 -> Option<ogeom_math::Point2> {
1092 if let Some(pc) = pc {
1093 return pc.point_at(t, tol).ok();
1094 }
1095 let p = curve.point_at(t, tol).ok()?;
1096 chart_inversion(surface, p, tol)
1097 };
1098 let (lo, hi) = curve.domain();
1099 let mut judged = 0_usize;
1105 for f in [0.07, 0.19, 0.37, 0.53, 0.71, 0.89] {
1106 let t = (hi - lo).mul_add(f, lo);
1107 let (Some(ua), Some(ub)) = (sample_uv(on_a, a, t), sample_uv(on_b, b, t)) else {
1108 continue;
1109 };
1110 let (Ok(na), Ok(nb)) = (a.normal_at(ua.x, ua.y, tol), b.normal_at(ub.x, ub.y, tol)) else {
1111 continue;
1112 };
1113 if na.vector().cross(nb.vector()).magnitude() > 1e-6 {
1114 return false;
1115 }
1116 judged += 1;
1117 }
1118 judged >= 3
1119}
1120
1121fn chart_inversion(
1123 surface: &SurfaceGeometry,
1124 p: ogeom_math::Point,
1125 tol: Tolerances,
1126) -> Option<ogeom_math::Point2> {
1127 use ogeom_math::elementary;
1128 let (u, v) = match surface {
1129 SurfaceGeometry::Plane(s) => elementary::plane_parameters(&s.plane(), p),
1130 SurfaceGeometry::Cylinder(s) => {
1131 elementary::cylinder_parameters(&s.cylinder(), p, tol).ok()?
1132 }
1133 SurfaceGeometry::Cone(s) => elementary::cone_parameters(&s.cone(), p, tol).ok()?,
1134 SurfaceGeometry::Sphere(s) => elementary::sphere_parameters(&s.sphere(), p, tol).ok()?,
1135 SurfaceGeometry::Torus(s) => elementary::torus_parameters(&s.torus(), p, tol).ok()?,
1136 _ => return None,
1137 };
1138 Some(ogeom_math::Point2::new(u, v))
1139}
1140
1141fn inside_box(pcurve: &PlanarCurve, surface: &SurfaceGeometry) -> Option<(f64, f64)> {
1144 let (o, d) = match pcurve {
1147 PlanarCurve::Line(line) => {
1148 let axis = line.axis();
1149 (axis.location, axis.direction.vector())
1150 }
1151 PlanarCurve::BSpline(spline)
1152 if spline.knots().degree() == 1 && spline.control_points().len() == 2 =>
1153 {
1154 let (t0, t1) = spline.knots().domain();
1155 let (p0, p1) = (
1156 spline.control_points()[0].point(),
1157 spline.control_points()[1].point(),
1158 );
1159 if t1 <= t0 {
1160 return None;
1161 }
1162 let rate = (p1 - p0) / (t1 - t0);
1163 (p0 - rate * t0, rate)
1164 }
1165 _ => return None,
1166 };
1167 let ((ua, ub), (va, vb)) = surface.domain();
1168
1169 let mut lo = f64::NEG_INFINITY;
1171 let mut hi = f64::INFINITY;
1172 for (origin, direction, low, high) in [(o.x, d.x, ua, ub), (o.y, d.y, va, vb)] {
1173 if direction.abs() <= f64::MIN_POSITIVE {
1174 if origin < low || origin > high {
1175 return None;
1176 }
1177 continue;
1178 }
1179 let (a, b) = ((low - origin) / direction, (high - origin) / direction);
1180 let (near, far) = if a < b { (a, b) } else { (b, a) };
1181 lo = lo.max(near);
1182 hi = hi.min(far);
1183 }
1184 if lo >= hi {
1185 return None;
1186 }
1187 Some((lo, hi))
1188}
1189
1190fn touches_box(pcurve: &PlanarCurve, surface: &SurfaceGeometry, tol: Tolerances) -> bool {
1192 use ogeom_geom::Curve2d;
1193 let ((ua, ub), (va, vb)) = surface.domain();
1194 let (lo, hi) = pcurve.domain();
1195 const SPANS: u32 = 64;
1204 let points: Vec<Option<ogeom_math::Point2>> = (0..=SPANS)
1205 .map(|i| {
1206 pcurve
1207 .point_at(lo + (hi - lo) * f64::from(i) / f64::from(SPANS), tol)
1208 .ok()
1209 })
1210 .collect();
1211 points.windows(2).any(|pair| {
1212 let (Some(p), Some(q)) = (pair[0], pair[1]) else {
1213 return false;
1214 };
1215 let pad = p.distance(q);
1216 let u_ok =
1218 surface.is_periodic_u() || (p.x.max(q.x) + pad >= ua && p.x.min(q.x) - pad <= ub);
1219 let v_ok =
1220 surface.is_periodic_v() || (p.y.max(q.y) + pad >= va && p.y.min(q.y) - pad <= vb);
1221 u_ok && v_ok
1222 })
1223}
1224
1225fn intersect_intervals(a: (f64, f64), b: Option<(f64, f64)>) -> Option<(f64, f64)> {
1227 let b = b?;
1228 let (lo, hi) = (a.0.max(b.0), a.1.min(b.1));
1229 if lo >= hi {
1230 return None;
1231 }
1232 Some((lo, hi))
1233}
1234
1235fn marched(
1237 a: &SurfaceGeometry,
1238 b: &SurfaceGeometry,
1239 options: IntersectOptions,
1240 tol: Tolerances,
1241) -> OgeomResult<SurfaceIntersection> {
1242 let traced = branches(a, b, options.marching, tol)?;
1243 if traced.is_empty() {
1244 return Ok(SurfaceIntersection::Apart);
1245 }
1246 let mut out = Vec::with_capacity(traced.len());
1247 let mut contacts: Vec<crate::march::Traced> = Vec::new();
1248 for branch in &traced {
1249 if branch_is_tangential(a, b, branch, tol)? {
1258 if let Some(contact) = walk_contact(a, b, branch, &contacts, options.marching, tol)? {
1259 contacts.push(contact);
1260 }
1261 continue;
1262 }
1263 if branch.stopped == crate::march::Stopped::RanOut {
1264 ogeom_bail!(
1265 NotDone,
1266 "a marched section ran out of its point budget before \
1267 finishing; the seam is longer than the chord affords and \
1268 fitting the truncation would state a curve that is not there"
1269 );
1270 }
1271 for fitted in fitted_in_pieces(a, b, branch, options.tolerance, tol)? {
1280 out.push(SectionCurve {
1281 curve: fitted.curve.into(),
1282 on_a: Some(fitted.on_a.into()),
1283 on_b: Some(fitted.on_b.into()),
1284 tolerance: options.marching.chord + fitted.fit_error,
1287 exact: false,
1288 closed: fitted.closed,
1289 tangential: false,
1290 });
1291 }
1292 }
1293 for contact in &contacts {
1294 let fitted = approximate_branch(a, b, contact, options.tolerance, tol)?;
1295 out.push(SectionCurve {
1296 curve: fitted.curve.into(),
1297 on_a: Some(fitted.on_a.into()),
1298 on_b: Some(fitted.on_b.into()),
1299 tolerance: options.marching.chord + fitted.fit_error,
1300 exact: false,
1301 closed: fitted.closed,
1302 tangential: true,
1303 });
1304 }
1305 if out.is_empty() {
1306 return Ok(SurfaceIntersection::Apart);
1307 }
1308 Ok(SurfaceIntersection::Along(out))
1309}
1310
1311fn fitted_in_pieces(
1331 a: &SurfaceGeometry,
1332 b: &SurfaceGeometry,
1333 branch: &crate::march::Traced,
1334 tolerance: f64,
1335 tol: Tolerances,
1336) -> OgeomResult<Vec<crate::approx::IntersectionCurve>> {
1337 const DEPTH: u32 = 6;
1338 const FLOOR: usize = 16;
1339 fn go(
1340 a: &SurfaceGeometry,
1341 b: &SurfaceGeometry,
1342 branch: &crate::march::Traced,
1343 tolerance: f64,
1344 depth: u32,
1345 tol: Tolerances,
1346 ) -> OgeomResult<Vec<crate::approx::IntersectionCurve>> {
1347 let whole = approximate_branch(a, b, branch, tolerance, tol)?;
1348 let points = &branch.points;
1352 let inner = points.get(1..points.len().saturating_sub(1)).unwrap_or(&[]);
1353 let step = inner
1354 .windows(2)
1355 .map(|w| w[0].distance(w[1]))
1356 .fold(f64::INFINITY, f64::min);
1357 if whole.met
1358 || whole.fit_error <= step.min(tolerance * 1e2)
1359 || depth == 0
1360 || branch.points.len() < 2 * FLOOR
1361 {
1362 return Ok(vec![whole]);
1363 }
1364 let middle = branch.points.len() / 2;
1365 let stopped = if branch.closed() {
1367 crate::march::Stopped::Stalled
1368 } else {
1369 branch.stopped
1370 };
1371 let half = |range: core::ops::RangeInclusive<usize>| crate::march::Traced {
1372 points: branch.points[range.clone()].to_vec(),
1373 on_a: branch.on_a[range.clone()].to_vec(),
1374 on_b: branch.on_b[range].to_vec(),
1375 stopped,
1376 };
1377 let mut pieces = go(a, b, &half(0..=middle), tolerance, depth - 1, tol)?;
1378 pieces.extend(go(
1379 a,
1380 b,
1381 &half(middle..=branch.points.len() - 1),
1382 tolerance,
1383 depth - 1,
1384 tol,
1385 )?);
1386 let worst = pieces.iter().map(|p| p.fit_error).fold(0.0_f64, f64::max);
1389 Ok(if worst < whole.fit_error {
1390 pieces
1391 } else {
1392 vec![whole]
1393 })
1394 }
1395 go(a, b, branch, tolerance, DEPTH, tol)
1396}
1397
1398fn walk_contact(
1407 a: &SurfaceGeometry,
1408 b: &SurfaceGeometry,
1409 fragment: &crate::march::Traced,
1410 already: &[crate::march::Traced],
1411 marching: Marching,
1412 tol: Tolerances,
1413) -> OgeomResult<Option<crate::march::Traced>> {
1414 let middle = fragment.points.len() / 2;
1415 let Some(point) = fragment.points.get(middle).copied() else {
1416 return Ok(None);
1417 };
1418 for traced in already {
1419 let spacing = traced
1422 .points
1423 .windows(2)
1424 .map(|w| w[0].distance(w[1]))
1425 .fold(0.0f64, f64::max);
1426 let near = traced
1427 .points
1428 .iter()
1429 .map(|p| p.distance(point))
1430 .fold(f64::INFINITY, f64::min);
1431 if near <= spacing.mul_add(0.5, marching.chord.max(tol.confusion())) {
1432 return Ok(None);
1433 }
1434 }
1435 let seed = crate::march::Contact {
1436 point,
1437 on_a: fragment.on_a[middle],
1438 on_b: fragment.on_b[middle],
1439 };
1440 Ok(trace_tangential(a, b, seed, marching, tol)
1445 .ok()
1446 .filter(|traced| traced.points.len() >= 4))
1447}
1448
1449fn branch_is_tangential(
1452 a: &SurfaceGeometry,
1453 b: &SurfaceGeometry,
1454 branch: &crate::march::Traced,
1455 tol: Tolerances,
1456) -> OgeomResult<bool> {
1457 use ogeom_geom::Surface as _;
1458 let count = branch.points.len();
1459 if count == 0 {
1460 return Ok(true);
1461 }
1462 for k in 0..5 {
1463 let i = (k * (count - 1)) / 4;
1464 let (ua, va) = branch.on_a[i.min(count - 1)];
1465 let (ub, vb) = branch.on_b[i.min(count - 1)];
1466 let (dau, dav) = a.d1_at(ua, va, tol)?;
1467 let (dbu, dbv) = b.d1_at(ub, vb, tol)?;
1468 let na = dau.cross(dav);
1469 let nb = dbu.cross(dbv);
1470 let (ma, mb) = (na.magnitude(), nb.magnitude());
1471 if ma <= tol.confusion() || mb <= tol.confusion() {
1472 continue;
1473 }
1474 if na.cross(nb).magnitude() / (ma * mb) > 3e-2 {
1481 return Ok(false);
1482 }
1483 }
1484 Ok(true)
1485}
1486
1487#[must_use]
1495pub fn exact_pcurve_of(
1496 curve: &Curve,
1497 surface: &SurfaceGeometry,
1498 tol: Tolerances,
1499) -> Option<PlanarCurve> {
1500 exact_pcurve(curve, curve.domain(), surface, tol)
1501}
1502
1503#[must_use]
1512pub fn exact_pcurve_over(
1513 curve: &Curve,
1514 range: (f64, f64),
1515 surface: &SurfaceGeometry,
1516 tol: Tolerances,
1517) -> Option<PlanarCurve> {
1518 exact_pcurve(curve, range, surface, tol)
1519}
1520
1521fn exact_pcurve(
1531 curve: &Curve,
1532 range: (f64, f64),
1533 surface: &SurfaceGeometry,
1534 tol: Tolerances,
1535) -> Option<PlanarCurve> {
1536 if let Curve::Trimmed(trimmed) = curve
1543 && !trimmed.is_reversed()
1544 {
1545 let window = ogeom_geom::Curve3d::domain(&**trimmed);
1546 let basis = exact_pcurve(trimmed.basis(), range, surface, tol)?;
1547 return ogeom_geom::Trimmed2d::new(basis, window.0, window.1, tol)
1548 .ok()
1549 .map(Into::into);
1550 }
1551 let forward = match curve {
1555 Curve::Circle(c) if c.is_reversed() => Some(Curve::Circle(c.forward(tol).ok()?)),
1556 Curve::Ellipse(e) if e.is_reversed() => Some(Curve::Ellipse(e.forward(tol).ok()?)),
1557 _ => None,
1558 };
1559 if let Some(forward) = forward {
1560 return exact_pcurve(&forward, range, surface, tol);
1561 }
1562 match surface {
1563 SurfaceGeometry::Plane(p) => on_plane(curve, p.plane(), tol),
1564 SurfaceGeometry::Cylinder(c) => on_cylinder(curve, range, c.cylinder(), tol),
1565 SurfaceGeometry::Sphere(s) => on_sphere(curve, range, s.sphere(), tol),
1566 SurfaceGeometry::Torus(t) => on_torus(curve, range, t.torus(), tol),
1567 SurfaceGeometry::Cone(c) => on_cone(curve, range, c.cone(), tol),
1568 _ => None,
1569 }
1570}
1571
1572fn circle_window(range: (f64, f64)) -> (f64, f64) {
1578 let tau = core::f64::consts::TAU;
1579 if range.0.is_finite() && range.1.is_finite() {
1580 (range.0.min(range.1).min(0.0), range.0.max(range.1).max(tau))
1581 } else {
1582 (0.0, tau)
1583 }
1584}
1585
1586fn on_cone(
1595 curve: &Curve,
1596 range: (f64, f64),
1597 cone: ogeom_math::Cone,
1598 tol: Tolerances,
1599) -> Option<PlanarCurve> {
1600 let frame = cone.frame();
1601 let axis_z = frame.z().vector();
1602 let tau = core::f64::consts::TAU;
1603 match curve {
1604 Curve::Circle(c) => {
1605 let circle = c.circle();
1606 if circle.frame().z().vector().cross(axis_z).magnitude() > tol.angular() {
1607 return None;
1608 }
1609 let local = frame.to_local(circle.centre());
1610 if local.x.hypot(local.y) > tol.confusion() {
1611 return None;
1612 }
1613 let expected = cone
1617 .half_angle()
1618 .tan()
1619 .mul_add(local.z, cone.reference_radius());
1620 let turned = if (expected - circle.radius()).abs() <= tol.confusion() * 10.0 {
1621 0.0
1622 } else if (expected + circle.radius()).abs() <= tol.confusion() * 10.0 {
1623 core::f64::consts::PI
1624 } else {
1625 return None;
1626 };
1627 let start = circle.centre() + circle.frame().x().vector() * circle.radius();
1628 let at = frame.to_local(start);
1629 let phase = at.y.atan2(at.x) + turned;
1630 let winding = circle.frame().z().vector().dot(axis_z).signum();
1631 let towards =
1632 ogeom_math::Direction2::new(ogeom_math::Vector2::new(winding, 0.0), tol).ok()?;
1633 Some(
1634 Line2d::over(
1635 ogeom_math::Axis2::new(Point2::new(phase, local.z), towards),
1636 circle_window(range).0,
1637 circle_window(range).1,
1638 )
1639 .ok()?
1640 .into(),
1641 )
1642 }
1643 Curve::Line(line) => {
1644 let axis = line.axis();
1647 let on = |t: f64| {
1648 let p = axis.location + axis.direction.vector() * t;
1649 cone.distance_to(p) <= tol.confusion() * 10.0
1650 };
1651 if !on(0.0) || !on(1.0) || !on(-1.0) {
1652 return None;
1653 }
1654 let (lo, hi) = if range.0.is_finite() && range.1.is_finite() && range.0 != range.1 {
1661 range
1662 } else {
1663 line.domain()
1664 };
1665 let mut local: Option<ogeom_math::Point> = None;
1671 for t in [lo, hi] {
1672 if !t.is_finite() {
1673 continue;
1674 }
1675 let candidate = frame.to_local(axis.location + axis.direction.vector() * t);
1676 if local.is_none_or(|held| candidate.x.hypot(candidate.y) > held.x.hypot(held.y)) {
1677 local = Some(candidate);
1678 }
1679 }
1680 let local = local?;
1681 if local.x.hypot(local.y) <= tol.confusion() {
1682 return None;
1683 }
1684 let u = local.y.atan2(local.x).rem_euclid(tau);
1685 let v_at = |t: f64| {
1689 frame
1690 .to_local(axis.location + axis.direction.vector() * t)
1691 .z
1692 };
1693 let knots = ogeom_math::KnotVector::new(vec![lo, lo, hi, hi], 1).ok()?;
1694 Some(
1695 ogeom_geom::BSpline2d::new(
1696 knots,
1697 vec![Point2::new(u, v_at(lo)), Point2::new(u, v_at(hi))],
1698 tol,
1699 )
1700 .ok()?
1701 .into(),
1702 )
1703 }
1704 _ => None,
1705 }
1706}
1707
1708fn on_torus(
1718 curve: &Curve,
1719 range: (f64, f64),
1720 torus: ogeom_math::Torus,
1721 tol: Tolerances,
1722) -> Option<PlanarCurve> {
1723 let Curve::Circle(c) = curve else {
1724 return None;
1725 };
1726 let circle = c.circle();
1727 let frame = torus.frame();
1728 let axis_z = frame.z().vector();
1729 let normal = circle.frame().z().vector();
1730 let local = frame.to_local(circle.centre());
1731
1732 if normal.cross(axis_z).magnitude() <= tol.angular()
1734 && local.x.hypot(local.y) <= tol.confusion()
1735 {
1736 let sin_v = local.z / torus.minor_radius();
1737 let (cos_v, turned) = [
1741 (circle.radius() - torus.major_radius(), 0.0),
1742 (
1743 -circle.radius() - torus.major_radius(),
1744 core::f64::consts::PI,
1745 ),
1746 ]
1747 .into_iter()
1748 .map(|(reach, turned)| (reach / torus.minor_radius(), turned))
1749 .find(|(cos_v, _)| (sin_v.hypot(*cos_v) - 1.0).abs() <= tol.confusion())?;
1750 let v = sin_v.atan2(cos_v);
1751 let start = circle.centre() + circle.frame().x().vector() * circle.radius();
1752 let at = frame.to_local(start);
1753 let phase = at.y.atan2(at.x) + turned;
1754 let winding = normal.dot(axis_z).signum();
1755 let towards =
1756 ogeom_math::Direction2::new(ogeom_math::Vector2::new(winding, 0.0), tol).ok()?;
1757 return Some(
1758 Line2d::over(
1759 ogeom_math::Axis2::new(Point2::new(phase, v), towards),
1760 circle_window(range).0,
1761 circle_window(range).1,
1762 )
1763 .ok()?
1764 .into(),
1765 );
1766 }
1767
1768 if (circle.radius() - torus.minor_radius()).abs() <= tol.confusion()
1770 && normal.dot(axis_z).abs() <= tol.angular()
1771 && (local.x.hypot(local.y) - torus.major_radius()).abs() <= tol.confusion()
1772 && local.z.abs() <= tol.confusion()
1773 {
1774 let u = local.y.atan2(local.x);
1775 let radial = frame.x().vector() * u.cos() + frame.y().vector() * u.sin();
1776 let xc = circle.frame().x().vector();
1777 let phase = xc.dot(axis_z).atan2(xc.dot(radial));
1778 let winding = normal.dot(radial.cross(axis_z)).signum();
1779 let towards =
1780 ogeom_math::Direction2::new(ogeom_math::Vector2::new(0.0, winding), tol).ok()?;
1781 return Some(
1782 Line2d::over(
1783 ogeom_math::Axis2::new(Point2::new(u, phase), towards),
1784 circle_window(range).0,
1785 circle_window(range).1,
1786 )
1787 .ok()?
1788 .into(),
1789 );
1790 }
1791 None
1792}
1793
1794fn on_plane(curve: &Curve, plane: ogeom_math::Plane, tol: Tolerances) -> Option<PlanarCurve> {
1800 let frame = plane.frame();
1801 let flat = |p: Point| {
1802 let local = frame.to_local(p);
1803 Point2::new(local.x, local.y)
1804 };
1805 let flat_direction = |d: ogeom_math::Direction| {
1810 let local = frame.vector_to_local(d.vector());
1811 ogeom_math::Direction2::new(ogeom_math::Vector2::new(local.x, local.y), tol).ok()
1812 };
1813 match curve {
1814 Curve::Line(line) => {
1815 let axis = line.axis();
1816 let through = flat(axis.location);
1817 let direction = flat_direction(axis.direction)?;
1818 let (lo, hi) = line.domain();
1819 Some(
1820 Line2d::over(ogeom_math::Axis2::new(through, direction), lo, hi)
1821 .ok()?
1822 .into(),
1823 )
1824 }
1825 Curve::Circle(c) => {
1826 let circle = c.circle();
1827 let frame2 = Frame2::from_axes(
1828 flat(circle.centre()),
1829 flat_direction(circle.frame().x())?,
1830 flat_direction(circle.frame().y())?,
1831 tol,
1832 )
1833 .ok()?;
1834 Some(Circle2d::new(Circle2::new(frame2, circle.radius(), tol).ok()?).into())
1835 }
1836 Curve::Ellipse(e) => {
1837 let ellipse = e.ellipse();
1838 let frame2 = Frame2::from_axes(
1839 flat(ellipse.centre()),
1840 flat_direction(ellipse.frame().x())?,
1841 flat_direction(ellipse.frame().y())?,
1842 tol,
1843 )
1844 .ok()?;
1845 Some(
1846 Ellipse2d::new(
1847 Ellipse2::new(frame2, ellipse.major_radius(), ellipse.minor_radius(), tol)
1848 .ok()?,
1849 )
1850 .into(),
1851 )
1852 }
1853 Curve::BSpline(b) => {
1854 let control = b
1859 .control_points()
1860 .iter()
1861 .map(|w| ogeom_math::Weighted::new(flat((*w).point()), w.weight, tol))
1862 .collect::<Result<Vec<_>, _>>()
1863 .ok()?;
1864 Some(
1865 ogeom_geom::BSpline2d::rational(b.knots().clone(), control)
1866 .ok()?
1867 .into(),
1868 )
1869 }
1870 _ => None,
1871 }
1872}
1873
1874fn on_cylinder(
1881 curve: &Curve,
1882 range: (f64, f64),
1883 cylinder: ogeom_math::Cylinder,
1884 tol: Tolerances,
1885) -> Option<PlanarCurve> {
1886 let axis = cylinder.axis();
1887 let frame = cylinder.frame();
1888 match curve {
1889 Curve::Line(line) => {
1890 let direction = line.axis().direction;
1892 let along = direction.dot(axis.direction);
1893 if !direction.is_parallel(axis.direction, tol) {
1894 return None;
1895 }
1896 let through = line.axis().location;
1897 if (axis.distance_to(through) - cylinder.radius()).abs() > tol.confusion() {
1898 return None;
1899 }
1900 let local = frame.to_local(through);
1901 let u = local.y.atan2(local.x).rem_euclid(core::f64::consts::TAU);
1902 let (lo, hi) = line.domain();
1906 let start = Point2::new(u, local.z);
1907 let towards =
1908 ogeom_math::Direction2::new(ogeom_math::Vector2::new(0.0, along.signum()), tol)
1909 .ok()?;
1910 Some(
1911 Line2d::over(ogeom_math::Axis2::new(start, towards), lo, hi)
1912 .ok()?
1913 .into(),
1914 )
1915 }
1916 Curve::Circle(c) => {
1917 let circle = c.circle();
1918 if circle
1920 .frame()
1921 .z()
1922 .cross_with(axis.direction.vector())
1923 .magnitude()
1924 > tol.angular()
1925 {
1926 return None;
1927 }
1928 if axis.distance_to(circle.centre()) > tol.confusion() {
1929 return None;
1930 }
1931 if (circle.radius() - cylinder.radius()).abs() > tol.confusion() {
1932 return None;
1933 }
1934 let local = frame.to_local(circle.centre());
1935 let start = circle.centre() + circle.frame().x().vector() * circle.radius();
1944 let at = frame.to_local(start);
1945 let phase = at.y.atan2(at.x);
1946 let winding = circle.frame().z().dot(axis.direction).signum();
1947 let towards =
1948 ogeom_math::Direction2::new(ogeom_math::Vector2::new(winding, 0.0), tol).ok()?;
1949 Some(
1950 Line2d::over(
1951 ogeom_math::Axis2::new(Point2::new(phase, local.z), towards),
1952 circle_window(range).0,
1953 circle_window(range).1,
1954 )
1955 .ok()?
1956 .into(),
1957 )
1958 }
1959 Curve::Ellipse(_) => {
1960 use ogeom_geom::Curve3d as _;
1966 let tau = core::f64::consts::TAU;
1967 let local = |t: f64| -> Option<ogeom_math::Point> {
1968 Some(frame.to_local(curve.point_at(t, tol).ok()?))
1969 };
1970 let l0 = local(0.0)?;
1971 let lq = local(tau / 4.0)?;
1972 let lh = local(tau / 2.0)?;
1973 let r = cylinder.radius();
1975 for l in [&l0, &lq, &lh] {
1976 if (l.x.hypot(l.y) - r).abs() > tol.confusion() * 10.0 {
1977 return None;
1978 }
1979 }
1980 let phase = l0.y.atan2(l0.x);
1981 let uq = lq.y.atan2(lq.x);
1984 let step = (uq - phase).rem_euclid(tau);
1985 let winding = if (step - tau / 4.0).abs() < 1e-6 {
1986 1.0
1987 } else if (step - 3.0 * tau / 4.0).abs() < 1e-6 {
1988 -1.0
1989 } else {
1990 return None;
1991 };
1992 let c0 = f64::midpoint(l0.z, lh.z);
1994 let a = (l0.z - lh.z) / 2.0;
1995 let b = lq.z - c0;
1996 let candidate = ogeom_geom::Trig2d::new(
2000 Point2::new(phase, c0),
2001 ogeom_math::Vector2::new(winding, 0.0),
2002 ogeom_math::Vector2::new(0.0, a),
2003 ogeom_math::Vector2::new(0.0, b),
2004 range,
2005 )
2006 .ok()?;
2007 use ogeom_geom::Curve2d as _;
2010 for i in 0..7 {
2011 let t = range.0 + (range.1 - range.0) * (0.09 + 0.13 * f64::from(i)) / 0.91;
2012 let l = local(t)?;
2013 let chart = candidate.point_at(t, tol).ok()?;
2014 let du = (chart.x - l.y.atan2(l.x)).rem_euclid(tau);
2015 if du.min(tau - du) > 1e-9 {
2016 return None;
2017 }
2018 if (chart.y - l.z).abs() > tol.confusion() * 10.0 {
2019 return None;
2020 }
2021 }
2022 Some(PlanarCurve::Trig(candidate))
2023 }
2024 _ => None,
2025 }
2026}
2027
2028fn on_meridian(
2046 curve: &ogeom_geom::CircleCurve,
2047 range: (f64, f64),
2048 sphere: ogeom_math::Sphere,
2049 tol: Tolerances,
2050) -> Option<PlanarCurve> {
2051 let circle = curve.circle();
2052 let sweep = if curve.is_reversed() { -1.0 } else { 1.0 };
2057 let frame = sphere.frame();
2058 let z = frame.z().vector();
2059 if circle.centre().distance(sphere.centre()) > tol.confusion() {
2062 return None;
2063 }
2064 if (circle.radius() - sphere.radius()).abs() > tol.confusion() {
2065 return None;
2066 }
2067 let (cx, cy) = (circle.frame().x().vector(), circle.frame().y().vector());
2068 let (xz, yz) = (cx.dot(z), cy.dot(z));
2069 if xz.hypot(yz) < 1.0 - tol.angular() {
2072 return None;
2073 }
2074 let raw_alpha = yz.atan2(xz);
2075 let w = cx * -raw_alpha.sin() + cy * raw_alpha.cos();
2078 let local = frame.to_local(sphere.centre() + w);
2079 let longitude = local.y.atan2(local.x);
2080
2081 let half = core::f64::consts::PI;
2082 let mid = f64::midpoint(range.0, range.1);
2083 let x_mid = (sweep * mid - raw_alpha).rem_euclid(core::f64::consts::TAU);
2086 let x_mid = if x_mid > half {
2087 x_mid - core::f64::consts::TAU
2088 } else {
2089 x_mid
2090 };
2091 let span = sweep * (range.1 - range.0);
2092 let (mut x0, mut x1) = (x_mid - span / 2.0, x_mid + span / 2.0);
2093 if x0 > x1 {
2094 core::mem::swap(&mut x0, &mut x1);
2095 }
2096 let alpha = sweep.mul_add(mid, -x_mid);
2101 let slack = tol.parametric().max(1e-9);
2102 let (axis_point, towards) = if x0 >= -slack && x1 <= half + slack {
2103 (
2106 Point2::new(longitude, half.mul_add(0.5, alpha)),
2107 ogeom_math::Vector2::new(0.0, -sweep),
2108 )
2109 } else if x0 >= -half - slack && x1 <= slack {
2110 (
2112 Point2::new(longitude + half, half.mul_add(0.5, -alpha)),
2113 ogeom_math::Vector2::new(0.0, sweep),
2114 )
2115 } else {
2116 return None;
2118 };
2119 let towards = ogeom_math::Direction2::new(towards, tol).ok()?;
2120 let margin = (range.1 - range.0) * 0.25;
2121 let line: PlanarCurve = Line2d::over(
2122 ogeom_math::Axis2::new(axis_point, towards),
2123 range.0 - margin,
2124 range.1 + margin,
2125 )
2126 .ok()?
2127 .into();
2128
2129 for k in 0..=4 {
2132 let t = (range.1 - range.0).mul_add(f64::from(k) / 4.0, range.0);
2133 let uv = line.point_at(t, tol).ok()?;
2134 let lifted = ogeom_math::elementary::sphere_at(&sphere, uv.x, uv.y).point;
2135 let want = curve.point_at(t, tol).ok()?;
2136 if lifted.distance(want) > tol.confusion() {
2137 return None;
2138 }
2139 }
2140 Some(line)
2141}
2142
2143fn on_sphere(
2146 curve: &Curve,
2147 range: (f64, f64),
2148 sphere: ogeom_math::Sphere,
2149 tol: Tolerances,
2150) -> Option<PlanarCurve> {
2151 let Curve::Circle(c) = curve else {
2152 return None;
2153 };
2154 let circle = c.circle();
2155 let frame = sphere.frame();
2156 if circle
2159 .frame()
2160 .z()
2161 .cross_with(frame.z().vector())
2162 .magnitude()
2163 > tol.angular()
2164 {
2165 return on_meridian(c, range, sphere, tol);
2166 }
2167 let local = frame.to_local(circle.centre());
2168 if local.x.abs() > tol.confusion() || local.y.abs() > tol.confusion() {
2169 return None;
2170 }
2171 let latitude = (local.z / sphere.radius()).clamp(-1.0, 1.0).asin();
2172 if (circle.radius() - sphere.radius() * latitude.cos()).abs() > tol.confusion() {
2174 return None;
2175 }
2176 let start = circle.centre() + circle.frame().x().vector() * circle.radius();
2177 let at = frame.to_local(start);
2178 let phase = at.y.atan2(at.x);
2179 let winding = circle.frame().z().vector().dot(frame.z().vector()).signum();
2182 let towards = ogeom_math::Direction2::new(ogeom_math::Vector2::new(winding, 0.0), tol).ok()?;
2183 Some(
2184 Line2d::over(
2185 ogeom_math::Axis2::new(Point2::new(phase, latitude), towards),
2186 circle_window(range).0,
2187 circle_window(range).1,
2188 )
2189 .ok()?
2190 .into(),
2191 )
2192}
2193
2194#[cfg(test)]
2195#[allow(clippy::unwrap_used, clippy::expect_used)]
2196mod tests {
2197 use super::*;
2198 use ogeom_geom::{Curve2d, Curve3d, CylinderSurface, PlaneSurface, SphereSurface};
2199 use ogeom_math::{Cylinder, Direction, Frame, Plane, Sphere, Vector};
2200
2201 const T: Tolerances = Tolerances::millimetres();
2202
2203 fn sphere(centre: Point, radius: f64) -> SurfaceGeometry {
2204 SphereSurface::new(Sphere::centred(centre, radius, T).unwrap()).into()
2205 }
2206
2207 fn cylinder(axis: Vector, radius: f64) -> SurfaceGeometry {
2208 let frame = Frame::new(
2209 Point::ORIGIN,
2210 Direction::new(axis, T).unwrap(),
2211 Direction::from_cross(axis, Vector::new(0.3, 0.5, 0.9), T).unwrap(),
2212 T,
2213 )
2214 .unwrap();
2215 CylinderSurface::new(Cylinder::new(frame, radius, T).unwrap(), (-4.0, 4.0))
2216 .unwrap()
2217 .into()
2218 }
2219
2220 fn plane(origin: Point, normal: Vector) -> SurfaceGeometry {
2221 PlaneSurface::over(
2222 Plane::through(origin, Direction::new(normal, T).unwrap()),
2223 (-6.0, 6.0),
2224 (-6.0, 6.0),
2225 )
2226 .unwrap()
2227 .into()
2228 }
2229
2230 fn assert_same_parameter(
2233 section: &SectionCurve,
2234 surface: &SurfaceGeometry,
2235 pcurve: &PlanarCurve,
2236 samples: usize,
2237 ) {
2238 let (lo, hi) = section.curve.domain();
2239 let (plo, phi) = pcurve.domain();
2240 assert!(
2241 (lo - plo).abs() < 1e-9 && (hi - phi).abs() < 1e-9,
2242 "domains disagree: [{lo}, {hi}] against [{plo}, {phi}]"
2243 );
2244 for i in 0..=samples {
2245 #[allow(clippy::cast_precision_loss)]
2246 let t = lo + (hi - lo) * i as f64 / samples as f64;
2247 let on_curve = section.curve.point_at(t, T).unwrap();
2248 let at = pcurve.point_at(t, T).unwrap();
2249 let lifted = surface.point_at(at.x, at.y, T).unwrap();
2250 assert!(
2251 on_curve.is_equal(lifted, T),
2252 "at t = {t}: curve {on_curve:?}, lifted {lifted:?}"
2253 );
2254 }
2255 }
2256
2257 #[test]
2258 fn an_analytic_pair_comes_back_exact_with_matching_pcurves() {
2259 let drum = cylinder(Vector::Z, 2.0);
2263 let cut = plane(Point::ORIGIN, Vector::X);
2264 let SurfaceIntersection::Along(curves) =
2265 intersect_surfaces(&drum, &cut, IntersectOptions::default(), T).unwrap()
2266 else {
2267 panic!("a plane through a cylinder meets it along curves");
2268 };
2269 assert_eq!(curves.len(), 2);
2270 for section in &curves {
2271 assert!(section.exact);
2272 assert!((section.tolerance - 0.0).abs() < f64::EPSILON);
2273 let on_a = section.on_a.as_ref().expect("a line has a cylinder pcurve");
2274 let on_b = section.on_b.as_ref().expect("and a plane pcurve");
2275 assert_same_parameter(section, &drum, on_a, 50);
2276 assert_same_parameter(section, &cut, on_b, 50);
2277 }
2278 }
2279
2280 #[test]
2281 fn an_oblique_cut_gives_the_ellipse_a_trig_pcurve_on_the_drum() {
2282 let drum = cylinder(Vector::Z, 2.0);
2286 let angle: f64 = 0.5;
2287 let cut = plane(Point::ORIGIN, Vector::new(0.0, angle.sin(), angle.cos()));
2288 let SurfaceIntersection::Along(curves) =
2289 intersect_surfaces(&drum, &cut, IntersectOptions::default(), T).unwrap()
2290 else {
2291 panic!("an oblique plane meets the cylinder along its ellipse");
2292 };
2293 assert_eq!(curves.len(), 1);
2294 let section = &curves[0];
2295 assert!(section.exact);
2296 assert!(matches!(section.curve, Curve::Ellipse(_)));
2297 let on_drum = section
2298 .on_a
2299 .as_ref()
2300 .expect("the oblique ellipse now carries its cylinder pcurve");
2301 assert!(
2302 matches!(on_drum, PlanarCurve::Trig(_)),
2303 "the chart trace is trig-affine: {on_drum:?}"
2304 );
2305 assert_same_parameter(section, &drum, on_drum, 60);
2306 let on_plane = section.on_b.as_ref().expect("and its plane pcurve");
2307 assert_same_parameter(section, &cut, on_plane, 60);
2308 }
2309
2310 #[test]
2311 fn a_perpendicular_cut_gives_a_circle_with_a_straight_pcurve() {
2312 let drum = cylinder(Vector::Z, 2.0);
2313 let cut = plane(Point::new(0.0, 0.0, 1.0), Vector::Z);
2314 let SurfaceIntersection::Along(curves) =
2315 intersect_surfaces(&drum, &cut, IntersectOptions::default(), T).unwrap()
2316 else {
2317 panic!("expected curves");
2318 };
2319 assert_eq!(curves.len(), 1);
2320 let section = &curves[0];
2321 assert!(section.closed);
2322 assert!(matches!(section.curve, Curve::Circle(_)));
2323 assert!(matches!(
2325 section.on_a.as_ref().unwrap(),
2326 PlanarCurve::Line(_)
2327 ));
2328 assert_same_parameter(section, &drum, section.on_a.as_ref().unwrap(), 60);
2329 assert_same_parameter(section, &cut, section.on_b.as_ref().unwrap(), 60);
2330 }
2331
2332 #[test]
2333 fn coaxial_cylinder_and_sphere_give_circles_with_pcurves_on_both() {
2334 let drum = cylinder(Vector::Z, 1.5);
2335 let ball = sphere(Point::ORIGIN, 3.0);
2336 let SurfaceIntersection::Along(curves) =
2337 intersect_surfaces(&drum, &ball, IntersectOptions::default(), T).unwrap()
2338 else {
2339 panic!("expected curves");
2340 };
2341 assert_eq!(curves.len(), 2);
2342 for section in &curves {
2343 assert!(section.exact);
2344 assert_same_parameter(section, &drum, section.on_a.as_ref().unwrap(), 40);
2345 assert_same_parameter(section, &ball, section.on_b.as_ref().unwrap(), 40);
2346 }
2347 }
2348
2349 fn torus(origin: Point, axis: Vector, major: f64, minor: f64) -> SurfaceGeometry {
2350 let frame = Frame::new(
2351 origin,
2352 Direction::new(axis, T).unwrap(),
2353 Direction::from_cross(axis, Vector::new(0.3, 0.5, 0.9), T).unwrap(),
2354 T,
2355 )
2356 .unwrap();
2357 ogeom_geom::TorusSurface::new(ogeom_math::Torus::new(frame, major, minor, T).unwrap())
2358 .into()
2359 }
2360
2361 #[test]
2362 fn an_axis_normal_plane_meets_a_torus_in_two_parallels_with_pcurves() {
2363 let ring = torus(Point::ORIGIN, Vector::Z, 2.0, 0.5);
2364 let cut = plane(Point::new(0.0, 0.0, 0.3), Vector::Z);
2365 let SurfaceIntersection::Along(curves) =
2366 intersect_surfaces(&ring, &cut, IntersectOptions::default(), T).unwrap()
2367 else {
2368 panic!("an axis-normal plane through the tube meets it along curves");
2369 };
2370 assert_eq!(curves.len(), 2);
2371 let spread = 0.5_f64.mul_add(0.5, -(0.3 * 0.3)).sqrt();
2372 let mut radii: Vec<f64> = curves
2373 .iter()
2374 .map(|s| {
2375 let Curve::Circle(c) = &s.curve else {
2376 panic!("a parallel is a circle");
2377 };
2378 c.circle().radius()
2379 })
2380 .collect();
2381 radii.sort_by(|a, b| a.partial_cmp(b).unwrap());
2382 assert!((radii[0] - (2.0 - spread)).abs() < 1e-12);
2383 assert!((radii[1] - (2.0 + spread)).abs() < 1e-12);
2384 for section in &curves {
2385 assert!(section.exact);
2386 assert_same_parameter(section, &ring, section.on_a.as_ref().unwrap(), 48);
2387 assert_same_parameter(section, &cut, section.on_b.as_ref().unwrap(), 48);
2388 }
2389 }
2390
2391 #[test]
2392 fn the_plane_a_ball_rolls_on_touches_its_torus_along_the_circle_it_rolled() {
2393 let ring = torus(Point::ORIGIN, Vector::Z, 2.0, 0.5);
2398 let cut = plane(Point::new(0.0, 0.0, 0.5), Vector::Z);
2399 let SurfaceIntersection::Along(curves) =
2400 intersect_surfaces(&ring, &cut, IntersectOptions::default(), T).unwrap()
2401 else {
2402 panic!("the rolling plane touches along a circle, not at points");
2403 };
2404 assert_eq!(curves.len(), 1);
2405 let Curve::Circle(c) = &curves[0].curve else {
2406 panic!("the tangency is a circle");
2407 };
2408 assert!((c.circle().radius() - 2.0).abs() < 1e-12);
2409 assert_same_parameter(&curves[0], &ring, curves[0].on_a.as_ref().unwrap(), 48);
2410 assert_same_parameter(&curves[0], &cut, curves[0].on_b.as_ref().unwrap(), 48);
2411 }
2412
2413 #[test]
2414 fn a_coaxial_cylinder_meets_a_torus_in_two_parallels_and_touches_in_one() {
2415 let ring = torus(Point::ORIGIN, Vector::Z, 2.0, 0.5);
2416 let drum = cylinder(Vector::Z, 2.2);
2417 let SurfaceIntersection::Along(curves) =
2418 intersect_surfaces(&drum, &ring, IntersectOptions::default(), T).unwrap()
2419 else {
2420 panic!("a coaxial cylinder through the tube meets it along curves");
2421 };
2422 assert_eq!(curves.len(), 2);
2423 for section in &curves {
2424 assert!(section.exact);
2425 let Curve::Circle(c) = §ion.curve else {
2426 panic!("a parallel is a circle");
2427 };
2428 assert!((c.circle().radius() - 2.2).abs() < 1e-12);
2429 assert_same_parameter(section, &drum, section.on_a.as_ref().unwrap(), 48);
2430 assert_same_parameter(section, &ring, section.on_b.as_ref().unwrap(), 48);
2431 }
2432
2433 let grazing = cylinder(Vector::Z, 2.5);
2435 let SurfaceIntersection::Along(touch) =
2436 intersect_surfaces(&grazing, &ring, IntersectOptions::default(), T).unwrap()
2437 else {
2438 panic!("the grazing cylinder touches along the equator");
2439 };
2440 assert_eq!(touch.len(), 1);
2441 assert_same_parameter(&touch[0], &grazing, touch[0].on_a.as_ref().unwrap(), 48);
2442 assert_same_parameter(&touch[0], &ring, touch[0].on_b.as_ref().unwrap(), 48);
2443 }
2444
2445 #[test]
2446 fn coaxial_tori_are_the_same_or_meet_in_parallels() {
2447 let ring = torus(Point::ORIGIN, Vector::Z, 2.0, 0.5);
2448 assert!(matches!(
2449 intersect_surfaces(&ring, &ring.clone(), IntersectOptions::default(), T).unwrap(),
2450 SurfaceIntersection::Same
2451 ));
2452
2453 let lifted = torus(Point::new(0.0, 0.0, 0.5), Vector::Z, 2.0, 0.5);
2456 let SurfaceIntersection::Along(curves) =
2457 intersect_surfaces(&ring, &lifted, IntersectOptions::default(), T).unwrap()
2458 else {
2459 panic!("lifted coaxial tori meet along curves");
2460 };
2461 assert_eq!(curves.len(), 2);
2462 for section in &curves {
2463 assert!(section.exact);
2464 assert_same_parameter(section, &ring, section.on_a.as_ref().unwrap(), 48);
2465 assert_same_parameter(section, &lifted, section.on_b.as_ref().unwrap(), 48);
2466 }
2467 }
2468
2469 #[test]
2470 fn a_pair_with_no_closed_form_comes_back_fitted_with_pcurves() {
2471 let a = cylinder(Vector::Z, 1.0);
2473 let b = cylinder(Vector::X, 1.6);
2474 let options = IntersectOptions {
2475 tolerance: 1e-5,
2476 marching: Marching {
2477 chord: 1e-5,
2478 ..Marching::default()
2479 },
2480 };
2481 let SurfaceIntersection::Along(curves) = intersect_surfaces(&a, &b, options, T).unwrap()
2482 else {
2483 panic!("crossed cylinders meet along curves");
2484 };
2485 assert_eq!(curves.len(), 2);
2486 for section in &curves {
2487 assert!(!section.exact);
2488 assert!(section.closed);
2489 assert!(
2490 section.tolerance <= 1e-5 + 1e-4,
2491 "got {}",
2492 section.tolerance
2493 );
2494 assert!(section.on_a.is_some() && section.on_b.is_some());
2495
2496 let (lo, hi) = section.curve.domain();
2498 for i in 0..=200 {
2499 #[allow(clippy::cast_precision_loss)]
2500 let t = lo + (hi - lo) * f64::from(i) / 200.0;
2501 let p = section.curve.point_at(t, T).unwrap();
2502 let (SurfaceGeometry::Cylinder(x), SurfaceGeometry::Cylinder(y)) = (&a, &b) else {
2503 unreachable!()
2504 };
2505 let off = x
2506 .cylinder()
2507 .distance_to(p)
2508 .abs()
2509 .max(y.cylinder().distance_to(p).abs());
2510 assert!(
2511 off <= section.tolerance * 2.0,
2512 "at t = {t} the fitted curve is {off:e} off, tolerance {}",
2513 section.tolerance
2514 );
2515 }
2516 }
2517 }
2518
2519 #[test]
2523 fn a_plane_all_but_along_the_axis_still_meets_a_short_drum() {
2524 let drum = cylinder(Vector::Z, 1.0);
2525 let wall: SurfaceGeometry = PlaneSurface::over(
2526 Plane::through(
2527 Point::new(0.0, 0.6, 0.0),
2528 Direction::new(Vector::new(0.0, 1.0, 1e-4), T).unwrap(),
2529 ),
2530 (-1e9, 1e9),
2531 (-1e9, 1e9),
2532 )
2533 .unwrap()
2534 .into();
2535 let met = intersect_surfaces(&wall, &drum, IntersectOptions::default(), T).unwrap();
2536 let SurfaceIntersection::Along(sections) = met else {
2537 panic!("the wall crosses the drum: {met:?}");
2538 };
2539 assert_eq!(sections.len(), 1);
2540 let curve = §ions[0].curve;
2541 let (lo, hi) = curve.domain();
2542 let inside = (0..=100_000).any(|k| {
2543 let p = curve
2544 .point_at(lo + (hi - lo) * f64::from(k) / 100_000.0, T)
2545 .unwrap();
2546 p.z.abs() <= 4.0
2547 });
2548 assert!(inside, "and the section runs through the drum's height");
2549 }
2550
2551 fn on_both(section: &SectionCurve, a: &SurfaceGeometry, b: &SurfaceGeometry) {
2554 let (lo, hi) = section.curve.domain();
2555 for k in 0..=64 {
2556 let p = section
2557 .curve
2558 .point_at(lo + (hi - lo) * f64::from(k) / 64.0, T)
2559 .unwrap();
2560 for surface in [a, b] {
2561 let off = match surface {
2562 SurfaceGeometry::Plane(plane) => plane.plane().signed_distance_to(p).abs(),
2563 SurfaceGeometry::Cylinder(drum) => {
2564 let axis = drum.cylinder().axis();
2565 let rel = p - axis.location;
2566 let d = axis.direction.vector();
2567 ((rel - d * rel.dot(d)).magnitude() - drum.cylinder().radius()).abs()
2568 }
2569 _ => unreachable!("planes and drums only"),
2570 };
2571 assert!(
2572 off <= section.tolerance + 1e-9,
2573 "{p:?} is {off:e} off, stated {:e}",
2574 section.tolerance
2575 );
2576 }
2577 }
2578 }
2579
2580 #[test]
2586 fn a_plane_all_but_along_a_drums_axis_meets_it_in_two_near_lines() {
2587 let drum = cylinder(Vector::Z, 1.0);
2588 let wall: SurfaceGeometry = PlaneSurface::over(
2589 Plane::through(
2590 Point::new(0.0, 0.99, 0.0),
2591 Direction::new(Vector::new(0.0, 1.0, 2e-5), T).unwrap(),
2592 ),
2593 (-1e9, 1e9),
2594 (-1e9, 1e9),
2595 )
2596 .unwrap()
2597 .into();
2598 let met = intersect_surfaces(&wall, &drum, IntersectOptions::default(), T).unwrap();
2599 let SurfaceIntersection::Along(sections) = met else {
2600 panic!("the wall crosses the drum: {met:?}");
2601 };
2602 assert_eq!(sections.len(), 2);
2603 for section in §ions {
2604 assert!(section.tolerance > 0.0 && section.tolerance <= 1e-5);
2605 on_both(section, &wall, &drum);
2606 }
2607 }
2608
2609 #[test]
2613 fn drums_all_but_parallel_meet_in_two_near_lines() {
2614 let drill = cylinder(Vector::Z, 1.0);
2615 let frame = Frame::new(
2616 Point::new(1.5, 0.0, 0.0),
2617 Direction::new(Vector::new(5e-5, 0.0, 1.0), T).unwrap(),
2618 Direction::X,
2619 T,
2620 )
2621 .unwrap();
2622 let bore: SurfaceGeometry =
2623 CylinderSurface::new(Cylinder::new(frame, 1.0, T).unwrap(), (-3.0, 3.0))
2624 .unwrap()
2625 .into();
2626 let met = intersect_surfaces(&drill, &bore, IntersectOptions::default(), T).unwrap();
2627 let SurfaceIntersection::Along(sections) = met else {
2628 panic!("the drums cross: {met:?}");
2629 };
2630 assert_eq!(sections.len(), 2);
2631 for section in §ions {
2632 assert!(!section.exact && section.tolerance <= 1e-5);
2633 let (lo, hi) = section.curve.domain();
2634 let (p, q) = (
2635 section.curve.point_at(lo, T).unwrap(),
2636 section.curve.point_at(hi, T).unwrap(),
2637 );
2638 assert!(
2639 (p.z - q.z).abs() > 5.9,
2640 "over the shared height: {p:?} {q:?}"
2641 );
2642 on_both(section, &drill, &bore);
2643 }
2644 }
2645
2646 #[test]
2647 fn exact_lines_are_clipped_to_the_surfaces_extents() {
2648 let drum = cylinder(Vector::Z, 2.0);
2653 let cut = plane(Point::ORIGIN, Vector::X);
2654 let SurfaceIntersection::Along(curves) =
2655 intersect_surfaces(&drum, &cut, IntersectOptions::default(), T).unwrap()
2656 else {
2657 panic!("expected curves");
2658 };
2659 for section in &curves {
2660 let (lo, hi) = section.curve.domain();
2661 assert!(
2663 hi - lo <= 8.0 + 1e-9,
2664 "the line was not clipped: [{lo}, {hi}]"
2665 );
2666 let start = section.curve.point_at(lo, T).unwrap();
2667 let end = section.curve.point_at(hi, T).unwrap();
2668 assert!(start.z >= -4.0 - 1e-9 && end.z <= 4.0 + 1e-9);
2669 }
2670
2671 let high = plane(Point::new(0.0, 0.0, 10.0), Vector::Z);
2675 assert_eq!(
2676 intersect_surfaces(&drum, &high, IntersectOptions::default(), T).unwrap(),
2677 SurfaceIntersection::Apart
2678 );
2679 }
2680
2681 #[test]
2682 fn the_degenerate_answers_pass_through() {
2683 assert_eq!(
2684 intersect_surfaces(
2685 &sphere(Point::ORIGIN, 1.0),
2686 &sphere(Point::new(5.0, 0.0, 0.0), 1.0),
2687 IntersectOptions::default(),
2688 T
2689 )
2690 .unwrap(),
2691 SurfaceIntersection::Apart
2692 );
2693 assert_eq!(
2694 intersect_surfaces(
2695 &sphere(Point::ORIGIN, 1.0),
2696 &sphere(Point::ORIGIN, 1.0),
2697 IntersectOptions::default(),
2698 T
2699 )
2700 .unwrap(),
2701 SurfaceIntersection::Same
2702 );
2703 assert!(matches!(
2704 intersect_surfaces(
2705 &plane(Point::ORIGIN, Vector::Z),
2706 &sphere(Point::new(0.0, 0.0, 2.0), 2.0),
2707 IntersectOptions::default(),
2708 T
2709 )
2710 .unwrap(),
2711 SurfaceIntersection::Touching(ref p) if p.len() == 1
2712 ));
2713 }
2714
2715 #[test]
2716 fn unusable_options_are_refused() {
2717 let a = sphere(Point::ORIGIN, 1.0);
2718 let b = plane(Point::ORIGIN, Vector::Z);
2719 for tolerance in [0.0, -1.0, f64::NAN] {
2720 let options = IntersectOptions {
2721 tolerance,
2722 ..IntersectOptions::default()
2723 };
2724 assert!(intersect_surfaces(&a, &b, options, T).is_err());
2725 }
2726 }
2727
2728 #[test]
2729 fn a_circle_wound_against_the_axis_keeps_its_pcurve_same_parameter() {
2730 let drum: SurfaceGeometry = CylinderSurface::new(
2737 Cylinder::new(
2738 Frame::new(Point::new(2.0, 2.0, -1.0), Direction::Z, Direction::X, T).unwrap(),
2739 0.5,
2740 T,
2741 )
2742 .unwrap(),
2743 (0.0, 3.0),
2744 )
2745 .unwrap()
2746 .into();
2747 for normal in [Direction::Z, -Direction::Z] {
2748 let frame = Frame::new(Point::ORIGIN, normal, Direction::X, T).unwrap();
2749 let ground: SurfaceGeometry =
2750 PlaneSurface::over(Plane::new(frame), (-4.0, 4.0), (-4.0, 4.0))
2751 .unwrap()
2752 .into();
2753 let met = intersect_surfaces(&ground, &drum, IntersectOptions::default(), T).unwrap();
2754 let SurfaceIntersection::Along(curves) = met else {
2755 panic!("a plane through a cylinder sections it");
2756 };
2757 for sc in &curves {
2758 let pcurve = sc
2759 .on_b
2760 .as_ref()
2761 .expect("a circle on its cylinder has a pcurve");
2762 let (lo, hi) = sc.curve.domain();
2763 for i in 0..8 {
2764 let t = lo + (hi - lo) * f64::from(i) / 8.0;
2765 let p3 = sc.curve.point_at(t, T).unwrap();
2766 let uv = pcurve.point_at(t, T).unwrap();
2767 let lifted = drum
2768 .point_at(uv.x.rem_euclid(core::f64::consts::TAU), uv.y, T)
2769 .unwrap();
2770 assert!(
2771 p3.distance(lifted) < 1e-9,
2772 "normal {normal:?}, t {t}: pcurve lifts {lifted:?} against {p3:?}"
2773 );
2774 }
2775 }
2776 }
2777 }
2778
2779 #[test]
2785 fn a_meridian_half_has_an_exact_line_for_a_pcurve() {
2786 use ogeom_geom::Surface as _;
2787 let half = core::f64::consts::PI;
2788 for (centre, radius) in [(Point::ORIGIN, 4.0), (Point::new(1.0, -2.0, 0.5), 1.25)] {
2789 let ball = sphere(centre, radius);
2790 let SurfaceGeometry::Sphere(s) = &ball else {
2791 panic!("a sphere surface");
2792 };
2793 for azimuth in [0.0_f64, 0.7, 2.4] {
2796 let normal = Vector::new(-azimuth.sin(), azimuth.cos(), 0.0);
2797 let cut = plane(centre, normal);
2798 let SurfaceIntersection::Along(curves) =
2799 intersect_surfaces(&ball, &cut, IntersectOptions::default(), T).unwrap()
2800 else {
2801 panic!("a plane through the centre meets the ball along a circle");
2802 };
2803 assert_eq!(curves.len(), 1, "one great circle");
2804 let circle = &curves[0].curve;
2805 assert!(curves[0].exact);
2806 assert!(
2808 exact_pcurve_over(circle, circle.domain(), &ball, T).is_none(),
2809 "the whole meridian has no single chart image"
2810 );
2811 for (lo, hi) in [(0.0, half), (half, 2.0 * half), (0.3, half - 0.1)] {
2812 let pcurve = exact_pcurve_over(circle, (lo, hi), &ball, T)
2813 .expect("half a meridian has an exact pcurve");
2814 assert!(
2815 matches!(pcurve, PlanarCurve::Line(_)),
2816 "and it is a straight line in the chart"
2817 );
2818 for i in 0..=16 {
2819 let t = (hi - lo).mul_add(f64::from(i) / 16.0, lo);
2820 let want = circle.point_at(t, T).unwrap();
2821 let uv = pcurve.point_at(t, T).unwrap();
2822 assert!(
2823 uv.y >= -half.mul_add(0.5, 1e-12) && uv.y <= half.mul_add(0.5, 1e-12),
2824 "the latitude stays inside the chart: {}",
2825 uv.y
2826 );
2827 let lifted = ball
2828 .point_at(uv.x.rem_euclid(core::f64::consts::TAU), uv.y, T)
2829 .unwrap();
2830 assert!(
2831 want.distance(lifted) < 1e-9,
2832 "azimuth {azimuth}, t {t}: {lifted:?} against {want:?}"
2833 );
2834 }
2835 }
2836 assert!(
2839 exact_pcurve_over(circle, (half - 0.2, half + 0.2), &ball, T).is_none(),
2840 "a range across a pole has no one line"
2841 );
2842 let _ = s;
2843 }
2844 }
2845 }
2846
2847 #[test]
2856 fn a_trimmed_curve_carries_its_basis_pcurve_trimmed_the_same_way() {
2857 use ogeom_geom::TrimmedCurve;
2858 let drum = cylinder(Vector::Z, 2.0);
2859 let ground = plane(Point::new(0.0, 0.0, 1.0), Vector::Z);
2860 let SurfaceIntersection::Along(curves) =
2862 intersect_surfaces(&drum, &ground, IntersectOptions::default(), T).unwrap()
2863 else {
2864 panic!("a plane across a cylinder meets it in a circle");
2865 };
2866 let whole = curves[0].curve.clone();
2867 let (lo, hi) = whole.domain();
2868 let quarter: Curve = TrimmedCurve::new(whole.clone(), lo + 0.3, lo + (hi - lo) / 4.0, T)
2869 .unwrap()
2870 .into();
2871
2872 for surface in [&drum, &ground] {
2873 let full = exact_pcurve_of(&whole, surface, T).expect("the whole circle has one");
2874 let part = exact_pcurve_of(&quarter, surface, T).expect("and so does a quarter of it");
2875 let (a, b) = quarter.domain();
2878 for i in 0..=8 {
2879 let t = (b - a).mul_add(f64::from(i) / 8.0, a);
2880 let (whole_at, part_at) =
2881 (full.point_at(t, T).unwrap(), part.point_at(t, T).unwrap());
2882 assert!(
2883 whole_at.distance(part_at) < 1e-12,
2884 "the trim carries the basis: {whole_at:?} against {part_at:?}"
2885 );
2886 let lifted = surface
2888 .point_at(part_at.x.rem_euclid(core::f64::consts::TAU), part_at.y, T)
2889 .or_else(|_| surface.point_at(part_at.x, part_at.y, T))
2890 .unwrap();
2891 assert!(
2892 lifted.distance(quarter.point_at(t, T).unwrap()) < 1e-9,
2893 "same-parameter, still"
2894 );
2895 }
2896 }
2897 }
2898
2899 #[test]
2904 fn an_arc_across_its_circles_origin_has_a_pcurve_over_its_range() {
2905 let drum = cylinder(Vector::Z, 2.0);
2906 let ball = sphere(Point::ORIGIN, 2.0);
2907 let ground = plane(Point::ORIGIN, Vector::Z);
2908 for surface in [&drum, &ball] {
2909 let SurfaceIntersection::Along(curves) =
2910 intersect_surfaces(surface, &ground, IntersectOptions::default(), T).unwrap()
2911 else {
2912 panic!("a plane through the axis's normal meets it in a circle");
2913 };
2914 let circle = curves[0].curve.clone();
2915 let tau = core::f64::consts::TAU;
2916 for range in [(4.7, tau + 1.0), (-1.0, 1.5)] {
2917 let pcurve = exact_pcurve_over(&circle, range, surface, T).expect("a parallel");
2918 for i in 0..=8 {
2919 let t = (range.1 - range.0).mul_add(f64::from(i) / 8.0, range.0);
2920 let at = pcurve.point_at(t, T).unwrap();
2921 let lifted = surface.point_at(at.x.rem_euclid(tau), at.y, T).unwrap();
2922 assert!(
2923 lifted.distance(circle.point_at(t, T).unwrap()) < 1e-9,
2924 "{range:?} at {t}"
2925 );
2926 }
2927 }
2928 }
2929 }
2930
2931 #[test]
2935 fn a_plane_through_a_cones_apex_holds_its_rulings() {
2936 use ogeom_geom::ConeSurface;
2937 let frame = Frame::new(
2938 Point::new(100.0, 200.0, 300.0),
2939 Direction::Z,
2940 Direction::X,
2941 T,
2942 )
2943 .unwrap();
2944 let cone = ogeom_math::Cone::new(frame, 10.0, core::f64::consts::FRAC_PI_4, T).unwrap();
2945 let surface: SurfaceGeometry = ConeSurface::new(cone, (-5.0, 50.0)).unwrap().into();
2946 let apex = Point::new(100.0, 200.0, 290.0);
2947 let plane = |normal: Vector| -> SurfaceGeometry {
2948 PlaneSurface::new(Plane::through(apex, Direction::new(normal, T).unwrap())).into()
2949 };
2950 let cases = [
2951 (Vector::new(1.0, 0.0, -1.0), 1, true),
2952 (Vector::new(1.0, 0.0, 0.0), 2, false),
2953 ];
2954 for (normal, count, tangent) in cases {
2955 let cut = plane(normal);
2956 let SurfaceIntersection::Along(sections) =
2957 intersect_surfaces(&surface, &cut, IntersectOptions::default(), T).unwrap()
2958 else {
2959 panic!("{normal:?}: rulings");
2960 };
2961 assert_eq!(sections.len(), count, "{normal:?}");
2962 for section in §ions {
2963 assert_eq!(section.tangential, tangent, "{normal:?}");
2964 let (lo, hi) = section.curve.domain();
2965 for k in 0..=4 {
2966 let p = section
2967 .curve
2968 .point_at(lo + (hi - lo) * f64::from(k) / 4.0, T)
2969 .unwrap();
2970 assert!(cone.distance_to(p) < 1e-9, "{p:?} on the cone");
2971 let height = p.z - 300.0;
2972 assert!(
2973 (-5.0 - 1e-9..=50.0 + 1e-9).contains(&height),
2974 "{p:?} in the window"
2975 );
2976 }
2977 }
2978 }
2979 let shallow = plane(Vector::new(0.2, 0.0, 1.0));
2980 assert!(matches!(
2981 intersect_surfaces(&surface, &shallow, IntersectOptions::default(), T).unwrap(),
2982 SurfaceIntersection::Touching(_) | SurfaceIntersection::Apart
2983 ));
2984 }
2985
2986 #[test]
2987 fn a_far_stated_ruling_reads_its_angle_on_the_used_nappe() {
2988 use ogeom_geom::ConeSurface;
2989 let cone =
2997 ogeom_math::Cone::new(Frame::WORLD, 24.0, core::f64::consts::FRAC_PI_4, T).unwrap();
2998 let surface: SurfaceGeometry = ConeSurface::new(cone, (-1e5, 1e5)).unwrap().into();
2999 let u_true = 0.01_f64;
3000 let radial = Vector::new(u_true.cos(), u_true.sin(), 0.0);
3001 let direction =
3004 Direction::new((radial + Vector::new(0.0, 0.0, 1.0)) / 2f64.sqrt(), T).unwrap();
3005 let far = -7.0e5;
3006 let origin = Point::ORIGIN + radial * 24.0 + direction.vector() * far;
3007 let line = ogeom_geom::LineCurve::over(
3008 ogeom_math::Axis::new(origin, direction),
3009 far.abs() - 1.0,
3010 far.abs() + 1.0,
3011 )
3012 .unwrap();
3013 let curve: Curve = line.into();
3014 let range = ogeom_geom::Curve3d::domain(&curve);
3015 let pcurve = exact_pcurve_over(&curve, range, &surface, T).expect("a ruling inverts");
3016 let at = pcurve.point_at(range.0, T).unwrap();
3017 let tau = core::f64::consts::TAU;
3018 let gap = (at.x - u_true)
3019 .rem_euclid(tau)
3020 .min(tau - (at.x - u_true).rem_euclid(tau));
3021 assert!(
3022 gap < 1e-6,
3023 "the ruling's chart angle must be the used side's: got u {} against {u_true}",
3024 at.x
3025 );
3026 }
3027
3028 #[test]
3035 fn loops_turning_sharply_where_drums_all_but_touch_fit_in_pieces() {
3036 let drum = |origin: Point, axis: Vector, x: Vector, radius: f64, height: (f64, f64)| {
3037 let frame = Frame::new(
3038 origin,
3039 Direction::new(axis, T).unwrap(),
3040 Direction::new(x, T).unwrap(),
3041 T,
3042 )
3043 .unwrap();
3044 let cylinder = Cylinder::new(frame, radius, T).unwrap();
3045 (
3046 cylinder,
3047 SurfaceGeometry::from(CylinderSurface::new(cylinder, height).unwrap()),
3048 )
3049 };
3050 let radius = 3.175;
3051 let (thin_drum, thin) = drum(
3052 Point::new(10.994_218_762_109_735, 53.975, -209.55),
3053 Vector::new(0.0, 0.0, -1.0),
3054 Vector::new(-1.0, 0.0, 0.0),
3055 radius,
3056 (-1000.0, 1000.0),
3057 );
3058 let (wide_drum, wide) = drum(
3059 Point::new(
3060 -14.478_610_818_124_423,
3061 3.999_371_635_181_902,
3062 -277.138_401_510_994_7,
3063 ),
3064 Vector::new(
3065 -0.565_016_635_381_368_8,
3066 0.528_780_602_945_684_7,
3067 0.633_361_883_673_714_3,
3068 ),
3069 Vector::new(0.0, 0.767_637_390_304_296_3, -0.640_884_417_821_817),
3070 57.088_766_757_544_09,
3071 (0.0, 171.306_147_621_762_6),
3072 );
3073 let options = IntersectOptions {
3074 tolerance: 1e-5,
3075 marching: crate::Marching {
3076 chord: 1e-5,
3077 ..crate::Marching::default()
3078 },
3079 };
3080 let SurfaceIntersection::Along(curves) =
3081 intersect_surfaces(&thin, &wide, options, T).unwrap()
3082 else {
3083 panic!("the drums cross");
3084 };
3085 let mut length = 0.0;
3086 for section in &curves {
3087 assert!(!section.tangential);
3088 assert!(
3089 section.tolerance < 1e-3,
3090 "a section states {} of doubt",
3091 section.tolerance
3092 );
3093 let (lo, hi) = section.curve.domain();
3094 let mut previous = section.curve.point_at(lo, T).unwrap();
3095 for i in 1..=400 {
3096 let t = lo + (hi - lo) * f64::from(i) / 400.0;
3097 let p = section.curve.point_at(t, T).unwrap();
3098 let off = thin_drum
3099 .distance_to(p)
3100 .abs()
3101 .max(wide_drum.distance_to(p).abs());
3102 assert!(off < 1e-3, "a section stands {off} off the drums");
3103 length += previous.distance(p);
3104 previous = p;
3105 }
3106 }
3107 assert!(
3110 length > 4.0 * core::f64::consts::PI * radius,
3111 "the sections cover {length}"
3112 );
3113 }
3114
3115 #[test]
3116 fn coincidence_is_measured_over_the_overlap_and_nowhere_else() {
3117 let patch = |plane: ogeom_math::Plane, u: (f64, f64), v: (f64, f64)| {
3120 let surface: SurfaceGeometry = PlaneSurface::over(plane, u, v).unwrap().into();
3121 SurfaceGeometry::from(surface.to_bspline(T).unwrap())
3122 };
3123 let reach = T.confusion() * 1e2;
3124
3125 let here = patch(ogeom_math::Plane::XY, (0.0, 10.0), (0.0, 10.0));
3128 let over = patch(ogeom_math::Plane::XY, (5.0, 15.0), (5.0, 15.0));
3129 assert!(surfaces_coincide(&here, &over, reach, T));
3130
3131 let above = patch(
3135 ogeom_math::Plane::new(
3136 Frame::new(Point::new(0.0, 0.0, 1.0), Direction::Z, Direction::X, T).unwrap(),
3137 ),
3138 (0.0, 10.0),
3139 (0.0, 10.0),
3140 );
3141 assert!(!surfaces_coincide(&here, &above, reach, T));
3142 let across = patch(
3143 ogeom_math::Plane::new(
3144 Frame::new(Point::new(5.0, 0.0, 0.0), Direction::X, Direction::Y, T).unwrap(),
3145 ),
3146 (0.0, 10.0),
3147 (0.0, 10.0),
3148 );
3149 assert!(!surfaces_coincide(&here, &across, reach, T));
3150 }
3151}