1use arrayvec::ArrayVec;
2use kurbo::{CubicBez, Line, Point, QuadBez};
3use std::ops::Range;
4
5fn 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
28pub trait Crop<const N: usize>
30where
31 Self: Sized,
32{
33 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 #[must_use]
46 fn transpose_axes(self) -> Self;
47
48 fn crop_x(self, x: f64, keep_smaller: bool) -> ArrayVec<Self, N>;
50
51 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 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 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 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 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 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 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 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 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}