1use axiolid_contracts::GeomError;
29use axiolid_core::{Aabb, Point3, Scalar};
30use axiolid_mesh::{decompose, TriMesh};
31
32pub const UNMERGED_UNION_CONTACT: &str =
37 "union of operands that meet within the linear tolerance, left as separate bodies";
38
39#[derive(Debug, Clone, Copy, PartialEq)]
41pub(crate) struct Unmerged {
42 pub distance: Scalar,
44 pub first: (Point3, Point3),
46 pub second: (Point3, Point3),
48}
49
50pub(crate) fn unmerged(mesh: &TriMesh, eps: Scalar) -> Option<Unmerged> {
55 let parts = decompose(mesh);
56 if parts.len() < 2 {
57 return None;
58 }
59 let boxes: Vec<Aabb> = parts.iter().map(bounds).collect();
60 for i in 0..parts.len() {
61 for j in i + 1..parts.len() {
62 if !close(&boxes[i], &boxes[j], eps) {
63 continue;
64 }
65 if let Some(distance) = axiolid_inspect::surface_gap(&parts[i], &parts[j], eps) {
66 return Some(Unmerged {
67 distance,
68 first: (boxes[i].min, boxes[i].max),
69 second: (boxes[j].min, boxes[j].max),
70 });
71 }
72 }
73 }
74 None
75}
76
77fn bounds(mesh: &TriMesh) -> Aabb {
78 let mut points = mesh.positions.iter();
79 let mut bounds = Aabb::from_point(points.next().copied().unwrap_or(Point3::ZERO));
80 for point in points {
81 bounds.extend(*point);
82 }
83 bounds
84}
85
86fn close(a: &Aabb, b: &Aabb, eps: Scalar) -> bool {
88 a.min.x - eps <= b.max.x
89 && b.min.x - eps <= a.max.x
90 && a.min.y - eps <= b.max.y
91 && b.min.y - eps <= a.max.y
92 && a.min.z - eps <= b.max.z
93 && b.min.z - eps <= a.max.z
94}
95
96pub(crate) fn refusal(found: &Unmerged, eps: Scalar) -> GeomError {
98 GeomError::Degenerate(format!(
99 "{UNMERGED_UNION_CONTACT}: two bodies {:e} apart, within the linear tolerance {eps:e} \
100 (boxes {:?} and {:?})",
101 found.distance, found.first, found.second
102 ))
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 fn cube(lo: Point3, hi: Point3) -> TriMesh {
110 let p = |x: bool, y: bool, z: bool| {
111 Point3::new(
112 if x { hi.x } else { lo.x },
113 if y { hi.y } else { lo.y },
114 if z { hi.z } else { lo.z },
115 )
116 };
117 let positions = vec![
118 p(false, false, false),
119 p(true, false, false),
120 p(true, true, false),
121 p(false, true, false),
122 p(false, false, true),
123 p(true, false, true),
124 p(true, true, true),
125 p(false, true, true),
126 ];
127 let indices = vec![
128 0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7, 0, 1, 5, 0, 5, 4, 1, 2, 6, 1, 6, 5, 2, 3, 7, 2, 7,
129 6, 3, 0, 4, 3, 4, 7,
130 ];
131 TriMesh::new(positions, indices)
132 }
133
134 fn unit(x: f64, y: f64, z: f64) -> TriMesh {
135 cube(Point3::new(x, y, z), Point3::new(x + 1.0, y + 1.0, z + 1.0))
136 }
137
138 fn together(a: &TriMesh, b: &TriMesh) -> TriMesh {
140 let mut positions = a.positions.clone();
141 let offset = positions.len() as u32;
142 positions.extend(b.positions.iter().copied());
143 let mut indices = a.indices.clone();
144 indices.extend(b.indices.iter().map(|i| i + offset));
145 TriMesh::new(positions, indices)
146 }
147
148 const EPS: f64 = 1e-6;
149
150 #[test]
151 fn one_body_is_never_unmerged() {
152 assert_eq!(unmerged(&unit(0.0, 0.0, 0.0), EPS), None);
153 }
154
155 #[test]
156 fn two_cubes_half_a_tolerance_apart_are_unmerged() {
157 let mesh = together(&unit(0.0, 0.0, 0.0), &unit(1.0 + 0.5e-6, 0.0, 0.0));
158 let found = unmerged(&mesh, EPS).expect("within the tolerance");
159 assert!(
160 (found.distance - 0.5e-6).abs() < 1e-15,
161 "{}",
162 found.distance
163 );
164 assert_eq!(found.first.1.x, 1.0);
165 assert_eq!(found.second.0.x, 1.0 + 0.5e-6);
166 }
167
168 #[test]
169 fn two_cubes_two_tolerances_apart_are_not() {
170 let mesh = together(&unit(0.0, 0.0, 0.0), &unit(1.0 + 2e-6, 0.0, 0.0));
171 assert_eq!(unmerged(&mesh, EPS), None);
172 }
173
174 #[test]
175 fn cubes_touching_along_an_edge_are_unmerged() {
176 let mesh = together(&unit(0.0, 0.0, 0.0), &unit(1.0, 1.0, 0.0));
177 let found = unmerged(&mesh, EPS).expect("touching");
178 assert_eq!(found.distance, 0.0);
179 }
180
181 #[test]
182 fn a_body_in_a_cavity_further_than_the_tolerance_is_not() {
183 let outer = cube(Point3::ZERO, Point3::new(10.0, 10.0, 10.0));
184 let mut void = cube(Point3::new(2.0, 2.0, 2.0), Point3::new(8.0, 8.0, 8.0));
185 void.indices
186 .as_chunks_mut::<3>()
187 .0
188 .iter_mut()
189 .for_each(|triangle| triangle.swap(1, 2));
190 let island = cube(Point3::new(3.0, 3.0, 3.0), Point3::new(4.0, 4.0, 4.0));
191 let mesh = together(&together(&outer, &void), &island);
192 assert_eq!(unmerged(&mesh, EPS), None);
193 }
194
195 #[test]
196 fn zero_tolerance_finds_only_touching() {
197 let touching = together(&unit(0.0, 0.0, 0.0), &unit(1.0, 0.0, 0.0));
198 assert!(unmerged(&touching, 0.0).is_some());
199 let apart = together(&unit(0.0, 0.0, 0.0), &unit(1.0 + 1e-9, 0.0, 0.0));
200 assert_eq!(unmerged(&apart, 0.0), None);
201 }
202}