1use alloc::vec;
7use alloc::vec::Vec;
8use core::ops::{Mul, Range};
9
10use crate::MAX_EXTREMA;
11use crate::{Line, QuadSpline, Vec2};
12use arrayvec::ArrayVec;
13
14use crate::common::{
15 solve_quadratic, solve_quartic, GAUSS_LEGENDRE_COEFFS_16_HALF, GAUSS_LEGENDRE_COEFFS_24_HALF,
16 GAUSS_LEGENDRE_COEFFS_8, GAUSS_LEGENDRE_COEFFS_8_HALF,
17};
18use crate::{
19 Affine, Nearest, ParamCurve, ParamCurveArclen, ParamCurveArea, ParamCurveCurvature,
20 ParamCurveDeriv, ParamCurveExtrema, ParamCurveNearest, PathEl, Point, QuadBez, Rect, Shape,
21};
22
23#[cfg(not(feature = "std"))]
24use crate::common::FloatFuncs;
25
26const MAX_SPLINE_SPLIT: usize = 100;
27
28#[derive(Clone, Copy, Debug, PartialEq)]
30#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32#[allow(missing_docs)]
33pub struct CubicBez {
34 pub p0: Point,
35 pub p1: Point,
36 pub p2: Point,
37 pub p3: Point,
38}
39
40struct ToQuads {
42 c: CubicBez,
43 i: usize,
44 n: usize,
45}
46
47#[derive(Debug)]
49pub enum CuspType {
50 Loop,
52 DoubleInflection,
54}
55
56impl CubicBez {
57 #[inline(always)]
59 pub fn new<P: Into<Point>>(p0: P, p1: P, p2: P, p3: P) -> CubicBez {
60 CubicBez {
61 p0: p0.into(),
62 p1: p1.into(),
63 p2: p2.into(),
64 p3: p3.into(),
65 }
66 }
67
68 #[inline]
78 pub fn to_quads(&self, accuracy: f64) -> impl Iterator<Item = (f64, f64, QuadBez)> {
79 let max_hypot2 = 432.0 * accuracy * accuracy;
91 let p1x2 = 3.0 * self.p1.to_vec2() - self.p0.to_vec2();
92 let p2x2 = 3.0 * self.p2.to_vec2() - self.p3.to_vec2();
93 let err = (p2x2 - p1x2).hypot2();
94 let n = ((err / max_hypot2).powf(1. / 6.0).ceil() as usize).max(1);
95
96 ToQuads { c: *self, n, i: 0 }
97 }
98
99 pub fn approx_spline(&self, accuracy: f64) -> Option<QuadSpline> {
104 (1..=MAX_SPLINE_SPLIT).find_map(|n| self.approx_spline_n(n, accuracy))
105 }
106
107 fn approx_spline_n(&self, n: usize, accuracy: f64) -> Option<QuadSpline> {
109 if n == 1 {
110 return self
111 .try_approx_quadratic(accuracy)
112 .map(|quad| QuadSpline::new(vec![quad.p0, quad.p1, quad.p2]));
113 }
114 let mut cubics = self.split_into_n(n);
115
116 let mut next_cubic = cubics.next().unwrap();
119 let mut next_q1: Point = next_cubic.approx_quad_control(0.0);
120 let mut q2 = self.p0;
121 let mut d1 = Vec2::ZERO;
122 let mut spline = vec![self.p0, next_q1];
123 for i in 1..=n {
124 let current_cubic: CubicBez = next_cubic;
125 let q0 = q2;
126 let q1 = next_q1;
127 q2 = if i < n {
128 next_cubic = cubics.next().unwrap();
129 next_q1 = next_cubic.approx_quad_control(i as f64 / (n - 1) as f64);
130
131 spline.push(next_q1);
132 q1.midpoint(next_q1)
133 } else {
134 current_cubic.p3
135 };
136 let d0 = d1;
137 d1 = q2.to_vec2() - current_cubic.p3.to_vec2();
138
139 if d1.hypot() > accuracy
140 || !CubicBez::new(
141 d0.to_point(),
142 q0.lerp(q1, 2.0 / 3.0) - current_cubic.p1.to_vec2(),
143 q2.lerp(q1, 2.0 / 3.0) - current_cubic.p2.to_vec2(),
144 d1.to_point(),
145 )
146 .fit_inside(accuracy)
147 {
148 return None;
149 }
150 }
151 spline.push(self.p3);
152 Some(QuadSpline::new(spline))
153 }
154
155 fn approx_quad_control(&self, t: f64) -> Point {
156 let p1 = self.p0 + (self.p1 - self.p0) * 1.5;
157 let p2 = self.p3 + (self.p2 - self.p3) * 1.5;
158 p1.lerp(p2, t)
159 }
160
161 fn try_approx_quadratic(&self, accuracy: f64) -> Option<QuadBez> {
166 if let Some(q1) = Line::new(self.p0, self.p1)
167 .crossing_point(Line::new(self.p2, self.p3))
168 .or_else(|| {
169 (self.p1 == self.p2 && (self.p0 == self.p1 || self.p2 == self.p3))
173 .then_some(self.p1)
174 })
175 {
176 let c1 = self.p0.lerp(q1, 2.0 / 3.0);
177 let c2 = self.p3.lerp(q1, 2.0 / 3.0);
178 if !CubicBez::new(
179 Point::ZERO,
180 c1 - self.p1.to_vec2(),
181 c2 - self.p2.to_vec2(),
182 Point::ZERO,
183 )
184 .fit_inside(accuracy)
185 {
186 return None;
187 }
188 return Some(QuadBez::new(self.p0, q1, self.p3));
189 }
190 None
191 }
192
193 fn split_into_n(&self, n: usize) -> impl Iterator<Item = CubicBez> {
194 let mut storage = ArrayVec::<_, 6>::new();
197
198 match n {
199 1 => storage.push(*self),
200 2 => {
201 let (l, r) = self.subdivide();
202 storage.try_extend_from_slice(&[r, l]).unwrap();
203 }
204 3 => {
205 let (left, mid, right) = self.subdivide_3();
206 storage.try_extend_from_slice(&[right, mid, left]).unwrap();
207 }
208 4 => {
209 let (l, r) = self.subdivide();
210 let (ll, lr) = l.subdivide();
211 let (rl, rr) = r.subdivide();
212 storage.try_extend_from_slice(&[rr, rl, lr, ll]).unwrap();
213 }
214 6 => {
215 let (l, r) = self.subdivide();
216 let (l1, l2, l3) = l.subdivide_3();
217 let (r1, r2, r3) = r.subdivide_3();
218 storage
219 .try_extend_from_slice(&[r3, r2, r1, l3, l2, l1])
220 .unwrap();
221 }
222 _ => (),
223 }
224
225 let mut storage = if storage.is_empty() {
234 None
235 } else {
236 Some(storage)
237 };
238
239 let mut i = 0;
241 let (a, b, c, d) = self.parameters();
242 let dt = 1.0 / n as f64;
243 let delta_2 = dt * dt;
244 let delta_3 = dt * delta_2;
245
246 core::iter::from_fn(move || {
247 if let Some(storage) = storage.as_mut() {
249 return storage.pop();
250 }
251
252 if i >= n {
254 return None;
255 }
256
257 let t1 = i as f64 * dt;
258 let t1_2 = t1 * t1;
259 let a1 = a * delta_3;
260 let b1 = (3.0 * a * t1 + b) * delta_2;
261 let c1 = (2.0 * b * t1 + c + 3.0 * a * t1_2) * dt;
262 let d1 = a * t1 * t1_2 + b * t1_2 + c * t1 + d;
263 let result = CubicBez::from_parameters(a1, b1, c1, d1);
264 i += 1;
265 Some(result)
266 })
267 }
268
269 fn parameters(&self) -> (Vec2, Vec2, Vec2, Vec2) {
270 let c = (self.p1 - self.p0) * 3.0;
271 let b = (self.p2 - self.p1) * 3.0 - c;
272 let d = self.p0.to_vec2();
273 let a = self.p3.to_vec2() - d - c - b;
274 (a, b, c, d)
275 }
276
277 fn from_parameters(a: Vec2, b: Vec2, c: Vec2, d: Vec2) -> Self {
279 let p0 = d.to_point();
280 let p1 = c.div_exact(3.0).to_point() + d;
281 let p2 = (b + c).div_exact(3.0).to_point() + p1.to_vec2();
282 let p3 = (a + d + c + b).to_point();
283 CubicBez::new(p0, p1, p2, p3)
284 }
285
286 fn subdivide_3(&self) -> (CubicBez, CubicBez, CubicBez) {
287 let (p0, p1, p2, p3) = (
288 self.p0.to_vec2(),
289 self.p1.to_vec2(),
290 self.p2.to_vec2(),
291 self.p3.to_vec2(),
292 );
293 let one_27th = 27.0_f64.recip();
301 let mid1 = ((8.0 * p0 + 12.0 * p1 + 6.0 * p2 + p3) * one_27th).to_point();
302 let deriv1 = (p3 + 3.0 * p2 - 4.0 * p0) * one_27th;
303 let mid2 = ((p0 + 6.0 * p1 + 12.0 * p2 + 8.0 * p3) * one_27th).to_point();
304 let deriv2 = (4.0 * p3 - 3.0 * p1 - p0) * one_27th;
305
306 let left = CubicBez::new(
307 self.p0,
308 (2.0 * p0 + p1).div_exact(3.0).to_point(),
309 mid1 - deriv1,
310 mid1,
311 );
312 let mid = CubicBez::new(mid1, mid1 + deriv1, mid2 - deriv2, mid2);
313 let right = CubicBez::new(
314 mid2,
315 mid2 + deriv2,
316 (p2 + 2.0 * p3).div_exact(3.0).to_point(),
317 self.p3,
318 );
319 (left, mid, right)
320 }
321
322 fn fit_inside(&self, distance: f64) -> bool {
326 if self.p2.to_vec2().hypot() <= distance && self.p1.to_vec2().hypot() <= distance {
327 return true;
328 }
329 let mid =
330 (self.p0.to_vec2() + 3.0 * (self.p1.to_vec2() + self.p2.to_vec2()) + self.p3.to_vec2())
331 * 0.125;
332 if mid.hypot() > distance {
333 return false;
334 }
335 let (left, right) = self.subdivide();
337 left.fit_inside(distance) && right.fit_inside(distance)
338 }
339
340 #[inline]
342 pub fn is_finite(&self) -> bool {
343 self.p0.is_finite() && self.p1.is_finite() && self.p2.is_finite() && self.p3.is_finite()
344 }
345
346 #[inline]
348 pub fn is_nan(&self) -> bool {
349 self.p0.is_nan() || self.p1.is_nan() || self.p2.is_nan() || self.p3.is_nan()
350 }
351
352 pub fn inflections(&self) -> ArrayVec<f64, 2> {
360 let a = self.p1 - self.p0;
361 let b = (self.p2 - self.p1) - a;
362 let c = (self.p3 - self.p0) - 3. * (self.p2 - self.p1);
363 solve_quadratic(a.cross(b), a.cross(c), b.cross(c))
364 .iter()
365 .copied()
366 .filter(|t| *t >= 0.0 && *t <= 1.0)
367 .collect()
368 }
369
370 pub fn tangents_to_point(&self, p: Point) -> ArrayVec<f64, 4> {
376 let (a, b, c, d_orig) = self.parameters();
377 let d = d_orig - p.to_vec2();
378 let c4 = b.cross(a);
380 let c3 = 2.0 * c.cross(a);
381 let c2 = c.cross(b) + 3.0 * d.cross(a);
382 let c1 = 2.0 * d.cross(b);
383 let c0 = d.cross(c);
384 solve_quartic(c0, c1, c2, c3, c4)
385 .iter()
386 .copied()
387 .filter(|t| *t >= 0.0 && *t <= 1.0)
388 .collect()
389 }
390
391 pub(crate) fn regularize_cusp(&self, dimension: f64) -> CubicBez {
397 let mut c = *self;
398 if let Some(cusp_type) = self.detect_cusp(dimension) {
401 let d01 = c.p1 - c.p0;
402 let d01h = d01.hypot();
403 let d23 = c.p3 - c.p2;
404 let d23h = d23.hypot();
405 match cusp_type {
406 CuspType::Loop => {
407 c.p1 += (dimension / d01h) * d01;
408 c.p2 -= (dimension / d23h) * d23;
409 }
410 CuspType::DoubleInflection => {
411 if d01h > 2.0 * dimension {
413 c.p1 -= (dimension / d01h) * d01;
414 }
415 if d23h > 2.0 * dimension {
416 c.p2 += (dimension / d23h) * d23;
417 }
418 }
419 }
420 }
421 c
422 }
423
424 fn detect_cusp(&self, dimension: f64) -> Option<CuspType> {
429 let d01 = self.p1 - self.p0;
430 let d02 = self.p2 - self.p0;
431 let d03 = self.p3 - self.p0;
432 let d12 = self.p2 - self.p1;
433 let d23 = self.p3 - self.p2;
434 let det_012 = d01.cross(d02);
435 let det_123 = d12.cross(d23);
436 let det_013 = d01.cross(d03);
437 let det_023 = d02.cross(d03);
438 if det_012 * det_123 > 0.0 && det_012 * det_013 < 0.0 && det_012 * det_023 < 0.0 {
439 let q = self.deriv();
440 let nearest = q.nearest(Point::ORIGIN, 1e-9);
442 let d = q.eval(nearest.t);
445 let d2 = q.deriv().eval(nearest.t);
446 let cross = d.to_vec2().cross(d2.to_vec2());
447 if nearest.distance_sq.powi(3) <= (cross * dimension).powi(2) {
448 let a = 3. * det_012 + det_023 - 2. * det_013;
449 let b = -3. * det_012 + det_013;
450 let c = det_012;
451 let d = b * b - 4. * a * c;
452 if d > 0.0 {
453 return Some(CuspType::DoubleInflection);
454 } else {
455 return Some(CuspType::Loop);
456 }
457 }
458 }
459 None
460 }
461}
462
463pub struct CubicBezIter {
465 cubic: CubicBez,
466 ix: usize,
467}
468
469impl Shape for CubicBez {
470 type PathElementsIter<'iter> = CubicBezIter;
471
472 #[inline]
473 fn path_elements(&self, _tolerance: f64) -> CubicBezIter {
474 CubicBezIter {
475 cubic: *self,
476 ix: 0,
477 }
478 }
479
480 #[inline(always)]
481 fn area(&self) -> f64 {
482 0.0
483 }
484
485 #[inline]
486 fn perimeter(&self, accuracy: f64) -> f64 {
487 self.arclen(accuracy)
488 }
489
490 #[inline(always)]
491 fn winding(&self, _pt: Point) -> i32 {
492 0
493 }
494
495 #[inline]
496 fn bounding_box(&self) -> Rect {
497 ParamCurveExtrema::bounding_box(self)
498 }
499}
500
501impl Iterator for CubicBezIter {
502 type Item = PathEl;
503
504 fn next(&mut self) -> Option<PathEl> {
505 self.ix += 1;
506 match self.ix {
507 1 => Some(PathEl::MoveTo(self.cubic.p0)),
508 2 => Some(PathEl::CurveTo(self.cubic.p1, self.cubic.p2, self.cubic.p3)),
509 _ => None,
510 }
511 }
512}
513
514impl ParamCurve for CubicBez {
515 #[inline]
516 fn eval(&self, t: f64) -> Point {
517 let mt = 1.0 - t;
518 let v = self.p0.to_vec2() * (mt * mt * mt)
519 + (self.p1.to_vec2() * (mt * mt * 3.0)
520 + (self.p2.to_vec2() * (mt * 3.0) + self.p3.to_vec2() * t) * t)
521 * t;
522 v.to_point()
523 }
524
525 fn subsegment(&self, range: Range<f64>) -> CubicBez {
526 let (t0, t1) = (range.start, range.end);
527 let p0 = self.eval(t0);
528 let p3 = self.eval(t1);
529 let d = self.deriv();
530 let scale = (t1 - t0) * (1.0 / 3.0);
531 let p1 = p0 + scale * d.eval(t0).to_vec2();
532 let p2 = p3 - scale * d.eval(t1).to_vec2();
533 CubicBez { p0, p1, p2, p3 }
534 }
535
536 #[inline]
538 fn subdivide(&self) -> (CubicBez, CubicBez) {
539 let pm = self.eval(0.5);
540 (
541 CubicBez::new(
542 self.p0,
543 self.p0.midpoint(self.p1),
544 ((self.p0.to_vec2() + self.p1.to_vec2() * 2.0 + self.p2.to_vec2()) * 0.25)
545 .to_point(),
546 pm,
547 ),
548 CubicBez::new(
549 pm,
550 ((self.p1.to_vec2() + self.p2.to_vec2() * 2.0 + self.p3.to_vec2()) * 0.25)
551 .to_point(),
552 self.p2.midpoint(self.p3),
553 self.p3,
554 ),
555 )
556 }
557
558 #[inline(always)]
559 fn start(&self) -> Point {
560 self.p0
561 }
562
563 #[inline(always)]
564 fn end(&self) -> Point {
565 self.p3
566 }
567}
568
569impl ParamCurveDeriv for CubicBez {
570 type DerivResult = QuadBez;
571
572 #[inline]
573 fn deriv(&self) -> QuadBez {
574 QuadBez::new(
575 (3.0 * (self.p1 - self.p0)).to_point(),
576 (3.0 * (self.p2 - self.p1)).to_point(),
577 (3.0 * (self.p3 - self.p2)).to_point(),
578 )
579 }
580}
581
582fn arclen_quadrature_core(coeffs: &[(f64, f64)], dm: Vec2, dm1: Vec2, dm2: Vec2) -> f64 {
583 coeffs
584 .iter()
585 .map(|&(wi, xi)| {
586 let d = dm + dm2 * (xi * xi);
587 let dpx = (d + dm1 * xi).hypot();
588 let dmx = (d - dm1 * xi).hypot();
589 (2.25f64.sqrt() * wi) * (dpx + dmx)
590 })
591 .sum::<f64>()
592}
593
594fn arclen_rec(c: &CubicBez, accuracy: f64, depth: usize) -> f64 {
595 let d03 = c.p3 - c.p0;
596 let d01 = c.p1 - c.p0;
597 let d12 = c.p2 - c.p1;
598 let d23 = c.p3 - c.p2;
599 let lp_lc = d01.hypot() + d12.hypot() + d23.hypot() - d03.hypot();
600 let dd1 = d12 - d01;
601 let dd2 = d23 - d12;
602 let dm = 0.25 * (d01 + d23) + 0.5 * d12; let dm1 = 0.5 * (dd2 + dd1); let dm2 = 0.25 * (dd2 - dd1); let est = GAUSS_LEGENDRE_COEFFS_8
609 .iter()
610 .map(|&(wi, xi)| {
611 wi * {
612 let d_norm2 = (dm + dm1 * xi + dm2 * (xi * xi)).hypot2();
613 let dd_norm2 = (dm1 + dm2 * (2.0 * xi)).hypot2();
614 dd_norm2 / d_norm2
615 }
616 })
617 .sum::<f64>();
618 let est_gauss8_error = (est.powi(3) * 2.5e-6).min(3e-2) * lp_lc;
619 if est_gauss8_error < accuracy {
620 return arclen_quadrature_core(GAUSS_LEGENDRE_COEFFS_8_HALF, dm, dm1, dm2);
621 }
622 let est_gauss16_error = (est.powi(6) * 1.5e-11).min(9e-3) * lp_lc;
623 if est_gauss16_error < accuracy {
624 return arclen_quadrature_core(GAUSS_LEGENDRE_COEFFS_16_HALF, dm, dm1, dm2);
625 }
626 let est_gauss24_error = (est.powi(9) * 3.5e-16).min(3.5e-3) * lp_lc;
627 if est_gauss24_error < accuracy || depth >= 20 {
628 return arclen_quadrature_core(GAUSS_LEGENDRE_COEFFS_24_HALF, dm, dm1, dm2);
629 }
630 let (c0, c1) = c.subdivide();
631 arclen_rec(&c0, accuracy * 0.5, depth + 1) + arclen_rec(&c1, accuracy * 0.5, depth + 1)
632}
633
634impl ParamCurveArclen for CubicBez {
635 fn arclen(&self, accuracy: f64) -> f64 {
640 arclen_rec(self, accuracy, 0)
641 }
642}
643
644impl ParamCurveArea for CubicBez {
645 #[inline]
646 fn signed_area(&self) -> f64 {
647 (self.p0.x * (6.0 * self.p1.y + 3.0 * self.p2.y + self.p3.y)
648 + 3.0
649 * (self.p1.x * (-2.0 * self.p0.y + self.p2.y + self.p3.y)
650 - self.p2.x * (self.p0.y + self.p1.y - 2.0 * self.p3.y))
651 - self.p3.x * (self.p0.y + 3.0 * self.p1.y + 6.0 * self.p2.y))
652 * (1.0 / 20.0)
653 }
654}
655
656impl ParamCurveNearest for CubicBez {
657 fn nearest(&self, p: Point, accuracy: f64) -> Nearest {
659 let mut best_r = None;
660 let mut best_t = 0.0;
661 for (t0, t1, q) in self.to_quads(accuracy) {
662 let nearest = q.nearest(p, accuracy);
663 if best_r
664 .map(|best_r| nearest.distance_sq < best_r)
665 .unwrap_or(true)
666 {
667 best_t = t0 + nearest.t * (t1 - t0);
668 best_r = Some(nearest.distance_sq);
669 }
670 }
671 Nearest {
672 t: best_t,
673 distance_sq: best_r.unwrap(),
674 }
675 }
676}
677
678impl ParamCurveCurvature for CubicBez {}
679
680impl ParamCurveExtrema for CubicBez {
681 fn extrema(&self) -> ArrayVec<f64, MAX_EXTREMA> {
682 fn one_coord(result: &mut ArrayVec<f64, MAX_EXTREMA>, d0: f64, d1: f64, d2: f64) {
683 let a = d0 - 2.0 * d1 + d2;
684 let b = 2.0 * (d1 - d0);
685 let c = d0;
686 let roots = solve_quadratic(c, b, a);
687 for &t in &roots {
688 if t > 0.0 && t < 1.0 {
689 result.push(t);
690 }
691 }
692 }
693 let mut result = ArrayVec::new();
694 let d0 = self.p1 - self.p0;
695 let d1 = self.p2 - self.p1;
696 let d2 = self.p3 - self.p2;
697 one_coord(&mut result, d0.x, d1.x, d2.x);
698 one_coord(&mut result, d0.y, d1.y, d2.y);
699 result.sort_by(|a, b| a.partial_cmp(b).unwrap());
700 result
701 }
702}
703
704impl Mul<CubicBez> for Affine {
705 type Output = CubicBez;
706
707 #[inline]
708 fn mul(self, c: CubicBez) -> CubicBez {
709 CubicBez {
710 p0: self * c.p0,
711 p1: self * c.p1,
712 p2: self * c.p2,
713 p3: self * c.p3,
714 }
715 }
716}
717
718impl Iterator for ToQuads {
719 type Item = (f64, f64, QuadBez);
720
721 #[inline]
722 fn next(&mut self) -> Option<(f64, f64, QuadBez)> {
723 if self.i == self.n {
724 return None;
725 }
726 let t0 = self.i as f64 / self.n as f64;
727 let t1 = (self.i + 1) as f64 / self.n as f64;
728 let seg = self.c.subsegment(t0..t1);
729 let p1x2 = 3.0 * seg.p1.to_vec2() - seg.p0.to_vec2();
730 let p2x2 = 3.0 * seg.p2.to_vec2() - seg.p3.to_vec2();
731 let result = QuadBez::new(seg.p0, ((p1x2 + p2x2) / 4.0).to_point(), seg.p3);
732 self.i += 1;
733 Some((t0, t1, result))
734 }
735
736 fn size_hint(&self) -> (usize, Option<usize>) {
737 let remaining = self.n - self.i;
738 (remaining, Some(remaining))
739 }
740}
741
742pub fn cubics_to_quadratic_splines(curves: &[CubicBez], accuracy: f64) -> Option<Vec<QuadSpline>> {
748 let mut result = Vec::new();
749 let mut split_order = 0;
750
751 while split_order <= MAX_SPLINE_SPLIT {
752 split_order += 1;
753 result.clear();
754
755 for curve in curves {
756 match curve.approx_spline_n(split_order, accuracy) {
757 Some(spline) => result.push(spline),
758 None => break,
759 }
760 }
761
762 if result.len() == curves.len() {
763 return Some(result);
764 }
765 }
766 None
767}
768#[cfg(test)]
769mod tests {
770 use crate::{
771 cubics_to_quadratic_splines, Affine, CubicBez, Nearest, ParamCurve, ParamCurveArclen,
772 ParamCurveArea, ParamCurveDeriv, ParamCurveExtrema, ParamCurveNearest, Point, QuadBez,
773 QuadSpline,
774 };
775
776 #[test]
777 fn cubicbez_deriv() {
778 let c = CubicBez::new(
780 (0.0, 0.0),
781 (1.0 / 3.0, 0.0),
782 (2.0 / 3.0, 1.0 / 3.0),
783 (1.0, 1.0),
784 );
785 let deriv = c.deriv();
786
787 let n = 10;
788 for i in 0..=n {
789 let t = (i as f64) * (n as f64).recip();
790 let delta = 1e-6;
791 let p = c.eval(t);
792 let p1 = c.eval(t + delta);
793 let d_approx = (p1 - p) * delta.recip();
794 let d = deriv.eval(t).to_vec2();
795 assert!((d - d_approx).hypot() < delta * 2.0);
796 }
797 }
798
799 #[test]
800 fn cubicbez_arclen() {
801 let c = CubicBez::new(
803 (0.0, 0.0),
804 (1.0 / 3.0, 0.0),
805 (2.0 / 3.0, 1.0 / 3.0),
806 (1.0, 1.0),
807 );
808 let true_arclen = 0.5 * 5.0f64.sqrt() + 0.25 * (2.0 + 5.0f64.sqrt()).ln();
809 for i in 0..12 {
810 let accuracy = 0.1f64.powi(i);
811 let error = c.arclen(accuracy) - true_arclen;
812 assert!(error.abs() < accuracy);
813 }
814 }
815
816 #[test]
817 fn cubicbez_inv_arclen() {
818 let c = CubicBez::new(
820 (0.0, 0.0),
821 (100.0 / 3.0, 0.0),
822 (200.0 / 3.0, 100.0 / 3.0),
823 (100.0, 100.0),
824 );
825 let true_arclen = 100.0 * (0.5 * 5.0f64.sqrt() + 0.25 * (2.0 + 5.0f64.sqrt()).ln());
826 for i in 0..12 {
827 let accuracy = 0.1f64.powi(i);
828 let n = 10;
829 for j in 0..=n {
830 let arc = (j as f64) * ((n as f64).recip() * true_arclen);
831 let t = c.inv_arclen(arc, accuracy * 0.5);
832 let actual_arc = c.subsegment(0.0..t).arclen(accuracy * 0.5);
833 assert!(
834 (arc - actual_arc).abs() < accuracy,
835 "at accuracy {accuracy:e}, wanted {actual_arc} got {arc}"
836 );
837 }
838 }
839 let accuracy = true_arclen * 1.1;
841 let arc = true_arclen * 0.5;
842 let t = c.inv_arclen(arc, accuracy);
843 let actual_arc = c.subsegment(0.0..t).arclen(accuracy);
844 assert!(
845 (arc - actual_arc).abs() < 2.0 * accuracy,
846 "at accuracy {accuracy:e}, want {actual_arc} got {arc}"
847 );
848 }
849
850 #[test]
851 fn cubicbez_inv_arclen_accuracy() {
852 let c = CubicBez::new((0.2, 0.73), (0.35, 1.08), (0.85, 1.08), (1.0, 0.73));
853 let true_t = c.inv_arclen(0.5, 1e-12);
854 for i in 1..12 {
855 let accuracy = (0.1f64).powi(i);
856 let approx_t = c.inv_arclen(0.5, accuracy);
857 assert!((approx_t - true_t).abs() <= accuracy);
858 }
859 }
860
861 #[test]
862 #[allow(clippy::float_cmp)]
863 fn cubicbez_signed_area_linear() {
864 let c = CubicBez::new(
866 (1.0, 0.0),
867 (2.0 / 3.0, 1.0 / 3.0),
868 (1.0 / 3.0, 2.0 / 3.0),
869 (0.0, 1.0),
870 );
871 let epsilon = 1e-12;
872 assert_eq!((Affine::rotate(0.5) * c).signed_area(), 0.5);
873 assert!(((Affine::rotate(0.5) * c).signed_area() - 0.5).abs() < epsilon);
874 assert!(((Affine::translate((0.0, 1.0)) * c).signed_area() - 1.0).abs() < epsilon);
875 assert!(((Affine::translate((1.0, 0.0)) * c).signed_area() - 1.0).abs() < epsilon);
876 }
877
878 #[test]
879 fn cubicbez_signed_area() {
880 let c = CubicBez::new((1.0, 0.0), (2.0 / 3.0, 1.0), (1.0 / 3.0, 1.0), (0.0, 1.0));
882 let epsilon = 1e-12;
883 assert!((c.signed_area() - 0.75).abs() < epsilon);
884 assert!(((Affine::rotate(0.5) * c).signed_area() - 0.75).abs() < epsilon);
885 assert!(((Affine::translate((0.0, 1.0)) * c).signed_area() - 1.25).abs() < epsilon);
886 assert!(((Affine::translate((1.0, 0.0)) * c).signed_area() - 1.25).abs() < epsilon);
887 }
888
889 #[test]
890 fn cubicbez_nearest() {
891 fn verify(result: Nearest, expected: f64) {
892 assert!(
893 (result.t - expected).abs() < 1e-6,
894 "got {result:?} expected {expected}"
895 );
896 }
897 let c = CubicBez::new((0.0, 0.0), (1.0 / 3.0, 0.0), (2.0 / 3.0, 0.0), (1.0, 1.0));
899 verify(c.nearest((0.1, 0.001).into(), 1e-6), 0.1);
900 verify(c.nearest((0.2, 0.008).into(), 1e-6), 0.2);
901 verify(c.nearest((0.3, 0.027).into(), 1e-6), 0.3);
902 verify(c.nearest((0.4, 0.064).into(), 1e-6), 0.4);
903 verify(c.nearest((0.5, 0.125).into(), 1e-6), 0.5);
904 verify(c.nearest((0.6, 0.216).into(), 1e-6), 0.6);
905 verify(c.nearest((0.7, 0.343).into(), 1e-6), 0.7);
906 verify(c.nearest((0.8, 0.512).into(), 1e-6), 0.8);
907 verify(c.nearest((0.9, 0.729).into(), 1e-6), 0.9);
908 verify(c.nearest((1.0, 1.0).into(), 1e-6), 1.0);
909 verify(c.nearest((1.1, 1.1).into(), 1e-6), 1.0);
910 verify(c.nearest((-0.1, 0.0).into(), 1e-6), 0.0);
911 let a = Affine::rotate(0.5);
912 verify((a * c).nearest(a * Point::new(0.1, 0.001), 1e-6), 0.1);
913 }
914
915 #[test]
917 fn degenerate_to_quads() {
918 let c = CubicBez::new((0., 9.), (6., 6.), (12., 3.0), (18., 0.0));
919 let quads = c.to_quads(1e-6).collect::<Vec<_>>();
920 assert_eq!(quads.len(), 1, "{:?}", &quads);
921 }
922
923 #[test]
924 fn cubicbez_extrema() {
925 let q = CubicBez::new((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0));
927 let extrema = q.extrema();
928 assert_eq!(extrema.len(), 1);
929 assert!((extrema[0] - 0.5).abs() < 1e-6);
930
931 let q = CubicBez::new((0.4, 0.5), (0.0, 1.0), (1.0, 0.0), (0.5, 0.4));
932 let extrema = q.extrema();
933 assert_eq!(extrema.len(), 4);
934 }
935
936 #[test]
937 fn cubicbez_toquads() {
938 let c = CubicBez::new((0.0, 0.0), (1.0 / 3.0, 0.0), (2.0 / 3.0, 0.0), (1.0, 1.0));
940 for i in 0..10 {
941 let accuracy = 0.1f64.powi(i);
942 let mut worst: f64 = 0.0;
943 for (t0, t1, q) in c.to_quads(accuracy) {
944 let epsilon = 1e-12;
945 assert!((q.start() - c.eval(t0)).hypot() < epsilon);
946 assert!((q.end() - c.eval(t1)).hypot() < epsilon);
947 let n = 4;
948 for j in 0..=n {
949 let t = (j as f64) * (n as f64).recip();
950 let p = q.eval(t);
951 let err = (p.y - p.x.powi(3)).abs();
952 worst = worst.max(err);
953 assert!(err < accuracy, "got {err} wanted {accuracy}");
954 }
955 }
956 }
957 }
958
959 #[test]
960 fn cubicbez_approx_spline() {
961 let c1 = CubicBez::new(
962 (550.0, 258.0),
963 (1044.0, 482.0),
964 (2029.0, 1841.0),
965 (1934.0, 1554.0),
966 );
967
968 let quad = c1.try_approx_quadratic(344.0);
969 let expected = QuadBez::new(
970 Point::new(550.0, 258.0),
971 Point::new(1673.665720592873, 767.5164401068898),
972 Point::new(1934.0, 1554.0),
973 );
974 assert!(quad.is_some());
975 assert_eq!(quad.unwrap(), expected);
976
977 let quad = c1.try_approx_quadratic(343.0);
978 assert!(quad.is_none());
979
980 let spline = c1.approx_spline_n(2, 343.0);
981 assert!(spline.is_some());
982 let spline = spline.unwrap();
983 let expected = [
984 Point::new(550.0, 258.0),
985 Point::new(920.5, 426.0),
986 Point::new(2005.25, 1769.25),
987 Point::new(1934.0, 1554.0),
988 ];
989 assert_eq!(spline.points().len(), expected.len());
990 for (got, &wanted) in spline.points().iter().zip(expected.iter()) {
991 assert!(got.distance(wanted) < 5.0);
992 }
993
994 let spline = c1.approx_spline(5.0);
995 let expected = [
996 Point::new(550.0, 258.0),
997 Point::new(673.5, 314.0),
998 Point::new(984.8777777777776, 584.2666666666667),
999 Point::new(1312.6305555555557, 927.825),
1000 Point::new(1613.1194444444443, 1267.425),
1001 Point::new(1842.7055555555555, 1525.8166666666666),
1002 Point::new(1957.75, 1625.75),
1003 Point::new(1934.0, 1554.0),
1004 ];
1005 assert!(spline.is_some());
1006 let spline = spline.unwrap();
1007 assert_eq!(spline.points().len(), expected.len());
1008 for (got, &wanted) in spline.points().iter().zip(expected.iter()) {
1009 assert!(got.distance(wanted) < 5.0);
1010 }
1011 }
1012
1013 #[test]
1014 fn cubicbez_cubics_to_quadratic_splines() {
1015 let curves = vec![
1016 CubicBez::new(
1017 (550.0, 258.0),
1018 (1044.0, 482.0),
1019 (2029.0, 1841.0),
1020 (1934.0, 1554.0),
1021 ),
1022 CubicBez::new(
1023 (859.0, 384.0),
1024 (1998.0, 116.0),
1025 (1596.0, 1772.0),
1026 (8.0, 1824.0),
1027 ),
1028 CubicBez::new(
1029 (1090.0, 937.0),
1030 (418.0, 1300.0),
1031 (125.0, 91.0),
1032 (104.0, 37.0),
1033 ),
1034 ];
1035 let converted = cubics_to_quadratic_splines(&curves, 5.0);
1036 assert!(converted.is_some());
1037 let converted = converted.unwrap();
1038 assert_eq!(converted[0].points().len(), 8);
1039 assert_eq!(converted[1].points().len(), 8);
1040 assert_eq!(converted[2].points().len(), 8);
1041 assert!(converted[0].points()[1].distance(Point::new(673.5, 314.0)) < 0.0001);
1042 assert!(
1043 converted[0].points()[2].distance(Point::new(88639.0 / 90.0, 52584.0 / 90.0)) < 0.0001
1044 );
1045 }
1046
1047 #[test]
1048 fn cubicbez_approx_spline_div_exact() {
1049 let cubic = CubicBez::new(
1053 Point::new(408.0, 321.0),
1054 Point::new(408.0, 452.0),
1055 Point::new(342.0, 560.0),
1056 Point::new(260.0, 560.0),
1057 );
1058 let spline = cubic.approx_spline(1.0).unwrap();
1059 assert_eq!(
1060 spline.points(),
1061 &[
1062 Point::new(408.0, 321.0),
1063 Point::new(408.0, 386.5),
1066 Point::new(368.16666666666663, 495.0833333333333),
1067 Point::new(301.0, 560.0),
1068 Point::new(260.0, 560.0)
1069 ]
1070 );
1071 }
1072
1073 #[test]
1074 fn cubicbez_inflections() {
1075 let c = CubicBez::new((0., 0.), (0.8, 1.), (0.2, 1.), (1., 0.));
1076 let inflections = c.inflections();
1077 assert_eq!(inflections.len(), 2);
1078 assert!((inflections[0] - 0.311018).abs() < 1e-6);
1079 assert!((inflections[1] - 0.688982).abs() < 1e-6);
1080 let c = CubicBez::new((0., 0.), (1., 1.), (2., -1.), (3., 0.));
1081 let inflections = c.inflections();
1082 assert_eq!(inflections.len(), 1);
1083 assert!((inflections[0] - 0.5).abs() < 1e-6);
1084 let c = CubicBez::new((0., 0.), (1., 1.), (2., 1.), (3., 0.));
1085 let inflections = c.inflections();
1086 assert_eq!(inflections.len(), 0);
1087 }
1088
1089 #[test]
1090 fn cubic_to_quadratic_matches_python() {
1091 let cubic = CubicBez {
1093 p0: (796.0, 319.0).into(),
1094 p1: (727.0, 314.0).into(),
1095 p2: (242.0, 303.0).into(),
1096 p3: (106.0, 303.0).into(),
1097 };
1098
1099 assert!(cubic.approx_spline_n(7, 1.0).is_some());
1101
1102 assert!(cubics_to_quadratic_splines(&[cubic], 0.001).is_some());
1104 }
1105
1106 #[test]
1107 fn cubic_to_quadratic_all_points_equal() {
1108 let pt = Point::new(5.0, 5.0);
1109 let cubic = CubicBez::new(pt, pt, pt, pt);
1110 let quads = cubics_to_quadratic_splines(&[cubic], 0.1).unwrap();
1111 assert_eq!(quads, [QuadSpline::new(vec![pt, pt, pt])]);
1112 }
1113
1114 #[test]
1115 fn cubic_to_quadratic_3_points_equal_single_quad_within_tolerance() {
1116 let p0 = Point::new(5.0, 5.0);
1117 let p1 = Point::new(5.0, 5.1);
1118 let cubic = CubicBez::new(p0, p0, p0, p1);
1119 let quads = cubics_to_quadratic_splines(&[cubic], 0.1).unwrap();
1120 assert_eq!(quads, [QuadSpline::new(vec![p0, p0, p1])]);
1122 }
1123
1124 #[test]
1125 fn cubic_to_quadratic_3_points_equal_exceeding_tolerance() {
1126 let p0 = Point::new(5.0, 5.0);
1127 let p1 = Point::new(5.0, 5.1);
1128 let cubic = CubicBez::new(p0, p0, p0, p1);
1129 let quads = cubics_to_quadratic_splines(&[cubic], 0.01).unwrap();
1130 assert_eq!(
1133 quads,
1134 [QuadSpline::new(vec![p0, p0, (5.0, 5.025).into(), p1])]
1135 );
1136 }
1137
1138 #[test]
1139 fn cubics_to_quadratic_splines_matches_python() {
1140 let light = CubicBez::new((378., 608.), (378., 524.), (355., 455.), (266., 455.));
1142 let regular = CubicBez::new((367., 607.), (367., 511.), (338., 472.), (243., 472.));
1143 let bold = CubicBez::new(
1144 (372.425, 593.05),
1145 (372.425, 524.95),
1146 (355.05, 485.95),
1147 (274., 485.95),
1148 );
1149 let qsplines = cubics_to_quadratic_splines(&[light, regular, bold], 1.0).unwrap();
1150 assert_eq!(
1151 qsplines,
1152 [
1153 QuadSpline::new(vec![
1154 (378.0, 608.0).into(),
1155 (378.0, 566.0).into(),
1156 (359.0833333333333, 496.5).into(),
1157 (310.5, 455.0).into(),
1158 (266.0, 455.0).into(),
1159 ]),
1160 QuadSpline::new(vec![
1161 (367.0, 607.0).into(),
1162 (367.0, 559.0).into(),
1163 (344.5833333333333, 499.49999999999994).into(),
1165 (290.5, 472.0).into(),
1166 (243.0, 472.0).into(),
1167 ]),
1168 QuadSpline::new(vec![
1169 (372.425, 593.05).into(),
1170 (372.425, 559.0).into(),
1171 (356.98333333333335, 511.125).into(),
1172 (314.525, 485.95).into(),
1173 (274.0, 485.95).into(),
1174 ]),
1175 ]
1176 );
1177 }
1178}