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::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"))]
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 fn width(&self) -> f64 {
101        self.rect.width()
102    }
103
104    /// The height of the rectangle.
105    #[inline]
106    pub fn height(&self) -> f64 {
107        self.rect.height()
108    }
109
110    /// Radii of the rounded corners.
111    #[inline(always)]
112    pub fn radii(&self) -> RoundedRectRadii {
113        self.radii
114    }
115
116    /// The (non-rounded) rectangle.
117    #[inline(always)]
118    pub 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 fn origin(&self) -> Point {
127        self.rect.origin()
128    }
129
130    /// The center point of the rectangle.
131    #[inline]
132    pub fn center(&self) -> Point {
133        self.rect.center()
134    }
135
136    /// Is this rounded rectangle finite?
137    #[inline]
138    pub 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 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 radii = self.radii();
305        let radius = match pt {
306            pt if pt.x < 0.0 && pt.y < 0.0 => radii.top_left,
307            pt if pt.x >= 0.0 && pt.y < 0.0 => radii.top_right,
308            pt if pt.x >= 0.0 && pt.y >= 0.0 => radii.bottom_right,
309            pt if pt.x < 0.0 && pt.y >= 0.0 => radii.bottom_left,
310            _ => 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        let inside_half_width = (self.width() / 2.0 - radius).max(0.0);
317        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        let px = (pt.x.abs() - inside_half_width).max(0.0);
330        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        let inside = px * px + py * py <= radius * radius;
337        if inside {
338            1
339        } else {
340            0
341        }
342    }
343
344    #[inline]
345    fn bounding_box(&self) -> Rect {
346        self.rect.bounding_box()
347    }
348
349    #[inline(always)]
350    fn as_rounded_rect(&self) -> Option<RoundedRect> {
351        Some(*self)
352    }
353}
354
355struct 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.
362impl Iterator for RectPathIter {
363    type Item = PathEl;
364
365    fn next(&mut self) -> Option<PathEl> {
366        self.ix += 1;
367        match self.ix {
368            1 => Some(PathEl::MoveTo(Point::new(
369                self.rect.x0,
370                self.rect.y0 + self.radii.top_left,
371            ))),
372            2 => Some(PathEl::LineTo(Point::new(
373                self.rect.x1 - self.radii.top_right,
374                self.rect.y0,
375            ))),
376            3 => Some(PathEl::LineTo(Point::new(
377                self.rect.x1,
378                self.rect.y1 - self.radii.bottom_right,
379            ))),
380            4 => Some(PathEl::LineTo(Point::new(
381                self.rect.x0 + self.radii.bottom_left,
382                self.rect.y1,
383            ))),
384            5 => Some(PathEl::ClosePath),
385            _ => None,
386        }
387    }
388}
389
390// This is clockwise in a y-down coordinate system for positive area.
391impl Iterator for RoundedRectPathIter {
392    type Item = PathEl;
393
394    fn next(&mut self) -> Option<PathEl> {
395        if self.idx > 4 {
396            return None;
397        }
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        if self.idx == 0 {
404            self.idx += 1;
405            return self.rect.next();
406        }
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        match self.arcs[self.idx - 1].next() {
411            Some(elem) => Some(elem),
412            None => {
413                self.idx += 1;
414                self.rect.next()
415            }
416        }
417    }
418}
419
420impl Add<Vec2> for RoundedRect {
421    type Output = RoundedRect;
422
423    #[inline]
424    fn add(self, v: Vec2) -> RoundedRect {
425        RoundedRect::from_rect(self.rect + v, self.radii)
426    }
427}
428
429impl Sub<Vec2> for RoundedRect {
430    type Output = RoundedRect;
431
432    #[inline]
433    fn sub(self, v: Vec2) -> RoundedRect {
434        RoundedRect::from_rect(self.rect - v, self.radii)
435    }
436}
437
438#[cfg(test)]
439mod 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}