1type V3 = [f64; 3];
24
25const GEO_EPS: f64 = 1e-10;
27
28const WELD_EPS: f64 = 1e-9;
30
31#[inline]
32fn sub(a: V3, b: V3) -> V3 {
33 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
34}
35
36#[inline]
37fn add(a: V3, b: V3) -> V3 {
38 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
39}
40
41#[inline]
42fn scale(a: V3, s: f64) -> V3 {
43 [a[0] * s, a[1] * s, a[2] * s]
44}
45
46#[inline]
47fn dot(a: V3, b: V3) -> f64 {
48 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
49}
50
51#[inline]
52fn cross(a: V3, b: V3) -> V3 {
53 [
54 a[1] * b[2] - a[2] * b[1],
55 a[2] * b[0] - a[0] * b[2],
56 a[0] * b[1] - a[1] * b[0],
57 ]
58}
59
60#[inline]
61fn length(a: V3) -> f64 {
62 dot(a, a).sqrt()
63}
64
65#[inline]
66fn normalize(a: V3) -> V3 {
67 let l = length(a);
68 if l < GEO_EPS {
69 [0.0, 0.0, 0.0]
70 } else {
71 [a[0] / l, a[1] / l, a[2] / l]
72 }
73}
74
75#[inline]
76fn lerp(a: V3, b: V3, t: f64) -> V3 {
77 [
78 a[0] + t * (b[0] - a[0]),
79 a[1] + t * (b[1] - a[1]),
80 a[2] + t * (b[2] - a[2]),
81 ]
82}
83
84#[inline]
85fn dist_sq(a: V3, b: V3) -> f64 {
86 let d = sub(a, b);
87 dot(d, d)
88}
89
90#[inline]
92fn triangle_area(a: V3, b: V3, c: V3) -> f64 {
93 0.5 * length(cross(sub(b, a), sub(c, a)))
94}
95
96#[inline]
98fn triangle_normal(a: V3, b: V3, c: V3) -> V3 {
99 cross(sub(b, a), sub(c, a))
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum MeshBooleanOp {
109 Union,
111 Intersection,
113 Difference,
115}
116
117#[derive(Debug, Clone)]
123pub struct SimpleMesh {
124 pub vertices: Vec<V3>,
126 pub triangles: Vec<[usize; 3]>,
128}
129
130impl SimpleMesh {
131 pub fn new() -> Self {
133 Self {
134 vertices: Vec::new(),
135 triangles: Vec::new(),
136 }
137 }
138
139 pub fn from_data(vertices: Vec<V3>, triangles: Vec<[usize; 3]>) -> Self {
141 Self {
142 vertices,
143 triangles,
144 }
145 }
146
147 pub fn n_triangles(&self) -> usize {
149 self.triangles.len()
150 }
151
152 pub fn n_vertices(&self) -> usize {
154 self.vertices.len()
155 }
156
157 pub fn triangle_verts(&self, tri_idx: usize) -> (V3, V3, V3) {
159 let t = self.triangles[tri_idx];
160 (
161 self.vertices[t[0]],
162 self.vertices[t[1]],
163 self.vertices[t[2]],
164 )
165 }
166
167 pub fn aabb(&self) -> (V3, V3) {
169 if self.vertices.is_empty() {
170 return ([0.0; 3], [0.0; 3]);
171 }
172 let mut mn = self.vertices[0];
173 let mut mx = self.vertices[0];
174 for v in &self.vertices {
175 for d in 0..3 {
176 if v[d] < mn[d] {
177 mn[d] = v[d];
178 }
179 if v[d] > mx[d] {
180 mx[d] = v[d];
181 }
182 }
183 }
184 (mn, mx)
185 }
186
187 pub fn add_vertex(&mut self, v: V3) -> usize {
189 let idx = self.vertices.len();
190 self.vertices.push(v);
191 idx
192 }
193
194 pub fn add_triangle(&mut self, tri: [usize; 3]) {
196 self.triangles.push(tri);
197 }
198
199 pub fn surface_area(&self) -> f64 {
201 let mut area = 0.0;
202 for &t in &self.triangles {
203 area += triangle_area(
204 self.vertices[t[0]],
205 self.vertices[t[1]],
206 self.vertices[t[2]],
207 );
208 }
209 area
210 }
211
212 pub fn signed_volume(&self) -> f64 {
214 let mut vol = 0.0;
215 for &t in &self.triangles {
216 let a = self.vertices[t[0]];
217 let b = self.vertices[t[1]];
218 let c = self.vertices[t[2]];
219 vol += dot(a, cross(b, c));
220 }
221 vol / 6.0
222 }
223
224 pub fn flip_normals(&mut self) {
226 for t in &mut self.triangles {
227 t.swap(0, 1);
228 }
229 }
230
231 pub fn unit_cube(centre: V3, half_extent: f64) -> Self {
233 let h = half_extent;
234 let c = centre;
235 let vertices = vec![
236 [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], ];
245 let triangles = vec![
246 [0, 2, 1],
248 [0, 3, 2],
249 [4, 5, 6],
251 [4, 6, 7],
252 [0, 1, 5],
254 [0, 5, 4],
255 [2, 3, 7],
257 [2, 7, 6],
258 [0, 4, 7],
260 [0, 7, 3],
261 [1, 2, 6],
263 [1, 6, 5],
264 ];
265 Self {
266 vertices,
267 triangles,
268 }
269 }
270
271 pub fn tetrahedron(centre: V3, _size: f64) -> Self {
273 let s = _size;
274 let vertices = vec![
275 [centre[0] + s, centre[1] + s, centre[2] + s],
276 [centre[0] + s, centre[1] - s, centre[2] - s],
277 [centre[0] - s, centre[1] + s, centre[2] - s],
278 [centre[0] - s, centre[1] - s, centre[2] + s],
279 ];
280 let triangles = vec![[0, 1, 2], [0, 3, 1], [0, 2, 3], [1, 3, 2]];
281 Self {
282 vertices,
283 triangles,
284 }
285 }
286}
287
288impl Default for SimpleMesh {
289 fn default() -> Self {
290 Self::new()
291 }
292}
293
294#[derive(Debug, Clone)]
300pub enum TriTriResult {
301 None,
303 Segment(V3, V3),
305 Coplanar,
307 Point(V3),
309}
310
311pub fn triangle_triangle_intersection(
315 a0: V3,
316 a1: V3,
317 a2: V3,
318 b0: V3,
319 b1: V3,
320 b2: V3,
321) -> TriTriResult {
322 let na = triangle_normal(a0, a1, a2);
324 let da = dot(na, a0);
325 let db0 = dot(na, b0) - da;
326 let db1 = dot(na, b1) - da;
327 let db2 = dot(na, b2) - da;
328
329 let db0 = if db0.abs() < GEO_EPS { 0.0 } else { db0 };
331 let db1 = if db1.abs() < GEO_EPS { 0.0 } else { db1 };
332 let db2 = if db2.abs() < GEO_EPS { 0.0 } else { db2 };
333
334 if db0 > 0.0 && db1 > 0.0 && db2 > 0.0 {
336 return TriTriResult::None;
337 }
338 if db0 < 0.0 && db1 < 0.0 && db2 < 0.0 {
339 return TriTriResult::None;
340 }
341
342 if db0.abs() < GEO_EPS && db1.abs() < GEO_EPS && db2.abs() < GEO_EPS {
344 if coplanar_triangles_overlap(a0, a1, a2, b0, b1, b2, na) {
345 return TriTriResult::Coplanar;
346 }
347 return TriTriResult::None;
348 }
349
350 let nb = triangle_normal(b0, b1, b2);
352 let _db_val = dot(nb, b0);
353 let da0 = dot(nb, a0) - _db_val;
354 let da1 = dot(nb, a1) - _db_val;
355 let da2 = dot(nb, a2) - _db_val;
356
357 let da0 = if da0.abs() < GEO_EPS { 0.0 } else { da0 };
358 let da1 = if da1.abs() < GEO_EPS { 0.0 } else { da1 };
359 let da2 = if da2.abs() < GEO_EPS { 0.0 } else { da2 };
360
361 if da0 > 0.0 && da1 > 0.0 && da2 > 0.0 {
362 return TriTriResult::None;
363 }
364 if da0 < 0.0 && da1 < 0.0 && da2 < 0.0 {
365 return TriTriResult::None;
366 }
367
368 let dir = cross(na, nb);
370 let dir_len = length(dir);
371 if dir_len < GEO_EPS {
372 return TriTriResult::None;
373 }
374 let dir = normalize(dir);
375
376 let seg_a = compute_interval_on_line(a0, a1, a2, da0, da1, da2, dir);
378 let seg_b = compute_interval_on_line(b0, b1, b2, db0, db1, db2, dir);
379
380 if let (Some((ta_min, ta_max, pa_min, pa_max)), Some((tb_min, tb_max, pb_min, pb_max))) =
381 (seg_a, seg_b)
382 {
383 let t0 = ta_min.max(tb_min);
385 let t1 = ta_max.min(tb_max);
386 if t0 > t1 + GEO_EPS {
387 return TriTriResult::None;
388 }
389 let p0 = if t0 >= ta_min - GEO_EPS && t0 <= ta_max + GEO_EPS {
391 interp_seg_param(pa_min, pa_max, ta_min, ta_max, t0)
392 } else {
393 interp_seg_param(pb_min, pb_max, tb_min, tb_max, t0)
394 };
395 let p1 = if t1 >= ta_min - GEO_EPS && t1 <= ta_max + GEO_EPS {
396 interp_seg_param(pa_min, pa_max, ta_min, ta_max, t1)
397 } else {
398 interp_seg_param(pb_min, pb_max, tb_min, tb_max, t1)
399 };
400 if dist_sq(p0, p1) < GEO_EPS * GEO_EPS {
401 return TriTriResult::Point(p0);
402 }
403 TriTriResult::Segment(p0, p1)
404 } else {
405 TriTriResult::None
406 }
407}
408
409fn compute_interval_on_line(
413 v0: V3,
414 v1: V3,
415 v2: V3,
416 d0: f64,
417 d1: f64,
418 d2: f64,
419 dir: V3,
420) -> Option<(f64, f64, V3, V3)> {
421 let verts = [v0, v1, v2];
422 let dists = [d0, d1, d2];
423
424 let mut points: Vec<(f64, V3)> = Vec::new();
426
427 for i in 0..3 {
429 if dists[i].abs() < GEO_EPS {
430 let t = dot(verts[i], dir);
431 points.push((t, verts[i]));
432 }
433 }
434
435 for (i, j) in [(0, 1), (1, 2), (2, 0)] {
437 if (dists[i] > GEO_EPS && dists[j] < -GEO_EPS)
438 || (dists[i] < -GEO_EPS && dists[j] > GEO_EPS)
439 {
440 let s = dists[i] / (dists[i] - dists[j]);
441 let p = lerp(verts[i], verts[j], s);
442 let t = dot(p, dir);
443 points.push((t, p));
444 }
445 }
446
447 if points.is_empty() {
448 return None;
449 }
450
451 let mut min_idx = 0;
453 let mut max_idx = 0;
454 for (k, (t, _)) in points.iter().enumerate() {
455 if *t < points[min_idx].0 {
456 min_idx = k;
457 }
458 if *t > points[max_idx].0 {
459 max_idx = k;
460 }
461 }
462
463 Some((
464 points[min_idx].0,
465 points[max_idx].0,
466 points[min_idx].1,
467 points[max_idx].1,
468 ))
469}
470
471fn interp_seg_param(p_min: V3, p_max: V3, t_min: f64, t_max: f64, t: f64) -> V3 {
473 if (t_max - t_min).abs() < GEO_EPS {
474 return p_min;
475 }
476 let s = (t - t_min) / (t_max - t_min);
477 lerp(p_min, p_max, s.clamp(0.0, 1.0))
478}
479
480fn coplanar_triangles_overlap(a0: V3, a1: V3, a2: V3, b0: V3, b1: V3, b2: V3, normal: V3) -> bool {
487 let abs_n = [normal[0].abs(), normal[1].abs(), normal[2].abs()];
489 let (ax1, ax2) = if abs_n[0] >= abs_n[1] && abs_n[0] >= abs_n[2] {
490 (1, 2)
491 } else if abs_n[1] >= abs_n[2] {
492 (0, 2)
493 } else {
494 (0, 1)
495 };
496
497 let proj = |v: V3| -> [f64; 2] { [v[ax1], v[ax2]] };
498
499 let pa = [proj(a0), proj(a1), proj(a2)];
500 let pb = [proj(b0), proj(b1), proj(b2)];
501
502 for i in 0..3 {
504 let j = (i + 1) % 3;
505 for k in 0..3 {
506 let l = (k + 1) % 3;
507 if segments_intersect_2d(pa[i], pa[j], pb[k], pb[l]) {
508 return true;
509 }
510 }
511 }
512
513 if point_in_triangle_2d(pa[0], pb[0], pb[1], pb[2]) {
515 return true;
516 }
517 if point_in_triangle_2d(pb[0], pa[0], pa[1], pa[2]) {
518 return true;
519 }
520
521 false
522}
523
524fn segments_intersect_2d(a: [f64; 2], b: [f64; 2], c: [f64; 2], d: [f64; 2]) -> bool {
526 let ab = [b[0] - a[0], b[1] - a[1]];
527 let cd = [d[0] - c[0], d[1] - c[1]];
528 let denom = ab[0] * cd[1] - ab[1] * cd[0];
529 if denom.abs() < GEO_EPS {
530 return false; }
532 let ac = [c[0] - a[0], c[1] - a[1]];
533 let t = (ac[0] * cd[1] - ac[1] * cd[0]) / denom;
534 let u = (ac[0] * ab[1] - ac[1] * ab[0]) / denom;
535 (-GEO_EPS..=1.0 + GEO_EPS).contains(&t) && (-GEO_EPS..=1.0 + GEO_EPS).contains(&u)
536}
537
538fn point_in_triangle_2d(p: [f64; 2], a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> bool {
540 let v0 = [c[0] - a[0], c[1] - a[1]];
541 let v1 = [b[0] - a[0], b[1] - a[1]];
542 let v2 = [p[0] - a[0], p[1] - a[1]];
543 let d00 = v0[0] * v0[0] + v0[1] * v0[1];
544 let d01 = v0[0] * v1[0] + v0[1] * v1[1];
545 let d02 = v0[0] * v2[0] + v0[1] * v2[1];
546 let d11 = v1[0] * v1[0] + v1[1] * v1[1];
547 let d12 = v1[0] * v2[0] + v1[1] * v2[1];
548 let inv_denom = d00 * d11 - d01 * d01;
549 if inv_denom.abs() < GEO_EPS {
550 return false;
551 }
552 let inv = 1.0 / inv_denom;
553 let u = (d11 * d02 - d01 * d12) * inv;
554 let v = (d00 * d12 - d01 * d02) * inv;
555 u >= -GEO_EPS && v >= -GEO_EPS && (u + v) <= 1.0 + GEO_EPS
556}
557
558pub fn winding_number(mesh: &SimpleMesh, p: V3) -> f64 {
566 let mut wn = 0.0;
567 for &tri in &mesh.triangles {
568 let a = sub(mesh.vertices[tri[0]], p);
569 let b = sub(mesh.vertices[tri[1]], p);
570 let c = sub(mesh.vertices[tri[2]], p);
571 let la = length(a);
572 let lb = length(b);
573 let lc = length(c);
574 if la < GEO_EPS || lb < GEO_EPS || lc < GEO_EPS {
575 continue;
576 }
577 let num = dot(a, cross(b, c));
578 let denom = la * lb * lc + dot(a, b) * lc + dot(b, c) * la + dot(c, a) * lb;
579 wn += num.atan2(denom);
580 }
581 wn / (2.0 * std::f64::consts::PI)
582}
583
584pub fn is_inside(mesh: &SimpleMesh, p: V3) -> bool {
586 winding_number(mesh, p).abs() > 0.5
587}
588
589#[derive(Debug, Clone)]
595pub struct IntersectionSegment {
596 pub start: V3,
598 pub end: V3,
600 pub tri_a: usize,
602 pub tri_b: usize,
604}
605
606pub fn extract_intersection_contours(
608 mesh_a: &SimpleMesh,
609 mesh_b: &SimpleMesh,
610) -> Vec<IntersectionSegment> {
611 let mut segments = Vec::new();
612 for i in 0..mesh_a.n_triangles() {
613 let (a0, a1, a2) = mesh_a.triangle_verts(i);
614 for j in 0..mesh_b.n_triangles() {
615 let (b0, b1, b2) = mesh_b.triangle_verts(j);
616 match triangle_triangle_intersection(a0, a1, a2, b0, b1, b2) {
617 TriTriResult::Segment(p0, p1) => {
618 segments.push(IntersectionSegment {
619 start: p0,
620 end: p1,
621 tri_a: i,
622 tri_b: j,
623 });
624 }
625 TriTriResult::Point(p) => {
626 segments.push(IntersectionSegment {
627 start: p,
628 end: p,
629 tri_a: i,
630 tri_b: j,
631 });
632 }
633 _ => {}
634 }
635 }
636 }
637 segments
638}
639
640pub fn split_triangle_by_plane(
647 v0: V3,
648 v1: V3,
649 v2: V3,
650 plane_normal: V3,
651 plane_point: V3,
652) -> (Vec<[V3; 3]>, Vec<[V3; 3]>) {
653 let verts = [v0, v1, v2];
654 let dists: Vec<f64> = verts
655 .iter()
656 .map(|v| dot(sub(*v, plane_point), plane_normal))
657 .collect();
658
659 let pos_count = dists.iter().filter(|&&d| d > GEO_EPS).count();
660 let neg_count = dists.iter().filter(|&&d| d < -GEO_EPS).count();
661
662 if neg_count == 0 {
664 return (vec![[v0, v1, v2]], Vec::new());
665 }
666 if pos_count == 0 {
667 return (Vec::new(), vec![[v0, v1, v2]]);
668 }
669
670 let mut front = Vec::new();
672 let mut back = Vec::new();
673
674 for start in 0..3 {
676 let i0 = start;
677 let i1 = (start + 1) % 3;
678 let i2 = (start + 2) % 3;
679
680 if (dists[i0] > GEO_EPS && dists[i1] < -GEO_EPS && dists[i2] < -GEO_EPS)
681 || (dists[i0] > GEO_EPS
682 && dists[i1] <= GEO_EPS
683 && dists[i2] < -GEO_EPS
684 && dists[i1].abs() <= GEO_EPS)
685 {
686 let t01 = dists[i0] / (dists[i0] - dists[i1]);
688 let t02 = dists[i0] / (dists[i0] - dists[i2]);
689 let p01 = lerp(verts[i0], verts[i1], t01);
690 let p02 = lerp(verts[i0], verts[i2], t02);
691 front.push([verts[i0], p01, p02]);
692 back.push([p01, verts[i1], verts[i2]]);
693 back.push([p01, verts[i2], p02]);
694 return (front, back);
695 }
696 if (dists[i0] < -GEO_EPS && dists[i1] > GEO_EPS && dists[i2] > GEO_EPS)
697 || (dists[i0] < -GEO_EPS
698 && dists[i1] >= -GEO_EPS
699 && dists[i2] > GEO_EPS
700 && dists[i1].abs() <= GEO_EPS)
701 {
702 let t01 = dists[i0] / (dists[i0] - dists[i1]);
704 let t02 = dists[i0] / (dists[i0] - dists[i2]);
705 let p01 = lerp(verts[i0], verts[i1], t01);
706 let p02 = lerp(verts[i0], verts[i2], t02);
707 back.push([verts[i0], p01, p02]);
708 front.push([p01, verts[i1], verts[i2]]);
709 front.push([p01, verts[i2], p02]);
710 return (front, back);
711 }
712 }
713
714 (vec![[v0, v1, v2]], Vec::new())
716}
717
718pub fn weld_vertices(mesh: &SimpleMesh, tolerance: f64) -> SimpleMesh {
725 let tol_sq = tolerance * tolerance;
726 let mut new_verts: Vec<V3> = Vec::new();
727 let mut remap: Vec<usize> = Vec::new();
728
729 for v in &mesh.vertices {
730 let mut found = None;
731 for (k, nv) in new_verts.iter().enumerate() {
732 if dist_sq(*v, *nv) < tol_sq {
733 found = Some(k);
734 break;
735 }
736 }
737 if let Some(k) = found {
738 remap.push(k);
739 } else {
740 remap.push(new_verts.len());
741 new_verts.push(*v);
742 }
743 }
744
745 let new_tris: Vec<[usize; 3]> = mesh
746 .triangles
747 .iter()
748 .map(|t| [remap[t[0]], remap[t[1]], remap[t[2]]])
749 .collect();
750
751 SimpleMesh::from_data(new_verts, new_tris)
752}
753
754pub fn remove_degenerate_triangles(mesh: &mut SimpleMesh) {
756 mesh.triangles.retain(|t| {
757 if t[0] == t[1] || t[1] == t[2] || t[0] == t[2] {
758 return false;
759 }
760 let a = mesh.vertices[t[0]];
761 let b = mesh.vertices[t[1]];
762 let c = mesh.vertices[t[2]];
763 triangle_area(a, b, c) > GEO_EPS
764 });
765}
766
767pub fn remove_unused_vertices(mesh: &mut SimpleMesh) {
769 let n = mesh.vertices.len();
770 let mut used = vec![false; n];
771 for t in &mesh.triangles {
772 used[t[0]] = true;
773 used[t[1]] = true;
774 used[t[2]] = true;
775 }
776 let mut remap = vec![0usize; n];
777 let mut new_verts = Vec::new();
778 for (i, &u) in used.iter().enumerate() {
779 if u {
780 remap[i] = new_verts.len();
781 new_verts.push(mesh.vertices[i]);
782 }
783 }
784 for t in &mut mesh.triangles {
785 t[0] = remap[t[0]];
786 t[1] = remap[t[1]];
787 t[2] = remap[t[2]];
788 }
789 mesh.vertices = new_verts;
790}
791
792pub fn cleanup_mesh(mesh: &SimpleMesh) -> SimpleMesh {
794 let mut result = weld_vertices(mesh, WELD_EPS);
795 remove_degenerate_triangles(&mut result);
796 remove_unused_vertices(&mut result);
797 result
798}
799
800pub fn stitch_meshes(a: &SimpleMesh, b: &SimpleMesh) -> SimpleMesh {
806 let offset = a.vertices.len();
807 let mut vertices = a.vertices.clone();
808 vertices.extend_from_slice(&b.vertices);
809 let mut triangles = a.triangles.clone();
810 for t in &b.triangles {
811 triangles.push([t[0] + offset, t[1] + offset, t[2] + offset]);
812 }
813 SimpleMesh {
814 vertices,
815 triangles,
816 }
817}
818
819pub fn classify_triangles(mesh: &SimpleMesh, other: &SimpleMesh) -> Vec<bool> {
826 let mut result = Vec::with_capacity(mesh.n_triangles());
827 for i in 0..mesh.n_triangles() {
828 let (a, b, c) = mesh.triangle_verts(i);
829 let centroid = scale(add(add(a, b), c), 1.0 / 3.0);
830 result.push(is_inside(other, centroid));
831 }
832 result
833}
834
835pub fn mesh_boolean(mesh_a: &SimpleMesh, mesh_b: &SimpleMesh, op: MeshBooleanOp) -> SimpleMesh {
841 let a_inside_b = classify_triangles(mesh_a, mesh_b);
842 let b_inside_a = classify_triangles(mesh_b, mesh_a);
843
844 let mut result = SimpleMesh::new();
845
846 match op {
847 MeshBooleanOp::Union => {
848 collect_triangles(mesh_a, &a_inside_b, false, &mut result);
850 collect_triangles(mesh_b, &b_inside_a, false, &mut result);
852 }
853 MeshBooleanOp::Intersection => {
854 collect_triangles(mesh_a, &a_inside_b, true, &mut result);
856 collect_triangles(mesh_b, &b_inside_a, true, &mut result);
858 }
859 MeshBooleanOp::Difference => {
860 collect_triangles(mesh_a, &a_inside_b, false, &mut result);
862 collect_triangles_flipped(mesh_b, &b_inside_a, true, &mut result);
864 }
865 }
866
867 cleanup_mesh(&result)
868}
869
870fn collect_triangles(
872 mesh: &SimpleMesh,
873 classification: &[bool],
874 keep_inside: bool,
875 result: &mut SimpleMesh,
876) {
877 let offset = result.vertices.len();
878 result.vertices.extend_from_slice(&mesh.vertices);
879 for (i, &is_inside_val) in classification.iter().enumerate() {
880 if is_inside_val == keep_inside {
881 let t = mesh.triangles[i];
882 result
883 .triangles
884 .push([t[0] + offset, t[1] + offset, t[2] + offset]);
885 }
886 }
887}
888
889fn collect_triangles_flipped(
891 mesh: &SimpleMesh,
892 classification: &[bool],
893 keep_inside: bool,
894 result: &mut SimpleMesh,
895) {
896 let offset = result.vertices.len();
897 result.vertices.extend_from_slice(&mesh.vertices);
898 for (i, &is_inside_val) in classification.iter().enumerate() {
899 if is_inside_val == keep_inside {
900 let t = mesh.triangles[i];
901 result
903 .triangles
904 .push([t[1] + offset, t[0] + offset, t[2] + offset]);
905 }
906 }
907}
908
909pub fn ray_triangle_intersect(origin: V3, dir: V3, v0: V3, v1: V3, v2: V3) -> Option<f64> {
916 let e1 = sub(v1, v0);
917 let e2 = sub(v2, v0);
918 let h = cross(dir, e2);
919 let a = dot(e1, h);
920 if a.abs() < GEO_EPS {
921 return None;
922 }
923 let f = 1.0 / a;
924 let s = sub(origin, v0);
925 let u = f * dot(s, h);
926 if !(0.0..=1.0).contains(&u) {
927 return None;
928 }
929 let q = cross(s, e1);
930 let v = f * dot(dir, q);
931 if v < 0.0 || u + v > 1.0 {
932 return None;
933 }
934 let t = f * dot(e2, q);
935 if t > GEO_EPS { Some(t) } else { None }
936}
937
938pub fn ray_mesh_intersection_count(mesh: &SimpleMesh, origin: V3, dir: V3) -> usize {
940 let mut count = 0;
941 for &tri in &mesh.triangles {
942 if ray_triangle_intersect(
943 origin,
944 dir,
945 mesh.vertices[tri[0]],
946 mesh.vertices[tri[1]],
947 mesh.vertices[tri[2]],
948 )
949 .is_some()
950 {
951 count += 1;
952 }
953 }
954 count
955}
956
957pub fn is_inside_ray(mesh: &SimpleMesh, p: V3) -> bool {
959 let dir = [1.0, 0.0, 0.0];
960 ray_mesh_intersection_count(mesh, p, dir) % 2 == 1
961}
962
963#[derive(Debug, Clone)]
969pub struct MeshStats {
970 pub n_vertices: usize,
972 pub n_triangles: usize,
974 pub surface_area: f64,
976 pub signed_volume: f64,
978 pub min_triangle_area: f64,
980 pub max_triangle_area: f64,
982}
983
984pub fn compute_mesh_stats(mesh: &SimpleMesh) -> MeshStats {
986 let mut min_area = f64::INFINITY;
987 let mut max_area = 0.0_f64;
988 let mut total_area = 0.0;
989 for i in 0..mesh.n_triangles() {
990 let (a, b, c) = mesh.triangle_verts(i);
991 let area = triangle_area(a, b, c);
992 total_area += area;
993 if area < min_area {
994 min_area = area;
995 }
996 if area > max_area {
997 max_area = area;
998 }
999 }
1000 MeshStats {
1001 n_vertices: mesh.n_vertices(),
1002 n_triangles: mesh.n_triangles(),
1003 surface_area: total_area,
1004 signed_volume: mesh.signed_volume(),
1005 min_triangle_area: min_area,
1006 max_triangle_area: max_area,
1007 }
1008}
1009
1010pub fn is_watertight(mesh: &SimpleMesh) -> bool {
1012 use std::collections::HashMap;
1013 let mut edge_count: HashMap<(usize, usize), u32> = HashMap::new();
1014 for t in &mesh.triangles {
1015 for (&a, &b) in t.iter().zip(t.iter().cycle().skip(1).take(3)) {
1016 let key = if a < b { (a, b) } else { (b, a) };
1017 *edge_count.entry(key).or_insert(0) += 1;
1018 }
1019 }
1020 edge_count.values().all(|&c| c == 2)
1021}
1022
1023pub fn euler_characteristic(mesh: &SimpleMesh) -> i64 {
1025 use std::collections::HashSet;
1026 let v = mesh.n_vertices() as i64;
1027 let f = mesh.n_triangles() as i64;
1028 let mut edges = HashSet::new();
1029 for t in &mesh.triangles {
1030 for (&a, &b) in t.iter().zip(t.iter().cycle().skip(1).take(3)) {
1031 let key = if a < b { (a, b) } else { (b, a) };
1032 edges.insert(key);
1033 }
1034 }
1035 let e = edges.len() as i64;
1036 v - e + f
1037}
1038
1039#[cfg(test)]
1044mod tests {
1045 use super::*;
1046
1047 #[test]
1050 fn test_unit_cube_creation() {
1051 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1052 assert_eq!(cube.n_vertices(), 8);
1053 assert_eq!(cube.n_triangles(), 12);
1054 }
1055
1056 #[test]
1057 fn test_cube_surface_area() {
1058 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1059 let area = cube.surface_area();
1060 assert!((area - 24.0).abs() < 1e-8, "area = {area}");
1062 }
1063
1064 #[test]
1065 fn test_cube_signed_volume() {
1066 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1067 let vol = cube.signed_volume();
1068 assert!((vol.abs() - 8.0).abs() < 1e-8, "vol = {vol}");
1070 }
1071
1072 #[test]
1073 fn test_tetrahedron_creation() {
1074 let tet = SimpleMesh::tetrahedron([0.0; 3], 1.0);
1075 assert_eq!(tet.n_vertices(), 4);
1076 assert_eq!(tet.n_triangles(), 4);
1077 }
1078
1079 #[test]
1080 fn test_empty_mesh() {
1081 let m = SimpleMesh::new();
1082 assert_eq!(m.n_vertices(), 0);
1083 assert_eq!(m.n_triangles(), 0);
1084 let (mn, mx) = m.aabb();
1085 assert_eq!(mn, [0.0; 3]);
1086 assert_eq!(mx, [0.0; 3]);
1087 }
1088
1089 #[test]
1090 fn test_add_vertex_and_triangle() {
1091 let mut m = SimpleMesh::new();
1092 let a = m.add_vertex([0.0, 0.0, 0.0]);
1093 let b = m.add_vertex([1.0, 0.0, 0.0]);
1094 let c = m.add_vertex([0.0, 1.0, 0.0]);
1095 m.add_triangle([a, b, c]);
1096 assert_eq!(m.n_triangles(), 1);
1097 let area = m.surface_area();
1098 assert!((area - 0.5).abs() < 1e-10);
1099 }
1100
1101 #[test]
1104 fn test_tri_tri_no_intersection() {
1105 let a0 = [0.0, 0.0, 0.0];
1106 let a1 = [1.0, 0.0, 0.0];
1107 let a2 = [0.0, 1.0, 0.0];
1108 let b0 = [5.0, 5.0, 5.0];
1109 let b1 = [6.0, 5.0, 5.0];
1110 let b2 = [5.0, 6.0, 5.0];
1111 match triangle_triangle_intersection(a0, a1, a2, b0, b1, b2) {
1112 TriTriResult::None => {}
1113 other => panic!("expected None, got {other:?}"),
1114 }
1115 }
1116
1117 #[test]
1118 fn test_tri_tri_segment_intersection() {
1119 let a0 = [-1.0, 0.0, 0.0];
1121 let a1 = [1.0, 0.0, 0.0];
1122 let a2 = [0.0, 0.0, 1.0];
1123 let b0 = [0.0, -1.0, 0.25];
1124 let b1 = [0.0, 1.0, 0.25];
1125 let b2 = [0.0, 0.0, 0.75];
1126 match triangle_triangle_intersection(a0, a1, a2, b0, b1, b2) {
1127 TriTriResult::Segment(p0, p1) => {
1128 assert!(dist_sq(p0, p1) > GEO_EPS, "segment too short");
1129 }
1130 other => panic!("expected Segment, got {other:?}"),
1131 }
1132 }
1133
1134 #[test]
1135 fn test_tri_tri_coplanar_overlap() {
1136 let a0 = [0.0, 0.0, 0.0];
1137 let a1 = [2.0, 0.0, 0.0];
1138 let a2 = [0.0, 2.0, 0.0];
1139 let b0 = [1.0, 0.0, 0.0];
1140 let b1 = [3.0, 0.0, 0.0];
1141 let b2 = [1.0, 2.0, 0.0];
1142 match triangle_triangle_intersection(a0, a1, a2, b0, b1, b2) {
1143 TriTriResult::Coplanar => {}
1144 other => panic!("expected Coplanar, got {other:?}"),
1145 }
1146 }
1147
1148 #[test]
1149 fn test_tri_tri_coplanar_no_overlap() {
1150 let a0 = [0.0, 0.0, 0.0];
1151 let a1 = [1.0, 0.0, 0.0];
1152 let a2 = [0.0, 1.0, 0.0];
1153 let b0 = [5.0, 5.0, 0.0];
1154 let b1 = [6.0, 5.0, 0.0];
1155 let b2 = [5.0, 6.0, 0.0];
1156 match triangle_triangle_intersection(a0, a1, a2, b0, b1, b2) {
1157 TriTriResult::None => {}
1158 other => panic!("expected None for separated coplanar, got {other:?}"),
1159 }
1160 }
1161
1162 #[test]
1165 fn test_winding_number_inside_cube() {
1166 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1167 let wn = winding_number(&cube, [0.0, 0.0, 0.0]);
1168 assert!(wn.abs() > 0.4, "wn = {wn}, expected ~1");
1169 }
1170
1171 #[test]
1172 fn test_winding_number_outside_cube() {
1173 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1174 let wn = winding_number(&cube, [5.0, 5.0, 5.0]);
1175 assert!(wn.abs() < 0.1, "wn = {wn}, expected ~0");
1176 }
1177
1178 #[test]
1179 fn test_is_inside_cube() {
1180 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1181 assert!(is_inside(&cube, [0.0, 0.0, 0.0]));
1182 assert!(!is_inside(&cube, [5.0, 5.0, 5.0]));
1183 }
1184
1185 #[test]
1188 fn test_ray_triangle_hit() {
1189 let v0 = [-1.0, -1.0, 1.0];
1190 let v1 = [1.0, -1.0, 1.0];
1191 let v2 = [0.0, 1.0, 1.0];
1192 let origin = [0.0, 0.0, 0.0];
1193 let dir = [0.0, 0.0, 1.0];
1194 let t = ray_triangle_intersect(origin, dir, v0, v1, v2);
1195 assert!(t.is_some());
1196 assert!((t.unwrap() - 1.0).abs() < 1e-8);
1197 }
1198
1199 #[test]
1200 fn test_ray_triangle_miss() {
1201 let v0 = [-1.0, -1.0, 1.0];
1202 let v1 = [1.0, -1.0, 1.0];
1203 let v2 = [0.0, 1.0, 1.0];
1204 let origin = [10.0, 10.0, 0.0];
1205 let dir = [0.0, 0.0, 1.0];
1206 assert!(ray_triangle_intersect(origin, dir, v0, v1, v2).is_none());
1207 }
1208
1209 #[test]
1210 fn test_ray_mesh_count() {
1211 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1212 let count = ray_mesh_intersection_count(&cube, [-5.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1213 assert_eq!(count % 2, 0, "count = {count}");
1215 }
1216
1217 #[test]
1220 fn test_contour_overlapping_cubes() {
1221 let a = SimpleMesh::unit_cube([0.0; 3], 1.0);
1222 let b = SimpleMesh::unit_cube([1.0, 0.0, 0.0], 1.0);
1223 let segs = extract_intersection_contours(&a, &b);
1224 assert!(!segs.is_empty(), "expected intersection segments");
1226 }
1227
1228 #[test]
1229 fn test_contour_separated_cubes() {
1230 let a = SimpleMesh::unit_cube([0.0; 3], 1.0);
1231 let b = SimpleMesh::unit_cube([10.0, 0.0, 0.0], 1.0);
1232 let segs = extract_intersection_contours(&a, &b);
1233 assert!(
1234 segs.is_empty(),
1235 "expected no intersections, got {}",
1236 segs.len()
1237 );
1238 }
1239
1240 #[test]
1243 fn test_split_triangle_all_front() {
1244 let (front, back) = split_triangle_by_plane(
1245 [0.0, 0.0, 1.0],
1246 [1.0, 0.0, 1.0],
1247 [0.0, 1.0, 1.0],
1248 [0.0, 0.0, 1.0], [0.0, 0.0, 0.0], );
1251 assert_eq!(front.len(), 1);
1252 assert!(back.is_empty());
1253 }
1254
1255 #[test]
1256 fn test_split_triangle_straddles_plane() {
1257 let (front, back) = split_triangle_by_plane(
1258 [0.0, 0.0, 1.0],
1259 [1.0, 0.0, -1.0],
1260 [0.0, 1.0, -1.0],
1261 [0.0, 0.0, 1.0],
1262 [0.0, 0.0, 0.0],
1263 );
1264 assert!(!front.is_empty(), "should have front triangles");
1265 assert!(!back.is_empty(), "should have back triangles");
1266 }
1267
1268 #[test]
1271 fn test_weld_vertices_merges_close() {
1272 let mut m = SimpleMesh::new();
1273 m.add_vertex([0.0, 0.0, 0.0]);
1274 m.add_vertex([1e-12, 0.0, 0.0]); m.add_vertex([1.0, 0.0, 0.0]);
1276 m.add_triangle([0, 2, 1]);
1277 let welded = weld_vertices(&m, 1e-8);
1278 assert_eq!(welded.n_vertices(), 2, "close vertices should merge");
1279 }
1280
1281 #[test]
1282 fn test_remove_degenerate() {
1283 let mut m = SimpleMesh::from_data(
1284 vec![[0.0; 3], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
1285 vec![[0, 0, 1], [0, 1, 2]], );
1287 remove_degenerate_triangles(&mut m);
1288 assert_eq!(m.n_triangles(), 1);
1289 }
1290
1291 #[test]
1292 fn test_cleanup_mesh() {
1293 let mut m = SimpleMesh::new();
1294 m.add_vertex([0.0, 0.0, 0.0]);
1295 m.add_vertex([1e-12, 0.0, 0.0]);
1296 m.add_vertex([1.0, 0.0, 0.0]);
1297 m.add_vertex([0.0, 1.0, 0.0]);
1298 m.add_vertex([99.0, 99.0, 99.0]); m.add_triangle([0, 2, 3]);
1300 let cleaned = cleanup_mesh(&m);
1301 assert!(cleaned.n_vertices() <= 3);
1302 assert_eq!(cleaned.n_triangles(), 1);
1303 }
1304
1305 #[test]
1308 fn test_stitch_meshes() {
1309 let a = SimpleMesh::unit_cube([0.0; 3], 1.0);
1310 let b = SimpleMesh::unit_cube([5.0, 0.0, 0.0], 1.0);
1311 let combined = stitch_meshes(&a, &b);
1312 assert_eq!(combined.n_vertices(), 16);
1313 assert_eq!(combined.n_triangles(), 24);
1314 }
1315
1316 #[test]
1319 fn test_boolean_union_separated() {
1320 let a = SimpleMesh::unit_cube([0.0; 3], 1.0);
1321 let b = SimpleMesh::unit_cube([10.0, 0.0, 0.0], 1.0);
1322 let result = mesh_boolean(&a, &b, MeshBooleanOp::Union);
1323 assert!(
1325 result.n_triangles() >= 20,
1326 "union tris = {}",
1327 result.n_triangles()
1328 );
1329 }
1330
1331 #[test]
1332 fn test_boolean_intersection_separated() {
1333 let a = SimpleMesh::unit_cube([0.0; 3], 1.0);
1334 let b = SimpleMesh::unit_cube([10.0, 0.0, 0.0], 1.0);
1335 let result = mesh_boolean(&a, &b, MeshBooleanOp::Intersection);
1336 assert_eq!(result.n_triangles(), 0);
1338 }
1339
1340 #[test]
1341 fn test_boolean_difference_separated() {
1342 let a = SimpleMesh::unit_cube([0.0; 3], 1.0);
1343 let b = SimpleMesh::unit_cube([10.0, 0.0, 0.0], 1.0);
1344 let result = mesh_boolean(&a, &b, MeshBooleanOp::Difference);
1345 assert!(
1347 result.n_triangles() >= 10,
1348 "diff tris = {}",
1349 result.n_triangles()
1350 );
1351 }
1352
1353 #[test]
1356 fn test_mesh_stats() {
1357 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1358 let stats = compute_mesh_stats(&cube);
1359 assert_eq!(stats.n_vertices, 8);
1360 assert_eq!(stats.n_triangles, 12);
1361 assert!((stats.surface_area - 24.0).abs() < 1e-8);
1362 }
1363
1364 #[test]
1365 fn test_euler_characteristic_cube() {
1366 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1367 let chi = euler_characteristic(&cube);
1368 assert_eq!(
1369 chi, 2,
1370 "Euler characteristic of a cube should be 2, got {chi}"
1371 );
1372 }
1373
1374 #[test]
1375 fn test_flip_normals() {
1376 let mut cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1377 let vol_before = cube.signed_volume();
1378 cube.flip_normals();
1379 let vol_after = cube.signed_volume();
1380 assert!(
1381 (vol_before + vol_after).abs() < 1e-8,
1382 "flipping should negate volume"
1383 );
1384 }
1385
1386 #[test]
1387 fn test_is_watertight_cube() {
1388 let cube = SimpleMesh::unit_cube([0.0; 3], 1.0);
1389 assert!(is_watertight(&cube), "unit cube should be watertight");
1390 }
1391
1392 #[test]
1393 fn test_classify_triangles_inside() {
1394 let big_cube = SimpleMesh::unit_cube([0.0; 3], 2.0);
1395 let small_cube = SimpleMesh::unit_cube([0.0; 3], 0.5);
1396 let classification = classify_triangles(&small_cube, &big_cube);
1397 let all_inside = classification.iter().all(|&x| x);
1399 assert!(all_inside, "small cube should be inside big cube");
1400 }
1401}