1use crate::{Number, Plane, Polygon};
5use std::collections::HashMap;
6
7pub fn remove_collinear_vertices<T: Number>(polygon: Polygon<T, 3>) -> Option<Polygon<T, 3>> {
12 let verts = polygon.vertices();
13 let n = verts.len();
14 if n < 3 {
15 return None;
16 }
17
18 let eps = T::from(1e-10).expect("BUG: f64 must represent 1e-10");
19 let mut keep = Vec::with_capacity(n);
20
21 for i in 0..n {
22 let prev = if i == 0 { n - 1 } else { i - 1 };
23 let next = if i + 1 < n { i + 1 } else { 0 };
24 let a = verts[prev];
25 let b = verts[i];
26 let c = verts[next];
27 let ab = b - a;
28 let ac = c - a;
29 let cross = ab.cross(&ac);
30 if cross.norm_squared() > eps {
31 keep.push(verts[i]);
32 }
33 }
34
35 if keep.len() < 3 {
36 return None;
37 }
38
39 Some(polygon.clone_with_points(ndarray::Array1::from_vec(keep)))
40}
41
42pub fn is_degenerate<T: Number>(polygon: &Polygon<T, 3>) -> bool {
44 let verts = polygon.vertices();
45 if verts.len() < 3 {
46 return true;
47 }
48
49 let eps = T::from(1e-10).expect("BUG: f64 must represent 1e-10");
51 let mut area_sq = T::zero();
52 let v0 = verts[0];
53 for i in 1..verts.len() - 1 {
54 let e1 = verts[i] - v0;
55 let e2 = verts[i + 1] - v0;
56 let cross = e1.cross(&e2);
57 area_sq += cross.norm_squared();
58 }
59 area_sq < eps
60}
61
62type VertexKey = [i64; 3];
64
65fn vertex_key<T: Number>(p: &nalgebra::Point3<T>) -> VertexKey {
66 let scale = T::from(1e5).expect("BUG: f64 must represent 1e5");
67 [
68 num_traits::cast::<T, f64>(num_traits::Float::round(p.x * scale)).unwrap_or(0.0) as i64,
69 num_traits::cast::<T, f64>(num_traits::Float::round(p.y * scale)).unwrap_or(0.0) as i64,
70 num_traits::cast::<T, f64>(num_traits::Float::round(p.z * scale)).unwrap_or(0.0) as i64,
71 ]
72}
73
74type PlaneKey = [i64; 4];
78
79fn plane_key<T: Number>(plane: &Plane<T, 3>) -> PlaneKey {
80 let scale = T::from(1e4).expect("BUG: f64 must represent 1e4");
81 let n = plane.normal();
82 let d = plane.distance();
83 [
84 num_traits::cast::<T, f64>(num_traits::Float::round(n.x * scale)).unwrap_or(0.0) as i64,
85 num_traits::cast::<T, f64>(num_traits::Float::round(n.y * scale)).unwrap_or(0.0) as i64,
86 num_traits::cast::<T, f64>(num_traits::Float::round(n.z * scale)).unwrap_or(0.0) as i64,
87 num_traits::cast::<T, f64>(num_traits::Float::round(d * scale)).unwrap_or(0.0) as i64,
88 ]
89}
90
91type DirectedEdge = (VertexKey, VertexKey);
92
93pub fn merge_coplanar_polygons<T: Number>(polygons: Vec<Polygon<T, 3>>) -> Vec<Polygon<T, 3>> {
101 let mut groups: HashMap<PlaneKey, Vec<Polygon<T, 3>>> = HashMap::new();
103 for p in polygons {
104 groups.entry(plane_key(&p.plane())).or_default().push(p);
105 }
106
107 let mut result = Vec::new();
108 for (_, group) in groups {
109 if group.len() < 2 {
110 result.extend(group);
111 continue;
112 }
113 result.extend(merge_group(group));
114 }
115
116 result
117}
118
119fn merge_group<T: Number>(mut polys: Vec<Polygon<T, 3>>) -> Vec<Polygon<T, 3>> {
121 let mut changed = true;
123 while changed {
124 changed = false;
125 let mut edge_to_poly: HashMap<DirectedEdge, usize> = HashMap::new();
129 for (idx, poly) in polys.iter().enumerate() {
130 let verts = poly.vertices();
131 let n = verts.len();
132 for i in 0..n {
133 let j = if i + 1 < n { i + 1 } else { 0 };
134 let a = vertex_key(&verts[i]);
135 let b = vertex_key(&verts[j]);
136 edge_to_poly.insert((a, b), idx);
137 }
138 }
139
140 let mut merge = None;
142 'outer: for (idx_a, poly_a) in polys.iter().enumerate() {
143 let verts_a = poly_a.vertices();
144 let na = verts_a.len();
145 for i in 0..na {
146 let j = if i + 1 < na { i + 1 } else { 0 };
147 let a_key = vertex_key(&verts_a[i]);
148 let b_key = vertex_key(&verts_a[j]);
149 if let Some(&idx_b) = edge_to_poly.get(&(b_key, a_key))
151 && idx_b != idx_a
152 {
153 merge = Some((idx_a, i, idx_b));
154 break 'outer;
155 }
156 }
157 }
158
159 if let Some((idx_a, edge_start_a, idx_b)) = merge {
160 let (ia, ib) = if idx_a < idx_b {
162 (idx_a, idx_b)
163 } else {
164 (idx_b, idx_a)
165 };
166 let poly_hi = polys.remove(ib);
167 let poly_lo = polys.remove(ia);
168 let (poly_a, edge_a, poly_b) = if idx_a < idx_b {
169 (poly_lo, edge_start_a, poly_hi)
170 } else {
171 (poly_hi, edge_start_a, poly_lo)
172 };
173 if let Some(merged) = merge_two_polygons(&poly_a, edge_a, &poly_b) {
174 polys.push(merged);
175 changed = true;
176 } else {
177 polys.push(poly_a);
179 polys.push(poly_b);
180 }
181 }
182 }
183 polys
184}
185
186fn merge_two_polygons<T: Number>(
195 poly_a: &Polygon<T, 3>,
196 edge_a_start: usize,
197 poly_b: &Polygon<T, 3>,
198) -> Option<Polygon<T, 3>> {
199 let va = poly_a.vertices();
200 let vb = poly_b.vertices();
201 let na = va.len();
202 let nb = vb.len();
203
204 let a_key = vertex_key(&va[edge_a_start]);
205 let b_key = vertex_key(&va[(edge_a_start + 1) % na]);
206
207 let edge_b_start = (0..nb).find(|&i| {
209 let j = (i + 1) % nb;
210 vertex_key(&vb[i]) == b_key && vertex_key(&vb[j]) == a_key
211 })?;
212
213 let mut merged = Vec::with_capacity(na + nb - 2);
226
227 for i in 0..=edge_a_start {
229 merged.push(va[i]);
230 }
231
232 let b_detour_count = nb - 2; for k in 0..b_detour_count {
235 let idx = (edge_b_start + 2 + k) % nb;
236 merged.push(vb[idx]);
237 }
238
239 for k in 0..na.saturating_sub(edge_a_start + 1) {
243 let idx = edge_a_start + 1 + k;
244 merged.push(va[idx]);
245 }
246
247 if merged.len() < 3 {
248 return None;
249 }
250
251 Some(poly_a.clone_with_points(ndarray::Array1::from_vec(merged)))
252}
253
254pub fn cleanup_polygons<T: Number>(polygons: Vec<Polygon<T, 3>>) -> Vec<Polygon<T, 3>> {
256 let cleaned: Vec<Polygon<T, 3>> = polygons
258 .into_iter()
259 .filter(|p| !is_degenerate(p))
260 .filter_map(remove_collinear_vertices)
261 .collect();
262
263 merge_coplanar_polygons(cleaned)
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use nalgebra::Point3;
271
272 fn total_vertices<T: Number>(polys: &[Polygon<T, 3>]) -> usize {
273 polys.iter().map(|p| p.vertices().len()).sum()
274 }
275
276 fn polygon_area_xy(poly: &Polygon<f64, 3>) -> f64 {
279 let v = poly.vertices();
280 let n = v.len();
281 let mut sum = 0.0;
282 for i in 0..n {
283 let j = (i + 1) % n;
284 sum += v[i].x * v[j].y - v[j].x * v[i].y;
285 }
286 0.5 * sum.abs()
287 }
288
289 fn vertex_keys(poly: &Polygon<f64, 3>) -> std::collections::BTreeSet<VertexKey> {
291 poly.vertices().iter().map(vertex_key).collect()
292 }
293
294 #[test]
295 fn collinear_vertex_removal() {
296 let p = Polygon::from_points(vec![
298 Point3::new(0.0f64, 0.0, 0.0),
299 Point3::new(5.0, 0.0, 0.0),
300 Point3::new(10.0, 0.0, 0.0), Point3::new(5.0, 10.0, 0.0),
302 ])
303 .unwrap();
304 let cleaned = remove_collinear_vertices(p).unwrap();
305 assert_eq!(
306 cleaned.vertices().len(),
307 3,
308 "collinear vertex should be removed"
309 );
310 }
311
312 #[test]
313 fn degenerate_polygon_filtered() {
314 let p = Polygon::from_points_and_plane(
316 vec![
317 Point3::new(0.0f64, 0.0, 0.0),
318 Point3::new(1.0, 0.0, 0.0),
319 Point3::new(2.0, 0.0, 0.0),
320 ],
321 Plane::xy_plane(),
322 )
323 .unwrap();
324 assert!(is_degenerate(&p));
325 }
326
327 #[test]
328 fn coplanar_merge_two_triangles() {
329 let a = Point3::new(0.0f64, 0.0, 0.0);
331 let b = Point3::new(1.0, 0.0, 0.0);
332 let c = Point3::new(1.0, 1.0, 0.0);
333 let d = Point3::new(0.0, 1.0, 0.0);
334
335 let p1 = Polygon::from_points(vec![a, b, c]).unwrap();
336 let p2 = Polygon::from_points(vec![a, c, d]).unwrap();
337
338 assert_eq!(total_vertices(&[p1.clone(), p2.clone()]), 6);
339
340 let merged = merge_coplanar_polygons(vec![p1, p2]);
341 let merged_verts = total_vertices(&merged);
342 assert!(
343 merged_verts <= 4,
344 "two triangles sharing an edge should merge to <= 4 vertices, got {merged_verts}"
345 );
346 }
347
348 #[test]
349 fn coplanar_merge_nonwraparound_edge_keeps_all_corners() {
350 let a = Point3::new(0.0f64, 0.0, 0.0);
359 let b = Point3::new(1.0, 0.0, 0.0);
360 let c = Point3::new(1.0, 1.0, 0.0);
361 let d = Point3::new(2.0, 0.5, 0.0);
362
363 let p1 = Polygon::from_points(vec![a, b, c]).unwrap();
364 let p2 = Polygon::from_points(vec![b, d, c]).unwrap();
365 let expected_area = polygon_area_xy(&p1) + polygon_area_xy(&p2);
366
367 let merged = merge_coplanar_polygons(vec![p1, p2]);
368
369 assert_eq!(
370 merged.len(),
371 1,
372 "the two triangles must merge into a single polygon"
373 );
374 let quad = &merged[0];
375 assert_eq!(
376 quad.vertices().len(),
377 4,
378 "merged polygon must be a quad (4 vertices), no dropped/duplicated vertex, got {}",
379 quad.vertices().len()
380 );
381
382 let got = vertex_keys(quad);
384 for (name, p) in [("a", a), ("b", b), ("c", c), ("d", d)] {
385 assert!(
386 got.contains(&vertex_key(&p)),
387 "merged quad is missing corner {name}"
388 );
389 }
390
391 assert!(
393 (polygon_area_xy(quad) - expected_area).abs() < 1e-9,
394 "merged quad area {} should equal the summed triangle area {expected_area}",
395 polygon_area_xy(quad)
396 );
397 }
398
399 #[test]
400 fn coplanar_merge_wraparound_edge_keeps_all_corners() {
401 let a = Point3::new(0.0f64, 0.0, 0.0);
408 let b = Point3::new(1.0, 0.0, 0.0);
409 let c = Point3::new(1.0, 1.0, 0.0);
410 let d = Point3::new(0.0, 1.0, 0.0);
411
412 let p1 = Polygon::from_points(vec![a, b, c]).unwrap();
413 let p2 = Polygon::from_points(vec![a, c, d]).unwrap();
414 let expected_area = polygon_area_xy(&p1) + polygon_area_xy(&p2);
415
416 let merged = merge_coplanar_polygons(vec![p1, p2]);
417
418 assert_eq!(merged.len(), 1, "triangles must merge into one polygon");
419 let quad = &merged[0];
420 assert_eq!(
421 quad.vertices().len(),
422 4,
423 "wrap-around merge must stay a quad, got {}",
424 quad.vertices().len()
425 );
426
427 let got = vertex_keys(quad);
428 for (name, p) in [("a", a), ("b", b), ("c", c), ("d", d)] {
429 assert!(
430 got.contains(&vertex_key(&p)),
431 "merged quad is missing corner {name}"
432 );
433 }
434
435 assert!(
436 (polygon_area_xy(quad) - expected_area).abs() < 1e-9,
437 "merged quad area {} should equal the summed triangle area {expected_area}",
438 polygon_area_xy(quad)
439 );
440 }
441
442 #[test]
443 fn cube_union_reduces_polygon_count() {
444 let a = crate::cuboid(10.0, 10.0, 10.0);
447 let b = crate::cuboid(10.0, 10.0, 10.0).translate(nalgebra::Vector3::new(5.0, 0.0, 0.0));
448 let raw_total = a.polygons().len() + b.polygons().len();
449 let result = a.union(b).cleanup();
450 let result_count = result.polygons().len();
451 assert!(
454 result_count < raw_total,
455 "union+cleanup should reduce polygon count: {result_count} should be < {raw_total}"
456 );
457 }
458}