axiolid_pointcloud_reconstruction_contract/
contract.rs

1//! The provider trait: what implementing reconstruction obliges you to.
2
3use axiolid_contracts::{
4    Backend, CancellationGranularity, Determinism, ExecutionOptions, GeomResult, ScratchRequirement,
5};
6use axiolid_core::Scalar;
7use axiolid_pointcloud::PointCloud;
8
9use crate::{ReconstructionOutcome, ReconstructionRequest};
10
11/// Why a reconstruction could not be performed.
12///
13/// Every variant is a *typed refusal*: the provider knows why it cannot
14/// answer and says so. A provider must never return an empty mesh in place
15/// of one of these — an empty result and "I cannot do this" are different
16/// answers, and a caller that cannot tell them apart will treat a failure as
17/// an object that is not there.
18#[derive(Debug, Clone, PartialEq)]
19#[non_exhaustive]
20pub enum ReconstructionRefusal {
21    /// Fewer points than the method needs.
22    TooFewPoints {
23        /// Points supplied.
24        supplied: usize,
25        /// Minimum this provider needs.
26        required: usize,
27    },
28    /// The points are degenerate: all coincident, collinear, or coplanar.
29    ///
30    /// A surface cannot be reconstructed from a set with no volume, and
31    /// producing a zero-thickness sheet would misrepresent the input.
32    DegenerateExtent {
33        /// What the provider measured.
34        detail: String,
35    },
36    /// The request demanded a closed result the data cannot support.
37    ///
38    /// Raised instead of inventing surface across a gap the capture never
39    /// covered.
40    CannotClose {
41        /// Why closure is impossible for this input.
42        detail: String,
43    },
44    /// The requested resolution is finer than the samples resolve.
45    ///
46    /// Honouring it would interpolate detail that was never measured and
47    /// present it as geometry.
48    ResolutionExceedsData {
49        /// Edge length the caller asked for.
50        requested: Scalar,
51        /// Spacing the samples actually resolve.
52        sample_spacing: Scalar,
53    },
54    /// The provider does not implement some part of the request.
55    Unsupported {
56        /// What is unsupported.
57        detail: String,
58    },
59}
60
61impl core::fmt::Display for ReconstructionRefusal {
62    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
63        match self {
64            Self::TooFewPoints { supplied, required } => write!(
65                f,
66                "reconstruction needs at least {required} points, got {supplied}"
67            ),
68            Self::DegenerateExtent { detail } => {
69                write!(f, "point set has no reconstructable extent: {detail}")
70            }
71            Self::CannotClose { detail } => {
72                write!(f, "a closed result was required but is not supported by the data: {detail}")
73            }
74            Self::ResolutionExceedsData {
75                requested,
76                sample_spacing,
77            } => write!(
78                f,
79                "requested edge length {requested} is finer than the sample spacing {sample_spacing}"
80            ),
81            Self::Unsupported { detail } => write!(f, "unsupported request: {detail}"),
82        }
83    }
84}
85
86impl core::error::Error for ReconstructionRefusal {}
87
88/// Either a reconstruction or a typed reason there is none.
89///
90/// Deliberately not `Option<TriMesh>`: an absent surface always has a
91/// reason, and this type makes the reason impossible to drop.
92#[derive(Debug, Clone, PartialEq)]
93pub enum Reconstruction {
94    /// A surface was produced.
95    Surface(Box<ReconstructionOutcome>),
96    /// No surface was produced, for this stated reason.
97    Refused(ReconstructionRefusal),
98}
99
100impl Reconstruction {
101    /// The outcome, if one was produced.
102    pub fn outcome(&self) -> Option<&ReconstructionOutcome> {
103        match self {
104            Self::Surface(outcome) => Some(outcome),
105            Self::Refused(_) => None,
106        }
107    }
108
109    /// The refusal, if there was one.
110    pub fn refusal(&self) -> Option<&ReconstructionRefusal> {
111        match self {
112            Self::Surface(_) => None,
113            Self::Refused(reason) => Some(reason),
114        }
115    }
116
117    /// Whether a surface was produced.
118    pub fn is_surface(&self) -> bool {
119        matches!(self, Self::Surface(_))
120    }
121}
122
123/// Pointcloud-to-surface reconstruction provider.
124///
125/// Implementing this trait is the capability declaration: a type that
126/// implements it claims it can turn an unstructured point set into a
127/// surface. Providers that cannot must not implement it.
128pub trait PointcloudReconstruction: Backend {
129    /// Scratch this provider needs beyond its inputs and result.
130    ///
131    /// Defaults to [`ScratchRequirement::Unbounded`] so an unaudited
132    /// provider is treated as unbudgetable rather than assumed cheap.
133    /// Reconstruction is memory-hungry — an octree over a hundred million
134    /// points is not a detail a caller should discover by being killed.
135    fn scratch_requirement(&self) -> ScratchRequirement {
136        ScratchRequirement::Unbounded
137    }
138
139    /// Reproducibility this provider guarantees.
140    ///
141    /// Defaults to the weakest level so a provider that has not audited
142    /// itself cannot silently satisfy a stronger request.
143    fn determinism(&self) -> Determinism {
144        Determinism::BestEffort
145    }
146
147    /// How finely this provider observes cancellation.
148    ///
149    /// Defaults to [`CancellationGranularity::None`]: a provider that has
150    /// not audited its own polling must not advertise responsiveness it
151    /// cannot deliver. Reconstruction runs long, so overstating this is
152    /// exactly the case a caller would feel.
153    fn cancellation_granularity(&self) -> CancellationGranularity {
154        CancellationGranularity::None
155    }
156
157    /// Minimum points this provider needs to attempt a reconstruction.
158    ///
159    /// Callers can check before paying for a large request. A provider
160    /// still refuses by name if given fewer.
161    fn minimum_points(&self) -> usize {
162        4
163    }
164
165    /// Reconstruct a surface from a point set.
166    ///
167    /// Returns [`Reconstruction::Refused`] with a typed reason when the
168    /// request cannot be honoured. An `Err` is reserved for a failure of
169    /// the machinery itself — a refusal is an answer, not an error.
170    fn reconstruct(
171        &self,
172        cloud: &PointCloud,
173        request: &ReconstructionRequest,
174        options: &ExecutionOptions,
175    ) -> GeomResult<Reconstruction>;
176}