1use ogeom_algo::{Built, Containment, History, classify_in_solid_exact, is_shell_closed};
11use ogeom_core::{OgeomError, OgeomResult, Tolerances, ogeom_bail};
12use ogeom_topo::{Model, Shape, ShapeType, explore_unique};
13
14pub fn solids_from_shells(
37 model: &mut Model,
38 shells: &[Shape],
39 tol: Tolerances,
40) -> OgeomResult<Built> {
41 if shells.is_empty() {
42 ogeom_bail!(Construction, "there are no shells to make solids of");
43 }
44 for shell in shells {
45 if model.kind_of(shell)? != ShapeType::Shell || !is_shell_closed(model, shell)? {
46 ogeom_bail!(
47 Construction,
48 "a solid is bounded by closed shells, and this is not one"
49 );
50 }
51 }
52 let n = shells.len();
54 let mut holds = vec![vec![false; n]; n];
55 for j in 0..n {
56 for i in (0..n).filter(|&i| i != j) {
57 holds[i][j] = holds_shell(model, &shells[i], &shells[j], tol)?;
58 }
59 }
60 if (0..n).any(|i| (0..i).any(|j| holds[i][j] && holds[j][i])) {
61 ogeom_bail!(Construction, "two shells cross: each holds the other");
62 }
63 let depth: Vec<usize> = (0..n)
64 .map(|j| (0..n).filter(|&i| holds[i][j]).count())
65 .collect();
66 let parent: Vec<Option<usize>> = (0..n)
67 .map(|j| (0..n).filter(|&i| holds[i][j]).max_by_key(|&i| depth[i]))
68 .collect();
69 let mut solids = Vec::new();
70 let mut history = History::new();
71 for outer in (0..n).filter(|&i| depth[i].is_multiple_of(2)) {
72 let mut bounds = vec![shells[outer].clone()];
73 bounds.extend(
74 (0..n)
75 .filter(|&j| !depth[j].is_multiple_of(2) && parent[j] == Some(outer))
76 .map(|j| shells[j].clone()),
77 );
78 let solid = ogeom_algo::make_solid(model, &bounds)?.shape;
79 for shell in &bounds {
80 history.generate(shell, solid.clone());
81 }
82 solids.push(solid);
83 }
84 if let [solid] = &solids[..] {
85 return Ok(Built::new(solid.clone(), history));
86 }
87 let compound = ogeom_algo::make_compound(model, &solids)?.shape;
88 Ok(Built::new(compound, history))
89}
90
91fn holds_shell(model: &Model, outer: &Shape, inner: &Shape, tol: Tolerances) -> OgeomResult<bool> {
94 let mut read = None;
95 for vertex in explore_unique(model, inner, ShapeType::Vertex)? {
96 let Some(data) = model.node(&vertex).and_then(|n| n.data().as_vertex()) else {
97 continue;
98 };
99 let point = vertex.transform(model.datums())?.apply(data.point);
100 let inside = match classify_in_solid_exact(model, outer, point, tol) {
101 Ok(Containment::In) => true,
102 Ok(Containment::Out) => false,
103 Ok(Containment::On) | Err(OgeomError::NotDone(_)) => continue,
104 Err(e) => return Err(e),
105 };
106 if read.is_some_and(|was| was != inside) {
107 ogeom_bail!(
108 Construction,
109 "two shells cross: one has vertices inside the other and outside it"
110 );
111 }
112 read = Some(inside);
113 }
114 let Some(inside) = read else {
115 ogeom_bail!(
116 Construction,
117 "every vertex of one shell lies on another: neither holds the other"
118 );
119 };
120 Ok(inside)
121}