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