kurbo/rounded_rect.rs
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, PathEl, Point, Rect, RoundedRectRadii, Shape, Size, Vec2, arc::ArcAppendIter};
10
11#[allow(unused_imports)] // This is unused in later versions of Rust because of additions to core::f32
12#[cfg(not(feature = "std"))]
13use 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))]
38pub struct RoundedRect {
39 /// Coordinates of the rectangle.
40 rect: Rect,
41 /// Radius of all four corners.
42 radii: RoundedRectRadii,
43}
44
45impl 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 pub fn new(
51 x0: f64,
52 y0: f64,
53 x1: f64,
54 y1: f64,
55 radii: impl Into<RoundedRectRadii>,
56 ) -> RoundedRect {
57 RoundedRect::from_rect(Rect::new(x0, y0, x1, y1), radii)
58 }
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 pub fn from_rect(rect: Rect, radii: impl Into<RoundedRectRadii>) -> RoundedRect {
67 let rect = rect.abs();
68 let shortest_side_length = (rect.width()).min(rect.height());
69 let radii = radii.into().abs().clamp(shortest_side_length / 2.0);
70
71 RoundedRect { rect, radii }
72 }
73
74 /// A new rectangle from two [`Point`]s.
75 ///
76 /// The result will have non-negative width, height and radius.
77 #[inline]
78 pub fn from_points(
79 p0: impl Into<Point>,
80 p1: impl Into<Point>,
81 radii: impl Into<RoundedRectRadii>,
82 ) -> RoundedRect {
83 Rect::from_points(p0, p1).to_rounded_rect(radii)
84 }
85
86 /// A new rectangle from origin and size.
87 ///
88 /// The result will have non-negative width, height and radius.
89 #[inline]
90 pub fn from_origin_size(
91 origin: impl Into<Point>,
92 size: impl Into<Size>,
93 radii: impl Into<RoundedRectRadii>,
94 ) -> RoundedRect {
95 Rect::from_origin_size(origin, size).to_rounded_rect(radii)
96 }
97
98 /// The width of the rectangle.
99 #[inline]
100 pub const fn width(&self) -> f64 {
101 self.rect.width()
102 }
103
104 /// The height of the rectangle.
105 #[inline]
106 pub const fn height(&self) -> f64 {
107 self.rect.height()
108 }
109
110 /// Radii of the rounded corners.
111 #[inline(always)]
112 pub const fn radii(&self) -> RoundedRectRadii {
113 self.radii
114 }
115
116 /// The (non-rounded) rectangle.
117 #[inline(always)]
118 pub const fn rect(&self) -> Rect {
119 self.rect
120 }
121
122 /// The origin of the rectangle.
123 ///
124 /// This is the top left corner in a y-down space.
125 #[inline(always)]
126 pub const fn origin(&self) -> Point {
127 self.rect.origin()
128 }
129
130 /// The center point of the rectangle.
131 #[inline]
132 pub const fn center(&self) -> Point {
133 self.rect.center()
134 }
135
136 /// Is this rounded rectangle finite?
137 #[inline]
138 pub const fn is_finite(&self) -> bool {
139 self.rect.is_finite() && self.radii.is_finite()
140 }
141
142 /// Is this rounded rectangle NaN?
143 #[inline]
144 pub const fn is_nan(&self) -> bool {
145 self.rect.is_nan() || self.radii.is_nan()
146 }
147}
148
149#[doc(hidden)]
150pub struct RoundedRectPathIter {
151 idx: usize,
152 rect: RectPathIter,
153 arcs: [ArcAppendIter; 4],
154}
155
156impl Shape for RoundedRect {
157 type PathElementsIter<'iter> = RoundedRectPathIter;
158
159 fn path_elements(&self, tolerance: f64) -> RoundedRectPathIter {
160 let radii = self.radii();
161
162 let build_arc_iter = |i, center, ellipse_radii| {
163 let arc = Arc {
164 center,
165 radii: ellipse_radii,
166 start_angle: FRAC_PI_2 * i as f64,
167 sweep_angle: FRAC_PI_2,
168 x_rotation: 0.0,
169 };
170 arc.append_iter(tolerance)
171 };
172
173 // Note: order follows the rectangle path iterator.
174 let arcs = [
175 build_arc_iter(
176 2,
177 Point {
178 x: self.rect.x0 + radii.top_left,
179 y: self.rect.y0 + radii.top_left,
180 },
181 Vec2 {
182 x: radii.top_left,
183 y: radii.top_left,
184 },
185 ),
186 build_arc_iter(
187 3,
188 Point {
189 x: self.rect.x1 - radii.top_right,
190 y: self.rect.y0 + radii.top_right,
191 },
192 Vec2 {
193 x: radii.top_right,
194 y: radii.top_right,
195 },
196 ),
197 build_arc_iter(
198 0,
199 Point {
200 x: self.rect.x1 - radii.bottom_right,
201 y: self.rect.y1 - radii.bottom_right,
202 },
203 Vec2 {
204 x: radii.bottom_right,
205 y: radii.bottom_right,
206 },
207 ),
208 build_arc_iter(
209 1,
210 Point {
211 x: self.rect.x0 + radii.bottom_left,
212 y: self.rect.y1 - radii.bottom_left,
213 },
214 Vec2 {
215 x: radii.bottom_left,
216 y: radii.bottom_left,
217 },
218 ),
219 ];
220
221 let rect = RectPathIter {
222 rect: self.rect,
223 ix: 0,
224 radii,
225 };
226
227 RoundedRectPathIter { idx: 0, rect, arcs }
228 }
229
230 #[inline]
231 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 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 self.rect.area()
250 + [
251 radii.top_left,
252 radii.top_right,
253 radii.bottom_right,
254 radii.bottom_left,
255 ]
256 .iter()
257 .map(|radius| (FRAC_PI_4 - 1.0) * radius * radius)
258 .sum::<f64>()
259 }
260
261 #[inline]
262 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 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 self.rect.perimeter(1.0)
283 + ([
284 radii.top_left,
285 radii.top_right,
286 radii.bottom_right,
287 radii.bottom_left,
288 ])
289 .iter()
290 .map(|radius| (-2.0 + FRAC_PI_2) * radius)
291 .sum::<f64>()
292 }
293
294 #[inline]
295 fn winding(&self, mut pt: Point) -> i32 {
296 let center = self.center();
297
298 // 1. Translate the point relative to the center of the rectangle.
299 pt.x -= center.x;
300 pt.y -= center.y;
301
302 // 2. Pick a radius value to use based on which quadrant the point is
303 // in.
304 let radius = {
305 /// Calculates `if !cond { a } else { b }`.
306 ///
307 /// This function is theoretically pretty nonsensical to have, as the compiler should
308 /// pretty trivially be able to compile both this function and the explicit
309 /// `if`-statement equivalent to the same thing. However, for some reason, writing
310 /// these bit operations explicitly causes the compiler to be better about prefetching
311 /// the rounded rect data.
312 ///
313 /// See <https://github.com/linebender/kurbo/pull/534> for more.
314 #[inline(always)]
315 fn select(a: f64, b: f64, cond: bool) -> f64 {
316 let mask = (cond as u64).wrapping_neg(); // 0 or !0
317 f64::from_bits((a.to_bits() & !mask) | (b.to_bits() & mask))
318 }
319
320 let radii = self.radii();
321 let radius_top = select(radii.top_left, radii.top_right, pt.x >= 0.);
322 let radius_bottom = select(radii.bottom_left, radii.bottom_right, pt.x >= 0.);
323 select(radius_top, radius_bottom, pt.y >= 0.)
324 };
325
326 // 3. This is the width and height of a rectangle with one corner at
327 // the center of the rounded rectangle, and another corner at the
328 // center of the relevant corner circle.
329 let inside_half_width = (self.width() / 2.0 - radius).max(0.0);
330 let inside_half_height = (self.height() / 2.0 - radius).max(0.0);
331
332 // 4. Three things are happening here.
333 //
334 // First, the x- and y-values are being reflected into the positive
335 // (bottom-right quadrant). The radius has already been determined,
336 // so it doesn't matter what quadrant is used.
337 //
338 // After reflecting, the points are clamped so that their x- and y-
339 // values can't be lower than the x- and y- values of the center of
340 // the corner circle, and the coordinate system is transformed
341 // again, putting (0, 0) at the center of the corner circle.
342 let px = (pt.x.abs() - inside_half_width).max(0.0);
343 let py = (pt.y.abs() - inside_half_height).max(0.0);
344
345 // 5. The transforms above clamp all input points such that they will
346 // be inside the rounded rectangle if the corresponding output point
347 // (px, py) is inside a circle centered around the origin with the
348 // given radius.
349 let inside = px * px + py * py <= radius * radius;
350 if inside { 1 } else { 0 }
351 }
352
353 #[inline]
354 fn bounding_box(&self) -> Rect {
355 self.rect.bounding_box()
356 }
357
358 #[inline(always)]
359 fn as_rounded_rect(&self) -> Option<RoundedRect> {
360 Some(*self)
361 }
362}
363
364struct RectPathIter {
365 rect: Rect,
366 radii: RoundedRectRadii,
367 ix: usize,
368}
369
370// This is clockwise in a y-down coordinate system for positive area.
371impl Iterator for RectPathIter {
372 type Item = PathEl;
373
374 fn next(&mut self) -> Option<PathEl> {
375 self.ix += 1;
376 match self.ix {
377 1 => Some(PathEl::MoveTo(Point::new(
378 self.rect.x0,
379 self.rect.y0 + self.radii.top_left,
380 ))),
381 2 => Some(PathEl::LineTo(Point::new(
382 self.rect.x1 - self.radii.top_right,
383 self.rect.y0,
384 ))),
385 3 => Some(PathEl::LineTo(Point::new(
386 self.rect.x1,
387 self.rect.y1 - self.radii.bottom_right,
388 ))),
389 4 => Some(PathEl::LineTo(Point::new(
390 self.rect.x0 + self.radii.bottom_left,
391 self.rect.y1,
392 ))),
393 5 => Some(PathEl::ClosePath),
394 _ => None,
395 }
396 }
397}
398
399// This is clockwise in a y-down coordinate system for positive area.
400impl Iterator for RoundedRectPathIter {
401 type Item = PathEl;
402
403 fn next(&mut self) -> Option<PathEl> {
404 if self.idx > 4 {
405 return None;
406 }
407
408 // Iterate between rectangle and arc iterators.
409 // Rect iterator will start and end the path.
410
411 // Initial point set by the rect iterator
412 if self.idx == 0 {
413 self.idx += 1;
414 return self.rect.next();
415 }
416
417 // Generate the arc curve elements.
418 // If we reached the end of the arc, add a line towards next arc (rect iterator).
419 match self.arcs[self.idx - 1].next() {
420 Some(elem) => Some(elem),
421 None => {
422 self.idx += 1;
423 self.rect.next()
424 }
425 }
426 }
427}
428
429impl Add<Vec2> for RoundedRect {
430 type Output = RoundedRect;
431
432 #[inline]
433 fn add(self, v: Vec2) -> RoundedRect {
434 RoundedRect::from_rect(self.rect + v, self.radii)
435 }
436}
437
438impl Sub<Vec2> for RoundedRect {
439 type Output = RoundedRect;
440
441 #[inline]
442 fn sub(self, v: Vec2) -> RoundedRect {
443 RoundedRect::from_rect(self.rect - v, self.radii)
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use crate::{Circle, Point, Rect, RoundedRect, Shape};
450
451 #[test]
452 fn area() {
453 let epsilon = 1e-9;
454
455 // Extremum: 0.0 radius corner -> rectangle
456 let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
457 let rounded_rect = RoundedRect::new(0.0, 0.0, 100.0, 100.0, 0.0);
458 assert!((rect.area() - rounded_rect.area()).abs() < epsilon);
459
460 // Extremum: half-size radius corner -> circle
461 let circle = Circle::new((0.0, 0.0), 50.0);
462 let rounded_rect = RoundedRect::new(0.0, 0.0, 100.0, 100.0, 50.0);
463 assert!((circle.area() - rounded_rect.area()).abs() < epsilon);
464 }
465
466 #[test]
467 fn winding() {
468 let rect = RoundedRect::new(-5.0, -5.0, 10.0, 20.0, (5.0, 5.0, 5.0, 0.0));
469 assert_eq!(rect.winding(Point::new(0.0, 0.0)), 1);
470 assert_eq!(rect.winding(Point::new(-5.0, 0.0)), 1); // left edge
471 assert_eq!(rect.winding(Point::new(0.0, 20.0)), 1); // bottom edge
472 assert_eq!(rect.winding(Point::new(10.0, 20.0)), 0); // bottom-right corner
473 assert_eq!(rect.winding(Point::new(-5.0, 20.0)), 1); // bottom-left corner (has a radius of 0)
474 assert_eq!(rect.winding(Point::new(-10.0, 0.0)), 0);
475
476 let rect = RoundedRect::new(-10.0, -20.0, 10.0, 20.0, 0.0); // rectangle
477 assert_eq!(rect.winding(Point::new(10.0, 20.0)), 1); // bottom-right corner
478 }
479
480 #[test]
481 fn bez_conversion() {
482 let rect = RoundedRect::new(-5.0, -5.0, 10.0, 20.0, 5.0);
483 let p = rect.to_path(1e-9);
484 // Note: could be more systematic about tolerance tightness.
485 let epsilon = 1e-7;
486 assert!((rect.area() - p.area()).abs() < epsilon);
487 assert_eq!(p.winding(Point::new(0.0, 0.0)), 1);
488 }
489}