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}