use lyon_path::{
geom::{euclid, Angle, Vector},
traits::PathBuilder,
ArcFlags, Attributes, Polygon, Winding,
};
pub type Point = euclid::default::Point2D<f32>;
pub fn add_rounded_polygon<B: PathBuilder>(
builder: &mut B,
polygon: Polygon<Point>,
radius: f32,
attributes: Attributes,
) {
if polygon.points.len() < 2 {
return;
}
let clamped_radius = clamp_radius(
radius,
polygon.points[polygon.points.len() - 1],
polygon.points[0],
polygon.points[1],
);
let q_first = get_point_between(polygon.points[0], polygon.points[1], clamped_radius);
builder.begin(q_first, attributes);
for index in 0..polygon.points.len() {
let p_current = polygon.points[index];
let p_next = polygon.points[(index + 1) % polygon.points.len()];
let p_after_next = polygon.points[(index + 2) % polygon.points.len()];
let clamped_radius = clamp_radius(radius, p_current, p_next, p_after_next);
let q1 = get_point_between(p_next, p_current, clamped_radius);
let q2 = get_point_between(p_next, p_after_next, clamped_radius);
builder.line_to(q1, attributes);
let turn_winding = get_winding(p_current, p_next, p_after_next);
arc(
builder,
Vector::new(clamped_radius, clamped_radius),
Angle { radians: 0.0 },
ArcFlags {
large_arc: false,
sweep: turn_winding == Winding::Negative,
},
q1,
q2,
attributes,
);
}
builder.end(polygon.closed);
}
fn clamp_radius(radius: f32, p_previous: Point, p_current: Point, p_next: Point) -> f32 {
let shorter_edge = ((p_current - p_next).length()).min((p_previous - p_current).length());
radius.min(shorter_edge * 0.5)
}
fn get_point_between(p1: Point, p2: Point, radius: f32) -> Point {
let dist = p1.distance_to(p2);
let ratio = radius / dist;
p1.lerp(p2, ratio)
}
fn get_winding(p0: Point, p1: Point, p2: Point) -> Winding {
let cross = (p2 - p0).cross(p1 - p0);
if cross.is_sign_positive() {
Winding::Positive
} else {
Winding::Negative
}
}
fn arc<B: PathBuilder>(
builder: &mut B,
radii: Vector<f32>,
x_rotation: Angle<f32>,
flags: ArcFlags,
from: Point,
to: Point,
attributes: Attributes,
) {
let svg_arc = lyon_path::geom::SvgArc {
from,
to,
radii,
x_rotation,
flags,
};
if svg_arc.is_straight_line() {
builder.line_to(to, attributes);
} else {
let geom_arc = svg_arc.to_arc();
geom_arc.for_each_quadratic_bezier(&mut |curve| {
builder.quadratic_bezier_to(curve.ctrl, curve.to, attributes);
});
}
}
#[test]
fn rounded_polygon() {
use crate::geom::point;
use crate::rounded_polygon::*;
use alloc::vec::Vec;
use euclid::approxeq::ApproxEq;
type Point = euclid::Point2D<f32, euclid::UnknownUnit>;
type Event = path::Event<Point, Point>;
let arrow_points = [
point(-1.0, -0.3),
point(0.0, -0.3),
point(0.0, -1.0),
point(1.5, 0.0),
point(0.0, 1.0),
point(0.0, 0.3),
point(-1.0, 0.3),
];
let arrow_polygon = Polygon {
points: &arrow_points,
closed: true,
};
let mut builder = lyon_path::Path::builder();
add_rounded_polygon(&mut builder, arrow_polygon, 0.2, lyon_path::NO_ATTRIBUTES);
let arrow_path = builder.build();
let actual_events: alloc::vec::Vec<_> = arrow_path.into_iter().collect();
let actual_event_types = actual_events
.iter()
.map(|x| match x {
Event::Begin { at: _ } => "b",
Event::Line { from: _, to: _ } => "l",
Event::Quadratic {
from: _,
ctrl: _,
to: _,
} => "q",
Event::Cubic {
from: _,
ctrl1: _,
ctrl2: _,
to: _,
} => "c",
Event::End {
last: _,
first: _,
close: _,
} => "e",
})
.collect::<alloc::vec::Vec<_>>()
.concat();
assert_eq!(actual_event_types, "blqqlqqlqqlqqlqqlqqlqqe");
let expected_lines = std::vec![
(point(-0.8, -0.3), point(-0.2, -0.3)),
(point(0.0, -0.5), point(0.0, -0.8)),
(point(0.166, -0.889), point(1.333, -0.111)),
(point(1.334, 0.111), point(0.166, 0.889)),
(point(0.0, 0.8), point(0.0, 0.5)),
(point(-0.2, 0.3), point(-0.8, 0.3)),
(point(-1.0, 0.1), point(-1.0, -0.1))
];
let actual_lines: Vec<_> = arrow_path
.into_iter()
.filter_map(|event| match event {
Event::Line { from, to } => Some((from, to)),
_ => None,
})
.collect();
for (actual, expected) in actual_lines.into_iter().zip(expected_lines.into_iter()) {
for (actual_point, expected_point) in [(actual.0, expected.0), (actual.1, expected.1)] {
assert!(actual_point.approx_eq_eps(&expected_point, &Point::new(0.01, 0.01)))
}
}
let mut previous = actual_events[0].to();
for e in actual_events {
assert!(e.from().approx_eq(&previous));
previous = e.to();
}
}