Skip to main content

path_offset/path/conversions/
flo_curves.rs

1//! Provides conversions to and from `flo_curves` path types.
2//!
3//! This module allows for interoperability with the `flo_curves` library by converting
4//! between this crate's [`Path`](crate::path::Path) and `flo_curves`'s `SimpleBezierPath`
5//! and `Vec<Curve<Coord2>>`. This is essential for leveraging `flo_curves`'s path
6//! manipulation algorithms.
7
8use flo_curves::{
9    BezierCurve, Coord2, Coordinate,
10    bezier::{
11        Curve,
12        path::{BezierPathBuilder, SimpleBezierPath},
13    },
14};
15use lyon::path::Event;
16
17use crate::path::point::PointConvert;
18
19/// Converts a reference to a [`Path`](crate::path::Path) into a `flo_curves::SimpleBezierPath`.
20///
21/// This conversion processes the `lyon::path::Event` stream of the input path:
22/// - `Event::Line`, `Event::Cubic`: Translated directly to `flo_curves` equivalents.
23/// - `Event::Quadratic`: Mathematically converted into a cubic Bézier curve, as
24///   `flo_curves` primarily works with cubic curves.
25/// - `Event::End`: If the path is not marked as closed by `lyon`, a closing line segment
26///   is added to ensure the `flo_curves` path is properly closed, which is often a
27///   requirement for path algorithms.
28impl From<&crate::path::Path> for SimpleBezierPath {
29    fn from(path: &crate::path::Path) -> SimpleBezierPath {
30        let mut builder = BezierPathBuilder::<SimpleBezierPath>::start(Coord2::from((0.0, 0.0)));
31        let mut current_pos = Coord2::from((0.0, 0.0)); // Track current position
32
33        for event in path.inner.iter() {
34            match event {
35                Event::Begin { at } => {
36                    let start_point = at.use_as();
37                    builder = BezierPathBuilder::start(start_point);
38                    current_pos = start_point;
39                }
40                Event::Line { to, .. } => {
41                    let to_point = to.use_as();
42                    builder = builder.line_to(to_point);
43                    current_pos = to_point;
44                }
45                Event::Quadratic { ctrl, to, .. } => {
46                    // Convert quadratic Bézier to cubic control points
47                    let cp1: Coord2 =
48                        current_pos + (ctrl.use_as::<Coord2>() - current_pos) * (2.0 / 3.0);
49                    let cp2: Coord2 = to.use_as::<Coord2>()
50                        + (ctrl.use_as::<Coord2>() - to.use_as::<Coord2>()) * (2.0 / 3.0);
51
52                    let to_point = to.use_as();
53                    builder = builder.curve_to((cp1, cp2), to_point);
54                    current_pos = to_point;
55                }
56                Event::Cubic {
57                    ctrl1, ctrl2, to, ..
58                } => {
59                    let to_point = to.use_as();
60                    builder = builder.curve_to((ctrl1.use_as(), ctrl2.use_as()), to_point);
61                    current_pos = to_point;
62                }
63                Event::End { first, close, .. } => {
64                    // Manually add a closing line segment only if lyon reports the path as open.
65                    if !close {
66                        // Also check to avoid adding a minuscule line due to floating point errors.
67                        if current_pos.distance_to(&first.use_as()) > 1e-6 {
68                            builder = builder.line_to(first.use_as());
69                        }
70                    }
71                    // If `close` is true, do nothing, as the path is already perfectly closed.
72                }
73            }
74        }
75
76        builder.build()
77    }
78}
79
80/// Converts a vector of `flo_curves::Curve`s into a [`Path`](crate::path::Path).
81///
82/// Each `Curve` is assumed to be a cubic Bézier segment. The conversion creates a
83/// new `Path` where each curve becomes a separate, unclosed subpath consisting of a
84/// single cubic Bézier segment.
85impl From<&Vec<Curve<Coord2>>> for crate::path::Path {
86    fn from(value: &Vec<Curve<Coord2>>) -> Self {
87        let mut builder = lyon::path::Path::builder();
88
89        let mut points = vec![];
90        for curve in value {
91            let start_point = curve.start_point();
92            let end_point = curve.end_point();
93            let (ctrl1, ctrl2) = curve.control_points();
94
95            points.push((
96                start_point.use_as(),
97                ctrl1.use_as(),
98                ctrl2.use_as(),
99                end_point.use_as(),
100            ));
101        }
102
103        for (start, ctrl1, ctrl2, end) in points {
104            builder.begin(start);
105            builder.cubic_bezier_to(ctrl1, ctrl2, end);
106            builder.end(false);
107        }
108
109        Self {
110            inner: builder.build(),
111        }
112    }
113}
114
115/// Converts a `flo_curves::SimpleBezierPath` back into a [`Path`](crate::path::Path).
116///
117/// This reconstructs a `lyon` path from the `flo_curves` representation. It handles
118/// both lines and cubic curves. The resulting path is explicitly closed by adding a
119/// line segment back to the start point and calling `close()`.
120impl From<&SimpleBezierPath> for crate::path::Path {
121    fn from(value: &SimpleBezierPath) -> Self {
122        let (start_point, segments) = value;
123        let mut builder = lyon::path::Path::builder();
124
125        // Begin path at the start point
126        builder.begin(start_point.use_as());
127
128        // Track last point for later closure
129        let mut last_point = start_point;
130
131        for (ctrl1, ctrl2, to) in segments {
132            if ctrl1.is_nan() || ctrl2.is_nan() || to.is_nan() {
133                continue;
134            }
135
136            // A line is represented in SimpleBezierPath where control points align with endpoints.
137            let is_line = ctrl1 == last_point && ctrl2 == to;
138
139            if is_line {
140                builder.line_to(to.use_as());
141            } else {
142                builder.cubic_bezier_to(ctrl1.use_as(), ctrl2.use_as(), to.use_as());
143            }
144
145            last_point = to;
146        }
147
148        // Close the path by returning to the start point.
149        builder.line_to(start_point.use_as());
150        builder.close();
151
152        Self {
153            inner: builder.build(),
154        }
155    }
156}