1use axiolid_core::{Aabb, Point3, Vec3};
14use axiolid_guarantees::Sign;
15use axiolid_mesh::TriMesh;
16use axiolid_predicates::orient3d;
17use axiolid_spatial::{Bvh, SpatialItem};
18
19pub fn min_gap(a: &TriMesh, b: &TriMesh, search_length: f64) -> Option<f64> {
30 if !search_length.is_finite() || search_length <= 0.0 {
31 return None;
32 }
33 if any_vertex_inside(a, b) || any_vertex_inside(b, a) {
39 return Some(0.0);
40 }
41 surface_gap(a, b, search_length)
42}
43
44pub fn surface_gap(a: &TriMesh, b: &TriMesh, search_length: f64) -> Option<f64> {
57 if !search_length.is_finite() || search_length < 0.0 {
58 return None;
59 }
60 let b_index = build_index(b, search_length);
61 let mut best: Option<f64> = None;
62 let mut candidates = Vec::new();
63
64 for a_triangle in triangles(a) {
65 let probe = grow(bounds_of(&a_triangle), search_length);
66 candidates.clear();
67 b_index.query_aabb(&probe, &mut candidates);
68 for &candidate in &candidates {
69 let Some(item) = b_index.item(candidate) else {
70 continue;
71 };
72 let Some(b_triangle) = triangle_at(b, item.key as usize) else {
73 continue;
74 };
75 let distance = triangle_distance(&a_triangle, &b_triangle);
76 if best.is_none_or(|current| distance < current) {
77 best = Some(distance);
78 }
79 if distance == 0.0 {
80 return Some(0.0);
81 }
82 }
83 }
84 best.filter(|distance| *distance <= search_length)
85}
86
87pub(crate) fn triangle_distance(a: &[Point3; 3], b: &[Point3; 3]) -> f64 {
93 if edge_crosses(a, b) || edge_crosses(b, a) {
97 return 0.0;
98 }
99 let mut best = f64::INFINITY;
100 for i in 0..3 {
101 for j in 0..3 {
102 let d = segment_distance(a[i], a[(i + 1) % 3], b[j], b[(j + 1) % 3]);
103 best = best.min(d);
104 }
105 }
106 for vertex in a {
107 best = best.min(point_triangle_distance(*vertex, b));
108 }
109 for vertex in b {
110 best = best.min(point_triangle_distance(*vertex, a));
111 }
112 best
113}
114
115fn edge_crosses(edges: &[Point3; 3], face: &[Point3; 3]) -> bool {
123 (0..3).any(|i| segment_crosses(edges[i], edges[(i + 1) % 3], face))
124}
125
126fn segment_crosses(p: Point3, q: Point3, face: &[Point3; 3]) -> bool {
127 let [a, b, c] = *face;
128 let sign = |value: Option<Sign>| match value {
129 Some(Sign::Positive) => Some(1),
130 Some(Sign::Negative) => Some(-1),
131 Some(Sign::Zero) => Some(0),
132 _ => None,
133 };
134 let (Some(sp), Some(sq)) = (
135 sign(orient3d(a, b, c, p).sign()),
136 sign(orient3d(a, b, c, q).sign()),
137 ) else {
138 return false;
139 };
140 if sp * sq > 0 || (sp == 0 && sq == 0) {
141 return false;
142 }
143 let mut seen = 0;
144 for (from, to) in [(a, b), (b, c), (c, a)] {
145 let Some(side) = sign(orient3d(p, q, from, to).sign()) else {
146 return false;
147 };
148 if side != 0 {
149 if seen != 0 && seen != side {
150 return false;
151 }
152 seen = side;
153 }
154 }
155 true
156}
157
158fn segment_distance(p0: Point3, p1: Point3, q0: Point3, q1: Point3) -> f64 {
160 let u = p1 - p0;
161 let v = q1 - q0;
162 let w = p0 - q0;
163 let a = u.dot(u);
164 let b = u.dot(v);
165 let c = v.dot(v);
166 let d = u.dot(w);
167 let e = v.dot(w);
168 let denominator = a * c - b * b;
169
170 let (mut s, mut t) = if denominator.abs() <= f64::EPSILON * a.max(c).max(1.0) {
173 (0.0, if c > 0.0 { e / c } else { 0.0 })
174 } else {
175 ((b * e - c * d) / denominator, (a * e - b * d) / denominator)
176 };
177 s = s.clamp(0.0, 1.0);
178 t = t.clamp(0.0, 1.0);
179
180 if c > 0.0 {
183 t = ((s * b - e) / c).clamp(0.0, 1.0);
184 }
185 if a > 0.0 {
186 s = (-((t * b + d) / a)).clamp(0.0, 1.0);
187 }
188 ((p0 + u * s) - (q0 + v * t)).length()
189}
190
191fn point_triangle_distance(point: Point3, triangle: &[Point3; 3]) -> f64 {
193 let [a, b, c] = *triangle;
194 let normal = (b - a).cross(c - a);
195 let area_squared = normal.length_squared();
196 if area_squared > 0.0 {
197 let distance = normal.dot(point - a) / area_squared.sqrt();
200 let projected = point - normal * (normal.dot(point - a) / area_squared);
201 let inside = [(a, b), (b, c), (c, a)]
202 .iter()
203 .all(|(from, to)| normal.dot((*to - *from).cross(projected - *from)) >= 0.0);
204 if inside {
205 return distance.abs();
206 }
207 }
208 let mut best = f64::INFINITY;
211 for i in 0..3 {
212 best = best.min(point_segment_distance(
213 point,
214 triangle[i],
215 triangle[(i + 1) % 3],
216 ));
217 }
218 best
219}
220
221fn point_segment_distance(point: Point3, from: Point3, to: Point3) -> f64 {
223 let direction = to - from;
224 let length_squared = direction.length_squared();
225 if length_squared == 0.0 {
226 return (point - from).length();
227 }
228 let t = ((point - from).dot(direction) / length_squared).clamp(0.0, 1.0);
229 (point - (from + direction * t)).length()
230}
231
232fn triangles(mesh: &TriMesh) -> impl Iterator<Item = [Point3; 3]> + '_ {
234 (0..mesh.indices.len() / 3).filter_map(move |index| triangle_at(mesh, index))
235}
236
237pub(crate) fn triangle_at(mesh: &TriMesh, index: usize) -> Option<[Point3; 3]> {
239 let corners = mesh.indices.get(index * 3..index * 3 + 3)?;
240 Some([
241 *mesh.positions.get(corners[0] as usize)?,
242 *mesh.positions.get(corners[1] as usize)?,
243 *mesh.positions.get(corners[2] as usize)?,
244 ])
245}
246
247pub(crate) fn bounds_of(triangle: &[Point3; 3]) -> Aabb {
249 let mut bounds = Aabb::from_point(triangle[0]);
250 bounds.extend(triangle[1]);
251 bounds.extend(triangle[2]);
252 bounds
253}
254
255pub(crate) fn grow(bounds: Aabb, margin: f64) -> Aabb {
257 let padding = Vec3::splat(margin);
258 let mut grown = Aabb::from_point(bounds.min - padding);
259 grown.extend(bounds.max + padding);
260 grown
261}
262
263pub(crate) fn build_index(mesh: &TriMesh, search_length: f64) -> Bvh<u32> {
265 let items = (0..mesh.indices.len() / 3).filter_map(|index| {
266 let triangle = triangle_at(mesh, index)?;
267 Some(SpatialItem::new(
268 index as u32,
269 grow(bounds_of(&triangle), search_length),
270 ))
271 });
272 Bvh::build(items)
273}
274
275fn any_vertex_inside(probe: &TriMesh, solid: &TriMesh) -> bool {
277 probe
278 .positions
279 .iter()
280 .any(|point| crate::containment::contains(solid, *point).unwrap_or(false))
281}