1// Copyright 2019 the Kurbo Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! A rectangle with rounded corners.
5
6use core::f64::consts::{FRAC_PI_2, FRAC_PI_4};
7use core::ops::{Add, Sub};
8
9use crate::{arc::ArcAppendIter, Arc, PathEl, Point, Rect, RoundedRectRadii, Shape, Size, Vec2};
10
11#[cfg(not(feature = "std"))]
12use 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))]
36pub struct RoundedRect {
37 /// Coordinates of the rectangle.
38 rect: Rect,
39 /// Radius of all four corners.
40 radii: RoundedRectRadii,
41}
42
43impl 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)]
147pub struct RoundedRectPathIter {
148 idx: usize,
149 rect: RectPathIter,
150 arcs: [ArcAppendIter; 4],
151}
152
153impl 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
352struct 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.
359impl 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.
388impl 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
417impl 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
426impl 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)]
436mod 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