1 | // Copyright 2018 the Kurbo Authors |
2 | // SPDX-License-Identifier: Apache-2.0 OR MIT |
3 | |
4 | //! Cubic Bézier segments. |
5 | |
6 | use alloc::vec; |
7 | use alloc::vec::Vec; |
8 | use core::ops::{Mul, Range}; |
9 | |
10 | use crate::MAX_EXTREMA; |
11 | use crate::{Line, QuadSpline, Vec2}; |
12 | use arrayvec::ArrayVec; |
13 | |
14 | use crate::common::{ |
15 | solve_quadratic, GAUSS_LEGENDRE_COEFFS_16_HALF, GAUSS_LEGENDRE_COEFFS_24_HALF, |
16 | GAUSS_LEGENDRE_COEFFS_8, GAUSS_LEGENDRE_COEFFS_8_HALF, |
17 | }; |
18 | use crate::{ |
19 | Affine, Nearest, ParamCurve, ParamCurveArclen, ParamCurveArea, ParamCurveCurvature, |
20 | ParamCurveDeriv, ParamCurveExtrema, ParamCurveNearest, PathEl, Point, QuadBez, Rect, Shape, |
21 | }; |
22 | |
23 | #[cfg (not(feature = "std" ))] |
24 | use crate::common::FloatFuncs; |
25 | |
26 | const 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)] |
33 | pub 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. |
41 | struct ToQuads { |
42 | c: CubicBez, |
43 | i: usize, |
44 | n: usize, |
45 | } |
46 | |
47 | /// Classification result for cusp detection. |
48 | #[derive (Debug)] |
49 | pub enum CuspType { |
50 | /// Cusp is a loop. |
51 | Loop, |
52 | /// Cusp has two closely spaced inflection points. |
53 | DoubleInflection, |
54 | } |
55 | |
56 | impl CubicBez { |
57 | /// Create a new cubic Bézier segment. |
58 | #[inline ] |
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).crossing_point(Line::new(self.p2, self.p3)) { |
167 | let c1 = self.p0.lerp(q1, 2.0 / 3.0); |
168 | let c2 = self.p3.lerp(q1, 2.0 / 3.0); |
169 | if !CubicBez::new( |
170 | Point::ZERO, |
171 | c1 - self.p1.to_vec2(), |
172 | c2 - self.p2.to_vec2(), |
173 | Point::ZERO, |
174 | ) |
175 | .fit_inside(accuracy) |
176 | { |
177 | return None; |
178 | } |
179 | return Some(QuadBez::new(self.p0, q1, self.p3)); |
180 | } |
181 | None |
182 | } |
183 | |
184 | fn split_into_n(&self, n: usize) -> impl Iterator<Item = CubicBez> { |
185 | // for certain small values of `n` we precompute all our values. |
186 | // if we have six or fewer items we precompute them. |
187 | let mut storage = ArrayVec::<_, 6>::new(); |
188 | |
189 | match n { |
190 | 1 => storage.push(*self), |
191 | 2 => { |
192 | let (l, r) = self.subdivide(); |
193 | storage.try_extend_from_slice(&[r, l]).unwrap(); |
194 | } |
195 | 3 => { |
196 | let (left, mid, right) = self.subdivide_3(); |
197 | storage.try_extend_from_slice(&[right, mid, left]).unwrap(); |
198 | } |
199 | 4 => { |
200 | let (l, r) = self.subdivide(); |
201 | let (ll, lr) = l.subdivide(); |
202 | let (rl, rr) = r.subdivide(); |
203 | storage.try_extend_from_slice(&[rr, rl, lr, ll]).unwrap(); |
204 | } |
205 | 6 => { |
206 | let (l, r) = self.subdivide(); |
207 | let (l1, l2, l3) = l.subdivide_3(); |
208 | let (r1, r2, r3) = r.subdivide_3(); |
209 | storage |
210 | .try_extend_from_slice(&[r3, r2, r1, l3, l2, l1]) |
211 | .unwrap(); |
212 | } |
213 | _ => (), |
214 | } |
215 | |
216 | // a limitation of returning 'impl Trait' is that the implementation |
217 | // can only return a single concrete type; that is you cannot return |
218 | // Vec::into_iter() from one branch, and then HashSet::into_iter from |
219 | // another branch. |
220 | // |
221 | // This means we have to get a bit fancy, and have a single concrete |
222 | // type that represents both of our possible cases. |
223 | |
224 | let mut storage = if storage.is_empty() { |
225 | None |
226 | } else { |
227 | Some(storage) |
228 | }; |
229 | |
230 | // used in the fallback case |
231 | let mut i = 0; |
232 | let (a, b, c, d) = self.parameters(); |
233 | let dt = 1.0 / n as f64; |
234 | let delta_2 = dt * dt; |
235 | let delta_3 = dt * delta_2; |
236 | |
237 | core::iter::from_fn(move || { |
238 | // if storage exists, we use it exclusively |
239 | if let Some(storage) = storage.as_mut() { |
240 | return storage.pop(); |
241 | } |
242 | |
243 | // if storage does not exist, we are exclusively working down here. |
244 | if i >= n { |
245 | return None; |
246 | } |
247 | |
248 | let t1 = i as f64 * dt; |
249 | let t1_2 = t1 * t1; |
250 | let a1 = a * delta_3; |
251 | let b1 = (3.0 * a * t1 + b) * delta_2; |
252 | let c1 = (2.0 * b * t1 + c + 3.0 * a * t1_2) * dt; |
253 | let d1 = a * t1 * t1_2 + b * t1_2 + c * t1 + d; |
254 | let result = CubicBez::from_parameters(a1, b1, c1, d1); |
255 | i += 1; |
256 | Some(result) |
257 | }) |
258 | } |
259 | |
260 | fn parameters(&self) -> (Vec2, Vec2, Vec2, Vec2) { |
261 | let c = (self.p1 - self.p0) * 3.0; |
262 | let b = (self.p2 - self.p1) * 3.0 - c; |
263 | let d = self.p0.to_vec2(); |
264 | let a = self.p3.to_vec2() - d - c - b; |
265 | (a, b, c, d) |
266 | } |
267 | |
268 | /// Rust port of cu2qu [calc_cubic_points](https://github.com/fonttools/fonttools/blob/3b9a73ff8379ab49d3ce35aaaaf04b3a7d9d1655/Lib/fontTools/cu2qu/cu2qu.py#L63-L68) |
269 | fn from_parameters(a: Vec2, b: Vec2, c: Vec2, d: Vec2) -> Self { |
270 | let p0 = d.to_point(); |
271 | let p1 = c.div_exact(3.0).to_point() + d; |
272 | let p2 = (b + c).div_exact(3.0).to_point() + p1.to_vec2(); |
273 | let p3 = (a + d + c + b).to_point(); |
274 | CubicBez::new(p0, p1, p2, p3) |
275 | } |
276 | |
277 | fn subdivide_3(&self) -> (CubicBez, CubicBez, CubicBez) { |
278 | let (p0, p1, p2, p3) = ( |
279 | self.p0.to_vec2(), |
280 | self.p1.to_vec2(), |
281 | self.p2.to_vec2(), |
282 | self.p3.to_vec2(), |
283 | ); |
284 | // The original Python cu2qu code here does not use division operator to divide by 27 but |
285 | // instead uses multiplication by the reciprocal 1 / 27. We want to match it exactly |
286 | // to avoid any floating point differences, hence in this particular case we do not use div_exact. |
287 | // I could directly use the Vec2 Div trait (also implemented as multiplication by reciprocal) |
288 | // but I prefer to be explicit here. |
289 | // Source: https://github.com/fonttools/fonttools/blob/85c80be/Lib/fontTools/cu2qu/cu2qu.py#L215-L218 |
290 | // See also: https://github.com/linebender/kurbo/issues/272 |
291 | let one_27th = 27.0_f64.recip(); |
292 | let mid1 = ((8.0 * p0 + 12.0 * p1 + 6.0 * p2 + p3) * one_27th).to_point(); |
293 | let deriv1 = (p3 + 3.0 * p2 - 4.0 * p0) * one_27th; |
294 | let mid2 = ((p0 + 6.0 * p1 + 12.0 * p2 + 8.0 * p3) * one_27th).to_point(); |
295 | let deriv2 = (4.0 * p3 - 3.0 * p1 - p0) * one_27th; |
296 | |
297 | let left = CubicBez::new( |
298 | self.p0, |
299 | (2.0 * p0 + p1).div_exact(3.0).to_point(), |
300 | mid1 - deriv1, |
301 | mid1, |
302 | ); |
303 | let mid = CubicBez::new(mid1, mid1 + deriv1, mid2 - deriv2, mid2); |
304 | let right = CubicBez::new( |
305 | mid2, |
306 | mid2 + deriv2, |
307 | (p2 + 2.0 * p3).div_exact(3.0).to_point(), |
308 | self.p3, |
309 | ); |
310 | (left, mid, right) |
311 | } |
312 | |
313 | /// Does this curve fit inside the given distance from the origin? |
314 | /// |
315 | /// Rust port of cu2qu [cubic_farthest_fit_inside](https://github.com/fonttools/fonttools/blob/3b9a73ff8379ab49d3ce35aaaaf04b3a7d9d1655/Lib/fontTools/cu2qu/cu2qu.py#L281) |
316 | fn fit_inside(&self, distance: f64) -> bool { |
317 | if self.p2.to_vec2().hypot() <= distance && self.p1.to_vec2().hypot() <= distance { |
318 | return true; |
319 | } |
320 | let mid = |
321 | (self.p0.to_vec2() + 3.0 * (self.p1.to_vec2() + self.p2.to_vec2()) + self.p3.to_vec2()) |
322 | * 0.125; |
323 | if mid.hypot() > distance { |
324 | return false; |
325 | } |
326 | // Split in two. Note that cu2qu here uses a 3/8 subdivision. I don't know why. |
327 | let (left, right) = self.subdivide(); |
328 | left.fit_inside(distance) && right.fit_inside(distance) |
329 | } |
330 | |
331 | /// Is this cubic Bezier curve finite? |
332 | #[inline ] |
333 | pub fn is_finite(&self) -> bool { |
334 | self.p0.is_finite() && self.p1.is_finite() && self.p2.is_finite() && self.p3.is_finite() |
335 | } |
336 | |
337 | /// Is this cubic Bezier curve NaN? |
338 | #[inline ] |
339 | pub fn is_nan(&self) -> bool { |
340 | self.p0.is_nan() || self.p1.is_nan() || self.p2.is_nan() || self.p3.is_nan() |
341 | } |
342 | |
343 | /// Determine the inflection points. |
344 | /// |
345 | /// Return value is t parameter for the inflection points of the curve segment. |
346 | /// There are a maximum of two for a cubic Bézier. |
347 | /// |
348 | /// See <https://www.caffeineowl.com/graphics/2d/vectorial/cubic-inflexion.html> |
349 | /// for the theory. |
350 | pub fn inflections(&self) -> ArrayVec<f64, 2> { |
351 | let a = self.p1 - self.p0; |
352 | let b = (self.p2 - self.p1) - a; |
353 | let c = (self.p3 - self.p0) - 3. * (self.p2 - self.p1); |
354 | solve_quadratic(a.cross(b), a.cross(c), b.cross(c)) |
355 | .iter() |
356 | .copied() |
357 | .filter(|t| *t >= 0.0 && *t <= 1.0) |
358 | .collect() |
359 | } |
360 | |
361 | /// Preprocess a cubic Bézier to ease numerical robustness. |
362 | /// |
363 | /// If the cubic Bézier segment has zero or near-zero derivatives, perturb |
364 | /// the control points to make it easier to process (especially offset and |
365 | /// stroke), avoiding numerical robustness problems. |
366 | pub(crate) fn regularize(&self, dimension: f64) -> CubicBez { |
367 | let mut c = *self; |
368 | // First step: if control point is too near the endpoint, nudge it away |
369 | // along the tangent. |
370 | let dim2 = dimension * dimension; |
371 | if c.p0.distance_squared(c.p1) < dim2 { |
372 | let d02 = c.p0.distance_squared(c.p2); |
373 | if d02 >= dim2 { |
374 | // TODO: moderate if this would move closer to p3 |
375 | c.p1 = c.p0.lerp(c.p2, (dim2 / d02).sqrt()); |
376 | } else { |
377 | c.p1 = c.p0.lerp(c.p3, 1.0 / 3.0); |
378 | c.p2 = c.p3.lerp(c.p0, 1.0 / 3.0); |
379 | return c; |
380 | } |
381 | } |
382 | if c.p3.distance_squared(c.p2) < dim2 { |
383 | let d13 = c.p1.distance_squared(c.p2); |
384 | if d13 >= dim2 { |
385 | // TODO: moderate if this would move closer to p0 |
386 | c.p2 = c.p3.lerp(c.p1, (dim2 / d13).sqrt()); |
387 | } else { |
388 | c.p1 = c.p0.lerp(c.p3, 1.0 / 3.0); |
389 | c.p2 = c.p3.lerp(c.p0, 1.0 / 3.0); |
390 | return c; |
391 | } |
392 | } |
393 | if let Some(cusp_type) = self.detect_cusp(dimension) { |
394 | let d01 = c.p1 - c.p0; |
395 | let d01h = d01.hypot(); |
396 | let d23 = c.p3 - c.p2; |
397 | let d23h = d23.hypot(); |
398 | match cusp_type { |
399 | CuspType::Loop => { |
400 | c.p1 += (dimension / d01h) * d01; |
401 | c.p2 -= (dimension / d23h) * d23; |
402 | } |
403 | CuspType::DoubleInflection => { |
404 | // Avoid making control distance smaller than dimension |
405 | if d01h > 2.0 * dimension { |
406 | c.p1 -= (dimension / d01h) * d01; |
407 | } |
408 | if d23h > 2.0 * dimension { |
409 | c.p2 += (dimension / d23h) * d23; |
410 | } |
411 | } |
412 | } |
413 | } |
414 | c |
415 | } |
416 | |
417 | /// Detect whether there is a cusp. |
418 | /// |
419 | /// Return a cusp classification if there is a cusp with curvature greater than |
420 | /// the reciprocal of the given dimension. |
421 | fn detect_cusp(&self, dimension: f64) -> Option<CuspType> { |
422 | let d01 = self.p1 - self.p0; |
423 | let d02 = self.p2 - self.p0; |
424 | let d03 = self.p3 - self.p0; |
425 | let d12 = self.p2 - self.p1; |
426 | let d23 = self.p3 - self.p2; |
427 | let det_012 = d01.cross(d02); |
428 | let det_123 = d12.cross(d23); |
429 | let det_013 = d01.cross(d03); |
430 | let det_023 = d02.cross(d03); |
431 | if det_012 * det_123 > 0.0 && det_012 * det_013 < 0.0 && det_012 * det_023 < 0.0 { |
432 | let q = self.deriv(); |
433 | // accuracy isn't used for quadratic nearest |
434 | let nearest = q.nearest(Point::ORIGIN, 1e-9); |
435 | // detect whether curvature at minimum derivative exceeds 1/dimension, |
436 | // without division. |
437 | let d = q.eval(nearest.t); |
438 | let d2 = q.deriv().eval(nearest.t); |
439 | let cross = d.to_vec2().cross(d2.to_vec2()); |
440 | if nearest.distance_sq.powi(3) <= (cross * dimension).powi(2) { |
441 | let a = 3. * det_012 + det_023 - 2. * det_013; |
442 | let b = -3. * det_012 + det_013; |
443 | let c = det_012; |
444 | let d = b * b - 4. * a * c; |
445 | if d > 0.0 { |
446 | return Some(CuspType::DoubleInflection); |
447 | } else { |
448 | return Some(CuspType::Loop); |
449 | } |
450 | } |
451 | } |
452 | None |
453 | } |
454 | } |
455 | |
456 | /// An iterator for cubic beziers. |
457 | pub struct CubicBezIter { |
458 | cubic: CubicBez, |
459 | ix: usize, |
460 | } |
461 | |
462 | impl Shape for CubicBez { |
463 | type PathElementsIter<'iter> = CubicBezIter; |
464 | |
465 | #[inline ] |
466 | fn path_elements(&self, _tolerance: f64) -> CubicBezIter { |
467 | CubicBezIter { |
468 | cubic: *self, |
469 | ix: 0, |
470 | } |
471 | } |
472 | |
473 | fn area(&self) -> f64 { |
474 | 0.0 |
475 | } |
476 | |
477 | #[inline ] |
478 | fn perimeter(&self, accuracy: f64) -> f64 { |
479 | self.arclen(accuracy) |
480 | } |
481 | |
482 | fn winding(&self, _pt: Point) -> i32 { |
483 | 0 |
484 | } |
485 | |
486 | #[inline ] |
487 | fn bounding_box(&self) -> Rect { |
488 | ParamCurveExtrema::bounding_box(self) |
489 | } |
490 | } |
491 | |
492 | impl Iterator for CubicBezIter { |
493 | type Item = PathEl; |
494 | |
495 | fn next(&mut self) -> Option<PathEl> { |
496 | self.ix += 1; |
497 | match self.ix { |
498 | 1 => Some(PathEl::MoveTo(self.cubic.p0)), |
499 | 2 => Some(PathEl::CurveTo(self.cubic.p1, self.cubic.p2, self.cubic.p3)), |
500 | _ => None, |
501 | } |
502 | } |
503 | } |
504 | |
505 | impl ParamCurve for CubicBez { |
506 | #[inline ] |
507 | fn eval(&self, t: f64) -> Point { |
508 | let mt = 1.0 - t; |
509 | let v = self.p0.to_vec2() * (mt * mt * mt) |
510 | + (self.p1.to_vec2() * (mt * mt * 3.0) |
511 | + (self.p2.to_vec2() * (mt * 3.0) + self.p3.to_vec2() * t) * t) |
512 | * t; |
513 | v.to_point() |
514 | } |
515 | |
516 | fn subsegment(&self, range: Range<f64>) -> CubicBez { |
517 | let (t0, t1) = (range.start, range.end); |
518 | let p0 = self.eval(t0); |
519 | let p3 = self.eval(t1); |
520 | let d = self.deriv(); |
521 | let scale = (t1 - t0) * (1.0 / 3.0); |
522 | let p1 = p0 + scale * d.eval(t0).to_vec2(); |
523 | let p2 = p3 - scale * d.eval(t1).to_vec2(); |
524 | CubicBez { p0, p1, p2, p3 } |
525 | } |
526 | |
527 | /// Subdivide into halves, using de Casteljau. |
528 | #[inline ] |
529 | fn subdivide(&self) -> (CubicBez, CubicBez) { |
530 | let pm = self.eval(0.5); |
531 | ( |
532 | CubicBez::new( |
533 | self.p0, |
534 | self.p0.midpoint(self.p1), |
535 | ((self.p0.to_vec2() + self.p1.to_vec2() * 2.0 + self.p2.to_vec2()) * 0.25) |
536 | .to_point(), |
537 | pm, |
538 | ), |
539 | CubicBez::new( |
540 | pm, |
541 | ((self.p1.to_vec2() + self.p2.to_vec2() * 2.0 + self.p3.to_vec2()) * 0.25) |
542 | .to_point(), |
543 | self.p2.midpoint(self.p3), |
544 | self.p3, |
545 | ), |
546 | ) |
547 | } |
548 | |
549 | #[inline ] |
550 | fn start(&self) -> Point { |
551 | self.p0 |
552 | } |
553 | |
554 | #[inline ] |
555 | fn end(&self) -> Point { |
556 | self.p3 |
557 | } |
558 | } |
559 | |
560 | impl ParamCurveDeriv for CubicBez { |
561 | type DerivResult = QuadBez; |
562 | |
563 | #[inline ] |
564 | fn deriv(&self) -> QuadBez { |
565 | QuadBez::new( |
566 | (3.0 * (self.p1 - self.p0)).to_point(), |
567 | (3.0 * (self.p2 - self.p1)).to_point(), |
568 | (3.0 * (self.p3 - self.p2)).to_point(), |
569 | ) |
570 | } |
571 | } |
572 | |
573 | fn arclen_quadrature_core(coeffs: &[(f64, f64)], dm: Vec2, dm1: Vec2, dm2: Vec2) -> f64 { |
574 | coeffsimpl Iterator |
575 | .iter() |
576 | .map(|&(wi: f64, xi: f64)| { |
577 | let d: Vec2 = dm + dm2 * (xi * xi); |
578 | let dpx: f64 = (d + dm1 * xi).hypot(); |
579 | let dmx: f64 = (d - dm1 * xi).hypot(); |
580 | (2.25f64.sqrt() * wi) * (dpx + dmx) |
581 | }) |
582 | .sum::<f64>() |
583 | } |
584 | |
585 | fn arclen_rec(c: &CubicBez, accuracy: f64, depth: usize) -> f64 { |
586 | let d03 = c.p3 - c.p0; |
587 | let d01 = c.p1 - c.p0; |
588 | let d12 = c.p2 - c.p1; |
589 | let d23 = c.p3 - c.p2; |
590 | let lp_lc = d01.hypot() + d12.hypot() + d23.hypot() - d03.hypot(); |
591 | let dd1 = d12 - d01; |
592 | let dd2 = d23 - d12; |
593 | // It might be faster to do direct multiplies, the data dependencies would be shorter. |
594 | // The following values don't have the factor of 3 for first deriv |
595 | let dm = 0.25 * (d01 + d23) + 0.5 * d12; // first derivative at midpoint |
596 | let dm1 = 0.5 * (dd2 + dd1); // second derivative at midpoint |
597 | let dm2 = 0.25 * (dd2 - dd1); // 0.5 * (third derivative at midpoint) |
598 | |
599 | let est = GAUSS_LEGENDRE_COEFFS_8 |
600 | .iter() |
601 | .map(|&(wi, xi)| { |
602 | wi * { |
603 | let d_norm2 = (dm + dm1 * xi + dm2 * (xi * xi)).hypot2(); |
604 | let dd_norm2 = (dm1 + dm2 * (2.0 * xi)).hypot2(); |
605 | dd_norm2 / d_norm2 |
606 | } |
607 | }) |
608 | .sum::<f64>(); |
609 | let est_gauss8_error = (est.powi(3) * 2.5e-6).min(3e-2) * lp_lc; |
610 | if est_gauss8_error < accuracy { |
611 | return arclen_quadrature_core(GAUSS_LEGENDRE_COEFFS_8_HALF, dm, dm1, dm2); |
612 | } |
613 | let est_gauss16_error = (est.powi(6) * 1.5e-11).min(9e-3) * lp_lc; |
614 | if est_gauss16_error < accuracy { |
615 | return arclen_quadrature_core(GAUSS_LEGENDRE_COEFFS_16_HALF, dm, dm1, dm2); |
616 | } |
617 | let est_gauss24_error = (est.powi(9) * 3.5e-16).min(3.5e-3) * lp_lc; |
618 | if est_gauss24_error < accuracy || depth >= 20 { |
619 | return arclen_quadrature_core(GAUSS_LEGENDRE_COEFFS_24_HALF, dm, dm1, dm2); |
620 | } |
621 | let (c0, c1) = c.subdivide(); |
622 | arclen_rec(&c0, accuracy * 0.5, depth + 1) + arclen_rec(&c1, accuracy * 0.5, depth + 1) |
623 | } |
624 | |
625 | impl ParamCurveArclen for CubicBez { |
626 | /// Arclength of a cubic Bézier segment. |
627 | /// |
628 | /// This is an adaptive subdivision approach using Legendre-Gauss quadrature |
629 | /// in the base case, and an error estimate to decide when to subdivide. |
630 | fn arclen(&self, accuracy: f64) -> f64 { |
631 | arclen_rec(self, accuracy, depth:0) |
632 | } |
633 | } |
634 | |
635 | impl ParamCurveArea for CubicBez { |
636 | #[inline ] |
637 | fn signed_area(&self) -> f64 { |
638 | (self.p0.x * (6.0 * self.p1.y + 3.0 * self.p2.y + self.p3.y) |
639 | + 3.0 |
640 | * (self.p1.x * (-2.0 * self.p0.y + self.p2.y + self.p3.y) |
641 | - self.p2.x * (self.p0.y + self.p1.y - 2.0 * self.p3.y)) |
642 | - self.p3.x * (self.p0.y + 3.0 * self.p1.y + 6.0 * self.p2.y)) |
643 | * (1.0 / 20.0) |
644 | } |
645 | } |
646 | |
647 | impl ParamCurveNearest for CubicBez { |
648 | /// Find the nearest point, using subdivision. |
649 | fn nearest(&self, p: Point, accuracy: f64) -> Nearest { |
650 | let mut best_r: Option = None; |
651 | let mut best_t: f64 = 0.0; |
652 | for (t0: f64, t1: f64, q: QuadBez) in self.to_quads(accuracy) { |
653 | let nearest: Nearest = q.nearest(p, accuracy); |
654 | if best_r |
655 | .map(|best_r| nearest.distance_sq < best_r) |
656 | .unwrap_or(default:true) |
657 | { |
658 | best_t = t0 + nearest.t * (t1 - t0); |
659 | best_r = Some(nearest.distance_sq); |
660 | } |
661 | } |
662 | Nearest { |
663 | t: best_t, |
664 | distance_sq: best_r.unwrap(), |
665 | } |
666 | } |
667 | } |
668 | |
669 | impl ParamCurveCurvature for CubicBez {} |
670 | |
671 | impl ParamCurveExtrema for CubicBez { |
672 | fn extrema(&self) -> ArrayVec<f64, MAX_EXTREMA> { |
673 | fn one_coord(result: &mut ArrayVec<f64, MAX_EXTREMA>, d0: f64, d1: f64, d2: f64) { |
674 | let a: f64 = d0 - 2.0 * d1 + d2; |
675 | let b: f64 = 2.0 * (d1 - d0); |
676 | let c: f64 = d0; |
677 | let roots: ArrayVec = solve_quadratic(c0:c, c1:b, c2:a); |
678 | for &t: f64 in &roots { |
679 | if t > 0.0 && t < 1.0 { |
680 | result.push(element:t); |
681 | } |
682 | } |
683 | } |
684 | let mut result: ArrayVec = ArrayVec::new(); |
685 | let d0: Vec2 = self.p1 - self.p0; |
686 | let d1: Vec2 = self.p2 - self.p1; |
687 | let d2: Vec2 = self.p3 - self.p2; |
688 | one_coord(&mut result, d0:d0.x, d1:d1.x, d2:d2.x); |
689 | one_coord(&mut result, d0:d0.y, d1:d1.y, d2:d2.y); |
690 | result.sort_by(|a: &f64, b: &f64| a.partial_cmp(b).unwrap()); |
691 | result |
692 | } |
693 | } |
694 | |
695 | impl Mul<CubicBez> for Affine { |
696 | type Output = CubicBez; |
697 | |
698 | #[inline ] |
699 | fn mul(self, c: CubicBez) -> CubicBez { |
700 | CubicBez { |
701 | p0: self * c.p0, |
702 | p1: self * c.p1, |
703 | p2: self * c.p2, |
704 | p3: self * c.p3, |
705 | } |
706 | } |
707 | } |
708 | |
709 | impl Iterator for ToQuads { |
710 | type Item = (f64, f64, QuadBez); |
711 | |
712 | fn next(&mut self) -> Option<(f64, f64, QuadBez)> { |
713 | if self.i == self.n { |
714 | return None; |
715 | } |
716 | let t0: f64 = self.i as f64 / self.n as f64; |
717 | let t1: f64 = (self.i + 1) as f64 / self.n as f64; |
718 | let seg: CubicBez = self.c.subsegment(range:t0..t1); |
719 | let p1x2: Vec2 = 3.0 * seg.p1.to_vec2() - seg.p0.to_vec2(); |
720 | let p2x2: Vec2 = 3.0 * seg.p2.to_vec2() - seg.p3.to_vec2(); |
721 | let result: QuadBez = QuadBez::new(seg.p0, ((p1x2 + p2x2) / 4.0).to_point(), p2:seg.p3); |
722 | self.i += 1; |
723 | Some((t0, t1, result)) |
724 | } |
725 | |
726 | fn size_hint(&self) -> (usize, Option<usize>) { |
727 | let remaining: usize = self.n - self.i; |
728 | (remaining, Some(remaining)) |
729 | } |
730 | } |
731 | |
732 | /// Convert multiple cubic Bézier curves to quadratic splines. |
733 | /// |
734 | /// Ensures that the resulting splines have the same number of control points. |
735 | /// |
736 | /// Rust port of cu2qu [cubic_approx_quadratic](https://github.com/fonttools/fonttools/blob/3b9a73ff8379ab49d3ce35aaaaf04b3a7d9d1655/Lib/fontTools/cu2qu/cu2qu.py#L322) |
737 | pub fn cubics_to_quadratic_splines(curves: &[CubicBez], accuracy: f64) -> Option<Vec<QuadSpline>> { |
738 | let mut result: Vec = Vec::new(); |
739 | let mut split_order: usize = 0; |
740 | |
741 | while split_order <= MAX_SPLINE_SPLIT { |
742 | split_order += 1; |
743 | result.clear(); |
744 | |
745 | for curve: &CubicBez in curves { |
746 | match curve.approx_spline_n(n:split_order, accuracy) { |
747 | Some(spline: QuadSpline) => result.push(spline), |
748 | None => break, |
749 | } |
750 | } |
751 | |
752 | if result.len() == curves.len() { |
753 | return Some(result); |
754 | } |
755 | } |
756 | None |
757 | } |
758 | #[cfg (test)] |
759 | mod tests { |
760 | use crate::{ |
761 | cubics_to_quadratic_splines, Affine, CubicBez, Nearest, ParamCurve, ParamCurveArclen, |
762 | ParamCurveArea, ParamCurveDeriv, ParamCurveExtrema, ParamCurveNearest, Point, QuadBez, |
763 | QuadSpline, |
764 | }; |
765 | |
766 | #[test ] |
767 | fn cubicbez_deriv() { |
768 | // y = x^2 |
769 | let c = CubicBez::new( |
770 | (0.0, 0.0), |
771 | (1.0 / 3.0, 0.0), |
772 | (2.0 / 3.0, 1.0 / 3.0), |
773 | (1.0, 1.0), |
774 | ); |
775 | let deriv = c.deriv(); |
776 | |
777 | let n = 10; |
778 | for i in 0..=n { |
779 | let t = (i as f64) * (n as f64).recip(); |
780 | let delta = 1e-6; |
781 | let p = c.eval(t); |
782 | let p1 = c.eval(t + delta); |
783 | let d_approx = (p1 - p) * delta.recip(); |
784 | let d = deriv.eval(t).to_vec2(); |
785 | assert!((d - d_approx).hypot() < delta * 2.0); |
786 | } |
787 | } |
788 | |
789 | #[test ] |
790 | fn cubicbez_arclen() { |
791 | // y = x^2 |
792 | let c = CubicBez::new( |
793 | (0.0, 0.0), |
794 | (1.0 / 3.0, 0.0), |
795 | (2.0 / 3.0, 1.0 / 3.0), |
796 | (1.0, 1.0), |
797 | ); |
798 | let true_arclen = 0.5 * 5.0f64.sqrt() + 0.25 * (2.0 + 5.0f64.sqrt()).ln(); |
799 | for i in 0..12 { |
800 | let accuracy = 0.1f64.powi(i); |
801 | let error = c.arclen(accuracy) - true_arclen; |
802 | assert!(error.abs() < accuracy); |
803 | } |
804 | } |
805 | |
806 | #[test ] |
807 | fn cubicbez_inv_arclen() { |
808 | // y = x^2 / 100 |
809 | let c = CubicBez::new( |
810 | (0.0, 0.0), |
811 | (100.0 / 3.0, 0.0), |
812 | (200.0 / 3.0, 100.0 / 3.0), |
813 | (100.0, 100.0), |
814 | ); |
815 | let true_arclen = 100.0 * (0.5 * 5.0f64.sqrt() + 0.25 * (2.0 + 5.0f64.sqrt()).ln()); |
816 | for i in 0..12 { |
817 | let accuracy = 0.1f64.powi(i); |
818 | let n = 10; |
819 | for j in 0..=n { |
820 | let arc = (j as f64) * ((n as f64).recip() * true_arclen); |
821 | let t = c.inv_arclen(arc, accuracy * 0.5); |
822 | let actual_arc = c.subsegment(0.0..t).arclen(accuracy * 0.5); |
823 | assert!( |
824 | (arc - actual_arc).abs() < accuracy, |
825 | "at accuracy {accuracy:e}, wanted {actual_arc} got {arc}" |
826 | ); |
827 | } |
828 | } |
829 | // corner case: user passes accuracy larger than total arc length |
830 | let accuracy = true_arclen * 1.1; |
831 | let arc = true_arclen * 0.5; |
832 | let t = c.inv_arclen(arc, accuracy); |
833 | let actual_arc = c.subsegment(0.0..t).arclen(accuracy); |
834 | assert!( |
835 | (arc - actual_arc).abs() < 2.0 * accuracy, |
836 | "at accuracy {accuracy:e}, want {actual_arc} got {arc}" |
837 | ); |
838 | } |
839 | |
840 | #[test ] |
841 | fn cubicbez_inv_arclen_accuracy() { |
842 | let c = CubicBez::new((0.2, 0.73), (0.35, 1.08), (0.85, 1.08), (1.0, 0.73)); |
843 | let true_t = c.inv_arclen(0.5, 1e-12); |
844 | for i in 1..12 { |
845 | let accuracy = (0.1f64).powi(i); |
846 | let approx_t = c.inv_arclen(0.5, accuracy); |
847 | assert!((approx_t - true_t).abs() <= accuracy); |
848 | } |
849 | } |
850 | |
851 | #[test ] |
852 | #[allow (clippy::float_cmp)] |
853 | fn cubicbez_signed_area_linear() { |
854 | // y = 1 - x |
855 | let c = CubicBez::new( |
856 | (1.0, 0.0), |
857 | (2.0 / 3.0, 1.0 / 3.0), |
858 | (1.0 / 3.0, 2.0 / 3.0), |
859 | (0.0, 1.0), |
860 | ); |
861 | let epsilon = 1e-12; |
862 | assert_eq!((Affine::rotate(0.5) * c).signed_area(), 0.5); |
863 | assert!(((Affine::rotate(0.5) * c).signed_area() - 0.5).abs() < epsilon); |
864 | assert!(((Affine::translate((0.0, 1.0)) * c).signed_area() - 1.0).abs() < epsilon); |
865 | assert!(((Affine::translate((1.0, 0.0)) * c).signed_area() - 1.0).abs() < epsilon); |
866 | } |
867 | |
868 | #[test ] |
869 | fn cubicbez_signed_area() { |
870 | // y = 1 - x^3 |
871 | let c = CubicBez::new((1.0, 0.0), (2.0 / 3.0, 1.0), (1.0 / 3.0, 1.0), (0.0, 1.0)); |
872 | let epsilon = 1e-12; |
873 | assert!((c.signed_area() - 0.75).abs() < epsilon); |
874 | assert!(((Affine::rotate(0.5) * c).signed_area() - 0.75).abs() < epsilon); |
875 | assert!(((Affine::translate((0.0, 1.0)) * c).signed_area() - 1.25).abs() < epsilon); |
876 | assert!(((Affine::translate((1.0, 0.0)) * c).signed_area() - 1.25).abs() < epsilon); |
877 | } |
878 | |
879 | #[test ] |
880 | fn cubicbez_nearest() { |
881 | fn verify(result: Nearest, expected: f64) { |
882 | assert!( |
883 | (result.t - expected).abs() < 1e-6, |
884 | "got {result:?} expected {expected}" |
885 | ); |
886 | } |
887 | // y = x^3 |
888 | let c = CubicBez::new((0.0, 0.0), (1.0 / 3.0, 0.0), (2.0 / 3.0, 0.0), (1.0, 1.0)); |
889 | verify(c.nearest((0.1, 0.001).into(), 1e-6), 0.1); |
890 | verify(c.nearest((0.2, 0.008).into(), 1e-6), 0.2); |
891 | verify(c.nearest((0.3, 0.027).into(), 1e-6), 0.3); |
892 | verify(c.nearest((0.4, 0.064).into(), 1e-6), 0.4); |
893 | verify(c.nearest((0.5, 0.125).into(), 1e-6), 0.5); |
894 | verify(c.nearest((0.6, 0.216).into(), 1e-6), 0.6); |
895 | verify(c.nearest((0.7, 0.343).into(), 1e-6), 0.7); |
896 | verify(c.nearest((0.8, 0.512).into(), 1e-6), 0.8); |
897 | verify(c.nearest((0.9, 0.729).into(), 1e-6), 0.9); |
898 | verify(c.nearest((1.0, 1.0).into(), 1e-6), 1.0); |
899 | verify(c.nearest((1.1, 1.1).into(), 1e-6), 1.0); |
900 | verify(c.nearest((-0.1, 0.0).into(), 1e-6), 0.0); |
901 | let a = Affine::rotate(0.5); |
902 | verify((a * c).nearest(a * Point::new(0.1, 0.001), 1e-6), 0.1); |
903 | } |
904 | |
905 | // ensure to_quads returns something given colinear points |
906 | #[test ] |
907 | fn degenerate_to_quads() { |
908 | let c = CubicBez::new((0., 9.), (6., 6.), (12., 3.0), (18., 0.0)); |
909 | let quads = c.to_quads(1e-6).collect::<Vec<_>>(); |
910 | assert_eq!(quads.len(), 1, " {:?}" , &quads); |
911 | } |
912 | |
913 | #[test ] |
914 | fn cubicbez_extrema() { |
915 | // y = x^2 |
916 | let q = CubicBez::new((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)); |
917 | let extrema = q.extrema(); |
918 | assert_eq!(extrema.len(), 1); |
919 | assert!((extrema[0] - 0.5).abs() < 1e-6); |
920 | |
921 | let q = CubicBez::new((0.4, 0.5), (0.0, 1.0), (1.0, 0.0), (0.5, 0.4)); |
922 | let extrema = q.extrema(); |
923 | assert_eq!(extrema.len(), 4); |
924 | } |
925 | |
926 | #[test ] |
927 | fn cubicbez_toquads() { |
928 | // y = x^3 |
929 | let c = CubicBez::new((0.0, 0.0), (1.0 / 3.0, 0.0), (2.0 / 3.0, 0.0), (1.0, 1.0)); |
930 | for i in 0..10 { |
931 | let accuracy = 0.1f64.powi(i); |
932 | let mut worst: f64 = 0.0; |
933 | for (_count, (t0, t1, q)) in c.to_quads(accuracy).enumerate() { |
934 | let epsilon = 1e-12; |
935 | assert!((q.start() - c.eval(t0)).hypot() < epsilon); |
936 | assert!((q.end() - c.eval(t1)).hypot() < epsilon); |
937 | let n = 4; |
938 | for j in 0..=n { |
939 | let t = (j as f64) * (n as f64).recip(); |
940 | let p = q.eval(t); |
941 | let err = (p.y - p.x.powi(3)).abs(); |
942 | worst = worst.max(err); |
943 | assert!(err < accuracy, "got {err} wanted {accuracy}" ); |
944 | } |
945 | } |
946 | } |
947 | } |
948 | |
949 | #[test ] |
950 | fn cubicbez_approx_spline() { |
951 | let c1 = CubicBez::new( |
952 | (550.0, 258.0), |
953 | (1044.0, 482.0), |
954 | (2029.0, 1841.0), |
955 | (1934.0, 1554.0), |
956 | ); |
957 | |
958 | let quad = c1.try_approx_quadratic(344.0); |
959 | let expected = QuadBez::new( |
960 | Point::new(550.0, 258.0), |
961 | Point::new(1673.665720592873, 767.5164401068898), |
962 | Point::new(1934.0, 1554.0), |
963 | ); |
964 | assert!(quad.is_some()); |
965 | assert_eq!(quad.unwrap(), expected); |
966 | |
967 | let quad = c1.try_approx_quadratic(343.0); |
968 | assert!(quad.is_none()); |
969 | |
970 | let spline = c1.approx_spline_n(2, 343.0); |
971 | assert!(spline.is_some()); |
972 | let spline = spline.unwrap(); |
973 | let expected = [ |
974 | Point::new(550.0, 258.0), |
975 | Point::new(920.5, 426.0), |
976 | Point::new(2005.25, 1769.25), |
977 | Point::new(1934.0, 1554.0), |
978 | ]; |
979 | assert_eq!(spline.points().len(), expected.len()); |
980 | for (got, &wanted) in spline.points().iter().zip(expected.iter()) { |
981 | assert!(got.distance(wanted) < 5.0) |
982 | } |
983 | |
984 | let spline = c1.approx_spline(5.0); |
985 | let expected = [ |
986 | Point::new(550.0, 258.0), |
987 | Point::new(673.5, 314.0), |
988 | Point::new(984.8777777777776, 584.2666666666667), |
989 | Point::new(1312.6305555555557, 927.825), |
990 | Point::new(1613.1194444444443, 1267.425), |
991 | Point::new(1842.7055555555555, 1525.8166666666666), |
992 | Point::new(1957.75, 1625.75), |
993 | Point::new(1934.0, 1554.0), |
994 | ]; |
995 | assert!(spline.is_some()); |
996 | let spline = spline.unwrap(); |
997 | assert_eq!(spline.points().len(), expected.len()); |
998 | for (got, &wanted) in spline.points().iter().zip(expected.iter()) { |
999 | assert!(got.distance(wanted) < 5.0) |
1000 | } |
1001 | } |
1002 | |
1003 | #[test ] |
1004 | fn cubicbez_cubics_to_quadratic_splines() { |
1005 | let curves = vec![ |
1006 | CubicBez::new( |
1007 | (550.0, 258.0), |
1008 | (1044.0, 482.0), |
1009 | (2029.0, 1841.0), |
1010 | (1934.0, 1554.0), |
1011 | ), |
1012 | CubicBez::new( |
1013 | (859.0, 384.0), |
1014 | (1998.0, 116.0), |
1015 | (1596.0, 1772.0), |
1016 | (8.0, 1824.0), |
1017 | ), |
1018 | CubicBez::new( |
1019 | (1090.0, 937.0), |
1020 | (418.0, 1300.0), |
1021 | (125.0, 91.0), |
1022 | (104.0, 37.0), |
1023 | ), |
1024 | ]; |
1025 | let converted = cubics_to_quadratic_splines(&curves, 5.0); |
1026 | assert!(converted.is_some()); |
1027 | let converted = converted.unwrap(); |
1028 | assert_eq!(converted[0].points().len(), 8); |
1029 | assert_eq!(converted[1].points().len(), 8); |
1030 | assert_eq!(converted[2].points().len(), 8); |
1031 | assert!(converted[0].points()[1].distance(Point::new(673.5, 314.0)) < 0.0001); |
1032 | assert!( |
1033 | converted[0].points()[2].distance(Point::new(88639.0 / 90.0, 52584.0 / 90.0)) < 0.0001 |
1034 | ); |
1035 | } |
1036 | |
1037 | #[test ] |
1038 | fn cubicbez_approx_spline_div_exact() { |
1039 | // Ensure rounding behavior for division matches fonttools |
1040 | // cu2qu. |
1041 | // See <https://github.com/linebender/kurbo/issues/272> |
1042 | let cubic = CubicBez::new( |
1043 | Point::new(408.0, 321.0), |
1044 | Point::new(408.0, 452.0), |
1045 | Point::new(342.0, 560.0), |
1046 | Point::new(260.0, 560.0), |
1047 | ); |
1048 | let spline = cubic.approx_spline(1.0).unwrap(); |
1049 | assert_eq!( |
1050 | spline.points(), |
1051 | &[ |
1052 | Point::new(408.0, 321.0), |
1053 | // Previous behavior produced 386.49999999999994 for the |
1054 | // y coordinate leading to inconsistent rounding. |
1055 | Point::new(408.0, 386.5), |
1056 | Point::new(368.16666666666663, 495.0833333333333), |
1057 | Point::new(301.0, 560.0), |
1058 | Point::new(260.0, 560.0) |
1059 | ] |
1060 | ) |
1061 | } |
1062 | |
1063 | #[test ] |
1064 | fn cubicbez_inflections() { |
1065 | let c = CubicBez::new((0., 0.), (0.8, 1.), (0.2, 1.), (1., 0.)); |
1066 | let inflections = c.inflections(); |
1067 | assert_eq!(inflections.len(), 2); |
1068 | assert!((inflections[0] - 0.311018).abs() < 1e-6); |
1069 | assert!((inflections[1] - 0.688982).abs() < 1e-6); |
1070 | let c = CubicBez::new((0., 0.), (1., 1.), (2., -1.), (3., 0.)); |
1071 | let inflections = c.inflections(); |
1072 | assert_eq!(inflections.len(), 1); |
1073 | assert!((inflections[0] - 0.5).abs() < 1e-6); |
1074 | let c = CubicBez::new((0., 0.), (1., 1.), (2., 1.), (3., 0.)); |
1075 | let inflections = c.inflections(); |
1076 | assert_eq!(inflections.len(), 0); |
1077 | } |
1078 | |
1079 | #[test ] |
1080 | fn cubic_to_quadratic_matches_python() { |
1081 | // from https://github.com/googlefonts/fontmake-rs/issues/217 |
1082 | let cubic = CubicBez { |
1083 | p0: (796.0, 319.0).into(), |
1084 | p1: (727.0, 314.0).into(), |
1085 | p2: (242.0, 303.0).into(), |
1086 | p3: (106.0, 303.0).into(), |
1087 | }; |
1088 | |
1089 | // FontTools can approximate this curve successfully in 7 splits, we can too |
1090 | assert!(cubic.approx_spline_n(7, 1.0).is_some()); |
1091 | |
1092 | // FontTools can solve this with accuracy 0.001, we can too |
1093 | assert!(cubics_to_quadratic_splines(&[cubic], 0.001).is_some()); |
1094 | } |
1095 | |
1096 | #[test ] |
1097 | fn cubics_to_quadratic_splines_matches_python() { |
1098 | // https://github.com/linebender/kurbo/pull/273 |
1099 | let light = CubicBez::new((378., 608.), (378., 524.), (355., 455.), (266., 455.)); |
1100 | let regular = CubicBez::new((367., 607.), (367., 511.), (338., 472.), (243., 472.)); |
1101 | let bold = CubicBez::new( |
1102 | (372.425, 593.05), |
1103 | (372.425, 524.95), |
1104 | (355.05, 485.95), |
1105 | (274., 485.95), |
1106 | ); |
1107 | let qsplines = cubics_to_quadratic_splines(&[light, regular, bold], 1.0).unwrap(); |
1108 | assert_eq!( |
1109 | qsplines, |
1110 | [ |
1111 | QuadSpline::new(vec![ |
1112 | (378.0, 608.0).into(), |
1113 | (378.0, 566.0).into(), |
1114 | (359.0833333333333, 496.5).into(), |
1115 | (310.5, 455.0).into(), |
1116 | (266.0, 455.0).into(), |
1117 | ]), |
1118 | QuadSpline::new(vec![ |
1119 | (367.0, 607.0).into(), |
1120 | (367.0, 559.0).into(), |
1121 | // Previous behavior produced 496.5 for the y coordinate |
1122 | (344.5833333333333, 499.49999999999994).into(), |
1123 | (290.5, 472.0).into(), |
1124 | (243.0, 472.0).into(), |
1125 | ]), |
1126 | QuadSpline::new(vec![ |
1127 | (372.425, 593.05).into(), |
1128 | (372.425, 559.0).into(), |
1129 | (356.98333333333335, 511.125).into(), |
1130 | (314.525, 485.95).into(), |
1131 | (274.0, 485.95).into(), |
1132 | ]), |
1133 | ] |
1134 | ) |
1135 | } |
1136 | } |
1137 | |