Skip to main content

axiolid_measure/
mesh_hausdorff.rs

1//! Certified Hausdorff distance between two triangle meshes (#148, D15).
2//!
3//! # What is certified
4//!
5//! [`one_sided_hausdorff`] returns an interval `[lower, upper]` certain to
6//! contain `h(A, B) = max over a in A of min over b in B of |a - b|`, the
7//! one-sided Hausdorff distance between the surfaces of two meshes, and
8//! [`hausdorff_distance`] the two-sided `max(h(A, B), h(B, A))` together
9//! with both one-sided intervals. Both ends are guarantees:
10//!
11//! - `lower` is `d(y, B)` for a sampled point `y` of `A`, bounded below.
12//!   `d(y, B)` is the minimum over the triangles `T` of `B` of `d(y, T)`, and
13//!   each `d(y, T)` is bounded below through the support function of the
14//!   convex `T`: for every direction `u` with `|u| <= 1`,
15//!   `d(y, T) >= min over corners c of T of u . (y - c)`. The direction is
16//!   taken towards the computed nearest point, where the bound is tight, and
17//!   triangles the search never opens are bounded by their boxes.
18//! - `upper` bounds every point of `A`. `A`'s triangles are subdivided, and
19//!   over one piece `t`, `max over x in t of d(x, B) <= min over T of max
20//!   over corners v of t of d(v, T)`: `d(., T)` is convex, so its maximum
21//!   over a triangle is at a corner. Each `d(v, T)` is bounded above by the
22//!   distance to a point of `T` itself.
23//!
24//! The witnesses are `point_from`, the sample realising `lower`, and
25//! `point_to`, its nearest point found on the other mesh.
26//!
27//! # Rounding accounted for
28//!
29//! The input coordinates are taken as exact. Every bound is computed in
30//! binary64 and widened by an explicit margin in the standard model
31//! (`fl(a op b) = (a op b)(1 + d)`, `|d| <= eps / 2`, `eps = 2^-52`), with
32//! no underflow assumed:
33//!
34//! - A point of `T` is `a + s (b - a) + t (c - a)` with `s, t >= 0` and
35//!   `s + t <= 1` up to three units in the last place. Its rounded
36//!   evaluation, the parameter overshoot and the rounded length `|v - p|`
37//!   together stay below `8.5 eps S`, with `S = |v| + |a| + |b| + |c|`; the
38//!   upper bound adds `16 eps S`.
39//! - The support bound normalises `u` and shrinks it by `4 eps` so `|u| <= 1`
40//!   survives rounding; each `u . (v - c)` is off by at most
41//!   `2.5 eps (|v| + |c|)`, and the lower bound subtracts `4 eps (|v| + max |c|)`.
42//! - A box gap is shrunk by `4 eps` of itself: coordinate differences keep
43//!   their sign and the rounded Euclidean length is within `2 eps`.
44//! - Subdivision points are rounded midpoints, so a piece is only close to
45//!   the exact piece of `A` it stands for. Each corner carries its drift, the
46//!   parent's plus `eps |m|` per midpoint; the distance is 1-Lipschitz, so a
47//!   piece's upper bound adds its largest drift and a sample's lower bound
48//!   subtracts its own. `point_from` is therefore within that drift (a few
49//!   units in the last place of its coordinates) of `A`, not on it.
50//! - A flat patch of `B` (below) carries a certified excess, rounded up.
51//!
52//! No other rounding enters: branch decisions (which Voronoi region, which
53//! edge to split, which triangles to try) only choose a point, a direction
54//! or a candidate, and any choice keeps the bounds sound; they affect how
55//! fast the interval closes, not whether it contains the distance.
56//!
57//! # Method
58//!
59//! Branch and bound over `A`'s triangles. Pieces are kept in a queue by
60//! upper bound; the largest is split at the midpoint of its longest edge,
61//! the midpoint is measured to raise `lower`, and pieces whose bound falls
62//! to `lower` are dropped. Nearest queries walk a bounding-volume hierarchy
63//! over `B` with a node bound of `max over corners of the box gap`, which is
64//! a certified lower bound of what any triangle below the node can offer,
65//! seeded with the triangles found nearest to the piece's corners.
66//!
67//! One triangle at a time, a piece over the seam between two triangles of a
68//! flat face stays loose until it is split below the accuracy, along every
69//! seam. So flat convex patches of `B` -- two triangles forming a convex
70//! quadrilateral, the fan around an interior vertex with a convex link --
71//! bound a piece as one convex set, up to a certified excess (see
72//! `patches`).
73//!
74//! The hierarchy is private rather than `axiolid-spatial`'s: that one ranks
75//! candidates by box-to-box gap and returns the nearest box, where this
76//! search needs a best-first walk under a caller's bound over several points
77//! at once, and keeps `axiolid-measure` free of a dependency for it.
78//!
79//! # Convergence
80//!
81//! A piece nearest to a single triangle or flat patch of `B` is bounded
82//! exactly up to rounding, so only pieces across the medial surfaces of `B`
83//! need splitting. Two things end the refinement early, and the interval is
84//! sound either way, only wider than asked: the split budget, and a request
85//! for an accuracy below the rounding margins, where pieces stop splitting
86//! once their edges are within rounding of their coordinates.
87//!
88//! # Scope
89//!
90//! This is the distance between SURFACES, like [`crate::mesh_distance`]: a
91//! mesh wholly inside a closed mesh is as far from it as from its boundary.
92//! Degenerate triangles are measured as their edges. Open, non-manifold and
93//! self-intersecting meshes are all fine; the Hausdorff distance is defined
94//! for any point sets.
95
96use std::cmp::Ordering;
97use std::collections::BinaryHeap;
98
99use axiolid_core::Point3;
100use axiolid_mesh::TriangleMeshView;
101
102mod patches;
103
104use patches::{flat_patches, Patch};
105
106const EPS: f64 = f64::EPSILON;
107
108/// Pieces split per one-sided query before it reports what it has.
109const MAX_SPLITS: usize = 400_000;
110
111/// Triangles per hierarchy leaf.
112const LEAF_SIZE: usize = 4;
113
114/// Why a Hausdorff query could not be answered.
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116#[non_exhaustive]
117pub enum HausdorffError {
118    /// A mesh had no triangles: the distance to or from nothing is undefined.
119    EmptyMesh,
120    /// A triangle referenced a position the mesh does not have.
121    IndexOutOfRange,
122    /// A position used by a triangle is NaN or infinite.
123    NonFiniteInput,
124    /// The accuracy was negative or NaN.
125    InvalidAccuracy,
126}
127
128impl core::fmt::Display for HausdorffError {
129    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
130        formatter.write_str(match self {
131            Self::EmptyMesh => "mesh has no triangles",
132            Self::IndexOutOfRange => "triangle index out of range",
133            Self::NonFiniteInput => "mesh position is not finite",
134            Self::InvalidAccuracy => "accuracy must be non-negative",
135        })
136    }
137}
138
139impl std::error::Error for HausdorffError {}
140
141/// An interval certain to contain a Hausdorff distance, with its witness.
142#[derive(Debug, Clone, Copy, PartialEq)]
143pub struct HausdorffBounds {
144    /// The distance is at least this: `point_from` is certainly this far
145    /// from every point of the other mesh (less its drift, see the module
146    /// documentation).
147    pub lower: f64,
148    /// The distance is at most this: every point of the measured mesh is
149    /// within it of the other mesh.
150    pub upper: f64,
151    /// The sample realising `lower`, on the mesh measured FROM (within
152    /// rounding of it).
153    pub point_from: Point3,
154    /// The nearest point found to `point_from` on the other mesh.
155    pub point_to: Point3,
156}
157
158impl HausdorffBounds {
159    /// Width of the interval.
160    #[must_use]
161    pub fn width(&self) -> f64 {
162        self.upper - self.lower
163    }
164
165    /// The interval contains `value`.
166    #[must_use]
167    pub fn contains(&self, value: f64) -> bool {
168        self.lower <= value && value <= self.upper
169    }
170}
171
172/// The two-sided Hausdorff distance and both one-sided ones.
173#[derive(Debug, Clone, Copy, PartialEq)]
174pub struct MeshHausdorff {
175    /// `max(h(A, B), h(B, A))`. The witness is that of the side with the
176    /// larger lower bound; `point_from` lies on that side's mesh.
177    pub distance: HausdorffBounds,
178    /// `h(A, B)`: how far `A` strays from `B`. Witness from `A` to `B`.
179    pub forward: HausdorffBounds,
180    /// `h(B, A)`: how far `B` strays from `A`. Witness from `B` to `A`.
181    pub backward: HausdorffBounds,
182}
183
184/// One-sided Hausdorff distance `h(from, to)`, to within `accuracy`.
185///
186/// Refines until `upper - lower <= accuracy` or the split budget runs out;
187/// the interval is sound either way.
188///
189/// # Errors
190///
191/// An empty mesh, an out-of-range index, a non-finite position used by a
192/// triangle, or a negative or NaN accuracy.
193pub fn one_sided_hausdorff<A, B>(
194    from: &A,
195    to: &B,
196    accuracy: f64,
197) -> Result<HausdorffBounds, HausdorffError>
198where
199    A: TriangleMeshView + ?Sized,
200    B: TriangleMeshView + ?Sized,
201{
202    let accuracy = check_accuracy(accuracy)?;
203    let source = collect(from)?;
204    let target = Target::new(collect(to)?, accuracy);
205    Ok(search(&source, &target, accuracy))
206}
207
208/// Two-sided Hausdorff distance between `a` and `b`, to within `accuracy`.
209///
210/// Both one-sided distances are refined to `accuracy`, so each of the three
211/// intervals returned is at most that wide unless the budget ran out.
212///
213/// # Errors
214///
215/// As [`one_sided_hausdorff`].
216pub fn hausdorff_distance<A, B>(
217    a: &A,
218    b: &B,
219    accuracy: f64,
220) -> Result<MeshHausdorff, HausdorffError>
221where
222    A: TriangleMeshView + ?Sized,
223    B: TriangleMeshView + ?Sized,
224{
225    let accuracy = check_accuracy(accuracy)?;
226    let first = collect(a)?;
227    let second = collect(b)?;
228    let forward = search(&first, &Target::new(second.clone(), accuracy), accuracy);
229    let backward = search(&second, &Target::new(first, accuracy), accuracy);
230    let mut distance = if backward.lower > forward.lower {
231        backward
232    } else {
233        forward
234    };
235    distance.upper = forward.upper.max(backward.upper);
236    Ok(MeshHausdorff {
237        distance,
238        forward,
239        backward,
240    })
241}
242
243fn check_accuracy(accuracy: f64) -> Result<f64, HausdorffError> {
244    if accuracy.is_nan() || accuracy < 0.0 {
245        Err(HausdorffError::InvalidAccuracy)
246    } else {
247        Ok(accuracy)
248    }
249}
250
251/// A mesh's triangles, as points and as position indices.
252#[derive(Clone)]
253struct Surface {
254    triangles: Vec<[Point3; 3]>,
255    indices: Vec<[usize; 3]>,
256    positions: Vec<Point3>,
257    /// Positions some triangle uses.
258    used: Vec<bool>,
259}
260
261fn collect<M: TriangleMeshView + ?Sized>(mesh: &M) -> Result<Surface, HausdorffError> {
262    let positions = mesh.position_count();
263    let mut used = vec![false; positions];
264    let mut triangles = Vec::with_capacity(mesh.triangle_count());
265    let mut indices = Vec::with_capacity(mesh.triangle_count());
266    for index in 0..mesh.triangle_count() {
267        let mut corners = [Point3::ZERO; 3];
268        let mut ids = [0; 3];
269        for (slot, corner) in mesh.triangle(index).iter().enumerate() {
270            let corner = usize::try_from(*corner)
271                .ok()
272                .filter(|corner| *corner < positions)
273                .ok_or(HausdorffError::IndexOutOfRange)?;
274            let point = mesh.position(corner);
275            if !point.is_finite() {
276                return Err(HausdorffError::NonFiniteInput);
277            }
278            corners[slot] = point;
279            ids[slot] = corner;
280            used[corner] = true;
281        }
282        triangles.push(corners);
283        indices.push(ids);
284    }
285    if triangles.is_empty() {
286        return Err(HausdorffError::EmptyMesh);
287    }
288    let positions = (0..positions).map(|index| mesh.position(index)).collect();
289    Ok(Surface {
290        triangles,
291        indices,
292        positions,
293        used,
294    })
295}
296
297/// A triangle of the mesh measured TO, with its rounding scales.
298struct Facet {
299    corners: [Point3; 3],
300    /// `|a| + |b| + |c|`.
301    scale: f64,
302    /// `max |corner|`.
303    reach: f64,
304    /// Too thin to project onto: measured as its three edges.
305    thin: bool,
306    lo: Point3,
307    hi: Point3,
308}
309
310enum NodeKind {
311    Leaf { start: usize, end: usize },
312    Branch { left: usize, right: usize },
313}
314
315struct Node {
316    lo: Point3,
317    hi: Point3,
318    kind: NodeKind,
319}
320
321/// The mesh measured TO, with a median-split hierarchy over its triangles.
322struct Target {
323    facets: Vec<Facet>,
324    /// Facet indices in leaf order.
325    order: Vec<usize>,
326    nodes: Vec<Node>,
327    /// Flat convex patches (see [`patches`]).
328    patches: Vec<Patch>,
329    /// Per facet, the patches it belongs to.
330    patches_of: Vec<Vec<usize>>,
331    /// Per patch, the box around its members.
332    patch_boxes: Vec<(Point3, Point3)>,
333}
334
335impl Target {
336    /// Pairs whose hull excess is within a quarter of `accuracy`, or within
337    /// rounding of the model, are used.
338    fn new(surface: Surface, accuracy: f64) -> Self {
339        let facets: Vec<Facet> = surface
340            .triangles
341            .into_iter()
342            .map(|corners| {
343                let [a, b, c] = corners;
344                let (ab, ac) = (b - a, c - a);
345                let area = ab.cross(ac).length_squared();
346                Facet {
347                    corners,
348                    scale: a.length() + b.length() + c.length(),
349                    reach: a.length().max(b.length()).max(c.length()),
350                    thin: area <= 1e-20 * ab.length_squared() * ac.length_squared(),
351                    lo: a.min(b).min(c),
352                    hi: a.max(b).max(c),
353                }
354            })
355            .collect();
356        let size = facets.iter().map(|f| f.reach).fold(0.0, f64::max);
357        let corners: Vec<[Point3; 3]> = facets.iter().map(|f| f.corners).collect();
358        let thin: Vec<bool> = facets.iter().map(|f| f.thin).collect();
359        let (patches, patches_of) =
360            flat_patches(&corners, &thin, (accuracy / 4.0).max(1024.0 * EPS * size));
361        let patch_boxes = patches
362            .iter()
363            .map(|patch| {
364                patch.members.iter().fold(
365                    (
366                        Point3::splat(f64::INFINITY),
367                        Point3::splat(f64::NEG_INFINITY),
368                    ),
369                    |(lo, hi), &m| (lo.min(facets[m].lo), hi.max(facets[m].hi)),
370                )
371            })
372            .collect();
373        let mut target = Self {
374            order: (0..facets.len()).collect(),
375            facets,
376            nodes: Vec::new(),
377            patches,
378            patches_of,
379            patch_boxes,
380        };
381        let count = target.order.len();
382        target.build(0, count);
383        target
384    }
385
386    fn build(&mut self, start: usize, end: usize) -> usize {
387        let (mut lo, mut hi) = (
388            Point3::splat(f64::INFINITY),
389            Point3::splat(f64::NEG_INFINITY),
390        );
391        let (mut clo, mut chi) = (lo, hi);
392        for &index in &self.order[start..end] {
393            for corner in self.facets[index].corners {
394                lo = lo.min(corner);
395                hi = hi.max(corner);
396            }
397            let centre = centroid(&self.facets[index].corners);
398            clo = clo.min(centre);
399            chi = chi.max(centre);
400        }
401        let node = self.nodes.len();
402        self.nodes.push(Node {
403            lo,
404            hi,
405            kind: NodeKind::Leaf { start, end },
406        });
407        if end - start <= LEAF_SIZE {
408            return node;
409        }
410        let extent = chi - clo;
411        let axis = if extent.x >= extent.y && extent.x >= extent.z {
412            0
413        } else if extent.y >= extent.z {
414            1
415        } else {
416            2
417        };
418        let middle = start + (end - start) / 2;
419        let facets = &self.facets;
420        self.order[start..end].select_nth_unstable_by(middle - start, |&i, &j| {
421            centroid(&facets[i].corners)[axis].total_cmp(&centroid(&facets[j].corners)[axis])
422        });
423        let left = self.build(start, middle);
424        let right = self.build(middle, end);
425        self.nodes[node].kind = NodeKind::Branch { left, right };
426        node
427    }
428
429    /// Certified bounds on `d(v, self)`, and the nearest point found with
430    /// its facet.
431    fn point(&self, v: Point3) -> Nearest {
432        let mut best = f64::INFINITY;
433        let mut nearest = (self.facets[0].corners[0], 0);
434        let mut lower = f64::INFINITY;
435        let root = &self.nodes[0];
436        let mut stack = vec![(0usize, box_gap(v, root.lo, root.hi))];
437        while let Some((node, gap)) = stack.pop() {
438            if gap >= best {
439                // Nothing below the node is nearer than `gap`.
440                lower = lower.min(gap);
441                continue;
442            }
443            match self.nodes[node].kind {
444                NodeKind::Leaf { start, end } => {
445                    for &index in &self.order[start..end] {
446                        let facet = &self.facets[index];
447                        let gap = box_gap(v, facet.lo, facet.hi);
448                        if gap >= best {
449                            lower = lower.min(gap);
450                            continue;
451                        }
452                        let p = nearest_on(v, facet);
453                        let up = upper_at(v, p, facet);
454                        lower = lower.min(lower_at(v, p, facet));
455                        if up < best {
456                            best = up;
457                            nearest = (p, index);
458                        }
459                    }
460                }
461                NodeKind::Branch { left, right } => {
462                    self.push_children(&mut stack, left, right, |n| box_gap(v, n.lo, n.hi));
463                }
464            }
465        }
466        Nearest {
467            lower: lower.max(0.0).min(best),
468            point: nearest.0,
469            facet: nearest.1,
470        }
471    }
472
473    /// A certified upper bound on `max over x in piece of d(x, self)`,
474    /// seeded with the facets nearest to the corners.
475    fn piece(&self, corners: &[Point3; 3], hints: [usize; 3]) -> f64 {
476        let gap = |lo: Point3, hi: Point3| {
477            corners
478                .iter()
479                .map(|&v| box_gap(v, lo, hi))
480                .fold(0.0, f64::max)
481        };
482        let mut best = hints
483            .iter()
484            .map(|&hint| self.worst(corners, &[hint]))
485            .fold(f64::INFINITY, f64::min);
486        let mut seen = Vec::new();
487        let root = &self.nodes[0];
488        let mut stack = vec![(0usize, gap(root.lo, root.hi))];
489        while let Some((node, lower)) = stack.pop() {
490            if lower >= best {
491                continue;
492            }
493            match self.nodes[node].kind {
494                NodeKind::Leaf { start, end } => {
495                    for &index in &self.order[start..end] {
496                        let facet = &self.facets[index];
497                        if gap(facet.lo, facet.hi) < best {
498                            best = best.min(self.worst(corners, &[index]));
499                        }
500                        // A flat convex patch bounds the piece by its hull,
501                        // whose distance is convex across the seams.
502                        for &id in &self.patches_of[index] {
503                            let (lo, hi) = self.patch_boxes[id];
504                            if seen.contains(&id) || gap(lo, hi) >= best {
505                                continue;
506                            }
507                            seen.push(id);
508                            let patch = &self.patches[id];
509                            best = best.min(self.worst(corners, &patch.members) + patch.excess);
510                        }
511                    }
512                }
513                NodeKind::Branch { left, right } => {
514                    self.push_children(&mut stack, left, right, |n| gap(n.lo, n.hi));
515                }
516            }
517        }
518        best
519    }
520
521    /// `max over corners v of min over members T of d(v, T)`, bounded above.
522    fn worst(&self, corners: &[Point3; 3], members: &[usize]) -> f64 {
523        corners
524            .iter()
525            .map(|&v| {
526                members
527                    .iter()
528                    .map(|&member| {
529                        let facet = &self.facets[member];
530                        upper_at(v, nearest_on(v, facet), facet)
531                    })
532                    .fold(f64::INFINITY, f64::min)
533            })
534            .fold(0.0, f64::max)
535    }
536
537    /// Push both children, the nearer last so it is walked first.
538    fn push_children(
539        &self,
540        stack: &mut Vec<(usize, f64)>,
541        left: usize,
542        right: usize,
543        gap: impl Fn(&Node) -> f64,
544    ) {
545        let (gl, gr) = (gap(&self.nodes[left]), gap(&self.nodes[right]));
546        if gl <= gr {
547            stack.push((right, gr));
548            stack.push((left, gl));
549        } else {
550            stack.push((left, gl));
551            stack.push((right, gr));
552        }
553    }
554}
555
556fn centroid(corners: &[Point3; 3]) -> Point3 {
557    (corners[0] + corners[1] + corners[2]) / 3.0
558}
559
560/// What a nearest-point query found.
561struct Nearest {
562    /// A certified lower bound on the distance.
563    lower: f64,
564    /// The nearest point found, on the facet below.
565    point: Point3,
566    facet: usize,
567}
568
569/// A certified lower bound on the distance from `v` to the box `[lo, hi]`.
570fn box_gap(v: Point3, lo: Point3, hi: Point3) -> f64 {
571    let gap = (lo - v).max(v - hi).max(Point3::ZERO);
572    gap.length() * (1.0 - 4.0 * EPS)
573}
574
575/// A certified upper bound on `d(v, facet)` from `p`, a computed point of it.
576fn upper_at(v: Point3, p: Point3, facet: &Facet) -> f64 {
577    (v - p).length() + 16.0 * EPS * (v.length() + facet.scale)
578}
579
580/// A certified lower bound on `d(v, facet)`, from the support function in
581/// the direction of `v - p`.
582fn lower_at(v: Point3, p: Point3, facet: &Facet) -> f64 {
583    let w = v - p;
584    let length = w.length();
585    if length <= 0.0 {
586        return 0.0;
587    }
588    let u = w * ((1.0 - 4.0 * EPS) / length);
589    let support = facet
590        .corners
591        .iter()
592        .map(|&corner| u.dot(v - corner))
593        .fold(f64::INFINITY, f64::min);
594    (support - 4.0 * EPS * (v.length() + facet.reach)).max(0.0)
595}
596
597/// A point of `facet` near `v`, as `a + s (b - a) + t (c - a)` with
598/// `s, t >= 0` and `s + t <= 1` (up to rounding the bounds account for).
599fn nearest_on(v: Point3, facet: &Facet) -> Point3 {
600    let [a, b, c] = facet.corners;
601    let (ab, ac) = (b - a, c - a);
602    let (s, t) = if facet.thin {
603        thin_params(v, a, b, c)
604    } else {
605        let (s, t) = region_params(v, a, b, c);
606        if s.is_finite() && t.is_finite() {
607            (s, t)
608        } else {
609            thin_params(v, a, b, c)
610        }
611    };
612    let (mut s, mut t) = (s.clamp(0.0, 1.0), t.clamp(0.0, 1.0));
613    let sum = s + t;
614    if sum > 1.0 {
615        s /= sum;
616        t /= sum;
617    }
618    a + ab * s + ac * t
619}
620
621/// Parameters of the nearest point by Voronoi region (Ericson, RTCD 5.1.5).
622fn region_params(v: Point3, a: Point3, b: Point3, c: Point3) -> (f64, f64) {
623    let (ab, ac) = (b - a, c - a);
624    let ap = v - a;
625    let (d1, d2) = (ab.dot(ap), ac.dot(ap));
626    if d1 <= 0.0 && d2 <= 0.0 {
627        return (0.0, 0.0);
628    }
629    let bp = v - b;
630    let (d3, d4) = (ab.dot(bp), ac.dot(bp));
631    if d3 >= 0.0 && d4 <= d3 {
632        return (1.0, 0.0);
633    }
634    let vc = d1 * d4 - d3 * d2;
635    if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
636        return (d1 / (d1 - d3), 0.0);
637    }
638    let cp = v - c;
639    let (d5, d6) = (ab.dot(cp), ac.dot(cp));
640    if d6 >= 0.0 && d5 <= d6 {
641        return (0.0, 1.0);
642    }
643    let vb = d5 * d2 - d1 * d6;
644    if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
645        return (0.0, d2 / (d2 - d6));
646    }
647    let va = d3 * d6 - d5 * d4;
648    if va <= 0.0 && d4 - d3 >= 0.0 && d5 - d6 >= 0.0 {
649        let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
650        return (1.0 - w, w);
651    }
652    let denominator = va + vb + vc;
653    (vb / denominator, vc / denominator)
654}
655
656/// Parameters of the nearest point on the three edges.
657fn thin_params(v: Point3, a: Point3, b: Point3, c: Point3) -> (f64, f64) {
658    let along = |from: Point3, to: Point3| {
659        let edge = to - from;
660        let length = edge.length_squared();
661        if length > 0.0 {
662            ((v - from).dot(edge) / length).clamp(0.0, 1.0)
663        } else {
664            0.0
665        }
666    };
667    let (x, y, z) = (along(a, b), along(a, c), along(b, c));
668    let candidates = [(x, 0.0), (0.0, y), (1.0 - z, z)];
669    let point = |(s, t): (f64, f64)| a + (b - a) * s + (c - a) * t;
670    candidates
671        .into_iter()
672        .min_by(|&p, &q| (v - point(p)).length().total_cmp(&(v - point(q)).length()))
673        .unwrap_or((0.0, 0.0))
674}
675
676/// A piece of the measured mesh: a triangle with per-corner drift and the
677/// facets found nearest to its corners.
678struct Piece {
679    corners: [Point3; 3],
680    drift: [f64; 3],
681    hints: [usize; 3],
682    upper: f64,
683}
684
685impl PartialEq for Piece {
686    fn eq(&self, other: &Self) -> bool {
687        self.cmp(other) == Ordering::Equal
688    }
689}
690
691impl Eq for Piece {}
692
693impl PartialOrd for Piece {
694    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
695        Some(self.cmp(other))
696    }
697}
698
699impl Ord for Piece {
700    fn cmp(&self, other: &Self) -> Ordering {
701        self.upper.total_cmp(&other.upper)
702    }
703}
704
705fn search(source: &Surface, target: &Target, accuracy: f64) -> HausdorffBounds {
706    let first = source.triangles[0][0];
707    let mut best = HausdorffBounds {
708        lower: 0.0,
709        upper: f64::INFINITY,
710        point_from: first,
711        point_to: target.point(first).point,
712    };
713    // Measure a sample `drift` from the mesh; returns its nearest facet.
714    let sample = |v: Point3, drift: f64, best: &mut HausdorffBounds| {
715        let nearest = target.point(v);
716        let lower = nearest.lower - drift;
717        if lower > best.lower {
718            best.lower = lower;
719            best.point_from = v;
720            best.point_to = nearest.point;
721        }
722        nearest.facet
723    };
724    let hints: Vec<usize> = (0..source.positions.len())
725        .map(|index| {
726            if source.used[index] {
727                sample(source.positions[index], 0.0, &mut best)
728            } else {
729                0
730            }
731        })
732        .collect();
733
734    let make = |corners: [Point3; 3], drift: [f64; 3], hints: [usize; 3]| {
735        let upper = target.piece(&corners, hints) + drift.iter().copied().fold(0.0, f64::max);
736        Piece {
737            corners,
738            drift,
739            hints,
740            upper,
741        }
742    };
743    let mut queue: BinaryHeap<Piece> = source
744        .triangles
745        .iter()
746        .zip(&source.indices)
747        .map(|(&corners, ids)| make(corners, [0.0; 3], ids.map(|id| hints[id])))
748        .collect();
749
750    // Upper bounds of pieces too small to split further.
751    let mut settled = 0.0_f64;
752    let mut splits = 0;
753    loop {
754        let open = queue.peek().map_or(0.0, |piece| piece.upper);
755        let upper = open.max(settled).max(best.lower);
756        if upper - best.lower <= accuracy || splits >= MAX_SPLITS {
757            best.upper = upper;
758            return best;
759        }
760        let Some(piece) = queue.pop() else {
761            best.upper = upper;
762            return best;
763        };
764        if piece.upper <= best.lower {
765            continue;
766        }
767        let [p0, p1, p2] = piece.corners;
768        let lengths = [
769            (p1 - p0).length_squared(),
770            (p2 - p1).length_squared(),
771            (p0 - p2).length_squared(),
772        ];
773        let edge = if lengths[0] >= lengths[1] && lengths[0] >= lengths[2] {
774            0
775        } else if lengths[1] >= lengths[2] {
776            1
777        } else {
778            2
779        };
780        let (i, j, k) = (edge, (edge + 1) % 3, (edge + 2) % 3);
781        let (vi, vj, vk) = (piece.corners[i], piece.corners[j], piece.corners[k]);
782        let size = vi.length().max(vj.length()).max(vk.length());
783        if lengths[edge].sqrt() <= 1024.0 * EPS * size {
784            settled = settled.max(piece.upper);
785            continue;
786        }
787        splits += 1;
788        let m = (vi + vj) * 0.5;
789        let dm = piece.drift[i].max(piece.drift[j]) + EPS * m.length();
790        let hm = sample(m, dm, &mut best);
791        let [di, dj, dk] = [piece.drift[i], piece.drift[j], piece.drift[k]];
792        let [hi, hj, hk] = [piece.hints[i], piece.hints[j], piece.hints[k]];
793        for (corners, drift, hints) in [
794            ([vi, m, vk], [di, dm, dk], [hi, hm, hk]),
795            ([m, vj, vk], [dm, dj, dk], [hm, hj, hk]),
796        ] {
797            let child = make(corners, drift, hints);
798            if child.upper > best.lower {
799                queue.push(child);
800            }
801        }
802    }
803}
804
805#[cfg(test)]
806mod tests {
807    use super::*;
808
809    fn facet(corners: [Point3; 3]) -> Facet {
810        let [a, b, c] = corners;
811        Facet {
812            corners,
813            scale: a.length() + b.length() + c.length(),
814            reach: a.length().max(b.length()).max(c.length()),
815            thin: false,
816            lo: a.min(b).min(c),
817            hi: a.max(b).max(c),
818        }
819    }
820
821    #[test]
822    fn point_bounds_bracket_the_distance_to_a_triangle() {
823        let t = facet([
824            Point3::new(0.0, 0.0, 0.0),
825            Point3::new(2.0, 0.0, 0.0),
826            Point3::new(0.0, 2.0, 0.0),
827        ]);
828        // Above the face, beyond an edge, beyond a corner.
829        for (v, d) in [
830            (Point3::new(0.5, 0.5, 3.0), 3.0),
831            (Point3::new(1.0, -4.0, 3.0), 5.0),
832            (Point3::new(-3.0, -4.0, 0.0), 5.0),
833            (Point3::new(0.25, 0.25, 0.0), 0.0),
834        ] {
835            let p = nearest_on(v, &t);
836            let (lo, hi) = (lower_at(v, p, &t), upper_at(v, p, &t));
837            assert!(lo <= d && d <= hi, "{lo} <= {d} <= {hi}");
838            assert!(hi - lo < 1e-13, "{lo} {hi}");
839        }
840    }
841
842    #[test]
843    fn a_thin_triangle_is_measured_along_its_edges() {
844        let t = Facet {
845            thin: true,
846            ..facet([
847                Point3::new(0.0, 0.0, 0.0),
848                Point3::new(1.0, 0.0, 0.0),
849                Point3::new(2.0, 0.0, 0.0),
850            ])
851        };
852        let v = Point3::new(1.5, 2.0, 0.0);
853        let p = nearest_on(v, &t);
854        assert!((lower_at(v, p, &t) - 2.0).abs() < 1e-13);
855        assert!((upper_at(v, p, &t) - 2.0).abs() < 1e-13);
856    }
857
858    #[test]
859    fn box_gaps_never_exceed_the_distance_to_the_box() {
860        let (lo, hi) = (Point3::ZERO, Point3::ONE);
861        assert!(box_gap(Point3::new(4.0, 5.0, 0.5), lo, hi) <= 5.0);
862        assert!(box_gap(Point3::new(4.0, 5.0, 0.5), lo, hi) > 5.0 - 1e-14);
863        assert_eq!(box_gap(Point3::new(0.5, 0.5, 0.5), lo, hi), 0.0);
864    }
865
866    #[test]
867    fn a_folded_pair_bounds_a_piece_across_its_hull() {
868        // A valley: two triangles sharing the y axis, rising to height `h`
869        // on either side. A piece spanning the valley's rim touches the
870        // surface at its corners, yet its middle hovers over the valley:
871        // the bound must pay the fold's excess, not read zero from corners.
872        // Shallower than 45 degrees, so the pair projects to a convex
873        // quadrilateral along either side's normal.
874        let h = 0.3;
875        let (a, b) = (Point3::new(0.0, -1.0, 0.0), Point3::new(0.0, 1.0, 0.0));
876        let (c, d) = (Point3::new(-1.0, 0.0, h), Point3::new(1.0, 0.0, h));
877        let surface = Surface {
878            triangles: vec![[a, b, c], [a, b, d]],
879            indices: vec![[0, 1, 2], [0, 1, 3]],
880            positions: vec![a, b, c, d],
881            used: vec![true; 4],
882        };
883        let target = Target::new(surface, 100.0);
884        assert_eq!(
885            target.patches.len(),
886            1,
887            "the fold is a flat-enough pair here"
888        );
889        // Every corner lies on the surface: the pair read without its
890        // excess would bound the piece by rounding alone.
891        let piece = [c, d, (b + c) * 0.5];
892        let bound = target.piece(&piece, [0, 1, 0]);
893        // The middle of `cd` is h / sqrt(1 + h^2) from either side.
894        let middle = (c + d) * 0.5;
895        let truth = [0, 1]
896            .map(|i| (middle - nearest_on(middle, &target.facets[i])).length())
897            .into_iter()
898            .fold(f64::INFINITY, f64::min);
899        assert!((truth - h / (1.0 + h * h).sqrt()).abs() < 1e-12);
900        assert!(bound >= truth, "bound {bound} below {truth}");
901    }
902}