axiolid_mesh_compile_contract/outcome.rs
1//! What a compilation did to the caller's attribute channels, and whether
2//! the result encloses a volume.
3
4use axiolid_contracts::{GeomError, GeomResult};
5use axiolid_mesh::{AttributeFate, TriMesh};
6
7/// Whether a compiled mesh bounds a solid (#161).
8///
9/// A triangle mesh looks the same either way: a closed surface model (say,
10/// an IFC `IfcShellBasedSurfaceModel` shaped like a box) is watertight, so
11/// its divergence sum is a finite, plausible number. It is still not a
12/// volume, because the source never claimed one. This is what tells the
13/// two apart.
14#[non_exhaustive]
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum MeshClosure {
17 /// Every part of the mesh is the boundary of a solid; its enclosed
18 /// volume is meaningful.
19 Solid,
20 /// At least one part is a surface with no solid behind it: an open
21 /// shell, or a closed shell the source never declared a solid. Area is
22 /// meaningful; volume is not.
23 Surface,
24 /// The compiler did not report it.
25 Unknown,
26}
27
28/// A compiled mesh and, when the compiler tracks it, each channel's fate.
29///
30/// Compilation moves and concatenates geometry, and a boolean inside the
31/// graph cuts it, so a channel can reach the result unchanged, derived, or
32/// not at all. `mesh.attributes` shows what survived; `attribute_fates`
33/// says why the rest did not -- the #84 contract, applied to graph
34/// compilation (#115).
35#[non_exhaustive]
36#[derive(Debug, Clone, PartialEq)]
37pub struct CompileOutcome {
38 /// The compiled mesh.
39 pub mesh: TriMesh,
40 /// Each channel seen anywhere in the compiled subgraph, in first-seen
41 /// order, with its worst fate on any path to the root.
42 ///
43 /// `None` when the compiler does not track channels: that is "unknown",
44 /// not "nothing was dropped".
45 pub attribute_fates: Option<Vec<(String, AttributeFate)>>,
46 /// Whether the mesh bounds a solid. [`MeshClosure::Unknown`] unless the
47 /// compiler reports it; read the mesh through [`Self::solid_mesh`]
48 /// before taking a volume from it.
49 pub closure: MeshClosure,
50}
51
52impl CompileOutcome {
53 /// A mesh from a compiler that does not report channel fates.
54 pub const fn untracked(mesh: TriMesh) -> Self {
55 Self {
56 mesh,
57 attribute_fates: None,
58 closure: MeshClosure::Unknown,
59 }
60 }
61
62 /// A mesh with its channel report.
63 pub const fn tracked(mesh: TriMesh, attribute_fates: Vec<(String, AttributeFate)>) -> Self {
64 Self {
65 mesh,
66 attribute_fates: Some(attribute_fates),
67 closure: MeshClosure::Unknown,
68 }
69 }
70
71 /// The same outcome with its closure reported.
72 #[must_use]
73 pub const fn with_closure(mut self, closure: MeshClosure) -> Self {
74 self.closure = closure;
75 self
76 }
77
78 /// The mesh, only if it is known to bound a solid.
79 ///
80 /// Use this before measuring volume, centroid or second moments. A
81 /// surface model's mesh can be closed and consistently wound, so a
82 /// volume computed from it would be a plausible number with no meaning;
83 /// this refuses instead of returning it (#161).
84 ///
85 /// # Errors
86 ///
87 /// [`GeomError::InvalidInput`] for [`MeshClosure::Surface`] and for
88 /// [`MeshClosure::Unknown`]: an unreported closure is not a solid.
89 pub fn solid_mesh(&self) -> GeomResult<&TriMesh> {
90 match self.closure {
91 MeshClosure::Solid => Ok(&self.mesh),
92 MeshClosure::Surface => Err(GeomError::InvalidInput(
93 "compiled mesh is a surface model: it has area but encloses no volume".to_owned(),
94 )),
95 MeshClosure::Unknown => Err(GeomError::InvalidInput(
96 "compiler did not report whether the mesh bounds a solid".to_owned(),
97 )),
98 }
99 }
100
101 /// The fate of the channel named `name`, if the compiler reported one.
102 pub fn fate(&self, name: &str) -> Option<&AttributeFate> {
103 self.attribute_fates
104 .as_ref()?
105 .iter()
106 .find(|(seen, _)| seen == name)
107 .map(|(_, fate)| fate)
108 }
109}