1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
25use ogeom_geom::{
26 Circle2d, Curve, Curve2d as _, Curve3d, Ellipse2d, Line2d, Offset2d, PlanarCurve, Surface,
27 SurfaceGeometry,
28};
29use ogeom_math::{Circle2, Ellipse2, Frame2, Point, Point2};
30
31use crate::approx::approximate_branch;
32use crate::contact::trace_tangential;
33use crate::march::{Marching, TANGENTIAL_SINE, branches_from_touches, leaves_at_an_angle};
34use crate::surface::{Meeting, surface_surface};
35
36#[derive(Debug, Clone, Copy, PartialEq)]
38pub struct IntersectOptions {
39 pub tolerance: f64,
41 pub marching: Marching,
43}
44
45impl Default for IntersectOptions {
46 fn default() -> Self {
47 Self {
48 tolerance: 1e-6,
49 marching: Marching::default(),
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq)]
56pub struct SectionCurve {
57 pub curve: Curve,
59 pub on_a: Option<PlanarCurve>,
66 pub on_b: Option<PlanarCurve>,
68 pub tolerance: f64,
73 pub exact: bool,
75 pub closed: bool,
77 pub tangential: bool,
87}
88
89#[derive(Debug, Clone, PartialEq)]
91pub enum SurfaceIntersection {
92 Apart,
100 Touching(Vec<Point>),
102 Along(Vec<SectionCurve>),
104 Same,
106}
107
108pub fn intersect_surfaces(
126 a: &SurfaceGeometry,
127 b: &SurfaceGeometry,
128 options: IntersectOptions,
129 tol: Tolerances,
130) -> OgeomResult<SurfaceIntersection> {
131 intersect_surfaces_within(a, b, (None, None), options, tol)
132}
133
134pub type ParameterWindow = ((f64, f64), (f64, f64));
136
137pub fn intersect_surfaces_within(
159 a: &SurfaceGeometry,
160 b: &SurfaceGeometry,
161 windows: (Option<ParameterWindow>, Option<ParameterWindow>),
162 options: IntersectOptions,
163 tol: Tolerances,
164) -> OgeomResult<SurfaceIntersection> {
165 if !options.tolerance.is_finite() || options.tolerance <= 0.0 {
166 ogeom_bail!(
167 Construction,
168 "a tolerance of {} is not a distance",
169 options.tolerance
170 );
171 }
172
173 if let Some(sections) = near_parallel_plane_drum(a, b, tol) {
178 return Ok(if sections.is_empty() {
179 SurfaceIntersection::Apart
180 } else {
181 SurfaceIntersection::Along(sections)
182 });
183 }
184 match surface_surface(a, b, tol) {
185 Ok(Meeting::Apart) => Ok(SurfaceIntersection::Apart),
186 Ok(Meeting::Same) => Ok(SurfaceIntersection::Same),
187 Ok(Meeting::Touching(points)) => Ok(SurfaceIntersection::Touching(points)),
188 Ok(Meeting::Along(curves)) => {
189 let sections: Vec<SectionCurve> = curves
190 .into_iter()
191 .filter_map(|curve| exact_section(curve, a, b, tol))
192 .collect();
193 Ok(if sections.is_empty() {
194 SurfaceIntersection::Apart
197 } else {
198 SurfaceIntersection::Along(sections)
199 })
200 }
201 Err(_) if surfaces_coincide(a, b, options.tolerance, tol) => Ok(SurfaceIntersection::Same),
206 Err(_) => match near_parallel_drums(a, b, tol)
209 .or_else(|| ball_through_drum(a, b, tol))
210 .or_else(|| axial_plane_revolution(a, b, tol))
211 .or_else(|| crate::extrusion::plane_extrusion(a, b, tol))
212 .or_else(|| crate::extrusion::drum_extrusion(a, b, tol))
213 .or_else(|| plane_along_spline_lines(a, b, tol))
214 {
215 Some(sections) if sections.is_empty() => Ok(SurfaceIntersection::Apart),
216 Some(sections) => Ok(SurfaceIntersection::Along(sections)),
217 None => marched(a, b, windows, options, tol),
218 },
219 }
220}
221
222fn surfaces_coincide(
251 a: &SurfaceGeometry,
252 b: &SurfaceGeometry,
253 reach: f64,
254 tol: Tolerances,
255) -> bool {
256 const GRID: i32 = 6;
259 const EVIDENCE: usize = 4;
260 const HALVINGS: u32 = 20;
263
264 let span = |s: &SurfaceGeometry| -> f64 {
265 let ((ua, ub), (va, vb)) = s.domain();
266 (ub - ua).abs().max((vb - va).abs())
267 };
268 let (sampled, against) = if span(a) <= span(b) { (a, b) } else { (b, a) };
269 let ((ua, ub), (va, vb)) = sampled.domain();
270 if !(ua.is_finite() && ub.is_finite() && va.is_finite() && vb.is_finite()) {
271 return false;
272 }
273 let ((wu0, wu1), (wv0, wv1)) = against.domain();
274 let (mu, mv) = ((wu1 - wu0) * 0.05, (wv1 - wv0) * 0.05);
277 let (du, dv) = ((ub - ua) / f64::from(GRID), (vb - va) / f64::from(GRID));
278
279 let measure = |u: f64, v: f64| -> Option<Option<(f64, f64)>> {
283 let p = sampled.point_at(u, v, tol).ok()?;
284 let foot = ogeom_geom::project_on_surface(against, p, 16, tol).ok()?;
285 let (fu, fv) = foot.parameters;
286 if fu <= wu0 + mu || fu >= wu1 - mu || fv <= wv0 + mv || fv >= wv1 - mv {
287 return Some(None);
288 }
289 (foot.distance <= reach).then_some(Some(foot.parameters))
290 };
291 let mut agreeing: Vec<((i32, i32), (f64, f64))> = Vec::new();
292 for i in 0..=GRID {
293 for j in 0..=GRID {
294 let (u, v) = (du.mul_add(f64::from(i), ua), dv.mul_add(f64::from(j), va));
295 match measure(u, v) {
296 None => return false,
297 Some(None) => {}
298 Some(Some(foot)) => agreeing.push(((i, j), foot)),
299 }
300 }
301 }
302 if agreeing.len() < EVIDENCE {
303 return false;
304 }
305
306 let (first, _) = agreeing[0];
309 let lined_up = agreeing
310 .iter()
311 .find(|(at, _)| *at != first)
312 .map(|&(at, _)| (at.0 - first.0, at.1 - first.1))
313 .filter(|&(di, dj)| {
314 agreeing
315 .iter()
316 .all(|&((i, j), _)| di * (j - first.1) == dj * (i - first.0))
317 });
318 let feet_lined_up = {
319 let mut far = (agreeing[0].1, agreeing[0].1, 0.0_f64);
320 for (x, (_, p)) in agreeing.iter().enumerate() {
321 for (_, q) in &agreeing[x + 1..] {
322 let d = (q.0 - p.0).hypot(q.1 - p.1);
323 if d > far.2 {
324 far = (*p, *q, d);
325 }
326 }
327 }
328 let (p, q, d) = far;
329 agreeing.iter().all(|(_, r)| {
330 ((q.0 - p.0) * (r.1 - p.1) - (q.1 - p.1) * (r.0 - p.0)).abs()
331 <= tol.parametric() * d * d
332 })
333 };
334 if lined_up.is_none() && !feet_lined_up {
335 return true;
336 }
337
338 let across: &[(f64, f64)] = match lined_up {
341 Some((0, _)) => &[(1.0, 0.0)],
342 Some(_) => &[(0.0, 1.0)],
343 None => &[(1.0, 0.0), (0.0, 1.0)],
344 };
345 let mut spread = false;
346 for &((i, j), _) in &agreeing {
347 for &(ku, kv) in across {
348 for sign in [-1.0, 1.0] {
349 let mut h = 0.5 * sign;
350 for _ in 0..HALVINGS {
351 let (u, v) = (
352 du.mul_add(ku.mul_add(h, f64::from(i)), ua),
353 dv.mul_add(kv.mul_add(h, f64::from(j)), va),
354 );
355 if !(ua..=ub).contains(&u) || !(va..=vb).contains(&v) {
356 break;
357 }
358 match measure(u, v) {
359 None => return false,
360 Some(None) => h *= 0.5,
361 Some(Some(_)) => {
362 spread = true;
363 break;
364 }
365 }
366 }
367 }
368 }
369 }
370 spread
371}
372
373fn axial_plane_revolution(
386 a: &SurfaceGeometry,
387 b: &SurfaceGeometry,
388 tol: Tolerances,
389) -> Option<Vec<SectionCurve>> {
390 const SAMPLES: u32 = 64;
391 let (plane, revolution, plane_first) = match (a, b) {
392 (SurfaceGeometry::Plane(p), SurfaceGeometry::Revolution(r)) => (p, r, true),
393 (SurfaceGeometry::Revolution(r), SurfaceGeometry::Plane(p)) => (p, r, false),
394 _ => return None,
395 };
396 let cut = plane.plane();
397 let axis = revolution.axis();
398 let normal = cut.normal();
399 if normal.dot(axis.direction).abs() > tol.angular()
400 || cut.signed_distance_to(axis.location).abs() > tol.confusion()
401 {
402 return None;
403 }
404 let profile = revolution.curve();
405 let (v0, v1) = profile.domain();
406 let at = |k: u32| v0 + (v1 - v0) * f64::from(k) / f64::from(SAMPLES);
407 let radial = |v: f64| {
408 let p = profile.point_at(v, tol).ok()?;
409 Some(p - axis.project(p))
410 };
411 let mut widest = ogeom_math::Vector::ZERO;
413 for k in 0..=SAMPLES {
414 let r = radial(at(k))?;
415 if r.magnitude() > widest.magnitude() {
416 widest = r;
417 }
418 }
419 let side = ogeom_math::Direction::new(widest, tol).ok()?;
420 let across = axis.direction.cross_with(side.vector());
421 let offset = |v: f64| radial(v).map(|r| (r.dot(side.vector()), r.dot(across)));
423 for k in 0..=SAMPLES {
424 let (_, off) = offset(at(k))?;
425 if off.abs() > tol.confusion() {
426 return None;
427 }
428 }
429 let mut cuts = vec![v0];
432 for k in 0..SAMPLES {
433 let (mut lo, mut hi) = (at(k), at(k + 1));
434 let (s_lo, s_hi) = (offset(lo)?.0, offset(hi)?.0);
435 if s_lo.abs() <= tol.confusion() || s_lo * s_hi >= 0.0 {
436 continue;
437 }
438 for _ in 0..80 {
439 let mid = f64::midpoint(lo, hi);
440 if offset(mid)?.0 * s_lo > 0.0 {
441 lo = mid;
442 } else {
443 hi = mid;
444 }
445 }
446 cuts.push(f64::midpoint(lo, hi));
447 }
448 cuts.push(v1);
449 let out = axis.direction.cross_with(normal.vector());
451 let first = across.dot(out).atan2(side.vector().dot(out));
452 let (u0, u1) = revolution.domain().0;
453 let turns: Vec<f64> = [first, first + core::f64::consts::PI]
454 .into_iter()
455 .filter_map(|u| {
456 let u = u0 + (u - u0).rem_euclid(core::f64::consts::TAU);
457 let u = if (u - u0 - core::f64::consts::TAU).abs() <= tol.angular() {
458 u0
459 } else {
460 u
461 };
462 (u <= u1 + tol.angular()).then_some(u.min(u1))
463 })
464 .collect();
465 let mut sections = Vec::new();
466 for &u in &turns {
467 let turned = ogeom_geom::Transformable::transformed(
468 profile,
469 &ogeom_math::Transform::rotation(axis, u),
470 tol,
471 )
472 .ok()?;
473 for piece in cuts.windows(2) {
474 let (va, vb) = (piece[0], piece[1]);
475 if vb - va <= tol.parametric() {
476 continue;
477 }
478 let curve: Curve = ogeom_geom::TrimmedCurve::new(turned.clone(), va, vb, tol)
479 .ok()?
480 .into();
481 let column: PlanarCurve = Line2d::over(
482 ogeom_math::Axis2::new(Point2::new(u, 0.0), ogeom_math::Direction2::Y),
483 va,
484 vb,
485 )
486 .ok()?
487 .into();
488 let flat = exact_pcurve(&curve, (va, vb), a_or_b(plane_first, a, b), tol);
489 let (on_a, on_b) = if plane_first {
490 (flat, Some(column))
491 } else {
492 (Some(column), flat)
493 };
494 sections.push(SectionCurve {
495 on_a,
496 on_b,
497 tolerance: 0.0,
498 exact: true,
499 closed: false,
500 tangential: false,
501 curve,
502 });
503 }
504 }
505 Some(sections)
506}
507
508fn plane_along_spline_lines(
533 a: &SurfaceGeometry,
534 b: &SurfaceGeometry,
535 tol: Tolerances,
536) -> Option<Vec<SectionCurve>> {
537 const SAMPLES: u32 = 96;
538 let (plane, other, plane_first) = match (a, b) {
539 (SurfaceGeometry::Plane(p), other) => (p.plane(), other, true),
540 (other, SurfaceGeometry::Plane(p)) => (p.plane(), other, false),
541 _ => return None,
542 };
543 let extruded;
544 let (spline, ruled) = match other {
545 SurfaceGeometry::BSpline(s) => (s, None),
546 SurfaceGeometry::Extrusion(e) => {
547 extruded = extrusion_patch(e, tol)?;
548 (&extruded, Some(e))
549 }
550 _ => return None,
551 };
552 let distance = |p: Point| plane.signed_distance_to(p);
553 let ((u0, u1), (v0, v1)) = spline.domain();
554 let line_at = |along_u: bool, t: f64| -> Option<ogeom_geom::BSplineCurve> {
557 if along_u {
558 spline.iso_u_curve(t, tol).ok()
559 } else {
560 spline.iso_v_curve(t, tol).ok()
561 }
562 };
563 let weighted = |curve: &ogeom_geom::BSplineCurve| -> Vec<f64> {
564 curve
565 .control_points()
566 .iter()
567 .map(|w| w.weight * distance(w.point()))
568 .collect()
569 };
570 let lies_in = |curve: &ogeom_geom::BSplineCurve| {
571 curve
572 .control_points()
573 .iter()
574 .all(|w| distance(w.point()).abs() <= tol.confusion())
575 };
576 let has_length = |curve: &ogeom_geom::BSplineCurve| {
577 let first = curve.control_points()[0].point();
578 curve
579 .control_points()
580 .iter()
581 .any(|w| w.point().distance(first) > tol.confusion())
582 };
583 let found = |along_u: bool| -> Option<Vec<f64>> {
586 let (lo, hi) = if along_u { (u0, u1) } else { (v0, v1) };
587 let at = |k: u32| lo + (hi - lo) * f64::from(k) / f64::from(SAMPLES);
588 let rows: Vec<Vec<f64>> = (0..=SAMPLES)
589 .map(|k| line_at(along_u, at(k)).map(|c| weighted(&c)))
590 .collect::<Option<_>>()?;
591 let count = rows[0].len();
592 if rows.iter().any(|r| r.len() != count) {
593 return None;
594 }
595 let widest = (0..count).max_by(|&i, &j| {
596 let spread = |i: usize| rows.iter().fold(0.0_f64, |m, r| m.max(r[i].abs()));
597 spread(i).total_cmp(&spread(j))
598 })?;
599 let mut roots = vec![lo, hi];
600 for k in 0..SAMPLES {
601 let (mut a, mut b) = (at(k), at(k + 1));
602 let (da, db) = (rows[k as usize][widest], rows[k as usize + 1][widest]);
603 if da == 0.0 {
604 roots.push(a);
605 continue;
606 }
607 if da * db > 0.0 {
608 continue;
609 }
610 let sign = da.signum();
611 for _ in 0..80 {
612 let mid = f64::midpoint(a, b);
613 let d = weighted(&line_at(along_u, mid)?)[widest];
614 if d * sign > 0.0 {
615 a = mid;
616 } else {
617 b = mid;
618 }
619 }
620 roots.push(f64::midpoint(a, b));
621 }
622 roots.sort_by(f64::total_cmp);
623 roots.dedup_by(|x, y| (*x - *y).abs() <= tol.parametric());
624 Some(
625 roots
626 .into_iter()
627 .filter(|&t| line_at(along_u, t).is_some_and(|c| lies_in(&c) && has_length(&c)))
628 .collect(),
629 )
630 };
631 let columns = found(true)?;
632 let rows = found(false)?;
633 if columns.is_empty() && rows.is_empty() {
634 return None;
635 }
636 let strip = |lines: &[f64], t: f64| lines.iter().filter(|&&x| x < t).count();
639 let near_line = |lines: &[f64], t: f64, span: f64| {
640 lines
641 .iter()
642 .any(|&x| (x - t).abs() <= span / f64::from(SAMPLES) * 0.25)
643 };
644 let mut sides: ogeom_core::FastMap<(usize, usize), f64> = ogeom_core::FastMap::default();
645 let band = tol.confusion() * 10.0;
646 for i in 0..=SAMPLES {
647 let u = u0 + (u1 - u0) * (f64::from(i) + 0.5) / f64::from(SAMPLES + 1);
648 if near_line(&columns, u, u1 - u0) {
649 continue;
650 }
651 for j in 0..=SAMPLES {
652 let v = v0 + (v1 - v0) * (f64::from(j) + 0.5) / f64::from(SAMPLES + 1);
653 if near_line(&rows, v, v1 - v0) {
654 continue;
655 }
656 let d = distance(spline.point_at(u, v, tol).ok()?);
657 if d.abs() <= band {
658 continue;
659 }
660 let cell = (strip(&columns, u), strip(&rows, v));
661 match sides.get(&cell) {
662 Some(side) if side * d < 0.0 => return None,
663 Some(_) => {}
664 None => {
665 sides.insert(cell, d.signum());
666 }
667 }
668 }
669 }
670 let closed_u = spline.is_closed_u(tol);
677 let closed_v = spline.is_closed_v(tol);
678 let plane_normal = plane.normal().vector();
679 let leaves_plane = |along_u: bool, t: f64| -> bool {
680 let (lo, hi) = if along_u { (v0, v1) } else { (u0, u1) };
681 let (from, to) = if along_u { (u0, u1) } else { (v0, v1) };
683 let half_step = (to - from) / (2.0 * f64::from(SAMPLES + 1));
684 let inside = if (t - from).abs() <= (to - t).abs() {
685 t + half_step
686 } else {
687 t - half_step
688 };
689 [0.0, 0.07, 0.19, 0.37, 0.53, 0.71, 0.89, 1.0]
690 .iter()
691 .all(|&f| {
692 let s = (hi - lo).mul_add(f, lo);
693 let (on, near) = if along_u {
694 ((t, s), (inside, s))
695 } else {
696 ((s, t), (s, inside))
697 };
698 let (Ok(n), Ok(p), Ok(q)) = (
699 spline.normal_at(on.0, on.1, tol),
700 spline.point_at(on.0, on.1, tol),
701 spline.point_at(near.0, near.1, tol),
702 ) else {
703 return false;
704 };
705 let sine = n.vector().cross(plane_normal).magnitude();
706 leaves_at_an_angle(sine, p.distance(q), distance(q).abs(), band)
707 })
708 };
709 let on_open_border = |t: f64, (lo, hi): (f64, f64), closed: bool| {
710 !closed && ((t - lo).abs() <= tol.parametric() || (t - hi).abs() <= tol.parametric())
711 };
712 let side_of = |cu: Option<usize>, cv: Option<usize>| -> Option<f64> {
713 let mut found = sides
714 .iter()
715 .filter(|((u, v), _)| cu.is_none_or(|c| c == *u) && cv.is_none_or(|c| c == *v))
716 .map(|(_, s)| *s);
717 let first = found.next()?;
718 found.all(|s| s == first).then_some(first)
719 };
720 let crosses = |k: usize,
721 count: usize,
722 closed: bool,
723 border: bool,
724 cell: &dyn Fn(usize) -> Option<f64>| {
725 let below = cell(k).or_else(|| closed.then(|| (0..=count).rev().find_map(cell)).flatten());
726 let above = cell(k + 1).or_else(|| closed.then(|| (0..=count).find_map(cell)).flatten());
727 match (below, above) {
728 (Some(x), Some(y)) => x * y < 0.0,
729 (Some(_), None) | (None, Some(_)) => border,
730 (None, None) => false,
731 }
732 };
733 for (k, &u) in columns.iter().enumerate() {
734 let border = on_open_border(u, (u0, u1), closed_u) && leaves_plane(true, u);
735 if !crosses(k, columns.len(), closed_u, border, &|c| {
736 side_of(Some(c), None)
737 }) {
738 return None;
739 }
740 }
741 for (k, &v) in rows.iter().enumerate() {
742 let border = on_open_border(v, (v0, v1), closed_v) && leaves_plane(false, v);
743 if !crosses(k, rows.len(), closed_v, border, &|c| side_of(None, Some(c))) {
744 return None;
745 }
746 }
747 let mut sections = Vec::new();
748 let mut emit = |along_u: bool, t: f64| -> Option<()> {
749 let iso = line_at(along_u, t)?;
750 let curve: Curve = match ruled {
751 Some(extrusion) if along_u => {
752 let range = Curve3d::domain(&iso);
753 let axis = ogeom_math::Axis {
754 location: extrusion.curve().point_at(t, tol).ok()?,
755 direction: extrusion.direction(),
756 };
757 ogeom_geom::LineCurve::over(axis, range.0, range.1)
758 .ok()?
759 .into()
760 }
761 _ => iso.into(),
762 };
763 let range = curve.domain();
764 let chart: PlanarCurve = if along_u {
765 Line2d::over(
766 ogeom_math::Axis2::new(Point2::new(t, 0.0), ogeom_math::Direction2::Y),
767 range.0,
768 range.1,
769 )
770 } else {
771 Line2d::over(
772 ogeom_math::Axis2::new(Point2::new(0.0, t), ogeom_math::Direction2::X),
773 range.0,
774 range.1,
775 )
776 }
777 .ok()?
778 .into();
779 let flat = exact_pcurve(&curve, range, a_or_b(plane_first, a, b), tol)?;
780 let (on_a, on_b) = if plane_first {
781 (Some(flat), Some(chart))
782 } else {
783 (Some(chart), Some(flat))
784 };
785 sections.push(SectionCurve {
786 on_a,
787 on_b,
788 tolerance: 0.0,
789 exact: true,
790 closed: curve.is_closed(tol),
791 tangential: false,
792 curve,
793 });
794 Some(())
795 };
796 for (k, &u) in columns.iter().enumerate() {
799 if closed_u
800 && k + 1 == columns.len()
801 && k > 0
802 && columns[0] == u0
803 && (u - u1).abs() <= tol.parametric()
804 {
805 continue;
806 }
807 emit(true, u)?;
808 }
809 for (k, &v) in rows.iter().enumerate() {
810 if closed_v
811 && k + 1 == rows.len()
812 && k > 0
813 && rows[0] == v0
814 && (v - v1).abs() <= tol.parametric()
815 {
816 continue;
817 }
818 emit(false, v)?;
819 }
820 Some(sections)
821}
822
823fn extrusion_patch(
828 extrusion: &ogeom_geom::ExtrusionSurface,
829 tol: Tolerances,
830) -> Option<ogeom_geom::BSplineSurface> {
831 let Curve::BSpline(profile) = extrusion.curve() else {
832 return None;
833 };
834 let (_, (v0, v1)) = extrusion.domain();
835 let step = extrusion.direction().vector();
836 let mut grid = Vec::with_capacity(profile.control_points().len() * 2);
837 for c in profile.control_points() {
838 for v in [v0, v1] {
839 grid.push(ogeom_math::Weighted {
840 scaled: Point::from_vector(c.scaled.to_vector() + step * (v * c.weight)),
841 weight: c.weight,
842 });
843 }
844 }
845 let count = profile.control_points().len();
846 ogeom_geom::BSplineSurface::rational(
847 profile.knots().clone(),
848 ogeom_math::KnotVector::new(vec![v0, v0, v1, v1], 1).ok()?,
849 ogeom_math::ControlGrid::new(grid, count, 2).ok()?,
850 )
851 .ok()
852 .filter(|patch| {
853 let same = |p: (f64, f64), q: (f64, f64)| {
854 (p.0 - q.0).abs() <= tol.parametric() && (p.1 - q.1).abs() <= tol.parametric()
855 };
856 let (pu, pv) = patch.domain();
857 let (eu, ev) = extrusion.domain();
858 same(pu, eu) && same(pv, ev)
859 })
860}
861
862fn a_or_b<'s>(first: bool, a: &'s SurfaceGeometry, b: &'s SurfaceGeometry) -> &'s SurfaceGeometry {
864 if first { a } else { b }
865}
866
867fn near_parallel_drums(
884 a: &SurfaceGeometry,
885 b: &SurfaceGeometry,
886 tol: Tolerances,
887) -> Option<Vec<SectionCurve>> {
888 const LEAN: f64 = 1e-3;
889 let (SurfaceGeometry::Cylinder(sa), SurfaceGeometry::Cylinder(sb)) = (a, b) else {
890 return None;
891 };
892 let (ca, cb) = (sa.cylinder(), sb.cylinder());
893 let (axis_a, axis_b) = (ca.axis(), cb.axis());
894 let (da, db) = (axis_a.direction.vector(), axis_b.direction.vector());
895 let (ra, rb) = (ca.radius(), cb.radius());
896 let cos = da.dot(db);
897 if da.cross(db).magnitude() > LEAN || cos.abs() < 0.5 {
898 return None;
899 }
900 let (pa, pb) = (axis_a.location, axis_b.location);
901 let (_, (a0, a1)) = a.domain();
903 let (_, (b0, b1)) = b.domain();
904 let along = |v: f64| (pb - pa).dot(da) + v * cos;
905 let (lo, hi) = (
906 a0.min(a1).max(along(b0).min(along(b1))),
907 a0.max(a1).min(along(b0).max(along(b1))),
908 );
909 if !(lo.is_finite() && hi.is_finite()) {
910 return None;
911 }
912 if hi - lo <= tol.confusion() {
913 return Some(Vec::new());
914 }
915 let meet = |z: f64| -> Option<[Point; 2]> {
919 let centre_a = pa + da * z;
920 let s = (centre_a - pb).dot(da) / cos;
921 let centre_b = pb + db * s;
922 let mut between = centre_b - centre_a;
923 between = between - da * between.dot(da);
924 let d = between.magnitude();
925 let margin = tol.confusion() * 50.0;
930 if d <= margin || d >= ra + rb - margin || d <= (ra - rb).abs() + margin {
931 return None;
932 }
933 let x = (d * d + ra * ra - rb * rb) / (2.0 * d);
934 let h = (ra * ra - x * x).max(0.0).sqrt();
935 let ex = between / d;
936 let ey = da.cross(ex);
937 Some([centre_a + ex * x + ey * h, centre_a + ex * x - ey * h])
938 };
939 lines_through_stations(lo, hi, meet, rb * (1.0 / cos.abs() - 1.0), tol)
940}
941
942const NEAR_PARALLEL_STRAY: f64 = 1e-5;
945
946fn lines_through_stations(
954 lo: f64,
955 hi: f64,
956 meet: impl Fn(f64) -> Option<[Point; 2]>,
957 stated: f64,
958 tol: Tolerances,
959) -> Option<Vec<SectionCurve>> {
960 const STATIONS: u32 = 32;
961 const STRAIGHT: f64 = 1e-6;
962 let at = |k: f64| (hi - lo).mul_add(k / f64::from(STATIONS), lo);
963 let heights: Vec<f64> = (0..=STATIONS).map(|k| at(f64::from(k))).collect();
964 let met: Vec<[Point; 2]> = heights.iter().map(|&z| meet(z)).collect::<Option<_>>()?;
965 let between: Vec<[Point; 2]> = (0..STATIONS)
966 .map(|k| meet(at(f64::from(k) + 0.5)))
967 .collect::<Option<_>>()?;
968 let mut out = Vec::with_capacity(2);
969 for side in 0..2 {
970 let (from, to) = (met[0][side], met[met.len() - 1][side]);
971 let span = to - from;
972 let length = span.magnitude();
973 if length <= tol.confusion() {
974 return None;
975 }
976 let off_line = |p: Point| {
977 let t = (p - from).dot(span) / (length * length);
978 p.distance(from + span * t)
979 };
980 let stray = met
981 .iter()
982 .chain(&between)
983 .map(|pair| off_line(pair[side]))
984 .fold(0.0_f64, f64::max);
985 let (curve, stray): (Curve, f64) = if stray <= STRAIGHT {
986 (
987 ogeom_geom::LineCurve::segment(from, to, tol).ok()?.into(),
988 stray,
989 )
990 } else {
991 let points: Vec<Point> = met.iter().map(|pair| pair[side]).collect();
992 let fitted =
993 ogeom_geom::fit::fit_points_at(&heights, &points, 3, tol.confusion(), tol).ok()?;
994 let curve: Curve = fitted.curve.into();
995 let mut worst = fitted.error;
996 for (k, pair) in (0..STATIONS).zip(&between) {
997 let p = curve.point_at(at(f64::from(k) + 0.5), tol).ok()?;
998 worst = worst.max(p.distance(pair[side]));
999 }
1000 (curve, worst)
1001 };
1002 let tolerance = stray + stated + tol.confusion();
1003 if tolerance > NEAR_PARALLEL_STRAY {
1004 return None;
1005 }
1006 out.push(SectionCurve {
1007 curve,
1008 on_a: None,
1009 on_b: None,
1010 tolerance,
1011 exact: false,
1012 closed: false,
1013 tangential: false,
1014 });
1015 }
1016 Some(out)
1017}
1018
1019fn ball_through_drum(
1031 a: &SurfaceGeometry,
1032 b: &SurfaceGeometry,
1033 tol: Tolerances,
1034) -> Option<Vec<SectionCurve>> {
1035 const SAMPLES: u32 = 256;
1036 const STRAY: f64 = 1e-5;
1037 let (ball, drum, ball_first) = match (a, b) {
1038 (SurfaceGeometry::Sphere(s), SurfaceGeometry::Cylinder(c)) => (s, c, true),
1039 (SurfaceGeometry::Cylinder(c), SurfaceGeometry::Sphere(s)) => (s, c, false),
1040 _ => return None,
1041 };
1042 let (sphere, cylinder) = (ball.sphere(), drum.cylinder());
1043 let frame = cylinder.frame();
1044 let (x, y, d) = (frame.x().vector(), frame.y().vector(), frame.z().vector());
1045 let (origin, r) = (frame.origin(), cylinder.radius());
1046 let (centre, big) = (sphere.centre(), sphere.radius());
1047 let ball_frame = sphere.frame();
1048 let (_, (h0, h1)) = drum.domain();
1049 let margin = r * 0.1;
1053 let heights = |angle: f64| -> Option<[f64; 2]> {
1055 let foot = origin + (x * angle.cos() + y * angle.sin()) * r;
1056 let w = foot - centre;
1057 let half = d.dot(w);
1058 let disc = half.mul_add(half, -(w.dot(w) - big * big));
1059 if disc <= margin * margin {
1060 return None;
1061 }
1062 let root = disc.sqrt();
1063 let pair = [-half - root, -half + root];
1064 pair.iter().all(|v| *v >= h0 && *v <= h1).then_some(pair)
1065 };
1066 let at = |angle: f64, v: f64| origin + (x * angle.cos() + y * angle.sin()) * r + d * v;
1067 let on_ball = |p: Point, before: Option<Point2>| -> Point2 {
1069 let local = ball_frame.to_local(p);
1070 let lat = local.z.atan2(local.x.hypot(local.y));
1071 let mut lon = local.y.atan2(local.x).rem_euclid(core::f64::consts::TAU);
1072 if let Some(prev) = before {
1073 while lon - prev.x > core::f64::consts::PI {
1074 lon -= core::f64::consts::TAU;
1075 }
1076 while prev.x - lon > core::f64::consts::PI {
1077 lon += core::f64::consts::TAU;
1078 }
1079 }
1080 Point2::new(lon, lat)
1081 };
1082 let angle_of = |k: f64| core::f64::consts::TAU * k / f64::from(SAMPLES);
1083 let params: Vec<f64> = (0..=SAMPLES).map(|k| angle_of(f64::from(k))).collect();
1084 let mut sampled: Vec<[f64; 2]> = Vec::with_capacity(params.len());
1085 for &angle in ¶ms {
1086 sampled.push(heights(angle)?);
1087 }
1088 let mut out = Vec::with_capacity(2);
1089 for side in 0..2 {
1090 let points: Vec<Point> = params
1091 .iter()
1092 .zip(&sampled)
1093 .map(|(&angle, pair)| at(angle, pair[side]))
1094 .collect();
1095 let on_drum: Vec<Point2> = params
1096 .iter()
1097 .zip(&sampled)
1098 .map(|(&angle, pair)| Point2::new(angle, pair[side]))
1099 .collect();
1100 let mut on_sphere: Vec<Point2> = Vec::with_capacity(points.len());
1101 for p in &points {
1102 let q = on_ball(*p, on_sphere.last().copied());
1103 on_sphere.push(q);
1104 }
1105 let target = tol.confusion() * 10.0;
1106 let curve: Curve = ogeom_geom::fit::fit_points_at(¶ms, &points, 3, target, tol)
1107 .ok()?
1108 .curve
1109 .into();
1110 let drum_image: PlanarCurve =
1111 ogeom_geom::fit::fit_points_2d_at(¶ms, &on_drum, 3, target, tol)
1112 .ok()?
1113 .curve
1114 .into();
1115 let ball_image: PlanarCurve =
1116 ogeom_geom::fit::fit_points_2d_at(¶ms, &on_sphere, 3, target, tol)
1117 .ok()?
1118 .curve
1119 .into();
1120 let mut stray = 0.0_f64;
1123 for k in 0..(2 * SAMPLES) {
1124 let angle = angle_of(f64::from(k) / 2.0);
1125 let truth = at(angle, heights(angle)?[side]);
1126 let on_curve = curve.point_at(angle, tol).ok()?;
1127 let uv = drum_image.point_at(angle, tol).ok()?;
1128 let through_drum = drum.point_at(uv.x, uv.y, tol).ok()?;
1129 let uv = ball_image.point_at(angle, tol).ok()?;
1130 let through_ball = ball.point_at(uv.x, uv.y, tol).ok()?;
1131 stray = stray
1132 .max(truth.distance(on_curve))
1133 .max(truth.distance(through_drum))
1134 .max(truth.distance(through_ball));
1135 }
1136 let tolerance = stray.max(tol.confusion());
1137 if tolerance > STRAY {
1138 return None;
1139 }
1140 let (on_a, on_b) = if ball_first {
1141 (ball_image, drum_image)
1142 } else {
1143 (drum_image, ball_image)
1144 };
1145 out.push(SectionCurve {
1146 curve,
1147 on_a: Some(on_a),
1148 on_b: Some(on_b),
1149 tolerance,
1150 exact: false,
1151 closed: true,
1152 tangential: false,
1153 });
1154 }
1155 Some(out)
1156}
1157
1158fn near_parallel_plane_drum(
1170 a: &SurfaceGeometry,
1171 b: &SurfaceGeometry,
1172 tol: Tolerances,
1173) -> Option<Vec<SectionCurve>> {
1174 const LEAN: f64 = 1e-3;
1175 const SPAN: f64 = 3e4;
1176 let (plane, drum, surface) = match (a, b) {
1177 (SurfaceGeometry::Plane(p), SurfaceGeometry::Cylinder(c)) => (p.plane(), c.cylinder(), b),
1178 (SurfaceGeometry::Cylinder(c), SurfaceGeometry::Plane(p)) => (p.plane(), c.cylinder(), a),
1179 _ => return None,
1180 };
1181 let axis = drum.axis();
1182 let (d, r) = (axis.direction.vector(), drum.radius());
1183 let n = plane.normal().vector();
1184 let lean = n.dot(d).abs();
1185 if lean <= tol.angular() || lean > LEAN || r / lean < SPAN {
1190 return None;
1191 }
1192 let across = n - d * n.dot(d);
1193 let k = across.magnitude();
1194 let e1 = across / k;
1195 let e2 = d.cross(e1);
1196 let (_, (lo, hi)) = surface.domain();
1197 if !(lo.is_finite() && hi.is_finite()) || hi - lo <= tol.confusion() {
1198 return None;
1199 }
1200 let meet = |z: f64| -> Option<[Point; 2]> {
1201 let centre = axis.location + d * z;
1202 let u = -plane.signed_distance_to(centre) / k;
1203 let margin = tol.confusion() * 1e3;
1204 if u.abs() >= r - margin {
1205 return None;
1206 }
1207 let w = r.mul_add(r, -(u * u)).sqrt();
1208 Some([centre + e1 * u + e2 * w, centre + e1 * u - e2 * w])
1209 };
1210 lines_through_stations(lo, hi, meet, 0.0, tol)
1211}
1212
1213fn exact_section(
1228 curve: Curve,
1229 a: &SurfaceGeometry,
1230 b: &SurfaceGeometry,
1231 tol: Tolerances,
1232) -> Option<SectionCurve> {
1233 let closed = match &curve {
1234 Curve::Circle(_) | Curve::Ellipse(_) => true,
1235 _ => curve.is_closed(tol),
1236 };
1237 let range = curve.domain();
1238 let on_a = exact_pcurve(&curve, range, a, tol);
1239 let on_b = exact_pcurve(&curve, range, b, tol);
1240
1241 if let Curve::Line(_) = &curve {
1242 let mut interval = curve.domain();
1245 if let Some(p) = &on_a {
1246 interval = intersect_intervals(interval, inside_box(p, a))?;
1247 }
1248 if let Some(p) = &on_b {
1249 interval = intersect_intervals(interval, inside_box(p, b))?;
1250 }
1251 let (lo, hi) = interval;
1252 let Curve::Line(line) = &curve else {
1253 unreachable!()
1254 };
1255 let clipped: Curve = ogeom_geom::LineCurve::over(line.axis(), lo, hi)
1256 .ok()?
1257 .into();
1258 let clip2 = |p: &PlanarCurve| -> Option<PlanarCurve> {
1259 let PlanarCurve::Line(l) = p else {
1260 return Some(p.clone());
1261 };
1262 Some(Line2d::over(l.axis(), lo, hi).ok()?.into())
1263 };
1264 let (ca, cb) = (on_a.as_ref().and_then(clip2), on_b.as_ref().and_then(clip2));
1265 let tangential = touching_along(&clipped, ca.as_ref(), cb.as_ref(), a, b, tol);
1266 return Some(SectionCurve {
1267 on_a: ca,
1268 on_b: cb,
1269 tolerance: 0.0,
1270 exact: true,
1271 closed: false,
1272 tangential,
1273 curve: clipped,
1274 });
1275 }
1276
1277 for (pcurve, surface) in [(&on_a, a), (&on_b, b)] {
1280 if let Some(p) = pcurve
1281 && !touches_box(p, surface, tol)
1282 {
1283 return None;
1284 }
1285 }
1286 let tangential = touching_along(&curve, on_a.as_ref(), on_b.as_ref(), a, b, tol);
1287 Some(SectionCurve {
1288 on_a,
1289 on_b,
1290 tolerance: 0.0,
1291 exact: true,
1292 closed,
1293 tangential,
1294 curve,
1295 })
1296}
1297
1298fn touching_along(
1307 curve: &Curve,
1308 on_a: Option<&PlanarCurve>,
1309 on_b: Option<&PlanarCurve>,
1310 a: &SurfaceGeometry,
1311 b: &SurfaceGeometry,
1312 tol: Tolerances,
1313) -> bool {
1314 let sample_uv = |pc: Option<&PlanarCurve>,
1322 surface: &SurfaceGeometry,
1323 t: f64|
1324 -> Option<ogeom_math::Point2> {
1325 if let Some(pc) = pc {
1326 return pc.point_at(t, tol).ok();
1327 }
1328 let p = curve.point_at(t, tol).ok()?;
1329 chart_inversion(surface, p, tol)
1330 };
1331 let (lo, hi) = curve.domain();
1332 let mut judged = 0_usize;
1338 for f in [0.07, 0.19, 0.37, 0.53, 0.71, 0.89] {
1339 let t = (hi - lo).mul_add(f, lo);
1340 let (Some(ua), Some(ub)) = (sample_uv(on_a, a, t), sample_uv(on_b, b, t)) else {
1341 continue;
1342 };
1343 let (Ok(na), Ok(nb)) = (a.normal_at(ua.x, ua.y, tol), b.normal_at(ub.x, ub.y, tol)) else {
1344 continue;
1345 };
1346 if na.vector().cross(nb.vector()).magnitude() > 1e-6 {
1347 return false;
1348 }
1349 judged += 1;
1350 }
1351 judged >= 3
1352}
1353
1354fn chart_inversion(
1356 surface: &SurfaceGeometry,
1357 p: ogeom_math::Point,
1358 tol: Tolerances,
1359) -> Option<ogeom_math::Point2> {
1360 use ogeom_math::elementary;
1361 let (u, v) = match surface {
1362 SurfaceGeometry::Plane(s) => elementary::plane_parameters(&s.plane(), p),
1363 SurfaceGeometry::Cylinder(s) => {
1364 elementary::cylinder_parameters(&s.cylinder(), p, tol).ok()?
1365 }
1366 SurfaceGeometry::Cone(s) => elementary::cone_parameters(&s.cone(), p, tol).ok()?,
1367 SurfaceGeometry::Sphere(s) => elementary::sphere_parameters(&s.sphere(), p, tol).ok()?,
1368 SurfaceGeometry::Torus(s) => elementary::torus_parameters(&s.torus(), p, tol).ok()?,
1369 _ => return None,
1370 };
1371 Some(ogeom_math::Point2::new(u, v))
1372}
1373
1374pub(crate) fn inside_box(pcurve: &PlanarCurve, surface: &SurfaceGeometry) -> Option<(f64, f64)> {
1377 let (o, d) = match pcurve {
1380 PlanarCurve::Line(line) => {
1381 let axis = line.axis();
1382 (axis.location, axis.direction.vector())
1383 }
1384 PlanarCurve::BSpline(spline)
1385 if spline.knots().degree() == 1 && spline.control_points().len() == 2 =>
1386 {
1387 let (t0, t1) = spline.knots().domain();
1388 let (p0, p1) = (
1389 spline.control_points()[0].point(),
1390 spline.control_points()[1].point(),
1391 );
1392 if t1 <= t0 {
1393 return None;
1394 }
1395 let rate = (p1 - p0) / (t1 - t0);
1396 (p0 - rate * t0, rate)
1397 }
1398 _ => return None,
1399 };
1400 let ((ua, ub), (va, vb)) = surface.domain();
1401
1402 let mut lo = f64::NEG_INFINITY;
1404 let mut hi = f64::INFINITY;
1405 for (origin, direction, low, high) in [(o.x, d.x, ua, ub), (o.y, d.y, va, vb)] {
1406 if direction.abs() <= f64::MIN_POSITIVE {
1407 if origin < low || origin > high {
1408 return None;
1409 }
1410 continue;
1411 }
1412 let (a, b) = ((low - origin) / direction, (high - origin) / direction);
1413 let (near, far) = if a < b { (a, b) } else { (b, a) };
1414 lo = lo.max(near);
1415 hi = hi.min(far);
1416 }
1417 if lo >= hi {
1418 return None;
1419 }
1420 Some((lo, hi))
1421}
1422
1423pub(crate) fn touches_box(
1425 pcurve: &PlanarCurve,
1426 surface: &SurfaceGeometry,
1427 tol: Tolerances,
1428) -> bool {
1429 use ogeom_geom::Curve2d;
1430 let ((ua, ub), (va, vb)) = surface.domain();
1431 let (lo, hi) = pcurve.domain();
1432 const SPANS: u32 = 64;
1441 let points: Vec<Option<ogeom_math::Point2>> = (0..=SPANS)
1442 .map(|i| {
1443 pcurve
1444 .point_at(lo + (hi - lo) * f64::from(i) / f64::from(SPANS), tol)
1445 .ok()
1446 })
1447 .collect();
1448 points.windows(2).any(|pair| {
1449 let (Some(p), Some(q)) = (pair[0], pair[1]) else {
1450 return false;
1451 };
1452 let pad = p.distance(q);
1453 let u_ok =
1455 surface.is_periodic_u() || (p.x.max(q.x) + pad >= ua && p.x.min(q.x) - pad <= ub);
1456 let v_ok =
1457 surface.is_periodic_v() || (p.y.max(q.y) + pad >= va && p.y.min(q.y) - pad <= vb);
1458 u_ok && v_ok
1459 })
1460}
1461
1462fn intersect_intervals(a: (f64, f64), b: Option<(f64, f64)>) -> Option<(f64, f64)> {
1464 let b = b?;
1465 let (lo, hi) = (a.0.max(b.0), a.1.min(b.1));
1466 if lo >= hi {
1467 return None;
1468 }
1469 Some((lo, hi))
1470}
1471
1472fn marched(
1474 a: &SurfaceGeometry,
1475 b: &SurfaceGeometry,
1476 windows: (Option<ParameterWindow>, Option<ParameterWindow>),
1477 options: IntersectOptions,
1478 tol: Tolerances,
1479) -> OgeomResult<SurfaceIntersection> {
1480 let view_a = windows.0.and_then(|w| windowed(a, w, tol));
1483 let view_b = windows.1.and_then(|w| windowed(b, w, tol));
1484 let mut pole_reach = 0.0;
1485 let (mut out, mut contacts, touched) = loop {
1486 let (traced, touched) = branches_from_touches(
1487 view_a.as_ref().unwrap_or(a),
1488 view_b.as_ref().unwrap_or(b),
1489 options.marching,
1490 pole_reach,
1491 tol,
1492 )?;
1493 if traced.is_empty() && touched.is_empty() {
1494 return Ok(SurfaceIntersection::Apart);
1495 }
1496 let mut out = Vec::with_capacity(traced.len());
1497 let mut contacts: Vec<crate::march::Traced> = Vec::new();
1498 let mut again: Option<f64> = None;
1499 let domains = (
1504 view_a.as_ref().unwrap_or(a).domain(),
1505 view_b.as_ref().unwrap_or(b).domain(),
1506 );
1507 let on_border = |params: &[(f64, f64)], ((u0, u1), (v0, v1)): ((f64, f64), (f64, f64))| {
1508 let Some(&(u, v)) = params.first() else {
1509 return false;
1510 };
1511 ((u == u0 || u == u1) && params.iter().all(|p| p.0 == u))
1512 || ((v == v0 || v == v1) && params.iter().all(|p| p.1 == v))
1513 };
1514 for (branch, crossing) in &traced {
1515 let lying = on_border(&branch.on_a, domains.0) || on_border(&branch.on_b, domains.1);
1528 if !lying && !crossing && branch_is_tangential(a, b, branch, tol)? {
1529 if let Some(contact) = walk_contact(a, b, branch, &contacts, options.marching, tol)?
1530 {
1531 contacts.push(contact);
1532 }
1533 continue;
1534 }
1535 if branch.stopped == crate::march::Stopped::RanOut {
1536 ogeom_bail!(
1537 NotDone,
1538 "a marched section ran out of its point budget before \
1539 finishing; the seam is longer than the chord affords and \
1540 fitting the truncation would state a curve that is not there"
1541 );
1542 }
1543 let fitted = fitted_in_pieces(a, b, branch, options, tol)?;
1552 if pole_reach == 0.0 {
1556 for f in &fitted {
1557 let stated = options.marching.chord + f.fit_error;
1558 if crate::march::passes_a_pole(&f.curve.clone().into(), [a, b], stated, tol) {
1559 again = Some(again.map_or(stated, |r: f64| r.max(stated)));
1560 }
1561 }
1562 }
1563 for fitted in fitted {
1564 out.push(SectionCurve {
1565 curve: fitted.curve.into(),
1566 on_a: Some(fitted.on_a.into()),
1567 on_b: Some(fitted.on_b.into()),
1568 tolerance: (options.marching.chord + fitted.fit_error).max(pole_reach),
1573 exact: false,
1574 closed: fitted.closed,
1575 tangential: false,
1576 });
1577 }
1578 }
1579 if let Some(reach) = again {
1580 pole_reach = reach;
1581 continue;
1582 }
1583 break (out, contacts, touched);
1584 };
1585 for walked in touched {
1588 let on_one = walked
1589 .points
1590 .get(walked.points.len() / 2)
1591 .is_some_and(|&p| on_one_already(p, &contacts, options.marching, tol));
1592 if !on_one {
1593 contacts.push(walked);
1594 }
1595 }
1596 for contact in &contacts {
1597 let fitted = approximate_branch(a, b, contact, options.tolerance, tol)?;
1598 out.push(SectionCurve {
1599 curve: fitted.curve.into(),
1600 on_a: Some(fitted.on_a.into()),
1601 on_b: Some(fitted.on_b.into()),
1602 tolerance: options.marching.chord + fitted.fit_error,
1603 exact: false,
1604 closed: fitted.closed,
1605 tangential: true,
1606 });
1607 }
1608 if out.is_empty() {
1609 return Ok(SurfaceIntersection::Apart);
1610 }
1611 Ok(SurfaceIntersection::Along(out))
1612}
1613
1614fn windowed(
1617 surface: &SurfaceGeometry,
1618 window: ParameterWindow,
1619 tol: Tolerances,
1620) -> Option<SurfaceGeometry> {
1621 let (du, dv) = surface.domain();
1622 let eps = tol.parametric();
1623 let narrow = |want: (f64, f64), have: (f64, f64), periodic: bool| -> Option<(f64, f64)> {
1628 if periodic {
1629 return (want.1 - want.0 < have.1 - have.0 - eps).then_some(want);
1630 }
1631 Some((want.0.max(have.0), want.1.min(have.1)))
1632 };
1633 let finite = |w: (f64, f64)| w.0.is_finite() && w.1.is_finite() && w.1 > w.0;
1634 if !finite(window.0) || !finite(window.1) {
1635 return None;
1636 }
1637 let narrower = |got: (f64, f64), have: (f64, f64)| {
1638 got.1 - got.0 < have.1 - have.0 - eps || got.0 > have.0 + eps || got.1 < have.1 - eps
1639 };
1640 let u = narrow(window.0, du, surface.is_periodic_u()).filter(|&u| narrower(u, du));
1641 let v = narrow(window.1, dv, surface.is_periodic_v()).filter(|&v| narrower(v, dv));
1642 if u.is_some_and(|u| u.1 - u.0 <= eps) || v.is_some_and(|v| v.1 - v.0 <= eps) {
1643 return None;
1644 }
1645 let base: Option<SurfaceGeometry> = match (surface, v) {
1648 (SurfaceGeometry::Cylinder(c), Some(v)) => {
1649 ogeom_geom::CylinderSurface::new(c.cylinder(), v)
1650 .ok()
1651 .map(Into::into)
1652 }
1653 (SurfaceGeometry::Cone(c), Some(v)) => ogeom_geom::ConeSurface::new(c.cone(), v)
1654 .ok()
1655 .map(Into::into),
1656 _ => None,
1657 };
1658 let trim = |basis: SurfaceGeometry, u: (f64, f64), v: (f64, f64)| {
1659 ogeom_geom::TrimmedSurface::new(basis, u, v, tol)
1660 .ok()
1661 .map(Into::into)
1662 };
1663 match (base, u) {
1664 (Some(base), Some(u)) => {
1665 let v = base.domain().1;
1666 trim(base, u, v)
1667 }
1668 (Some(base), None) => Some(base),
1669 (None, _) => {
1670 let wraps_u = surface.is_periodic_u() && u.is_none();
1673 let wraps_v = surface.is_periodic_v() && v.is_none();
1674 if (u.is_none() && v.is_none()) || wraps_u || wraps_v {
1675 return None;
1676 }
1677 trim(surface.clone(), u.unwrap_or(du), v.unwrap_or(dv))
1678 }
1679 }
1680}
1681
1682fn fitted_in_pieces(
1703 a: &SurfaceGeometry,
1704 b: &SurfaceGeometry,
1705 branch: &crate::march::Traced,
1706 options: IntersectOptions,
1707 tol: Tolerances,
1708) -> OgeomResult<Vec<crate::approx::IntersectionCurve>> {
1709 const DEPTH: u32 = 6;
1710 const FLOOR: usize = 16;
1711 fn go(
1712 a: &SurfaceGeometry,
1713 b: &SurfaceGeometry,
1714 branch: &crate::march::Traced,
1715 options: IntersectOptions,
1716 depth: u32,
1717 tol: Tolerances,
1718 ) -> OgeomResult<Vec<crate::approx::IntersectionCurve>> {
1719 let tolerance = options.tolerance;
1720 let whole = approximate_branch(a, b, branch, tolerance, tol)?;
1721 let points = &branch.points;
1725 let inner = points.get(1..points.len().saturating_sub(1)).unwrap_or(&[]);
1726 let step = inner
1727 .windows(2)
1728 .map(|w| w[0].distance(w[1]))
1729 .fold(f64::INFINITY, f64::min);
1730 if whole.met
1731 || whole.fit_error <= step.min(tolerance * 1e2)
1732 || depth == 0
1733 || branch.points.len() < 2 * FLOOR
1734 {
1735 return Ok(vec![filled_in(a, b, branch, whole, options, tol)?]);
1736 }
1737 let middle = branch.points.len() / 2;
1738 let stopped = if branch.closed() {
1740 crate::march::Stopped::Stalled
1741 } else {
1742 branch.stopped
1743 };
1744 let half = |range: core::ops::RangeInclusive<usize>| crate::march::Traced {
1745 points: branch.points[range.clone()].to_vec(),
1746 on_a: branch.on_a[range.clone()].to_vec(),
1747 on_b: branch.on_b[range].to_vec(),
1748 stopped,
1749 };
1750 let mut pieces = go(a, b, &half(0..=middle), options, depth - 1, tol)?;
1751 pieces.extend(go(
1752 a,
1753 b,
1754 &half(middle..=branch.points.len() - 1),
1755 options,
1756 depth - 1,
1757 tol,
1758 )?);
1759 let worst = pieces.iter().map(|p| p.fit_error).fold(0.0_f64, f64::max);
1762 Ok(if worst < whole.fit_error {
1763 pieces
1764 } else {
1765 vec![whole]
1766 })
1767 }
1768 go(a, b, branch, options, DEPTH, tol)
1769}
1770
1771fn filled_in(
1784 a: &SurfaceGeometry,
1785 b: &SurfaceGeometry,
1786 branch: &crate::march::Traced,
1787 whole: crate::approx::IntersectionCurve,
1788 options: IntersectOptions,
1789 tol: Tolerances,
1790) -> OgeomResult<crate::approx::IntersectionCurve> {
1791 let mut best = whole;
1792 let mut dense = branch.clone();
1793 while best.fit_error > options.tolerance {
1794 let marching = options.marching;
1795 let Some(more) = crate::march::with_middles(
1796 (a, b),
1797 &dense,
1798 (options.tolerance, marching.chord),
1799 marching.max_points,
1800 tol,
1801 ) else {
1802 break;
1803 };
1804 if more.points.len() == dense.points.len() {
1805 break;
1806 }
1807 match approximate_branch(a, b, &more, options.tolerance, tol) {
1808 Ok(again) if again.fit_error < best.fit_error => best = again,
1809 _ => break,
1810 }
1811 dense = more;
1812 }
1813 Ok(best)
1814}
1815
1816fn walk_contact(
1825 a: &SurfaceGeometry,
1826 b: &SurfaceGeometry,
1827 fragment: &crate::march::Traced,
1828 already: &[crate::march::Traced],
1829 marching: Marching,
1830 tol: Tolerances,
1831) -> OgeomResult<Option<crate::march::Traced>> {
1832 let middle = fragment.points.len() / 2;
1833 let Some(point) = fragment.points.get(middle).copied() else {
1834 return Ok(None);
1835 };
1836 if on_one_already(point, already, marching, tol) {
1837 return Ok(None);
1838 }
1839 let seed = crate::march::Contact {
1840 point,
1841 on_a: fragment.on_a[middle],
1842 on_b: fragment.on_b[middle],
1843 };
1844 Ok(trace_tangential(a, b, seed, marching, tol)
1849 .ok()
1850 .filter(|traced| traced.points.len() >= 4))
1851}
1852
1853fn on_one_already(
1857 point: Point,
1858 already: &[crate::march::Traced],
1859 marching: Marching,
1860 tol: Tolerances,
1861) -> bool {
1862 already.iter().any(|traced| {
1863 let spacing = traced
1864 .points
1865 .windows(2)
1866 .map(|w| w[0].distance(w[1]))
1867 .fold(0.0f64, f64::max);
1868 let near = traced
1869 .points
1870 .iter()
1871 .map(|p| p.distance(point))
1872 .fold(f64::INFINITY, f64::min);
1873 near <= spacing.mul_add(0.5, marching.chord.max(tol.confusion()))
1874 })
1875}
1876
1877fn branch_is_tangential(
1880 a: &SurfaceGeometry,
1881 b: &SurfaceGeometry,
1882 branch: &crate::march::Traced,
1883 tol: Tolerances,
1884) -> OgeomResult<bool> {
1885 use ogeom_geom::Surface as _;
1886 let count = branch.points.len();
1887 if count == 0 {
1888 return Ok(true);
1889 }
1890 let exact = exact_surface(a) && exact_surface(b);
1891 for k in 0..5 {
1892 let i = ((k * (count - 1)) / 4).min(count - 1);
1893 let (ua, va) = branch.on_a[i];
1894 let (ub, vb) = branch.on_b[i];
1895 let (dau, dav) = a.d1_at(ua, va, tol)?;
1896 let (dbu, dbv) = b.d1_at(ub, vb, tol)?;
1897 let na = dau.cross(dav);
1898 let nb = dbu.cross(dbv);
1899 let (ma, mb) = (na.magnitude(), nb.magnitude());
1900 if ma <= tol.confusion() || mb <= tol.confusion() {
1901 continue;
1902 }
1903 if na.cross(nb).magnitude() / (ma * mb) > TANGENTIAL_SINE {
1910 return Ok(false);
1911 }
1912 let at = crate::march::Contact {
1916 point: branch.points[i],
1917 on_a: (ua, va),
1918 on_b: (ub, vb),
1919 };
1920 if exact && crate::march::crosses_measurably(a, b, at, tol) {
1921 return Ok(false);
1922 }
1923 }
1924 Ok(true)
1925}
1926
1927fn exact_surface(surface: &SurfaceGeometry) -> bool {
1932 fn exact_curve(curve: &Curve) -> bool {
1933 match curve {
1934 Curve::Line(_)
1935 | Curve::Circle(_)
1936 | Curve::Ellipse(_)
1937 | Curve::Hyperbola(_)
1938 | Curve::Parabola(_)
1939 | Curve::Helix(_) => true,
1940 Curve::Trimmed(t) => exact_curve(t.basis()),
1941 _ => false,
1942 }
1943 }
1944 match surface {
1945 SurfaceGeometry::Plane(_)
1946 | SurfaceGeometry::Cylinder(_)
1947 | SurfaceGeometry::Cone(_)
1948 | SurfaceGeometry::Sphere(_)
1949 | SurfaceGeometry::Torus(_) => true,
1950 SurfaceGeometry::Extrusion(e) => exact_curve(e.curve()),
1951 SurfaceGeometry::Revolution(r) => exact_curve(r.curve()),
1952 SurfaceGeometry::Trimmed(t) => exact_surface(t.basis()),
1953 _ => false,
1954 }
1955}
1956
1957#[must_use]
1965pub fn exact_pcurve_of(
1966 curve: &Curve,
1967 surface: &SurfaceGeometry,
1968 tol: Tolerances,
1969) -> Option<PlanarCurve> {
1970 exact_pcurve(curve, curve.domain(), surface, tol)
1971}
1972
1973#[must_use]
1982pub fn exact_pcurve_over(
1983 curve: &Curve,
1984 range: (f64, f64),
1985 surface: &SurfaceGeometry,
1986 tol: Tolerances,
1987) -> Option<PlanarCurve> {
1988 exact_pcurve(curve, range, surface, tol)
1989}
1990
1991fn exact_pcurve(
2001 curve: &Curve,
2002 range: (f64, f64),
2003 surface: &SurfaceGeometry,
2004 tol: Tolerances,
2005) -> Option<PlanarCurve> {
2006 if let Curve::Trimmed(trimmed) = curve {
2015 let window = ogeom_geom::Curve3d::domain(&**trimmed);
2016 let range = if trimmed.is_reversed() {
2017 let sum = window.0 + window.1;
2018 (sum - range.1, sum - range.0)
2019 } else {
2020 range
2021 };
2022 let basis = exact_pcurve(trimmed.basis(), range, surface, tol)?;
2023 let pcurve: PlanarCurve = ogeom_geom::Trimmed2d::new(basis, window.0, window.1, tol)
2024 .ok()?
2025 .into();
2026 return Some(if trimmed.is_reversed() {
2027 ogeom_geom::Reversible::reversed(&pcurve)
2028 } else {
2029 pcurve
2030 });
2031 }
2032 let forward = match curve {
2036 Curve::Circle(c) if c.is_reversed() => Some(Curve::Circle(c.forward(tol).ok()?)),
2037 Curve::Ellipse(e) if e.is_reversed() => Some(Curve::Ellipse(e.forward(tol).ok()?)),
2038 _ => None,
2039 };
2040 if let Some(forward) = forward {
2041 return exact_pcurve(&forward, range, surface, tol);
2042 }
2043 if let Curve::Circle(c) = curve
2050 && c.circle().frame().handedness() == ogeom_math::Handedness::Left
2051 {
2052 let frame = c.circle().frame();
2053 let right =
2054 ogeom_math::Frame::from_axes(frame.origin(), frame.x(), frame.y(), -frame.z(), tol)
2055 .ok()?;
2056 let circle = ogeom_math::Circle::new(right, c.circle().radius(), tol).ok()?;
2057 return exact_pcurve(
2058 &Curve::Circle(ogeom_geom::CircleCurve::new(circle)),
2059 range,
2060 surface,
2061 tol,
2062 );
2063 }
2064 if let (Curve::Offset(offset), SurfaceGeometry::Plane(p)) = (curve, surface) {
2065 return offset_on_plane(offset, range, surface, p.plane(), tol);
2066 }
2067 match surface {
2068 SurfaceGeometry::Plane(p) => on_plane(curve, p.plane(), tol),
2069 SurfaceGeometry::Cylinder(c) => on_cylinder(curve, range, c.cylinder(), tol),
2070 SurfaceGeometry::Sphere(s) => on_sphere(curve, range, s.sphere(), tol),
2071 SurfaceGeometry::Torus(t) => on_torus(curve, range, t.torus(), tol),
2072 SurfaceGeometry::Cone(c) => on_cone(curve, range, c.cone(), tol),
2073 _ => None,
2074 }
2075}
2076
2077fn circle_window(range: (f64, f64)) -> (f64, f64) {
2083 let tau = core::f64::consts::TAU;
2084 if range.0.is_finite() && range.1.is_finite() {
2085 (range.0.min(range.1).min(0.0), range.0.max(range.1).max(tau))
2086 } else {
2087 (0.0, tau)
2088 }
2089}
2090
2091fn spin(frame: &ogeom_math::Frame) -> ogeom_math::Vector {
2096 frame.z().vector() * frame.handedness().sign()
2097}
2098
2099fn on_cone(
2108 curve: &Curve,
2109 range: (f64, f64),
2110 cone: ogeom_math::Cone,
2111 tol: Tolerances,
2112) -> Option<PlanarCurve> {
2113 let frame = cone.frame();
2114 let axis_z = frame.z().vector();
2115 let tau = core::f64::consts::TAU;
2116 match curve {
2117 Curve::Circle(c) => {
2118 let circle = c.circle();
2119 if circle.frame().z().vector().cross(axis_z).magnitude() > tol.angular() {
2120 return None;
2121 }
2122 let local = frame.to_local(circle.centre());
2123 if local.x.hypot(local.y) > tol.confusion() {
2124 return None;
2125 }
2126 let expected = cone
2130 .half_angle()
2131 .tan()
2132 .mul_add(local.z, cone.reference_radius());
2133 let turned = if (expected - circle.radius()).abs() <= tol.confusion() * 10.0 {
2134 0.0
2135 } else if (expected + circle.radius()).abs() <= tol.confusion() * 10.0 {
2136 core::f64::consts::PI
2137 } else {
2138 return None;
2139 };
2140 let start = circle.centre() + circle.frame().x().vector() * circle.radius();
2141 let at = frame.to_local(start);
2142 let phase = at.y.atan2(at.x) + turned;
2143 let winding = circle.frame().z().vector().dot(spin(&frame)).signum();
2144 let towards =
2145 ogeom_math::Direction2::new(ogeom_math::Vector2::new(winding, 0.0), tol).ok()?;
2146 Some(
2147 Line2d::over(
2148 ogeom_math::Axis2::new(Point2::new(phase, local.z), towards),
2149 circle_window(range).0,
2150 circle_window(range).1,
2151 )
2152 .ok()?
2153 .into(),
2154 )
2155 }
2156 Curve::Line(line) => {
2157 let axis = line.axis();
2160 let on = |t: f64| {
2161 let p = axis.location + axis.direction.vector() * t;
2162 cone.distance_to(p) <= tol.confusion() * 10.0
2163 };
2164 if !on(0.0) || !on(1.0) || !on(-1.0) {
2165 return None;
2166 }
2167 let (lo, hi) = if range.0.is_finite() && range.1.is_finite() && range.0 != range.1 {
2174 range
2175 } else {
2176 line.domain()
2177 };
2178 let mut local: Option<ogeom_math::Point> = None;
2184 for t in [lo, hi] {
2185 if !t.is_finite() {
2186 continue;
2187 }
2188 let candidate = frame.to_local(axis.location + axis.direction.vector() * t);
2189 if local.is_none_or(|held| candidate.x.hypot(candidate.y) > held.x.hypot(held.y)) {
2190 local = Some(candidate);
2191 }
2192 }
2193 let local = local?;
2194 if local.x.hypot(local.y) <= tol.confusion() {
2195 return None;
2196 }
2197 let side = cone.radius_at(local.z).signum();
2203 let u = (local.y * side).atan2(local.x * side).rem_euclid(tau);
2204 let v_at = |t: f64| {
2208 frame
2209 .to_local(axis.location + axis.direction.vector() * t)
2210 .z
2211 };
2212 let knots = ogeom_math::KnotVector::new(vec![lo, lo, hi, hi], 1).ok()?;
2213 Some(
2214 ogeom_geom::BSpline2d::new(
2215 knots,
2216 vec![Point2::new(u, v_at(lo)), Point2::new(u, v_at(hi))],
2217 tol,
2218 )
2219 .ok()?
2220 .into(),
2221 )
2222 }
2223 _ => None,
2224 }
2225}
2226
2227fn on_torus(
2237 curve: &Curve,
2238 range: (f64, f64),
2239 torus: ogeom_math::Torus,
2240 tol: Tolerances,
2241) -> Option<PlanarCurve> {
2242 let Curve::Circle(c) = curve else {
2243 return None;
2244 };
2245 let circle = c.circle();
2246 let frame = torus.frame();
2247 let axis_z = frame.z().vector();
2248 let normal = circle.frame().z().vector();
2249 let local = frame.to_local(circle.centre());
2250
2251 if normal.cross(axis_z).magnitude() <= tol.angular()
2253 && local.x.hypot(local.y) <= tol.confusion()
2254 {
2255 let sin_v = local.z / torus.minor_radius();
2256 let (cos_v, turned) = [
2260 (circle.radius() - torus.major_radius(), 0.0),
2261 (
2262 -circle.radius() - torus.major_radius(),
2263 core::f64::consts::PI,
2264 ),
2265 ]
2266 .into_iter()
2267 .map(|(reach, turned)| (reach / torus.minor_radius(), turned))
2268 .find(|(cos_v, _)| (sin_v.hypot(*cos_v) - 1.0).abs() <= tol.confusion())?;
2269 let v = sin_v.atan2(cos_v);
2270 let start = circle.centre() + circle.frame().x().vector() * circle.radius();
2271 let at = frame.to_local(start);
2272 let phase = at.y.atan2(at.x) + turned;
2273 let winding = normal.dot(spin(&frame)).signum();
2274 let towards =
2275 ogeom_math::Direction2::new(ogeom_math::Vector2::new(winding, 0.0), tol).ok()?;
2276 return Some(
2277 Line2d::over(
2278 ogeom_math::Axis2::new(Point2::new(phase, v), towards),
2279 circle_window(range).0,
2280 circle_window(range).1,
2281 )
2282 .ok()?
2283 .into(),
2284 );
2285 }
2286
2287 if (circle.radius() - torus.minor_radius()).abs() <= tol.confusion()
2289 && normal.dot(axis_z).abs() <= tol.angular()
2290 && (local.x.hypot(local.y) - torus.major_radius()).abs() <= tol.confusion()
2291 && local.z.abs() <= tol.confusion()
2292 {
2293 let u = local.y.atan2(local.x);
2294 let radial = frame.x().vector() * u.cos() + frame.y().vector() * u.sin();
2295 let xc = circle.frame().x().vector();
2296 let phase = xc.dot(axis_z).atan2(xc.dot(radial));
2297 let winding = normal.dot(radial.cross(axis_z)).signum();
2298 let towards =
2299 ogeom_math::Direction2::new(ogeom_math::Vector2::new(0.0, winding), tol).ok()?;
2300 return Some(
2301 Line2d::over(
2302 ogeom_math::Axis2::new(Point2::new(u, phase), towards),
2303 circle_window(range).0,
2304 circle_window(range).1,
2305 )
2306 .ok()?
2307 .into(),
2308 );
2309 }
2310 None
2311}
2312
2313fn offset_on_plane(
2326 offset: &ogeom_geom::OffsetCurve,
2327 range: (f64, f64),
2328 surface: &SurfaceGeometry,
2329 plane: ogeom_math::Plane,
2330 tol: Tolerances,
2331) -> Option<PlanarCurve> {
2332 let frame = plane.frame();
2333 let up = frame.x().vector().cross(frame.y().vector());
2334 let reference = offset.reference().vector();
2335 let (cos, sin) = (reference.dot(up), reference.cross(up).magnitude());
2336 if cos == 0.0 || offset.distance().abs() * sin > tol.confusion() * cos.abs() {
2337 return None;
2338 }
2339 let basis = exact_pcurve(offset.basis(), range, surface, tol)?;
2340 Offset2d::new(basis, -cos.signum() * offset.distance())
2341 .ok()
2342 .map(|o| PlanarCurve::Offset(Box::new(o)))
2343}
2344
2345fn on_plane(curve: &Curve, plane: ogeom_math::Plane, tol: Tolerances) -> Option<PlanarCurve> {
2352 let frame = plane.frame();
2353 let flat = |p: Point| {
2354 let local = frame.to_local(p);
2355 Point2::new(local.x, local.y)
2356 };
2357 let flat_direction = |d: ogeom_math::Direction| {
2362 let local = frame.vector_to_local(d.vector());
2363 ogeom_math::Direction2::new(ogeom_math::Vector2::new(local.x, local.y), tol).ok()
2364 };
2365 match curve {
2366 Curve::Line(line) => {
2367 let axis = line.axis();
2368 let through = flat(axis.location);
2369 let direction = flat_direction(axis.direction)?;
2370 let (lo, hi) = line.domain();
2371 Some(
2372 Line2d::over(ogeom_math::Axis2::new(through, direction), lo, hi)
2373 .ok()?
2374 .into(),
2375 )
2376 }
2377 Curve::Circle(c) => {
2378 let circle = c.circle();
2379 let frame2 = Frame2::from_axes(
2380 flat(circle.centre()),
2381 flat_direction(circle.frame().x())?,
2382 flat_direction(circle.frame().y())?,
2383 tol,
2384 )
2385 .ok()?;
2386 Some(Circle2d::new(Circle2::new(frame2, circle.radius(), tol).ok()?).into())
2387 }
2388 Curve::Ellipse(e) => {
2389 let ellipse = e.ellipse();
2390 let frame2 = Frame2::from_axes(
2391 flat(ellipse.centre()),
2392 flat_direction(ellipse.frame().x())?,
2393 flat_direction(ellipse.frame().y())?,
2394 tol,
2395 )
2396 .ok()?;
2397 Some(
2398 Ellipse2d::new(
2399 Ellipse2::new(frame2, ellipse.major_radius(), ellipse.minor_radius(), tol)
2400 .ok()?,
2401 )
2402 .into(),
2403 )
2404 }
2405 Curve::Hyperbola(h) => {
2406 let hyperbola = h.hyperbola();
2414 let (lo, hi) = h.domain();
2415 let turned = h.is_reversed() && lo + hi == 0.0;
2416 let y = if turned {
2417 -hyperbola.frame().y()
2418 } else {
2419 hyperbola.frame().y()
2420 };
2421 let frame2 = Frame2::from_axes(
2422 flat(hyperbola.centre()),
2423 flat_direction(hyperbola.frame().x())?,
2424 flat_direction(y)?,
2425 tol,
2426 )
2427 .ok()?;
2428 let flat_hyperbola = ogeom_math::Hyperbola2::new(
2429 frame2,
2430 hyperbola.major_radius(),
2431 hyperbola.minor_radius(),
2432 tol,
2433 )
2434 .ok()?;
2435 let image: PlanarCurve = ogeom_geom::Hyperbola2d::over(flat_hyperbola, lo, hi)
2436 .ok()?
2437 .into();
2438 Some(if h.is_reversed() && !turned {
2439 ogeom_geom::Reversible::reversed(&image)
2440 } else {
2441 image
2442 })
2443 }
2444 Curve::BSpline(b) => {
2445 let control = b
2450 .control_points()
2451 .iter()
2452 .map(|w| ogeom_math::Weighted::new(flat((*w).point()), w.weight, tol))
2453 .collect::<Result<Vec<_>, _>>()
2454 .ok()?;
2455 Some(
2456 ogeom_geom::BSpline2d::rational(b.knots().clone(), control)
2457 .ok()?
2458 .into(),
2459 )
2460 }
2461 Curve::Parabola(parabola) => {
2462 let (lo, hi) = parabola.domain();
2467 let start = parabola.point_at(lo, tol).ok()?;
2468 let middle = start + parabola.d1_at(lo, tol).ok()? * ((hi - lo) / 2.0);
2469 let end = parabola.point_at(hi, tol).ok()?;
2470 let knots = ogeom_math::KnotVector::new(vec![lo, lo, lo, hi, hi, hi], 2).ok()?;
2471 Some(
2472 ogeom_geom::BSpline2d::new(knots, vec![flat(start), flat(middle), flat(end)], tol)
2473 .ok()?
2474 .into(),
2475 )
2476 }
2477 Curve::OnSurface(on) => {
2478 let chart = chart_plane(on.surface())?.frame();
2484 if !chart.z().is_parallel(frame.z(), tol)
2485 || plane.distance_to(chart.origin()) > tol.confusion()
2486 {
2487 return None;
2488 }
2489 let frame2 = Frame2::from_axes(
2490 flat(chart.origin()),
2491 flat_direction(chart.x())?,
2492 flat_direction(chart.y())?,
2493 tol,
2494 )
2495 .ok()?;
2496 on.pcurve()
2497 .transformed(&ogeom_math::Transform2::from_frame(&frame2), tol)
2498 .ok()
2499 }
2500 _ => None,
2501 }
2502}
2503
2504fn chart_plane(surface: &SurfaceGeometry) -> Option<ogeom_math::Plane> {
2507 match surface {
2508 SurfaceGeometry::Plane(p) => Some(p.plane()),
2509 SurfaceGeometry::Trimmed(t) => chart_plane(t.basis()),
2510 _ => None,
2511 }
2512}
2513
2514fn on_cylinder(
2521 curve: &Curve,
2522 range: (f64, f64),
2523 cylinder: ogeom_math::Cylinder,
2524 tol: Tolerances,
2525) -> Option<PlanarCurve> {
2526 let axis = cylinder.axis();
2527 let frame = cylinder.frame();
2528 match curve {
2529 Curve::Line(line) => {
2530 let direction = line.axis().direction;
2532 let along = direction.dot(axis.direction);
2533 if !direction.is_parallel(axis.direction, tol) {
2534 return None;
2535 }
2536 let through = line.axis().location;
2537 if (axis.distance_to(through) - cylinder.radius()).abs() > tol.confusion() {
2538 return None;
2539 }
2540 let local = frame.to_local(through);
2541 let u = local.y.atan2(local.x).rem_euclid(core::f64::consts::TAU);
2542 let (lo, hi) = line.domain();
2546 let start = Point2::new(u, local.z);
2547 let towards =
2548 ogeom_math::Direction2::new(ogeom_math::Vector2::new(0.0, along.signum()), tol)
2549 .ok()?;
2550 Some(
2551 Line2d::over(ogeom_math::Axis2::new(start, towards), lo, hi)
2552 .ok()?
2553 .into(),
2554 )
2555 }
2556 Curve::Circle(c) => {
2557 let circle = c.circle();
2558 if circle
2560 .frame()
2561 .z()
2562 .cross_with(axis.direction.vector())
2563 .magnitude()
2564 > tol.angular()
2565 {
2566 return None;
2567 }
2568 if axis.distance_to(circle.centre()) > tol.confusion() {
2569 return None;
2570 }
2571 if (circle.radius() - cylinder.radius()).abs() > tol.confusion() {
2572 return None;
2573 }
2574 let local = frame.to_local(circle.centre());
2575 let start = circle.centre() + circle.frame().x().vector() * circle.radius();
2584 let at = frame.to_local(start);
2585 let phase = at.y.atan2(at.x);
2586 let winding = circle.frame().z().vector().dot(spin(&frame)).signum();
2587 let towards =
2588 ogeom_math::Direction2::new(ogeom_math::Vector2::new(winding, 0.0), tol).ok()?;
2589 Some(
2590 Line2d::over(
2591 ogeom_math::Axis2::new(Point2::new(phase, local.z), towards),
2592 circle_window(range).0,
2593 circle_window(range).1,
2594 )
2595 .ok()?
2596 .into(),
2597 )
2598 }
2599 Curve::Ellipse(_) => {
2600 use ogeom_geom::Curve3d as _;
2606 let tau = core::f64::consts::TAU;
2607 let local = |t: f64| -> Option<ogeom_math::Point> {
2608 Some(frame.to_local(curve.point_at(t, tol).ok()?))
2609 };
2610 let l0 = local(0.0)?;
2611 let lq = local(tau / 4.0)?;
2612 let lh = local(tau / 2.0)?;
2613 let r = cylinder.radius();
2615 for l in [&l0, &lq, &lh] {
2616 if (l.x.hypot(l.y) - r).abs() > tol.confusion() * 10.0 {
2617 return None;
2618 }
2619 }
2620 let phase = l0.y.atan2(l0.x);
2621 let uq = lq.y.atan2(lq.x);
2624 let step = (uq - phase).rem_euclid(tau);
2625 let winding = if (step - tau / 4.0).abs() < 1e-6 {
2626 1.0
2627 } else if (step - 3.0 * tau / 4.0).abs() < 1e-6 {
2628 -1.0
2629 } else {
2630 return None;
2631 };
2632 let c0 = f64::midpoint(l0.z, lh.z);
2634 let a = (l0.z - lh.z) / 2.0;
2635 let b = lq.z - c0;
2636 let candidate = ogeom_geom::Trig2d::new(
2640 Point2::new(phase, c0),
2641 ogeom_math::Vector2::new(winding, 0.0),
2642 ogeom_math::Vector2::new(0.0, a),
2643 ogeom_math::Vector2::new(0.0, b),
2644 range,
2645 )
2646 .ok()?;
2647 use ogeom_geom::Curve2d as _;
2650 for i in 0..7 {
2651 let t = range.0 + (range.1 - range.0) * (0.09 + 0.13 * f64::from(i)) / 0.91;
2652 let l = local(t)?;
2653 let chart = candidate.point_at(t, tol).ok()?;
2654 let du = (chart.x - l.y.atan2(l.x)).rem_euclid(tau);
2655 if du.min(tau - du) > 1e-9 {
2656 return None;
2657 }
2658 if (chart.y - l.z).abs() > tol.confusion() * 10.0 {
2659 return None;
2660 }
2661 }
2662 Some(PlanarCurve::Trig(candidate))
2663 }
2664 _ => None,
2665 }
2666}
2667
2668fn on_meridian(
2686 curve: &ogeom_geom::CircleCurve,
2687 range: (f64, f64),
2688 sphere: ogeom_math::Sphere,
2689 tol: Tolerances,
2690) -> Option<PlanarCurve> {
2691 let circle = curve.circle();
2692 let sweep = if curve.is_reversed() { -1.0 } else { 1.0 };
2697 let frame = sphere.frame();
2698 let z = frame.z().vector();
2699 if circle.centre().distance(sphere.centre()) > tol.confusion() {
2702 return None;
2703 }
2704 if (circle.radius() - sphere.radius()).abs() > tol.confusion() {
2705 return None;
2706 }
2707 let (cx, cy) = (circle.frame().x().vector(), circle.frame().y().vector());
2708 let (xz, yz) = (cx.dot(z), cy.dot(z));
2709 if xz.hypot(yz) < 1.0 - tol.angular() {
2712 return None;
2713 }
2714 let raw_alpha = yz.atan2(xz);
2715 let w = cx * -raw_alpha.sin() + cy * raw_alpha.cos();
2718 let local = frame.to_local(sphere.centre() + w);
2719 let longitude = local.y.atan2(local.x);
2720
2721 let half = core::f64::consts::PI;
2722 let mid = f64::midpoint(range.0, range.1);
2723 let x_mid = (sweep * mid - raw_alpha).rem_euclid(core::f64::consts::TAU);
2726 let x_mid = if x_mid > half {
2727 x_mid - core::f64::consts::TAU
2728 } else {
2729 x_mid
2730 };
2731 let span = sweep * (range.1 - range.0);
2732 let (mut x0, mut x1) = (x_mid - span / 2.0, x_mid + span / 2.0);
2733 if x0 > x1 {
2734 core::mem::swap(&mut x0, &mut x1);
2735 }
2736 let alpha = sweep.mul_add(mid, -x_mid);
2741 let slack = tol
2745 .parametric()
2746 .max(1e-9)
2747 .max(tol.confusion() / sphere.radius());
2748 let (axis_point, towards) = if x0 >= -slack && x1 <= half + slack {
2749 (
2752 Point2::new(longitude, half.mul_add(0.5, alpha)),
2753 ogeom_math::Vector2::new(0.0, -sweep),
2754 )
2755 } else if x0 >= -half - slack && x1 <= slack {
2756 (
2758 Point2::new(longitude + half, half.mul_add(0.5, -alpha)),
2759 ogeom_math::Vector2::new(0.0, sweep),
2760 )
2761 } else {
2762 return None;
2764 };
2765 let towards = ogeom_math::Direction2::new(towards, tol).ok()?;
2766 let margin = (range.1 - range.0) * 0.25;
2767 let line: PlanarCurve = Line2d::over(
2768 ogeom_math::Axis2::new(axis_point, towards),
2769 range.0 - margin,
2770 range.1 + margin,
2771 )
2772 .ok()?
2773 .into();
2774
2775 for k in 0..=4 {
2778 let t = (range.1 - range.0).mul_add(f64::from(k) / 4.0, range.0);
2779 let uv = line.point_at(t, tol).ok()?;
2780 let lifted = ogeom_math::elementary::sphere_at(&sphere, uv.x, uv.y).point;
2781 let want = curve.point_at(t, tol).ok()?;
2782 if lifted.distance(want) > tol.confusion() {
2783 return None;
2784 }
2785 }
2786 Some(line)
2787}
2788
2789fn on_sphere(
2792 curve: &Curve,
2793 range: (f64, f64),
2794 sphere: ogeom_math::Sphere,
2795 tol: Tolerances,
2796) -> Option<PlanarCurve> {
2797 let Curve::Circle(c) = curve else {
2798 return None;
2799 };
2800 let circle = c.circle();
2801 let frame = sphere.frame();
2802 if circle
2805 .frame()
2806 .z()
2807 .cross_with(frame.z().vector())
2808 .magnitude()
2809 > tol.angular()
2810 {
2811 return on_meridian(c, range, sphere, tol);
2812 }
2813 let local = frame.to_local(circle.centre());
2814 if local.x.abs() > tol.confusion() || local.y.abs() > tol.confusion() {
2815 return None;
2816 }
2817 let latitude = (local.z / sphere.radius()).clamp(-1.0, 1.0).asin();
2818 if (circle.radius() - sphere.radius() * latitude.cos()).abs() > tol.confusion() {
2820 return None;
2821 }
2822 let start = circle.centre() + circle.frame().x().vector() * circle.radius();
2823 let at = frame.to_local(start);
2824 let phase = at.y.atan2(at.x);
2825 let winding = circle.frame().z().vector().dot(spin(&frame)).signum();
2828 let towards = ogeom_math::Direction2::new(ogeom_math::Vector2::new(winding, 0.0), tol).ok()?;
2829 Some(
2830 Line2d::over(
2831 ogeom_math::Axis2::new(Point2::new(phase, latitude), towards),
2832 circle_window(range).0,
2833 circle_window(range).1,
2834 )
2835 .ok()?
2836 .into(),
2837 )
2838}
2839
2840#[cfg(test)]
2841#[allow(clippy::unwrap_used, clippy::expect_used)]
2842mod tests {
2843 use super::*;
2844 use ogeom_geom::{Curve2d, Curve3d, CylinderSurface, PlaneSurface, SphereSurface};
2845 use ogeom_math::{Cylinder, Direction, Frame, Plane, Sphere, Vector};
2846
2847 const T: Tolerances = Tolerances::millimetres();
2848
2849 fn sphere(centre: Point, radius: f64) -> SurfaceGeometry {
2850 SphereSurface::new(Sphere::centred(centre, radius, T).unwrap()).into()
2851 }
2852
2853 fn cylinder(axis: Vector, radius: f64) -> SurfaceGeometry {
2854 let frame = Frame::new(
2855 Point::ORIGIN,
2856 Direction::new(axis, T).unwrap(),
2857 Direction::from_cross(axis, Vector::new(0.3, 0.5, 0.9), T).unwrap(),
2858 T,
2859 )
2860 .unwrap();
2861 CylinderSurface::new(Cylinder::new(frame, radius, T).unwrap(), (-4.0, 4.0))
2862 .unwrap()
2863 .into()
2864 }
2865
2866 fn plane(origin: Point, normal: Vector) -> SurfaceGeometry {
2867 PlaneSurface::over(
2868 Plane::through(origin, Direction::new(normal, T).unwrap()),
2869 (-6.0, 6.0),
2870 (-6.0, 6.0),
2871 )
2872 .unwrap()
2873 .into()
2874 }
2875
2876 fn assert_same_parameter(
2879 section: &SectionCurve,
2880 surface: &SurfaceGeometry,
2881 pcurve: &PlanarCurve,
2882 samples: usize,
2883 ) {
2884 let (lo, hi) = section.curve.domain();
2885 let (plo, phi) = pcurve.domain();
2886 assert!(
2887 (lo - plo).abs() < 1e-9 && (hi - phi).abs() < 1e-9,
2888 "domains disagree: [{lo}, {hi}] against [{plo}, {phi}]"
2889 );
2890 for i in 0..=samples {
2891 #[allow(clippy::cast_precision_loss)]
2892 let t = lo + (hi - lo) * i as f64 / samples as f64;
2893 let on_curve = section.curve.point_at(t, T).unwrap();
2894 let at = pcurve.point_at(t, T).unwrap();
2895 let lifted = surface.point_at(at.x, at.y, T).unwrap();
2896 assert!(
2897 on_curve.is_equal(lifted, T),
2898 "at t = {t}: curve {on_curve:?}, lifted {lifted:?}"
2899 );
2900 }
2901 }
2902
2903 fn straight_spline_sides() -> [SurfaceGeometry; 3] {
2908 let (from, to) = (
2909 Point::new(2.0_f64.sqrt(), 0.0, 0.0),
2910 Point::new(0.0, 2.0_f64.sqrt(), 0.0),
2911 );
2912 let along = |k: u32| from + (to - from) * (f64::from(k) / 3.0);
2913 let knots = || ogeom_math::KnotVector::clamped_uniform(3, 4).unwrap();
2914 let side = ogeom_geom::BSplineCurve::new(knots(), (0..4).map(along).collect(), T).unwrap();
2915 let lowered =
2916 ogeom_geom::ExtrusionSurface::over(side.clone().into(), Direction::Z, (1.0, 4.0))
2917 .unwrap();
2918 let extruded = ogeom_geom::ExtrusionSurface::new(side.into(), Direction::Z, 3.0).unwrap();
2919 let rows: Vec<Point> = (0..4)
2920 .flat_map(|k| [along(k), along(k) + Vector::new(0.0, 0.0, 3.0)])
2921 .collect();
2922 let patch = ogeom_geom::BSplineSurface::new(
2923 knots(),
2924 ogeom_math::KnotVector::clamped_uniform(1, 2).unwrap(),
2925 &ogeom_math::ControlGrid::new(rows, 4, 2).unwrap(),
2926 T,
2927 )
2928 .unwrap();
2929 [extruded.into(), lowered.into(), patch.into()]
2930 }
2931
2932 #[test]
2938 fn a_plane_through_a_spline_patch_s_border_ruling_meets_it_there() {
2939 let r = 2.0_f64.sqrt();
2940 for side in straight_spline_sides() {
2941 for (through, normal, u) in [
2942 (Point::ORIGIN, Vector::Y, 0.0),
2943 (Point::ORIGIN, Vector::X, 1.0),
2944 (Point::new(0.5, 0.0, 0.0), Vector::X, 1.0 - 0.5 / r),
2945 ] {
2946 let cut = plane(through, normal);
2947 for (a, b, cut_first) in [(&cut, &side, true), (&side, &cut, false)] {
2948 let met = intersect_surfaces(a, b, IntersectOptions::default(), T).unwrap();
2949 let SurfaceIntersection::Along(sections) = met else {
2950 panic!("the plane through {through:?} holds a ruling: {met:?}");
2951 };
2952 assert_eq!(sections.len(), 1, "{through:?}, {normal:?}");
2953 let section = §ions[0];
2954 assert!(section.exact && !section.tangential, "{section:?}");
2955 let (_, (v0, v1)) = side.domain();
2956 let want = [
2957 side.point_at(u, v0, T).unwrap(),
2958 side.point_at(u, v1, T).unwrap(),
2959 ];
2960 let (lo, hi) = section.curve.domain();
2961 let ends = [
2962 section.curve.point_at(lo, T).unwrap(),
2963 section.curve.point_at(hi, T).unwrap(),
2964 ];
2965 assert!(
2966 ends[0].is_equal(want[0], T) && ends[1].is_equal(want[1], T),
2967 "{through:?}: the ruling at u = {u}, {want:?}, got {ends:?}"
2968 );
2969 let (on_side, on_cut) = if cut_first {
2970 (§ion.on_b, §ion.on_a)
2971 } else {
2972 (§ion.on_a, §ion.on_b)
2973 };
2974 assert_same_parameter(section, &side, on_side.as_ref().unwrap(), 16);
2975 assert_same_parameter(section, &cut, on_cut.as_ref().unwrap(), 16);
2976 }
2977 }
2978 }
2979
2980 let bend = [(0.0, 0.0), (1.0, 0.0), (2.0, 0.5), (2.0, 2.0)]
2983 .map(|(x, y)| Point::new(x, y, 0.0))
2984 .to_vec();
2985 let knots = ogeom_math::KnotVector::clamped_uniform(3, 4).unwrap();
2986 let side = ogeom_geom::BSplineCurve::new(knots, bend, T).unwrap();
2987 let bent: SurfaceGeometry =
2988 ogeom_geom::ExtrusionSurface::new(side.into(), Direction::Z, 3.0)
2989 .unwrap()
2990 .into();
2991 let cut = plane(Point::ORIGIN, Vector::Y);
2992 if let SurfaceIntersection::Along(sections) =
2993 intersect_surfaces(&cut, &bent, IntersectOptions::default(), T).unwrap()
2994 {
2995 assert!(
2996 sections.iter().all(|s| !s.exact || s.tangential),
2997 "a tangent border is no crossing: {sections:?}"
2998 );
2999 }
3000 }
3001
3002 fn sides_from_the_corner() -> Vec<SurfaceGeometry> {
3010 use ogeom_geom::{BSplineCurve, EllipseCurve, ExtrusionSurface, OffsetCurve, TrimmedCurve};
3011 let r = 2.0_f64.sqrt();
3012 let (from, to) = (Point::new(r, 0.0, 0.0), Point::new(0.0, r, 0.0));
3013 let line = |from: Point, to: Point| -> Curve {
3014 let along = |k: u32| from + (to - from) * (f64::from(k) / 3.0);
3015 let knots = ogeom_math::KnotVector::clamped_uniform(3, 4).unwrap();
3016 BSplineCurve::new(knots, (0..4).map(along).collect(), T)
3017 .unwrap()
3018 .into()
3019 };
3020 let middle = from.midpoint(to);
3021 let frame = Frame::new(
3022 middle,
3023 Direction::Z,
3024 Direction::new(from - to, T).unwrap(),
3025 T,
3026 )
3027 .unwrap();
3028 let half_ellipse = TrimmedCurve::new(
3029 EllipseCurve::new(ogeom_math::Ellipse::new(frame, 1.0, 0.4, T).unwrap()).into(),
3030 0.0,
3031 core::f64::consts::PI,
3032 T,
3033 )
3034 .unwrap();
3035 let run = to - from;
3036 let trimmed_line =
3037 TrimmedCurve::new(line(from - run * 0.5, to + run * 0.5), 0.25, 0.75, T).unwrap();
3038 let inset = Vector::new(1.0, 1.0, 0.0) * (0.25 / r);
3039 let offset = OffsetCurve::new(line(from - inset, to - inset), 0.25, Direction::Z).unwrap();
3040 let extruded = |curve: Curve| -> SurfaceGeometry {
3041 ExtrusionSurface::new(curve, Direction::Z, 3.0)
3042 .unwrap()
3043 .into()
3044 };
3045 let [_, _, patch] = straight_spline_sides();
3046 vec![
3047 extruded(half_ellipse.into()),
3048 extruded(trimmed_line.into()),
3049 extruded(Curve::Offset(Box::new(offset))),
3050 patch,
3051 extruded(line(from, to)),
3052 ]
3053 }
3054
3055 #[test]
3062 fn a_border_lying_in_the_other_surface_is_a_section_along_it() {
3063 let r = 2.0_f64.sqrt();
3064 let drum: SurfaceGeometry = CylinderSurface::new(
3065 Cylinder::new(
3066 Frame::new(Point::new(r, -1.0, -1.0), Direction::Z, Direction::X, T).unwrap(),
3067 1.0,
3068 T,
3069 )
3070 .unwrap(),
3071 (0.0, 5.0),
3072 )
3073 .unwrap()
3074 .into();
3075 let cutters = [
3076 ("the plane y = 0", plane(Point::ORIGIN, Vector::Y)),
3077 ("the drum", drum),
3078 ];
3079 for (k, side) in sides_from_the_corner().iter().enumerate() {
3080 for (name, cutter) in &cutters {
3081 for (a, b) in [(cutter, side), (side, cutter)] {
3082 let met = intersect_surfaces(a, b, IntersectOptions::default(), T).unwrap();
3083 let SurfaceIntersection::Along(sections) = met else {
3084 panic!("side {k}, {name}: the ruling is shared, got {met:?}");
3085 };
3086 assert_eq!(sections.len(), 1, "side {k}, {name}: {sections:?}");
3087 let section = §ions[0];
3088 assert!(!section.tangential, "side {k}, {name}: {section:?}");
3089 let (lo, hi) = section.curve.domain();
3092 let near = section.tolerance + T.confusion();
3093 for i in 0..=16 {
3094 let p = section
3095 .curve
3096 .point_at(lo + (hi - lo) * f64::from(i) / 16.0, T)
3097 .unwrap();
3098 let off = (p.x - r).hypot(p.y);
3099 assert!(
3100 off <= near,
3101 "side {k}, {name}: {p:?} is {off} off the ruling"
3102 );
3103 }
3104 let ends = [
3105 section.curve.point_at(lo, T).unwrap(),
3106 section.curve.point_at(hi, T).unwrap(),
3107 ];
3108 let mut heights = [ends[0].z, ends[1].z];
3109 heights.sort_by(f64::total_cmp);
3110 assert!(
3111 heights[0].abs() <= near && (heights[1] - 3.0).abs() <= near,
3112 "side {k}, {name}: the section runs {heights:?}"
3113 );
3114 }
3115 }
3116 }
3117
3118 let [_, _, patch] = straight_spline_sides();
3123 let resting: SurfaceGeometry = CylinderSurface::new(
3124 Cylinder::new(
3125 Frame::new(
3126 Point::new(r - 1.0 / r, -1.0 / r, -1.0),
3127 Direction::Z,
3128 Direction::X,
3129 T,
3130 )
3131 .unwrap(),
3132 1.0,
3133 T,
3134 )
3135 .unwrap(),
3136 (0.0, 5.0),
3137 )
3138 .unwrap()
3139 .into();
3140 for (a, b) in [(&resting, &patch), (&patch, &resting)] {
3141 if let SurfaceIntersection::Along(sections) =
3142 intersect_surfaces(a, b, IntersectOptions::default(), T).unwrap()
3143 {
3144 assert!(
3145 sections.iter().all(|s| s.tangential),
3146 "a border left tangentially is no crossing: {sections:?}"
3147 );
3148 }
3149 }
3150 }
3151
3152 fn wall_leaving(s: f64, q: f64) -> SurfaceGeometry {
3158 let points: Vec<Point> = [0.0, 1.0]
3159 .into_iter()
3160 .flat_map(|x| {
3161 [(0.0, 0.0), (6.0, 6.0 * s), (12.0, 12.0 * s + 144.0 * q)]
3162 .map(|(y, z)| Point::new(x, y, z))
3163 })
3164 .collect();
3165 ogeom_geom::BSplineSurface::new(
3166 ogeom_math::KnotVector::clamped_uniform(1, 2).unwrap(),
3167 ogeom_math::KnotVector::clamped_uniform(2, 3).unwrap(),
3168 &ogeom_math::ControlGrid::new(points, 2, 3).unwrap(),
3169 T,
3170 )
3171 .unwrap()
3172 .into()
3173 }
3174
3175 #[test]
3184 fn a_wall_leaving_a_plane_window_shallowly_meets_it_along_its_border() {
3185 let window: SurfaceGeometry =
3186 PlaneSurface::over(Plane::new(Frame::WORLD), (-1.0, 2.0), (-1.0, 1.0))
3187 .unwrap()
3188 .into();
3189 for s in [0.005, 0.012, 0.023] {
3190 let wall = wall_leaving(s, 0.002);
3191 for (a, b) in [(&window, &wall), (&wall, &window)] {
3192 let met = intersect_surfaces(a, b, IntersectOptions::default(), T).unwrap();
3193 let SurfaceIntersection::Along(sections) = met else {
3194 panic!("sine {s}: the border is shared, got {met:?}");
3195 };
3196 assert_eq!(sections.len(), 1, "sine {s}: {sections:?}");
3197 let section = §ions[0];
3198 assert!(!section.tangential, "sine {s}: {section:?}");
3199 let (lo, hi) = section.curve.domain();
3200 let near = section.tolerance + T.confusion();
3201 for i in 0..=16 {
3202 let p = section
3203 .curve
3204 .point_at(lo + (hi - lo) * f64::from(i) / 16.0, T)
3205 .unwrap();
3206 assert!(
3207 p.y.hypot(p.z) <= near && (-near..=1.0 + near).contains(&p.x),
3208 "sine {s}: {p:?} is off the border"
3209 );
3210 }
3211 let mut xs = [lo, hi].map(|t| section.curve.point_at(t, T).unwrap().x);
3212 xs.sort_by(f64::total_cmp);
3213 assert!(
3214 xs[0].abs() <= near && (xs[1] - 1.0).abs() <= near,
3215 "sine {s}: the section runs {xs:?}"
3216 );
3217 }
3218 }
3219 let resting = wall_leaving(0.0, 0.002);
3220 for (a, b) in [(&window, &resting), (&resting, &window)] {
3221 if let SurfaceIntersection::Along(sections) =
3222 intersect_surfaces(a, b, IntersectOptions::default(), T).unwrap()
3223 {
3224 assert!(
3225 sections.iter().all(|s| s.tangential),
3226 "a border left tangentially is no crossing: {sections:?}"
3227 );
3228 }
3229 }
3230 }
3231
3232 #[test]
3233 fn an_analytic_pair_comes_back_exact_with_matching_pcurves() {
3234 let drum = cylinder(Vector::Z, 2.0);
3238 let cut = plane(Point::ORIGIN, Vector::X);
3239 let SurfaceIntersection::Along(curves) =
3240 intersect_surfaces(&drum, &cut, IntersectOptions::default(), T).unwrap()
3241 else {
3242 panic!("a plane through a cylinder meets it along curves");
3243 };
3244 assert_eq!(curves.len(), 2);
3245 for section in &curves {
3246 assert!(section.exact);
3247 assert!((section.tolerance - 0.0).abs() < f64::EPSILON);
3248 let on_a = section.on_a.as_ref().expect("a line has a cylinder pcurve");
3249 let on_b = section.on_b.as_ref().expect("and a plane pcurve");
3250 assert_same_parameter(section, &drum, on_a, 50);
3251 assert_same_parameter(section, &cut, on_b, 50);
3252 }
3253 }
3254
3255 #[test]
3256 fn an_oblique_cut_gives_the_ellipse_a_trig_pcurve_on_the_drum() {
3257 let drum = cylinder(Vector::Z, 2.0);
3261 let angle: f64 = 0.5;
3262 let cut = plane(Point::ORIGIN, Vector::new(0.0, angle.sin(), angle.cos()));
3263 let SurfaceIntersection::Along(curves) =
3264 intersect_surfaces(&drum, &cut, IntersectOptions::default(), T).unwrap()
3265 else {
3266 panic!("an oblique plane meets the cylinder along its ellipse");
3267 };
3268 assert_eq!(curves.len(), 1);
3269 let section = &curves[0];
3270 assert!(section.exact);
3271 assert!(matches!(section.curve, Curve::Ellipse(_)));
3272 let on_drum = section
3273 .on_a
3274 .as_ref()
3275 .expect("the oblique ellipse now carries its cylinder pcurve");
3276 assert!(
3277 matches!(on_drum, PlanarCurve::Trig(_)),
3278 "the chart trace is trig-affine: {on_drum:?}"
3279 );
3280 assert_same_parameter(section, &drum, on_drum, 60);
3281 let on_plane = section.on_b.as_ref().expect("and its plane pcurve");
3282 assert_same_parameter(section, &cut, on_plane, 60);
3283 }
3284
3285 #[test]
3286 fn a_perpendicular_cut_gives_a_circle_with_a_straight_pcurve() {
3287 let drum = cylinder(Vector::Z, 2.0);
3288 let cut = plane(Point::new(0.0, 0.0, 1.0), Vector::Z);
3289 let SurfaceIntersection::Along(curves) =
3290 intersect_surfaces(&drum, &cut, IntersectOptions::default(), T).unwrap()
3291 else {
3292 panic!("expected curves");
3293 };
3294 assert_eq!(curves.len(), 1);
3295 let section = &curves[0];
3296 assert!(section.closed);
3297 assert!(matches!(section.curve, Curve::Circle(_)));
3298 assert!(matches!(
3300 section.on_a.as_ref().unwrap(),
3301 PlanarCurve::Line(_)
3302 ));
3303 assert_same_parameter(section, &drum, section.on_a.as_ref().unwrap(), 60);
3304 assert_same_parameter(section, &cut, section.on_b.as_ref().unwrap(), 60);
3305 }
3306
3307 #[test]
3308 fn coaxial_cylinder_and_sphere_give_circles_with_pcurves_on_both() {
3309 let drum = cylinder(Vector::Z, 1.5);
3310 let ball = sphere(Point::ORIGIN, 3.0);
3311 let SurfaceIntersection::Along(curves) =
3312 intersect_surfaces(&drum, &ball, IntersectOptions::default(), T).unwrap()
3313 else {
3314 panic!("expected curves");
3315 };
3316 assert_eq!(curves.len(), 2);
3317 for section in &curves {
3318 assert!(section.exact);
3319 assert_same_parameter(section, &drum, section.on_a.as_ref().unwrap(), 40);
3320 assert_same_parameter(section, &ball, section.on_b.as_ref().unwrap(), 40);
3321 }
3322 }
3323
3324 fn torus(origin: Point, axis: Vector, major: f64, minor: f64) -> SurfaceGeometry {
3325 let frame = Frame::new(
3326 origin,
3327 Direction::new(axis, T).unwrap(),
3328 Direction::from_cross(axis, Vector::new(0.3, 0.5, 0.9), T).unwrap(),
3329 T,
3330 )
3331 .unwrap();
3332 ogeom_geom::TorusSurface::new(ogeom_math::Torus::new(frame, major, minor, T).unwrap())
3333 .into()
3334 }
3335
3336 #[test]
3337 fn an_axis_normal_plane_meets_a_torus_in_two_parallels_with_pcurves() {
3338 let ring = torus(Point::ORIGIN, Vector::Z, 2.0, 0.5);
3339 let cut = plane(Point::new(0.0, 0.0, 0.3), Vector::Z);
3340 let SurfaceIntersection::Along(curves) =
3341 intersect_surfaces(&ring, &cut, IntersectOptions::default(), T).unwrap()
3342 else {
3343 panic!("an axis-normal plane through the tube meets it along curves");
3344 };
3345 assert_eq!(curves.len(), 2);
3346 let spread = 0.5_f64.mul_add(0.5, -(0.3 * 0.3)).sqrt();
3347 let mut radii: Vec<f64> = curves
3348 .iter()
3349 .map(|s| {
3350 let Curve::Circle(c) = &s.curve else {
3351 panic!("a parallel is a circle");
3352 };
3353 c.circle().radius()
3354 })
3355 .collect();
3356 radii.sort_by(|a, b| a.partial_cmp(b).unwrap());
3357 assert!((radii[0] - (2.0 - spread)).abs() < 1e-12);
3358 assert!((radii[1] - (2.0 + spread)).abs() < 1e-12);
3359 for section in &curves {
3360 assert!(section.exact);
3361 assert_same_parameter(section, &ring, section.on_a.as_ref().unwrap(), 48);
3362 assert_same_parameter(section, &cut, section.on_b.as_ref().unwrap(), 48);
3363 }
3364 }
3365
3366 #[test]
3367 fn the_plane_a_ball_rolls_on_touches_its_torus_along_the_circle_it_rolled() {
3368 let ring = torus(Point::ORIGIN, Vector::Z, 2.0, 0.5);
3373 let cut = plane(Point::new(0.0, 0.0, 0.5), Vector::Z);
3374 let SurfaceIntersection::Along(curves) =
3375 intersect_surfaces(&ring, &cut, IntersectOptions::default(), T).unwrap()
3376 else {
3377 panic!("the rolling plane touches along a circle, not at points");
3378 };
3379 assert_eq!(curves.len(), 1);
3380 let Curve::Circle(c) = &curves[0].curve else {
3381 panic!("the tangency is a circle");
3382 };
3383 assert!((c.circle().radius() - 2.0).abs() < 1e-12);
3384 assert_same_parameter(&curves[0], &ring, curves[0].on_a.as_ref().unwrap(), 48);
3385 assert_same_parameter(&curves[0], &cut, curves[0].on_b.as_ref().unwrap(), 48);
3386 }
3387
3388 #[test]
3389 fn a_coaxial_cylinder_meets_a_torus_in_two_parallels_and_touches_in_one() {
3390 let ring = torus(Point::ORIGIN, Vector::Z, 2.0, 0.5);
3391 let drum = cylinder(Vector::Z, 2.2);
3392 let SurfaceIntersection::Along(curves) =
3393 intersect_surfaces(&drum, &ring, IntersectOptions::default(), T).unwrap()
3394 else {
3395 panic!("a coaxial cylinder through the tube meets it along curves");
3396 };
3397 assert_eq!(curves.len(), 2);
3398 for section in &curves {
3399 assert!(section.exact);
3400 let Curve::Circle(c) = §ion.curve else {
3401 panic!("a parallel is a circle");
3402 };
3403 assert!((c.circle().radius() - 2.2).abs() < 1e-12);
3404 assert_same_parameter(section, &drum, section.on_a.as_ref().unwrap(), 48);
3405 assert_same_parameter(section, &ring, section.on_b.as_ref().unwrap(), 48);
3406 }
3407
3408 let grazing = cylinder(Vector::Z, 2.5);
3410 let SurfaceIntersection::Along(touch) =
3411 intersect_surfaces(&grazing, &ring, IntersectOptions::default(), T).unwrap()
3412 else {
3413 panic!("the grazing cylinder touches along the equator");
3414 };
3415 assert_eq!(touch.len(), 1);
3416 assert_same_parameter(&touch[0], &grazing, touch[0].on_a.as_ref().unwrap(), 48);
3417 assert_same_parameter(&touch[0], &ring, touch[0].on_b.as_ref().unwrap(), 48);
3418 }
3419
3420 #[test]
3421 fn coaxial_tori_are_the_same_or_meet_in_parallels() {
3422 let ring = torus(Point::ORIGIN, Vector::Z, 2.0, 0.5);
3423 assert!(matches!(
3424 intersect_surfaces(&ring, &ring.clone(), IntersectOptions::default(), T).unwrap(),
3425 SurfaceIntersection::Same
3426 ));
3427
3428 let lifted = torus(Point::new(0.0, 0.0, 0.5), Vector::Z, 2.0, 0.5);
3431 let SurfaceIntersection::Along(curves) =
3432 intersect_surfaces(&ring, &lifted, IntersectOptions::default(), T).unwrap()
3433 else {
3434 panic!("lifted coaxial tori meet along curves");
3435 };
3436 assert_eq!(curves.len(), 2);
3437 for section in &curves {
3438 assert!(section.exact);
3439 assert_same_parameter(section, &ring, section.on_a.as_ref().unwrap(), 48);
3440 assert_same_parameter(section, &lifted, section.on_b.as_ref().unwrap(), 48);
3441 }
3442 }
3443
3444 #[test]
3445 fn a_pair_with_no_closed_form_comes_back_fitted_with_pcurves() {
3446 let a = cylinder(Vector::Z, 1.0);
3448 let b = cylinder(Vector::X, 1.6);
3449 let options = IntersectOptions {
3450 tolerance: 1e-5,
3451 marching: Marching {
3452 chord: 1e-5,
3453 ..Marching::default()
3454 },
3455 };
3456 let SurfaceIntersection::Along(curves) = intersect_surfaces(&a, &b, options, T).unwrap()
3457 else {
3458 panic!("crossed cylinders meet along curves");
3459 };
3460 assert_eq!(curves.len(), 2);
3461 for section in &curves {
3462 assert!(!section.exact);
3463 assert!(section.closed);
3464 assert!(
3465 section.tolerance <= 1e-5 + 1e-4,
3466 "got {}",
3467 section.tolerance
3468 );
3469 assert!(section.on_a.is_some() && section.on_b.is_some());
3470
3471 let (lo, hi) = section.curve.domain();
3473 for i in 0..=200 {
3474 #[allow(clippy::cast_precision_loss)]
3475 let t = lo + (hi - lo) * f64::from(i) / 200.0;
3476 let p = section.curve.point_at(t, T).unwrap();
3477 let (SurfaceGeometry::Cylinder(x), SurfaceGeometry::Cylinder(y)) = (&a, &b) else {
3478 unreachable!()
3479 };
3480 let off = x
3481 .cylinder()
3482 .distance_to(p)
3483 .abs()
3484 .max(y.cylinder().distance_to(p).abs());
3485 assert!(
3486 off <= section.tolerance * 2.0,
3487 "at t = {t} the fitted curve is {off:e} off, tolerance {}",
3488 section.tolerance
3489 );
3490 }
3491 }
3492 }
3493
3494 #[test]
3498 fn a_plane_all_but_along_the_axis_still_meets_a_short_drum() {
3499 let drum = cylinder(Vector::Z, 1.0);
3500 let wall: SurfaceGeometry = PlaneSurface::over(
3501 Plane::through(
3502 Point::new(0.0, 0.6, 0.0),
3503 Direction::new(Vector::new(0.0, 1.0, 1e-4), T).unwrap(),
3504 ),
3505 (-1e9, 1e9),
3506 (-1e9, 1e9),
3507 )
3508 .unwrap()
3509 .into();
3510 let met = intersect_surfaces(&wall, &drum, IntersectOptions::default(), T).unwrap();
3511 let SurfaceIntersection::Along(sections) = met else {
3512 panic!("the wall crosses the drum: {met:?}");
3513 };
3514 assert_eq!(sections.len(), 1);
3515 let curve = §ions[0].curve;
3516 let (lo, hi) = curve.domain();
3517 let inside = (0..=100_000).any(|k| {
3518 let p = curve
3519 .point_at(lo + (hi - lo) * f64::from(k) / 100_000.0, T)
3520 .unwrap();
3521 p.z.abs() <= 4.0
3522 });
3523 assert!(inside, "and the section runs through the drum's height");
3524 }
3525
3526 fn on_both(section: &SectionCurve, a: &SurfaceGeometry, b: &SurfaceGeometry) {
3529 let (lo, hi) = section.curve.domain();
3530 for k in 0..=64 {
3531 let p = section
3532 .curve
3533 .point_at(lo + (hi - lo) * f64::from(k) / 64.0, T)
3534 .unwrap();
3535 for surface in [a, b] {
3536 let off = match surface {
3537 SurfaceGeometry::Plane(plane) => plane.plane().signed_distance_to(p).abs(),
3538 SurfaceGeometry::Cylinder(drum) => {
3539 let axis = drum.cylinder().axis();
3540 let rel = p - axis.location;
3541 let d = axis.direction.vector();
3542 ((rel - d * rel.dot(d)).magnitude() - drum.cylinder().radius()).abs()
3543 }
3544 _ => unreachable!("planes and drums only"),
3545 };
3546 assert!(
3547 off <= section.tolerance + 1e-9,
3548 "{p:?} is {off:e} off, stated {:e}",
3549 section.tolerance
3550 );
3551 }
3552 }
3553 }
3554
3555 #[test]
3561 fn a_plane_all_but_along_a_drums_axis_meets_it_in_two_near_lines() {
3562 let drum = cylinder(Vector::Z, 1.0);
3563 let wall: SurfaceGeometry = PlaneSurface::over(
3564 Plane::through(
3565 Point::new(0.0, 0.99, 0.0),
3566 Direction::new(Vector::new(0.0, 1.0, 2e-5), T).unwrap(),
3567 ),
3568 (-1e9, 1e9),
3569 (-1e9, 1e9),
3570 )
3571 .unwrap()
3572 .into();
3573 let met = intersect_surfaces(&wall, &drum, IntersectOptions::default(), T).unwrap();
3574 let SurfaceIntersection::Along(sections) = met else {
3575 panic!("the wall crosses the drum: {met:?}");
3576 };
3577 assert_eq!(sections.len(), 2);
3578 for section in §ions {
3579 assert!(section.tolerance > 0.0 && section.tolerance <= 1e-5);
3580 on_both(section, &wall, &drum);
3581 }
3582 }
3583
3584 #[test]
3588 fn drums_all_but_parallel_meet_in_two_near_lines() {
3589 let drill = cylinder(Vector::Z, 1.0);
3590 let frame = Frame::new(
3591 Point::new(1.5, 0.0, 0.0),
3592 Direction::new(Vector::new(5e-5, 0.0, 1.0), T).unwrap(),
3593 Direction::X,
3594 T,
3595 )
3596 .unwrap();
3597 let bore: SurfaceGeometry =
3598 CylinderSurface::new(Cylinder::new(frame, 1.0, T).unwrap(), (-3.0, 3.0))
3599 .unwrap()
3600 .into();
3601 let met = intersect_surfaces(&drill, &bore, IntersectOptions::default(), T).unwrap();
3602 let SurfaceIntersection::Along(sections) = met else {
3603 panic!("the drums cross: {met:?}");
3604 };
3605 assert_eq!(sections.len(), 2);
3606 for section in §ions {
3607 assert!(!section.exact && section.tolerance <= 1e-5);
3608 let (lo, hi) = section.curve.domain();
3609 let (p, q) = (
3610 section.curve.point_at(lo, T).unwrap(),
3611 section.curve.point_at(hi, T).unwrap(),
3612 );
3613 assert!(
3614 (p.z - q.z).abs() > 5.9,
3615 "over the shared height: {p:?} {q:?}"
3616 );
3617 on_both(section, &drill, &bore);
3618 }
3619 }
3620
3621 #[test]
3622 fn exact_lines_are_clipped_to_the_surfaces_extents() {
3623 let drum = cylinder(Vector::Z, 2.0);
3628 let cut = plane(Point::ORIGIN, Vector::X);
3629 let SurfaceIntersection::Along(curves) =
3630 intersect_surfaces(&drum, &cut, IntersectOptions::default(), T).unwrap()
3631 else {
3632 panic!("expected curves");
3633 };
3634 for section in &curves {
3635 let (lo, hi) = section.curve.domain();
3636 assert!(
3638 hi - lo <= 8.0 + 1e-9,
3639 "the line was not clipped: [{lo}, {hi}]"
3640 );
3641 let start = section.curve.point_at(lo, T).unwrap();
3642 let end = section.curve.point_at(hi, T).unwrap();
3643 assert!(start.z >= -4.0 - 1e-9 && end.z <= 4.0 + 1e-9);
3644 }
3645
3646 let high = plane(Point::new(0.0, 0.0, 10.0), Vector::Z);
3650 assert_eq!(
3651 intersect_surfaces(&drum, &high, IntersectOptions::default(), T).unwrap(),
3652 SurfaceIntersection::Apart
3653 );
3654 }
3655
3656 #[test]
3657 fn the_degenerate_answers_pass_through() {
3658 assert_eq!(
3659 intersect_surfaces(
3660 &sphere(Point::ORIGIN, 1.0),
3661 &sphere(Point::new(5.0, 0.0, 0.0), 1.0),
3662 IntersectOptions::default(),
3663 T
3664 )
3665 .unwrap(),
3666 SurfaceIntersection::Apart
3667 );
3668 assert_eq!(
3669 intersect_surfaces(
3670 &sphere(Point::ORIGIN, 1.0),
3671 &sphere(Point::ORIGIN, 1.0),
3672 IntersectOptions::default(),
3673 T
3674 )
3675 .unwrap(),
3676 SurfaceIntersection::Same
3677 );
3678 assert!(matches!(
3679 intersect_surfaces(
3680 &plane(Point::ORIGIN, Vector::Z),
3681 &sphere(Point::new(0.0, 0.0, 2.0), 2.0),
3682 IntersectOptions::default(),
3683 T
3684 )
3685 .unwrap(),
3686 SurfaceIntersection::Touching(ref p) if p.len() == 1
3687 ));
3688 }
3689
3690 #[test]
3691 fn unusable_options_are_refused() {
3692 let a = sphere(Point::ORIGIN, 1.0);
3693 let b = plane(Point::ORIGIN, Vector::Z);
3694 for tolerance in [0.0, -1.0, f64::NAN] {
3695 let options = IntersectOptions {
3696 tolerance,
3697 ..IntersectOptions::default()
3698 };
3699 assert!(intersect_surfaces(&a, &b, options, T).is_err());
3700 }
3701 }
3702
3703 #[test]
3704 fn a_circle_wound_against_the_axis_keeps_its_pcurve_same_parameter() {
3705 let drum: SurfaceGeometry = CylinderSurface::new(
3712 Cylinder::new(
3713 Frame::new(Point::new(2.0, 2.0, -1.0), Direction::Z, Direction::X, T).unwrap(),
3714 0.5,
3715 T,
3716 )
3717 .unwrap(),
3718 (0.0, 3.0),
3719 )
3720 .unwrap()
3721 .into();
3722 for normal in [Direction::Z, -Direction::Z] {
3723 let frame = Frame::new(Point::ORIGIN, normal, Direction::X, T).unwrap();
3724 let ground: SurfaceGeometry =
3725 PlaneSurface::over(Plane::new(frame), (-4.0, 4.0), (-4.0, 4.0))
3726 .unwrap()
3727 .into();
3728 let met = intersect_surfaces(&ground, &drum, IntersectOptions::default(), T).unwrap();
3729 let SurfaceIntersection::Along(curves) = met else {
3730 panic!("a plane through a cylinder sections it");
3731 };
3732 for sc in &curves {
3733 let pcurve = sc
3734 .on_b
3735 .as_ref()
3736 .expect("a circle on its cylinder has a pcurve");
3737 let (lo, hi) = sc.curve.domain();
3738 for i in 0..8 {
3739 let t = lo + (hi - lo) * f64::from(i) / 8.0;
3740 let p3 = sc.curve.point_at(t, T).unwrap();
3741 let uv = pcurve.point_at(t, T).unwrap();
3742 let lifted = drum
3743 .point_at(uv.x.rem_euclid(core::f64::consts::TAU), uv.y, T)
3744 .unwrap();
3745 assert!(
3746 p3.distance(lifted) < 1e-9,
3747 "normal {normal:?}, t {t}: pcurve lifts {lifted:?} against {p3:?}"
3748 );
3749 }
3750 }
3751 }
3752 }
3753
3754 #[test]
3755 fn a_circle_on_a_left_handed_frame_keeps_its_pcurve_same_parameter() {
3756 use ogeom_geom::ConeSurface;
3764 let cone: SurfaceGeometry = ConeSurface::new(
3765 ogeom_math::Cone::new(Frame::WORLD, 1.0, core::f64::consts::FRAC_PI_6, T).unwrap(),
3766 (-1.0, 3.0),
3767 )
3768 .unwrap()
3769 .into();
3770 let on = |centre: Point, normal: Direction, radius: f64| {
3771 (Frame::new(centre, normal, Direction::X, T).unwrap(), radius)
3772 };
3773 let cases = [
3774 (
3775 cylinder(Vector::Z, 2.0),
3776 on(Point::new(0.0, 0.0, 1.0), Direction::Z, 2.0),
3777 ),
3778 (
3779 sphere(Point::ORIGIN, 2.0),
3780 on(Point::new(0.0, 0.0, 1.0), Direction::Z, 3.0_f64.sqrt()),
3781 ),
3782 (
3783 cone.clone(),
3784 on(
3785 Point::new(0.0, 0.0, 1.0),
3786 Direction::Z,
3787 1.0 + core::f64::consts::FRAC_PI_6.tan(),
3788 ),
3789 ),
3790 (
3791 torus(Point::ORIGIN, Vector::Z, 2.0, 0.5),
3792 on(Point::ORIGIN, Direction::Z, 2.5),
3793 ),
3794 (
3795 torus(Point::ORIGIN, Vector::Z, 2.0, 0.5),
3796 on(Point::new(2.0, 0.0, 0.0), Direction::Y, 0.5),
3797 ),
3798 ];
3799 for (surface, (frame, radius)) in &cases {
3800 for frame in [*frame, frame.mirrored()] {
3801 let circle: Curve = ogeom_geom::CircleCurve::new(
3802 ogeom_math::Circle::new(frame, *radius, T).unwrap(),
3803 )
3804 .into();
3805 let pcurve = exact_pcurve_of(&circle, surface, T)
3806 .expect("a circle of the surface has a closed-form pcurve");
3807 for i in 0..=8 {
3808 let t = core::f64::consts::TAU * f64::from(i) / 8.0;
3809 let uv = pcurve.point_at(t, T).unwrap();
3810 let lifted = surface
3811 .point_at(uv.x.rem_euclid(core::f64::consts::TAU), uv.y, T)
3812 .or_else(|_| surface.point_at(uv.x, uv.y, T))
3813 .unwrap();
3814 let p3 = circle.point_at(t, T).unwrap();
3815 assert!(
3818 p3.distance(lifted) < 1e-9,
3819 "{:?} frame, t {t}: pcurve lifts {lifted:?} against {p3:?}",
3820 frame.handedness()
3821 );
3822 }
3823 }
3824 }
3825 }
3826
3827 #[test]
3834 fn a_circle_on_a_left_handed_surface_keeps_its_pcurve_same_parameter() {
3835 use ogeom_geom::{ConeSurface, TorusSurface};
3836 let on = |centre: Point, normal: Direction, radius: f64| {
3837 (Frame::new(centre, normal, Direction::X, T).unwrap(), radius)
3838 };
3839 let cylinder = |frame: Frame| -> SurfaceGeometry {
3840 CylinderSurface::new(Cylinder::new(frame, 2.0, T).unwrap(), (-4.0, 4.0))
3841 .unwrap()
3842 .into()
3843 };
3844 let cone = |frame: Frame| -> SurfaceGeometry {
3845 ConeSurface::new(
3846 ogeom_math::Cone::new(frame, 1.0, core::f64::consts::FRAC_PI_6, T).unwrap(),
3847 (-1.0, 3.0),
3848 )
3849 .unwrap()
3850 .into()
3851 };
3852 let sphere = |frame: Frame| -> SurfaceGeometry {
3853 SphereSurface::new(Sphere::new(frame, 2.0, T).unwrap()).into()
3854 };
3855 let torus = |frame: Frame| -> SurfaceGeometry {
3856 TorusSurface::new(ogeom_math::Torus::new(frame, 2.0, 0.5, T).unwrap()).into()
3857 };
3858 type Make<'a> = &'a dyn Fn(Frame) -> SurfaceGeometry;
3859 let surfaces: [(Make, Vec<(Frame, f64)>); 4] = [
3860 (
3861 &cylinder,
3862 vec![on(Point::new(0.0, 0.0, 1.0), Direction::Z, 2.0)],
3863 ),
3864 (
3865 &cone,
3866 vec![on(
3867 Point::new(0.0, 0.0, 1.0),
3868 Direction::Z,
3869 1.0 + core::f64::consts::FRAC_PI_6.tan(),
3870 )],
3871 ),
3872 (
3873 &sphere,
3874 vec![
3875 on(Point::new(0.0, 0.0, 1.0), Direction::Z, 3.0_f64.sqrt()),
3876 on(Point::ORIGIN, Direction::Y, 2.0),
3877 ],
3878 ),
3879 (
3880 &torus,
3881 vec![
3882 on(Point::ORIGIN, Direction::Z, 2.5),
3883 on(Point::new(0.0, 0.0, 0.5), Direction::Z, 2.0),
3884 on(Point::new(2.0, 0.0, 0.0), Direction::Y, 0.5),
3885 ],
3886 ),
3887 ];
3888 let tau = core::f64::consts::TAU;
3889 for (make, circles) in &surfaces {
3890 for surface_frame in [Frame::WORLD, Frame::WORLD.mirrored()] {
3891 let surface = make(surface_frame);
3892 for (frame, radius) in circles {
3893 for frame in [*frame, frame.mirrored()] {
3894 let circle: Curve = ogeom_geom::CircleCurve::new(
3895 ogeom_math::Circle::new(frame, *radius, T).unwrap(),
3896 )
3897 .into();
3898 let range = if frame.z().vector().dot(Vector::Z).abs() < 0.5 {
3900 let half = core::f64::consts::FRAC_PI_2 - 0.1;
3901 (-half, half)
3902 } else {
3903 (0.0, tau)
3904 };
3905 let pcurve = exact_pcurve_over(&circle, range, &surface, T)
3906 .expect("a circle of the surface has a closed-form pcurve");
3907 for i in 0..=8 {
3908 let t = (range.1 - range.0).mul_add(f64::from(i) / 8.0, range.0);
3909 let uv = pcurve.point_at(t, T).unwrap();
3910 let lifted = surface
3911 .point_at(uv.x.rem_euclid(tau), uv.y, T)
3912 .or_else(|_| {
3913 surface.point_at(uv.x.rem_euclid(tau), uv.y.rem_euclid(tau), T)
3914 })
3915 .unwrap();
3916 let p3 = circle.point_at(t, T).unwrap();
3917 assert!(
3920 p3.distance(lifted) < 1e-9,
3921 "{:?} surface, {:?} circle {frame:?}, t {t}: pcurve lifts \
3922 {lifted:?} against {p3:?}",
3923 surface_frame.handedness(),
3924 frame.handedness()
3925 );
3926 }
3927 }
3928 }
3929 }
3930 }
3931 }
3932
3933 #[test]
3939 fn a_meridian_half_has_an_exact_line_for_a_pcurve() {
3940 use ogeom_geom::Surface as _;
3941 let half = core::f64::consts::PI;
3942 for (centre, radius) in [(Point::ORIGIN, 4.0), (Point::new(1.0, -2.0, 0.5), 1.25)] {
3943 let ball = sphere(centre, radius);
3944 let SurfaceGeometry::Sphere(s) = &ball else {
3945 panic!("a sphere surface");
3946 };
3947 for azimuth in [0.0_f64, 0.7, 2.4] {
3950 let normal = Vector::new(-azimuth.sin(), azimuth.cos(), 0.0);
3951 let cut = plane(centre, normal);
3952 let SurfaceIntersection::Along(curves) =
3953 intersect_surfaces(&ball, &cut, IntersectOptions::default(), T).unwrap()
3954 else {
3955 panic!("a plane through the centre meets the ball along a circle");
3956 };
3957 assert_eq!(curves.len(), 1, "one great circle");
3958 let circle = &curves[0].curve;
3959 assert!(curves[0].exact);
3960 assert!(
3962 exact_pcurve_over(circle, circle.domain(), &ball, T).is_none(),
3963 "the whole meridian has no single chart image"
3964 );
3965 for (lo, hi) in [(0.0, half), (half, 2.0 * half), (0.3, half - 0.1)] {
3966 let pcurve = exact_pcurve_over(circle, (lo, hi), &ball, T)
3967 .expect("half a meridian has an exact pcurve");
3968 assert!(
3969 matches!(pcurve, PlanarCurve::Line(_)),
3970 "and it is a straight line in the chart"
3971 );
3972 for i in 0..=16 {
3973 let t = (hi - lo).mul_add(f64::from(i) / 16.0, lo);
3974 let want = circle.point_at(t, T).unwrap();
3975 let uv = pcurve.point_at(t, T).unwrap();
3976 assert!(
3977 uv.y >= -half.mul_add(0.5, 1e-12) && uv.y <= half.mul_add(0.5, 1e-12),
3978 "the latitude stays inside the chart: {}",
3979 uv.y
3980 );
3981 let lifted = ball
3982 .point_at(uv.x.rem_euclid(core::f64::consts::TAU), uv.y, T)
3983 .unwrap();
3984 assert!(
3985 want.distance(lifted) < 1e-9,
3986 "azimuth {azimuth}, t {t}: {lifted:?} against {want:?}"
3987 );
3988 }
3989 }
3990 assert!(
3993 exact_pcurve_over(circle, (half - 0.2, half + 0.2), &ball, T).is_none(),
3994 "a range across a pole has no one line"
3995 );
3996 let _ = s;
3997 }
3998 }
3999 }
4000
4001 #[test]
4006 fn a_meridian_half_read_slightly_past_its_poles_is_still_a_line() {
4007 let half = core::f64::consts::PI;
4008 let ball = sphere(Point::new(1.0, -2.0, 0.5), 0.2);
4009 let cut = plane(
4010 Point::new(1.0, -2.0, 0.5),
4011 Vector::new(-0.7_f64.sin(), 0.7_f64.cos(), 0.0),
4012 );
4013 let SurfaceIntersection::Along(curves) =
4014 intersect_surfaces(&ball, &cut, IntersectOptions::default(), T).unwrap()
4015 else {
4016 panic!("a plane through the centre meets the ball along a circle");
4017 };
4018 let circle = &curves[0].curve;
4019 let SurfaceGeometry::Sphere(s) = &ball else {
4020 panic!("a sphere surface");
4021 };
4022 for over in [2e-9, 1e-8, 1e-7] {
4023 for (lo, hi) in [(-over, half + over), (half - over, 2.0 * half + over)] {
4024 let pcurve = exact_pcurve_over(circle, (lo, hi), &ball, T)
4025 .unwrap_or_else(|| panic!("{over} past the poles: no line"));
4026 for i in 0..=16 {
4027 let t = (hi - lo).mul_add(f64::from(i) / 16.0, lo);
4028 let uv = pcurve.point_at(t, T).unwrap();
4029 let lifted = ogeom_math::elementary::sphere_at(&s.sphere(), uv.x, uv.y).point;
4030 let want = circle.point_at(t, T).unwrap();
4031 assert!(
4032 want.distance(lifted) < 1e-9,
4033 "{over}, t {t}: {lifted:?} against {want:?}"
4034 );
4035 }
4036 }
4037 }
4038 }
4039
4040 #[test]
4049 fn a_trimmed_curve_carries_its_basis_pcurve_trimmed_the_same_way() {
4050 use ogeom_geom::TrimmedCurve;
4051 let drum = cylinder(Vector::Z, 2.0);
4052 let ground = plane(Point::new(0.0, 0.0, 1.0), Vector::Z);
4053 let SurfaceIntersection::Along(curves) =
4055 intersect_surfaces(&drum, &ground, IntersectOptions::default(), T).unwrap()
4056 else {
4057 panic!("a plane across a cylinder meets it in a circle");
4058 };
4059 let whole = curves[0].curve.clone();
4060 let (lo, hi) = whole.domain();
4061 let quarter: Curve = TrimmedCurve::new(whole.clone(), lo + 0.3, lo + (hi - lo) / 4.0, T)
4062 .unwrap()
4063 .into();
4064
4065 for surface in [&drum, &ground] {
4066 let full = exact_pcurve_of(&whole, surface, T).expect("the whole circle has one");
4067 let part = exact_pcurve_of(&quarter, surface, T).expect("and so does a quarter of it");
4068 let (a, b) = quarter.domain();
4071 for i in 0..=8 {
4072 let t = (b - a).mul_add(f64::from(i) / 8.0, a);
4073 let (whole_at, part_at) =
4074 (full.point_at(t, T).unwrap(), part.point_at(t, T).unwrap());
4075 assert!(
4076 whole_at.distance(part_at) < 1e-12,
4077 "the trim carries the basis: {whole_at:?} against {part_at:?}"
4078 );
4079 let lifted = surface
4081 .point_at(part_at.x.rem_euclid(core::f64::consts::TAU), part_at.y, T)
4082 .or_else(|_| surface.point_at(part_at.x, part_at.y, T))
4083 .unwrap();
4084 assert!(
4085 lifted.distance(quarter.point_at(t, T).unwrap()) < 1e-9,
4086 "same-parameter, still"
4087 );
4088 }
4089 }
4090 }
4091
4092 #[test]
4097 fn an_arc_across_its_circles_origin_has_a_pcurve_over_its_range() {
4098 let drum = cylinder(Vector::Z, 2.0);
4099 let ball = sphere(Point::ORIGIN, 2.0);
4100 let ground = plane(Point::ORIGIN, Vector::Z);
4101 for surface in [&drum, &ball] {
4102 let SurfaceIntersection::Along(curves) =
4103 intersect_surfaces(surface, &ground, IntersectOptions::default(), T).unwrap()
4104 else {
4105 panic!("a plane through the axis's normal meets it in a circle");
4106 };
4107 let circle = curves[0].curve.clone();
4108 let tau = core::f64::consts::TAU;
4109 for range in [(4.7, tau + 1.0), (-1.0, 1.5)] {
4110 let pcurve = exact_pcurve_over(&circle, range, surface, T).expect("a parallel");
4111 for i in 0..=8 {
4112 let t = (range.1 - range.0).mul_add(f64::from(i) / 8.0, range.0);
4113 let at = pcurve.point_at(t, T).unwrap();
4114 let lifted = surface.point_at(at.x.rem_euclid(tau), at.y, T).unwrap();
4115 assert!(
4116 lifted.distance(circle.point_at(t, T).unwrap()) < 1e-9,
4117 "{range:?} at {t}"
4118 );
4119 }
4120 }
4121 }
4122 }
4123
4124 #[test]
4128 fn a_plane_through_a_cones_apex_holds_its_rulings() {
4129 use ogeom_geom::ConeSurface;
4130 let frame = Frame::new(
4131 Point::new(100.0, 200.0, 300.0),
4132 Direction::Z,
4133 Direction::X,
4134 T,
4135 )
4136 .unwrap();
4137 let cone = ogeom_math::Cone::new(frame, 10.0, core::f64::consts::FRAC_PI_4, T).unwrap();
4138 let surface: SurfaceGeometry = ConeSurface::new(cone, (-5.0, 50.0)).unwrap().into();
4139 let apex = Point::new(100.0, 200.0, 290.0);
4140 let plane = |normal: Vector| -> SurfaceGeometry {
4141 PlaneSurface::new(Plane::through(apex, Direction::new(normal, T).unwrap())).into()
4142 };
4143 let cases = [
4144 (Vector::new(1.0, 0.0, -1.0), 1, true),
4145 (Vector::new(1.0, 0.0, 0.0), 2, false),
4146 ];
4147 for (normal, count, tangent) in cases {
4148 let cut = plane(normal);
4149 let SurfaceIntersection::Along(sections) =
4150 intersect_surfaces(&surface, &cut, IntersectOptions::default(), T).unwrap()
4151 else {
4152 panic!("{normal:?}: rulings");
4153 };
4154 assert_eq!(sections.len(), count, "{normal:?}");
4155 for section in §ions {
4156 assert_eq!(section.tangential, tangent, "{normal:?}");
4157 let (lo, hi) = section.curve.domain();
4158 for k in 0..=4 {
4159 let p = section
4160 .curve
4161 .point_at(lo + (hi - lo) * f64::from(k) / 4.0, T)
4162 .unwrap();
4163 assert!(cone.distance_to(p) < 1e-9, "{p:?} on the cone");
4164 let height = p.z - 300.0;
4165 assert!(
4166 (-5.0 - 1e-9..=50.0 + 1e-9).contains(&height),
4167 "{p:?} in the window"
4168 );
4169 }
4170 }
4171 }
4172 let shallow = plane(Vector::new(0.2, 0.0, 1.0));
4173 assert!(matches!(
4174 intersect_surfaces(&surface, &shallow, IntersectOptions::default(), T).unwrap(),
4175 SurfaceIntersection::Touching(_) | SurfaceIntersection::Apart
4176 ));
4177 }
4178
4179 #[test]
4180 fn a_far_stated_ruling_reads_its_angle_on_the_used_nappe() {
4181 use ogeom_geom::ConeSurface;
4182 let cone =
4190 ogeom_math::Cone::new(Frame::WORLD, 24.0, core::f64::consts::FRAC_PI_4, T).unwrap();
4191 let surface: SurfaceGeometry = ConeSurface::new(cone, (-1e5, 1e5)).unwrap().into();
4192 let u_true = 0.01_f64;
4193 let radial = Vector::new(u_true.cos(), u_true.sin(), 0.0);
4194 let direction =
4197 Direction::new((radial + Vector::new(0.0, 0.0, 1.0)) / 2f64.sqrt(), T).unwrap();
4198 let far = -7.0e5;
4199 let origin = Point::ORIGIN + radial * 24.0 + direction.vector() * far;
4200 let line = ogeom_geom::LineCurve::over(
4201 ogeom_math::Axis::new(origin, direction),
4202 far.abs() - 1.0,
4203 far.abs() + 1.0,
4204 )
4205 .unwrap();
4206 let curve: Curve = line.into();
4207 let range = ogeom_geom::Curve3d::domain(&curve);
4208 let pcurve = exact_pcurve_over(&curve, range, &surface, T).expect("a ruling inverts");
4209 let at = pcurve.point_at(range.0, T).unwrap();
4210 let tau = core::f64::consts::TAU;
4211 let gap = (at.x - u_true)
4212 .rem_euclid(tau)
4213 .min(tau - (at.x - u_true).rem_euclid(tau));
4214 assert!(
4215 gap < 1e-6,
4216 "the ruling's chart angle must be the used side's: got u {} against {u_true}",
4217 at.x
4218 );
4219 }
4220
4221 #[test]
4228 fn loops_turning_sharply_where_drums_all_but_touch_fit_in_pieces() {
4229 let drum = |origin: Point, axis: Vector, x: Vector, radius: f64, height: (f64, f64)| {
4230 let frame = Frame::new(
4231 origin,
4232 Direction::new(axis, T).unwrap(),
4233 Direction::new(x, T).unwrap(),
4234 T,
4235 )
4236 .unwrap();
4237 let cylinder = Cylinder::new(frame, radius, T).unwrap();
4238 (
4239 cylinder,
4240 SurfaceGeometry::from(CylinderSurface::new(cylinder, height).unwrap()),
4241 )
4242 };
4243 let radius = 3.175;
4244 let (thin_drum, thin) = drum(
4245 Point::new(10.994_218_762_109_735, 53.975, -209.55),
4246 Vector::new(0.0, 0.0, -1.0),
4247 Vector::new(-1.0, 0.0, 0.0),
4248 radius,
4249 (-1000.0, 1000.0),
4250 );
4251 let (wide_drum, wide) = drum(
4252 Point::new(
4253 -14.478_610_818_124_423,
4254 3.999_371_635_181_902,
4255 -277.138_401_510_994_7,
4256 ),
4257 Vector::new(
4258 -0.565_016_635_381_368_8,
4259 0.528_780_602_945_684_7,
4260 0.633_361_883_673_714_3,
4261 ),
4262 Vector::new(0.0, 0.767_637_390_304_296_3, -0.640_884_417_821_817),
4263 57.088_766_757_544_09,
4264 (0.0, 171.306_147_621_762_6),
4265 );
4266 let options = IntersectOptions {
4267 tolerance: 1e-5,
4268 marching: crate::Marching {
4269 chord: 1e-5,
4270 ..crate::Marching::default()
4271 },
4272 };
4273 let SurfaceIntersection::Along(curves) =
4274 intersect_surfaces(&thin, &wide, options, T).unwrap()
4275 else {
4276 panic!("the drums cross");
4277 };
4278 let mut length = 0.0;
4279 for section in &curves {
4280 assert!(!section.tangential);
4281 assert!(
4282 section.tolerance < 1e-3,
4283 "a section states {} of doubt",
4284 section.tolerance
4285 );
4286 let (lo, hi) = section.curve.domain();
4287 let mut previous = section.curve.point_at(lo, T).unwrap();
4288 for i in 1..=400 {
4289 let t = lo + (hi - lo) * f64::from(i) / 400.0;
4290 let p = section.curve.point_at(t, T).unwrap();
4291 let off = thin_drum
4292 .distance_to(p)
4293 .abs()
4294 .max(wide_drum.distance_to(p).abs());
4295 assert!(off < 1e-3, "a section stands {off} off the drums");
4296 length += previous.distance(p);
4297 previous = p;
4298 }
4299 }
4300 assert!(
4303 length > 4.0 * core::f64::consts::PI * radius,
4304 "the sections cover {length}"
4305 );
4306 }
4307
4308 #[test]
4309 fn coincidence_is_measured_over_the_overlap_and_nowhere_else() {
4310 let patch = |plane: ogeom_math::Plane, u: (f64, f64), v: (f64, f64)| {
4313 let surface: SurfaceGeometry = PlaneSurface::over(plane, u, v).unwrap().into();
4314 SurfaceGeometry::from(surface.to_bspline(T).unwrap())
4315 };
4316 let reach = T.confusion() * 1e2;
4317
4318 let here = patch(ogeom_math::Plane::XY, (0.0, 10.0), (0.0, 10.0));
4321 let over = patch(ogeom_math::Plane::XY, (5.0, 15.0), (5.0, 15.0));
4322 assert!(surfaces_coincide(&here, &over, reach, T));
4323
4324 let above = patch(
4328 ogeom_math::Plane::new(
4329 Frame::new(Point::new(0.0, 0.0, 1.0), Direction::Z, Direction::X, T).unwrap(),
4330 ),
4331 (0.0, 10.0),
4332 (0.0, 10.0),
4333 );
4334 assert!(!surfaces_coincide(&here, &above, reach, T));
4335 let across = patch(
4336 ogeom_math::Plane::new(
4337 Frame::new(Point::new(5.0, 0.0, 0.0), Direction::X, Direction::Y, T).unwrap(),
4338 ),
4339 (0.0, 10.0),
4340 (0.0, 10.0),
4341 );
4342 assert!(!surfaces_coincide(&here, &across, reach, T));
4343 }
4344
4345 #[test]
4352 fn an_offset_square_to_a_plane_and_a_parabola_have_pcurves_on_it() {
4353 use ogeom_geom::{BSplineCurve, CircleCurve, OffsetCurve, ParabolaCurve};
4354 let normal = Vector::new(0.3, -0.5, 0.8);
4355 let up = Direction::new(normal, T).unwrap();
4356 let origin = Point::new(1.0, 2.0, -0.5);
4357 let ground = plane(origin, normal);
4358 let frame = Frame::new(
4359 origin,
4360 up,
4361 Direction::new(Vector::new(1.0, 0.0, -0.375), T).unwrap(),
4362 T,
4363 )
4364 .unwrap();
4365 let on = |x: f64, y: f64| frame.origin() + frame.x().vector() * x + frame.y().vector() * y;
4366 let knots = ogeom_math::KnotVector::clamped_uniform(3, 5).unwrap();
4367 let wiggle: Curve = BSplineCurve::new(
4368 knots,
4369 vec![
4370 on(0.0, 0.0),
4371 on(1.0, 1.0),
4372 on(2.0, -0.5),
4373 on(3.0, 0.5),
4374 on(4.0, 0.0),
4375 ],
4376 T,
4377 )
4378 .unwrap()
4379 .into();
4380 let round: Curve = CircleCurve::new(ogeom_math::Circle::new(frame, 1.5, T).unwrap()).into();
4381 let parabola: Curve =
4382 ParabolaCurve::over(ogeom_math::Parabola::new(frame, 0.7, T).unwrap(), -1.5, 2.0)
4383 .unwrap()
4384 .into();
4385 let mut curves = vec![parabola];
4386 for basis in [wiggle, round] {
4387 for reference in [up, -up] {
4388 for distance in [0.4, -0.3] {
4389 let offset = OffsetCurve::new(basis.clone(), distance, reference).unwrap();
4390 curves.push(Curve::Offset(Box::new(offset)));
4391 }
4392 }
4393 }
4394 for curve in &curves {
4395 let pcurve = exact_pcurve_of(curve, &ground, T).expect("it lies in the plane");
4396 let (lo, hi) = curve.domain();
4397 for i in 0..=16 {
4398 let t = (hi - lo).mul_add(f64::from(i) / 16.0, lo);
4399 let at = pcurve.point_at(t, T).unwrap();
4400 let lifted = ground.point_at(at.x, at.y, T).unwrap();
4401 let point = curve.point_at(t, T).unwrap();
4402 assert!(
4403 lifted.distance(point) < 1e-9,
4404 "{curve:?} at {t}: {lifted:?} against {point:?}"
4405 );
4406 }
4407 }
4408 let leaned = Direction::new(normal + frame.x().vector() * 0.05, T).unwrap();
4411 let knots = ogeom_math::KnotVector::clamped_uniform(3, 4).unwrap();
4412 let straight: Curve = BSplineCurve::new(
4413 knots,
4414 vec![on(0.0, 0.0), on(1.0, 0.0), on(2.0, 0.0), on(3.0, 0.0)],
4415 T,
4416 )
4417 .unwrap()
4418 .into();
4419 let off = Curve::Offset(Box::new(OffsetCurve::new(straight, 0.4, leaned).unwrap()));
4420 assert!(exact_pcurve_of(&off, &ground, T).is_none());
4421 }
4422
4423 #[test]
4428 fn a_hyperbola_in_a_plane_has_its_pcurve_there() {
4429 use ogeom_geom::{HyperbolaCurve, OffsetCurve, Reversible as _};
4430 let normal = Vector::new(0.3, -0.5, 0.8);
4431 let up = Direction::new(normal, T).unwrap();
4432 let origin = Point::new(1.0, 2.0, -0.5);
4433 let ground = plane(origin, normal);
4434 let x = Direction::new(Vector::new(1.0, 0.0, -0.375), T).unwrap();
4435 let base = Frame::new(origin, up, x, T).unwrap();
4436 let centre = origin + base.x().vector() * 0.2 + base.y().vector() * 0.1;
4437 let frame = Frame::new(centre, up, x, T).unwrap();
4438 let turned_over =
4439 Frame::from_axes(frame.origin(), frame.x(), -frame.y(), -frame.z(), T).unwrap();
4440 let branch = |frame: Frame, lo: f64, hi: f64| -> Curve {
4441 HyperbolaCurve::over(
4442 ogeom_math::Hyperbola::new(frame, 1.5, 0.8, T).unwrap(),
4443 lo,
4444 hi,
4445 )
4446 .unwrap()
4447 .into()
4448 };
4449 let forward = branch(frame, -1.2, 0.7);
4450 let mut curves = vec![
4451 forward.clone(),
4452 branch(turned_over, -1.2, 0.7),
4453 branch(frame, -1.2, 1.2).reversed(),
4454 branch(frame, -1.2, 0.7).reversed(),
4455 ];
4456 for reference in [up, -up] {
4457 let offset = OffsetCurve::new(forward.clone(), 0.3, reference).unwrap();
4458 curves.push(Curve::Offset(Box::new(offset)));
4459 }
4460 for curve in &curves {
4461 let pcurve = exact_pcurve_of(curve, &ground, T).expect("it lies in the plane");
4462 let (lo, hi) = curve.domain();
4463 for i in 0..=16 {
4464 let t = (hi - lo).mul_add(f64::from(i) / 16.0, lo);
4465 let at = pcurve.point_at(t, T).unwrap();
4466 let lifted = ground.point_at(at.x, at.y, T).unwrap();
4467 let point = curve.point_at(t, T).unwrap();
4468 assert!(
4469 lifted.distance(point) < 1e-9,
4470 "{curve:?} at {t}: {lifted:?} against {point:?}"
4471 );
4472 if matches!(curve, Curve::Offset(_)) {
4473 continue;
4474 }
4475 let flat = pcurve.derivatives_at(t, 2, T).unwrap();
4478 let space = curve.derivatives_at(t, 2, T).unwrap();
4479 let axes = ground_axes(&ground);
4480 for (order, (f, s)) in flat.iter().zip(&space).enumerate().skip(1) {
4481 let lifted = axes.0 * f.x + axes.1 * f.y;
4482 assert!(
4483 (lifted - *s).magnitude() < 1e-9,
4484 "{curve:?} at {t}, order {order}: {lifted:?} against {s:?}"
4485 );
4486 }
4487 }
4488 }
4489 }
4490
4491 fn lifts_onto(
4495 curve: &Curve,
4496 pcurve: &PlanarCurve,
4497 range: (f64, f64),
4498 surface: &SurfaceGeometry,
4499 ) {
4500 use ogeom_geom::Surface as _;
4501 for i in 0..=64 {
4502 let t = (range.1 - range.0).mul_add(f64::from(i) / 64.0, range.0);
4503 let at = pcurve.derivatives_at(t, 1, T).unwrap();
4504 let lifted = surface.point_at(at[0].x, at[0].y, T).unwrap();
4505 let point = curve.point_at(t, T).unwrap();
4506 assert!(
4507 lifted.distance(point) < 1e-12,
4508 "{curve:?} at {t}: {lifted:?} against {point:?}"
4509 );
4510 let (su, sv) = surface.d1_at(at[0].x, at[0].y, T).unwrap();
4511 let tangent = su * at[1].x + sv * at[1].y;
4512 let own = curve.d1_at(t, T).unwrap();
4513 assert!(
4514 (tangent - own).magnitude() < 1e-12,
4515 "{curve:?} at {t}: tangent {tangent:?} against {own:?}"
4516 );
4517 }
4518 }
4519
4520 #[test]
4525 fn a_reversed_trim_in_a_plane_has_its_pcurve_there() {
4526 use ogeom_geom::{CircleCurve, HyperbolaCurve, Reversible as _, TrimmedCurve};
4527 let normal = Vector::new(0.3, -0.5, 0.8);
4528 let up = Direction::new(normal, T).unwrap();
4529 let origin = Point::new(1.0, 2.0, -0.5);
4530 let ground = plane(origin, normal);
4531 let x = Direction::new(Vector::new(1.0, 0.0, -0.375), T).unwrap();
4532 let base = Frame::new(origin, up, x, T).unwrap();
4533 let centre = origin + base.x().vector() * 0.2 + base.y().vector() * 0.1;
4534 let frame = Frame::new(centre, up, x, T).unwrap();
4535 let circle: Curve =
4536 CircleCurve::new(ogeom_math::Circle::new(frame, 1.3, T).unwrap()).into();
4537 let hyperbola: Curve = HyperbolaCurve::over(
4538 ogeom_math::Hyperbola::new(frame, 1.5, 0.8, T).unwrap(),
4539 -1.2,
4540 0.7,
4541 )
4542 .unwrap()
4543 .into();
4544 let trim = |basis: &Curve, lo: f64, hi: f64| -> Curve {
4545 TrimmedCurve::new(basis.clone(), lo, hi, T).unwrap().into()
4546 };
4547 let curves = [
4548 trim(&circle, 0.4, 2.9).reversed(),
4549 trim(&circle, 5.5, 7.1).reversed(),
4550 trim(&circle.reversed(), 0.4, 2.9).reversed(),
4551 trim(&hyperbola, -0.9, 0.5).reversed(),
4552 trim(&trim(&hyperbola, -1.0, 0.6).reversed(), -0.9, 0.5).reversed(),
4553 ];
4554 for curve in &curves {
4555 let pcurve = exact_pcurve_of(curve, &ground, T).expect("it lies in the plane");
4556 lifts_onto(curve, &pcurve, curve.domain(), &ground);
4557 }
4558 }
4559
4560 #[test]
4564 fn a_reversed_trim_of_a_ruling_on_a_cone_has_its_pcurve_there() {
4565 use ogeom_geom::{ConeSurface, Reversible as _, TrimmedCurve};
4566 let cone =
4567 ogeom_math::Cone::new(Frame::WORLD, 24.0, core::f64::consts::FRAC_PI_4, T).unwrap();
4568 let surface: SurfaceGeometry = ConeSurface::new(cone, (-1e3, 1e3)).unwrap().into();
4569 let radial = Vector::new(0.3_f64.cos(), 0.3_f64.sin(), 0.0);
4570 let direction =
4571 Direction::new((radial + Vector::new(0.0, 0.0, 1.0)) / 2f64.sqrt(), T).unwrap();
4572 let apex = Point::new(0.0, 0.0, -24.0);
4573 let line: Curve =
4574 ogeom_geom::LineCurve::over(ogeom_math::Axis::new(apex, direction), 5.0, 50.0)
4575 .unwrap()
4576 .into();
4577 let curve = Curve::from(TrimmedCurve::new(line, 10.0, 30.0, T).unwrap()).reversed();
4578 let pcurve = exact_pcurve_of(&curve, &surface, T).expect("a ruling inverts");
4579 lifts_onto(&curve, &pcurve, curve.domain(), &surface);
4580 }
4581
4582 #[test]
4590 fn a_ruling_past_the_apex_has_its_pcurve_on_its_own_nappe() {
4591 use ogeom_geom::{ConeSurface, Reversible as _};
4592 let frame = Frame::new(
4593 Point::new(1.0, -2.0, 0.5),
4594 Direction::new(Vector::new(0.2, -0.3, 1.0), T).unwrap(),
4595 Direction::from_cross(Vector::new(0.2, -0.3, 1.0), Vector::new(0.0, 1.0, 0.0), T)
4596 .unwrap(),
4597 T,
4598 )
4599 .unwrap();
4600 for half_angle in [core::f64::consts::FRAC_PI_4, -core::f64::consts::FRAC_PI_6] {
4601 let cone = ogeom_math::Cone::new(frame, 3.0, half_angle, T).unwrap();
4602 let surface: SurfaceGeometry = ConeSurface::new(cone, (-200.0, 200.0)).unwrap().into();
4603 let u = 0.3_f64;
4606 let (x, y, z) = (frame.x().vector(), frame.y().vector(), frame.z().vector());
4607 let bearing = x * u.cos() + y * u.sin();
4608 let apex = frame.origin() + z * (-3.0 / half_angle.tan());
4609 let up = Direction::new(bearing * half_angle.tan() + z, T).unwrap();
4610 let stated = |origin: f64, range: (f64, f64)| -> Curve {
4611 let at = apex + up.vector() * origin;
4612 ogeom_geom::LineCurve::over(
4613 ogeom_math::Axis::new(at, up),
4614 range.0 - origin,
4615 range.1 - origin,
4616 )
4617 .unwrap()
4618 .into()
4619 };
4620 let lines = [
4621 stated(0.0, (5.0, 40.0)),
4622 stated(0.0, (-40.0, -5.0)),
4623 stated(0.0, (-10.0, 30.0)),
4624 stated(0.0, (-30.0, 10.0)),
4625 stated(60.0, (-40.0, -5.0)),
4626 stated(-60.0, (5.0, 40.0)),
4627 stated(0.0, (-30.0, 10.0)).reversed(),
4628 stated(0.0, (-40.0, -5.0)).reversed(),
4629 ];
4630 for line in &lines {
4631 let range = line.domain();
4632 let pcurve = exact_pcurve_over(line, range, &surface, T).expect("a ruling inverts");
4633 lifts_onto(line, &pcurve, range, &surface);
4634 }
4635 }
4636 }
4637
4638 #[test]
4645 fn a_curve_drawn_on_the_plane_under_any_chart_has_its_pcurve_there() {
4646 use ogeom_geom::{
4647 BSpline2d, Circle2d, CurveOnSurface, Line2d, PlaneSurface, Reversible as _,
4648 TrimmedSurface,
4649 };
4650 let normal = Vector::new(0.3, -0.5, 0.8);
4651 let up = Direction::new(normal, T).unwrap();
4652 let origin = Point::new(1.0, 2.0, -0.5);
4653 let ground = plane(origin, normal);
4654 let x = Direction::new(Vector::new(1.0, 0.0, -0.375), T).unwrap();
4655 let own = Frame::new(origin, up, x, T).unwrap();
4656 let elsewhere = origin + own.x().vector() * 0.4 + own.y().vector() * 0.5;
4657 let chart = |frame: Frame| -> SurfaceGeometry {
4658 PlaneSurface::over(Plane::new(frame), (-6.0, 6.0), (-6.0, 6.0))
4659 .unwrap()
4660 .into()
4661 };
4662 let turned = Frame::new(
4663 elsewhere,
4664 up,
4665 Direction::from_cross(normal, Vector::new(0.2, 0.9, 0.1), T).unwrap(),
4666 T,
4667 )
4668 .unwrap();
4669 let mirrored = Frame::new(elsewhere, -up, x, T).unwrap();
4670 let trimmed: SurfaceGeometry =
4671 TrimmedSurface::new(chart(turned), (-3.0, 3.0), (-3.0, 3.0), T)
4672 .unwrap()
4673 .into();
4674 let ring = ogeom_math::Circle2::new(
4675 ogeom_math::Frame2::new(Point2::new(0.3, -0.2), ogeom_math::Direction2::X),
4676 0.9,
4677 T,
4678 )
4679 .unwrap();
4680 let knots = ogeom_math::KnotVector::clamped_uniform(3, 5).unwrap();
4681 let wave = BSpline2d::new(
4682 knots,
4683 vec![
4684 Point2::new(-1.0, 0.0),
4685 Point2::new(-0.5, 0.7),
4686 Point2::new(0.0, -0.4),
4687 Point2::new(0.6, 0.5),
4688 Point2::new(1.1, 0.1),
4689 ],
4690 T,
4691 )
4692 .unwrap();
4693 let pcurves: [PlanarCurve; 4] = [
4694 Line2d::segment(Point2::new(-0.5, 0.25), Point2::new(1.0, 0.75), T)
4695 .unwrap()
4696 .into(),
4697 Circle2d::new(ring).into(),
4698 PlanarCurve::from(Circle2d::new(ring)).reversed(),
4699 wave.into(),
4700 ];
4701 for surface in [chart(own), chart(turned), chart(mirrored), trimmed] {
4702 for drawn in &pcurves {
4703 let curve = Curve::OnSurface(Box::new(CurveOnSurface::new(
4704 drawn.clone(),
4705 surface.clone(),
4706 )));
4707 let pcurve = exact_pcurve_of(&curve, &ground, T).expect("it lies in the plane");
4708 lifts_onto(&curve, &pcurve, curve.domain(), &ground);
4709 }
4710 }
4711 let tipped = Frame::new(
4712 elsewhere,
4713 Direction::new(normal + Vector::new(1e-3, 0.0, 0.0), T).unwrap(),
4714 x,
4715 T,
4716 );
4717 let standing = Frame::new(elsewhere + up.vector() * 1e-3, up, x, T).unwrap();
4718 for frame in [tipped.unwrap(), standing] {
4719 let curve = Curve::OnSurface(Box::new(CurveOnSurface::new(
4720 pcurves[0].clone(),
4721 chart(frame),
4722 )));
4723 assert!(exact_pcurve_of(&curve, &ground, T).is_none());
4724 }
4725 }
4726
4727 fn ground_axes(ground: &SurfaceGeometry) -> (Vector, Vector) {
4729 let SurfaceGeometry::Plane(p) = ground else {
4730 unreachable!("a plane")
4731 };
4732 let frame = p.plane().frame();
4733 (frame.x().vector(), frame.y().vector())
4734 }
4735
4736 fn drafted_drum() -> SurfaceGeometry {
4738 let frame = Frame::new(Point::new(0.0, 0.0, 2.0), -Direction::Z, -Direction::X, T).unwrap();
4739 ogeom_geom::ConeSurface::new(
4740 ogeom_math::Cone::new(frame, 3.0, 0.1_f64.atan(), T).unwrap(),
4741 (0.0, 2.0),
4742 )
4743 .unwrap()
4744 .into()
4745 }
4746
4747 fn carried_round(turn: f64, sweep: (f64, f64)) -> (SurfaceGeometry, f64) {
4753 let lean = 1.01_f64.sqrt();
4754 let c = lean.mul_add(-0.5, 3.05);
4755 let frame = Frame::from_axes(
4756 Point::new(c, 0.0, 1.5),
4757 Direction::new(Vector::new(1.0, 0.0, 0.1), T).unwrap(),
4758 Direction::new(Vector::new(-0.1, 0.0, 1.0), T).unwrap(),
4759 -Direction::Y,
4760 T,
4761 )
4762 .unwrap();
4763 let arc: Curve = ogeom_geom::TrimmedCurve::new(
4764 ogeom_geom::CircleCurve::new(ogeom_math::Circle::new(frame, 0.5, T).unwrap()).into(),
4765 0.0,
4766 turn,
4767 T,
4768 )
4769 .unwrap()
4770 .into();
4771 let band = ogeom_geom::ExtrusionSurface::over(arc, Direction::Y, sweep)
4772 .unwrap()
4773 .into();
4774 (band, c)
4775 }
4776
4777 fn crosses_from_the_node_to_both_ends(
4791 (turn, sweep): (f64, (f64, f64)),
4792 angle: f64,
4793 window: Option<ParameterWindow>,
4794 ) {
4795 let (band, c) = carried_round(turn, sweep);
4796 let about = ogeom_math::Transform::rotation(
4797 ogeom_math::Axis {
4798 location: Point::ORIGIN,
4799 direction: Direction::Z,
4800 },
4801 angle,
4802 );
4803 let band = ogeom_geom::Transformable::transformed(&band, &about, T).unwrap();
4804 let (sin, cos) = angle.sin_cos();
4805 let turned = |p: Point| {
4806 Point::new(
4807 p.x.mul_add(cos, -p.y * sin),
4808 p.x.mul_add(sin, p.y * cos),
4809 p.z,
4810 )
4811 };
4812 let back = |p: Point| {
4813 Point::new(
4814 p.x.mul_add(cos, p.y * sin),
4815 p.y.mul_add(cos, -p.x * sin),
4816 p.z,
4817 )
4818 };
4819 let drum = drafted_drum();
4820 let lean = 1.01_f64.sqrt();
4821 let at = |u: f64, y: f64| {
4822 let (cos, sin) = (u.cos(), u.sin());
4823 Point::new(
4824 0.5f64.mul_add((cos - 0.1 * sin) / lean, c),
4825 y,
4826 0.5f64.mul_add((0.1f64.mul_add(cos, sin)) / lean, 1.5),
4827 )
4828 };
4829 let off = |p: Point| p.x.hypot(p.y) - 0.1f64.mul_add(-p.z, 3.2);
4830 let crossing = |y: f64| {
4831 let (mut lo, mut hi) = (1e-9, 0.425);
4832 for _ in 0..200 {
4833 let mid = f64::midpoint(lo, hi);
4834 if off(at(mid, y)).signum() == off(at(lo, y)).signum() {
4835 lo = mid;
4836 } else {
4837 hi = mid;
4838 }
4839 }
4840 turned(at(f64::midpoint(lo, hi), y))
4841 };
4842 let node = turned(at(0.0, 0.0));
4843 let ends = window.map_or(sweep, |w| w.1);
4844 let far = [crossing(ends.0), crossing(ends.1)];
4845 let options = window.map_or_else(IntersectOptions::default, |_| IntersectOptions {
4847 tolerance: 1e-7,
4848 marching: Marching {
4849 chord: 1e-7,
4850 ..Marching::default()
4851 },
4852 });
4853 for band_first in [true, false] {
4854 let met = if band_first {
4855 intersect_surfaces_within(&band, &drum, (window, None), options, T)
4856 } else {
4857 intersect_surfaces_within(&drum, &band, (None, window), options, T)
4858 }
4859 .unwrap();
4860 let SurfaceIntersection::Along(sections) = met else {
4861 panic!("turned {angle}: the band crosses the cone, got {met:?}");
4862 };
4863 let mut found: Vec<Point> = Vec::new();
4864 for section in §ions {
4865 assert!(
4866 !section.tangential,
4867 "turned {angle}, band first {band_first}: {section:?}"
4868 );
4869 assert!(
4870 section.tolerance <= options.marching.chord + options.tolerance,
4871 "turned {angle}, band first {band_first}: a tolerance of {:e}",
4872 section.tolerance
4873 );
4874 let (lo, hi) = section.curve.domain();
4875 let near = section.tolerance + T.confusion();
4876 for i in 0..=16 {
4877 let p = section
4878 .curve
4879 .point_at(lo + (hi - lo) * f64::from(i) / 16.0, T)
4880 .unwrap();
4881 let q = back(p);
4882 let from_band = ((q.x - c).hypot(q.z - 1.5) - 0.5).abs();
4883 assert!(
4884 from_band <= near && off(p).abs() <= near,
4885 "{p:?} is off the band by {from_band:e} or the cone by {:e}",
4886 off(p)
4887 );
4888 }
4889 found.extend([lo, hi].map(|t| section.curve.point_at(t, T).unwrap()));
4890 }
4891 let near = sections.iter().map(|s| s.tolerance).fold(0.0, f64::max) + T.confusion();
4892 assert!(
4893 found.iter().any(|p| p.distance(node) <= near),
4894 "turned {angle}: no section ends at the node {node:?}: {found:?}"
4895 );
4896 for want in far {
4897 let count = found.iter().filter(|p| p.distance(want) <= near).count();
4898 assert_eq!(
4899 count, 1,
4900 "turned {angle}: {count} sections end at {want:?}: {found:?}"
4901 );
4902 }
4903 for p in &found {
4904 assert!(
4905 p.distance(node) <= near || far.iter().any(|q| p.distance(*q) <= near),
4906 "turned {angle}, band first {band_first}: a section ends at {p:?}, \
4907 neither the node nor an end: {found:?}"
4908 );
4909 }
4910 }
4911 }
4912
4913 #[test]
4919 fn a_band_crossing_a_cone_shallowly_from_a_node_on_its_border_crosses_it() {
4920 crosses_from_the_node_to_both_ends((0.425, (-0.012, 0.02)), 0.0, None);
4921 }
4922
4923 #[test]
4931 fn a_band_crossing_a_cone_from_a_node_keeps_its_arm_short_of_the_node_s_radius() {
4932 for turn in [0.425, 1.2] {
4933 for low in [-0.004, -0.002] {
4934 crosses_from_the_node_to_both_ends((turn, (low, 0.02)), 0.0, None);
4935 }
4936 }
4937 }
4938
4939 #[test]
4946 fn a_band_crossing_a_cone_from_a_node_keeps_its_arm_shorter_than_ten_chords() {
4947 for turn in [0.425, 1.2] {
4948 for low in [-0.000_74, -0.000_5] {
4949 crosses_from_the_node_to_both_ends((turn, (low, 0.02)), 0.0, None);
4950 }
4951 }
4952 }
4953
4954 #[test]
4968 fn a_band_crossing_a_cone_is_two_crossings_at_every_turn() {
4969 let window = (
4970 (0.0, 0.424_721_486_693_660_6),
4971 (-0.003_979_668_916_835_106, 0.019_898_344_584_175_24),
4972 );
4973 for k in 0..720 {
4974 crosses_from_the_node_to_both_ends(
4975 (
4976 1.471_127_674_303_734_5,
4977 (-2.415_259_779_545_657_6, 4.830_519_559_091_315),
4978 ),
4979 (f64::from(k) * 0.5).to_radians(),
4980 Some(window),
4981 );
4982 }
4983 }
4984
4985 #[test]
4994 fn a_band_crossing_a_cone_unwindowed_is_two_crossings_at_every_turn() {
4995 for turn in [1.2, 1.471_127_674_303_734_5] {
4996 for k in 0..720 {
4997 crosses_from_the_node_to_both_ends(
4998 (turn, (-0.004, 0.02)),
4999 (f64::from(k) * 0.5).to_radians(),
5000 None,
5001 );
5002 }
5003 }
5004 }
5005
5006 #[test]
5014 fn a_band_crossing_a_cone_unwindowed_states_its_fit_on_a_short_branch() {
5015 for k in 0..720 {
5016 crosses_from_the_node_to_both_ends(
5017 (1.471_127_674_303_734_5, (-0.002, 0.02)),
5018 (f64::from(k) * 0.5).to_radians(),
5019 None,
5020 );
5021 }
5022 }
5023
5024 #[test]
5039 fn a_band_crossing_a_cone_in_a_small_window_ends_on_its_border() {
5040 for low in [-8e-4, -5e-5] {
5041 let window = (
5042 (0.0, 0.424_721_486_693_660_6),
5043 (low, 0.019_898_344_584_175_24),
5044 );
5045 for k in 0..720 {
5046 crosses_from_the_node_to_both_ends(
5047 (
5048 1.471_127_674_303_734_5,
5049 (-2.415_259_779_545_657_6, 4.830_519_559_091_315),
5050 ),
5051 (f64::from(k) * 0.5).to_radians(),
5052 Some(window),
5053 );
5054 }
5055 }
5056 }
5057
5058 #[test]
5068 fn a_band_crossing_a_cone_in_a_window_cut_by_its_node_has_no_section_along_the_cut() {
5069 let window = (
5070 (0.0, 0.424_721_486_693_660_6),
5071 (-1e-5, 0.019_898_344_584_175_24),
5072 );
5073 for k in 0..720 {
5074 crosses_from_the_node_to_both_ends(
5075 (
5076 1.471_127_674_303_734_5,
5077 (-2.415_259_779_545_657_6, 4.830_519_559_091_315),
5078 ),
5079 (f64::from(k) * 0.5).to_radians(),
5080 Some(window),
5081 );
5082 }
5083 }
5084
5085 #[test]
5091 fn a_band_crossing_a_cone_from_a_node_keeps_its_arm_shorter_than_the_snap() {
5092 for low in [-0.000_32, -0.000_2, -0.000_11] {
5093 for k in 0..360 {
5094 crosses_from_the_node_to_both_ends(
5095 (0.425, (low, 0.02)),
5096 f64::from(k).to_radians(),
5097 None,
5098 );
5099 }
5100 }
5101 }
5102
5103 #[test]
5110 fn a_band_crossing_a_cone_states_a_tight_tolerance_by_its_node() {
5111 let window = (
5112 (0.0, 0.424_721_486_693_660_6),
5113 (-0.003_979_668_916_835_106, 0.019_898_344_584_175_24),
5114 );
5115 crosses_from_the_node_to_both_ends(
5116 (
5117 1.471_127_674_303_734_5,
5118 (-2.415_259_779_545_657_6, 4.830_519_559_091_315),
5119 ),
5120 60.0_f64.to_radians(),
5121 Some(window),
5122 );
5123 }
5124
5125 #[test]
5136 fn a_drum_pressed_into_a_cone_along_a_ruling_crosses_it_only_measurably() {
5137 let drum = drafted_drum();
5138 let lean = 1.01_f64.sqrt();
5139 let up = Vector::new(-0.1, 0.0, 1.0) / lean;
5140 let out = Vector::new(1.0, 0.0, 0.1) / lean;
5141 let off = |p: Point| p.x.hypot(p.y) - 0.1f64.mul_add(-p.z, 3.2);
5142 for into in [0.0, 0.4 * T.confusion(), 1e-6, 1e-5] {
5143 let foot = Point::new(3.0, 0.0, 2.0) + out * (0.5 - into);
5144 let frame = Frame::new(
5145 foot,
5146 Direction::new(up, T).unwrap(),
5147 Direction::new(out, T).unwrap(),
5148 T,
5149 )
5150 .unwrap();
5151 let roller: SurfaceGeometry =
5152 CylinderSurface::new(Cylinder::new(frame, 0.5, T).unwrap(), (-1.8, -0.2))
5153 .unwrap()
5154 .into();
5155 let from_axis = |p: Point| {
5156 let d = p - foot;
5157 (d - up * d.dot(up)).magnitude()
5158 };
5159 for (a, b) in [(&roller, &drum), (&drum, &roller)] {
5160 let met = intersect_surfaces(a, b, IntersectOptions::default(), T).unwrap();
5161 let SurfaceIntersection::Along(sections) = met else {
5162 panic!("pressed in {into:e}: {met:?}");
5163 };
5164 if into <= T.confusion() {
5165 assert!(
5166 sections.iter().all(|s| s.tangential),
5167 "pressed in {into:e}: {sections:?}"
5168 );
5169 continue;
5170 }
5171 assert_eq!(sections.len(), 2, "pressed in {into:e}: {sections:?}");
5172 let mut sides = Vec::new();
5173 for section in §ions {
5174 assert!(!section.tangential, "pressed in {into:e}: {section:?}");
5175 let (lo, hi) = section.curve.domain();
5176 let near = section.tolerance + T.confusion();
5177 let mut side = 0.0;
5178 for i in 0..=16 {
5179 let p = section
5180 .curve
5181 .point_at(lo + (hi - lo) * f64::from(i) / 16.0, T)
5182 .unwrap();
5183 assert!(
5184 (from_axis(p) - 0.5).abs() <= near && off(p).abs() <= near,
5185 "pressed in {into:e}: {p:?} is off the drum or the cone"
5186 );
5187 side += p.y;
5188 }
5189 sides.push(side);
5190 }
5191 assert!(
5192 sides[0] * sides[1] < 0.0,
5193 "pressed in {into:e}: both on one side {sides:?}"
5194 );
5195 }
5196 }
5197 }
5198}