Skip to main content

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 computed
7//! points, one on each boundary within its evaluation rounding, whose
8//! boundary points `upper` covers (#439). Both ends are guarantees, not
9//! estimates:
10//!
11//! - `upper` is at least the distance between two points that lie on the
12//!   boundaries, which the two computed witnesses stand for: points on
13//!   edges, or surface points at parameters the face's domain is certified
14//!   to contain (the crate-private `Domain` classifier). It is their
15//!   computed distance plus their rounding (see "Rounding").
16//! - `lower` comes from bounding spheres that enclose every point of a patch
17//!   of a face's parameter domain, from Lipschitz bounds on the exact
18//!   surface, with a margin for rounding. Patches are dropped only when
19//!   certified to lie outside the face.
20//!
21//! A rule that compares a distance with a limit asks [`boundary_clearance`],
22//! which refines only until the interval clears the limit and otherwise says
23//! [`Clearance::Indeterminate`] -- a value within rounding of the limit is
24//! never reported as a pass or a fail.
25//!
26//! # Method
27//!
28//! Branch and bound over pairs of elements, one from each B-rep: face
29//! patches (a rectangle of a face's parameters) and edge spans. The pair
30//! with the smallest lower bound is refined by splitting its larger element.
31//! Refinement stops when the smallest remaining lower bound is within the
32//! requested accuracy of the best upper bound, or the step budget runs out;
33//! either way the interval returned is sound.
34//!
35//! Bounds are second order where the family allows: the range of `d . x`
36//! over a patch is exact for planes, cylinders, elliptical cylinders, cones,
37//! spheres and tori and for line, circle and ellipse edges, and `d` is taken
38//! along the line between centres and along each patch's normal. A face
39//! patch whose normals cannot point at the other element is dropped from
40//! that pair (`critical_possible`): the closest pair of two separated
41//! boundaries is critical on each face it lies inside or lies on an edge,
42//! and edges are elements of their own.
43//!
44//! # Convergence
45//!
46//! An isolated nearest pair (pole to pole, apex to sphere, a wall to a
47//! block face) closes to `1e-9` in well under a second. Where the nearest
48//! points form a whole line -- two parallel columns -- every slice along
49//! that line is a near-minimal pair and refinement slows; ask such cases for
50//! a looser accuracy, or use [`boundary_clearance`], which stops as soon as
51//! the limit is cleared. The step budget is fixed; when it runs out the
52//! interval returned is still sound, only wider.
53//!
54//! Where a lower bound does not depend on a patch's size -- along a planar
55//! patch's own normal, against a parallel planar patch or a line edge -- the
56//! minimum is reached on a whole region and the bound is right from the
57//! start; what never closes is the upper bound, since patch centres line up
58//! only by chance. Such ties close on foot witnesses (`witness`): the foot,
59//! on a planar face, of the other element's witness, and the closest points
60//! of two line spans, each certified as any witness is (#318).
61//!
62//! # Touching and crossing boundaries
63//!
64//! Where the boundaries meet, the distance is zero, but witnesses at patch
65//! centres and edge midpoints land on the contact only by chance, so the
66//! refinement alone would close on it only when the patches round it were
67//! within the accuracy -- often not within the step budget. A pair popped
68//! with a lower bound of zero is therefore first asked for a point of each
69//! element that the two share (#273): the closest points of two line edges,
70//! or a point of a line edge and the point of a planar face at the same
71//! place, each evaluated on its own boundary as any witness is, the face's
72//! certified by the face's domain. Two such points no farther apart than
73//! the rounding margin (128 ulps of the points' magnitudes: about `6e-14`
74//! at unit scale) end the search with `[0, d']`, `d'` their distance plus
75//! their rounding (#439), they being the witnesses: no lower bound can
76//! rise above zero there, so this is the interval the search would only
77//! converge to. Two boxes sharing a face, or crossing, stop within a few
78//! steps. A gap wider than that margin is never taken for a touch, and
79//! boundaries that are apart measure exactly as
80//! before. Curved faces and circle or ellipse edges show no touch this way
81//! and are refined as before.
82//!
83//! # Rounding (#318)
84//!
85//! Every margin is derived from the magnitudes the computed numbers are
86//! summed from, in units of `eps` (`f64::EPSILON`, twice the unit
87//! roundoff), not from `1e-12` of the points' magnitudes, which left an
88//! interval about `1e-5` wide at georeferenced coordinates (450000,
89//! 5600000), where a point cannot be computed to better than a few `1e-9`.
90//!
91//! `M` is the summed magnitude of the terms a point of an element is
92//! computed from, taken at the element's farthest parameters. It never
93//! grows when an element is split, since a child's rectangle or span lies
94//! in its parent's:
95//!
96//! | element | `M` |
97//! |---|---|
98//! | plane patch | `|o| + |u| Lu + |v| Lv` |
99//! | cylinder patch | `|o| + 2 |r| s + |u| Lu + |v| Lv` |
100//! | elliptical cylinder patch | `|o| + (|a| + |b|) s + ...` |
101//! | cone patch | `|o| + 2 (|r| + |v| tan(angle)) s + ...` |
102//! | sphere patch | `|o| + 3 |r| s + ...` |
103//! | torus patch | `|o| + (2 R + 3 r) s + ...` |
104//! | B-spline patch (sphere) | the largest control point |
105//! | B-spline patch (witness) | `(p_u + p_v + 1) (w_max / w_min) max|P|` |
106//! | line span | `|o| + |t| |d|` |
107//! | circle span | `|o| + (2 + |t|) |r| s` |
108//! | ellipse span | `|o| + (|a| + |b| + |t| max(|a|, |b|)) s` |
109//!
110//! where `o` is the frame origin, `s` the frame's scale, `Lu` and `Lv` the
111//! Lipschitz bounds of the parameters. A side's scale `S` is the largest
112//! `M` over its root elements, and every element of that side is padded by
113//! it. The pad is therefore the same for every element, so it cannot
114//! reorder the queue: a pair's lower bound is its geometric bound less a
115//! constant of the whole search, and the rounding floor enters once.
116//!
117//! The margins, with `u = eps / 2` the unit roundoff and `gamma_n` the usual
118//! `n u / (1 - n u)`:
119//!
120//! - `POINT` (16): a computed centre or witness against the exact point.
121//!   A coordinate sums at most twelve rounded terms of magnitude at most
122//!   `M` (`gamma_12 = 6 eps`), a vector's length takes a factor `sqrt 3`
123//!   (10.4 `eps`), and the midpoint of the parameters and a cone's apex add
124//!   the rest.
125//! - `PROJECTION` (8): each end of a projected range `d . x`: a sum of
126//!   rounded products of terms at most `M` against a unit direction.
127//! - `RATIO` (8): relative: lengths, sums, differences, and directions that
128//!   are unit only to rounding.
129//! - `FLOOR` (128): the width a search settles at, in `eps (S_a + S_b)`.
130//!   At zero size the computed bound may lie `2 (POINT + PROJECTION) eps
131//!   (S_a + S_b) = 48 eps (S_a + S_b)` below the exact one, and the
132//!   witnesses' own evaluation adds up to `32 eps (S_a + S_b)`: 80, with
133//!   room to spare.
134//!
135//! A witness is a point evaluated at a parameter, so it is off its boundary
136//! by `POINT eps W`, `W` the side's witness scale: the largest witness `M`
137//! over its root elements, equal to `S` unless the side has a B-spline face.
138//! The reported upper bound is
139//! `|pa - pb| (1 + RATIO eps) + POINT eps (W_a + W_b)`, so it is at least
140//! the distance between the boundary points the witnesses stand for, and
141//! `Clearance::Below` is never claimed within rounding of a limit the
142//! distance does not go below (#439). The search order and the witnesses
143//! are those of the computed distance; only the stop test and the reported
144//! value widen. A plan overlap shown by two planar faces still reports
145//! `[0, 0]`. The floor adds `POINT eps ((W_a - S_a) + (W_b - S_b))`, nothing
146//! without B-spline faces.
147//!
148//! A B-spline sphere's `M` is its control points, since its patch is
149//! enclosed by them, but a witness runs `p_u + p_v` levels of de Boor in
150//! homogeneous coordinates and divides by the weight. Each level adds at
151//! most `4 eps` of the largest homogeneous coordinate `w_max |P|max`; the
152//! division by a weight of at least `w_min` gives
153//! `((8L + 0.5) rho + 0.5) eps |P|max` per coordinate (`L = p_u + p_v`,
154//! `rho = w_max / w_min`). Times `sqrt 3` that is below
155//! `16 (L + 1) rho eps |P|max`, which is `POINT eps M` for the witness `M`
156//! of the table. This is first order in `L rho eps` (the division is by the
157//! computed weight sum, off by up to `4 L eps w_max`), so a weight ratio
158//! above `1 / (64 L eps)` is refused, as is a non-positive weight.
159//!
160//! The `RATIO` guards of the pair and point bounds and a point query's own
161//! pad `PROJECTION eps |p|` cover rounding that another margin covers too
162//! (#447): `S` is at least the magnitude of every point of its side, so a
163//! computed gap or projection is off by a few `eps (S_a + S_b)` at most,
164//! inside the side pads. They stay, so each bound is sound by its own
165//! arithmetic, but no test can observe their loss.
166//!
167//! The libm functions the evaluators call are assumed within one ulp.
168//! The floor is about `3.2e-7` at (450000, 5600000) and about `6e-14` at
169//! unit scale. It only stops refinement and changes neither bound; an
170//! accuracy below it settles at it and is never reported as met, so the
171//! width of the interval returned shows what was reached.
172//!
173//! # In plan
174//!
175//! [`plan_boundary_distance`] measures between the bodies' projections
176//! onto the XY plane (#217). A solid's shadow is its boundary's, so the
177//! same search applies with every gap, direction and enclosing disc
178//! horizontal: elements carry a plan radius from their horizontal speeds,
179//! lower bounds project onto horizontal directions only (a normal by its
180//! horizontal part), and a face patch is dropped only where no normal of
181//! it is horizontal and along the plan offset. [`plan_overlap`] shows two
182//! shadows overlap over a patch of positive area from two planar faces,
183//! not vertical, whose shadows share an open patch, and shows them apart
184//! once the lower bound is positive. When the distance search shows
185//! neither, a second search over patches of such faces alone, coarsest
186//! pair first, looks for the overlap (#237): in the first, pairs of walls
187//! and edges whose shadows merely cross take the budget, and a plan
188//! distance of zero met between two boundary points ends it.
189//!
190//! # Scope
191//!
192//! This is the distance between BOUNDARIES. Two solids that overlap measure
193//! the distance between their surfaces where they cross (zero), but a solid
194//! wholly inside another measures the gap between the two boundaries, not
195//! zero. Containment is a separate classification.
196
197use std::cmp::Reverse;
198use std::collections::BinaryHeap;
199
200use axiolid_brep::ExactBRep;
201use axiolid_core::{Point2, Point3, Scalar, Tolerance, Vec3};
202use axiolid_curve::Curve3;
203use axiolid_evaluate::evaluate3;
204use axiolid_evaluate::surface::{evaluate, normal};
205use axiolid_surface::Surface;
206
207use crate::exact::ExactMeasureError;
208use crate::exact_domain::Domain;
209
210mod touch;
211pub(crate) mod witness;
212
213/// Pairs refined before a query stops and reports what it has.
214const MAX_STEPS: usize = 400_000;
215
216/// Pairs refined before [`plan_overlap`] reports it could not decide.
217pub(crate) const OVERLAP_STEPS: usize = 20_000;
218
219/// Rounding margins, in units of `eps * S`, where `S` is a side's rounding
220/// scale: the largest summed magnitude `M` of the terms an element's points
221/// are computed from (see "Rounding" in the module docs for the derivations).
222///
223/// A computed centre or witness against the exact point it stands for.
224pub(crate) const POINT_EPSILONS: Scalar = 16.0;
225/// Each end of a projected range `d . x`.
226pub(crate) const PROJECTION_EPSILONS: Scalar = 8.0;
227/// Relative: lengths, sums, differences and directions that are unit only
228/// to rounding.
229pub(crate) const RATIO_EPSILONS: Scalar = 8.0;
230/// The width an interval settles at, in units of `eps * (S_a + S_b)`.
231pub(crate) const FLOOR_EPSILONS: Scalar = 128.0;
232
233/// An interval certain to contain the distance between two boundaries,
234/// and two witnesses. `point_a` and `point_b` are computed points, each
235/// within its evaluation rounding of a point of its boundary
236/// (`POINT eps W`, see "Rounding" in the module docs), and `upper` is at
237/// least the distance between those two boundary points (#439): the
238/// computed `|point_a - point_b|` plus that rounding, never less. In plan,
239/// an overlap shown by two planar faces reports `[0, 0]`.
240#[derive(Debug, Clone, PartialEq)]
241pub struct DistanceBounds {
242    /// No two boundary points are closer than this.
243    pub lower: Scalar,
244    /// The boundary points `point_a` and `point_b` stand for are at most
245    /// this far apart (in plan, for the plan queries).
246    pub upper: Scalar,
247    /// A computed point within rounding of the first boundary.
248    pub point_a: Point3,
249    /// A computed point within rounding of the second boundary.
250    pub point_b: Point3,
251}
252
253/// How a certified distance compares with a limit.
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum Clearance {
256    /// Certainly closer than the limit: `upper < limit`, `upper` covering
257    /// the witnesses' rounding.
258    Below,
259    /// Certainly farther than the limit: `lower > limit`.
260    Above,
261    /// The interval still contains the limit.
262    Indeterminate,
263}
264
265impl DistanceBounds {
266    /// Compare with `limit` without rounding the interval to a verdict.
267    #[must_use]
268    pub fn against(&self, limit: Scalar) -> Clearance {
269        if self.upper < limit {
270            Clearance::Below
271        } else if self.lower > limit {
272            Clearance::Above
273        } else {
274            Clearance::Indeterminate
275        }
276    }
277}
278
279/// Distance between the boundaries of `a` and `b`, to within `accuracy`.
280///
281/// An accuracy below the rounding floor settles at the floor and is never
282/// reported as met: the interval returned shows its width (see "Rounding"
283/// in the module docs).
284///
285/// # Errors
286///
287/// A face whose domain cannot be bounded (an unbounded surface trimmed by a
288/// pcurve family the domain classifier does not split), an evaluation
289/// failure, or boundaries with no boundable edge or face.
290pub fn boundary_distance(
291    a: &ExactBRep,
292    b: &ExactBRep,
293    accuracy: Scalar,
294    tolerance: Tolerance,
295) -> Result<DistanceBounds, ExactMeasureError> {
296    let accuracy = accuracy.max(0.0);
297    Ok(search(
298        a,
299        b,
300        tolerance,
301        Metric::Space,
302        &mut |lower, upper, floor| closed(lower, upper, accuracy, floor),
303    )?
304    .bounds)
305}
306
307/// Distance between the plan projections of `a` and `b` -- their shadows
308/// on the XY plane -- to within `accuracy` (#217).
309///
310/// A solid's shadow is its boundary's: a vertical line through a point of
311/// the solid meets the boundary. So this is the least horizontal distance
312/// between two boundary points, found by the same certified search as
313/// [`boundary_distance`] with horizontal gaps, directions and enclosing
314/// discs. It is zero when the shadows overlap, and unlike the distance in
315/// space it is zero for a body standing inside another's footprint.
316/// `point_a` and `point_b` lie within rounding of the boundaries, their
317/// projections at most `upper` apart. When two planar faces show the
318/// shadows overlap over a patch of positive area, the interval is `[0, 0]`
319/// and the points project into that patch (see [`plan_overlap`]).
320///
321/// # Errors
322///
323/// As [`boundary_distance`].
324pub fn plan_boundary_distance(
325    a: &ExactBRep,
326    b: &ExactBRep,
327    accuracy: Scalar,
328    tolerance: Tolerance,
329) -> Result<DistanceBounds, ExactMeasureError> {
330    let accuracy = accuracy.max(0.0);
331    Ok(
332        search(a, b, tolerance, Metric::Plan, &mut |lower, upper, floor| {
333            closed(lower, upper, accuracy, floor)
334        })?
335        .bounds,
336    )
337}
338
339/// [`plan_boundary_distance`] refined only until it clears `limit`.
340///
341/// # Errors
342///
343/// As [`boundary_distance`].
344pub fn plan_boundary_clearance(
345    a: &ExactBRep,
346    b: &ExactBRep,
347    limit: Scalar,
348    tolerance: Tolerance,
349) -> Result<(DistanceBounds, Clearance), ExactMeasureError> {
350    let bounds = search(a, b, tolerance, Metric::Plan, &mut |lower, upper, floor| {
351        upper < limit || lower > limit || closed(lower, upper, 0.0, floor)
352    })?
353    .bounds;
354    let clearance = bounds.against(limit);
355    Ok((bounds, clearance))
356}
357
358/// Whether the plan projections of two bodies overlap.
359#[derive(Debug, Clone, Copy, PartialEq)]
360pub enum PlanOverlap {
361    /// Over a patch of positive area: `at` and a disc around it lie in
362    /// both shadows, shown by a planar face of each that is not vertical.
363    Overlapping {
364        /// A plan point inside both shadows.
365        at: Point2,
366    },
367    /// Certainly apart: the plan distance is at least `gap`, positive.
368    Disjoint {
369        /// A certified lower bound on the plan distance.
370        gap: Scalar,
371    },
372    /// Neither shown: the shadows may only touch, overlap where no planar
373    /// face covers both, or lie closer than the search resolves.
374    Undecided,
375}
376
377/// Whether the plan projections of `a` and `b` overlap over a patch of
378/// positive area, are certainly apart, or neither could be shown (#217).
379///
380/// Overlap is shown by two planar faces, one of each body, not vertical,
381/// whose shadows share an open patch: a slab under a column's base, two
382/// slabs, a beam's soffit over a wall's top. Curved faces never show it;
383/// their shadows are still measured by [`plan_boundary_distance`].
384///
385/// # Errors
386///
387/// As [`boundary_distance`].
388pub fn plan_overlap(
389    a: &ExactBRep,
390    b: &ExactBRep,
391    tolerance: Tolerance,
392) -> Result<PlanOverlap, ExactMeasureError> {
393    Ok(overlap_search(a, b, tolerance)?.0)
394}
395
396/// The elements of the two B-reps a result was shown on.
397pub(crate) type Pair = (Shape, Shape);
398
399/// [`plan_overlap`], with the elements an overlap was shown on.
400///
401/// First the distance search, which shows the shadows apart or meets an
402/// overlap on its way; then, when it shows neither, a search over pairs of
403/// patches of planar faces that are not vertical alone, coarsest first, so
404/// that pairs of walls and edges whose shadows merely cross cannot use up
405/// the budget, and a plan distance of zero shown by two boundary points
406/// does not end it.
407pub(crate) fn overlap_search(
408    a: &ExactBRep,
409    b: &ExactBRep,
410    tolerance: Tolerance,
411) -> Result<(PlanOverlap, Option<Pair>), ExactMeasureError> {
412    // Overlap on curved faces alone is never shown, so the search is
413    // bounded more tightly than a distance query's.
414    let found = search_within(
415        a,
416        b,
417        tolerance,
418        Metric::Plan,
419        OVERLAP_STEPS,
420        &mut |lower, _, _| lower > 0.0,
421    )?;
422    if let Some(at) = found.overlap {
423        return Ok((PlanOverlap::Overlapping { at }, Some(found.on)));
424    }
425    if found.bounds.lower > 0.0 {
426        return Ok((
427            PlanOverlap::Disjoint {
428                gap: found.bounds.lower,
429            },
430            None,
431        ));
432    }
433    Ok(match level_overlap(a, b, tolerance, OVERLAP_STEPS)? {
434        Some((at, on)) => (PlanOverlap::Overlapping { at }, Some(on)),
435        None => (PlanOverlap::Undecided, None),
436    })
437}
438
439/// Search pairs of patches of planar faces that are not vertical, one of
440/// each B-rep, whose shadows may meet, refining the coarsest pair first,
441/// for two inside patches whose shadows share an open patch.
442fn level_overlap(
443    a: &ExactBRep,
444    b: &ExactBRep,
445    tolerance: Tolerance,
446    max_steps: usize,
447) -> Result<Option<(Point2, Pair)>, ExactMeasureError> {
448    let linear = tolerance.linear().max(1e-12);
449    let side_a = Side::new(a, linear, Metric::Plan)?;
450    let side_b = Side::new(b, linear, Metric::Plan)?;
451    let level = |side: &Side<'_>| -> Result<Vec<Element>, ExactMeasureError> {
452        let mut out = Vec::new();
453        for element in &side.elements {
454            if let Shape::Face { face, .. } = element.shape {
455                if level_frame(side, face)?.is_some() {
456                    out.push(*element);
457                }
458            }
459        }
460        Ok(out)
461    };
462    let mut elements_a = level(&side_a)?;
463    let mut elements_b = level(&side_b)?;
464    // Coarsest first: the pair with the larger plan discs.
465    let mut heap = BinaryHeap::new();
466    let push = |heap: &mut BinaryHeap<(Key, usize, usize)>,
467                ea: &Element,
468                eb: &Element,
469                i: usize,
470                j: usize|
471     -> Result<(), ExactMeasureError> {
472        if lower_bound(&side_a, ea, &side_b, eb)? <= 0.0 {
473            heap.push((Key(ea.plan_radius + eb.plan_radius), i, j));
474        }
475        Ok(())
476    };
477    for (i, ea) in elements_a.iter().enumerate() {
478        for (j, eb) in elements_b.iter().enumerate() {
479            push(&mut heap, ea, eb, i, j)?;
480        }
481    }
482    let mut steps = 0;
483    while let Some((_, i, j)) = heap.pop() {
484        let (ea, eb) = (elements_a[i], elements_b[j]);
485        if let Some((at, _, _)) = plan_patches_overlap(&side_a, &ea, &side_b, &eb)? {
486            return Ok(Some((at, (ea.shape, eb.shape))));
487        }
488        steps += 1;
489        if steps > max_steps {
490            break;
491        }
492        let split_a = side_a.size(&ea) >= side_b.size(&eb);
493        let (side, parent) = if split_a {
494            (&side_a, ea)
495        } else {
496            (&side_b, eb)
497        };
498        let children = side.split(&parent)?;
499        // A pair that can shrink no further shows nothing more.
500        if children
501            .iter()
502            .any(|child| side.size(child) >= side.size(&parent))
503        {
504            continue;
505        }
506        for child in children {
507            if split_a {
508                elements_a.push(child);
509                push(&mut heap, &child, &eb, elements_a.len() - 1, j)?;
510            } else {
511                elements_b.push(child);
512                push(&mut heap, &ea, &child, i, elements_b.len() - 1)?;
513            }
514        }
515    }
516    Ok(None)
517}
518
519/// The frame of face `face` when it is planar and not vertical (nor within
520/// rounding of it): a plane whose shadow has area.
521fn level_frame(
522    side: &Side<'_>,
523    face: usize,
524) -> Result<Option<axiolid_core::Frame3>, ExactMeasureError> {
525    let surface = surface_of(side.brep, side.brep.topology().faces()[face].surface)?;
526    let Surface::Plane(plane) = surface else {
527        return Ok(None);
528    };
529    let f = plane.frame;
530    let det = f.x.x * f.y.y - f.x.y * f.y.x;
531    let scale = f.x.length() * f.y.length();
532    // A vertical or near-vertical plane casts no area worth trusting.
533    if det.abs().is_nan() || det.abs() <= 1e-9 * scale {
534        return Ok(None);
535    }
536    Ok(Some(f))
537}
538
539/// Refine the distance only until it clears `limit`.
540///
541/// Like an accuracy below the rounding floor, a limit within the floor of
542/// the distance settles at the floor and is never decided: the result is
543/// [`Clearance::Indeterminate`].
544///
545/// # Errors
546///
547/// As [`boundary_distance`].
548pub fn boundary_clearance(
549    a: &ExactBRep,
550    b: &ExactBRep,
551    limit: Scalar,
552    tolerance: Tolerance,
553) -> Result<(DistanceBounds, Clearance), ExactMeasureError> {
554    let bounds = search(
555        a,
556        b,
557        tolerance,
558        Metric::Space,
559        &mut |lower, upper, floor| {
560            upper < limit || lower > limit || closed(lower, upper, 0.0, floor)
561        },
562    )?
563    .bounds;
564    let clearance = bounds.against(limit);
565    Ok((bounds, clearance))
566}
567
568/// Which distance a search measures.
569#[derive(Debug, Clone, Copy, PartialEq, Eq)]
570pub(crate) enum Metric {
571    /// In space.
572    Space,
573    /// Between projections onto the XY plane: every gap, direction and
574    /// enclosing radius horizontal.
575    Plan,
576}
577
578/// The horizontal part of a vector.
579fn flat(v: Vec3) -> Vec3 {
580    Vec3::new(v.x, v.y, 0.0)
581}
582
583/// Length of the horizontal part of a vector.
584fn flat_length(v: Vec3) -> Scalar {
585    v.x.hypot(v.y)
586}
587
588/// What an element covers.
589#[derive(Debug, Clone, Copy)]
590pub(crate) enum Shape {
591    /// A rectangle of face `face`'s parameters; `inside` once certified to
592    /// lie wholly in the face.
593    Face {
594        face: usize,
595        lo: Point2,
596        hi: Point2,
597        inside: bool,
598    },
599    /// A span of edge `edge`'s curve.
600    Edge { edge: usize, t0: Scalar, t1: Scalar },
601}
602
603/// A piece of one boundary with a sphere that encloses it.
604#[derive(Debug, Clone, Copy)]
605pub(crate) struct Element {
606    pub(crate) shape: Shape,
607    pub(crate) centre: Point3,
608    pub(crate) radius: Scalar,
609    /// A disc about the centre's projection holding the element's
610    /// projection onto the XY plane; never above `radius`.
611    pub(crate) plan_radius: Scalar,
612    /// A point certainly on the boundary, when one is known.
613    pub(crate) witness: Option<Point3>,
614    /// The surface normal at the centre of a face patch.
615    pub(crate) normal: Option<Vec3>,
616    /// Half-angle of a cone about `normal` holding every normal of the
617    /// patch, when the patch may be dropped from a pair whose directions
618    /// its normals cannot meet (see [`critical_possible`]).
619    pub(crate) spread: Option<Scalar>,
620}
621
622/// One B-rep, prepared for bounding.
623pub(crate) struct Side<'a> {
624    pub(crate) brep: &'a ExactBRep,
625    metric: Metric,
626    domains: Vec<Option<Domain<'a>>>,
627    /// Whether every edge on the face's boundary is an element, so a
628    /// closest point on that boundary is found through the edges.
629    edges_bounded: Vec<bool>,
630    pub(crate) elements: Vec<Element>,
631    /// `S`: the largest `M` over the root elements, the rounding scale of a
632    /// search over this side. Every element is padded by it, so the pads
633    /// cannot reorder the queue (#318).
634    pub(crate) scale: Scalar,
635    /// `W`: the largest `M` of a point evaluated on this side, at least `S`.
636    pub(crate) witness_scale: Scalar,
637}
638
639impl<'a> Side<'a> {
640    pub(crate) fn new(
641        brep: &'a ExactBRep,
642        linear: Scalar,
643        metric: Metric,
644    ) -> Result<Self, ExactMeasureError> {
645        let topology = brep.topology();
646        let mut side = Side {
647            brep,
648            metric,
649            domains: Vec::with_capacity(topology.faces().len()),
650            edges_bounded: Vec::with_capacity(topology.faces().len()),
651            elements: Vec::new(),
652            scale: 0.0,
653            witness_scale: 0.0,
654        };
655        for face in topology.faces() {
656            let mut bounded = true;
657            for bound in &face.bounds {
658                let wire = topology
659                    .loops()
660                    .get(bound.loop_id.index())
661                    .ok_or(ExactMeasureError::DanglingReference)?;
662                for use_ in &wire.edges {
663                    let curve = topology.edges()[use_.edge.index()]
664                        .curve
665                        .and_then(|id| brep.curves3().get(id.index()));
666                    bounded &= matches!(
667                        curve,
668                        Some(Curve3::Line(_) | Curve3::Circle(_) | Curve3::Ellipse(_))
669                    );
670                }
671            }
672            side.edges_bounded.push(bounded);
673        }
674        for face in topology.faces() {
675            let surface = surface_of(brep, face.surface)?;
676            side.domains.push(Domain::new(brep, face, surface, linear)?);
677        }
678        // First the scale, over every root element; then the elements, all
679        // padded by it.
680        let mut roots = Vec::with_capacity(topology.faces().len());
681        for (index, face) in topology.faces().iter().enumerate() {
682            let surface = surface_of(brep, face.surface)?;
683            let (lo, hi) = match &side.domains[index] {
684                Some(domain) => (domain.min, domain.max),
685                None => natural_range(surface).ok_or(ExactMeasureError::NonPlanarFace(
686                    "an unbounded face trimmed by a pcurve family the distance query cannot bound",
687                ))?,
688            };
689            side.scale = side.scale.max(patch_scale(surface, lo, hi)?);
690            side.witness_scale = side.witness_scale.max(witness_scale(surface, lo, hi)?);
691            roots.push((index, lo, hi));
692        }
693        let mut spans = Vec::new();
694        for (index, edge) in topology.edges().iter().enumerate() {
695            let Some(curve) = edge.curve.and_then(|id| brep.curves3().get(id.index())) else {
696                continue;
697            };
698            let Some(span) = topology
699                .edge_id_at(index)
700                .and_then(|id| brep.edge_interval(id))
701            else {
702                continue;
703            };
704            if let Some(scale) = span_scale(curve, span.start, span.end) {
705                side.scale = side.scale.max(scale);
706                side.witness_scale = side.witness_scale.max(scale);
707            }
708            spans.push((curve, index, span));
709        }
710        if !(side.scale.is_finite() && side.witness_scale.is_finite()) {
711            return Err(crate::exact::EVALUATION);
712        }
713        for (index, lo, hi) in roots {
714            if let Some(element) = side.face_element(index, lo, hi, false)? {
715                side.elements.push(element);
716            }
717        }
718        for (curve, index, span) in spans {
719            if let Some(element) = edge_element(curve, index, span.start, span.end, side.scale)? {
720                side.elements.push(element);
721            }
722        }
723        if side.elements.is_empty() {
724            return Err(ExactMeasureError::Degenerate);
725        }
726        Ok(side)
727    }
728
729    /// A face patch, or `None` when it is certified to lie outside the face.
730    fn face_element(
731        &self,
732        face: usize,
733        lo: Point2,
734        hi: Point2,
735        inside: bool,
736    ) -> Result<Option<Element>, ExactMeasureError> {
737        let topology = self.brep.topology();
738        let surface = surface_of(self.brep, topology.faces()[face].surface)?;
739        let mid = (lo + hi) * 0.5;
740        let mut inside = inside;
741        if !inside {
742            if let Some(domain) = &self.domains[face] {
743                if !domain.touches(lo, hi)? {
744                    match domain.contains(mid)? {
745                        Some(false) => return Ok(None),
746                        Some(true) => inside = true,
747                        None => {}
748                    }
749                }
750            }
751        }
752        let (centre, radius) = patch_sphere(surface, lo, hi, self.scale)?;
753        let plan_radius = patch_plan_radius(surface, lo, hi, centre, radius, self.scale)?;
754        let witness = if inside {
755            Some(point_on(surface, mid)?)
756        } else {
757            None
758        };
759        let normal = normal(surface, mid.x, mid.y).ok();
760        let spread = if self.edges_bounded[face] {
761            normal_spread(surface, lo, hi)
762        } else {
763            None
764        };
765        Ok(Some(Element {
766            normal,
767            spread,
768            shape: Shape::Face {
769                face,
770                lo,
771                hi,
772                inside,
773            },
774            centre,
775            radius,
776            plan_radius,
777            witness,
778        }))
779    }
780
781    /// How far a point evaluated on this side may lie from the exact point.
782    pub(crate) fn witness_error(&self) -> Scalar {
783        POINT_EPSILONS * Scalar::EPSILON * self.witness_scale
784    }
785
786    /// What [`Side::witness_error`] adds to the floor beyond the
787    /// `POINT eps S` it already holds: zero but for B-spline faces.
788    pub(crate) fn witness_excess(&self) -> Scalar {
789        POINT_EPSILONS * Scalar::EPSILON * (self.witness_scale - self.scale)
790    }
791
792    /// The radius a search by this side's metric refines by.
793    pub(crate) fn size(&self, element: &Element) -> Scalar {
794        match self.metric {
795            Metric::Space => element.radius,
796            Metric::Plan => element.plan_radius,
797        }
798    }
799
800    /// The element's two halves, less any certified outside the face: a
801    /// face patch across its metrically longer side, an edge span in the
802    /// middle.
803    pub(crate) fn split(&self, element: &Element) -> Result<Vec<Element>, ExactMeasureError> {
804        match element.shape {
805            Shape::Face {
806                face,
807                lo,
808                hi,
809                inside,
810            } => {
811                let surface = surface_of(self.brep, self.brep.topology().faces()[face].surface)?;
812                let (lu, lv) = match self.metric {
813                    Metric::Space => lipschitz(surface, lo, hi)?,
814                    Metric::Plan => plan_lipschitz(surface, lo, hi)?,
815                };
816                let mid = (lo + hi) * 0.5;
817                let halves = if (hi.x - lo.x).abs() * lu >= (hi.y - lo.y).abs() * lv {
818                    [
819                        (lo, Point2::new(mid.x, hi.y)),
820                        (Point2::new(mid.x, lo.y), hi),
821                    ]
822                } else {
823                    [
824                        (lo, Point2::new(hi.x, mid.y)),
825                        (Point2::new(lo.x, mid.y), hi),
826                    ]
827                };
828                let mut out = Vec::with_capacity(2);
829                for (lo, hi) in halves {
830                    if let Some(child) = self.face_element(face, lo, hi, inside)? {
831                        out.push(child);
832                    }
833                }
834                Ok(out)
835            }
836            Shape::Edge { edge, t0, t1 } => {
837                let curve = self.brep.topology().edges()[edge]
838                    .curve
839                    .and_then(|id| self.brep.curves3().get(id.index()))
840                    .ok_or(ExactMeasureError::DanglingReference)?;
841                let tm = 0.5 * (t0 + t1);
842                let mut out = Vec::with_capacity(2);
843                for (a, b) in [(t0, tm), (tm, t1)] {
844                    if let Some(child) = edge_element(curve, edge, a, b, self.scale)? {
845                        out.push(child);
846                    }
847                }
848                Ok(out)
849            }
850        }
851    }
852}
853
854pub(crate) fn surface_of(
855    brep: &ExactBRep,
856    id: Option<axiolid_brep::SurfaceId>,
857) -> Result<&Surface, ExactMeasureError> {
858    let id = id.ok_or(ExactMeasureError::MissingSurface)?;
859    brep.surfaces()
860        .get(id.index())
861        .ok_or(ExactMeasureError::DanglingReference)
862}
863
864fn point_on(surface: &Surface, at: Point2) -> Result<Point3, ExactMeasureError> {
865    evaluate(surface, at.x, at.y).map_err(|_| crate::exact::EVALUATION)
866}
867
868/// The whole parameter range of a closed surface, for a face whose trim
869/// cannot be classified.
870fn natural_range(surface: &Surface) -> Option<(Point2, Point2)> {
871    use core::f64::consts::{FRAC_PI_2, TAU};
872    match surface {
873        Surface::Sphere(_) => Some((Point2::new(0.0, -FRAC_PI_2), Point2::new(TAU, FRAC_PI_2))),
874        Surface::Torus(_) => Some((Point2::ZERO, Point2::new(TAU, TAU))),
875        _ => None,
876    }
877}
878
879/// Largest length a frame axis carries (1 for an orthonormal frame).
880fn frame_scale(frame: &axiolid_core::Frame3) -> Scalar {
881    frame.x.length().max(frame.y.length()).max(frame.z.length())
882}
883
884/// Bounds on `|S_u|` and `|S_v|` over the patch.
885fn lipschitz(
886    surface: &Surface,
887    lo: Point2,
888    hi: Point2,
889) -> Result<(Scalar, Scalar), ExactMeasureError> {
890    let bounds = match surface {
891        Surface::Plane(p) => (p.frame.x.length(), p.frame.y.length()),
892        Surface::Cylinder(c) => (c.radius.abs() * frame_scale(&c.frame), c.frame.z.length()),
893        Surface::EllipticalCylinder(c) => (
894            c.semi_axis_x.abs().max(c.semi_axis_y.abs()) * frame_scale(&c.frame),
895            c.frame.z.length(),
896        ),
897        Surface::Cone(c) => {
898            let slope = c.semi_angle.tan();
899            let radius = (c.radius + lo.y * slope)
900                .abs()
901                .max((c.radius + hi.y * slope).abs());
902            let scale = frame_scale(&c.frame);
903            (radius * scale, scale * (1.0 + slope * slope).sqrt())
904        }
905        Surface::Sphere(s) => {
906            // |S_u| = r cos v, which vanishes at the poles: bounding it by r
907            // would split near-polar patches round the pole for nothing.
908            let r = s.radius.abs() * frame_scale(&s.frame);
909            (r * max_cos(lo.y, hi.y), r)
910        }
911        Surface::Torus(t) => {
912            let scale = frame_scale(&t.frame);
913            (
914                (t.major_radius.abs() + t.minor_radius.abs() * max_cos(lo.y, hi.y)) * scale,
915                t.minor_radius.abs() * scale,
916            )
917        }
918        // A B-spline patch is bounded by its control net instead.
919        Surface::BSpline(_) => (0.0, 0.0),
920        _ => {
921            return Err(ExactMeasureError::NonPlanarFace(crate::exact::family(
922                surface,
923            )))
924        }
925    };
926    if bounds.0.is_finite() && bounds.1.is_finite() {
927        Ok(bounds)
928    } else {
929        Err(crate::exact::EVALUATION)
930    }
931}
932
933/// Bounds on `|P S_u|` and `|P S_v|` over the patch, `P` dropping the
934/// vertical: the horizontal speeds, never above [`lipschitz`]'s.
935fn plan_lipschitz(
936    surface: &Surface,
937    lo: Point2,
938    hi: Point2,
939) -> Result<(Scalar, Scalar), ExactMeasureError> {
940    let full = lipschitz(surface, lo, hi)?;
941    // |P (a X + b Y)| <= sqrt(a^2 |P X|^2 + b^2 |P Y|^2) when a^2 + b^2 <= 1.
942    let turning = |frame: &axiolid_core::Frame3, a: Scalar, b: Scalar| {
943        (a * flat_length(frame.x)).hypot(b * flat_length(frame.y))
944    };
945    let bounds = match surface {
946        Surface::Plane(p) => (flat_length(p.frame.x), flat_length(p.frame.y)),
947        Surface::Cylinder(c) => (
948            turning(&c.frame, c.radius.abs(), c.radius.abs()),
949            flat_length(c.frame.z),
950        ),
951        Surface::EllipticalCylinder(c) => (
952            turning(&c.frame, c.semi_axis_x.abs(), c.semi_axis_y.abs()),
953            flat_length(c.frame.z),
954        ),
955        Surface::Cone(c) => {
956            let slope = c.semi_angle.tan();
957            let radius = (c.radius + lo.y * slope)
958                .abs()
959                .max((c.radius + hi.y * slope).abs());
960            (
961                turning(&c.frame, radius, radius),
962                turning(&c.frame, slope.abs(), slope.abs()) + flat_length(c.frame.z),
963            )
964        }
965        _ => return Ok(full),
966    };
967    if bounds.0.is_finite() && bounds.1.is_finite() {
968        Ok((bounds.0.min(full.0), bounds.1.min(full.1)))
969    } else {
970        Ok(full)
971    }
972}
973
974/// A radius about the centre's projection holding the patch's projection:
975/// as [`patch_sphere`], from the horizontal speeds, and never above the
976/// sphere's own `radius`.
977fn patch_plan_radius(
978    surface: &Surface,
979    lo: Point2,
980    hi: Point2,
981    centre: Point3,
982    radius: Scalar,
983    scale: Scalar,
984) -> Result<Scalar, ExactMeasureError> {
985    if let Surface::BSpline(spline) = surface {
986        let reach = spline
987            .control_points
988            .iter()
989            .flatten()
990            .map(|p| flat_length(*p - centre))
991            .fold(0.0, Scalar::max);
992        return Ok(pad(reach, scale).min(radius));
993    }
994    let (lu, lv) = plan_lipschitz(surface, lo, hi)?;
995    let plan = 0.5 * ((hi.x - lo.x).abs() * lu + (hi.y - lo.y).abs() * lv);
996    Ok(pad(plan, scale).min(radius))
997}
998
999/// Largest `|cos v|` over `[a, b]`.
1000fn max_cos(a: Scalar, b: Scalar) -> Scalar {
1001    let (a, b) = (a.min(b), a.max(b));
1002    let k = (a / core::f64::consts::PI).ceil();
1003    if k * core::f64::consts::PI <= b {
1004        1.0
1005    } else {
1006        a.cos().abs().max(b.cos().abs())
1007    }
1008}
1009
1010/// A sphere enclosing every surface point of the patch.
1011///
1012/// From the patch centre, any point is reached by a path along `u` then
1013/// along `v`, no longer than `du/2 |S_u|max + dv/2 |S_v|max`. A B-spline
1014/// patch lies in the convex hull of the surface's control net (positive
1015/// weights), which is not refined but is sound.
1016fn patch_sphere(
1017    surface: &Surface,
1018    lo: Point2,
1019    hi: Point2,
1020    scale: Scalar,
1021) -> Result<(Point3, Scalar), ExactMeasureError> {
1022    if let Surface::BSpline(spline) = surface {
1023        if let Some(weights) = &spline.weights {
1024            if weights.iter().flatten().any(|w| w.is_nan() || *w <= 0.0) {
1025                return Err(ExactMeasureError::NonPlanarFace(
1026                    "non-positive-weight B-spline",
1027                ));
1028            }
1029        }
1030        let points: Vec<Point3> = spline.control_points.iter().flatten().copied().collect();
1031        if points.is_empty() {
1032            return Err(ExactMeasureError::Degenerate);
1033        }
1034        let (mut min, mut max) = (points[0], points[0]);
1035        for p in &points {
1036            min = min.min(*p);
1037            max = max.max(*p);
1038        }
1039        let centre = (min + max) * 0.5;
1040        let radius = points
1041            .iter()
1042            .map(|p| (*p - centre).length())
1043            .fold(0.0, Scalar::max);
1044        return Ok((centre, pad(radius, scale)));
1045    }
1046    let centre = point_on(surface, (lo + hi) * 0.5)?;
1047    let (lu, lv) = lipschitz(surface, lo, hi)?;
1048    let radius = 0.5 * ((hi.x - lo.x).abs() * lu + (hi.y - lo.y).abs() * lv);
1049    Ok((centre, pad(radius, scale)))
1050}
1051
1052/// Widen a bounding radius by the rounding its centre may carry: the radius
1053/// itself is a computed length, and the centre is a computed point whose
1054/// error scales with the side's `scale` (`S`), the same for every element,
1055/// not with the distance from the origin alone.
1056pub(crate) fn pad(radius: Scalar, scale: Scalar) -> Scalar {
1057    radius * (1.0 + RATIO_EPSILONS * Scalar::EPSILON)
1058        + POINT_EPSILONS * Scalar::EPSILON * scale
1059        + Scalar::MIN_POSITIVE
1060}
1061
1062/// The width an interval settles at, whatever accuracy is asked: the
1063/// rounding of the lower bounds at zero size (about `32 eps S`, twice that
1064/// here) and of the upper, for a search of scale `scale` (`S_a + S_b`, or
1065/// `|p| + S` for a point) (#318).
1066pub(crate) fn floor(scale: Scalar, upper: Scalar) -> Scalar {
1067    FLOOR_EPSILONS * Scalar::EPSILON * scale + RATIO_EPSILONS * Scalar::EPSILON * upper
1068}
1069
1070/// Whether `[lower, upper]` is as narrow as asked, or as the rounding
1071/// allows (#318). An accuracy below the floor is never reported as met: the
1072/// interval returned shows its width.
1073pub(crate) fn closed(lower: Scalar, upper: Scalar, accuracy: Scalar, floor: Scalar) -> bool {
1074    upper - lower <= accuracy.max(floor)
1075}
1076
1077/// `M` of a patch: the magnitude of the terms `S(u, v)` is summed from. The
1078/// frame origin, the frame-scaled radii and the parameter terms
1079/// `|u| Lu + |v| Lv` at the patch's farthest parameters.
1080fn patch_scale(surface: &Surface, lo: Point2, hi: Point2) -> Result<Scalar, ExactMeasureError> {
1081    if let Surface::BSpline(spline) = surface {
1082        let scale = spline
1083            .control_points
1084            .iter()
1085            .flatten()
1086            .map(|p| p.length())
1087            .fold(0.0, Scalar::max);
1088        return if scale.is_finite() {
1089            Ok(scale)
1090        } else {
1091            Err(crate::exact::EVALUATION)
1092        };
1093    }
1094    let (lu, lv) = lipschitz(surface, lo, hi)?;
1095    let params = lo.x.abs().max(hi.x.abs()) * lu + lo.y.abs().max(hi.y.abs()) * lv;
1096    let scale = match surface {
1097        Surface::Plane(p) => p.frame.origin.length() + params,
1098        Surface::Cylinder(c) => {
1099            c.frame.origin.length() + 2.0 * c.radius.abs() * frame_scale(&c.frame) + params
1100        }
1101        Surface::EllipticalCylinder(c) => {
1102            c.frame.origin.length()
1103                + (c.semi_axis_x.abs() + c.semi_axis_y.abs()) * frame_scale(&c.frame)
1104                + params
1105        }
1106        Surface::Cone(c) => {
1107            let reach = lo.y.abs().max(hi.y.abs());
1108            c.frame.origin.length()
1109                + 2.0 * (c.radius.abs() + reach * c.semi_angle.tan().abs()) * frame_scale(&c.frame)
1110                + params
1111        }
1112        Surface::Sphere(s) => {
1113            s.frame.origin.length() + 3.0 * s.radius.abs() * frame_scale(&s.frame) + params
1114        }
1115        Surface::Torus(t) => {
1116            t.frame.origin.length()
1117                + (2.0 * t.major_radius.abs() + 3.0 * t.minor_radius.abs()) * frame_scale(&t.frame)
1118                + params
1119        }
1120        _ => {
1121            return Err(ExactMeasureError::NonPlanarFace(crate::exact::family(
1122                surface,
1123            )))
1124        }
1125    };
1126    if scale.is_finite() {
1127        Ok(scale)
1128    } else {
1129        Err(crate::exact::EVALUATION)
1130    }
1131}
1132
1133/// `M` of the points evaluated on a patch as witnesses: [`patch_scale`]'s,
1134/// except for a B-spline (#439).
1135fn witness_scale(surface: &Surface, lo: Point2, hi: Point2) -> Result<Scalar, ExactMeasureError> {
1136    let Surface::BSpline(spline) = surface else {
1137        return patch_scale(surface, lo, hi);
1138    };
1139    let largest = spline
1140        .control_points
1141        .iter()
1142        .flatten()
1143        .map(|p| p.length())
1144        .fold(0.0, Scalar::max);
1145    let ratio = match &spline.weights {
1146        None => 1.0,
1147        Some(weights) => {
1148            let (mut lo, mut hi) = (Scalar::INFINITY, 0.0_f64);
1149            for w in weights.iter().flatten() {
1150                if w.is_nan() || *w <= 0.0 {
1151                    return Err(ExactMeasureError::NonPlanarFace(
1152                        "non-positive-weight B-spline",
1153                    ));
1154                }
1155                lo = lo.min(*w);
1156                hi = hi.max(*w);
1157            }
1158            hi / lo
1159        }
1160    };
1161    let degrees = Scalar::from(spline.u_degree) + Scalar::from(spline.v_degree);
1162    // The derivation in the module docs is first order in `L rho eps`.
1163    if ratio > 1.0 / (64.0 * degrees.max(1.0) * Scalar::EPSILON) {
1164        return Err(ExactMeasureError::NonPlanarFace(
1165            "extreme weight ratio B-spline",
1166        ));
1167    }
1168    let levels = degrees + 1.0;
1169    let scale = levels * ratio * largest;
1170    if scale.is_finite() {
1171        Ok(scale)
1172    } else {
1173        Err(crate::exact::EVALUATION)
1174    }
1175}
1176
1177/// The upper bound reported for witnesses computed `distance` apart.
1178pub(crate) fn witnessed_upper(distance: Scalar, error: Scalar) -> Scalar {
1179    distance * (1.0 + RATIO_EPSILONS * Scalar::EPSILON) + error
1180}
1181
1182/// `M` of an edge span, or `None` for a curve family with no bound here.
1183fn span_scale(curve: &Curve3, t0: Scalar, t1: Scalar) -> Option<Scalar> {
1184    let t = t0.abs().max(t1.abs());
1185    match curve {
1186        Curve3::Line(line) => Some(line.origin.length() + t * line.direction.length()),
1187        Curve3::Circle(c) => {
1188            Some(c.frame.origin.length() + (2.0 + t) * c.radius.abs() * frame_scale(&c.frame))
1189        }
1190        Curve3::Ellipse(e) => {
1191            let (a, b) = (e.semi_axis_x.abs(), e.semi_axis_y.abs());
1192            Some(e.frame.origin.length() + (a + b + t * a.max(b)) * frame_scale(&e.frame))
1193        }
1194        _ => None,
1195    }
1196}
1197
1198/// An edge span with its enclosing sphere and its midpoint as a witness,
1199/// or `None` for a curve family with no derivative bound here.
1200fn edge_element(
1201    curve: &Curve3,
1202    edge: usize,
1203    t0: Scalar,
1204    t1: Scalar,
1205    scale: Scalar,
1206) -> Result<Option<Element>, ExactMeasureError> {
1207    let speed = match curve {
1208        Curve3::Line(line) => line.direction.length(),
1209        Curve3::Circle(circle) => circle.radius.abs() * frame_scale(&circle.frame),
1210        Curve3::Ellipse(ellipse) => {
1211            ellipse.semi_axis_x.abs().max(ellipse.semi_axis_y.abs()) * frame_scale(&ellipse.frame)
1212        }
1213        _ => return Ok(None),
1214    };
1215    // The horizontal speed, as for a patch (see `plan_lipschitz`).
1216    let plan_speed = match curve {
1217        Curve3::Line(line) => flat_length(line.direction),
1218        Curve3::Circle(c) => {
1219            (c.radius.abs() * flat_length(c.frame.x)).hypot(c.radius.abs() * flat_length(c.frame.y))
1220        }
1221        Curve3::Ellipse(e) => (e.semi_axis_x.abs() * flat_length(e.frame.x))
1222            .hypot(e.semi_axis_y.abs() * flat_length(e.frame.y)),
1223        _ => speed,
1224    };
1225    let centre = evaluate3(curve, 0.5 * (t0 + t1)).map_err(|_| crate::exact::EVALUATION)?;
1226    let radius = pad(0.5 * (t1 - t0).abs() * speed, scale);
1227    let plan_radius = if plan_speed.is_finite() {
1228        pad(0.5 * (t1 - t0).abs() * plan_speed, scale).min(radius)
1229    } else {
1230        radius
1231    };
1232    Ok(Some(Element {
1233        normal: None,
1234        spread: None,
1235        shape: Shape::Edge { edge, t0, t1 },
1236        centre,
1237        radius,
1238        plan_radius,
1239        witness: Some(centre),
1240    }))
1241}
1242
1243/// Half-angle bound on how far the surface normal turns across the patch.
1244fn normal_spread(surface: &Surface, lo: Point2, hi: Point2) -> Option<Scalar> {
1245    let (du, dv) = ((hi.x - lo.x).abs(), (hi.y - lo.y).abs());
1246    let spread = match surface {
1247        Surface::Plane(_) => 0.0,
1248        // The normal turns with u at unit rate on a circular section.
1249        Surface::Cylinder(_) => 0.5 * du,
1250        Surface::Cone(c) => {
1251            // The apex is not smooth: a nearest point there is neither
1252            // critical nor on an edge, so a patch reaching it is never
1253            // dropped.
1254            let slope = c.semi_angle.tan();
1255            let apex = -c.radius / slope;
1256            if !apex.is_finite() || (apex >= lo.y.min(hi.y) - 1e-9 && apex <= lo.y.max(hi.y) + 1e-9)
1257            {
1258                return None;
1259            }
1260            0.5 * du
1261        }
1262        // On an ellipse it turns at most max/min times faster.
1263        Surface::EllipticalCylinder(c) => {
1264            let (a, b) = (c.semi_axis_x.abs(), c.semi_axis_y.abs());
1265            0.5 * du * a.max(b) / a.min(b)
1266        }
1267        Surface::Sphere(_) | Surface::Torus(_) => 0.5 * (du + dv),
1268        _ => return None,
1269    };
1270    spread.is_finite().then_some(spread + 1e-9)
1271}
1272
1273/// Whether some pair of points of the two elements can be critical for
1274/// the distance on the face side: the segment joining them along the
1275/// surface normal there.
1276///
1277/// A closest pair of points of two separated boundaries is either critical
1278/// on each face it lies inside, or lies on an edge -- and every edge is an
1279/// element of its own. So a face patch whose normals cannot meet any
1280/// direction towards the other element holds no closest point that the
1281/// edges do not already hold, and the pair can be dropped. This is what
1282/// lets the bound close where the nearest points run along an edge: the
1283/// patches straddling that edge also hold points just outside the face,
1284/// closer than the true distance, that no bound on the patch can exclude.
1285///
1286/// Between plan projections the same holds with the offset horizontal: a
1287/// point inside a face is critical for the horizontal distance only where
1288/// its normal is horizontal and along the plan offset, as on a vertical
1289/// wall or at a silhouette.
1290fn critical_possible(face: &Element, other: &Element, metric: Metric) -> bool {
1291    let (offset, reach) = match metric {
1292        Metric::Space => (other.centre - face.centre, face.radius + other.radius),
1293        Metric::Plan => (
1294            flat(other.centre - face.centre),
1295            face.plan_radius + other.plan_radius,
1296        ),
1297    };
1298    critical_along(face, offset, reach)
1299}
1300
1301/// [`critical_possible`] towards a single point `p`, in space: whether the
1302/// face patch can hold a point nearest to `p` that no edge holds.
1303pub(crate) fn critical_toward(face: &Element, p: Point3) -> bool {
1304    critical_along(face, p - face.centre, face.radius)
1305}
1306
1307/// Whether some normal of the face patch can meet a direction from it to
1308/// a ball of `reach` about `face.centre + offset`.
1309fn critical_along(face: &Element, offset: Vec3, reach: Scalar) -> bool {
1310    let (Some(normal), Some(spread)) = (face.normal, face.spread) else {
1311        return true;
1312    };
1313    let gap = offset.length();
1314    if gap.is_nan() || gap <= reach {
1315        return true;
1316    }
1317    let aperture = (reach / gap).asin();
1318    let angle = spread + aperture + 1e-9;
1319    if angle >= core::f64::consts::FRAC_PI_2 {
1320        return true;
1321    }
1322    (offset / gap).dot(normal).abs() >= angle.cos() * normal.length()
1323}
1324
1325/// Range of `a cos t + b sin t` over `[t0, t1]`.
1326fn trig_range(a: Scalar, b: Scalar, t0: Scalar, t1: Scalar) -> (Scalar, Scalar) {
1327    let (t0, t1) = (t0.min(t1), t0.max(t1));
1328    let f = |t: Scalar| a * t.cos() + b * t.sin();
1329    let (mut lo, mut hi) = (f(t0).min(f(t1)), f(t0).max(f(t1)));
1330    let amplitude = a.hypot(b);
1331    let peak = b.atan2(a);
1332    let reaches = |angle: Scalar| {
1333        let k = ((t0 - angle) / core::f64::consts::TAU).ceil();
1334        angle + k * core::f64::consts::TAU <= t1
1335    };
1336    if reaches(peak) {
1337        hi = amplitude;
1338    }
1339    if reaches(peak + core::f64::consts::PI) {
1340        lo = -amplitude;
1341    }
1342    (lo, hi)
1343}
1344
1345/// Range of `d . x` over an element's enclosing sphere.
1346fn sphere_range(element: &Element, d: Vec3) -> (Scalar, Scalar) {
1347    let c = element.centre.dot(d);
1348    (c - element.radius, c + element.radius)
1349}
1350
1351impl Side<'_> {
1352    /// Exact range of `d . x` over the element where the family allows,
1353    /// else the enclosing sphere's.
1354    pub(crate) fn project(
1355        &self,
1356        element: &Element,
1357        d: Vec3,
1358    ) -> Result<(Scalar, Scalar), ExactMeasureError> {
1359        // Rounding in the projection itself, on either range, the same for
1360        // every element of the side (#318).
1361        let pad = PROJECTION_EPSILONS * Scalar::EPSILON * self.scale;
1362        let sphere = match self.metric {
1363            Metric::Space => sphere_range(element, d),
1364            // A horizontal direction reads only the projection, which the
1365            // plan disc holds.
1366            Metric::Plan => {
1367                let c = element.centre.dot(d);
1368                (c - element.plan_radius, c + element.plan_radius)
1369            }
1370        };
1371        let exact = match element.shape {
1372            Shape::Face { face, lo, hi, .. } => {
1373                let surface = surface_of(self.brep, self.brep.topology().faces()[face].surface)?;
1374                match surface {
1375                    Surface::Plane(p) => {
1376                        let base = p.frame.origin.dot(d);
1377                        let (x, y) = (p.frame.x.dot(d), p.frame.y.dot(d));
1378                        let values = [
1379                            base + x * lo.x + y * lo.y,
1380                            base + x * hi.x + y * lo.y,
1381                            base + x * lo.x + y * hi.y,
1382                            base + x * hi.x + y * hi.y,
1383                        ];
1384                        Some(
1385                            values
1386                                .iter()
1387                                .fold((Scalar::INFINITY, Scalar::NEG_INFINITY), |(a, b), v| {
1388                                    (a.min(*v), b.max(*v))
1389                                }),
1390                        )
1391                    }
1392                    Surface::Cylinder(c) => {
1393                        let (a, b) = trig_range(
1394                            c.radius * c.frame.x.dot(d),
1395                            c.radius * c.frame.y.dot(d),
1396                            lo.x,
1397                            hi.x,
1398                        );
1399                        let z = c.frame.z.dot(d);
1400                        let base = c.frame.origin.dot(d);
1401                        Some((
1402                            base + a + (z * lo.y).min(z * hi.y),
1403                            base + b + (z * lo.y).max(z * hi.y),
1404                        ))
1405                    }
1406                    Surface::EllipticalCylinder(c) => {
1407                        let (a, b) = trig_range(
1408                            c.semi_axis_x * c.frame.x.dot(d),
1409                            c.semi_axis_y * c.frame.y.dot(d),
1410                            lo.x,
1411                            hi.x,
1412                        );
1413                        let z = c.frame.z.dot(d);
1414                        let base = c.frame.origin.dot(d);
1415                        Some((
1416                            base + a + (z * lo.y).min(z * hi.y),
1417                            base + b + (z * lo.y).max(z * hi.y),
1418                        ))
1419                    }
1420                    Surface::Cone(c) => {
1421                        // Linear in v for fixed u: the extremes sit on the
1422                        // two v edges of the rectangle.
1423                        let slope = c.semi_angle.tan();
1424                        let base = c.frame.origin.dot(d);
1425                        let (x, y, z) = (c.frame.x.dot(d), c.frame.y.dot(d), c.frame.z.dot(d));
1426                        let mut range = (Scalar::INFINITY, Scalar::NEG_INFINITY);
1427                        for v in [lo.y, hi.y] {
1428                            let r = c.radius + v * slope;
1429                            let (a, b) = trig_range(r * x, r * y, lo.x, hi.x);
1430                            range = (range.0.min(base + a + z * v), range.1.max(base + b + z * v));
1431                        }
1432                        Some(range)
1433                    }
1434                    Surface::Sphere(sphere) => {
1435                        // d.S = d.c + r (cos v W(u) + sin v Z), W linear in
1436                        // the u-trig term: the extremes take W at its ends.
1437                        let r = sphere.radius;
1438                        let base = sphere.frame.origin.dot(d);
1439                        let (w_lo, w_hi) =
1440                            trig_range(sphere.frame.x.dot(d), sphere.frame.y.dot(d), lo.x, hi.x);
1441                        let z = sphere.frame.z.dot(d);
1442                        let mut range = (Scalar::INFINITY, Scalar::NEG_INFINITY);
1443                        for w in [w_lo, w_hi] {
1444                            let (a, b) = trig_range(r * w, r * z, lo.y, hi.y);
1445                            range = (range.0.min(base + a), range.1.max(base + b));
1446                        }
1447                        Some(range)
1448                    }
1449                    Surface::Torus(torus) => {
1450                        // d.S = d.c + R W(u) + r (cos v W(u) + sin v Z); for
1451                        // R > r the coefficient of W is positive, so again W
1452                        // takes its ends.
1453                        let (big, small) = (torus.major_radius, torus.minor_radius);
1454                        if big.is_nan() || big <= small.abs() {
1455                            return Ok((sphere.0 - pad, sphere.1 + pad));
1456                        }
1457                        let base = torus.frame.origin.dot(d);
1458                        let (w_lo, w_hi) =
1459                            trig_range(torus.frame.x.dot(d), torus.frame.y.dot(d), lo.x, hi.x);
1460                        let z = torus.frame.z.dot(d);
1461                        let mut range = (Scalar::INFINITY, Scalar::NEG_INFINITY);
1462                        for w in [w_lo, w_hi] {
1463                            let (a, b) = trig_range(small * w, small * z, lo.y, hi.y);
1464                            range = (
1465                                range.0.min(base + big * w + a),
1466                                range.1.max(base + big * w + b),
1467                            );
1468                        }
1469                        Some(range)
1470                    }
1471                    _ => None,
1472                }
1473            }
1474            Shape::Edge { edge, t0, t1 } => {
1475                let curve = self.brep.topology().edges()[edge]
1476                    .curve
1477                    .and_then(|id| self.brep.curves3().get(id.index()))
1478                    .ok_or(ExactMeasureError::DanglingReference)?;
1479                match curve {
1480                    Curve3::Line(line) => {
1481                        let (a, b) = (
1482                            (line.origin + line.direction * t0).dot(d),
1483                            (line.origin + line.direction * t1).dot(d),
1484                        );
1485                        Some((a.min(b), a.max(b)))
1486                    }
1487                    Curve3::Circle(c) => {
1488                        let (a, b) = trig_range(
1489                            c.radius * c.frame.x.dot(d),
1490                            c.radius * c.frame.y.dot(d),
1491                            t0,
1492                            t1,
1493                        );
1494                        let base = c.frame.origin.dot(d);
1495                        Some((base + a, base + b))
1496                    }
1497                    Curve3::Ellipse(e) => {
1498                        let (a, b) = trig_range(
1499                            e.semi_axis_x * e.frame.x.dot(d),
1500                            e.semi_axis_y * e.frame.y.dot(d),
1501                            t0,
1502                            t1,
1503                        );
1504                        let base = e.frame.origin.dot(d);
1505                        Some((base + a, base + b))
1506                    }
1507                    _ => None,
1508                }
1509            }
1510        };
1511        let (lo, hi) = exact.map_or(sphere, |(lo, hi)| (lo.max(sphere.0), hi.min(sphere.1)));
1512        Ok((lo - pad, hi + pad))
1513    }
1514}
1515
1516/// The largest separation certified along the centre line or either
1517/// patch's normal, and never less than the spheres give.
1518fn lower_bound(
1519    side_a: &Side<'_>,
1520    a: &Element,
1521    side_b: &Side<'_>,
1522    b: &Element,
1523) -> Result<Scalar, ExactMeasureError> {
1524    let plan = side_a.metric == Metric::Plan;
1525    let (offset, reach) = if plan {
1526        (flat(b.centre - a.centre), a.plan_radius + b.plan_radius)
1527    } else {
1528        (b.centre - a.centre, a.radius + b.radius)
1529    };
1530    let gap = offset.length();
1531    let rounding = RATIO_EPSILONS * Scalar::EPSILON * (gap + reach);
1532    let mut best = (gap - reach - rounding).max(0.0);
1533    // Between projections only horizontal directions separate them: a
1534    // normal counts by its horizontal part.
1535    let direction = |n: Option<Vec3>| -> Option<Vec3> {
1536        let n = n?;
1537        if !plan {
1538            return Some(n);
1539        }
1540        let h = flat(n);
1541        let length = h.length();
1542        (length > 1e-6 * n.length()).then(|| h / length)
1543    };
1544    let directions = [
1545        (gap > 0.0).then(|| offset / gap),
1546        direction(a.normal),
1547        direction(b.normal),
1548    ];
1549    for d in directions.into_iter().flatten() {
1550        let (a_lo, a_hi) = side_a.project(a, d)?;
1551        let (b_lo, b_hi) = side_b.project(b, d)?;
1552        best = best.max(b_lo - a_hi).max(a_lo - b_hi);
1553    }
1554    Ok(best * (1.0 - RATIO_EPSILONS * Scalar::EPSILON))
1555}
1556
1557/// A lower bound on the distance from the point `p` to every point of
1558/// `element`, in space: the enclosing sphere's, raised by the exact range
1559/// of `d . x` along the line to the centre and along the patch's normal.
1560pub(crate) fn point_lower_bound(
1561    p: Point3,
1562    side: &Side<'_>,
1563    element: &Element,
1564) -> Result<Scalar, ExactMeasureError> {
1565    let offset = element.centre - p;
1566    let gap = offset.length();
1567    let rounding = RATIO_EPSILONS * Scalar::EPSILON * (gap + element.radius);
1568    let mut best = (gap - element.radius - rounding).max(0.0);
1569    let directions = [(gap > 0.0).then(|| offset / gap), element.normal];
1570    for d in directions.into_iter().flatten() {
1571        let length = d.length();
1572        if length.is_nan() || length <= 0.0 {
1573            continue;
1574        }
1575        let d = d / length;
1576        let (lo, hi) = side.project(element, d)?;
1577        let x = p.dot(d);
1578        let pad = PROJECTION_EPSILONS * Scalar::EPSILON * p.length();
1579        best = best.max(lo - x - pad).max(x - hi - pad);
1580    }
1581    Ok(best * (1.0 - RATIO_EPSILONS * Scalar::EPSILON))
1582}
1583
1584/// Total order on finite lower bounds for the heap.
1585#[derive(Debug, Clone, Copy, PartialEq)]
1586pub(crate) struct Key(pub(crate) Scalar);
1587
1588impl Eq for Key {}
1589
1590impl PartialOrd for Key {
1591    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
1592        Some(self.cmp(other))
1593    }
1594}
1595
1596impl Ord for Key {
1597    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1598        self.0.total_cmp(&other.0)
1599    }
1600}
1601
1602/// What a search found: the bounds, the elements the witnesses lie on, and
1603/// a plan point shown inside both shadows when planar faces show them
1604/// overlapping.
1605pub(crate) struct Found {
1606    pub(crate) bounds: DistanceBounds,
1607    /// The element of each boundary holding `point_a` and `point_b`.
1608    pub(crate) on: (Shape, Shape),
1609    pub(crate) overlap: Option<Point2>,
1610    /// Pairs refined, which the tests count.
1611    #[cfg(test)]
1612    pub(crate) steps: usize,
1613}
1614
1615pub(crate) fn search(
1616    a: &ExactBRep,
1617    b: &ExactBRep,
1618    tolerance: Tolerance,
1619    metric: Metric,
1620    done: &mut dyn FnMut(Scalar, Scalar, Scalar) -> bool,
1621) -> Result<Found, ExactMeasureError> {
1622    search_within(a, b, tolerance, metric, MAX_STEPS, done)
1623}
1624
1625pub(crate) fn search_within(
1626    a: &ExactBRep,
1627    b: &ExactBRep,
1628    tolerance: Tolerance,
1629    metric: Metric,
1630    max_steps: usize,
1631    done: &mut dyn FnMut(Scalar, Scalar, Scalar) -> bool,
1632) -> Result<Found, ExactMeasureError> {
1633    search_with(a, b, tolerance, metric, max_steps, done, true)
1634}
1635
1636/// [`search_within`], with the early end on a touch in space (#273) on or
1637/// off: off, it is the search before it, which the tests compare against.
1638fn search_with(
1639    a: &ExactBRep,
1640    b: &ExactBRep,
1641    tolerance: Tolerance,
1642    metric: Metric,
1643    max_steps: usize,
1644    done: &mut dyn FnMut(Scalar, Scalar, Scalar) -> bool,
1645    early_touch: bool,
1646) -> Result<Found, ExactMeasureError> {
1647    let linear = tolerance.linear().max(1e-12);
1648    let side_a = Side::new(a, linear, metric)?;
1649    let side_b = Side::new(b, linear, metric)?;
1650    let scale = side_a.scale + side_b.scale;
1651    let error = side_a.witness_error() + side_b.witness_error();
1652    let excess = side_a.witness_excess() + side_b.witness_excess();
1653    let apart = |p: Point3, q: Point3| match metric {
1654        Metric::Space => (p - q).length(),
1655        Metric::Plan => flat_length(p - q),
1656    };
1657    // A witness counts as soon as its pair is queued, not when it is popped
1658    // (#318): refining pairs whose bound cannot rise never runs ahead of the
1659    // witnesses in hand. Ties keep the first.
1660    let offer =
1661        |best: &mut Option<(Scalar, Point3, Point3, (Shape, Shape))>, a: &Element, b: &Element| {
1662            if let (Some(wa), Some(wb)) = (a.witness, b.witness) {
1663                let d = apart(wa, wb);
1664                if best.is_none_or(|(current, ..)| d < current) {
1665                    *best = Some((d, wa, wb, (a.shape, b.shape)));
1666                }
1667            }
1668        };
1669    let mut elements_a = side_a.elements.clone();
1670    let mut elements_b = side_b.elements.clone();
1671
1672    let mut best: Option<(Scalar, Point3, Point3, (Shape, Shape))> = None;
1673    let mut heap = BinaryHeap::new();
1674    let viable = |a: &Element, b: &Element| {
1675        critical_possible(a, b, metric) && critical_possible(b, a, metric)
1676    };
1677    for (i, ea) in elements_a.iter().enumerate() {
1678        for (j, eb) in elements_b.iter().enumerate() {
1679            if viable(ea, eb) {
1680                heap.push(Reverse((Key(lower_bound(&side_a, ea, &side_b, eb)?), i, j)));
1681                offer(&mut best, ea, eb);
1682            }
1683        }
1684    }
1685
1686    let mut lower = 0.0;
1687    let mut steps = 0;
1688    let mut overlap = None;
1689    while let Some(Reverse((Key(bound), i, j))) = heap.pop() {
1690        lower = bound;
1691        let (ea, eb) = (elements_a[i], elements_b[j]);
1692        if metric == Metric::Plan && bound == 0.0 {
1693            if let Some((at, wa, wb)) = plan_patches_overlap(&side_a, &ea, &side_b, &eb)? {
1694                overlap = Some(at);
1695                best = Some((0.0, wa, wb, (ea.shape, eb.shape)));
1696                break;
1697            }
1698        }
1699        // Boundaries shown to meet within rounding: no lower bound can rise
1700        // above zero, so the search would only close on this (#273).
1701        if early_touch && metric == Metric::Space && bound == 0.0 {
1702            if let Some((wa, wb)) = touch::touch(&side_a, &ea, &side_b, &eb)? {
1703                let d = apart(wa, wb);
1704                if best.is_none_or(|(current, ..)| d < current) {
1705                    best = Some((d, wa, wb, (ea.shape, eb.shape)));
1706                }
1707                break;
1708            }
1709        }
1710        // Where the bound is positive and still short of what is in hand, the
1711        // boundary point of each element nearest the other's witness may close
1712        // the gap at once (#318); a pair at zero is the touch check's.
1713        if bound > 0.0 && best.is_none_or(|(current, ..)| bound < current) {
1714            if let Some((wa, wb, on)) = witness::pair(&side_a, &ea, &side_b, &eb)? {
1715                let d = apart(wa, wb);
1716                if best.is_none_or(|(current, ..)| d < current) {
1717                    best = Some((d, wa, wb, on));
1718                }
1719            }
1720        }
1721        let upper = best.map_or(Scalar::INFINITY, |(d, ..)| witnessed_upper(d, error));
1722        // A bound at the computed distance ends the search whatever the
1723        // witnesses' rounding adds to the reported upper: stopping is sound,
1724        // and coincident witnesses must not refine to the step budget.
1725        if best.is_some_and(|(d, ..)| bound >= d)
1726            || (upper.is_finite()
1727                && (bound >= upper || done(bound, upper, floor(scale, upper) + excess)))
1728        {
1729            break;
1730        }
1731        steps += 1;
1732        if steps > max_steps {
1733            break;
1734        }
1735        // Refine the larger of the two; a pair that can shrink no further
1736        // holds the lower bound where it is.
1737        let split_a = side_a.size(&ea) >= side_b.size(&eb);
1738        let children = if split_a {
1739            side_a.split(&ea)?
1740        } else {
1741            side_b.split(&eb)?
1742        };
1743        let (side, parent) = if split_a {
1744            (&side_a, side_a.size(&ea))
1745        } else {
1746            (&side_b, side_b.size(&eb))
1747        };
1748        if children.iter().any(|child| side.size(child) >= parent) {
1749            heap.push(Reverse((Key(bound), i, j)));
1750            break;
1751        }
1752        for child in children {
1753            if split_a {
1754                elements_a.push(child);
1755                let index = elements_a.len() - 1;
1756                if viable(&child, &eb) {
1757                    heap.push(Reverse((
1758                        Key(lower_bound(&side_a, &child, &side_b, &eb)?),
1759                        index,
1760                        j,
1761                    )));
1762                    offer(&mut best, &child, &eb);
1763                }
1764            } else {
1765                elements_b.push(child);
1766                let index = elements_b.len() - 1;
1767                if viable(&ea, &child) {
1768                    heap.push(Reverse((
1769                        Key(lower_bound(&side_a, &ea, &side_b, &child)?),
1770                        i,
1771                        index,
1772                    )));
1773                    offer(&mut best, &ea, &child);
1774                }
1775            }
1776        }
1777    }
1778    // Every pair still queued bounds from below what is left unexplored.
1779    if let Some(Reverse((Key(bound), _, _))) = heap.peek() {
1780        lower = lower.min(*bound);
1781    }
1782    let (distance, point_a, point_b, on) = best.ok_or(crate::exact::NOT_CONVERGED)?;
1783    let upper = if overlap.is_some() {
1784        lower = 0.0;
1785        0.0
1786    } else {
1787        witnessed_upper(distance, error)
1788    };
1789    Ok(Found {
1790        bounds: DistanceBounds {
1791            lower: lower.min(upper),
1792            upper,
1793            point_a,
1794            point_b,
1795        },
1796        on,
1797        overlap,
1798        #[cfg(test)]
1799        steps,
1800    })
1801}
1802
1803/// When both elements are patches certified inside planar faces that are
1804/// not vertical, and their shadows -- parallelograms -- share an open
1805/// patch: a plan point in it with a margin, and the boundary points above
1806/// it on each face.
1807fn plan_patches_overlap(
1808    side_a: &Side<'_>,
1809    a: &Element,
1810    side_b: &Side<'_>,
1811    b: &Element,
1812) -> Result<Option<(Point2, Point3, Point3)>, ExactMeasureError> {
1813    // The shadow of an inside patch of a planar face: its corners in plan,
1814    // counter-clockwise, and the plane's plan map to invert.
1815    let shadow = |side: &Side<'_>,
1816                  e: &Element|
1817     -> Result<Option<([Point2; 4], axiolid_core::Frame3)>, ExactMeasureError> {
1818        let Shape::Face {
1819            face,
1820            lo,
1821            hi,
1822            inside: true,
1823        } = e.shape
1824        else {
1825            return Ok(None);
1826        };
1827        let Some(f) = level_frame(side, face)? else {
1828            return Ok(None);
1829        };
1830        let map = |u: Scalar, v: Scalar| {
1831            let p = f.origin + f.x * u + f.y * v;
1832            Point2::new(p.x, p.y)
1833        };
1834        let mut corners = [
1835            map(lo.x, lo.y),
1836            map(hi.x, lo.y),
1837            map(hi.x, hi.y),
1838            map(lo.x, hi.y),
1839        ];
1840        let det = f.x.x * f.y.y - f.x.y * f.y.x;
1841        if (hi.x - lo.x) * (hi.y - lo.y) * det < 0.0 {
1842            corners.reverse();
1843        }
1844        Ok(Some((corners, f)))
1845    };
1846    let (Some((pa, fa)), Some((pb, fb))) = (shadow(side_a, a)?, shadow(side_b, b)?) else {
1847        return Ok(None);
1848    };
1849    // Clip one parallelogram by the other; the clip's centroid, if it
1850    // clears every edge of both by a margin, sits in an open patch of both.
1851    let cross =
1852        |o: Point2, p: Point2, q: Point2| (p.x - o.x) * (q.y - o.y) - (p.y - o.y) * (q.x - o.x);
1853    let mut poly: Vec<Point2> = pa.to_vec();
1854    for k in 0..4 {
1855        let (e0, e1) = (pb[k], pb[(k + 1) % 4]);
1856        let input = core::mem::take(&mut poly);
1857        for m in 0..input.len() {
1858            let (p, q) = (input[m], input[(m + 1) % input.len()]);
1859            let (sp, sq) = (cross(e0, e1, p), cross(e0, e1, q));
1860            if sp >= 0.0 {
1861                poly.push(p);
1862            }
1863            if (sp >= 0.0) != (sq >= 0.0) {
1864                let t = sp / (sp - sq);
1865                poly.push(p + (q - p) * t);
1866            }
1867        }
1868        if poly.is_empty() {
1869            return Ok(None);
1870        }
1871    }
1872    let at = poly.iter().fold(Point2::ZERO, |s, p| s + *p) / poly.len() as Scalar;
1873    let scale = pa
1874        .iter()
1875        .chain(pb.iter())
1876        .fold(1.0f64, |m, p| m.max(p.x.abs()).max(p.y.abs()));
1877    let margin = 1e-9 * scale;
1878    let clears = |ring: &[Point2; 4]| {
1879        (0..4).all(|k| {
1880            let (e0, e1) = (ring[k], ring[(k + 1) % 4]);
1881            let length = (e1 - e0).length();
1882            length > 0.0 && cross(e0, e1, at) / length > margin
1883        })
1884    };
1885    if !(clears(&pa) && clears(&pb)) {
1886        return Ok(None);
1887    }
1888    // The points above `at` on each plane, for witnesses.
1889    let lift = |f: axiolid_core::Frame3| {
1890        let det = f.x.x * f.y.y - f.x.y * f.y.x;
1891        let (dx, dy) = (at.x - f.origin.x, at.y - f.origin.y);
1892        let u = (dx * f.y.y - dy * f.y.x) / det;
1893        let v = (f.x.x * dy - f.x.y * dx) / det;
1894        f.origin + f.x * u + f.y * v
1895    };
1896    Ok(Some((at, lift(fa), lift(fb))))
1897}
1898
1899#[cfg(test)]
1900pub(crate) mod fixtures {
1901    //! Boxes built by hand, shared by the tests of the distance search.
1902
1903    use axiolid_brep::{ExactBRep, ExactBRepBuilder};
1904    use axiolid_core::{Frame3, Interval, Point2, Point3, Vec2, Vec3};
1905    use axiolid_curve::{Curve2, Curve3, Line2, Line3};
1906    use axiolid_surface::{Plane, Surface};
1907    use axiolid_topology::{Edge, EdgeUse, Face, FaceBound, Loop, Orientation, Vertex};
1908
1909    /// A box from corner `o` along the edge vectors `x`, `y`, `z`
1910    /// (orthogonal), with twelve shared line edges and six planar faces.
1911    pub(crate) fn cuboid(o: Point3, x: Vec3, y: Vec3, z: Vec3) -> ExactBRep {
1912        let mut builder = ExactBRepBuilder::default();
1913        let axes = [x, y, z];
1914        let corner = |i: usize| {
1915            o + x * (i & 1) as f64 + y * ((i >> 1) & 1) as f64 + z * ((i >> 2) & 1) as f64
1916        };
1917        let vertices: Vec<_> = (0..8)
1918            .map(|i| {
1919                builder.topology_mut().add_vertex(Vertex {
1920                    position: corner(i),
1921                })
1922            })
1923            .collect();
1924        // Edge from corner `i` along axis `k` (bit `k` of `i` clear).
1925        let mut edges = std::collections::HashMap::new();
1926        for i in 0..8usize {
1927            for (k, axis) in axes.iter().enumerate() {
1928                if i & (1 << k) != 0 {
1929                    continue;
1930                }
1931                let length = axis.length();
1932                let curve = builder.add_curve3(Curve3::Line(Line3 {
1933                    origin: corner(i),
1934                    direction: *axis / length,
1935                }));
1936                let edge = builder.topology_mut().add_edge(Edge {
1937                    start: vertices[i],
1938                    end: vertices[i | (1 << k)],
1939                    curve: Some(curve),
1940                });
1941                builder.set_edge_interval(edge, Interval::new(0.0, length));
1942                edges.insert((i, i | (1 << k)), edge);
1943            }
1944        }
1945        // Each face: base corner and its two axes; the third axis fixed.
1946        for (fixed, side) in [(2, 0), (2, 1), (1, 0), (1, 1), (0, 0), (0, 1)] {
1947            let (ka, kb) = match fixed {
1948                0 => (1, 2),
1949                1 => (0, 2),
1950                _ => (0, 1),
1951            };
1952            let base = side << fixed;
1953            let (la, lb) = (axes[ka].length(), axes[kb].length());
1954            let (ux, uy) = (axes[ka] / la, axes[kb] / lb);
1955            let surface = builder.add_surface(Surface::Plane(Plane {
1956                frame: Frame3 {
1957                    origin: corner(base),
1958                    x: ux,
1959                    y: uy,
1960                    z: ux.cross(uy),
1961                },
1962            }));
1963            // Counter-clockwise in (u, v): base, +a, +a+b, +b.
1964            let ring = [
1965                base,
1966                base | (1 << ka),
1967                base | (1 << ka) | (1 << kb),
1968                base | (1 << kb),
1969            ];
1970            let params = [
1971                Point2::new(0.0, 0.0),
1972                Point2::new(la, 0.0),
1973                Point2::new(la, lb),
1974                Point2::new(0.0, lb),
1975            ];
1976            let mut uses = Vec::new();
1977            let mut intervals = Vec::new();
1978            for m in 0..4 {
1979                let (from, to) = (ring[m], ring[(m + 1) % 4]);
1980                let (edge, orientation) = match edges.get(&(from, to)) {
1981                    Some(edge) => (*edge, Orientation::Forward),
1982                    None => (edges[&(to, from)], Orientation::Reversed),
1983                };
1984                let (p, q) = (params[m], params[(m + 1) % 4]);
1985                let length = (q - p).length();
1986                let pcurve = builder.add_curve2(Curve2::Line(Line2 {
1987                    origin: p,
1988                    direction: Vec2::new((q.x - p.x) / length, (q.y - p.y) / length),
1989                }));
1990                uses.push(EdgeUse {
1991                    edge,
1992                    orientation,
1993                    pcurve: Some(pcurve),
1994                });
1995                intervals.push(Interval::new(0.0, length));
1996            }
1997            let loop_id = builder.topology_mut().add_loop(Loop { edges: uses });
1998            for (index, interval) in intervals.into_iter().enumerate() {
1999                builder.set_pcurve_interval(loop_id, index, interval);
2000            }
2001            builder.topology_mut().add_face(Face {
2002                surface: Some(surface),
2003                bounds: vec![FaceBound {
2004                    loop_id,
2005                    orientation: Orientation::Forward,
2006                    outer: true,
2007                }],
2008                orientation: Orientation::Forward,
2009            });
2010        }
2011        builder.finish().expect("a valid box")
2012    }
2013
2014    pub(crate) fn unit_box(o: Point3) -> ExactBRep {
2015        cuboid(o, Vec3::X, Vec3::Y, Vec3::Z)
2016    }
2017}
2018
2019#[cfg(test)]
2020mod tests {
2021    //! The two claims the lower bound rests on, checked by dense sampling:
2022    //! every point of a patch lies in its sphere, and every normal lies in
2023    //! its cone.
2024
2025    use super::{
2026        critical_possible, flat_length, floor, normal_spread, pad, patch_plan_radius, patch_scale,
2027        patch_sphere, plan_lipschitz, witness_scale, witnessed_upper, Element, Metric, Shape, Side,
2028        PROJECTION_EPSILONS,
2029    };
2030    use axiolid_core::{Frame3, Point2, Point3, Vec3};
2031    use axiolid_evaluate::surface::partials;
2032    use axiolid_evaluate::surface::{evaluate, normal};
2033    use axiolid_surface::{Cone, Cylinder, EllipticalCylinder, Plane, Sphere, Surface, Torus};
2034
2035    /// A georeferenced shift (#318).
2036    const FAR: Vec3 = Vec3::new(450_000.0, 5_600_000.0, 0.0);
2037
2038    fn frame_at(shift: Vec3) -> Frame3 {
2039        // Off the origin and turned, so no term vanishes by symmetry.
2040        let x = Vec3::new(0.6, 0.8, 0.0);
2041        let z = Vec3::new(0.0, 0.0, 1.0);
2042        Frame3 {
2043            origin: Point3::new(1.5, -2.0, 0.75) + shift,
2044            x,
2045            y: z.cross(x),
2046            z,
2047        }
2048    }
2049
2050    fn families() -> Vec<(Surface, Point2, Point2)> {
2051        families_at(Vec3::ZERO)
2052    }
2053
2054    fn families_at(shift: Vec3) -> Vec<(Surface, Point2, Point2)> {
2055        let f = frame_at(shift);
2056        vec![
2057            (
2058                Surface::Plane(Plane { frame: f }),
2059                Point2::new(-1.0, 0.5),
2060                Point2::new(2.0, 1.25),
2061            ),
2062            (
2063                Surface::Cylinder(Cylinder {
2064                    frame: f,
2065                    radius: 2.5,
2066                }),
2067                Point2::new(0.3, -1.0),
2068                Point2::new(1.9, 2.0),
2069            ),
2070            (
2071                Surface::EllipticalCylinder(EllipticalCylinder {
2072                    frame: f,
2073                    semi_axis_x: 3.0,
2074                    semi_axis_y: 1.0,
2075                }),
2076                Point2::new(0.2, 0.0),
2077                Point2::new(1.4, 1.0),
2078            ),
2079            (
2080                Surface::Cone(Cone {
2081                    frame: f,
2082                    radius: 1.5,
2083                    semi_angle: 0.4,
2084                }),
2085                Point2::new(0.5, 0.2),
2086                Point2::new(2.5, 1.5),
2087            ),
2088            (
2089                Surface::Sphere(Sphere {
2090                    frame: f,
2091                    radius: 2.0,
2092                }),
2093                Point2::new(0.4, -0.3),
2094                Point2::new(2.0, 1.1),
2095            ),
2096            (
2097                Surface::Torus(Torus {
2098                    frame: f,
2099                    major_radius: 3.0,
2100                    minor_radius: 1.0,
2101                }),
2102                Point2::new(0.1, 0.5),
2103                Point2::new(1.7, 2.9),
2104            ),
2105        ]
2106    }
2107
2108    fn samples(lo: Point2, hi: Point2) -> impl Iterator<Item = Point2> {
2109        const N: usize = 24;
2110        (0..=N).flat_map(move |i| {
2111            (0..=N).map(move |j| {
2112                Point2::new(
2113                    lo.x + (hi.x - lo.x) * i as f64 / N as f64,
2114                    lo.y + (hi.y - lo.y) * j as f64 / N as f64,
2115                )
2116            })
2117        })
2118    }
2119
2120    /// Walls side by side at georeferenced coordinates, turned off the
2121    /// axes so that the witnesses round across the gap (#439): the upper
2122    /// bound covers the distance, and a limit just below it is never
2123    /// `Below`.
2124    /// Coincident witnesses (distance 0) must not defeat the raw short
2125    /// circuit: a plan search that stops on a positive lower bound ends at
2126    /// once for side-by-side boxes, instead of refining to the step cap.
2127    #[test]
2128    fn side_by_side_boxes_in_plan_stop_at_once() {
2129        use super::fixtures::cuboid;
2130        use axiolid_core::Tolerance;
2131        let a = cuboid(Point3::ZERO, Vec3::X, Vec3::Y, Vec3::Z);
2132        let b = cuboid(Point3::new(1.0, 0.0, 0.0), Vec3::X, Vec3::Y, Vec3::Z);
2133        let found = super::search(
2134            &a,
2135            &b,
2136            Tolerance::METRE,
2137            Metric::Plan,
2138            &mut |lower, _, _| lower > 0.0,
2139        )
2140        .expect("bounded");
2141        assert!(found.steps <= 10, "{} steps", found.steps);
2142    }
2143
2144    #[test]
2145    fn an_extreme_weight_ratio_witness_scale_is_refused() {
2146        use crate::exact::ExactMeasureError;
2147        use axiolid_curve::{BSplineSurface, KnotSpec};
2148        let net = |high: f64| {
2149            let mut weights = vec![vec![1.0; 2]; 2];
2150            weights[1][1] = high;
2151            Surface::BSpline(BSplineSurface {
2152                u_degree: 1,
2153                v_degree: 1,
2154                control_points: vec![
2155                    vec![Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 1.0, 0.0)],
2156                    vec![Point3::new(1.0, 0.0, 0.0), Point3::new(1.0, 1.0, 0.0)],
2157                ],
2158                u_knots: vec![0.0, 1.0],
2159                u_multiplicities: vec![2, 2],
2160                v_knots: vec![0.0, 1.0],
2161                v_multiplicities: vec![2, 2],
2162                weights: Some(weights),
2163                u_closed: false,
2164                v_closed: false,
2165                knot_spec: KnotSpec::Unspecified,
2166                self_intersect: None,
2167            })
2168        };
2169        let (lo, hi) = (Point2::new(0.0, 0.0), Point2::new(1.0, 1.0));
2170        assert!(witness_scale(&net(1e6), lo, hi).is_ok());
2171        // 1 / (64 * 2 * eps) is about 3.5e13.
2172        assert!(matches!(
2173            witness_scale(&net(1e14), lo, hi),
2174            Err(ExactMeasureError::NonPlanarFace(
2175                "extreme weight ratio B-spline"
2176            ))
2177        ));
2178    }
2179
2180    #[test]
2181    fn a_far_clearance_is_never_below_a_limit_the_distance_reaches() {
2182        use super::fixtures::cuboid;
2183        use super::{boundary_clearance, boundary_distance, Clearance};
2184        use axiolid_core::Tolerance;
2185        let (x, y, z) = (Vec3::new(0.6, 0.8, 0.0), Vec3::new(-0.8, 0.6, 0.0), Vec3::Z);
2186        for k in 0..40_i32 {
2187            let o = Point3::new(
2188                450_000.0 + f64::from(k) * 0.0137,
2189                5_600_000.0 + f64::from(k) * 0.0291,
2190                0.0,
2191            );
2192            let a = cuboid(o, x * 4.0, y, z * 2.0);
2193            let ob = o
2194                + y * 1.5
2195                + x * (0.5 + f64::from(k % 3) * 0.75)
2196                + z * (0.25 + f64::from(k % 4) * 0.125);
2197            let b = cuboid(ob, x, y, z * 0.5);
2198            // b's -y face lies over a's +y face, parallel: both planes carry
2199            // the unit axis of `x * 4`, and the far differences are exact.
2200            let ux = (x * 4.0) / (x * 4.0).length();
2201            let n = Vec3::new(ux.y, -ux.x, 0.0);
2202            let truth = (ob - (o + y)).dot(n / n.length()).abs();
2203            let d = boundary_distance(&a, &b, 1e-9, Tolerance::METRE).expect("bounded");
2204            assert!(d.lower <= truth && truth <= d.upper, "{k}: {truth} {d:?}");
2205            assert!((d.point_a - d.point_b).length() <= d.upper, "{k}: {d:?}");
2206            let verdict = |limit: f64| {
2207                boundary_clearance(&a, &b, limit, Tolerance::METRE)
2208                    .expect("bounded")
2209                    .1
2210            };
2211            assert_eq!(
2212                verdict(truth - 1e-15),
2213                Clearance::Indeterminate,
2214                "{k}: {truth}"
2215            );
2216            assert_eq!(verdict(truth + 1e-6), Clearance::Below, "{k}");
2217            assert_eq!(verdict(truth - 1e-6), Clearance::Above, "{k}");
2218        }
2219    }
2220
2221    /// A B-spline's points are summed over `p_u + p_v` levels of de Boor
2222    /// and divided by the weight: its witnesses' `M` counts the levels and
2223    /// the weights' spread, not only the largest control point (#439).
2224    #[test]
2225    fn a_spline_witness_scale_counts_its_degrees_and_weights() {
2226        use axiolid_curve::{BSplineSurface, KnotSpec};
2227        let net = |weights: Option<Vec<Vec<f64>>>| {
2228            Surface::BSpline(BSplineSurface {
2229                u_degree: 1,
2230                v_degree: 1,
2231                control_points: vec![
2232                    vec![Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 1.0, 0.0)],
2233                    vec![Point3::new(1.0, 0.0, 0.0), Point3::new(3.0, 4.0, 0.0)],
2234                ],
2235                u_knots: vec![0.0, 1.0],
2236                u_multiplicities: vec![2, 2],
2237                v_knots: vec![0.0, 1.0],
2238                v_multiplicities: vec![2, 2],
2239                weights,
2240                u_closed: false,
2241                v_closed: false,
2242                knot_spec: KnotSpec::Unspecified,
2243                self_intersect: None,
2244            })
2245        };
2246        let (lo, hi) = (Point2::new(0.0, 0.0), Point2::new(1.0, 1.0));
2247        assert_eq!(patch_scale(&net(None), lo, hi).expect("bounded"), 5.0);
2248        assert_eq!(witness_scale(&net(None), lo, hi).expect("bounded"), 15.0);
2249        let rational = net(Some(vec![vec![1.0, 2.0], vec![4.0, 1.0]]));
2250        assert_eq!(witness_scale(&rational, lo, hi).expect("bounded"), 60.0);
2251        // Every other family's witnesses are its patches' points.
2252        for (surface, lo, hi) in families_at(FAR) {
2253            assert_eq!(
2254                witness_scale(&surface, lo, hi).expect("bounded"),
2255                patch_scale(&surface, lo, hi).expect("bounded")
2256            );
2257        }
2258    }
2259
2260    /// A rational bicubic net moved far away by an exact shift: every point
2261    /// evaluated there lies within the witness error of the same point
2262    /// evaluated at the origin and shifted (#439).
2263    #[test]
2264    fn far_spline_points_lie_within_their_witness_error() {
2265        use axiolid_curve::{BSplineSurface, KnotSpec};
2266        let net = |shift: Vec3| {
2267            let control_points = (0..4)
2268                .map(|i| {
2269                    (0..4)
2270                        .map(|j| {
2271                            let (u, v) = (f64::from(i) * 0.37, f64::from(j) * 0.29);
2272                            Point3::new(u, v, 0.1 * (u * 3.1).sin() * (v * 2.3).cos()) + shift
2273                        })
2274                        .collect()
2275                })
2276                .collect();
2277            let weights = (0..4)
2278                .map(|i| {
2279                    (0..4)
2280                        .map(|j| 1.0 + 0.25 * f64::from((i * 3 + j) % 5))
2281                        .collect()
2282                })
2283                .collect();
2284            Surface::BSpline(BSplineSurface {
2285                u_degree: 3,
2286                v_degree: 3,
2287                control_points,
2288                u_knots: vec![0.0, 1.0],
2289                u_multiplicities: vec![4, 4],
2290                v_knots: vec![0.0, 1.0],
2291                v_multiplicities: vec![4, 4],
2292                weights: Some(weights),
2293                u_closed: false,
2294                v_closed: false,
2295                knot_spec: KnotSpec::Unspecified,
2296                self_intersect: None,
2297            })
2298        };
2299        let (near, far) = (net(Vec3::ZERO), net(FAR));
2300        let (lo, hi) = (Point2::new(0.0, 0.0), Point2::new(1.0, 1.0));
2301        let error = 16.0 * f64::EPSILON * witness_scale(&far, lo, hi).expect("bounded");
2302        let mut worst: f64 = 0.0;
2303        for p in samples(lo, hi) {
2304            let there = evaluate(&far, p.x, p.y).expect("point");
2305            let here = evaluate(&near, p.x, p.y).expect("point") + FAR;
2306            worst = worst.max((there - here).length());
2307        }
2308        // `here` carries the rounding of the shift itself, half an ulp a coordinate.
2309        assert!(worst <= error, "{worst} > {error}");
2310    }
2311
2312    #[test]
2313    fn rounding_margins_are_the_derived_constants() {
2314        assert_eq!(pad(0.0, 1.0), 16.0 * f64::EPSILON + f64::MIN_POSITIVE);
2315        assert_eq!(floor(3.0, 0.0), 128.0 * f64::EPSILON * 3.0);
2316        assert_eq!(
2317            witnessed_upper(0.5, 1e-9),
2318            0.5 * (1.0 + 8.0 * f64::EPSILON) + 1e-9
2319        );
2320        let origin = Point3::new(450_000.0, 5_600_000.0, 0.0);
2321        let plane = Surface::Plane(Plane {
2322            frame: Frame3 {
2323                origin,
2324                x: Vec3::X,
2325                y: Vec3::Y,
2326                z: Vec3::Z,
2327            },
2328        });
2329        let scale = patch_scale(&plane, Point2::new(0.0, 0.0), Point2::new(1.0, 1.0));
2330        assert_eq!(scale.expect("bounded"), origin.length() + 2.0);
2331    }
2332
2333    /// `M(child) <= M(parent)`: the pad of a side, taken from its root
2334    /// elements, covers every element a split yields.
2335    #[test]
2336    fn child_scales_never_exceed_their_parent() {
2337        for shift in [Vec3::ZERO, FAR] {
2338            for (surface, lo, hi) in families_at(shift) {
2339                let whole = patch_scale(&surface, lo, hi).expect("bounded");
2340                let mid = (lo + hi) * 0.5;
2341                for (a, b) in [
2342                    (lo, mid),
2343                    (Point2::new(mid.x, lo.y), Point2::new(hi.x, mid.y)),
2344                    (Point2::new(lo.x, mid.y), Point2::new(mid.x, hi.y)),
2345                    (mid, hi),
2346                ] {
2347                    let part = patch_scale(&surface, a, b).expect("bounded");
2348                    assert!(part <= whole, "{surface:?}: {part} > {whole}");
2349                }
2350            }
2351        }
2352    }
2353
2354    #[test]
2355    fn every_far_patch_point_lies_in_its_sphere() {
2356        for (surface, lo, hi) in families_at(FAR) {
2357            let scale = patch_scale(&surface, lo, hi).expect("bounded");
2358            let (centre, radius) = patch_sphere(&surface, lo, hi, scale).expect("bounded");
2359            let mut reach: f64 = 0.0;
2360            for p in samples(lo, hi) {
2361                let point = evaluate(&surface, p.x, p.y).expect("point");
2362                reach = reach.max((point - centre).length());
2363            }
2364            assert!(
2365                reach <= radius,
2366                "{surface:?}: reach {reach} > radius {radius}"
2367            );
2368            assert!(
2369                radius <= 4.0 * reach + 1e-9,
2370                "{surface:?}: {radius} vs {reach}"
2371            );
2372        }
2373    }
2374
2375    #[test]
2376    fn a_far_projection_carries_its_pad() {
2377        use super::fixtures::unit_box;
2378        let b = unit_box(Point3::ZERO + FAR);
2379        let side = Side::new(&b, 1e-12, Metric::Space).expect("a side");
2380        let top = side
2381            .elements
2382            .iter()
2383            .find(|e| matches!(e.shape, Shape::Face { .. }) && e.centre.z == 1.0)
2384            .expect("the top face");
2385        let (lo, hi) = side.project(top, Vec3::Z).expect("projects");
2386        let pad = PROJECTION_EPSILONS * f64::EPSILON * side.scale;
2387        assert!(pad > 1e-9, "{pad}");
2388        assert!(hi - lo >= 2.0 * pad - 4.0 * f64::EPSILON, "[{lo}, {hi}]");
2389    }
2390
2391    #[test]
2392    fn every_patch_point_lies_in_its_sphere() {
2393        for (surface, lo, hi) in families() {
2394            let scale = patch_scale(&surface, lo, hi).expect("bounded");
2395            let (centre, radius) = patch_sphere(&surface, lo, hi, scale).expect("bounded");
2396            let mut reach: f64 = 0.0;
2397            for p in samples(lo, hi) {
2398                let point = evaluate(&surface, p.x, p.y).expect("point");
2399                reach = reach.max((point - centre).length());
2400            }
2401            assert!(
2402                reach <= radius,
2403                "{surface:?}: reach {reach} > radius {radius}"
2404            );
2405            // Not vacuous: the bound is within a small factor of the reach.
2406            assert!(
2407                radius <= 4.0 * reach + 1e-9,
2408                "{surface:?}: {radius} vs {reach}"
2409            );
2410        }
2411    }
2412
2413    /// Every point of a patch projects within its plan radius of the
2414    /// centre's projection -- for the frame of `families` and for one
2415    /// tilted off the vertical, where every axis has a horizontal part --
2416    /// and the plan radius is not vacuous.
2417    #[test]
2418    fn every_patch_projection_lies_in_its_plan_disc() {
2419        let tilt = |surface: Surface| -> Surface {
2420            let x = Vec3::new(0.6, 0.64, 0.48);
2421            let z = Vec3::new(0.0, -0.6, 0.8);
2422            let y = z.cross(x);
2423            let f = Frame3 {
2424                origin: Point3::new(1.5, -2.0, 0.75),
2425                x,
2426                y,
2427                z,
2428            };
2429            match surface {
2430                Surface::Plane(_) => Surface::Plane(Plane { frame: f }),
2431                Surface::Cylinder(c) => Surface::Cylinder(Cylinder { frame: f, ..c }),
2432                Surface::EllipticalCylinder(c) => {
2433                    Surface::EllipticalCylinder(EllipticalCylinder { frame: f, ..c })
2434                }
2435                Surface::Cone(c) => Surface::Cone(Cone { frame: f, ..c }),
2436                Surface::Sphere(c) => Surface::Sphere(Sphere { frame: f, ..c }),
2437                Surface::Torus(c) => Surface::Torus(Torus { frame: f, ..c }),
2438                other => other,
2439            }
2440        };
2441        let cases = families()
2442            .into_iter()
2443            .flat_map(|(s, lo, hi)| [(s.clone(), lo, hi), (tilt(s), lo, hi)]);
2444        for (surface, lo, hi) in cases {
2445            let scale = patch_scale(&surface, lo, hi).expect("bounded");
2446            let (centre, radius) = patch_sphere(&surface, lo, hi, scale).expect("bounded");
2447            let plan = patch_plan_radius(&surface, lo, hi, centre, radius, scale).expect("bounded");
2448            assert!(plan <= radius);
2449            let mut reach: f64 = 0.0;
2450            for p in samples(lo, hi) {
2451                let point = evaluate(&surface, p.x, p.y).expect("point");
2452                reach = reach.max((point.x - centre.x).hypot(point.y - centre.y));
2453            }
2454            assert!(
2455                reach <= plan,
2456                "{surface:?}: reach {reach} > plan radius {plan}"
2457            );
2458            assert!(plan <= 4.0 * reach + 1e-9, "{surface:?}: {plan} vs {reach}");
2459        }
2460    }
2461
2462    /// The horizontal speeds bound every horizontal derivative, over a whole
2463    /// turn, on a frame tilted so the axes' shadows are not orthogonal:
2464    /// there the steepest horizontal turn exceeds either axis's shadow.
2465    #[test]
2466    fn plan_speeds_bound_every_horizontal_derivative() {
2467        let x = Vec3::new(0.6, 0.64, 0.48);
2468        let z = Vec3::new(0.0, -0.6, 0.8);
2469        let f = Frame3 {
2470            origin: Point3::new(1.5, -2.0, 0.75),
2471            x,
2472            y: z.cross(x),
2473            z,
2474        };
2475        let (lo, hi) = (
2476            Point2::new(0.0, 0.2),
2477            Point2::new(core::f64::consts::TAU, 1.5),
2478        );
2479        for surface in [
2480            Surface::Cylinder(Cylinder {
2481                frame: f,
2482                radius: 2.5,
2483            }),
2484            Surface::EllipticalCylinder(EllipticalCylinder {
2485                frame: f,
2486                semi_axis_x: 3.0,
2487                semi_axis_y: 1.0,
2488            }),
2489            Surface::Cone(Cone {
2490                frame: f,
2491                radius: 1.5,
2492                semi_angle: 0.4,
2493            }),
2494        ] {
2495            let (lu, lv) = plan_lipschitz(&surface, lo, hi).expect("bounded");
2496            let (mut su, mut sv) = (0.0f64, 0.0f64);
2497            for i in 0..=720 {
2498                for j in 0..=8 {
2499                    let u = lo.x + (hi.x - lo.x) * f64::from(i) / 720.0;
2500                    let v = lo.y + (hi.y - lo.y) * f64::from(j) / 8.0;
2501                    let (du, dv) = partials(&surface, u, v).expect("partials");
2502                    su = su.max(flat_length(du));
2503                    sv = sv.max(flat_length(dv));
2504                }
2505            }
2506            assert!(su <= lu * (1.0 + 1e-12), "{surface:?}: {su} > {lu}");
2507            assert!(sv <= lv * (1.0 + 1e-12), "{surface:?}: {sv} > {lv}");
2508        }
2509    }
2510
2511    /// A wall patch is kept for an element across from it in plan, however
2512    /// far above: only the plan offset has to meet its normal.
2513    #[test]
2514    fn a_wall_patch_faces_an_element_high_above_in_plan() {
2515        let element = |centre: Point3, normal: Option<Vec3>, spread: Option<f64>| Element {
2516            shape: Shape::Edge {
2517                edge: 0,
2518                t0: 0.0,
2519                t1: 1.0,
2520            },
2521            centre,
2522            radius: 0.1,
2523            plan_radius: 0.1,
2524            witness: None,
2525            normal,
2526            spread,
2527        };
2528        let wall = element(Point3::ZERO, Some(Vec3::X), Some(0.01));
2529        let above = element(Point3::new(10.0, 0.0, 10.0), None, None);
2530        assert!(critical_possible(&wall, &above, Metric::Plan));
2531        // In space the offset turns 45 degrees off the normal: dropped.
2532        assert!(!critical_possible(&wall, &above, Metric::Space));
2533    }
2534
2535    #[test]
2536    fn every_patch_normal_lies_in_its_cone() {
2537        let mid = |lo: Point2, hi: Point2| (lo + hi) * 0.5;
2538        for (surface, lo, hi) in families() {
2539            let Some(spread) = normal_spread(&surface, lo, hi) else {
2540                continue;
2541            };
2542            let c = mid(lo, hi);
2543            let axis = normal(&surface, c.x, c.y).expect("normal");
2544            let mut widest: f64 = 0.0;
2545            for p in samples(lo, hi) {
2546                let n = normal(&surface, p.x, p.y).expect("normal");
2547                widest = widest.max(n.dot(axis).clamp(-1.0, 1.0).acos());
2548            }
2549            assert!(
2550                widest <= spread,
2551                "{surface:?}: normals turn {widest} > {spread}"
2552            );
2553        }
2554    }
2555}