visioncortex/path/
spline.rs

1use std::{cmp::Ordering};
2use crate::{BinaryImage, PathF64, PointF64, PathSimplifyMode};
3use super::{PathI32, smooth::SubdivideSmooth};
4
5#[derive(Debug, Default, Clone)]
6/// Series of connecting 2D Bezier Curves
7pub struct Spline {
8    /// 1+3*(num_curves) points, where the first curve is represented by the first 4 points and each subsequent curve is represented by the last point in the previous curve plus 3 points
9    /// Points are of PointF64 type.
10    pub points: Vec<PointF64>,
11}
12
13impl Spline {
14
15    /// Creates an empty spline defined by a starting point
16    pub fn new(point: PointF64) -> Self {
17        Self {
18            points: vec![point],
19        }
20    }
21
22    /// Adds a curve to the end of the spline. Takes 3 points that are the second to fourth control points of the bezier curve. Note that the first control point is taken from the last point of the previous curve.
23    pub fn add(&mut self, point2: PointF64, point3: PointF64, point4: PointF64) {
24        self.points.push(point2);
25        self.points.push(point3);
26        self.points.push(point4);
27    }
28
29    /// Returns an iterator on the vector of points on the spline
30    pub fn iter(&self) -> std::slice::Iter<PointF64> {
31        self.points.iter()
32    }
33
34    pub fn get_control_points(&self) -> Vec<&[PointF64]> {
35        self.points.iter().as_slice().windows(4).step_by(3).collect()
36    }
37
38    /// Returns the number of points on the spline
39    pub fn len(&self) -> usize {
40        self.points.len()
41    }
42
43    /// Returns the number of curves on the spline
44    pub fn num_curves(&self) -> usize {
45        if !self.points.is_empty() {(self.points.len()-1)/3} else {0}
46    }
47
48    /// Returns true if the spline contains no curve, false otherwise
49    /// A curve is defined by 4 points, so a non-empty spline should contain at least 4 points.
50    pub fn is_empty(&self) -> bool {
51        self.points.len() <= 3
52    }
53
54    /// Applies an offset to all points on the spline
55    pub fn offset(&mut self, offset: &PointF64) {
56        for path in self.points.iter_mut() {
57            path.x += offset.x;
58            path.y += offset.y;
59        }
60    }
61
62    /// Returns a spline created from image.
63    /// The following steps are performed:
64    /// 1. Convert pixels into path
65    /// 2. Simplify the path into polygon
66    /// 3. Smoothen the polygon and approximate it with a curve-fitter
67    /// 
68    /// Corner/Splice thresholds are specified in radians.
69    /// Length threshold is specified in pixels (length unit in path coordinate system).
70    pub fn from_image(
71        image: &BinaryImage, clockwise: bool, corner_threshold: f64, outset_ratio: f64,
72        segment_length: f64, max_iterations: usize, splice_threshold: f64
73    ) -> Self {
74        let path = PathI32::image_to_path(image, clockwise, PathSimplifyMode::Polygon);
75        let path = path.smooth(corner_threshold, outset_ratio, segment_length, max_iterations);
76        Self::from_path_f64(&path, splice_threshold)
77    }
78
79    /// Returns a spline by curve-fitting a path.
80    /// 
81    /// Splice threshold is specified in radians.
82    pub fn from_path_f64(path: &PathF64, splice_threshold: f64) -> Self {
83        // First locate all the splice points
84        let splice_points = SubdivideSmooth::find_splice_points(&path, splice_threshold);
85        let path = &path.path[0..path.len()-1];
86        let len = path.len();
87        if len<=1 {
88            return Self::new(PointF64 {x:0.0,y:0.0});
89        }
90        if len==2 {
91            let mut result = Self::new(path[0]);
92            result.add(path[1], path[1], path[1]);
93            return result;
94        }
95
96        // This vector stores the indices
97        let mut cut_points: Vec<usize> = splice_points.iter()
98            .enumerate()
99            .filter(|(_, &cut)| {cut})
100            .map(|(i, _)| {i})
101            .collect();
102
103        if cut_points.is_empty() {
104            cut_points.push(0);
105        }
106        if cut_points.len() == 1 {
107            cut_points.push((cut_points[0]+len/2)%len);
108        }
109        let num_cut_points = cut_points.len();
110
111        let mut result = Self::new(PointF64 {x:0.0,y:0.0}); // Dummy initialization
112        for i in 0..num_cut_points {
113            let j = (i+1)%num_cut_points;
114
115            let current = cut_points[i];
116            let next = cut_points[j];
117            let subpath = Self::get_circular_subpath(path, current, next);
118            let bezier_points = SubdivideSmooth::fit_points_with_bezier(&subpath);
119
120            // Only the first curve need to add the first point
121            if i==0 {
122                result = Self::new(bezier_points[0]);
123            }
124            // Subsequent curves take their first point from previous curve's last point
125            result.add(bezier_points[1], bezier_points[2], bezier_points[3]);
126        }
127
128        result
129    }
130
131    /// Converts spline to svg path. Panic if the length of spline is not valid (not 1+3n for some integer n)
132    pub fn to_svg_string(&self, close: bool, offset: &PointF64, precision: Option<u32>) -> String {
133
134        let o = offset;
135
136        if self.is_empty() {
137            return String::from("");
138        }
139
140        if (self.len() - 1) % 3 != 0 {
141            panic!("Invalid spline! Length must be 1+3n.");
142        }
143
144        let points = &self.points;
145        let len = points.len();
146        let mut result: Vec<String> = vec![format!("M{} {} ", PointF64::number_format(points[0].x + o.x, precision), PointF64::number_format(points[0].y + o.y, precision))];
147
148        let mut i = 1;
149        while i < len {
150            result.push(
151                format!("C{} {} {} {} {} {} ",
152                PointF64::number_format(points[i].x + o.x, precision), PointF64::number_format(points[i].y + o.y, precision),
153                PointF64::number_format(points[i+1].x + o.x, precision), PointF64::number_format(points[i+1].y + o.y, precision),
154                PointF64::number_format(points[i+2].x + o.x, precision), PointF64::number_format(points[i+2].y + o.y, precision))
155            );
156            i += 3;
157        }
158
159        if close {
160            result.push(String::from("Z "));
161        }
162
163        result.concat()
164    }
165
166    fn get_circular_subpath(path: &[PointF64], from: usize, to: usize) -> Vec<PointF64> {
167
168        let len = path.len();
169        let mut subpath: Vec<PointF64> = vec![];
170    
171        match from.cmp(&to) {
172            Ordering::Less => {
173                subpath.extend_from_slice(&path[from..=to]);
174            },
175            Ordering::Greater => {
176                subpath.extend_from_slice(&path[from..len]);
177                subpath.extend_from_slice(&path[0..=to]);
178            },
179            Ordering:: Equal => {}
180        }
181        
182        subpath
183    }
184
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_spline_to_svg() {
193        let spline = Spline {
194            points: vec![
195                PointF64 { x: 2.22, y: 2.67 },
196                PointF64 { x: 3.50, y: 3.48 },
197                PointF64 { x: 4.19, y: 4.72 },
198                PointF64 { x: 5.68, y: 5.26 },
199            ]
200        };
201        assert_eq!(
202            spline.to_svg_string(false, &PointF64 { x: 0.0, y: 0.0 }, None),
203            "M2.22 2.67 C3.5 3.48 4.19 4.72 5.68 5.26 ".to_owned()
204        );
205        assert_eq!(
206            spline.to_svg_string(false, &PointF64 { x: 0.0, y: 0.0 }, Some(1)),
207            "M2.2 2.7 C3.5 3.5 4.2 4.7 5.7 5.3 ".to_owned()
208        );
209        assert_eq!(
210            spline.to_svg_string(false, &PointF64 { x: 0.0, y: 0.0 }, Some(0)),
211            "M2 3 C4 3 4 5 6 5 ".to_owned()
212        );
213    }
214}