Skip to main content

axiolid_measure/
exact_hausdorff.rs

1//! Certified Hausdorff distance between the boundaries of two exact B-reps
2//! (#224).
3//!
4//! # What is certified
5//!
6//! [`one_sided_boundary_hausdorff`] returns an interval `[lower, upper]`
7//! certain to contain `h(A, B) = max over p in dA of d(p, dB)`, how far the
8//! boundary of `A` strays from the boundary of `B`, and
9//! [`boundary_hausdorff_distance`] the two-sided `max(h(A, B), h(B, A))`
10//! with both one-sided intervals: the contract of
11//! [`crate::hausdorff_distance`] for meshes, on the exact boundaries instead
12//! of a tessellation of them.
13//! Both ends are guarantees:
14//!
15//! - `lower` is `d(p, dB)` bounded below for a point `p` certainly on `dA`
16//!   (an edge point, or a surface point at parameters its face's domain is
17//!   certified to contain), less `p`'s evaluation rounding (#439), by the
18//!   same branch and bound as [`crate::boundary_distance`] with one side shrunk to the point.
19//! - `upper` bounds `d(p, dB)` for every point of `dA`. The faces of `A` are
20//!   covered by patches of their parameter domains (a patch is dropped only
21//!   when certified outside its face), and each patch's bound is the better
22//!   of two:
23//!   - a *matched* bound: when a face of `B` has the same surface family
24//!     (for a cone the same radius and angle, for a B-spline the same knots,
25//!     degrees and weights) and the very same trimming pcurves with the same
26//!     intervals, both faces are one domain `D` mapped by `S_A` and `S_B`,
27//!     so every `S_A(u, v)` of the patch has
28//!     the boundary point `S_B(u, v)` of `B` within `|S_A - S_B|`. Written
29//!     over the family's basis, `S_A - S_B = (o_A - o_B) + sum phi_k (a_k^A -
30//!     a_k^B)`, bounded over the patch by its value at the centre plus each
31//!     basis function's oscillation times its coefficient difference. A
32//!     rigid move of a solid changes only frames, so for a pure translation
33//!     the bound is the translation itself on every patch at once, and for
34//!     an identical copy zero up to rounding. For a rotation it is the
35//!     displacement of the rotation, which may exceed the distance: it then
36//!     only caps what the Lipschitz bound must refine;
37//!   - the same bound for a face of `B` that is a translate of `A`'s but
38//!     trimmed in another chart (#227), as a builder that trims in world
39//!     coordinates makes it: same family, axes and shape, and trims equal
40//!     after the parameter shift the translation induces, to within a
41//!     measured residue. `S_B` re-charted by the shift is again one domain
42//!     with `S_A`, the residue is folded in, and the bound is `|t|`. A
43//!     turned or resized face is never matched this way (the derivation and
44//!     the gate are in the private `exact_hausdorff/translate.rs`);
45//!   - a *Lipschitz* bound: `d(., dB)` is 1-Lipschitz, so over a patch inside
46//!     a sphere of radius `r` about `c` it is at most `d(c, dB) + r`, with
47//!     `d(c, dB)` bounded above by a point found on `dB` plus that point's
48//!     evaluation rounding (#439). A B-spline patch's sphere is
49//!     about the control net's box midpoint, not its witness, so the bound
50//!     about the witness adds their distance (#459).
51//!
52//! The witnesses are `point_from`, the point realising `lower`, and
53//! `point_to`, the nearest point found to it on the other boundary.
54//!
55//! # Method
56//!
57//! Branch and bound over `A`'s face patches, largest upper bound first: the
58//! patch is split across its longer side, and each half is bounded and, if
59//! its centre is certainly on the face, measured to raise `lower`. Edge
60//! spans of `A` are split alongside, only to place witnesses (the faces'
61//! closures already hold every edge point), so a farthest point on an edge
62//! is approached along the edge. Point queries walk a tree of `B`'s
63//! elements that is refined once and shared by every query.
64//!
65//! Before any split, each translation `t` a matched face carries seeds the
66//! lower bound with `A`'s support point against `t` on its edges: for a
67//! translate that point is exactly `|t|` from `B`'s boundary, whatever the
68//! shape, so a farthest point isolated at a corner is not left to edge
69//! bisection. For a body of several items ([`crate::exact_bodies`]) each
70//! item's own support point is seeded: an item moved alone has its
71//! farthest point there, wherever the union's support point lies.
72//!
73//! # Convergence and refusal
74//!
75//! Copies offset by a translation (and identical copies) close in a few
76//! dozen splits whatever the accuracy, and usually in none: the matched
77//! bound holds every patch at `|t|` at once, and the support point is `|t|`
78//! from the other boundary when the solid's support against `t` is on an
79//! edge (faces on planes, cylinders and cones). Elsewhere the Lipschitz bound closes at
80//! first order: an accuracy `e` needs patches of radius about `e` wherever
81//! the distance comes within `e` of the farthest, so the work grows with
82//! that area over `e^2`. Refinement stops at a fixed budget, or at the
83//! caller's ([`one_sided_boundary_hausdorff_with_budget`]); the interval
84//! returned is then still sound, only wider than asked -- a caller decides
85//! by its width, never by a guess. A face whose domain cannot be bounded is
86//! refused as by [`crate::boundary_distance`].
87//!
88//! # Scope
89//!
90//! This is the distance between BOUNDARIES. A solid wholly inside another
91//! is as far from it as its boundary is from the other's boundary, not
92//! zero; containment is a separate classification.
93
94use std::cmp::{Ordering, Reverse};
95use std::collections::BinaryHeap;
96
97use axiolid_brep::ExactBRep;
98use axiolid_core::{Point2, Point3, Scalar, Tolerance, Vec3};
99use axiolid_surface::Surface;
100use axiolid_topology::Face;
101
102use crate::exact::ExactMeasureError;
103use crate::exact_distance::witness;
104use crate::exact_distance::Key as DistanceKey;
105use crate::exact_distance::{
106    closed, critical_toward, floor, point_lower_bound, surface_of, Element, Metric, Shape, Side,
107    POINT_EPSILONS, RATIO_EPSILONS,
108};
109use crate::mesh_hausdorff::HausdorffBounds;
110
111mod cut;
112mod translate;
113
114pub(crate) use cut::Cut;
115use cut::CutBounds;
116
117use axiolid_evaluate::surface::evaluate;
118use translate::{support_point, translate, Shifted};
119
120/// Splits of the measured boundary's patches and edge spans before a
121/// one-sided query reports what it has.
122pub(crate) const MAX_SPLITS: usize = 200_000;
123
124/// Refinement steps one point query may take.
125const POINT_STEPS: usize = 20_000;
126
127/// Translations of matched faces whose support points seed the lower
128/// bound; a translate gives one.
129const MAX_DISPLACEMENTS: usize = 16;
130
131/// The two-sided Hausdorff distance between two exact boundaries and both
132/// one-sided ones.
133#[derive(Debug, Clone, Copy, PartialEq)]
134pub struct BoundaryHausdorff {
135    /// `max(h(A, B), h(B, A))`. The witness is that of the side with the
136    /// larger lower bound; `point_from` lies on that side's boundary.
137    pub distance: HausdorffBounds,
138    /// `h(A, B)`: how far `A`'s boundary strays from `B`'s. Witness from
139    /// `A` to `B`.
140    pub forward: HausdorffBounds,
141    /// `h(B, A)`: how far `B`'s boundary strays from `A`'s. Witness from
142    /// `B` to `A`.
143    pub backward: HausdorffBounds,
144}
145
146/// One-sided Hausdorff distance `h(from, to)` between two exact boundaries,
147/// to within `accuracy` (#224).
148///
149/// Refines until `upper - lower <= accuracy` or the budget runs out; the
150/// interval is sound either way. A negative or NaN accuracy is read as
151/// zero, as by [`crate::boundary_distance`]: refine as far as the budget
152/// allows.
153///
154/// # Errors
155///
156/// As [`crate::boundary_distance`]: a face whose domain cannot be bounded,
157/// an evaluation failure, or a boundary with no boundable element; and
158/// [`ExactMeasureError::NonPlanarFace`] (not converged) when no point of
159/// `from` could be certified on its boundary.
160pub fn one_sided_boundary_hausdorff(
161    from: &ExactBRep,
162    to: &ExactBRep,
163    accuracy: Scalar,
164    tolerance: Tolerance,
165) -> Result<HausdorffBounds, ExactMeasureError> {
166    one_sided_boundary_hausdorff_with_budget(from, to, accuracy, tolerance, MAX_SPLITS)
167}
168
169/// [`one_sided_boundary_hausdorff`] with the caller's refinement budget: at
170/// most `max_splits` splits of `from`'s face patches and edge spans (#227).
171///
172/// The interval is sound whatever the budget; a small one only leaves it
173/// wider than `accuracy`. Translated and identical copies close in a few
174/// dozen splits, so a caller comparing versions of a model can cap the work
175/// per pair and read an interval left open as "not evaluated".
176///
177/// # Errors
178///
179/// As [`one_sided_boundary_hausdorff`].
180pub fn one_sided_boundary_hausdorff_with_budget(
181    from: &ExactBRep,
182    to: &ExactBRep,
183    accuracy: Scalar,
184    tolerance: Tolerance,
185    max_splits: usize,
186) -> Result<HausdorffBounds, ExactMeasureError> {
187    // One item: every edge.
188    let every = 0..from.topology().edges().len();
189    let edges = core::slice::from_ref(&every);
190    Ok(witnessed(from, to, accuracy, tolerance, max_splits, edges, None)?.bounds)
191}
192
193/// A one-sided interval and the elements its witnesses lie on.
194pub(crate) struct Witnessed {
195    pub(crate) bounds: HausdorffBounds,
196    /// The element of `from` holding `point_from`.
197    pub(crate) from: Shape,
198    /// The element of `to` holding `point_to`.
199    pub(crate) to: Shape,
200}
201
202/// [`one_sided_boundary_hausdorff_with_budget`], naming the elements the
203/// witnesses lie on. `items` partitions `from`'s edges by item: each item's
204/// support point against a matched translation seeds the lower bound, so
205/// an item moved alone is seeded even where another holds the union's.
206/// `cuts` says how each boundary was cut from its items where they share
207/// patches of face (see `cut`).
208#[allow(clippy::too_many_arguments)]
209pub(crate) fn witnessed(
210    from: &ExactBRep,
211    to: &ExactBRep,
212    accuracy: Scalar,
213    tolerance: Tolerance,
214    max_splits: usize,
215    items: &[core::ops::Range<usize>],
216    cuts: Option<(&Cut<'_>, &Cut<'_>)>,
217) -> Result<Witnessed, ExactMeasureError> {
218    let accuracy = accuracy.max(0.0);
219    let linear = tolerance.linear().max(1e-12);
220    let source = Side::new(from, linear, Metric::Space)?;
221    let mut target = Nearest::new(Side::new(to, linear, Metric::Space)?);
222    let mut cuts = cuts.map(|(from, to)| CutBounds::new(from, to));
223    search(
224        &source,
225        &mut target,
226        accuracy,
227        max_splits,
228        items,
229        cuts.as_mut(),
230    )
231}
232
233/// Two-sided Hausdorff distance between the boundaries of `a` and `b`, to
234/// within `accuracy` (#224).
235///
236/// Both one-sided distances are refined to `accuracy`, so each of the three
237/// intervals is at most that wide unless the budget ran out.
238///
239/// # Errors
240///
241/// As [`one_sided_boundary_hausdorff`].
242pub fn boundary_hausdorff_distance(
243    a: &ExactBRep,
244    b: &ExactBRep,
245    accuracy: Scalar,
246    tolerance: Tolerance,
247) -> Result<BoundaryHausdorff, ExactMeasureError> {
248    let forward = one_sided_boundary_hausdorff(a, b, accuracy, tolerance)?;
249    let backward = one_sided_boundary_hausdorff(b, a, accuracy, tolerance)?;
250    let mut distance = if backward.lower > forward.lower {
251        backward
252    } else {
253        forward
254    };
255    distance.upper = forward.upper.max(backward.upper);
256    Ok(BoundaryHausdorff {
257        distance,
258        forward,
259        backward,
260    })
261}
262
263/// Total order on finite bounds, ties broken first come, first served.
264#[derive(Debug, Clone, Copy, PartialEq)]
265struct Key(Scalar, Reverse<usize>);
266
267impl Eq for Key {}
268
269impl PartialOrd for Key {
270    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
271        Some(self.cmp(other))
272    }
273}
274
275impl Ord for Key {
276    fn cmp(&self, other: &Self) -> Ordering {
277        self.0.total_cmp(&other.0).then(self.1.cmp(&other.1))
278    }
279}
280
281/// Point queries against one boundary, over a tree of its elements refined
282/// on demand and kept for the next query.
283struct Nearest<'a> {
284    side: Side<'a>,
285    nodes: Vec<Element>,
286    children: Vec<Option<Vec<usize>>>,
287    roots: Vec<usize>,
288}
289
290/// A certified interval on a point's distance to a boundary, and the
291/// nearest boundary point found.
292#[derive(Debug, Clone, Copy)]
293struct PointDistance {
294    lower: Scalar,
295    upper: Scalar,
296    nearest: Point3,
297    /// The element `nearest` lies on.
298    on: Shape,
299}
300
301impl<'a> Nearest<'a> {
302    fn new(side: Side<'a>) -> Self {
303        let nodes = side.elements.clone();
304        let roots = (0..nodes.len()).collect();
305        let children = vec![None; nodes.len()];
306        Self {
307            side,
308            nodes,
309            children,
310            roots,
311        }
312    }
313
314    fn children(&mut self, node: usize) -> Result<Vec<usize>, ExactMeasureError> {
315        if let Some(known) = &self.children[node] {
316            return Ok(known.clone());
317        }
318        let parent = self.side.size(&self.nodes[node]);
319        let split = self.side.split(&self.nodes[node])?;
320        // A pair that can shrink no further is a leaf.
321        let ids = if split.iter().any(|child| self.side.size(child) >= parent) {
322            Vec::new()
323        } else {
324            split
325                .into_iter()
326                .map(|child| {
327                    self.nodes.push(child);
328                    self.children.push(None);
329                    self.nodes.len() - 1
330                })
331                .collect()
332        };
333        self.children[node] = Some(ids.clone());
334        Ok(ids)
335    }
336
337    /// `d(p, boundary)` to within `accuracy` (never finer than the
338    /// rounding floor of [`floor`]), or as far as the step budget allows; `None` when no boundary point was reached.
339    fn query(
340        &mut self,
341        p: Point3,
342        accuracy: Scalar,
343    ) -> Result<Option<PointDistance>, ExactMeasureError> {
344        let scale = p.length() + self.side.scale;
345        let error = self.side.witness_error();
346        let excess = self.side.witness_excess();
347        // Ties pop first come, first served: the third entry counts up.
348        let mut heap = BinaryHeap::new();
349        let mut order = 0;
350        let mut best: Option<(Scalar, Point3, Shape)> = None;
351        for &root in &self.roots {
352            let node = &self.nodes[root];
353            if critical_toward(node, p) {
354                let bound = point_lower_bound(p, &self.side, node)?;
355                heap.push(Reverse((DistanceKey(bound), order, root)));
356                order += 1;
357                offer(&mut best, p, node);
358            }
359        }
360        let mut lower = 0.0;
361        let mut steps = 0;
362        while let Some(Reverse((DistanceKey(bound), popped, node))) = heap.pop() {
363            lower = bound;
364            let upper = best.map_or(Scalar::INFINITY, |(d, ..)| d);
365            // The boundary point of the node nearest `p` may close the gap
366            // where the bound cannot rise (#318).
367            if bound < upper {
368                let element = self.nodes[node];
369                if let Some((q, on)) = witness::toward(p, &self.side, &element)? {
370                    let d = (q - p).length();
371                    if best.is_none_or(|(current, ..)| d < current) {
372                        best = Some((d, q, on));
373                    }
374                }
375            }
376            let upper = best.map_or(Scalar::INFINITY, |(d, ..)| {
377                super::exact_distance::witnessed_upper(d, error)
378            });
379            if upper.is_finite()
380                && (bound >= upper || closed(bound, upper, accuracy, floor(scale, upper) + excess))
381            {
382                break;
383            }
384            steps += 1;
385            let children = self.children(node)?;
386            if steps > POINT_STEPS || children.is_empty() {
387                heap.push(Reverse((DistanceKey(bound), popped, node)));
388                break;
389            }
390            for child in children {
391                let element = &self.nodes[child];
392                if critical_toward(element, p) {
393                    let bound = point_lower_bound(p, &self.side, element)?;
394                    heap.push(Reverse((DistanceKey(bound), order, child)));
395                    order += 1;
396                    offer(&mut best, p, element);
397                }
398            }
399        }
400        if let Some(Reverse((DistanceKey(bound), ..))) = heap.peek() {
401            lower = Scalar::min(lower, *bound);
402        }
403        Ok(best.map(|(d, nearest, on)| {
404            let upper = super::exact_distance::witnessed_upper(d, error);
405            PointDistance {
406                lower: lower.min(upper),
407                upper,
408                nearest,
409                on,
410            }
411        }))
412    }
413}
414
415/// Count `element`'s witness, a point certainly on the boundary, towards
416/// `best`: as soon as its node is queued, so that refining patches whose
417/// bound cannot rise (a planar patch along its own normal) never runs
418/// ahead of the witnesses already in hand (#318). Ties keep the first.
419fn offer(best: &mut Option<(Scalar, Point3, Shape)>, p: Point3, element: &Element) {
420    if let Some(w) = element.witness {
421        let d = (w - p).length();
422        if best.is_none_or(|(current, ..)| d < current) {
423            *best = Some((d, w, element.shape));
424        }
425    }
426}
427
428/// A face of `B` matched to a face of `A`: on the same parameters, or on
429/// `A`'s parameters once re-charted by a translate's shift.
430#[derive(Debug, Clone)]
431struct Match {
432    face: usize,
433    shifted: Option<Shifted>,
434}
435
436/// The faces of `B` matched to each face of `A` (see the module docs).
437fn matches(a: &ExactBRep, b: &ExactBRep) -> Vec<Vec<Match>> {
438    let fa = a.topology().faces();
439    let fb = b.topology().faces();
440    fa.iter()
441        .map(|face_a| {
442            fb.iter()
443                .enumerate()
444                .filter_map(|(face, face_b)| {
445                    if same_face(a, face_a, b, face_b) {
446                        Some(Match {
447                            face,
448                            shifted: None,
449                        })
450                    } else {
451                        translate(a, face_a, b, face_b).map(|shifted| Match {
452                            face,
453                            shifted: Some(shifted),
454                        })
455                    }
456                })
457                .collect()
458        })
459        .collect()
460}
461
462/// Same surface family (see [`same_shape`]), and the very same trimming
463/// pcurves with the same intervals in the same loops: one domain on both.
464fn same_face(
465    a: &ExactBRep,
466    fa: &Face<axiolid_brep::SurfaceId>,
467    b: &ExactBRep,
468    fb: &Face<axiolid_brep::SurfaceId>,
469) -> bool {
470    let (Ok(sa), Ok(sb)) = (surface_of(a, fa.surface), surface_of(b, fb.surface)) else {
471        return false;
472    };
473    if !same_shape(sa, sb) || fa.bounds.len() != fb.bounds.len() {
474        return false;
475    }
476    let (ta, tb) = (a.topology(), b.topology());
477    fa.bounds.iter().zip(&fb.bounds).all(|(ba, bb)| {
478        if ba.orientation != bb.orientation || ba.outer != bb.outer {
479            return false;
480        }
481        let (Some(la), Some(lb)) = (
482            ta.loops().get(ba.loop_id.index()),
483            tb.loops().get(bb.loop_id.index()),
484        ) else {
485            return false;
486        };
487        la.edges.len() == lb.edges.len()
488            && la
489                .edges
490                .iter()
491                .zip(&lb.edges)
492                .enumerate()
493                .all(|(i, (ua, ub))| {
494                    let (Some(ca), Some(cb)) = (pcurve(a, ua.pcurve), pcurve(b, ub.pcurve)) else {
495                        return false;
496                    };
497                    let ia = a.pcurve_interval(ba.loop_id, i);
498                    let ib = b.pcurve_interval(bb.loop_id, i);
499                    ia.is_some() && ia == ib && ca == cb
500                })
501    })
502}
503
504fn pcurve(brep: &ExactBRep, id: Option<axiolid_brep::Curve2Id>) -> Option<&axiolid_curve::Curve2> {
505    id.and_then(|id| brep.curves2().get(id.index()))
506}
507
508/// Whether one parameter domain stands for a face on both surfaces, so
509/// that `S_B(u, v)` is on `B`'s face wherever `S_A(u, v)` is on `A`'s: the
510/// same family, whose chart (periods and poles) is then the same -- for a
511/// cone the same radius and angle, which place its apex, and for a
512/// B-spline the same degrees, knots and positive weights, so that the
513/// difference is a convex combination of control point moves. Radii may
514/// otherwise differ: they are folded into the coefficients.
515fn same_shape(a: &Surface, b: &Surface) -> bool {
516    match (a, b) {
517        (Surface::Plane(_), Surface::Plane(_))
518        | (Surface::Cylinder(_), Surface::Cylinder(_))
519        | (Surface::EllipticalCylinder(_), Surface::EllipticalCylinder(_))
520        | (Surface::Sphere(_), Surface::Sphere(_))
521        | (Surface::Torus(_), Surface::Torus(_)) => true,
522        (Surface::Cone(p), Surface::Cone(q)) => {
523            p.radius == q.radius && p.semi_angle == q.semi_angle
524        }
525        (Surface::BSpline(p), Surface::BSpline(q)) => {
526            let shape = |s: &axiolid_surface::BSplineSurface| {
527                s.control_points.iter().map(Vec::len).collect::<Vec<_>>()
528            };
529            p.u_degree == q.u_degree
530                && p.v_degree == q.v_degree
531                && p.u_knots == q.u_knots
532                && p.v_knots == q.v_knots
533                && p.u_multiplicities == q.u_multiplicities
534                && p.v_multiplicities == q.v_multiplicities
535                && p.weights == q.weights
536                && shape(p) == shape(q)
537                && p.weights
538                    .as_ref()
539                    .is_none_or(|w| w.iter().flatten().all(|w| w.is_finite() && *w > 0.0))
540        }
541        _ => false,
542    }
543}
544
545/// A surface as `o + sum phi_k(u, v) a_k`: its origin and coefficient
546/// vectors, over the family's basis (see [`basis`]).
547fn terms(surface: &Surface) -> Option<(Point3, Vec<Vec3>)> {
548    Some(match surface {
549        Surface::Plane(p) => (p.frame.origin, vec![p.frame.x, p.frame.y]),
550        Surface::Cylinder(c) => (
551            c.frame.origin,
552            vec![c.frame.x * c.radius, c.frame.y * c.radius, c.frame.z],
553        ),
554        Surface::EllipticalCylinder(c) => (
555            c.frame.origin,
556            vec![
557                c.frame.x * c.semi_axis_x,
558                c.frame.y * c.semi_axis_y,
559                c.frame.z,
560            ],
561        ),
562        Surface::Cone(c) => {
563            let slope = c.semi_angle.tan();
564            (
565                c.frame.origin,
566                vec![
567                    c.frame.x * c.radius,
568                    c.frame.y * c.radius,
569                    c.frame.x * slope,
570                    c.frame.y * slope,
571                    c.frame.z,
572                ],
573            )
574        }
575        Surface::Sphere(s) => (
576            s.frame.origin,
577            vec![
578                s.frame.x * s.radius,
579                s.frame.y * s.radius,
580                s.frame.z * s.radius,
581            ],
582        ),
583        Surface::Torus(t) => (
584            t.frame.origin,
585            vec![
586                t.frame.x * t.major_radius,
587                t.frame.y * t.major_radius,
588                t.frame.x * t.minor_radius,
589                t.frame.y * t.minor_radius,
590                t.frame.z * t.minor_radius,
591            ],
592        ),
593        _ => return None,
594    })
595}
596
597/// The family's basis at `(u, v)`, each function's largest oscillation
598/// from its value at the patch centre `m` over the patch, and each one's
599/// largest magnitude there.
600fn basis(
601    surface: &Surface,
602    lo: Point2,
603    hi: Point2,
604) -> Option<(Vec<Scalar>, Vec<Scalar>, Vec<Scalar>)> {
605    let m = (lo + hi) * 0.5;
606    let (hu, hv) = (0.5 * (hi.x - lo.x).abs(), 0.5 * (hi.y - lo.y).abs());
607    // `cos` and `sin` are 1-Lipschitz and stay within [-1, 1].
608    let (tu, tv) = (hu.min(2.0), hv.min(2.0));
609    let reach_u = lo.x.abs().max(hi.x.abs());
610    let reach_v = lo.y.abs().max(hi.y.abs());
611    let (su, cu) = m.x.sin_cos();
612    let (sv, cv) = m.y.sin_cos();
613    Some(match surface {
614        Surface::Plane(_) => (vec![m.x, m.y], vec![hu, hv], vec![reach_u, reach_v]),
615        Surface::Cylinder(_) | Surface::EllipticalCylinder(_) => {
616            (vec![cu, su, m.y], vec![tu, tu, hv], vec![1.0, 1.0, reach_v])
617        }
618        Surface::Cone(_) => {
619            // |v cos u - m_v cos m_u| <= |v - m_v| + |m_v| |cos u - cos m_u|.
620            let mixed = hv + m.y.abs() * tu;
621            (
622                vec![cu, su, m.y * cu, m.y * su, m.y],
623                vec![tu, tu, mixed, mixed, hv],
624                vec![1.0, 1.0, reach_v, reach_v, reach_v],
625            )
626        }
627        Surface::Sphere(_) => (
628            vec![cv * cu, cv * su, sv],
629            vec![tv + tu, tv + tu, tv],
630            vec![1.0, 1.0, 1.0],
631        ),
632        Surface::Torus(_) => (
633            vec![cu, su, cv * cu, cv * su, sv],
634            vec![tu, tu, tv + tu, tv + tu, tv],
635            vec![1.0, 1.0, 1.0, 1.0, 1.0],
636        ),
637        _ => return None,
638    })
639}
640
641/// A bound on `|S_A(u, v) - S_B(u, v)|` over the patch `[lo, hi]`, for two
642/// surfaces matched by [`same_shape`].
643fn matched_bound(a: &Surface, b: &Surface, lo: Point2, hi: Point2) -> Option<Scalar> {
644    if let (Surface::BSpline(p), Surface::BSpline(q)) = (a, b) {
645        // Equal knots and positive equal weights: `S_A - S_B` is a convex
646        // combination of the control point differences, everywhere.
647        let mut reach: Scalar = 0.0;
648        let mut scale: Scalar = 0.0;
649        for (row_a, row_b) in p.control_points.iter().zip(&q.control_points) {
650            for (pa, pb) in row_a.iter().zip(row_b) {
651                reach = reach.max((*pa - *pb).length());
652                scale = scale.max(pa.length()).max(pb.length());
653            }
654        }
655        return Some(
656            reach * (1.0 + RATIO_EPSILONS * Scalar::EPSILON)
657                + POINT_EPSILONS * Scalar::EPSILON * scale
658                + Scalar::MIN_POSITIVE,
659        );
660    }
661    let (oa, ka) = terms(a)?;
662    let (ob, kb) = terms(b)?;
663    let (phi, wobble, reach) = basis(a, lo, hi)?;
664    let mut centre = oa - ob;
665    let mut spread = 0.0;
666    let mut scale = oa.length() + ob.length();
667    for k in 0..phi.len() {
668        let delta = ka[k] - kb[k];
669        centre += delta * phi[k];
670        spread += wobble[k] * delta.length();
671        scale += reach[k] * (ka[k].length() + kb[k].length());
672    }
673    let bound = (centre.length() + spread) * (1.0 + RATIO_EPSILONS * Scalar::EPSILON)
674        + POINT_EPSILONS * Scalar::EPSILON * scale
675        + Scalar::MIN_POSITIVE;
676    bound.is_finite().then_some(bound)
677}
678
679/// Distinct translations matched faces carry: `S_B - S_A` where it is the
680/// same at the centre and two corners of a matched face's parameter box,
681/// at most [`MAX_DISPLACEMENTS`] of them. For a translate every face gives
682/// the translation; a turned face gives none.
683fn displacements(
684    source: &Side<'_>,
685    target: &Side<'_>,
686    matched: &[Vec<Match>],
687) -> Result<Vec<Vec3>, ExactMeasureError> {
688    // The displacements are differences of computed points: each carries the
689    // rounding of its side's scale, four times over (two points, two sides).
690    let rounding = 4.0 * POINT_EPSILONS * Scalar::EPSILON * (source.scale + target.scale);
691    let mut found: Vec<Vec3> = Vec::new();
692    for element in &source.elements {
693        let Shape::Face { face, lo, hi, .. } = element.shape else {
694            continue;
695        };
696        let surface = surface_of(source.brep, source.brep.topology().faces()[face].surface)?;
697        for other in &matched[face] {
698            let theirs = match &other.shifted {
699                Some(shifted) => &shifted.surface,
700                None => surface_of(
701                    target.brep,
702                    target.brep.topology().faces()[other.face].surface,
703                )?,
704            };
705            // The displacement at the centre and two corners of the box:
706            // only a translation, the same at all three, is a seed.
707            let moves: Vec<Vec3> = [(lo + hi) * 0.5, lo, hi]
708                .iter()
709                .filter_map(|q| {
710                    let here = evaluate(surface, q.x, q.y).ok()?;
711                    let there = evaluate(theirs, q.x, q.y).ok()?;
712                    Some(there - here)
713                })
714                .collect();
715            let [t, a, b] = moves[..] else {
716                continue;
717            };
718            let rigid =
719                (a - t).length().max((b - t).length()) <= 1e-9 * (1.0 + t.length()) + rounding;
720            let fresh = rigid
721                && t.is_finite()
722                && t.length() > 0.0
723                && found
724                    .iter()
725                    .all(|seen| (*seen - t).length() > 1e-9 * (1.0 + t.length()) + rounding);
726            if fresh && found.len() < MAX_DISPLACEMENTS {
727                found.push(t);
728            }
729        }
730    }
731    Ok(found)
732}
733
734/// How far an element's witness `w` lies from the centre of its enclosing
735/// sphere: a patch point is within `radius + witness_offset` of the witness
736/// (#459). It is zero for an analytic patch, whose centre is its witness,
737/// and positive for a B-spline patch, whose centre is the control net's
738/// box midpoint.
739fn witness_offset(element: &Element, w: Point3) -> Scalar {
740    (w - element.centre).length() * (1.0 + RATIO_EPSILONS * Scalar::EPSILON)
741}
742
743/// A lower bound on `h(A, B)` from a point computed on `A`'s boundary: the
744/// point's own bound less the point's evaluation rounding `error` (#439).
745fn witnessed_lower(found: &PointDistance, error: Scalar) -> Scalar {
746    (found.lower - error).max(0.0)
747}
748
749/// What the search has certified so far.
750struct State {
751    lower: Scalar,
752    /// The witnesses, and the elements of each boundary they lie on.
753    witness: Option<(Point3, Point3, Shape, Shape)>,
754}
755
756fn search(
757    source: &Side<'_>,
758    target: &mut Nearest<'_>,
759    accuracy: Scalar,
760    max_splits: usize,
761    items: &[core::ops::Range<usize>],
762    mut cuts: Option<&mut CutBounds<'_>>,
763) -> Result<Witnessed, ExactMeasureError> {
764    let matched = matches(source.brep, target.side.brep);
765    let point_accuracy = 0.25 * accuracy;
766    // The width the outer loop settles at: the point queries' floor and the
767    // matched bounds' pads, each below 128 eps of a side's scale.
768    let outer_scale = 2.0 * (source.scale + target.side.scale);
769    let outer_excess = 2.0 * (source.witness_excess() + target.side.witness_excess());
770    let source_error = source.witness_error();
771    let mut state = State {
772        lower: 0.0,
773        witness: None,
774    };
775    let mut faces: BinaryHeap<(Key, usize)> = BinaryHeap::new();
776    let mut edges: BinaryHeap<(Key, usize)> = BinaryHeap::new();
777    // Every element queued, by index; a queue holds its bound.
778    let mut pieces: Vec<Element> = Vec::new();
779
780    // A matched face moved by `t` puts the farthest point of a translate at
781    // `A`'s support point against `t` (see `translate::support_point`).
782    let seeds = displacements(source, &target.side, &matched)?
783        .into_iter()
784        .flat_map(|t| items.iter().map(move |edges| (t, edges.clone())));
785    for (t, edges) in seeds {
786        let Some((p, edge)) = support_point(source.brep, -t, edges) else {
787            continue;
788        };
789        if let Some(found) = target.query(p, point_accuracy)? {
790            let lower = witnessed_lower(&found, source_error);
791            if lower > state.lower || state.witness.is_none() {
792                state.lower = state.lower.max(lower);
793                let on = Shape::Edge {
794                    edge,
795                    t0: 0.0,
796                    t1: 0.0,
797                };
798                state.witness = Some((p, found.nearest, on, found.on));
799            }
800        }
801    }
802
803    // Bound one element and queue it; measure its witness if it may raise
804    // the lower bound.
805    let mut admit = |element: Element,
806                     state: &mut State,
807                     faces: &mut BinaryHeap<(Key, usize)>,
808                     edges: &mut BinaryHeap<(Key, usize)>,
809                     pieces: &mut Vec<Element>|
810     -> Result<(), ExactMeasureError> {
811        let mut upper = Scalar::INFINITY;
812        if let Shape::Face { face, lo, hi, .. } = element.shape {
813            let surface = surface_of(source.brep, source.brep.topology().faces()[face].surface)?;
814            for other in &matched[face] {
815                let bound = match &other.shifted {
816                    None => {
817                        let target_surface = surface_of(
818                            target.side.brep,
819                            target.side.brep.topology().faces()[other.face].surface,
820                        )?;
821                        matched_bound(surface, target_surface, lo, hi)
822                    }
823                    Some(shifted) => matched_bound(surface, &shifted.surface, lo, hi)
824                        .and_then(|bound| shifted.bound(bound, lo, hi)),
825                };
826                if let Some(bound) = bound {
827                    upper = upper.min(bound);
828                }
829            }
830            // A face cut to its free region, through the face it was cut
831            // from (see `cut`).
832            if let Some(cuts) = cuts.as_deref_mut() {
833                if let Some(bound) = cuts.bound(source.brep, face, lo, hi)? {
834                    upper = upper.min(bound);
835                }
836            }
837        }
838        // The witness raises the lower bound only if it can beat it.
839        if let Some(w) = element.witness {
840            if upper > state.lower {
841                if let Some(found) = target.query(w, point_accuracy)? {
842                    let lower = witnessed_lower(&found, source_error);
843                    if lower > state.lower || state.witness.is_none() {
844                        state.lower = state.lower.max(lower);
845                        state.witness = Some((w, found.nearest, element.shape, found.on));
846                    }
847                    // `d(., dB)` is 1-Lipschitz about the witness, and the
848                    // sphere about `centre` need not be centred on it (#459).
849                    upper = upper.min(found.upper + element.radius + witness_offset(&element, w));
850                }
851            }
852        }
853        // Without a witness, about the enclosing sphere's centre, which
854        // need not lie on the face.
855        if element.witness.is_none() && upper > state.lower {
856            if let Some(found) = target.query(element.centre, point_accuracy)? {
857                upper = upper.min(found.upper + element.radius);
858            }
859        }
860        if !upper.is_finite() {
861            return Err(crate::exact::NOT_CONVERGED);
862        }
863        pieces.push(element);
864        let key = Key(upper, Reverse(pieces.len()));
865        match element.shape {
866            Shape::Face { .. } => faces.push((key, pieces.len() - 1)),
867            Shape::Edge { .. } => edges.push((key, pieces.len() - 1)),
868        }
869        Ok(())
870    };
871
872    for element in source.elements.clone() {
873        admit(element, &mut state, &mut faces, &mut edges, &mut pieces)?;
874    }
875
876    let mut splits = 0;
877    loop {
878        // Every point of the boundary lies in a queued face patch, or in
879        // one already bounded below `lower`.
880        let face_top = faces.peek().map_or(Scalar::NEG_INFINITY, |(key, _)| key.0);
881        let upper = face_top.max(state.lower);
882        let settled = floor(outer_scale, upper) + outer_excess;
883        if upper - state.lower <= accuracy.max(settled) || splits >= max_splits {
884            break;
885        }
886        splits += 1;
887        // Alternate: an edge span that may hold a farther point than the
888        // lower bound is split every other step, to place witnesses.
889        let edge_worth = edges
890            .peek()
891            .is_some_and(|(key, _)| key.0 > state.lower + accuracy);
892        let take_edge = edge_worth && (splits % 2 == 0 || faces.is_empty());
893        let popped = if take_edge { edges.pop() } else { faces.pop() };
894        let Some((key, index)) = popped else {
895            break;
896        };
897        let element = pieces[index];
898        let children = source.split(&element)?;
899        let parent = source.size(&element);
900        if children.iter().any(|child| source.size(child) >= parent) {
901            // Can shrink no further: a face patch keeps its bound, and the
902            // search stops there; an edge span is only a witness source.
903            if !take_edge {
904                faces.push((key, index));
905                break;
906            }
907            continue;
908        }
909        for child in children {
910            admit(child, &mut state, &mut faces, &mut edges, &mut pieces)?;
911        }
912    }
913
914    let face_top = faces.peek().map_or(Scalar::NEG_INFINITY, |(key, _)| key.0);
915    let (point_from, point_to, from, to) = state.witness.ok_or(crate::exact::NOT_CONVERGED)?;
916    Ok(Witnessed {
917        bounds: HausdorffBounds {
918            lower: state.lower,
919            upper: face_top.max(state.lower),
920            point_from,
921            point_to,
922        },
923        from,
924        to,
925    })
926}
927
928#[cfg(test)]
929mod tests {
930    //! The matched bound, checked by dense sampling: over a patch it holds
931    //! `|S_A - S_B|` for every family, and it is not vacuous.
932
933    use super::{matched_bound, same_shape};
934    use axiolid_core::{Frame3, Point2, Point3, Vec3};
935    use axiolid_curve::{BSplineSurface, KnotSpec};
936    use axiolid_evaluate::surface::evaluate;
937    use axiolid_surface::{Cone, Cylinder, EllipticalCylinder, Plane, Sphere, Surface, Torus};
938
939    fn frame(turn: f64, shift: Vec3) -> Frame3 {
940        // A tilted frame, turned about its own z by `turn`, then shifted.
941        let z = Vec3::new(0.0, -0.6, 0.8);
942        let x0 = Vec3::new(0.6, 0.64, 0.48);
943        let y0 = z.cross(x0);
944        let (s, c) = turn.sin_cos();
945        Frame3 {
946            origin: Point3::new(1.5, -2.0, 0.75) + shift,
947            x: x0 * c + y0 * s,
948            y: y0 * c - x0 * s,
949            z,
950        }
951    }
952
953    fn families(f: Frame3) -> Vec<(Surface, Point2, Point2)> {
954        vec![
955            (
956                Surface::Plane(Plane { frame: f }),
957                Point2::new(-1.0, 0.5),
958                Point2::new(2.0, 1.25),
959            ),
960            (
961                Surface::Cylinder(Cylinder {
962                    frame: f,
963                    radius: 2.5,
964                }),
965                Point2::new(0.3, -1.0),
966                Point2::new(1.9, 2.0),
967            ),
968            (
969                Surface::EllipticalCylinder(EllipticalCylinder {
970                    frame: f,
971                    semi_axis_x: 3.0,
972                    semi_axis_y: 1.0,
973                }),
974                Point2::new(0.2, 0.0),
975                Point2::new(1.4, 1.0),
976            ),
977            (
978                Surface::Cone(Cone {
979                    frame: f,
980                    radius: 1.5,
981                    semi_angle: 0.4,
982                }),
983                Point2::new(0.5, 0.2),
984                Point2::new(2.5, 1.5),
985            ),
986            (
987                Surface::Sphere(Sphere {
988                    frame: f,
989                    radius: 2.0,
990                }),
991                Point2::new(0.4, -0.3),
992                Point2::new(2.0, 1.1),
993            ),
994            (
995                Surface::Torus(Torus {
996                    frame: f,
997                    major_radius: 3.0,
998                    minor_radius: 1.0,
999                }),
1000                Point2::new(0.1, 0.5),
1001                Point2::new(1.7, 2.9),
1002            ),
1003        ]
1004    }
1005
1006    fn samples(lo: Point2, hi: Point2) -> impl Iterator<Item = Point2> {
1007        const N: usize = 24;
1008        (0..=N).flat_map(move |i| {
1009            (0..=N).map(move |j| {
1010                Point2::new(
1011                    lo.x + (hi.x - lo.x) * i as f64 / N as f64,
1012                    lo.y + (hi.y - lo.y) * j as f64 / N as f64,
1013                )
1014            })
1015        })
1016    }
1017
1018    /// The largest `|S_A - S_B|` over the samples, and the bound.
1019    fn reach_and_bound(a: &Surface, b: &Surface, lo: Point2, hi: Point2) -> (f64, f64) {
1020        let bound = matched_bound(a, b, lo, hi).expect("a matched pair");
1021        let mut reach: f64 = 0.0;
1022        for p in samples(lo, hi) {
1023            let pa = evaluate(a, p.x, p.y).expect("point");
1024            let pb = evaluate(b, p.x, p.y).expect("point");
1025            reach = reach.max((pa - pb).length());
1026        }
1027        (reach, bound)
1028    }
1029
1030    #[test]
1031    fn a_translate_is_bounded_by_its_translation_exactly() {
1032        let shift = Vec3::new(0.3, -0.4, 1.2);
1033        let a = families(frame(0.0, Vec3::ZERO));
1034        let b = families(frame(0.0, shift));
1035        for ((sa, lo, hi), (sb, _, _)) in a.iter().zip(&b) {
1036            assert!(same_shape(sa, sb));
1037            let (reach, bound) = reach_and_bound(sa, sb, *lo, *hi);
1038            assert!(reach <= bound, "{sa:?}: {reach} > {bound}");
1039            assert!(
1040                (bound - shift.length()).abs() < 1e-9,
1041                "{sa:?}: {bound} is not the translation"
1042            );
1043        }
1044    }
1045
1046    /// One frame axis moved alone, over patches tall in `v` or wide in
1047    /// `u` far from `v = 0`: each basis function's oscillation is needed
1048    /// on its own, which a rigid turn (moving two axes together) hides.
1049    #[test]
1050    fn each_axis_moved_alone_is_held_over_tall_and_wide_patches() {
1051        let base = Frame3 {
1052            origin: Point3::ZERO,
1053            x: Vec3::X,
1054            y: Vec3::Y,
1055            z: Vec3::Z,
1056        };
1057        let tall = (Point2::new(0.3, 0.2), Point2::new(0.35, 1.4));
1058        let cases = |f: Frame3| {
1059            vec![
1060                (
1061                    Surface::Sphere(Sphere {
1062                        frame: f,
1063                        radius: 2.0,
1064                    }),
1065                    tall,
1066                ),
1067                (
1068                    Surface::Torus(Torus {
1069                        frame: f,
1070                        major_radius: 3.0,
1071                        minor_radius: 1.0,
1072                    }),
1073                    tall,
1074                ),
1075                (
1076                    Surface::Cone(Cone {
1077                        frame: f,
1078                        radius: 0.1,
1079                        semi_angle: 0.4,
1080                    }),
1081                    (Point2::new(0.0, 5.0), Point2::new(1.6, 5.2)),
1082                ),
1083            ]
1084        };
1085        for axis in 0..3 {
1086            let mut moved = base;
1087            let nudge = Vec3::new(0.3, 0.0, 0.0);
1088            match axis {
1089                0 => moved.x += nudge,
1090                1 => moved.y += nudge,
1091                _ => moved.z += nudge,
1092            }
1093            for ((sa, (lo, hi)), (sb, _)) in cases(base).iter().zip(cases(moved)) {
1094                let (reach, bound) = reach_and_bound(sa, &sb, *lo, *hi);
1095                assert!(reach <= bound, "{sa:?}, axis {axis}: {reach} > {bound}");
1096            }
1097        }
1098    }
1099
1100    #[test]
1101    fn a_turned_and_moved_copy_is_held_and_not_vacuously() {
1102        let a = families(frame(0.0, Vec3::ZERO));
1103        let b = families(frame(0.05, Vec3::new(0.01, 0.0, -0.02)));
1104        for ((sa, lo, hi), (sb, _, _)) in a.iter().zip(&b) {
1105            let (reach, bound) = reach_and_bound(sa, sb, *lo, *hi);
1106            assert!(reach <= bound, "{sa:?}: {reach} > {bound}");
1107            assert!(bound <= 4.0 * reach + 1e-9, "{sa:?}: {bound} vs {reach}");
1108            // A quarter of the patch is held tighter.
1109            let mid = (*lo + *hi) * 0.5;
1110            let (small_reach, small) = reach_and_bound(sa, sb, *lo, mid);
1111            assert!(small_reach <= small, "{sa:?}: {small_reach} > {small}");
1112        }
1113    }
1114
1115    #[test]
1116    fn a_different_family_cone_or_net_is_not_matched() {
1117        let f = frame(0.0, Vec3::ZERO);
1118        let thin = Surface::Cylinder(Cylinder {
1119            frame: f,
1120            radius: 1.0,
1121        });
1122        let thick = Surface::Cylinder(Cylinder {
1123            frame: f,
1124            radius: 1.5,
1125        });
1126        // Radii fold into the coefficients: still one family, and held.
1127        assert!(same_shape(&thin, &thick));
1128        let (lo, hi) = (Point2::new(0.2, -0.5), Point2::new(1.1, 0.5));
1129        let (reach, bound) = reach_and_bound(&thin, &thick, lo, hi);
1130        assert!(reach <= bound && bound <= 4.0 * reach, "{reach} vs {bound}");
1131        assert!(!same_shape(&thin, &Surface::Plane(Plane { frame: f })));
1132        let cone = |semi_angle| {
1133            Surface::Cone(Cone {
1134                frame: f,
1135                radius: 1.0,
1136                semi_angle,
1137            })
1138        };
1139        assert!(!same_shape(&cone(0.3), &cone(0.4)));
1140        // Another radius or angle moves a cone's apex, a pole of its chart.
1141        assert!(!same_shape(
1142            &cone(0.3),
1143            &Surface::Cone(Cone {
1144                frame: f,
1145                radius: 1.2,
1146                semi_angle: 0.3,
1147            })
1148        ));
1149    }
1150
1151    #[test]
1152    fn a_spline_net_moved_point_by_point_is_held_by_its_largest_move() {
1153        let net = |lift: f64| BSplineSurface {
1154            u_degree: 1,
1155            v_degree: 1,
1156            control_points: vec![
1157                vec![Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 1.0, lift)],
1158                vec![
1159                    Point3::new(1.0, 0.0, 0.0),
1160                    Point3::new(1.0, 1.0, 0.5 * lift),
1161                ],
1162            ],
1163            u_knots: vec![0.0, 1.0],
1164            u_multiplicities: vec![2, 2],
1165            v_knots: vec![0.0, 1.0],
1166            v_multiplicities: vec![2, 2],
1167            weights: None,
1168            u_closed: false,
1169            v_closed: false,
1170            knot_spec: KnotSpec::Unspecified,
1171            self_intersect: None,
1172        };
1173        let (a, b) = (Surface::BSpline(net(0.0)), Surface::BSpline(net(0.2)));
1174        assert!(same_shape(&a, &b));
1175        let (lo, hi) = (Point2::new(0.0, 0.0), Point2::new(1.0, 1.0));
1176        let (reach, bound) = reach_and_bound(&a, &b, lo, hi);
1177        assert!(reach <= bound && bound <= 0.2 + 1e-9, "{reach} vs {bound}");
1178        // Other knots are another family member.
1179        let mut other = net(0.0);
1180        other.u_knots = vec![0.0, 2.0];
1181        assert!(!same_shape(&a, &Surface::BSpline(other)));
1182    }
1183
1184    /// A one-face sheet on the plane through `origin`, its loop's three
1185    /// uses carrying `pcurve` over `interval`.
1186    fn sheet(
1187        origin: Point3,
1188        pcurve: axiolid_curve::Curve2,
1189        interval: axiolid_core::Interval,
1190    ) -> axiolid_brep::ExactBRep {
1191        use axiolid_topology::{Edge, EdgeUse, Face, FaceBound, Loop, Orientation, Vertex};
1192        let mut builder = axiolid_brep::ExactBRepBuilder::default();
1193        let curve3 = builder.add_curve3(axiolid_curve::Curve3::Line(axiolid_curve::Line3 {
1194            origin,
1195            direction: Vec3::X,
1196        }));
1197        let curve2 = builder.add_curve2(pcurve);
1198        let surface = builder.add_surface(Surface::Plane(Plane {
1199            frame: Frame3 {
1200                origin,
1201                x: Vec3::X,
1202                y: Vec3::Y,
1203                z: Vec3::Z,
1204            },
1205        }));
1206        let topology = builder.topology_mut();
1207        let vertices: Vec<_> = [Point3::X, Point3::Y, Point3::Z]
1208            .into_iter()
1209            .map(|position| topology.add_vertex(Vertex { position }))
1210            .collect();
1211        let edges: Vec<_> = (0..3)
1212            .map(|i| {
1213                topology.add_edge(Edge {
1214                    start: vertices[i],
1215                    end: vertices[(i + 1) % 3],
1216                    curve: Some(curve3),
1217                })
1218            })
1219            .collect();
1220        let loop_id = topology.add_loop(Loop {
1221            edges: edges
1222                .iter()
1223                .map(|&edge| EdgeUse {
1224                    edge,
1225                    orientation: Orientation::Forward,
1226                    pcurve: Some(curve2),
1227                })
1228                .collect(),
1229        });
1230        topology.add_face(Face {
1231            surface: Some(surface),
1232            bounds: vec![FaceBound {
1233                loop_id,
1234                orientation: Orientation::Forward,
1235                outer: true,
1236            }],
1237            orientation: Orientation::Forward,
1238        });
1239        for edge in edges {
1240            builder.set_edge_interval(edge, axiolid_core::Interval::UNIT);
1241        }
1242        for use_index in 0..3 {
1243            builder.set_pcurve_interval(loop_id, use_index, interval);
1244        }
1245        builder.finish().expect("a valid sheet")
1246    }
1247
1248    /// Faces match only on the very same trimming pcurves and intervals,
1249    /// wherever their planes are.
1250    #[test]
1251    fn faces_match_only_on_the_same_trim() {
1252        use axiolid_core::{Interval, Vec2};
1253        use axiolid_curve::{Curve2, Line2};
1254        let line = |dx: f64| {
1255            Curve2::Line(Line2 {
1256                origin: Point2::new(0.5, 0.0),
1257                direction: Vec2::new(dx, 1.0),
1258            })
1259        };
1260        let unit = Interval::UNIT;
1261        let a = sheet(Point3::ZERO, line(1.0), unit);
1262        let matched = |b: &axiolid_brep::ExactBRep| {
1263            super::same_face(&a, &a.topology().faces()[0], b, &b.topology().faces()[0])
1264        };
1265        assert!(matched(&sheet(
1266            Point3::new(3.0, 1.0, -2.0),
1267            line(1.0),
1268            unit
1269        )));
1270        assert!(!matched(&sheet(Point3::ZERO, line(2.0), unit)));
1271        assert!(!matched(&sheet(
1272            Point3::ZERO,
1273            line(1.0),
1274            Interval::new(0.0, 0.5)
1275        )));
1276        let found = super::matches(&a, &a);
1277        assert_eq!(found.len(), 1);
1278        assert_eq!(found[0].len(), 1);
1279        assert_eq!(found[0][0].face, 0);
1280        assert!(found[0][0].shifted.is_none());
1281    }
1282
1283    /// A point 0.02 under a box's bottom, under its edge or its face, at the
1284    /// origin and in georeferenced coordinates: the patches of that face tie
1285    /// along its own normal, and the edge spans' witnesses have to be in
1286    /// hand before they are refined (#318).
1287    #[test]
1288    fn a_point_under_an_edge_finds_the_edge_witness() {
1289        use super::Nearest;
1290        use crate::exact_distance::fixtures::unit_box;
1291        use crate::exact_distance::{floor, Metric, Side};
1292        for shift in [Vec3::ZERO, Vec3::new(450_000.0, 5_600_000.0, 0.0)] {
1293            let b = unit_box(Point3::ZERO + shift + Vec3::new(0.0, 0.0, 0.02));
1294            let mut nearest = Nearest::new(Side::new(&b, 1e-12, Metric::Space).expect("a side"));
1295            for k in 1..64 {
1296                let t = f64::from(k) / 64.0;
1297                for q in [Point3::new(t, 0.0, 0.0), Point3::new(t, t, 0.0)] {
1298                    let p = q + shift;
1299                    let found = nearest
1300                        .query(p, 1e-10)
1301                        .expect("evaluates")
1302                        .expect("a boundary point");
1303                    let settled = floor(p.length() + nearest.side.scale, found.upper);
1304                    assert!(
1305                        found.lower <= 0.02 && 0.02 <= found.upper,
1306                        "{p:?}: [{}, {}]",
1307                        found.lower,
1308                        found.upper
1309                    );
1310                    assert!(
1311                        found.upper - found.lower <= 1e-10_f64.max(settled),
1312                        "{p:?}: [{}, {}]",
1313                        found.lower,
1314                        found.upper
1315                    );
1316                }
1317            }
1318        }
1319    }
1320
1321    /// A point over a face, off every patch centre: the foot of the point on
1322    /// the face closes the interval, not the patches' own witnesses (#318).
1323    #[test]
1324    fn a_point_over_a_face_closes_on_its_foot() {
1325        use super::Nearest;
1326        use crate::exact_distance::fixtures::unit_box;
1327        use crate::exact_distance::{Metric, Side};
1328        let b = unit_box(Point3::ZERO);
1329        let mut nearest = Nearest::new(Side::new(&b, 1e-12, Metric::Space).expect("a side"));
1330        let found = nearest
1331            .query(Point3::new(0.37, 0.61, 1.5), 1e-12)
1332            .expect("evaluates")
1333            .expect("a boundary point");
1334        assert!(found.lower <= 0.5 && 0.5 <= found.upper, "{found:?}");
1335        assert!(found.upper - found.lower <= 1e-12, "{found:?}");
1336    }
1337
1338    /// A one-face sheet on the plane of `frame`, bounded by the polygon
1339    /// `corners` of its parameters, counter-clockwise; its edges are the
1340    /// lines through the corners' points.
1341    fn charted_sheet(frame: Frame3, corners: &[Point2]) -> axiolid_brep::ExactBRep {
1342        use axiolid_core::{Interval, Vec2};
1343        use axiolid_curve::{Curve2, Curve3, Line2, Line3};
1344        use axiolid_topology::{Edge, EdgeUse, Face, FaceBound, Loop, Orientation, Vertex};
1345        let mut builder = axiolid_brep::ExactBRepBuilder::default();
1346        let surface = builder.add_surface(Surface::Plane(Plane { frame }));
1347        let at = |p: Point2| frame.origin + frame.x * p.x + frame.y * p.y;
1348        let vertices: Vec<_> = corners
1349            .iter()
1350            .map(|p| {
1351                builder
1352                    .topology_mut()
1353                    .add_vertex(Vertex { position: at(*p) })
1354            })
1355            .collect();
1356        let n = corners.len();
1357        let mut uses = Vec::new();
1358        let mut lengths = Vec::new();
1359        for i in 0..n {
1360            let (p, q) = (corners[i], corners[(i + 1) % n]);
1361            let length = (q - p).length();
1362            let d = Vec2::new((q.x - p.x) / length, (q.y - p.y) / length);
1363            let curve = builder.add_curve3(Curve3::Line(Line3 {
1364                origin: at(p),
1365                direction: frame.x * d.x + frame.y * d.y,
1366            }));
1367            let pcurve = builder.add_curve2(Curve2::Line(Line2 {
1368                origin: p,
1369                direction: d,
1370            }));
1371            let edge = builder.topology_mut().add_edge(Edge {
1372                start: vertices[i],
1373                end: vertices[(i + 1) % n],
1374                curve: Some(curve),
1375            });
1376            builder.set_edge_interval(edge, Interval::new(0.0, length));
1377            uses.push(EdgeUse {
1378                edge,
1379                orientation: Orientation::Forward,
1380                pcurve: Some(pcurve),
1381            });
1382            lengths.push(length);
1383        }
1384        let loop_id = builder.topology_mut().add_loop(Loop { edges: uses });
1385        for (index, length) in lengths.into_iter().enumerate() {
1386            builder.set_pcurve_interval(loop_id, index, Interval::new(0.0, length));
1387        }
1388        builder.topology_mut().add_face(Face {
1389            surface: Some(surface),
1390            bounds: vec![FaceBound {
1391                loop_id,
1392                orientation: Orientation::Forward,
1393                outer: true,
1394            }],
1395            orientation: Orientation::Forward,
1396        });
1397        builder.finish().expect("a valid sheet")
1398    }
1399
1400    /// A sheet at the origin charted from far away -- its frame origin up to
1401    /// (9600000, 12800000), its parameters as large -- over a parallel sheet
1402    /// `0.3125` from it, so `h = 0.3125` exactly. Its witnesses round by up
1403    /// to `1e-9` off it, while the point queries against the near sheet are
1404    /// exact to `1e-15`: the lower bound must take the witnesses' rounding
1405    /// off (#439).
1406    #[test]
1407    fn a_far_charted_face_never_lifts_the_lower_bound_past_the_distance() {
1408        let x = Vec3::new(0.375, 0.5, 0.0);
1409        let frame = |origin: Point3| Frame3 {
1410            origin,
1411            x,
1412            y: Vec3::Z,
1413            z: x.cross(Vec3::Z),
1414        };
1415        let b = charted_sheet(
1416            frame(Point3::new(0.25, -0.1875, 0.0)),
1417            &[
1418                Point2::new(-1.0, -1.0),
1419                Point2::new(4.0, -1.0),
1420                Point2::new(4.0, 2.0),
1421                Point2::new(-1.0, 2.0),
1422            ],
1423        );
1424        for k in 1..=16_i32 {
1425            let along = 1.6e6 * f64::from(k);
1426            let a = charted_sheet(
1427                frame(Point3::ZERO + x * along),
1428                &[
1429                    Point2::new(-along, 0.0),
1430                    Point2::new(-along + 1.6, 0.0),
1431                    Point2::new(-along + 1.6, 1.0),
1432                    Point2::new(-along, 1.0),
1433                ],
1434            );
1435            let found = super::one_sided_boundary_hausdorff_with_budget(
1436                &a,
1437                &b,
1438                1e-9,
1439                axiolid_core::Tolerance::METRE,
1440                16,
1441            )
1442            .expect("bounded");
1443            assert!(
1444                found.lower <= 0.3125 && 0.3125 <= found.upper,
1445                "{k}: [{}, {}]",
1446                found.lower,
1447                found.upper
1448            );
1449            assert!(found.lower >= 0.3125 - 2e-7, "{k}: {}", found.lower);
1450        }
1451    }
1452
1453    /// A sheet on a bicubic rational B-spline surface, over its
1454    /// parameter square, bounded by the four straight edges of the net's
1455    /// boundary: the points `(i / 3, j / 3, 0)` at the weight 1 (so the
1456    /// edges are linear in the parameter), whatever the interior.
1457    fn spline_sheet(interior: [[f64; 2]; 2], weight: f64) -> axiolid_brep::ExactBRep {
1458        use axiolid_core::{Interval, Vec2};
1459        use axiolid_curve::{Curve2, Curve3, Line2, Line3};
1460        use axiolid_topology::{Edge, EdgeUse, Face, FaceBound, Loop, Orientation, Vertex};
1461        let mut points = vec![vec![Point3::ZERO; 4]; 4];
1462        let mut weights = vec![vec![1.0; 4]; 4];
1463        for (i, column) in points.iter_mut().enumerate() {
1464            for (j, point) in column.iter_mut().enumerate() {
1465                let height = if (1..3).contains(&i) && (1..3).contains(&j) {
1466                    interior[i - 1][j - 1]
1467                } else {
1468                    0.0
1469                };
1470                *point = Point3::new(i as f64 / 3.0, j as f64 / 3.0, height);
1471            }
1472        }
1473        weights[1][1] = weight;
1474        let net = BSplineSurface {
1475            u_degree: 3,
1476            v_degree: 3,
1477            control_points: points,
1478            u_knots: vec![0.0, 1.0],
1479            u_multiplicities: vec![4, 4],
1480            v_knots: vec![0.0, 1.0],
1481            v_multiplicities: vec![4, 4],
1482            weights: Some(weights),
1483            u_closed: false,
1484            v_closed: false,
1485            knot_spec: KnotSpec::Unspecified,
1486            self_intersect: None,
1487        };
1488        let mut builder = axiolid_brep::ExactBRepBuilder::default();
1489        let surface = builder.add_surface(Surface::BSpline(net));
1490        let corners = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
1491        let directions = [(1.0, 0.0), (0.0, 1.0), (-1.0, 0.0), (0.0, -1.0)];
1492        let vertices: Vec<_> = corners
1493            .iter()
1494            .map(|c| {
1495                builder.topology_mut().add_vertex(Vertex {
1496                    position: Point3::new(c.0, c.1, 0.0),
1497                })
1498            })
1499            .collect();
1500        let mut uses = Vec::new();
1501        for i in 0..4 {
1502            let (p, d) = (corners[i], directions[i]);
1503            let curve = builder.add_curve3(Curve3::Line(Line3 {
1504                origin: Point3::new(p.0, p.1, 0.0),
1505                direction: Vec3::new(d.0, d.1, 0.0),
1506            }));
1507            let pcurve = builder.add_curve2(Curve2::Line(Line2 {
1508                origin: Point2::new(p.0, p.1),
1509                direction: Vec2::new(d.0, d.1),
1510            }));
1511            let edge = builder.topology_mut().add_edge(Edge {
1512                start: vertices[i],
1513                end: vertices[(i + 1) % 4],
1514                curve: Some(curve),
1515            });
1516            builder.set_edge_interval(edge, Interval::new(0.0, 1.0));
1517            uses.push(EdgeUse {
1518                edge,
1519                orientation: Orientation::Forward,
1520                pcurve: Some(pcurve),
1521            });
1522        }
1523        let loop_id = builder.topology_mut().add_loop(Loop { edges: uses });
1524        for i in 0..4 {
1525            builder.set_pcurve_interval(loop_id, i, Interval::new(0.0, 1.0));
1526        }
1527        builder.topology_mut().add_face(Face {
1528            surface: Some(surface),
1529            bounds: vec![FaceBound {
1530                loop_id,
1531                orientation: Orientation::Forward,
1532                outer: true,
1533            }],
1534            orientation: Orientation::Forward,
1535        });
1536        builder.finish().expect("a valid sheet")
1537    }
1538
1539    /// A square of side `0.15` in the plane `z = 0`, near the origin.
1540    fn small_sheet() -> axiolid_brep::ExactBRep {
1541        charted_sheet(
1542            Frame3 {
1543                origin: Point3::ZERO,
1544                x: Vec3::X,
1545                y: Vec3::Y,
1546                z: Vec3::Z,
1547            },
1548            &[
1549                Point2::new(0.25, 0.25),
1550                Point2::new(0.4, 0.25),
1551                Point2::new(0.4, 0.4),
1552                Point2::new(0.25, 0.4),
1553            ],
1554        )
1555    }
1556
1557    /// The heavy control point pulls the witness `S(1/2, 1/2)` towards the
1558    /// small sheet, `0.25` from the centre of the control net's box. The far
1559    /// corner of the sheet is `0.6 sqrt 2 = 0.8485` from the small sheet and
1560    /// no edge witness shows it: bounding the patch about the witness with
1561    /// the radius about the centre gave an upper bound of `0.7681` (#459).
1562    #[test]
1563    fn a_spline_patch_is_bounded_about_its_witness_not_its_net_centre() {
1564        let b = small_sheet();
1565        let a = spline_sheet([[0.05, 0.1], [-0.05, 0.3]], 1000.0);
1566        let h = 0.6 * 2.0_f64.sqrt();
1567        for budget in 0..4 {
1568            let found = super::one_sided_boundary_hausdorff_with_budget(
1569                &a,
1570                &b,
1571                1e-9,
1572                axiolid_core::Tolerance::METRE,
1573                budget,
1574            )
1575            .expect("bounded");
1576            assert!(
1577                found.lower <= h && h <= found.upper,
1578                "{budget}: [{}, {}] must contain {h}",
1579                found.lower,
1580                found.upper
1581            );
1582        }
1583    }
1584
1585    #[test]
1586    fn a_witness_is_off_its_patch_centre_only_on_a_spline() {
1587        use super::{witness_offset, Metric, Shape, Side};
1588        let tolerance = axiolid_core::Tolerance::METRE.linear();
1589        let offsets = |brep: &axiolid_brep::ExactBRep| -> Vec<f64> {
1590            let side = Side::new(brep, tolerance, Metric::Space).expect("a side");
1591            side.elements
1592                .iter()
1593                .filter(|e| matches!(e.shape, Shape::Face { .. }))
1594                .map(|e| witness_offset(e, e.witness.expect("a face witness")))
1595                .collect()
1596        };
1597        assert_eq!(offsets(&small_sheet()), [0.0]);
1598        let spline = offsets(&spline_sheet([[0.05, 0.1], [-0.05, 0.3]], 1000.0));
1599        assert_eq!(spline.len(), 1);
1600        assert!(spline[0] > 0.2, "{spline:?}");
1601    }
1602}