Skip to main content

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, a face or a trimmed surface patch (#311), or a closed shell
22    /// the source never declared a solid. Area is meaningful; volume is
23    /// not.
24    Surface,
25    /// The compiler did not report it.
26    Unknown,
27    /// The source declared a solid, but the compiled mesh is not closed:
28    /// some edge is not shared by exactly two triangles running it in
29    /// opposite directions (#265). Neither its volume nor its closure can
30    /// be trusted; the compiler reports this rather than claiming a solid
31    /// for a mesh with boundary edges.
32    OpenSolid,
33}
34
35/// A compiled mesh and, when the compiler tracks it, each channel's fate.
36///
37/// Compilation moves and concatenates geometry, and a boolean inside the
38/// graph cuts it, so a channel can reach the result unchanged, derived, or
39/// not at all. `mesh.attributes` shows what survived; `attribute_fates`
40/// says why the rest did not -- the #84 contract, applied to graph
41/// compilation (#115).
42#[non_exhaustive]
43#[derive(Debug, Clone, PartialEq)]
44pub struct CompileOutcome {
45    /// The compiled mesh.
46    pub mesh: TriMesh,
47    /// Each channel seen anywhere in the compiled subgraph, in first-seen
48    /// order, with its worst fate on any path to the root.
49    ///
50    /// `None` when the compiler does not track channels: that is "unknown",
51    /// not "nothing was dropped".
52    pub attribute_fates: Option<Vec<(String, AttributeFate)>>,
53    /// Whether the mesh bounds a solid. [`MeshClosure::Unknown`] unless the
54    /// compiler reports it; read the mesh through [`Self::solid_mesh`]
55    /// before taking a volume from it.
56    pub closure: MeshClosure,
57}
58
59impl CompileOutcome {
60    /// A mesh from a compiler that does not report channel fates.
61    pub const fn untracked(mesh: TriMesh) -> Self {
62        Self {
63            mesh,
64            attribute_fates: None,
65            closure: MeshClosure::Unknown,
66        }
67    }
68
69    /// A mesh with its channel report.
70    pub const fn tracked(mesh: TriMesh, attribute_fates: Vec<(String, AttributeFate)>) -> Self {
71        Self {
72            mesh,
73            attribute_fates: Some(attribute_fates),
74            closure: MeshClosure::Unknown,
75        }
76    }
77
78    /// The same outcome with its closure reported.
79    #[must_use]
80    pub const fn with_closure(mut self, closure: MeshClosure) -> Self {
81        self.closure = closure;
82        self
83    }
84
85    /// The mesh, only if it is known to bound a solid.
86    ///
87    /// Use this before measuring volume, centroid or second moments. A
88    /// surface model's mesh can be closed and consistently wound, so a
89    /// volume computed from it would be a plausible number with no meaning;
90    /// this refuses instead of returning it (#161).
91    ///
92    /// # Errors
93    ///
94    /// [`GeomError::InvalidInput`] for [`MeshClosure::Surface`], for
95    /// [`MeshClosure::Unknown`] (an unreported closure is not a solid) and
96    /// for [`MeshClosure::OpenSolid`] (a declared solid whose mesh does not
97    /// close).
98    pub fn solid_mesh(&self) -> GeomResult<&TriMesh> {
99        match self.closure {
100            MeshClosure::Solid => Ok(&self.mesh),
101            MeshClosure::Surface => Err(GeomError::InvalidInput(
102                "compiled mesh is a surface model: it has area but encloses no volume".to_owned(),
103            )),
104            MeshClosure::Unknown => Err(GeomError::InvalidInput(
105                "compiler did not report whether the mesh bounds a solid".to_owned(),
106            )),
107            MeshClosure::OpenSolid => Err(GeomError::InvalidInput(
108                "compiled mesh of a declared solid is not closed: it has boundary or \
109                 non-manifold edges, so it encloses no volume"
110                    .to_owned(),
111            )),
112        }
113    }
114
115    /// The fate of the channel named `name`, if the compiler reported one.
116    pub fn fate(&self, name: &str) -> Option<&AttributeFate> {
117        self.attribute_fates
118            .as_ref()?
119            .iter()
120            .find(|(seen, _)| seen == name)
121            .map(|(_, fate)| fate)
122    }
123}