1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
15
16use crate::{Axis, Direction, Frame, Point, Transform, Vector};
17
18fn check_positive(name: &str, value: f64, tol: Tolerances) -> OgeomResult<()> {
20 if !value.is_finite() || value <= tol.confusion() {
21 ogeom_bail!(Construction, "{name} {value} must be finite and positive");
22 }
23 Ok(())
24}
25
26#[derive(Debug, Clone, Copy, PartialEq)]
31pub struct Plane {
32 frame: Frame,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq)]
37pub struct Cylinder {
38 frame: Frame,
39 radius: f64,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq)]
48pub struct Cone {
49 frame: Frame,
50 reference_radius: f64,
51 half_angle: f64,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq)]
56pub struct Sphere {
57 frame: Frame,
58 radius: f64,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq)]
63pub struct Torus {
64 frame: Frame,
65 major_radius: f64,
66 minor_radius: f64,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum TorusKind {
76 Ring,
78 Horn,
80 Spindle,
82}
83
84impl Plane {
85 pub const XY: Self = Self {
87 frame: Frame::WORLD,
88 };
89
90 #[must_use]
92 pub const fn new(frame: Frame) -> Self {
93 Self { frame }
94 }
95
96 #[must_use]
99 pub fn through(origin: Point, normal: Direction) -> Self {
100 Self {
101 frame: Frame::about(origin, normal),
102 }
103 }
104
105 pub fn through_points(a: Point, b: Point, c: Point, tol: Tolerances) -> OgeomResult<Self> {
113 let normal = Direction::from_cross(b - a, c - a, tol)?;
114 Ok(Self::through(a, normal))
115 }
116
117 #[must_use]
119 pub const fn frame(&self) -> Frame {
120 self.frame
121 }
122
123 #[must_use]
125 pub const fn origin(&self) -> Point {
126 self.frame.origin()
127 }
128
129 #[must_use]
131 pub const fn normal(&self) -> Direction {
132 self.frame.z()
133 }
134
135 #[must_use]
137 pub fn signed_distance_to(&self, p: Point) -> f64 {
138 self.frame.signed_distance_to_plane(p)
139 }
140
141 #[must_use]
143 pub fn distance_to(&self, p: Point) -> f64 {
144 self.signed_distance_to(p).abs()
145 }
146
147 #[must_use]
149 pub fn project(&self, p: Point) -> Point {
150 p - self.normal() * self.signed_distance_to(p)
151 }
152
153 #[must_use]
155 pub fn contains(&self, p: Point, tol: Tolerances) -> bool {
156 self.distance_to(p) <= tol.confusion()
157 }
158
159 #[must_use]
161 pub const fn reversed(&self) -> Self {
162 Self {
163 frame: self.frame.with_z_reversed(),
164 }
165 }
166
167 pub fn transformed(&self, t: &Transform, tol: Tolerances) -> OgeomResult<Self> {
174 Ok(Self::new(t.apply_frame(&self.frame, tol)?))
175 }
176}
177
178impl Cylinder {
179 pub fn new(frame: Frame, radius: f64, tol: Tolerances) -> OgeomResult<Self> {
186 check_positive("cylinder radius", radius, tol)?;
187 Ok(Self { frame, radius })
188 }
189
190 pub fn about(axis: Axis, radius: f64, tol: Tolerances) -> OgeomResult<Self> {
197 Self::new(Frame::about(axis.location, axis.direction), radius, tol)
198 }
199
200 #[must_use]
202 pub const fn frame(&self) -> Frame {
203 self.frame
204 }
205
206 #[must_use]
208 pub const fn axis(&self) -> Axis {
209 self.frame.axis()
210 }
211
212 #[must_use]
214 pub const fn radius(&self) -> f64 {
215 self.radius
216 }
217
218 #[must_use]
220 pub fn signed_distance_to(&self, p: Point) -> f64 {
221 self.axis().distance_to(p) - self.radius
222 }
223
224 #[must_use]
226 pub fn distance_to(&self, p: Point) -> f64 {
227 self.signed_distance_to(p).abs()
228 }
229
230 #[must_use]
232 pub fn contains(&self, p: Point, tol: Tolerances) -> bool {
233 self.distance_to(p) <= tol.confusion()
234 }
235
236 pub fn normal_at(&self, p: Point, tol: Tolerances) -> OgeomResult<Direction> {
243 let axis = self.axis();
244 Direction::new(p - axis.project(p), tol)
245 }
246
247 #[must_use]
249 pub fn lateral_area(&self, height: f64) -> f64 {
250 core::f64::consts::TAU * self.radius * height.abs()
251 }
252
253 #[must_use]
255 pub fn volume(&self, height: f64) -> f64 {
256 core::f64::consts::PI * self.radius * self.radius * height.abs()
257 }
258
259 pub fn transformed(&self, t: &Transform, tol: Tolerances) -> OgeomResult<Self> {
266 Self::new(
267 t.apply_frame(&self.frame, tol)?,
268 self.radius * t.scale_factor().abs(),
269 tol,
270 )
271 }
272}
273
274impl Cone {
275 pub fn new(
288 frame: Frame,
289 reference_radius: f64,
290 half_angle: f64,
291 tol: Tolerances,
292 ) -> OgeomResult<Self> {
293 if !reference_radius.is_finite() || reference_radius < 0.0 {
294 ogeom_bail!(
295 Construction,
296 "cone reference radius {reference_radius} must be finite and non-negative"
297 );
298 }
299 if !half_angle.is_finite()
300 || half_angle.abs() <= tol.angular()
301 || half_angle.abs() >= core::f64::consts::FRAC_PI_2 - tol.angular()
302 {
303 ogeom_bail!(
304 Construction,
305 "cone half angle {half_angle} must lie strictly between 0 and pi/2"
306 );
307 }
308 Ok(Self {
309 frame,
310 reference_radius,
311 half_angle,
312 })
313 }
314
315 #[must_use]
317 pub const fn frame(&self) -> Frame {
318 self.frame
319 }
320
321 #[must_use]
323 pub const fn axis(&self) -> Axis {
324 self.frame.axis()
325 }
326
327 #[must_use]
329 pub const fn reference_radius(&self) -> f64 {
330 self.reference_radius
331 }
332
333 #[must_use]
336 pub const fn half_angle(&self) -> f64 {
337 self.half_angle
338 }
339
340 #[must_use]
342 pub fn apex(&self) -> Point {
343 self.frame.origin() - self.frame.z() * (self.reference_radius / self.half_angle.tan())
346 }
347
348 #[must_use]
350 pub fn radius_at(&self, z: f64) -> f64 {
351 self.half_angle.tan().mul_add(z, self.reference_radius)
352 }
353
354 #[must_use]
359 pub fn distance_to(&self, p: Point) -> f64 {
360 let local = self.frame.to_local(p);
361 let radial = local.xy().to_vector().magnitude();
362 let apex_z = -self.reference_radius / self.half_angle.tan();
363 let (sin, cos) = self.half_angle.sin_cos();
376 let sin = sin.abs();
377 let height = local.z - apex_z;
378 let apex_distance = radial.hypot(height);
379 let nappe = |along: f64, across: f64| {
380 if along <= 0.0 {
381 apex_distance
382 } else {
383 across.abs()
384 }
385 };
386 let up = nappe(
387 height.mul_add(cos, radial * sin),
388 height.mul_add(sin, -(radial * cos)),
389 );
390 let down = nappe(
391 height.mul_add(-cos, radial * sin),
392 height.mul_add(sin, radial * cos),
393 );
394 up.min(down)
395 }
396
397 #[must_use]
399 pub fn contains(&self, p: Point, tol: Tolerances) -> bool {
400 self.distance_to(p) <= tol.confusion()
401 }
402
403 pub fn transformed(&self, t: &Transform, tol: Tolerances) -> OgeomResult<Self> {
410 Self::new(
415 t.apply_frame(&self.frame, tol)?,
416 self.reference_radius * t.scale_factor().abs(),
417 self.half_angle,
418 tol,
419 )
420 }
421}
422
423impl Sphere {
424 pub fn new(frame: Frame, radius: f64, tol: Tolerances) -> OgeomResult<Self> {
431 check_positive("sphere radius", radius, tol)?;
432 Ok(Self { frame, radius })
433 }
434
435 pub fn centred(centre: Point, radius: f64, tol: Tolerances) -> OgeomResult<Self> {
442 Self::new(Frame::about(centre, Direction::Z), radius, tol)
443 }
444
445 #[must_use]
447 pub const fn frame(&self) -> Frame {
448 self.frame
449 }
450
451 #[must_use]
453 pub const fn centre(&self) -> Point {
454 self.frame.origin()
455 }
456
457 #[must_use]
459 pub const fn radius(&self) -> f64 {
460 self.radius
461 }
462
463 #[must_use]
465 pub fn signed_distance_to(&self, p: Point) -> f64 {
466 self.centre().distance(p) - self.radius
467 }
468
469 #[must_use]
471 pub fn distance_to(&self, p: Point) -> f64 {
472 self.signed_distance_to(p).abs()
473 }
474
475 #[must_use]
477 pub fn contains(&self, p: Point, tol: Tolerances) -> bool {
478 self.distance_to(p) <= tol.confusion()
479 }
480
481 #[must_use]
483 pub fn encloses(&self, p: Point, tol: Tolerances) -> bool {
484 self.signed_distance_to(p) < -tol.confusion()
485 }
486
487 pub fn normal_at(&self, p: Point, tol: Tolerances) -> OgeomResult<Direction> {
494 Direction::new(p - self.centre(), tol)
495 }
496
497 #[must_use]
499 pub fn area(&self) -> f64 {
500 4.0 * core::f64::consts::PI * self.radius * self.radius
501 }
502
503 #[must_use]
505 pub fn volume(&self) -> f64 {
506 4.0 / 3.0 * core::f64::consts::PI * self.radius.powi(3)
507 }
508
509 pub fn transformed(&self, t: &Transform, tol: Tolerances) -> OgeomResult<Self> {
516 Self::new(
517 t.apply_frame(&self.frame, tol)?,
518 self.radius * t.scale_factor().abs(),
519 tol,
520 )
521 }
522}
523
524impl Torus {
525 pub fn new(
537 frame: Frame,
538 major_radius: f64,
539 minor_radius: f64,
540 tol: Tolerances,
541 ) -> OgeomResult<Self> {
542 check_positive("major radius", major_radius, tol)?;
543 check_positive("minor radius", minor_radius, tol)?;
544 Ok(Self {
545 frame,
546 major_radius,
547 minor_radius,
548 })
549 }
550
551 #[must_use]
553 pub const fn frame(&self) -> Frame {
554 self.frame
555 }
556
557 #[must_use]
559 pub const fn centre(&self) -> Point {
560 self.frame.origin()
561 }
562
563 #[must_use]
565 pub const fn axis(&self) -> Axis {
566 self.frame.axis()
567 }
568
569 #[must_use]
571 pub const fn major_radius(&self) -> f64 {
572 self.major_radius
573 }
574
575 #[must_use]
577 pub const fn minor_radius(&self) -> f64 {
578 self.minor_radius
579 }
580
581 #[must_use]
583 pub fn kind(&self, tol: Tolerances) -> TorusKind {
584 let difference = self.major_radius - self.minor_radius;
585 if difference.abs() <= tol.confusion() {
586 TorusKind::Horn
587 } else if difference > 0.0 {
588 TorusKind::Ring
589 } else {
590 TorusKind::Spindle
591 }
592 }
593
594 #[must_use]
596 pub fn self_intersects(&self, tol: Tolerances) -> bool {
597 self.kind(tol) == TorusKind::Spindle
598 }
599
600 #[must_use]
612 pub fn signed_distance_to(&self, p: Point) -> f64 {
613 let local = self.frame.to_local(p);
614 let radial = local.xy().to_vector().magnitude();
615 let unfolded = (radial - self.major_radius).hypot(local.z) - self.minor_radius;
619 let folded = (radial + self.major_radius).hypot(local.z) - self.minor_radius;
620 let magnitude = unfolded.abs().min(folded.abs());
621 if unfolded < 0.0 {
622 -magnitude
623 } else {
624 magnitude
625 }
626 }
627
628 #[must_use]
637 pub fn distance_to(&self, p: Point) -> f64 {
638 let local = self.frame.to_local(p);
639 let radial = local.xy().to_vector().magnitude();
640 let near = (radial - self.major_radius).hypot(local.z) - self.minor_radius;
641 let folded = (radial + self.major_radius).hypot(local.z) - self.minor_radius;
642 near.abs().min(folded.abs())
643 }
644
645 #[must_use]
647 pub fn contains(&self, p: Point, tol: Tolerances) -> bool {
648 self.distance_to(p) <= tol.confusion()
649 }
650
651 #[must_use]
655 pub fn area(&self) -> f64 {
656 4.0 * core::f64::consts::PI * core::f64::consts::PI * self.major_radius * self.minor_radius
657 }
658
659 #[must_use]
661 pub fn volume(&self) -> f64 {
662 2.0 * core::f64::consts::PI
663 * core::f64::consts::PI
664 * self.major_radius
665 * self.minor_radius
666 * self.minor_radius
667 }
668
669 pub fn transformed(&self, t: &Transform, tol: Tolerances) -> OgeomResult<Self> {
676 let s = t.scale_factor().abs();
677 Self::new(
678 t.apply_frame(&self.frame, tol)?,
679 self.major_radius * s,
680 self.minor_radius * s,
681 tol,
682 )
683 }
684}
685
686pub fn plane_normal(a: Vector, b: Vector, tol: Tolerances) -> OgeomResult<Direction> {
693 Direction::from_cross(a, b, tol)
694}
695
696#[cfg(test)]
697#[allow(clippy::unwrap_used)]
698mod tests {
699 use super::*;
700 use approx::assert_relative_eq;
701
702 const T: Tolerances = Tolerances::millimetres();
703
704 #[test]
705 fn plane_distance_is_signed_by_the_normal() {
706 let p = Plane::XY;
707 assert_relative_eq!(p.signed_distance_to(Point::new(1.0, 2.0, 3.0)), 3.0);
708 assert_relative_eq!(p.signed_distance_to(Point::new(1.0, 2.0, -3.0)), -3.0);
709 assert_relative_eq!(p.distance_to(Point::new(0.0, 0.0, -3.0)), 3.0);
710 assert!(p.contains(Point::new(9.0, -9.0, 0.0), T));
711 assert!(
712 p.project(Point::new(1.0, 2.0, 3.0))
713 .is_equal(Point::new(1.0, 2.0, 0.0), T)
714 );
715 }
716
717 #[test]
718 fn reversing_a_plane_flips_the_sign_but_not_the_surface() {
719 let p = Plane::XY;
720 let r = p.reversed();
721 let q = Point::new(0.0, 0.0, 5.0);
722 assert_relative_eq!(r.signed_distance_to(q), -p.signed_distance_to(q));
723 assert_relative_eq!(r.distance_to(q), p.distance_to(q));
724 assert!(r.contains(Point::new(1.0, 1.0, 0.0), T));
725 }
726
727 #[test]
728 fn plane_through_three_points_follows_the_right_hand_rule() {
729 let p = Plane::through_points(
730 Point::ORIGIN,
731 Point::new(1.0, 0.0, 0.0),
732 Point::new(0.0, 1.0, 0.0),
733 T,
734 )
735 .unwrap();
736 assert!(p.normal().is_equal(Direction::Z, T));
737 let q = Plane::through_points(
739 Point::ORIGIN,
740 Point::new(0.0, 1.0, 0.0),
741 Point::new(1.0, 0.0, 0.0),
742 T,
743 )
744 .unwrap();
745 assert!(q.normal().is_equal(-Direction::Z, T));
746 assert!(
747 Plane::through_points(
748 Point::ORIGIN,
749 Point::new(1.0, 1.0, 1.0),
750 Point::new(2.0, 2.0, 2.0),
751 T
752 )
753 .is_err()
754 );
755 }
756
757 #[test]
758 fn plane_through_tiny_triangles_still_works() {
759 let s = 1e-6;
762 let p = Plane::through_points(
763 Point::ORIGIN,
764 Point::new(s, 0.0, 0.0),
765 Point::new(0.0, s, 0.0),
766 T,
767 )
768 .unwrap();
769 assert!(p.normal().is_equal(Direction::Z, T));
770 }
771
772 #[test]
773 fn cylinder_distance_and_normal() {
774 let c = Cylinder::new(Frame::WORLD, 2.0, T).unwrap();
775 assert_relative_eq!(c.signed_distance_to(Point::new(3.0, 0.0, 100.0)), 1.0);
776 assert_relative_eq!(c.signed_distance_to(Point::new(1.0, 0.0, -50.0)), -1.0);
777 assert_relative_eq!(c.signed_distance_to(Point::ORIGIN), -2.0);
778 assert!(c.contains(Point::new(0.0, 2.0, 7.0), T));
779 assert!(
780 c.normal_at(Point::new(3.0, 0.0, 5.0), T)
781 .unwrap()
782 .is_equal(Direction::X, T)
783 );
784 assert!(c.normal_at(Point::new(0.0, 0.0, 5.0), T).is_err());
786 }
787
788 #[test]
789 fn cylinder_measurements() {
790 let c = Cylinder::new(Frame::WORLD, 2.0, T).unwrap();
791 assert_relative_eq!(c.lateral_area(5.0), core::f64::consts::TAU * 10.0);
792 assert_relative_eq!(c.volume(5.0), core::f64::consts::PI * 20.0);
793 assert_relative_eq!(c.volume(-5.0), c.volume(5.0));
795 }
796
797 #[test]
798 fn a_cone_narrowing_up_its_axis_measures_to_both_nappes() {
799 let f = Frame::WORLD;
800 let narrowing = Cone::new(f, 2.0, -0.5, T).unwrap();
803 for z in [-3.0, 0.0, 1.5, 6.0] {
804 let r = narrowing.radius_at(z);
805 let p = f.to_world(Point::new(r.abs() * 0.6, r.abs() * 0.8, z));
806 assert!(
807 narrowing.distance_to(p) < 1e-12,
808 "{z}: {}",
809 narrowing.distance_to(p)
810 );
811 }
812 let beside = f.to_world(Point::new(2.5, 0.0, 0.0));
813 assert!((narrowing.distance_to(beside) - 0.5 * 0.5_f64.cos()).abs() < 1e-12);
814 }
815
816 #[test]
817 fn cone_degenerate_angles_are_refused() {
818 let f = Frame::WORLD;
819 assert!(
820 Cone::new(f, 1.0, 0.0, T).is_err(),
821 "zero angle is a cylinder"
822 );
823 assert!(
824 Cone::new(f, 1.0, core::f64::consts::FRAC_PI_2, T).is_err(),
825 "a right angle is a plane"
826 );
827 assert!(Cone::new(f, 1.0, f64::NAN, T).is_err());
828 assert!(Cone::new(f, -1.0, 0.5, T).is_err());
829 assert!(Cone::new(f, 0.0, 0.5, T).is_ok());
831 assert!(Cone::new(f, 1.0, 0.5, T).is_ok());
832 }
833
834 #[test]
835 fn cone_apex_and_radius_profile() {
836 let quarter = core::f64::consts::FRAC_PI_4;
838 let c = Cone::new(Frame::WORLD, 3.0, quarter, T).unwrap();
839 assert!(c.apex().is_equal(Point::new(0.0, 0.0, -3.0), T));
840 assert_relative_eq!(c.radius_at(0.0), 3.0, epsilon = 1e-12);
841 assert_relative_eq!(c.radius_at(2.0), 5.0, epsilon = 1e-12);
842 assert_relative_eq!(c.radius_at(-3.0), 0.0, epsilon = 1e-12);
843 }
844
845 #[test]
846 fn cone_distance_is_zero_on_the_surface() {
847 let quarter = core::f64::consts::FRAC_PI_4;
848 let c = Cone::new(Frame::WORLD, 3.0, quarter, T).unwrap();
849 for z in [-3.0_f64, -1.0, 0.0, 2.0, 10.0] {
850 let r = c.radius_at(z);
851 for angle in [0.0_f64, 1.0, 2.5] {
852 let p = Point::new(r * angle.cos(), r * angle.sin(), z);
853 assert!(c.contains(p, T), "z = {z}, distance = {}", c.distance_to(p));
854 }
855 }
856 }
857
858 #[test]
859 fn cone_distance_off_the_surface_is_perpendicular() {
860 let quarter = core::f64::consts::FRAC_PI_4;
861 let c = Cone::new(Frame::WORLD, 0.0, quarter, T).unwrap();
862 assert_relative_eq!(
865 c.distance_to(Point::new(1.0, 0.0, 0.0)),
866 core::f64::consts::FRAC_1_SQRT_2,
867 epsilon = 1e-12
868 );
869 assert_relative_eq!(
873 c.distance_to(Point::new(0.0, 0.0, -5.0)),
874 5.0 * core::f64::consts::FRAC_1_SQRT_2,
875 epsilon = 1e-12
876 );
877 assert_relative_eq!(
879 c.distance_to(Point::new(2.0, 0.0, -2.0)),
880 0.0,
881 epsilon = 1e-12
882 );
883 }
884
885 #[test]
886 fn sphere_queries() {
887 let s = Sphere::centred(Point::new(1.0, 2.0, 3.0), 5.0, T).unwrap();
888 assert_relative_eq!(s.signed_distance_to(Point::new(1.0, 2.0, 3.0)), -5.0);
889 assert_relative_eq!(s.signed_distance_to(Point::new(6.0, 2.0, 3.0)), 0.0);
890 assert_relative_eq!(s.signed_distance_to(Point::new(11.0, 2.0, 3.0)), 5.0);
891 assert!(s.encloses(s.centre(), T));
892 assert!(!s.encloses(Point::new(6.0, 2.0, 3.0), T));
893 assert!(s.contains(Point::new(6.0, 2.0, 3.0), T));
894 assert!(
895 s.normal_at(Point::new(6.0, 2.0, 3.0), T)
896 .unwrap()
897 .is_equal(Direction::X, T)
898 );
899 assert!(s.normal_at(s.centre(), T).is_err());
900 }
901
902 #[test]
903 fn sphere_measurements() {
904 let s = Sphere::centred(Point::ORIGIN, 3.0, T).unwrap();
905 assert_relative_eq!(s.area(), 4.0 * core::f64::consts::PI * 9.0);
906 assert_relative_eq!(s.volume(), 4.0 / 3.0 * core::f64::consts::PI * 27.0);
907 }
908
909 #[test]
910 fn torus_kinds_are_distinguished() {
911 let f = Frame::WORLD;
912 assert_eq!(Torus::new(f, 5.0, 1.0, T).unwrap().kind(T), TorusKind::Ring);
913 assert_eq!(Torus::new(f, 5.0, 5.0, T).unwrap().kind(T), TorusKind::Horn);
914 assert_eq!(
915 Torus::new(f, 5.0, 8.0, T).unwrap().kind(T),
916 TorusKind::Spindle
917 );
918 assert!(Torus::new(f, 5.0, 8.0, T).unwrap().self_intersects(T));
919 assert!(!Torus::new(f, 5.0, 1.0, T).unwrap().self_intersects(T));
920 assert!(Torus::new(f, 1.0, 2.0, T).is_ok());
922 assert!(Torus::new(f, 0.0, 1.0, T).is_err());
923 }
924
925 #[test]
926 fn spindle_torus_distance_accounts_for_the_folded_branch() {
927 let t = Torus::new(Frame::WORLD, 1.0, 3.0, T).unwrap();
931 assert_eq!(t.kind(T), TorusKind::Spindle);
932
933 let v = core::f64::consts::PI;
936 let radial = 1.0 + 3.0 * v.cos(); let p = Point::new(radial.abs(), 0.0, 3.0 * v.sin());
938 assert!(t.contains(p, T), "distance was {}", t.distance_to(p));
939
940 let ring = Torus::new(Frame::WORLD, 5.0, 2.0, T).unwrap();
942 for p in [
943 Point::new(7.0, 0.0, 0.0),
944 Point::new(3.0, 0.0, 0.0),
945 Point::new(5.0, 0.0, 2.0),
946 ] {
947 assert_relative_eq!(ring.distance_to(p), 0.0, epsilon = 1e-12);
948 }
949 }
950
951 #[test]
952 fn torus_distance_and_measurements() {
953 let t = Torus::new(Frame::WORLD, 5.0, 2.0, T).unwrap();
954 assert!(t.contains(Point::new(7.0, 0.0, 0.0), T));
956 assert!(t.contains(Point::new(3.0, 0.0, 0.0), T));
957 assert!(t.contains(Point::new(5.0, 0.0, 2.0), T));
958 assert_relative_eq!(t.signed_distance_to(Point::new(5.0, 0.0, 0.0)), -2.0);
960 assert_relative_eq!(t.signed_distance_to(Point::ORIGIN), 3.0);
962
963 let pi2 = core::f64::consts::PI * core::f64::consts::PI;
964 assert_relative_eq!(t.area(), 4.0 * pi2 * 10.0);
965 assert_relative_eq!(t.volume(), 2.0 * pi2 * 5.0 * 4.0);
966 }
967
968 #[test]
969 fn transforms_scale_sizes_and_preserve_shape() {
970 let scale = Transform::scaling(Point::ORIGIN, 3.0, T).unwrap();
971
972 let c = Cylinder::new(Frame::WORLD, 2.0, T).unwrap();
973 assert_relative_eq!(
974 c.transformed(&scale, T).unwrap().radius(),
975 6.0,
976 epsilon = 1e-12
977 );
978
979 let s = Sphere::centred(Point::new(1.0, 0.0, 0.0), 2.0, T).unwrap();
980 let moved = s.transformed(&scale, T).unwrap();
981 assert_relative_eq!(moved.radius(), 6.0, epsilon = 1e-12);
982 assert!(moved.centre().is_equal(Point::new(3.0, 0.0, 0.0), T));
983
984 let t = Torus::new(Frame::WORLD, 5.0, 2.0, T).unwrap();
985 let scaled = t.transformed(&scale, T).unwrap();
986 assert_relative_eq!(scaled.major_radius(), 15.0, epsilon = 1e-12);
987 assert_relative_eq!(scaled.minor_radius(), 6.0, epsilon = 1e-12);
988
989 let cone = Cone::new(Frame::WORLD, 3.0, 0.5, T).unwrap();
992 let big = cone.transformed(&scale, T).unwrap();
993 assert_relative_eq!(big.half_angle(), 0.5);
994 assert_relative_eq!(big.reference_radius(), 9.0, epsilon = 1e-12);
995 }
996
997 #[test]
998 fn transformed_surfaces_still_contain_their_transformed_points() {
999 let t =
1000 Transform::rotation(Axis::X, 0.7) * Transform::translation(Vector::new(1.0, 2.0, 3.0));
1001 let s = Sphere::centred(Point::ORIGIN, 4.0, T).unwrap();
1002 let moved = s.transformed(&t, T).unwrap();
1003 let on_surface = Point::new(4.0, 0.0, 0.0);
1004 assert!(s.contains(on_surface, T));
1005 assert!(moved.contains(t.apply(on_surface), T));
1006 }
1007}
1008
1009#[cfg(test)]
1010#[allow(clippy::unwrap_used, clippy::expect_used)]
1011mod spindle_tests {
1012 use super::*;
1013 use approx::assert_relative_eq;
1014
1015 const T: Tolerances = Tolerances::millimetres();
1016
1017 #[test]
1021 fn a_spindle_signs_its_apple_and_a_ring_is_unchanged() {
1022 let f = Frame::WORLD;
1023 let spindle = Torus::new(f, 2.0, 5.0, T).unwrap();
1024 assert_relative_eq!(
1026 spindle.signed_distance_to(Point::new(0.0, 0.0, 0.0)),
1027 -3.0,
1028 epsilon = 1e-12
1029 );
1030 let cusp_z = (5.0_f64 * 5.0 - 2.0 * 2.0).sqrt();
1032 assert_relative_eq!(
1033 spindle.signed_distance_to(Point::new(0.0, 0.0, cusp_z)),
1034 0.0,
1035 epsilon = 1e-12
1036 );
1037 assert_relative_eq!(
1039 spindle.signed_distance_to(Point::new(7.0, 0.0, 0.0)),
1040 0.0,
1041 epsilon = 1e-12
1042 );
1043 assert_relative_eq!(
1044 spindle.signed_distance_to(Point::new(10.0, 0.0, 0.0)),
1045 3.0,
1046 epsilon = 1e-12
1047 );
1048 for p in [
1050 Point::new(0.0, 0.0, 2.0),
1051 Point::new(1.0, 1.0, 3.0),
1052 Point::new(4.0, -2.0, 1.0),
1053 ] {
1054 assert_relative_eq!(
1055 spindle.signed_distance_to(p).abs(),
1056 spindle.distance_to(p),
1057 epsilon = 1e-12
1058 );
1059 }
1060
1061 let ring = Torus::new(f, 5.0, 1.0, T).unwrap();
1062 for p in [
1063 Point::new(5.0, 0.0, 0.0),
1064 Point::new(6.5, 0.0, 0.0),
1065 Point::new(0.0, 4.2, 0.3),
1066 Point::new(0.0, 0.0, 0.0),
1067 ] {
1068 let local_radial = p.x.hypot(p.y) - 5.0;
1069 let classical = local_radial.hypot(p.z) - 1.0;
1070 assert_relative_eq!(ring.signed_distance_to(p), classical, epsilon = 1e-12);
1071 }
1072 }
1073}