1use core::f64::consts::PI;
2
3use crate::{aabb::Aabb2, error::GeometryError, point::Point2};
4
5#[derive(Debug, Clone, Copy, PartialEq)]
23pub struct Circle {
24 center: Point2,
25 radius: f64,
26}
27
28impl Circle {
29 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 #[must_use]
71 pub const fn center(&self) -> Point2 {
72 self.center
73 }
74
75 #[must_use]
77 pub const fn radius(&self) -> f64 {
78 self.radius
79 }
80
81 #[must_use]
83 pub fn diameter(&self) -> f64 {
84 self.radius * 2.0
85 }
86
87 #[must_use]
89 pub fn area(&self) -> f64 {
90 PI * self.radius * self.radius
91 }
92
93 #[must_use]
95 pub fn circumference(&self) -> f64 {
96 2.0 * PI * self.radius
97 }
98
99 #[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 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 #[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}