axiolid_inspect/
clearance.rs1use axiolid_core::{Aabb, Point3, Vec3};
14use axiolid_mesh::TriMesh;
15use axiolid_spatial::{Bvh, SpatialItem};
16
17pub fn min_gap(a: &TriMesh, b: &TriMesh, search_length: f64) -> Option<f64> {
28 if !search_length.is_finite() || search_length <= 0.0 {
29 return None;
30 }
31 if any_vertex_inside(a, b) || any_vertex_inside(b, a) {
37 return Some(0.0);
38 }
39
40 let b_index = build_index(b, search_length);
41 let mut best: Option<f64> = None;
42 let mut candidates = Vec::new();
43
44 for a_triangle in triangles(a) {
45 let probe = grow(bounds_of(&a_triangle), search_length);
46 candidates.clear();
47 b_index.query_aabb(&probe, &mut candidates);
48 for &candidate in &candidates {
49 let Some(item) = b_index.item(candidate) else {
50 continue;
51 };
52 let Some(b_triangle) = triangle_at(b, item.key as usize) else {
53 continue;
54 };
55 let distance = triangle_distance(&a_triangle, &b_triangle);
61 if best.is_none_or(|current| distance < current) {
62 best = Some(distance);
63 }
64 if distance == 0.0 {
65 return Some(0.0);
66 }
67 }
68 }
69 best.filter(|distance| *distance <= search_length)
70}
71
72fn triangle_distance(a: &[Point3; 3], b: &[Point3; 3]) -> f64 {
78 let mut best = f64::INFINITY;
79 for i in 0..3 {
80 for j in 0..3 {
81 let d = segment_distance(a[i], a[(i + 1) % 3], b[j], b[(j + 1) % 3]);
82 best = best.min(d);
83 }
84 }
85 for vertex in a {
86 best = best.min(point_triangle_distance(*vertex, b));
87 }
88 for vertex in b {
89 best = best.min(point_triangle_distance(*vertex, a));
90 }
91 best
92}
93
94fn segment_distance(p0: Point3, p1: Point3, q0: Point3, q1: Point3) -> f64 {
96 let u = p1 - p0;
97 let v = q1 - q0;
98 let w = p0 - q0;
99 let a = u.dot(u);
100 let b = u.dot(v);
101 let c = v.dot(v);
102 let d = u.dot(w);
103 let e = v.dot(w);
104 let denominator = a * c - b * b;
105
106 let (mut s, mut t) = if denominator.abs() <= f64::EPSILON * a.max(c).max(1.0) {
109 (0.0, if c > 0.0 { e / c } else { 0.0 })
110 } else {
111 ((b * e - c * d) / denominator, (a * e - b * d) / denominator)
112 };
113 s = s.clamp(0.0, 1.0);
114 t = t.clamp(0.0, 1.0);
115
116 if c > 0.0 {
119 t = ((s * b - e) / c).clamp(0.0, 1.0);
120 }
121 if a > 0.0 {
122 s = (-((t * b + d) / a)).clamp(0.0, 1.0);
123 }
124 ((p0 + u * s) - (q0 + v * t)).length()
125}
126
127fn point_triangle_distance(point: Point3, triangle: &[Point3; 3]) -> f64 {
129 let [a, b, c] = *triangle;
130 let normal = (b - a).cross(c - a);
131 let area_squared = normal.length_squared();
132 if area_squared > 0.0 {
133 let distance = normal.dot(point - a) / area_squared.sqrt();
136 let projected = point - normal * (normal.dot(point - a) / area_squared);
137 let inside = [(a, b), (b, c), (c, a)]
138 .iter()
139 .all(|(from, to)| normal.dot((*to - *from).cross(projected - *from)) >= 0.0);
140 if inside {
141 return distance.abs();
142 }
143 }
144 let mut best = f64::INFINITY;
147 for i in 0..3 {
148 best = best.min(point_segment_distance(
149 point,
150 triangle[i],
151 triangle[(i + 1) % 3],
152 ));
153 }
154 best
155}
156
157fn point_segment_distance(point: Point3, from: Point3, to: Point3) -> f64 {
159 let direction = to - from;
160 let length_squared = direction.length_squared();
161 if length_squared == 0.0 {
162 return (point - from).length();
163 }
164 let t = ((point - from).dot(direction) / length_squared).clamp(0.0, 1.0);
165 (point - (from + direction * t)).length()
166}
167
168fn triangles(mesh: &TriMesh) -> impl Iterator<Item = [Point3; 3]> + '_ {
170 (0..mesh.indices.len() / 3).filter_map(move |index| triangle_at(mesh, index))
171}
172
173pub(crate) fn triangle_at(mesh: &TriMesh, index: usize) -> Option<[Point3; 3]> {
175 let corners = mesh.indices.get(index * 3..index * 3 + 3)?;
176 Some([
177 *mesh.positions.get(corners[0] as usize)?,
178 *mesh.positions.get(corners[1] as usize)?,
179 *mesh.positions.get(corners[2] as usize)?,
180 ])
181}
182
183fn bounds_of(triangle: &[Point3; 3]) -> Aabb {
185 let mut bounds = Aabb::from_point(triangle[0]);
186 bounds.extend(triangle[1]);
187 bounds.extend(triangle[2]);
188 bounds
189}
190
191fn grow(bounds: Aabb, margin: f64) -> Aabb {
193 let padding = Vec3::splat(margin);
194 let mut grown = Aabb::from_point(bounds.min - padding);
195 grown.extend(bounds.max + padding);
196 grown
197}
198
199fn build_index(mesh: &TriMesh, search_length: f64) -> Bvh<u32> {
201 let items = (0..mesh.indices.len() / 3).filter_map(|index| {
202 let triangle = triangle_at(mesh, index)?;
203 Some(SpatialItem::new(
204 index as u32,
205 grow(bounds_of(&triangle), search_length),
206 ))
207 });
208 Bvh::build(items)
209}
210
211fn any_vertex_inside(probe: &TriMesh, solid: &TriMesh) -> bool {
213 probe
214 .positions
215 .iter()
216 .any(|point| crate::containment::contains(solid, *point).unwrap_or(false))
217}