Coverage Report

Created: 2025-12-31 07:38

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/kurbo-0.13.0/src/rounded_rect.rs
Line
Count
Source
1
// Copyright 2019 the Kurbo Authors
2
// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4
//! A rectangle with rounded corners.
5
6
use core::f64::consts::{FRAC_PI_2, FRAC_PI_4};
7
use core::ops::{Add, Sub};
8
9
use 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"))]
13
use 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))]
38
pub struct RoundedRect {
39
    /// Coordinates of the rectangle.
40
    rect: Rect,
41
    /// Radius of all four corners.
42
    radii: RoundedRectRadii,
43
}
44
45
impl 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
0
    pub fn new(
51
0
        x0: f64,
52
0
        y0: f64,
53
0
        x1: f64,
54
0
        y1: f64,
55
0
        radii: impl Into<RoundedRectRadii>,
56
0
    ) -> RoundedRect {
57
0
        RoundedRect::from_rect(Rect::new(x0, y0, x1, y1), radii)
58
0
    }
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
0
    pub fn from_rect(rect: Rect, radii: impl Into<RoundedRectRadii>) -> RoundedRect {
67
0
        let rect = rect.abs();
68
0
        let shortest_side_length = (rect.width()).min(rect.height());
69
0
        let radii = radii.into().abs().clamp(shortest_side_length / 2.0);
70
71
0
        RoundedRect { rect, radii }
72
0
    }
73
74
    /// A new rectangle from two [`Point`]s.
75
    ///
76
    /// The result will have non-negative width, height and radius.
77
    #[inline]
78
0
    pub fn from_points(
79
0
        p0: impl Into<Point>,
80
0
        p1: impl Into<Point>,
81
0
        radii: impl Into<RoundedRectRadii>,
82
0
    ) -> RoundedRect {
83
0
        Rect::from_points(p0, p1).to_rounded_rect(radii)
84
0
    }
85
86
    /// A new rectangle from origin and size.
87
    ///
88
    /// The result will have non-negative width, height and radius.
89
    #[inline]
90
0
    pub fn from_origin_size(
91
0
        origin: impl Into<Point>,
92
0
        size: impl Into<Size>,
93
0
        radii: impl Into<RoundedRectRadii>,
94
0
    ) -> RoundedRect {
95
0
        Rect::from_origin_size(origin, size).to_rounded_rect(radii)
96
0
    }
97
98
    /// The width of the rectangle.
99
    #[inline]
100
0
    pub fn width(&self) -> f64 {
101
0
        self.rect.width()
102
0
    }
103
104
    /// The height of the rectangle.
105
    #[inline]
106
0
    pub fn height(&self) -> f64 {
107
0
        self.rect.height()
108
0
    }
109
110
    /// Radii of the rounded corners.
111
    #[inline(always)]
112
0
    pub fn radii(&self) -> RoundedRectRadii {
113
0
        self.radii
114
0
    }
115
116
    /// The (non-rounded) rectangle.
117
    #[inline(always)]
118
0
    pub fn rect(&self) -> Rect {
119
0
        self.rect
120
0
    }
121
122
    /// The origin of the rectangle.
123
    ///
124
    /// This is the top left corner in a y-down space.
125
    #[inline(always)]
126
0
    pub fn origin(&self) -> Point {
127
0
        self.rect.origin()
128
0
    }
129
130
    /// The center point of the rectangle.
131
    #[inline]
132
0
    pub fn center(&self) -> Point {
133
0
        self.rect.center()
134
0
    }
135
136
    /// Is this rounded rectangle finite?
137
    #[inline]
138
0
    pub const fn is_finite(&self) -> bool {
139
0
        self.rect.is_finite() && self.radii.is_finite()
140
0
    }
141
142
    /// Is this rounded rectangle NaN?
143
    #[inline]
144
0
    pub const fn is_nan(&self) -> bool {
145
0
        self.rect.is_nan() || self.radii.is_nan()
146
0
    }
