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