vsvg/
crop.rs

1use arrayvec::ArrayVec;
2use kurbo::{CubicBez, Line, Point, QuadBez};
3use std::ops::Range;
4
5//TODO: remove dependency on lyon_geom by reimplementing the x-axis intersection code.
6//TODO: implement cropping for QuadBez
7
8fn k2l_cubic(bez: CubicBez) -> lyon_geom::CubicBezierSegment<f64> {
9    lyon_geom::CubicBezierSegment {
10        from: lyon_geom::point(bez.p0.x, bez.p0.y),
11        ctrl1: lyon_geom::point(bez.p1.x, bez.p1.y),
12        ctrl2: lyon_geom::point(bez.p2.x, bez.p2.y),
13        to: lyon_geom::point(bez.p3.x, bez.p3.y),
14    }
15}
16
17fn l2k_cubic(cbs: lyon_geom::CubicBezierSegment<f64>) -> CubicBez {
18    CubicBez {
19        p0: Point::new(cbs.from.x, cbs.from.y),
20        p1: Point::new(cbs.ctrl1.x, cbs.ctrl1.y),
21        p2: Point::new(cbs.ctrl2.x, cbs.ctrl2.y),
22        p3: Point::new(cbs.to.x, cbs.to.y),
23    }
24}
25
26const EPSILON: f64 = 10. * f64::EPSILON;
27
28/// Implement cropability for a geometric type.
29pub trait Crop<const N: usize>
30where
31    Self: Sized,
32{
33    /// Crop the geometry to a given rectangle.
34    fn crop(self, x_min: f64, y_min: f64, x_max: f64, y_max: f64) -> Vec<Self> {
35        self.crop_x(x_min, false)
36            .into_iter()
37            .flat_map(|bez| bez.crop_x(x_max, true))
38            .flat_map(|bez| bez.crop_y(y_min, false))
39            .flat_map(|bez| bez.crop_y(y_max, true))
40            .collect()
41    }
42
43    /// Transpose the geometry axes. This is used to provide a default implementation of `crop_y`
44    /// using `crop_x`.
45    #[must_use]
46    fn transpose_axes(self) -> Self;
47
48    /// Crop the geometry to a given vertically-split, half-plane defined with a X coordinate.
49    fn crop_x(self, x: f64, keep_smaller: bool) -> ArrayVec<Self, N>;
50
51    /// Crop the geometry to a given horizontally-split, half-plane defined with a X coordinate.
52    fn crop_y(self, y: f64, keep_smaller: bool) -> ArrayVec<Self, N> {
53        self.transpose_axes()
54            .crop_x(y, keep_smaller)
55            .into_iter()
56            .map(Self::transpose_axes)
57            .collect()
58    }
59}
60
61impl Crop<2> for Line {
62    fn transpose_axes(self) -> Self {
63        Line::new(
64            Point::new(self.p0.y, self.p0.x),
65            Point::new(self.p1.y, self.p1.x),
66        )
67    }
68
69    fn crop_x(self, x: f64, keep_smaller: bool) -> ArrayVec<Self, 2> {
70        let line = lyon_geom::LineSegment {
71            from: lyon_geom::point(self.p0.x, self.p0.y),
72            to: lyon_geom::point(self.p1.x, self.p1.y),
73        };
74
75        let mut out = ArrayVec::new();
76
77        #[allow(clippy::float_cmp)]
78        if let Some(mut t) = line.line_intersection_t(&lyon_geom::Line {
79            point: lyon_geom::point(x, 0.),
80            vector: lyon_geom::vector(0., 1.),
81        }) {
82            if t < EPSILON {
83                t = 0.;
84            } else if t > 1. - EPSILON {
85                t = 1.;
86            }
87
88            let first_in = (line.from.x < x) == keep_smaller;
89            let last_in = (line.to.x < x) == keep_smaller;
90
91            if t == 0. {
92                if last_in {
93                    out.push(self);
94                }
95            } else if t == 1. {
96                if first_in {
97                    out.push(self);
98                }
99            } else {
100                let p = line.from.lerp(line.to, t);
101                if first_in {
102                    out.push(Line {
103                        p0: Point {
104                            x: self.p0.x,
105                            y: self.p0.y,
106                        },
107                        p1: Point { x: p.x, y: p.y },
108                    });
109                } else {
110                    out.push(Line {
111                        p0: Point { x: p.x, y: p.y },
112                        p1: Point {
113                            x: self.p1.x,
114                            y: self.p1.y,
115                        },
116                    });
117                }
118            }
119        } else {
120            // No intersection
121            if (line.from.x < x) == keep_smaller || line.from.x == x {
122                out.push(self);
123            }
124        }
125
126        out
127    }
128}
129
130impl Crop<3> for CubicBez {
131    fn transpose_axes(self) -> Self {
132        Self {
133            p0: Point::new(self.p0.y, self.p0.x),
134            p1: Point::new(self.p1.y, self.p1.x),
135            p2: Point::new(self.p2.y, self.p2.x),
136            p3: Point::new(self.p3.y, self.p3.x),
137        }
138    }
139
140    fn crop_x(self, x: f64, keep_smaller: bool) -> ArrayVec<Self, 3> {
141        let cbs = k2l_cubic(self);
142        let mut intsct = cbs.solve_t_for_x(x);
143
144        // Strategy:
145        // - Sort intersections by increasing t.
146        // - Preprocess the intersection list by removing out-of-bound intersections. This includes
147        //   slightly in-bound intersections, effectively snapping extremities to the crop line.
148        // - Filter out intersections at extremities.
149        // - Keep only those ranges between intersections that are in the correct half-plane.
150        // - Merge contiguous ranges (happens when the curve is tangent to the crop line).
151
152        intsct.sort_by(|a, b| a.partial_cmp(b).unwrap());
153        let mut prev_t = 0.;
154        let keep_range: ArrayVec<Range<_>, 4> = intsct
155            .as_slice()
156            .iter()
157            .copied()
158            .filter(|&t| t > EPSILON && t < 1.0 - EPSILON)
159            .chain([1.0])
160            .filter_map(|t| {
161                let res = if (cbs.sample((prev_t + t) * 0.5).x < x) == keep_smaller {
162                    Some(prev_t..t)
163                } else {
164                    None
165                };
166                prev_t = t;
167                res
168            })
169            .collect();
170
171        // merge contiguous ranges
172        let mut merged = ArrayVec::<Range<_>, 4>::new();
173        for r in keep_range {
174            if let Some(last) = merged.last_mut() {
175                #[allow(clippy::float_cmp)]
176                if last.end == r.start {
177                    last.end = r.end;
178                    continue;
179                }
180            }
181            merged.push(r);
182        }
183
184        merged
185            .into_iter()
186            .map(|r| l2k_cubic(cbs.split_range(r)))
187            .collect()
188    }
189}
190
191pub(crate) enum QuadCropResult {
192    Quad(QuadBez),
193    Cubic(Vec<CubicBez>),
194}
195
196pub(crate) fn crop_quad_bezier(
197    quad: QuadBez,
198    x_min: f64,
199    y_min: f64,
200    x_max: f64,
201    y_max: f64,
202) -> QuadCropResult {
203    // TODO: this is a massive hack, implement proper quad cropping code
204
205    let cubic = CubicBez {
206        p0: quad.p0,
207        p1: quad.p0 + 2. / 3. * (quad.p1 - quad.p0),
208        p2: quad.p2 + 2. / 3. * (quad.p1 - quad.p2),
209        p3: quad.p2,
210    };
211
212    let cropped = cubic.crop(x_min, y_min, x_max, y_max);
213    if cropped.len() == 1 && cropped[0] == cubic {
214        QuadCropResult::Quad(quad)
215    } else {
216        QuadCropResult::Cubic(cropped)
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use approx::assert_abs_diff_eq;
224    use kurbo::{ParamCurve, Point};
225
226    fn assert_approx_point(a: Point, b: Point) {
227        assert_abs_diff_eq!(a.x, b.x, epsilon = 1e-10);
228        assert_abs_diff_eq!(a.y, b.y, epsilon = 1e-10);
229    }
230
231    fn assert_line_eq(actual: &[Line], expected: &[Line]) {
232        assert_eq!(actual.len(), expected.len());
233        for (a, b) in actual.iter().zip(expected.iter()) {
234            assert_approx_point(a.p0, b.p0);
235            assert_approx_point(a.p1, b.p1);
236        }
237    }
238
239    fn assert_bezvec_eq(actual: &[CubicBez], expected: &[CubicBez]) {
240        assert_eq!(actual.len(), expected.len());
241        for (a, b) in actual.iter().zip(expected.iter()) {
242            assert_approx_point(a.p0, b.p0);
243            assert_approx_point(a.p1, b.p1);
244            assert_approx_point(a.p2, b.p2);
245            assert_approx_point(a.p3, b.p3);
246        }
247    }
248
249    #[test]
250    fn test_crop_line() {
251        let line = Line::new(Point::new(-2., -2.), Point::new(1., 1.));
252
253        assert_line_eq(
254            line.crop_x(0., true).as_slice(),
255            &[Line::new(Point::new(-2., -2.), Point::new(0., 0.))],
256        );
257        assert_line_eq(
258            line.crop_x(0., false).as_slice(),
259            &[Line::new(Point::new(0., 0.), Point::new(1., 1.))],
260        );
261
262        assert_line_eq(line.crop_x(-2., true).as_slice(), &[]);
263        assert_line_eq(line.crop_x(-2., false).as_slice(), &[line]);
264
265        assert_line_eq(line.crop_x(1., false).as_slice(), &[]);
266        assert_line_eq(line.crop_x(1., true).as_slice(), &[line]);
267    }
268
269    #[test]
270    fn test_crop_line_colinear() {
271        let line = Line::new(Point::new(0., 0.), Point::new(0., 5.));
272
273        assert_line_eq(line.crop_x(0., true).as_slice(), &[line]);
274        assert_line_eq(line.crop_x(0., false).as_slice(), &[line]);
275
276        assert_line_eq(line.crop_x(5., true).as_slice(), &[line]);
277        assert_line_eq(line.crop_x(5., false).as_slice(), &[]);
278
279        assert_line_eq(line.crop_x(-5., false).as_slice(), &[line]);
280        assert_line_eq(line.crop_x(-5., true).as_slice(), &[]);
281    }
282
283    #[test]
284    fn test_crop_x_symmetrical_s_bezier() {
285        // S shaped bezier along x, with symmetrical control points
286        // start, ends and mid-point at x = 0
287        let bez = CubicBez::new(
288            Point::new(0.0, 0.0),
289            Point::new(-5.0, 1.0),
290            Point::new(5.0, 2.0),
291            Point::new(0.0, 3.0),
292        );
293        let cbs = k2l_cubic(bez);
294
295        // far off cut
296        assert_bezvec_eq(bez.crop_x(50., true).as_slice(), &[bez.subsegment(0.0..1.)]);
297        assert_bezvec_eq(bez.crop_x(50., false).as_slice(), &[]);
298
299        // symmetrical cut
300        assert_bezvec_eq(bez.crop_x(0., true).as_slice(), &[bez.subsegment(0.0..0.5)]);
301        assert_bezvec_eq(bez.crop_x(0., false).as_slice(), &[bez.subsegment(0.5..1.)]);
302
303        // tangent cuts
304        let (x_min, x_max) = cbs.bounding_range_x();
305        assert_bezvec_eq(
306            bez.crop_x(x_max, true).as_slice(),
307            &[bez.subsegment(0.0..1.)],
308        );
309
310        assert_bezvec_eq(bez.crop_x(x_max, false).as_slice(), &[]);
311        assert_bezvec_eq(
312            bez.crop_x(x_min, false).as_slice(),
313            &[bez.subsegment(0.0..1.)],
314        );
315        assert_bezvec_eq(bez.crop_x(x_min, true).as_slice(), &[]);
316    }
317}