axiolid_pointcloud_reconstruction_contract/evidence.rs
1//! What a reconstruction was asked to do, and what it actually did.
2//!
3//! A reconstruction is an *estimate*: a point set does not determine a unique
4//! surface, so every result carries the assumptions that produced it. The
5//! request records what the caller asked for; the evidence records what the
6//! provider did, including where it had to guess.
7
8use axiolid_core::Scalar;
9use axiolid_mesh::TriMesh;
10
11/// How densely the surface should be reconstructed.
12///
13/// Not an algorithm choice: a caller states the resolution it needs in model
14/// units and the provider maps that onto whatever its method uses (octree
15/// depth, ball radius, alpha value). Stating it in units keeps the request
16/// portable across providers that share no parameters.
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub enum Resolution {
19 /// Target edge length of the reconstructed surface, in model units.
20 ///
21 /// The provider is expected to approach this, not to guarantee it: the
22 /// achieved value is reported in [`ReconstructionEvidence`].
23 TargetEdgeLength(Scalar),
24 /// Let the provider choose from the sample spacing it measures.
25 ///
26 /// Honest default for a caller with no resolution requirement. What was
27 /// chosen is reported rather than left implicit.
28 FromSampleSpacing,
29}
30
31/// What the caller wants reconstructed.
32#[derive(Debug, Clone, PartialEq)]
33#[non_exhaustive]
34pub struct ReconstructionRequest {
35 /// Surface density to aim for.
36 pub resolution: Resolution,
37 /// Whether the result must be a closed solid.
38 ///
39 /// A scan of a room's interior has no back faces, so demanding closure
40 /// forces the provider to invent surface where it has no data. Set this
41 /// only when the capture genuinely covers the whole object; a provider
42 /// that cannot honour it must refuse rather than fabricate.
43 pub require_closed: bool,
44 /// Whether per-point normals may be used when present.
45 ///
46 /// Normals dramatically improve most methods, but a capture's normals
47 /// can be wrong. Turning this off asks the provider to work from
48 /// positions alone.
49 pub use_normals: bool,
50}
51
52impl Default for ReconstructionRequest {
53 fn default() -> Self {
54 Self {
55 resolution: Resolution::FromSampleSpacing,
56 require_closed: false,
57 use_normals: true,
58 }
59 }
60}
61
62impl ReconstructionRequest {
63 /// A request at a stated resolution.
64 pub fn at_edge_length(edge_length: Scalar) -> Self {
65 Self {
66 resolution: Resolution::TargetEdgeLength(edge_length),
67 ..Self::default()
68 }
69 }
70
71 /// Demand a closed result, refusing rather than inventing surface.
72 pub fn requiring_closed(mut self) -> Self {
73 self.require_closed = true;
74 self
75 }
76
77 /// Ignore per-point normals even when the cloud carries them.
78 pub fn ignoring_normals(mut self) -> Self {
79 self.use_normals = false;
80 self
81 }
82}
83
84/// Counters describing one reconstruction.
85///
86/// Every field is a fact about the computation, never a quality verdict:
87/// whether 12% unsupported area is acceptable is the caller's decision, not
88/// the kernel's.
89#[derive(Debug, Clone, PartialEq, Default)]
90#[non_exhaustive]
91pub struct ReconstructionEvidence {
92 /// Points supplied.
93 pub input_points: usize,
94 /// Points the provider actually used.
95 ///
96 /// Lower than `input_points` when the provider discarded outliers or
97 /// duplicates. A large gap is worth a caller's attention.
98 pub used_points: usize,
99 /// Triangles in the result.
100 pub output_triangles: usize,
101 /// Connected components in the result.
102 ///
103 /// A capture with gaps often reconstructs into several shells. Reporting
104 /// it lets a caller detect the split here rather than downstream.
105 pub output_components: usize,
106 /// Whether the result is a closed two-manifold solid.
107 pub closed: bool,
108 /// Edge length the provider achieved, in model units.
109 ///
110 /// Reported whether or not the caller stated one, so a
111 /// `FromSampleSpacing` request still learns what it got.
112 pub achieved_edge_length: Scalar,
113 /// Median distance between neighbouring input samples.
114 ///
115 /// The scale the capture actually resolves. A result finer than this is
116 /// interpolation, not measurement.
117 pub sample_spacing: Scalar,
118 /// Whether per-point normals were used.
119 ///
120 /// False when the cloud carried none, or the request declined them, or
121 /// the provider could not use them. Distinguishes a normal-driven
122 /// result from a positions-only one.
123 pub used_normals: bool,
124 /// Triangles whose vertices came from interpolation across a gap in the
125 /// data rather than from measured samples.
126 ///
127 /// This is the honest measure of how much of the surface is invented.
128 /// Zero means every triangle rests on real points.
129 pub interpolated_triangles: usize,
130}
131
132impl ReconstructionEvidence {
133 /// Start from a cleared record and fill in what the provider measured.
134 ///
135 /// `#[non_exhaustive]` keeps the struct additive, so a provider outside
136 /// this crate cannot build one with a literal. This constructor is the
137 /// supported route: take the default and set the fields you measured.
138 /// A field left at its default is honestly "not measured" rather than a
139 /// value invented to satisfy the type.
140 pub fn measured() -> Self {
141 Self::default()
142 }
143}
144
145/// A reconstruction and the evidence that produced it.
146#[derive(Debug, Clone, PartialEq)]
147pub struct ReconstructionOutcome {
148 /// The reconstructed surface. An empty mesh is a legitimate value only
149 /// when the provider also explains it in the evidence.
150 pub mesh: TriMesh,
151 /// What the provider did.
152 pub evidence: ReconstructionEvidence,
153}
154
155impl ReconstructionOutcome {
156 /// Pair a mesh with its evidence.
157 pub const fn new(mesh: TriMesh, evidence: ReconstructionEvidence) -> Self {
158 Self { mesh, evidence }
159 }
160}