Skip to main content

ogeom_heal/
solid.rs

1//! Solids from closed shells, outer boundaries told from voids by which
2//! shell lies inside which.
3//!
4//! A file or a sewing hands over closed shells with nothing saying which
5//! bounds a part and which a cavity in one. Nesting says it: a shell inside
6//! no other is a part's outer boundary, a shell inside one is a void of
7//! it, a shell inside two is the outer boundary of a part inside that
8//! cavity, and so on down.
9
10use 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
14/// The solids `shells` bound: one where a single shell is outermost, else
15/// a compound of them, one per outermost shell and per shell lying in a
16/// void.
17///
18/// Each shell is placed against each other shell by its vertices that lie
19/// off it, classified by ray parity, which needs no shell to face either
20/// way; they must all read inside or all outside. Shells whose faces cross
21/// with no vertex of either inside the other are not told apart from
22/// shells side by side. A shell's parent is the deepest shell that
23/// holds it. A shell held by an even number of shells is an outer
24/// boundary, and the shells it is the parent of are its voids. Each solid
25/// is put together by [`make_solid`](ogeom_algo::make_solid), which turns
26/// an outer shell to face out of the volume it bounds and a void's to face
27/// into its cavity.
28///
29/// # Errors
30///
31/// [`OgeomError::Construction`] if
32/// `shells` is empty, holds something that is not a closed shell, or two
33/// shells cross (vertices of one on both sides of the other, or each
34/// holding the other) or one has every vertex on the other; as
35/// classification otherwise.
36pub 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    // holds[i][j]: shell i holds shell j.
53    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
91/// Whether `inner` lies inside the volume `outer` encloses, read at every
92/// vertex of `inner` that is not on `outer`: all must agree.
93fn 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}