1use axiolid_core::{Point3, Vec3};
24use axiolid_exact::{certify, Arith, Dyadic, SignExpr};
25use axiolid_guarantees::Sign;
26use axiolid_mesh::TriangleMeshView;
27use std::collections::HashMap;
28
29#[derive(Debug, Clone, Copy, PartialEq)]
31pub struct PlaneTolerance {
32 pub distance: f64,
34 pub angle: f64,
37}
38
39#[derive(Debug, Clone, PartialEq)]
41#[non_exhaustive]
42pub struct DetectedPlane {
43 pub triangles: Vec<u32>,
45 pub point: Point3,
47 pub normal: Vec3,
50 pub deviation: f64,
55 pub coplanar: bool,
57 pub area: f64,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63#[non_exhaustive]
64pub enum PlaneError {
65 NonFinite,
67 InvalidTolerance,
69}
70
71pub fn detect_planes<M: TriangleMeshView + ?Sized>(
78 mesh: &M,
79 tolerance: PlaneTolerance,
80) -> Result<Vec<DetectedPlane>, PlaneError> {
81 let PlaneTolerance { distance, angle } = tolerance;
82 if !(distance.is_finite() && distance >= 0.0 && angle.is_finite() && angle >= 0.0) {
83 return Err(PlaneError::InvalidTolerance);
84 }
85 if (0..mesh.position_count()).any(|i| !mesh.position(i).is_finite()) {
86 return Err(PlaneError::NonFinite);
87 }
88 let n = mesh.triangle_count();
89 let corners: Vec<[u64; 3]> = (0..n).map(|t| mesh.triangle(t)).collect();
90 let at = |i: u64| mesh.position(i as usize);
91 let tri: Vec<[Point3; 3]> = corners.iter().map(|c| c.map(at)).collect();
92 let normals: Vec<Vec3> = tri
93 .iter()
94 .map(|t| (t[1] - t[0]).cross(t[2] - t[0]))
95 .collect();
96 let areas: Vec<f64> = normals.iter().map(|v| 0.5 * v.length()).collect();
97 let mut edges: HashMap<(u64, u64), Vec<usize>> = HashMap::new();
99 for (t, c) in corners.iter().enumerate() {
100 for k in 0..3 {
101 let (a, b) = (c[k], c[(k + 1) % 3]);
102 edges.entry((a.min(b), a.max(b))).or_default().push(t);
103 }
104 }
105 let neighbours = |t: usize| -> Vec<usize> {
106 let c = corners[t];
107 let mut out = Vec::new();
108 for k in 0..3 {
109 let (a, b) = (c[k], c[(k + 1) % 3]);
110 for &o in &edges[&(a.min(b), a.max(b))] {
111 if o != t && !out.contains(&o) {
112 out.push(o);
113 }
114 }
115 }
116 out
117 };
118 let cos_limit = angle.min(std::f64::consts::PI).cos();
119 let mut order: Vec<usize> = (0..n).filter(|&t| areas[t] > 0.0).collect();
120 order.sort_by(|&a, &b| areas[b].total_cmp(&areas[a]).then(a.cmp(&b)));
121 let mut owner = vec![usize::MAX; n];
122 let mut out: Vec<DetectedPlane> = Vec::new();
123 for &seed in &order {
124 if owner[seed] != usize::MAX {
125 continue;
126 }
127 let id = out.len();
128 let mut members = vec![seed];
129 owner[seed] = id;
130 let mut fit = Fit::of(&members, &tri, &normals, &areas);
131 let mut frontier = vec![seed];
132 let mut since_refit = 0;
133 while let Some(t) = frontier.pop() {
134 for o in neighbours(t) {
135 if owner[o] != usize::MAX || areas[o] == 0.0 {
136 continue;
137 }
138 let unit = normals[o] / (2.0 * areas[o]);
139 if unit.dot(fit.normal) < cos_limit {
140 continue;
141 }
142 if tri[o].iter().any(|&v| fit.distance(v) > distance) {
143 continue;
144 }
145 owner[o] = id;
146 members.push(o);
147 frontier.push(o);
148 since_refit += 1;
149 if since_refit >= 16 {
150 fit = Fit::of(&members, &tri, &normals, &areas);
151 since_refit = 0;
152 }
153 }
154 }
155 loop {
157 fit = Fit::of(&members, &tri, &normals, &areas);
158 let bounds: Vec<f64> = members.iter().map(|&t| fit.bound(&tri[t])).collect();
159 let (worst, bound) = bounds.iter().enumerate().fold(
160 (0, 0.0f64),
161 |(wi, wb), (i, &b)| if b > wb { (i, b) } else { (wi, wb) },
162 );
163 if bound <= distance + fit.slack || members.len() == 1 {
164 let mut sorted: Vec<u32> = members.iter().map(|&t| t as u32).collect();
167 sorted.sort_unstable();
168 let coplanar = coplanar(members.iter().flat_map(|&t| tri[t]));
169 out.push(DetectedPlane {
170 triangles: sorted,
171 point: fit.point,
172 normal: fit.normal,
173 deviation: bound,
174 coplanar,
175 area: members.iter().map(|&t| areas[t]).sum(),
176 });
177 break;
178 }
179 let t = members.swap_remove(worst);
180 owner[t] = usize::MAX;
181 }
182 }
185 let mut left: Vec<usize> = order
187 .iter()
188 .copied()
189 .filter(|&t| owner[t] == usize::MAX)
190 .collect();
191 while let Some(t) = left.pop() {
192 if owner[t] != usize::MAX {
193 continue;
194 }
195 let fit = Fit::of(&[t], &tri, &normals, &areas);
196 owner[t] = out.len();
197 out.push(DetectedPlane {
198 triangles: vec![t as u32],
199 point: fit.point,
200 normal: fit.normal,
201 deviation: fit.bound(&tri[t]),
202 coplanar: true,
203 area: areas[t],
204 });
205 }
206 out.sort_by(|a, b| {
207 b.area
208 .total_cmp(&a.area)
209 .then(a.triangles[0].cmp(&b.triangles[0]))
210 });
211 Ok(out)
212}
213
214struct Fit {
217 point: Point3,
218 normal: Vec3,
219 slack: f64,
220}
221
222impl Fit {
223 fn of(members: &[usize], tri: &[[Point3; 3]], normals: &[Vec3], areas: &[f64]) -> Self {
224 let mut total = 0.0;
225 let mut weighted = Vec3::ZERO;
226 let mut direction = Vec3::ZERO;
227 let base = tri[members[0]][0];
229 for &t in members {
230 let [a, b, c] = tri[t];
231 weighted += ((a - base) + (b - base) + (c - base)) * (areas[t] / 3.0);
232 total += areas[t];
233 direction += normals[t];
235 }
236 let reach = members
237 .iter()
238 .flat_map(|&t| tri[t])
239 .map(|v| (v - base).abs().max_element())
240 .fold(0.0, f64::max);
241 let far = base.abs().max_element() + reach;
242 Self {
243 point: base + weighted / total,
244 normal: direction.normalize(),
245 slack: 32.0 * f64::EPSILON * far,
246 }
247 }
248
249 fn distance(&self, v: Point3) -> f64 {
250 self.normal.dot(v - self.point).abs()
251 }
252
253 fn bound(&self, t: &[Point3; 3]) -> f64 {
257 let n = [self.normal.x, self.normal.y, self.normal.z].map(Iv::point);
258 let norm2 = n[0].mul(n[0]).add(n[1].mul(n[1])).add(n[2].mul(n[2]));
259 t.iter()
260 .map(|v| {
261 let d = [v.x, v.y, v.z];
262 let p = [self.point.x, self.point.y, self.point.z];
263 let dot = (0..3)
264 .map(|k| n[k].mul(Iv::point(d[k]).sub(Iv::point(p[k]))))
265 .fold(Iv::point(0.0), Iv::add);
266 let top = dot.lo.abs().max(dot.hi.abs());
267 let q = (top * top).next_up() / norm2.lo;
269 q.next_up().sqrt().next_up()
270 })
271 .fold(0.0, f64::max)
272 }
273}
274
275fn coplanar(points: impl Iterator<Item = Point3>) -> bool {
277 let points: Vec<Point3> = points.collect();
278 let Some((a, b, c)) = spanning(&points) else {
279 return true;
280 };
281 points
282 .iter()
283 .all(|&d| certify(&Orient3 { p: [a, b, c, d] }).ok() == Some(Sign::Zero))
284}
285
286fn spanning(points: &[Point3]) -> Option<(Point3, Point3, Point3)> {
288 let a = points[0];
289 let b = *points.iter().find(|&&p| p != a)?;
290 let c = *points.iter().find(|&&p| {
291 let (u, v) = (b - a, p - a);
292 let cross = [
293 exact(u.y)
294 .mul(&exact(v.z))
295 .sub(&exact(u.z).mul(&exact(v.y))),
296 exact(u.z)
297 .mul(&exact(v.x))
298 .sub(&exact(u.x).mul(&exact(v.z))),
299 exact(u.x)
300 .mul(&exact(v.y))
301 .sub(&exact(u.y).mul(&exact(v.x))),
302 ];
303 cross.iter().any(|x| x.sign() != Some(Sign::Zero))
304 })?;
305 Some((a, b, c))
306}
307
308fn exact(x: f64) -> Dyadic {
309 Dyadic::from_f64(x)
310}
311
312struct Orient3 {
313 p: [Point3; 4],
314}
315
316impl SignExpr for Orient3 {
317 fn sign_in<T: Arith>(&self) -> Option<Sign> {
318 let q = |i: usize| {
319 let p = self.p[i];
320 [T::from_f64(p.x), T::from_f64(p.y), T::from_f64(p.z)]
321 };
322 let (a, b, c, d) = (q(0), q(1), q(2), q(3));
323 let sub = |x: &[T; 3], y: &[T; 3]| [x[0].sub(&y[0]), x[1].sub(&y[1]), x[2].sub(&y[2])];
324 let (u, v, w) = (sub(&b, &a), sub(&c, &a), sub(&d, &a));
325 let n = [
326 u[1].mul(&v[2]).sub(&u[2].mul(&v[1])),
327 u[2].mul(&v[0]).sub(&u[0].mul(&v[2])),
328 u[0].mul(&v[1]).sub(&u[1].mul(&v[0])),
329 ];
330 n[0].mul(&w[0])
331 .add(&n[1].mul(&w[1]))
332 .add(&n[2].mul(&w[2]))
333 .sign()
334 }
335}
336
337#[derive(Debug, Clone, Copy)]
339struct Iv {
340 lo: f64,
341 hi: f64,
342}
343
344impl Iv {
345 fn point(v: f64) -> Self {
346 Self { lo: v, hi: v }
347 }
348
349 fn outward(lo: f64, hi: f64) -> Self {
350 Self {
351 lo: lo.next_down(),
352 hi: hi.next_up(),
353 }
354 }
355
356 fn add(self, o: Self) -> Self {
357 Self::outward(self.lo + o.lo, self.hi + o.hi)
358 }
359
360 fn sub(self, o: Self) -> Self {
361 Self::outward(self.lo - o.hi, self.hi - o.lo)
362 }
363
364 fn mul(self, o: Self) -> Self {
365 let p = [
366 self.lo * o.lo,
367 self.lo * o.hi,
368 self.hi * o.lo,
369 self.hi * o.hi,
370 ];
371 Self::outward(
372 p.iter().copied().fold(f64::INFINITY, f64::min),
373 p.iter().copied().fold(f64::NEG_INFINITY, f64::max),
374 )
375 }
376}