vsvg/path/
into_bezpath.rs

1//! Trait and implementation to support flexible conversion to `kurbo::BezPath`.
2
3use crate::{Point, Polyline, DEFAULT_TOLERANCE};
4
5use kurbo::{BezPath, PathEl, PathSeg};
6
7/// Provided a tolerance, can be converted into a `BezPath`.
8pub trait IntoBezPathTolerance {
9    fn into_bezpath_with_tolerance(self, tolerance: f64) -> BezPath;
10}
11
12/// Can be converted into a `BezPath`.
13///
14/// This is blanket-implemented using [`IntoBezPathTolerance`]. Do not implement this trait
15/// directly.
16pub trait IntoBezPath {
17    fn into_bezpath(self) -> BezPath;
18}
19
20impl<T: IntoBezPathTolerance> IntoBezPath for T {
21    fn into_bezpath(self) -> BezPath {
22        <Self as IntoBezPathTolerance>::into_bezpath_with_tolerance(self, DEFAULT_TOLERANCE)
23    }
24}
25
26impl IntoBezPathTolerance for &[(f64, f64)] {
27    fn into_bezpath_with_tolerance(self, _tolerance: f64) -> BezPath {
28        points_to_bezpath(self.iter().copied())
29    }
30}
31
32impl IntoBezPathTolerance for Vec<(f64, f64)> {
33    fn into_bezpath_with_tolerance(self, _tolerance: f64) -> BezPath {
34        points_to_bezpath(self)
35    }
36}
37
38impl IntoBezPathTolerance for &[Point] {
39    fn into_bezpath_with_tolerance(self, _tolerance: f64) -> BezPath {
40        points_to_bezpath(self.iter().copied())
41    }
42}
43
44impl<const N: usize> IntoBezPathTolerance for [Point; N] {
45    fn into_bezpath_with_tolerance(self, _tolerance: f64) -> BezPath {
46        points_to_bezpath(self.iter().copied())
47    }
48}
49
50impl IntoBezPathTolerance for &Vec<Point> {
51    fn into_bezpath_with_tolerance(self, _tolerance: f64) -> BezPath {
52        points_to_bezpath(self.iter().copied())
53    }
54}
55
56impl IntoBezPathTolerance for &[(Point, Point)] {
57    fn into_bezpath_with_tolerance(self, _tolerance: f64) -> BezPath {
58        line_segment_to_bezpath(self.iter().copied())
59    }
60}
61
62impl<const N: usize> IntoBezPathTolerance for [(Point, Point); N] {
63    fn into_bezpath_with_tolerance(self, _tolerance: f64) -> BezPath {
64        line_segment_to_bezpath(self)
65    }
66}
67
68// Note: we can't use a blanket implementation on `kurbo::Shape`, so let's unroll on the available
69// types.
70
71macro_rules! kurbo_shape_into_bezpath {
72    ($t:ty) => {
73        impl IntoBezPathTolerance for $t {
74            fn into_bezpath_with_tolerance(self, tolerance: f64) -> BezPath {
75                <$t as kurbo::Shape>::into_path(self, tolerance)
76            }
77        }
78    };
79}
80
81kurbo_shape_into_bezpath!(kurbo::PathSeg);
82kurbo_shape_into_bezpath!(kurbo::Arc);
83kurbo_shape_into_bezpath!(kurbo::BezPath);
84kurbo_shape_into_bezpath!(kurbo::Circle);
85kurbo_shape_into_bezpath!(kurbo::CircleSegment);
86kurbo_shape_into_bezpath!(kurbo::CubicBez);
87kurbo_shape_into_bezpath!(kurbo::Ellipse);
88kurbo_shape_into_bezpath!(kurbo::Line);
89kurbo_shape_into_bezpath!(kurbo::QuadBez);
90kurbo_shape_into_bezpath!(kurbo::Rect);
91kurbo_shape_into_bezpath!(kurbo::RoundedRect);
92
93impl IntoBezPathTolerance for Polyline {
94    fn into_bezpath_with_tolerance(self, tolerance: f64) -> BezPath {
95        self.points().into_bezpath_with_tolerance(tolerance)
96    }
97}
98
99#[cfg(feature = "geo")]
100pub mod geo_impl {
101    #[allow(clippy::wildcard_imports)]
102    use super::*;
103
104    impl IntoBezPathTolerance for geo::Geometry<f64> {
105        fn into_bezpath_with_tolerance(self, _tolerance: f64) -> BezPath {
106            match self {
107                geo::Geometry::Point(pt) => BezPath::from_vec(vec![
108                    PathEl::MoveTo((pt.x(), pt.y()).into()),
109                    PathEl::LineTo((pt.x(), pt.y()).into()),
110                ]),
111                geo::Geometry::MultiPoint(mp) => BezPath::from_vec(
112                    mp.into_iter()
113                        .flat_map(|pt| {
114                            [
115                                PathEl::MoveTo((pt.x(), pt.y()).into()),
116                                PathEl::LineTo((pt.x(), pt.y()).into()),
117                            ]
118                        })
119                        .collect(),
120                ),
121                geo::Geometry::Line(line) => {
122                    BezPath::from_path_segments(std::iter::once(PathSeg::Line(kurbo::Line::new(
123                        (line.start.x, line.start.y),
124                        (line.end.x, line.end.y),
125                    ))))
126                }
127
128                geo::Geometry::LineString(pts) => linestring_to_path_el(pts).collect::<BezPath>(),
129                geo::Geometry::MultiLineString(mls) => mls
130                    .into_iter()
131                    .flat_map(linestring_to_path_el)
132                    .collect::<BezPath>(),
133
134                geo::Geometry::Polygon(poly) => {
135                    let (exterior, interiors) = poly.into_inner();
136
137                    linestring_to_path_el(exterior)
138                        .chain(interiors.into_iter().flat_map(linestring_to_path_el))
139                        .collect::<BezPath>()
140                }
141
142                geo::Geometry::MultiPolygon(mp) => mp
143                    .into_iter()
144                    .flat_map(|poly| {
145                        let (exterior, interiors) = poly.into_inner();
146
147                        linestring_to_path_el(exterior)
148                            .chain(interiors.into_iter().flat_map(linestring_to_path_el))
149                    })
150                    .collect::<BezPath>(),
151
152                geo::Geometry::GeometryCollection(coll) => coll
153                    .into_iter()
154                    .flat_map(IntoBezPath::into_bezpath)
155                    .collect(),
156                geo::Geometry::Rect(rect) => BezPath::from_vec(vec![
157                    PathEl::MoveTo((rect.min().x, rect.min().y).into()),
158                    PathEl::LineTo((rect.min().x, rect.max().y).into()),
159                    PathEl::LineTo((rect.max().x, rect.max().y).into()),
160                    PathEl::LineTo((rect.max().x, rect.min().y).into()),
161                    PathEl::ClosePath,
162                ]),
163                geo::Geometry::Triangle(tri) => BezPath::from_vec(vec![
164                    PathEl::MoveTo((tri.0.x, tri.0.y).into()),
165                    PathEl::LineTo((tri.1.x, tri.1.y).into()),
166                    PathEl::LineTo((tri.2.x, tri.2.y).into()),
167                    PathEl::ClosePath,
168                ]),
169            }
170        }
171    }
172
173    macro_rules! geo_object_into_bezpath {
174        ( $ t: ty) => {
175            impl IntoBezPathTolerance for $t {
176                fn into_bezpath_with_tolerance(self, tolerance: f64) -> BezPath {
177                    let geom: ::geo::Geometry = self.into();
178                    geom.into_bezpath_with_tolerance(tolerance)
179                }
180            }
181        };
182    }
183
184    geo_object_into_bezpath!(geo::Point<f64>);
185    geo_object_into_bezpath!(geo::Line<f64>);
186    geo_object_into_bezpath!(geo::LineString<f64>);
187    geo_object_into_bezpath!(geo::Polygon<f64>);
188    geo_object_into_bezpath!(geo::MultiPoint<f64>);
189    geo_object_into_bezpath!(geo::MultiLineString<f64>);
190    geo_object_into_bezpath!(geo::MultiPolygon<f64>);
191    geo_object_into_bezpath!(geo::Rect<f64>);
192    geo_object_into_bezpath!(geo::Triangle<f64>);
193
194    pub(super) fn linestring_to_path_el(ls: geo::LineString<f64>) -> impl Iterator<Item = PathEl> {
195        let closed = ls.is_closed();
196        let len = ls.0.len();
197
198        ls.into_iter().enumerate().map(move |(i, pt)| {
199            if i == 0 {
200                PathEl::MoveTo(coord_to_point(pt).into())
201            } else if i == len - 1 && closed {
202                PathEl::ClosePath
203            } else {
204                PathEl::LineTo(coord_to_point(pt).into())
205            }
206        })
207    }
208
209    #[inline]
210    pub(super) fn coord_to_point(c: geo::Coord<f64>) -> Point {
211        (c.x, c.y).into()
212    }
213}
214
215pub(crate) fn points_to_bezpath(
216    points: impl IntoIterator<Item = impl Into<Point>>,
217) -> kurbo::BezPath {
218    let mut bezpath = kurbo::BezPath::new();
219
220    let mut points = points.into_iter().map(Into::into);
221
222    if let Some(pt) = points.next() {
223        bezpath.move_to(pt);
224    }
225
226    for pt in points {
227        bezpath.line_to(pt);
228    }
229
230    bezpath
231}
232
233pub(crate) fn line_segment_to_bezpath(
234    segments: impl IntoIterator<Item = impl Into<(Point, Point)>>,
235) -> BezPath {
236    let segments = segments
237        .into_iter()
238        .map(Into::into)
239        .map(|(a, b)| kurbo::PathSeg::Line(kurbo::Line::new(a, b)));
240
241    BezPath::from_path_segments(segments)
242}
243#[cfg(test)]
244mod test {
245    use super::geo_impl::*;
246    use super::*;
247
248    #[test]
249    fn test_linestring_to_path_el() {
250        // empty case
251        assert_eq!(
252            linestring_to_path_el(geo::LineString(vec![])).collect::<Vec<_>>(),
253            vec![]
254        );
255
256        // single point, should be just a single MoveTo
257        assert_eq!(
258            linestring_to_path_el(geo::LineString(vec![(0., 0.).into()])).collect::<Vec<_>>(),
259            vec![PathEl::MoveTo((0., 0.).into())]
260        );
261
262        // three points, not closed
263        assert_eq!(
264            linestring_to_path_el(geo::LineString(vec![
265                (0., 0.).into(),
266                (1., 1.).into(),
267                (2., 2.).into()
268            ]))
269            .collect::<Vec<_>>(),
270            vec![
271                PathEl::MoveTo((0., 0.).into()),
272                PathEl::LineTo((1., 1.).into()),
273                PathEl::LineTo((2., 2.).into()),
274            ]
275        );
276
277        // three points, closed
278        assert_eq!(
279            linestring_to_path_el(geo::LineString(vec![
280                (0., 0.).into(),
281                (1., 1.).into(),
282                (2., 2.).into(),
283                (0., 0.).into(),
284            ]))
285            .collect::<Vec<_>>(),
286            vec![
287                PathEl::MoveTo((0., 0.).into()),
288                PathEl::LineTo((1., 1.).into()),
289                PathEl::LineTo((2., 2.).into()),
290                PathEl::ClosePath,
291            ]
292        );
293
294        // three points, closed
295        let mut ls = geo::LineString(vec![(0., 0.).into(), (1., 1.).into(), (2., 2.).into()]);
296        ls.close();
297
298        assert_eq!(
299            linestring_to_path_el(ls).collect::<Vec<_>>(),
300            vec![
301                PathEl::MoveTo((0., 0.).into()),
302                PathEl::LineTo((1., 1.).into()),
303                PathEl::LineTo((2., 2.).into()),
304                PathEl::ClosePath,
305            ]
306        );
307    }
308
309    #[test]
310    fn test_points_to_bezpath() {
311        let points = vec![[0.0, 0.0], [10.0, 12.0], [1.0, 2.0]];
312
313        assert_eq!(
314            points_to_bezpath(points),
315            BezPath::from_vec(vec![
316                PathEl::MoveTo(kurbo::Point::new(0.0, 0.0)),
317                PathEl::LineTo(kurbo::Point::new(10.0, 12.0)),
318                PathEl::LineTo(kurbo::Point::new(1.0, 2.0))
319            ])
320        );
321    }
322
323    #[test]
324    fn test_points_to_bezpath_empty() {
325        let points: [Point; 0] = [];
326        assert!(points_to_bezpath(points).is_empty());
327
328        let points = [Point::new(0.0, 0.0)];
329        assert!(points_to_bezpath(points).is_empty());
330    }
331
332    #[test]
333    fn test_line_segments_to_bezpath() {
334        let segs = [
335            (Point::new(0.0, 0.0), Point::new(10.0, 12.0)),
336            (Point::new(1.0, 2.0), Point::new(3.0, 4.0)),
337        ];
338        let bezpath = line_segment_to_bezpath(segs);
339
340        assert_eq!(
341            bezpath,
342            BezPath::from_vec(vec![
343                PathEl::MoveTo(kurbo::Point::new(0.0, 0.0)),
344                PathEl::LineTo(kurbo::Point::new(10.0, 12.0)),
345                PathEl::MoveTo(kurbo::Point::new(1.0, 2.0)),
346                PathEl::LineTo(kurbo::Point::new(3.0, 4.0))
347            ])
348        );
349    }
350}