1use std::sync::Arc;
29
30use axioval_ir::{Evidence, ObjectId};
31
32use crate::plan_span::{
33 PlanLength, PlanRectangle, PlanSpanError, PlanSpanService, RectangleOrientation,
34};
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
38pub enum RectangleSide {
39 AlongFirst,
41 AlongSecond,
43 AgainstFirst,
45 AgainstSecond,
47}
48
49impl RectangleSide {
50 pub const ALL: [Self; 4] = [
52 Self::AlongFirst,
53 Self::AlongSecond,
54 Self::AgainstFirst,
55 Self::AgainstSecond,
56 ];
57
58 #[must_use]
60 pub fn axis(self) -> usize {
61 match self {
62 Self::AlongFirst | Self::AgainstFirst => 0,
63 Self::AlongSecond | Self::AgainstSecond => 1,
64 }
65 }
66
67 #[must_use]
69 pub fn sign(self) -> f64 {
70 match self {
71 Self::AlongFirst | Self::AlongSecond => 1.0,
72 Self::AgainstFirst | Self::AgainstSecond => -1.0,
73 }
74 }
75
76 #[must_use]
78 pub fn opposite(self) -> Self {
79 match self {
80 Self::AlongFirst => Self::AgainstFirst,
81 Self::AlongSecond => Self::AgainstSecond,
82 Self::AgainstFirst => Self::AlongFirst,
83 Self::AgainstSecond => Self::AlongSecond,
84 }
85 }
86
87 #[must_use]
89 pub fn outward(self, rectangle: &PlanRectangle) -> [f64; 2] {
90 rectangle.axes()[self.axis()].map(|value| self.sign() * value)
91 }
92
93 #[must_use]
95 pub fn name(self) -> &'static str {
96 match self {
97 Self::AlongFirst => "+first",
98 Self::AlongSecond => "+second",
99 Self::AgainstFirst => "-first",
100 Self::AgainstSecond => "-second",
101 }
102 }
103}
104
105#[derive(Clone, Debug, PartialEq)]
108pub struct SideDistanceRequest {
109 object: ObjectId,
110 candidates: Vec<ObjectId>,
111 reach: f64,
112 inset: f64,
113}
114
115impl SideDistanceRequest {
116 pub fn try_new(
122 object: ObjectId,
123 candidates: impl IntoIterator<Item = ObjectId>,
124 reach: f64,
125 inset: f64,
126 ) -> Result<Self, PlanSpanError> {
127 let mut candidates: Vec<ObjectId> = candidates.into_iter().collect();
128 candidates.sort();
129 candidates.dedup();
130 if candidates.contains(&object) {
131 return Err(PlanSpanError::Unavailable(format!(
132 "{object} cannot lie beside itself"
133 )));
134 }
135 if !(reach.is_finite() && reach > 0.0 && inset.is_finite() && inset >= 0.0) {
136 return Err(PlanSpanError::Unavailable(
137 "a side-distance reach must be positive and its inset non-negative".into(),
138 ));
139 }
140 Ok(Self {
141 object,
142 candidates,
143 reach,
144 inset,
145 })
146 }
147
148 #[must_use]
150 pub fn object(&self) -> &ObjectId {
151 &self.object
152 }
153
154 #[must_use]
156 pub fn candidates(&self) -> &[ObjectId] {
157 &self.candidates
158 }
159
160 #[must_use]
162 pub fn reach_metres(&self) -> f64 {
163 self.reach
164 }
165
166 #[must_use]
168 pub fn inset_metres(&self) -> f64 {
169 self.inset
170 }
171}
172
173#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
175pub enum SidePresence {
176 Sure,
178 Possible,
180}
181
182#[derive(Clone, Debug, PartialEq)]
184pub struct SideDistance {
185 candidate: ObjectId,
186 side: RectangleSide,
187 presence: SidePresence,
188 distance: PlanLength,
189}
190
191impl SideDistance {
192 #[must_use]
195 pub fn new(
196 candidate: ObjectId,
197 side: RectangleSide,
198 presence: SidePresence,
199 distance: PlanLength,
200 ) -> Self {
201 Self {
202 candidate,
203 side,
204 presence,
205 distance,
206 }
207 }
208
209 #[must_use]
211 pub fn candidate(&self) -> &ObjectId {
212 &self.candidate
213 }
214
215 #[must_use]
217 pub fn side(&self) -> RectangleSide {
218 self.side
219 }
220
221 #[must_use]
223 pub fn presence(&self) -> SidePresence {
224 self.presence
225 }
226
227 #[must_use]
229 pub fn distance(&self) -> &PlanLength {
230 &self.distance
231 }
232}
233
234#[derive(Clone, Debug, PartialEq)]
237pub struct SideDistances {
238 request: SideDistanceRequest,
239 rectangle: PlanRectangle,
240 distances: Vec<SideDistance>,
241 evidence: Evidence,
242}
243
244impl SideDistances {
245 pub fn try_new(
252 request: SideDistanceRequest,
253 rectangle: PlanRectangle,
254 mut distances: Vec<SideDistance>,
255 evidence: Evidence,
256 ) -> Result<Self, PlanSpanError> {
257 if rectangle.object() != request.object() {
258 return Err(PlanSpanError::Unavailable(format!(
259 "side distances from a rectangle of {} were returned for {}",
260 rectangle.object(),
261 request.object()
262 )));
263 }
264 if rectangle.orientation() != RectangleOrientation::Unique {
265 return Err(PlanSpanError::Unavailable(format!(
266 "the least-area rectangle of {} is {}, so its sides are not the footprint's own",
267 request.object(),
268 rectangle.orientation().name()
269 )));
270 }
271 distances.sort_by(|a, b| (a.side, &a.candidate).cmp(&(b.side, &b.candidate)));
272 let repeated = distances
273 .windows(2)
274 .any(|pair| pair[0].side == pair[1].side && pair[0].candidate == pair[1].candidate);
275 let unrequested = distances.iter().any(|distance| {
276 request
277 .candidates
278 .binary_search(&distance.candidate)
279 .is_err()
280 });
281 if repeated || unrequested {
282 return Err(PlanSpanError::Unavailable(format!(
283 "side distances of {} name a candidate twice or one not requested",
284 request.object()
285 )));
286 }
287 let exact = rectangle.is_exact()
288 && distances.iter().all(|distance| {
289 distance.distance.is_exact() && distance.presence == SidePresence::Sure
290 });
291 if (evidence.exact && !exact) || evidence.locator.trim().is_empty() {
292 return Err(PlanSpanError::InexactEvidence);
293 }
294 Ok(Self {
295 request,
296 rectangle,
297 distances,
298 evidence,
299 })
300 }
301
302 #[must_use]
304 pub fn request(&self) -> &SideDistanceRequest {
305 &self.request
306 }
307
308 #[must_use]
310 pub fn rectangle(&self) -> &PlanRectangle {
311 &self.rectangle
312 }
313
314 #[must_use]
316 pub fn distances(&self) -> &[SideDistance] {
317 &self.distances
318 }
319
320 pub fn beside(&self, side: RectangleSide) -> impl Iterator<Item = &SideDistance> {
322 self.distances
323 .iter()
324 .filter(move |distance| distance.side == side)
325 }
326
327 #[must_use]
329 pub fn evidence(&self) -> &Evidence {
330 &self.evidence
331 }
332}
333
334pub(crate) fn measure(
336 service: &Arc<dyn PlanSpanService>,
337 request: &SideDistanceRequest,
338) -> Result<SideDistances, PlanSpanError> {
339 let answer = service.measure_side_distances(request)?;
340 if answer.request() != request {
341 return Err(PlanSpanError::Unavailable(format!(
342 "side distances for another request were returned for {}",
343 request.object()
344 )));
345 }
346 Ok(answer)
347}
348
349#[cfg(test)]
350mod tests {
351 use std::sync::Arc;
352
353 use axioval_ir::{Evidence, ObjectId, SourceId};
354
355 use super::{RectangleSide, SideDistance, SideDistanceRequest, SideDistances, SidePresence};
356 use crate::plan_span::{
357 PlanLength, PlanRectangle, PlanSpanError, PlanSpanService, PlanSpanServiceHandle,
358 RectangleOrientation,
359 };
360
361 fn id(local: &str) -> ObjectId {
362 ObjectId::new(SourceId::new("cad", "m").unwrap(), local).unwrap()
363 }
364
365 fn evidence(exact: bool) -> Evidence {
366 Evidence {
367 source: SourceId::new("cad", "m").unwrap(),
368 locator: "test".into(),
369 exact,
370 }
371 }
372
373 fn rectangle(orientation: RectangleOrientation) -> PlanRectangle {
374 PlanRectangle::try_new(
375 id("wc"),
376 [1.2, 0.35],
377 0.0,
378 [[1.0, 0.0], [0.0, 1.0]],
379 0.0,
380 [(0.2, 0.2), (0.35, 0.35)],
381 orientation,
382 evidence(orientation == RectangleOrientation::Unique),
383 )
384 .unwrap()
385 }
386
387 fn beside(candidate: &str) -> SideDistance {
388 SideDistance::new(
389 id(candidate),
390 RectangleSide::AgainstSecond,
391 SidePresence::Sure,
392 PlanLength::try_new(0.34, 0.36, evidence(false)).unwrap(),
393 )
394 }
395
396 fn request() -> SideDistanceRequest {
397 SideDistanceRequest::try_new(id("wc"), [id("wall")], 1.0, 0.0).unwrap()
398 }
399
400 #[test]
401 fn requests_refuse_the_object_itself_and_bad_lengths() {
402 assert!(SideDistanceRequest::try_new(id("wc"), [id("wc")], 1.0, 0.0).is_err());
403 assert!(SideDistanceRequest::try_new(id("wc"), [], 0.0, 0.0).is_err());
404 assert!(SideDistanceRequest::try_new(id("wc"), [], 1.0, -0.1).is_err());
405 }
406
407 #[test]
408 fn answers_need_a_unique_rectangle_and_requested_candidates_once() {
409 let unique = rectangle(RectangleOrientation::Unique);
410 assert!(
411 SideDistances::try_new(
412 request(),
413 unique.clone(),
414 vec![beside("wall")],
415 evidence(false)
416 )
417 .is_ok()
418 );
419 assert!(
420 SideDistances::try_new(
421 request(),
422 rectangle(RectangleOrientation::Tied),
423 vec![beside("wall")],
424 evidence(false)
425 )
426 .is_err()
427 );
428 assert!(
429 SideDistances::try_new(
430 request(),
431 unique.clone(),
432 vec![beside("door")],
433 evidence(false)
434 )
435 .is_err()
436 );
437 assert!(
438 SideDistances::try_new(
439 request(),
440 unique.clone(),
441 vec![beside("wall"), beside("wall")],
442 evidence(false)
443 )
444 .is_err()
445 );
446 assert!(
448 SideDistances::try_new(request(), unique, vec![beside("wall")], evidence(true))
449 .is_err()
450 );
451 let out = RectangleSide::AgainstSecond.outward(&rectangle(RectangleOrientation::Unique));
452 assert!(
453 out[0].abs() < 1e-12 && (out[1] + 1.0).abs() < 1e-12,
454 "{out:?}"
455 );
456 }
457
458 struct Farther;
460
461 impl PlanSpanService for Farther {
462 fn measure_diameter(&self, _: &ObjectId) -> Result<PlanLength, PlanSpanError> {
463 Err(PlanSpanError::Unavailable("unused".into()))
464 }
465 fn measure_span(
466 &self,
467 _: &ObjectId,
468 _: &ObjectId,
469 _: crate::plan_span::PlanSpan,
470 ) -> Result<PlanLength, PlanSpanError> {
471 Err(PlanSpanError::Unavailable("unused".into()))
472 }
473 fn measure_side_distances(
474 &self,
475 request: &SideDistanceRequest,
476 ) -> Result<SideDistances, PlanSpanError> {
477 let farther = SideDistanceRequest::try_new(
478 request.object().clone(),
479 request.candidates().to_vec(),
480 request.reach_metres() + 1.0,
481 request.inset_metres(),
482 )?;
483 SideDistances::try_new(
484 farther,
485 rectangle(RectangleOrientation::Unique),
486 Vec::new(),
487 evidence(false),
488 )
489 }
490 }
491
492 #[test]
493 fn the_handle_refuses_an_answer_to_another_request() {
494 let handle = PlanSpanServiceHandle::new(Arc::new(Farther));
495 assert!(matches!(
496 handle.measure_side_distances(&request()),
497 Err(PlanSpanError::Unavailable(message)) if message.contains("another request")
498 ));
499 }
500}