1use std::collections::{HashMap, HashSet};
8
9use brepkit_math::tolerance::Tolerance;
10use brepkit_math::vec::{Point3, Vec3};
11use brepkit_topology::Topology;
12use brepkit_topology::face::{FaceId, FaceSurface};
13use brepkit_topology::solid::SolidId;
14
15use crate::boolean::{FaceSpec, assemble_solid_mixed};
16use crate::dot_normal_point;
17
18fn compute_miter_offset(outer: Point3, unique_normals: &[(Vec3, bool)], thickness: f64) -> Point3 {
30 let mut normals: Vec<Vec3> = Vec::new();
33 let mut weights: Vec<f64> = Vec::new();
34
35 for &(n, is_open) in unique_normals {
36 normals.push(n);
37 weights.push(if is_open { 0.0 } else { 1.0 });
38 }
39
40 let miter = match normals.len() {
41 0 => return outer,
42 1 => {
43 normals[0] * weights[0]
45 }
46 2 => {
47 let n1 = normals[0];
50 let n2 = normals[1];
51 let w1 = weights[0];
52 let w2 = weights[1];
53
54 let g11 = n1.dot(n1);
55 let g12 = n1.dot(n2);
56 let g22 = n2.dot(n2);
57 let det = g11 * g22 - g12 * g12;
58
59 if det.abs() < 1e-12 {
60 if w1 > 0.5 { n1 * w1 } else { n2 * w2 }
62 } else {
63 let inv_det = 1.0 / det;
64 let a1 = (g22 * w1 - g12 * w2) * inv_det;
65 let a2 = (-g12 * w1 + g11 * w2) * inv_det;
66 n1 * a1 + n2 * a2
67 }
68 }
69 _ => {
70 let n1 = normals[0];
73 let n2 = normals[1];
74 let n3 = normals[2];
75 let w1 = weights[0];
76 let w2 = weights[1];
77 let w3 = weights[2];
78
79 let n2_cross_n3 = n2.cross(n3);
80 let det = n1.dot(n2_cross_n3);
81
82 if det.abs() < 1e-12 {
83 let g11 = n1.dot(n1);
85 let g12 = n1.dot(n2);
86 let g22 = n2.dot(n2);
87 let d2 = g11 * g22 - g12 * g12;
88 if d2.abs() < 1e-12 {
89 n1 * w1
90 } else {
91 let inv = 1.0 / d2;
92 let a1 = (g22 * w1 - g12 * w2) * inv;
93 let a2 = (-g12 * w1 + g11 * w2) * inv;
94 n1 * a1 + n2 * a2
95 }
96 } else {
97 let n3_cross_n1 = n3.cross(n1);
98 let n1_cross_n2 = n1.cross(n2);
99 let inv_det = 1.0 / det;
100 let mx =
101 (w1 * n2_cross_n3.x() + w2 * n3_cross_n1.x() + w3 * n1_cross_n2.x()) * inv_det;
102 let my =
103 (w1 * n2_cross_n3.y() + w2 * n3_cross_n1.y() + w3 * n1_cross_n2.y()) * inv_det;
104 let mz =
105 (w1 * n2_cross_n3.z() + w2 * n3_cross_n1.z() + w3 * n1_cross_n2.z()) * inv_det;
106 Vec3::new(mx, my, mz)
107 }
108 }
109 };
110
111 Point3::new(
112 outer.x() - thickness * miter.x(),
113 outer.y() - thickness * miter.y(),
114 outer.z() - thickness * miter.z(),
115 )
116}
117
118#[allow(clippy::too_many_lines)]
133pub fn shell(
134 topo: &mut Topology,
135 solid: SolidId,
136 thickness: f64,
137 open_faces: &[FaceId],
138) -> Result<SolidId, crate::OperationsError> {
139 let tol = Tolerance::new();
140
141 if thickness <= tol.linear {
142 return Err(crate::OperationsError::InvalidInput {
143 reason: format!("shell thickness must be positive, got {thickness}"),
144 });
145 }
146
147 let solid_data = topo.solid(solid)?;
148 let shell_data = topo.shell(solid_data.outer_shell())?;
149 let all_face_ids: Vec<FaceId> = shell_data.faces().to_vec();
150
151 let open_set: HashSet<usize> = open_faces.iter().map(|f| f.index()).collect();
152
153 let solid_face_set: HashSet<usize> = all_face_ids.iter().map(|f| f.index()).collect();
154 for &of in open_faces {
155 if !solid_face_set.contains(&of.index()) {
156 return Err(crate::OperationsError::InvalidInput {
157 reason: format!("face {} is not part of the solid", of.index()),
158 });
159 }
160 }
161
162 let mut face_verts: Vec<(FaceId, Vec<Point3>)> = Vec::new();
164 for &fid in &all_face_ids {
165 let verts = crate::boolean::face_polygon(topo, fid)?;
166 face_verts.push((fid, verts));
167 }
168
169 let mut result_specs: Vec<FaceSpec> = Vec::new();
170
171 let inv_tol = 1.0 / tol.linear;
178 let quantize_pt = |p: Point3| -> (i64, i64, i64) {
179 (
180 (p.x() * inv_tol).round() as i64,
181 (p.y() * inv_tol).round() as i64,
182 (p.z() * inv_tol).round() as i64,
183 )
184 };
185
186 let mut vertex_normals: HashMap<(i64, i64, i64), Vec<(Vec3, bool)>> = HashMap::new();
187
188 for &(fid, ref verts) in &face_verts {
189 let face = topo.face(fid)?;
190 let is_open = open_set.contains(&fid.index());
191
192 let collapsing = match face.surface() {
202 FaceSurface::Cylinder(cyl) => cyl.radius() - thickness <= tol.linear,
203 _ => false,
204 };
205 let extreme_normals = if collapsing {
206 extreme_face_normals(&face_surface_normals(face, verts))
207 } else {
208 None
209 };
210
211 for v in verts {
212 let (u, v_param) = face.surface().project_point(*v).unwrap_or((0.0, 0.0));
213 let mut normal = face.surface().normal(u, v_param);
214 if face.is_reversed() {
217 normal = -normal;
218 }
219 let entry = vertex_normals.entry(quantize_pt(*v)).or_default();
220 if let Some((n_a, n_b)) = extreme_normals {
221 entry.push((n_a, is_open));
222 entry.push((n_b, is_open));
223 } else {
224 entry.push((normal, is_open));
225 }
226 }
227 }
228
229 let mut inner_pos: HashMap<(i64, i64, i64), Point3> = HashMap::new();
239
240 for (&key, normals) in &vertex_normals {
241 let mut unique: Vec<(Vec3, bool)> = Vec::new();
244 for &(n, is_open) in normals {
245 let dominated = unique.iter_mut().any(|(un, existing_open)| {
250 let dot = un.dot(n);
251 if dot > 0.995 {
252 if *existing_open && !is_open {
254 *un = n;
255 *existing_open = false;
256 }
257 true
258 } else {
259 false
260 }
261 });
262 if !dominated {
263 unique.push((n, is_open));
264 }
265 }
266
267 let outer_pt = Point3::new(
269 key.0 as f64 / inv_tol,
270 key.1 as f64 / inv_tol,
271 key.2 as f64 / inv_tol,
272 );
273
274 let inner = compute_miter_offset(outer_pt, &unique, thickness);
277 inner_pos.insert(key, inner);
278 }
279
280 for &(fid, ref verts) in &face_verts {
282 if open_set.contains(&fid.index()) {
283 continue;
284 }
285 let face = topo.face(fid)?;
286 match face.surface() {
287 FaceSurface::Plane { normal, d } => {
288 result_specs.push(FaceSpec::Planar {
289 vertices: verts.clone(),
290 normal: *normal,
291 d: *d,
292 inner_wires: vec![],
293 });
294 }
295 FaceSurface::Cylinder(cyl) => {
296 let wire = topo.wire(face.outer_wire())?;
299 let has_closed_edge = wire
300 .edges()
301 .iter()
302 .any(|oe| topo.edge(oe.edge()).is_ok_and(|e| e.start() == e.end()));
303 if has_closed_edge {
304 result_specs.push(FaceSpec::Surface {
305 vertices: verts.clone(),
306 surface: FaceSurface::Cylinder(cyl.clone()),
307 reversed: false,
308 inner_wires: vec![],
309 });
310 } else {
311 result_specs.push(FaceSpec::CylindricalFace {
312 vertices: verts.clone(),
313 cylinder: cyl.clone(),
314 reversed: false,
315 inner_wires: vec![],
316 });
317 }
318 }
319 other => {
320 result_specs.push(FaceSpec::Surface {
321 vertices: verts.clone(),
322 surface: other.clone(),
323 reversed: false,
324 inner_wires: vec![],
325 });
326 }
327 }
328 }
329
330 for &(fid, ref outer_verts) in &face_verts {
337 if open_set.contains(&fid.index()) {
338 continue;
339 }
340 let face = topo.face(fid)?;
341
342 let inner_verts: Vec<Point3> = outer_verts
344 .iter()
345 .map(|v| inner_pos.get(&quantize_pt(*v)).copied().unwrap_or(*v))
346 .rev()
347 .collect();
348
349 match face.surface() {
350 FaceSurface::Plane { normal, .. } => {
351 let inner_normal = -*normal;
352 let inner_d = dot_normal_point(inner_normal, inner_verts[0]);
353 result_specs.push(FaceSpec::Planar {
354 vertices: inner_verts,
355 normal: inner_normal,
356 d: inner_d,
357 inner_wires: vec![],
358 });
359 }
360 FaceSurface::Cylinder(cyl) => {
361 let new_radius = cyl.radius() - thickness;
362 if new_radius <= tol.linear {
363 let wire = topo.wire(face.outer_wire())?;
374 let mut strip: Vec<Point3> = Vec::new();
375 for oe in wire.edges() {
376 let e = topo.edge(oe.edge())?;
377 let v = topo.vertex(oe.oriented_start(e))?.point();
378 let p = inner_pos.get(&quantize_pt(v)).copied().unwrap_or(v);
379 if strip.last().is_none_or(|q| (*q - p).length() > tol.linear) {
380 strip.push(p);
381 }
382 }
383 if strip.len() > 2 && (strip[0] - strip[strip.len() - 1]).length() <= tol.linear
384 {
385 strip.pop();
386 }
387 if strip.len() >= 3
388 && let Some((n_a, n_b)) =
389 extreme_face_normals(&face_surface_normals(face, outer_verts))
390 && let Ok(outward) = (n_a + n_b).normalize()
391 {
392 strip.reverse();
393 let inner_normal = -outward;
394 let inner_d = dot_normal_point(inner_normal, strip[0]);
395 result_specs.push(FaceSpec::Planar {
396 vertices: strip,
397 normal: inner_normal,
398 d: inner_d,
399 inner_wires: vec![],
400 });
401 }
402 } else if let Ok(new_cyl) = brepkit_math::surfaces::CylindricalSurface::new(
403 cyl.origin(),
404 cyl.axis(),
405 new_radius,
406 ) {
407 let wire = topo.wire(face.outer_wire())?;
413 let has_closed_edge = wire
414 .edges()
415 .iter()
416 .any(|oe| topo.edge(oe.edge()).is_ok_and(|e| e.start() == e.end()));
417 if has_closed_edge {
418 result_specs.push(FaceSpec::Surface {
419 vertices: inner_verts,
420 surface: FaceSurface::Cylinder(new_cyl),
421 reversed: true,
422 inner_wires: vec![],
423 });
424 } else {
425 result_specs.push(FaceSpec::CylindricalFace {
426 vertices: inner_verts,
427 cylinder: new_cyl,
428 reversed: true,
429 inner_wires: vec![],
430 });
431 }
432 }
433 }
434 FaceSurface::Cone(_cone) => {
435 let inner_fid = crate::offset_face::offset_face(topo, fid, -thickness, 8)?;
436 let inner_face = topo.face(inner_fid)?;
437 result_specs.push(FaceSpec::Surface {
438 vertices: inner_verts,
439 surface: inner_face.surface().clone(),
440 reversed: true,
441 inner_wires: vec![],
442 });
443 }
444 FaceSurface::Sphere(sphere) => {
445 let new_r = sphere.radius() - thickness;
446 if new_r <= 0.0 {
447 return Err(crate::OperationsError::InvalidInput {
448 reason: format!(
449 "shell thickness ({thickness}) exceeds sphere radius ({}), \
450 resulting inner sphere would have non-positive radius ({new_r})",
451 sphere.radius(),
452 ),
453 });
454 }
455 let new_sph = brepkit_math::surfaces::SphericalSurface::new(sphere.center(), new_r)
456 .map_err(crate::OperationsError::Math)?;
457 result_specs.push(FaceSpec::Surface {
458 vertices: inner_verts,
459 surface: FaceSurface::Sphere(new_sph),
460 reversed: true,
461 inner_wires: vec![],
462 });
463 }
464 FaceSurface::Nurbs(_) | FaceSurface::Torus(_) => {
465 let inner_fid = crate::offset_face::offset_face(topo, fid, -thickness, 8)?;
466 let inner_face = topo.face(inner_fid)?;
467 result_specs.push(FaceSpec::Surface {
468 vertices: inner_verts,
469 surface: inner_face.surface().clone(),
470 reversed: true,
471 inner_wires: vec![],
472 });
473 }
474 }
475 }
476
477 if result_specs.is_empty() {
486 return Err(crate::OperationsError::InvalidInput {
487 reason: "shell operation produced no faces".into(),
488 });
489 }
490
491 let solid = assemble_solid_mixed(topo, &result_specs, tol)?;
492
493 let edge_face_map = brepkit_topology::explorer::edge_to_face_map(topo, solid)?;
494 let mut boundary_edge_ids: Vec<brepkit_topology::edge::EdgeId> = Vec::new();
495 for (&edge_idx, faces) in &edge_face_map {
496 if faces.len() == 1
497 && let Some(eid) = topo.edge_id_from_index(edge_idx)
498 {
499 boundary_edge_ids.push(eid);
500 }
501 }
502
503 if boundary_edge_ids.is_empty() {
504 return Ok(solid);
506 }
507
508 boundary_edge_ids.sort_by_key(|e| e.index());
512
513 let mut boundary_oriented: Vec<brepkit_topology::wire::OrientedEdge> = Vec::new();
516 for &eid in &boundary_edge_ids {
517 let face_id = edge_face_map[&eid.index()][0];
518 let face = topo.face(face_id)?;
519 let rev = face.is_reversed();
524 let wire = topo.wire(face.outer_wire())?;
525 let mut found = false;
526 for oe in wire.edges() {
527 if oe.edge() == eid {
528 boundary_oriented.push(brepkit_topology::wire::OrientedEdge::new(
529 eid,
530 oe.is_forward() == rev,
531 ));
532 found = true;
533 break;
534 }
535 }
536 if !found {
537 for &iw_id in face.inner_wires() {
538 let iw = topo.wire(iw_id)?;
539 for oe in iw.edges() {
540 if oe.edge() == eid {
541 boundary_oriented.push(brepkit_topology::wire::OrientedEdge::new(
542 eid,
543 oe.is_forward() == rev,
544 ));
545 found = true;
546 break;
547 }
548 }
549 if found {
550 break;
551 }
552 }
553 if !found {
554 boundary_oriented.push(brepkit_topology::wire::OrientedEdge::new(eid, true));
556 }
557 }
558 }
559
560 let loops = sort_edges_into_loops(topo, &boundary_oriented)?;
561
562 if loops.len() < 2 {
563 return Ok(solid);
566 }
567
568 let mut centroid = Vec3::new(0.0, 0.0, 0.0);
570 let mut vert_count = 0.0;
571 let mut rim_z = 0.0_f64;
572 for oe in &boundary_oriented {
573 let edge = topo.edge(oe.edge())?;
574 let p = topo.vertex(edge.start())?.point();
575 centroid += Vec3::new(p.x(), p.y(), p.z());
576 rim_z += p.z();
577 vert_count += 1.0;
578 }
579 if vert_count > 0.0 {
580 centroid = centroid * (1.0 / vert_count);
581 rim_z /= vert_count;
582 }
583
584 let mut loop_radii: Vec<(usize, f64)> = Vec::new();
585 for (i, lp) in loops.iter().enumerate() {
586 let mut avg_r = 0.0;
587 let mut n = 0.0;
588 for oe in lp {
589 let edge = topo.edge(oe.edge())?;
590 let p = topo.vertex(edge.start())?.point();
591 let dx = p.x() - centroid.x();
592 let dy = p.y() - centroid.y();
593 avg_r += (dx * dx + dy * dy).sqrt();
594 n += 1.0;
595 }
596 if n > 0.0 {
597 avg_r /= n;
598 }
599 loop_radii.push((i, avg_r));
600 }
601 loop_radii.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
602
603 let outer_loop_idx = loop_radii[0].0;
605
606 let outer_wire = brepkit_topology::wire::Wire::new(loops[outer_loop_idx].clone(), true)
607 .map_err(crate::OperationsError::Topology)?;
608 let outer_wire_id = topo.add_wire(outer_wire);
609
610 let mut inner_wire_ids = Vec::new();
611 for &(idx, _) in &loop_radii[1..] {
612 let inner_wire = brepkit_topology::wire::Wire::new(loops[idx].clone(), true)
613 .map_err(crate::OperationsError::Topology)?;
614 inner_wire_ids.push(topo.add_wire(inner_wire));
615 }
616
617 let rim_normal = {
621 let mut n = Vec3::new(0.0, 0.0, 1.0);
622 for &(fid, _) in &face_verts {
623 if open_set.contains(&fid.index())
624 && let Ok(f) = topo.face(fid)
625 && let FaceSurface::Plane { normal, .. } = f.surface()
626 {
627 n = if f.is_reversed() { -*normal } else { *normal };
630 break;
631 }
632 }
633 n
634 };
635
636 let rim_d =
637 rim_normal.x() * centroid.x() + rim_normal.y() * centroid.y() + rim_normal.z() * rim_z;
638 let rim_face = brepkit_topology::face::Face::new(
639 outer_wire_id,
640 inner_wire_ids,
641 FaceSurface::Plane {
642 normal: rim_normal,
643 d: rim_d,
644 },
645 );
646 let rim_face_id = topo.add_face(rim_face);
647
648 let solid_data = topo.solid(solid)?;
649 let shell_id = solid_data.outer_shell();
650 let shell = topo.shell(shell_id)?;
651 let mut new_faces: Vec<FaceId> = shell.faces().to_vec();
652 new_faces.push(rim_face_id);
653 let new_shell =
654 brepkit_topology::shell::Shell::new(new_faces).map_err(crate::OperationsError::Topology)?;
655 *topo.shell_mut(shell_id)? = new_shell;
656
657 Ok(solid)
658}
659
660fn sort_edges_into_loops(
665 topo: &Topology,
666 edges: &[brepkit_topology::wire::OrientedEdge],
667) -> Result<Vec<Vec<brepkit_topology::wire::OrientedEdge>>, crate::OperationsError> {
668 use brepkit_topology::vertex::VertexId;
669
670 if edges.is_empty() {
671 return Ok(Vec::new());
672 }
673
674 let mut endpoints: Vec<(VertexId, VertexId)> = Vec::with_capacity(edges.len());
681 let mut incident: HashMap<usize, Vec<usize>> = HashMap::new();
682 for (i, oe) in edges.iter().enumerate() {
683 let edge = topo.edge(oe.edge())?;
684 let (sv, ev) = if oe.is_forward() {
685 (edge.start(), edge.end())
686 } else {
687 (edge.end(), edge.start())
688 };
689 incident.entry(sv.index()).or_default().push(i);
690 incident.entry(ev.index()).or_default().push(i);
691 endpoints.push((sv, ev));
692 }
693
694 let mut used = vec![false; edges.len()];
695 let mut loops = Vec::new();
696
697 while let Some(start_idx) = used.iter().position(|&u| !u) {
698 let mut current_loop = Vec::new();
699 used[start_idx] = true;
700 current_loop.push(edges[start_idx]);
701 let chain_start = endpoints[start_idx].0.index();
702 let mut at = endpoints[start_idx].1.index();
703
704 let mut closed = at == chain_start;
705 while at != chain_start {
706 let mut next: Option<(usize, bool)> = None;
707 if let Some(candidates) = incident.get(&at) {
708 for &idx in candidates {
709 if used[idx] {
710 continue;
711 }
712 let (sv, ev) = endpoints[idx];
713 if sv.index() == at {
714 next = Some((idx, true));
715 } else if ev.index() == at {
716 next = Some((idx, false));
717 } else {
718 continue;
719 }
720 break;
721 }
722 }
723 let Some((idx, as_given)) = next else {
724 break; };
726 used[idx] = true;
727 let oe = edges[idx];
728 let oriented = if as_given {
729 oe
730 } else {
731 brepkit_topology::wire::OrientedEdge::new(oe.edge(), !oe.is_forward())
732 };
733 current_loop.push(oriented);
734 let (sv, ev) = endpoints[idx];
735 at = if as_given { ev.index() } else { sv.index() };
736 closed = at == chain_start;
737 }
738
739 if closed && !current_loop.is_empty() {
742 loops.push(current_loop);
743 } else if !current_loop.is_empty() {
744 log::warn!(
745 "shell rim: dropping an unclosed boundary chain of {} edge(s)",
746 current_loop.len()
747 );
748 }
749 }
750
751 Ok(loops)
752}
753
754#[cfg(test)]
755mod tests;
756
757fn face_surface_normals(face: &brepkit_topology::face::Face, verts: &[Point3]) -> Vec<Vec3> {
759 verts
760 .iter()
761 .map(|v| {
762 let (u, vp) = face.surface().project_point(*v).unwrap_or((0.0, 0.0));
763 let n = face.surface().normal(u, vp);
764 if face.is_reversed() { -n } else { n }
765 })
766 .collect()
767}
768
769fn extreme_face_normals(normals: &[Vec3]) -> Option<(Vec3, Vec3)> {
773 let mut best: Option<(f64, Vec3, Vec3)> = None;
774 for (i, a) in normals.iter().enumerate() {
775 for b in &normals[i + 1..] {
776 let d = a.dot(*b);
777 if best.is_none_or(|(bd, _, _)| d < bd) {
778 best = Some((d, *a, *b));
779 }
780 }
781 }
782 best.filter(|&(d, _, _)| d < 0.999).map(|(_, a, b)| (a, b))
784}