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 | #[cfg (not(feature = "std" ))]
|
12 | use 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))]
|
36 | pub struct RoundedRect {
|
37 | /// Coordinates of the rectangle.
|
38 | rect: Rect,
|
39 | /// Radius of all four corners.
|
40 | radii: RoundedRectRadii,
|
41 | }
|
42 |
|
43 | impl 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)]
|
147 | pub struct RoundedRectPathIter {
|
148 | idx: usize,
|
149 | rect: RectPathIter,
|
150 | arcs: [ArcAppendIter; 4],
|
151 | }
|
152 |
|
153 | impl 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 |
|
352 | struct 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.
|
359 | impl 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.
|
388 | impl 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 |
|
417 | impl 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 |
|
426 | impl 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)]
|
436 | mod 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 | }
|
478 | |