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#[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}