147
}
148
149
#[doc(hidden)]
150
pub struct RoundedRectPathIter {
151
    idx: usize,
152
    rect: RectPathIter,
153
    arcs: [ArcAppendIter; 4],
154
}
155
156
impl Shape for RoundedRect {
157
    type PathElementsIter<'iter> = RoundedRectPathIter;
158
159
0
    fn path_elements(&self, tolerance: f64) -> RoundedRectPathIter {
160
0
        let radii = self.radii();
161
162
0
        let build_arc_iter = |i, center, ellipse_radii| {
163
0
            let arc = Arc {
164
0
                center,
165
0
                radii: ellipse_radii,
166
0
                start_angle: FRAC_PI_2 * i as f64,
167
0
                sweep_angle: FRAC_PI_2,
168
0
                x_rotation: 0.0,
169
0
            };
170
0
            arc.append_iter(tolerance)
171
0
        };
172
173
        // Note: order follows the rectangle path iterator.
174
0
        let arcs = [
175
0
            build_arc_iter(
176
0
                2,
177
0
                Point {
178
0
                    x: self.rect.x0 + radii.top_left,
179
0
                    y: self.rect.y0 + radii.top_left,
180
0
                },
181
0
                Vec2 {
182
0
                    x: radii.top_left,
183
0
                    y: radii.top_left,
184
0
                },
185
0
            ),
186
0
            build_arc_iter(
187
0
                3,
188
0
                Point {
189
0
                    x: self.rect.x1 - radii.top_right,
190
0
                    y: self.rect.y0 + radii.top_right,
191
0
                },
192
0
                Vec2 {
193
0
                    x: radii.top_right,
194
0
                    y: radii.top_right,
195
0
                },
196
0
            ),
197
0
            build_arc_iter(
198
0
                0,
199
0
                Point {
200
0
                    x: self.rect.x1 - radii.bottom_right,
201
0
                    y: self.rect.y1 - radii.bottom_right,
202
0
                },
203
0
                Vec2 {
204
0
                    x: radii.bottom_right,
205
0
                    y: radii.bottom_right,
206
0
                },
207
0
            ),
208
0
            build_arc_iter(
209
0
                1,
210
0
                Point {
211
0
                    x: self.rect.x0 + radii.bottom_left,
212
0
                    y: self.rect.y1 - radii.bottom_left,
213
0
                },
214
0
                Vec2 {
215
0
                    x: radii.bottom_left,
216
0
                    y: radii.bottom_left,
217
0
                },
218
0
            ),
219
0
        ];
220
221
0
        let rect = RectPathIter {
222
0
            rect: self.rect,
223
0
            ix: 0,
224
0
            radii,
225
0
        };
226
227
0
        RoundedRectPathIter { idx: 0, rect, arcs }
228
0
    }
229
230
    #[inline]
231
0
    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
0
        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
0
        self.rect.area()
250
0
            + [
251
0
                radii.top_left,
252
0
                radii.top_right,
253
0
                radii.bottom_right,
254
0
                radii.bottom_left,
255
0
            ]
256
0
            .iter()
257
0
            .map(|radius| (FRAC_PI_4 - 1.0) * radius * radius)
258
0
            .sum::<f64>()
259
0
    }
260
261
    #[inline]
262
0
    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
0
        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
0
        self.rect.perimeter(1.0)
283
0
            + ([
284
0
                radii.top_left,
285
0
                radii.top_right,
286
0
                radii.bottom_right,
287
0
                radii.bottom_left,
288
0
            ])
289
0
            .iter()
290
0
            .map(|radius| (-2.0 + FRAC_PI_2) * radius)
291
0
            .sum::<f64>()
292
0
    }
293
294
    #[inline]
