Skip to main content

use_geometry/
circle.rs

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