lyon_algorithms/
rounded_polygon.rs

1use lyon_path::{
2    geom::{euclid, Angle, Vector},
3    traits::PathBuilder,
4    ArcFlags, Attributes, Polygon, Winding,
5};
6
7pub type Point = euclid::default::Point2D<f32>;
8
9/// Adds a sub-path from a polygon but rounds the corners.
10///
11/// There must be no sub-path in progress when this method is called.
12/// No sub-path is in progress after the method is called.
13pub fn add_rounded_polygon<B: PathBuilder>(
14    builder: &mut B,
15    polygon: Polygon<Point>,
16    radius: f32,
17    attributes: Attributes,
18) {
19    if polygon.points.len() < 2 {
20        return;
21    }
22
23    //p points are original polygon points
24    //q points are the actual points we will draw lines and arcs between
25    let clamped_radius = clamp_radius(
26        radius,
27        polygon.points[polygon.points.len() - 1],
28        polygon.points[0],
29        polygon.points[1],
30    );
31    let q_first = get_point_between(polygon.points[0], polygon.points[1], clamped_radius);
32
33    //We begin on the line just after the first point
34    builder.begin(q_first, attributes);
35
36    for index in 0..polygon.points.len() {
37        let p_current = polygon.points[index];
38        let p_next = polygon.points[(index + 1) % polygon.points.len()];
39        let p_after_next = polygon.points[(index + 2) % polygon.points.len()];
40
41        let clamped_radius = clamp_radius(radius, p_current, p_next, p_after_next);
42
43        //q1 is the second point on the line between p_current and p_next
44        let q1 = get_point_between(p_next, p_current, clamped_radius);
45        //q2 is the first point on the line between p_next and p_after_next
46        let q2 = get_point_between(p_next, p_after_next, clamped_radius);
47
48        builder.line_to(q1, attributes);
49        let turn_winding = get_winding(p_current, p_next, p_after_next);
50
51        //Draw the arc near p_next
52        arc(
53            builder,
54            Vector::new(clamped_radius, clamped_radius),
55            Angle { radians: 0.0 },
56            ArcFlags {
57                large_arc: false,
58                sweep: turn_winding == Winding::Negative,
59            },
60            q1,
61            q2,
62            attributes,
63        );
64    }
65
66    builder.end(polygon.closed);
67}
68
69fn clamp_radius(radius: f32, p_previous: Point, p_current: Point, p_next: Point) -> f32 {
70    let shorter_edge = ((p_current - p_next).length()).min((p_previous - p_current).length());
71
72    radius.min(shorter_edge * 0.5)
73}
74
75fn get_point_between(p1: Point, p2: Point, radius: f32) -> Point {
76    let dist = p1.distance_to(p2);
77    let ratio = radius / dist;
78
79    p1.lerp(p2, ratio)
80}
81
82fn get_winding(p0: Point, p1: Point, p2: Point) -> Winding {
83    let cross = (p2 - p0).cross(p1 - p0);
84    if cross.is_sign_positive() {
85        Winding::Positive
86    } else {
87        Winding::Negative
88    }
89}
90
91fn arc<B: PathBuilder>(
92    builder: &mut B,
93    radii: Vector<f32>,
94    x_rotation: Angle<f32>,
95    flags: ArcFlags,
96    from: Point,
97    to: Point,
98    attributes: Attributes,
99) {
100    let svg_arc = lyon_path::geom::SvgArc {
101        from,
102        to,
103        radii,
104        x_rotation,
105        flags,
106    };
107
108    if svg_arc.is_straight_line() {
109        builder.line_to(to, attributes);
110    } else {
111        let geom_arc = svg_arc.to_arc();
112        geom_arc.for_each_quadratic_bezier(&mut |curve| {
113            builder.quadratic_bezier_to(curve.ctrl, curve.to, attributes);
114        });
115    }
116}
117
118#[test]
119fn rounded_polygon() {
120    use crate::geom::point;
121    use crate::rounded_polygon::*;
122    use alloc::vec::Vec;
123    use euclid::approxeq::ApproxEq;
124
125    type Point = euclid::Point2D<f32, euclid::UnknownUnit>;
126    type Event = path::Event<Point, Point>;
127    let arrow_points = [
128        point(-1.0, -0.3),
129        point(0.0, -0.3),
130        point(0.0, -1.0),
131        point(1.5, 0.0),
132        point(0.0, 1.0),
133        point(0.0, 0.3),
134        point(-1.0, 0.3),
135    ];
136
137    let arrow_polygon = Polygon {
138        points: &arrow_points,
139        closed: true,
140    };
141
142    let mut builder = lyon_path::Path::builder();
143    add_rounded_polygon(&mut builder, arrow_polygon, 0.2, lyon_path::NO_ATTRIBUTES);
144    let arrow_path = builder.build();
145
146    //check that we have the right ordering of event types
147    let actual_events: alloc::vec::Vec<_> = arrow_path.into_iter().collect();
148
149    let actual_event_types = actual_events
150        .iter()
151        .map(|x| match x {
152            Event::Begin { at: _ } => "b",
153            Event::Line { from: _, to: _ } => "l",
154            Event::Quadratic {
155                from: _,
156                ctrl: _,
157                to: _,
158            } => "q",
159            Event::Cubic {
160                from: _,
161                ctrl1: _,
162                ctrl2: _,
163                to: _,
164            } => "c",
165            Event::End {
166                last: _,
167                first: _,
168                close: _,
169            } => "e",
170        })
171        .collect::<alloc::vec::Vec<_>>()
172        .concat();
173
174    assert_eq!(actual_event_types, "blqqlqqlqqlqqlqqlqqlqqe");
175
176    let expected_lines = std::vec![
177        (point(-0.8, -0.3), point(-0.2, -0.3)),
178        (point(0.0, -0.5), point(0.0, -0.8)),
179        (point(0.166, -0.889), point(1.333, -0.111)),
180        (point(1.334, 0.111), point(0.166, 0.889)),
181        (point(0.0, 0.8), point(0.0, 0.5)),
182        (point(-0.2, 0.3), point(-0.8, 0.3)),
183        (point(-1.0, 0.1), point(-1.0, -0.1))
184    ];
185
186    //Check that the lines are approximately correct
187    let actual_lines: Vec<_> = arrow_path
188        .into_iter()
189        .filter_map(|event| match event {
190            Event::Line { from, to } => Some((from, to)),
191            _ => None,
192        })
193        .collect();
194
195    for (actual, expected) in actual_lines.into_iter().zip(expected_lines.into_iter()) {
196        for (actual_point, expected_point) in [(actual.0, expected.0), (actual.1, expected.1)] {
197            assert!(actual_point.approx_eq_eps(&expected_point, &Point::new(0.01, 0.01)))
198        }
199    }
200
201    //Check that each event goes from the end of the previous event
202
203    let mut previous = actual_events[0].to();
204
205    for e in actual_events {
206        e.from().approx_eq(&previous);
207        previous = e.to();
208    }
209}