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 merge_connected_shells(solid: &mut BrepSolid) -> usize {
238 let mut owner_of_edge = HashMap::<u64, usize>::default();
239 let mut parent = (0..solid.shells.len()).collect::<Vec<_>>();
240 fn root(parent: &mut [usize], index: usize) -> usize {
241 if parent[index] != index {
242 parent[index] = root(parent, parent[index]);
243 }
244 parent[index]
245 }
246 for (shell_index, shell) in solid.shells.iter().enumerate() {
247 for coedge in shell
248 .faces
249 .iter()
250 .flat_map(|face| &face.loops)
251 .flat_map(|loop_record| &loop_record.coedges)
252 {
253 if let Some(&other) = owner_of_edge.get(&coedge.edge_id) {
254 let first = root(&mut parent, other);
255 let second = root(&mut parent, shell_index);
256 if first != second {
257 parent[second] = first;
258 }
259 } else {
260 owner_of_edge.insert(coedge.edge_id, shell_index);
261 }
262 }
263 }
264 let before = solid.shells.len();
265 let original = std::mem::take(&mut solid.shells);
266 let mut merged = Vec::<ShellRecord>::new();
267 let mut group_of = HashMap::<usize, usize>::default();
268 for (index, shell) in original.into_iter().enumerate() {
269 let group = root(&mut parent, index);
270 if let Some(&target) = group_of.get(&group) {
271 merged[target].faces.extend(shell.faces);
272 } else {
273 group_of.insert(group, merged.len());
274 merged.push(shell);
275 }
276 }
277 solid.shells = merged;
278 before - solid.shells.len()
279}
280
281pub fn sew_solid(solid: &BrepSolid, tolerance: f64) -> Result<(BrepSolid, SewReport), String> {
290 if !(tolerance.is_finite() && tolerance > 0.0) {
291 return Err("sew_solid: tolerance must be positive".into());
292 }
293 let mut result = solid.clone();
294 let open_edges_before = one_use_edge_ids(&result).len();
295 let mut edges_sewn = 0usize;
296 let mut blocked = HashSet::<(u64, u64)>::default();
299 loop {
300 let one_use = one_use_edge_ids(&result);
301 let candidates = result
302 .edges
303 .iter()
304 .filter(|edge| !edge.degenerate && one_use.contains(&edge.id))
305 .cloned()
306 .collect::<Vec<_>>();
307 let points = result
308 .vertices
309 .iter()
310 .map(|vertex| (vertex.id, vertex.point))
311 .collect::<HashMap<_, _>>();
312 let mut chosen = None;
313 'pairs: for (index, first) in candidates.iter().enumerate() {
314 for second in candidates.iter().skip(index + 1) {
315 if blocked.contains(&(first.id, second.id)) {
316 continue;
317 }
318 let first_closed = first.start_vertex_id == first.end_vertex_id;
319 let second_closed = second.start_vertex_id == second.end_vertex_id;
320 if first_closed != second_closed {
321 continue;
322 }
323 if !first_closed {
324 let (Some(&fs), Some(&fe), Some(&ss), Some(&se)) = (
325 points.get(&first.start_vertex_id),
326 points.get(&first.end_vertex_id),
327 points.get(&second.start_vertex_id),
328 points.get(&second.end_vertex_id),
329 ) else {
330 continue;
331 };
332 let direct =
333 fs.sub(ss).length() <= tolerance && fe.sub(se).length() <= tolerance;
334 let reversed =
335 fs.sub(se).length() <= tolerance && fe.sub(ss).length() <= tolerance;
336 if !direct && !reversed {
337 continue;
338 }
339 }
340 if locus_deviation(first, second, 8)? > tolerance
341 || locus_deviation(second, first, 8)? > tolerance
342 {
343 continue;
344 }
345 chosen = Some((first.clone(), second.clone()));
346 break 'pairs;
347 }
348 }
349 let Some((keep, remove)) = chosen else {
350 break;
351 };
352 match plan_rebind(&result, &keep, &remove, tolerance) {
353 Ok(patches) => {
354 apply_rebind(&mut result, &keep, &remove, patches, tolerance);
355 edges_sewn += 1;
356 }
357 Err(_) => {
358 blocked.insert((keep.id, remove.id));
359 }
360 }
361 }
362 let used_vertices = result
364 .edges
365 .iter()
366 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
367 .collect::<HashSet<_>>();
368 result
369 .vertices
370 .retain(|vertex| used_vertices.contains(&vertex.id));
371
372 let shells_merged = merge_connected_shells(&mut result);
373 let mut oriented_outward = false;
374 if edges_sewn > 0 {
375 orient_open_solid_faces(&mut result)?;
379 if let Ok(volume) = solid_signed_volume(&result) {
380 if volume < 0.0 {
381 flip_all_faces(&mut result)?;
382 }
383 oriented_outward = volume.abs() > tolerance * tolerance * tolerance;
384 }
385 }
386 if edges_sewn > 0 || shells_merged > 0 {
391 let vertex_count = result.vertices.len() as i64;
392 let edge_count = result.edges.iter().filter(|edge| !edge.degenerate).count() as i64;
393 let face_count = result
394 .shells
395 .iter()
396 .map(|shell| shell.faces.len())
397 .sum::<usize>() as i64;
398 let ring_count = result
399 .shells
400 .iter()
401 .flat_map(|shell| &shell.faces)
402 .map(|face| face.loops.len().saturating_sub(1))
403 .sum::<usize>() as i64;
404 let numerator = 2 - (vertex_count - edge_count + face_count - ring_count);
405 if numerator >= 0 && numerator % 2 == 0 {
406 result.genus = numerator / 2;
407 }
408 }
409 let open_edges_after = one_use_edge_ids(&result).len();
410 let issues = result
411 .validate()
412 .into_iter()
413 .map(|issue| issue.message)
414 .collect();
415 Ok((
416 result,
417 SewReport {
418 edges_sewn,
419 shells_merged,
420 open_edges_before,
421 open_edges_after,
422 oriented_outward,
423 issues,
424 },
425 ))
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431 use crate::topology::VertexRecord;
432 use crate::{make_box_brep, make_cylinder_brep, solid_mass_properties, Vec4};
433
434 fn max_id(solid: &BrepSolid) -> u64 {
435 let vertex_max = solid.vertices.iter().map(|vertex| vertex.id).max();
436 let edge_max = solid.edges.iter().map(|edge| edge.id).max();
437 let face_max = solid
438 .shells
439 .iter()
440 .flat_map(|shell| &shell.faces)
441 .flat_map(|face| {
442 std::iter::once(face.id).chain(face.loops.iter().flat_map(|loop_record| {
443 std::iter::once(loop_record.id)
444 .chain(loop_record.coedges.iter().map(|coedge| coedge.id))
445 }))
446 })
447 .max();
448 [vertex_max, edge_max, face_max, Some(solid.id)]
449 .into_iter()
450 .flatten()
451 .max()
452 .unwrap_or(0)
453 }
454
455 fn detach_faces(solid: &BrepSolid, face_ids: &[u64]) -> BrepSolid {
459 let mut result = solid.clone();
460 let mut next = max_id(&result) + 1;
461 let mut moved = Vec::new();
462 for shell in &mut result.shells {
463 let mut kept = Vec::new();
464 for face in shell.faces.drain(..) {
465 if face_ids.contains(&face.id) {
466 moved.push(face);
467 } else {
468 kept.push(face);
469 }
470 }
471 shell.faces = kept;
472 }
473 assert_eq!(moved.len(), face_ids.len(), "all faces found");
474 let mut edge_map = HashMap::<u64, u64>::default();
475 let mut vertex_map = HashMap::<u64, u64>::default();
476 for face in &mut moved {
477 for coedge in face
478 .loops
479 .iter_mut()
480 .flat_map(|loop_record| &mut loop_record.coedges)
481 {
482 if let Some(&mapped) = edge_map.get(&coedge.edge_id) {
483 coedge.edge_id = mapped;
484 continue;
485 }
486 let mut duplicate = result
487 .edges
488 .iter()
489 .find(|edge| edge.id == coedge.edge_id)
490 .expect("edge exists")
491 .clone();
492 for vertex_id in [&mut duplicate.start_vertex_id, &mut duplicate.end_vertex_id] {
493 if let Some(&mapped) = vertex_map.get(vertex_id) {
494 *vertex_id = mapped;
495 continue;
496 }
497 let point = result
498 .vertices
499 .iter()
500 .find(|vertex| vertex.id == *vertex_id)
501 .expect("vertex exists")
502 .point;
503 result.vertices.push(VertexRecord { id: next, point });
504 vertex_map.insert(*vertex_id, next);
505 *vertex_id = next;
506 next += 1;
507 }
508 duplicate.id = next;
509 next += 1;
510 edge_map.insert(coedge.edge_id, duplicate.id);
511 coedge.edge_id = duplicate.id;
512 result.edges.push(duplicate);
513 }
514 }
515 result.shells.push(ShellRecord {
516 id: next,
517 faces: moved,
518 });
519 let used_edges = result
522 .shells
523 .iter()
524 .flat_map(|shell| &shell.faces)
525 .flat_map(|face| &face.loops)
526 .flat_map(|loop_record| &loop_record.coedges)
527 .map(|coedge| coedge.edge_id)
528 .collect::<HashSet<_>>();
529 result.edges.retain(|edge| used_edges.contains(&edge.id));
530 let used_vertices = result
531 .edges
532 .iter()
533 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
534 .collect::<HashSet<_>>();
535 result
536 .vertices
537 .retain(|vertex| used_vertices.contains(&vertex.id));
538 result
539 }
540
541 fn translate_shell(solid: &mut BrepSolid, shell_index: usize, delta: Vec3) {
545 let mut edge_ids = HashSet::default();
546 for face in &mut solid.shells[shell_index].faces {
547 for row in &mut face.surface.control_points {
548 for control in row.iter_mut() {
549 let point = control.point().unwrap().add(delta);
550 *control = Vec4::from_point(point, control.w);
551 }
552 }
553 for coedge in face
554 .loops
555 .iter_mut()
556 .flat_map(|loop_record| &mut loop_record.coedges)
557 {
558 edge_ids.insert(coedge.edge_id);
559 }
560 }
561 let mut vertex_ids = HashSet::default();
562 for edge in &mut solid.edges {
563 if !edge_ids.contains(&edge.id) {
564 continue;
565 }
566 for control in &mut edge.curve.control_points {
567 let point = control.point().unwrap().add(delta);
568 *control = Vec4::from_point(point, control.w);
569 }
570 vertex_ids.insert(edge.start_vertex_id);
571 vertex_ids.insert(edge.end_vertex_id);
572 }
573 for vertex in &mut solid.vertices {
574 if vertex_ids.contains(&vertex.id) {
575 vertex.point = vertex.point.add(delta);
576 }
577 }
578 }
579
580 fn one_use_count(solid: &BrepSolid) -> usize {
581 one_use_edge_ids(solid).len()
582 }
583
584 fn faces_beyond_x(solid: &BrepSolid, x: f64) -> Vec<u64> {
586 solid
587 .shells
588 .iter()
589 .flat_map(|shell| &shell.faces)
590 .filter(|face| {
591 face.surface
592 .evaluate(0.5, 0.5)
593 .map(|point| point.x > x)
594 .unwrap_or(false)
595 })
596 .map(|face| face.id)
597 .collect()
598 }
599
600 #[test]
601 fn sews_detached_box_faces_into_watertight_solid() {
602 let solid = make_box_brep(Vec3::default(), 4.0, 3.0, 2.0).unwrap();
603 let moved = faces_beyond_x(&solid, 3.9); assert_eq!(moved.len(), 1);
605 let detached = detach_faces(&solid, &moved);
606 assert_eq!(detached.shells.len(), 2);
607 assert_eq!(one_use_count(&detached), 8, "4 rim edges duplicated");
608
609 let (sewn, report) = sew_solid(&detached, 1e-6).unwrap();
610 assert_eq!(report.edges_sewn, 4, "{report:?}");
611 assert_eq!(report.shells_merged, 1);
612 assert_eq!(report.open_edges_after, 0);
613 assert!(report.issues.is_empty(), "{:?}", report.issues);
614 assert_eq!(sewn.shells.len(), 1);
615 assert_eq!(one_use_count(&sewn), 0);
616 let volume = solid_mass_properties(&sewn).unwrap().volume;
617 assert!((volume - 24.0).abs() < 1e-9, "{volume}");
618 }
619
620 #[test]
621 fn sews_a_flipped_component_and_orients_outward() {
622 let solid = make_box_brep(Vec3::default(), 4.0, 3.0, 2.0).unwrap();
623 let moved = faces_beyond_x(&solid, 3.9);
624 let mut detached = detach_faces(&solid, &moved);
625 crate::offset_shell::flip_shell_faces(&mut detached.shells[1]).unwrap();
628
629 let (sewn, report) = sew_solid(&detached, 1e-6).unwrap();
630 assert_eq!(report.edges_sewn, 4, "{report:?}");
631 assert!(report.issues.is_empty(), "{:?}", report.issues);
632 assert_eq!(one_use_count(&sewn), 0);
633 let volume = solid_signed_volume(&sewn).unwrap();
634 assert!((volume - 24.0).abs() < 1e-9, "outward positive: {volume}");
635 }
636
637 #[test]
638 fn sews_a_detached_cap_along_its_closed_rim() {
639 let axis = Vec3::new(0.0, 0.0, 1.0);
640 let solid = make_cylinder_brep(Vec3::default(), axis, 1.5, 3.0).unwrap();
641 let top = solid.shells[0]
643 .faces
644 .iter()
645 .max_by(|first, second| {
646 let height = |face: &FaceRecord| {
647 face.surface
648 .evaluate(0.5, 0.5)
649 .map(|point| point.z)
650 .unwrap_or(f64::NEG_INFINITY)
651 };
652 height(first).total_cmp(&height(second))
653 })
654 .unwrap()
655 .id;
656 let detached = detach_faces(&solid, &[top]);
657 assert_eq!(one_use_count(&detached), 2, "one closed rim duplicated");
658
659 let (sewn, report) = sew_solid(&detached, 1e-6).unwrap();
660 assert_eq!(report.edges_sewn, 1, "{report:?}");
661 assert_eq!(report.open_edges_after, 0);
662 assert!(report.issues.is_empty(), "{:?}", report.issues);
663 assert_eq!(sewn.shells.len(), 1);
664 let volume = solid_mass_properties(&sewn).unwrap().volume;
665 let expected = std::f64::consts::PI * 1.5 * 1.5 * 3.0;
666 assert!((volume - expected).abs() < 1e-6, "{volume} vs {expected}");
667 }
668
669 #[test]
670 fn sews_a_multi_face_group_with_shared_corner_vertices() {
671 let solid = make_box_brep(Vec3::default(), 4.0, 3.0, 2.0).unwrap();
674 let moved = solid.shells[0]
675 .faces
676 .iter()
677 .filter(|face| {
678 face.surface
679 .evaluate(0.5, 0.5)
680 .map(|point| point.x > 3.9 || point.y > 2.9 || point.z > 1.9)
681 .unwrap_or(false)
682 })
683 .map(|face| face.id)
684 .collect::<Vec<_>>();
685 assert_eq!(moved.len(), 3);
686 let detached = detach_faces(&solid, &moved);
687
688 let (sewn, report) = sew_solid(&detached, 1e-6).unwrap();
689 assert_eq!(report.open_edges_after, 0, "{report:?}");
690 assert!(report.issues.is_empty(), "{:?}", report.issues);
691 assert_eq!(sewn.shells.len(), 1);
692 let volume = solid_mass_properties(&sewn).unwrap().volume;
693 assert!((volume - 24.0).abs() < 1e-9, "{volume}");
694 }
695
696 #[test]
697 fn sews_reparametrized_duplicates_via_pcurve_refit() {
698 let solid = make_box_brep(Vec3::default(), 4.0, 3.0, 2.0).unwrap();
703 let moved = faces_beyond_x(&solid, 3.9);
704 let mut detached = detach_faces(&solid, &moved);
705 let duplicated = detached.shells[1]
706 .faces
707 .iter()
708 .flat_map(|face| &face.loops)
709 .flat_map(|loop_record| &loop_record.coedges)
710 .map(|coedge| coedge.edge_id)
711 .collect::<HashSet<_>>();
712 for edge in &mut detached.edges {
713 if duplicated.contains(&edge.id) {
714 edge.curve = edge.curve.reversed().unwrap();
715 [edge.t0, edge.t1] = edge.curve.domain().unwrap();
716 std::mem::swap(&mut edge.start_vertex_id, &mut edge.end_vertex_id);
717 }
718 }
719
720 let (sewn, report) = sew_solid(&detached, 1e-6).unwrap();
721 assert_eq!(report.edges_sewn, 4, "{report:?}");
722 assert_eq!(report.open_edges_after, 0);
723 assert!(report.issues.is_empty(), "{:?}", report.issues);
724 let volume = solid_mass_properties(&sewn).unwrap().volume;
725 assert!((volume - 24.0).abs() < 1e-9, "{volume}");
726 }
727
728 #[test]
729 fn sews_a_holed_face_with_ring_aware_genus() {
730 let block = make_box_brep(Vec3::default(), 4.0, 4.0, 2.0).unwrap();
736 let drill = make_cylinder_brep(
737 Vec3::new(2.0, 2.0, -1.0),
738 Vec3::new(0.0, 0.0, 1.0),
739 1.0,
740 4.0,
741 )
742 .unwrap();
743 let holed = crate::boolean_operation(
744 &block,
745 &drill,
746 crate::BooleanOperation::Subtract,
747 &crate::BooleanOptions::default(),
748 )
749 .unwrap();
750 let top = holed
751 .shells
752 .iter()
753 .flat_map(|shell| &shell.faces)
754 .find(|face| {
755 face.loops.len() > 1
756 && face
757 .surface
758 .evaluate(0.05, 0.05)
759 .map(|point| point.z > 1.9)
760 .unwrap_or(false)
761 })
762 .expect("holed top face")
763 .id;
764 let detached = detach_faces(&holed, &[top]);
765
766 let (sewn, report) = sew_solid(&detached, 1e-6).unwrap();
767 assert_eq!(report.open_edges_after, 0, "{report:?}");
768 assert!(report.issues.is_empty(), "{:?}", report.issues);
769 assert_eq!(sewn.shells.len(), 1);
770 let expected = 4.0 * 4.0 * 2.0 - std::f64::consts::PI * 2.0;
771 let volume = solid_mass_properties(&sewn).unwrap().volume;
772 assert!((volume - expected).abs() < 1e-6, "{volume} vs {expected}");
773 }
774
775 #[test]
776 fn watertight_input_is_a_no_op() {
777 let solid = make_box_brep(Vec3::default(), 4.0, 3.0, 2.0).unwrap();
778 let (sewn, report) = sew_solid(&solid, 1e-6).unwrap();
779 assert_eq!(report.edges_sewn, 0);
780 assert_eq!(report.shells_merged, 0);
781 assert_eq!(report.open_edges_before, 0);
782 assert_eq!(report.open_edges_after, 0);
783 assert!(report.issues.is_empty(), "{:?}", report.issues);
784 let volume = solid_mass_properties(&sewn).unwrap().volume;
785 assert!((volume - 24.0).abs() < 1e-9, "{volume}");
786 }
787
788 #[test]
789 fn reports_an_unsewable_gap_honestly() {
790 let solid = make_box_brep(Vec3::default(), 4.0, 3.0, 2.0).unwrap();
791 let moved = faces_beyond_x(&solid, 3.9);
792 let mut detached = detach_faces(&solid, &moved);
793 translate_shell(&mut detached, 1, Vec3::new(0.5, 0.0, 0.0));
794
795 let open_before = one_use_count(&detached);
796 let (sewn, report) = sew_solid(&detached, 1e-4).unwrap();
797 assert_eq!(report.edges_sewn, 0, "{report:?}");
798 assert_eq!(report.shells_merged, 0);
799 assert_eq!(report.open_edges_after, open_before);
800 assert_eq!(sewn.shells.len(), 2, "nothing force-welded");
801 }
802}
803
804pub fn split_pinched_vertices(solid: &mut BrepSolid) -> Result<usize, String> {
814 let mut split_count = 0usize;
815 let vertex_ids: Vec<u64> = solid.vertices.iter().map(|vertex| vertex.id).collect();
816 let mut next_id = solid
817 .vertices
818 .iter()
819 .map(|vertex| vertex.id)
820 .chain(solid.edges.iter().map(|edge| edge.id))
821 .max()
822 .unwrap_or(0)
823 + 1;
824 let edge_of_id: HashMap<u64, usize> = solid
829 .edges
830 .iter()
831 .enumerate()
832 .map(|(index, edge)| (edge.id, index))
833 .collect();
834 let vertex_of_id: HashMap<u64, usize> = solid
835 .vertices
836 .iter()
837 .enumerate()
838 .map(|(index, vertex)| (vertex.id, index))
839 .collect();
840 for vertex_id in vertex_ids {
841 let incident: Vec<u64> = solid
844 .edges
845 .iter()
846 .filter(|edge| edge.start_vertex_id == vertex_id || edge.end_vertex_id == vertex_id)
847 .map(|edge| edge.id)
848 .collect();
849 if incident.len() < 4 {
850 continue;
852 }
853 let index_of: HashMap<u64, usize> = incident
854 .iter()
855 .enumerate()
856 .map(|(index, id)| (*id, index))
857 .collect();
858 let mut parent: Vec<usize> = (0..incident.len()).collect();
859 fn root(parent: &mut [usize], index: usize) -> usize {
860 if parent[index] != index {
861 parent[index] = root(parent, parent[index]);
862 }
863 parent[index]
864 }
865 let edge_end = |edge_id: u64, forward: bool| -> Option<u64> {
866 edge_of_id.get(&edge_id).map(|&index| {
867 let edge = &solid.edges[index];
868 if forward {
869 edge.end_vertex_id
870 } else {
871 edge.start_vertex_id
872 }
873 })
874 };
875 for shell in &solid.shells {
876 for face in &shell.faces {
877 for loop_record in &face.loops {
878 let count = loop_record.coedges.len();
879 for index in 0..count {
880 let current = &loop_record.coedges[index];
881 let next = &loop_record.coedges[(index + 1) % count];
882 let Some(junction) = edge_end(current.edge_id, current.forward) else {
885 continue;
886 };
887 if junction != vertex_id {
888 continue;
889 }
890 let (Some(&a), Some(&b)) =
891 (index_of.get(¤t.edge_id), index_of.get(&next.edge_id))
892 else {
893 continue;
894 };
895 let ra = root(&mut parent, a);
896 let rb = root(&mut parent, b);
897 if ra != rb {
898 parent[rb] = ra;
899 }
900 }
901 }
902 }
903 }
904 let mut component_of: HashMap<usize, usize> = HashMap::default();
905 let mut components = 0usize;
906 let mut assignment: Vec<usize> = vec![0; incident.len()];
907 for index in 0..incident.len() {
908 let r = root(&mut parent, index);
909 let component = *component_of.entry(r).or_insert_with(|| {
910 components += 1;
911 components - 1
912 });
913 assignment[index] = component;
914 }
915 if components < 2 {
916 continue;
917 }
918 let point = vertex_of_id
921 .get(&vertex_id)
922 .map(|&index| solid.vertices[index].point)
923 .ok_or("split_pinched_vertices: vertex vanished")?;
924 let mut replacement_ids = vec![vertex_id];
925 for _ in 1..components {
926 let id = next_id;
927 next_id += 1;
928 solid.vertices.push(VertexRecord { id, point });
929 replacement_ids.push(id);
930 }
931 for (offset, edge_id) in incident.iter().enumerate() {
932 let replacement = replacement_ids[assignment[offset]];
933 if replacement == vertex_id {
934 continue;
935 }
936 if let Some(edge) = solid.edges.iter_mut().find(|edge| edge.id == *edge_id) {
937 if edge.start_vertex_id == vertex_id {
938 edge.start_vertex_id = replacement;
939 }
940 if edge.end_vertex_id == vertex_id {
941 edge.end_vertex_id = replacement;
942 }
943 }
944 }
945 split_count += components - 1;
946 }
947 Ok(split_count)
948}
949
950#[cfg(test)]
951mod pinch_tests {
952 use super::*;
953 use crate::make_box_brep;
954
955 #[test]
956 fn a_hand_welded_corner_pinch_splits_back_to_manifold() {
957 let first = make_box_brep(Vec3::default(), 2.0, 2.0, 2.0).unwrap();
961 let second = make_box_brep(Vec3::new(2.0, 2.0, 2.0), 2.0, 2.0, 2.0).unwrap();
962 let mut solid = first.clone();
964 let offset = 1000u64;
965 let mut moved = second.clone();
966 for vertex in &mut moved.vertices {
967 vertex.id += offset;
968 }
969 for edge in &mut moved.edges {
970 edge.id += offset;
971 edge.start_vertex_id += offset;
972 edge.end_vertex_id += offset;
973 }
974 for shell in &mut moved.shells {
975 shell.id += offset;
976 for face in &mut shell.faces {
977 face.id += offset;
978 for loop_record in &mut face.loops {
979 loop_record.id += offset;
980 for coedge in &mut loop_record.coedges {
981 coedge.id += offset;
982 coedge.edge_id += offset;
983 }
984 }
985 }
986 }
987 solid.vertices.extend(moved.vertices);
988 solid.edges.extend(moved.edges);
989 solid.shells.extend(moved.shells);
990
991 let corner = Vec3::new(2.0, 2.0, 2.0);
993 let ids: Vec<u64> = solid
994 .vertices
995 .iter()
996 .filter(|vertex| vertex.point.sub(corner).length() < 1e-9)
997 .map(|vertex| vertex.id)
998 .collect();
999 assert_eq!(ids.len(), 2, "both corner vertices present");
1000 let (keep, remove) = (ids[0], ids[1]);
1001 for edge in &mut solid.edges {
1002 if edge.start_vertex_id == remove {
1003 edge.start_vertex_id = keep;
1004 }
1005 if edge.end_vertex_id == remove {
1006 edge.end_vertex_id = keep;
1007 }
1008 }
1009 solid.vertices.retain(|vertex| vertex.id != remove);
1010
1011 let split = split_pinched_vertices(&mut solid).unwrap();
1013 assert_eq!(split, 1, "exactly one fan duplicated");
1014 assert_eq!(
1015 solid
1016 .vertices
1017 .iter()
1018 .filter(|vertex| vertex.point.sub(corner).length() < 1e-9)
1019 .count(),
1020 2,
1021 "corner vertices restored"
1022 );
1023 assert!(solid.validate().is_empty(), "{:?}", solid.validate());
1025 }
1026
1027 #[test]
1028 fn manifold_solids_are_untouched() {
1029 let mut solid = make_box_brep(Vec3::default(), 2.0, 2.0, 2.0).unwrap();
1030 let before = solid.vertices.len();
1031 assert_eq!(split_pinched_vertices(&mut solid).unwrap(), 0);
1032 assert_eq!(solid.vertices.len(), before);
1033 }
1034}