use crate::{arc::ArcAppendIter, Arc, PathEl, Point, Rect, RoundedRectRadii, Shape, Size, Vec2};
use std::f64::consts::{FRAC_PI_2, FRAC_PI_4};
#[derive(Clone, Copy, Default, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RoundedRect {
rect: Rect,
radii: RoundedRectRadii,
}
impl RoundedRect {
#[inline]
pub fn new(
x0: f64,
y0: f64,
x1: f64,
y1: f64,
radii: impl Into<RoundedRectRadii>,
) -> RoundedRect {
RoundedRect::from_rect(Rect::new(x0, y0, x1, y1), radii)
}
#[inline]
pub fn from_rect(rect: Rect, radii: impl Into<RoundedRectRadii>) -> RoundedRect {
let rect = rect.abs();
let shortest_side_length = (rect.width()).min(rect.height());
let radii = radii.into().abs().clamp(shortest_side_length / 2.0);
RoundedRect { rect, radii }
}
#[inline]
pub fn from_points(
p0: impl Into<Point>,
p1: impl Into<Point>,
radii: impl Into<RoundedRectRadii>,
) -> RoundedRect {
Rect::from_points(p0, p1).to_rounded_rect(radii)
}
#[inline]
pub fn from_origin_size(
origin: impl Into<Point>,
size: impl Into<Size>,
radii: impl Into<RoundedRectRadii>,
) -> RoundedRect {
Rect::from_origin_size(origin, size).to_rounded_rect(radii)
}
#[inline]
pub fn width(&self) -> f64 {
self.rect.width()
}
#[inline]
pub fn height(&self) -> f64 {
self.rect.height()
}
#[inline]
pub fn radii(&self) -> RoundedRectRadii {
self.radii
}
pub fn rect(&self) -> Rect {
self.rect
}
#[inline]
pub fn origin(&self) -> Point {
self.rect.origin()
}
#[inline]
pub fn center(&self) -> Point {
self.rect.center()
}
#[inline]
pub fn is_finite(&self) -> bool {
self.rect.is_finite() && self.radii.is_finite()
}
#[inline]
pub fn is_nan(&self) -> bool {
self.rect.is_nan() || self.radii.is_nan()
}
}
#[doc(hidden)]
pub struct RoundedRectPathIter {
idx: usize,
rect: RectPathIter,
arcs: [ArcAppendIter; 4],
}
impl Shape for RoundedRect {
type PathElementsIter = RoundedRectPathIter;
fn path_elements(&self, tolerance: f64) -> RoundedRectPathIter {
let radii = self.radii();
let build_arc_iter = |i, center, ellipse_radii| {
let arc = Arc {
center,
radii: ellipse_radii,
start_angle: FRAC_PI_2 * i as f64,
sweep_angle: FRAC_PI_2,
x_rotation: 0.0,
};
arc.append_iter(tolerance)
};
let arcs = [
build_arc_iter(
2,
Point {
x: self.rect.x0 + radii.top_left,
y: self.rect.y0 + radii.top_left,
},
Vec2 {
x: radii.top_left,
y: radii.top_left,
},
),
build_arc_iter(
3,
Point {
x: self.rect.x1 - radii.top_right,
y: self.rect.y0 + radii.top_right,
},
Vec2 {
x: radii.top_right,
y: radii.top_right,
},
),
build_arc_iter(
0,
Point {
x: self.rect.x1 - radii.bottom_right,
y: self.rect.y1 - radii.bottom_right,
},
Vec2 {
x: radii.bottom_right,
y: radii.bottom_right,
},
),
build_arc_iter(
1,
Point {
x: self.rect.x0 + radii.bottom_left,
y: self.rect.y1 - radii.bottom_left,
},
Vec2 {
x: radii.bottom_left,
y: radii.bottom_left,
},
),
];
let rect = RectPathIter {
rect: self.rect,
ix: 0,
radii,
};
RoundedRectPathIter { idx: 0, rect, arcs }
}
#[inline]
fn area(&self) -> f64 {
let radii = self.radii();
self.rect.area()
+ [
radii.top_left,
radii.top_right,
radii.bottom_right,
radii.bottom_left,
]
.iter()
.map(|radius| (FRAC_PI_4 - 1.0) * radius * radius)
.sum::<f64>()
}
#[inline]
fn perimeter(&self, _accuracy: f64) -> f64 {
let radii = self.radii();
self.rect.perimeter(1.0)
+ ([
radii.top_left,
radii.top_right,
radii.bottom_right,
radii.bottom_left,
])
.iter()
.map(|radius| (-2.0 + FRAC_PI_2) * radius)
.sum::<f64>()
}
#[inline]
fn winding(&self, mut pt: Point) -> i32 {
let center = self.center();
pt.x -= center.x;
pt.y -= center.y;
let radii = self.radii();
let radius = match pt {
pt if pt.x < 0.0 && pt.y < 0.0 => radii.top_left,
pt if pt.x >= 0.0 && pt.y < 0.0 => radii.top_right,
pt if pt.x >= 0.0 && pt.y >= 0.0 => radii.bottom_right,
pt if pt.x < 0.0 && pt.y >= 0.0 => radii.bottom_left,
_ => 0.0,
};
let inside_half_width = (self.width() / 2.0 - radius).max(0.0);
let inside_half_height = (self.height() / 2.0 - radius).max(0.0);
let px = (pt.x.abs() - inside_half_width).max(0.0);
let py = (pt.y.abs() - inside_half_height).max(0.0);
let inside = px * px + py * py <= radius * radius;
if inside {
1
} else {
0
}
}
#[inline]
fn bounding_box(&self) -> Rect {
self.rect.bounding_box()
}
#[inline]
fn as_rounded_rect(&self) -> Option<RoundedRect> {
Some(*self)
}
}
struct RectPathIter {
rect: Rect,
radii: RoundedRectRadii,
ix: usize,
}
impl Iterator for RectPathIter {
type Item = PathEl;
fn next(&mut self) -> Option<PathEl> {
self.ix += 1;
match self.ix {
1 => Some(PathEl::MoveTo(Point::new(
self.rect.x0,
self.rect.y0 + self.radii.top_left,
))),
2 => Some(PathEl::LineTo(Point::new(
self.rect.x1 - self.radii.top_right,
self.rect.y0,
))),
3 => Some(PathEl::LineTo(Point::new(
self.rect.x1,
self.rect.y1 - self.radii.bottom_right,
))),
4 => Some(PathEl::LineTo(Point::new(
self.rect.x0 + self.radii.bottom_left,
self.rect.y1,
))),
5 => Some(PathEl::ClosePath),
_ => None,
}
}
}
impl Iterator for RoundedRectPathIter {
type Item = PathEl;
fn next(&mut self) -> Option<PathEl> {
if self.idx > 4 {
return None;
}
if self.idx == 0 {
self.idx += 1;
return self.rect.next();
}
match self.arcs[self.idx - 1].next() {
Some(elem) => Some(elem),
None => {
self.idx += 1;
self.rect.next()
}
}
}
}
#[cfg(test)]
mod tests {
use crate::{Circle, Point, Rect, RoundedRect, Shape};
#[test]
fn area() {
let epsilon = 1e-9;
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
let rounded_rect = RoundedRect::new(0.0, 0.0, 100.0, 100.0, 0.0);
assert!((rect.area() - rounded_rect.area()).abs() < epsilon);
let circle = Circle::new((0.0, 0.0), 50.0);
let rounded_rect = RoundedRect::new(0.0, 0.0, 100.0, 100.0, 50.0);
assert!((circle.area() - rounded_rect.area()).abs() < epsilon);
}
#[test]
fn winding() {
let rect = RoundedRect::new(-5.0, -5.0, 10.0, 20.0, (5.0, 5.0, 5.0, 0.0));
assert_eq!(rect.winding(Point::new(0.0, 0.0)), 1);
assert_eq!(rect.winding(Point::new(-5.0, 0.0)), 1);
assert_eq!(rect.winding(Point::new(0.0, 20.0)), 1);
assert_eq!(rect.winding(Point::new(10.0, 20.0)), 0);
assert_eq!(rect.winding(Point::new(-5.0, 20.0)), 1);
assert_eq!(rect.winding(Point::new(-10.0, 0.0)), 0);
let rect = RoundedRect::new(-10.0, -20.0, 10.0, 20.0, 0.0);
assert_eq!(rect.winding(Point::new(10.0, 20.0)), 1);
}
#[test]
fn bez_conversion() {
let rect = RoundedRect::new(-5.0, -5.0, 10.0, 20.0, 5.0);
let p = rect.to_path(1e-9);
let epsilon = 1e-7;
assert!((rect.area() - p.area()).abs() < epsilon);
assert_eq!(p.winding(Point::new(0.0, 0.0)), 1);
}
}