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 if parent[index] != index {
302 parent[index] = root(parent, parent[index]);
303 }
304 parent[index]
305 }
306 for (shell_index, shell) in solid.shells.iter().enumerate() {
307 for coedge in shell
308 .faces
309 .iter()
310 .flat_map(|face| &face.loops)
311 .flat_map(|loop_record| &loop_record.coedges)
312 {
313 if let Some(&other) = owner_of_edge.get(&coedge.edge_id) {
314 let first = root(&mut parent, other);
315 let second = root(&mut parent, shell_index);
316 if first != second {
317 parent[second] = first;
318 }
319 } else {
320 owner_of_edge.insert(coedge.edge_id, shell_index);
321 }
322 }
323 }
324 let before = solid.shells.len();
325 let original = std::mem::take(&mut solid.shells);
326 let mut merged = Vec::<ShellRecord>::new();
327 let mut group_of = HashMap::<usize, usize>::default();
328 for (index, shell) in original.into_iter().enumerate() {
329 let group = root(&mut parent, index);
330 if let Some(&target) = group_of.get(&group) {
331 merged[target].faces.extend(shell.faces);
332 } else {
333 group_of.insert(group, merged.len());
334 merged.push(shell);
335 }
336 }
337 solid.shells = merged;
338 before - solid.shells.len()
339}
340
341#[derive(Clone, Copy, PartialEq, Eq, Debug)]
350enum PairSearch {
351 Hashed,
354 #[cfg_attr(not(test), allow(dead_code))]
356 Exhaustive,
357}
358
359fn grid_cell(point: Vec3, cell: f64) -> Option<[i64; 3]> {
365 let mut key = [0i64; 3];
366 for (slot, value) in key.iter_mut().zip([point.x, point.y, point.z]) {
367 if !value.is_finite() {
368 return None;
369 }
370 let scaled = (value / cell).floor();
371 if !scaled.is_finite() || scaled.abs() > 9.0e15 {
374 return None;
375 }
376 *slot = scaled as i64;
377 }
378 Some(key)
379}
380
381struct EndpointGrid {
394 cell: f64,
395 buckets: HashMap<[i64; 3], Vec<usize>>,
396}
397
398impl EndpointGrid {
399 fn build(endpoints: &[Option<[Vec3; 2]>], closed: &[bool], tolerance: f64) -> Option<Self> {
412 let cell = 2.0 * tolerance;
413 if !(cell.is_finite() && cell > 0.0) {
414 return None;
415 }
416 let mut buckets = HashMap::<[i64; 3], Vec<usize>>::default();
417 for (position, ends) in endpoints.iter().enumerate() {
418 if closed[position] {
419 continue;
420 }
421 let Some(ends) = ends else {
422 continue;
423 };
424 for point in ends {
425 buckets
426 .entry(grid_cell(*point, cell)?)
427 .or_default()
428 .push(position);
429 }
430 }
431 Some(EndpointGrid { cell, buckets })
432 }
433
434 fn near(&self, point: Vec3, out: &mut Vec<usize>) -> bool {
440 out.clear();
441 let Some(centre) = grid_cell(point, self.cell) else {
442 return false;
443 };
444 for dx in -1..=1 {
445 for dy in -1..=1 {
446 for dz in -1..=1 {
447 if let Some(bucket) =
448 self.buckets
449 .get(&[centre[0] + dx, centre[1] + dy, centre[2] + dz])
450 {
451 out.extend_from_slice(bucket);
452 }
453 }
454 }
455 }
456 out.sort_unstable();
457 out.dedup();
458 true
459 }
460}
461
462#[allow(clippy::too_many_arguments)]
482fn offer_seconds(
483 grid: Option<&EndpointGrid>,
484 first_position: usize,
485 first_closed: bool,
486 endpoints: &[Option<[Vec3; 2]>],
487 closed_positions: &[usize],
488 candidate_count: usize,
489 neighbours: &mut Vec<usize>,
490 offered: &mut Vec<usize>,
491) {
492 offered.clear();
493 let Some(grid) = grid else {
494 offered.extend(first_position + 1..candidate_count);
495 return;
496 };
497 if first_closed {
498 offered.extend(
499 closed_positions
500 .iter()
501 .copied()
502 .filter(|&position| position > first_position),
503 );
504 return;
505 }
506 let Some([start, _]) = endpoints[first_position] else {
507 return;
508 };
509 if grid.near(start, neighbours) {
510 offered.extend(
511 neighbours
512 .iter()
513 .copied()
514 .filter(|&position| position > first_position),
515 );
516 }
517}
518
519pub fn sew_solid(solid: &BrepSolid, tolerance: f64) -> Result<(BrepSolid, SewReport), KernelRefusal> {
528 let (sewn, report, _examined) = sew_solid_with_search(solid, tolerance, PairSearch::Hashed)?;
529 Ok((sewn, report))
530}
531
532fn sew_solid_with_search(
539 solid: &BrepSolid,
540 tolerance: f64,
541 search: PairSearch,
542) -> Result<(BrepSolid, SewReport, usize), KernelRefusal> {
543 if !(tolerance.is_finite() && tolerance > 0.0) {
544 return Err(KernelRefusal::input(KernelStage::Collect, "tolerance", "sew_solid: tolerance must be positive"));
545 }
546 let mut result = solid.clone();
547 let open_edges_before = one_use_edge_ids(&result).len();
548 let mut edges_sewn = 0usize;
549 let mut pairs_examined = 0usize;
550 let mut blocked = HashSet::<(u64, u64)>::default();
560 let unordered = |first: u64, second: u64| (first.min(second), first.max(second));
561 let mut offered = Vec::<usize>::new();
562 let mut neighbours = Vec::<usize>::new();
563 loop {
564 let one_use = one_use_edge_ids(&result);
565 let candidates = result
570 .edges
571 .iter()
572 .enumerate()
573 .filter(|(_, edge)| !edge.degenerate && one_use.contains(&edge.id))
574 .map(|(index, _)| index)
575 .collect::<Vec<_>>();
576 let points = result
577 .vertices
578 .iter()
579 .map(|vertex| (vertex.id, vertex.point))
580 .collect::<HashMap<_, _>>();
581 let closed = candidates
582 .iter()
583 .map(|&index| {
584 let edge = &result.edges[index];
585 edge.start_vertex_id == edge.end_vertex_id
586 })
587 .collect::<Vec<_>>();
588 let endpoints = candidates
592 .iter()
593 .map(|&index| {
594 let edge = &result.edges[index];
595 match (
596 points.get(&edge.start_vertex_id),
597 points.get(&edge.end_vertex_id),
598 ) {
599 (Some(&start), Some(&end)) => Some([start, end]),
600 _ => None,
601 }
602 })
603 .collect::<Vec<_>>();
604 let closed_positions = (0..candidates.len())
605 .filter(|&position| closed[position])
606 .collect::<Vec<_>>();
607 let grid = match search {
608 PairSearch::Hashed => EndpointGrid::build(&endpoints, &closed, tolerance),
609 PairSearch::Exhaustive => None,
610 };
611
612 let mut chosen = None;
613 'pairs: for first_position in 0..candidates.len() {
614 let first = &result.edges[candidates[first_position]];
615 let first_closed = closed[first_position];
616 offer_seconds(
617 grid.as_ref(),
618 first_position,
619 first_closed,
620 &endpoints,
621 &closed_positions,
622 candidates.len(),
623 &mut neighbours,
624 &mut offered,
625 );
626 for second_position in offered.iter().copied() {
627 let second = &result.edges[candidates[second_position]];
628 pairs_examined += 1;
629 if blocked.contains(&unordered(first.id, second.id)) {
630 continue;
631 }
632 if first_closed != closed[second_position] {
633 continue;
634 }
635 if !first_closed {
636 let (Some([fs, fe]), Some([ss, se])) =
637 (endpoints[first_position], endpoints[second_position])
638 else {
639 continue;
640 };
641 let direct =
642 fs.sub(ss).length() <= tolerance && fe.sub(se).length() <= tolerance;
643 let reversed =
644 fs.sub(se).length() <= tolerance && fe.sub(ss).length() <= tolerance;
645 if !direct && !reversed {
646 continue;
647 }
648 }
649 if locus_deviation(first, second, 8)? > tolerance
650 || locus_deviation(second, first, 8)? > tolerance
651 {
652 continue;
653 }
654 let Some((keep, remove)) = orient_pair(&result, first, second, &points, tolerance)
655 else {
656 continue;
659 };
660 chosen = Some((keep.clone(), remove.clone()));
661 break 'pairs;
662 }
663 }
664 let Some((keep, remove)) = chosen else {
665 break;
666 };
667 match plan_rebind(&result, &keep, &remove, tolerance) {
668 Ok(patches) => {
669 apply_rebind(&mut result, &keep, &remove, patches, tolerance);
670 edges_sewn += 1;
671 }
672 Err(_) => {
673 blocked.insert(unordered(keep.id, remove.id));
674 }
675 }
676 }
677 let used_vertices = result
679 .edges
680 .iter()
681 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
682 .collect::<HashSet<_>>();
683 result
684 .vertices
685 .retain(|vertex| used_vertices.contains(&vertex.id));
686
687 let shells_merged = merge_connected_shells(&mut result);
688 let mut oriented_outward = false;
689 if edges_sewn > 0 {
690 orient_open_solid_faces(&mut result)?;
694 if let Ok(volume) = solid_signed_volume(&result) {
695 if volume < 0.0 {
696 flip_all_faces(&mut result)?;
697 }
698 oriented_outward = volume.abs() > tolerance * tolerance * tolerance;
699 }
700 }
701 if edges_sewn > 0 || shells_merged > 0 {
706 let vertex_count = result.vertices.len() as i64;
707 let edge_count = result.edges.iter().filter(|edge| !edge.degenerate).count() as i64;
708 let face_count = result
709 .shells
710 .iter()
711 .map(|shell| shell.faces.len())
712 .sum::<usize>() as i64;
713 let ring_count = result
714 .shells
715 .iter()
716 .flat_map(|shell| &shell.faces)
717 .map(|face| face.loops.len().saturating_sub(1))
718 .sum::<usize>() as i64;
719 let numerator = 2 - (vertex_count - edge_count + face_count - ring_count);
720 if numerator >= 0 && numerator % 2 == 0 {
721 result.genus = numerator / 2;
722 }
723 }
724 let open_edges_after = one_use_edge_ids(&result).len();
725 let issues = result
726 .validate()
727 .into_iter()
728 .map(|issue| issue.message)
729 .collect();
730 Ok((
731 result,
732 SewReport {
733 edges_sewn,
734 shells_merged,
735 open_edges_before,
736 open_edges_after,
737 oriented_outward,
738 issues,
739 },
740 pairs_examined,
741 ))
742}
743
744
745pub fn split_pinched_vertices(solid: &mut BrepSolid) -> Result<usize, KernelRefusal> {
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(KernelRefusal::internal(KernelStage::Sew, "pinched_vertex", "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