1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
32use ogeom_geom::{Curve, Curve3d, Surface, SurfaceGeometry};
33use ogeom_math::{Point, Vector, solve};
34
35#[derive(Debug, Clone, Copy, PartialEq)]
37pub struct Approach<A, B> {
38 pub on_a: A,
40 pub on_b: B,
42 pub point_a: Point,
44 pub point_b: Point,
46 pub distance: f64,
48}
49
50#[derive(Debug, Clone, PartialEq)]
52pub struct Extrema<A, B> {
53 pub approaches: Vec<Approach<A, B>>,
57 pub family: bool,
62}
63
64impl<A, B> Extrema<A, B> {
65 #[must_use]
67 pub fn nearest(&self) -> Option<&Approach<A, B>> {
68 self.approaches.first()
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq)]
74pub struct ExtremaOptions {
75 pub samples: usize,
77 pub grid: usize,
79}
80
81impl Default for ExtremaOptions {
82 fn default() -> Self {
83 Self {
84 samples: 64,
85 grid: 24,
86 }
87 }
88}
89
90const WIDEST_DOMAIN: f64 = 1e8;
98
99const DISTINCT: f64 = 1e2;
101
102const MOST_SEEDS: usize = 256;
108
109pub fn extrema_curve_curve(
123 a: &Curve,
124 b: &Curve,
125 options: ExtremaOptions,
126 tol: Tolerances,
127) -> OgeomResult<Extrema<f64, f64>> {
128 if options.samples < 2 {
129 ogeom_bail!(Construction, "seeding needs at least two samples");
130 }
131 if let (Curve::Line(la), Curve::Line(lb)) = (a, b) {
132 return Ok(line_line(la, lb, tol));
133 }
134 for (name, curve) in [("first", a), ("second", b)] {
135 let (lo, hi) = curve.domain();
136 if hi - lo > WIDEST_DOMAIN {
137 ogeom_bail!(
138 Domain,
139 "the {name} curve's domain spans {:.0e}; trim it before asking",
140 hi - lo
141 );
142 }
143 }
144
145 let sa = sample_curve(a, options.samples, tol);
146 let sb = sample_curve(b, options.samples, tol);
147 if sa.len() < 2 || sb.len() < 2 {
148 ogeom_bail!(Construction, "a curve failed to evaluate over its domain");
149 }
150
151 let mut seeds = Vec::new();
155 for i in 0..sa.len() {
156 for j in 0..sb.len() {
157 let here = sa[i].1.square_distance(sb[j].1);
158 let mut minimal = true;
159 let mut maximal = true;
160 let neighbours_a = sa
161 .iter()
162 .enumerate()
163 .take((i + 2).min(sa.len()))
164 .skip(i.saturating_sub(1));
165 for (ni, near_a) in neighbours_a {
166 let neighbours_b = sb
167 .iter()
168 .enumerate()
169 .take((j + 2).min(sb.len()))
170 .skip(j.saturating_sub(1));
171 for (nj, near_b) in neighbours_b {
172 if ni == i && nj == j {
173 continue;
174 }
175 let there = near_a.1.square_distance(near_b.1);
176 if there < here {
177 minimal = false;
178 }
179 if there > here {
180 maximal = false;
181 }
182 }
183 }
184 if minimal || maximal {
185 seeds.push((sa[i].0, sb[j].0));
186 }
187 }
188 }
189 thin(&mut seeds);
190
191 let mut approaches: Vec<Approach<f64, f64>> = Vec::new();
192 for (seed_a, seed_b) in seeds {
193 if let Some((t, s)) = stationary_curve_curve(a, b, seed_a, seed_b, tol) {
194 let (Ok(pa), Ok(pb)) = (a.point_at(t, tol), b.point_at(s, tol)) else {
195 continue;
196 };
197 keep(
198 &mut approaches,
199 Approach {
200 on_a: t,
201 on_b: s,
202 point_a: pa,
203 point_b: pb,
204 distance: pa.distance(pb),
205 },
206 tol,
207 );
208 }
209 }
210 Ok(finish(approaches, tol))
211}
212
213pub(crate) fn stationary_curve_curve(
215 a: &Curve,
216 b: &Curve,
217 seed_a: f64,
218 seed_b: f64,
219 tol: Tolerances,
220) -> Option<(f64, f64)> {
221 let system = |x: &[f64]| {
222 let (t, s) = (fold_curve(a, x[0]), fold_curve(b, x[1]));
223 let pa = a.point_at(t, tol).unwrap_or(Point::ORIGIN);
224 let pb = b.point_at(s, tol).unwrap_or(Point::ORIGIN);
225 let da = a.derivatives_at(t, 2, tol).unwrap_or_default();
226 let db = b.derivatives_at(s, 2, tol).unwrap_or_default();
227 let zero = Vector::ZERO;
228 let (d1a, d2a) = (
229 da.get(1).copied().unwrap_or(zero),
230 da.get(2).copied().unwrap_or(zero),
231 );
232 let (d1b, d2b) = (
233 db.get(1).copied().unwrap_or(zero),
234 db.get(2).copied().unwrap_or(zero),
235 );
236 let gap = pa - pb;
237 (
238 vec![gap.dot(d1a), -gap.dot(d1b)],
239 vec![
240 vec![d1a.dot(d1a) + gap.dot(d2a), -d1a.dot(d1b)],
241 vec![-d1a.dot(d1b), d1b.dot(d1b) - gap.dot(d2b)],
242 ],
243 )
244 };
245 let criteria = solve::Criteria {
246 residual: tol.confusion() * tol.confusion(),
249 step: tol.parametric(),
250 max_iterations: 40,
251 };
252 let found = solve::newton_system(system, &[seed_a, seed_b], criteria).ok()?;
253 Some((fold_curve(a, found.value[0]), fold_curve(b, found.value[1])))
254}
255
256fn line_line(
258 a: &ogeom_geom::LineCurve,
259 b: &ogeom_geom::LineCurve,
260 tol: Tolerances,
261) -> Extrema<f64, f64> {
262 let (oa, da) = (a.axis().location, a.axis().direction.vector());
263 let (ob, db) = (b.axis().location, b.axis().direction.vector());
264 let cross = da.cross(db);
265 let denominator = cross.dot(cross);
266
267 if denominator <= tol.angular() * tol.angular() {
268 let (a_lo, a_hi) = a.domain();
272 let (b_lo, b_hi) = b.domain();
273 let project = |p: Point| (p - oa).dot(da);
274 let (s0, s1) = (project(ob + db * b_lo), project(ob + db * b_hi));
275 let (lo, hi) = (s0.min(s1).max(a_lo), s0.max(s1).min(a_hi));
276 if lo > hi {
277 return Extrema {
278 approaches: Vec::new(),
279 family: true,
280 };
281 }
282 let t = f64::midpoint(lo, hi);
283 let pa = oa + da * t;
284 let s = (pa - ob).dot(db);
285 let pb = ob + db * s;
286 return Extrema {
287 approaches: vec![Approach {
288 on_a: t,
289 on_b: s,
290 point_a: pa,
291 point_b: pb,
292 distance: pa.distance(pb),
293 }],
294 family: true,
295 };
296 }
297
298 let between = ob - oa;
299 let t = between.cross(db).dot(cross) / denominator;
300 let s = between.cross(da).dot(cross) / denominator;
301 let (a_lo, a_hi) = a.domain();
302 let (b_lo, b_hi) = b.domain();
303 if t < a_lo || t > a_hi || s < b_lo || s > b_hi {
304 return Extrema {
307 approaches: Vec::new(),
308 family: false,
309 };
310 }
311 let pa = oa + da * t;
312 let pb = ob + db * s;
313 Extrema {
314 approaches: vec![Approach {
315 on_a: t,
316 on_b: s,
317 point_a: pa,
318 point_b: pb,
319 distance: pa.distance(pb),
320 }],
321 family: false,
322 }
323}
324
325pub fn extrema_curve_surface(
333 curve: &Curve,
334 surface: &SurfaceGeometry,
335 options: ExtremaOptions,
336 tol: Tolerances,
337) -> OgeomResult<Extrema<f64, (f64, f64)>> {
338 if options.samples < 2 || options.grid < 2 {
339 ogeom_bail!(Construction, "seeding needs at least two steps each way");
340 }
341 let (lo, hi) = curve.domain();
342 if hi - lo > WIDEST_DOMAIN {
343 ogeom_bail!(
344 Domain,
345 "the curve's domain spans {:.0e}; trim it before asking",
346 hi - lo
347 );
348 }
349 wide_surface_check(surface)?;
350
351 let sc = sample_curve(curve, options.samples, tol);
352 let ss = sample_surface(surface, options.grid, tol);
353 if sc.len() < 2 || ss.is_empty() {
354 ogeom_bail!(
355 Construction,
356 "a geometry failed to evaluate over its domain"
357 );
358 }
359
360 let mut best = Vec::with_capacity(sc.len());
363 let mut worst = Vec::with_capacity(sc.len());
364 for (_, p) in &sc {
365 let mut near = (f64::INFINITY, (0.0, 0.0));
366 let mut far = (f64::NEG_INFINITY, (0.0, 0.0));
367 for (uv, q) in &ss {
368 let d = p.square_distance(*q);
369 if d < near.0 {
370 near = (d, *uv);
371 }
372 if d > far.0 {
373 far = (d, *uv);
374 }
375 }
376 best.push(near);
377 worst.push(far);
378 }
379 let mut seeds = Vec::new();
380 for i in 0..sc.len() {
381 let lower = i == 0 || best[i].0 <= best[i - 1].0;
382 let upper = i + 1 == sc.len() || best[i].0 <= best[i + 1].0;
383 if lower && upper {
384 seeds.push((sc[i].0, best[i].1));
385 }
386 let lower = i == 0 || worst[i].0 >= worst[i - 1].0;
387 let upper = i + 1 == sc.len() || worst[i].0 >= worst[i + 1].0;
388 if lower && upper {
389 seeds.push((sc[i].0, worst[i].1));
390 }
391 }
392 thin(&mut seeds);
393
394 let mut approaches: Vec<Approach<f64, (f64, f64)>> = Vec::new();
395 for (seed_t, seed_uv) in seeds {
396 if let Some((t, u, v)) = stationary_curve_surface(curve, surface, seed_t, seed_uv, tol) {
397 let (Ok(pc), Ok(ps)) = (curve.point_at(t, tol), surface.point_at(u, v, tol)) else {
398 continue;
399 };
400 keep(
401 &mut approaches,
402 Approach {
403 on_a: t,
404 on_b: (u, v),
405 point_a: pc,
406 point_b: ps,
407 distance: pc.distance(ps),
408 },
409 tol,
410 );
411 }
412 }
413 Ok(finish(approaches, tol))
414}
415
416fn stationary_curve_surface(
418 curve: &Curve,
419 surface: &SurfaceGeometry,
420 seed_t: f64,
421 seed_uv: (f64, f64),
422 tol: Tolerances,
423) -> Option<(f64, f64, f64)> {
424 let system = |x: &[f64]| {
425 let t = fold_curve(curve, x[0]);
426 let (u, v) = fold_surface(surface, x[1], x[2]);
427 let pc = curve.point_at(t, tol).unwrap_or(Point::ORIGIN);
428 let ps = surface.point_at(u, v, tol).unwrap_or(Point::ORIGIN);
429 let dc = curve.derivatives_at(t, 2, tol).unwrap_or_default();
430 let zero = Vector::ZERO;
431 let (ct, ctt) = (
432 dc.get(1).copied().unwrap_or(zero),
433 dc.get(2).copied().unwrap_or(zero),
434 );
435 let (su, sv) = surface.d1_at(u, v, tol).unwrap_or((zero, zero));
436 let (suu, suv, svv) = surface.d2_at(u, v, tol).unwrap_or((zero, zero, zero));
437 let gap = pc - ps;
438 (
439 vec![gap.dot(ct), gap.dot(su), gap.dot(sv)],
440 vec![
441 vec![ct.dot(ct) + gap.dot(ctt), -su.dot(ct), -sv.dot(ct)],
442 vec![
443 ct.dot(su),
444 -su.dot(su) + gap.dot(suu),
445 -sv.dot(su) + gap.dot(suv),
446 ],
447 vec![
448 ct.dot(sv),
449 -su.dot(sv) + gap.dot(suv),
450 -sv.dot(sv) + gap.dot(svv),
451 ],
452 ],
453 )
454 };
455 let criteria = solve::Criteria {
456 residual: tol.confusion() * tol.confusion(),
457 step: tol.parametric(),
458 max_iterations: 40,
459 };
460 let found = solve::newton_system(system, &[seed_t, seed_uv.0, seed_uv.1], criteria).ok()?;
461 let t = fold_curve(curve, found.value[0]);
462 let (u, v) = fold_surface(surface, found.value[1], found.value[2]);
463 Some((t, u, v))
464}
465
466pub fn extrema_surface_surface(
474 a: &SurfaceGeometry,
475 b: &SurfaceGeometry,
476 options: ExtremaOptions,
477 tol: Tolerances,
478) -> OgeomResult<Extrema<(f64, f64), (f64, f64)>> {
479 if options.grid < 2 {
480 ogeom_bail!(Construction, "seeding needs at least two steps each way");
481 }
482 wide_surface_check(a)?;
483 wide_surface_check(b)?;
484
485 let sa = sample_surface(a, options.grid, tol);
486 let sb = sample_surface(b, options.grid, tol);
487 if sa.is_empty() || sb.is_empty() {
488 ogeom_bail!(Construction, "a surface failed to evaluate over its domain");
489 }
490
491 let mut seeds = Vec::new();
494 for (uv_a, p) in &sa {
495 let mut near = (f64::INFINITY, (0.0, 0.0));
496 let mut far = (f64::NEG_INFINITY, (0.0, 0.0));
497 for (uv_b, q) in &sb {
498 let d = p.square_distance(*q);
499 if d < near.0 {
500 near = (d, *uv_b);
501 }
502 if d > far.0 {
503 far = (d, *uv_b);
504 }
505 }
506 seeds.push((*uv_a, near.1));
507 seeds.push((*uv_a, far.1));
508 }
509 for (uv_b, q) in &sb {
510 let mut near = (f64::INFINITY, (0.0, 0.0));
511 for (uv_a, p) in &sa {
512 let d = q.square_distance(*p);
513 if d < near.0 {
514 near = (d, *uv_a);
515 }
516 }
517 seeds.push((near.1, *uv_b));
518 }
519 thin(&mut seeds);
520
521 let mut approaches: Vec<Approach<(f64, f64), (f64, f64)>> = Vec::new();
522 for (seed_a, seed_b) in seeds {
523 if let Some((ua, va, ub, vb)) = stationary_surface_surface(a, b, seed_a, seed_b, tol) {
524 let (Ok(pa), Ok(pb)) = (a.point_at(ua, va, tol), b.point_at(ub, vb, tol)) else {
525 continue;
526 };
527 keep(
528 &mut approaches,
529 Approach {
530 on_a: (ua, va),
531 on_b: (ub, vb),
532 point_a: pa,
533 point_b: pb,
534 distance: pa.distance(pb),
535 },
536 tol,
537 );
538 }
539 }
540 Ok(finish(approaches, tol))
541}
542
543fn stationary_surface_surface(
545 a: &SurfaceGeometry,
546 b: &SurfaceGeometry,
547 seed_a: (f64, f64),
548 seed_b: (f64, f64),
549 tol: Tolerances,
550) -> Option<(f64, f64, f64, f64)> {
551 let system = |x: &[f64]| {
552 let (ua, va) = fold_surface(a, x[0], x[1]);
553 let (ub, vb) = fold_surface(b, x[2], x[3]);
554 let zero = Vector::ZERO;
555 let pa = a.point_at(ua, va, tol).unwrap_or(Point::ORIGIN);
556 let pb = b.point_at(ub, vb, tol).unwrap_or(Point::ORIGIN);
557 let (au, av) = a.d1_at(ua, va, tol).unwrap_or((zero, zero));
558 let (auu, auv, avv) = a.d2_at(ua, va, tol).unwrap_or((zero, zero, zero));
559 let (bu, bv) = b.d1_at(ub, vb, tol).unwrap_or((zero, zero));
560 let (buu, buv, bvv) = b.d2_at(ub, vb, tol).unwrap_or((zero, zero, zero));
561 let gap = pa - pb;
562 (
563 vec![gap.dot(au), gap.dot(av), gap.dot(bu), gap.dot(bv)],
564 vec![
565 vec![
566 au.dot(au) + gap.dot(auu),
567 au.dot(av) + gap.dot(auv),
568 -bu.dot(au),
569 -bv.dot(au),
570 ],
571 vec![
572 au.dot(av) + gap.dot(auv),
573 av.dot(av) + gap.dot(avv),
574 -bu.dot(av),
575 -bv.dot(av),
576 ],
577 vec![
578 au.dot(bu),
579 av.dot(bu),
580 -bu.dot(bu) + gap.dot(buu),
581 -bv.dot(bu) + gap.dot(buv),
582 ],
583 vec![
584 au.dot(bv),
585 av.dot(bv),
586 -bu.dot(bv) + gap.dot(buv),
587 -bv.dot(bv) + gap.dot(bvv),
588 ],
589 ],
590 )
591 };
592 let criteria = solve::Criteria {
593 residual: tol.confusion() * tol.confusion(),
594 step: tol.parametric(),
595 max_iterations: 40,
596 };
597 let found =
598 solve::newton_system(system, &[seed_a.0, seed_a.1, seed_b.0, seed_b.1], criteria).ok()?;
599 let (ua, va) = fold_surface(a, found.value[0], found.value[1]);
600 let (ub, vb) = fold_surface(b, found.value[2], found.value[3]);
601 Some((ua, va, ub, vb))
602}
603
604fn wide_surface_check(surface: &SurfaceGeometry) -> OgeomResult<()> {
607 let ((ua, ub), (va, vb)) = surface.domain();
608 if ub - ua > WIDEST_DOMAIN || vb - va > WIDEST_DOMAIN {
609 ogeom_bail!(
610 Domain,
611 "a surface domain spans more than {WIDEST_DOMAIN:.0e}; trim it before asking"
612 );
613 }
614 Ok(())
615}
616
617fn sample_curve(curve: &Curve, samples: usize, tol: Tolerances) -> Vec<(f64, Point)> {
618 let (lo, hi) = curve.domain();
619 let mut out = Vec::with_capacity(samples + 1);
620 for i in 0..=samples {
621 #[allow(clippy::cast_precision_loss)]
622 let t = lo + (hi - lo) * i as f64 / samples as f64;
623 if let Ok(p) = curve.point_at(t, tol) {
624 out.push((t, p));
625 }
626 }
627 out
628}
629
630#[allow(clippy::type_complexity)]
631fn sample_surface(
632 surface: &SurfaceGeometry,
633 grid: usize,
634 tol: Tolerances,
635) -> Vec<((f64, f64), Point)> {
636 let ((ua, ub), (va, vb)) = surface.domain();
637 let mut out = Vec::with_capacity((grid + 1) * (grid + 1));
638 for i in 0..=grid {
639 for j in 0..=grid {
640 #[allow(clippy::cast_precision_loss)]
641 let u = ua + (ub - ua) * i as f64 / grid as f64;
642 #[allow(clippy::cast_precision_loss)]
643 let v = va + (vb - va) * j as f64 / grid as f64;
644 if let Ok(p) = surface.point_at(u, v, tol) {
645 out.push(((u, v), p));
646 }
647 }
648 }
649 out
650}
651
652fn thin<T>(seeds: &mut Vec<T>) {
654 if seeds.len() <= MOST_SEEDS {
655 return;
656 }
657 let step = seeds.len().div_ceil(MOST_SEEDS);
658 let mut index = 0;
659 seeds.retain(|_| {
660 let kept = index % step == 0;
661 index += 1;
662 kept
663 });
664}
665
666fn keep<A: Copy, B: Copy>(
668 approaches: &mut Vec<Approach<A, B>>,
669 candidate: Approach<A, B>,
670 tol: Tolerances,
671) {
672 let reach = tol.confusion() * DISTINCT;
673 if approaches.iter().any(|known| {
674 known.point_a.distance(candidate.point_a) <= reach
675 && known.point_b.distance(candidate.point_b) <= reach
676 }) {
677 return;
678 }
679 approaches.push(candidate);
680}
681
682fn finish<A: Copy, B: Copy>(mut approaches: Vec<Approach<A, B>>, tol: Tolerances) -> Extrema<A, B> {
684 approaches.sort_by(|a, b| {
685 a.distance
686 .partial_cmp(&b.distance)
687 .unwrap_or(core::cmp::Ordering::Equal)
688 });
689 let family = match approaches.first() {
690 None => false,
691 Some(first) => {
692 let near = tol.confusion().max(first.distance * 1e-9);
693 let ties: Vec<&Approach<A, B>> = approaches
694 .iter()
695 .take_while(|a| a.distance - first.distance <= near)
696 .collect();
697 ties.len() >= 3
701 && ties
702 .iter()
703 .any(|a| a.point_a.distance(first.point_a) > tol.confusion() * DISTINCT * 10.0)
704 }
705 };
706 Extrema { approaches, family }
707}
708
709fn fold_curve(curve: &Curve, t: f64) -> f64 {
710 let (lo, hi) = curve.domain();
711 if curve.is_periodic() {
712 let span = hi - lo;
713 if span > 0.0 {
714 return lo + (t - lo).rem_euclid(span);
715 }
716 }
717 t.clamp(lo, hi)
718}
719
720fn fold_surface(surface: &SurfaceGeometry, u: f64, v: f64) -> (f64, f64) {
721 let ((ua, ub), (va, vb)) = surface.domain();
722 let fold = |x: f64, lo: f64, hi: f64, periodic: bool| {
723 if periodic {
724 let span = hi - lo;
725 if span > 0.0 {
726 return lo + (x - lo).rem_euclid(span);
727 }
728 }
729 x.clamp(lo, hi)
730 };
731 (
732 fold(u, ua, ub, surface.is_periodic_u()),
733 fold(v, va, vb, surface.is_periodic_v()),
734 )
735}
736
737#[cfg(test)]
738#[allow(clippy::unwrap_used)]
739mod tests {
740 use super::*;
741 use ogeom_geom::{CircleCurve, CylinderSurface, LineCurve, PlaneSurface, SphereSurface};
742 use ogeom_math::{Circle, Cylinder, Direction, Frame, Plane, Sphere};
743
744 const T: Tolerances = Tolerances::millimetres();
745
746 fn segment(from: Point, to: Point) -> Curve {
747 LineCurve::segment(from, to, T).unwrap().into()
748 }
749
750 fn circle_at(centre: Point, normal: Vector, radius: f64) -> Curve {
751 CircleCurve::new(
752 Circle::new(
753 Frame::new(
754 centre,
755 Direction::new(normal, T).unwrap(),
756 Direction::from_cross(normal, Vector::new(0.3, 0.5, 0.9), T).unwrap(),
757 T,
758 )
759 .unwrap(),
760 radius,
761 T,
762 )
763 .unwrap(),
764 )
765 .into()
766 }
767
768 fn sphere_at(centre: Point, radius: f64) -> SurfaceGeometry {
769 SphereSurface::new(Sphere::centred(centre, radius, T).unwrap()).into()
770 }
771
772 #[test]
773 fn skew_segments_meet_the_closed_form() {
774 let a = segment(Point::new(-5.0, 0.0, 0.0), Point::new(5.0, 0.0, 0.0));
777 let b = segment(Point::new(0.0, -5.0, 1.0), Point::new(0.0, 5.0, 1.0));
778 let found = extrema_curve_curve(&a, &b, ExtremaOptions::default(), T).unwrap();
779 let nearest = found.nearest().unwrap();
780 assert!((nearest.distance - 1.0).abs() < 1e-9);
781 assert!(nearest.point_a.is_equal(Point::ORIGIN, T));
782 assert!(nearest.point_b.is_equal(Point::new(0.0, 0.0, 1.0), T));
783 assert!(!found.family);
784 }
785
786 #[test]
787 fn endpoint_to_endpoint_nearness_is_the_callers_and_says_so() {
788 let a = segment(Point::new(0.0, 0.0, 0.0), Point::new(1.0, 0.0, 0.0));
793 let b = segment(Point::new(3.0, 0.0, 0.0), Point::new(5.0, 0.0, 0.0));
794 let found = extrema_curve_curve(&a, &b, ExtremaOptions::default(), T).unwrap();
795 assert!(found.approaches.is_empty());
796 }
797
798 #[test]
799 fn parallel_lines_are_a_family_with_a_representative() {
800 let a = segment(Point::new(-4.0, 0.0, 0.0), Point::new(4.0, 0.0, 0.0));
801 let b = segment(Point::new(-2.0, 2.0, 0.0), Point::new(6.0, 2.0, 0.0));
802 let found = extrema_curve_curve(&a, &b, ExtremaOptions::default(), T).unwrap();
803 assert!(found.family);
804 let nearest = found.nearest().unwrap();
805 assert!((nearest.distance - 2.0).abs() < 1e-12);
806 assert!(nearest.on_a >= -2.0 && nearest.on_a <= 8.0);
808 }
809
810 #[test]
811 fn concentric_circles_are_a_family_found_by_sampling() {
812 let a = circle_at(Point::ORIGIN, Vector::Z, 3.0);
815 let b = circle_at(Point::ORIGIN, Vector::Z, 1.0);
816 let found = extrema_curve_curve(&a, &b, ExtremaOptions::default(), T).unwrap();
817 assert!(found.family);
818 assert!((found.nearest().unwrap().distance - 2.0).abs() < 1e-9);
819 }
820
821 #[test]
822 fn a_tilted_circle_over_a_circle_has_isolated_extrema() {
823 let a = circle_at(Point::new(0.0, 0.0, 2.0), Vector::new(0.3, 0.0, 1.0), 3.0);
826 let b = circle_at(Point::ORIGIN, Vector::Z, 3.0);
827 let found = extrema_curve_curve(&a, &b, ExtremaOptions::default(), T).unwrap();
828 assert!(!found.family);
829 let nearest = found.nearest().unwrap();
830 assert!((nearest.point_a.distance(nearest.point_b) - nearest.distance).abs() < 1e-12);
833 assert!(nearest.distance < 2.0, "the tilt brings the rims closer");
834 }
835
836 #[test]
837 fn a_segment_passing_a_sphere_finds_the_gap_to_it() {
838 let line = segment(Point::new(-5.0, 3.0, 0.0), Point::new(5.0, 3.0, 0.0));
841 let ball = sphere_at(Point::ORIGIN, 1.0);
842 let found = extrema_curve_surface(&line, &ball, ExtremaOptions::default(), T).unwrap();
843 let nearest = found.nearest().unwrap();
844 assert!((nearest.distance - 2.0).abs() < 1e-9);
845 assert!(nearest.point_a.is_equal(Point::new(0.0, 3.0, 0.0), T));
846 assert!(nearest.point_b.is_equal(Point::new(0.0, 1.0, 0.0), T));
847 }
848
849 #[test]
850 fn a_circle_parallel_to_a_plane_is_a_family_above_it() {
851 let ring = circle_at(Point::new(0.0, 0.0, 2.0), Vector::Z, 3.0);
852 let ground: SurfaceGeometry = PlaneSurface::over(
853 Plane::through(Point::ORIGIN, Direction::Z),
854 (-8.0, 8.0),
855 (-8.0, 8.0),
856 )
857 .unwrap()
858 .into();
859 let found = extrema_curve_surface(&ring, &ground, ExtremaOptions::default(), T).unwrap();
860 assert!(found.family);
861 assert!((found.nearest().unwrap().distance - 2.0).abs() < 1e-9);
862 }
863
864 #[test]
865 fn two_spheres_apart_meet_along_the_line_of_centres() {
866 let a = sphere_at(Point::ORIGIN, 1.0);
867 let b = sphere_at(Point::new(5.0, 0.0, 0.0), 2.0);
868 let found = extrema_surface_surface(&a, &b, ExtremaOptions::default(), T).unwrap();
869 let nearest = found.nearest().unwrap();
870 assert!((nearest.distance - 2.0).abs() < 1e-9);
871 assert!(nearest.point_a.is_equal(Point::new(1.0, 0.0, 0.0), T));
872 assert!(nearest.point_b.is_equal(Point::new(3.0, 0.0, 0.0), T));
873 assert!(!found.family);
874 }
875
876 #[test]
877 fn concentric_spheres_are_a_family() {
878 let a = sphere_at(Point::ORIGIN, 1.0);
879 let b = sphere_at(Point::ORIGIN, 3.0);
880 let found = extrema_surface_surface(&a, &b, ExtremaOptions::default(), T).unwrap();
881 assert!(found.family);
882 assert!((found.nearest().unwrap().distance - 2.0).abs() < 1e-9);
883 }
884
885 #[test]
886 fn a_cylinder_beside_a_plane_reports_the_ruling_gap_as_a_family() {
887 let drum: SurfaceGeometry =
889 CylinderSurface::new(Cylinder::new(Frame::WORLD, 1.0, T).unwrap(), (-3.0, 3.0))
890 .unwrap()
891 .into();
892 let wall: SurfaceGeometry = PlaneSurface::over(
893 Plane::through(Point::new(4.0, 0.0, 0.0), Direction::X),
894 (-8.0, 8.0),
895 (-8.0, 8.0),
896 )
897 .unwrap()
898 .into();
899 let found = extrema_surface_surface(&drum, &wall, ExtremaOptions::default(), T).unwrap();
900 assert!(found.family);
901 assert!((found.nearest().unwrap().distance - 3.0).abs() < 1e-9);
902 }
903
904 #[test]
905 fn an_untrimmed_line_is_refused_with_instructions() {
906 let endless: Curve = LineCurve::new(ogeom_math::Axis {
907 location: Point::ORIGIN,
908 direction: Direction::X,
909 })
910 .into();
911 let ring = circle_at(Point::ORIGIN, Vector::Z, 1.0);
912 assert!(extrema_curve_curve(&endless, &ring, ExtremaOptions::default(), T).is_err());
913 let other: Curve = LineCurve::new(ogeom_math::Axis {
915 location: Point::new(0.0, 1.0, 0.0),
916 direction: Direction::Y,
917 })
918 .into();
919 assert!(extrema_curve_curve(&endless, &other, ExtremaOptions::default(), T).is_ok());
920 }
921}