Skip to main content

use_circle/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::f64::consts::PI;
5
6use use_bounds::Aabb2;
7use use_coordinate::GeometryError;
8use use_point::Point2;
9
10/// A circle in 2D Euclidean space.
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct Circle {
13    center: Point2,
14    radius: f64,
15}
16
17impl Circle {
18    /// Creates a circle from a center point and a finite, non-negative radius.
19    ///
20    /// # Errors
21    ///
22    /// Returns [`GeometryError::NonFiniteComponent`] when `center` contains a
23    /// non-finite coordinate.
24    ///
25    /// Returns [`GeometryError::NonFiniteRadius`] when `radius` is `NaN` or
26    /// infinite.
27    ///
28    /// Returns [`GeometryError::NegativeRadius`] when `radius` is negative.
29    pub fn try_new(center: Point2, radius: f64) -> Result<Self, GeometryError> {
30        let center = center.validate()?;
31
32        if !radius.is_finite() {
33            return Err(GeometryError::NonFiniteRadius(radius));
34        }
35
36        if radius < 0.0 {
37            return Err(GeometryError::NegativeRadius(radius));
38        }
39
40        Ok(Self { center, radius })
41    }
42
43    /// Returns the circle center.
44    #[must_use]
45    pub const fn center(&self) -> Point2 {
46        self.center
47    }
48
49    /// Returns the circle radius.
50    #[must_use]
51    pub const fn radius(&self) -> f64 {
52        self.radius
53    }
54
55    /// Returns the circle diameter.
56    #[must_use]
57    pub fn diameter(&self) -> f64 {
58        self.radius * 2.0
59    }
60
61    /// Returns the circle area.
62    #[must_use]
63    pub fn area(&self) -> f64 {
64        PI * self.radius * self.radius
65    }
66
67    /// Returns the circle circumference.
68    #[must_use]
69    pub fn circumference(&self) -> f64 {
70        2.0 * PI * self.radius
71    }
72
73    /// Returns `true` when `point` lies inside or on the circle boundary.
74    #[must_use]
75    pub fn contains_point(&self, point: Point2) -> bool {
76        self.center.distance_squared_to(point) <= self.radius * self.radius
77    }
78
79    /// Returns `true` when `point` lies inside the circle expanded by `tolerance`.
80    ///
81    /// # Errors
82    ///
83    /// Returns [`GeometryError::NonFiniteTolerance`] when `tolerance` is `NaN`
84    /// or infinite.
85    ///
86    /// Returns [`GeometryError::NegativeTolerance`] when `tolerance` is negative.
87    pub fn contains_point_with_tolerance(
88        &self,
89        point: Point2,
90        tolerance: f64,
91    ) -> Result<bool, GeometryError> {
92        let tolerance = GeometryError::validate_tolerance(tolerance)?;
93        let radius = self.radius + tolerance;
94
95        Ok(self.center.distance_squared_to(point) <= radius * radius)
96    }
97
98    /// Returns the circle bounding box.
99    #[must_use]
100    pub fn aabb(&self) -> Aabb2 {
101        Aabb2::from_points(
102            Point2::new(self.center.x() - self.radius, self.center.y() - self.radius),
103            Point2::new(self.center.x() + self.radius, self.center.y() + self.radius),
104        )
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use core::f64::consts::PI;
111
112    use super::Circle;
113    use use_coordinate::GeometryError;
114    use use_point::Point2;
115
116    fn approx_eq(left: f64, right: f64) -> bool {
117        (left - right).abs() < 1.0e-10
118    }
119
120    #[test]
121    fn constructs_circle_with_valid_radius() {
122        let circle = Circle::try_new(Point2::new(1.0, 2.0), 3.0).expect("valid circle");
123
124        assert_eq!(circle.center(), Point2::new(1.0, 2.0));
125        assert!(approx_eq(circle.radius(), 3.0));
126    }
127
128    #[test]
129    fn constructs_circle_with_zero_radius() {
130        let circle = Circle::try_new(Point2::origin(), 0.0).expect("zero radius should be valid");
131
132        assert!(approx_eq(circle.radius(), 0.0));
133    }
134
135    #[test]
136    fn rejects_negative_radius() {
137        assert_eq!(
138            Circle::try_new(Point2::origin(), -1.0),
139            Err(GeometryError::NegativeRadius(-1.0))
140        );
141    }
142
143    #[test]
144    fn rejects_nan_radius() {
145        let radius = f64::NAN;
146
147        assert!(matches!(
148            Circle::try_new(Point2::origin(), radius),
149            Err(GeometryError::NonFiniteRadius(value)) if value.is_nan()
150        ));
151    }
152
153    #[test]
154    fn rejects_infinite_radius() {
155        assert_eq!(
156            Circle::try_new(Point2::origin(), f64::INFINITY),
157            Err(GeometryError::NonFiniteRadius(f64::INFINITY))
158        );
159    }
160
161    #[test]
162    fn rejects_non_finite_center_coordinates() {
163        assert!(matches!(
164            Circle::try_new(Point2::new(f64::NAN, 0.0), 1.0),
165            Err(GeometryError::NonFiniteComponent {
166                type_name: "Point2",
167                component: "x",
168                value,
169            }) if value.is_nan()
170        ));
171    }
172
173    #[test]
174    fn computes_area() {
175        let circle = Circle::try_new(Point2::origin(), 3.0).expect("valid circle");
176
177        assert!(approx_eq(circle.area(), PI * 9.0));
178    }
179
180    #[test]
181    fn computes_diameter() {
182        let circle = Circle::try_new(Point2::origin(), 3.0).expect("valid circle");
183
184        assert!(approx_eq(circle.diameter(), 6.0));
185    }
186
187    #[test]
188    fn computes_circumference() {
189        let circle = Circle::try_new(Point2::origin(), 3.0).expect("valid circle");
190
191        assert!(approx_eq(circle.circumference(), 2.0 * PI * 3.0));
192    }
193
194    #[test]
195    fn contains_points() {
196        let circle = Circle::try_new(Point2::origin(), 3.0).expect("valid circle");
197
198        assert!(circle.contains_point(Point2::new(0.0, 0.0)));
199        assert!(circle.contains_point(Point2::new(3.0, 0.0)));
200        assert!(!circle.contains_point(Point2::new(3.1, 0.0)));
201    }
202
203    #[test]
204    fn supports_tolerance_based_containment() {
205        let circle = Circle::try_new(Point2::origin(), 3.0).expect("valid circle");
206
207        assert_eq!(
208            circle.contains_point_with_tolerance(Point2::new(3.1, 0.0), 0.1),
209            Ok(true)
210        );
211        assert_eq!(
212            circle.contains_point_with_tolerance(Point2::new(3.1, 0.0), -0.1),
213            Err(GeometryError::NegativeTolerance(-0.1))
214        );
215    }
216
217    #[test]
218    fn computes_bounds() {
219        let circle = Circle::try_new(Point2::new(2.0, 3.0), 1.5).expect("valid circle");
220
221        assert_eq!(circle.aabb().min(), Point2::new(0.5, 1.5));
222        assert_eq!(circle.aabb().max(), Point2::new(3.5, 4.5));
223    }
224}