Skip to main content

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}