axiolid_measure/
exact_distance.rs

1//! Certified distance between the boundaries of two exact B-reps (#125, C18).
2//!
3//! # What is certified
4//!
5//! [`boundary_distance`] returns an interval `[lower, upper]` that contains
6//! the true minimum distance between the two boundaries, and two points, one
7//! on each boundary, exactly `upper` apart. Both ends are guarantees, not
8//! estimates:
9//!
10//! - `upper` is the distance between two points that lie on the boundaries:
11//!   points on edges, or surface points at parameters the face's domain is
12//!   certified to contain (the crate-private `Domain` classifier).
13//! - `lower` comes from bounding spheres that enclose every point of a patch
14//!   of a face's parameter domain, from Lipschitz bounds on the exact
15//!   surface, with a margin for rounding. Patches are dropped only when
16//!   certified to lie outside the face.
17//!
18//! A rule that compares a distance with a limit asks [`boundary_clearance`],
19//! which refines only until the interval clears the limit and otherwise says
20//! [`Clearance::Indeterminate`] -- a value within rounding of the limit is
21//! never reported as a pass or a fail.
22//!
23//! # Method
24//!
25//! Branch and bound over pairs of elements, one from each B-rep: face
26//! patches (a rectangle of a face's parameters) and edge spans. The pair
27//! with the smallest lower bound is refined by splitting its larger element.
28//! Refinement stops when the smallest remaining lower bound is within the
29//! requested accuracy of the best upper bound, or the step budget runs out;
30//! either way the interval returned is sound.
31//!
32//! Bounds are second order where the family allows: the range of `d . x`
33//! over a patch is exact for planes, cylinders, elliptical cylinders, cones,
34//! spheres and tori and for line, circle and ellipse edges, and `d` is taken
35//! along the line between centres and along each patch's normal. A face
36//! patch whose normals cannot point at the other element is dropped from
37//! that pair (`critical_possible`): the closest pair of two separated
38//! boundaries is critical on each face it lies inside or lies on an edge,
39//! and edges are elements of their own.
40//!
41//! # Convergence
42//!
43//! An isolated nearest pair (pole to pole, apex to sphere, a wall to a
44//! block face) closes to `1e-9` in well under a second. Where the nearest
45//! points form a whole line -- two parallel columns -- every slice along
46//! that line is a near-minimal pair and refinement slows; ask such cases for
47//! a looser accuracy, or use [`boundary_clearance`], which stops as soon as
48//! the limit is cleared. The step budget is fixed; when it runs out the
49//! interval returned is still sound, only wider.
50//!
51//! # Scope
52//!
53//! This is the distance between BOUNDARIES. Two solids that overlap measure
54//! the distance between their surfaces where they cross (zero), but a solid
55//! wholly inside another measures the gap between the two boundaries, not
56//! zero. Containment is a separate classification.
57
58use std::cmp::Reverse;
59use std::collections::BinaryHeap;
60
61use axiolid_brep::ExactBRep;
62use axiolid_core::{Point2, Point3, Scalar, Tolerance, Vec3};
63use axiolid_curve::Curve3;
64use axiolid_evaluate::evaluate3;
65use axiolid_evaluate::surface::{evaluate, normal};
66use axiolid_surface::Surface;
67
68use crate::exact::ExactMeasureError;
69use crate::exact_domain::Domain;
70
71/// Pairs refined before a query stops and reports what it has.
72const MAX_STEPS: usize = 400_000;
73
74/// An interval certain to contain the distance between two boundaries.
75#[derive(Debug, Clone, PartialEq)]
76pub struct DistanceBounds {
77    /// No two boundary points are closer than this.
78    pub lower: Scalar,
79    /// `point_a` and `point_b` are this far apart.
80    pub upper: Scalar,
81    /// A point on the first boundary.
82    pub point_a: Point3,
83    /// A point on the second boundary, `upper` from `point_a`.
84    pub point_b: Point3,
85}
86
87/// How a certified distance compares with a limit.
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum Clearance {
90    /// Certainly closer than the limit: `upper < limit`.
91    Below,
92    /// Certainly farther than the limit: `lower > limit`.
93    Above,
94    /// The interval still contains the limit.
95    Indeterminate,
96}
97
98impl DistanceBounds {
99    /// Compare with `limit` without rounding the interval to a verdict.
100    #[must_use]
101    pub fn against(&self, limit: Scalar) -> Clearance {
102        if self.upper < limit {
103            Clearance::Below
104        } else if self.lower > limit {
105            Clearance::Above
106        } else {
107            Clearance::Indeterminate
108        }
109    }
110}
111
112/// Distance between the boundaries of `a` and `b`, to within `accuracy`.
113///
114/// # Errors
115///
116/// A face whose domain cannot be bounded (an unbounded surface trimmed by a
117/// pcurve family the domain classifier does not split), an evaluation
118/// failure, or boundaries with no boundable edge or face.
119pub fn boundary_distance(
120    a: &ExactBRep,
121    b: &ExactBRep,
122    accuracy: Scalar,
123    tolerance: Tolerance,
124) -> Result<DistanceBounds, ExactMeasureError> {
125    let accuracy = accuracy.max(0.0);
126    search(a, b, tolerance, &mut |lower, upper| {
127        upper - lower <= accuracy
128    })
129}
130
131/// Refine the distance only until it clears `limit`.
132///
133/// # Errors
134///
135/// As [`boundary_distance`].
136pub fn boundary_clearance(
137    a: &ExactBRep,
138    b: &ExactBRep,
139    limit: Scalar,
140    tolerance: Tolerance,
141) -> Result<(DistanceBounds, Clearance), ExactMeasureError> {
142    let bounds = search(a, b, tolerance, &mut |lower, upper| {
143        upper < limit || lower > limit
144    })?;
145    let clearance = bounds.against(limit);
146    Ok((bounds, clearance))
147}
148
149/// What an element covers.
150#[derive(Debug, Clone, Copy)]
151enum Shape {
152    /// A rectangle of face `face`'s parameters; `inside` once certified to
153    /// lie wholly in the face.
154    Face {
155        face: usize,
156        lo: Point2,
157        hi: Point2,
158        inside: bool,
159    },
160    /// A span of edge `edge`'s curve.
161    Edge { edge: usize, t0: Scalar, t1: Scalar },
162}
163
164/// A piece of one boundary with a sphere that encloses it.
165#[derive(Debug, Clone, Copy)]
166struct Element {
167    shape: Shape,
168    centre: Point3,
169    radius: Scalar,
170    /// A point certainly on the boundary, when one is known.
171    witness: Option<Point3>,
172    /// The surface normal at the centre of a face patch.
173    normal: Option<Vec3>,
174    /// Half-angle of a cone about `normal` holding every normal of the
175    /// patch, when the patch may be dropped from a pair whose directions
176    /// its normals cannot meet (see [`critical_possible`]).
177    spread: Option<Scalar>,
178}
179
180/// One B-rep, prepared for bounding.
181struct Side<'a> {
182    brep: &'a ExactBRep,
183    domains: Vec<Option<Domain<'a>>>,
184    /// Whether every edge on the face's boundary is an element, so a
185    /// closest point on that boundary is found through the edges.
186    edges_bounded: Vec<bool>,
187    elements: Vec<Element>,
188}
189
190impl<'a> Side<'a> {
191    fn new(brep: &'a ExactBRep, linear: Scalar) -> Result<Self, ExactMeasureError> {
192        let topology = brep.topology();
193        let mut side = Side {
194            brep,
195            domains: Vec::with_capacity(topology.faces().len()),
196            edges_bounded: Vec::with_capacity(topology.faces().len()),
197            elements: Vec::new(),
198        };
199        for face in topology.faces() {
200            let mut bounded = true;
201            for bound in &face.bounds {
202                let wire = topology
203                    .loops()
204                    .get(bound.loop_id.index())
205                    .ok_or(ExactMeasureError::DanglingReference)?;
206                for use_ in &wire.edges {
207                    let curve = topology.edges()[use_.edge.index()]
208                        .curve
209                        .and_then(|id| brep.curves3().get(id.index()));
210                    bounded &= matches!(
211                        curve,
212                        Some(Curve3::Line(_) | Curve3::Circle(_) | Curve3::Ellipse(_))
213                    );
214                }
215            }
216            side.edges_bounded.push(bounded);
217        }
218        for face in topology.faces() {
219            let surface = surface_of(brep, face.surface)?;
220            side.domains.push(Domain::new(brep, face, surface, linear)?);
221        }
222        for (index, face) in topology.faces().iter().enumerate() {
223            let surface = surface_of(brep, face.surface)?;
224            let (lo, hi) = match &side.domains[index] {
225                Some(domain) => (domain.min, domain.max),
226                None => natural_range(surface).ok_or(ExactMeasureError::NonPlanarFace(
227                    "an unbounded face trimmed by a pcurve family the distance query cannot bound",
228                ))?,
229            };
230            if let Some(element) = side.face_element(index, lo, hi, false)? {
231                side.elements.push(element);
232            }
233        }
234        for (index, edge) in topology.edges().iter().enumerate() {
235            let Some(curve) = edge.curve.and_then(|id| brep.curves3().get(id.index())) else {
236                continue;
237            };
238            let Some(span) = topology
239                .edge_id_at(index)
240                .and_then(|id| brep.edge_interval(id))
241            else {
242                continue;
243            };
244            if let Some(element) = edge_element(curve, index, span.start, span.end)? {
245                side.elements.push(element);
246            }
247        }
248        if side.elements.is_empty() {
249            return Err(ExactMeasureError::Degenerate);
250        }
251        Ok(side)
252    }
253
254    /// A face patch, or `None` when it is certified to lie outside the face.
255    fn face_element(
256        &self,
257        face: usize,
258        lo: Point2,
259        hi: Point2,
260        inside: bool,
261    ) -> Result<Option<Element>, ExactMeasureError> {
262        let topology = self.brep.topology();
263        let surface = surface_of(self.brep, topology.faces()[face].surface)?;
264        let mid = (lo + hi) * 0.5;
265        let mut inside = inside;
266        if !inside {
267            if let Some(domain) = &self.domains[face] {
268                if !domain.touches(lo, hi)? {
269                    match domain.contains(mid)? {
270                        Some(false) => return Ok(None),
271                        Some(true) => inside = true,
272                        None => {}
273                    }
274                }
275            }
276        }
277        let (centre, radius) = patch_sphere(surface, lo, hi)?;
278        let witness = if inside {
279            Some(point_on(surface, mid)?)
280        } else {
281            None
282        };
283        let normal = normal(surface, mid.x, mid.y).ok();
284        let spread = if self.edges_bounded[face] {
285            normal_spread(surface, lo, hi)
286        } else {
287            None
288        };
289        Ok(Some(Element {
290            normal,
291            spread,
292            shape: Shape::Face {
293                face,
294                lo,
295                hi,
296                inside,
297            },
298            centre,
299            radius,
300            witness,
301        }))
302    }
303
304    /// The element's two halves, less any certified outside the face: a
305    /// face patch across its metrically longer side, an edge span in the
306    /// middle.
307    fn split(&self, element: &Element) -> Result<Vec<Element>, ExactMeasureError> {
308        match element.shape {
309            Shape::Face {
310                face,
311                lo,
312                hi,
313                inside,
314            } => {
315                let surface = surface_of(self.brep, self.brep.topology().faces()[face].surface)?;
316                let (lu, lv) = lipschitz(surface, lo, hi)?;
317                let mid = (lo + hi) * 0.5;
318                let halves = if (hi.x - lo.x).abs() * lu >= (hi.y - lo.y).abs() * lv {
319                    [
320                        (lo, Point2::new(mid.x, hi.y)),
321                        (Point2::new(mid.x, lo.y), hi),
322                    ]
323                } else {
324                    [
325                        (lo, Point2::new(hi.x, mid.y)),
326                        (Point2::new(lo.x, mid.y), hi),
327                    ]
328                };
329                let mut out = Vec::with_capacity(2);
330                for (lo, hi) in halves {
331                    if let Some(child) = self.face_element(face, lo, hi, inside)? {
332                        out.push(child);
333                    }
334                }
335                Ok(out)
336            }
337            Shape::Edge { edge, t0, t1 } => {
338                let curve = self.brep.topology().edges()[edge]
339                    .curve
340                    .and_then(|id| self.brep.curves3().get(id.index()))
341                    .ok_or(ExactMeasureError::DanglingReference)?;
342                let tm = 0.5 * (t0 + t1);
343                let mut out = Vec::with_capacity(2);
344                for (a, b) in [(t0, tm), (tm, t1)] {
345                    if let Some(child) = edge_element(curve, edge, a, b)? {
346                        out.push(child);
347                    }
348                }
349                Ok(out)
350            }
351        }
352    }
353}
354
355fn surface_of(
356    brep: &ExactBRep,
357    id: Option<axiolid_brep::SurfaceId>,
358) -> Result<&Surface, ExactMeasureError> {
359    let id = id.ok_or(ExactMeasureError::MissingSurface)?;
360    brep.surfaces()
361        .get(id.index())
362        .ok_or(ExactMeasureError::DanglingReference)
363}
364
365fn point_on(surface: &Surface, at: Point2) -> Result<Point3, ExactMeasureError> {
366    evaluate(surface, at.x, at.y).map_err(|_| crate::exact::EVALUATION)
367}
368
369/// The whole parameter range of a closed surface, for a face whose trim
370/// cannot be classified.
371fn natural_range(surface: &Surface) -> Option<(Point2, Point2)> {
372    use core::f64::consts::{FRAC_PI_2, TAU};
373    match surface {
374        Surface::Sphere(_) => Some((Point2::new(0.0, -FRAC_PI_2), Point2::new(TAU, FRAC_PI_2))),
375        Surface::Torus(_) => Some((Point2::ZERO, Point2::new(TAU, TAU))),
376        _ => None,
377    }
378}
379
380/// Largest length a frame axis carries (1 for an orthonormal frame).
381fn frame_scale(frame: &axiolid_core::Frame3) -> Scalar {
382    frame.x.length().max(frame.y.length()).max(frame.z.length())
383}
384
385/// Bounds on `|S_u|` and `|S_v|` over the patch.
386fn lipschitz(
387    surface: &Surface,
388    lo: Point2,
389    hi: Point2,
390) -> Result<(Scalar, Scalar), ExactMeasureError> {
391    let bounds = match surface {
392        Surface::Plane(p) => (p.frame.x.length(), p.frame.y.length()),
393        Surface::Cylinder(c) => (c.radius.abs() * frame_scale(&c.frame), c.frame.z.length()),
394        Surface::EllipticalCylinder(c) => (
395            c.semi_axis_x.abs().max(c.semi_axis_y.abs()) * frame_scale(&c.frame),
396            c.frame.z.length(),
397        ),
398        Surface::Cone(c) => {
399            let slope = c.semi_angle.tan();
400            let radius = (c.radius + lo.y * slope)
401                .abs()
402                .max((c.radius + hi.y * slope).abs());
403            let scale = frame_scale(&c.frame);
404            (radius * scale, scale * (1.0 + slope * slope).sqrt())
405        }
406        Surface::Sphere(s) => {
407            // |S_u| = r cos v, which vanishes at the poles: bounding it by r
408            // would split near-polar patches round the pole for nothing.
409            let r = s.radius.abs() * frame_scale(&s.frame);
410            (r * max_cos(lo.y, hi.y), r)
411        }
412        Surface::Torus(t) => {
413            let scale = frame_scale(&t.frame);
414            (
415                (t.major_radius.abs() + t.minor_radius.abs() * max_cos(lo.y, hi.y)) * scale,
416                t.minor_radius.abs() * scale,
417            )
418        }
419        // A B-spline patch is bounded by its control net instead.
420        Surface::BSpline(_) => (0.0, 0.0),
421        _ => {
422            return Err(ExactMeasureError::NonPlanarFace(crate::exact::family(
423                surface,
424            )))
425        }
426    };
427    if bounds.0.is_finite() && bounds.1.is_finite() {
428        Ok(bounds)
429    } else {
430        Err(crate::exact::EVALUATION)
431    }
432}
433
434/// Largest `|cos v|` over `[a, b]`.
435fn max_cos(a: Scalar, b: Scalar) -> Scalar {
436    let (a, b) = (a.min(b), a.max(b));
437    let k = (a / core::f64::consts::PI).ceil();
438    if k * core::f64::consts::PI <= b {
439        1.0
440    } else {
441        a.cos().abs().max(b.cos().abs())
442    }
443}
444
445/// A sphere enclosing every surface point of the patch.
446///
447/// From the patch centre, any point is reached by a path along `u` then
448/// along `v`, no longer than `du/2 |S_u|max + dv/2 |S_v|max`. A B-spline
449/// patch lies in the convex hull of the surface's control net (positive
450/// weights), which is not refined but is sound.
451fn patch_sphere(
452    surface: &Surface,
453    lo: Point2,
454    hi: Point2,
455) -> Result<(Point3, Scalar), ExactMeasureError> {
456    if let Surface::BSpline(spline) = surface {
457        if let Some(weights) = &spline.weights {
458            if weights.iter().flatten().any(|w| w.is_nan() || *w <= 0.0) {
459                return Err(ExactMeasureError::NonPlanarFace(
460                    "non-positive-weight B-spline",
461                ));
462            }
463        }
464        let points: Vec<Point3> = spline.control_points.iter().flatten().copied().collect();
465        if points.is_empty() {
466            return Err(ExactMeasureError::Degenerate);
467        }
468        let (mut min, mut max) = (points[0], points[0]);
469        for p in &points {
470            min = min.min(*p);
471            max = max.max(*p);
472        }
473        let centre = (min + max) * 0.5;
474        let radius = points
475            .iter()
476            .map(|p| (*p - centre).length())
477            .fold(0.0, Scalar::max);
478        return Ok((centre, pad(centre, radius)));
479    }
480    let centre = point_on(surface, (lo + hi) * 0.5)?;
481    let (lu, lv) = lipschitz(surface, lo, hi)?;
482    let radius = 0.5 * ((hi.x - lo.x).abs() * lu + (hi.y - lo.y).abs() * lv);
483    Ok((centre, pad(centre, radius)))
484}
485
486/// Widen a bounding radius by the rounding its centre may carry.
487fn pad(centre: Point3, radius: Scalar) -> Scalar {
488    radius + 1e-12 * (centre.length() + radius) + Scalar::MIN_POSITIVE
489}
490
491/// An edge span with its enclosing sphere and its midpoint as a witness,
492/// or `None` for a curve family with no derivative bound here.
493fn edge_element(
494    curve: &Curve3,
495    edge: usize,
496    t0: Scalar,
497    t1: Scalar,
498) -> Result<Option<Element>, ExactMeasureError> {
499    let speed = match curve {
500        Curve3::Line(line) => line.direction.length(),
501        Curve3::Circle(circle) => circle.radius.abs() * frame_scale(&circle.frame),
502        Curve3::Ellipse(ellipse) => {
503            ellipse.semi_axis_x.abs().max(ellipse.semi_axis_y.abs()) * frame_scale(&ellipse.frame)
504        }
505        _ => return Ok(None),
506    };
507    let centre = evaluate3(curve, 0.5 * (t0 + t1)).map_err(|_| crate::exact::EVALUATION)?;
508    let radius = pad(centre, 0.5 * (t1 - t0).abs() * speed);
509    Ok(Some(Element {
510        normal: None,
511        spread: None,
512        shape: Shape::Edge { edge, t0, t1 },
513        centre,
514        radius,
515        witness: Some(centre),
516    }))
517}
518
519/// Half-angle bound on how far the surface normal turns across the patch.
520fn normal_spread(surface: &Surface, lo: Point2, hi: Point2) -> Option<Scalar> {
521    let (du, dv) = ((hi.x - lo.x).abs(), (hi.y - lo.y).abs());
522    let spread = match surface {
523        Surface::Plane(_) => 0.0,
524        // The normal turns with u at unit rate on a circular section.
525        Surface::Cylinder(_) => 0.5 * du,
526        Surface::Cone(c) => {
527            // The apex is not smooth: a nearest point there is neither
528            // critical nor on an edge, so a patch reaching it is never
529            // dropped.
530            let slope = c.semi_angle.tan();
531            let apex = -c.radius / slope;
532            if !apex.is_finite() || (apex >= lo.y.min(hi.y) - 1e-9 && apex <= lo.y.max(hi.y) + 1e-9)
533            {
534                return None;
535            }
536            0.5 * du
537        }
538        // On an ellipse it turns at most max/min times faster.
539        Surface::EllipticalCylinder(c) => {
540            let (a, b) = (c.semi_axis_x.abs(), c.semi_axis_y.abs());
541            0.5 * du * a.max(b) / a.min(b)
542        }
543        Surface::Sphere(_) | Surface::Torus(_) => 0.5 * (du + dv),
544        _ => return None,
545    };
546    spread.is_finite().then_some(spread + 1e-9)
547}
548
549/// Whether some pair of points of the two elements can be critical for
550/// the distance on the face side: the segment joining them along the
551/// surface normal there.
552///
553/// A closest pair of points of two separated boundaries is either critical
554/// on each face it lies inside, or lies on an edge -- and every edge is an
555/// element of its own. So a face patch whose normals cannot meet any
556/// direction towards the other element holds no closest point that the
557/// edges do not already hold, and the pair can be dropped. This is what
558/// lets the bound close where the nearest points run along an edge: the
559/// patches straddling that edge also hold points just outside the face,
560/// closer than the true distance, that no bound on the patch can exclude.
561fn critical_possible(face: &Element, other: &Element) -> bool {
562    let (Some(normal), Some(spread)) = (face.normal, face.spread) else {
563        return true;
564    };
565    let offset = other.centre - face.centre;
566    let gap = offset.length();
567    let reach = face.radius + other.radius;
568    if gap.is_nan() || gap <= reach {
569        return true;
570    }
571    let aperture = (reach / gap).asin();
572    let angle = spread + aperture + 1e-9;
573    if angle >= core::f64::consts::FRAC_PI_2 {
574        return true;
575    }
576    (offset / gap).dot(normal).abs() >= angle.cos() * normal.length()
577}
578
579/// Range of `a cos t + b sin t` over `[t0, t1]`.
580fn trig_range(a: Scalar, b: Scalar, t0: Scalar, t1: Scalar) -> (Scalar, Scalar) {
581    let (t0, t1) = (t0.min(t1), t0.max(t1));
582    let f = |t: Scalar| a * t.cos() + b * t.sin();
583    let (mut lo, mut hi) = (f(t0).min(f(t1)), f(t0).max(f(t1)));
584    let amplitude = a.hypot(b);
585    let peak = b.atan2(a);
586    let reaches = |angle: Scalar| {
587        let k = ((t0 - angle) / core::f64::consts::TAU).ceil();
588        angle + k * core::f64::consts::TAU <= t1
589    };
590    if reaches(peak) {
591        hi = amplitude;
592    }
593    if reaches(peak + core::f64::consts::PI) {
594        lo = -amplitude;
595    }
596    (lo, hi)
597}
598
599/// Range of `d . x` over an element's enclosing sphere.
600fn sphere_range(element: &Element, d: Vec3) -> (Scalar, Scalar) {
601    let c = element.centre.dot(d);
602    (c - element.radius, c + element.radius)
603}
604
605impl Side<'_> {
606    /// Exact range of `d . x` over the element where the family allows,
607    /// else the enclosing sphere's.
608    fn project(&self, element: &Element, d: Vec3) -> Result<(Scalar, Scalar), ExactMeasureError> {
609        let sphere = sphere_range(element, d);
610        let exact = match element.shape {
611            Shape::Face { face, lo, hi, .. } => {
612                let surface = surface_of(self.brep, self.brep.topology().faces()[face].surface)?;
613                match surface {
614                    Surface::Plane(p) => {
615                        let base = p.frame.origin.dot(d);
616                        let (x, y) = (p.frame.x.dot(d), p.frame.y.dot(d));
617                        let values = [
618                            base + x * lo.x + y * lo.y,
619                            base + x * hi.x + y * lo.y,
620                            base + x * lo.x + y * hi.y,
621                            base + x * hi.x + y * hi.y,
622                        ];
623                        Some(
624                            values
625                                .iter()
626                                .fold((Scalar::INFINITY, Scalar::NEG_INFINITY), |(a, b), v| {
627                                    (a.min(*v), b.max(*v))
628                                }),
629                        )
630                    }
631                    Surface::Cylinder(c) => {
632                        let (a, b) = trig_range(
633                            c.radius * c.frame.x.dot(d),
634                            c.radius * c.frame.y.dot(d),
635                            lo.x,
636                            hi.x,
637                        );
638                        let z = c.frame.z.dot(d);
639                        let base = c.frame.origin.dot(d);
640                        Some((
641                            base + a + (z * lo.y).min(z * hi.y),
642                            base + b + (z * lo.y).max(z * hi.y),
643                        ))
644                    }
645                    Surface::EllipticalCylinder(c) => {
646                        let (a, b) = trig_range(
647                            c.semi_axis_x * c.frame.x.dot(d),
648                            c.semi_axis_y * c.frame.y.dot(d),
649                            lo.x,
650                            hi.x,
651                        );
652                        let z = c.frame.z.dot(d);
653                        let base = c.frame.origin.dot(d);
654                        Some((
655                            base + a + (z * lo.y).min(z * hi.y),
656                            base + b + (z * lo.y).max(z * hi.y),
657                        ))
658                    }
659                    Surface::Cone(c) => {
660                        // Linear in v for fixed u: the extremes sit on the
661                        // two v edges of the rectangle.
662                        let slope = c.semi_angle.tan();
663                        let base = c.frame.origin.dot(d);
664                        let (x, y, z) = (c.frame.x.dot(d), c.frame.y.dot(d), c.frame.z.dot(d));
665                        let mut range = (Scalar::INFINITY, Scalar::NEG_INFINITY);
666                        for v in [lo.y, hi.y] {
667                            let r = c.radius + v * slope;
668                            let (a, b) = trig_range(r * x, r * y, lo.x, hi.x);
669                            range = (range.0.min(base + a + z * v), range.1.max(base + b + z * v));
670                        }
671                        Some(range)
672                    }
673                    Surface::Sphere(sphere) => {
674                        // d.S = d.c + r (cos v W(u) + sin v Z), W linear in
675                        // the u-trig term: the extremes take W at its ends.
676                        let r = sphere.radius;
677                        let base = sphere.frame.origin.dot(d);
678                        let (w_lo, w_hi) =
679                            trig_range(sphere.frame.x.dot(d), sphere.frame.y.dot(d), lo.x, hi.x);
680                        let z = sphere.frame.z.dot(d);
681                        let mut range = (Scalar::INFINITY, Scalar::NEG_INFINITY);
682                        for w in [w_lo, w_hi] {
683                            let (a, b) = trig_range(r * w, r * z, lo.y, hi.y);
684                            range = (range.0.min(base + a), range.1.max(base + b));
685                        }
686                        Some(range)
687                    }
688                    Surface::Torus(torus) => {
689                        // d.S = d.c + R W(u) + r (cos v W(u) + sin v Z); for
690                        // R > r the coefficient of W is positive, so again W
691                        // takes its ends.
692                        let (big, small) = (torus.major_radius, torus.minor_radius);
693                        if big.is_nan() || big <= small.abs() {
694                            return Ok(sphere_range(element, d));
695                        }
696                        let base = torus.frame.origin.dot(d);
697                        let (w_lo, w_hi) =
698                            trig_range(torus.frame.x.dot(d), torus.frame.y.dot(d), lo.x, hi.x);
699                        let z = torus.frame.z.dot(d);
700                        let mut range = (Scalar::INFINITY, Scalar::NEG_INFINITY);
701                        for w in [w_lo, w_hi] {
702                            let (a, b) = trig_range(small * w, small * z, lo.y, hi.y);
703                            range = (
704                                range.0.min(base + big * w + a),
705                                range.1.max(base + big * w + b),
706                            );
707                        }
708                        Some(range)
709                    }
710                    _ => None,
711                }
712            }
713            Shape::Edge { edge, t0, t1 } => {
714                let curve = self.brep.topology().edges()[edge]
715                    .curve
716                    .and_then(|id| self.brep.curves3().get(id.index()))
717                    .ok_or(ExactMeasureError::DanglingReference)?;
718                match curve {
719                    Curve3::Line(line) => {
720                        let (a, b) = (
721                            (line.origin + line.direction * t0).dot(d),
722                            (line.origin + line.direction * t1).dot(d),
723                        );
724                        Some((a.min(b), a.max(b)))
725                    }
726                    Curve3::Circle(c) => {
727                        let (a, b) = trig_range(
728                            c.radius * c.frame.x.dot(d),
729                            c.radius * c.frame.y.dot(d),
730                            t0,
731                            t1,
732                        );
733                        let base = c.frame.origin.dot(d);
734                        Some((base + a, base + b))
735                    }
736                    Curve3::Ellipse(e) => {
737                        let (a, b) = trig_range(
738                            e.semi_axis_x * e.frame.x.dot(d),
739                            e.semi_axis_y * e.frame.y.dot(d),
740                            t0,
741                            t1,
742                        );
743                        let base = e.frame.origin.dot(d);
744                        Some((base + a, base + b))
745                    }
746                    _ => None,
747                }
748            }
749        };
750        Ok(match exact {
751            Some((lo, hi)) => {
752                // Rounding in the projection itself.
753                let pad =
754                    1e-12 * (element.centre.length() + element.radius + lo.abs().max(hi.abs()));
755                (lo.max(sphere.0) - pad, hi.min(sphere.1) + pad)
756            }
757            None => sphere,
758        })
759    }
760}
761
762/// The largest separation certified along the centre line or either
763/// patch's normal, and never less than the spheres give.
764fn lower_bound(
765    side_a: &Side<'_>,
766    a: &Element,
767    side_b: &Side<'_>,
768    b: &Element,
769) -> Result<Scalar, ExactMeasureError> {
770    let gap = (a.centre - b.centre).length();
771    let rounding = 1e-12 * (a.centre.length() + b.centre.length() + gap);
772    let mut best = (gap - a.radius - b.radius - rounding).max(0.0);
773    let directions = [
774        (gap > 0.0).then(|| (b.centre - a.centre) / gap),
775        a.normal,
776        b.normal,
777    ];
778    for d in directions.into_iter().flatten() {
779        let (a_lo, a_hi) = side_a.project(a, d)?;
780        let (b_lo, b_hi) = side_b.project(b, d)?;
781        best = best.max(b_lo - a_hi).max(a_lo - b_hi);
782    }
783    Ok(best)
784}
785
786/// Total order on finite lower bounds for the heap.
787#[derive(Debug, Clone, Copy, PartialEq)]
788struct Key(Scalar);
789
790impl Eq for Key {}
791
792impl PartialOrd for Key {
793    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
794        Some(self.cmp(other))
795    }
796}
797
798impl Ord for Key {
799    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
800        self.0.total_cmp(&other.0)
801    }
802}
803
804fn search(
805    a: &ExactBRep,
806    b: &ExactBRep,
807    tolerance: Tolerance,
808    done: &mut dyn FnMut(Scalar, Scalar) -> bool,
809) -> Result<DistanceBounds, ExactMeasureError> {
810    let linear = tolerance.linear().max(1e-12);
811    let side_a = Side::new(a, linear)?;
812    let side_b = Side::new(b, linear)?;
813    let mut elements_a = side_a.elements.clone();
814    let mut elements_b = side_b.elements.clone();
815
816    let mut best: Option<(Scalar, Point3, Point3)> = None;
817    let mut heap = BinaryHeap::new();
818    let viable = |a: &Element, b: &Element| critical_possible(a, b) && critical_possible(b, a);
819    for (i, ea) in elements_a.iter().enumerate() {
820        for (j, eb) in elements_b.iter().enumerate() {
821            if viable(ea, eb) {
822                heap.push(Reverse((Key(lower_bound(&side_a, ea, &side_b, eb)?), i, j)));
823            }
824        }
825    }
826
827    let mut lower = 0.0;
828    let mut steps = 0;
829    while let Some(Reverse((Key(bound), i, j))) = heap.pop() {
830        lower = bound;
831        let (ea, eb) = (elements_a[i], elements_b[j]);
832        if let (Some(wa), Some(wb)) = (ea.witness, eb.witness) {
833            let d = (wa - wb).length();
834            if best.is_none_or(|(current, _, _)| d < current) {
835                best = Some((d, wa, wb));
836            }
837        }
838        let upper = best.map_or(Scalar::INFINITY, |(d, _, _)| d);
839        if upper.is_finite() && (bound >= upper || done(bound, upper)) {
840            break;
841        }
842        steps += 1;
843        if steps > MAX_STEPS {
844            break;
845        }
846        // Refine the larger of the two; a pair that can shrink no further
847        // holds the lower bound where it is.
848        // Refine the larger of the two; a pair that can shrink no further
849        // holds the lower bound where it is.
850        let split_a = ea.radius >= eb.radius;
851        let children = if split_a {
852            side_a.split(&ea)?
853        } else {
854            side_b.split(&eb)?
855        };
856        let parent = if split_a { ea.radius } else { eb.radius };
857        if children.iter().any(|child| child.radius >= parent) {
858            heap.push(Reverse((Key(bound), i, j)));
859            break;
860        }
861        for child in children {
862            if split_a {
863                elements_a.push(child);
864                let index = elements_a.len() - 1;
865                if viable(&child, &eb) {
866                    heap.push(Reverse((
867                        Key(lower_bound(&side_a, &child, &side_b, &eb)?),
868                        index,
869                        j,
870                    )));
871                }
872            } else {
873                elements_b.push(child);
874                let index = elements_b.len() - 1;
875                if viable(&ea, &child) {
876                    heap.push(Reverse((
877                        Key(lower_bound(&side_a, &ea, &side_b, &child)?),
878                        i,
879                        index,
880                    )));
881                }
882            }
883        }
884    }
885    // Every pair still queued bounds from below what is left unexplored.
886    if let Some(Reverse((Key(bound), _, _))) = heap.peek() {
887        lower = lower.min(*bound);
888    }
889    let (upper, point_a, point_b) = best.ok_or(crate::exact::NOT_CONVERGED)?;
890    Ok(DistanceBounds {
891        lower: lower.min(upper),
892        upper,
893        point_a,
894        point_b,
895    })
896}
897
898#[cfg(test)]
899mod tests {
900    //! The two claims the lower bound rests on, checked by dense sampling:
901    //! every point of a patch lies in its sphere, and every normal lies in
902    //! its cone.
903
904    use super::{normal_spread, patch_sphere};
905    use axiolid_core::{Frame3, Point2, Point3, Vec3};
906    use axiolid_evaluate::surface::{evaluate, normal};
907    use axiolid_surface::{Cone, Cylinder, EllipticalCylinder, Plane, Sphere, Surface, Torus};
908
909    fn frame() -> Frame3 {
910        // Off the origin and turned, so no term vanishes by symmetry.
911        let x = Vec3::new(0.6, 0.8, 0.0);
912        let z = Vec3::new(0.0, 0.0, 1.0);
913        Frame3 {
914            origin: Point3::new(1.5, -2.0, 0.75),
915            x,
916            y: z.cross(x),
917            z,
918        }
919    }
920
921    fn families() -> Vec<(Surface, Point2, Point2)> {
922        let f = frame();
923        vec![
924            (
925                Surface::Plane(Plane { frame: f }),
926                Point2::new(-1.0, 0.5),
927                Point2::new(2.0, 1.25),
928            ),
929            (
930                Surface::Cylinder(Cylinder {
931                    frame: f,
932                    radius: 2.5,
933                }),
934                Point2::new(0.3, -1.0),
935                Point2::new(1.9, 2.0),
936            ),
937            (
938                Surface::EllipticalCylinder(EllipticalCylinder {
939                    frame: f,
940                    semi_axis_x: 3.0,
941                    semi_axis_y: 1.0,
942                }),
943                Point2::new(0.2, 0.0),
944                Point2::new(1.4, 1.0),
945            ),
946            (
947                Surface::Cone(Cone {
948                    frame: f,
949                    radius: 1.5,
950                    semi_angle: 0.4,
951                }),
952                Point2::new(0.5, 0.2),
953                Point2::new(2.5, 1.5),
954            ),
955            (
956                Surface::Sphere(Sphere {
957                    frame: f,
958                    radius: 2.0,
959                }),
960                Point2::new(0.4, -0.3),
961                Point2::new(2.0, 1.1),
962            ),
963            (
964                Surface::Torus(Torus {
965                    frame: f,
966                    major_radius: 3.0,
967                    minor_radius: 1.0,
968                }),
969                Point2::new(0.1, 0.5),
970                Point2::new(1.7, 2.9),
971            ),
972        ]
973    }
974
975    fn samples(lo: Point2, hi: Point2) -> impl Iterator<Item = Point2> {
976        const N: usize = 24;
977        (0..=N).flat_map(move |i| {
978            (0..=N).map(move |j| {
979                Point2::new(
980                    lo.x + (hi.x - lo.x) * i as f64 / N as f64,
981                    lo.y + (hi.y - lo.y) * j as f64 / N as f64,
982                )
983            })
984        })
985    }
986
987    #[test]
988    fn every_patch_point_lies_in_its_sphere() {
989        for (surface, lo, hi) in families() {
990            let (centre, radius) = patch_sphere(&surface, lo, hi).expect("bounded");
991            let mut reach: f64 = 0.0;
992            for p in samples(lo, hi) {
993                let point = evaluate(&surface, p.x, p.y).expect("point");
994                reach = reach.max((point - centre).length());
995            }
996            assert!(
997                reach <= radius,
998                "{surface:?}: reach {reach} > radius {radius}"
999            );
1000            // Not vacuous: the bound is within a small factor of the reach.
1001            assert!(
1002                radius <= 4.0 * reach + 1e-9,
1003                "{surface:?}: {radius} vs {reach}"
1004            );
1005        }
1006    }
1007
1008    #[test]
1009    fn every_patch_normal_lies_in_its_cone() {
1010        let mid = |lo: Point2, hi: Point2| (lo + hi) * 0.5;
1011        for (surface, lo, hi) in families() {
1012            let Some(spread) = normal_spread(&surface, lo, hi) else {
1013                continue;
1014            };
1015            let c = mid(lo, hi);
1016            let axis = normal(&surface, c.x, c.y).expect("normal");
1017            let mut widest: f64 = 0.0;
1018            for p in samples(lo, hi) {
1019                let n = normal(&surface, p.x, p.y).expect("normal");
1020                widest = widest.max(n.dot(axis).clamp(-1.0, 1.0).acos());
1021            }
1022            assert!(
1023                widest <= spread,
1024                "{surface:?}: normals turn {widest} > {spread}"
1025            );
1026        }
1027    }
1028}