1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
25use ogeom_geom::{Curve, Curve3d, Surface, SurfaceGeometry};
26use ogeom_math::{Point, Vector, solve};
27
28use crate::march::{Cell, sample_by, segment_meets_triangle};
29
30#[derive(Debug, Clone, Copy, PartialEq)]
32pub struct Piercing {
33 pub on_curve: f64,
35 pub on_surface: (f64, f64),
37 pub point: Point,
39 pub gap: f64,
41}
42
43#[derive(Debug, Clone, PartialEq)]
45pub struct CurveSurfaceIntersection {
46 pub crossings: Vec<Piercing>,
48 pub lying: Vec<(f64, f64)>,
54}
55
56impl CurveSurfaceIntersection {
57 #[must_use]
59 pub fn is_empty(&self) -> bool {
60 self.crossings.is_empty() && self.lying.is_empty()
61 }
62
63 const fn empty() -> Self {
64 Self {
65 crossings: Vec::new(),
66 lying: Vec::new(),
67 }
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq)]
73pub struct CurveSurfaceOptions {
74 pub samples: usize,
76 pub grid: usize,
78 pub gap: f64,
80}
81
82impl Default for CurveSurfaceOptions {
83 fn default() -> Self {
84 Self {
85 samples: 128,
86 grid: 24,
87 gap: 1e-7,
88 }
89 }
90}
91
92pub fn intersect_curve_surface(
103 curve: &Curve,
104 surface: &SurfaceGeometry,
105 options: CurveSurfaceOptions,
106 tol: Tolerances,
107) -> OgeomResult<CurveSurfaceIntersection> {
108 if let (Curve::Line(_), SurfaceGeometry::BSpline(spline)) = (curve, surface)
109 && options.samples >= 2
110 && options.gap.is_finite()
111 && options.gap > 0.0
112 && let Some(patches) = crate::bezier_clip::Patches::of(spline, tol)?
113 {
114 return clipped(curve, surface, &patches, options, tol);
115 }
116 intersect_with(curve, surface, options, tol, || {
117 let cells = sample_by(surface, seeding(surface, options.grid), tol);
118 let tree = CellTree::over(&cells, options.gap);
119 (
120 std::borrow::Cow::Owned(cells),
121 std::borrow::Cow::Owned(tree),
122 )
123 })
124}
125
126#[must_use]
142pub fn touch_curve_surface(
143 curve: &Curve,
144 surface: &SurfaceGeometry,
145 seed: f64,
146 tol: Tolerances,
147) -> Option<Piercing> {
148 let foot =
149 ogeom_geom::project_on_surface(surface, curve.point_at(seed, tol).ok()?, 16, tol).ok()?;
150 let along = |t: f64| {
151 let d = curve.derivatives_at(t, 2, tol).ok()?;
152 Some((Point::ORIGIN + *d.first()?, *d.get(1)?, *d.get(2)?))
153 };
154 let start = [seed, foot.parameters.0, foot.parameters.1];
155 let [t, u, v] = touch_solved(along, surface, start, tol)?;
156 let (lo, hi) = curve.domain();
157 let ((ua, ub), (va, vb)) = surface.domain();
158 let within = |x: f64, lo: f64, hi: f64, periodic: bool| {
159 periodic || (x >= lo - tol.parametric() && x <= hi + tol.parametric())
160 };
161 if !within(t, lo, hi, curve.is_periodic())
162 || !within(u, ua, ub, surface.is_periodic_u())
163 || !within(v, va, vb, surface.is_periodic_v())
164 {
165 return None;
166 }
167 let point = curve.point_at(t, tol).ok()?;
168 let gap = point.distance(surface.point_at(u, v, tol).ok()?);
169 (gap <= tol.confusion()).then_some(Piercing {
170 on_curve: t,
171 on_surface: (u, v),
172 point,
173 gap,
174 })
175}
176
177pub(crate) fn touch_solved(
196 along: impl Fn(f64) -> Option<(Point, Vector, Vector)>,
197 surface: &SurfaceGeometry,
198 start: [f64; 3],
199 tol: Tolerances,
200) -> Option<[f64; 3]> {
201 let system = |x: &[f64; 3]| -> Option<([f64; 3], [[f64; 3]; 3])> {
202 let (p, c, c2) = along(x[0])?;
203 let (q, ou, ov) = surface.point_d1_at(x[1], x[2], tol).ok()?;
204 let (ouu, ouv, ovv) = surface.d2_at(x[1], x[2], tol).ok()?;
205 let gap = p - q;
206 let normal = ou.cross(ov);
207 let residual = [gap.dot(ou), gap.dot(ov), c.dot(normal)];
208 let jacobian = [
209 [
210 c.dot(ou),
211 gap.dot(ouu) - ou.dot(ou),
212 gap.dot(ouv) - ov.dot(ou),
213 ],
214 [
215 c.dot(ov),
216 gap.dot(ouv) - ou.dot(ov),
217 gap.dot(ovv) - ov.dot(ov),
218 ],
219 [
220 c2.dot(normal),
221 c.dot(ouu.cross(ov) + ou.cross(ouv)),
222 c.dot(ouv.cross(ov) + ou.cross(ovv)),
223 ],
224 ];
225 Some((residual, jacobian))
226 };
227 let (_, jacobian) = system(&start)?;
228 let criteria = solve::Criteria {
234 residual: 0.0,
235 step: tol.parametric(),
236 max_iterations: 40,
237 };
238 let (found, _, _, _) = solve::newton_system_fixed(
239 |x| system(x).unwrap_or(([f64::INFINITY; 3], [[0.0; 3]; 3])),
240 start,
241 criteria,
242 )
243 .ok()?;
244 let (left, after) = system(&found)?;
245 let step = solve::linear_fixed(after, left)?;
246 if step.iter().any(|d| d.abs() > tol.parametric()) {
247 return None;
248 }
249 let mut drift = 0.0;
250 for column in 0..3 {
251 let change: [f64; 3] =
252 core::array::from_fn(|row| after[row][column] - jacobian[row][column]);
253 let moved = solve::linear_fixed(jacobian, change)?;
254 drift += moved.iter().map(|v| v * v).sum::<f64>();
255 }
256 (drift.sqrt() <= 0.5).then_some(found)
257}
258
259#[derive(Debug)]
268pub struct PreparedSurface {
269 surface: SurfaceGeometry,
270 options: CurveSurfaceOptions,
271 tol: Tolerances,
272 cells: std::sync::OnceLock<(Vec<Cell>, CellTree)>,
273 patches: std::sync::OnceLock<Option<crate::bezier_clip::Patches>>,
275}
276
277impl PreparedSurface {
278 #[must_use]
280 pub const fn new(
281 surface: SurfaceGeometry,
282 options: CurveSurfaceOptions,
283 tol: Tolerances,
284 ) -> Self {
285 Self {
286 surface,
287 options,
288 tol,
289 cells: std::sync::OnceLock::new(),
290 patches: std::sync::OnceLock::new(),
291 }
292 }
293
294 #[must_use]
296 pub const fn surface(&self) -> &SurfaceGeometry {
297 &self.surface
298 }
299
300 pub fn intersect(&self, curve: &Curve) -> OgeomResult<CurveSurfaceIntersection> {
306 let (surface, options, tol) = (&self.surface, self.options, self.tol);
307 if let (Curve::Line(_), SurfaceGeometry::BSpline(spline)) = (curve, surface)
310 && options.samples >= 2
311 && options.gap.is_finite()
312 && options.gap > 0.0
313 && let Some(patches) = self
314 .patches
315 .get_or_init(|| crate::bezier_clip::Patches::of(spline, tol).ok().flatten())
316 {
317 return clipped(curve, surface, patches, options, tol);
318 }
319 intersect_with(curve, surface, options, tol, || {
320 let (cells, tree) = self.cells.get_or_init(|| {
321 let cells = sample_by(surface, seeding(surface, options.grid), tol);
322 let tree = CellTree::over(&cells, options.gap);
323 (cells, tree)
324 });
325 (
326 std::borrow::Cow::Borrowed(cells.as_slice()),
327 std::borrow::Cow::Borrowed(tree),
328 )
329 })
330 }
331}
332
333fn intersect_with<'c>(
334 curve: &Curve,
335 surface: &SurfaceGeometry,
336 options: CurveSurfaceOptions,
337 tol: Tolerances,
338 cells: impl FnOnce() -> (std::borrow::Cow<'c, [Cell]>, std::borrow::Cow<'c, CellTree>),
339) -> OgeomResult<CurveSurfaceIntersection> {
340 if options.samples < 2 || options.grid < 2 {
341 ogeom_bail!(Construction, "seeding needs at least two steps each way");
342 }
343 if !options.gap.is_finite() || options.gap <= 0.0 {
344 ogeom_bail!(Construction, "a gap of {} is not a distance", options.gap);
345 }
346
347 match (curve, surface) {
348 (Curve::Line(line), SurfaceGeometry::Plane(p)) => {
349 Ok(line_plane(line, p.plane(), curve, surface, tol))
350 }
351 (Curve::Line(line), SurfaceGeometry::Sphere(s)) => Ok(line_quadric(
352 line,
353 curve,
354 surface,
355 sphere_roots(line, s.sphere()),
356 options,
357 tol,
358 )),
359 (Curve::Line(line), SurfaceGeometry::Cylinder(c)) => Ok(line_quadric(
360 line,
361 curve,
362 surface,
363 cylinder_roots(line, c.cylinder()),
364 options,
365 tol,
366 )),
367 (Curve::Line(line), SurfaceGeometry::Cone(c)) => Ok(line_quadric(
368 line,
369 curve,
370 surface,
371 cone_roots(line, c.cone(), tol),
372 options,
373 tol,
374 )),
375 (Curve::Line(line), SurfaceGeometry::Torus(t)) => Ok(line_quadric(
376 line,
377 curve,
378 surface,
379 torus_roots(line, t.torus()),
380 options,
381 tol,
382 )),
383 _ => {
384 let (cells, tree) = cells();
385 general(curve, surface, &cells, &tree, options, tol)
386 }
387 }
388}
389
390fn line_plane(
393 line: &ogeom_geom::LineCurve,
394 plane: ogeom_math::Plane,
395 curve: &Curve,
396 surface: &SurfaceGeometry,
397 tol: Tolerances,
398) -> CurveSurfaceIntersection {
399 let axis = line.axis();
400 let along = plane.normal().dot(axis.direction);
401 let height = plane.signed_distance_to(axis.location);
402
403 if along.abs() <= tol.angular() {
404 if height.abs() <= tol.confusion() {
406 return CurveSurfaceIntersection {
407 crossings: Vec::new(),
408 lying: vec![line.domain()],
409 };
410 }
411 return CurveSurfaceIntersection::empty();
412 }
413
414 let t = -height / along;
415 let (lo, hi) = line.domain();
416 if t < lo - tol.parametric() || t > hi + tol.parametric() {
417 return CurveSurfaceIntersection::empty();
418 }
419 let point = axis.location + axis.direction.vector() * t;
420 let Some(found) = invert(surface, point, curve, t, tol) else {
421 return CurveSurfaceIntersection::empty();
422 };
423 if found.gap > tol.confusion() {
424 return CurveSurfaceIntersection::empty();
427 }
428 CurveSurfaceIntersection {
429 crossings: vec![found],
430 lying: Vec::new(),
431 }
432}
433
434fn rounding(p: f64, q: f64) -> f64 {
436 8.0 * f64::EPSILON * p.abs().max(q.abs())
437}
438
439fn sphere_roots(line: &ogeom_geom::LineCurve, sphere: ogeom_math::Sphere) -> Vec<f64> {
441 let axis = line.axis();
442 let d = axis.direction.vector();
443 let m = axis.location - sphere.centre();
444 let b = m.dot(d);
446 let c = sphere.radius().mul_add(-sphere.radius(), m.dot(m));
447 let discriminant = b.mul_add(b, -c);
448 if discriminant < -rounding(b * b, c) {
452 return Vec::new();
453 }
454 let root = discriminant.max(0.0).sqrt();
455 if root == 0.0 {
456 vec![-b]
457 } else {
458 vec![-b - root, -b + root]
459 }
460}
461
462fn in_frame(
466 line: &ogeom_geom::LineCurve,
467 frame: ogeom_math::Frame,
468) -> (ogeom_math::Vector, ogeom_math::Vector) {
469 let axis = line.axis();
470 let local = |v: ogeom_math::Vector| {
471 ogeom_math::Vector::new(
472 v.dot(frame.x().vector()),
473 v.dot(frame.y().vector()),
474 v.dot(frame.z().vector()),
475 )
476 };
477 (
478 local(axis.location - frame.origin()),
479 local(axis.direction.vector()),
480 )
481}
482
483fn cone_roots(
486 line: &ogeom_geom::LineCurve,
487 cone: ogeom_math::Cone,
488 tol: ogeom_core::Tolerances,
489) -> Vec<f64> {
490 let (m, d) = in_frame(line, cone.frame());
491 let (r0, k) = (cone.reference_radius(), cone.half_angle().tan());
493 let rim = k.mul_add(m.z, r0);
494 let a = d.x.mul_add(d.x, d.y * d.y) - k * k * d.z * d.z;
495 let b = 2.0 * (m.x.mul_add(d.x, m.y * d.y) - k * d.z * rim);
496 let c = m.x.mul_add(m.x, m.y * m.y) - rim * rim;
497 ogeom_math::solve::roots(&[c, b, a], tol.parametric()).unwrap_or_default()
498}
499
500fn torus_roots(line: &ogeom_geom::LineCurve, torus: ogeom_math::Torus) -> Vec<f64> {
504 let (m, d) = in_frame(line, torus.frame());
505 let (big, small) = (torus.major_radius(), torus.minor_radius());
506 let near = -m.dot(d) / d.dot(d);
511 let size = big + small;
512 let m = (m + d * near) / size;
513 let (big, small) = (big / size, small / size);
514 let a = d.dot(d);
515 let b = 2.0 * m.dot(d);
516 let c = m.dot(m) + big * big - small * small;
517 let p = d.x.mul_add(d.x, d.y * d.y);
518 let q = 2.0 * m.x.mul_add(d.x, m.y * d.y);
519 let s = m.x.mul_add(m.x, m.y * m.y);
520 let four = 4.0 * big * big;
521 let coefficients = [
522 c.mul_add(c, -four * s),
523 2.0f64.mul_add(b * c, -four * q),
524 b.mul_add(b, 2.0 * a * c) - four * p,
525 2.0 * a * b,
526 a * a,
527 ];
528 let (found, count) = quartic_roots(&coefficients);
529 found[..count]
530 .iter()
531 .map(|sigma| near + sigma * size)
532 .collect()
533}
534
535fn quartic_roots(c: &[f64; 5]) -> ([f64; 4], usize) {
541 let mut found = [0.0; 4];
542 let touch = 1e-9 * c.iter().map(|x| x.abs()).sum::<f64>();
543 let count = ogeom_math::solve::real_roots(c, touch, &mut found);
544 let mut merged = 0;
545 for i in 0..count {
546 let t = found[i];
547 if merged > 0 && (t - found[merged - 1]).abs() <= 1e-6 * (1.0 + t.abs()) {
548 found[merged - 1] = f64::midpoint(found[merged - 1], t);
549 } else {
550 found[merged] = t;
551 merged += 1;
552 }
553 }
554 (found, merged)
555}
556
557fn cylinder_roots(line: &ogeom_geom::LineCurve, cylinder: ogeom_math::Cylinder) -> Vec<f64> {
558 let axis = line.axis();
559 let w = cylinder.axis().direction.vector();
560 let d = axis.direction.vector();
563 let m = axis.location - cylinder.axis().location;
564 let d_perp = d - w * d.dot(w);
565 let m_perp = m - w * m.dot(w);
566 let a = d_perp.dot(d_perp);
567 if a <= f64::MIN_POSITIVE {
568 return Vec::new();
571 }
572 let b = d_perp.dot(m_perp);
573 let c = cylinder
574 .radius()
575 .mul_add(-cylinder.radius(), m_perp.dot(m_perp));
576 let discriminant = b.mul_add(b, -(a * c));
577 if discriminant < -rounding(b * b, a * c) {
578 return Vec::new();
579 }
580 let root = discriminant.max(0.0).sqrt();
581 if root == 0.0 {
582 vec![-b / a]
583 } else {
584 vec![(-b - root) / a, (-b + root) / a]
585 }
586}
587
588fn line_quadric(
591 line: &ogeom_geom::LineCurve,
592 curve: &Curve,
593 surface: &SurfaceGeometry,
594 roots: Vec<f64>,
595 options: CurveSurfaceOptions,
596 tol: Tolerances,
597) -> CurveSurfaceIntersection {
598 let axis = line.axis();
599 let (lo, hi) = line.domain();
600 let mut crossings = Vec::new();
601 for t in roots {
602 if t < lo - tol.parametric() || t > hi + tol.parametric() {
603 continue;
604 }
605 let point = axis.location + axis.direction.vector() * t;
606 let Some(found) = invert(surface, point, curve, t, tol) else {
607 continue;
608 };
609 if found.gap > tol.confusion() {
614 continue;
615 }
616 let _ = options;
617 crossings.push(Piercing {
618 on_curve: t,
619 on_surface: found.on_surface,
620 point,
621 gap: found.gap,
622 });
623 }
624 crossings.sort_by(|a, b| {
625 a.on_curve
626 .partial_cmp(&b.on_curve)
627 .unwrap_or(core::cmp::Ordering::Equal)
628 });
629 CurveSurfaceIntersection {
630 crossings,
631 lying: Vec::new(),
632 }
633}
634
635fn invert(
640 surface: &SurfaceGeometry,
641 point: Point,
642 curve: &Curve,
643 on_curve: f64,
644 tol: Tolerances,
645) -> Option<Piercing> {
646 let guess = match surface {
647 SurfaceGeometry::Plane(p) => {
648 let local = p.plane().frame().to_local(point);
649 (local.x, local.y)
650 }
651 SurfaceGeometry::Sphere(s) => {
652 let local = s.sphere().frame().to_local(point);
653 let latitude = (local.z / s.sphere().radius()).clamp(-1.0, 1.0).asin();
654 (
655 local.y.atan2(local.x).rem_euclid(core::f64::consts::TAU),
656 latitude,
657 )
658 }
659 SurfaceGeometry::Cylinder(c) => {
660 let local = c.cylinder().frame().to_local(point);
661 (
662 local.y.atan2(local.x).rem_euclid(core::f64::consts::TAU),
663 local.z,
664 )
665 }
666 SurfaceGeometry::Cone(c) => {
667 ogeom_math::elementary::cone_parameters(&c.cone(), point, tol).ok()?
668 }
669 SurfaceGeometry::Torus(t) => {
670 ogeom_math::elementary::torus_parameters(&t.torus(), point, tol).ok()?
671 }
672 _ => return None,
673 };
674 polish(curve, surface, on_curve, guess, tol)
679}
680
681fn general(
684 curve: &Curve,
685 surface: &SurfaceGeometry,
686 cells: &[Cell],
687 tree: &CellTree,
688 options: CurveSurfaceOptions,
689 tol: Tolerances,
690) -> OgeomResult<CurveSurfaceIntersection> {
691 let (lo, hi) = curve.domain();
692
693 let mut points = Vec::with_capacity(options.samples + 1);
694 for i in 0..=options.samples {
695 #[allow(clippy::cast_precision_loss)]
696 let t = lo + (hi - lo) * i as f64 / options.samples as f64;
697 if let Ok(p) = curve.point_at(t, tol) {
698 points.push((t, p));
699 }
700 }
701
702 let segments: Vec<((f64, Point), (f64, Point))> =
703 points.windows(2).map(|pair| (pair[0], pair[1])).collect();
704 let mut pairs: Vec<(usize, usize)> = Vec::new();
706 let meet = |i: usize, c: usize, pairs: &mut Vec<(usize, usize)>| {
707 let cell = &cells[c];
708 let ((_, p0), (_, p1)) = segments[i];
709 if segment_near_cell(p0, p1, cell, options.gap.max(cell.sag)) {
710 pairs.push((i, c));
711 }
712 };
713 let mut found: Vec<usize> = Vec::new();
716 for (i, &((_, p0), (_, p1))) in segments.iter().enumerate() {
717 let low = Point::new(p0.x.min(p1.x), p0.y.min(p1.y), p0.z.min(p1.z));
718 let high = Point::new(p0.x.max(p1.x), p0.y.max(p1.y), p0.z.max(p1.z));
719 found.clear();
720 tree.visit(low, high, &mut found);
721 for &c in &found {
722 meet(i, c, &mut pairs);
723 }
724 }
725 pairs.sort_unstable();
726 let mut crossings: Vec<Piercing> = Vec::new();
727 for (segment, k) in pairs {
728 let ((t0, p0), (t1, p1)) = segments[segment];
729 let cell = &cells[k];
730 {
731 if segment_meets_triangle(p0, p1, cell.corners).is_none()
737 && !(cell.sag > options.gap
738 && segment_near_cell(p0, p1, cell, cell.sag)
739 && segment_triangle_distance(p0, p1, cell.corners) <= cell.sag)
740 {
741 continue;
742 }
743 let (near_t, near_uv) = seed_in(cell, p0, p1, t0, t1);
749 let Some(found) = [(near_t, near_uv), (f64::midpoint(t0, t1), cell.at)]
750 .into_iter()
751 .filter_map(|(t, uv)| polish(curve, surface, t, uv, tol))
752 .find(|found| found.gap <= options.gap)
753 else {
754 continue;
755 };
756 let reach = tol.confusion() * 100.0;
757 if !crossings
758 .iter()
759 .any(|c| c.point.distance(found.point) <= reach)
760 {
761 crossings.push(found);
762 }
763 }
764 }
765 crossings.sort_by(|a, b| {
766 a.on_curve
767 .partial_cmp(&b.on_curve)
768 .unwrap_or(core::cmp::Ordering::Equal)
769 });
770 Ok(gathered(curve, surface, crossings, options, tol))
771}
772
773fn clipped(
776 curve: &Curve,
777 surface: &SurfaceGeometry,
778 patches: &crate::bezier_clip::Patches,
779 options: CurveSurfaceOptions,
780 tol: Tolerances,
781) -> OgeomResult<CurveSurfaceIntersection> {
782 let (lo, hi) = curve.domain();
783 let (a, b) = (curve.point_at(lo, tol)?, curve.point_at(hi, tol)?);
784 let reach = tol.confusion() * 100.0;
785 let mut crossings: Vec<Piercing> = Vec::new();
786 for (f, uv) in patches.seeds(a, b, options.gap) {
787 let Some(found) = polish(curve, surface, (hi - lo).mul_add(f, lo), uv, tol) else {
788 continue;
789 };
790 if found.gap > options.gap
791 || crossings
792 .iter()
793 .any(|c| c.point.distance(found.point) <= reach)
794 {
795 continue;
796 }
797 crossings.push(found);
798 }
799 crossings.sort_by(|a, b| {
800 a.on_curve
801 .partial_cmp(&b.on_curve)
802 .unwrap_or(core::cmp::Ordering::Equal)
803 });
804 Ok(gathered(curve, surface, crossings, options, tol))
805}
806
807fn gathered(
817 curve: &Curve,
818 surface: &SurfaceGeometry,
819 crossings: Vec<Piercing>,
820 options: CurveSurfaceOptions,
821 tol: Tolerances,
822) -> CurveSurfaceIntersection {
823 let stays_on = |x: &Piercing, y: &Piercing| -> bool {
824 (1..=3).all(|k| {
825 let t = x.on_curve + (y.on_curve - x.on_curve) * f64::from(k) / 4.0;
826 let Ok(p) = curve.point_at(t, tol) else {
827 return false;
828 };
829 [x.on_surface, y.on_surface].into_iter().any(|seed| {
832 crate::march::nearest_on(surface, seed, p, tol)
833 .is_some_and(|(_, q)| q.distance(p) <= options.gap)
834 })
835 })
836 };
837 let (lo, hi) = curve.domain();
838 #[allow(clippy::cast_precision_loss)]
839 let step = (hi - lo).abs() / options.samples.max(1) as f64;
840 let mut runs: Vec<Vec<Piercing>> = Vec::new();
841 for crossing in crossings {
842 match runs.last_mut() {
843 Some(run) if run.last().is_some_and(|last| stays_on(last, &crossing)) => {
844 run.push(crossing);
845 }
846 _ => runs.push(vec![crossing]),
847 }
848 }
849 let mut out = CurveSurfaceIntersection {
850 crossings: Vec::new(),
851 lying: Vec::new(),
852 };
853 for run in runs {
854 let (Some(first), Some(last)) = (run.first(), run.last()) else {
855 continue;
856 };
857 if run.len() > 1 && (last.on_curve - first.on_curve).abs() >= step {
858 out.lying.push((first.on_curve, last.on_curve));
859 } else if let Some(best) = run.iter().min_by(|a, b| {
860 a.gap
861 .partial_cmp(&b.gap)
862 .unwrap_or(core::cmp::Ordering::Equal)
863 }) {
864 out.crossings.push(*best);
865 }
866 }
867 out
868}
869
870fn seeding(surface: &SurfaceGeometry, grid: usize) -> (usize, usize) {
877 const CAP: usize = 1024;
878 let SurfaceGeometry::BSpline(spline) = surface else {
879 return (grid, grid);
880 };
881 let spans = |knots: &ogeom_math::KnotVector| knots.distinct().len().saturating_sub(1);
882 (
883 grid.max(2 * spans(spline.u_knots())).min(CAP.max(grid)),
884 grid.max(2 * spans(spline.v_knots())).min(CAP.max(grid)),
885 )
886}
887
888fn seed_in(cell: &Cell, p0: Point, p1: Point, t0: f64, t1: f64) -> (f64, (f64, f64)) {
893 let [a, b, c] = cell.corners;
894 let (t, at) = segment_meets_triangle(p0, p1, cell.corners).map_or_else(
895 || (f64::midpoint(t0, t1), p0.midpoint(p1)),
896 |x| {
897 let length = p0.distance(p1);
898 let f = if length > 0.0 {
899 p0.distance(x) / length
900 } else {
901 0.5
902 };
903 (t0 + (t1 - t0) * f, x)
904 },
905 );
906 let (e1, e2, d) = (b - a, c - a, at - a);
909 let (d11, d12, d22) = (e1.dot(e1), e1.dot(e2), e2.dot(e2));
910 let (d1, d2) = (d.dot(e1), d.dot(e2));
911 let det = d11 * d22 - d12 * d12;
912 let (mut wb, mut wc) = if det > 0.0 {
913 ((d22 * d1 - d12 * d2) / det, (d11 * d2 - d12 * d1) / det)
914 } else {
915 (1.0 / 3.0, 1.0 / 3.0)
916 };
917 wb = wb.clamp(0.0, 1.0);
918 wc = wc.clamp(0.0, 1.0);
919 if wb + wc > 1.0 {
920 let sum = wb + wc;
921 wb /= sum;
922 wc /= sum;
923 }
924 let wa = 1.0 - wb - wc;
925 let [pa, pb, pc] = cell.params;
926 (
927 t,
928 (
929 wa * pa.0 + wb * pb.0 + wc * pc.0,
930 wa * pa.1 + wb * pb.1 + wc * pc.1,
931 ),
932 )
933}
934
935fn segment_triangle_distance(p: Point, q: Point, corners: [Point; 3]) -> f64 {
939 if segment_meets_triangle(p, q, corners).is_some() {
940 return 0.0;
941 }
942 let [a, b, c] = corners;
943 let mut least = point_triangle_distance(p, corners).min(point_triangle_distance(q, corners));
944 for (x, y) in [(a, b), (b, c), (c, a)] {
945 least = least.min(segment_segment_distance(p, q, x, y));
946 }
947 least
948}
949
950fn point_triangle_distance(p: Point, [a, b, c]: [Point; 3]) -> f64 {
952 let normal = (b - a).cross(c - a);
953 let area = normal.magnitude();
954 if area > 0.0 {
955 let n = normal / area;
956 let foot = p - n * (p - a).dot(n);
957 let inside = [(a, b), (b, c), (c, a)]
958 .iter()
959 .all(|&(x, y)| (y - x).cross(foot - x).dot(n) >= 0.0);
960 if inside {
961 return (p - a).dot(n).abs();
962 }
963 }
964 distance_to_segment(p, a, b)
965 .min(distance_to_segment(p, b, c))
966 .min(distance_to_segment(p, c, a))
967}
968
969fn segment_segment_distance(p: Point, q: Point, r: Point, s: Point) -> f64 {
971 let (d1, d2, w) = (q - p, s - r, p - r);
972 let (a, e, f) = (d1.dot(d1), d2.dot(d2), d2.dot(w));
973 let (b, c) = (d1.dot(d2), d1.dot(w));
974 let denominator = a * e - b * b;
975 let mut t = if denominator > 1e-300 {
976 ((b * f - c * e) / denominator).clamp(0.0, 1.0)
977 } else {
978 0.0
979 };
980 let mut u = if e > 0.0 { (b * t + f) / e } else { 0.0 };
981 if u < 0.0 {
982 u = 0.0;
983 t = if a > 0.0 {
984 (-c / a).clamp(0.0, 1.0)
985 } else {
986 0.0
987 };
988 } else if u > 1.0 {
989 u = 1.0;
990 t = if a > 0.0 {
991 ((b - c) / a).clamp(0.0, 1.0)
992 } else {
993 0.0
994 };
995 }
996 (p + d1 * t).distance(r + d2 * u)
997}
998
999fn distance_to_segment(p: Point, a: Point, b: Point) -> f64 {
1001 let ab = b - a;
1002 let length = ab.dot(ab);
1003 let f = if length > 0.0 {
1004 ((p - a).dot(ab) / length).clamp(0.0, 1.0)
1005 } else {
1006 0.0
1007 };
1008 (a + ab * f).distance(p)
1009}
1010
1011#[derive(Debug, Clone)]
1016pub(crate) struct CellTree {
1017 nodes: Vec<TreeNode>,
1018 order: Vec<usize>,
1019}
1020
1021#[derive(Debug, Clone)]
1022struct TreeNode {
1023 low: Point,
1024 high: Point,
1025 span: (usize, usize),
1027 leaf: bool,
1028}
1029
1030impl CellTree {
1031 const LEAF: usize = 8;
1033
1034 pub(crate) fn over(cells: &[Cell], gap: f64) -> Self {
1035 let boxes: Vec<(Point, Point)> = cells
1036 .iter()
1037 .map(|c| {
1038 let m = gap.max(c.sag);
1039 (
1040 Point::new(c.low.x - m, c.low.y - m, c.low.z - m),
1041 Point::new(c.high.x + m, c.high.y + m, c.high.z + m),
1042 )
1043 })
1044 .collect();
1045 let mut tree = Self {
1046 nodes: Vec::new(),
1047 order: (0..cells.len()).collect(),
1048 };
1049 if !cells.is_empty() {
1050 tree.build(&boxes, 0, cells.len());
1051 }
1052 tree
1053 }
1054
1055 fn build(&mut self, boxes: &[(Point, Point)], from: usize, to: usize) -> usize {
1056 let (mut low, mut high) = boxes[self.order[from]];
1057 for &i in &self.order[from..to] {
1058 let (l, h) = boxes[i];
1059 low = Point::new(low.x.min(l.x), low.y.min(l.y), low.z.min(l.z));
1060 high = Point::new(high.x.max(h.x), high.y.max(h.y), high.z.max(h.z));
1061 }
1062 let at = self.nodes.len();
1063 self.nodes.push(TreeNode {
1064 low,
1065 high,
1066 span: (from, to),
1067 leaf: true,
1068 });
1069 if to - from <= Self::LEAF {
1070 return at;
1071 }
1072 let extent = high - low;
1074 let axis = if extent.x >= extent.y && extent.x >= extent.z {
1075 0
1076 } else if extent.y >= extent.z {
1077 1
1078 } else {
1079 2
1080 };
1081 let key = |i: usize| {
1082 let (l, h) = boxes[i];
1083 match axis {
1084 0 => l.x + h.x,
1085 1 => l.y + h.y,
1086 _ => l.z + h.z,
1087 }
1088 };
1089 let middle = from + (to - from) / 2;
1090 self.order[from..to]
1091 .select_nth_unstable_by(middle - from, |&a, &b| key(a).total_cmp(&key(b)));
1092 let left = self.build(boxes, from, middle);
1093 let right = self.build(boxes, middle, to);
1094 self.nodes[at].span = (left, right);
1095 self.nodes[at].leaf = false;
1096 at
1097 }
1098
1099 pub(crate) fn visit(&self, low: Point, high: Point, out: &mut Vec<usize>) {
1101 if self.nodes.is_empty() {
1102 return;
1103 }
1104 let mut stack = vec![0_usize];
1105 while let Some(n) = stack.pop() {
1106 let node = &self.nodes[n];
1107 if node.low.x > high.x
1108 || low.x > node.high.x
1109 || node.low.y > high.y
1110 || low.y > node.high.y
1111 || node.low.z > high.z
1112 || low.z > node.high.z
1113 {
1114 continue;
1115 }
1116 if node.leaf {
1117 out.extend_from_slice(&self.order[node.span.0..node.span.1]);
1118 } else {
1119 stack.push(node.span.0);
1120 stack.push(node.span.1);
1121 }
1122 }
1123 }
1124}
1125
1126fn segment_near_cell(a: Point, b: Point, cell: &Cell, margin: f64) -> bool {
1128 let low = Point::new(a.x.min(b.x), a.y.min(b.y), a.z.min(b.z));
1129 let high = Point::new(a.x.max(b.x), a.y.max(b.y), a.z.max(b.z));
1130 low.x <= cell.high.x + margin
1131 && cell.low.x <= high.x + margin
1132 && low.y <= cell.high.y + margin
1133 && cell.low.y <= high.y + margin
1134 && low.z <= cell.high.z + margin
1135 && cell.low.z <= high.z + margin
1136}
1137
1138fn polish(
1140 curve: &Curve,
1141 surface: &SurfaceGeometry,
1142 seed_t: f64,
1143 seed_uv: (f64, f64),
1144 tol: Tolerances,
1145) -> Option<Piercing> {
1146 let clamp_t = |t: f64| {
1147 let (lo, hi) = curve.domain();
1148 if curve.is_periodic() {
1149 let span = hi - lo;
1150 if span > 0.0 {
1151 return lo + (t - lo).rem_euclid(span);
1152 }
1153 }
1154 t.clamp(lo, hi)
1155 };
1156 let clamp_uv = |u: f64, v: f64| {
1157 let ((ua, ub), (va, vb)) = surface.domain();
1158 let fold = |x: f64, lo: f64, hi: f64, periodic: bool| {
1159 if periodic {
1160 let span = hi - lo;
1161 if span > 0.0 {
1162 return lo + (x - lo).rem_euclid(span);
1163 }
1164 }
1165 x.clamp(lo, hi)
1166 };
1167 (
1168 fold(u, ua, ub, surface.is_periodic_u()),
1169 fold(v, va, vb, surface.is_periodic_v()),
1170 )
1171 };
1172
1173 let gap_at = |x: &[f64; 3]| {
1177 let t = clamp_t(x[0]);
1178 let (u, v) = clamp_uv(x[1], x[2]);
1179 let (Ok(pc), Ok(ps)) = (curve.point_at(t, tol), surface.point_at(u, v, tol)) else {
1180 return [f64::INFINITY; 3];
1182 };
1183 let gap = pc - ps;
1184 [gap.x, gap.y, gap.z]
1185 };
1186 let jacobian_at = |x: &[f64; 3]| {
1187 let t = clamp_t(x[0]);
1188 let (u, v) = clamp_uv(x[1], x[2]);
1189 let (Ok(dc), Ok((du, dv))) = (curve.d1_at(t, tol), surface.d1_at(u, v, tol)) else {
1190 return None;
1191 };
1192 Some([
1193 [dc.x, -du.x, -dv.x],
1194 [dc.y, -du.y, -dv.y],
1195 [dc.z, -du.z, -dv.z],
1196 ])
1197 };
1198 let criteria = solve::Criteria {
1199 residual: tol.confusion() * 0.01,
1200 step: tol.parametric(),
1201 max_iterations: 40,
1202 };
1203 let found = solve::newton_system_fixed_lazy(
1204 gap_at,
1205 jacobian_at,
1206 [seed_t, seed_uv.0, seed_uv.1],
1207 criteria,
1208 )
1209 .ok()?;
1210 let t = clamp_t(found.0[0]);
1211 let (u, v) = clamp_uv(found.0[1], found.0[2]);
1212 let pc = curve.point_at(t, tol).ok()?;
1213 let ps = surface.point_at(u, v, tol).ok()?;
1214 Some(Piercing {
1215 on_curve: t,
1216 on_surface: (u, v),
1217 point: pc,
1218 gap: pc.distance(ps),
1219 })
1220}
1221
1222#[cfg(test)]
1223#[allow(clippy::unwrap_used)]
1224mod tests {
1225 use super::*;
1226 use ogeom_geom::{
1227 BSplineCurve, CircleCurve, CylinderSurface, LineCurve, PlaneSurface, SphereSurface,
1228 };
1229 use ogeom_math::{Circle, Cylinder, Direction, Frame, KnotVector, Plane, Sphere, Vector};
1230
1231 const T: Tolerances = Tolerances::millimetres();
1232
1233 fn sphere(radius: f64) -> SurfaceGeometry {
1234 SphereSurface::new(Sphere::centred(Point::ORIGIN, radius, T).unwrap()).into()
1235 }
1236
1237 fn cylinder(radius: f64, height: (f64, f64)) -> SurfaceGeometry {
1238 CylinderSurface::new(Cylinder::new(Frame::WORLD, radius, T).unwrap(), height)
1239 .unwrap()
1240 .into()
1241 }
1242
1243 fn plane(origin: Point, normal: Vector) -> SurfaceGeometry {
1244 PlaneSurface::over(
1245 Plane::through(origin, Direction::new(normal, T).unwrap()),
1246 (-6.0, 6.0),
1247 (-6.0, 6.0),
1248 )
1249 .unwrap()
1250 .into()
1251 }
1252
1253 fn segment(from: Point, to: Point) -> Curve {
1254 LineCurve::segment(from, to, T).unwrap().into()
1255 }
1256
1257 #[test]
1258 fn a_line_through_a_sphere_pierces_it_where_the_quadratic_says() {
1259 let ball = sphere(2.0);
1260 let ray = segment(Point::new(-5.0, 0.0, 0.0), Point::new(5.0, 0.0, 0.0));
1261 let found =
1262 intersect_curve_surface(&ray, &ball, CurveSurfaceOptions::default(), T).unwrap();
1263 assert_eq!(found.crossings.len(), 2);
1264 assert!(
1265 found.crossings[0]
1266 .point
1267 .is_equal(Point::new(-2.0, 0.0, 0.0), T)
1268 );
1269 assert!(
1270 found.crossings[1]
1271 .point
1272 .is_equal(Point::new(2.0, 0.0, 0.0), T)
1273 );
1274 for hit in &found.crossings {
1275 assert!(hit.gap < 1e-12);
1276 let lifted = ball
1278 .point_at(hit.on_surface.0, hit.on_surface.1, T)
1279 .unwrap();
1280 assert!(lifted.is_equal(hit.point, T));
1281 }
1282
1283 let grazing = segment(Point::new(-5.0, 0.0, 2.0), Point::new(5.0, 0.0, 2.0));
1285 assert_eq!(
1286 intersect_curve_surface(&grazing, &ball, CurveSurfaceOptions::default(), T)
1287 .unwrap()
1288 .crossings
1289 .len(),
1290 1
1291 );
1292 let missing = segment(Point::new(-5.0, 0.0, 3.0), Point::new(5.0, 0.0, 3.0));
1293 assert!(
1294 intersect_curve_surface(&missing, &ball, CurveSurfaceOptions::default(), T)
1295 .unwrap()
1296 .is_empty()
1297 );
1298 }
1299
1300 #[test]
1301 fn a_line_through_a_cylinder_respects_its_height() {
1302 let drum = cylinder(2.0, (-1.0, 1.0));
1303 let level = segment(Point::new(-5.0, 0.0, 0.0), Point::new(5.0, 0.0, 0.0));
1305 assert_eq!(
1306 intersect_curve_surface(&level, &drum, CurveSurfaceOptions::default(), T)
1307 .unwrap()
1308 .crossings
1309 .len(),
1310 2
1311 );
1312 let high = segment(Point::new(-5.0, 0.0, 3.0), Point::new(5.0, 0.0, 3.0));
1315 assert!(
1316 intersect_curve_surface(&high, &drum, CurveSurfaceOptions::default(), T)
1317 .unwrap()
1318 .is_empty()
1319 );
1320 }
1321
1322 #[test]
1323 fn a_line_lying_in_a_plane_is_an_overlap_not_a_crossing_list() {
1324 let ground = plane(Point::ORIGIN, Vector::Z);
1325 let lying = segment(Point::new(-3.0, 1.0, 0.0), Point::new(3.0, 1.0, 0.0));
1326 let found =
1327 intersect_curve_surface(&lying, &ground, CurveSurfaceOptions::default(), T).unwrap();
1328 assert!(found.crossings.is_empty());
1329 assert_eq!(found.lying.len(), 1);
1330
1331 let crossing = segment(Point::new(0.0, 0.0, -1.0), Point::new(0.0, 0.0, 1.0));
1332 let found =
1333 intersect_curve_surface(&crossing, &ground, CurveSurfaceOptions::default(), T).unwrap();
1334 assert_eq!(found.crossings.len(), 1);
1335 assert!(found.crossings[0].point.is_equal(Point::ORIGIN, T));
1336
1337 let parallel = segment(Point::new(-3.0, 0.0, 1.0), Point::new(3.0, 0.0, 1.0));
1338 assert!(
1339 intersect_curve_surface(¶llel, &ground, CurveSurfaceOptions::default(), T)
1340 .unwrap()
1341 .is_empty()
1342 );
1343 }
1344
1345 #[test]
1346 fn a_circle_pierces_a_plane_twice_through_the_general_path() {
1347 let ring: Curve = CircleCurve::new(
1351 Circle::new(
1352 Frame::new(Point::new(0.0, 0.0, 0.0), -Direction::Y, Direction::X, T).unwrap(),
1353 2.0,
1354 T,
1355 )
1356 .unwrap(),
1357 )
1358 .into();
1359 let ground = plane(Point::ORIGIN, Vector::Z);
1360 let found =
1361 intersect_curve_surface(&ring, &ground, CurveSurfaceOptions::default(), T).unwrap();
1362 assert_eq!(found.crossings.len(), 2);
1363 for hit in &found.crossings {
1364 assert!(hit.gap < 1e-9);
1365 assert!(hit.point.z.abs() < 1e-9);
1366 assert!((hit.point.to_vector().magnitude() - 2.0).abs() < 1e-9);
1367 }
1368 }
1369
1370 #[test]
1371 fn a_prepared_surface_answers_as_the_one_shot_query() {
1372 use ogeom_geom::BSplineSurface;
1376 use ogeom_math::ControlGrid;
1377 let mut points = Vec::new();
1378 for i in 0..7 {
1379 let a = core::f64::consts::PI * f64::from(i) / 6.0;
1380 for j in 0..2 {
1381 points.push(Point::new(2.0 * a.cos(), 3.0 * f64::from(j), 2.0 * a.sin()));
1382 }
1383 }
1384 let strip: SurfaceGeometry = BSplineSurface::new(
1385 KnotVector::clamped_uniform(3, 7).unwrap(),
1386 KnotVector::clamped_uniform(1, 2).unwrap(),
1387 &ControlGrid::new(points, 7, 2).unwrap(),
1388 T,
1389 )
1390 .unwrap()
1391 .into();
1392 let options = CurveSurfaceOptions::default();
1393 let prepared = PreparedSurface::new(strip.clone(), options, T);
1394 let mut pierced = 0;
1395 for k in 0..12 {
1396 let x = -2.5 + 5.0 * f64::from(k) / 11.0;
1397 let lines: [Curve; 2] = [
1398 LineCurve::new(ogeom_math::Axis::new(
1399 Point::new(x, 1.5, 0.0),
1400 Direction::new(Vector::new(0.1, 0.2, 1.0), T).unwrap(),
1401 ))
1402 .into(),
1403 segment(Point::new(x, -1.0, -1.0), Point::new(-x, 4.0, 3.0)),
1404 ];
1405 for line in &lines {
1406 let once = intersect_curve_surface(line, &strip, options, T).unwrap();
1407 pierced += once.crossings.len();
1408 assert_eq!(prepared.intersect(line).unwrap(), once, "line {k}");
1409 }
1410 }
1411 assert!(pierced > 0, "some lines pass through the strip");
1412 }
1413
1414 #[test]
1415 fn a_spline_through_a_sphere_is_found_and_polished() {
1416 let wander: Curve = BSplineCurve::new(
1420 KnotVector::new(vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], 3).unwrap(),
1421 vec![
1422 Point::new(-4.0, -1.0, -1.0),
1423 Point::new(-1.0, 2.0, 1.0),
1424 Point::new(1.0, -2.0, -1.0),
1425 Point::new(4.0, 1.0, 1.0),
1426 ],
1427 T,
1428 )
1429 .unwrap()
1430 .into();
1431 let ball = sphere(2.0);
1432 let found =
1433 intersect_curve_surface(&wander, &ball, CurveSurfaceOptions::default(), T).unwrap();
1434 assert!(!found.crossings.is_empty(), "the spline passes through");
1435 for hit in &found.crossings {
1436 assert!(hit.gap < 1e-9);
1437 let SurfaceGeometry::Sphere(s) = &ball else {
1438 unreachable!()
1439 };
1440 assert!(s.sphere().distance_to(hit.point).abs() < 1e-9);
1441 }
1442 }
1443
1444 #[test]
1445 fn unusable_options_are_refused() {
1446 let ball = sphere(1.0);
1447 let ray = segment(Point::new(-5.0, 0.0, 0.0), Point::new(5.0, 0.0, 0.0));
1448 for options in [
1449 CurveSurfaceOptions {
1450 samples: 1,
1451 ..CurveSurfaceOptions::default()
1452 },
1453 CurveSurfaceOptions {
1454 grid: 1,
1455 ..CurveSurfaceOptions::default()
1456 },
1457 CurveSurfaceOptions {
1458 gap: 0.0,
1459 ..CurveSurfaceOptions::default()
1460 },
1461 ] {
1462 assert!(intersect_curve_surface(&ray, &ball, options, T).is_err());
1463 }
1464 }
1465
1466 #[test]
1473 fn a_torus_far_down_a_line_is_still_met() {
1474 use ogeom_geom::TorusSurface;
1475 use ogeom_math::Torus;
1476 let options = CurveSurfaceOptions::default();
1477 let tilted = Frame::new(
1478 Point::new(0.0, 785.0, -140.0),
1479 Direction::new(Vector::new(0.0, -0.999_390_827, 0.034_899_497), T).unwrap(),
1480 Direction::X,
1481 T,
1482 )
1483 .unwrap();
1484 let torus: SurfaceGeometry =
1485 TorusSurface::new(Torus::new(tilted, 120.0, 12.0, T).unwrap()).into();
1486 let w = Vector::new(1.0, 1.0, 1.0) / 3.0_f64.sqrt();
1487 let from = Point::new(-160.0, 531.0, -320.0);
1488 let line = segment(from, from + w * 800.0);
1489 let found = intersect_curve_surface(&line, &torus, options, T).unwrap();
1490 let cells = sample_by(&torus, seeding(&torus, options.grid), T);
1491 let tree = CellTree::over(&cells, options.gap);
1492 let general = general(&line, &torus, &cells, &tree, options, T).unwrap();
1493 assert!(!general.crossings.is_empty());
1494 assert_eq!(found.crossings.len(), general.crossings.len());
1495 for (a, b) in found.crossings.iter().zip(&general.crossings) {
1496 assert!(
1497 a.point.distance(b.point) < 1e-6,
1498 "{:?} against {:?}",
1499 a.point,
1500 b.point
1501 );
1502 }
1503 }
1504
1505 #[test]
1509 fn lines_cross_a_torus_and_a_cone_where_the_closed_forms_say() {
1510 use ogeom_geom::{ConeSurface, TorusSurface};
1511 use ogeom_math::{Cone, Torus};
1512 let options = CurveSurfaceOptions::default();
1513 let torus: SurfaceGeometry =
1514 TorusSurface::new(Torus::new(Frame::WORLD, 60.0, 20.0, T).unwrap()).into();
1515 let across = segment(Point::new(-100.0, 0.0, 0.0), Point::new(100.0, 0.0, 0.0));
1516 let found = intersect_curve_surface(&across, &torus, options, T).unwrap();
1517 let xs: Vec<f64> = found.crossings.iter().map(|c| c.point.x).collect();
1518 assert_eq!(xs.len(), 4, "{xs:?}");
1519 for (got, want) in xs.iter().zip([-80.0, -40.0, 40.0, 80.0]) {
1520 assert!((got - want).abs() < 1e-9, "{xs:?}");
1521 }
1522 let cone: SurfaceGeometry = ConeSurface::new(
1523 Cone::new(Frame::WORLD, 10.0, core::f64::consts::FRAC_PI_4, T).unwrap(),
1524 (0.0, 20.0),
1525 )
1526 .unwrap()
1527 .into();
1528 let level = segment(Point::new(-50.0, 0.0, 5.0), Point::new(50.0, 0.0, 5.0));
1529 let found = intersect_curve_surface(&level, &cone, options, T).unwrap();
1530 let xs: Vec<f64> = found.crossings.iter().map(|c| c.point.x).collect();
1531 assert_eq!(xs.len(), 2, "{xs:?}");
1532 for (got, want) in xs.iter().zip([-15.0, 15.0]) {
1533 assert!((got - want).abs() < 1e-9, "{xs:?}");
1534 }
1535 }
1536
1537 #[test]
1541 fn a_line_anchored_far_away_still_crosses_a_cone() {
1542 use ogeom_geom::ConeSurface;
1543 use ogeom_math::Cone;
1544 let options = CurveSurfaceOptions::default();
1545 let cone: SurfaceGeometry = ConeSurface::new(
1546 Cone::new(Frame::WORLD, 10.0, core::f64::consts::FRAC_PI_4, T).unwrap(),
1547 (0.0, 20.0),
1548 )
1549 .unwrap()
1550 .into();
1551 let far = segment(Point::new(-1e5, 0.0, 5.0), Point::new(50.0, 0.0, 5.0));
1552 let found = intersect_curve_surface(&far, &cone, options, T).unwrap();
1553 let xs: Vec<f64> = found.crossings.iter().map(|c| c.point.x).collect();
1554 assert_eq!(xs.len(), 2, "{xs:?}");
1555 for (got, want) in xs.iter().zip([-15.0, 15.0]) {
1556 assert!((got - want).abs() < 1e-6, "{xs:?}");
1557 }
1558 }
1559
1560 #[test]
1564 fn lines_through_the_hole_along_the_axis_and_grazing_a_torus() {
1565 use ogeom_geom::TorusSurface;
1566 use ogeom_math::Torus;
1567 let options = CurveSurfaceOptions::default();
1568 let tilted = Frame::new(
1569 Point::new(3.0, -2.0, 1.0),
1570 Direction::new(Vector::new(0.1, 0.2, 1.0), T).unwrap(),
1571 Direction::new(Vector::new(1.0, 0.0, -0.1), T).unwrap(),
1572 T,
1573 )
1574 .unwrap();
1575 let torus: SurfaceGeometry =
1576 TorusSurface::new(Torus::new(tilted, 60.0, 20.0, T).unwrap()).into();
1577 let local = |x: f64, y: f64, z: f64| tilted.to_world(Point::new(x, y, z));
1578 let meets = |from: Point, to: Point| {
1579 intersect_curve_surface(&segment(from, to), &torus, options, T)
1580 .unwrap()
1581 .crossings
1582 };
1583 assert!(meets(local(0.0, 0.0, -100.0), local(0.0, 0.0, 100.0)).is_empty());
1584 assert!(meets(local(5.0, 3.0, -100.0), local(-15.0, 3.0, 100.0)).is_empty());
1585 let grazing = meets(local(-100.0, 80.0, 0.0), local(100.0, 80.0, 0.0));
1586 assert_eq!(grazing.len(), 1, "{grazing:?}");
1587 assert!(grazing[0].point.distance(local(0.0, 80.0, 0.0)) < 1e-6);
1588 let ends = meets(local(-80.0, 0.0, 0.0), local(80.0, 0.0, 0.0));
1589 let xs: Vec<f64> = ends.iter().map(|c| tilted.to_local(c.point).x).collect();
1590 assert_eq!(xs.len(), 4, "{xs:?}");
1591 for (got, want) in xs.iter().zip([-80.0, -40.0, 40.0, 80.0]) {
1592 assert!((got - want).abs() < 1e-9, "{xs:?}");
1593 }
1594 }
1595
1596 #[test]
1597 fn tangencies_come_back_once_and_lying_curves_as_stretches() {
1598 use ogeom_geom::TorusSurface;
1599 use ogeom_math::Torus;
1600 let torus: SurfaceGeometry =
1601 TorusSurface::new(Torus::new(Frame::WORLD, 60.0, 20.0, T).unwrap()).into();
1602 let options = CurveSurfaceOptions::default();
1603 let top = segment(Point::new(-100.0, 0.0, 20.0), Point::new(100.0, 0.0, 20.0));
1605 let found = intersect_curve_surface(&top, &torus, options, T).unwrap();
1606 assert_eq!(found.crossings.len(), 2, "{:?}", found.crossings);
1607 assert!(found.lying.is_empty());
1608 let inner = segment(Point::new(-100.0, 40.0, 0.0), Point::new(100.0, 40.0, 0.0));
1610 let found = intersect_curve_surface(&inner, &torus, options, T).unwrap();
1611 assert_eq!(found.crossings.len(), 3, "{:?}", found.crossings);
1612 let parallel: Curve = CircleCurve::new(
1614 Circle::new(
1615 Frame::new(Point::new(0.0, 0.0, 20.0), Direction::Z, Direction::X, T).unwrap(),
1616 60.0,
1617 T,
1618 )
1619 .unwrap(),
1620 )
1621 .into();
1622 let found = intersect_curve_surface(¶llel, &torus, options, T).unwrap();
1623 assert!(found.crossings.is_empty(), "{:?}", found.crossings);
1624 assert_eq!(found.lying.len(), 1);
1625 let (from, to) = found.lying[0];
1626 assert!((to - from).abs() > 6.0, "{from} to {to}");
1627 }
1628
1629 #[test]
1639 fn an_edge_tangent_to_a_band_touches_it_where_the_band_meets_its_face() {
1640 use ogeom_geom::TorusSurface;
1641 use ogeom_math::Torus;
1642 let band: SurfaceGeometry = TorusSurface::new(
1643 Torus::new(
1644 Frame::new(Point::new(4.0, 4.0, 1.5), Direction::Z, -Direction::X, T).unwrap(),
1645 2.0,
1646 0.5,
1647 T,
1648 )
1649 .unwrap(),
1650 )
1651 .into();
1652 let lean = 0.1_f64;
1653 let wall = plane(Point::new(4.0, 4.0, 2.0), Vector::new(0.0, 1.0, -lean));
1654 let drum = CylinderSurface::new(
1655 Cylinder::new(
1656 Frame::new(Point::new(4.0, 4.0, 0.0), Direction::Z, Direction::X, T).unwrap(),
1657 2.5,
1658 T,
1659 )
1660 .unwrap(),
1661 (0.0, 2.0),
1662 )
1663 .unwrap()
1664 .into();
1665 let crate::section::SurfaceIntersection::Along(seams) = crate::section::intersect_surfaces(
1666 &wall,
1667 &drum,
1668 crate::section::IntersectOptions::default(),
1669 T,
1670 )
1671 .unwrap() else {
1672 panic!("the wall meets the drum");
1673 };
1674 let foot = Point::new(4.0 + 2.5_f64.mul_add(2.5, -0.05 * 0.05).sqrt(), 3.95, 1.5);
1675 let nearest = |c: &Curve, p: Point| {
1677 let (lo, hi) = c.domain();
1678 (0..=20_000)
1679 .map(|k| lo + (hi - lo) * f64::from(k) / 20_000.0)
1680 .map(|t| (t, c.point_at(t, T).unwrap().distance(p)))
1681 .min_by(|a, b| a.1.total_cmp(&b.1))
1682 .unwrap()
1683 };
1684 let seam = seams
1685 .iter()
1686 .map(|s| &s.curve)
1687 .find(|c| nearest(c, foot).1 < 1e-2)
1688 .unwrap();
1689 let top = segment(Point::new(1.5, 4.0, 2.0), Point::new(6.5, 4.0, 2.0));
1690 for (edge, want) in [(&top, Point::new(6.0, 4.0, 2.0)), (seam, foot)] {
1691 let at = nearest(edge, want).0;
1692 let step = 1e-3 / edge.d1_at(at, T).unwrap().magnitude();
1693 for seed in [at - step, at + step] {
1694 let touch = touch_curve_surface(edge, &band, seed, T).unwrap();
1695 assert!(
1696 touch.point.distance(want) < 1e-9 && touch.gap < 1e-9,
1697 "{touch:?} against {want:?}"
1698 );
1699 }
1700 }
1701 let across = segment(Point::new(6.2, 4.0, 3.0), Point::new(6.2, 4.0, 0.0));
1702 let found =
1703 intersect_curve_surface(&across, &band, CurveSurfaceOptions::default(), T).unwrap();
1704 assert_eq!(found.crossings.len(), 2, "{:?}", found.crossings);
1705 for crossing in found.crossings {
1706 assert!(touch_curve_surface(&across, &band, crossing.on_curve, T).is_none());
1707 }
1708 }
1709}