1use std::collections::{HashMap, HashSet};
7
8use brepkit_math::tolerance::Tolerance;
9use brepkit_math::vec::{Point3, Vec3};
10use brepkit_topology::Topology;
11use brepkit_topology::edge::{Edge, EdgeId};
12use brepkit_topology::face::{Face, FaceId, FaceSurface};
13use brepkit_topology::shell::Shell;
14use brepkit_topology::solid::SolidId;
15use brepkit_topology::vertex::VertexId;
16use brepkit_topology::wire::{OrientedEdge, Wire};
17
18#[derive(Debug, Clone)]
20pub struct RepairReport {
21 pub before: crate::validate::ValidationReport,
23 pub healing: HealingReport,
25 pub after: crate::validate::ValidationReport,
27}
28
29impl RepairReport {
30 #[must_use]
32 pub fn is_valid_after(&self) -> bool {
33 self.after.is_valid()
34 }
35
36 #[must_use]
38 pub fn total_repairs(&self) -> usize {
39 self.healing.vertices_merged
40 + self.healing.degenerate_edges_removed
41 + self.healing.orientations_fixed
42 + self.healing.wire_gaps_closed
43 + self.healing.small_faces_removed
44 + self.healing.duplicate_faces_removed
45 }
46}
47
48pub fn repair_solid(
58 topo: &mut Topology,
59 solid: SolidId,
60 tolerance: f64,
61) -> Result<RepairReport, crate::OperationsError> {
62 let before = crate::validate::validate_solid(topo, solid)?;
63 let healing = heal_solid(topo, solid, tolerance)?;
64 let after = crate::validate::validate_solid(topo, solid)?;
65
66 Ok(RepairReport {
67 before,
68 healing,
69 after,
70 })
71}
72
73#[derive(Debug, Default, Clone)]
75pub struct HealingReport {
76 pub vertices_merged: usize,
78 pub degenerate_edges_removed: usize,
80 pub orientations_fixed: usize,
82 pub wire_gaps_closed: usize,
84 pub small_faces_removed: usize,
86 pub duplicate_faces_removed: usize,
88}
89
90pub fn heal_solid(
100 topo: &mut Topology,
101 solid: SolidId,
102 tolerance: f64,
103) -> Result<HealingReport, crate::OperationsError> {
104 let wire_gaps_closed = close_wire_gaps(topo, solid, tolerance)?;
106 let vertices_merged = merge_coincident_vertices(topo, solid, tolerance)?;
107 let degenerate_edges_removed = remove_degenerate_edges(topo, solid, tolerance)?;
108 let small_faces_removed = remove_small_faces(topo, solid, tolerance)?;
109 let duplicate_faces_removed = remove_duplicate_faces(topo, solid, tolerance)?;
110 let orientations_fixed = fix_face_orientations(topo, solid)?;
112
113 Ok(HealingReport {
114 vertices_merged,
115 degenerate_edges_removed,
116 orientations_fixed,
117 wire_gaps_closed,
118 small_faces_removed,
119 duplicate_faces_removed,
120 })
121}
122
123pub fn merge_coincident_vertices(
135 topo: &mut Topology,
136 solid: SolidId,
137 tolerance: f64,
138) -> Result<usize, crate::OperationsError> {
139 let tol = if tolerance > 0.0 {
140 tolerance
141 } else {
142 Tolerance::new().linear
143 };
144 let tol_sq = tol * tol;
145
146 let solid_data = topo.solid(solid)?;
147 let shell = topo.shell(solid_data.outer_shell())?;
148 let face_ids: Vec<_> = shell.faces().to_vec();
149
150 let mut vertex_ids: Vec<VertexId> = Vec::new();
151 let mut positions: Vec<Point3> = Vec::new();
152 let mut seen = std::collections::HashSet::new();
153
154 for &fid in &face_ids {
155 let face = topo.face(fid)?;
156 let wire = topo.wire(face.outer_wire())?;
157 for oe in wire.edges() {
158 let edge = topo.edge(oe.edge())?;
159 for &vid in &[edge.start(), edge.end()] {
160 if seen.insert(vid.index()) {
161 let point = topo.vertex(vid)?.point();
162 vertex_ids.push(vid);
163 positions.push(point);
164 }
165 }
166 }
167 }
168
169 let num_verts = vertex_ids.len();
172 let mut merge_to: HashMap<usize, VertexId> = HashMap::new();
173 let mut merged_count = 0;
174
175 for i in 0..num_verts {
176 if merge_to.contains_key(&vertex_ids[i].index()) {
177 continue;
178 }
179 for j in (i + 1)..num_verts {
180 if merge_to.contains_key(&vertex_ids[j].index()) {
181 continue;
182 }
183 let dist_sq = (positions[i] - positions[j]).length_squared();
184 if dist_sq < tol_sq {
185 merge_to.insert(vertex_ids[j].index(), vertex_ids[i]);
186 merged_count += 1;
187 }
188 }
189 }
190
191 if merged_count == 0 {
192 return Ok(0);
193 }
194
195 let mut edge_ids = Vec::new();
196 for &fid in &face_ids {
197 let face = topo.face(fid)?;
198 let wire = topo.wire(face.outer_wire())?;
199 for oe in wire.edges() {
200 edge_ids.push(oe.edge());
201 }
202 }
203 edge_ids.sort_by_key(|e| e.index());
204 edge_ids.dedup_by_key(|e| e.index());
205
206 let updates: Vec<_> = edge_ids
207 .iter()
208 .filter_map(|&eid| {
209 let edge = topo.edge(eid).ok()?;
210 let new_start = merge_to
211 .get(&edge.start().index())
212 .copied()
213 .unwrap_or_else(|| edge.start());
214 let new_end = merge_to
215 .get(&edge.end().index())
216 .copied()
217 .unwrap_or_else(|| edge.end());
218 if new_start != edge.start() || new_end != edge.end() {
219 Some((eid, new_start, new_end))
220 } else {
221 None
222 }
223 })
224 .collect();
225
226 for (eid, new_start, new_end) in updates {
227 let edge = topo.edge_mut(eid)?;
228 *edge = brepkit_topology::edge::Edge::new(new_start, new_end, edge.curve().clone());
229 }
230
231 Ok(merged_count)
232}
233
234pub fn remove_degenerate_edges(
245 topo: &mut Topology,
246 solid: SolidId,
247 tolerance: f64,
248) -> Result<usize, crate::OperationsError> {
249 let tol = if tolerance > 0.0 {
250 tolerance
251 } else {
252 Tolerance::new().linear
253 };
254 let tol_sq = tol * tol;
255
256 let solid_data = topo.solid(solid)?;
257 let shell = topo.shell(solid_data.outer_shell())?;
258 let face_ids: Vec<_> = shell.faces().to_vec();
259
260 let mut removed_count = 0;
261
262 for &fid in &face_ids {
263 let face = topo.face(fid)?;
264 let wire_id = face.outer_wire();
265 let wire = topo.wire(wire_id)?;
266
267 let mut new_edges = Vec::new();
268 let mut any_removed = false;
269
270 for oe in wire.edges() {
271 let edge = topo.edge(oe.edge())?;
272 let start_pos = topo.vertex(edge.start())?.point();
273 let end_pos = topo.vertex(edge.end())?.point();
274 let len_sq = (end_pos - start_pos).length_squared();
275
276 if len_sq < tol_sq && edge.start() != edge.end() {
277 any_removed = true;
278 removed_count += 1;
279 } else {
280 new_edges.push(*oe);
281 }
282 }
283
284 if any_removed && !new_edges.is_empty() {
285 let new_wire = brepkit_topology::wire::Wire::new(new_edges, wire.is_closed())?;
290 let new_wire_id = topo.add_wire(new_wire);
291 let face = topo.face_mut(fid)?;
292 if face.outer_wire() == wire_id {
293 face.set_outer_wire(new_wire_id);
294 } else {
295 let iw = face.inner_wires().to_vec();
296 for (i, &iw_id) in iw.iter().enumerate() {
297 if iw_id == wire_id {
298 face.inner_wires_mut()[i] = new_wire_id;
299 }
300 }
301 }
302 }
303 }
304
305 Ok(removed_count)
306}
307
308pub fn remove_wire_spurs(
328 topo: &mut Topology,
329 solid: SolidId,
330) -> Result<usize, crate::OperationsError> {
331 let face_ids = brepkit_topology::explorer::solid_faces(topo, solid)?;
332 let mut removed = 0;
333
334 for fid in face_ids {
335 let wire_ids: Vec<_> = {
336 let face = topo.face(fid)?;
337 std::iter::once(face.outer_wire())
338 .chain(face.inner_wires().iter().copied())
339 .collect()
340 };
341
342 for wid in wire_ids {
343 let (mut oes, closed) = {
344 let wire = topo.wire(wid)?;
345 (wire.edges().to_vec(), wire.is_closed())
346 };
347
348 let n_removed = strip_wire_spurs(&mut oes);
349 if n_removed == 0 {
350 continue;
351 }
352 if oes.is_empty() {
360 continue;
361 }
362
363 let new_wire = Wire::new(oes, closed)?;
364 let new_wid = topo.add_wire(new_wire);
365 let face = topo.face_mut(fid)?;
366 if face.outer_wire() == wid {
367 face.set_outer_wire(new_wid);
368 } else {
369 let inner = face.inner_wires().to_vec();
370 for (i, &iwid) in inner.iter().enumerate() {
371 if iwid == wid {
372 face.inner_wires_mut()[i] = new_wid;
373 }
374 }
375 }
376 removed += n_removed;
377 }
378 }
379
380 Ok(removed)
381}
382
383fn strip_wire_spurs(oes: &mut Vec<OrientedEdge>) -> usize {
387 let mut removed = 0;
388 loop {
389 let n = oes.len();
390 if n < 2 {
391 break;
392 }
393 let spur = (0..n).find_map(|i| {
394 let j = (i + 1) % n;
395 (oes[i].edge() == oes[j].edge() && oes[i].is_forward() != oes[j].is_forward())
396 .then_some((i, j))
397 });
398 match spur {
399 Some((i, j)) => {
400 let (lo, hi) = if i < j { (i, j) } else { (j, i) };
401 oes.remove(hi);
402 oes.remove(lo);
403 removed += 2;
404 }
405 None => break,
406 }
407 }
408 removed
409}
410
411pub fn fix_face_orientations(
423 topo: &mut Topology,
424 solid: SolidId,
425) -> Result<usize, crate::OperationsError> {
426 let solid_data = topo.solid(solid)?;
427 let shell = topo.shell(solid_data.outer_shell())?;
428 let face_ids: Vec<_> = shell.faces().to_vec();
429
430 let mut center = Vec3::new(0.0, 0.0, 0.0);
431 let mut total_faces: usize = 0;
432
433 for &fid in &face_ids {
434 let face = topo.face(fid)?;
435 let wire = topo.wire(face.outer_wire())?;
436 let mut face_center = Vec3::new(0.0, 0.0, 0.0);
437 let edges = wire.edges();
438 for oe in edges {
439 let edge = topo.edge(oe.edge())?;
440 let pos = topo.vertex(edge.start())?.point();
441 face_center += Vec3::new(pos.x(), pos.y(), pos.z());
442 }
443
444 let vert_count = edges.len();
445 if vert_count > 0 {
446 #[allow(clippy::cast_precision_loss)]
447 let inv = 1.0 / vert_count as f64;
448 center += face_center * inv;
449 total_faces += 1;
450 }
451 }
452
453 if total_faces == 0 {
454 return Ok(0);
455 }
456
457 #[allow(clippy::cast_precision_loss)]
458 let inv_faces = 1.0 / total_faces as f64;
459 let center_pt = Point3::new(
460 center.x() * inv_faces,
461 center.y() * inv_faces,
462 center.z() * inv_faces,
463 );
464
465 let mut fixed_count = 0;
466 let mut faces_to_flip = Vec::new();
467
468 for &fid in &face_ids {
469 let face = topo.face(fid)?;
470 let wire = topo.wire(face.outer_wire())?;
471 let first_oe = match wire.edges().first() {
472 Some(oe) => oe,
473 None => continue,
474 };
475 let edge = topo.edge(first_oe.edge())?;
476 let face_point = topo.vertex(edge.start())?.point();
477 let to_face = face_point - center_pt;
478
479 match face.surface() {
480 FaceSurface::Plane { normal, d } => {
481 if normal.dot(to_face) < 0.0 {
482 faces_to_flip.push((fid, *normal, *d));
483 fixed_count += 1;
484 }
485 }
486 FaceSurface::Cylinder(cyl) => {
487 let to_pt = Vec3::new(
489 face_point.x() - cyl.origin().x(),
490 face_point.y() - cyl.origin().y(),
491 face_point.z() - cyl.origin().z(),
492 );
493 let h = to_pt.dot(cyl.axis());
494 let radial = to_pt - cyl.axis() * h;
495 if radial.dot(to_face) < 0.0 {
496 }
499 }
500 _ => {}
503 }
504 }
505
506 for (fid, normal, d) in faces_to_flip {
507 let face = topo.face_mut(fid)?;
508 face.set_surface(FaceSurface::Plane {
509 normal: -normal,
510 d: -d,
511 });
512 }
513
514 Ok(fixed_count)
515}
516
517pub fn close_wire_gaps(
529 topo: &mut Topology,
530 solid: SolidId,
531 tolerance: f64,
532) -> Result<usize, crate::OperationsError> {
533 let tol = if tolerance > 0.0 {
534 tolerance
535 } else {
536 Tolerance::new().linear
537 };
538 let tol_sq = tol * tol;
539
540 let solid_data = topo.solid(solid)?;
541 let shell = topo.shell(solid_data.outer_shell())?;
542 let face_ids: Vec<_> = shell.faces().to_vec();
543
544 let mut gaps_closed = 0;
545
546 for &fid in &face_ids {
547 let face = topo.face(fid)?;
548
549 let wire_ids: Vec<_> = std::iter::once(face.outer_wire())
550 .chain(face.inner_wires().iter().copied())
551 .collect();
552
553 for wire_id in wire_ids {
554 let wire = topo.wire(wire_id)?;
555 let edges_list: Vec<_> = wire.edges().to_vec();
556 let n_edges = edges_list.len();
557
558 if n_edges < 2 {
559 continue;
560 }
561
562 let mut merge_pairs: Vec<(VertexId, VertexId)> = Vec::new();
563
564 for i in 0..n_edges {
565 let next_i = (i + 1) % n_edges;
566
567 let edge_i = topo.edge(edges_list[i].edge())?;
568 let edge_next = topo.edge(edges_list[next_i].edge())?;
569
570 let end_vid = if edges_list[i].is_forward() {
571 edge_i.end()
572 } else {
573 edge_i.start()
574 };
575
576 let start_vid = if edges_list[next_i].is_forward() {
577 edge_next.start()
578 } else {
579 edge_next.end()
580 };
581
582 if end_vid == start_vid {
583 continue; }
585
586 let end_pos = topo.vertex(end_vid)?.point();
587 let start_pos = topo.vertex(start_vid)?.point();
588 let dist_sq = (end_pos - start_pos).length_squared();
589
590 if dist_sq < tol_sq {
591 merge_pairs.push((start_vid, end_vid)); }
594 }
595
596 for (merge_from, merge_to) in &merge_pairs {
598 let solid_d = topo.solid(solid)?;
600 let sh = topo.shell(solid_d.outer_shell())?;
601 let fids: Vec<_> = sh.faces().to_vec();
602
603 let mut updates = Vec::new();
604 for &fid2 in &fids {
605 let f = topo.face(fid2)?;
606 let w = topo.wire(f.outer_wire())?;
607 for oe in w.edges() {
608 let edge = topo.edge(oe.edge())?;
609 let cur_start = edge.start();
610 let cur_end = edge.end();
611 let new_start = if cur_start == *merge_from {
612 *merge_to
613 } else {
614 cur_start
615 };
616 let new_end = if cur_end == *merge_from {
617 *merge_to
618 } else {
619 cur_end
620 };
621 if new_start != cur_start || new_end != cur_end {
622 let curve = edge.curve().clone();
623 updates.push((oe.edge(), new_start, new_end, curve));
624 }
625 }
626 }
627
628 for (eid, new_start, new_end, curve) in updates {
630 let em = topo.edge_mut(eid)?;
631 *em = brepkit_topology::edge::Edge::new(new_start, new_end, curve);
632 }
633 gaps_closed += 1;
634 }
635 }
636 }
637
638 Ok(gaps_closed)
639}
640
641pub fn remove_small_faces(
653 topo: &mut Topology,
654 solid: SolidId,
655 tolerance: f64,
656) -> Result<usize, crate::OperationsError> {
657 let tol = if tolerance > 0.0 {
658 tolerance
659 } else {
660 Tolerance::new().linear
661 };
662
663 let solid_data = topo.solid(solid)?;
664 let shell_id = solid_data.outer_shell();
665 let shell = topo.shell(shell_id)?;
666 let face_ids: Vec<_> = shell.faces().to_vec();
667
668 let mut small_faces: Vec<FaceId> = Vec::new();
669
670 for &fid in &face_ids {
671 let face = topo.face(fid)?;
672 let wire = topo.wire(face.outer_wire())?;
673
674 let mut min_pt = Vec3::new(f64::MAX, f64::MAX, f64::MAX);
676 let mut max_pt = Vec3::new(f64::MIN, f64::MIN, f64::MIN);
677
678 for oe in wire.edges() {
679 let edge = topo.edge(oe.edge())?;
680 for &vid in &[edge.start(), edge.end()] {
681 let pos = topo.vertex(vid)?.point();
682 min_pt = Vec3::new(
683 min_pt.x().min(pos.x()),
684 min_pt.y().min(pos.y()),
685 min_pt.z().min(pos.z()),
686 );
687 max_pt = Vec3::new(
688 max_pt.x().max(pos.x()),
689 max_pt.y().max(pos.y()),
690 max_pt.z().max(pos.z()),
691 );
692 }
693 }
694
695 let diagonal = (max_pt - min_pt).length();
696 if diagonal < tol {
697 small_faces.push(fid);
698 }
699 }
700
701 if small_faces.is_empty() {
702 return Ok(0);
703 }
704
705 let removed_count = small_faces.len();
706 let small_set: std::collections::HashSet<usize> =
707 small_faces.iter().map(|f| f.index()).collect();
708
709 let remaining: Vec<FaceId> = face_ids
711 .into_iter()
712 .filter(|f| !small_set.contains(&f.index()))
713 .collect();
714
715 if remaining.is_empty() {
716 return Ok(0); }
718
719 let new_shell =
720 brepkit_topology::shell::Shell::new(remaining).map_err(crate::OperationsError::Topology)?;
721 *topo.shell_mut(shell_id)? = new_shell;
722
723 Ok(removed_count)
724}
725
726pub fn remove_duplicate_faces(
738 topo: &mut Topology,
739 solid: SolidId,
740 tolerance: f64,
741) -> Result<usize, crate::OperationsError> {
742 let tol = if tolerance > 0.0 {
743 tolerance
744 } else {
745 Tolerance::new().linear
746 };
747
748 let solid_data = topo.solid(solid)?;
749 let shell_id = solid_data.outer_shell();
750 let shell = topo.shell(shell_id)?;
751 let face_ids: Vec<_> = shell.faces().to_vec();
752
753 let mut face_data: Vec<(FaceId, Point3, Vec3, usize)> = Vec::new();
756
757 for &fid in &face_ids {
758 let face = topo.face(fid)?;
759 let normal = match face.surface() {
760 FaceSurface::Plane { normal, .. } => *normal,
761 FaceSurface::Cylinder(cyl) => cyl.axis(),
762 FaceSurface::Cone(cone) => cone.axis(),
763 FaceSurface::Sphere(_) => Vec3::new(0.0, 0.0, 1.0), FaceSurface::Torus(tor) => tor.z_axis(),
765 FaceSurface::Nurbs(_) => continue, };
767
768 let wire = topo.wire(face.outer_wire())?;
769 let mut centroid = Vec3::new(0.0, 0.0, 0.0);
770 let mut count = 0;
771
772 for oe in wire.edges() {
773 let edge = topo.edge(oe.edge())?;
774 let pos = topo.vertex(edge.start())?.point();
775 centroid += Vec3::new(pos.x(), pos.y(), pos.z());
776 count += 1;
777 }
778
779 if count > 0 {
780 #[allow(clippy::cast_precision_loss)]
781 let inv = 1.0 / count as f64;
782 centroid = centroid * inv;
783 }
784
785 let centroid_pt = Point3::new(centroid.x(), centroid.y(), centroid.z());
786 face_data.push((fid, centroid_pt, normal, count));
787 }
788
789 let mut duplicates: std::collections::HashSet<usize> = std::collections::HashSet::new();
791
792 for i in 0..face_data.len() {
793 if duplicates.contains(&face_data[i].0.index()) {
794 continue;
795 }
796 for j in (i + 1)..face_data.len() {
797 if duplicates.contains(&face_data[j].0.index()) {
798 continue;
799 }
800
801 let (_, centroid_a, normal_a, count_a) = &face_data[i];
802 let (fid_j, centroid_b, normal_b, count_b) = &face_data[j];
803
804 if count_a != count_b {
806 continue;
807 }
808
809 let dot = normal_a.dot(*normal_b).abs();
811 if dot < 1.0 - tol {
812 continue;
813 }
814
815 let centroid_dist = (*centroid_a - *centroid_b).length();
817 if centroid_dist < tol {
818 duplicates.insert(fid_j.index());
819 }
820 }
821 }
822
823 if duplicates.is_empty() {
824 return Ok(0);
825 }
826
827 let removed_count = duplicates.len();
828
829 let remaining: Vec<FaceId> = face_ids
831 .into_iter()
832 .filter(|f| !duplicates.contains(&f.index()))
833 .collect();
834
835 if remaining.is_empty() {
836 return Ok(0);
837 }
838
839 let new_shell =
840 brepkit_topology::shell::Shell::new(remaining).map_err(crate::OperationsError::Topology)?;
841 *topo.shell_mut(shell_id)? = new_shell;
842
843 Ok(removed_count)
844}
845
846#[must_use]
855pub fn surfaces_equivalent_pub(a: &FaceSurface, b: &FaceSurface) -> bool {
856 surfaces_equivalent(a, b)
857}
858
859fn surfaces_equivalent(a: &FaceSurface, b: &FaceSurface) -> bool {
860 let tol = Tolerance::new();
861 let lin = tol.linear;
862 let ang = tol.angular;
863
864 match (a, b) {
865 (FaceSurface::Plane { normal: na, d: da }, FaceSurface::Plane { normal: nb, d: db }) => {
866 let plane_ang = 1e-4_f64;
872 let plane_lin = 1e-3_f64;
873 let dot = na.dot(*nb);
874 (dot.abs() - 1.0).abs() < plane_ang && (da - db * dot.signum()).abs() < plane_lin
875 }
876 (FaceSurface::Cylinder(ca), FaceSurface::Cylinder(cb)) => {
877 (ca.radius() - cb.radius()).abs() < lin
878 && ca.axis().dot(cb.axis()).abs() > 1.0 - ang
879 && {
880 let d = cb.origin() - ca.origin();
881 d.cross(ca.axis()).length_squared() < lin * lin
882 }
883 }
884 (FaceSurface::Cone(ca), FaceSurface::Cone(cb)) => {
885 (ca.half_angle() - cb.half_angle()).abs() < ang
886 && ca.axis().dot(cb.axis()).abs() > 1.0 - ang
887 && {
888 let d = cb.apex() - ca.apex();
889 d.dot(d) < lin * lin
890 }
891 }
892 (FaceSurface::Sphere(sa), FaceSurface::Sphere(sb)) => {
893 (sa.radius() - sb.radius()).abs() < lin && {
894 let d = sb.center() - sa.center();
895 d.dot(d) < lin * lin
896 }
897 }
898 (FaceSurface::Torus(ta), FaceSurface::Torus(tb)) => {
899 (ta.major_radius() - tb.major_radius()).abs() < lin
900 && (ta.minor_radius() - tb.minor_radius()).abs() < lin
901 && ta.z_axis().dot(tb.z_axis()).abs() > 1.0 - ang
902 && {
903 let d = tb.center() - ta.center();
904 d.dot(d) < lin * lin
905 }
906 }
907 (
909 FaceSurface::Plane { .. }
910 | FaceSurface::Cylinder(_)
911 | FaceSurface::Cone(_)
912 | FaceSurface::Sphere(_)
913 | FaceSurface::Torus(_)
914 | FaceSurface::Nurbs(_),
915 _,
916 ) => false,
917 }
918}
919
920fn normals_compatible_at_edge(
928 topo: &Topology,
929 face_a: FaceId,
930 face_b: FaceId,
931 surface: &FaceSurface,
932) -> bool {
933 if let FaceSurface::Plane { normal: na, .. } = surface {
935 let Ok(fb) = topo.face(face_b) else {
936 return false;
937 };
938 let nb = match fb.surface() {
939 FaceSurface::Plane { normal, .. } => *normal,
940 _ => return false,
941 };
942 let Ok(fa) = topo.face(face_a) else {
943 return false;
944 };
945 let eff_na = if fa.is_reversed() { -*na } else { *na };
946 let eff_nb = if fb.is_reversed() { -nb } else { nb };
947 return eff_na.dot(eff_nb) > 0.0;
948 }
949
950 let sample_pt = find_shared_vertex(topo, face_a, face_b);
952 let Some(pt) = sample_pt else {
953 return false; };
955 let Ok(fa) = topo.face(face_a) else {
956 return false;
957 };
958 let Ok(fb) = topo.face(face_b) else {
959 return false;
960 };
961 let uv_a = fa.surface().project_point(pt);
962 let uv_b = fb.surface().project_point(pt);
963 let (Some((ua, va)), Some((ub, vb))) = (uv_a, uv_b) else {
964 return false;
965 };
966 let mut na = fa.surface().normal(ua, va);
967 let mut nb = fb.surface().normal(ub, vb);
968 if fa.is_reversed() {
969 na = -na;
970 }
971 if fb.is_reversed() {
972 nb = -nb;
973 }
974 na.dot(nb) > 0.0
975}
976
977fn find_shared_vertex(
979 topo: &Topology,
980 face_a: FaceId,
981 face_b: FaceId,
982) -> Option<brepkit_math::vec::Point3> {
983 let fa = topo.face(face_a).ok()?;
984 let fb = topo.face(face_b).ok()?;
985
986 let mut b_verts: std::collections::HashSet<usize> = std::collections::HashSet::new();
988 let mut b_positions: std::collections::HashSet<QVPos> = std::collections::HashSet::new();
989 for wid in std::iter::once(fb.outer_wire()).chain(fb.inner_wires().iter().copied()) {
990 let Ok(wire) = topo.wire(wid) else { continue };
991 for oe in wire.edges() {
992 let Ok(e) = topo.edge(oe.edge()) else {
993 continue;
994 };
995 for &vid in &[e.start(), e.end()] {
996 b_verts.insert(vid.index());
997 if let Ok(v) = topo.vertex(vid) {
998 b_positions.insert(quantize_vertex(v.point()));
999 }
1000 }
1001 }
1002 }
1003
1004 for wid in std::iter::once(fa.outer_wire()).chain(fa.inner_wires().iter().copied()) {
1013 let Ok(wire) = topo.wire(wid) else { continue };
1014 for oe in wire.edges() {
1015 let Ok(e) = topo.edge(oe.edge()) else {
1016 continue;
1017 };
1018 for &vid in &[e.start(), e.end()] {
1019 if b_verts.contains(&vid.index()) {
1020 return topo
1021 .vertex(vid)
1022 .ok()
1023 .map(brepkit_topology::vertex::Vertex::point);
1024 }
1025 if let Ok(v) = topo.vertex(vid) {
1026 let qp = quantize_vertex(v.point());
1027 if b_positions.contains(&qp) {
1028 return Some(v.point());
1029 }
1030 }
1031 }
1032 }
1033 }
1034 None
1035}
1036
1037fn uf_find(parent: &mut [usize], mut x: usize) -> usize {
1039 while parent[x] != x {
1040 parent[x] = parent[parent[x]];
1041 x = parent[x];
1042 }
1043 x
1044}
1045
1046fn uf_union(parent: &mut [usize], a: usize, b: usize) {
1048 let ra = uf_find(parent, a);
1049 let rb = uf_find(parent, b);
1050 if ra != rb {
1051 parent[rb] = ra;
1052 }
1053}
1054
1055#[allow(clippy::too_many_lines)]
1074pub fn unify_faces(topo: &mut Topology, solid: SolidId) -> Result<usize, crate::OperationsError> {
1075 const MAX_BOUNDARY_EDGES: usize = 200;
1080
1081 let solid_data = topo.solid(solid)?;
1082 let shell_id = solid_data.outer_shell();
1083 let shell = topo.shell(shell_id)?;
1084 let all_face_ids: Vec<FaceId> = shell.faces().to_vec();
1085 let original_count = all_face_ids.len();
1086
1087 if original_count < 2 {
1088 return Ok(0);
1089 }
1090
1091 let edge_face_map = brepkit_topology::explorer::edge_to_face_map(topo, solid)?;
1093
1094 #[allow(clippy::type_complexity)]
1099 let mut geom_edge_faces: HashMap<(usize, usize, u8, i64, i64, i64, i64), Vec<FaceId>> =
1100 HashMap::new();
1101 let q = |v: f64| -> i64 { (v * 1e5).round() as i64 };
1102 for &fid in &all_face_ids {
1103 let face = topo.face(fid)?;
1104 for wid in std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied()) {
1106 let wire = topo.wire(wid)?;
1107 for oe in wire.edges() {
1108 let edge = topo.edge(oe.edge())?;
1109 let si = edge.start().index();
1110 let ei = edge.end().index();
1111 let (kmin, kmax) = if si <= ei { (si, ei) } else { (ei, si) };
1112 #[allow(clippy::type_complexity)]
1113 let key: Option<(usize, usize, u8, i64, i64, i64, i64)> = match edge.curve() {
1114 brepkit_topology::edge::EdgeCurve::Circle(c) => {
1115 let center = c.center();
1116 Some((
1117 kmin,
1118 kmax,
1119 1, q(center.x()),
1121 q(center.y()),
1122 q(center.z()),
1123 q(c.radius()),
1124 ))
1125 }
1126 brepkit_topology::edge::EdgeCurve::Ellipse(e) => {
1127 let center = e.center();
1128 Some((
1129 kmin,
1130 kmax,
1131 2, q(center.x()),
1133 q(center.y()),
1134 q(center.z()),
1135 q(e.semi_major()),
1136 ))
1137 }
1138 brepkit_topology::edge::EdgeCurve::Line
1139 | brepkit_topology::edge::EdgeCurve::NurbsCurve(_) => None,
1140 };
1141 if let Some(k) = key {
1142 geom_edge_faces.entry(k).or_default().push(fid);
1143 }
1144 }
1145 }
1146 }
1147
1148 let pos_scale = 1e7_f64; #[allow(clippy::type_complexity)]
1154 let mut pos_edge_faces: HashMap<((i64, i64, i64), (i64, i64, i64)), Vec<FaceId>> =
1155 HashMap::new();
1156 for &fid in &all_face_ids {
1157 let face = topo.face(fid)?;
1158 for wid in std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied()) {
1159 let wire = topo.wire(wid)?;
1160 for oe in wire.edges() {
1161 let edge = topo.edge(oe.edge())?;
1162 let sp = topo.vertex(edge.start())?.point();
1163 let ep = topo.vertex(edge.end())?.point();
1164 let qs = (
1165 (sp.x() * pos_scale).round() as i64,
1166 (sp.y() * pos_scale).round() as i64,
1167 (sp.z() * pos_scale).round() as i64,
1168 );
1169 let qe = (
1170 (ep.x() * pos_scale).round() as i64,
1171 (ep.y() * pos_scale).round() as i64,
1172 (ep.z() * pos_scale).round() as i64,
1173 );
1174 let key = if qs <= qe { (qs, qe) } else { (qe, qs) };
1175 pos_edge_faces.entry(key).or_default().push(fid);
1176 }
1177 }
1178 }
1179
1180 let face_index_map: HashMap<usize, usize> = all_face_ids
1182 .iter()
1183 .enumerate()
1184 .map(|(i, fid)| (fid.index(), i))
1185 .collect();
1186
1187 let n = all_face_ids.len();
1188 let mut parent: Vec<usize> = (0..n).collect();
1189
1190 for faces in edge_face_map.values() {
1196 if faces.len() < 2 {
1197 continue;
1198 }
1199 for i in 0..faces.len() {
1200 for j in (i + 1)..faces.len() {
1201 let fa_idx = match face_index_map.get(&faces[i].index()) {
1202 Some(&idx) => idx,
1203 None => continue,
1204 };
1205 let fb_idx = match face_index_map.get(&faces[j].index()) {
1206 Some(&idx) => idx,
1207 None => continue,
1208 };
1209 let surface_a = topo.face(faces[i])?.surface().clone();
1210 let surface_b = topo.face(faces[j])?.surface().clone();
1211 if !surfaces_equivalent(&surface_a, &surface_b) {
1212 continue;
1213 }
1214 if !normals_compatible_at_edge(topo, faces[i], faces[j], &surface_a) {
1219 continue;
1220 }
1221 uf_union(&mut parent, fa_idx, fb_idx);
1222 }
1223 }
1224 }
1225
1226 for faces in geom_edge_faces.values() {
1228 if faces.len() < 2 {
1229 continue;
1230 }
1231 for i in 0..faces.len() {
1232 for j in (i + 1)..faces.len() {
1233 let fa_idx = match face_index_map.get(&faces[i].index()) {
1234 Some(&idx) => idx,
1235 None => continue,
1236 };
1237 let fb_idx = match face_index_map.get(&faces[j].index()) {
1238 Some(&idx) => idx,
1239 None => continue,
1240 };
1241 let surface_a = topo.face(faces[i])?.surface().clone();
1242 let surface_b = topo.face(faces[j])?.surface().clone();
1243 if surfaces_equivalent(&surface_a, &surface_b)
1244 && normals_compatible_at_edge(topo, faces[i], faces[j], &surface_a)
1245 {
1246 uf_union(&mut parent, fa_idx, fb_idx);
1247 }
1248 }
1249 }
1250 }
1251
1252 for faces in pos_edge_faces.values() {
1256 if faces.len() < 2 {
1257 continue;
1258 }
1259 let mut unique: Vec<FaceId> = faces.clone();
1261 unique.sort_by_key(|f| f.index());
1262 unique.dedup();
1263 if unique.len() < 2 {
1264 continue;
1265 }
1266 for i in 0..unique.len() {
1267 for j in (i + 1)..unique.len() {
1268 let fa_idx = match face_index_map.get(&unique[i].index()) {
1269 Some(&idx) => idx,
1270 None => continue,
1271 };
1272 let fb_idx = match face_index_map.get(&unique[j].index()) {
1273 Some(&idx) => idx,
1274 None => continue,
1275 };
1276 let surface_a = topo.face(unique[i])?.surface().clone();
1277 let surface_b = topo.face(unique[j])?.surface().clone();
1278 if surfaces_equivalent(&surface_a, &surface_b)
1279 && normals_compatible_at_edge(topo, unique[i], unique[j], &surface_a)
1280 {
1281 uf_union(&mut parent, fa_idx, fb_idx);
1282 }
1283 }
1284 }
1285 }
1286
1287 let mut groups: HashMap<usize, Vec<usize>> = HashMap::new();
1289 for i in 0..n {
1290 let root = uf_find(&mut parent, i);
1291 groups.entry(root).or_default().push(i);
1292 }
1293
1294 let mut merge_groups: Vec<Vec<usize>> = groups.into_values().filter(|g| g.len() >= 2).collect();
1302 for g in &mut merge_groups {
1303 g.sort_unstable();
1304 }
1305 merge_groups.sort_unstable_by_key(|g| g.first().copied().unwrap_or(usize::MAX));
1306
1307 if merge_groups.is_empty() {
1308 return Ok(0);
1309 }
1310
1311 #[allow(clippy::items_after_statements)]
1317 struct MergeGroupData {
1318 face_ids: Vec<FaceId>,
1319 boundary_edges: Vec<OrientedEdge>,
1320 inner_wires: Vec<brepkit_topology::wire::WireId>,
1321 surface: FaceSurface,
1322 reversed: bool,
1323 }
1324
1325 let mut group_data: Vec<MergeGroupData> = Vec::new();
1326
1327 for group in &merge_groups {
1328 let group_face_ids: Vec<FaceId> = group.iter().map(|&i| all_face_ids[i]).collect();
1329
1330 let group_set: HashSet<usize> = group_face_ids.iter().map(|f| f.index()).collect();
1331 let mut internal_edges: HashSet<usize> = HashSet::new();
1332
1333 for (edge_idx, faces) in &edge_face_map {
1334 if faces.len() == 2
1335 && group_set.contains(&faces[0].index())
1336 && group_set.contains(&faces[1].index())
1337 {
1338 internal_edges.insert(*edge_idx);
1339 }
1340 }
1341
1342 let mut boundary_edges: Vec<OrientedEdge> = Vec::new();
1343 let mut all_inner_wires: Vec<brepkit_topology::wire::WireId> = Vec::new();
1344 let mut representative_surface: Option<FaceSurface> = None;
1345 let mut representative_reversed = false;
1346
1347 for &fid in &group_face_ids {
1348 let face = topo.face(fid)?;
1349 if representative_surface.is_none() {
1350 representative_surface = Some(face.surface().clone());
1351 representative_reversed = face.is_reversed();
1352 }
1353 all_inner_wires.extend_from_slice(face.inner_wires());
1354
1355 let wire = topo.wire(face.outer_wire())?;
1356 for oe in wire.edges() {
1357 if !internal_edges.contains(&oe.edge().index()) {
1358 boundary_edges.push(*oe);
1359 }
1360 }
1361 }
1362
1363 if boundary_edges.len() > MAX_BOUNDARY_EDGES {
1367 log::debug!(
1368 "unify_faces: skipping merge group with {} boundary edges (limit {})",
1369 boundary_edges.len(),
1370 MAX_BOUNDARY_EDGES
1371 );
1372 continue;
1373 }
1374
1375 let Some(surface) = representative_surface else {
1376 continue;
1377 };
1378
1379 group_data.push(MergeGroupData {
1380 face_ids: group_face_ids,
1381 boundary_edges,
1382 inner_wires: all_inner_wires,
1383 surface,
1384 reversed: representative_reversed,
1385 });
1386 }
1387
1388 let quantize_vtx = quantize_vertex;
1392 let mut canonical_vtx: HashMap<QVPos, VertexId> = HashMap::new();
1393 for gd in &group_data {
1394 for oe in &gd.boundary_edges {
1395 let edge = topo.edge(oe.edge())?;
1396 for &vid in &[edge.start(), edge.end()] {
1397 let pos = topo.vertex(vid)?.point();
1398 canonical_vtx.entry(quantize_vtx(pos)).or_insert(vid);
1399 }
1400 }
1401 }
1402
1403 let mut edge_replace: HashMap<usize, EdgeId> = HashMap::new();
1405 for gd in &group_data {
1406 for oe in &gd.boundary_edges {
1407 let eid = oe.edge();
1408 if edge_replace.contains_key(&eid.index()) {
1409 continue;
1410 }
1411 let edge = topo.edge(eid)?;
1412 let sp = topo.vertex(edge.start())?.point();
1413 let ep = topo.vertex(edge.end())?.point();
1414 let canon_start = canonical_vtx
1415 .get(&quantize_vtx(sp))
1416 .copied()
1417 .ok_or_else(|| crate::OperationsError::InvalidInput {
1418 reason: "canonical vertex not found for edge start".to_string(),
1419 })?;
1420 let canon_end = canonical_vtx
1421 .get(&quantize_vtx(ep))
1422 .copied()
1423 .ok_or_else(|| crate::OperationsError::InvalidInput {
1424 reason: "canonical vertex not found for edge end".to_string(),
1425 })?;
1426 if canon_start != edge.start() || canon_end != edge.end() {
1427 let new_edge = Edge::new(canon_start, canon_end, edge.curve().clone());
1428 let new_eid = topo.add_edge(new_edge);
1429 edge_replace.insert(eid.index(), new_eid);
1430 }
1431 }
1432 }
1433
1434 let mut merged_face_ids: Vec<FaceId> = Vec::new();
1436 let mut consumed: HashSet<usize> = HashSet::new();
1437
1438 for gd in group_data {
1439 let replaced_edges: Vec<OrientedEdge> = gd
1441 .boundary_edges
1442 .iter()
1443 .map(|oe| {
1444 if let Some(&new_eid) = edge_replace.get(&oe.edge().index()) {
1445 OrientedEdge::new(new_eid, oe.is_forward())
1446 } else {
1447 *oe
1448 }
1449 })
1450 .collect();
1451
1452 let mut loops = order_edges_into_loops(topo, &replaced_edges)?;
1453
1454 if loops.is_empty() {
1455 continue;
1456 }
1457
1458 let mut all_inner_wires = gd.inner_wires;
1459
1460 let outer_idx = if loops.len() > 1 {
1464 loops
1465 .iter()
1466 .enumerate()
1467 .max_by(|(_, a), (_, b)| {
1468 let area_a = loop_area_3d(topo, a);
1469 let area_b = loop_area_3d(topo, b);
1470 area_a
1471 .partial_cmp(&area_b)
1472 .unwrap_or(std::cmp::Ordering::Equal)
1473 })
1474 .map_or(0, |(i, _)| i)
1475 } else {
1476 0
1477 };
1478 let outer_loop = loops.remove(outer_idx);
1479
1480 let new_wire = Wire::new(outer_loop, true).map_err(crate::OperationsError::Topology)?;
1481 let new_wire_id = topo.add_wire(new_wire);
1482
1483 for inner_loop in loops {
1485 if let Ok(iw) = Wire::new(inner_loop, true) {
1486 all_inner_wires.push(topo.add_wire(iw));
1487 }
1488 }
1489
1490 let new_face = if gd.reversed {
1491 Face::new_reversed(new_wire_id, all_inner_wires, gd.surface)
1492 } else {
1493 Face::new(new_wire_id, all_inner_wires, gd.surface)
1494 };
1495 let new_face_id = topo.add_face(new_face);
1496 merged_face_ids.push(new_face_id);
1497
1498 for &fid in &gd.face_ids {
1499 consumed.insert(fid.index());
1500 }
1501 }
1502
1503 if consumed.is_empty() {
1504 return Ok(0);
1505 }
1506
1507 let mut new_faces: Vec<FaceId> = all_face_ids
1509 .into_iter()
1510 .filter(|f| !consumed.contains(&f.index()))
1511 .collect();
1512 new_faces.extend(merged_face_ids);
1513
1514 let new_shell = Shell::new(new_faces).map_err(crate::OperationsError::Topology)?;
1515 *topo.shell_mut(shell_id)? = new_shell;
1516
1517 let final_count = topo.shell(shell_id)?.faces().len();
1518 Ok(original_count - final_count)
1519}
1520
1521fn loop_area_3d(topo: &Topology, loop_edges: &[OrientedEdge]) -> f64 {
1525 let mut positions: Vec<Point3> = Vec::with_capacity(loop_edges.len());
1526 for oe in loop_edges {
1527 let edge = match topo.edge(oe.edge()) {
1528 Ok(e) => e,
1529 Err(_) => return 0.0,
1530 };
1531 let vid = if oe.is_forward() {
1532 edge.start()
1533 } else {
1534 edge.end()
1535 };
1536 match topo.vertex(vid) {
1537 Ok(v) => positions.push(v.point()),
1538 Err(_) => return 0.0,
1539 }
1540 }
1541 if positions.len() < 3 {
1542 return 0.0;
1543 }
1544 crate::winding::newell_normal(&positions).length() * 0.5
1546}
1547
1548type QVPos = (i64, i64, i64);
1550
1551fn quantize_vertex(p: Point3) -> QVPos {
1553 let scale = 1e7; (
1555 (p.x() * scale).round() as i64,
1556 (p.y() * scale).round() as i64,
1557 (p.z() * scale).round() as i64,
1558 )
1559}
1560
1561struct EdgeInfo {
1567 oe: OrientedEdge,
1568 start_pos: QVPos,
1569 end_pos: QVPos,
1570}
1571
1572fn order_edges_into_loops(
1578 topo: &Topology,
1579 edges: &[OrientedEdge],
1580) -> Result<Vec<Vec<OrientedEdge>>, crate::OperationsError> {
1581 if edges.is_empty() {
1582 return Ok(Vec::new());
1583 }
1584
1585 let mut infos: Vec<EdgeInfo> = Vec::with_capacity(edges.len());
1586 for oe in edges {
1587 let edge = topo.edge(oe.edge())?;
1588 let sp = topo.vertex(edge.start())?.point();
1589 let ep = topo.vertex(edge.end())?.point();
1590 let (start_pos, end_pos) = if oe.is_forward() {
1591 (quantize_vertex(sp), quantize_vertex(ep))
1592 } else {
1593 (quantize_vertex(ep), quantize_vertex(sp))
1594 };
1595 infos.push(EdgeInfo {
1596 oe: *oe,
1597 start_pos,
1598 end_pos,
1599 });
1600 }
1601
1602 let mut start_map: HashMap<QVPos, Vec<usize>> = HashMap::new();
1604 for (i, info) in infos.iter().enumerate() {
1605 start_map.entry(info.start_pos).or_default().push(i);
1606 }
1607
1608 let mut used = vec![false; edges.len()];
1609 let mut loops: Vec<Vec<OrientedEdge>> = Vec::new();
1610
1611 while let Some(start_idx) = used.iter().position(|&u| !u) {
1613 let mut chain = Vec::new();
1614 chain.push(infos[start_idx].oe);
1615 used[start_idx] = true;
1616 let chain_start = infos[start_idx].start_pos;
1617 let mut current_end = infos[start_idx].end_pos;
1618
1619 let max_steps = edges.len();
1620 for _ in 1..=max_steps {
1621 if current_end == chain_start {
1622 break; }
1624 let candidates = match start_map.get(¤t_end) {
1625 Some(c) => c,
1626 None => break, };
1628 let mut found = false;
1629 for &idx in candidates {
1630 if !used[idx] {
1631 used[idx] = true;
1632 chain.push(infos[idx].oe);
1633 current_end = infos[idx].end_pos;
1634 found = true;
1635 break;
1636 }
1637 }
1638 if !found {
1639 break; }
1641 }
1642
1643 if current_end == chain_start && !chain.is_empty() {
1645 loops.push(chain);
1646 }
1647 }
1648
1649 Ok(loops)
1650}
1651
1652pub fn convert_to_bspline(
1669 topo: &mut Topology,
1670 solid: SolidId,
1671) -> Result<usize, crate::OperationsError> {
1672 brepkit_heal::custom::convert_to_bspline::convert_solid_to_bspline(topo, solid).map_err(|e| {
1673 crate::OperationsError::InvalidInput {
1674 reason: format!("convert_to_bspline failed: {e}"),
1675 }
1676 })
1677}
1678
1679pub fn convert_to_elementary(
1718 topo: &mut Topology,
1719 solid: SolidId,
1720 tolerance: f64,
1721) -> Result<usize, crate::OperationsError> {
1722 let tol = brepkit_math::tolerance::Tolerance {
1723 linear: tolerance,
1724 ..brepkit_math::tolerance::Tolerance::new()
1725 };
1726 let surfaces =
1727 brepkit_heal::custom::convert_to_elementary::convert_to_elementary(topo, solid, &tol)
1728 .map_err(|e| crate::OperationsError::InvalidInput {
1729 reason: format!("convert_to_elementary (surfaces) failed: {e}"),
1730 })?;
1731 let edges =
1732 brepkit_heal::custom::convert_to_elementary::convert_edges_to_elementary(topo, solid, &tol)
1733 .map_err(|e| crate::OperationsError::InvalidInput {
1734 reason: format!("convert_to_elementary (edges) failed: {e}"),
1735 })?;
1736 Ok(surfaces + edges)
1737}
1738
1739#[cfg(test)]
1740mod tests;