/rust/registry/src/index.crates.io-1949cf8c6b5b557f/kurbo-0.13.0/src/rounded_rect.rs
Line | Count | Source |
1 | | // Copyright 2019 the Kurbo Authors |
2 | | // SPDX-License-Identifier: Apache-2.0 OR MIT |
3 | | |
4 | | //! A rectangle with rounded corners. |
5 | | |
6 | | use core::f64::consts::{FRAC_PI_2, FRAC_PI_4}; |
7 | | use core::ops::{Add, Sub}; |
8 | | |
9 | | use crate::{arc::ArcAppendIter, Arc, PathEl, Point, Rect, RoundedRectRadii, Shape, Size, Vec2}; |
10 | | |
11 | | #[allow(unused_imports)] // This is unused in later versions of Rust because of additions to core::f32 |
12 | | #[cfg(not(feature = "std"))] |
13 | | use crate::common::FloatFuncs; |
14 | | |
15 | | /// A rectangle with rounded corners. |
16 | | /// |
17 | | /// By construction the rounded rectangle will have |
18 | | /// non-negative dimensions and radii clamped to half size of the rect. |
19 | | /// The rounded rectangle can have different radii for each corner. |
20 | | /// |
21 | | /// The easiest way to create a `RoundedRect` is often to create a [`Rect`], |
22 | | /// and then call [`to_rounded_rect`]. |
23 | | /// |
24 | | /// ``` |
25 | | /// use kurbo::{RoundedRect, RoundedRectRadii}; |
26 | | /// |
27 | | /// // Create a rounded rectangle with a single radius for all corners: |
28 | | /// RoundedRect::new(0.0, 0.0, 10.0, 10.0, 5.0); |
29 | | /// |
30 | | /// // Or, specify different radii for each corner, clockwise from the top-left: |
31 | | /// RoundedRect::new(0.0, 0.0, 10.0, 10.0, (1.0, 2.0, 3.0, 4.0)); |
32 | | /// ``` |
33 | | /// |
34 | | /// [`to_rounded_rect`]: Rect::to_rounded_rect |
35 | | #[derive(Clone, Copy, Default, Debug, PartialEq)] |
36 | | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] |
37 | | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
38 | | pub struct RoundedRect { |
39 | | /// Coordinates of the rectangle. |
40 | | rect: Rect, |
41 | | /// Radius of all four corners. |
42 | | radii: RoundedRectRadii, |
43 | | } |
44 | | |
45 | | impl RoundedRect { |
46 | | /// A new rectangle from minimum and maximum coordinates. |
47 | | /// |
48 | | /// The result will have non-negative width, height and radii. |
49 | | #[inline] |
50 | 0 | pub fn new( |
51 | 0 | x0: f64, |
52 | 0 | y0: f64, |
53 | 0 | x1: f64, |
54 | 0 | y1: f64, |
55 | 0 | radii: impl Into<RoundedRectRadii>, |
56 | 0 | ) -> RoundedRect { |
57 | 0 | RoundedRect::from_rect(Rect::new(x0, y0, x1, y1), radii) |
58 | 0 | } |
59 | | |
60 | | /// A new rounded rectangle from a rectangle and corner radii. |
61 | | /// |
62 | | /// The result will have non-negative width, height and radii. |
63 | | /// |
64 | | /// See also [`Rect::to_rounded_rect`], which offers the same utility. |
65 | | #[inline] |
66 | 0 | pub fn from_rect(rect: Rect, radii: impl Into<RoundedRectRadii>) -> RoundedRect { |
67 | 0 | let rect = rect.abs(); |
68 | 0 | let shortest_side_length = (rect.width()).min(rect.height()); |
69 | 0 | let radii = radii.into().abs().clamp(shortest_side_length / 2.0); |
70 | | |
71 | 0 | RoundedRect { rect, radii } |
72 | 0 | } |
73 | | |
74 | | /// A new rectangle from two [`Point`]s. |
75 | | /// |
76 | | /// The result will have non-negative width, height and radius. |
77 | | #[inline] |
78 | 0 | pub fn from_points( |
79 | 0 | p0: impl Into<Point>, |
80 | 0 | p1: impl Into<Point>, |
81 | 0 | radii: impl Into<RoundedRectRadii>, |
82 | 0 | ) -> RoundedRect { |
83 | 0 | Rect::from_points(p0, p1).to_rounded_rect(radii) |
84 | 0 | } |
85 | | |
86 | | /// A new rectangle from origin and size. |
87 | | /// |
88 | | /// The result will have non-negative width, height and radius. |
89 | | #[inline] |
90 | 0 | pub fn from_origin_size( |
91 | 0 | origin: impl Into<Point>, |
92 | 0 | size: impl Into<Size>, |
93 | 0 | radii: impl Into<RoundedRectRadii>, |
94 | 0 | ) -> RoundedRect { |
95 | 0 | Rect::from_origin_size(origin, size).to_rounded_rect(radii) |
96 | 0 | } |
97 | | |
98 | | /// The width of the rectangle. |
99 | | #[inline] |
100 | 0 | pub fn width(&self) -> f64 { |
101 | 0 | self.rect.width() |
102 | 0 | } |
103 | | |
104 | | /// The height of the rectangle. |
105 | | #[inline] |
106 | 0 | pub fn height(&self) -> f64 { |
107 | 0 | self.rect.height() |
108 | 0 | } |
109 | | |
110 | | /// Radii of the rounded corners. |
111 | | #[inline(always)] |
112 | 0 | pub fn radii(&self) -> RoundedRectRadii { |
113 | 0 | self.radii |
114 | 0 | } |
115 | | |
116 | | /// The (non-rounded) rectangle. |
117 | | #[inline(always)] |
118 | 0 | pub fn rect(&self) -> Rect { |
119 | 0 | self.rect |
120 | 0 | } |
121 | | |
122 | | /// The origin of the rectangle. |
123 | | /// |
124 | | /// This is the top left corner in a y-down space. |
125 | | #[inline(always)] |
126 | 0 | pub fn origin(&self) -> Point { |
127 | 0 | self.rect.origin() |
128 | 0 | } |
129 | | |
130 | | /// The center point of the rectangle. |
131 | | #[inline] |
132 | 0 | pub fn center(&self) -> Point { |
133 | 0 | self.rect.center() |
134 | 0 | } |
135 | | |
136 | | /// Is this rounded rectangle finite? |
137 | | #[inline] |
138 | 0 | pub const fn is_finite(&self) -> bool { |
139 | 0 | self.rect.is_finite() && self.radii.is_finite() |
140 | 0 | } |
141 | | |
142 | | /// Is this rounded rectangle NaN? |
143 | | #[inline] |
144 | 0 | pub const fn is_nan(&self) -> bool { |
145 | 0 | self.rect.is_nan() || self.radii.is_nan() |
146 | 0 | } |
147 | | } |
148 | | |
149 | | #[doc(hidden)] |
150 | | pub struct RoundedRectPathIter { |
151 | | idx: usize, |
152 | | rect: RectPathIter, |
153 | | arcs: [ArcAppendIter; 4], |
154 | | } |
155 | | |
156 | | impl Shape for RoundedRect { |
157 | | type PathElementsIter<'iter> = RoundedRectPathIter; |
158 | | |
159 | 0 | fn path_elements(&self, tolerance: f64) -> RoundedRectPathIter { |
160 | 0 | let radii = self.radii(); |
161 | | |
162 | 0 | let build_arc_iter = |i, center, ellipse_radii| { |
163 | 0 | let arc = Arc { |
164 | 0 | center, |
165 | 0 | radii: ellipse_radii, |
166 | 0 | start_angle: FRAC_PI_2 * i as f64, |
167 | 0 | sweep_angle: FRAC_PI_2, |
168 | 0 | x_rotation: 0.0, |
169 | 0 | }; |
170 | 0 | arc.append_iter(tolerance) |
171 | 0 | }; |
172 | | |
173 | | // Note: order follows the rectangle path iterator. |
174 | 0 | let arcs = [ |
175 | 0 | build_arc_iter( |
176 | 0 | 2, |
177 | 0 | Point { |
178 | 0 | x: self.rect.x0 + radii.top_left, |
179 | 0 | y: self.rect.y0 + radii.top_left, |
180 | 0 | }, |
181 | 0 | Vec2 { |
182 | 0 | x: radii.top_left, |
183 | 0 | y: radii.top_left, |
184 | 0 | }, |
185 | 0 | ), |
186 | 0 | build_arc_iter( |
187 | 0 | 3, |
188 | 0 | Point { |
189 | 0 | x: self.rect.x1 - radii.top_right, |
190 | 0 | y: self.rect.y0 + radii.top_right, |
191 | 0 | }, |
192 | 0 | Vec2 { |
193 | 0 | x: radii.top_right, |
194 | 0 | y: radii.top_right, |
195 | 0 | }, |
196 | 0 | ), |
197 | 0 | build_arc_iter( |
198 | 0 | 0, |
199 | 0 | Point { |
200 | 0 | x: self.rect.x1 - radii.bottom_right, |
201 | 0 | y: self.rect.y1 - radii.bottom_right, |
202 | 0 | }, |
203 | 0 | Vec2 { |
204 | 0 | x: radii.bottom_right, |
205 | 0 | y: radii.bottom_right, |
206 | 0 | }, |
207 | 0 | ), |
208 | 0 | build_arc_iter( |
209 | 0 | 1, |
210 | 0 | Point { |
211 | 0 | x: self.rect.x0 + radii.bottom_left, |
212 | 0 | y: self.rect.y1 - radii.bottom_left, |
213 | 0 | }, |
214 | 0 | Vec2 { |
215 | 0 | x: radii.bottom_left, |
216 | 0 | y: radii.bottom_left, |
217 | 0 | }, |
218 | 0 | ), |
219 | 0 | ]; |
220 | | |
221 | 0 | let rect = RectPathIter { |
222 | 0 | rect: self.rect, |
223 | 0 | ix: 0, |
224 | 0 | radii, |
225 | 0 | }; |
226 | | |
227 | 0 | RoundedRectPathIter { idx: 0, rect, arcs } |
228 | 0 | } |
229 | | |
230 | | #[inline] |
231 | 0 | fn area(&self) -> f64 { |
232 | | // A corner is a quarter-circle, i.e. |
233 | | // .............# |
234 | | // . ###### |
235 | | // . ######### |
236 | | // . ########### |
237 | | // . ############ |
238 | | // .############# |
239 | | // ############## |
240 | | // |-----r------| |
241 | | // For each corner, we need to subtract the square that bounds this |
242 | | // quarter-circle, and add back in the area of quarter circle. |
243 | | |
244 | 0 | let radii = self.radii(); |
245 | | |
246 | | // Start with the area of the bounding rectangle. For each corner, |
247 | | // subtract the area of the corner under the quarter-circle, and add |
248 | | // back the area of the quarter-circle. |
249 | 0 | self.rect.area() |
250 | 0 | + [ |
251 | 0 | radii.top_left, |
252 | 0 | radii.top_right, |
253 | 0 | radii.bottom_right, |
254 | 0 | radii.bottom_left, |
255 | 0 | ] |
256 | 0 | .iter() |
257 | 0 | .map(|radius| (FRAC_PI_4 - 1.0) * radius * radius) |
258 | 0 | .sum::<f64>() |
259 | 0 | } |
260 | | |
261 | | #[inline] |
262 | 0 | fn perimeter(&self, _accuracy: f64) -> f64 { |
263 | | // A corner is a quarter-circle, i.e. |
264 | | // .............# |
265 | | // . # |
266 | | // . # |
267 | | // . # |
268 | | // . # |
269 | | // .# |
270 | | // # |
271 | | // |-----r------| |
272 | | // If we start with the bounding rectangle, then subtract 2r (the |
273 | | // straight edge outside the circle) and add 1/4 * pi * (2r) (the |
274 | | // perimeter of the quarter-circle) for each corner with radius r, we |
275 | | // get the perimeter of the shape. |
276 | | |
277 | 0 | let radii = self.radii(); |
278 | | |
279 | | // Start with the full perimeter. For each corner, subtract the |
280 | | // border surrounding the rounded corner and add the quarter-circle |
281 | | // perimeter. |
282 | 0 | self.rect.perimeter(1.0) |
283 | 0 | + ([ |
284 | 0 | radii.top_left, |
285 | 0 | radii.top_right, |
286 | 0 | radii.bottom_right, |
287 | 0 | radii.bottom_left, |
288 | 0 | ]) |
289 | 0 | .iter() |
290 | 0 | .map(|radius| (-2.0 + FRAC_PI_2) * radius) |
291 | 0 | .sum::<f64>() |
292 | 0 | } |
293 | | |
294 | | #[inline] |
295 | 0 | fn winding(&self, mut pt: Point) -> i32 { |
296 | 0 | let center = self.center(); |
297 | | |
298 | | // 1. Translate the point relative to the center of the rectangle. |
299 | 0 | pt.x -= center.x; |
300 | 0 | pt.y -= center.y; |
301 | | |
302 | | // 2. Pick a radius value to use based on which quadrant the point is |
303 | | // in. |
304 | 0 | let radii = self.radii(); |
305 | 0 | let radius = match pt { |
306 | 0 | pt if pt.x < 0.0 && pt.y < 0.0 => radii.top_left, |
307 | 0 | pt if pt.x >= 0.0 && pt.y < 0.0 => radii.top_right, |
308 | 0 | pt if pt.x >= 0.0 && pt.y >= 0.0 => radii.bottom_right, |
309 | 0 | pt if pt.x < 0.0 && pt.y >= 0.0 => radii.bottom_left, |
310 | 0 | _ => 0.0, |
311 | | }; |
312 | | |
313 | | // 3. This is the width and height of a rectangle with one corner at |
314 | | // the center of the rounded rectangle, and another corner at the |
315 | | // center of the relevant corner circle. |
316 | 0 | let inside_half_width = (self.width() / 2.0 - radius).max(0.0); |
317 | 0 | let inside_half_height = (self.height() / 2.0 - radius).max(0.0); |
318 | | |
319 | | // 4. Three things are happening here. |
320 | | // |
321 | | // First, the x- and y-values are being reflected into the positive |
322 | | // (bottom-right quadrant). The radius has already been determined, |
323 | | // so it doesn't matter what quadrant is used. |
324 | | // |
325 | | // After reflecting, the points are clamped so that their x- and y- |
326 | | // values can't be lower than the x- and y- values of the center of |
327 | | // the corner circle, and the coordinate system is transformed |
328 | | // again, putting (0, 0) at the center of the corner circle. |
329 | 0 | let px = (pt.x.abs() - inside_half_width).max(0.0); |
330 | 0 | let py = (pt.y.abs() - inside_half_height).max(0.0); |
331 | | |
332 | | // 5. The transforms above clamp all input points such that they will |
333 | | // be inside the rounded rectangle if the corresponding output point |
334 | | // (px, py) is inside a circle centered around the origin with the |
335 | | // given radius. |
336 | 0 | let inside = px * px + py * py <= radius * radius; |
337 | 0 | if inside { |
338 | 0 | 1 |
339 | | } else { |
340 | 0 | 0 |
341 | | } |
342 | 0 | } |
343 | | |
344 | | #[inline] |
345 | 0 | fn bounding_box(&self) -> Rect { |
346 | 0 | self.rect.bounding_box() |
347 | 0 | } |
348 | | |
349 | | #[inline(always)] |
350 | 0 | fn as_rounded_rect(&self) -> Option<RoundedRect> { |
351 | 0 | Some(*self) |
352 | 0 | } |
353 | | } |
354 | | |
355 | | struct RectPathIter { |
356 | | rect: Rect, |
357 | | radii: RoundedRectRadii, |
358 | | ix: usize, |
359 | | } |
360 | | |
361 | | // This is clockwise in a y-down coordinate system for positive area. |
362 | | impl Iterator for RectPathIter { |
363 | | type Item = PathEl; |
364 | | |
365 | 0 | fn next(&mut self) -> Option<PathEl> { |
366 | 0 | self.ix += 1; |
367 | 0 | match self.ix { |
368 | 0 | 1 => Some(PathEl::MoveTo(Point::new( |
369 | 0 | self.rect.x0, |
370 | 0 | self.rect.y0 + self.radii.top_left, |
371 | 0 | ))), |
372 | 0 | 2 => Some(PathEl::LineTo(Point::new( |
373 | 0 | self.rect.x1 - self.radii.top_right, |
374 | 0 | self.rect.y0, |
375 | 0 | ))), |
376 | 0 | 3 => Some(PathEl::LineTo(Point::new( |
377 | 0 | self.rect.x1, |
378 | 0 | self.rect.y1 - self.radii.bottom_right, |
379 | 0 | ))), |
380 | 0 | 4 => Some(PathEl::LineTo(Point::new( |
381 | 0 | self.rect.x0 + self.radii.bottom_left, |
382 | 0 | self.rect.y1, |
383 | 0 | ))), |
384 | 0 | 5 => Some(PathEl::ClosePath), |
385 | 0 | _ => None, |
386 | | } |
387 | 0 | } |
388 | | } |
389 | | |
390 | | // This is clockwise in a y-down coordinate system for positive area. |
391 | | impl Iterator for RoundedRectPathIter { |
392 | | type Item = PathEl; |
393 | | |
394 | 0 | fn next(&mut self) -> Option<PathEl> { |
395 | 0 | if self.idx > 4 { |
396 | 0 | return None; |
397 | 0 | } |
398 | | |
399 | | // Iterate between rectangle and arc iterators. |
400 | | // Rect iterator will start and end the path. |
401 | | |
402 | | // Initial point set by the rect iterator |
403 | 0 | if self.idx == 0 { |
404 | 0 | self.idx += 1; |
405 | 0 | return self.rect.next(); |
406 | 0 | } |
407 | | |
408 | | // Generate the arc curve elements. |
409 | | // If we reached the end of the arc, add a line towards next arc (rect iterator). |
410 | 0 | match self.arcs[self.idx - 1].next() { |
411 | 0 | Some(elem) => Some(elem), |
412 | | None => { |
413 | 0 | self.idx += 1; |
414 | 0 | self.rect.next() |
415 | | } |
416 | | } |
417 | 0 | } |
418 | | } |
419 | | |
420 | | impl Add<Vec2> for RoundedRect { |
421 | | type Output = RoundedRect; |
422 | | |
423 | | #[inline] |
424 | 0 | fn add(self, v: Vec2) -> RoundedRect { |
425 | 0 | RoundedRect::from_rect(self.rect + v, self.radii) |
426 | 0 | } |
427 | | } |
428 | | |
429 | | impl Sub<Vec2> for RoundedRect { |
430 | | type Output = RoundedRect; |
431 | | |
432 | | #[inline] |
433 | 0 | fn sub(self, v: Vec2) -> RoundedRect { |
434 | 0 | RoundedRect::from_rect(self.rect - v, self.radii) |
435 | 0 | } |
436 | | } |
437 | | |
438 | | #[cfg(test)] |
439 | | mod tests { |
440 | | use crate::{Circle, Point, Rect, RoundedRect, Shape}; |
441 | | |
442 | | #[test] |
443 | | fn area() { |
444 | | let epsilon = 1e-9; |
445 | | |
446 | | // Extremum: 0.0 radius corner -> rectangle |
447 | | let rect = Rect::new(0.0, 0.0, 100.0, 100.0); |
448 | | let rounded_rect = RoundedRect::new(0.0, 0.0, 100.0, 100.0, 0.0); |
449 | | assert!((rect.area() - rounded_rect.area()).abs() < epsilon); |
450 | | |
451 | | // Extremum: half-size radius corner -> circle |
452 | | let circle = Circle::new((0.0, 0.0), 50.0); |
453 | | let rounded_rect = RoundedRect::new(0.0, 0.0, 100.0, 100.0, 50.0); |
454 | | assert!((circle.area() - rounded_rect.area()).abs() < epsilon); |
455 | | } |
456 | | |
457 | | #[test] |
458 | | fn winding() { |
459 | | let rect = RoundedRect::new(-5.0, -5.0, 10.0, 20.0, (5.0, 5.0, 5.0, 0.0)); |
460 | | assert_eq!(rect.winding(Point::new(0.0, 0.0)), 1); |
461 | | assert_eq!(rect.winding(Point::new(-5.0, 0.0)), 1); // left edge |
462 | | assert_eq!(rect.winding(Point::new(0.0, 20.0)), 1); // bottom edge |
463 | | assert_eq!(rect.winding(Point::new(10.0, 20.0)), 0); // bottom-right corner |
464 | | assert_eq!(rect.winding(Point::new(-5.0, 20.0)), 1); // bottom-left corner (has a radius of 0) |
465 | | assert_eq!(rect.winding(Point::new(-10.0, 0.0)), 0); |
466 | | |
467 | | let rect = RoundedRect::new(-10.0, -20.0, 10.0, 20.0, 0.0); // rectangle |
468 | | assert_eq!(rect.winding(Point::new(10.0, 20.0)), 1); // bottom-right corner |
469 | | } |
470 | | |
471 | | #[test] |
472 | | fn bez_conversion() { |
473 | | let rect = RoundedRect::new(-5.0, -5.0, 10.0, 20.0, 5.0); |
474 | | let p = rect.to_path(1e-9); |
475 | | // Note: could be more systematic about tolerance tightness. |
476 | | let epsilon = 1e-7; |
477 | | assert!((rect.area() - p.area()).abs() < epsilon); |
478 | | assert_eq!(p.winding(Point::new(0.0, 0.0)), 1); |
479 | | } |
480 | | } |