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#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct Circle {
13 center: Point2,
14 radius: f64,
15}
16
17impl Circle {
18 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 #[must_use]
45 pub const fn center(&self) -> Point2 {
46 self.center
47 }
48
49 #[must_use]
51 pub const fn radius(&self) -> f64 {
52 self.radius
53 }
54
55 #[must_use]
57 pub fn diameter(&self) -> f64 {
58 self.radius * 2.0
59 }
60
61 #[must_use]
63 pub fn area(&self) -> f64 {
64 PI * self.radius * self.radius
65 }
66
67 #[must_use]
69 pub fn circumference(&self) -> f64 {
70 2.0 * PI * self.radius
71 }
72
73 #[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 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 #[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}