295
0
    fn winding(&self, mut pt: Point) -> i32 {
296
0
        let center = self.center();
297
298
        // 1. Translate the point relative to the center of the rectangle.
299
0
        pt.x -= center.x;
300
0
        pt.y -= center.y;
301
302
        // 2. Pick a radius value to use based on which quadrant the point is
303
        //    in.
304
0
        let radii = self.radii();
305
0
        let radius = match pt {
306
0
            pt if pt.x < 0.0 && pt.y < 0.0 => radii.top_left,
307
0
            pt if pt.x >= 0.0 && pt.y < 0.0 => radii.top_right,
308
0
            pt if pt.x >= 0.0 && pt.y >= 0.0 => radii.bottom_right,
309
0
            pt if pt.x < 0.0 && pt.y >= 0.0 => radii.bottom_left,
310
0
            _ => 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
0
        let inside_half_width = (self.width() / 2.0 - radius).max(0.0);
317
0
        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
0
        let px = (pt.x.abs() - inside_half_width).max(0.0);
330
0
        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
0
        let inside = px * px + py * py <= radius * radius;
337
0
        if inside {
338
0
            1
339
        } else {
340
0
            0
341
        }
342
0
    }
343
344
    #[inline]
345
0
    fn bounding_box(&self) -> Rect {
346
0
        self.rect.bounding_box()
347
0
    }
348
349
    #[inline(always)]
350
0
    fn as_rounded_rect(&self) -> Option<RoundedRect> {
351
0
        Some(*self)
352
0
    }
353
}
354
355
struct 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.
362
impl Iterator for RectPathIter {
363
    type Item = PathEl;
364
365
0
    fn next(&mut self) -> Option<PathEl> {
366
0
        self.ix += 1;
367
0
        match self.ix {
368
0
            1 => Some(PathEl::MoveTo(Point::new(
369
0
                self.rect.x0,
370
0
                self.rect.y0 + self.radii.top_left,
371
0
            ))),
372
0
            2 => Some(PathEl::LineTo(Point::new(
373
0
                self.rect.x1 - self.radii.top_right,
374
0
                self.rect.y0,
375
0
            ))),
376
0
            3 => Some(PathEl::LineTo(Point::new(
377
0
                self.rect.x1,
378
0
                self.rect.y1 - self.radii.bottom_right,
379
0
            ))),
380
0
            4 => Some(PathEl::LineTo(Point::new(
381
0
                self.rect.x0 + self.radii.bottom_left,
382
0
                self.rect.y1,
383
0
            ))),
384
0
            5 => Some(PathEl::ClosePath),
385
0
            _ => None,
386
        }
387
0
    }
388
}
389
390
// This is clockwise in a y-down coordinate system for positive area.
391
impl Iterator for RoundedRectPathIter {
392
    type Item = PathEl;
393
394
0
    fn next(&mut self) -> Option<PathEl> {
395
0
        if self.idx > 4 {
396
0
            return None;
397
0
        }
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
0
        if self.idx == 0 {
404
0
            self.idx += 1;
405
0
            return self.rect.next();
406
0
        }
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
0
        match self.arcs[self.idx - 1].next() {
411
0
            Some(elem) => Some(elem),
412
            None => {
413
0
                self.idx += 1;
414
0
                self.rect.next()
415
            }
416
        }
417
0
    }
418
}
419
420
impl Add<Vec2> for RoundedRect {
421
    type Output = RoundedRect;
422
423
    #[inline]
424
0
    fn add(self, v: Vec2) -> RoundedRect {
425
0
        RoundedRect::from_rect(self.rect + v, self.radii)
426
0
    }
427
}
428
429
impl Sub<Vec2> for RoundedRect {
430
    type Output = RoundedRect;
431
432
    #[inline]
433
0
    fn sub(self, v: Vec2) -> RoundedRect {
434
0
        RoundedRect::from_rect(self.rect - v, self.radii)
435
0
    }
436
}
437
438
#[cfg(test)]
439
mod 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
}