1use axioval_ir::{Evidence, ObjectId, SourceId};
31use thiserror::Error;
32
33use crate::MetricDirection;
34use crate::services::reviewable_exact_evidence;
35
36const TOLERANCE: f64 = 1.0e-9;
39
40pub type PlanRing = Vec<[f64; 2]>;
43
44#[derive(Clone, Debug, Error, PartialEq, Eq)]
46pub enum DoorLeavesError {
47 #[error("door-leaf service does not cover source `{0}`")]
49 UncoveredSource(SourceId),
50 #[error("object `{0}` is not in the source")]
52 UnknownObject(ObjectId),
53 #[error("`{0}` is not a door or window")]
55 NotADoor(ObjectId),
56 #[error("the source does not state the door's leaves: {0}")]
59 NotStated(String),
60 #[error("the door's leaves cannot be derived exactly: {0}")]
65 Refused(String),
66 #[error("the door cannot be read exactly: {0}")]
68 Unreadable(String),
69 #[error("the object-frame service supplies no door leaves")]
71 Unsupported,
72 #[error("door leaves are invalid: {0}")]
75 InvalidLeaves(String),
76 #[error("door-leaf evidence is not exact and reviewable")]
78 InexactEvidence,
79 #[error("door-leaf service answered for another door")]
81 ResponseRequestMismatch,
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
86pub enum HingeSide {
87 Left,
89 Right,
91}
92
93impl HingeSide {
94 #[must_use]
96 pub fn name(self) -> &'static str {
97 match self {
98 Self::Left => "left",
99 Self::Right => "right",
100 }
101 }
102}
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
106pub enum LeafPosition {
107 Left,
109 Middle,
111 Right,
113 Bottom,
115 Top,
117 NotDefined,
119}
120
121#[derive(Clone, Copy, Debug, PartialEq)]
123pub enum LeafMotion {
124 Swing,
126 DoubleSwing,
128 TiltAndTurn,
131 Tilt,
134 Slide(MetricDirection),
137 RollUp,
139 Removable,
141 Fixed,
143}
144
145impl LeafMotion {
146 #[must_use]
149 pub fn is_hinged(self) -> bool {
150 matches!(self, Self::Swing | Self::DoubleSwing | Self::TiltAndTurn)
151 }
152
153 #[must_use]
156 pub fn tilts(self) -> bool {
157 matches!(self, Self::TiltAndTurn | Self::Tilt)
158 }
159
160 #[must_use]
162 pub fn name(self) -> &'static str {
163 match self {
164 Self::Swing => "swinging",
165 Self::DoubleSwing => "double-acting",
166 Self::TiltAndTurn => "tilt-and-turn",
167 Self::Tilt => "tilting",
168 Self::Slide(_) => "sliding",
169 Self::RollUp => "rolling up",
170 Self::Removable => "removable",
171 Self::Fixed => "fixed",
172 }
173 }
174}
175
176fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
177 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
178}
179
180fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
181 [
182 a[1] * b[2] - a[2] * b[1],
183 a[2] * b[0] - a[0] * b[2],
184 a[0] * b[1] - a[1] * b[0],
185 ]
186}
187
188fn finite(point: [f64; 3]) -> bool {
189 point.iter().all(|c| c.is_finite())
190}
191
192fn invalid(message: impl Into<String>) -> DoorLeavesError {
193 DoorLeavesError::InvalidLeaves(message.into())
194}
195
196#[derive(Clone, Copy, Debug, PartialEq)]
203pub struct SwingSector {
204 hinge: [f64; 3],
205 radius: f64,
206 closed: MetricDirection,
207 open: MetricDirection,
208 double_acting: bool,
209}
210
211impl SwingSector {
212 pub fn try_new(
215 hinge: [f64; 3],
216 radius: f64,
217 closed: MetricDirection,
218 open: MetricDirection,
219 double_acting: bool,
220 ) -> Result<Self, DoorLeavesError> {
221 if !finite(hinge) || !radius.is_finite() || radius <= 0.0 {
222 return Err(invalid(
223 "a sector needs a finite hinge and a positive radius",
224 ));
225 }
226 if dot(closed.components(), open.components()).abs() > TOLERANCE {
227 return Err(invalid(
228 "a sector's closed and open directions are not perpendicular",
229 ));
230 }
231 Ok(Self {
232 hinge,
233 radius,
234 closed,
235 open,
236 double_acting,
237 })
238 }
239
240 #[must_use]
242 pub fn hinge(&self) -> [f64; 3] {
243 self.hinge
244 }
245
246 #[must_use]
248 pub fn radius_metres(&self) -> f64 {
249 self.radius
250 }
251
252 #[must_use]
254 pub fn closed(&self) -> MetricDirection {
255 self.closed
256 }
257
258 #[must_use]
260 pub fn open(&self) -> MetricDirection {
261 self.open
262 }
263
264 #[must_use]
266 pub fn is_double_acting(&self) -> bool {
267 self.double_acting
268 }
269
270 #[must_use]
272 pub fn sweep(&self) -> f64 {
273 if self.double_acting {
274 std::f64::consts::PI
275 } else {
276 std::f64::consts::FRAC_PI_2
277 }
278 }
279
280 #[must_use]
284 pub fn direction_at(&self, angle: f64) -> [f64; 3] {
285 let (closed, open) = (self.closed.components(), self.open.components());
286 let from_closed = if self.double_acting {
289 angle - std::f64::consts::FRAC_PI_2
290 } else {
291 angle
292 };
293 let (sin, cos) = from_closed.sin_cos();
294 [
295 cos * closed[0] + sin * open[0],
296 cos * closed[1] + sin * open[1],
297 cos * closed[2] + sin * open[2],
298 ]
299 }
300
301 #[must_use]
304 pub fn is_horizontal(&self) -> bool {
305 let axis = cross(self.closed.components(), self.open.components());
306 (axis[2].abs() - 1.0).abs() <= 1.0e-6
307 }
308
309 #[must_use]
317 pub fn plan_bounds(&self, segments: usize) -> Option<(PlanRing, PlanRing)> {
318 if segments == 0 || !self.is_horizontal() {
319 return None;
320 }
321 let quarters = if self.double_acting { 2 } else { 1 };
322 let count = segments * quarters;
323 #[allow(clippy::cast_precision_loss)]
324 let step = self.sweep() / count as f64;
325 let [hx, hy, _] = self.hinge;
326 let at = |angle: f64, radius: f64| {
327 let [dx, dy, _] = self.direction_at(angle);
328 [hx + radius * dx, hy + radius * dy]
329 };
330 let mut inner = vec![[hx, hy]];
331 let mut outer = vec![[hx, hy]];
332 let reach = self.radius / (step / 2.0).cos();
334 outer.push(at(0.0, self.radius));
335 for index in 0..=count {
336 #[allow(clippy::cast_precision_loss)]
337 let angle = step * index as f64;
338 inner.push(at(angle, self.radius));
339 if index < count {
340 outer.push(at(angle + step / 2.0, reach));
341 }
342 }
343 outer.push(at(self.sweep(), self.radius));
344 for polygon in [&mut inner, &mut outer] {
345 if signed_area(polygon) < 0.0 {
346 polygon.reverse();
347 }
348 }
349 Some((inner, outer))
350 }
351}
352
353fn signed_area(polygon: &[[f64; 2]]) -> f64 {
355 polygon
356 .iter()
357 .zip(polygon.iter().cycle().skip(1))
358 .map(|(a, b)| a[0] * b[1] - b[0] * a[1])
359 .sum()
360}
361
362#[derive(Clone, Debug, PartialEq)]
364pub struct DoorLeaf {
365 position: LeafPosition,
366 motion: LeafMotion,
367 origin: [f64; 3],
368 along: MetricDirection,
369 opening: MetricDirection,
370 up: MetricDirection,
371 width: f64,
372 depth: Option<f64>,
373 hinge_side: Option<HingeSide>,
374 swing: Option<SwingSector>,
375 height: Option<f64>,
376 tilt: Option<SwingSector>,
377}
378
379impl DoorLeaf {
380 #[allow(clippy::too_many_arguments)]
392 pub fn try_new(
393 position: LeafPosition,
394 motion: LeafMotion,
395 origin: [f64; 3],
396 along: MetricDirection,
397 opening: MetricDirection,
398 up: MetricDirection,
399 width: f64,
400 depth: Option<f64>,
401 hinge_side: Option<HingeSide>,
402 swing: Option<SwingSector>,
403 ) -> Result<Self, DoorLeavesError> {
404 let (x, y, z) = (along.components(), opening.components(), up.components());
405 if dot(x, y).abs() > TOLERANCE || dot(y, z).abs() > TOLERANCE || dot(z, x).abs() > TOLERANCE
406 {
407 return Err(invalid("a leaf's axes are not orthonormal"));
408 }
409 if !finite(origin) || !width.is_finite() || width <= 0.0 {
410 return Err(invalid("a leaf needs a finite origin and a positive width"));
411 }
412 if depth.is_some_and(|depth| !depth.is_finite() || depth <= 0.0) {
413 return Err(invalid("a leaf's stated depth must be positive"));
414 }
415 match (motion, hinge_side, &swing) {
416 (
417 LeafMotion::Swing | LeafMotion::DoubleSwing | LeafMotion::TiltAndTurn,
418 Some(_),
419 Some(sector),
420 ) => {
421 let double = matches!(motion, LeafMotion::DoubleSwing);
422 let same = |a: [f64; 3], b: [f64; 3]| {
423 a.iter().zip(b).all(|(a, b)| (a - b).abs() <= TOLERANCE)
424 };
425 if sector.is_double_acting() != double
426 || (sector.radius_metres() - width).abs() > TOLERANCE * width.max(1.0)
427 || !same(sector.open().components(), y)
428 || dot(sector.closed().components(), x).abs() < 1.0 - TOLERANCE
429 {
430 return Err(invalid(
431 "a leaf's sector does not match its width, motion or axes",
432 ));
433 }
434 }
435 (LeafMotion::Swing | LeafMotion::DoubleSwing | LeafMotion::TiltAndTurn, _, _) => {
436 return Err(invalid("a hinged leaf needs a hinge side and a sector"));
437 }
438 (_, None, None) => {
439 if let LeafMotion::Slide(direction) = motion
440 && dot(direction.components(), x).abs() < 1.0 - TOLERANCE
441 && dot(direction.components(), z).abs() < 1.0 - TOLERANCE
442 {
443 return Err(invalid("a leaf slides along its width or height"));
444 }
445 }
446 _ => return Err(invalid("only a hinged leaf has a hinge side or a sector")),
447 }
448 Ok(Self {
449 position,
450 motion,
451 origin,
452 along,
453 opening,
454 up,
455 width,
456 depth,
457 hinge_side,
458 swing,
459 height: None,
460 tilt: None,
461 })
462 }
463
464 pub fn with_height(mut self, height: f64) -> Result<Self, DoorLeavesError> {
467 if !height.is_finite() || height <= 0.0 {
468 return Err(invalid("a leaf's height must be positive"));
469 }
470 self.height = Some(height);
471 Ok(self)
472 }
473
474 pub fn with_tilt(mut self, tilt: SwingSector) -> Result<Self, DoorLeavesError> {
481 let Some(height) = self.height.filter(|_| self.motion.tilts()) else {
482 return Err(invalid("only a tilting leaf with a height has a tilt"));
483 };
484 let same =
485 |a: [f64; 3], b: [f64; 3]| a.iter().zip(b).all(|(a, b)| (a - b).abs() <= TOLERANCE);
486 if tilt.is_double_acting()
487 || (tilt.radius_metres() - height).abs() > TOLERANCE * height.max(1.0)
488 || !same(tilt.open().components(), self.opening.components())
489 || dot(tilt.closed().components(), self.up.components()).abs() < 1.0 - TOLERANCE
490 {
491 return Err(invalid("a leaf's tilt does not match its height or axes"));
492 }
493 self.tilt = Some(tilt);
494 Ok(self)
495 }
496
497 #[must_use]
499 pub fn position(&self) -> LeafPosition {
500 self.position
501 }
502
503 #[must_use]
505 pub fn motion(&self) -> LeafMotion {
506 self.motion
507 }
508
509 #[must_use]
511 pub fn origin(&self) -> [f64; 3] {
512 self.origin
513 }
514
515 #[must_use]
517 pub fn along(&self) -> MetricDirection {
518 self.along
519 }
520
521 #[must_use]
524 pub fn opening(&self) -> MetricDirection {
525 self.opening
526 }
527
528 #[must_use]
530 pub fn up(&self) -> MetricDirection {
531 self.up
532 }
533
534 #[must_use]
536 pub fn is_mirrored(&self) -> bool {
537 dot(
538 cross(self.along.components(), self.opening.components()),
539 self.up.components(),
540 ) < 0.0
541 }
542
543 #[must_use]
545 pub fn width_metres(&self) -> f64 {
546 self.width
547 }
548
549 #[must_use]
551 pub fn depth_metres(&self) -> Option<f64> {
552 self.depth
553 }
554
555 #[must_use]
557 pub fn closed_edge(&self) -> ([f64; 3], [f64; 3]) {
558 let [dx, dy, dz] = self.along.components();
559 let [ox, oy, oz] = self.origin;
560 (
561 self.origin,
562 [
563 ox + self.width * dx,
564 oy + self.width * dy,
565 oz + self.width * dz,
566 ],
567 )
568 }
569
570 #[must_use]
573 pub fn hinge_side(&self) -> Option<HingeSide> {
574 self.hinge_side
575 }
576
577 #[must_use]
579 pub fn swing(&self) -> Option<&SwingSector> {
580 self.swing.as_ref()
581 }
582
583 #[must_use]
586 pub fn height_metres(&self) -> Option<f64> {
587 self.height
588 }
589
590 #[must_use]
593 pub fn tilt(&self) -> Option<&SwingSector> {
594 self.tilt.as_ref()
595 }
596}
597
598#[derive(Clone, Debug, PartialEq)]
600pub struct DoorLeaves {
601 door: ObjectId,
602 operation: String,
603 overall_width: f64,
604 lining_thickness: Option<f64>,
605 leaves: Vec<DoorLeaf>,
606 evidence: Evidence,
607}
608
609impl DoorLeaves {
610 pub fn try_new(
620 door: ObjectId,
621 operation: impl Into<String>,
622 overall_width: f64,
623 lining_thickness: Option<f64>,
624 leaves: Vec<DoorLeaf>,
625 evidence: Evidence,
626 ) -> Result<Self, DoorLeavesError> {
627 let operation = operation.into();
628 if operation.trim().is_empty() || leaves.is_empty() {
629 return Err(invalid("a door has an operation and at least one leaf"));
630 }
631 if leaves
632 .iter()
633 .any(|leaf| leaf.motion.tilts() && leaf.tilt.is_none())
634 {
635 return Err(invalid("a tilting leaf needs its tilt sector"));
636 }
637 if !overall_width.is_finite() || overall_width <= 0.0 {
638 return Err(invalid("a door's overall width must be positive"));
639 }
640 if lining_thickness.is_some_and(|thickness| !thickness.is_finite() || thickness < 0.0) {
641 return Err(invalid("a lining thickness must not be negative"));
642 }
643 if !reviewable_exact_evidence(&evidence) || evidence.source != door.source {
644 return Err(DoorLeavesError::InexactEvidence);
645 }
646 Ok(Self {
647 door,
648 operation,
649 overall_width,
650 lining_thickness,
651 leaves,
652 evidence,
653 })
654 }
655
656 #[must_use]
658 pub fn door(&self) -> &ObjectId {
659 &self.door
660 }
661
662 #[must_use]
664 pub fn operation(&self) -> &str {
665 &self.operation
666 }
667
668 #[must_use]
670 pub fn overall_width_metres(&self) -> f64 {
671 self.overall_width
672 }
673
674 #[must_use]
677 pub fn lining_thickness_metres(&self) -> Option<f64> {
678 self.lining_thickness
679 }
680
681 #[must_use]
683 pub fn leaves(&self) -> &[DoorLeaf] {
684 &self.leaves
685 }
686
687 pub fn hinged(&self) -> impl Iterator<Item = &DoorLeaf> {
690 self.leaves.iter().filter(|leaf| leaf.motion.is_hinged())
691 }
692
693 #[must_use]
695 pub fn evidence(&self) -> &Evidence {
696 &self.evidence
697 }
698}
699
700pub const SWEPT_FLOOR_REACH_METRES: f64 = 0.5;
705
706#[derive(Clone, Debug, PartialEq)]
716pub struct SweptDoor {
717 door: ObjectId,
718 sectors: Vec<SwingSector>,
719}
720
721impl SweptDoor {
722 pub fn try_new(door: ObjectId, sectors: Vec<SwingSector>) -> Result<Self, DoorLeavesError> {
729 if sectors.is_empty() {
730 return Err(invalid("a swept door sweeps at least one sector"));
731 }
732 if sectors.iter().any(|sector| !sector.is_horizontal()) {
733 return Err(invalid("a swept sector must lie in a horizontal plane"));
734 }
735 Ok(Self { door, sectors })
736 }
737
738 pub fn of(leaves: &DoorLeaves) -> Result<Option<Self>, DoorLeavesError> {
747 let sectors: Vec<SwingSector> = leaves
748 .hinged()
749 .filter_map(DoorLeaf::swing)
750 .copied()
751 .collect();
752 if sectors.is_empty() {
753 return Ok(None);
754 }
755 Self::try_new(leaves.door().clone(), sectors).map(Some)
756 }
757
758 #[must_use]
760 pub fn door(&self) -> &ObjectId {
761 &self.door
762 }
763
764 #[must_use]
766 pub fn sectors(&self) -> &[SwingSector] {
767 &self.sectors
768 }
769
770 pub fn stands_on(&self, floor: f64, top: f64) -> impl Iterator<Item = &SwingSector> {
774 self.sectors.iter().filter(move |sector| {
775 let z = sector.hinge()[2];
776 z >= floor - SWEPT_FLOOR_REACH_METRES && z < top
777 })
778 }
779}
780
781pub(crate) fn tidy_swept(mut swept: Vec<SweptDoor>) -> Option<Vec<SweptDoor>> {
784 swept.sort_by(|a, b| a.door.cmp(&b.door));
785 swept.dedup();
786 if swept.windows(2).any(|pair| pair[0].door == pair[1].door) {
787 return None;
788 }
789 Some(swept)
790}
791
792#[cfg(test)]
793mod tests {
794 use super::*;
795
796 fn source() -> SourceId {
797 SourceId::new("cad", "m").unwrap()
798 }
799
800 fn door() -> ObjectId {
801 ObjectId::new(source(), "d").unwrap()
802 }
803
804 fn direction(vector: [f64; 3]) -> MetricDirection {
805 MetricDirection::try_new(vector).unwrap()
806 }
807
808 fn left_hinged(double: bool) -> DoorLeaf {
811 let sector = SwingSector::try_new(
812 [0.0; 3],
813 0.9,
814 direction([1.0, 0.0, 0.0]),
815 direction([0.0, 1.0, 0.0]),
816 double,
817 )
818 .unwrap();
819 DoorLeaf::try_new(
820 LeafPosition::NotDefined,
821 if double {
822 LeafMotion::DoubleSwing
823 } else {
824 LeafMotion::Swing
825 },
826 [0.0; 3],
827 direction([1.0, 0.0, 0.0]),
828 direction([0.0, 1.0, 0.0]),
829 direction([0.0, 0.0, 1.0]),
830 0.9,
831 Some(0.04),
832 Some(HingeSide::Left),
833 Some(sector),
834 )
835 .unwrap()
836 }
837
838 #[test]
839 fn a_sector_brackets_the_quarter_disc() {
840 let leaf = left_hinged(false);
841 let sector = leaf.swing().unwrap();
842 assert!(sector.is_horizontal());
843 let (inner, outer) = sector.plan_bounds(16).unwrap();
844 assert!(signed_area(&inner) > 0.0 && signed_area(&outer) > 0.0);
845 let quarter = std::f64::consts::FRAC_PI_4 * 0.81;
846 let area = |polygon: &[[f64; 2]]| signed_area(polygon) / 2.0;
847 assert!(area(&inner) < quarter && quarter < area(&outer));
848 assert!(area(&outer) - area(&inner) < 1e-2);
849 for [x, y] in inner {
851 assert!(x >= -1e-12 && y >= -1e-12 && x.hypot(y) <= 0.9 + 1e-12);
852 }
853 let open = sector.direction_at(sector.sweep());
854 assert!((open[1] - 1.0).abs() < 1e-12);
855 }
856
857 #[test]
858 fn a_double_acting_sector_is_a_half_disc() {
859 let sector = *left_hinged(true).swing().unwrap();
860 assert!((sector.sweep() - std::f64::consts::PI).abs() < 1e-12);
861 let start = sector.direction_at(0.0);
862 assert!((start[1] + 1.0).abs() < 1e-12, "{start:?}");
863 let (inner, _) = sector.plan_bounds(8).unwrap();
864 assert!(inner.iter().any(|[_, y]| *y < -0.5));
865 assert!(inner.iter().any(|[_, y]| *y > 0.5));
866 }
867
868 #[test]
869 fn a_swept_door_takes_its_hinged_leaves_sectors() {
870 let exact = Evidence::exact(source(), "leaves");
871 let leaves = DoorLeaves::try_new(
872 door(),
873 "SINGLE_SWING_LEFT",
874 0.9,
875 None,
876 vec![left_hinged(false)],
877 exact,
878 )
879 .unwrap();
880 let swept = SweptDoor::of(&leaves).unwrap().unwrap();
881 assert_eq!(swept.door(), &door());
882 assert_eq!(swept.sectors().len(), 1);
883 assert_eq!(swept.stands_on(0.4, 2.0).count(), 1);
886 assert_eq!(swept.stands_on(-0.2, 2.0).count(), 1);
887 assert_eq!(swept.stands_on(0.6, 2.6).count(), 0);
888 assert_eq!(swept.stands_on(-3.0, -0.9).count(), 0);
889 assert!(SweptDoor::try_new(door(), Vec::new()).is_err());
890 let vertical = SwingSector::try_new(
891 [0.0; 3],
892 1.0,
893 direction([1.0, 0.0, 0.0]),
894 direction([0.0, 0.0, 1.0]),
895 false,
896 )
897 .unwrap();
898 assert!(SweptDoor::try_new(door(), vec![vertical]).is_err());
899 let other = SweptDoor::try_new(door(), vec![*left_hinged(true).swing().unwrap()]).unwrap();
901 assert_eq!(
902 tidy_swept(vec![swept.clone(), swept.clone()]),
903 Some(vec![swept.clone()])
904 );
905 assert_eq!(tidy_swept(vec![swept, other]), None);
906 }
907
908 #[test]
909 fn a_vertical_sector_has_no_plan_footprint() {
910 let sector = SwingSector::try_new(
911 [0.0; 3],
912 1.0,
913 direction([1.0, 0.0, 0.0]),
914 direction([0.0, 0.0, 1.0]),
915 false,
916 )
917 .unwrap();
918 assert!(!sector.is_horizontal());
919 assert!(sector.plan_bounds(4).is_none());
920 }
921
922 #[test]
923 fn leaves_must_be_consistent() {
924 let leaf = left_hinged(false);
925 assert!(!leaf.is_mirrored());
926 let (_, end) = leaf.closed_edge();
927 assert!((end[0] - 0.9).abs() < 1e-12 && end[1].abs() < 1e-12);
928 let wrong = SwingSector::try_new(
931 [0.0; 3],
932 0.8,
933 direction([1.0, 0.0, 0.0]),
934 direction([0.0, 1.0, 0.0]),
935 false,
936 )
937 .unwrap();
938 let axes = (
939 direction([1.0, 0.0, 0.0]),
940 direction([0.0, 1.0, 0.0]),
941 direction([0.0, 0.0, 1.0]),
942 );
943 let build = |motion, side, sector| {
944 DoorLeaf::try_new(
945 LeafPosition::Left,
946 motion,
947 [0.0; 3],
948 axes.0,
949 axes.1,
950 axes.2,
951 0.9,
952 None,
953 side,
954 sector,
955 )
956 };
957 assert!(build(LeafMotion::Swing, Some(HingeSide::Left), Some(wrong)).is_err());
958 assert!(build(LeafMotion::Swing, Some(HingeSide::Left), None).is_err());
959 assert!(build(LeafMotion::Fixed, Some(HingeSide::Left), None).is_err());
960 assert!(build(LeafMotion::Slide(axes.1), None, None).is_err());
961 assert!(build(LeafMotion::Slide(axes.0), None, None).is_ok());
962 assert!(build(LeafMotion::Slide(axes.2), None, None).is_ok());
963 assert!(build(LeafMotion::TiltAndTurn, Some(HingeSide::Left), None).is_err());
964 let mirrored = DoorLeaf::try_new(
966 LeafPosition::Left,
967 LeafMotion::Fixed,
968 [0.0; 3],
969 axes.0,
970 direction([0.0, -1.0, 0.0]),
971 axes.2,
972 0.9,
973 None,
974 None,
975 None,
976 )
977 .unwrap();
978 assert!(mirrored.is_mirrored());
979 }
980
981 #[test]
982 fn door_leaves_need_exact_evidence_from_the_door_source() {
983 let exact = Evidence::exact(source(), "door-operation:d");
984 assert!(
985 DoorLeaves::try_new(
986 door(),
987 "SINGLE_SWING_LEFT",
988 1.0,
989 Some(0.05),
990 vec![left_hinged(false)],
991 exact.clone()
992 )
993 .is_ok()
994 );
995 let mut approximate = exact.clone();
996 approximate.exact = false;
997 let foreign = Evidence::exact(SourceId::new("cad", "other").unwrap(), "x");
998 for evidence in [approximate, foreign] {
999 assert_eq!(
1000 DoorLeaves::try_new(door(), "X", 1.0, None, vec![left_hinged(false)], evidence),
1001 Err(DoorLeavesError::InexactEvidence)
1002 );
1003 }
1004 assert!(DoorLeaves::try_new(door(), "X", 1.0, None, vec![], exact).is_err());
1005 }
1006 #[test]
1007 fn a_tilting_panel_states_its_height_and_tilt() {
1008 let axes = (
1009 direction([1.0, 0.0, 0.0]),
1010 direction([0.0, 1.0, 0.0]),
1011 direction([0.0, 0.0, 1.0]),
1012 );
1013 let tilt = |radius: f64, closed: [f64; 3]| {
1014 SwingSector::try_new([0.0, 0.0, 0.9], radius, direction(closed), axes.1, false).unwrap()
1015 };
1016 let panel = || {
1017 DoorLeaf::try_new(
1018 LeafPosition::NotDefined,
1019 LeafMotion::Tilt,
1020 [0.0, 0.0, 0.9],
1021 axes.0,
1022 axes.1,
1023 axes.2,
1024 0.8,
1025 None,
1026 None,
1027 None,
1028 )
1029 .unwrap()
1030 };
1031 let tilted = panel()
1032 .with_height(1.2)
1033 .unwrap()
1034 .with_tilt(tilt(1.2, [0.0, 0.0, 1.0]))
1035 .unwrap();
1036 assert_eq!(tilted.height_metres(), Some(1.2));
1037 assert!(!tilted.tilt().unwrap().is_horizontal());
1038 let tall = || panel().with_height(1.2).unwrap();
1041 assert!(tall().with_tilt(tilt(1.0, [0.0, 0.0, 1.0])).is_err());
1042 assert!(tall().with_tilt(tilt(1.2, [1.0, 0.0, 0.0])).is_err());
1043 assert!(panel().with_tilt(tilt(1.2, [0.0, 0.0, 1.0])).is_err());
1044 assert!(panel().with_height(0.0).is_err());
1045 let exact = Evidence::exact(source(), "window-operation:d");
1046 assert!(
1047 DoorLeaves::try_new(
1048 door(),
1049 "SINGLE_PANEL",
1050 0.8,
1051 None,
1052 vec![tall()],
1053 exact.clone()
1054 )
1055 .is_err()
1056 );
1057 assert!(
1058 DoorLeaves::try_new(door(), "SINGLE_PANEL", 0.8, None, vec![tilted], exact).is_ok()
1059 );
1060 }
1061}