1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
19
20use crate::{Frame, Frame2, Point, Point2, Transform, Transform2};
21
22#[must_use]
32pub fn complete_elliptic_e(m: f64) -> f64 {
33 let m = m.clamp(0.0, 1.0);
34 if m >= 1.0 {
35 return 1.0;
36 }
37 let mut a = 1.0_f64;
38 let mut b = (1.0 - m).sqrt();
39 let mut sum = m * 0.5;
41 let mut power = 1.0_f64;
42 for _ in 0..20 {
45 let c = (a - b) * 0.5;
46 let next_a = f64::midpoint(a, b);
47 b = (a * b).sqrt();
48 a = next_a;
49 sum += power * c * c;
50 power *= 2.0;
51 if c.abs() <= f64::EPSILON * a {
52 break;
53 }
54 }
55 core::f64::consts::FRAC_PI_2 / a * (1.0 - sum)
57}
58
59fn check_positive(name: &str, value: f64, tol: Tolerances) -> OgeomResult<()> {
61 if !value.is_finite() || value <= tol.confusion() {
62 ogeom_bail!(Construction, "{name} {value} must be finite and positive");
63 }
64 Ok(())
65}
66
67#[derive(Debug, Clone, Copy, PartialEq)]
69pub struct Circle {
70 frame: Frame,
71 radius: f64,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq)]
76pub struct Ellipse {
77 frame: Frame,
78 major_radius: f64,
79 minor_radius: f64,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq)]
87pub struct Hyperbola {
88 frame: Frame,
89 major_radius: f64,
90 minor_radius: f64,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq)]
95pub struct Parabola {
96 frame: Frame,
97 focal: f64,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq)]
102pub struct Circle2 {
103 frame: Frame2,
104 radius: f64,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq)]
109pub struct Ellipse2 {
110 frame: Frame2,
111 major_radius: f64,
112 minor_radius: f64,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq)]
117pub struct Hyperbola2 {
118 frame: Frame2,
119 major_radius: f64,
120 minor_radius: f64,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq)]
125pub struct Parabola2 {
126 frame: Frame2,
127 focal: f64,
128}
129
130impl Circle {
131 pub fn new(frame: Frame, radius: f64, tol: Tolerances) -> OgeomResult<Self> {
138 check_positive("circle radius", radius, tol)?;
139 Ok(Self { frame, radius })
140 }
141
142 pub fn through(a: Point, b: Point, c: Point, tol: Tolerances) -> OgeomResult<Self> {
149 let (ab, ac) = (b - a, c - a);
150
151 let z = crate::Direction::from_cross(ab, ac, tol)?;
158
159 let normal = ab.cross(ac);
160 let to_centre = (normal.cross(ab) * ac.square_magnitude()
161 + ac.cross(normal) * ab.square_magnitude())
162 / (2.0 * normal.square_magnitude());
163 let centre = a + to_centre;
164
165 let x = crate::Direction::new(a - centre, tol)?;
168 Self::new(Frame::new(centre, z, x, tol)?, to_centre.magnitude(), tol)
169 }
170
171 #[must_use]
173 pub const fn frame(&self) -> Frame {
174 self.frame
175 }
176
177 #[must_use]
179 pub const fn centre(&self) -> Point {
180 self.frame.origin()
181 }
182
183 #[must_use]
185 pub const fn radius(&self) -> f64 {
186 self.radius
187 }
188
189 #[must_use]
191 pub fn length(&self) -> f64 {
192 core::f64::consts::TAU * self.radius
193 }
194
195 #[must_use]
197 pub fn area(&self) -> f64 {
198 core::f64::consts::PI * self.radius * self.radius
199 }
200
201 #[must_use]
206 pub fn distance_to(&self, p: Point) -> f64 {
207 let local = self.frame.to_local(p);
208 let radial = local.xy().to_vector().magnitude() - self.radius;
212 radial.hypot(local.z)
213 }
214
215 #[must_use]
217 pub fn contains(&self, p: Point, tol: Tolerances) -> bool {
218 self.distance_to(p) <= tol.confusion()
219 }
220
221 pub fn transformed(&self, t: &Transform, tol: Tolerances) -> OgeomResult<Self> {
228 Self::new(
229 t.apply_frame(&self.frame, tol)?,
230 self.radius * t.scale_factor().abs(),
231 tol,
232 )
233 }
234}
235
236impl Ellipse {
237 pub fn new(
246 frame: Frame,
247 major_radius: f64,
248 minor_radius: f64,
249 tol: Tolerances,
250 ) -> OgeomResult<Self> {
251 check_positive("major radius", major_radius, tol)?;
252 check_positive("minor radius", minor_radius, tol)?;
253 if minor_radius > major_radius + tol.confusion() {
254 ogeom_bail!(
255 Construction,
256 "minor radius {minor_radius} exceeds major radius {major_radius}"
257 );
258 }
259 Ok(Self {
260 frame,
261 major_radius,
262 minor_radius,
263 })
264 }
265
266 #[must_use]
268 pub const fn frame(&self) -> Frame {
269 self.frame
270 }
271
272 #[must_use]
274 pub const fn centre(&self) -> Point {
275 self.frame.origin()
276 }
277
278 #[must_use]
280 pub const fn major_radius(&self) -> f64 {
281 self.major_radius
282 }
283
284 #[must_use]
286 pub const fn minor_radius(&self) -> f64 {
287 self.minor_radius
288 }
289
290 #[must_use]
292 pub fn focal_distance(&self) -> f64 {
293 ((self.major_radius - self.minor_radius) * (self.major_radius + self.minor_radius)).sqrt()
297 }
298
299 #[must_use]
301 pub fn eccentricity(&self) -> f64 {
302 self.focal_distance() / self.major_radius
303 }
304
305 #[must_use]
307 pub fn foci(&self) -> (Point, Point) {
308 let offset = self.frame.x() * self.focal_distance();
309 (self.centre() + offset, self.centre() - offset)
310 }
311
312 #[must_use]
314 pub fn area(&self) -> f64 {
315 core::f64::consts::PI * self.major_radius * self.minor_radius
316 }
317
318 #[must_use]
329 pub fn length(&self) -> f64 {
330 let ratio = self.minor_radius / self.major_radius;
333 let m = (1.0 - ratio) * (1.0 + ratio);
334 4.0 * self.major_radius * complete_elliptic_e(m)
335 }
336
337 pub fn transformed(&self, t: &Transform, tol: Tolerances) -> OgeomResult<Self> {
344 let s = t.scale_factor().abs();
345 Self::new(
346 t.apply_frame(&self.frame, tol)?,
347 self.major_radius * s,
348 self.minor_radius * s,
349 tol,
350 )
351 }
352}
353
354impl Hyperbola {
355 pub fn new(
363 frame: Frame,
364 major_radius: f64,
365 minor_radius: f64,
366 tol: Tolerances,
367 ) -> OgeomResult<Self> {
368 check_positive("major radius", major_radius, tol)?;
369 check_positive("minor radius", minor_radius, tol)?;
370 Ok(Self {
371 frame,
372 major_radius,
373 minor_radius,
374 })
375 }
376
377 #[must_use]
379 pub const fn frame(&self) -> Frame {
380 self.frame
381 }
382
383 #[must_use]
385 pub const fn centre(&self) -> Point {
386 self.frame.origin()
387 }
388
389 #[must_use]
391 pub const fn major_radius(&self) -> f64 {
392 self.major_radius
393 }
394
395 #[must_use]
397 pub const fn minor_radius(&self) -> f64 {
398 self.minor_radius
399 }
400
401 #[must_use]
403 pub fn focal_distance(&self) -> f64 {
404 self.major_radius.hypot(self.minor_radius)
405 }
406
407 #[must_use]
409 pub fn eccentricity(&self) -> f64 {
410 self.focal_distance() / self.major_radius
411 }
412
413 #[must_use]
415 pub fn foci(&self) -> (Point, Point) {
416 let offset = self.frame.x() * self.focal_distance();
417 (self.centre() + offset, self.centre() - offset)
418 }
419
420 #[must_use]
422 pub fn vertex(&self) -> Point {
423 self.centre() + self.frame.x() * self.major_radius
424 }
425
426 pub fn transformed(&self, t: &Transform, tol: Tolerances) -> OgeomResult<Self> {
433 let s = t.scale_factor().abs();
434 Self::new(
435 t.apply_frame(&self.frame, tol)?,
436 self.major_radius * s,
437 self.minor_radius * s,
438 tol,
439 )
440 }
441}
442
443impl Parabola {
444 pub fn new(frame: Frame, focal: f64, tol: Tolerances) -> OgeomResult<Self> {
454 check_positive("focal length", focal, tol)?;
455 Ok(Self { frame, focal })
456 }
457
458 #[must_use]
460 pub const fn frame(&self) -> Frame {
461 self.frame
462 }
463
464 #[must_use]
466 pub const fn apex(&self) -> Point {
467 self.frame.origin()
468 }
469
470 #[must_use]
472 pub const fn focal(&self) -> f64 {
473 self.focal
474 }
475
476 #[must_use]
478 pub fn focus(&self) -> Point {
479 self.apex() + self.frame.x() * self.focal
480 }
481
482 #[must_use]
484 pub const fn eccentricity(&self) -> f64 {
485 1.0
486 }
487
488 pub fn transformed(&self, t: &Transform, tol: Tolerances) -> OgeomResult<Self> {
495 Self::new(
496 t.apply_frame(&self.frame, tol)?,
497 self.focal * t.scale_factor().abs(),
498 tol,
499 )
500 }
501}
502
503impl Circle2 {
504 pub fn new(frame: Frame2, radius: f64, tol: Tolerances) -> OgeomResult<Self> {
511 check_positive("circle radius", radius, tol)?;
512 Ok(Self { frame, radius })
513 }
514
515 pub fn centred(centre: Point2, radius: f64, tol: Tolerances) -> OgeomResult<Self> {
521 Self::new(Frame2::new(centre, crate::Direction2::X), radius, tol)
522 }
523
524 #[must_use]
526 pub const fn frame(&self) -> Frame2 {
527 self.frame
528 }
529
530 #[must_use]
532 pub const fn centre(&self) -> Point2 {
533 self.frame.origin()
534 }
535
536 #[must_use]
538 pub const fn radius(&self) -> f64 {
539 self.radius
540 }
541
542 #[must_use]
544 pub fn length(&self) -> f64 {
545 core::f64::consts::TAU * self.radius
546 }
547
548 #[must_use]
550 pub fn area(&self) -> f64 {
551 core::f64::consts::PI * self.radius * self.radius
552 }
553
554 #[must_use]
556 pub fn signed_distance_to(&self, p: Point2) -> f64 {
557 self.centre().distance(p) - self.radius
558 }
559
560 #[must_use]
562 pub fn contains(&self, p: Point2, tol: Tolerances) -> bool {
563 self.signed_distance_to(p).abs() <= tol.confusion()
564 }
565
566 #[must_use]
568 pub fn encloses(&self, p: Point2, tol: Tolerances) -> bool {
569 self.signed_distance_to(p) < -tol.confusion()
570 }
571
572 pub fn transformed(&self, t: &Transform2, tol: Tolerances) -> OgeomResult<Self> {
584 Self::new(
585 t.apply_frame(&self.frame, tol)?,
586 self.radius * t.scale_factor().abs(),
587 tol,
588 )
589 }
590}
591
592impl Ellipse2 {
593 pub fn new(
600 frame: Frame2,
601 major_radius: f64,
602 minor_radius: f64,
603 tol: Tolerances,
604 ) -> OgeomResult<Self> {
605 check_positive("major radius", major_radius, tol)?;
606 check_positive("minor radius", minor_radius, tol)?;
607 if minor_radius > major_radius + tol.confusion() {
608 ogeom_bail!(
609 Construction,
610 "minor radius {minor_radius} exceeds major radius {major_radius}"
611 );
612 }
613 Ok(Self {
614 frame,
615 major_radius,
616 minor_radius,
617 })
618 }
619
620 #[must_use]
622 pub const fn frame(&self) -> Frame2 {
623 self.frame
624 }
625
626 #[must_use]
628 pub const fn centre(&self) -> Point2 {
629 self.frame.origin()
630 }
631
632 #[must_use]
634 pub const fn major_radius(&self) -> f64 {
635 self.major_radius
636 }
637
638 #[must_use]
640 pub const fn minor_radius(&self) -> f64 {
641 self.minor_radius
642 }
643
644 #[must_use]
646 pub fn focal_distance(&self) -> f64 {
647 ((self.major_radius - self.minor_radius) * (self.major_radius + self.minor_radius)).sqrt()
648 }
649
650 #[must_use]
652 pub fn eccentricity(&self) -> f64 {
653 self.focal_distance() / self.major_radius
654 }
655
656 #[must_use]
658 pub fn area(&self) -> f64 {
659 core::f64::consts::PI * self.major_radius * self.minor_radius
660 }
661
662 pub fn transformed(&self, t: &Transform2, tol: Tolerances) -> OgeomResult<Self> {
668 let s = t.scale_factor().abs();
669 Self::new(
670 t.apply_frame(&self.frame, tol)?,
671 self.major_radius * s,
672 self.minor_radius * s,
673 tol,
674 )
675 }
676}
677
678impl Hyperbola2 {
679 pub fn new(
686 frame: Frame2,
687 major_radius: f64,
688 minor_radius: f64,
689 tol: Tolerances,
690 ) -> OgeomResult<Self> {
691 check_positive("major radius", major_radius, tol)?;
692 check_positive("minor radius", minor_radius, tol)?;
693 Ok(Self {
694 frame,
695 major_radius,
696 minor_radius,
697 })
698 }
699
700 #[must_use]
702 pub const fn frame(&self) -> Frame2 {
703 self.frame
704 }
705
706 #[must_use]
708 pub const fn centre(&self) -> Point2 {
709 self.frame.origin()
710 }
711
712 #[must_use]
714 pub const fn major_radius(&self) -> f64 {
715 self.major_radius
716 }
717
718 #[must_use]
720 pub const fn minor_radius(&self) -> f64 {
721 self.minor_radius
722 }
723
724 #[must_use]
726 pub fn eccentricity(&self) -> f64 {
727 self.major_radius.hypot(self.minor_radius) / self.major_radius
728 }
729}
730
731impl Parabola2 {
732 pub fn new(frame: Frame2, focal: f64, tol: Tolerances) -> OgeomResult<Self> {
739 check_positive("focal length", focal, tol)?;
740 Ok(Self { frame, focal })
741 }
742
743 #[must_use]
745 pub const fn frame(&self) -> Frame2 {
746 self.frame
747 }
748
749 #[must_use]
751 pub const fn apex(&self) -> Point2 {
752 self.frame.origin()
753 }
754
755 #[must_use]
757 pub const fn focal(&self) -> f64 {
758 self.focal
759 }
760
761 #[must_use]
763 pub fn focus(&self) -> Point2 {
764 self.apex() + self.frame.x() * self.focal
765 }
766}
767
768#[cfg(test)]
769#[allow(clippy::unwrap_used)]
770mod tests {
771 use super::*;
772 use crate::{Axis, Direction, Vector};
773 use approx::assert_relative_eq;
774
775 const T: Tolerances = Tolerances::millimetres();
776
777 #[test]
778 fn degenerate_sizes_are_refused() {
779 let f = Frame::WORLD;
780 assert!(Circle::new(f, 0.0, T).is_err());
781 assert!(Circle::new(f, -1.0, T).is_err());
782 assert!(Circle::new(f, f64::NAN, T).is_err());
783 assert!(Circle::new(f, f64::INFINITY, T).is_err());
784 assert!(Parabola::new(f, 0.0, T).is_err());
785 assert!(Ellipse::new(f, 1.0, 2.0, T).is_err());
788 assert!(Ellipse::new(f, 2.0, 1.0, T).is_ok());
789 assert!(Hyperbola::new(f, 1.0, 2.0, T).is_ok());
791 }
792
793 #[test]
794 fn circle_measurements() {
795 let c = Circle::new(Frame::WORLD, 2.0, T).unwrap();
796 assert_relative_eq!(c.length(), core::f64::consts::TAU * 2.0);
797 assert_relative_eq!(c.area(), core::f64::consts::PI * 4.0);
798 assert_eq!(c.centre(), Point::ORIGIN);
799 }
800
801 #[test]
802 fn circle_distance_is_to_the_curve_not_the_disc() {
803 let c = Circle::new(Frame::WORLD, 5.0, T).unwrap();
804 assert_relative_eq!(c.distance_to(Point::ORIGIN), 5.0);
806 assert_relative_eq!(c.distance_to(Point::new(5.0, 0.0, 0.0)), 0.0);
807 assert_relative_eq!(c.distance_to(Point::new(7.0, 0.0, 0.0)), 2.0);
808 assert_relative_eq!(c.distance_to(Point::new(3.0, 0.0, 0.0)), 2.0);
809 assert_relative_eq!(c.distance_to(Point::new(5.0, 0.0, 3.0)), 3.0);
811 assert_relative_eq!(c.distance_to(Point::new(1.0, 0.0, 3.0)), 5.0);
812 assert!(c.contains(Point::new(0.0, 5.0, 0.0), T));
813 }
814
815 #[test]
816 fn circle_through_three_points() {
817 let c = Circle::through(
819 Point::new(1.0, 0.0, 0.0),
820 Point::new(0.0, 1.0, 0.0),
821 Point::new(-1.0, 0.0, 0.0),
822 T,
823 )
824 .unwrap();
825 assert_relative_eq!(c.radius(), 1.0, epsilon = 1e-12);
826 assert!(c.centre().is_equal(Point::ORIGIN, T));
827 assert!(c.contains(Point::new(0.0, -1.0, 0.0), T));
828 }
829
830 #[test]
831 fn circle_through_three_points_works_off_axis_and_at_scale() {
832 for scale in [1e-3_f64, 1.0, 1e3] {
833 let a = Point::new(3.0 * scale, scale, 2.0 * scale);
834 let b = Point::new(scale, 5.0 * scale, -scale);
835 let c = Point::new(-2.0 * scale, 0.0, 4.0 * scale);
836 let circle = Circle::through(a, b, c, T).unwrap();
837 for p in [a, b, c] {
839 assert_relative_eq!(
840 circle.centre().distance(p),
841 circle.radius(),
842 max_relative = 1e-10
843 );
844 }
845 }
846 }
847
848 #[test]
849 fn collinear_points_admit_no_circle() {
850 let a = Point::ORIGIN;
851 let b = Point::new(1.0, 1.0, 1.0);
852 let c = Point::new(2.0, 2.0, 2.0);
853 assert!(Circle::through(a, b, c, T).is_err());
854 assert!(Circle::through(a, b, b, T).is_err());
855 assert!(Circle::through(a, a, a, T).is_err());
856 }
857
858 #[test]
859 fn collinearity_test_is_scale_free() {
860 let s = 1e-6;
864 let circle = Circle::through(
865 Point::new(s, 0.0, 0.0),
866 Point::new(0.0, s, 0.0),
867 Point::new(-s, 0.0, 0.0),
868 T,
869 )
870 .unwrap();
871 assert_relative_eq!(circle.radius(), s, max_relative = 1e-9);
872 }
873
874 #[test]
875 fn ellipse_focal_geometry() {
876 let e = Ellipse::new(Frame::WORLD, 5.0, 3.0, T).unwrap();
877 assert_relative_eq!(e.focal_distance(), 4.0, epsilon = 1e-12);
878 assert_relative_eq!(e.eccentricity(), 0.8, epsilon = 1e-12);
879 let (f1, f2) = e.foci();
880 assert!(f1.is_equal(Point::new(4.0, 0.0, 0.0), T));
881 assert!(f2.is_equal(Point::new(-4.0, 0.0, 0.0), T));
882 let on_curve = Point::new(0.0, 3.0, 0.0);
884 assert_relative_eq!(
885 on_curve.distance(f1) + on_curve.distance(f2),
886 10.0,
887 epsilon = 1e-12
888 );
889 }
890
891 #[test]
892 fn nearly_circular_ellipse_keeps_focal_precision() {
893 let a = 1.0;
896 let b = 1.0 - 1e-12;
897 let e = Ellipse::new(Frame::WORLD, a, b, T).unwrap();
898 let expected = ((a - b) * (a + b)).sqrt();
899 assert_relative_eq!(e.focal_distance(), expected, max_relative = 1e-12);
900 assert!(e.focal_distance() > 0.0, "must not collapse to zero");
901 }
902
903 #[test]
904 fn ellipse_length_matches_the_circle_it_degenerates_to() {
905 let r = 3.0;
906 let e = Ellipse::new(Frame::WORLD, r, r, T).unwrap();
907 assert_relative_eq!(e.length(), core::f64::consts::TAU * r, max_relative = 1e-12);
908 assert_relative_eq!(e.area(), core::f64::consts::PI * r * r);
909 assert_relative_eq!(e.eccentricity(), 0.0);
910 }
911
912 #[test]
913 fn ellipse_length_is_accurate_at_every_eccentricity() {
914 let cases = [
917 (10.0, 1.0, 40.639_741_801_0),
918 (2.0, 1.0, 9.688_448_220_5),
919 (1.0, 1.0, core::f64::consts::TAU),
920 (100.0, 1.0, 400.109_832_972_2),
921 ];
922 for (a, b, expected) in cases {
923 let e = Ellipse::new(Frame::WORLD, a, b, T).unwrap();
924 assert_relative_eq!(e.length(), expected, max_relative = 1e-9);
927 }
928 }
929
930 #[test]
931 fn complete_elliptic_integral_endpoints() {
932 assert_relative_eq!(complete_elliptic_e(0.0), core::f64::consts::FRAC_PI_2);
933 assert_relative_eq!(complete_elliptic_e(1.0), 1.0);
934 let mut previous = complete_elliptic_e(0.0);
936 for i in 1..=20 {
937 let e = complete_elliptic_e(f64::from(i) / 20.0);
938 assert!(
939 e < previous,
940 "not decreasing at m = {}",
941 f64::from(i) / 20.0
942 );
943 previous = e;
944 }
945 }
946
947 #[test]
948 fn hyperbola_focal_geometry() {
949 let h = Hyperbola::new(Frame::WORLD, 3.0, 4.0, T).unwrap();
950 assert_relative_eq!(h.focal_distance(), 5.0, epsilon = 1e-12);
951 assert_relative_eq!(h.eccentricity(), 5.0 / 3.0, epsilon = 1e-12);
952 assert!(h.eccentricity() > 1.0);
953 assert!(h.vertex().is_equal(Point::new(3.0, 0.0, 0.0), T));
954 }
955
956 #[test]
957 fn parabola_focal_geometry() {
958 let p = Parabola::new(Frame::WORLD, 2.0, T).unwrap();
959 assert!(p.apex().is_equal(Point::ORIGIN, T));
960 assert!(p.focus().is_equal(Point::new(2.0, 0.0, 0.0), T));
961 assert_relative_eq!(p.eccentricity(), 1.0);
962 }
963
964 #[test]
965 fn transforms_scale_sizes_and_move_frames() {
966 let c = Circle::new(Frame::WORLD, 2.0, T).unwrap();
967 let t = Transform::scaling(Point::ORIGIN, 3.0, T).unwrap()
968 * Transform::translation(Vector::new(1.0, 0.0, 0.0));
969 let moved = c.transformed(&t, T).unwrap();
970 assert_relative_eq!(moved.radius(), 6.0, epsilon = 1e-12);
971 assert!(moved.centre().is_equal(Point::new(3.0, 0.0, 0.0), T));
972
973 let r = Transform::rotation(Axis::X, core::f64::consts::FRAC_PI_2);
975 let rotated = c.transformed(&r, T).unwrap();
976 assert_relative_eq!(rotated.radius(), 2.0, epsilon = 1e-12);
977 assert!(rotated.frame().z().is_equal(-Direction::Y, T));
978 }
979
980 #[test]
981 fn mirroring_a_circle_keeps_a_positive_radius() {
982 let c = Circle::new(Frame::WORLD, 2.0, T).unwrap();
985 let m = Transform::point_mirror(Point::ORIGIN);
986 let mirrored = c.transformed(&m, T).unwrap();
987 assert_relative_eq!(mirrored.radius(), 2.0);
988 }
989
990 #[test]
991 fn circle2_signed_distance_and_containment() {
992 let c = Circle2::centred(Point2::new(1.0, 1.0), 2.0, T).unwrap();
993 assert_relative_eq!(c.signed_distance_to(Point2::new(1.0, 1.0)), -2.0);
994 assert_relative_eq!(c.signed_distance_to(Point2::new(3.0, 1.0)), 0.0);
995 assert_relative_eq!(c.signed_distance_to(Point2::new(5.0, 1.0)), 2.0);
996 assert!(c.encloses(Point2::new(1.0, 1.0), T));
997 assert!(
998 !c.encloses(Point2::new(3.0, 1.0), T),
999 "on the boundary is not inside"
1000 );
1001 assert!(c.contains(Point2::new(3.0, 1.0), T));
1002 }
1003
1004 #[test]
1005 fn a_planar_circles_frame_survives_a_transform() {
1006 let f = Frame2::new(Point2::new(1.0, 2.0), crate::Direction2::from_angle(0.9));
1011 let c = Circle2::new(f, 2.0, T).unwrap();
1012 let rot = Transform2::rotation(Point2::ORIGIN, 0.5);
1013 let moved = c.transformed(&rot, T).unwrap();
1014
1015 assert!(moved.centre().is_equal(rot.apply(c.centre()), T));
1016 let expected = rot.apply_direction(f.x(), T).unwrap();
1017 assert!(moved.frame().x().is_equal(expected, T));
1018
1019 let at_zero = c.centre() + f.x() * 2.0;
1022 let moved_at_zero = moved.centre() + moved.frame().x() * 2.0;
1023 assert!(moved_at_zero.is_equal(rot.apply(at_zero), T));
1024 }
1025
1026 #[test]
1027 fn planar_conics_mirror_their_spatial_counterparts() {
1028 let e = Ellipse2::new(Frame2::WORLD, 5.0, 3.0, T).unwrap();
1029 assert_relative_eq!(e.focal_distance(), 4.0, epsilon = 1e-12);
1030 assert_relative_eq!(e.eccentricity(), 0.8, epsilon = 1e-12);
1031 assert_relative_eq!(e.area(), core::f64::consts::PI * 15.0);
1032
1033 let h = Hyperbola2::new(Frame2::WORLD, 3.0, 4.0, T).unwrap();
1034 assert_relative_eq!(h.eccentricity(), 5.0 / 3.0, epsilon = 1e-12);
1035
1036 let p = Parabola2::new(Frame2::WORLD, 2.0, T).unwrap();
1037 assert!(p.focus().is_equal(Point2::new(2.0, 0.0), T));
1038
1039 assert!(Ellipse2::new(Frame2::WORLD, 1.0, 2.0, T).is_err());
1040 }
1041}