1#![allow(dead_code)]
19
20type V3 = [f64; 3];
26
27const GEO_EPS: f64 = 1e-10;
29
30const COPLANAR_EPS: f64 = 1e-8;
32
33const WELD_EPS: f64 = 1e-9;
35
36#[inline]
37fn sub(a: V3, b: V3) -> V3 {
38 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
39}
40
41#[inline]
42fn add(a: V3, b: V3) -> V3 {
43 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
44}
45
46#[inline]
47fn scale(a: V3, s: f64) -> V3 {
48 [a[0] * s, a[1] * s, a[2] * s]
49}
50
51#[inline]
52fn dot(a: V3, b: V3) -> f64 {
53 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
54}
55
56#[inline]
57fn cross(a: V3, b: V3) -> V3 {
58 [
59 a[1] * b[2] - a[2] * b[1],
60 a[2] * b[0] - a[0] * b[2],
61 a[0] * b[1] - a[1] * b[0],
62 ]
63}
64
65#[inline]
66fn length(a: V3) -> f64 {
67 dot(a, a).sqrt()
68}
69
70#[inline]
71fn normalize(a: V3) -> V3 {
72 let l = length(a);
73 if l < GEO_EPS {
74 [0.0, 0.0, 0.0]
75 } else {
76 [a[0] / l, a[1] / l, a[2] / l]
77 }
78}
79
80#[inline]
81fn lerp(a: V3, b: V3, t: f64) -> V3 {
82 [
83 a[0] + t * (b[0] - a[0]),
84 a[1] + t * (b[1] - a[1]),
85 a[2] + t * (b[2] - a[2]),
86 ]
87}
88
89#[inline]
90fn dist_sq(a: V3, b: V3) -> f64 {
91 let d = sub(a, b);
92 dot(d, d)
93}
94
95#[inline]
97fn triangle_area(a: V3, b: V3, c: V3) -> f64 {
98 0.5 * length(cross(sub(b, a), sub(c, a)))
99}
100
101#[inline]
103fn triangle_normal(a: V3, b: V3, c: V3) -> V3 {
104 cross(sub(b, a), sub(c, a))
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum MeshBooleanOp {
114 Union,
116 Intersection,
118 Difference,
120}
121
122#[derive(Debug, Clone)]
128pub struct SimpleMesh {
129 pub vertices: Vec<V3>,
131 pub triangles: Vec<[usize; 3]>,
133}
134
135impl SimpleMesh {
136 pub fn new() -> Self {
138 Self {
139 vertices: Vec::new(),
140 triangles: Vec::new(),
141 }
142 }
143
144 pub fn from_data(vertices: Vec<V3>, triangles: Vec<[usize; 3]>) -> Self {
146 Self {
147 vertices,
148 triangles,
149 }
150 }
151
152 pub fn n_triangles(&self) -> usize {
154 self.triangles.len()
155 }
156
157 pub fn n_vertices(&self) -> usize {
159 self.vertices.len()
160 }
161
162 pub fn triangle_verts(&self, tri_idx: usize) -> (V3, V3, V3) {
164 let t = self.triangles[tri_idx];
165 (
166 self.vertices[t[0]],
167 self.vertices[t[1]],
168 self.vertices[t[2]],
169 )
170 }
171
172 pub fn aabb(&self) -> (V3, V3) {
174 if self.vertices.is_empty() {
175 return ([0.0; 3], [0.0; 3]);
176 }
177 let mut mn = self.vertices[0];
178 let mut mx = self.vertices[0];
179 for v in &self.vertices {
180 for d in 0..3 {
181 if v[d] < mn[d] {
182 mn[d] = v[d];
183 }
184 if v[d] > mx[d] {
185 mx[d] = v[d];
186 }
187 }
188 }
189 (mn, mx)
190 }
191
192 pub fn add_vertex(&mut self, v: V3) -> usize {
194 let idx = self.vertices.len();
195 self.vertices.push(v);
196 idx
197 }
198
199 pub fn add_triangle(&mut self, tri: [usize; 3]) {
201 self.triangles.push(tri);
202 }
203
204 pub fn surface_area(&self) -> f64 {
206 let mut area = 0.0;
207 for &t in &self.triangles {
208 area += triangle_area(
209 self.vertices[t[0]],
210 self.vertices[t[1]],
211 self.vertices[t[2]],
212 );
213 }
214 area
215 }
216
217 pub fn signed_volume(&self) -> f64 {
219 let mut vol = 0.0;
220 for &t in &self.triangles {
221 let a = self.vertices[t[0]];
222 let b = self.vertices[t[1]];
223 let c = self.vertices[t[2]];
224 vol += dot(a, cross(b, c));
225 }
226 vol / 6.0
227 }
228
229 pub fn flip_normals(&mut self) {
231 for t in &mut self.triangles {
232 t.swap(0, 1);
233 }
234 }
235
236 pub fn unit_cube(centre: V3, half_extent: f64) -> Self {
238 let h = half_extent;
239 let c = centre;
240 let vertices = vec![
241 [c[0] - h, c[1] - h, c[2] - h], [c[0] + h, c[1] - h, c[2] - h], [c[0] + h, c[1] + h, c[2] - h], [c[0] - h, c[1] + h, c[2] - h], [c[0] - h, c[1] - h, c[2] + h], [c[0] + h, c[1] - h, c[2] + h], [c[0] + h, c[1] + h, c[2] + h], [c[0] - h, c[1] + h, c[2] + h], ];
250 let triangles = vec![
251 [0, 2, 1],
253 [0, 3, 2],
254 [4, 5, 6],
256 [4, 6, 7],
257 [0, 1, 5],
259 [0, 5, 4],
260 [2, 3, 7],
262 [2, 7, 6],
263 [0, 4, 7],
265 [0, 7, 3],
266 [1, 2, 6],
268 [1, 6, 5],
269 ];
270 Self {
271 vertices,
272 triangles,
273 }
274 }
275
276 pub fn tetrahedron(centre: V3, _size: f64) -> Self {
278 let s = _size;
279 let vertices = vec![
280 [centre[0] + s, centre[1] + s, centre[2] + s],
281 [centre[0] + s, centre[1] - s, centre[2] - s],
282 [centre[0] - s, centre[1] + s, centre[2] - s],
283 [centre[0] - s, centre[1] - s, centre[2] + s],
284 ];
285 let triangles = vec![[0, 1, 2], [0, 3, 1], [0, 2, 3], [1, 3, 2]];
286 Self {
287 vertices,
288 triangles,
289 }
290 }
291}
292
293impl Default for SimpleMesh {
294 fn default() -> Self {
295 Self::new()
296 }
297}
298
299#[derive(Debug, Clone)]
305pub enum TriTriResult {
306 None,
308 Segment(V3, V3),
310 Coplanar,
312 Point(V3),
314}
315
316pub fn triangle_triangle_intersection(
320 a0: V3,
321 a1: V3,
322 a2: V3,
323 b0: V3,
324 b1: V3,
325 b2: V3,
326) -> TriTriResult {
327 let na = triangle_normal(a0, a1, a2);
329 let da = dot(na, a0);
330 let db0 = dot(na, b0) - da;
331 let db1 = dot(na, b1) - da;
332 let db2 = dot(na, b2) - da;
333
334 let db0 = if db0.abs() < GEO_EPS { 0.0 } else { db0 };
336 let db1 = if db1.abs() < GEO_EPS { 0.0 } else { db1 };
337 let db2 = if db2.abs() < GEO_EPS { 0.0 } else { db2 };
338
339 if db0 > 0.0 && db1 > 0.0 && db2 > 0.0 {
341 return TriTriResult::None;
342 }
343 if db0 < 0.0 && db1 < 0.0 && db2 < 0.0 {
344 return TriTriResult::None;
345 }
346
347 if db0.abs() < GEO_EPS && db1.abs() < GEO_EPS && db2.abs() < GEO_EPS {
349 if coplanar_triangles_overlap(a0, a1, a2, b0, b1, b2, na) {
350 return TriTriResult::Coplanar;
351 }
352 return TriTriResult::None;
353 }
354
355 let nb = triangle_normal(b0, b1, b2);
357 let _db_val = dot(nb, b0);
358 let da0 = dot(nb, a0) - _db_val;
359 let da1 = dot(nb, a1) - _db_val;
360 let da2 = dot(nb, a2) - _db_val;
361
362 let da0 = if da0.abs() < GEO_EPS { 0.0 } else { da0 };
363 let da1 = if da1.abs() < GEO_EPS { 0.0 } else { da1 };
364 let da2 = if da2.abs() < GEO_EPS { 0.0 } else { da2 };
365
366 if da0 > 0.0 && da1 > 0.0 && da2 > 0.0 {
367 return TriTriResult::None;
368 }
369 if da0 < 0.0 && da1 < 0.0 && da2 < 0.0 {
370 return TriTriResult::None;
371 }
372
373 let dir = cross(na, nb);
375 let dir_len = length(dir);
376 if dir_len < GEO_EPS {
377 return TriTriResult::None;
378 }
379 let dir = normalize(dir);
380
381 let seg_a = compute_interval_on_line(a0, a1, a2, da0, da1, da2, dir);
383 let seg_b = compute_interval_on_line(b0, b1, b2, db0, db1, db2, dir);
384
385 if let (Some((ta_min, ta_max, pa_min, pa_max)), Some((tb_min, tb_max, pb_min, pb_max))) =
386 (seg_a, seg_b)
387 {
388 let t0 = ta_min.max(tb_min);
390 let t1 = ta_max.min(tb_max);
391 if t0 > t1 + GEO_EPS {
392 return TriTriResult::None;
393 }
394 let p0 = if t0 >= ta_min - GEO_EPS && t0 <= ta_max + GEO_EPS {
396 interp_seg_param(pa_min, pa_max, ta_min, ta_max, t0)
397 } else {
398 interp_seg_param(pb_min, pb_max, tb_min, tb_max, t0)
399 };
400 let p1 = if t1 >= ta_min - GEO_EPS && t1 <= ta_max + GEO_EPS {
401 interp_seg_param(pa_min, pa_max, ta_min, ta_max, t1)
402 } else {
403 interp_seg_param(pb_min, pb_max, tb_min, tb_max, t1)
404 };
405 if dist_sq(p0, p1) < GEO_EPS * GEO_EPS {
406 return TriTriResult::Point(p0);
407 }
408 TriTriResult::Segment(p0, p1)
409 } else {
410 TriTriResult::None
411 }
412}
413
414fn compute_interval_on_line(
418 v0: V3,
419 v1: V3,
420 v2: V3,
421 d0: f64,
422 d1: f64,
423 d2: f64,
424 dir: V3,
425) -> Option<(f64, f64, V3, V3)> {
426 let verts = [v0, v1, v2];
427 let dists = [d0, d1, d2];
428
429 let mut points: Vec<(f64, V3)> = Vec::new();
431
432 for i in 0..3 {
434 if dists[i].abs() < GEO_EPS {
435 let t = dot(verts[i], dir);
436 points.push((t, verts[i]));
437 }
438 }
439
440 for (i, j) in [(0, 1), (1, 2), (2, 0)] {
442 if (dists[i] > GEO_EPS && dists[j] < -GEO_EPS)
443 || (dists[i] < -GEO_EPS && dists[j] > GEO_EPS)
444 {
445 let s = dists[i] / (dists[i] - dists[j]);
446 let p = lerp(verts[i], verts[j], s);
447 let t = dot(p, dir);
448 points.push((t, p));
449 }
450 }
451
452 if points.is_empty() {
453 return None;
454 }
455
456 let mut min_idx = 0;
458 let mut max_idx = 0;
459 for (k, (t, _)) in points.iter().enumerate() {
460 if *t < points[min_idx].0 {
461 min_idx = k;
462 }
463 if *t > points[max_idx].0 {
464 max_idx = k;
465 }
466 }
467
468 Some((
469 points[min_idx].0,
470 points[max_idx].0,
471 points[min_idx].1,
472 points[max_idx].1,
473 ))
474}
475
476fn interp_seg_param(p_min: V3, p_max: V3, t_min: f64, t_max: f64, t: f64) -> V3 {
478 if (t_max - t_min).abs() < GEO_EPS {
479 return p_min;
480 }
481 let s = (t - t_min) / (t_max - t_min);
482 lerp(p_min, p_max, s.clamp(0.0, 1.0))
483}
484
485fn coplanar_triangles_overlap(a0: V3, a1: V3, a2: V3, b0: V3, b1: V3, b2: V3, normal: V3) -> bool {
492 let abs_n = [normal[0].abs(), normal[1].abs(), normal[2].abs()];
494 let (ax1, ax2) = if abs_n[0] >= abs_n[1] && abs_n[0] >= abs_n[2] {
495 (1, 2)
496 } else if abs_n[1] >= abs_n[2] {
497 (0, 2)
498 } else {
499 (0, 1)
500 };
501
502 let proj = |v: V3| -> [f64; 2] { [v[ax1], v[ax2]] };
503
504 let pa = [proj(a0), proj(a1), proj(a2)];
505 let pb = [proj(b0), proj(b1), proj(b2)];
506
507 for i in 0..3 {
509 let j = (i + 1) % 3;
510 for k in 0..3 {
511 let l = (k + 1) % 3;
512 if segments_intersect_2d(pa[i], pa[j], pb[k], pb[l]) {
513 return true;
514 }
515 }
516 }
517
518 if point_in_triangle_2d(pa[0], pb[0], pb[1], pb[2]) {
520 return true;
521 }
522 if point_in_triangle_2d(pb[0], pa[0], pa[1], pa[2]) {
523 return true;
524 }
525
526 false
527}
528
529fn segments_intersect_2d(a: [f64; 2], b: [f64; 2], c: [f64; 2], d: [f64; 2]) -> bool {
531 let ab = [b[0] - a[0], b[1] - a[1]];
532 let cd = [d[0] - c[0], d[1] - c[1]];
533 let denom = ab[0] * cd[1] - ab[1] * cd[0];
534 if denom.abs() < GEO_EPS {
535 return false; }
537 let ac = [c[0] - a[0], c[1] - a[1]];
538 let t = (ac[0] * cd[1] - ac[1] * cd[0]) / denom;
539 let u = (ac[0] * ab[1] - ac[1] * ab[0]) / denom;
540 (-GEO_EPS..=1.0 + GEO_EPS).contains(&t) && (-GEO_EPS..=1.0 + GEO_EPS).contains(&u)
541}
542
543fn point_in_triangle_2d(p: [f64; 2], a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> bool {
545 let v0 = [c[0] - a[0], c[1] - a[1]];
546 let v1 = [b[0] - a[0], b[1] - a[1]];
547 let v2 = [p[0] - a[0], p[1] - a[1]];
548 let d00 = v0[0] * v0[0] + v0[1] * v0[1];
549 let d01 = v0[0] * v1[0] + v0[1] * v1[1];
550 let d02 = v0[0] * v2[0] + v0[1] * v2[1];
551 let d11 = v1[0] * v1[0] + v1[1] * v1[1];
552 let d12 = v1[0] * v2[0] + v1[1] * v2[1];
553 let inv_denom = d00 * d11 - d01 * d01;
554 if inv_denom.abs() < GEO_EPS {
555 return false;
556 }
557 let inv = 1.0 / inv_denom;
558 let u = (d11 * d02 - d01 * d12) * inv;
559 let v = (d00 * d12 - d01 * d02) * inv;
560 u >= -GEO_EPS && v >= -GEO_EPS && (u + v) <= 1.0 + GEO_EPS
561}
562
563pub fn winding_number(mesh: &SimpleMesh, p: V3) -> f64 {
571 let mut wn = 0.0;
572 for &tri in &mesh.triangles {
573 let a = sub(mesh.vertices[tri[0]], p);
574 let b = sub(mesh.vertices[tri[1]], p);
575 let c = sub(mesh.vertices[tri[2]], p);
576 let la = length(a);
577 let lb = length(b);
578 let lc = length(c);
579 if la < GEO_EPS || lb < GEO_EPS || lc < GEO_EPS {
580 continue;
581 }
582 let num = dot(a, cross(b, c));
583 let denom = la * lb * lc + dot(a, b) * lc + dot(b, c) * la + dot(c, a) * lb;
584 wn += num.atan2(denom);
585 }
586 wn / (2.0 * std::f64::consts::PI)
587}
588
589pub fn is_inside(mesh: &SimpleMesh, p: V3) -> bool {
591 winding_number(mesh, p).abs() > 0.5
592}
593
594#[derive(Debug, Clone)]
600pub struct IntersectionSegment {
601 pub start: V3,
603 pub end: V3,
605 pub tri_a: usize,
607 pub tri_b: usize,
609}
610
611pub fn extract_intersection_contours(
613 mesh_a: &SimpleMesh,
614 mesh_b: &SimpleMesh,
615) -> Vec<IntersectionSegment> {
616 let mut segments = Vec::new();
617 for i in 0..mesh_a.n_triangles() {
618 let (a0, a1, a2) = mesh_a.triangle_verts(i);
619 for j in 0..mesh_b.n_triangles() {
620 let (b0, b1, b2) = mesh_b.triangle_verts(j);
621 match triangle_triangle_intersection(a0, a1, a2, b0, b1, b2) {
622 TriTriResult::Segment(p0, p1) => {
623 segments.push(IntersectionSegment {
624 start: p0,
625 end: p1,
626 tri_a: i,
627 tri_b: j,
628 });
629 }
630 TriTriResult::Point(p) => {
631 segments.push(IntersectionSegment {
632 start: p,
633 end: p,
634 tri_a: i,
635 tri_b: j,
636 });
637 }
638 _ => {}
639 }
640 }
641 }
642 segments
643}
644
645pub fn split_triangle_by_plane(
652 v0: V3,
653 v1: V3,
654 v2: V3,
655 plane_normal: V3,
656 plane_point: V3,
657) -> (Vec<[V3; 3]>, Vec<[V3; 3]>) {
658 let verts = [v0, v1, v2];
659 let dists: Vec<f64> = verts
660 .iter()
661 .map(|v| dot(sub(*v, plane_point), plane_normal))
662 .collect();
663
664 let pos_count = dists.iter().filter(|&&d| d > GEO_EPS).count();
665 let neg_count = dists.iter().filter(|&&d| d < -GEO_EPS).count();
666
667 if neg_count == 0 {
669 return (vec![[v0, v1, v2]], Vec::new());
670 }
671 if pos_count == 0 {
672 return (Vec::new(), vec![[v0, v1, v2]]);
673 }
674
675 let mut front = Vec::new();
677 let mut back = Vec::new();
678
679 for start in 0..3 {
681 let i0 = start;
682 let i1 = (start + 1) % 3;
683 let i2 = (start + 2) % 3;
684
685 if (dists[i0] > GEO_EPS && dists[i1] < -GEO_EPS && dists[i2] < -GEO_EPS)
686 || (dists[i0] > GEO_EPS
687 && dists[i1] <= GEO_EPS
688 && dists[i2] < -GEO_EPS
689 && dists[i1].abs() <= GEO_EPS)
690 {
691 let t01 = dists[i0] / (dists[i0] - dists[i1]);
693 let t02 = dists[i0] / (dists[i0] - dists[i2]);
694 let p01 = lerp(verts[i0], verts[i1], t01);
695 let p02 = lerp(verts[i0], verts[i2], t02);
696 front.push([verts[i0], p01, p02]);
697 back.push([p01, verts[i1], verts[i2]]);
698 back.push([p01, verts[i2], p02]);
699 return (front, back);
700 }
701 if (dists[i0] < -GEO_EPS && dists[i1] > GEO_EPS && dists[i2] > GEO_EPS)
702 || (dists[i0] < -GEO_EPS
703 && dists[i1] >= -GEO_EPS
704 && dists[i2] > GEO_EPS
705 && dists[i1].abs() <= GEO_EPS)
706 {
707 let t01 = dists[i0] / (dists[i0] - dists[i1]);
709 let t02 = dists[i0] / (dists[i0] - dists[i2]);
710 let p01 = lerp(verts[i0], verts[i1], t01);
711 let p02 = lerp(verts[i0], verts[i2], t02);
712 back.push([verts[i0], p01, p02]);
713 front.push([p01, verts[i1], verts[i2]]);
714 front.push([p01, verts[i2], p02]);
715 return (front, back);
716 }
717 }
718
719 (vec![[v0, v1, v2]], Vec::new())
721}
722
723pub fn weld_vertices(mesh: &SimpleMesh, tolerance: f64) -> SimpleMesh {
730 let tol_sq = tolerance * tolerance;
731 let mut new_verts: Vec<V3> = Vec::new();
732 let mut remap: Vec<usize> = Vec::new();
733
734 for v in &mesh.vertices {
735 let mut found = None;
736 for (k, nv) in new_verts.iter().enumerate() {
737 if dist_sq(*v, *nv) < tol_sq {
738 found = Some(k);
739 break;
740 }
741 }
742 if let Some(k) = found {
743 remap.push(k);
744 } else {
745 remap.push(new_verts.len());
746 new_verts.push(*v);
747 }
748 }
749
750 let new_tris: Vec<[usize; 3]> = mesh
751 .triangles
752 .iter()
753 .map(|t| [remap[t[0]], remap[t[1]], remap[t[2]]])
754 .collect();
755
756 SimpleMesh::from_data(new_verts, new_tris)
757}
758
759pub fn remove_degenerate_triangles(mesh: &mut SimpleMesh) {
761 mesh.triangles.retain(|t| {
762 if t[0] == t[1] || t[1] == t[2] || t[0] == t[2] {
763 return false;
764 }
765 let a = mesh.vertices[t[0]];
766 let b = mesh.vertices[t[1]];
767 let c = mesh.vertices[t[2]];
768 triangle_area(a, b, c) > GEO_EPS
769 });
770}
771
772pub fn remove_unused_vertices(mesh: &mut SimpleMesh) {
774 let n = mesh.vertices.len();
775 let mut used = vec![false; n];
776 for t in &mesh.triangles {
777 used[t[0]] = true;
778 used[t[1]] = true;
779 used[t[2]] = true;
780 }
781 let mut remap = vec![0usize; n];
782 let mut new_verts = Vec::new();
783 for (i, &u) in used.iter().enumerate() {
784 if u {
785 remap[i] = new_verts.len();
786 new_verts.push(mesh.vertices[i]);
787 }
788 }
789 for t in &mut mesh.triangles {
790 t[0] = remap[t[0]];
791 t[1] = remap[t[1]];
792 t[2] = remap[t[2]];
793 }
794 mesh.vertices = new_verts;
795}
796
797pub fn cleanup_mesh(mesh: &SimpleMesh) -> SimpleMesh {
799 let mut result = weld_vertices(mesh, WELD_EPS);
800 remove_degenerate_triangles(&mut result);
801 remove_unused_vertices(&mut result);
802 result
803}
804
805pub fn stitch_meshes(a: &SimpleMesh, b: &SimpleMesh) -> SimpleMesh {
811 let offset = a.vertices.len();
812 let mut vertices = a.vertices.clone();
813 vertices.extend_from_slice(&b.vertices);
814 let mut triangles = a.triangles.clone();
815 for t in &b.triangles {
816 triangles.push([t[0] + offset, t[1] + offset, t[2] + offset]);
817 }
818 SimpleMesh {
819 vertices,
820 triangles,
821 }
822}
823
824pub fn classify_triangles(mesh: &SimpleMesh, other: &SimpleMesh) -> Vec<bool> {
831 let mut result = Vec::with_capacity(mesh.n_triangles());
832 for i in 0..mesh.n_triangles() {
833 let (a, b, c) = mesh.triangle_verts(i);
834 let centroid = scale(add(add(a, b), c), 1.0 / 3.0);
835 result.push(is_inside(other, centroid));
836 }
837 result
838}
839
840pub fn mesh_boolean(mesh_a: &SimpleMesh, mesh_b: &SimpleMesh, op: MeshBooleanOp) -> SimpleMesh {
846 let a_inside_b = classify_triangles(mesh_a, mesh_b);
847 let b_inside_a = classify_triangles(mesh_b, mesh_a);
848
849 let mut result = SimpleMesh::new();
850
851 match op {
852 MeshBooleanOp::Union => {
853 collect_triangles(mesh_a, &a_inside_b, false, &mut result);
855 collect_triangles(mesh_b, &b_inside_a, false, &mut result);
857 }
858 MeshBooleanOp::Intersection => {
859 collect_triangles(mesh_a, &a_inside_b, true, &mut result);
861 collect_triangles(mesh_b, &b_inside_a, true, &mut result);
863 }
864 MeshBooleanOp::Difference => {
865 collect_triangles(mesh_a, &a_inside_b, false, &mut result);
867 collect_triangles_flipped(mesh_b, &b_inside_a, true, &mut result);
869 }
870 }
871
872 cleanup_mesh(&result)
873}
874
875fn collect_triangles(
877 mesh: &SimpleMesh,
878 classification: &[bool],
879 keep_inside: bool,
880 result: &mut SimpleMesh,
881) {
882 let offset = result.vertices.len();
883 result.vertices.extend_from_slice(&mesh.vertices);
884 for (i, &is_inside_val) in classification.iter().enumerate() {
885 if is_inside_val == keep_inside {
886 let t = mesh.triangles[i];
887 result
888 .triangles
889 .push([t[0] + offset, t[1] + offset, t[2] + offset]);
890 }
891 }
892}
893
894fn collect_triangles_flipped(
896 mesh: &SimpleMesh,
897 classification: &[bool],
898 keep_inside: bool,
899 result: &mut SimpleMesh,
900) {
901 let offset = result.vertices.len();
902 result.vertices.extend_from_slice(&mesh.vertices);
903 for (i, &is_inside_val) in classification.iter().enumerate() {
904 if is_inside_val == keep_inside {
905 let t = mesh.triangles[i];
906 result
908 .triangles
909 .push([t[1] + offset, t[0] + offset, t[2] + offset]);
910 }
911 }
912}
913
914pub fn ray_triangle_intersect(origin: V3, dir: V3, v0: V3, v1: V3, v2: V3) -> Option<f64> {
921 let e1 = sub(v1, v0);
922 let e2 = sub(v2, v0);
923 let h = cross(dir, e2);
924 let a = dot(e1, h);
925 if a.abs() < GEO_EPS {
926 return None;
927 }
928 let f = 1.0 / a;
929 let s = sub(origin, v0);
930 let u = f * dot(s, h);
931 if !(0.0..=1.0).contains(&u) {
932 return None;
933 }
934 let q = cross(s, e1);
935 let v = f * dot(dir, q);
936 if v < 0.0 || u + v > 1.0 {
937 return None;
938 }
939 let t = f * dot(e2, q);
940 if t > GEO_EPS { Some(t) } else { None }
941}
942
943pub fn ray_mesh_intersection_count(mesh: &SimpleMesh, origin: V3, dir: V3) -> usize {
945 let mut count = 0;
946 for &tri in &mesh.triangles {
947 if ray_triangle_intersect(
948 origin,
949 dir,
950 mesh.vertices[tri[0]],
951 mesh.vertices[tri[1]],
952 mesh.vertices[tri[2]],
953 )
954 .is_some()
955 {
956 count += 1;
957 }
958 }
959 count
960}
961
962pub fn is_inside_ray(mesh: &SimpleMesh, p: V3) -> bool {
964 let dir = [1.0, 0.0, 0.0];
965 ray_mesh_intersection_count(mesh, p, dir) % 2 == 1
966}
967
968#[derive(Debug, Clone)]
974pub struct MeshStats {
975 pub n_vertices: usize,
977 pub n_triangles: usize,
979 pub surface_area: f64,
981 pub signed_volume: f64,
983 pub min_triangle_area: f64,
985 pub max_triangle_area: f64,
987}
988
989pub fn compute_mesh_stats(mesh: &SimpleMesh) -> MeshStats {
991 let mut min_area = f64::INFINITY;
992 let mut max_area = 0.0_f64;
993 let mut total_area = 0.0;
994 for i in 0..mesh.n_triangles() {
995 let (a, b, c) = mesh.triangle_verts(i);
996 let area = triangle_area(a, b, c);
997 total_area += area;
998 if area < min_area {
999 min_area = area;
1000 }
1001 if area > max_area {
1002 max_area = area;
1003 }
1004 }
1005 MeshStats {
1006 n_vertices: mesh.n_vertices(),
1007 n_triangles: mesh.n_triangles(),
1008 surface_area: total_area,
1009 signed_volume: mesh.signed_volume(),
1010 min_triangle_area: min_area,
1011 max_triangle_area: max_area,
1012 }
1013}
1014
1015pub fn is_watertight(mesh: &SimpleMesh) -> bool {
1017 use std::collections::HashMap;
1018 let mut edge_count: HashMap<(usize, usize), u32> = HashMap::new();
1019 for t in &mesh.triangles {
1020 for (&a, &b) in t.iter().zip(t.iter().cycle().skip(1).take(3)) {
1021 let key = if a < b { (a, b) } else { (b, a) };
1022 *edge_count.entry(key).or_insert(0) += 1;
1023 }
1024 }
1025 edge_count.values().all(|&c| c == 2)
1026}
1027
1028pub fn euler_characteristic(mesh: &SimpleMesh) -> i64 {
1030 use std::collections::HashSet;
1031 let v = mesh.n_vertices() as i64;
1032 let f = mesh.n_triangles() as i64;
1033 let mut edges = HashSet::new();
1034 for t in &mesh.triangles {
1035 for (&a, &b) in t.iter().zip(t.iter().cycle().skip(1).take(3)) {
1036 let key = if a < b { (a, b) } else { (b, a) };
1037 edges.insert(key);
1038 }
1039 }
1040 let e = edges.len() as i64;
1041 v - e + f
1042}
1043
1044#[cfg(test)]
1049mod tests {
1050 use super::*;
1051
1052 #[test]
1055 fn test_unit_cube_creation() {
1056 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1057 assert_eq!(cube.n_vertices(), 8);
1058 assert_eq!(cube.n_triangles(), 12);
1059 }
1060
1061 #[test]
1062 fn test_cube_surface_area() {
1063 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1064 let area = cube.surface_area();
1065 assert!((area - 24.0).abs() < 1e-8, "area = {area}");
1067 }
1068
1069 #[test]
1070 fn test_cube_signed_volume() {
1071 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1072 let vol = cube.signed_volume();
1073 assert!((vol.abs() - 8.0).abs() < 1e-8, "vol = {vol}");
1075 }
1076
1077 #[test]
1078 fn test_tetrahedron_creation() {
1079 let tet = SimpleMesh::tetrahedron([0.0; 3], 1.0);
1080 assert_eq!(tet.n_vertices(), 4);
1081 assert_eq!(tet.n_triangles(), 4);
1082 }
1083
1084 #[test]
1085 fn test_empty_mesh() {
1086 let m = SimpleMesh::new();
1087 assert_eq!(m.n_vertices(), 0);
1088 assert_eq!(m.n_triangles(), 0);
1089 let (mn, mx) = m.aabb();
1090 assert_eq!(mn, [0.0; 3]);
1091 assert_eq!(mx, [0.0; 3]);
1092 }
1093
1094 #[test]
1095 fn test_add_vertex_and_triangle() {
1096 let mut m = SimpleMesh::new();
1097 let a = m.add_vertex([0.0, 0.0, 0.0]);
1098 let b = m.add_vertex([1.0, 0.0, 0.0]);
1099 let c = m.add_vertex([0.0, 1.0, 0.0]);
1100 m.add_triangle([a, b, c]);
1101 assert_eq!(m.n_triangles(), 1);
1102 let area = m.surface_area();
1103 assert!((area - 0.5).abs() < 1e-10);
1104 }
1105
1106 #[test]
1109 fn test_tri_tri_no_intersection() {
1110 let a0 = [0.0, 0.0, 0.0];
1111 let a1 = [1.0, 0.0, 0.0];
1112 let a2 = [0.0, 1.0, 0.0];
1113 let b0 = [5.0, 5.0, 5.0];
1114 let b1 = [6.0, 5.0, 5.0];
1115 let b2 = [5.0, 6.0, 5.0];
1116 match triangle_triangle_intersection(a0, a1, a2, b0, b1, b2) {
1117 TriTriResult::None => {}
1118 other => panic!("expected None, got {other:?}"),
1119 }
1120 }
1121
1122 #[test]
1123 fn test_tri_tri_segment_intersection() {
1124 let a0 = [-1.0, 0.0, 0.0];
1126 let a1 = [1.0, 0.0, 0.0];
1127 let a2 = [0.0, 0.0, 1.0];
1128 let b0 = [0.0, -1.0, 0.25];
1129 let b1 = [0.0, 1.0, 0.25];
1130 let b2 = [0.0, 0.0, 0.75];
1131 match triangle_triangle_intersection(a0, a1, a2, b0, b1, b2) {
1132 TriTriResult::Segment(p0, p1) => {
1133 assert!(dist_sq(p0, p1) > GEO_EPS, "segment too short");
1134 }
1135 other => panic!("expected Segment, got {other:?}"),
1136 }
1137 }
1138
1139 #[test]
1140 fn test_tri_tri_coplanar_overlap() {
1141 let a0 = [0.0, 0.0, 0.0];
1142 let a1 = [2.0, 0.0, 0.0];
1143 let a2 = [0.0, 2.0, 0.0];
1144 let b0 = [1.0, 0.0, 0.0];
1145 let b1 = [3.0, 0.0, 0.0];
1146 let b2 = [1.0, 2.0, 0.0];
1147 match triangle_triangle_intersection(a0, a1, a2, b0, b1, b2) {
1148 TriTriResult::Coplanar => {}
1149 other => panic!("expected Coplanar, got {other:?}"),
1150 }
1151 }
1152
1153 #[test]
1154 fn test_tri_tri_coplanar_no_overlap() {
1155 let a0 = [0.0, 0.0, 0.0];
1156 let a1 = [1.0, 0.0, 0.0];
1157 let a2 = [0.0, 1.0, 0.0];
1158 let b0 = [5.0, 5.0, 0.0];
1159 let b1 = [6.0, 5.0, 0.0];
1160 let b2 = [5.0, 6.0, 0.0];
1161 match triangle_triangle_intersection(a0, a1, a2, b0, b1, b2) {
1162 TriTriResult::None => {}
1163 other => panic!("expected None for separated coplanar, got {other:?}"),
1164 }
1165 }
1166
1167 #[test]
1170 fn test_winding_number_inside_cube() {
1171 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1172 let wn = winding_number(&cube, [0.0, 0.0, 0.0]);
1173 assert!(wn.abs() > 0.4, "wn = {wn}, expected ~1");
1174 }
1175
1176 #[test]
1177 fn test_winding_number_outside_cube() {
1178 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1179 let wn = winding_number(&cube, [5.0, 5.0, 5.0]);
1180 assert!(wn.abs() < 0.1, "wn = {wn}, expected ~0");
1181 }
1182
1183 #[test]
1184 fn test_is_inside_cube() {
1185 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1186 assert!(is_inside(&cube, [0.0, 0.0, 0.0]));
1187 assert!(!is_inside(&cube, [5.0, 5.0, 5.0]));
1188 }
1189
1190 #[test]
1193 fn test_ray_triangle_hit() {
1194 let v0 = [-1.0, -1.0, 1.0];
1195 let v1 = [1.0, -1.0, 1.0];
1196 let v2 = [0.0, 1.0, 1.0];
1197 let origin = [0.0, 0.0, 0.0];
1198 let dir = [0.0, 0.0, 1.0];
1199 let t = ray_triangle_intersect(origin, dir, v0, v1, v2);
1200 assert!(t.is_some());
1201 assert!((t.unwrap() - 1.0).abs() < 1e-8);
1202 }
1203
1204 #[test]
1205 fn test_ray_triangle_miss() {
1206 let v0 = [-1.0, -1.0, 1.0];
1207 let v1 = [1.0, -1.0, 1.0];
1208 let v2 = [0.0, 1.0, 1.0];
1209 let origin = [10.0, 10.0, 0.0];
1210 let dir = [0.0, 0.0, 1.0];
1211 assert!(ray_triangle_intersect(origin, dir, v0, v1, v2).is_none());
1212 }
1213
1214 #[test]
1215 fn test_ray_mesh_count() {
1216 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1217 let count = ray_mesh_intersection_count(&cube, [-5.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1218 assert_eq!(count % 2, 0, "count = {count}");
1220 }
1221
1222 #[test]
1225 fn test_contour_overlapping_cubes() {
1226 let a = SimpleMesh::unit_cube([0.0; 3], 1.0);
1227 let b = SimpleMesh::unit_cube([1.0, 0.0, 0.0], 1.0);
1228 let segs = extract_intersection_contours(&a, &b);
1229 assert!(!segs.is_empty(), "expected intersection segments");
1231 }
1232
1233 #[test]
1234 fn test_contour_separated_cubes() {
1235 let a = SimpleMesh::unit_cube([0.0; 3], 1.0);
1236 let b = SimpleMesh::unit_cube([10.0, 0.0, 0.0], 1.0);
1237 let segs = extract_intersection_contours(&a, &b);
1238 assert!(
1239 segs.is_empty(),
1240 "expected no intersections, got {}",
1241 segs.len()
1242 );
1243 }
1244
1245 #[test]
1248 fn test_split_triangle_all_front() {
1249 let (front, back) = split_triangle_by_plane(
1250 [0.0, 0.0, 1.0],
1251 [1.0, 0.0, 1.0],
1252 [0.0, 1.0, 1.0],
1253 [0.0, 0.0, 1.0], [0.0, 0.0, 0.0], );
1256 assert_eq!(front.len(), 1);
1257 assert!(back.is_empty());
1258 }
1259
1260 #[test]
1261 fn test_split_triangle_straddles_plane() {
1262 let (front, back) = split_triangle_by_plane(
1263 [0.0, 0.0, 1.0],
1264 [1.0, 0.0, -1.0],
1265 [0.0, 1.0, -1.0],
1266 [0.0, 0.0, 1.0],
1267 [0.0, 0.0, 0.0],
1268 );
1269 assert!(!front.is_empty(), "should have front triangles");
1270 assert!(!back.is_empty(), "should have back triangles");
1271 }
1272
1273 #[test]
1276 fn test_weld_vertices_merges_close() {
1277 let mut m = SimpleMesh::new();
1278 m.add_vertex([0.0, 0.0, 0.0]);
1279 m.add_vertex([1e-12, 0.0, 0.0]); m.add_vertex([1.0, 0.0, 0.0]);
1281 m.add_triangle([0, 2, 1]);
1282 let welded = weld_vertices(&m, 1e-8);
1283 assert_eq!(welded.n_vertices(), 2, "close vertices should merge");
1284 }
1285
1286 #[test]
1287 fn test_remove_degenerate() {
1288 let mut m = SimpleMesh::from_data(
1289 vec![[0.0; 3], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
1290 vec![[0, 0, 1], [0, 1, 2]], );
1292 remove_degenerate_triangles(&mut m);
1293 assert_eq!(m.n_triangles(), 1);
1294 }
1295
1296 #[test]
1297 fn test_cleanup_mesh() {
1298 let mut m = SimpleMesh::new();
1299 m.add_vertex([0.0, 0.0, 0.0]);
1300 m.add_vertex([1e-12, 0.0, 0.0]);
1301 m.add_vertex([1.0, 0.0, 0.0]);
1302 m.add_vertex([0.0, 1.0, 0.0]);
1303 m.add_vertex([99.0, 99.0, 99.0]); m.add_triangle([0, 2, 3]);
1305 let cleaned = cleanup_mesh(&m);
1306 assert!(cleaned.n_vertices() <= 3);
1307 assert_eq!(cleaned.n_triangles(), 1);
1308 }
1309
1310 #[test]
1313 fn test_stitch_meshes() {
1314 let a = SimpleMesh::unit_cube([0.0; 3], 1.0);
1315 let b = SimpleMesh::unit_cube([5.0, 0.0, 0.0], 1.0);
1316 let combined = stitch_meshes(&a, &b);
1317 assert_eq!(combined.n_vertices(), 16);
1318 assert_eq!(combined.n_triangles(), 24);
1319 }
1320
1321 #[test]
1324 fn test_boolean_union_separated() {
1325 let a = SimpleMesh::unit_cube([0.0; 3], 1.0);
1326 let b = SimpleMesh::unit_cube([10.0, 0.0, 0.0], 1.0);
1327 let result = mesh_boolean(&a, &b, MeshBooleanOp::Union);
1328 assert!(
1330 result.n_triangles() >= 20,
1331 "union tris = {}",
1332 result.n_triangles()
1333 );
1334 }
1335
1336 #[test]
1337 fn test_boolean_intersection_separated() {
1338 let a = SimpleMesh::unit_cube([0.0; 3], 1.0);
1339 let b = SimpleMesh::unit_cube([10.0, 0.0, 0.0], 1.0);
1340 let result = mesh_boolean(&a, &b, MeshBooleanOp::Intersection);
1341 assert_eq!(result.n_triangles(), 0);
1343 }
1344
1345 #[test]
1346 fn test_boolean_difference_separated() {
1347 let a = SimpleMesh::unit_cube([0.0; 3], 1.0);
1348 let b = SimpleMesh::unit_cube([10.0, 0.0, 0.0], 1.0);
1349 let result = mesh_boolean(&a, &b, MeshBooleanOp::Difference);
1350 assert!(
1352 result.n_triangles() >= 10,
1353 "diff tris = {}",
1354 result.n_triangles()
1355 );
1356 }
1357
1358 #[test]
1361 fn test_mesh_stats() {
1362 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1363 let stats = compute_mesh_stats(&cube);
1364 assert_eq!(stats.n_vertices, 8);
1365 assert_eq!(stats.n_triangles, 12);
1366 assert!((stats.surface_area - 24.0).abs() < 1e-8);
1367 }
1368
1369 #[test]
1370 fn test_euler_characteristic_cube() {
1371 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1372 let chi = euler_characteristic(&cube);
1373 assert_eq!(
1374 chi, 2,
1375 "Euler characteristic of a cube should be 2, got {chi}"
1376 );
1377 }
1378
1379 #[test]
1380 fn test_flip_normals() {
1381 let mut cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1382 let vol_before = cube.signed_volume();
1383 cube.flip_normals();
1384 let vol_after = cube.signed_volume();
1385 assert!(
1386 (vol_before + vol_after).abs() < 1e-8,
1387 "flipping should negate volume"
1388 );
1389 }
1390
1391 #[test]
1392 fn test_is_watertight_cube() {
1393 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1394 assert!(is_watertight(&cube), "unit cube should be watertight");
1395 }
1396
1397 #[test]
1398 fn test_classify_triangles_inside() {
1399 let big_cube = SimpleMesh::unit_cube([0.0; 3], 2.0);
1400 let small_cube = SimpleMesh::unit_cube([0.0; 3], 0.5);
1401 let classification = classify_triangles(&small_cube, &big_cube);
1402 let all_inside = classification.iter().all(|&x| x);
1404 assert!(all_inside, "small cube should be inside big cube");
1405 }
1406}