1 | use crate::scalar::Scalar; |
2 | use crate::CubicBezierSegment; |
3 | ///! Computes intersection parameters for two cubic bézier curves using bézier clipping, also known |
4 | ///! as fat line clipping. |
5 | ///! |
6 | ///! The implementation here was originally ported from that of paper.js: |
7 | ///! https://github.com/paperjs/paper.js/blob/0deddebb2c83ea2a0c848f7c8ba5e22fa7562a4e/src/path/Curve.js#L2008 |
8 | ///! See "bézier Clipping method" in |
9 | ///! https://scholarsarchive.byu.edu/facpub/1/ |
10 | ///! for motivation and details of how the process works. |
11 | use crate::{point, Box2D, Point}; |
12 | use arrayvec::ArrayVec; |
13 | use core::mem; |
14 | use core::ops::Range; |
15 | |
16 | // Computes the intersections (if any) between two cubic bézier curves in the form of the `t` |
17 | // parameters of each intersection point along the curves. |
18 | // |
19 | // Returns endpoint intersections where an endpoint intersects the interior of the other curve, |
20 | // but not endpoint/endpoint intersections. |
21 | // |
22 | // Returns no intersections if either curve is a point or if the curves are parallel lines. |
23 | pub fn cubic_bezier_intersections_t<S: Scalar>( |
24 | curve1: &CubicBezierSegment<S>, |
25 | curve2: &CubicBezierSegment<S>, |
26 | ) -> ArrayVec<(S, S), 9> { |
27 | if !curve1 |
28 | .fast_bounding_box() |
29 | .intersects(&curve2.fast_bounding_box()) |
30 | || curve1 == curve2 |
31 | || (curve1.from == curve2.to |
32 | && curve1.ctrl1 == curve2.ctrl2 |
33 | && curve1.ctrl2 == curve2.ctrl1 |
34 | && curve1.to == curve2.from) |
35 | { |
36 | return ArrayVec::new(); |
37 | } |
38 | |
39 | let mut result = ArrayVec::new(); |
40 | |
41 | #[inline ] |
42 | fn midpoint<S: Scalar>(point1: &Point<S>, point2: &Point<S>) -> Point<S> { |
43 | point( |
44 | (point1.x + point2.x) * S::HALF, |
45 | (point1.y + point2.y) * S::HALF, |
46 | ) |
47 | } |
48 | |
49 | let curve1_is_a_point = curve1.is_a_point(S::EPSILON); |
50 | let curve2_is_a_point = curve2.is_a_point(S::EPSILON); |
51 | if curve1_is_a_point && !curve2_is_a_point { |
52 | let point1 = midpoint(&curve1.from, &curve1.to); |
53 | let curve_params = point_curve_intersections(&point1, curve2, S::EPSILON); |
54 | for t in curve_params { |
55 | if t > S::EPSILON && t < S::ONE - S::EPSILON { |
56 | result.push((S::ZERO, t)); |
57 | } |
58 | } |
59 | } else if !curve1_is_a_point && curve2_is_a_point { |
60 | let point2 = midpoint(&curve2.from, &curve2.to); |
61 | let curve_params = point_curve_intersections(&point2, curve1, S::EPSILON); |
62 | for t in curve_params { |
63 | if t > S::EPSILON && t < S::ONE - S::EPSILON { |
64 | result.push((t, S::ZERO)); |
65 | } |
66 | } |
67 | } |
68 | if curve1_is_a_point || curve2_is_a_point { |
69 | // Caller is always responsible for checking endpoints and overlaps, in the case that both |
70 | // curves were points. |
71 | return result; |
72 | } |
73 | |
74 | let linear1 = curve1.is_linear(S::EPSILON); |
75 | let linear2 = curve2.is_linear(S::EPSILON); |
76 | if linear1 && !linear2 { |
77 | result = line_curve_intersections(curve1, curve2, /* flip */ false); |
78 | } else if !linear1 && linear2 { |
79 | result = line_curve_intersections(curve2, curve1, /* flip */ true); |
80 | } else if linear1 && linear2 { |
81 | result = line_line_intersections(curve1, curve2); |
82 | } else { |
83 | add_curve_intersections( |
84 | curve1, |
85 | curve2, |
86 | &(S::ZERO..S::ONE), |
87 | &(S::ZERO..S::ONE), |
88 | &mut result, |
89 | /* flip */ false, |
90 | /* recursion_count */ 0, |
91 | /* call_count */ 0, |
92 | /* original curve1 */ curve1, |
93 | /* original curve2 */ curve2, |
94 | ); |
95 | } |
96 | |
97 | result |
98 | } |
99 | |
100 | fn point_curve_intersections<S: Scalar>( |
101 | pt: &Point<S>, |
102 | curve: &CubicBezierSegment<S>, |
103 | epsilon: S, |
104 | ) -> ArrayVec<S, 9> { |
105 | let mut result = ArrayVec::new(); |
106 | |
107 | // (If both endpoints are epsilon close, we only return S::ZERO.) |
108 | if (*pt - curve.from).square_length() < epsilon { |
109 | result.push(S::ZERO); |
110 | return result; |
111 | } |
112 | if (*pt - curve.to).square_length() < epsilon { |
113 | result.push(S::ONE); |
114 | return result; |
115 | } |
116 | |
117 | let curve_x_t_params = curve.solve_t_for_x(pt.x); |
118 | let curve_y_t_params = curve.solve_t_for_y(pt.y); |
119 | // We want to coalesce parameters representing the same intersection from the x and y |
120 | // directions, but the parameter calculations aren't very accurate, so give a little more |
121 | // leeway there (TODO: this isn't perfect, as you might expect - the dupes that pass here are |
122 | // currently being detected in add_intersection). |
123 | let param_eps = S::TEN * epsilon; |
124 | for params in [curve_x_t_params, curve_y_t_params].iter() { |
125 | for t in params { |
126 | let t = *t; |
127 | if (*pt - curve.sample(t)).square_length() > epsilon { |
128 | continue; |
129 | } |
130 | let mut already_found_t = false; |
131 | for u in &result { |
132 | if S::abs(t - *u) < param_eps { |
133 | already_found_t = true; |
134 | break; |
135 | } |
136 | } |
137 | if !already_found_t { |
138 | result.push(t); |
139 | } |
140 | } |
141 | } |
142 | |
143 | if !result.is_empty() { |
144 | return result; |
145 | } |
146 | |
147 | // The remaining case is if pt is within epsilon of an interior point of curve, but not within |
148 | // the x-range or y-range of the curve (which we already checked) - for example if curve is a |
149 | // horizontal line that extends beyond its endpoints, and pt is just outside an end of the line; |
150 | // or if the curve has a cusp in one of the corners of its convex hull and pt is |
151 | // diagonally just outside the hull. This is a rare case (could we even ignore it?). |
152 | #[inline ] |
153 | fn maybe_add<S: Scalar>( |
154 | t: S, |
155 | pt: &Point<S>, |
156 | curve: &CubicBezierSegment<S>, |
157 | epsilon: S, |
158 | result: &mut ArrayVec<S, 9>, |
159 | ) -> bool { |
160 | if (curve.sample(t) - *pt).square_length() < epsilon { |
161 | result.push(t); |
162 | return true; |
163 | } |
164 | false |
165 | } |
166 | |
167 | let _ = maybe_add(curve.x_minimum_t(), pt, curve, epsilon, &mut result) |
168 | || maybe_add(curve.x_maximum_t(), pt, curve, epsilon, &mut result) |
169 | || maybe_add(curve.y_minimum_t(), pt, curve, epsilon, &mut result) |
170 | || maybe_add(curve.y_maximum_t(), pt, curve, epsilon, &mut result); |
171 | |
172 | result |
173 | } |
174 | |
175 | fn line_curve_intersections<S: Scalar>( |
176 | line_as_curve: &CubicBezierSegment<S>, |
177 | curve: &CubicBezierSegment<S>, |
178 | flip: bool, |
179 | ) -> ArrayVec<(S, S), 9> { |
180 | let mut result: ArrayVec<(S, S), 9> = ArrayVec::new(); |
181 | let baseline: LineSegment = line_as_curve.baseline(); |
182 | let curve_intersections: ArrayVec = curve.line_intersections_t(&baseline.to_line()); |
183 | let line_is_mostly_vertical: bool = |
184 | S::abs(self:baseline.from.y - baseline.to.y) >= S::abs(self:baseline.from.x - baseline.to.x); |
185 | for curve_t: S in curve_intersections { |
186 | let line_intersections: ArrayVec = if line_is_mostly_vertical { |
187 | let intersection_y: S = curve.y(curve_t); |
188 | line_as_curve.solve_t_for_y(intersection_y) |
189 | } else { |
190 | let intersection_x: S = curve.x(curve_t); |
191 | line_as_curve.solve_t_for_x(intersection_x) |
192 | }; |
193 | |
194 | for line_t: S in line_intersections { |
195 | add_intersection(t1:line_t, orig_curve1:line_as_curve, t2:curve_t, orig_curve2:curve, flip, &mut result); |
196 | } |
197 | } |
198 | |
199 | result |
200 | } |
201 | |
202 | fn line_line_intersections<S: Scalar>( |
203 | curve1: &CubicBezierSegment<S>, |
204 | curve2: &CubicBezierSegment<S>, |
205 | ) -> ArrayVec<(S, S), 9> { |
206 | let mut result = ArrayVec::new(); |
207 | |
208 | let intersection = curve1 |
209 | .baseline() |
210 | .to_line() |
211 | .intersection(&curve2.baseline().to_line()); |
212 | if intersection.is_none() { |
213 | return result; |
214 | } |
215 | |
216 | let intersection = intersection.unwrap(); |
217 | |
218 | #[inline ] |
219 | fn parameters_for_line_point<S: Scalar>( |
220 | curve: &CubicBezierSegment<S>, |
221 | pt: &Point<S>, |
222 | ) -> ArrayVec<S, 3> { |
223 | let line_is_mostly_vertical = |
224 | S::abs(curve.from.y - curve.to.y) >= S::abs(curve.from.x - curve.to.x); |
225 | if line_is_mostly_vertical { |
226 | curve.solve_t_for_y(pt.y) |
227 | } else { |
228 | curve.solve_t_for_x(pt.x) |
229 | } |
230 | } |
231 | |
232 | let line1_params = parameters_for_line_point(curve1, &intersection); |
233 | if line1_params.is_empty() { |
234 | return result; |
235 | } |
236 | |
237 | let line2_params = parameters_for_line_point(curve2, &intersection); |
238 | if line2_params.is_empty() { |
239 | return result; |
240 | } |
241 | |
242 | for t1 in &line1_params { |
243 | for t2 in &line2_params { |
244 | // It could be argued that an endpoint intersection located in the interior of one |
245 | // or both curves should be returned here; we currently don't. |
246 | add_intersection(*t1, curve1, *t2, curve2, /* flip */ false, &mut result); |
247 | } |
248 | } |
249 | |
250 | result |
251 | } |
252 | |
253 | // This function implements the main bézier clipping algorithm by recursively subdividing curve1 and |
254 | // curve2 in to smaller and smaller portions of the original curves with the property that one of |
255 | // the curves intersects the fat line of the other curve at each stage. |
256 | // |
257 | // curve1 and curve2 at each stage are sub-bézier curves of the original curves; flip tells us |
258 | // whether curve1 at a given stage is a sub-curve of the original curve1 or the original curve2; |
259 | // similarly for curve2. domain1 and domain2 shrink (or stay the same) at each stage and describe |
260 | // which subdomain of an original curve the current curve1 and curve2 correspond to. (The domains of |
261 | // curve1 and curve2 are 0..1 at every stage.) |
262 | #[allow (clippy::too_many_arguments)] |
263 | fn add_curve_intersections<S: Scalar>( |
264 | curve1: &CubicBezierSegment<S>, |
265 | curve2: &CubicBezierSegment<S>, |
266 | domain1: &Range<S>, |
267 | domain2: &Range<S>, |
268 | intersections: &mut ArrayVec<(S, S), 9>, |
269 | flip: bool, |
270 | mut recursion_count: u32, |
271 | mut call_count: u32, |
272 | orig_curve1: &CubicBezierSegment<S>, |
273 | orig_curve2: &CubicBezierSegment<S>, |
274 | ) -> u32 { |
275 | call_count += 1; |
276 | recursion_count += 1; |
277 | if call_count >= 4096 || recursion_count >= 60 { |
278 | return call_count; |
279 | } |
280 | |
281 | let epsilon = if inputs_are_f32::<S>() { |
282 | S::value(5e-6) |
283 | } else { |
284 | S::value(1e-9) |
285 | }; |
286 | |
287 | if domain2.start == domain2.end || curve2.is_a_point(S::ZERO) { |
288 | add_point_curve_intersection( |
289 | curve2, |
290 | /* point is curve1 */ false, |
291 | curve1, |
292 | domain2, |
293 | domain1, |
294 | intersections, |
295 | flip, |
296 | ); |
297 | return call_count; |
298 | } else if curve2.from == curve2.to { |
299 | // There's no curve2 baseline to fat-line against (and we'll (debug) crash if we try with |
300 | // the current implementation), so split curve2 and try again. |
301 | let new_2_curves = orig_curve2.split_range(domain2.clone()).split(S::HALF); |
302 | let domain2_mid = (domain2.start + domain2.end) * S::HALF; |
303 | call_count = add_curve_intersections( |
304 | curve1, |
305 | &new_2_curves.0, |
306 | domain1, |
307 | &(domain2.start..domain2_mid), |
308 | intersections, |
309 | flip, |
310 | recursion_count, |
311 | call_count, |
312 | orig_curve1, |
313 | orig_curve2, |
314 | ); |
315 | call_count = add_curve_intersections( |
316 | curve1, |
317 | &new_2_curves.1, |
318 | domain1, |
319 | &(domain2_mid..domain2.end), |
320 | intersections, |
321 | flip, |
322 | recursion_count, |
323 | call_count, |
324 | orig_curve1, |
325 | orig_curve2, |
326 | ); |
327 | return call_count; |
328 | } |
329 | |
330 | // (Don't call this before checking for point curves: points are inexact and can lead to false |
331 | // negatives here.) |
332 | if !rectangles_overlap(&curve1.fast_bounding_box(), &curve2.fast_bounding_box()) { |
333 | return call_count; |
334 | } |
335 | |
336 | let (t_min_clip, t_max_clip) = match restrict_curve_to_fat_line(curve1, curve2) { |
337 | Some((min, max)) => (min, max), |
338 | None => return call_count, |
339 | }; |
340 | |
341 | // t_min_clip and t_max_clip are (0, 1)-based, so project them back to get the new restricted |
342 | // range: |
343 | let new_domain1 = |
344 | &(domain_value_at_t(domain1, t_min_clip)..domain_value_at_t(domain1, t_max_clip)); |
345 | |
346 | if S::max( |
347 | domain2.end - domain2.start, |
348 | new_domain1.end - new_domain1.start, |
349 | ) < epsilon |
350 | { |
351 | let t1 = (new_domain1.start + new_domain1.end) * S::HALF; |
352 | let t2 = (domain2.start + domain2.end) * S::HALF; |
353 | if inputs_are_f32::<S>() { |
354 | // There's an unfortunate tendency for curve2 endpoints that end near (but not all |
355 | // that near) to the interior of curve1 to register as intersections, so try to avoid |
356 | // that. (We could be discarding a legitimate intersection here.) |
357 | let end_eps = S::value(1e-3); |
358 | if (t2 < end_eps || t2 > S::ONE - end_eps) |
359 | && (orig_curve1.sample(t1) - orig_curve2.sample(t2)).length() > S::FIVE |
360 | { |
361 | return call_count; |
362 | } |
363 | } |
364 | add_intersection(t1, orig_curve1, t2, orig_curve2, flip, intersections); |
365 | return call_count; |
366 | } |
367 | |
368 | // Reduce curve1 to the part that might intersect curve2. |
369 | let curve1 = &orig_curve1.split_range(new_domain1.clone()); |
370 | |
371 | // (Note: it's possible for new_domain1 to have become a point, even if |
372 | // t_min_clip < t_max_clip. It's also possible for curve1 to not be a point even if new_domain1 |
373 | // is a point (but then curve1 will be very small).) |
374 | if new_domain1.start == new_domain1.end || curve1.is_a_point(S::ZERO) { |
375 | add_point_curve_intersection( |
376 | curve1, |
377 | /* point is curve1 */ true, |
378 | curve2, |
379 | new_domain1, |
380 | domain2, |
381 | intersections, |
382 | flip, |
383 | ); |
384 | return call_count; |
385 | } |
386 | |
387 | // If the new range is still 80% or more of the old range, subdivide and try again. |
388 | if t_max_clip - t_min_clip > S::EIGHT / S::TEN { |
389 | // Subdivide the curve which has converged the least. |
390 | if new_domain1.end - new_domain1.start > domain2.end - domain2.start { |
391 | let new_1_curves = curve1.split(S::HALF); |
392 | let new_domain1_mid = (new_domain1.start + new_domain1.end) * S::HALF; |
393 | call_count = add_curve_intersections( |
394 | curve2, |
395 | &new_1_curves.0, |
396 | domain2, |
397 | &(new_domain1.start..new_domain1_mid), |
398 | intersections, |
399 | !flip, |
400 | recursion_count, |
401 | call_count, |
402 | orig_curve2, |
403 | orig_curve1, |
404 | ); |
405 | call_count = add_curve_intersections( |
406 | curve2, |
407 | &new_1_curves.1, |
408 | domain2, |
409 | &(new_domain1_mid..new_domain1.end), |
410 | intersections, |
411 | !flip, |
412 | recursion_count, |
413 | call_count, |
414 | orig_curve2, |
415 | orig_curve1, |
416 | ); |
417 | } else { |
418 | let new_2_curves = orig_curve2.split_range(domain2.clone()).split(S::HALF); |
419 | let domain2_mid = (domain2.start + domain2.end) * S::HALF; |
420 | call_count = add_curve_intersections( |
421 | &new_2_curves.0, |
422 | curve1, |
423 | &(domain2.start..domain2_mid), |
424 | new_domain1, |
425 | intersections, |
426 | !flip, |
427 | recursion_count, |
428 | call_count, |
429 | orig_curve2, |
430 | orig_curve1, |
431 | ); |
432 | call_count = add_curve_intersections( |
433 | &new_2_curves.1, |
434 | curve1, |
435 | &(domain2_mid..domain2.end), |
436 | new_domain1, |
437 | intersections, |
438 | !flip, |
439 | recursion_count, |
440 | call_count, |
441 | orig_curve2, |
442 | orig_curve1, |
443 | ); |
444 | } |
445 | } else { |
446 | // Iterate. |
447 | if domain2.end - domain2.start >= epsilon { |
448 | call_count = add_curve_intersections( |
449 | curve2, |
450 | curve1, |
451 | domain2, |
452 | new_domain1, |
453 | intersections, |
454 | !flip, |
455 | recursion_count, |
456 | call_count, |
457 | orig_curve2, |
458 | orig_curve1, |
459 | ); |
460 | } else { |
461 | // The interval on curve2 is already tight enough, so just continue iterating on curve1. |
462 | call_count = add_curve_intersections( |
463 | curve1, |
464 | curve2, |
465 | new_domain1, |
466 | domain2, |
467 | intersections, |
468 | flip, |
469 | recursion_count, |
470 | call_count, |
471 | orig_curve1, |
472 | orig_curve2, |
473 | ); |
474 | } |
475 | } |
476 | |
477 | call_count |
478 | } |
479 | |
480 | fn add_point_curve_intersection<S: Scalar>( |
481 | pt_curve: &CubicBezierSegment<S>, |
482 | pt_curve_is_curve1: bool, |
483 | curve: &CubicBezierSegment<S>, |
484 | pt_domain: &Range<S>, |
485 | curve_domain: &Range<S>, |
486 | intersections: &mut ArrayVec<(S, S), 9>, |
487 | flip: bool, |
488 | ) { |
489 | let pt = pt_curve.from; |
490 | // We assume pt is curve1 when we add intersections below. |
491 | let flip = if pt_curve_is_curve1 { flip } else { !flip }; |
492 | |
493 | // Generally speaking |curve| will be quite small at this point, so see if we can get away with |
494 | // just sampling here. |
495 | |
496 | let epsilon = epsilon_for_point(&pt); |
497 | let pt_t = (pt_domain.start + pt_domain.end) * S::HALF; |
498 | |
499 | let curve_t = { |
500 | let mut t_for_min = S::ZERO; |
501 | let mut min_dist_sq = epsilon; |
502 | let tenths = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]; |
503 | for &t in tenths.iter() { |
504 | let t = S::value(t); |
505 | let d = (pt - curve.sample(t)).square_length(); |
506 | if d < min_dist_sq { |
507 | t_for_min = t; |
508 | min_dist_sq = d; |
509 | } |
510 | } |
511 | |
512 | if min_dist_sq == epsilon { |
513 | -S::ONE |
514 | } else { |
515 | let curve_t = domain_value_at_t(curve_domain, t_for_min); |
516 | curve_t |
517 | } |
518 | }; |
519 | |
520 | if curve_t != -S::ONE { |
521 | add_intersection(pt_t, pt_curve, curve_t, curve, flip, intersections); |
522 | return; |
523 | } |
524 | |
525 | // If sampling didn't work, try a different approach. |
526 | let results = point_curve_intersections(&pt, curve, epsilon); |
527 | for t in results { |
528 | let curve_t = domain_value_at_t(curve_domain, t); |
529 | add_intersection(pt_t, pt_curve, curve_t, curve, flip, intersections); |
530 | } |
531 | } |
532 | |
533 | // TODO: replace with Scalar::epsilon_for? |
534 | // If we're comparing distances between samples of curves, our epsilon should depend on how big the |
535 | // points we're comparing are. This function returns an epsilon appropriate for the size of pt. |
536 | fn epsilon_for_point<S: Scalar>(pt: &Point<S>) -> S { |
537 | let max: S = S::max(S::abs(pt.x), S::abs(self:pt.y)); |
538 | let epsilon: S = if inputs_are_f32::<S>() { |
539 | match max.to_i32().unwrap() { |
540 | 0..=9 => S::value(0.001), |
541 | 10..=99 => S::value(0.01), |
542 | 100..=999 => S::value(0.1), |
543 | 1_000..=9_999 => S::value(0.25), |
544 | 10_000..=999_999 => S::HALF, |
545 | _ => S::ONE, |
546 | } |
547 | } else { |
548 | match max.to_i64().unwrap() { |
549 | 0..=99_999 => S::EPSILON, |
550 | 100_000..=99_999_999 => S::value(1e-5), |
551 | 100_000_000..=9_999_999_999 => S::value(1e-3), |
552 | _ => S::value(1e-1), |
553 | } |
554 | }; |
555 | |
556 | epsilon |
557 | } |
558 | |
559 | fn add_intersection<S: Scalar>( |
560 | t1: S, |
561 | orig_curve1: &CubicBezierSegment<S>, |
562 | t2: S, |
563 | orig_curve2: &CubicBezierSegment<S>, |
564 | flip: bool, |
565 | intersections: &mut ArrayVec<(S, S), 9>, |
566 | ) { |
567 | let (t1, t2) = if flip { (t2, t1) } else { (t1, t2) }; |
568 | // (This should probably depend in some way on how large our input coefficients are.) |
569 | let epsilon = if inputs_are_f32::<S>() { |
570 | S::value(1e-3) |
571 | } else { |
572 | S::EPSILON |
573 | }; |
574 | // Discard endpoint/endpoint intersections. |
575 | let t1_is_an_endpoint = t1 < epsilon || t1 > S::ONE - epsilon; |
576 | let t2_is_an_endpoint = t2 < epsilon || t2 > S::ONE - epsilon; |
577 | if t1_is_an_endpoint && t2_is_an_endpoint { |
578 | return; |
579 | } |
580 | |
581 | // We can get repeated intersections when we split a curve at an intersection point, or when |
582 | // two curves intersect at a point where the curves are very close together, or when the fat |
583 | // line process breaks down. |
584 | for i in 0..intersections.len() { |
585 | let (old_t1, old_t2) = intersections[i]; |
586 | // f32 errors can be particularly bad (over a hundred) if we wind up keeping the "wrong" |
587 | // duplicate intersection, so always keep the one that minimizes sample distance. |
588 | if S::abs(t1 - old_t1) < epsilon && S::abs(t2 - old_t2) < epsilon { |
589 | let cur_dist = |
590 | (orig_curve1.sample(old_t1) - orig_curve2.sample(old_t2)).square_length(); |
591 | let new_dist = (orig_curve1.sample(t1) - orig_curve2.sample(t2)).square_length(); |
592 | if new_dist < cur_dist { |
593 | intersections[i] = (t1, t2); |
594 | } |
595 | return; |
596 | } |
597 | } |
598 | |
599 | if intersections.len() < 9 { |
600 | intersections.push((t1, t2)); |
601 | } |
602 | } |
603 | |
604 | // Returns an interval (t_min, t_max) with the property that for parameter values outside that |
605 | // interval, curve1 is guaranteed to not intersect curve2; uses the fat line of curve2 as its basis |
606 | // for the guarantee. (See the Sederberg document for what's going on here.) |
607 | fn restrict_curve_to_fat_line<S: Scalar>( |
608 | curve1: &CubicBezierSegment<S>, |
609 | curve2: &CubicBezierSegment<S>, |
610 | ) -> Option<(S, S)> { |
611 | // TODO: Consider clipping against the perpendicular fat line as well (recommended by |
612 | // Sederberg). |
613 | // TODO: The current algorithm doesn't handle the (rare) case where curve1 and curve2 are |
614 | // overlapping lines. |
615 | |
616 | let baseline2: LineEquation = curve2.baseline().to_line().equation(); |
617 | |
618 | let d_0: S = baseline2.signed_distance_to_point(&curve1.from); |
619 | let d_1: S = baseline2.signed_distance_to_point(&curve1.ctrl1); |
620 | let d_2: S = baseline2.signed_distance_to_point(&curve1.ctrl2); |
621 | let d_3: S = baseline2.signed_distance_to_point(&curve1.to); |
622 | |
623 | let mut top: ArrayVec, 4> = ArrayVec::new(); |
624 | let mut bottom: ArrayVec, 4> = ArrayVec::new(); |
625 | convex_hull_of_distance_curve(d0:d_0, d1:d_1, d2:d_2, d3:d_3, &mut top, &mut bottom); |
626 | let (d_min: S, d_max: S) = curve2.fat_line_min_max(); |
627 | |
628 | clip_convex_hull_to_fat_line(&mut top, &mut bottom, d_min, d_max) |
629 | } |
630 | |
631 | // Returns the convex hull of the curve that's the graph of the function |
632 | // t -> d(curve1(t), baseline(curve2)). The convex hull is described as a top and a bottom, where |
633 | // each of top and bottom is described by the list of its vertices from left to right (the number of |
634 | // vertices for each is variable). |
635 | fn convex_hull_of_distance_curve<S: Scalar>( |
636 | d0: S, |
637 | d1: S, |
638 | d2: S, |
639 | d3: S, |
640 | top: &mut ArrayVec<Point<S>, 4>, |
641 | bottom: &mut ArrayVec<Point<S>, 4>, |
642 | ) { |
643 | debug_assert!(top.is_empty()); |
644 | debug_assert!(bottom.is_empty()); |
645 | |
646 | let p0 = point(S::ZERO, d0); |
647 | let p1 = point(S::ONE / S::THREE, d1); |
648 | let p2 = point(S::TWO / S::THREE, d2); |
649 | let p3 = point(S::ONE, d3); |
650 | // Compute the vertical signed distance of p1 and p2 from [p0, p3]. |
651 | let dist1 = d1 - (S::TWO * d0 + d3) / S::THREE; |
652 | let dist2 = d2 - (d0 + S::TWO * d3) / S::THREE; |
653 | |
654 | // Compute the hull assuming p1 is on top - we'll switch later if needed. |
655 | if dist1 * dist2 < S::ZERO { |
656 | // p1 and p2 lie on opposite sides of [p0, p3], so the hull is a quadrilateral: |
657 | let _ = top.try_extend_from_slice(&[p0, p1, p3]); |
658 | let _ = bottom.try_extend_from_slice(&[p0, p2, p3]); |
659 | } else { |
660 | // p1 and p2 lie on the same side of [p0, p3]. The hull can be a triangle or a |
661 | // quadrilateral, and [p0, p3] is part of the hull. The hull is a triangle if the vertical |
662 | // distance of one of the middle points p1, p2 is <= half the vertical distance of the |
663 | // other middle point. |
664 | let dist1 = S::abs(dist1); |
665 | let dist2 = S::abs(dist2); |
666 | if dist1 >= S::TWO * dist2 { |
667 | let _ = top.try_extend_from_slice(&[p0, p1, p3]); |
668 | let _ = bottom.try_extend_from_slice(&[p0, p3]); |
669 | } else if dist2 >= S::TWO * dist1 { |
670 | let _ = top.try_extend_from_slice(&[p0, p2, p3]); |
671 | let _ = bottom.try_extend_from_slice(&[p0, p3]); |
672 | } else { |
673 | let _ = top.try_extend_from_slice(&[p0, p1, p2, p3]); |
674 | let _ = bottom.try_extend_from_slice(&[p0, p3]); |
675 | } |
676 | } |
677 | |
678 | // Flip the hull if needed: |
679 | if dist1 < S::ZERO || (dist1 == S::ZERO && dist2 < S::ZERO) { |
680 | mem::swap(top, bottom); |
681 | } |
682 | } |
683 | |
684 | // Returns the min and max values at which the convex hull enters the fat line min/max offset lines. |
685 | fn clip_convex_hull_to_fat_line<S: Scalar>( |
686 | hull_top: &mut [Point<S>], |
687 | hull_bottom: &mut [Point<S>], |
688 | d_min: S, |
689 | d_max: S, |
690 | ) -> Option<(S, S)> { |
691 | // Walk from the left corner of the convex hull until we enter the fat line limits: |
692 | let t_clip_min: S = walk_convex_hull_start_to_fat_line(hull_top_vertices:hull_top, hull_bottom_vertices:hull_bottom, d_min, d_max)?; |
693 | |
694 | // Now walk from the right corner of the convex hull until we enter the fat line limits - to |
695 | // walk right to left we just reverse the order of the hull vertices, so that hull_top and |
696 | // hull_bottom start at the right corner now: |
697 | hull_top.reverse(); |
698 | hull_bottom.reverse(); |
699 | let t_clip_max: S = walk_convex_hull_start_to_fat_line(hull_top_vertices:hull_top, hull_bottom_vertices:hull_bottom, d_min, d_max)?; |
700 | |
701 | Some((t_clip_min, t_clip_max)) |
702 | } |
703 | |
704 | // Walk the edges of the convex hull until you hit a fat line offset value, starting from the |
705 | // (first vertex in hull_top_vertices == first vertex in hull_bottom_vertices). |
706 | fn walk_convex_hull_start_to_fat_line<S: Scalar>( |
707 | hull_top_vertices: &[Point<S>], |
708 | hull_bottom_vertices: &[Point<S>], |
709 | d_min: S, |
710 | d_max: S, |
711 | ) -> Option<S> { |
712 | let start_corner: Point2D = hull_top_vertices[0]; |
713 | |
714 | if start_corner.y < d_min { |
715 | walk_convex_hull_edges_to_fat_line(hull_vertices:hull_top_vertices, vertices_are_for_top:true, threshold:d_min) |
716 | } else if start_corner.y > d_max { |
717 | walk_convex_hull_edges_to_fat_line(hull_vertices:hull_bottom_vertices, vertices_are_for_top:false, threshold:d_max) |
718 | } else { |
719 | Some(start_corner.x) |
720 | } |
721 | } |
722 | |
723 | // Do the actual walking, starting from the first vertex of hull_vertices. |
724 | fn walk_convex_hull_edges_to_fat_line<S: Scalar>( |
725 | hull_vertices: &[Point<S>], |
726 | vertices_are_for_top: bool, |
727 | threshold: S, |
728 | ) -> Option<S> { |
729 | for i: usize in 0..hull_vertices.len() - 1 { |
730 | let p: Point2D = hull_vertices[i]; |
731 | let q: Point2D = hull_vertices[i + 1]; |
732 | if (vertices_are_for_top && q.y >= threshold) || (!vertices_are_for_top && q.y <= threshold) |
733 | { |
734 | return if q.y == threshold { |
735 | Some(q.x) |
736 | } else { |
737 | Some(p.x + (threshold - p.y) * (q.x - p.x) / (q.y - p.y)) |
738 | }; |
739 | } |
740 | } |
741 | // All points of the hull are outside the threshold: |
742 | None |
743 | } |
744 | |
745 | #[inline ] |
746 | fn inputs_are_f32<S: Scalar>() -> bool { |
747 | S::EPSILON > S::value(1e-6) |
748 | } |
749 | |
750 | #[inline ] |
751 | // Return the point of domain corresponding to the point t, 0 <= t <= 1. |
752 | fn domain_value_at_t<S: Scalar>(domain: &Range<S>, t: S) -> S { |
753 | domain.start + (domain.end - domain.start) * t |
754 | } |
755 | |
756 | #[inline ] |
757 | // Box2D::intersects doesn't count edge/corner intersections, this version does. |
758 | fn rectangles_overlap<S: Scalar>(r1: &Box2D<S>, r2: &Box2D<S>) -> bool { |
759 | r1.min.x <= r2.max.x && r2.min.x <= r1.max.x && r1.min.y <= r2.max.y && r2.min.y <= r1.max.y |
760 | } |
761 | |
762 | #[cfg (test)] |
763 | fn do_test<S: Scalar>( |
764 | curve1: &CubicBezierSegment<S>, |
765 | curve2: &CubicBezierSegment<S>, |
766 | intersection_count: i32, |
767 | ) { |
768 | do_test_once(curve1, curve2, intersection_count); |
769 | do_test_once(curve2, curve1, intersection_count); |
770 | } |
771 | |
772 | #[cfg (test)] |
773 | fn do_test_once<S: Scalar>( |
774 | curve1: &CubicBezierSegment<S>, |
775 | curve2: &CubicBezierSegment<S>, |
776 | intersection_count: i32, |
777 | ) { |
778 | let intersections = cubic_bezier_intersections_t(curve1, curve2); |
779 | for intersection in &intersections { |
780 | let p1 = curve1.sample(intersection.0); |
781 | let p2 = curve2.sample(intersection.1); |
782 | check_dist(&p1, &p2); |
783 | } |
784 | |
785 | assert_eq!(intersections.len() as i32, intersection_count); |
786 | } |
787 | |
788 | #[cfg (test)] |
789 | fn check_dist<S: Scalar>(p1: &Point<S>, p2: &Point<S>) { |
790 | let dist = S::sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y)); |
791 | if dist > S::HALF { |
792 | assert!(false, "Intersection points too far apart." ); |
793 | } |
794 | } |
795 | |
796 | #[test ] |
797 | fn test_cubic_curve_curve_intersections() { |
798 | do_test( |
799 | &CubicBezierSegment { |
800 | from: point(0.0, 0.0), |
801 | ctrl1: point(0.0, 1.0), |
802 | ctrl2: point(0.0, 1.0), |
803 | to: point(1.0, 1.0), |
804 | }, |
805 | &CubicBezierSegment { |
806 | from: point(0.0, 1.0), |
807 | ctrl1: point(1.0, 1.0), |
808 | ctrl2: point(1.0, 1.0), |
809 | to: point(1.0, 0.0), |
810 | }, |
811 | 1, |
812 | ); |
813 | do_test( |
814 | &CubicBezierSegment { |
815 | from: point(48.0f32, 84.0), |
816 | ctrl1: point(104.0, 176.0), |
817 | ctrl2: point(190.0, 37.0), |
818 | to: point(121.0, 75.0), |
819 | }, |
820 | &CubicBezierSegment { |
821 | from: point(68.0, 145.0), |
822 | ctrl1: point(74.0, 6.0), |
823 | ctrl2: point(143.0, 197.0), |
824 | to: point(138.0, 55.0), |
825 | }, |
826 | 4, |
827 | ); |
828 | do_test( |
829 | &CubicBezierSegment { |
830 | from: point(0.0, 0.0), |
831 | ctrl1: point(0.5, 1.0), |
832 | ctrl2: point(0.5, 1.0), |
833 | to: point(1.0, 0.0), |
834 | }, |
835 | &CubicBezierSegment { |
836 | from: point(0.0, 1.0), |
837 | ctrl1: point(0.5, 0.0), |
838 | ctrl2: point(0.5, 0.0), |
839 | to: point(1.0, 1.0), |
840 | }, |
841 | 2, |
842 | ); |
843 | do_test( |
844 | &CubicBezierSegment { |
845 | from: point(0.2, 0.0), |
846 | ctrl1: point(0.5, 3.0), |
847 | ctrl2: point(0.5, -2.0), |
848 | to: point(0.8, 1.0), |
849 | }, |
850 | &CubicBezierSegment { |
851 | from: point(0.0, 0.0), |
852 | ctrl1: point(2.5, 0.5), |
853 | ctrl2: point(-1.5, 0.5), |
854 | to: point(1.0, 0.0), |
855 | }, |
856 | 9, |
857 | ); |
858 | |
859 | // (A previous version of the code was returning two practically identical |
860 | // intersection points here.) |
861 | do_test( |
862 | &CubicBezierSegment { |
863 | from: point(718133.1363092018, 673674.987999388), |
864 | ctrl1: point(-53014.13135835016, 286988.87959900266), |
865 | ctrl2: point(-900630.1880107201, -7527.6889376943), |
866 | to: point(417822.48349384824, -149039.14932848653), |
867 | }, |
868 | &CubicBezierSegment { |
869 | from: point(924715.3309247112, 719414.5221912428), |
870 | ctrl1: point(965365.9679664494, -563421.3040676294), |
871 | ctrl2: point(273552.85484064696, 643090.0890117711), |
872 | to: point(-113963.134524995, 732017.9466050486), |
873 | }, |
874 | 1, |
875 | ); |
876 | |
877 | // On these curves the algorithm runs to a state at which the new clipped domain1 becomes a |
878 | // point even though t_min_clip < t_max_clip (because domain1 was small enough to begin with |
879 | // relative to the small distance between t_min_clip and t_max_clip), and the new curve1 is not |
880 | // a point (it's split off the old curve1 using t_min_clip < t_max_clip). |
881 | do_test( |
882 | &CubicBezierSegment { |
883 | from: point(423394.5967598548, -91342.7434613118), |
884 | ctrl1: point(333212.450870987, 225564.45711810607), |
885 | ctrl2: point(668108.668469816, -626100.8367380127), |
886 | to: point(-481885.0610437216, 893767.5320803947), |
887 | }, |
888 | &CubicBezierSegment { |
889 | from: point(-484505.2601961801, -222621.44229855016), |
890 | ctrl1: point(22432.829984141514, -944727.7102144773), |
891 | ctrl2: point(-433294.66549074976, -168018.60431004688), |
892 | to: point(567688.5977972192, 13975.09633399453), |
893 | }, |
894 | 3, |
895 | ); |
896 | } |
897 | |
898 | #[test ] |
899 | fn test_cubic_control_point_touching() { |
900 | // After splitting the curve2 loop in half, curve1.ctrl1 (and only that |
901 | // point) touches the curve2 fat line - make sure we don't accept that as an |
902 | // intersection. [We're really only interested in the right half of the loop - the rest of the |
903 | // loop is there just to get past an initial fast_bounding_box check.] |
904 | do_test( |
905 | &CubicBezierSegment { |
906 | from: point(x:-1.0, y:0.0), |
907 | ctrl1: point(x:0.0, y:0.0), |
908 | ctrl2: point(x:-1.0, y:-0.1), |
909 | to: point(x:-1.0, y:-0.1), |
910 | }, |
911 | &CubicBezierSegment { |
912 | from: point(x:0.0, y:0.0), |
913 | ctrl1: point(x:5.0, y:-5.0), |
914 | ctrl2: point(x:-5.0, y:-5.0), |
915 | to: point(x:0.0, y:0.0), |
916 | }, |
917 | 0, |
918 | ); |
919 | } |
920 | |
921 | #[test ] |
922 | fn test_cubic_self_intersections() { |
923 | // Two self-intersecting curves intersecting at their self-intersections (the origin). |
924 | do_test( |
925 | &CubicBezierSegment { |
926 | from: point(x:-10.0, y:-13.636363636363636), |
927 | ctrl1: point(x:15.0, y:11.363636363636363), |
928 | ctrl2: point(x:-15.0, y:11.363636363636363), |
929 | to: point(x:10.0, y:-13.636363636363636), |
930 | }, |
931 | &CubicBezierSegment { |
932 | from: point(x:13.636363636363636, y:-10.0), |
933 | ctrl1: point(x:-11.363636363636363, y:15.0), |
934 | ctrl2: point(x:-11.363636363636363, y:-15.0), |
935 | to: point(x:13.636363636363636, y:10.0), |
936 | }, |
937 | 4, |
938 | ); |
939 | } |
940 | |
941 | #[test ] |
942 | fn test_cubic_loops() { |
943 | // This gets up to a recursion count of 53 trying to find (0.0, 0.0) and (1.0, 1.0) (which |
944 | // aren't actually needed) - with the curves in the opposite order it gets up to 81! |
945 | do_test( |
946 | &CubicBezierSegment { |
947 | from: point(0.0, 0.0), |
948 | ctrl1: point(-10.0, 10.0), |
949 | ctrl2: point(10.0, 10.0), |
950 | to: point(0.0, 0.0), |
951 | }, |
952 | &CubicBezierSegment { |
953 | from: point(0.0, 0.0), |
954 | ctrl1: point(-1.0, 1.0), |
955 | ctrl2: point(1.0, 1.0), |
956 | to: point(0.0, 0.0), |
957 | }, |
958 | 0, |
959 | ); |
960 | |
961 | do_test( |
962 | &CubicBezierSegment { |
963 | from: point(0.0f32, 0.0), |
964 | ctrl1: point(-100.0, 0.0), |
965 | ctrl2: point(-500.0, 500.0), |
966 | to: point(0.0, 0.0), |
967 | }, |
968 | &CubicBezierSegment { |
969 | from: point(0.0, 0.0), |
970 | ctrl1: point(-800.0, 100.0), |
971 | ctrl2: point(500.0, 500.0), |
972 | to: point(0.0, 0.0), |
973 | }, |
974 | 3, |
975 | ); |
976 | } |
977 | |
978 | #[test ] |
979 | fn test_cubic_line_curve_intersections() { |
980 | do_test( |
981 | &CubicBezierSegment { |
982 | /* line */ |
983 | from: point(1.0, 2.0), |
984 | ctrl1: point(20.0, 1.0), |
985 | ctrl2: point(1.0, 2.0), |
986 | to: point(20.0, 1.0), |
987 | }, |
988 | &CubicBezierSegment { |
989 | from: point(1.0, 0.0), |
990 | ctrl1: point(1.0, 5.0), |
991 | ctrl2: point(20.0, 25.0), |
992 | to: point(20.0, 0.0), |
993 | }, |
994 | 2, |
995 | ); |
996 | |
997 | do_test( |
998 | &CubicBezierSegment { |
999 | /* line */ |
1000 | from: point(0.0f32, 0.0), |
1001 | ctrl1: point(-10.0, 0.0), |
1002 | ctrl2: point(20.0, 0.0), |
1003 | to: point(10.0, 0.0), |
1004 | }, |
1005 | &CubicBezierSegment { |
1006 | from: point(-1.0, -1.0), |
1007 | ctrl1: point(0.0, 4.0), |
1008 | ctrl2: point(10.0, -4.0), |
1009 | to: point(11.0, 1.0), |
1010 | }, |
1011 | 5, |
1012 | ); |
1013 | |
1014 | do_test( |
1015 | &CubicBezierSegment { |
1016 | from: point(-1.0, -2.0), |
1017 | ctrl1: point(-1.0, 8.0), |
1018 | ctrl2: point(1.0, -8.0), |
1019 | to: point(1.0, 2.0), |
1020 | }, |
1021 | &CubicBezierSegment { |
1022 | /* line */ |
1023 | from: point(-10.0, -10.0), |
1024 | ctrl1: point(20.0, 20.0), |
1025 | ctrl2: point(-20.0, -20.0), |
1026 | to: point(10.0, 10.0), |
1027 | }, |
1028 | 9, |
1029 | ); |
1030 | } |
1031 | |
1032 | #[test ] |
1033 | fn test_cubic_line_line_intersections() { |
1034 | do_test( |
1035 | &CubicBezierSegment { |
1036 | from: point(-10.0, -10.0), |
1037 | ctrl1: point(20.0, 20.0), |
1038 | ctrl2: point(-20.0, -20.0), |
1039 | to: point(10.0, 10.0), |
1040 | }, |
1041 | &CubicBezierSegment { |
1042 | from: point(-10.0, 10.0), |
1043 | ctrl1: point(20.0, -20.0), |
1044 | ctrl2: point(-20.0, 20.0), |
1045 | to: point(10.0, -10.0), |
1046 | }, |
1047 | 9, |
1048 | ); |
1049 | |
1050 | // Overlapping line segments should return 0 intersections. |
1051 | do_test( |
1052 | &CubicBezierSegment { |
1053 | from: point(0.0, 0.0), |
1054 | ctrl1: point(0.0, 0.0), |
1055 | ctrl2: point(10.0, 0.0), |
1056 | to: point(10.0, 0.0), |
1057 | }, |
1058 | &CubicBezierSegment { |
1059 | from: point(5.0, 0.0), |
1060 | ctrl1: point(5.0, 0.0), |
1061 | ctrl2: point(15.0, 0.0), |
1062 | to: point(15.0, 0.0), |
1063 | }, |
1064 | 0, |
1065 | ); |
1066 | } |
1067 | |
1068 | #[test ] |
1069 | // (This test used to fail on a previous version of the algorithm by returning an intersection close |
1070 | // to (1.0, 0.0), but not close enough to consider it the same as (1.0, 0.0) - the curves are quite |
1071 | // close at that endpoint.) |
1072 | fn test_cubic_similar_loops() { |
1073 | do_test( |
1074 | &CubicBezierSegment { |
1075 | from: point(x:-0.281604145719379, y:-0.3129629924180608), |
1076 | ctrl1: point(x:-0.04393998118946163, y:0.13714701102906668), |
1077 | ctrl2: point(x:0.4472584256288119, y:0.2876115686206546), |
1078 | to: point(x:-0.281604145719379, y:-0.3129629924180608), |
1079 | }, |
1080 | &CubicBezierSegment { |
1081 | from: point(x:-0.281604145719379, y:-0.3129629924180608), |
1082 | ctrl1: point(x:-0.1560415754252551, y:-0.22924729391648402), |
1083 | ctrl2: point(x:-0.9224550447067958, y:0.19110227764357646), |
1084 | to: point(x:-0.281604145719379, y:-0.3129629924180608), |
1085 | }, |
1086 | 2, |
1087 | ); |
1088 | } |
1089 | |
1090 | #[test ] |
1091 | // (A previous version of the algorithm returned an intersection close to (0.5, 0.5), but not close |
1092 | // enough to be considered the same as (0.5, 0.5).) |
1093 | fn test_cubic_no_duplicated_root() { |
1094 | do_test( |
1095 | &CubicBezierSegment { |
1096 | from: point(x:0.0, y:0.0), |
1097 | ctrl1: point(x:-10.0, y:1.0), |
1098 | ctrl2: point(x:10.0, y:1.0), |
1099 | to: point(x:0.0, y:0.0), |
1100 | }, |
1101 | &CubicBezierSegment { |
1102 | from: point(x:0.0, y:0.0), |
1103 | ctrl1: point(x:-1.0, y:1.0), |
1104 | ctrl2: point(x:1.0, y:1.0), |
1105 | to: point(x:0.0, y:0.0), |
1106 | }, |
1107 | 1, |
1108 | ); |
1109 | } |
1110 | |
1111 | #[test ] |
1112 | fn test_cubic_glancing_intersection() { |
1113 | use std::panic; |
1114 | // The f64 version currently fails on a very close fat line miss after 57 recursions. |
1115 | let result = panic::catch_unwind(|| { |
1116 | do_test( |
1117 | &CubicBezierSegment { |
1118 | from: point(0.0, 0.0), |
1119 | ctrl1: point(0.0, 8.0), |
1120 | ctrl2: point(10.0, 8.0), |
1121 | to: point(10.0, 0.0), |
1122 | }, |
1123 | &CubicBezierSegment { |
1124 | from: point(0.0, 12.0), |
1125 | ctrl1: point(0.0, 4.0), |
1126 | ctrl2: point(10.0, 4.0), |
1127 | to: point(10.0, 12.0), |
1128 | }, |
1129 | 1, |
1130 | ); |
1131 | }); |
1132 | assert!(result.is_err()); |
1133 | |
1134 | let result = panic::catch_unwind(|| { |
1135 | do_test( |
1136 | &CubicBezierSegment { |
1137 | from: point(0.0f32, 0.0), |
1138 | ctrl1: point(0.0, 8.0), |
1139 | ctrl2: point(10.0, 8.0), |
1140 | to: point(10.0, 0.0), |
1141 | }, |
1142 | &CubicBezierSegment { |
1143 | from: point(0.0, 12.0), |
1144 | ctrl1: point(0.0, 4.0), |
1145 | ctrl2: point(10.0, 4.0), |
1146 | to: point(10.0, 12.0), |
1147 | }, |
1148 | 1, |
1149 | ); |
1150 | }); |
1151 | assert!(result.is_err()); |
1152 | } |
1153 | |
1154 | #[test ] |
1155 | fn test_cubic_duplicated_intersections() { |
1156 | use std::panic; |
1157 | let result: Result<(), Box> = panic::catch_unwind(|| { |
1158 | // This finds an extra intersection (0.49530116, 0.74361485) - the actual, also found, is |
1159 | // (0.49633604, 0.74361396). Their difference is (−0.00103488, 0.00000089) - we consider |
1160 | // the two to be the same if both difference values are < 1e-3. |
1161 | do_test( |
1162 | &CubicBezierSegment { |
1163 | from: point(x:-33307.36f32, y:-1804.0625), |
1164 | ctrl1: point(x:-59259.727, y:70098.31), |
1165 | ctrl2: point(x:98661.78, y:48235.703), |
1166 | to: point(x:28422.234, y:31845.219), |
1167 | }, |
1168 | &CubicBezierSegment { |
1169 | from: point(x:-21501.133, y:51935.344), |
1170 | ctrl1: point(x:-95301.96, y:95031.45), |
1171 | ctrl2: point(x:-25882.242, y:-12896.75), |
1172 | to: point(x:94618.97, y:94288.66), |
1173 | }, |
1174 | 2, |
1175 | ); |
1176 | }); |
1177 | assert!(result.is_err()); |
1178 | } |
1179 | |
1180 | #[test ] |
1181 | fn test_cubic_endpoint_not_an_intersection() { |
1182 | // f32 curves seem to be somewhat prone to picking up not-an-intersections where an endpoint of |
1183 | // one curve is close to and points into the interior of the other curve, and both curves are |
1184 | // "mostly linear" on some level. |
1185 | use std::panic; |
1186 | let result: Result<(), Box> = panic::catch_unwind(|| { |
1187 | do_test( |
1188 | &CubicBezierSegment { |
1189 | from: point(x:76868.875f32, y:47679.28), |
1190 | ctrl1: point(x:65326.86, y:856.21094), |
1191 | ctrl2: point(x:-85621.64, y:-80823.375), |
1192 | to: point(x:-56517.53, y:28062.227), |
1193 | }, |
1194 | &CubicBezierSegment { |
1195 | from: point(x:-67977.72, y:77673.53), |
1196 | ctrl1: point(x:-59829.57, y:-41917.87), |
1197 | ctrl2: point(x:57.4375, y:52822.97), |
1198 | to: point(x:51075.86, y:85772.84), |
1199 | }, |
1200 | 0, |
1201 | ); |
1202 | }); |
1203 | assert!(result.is_err()); |
1204 | } |
1205 | |
1206 | #[test ] |
1207 | // The endpoints of curve2 intersect the interior of curve1. |
1208 | fn test_cubic_interior_endpoint() { |
1209 | do_test( |
1210 | &CubicBezierSegment { |
1211 | // Has its apex at 6.0. |
1212 | from: point(x:-5.0, y:0.0), |
1213 | ctrl1: point(x:-5.0, y:8.0), |
1214 | ctrl2: point(x:5.0, y:8.0), |
1215 | to: point(x:5.0, y:0.0), |
1216 | }, |
1217 | &CubicBezierSegment { |
1218 | from: point(x:0.0, y:6.0), |
1219 | ctrl1: point(x:-5.0, y:0.0), |
1220 | ctrl2: point(x:5.0, y:0.0), |
1221 | to: point(x:0.0, y:6.0), |
1222 | }, |
1223 | 2, |
1224 | ); |
1225 | } |
1226 | |
1227 | #[test ] |
1228 | fn test_cubic_point_curve_intersections() { |
1229 | let epsilon = 1e-5; |
1230 | { |
1231 | let curve1 = CubicBezierSegment { |
1232 | from: point(0.0, 0.0), |
1233 | ctrl1: point(0.0, 1.0), |
1234 | ctrl2: point(0.0, 1.0), |
1235 | to: point(1.0, 1.0), |
1236 | }; |
1237 | let sample_t = 0.123456789; |
1238 | let pt = curve1.sample(sample_t); |
1239 | let curve2 = CubicBezierSegment { |
1240 | from: pt, |
1241 | ctrl1: pt, |
1242 | ctrl2: pt, |
1243 | to: pt, |
1244 | }; |
1245 | let intersections = cubic_bezier_intersections_t(&curve1, &curve2); |
1246 | assert_eq!(intersections.len(), 1); |
1247 | let intersection_t = intersections[0].0; |
1248 | assert!(f64::abs(intersection_t - sample_t) < epsilon); |
1249 | } |
1250 | { |
1251 | let curve1 = CubicBezierSegment { |
1252 | from: point(-10.0, -13.636363636363636), |
1253 | ctrl1: point(15.0, 11.363636363636363), |
1254 | ctrl2: point(-15.0, 11.363636363636363), |
1255 | to: point(10.0, -13.636363636363636), |
1256 | }; |
1257 | // curve1 has a self intersection at the following parameter values: |
1258 | let parameter1 = 0.7611164839335472; |
1259 | let parameter2 = 0.23888351606645375; |
1260 | let pt = curve1.sample(parameter1); |
1261 | let curve2 = CubicBezierSegment { |
1262 | from: pt, |
1263 | ctrl1: pt, |
1264 | ctrl2: pt, |
1265 | to: pt, |
1266 | }; |
1267 | let intersections = cubic_bezier_intersections_t(&curve1, &curve2); |
1268 | assert_eq!(intersections.len(), 2); |
1269 | let intersection_t1 = intersections[0].0; |
1270 | let intersection_t2 = intersections[1].0; |
1271 | assert!(f64::abs(intersection_t1 - parameter1) < epsilon); |
1272 | assert!(f64::abs(intersection_t2 - parameter2) < epsilon); |
1273 | } |
1274 | { |
1275 | let epsilon = epsilon as f32; |
1276 | let curve1 = CubicBezierSegment { |
1277 | from: point(0.0f32, 0.0), |
1278 | ctrl1: point(50.0, 50.0), |
1279 | ctrl2: point(-50.0, -50.0), |
1280 | to: point(10.0, 10.0), |
1281 | }; |
1282 | // curve1 is a line that passes through (5.0, 5.0) three times: |
1283 | let parameter1 = 0.96984464; |
1284 | let parameter2 = 0.037427425; |
1285 | let parameter3 = 0.44434106; |
1286 | let pt = curve1.sample(parameter1); |
1287 | let curve2 = CubicBezierSegment { |
1288 | from: pt, |
1289 | ctrl1: pt, |
1290 | ctrl2: pt, |
1291 | to: pt, |
1292 | }; |
1293 | let intersections = cubic_bezier_intersections_t(&curve1, &curve2); |
1294 | assert_eq!(intersections.len(), 3); |
1295 | let intersection_t1 = intersections[0].0; |
1296 | let intersection_t2 = intersections[1].0; |
1297 | let intersection_t3 = intersections[2].0; |
1298 | assert!(f32::abs(intersection_t1 - parameter1) < epsilon); |
1299 | assert!(f32::abs(intersection_t2 - parameter2) < epsilon); |
1300 | assert!(f32::abs(intersection_t3 - parameter3) < epsilon); |
1301 | } |
1302 | } |
1303 | |
1304 | #[test ] |
1305 | fn test_cubic_subcurve_intersections() { |
1306 | let curve1: CubicBezierSegment = CubicBezierSegment { |
1307 | from: point(x:0.0, y:0.0), |
1308 | ctrl1: point(x:0.0, y:1.0), |
1309 | ctrl2: point(x:0.0, y:1.0), |
1310 | to: point(x:1.0, y:1.0), |
1311 | }; |
1312 | let curve2: CubicBezierSegment = curve1.split_range(t_range:0.25..0.75); |
1313 | // The algorithm will find as many intersections as you let it, basically - make sure we're |
1314 | // not allowing too many intersections to be added, and not crashing on out of resources. |
1315 | do_test(&curve1, &curve2, 9); |
1316 | } |
1317 | |
1318 | #[test ] |
1319 | fn test_cubic_result_distance() { |
1320 | // In a previous version this used to return an intersection pair (0.17933762, 0.23783168), |
1321 | // close to an actual intersection, where the sampled intersection points on respective curves |
1322 | // were at distance 160.08488. The point here is that the old extra intersection was the result |
1323 | // of an anomalous fat line calculation, in other words an actual error, not just a "not quite |
1324 | // computationally close enough" error. |
1325 | do_test( |
1326 | &CubicBezierSegment { |
1327 | from: point(x:5893.133f32, y:-51377.152), |
1328 | ctrl1: point(x:-94403.984, y:37668.156), |
1329 | ctrl2: point(x:-58914.684, y:30339.195), |
1330 | to: point(x:4895.875, y:83473.3), |
1331 | }, |
1332 | &CubicBezierSegment { |
1333 | from: point(x:-51523.734, y:75047.05), |
1334 | ctrl1: point(x:-58162.76, y:-91093.875), |
1335 | ctrl2: point(x:82137.516, y:-59844.35), |
1336 | to: point(x:46856.406, y:40479.64), |
1337 | }, |
1338 | 3, |
1339 | ); |
1340 | } |
1341 | |