1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
17
18use crate::{Frame, Frame2, Point, Point2, Transform, Transform2};
19
20#[must_use]
30pub fn complete_elliptic_e(m: f64) -> f64 {
31 let m = m.clamp(0.0, 1.0);
32 if m >= 1.0 {
33 return 1.0;
34 }
35 let mut a = 1.0_f64;
36 let mut b = (1.0 - m).sqrt();
37 let mut sum = m * 0.5;
39 let mut power = 1.0_f64;
40 for _ in 0..20 {
43 let c = (a - b) * 0.5;
44 let next_a = f64::midpoint(a, b);
45 b = (a * b).sqrt();
46 a = next_a;
47 sum += power * c * c;
48 power *= 2.0;
49 if c.abs() <= f64::EPSILON * a {
50 break;
51 }
52 }
53 core::f64::consts::FRAC_PI_2 / a * (1.0 - sum)
55}
56
57fn check_positive(name: &str, value: f64, tol: Tolerances) -> OgeomResult<()> {
59 if !value.is_finite() || value <= tol.confusion() {
60 ogeom_bail!(Construction, "{name} {value} must be finite and positive");
61 }
62 Ok(())
63}
64
65#[derive(Debug, Clone, Copy, PartialEq)]
67pub struct Circle {
68 frame: Frame,
69 radius: f64,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq)]
74pub struct Ellipse {
75 frame: Frame,
76 major_radius: f64,
77 minor_radius: f64,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq)]
85pub struct Hyperbola {
86 frame: Frame,
87 major_radius: f64,
88 minor_radius: f64,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq)]
93pub struct Parabola {
94 frame: Frame,
95 focal: f64,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq)]
100pub struct Circle2 {
101 frame: Frame2,
102 radius: f64,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq)]
107pub struct Ellipse2 {
108 frame: Frame2,
109 major_radius: f64,
110 minor_radius: f64,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq)]
115pub struct Hyperbola2 {
116 frame: Frame2,
117 major_radius: f64,
118 minor_radius: f64,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq)]
123pub struct Parabola2 {
124 frame: Frame2,
125 focal: f64,
126}
127
128impl Circle {
129 pub fn new(frame: Frame, radius: f64, tol: Tolerances) -> OgeomResult<Self> {
136 check_positive("circle radius", radius, tol)?;
137 Ok(Self { frame, radius })
138 }
139
140 pub fn through(a: Point, b: Point, c: Point, tol: Tolerances) -> OgeomResult<Self> {
147 let (ab, ac) = (b - a, c - a);
148
149 let z = crate::Direction::from_cross(ab, ac, tol)?;
156
157 let normal = ab.cross(ac);
158 let to_centre = (normal.cross(ab) * ac.square_magnitude()
159 + ac.cross(normal) * ab.square_magnitude())
160 / (2.0 * normal.square_magnitude());
161 let centre = a + to_centre;
162
163 let x = crate::Direction::new(a - centre, tol)?;
166 Self::new(Frame::new(centre, z, x, tol)?, to_centre.magnitude(), tol)
167 }
168
169 #[must_use]
171 pub const fn frame(&self) -> Frame {
172 self.frame
173 }
174
175 #[must_use]
177 pub const fn centre(&self) -> Point {
178 self.frame.origin()
179 }
180
181 #[must_use]
183 pub const fn radius(&self) -> f64 {
184 self.radius
185 }
186
187 #[must_use]
189 pub fn length(&self) -> f64 {
190 core::f64::consts::TAU * self.radius
191 }
192
193 #[must_use]
195 pub fn area(&self) -> f64 {
196 core::f64::consts::PI * self.radius * self.radius
197 }
198
199 #[must_use]
204 pub fn distance_to(&self, p: Point) -> f64 {
205 let local = self.frame.to_local(p);
206 let radial = local.xy().to_vector().magnitude() - self.radius;
210 radial.hypot(local.z)
211 }
212
213 #[must_use]
215 pub fn contains(&self, p: Point, tol: Tolerances) -> bool {
216 self.distance_to(p) <= tol.confusion()
217 }
218
219 pub fn transformed(&self, t: &Transform, tol: Tolerances) -> OgeomResult<Self> {
226 Self::new(
227 t.apply_frame(&self.frame, tol)?,
228 self.radius * t.scale_factor().abs(),
229 tol,
230 )
231 }
232}
233
234impl Ellipse {
235 pub fn new(
244 frame: Frame,
245 major_radius: f64,
246 minor_radius: f64,
247 tol: Tolerances,
248 ) -> OgeomResult<Self> {
249 check_positive("major radius", major_radius, tol)?;
250 check_positive("minor radius", minor_radius, tol)?;
251 if minor_radius > major_radius + tol.confusion() {
252 ogeom_bail!(
253 Construction,
254 "minor radius {minor_radius} exceeds major radius {major_radius}"
255 );
256 }
257 Ok(Self {
261 frame,
262 major_radius,
263 minor_radius: minor_radius.min(major_radius),
264 })
265 }
266
267 #[must_use]
269 pub const fn frame(&self) -> Frame {
270 self.frame
271 }
272
273 #[must_use]
275 pub const fn centre(&self) -> Point {
276 self.frame.origin()
277 }
278
279 #[must_use]
281 pub const fn major_radius(&self) -> f64 {
282 self.major_radius
283 }
284
285 #[must_use]
287 pub const fn minor_radius(&self) -> f64 {
288 self.minor_radius
289 }
290
291 #[must_use]
293 pub fn focal_distance(&self) -> f64 {
294 ((self.major_radius - self.minor_radius) * (self.major_radius + self.minor_radius)).sqrt()
298 }
299
300 #[must_use]
302 pub fn eccentricity(&self) -> f64 {
303 self.focal_distance() / self.major_radius
304 }
305
306 #[must_use]
308 pub fn foci(&self) -> (Point, Point) {
309 let offset = self.frame.x() * self.focal_distance();
310 (self.centre() + offset, self.centre() - offset)
311 }
312
313 #[must_use]
315 pub fn area(&self) -> f64 {
316 core::f64::consts::PI * self.major_radius * self.minor_radius
317 }
318
319 #[must_use]
330 pub fn length(&self) -> f64 {
331 let ratio = self.minor_radius / self.major_radius;
334 let m = (1.0 - ratio) * (1.0 + ratio);
335 4.0 * self.major_radius * complete_elliptic_e(m)
336 }
337
338 pub fn transformed(&self, t: &Transform, tol: Tolerances) -> OgeomResult<Self> {
345 let s = t.scale_factor().abs();
346 Self::new(
347 t.apply_frame(&self.frame, tol)?,
348 self.major_radius * s,
349 self.minor_radius * s,
350 tol,
351 )
352 }
353}
354
355impl Hyperbola {
356 pub fn new(
364 frame: Frame,
365 major_radius: f64,
366 minor_radius: f64,
367 tol: Tolerances,
368 ) -> OgeomResult<Self> {
369 check_positive("major radius", major_radius, tol)?;
370 check_positive("minor radius", minor_radius, tol)?;
371 Ok(Self {
372 frame,
373 major_radius,
374 minor_radius,
375 })
376 }
377
378 #[must_use]
380 pub const fn frame(&self) -> Frame {
381 self.frame
382 }
383
384 #[must_use]
386 pub const fn centre(&self) -> Point {
387 self.frame.origin()
388 }
389
390 #[must_use]
392 pub const fn major_radius(&self) -> f64 {
393 self.major_radius
394 }
395
396 #[must_use]
398 pub const fn minor_radius(&self) -> f64 {
399 self.minor_radius
400 }
401
402 #[must_use]
404 pub fn focal_distance(&self) -> f64 {
405 self.major_radius.hypot(self.minor_radius)
406 }
407
408 #[must_use]
410 pub fn eccentricity(&self) -> f64 {
411 self.focal_distance() / self.major_radius
412 }
413
414 #[must_use]
416 pub fn foci(&self) -> (Point, Point) {
417 let offset = self.frame.x() * self.focal_distance();
418 (self.centre() + offset, self.centre() - offset)
419 }
420
421 #[must_use]
423 pub fn vertex(&self) -> Point {
424 self.centre() + self.frame.x() * self.major_radius
425 }
426
427 pub fn transformed(&self, t: &Transform, tol: Tolerances) -> OgeomResult<Self> {
434 let s = t.scale_factor().abs();
435 Self::new(
436 t.apply_frame(&self.frame, tol)?,
437 self.major_radius * s,
438 self.minor_radius * s,
439 tol,
440 )
441 }
442}
443
444impl Parabola {
445 pub fn new(frame: Frame, focal: f64, tol: Tolerances) -> OgeomResult<Self> {
455 check_positive("focal length", focal, tol)?;
456 Ok(Self { frame, focal })
457 }
458
459 #[must_use]
461 pub const fn frame(&self) -> Frame {
462 self.frame
463 }
464
465 #[must_use]
467 pub const fn apex(&self) -> Point {
468 self.frame.origin()
469 }
470
471 #[must_use]
473 pub const fn focal(&self) -> f64 {
474 self.focal
475 }
476
477 #[must_use]
479 pub fn focus(&self) -> Point {
480 self.apex() + self.frame.x() * self.focal
481 }
482
483 #[must_use]
485 pub const fn eccentricity(&self) -> f64 {
486 1.0
487 }
488
489 pub fn transformed(&self, t: &Transform, tol: Tolerances) -> OgeomResult<Self> {
496 Self::new(
497 t.apply_frame(&self.frame, tol)?,
498 self.focal * t.scale_factor().abs(),
499 tol,
500 )
501 }
502}
503
504impl Circle2 {
505 pub fn new(frame: Frame2, radius: f64, tol: Tolerances) -> OgeomResult<Self> {
512 check_positive("circle radius", radius, tol)?;
513 Ok(Self { frame, radius })
514 }
515
516 pub fn centred(centre: Point2, radius: f64, tol: Tolerances) -> OgeomResult<Self> {
522 Self::new(Frame2::new(centre, crate::Direction2::X), radius, tol)
523 }
524
525 #[must_use]
527 pub const fn frame(&self) -> Frame2 {
528 self.frame
529 }
530
531 #[must_use]
533 pub const fn centre(&self) -> Point2 {
534 self.frame.origin()
535 }
536
537 #[must_use]
539 pub const fn radius(&self) -> f64 {
540 self.radius
541 }
542
543 #[must_use]
545 pub fn length(&self) -> f64 {
546 core::f64::consts::TAU * self.radius
547 }
548
549 #[must_use]
551 pub fn area(&self) -> f64 {
552 core::f64::consts::PI * self.radius * self.radius
553 }
554
555 #[must_use]
557 pub fn signed_distance_to(&self, p: Point2) -> f64 {
558 self.centre().distance(p) - self.radius
559 }
560
561 #[must_use]
563 pub fn contains(&self, p: Point2, tol: Tolerances) -> bool {
564 self.signed_distance_to(p).abs() <= tol.confusion()
565 }
566
567 #[must_use]
569 pub fn encloses(&self, p: Point2, tol: Tolerances) -> bool {
570 self.signed_distance_to(p) < -tol.confusion()
571 }
572
573 pub fn transformed(&self, t: &Transform2, tol: Tolerances) -> OgeomResult<Self> {
585 Self::new(
586 t.apply_frame(&self.frame, tol)?,
587 self.radius * t.scale_factor().abs(),
588 tol,
589 )
590 }
591}
592
593impl Ellipse2 {
594 pub fn new(
601 frame: Frame2,
602 major_radius: f64,
603 minor_radius: f64,
604 tol: Tolerances,
605 ) -> OgeomResult<Self> {
606 check_positive("major radius", major_radius, tol)?;
607 check_positive("minor radius", minor_radius, tol)?;
608 if minor_radius > major_radius + tol.confusion() {
609 ogeom_bail!(
610 Construction,
611 "minor radius {minor_radius} exceeds major radius {major_radius}"
612 );
613 }
614 Ok(Self {
616 frame,
617 major_radius,
618 minor_radius: minor_radius.min(major_radius),
619 })
620 }
621
622 #[must_use]
624 pub const fn frame(&self) -> Frame2 {
625 self.frame
626 }
627
628 #[must_use]
630 pub const fn centre(&self) -> Point2 {
631 self.frame.origin()
632 }
633
634 #[must_use]
636 pub const fn major_radius(&self) -> f64 {
637 self.major_radius
638 }
639
640 #[must_use]
642 pub const fn minor_radius(&self) -> f64 {
643 self.minor_radius
644 }
645
646 #[must_use]
648 pub fn focal_distance(&self) -> f64 {
649 ((self.major_radius - self.minor_radius) * (self.major_radius + self.minor_radius)).sqrt()
650 }
651
652 #[must_use]
654 pub fn eccentricity(&self) -> f64 {
655 self.focal_distance() / self.major_radius
656 }
657
658 #[must_use]
660 pub fn area(&self) -> f64 {
661 core::f64::consts::PI * self.major_radius * self.minor_radius
662 }
663
664 pub fn transformed(&self, t: &Transform2, tol: Tolerances) -> OgeomResult<Self> {
670 let s = t.scale_factor().abs();
671 Self::new(
672 t.apply_frame(&self.frame, tol)?,
673 self.major_radius * s,
674 self.minor_radius * s,
675 tol,
676 )
677 }
678}
679
680impl Hyperbola2 {
681 pub fn new(
688 frame: Frame2,
689 major_radius: f64,
690 minor_radius: f64,
691 tol: Tolerances,
692 ) -> OgeomResult<Self> {
693 check_positive("major radius", major_radius, tol)?;
694 check_positive("minor radius", minor_radius, tol)?;
695 Ok(Self {
696 frame,
697 major_radius,
698 minor_radius,
699 })
700 }
701
702 #[must_use]
704 pub const fn frame(&self) -> Frame2 {
705 self.frame
706 }
707
708 #[must_use]
710 pub const fn centre(&self) -> Point2 {
711 self.frame.origin()
712 }
713
714 #[must_use]
716 pub const fn major_radius(&self) -> f64 {
717 self.major_radius
718 }
719
720 #[must_use]
722 pub const fn minor_radius(&self) -> f64 {
723 self.minor_radius
724 }
725
726 #[must_use]
728 pub fn eccentricity(&self) -> f64 {
729 self.major_radius.hypot(self.minor_radius) / self.major_radius
730 }
731
732 pub fn transformed(&self, t: &Transform2, tol: Tolerances) -> OgeomResult<Self> {
738 let s = t.scale_factor().abs();
739 Self::new(
740 t.apply_frame(&self.frame, tol)?,
741 self.major_radius * s,
742 self.minor_radius * s,
743 tol,
744 )
745 }
746}
747
748impl Parabola2 {
749 pub fn new(frame: Frame2, focal: f64, tol: Tolerances) -> OgeomResult<Self> {
756 check_positive("focal length", focal, tol)?;
757 Ok(Self { frame, focal })
758 }
759
760 #[must_use]
762 pub const fn frame(&self) -> Frame2 {
763 self.frame
764 }
765
766 #[must_use]
768 pub const fn apex(&self) -> Point2 {
769 self.frame.origin()
770 }
771
772 #[must_use]
774 pub const fn focal(&self) -> f64 {
775 self.focal
776 }
777
778 #[must_use]
780 pub fn focus(&self) -> Point2 {
781 self.apex() + self.frame.x() * self.focal
782 }
783}
784
785#[cfg(test)]
786#[allow(clippy::unwrap_used)]
787mod tests {
788 use super::*;
789 use crate::{Axis, Direction, Vector};
790 use approx::assert_relative_eq;
791
792 const T: Tolerances = Tolerances::millimetres();
793
794 #[test]
795 fn degenerate_sizes_are_refused() {
796 let f = Frame::WORLD;
797 assert!(Circle::new(f, 0.0, T).is_err());
798 assert!(Circle::new(f, -1.0, T).is_err());
799 assert!(Circle::new(f, f64::NAN, T).is_err());
800 assert!(Circle::new(f, f64::INFINITY, T).is_err());
801 assert!(Parabola::new(f, 0.0, T).is_err());
802 assert!(Ellipse::new(f, 1.0, 2.0, T).is_err());
805 assert!(Ellipse::new(f, 2.0, 1.0, T).is_ok());
806 assert!(Hyperbola::new(f, 1.0, 2.0, T).is_ok());
808 }
809
810 #[test]
811 fn circle_measurements() {
812 let c = Circle::new(Frame::WORLD, 2.0, T).unwrap();
813 assert_relative_eq!(c.length(), core::f64::consts::TAU * 2.0);
814 assert_relative_eq!(c.area(), core::f64::consts::PI * 4.0);
815 assert_eq!(c.centre(), Point::ORIGIN);
816 }
817
818 #[test]
819 fn circle_distance_is_to_the_curve_not_the_disc() {
820 let c = Circle::new(Frame::WORLD, 5.0, T).unwrap();
821 assert_relative_eq!(c.distance_to(Point::ORIGIN), 5.0);
823 assert_relative_eq!(c.distance_to(Point::new(5.0, 0.0, 0.0)), 0.0);
824 assert_relative_eq!(c.distance_to(Point::new(7.0, 0.0, 0.0)), 2.0);
825 assert_relative_eq!(c.distance_to(Point::new(3.0, 0.0, 0.0)), 2.0);
826 assert_relative_eq!(c.distance_to(Point::new(5.0, 0.0, 3.0)), 3.0);
828 assert_relative_eq!(c.distance_to(Point::new(1.0, 0.0, 3.0)), 5.0);
829 assert!(c.contains(Point::new(0.0, 5.0, 0.0), T));
830 }
831
832 #[test]
833 fn circle_through_three_points() {
834 let c = Circle::through(
836 Point::new(1.0, 0.0, 0.0),
837 Point::new(0.0, 1.0, 0.0),
838 Point::new(-1.0, 0.0, 0.0),
839 T,
840 )
841 .unwrap();
842 assert_relative_eq!(c.radius(), 1.0, epsilon = 1e-12);
843 assert!(c.centre().is_equal(Point::ORIGIN, T));
844 assert!(c.contains(Point::new(0.0, -1.0, 0.0), T));
845 }
846
847 #[test]
848 fn circle_through_three_points_works_off_axis_and_at_scale() {
849 for scale in [1e-3_f64, 1.0, 1e3] {
850 let a = Point::new(3.0 * scale, scale, 2.0 * scale);
851 let b = Point::new(scale, 5.0 * scale, -scale);
852 let c = Point::new(-2.0 * scale, 0.0, 4.0 * scale);
853 let circle = Circle::through(a, b, c, T).unwrap();
854 for p in [a, b, c] {
856 assert_relative_eq!(
857 circle.centre().distance(p),
858 circle.radius(),
859 max_relative = 1e-10
860 );
861 }
862 }
863 }
864
865 #[test]
866 fn collinear_points_admit_no_circle() {
867 let a = Point::ORIGIN;
868 let b = Point::new(1.0, 1.0, 1.0);
869 let c = Point::new(2.0, 2.0, 2.0);
870 assert!(Circle::through(a, b, c, T).is_err());
871 assert!(Circle::through(a, b, b, T).is_err());
872 assert!(Circle::through(a, a, a, T).is_err());
873 }
874
875 #[test]
876 fn collinearity_test_is_scale_free() {
877 let s = 1e-6;
881 let circle = Circle::through(
882 Point::new(s, 0.0, 0.0),
883 Point::new(0.0, s, 0.0),
884 Point::new(-s, 0.0, 0.0),
885 T,
886 )
887 .unwrap();
888 assert_relative_eq!(circle.radius(), s, max_relative = 1e-9);
889 }
890
891 #[test]
892 fn ellipse_focal_geometry() {
893 let e = Ellipse::new(Frame::WORLD, 5.0, 3.0, T).unwrap();
894 assert_relative_eq!(e.focal_distance(), 4.0, epsilon = 1e-12);
895 assert_relative_eq!(e.eccentricity(), 0.8, epsilon = 1e-12);
896 let (f1, f2) = e.foci();
897 assert!(f1.is_equal(Point::new(4.0, 0.0, 0.0), T));
898 assert!(f2.is_equal(Point::new(-4.0, 0.0, 0.0), T));
899 let on_curve = Point::new(0.0, 3.0, 0.0);
901 assert_relative_eq!(
902 on_curve.distance(f1) + on_curve.distance(f2),
903 10.0,
904 epsilon = 1e-12
905 );
906 }
907
908 #[test]
909 fn nearly_circular_ellipse_keeps_focal_precision() {
910 let a = 1.0;
913 let b = 1.0 - 1e-12;
914 let e = Ellipse::new(Frame::WORLD, a, b, T).unwrap();
915 let expected = ((a - b) * (a + b)).sqrt();
916 assert_relative_eq!(e.focal_distance(), expected, max_relative = 1e-12);
917 assert!(e.focal_distance() > 0.0, "must not collapse to zero");
918 }
919
920 #[test]
921 fn ellipse_length_matches_the_circle_it_degenerates_to() {
922 let r = 3.0;
923 let e = Ellipse::new(Frame::WORLD, r, r, T).unwrap();
924 assert_relative_eq!(e.length(), core::f64::consts::TAU * r, max_relative = 1e-12);
925 assert_relative_eq!(e.area(), core::f64::consts::PI * r * r);
926 assert_relative_eq!(e.eccentricity(), 0.0);
927 }
928
929 #[test]
930 fn ellipse_length_is_accurate_at_every_eccentricity() {
931 let cases = [
934 (10.0, 1.0, 40.639_741_801_0),
935 (2.0, 1.0, 9.688_448_220_5),
936 (1.0, 1.0, core::f64::consts::TAU),
937 (100.0, 1.0, 400.109_832_972_2),
938 ];
939 for (a, b, expected) in cases {
940 let e = Ellipse::new(Frame::WORLD, a, b, T).unwrap();
941 assert_relative_eq!(e.length(), expected, max_relative = 1e-9);
944 }
945 }
946
947 #[test]
948 fn complete_elliptic_integral_endpoints() {
949 assert_relative_eq!(complete_elliptic_e(0.0), core::f64::consts::FRAC_PI_2);
950 assert_relative_eq!(complete_elliptic_e(1.0), 1.0);
951 let mut previous = complete_elliptic_e(0.0);
953 for i in 1..=20 {
954 let e = complete_elliptic_e(f64::from(i) / 20.0);
955 assert!(
956 e < previous,
957 "not decreasing at m = {}",
958 f64::from(i) / 20.0
959 );
960 previous = e;
961 }
962 }
963
964 #[test]
965 fn hyperbola_focal_geometry() {
966 let h = Hyperbola::new(Frame::WORLD, 3.0, 4.0, T).unwrap();
967 assert_relative_eq!(h.focal_distance(), 5.0, epsilon = 1e-12);
968 assert_relative_eq!(h.eccentricity(), 5.0 / 3.0, epsilon = 1e-12);
969 assert!(h.eccentricity() > 1.0);
970 assert!(h.vertex().is_equal(Point::new(3.0, 0.0, 0.0), T));
971 }
972
973 #[test]
974 fn parabola_focal_geometry() {
975 let p = Parabola::new(Frame::WORLD, 2.0, T).unwrap();
976 assert!(p.apex().is_equal(Point::ORIGIN, T));
977 assert!(p.focus().is_equal(Point::new(2.0, 0.0, 0.0), T));
978 assert_relative_eq!(p.eccentricity(), 1.0);
979 }
980
981 #[test]
982 fn transforms_scale_sizes_and_move_frames() {
983 let c = Circle::new(Frame::WORLD, 2.0, T).unwrap();
984 let t = Transform::scaling(Point::ORIGIN, 3.0, T).unwrap()
985 * Transform::translation(Vector::new(1.0, 0.0, 0.0));
986 let moved = c.transformed(&t, T).unwrap();
987 assert_relative_eq!(moved.radius(), 6.0, epsilon = 1e-12);
988 assert!(moved.centre().is_equal(Point::new(3.0, 0.0, 0.0), T));
989
990 let r = Transform::rotation(Axis::X, core::f64::consts::FRAC_PI_2);
992 let rotated = c.transformed(&r, T).unwrap();
993 assert_relative_eq!(rotated.radius(), 2.0, epsilon = 1e-12);
994 assert!(rotated.frame().z().is_equal(-Direction::Y, T));
995 }
996
997 #[test]
998 fn mirroring_a_circle_keeps_a_positive_radius() {
999 let c = Circle::new(Frame::WORLD, 2.0, T).unwrap();
1002 let m = Transform::point_mirror(Point::ORIGIN);
1003 let mirrored = c.transformed(&m, T).unwrap();
1004 assert_relative_eq!(mirrored.radius(), 2.0);
1005 }
1006
1007 #[test]
1008 fn circle2_signed_distance_and_containment() {
1009 let c = Circle2::centred(Point2::new(1.0, 1.0), 2.0, T).unwrap();
1010 assert_relative_eq!(c.signed_distance_to(Point2::new(1.0, 1.0)), -2.0);
1011 assert_relative_eq!(c.signed_distance_to(Point2::new(3.0, 1.0)), 0.0);
1012 assert_relative_eq!(c.signed_distance_to(Point2::new(5.0, 1.0)), 2.0);
1013 assert!(c.encloses(Point2::new(1.0, 1.0), T));
1014 assert!(
1015 !c.encloses(Point2::new(3.0, 1.0), T),
1016 "on the boundary is not inside"
1017 );
1018 assert!(c.contains(Point2::new(3.0, 1.0), T));
1019 }
1020
1021 #[test]
1022 fn a_planar_circles_frame_survives_a_transform() {
1023 let f = Frame2::new(Point2::new(1.0, 2.0), crate::Direction2::from_angle(0.9));
1028 let c = Circle2::new(f, 2.0, T).unwrap();
1029 let rot = Transform2::rotation(Point2::ORIGIN, 0.5);
1030 let moved = c.transformed(&rot, T).unwrap();
1031
1032 assert!(moved.centre().is_equal(rot.apply(c.centre()), T));
1033 let expected = rot.apply_direction(f.x(), T).unwrap();
1034 assert!(moved.frame().x().is_equal(expected, T));
1035
1036 let at_zero = c.centre() + f.x() * 2.0;
1039 let moved_at_zero = moved.centre() + moved.frame().x() * 2.0;
1040 assert!(moved_at_zero.is_equal(rot.apply(at_zero), T));
1041 }
1042
1043 #[test]
1044 fn planar_conics_mirror_their_spatial_counterparts() {
1045 let e = Ellipse2::new(Frame2::WORLD, 5.0, 3.0, T).unwrap();
1046 assert_relative_eq!(e.focal_distance(), 4.0, epsilon = 1e-12);
1047 assert_relative_eq!(e.eccentricity(), 0.8, epsilon = 1e-12);
1048 assert_relative_eq!(e.area(), core::f64::consts::PI * 15.0);
1049
1050 let h = Hyperbola2::new(Frame2::WORLD, 3.0, 4.0, T).unwrap();
1051 assert_relative_eq!(h.eccentricity(), 5.0 / 3.0, epsilon = 1e-12);
1052
1053 let p = Parabola2::new(Frame2::WORLD, 2.0, T).unwrap();
1054 assert!(p.focus().is_equal(Point2::new(2.0, 0.0), T));
1055
1056 assert!(Ellipse2::new(Frame2::WORLD, 1.0, 2.0, T).is_err());
1057 }
1058
1059 #[test]
1062 fn a_nearly_circular_ellipse_is_a_circle() {
1063 let e = Ellipse::new(Frame::WORLD, 1.0, 1.000_000_05, T).unwrap();
1064 assert!(e.focal_distance() == 0.0 && e.eccentricity() == 0.0);
1065 assert!((e.length() - core::f64::consts::TAU).abs() < 1e-12);
1066 let flat = Ellipse2::new(
1067 Frame2::new(Point2::ORIGIN, crate::Direction2::X),
1068 1.0,
1069 1.000_000_05,
1070 T,
1071 )
1072 .unwrap();
1073 assert!(flat.focal_distance() == 0.0);
1074 }
1075}