1use std::sync::Arc;
18
19use axioval_ir::{Evidence, ObjectId};
20
21use crate::services::reviewable_exact_evidence;
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
25pub enum ContactError {
26 #[error("contact areas must be finite, non-negative and contained")]
28 InvalidAreas,
29 #[error("contact evidence must be exact and reviewable")]
31 InexactEvidence,
32 #[error("contact measurement is unavailable for the requested scope")]
34 Unavailable,
35 #[error("object body has no checkable orientation")]
37 UncheckableOrientation,
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum ContactSide {
43 Above,
44 Below,
45}
46
47#[derive(Clone, Copy, Debug, PartialEq)]
52#[allow(clippy::struct_field_names)]
56pub struct ContactTolerance {
57 maximum_gap_metres: f64,
58 maximum_intersection_metres: f64,
59 minimum_polygon_area_square_metres: f64,
60}
61
62impl ContactTolerance {
63 pub fn try_new(
64 maximum_gap_metres: f64,
65 maximum_intersection_metres: f64,
66 minimum_polygon_area_square_metres: f64,
67 ) -> Result<Self, ContactError> {
68 let ok = |v: f64| v.is_finite() && v >= 0.0;
69 if !ok(maximum_gap_metres)
70 || !ok(maximum_intersection_metres)
71 || !ok(minimum_polygon_area_square_metres)
72 {
73 return Err(ContactError::InvalidAreas);
74 }
75 Ok(Self {
76 maximum_gap_metres,
77 maximum_intersection_metres,
78 minimum_polygon_area_square_metres,
79 })
80 }
81 pub fn maximum_gap_metres(&self) -> f64 {
82 self.maximum_gap_metres
83 }
84 pub fn maximum_intersection_metres(&self) -> f64 {
85 self.maximum_intersection_metres
86 }
87 pub fn minimum_polygon_area_square_metres(&self) -> f64 {
88 self.minimum_polygon_area_square_metres
89 }
90}
91
92#[derive(Clone, Debug, PartialEq)]
94pub struct ContactRequest {
95 subject: ObjectId,
96 side: ContactSide,
97 tolerance: ContactTolerance,
98}
99
100impl ContactRequest {
101 pub fn new(subject: ObjectId, side: ContactSide, tolerance: ContactTolerance) -> Self {
102 Self {
103 subject,
104 side,
105 tolerance,
106 }
107 }
108 pub fn subject(&self) -> &ObjectId {
109 &self.subject
110 }
111 pub fn side(&self) -> ContactSide {
112 self.side
113 }
114 pub fn tolerance(&self) -> ContactTolerance {
115 self.tolerance
116 }
117}
118
119#[derive(Clone, Debug, PartialEq)]
121pub struct ContactEvidence {
122 request: ContactRequest,
123 whole_area_square_metres: f64,
124 contact_area_square_metres: f64,
125 nearest_distance_metres: Option<f64>,
126 touching: Vec<ObjectId>,
127 evidence: Evidence,
128}
129
130impl ContactEvidence {
131 pub fn try_new(
134 request: ContactRequest,
135 whole_area_square_metres: f64,
136 contact_area_square_metres: f64,
137 nearest_distance_metres: Option<f64>,
138 mut touching: Vec<ObjectId>,
139 evidence: Evidence,
140 ) -> Result<Self, ContactError> {
141 let finite_non_negative = |v: f64| v.is_finite() && v >= 0.0;
142 if !finite_non_negative(whole_area_square_metres)
143 || !finite_non_negative(contact_area_square_metres)
144 || whole_area_square_metres <= 0.0
145 || contact_area_square_metres > whole_area_square_metres
148 {
149 return Err(ContactError::InvalidAreas);
150 }
151 if nearest_distance_metres.is_some_and(|d| !finite_non_negative(d)) {
152 return Err(ContactError::InvalidAreas);
153 }
154 if !reviewable_exact_evidence(&evidence) {
155 return Err(ContactError::InexactEvidence);
156 }
157 touching.sort();
158 touching.dedup();
159 Ok(Self {
160 request,
161 whole_area_square_metres,
162 contact_area_square_metres,
163 nearest_distance_metres,
164 touching,
165 evidence,
166 })
167 }
168
169 pub fn request(&self) -> &ContactRequest {
170 &self.request
171 }
172 pub fn whole_area_square_metres(&self) -> f64 {
173 self.whole_area_square_metres
174 }
175 pub fn contact_area_square_metres(&self) -> f64 {
176 self.contact_area_square_metres
177 }
178 pub fn nearest_distance_metres(&self) -> Option<f64> {
180 self.nearest_distance_metres
181 }
182 pub fn touching(&self) -> &[ObjectId] {
184 &self.touching
185 }
186 pub fn evidence(&self) -> &Evidence {
187 &self.evidence
188 }
189
190 pub fn contact_ratio(&self) -> f64 {
195 self.contact_area_square_metres / self.whole_area_square_metres
196 }
197}
198
199pub trait ContactService: Send + Sync + 'static {
203 fn measure_contact(&self, request: &ContactRequest) -> Result<ContactEvidence, ContactError>;
204}
205
206#[derive(Clone)]
208pub struct ContactServiceHandle(Arc<dyn ContactService>);
209
210impl ContactServiceHandle {
211 pub fn new(service: Arc<dyn ContactService>) -> Self {
212 Self(service)
213 }
214 pub fn measure_contact(
215 &self,
216 request: &ContactRequest,
217 ) -> Result<ContactEvidence, ContactError> {
218 self.0.measure_contact(request)
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use axioval_ir::SourceId;
226
227 fn id(local: &str) -> ObjectId {
228 ObjectId::new(SourceId::new("cad", "m").unwrap(), local).unwrap()
229 }
230 fn request() -> ContactRequest {
231 ContactRequest::new(
232 id("wall"),
233 ContactSide::Above,
234 ContactTolerance::try_new(0.01, 0.01, 0.001).unwrap(),
235 )
236 }
237 fn evidence() -> Evidence {
238 Evidence::exact(SourceId::new("cad", "m").unwrap(), "contact:wall")
239 }
240
241 #[test]
244 fn contact_exceeding_the_whole_face_is_refused() {
245 assert_eq!(
246 ContactEvidence::try_new(request(), 10.0, 11.0, None, Vec::new(), evidence()),
247 Err(ContactError::InvalidAreas)
248 );
249 }
250
251 #[test]
252 fn zero_or_non_finite_whole_area_is_refused() {
253 for whole in [0.0, f64::NAN, f64::INFINITY, -1.0] {
254 assert_eq!(
255 ContactEvidence::try_new(request(), whole, 0.0, None, Vec::new(), evidence()),
256 Err(ContactError::InvalidAreas)
257 );
258 }
259 }
260
261 #[test]
264 fn contact_ratio_is_exact_and_unrounded() {
265 let measured =
266 ContactEvidence::try_new(request(), 3.0, 1.0, None, Vec::new(), evidence()).unwrap();
267 assert!((measured.contact_ratio() - 1.0 / 3.0).abs() < f64::EPSILON);
268 }
269
270 #[test]
271 fn touching_objects_are_sorted_and_deduplicated() {
272 let measured = ContactEvidence::try_new(
273 request(),
274 4.0,
275 2.0,
276 None,
277 vec![id("slab-b"), id("slab-a"), id("slab-b")],
278 evidence(),
279 )
280 .unwrap();
281 assert_eq!(measured.touching(), &[id("slab-a"), id("slab-b")]);
282 }
283}