1use crate::scalar::Scalar;
2use 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.
11use crate::{point, Box2D, Point};
12use arrayvec::ArrayVec;
13use core::mem;
14use 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.
23pub 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
100fn 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
175fn 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
202fn 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)]
263fn 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
480fn 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.
536fn 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
559fn 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.)
607fn 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).
635fn 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.
685fn 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).
706fn 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.
724fn 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]
746fn 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.
752fn 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.
758fn 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)]
763fn 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)]
773fn 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)]
789fn 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]
797fn 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]
899fn 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]
922fn 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]
942fn 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]
979fn 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]
1033fn 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.)
1072fn 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).)
1093fn 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]
1112fn 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]
1155fn 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]
1181fn 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.
1208fn 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]
1228fn 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]
1305fn 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]
1319fn 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