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}