1use crate::mass_properties::solid_signed_volume;
20use crate::offset_shell::{flip_all_faces, orient_open_solid_faces};
21use crate::topology::{BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, ShellRecord, VertexRecord};
22use crate::{build_pcurve_on_surface, project_point_to_curve, NurbsCurve, Vec3};
23use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
24use serde::{Deserialize, Serialize};
25
26#[derive(Clone, Debug, Serialize, Deserialize)]
27pub struct SewReport {
28 pub edges_sewn: usize,
29 pub shells_merged: usize,
30 pub open_edges_before: usize,
31 pub open_edges_after: usize,
32 pub oriented_outward: bool,
33 pub issues: Vec<String>,
34}
35
36fn one_use_edge_ids(solid: &BrepSolid) -> HashSet<u64> {
37 let mut counts = HashMap::<u64, usize>::default();
38 for coedge in solid
39 .shells
40 .iter()
41 .flat_map(|shell| &shell.faces)
42 .flat_map(|face| &face.loops)
43 .flat_map(|loop_record| &loop_record.coedges)
44 {
45 *counts.entry(coedge.edge_id).or_default() += 1;
46 }
47 counts
48 .into_iter()
49 .filter(|(_, count)| *count == 1)
50 .map(|(id, _)| id)
51 .collect()
52}
53
54fn curve_point(edge: &EdgeRecord, fraction: f64) -> Result<Vec3, String> {
55 edge.curve
56 .evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)
57}
58
59fn locus_deviation(
61 piece: &EdgeRecord,
62 carrier: &EdgeRecord,
63 samples: usize,
64) -> Result<f64, String> {
65 let mut worst = 0.0f64;
66 for index in 0..=samples {
67 let point = curve_point(piece, index as f64 / samples as f64)?;
68 worst = worst.max(project_point_to_curve(&carrier.curve, point)?.distance);
69 }
70 Ok(worst)
71}
72
73fn same_parametrization(
77 first: &EdgeRecord,
78 second: &EdgeRecord,
79 tolerance: f64,
80) -> Result<bool, String> {
81 for index in 0..=8 {
82 let fraction = index as f64 / 8.0;
83 if curve_point(first, fraction)?
84 .sub(curve_point(second, fraction)?)
85 .length()
86 > tolerance
87 {
88 return Ok(false);
89 }
90 }
91 Ok(true)
92}
93
94fn walk_points(face: &FaceRecord, coedge: &CoedgeRecord) -> Result<[Vec3; 2], String> {
98 let [start, end] = coedge.pcurve.domain()?;
99 let sample = |fraction: f64| -> Result<Vec3, String> {
100 let uv = coedge.pcurve.evaluate(start + (end - start) * fraction)?;
101 face.surface.evaluate(uv.x, uv.y)
102 };
103 Ok([sample(0.0)?, sample(0.35)?])
104}
105
106struct RebindPatch {
109 shell: usize,
110 face: usize,
111 loop_index: usize,
112 coedge: usize,
113 forward: bool,
114 pcurve: Option<NurbsCurve>,
115}
116
117fn plan_rebind(
122 solid: &BrepSolid,
123 keep: &EdgeRecord,
124 remove: &EdgeRecord,
125 tolerance: f64,
126) -> Result<Vec<RebindPatch>, String> {
127 let identical = same_parametrization(keep, remove, tolerance)?;
128 let closed = keep.start_vertex_id == keep.end_vertex_id;
129 let period = keep.t1 - keep.t0;
130 let mut patches = Vec::new();
131 for (shell_index, shell) in solid.shells.iter().enumerate() {
132 for (face_index, face) in shell.faces.iter().enumerate() {
133 for (loop_index, loop_record) in face.loops.iter().enumerate() {
134 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
135 if coedge.edge_id != remove.id {
136 continue;
137 }
138 let [walk_start, walk_next] = walk_points(face, coedge)?;
139 let t_start = project_point_to_curve(&keep.curve, walk_start)?.u;
140 let t_next = project_point_to_curve(&keep.curve, walk_next)?.u;
141 let forward = if closed {
142 let mut delta = t_next - t_start;
143 while delta > period * 0.5 {
144 delta -= period;
145 }
146 while delta < -period * 0.5 {
147 delta += period;
148 }
149 delta > 0.0
150 } else {
151 t_next > t_start
152 };
153 let pcurve = if identical {
154 None
155 } else {
156 let traversal = if forward {
157 keep.curve.clone()
158 } else {
159 keep.curve.reversed()?
160 };
161 Some(build_pcurve_on_surface(&face.surface, &traversal)?)
162 };
163 patches.push(RebindPatch {
164 shell: shell_index,
165 face: face_index,
166 loop_index,
167 coedge: coedge_index,
168 forward,
169 pcurve,
170 });
171 }
172 }
173 }
174 }
175 Ok(patches)
176}
177
178fn apply_rebind(
179 solid: &mut BrepSolid,
180 keep: &EdgeRecord,
181 remove: &EdgeRecord,
182 patches: Vec<RebindPatch>,
183 tolerance: f64,
184) {
185 for patch in patches {
186 let coedge = &mut solid.shells[patch.shell].faces[patch.face].loops[patch.loop_index]
187 .coedges[patch.coedge];
188 coedge.edge_id = keep.id;
189 coedge.forward = patch.forward;
190 if let Some(pcurve) = patch.pcurve {
191 coedge.pcurve = pcurve;
192 }
193 }
194 solid.edges.retain(|edge| edge.id != remove.id);
195 let point_of = |vertex_id: u64| {
201 solid
202 .vertices
203 .iter()
204 .find(|vertex| vertex.id == vertex_id)
205 .map(|vertex| vertex.point)
206 };
207 let mut welds = Vec::<(u64, u64)>::new();
208 for from in [remove.start_vertex_id, remove.end_vertex_id] {
209 let Some(from_point) = point_of(from) else {
210 continue;
211 };
212 let target = [keep.start_vertex_id, keep.end_vertex_id]
213 .into_iter()
214 .filter_map(|candidate| {
215 point_of(candidate).map(|point| (candidate, point.sub(from_point).length()))
216 })
217 .min_by(|first, second| first.1.total_cmp(&second.1));
218 if let Some((to, distance)) = target {
219 if distance <= tolerance && from != to {
220 welds.push((from, to));
221 }
222 }
223 }
224 for (from, to) in welds {
225 for edge in &mut solid.edges {
226 if edge.start_vertex_id == from {
227 edge.start_vertex_id = to;
228 }
229 if edge.end_vertex_id == from {
230 edge.end_vertex_id = to;
231 }
232 }
233 }
234}
235
236fn orient_pair<'a>(
257 solid: &BrepSolid,
258 first: &'a EdgeRecord,
259 second: &'a EdgeRecord,
260 points: &HashMap<u64, Vec3>,
261 tolerance: f64,
262) -> Option<(&'a EdgeRecord, &'a EdgeRecord)> {
263 if first.start_vertex_id != first.end_vertex_id {
266 return Some((first, second));
267 }
268 let (Some(&first_seam), Some(&second_seam)) = (
269 points.get(&first.start_vertex_id),
270 points.get(&second.start_vertex_id),
271 ) else {
272 return Some((first, second));
273 };
274 if first_seam.sub(second_seam).length() <= tolerance {
275 return Some((first, second));
278 }
279 let seam_is_exclusive = |rim: &EdgeRecord| {
280 !solid.edges.iter().any(|edge| {
281 edge.id != rim.id
282 && (edge.start_vertex_id == rim.start_vertex_id
283 || edge.end_vertex_id == rim.start_vertex_id)
284 })
285 };
286 if seam_is_exclusive(second) {
287 Some((first, second))
288 } else if seam_is_exclusive(first) {
289 Some((second, first))
290 } else {
291 None
292 }
293}
294
295fn merge_connected_shells(solid: &mut BrepSolid) -> usize {
297 let mut owner_of_edge = HashMap::<u64, usize>::default();
298 let mut parent = (0..solid.shells.len()).collect::<Vec<_>>();
299 fn root(parent: &mut [usize], index: usize) -> usize {
300 if parent[index] != index {
301 parent[index] = root(parent, parent[index]);
302 }
303 parent[index]
304 }
305 for (shell_index, shell) in solid.shells.iter().enumerate() {
306 for coedge in shell
307 .faces
308 .iter()
309 .flat_map(|face| &face.loops)
310 .flat_map(|loop_record| &loop_record.coedges)
311 {
312 if let Some(&other) = owner_of_edge.get(&coedge.edge_id) {
313 let first = root(&mut parent, other);
314 let second = root(&mut parent, shell_index);
315 if first != second {
316 parent[second] = first;
317 }
318 } else {
319 owner_of_edge.insert(coedge.edge_id, shell_index);
320 }
321 }
322 }
323 let before = solid.shells.len();
324 let original = std::mem::take(&mut solid.shells);
325 let mut merged = Vec::<ShellRecord>::new();
326 let mut group_of = HashMap::<usize, usize>::default();
327 for (index, shell) in original.into_iter().enumerate() {
328 let group = root(&mut parent, index);
329 if let Some(&target) = group_of.get(&group) {
330 merged[target].faces.extend(shell.faces);
331 } else {
332 group_of.insert(group, merged.len());
333 merged.push(shell);
334 }
335 }
336 solid.shells = merged;
337 before - solid.shells.len()
338}
339
340#[derive(Clone, Copy, PartialEq, Eq, Debug)]
349enum PairSearch {
350 Hashed,
353 #[cfg_attr(not(test), allow(dead_code))]
355 Exhaustive,
356}
357
358fn grid_cell(point: Vec3, cell: f64) -> Option<[i64; 3]> {
364 let mut key = [0i64; 3];
365 for (slot, value) in key.iter_mut().zip([point.x, point.y, point.z]) {
366 if !value.is_finite() {
367 return None;
368 }
369 let scaled = (value / cell).floor();
370 if !scaled.is_finite() || scaled.abs() > 9.0e15 {
373 return None;
374 }
375 *slot = scaled as i64;
376 }
377 Some(key)
378}
379
380struct EndpointGrid {
393 cell: f64,
394 buckets: HashMap<[i64; 3], Vec<usize>>,
395}
396
397impl EndpointGrid {
398 fn build(endpoints: &[Option<[Vec3; 2]>], closed: &[bool], tolerance: f64) -> Option<Self> {
411 let cell = 2.0 * tolerance;
412 if !(cell.is_finite() && cell > 0.0) {
413 return None;
414 }
415 let mut buckets = HashMap::<[i64; 3], Vec<usize>>::default();
416 for (position, ends) in endpoints.iter().enumerate() {
417 if closed[position] {
418 continue;
419 }
420 let Some(ends) = ends else {
421 continue;
422 };
423 for point in ends {
424 buckets
425 .entry(grid_cell(*point, cell)?)
426 .or_default()
427 .push(position);
428 }
429 }
430 Some(EndpointGrid { cell, buckets })
431 }
432
433 fn near(&self, point: Vec3, out: &mut Vec<usize>) -> bool {
439 out.clear();
440 let Some(centre) = grid_cell(point, self.cell) else {
441 return false;
442 };
443 for dx in -1..=1 {
444 for dy in -1..=1 {
445 for dz in -1..=1 {
446 if let Some(bucket) =
447 self.buckets
448 .get(&[centre[0] + dx, centre[1] + dy, centre[2] + dz])
449 {
450 out.extend_from_slice(bucket);
451 }
452 }
453 }
454 }
455 out.sort_unstable();
456 out.dedup();
457 true
458 }
459}
460
461#[allow(clippy::too_many_arguments)]
481fn offer_seconds(
482 grid: Option<&EndpointGrid>,
483 first_position: usize,
484 first_closed: bool,
485 endpoints: &[Option<[Vec3; 2]>],
486 closed_positions: &[usize],
487 candidate_count: usize,
488 neighbours: &mut Vec<usize>,
489 offered: &mut Vec<usize>,
490) {
491 offered.clear();
492 let Some(grid) = grid else {
493 offered.extend(first_position + 1..candidate_count);
494 return;
495 };
496 if first_closed {
497 offered.extend(
498 closed_positions
499 .iter()
500 .copied()
501 .filter(|&position| position > first_position),
502 );
503 return;
504 }
505 let Some([start, _]) = endpoints[first_position] else {
506 return;
507 };
508 if grid.near(start, neighbours) {
509 offered.extend(
510 neighbours
511 .iter()
512 .copied()
513 .filter(|&position| position > first_position),
514 );
515 }
516}
517
518pub fn sew_solid(solid: &BrepSolid, tolerance: f64) -> Result<(BrepSolid, SewReport), String> {
527 let (sewn, report, _examined) = sew_solid_with_search(solid, tolerance, PairSearch::Hashed)?;
528 Ok((sewn, report))
529}
530
531fn sew_solid_with_search(
538 solid: &BrepSolid,
539 tolerance: f64,
540 search: PairSearch,
541) -> Result<(BrepSolid, SewReport, usize), String> {
542 if !(tolerance.is_finite() && tolerance > 0.0) {
543 return Err("sew_solid: tolerance must be positive".into());
544 }
545 let mut result = solid.clone();
546 let open_edges_before = one_use_edge_ids(&result).len();
547 let mut edges_sewn = 0usize;
548 let mut pairs_examined = 0usize;
549 let mut blocked = HashSet::<(u64, u64)>::default();
559 let unordered = |first: u64, second: u64| (first.min(second), first.max(second));
560 let mut offered = Vec::<usize>::new();
561 let mut neighbours = Vec::<usize>::new();
562 loop {
563 let one_use = one_use_edge_ids(&result);
564 let candidates = result
569 .edges
570 .iter()
571 .enumerate()
572 .filter(|(_, edge)| !edge.degenerate && one_use.contains(&edge.id))
573 .map(|(index, _)| index)
574 .collect::<Vec<_>>();
575 let points = result
576 .vertices
577 .iter()
578 .map(|vertex| (vertex.id, vertex.point))
579 .collect::<HashMap<_, _>>();
580 let closed = candidates
581 .iter()
582 .map(|&index| {
583 let edge = &result.edges[index];
584 edge.start_vertex_id == edge.end_vertex_id
585 })
586 .collect::<Vec<_>>();
587 let endpoints = candidates
591 .iter()
592 .map(|&index| {
593 let edge = &result.edges[index];
594 match (
595 points.get(&edge.start_vertex_id),
596 points.get(&edge.end_vertex_id),
597 ) {
598 (Some(&start), Some(&end)) => Some([start, end]),
599 _ => None,
600 }
601 })
602 .collect::<Vec<_>>();
603 let closed_positions = (0..candidates.len())
604 .filter(|&position| closed[position])
605 .collect::<Vec<_>>();
606 let grid = match search {
607 PairSearch::Hashed => EndpointGrid::build(&endpoints, &closed, tolerance),
608 PairSearch::Exhaustive => None,
609 };
610
611 let mut chosen = None;
612 'pairs: for first_position in 0..candidates.len() {
613 let first = &result.edges[candidates[first_position]];
614 let first_closed = closed[first_position];
615 offer_seconds(
616 grid.as_ref(),
617 first_position,
618 first_closed,
619 &endpoints,
620 &closed_positions,
621 candidates.len(),
622 &mut neighbours,
623 &mut offered,
624 );
625 for second_position in offered.iter().copied() {
626 let second = &result.edges[candidates[second_position]];
627 pairs_examined += 1;
628 if blocked.contains(&unordered(first.id, second.id)) {
629 continue;
630 }
631 if first_closed != closed[second_position] {
632 continue;
633 }
634 if !first_closed {
635 let (Some([fs, fe]), Some([ss, se])) =
636 (endpoints[first_position], endpoints[second_position])
637 else {
638 continue;
639 };
640 let direct =
641 fs.sub(ss).length() <= tolerance && fe.sub(se).length() <= tolerance;
642 let reversed =
643 fs.sub(se).length() <= tolerance && fe.sub(ss).length() <= tolerance;
644 if !direct && !reversed {
645 continue;
646 }
647 }
648 if locus_deviation(first, second, 8)? > tolerance
649 || locus_deviation(second, first, 8)? > tolerance
650 {
651 continue;
652 }
653 let Some((keep, remove)) = orient_pair(&result, first, second, &points, tolerance)
654 else {
655 continue;
658 };
659 chosen = Some((keep.clone(), remove.clone()));
660 break 'pairs;
661 }
662 }
663 let Some((keep, remove)) = chosen else {
664 break;
665 };
666 match plan_rebind(&result, &keep, &remove, tolerance) {
667 Ok(patches) => {
668 apply_rebind(&mut result, &keep, &remove, patches, tolerance);
669 edges_sewn += 1;
670 }
671 Err(_) => {
672 blocked.insert(unordered(keep.id, remove.id));
673 }
674 }
675 }
676 let used_vertices = result
678 .edges
679 .iter()
680 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
681 .collect::<HashSet<_>>();
682 result
683 .vertices
684 .retain(|vertex| used_vertices.contains(&vertex.id));
685
686 let shells_merged = merge_connected_shells(&mut result);
687 let mut oriented_outward = false;
688 if edges_sewn > 0 {
689 orient_open_solid_faces(&mut result)?;
693 if let Ok(volume) = solid_signed_volume(&result) {
694 if volume < 0.0 {
695 flip_all_faces(&mut result)?;
696 }
697 oriented_outward = volume.abs() > tolerance * tolerance * tolerance;
698 }
699 }
700 if edges_sewn > 0 || shells_merged > 0 {
705 let vertex_count = result.vertices.len() as i64;
706 let edge_count = result.edges.iter().filter(|edge| !edge.degenerate).count() as i64;
707 let face_count = result
708 .shells
709 .iter()
710 .map(|shell| shell.faces.len())
711 .sum::<usize>() as i64;
712 let ring_count = result
713 .shells
714 .iter()
715 .flat_map(|shell| &shell.faces)
716 .map(|face| face.loops.len().saturating_sub(1))
717 .sum::<usize>() as i64;
718 let numerator = 2 - (vertex_count - edge_count + face_count - ring_count);
719 if numerator >= 0 && numerator % 2 == 0 {
720 result.genus = numerator / 2;
721 }
722 }
723 let open_edges_after = one_use_edge_ids(&result).len();
724 let issues = result
725 .validate()
726 .into_iter()
727 .map(|issue| issue.message)
728 .collect();
729 Ok((
730 result,
731 SewReport {
732 edges_sewn,
733 shells_merged,
734 open_edges_before,
735 open_edges_after,
736 oriented_outward,
737 issues,
738 },
739 pairs_examined,
740 ))
741}
742
743pub fn split_pinched_vertices(solid: &mut BrepSolid) -> Result<usize, String> {
755 let mut split_count = 0usize;
756 let vertex_ids: Vec<u64> = solid.vertices.iter().map(|vertex| vertex.id).collect();
757 let mut next_id = solid
758 .vertices
759 .iter()
760 .map(|vertex| vertex.id)
761 .chain(solid.edges.iter().map(|edge| edge.id))
762 .max()
763 .unwrap_or(0)
764 + 1;
765 let edge_of_id: HashMap<u64, usize> = solid
770 .edges
771 .iter()
772 .enumerate()
773 .map(|(index, edge)| (edge.id, index))
774 .collect();
775 let vertex_of_id: HashMap<u64, usize> = solid
776 .vertices
777 .iter()
778 .enumerate()
779 .map(|(index, vertex)| (vertex.id, index))
780 .collect();
781 for vertex_id in vertex_ids {
782 let incident: Vec<u64> = solid
785 .edges
786 .iter()
787 .filter(|edge| edge.start_vertex_id == vertex_id || edge.end_vertex_id == vertex_id)
788 .map(|edge| edge.id)
789 .collect();
790 if incident.len() < 4 {
791 continue;
793 }
794 let index_of: HashMap<u64, usize> = incident
795 .iter()
796 .enumerate()
797 .map(|(index, id)| (*id, index))
798 .collect();
799 let mut parent: Vec<usize> = (0..incident.len()).collect();
800 fn root(parent: &mut [usize], index: usize) -> usize {
801 if parent[index] != index {
802 parent[index] = root(parent, parent[index]);
803 }
804 parent[index]
805 }
806 let edge_end = |edge_id: u64, forward: bool| -> Option<u64> {
807 edge_of_id.get(&edge_id).map(|&index| {
808 let edge = &solid.edges[index];
809 if forward {
810 edge.end_vertex_id
811 } else {
812 edge.start_vertex_id
813 }
814 })
815 };
816 for shell in &solid.shells {
817 for face in &shell.faces {
818 for loop_record in &face.loops {
819 let count = loop_record.coedges.len();
820 for index in 0..count {
821 let current = &loop_record.coedges[index];
822 let next = &loop_record.coedges[(index + 1) % count];
823 let Some(junction) = edge_end(current.edge_id, current.forward) else {
826 continue;
827 };
828 if junction != vertex_id {
829 continue;
830 }
831 let (Some(&a), Some(&b)) =
832 (index_of.get(¤t.edge_id), index_of.get(&next.edge_id))
833 else {
834 continue;
835 };
836 let ra = root(&mut parent, a);
837 let rb = root(&mut parent, b);
838 if ra != rb {
839 parent[rb] = ra;
840 }
841 }
842 }
843 }
844 }
845 let mut component_of: HashMap<usize, usize> = HashMap::default();
846 let mut components = 0usize;
847 let mut assignment: Vec<usize> = vec![0; incident.len()];
848 for index in 0..incident.len() {
849 let r = root(&mut parent, index);
850 let component = *component_of.entry(r).or_insert_with(|| {
851 components += 1;
852 components - 1
853 });
854 assignment[index] = component;
855 }
856 if components < 2 {
857 continue;
858 }
859 let point = vertex_of_id
862 .get(&vertex_id)
863 .map(|&index| solid.vertices[index].point)
864 .ok_or("split_pinched_vertices: vertex vanished")?;
865 let mut replacement_ids = vec![vertex_id];
866 for _ in 1..components {
867 let id = next_id;
868 next_id += 1;
869 solid.vertices.push(VertexRecord { id, point });
870 replacement_ids.push(id);
871 }
872 for (offset, edge_id) in incident.iter().enumerate() {
873 let replacement = replacement_ids[assignment[offset]];
874 if replacement == vertex_id {
875 continue;
876 }
877 if let Some(edge) = solid.edges.iter_mut().find(|edge| edge.id == *edge_id) {
878 if edge.start_vertex_id == vertex_id {
879 edge.start_vertex_id = replacement;
880 }
881 if edge.end_vertex_id == vertex_id {
882 edge.end_vertex_id = replacement;
883 }
884 }
885 }
886 split_count += components - 1;
887 }
888 Ok(split_count)
889}
890
891