Skip to main content

kurbo/
cubicbez.rs

1// Copyright 2018 the Kurbo Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Cubic Bézier segments.
5
6use 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    GAUSS_LEGENDRE_COEFFS_8, GAUSS_LEGENDRE_COEFFS_8_HALF, GAUSS_LEGENDRE_COEFFS_16_HALF,
16    GAUSS_LEGENDRE_COEFFS_24_HALF, solve_cubic, solve_quadratic, solve_quartic,
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/// A single cubic Bézier segment.
29#[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
40/// An iterator which produces quadratic Bézier segments.
41struct ToQuads {
42    c: CubicBez,
43    i: usize,
44    n: usize,
45}
46
47/// Classification result for cusp detection.
48#[derive(Debug)]
49pub enum CuspType {
50    /// Cusp is a loop.
51    Loop,
52    /// Cusp has two closely spaced inflection points.
53    DoubleInflection,
54}
55
56impl CubicBez {
57    /// Create a new cubic Bézier segment.
58    #[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    /// Convert to quadratic Béziers.
69    ///
70    /// The iterator returns the start and end parameter in the cubic of each quadratic
71    /// segment, along with the quadratic.
72    ///
73    /// Note that the resulting quadratic Béziers are not in general G1 continuous;
74    /// they are optimized for minimizing distance error.
75    ///
76    /// This iterator will always produce at least one `QuadBez`.
77    #[inline]
78    pub fn to_quads(&self, accuracy: f64) -> impl Iterator<Item = (f64, f64, QuadBez)> {
79        // The maximum error, as a vector from the cubic to the best approximating
80        // quadratic, is proportional to the third derivative, which is constant
81        // across the segment. Thus, the error scales down as the third power of
82        // the number of subdivisions. Our strategy then is to subdivide `t` evenly.
83        //
84        // This is an overestimate of the error because only the component
85        // perpendicular to the first derivative is important. But the simplicity is
86        // appealing.
87
88        // This magic number is the square of 36 / sqrt(3).
89        // See: http://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html
90        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    /// Return a [`QuadSpline`] approximating this cubic Bézier.
100    ///
101    /// Returns `None` if no suitable approximation is found within the given
102    /// tolerance.
103    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    // Approximate a cubic curve with a quadratic spline of `n` curves
108    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        // The above function guarantees that the iterator returns n items,
117        // which is why we're unwrapping things with wild abandon.
118        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    /// Approximate a cubic with a single quadratic
162    ///
163    /// Returns a quadratic approximating the given cubic that maintains
164    /// endpoint tangents if that is within tolerance, or `None` otherwise.
165    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                // with 3 to 4 consecutive equal points (with no valid crossing_point),
170                // we can try to use the common point as the quadratic control point.
171                // This matches fonttools' cu2qu: https://github.com/fonttools/fonttools/pull/3904
172                (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        // for certain small values of `n` we precompute all our values.
195        // if we have six or fewer items we precompute them.
196        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        // a limitation of returning 'impl Trait' is that the implementation
226        // can only return a single concrete type; that is you cannot return
227        // Vec::into_iter() from one branch, and then HashSet::into_iter from
228        // another branch.
229        //
230        // This means we have to get a bit fancy, and have a single concrete
231        // type that represents both of our possible cases.
232
233        let mut storage = if storage.is_empty() {
234            None
235        } else {
236            Some(storage)
237        };
238
239        // used in the fallback case
240        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 storage exists, we use it exclusively
248            if let Some(storage) = storage.as_mut() {
249                return storage.pop();
250            }
251
252            // if storage does not exist, we are exclusively working down here.
253            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    /// Rust port of cu2qu [calc_cubic_points](https://github.com/fonttools/fonttools/blob/3b9a73ff8379ab49d3ce35aaaaf04b3a7d9d1655/Lib/fontTools/cu2qu/cu2qu.py#L63-L68)
278    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        // The original Python cu2qu code here does not use division operator to divide by 27 but
294        // instead uses multiplication by the reciprocal 1 / 27. We want to match it exactly
295        // to avoid any floating point differences, hence in this particular case we do not use div_exact.
296        // I could directly use the Vec2 Div trait (also implemented as multiplication by reciprocal)
297        // but I prefer to be explicit here.
298        // Source: https://github.com/fonttools/fonttools/blob/85c80be/Lib/fontTools/cu2qu/cu2qu.py#L215-L218
299        // See also: https://github.com/linebender/kurbo/issues/272
300        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    /// Does this curve fit inside the given distance from the origin?
323    ///
324    /// Rust port of cu2qu [cubic_farthest_fit_inside](https://github.com/fonttools/fonttools/blob/3b9a73ff8379ab49d3ce35aaaaf04b3a7d9d1655/Lib/fontTools/cu2qu/cu2qu.py#L281)
325    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        // Split in two. Note that cu2qu here uses a 3/8 subdivision. I don't know why.
336        let (left, right) = self.subdivide();
337        left.fit_inside(distance) && right.fit_inside(distance)
338    }
339
340    /// Is this cubic Bezier curve finite?
341    #[inline]
342    pub const 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    /// Is this cubic Bezier curve NaN?
347    #[inline]
348    pub const 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    /// Determine the inflection points.
353    ///
354    /// Return value is t parameter for the inflection points of the curve segment.
355    /// There are a maximum of two for a cubic Bézier.
356    ///
357    /// See <https://www.caffeineowl.com/graphics/2d/vectorial/cubic-inflexion.html>
358    /// for the theory.
359    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    /// Find points on the curve where the tangent line passes through the
371    /// given point.
372    ///
373    /// Result is array of t values such that the tangent line from the curve
374    /// evaluated at that point goes through the argument point.
375    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        // coefficients of x(t) \cross x'(t)
379        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    /// Preprocess a cubic Bézier to ease numerical robustness.
392    ///
393    /// If the cubic Bézier segment has zero or near-zero derivatives as an interior
394    /// cusp, perturb the control points to make curvature finite, avoiding
395    /// numerical robustness problems in offset and stroke.
396    pub(crate) fn regularize_cusp(&self, dimension: f64) -> CubicBez {
397        let mut c = *self;
398        // First step: if control point is too near the endpoint, nudge it away
399        // along the tangent.
400        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                    // Avoid making control distance smaller than dimension
412                    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    /// Detect whether there is a cusp.
425    ///
426    /// Return a cusp classification if there is a cusp with curvature greater than
427    /// the reciprocal of the given dimension.
428    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            // accuracy isn't used for quadratic nearest
441            let nearest = q.nearest(Point::ORIGIN, 1e-9);
442            // detect whether curvature at minimum derivative exceeds 1/dimension,
443            // without division.
444            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    /// Finds the value of `t` for which `self.eval(t)` is about `y`.
463    ///
464    /// Assumes that this segment is monotonic in `y` and that it crosses
465    /// the height `y`. (Under these assumptions, there is a unique answer.)
466    pub(crate) fn solve_monotonic_for_y(&self, y: f64) -> f64 {
467        let start = self.start();
468        let end = self.end();
469
470        debug_assert!(start.y.min(end.y) <= y && y <= start.y.max(end.y));
471
472        let p1 = self.p1;
473        let p2 = self.p2;
474        let a = end.y - 3.0 * p2.y + 3.0 * p1.y - start.y;
475        let b = 3.0 * (p2.y - 2.0 * p1.y + start.y);
476        let c = 3.0 * (p1.y - start.y);
477        let d = start.y - y;
478        for t in solve_cubic(d, c, b, a) {
479            if (0.0..=1.0).contains(&t) {
480                return t;
481            }
482        }
483
484        // Even though we asserted that our y range contains `y`, it's possible
485        // that we failed to find a solution numerically. (For example, rounding
486        // of a, b, or c might have pushed the root outside of [0.0, 1.0].)
487        // If we failed to find a solution, the real solution should be close
488        // to one of the endpoints. So just find whichever endpoint was closer.
489        if (start.y - y).abs() <= (end.y - y).abs() {
490            0.0
491        } else {
492            1.0
493        }
494    }
495}
496
497/// An iterator for cubic beziers.
498pub struct CubicBezIter {
499    cubic: CubicBez,
500    ix: usize,
501}
502
503impl Shape for CubicBez {
504    type PathElementsIter<'iter> = CubicBezIter;
505
506    #[inline]
507    fn path_elements(&self, _tolerance: f64) -> CubicBezIter {
508        CubicBezIter {
509            cubic: *self,
510            ix: 0,
511        }
512    }
513
514    #[inline(always)]
515    fn area(&self) -> f64 {
516        0.0
517    }
518
519    #[inline]
520    fn perimeter(&self, accuracy: f64) -> f64 {
521        self.arclen(accuracy)
522    }
523
524    #[inline(always)]
525    fn winding(&self, _pt: Point) -> i32 {
526        0
527    }
528
529    #[inline]
530    fn bounding_box(&self) -> Rect {
531        ParamCurveExtrema::bounding_box(self)
532    }
533}
534
535impl Iterator for CubicBezIter {
536    type Item = PathEl;
537
538    fn next(&mut self) -> Option<PathEl> {
539        self.ix += 1;
540        match self.ix {
541            1 => Some(PathEl::MoveTo(self.cubic.p0)),
542            2 => Some(PathEl::CurveTo(self.cubic.p1, self.cubic.p2, self.cubic.p3)),
543            _ => None,
544        }
545    }
546}
547
548impl ParamCurve for CubicBez {
549    #[inline]
550    fn eval(&self, t: f64) -> Point {
551        let mt = 1.0 - t;
552        let v = self.p0.to_vec2() * (mt * mt * mt)
553            + (self.p1.to_vec2() * (mt * mt * 3.0)
554                + (self.p2.to_vec2() * (mt * 3.0) + self.p3.to_vec2() * t) * t)
555                * t;
556        v.to_point()
557    }
558
559    fn subsegment(&self, range: Range<f64>) -> CubicBez {
560        let (t0, t1) = (range.start, range.end);
561        let p0 = self.eval(t0);
562        let p3 = self.eval(t1);
563        let d = self.deriv();
564        let scale = (t1 - t0) * (1.0 / 3.0);
565        let p1 = p0 + scale * d.eval(t0).to_vec2();
566        let p2 = p3 - scale * d.eval(t1).to_vec2();
567        CubicBez { p0, p1, p2, p3 }
568    }
569
570    /// Subdivide into halves, using de Casteljau.
571    #[inline]
572    fn subdivide(&self) -> (CubicBez, CubicBez) {
573        let pm = self.eval(0.5);
574        (
575            CubicBez::new(
576                self.p0,
577                self.p0.midpoint(self.p1),
578                ((self.p0.to_vec2() + self.p1.to_vec2() * 2.0 + self.p2.to_vec2()) * 0.25)
579                    .to_point(),
580                pm,
581            ),
582            CubicBez::new(
583                pm,
584                ((self.p1.to_vec2() + self.p2.to_vec2() * 2.0 + self.p3.to_vec2()) * 0.25)
585                    .to_point(),
586                self.p2.midpoint(self.p3),
587                self.p3,
588            ),
589        )
590    }
591
592    #[inline(always)]
593    fn start(&self) -> Point {
594        self.p0
595    }
596
597    #[inline(always)]
598    fn end(&self) -> Point {
599        self.p3
600    }
601}
602
603impl ParamCurveDeriv for CubicBez {
604    type DerivResult = QuadBez;
605
606    #[inline]
607    fn deriv(&self) -> QuadBez {
608        QuadBez::new(
609            (3.0 * (self.p1 - self.p0)).to_point(),
610            (3.0 * (self.p2 - self.p1)).to_point(),
611            (3.0 * (self.p3 - self.p2)).to_point(),
612        )
613    }
614}
615
616fn arclen_quadrature_core(coeffs: &[(f64, f64)], dm: Vec2, dm1: Vec2, dm2: Vec2) -> f64 {
617    coeffs
618        .iter()
619        .map(|&(wi, xi)| {
620            let d = dm + dm2 * (xi * xi);
621            let dpx = (d + dm1 * xi).hypot();
622            let dmx = (d - dm1 * xi).hypot();
623            (2.25f64.sqrt() * wi) * (dpx + dmx)
624        })
625        .sum::<f64>()
626}
627
628fn arclen_rec(c: &CubicBez, accuracy: f64, depth: usize) -> f64 {
629    let d03 = c.p3 - c.p0;
630    let d01 = c.p1 - c.p0;
631    let d12 = c.p2 - c.p1;
632    let d23 = c.p3 - c.p2;
633    let lp_lc = d01.hypot() + d12.hypot() + d23.hypot() - d03.hypot();
634    let dd1 = d12 - d01;
635    let dd2 = d23 - d12;
636    // It might be faster to do direct multiplies, the data dependencies would be shorter.
637    // The following values don't have the factor of 3 for first deriv
638    let dm = 0.25 * (d01 + d23) + 0.5 * d12; // first derivative at midpoint
639    let dm1 = 0.5 * (dd2 + dd1); // second derivative at midpoint
640    let dm2 = 0.25 * (dd2 - dd1); // 0.5 * (third derivative at midpoint)
641
642    let est = GAUSS_LEGENDRE_COEFFS_8
643        .iter()
644        .map(|&(wi, xi)| {
645            wi * {
646                let d_norm2 = (dm + dm1 * xi + dm2 * (xi * xi)).hypot2();
647                let dd_norm2 = (dm1 + dm2 * (2.0 * xi)).hypot2();
648                dd_norm2 / d_norm2
649            }
650        })
651        .sum::<f64>();
652    let est_gauss8_error = (est.powi(3) * 2.5e-6).min(3e-2) * lp_lc;
653    if est_gauss8_error < accuracy {
654        return arclen_quadrature_core(GAUSS_LEGENDRE_COEFFS_8_HALF, dm, dm1, dm2);
655    }
656    let est_gauss16_error = (est.powi(6) * 1.5e-11).min(9e-3) * lp_lc;
657    if est_gauss16_error < accuracy {
658        return arclen_quadrature_core(GAUSS_LEGENDRE_COEFFS_16_HALF, dm, dm1, dm2);
659    }
660    let est_gauss24_error = (est.powi(9) * 3.5e-16).min(3.5e-3) * lp_lc;
661    if est_gauss24_error < accuracy || depth >= 20 {
662        return arclen_quadrature_core(GAUSS_LEGENDRE_COEFFS_24_HALF, dm, dm1, dm2);
663    }
664    let (c0, c1) = c.subdivide();
665    arclen_rec(&c0, accuracy * 0.5, depth + 1) + arclen_rec(&c1, accuracy * 0.5, depth + 1)
666}
667
668impl ParamCurveArclen for CubicBez {
669    /// Arclength of a cubic Bézier segment.
670    ///
671    /// This is an adaptive subdivision approach using Legendre-Gauss quadrature
672    /// in the base case, and an error estimate to decide when to subdivide.
673    fn arclen(&self, accuracy: f64) -> f64 {
674        arclen_rec(self, accuracy, 0)
675    }
676}
677
678impl ParamCurveArea for CubicBez {
679    #[inline]
680    fn signed_area(&self) -> f64 {
681        (self.p0.x * (6.0 * self.p1.y + 3.0 * self.p2.y + self.p3.y)
682            + 3.0
683                * (self.p1.x * (-2.0 * self.p0.y + self.p2.y + self.p3.y)
684                    - self.p2.x * (self.p0.y + self.p1.y - 2.0 * self.p3.y))
685            - self.p3.x * (self.p0.y + 3.0 * self.p1.y + 6.0 * self.p2.y))
686            * (1.0 / 20.0)
687    }
688}
689
690impl ParamCurveNearest for CubicBez {
691    /// Find the nearest point using a quintic solver.
692    ///
693    /// The polynomial `|self - p|^2` has degree 6, so we find its critical
694    /// points and evaluate them all to find the best one.
695    fn nearest(&self, p: Point, accuracy: f64) -> Nearest {
696        fn eval_t(p: Point, t_best: &mut f64, r_best: &mut Option<f64>, t: f64, p0: Point) {
697            let r = (p0 - p).hypot2();
698            if r_best.map(|r_best| r < r_best).unwrap_or(true) {
699                *r_best = Some(r);
700                *t_best = t;
701            }
702        }
703
704        let mut r_best = None;
705        let mut t_best = 0.0;
706
707        // Reparameterize `self - p` as q0 + q1 t + q2 t^2 + q3 t^3.
708        let q0 = self.p0 - p;
709        let q1 = 3.0 * (self.p1 - self.p0);
710        let q2 = 3.0 * (self.p0.to_vec2() - 2.0 * self.p1.to_vec2() + self.p2.to_vec2());
711        let q3 = -self.p0.to_vec2() + 3.0 * self.p1.to_vec2() - 3.0 * self.p2.to_vec2()
712            + self.p3.to_vec2();
713
714        // Coefficients of the degree-5 polynomial (self - p) \cdot tangent.
715        let c0 = q0.dot(q1);
716        let c1 = q1.hypot2() + 2.0 * q2.dot(q0);
717        let c2 = 3.0 * (q2.dot(q1) + q3.dot(q0));
718        let c3 = 4.0 * q3.dot(q1) + 2.0 * q2.hypot2();
719        let c4 = 5.0 * q3.dot(q2);
720        let c5 = 3.0 * q3.hypot2();
721
722        let roots = polycool::Poly::new([c0, c1, c2, c3, c4, c5]).roots_between(0.0, 1.0, accuracy);
723
724        for &t in &roots {
725            eval_t(p, &mut t_best, &mut r_best, t, self.eval(t));
726        }
727
728        // If we found all 5 critical points, we can skip evaluating the endpoints.
729        if roots.len() != 5 {
730            eval_t(p, &mut t_best, &mut r_best, 0.0, self.p0);
731            eval_t(p, &mut t_best, &mut r_best, 1.0, self.p3);
732        }
733
734        Nearest {
735            t: t_best,
736            distance_sq: r_best.unwrap(),
737        }
738    }
739}
740
741impl ParamCurveCurvature for CubicBez {}
742
743impl ParamCurveExtrema for CubicBez {
744    fn extrema(&self) -> ArrayVec<f64, MAX_EXTREMA> {
745        fn one_coord(result: &mut ArrayVec<f64, MAX_EXTREMA>, d0: f64, d1: f64, d2: f64) {
746            let a = d0 - 2.0 * d1 + d2;
747            let b = 2.0 * (d1 - d0);
748            let c = d0;
749            let roots = solve_quadratic(c, b, a);
750            for &t in &roots {
751                if t > 0.0 && t < 1.0 {
752                    result.push(t);
753                }
754            }
755        }
756        let mut result = ArrayVec::new();
757        let d0 = self.p1 - self.p0;
758        let d1 = self.p2 - self.p1;
759        let d2 = self.p3 - self.p2;
760        one_coord(&mut result, d0.x, d1.x, d2.x);
761        one_coord(&mut result, d0.y, d1.y, d2.y);
762        result.sort_by(|a, b| a.partial_cmp(b).unwrap());
763        result
764    }
765}
766
767impl Mul<CubicBez> for Affine {
768    type Output = CubicBez;
769
770    #[inline]
771    fn mul(self, c: CubicBez) -> CubicBez {
772        CubicBez {
773            p0: self * c.p0,
774            p1: self * c.p1,
775            p2: self * c.p2,
776            p3: self * c.p3,
777        }
778    }
779}
780
781impl Iterator for ToQuads {
782    type Item = (f64, f64, QuadBez);
783
784    #[inline]
785    fn next(&mut self) -> Option<(f64, f64, QuadBez)> {
786        if self.i == self.n {
787            return None;
788        }
789        let t0 = self.i as f64 / self.n as f64;
790        let t1 = (self.i + 1) as f64 / self.n as f64;
791        let seg = self.c.subsegment(t0..t1);
792        let p1x2 = 3.0 * seg.p1.to_vec2() - seg.p0.to_vec2();
793        let p2x2 = 3.0 * seg.p2.to_vec2() - seg.p3.to_vec2();
794        let result = QuadBez::new(seg.p0, ((p1x2 + p2x2) / 4.0).to_point(), seg.p3);
795        self.i += 1;
796        Some((t0, t1, result))
797    }
798
799    fn size_hint(&self) -> (usize, Option<usize>) {
800        let remaining = self.n - self.i;
801        (remaining, Some(remaining))
802    }
803}
804
805/// Convert multiple cubic Bézier curves to quadratic splines.
806///
807/// Ensures that the resulting splines have the same number of control points.
808///
809/// Rust port of cu2qu [cubic_approx_quadratic](https://github.com/fonttools/fonttools/blob/3b9a73ff8379ab49d3ce35aaaaf04b3a7d9d1655/Lib/fontTools/cu2qu/cu2qu.py#L322)
810pub fn cubics_to_quadratic_splines(curves: &[CubicBez], accuracy: f64) -> Option<Vec<QuadSpline>> {
811    let mut result = Vec::new();
812    let mut split_order = 0;
813
814    while split_order <= MAX_SPLINE_SPLIT {
815        split_order += 1;
816        result.clear();
817
818        for curve in curves {
819            match curve.approx_spline_n(split_order, accuracy) {
820                Some(spline) => result.push(spline),
821                None => break,
822            }
823        }
824
825        if result.len() == curves.len() {
826            return Some(result);
827        }
828    }
829    None
830}
831#[cfg(test)]
832mod tests {
833    use crate::{
834        Affine, CubicBez, Nearest, ParamCurve, ParamCurveArclen, ParamCurveArea, ParamCurveDeriv,
835        ParamCurveExtrema, ParamCurveNearest, Point, QuadBez, QuadSpline,
836        cubics_to_quadratic_splines,
837    };
838
839    #[test]
840    fn cubicbez_deriv() {
841        // y = x^2
842        let c = CubicBez::new(
843            (0.0, 0.0),
844            (1.0 / 3.0, 0.0),
845            (2.0 / 3.0, 1.0 / 3.0),
846            (1.0, 1.0),
847        );
848        let deriv = c.deriv();
849
850        let n = 10;
851        for i in 0..=n {
852            let t = (i as f64) * (n as f64).recip();
853            let delta = 1e-6;
854            let p = c.eval(t);
855            let p1 = c.eval(t + delta);
856            let d_approx = (p1 - p) * delta.recip();
857            let d = deriv.eval(t).to_vec2();
858            assert!((d - d_approx).hypot() < delta * 2.0);
859        }
860    }
861
862    #[test]
863    fn cubicbez_arclen() {
864        // y = x^2
865        let c = CubicBez::new(
866            (0.0, 0.0),
867            (1.0 / 3.0, 0.0),
868            (2.0 / 3.0, 1.0 / 3.0),
869            (1.0, 1.0),
870        );
871        let true_arclen = 0.5 * 5.0f64.sqrt() + 0.25 * (2.0 + 5.0f64.sqrt()).ln();
872        for i in 0..12 {
873            let accuracy = 0.1f64.powi(i);
874            let error = c.arclen(accuracy) - true_arclen;
875            assert!(error.abs() < accuracy);
876        }
877    }
878
879    #[test]
880    fn cubicbez_inv_arclen() {
881        // y = x^2 / 100
882        let c = CubicBez::new(
883            (0.0, 0.0),
884            (100.0 / 3.0, 0.0),
885            (200.0 / 3.0, 100.0 / 3.0),
886            (100.0, 100.0),
887        );
888        let true_arclen = 100.0 * (0.5 * 5.0f64.sqrt() + 0.25 * (2.0 + 5.0f64.sqrt()).ln());
889        for i in 0..12 {
890            let accuracy = 0.1f64.powi(i);
891            let n = 10;
892            for j in 0..=n {
893                let arc = (j as f64) * ((n as f64).recip() * true_arclen);
894                let t = c.inv_arclen(arc, accuracy * 0.5);
895                let actual_arc = c.subsegment(0.0..t).arclen(accuracy * 0.5);
896                assert!(
897                    (arc - actual_arc).abs() < accuracy,
898                    "at accuracy {accuracy:e}, wanted {actual_arc} got {arc}"
899                );
900            }
901        }
902        // corner case: user passes accuracy larger than total arc length
903        let accuracy = true_arclen * 1.1;
904        let arc = true_arclen * 0.5;
905        let t = c.inv_arclen(arc, accuracy);
906        let actual_arc = c.subsegment(0.0..t).arclen(accuracy);
907        assert!(
908            (arc - actual_arc).abs() < 2.0 * accuracy,
909            "at accuracy {accuracy:e}, want {actual_arc} got {arc}"
910        );
911    }
912
913    #[test]
914    fn cubicbez_inv_arclen_accuracy() {
915        let c = CubicBez::new((0.2, 0.73), (0.35, 1.08), (0.85, 1.08), (1.0, 0.73));
916        let true_t = c.inv_arclen(0.5, 1e-12);
917        for i in 1..12 {
918            let accuracy = (0.1f64).powi(i);
919            let approx_t = c.inv_arclen(0.5, accuracy);
920            assert!((approx_t - true_t).abs() <= accuracy);
921        }
922    }
923
924    #[test]
925    #[allow(clippy::float_cmp)]
926    fn cubicbez_signed_area_linear() {
927        // y = 1 - x
928        let c = CubicBez::new(
929            (1.0, 0.0),
930            (2.0 / 3.0, 1.0 / 3.0),
931            (1.0 / 3.0, 2.0 / 3.0),
932            (0.0, 1.0),
933        );
934        let epsilon = 1e-12;
935        assert_eq!((Affine::rotate(0.5) * c).signed_area(), 0.5);
936        assert!(((Affine::rotate(0.5) * c).signed_area() - 0.5).abs() < epsilon);
937        assert!(((Affine::translate((0.0, 1.0)) * c).signed_area() - 1.0).abs() < epsilon);
938        assert!(((Affine::translate((1.0, 0.0)) * c).signed_area() - 1.0).abs() < epsilon);
939    }
940
941    #[test]
942    fn cubicbez_signed_area() {
943        // y = 1 - x^3
944        let c = CubicBez::new((1.0, 0.0), (2.0 / 3.0, 1.0), (1.0 / 3.0, 1.0), (0.0, 1.0));
945        let epsilon = 1e-12;
946        assert!((c.signed_area() - 0.75).abs() < epsilon);
947        assert!(((Affine::rotate(0.5) * c).signed_area() - 0.75).abs() < epsilon);
948        assert!(((Affine::translate((0.0, 1.0)) * c).signed_area() - 1.25).abs() < epsilon);
949        assert!(((Affine::translate((1.0, 0.0)) * c).signed_area() - 1.25).abs() < epsilon);
950    }
951
952    #[test]
953    fn cubicbez_nearest() {
954        fn verify(result: Nearest, expected: f64) {
955            assert!(
956                (result.t - expected).abs() < 1e-6,
957                "got {result:?} expected {expected}"
958            );
959        }
960        // y = x^3
961        let c = CubicBez::new((0.0, 0.0), (1.0 / 3.0, 0.0), (2.0 / 3.0, 0.0), (1.0, 1.0));
962        verify(c.nearest((0.1, 0.001).into(), 1e-6), 0.1);
963        verify(c.nearest((0.2, 0.008).into(), 1e-6), 0.2);
964        verify(c.nearest((0.3, 0.027).into(), 1e-6), 0.3);
965        verify(c.nearest((0.4, 0.064).into(), 1e-6), 0.4);
966        verify(c.nearest((0.5, 0.125).into(), 1e-6), 0.5);
967        verify(c.nearest((0.6, 0.216).into(), 1e-6), 0.6);
968        verify(c.nearest((0.7, 0.343).into(), 1e-6), 0.7);
969        verify(c.nearest((0.8, 0.512).into(), 1e-6), 0.8);
970        verify(c.nearest((0.9, 0.729).into(), 1e-6), 0.9);
971        verify(c.nearest((1.0, 1.0).into(), 1e-6), 1.0);
972        verify(c.nearest((1.1, 1.1).into(), 1e-6), 1.0);
973        verify(c.nearest((-0.1, 0.0).into(), 1e-6), 0.0);
974        let a = Affine::rotate(0.5);
975        verify((a * c).nearest(a * Point::new(0.1, 0.001), 1e-6), 0.1);
976
977        // Here's a case that tripped up the old solver because the start is close to
978        // degenerate and the end is actually degenerate; see #446.
979        let curve = CubicBez::new(
980            (461.0, 123.0),
981            (460.99999999999994, 123.00000000000004),
982            (111.0, 319.0),
983            (111.0, 319.0),
984        );
985        let p = Point::new(282.0379003395483, 223.21877580985594);
986        let eps = 0.0005;
987        let nearest = curve.nearest(p, eps);
988        let q = curve.eval(nearest.t);
989        let r = curve.eval(0.5075474297354187);
990
991        assert!((q - p).hypot() <= (r - p).hypot() + eps);
992    }
993
994    // ensure to_quads returns something given collinear points
995    #[test]
996    fn degenerate_to_quads() {
997        let c = CubicBez::new((0., 9.), (6., 6.), (12., 3.0), (18., 0.0));
998        let quads = c.to_quads(1e-6).collect::<Vec<_>>();
999        assert_eq!(quads.len(), 1, "{:?}", &quads);
1000    }
1001
1002    #[test]
1003    fn cubicbez_extrema() {
1004        // y = x^2
1005        let q = CubicBez::new((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0));
1006        let extrema = q.extrema();
1007        assert_eq!(extrema.len(), 1);
1008        assert!((extrema[0] - 0.5).abs() < 1e-6);
1009
1010        let q = CubicBez::new((0.4, 0.5), (0.0, 1.0), (1.0, 0.0), (0.5, 0.4));
1011        let extrema = q.extrema();
1012        assert_eq!(extrema.len(), 4);
1013    }
1014
1015    #[test]
1016    fn cubicbez_toquads() {
1017        // y = x^3
1018        let c = CubicBez::new((0.0, 0.0), (1.0 / 3.0, 0.0), (2.0 / 3.0, 0.0), (1.0, 1.0));
1019        for i in 0..10 {
1020            let accuracy = 0.1f64.powi(i);
1021            let mut worst: f64 = 0.0;
1022            for (t0, t1, q) in c.to_quads(accuracy) {
1023                let epsilon = 1e-12;
1024                assert!((q.start() - c.eval(t0)).hypot() < epsilon);
1025                assert!((q.end() - c.eval(t1)).hypot() < epsilon);
1026                let n = 4;
1027                for j in 0..=n {
1028                    let t = (j as f64) * (n as f64).recip();
1029                    let p = q.eval(t);
1030                    let err = (p.y - p.x.powi(3)).abs();
1031                    worst = worst.max(err);
1032                    assert!(err < accuracy, "got {err} wanted {accuracy}");
1033                }
1034            }
1035        }
1036    }
1037
1038    #[test]
1039    fn cubicbez_approx_spline() {
1040        let c1 = CubicBez::new(
1041            (550.0, 258.0),
1042            (1044.0, 482.0),
1043            (2029.0, 1841.0),
1044            (1934.0, 1554.0),
1045        );
1046
1047        let quad = c1.try_approx_quadratic(344.0);
1048        let expected = QuadBez::new(
1049            Point::new(550.0, 258.0),
1050            Point::new(1673.665720592873, 767.5164401068898),
1051            Point::new(1934.0, 1554.0),
1052        );
1053        assert!(quad.is_some());
1054        assert_eq!(quad.unwrap(), expected);
1055
1056        let quad = c1.try_approx_quadratic(343.0);
1057        assert!(quad.is_none());
1058
1059        let spline = c1.approx_spline_n(2, 343.0);
1060        assert!(spline.is_some());
1061        let spline = spline.unwrap();
1062        let expected = [
1063            Point::new(550.0, 258.0),
1064            Point::new(920.5, 426.0),
1065            Point::new(2005.25, 1769.25),
1066            Point::new(1934.0, 1554.0),
1067        ];
1068        assert_eq!(spline.points().len(), expected.len());
1069        for (got, &wanted) in spline.points().iter().zip(expected.iter()) {
1070            assert!(got.distance(wanted) < 5.0);
1071        }
1072
1073        let spline = c1.approx_spline(5.0);
1074        let expected = [
1075            Point::new(550.0, 258.0),
1076            Point::new(673.5, 314.0),
1077            Point::new(984.8777777777776, 584.2666666666667),
1078            Point::new(1312.6305555555557, 927.825),
1079            Point::new(1613.1194444444443, 1267.425),
1080            Point::new(1842.7055555555555, 1525.8166666666666),
1081            Point::new(1957.75, 1625.75),
1082            Point::new(1934.0, 1554.0),
1083        ];
1084        assert!(spline.is_some());
1085        let spline = spline.unwrap();
1086        assert_eq!(spline.points().len(), expected.len());
1087        for (got, &wanted) in spline.points().iter().zip(expected.iter()) {
1088            assert!(got.distance(wanted) < 5.0);
1089        }
1090    }
1091
1092    #[test]
1093    fn cubicbez_cubics_to_quadratic_splines() {
1094        let curves = vec![
1095            CubicBez::new(
1096                (550.0, 258.0),
1097                (1044.0, 482.0),
1098                (2029.0, 1841.0),
1099                (1934.0, 1554.0),
1100            ),
1101            CubicBez::new(
1102                (859.0, 384.0),
1103                (1998.0, 116.0),
1104                (1596.0, 1772.0),
1105                (8.0, 1824.0),
1106            ),
1107            CubicBez::new(
1108                (1090.0, 937.0),
1109                (418.0, 1300.0),
1110                (125.0, 91.0),
1111                (104.0, 37.0),
1112            ),
1113        ];
1114        let converted = cubics_to_quadratic_splines(&curves, 5.0);
1115        assert!(converted.is_some());
1116        let converted = converted.unwrap();
1117        assert_eq!(converted[0].points().len(), 8);
1118        assert_eq!(converted[1].points().len(), 8);
1119        assert_eq!(converted[2].points().len(), 8);
1120        assert!(converted[0].points()[1].distance(Point::new(673.5, 314.0)) < 0.0001);
1121        assert!(
1122            converted[0].points()[2].distance(Point::new(88639.0 / 90.0, 52584.0 / 90.0)) < 0.0001
1123        );
1124    }
1125
1126    #[test]
1127    fn cubicbez_approx_spline_div_exact() {
1128        // Ensure rounding behavior for division matches fonttools
1129        // cu2qu.
1130        // See <https://github.com/linebender/kurbo/issues/272>
1131        let cubic = CubicBez::new(
1132            Point::new(408.0, 321.0),
1133            Point::new(408.0, 452.0),
1134            Point::new(342.0, 560.0),
1135            Point::new(260.0, 560.0),
1136        );
1137        let spline = cubic.approx_spline(1.0).unwrap();
1138        assert_eq!(
1139            spline.points(),
1140            &[
1141                Point::new(408.0, 321.0),
1142                // Previous behavior produced 386.49999999999994 for the
1143                // y coordinate leading to inconsistent rounding.
1144                Point::new(408.0, 386.5),
1145                Point::new(368.16666666666663, 495.0833333333333),
1146                Point::new(301.0, 560.0),
1147                Point::new(260.0, 560.0)
1148            ]
1149        );
1150    }
1151
1152    #[test]
1153    fn cubicbez_inflections() {
1154        let c = CubicBez::new((0., 0.), (0.8, 1.), (0.2, 1.), (1., 0.));
1155        let inflections = c.inflections();
1156        assert_eq!(inflections.len(), 2);
1157        assert!((inflections[0] - 0.311018).abs() < 1e-6);
1158        assert!((inflections[1] - 0.688982).abs() < 1e-6);
1159        let c = CubicBez::new((0., 0.), (1., 1.), (2., -1.), (3., 0.));
1160        let inflections = c.inflections();
1161        assert_eq!(inflections.len(), 1);
1162        assert!((inflections[0] - 0.5).abs() < 1e-6);
1163        let c = CubicBez::new((0., 0.), (1., 1.), (2., 1.), (3., 0.));
1164        let inflections = c.inflections();
1165        assert_eq!(inflections.len(), 0);
1166    }
1167
1168    #[test]
1169    fn cubic_to_quadratic_matches_python() {
1170        // from https://github.com/googlefonts/fontmake-rs/issues/217
1171        let cubic = CubicBez {
1172            p0: (796.0, 319.0).into(),
1173            p1: (727.0, 314.0).into(),
1174            p2: (242.0, 303.0).into(),
1175            p3: (106.0, 303.0).into(),
1176        };
1177
1178        // FontTools can approximate this curve successfully in 7 splits, we can too
1179        assert!(cubic.approx_spline_n(7, 1.0).is_some());
1180
1181        // FontTools can solve this with accuracy 0.001, we can too
1182        assert!(cubics_to_quadratic_splines(&[cubic], 0.001).is_some());
1183    }
1184
1185    #[test]
1186    fn cubic_to_quadratic_all_points_equal() {
1187        let pt = Point::new(5.0, 5.0);
1188        let cubic = CubicBez::new(pt, pt, pt, pt);
1189        let quads = cubics_to_quadratic_splines(&[cubic], 0.1).unwrap();
1190        assert_eq!(quads, [QuadSpline::new(vec![pt, pt, pt])]);
1191    }
1192
1193    #[test]
1194    fn cubic_to_quadratic_3_points_equal_single_quad_within_tolerance() {
1195        let p0 = Point::new(5.0, 5.0);
1196        let p1 = Point::new(5.0, 5.1);
1197        let cubic = CubicBez::new(p0, p0, p0, p1);
1198        let quads = cubics_to_quadratic_splines(&[cubic], 0.1).unwrap();
1199        // a single quadratic bezier approximates this cubic for the given tolerance
1200        assert_eq!(quads, [QuadSpline::new(vec![p0, p0, p1])]);
1201    }
1202
1203    #[test]
1204    fn cubic_to_quadratic_3_points_equal_exceeding_tolerance() {
1205        let p0 = Point::new(5.0, 5.0);
1206        let p1 = Point::new(5.0, 5.1);
1207        let cubic = CubicBez::new(p0, p0, p0, p1);
1208        let quads = cubics_to_quadratic_splines(&[cubic], 0.01).unwrap();
1209        // 2 quadratic off-curves are required to approximate the same cubic
1210        // given the smaller tolerance
1211        assert_eq!(
1212            quads,
1213            [QuadSpline::new(vec![p0, p0, (5.0, 5.025).into(), p1])]
1214        );
1215    }
1216
1217    #[test]
1218    fn cubics_to_quadratic_splines_matches_python() {
1219        // https://github.com/linebender/kurbo/pull/273
1220        let light = CubicBez::new((378., 608.), (378., 524.), (355., 455.), (266., 455.));
1221        let regular = CubicBez::new((367., 607.), (367., 511.), (338., 472.), (243., 472.));
1222        let bold = CubicBez::new(
1223            (372.425, 593.05),
1224            (372.425, 524.95),
1225            (355.05, 485.95),
1226            (274., 485.95),
1227        );
1228        let qsplines = cubics_to_quadratic_splines(&[light, regular, bold], 1.0).unwrap();
1229        assert_eq!(
1230            qsplines,
1231            [
1232                QuadSpline::new(vec![
1233                    (378.0, 608.0).into(),
1234                    (378.0, 566.0).into(),
1235                    (359.0833333333333, 496.5).into(),
1236                    (310.5, 455.0).into(),
1237                    (266.0, 455.0).into(),
1238                ]),
1239                QuadSpline::new(vec![
1240                    (367.0, 607.0).into(),
1241                    (367.0, 559.0).into(),
1242                    // Previous behavior produced 496.5 for the y coordinate
1243                    (344.5833333333333, 499.49999999999994).into(),
1244                    (290.5, 472.0).into(),
1245                    (243.0, 472.0).into(),
1246                ]),
1247                QuadSpline::new(vec![
1248                    (372.425, 593.05).into(),
1249                    (372.425, 559.0).into(),
1250                    (356.98333333333335, 511.125).into(),
1251                    (314.525, 485.95).into(),
1252                    (274.0, 485.95).into(),
1253                ]),
1254            ]
1255        );
1256    }
1257}