Skip to main content

ogeom_algo/
proximity.rs

1//! Minimum distance between shapes.
2//!
3//! The geometry-level extrema in `ogeom-intersect` answer where two curves
4//! or surfaces come nearest; this module assembles those answers for
5//! topology, where a shape is vertices, edges and faces and the nearest
6//! approach may land on any of them.
7//!
8//! # The assembly argument
9//!
10//! The nearest distance between two shapes is attained either at an interior
11//! stationary approach of a pair of elements, or on some element's boundary,
12//! and an element's boundary is itself an element: a face's boundary is its
13//! edges, an edge's boundary is its vertices. So walking every pair of
14//! elements (vertex against vertex, edge and face; edge against edge and
15//! face; face against face) with stationary approaches for the interiors and
16//! projections for the points covers every candidate, and the geometry level
17//! is allowed to answer "no interior approach" honestly because the pair that
18//! owns the boundary case is in the same sweep.
19//!
20//! A face's interior approach is accepted only where its foot lands inside
21//! the face's trimming; a foot outside or too near the boundary is dropped,
22//! because the true nearest point of that configuration is on an edge and the
23//! edge pairs find it exactly.
24//!
25//! # What the distance is between
26//!
27//! Boundaries. A shape strictly inside another reports the gap between their
28//! boundaries, not zero: whether a point is *inside* a solid is
29//! [`classify_in_solid_exact`](crate::classify_in_solid_exact)'s question,
30//! and conflating the two would make this answer wrong for the shells it is
31//! right for.
32
33use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
34use ogeom_geom::{Curve, SurfaceGeometry, Transformable, TrimmedCurve};
35use ogeom_math::{Point, Point2, Transform, Vector};
36use ogeom_mesh::{Deflection, face_boundary, inside_boundary};
37use ogeom_topo::{EdgeRepr, Model, NodeData, Shape, ShapeType, explore_unique};
38
39use crate::classify::{fold_toward_rings, parametric_band};
40use crate::measure::{project_on_curve, project_on_surface};
41use ogeom_intersect::ExtremaOptions;
42
43/// One pair of nearest points, with the elements they lie on.
44#[derive(Debug, Clone)]
45pub struct ClosestPair {
46    /// The nearest point on the first shape.
47    pub point_a: Point,
48    /// The nearest point on the second.
49    pub point_b: Point,
50    /// The vertex, edge or face of the first shape the point lies on.
51    pub support_a: Shape,
52    /// The same for the second shape.
53    pub support_b: Shape,
54}
55
56/// The minimum distance between two shapes, with everywhere it is attained.
57#[derive(Debug, Clone)]
58pub struct ShapeDistance {
59    /// The distance.
60    pub distance: f64,
61    /// Every pair of nearest points found within tolerance of the minimum.
62    /// Parallel walls meet at a representative pair, not at every point of
63    /// the overlap.
64    pub pairs: Vec<ClosestPair>,
65}
66
67/// One element of a shape, with its geometry carried into world space.
68pub(crate) enum Element {
69    Vertex(Shape, Point),
70    Edge(Shape, Box<Curve>),
71    Face(Shape, Box<Prepared>),
72}
73
74/// A face's surface twice over: in world space for the extrema, and local
75/// with its placement and rings for the trim test. Parameters on a
76/// transformed surface need not match the rings, which live in the stored
77/// surface's parameter space, so the trim question is always asked locally.
78pub(crate) struct Prepared {
79    pub(crate) world: SurfaceGeometry,
80    local: SurfaceGeometry,
81    to_local: Transform,
82    rings: Vec<Vec<Point2>>,
83    /// The face's placement, carrying `local` into world space.
84    placement: Transform,
85    /// The parameter rectangle `world` is restricted to, when it is.
86    span: Option<((f64, f64), (f64, f64))>,
87}
88
89/// The minimum distance between two shapes' boundaries.
90///
91/// # Errors
92///
93/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if either shape
94/// has no vertices, edges or faces to measure to;
95/// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if a handle fails to
96/// resolve.
97pub fn distance_between_shapes(
98    model: &Model,
99    a: &Shape,
100    b: &Shape,
101    options: ExtremaOptions,
102    tol: Tolerances,
103) -> OgeomResult<ShapeDistance> {
104    let ea = elements(model, a, tol)?;
105    let eb = elements(model, b, tol)?;
106    distance_between_elements(&ea, &eb, options, tol)
107}
108
109/// A shape's boundary gathered once, to measure many points against.
110pub struct PointDistance(Vec<Element>);
111
112impl core::fmt::Debug for PointDistance {
113    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
114        f.debug_struct("PointDistance")
115            .field("elements", &self.0.len())
116            .finish()
117    }
118}
119
120impl PointDistance {
121    /// The vertices, edges and faces of `shape`, placed.
122    ///
123    /// # Errors
124    ///
125    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if a handle fails to
126    /// resolve.
127    pub fn of(model: &Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Self> {
128        Ok(Self(elements(model, shape, tol)?))
129    }
130
131    /// The vertices, then the edges, then the faces.
132    pub(crate) fn elements(&self) -> &[Element] {
133        &self.0
134    }
135
136    /// The least distance from `p` to the shape's boundary and a point of it
137    /// at that distance, as [`distance_between_shapes`] measures it from a
138    /// vertex at `p`: to each vertex, the foot on each edge, and the foot on
139    /// each face where it lands inside the face's trimming. Where a curved
140    /// face's foot stands outside its trim, its local feet inside it, sought
141    /// from samples of the face, are candidates too.
142    ///
143    /// # Errors
144    ///
145    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the shape
146    /// has no vertex, edge or face foot to measure to.
147    pub fn nearest(&self, p: Point, tol: Tolerances) -> OgeomResult<(f64, Point)> {
148        let mut best: Option<(f64, Point)> = None;
149        let mut keep = |d: f64, q: Point| {
150            if d.is_finite() && best.is_none_or(|(held, _)| d < held) {
151                best = Some((d, q));
152            }
153        };
154        for element in &self.0 {
155            match element {
156                Element::Vertex(_, q) => keep(p.distance(*q), *q),
157                Element::Edge(_, curve) => {
158                    let foot = project_on_curve(curve, p, 64, tol)?;
159                    keep(foot.distance, foot.point);
160                }
161                Element::Face(_, face) => {
162                    let foot = project_on_surface(&face.world, p, 32, tol)?;
163                    if inside_trim(face, foot.point, tol)? {
164                        keep(foot.distance, foot.point);
165                    } else if !matches!(face.local, SurfaceGeometry::Plane(_)) {
166                        for (d, q) in feet_inside(face, p, tol)? {
167                            keep(d, q);
168                        }
169                    }
170                }
171            }
172        }
173        best.ok_or_else(|| {
174            ogeom_core::ogeom_err!(Construction, "a shape with no elements has no distance")
175        })
176    }
177}
178
179/// How many rows and columns of a face's parameter rectangle the local
180/// feet inside its trim are sought from.
181const LOCAL_SEEDS: u32 = 8;
182
183/// The local nearest points of `face`'s curved surface to `p` that land
184/// inside its trim, each with its distance: the surface's nearest point
185/// overall may stand outside the trim while a nearer point of the face
186/// than its edges stands inside it, at another local minimum (a second
187/// dip of a free-form sheet), or at another point of a set of nearest
188/// points (a centre on a cylinder's axis). Each is sought from the samples
189/// of the face's rectangle that lie inside the trim. A plane's distance
190/// has one minimum and no other: its edges hold the face's nearest point
191/// wherever its foot stands outside.
192fn feet_inside(face: &Prepared, p: Point, tol: Tolerances) -> OgeomResult<Vec<(f64, Point)>> {
193    use ogeom_geom::Surface as _;
194    let ((u0, u1), (v0, v1)) = face.world.domain();
195    if ![u0, u1, v0, v1].iter().all(|x| x.is_finite()) {
196        return Ok(Vec::new());
197    }
198    let mut found = Vec::new();
199    for i in 0..LOCAL_SEEDS {
200        for j in 0..LOCAL_SEEDS {
201            let at = |k: u32, lo: f64, hi: f64| {
202                lo + (hi - lo) * (f64::from(k) + 0.5) / f64::from(LOCAL_SEEDS)
203            };
204            let (u, v) = (at(i, u0, u1), at(j, v0, v1));
205            if !inside_trim(face, face.world.point_at(u, v, tol)?, tol)? {
206                continue;
207            }
208            let foot = ogeom_geom::project_on_surface_from(&face.world, p, (u, v), tol)?;
209            if inside_trim(face, foot.point, tol)? {
210                found.push((foot.distance, foot.point));
211            }
212        }
213    }
214    Ok(found)
215}
216
217/// The elements of a shape (its vertices, edges and faces, placed) for
218/// measuring it against others many times: see [`distance_between_elements`].
219pub(crate) struct Elements(Vec<Element>);
220
221impl Elements {
222    pub(crate) fn of(model: &Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Self> {
223        Ok(Self(elements(model, shape, tol)?))
224    }
225
226    /// Whether every point of these elements is proven farther than `gap`
227    /// from every point of `other`.
228    ///
229    /// Each element is held by the convex hull of a few points: a vertex by
230    /// itself, a B-spline curve or patch by its control net, a line by its
231    /// ends, a plane by its corners, any other curve by the corners of its
232    /// bound. Two hulls are apart where their boxes are, or where their
233    /// shadows on one axis are (a patch's normal, or the line between the
234    /// pieces with a curve's chord taken out). Pairs not yet apart are
235    /// refined by halving the larger piece, until every pair is apart or a
236    /// pair cannot be refined within a fixed budget. `false` proves
237    /// nothing: the pair is then measured.
238    ///
239    /// A face counts as its whole surface over the rectangle its rings
240    /// enclose, the same restricted surface the distance is measured on,
241    /// so the hulls hold every candidate the measurement could find.
242    pub(crate) fn apart_by_more_than(&self, other: &Self, gap: f64, tol: Tolerances) -> bool {
243        // Without a vertex on each side a measurement may find no candidate
244        // at all and refuse; that refusal is the measurement's to make.
245        let has_vertex = |e: &Self| e.0.iter().any(|e| matches!(e, Element::Vertex(..)));
246        if !has_vertex(self) || !has_vertex(other) {
247            return false;
248        }
249        let mut arena = Arena::default();
250        let (Some(mine), Some(theirs)) = (arena.roots(&self.0, tol), arena.roots(&other.0, tol))
251        else {
252            return false;
253        };
254        let mut stack: Vec<(usize, usize)> = mine
255            .iter()
256            .flat_map(|&i| theirs.iter().map(move |&j| (i, j)))
257            .collect();
258        let mut visits = 0_usize;
259        while let Some((i, j)) = stack.pop() {
260            visits += 1;
261            if visits > COVER_VISITS {
262                return false;
263            }
264            if arena.nodes[i].apart_from(&arena.nodes[j], gap) {
265                continue;
266            }
267            let (big, small) = if arena.nodes[i].size >= arena.nodes[j].size {
268                (i, j)
269            } else {
270                (j, i)
271            };
272            let halves = match arena.halves(big, gap, tol) {
273                Some(h) => Some((h, small)),
274                None => arena.halves(small, gap, tol).map(|h| (h, big)),
275            };
276            let Some(([a, b], with)) = halves else {
277                return false;
278            };
279            stack.push((a, with));
280            stack.push((b, with));
281        }
282        true
283    }
284}
285
286/// How many pairs of pieces [`Elements::apart_by_more_than`] looks at before
287/// it leaves the pair to be measured.
288const COVER_VISITS: usize = 20_000;
289
290/// A piece of one element's geometry that can be held and halved.
291enum Piece<'a> {
292    /// Points whose hull holds the piece, with nothing finer to offer.
293    Fixed(Vec<Point>),
294    /// A non-periodic B-spline curve, held by its control polygon.
295    Spline(Box<ogeom_geom::BSplineCurve>),
296    /// A line or conic over a range.
297    Arc(&'a Curve, (f64, f64)),
298    /// A B-spline patch in its face's frame, held by its control net.
299    Patch(Box<ogeom_geom::BSplineSurface>, &'a Transform),
300    /// A plane over a parameter rectangle, held by its corners.
301    Flat(
302        &'a ogeom_geom::PlaneSurface,
303        (f64, f64),
304        (f64, f64),
305        &'a Transform,
306    ),
307}
308
309struct Node<'a> {
310    piece: Piece<'a>,
311    /// World points whose convex hull holds the piece.
312    hull: Vec<Point>,
313    bound: ogeom_math::Aabb,
314    /// The bound's diagonal.
315    size: f64,
316    /// Across a patch or a plane: the normal of its corners.
317    normal: Option<Vector>,
318    /// Along a curve: from its start to its end.
319    chord: Option<Vector>,
320    /// The two halves once asked for; `Some(None)` where there are none.
321    halves: Option<Option<[usize; 2]>>,
322}
323
324impl Node<'_> {
325    /// Whether the two hulls are proven farther than `gap` apart.
326    fn apart_from(&self, other: &Self, gap: f64) -> bool {
327        if self.bound.distance_to_box(&other.bound) > gap {
328            return true;
329        }
330        let (Some(here), Some(there)) = (self.bound.centre(), other.bound.centre()) else {
331            return false;
332        };
333        let between = there - here;
334        let square = |v: Vector| {
335            let length = v.magnitude();
336            (length > 0.0 && length.is_finite()).then(|| v / length)
337        };
338        // The line between the pieces with a curve's own direction taken
339        // out, so two runs side by side are told apart across their gap.
340        let across = |chord: Option<Vector>| {
341            let chord = square(chord?)?;
342            square(between - chord * between.dot(chord))
343        };
344        [
345            self.normal.and_then(square),
346            other.normal.and_then(square),
347            square(between),
348            across(self.chord),
349            across(other.chord),
350        ]
351        .into_iter()
352        .flatten()
353        .any(|axis| {
354            let shadow = |hull: &[Point]| {
355                hull.iter()
356                    .map(|p| (*p - Point::ORIGIN).dot(axis))
357                    .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), d| {
358                        (lo.min(d), hi.max(d))
359                    })
360            };
361            let (a, b) = (shadow(&self.hull), shadow(&other.hull));
362            b.0 - a.1 > gap || a.0 - b.1 > gap
363        })
364    }
365}
366
367#[derive(Default)]
368struct Arena<'a> {
369    nodes: Vec<Node<'a>>,
370}
371
372impl<'a> Arena<'a> {
373    /// One node per element, or `None` if an element has no hull to start
374    /// from.
375    fn roots(&mut self, elements: &'a [Element], tol: Tolerances) -> Option<Vec<usize>> {
376        let mut out = Vec::with_capacity(elements.len());
377        for element in elements {
378            let piece = match element {
379                Element::Vertex(_, p) => Piece::Fixed(vec![*p]),
380                Element::Edge(_, curve) => curve_piece(curve, tol)?,
381                Element::Face(_, face) => face_piece(face, tol)?,
382            };
383            out.push(self.add(piece, tol)?);
384        }
385        Some(out)
386    }
387
388    fn add(&mut self, piece: Piece<'a>, tol: Tolerances) -> Option<usize> {
389        let (hull, normal, chord) = hull_of(&piece, tol)?;
390        // An empty or unmeasurable hull would prove a distance it does not
391        // hold.
392        if hull.is_empty()
393            || !hull
394                .iter()
395                .all(|p| p.x.is_finite() && p.y.is_finite() && p.z.is_finite())
396        {
397            return None;
398        }
399        let bound = ogeom_math::Aabb::of_points(&hull);
400        self.nodes.push(Node {
401            piece,
402            size: bound.diagonal(),
403            hull,
404            bound,
405            normal,
406            chord,
407            halves: None,
408        });
409        Some(self.nodes.len() - 1)
410    }
411
412    /// The node's two halves, made once; `None` where it cannot be halved
413    /// or is no larger than `gap` already.
414    fn halves(&mut self, i: usize, gap: f64, tol: Tolerances) -> Option<[usize; 2]> {
415        if let Some(known) = self.nodes[i].halves {
416            return known;
417        }
418        let found = if self.nodes[i].size <= gap {
419            None
420        } else {
421            split(&self.nodes[i].piece, tol).and_then(|(a, b)| {
422                let a = self.add(a, tol)?;
423                let b = self.add(b, tol)?;
424                Some([a, b])
425            })
426        };
427        self.nodes[i].halves = Some(found);
428        found
429    }
430}
431
432/// An edge's curve as a piece; `None` where it has no bound.
433fn curve_piece(curve: &Curve, tol: Tolerances) -> Option<Piece<'_>> {
434    use ogeom_geom::Curve3d as _;
435    let range = curve.domain();
436    let whole = || {
437        crate::measure::curve_bounds(curve, tol)
438            .ok()
439            .map(|bound| Piece::Fixed(bound.corners()))
440    };
441    match curve {
442        Curve::BSpline(spline) if !spline.is_periodic() => {
443            Some(Piece::Spline(Box::new(spline.clone())))
444        }
445        Curve::Line(_) | Curve::Circle(_) | Curve::Ellipse(_) => Some(Piece::Arc(curve, range)),
446        Curve::Trimmed(trimmed) if !trimmed.is_reversed() => match trimmed.basis() {
447            Curve::BSpline(spline) => spline
448                .segment(range, tol)
449                .map_or_else(|_| whole(), |piece| Some(Piece::Spline(Box::new(piece)))),
450            basis @ (Curve::Line(_) | Curve::Circle(_) | Curve::Ellipse(_)) => {
451                Some(Piece::Arc(basis, range))
452            }
453            _ => whole(),
454        },
455        _ => whole(),
456    }
457}
458
459/// A face's restricted surface as a piece; `None` for a surface this does
460/// not hold.
461fn face_piece(face: &Prepared, tol: Tolerances) -> Option<Piece<'_>> {
462    match &face.local {
463        SurfaceGeometry::BSpline(spline) => {
464            let patch = match face.span {
465                Some((u, v)) => spline.segment(u, v, tol).ok()?,
466                None => spline.clone(),
467            };
468            Some(Piece::Patch(Box::new(patch), &face.placement))
469        }
470        SurfaceGeometry::Plane(plane) => {
471            let (u, v) = face.span?;
472            Some(Piece::Flat(plane, u, v, &face.placement))
473        }
474        _ => None,
475    }
476}
477
478/// The points whose hull holds a piece, its normal where it is a surface,
479/// and its chord where it is a curve.
480type Hull = (Vec<Point>, Option<Vector>, Option<Vector>);
481
482fn hull_of(piece: &Piece<'_>, tol: Tolerances) -> Option<Hull> {
483    use ogeom_geom::{Curve3d as _, Surface as _};
484    // The normal of four corners, as the cross of the diagonals.
485    let normal = |[a, b, c, d]: [Point; 4]| (d - a).cross(c - b);
486    Some(match piece {
487        Piece::Fixed(points) => (points.clone(), None, None),
488        Piece::Spline(spline) => {
489            let hull: Vec<Point> = spline.control_points().iter().map(|w| w.point()).collect();
490            let chord = *hull.last()? - *hull.first()?;
491            (hull, None, Some(chord))
492        }
493        Piece::Arc(curve, range) => {
494            let (start, end) = (
495                curve.point_at(range.0, tol).ok()?,
496                curve.point_at(range.1, tol).ok()?,
497            );
498            let hull = match curve {
499                Curve::Line(_) => vec![start, end],
500                _ => crate::measure::curve_bounds_over(curve, *range, tol)
501                    .ok()?
502                    .corners(),
503            };
504            (hull, None, Some(end - start))
505        }
506        Piece::Patch(patch, placement) => {
507            let grid = patch.grid();
508            let (nu, nv) = (grid.u_count(), grid.v_count());
509            let at = |i, j| grid.get(i, j).map(|w| placement.apply(w.point()));
510            let corners = [
511                at(0, 0)?,
512                at(nu - 1, 0)?,
513                at(0, nv - 1)?,
514                at(nu - 1, nv - 1)?,
515            ];
516            let hull = grid
517                .points()
518                .iter()
519                .map(|w| placement.apply(w.point()))
520                .collect();
521            (hull, Some(normal(corners)), None)
522        }
523        Piece::Flat(plane, u, v, placement) => {
524            let mut corners = [Point::ORIGIN; 4];
525            for (corner, (a, b)) in
526                corners
527                    .iter_mut()
528                    .zip([(u.0, v.0), (u.1, v.0), (u.0, v.1), (u.1, v.1)])
529            {
530                *corner = placement.apply(plane.point_at(a, b, tol).ok()?);
531            }
532            (corners.to_vec(), Some(normal(corners)), None)
533        }
534    })
535}
536
537/// A piece cut into two halves of its parameter range.
538fn split<'a>(piece: &Piece<'a>, tol: Tolerances) -> Option<(Piece<'a>, Piece<'a>)> {
539    use ogeom_geom::{Curve3d as _, Surface as _};
540    let mid = |(a, b): (f64, f64)| 0.5 * (a + b);
541    match piece {
542        Piece::Fixed(_) => None,
543        Piece::Spline(spline) => {
544            let (a, b) = spline.domain();
545            let m = mid((a, b));
546            Some((
547                Piece::Spline(Box::new(spline.segment((a, m), tol).ok()?)),
548                Piece::Spline(Box::new(spline.segment((m, b), tol).ok()?)),
549            ))
550        }
551        Piece::Arc(curve, (a, b)) => {
552            let m = mid((*a, *b));
553            Some((Piece::Arc(curve, (*a, m)), Piece::Arc(curve, (m, *b))))
554        }
555        Piece::Patch(patch, placement) => {
556            let (u, v) = patch.domain();
557            // Across the direction whose control polygons run longer.
558            let grid = patch.grid();
559            let length = |along_u: bool| {
560                let (across, along) = if along_u {
561                    (grid.v_count(), grid.u_count())
562                } else {
563                    (grid.u_count(), grid.v_count())
564                };
565                let at = |k: usize, o: usize| {
566                    if along_u {
567                        grid.get(k, o)
568                    } else {
569                        grid.get(o, k)
570                    }
571                };
572                let mut longest = 0.0_f64;
573                for o in 0..across {
574                    let mut run = 0.0;
575                    for k in 1..along {
576                        if let (Some(p), Some(q)) = (at(k - 1, o), at(k, o)) {
577                            run += p.point().distance(q.point());
578                        }
579                    }
580                    longest = longest.max(run);
581                }
582                longest
583            };
584            let pieces = if length(true) >= length(false) {
585                let m = mid(u);
586                (
587                    patch.segment((u.0, m), v, tol),
588                    patch.segment((m, u.1), v, tol),
589                )
590            } else {
591                let m = mid(v);
592                (
593                    patch.segment(u, (v.0, m), tol),
594                    patch.segment(u, (m, v.1), tol),
595                )
596            };
597            Some((
598                Piece::Patch(Box::new(pieces.0.ok()?), placement),
599                Piece::Patch(Box::new(pieces.1.ok()?), placement),
600            ))
601        }
602        Piece::Flat(plane, u, v, placement) => {
603            if u.1 - u.0 >= v.1 - v.0 {
604                let m = mid(*u);
605                Some((
606                    Piece::Flat(plane, (u.0, m), *v, placement),
607                    Piece::Flat(plane, (m, u.1), *v, placement),
608                ))
609            } else {
610                let m = mid(*v);
611                Some((
612                    Piece::Flat(plane, *u, (v.0, m), placement),
613                    Piece::Flat(plane, *u, (m, v.1), placement),
614                ))
615            }
616        }
617    }
618}
619
620/// [`distance_between_shapes`] on elements already gathered.
621pub(crate) fn distance_between_prepared(
622    a: &Elements,
623    b: &Elements,
624    options: ExtremaOptions,
625    tol: Tolerances,
626) -> OgeomResult<ShapeDistance> {
627    distance_between_elements(&a.0, &b.0, options, tol)
628}
629
630fn distance_between_elements(
631    ea: &[Element],
632    eb: &[Element],
633    options: ExtremaOptions,
634    tol: Tolerances,
635) -> OgeomResult<ShapeDistance> {
636    if ea.is_empty() || eb.is_empty() {
637        ogeom_bail!(Construction, "a shape with no elements has no distance");
638    }
639
640    let mut candidates: Vec<(f64, ClosestPair)> = Vec::new();
641    for element_a in ea {
642        for element_b in eb {
643            approach(element_a, element_b, options, tol, &mut candidates)?;
644        }
645    }
646    // A candidate that is not a number measures nothing; it neither wins
647    // nor, compared equal, lets every other through.
648    candidates.retain(|(d, _)| d.is_finite());
649    let Some(least) = candidates.iter().map(|(d, _)| *d).min_by(f64::total_cmp) else {
650        ogeom_bail!(
651            NotDone,
652            "no candidate approach was found between these shapes"
653        );
654    };
655
656    let mut pairs: Vec<ClosestPair> = Vec::new();
657    for (d, pair) in candidates {
658        if d - least > tol.confusion() {
659            continue;
660        }
661        // The same nearest pair arrives from several element pairs: a corner
662        // is on a vertex, three edges and three faces at once. Keep the first
663        // at each location.
664        if pairs.iter().any(|known| {
665            known.point_a.distance(pair.point_a) <= tol.confusion() * 1e2
666                && known.point_b.distance(pair.point_b) <= tol.confusion() * 1e2
667        }) {
668            continue;
669        }
670        pairs.push(pair);
671    }
672    Ok(ShapeDistance {
673        distance: least,
674        pairs,
675    })
676}
677
678/// Candidate approaches between one pair of elements.
679fn approach(
680    a: &Element,
681    b: &Element,
682    options: ExtremaOptions,
683    tol: Tolerances,
684    out: &mut Vec<(f64, ClosestPair)>,
685) -> OgeomResult<()> {
686    let mut push = |distance: f64, pa: Point, pb: Point, sa: &Shape, sb: &Shape| {
687        out.push((
688            distance,
689            ClosestPair {
690                point_a: pa,
691                point_b: pb,
692                support_a: sa.clone(),
693                support_b: sb.clone(),
694            },
695        ));
696    };
697    match (a, b) {
698        (Element::Vertex(sa, pa), Element::Vertex(sb, pb)) => {
699            push(pa.distance(*pb), *pa, *pb, sa, sb);
700        }
701        (Element::Vertex(sa, pa), Element::Edge(sb, curve)) => {
702            let foot = project_on_curve(curve, *pa, 64, tol)?;
703            push(foot.distance, *pa, foot.point, sa, sb);
704        }
705        (Element::Edge(sa, curve), Element::Vertex(sb, pb)) => {
706            let foot = project_on_curve(curve, *pb, 64, tol)?;
707            push(foot.distance, foot.point, *pb, sa, sb);
708        }
709        (Element::Vertex(sa, pa), Element::Face(sb, face)) => {
710            let foot = project_on_surface(&face.world, *pa, 32, tol)?;
711            if inside_trim(face, foot.point, tol)? {
712                push(foot.distance, *pa, foot.point, sa, sb);
713            }
714        }
715        (Element::Face(sa, face), Element::Vertex(sb, pb)) => {
716            let foot = project_on_surface(&face.world, *pb, 32, tol)?;
717            if inside_trim(face, foot.point, tol)? {
718                push(foot.distance, foot.point, *pb, sa, sb);
719            }
720        }
721        (Element::Edge(sa, ca), Element::Edge(sb, cb)) => {
722            let found = ogeom_intersect::extrema_curve_curve(ca, cb, options, tol)?;
723            for near in &found.approaches {
724                push(near.distance, near.point_a, near.point_b, sa, sb);
725            }
726        }
727        (Element::Edge(sa, curve), Element::Face(sb, face)) => {
728            let found = ogeom_intersect::extrema_curve_surface(curve, &face.world, options, tol)?;
729            for near in &found.approaches {
730                if inside_trim(face, near.point_b, tol)? {
731                    push(near.distance, near.point_a, near.point_b, sa, sb);
732                }
733            }
734        }
735        (Element::Face(sa, face), Element::Edge(sb, curve)) => {
736            let found = ogeom_intersect::extrema_curve_surface(curve, &face.world, options, tol)?;
737            for near in &found.approaches {
738                if inside_trim(face, near.point_b, tol)? {
739                    push(near.distance, near.point_b, near.point_a, sa, sb);
740                }
741            }
742        }
743        (Element::Face(sa, fa), Element::Face(sb, fb)) => {
744            let found =
745                ogeom_intersect::extrema_surface_surface(&fa.world, &fb.world, options, tol)?;
746            for near in &found.approaches {
747                if inside_trim(fa, near.point_a, tol)? && inside_trim(fb, near.point_b, tol)? {
748                    push(near.distance, near.point_a, near.point_b, sa, sb);
749                }
750            }
751        }
752    }
753    Ok(())
754}
755
756/// Whether a world-space point on a face's surface lands inside its trimming.
757///
758/// Asked in the stored surface's own parameter space: the point is carried
759/// into the face's frame and projected there, exactly as `classify_on_face`
760/// does it, because rings and world-surface parameters need not agree under a
761/// placement that scales. Too near the boundary counts as outside: the edge
762/// pairs own that candidate and answer it exactly.
763fn inside_trim(face: &Prepared, world_point: Point, tol: Tolerances) -> OgeomResult<bool> {
764    Ok(trim_side(face, None, RING_CHORD, world_point, tol)? == Trim::Inside)
765}
766
767/// Where a point on a face's surface stands against its trim.
768#[derive(Debug, Clone, Copy, PartialEq, Eq)]
769pub(crate) enum Trim {
770    /// Inside the trim, farther than the rings' band from it.
771    Inside,
772    /// Outside it, as far.
773    Outside,
774    /// Within the band, where the rings cannot tell.
775    Band,
776}
777
778/// Where `world_point`, on `face`'s surface, stands against rings drawn
779/// within `chord` of the face's edges: the face's own rings at
780/// [`RING_CHORD`] where `rings` is `None`. The band is the chord and the
781/// confusion distance, in space.
782pub(crate) fn trim_side(
783    face: &Prepared,
784    rings: Option<&[Vec<Point2>]>,
785    chord: f64,
786    world_point: Point,
787    tol: Tolerances,
788) -> OgeomResult<Trim> {
789    let rings = rings.unwrap_or(&face.rings);
790    let local = face.to_local.apply(world_point);
791    let projection = project_on_surface(&face.local, local, 32, tol)?;
792    let (u, v) = projection.parameters;
793    let at = fold_toward_rings(&face.local, rings, Point2::new(u, v));
794    let band = parametric_band(&face.local, (at.x, at.y), tol.confusion() + chord, tol);
795    if band.meets_rings(rings, at) {
796        return Ok(Trim::Band);
797    }
798    Ok(if inside_boundary(rings, at) {
799        Trim::Inside
800    } else {
801        Trim::Outside
802    })
803}
804
805/// The rings' polylining error, spatially: the trim test's uncertainty band.
806pub(crate) const RING_CHORD: f64 = 1e-3;
807
808/// The rings' greatest turn across one segment, in radians.
809pub(crate) const RING_ANGULAR: f64 = 0.05;
810
811/// Every element of a shape, with world-space geometry.
812fn elements(model: &Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Vec<Element>> {
813    let mut out = Vec::new();
814    for vertex in explore_unique(model, shape, ShapeType::Vertex)? {
815        let Some(node) = model.node(&vertex) else {
816            ogeom_bail!(Dangling, "vertex is not in this model");
817        };
818        let Some(data) = node.data().as_vertex() else {
819            ogeom_bail!(Construction, "vertex node holds no vertex data");
820        };
821        let placed = vertex.transform(model.datums())?.apply(data.point);
822        out.push(Element::Vertex(vertex, placed));
823    }
824    for edge in explore_unique(model, shape, ShapeType::Edge)? {
825        let Some(node) = model.node(&edge) else {
826            ogeom_bail!(Dangling, "edge is not in this model");
827        };
828        let NodeData::Edge(data) = node.data() else {
829            ogeom_bail!(Construction, "edge node holds no edge data");
830        };
831        let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
832            // A degenerate edge has no extent of its own; its vertex and its
833            // face carry its geometry.
834            continue;
835        };
836        let Some(geometry) = model.geometry().curve(*curve) else {
837            ogeom_bail!(Dangling, "curve is not in this model");
838        };
839        let placement = edge.transform(model.datums())?;
840        let trimmed: Curve = if (range.0, range.1) == {
841            use ogeom_geom::Curve3d as _;
842            geometry.domain()
843        } {
844            geometry.clone()
845        } else {
846            TrimmedCurve::new(geometry.clone(), range.0, range.1, tol)?.into()
847        };
848        out.push(Element::Edge(
849            edge,
850            Box::new(trimmed.transformed(&placement, tol)?),
851        ));
852    }
853    let ring_deflection = Deflection {
854        chord: RING_CHORD,
855        angular: RING_ANGULAR,
856        ..Deflection::default()
857    };
858    for face in explore_unique(model, shape, ShapeType::Face)? {
859        let Some(node) = model.node(&face) else {
860            ogeom_bail!(Dangling, "face is not in this model");
861        };
862        let NodeData::Face(data) = node.data() else {
863            ogeom_bail!(Construction, "face node holds no face data");
864        };
865        let Some(surface) = model.geometry().surface(data.surface) else {
866            ogeom_bail!(Dangling, "face refers to a surface not in this model");
867        };
868        let placement = face.transform(model.datums())?;
869        let rings = face_boundary(model, &face, ring_deflection, tol)?;
870        // The stored surface may declare an enormous domain (a plane spans
871        // ±1e9), and the extrema layer rightly refuses to sample that. The
872        // face only uses what its rings enclose, so the surface handed over
873        // is trimmed to their parameter bound, with a margin for the rings'
874        // own polylining, before being carried into world space.
875        let span = ring_span(surface, &rings, tol);
876        let restricted = match span {
877            Some((u, v)) => ogeom_geom::TrimmedSurface::new(surface.clone(), u, v, tol)?.into(),
878            None => surface.clone(),
879        };
880        out.push(Element::Face(
881            face,
882            Box::new(Prepared {
883                world: restricted.transformed(&placement, tol)?,
884                local: surface.clone(),
885                to_local: placement.inverse()?,
886                rings,
887                placement,
888                span,
889            }),
890        ));
891    }
892    Ok(out)
893}
894
895/// The parameter rectangle a face's rings enclose, the surface restricted
896/// to it being all of the face there is to measure; `None` for a face with
897/// no rings, which uses its surface's whole domain.
898///
899/// The margin is proportional to the used span: the exact boundary lies
900/// within the rings' polylining of it, and `inside_trim` already treats the
901/// near-boundary band as the edges' territory, so the margin only has to
902/// keep the whole face inside the restriction; it does not have to be
903/// tight.
904fn ring_span(
905    surface: &SurfaceGeometry,
906    rings: &[Vec<Point2>],
907    tol: Tolerances,
908) -> Option<((f64, f64), (f64, f64))> {
909    use ogeom_geom::Surface as _;
910    let ((ua, ub), (va, vb)) = surface.domain();
911    let mut u = (f64::INFINITY, f64::NEG_INFINITY);
912    let mut v = (f64::INFINITY, f64::NEG_INFINITY);
913    for ring in rings {
914        for p in ring {
915            u = (u.0.min(p.x), u.1.max(p.x));
916            v = (v.0.min(p.y), v.1.max(p.y));
917        }
918    }
919    if u.0 > u.1 || v.0 > v.1 {
920        return None;
921    }
922    let margin_u = (u.1 - u.0).mul_add(0.05, tol.parametric());
923    let margin_v = (v.1 - v.0).mul_add(0.05, tol.parametric());
924    // A periodic domain names one turn, not a limit on the face's chart.
925    // Its rings may straddle that turn or stand whole periods away.
926    let window = |range: (f64, f64), margin: f64, domain: (f64, f64), periodic| {
927        let (lo, hi) = (range.0 - margin, range.1 + margin);
928        if periodic {
929            (lo, hi)
930        } else {
931            (lo.max(domain.0), hi.min(domain.1))
932        }
933    };
934    let (lo_u, hi_u) = window(u, margin_u, (ua, ub), surface.is_periodic_u());
935    let (lo_v, hi_v) = window(v, margin_v, (va, vb), surface.is_periodic_v());
936    (lo_u < hi_u && lo_v < hi_v).then_some(((lo_u, hi_u), (lo_v, hi_v)))
937}
938
939#[cfg(test)]
940#[allow(clippy::unwrap_used)]
941mod tests {
942    use super::*;
943    use crate::{make_box, make_cylinder, make_sphere};
944    use ogeom_math::{Direction, Frame, Vector};
945
946    const T: Tolerances = Tolerances::millimetres();
947
948    fn frame_at(origin: Point) -> Frame {
949        Frame::new(origin, Direction::Z, Direction::X, T).unwrap()
950    }
951
952    #[test]
953    fn a_torus_distance_is_independent_of_its_seam_window() {
954        use core::f64::consts::{FRAC_PI_2, PI, TAU};
955        use ogeom_topo::VertexData;
956
957        // Each chart describes the same torus. Its exact nearest point lies
958        // on the tube's radial line, and the distance is the radial offset.
959        // Model confusion bounds the projection's numerical error.
960        let (major, minor) = (5.0, 2.0);
961        let u = 0.7_f64;
962        for seam in [-2.0 * TAU, -PI, 0.0, PI, TAU + FRAC_PI_2] {
963            let mut model = Model::new();
964            let face = crate::torus_face_seamed_at(&mut model, Frame::WORLD, major, minor, seam, T)
965                .unwrap();
966            let distance = PointDistance::of(&model, &face, T).unwrap();
967            // Neighbours of the seam stand beyond the trim's spatial chord
968            // band (1e-3); inside that band the boundary edge owns the foot.
969            for v in [
970                -FRAC_PI_2,
971                0.0,
972                FRAC_PI_2,
973                PI,
974                3.0 * FRAC_PI_2,
975                TAU - 0.01,
976                TAU,
977                TAU + 0.01,
978                2.0 * TAU + FRAC_PI_2,
979            ] {
980                for offset in [-0.125_f64, 0.0, 0.125] {
981                    let point = |radius: f64| {
982                        let reach = major + radius * v.cos();
983                        Point::new(reach * u.cos(), reach * u.sin(), radius * v.sin())
984                    };
985                    let query = point(minor + offset);
986                    let exact = point(minor);
987                    let (found, foot) = distance.nearest(query, T).unwrap();
988                    assert!(
989                        (found - offset.abs()).abs() <= T.confusion(),
990                        "seam={seam}, v={v}, offset={offset}: {found}"
991                    );
992                    assert!(
993                        foot.distance(exact) <= T.confusion(),
994                        "foot: seam={seam}, v={v}, offset={offset}: {foot:?} vs {exact:?}, error={}",
995                        foot.distance(exact)
996                    );
997                    let vertex = model.add_vertex(VertexData::new(query));
998                    let found = distance_between_shapes(
999                        &model,
1000                        &vertex,
1001                        &face,
1002                        ExtremaOptions::default(),
1003                        T,
1004                    )
1005                    .unwrap();
1006                    assert!(
1007                        (found.distance - offset.abs()).abs() <= T.confusion(),
1008                        "shape distance: seam={seam}, v={v}, offset={offset}: {}",
1009                        found.distance
1010                    );
1011                    assert!(found.pairs.iter().any(|pair| {
1012                        pair.point_a.distance(query) <= T.confusion()
1013                            && pair.point_b.distance(exact) <= T.confusion()
1014                    }));
1015                }
1016            }
1017        }
1018    }
1019
1020    #[test]
1021    fn parallel_box_walls_meet_at_the_gap_between_them() {
1022        let mut model = Model::new();
1023        let a = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
1024        let b = make_box(
1025            &mut model,
1026            frame_at(Point::new(5.0, 0.0, 0.0)),
1027            (2.0, 2.0, 2.0),
1028            T,
1029        )
1030        .unwrap();
1031        let found =
1032            distance_between_shapes(&model, &a.shape, &b.shape, ExtremaOptions::default(), T)
1033                .unwrap();
1034        assert!((found.distance - 3.0).abs() < 1e-9, "{}", found.distance);
1035        assert!(!found.pairs.is_empty());
1036        for pair in &found.pairs {
1037            assert!((pair.point_a.distance(pair.point_b) - found.distance).abs() < 1e-9);
1038        }
1039    }
1040
1041    #[test]
1042    fn diagonal_boxes_meet_corner_to_corner() {
1043        // Offset along all three axes: the nearest points are two vertices,
1044        // exactly the candidates the geometry level declines to invent.
1045        let mut model = Model::new();
1046        let a = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
1047        let b = make_box(
1048            &mut model,
1049            frame_at(Point::new(3.0, 3.0, 3.0)),
1050            (1.0, 1.0, 1.0),
1051            T,
1052        )
1053        .unwrap();
1054        let found =
1055            distance_between_shapes(&model, &a.shape, &b.shape, ExtremaOptions::default(), T)
1056                .unwrap();
1057        let exact = (3.0_f64 * 4.0).sqrt(); // corner (1,1,1) to corner (3,3,3)
1058        assert!((found.distance - exact).abs() < 1e-9);
1059        let pair = &found.pairs[0];
1060        assert!(pair.point_a.is_equal(Point::new(1.0, 1.0, 1.0), T));
1061        assert!(pair.point_b.is_equal(Point::new(3.0, 3.0, 3.0), T));
1062        assert_eq!(model.kind_of(&pair.support_a).unwrap(), ShapeType::Vertex);
1063        assert_eq!(model.kind_of(&pair.support_b).unwrap(), ShapeType::Vertex);
1064    }
1065
1066    #[test]
1067    fn a_sphere_over_a_box_measures_to_the_top_face() {
1068        let mut model = Model::new();
1069        let block = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 1.0), T).unwrap();
1070        let ball = make_sphere(&mut model, frame_at(Point::new(2.0, 2.0, 4.0)), 1.0, T).unwrap();
1071        let found = distance_between_shapes(
1072            &model,
1073            &block.shape,
1074            &ball.shape,
1075            ExtremaOptions::default(),
1076            T,
1077        )
1078        .unwrap();
1079        assert!((found.distance - 2.0).abs() < 1e-7, "{}", found.distance);
1080        let pair = &found.pairs[0];
1081        assert!(pair.point_a.is_equal(Point::new(2.0, 2.0, 1.0), T));
1082        assert!(pair.point_b.is_equal(Point::new(2.0, 2.0, 3.0), T));
1083    }
1084
1085    #[test]
1086    fn parallel_cylinders_meet_wall_to_wall() {
1087        // The nearest locus is a pair of facing rulings: a family at the
1088        // geometry level, a representative pair here, with the distance exact.
1089        let mut model = Model::new();
1090        let a = make_cylinder(&mut model, Frame::WORLD, 1.0, 4.0, T).unwrap();
1091        let b =
1092            make_cylinder(&mut model, frame_at(Point::new(5.0, 0.0, 0.0)), 1.0, 4.0, T).unwrap();
1093        let found =
1094            distance_between_shapes(&model, &a.shape, &b.shape, ExtremaOptions::default(), T)
1095                .unwrap();
1096        assert!((found.distance - 3.0).abs() < 1e-7, "{}", found.distance);
1097    }
1098
1099    #[test]
1100    fn touching_boxes_report_zero() {
1101        let mut model = Model::new();
1102        let a = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
1103        let b = make_box(
1104            &mut model,
1105            frame_at(Point::new(2.0, 0.0, 0.0)),
1106            (2.0, 2.0, 2.0),
1107            T,
1108        )
1109        .unwrap();
1110        let found =
1111            distance_between_shapes(&model, &a.shape, &b.shape, ExtremaOptions::default(), T)
1112                .unwrap();
1113        assert!(found.distance < 1e-9, "{}", found.distance);
1114    }
1115
1116    #[test]
1117    fn a_box_inside_a_box_measures_boundary_to_boundary() {
1118        // Containment is the classifier's question. Distance is between
1119        // boundaries, and the gap between nested walls is what comes back.
1120        let mut model = Model::new();
1121        let outer = make_box(&mut model, Frame::WORLD, (6.0, 6.0, 6.0), T).unwrap();
1122        let inner = make_box(
1123            &mut model,
1124            frame_at(Point::new(2.0, 2.0, 2.0)),
1125            (2.0, 2.0, 2.0),
1126            T,
1127        )
1128        .unwrap();
1129        let found = distance_between_shapes(
1130            &model,
1131            &outer.shape,
1132            &inner.shape,
1133            ExtremaOptions::default(),
1134            T,
1135        )
1136        .unwrap();
1137        assert!((found.distance - 2.0).abs() < 1e-9, "{}", found.distance);
1138    }
1139
1140    #[test]
1141    fn a_rotated_box_measures_edge_to_edge() {
1142        // Roll one box forty-five degrees about x and lift it: what faces the
1143        // top of the lower box is a single edge, and the nearest pair is that
1144        // edge against the top face.
1145        let mut model = Model::new();
1146        let a = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 1.0), T).unwrap();
1147        let tilted = Frame::new(
1148            Point::new(2.0, 2.0, 3.0),
1149            Direction::new(Vector::new(0.0, 1.0, 1.0), T).unwrap(),
1150            Direction::X,
1151            T,
1152        )
1153        .unwrap();
1154        let b = make_box(&mut model, tilted, (1.0, 1.0, 1.0), T).unwrap();
1155        let found =
1156            distance_between_shapes(&model, &a.shape, &b.shape, ExtremaOptions::default(), T)
1157                .unwrap();
1158        // The tilted box's lowest feature is the edge its roll brings down to
1159        // z = 3 - 1/sqrt(2), facing the top face at z = 1.
1160        let exact = 2.0 - core::f64::consts::FRAC_1_SQRT_2;
1161        assert!((found.distance - exact).abs() < 1e-7, "{}", found.distance);
1162    }
1163}