keyset_geom/
round_rect.rs

1use std::f64::consts::{FRAC_PI_2, PI};
2use std::ops::{Add, Sub};
3
4use kurbo::{Arc, ArcAppendIter, Ellipse, PathEl, Point, Rect, Shape, Size, Vec2};
5
6/// A rectangle with rounded corners. Unlike [`kurbo::RoundedRect`] this has one set of radii
7/// shared between all corners, but it does support elliptical corners.
8#[derive(Debug, Clone, Copy)]
9pub struct RoundRect {
10    rect: Rect,
11    radii: Vec2,
12}
13
14impl RoundRect {
15    /// Create a new rounded rectangle from minimum and maximum coordinates.
16    #[inline]
17    #[must_use]
18    pub fn new(x0: f64, y0: f64, x1: f64, y1: f64, rx: f64, ry: f64) -> Self {
19        Self::from_rect(Rect::new(x0, y0, x1, y1), Vec2::new(rx, ry))
20    }
21
22    /// Create a new rounded rectangle from a [`kurbo::Rect`] and its radii.
23    #[inline]
24    #[must_use]
25    pub fn from_rect(rect: Rect, radii: impl Into<Vec2>) -> Self {
26        let rect = rect.abs();
27        let radii = radii.into();
28        let radii = Vec2::new(
29            radii.x.min(rect.width() / 2.0),
30            radii.y.min(rect.height() / 2.0),
31        );
32
33        Self { rect, radii }
34    }
35
36    /// Create a new rounded rectangle from two points and its radii.
37    #[inline]
38    #[must_use]
39    pub fn from_points(p0: impl Into<Point>, p1: impl Into<Point>, radii: impl Into<Vec2>) -> Self {
40        Self::from_rect(Rect::from_points(p0, p1), radii)
41    }
42
43    /// Create a new rounded rectangle from its origin point, size, and radii.
44    #[inline]
45    #[must_use]
46    pub fn from_origin_size(
47        origin: impl Into<Point>,
48        size: impl Into<Size>,
49        radii: impl Into<Vec2>,
50    ) -> Self {
51        Self::from_rect(Rect::from_origin_size(origin, size), radii)
52    }
53
54    /// Create a new rounded rectangle from its center point, size, and radii.
55    #[inline]
56    #[must_use]
57    pub fn from_center_size(
58        origin: impl Into<Point>,
59        size: impl Into<Size>,
60        radii: impl Into<Vec2>,
61    ) -> Self {
62        Self::from_rect(Rect::from_center_size(origin, size), radii)
63    }
64
65    /// Returns the width of the rounded rectangle
66    #[inline]
67    #[must_use]
68    pub fn width(&self) -> f64 {
69        self.rect.width()
70    }
71
72    /// Returns the height of the rounded rectangle
73    #[inline]
74    #[must_use]
75    pub fn height(&self) -> f64 {
76        self.rect.height()
77    }
78
79    /// Returns the radii of the rounded rectangle
80    #[inline]
81    #[must_use]
82    pub const fn radii(&self) -> Vec2 {
83        self.radii
84    }
85
86    /// Returns a rectangle with the same position and size as the rounded rectangle
87    #[inline]
88    #[must_use]
89    pub const fn rect(&self) -> Rect {
90        self.rect
91    }
92
93    /// Returns the origin point of the rounded rectangle
94    #[inline]
95    #[must_use]
96    pub fn origin(&self) -> Point {
97        self.rect.origin()
98    }
99
100    /// Returns the center point of the rounded rectangle
101    #[inline]
102    #[must_use]
103    pub fn center(&self) -> Point {
104        self.rect.center()
105    }
106
107    /// Returns the size of the rounded rectangle
108    #[inline]
109    #[must_use]
110    pub fn size(&self) -> Size {
111        self.rect().size()
112    }
113
114    /// Returns a copy of the rounded rectangle with a new origin point
115    #[inline]
116    #[must_use]
117    pub fn with_origin(self, origin: impl Into<Point>) -> Self {
118        Self::from_origin_size(origin, self.size(), self.radii())
119    }
120
121    /// Returns a copy of the rounded rectangle with a new size
122    #[inline]
123    #[must_use]
124    pub fn with_size(self, size: impl Into<Size>) -> Self {
125        Self::from_origin_size(self.origin(), size, self.radii())
126    }
127
128    /// Returns a copy of the rounded rectangle with new radii
129    #[inline]
130    #[must_use]
131    pub fn with_radii(self, radii: impl Into<Vec2>) -> Self {
132        Self::from_origin_size(self.origin(), self.size(), radii)
133    }
134}
135
136#[doc(hidden)]
137#[allow(clippy::module_name_repetitions)]
138pub struct RoundRectPathIter {
139    idx: usize,
140    rect: RectPathIter,
141    arcs: [ArcAppendIter; 4],
142}
143
144impl Shape for RoundRect {
145    type PathElementsIter<'iter> = RoundRectPathIter;
146
147    fn path_elements(&self, tolerance: f64) -> RoundRectPathIter {
148        let radii = self.radii();
149
150        let build_arc_iter = |i, center, ellipse_radii| {
151            let arc = Arc {
152                center,
153                radii: ellipse_radii,
154                start_angle: FRAC_PI_2 * f64::from(i),
155                sweep_angle: FRAC_PI_2,
156                x_rotation: 0.0,
157            };
158            arc.append_iter(tolerance)
159        };
160
161        // Note: order follows the rectangle path iterator.
162        let arcs = [
163            build_arc_iter(
164                2,
165                Point {
166                    x: self.rect.x0 + radii.x,
167                    y: self.rect.y0 + radii.y,
168                },
169                radii,
170            ),
171            build_arc_iter(
172                3,
173                Point {
174                    x: self.rect.x1 - radii.x,
175                    y: self.rect.y0 + radii.y,
176                },
177                radii,
178            ),
179            build_arc_iter(
180                0,
181                Point {
182                    x: self.rect.x1 - radii.x,
183                    y: self.rect.y1 - radii.y,
184                },
185                radii,
186            ),
187            build_arc_iter(
188                1,
189                Point {
190                    x: self.rect.x0 + radii.x,
191                    y: self.rect.y1 - radii.y,
192                },
193                radii,
194            ),
195        ];
196
197        let rect = RectPathIter {
198            rect: self.rect,
199            ix: 0,
200            radii,
201        };
202
203        RoundRectPathIter { idx: 0, rect, arcs }
204    }
205
206    #[inline]
207    fn area(&self) -> f64 {
208        self.rect.area() - (4.0 - PI) * self.radii().x * self.radii().y
209    }
210
211    #[inline]
212    fn perimeter(&self, accuracy: f64) -> f64 {
213        self.rect.perimeter(accuracy) - 4.0 * (self.radii.x + self.radii.y)
214            + Ellipse::new(Point::ORIGIN, self.radii, 0.0).perimeter(accuracy)
215    }
216
217    #[inline]
218    fn winding(&self, mut pt: Point) -> i32 {
219        let center = self.center();
220        let Vec2 { x: rx, y: ry } = self.radii;
221
222        if rx <= 0.0 || ry <= 0.0 {
223            return self.bounding_box().winding(pt);
224        }
225
226        // 1. Translate the point relative to the center of the rectangle.
227        pt.x -= center.x;
228        pt.y -= center.y;
229
230        // 2. This is the width and height of a rectangle with one corner at
231        //    the center of the rounded rectangle, and another corner at the
232        //    center of the relevant corner circle.
233        let inside_half_width = (self.width() / 2.0 - rx).max(0.0);
234        let inside_half_height = (self.height() / 2.0 - ry).max(0.0);
235
236        // 3. Three things are happening here.
237        //
238        //    First, the x- and y-values are being reflected into the positive
239        //    (bottom-right quadrant).
240        //
241        //    After reflecting, the points are clamped so that their x- and y-
242        //    values can't be lower than the x- and y- values of the center of
243        //    the corner ellipse, and the coordinate system is transformed
244        //    again, putting (0, 0) at the center of the corner ellipse.
245        let px = (pt.x.abs() - inside_half_width).max(0.0);
246        let py = (pt.y.abs() - inside_half_height).max(0.0);
247
248        // 4. The transforms above clamp all input points such that they will
249        //    be inside the rounded rectangle if the corresponding output point
250        //    (px, py) is inside a ellipse centered around the origin with the
251        //    given radii.
252        let inside = (px * px) / (rx * rx) + (py * py) / (ry * ry) <= 1.0;
253        i32::from(inside)
254    }
255
256    #[inline]
257    fn bounding_box(&self) -> Rect {
258        self.rect.bounding_box()
259    }
260}
261
262struct RectPathIter {
263    rect: Rect,
264    radii: Vec2,
265    ix: usize,
266}
267
268// This is clockwise in a y-down coordinate system for positive area.
269impl Iterator for RectPathIter {
270    type Item = PathEl;
271
272    fn next(&mut self) -> Option<PathEl> {
273        self.ix += 1;
274        match self.ix {
275            1 => Some(PathEl::MoveTo(Point::new(
276                self.rect.x0,
277                self.rect.y0 + self.radii.y,
278            ))),
279            2 => Some(PathEl::LineTo(Point::new(
280                self.rect.x1 - self.radii.x,
281                self.rect.y0,
282            ))),
283            3 => Some(PathEl::LineTo(Point::new(
284                self.rect.x1,
285                self.rect.y1 - self.radii.y,
286            ))),
287            4 => Some(PathEl::LineTo(Point::new(
288                self.rect.x0 + self.radii.x,
289                self.rect.y1,
290            ))),
291            5 => Some(PathEl::ClosePath),
292            _ => None, // GRCOV_EXCL_LINE - unreachable?
293        }
294    }
295}
296
297// This is clockwise in a y-down coordinate system for positive area.
298impl Iterator for RoundRectPathIter {
299    type Item = PathEl;
300
301    fn next(&mut self) -> Option<PathEl> {
302        if self.idx > 4 {
303            return None;
304        }
305
306        // Iterate between rectangle and arc iterators.
307        // Rect iterator will start and end the path.
308
309        // Initial point set by the rect iterator
310        if self.idx == 0 {
311            self.idx += 1;
312            return self.rect.next();
313        }
314
315        // Generate the arc curve elements.
316        // If we reached the end of the arc, add a line towards next arc (rect iterator).
317        if let Some(elem) = self.arcs[self.idx - 1].next() {
318            Some(elem)
319        } else {
320            self.idx += 1;
321            self.rect.next()
322        }
323    }
324}
325
326impl Add<Vec2> for RoundRect {
327    type Output = Self;
328
329    #[inline]
330    fn add(self, v: Vec2) -> Self::Output {
331        Self::from_rect(self.rect + v, self.radii)
332    }
333}
334
335impl Sub<Vec2> for RoundRect {
336    type Output = Self;
337
338    #[inline]
339    fn sub(self, v: Vec2) -> Self::Output {
340        Self::from_rect(self.rect - v, self.radii)
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use assert_approx_eq::assert_approx_eq;
347    use kurbo::{Circle, Point, Rect, Shape};
348
349    use super::*;
350
351    #[test]
352    fn test_round_rect_new() {
353        let rect = RoundRect::new(1.0, 2.0, 3.0, 5.0, 0.25, 0.75);
354
355        assert_eq!(rect.rect.x0, 1.0);
356        assert_eq!(rect.rect.y0, 2.0);
357        assert_eq!(rect.rect.x1, 3.0);
358        assert_eq!(rect.rect.y1, 5.0);
359        assert_eq!(rect.radii.x, 0.25);
360        assert_eq!(rect.radii.y, 0.75);
361    }
362
363    #[test]
364    fn test_round_rect_from_rect() {
365        let rect = RoundRect::from_rect(Rect::new(1.0, 2.0, 3.0, 5.0), (0.25, 0.75));
366
367        assert_eq!(rect.rect.x0, 1.0);
368        assert_eq!(rect.rect.y0, 2.0);
369        assert_eq!(rect.rect.x1, 3.0);
370        assert_eq!(rect.rect.y1, 5.0);
371        assert_eq!(rect.radii.x, 0.25);
372        assert_eq!(rect.radii.y, 0.75);
373    }
374
375    #[test]
376    fn test_round_rect_from_points() {
377        let rect = RoundRect::from_points((1.0, 2.0), (3.0, 5.0), (0.25, 0.75));
378
379        assert_eq!(rect.rect.x0, 1.0);
380        assert_eq!(rect.rect.y0, 2.0);
381        assert_eq!(rect.rect.x1, 3.0);
382        assert_eq!(rect.rect.y1, 5.0);
383        assert_eq!(rect.radii.x, 0.25);
384        assert_eq!(rect.radii.y, 0.75);
385    }
386
387    #[test]
388    fn test_round_rect_from_origin_size() {
389        let rect = RoundRect::from_origin_size((1.0, 2.0), (2.0, 3.0), (0.25, 0.75));
390
391        assert_eq!(rect.rect.x0, 1.0);
392        assert_eq!(rect.rect.y0, 2.0);
393        assert_eq!(rect.rect.x1, 3.0);
394        assert_eq!(rect.rect.y1, 5.0);
395        assert_eq!(rect.radii.x, 0.25);
396        assert_eq!(rect.radii.y, 0.75);
397    }
398
399    #[test]
400    fn test_round_rect_from_center_size() {
401        let rect = RoundRect::from_center_size((2.0, 3.5), (2.0, 3.0), (0.25, 0.75));
402
403        assert_eq!(rect.rect.x0, 1.0);
404        assert_eq!(rect.rect.y0, 2.0);
405        assert_eq!(rect.rect.x1, 3.0);
406        assert_eq!(rect.rect.y1, 5.0);
407        assert_eq!(rect.radii.x, 0.25);
408        assert_eq!(rect.radii.y, 0.75);
409    }
410
411    #[test]
412    fn test_round_rect_width() {
413        let rect = RoundRect::new(1.0, 2.0, 3.0, 5.0, 0.25, 0.75);
414
415        assert_eq!(rect.width(), 2.0);
416    }
417
418    #[test]
419    fn test_round_rect_height() {
420        let rect = RoundRect::new(1.0, 2.0, 3.0, 5.0, 0.25, 0.75);
421
422        assert_eq!(rect.height(), 3.0);
423    }
424
425    #[test]
426    fn test_round_rect_radii() {
427        let rect = RoundRect::new(1.0, 2.0, 3.0, 5.0, 0.25, 0.75);
428
429        assert_eq!(rect.radii(), Vec2::new(0.25, 0.75));
430    }
431
432    #[test]
433    fn test_round_rect_rect() {
434        let rect = RoundRect::new(1.0, 2.0, 3.0, 5.0, 0.25, 0.75);
435
436        assert_eq!(rect.rect(), Rect::new(1.0, 2.0, 3.0, 5.0));
437    }
438
439    #[test]
440    fn test_round_rect_origin() {
441        let rect = RoundRect::new(1.0, 2.0, 3.0, 5.0, 0.25, 0.75);
442
443        assert_eq!(rect.origin(), Point::new(1.0, 2.0));
444    }
445
446    #[test]
447    fn test_round_rect_center() {
448        let rect = RoundRect::new(1.0, 2.0, 3.0, 5.0, 0.25, 0.75);
449
450        assert_eq!(rect.center(), Point::new(2.0, 3.5));
451    }
452
453    #[test]
454    fn test_round_rect_size() {
455        let rect = RoundRect::new(1.0, 2.0, 3.0, 5.0, 0.25, 0.75);
456
457        assert_eq!(rect.size(), Size::new(2.0, 3.0));
458    }
459
460    #[test]
461    fn test_round_rect_with_origin() {
462        let rect = RoundRect::new(1.0, 2.0, 3.0, 5.0, 0.25, 0.75).with_origin((2.0, 4.0));
463
464        assert_eq!(rect.origin(), Point::new(2.0, 4.0));
465    }
466
467    #[test]
468    fn test_round_rect_with_size() {
469        let rect = RoundRect::new(1.0, 2.0, 3.0, 5.0, 0.25, 0.75).with_size((3.0, 1.0));
470
471        assert_eq!(rect.size(), Size::new(3.0, 1.0));
472    }
473
474    #[test]
475    fn test_round_rect_with_radii() {
476        let rect = RoundRect::new(1.0, 2.0, 3.0, 5.0, 0.25, 0.75).with_radii((0.5, 0.25));
477
478        assert_eq!(rect.radii(), Vec2::new(0.5, 0.25));
479    }
480
481    #[test]
482    fn test_round_rect_path_elements() {
483        let rect = RoundRect::new(1.0, 2.0, 3.0, 5.0, 0.25, 0.75);
484        let path = rect.to_path(1e-9);
485
486        assert_approx_eq!(rect.area(), path.area());
487        assert_eq!(path.winding(Point::new(2.0, 3.0)), 1);
488    }
489
490    #[test]
491    fn test_round_rect_area() {
492        // Extremum: 0.0 radius corner -> rectangle
493        let ref_rect = Rect::new(0.0, 0.0, 10.0, 10.0);
494        let rect = RoundRect::new(0.0, 0.0, 10.0, 10.0, 0.0, 0.0);
495        assert_approx_eq!(ref_rect.area(), rect.area());
496
497        // Extremum: half-size radius corner -> circle
498        let circle = Circle::new((0.0, 0.0), 5.0);
499        let rect = RoundRect::new(0.0, 0.0, 10.0, 10.0, 5.0, 5.0);
500        assert_approx_eq!(circle.area(), rect.area());
501    }
502
503    #[test]
504    fn test_round_rect_perimeter() {
505        // Extremum: 0.0 radius corner -> rectangle
506        let rect = RoundRect::new(0.0, 0.0, 10.0, 10.0, 0.0, 0.0);
507        assert_approx_eq!(rect.perimeter(1.0), 40.0);
508
509        // Extremum: half-size radius corner -> circle
510        let rect = RoundRect::new(0.0, 0.0, 10.0, 10.0, 5.0, 5.0);
511        assert_approx_eq!(rect.perimeter(1.0), 10. * PI, 0.01);
512    }
513
514    #[test]
515    fn test_round_rect_winding() {
516        let rect = RoundRect::new(-5.0, -5.0, 10.0, 20.0, 5.0, 5.0);
517        assert_eq!(rect.winding(Point::new(0.0, 0.0)), 1);
518        assert_eq!(rect.winding(Point::new(-5.0, 0.0)), 1); // left edge
519        assert_eq!(rect.winding(Point::new(0.0, 20.0)), 1); // bottom edge
520        assert_eq!(rect.winding(Point::new(10.0, 20.0)), 0); // bottom-right corner
521        assert_eq!(rect.winding(Point::new(-5.0, 20.0)), 0); // bottom-left corner
522        assert_eq!(rect.winding(Point::new(-10.0, 0.0)), 0);
523
524        let rect = RoundRect::new(-10.0, -20.0, 10.0, 20.0, 0.0, 0.0); // rectangle
525        assert_eq!(rect.winding(Point::new(10.0, 20.0)), 0); // bottom-right corner
526    }
527
528    #[test]
529    fn test_round_rect_add_sub() {
530        let rect = RoundRect::new(1.0, 2.0, 3.0, 5.0, 0.25, 0.75) + Vec2::new(2.0, 3.0);
531
532        assert_eq!(rect.rect, Rect::new(3.0, 5.0, 5.0, 8.0));
533        assert_eq!(rect.radii, Vec2::new(0.25, 0.75));
534
535        let rect = rect - Vec2::new(1.0, 4.0);
536
537        assert_eq!(rect.rect, Rect::new(2.0, 1.0, 4.0, 4.0));
538        assert_eq!(rect.radii, Vec2::new(0.25, 0.75));
539    }
540}