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