1use std::collections::{HashMap, HashSet};
9
10use brepkit_math::aabb::Aabb3;
11use brepkit_math::tolerance::Tolerance;
12use brepkit_math::vec::{Point3, Vec3};
13use brepkit_topology::Topology;
14use brepkit_topology::edge::{Edge, EdgeCurve, EdgeId};
15use brepkit_topology::face::{Face, FaceId, FaceSurface};
16use brepkit_topology::shell::Shell;
17use brepkit_topology::solid::{Solid, SolidId};
18use brepkit_topology::vertex::{Vertex, VertexId};
19use brepkit_topology::wire::{OrientedEdge, Wire, WireId};
20
21use super::classify::polygon_centroid;
22use super::face_polygon;
23use super::types::{FaceSpec, MIN_SOLID_FACES};
24
25#[inline]
27#[allow(clippy::cast_possible_truncation)] pub(super) fn quantize(v: f64, resolution: f64) -> i64 {
29 (v * resolution).round() as i64
30}
31
32#[inline]
34pub(super) fn quantize_point(p: Point3, resolution: f64) -> (i64, i64, i64) {
35 (
36 quantize(p.x(), resolution),
37 quantize(p.y(), resolution),
38 quantize(p.z(), resolution),
39 )
40}
41
42pub(super) fn vertex_merge_resolution(
48 all_pts: impl Iterator<Item = Point3>,
49 tol: Tolerance,
50) -> f64 {
51 let fallback = 1.0 / tol.linear;
52 if let Some(bbox) = Aabb3::try_from_points(all_pts) {
53 let diagonal = (bbox.max - bbox.min).length();
54 if diagonal > tol.linear {
55 1.0 / (diagonal * 1e-7_f64)
58 } else {
59 fallback
60 }
61 } else {
62 fallback
63 }
64}
65
66#[allow(clippy::redundant_pub_crate)]
72pub(crate) fn assemble_solid(
73 topo: &mut Topology,
74 faces: &[(Vec<Point3>, Vec3, f64)],
75 tol: Tolerance,
76) -> Result<SolidId, crate::OperationsError> {
77 let specs: Vec<FaceSpec> = faces
78 .iter()
79 .map(|(verts, normal, d)| FaceSpec::Planar {
80 vertices: verts.clone(),
81 normal: *normal,
82 d: *d,
83 inner_wires: vec![],
84 })
85 .collect();
86 assemble_solid_mixed(topo, &specs, tol)
87}
88
89fn build_inner_wires(
95 topo: &mut Topology,
96 inner_wire_specs: &[Vec<Point3>],
97 vertex_map: &mut HashMap<(i64, i64, i64), VertexId>,
98 edge_map: &mut HashMap<(usize, usize), EdgeId>,
99 resolution: f64,
100 tol: Tolerance,
101) -> Result<Vec<WireId>, crate::OperationsError> {
102 let mut inner_wire_ids = Vec::with_capacity(inner_wire_specs.len());
103 for iw_verts in inner_wire_specs {
104 let iw_n = iw_verts.len();
105 if iw_n < 3 {
106 continue;
107 }
108
109 let iw_vert_ids: Vec<VertexId> = iw_verts
110 .iter()
111 .map(|p| {
112 let key = quantize_point(*p, resolution);
113 *vertex_map
114 .entry(key)
115 .or_insert_with(|| topo.add_vertex(Vertex::new(*p, tol.linear)))
116 })
117 .collect();
118
119 let mut iw_oriented_edges = Vec::with_capacity(iw_n);
120 for i in 0..iw_n {
121 let j = (i + 1) % iw_n;
122 let vi = iw_vert_ids[i].index();
123 let vj = iw_vert_ids[j].index();
124 let (key_min, key_max) = if vi <= vj { (vi, vj) } else { (vj, vi) };
125
126 let edge_id = *edge_map.entry((key_min, key_max)).or_insert_with(|| {
127 topo.add_edge(Edge::new(iw_vert_ids[i], iw_vert_ids[j], EdgeCurve::Line))
128 });
129 let is_forward = topo.edge(edge_id)?.start() == iw_vert_ids[i];
132
133 iw_oriented_edges.push(OrientedEdge::new(edge_id, is_forward));
134 }
135
136 let wire = Wire::new(iw_oriented_edges, true).map_err(crate::OperationsError::Topology)?;
137 inner_wire_ids.push(topo.add_wire(wire));
138 }
139 Ok(inner_wire_ids)
140}
141
142#[allow(clippy::redundant_pub_crate)]
151pub(crate) fn assemble_solid_mixed(
152 topo: &mut Topology,
153 face_specs: &[FaceSpec],
154 tol: Tolerance,
155) -> Result<SolidId, crate::OperationsError> {
156 let n = face_specs.len();
159 log::debug!(
160 "assemble_solid_mixed: {n} specs; arena before reserve V={} E={} W={} F={}",
161 topo.num_vertices(),
162 topo.num_edges(),
163 topo.num_wires(),
164 topo.num_faces(),
165 );
166 topo.reserve(n.saturating_mul(2), n.saturating_mul(3), n, n, 1, 1);
167
168 let resolution = vertex_merge_resolution(
169 face_specs.iter().flat_map(|s| match s {
170 FaceSpec::Planar { vertices, .. }
171 | FaceSpec::Surface { vertices, .. }
172 | FaceSpec::CylindricalFace { vertices, .. } => vertices.iter().copied(),
173 }),
174 tol,
175 );
176
177 let mut vertex_map: HashMap<(i64, i64, i64), VertexId> =
178 HashMap::with_capacity(face_specs.len() * 4);
179 let mut edge_map: HashMap<(usize, usize), brepkit_topology::edge::EdgeId> =
180 HashMap::with_capacity(face_specs.len() * 4);
181
182 let mut face_ids = Vec::with_capacity(face_specs.len());
183
184 let cylindrical_first = face_specs
188 .iter()
189 .filter(|s| matches!(s, FaceSpec::CylindricalFace { .. }))
190 .chain(
191 face_specs
192 .iter()
193 .filter(|s| !matches!(s, FaceSpec::CylindricalFace { .. })),
194 );
195
196 for spec in cylindrical_first {
197 match spec {
198 FaceSpec::CylindricalFace {
199 vertices,
200 cylinder,
201 reversed,
202 ..
203 } => {
204 let verts = vertices;
205 let n = verts.len();
206 if n < 3 {
207 continue;
208 }
209
210 let vert_ids: Vec<VertexId> = verts
211 .iter()
212 .map(|p| {
213 let key = quantize_point(*p, resolution);
214 *vertex_map
215 .entry(key)
216 .or_insert_with(|| topo.add_vertex(Vertex::new(*p, tol.linear)))
217 })
218 .collect();
219
220 let mut oriented_edges = Vec::with_capacity(n);
221 for i in 0..n {
222 let j = (i + 1) % n;
223 let vi = vert_ids[i].index();
224 let vj = vert_ids[j].index();
225 if vi == vj {
226 continue; }
228 let (key_min, key_max) = if vi <= vj { (vi, vj) } else { (vj, vi) };
229
230 let edge_id = *edge_map.entry((key_min, key_max)).or_insert_with(|| {
231 let start = vert_ids[i];
232 let end = vert_ids[j];
233
234 let (u1, v1) = cylinder.project_point(verts[i]);
237 let (u2, v2) = cylinder.project_point(verts[j]);
238 let u_diff = (u1 - u2).abs();
239 let v_diff = (v1 - v2).abs();
240
241 if u_diff > tol.linear
244 && u_diff < (std::f64::consts::TAU - tol.linear)
245 && v_diff < tol.linear * 100.0
246 {
247 let mut du = u2 - u1;
254 if du > std::f64::consts::PI {
255 du -= std::f64::consts::TAU;
256 } else if du < -std::f64::consts::PI {
257 du += std::f64::consts::TAU;
258 }
259 let axis = if du >= 0.0 {
260 cylinder.axis()
261 } else {
262 -cylinder.axis()
263 };
264 let center = cylinder.origin() + cylinder.axis() * ((v1 + v2) * 0.5);
266 if let Ok(circle) =
267 brepkit_math::curves::Circle3D::new(center, axis, cylinder.radius())
268 {
269 topo.add_edge(Edge::new(start, end, EdgeCurve::Circle(circle)))
270 } else {
271 topo.add_edge(Edge::new(start, end, EdgeCurve::Line))
272 }
273 } else {
274 topo.add_edge(Edge::new(start, end, EdgeCurve::Line))
276 }
277 });
278 let is_forward = topo.edge(edge_id)?.start() == vert_ids[i];
279
280 if oriented_edges
281 .last()
282 .is_some_and(|last: &OrientedEdge| last.edge() == edge_id)
283 {
284 continue;
285 }
286 oriented_edges.push(OrientedEdge::new(edge_id, is_forward));
287 }
288
289 if oriented_edges.is_empty() {
292 continue;
293 }
294
295 if *reversed {
302 oriented_edges.reverse();
303 for oe in &mut oriented_edges {
304 *oe = OrientedEdge::new(oe.edge(), !oe.is_forward());
305 }
306 }
307
308 let wire =
309 Wire::new(oriented_edges, true).map_err(crate::OperationsError::Topology)?;
310 let wire_id = topo.add_wire(wire);
311
312 let inner_wire_ids = build_inner_wires(
314 topo,
315 spec.inner_wires(),
316 &mut vertex_map,
317 &mut edge_map,
318 resolution,
319 tol,
320 )?;
321
322 let surface = FaceSurface::Cylinder(cylinder.clone());
323 let face = if *reversed {
324 topo.add_face(Face::new_reversed(wire_id, inner_wire_ids, surface))
325 } else {
326 topo.add_face(Face::new(wire_id, inner_wire_ids, surface))
327 };
328 face_ids.push(face);
329 }
330 spec => {
331 let (verts, surface, reversed) = match spec {
333 FaceSpec::Planar {
334 vertices,
335 normal,
336 d,
337 ..
338 } => (
339 vertices.clone(),
340 FaceSurface::Plane {
341 normal: *normal,
342 d: *d,
343 },
344 false,
345 ),
346 FaceSpec::Surface {
347 vertices,
348 surface,
349 reversed,
350 ..
351 } => (vertices.clone(), surface.clone(), *reversed),
352 FaceSpec::CylindricalFace { .. } => unreachable!(),
353 };
354
355 let n = verts.len();
356 if n < 3 {
357 continue;
358 }
359
360 let vert_ids: Vec<VertexId> = verts
361 .iter()
362 .map(|p| {
363 let key = quantize_point(*p, resolution);
364 *vertex_map
365 .entry(key)
366 .or_insert_with(|| topo.add_vertex(Vertex::new(*p, tol.linear)))
367 })
368 .collect();
369
370 let mut oriented_edges = Vec::with_capacity(n);
371 for i in 0..n {
372 let j = (i + 1) % n;
373 let vi = vert_ids[i].index();
374 let vj = vert_ids[j].index();
375 if vi == vj {
377 continue;
378 }
379 let (key_min, key_max) = if vi <= vj { (vi, vj) } else { (vj, vi) };
380
381 let edge_id = *edge_map.entry((key_min, key_max)).or_insert_with(|| {
382 topo.add_edge(Edge::new(vert_ids[i], vert_ids[j], EdgeCurve::Line))
383 });
384 let is_forward = topo.edge(edge_id)?.start() == vert_ids[i];
389
390 if oriented_edges
394 .iter()
395 .any(|oe: &OrientedEdge| oe.edge() == edge_id)
396 {
397 continue;
398 }
399 oriented_edges.push(OrientedEdge::new(edge_id, is_forward));
400 }
401
402 if oriented_edges.is_empty() {
407 continue;
408 }
409
410 let wire =
411 Wire::new(oriented_edges, true).map_err(crate::OperationsError::Topology)?;
412 let wire_id = topo.add_wire(wire);
413
414 let inner_wire_ids = build_inner_wires(
416 topo,
417 spec.inner_wires(),
418 &mut vertex_map,
419 &mut edge_map,
420 resolution,
421 tol,
422 )?;
423
424 let face = if reversed {
425 topo.add_face(Face::new_reversed(wire_id, inner_wire_ids, surface))
426 } else {
427 topo.add_face(Face::new(wire_id, inner_wire_ids, surface))
428 };
429 face_ids.push(face);
430 }
431 }
432 }
433
434 if face_ids.is_empty() {
435 return Err(crate::OperationsError::InvalidInput {
436 reason: "solid assembly produced no faces".into(),
437 });
438 }
439
440 let vertex_positions: HashMap<VertexId, Point3> = vertex_map
445 .values()
446 .filter_map(|&vid| topo.vertex(vid).ok().map(|v| (vid, v.point())))
447 .collect();
448 refine_boundary_edges(
449 topo,
450 &mut face_ids,
451 &mut edge_map,
452 tol,
453 Some(&vertex_positions),
454 )?;
455
456 stitch_boundary_edges(topo, &mut face_ids, tol)?;
459
460 let mut shell_face_ids = build_manifold_shell(topo, &face_ids)?;
464
465 for _ in 0..3 {
468 split_nonmanifold_edges(topo, &mut shell_face_ids)?;
469 }
470
471 let shell = Shell::new(shell_face_ids).map_err(crate::OperationsError::Topology)?;
472 let shell_id = topo.add_shell(shell);
473 Ok(topo.add_solid(Solid::new(shell_id, vec![])))
474}
475
476fn build_manifold_shell(
486 topo: &Topology,
487 face_ids: &[FaceId],
488) -> Result<Vec<FaceId>, crate::OperationsError> {
489 if face_ids.is_empty() {
490 return Ok(Vec::new());
491 }
492
493 let mut edge_faces: HashMap<EdgeId, Vec<(usize, bool)>> = HashMap::new();
495 for (fi, &fid) in face_ids.iter().enumerate() {
496 let face = topo.face(fid)?;
497 for wid in std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied()) {
498 let wire = topo.wire(wid)?;
499 for oe in wire.edges() {
500 edge_faces
501 .entry(oe.edge())
502 .or_default()
503 .push((fi, oe.is_forward()));
504 }
505 }
506 }
507
508 let mut nonmanifold: Vec<(EdgeId, Vec<(usize, bool)>)> = edge_faces
515 .into_iter()
516 .filter(|(_, faces)| faces.len() > 2)
517 .collect();
518 nonmanifold.sort_by_key(|(eid, _)| eid.index());
519
520 if nonmanifold.is_empty() {
521 return Ok(face_ids.to_vec());
522 }
523
524 let mut faces_to_remove: HashSet<usize> = HashSet::new();
527 for (_eid, face_refs) in &nonmanifold {
531 let mut has_opposing = false;
536 let face_normals: Vec<(usize, Vec3)> = face_refs
537 .iter()
538 .filter_map(|&(fi, _)| {
539 let face = topo.face(face_ids[fi]).ok()?;
540 let n = match face.surface() {
541 FaceSurface::Plane { normal, .. } => {
542 if face.is_reversed() {
543 -*normal
544 } else {
545 *normal
546 }
547 }
548 _ => return None, };
550 Some((fi, n))
551 })
552 .collect();
553
554 for i in 0..face_normals.len() {
555 for j in (i + 1)..face_normals.len() {
556 if face_normals[i].1.dot(face_normals[j].1) < -0.5 {
557 has_opposing = true;
558 }
559 }
560 }
561
562 if !has_opposing {
563 continue;
566 }
567
568 for i in 0..face_normals.len() {
576 let mut opposing_count = 0;
577 for j in 0..face_normals.len() {
578 if i != j && face_normals[i].1.dot(face_normals[j].1) < -0.5 {
579 opposing_count += 1;
580 }
581 }
582 if opposing_count > face_normals.len() / 2 {
584 faces_to_remove.insert(face_normals[i].0);
585 }
586 }
587 }
588
589 if !faces_to_remove.is_empty() {
591 let result: Vec<FaceId> = face_ids
592 .iter()
593 .enumerate()
594 .filter(|(i, _)| !faces_to_remove.contains(i))
595 .map(|(_, &fid)| fid)
596 .collect();
597 return Ok(result);
598 }
599
600 Ok(face_ids.to_vec())
601}
602
603pub(super) fn validate_boolean_result(
616 topo: &Topology,
617 solid: SolidId,
618) -> Result<(), crate::OperationsError> {
619 validate_boolean_result_lenient(topo, solid)?;
620
621 let mut unclosed_wires = 0usize;
625 let mut edge_uses: HashMap<usize, usize> = HashMap::new();
626 let nm_dump = std::env::var("BK_DUMP_NM").is_ok();
627 let mut edge_by_idx: HashMap<usize, brepkit_topology::edge::EdgeId> = HashMap::new();
628 for fid in brepkit_topology::explorer::solid_faces(topo, solid)? {
629 let face = topo.face(fid)?;
630 for wid in std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied()) {
631 let wire = topo.wire(wid)?;
632 if brepkit_topology::validation::validate_wire_closed(wire, topo).is_err() {
633 unclosed_wires += 1;
634 }
635 for oe in wire.edges() {
636 *edge_uses.entry(oe.edge().index()).or_insert(0) += 1;
637 if nm_dump {
638 edge_by_idx
639 .entry(oe.edge().index())
640 .or_insert_with(|| oe.edge());
641 }
642 }
643 }
644 }
645 let non_manifold_edges = edge_uses.values().filter(|&&c| c > 2).count();
646 if non_manifold_edges > 0 && nm_dump {
647 for (&eid, &c) in &edge_uses {
648 if c > 2
649 && let Some(&real_eid) = edge_by_idx.get(&eid)
650 && let Ok(e) = topo.edge(real_eid)
651 && let (Ok(a), Ok(b)) = (topo.vertex(e.start()), topo.vertex(e.end()))
652 {
653 let pa = a.point();
654 let pb = b.point();
655 log::warn!(
656 "NM edge {eid} x{c} {} ({:.3},{:.3},{:.3})->({:.3},{:.3},{:.3})",
657 e.curve().type_tag(),
658 pa.x(),
659 pa.y(),
660 pa.z(),
661 pb.x(),
662 pb.y(),
663 pb.z()
664 );
665 for fid in brepkit_topology::explorer::solid_faces(topo, solid).unwrap_or_default()
666 {
667 let Ok(f) = topo.face(fid) else { continue };
668 for wid in
669 std::iter::once(f.outer_wire()).chain(f.inner_wires().iter().copied())
670 {
671 let Ok(w) = topo.wire(wid) else { continue };
672 for oe in w.edges() {
673 if oe.edge() == real_eid {
674 let n_out = topo
675 .wire(f.outer_wire())
676 .map(|w| w.edges().len())
677 .unwrap_or(0);
678 log::warn!(
679 " user face {fid:?} ({}) outer_edges={n_out} inners={}",
680 f.surface().type_tag(),
681 f.inner_wires().len()
682 );
683 }
684 }
685 }
686 }
687 }
688 }
689 }
690 let free_edges = edge_uses.values().filter(|&&c| c == 1).count();
698 if unclosed_wires > 0 || non_manifold_edges > 0 || free_edges > 0 {
699 return Err(crate::OperationsError::InvalidInput {
700 reason: format!(
701 "boolean result has {unclosed_wires} unclosed wire(s), \
702 {non_manifold_edges} non-manifold edge(s), and \
703 {free_edges} free boundary edge(s)"
704 ),
705 });
706 }
707
708 Ok(())
709}
710
711pub(super) fn validate_boolean_result_lenient(
714 topo: &Topology,
715 solid: SolidId,
716) -> Result<(), crate::OperationsError> {
717 let s = topo.solid(solid)?;
718 let shell = topo.shell(s.outer_shell())?;
719 let face_count = shell.faces().len();
720
721 if face_count < MIN_SOLID_FACES {
722 return Err(crate::OperationsError::InvalidInput {
723 reason: format!(
724 "boolean result has only {face_count} faces (minimum {MIN_SOLID_FACES} required for a closed solid)"
725 ),
726 });
727 }
728
729 let (f, e, v) = brepkit_topology::explorer::solid_entity_counts(topo, solid)?;
731 if e == 0 || v == 0 {
732 return Err(crate::OperationsError::InvalidInput {
733 reason: format!("boolean result has degenerate topology (F={f}, E={e}, V={v})"),
734 });
735 }
736
737 match crate::validate::validate_solid(topo, solid) {
744 Ok(report) if !report.is_valid() => {
745 let errors: Vec<_> = report
746 .issues
747 .iter()
748 .filter(|i| i.severity == crate::validate::Severity::Error)
749 .map(|i| i.description.as_str())
750 .collect();
751 log::warn!(
752 "boolean result has {} validation error(s): {}",
753 errors.len(),
754 errors.join("; ")
755 );
756 }
757 Err(e) => {
758 log::warn!("validate_solid failed (skipping validation): {e}");
759 }
760 Ok(_) => {}
761 }
762
763 Ok(())
764}
765
766pub(super) fn face_components(topo: &Topology, solid: SolidId) -> Vec<Vec<FaceId>> {
772 let shell = match topo.solid(solid).and_then(|s| topo.shell(s.outer_shell())) {
773 Ok(sh) => sh,
774 Err(_) => return Vec::new(),
775 };
776 let face_ids: Vec<FaceId> = shell.faces().to_vec();
777 if face_ids.is_empty() {
778 return Vec::new();
779 }
780 let n = face_ids.len();
781
782 let mut edge_faces: HashMap<usize, Vec<usize>> = HashMap::new();
783 for (fi, &fid) in face_ids.iter().enumerate() {
784 let Ok(face) = topo.face(fid) else { continue };
785 for wid in std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied()) {
786 let Ok(wire) = topo.wire(wid) else { continue };
787 for oe in wire.edges() {
788 edge_faces.entry(oe.edge().index()).or_default().push(fi);
789 }
790 }
791 }
792
793 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
794 for faces_at_edge in edge_faces.values() {
795 for &fi in faces_at_edge {
796 for &fj in faces_at_edge {
797 if fi != fj {
798 adj[fi].push(fj);
799 }
800 }
801 }
802 }
803
804 for neighbors in &mut adj {
810 neighbors.sort_unstable();
811 neighbors.dedup();
812 }
813
814 let mut visited = vec![false; n];
815 let mut components: Vec<Vec<FaceId>> = Vec::new();
816 for start in 0..n {
817 if visited[start] {
818 continue;
819 }
820 let mut comp_faces = Vec::new();
821 let mut stack = vec![start];
822 while let Some(fi) = stack.pop() {
823 if visited[fi] {
824 continue;
825 }
826 visited[fi] = true;
827 comp_faces.push(face_ids[fi]);
828 for &nfi in &adj[fi] {
829 if !visited[nfi] {
830 stack.push(nfi);
831 }
832 }
833 }
834 components.push(comp_faces);
835 }
836 components
837}
838
839#[allow(clippy::too_many_lines)]
846pub(super) fn refine_boundary_edges(
847 topo: &mut Topology,
848 face_ids: &mut [FaceId],
849 edge_map: &mut HashMap<(usize, usize), EdgeId>,
850 tol: Tolerance,
851 precomputed_positions: Option<&HashMap<VertexId, Point3>>,
852) -> Result<(), crate::OperationsError> {
853 let mut edge_face_count: HashMap<EdgeId, usize> = HashMap::new();
856 let mut edge_vertices: HashMap<EdgeId, (VertexId, VertexId)> = HashMap::new();
857 for &fid in face_ids.iter() {
858 let face = topo.face(fid)?;
859 for wid in std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied()) {
860 let wire = topo.wire(wid)?;
861 for oe in wire.edges() {
862 let eid = oe.edge();
863 *edge_face_count.entry(eid).or_default() += 1;
864 if let std::collections::hash_map::Entry::Vacant(e) = edge_vertices.entry(eid)
865 && let Ok(edge) = topo.edge(eid)
866 {
867 e.insert((edge.start(), edge.end()));
868 }
869 }
870 }
871 }
872
873 let boundary_edges: HashSet<EdgeId> = edge_face_count
875 .iter()
876 .filter(|&(_, &count)| count == 1)
877 .map(|(&eid, _)| eid)
878 .collect();
879
880 if boundary_edges.is_empty() {
881 return Ok(());
882 }
883
884 let mut extra_positions: HashMap<VertexId, Point3> = HashMap::new();
888 for &(start, end) in edge_vertices.values() {
889 for &vid in &[start, end] {
890 let in_pre = precomputed_positions.is_some_and(|p| p.contains_key(&vid));
891 if !in_pre
892 && let std::collections::hash_map::Entry::Vacant(e) = extra_positions.entry(vid)
893 && let Ok(v) = topo.vertex(vid)
894 {
895 e.insert(v.point());
896 }
897 }
898 }
899
900 let get_pos = |vid: &VertexId| -> Option<Point3> {
904 precomputed_positions
905 .and_then(|p| p.get(vid))
906 .or_else(|| extra_positions.get(vid))
907 .copied()
908 };
909 let mut seen: HashSet<VertexId> = HashSet::new();
911 let mut vert_list: Vec<(VertexId, Point3)> = Vec::new();
912 if let Some(pre) = precomputed_positions {
913 for (&vid, &pos) in pre {
914 if seen.insert(vid) {
915 vert_list.push((vid, pos));
916 }
917 }
918 }
919 for (&vid, &pos) in &extra_positions {
920 if seen.insert(vid) {
921 vert_list.push((vid, pos));
922 }
923 }
924
925 let (mut bb_min, mut bb_max) = (
933 Point3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY),
934 Point3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
935 );
936 for &(_, pos) in &vert_list {
937 bb_min = Point3::new(
938 bb_min.x().min(pos.x()),
939 bb_min.y().min(pos.y()),
940 bb_min.z().min(pos.z()),
941 );
942 bb_max = Point3::new(
943 bb_max.x().max(pos.x()),
944 bb_max.y().max(pos.y()),
945 bb_max.z().max(pos.z()),
946 );
947 }
948 let diag = ((bb_max.x() - bb_min.x()).powi(2)
949 + (bb_max.y() - bb_min.y()).powi(2)
950 + (bb_max.z() - bb_min.z()).powi(2))
951 .sqrt();
952 let cell_size = (diag / (vert_list.len() as f64).cbrt()).max(tol.linear);
953 let inv_cell = 1.0 / cell_size;
954
955 let mut grid: HashMap<(i64, i64, i64), Vec<usize>> = HashMap::new();
956 for (i, &(_, pos)) in vert_list.iter().enumerate() {
957 let cx = (pos.x() * inv_cell).floor() as i64;
958 let cy = (pos.y() * inv_cell).floor() as i64;
959 let cz = (pos.z() * inv_cell).floor() as i64;
960 grid.entry((cx, cy, cz)).or_default().push(i);
961 }
962
963 let mut edge_splits: HashMap<EdgeId, Vec<VertexId>> = HashMap::new();
964
965 for &eid in &boundary_edges {
966 let &(start_vid, end_vid) = match edge_vertices.get(&eid) {
967 Some(v) => v,
968 None => continue,
969 };
970 let (p0, p1) = match (get_pos(&start_vid), get_pos(&end_vid)) {
971 (Some(a), Some(b)) => (a, b),
972 _ => continue,
973 };
974 let dx = p1.x() - p0.x();
975 let dy = p1.y() - p0.y();
976 let dz = p1.z() - p0.z();
977 let len_sq = dx * dx + dy * dy + dz * dz;
978 if len_sq < tol.linear * tol.linear {
979 continue;
980 }
981 let len = len_sq.sqrt();
982
983 let edge_aabb = Aabb3 {
985 min: Point3::new(p0.x().min(p1.x()), p0.y().min(p1.y()), p0.z().min(p1.z())),
986 max: Point3::new(p0.x().max(p1.x()), p0.y().max(p1.y()), p0.z().max(p1.z())),
987 }
988 .expanded(tol.linear);
989 let min_cx = (edge_aabb.min.x() * inv_cell).floor() as i64;
990 let min_cy = (edge_aabb.min.y() * inv_cell).floor() as i64;
991 let min_cz = (edge_aabb.min.z() * inv_cell).floor() as i64;
992 let max_cx = (edge_aabb.max.x() * inv_cell).floor() as i64;
993 let max_cy = (edge_aabb.max.y() * inv_cell).floor() as i64;
994 let max_cz = (edge_aabb.max.z() * inv_cell).floor() as i64;
995
996 let mut intermediates: Vec<(f64, VertexId)> = Vec::new();
997
998 for gx in min_cx..=max_cx {
999 for gy in min_cy..=max_cy {
1000 for gz in min_cz..=max_cz {
1001 if let Some(indices) = grid.get(&(gx, gy, gz)) {
1002 for &cand_idx in indices {
1003 let (vid, pos) = vert_list[cand_idx];
1004 if vid == start_vid || vid == end_vid {
1005 continue;
1006 }
1007 let dpx = pos.x() - p0.x();
1009 let dpy = pos.y() - p0.y();
1010 let dpz = pos.z() - p0.z();
1011 let t = (dpx * dx + dpy * dy + dpz * dz) / len_sq;
1012
1013 if t <= tol.linear / len || t >= 1.0 - tol.linear / len {
1015 continue;
1016 }
1017
1018 let proj_x = p0.x() + t * dx;
1020 let proj_y = p0.y() + t * dy;
1021 let proj_z = p0.z() + t * dz;
1022 let dist_sq = (pos.x() - proj_x).powi(2)
1023 + (pos.y() - proj_y).powi(2)
1024 + (pos.z() - proj_z).powi(2);
1025
1026 if dist_sq < tol.linear * tol.linear {
1027 intermediates.push((t, vid));
1028 }
1029 }
1030 }
1031 }
1032 }
1033 }
1034
1035 if !intermediates.is_empty() {
1036 intermediates
1037 .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1038 intermediates.dedup_by_key(|(_, vid)| *vid);
1039 edge_splits.insert(eid, intermediates.into_iter().map(|(_, vid)| vid).collect());
1040 }
1041 }
1042
1043 if edge_splits.is_empty() {
1044 return Ok(());
1045 }
1046
1047 for fi in 0..face_ids.len() {
1048 let fid = face_ids[fi];
1049 let face = topo.face(fid)?;
1050 let outer_wire_id = face.outer_wire();
1051 let outer_wire = topo.wire(outer_wire_id)?;
1052
1053 let mut needs_rebuild = false;
1054 for oe in outer_wire.edges() {
1055 if edge_splits.contains_key(&oe.edge()) {
1056 needs_rebuild = true;
1057 break;
1058 }
1059 }
1060
1061 if !needs_rebuild {
1062 continue;
1063 }
1064
1065 let surface = face.surface().clone();
1067 let inner_wires = face.inner_wires().to_vec();
1068 let is_reversed = face.is_reversed();
1069 let old_edges: Vec<OrientedEdge> = outer_wire.edges().to_vec();
1070
1071 let mut new_oriented_edges = Vec::new();
1072 for oe in &old_edges {
1073 if let Some(intermediates) = edge_splits.get(&oe.edge()) {
1074 let (start_vid, end_vid) = match edge_vertices.get(&oe.edge()) {
1075 Some(&v) => v,
1076 None => continue,
1077 };
1078 let original_curve = topo.edge(oe.edge())?.curve().clone();
1079
1080 let chain: Vec<VertexId> = if oe.is_forward() {
1082 let mut c = vec![start_vid];
1083 c.extend(intermediates.iter().copied());
1084 c.push(end_vid);
1085 c
1086 } else {
1087 let mut c = vec![end_vid];
1088 c.extend(intermediates.iter().rev().copied());
1089 c.push(start_vid);
1090 c
1091 };
1092
1093 for k in 0..chain.len() - 1 {
1097 let va = chain[k];
1098 let vb = chain[k + 1];
1099 let va_idx = va.index();
1100 let vb_idx = vb.index();
1101 let (key_min, key_max) = if va_idx <= vb_idx {
1102 (va_idx, vb_idx)
1103 } else {
1104 (vb_idx, va_idx)
1105 };
1106 let sub_eid = *edge_map.entry((key_min, key_max)).or_insert_with(|| {
1107 let (s, e) = if oe.is_forward() { (va, vb) } else { (vb, va) };
1113 topo.add_edge(Edge::new(s, e, original_curve.clone()))
1114 });
1115 let fwd = topo.edge(sub_eid)?.start() == va;
1116 if !new_oriented_edges
1119 .iter()
1120 .any(|e: &OrientedEdge| e.edge() == sub_eid)
1121 {
1122 new_oriented_edges.push(OrientedEdge::new(sub_eid, fwd));
1123 }
1124 }
1125 } else {
1126 if !new_oriented_edges
1128 .iter()
1129 .any(|e: &OrientedEdge| e.edge() == oe.edge())
1130 {
1131 new_oriented_edges.push(*oe);
1132 }
1133 }
1134 }
1135
1136 let new_wire =
1137 Wire::new(new_oriented_edges, true).map_err(crate::OperationsError::Topology)?;
1138 let new_wire_id = topo.add_wire(new_wire);
1139
1140 let new_face = if is_reversed {
1141 Face::new_reversed(new_wire_id, inner_wires, surface)
1142 } else {
1143 Face::new(new_wire_id, inner_wires, surface)
1144 };
1145 face_ids[fi] = topo.add_face(new_face);
1146 }
1147
1148 Ok(())
1149}
1150
1151#[allow(clippy::too_many_lines)]
1162pub(super) fn stitch_boundary_edges(
1163 topo: &mut Topology,
1164 face_ids: &mut [FaceId],
1165 tol: Tolerance,
1166) -> Result<usize, crate::OperationsError> {
1167 struct BoundaryEdgeInfo {
1168 edge_id: EdgeId,
1169 start_vid: VertexId,
1170 end_vid: VertexId,
1171 start_pos: Point3,
1172 end_pos: Point3,
1173 midpoint: Point3,
1174 face_idx: usize,
1175 }
1176
1177 let mut edge_face_count: HashMap<EdgeId, usize> = HashMap::new();
1178 let mut edge_vertices: HashMap<EdgeId, (VertexId, VertexId)> = HashMap::new();
1179 let mut edge_owner: HashMap<EdgeId, (usize, WireId)> = HashMap::new();
1181
1182 for (fi, &fid) in face_ids.iter().enumerate() {
1183 let face = topo.face(fid)?;
1184 let outer_wire_id = face.outer_wire();
1185 for wid in std::iter::once(outer_wire_id).chain(face.inner_wires().iter().copied()) {
1187 let wire = topo.wire(wid)?;
1188 for oe in wire.edges() {
1189 let eid = oe.edge();
1190 *edge_face_count.entry(eid).or_default() += 1;
1191 if let std::collections::hash_map::Entry::Vacant(e) = edge_vertices.entry(eid)
1192 && let Ok(edge) = topo.edge(eid)
1193 {
1194 e.insert((edge.start(), edge.end()));
1195 }
1196 edge_owner.entry(eid).or_insert((fi, outer_wire_id));
1197 }
1198 }
1199 }
1200
1201 let mut boundary_edges: Vec<BoundaryEdgeInfo> = Vec::new();
1202 for (&eid, &count) in &edge_face_count {
1203 if count != 1 {
1204 continue;
1205 }
1206 let &(sv, ev) = match edge_vertices.get(&eid) {
1207 Some(v) => v,
1208 None => continue,
1209 };
1210 let sp = topo.vertex(sv)?.point();
1211 let ep = topo.vertex(ev)?.point();
1212 let mid = Point3::new(
1213 (sp.x() + ep.x()) * 0.5,
1214 (sp.y() + ep.y()) * 0.5,
1215 (sp.z() + ep.z()) * 0.5,
1216 );
1217 let &(fi, _wid) = match edge_owner.get(&eid) {
1218 Some(v) => v,
1219 None => continue,
1220 };
1221 boundary_edges.push(BoundaryEdgeInfo {
1222 edge_id: eid,
1223 start_vid: sv,
1224 end_vid: ev,
1225 start_pos: sp,
1226 end_pos: ep,
1227 midpoint: mid,
1228 face_idx: fi,
1229 });
1230 }
1231
1232 if boundary_edges.len() < 2 {
1233 return Ok(0);
1234 }
1235
1236 let tol_linear = tol.linear;
1237 let cell_size = boundary_edges
1238 .iter()
1239 .map(|be| {
1240 let dx = be.end_pos.x() - be.start_pos.x();
1241 let dy = be.end_pos.y() - be.start_pos.y();
1242 let dz = be.end_pos.z() - be.start_pos.z();
1243 (dx * dx + dy * dy + dz * dz).sqrt() * 0.5
1244 })
1245 .fold(f64::INFINITY, f64::min)
1246 .max(tol_linear * 10.0);
1247 let inv_cell = 1.0 / cell_size;
1248
1249 let mut grid: HashMap<(i64, i64, i64), Vec<usize>> = HashMap::new();
1250 for (i, be) in boundary_edges.iter().enumerate() {
1251 let cx = (be.midpoint.x() * inv_cell).floor() as i64;
1252 let cy = (be.midpoint.y() * inv_cell).floor() as i64;
1253 let cz = (be.midpoint.z() * inv_cell).floor() as i64;
1254 grid.entry((cx, cy, cz)).or_default().push(i);
1255 }
1256
1257 let mut stitched: HashSet<EdgeId> = HashSet::new();
1258 let mut replacements: HashMap<(usize, EdgeId), EdgeId> = HashMap::new();
1260 let mut vertex_remap: HashMap<VertexId, VertexId> = HashMap::new();
1262 let mut stitch_count = 0;
1263
1264 let tol_sq = tol_linear * tol_linear;
1265
1266 for i in 0..boundary_edges.len() {
1267 let be1 = &boundary_edges[i];
1268 if stitched.contains(&be1.edge_id) {
1269 continue;
1270 }
1271
1272 let mid = be1.midpoint;
1273 let cx = (mid.x() * inv_cell).floor() as i64;
1274 let cy = (mid.y() * inv_cell).floor() as i64;
1275 let cz = (mid.z() * inv_cell).floor() as i64;
1276
1277 let mut best_match: Option<usize> = None;
1278 let mut best_dist_sq = f64::INFINITY;
1279
1280 for dx in -1..=1 {
1281 for dy in -1..=1 {
1282 for dz in -1..=1 {
1283 if let Some(indices) = grid.get(&(cx + dx, cy + dy, cz + dz)) {
1284 for &j in indices {
1285 if j <= i {
1286 continue;
1287 }
1288 let be2 = &boundary_edges[j];
1289 if stitched.contains(&be2.edge_id) {
1290 continue;
1291 }
1292 if be1.face_idx == be2.face_idx {
1294 continue;
1295 }
1296
1297 let same_dir = (be1.start_pos - be2.start_pos).length_squared()
1299 < tol_sq
1300 && (be1.end_pos - be2.end_pos).length_squared() < tol_sq;
1301 let rev_dir = (be1.start_pos - be2.end_pos).length_squared() < tol_sq
1302 && (be1.end_pos - be2.start_pos).length_squared() < tol_sq;
1303
1304 if !same_dir && !rev_dir {
1305 continue;
1306 }
1307
1308 let mid_dist_sq = (be1.midpoint - be2.midpoint).length_squared();
1309 if mid_dist_sq < best_dist_sq {
1310 best_dist_sq = mid_dist_sq;
1311 best_match = Some(j);
1312 }
1313 }
1314 }
1315 }
1316 }
1317 }
1318
1319 if let Some(j) = best_match {
1320 let be2 = &boundary_edges[j];
1321
1322 let same_dir = (be1.start_pos - be2.start_pos).length_squared() < tol_sq;
1325
1326 if same_dir {
1327 if be2.start_vid != be1.start_vid {
1329 vertex_remap.insert(be2.start_vid, be1.start_vid);
1330 }
1331 if be2.end_vid != be1.end_vid {
1332 vertex_remap.insert(be2.end_vid, be1.end_vid);
1333 }
1334 } else {
1335 if be2.start_vid != be1.end_vid {
1337 vertex_remap.insert(be2.start_vid, be1.end_vid);
1338 }
1339 if be2.end_vid != be1.start_vid {
1340 vertex_remap.insert(be2.end_vid, be1.start_vid);
1341 }
1342 }
1343
1344 replacements.insert((be2.face_idx, be2.edge_id), be1.edge_id);
1346
1347 stitched.insert(be1.edge_id);
1348 stitched.insert(be2.edge_id);
1349 stitch_count += 1;
1350 }
1351 }
1352
1353 if stitch_count == 0 {
1354 return Ok(0);
1355 }
1356
1357 log::debug!(
1358 "[boolean] stitch_boundary_edges: {} pairs, {} vertex remaps",
1359 stitch_count,
1360 vertex_remap.len()
1361 );
1362
1363 let mut resolved_remap: HashMap<VertexId, VertexId> = HashMap::new();
1365 for (&from, &to) in &vertex_remap {
1366 let mut target = to;
1367 let mut depth = 0;
1368 while let Some(&next) = vertex_remap.get(&target) {
1369 if next == target || depth > 10 {
1370 break;
1371 }
1372 target = next;
1373 depth += 1;
1374 }
1375 resolved_remap.insert(from, target);
1376 }
1377
1378 let affected_face_indices: HashSet<usize> = replacements.keys().map(|(fi, _)| *fi).collect();
1381
1382 for &fi in &affected_face_indices {
1383 let fid = face_ids[fi];
1384 let face = topo.face(fid)?;
1385 let outer_wire_id = face.outer_wire();
1386 let wire = topo.wire(outer_wire_id)?;
1387 let surface = face.surface().clone();
1388 let is_reversed = face.is_reversed();
1389 let inner_wires: Vec<WireId> = face.inner_wires().to_vec();
1390 let old_edges: Vec<OrientedEdge> = wire.edges().to_vec();
1391
1392 let mut new_oriented_edges: Vec<OrientedEdge> = Vec::with_capacity(old_edges.len());
1393
1394 for oe in &old_edges {
1395 if let Some(&replacement_eid) = replacements.get(&(fi, oe.edge())) {
1396 let keeper = topo.edge(replacement_eid)?;
1401 let keeper_start = keeper.start();
1402 let keeper_end = keeper.end();
1403
1404 let old_edge = topo.edge(oe.edge())?;
1407 let expected_start = if oe.is_forward() {
1408 old_edge.start()
1409 } else {
1410 old_edge.end()
1411 };
1412 let resolved_expected = resolved_remap
1413 .get(&expected_start)
1414 .copied()
1415 .unwrap_or(expected_start);
1416
1417 let is_forward =
1420 keeper_start == resolved_expected || keeper_end != resolved_expected;
1421 new_oriented_edges.push(OrientedEdge::new(replacement_eid, is_forward));
1422 } else {
1423 let edge = topo.edge(oe.edge())?;
1425 let old_start = edge.start();
1426 let old_end = edge.end();
1427 let new_start = resolved_remap.get(&old_start).copied();
1428 let new_end = resolved_remap.get(&old_end).copied();
1429
1430 if new_start.is_some() || new_end.is_some() {
1431 let curve = edge.curve().clone();
1432 let s = new_start.unwrap_or(old_start);
1433 let e = new_end.unwrap_or(old_end);
1434 let new_eid = topo.add_edge(Edge::new(s, e, curve));
1435 new_oriented_edges.push(OrientedEdge::new(new_eid, oe.is_forward()));
1436 } else {
1437 new_oriented_edges.push(*oe);
1438 }
1439 }
1440 }
1441
1442 let new_wire =
1443 Wire::new(new_oriented_edges, true).map_err(crate::OperationsError::Topology)?;
1444 let new_wire_id = topo.add_wire(new_wire);
1445 let new_face = if is_reversed {
1446 Face::new_reversed(new_wire_id, inner_wires, surface)
1447 } else {
1448 Face::new(new_wire_id, inner_wires, surface)
1449 };
1450 face_ids[fi] = topo.add_face(new_face);
1451 }
1452
1453 Ok(stitch_count)
1454}
1455
1456#[allow(clippy::too_many_lines)]
1468pub(super) fn split_nonmanifold_edges(
1469 topo: &mut Topology,
1470 face_ids: &mut [FaceId],
1471) -> Result<(), crate::OperationsError> {
1472 let mut edge_faces: HashMap<usize, Vec<(usize, bool)>> = HashMap::new();
1474 for (fi, &fid) in face_ids.iter().enumerate() {
1475 let face = topo.face(fid)?;
1476 for wid in std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied()) {
1477 let wire = topo.wire(wid)?;
1478 for oe in wire.edges() {
1479 edge_faces
1480 .entry(oe.edge().index())
1481 .or_default()
1482 .push((fi, oe.is_forward()));
1483 }
1484 }
1485 }
1486
1487 let mut nonmanifold: Vec<(usize, Vec<(usize, bool)>)> = edge_faces
1493 .into_iter()
1494 .filter(|(_, faces)| faces.len() > 2)
1495 .collect();
1496 nonmanifold.sort_by_key(|(eid, _)| *eid);
1497
1498 if nonmanifold.is_empty() {
1499 return Ok(());
1500 }
1501
1502 let mut edge_replacements: HashMap<(usize, usize), EdgeId> = HashMap::new();
1505
1506 for (edge_idx, face_refs) in &nonmanifold {
1507 let edge_id = topo.edge_id_from_index(*edge_idx).ok_or_else(|| {
1508 crate::OperationsError::InvalidInput {
1509 reason: format!("edge index {edge_idx} not found"),
1510 }
1511 })?;
1512 let edge_start = topo.edge(edge_id)?.start();
1514 let edge_end = topo.edge(edge_id)?.end();
1515 let edge_curve = topo.edge(edge_id)?.curve().clone();
1516 let start_pos = topo.vertex(edge_start)?.point();
1517 let end_pos = topo.vertex(edge_end)?.point();
1518
1519 let edge_dir = Vec3::new(
1520 end_pos.x() - start_pos.x(),
1521 end_pos.y() - start_pos.y(),
1522 end_pos.z() - start_pos.z(),
1523 );
1524 let edge_len = edge_dir.length();
1525 if edge_len < 1e-15 {
1528 continue;
1529 }
1530 let edge_axis = Vec3::new(
1531 edge_dir.x() / edge_len,
1532 edge_dir.y() / edge_len,
1533 edge_dir.z() / edge_len,
1534 );
1535
1536 let perp = if edge_axis.x().abs() < 0.9 {
1538 Vec3::new(1.0, 0.0, 0.0)
1539 } else {
1540 Vec3::new(0.0, 1.0, 0.0)
1541 };
1542 let u_axis = edge_axis.cross(perp);
1543 let u_len = u_axis.length();
1544 if u_len < 1e-15 {
1547 continue;
1548 }
1549 let u_axis = Vec3::new(u_axis.x() / u_len, u_axis.y() / u_len, u_axis.z() / u_len);
1550 let v_axis = edge_axis.cross(u_axis);
1551
1552 let mut face_angles: Vec<(usize, bool, f64)> = Vec::new();
1554 for &(fi, is_fwd) in face_refs {
1555 let face = topo.face(face_ids[fi])?;
1556 let normal = if let FaceSurface::Plane { normal, .. } = face.surface() {
1557 *normal
1558 } else {
1559 let mid = Point3::new(
1566 (start_pos.x() + end_pos.x()) * 0.5,
1567 (start_pos.y() + end_pos.y()) * 0.5,
1568 (start_pos.z() + end_pos.z()) * 0.5,
1569 );
1570 if let Some((u, v)) = face.surface().project_point(mid) {
1571 face.surface().normal(u, v)
1572 } else {
1573 let wire = topo.wire(face.outer_wire())?;
1575 let mut sum = Vec3::new(0.0, 0.0, 0.0);
1576 let mut count = 0usize;
1577 for oe in wire.edges() {
1578 if let Ok(e) = topo.edge(oe.edge())
1579 && let Ok(vx) = topo.vertex(e.start())
1580 {
1581 let p = vx.point();
1582 sum = Vec3::new(sum.x() + p.x(), sum.y() + p.y(), sum.z() + p.z());
1583 count += 1;
1584 }
1585 }
1586 if count == 0 {
1587 continue;
1588 }
1589 #[allow(clippy::cast_precision_loss)]
1590 let inv = 1.0 / count as f64;
1591 let centroid = Vec3::new(sum.x() * inv, sum.y() * inv, sum.z() * inv);
1592 Vec3::new(
1593 centroid.x() - mid.x(),
1594 centroid.y() - mid.y(),
1595 centroid.z() - mid.z(),
1596 )
1597 }
1598 };
1599
1600 let effective_normal = if face.is_reversed() { -normal } else { normal };
1602
1603 let proj_u = effective_normal.dot(u_axis);
1605 let proj_v = effective_normal.dot(v_axis);
1606 let angle = proj_v.atan2(proj_u);
1607 face_angles.push((fi, is_fwd, angle));
1608 }
1609
1610 face_angles.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
1611
1612 let n = face_angles.len();
1614 for pair_idx in 0..(n / 2) {
1615 let i = pair_idx * 2;
1616 let j = i + 1;
1617 if j >= n {
1618 break;
1619 }
1620 let new_edge_id = if pair_idx == 0 {
1621 edge_id
1622 } else {
1623 topo.add_edge(Edge::new(edge_start, edge_end, edge_curve.clone()))
1624 };
1625 edge_replacements.insert((face_angles[i].0, *edge_idx), new_edge_id);
1626 edge_replacements.insert((face_angles[j].0, *edge_idx), new_edge_id);
1627 }
1628 if n % 2 == 1 {
1631 let last = &face_angles[n - 1];
1632 edge_replacements.insert((last.0, *edge_idx), edge_id);
1633 }
1634 }
1635
1636 if edge_replacements.is_empty() {
1637 return Ok(());
1638 }
1639
1640 let affected_faces: HashSet<usize> = edge_replacements.keys().map(|(fi, _)| *fi).collect();
1641 for fi in affected_faces {
1642 let fid = face_ids[fi];
1643 let face = topo.face(fid)?;
1644 let wire = topo.wire(face.outer_wire())?;
1645 let surface = face.surface().clone();
1646 let is_reversed = face.is_reversed();
1647 let inner_wires: Vec<WireId> = face.inner_wires().to_vec();
1648
1649 let new_edges: Vec<OrientedEdge> = wire
1650 .edges()
1651 .iter()
1652 .map(|oe| {
1653 if let Some(&new_eid) = edge_replacements.get(&(fi, oe.edge().index())) {
1654 OrientedEdge::new(new_eid, oe.is_forward())
1655 } else {
1656 *oe
1657 }
1658 })
1659 .collect();
1660
1661 let new_wire = Wire::new(new_edges, true).map_err(crate::OperationsError::Topology)?;
1662 let new_wire_id = topo.add_wire(new_wire);
1663 let new_face = if is_reversed {
1664 Face::new_reversed(new_wire_id, inner_wires, surface)
1665 } else {
1666 Face::new(new_wire_id, inner_wires, surface)
1667 };
1668 face_ids[fi] = topo.add_face(new_face);
1669 }
1670
1671 Ok(())
1672}
1673
1674#[allow(dead_code)]
1676fn analytic_face_normal_d(surface: &FaceSurface, verts: &[Point3]) -> (Vec3, f64) {
1677 match surface {
1678 FaceSurface::Plane { normal, d } => (*normal, *d),
1679 _ => {
1680 if verts.len() >= 3 {
1681 let e1 = verts[1] - verts[0];
1682 let e2 = verts[2] - verts[0];
1683 let n = e1.cross(e2).normalize().unwrap_or(Vec3::new(0.0, 0.0, 1.0));
1684 (n, crate::dot_normal_point(n, verts[0]))
1685 } else {
1686 (Vec3::new(0.0, 0.0, 1.0), 0.0)
1687 }
1688 }
1689 }
1690}
1691
1692#[allow(dead_code, clippy::too_many_lines)]
1697pub(super) fn try_shared_boundary_fuse(
1698 topo: &mut Topology,
1699 _a: SolidId,
1700 _b: SolidId,
1701 face_ids_a: &[FaceId],
1702 face_ids_b: &[FaceId],
1703 tol: Tolerance,
1704) -> Result<Option<SolidId>, crate::OperationsError> {
1705 struct PlaneInfo {
1706 normal: Vec3,
1707 d: f64,
1708 vertices: Vec<Point3>,
1709 }
1710
1711 const SHARED_FACE_AREA_RATIO_MIN: f64 = 0.99;
1713
1714 if face_ids_a.len() > 20 || face_ids_b.len() > 20 {
1716 return Ok(None);
1717 }
1718
1719 for &fid in face_ids_a.iter().chain(face_ids_b.iter()) {
1721 if !matches!(topo.face(fid)?.surface(), FaceSurface::Plane { .. }) {
1722 return Ok(None);
1723 }
1724 }
1725
1726 let snapshot = |fid: FaceId| -> Result<PlaneInfo, crate::OperationsError> {
1728 let face = topo.face(fid)?;
1729 let surface = face.surface().clone();
1730 let reversed = face.is_reversed();
1731 let verts = face_polygon(topo, fid)?;
1732 let (mut normal, mut d) = analytic_face_normal_d(&surface, &verts);
1733 if reversed {
1734 normal = -normal;
1735 d = -d;
1736 }
1737 Ok(PlaneInfo {
1738 normal,
1739 d,
1740 vertices: verts,
1741 })
1742 };
1743
1744 let infos_a: Vec<PlaneInfo> = face_ids_a
1745 .iter()
1746 .map(|&fid| snapshot(fid))
1747 .collect::<Result<Vec<_>, _>>()?;
1748 let infos_b: Vec<PlaneInfo> = face_ids_b
1749 .iter()
1750 .map(|&fid| snapshot(fid))
1751 .collect::<Result<Vec<_>, _>>()?;
1752
1753 let mut shared_a = None;
1755 let mut shared_b = None;
1756 let mut shared_count = 0;
1757
1758 for (ia, pa) in infos_a.iter().enumerate() {
1759 for (ib, pb) in infos_b.iter().enumerate() {
1760 let dot = pa.normal.dot(pb.normal);
1762 if dot > -1.0 + tol.angular {
1763 continue;
1764 }
1765 if !tol.approx_eq(pa.d, -pb.d) {
1766 continue;
1767 }
1768
1769 let area_a = polygon_area_3d(&pa.vertices, pa.normal);
1772 let area_b = polygon_area_3d(&pb.vertices, pb.normal);
1773 let area_ratio = if area_a > area_b {
1774 area_b / area_a
1775 } else {
1776 area_a / area_b
1777 };
1778 if area_ratio < SHARED_FACE_AREA_RATIO_MIN {
1779 continue;
1780 }
1781
1782 let centroid_a = polygon_centroid(&pa.vertices);
1785 let centroid_b = polygon_centroid(&pb.vertices);
1786 let dist = (centroid_a - centroid_b).length();
1787 let face_extent = area_a.sqrt().max(tol.linear);
1788 if dist > face_extent * 1e-6 {
1793 continue;
1794 }
1795
1796 shared_a = Some(ia);
1797 shared_b = Some(ib);
1798 shared_count += 1;
1799
1800 if shared_count > 1 {
1801 return Ok(None);
1803 }
1804 }
1805 }
1806
1807 let (skip_a, skip_b) = match (shared_a, shared_b) {
1808 (Some(a), Some(b)) => (a, b),
1809 _ => return Ok(None),
1810 };
1811
1812 let mut face_specs: Vec<FaceSpec> = Vec::with_capacity(face_ids_a.len() + face_ids_b.len() - 2);
1814
1815 for (i, info) in infos_a.iter().enumerate() {
1816 if i == skip_a {
1817 continue;
1818 }
1819 face_specs.push(FaceSpec::Planar {
1820 vertices: info.vertices.clone(),
1821 normal: info.normal,
1822 d: info.d,
1823 inner_wires: vec![],
1824 });
1825 }
1826 for (i, info) in infos_b.iter().enumerate() {
1827 if i == skip_b {
1828 continue;
1829 }
1830 face_specs.push(FaceSpec::Planar {
1831 vertices: info.vertices.clone(),
1832 normal: info.normal,
1833 d: info.d,
1834 inner_wires: vec![],
1835 });
1836 }
1837
1838 let result = assemble_solid_mixed(topo, &face_specs, tol)?;
1839 Ok(Some(result))
1840}
1841
1842#[allow(dead_code)]
1844pub(super) fn polygon_area_3d(vertices: &[Point3], normal: Vec3) -> f64 {
1845 if vertices.len() < 3 {
1846 return 0.0;
1847 }
1848 let mut area = Vec3::new(0.0, 0.0, 0.0);
1849 let v0 = vertices[0];
1850 for i in 1..vertices.len() - 1 {
1851 let e1 = vertices[i] - v0;
1852 let e2 = vertices[i + 1] - v0;
1853 area += e1.cross(e2);
1854 }
1855 (area.dot(normal) * 0.5).abs()
1856}
1857
1858#[allow(clippy::too_many_lines, dead_code)]
1873pub(super) fn build_manifold_shells(
1874 topo: &mut Topology,
1875 face_ids: &[FaceId],
1876) -> Result<SolidId, crate::OperationsError> {
1877 if face_ids.is_empty() {
1878 return Err(crate::OperationsError::InvalidInput {
1879 reason: "build_manifold_shells: no faces".into(),
1880 });
1881 }
1882
1883 let mut edge_faces: HashMap<usize, Vec<(usize, bool)>> = HashMap::new();
1886 for (fi, &fid) in face_ids.iter().enumerate() {
1887 let face = topo.face(fid)?;
1888 let wire = topo.wire(face.outer_wire())?;
1889 for oe in wire.edges() {
1890 edge_faces
1891 .entry(oe.edge().index())
1892 .or_default()
1893 .push((fi, oe.is_forward()));
1894 }
1895 }
1896
1897 let nm_count = edge_faces.values().filter(|fs| fs.len() > 2).count();
1903 if nm_count <= 30 {
1908 let shell = Shell::new(face_ids.to_vec()).map_err(crate::OperationsError::Topology)?;
1910 let shell_id = topo.add_shell(shell);
1911 return Ok(topo.add_solid(Solid::new(shell_id, vec![])));
1912 }
1913
1914 let mut added: HashSet<usize> = HashSet::new();
1915 let mut shells: Vec<Vec<FaceId>> = Vec::new();
1916
1917 for seed_fi in 0..face_ids.len() {
1918 if added.contains(&seed_fi) {
1919 continue;
1920 }
1921 added.insert(seed_fi);
1922
1923 let mut shell_faces: Vec<usize> = vec![seed_fi];
1924 let mut shell_edge_count: HashMap<usize, u32> = HashMap::new();
1926
1927 count_face_edges(topo, face_ids[seed_fi], &mut shell_edge_count)?;
1928
1929 let mut queue_idx = 0;
1930 while queue_idx < shell_faces.len() {
1931 let current_fi = shell_faces[queue_idx];
1932 queue_idx += 1;
1933
1934 let face = topo.face(face_ids[current_fi])?;
1935 let mut face_edge_list: Vec<(usize, bool)> = Vec::new();
1936 let wire = topo.wire(face.outer_wire())?;
1938 for oe in wire.edges() {
1939 face_edge_list.push((oe.edge().index(), oe.is_forward()));
1940 }
1941
1942 for (edge_idx, edge_fwd) in face_edge_list {
1943 if shell_edge_count.get(&edge_idx).copied().unwrap_or(0) >= 2 {
1945 continue;
1946 }
1947
1948 let Some(neighbors) = edge_faces.get(&edge_idx) else {
1950 continue;
1951 };
1952
1953 let candidates: Vec<(usize, bool)> = neighbors
1957 .iter()
1958 .filter(|(fi, fwd)| {
1959 *fi != current_fi && !added.contains(fi) && *fwd != edge_fwd
1960 })
1961 .copied()
1962 .collect();
1963
1964 if candidates.is_empty() {
1965 continue;
1966 }
1967
1968 let selected_fi = if candidates.len() == 1 {
1970 candidates[0].0
1971 } else {
1972 select_angular_neighbor(
1974 topo,
1975 face_ids,
1976 edge_idx,
1977 current_fi,
1978 edge_fwd,
1979 &candidates,
1980 )?
1981 .unwrap_or(candidates[0].0)
1982 };
1983
1984 if added.insert(selected_fi) {
1985 shell_faces.push(selected_fi);
1986 count_face_edges(topo, face_ids[selected_fi], &mut shell_edge_count)?;
1987 }
1988 }
1989 }
1990
1991 shells.push(shell_faces.into_iter().map(|fi| face_ids[fi]).collect());
1992 }
1993
1994 if shells.is_empty() {
1996 return Err(crate::OperationsError::InvalidInput {
1997 reason: "build_manifold_shells: no shells produced".into(),
1998 });
1999 }
2000
2001 shells.sort_by_key(|s| std::cmp::Reverse(s.len()));
2003
2004 let outer_shell = Shell::new(shells[0].clone()).map_err(crate::OperationsError::Topology)?;
2005 let outer_id = topo.add_shell(outer_shell);
2006
2007 let mut inner_ids = Vec::new();
2008 for inner_faces in &shells[1..] {
2009 if !inner_faces.is_empty()
2010 && let Ok(inner_shell) = Shell::new(inner_faces.clone())
2011 {
2012 inner_ids.push(topo.add_shell(inner_shell));
2013 }
2014 }
2015
2016 Ok(topo.add_solid(Solid::new(outer_id, inner_ids)))
2017}
2018
2019fn count_face_edges(
2021 topo: &Topology,
2022 fid: FaceId,
2023 edge_count: &mut HashMap<usize, u32>,
2024) -> Result<(), crate::OperationsError> {
2025 let face = topo.face(fid)?;
2026 let wire = topo.wire(face.outer_wire())?;
2027 for oe in wire.edges() {
2028 *edge_count.entry(oe.edge().index()).or_default() += 1;
2029 }
2030 Ok(())
2031}
2032
2033fn select_angular_neighbor(
2040 topo: &Topology,
2041 face_ids: &[FaceId],
2042 edge_idx: usize,
2043 current_fi: usize,
2044 current_fwd: bool,
2045 candidates: &[(usize, bool)],
2046) -> Result<Option<usize>, crate::OperationsError> {
2047 let edge_id =
2048 topo.edge_id_from_index(edge_idx)
2049 .ok_or_else(|| crate::OperationsError::InvalidInput {
2050 reason: format!("edge index {edge_idx} not found"),
2051 })?;
2052
2053 let edge = topo.edge(edge_id)?;
2054 let start_pos = topo.vertex(edge.start())?.point();
2055 let end_pos = topo.vertex(edge.end())?.point();
2056 let edge_dir = end_pos - start_pos;
2057 let edge_len = edge_dir.length();
2058 if edge_len < 1e-12 {
2059 return Ok(None);
2060 }
2061 let tangent = edge_dir * (1.0 / edge_len);
2062 let tangent = if current_fwd { tangent } else { -tangent };
2064 let mid = Point3::new(
2065 (start_pos.x() + end_pos.x()) * 0.5,
2066 (start_pos.y() + end_pos.y()) * 0.5,
2067 (start_pos.z() + end_pos.z()) * 0.5,
2068 );
2069
2070 let current_face = topo.face(face_ids[current_fi])?;
2072 let normal1 = face_normal_at_point(current_face, mid);
2073 let binormal1 = pcurve_binormal(
2074 topo,
2075 edge_id,
2076 face_ids[current_fi],
2077 current_face,
2078 mid,
2079 tangent,
2080 normal1,
2081 current_fwd,
2082 );
2083 let ref_dir = normal1.cross(binormal1);
2084
2085 let mut best_angle = f64::MAX;
2087 let mut best_fi = None;
2088
2089 for &(cand_fi, cand_fwd) in candidates {
2090 let cand_face = topo.face(face_ids[cand_fi])?;
2091 let tangent2 = if cand_fwd == current_fwd {
2092 tangent
2093 } else {
2094 -tangent
2095 };
2096 let normal2 = face_normal_at_point(cand_face, mid);
2097 let binormal2 = pcurve_binormal(
2098 topo,
2099 edge_id,
2100 face_ids[cand_fi],
2101 cand_face,
2102 mid,
2103 tangent2,
2104 normal2,
2105 cand_fwd,
2106 );
2107
2108 let cross = binormal1.cross(binormal2);
2110 let cos_val = binormal1.dot(binormal2);
2111 let sin_sign = cross.dot(ref_dir);
2112
2113 let beta = std::f64::consts::FRAC_PI_2 * (1.0 - cos_val);
2115 let mut angle = if sin_sign < 0.0 { -beta } else { beta };
2116 if angle < 1e-10 {
2117 angle += std::f64::consts::TAU;
2118 }
2119
2120 if angle < best_angle {
2121 best_angle = angle;
2122 best_fi = Some(cand_fi);
2123 }
2124 }
2125
2126 Ok(best_fi)
2127}
2128
2129fn face_normal_at_point(face: &Face, point: Point3) -> Vec3 {
2134 let raw_normal = match face.surface() {
2135 FaceSurface::Plane { normal, .. } => *normal,
2136 surface => {
2137 if let Some((u, v)) = surface.project_point(point) {
2138 surface.normal(u, v)
2139 } else {
2140 Vec3::new(0.0, 0.0, 1.0) }
2142 }
2143 };
2144 if face.is_reversed() {
2145 -raw_normal
2146 } else {
2147 raw_normal
2148 }
2149}
2150
2151#[allow(dead_code)]
2160pub(super) fn register_pcurves(
2161 topo: &mut Topology,
2162 face_ids: &[FaceId],
2163) -> Result<(), crate::OperationsError> {
2164 use brepkit_algo::compute_pcurve_on_surface;
2165 use brepkit_topology::pcurve::PCurve;
2166
2167 for &fid in face_ids {
2168 let face = topo.face(fid)?;
2169 let surface = face.surface().clone();
2170
2171 let wire_pts: Vec<Point3> = {
2173 let wire = topo.wire(face.outer_wire())?;
2174 wire.edges()
2175 .iter()
2176 .filter_map(|oe| {
2177 topo.edge(oe.edge()).ok().and_then(|e| {
2178 topo.vertex(e.start())
2179 .ok()
2180 .map(brepkit_topology::vertex::Vertex::point)
2181 })
2182 })
2183 .collect()
2184 };
2185
2186 let wire_ids: Vec<_> = {
2187 let f = topo.face(fid)?;
2188 std::iter::once(f.outer_wire())
2189 .chain(f.inner_wires().iter().copied())
2190 .collect()
2191 };
2192
2193 for wid in wire_ids {
2194 let wire = topo.wire(wid)?;
2195 let edges: Vec<_> = wire.edges().to_vec();
2196 for oe in &edges {
2197 let eid = oe.edge();
2198 if topo.pcurves().contains(eid, fid) {
2200 continue;
2201 }
2202
2203 let edge = topo.edge(eid)?;
2204 let start = topo.vertex(edge.start())?.point();
2205 let end = topo.vertex(edge.end())?.point();
2206 let curve_3d = edge.curve();
2207
2208 let pcurve_2d =
2209 compute_pcurve_on_surface(curve_3d, start, end, &surface, &wire_pts, None);
2210
2211 let pc = PCurve::new(pcurve_2d, 0.0, 1.0);
2213 topo.pcurves_mut().set(eid, fid, pc);
2214 }
2215 }
2216 }
2217 Ok(())
2218}
2219
2220#[allow(clippy::too_many_arguments)]
2229fn pcurve_binormal(
2230 topo: &Topology,
2231 edge_id: EdgeId,
2232 face_id: FaceId,
2233 face: &Face,
2234 edge_point: Point3,
2235 tangent_3d: Vec3,
2236 normal: Vec3,
2237 is_edge_forward: bool,
2238) -> Vec3 {
2239 let initial = normal.cross(tangent_3d);
2240 let initial_len = initial.length();
2241 if initial_len < 1e-12 {
2242 return initial;
2243 }
2244 let initial_dir = initial * (1.0 / initial_len);
2245
2246 if matches!(face.surface(), FaceSurface::Plane { .. }) {
2248 return initial_dir;
2249 }
2250
2251 let Some(pcurve) = topo.pcurves().get(edge_id, face_id) else {
2253 return initial_dir;
2254 };
2255
2256 let t_mid = 0.5 * (pcurve.t_start() + pcurve.t_end());
2258 let uv_mid = pcurve.evaluate(t_mid);
2259
2260 let dt = 1e-5;
2262 let t_near = t_mid + dt;
2263 let uv_near = pcurve.evaluate(t_near);
2264 let du = uv_near.x() - uv_mid.x();
2265 let dv = uv_near.y() - uv_mid.y();
2266 let uv_len = (du * du + dv * dv).sqrt();
2267 if uv_len < 1e-15 {
2268 return initial_dir;
2269 }
2270
2271 let mut inward_u = -dv / uv_len;
2274 let mut inward_v = du / uv_len;
2275 if !is_edge_forward {
2276 inward_u = -inward_u;
2277 inward_v = -inward_v;
2278 }
2279 if face.is_reversed() {
2280 inward_u = -inward_u;
2281 inward_v = -inward_v;
2282 }
2283
2284 let uv_step = 1e-4;
2286 let u_inside = uv_mid.x() + inward_u * uv_step;
2287 let v_inside = uv_mid.y() + inward_v * uv_step;
2288
2289 let Some(pt_inside) = face.surface().evaluate(u_inside, v_inside) else {
2291 return initial_dir;
2292 };
2293
2294 let dir = pt_inside - edge_point;
2297 let along = dir.dot(tangent_3d);
2298 let perp = dir - tangent_3d * along;
2299 let perp_len = perp.length();
2300 if perp_len < 1e-15 {
2301 return initial_dir;
2302 }
2303 perp * (1.0 / perp_len)
2304}