1use crate::{Mesh, MeshError, MeshResult, Vertex};
37use nalgebra::{Point3, Vector3};
38use std::collections::{HashMap, HashSet};
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum BooleanOp {
43 Union,
45
46 Difference,
48
49 Intersection,
51}
52
53#[derive(Debug, Clone)]
55pub struct BooleanParams {
56 pub tolerance: f64,
58
59 pub cleanup: bool,
61
62 pub triangulate: bool,
64
65 pub coplanar_strategy: CoplanarStrategy,
67}
68
69impl Default for BooleanParams {
70 fn default() -> Self {
71 Self {
72 tolerance: 1e-8,
73 cleanup: true,
74 triangulate: true,
75 coplanar_strategy: CoplanarStrategy::Include,
76 }
77 }
78}
79
80impl BooleanParams {
81 pub fn for_scans() -> Self {
83 Self {
84 tolerance: 1e-5,
85 cleanup: true,
86 ..Default::default()
87 }
88 }
89
90 pub fn for_cad() -> Self {
92 Self {
93 tolerance: 1e-10,
94 cleanup: true,
95 ..Default::default()
96 }
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum CoplanarStrategy {
103 Include,
105
106 Exclude,
108
109 KeepBoth,
111}
112
113#[derive(Debug)]
115pub struct BooleanResult {
116 pub mesh: Mesh,
118
119 pub intersection_edge_count: usize,
121
122 pub new_vertex_count: usize,
124
125 pub had_coplanar_faces: bool,
127
128 pub stats: BooleanStats,
130}
131
132#[derive(Debug, Clone, Default)]
134pub struct BooleanStats {
135 pub faces_from_a: usize,
137
138 pub faces_from_b: usize,
140
141 pub faces_split: usize,
143
144 pub coplanar_pairs: usize,
146
147 pub non_manifold_edges_fixed: usize,
149
150 pub phase_times_ms: Vec<(String, f64)>,
152}
153
154#[derive(Debug, Clone)]
160struct Aabb {
161 min: Point3<f64>,
162 max: Point3<f64>,
163}
164
165impl Aabb {
166 fn new() -> Self {
167 Self {
168 min: Point3::new(f64::MAX, f64::MAX, f64::MAX),
169 max: Point3::new(f64::MIN, f64::MIN, f64::MIN),
170 }
171 }
172
173 fn from_triangle(v0: &Point3<f64>, v1: &Point3<f64>, v2: &Point3<f64>) -> Self {
174 Self {
175 min: Point3::new(
176 v0.x.min(v1.x).min(v2.x),
177 v0.y.min(v1.y).min(v2.y),
178 v0.z.min(v1.z).min(v2.z),
179 ),
180 max: Point3::new(
181 v0.x.max(v1.x).max(v2.x),
182 v0.y.max(v1.y).max(v2.y),
183 v0.z.max(v1.z).max(v2.z),
184 ),
185 }
186 }
187
188 fn expand(&mut self, other: &Aabb) {
189 self.min.x = self.min.x.min(other.min.x);
190 self.min.y = self.min.y.min(other.min.y);
191 self.min.z = self.min.z.min(other.min.z);
192 self.max.x = self.max.x.max(other.max.x);
193 self.max.y = self.max.y.max(other.max.y);
194 self.max.z = self.max.z.max(other.max.z);
195 }
196
197 fn intersects(&self, other: &Aabb, tolerance: f64) -> bool {
198 !(self.max.x + tolerance < other.min.x
199 || other.max.x + tolerance < self.min.x
200 || self.max.y + tolerance < other.min.y
201 || other.max.y + tolerance < self.min.y
202 || self.max.z + tolerance < other.min.z
203 || other.max.z + tolerance < self.min.z)
204 }
205
206 fn center(&self) -> Point3<f64> {
207 Point3::new(
208 (self.min.x + self.max.x) * 0.5,
209 (self.min.y + self.max.y) * 0.5,
210 (self.min.z + self.max.z) * 0.5,
211 )
212 }
213
214 fn longest_axis(&self) -> usize {
215 let dx = self.max.x - self.min.x;
216 let dy = self.max.y - self.min.y;
217 let dz = self.max.z - self.min.z;
218 if dx >= dy && dx >= dz {
219 0
220 } else if dy >= dz {
221 1
222 } else {
223 2
224 }
225 }
226}
227
228#[derive(Debug)]
230enum BvhNode {
231 Leaf {
232 bbox: Aabb,
233 triangles: Vec<usize>,
234 },
235 Internal {
236 bbox: Aabb,
237 left: Box<BvhNode>,
238 right: Box<BvhNode>,
239 },
240}
241
242struct Bvh {
244 root: Option<BvhNode>,
245}
246
247impl Bvh {
248 fn build(mesh: &Mesh, max_leaf_size: usize) -> Self {
250 if mesh.faces.is_empty() {
251 return Self { root: None };
252 }
253
254 let triangles: Vec<(usize, Aabb)> = mesh
256 .faces
257 .iter()
258 .enumerate()
259 .map(|(i, face)| {
260 let v0 = &mesh.vertices[face[0] as usize].position;
261 let v1 = &mesh.vertices[face[1] as usize].position;
262 let v2 = &mesh.vertices[face[2] as usize].position;
263 (i, Aabb::from_triangle(v0, v1, v2))
264 })
265 .collect();
266
267 let indices: Vec<usize> = (0..triangles.len()).collect();
268 let root = Self::build_recursive(&triangles, indices, max_leaf_size);
269
270 Self { root: Some(root) }
271 }
272
273 fn build_recursive(
274 triangles: &[(usize, Aabb)],
275 indices: Vec<usize>,
276 max_leaf_size: usize,
277 ) -> BvhNode {
278 let mut bbox = Aabb::new();
280 for &i in &indices {
281 bbox.expand(&triangles[i].1);
282 }
283
284 if indices.len() <= max_leaf_size {
286 let triangle_indices: Vec<usize> = indices.iter().map(|&i| triangles[i].0).collect();
287 return BvhNode::Leaf {
288 bbox,
289 triangles: triangle_indices,
290 };
291 }
292
293 let axis = bbox.longest_axis();
295 let mut sorted_indices = indices;
296 sorted_indices.sort_by(|&a, &b| {
297 let ca = triangles[a].1.center();
298 let cb = triangles[b].1.center();
299 let va = match axis {
300 0 => ca.x,
301 1 => ca.y,
302 _ => ca.z,
303 };
304 let vb = match axis {
305 0 => cb.x,
306 1 => cb.y,
307 _ => cb.z,
308 };
309 va.partial_cmp(&vb).unwrap_or(std::cmp::Ordering::Equal)
310 });
311
312 let mid = sorted_indices.len() / 2;
313 let left_indices: Vec<usize> = sorted_indices[..mid].to_vec();
314 let right_indices: Vec<usize> = sorted_indices[mid..].to_vec();
315
316 let left = Self::build_recursive(triangles, left_indices, max_leaf_size);
317 let right = Self::build_recursive(triangles, right_indices, max_leaf_size);
318
319 BvhNode::Internal {
320 bbox,
321 left: Box::new(left),
322 right: Box::new(right),
323 }
324 }
325
326 fn query(&self, query_bbox: &Aabb, tolerance: f64) -> Vec<usize> {
328 let mut result = Vec::new();
329 if let Some(ref root) = self.root {
330 Self::query_recursive(root, query_bbox, tolerance, &mut result);
331 }
332 result
333 }
334
335 fn query_recursive(node: &BvhNode, query_bbox: &Aabb, tolerance: f64, result: &mut Vec<usize>) {
336 match node {
337 BvhNode::Leaf { bbox, triangles } => {
338 if bbox.intersects(query_bbox, tolerance) {
339 result.extend(triangles.iter().copied());
340 }
341 }
342 BvhNode::Internal { bbox, left, right } => {
343 if bbox.intersects(query_bbox, tolerance) {
344 Self::query_recursive(left, query_bbox, tolerance, result);
345 Self::query_recursive(right, query_bbox, tolerance, result);
346 }
347 }
348 }
349 }
350}
351
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358enum CoplanarityResult {
359 NotCoplanar,
361 CoplanarSameOrientation,
363 CoplanarOppositeOrientation,
365}
366
367fn check_coplanarity(
369 a0: &Point3<f64>,
370 a1: &Point3<f64>,
371 a2: &Point3<f64>,
372 b0: &Point3<f64>,
373 b1: &Point3<f64>,
374 b2: &Point3<f64>,
375 tolerance: f64,
376) -> CoplanarityResult {
377 let edge1_a = a1 - a0;
379 let edge2_a = a2 - a0;
380 let normal_a = edge1_a.cross(&edge2_a);
381 let normal_a_len = normal_a.norm();
382
383 if normal_a_len < tolerance {
384 return CoplanarityResult::NotCoplanar;
386 }
387
388 let normal_a = normal_a / normal_a_len;
389
390 let d_a = normal_a.dot(&a0.coords);
392 let dist_b0 = (normal_a.dot(&b0.coords) - d_a).abs();
393 let dist_b1 = (normal_a.dot(&b1.coords) - d_a).abs();
394 let dist_b2 = (normal_a.dot(&b2.coords) - d_a).abs();
395
396 if dist_b0 > tolerance || dist_b1 > tolerance || dist_b2 > tolerance {
397 return CoplanarityResult::NotCoplanar;
398 }
399
400 let edge1_b = b1 - b0;
402 let edge2_b = b2 - b0;
403 let normal_b = edge1_b.cross(&edge2_b);
404 let normal_b_len = normal_b.norm();
405
406 if normal_b_len < tolerance {
407 return CoplanarityResult::NotCoplanar;
409 }
410
411 let normal_b = normal_b / normal_b_len;
412 let dot = normal_a.dot(&normal_b);
413
414 if dot > 0.0 {
415 CoplanarityResult::CoplanarSameOrientation
416 } else {
417 CoplanarityResult::CoplanarOppositeOrientation
418 }
419}
420
421fn triangles_overlap_2d(
423 a0: &[f64; 2],
424 a1: &[f64; 2],
425 a2: &[f64; 2],
426 b0: &[f64; 2],
427 b1: &[f64; 2],
428 b2: &[f64; 2],
429) -> bool {
430 let edges = [
432 [a1[0] - a0[0], a1[1] - a0[1]],
433 [a2[0] - a1[0], a2[1] - a1[1]],
434 [a0[0] - a2[0], a0[1] - a2[1]],
435 [b1[0] - b0[0], b1[1] - b0[1]],
436 [b2[0] - b1[0], b2[1] - b1[1]],
437 [b0[0] - b2[0], b0[1] - b2[1]],
438 ];
439
440 for edge in &edges {
441 let axis = [-edge[1], edge[0]];
443
444 let project = |p: &[f64; 2]| axis[0] * p[0] + axis[1] * p[1];
446
447 let a_proj = [project(a0), project(a1), project(a2)];
448 let b_proj = [project(b0), project(b1), project(b2)];
449
450 let a_min = a_proj.iter().cloned().fold(f64::MAX, f64::min);
451 let a_max = a_proj.iter().cloned().fold(f64::MIN, f64::max);
452 let b_min = b_proj.iter().cloned().fold(f64::MAX, f64::min);
453 let b_max = b_proj.iter().cloned().fold(f64::MIN, f64::max);
454
455 if a_max < b_min || b_max < a_min {
456 return false; }
458 }
459
460 true }
462
463fn project_to_2d(point: &Point3<f64>, normal: &Vector3<f64>) -> [f64; 2] {
465 let abs_normal = [normal.x.abs(), normal.y.abs(), normal.z.abs()];
467
468 if abs_normal[0] >= abs_normal[1] && abs_normal[0] >= abs_normal[2] {
469 [point.y, point.z]
471 } else if abs_normal[1] >= abs_normal[2] {
472 [point.x, point.z]
474 } else {
475 [point.x, point.y]
477 }
478}
479
480#[derive(Debug, Clone)]
482struct IntersectionInfo {
483 tri_a: usize,
485 tri_b: usize,
487 coplanarity: CoplanarityResult,
489}
490
491pub fn boolean_operation(
493 mesh_a: &Mesh,
494 mesh_b: &Mesh,
495 operation: BooleanOp,
496 params: &BooleanParams,
497) -> MeshResult<BooleanResult> {
498 if mesh_a.vertices.is_empty() || mesh_a.faces.is_empty() {
500 return Err(MeshError::EmptyMesh {
501 details: "Mesh A is empty".to_string(),
502 });
503 }
504 if mesh_b.vertices.is_empty() || mesh_b.faces.is_empty() {
505 return Err(MeshError::EmptyMesh {
506 details: "Mesh B is empty".to_string(),
507 });
508 }
509
510 let bbox_a = compute_bbox(mesh_a);
512 let bbox_b = compute_bbox(mesh_b);
513
514 if !bboxes_overlap(&bbox_a, &bbox_b) {
515 return Ok(handle_non_overlapping(mesh_a, mesh_b, operation));
517 }
518
519 let intersections = find_mesh_intersections(mesh_a, mesh_b, params.tolerance);
521
522 if intersections.is_empty() {
523 return Ok(handle_non_intersecting(mesh_a, mesh_b, operation));
525 }
526
527 let coplanar_count = intersections
529 .iter()
530 .filter(|i| i.coplanarity != CoplanarityResult::NotCoplanar)
531 .count();
532
533 let coplanar_faces_a: HashSet<usize> = intersections
535 .iter()
536 .filter(|i| i.coplanarity != CoplanarityResult::NotCoplanar)
537 .map(|i| i.tri_a)
538 .collect();
539
540 let coplanar_faces_b: HashSet<usize> = intersections
541 .iter()
542 .filter(|i| i.coplanarity != CoplanarityResult::NotCoplanar)
543 .map(|i| i.tri_b)
544 .collect();
545
546 let a_classifications = classify_faces(mesh_a, mesh_b, params);
548 let b_classifications = classify_faces(mesh_b, mesh_a, params);
549
550 let mut result = Mesh::new();
552 let mut stats = BooleanStats {
553 coplanar_pairs: coplanar_count,
554 ..Default::default()
555 };
556
557 match operation {
558 BooleanOp::Union => {
559 add_faces_with_classification_and_coplanar(
562 &mut result,
563 mesh_a,
564 &a_classifications,
565 FaceLocation::Outside,
566 &coplanar_faces_a,
567 params.coplanar_strategy,
568 true, );
570 stats.faces_from_a = result.faces.len();
571
572 add_faces_with_classification_and_coplanar(
573 &mut result,
574 mesh_b,
575 &b_classifications,
576 FaceLocation::Outside,
577 &coplanar_faces_b,
578 params.coplanar_strategy,
579 false, );
581 stats.faces_from_b = result.faces.len() - stats.faces_from_a;
582 }
583
584 BooleanOp::Difference => {
585 add_faces_with_classification_and_coplanar(
588 &mut result,
589 mesh_a,
590 &a_classifications,
591 FaceLocation::Outside,
592 &coplanar_faces_a,
593 params.coplanar_strategy,
594 true,
595 );
596 stats.faces_from_a = result.faces.len();
597
598 add_faces_inverted_with_coplanar(
599 &mut result,
600 mesh_b,
601 &b_classifications,
602 FaceLocation::Inside,
603 &coplanar_faces_b,
604 params.coplanar_strategy,
605 false,
606 );
607 stats.faces_from_b = result.faces.len() - stats.faces_from_a;
608 }
609
610 BooleanOp::Intersection => {
611 add_faces_with_classification_and_coplanar(
614 &mut result,
615 mesh_a,
616 &a_classifications,
617 FaceLocation::Inside,
618 &coplanar_faces_a,
619 params.coplanar_strategy,
620 true,
621 );
622 stats.faces_from_a = result.faces.len();
623
624 add_faces_with_classification_and_coplanar(
625 &mut result,
626 mesh_b,
627 &b_classifications,
628 FaceLocation::Inside,
629 &coplanar_faces_b,
630 params.coplanar_strategy,
631 false,
632 );
633 stats.faces_from_b = result.faces.len() - stats.faces_from_a;
634 }
635 }
636
637 if params.cleanup {
639 weld_vertices(&mut result, params.tolerance);
641
642 let non_manifold_fixed = fix_non_manifold_edges(&mut result);
644 stats.non_manifold_edges_fixed = non_manifold_fixed;
645 }
646
647 Ok(BooleanResult {
648 mesh: result,
649 intersection_edge_count: intersections.len(),
650 new_vertex_count: 0, had_coplanar_faces: coplanar_count > 0,
652 stats,
653 })
654}
655
656#[derive(Debug, Clone, Copy, PartialEq, Eq)]
658enum FaceLocation {
659 Inside,
660 Outside,
661 #[allow(dead_code)]
662 OnBoundary, }
664
665fn compute_bbox(mesh: &Mesh) -> (Point3<f64>, Point3<f64>) {
667 if mesh.vertices.is_empty() {
668 return (Point3::origin(), Point3::origin());
669 }
670
671 let mut min = mesh.vertices[0].position;
672 let mut max = mesh.vertices[0].position;
673
674 for v in &mesh.vertices {
675 min.x = min.x.min(v.position.x);
676 min.y = min.y.min(v.position.y);
677 min.z = min.z.min(v.position.z);
678 max.x = max.x.max(v.position.x);
679 max.y = max.y.max(v.position.y);
680 max.z = max.z.max(v.position.z);
681 }
682
683 (min, max)
684}
685
686fn bboxes_overlap(a: &(Point3<f64>, Point3<f64>), b: &(Point3<f64>, Point3<f64>)) -> bool {
688 let (a_min, a_max) = a;
689 let (b_min, b_max) = b;
690
691 !(a_max.x < b_min.x
692 || b_max.x < a_min.x
693 || a_max.y < b_min.y
694 || b_max.y < a_min.y
695 || a_max.z < b_min.z
696 || b_max.z < a_min.z)
697}
698
699fn handle_non_overlapping(mesh_a: &Mesh, mesh_b: &Mesh, operation: BooleanOp) -> BooleanResult {
701 let mesh = match operation {
702 BooleanOp::Union => {
703 let mut result = mesh_a.clone();
705 let offset = result.vertices.len() as u32;
706 result.vertices.extend(mesh_b.vertices.iter().cloned());
707 for face in &mesh_b.faces {
708 result
709 .faces
710 .push([face[0] + offset, face[1] + offset, face[2] + offset]);
711 }
712 result
713 }
714 BooleanOp::Difference => {
715 mesh_a.clone()
717 }
718 BooleanOp::Intersection => {
719 Mesh::new()
721 }
722 };
723
724 BooleanResult {
725 mesh,
726 intersection_edge_count: 0,
727 new_vertex_count: 0,
728 had_coplanar_faces: false,
729 stats: BooleanStats::default(),
730 }
731}
732
733fn handle_non_intersecting(mesh_a: &Mesh, mesh_b: &Mesh, operation: BooleanOp) -> BooleanResult {
735 let a_inside_b = is_point_inside_mesh(&mesh_a.vertices[0].position, mesh_b);
737 let b_inside_a = is_point_inside_mesh(&mesh_b.vertices[0].position, mesh_a);
738
739 let mesh = match (operation, a_inside_b, b_inside_a) {
740 (BooleanOp::Union, true, _) => mesh_b.clone(), (BooleanOp::Union, _, true) => mesh_a.clone(), (BooleanOp::Union, false, false) => {
744 let mut result = mesh_a.clone();
746 let offset = result.vertices.len() as u32;
747 result.vertices.extend(mesh_b.vertices.iter().cloned());
748 for face in &mesh_b.faces {
749 result
750 .faces
751 .push([face[0] + offset, face[1] + offset, face[2] + offset]);
752 }
753 result
754 }
755
756 (BooleanOp::Difference, true, _) => Mesh::new(), (BooleanOp::Difference, _, true) => {
759 mesh_a.clone()
761 }
762 (BooleanOp::Difference, false, false) => mesh_a.clone(), (BooleanOp::Intersection, true, _) => mesh_a.clone(), (BooleanOp::Intersection, _, true) => mesh_b.clone(), (BooleanOp::Intersection, false, false) => Mesh::new(), };
769
770 BooleanResult {
771 mesh,
772 intersection_edge_count: 0,
773 new_vertex_count: 0,
774 had_coplanar_faces: false,
775 stats: BooleanStats::default(),
776 }
777}
778
779fn is_point_inside_mesh(point: &Point3<f64>, mesh: &Mesh) -> bool {
781 let ray_dir = Vector3::new(1.0, 0.0, 0.0);
783 let mut intersection_count = 0;
784
785 for face in &mesh.faces {
786 let v0 = &mesh.vertices[face[0] as usize].position;
787 let v1 = &mesh.vertices[face[1] as usize].position;
788 let v2 = &mesh.vertices[face[2] as usize].position;
789
790 if ray_triangle_intersect(point, &ray_dir, v0, v1, v2) {
791 intersection_count += 1;
792 }
793 }
794
795 intersection_count % 2 == 1
796}
797
798fn ray_triangle_intersect(
800 origin: &Point3<f64>,
801 dir: &Vector3<f64>,
802 v0: &Point3<f64>,
803 v1: &Point3<f64>,
804 v2: &Point3<f64>,
805) -> bool {
806 let epsilon = 1e-10;
807
808 let edge1 = v1 - v0;
809 let edge2 = v2 - v0;
810 let h = dir.cross(&edge2);
811 let a = edge1.dot(&h);
812
813 if a.abs() < epsilon {
814 return false;
815 }
816
817 let f = 1.0 / a;
818 let s = origin - v0;
819 let u = f * s.dot(&h);
820
821 if !(0.0..=1.0).contains(&u) {
822 return false;
823 }
824
825 let q = s.cross(&edge1);
826 let v = f * dir.dot(&q);
827
828 if v < 0.0 || u + v > 1.0 {
829 return false;
830 }
831
832 let t = f * edge2.dot(&q);
833 t > epsilon
834}
835
836fn find_mesh_intersections(mesh_a: &Mesh, mesh_b: &Mesh, tolerance: f64) -> Vec<IntersectionInfo> {
839 let mut intersections = Vec::new();
840
841 let bvh_b = Bvh::build(mesh_b, 8);
843
844 for (ai, face_a) in mesh_a.faces.iter().enumerate() {
846 let a0 = &mesh_a.vertices[face_a[0] as usize].position;
847 let a1 = &mesh_a.vertices[face_a[1] as usize].position;
848 let a2 = &mesh_a.vertices[face_a[2] as usize].position;
849
850 let bbox_a = Aabb::from_triangle(a0, a1, a2);
852
853 let candidates = bvh_b.query(&bbox_a, tolerance);
855
856 for bi in candidates {
857 let face_b = &mesh_b.faces[bi];
858 let b0 = &mesh_b.vertices[face_b[0] as usize].position;
859 let b1 = &mesh_b.vertices[face_b[1] as usize].position;
860 let b2 = &mesh_b.vertices[face_b[2] as usize].position;
861
862 let coplanarity = check_coplanarity(a0, a1, a2, b0, b1, b2, tolerance);
864
865 let intersects = match coplanarity {
866 CoplanarityResult::NotCoplanar => triangles_intersect(a0, a1, a2, b0, b1, b2),
867 CoplanarityResult::CoplanarSameOrientation
868 | CoplanarityResult::CoplanarOppositeOrientation => {
869 let edge1 = a1 - a0;
871 let edge2 = a2 - a0;
872 let normal = edge1.cross(&edge2);
873
874 let a0_2d = project_to_2d(a0, &normal);
875 let a1_2d = project_to_2d(a1, &normal);
876 let a2_2d = project_to_2d(a2, &normal);
877 let b0_2d = project_to_2d(b0, &normal);
878 let b1_2d = project_to_2d(b1, &normal);
879 let b2_2d = project_to_2d(b2, &normal);
880
881 triangles_overlap_2d(&a0_2d, &a1_2d, &a2_2d, &b0_2d, &b1_2d, &b2_2d)
882 }
883 };
884
885 if intersects {
886 intersections.push(IntersectionInfo {
887 tri_a: ai,
888 tri_b: bi,
889 coplanarity,
890 });
891 }
892 }
893 }
894
895 intersections
896}
897
898fn triangles_intersect(
900 a0: &Point3<f64>,
901 a1: &Point3<f64>,
902 a2: &Point3<f64>,
903 b0: &Point3<f64>,
904 b1: &Point3<f64>,
905 b2: &Point3<f64>,
906) -> bool {
907 let edges_a = [(a0, a1), (a1, a2), (a2, a0)];
909
910 for (e0, e1) in &edges_a {
911 let dir = *e1 - **e0;
912 if ray_triangle_intersect(e0, &dir, b0, b1, b2) {
913 let t = compute_intersection_t(e0, e1, b0, b1, b2);
915 if let Some(t) = t
916 && (0.0..=1.0).contains(&t)
917 {
918 return true;
919 }
920 }
921 }
922
923 let edges_b = [(b0, b1), (b1, b2), (b2, b0)];
925
926 for (e0, e1) in &edges_b {
927 let dir = *e1 - **e0;
928 if ray_triangle_intersect(e0, &dir, a0, a1, a2) {
929 let t = compute_intersection_t(e0, e1, a0, a1, a2);
930 if let Some(t) = t
931 && (0.0..=1.0).contains(&t)
932 {
933 return true;
934 }
935 }
936 }
937
938 false
939}
940
941fn compute_intersection_t(
943 e0: &Point3<f64>,
944 e1: &Point3<f64>,
945 v0: &Point3<f64>,
946 v1: &Point3<f64>,
947 v2: &Point3<f64>,
948) -> Option<f64> {
949 let epsilon = 1e-10;
950 let dir = e1 - e0;
951
952 let edge1 = v1 - v0;
953 let edge2 = v2 - v0;
954 let h = dir.cross(&edge2);
955 let a = edge1.dot(&h);
956
957 if a.abs() < epsilon {
958 return None;
959 }
960
961 let f = 1.0 / a;
962 let s = e0 - v0;
963 let u = f * s.dot(&h);
964
965 if !(0.0..=1.0).contains(&u) {
966 return None;
967 }
968
969 let q = s.cross(&edge1);
970 let v = f * dir.dot(&q);
971
972 if v < 0.0 || u + v > 1.0 {
973 return None;
974 }
975
976 Some(f * edge2.dot(&q))
977}
978
979fn classify_faces(mesh: &Mesh, other: &Mesh, _params: &BooleanParams) -> Vec<FaceLocation> {
981 mesh.faces
982 .iter()
983 .map(|face| {
984 let v0 = &mesh.vertices[face[0] as usize].position;
986 let v1 = &mesh.vertices[face[1] as usize].position;
987 let v2 = &mesh.vertices[face[2] as usize].position;
988
989 let centroid = Point3::from((v0.coords + v1.coords + v2.coords) / 3.0);
990
991 if is_point_inside_mesh(¢roid, other) {
992 FaceLocation::Inside
993 } else {
994 FaceLocation::Outside
995 }
996 })
997 .collect()
998}
999
1000fn add_faces_with_classification_and_coplanar(
1002 result: &mut Mesh,
1003 source: &Mesh,
1004 classifications: &[FaceLocation],
1005 keep_location: FaceLocation,
1006 coplanar_faces: &HashSet<usize>,
1007 coplanar_strategy: CoplanarStrategy,
1008 is_first_mesh: bool,
1009) {
1010 let mut vertex_map: HashMap<u32, u32> = HashMap::new();
1011
1012 for (fi, face) in source.faces.iter().enumerate() {
1013 let passes_classification = classifications[fi] == keep_location;
1015
1016 let is_coplanar = coplanar_faces.contains(&fi);
1018 let should_include = if is_coplanar {
1019 match coplanar_strategy {
1020 CoplanarStrategy::Include => is_first_mesh, CoplanarStrategy::Exclude => false,
1022 CoplanarStrategy::KeepBoth => true,
1023 }
1024 } else {
1025 passes_classification
1026 };
1027
1028 if should_include {
1029 let new_face: [u32; 3] = [
1030 *vertex_map.entry(face[0]).or_insert_with(|| {
1031 let idx = result.vertices.len() as u32;
1032 result
1033 .vertices
1034 .push(source.vertices[face[0] as usize].clone());
1035 idx
1036 }),
1037 *vertex_map.entry(face[1]).or_insert_with(|| {
1038 let idx = result.vertices.len() as u32;
1039 result
1040 .vertices
1041 .push(source.vertices[face[1] as usize].clone());
1042 idx
1043 }),
1044 *vertex_map.entry(face[2]).or_insert_with(|| {
1045 let idx = result.vertices.len() as u32;
1046 result
1047 .vertices
1048 .push(source.vertices[face[2] as usize].clone());
1049 idx
1050 }),
1051 ];
1052 result.faces.push(new_face);
1053 }
1054 }
1055}
1056
1057fn add_faces_inverted_with_coplanar(
1059 result: &mut Mesh,
1060 source: &Mesh,
1061 classifications: &[FaceLocation],
1062 keep_location: FaceLocation,
1063 coplanar_faces: &HashSet<usize>,
1064 coplanar_strategy: CoplanarStrategy,
1065 is_first_mesh: bool,
1066) {
1067 let mut vertex_map: HashMap<u32, u32> = HashMap::new();
1068
1069 for (fi, face) in source.faces.iter().enumerate() {
1070 let passes_classification = classifications[fi] == keep_location;
1071
1072 let is_coplanar = coplanar_faces.contains(&fi);
1073 let should_include = if is_coplanar {
1074 match coplanar_strategy {
1075 CoplanarStrategy::Include => is_first_mesh,
1076 CoplanarStrategy::Exclude => false,
1077 CoplanarStrategy::KeepBoth => true,
1078 }
1079 } else {
1080 passes_classification
1081 };
1082
1083 if should_include {
1084 let new_face: [u32; 3] = [
1086 *vertex_map.entry(face[0]).or_insert_with(|| {
1087 let idx = result.vertices.len() as u32;
1088 result
1089 .vertices
1090 .push(source.vertices[face[0] as usize].clone());
1091 idx
1092 }),
1093 *vertex_map.entry(face[2]).or_insert_with(|| {
1094 let idx = result.vertices.len() as u32;
1095 result
1096 .vertices
1097 .push(source.vertices[face[2] as usize].clone());
1098 idx
1099 }),
1100 *vertex_map.entry(face[1]).or_insert_with(|| {
1101 let idx = result.vertices.len() as u32;
1102 result
1103 .vertices
1104 .push(source.vertices[face[1] as usize].clone());
1105 idx
1106 }),
1107 ];
1108 result.faces.push(new_face);
1109 }
1110 }
1111}
1112
1113fn weld_vertices(mesh: &mut Mesh, tolerance: f64) {
1115 if mesh.vertices.is_empty() {
1116 return;
1117 }
1118
1119 let tol_sq = tolerance * tolerance;
1120 let mut vertex_map: Vec<u32> = (0..mesh.vertices.len() as u32).collect();
1121 let mut kept_vertices: Vec<Vertex> = Vec::new();
1122
1123 for (i, v) in mesh.vertices.iter().enumerate() {
1124 let mut found = None;
1125 for (j, kv) in kept_vertices.iter().enumerate() {
1126 let dist_sq = (v.position - kv.position).norm_squared();
1127 if dist_sq < tol_sq {
1128 found = Some(j);
1129 break;
1130 }
1131 }
1132
1133 if let Some(j) = found {
1134 vertex_map[i] = j as u32;
1135 } else {
1136 vertex_map[i] = kept_vertices.len() as u32;
1137 kept_vertices.push(v.clone());
1138 }
1139 }
1140
1141 for face in &mut mesh.faces {
1143 face[0] = vertex_map[face[0] as usize];
1144 face[1] = vertex_map[face[1] as usize];
1145 face[2] = vertex_map[face[2] as usize];
1146 }
1147
1148 mesh.vertices = kept_vertices;
1149
1150 mesh.faces
1152 .retain(|f| f[0] != f[1] && f[1] != f[2] && f[0] != f[2]);
1153}
1154
1155fn fix_non_manifold_edges(mesh: &mut Mesh) -> usize {
1158 let mut edge_faces: HashMap<(u32, u32), Vec<usize>> = HashMap::new();
1161
1162 for (fi, face) in mesh.faces.iter().enumerate() {
1163 let edges = [
1164 (face[0].min(face[1]), face[0].max(face[1])),
1165 (face[1].min(face[2]), face[1].max(face[2])),
1166 (face[2].min(face[0]), face[2].max(face[0])),
1167 ];
1168
1169 for edge in &edges {
1170 edge_faces.entry(*edge).or_default().push(fi);
1171 }
1172 }
1173
1174 let non_manifold_edges: Vec<(u32, u32)> = edge_faces
1176 .iter()
1177 .filter(|(_, faces)| faces.len() > 2)
1178 .map(|(edge, _)| *edge)
1179 .collect();
1180
1181 if non_manifold_edges.is_empty() {
1182 return 0;
1183 }
1184
1185 let mut faces_to_remove: HashSet<usize> = HashSet::new();
1187
1188 for edge in &non_manifold_edges {
1189 if let Some(faces) = edge_faces.get(edge) {
1190 for &fi in faces.iter().skip(2) {
1192 faces_to_remove.insert(fi);
1193 }
1194 }
1195 }
1196
1197 let faces_removed = faces_to_remove.len();
1199 let mut new_faces = Vec::with_capacity(mesh.faces.len() - faces_removed);
1200 for (fi, face) in mesh.faces.iter().enumerate() {
1201 if !faces_to_remove.contains(&fi) {
1202 new_faces.push(*face);
1203 }
1204 }
1205 mesh.faces = new_faces;
1206
1207 non_manifold_edges.len()
1208}
1209
1210#[derive(Debug, Clone)]
1216pub struct BooleanOffsetParams {
1217 pub offset: f64,
1219
1220 pub sphere_segments: usize,
1223
1224 pub cylinder_segments: usize,
1226
1227 pub tolerance: f64,
1229
1230 pub cleanup: bool,
1232}
1233
1234impl Default for BooleanOffsetParams {
1235 fn default() -> Self {
1236 Self {
1237 offset: 1.0,
1238 sphere_segments: 8,
1239 cylinder_segments: 8,
1240 tolerance: 1e-8,
1241 cleanup: true,
1242 }
1243 }
1244}
1245
1246impl BooleanOffsetParams {
1247 pub fn with_offset(offset: f64) -> Self {
1249 Self {
1250 offset,
1251 ..Default::default()
1252 }
1253 }
1254
1255 pub fn high_quality(offset: f64) -> Self {
1257 Self {
1258 offset,
1259 sphere_segments: 16,
1260 cylinder_segments: 16,
1261 tolerance: 1e-10,
1262 cleanup: true,
1263 }
1264 }
1265
1266 pub fn fast_preview(offset: f64) -> Self {
1268 Self {
1269 offset,
1270 sphere_segments: 4,
1271 cylinder_segments: 4,
1272 tolerance: 1e-6,
1273 cleanup: true,
1274 }
1275 }
1276
1277 pub fn with_sphere_segments(mut self, segments: usize) -> Self {
1279 self.sphere_segments = segments.max(3);
1280 self
1281 }
1282
1283 pub fn with_cylinder_segments(mut self, segments: usize) -> Self {
1285 self.cylinder_segments = segments.max(3);
1286 self
1287 }
1288}
1289
1290#[derive(Debug)]
1292pub struct BooleanOffsetResult {
1293 pub mesh: Mesh,
1295
1296 pub sphere_count: usize,
1298
1299 pub cylinder_count: usize,
1301
1302 pub prism_count: usize,
1304
1305 pub stats: BooleanOffsetStats,
1307}
1308
1309#[derive(Debug, Clone, Default)]
1311pub struct BooleanOffsetStats {
1312 pub primitive_generation_time_ms: u64,
1314
1315 pub union_time_ms: u64,
1317
1318 pub intermediate_triangles: usize,
1320
1321 pub final_triangles: usize,
1323}
1324
1325pub fn offset_boolean(
1354 mesh: &Mesh,
1355 params: &BooleanOffsetParams,
1356) -> MeshResult<BooleanOffsetResult> {
1357 use std::time::Instant;
1358
1359 if mesh.vertices.is_empty() || mesh.faces.is_empty() {
1360 return Err(MeshError::EmptyMesh {
1361 details: "Cannot offset empty mesh".to_string(),
1362 });
1363 }
1364
1365 let offset = params.offset.abs();
1366 if offset < 1e-10 {
1367 return Ok(BooleanOffsetResult {
1369 mesh: mesh.clone(),
1370 sphere_count: 0,
1371 cylinder_count: 0,
1372 prism_count: 0,
1373 stats: BooleanOffsetStats::default(),
1374 });
1375 }
1376
1377 let primitive_start = Instant::now();
1378
1379 let vertex_normals = compute_vertex_normals(mesh);
1381
1382 let edges = collect_unique_edges(mesh);
1384
1385 let mut primitives: Vec<Mesh> = Vec::new();
1387
1388 for (vi, vertex) in mesh.vertices.iter().enumerate() {
1390 let sphere = generate_sphere(&vertex.position, offset, params.sphere_segments);
1391 primitives.push(sphere);
1392
1393 if params.offset < 0.0 {
1395 let inward_center = vertex.position - vertex_normals[vi] * offset;
1397 let inward_sphere = generate_sphere(&inward_center, offset, params.sphere_segments);
1398 primitives.push(inward_sphere);
1399 }
1400 }
1401 let sphere_count = mesh.vertices.len();
1402
1403 for (vi0, vi1) in &edges {
1405 let v0 = &mesh.vertices[*vi0].position;
1406 let v1 = &mesh.vertices[*vi1].position;
1407 let cylinder = generate_cylinder(v0, v1, offset, params.cylinder_segments);
1408 primitives.push(cylinder);
1409 }
1410 let cylinder_count = edges.len();
1411
1412 for face in &mesh.faces {
1414 let v0 = &mesh.vertices[face[0] as usize].position;
1415 let v1 = &mesh.vertices[face[1] as usize].position;
1416 let v2 = &mesh.vertices[face[2] as usize].position;
1417
1418 let edge1 = v1 - v0;
1420 let edge2 = v2 - v0;
1421 let normal = edge1.cross(&edge2);
1422 let normal_len = normal.norm();
1423
1424 if normal_len > 1e-10 {
1425 let normal = normal / normal_len;
1426
1427 let prism = generate_triangular_prism(v0, v1, v2, &normal, offset);
1429 primitives.push(prism);
1430 }
1431 }
1432 let prism_count = mesh.faces.len();
1433
1434 let primitive_time_ms = primitive_start.elapsed().as_millis() as u64;
1435
1436 let union_start = Instant::now();
1438
1439 let mut result = if params.offset > 0.0 {
1441 mesh.clone()
1442 } else {
1443 mesh.clone()
1445 };
1446
1447 let mut intermediate_triangles = result.faces.len();
1451
1452 for primitive in primitives {
1453 let offset_verts = result.vertices.len() as u32;
1456 result.vertices.extend(primitive.vertices.iter().cloned());
1457 for face in &primitive.faces {
1458 result.faces.push([
1459 face[0] + offset_verts,
1460 face[1] + offset_verts,
1461 face[2] + offset_verts,
1462 ]);
1463 }
1464 intermediate_triangles = result.faces.len();
1465 }
1466
1467 let union_time_ms = union_start.elapsed().as_millis() as u64;
1468
1469 if params.offset < 0.0 {
1472 }
1475
1476 if params.cleanup {
1478 weld_vertices(&mut result, params.tolerance);
1479 }
1480
1481 let final_triangles = result.faces.len();
1482
1483 Ok(BooleanOffsetResult {
1484 mesh: result,
1485 sphere_count,
1486 cylinder_count,
1487 prism_count,
1488 stats: BooleanOffsetStats {
1489 primitive_generation_time_ms: primitive_time_ms,
1490 union_time_ms,
1491 intermediate_triangles,
1492 final_triangles,
1493 },
1494 })
1495}
1496
1497pub fn shell_boolean(
1515 mesh: &Mesh,
1516 thickness: f64,
1517 params: Option<&BooleanOffsetParams>,
1518) -> MeshResult<Mesh> {
1519 let default_params = BooleanOffsetParams::default();
1520 let params = params.unwrap_or(&default_params);
1521
1522 if thickness <= 0.0 {
1523 return Err(MeshError::repair_failed("Shell thickness must be positive"));
1524 }
1525
1526 let outer_params = BooleanOffsetParams {
1528 offset: thickness / 2.0,
1529 ..params.clone()
1530 };
1531 let outer = offset_boolean(mesh, &outer_params)?;
1532
1533 let inner_params = BooleanOffsetParams {
1535 offset: thickness / 2.0, ..params.clone()
1537 };
1538 let inner = offset_boolean(mesh, &inner_params)?;
1539
1540 let mut inner_inverted = inner.mesh;
1542 for face in &mut inner_inverted.faces {
1543 face.swap(1, 2); }
1545
1546 let bool_params = BooleanParams {
1548 tolerance: params.tolerance,
1549 cleanup: params.cleanup,
1550 ..Default::default()
1551 };
1552
1553 let result = boolean_operation(
1554 &outer.mesh,
1555 &inner_inverted,
1556 BooleanOp::Difference,
1557 &bool_params,
1558 )?;
1559
1560 Ok(result.mesh)
1561}
1562
1563fn compute_vertex_normals(mesh: &Mesh) -> Vec<Vector3<f64>> {
1565 let mut normals = vec![Vector3::zeros(); mesh.vertices.len()];
1566 let mut counts = vec![0usize; mesh.vertices.len()];
1567
1568 for face in &mesh.faces {
1569 let v0 = &mesh.vertices[face[0] as usize].position;
1570 let v1 = &mesh.vertices[face[1] as usize].position;
1571 let v2 = &mesh.vertices[face[2] as usize].position;
1572
1573 let edge1 = v1 - v0;
1574 let edge2 = v2 - v0;
1575 let normal = edge1.cross(&edge2);
1576
1577 for &vi in face {
1578 normals[vi as usize] += normal;
1579 counts[vi as usize] += 1;
1580 }
1581 }
1582
1583 for (i, normal) in normals.iter_mut().enumerate() {
1584 if counts[i] > 0 {
1585 let len = normal.norm();
1586 if len > 1e-10 {
1587 *normal /= len;
1588 }
1589 }
1590 }
1591
1592 normals
1593}
1594
1595fn collect_unique_edges(mesh: &Mesh) -> Vec<(usize, usize)> {
1597 let mut edge_set: HashSet<(usize, usize)> = HashSet::new();
1598
1599 for face in &mesh.faces {
1600 let edges = [
1601 (face[0] as usize, face[1] as usize),
1602 (face[1] as usize, face[2] as usize),
1603 (face[2] as usize, face[0] as usize),
1604 ];
1605
1606 for (a, b) in edges {
1607 let edge = if a < b { (a, b) } else { (b, a) };
1608 edge_set.insert(edge);
1609 }
1610 }
1611
1612 edge_set.into_iter().collect()
1613}
1614
1615fn generate_sphere(center: &Point3<f64>, radius: f64, segments: usize) -> Mesh {
1617 let mut mesh = Mesh::new();
1618 let segments = segments.max(3);
1619
1620 mesh.vertices.push(Vertex::new(Point3::new(
1623 center.x,
1624 center.y,
1625 center.z + radius,
1626 )));
1627
1628 for lat in 1..segments {
1630 let theta = std::f64::consts::PI * lat as f64 / segments as f64;
1631 let sin_theta = theta.sin();
1632 let cos_theta = theta.cos();
1633
1634 for lon in 0..(segments * 2) {
1635 let phi = std::f64::consts::PI * lon as f64 / segments as f64;
1636 let x = center.x + radius * sin_theta * phi.cos();
1637 let y = center.y + radius * sin_theta * phi.sin();
1638 let z = center.z + radius * cos_theta;
1639 mesh.vertices.push(Vertex::new(Point3::new(x, y, z)));
1640 }
1641 }
1642
1643 let south_pole_idx = mesh.vertices.len() as u32;
1645 mesh.vertices.push(Vertex::new(Point3::new(
1646 center.x,
1647 center.y,
1648 center.z - radius,
1649 )));
1650
1651 let ring_size = (segments * 2) as u32;
1653
1654 for i in 0..ring_size {
1656 let next = (i + 1) % ring_size;
1657 mesh.faces.push([0, 1 + next, 1 + i]);
1658 }
1659
1660 for lat in 0..(segments - 2) {
1662 let ring_start = 1 + lat as u32 * ring_size;
1663 let next_ring_start = ring_start + ring_size;
1664
1665 for i in 0..ring_size {
1666 let next = (i + 1) % ring_size;
1667
1668 mesh.faces
1669 .push([ring_start + i, ring_start + next, next_ring_start + i]);
1670 mesh.faces.push([
1671 ring_start + next,
1672 next_ring_start + next,
1673 next_ring_start + i,
1674 ]);
1675 }
1676 }
1677
1678 let last_ring_start = 1 + ((segments - 2) as u32) * ring_size;
1680 for i in 0..ring_size {
1681 let next = (i + 1) % ring_size;
1682 mesh.faces
1683 .push([last_ring_start + i, last_ring_start + next, south_pole_idx]);
1684 }
1685
1686 mesh
1687}
1688
1689fn generate_cylinder(p0: &Point3<f64>, p1: &Point3<f64>, radius: f64, segments: usize) -> Mesh {
1691 let mut mesh = Mesh::new();
1692 let segments = segments.max(3);
1693
1694 let axis = p1 - p0;
1695 let length = axis.norm();
1696 if length < 1e-10 {
1697 return mesh;
1698 }
1699 let axis_norm = axis / length;
1700
1701 let perp1 = if axis_norm.x.abs() < 0.9 {
1703 axis_norm.cross(&Vector3::x())
1704 } else {
1705 axis_norm.cross(&Vector3::y())
1706 }
1707 .normalize();
1708 let perp2 = axis_norm.cross(&perp1);
1709
1710 for (end_point, _) in [(p0, 0), (p1, 1)] {
1712 for i in 0..segments {
1713 let angle = 2.0 * std::f64::consts::PI * i as f64 / segments as f64;
1714 let offset = perp1 * angle.cos() * radius + perp2 * angle.sin() * radius;
1715 let pos = Point3::from(end_point.coords + offset);
1716 mesh.vertices.push(Vertex::new(pos));
1717 }
1718 }
1719
1720 mesh.vertices.push(Vertex::new(*p0));
1722 mesh.vertices.push(Vertex::new(*p1));
1723
1724 let start_center = (2 * segments) as u32;
1725 let end_center = start_center + 1;
1726 let seg = segments as u32;
1727
1728 for i in 0..seg {
1730 let i0 = i;
1731 let i1 = (i + 1) % seg;
1732 let i2 = seg + i;
1733 let i3 = seg + (i + 1) % seg;
1734
1735 mesh.faces.push([i0, i2, i1]);
1736 mesh.faces.push([i1, i2, i3]);
1737 }
1738
1739 for i in 0..seg {
1741 let i0 = i;
1742 let i1 = (i + 1) % seg;
1743 mesh.faces.push([start_center, i1, i0]); let i2 = seg + i;
1746 let i3 = seg + (i + 1) % seg;
1747 mesh.faces.push([end_center, i2, i3]); }
1749
1750 mesh
1751}
1752
1753fn generate_triangular_prism(
1755 v0: &Point3<f64>,
1756 v1: &Point3<f64>,
1757 v2: &Point3<f64>,
1758 normal: &Vector3<f64>,
1759 height: f64,
1760) -> Mesh {
1761 let mut mesh = Mesh::new();
1762 let offset = normal * height;
1763
1764 mesh.vertices.push(Vertex::new(*v0));
1766 mesh.vertices.push(Vertex::new(*v1));
1767 mesh.vertices.push(Vertex::new(*v2));
1768
1769 mesh.vertices
1771 .push(Vertex::new(Point3::from(v0.coords + offset)));
1772 mesh.vertices
1773 .push(Vertex::new(Point3::from(v1.coords + offset)));
1774 mesh.vertices
1775 .push(Vertex::new(Point3::from(v2.coords + offset)));
1776
1777 mesh.faces.push([0, 2, 1]);
1779
1780 mesh.faces.push([3, 4, 5]);
1782
1783 mesh.faces.push([0, 1, 4]);
1786 mesh.faces.push([0, 4, 3]);
1787
1788 mesh.faces.push([1, 2, 5]);
1790 mesh.faces.push([1, 5, 4]);
1791
1792 mesh.faces.push([2, 0, 3]);
1794 mesh.faces.push([2, 3, 5]);
1795
1796 mesh
1797}
1798
1799impl Mesh {
1801 pub fn boolean_union(&self, other: &Mesh) -> MeshResult<Mesh> {
1803 let result = boolean_operation(self, other, BooleanOp::Union, &BooleanParams::default())?;
1804 Ok(result.mesh)
1805 }
1806
1807 pub fn boolean_difference(&self, other: &Mesh) -> MeshResult<Mesh> {
1809 let result = boolean_operation(
1810 self,
1811 other,
1812 BooleanOp::Difference,
1813 &BooleanParams::default(),
1814 )?;
1815 Ok(result.mesh)
1816 }
1817
1818 pub fn boolean_intersection(&self, other: &Mesh) -> MeshResult<Mesh> {
1820 let result = boolean_operation(
1821 self,
1822 other,
1823 BooleanOp::Intersection,
1824 &BooleanParams::default(),
1825 )?;
1826 Ok(result.mesh)
1827 }
1828
1829 pub fn boolean_with_params(
1831 &self,
1832 other: &Mesh,
1833 operation: BooleanOp,
1834 params: &BooleanParams,
1835 ) -> MeshResult<BooleanResult> {
1836 boolean_operation(self, other, operation, params)
1837 }
1838
1839 pub fn offset_boolean(&self, offset: f64) -> MeshResult<Mesh> {
1853 let params = BooleanOffsetParams::with_offset(offset);
1854 let result = offset_boolean(self, ¶ms)?;
1855 Ok(result.mesh)
1856 }
1857
1858 pub fn offset_boolean_with_params(
1860 &self,
1861 params: &BooleanOffsetParams,
1862 ) -> MeshResult<BooleanOffsetResult> {
1863 offset_boolean(self, params)
1864 }
1865
1866 pub fn shell_boolean(&self, thickness: f64) -> MeshResult<Mesh> {
1879 shell_boolean(self, thickness, None)
1880 }
1881}
1882
1883pub fn boolean_operation_with_progress(
1923 mesh_a: &Mesh,
1924 mesh_b: &Mesh,
1925 operation: BooleanOp,
1926 params: &BooleanParams,
1927 callback: Option<&crate::progress::ProgressCallback>,
1928) -> MeshResult<BooleanResult> {
1929 use crate::progress::ProgressTracker;
1930
1931 let tracker = ProgressTracker::new(100);
1933
1934 if mesh_a.vertices.is_empty() || mesh_a.faces.is_empty() {
1936 return Err(MeshError::EmptyMesh {
1937 details: "Mesh A is empty".to_string(),
1938 });
1939 }
1940 if mesh_b.vertices.is_empty() || mesh_b.faces.is_empty() {
1941 return Err(MeshError::EmptyMesh {
1942 details: "Mesh B is empty".to_string(),
1943 });
1944 }
1945
1946 tracker.set(5);
1948 if !tracker.maybe_callback(callback, "Computing bounding boxes".to_string()) {
1949 return Ok(BooleanResult {
1950 mesh: Mesh::new(),
1951 intersection_edge_count: 0,
1952 new_vertex_count: 0,
1953 had_coplanar_faces: false,
1954 stats: BooleanStats::default(),
1955 });
1956 }
1957
1958 let bbox_a = compute_bbox(mesh_a);
1959 let bbox_b = compute_bbox(mesh_b);
1960
1961 if !bboxes_overlap(&bbox_a, &bbox_b) {
1962 return Ok(handle_non_overlapping(mesh_a, mesh_b, operation));
1963 }
1964
1965 tracker.set(10);
1967 if !tracker.maybe_callback(
1968 callback,
1969 "Building BVH and detecting intersections".to_string(),
1970 ) {
1971 return Ok(BooleanResult {
1972 mesh: Mesh::new(),
1973 intersection_edge_count: 0,
1974 new_vertex_count: 0,
1975 had_coplanar_faces: false,
1976 stats: BooleanStats::default(),
1977 });
1978 }
1979
1980 let intersections = find_mesh_intersections(mesh_a, mesh_b, params.tolerance);
1981
1982 tracker.set(40);
1983 if !tracker.maybe_callback(
1984 callback,
1985 format!("Found {} intersection pairs", intersections.len()),
1986 ) {
1987 return Ok(BooleanResult {
1988 mesh: Mesh::new(),
1989 intersection_edge_count: intersections.len(),
1990 new_vertex_count: 0,
1991 had_coplanar_faces: false,
1992 stats: BooleanStats::default(),
1993 });
1994 }
1995
1996 if intersections.is_empty() {
1997 return Ok(handle_non_intersecting(mesh_a, mesh_b, operation));
1998 }
1999
2000 let coplanar_count = intersections
2002 .iter()
2003 .filter(|i| i.coplanarity != CoplanarityResult::NotCoplanar)
2004 .count();
2005
2006 let coplanar_faces_a: HashSet<usize> = intersections
2008 .iter()
2009 .filter(|i| i.coplanarity != CoplanarityResult::NotCoplanar)
2010 .map(|i| i.tri_a)
2011 .collect();
2012
2013 let coplanar_faces_b: HashSet<usize> = intersections
2014 .iter()
2015 .filter(|i| i.coplanarity != CoplanarityResult::NotCoplanar)
2016 .map(|i| i.tri_b)
2017 .collect();
2018
2019 tracker.set(50);
2021 if !tracker.maybe_callback(callback, "Classifying mesh A faces".to_string()) {
2022 return Ok(BooleanResult {
2023 mesh: Mesh::new(),
2024 intersection_edge_count: intersections.len(),
2025 new_vertex_count: 0,
2026 had_coplanar_faces: coplanar_count > 0,
2027 stats: BooleanStats::default(),
2028 });
2029 }
2030
2031 let a_classifications = classify_faces(mesh_a, mesh_b, params);
2032
2033 tracker.set(70);
2034 if !tracker.maybe_callback(callback, "Classifying mesh B faces".to_string()) {
2035 return Ok(BooleanResult {
2036 mesh: Mesh::new(),
2037 intersection_edge_count: intersections.len(),
2038 new_vertex_count: 0,
2039 had_coplanar_faces: coplanar_count > 0,
2040 stats: BooleanStats::default(),
2041 });
2042 }
2043
2044 let b_classifications = classify_faces(mesh_b, mesh_a, params);
2045
2046 tracker.set(80);
2048 if !tracker.maybe_callback(callback, "Building result mesh".to_string()) {
2049 return Ok(BooleanResult {
2050 mesh: Mesh::new(),
2051 intersection_edge_count: intersections.len(),
2052 new_vertex_count: 0,
2053 had_coplanar_faces: coplanar_count > 0,
2054 stats: BooleanStats::default(),
2055 });
2056 }
2057
2058 let mut result = Mesh::new();
2059 let mut stats = BooleanStats {
2060 coplanar_pairs: coplanar_count,
2061 ..Default::default()
2062 };
2063
2064 match operation {
2065 BooleanOp::Union => {
2066 add_faces_with_classification_and_coplanar(
2067 &mut result,
2068 mesh_a,
2069 &a_classifications,
2070 FaceLocation::Outside,
2071 &coplanar_faces_a,
2072 params.coplanar_strategy,
2073 true,
2074 );
2075 stats.faces_from_a = result.faces.len();
2076
2077 add_faces_with_classification_and_coplanar(
2078 &mut result,
2079 mesh_b,
2080 &b_classifications,
2081 FaceLocation::Outside,
2082 &coplanar_faces_b,
2083 params.coplanar_strategy,
2084 false,
2085 );
2086 stats.faces_from_b = result.faces.len() - stats.faces_from_a;
2087 }
2088
2089 BooleanOp::Difference => {
2090 add_faces_with_classification_and_coplanar(
2091 &mut result,
2092 mesh_a,
2093 &a_classifications,
2094 FaceLocation::Outside,
2095 &coplanar_faces_a,
2096 params.coplanar_strategy,
2097 true,
2098 );
2099 stats.faces_from_a = result.faces.len();
2100
2101 add_faces_inverted_with_coplanar(
2102 &mut result,
2103 mesh_b,
2104 &b_classifications,
2105 FaceLocation::Inside,
2106 &coplanar_faces_b,
2107 params.coplanar_strategy,
2108 false,
2109 );
2110 stats.faces_from_b = result.faces.len() - stats.faces_from_a;
2111 }
2112
2113 BooleanOp::Intersection => {
2114 add_faces_with_classification_and_coplanar(
2115 &mut result,
2116 mesh_a,
2117 &a_classifications,
2118 FaceLocation::Inside,
2119 &coplanar_faces_a,
2120 params.coplanar_strategy,
2121 true,
2122 );
2123 stats.faces_from_a = result.faces.len();
2124
2125 add_faces_with_classification_and_coplanar(
2126 &mut result,
2127 mesh_b,
2128 &b_classifications,
2129 FaceLocation::Inside,
2130 &coplanar_faces_b,
2131 params.coplanar_strategy,
2132 false,
2133 );
2134 stats.faces_from_b = result.faces.len() - stats.faces_from_a;
2135 }
2136 }
2137
2138 if params.cleanup {
2140 tracker.set(90);
2141 if !tracker.maybe_callback(callback, "Cleaning up result mesh".to_string()) {
2142 return Ok(BooleanResult {
2143 mesh: result,
2144 intersection_edge_count: intersections.len(),
2145 new_vertex_count: 0,
2146 had_coplanar_faces: coplanar_count > 0,
2147 stats,
2148 });
2149 }
2150
2151 weld_vertices(&mut result, params.tolerance);
2152 let non_manifold_fixed = fix_non_manifold_edges(&mut result);
2153 stats.non_manifold_edges_fixed = non_manifold_fixed;
2154 }
2155
2156 tracker.set(100);
2157 let _ = tracker.maybe_callback(callback, "Boolean operation complete".to_string());
2158
2159 Ok(BooleanResult {
2160 mesh: result,
2161 intersection_edge_count: intersections.len(),
2162 new_vertex_count: 0,
2163 had_coplanar_faces: coplanar_count > 0,
2164 stats,
2165 })
2166}
2167
2168#[cfg(test)]
2169mod tests {
2170 use super::*;
2171
2172 fn create_cube(center: Point3<f64>, size: f64) -> Mesh {
2173 let half = size / 2.0;
2174 let mut mesh = Mesh::new();
2175
2176 let vertices = [
2178 Point3::new(center.x - half, center.y - half, center.z - half),
2179 Point3::new(center.x + half, center.y - half, center.z - half),
2180 Point3::new(center.x + half, center.y + half, center.z - half),
2181 Point3::new(center.x - half, center.y + half, center.z - half),
2182 Point3::new(center.x - half, center.y - half, center.z + half),
2183 Point3::new(center.x + half, center.y - half, center.z + half),
2184 Point3::new(center.x + half, center.y + half, center.z + half),
2185 Point3::new(center.x - half, center.y + half, center.z + half),
2186 ];
2187
2188 for v in &vertices {
2189 mesh.vertices.push(Vertex::new(*v));
2190 }
2191
2192 let faces = [
2194 [0, 1, 2],
2196 [0, 2, 3],
2197 [5, 4, 7],
2199 [5, 7, 6],
2200 [3, 2, 6],
2202 [3, 6, 7],
2203 [4, 5, 1],
2205 [4, 1, 0],
2206 [4, 0, 3],
2208 [4, 3, 7],
2209 [1, 5, 6],
2211 [1, 6, 2],
2212 ];
2213
2214 for f in &faces {
2215 mesh.faces.push(*f);
2216 }
2217
2218 mesh
2219 }
2220
2221 #[test]
2222 fn test_non_overlapping_union() {
2223 let cube_a = create_cube(Point3::new(0.0, 0.0, 0.0), 1.0);
2224 let cube_b = create_cube(Point3::new(10.0, 0.0, 0.0), 1.0);
2225
2226 let result = boolean_operation(
2227 &cube_a,
2228 &cube_b,
2229 BooleanOp::Union,
2230 &BooleanParams::default(),
2231 )
2232 .unwrap();
2233
2234 assert_eq!(result.mesh.vertices.len(), 16); assert_eq!(result.mesh.faces.len(), 24); }
2237
2238 #[test]
2239 fn test_non_overlapping_difference() {
2240 let cube_a = create_cube(Point3::new(0.0, 0.0, 0.0), 1.0);
2241 let cube_b = create_cube(Point3::new(10.0, 0.0, 0.0), 1.0);
2242
2243 let result = boolean_operation(
2244 &cube_a,
2245 &cube_b,
2246 BooleanOp::Difference,
2247 &BooleanParams::default(),
2248 )
2249 .unwrap();
2250
2251 assert_eq!(result.mesh.vertices.len(), 8); assert_eq!(result.mesh.faces.len(), 12);
2253 }
2254
2255 #[test]
2256 fn test_non_overlapping_intersection() {
2257 let cube_a = create_cube(Point3::new(0.0, 0.0, 0.0), 1.0);
2258 let cube_b = create_cube(Point3::new(10.0, 0.0, 0.0), 1.0);
2259
2260 let result = boolean_operation(
2261 &cube_a,
2262 &cube_b,
2263 BooleanOp::Intersection,
2264 &BooleanParams::default(),
2265 )
2266 .unwrap();
2267
2268 assert!(result.mesh.vertices.is_empty()); assert!(result.mesh.faces.is_empty());
2270 }
2271
2272 #[test]
2273 fn test_overlapping_union() {
2274 let cube_a = create_cube(Point3::new(0.0, 0.0, 0.0), 2.0);
2275 let cube_b = create_cube(Point3::new(1.0, 0.0, 0.0), 2.0);
2276
2277 let result = boolean_operation(
2278 &cube_a,
2279 &cube_b,
2280 BooleanOp::Union,
2281 &BooleanParams::default(),
2282 )
2283 .unwrap();
2284
2285 assert!(!result.mesh.faces.is_empty());
2287 }
2288
2289 #[test]
2290 fn test_empty_mesh_error() {
2291 let empty = Mesh::new();
2292 let cube = create_cube(Point3::origin(), 1.0);
2293
2294 let result = boolean_operation(&empty, &cube, BooleanOp::Union, &BooleanParams::default());
2295 assert!(result.is_err());
2296 }
2297
2298 #[test]
2299 fn test_mesh_boolean_methods() {
2300 let cube_a = create_cube(Point3::new(0.0, 0.0, 0.0), 1.0);
2301 let cube_b = create_cube(Point3::new(10.0, 0.0, 0.0), 1.0);
2302
2303 let union = cube_a.boolean_union(&cube_b).unwrap();
2304 assert_eq!(union.faces.len(), 24);
2305
2306 let diff = cube_a.boolean_difference(&cube_b).unwrap();
2307 assert_eq!(diff.faces.len(), 12);
2308
2309 let inter = cube_a.boolean_intersection(&cube_b).unwrap();
2310 assert!(inter.faces.is_empty());
2311 }
2312
2313 #[test]
2314 fn test_point_inside_mesh() {
2315 let cube = create_cube(Point3::origin(), 2.0);
2316
2317 assert!(!is_point_inside_mesh(&Point3::new(10.0, 0.0, 0.0), &cube));
2319 }
2323
2324 #[test]
2325 fn test_params_presets() {
2326 let scan_params = BooleanParams::for_scans();
2327 assert!(scan_params.tolerance > 1e-8);
2328
2329 let cad_params = BooleanParams::for_cad();
2330 assert!(cad_params.tolerance < 1e-8);
2331 }
2332
2333 #[test]
2334 fn test_coplanar_detection() {
2335 let a0 = Point3::new(0.0, 0.0, 0.0);
2337 let a1 = Point3::new(1.0, 0.0, 0.0);
2338 let a2 = Point3::new(0.5, 1.0, 0.0);
2339
2340 let b0 = Point3::new(0.5, 0.5, 0.0);
2341 let b1 = Point3::new(1.5, 0.5, 0.0);
2342 let b2 = Point3::new(1.0, 1.5, 0.0);
2343
2344 let result = check_coplanarity(&a0, &a1, &a2, &b0, &b1, &b2, 1e-8);
2345 assert_eq!(result, CoplanarityResult::CoplanarSameOrientation);
2346
2347 let b0_flip = Point3::new(0.5, 0.5, 0.0);
2349 let b1_flip = Point3::new(1.0, 1.5, 0.0);
2350 let b2_flip = Point3::new(1.5, 0.5, 0.0);
2351
2352 let result = check_coplanarity(&a0, &a1, &a2, &b0_flip, &b1_flip, &b2_flip, 1e-8);
2353 assert_eq!(result, CoplanarityResult::CoplanarOppositeOrientation);
2354 }
2355
2356 #[test]
2357 fn test_not_coplanar() {
2358 let a0 = Point3::new(0.0, 0.0, 0.0);
2359 let a1 = Point3::new(1.0, 0.0, 0.0);
2360 let a2 = Point3::new(0.5, 1.0, 0.0);
2361
2362 let b0 = Point3::new(0.0, 0.0, 1.0);
2364 let b1 = Point3::new(1.0, 0.0, 1.0);
2365 let b2 = Point3::new(0.5, 1.0, 1.0);
2366
2367 let result = check_coplanarity(&a0, &a1, &a2, &b0, &b1, &b2, 1e-8);
2368 assert_eq!(result, CoplanarityResult::NotCoplanar);
2369 }
2370
2371 #[test]
2372 fn test_coplanar_cubes_union() {
2373 let cube_a = create_cube(Point3::new(0.0, 0.0, 0.0), 2.0);
2375 let cube_b = create_cube(Point3::new(2.0, 0.0, 0.0), 2.0); let result = boolean_operation(
2378 &cube_a,
2379 &cube_b,
2380 BooleanOp::Union,
2381 &BooleanParams::default(),
2382 )
2383 .unwrap();
2384
2385 assert!(!result.mesh.faces.is_empty());
2388 }
2389
2390 #[test]
2391 fn test_coplanar_strategy_exclude() {
2392 let cube_a = create_cube(Point3::new(0.0, 0.0, 0.0), 2.0);
2393 let cube_b = create_cube(Point3::new(2.0, 0.0, 0.0), 2.0);
2394
2395 let params = BooleanParams {
2396 coplanar_strategy: CoplanarStrategy::Exclude,
2397 ..Default::default()
2398 };
2399
2400 let result = boolean_operation(&cube_a, &cube_b, BooleanOp::Union, ¶ms).unwrap();
2401
2402 assert!(!result.mesh.faces.is_empty());
2404 }
2405
2406 #[test]
2407 fn test_bvh_construction() {
2408 let cube = create_cube(Point3::origin(), 2.0);
2409 let bvh = Bvh::build(&cube, 4);
2410
2411 assert!(bvh.root.is_some());
2412
2413 let query_bbox = Aabb::from_triangle(
2415 &Point3::new(-0.5, -0.5, -0.5),
2416 &Point3::new(0.5, -0.5, -0.5),
2417 &Point3::new(0.0, 0.5, -0.5),
2418 );
2419
2420 let candidates = bvh.query(&query_bbox, 1e-8);
2421 assert!(!candidates.is_empty());
2423 }
2424
2425 #[test]
2426 fn test_non_manifold_fix() {
2427 let mut mesh = Mesh::new();
2428
2429 for i in 0..6 {
2431 mesh.vertices
2432 .push(Vertex::new(Point3::new(i as f64, 0.0, 0.0)));
2433 }
2434
2435 mesh.faces.push([0, 1, 2]);
2437 mesh.faces.push([0, 1, 3]);
2438 mesh.faces.push([0, 1, 4]); let fixed = fix_non_manifold_edges(&mut mesh);
2441
2442 assert!(fixed > 0);
2444 assert_eq!(mesh.faces.len(), 2); }
2446
2447 #[test]
2448 fn test_boolean_result_stats() {
2449 let cube_a = create_cube(Point3::new(0.0, 0.0, 0.0), 2.0);
2450 let cube_b = create_cube(Point3::new(1.0, 0.0, 0.0), 2.0);
2451
2452 let result = boolean_operation(
2453 &cube_a,
2454 &cube_b,
2455 BooleanOp::Union,
2456 &BooleanParams::default(),
2457 )
2458 .unwrap();
2459
2460 assert!(result.stats.faces_from_a > 0 || result.stats.faces_from_b > 0);
2462 }
2463
2464 #[test]
2465 fn test_triangles_overlap_2d() {
2466 let a0 = [0.0, 0.0];
2468 let a1 = [2.0, 0.0];
2469 let a2 = [1.0, 2.0];
2470
2471 let b0 = [1.0, 0.0];
2472 let b1 = [3.0, 0.0];
2473 let b2 = [2.0, 2.0];
2474
2475 assert!(triangles_overlap_2d(&a0, &a1, &a2, &b0, &b1, &b2));
2476
2477 let c0 = [10.0, 0.0];
2479 let c1 = [12.0, 0.0];
2480 let c2 = [11.0, 2.0];
2481
2482 assert!(!triangles_overlap_2d(&a0, &a1, &a2, &c0, &c1, &c2));
2483 }
2484
2485 #[test]
2490 fn test_boolean_offset_params_default() {
2491 let params = BooleanOffsetParams::default();
2492 assert!((params.offset - 1.0).abs() < 1e-10);
2493 assert_eq!(params.sphere_segments, 8);
2494 assert_eq!(params.cylinder_segments, 8);
2495 assert!(params.cleanup);
2496 }
2497
2498 #[test]
2499 fn test_boolean_offset_params_presets() {
2500 let high_quality = BooleanOffsetParams::high_quality(2.0);
2501 assert!((high_quality.offset - 2.0).abs() < 1e-10);
2502 assert_eq!(high_quality.sphere_segments, 16);
2503 assert_eq!(high_quality.cylinder_segments, 16);
2504
2505 let fast = BooleanOffsetParams::fast_preview(1.5);
2506 assert!((fast.offset - 1.5).abs() < 1e-10);
2507 assert_eq!(fast.sphere_segments, 4);
2508 assert_eq!(fast.cylinder_segments, 4);
2509 }
2510
2511 #[test]
2512 fn test_generate_sphere() {
2513 let center = Point3::new(5.0, 5.0, 5.0);
2514 let sphere = generate_sphere(¢er, 2.0, 8);
2515
2516 assert!(!sphere.vertices.is_empty());
2518 assert!(!sphere.faces.is_empty());
2519
2520 for vertex in &sphere.vertices {
2522 let dist = (vertex.position - center).norm();
2523 assert!(
2524 (dist - 2.0).abs() < 1e-6,
2525 "Vertex at distance {} from center, expected 2.0",
2526 dist
2527 );
2528 }
2529 }
2530
2531 #[test]
2532 fn test_generate_cylinder() {
2533 let p0 = Point3::new(0.0, 0.0, 0.0);
2534 let p1 = Point3::new(10.0, 0.0, 0.0);
2535 let cylinder = generate_cylinder(&p0, &p1, 1.0, 8);
2536
2537 assert!(!cylinder.vertices.is_empty());
2539 assert!(!cylinder.faces.is_empty());
2540
2541 let mut min_x = f64::MAX;
2543 let mut max_x = f64::MIN;
2544 for vertex in &cylinder.vertices {
2545 min_x = min_x.min(vertex.position.x);
2546 max_x = max_x.max(vertex.position.x);
2547 }
2548 assert!(min_x < 0.5, "Cylinder should start near x=0");
2549 assert!(max_x > 9.5, "Cylinder should extend to near x=10");
2550 }
2551
2552 #[test]
2553 fn test_generate_triangular_prism() {
2554 let v0 = Point3::new(0.0, 0.0, 0.0);
2555 let v1 = Point3::new(1.0, 0.0, 0.0);
2556 let v2 = Point3::new(0.5, 1.0, 0.0);
2557 let normal = Vector3::new(0.0, 0.0, 1.0);
2558
2559 let prism = generate_triangular_prism(&v0, &v1, &v2, &normal, 2.0);
2560
2561 assert_eq!(prism.vertices.len(), 6);
2563
2564 assert_eq!(prism.faces.len(), 8);
2566
2567 let mut min_z = f64::MAX;
2569 let mut max_z = f64::MIN;
2570 for vertex in &prism.vertices {
2571 min_z = min_z.min(vertex.position.z);
2572 max_z = max_z.max(vertex.position.z);
2573 }
2574 assert!(
2575 (max_z - min_z - 2.0).abs() < 1e-6,
2576 "Prism height should be 2.0"
2577 );
2578 }
2579
2580 #[test]
2581 fn test_collect_unique_edges() {
2582 let cube = create_cube(Point3::origin(), 2.0);
2583 let edges = collect_unique_edges(&cube);
2584
2585 assert_eq!(
2589 edges.len(),
2590 18,
2591 "Triangulated cube should have 18 unique edges, got {}",
2592 edges.len()
2593 );
2594 }
2595
2596 #[test]
2597 fn test_offset_boolean_cube() {
2598 let cube = create_cube(Point3::origin(), 2.0);
2599
2600 let params = BooleanOffsetParams::fast_preview(0.5);
2601 let result = offset_boolean(&cube, ¶ms).unwrap();
2602
2603 assert!(!result.mesh.vertices.is_empty());
2605 assert!(!result.mesh.faces.is_empty());
2606
2607 assert_eq!(result.sphere_count, 8, "Cube has 8 vertices -> 8 spheres");
2609 assert_eq!(
2610 result.cylinder_count, 18,
2611 "Triangulated cube has 18 edges -> 18 cylinders"
2612 );
2613 assert_eq!(
2614 result.prism_count, 12,
2615 "Cube has 12 triangular faces -> 12 prisms"
2616 );
2617
2618 let result_bounds = compute_bbox(&result.mesh);
2620 let original_bounds = compute_bbox(&cube);
2621
2622 let result_size = (result_bounds.1 - result_bounds.0).norm();
2623 let original_size = (original_bounds.1 - original_bounds.0).norm();
2624
2625 assert!(result_size > original_size, "Offset mesh should be larger");
2626 }
2627
2628 #[test]
2629 fn test_offset_boolean_zero_offset() {
2630 let cube = create_cube(Point3::origin(), 2.0);
2631
2632 let params = BooleanOffsetParams::with_offset(0.0);
2633 let result = offset_boolean(&cube, ¶ms).unwrap();
2634
2635 assert_eq!(result.mesh.vertices.len(), cube.vertices.len());
2637 assert_eq!(result.mesh.faces.len(), cube.faces.len());
2638 assert_eq!(result.sphere_count, 0);
2639 }
2640
2641 #[test]
2642 fn test_offset_boolean_empty_mesh() {
2643 let empty = Mesh::new();
2644 let params = BooleanOffsetParams::default();
2645
2646 let result = offset_boolean(&empty, ¶ms);
2647 assert!(result.is_err());
2648 }
2649
2650 #[test]
2651 fn test_mesh_offset_boolean_method() {
2652 let cube = create_cube(Point3::origin(), 2.0);
2653
2654 let offset_mesh = cube.offset_boolean(1.0).unwrap();
2655
2656 assert!(!offset_mesh.vertices.is_empty());
2658 assert!(!offset_mesh.faces.is_empty());
2659 }
2660
2661 #[test]
2662 fn test_shell_boolean() {
2663 let cube = create_cube(Point3::origin(), 4.0);
2664
2665 let shell = shell_boolean(&cube, 1.0, None).unwrap();
2666
2667 assert!(!shell.vertices.is_empty());
2669 assert!(!shell.faces.is_empty());
2670 }
2671
2672 #[test]
2673 fn test_shell_boolean_invalid_thickness() {
2674 let cube = create_cube(Point3::origin(), 4.0);
2675
2676 let result = shell_boolean(&cube, 0.0, None);
2677 assert!(result.is_err());
2678
2679 let result_neg = shell_boolean(&cube, -1.0, None);
2680 assert!(result_neg.is_err());
2681 }
2682
2683 #[test]
2684 fn test_mesh_shell_boolean_method() {
2685 let cube = create_cube(Point3::origin(), 4.0);
2686
2687 let shell = cube.shell_boolean(1.0).unwrap();
2688
2689 assert!(!shell.vertices.is_empty());
2691 assert!(!shell.faces.is_empty());
2692 }
2693
2694 #[test]
2695 fn test_compute_vertex_normals() {
2696 let cube = create_cube(Point3::origin(), 2.0);
2697
2698 let normals = compute_vertex_normals(&cube);
2699
2700 assert_eq!(normals.len(), cube.vertices.len());
2702
2703 for normal in &normals {
2705 let len = normal.norm();
2706 assert!(
2707 len < 1e-6 || (len - 1.0).abs() < 1e-6,
2708 "Normal should be unit or zero, got {}",
2709 len
2710 );
2711 }
2712 }
2713}