1 | // Copyright 2019 the Kurbo Authors |
2 | // SPDX-License-Identifier: Apache-2.0 OR MIT |
3 | |
4 | //! Implementation of circle shape. |
5 | |
6 | use core::{ |
7 | f64::consts::{FRAC_PI_2, PI}, |
8 | iter, |
9 | ops::{Add, Mul, Sub}, |
10 | }; |
11 | |
12 | use crate::{Affine, Arc, ArcAppendIter, Ellipse, PathEl, Point, Rect, Shape, Vec2}; |
13 | |
14 | #[cfg (not(feature = "std" ))] |
15 | use crate::common::FloatFuncs; |
16 | |
17 | /// A circle. |
18 | #[derive (Clone, Copy, Default, Debug, PartialEq)] |
19 | #[cfg_attr (feature = "schemars" , derive(schemars::JsonSchema))] |
20 | #[cfg_attr (feature = "serde" , derive(serde::Serialize, serde::Deserialize))] |
21 | pub struct Circle { |
22 | /// The center. |
23 | pub center: Point, |
24 | /// The radius. |
25 | pub radius: f64, |
26 | } |
27 | |
28 | impl Circle { |
29 | /// A new circle from center and radius. |
30 | #[inline ] |
31 | pub fn new(center: impl Into<Point>, radius: f64) -> Circle { |
32 | Circle { |
33 | center: center.into(), |
34 | radius, |
35 | } |
36 | } |
37 | |
38 | /// Create a [`CircleSegment`] by cutting out parts of this circle. |
39 | pub fn segment(self, inner_radius: f64, start_angle: f64, sweep_angle: f64) -> CircleSegment { |
40 | CircleSegment { |
41 | center: self.center, |
42 | outer_radius: self.radius, |
43 | inner_radius, |
44 | start_angle, |
45 | sweep_angle, |
46 | } |
47 | } |
48 | |
49 | /// Is this circle [finite]? |
50 | /// |
51 | /// [finite]: f64::is_finite |
52 | #[inline ] |
53 | pub fn is_finite(&self) -> bool { |
54 | self.center.is_finite() && self.radius.is_finite() |
55 | } |
56 | |
57 | /// Is this circle [NaN]? |
58 | /// |
59 | /// [NaN]: f64::is_nan |
60 | #[inline ] |
61 | pub fn is_nan(&self) -> bool { |
62 | self.center.is_nan() || self.radius.is_nan() |
63 | } |
64 | } |
65 | |
66 | impl Add<Vec2> for Circle { |
67 | type Output = Circle; |
68 | |
69 | #[inline ] |
70 | fn add(self, v: Vec2) -> Circle { |
71 | Circle { |
72 | center: self.center + v, |
73 | radius: self.radius, |
74 | } |
75 | } |
76 | } |
77 | |
78 | impl Sub<Vec2> for Circle { |
79 | type Output = Circle; |
80 | |
81 | #[inline ] |
82 | fn sub(self, v: Vec2) -> Circle { |
83 | Circle { |
84 | center: self.center - v, |
85 | radius: self.radius, |
86 | } |
87 | } |
88 | } |
89 | |
90 | impl Mul<Circle> for Affine { |
91 | type Output = Ellipse; |
92 | fn mul(self, other: Circle) -> Self::Output { |
93 | self * Ellipse::from(other) |
94 | } |
95 | } |
96 | |
97 | #[doc (hidden)] |
98 | pub struct CirclePathIter { |
99 | circle: Circle, |
100 | delta_th: f64, |
101 | arm_len: f64, |
102 | ix: usize, |
103 | n: usize, |
104 | } |
105 | |
106 | impl Shape for Circle { |
107 | type PathElementsIter<'iter> = CirclePathIter; |
108 | |
109 | fn path_elements(&self, tolerance: f64) -> CirclePathIter { |
110 | let scaled_err = self.radius.abs() / tolerance; |
111 | let (n, arm_len) = if scaled_err < 1.0 / 1.9608e-4 { |
112 | // Solution from http://spencermortensen.com/articles/bezier-circle/ |
113 | (4, 0.551915024494) |
114 | } else { |
115 | // This is empirically determined to fall within error tolerance. |
116 | let n = (1.1163 * scaled_err).powf(1.0 / 6.0).ceil() as usize; |
117 | // Note: this isn't minimum error, but it is simple and we can easily |
118 | // estimate the error. |
119 | let arm_len = (4.0 / 3.0) * (FRAC_PI_2 / (n as f64)).tan(); |
120 | (n, arm_len) |
121 | }; |
122 | CirclePathIter { |
123 | circle: *self, |
124 | delta_th: 2.0 * PI / (n as f64), |
125 | arm_len, |
126 | ix: 0, |
127 | n, |
128 | } |
129 | } |
130 | |
131 | #[inline ] |
132 | fn area(&self) -> f64 { |
133 | PI * self.radius.powi(2) |
134 | } |
135 | |
136 | #[inline ] |
137 | fn perimeter(&self, _accuracy: f64) -> f64 { |
138 | (2.0 * PI * self.radius).abs() |
139 | } |
140 | |
141 | fn winding(&self, pt: Point) -> i32 { |
142 | if (pt - self.center).hypot2() < self.radius.powi(2) { |
143 | 1 |
144 | } else { |
145 | 0 |
146 | } |
147 | } |
148 | |
149 | #[inline ] |
150 | fn bounding_box(&self) -> Rect { |
151 | let r = self.radius.abs(); |
152 | let (x, y) = self.center.into(); |
153 | Rect::new(x - r, y - r, x + r, y + r) |
154 | } |
155 | |
156 | fn as_circle(&self) -> Option<Circle> { |
157 | Some(*self) |
158 | } |
159 | } |
160 | |
161 | impl Iterator for CirclePathIter { |
162 | type Item = PathEl; |
163 | |
164 | fn next(&mut self) -> Option<PathEl> { |
165 | let a = self.arm_len; |
166 | let r = self.circle.radius; |
167 | let (x, y) = self.circle.center.into(); |
168 | let ix = self.ix; |
169 | self.ix += 1; |
170 | if ix == 0 { |
171 | Some(PathEl::MoveTo(Point::new(x + r, y))) |
172 | } else if ix <= self.n { |
173 | let th1 = self.delta_th * (ix as f64); |
174 | let th0 = th1 - self.delta_th; |
175 | let (s0, c0) = th0.sin_cos(); |
176 | let (s1, c1) = if ix == self.n { |
177 | (0.0, 1.0) |
178 | } else { |
179 | th1.sin_cos() |
180 | }; |
181 | Some(PathEl::CurveTo( |
182 | Point::new(x + r * (c0 - a * s0), y + r * (s0 + a * c0)), |
183 | Point::new(x + r * (c1 + a * s1), y + r * (s1 - a * c1)), |
184 | Point::new(x + r * c1, y + r * s1), |
185 | )) |
186 | } else if ix == self.n + 1 { |
187 | Some(PathEl::ClosePath) |
188 | } else { |
189 | None |
190 | } |
191 | } |
192 | } |
193 | |
194 | /// A segment of a circle. |
195 | /// |
196 | /// If `inner_radius > 0`, then the shape will be a doughnut segment. |
197 | #[derive (Clone, Copy, Debug, PartialEq)] |
198 | #[cfg_attr (feature = "schemars" , derive(schemars::JsonSchema))] |
199 | #[cfg_attr (feature = "serde" , derive(serde::Serialize, serde::Deserialize))] |
200 | pub struct CircleSegment { |
201 | /// The center. |
202 | pub center: Point, |
203 | /// The outer radius. |
204 | pub outer_radius: f64, |
205 | /// The inner radius. |
206 | pub inner_radius: f64, |
207 | /// The angle to start drawing the segment (in radians). |
208 | pub start_angle: f64, |
209 | /// The arc length of the segment (in radians). |
210 | pub sweep_angle: f64, |
211 | } |
212 | |
213 | impl CircleSegment { |
214 | /// Create a `CircleSegment` out of its constituent parts. |
215 | pub fn new( |
216 | center: impl Into<Point>, |
217 | outer_radius: f64, |
218 | inner_radius: f64, |
219 | start_angle: f64, |
220 | sweep_angle: f64, |
221 | ) -> Self { |
222 | CircleSegment { |
223 | center: center.into(), |
224 | outer_radius, |
225 | inner_radius, |
226 | start_angle, |
227 | sweep_angle, |
228 | } |
229 | } |
230 | |
231 | /// Return an arc representing the outer radius. |
232 | #[must_use ] |
233 | #[inline ] |
234 | pub fn outer_arc(&self) -> Arc { |
235 | Arc { |
236 | center: self.center, |
237 | radii: Vec2::new(self.outer_radius, self.outer_radius), |
238 | start_angle: self.start_angle, |
239 | sweep_angle: self.sweep_angle, |
240 | x_rotation: 0.0, |
241 | } |
242 | } |
243 | |
244 | /// Return an arc representing the inner radius. |
245 | /// |
246 | /// This is [reversed] from the outer arc, so that it is in the |
247 | /// same direction as the arc that would be drawn (as the path |
248 | /// elements for this circle segment produce a closed path). |
249 | /// |
250 | /// [reversed]: Arc::reversed |
251 | #[must_use ] |
252 | #[inline ] |
253 | pub fn inner_arc(&self) -> Arc { |
254 | Arc { |
255 | center: self.center, |
256 | radii: Vec2::new(self.inner_radius, self.inner_radius), |
257 | start_angle: self.start_angle + self.sweep_angle, |
258 | sweep_angle: -self.sweep_angle, |
259 | x_rotation: 0.0, |
260 | } |
261 | } |
262 | |
263 | /// Is this circle segment [finite]? |
264 | /// |
265 | /// [finite]: f64::is_finite |
266 | #[inline ] |
267 | pub fn is_finite(&self) -> bool { |
268 | self.center.is_finite() |
269 | && self.outer_radius.is_finite() |
270 | && self.inner_radius.is_finite() |
271 | && self.start_angle.is_finite() |
272 | && self.sweep_angle.is_finite() |
273 | } |
274 | |
275 | /// Is this circle segment [NaN]? |
276 | /// |
277 | /// [NaN]: f64::is_nan |
278 | #[inline ] |
279 | pub fn is_nan(&self) -> bool { |
280 | self.center.is_nan() |
281 | || self.outer_radius.is_nan() |
282 | || self.inner_radius.is_nan() |
283 | || self.start_angle.is_nan() |
284 | || self.sweep_angle.is_nan() |
285 | } |
286 | } |
287 | |
288 | impl Add<Vec2> for CircleSegment { |
289 | type Output = CircleSegment; |
290 | |
291 | #[inline ] |
292 | fn add(self, v: Vec2) -> Self { |
293 | Self { |
294 | center: self.center + v, |
295 | ..self |
296 | } |
297 | } |
298 | } |
299 | |
300 | impl Sub<Vec2> for CircleSegment { |
301 | type Output = CircleSegment; |
302 | |
303 | #[inline ] |
304 | fn sub(self, v: Vec2) -> Self { |
305 | Self { |
306 | center: self.center - v, |
307 | ..self |
308 | } |
309 | } |
310 | } |
311 | |
312 | type CircleSegmentPathIter = iter::Chain< |
313 | iter::Chain< |
314 | iter::Chain<iter::Chain<iter::Once<PathEl>, iter::Once<PathEl>>, ArcAppendIter>, |
315 | iter::Once<PathEl>, |
316 | >, |
317 | ArcAppendIter, |
318 | >; |
319 | |
320 | impl Shape for CircleSegment { |
321 | type PathElementsIter<'iter> = CircleSegmentPathIter; |
322 | |
323 | fn path_elements(&self, tolerance: f64) -> CircleSegmentPathIter { |
324 | iter::once(PathEl::MoveTo(point_on_circle( |
325 | self.center, |
326 | self.inner_radius, |
327 | self.start_angle, |
328 | ))) |
329 | // First radius |
330 | .chain(iter::once(PathEl::LineTo(point_on_circle( |
331 | self.center, |
332 | self.outer_radius, |
333 | self.start_angle, |
334 | )))) |
335 | // outer arc |
336 | .chain(self.outer_arc().append_iter(tolerance)) |
337 | // second radius |
338 | .chain(iter::once(PathEl::LineTo(point_on_circle( |
339 | self.center, |
340 | self.inner_radius, |
341 | self.start_angle + self.sweep_angle, |
342 | )))) |
343 | // inner arc |
344 | .chain(self.inner_arc().append_iter(tolerance)) |
345 | } |
346 | |
347 | #[inline ] |
348 | fn area(&self) -> f64 { |
349 | 0.5 * (self.outer_radius.powi(2) - self.inner_radius.powi(2)).abs() * self.sweep_angle |
350 | } |
351 | |
352 | #[inline ] |
353 | fn perimeter(&self, _accuracy: f64) -> f64 { |
354 | 2.0 * (self.outer_radius - self.inner_radius).abs() |
355 | + self.sweep_angle * (self.inner_radius + self.outer_radius) |
356 | } |
357 | |
358 | fn winding(&self, pt: Point) -> i32 { |
359 | let angle = (pt - self.center).atan2(); |
360 | if angle < self.start_angle || angle > self.start_angle + self.sweep_angle { |
361 | return 0; |
362 | } |
363 | let dist2 = (pt - self.center).hypot2(); |
364 | if (dist2 < self.outer_radius.powi(2) && dist2 > self.inner_radius.powi(2)) || |
365 | // case where outer_radius < inner_radius |
366 | (dist2 < self.inner_radius.powi(2) && dist2 > self.outer_radius.powi(2)) |
367 | { |
368 | 1 |
369 | } else { |
370 | 0 |
371 | } |
372 | } |
373 | |
374 | #[inline ] |
375 | fn bounding_box(&self) -> Rect { |
376 | // todo this is currently not tight |
377 | let r = self.inner_radius.max(self.outer_radius); |
378 | let (x, y) = self.center.into(); |
379 | Rect::new(x - r, y - r, x + r, y + r) |
380 | } |
381 | } |
382 | |
383 | #[inline ] |
384 | fn point_on_circle(center: Point, radius: f64, angle: f64) -> Point { |
385 | let (angle_sin: f64, angle_cos: f64) = angle.sin_cos(); |
386 | center |
387 | + Vec2 { |
388 | x: angle_cos * radius, |
389 | y: angle_sin * radius, |
390 | } |
391 | } |
392 | |
393 | #[cfg (test)] |
394 | mod tests { |
395 | use crate::{Circle, Point, Shape}; |
396 | use std::f64::consts::PI; |
397 | |
398 | fn assert_approx_eq(x: f64, y: f64) { |
399 | // Note: we might want to be more rigorous in testing the accuracy |
400 | // of the conversion into Béziers. But this seems good enough. |
401 | assert!((x - y).abs() < 1e-7, "{x} != {y}" ); |
402 | } |
403 | |
404 | #[test ] |
405 | fn area_sign() { |
406 | let center = Point::new(5.0, 5.0); |
407 | let c = Circle::new(center, 5.0); |
408 | assert_approx_eq(c.area(), 25.0 * PI); |
409 | |
410 | assert_eq!(c.winding(center), 1); |
411 | |
412 | let p = c.to_path(1e-9); |
413 | assert_approx_eq(c.area(), p.area()); |
414 | assert_eq!(c.winding(center), p.winding(center)); |
415 | |
416 | let c_neg_radius = Circle::new(center, -5.0); |
417 | assert_approx_eq(c_neg_radius.area(), 25.0 * PI); |
418 | |
419 | assert_eq!(c_neg_radius.winding(center), 1); |
420 | |
421 | let p_neg_radius = c_neg_radius.to_path(1e-9); |
422 | assert_approx_eq(c_neg_radius.area(), p_neg_radius.area()); |
423 | assert_eq!(c_neg_radius.winding(center), p_neg_radius.winding(center)); |
424 | } |
425 | } |
426 | |