185 lines
5.5 KiB
Rust
185 lines
5.5 KiB
Rust
use super::*;
|
|
|
|
use std::mem;
|
|
|
|
/// A rectangle with rounded corners.
|
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
|
pub struct RoundedRect {
|
|
/// The size of the rectangle.
|
|
pub size: Size,
|
|
/// The radius at each corner.
|
|
pub radius: Corners<Length>,
|
|
}
|
|
|
|
impl RoundedRect {
|
|
/// Create a new rounded rectangle.
|
|
pub fn new(size: Size, radius: Corners<Length>) -> Self {
|
|
Self { size, radius }
|
|
}
|
|
|
|
/// Output all constituent shapes of the rectangle in order. The last one is
|
|
/// in the foreground. The function will output multiple items if the stroke
|
|
/// properties differ by side.
|
|
pub fn shapes(
|
|
self,
|
|
fill: Option<Paint>,
|
|
stroke: Sides<Option<Stroke>>,
|
|
) -> Vec<Shape> {
|
|
let mut res = vec![];
|
|
if fill.is_some() || (stroke.iter().any(Option::is_some) && stroke.is_uniform()) {
|
|
res.push(Shape {
|
|
geometry: self.fill_geometry(),
|
|
fill,
|
|
stroke: if stroke.is_uniform() { stroke.top } else { None },
|
|
});
|
|
}
|
|
|
|
if !stroke.is_uniform() {
|
|
for (path, stroke) in self.stroke_segments(stroke) {
|
|
if stroke.is_some() {
|
|
res.push(Shape {
|
|
geometry: Geometry::Path(path),
|
|
fill: None,
|
|
stroke,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
res
|
|
}
|
|
|
|
/// Output the shape of the rectangle as a path or primitive rectangle,
|
|
/// depending on whether it is rounded.
|
|
fn fill_geometry(self) -> Geometry {
|
|
if self.radius.iter().copied().all(Length::is_zero) {
|
|
Geometry::Rect(self.size)
|
|
} else {
|
|
let mut paths = self.stroke_segments(Sides::splat(None));
|
|
assert_eq!(paths.len(), 1);
|
|
Geometry::Path(paths.pop().unwrap().0)
|
|
}
|
|
}
|
|
|
|
/// Output the minimum number of paths along the rectangles border.
|
|
fn stroke_segments(
|
|
self,
|
|
strokes: Sides<Option<Stroke>>,
|
|
) -> Vec<(Path, Option<Stroke>)> {
|
|
let mut res = vec![];
|
|
|
|
let mut connection = Connection::default();
|
|
let mut path = Path::new();
|
|
let mut always_continuous = true;
|
|
|
|
for side in [Side::Top, Side::Right, Side::Bottom, Side::Left] {
|
|
let continuous = strokes.get(side) == strokes.get(side.next_cw());
|
|
connection = connection.advance(continuous && side != Side::Left);
|
|
always_continuous &= continuous;
|
|
|
|
draw_side(
|
|
&mut path,
|
|
side,
|
|
self.size,
|
|
self.radius.get(side.start_corner()),
|
|
self.radius.get(side.end_corner()),
|
|
connection,
|
|
);
|
|
|
|
if !continuous {
|
|
res.push((mem::take(&mut path), strokes.get(side)));
|
|
}
|
|
}
|
|
|
|
if always_continuous {
|
|
path.close_path();
|
|
}
|
|
|
|
if !path.0.is_empty() {
|
|
res.push((path, strokes.left));
|
|
}
|
|
|
|
res
|
|
}
|
|
}
|
|
|
|
/// Draws one side of the rounded rectangle. Will always draw the left arc. The
|
|
/// right arc will be drawn halfway if and only if there is no connection.
|
|
fn draw_side(
|
|
path: &mut Path,
|
|
side: Side,
|
|
size: Size,
|
|
start_radius: Length,
|
|
end_radius: Length,
|
|
connection: Connection,
|
|
) {
|
|
let angle_left = Angle::deg(if connection.prev { 90.0 } else { 45.0 });
|
|
let angle_right = Angle::deg(if connection.next { 90.0 } else { 45.0 });
|
|
let length = size.get(side.axis());
|
|
|
|
// The arcs for a border of the rectangle along the x-axis, starting at (0,0).
|
|
let p1 = Point::with_x(start_radius);
|
|
let mut arc1 = bezier_arc(
|
|
p1 + Point::new(
|
|
-angle_left.sin() * start_radius,
|
|
(1.0 - angle_left.cos()) * start_radius,
|
|
),
|
|
Point::new(start_radius, start_radius),
|
|
p1,
|
|
);
|
|
|
|
let p2 = Point::with_x(length - end_radius);
|
|
let mut arc2 = bezier_arc(
|
|
p2,
|
|
Point::new(length - end_radius, end_radius),
|
|
p2 + Point::new(
|
|
angle_right.sin() * end_radius,
|
|
(1.0 - angle_right.cos()) * end_radius,
|
|
),
|
|
);
|
|
|
|
let transform = match side {
|
|
Side::Left => Transform::rotate(Angle::deg(-90.0))
|
|
.post_concat(Transform::translate(Length::zero(), size.y)),
|
|
Side::Bottom => Transform::rotate(Angle::deg(180.0))
|
|
.post_concat(Transform::translate(size.x, size.y)),
|
|
Side::Right => Transform::rotate(Angle::deg(90.0))
|
|
.post_concat(Transform::translate(size.x, Length::zero())),
|
|
_ => Transform::identity(),
|
|
};
|
|
|
|
arc1 = arc1.map(|x| x.transform(transform));
|
|
arc2 = arc2.map(|x| x.transform(transform));
|
|
|
|
if !connection.prev {
|
|
path.move_to(if start_radius.is_zero() { arc1[3] } else { arc1[0] });
|
|
}
|
|
|
|
if !start_radius.is_zero() {
|
|
path.cubic_to(arc1[1], arc1[2], arc1[3]);
|
|
}
|
|
|
|
path.line_to(arc2[0]);
|
|
|
|
if !connection.next && !end_radius.is_zero() {
|
|
path.cubic_to(arc2[1], arc2[2], arc2[3]);
|
|
}
|
|
}
|
|
|
|
/// Indicates which sides of the border strokes in a 2D polygon are connected to
|
|
/// their neighboring sides.
|
|
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
|
|
struct Connection {
|
|
prev: bool,
|
|
next: bool,
|
|
}
|
|
|
|
impl Connection {
|
|
/// Advance to the next clockwise side of the polygon. The argument
|
|
/// indicates whether the border is connected on the right side of the next
|
|
/// edge.
|
|
pub fn advance(self, next: bool) -> Self {
|
|
Self { prev: self.next, next }
|
|
}
|
|
}
|