| 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, solve_quartic, 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 | /// Find points on the curve where the tangent line passes through the |
| 362 | /// given point. |
| 363 | /// |
| 364 | /// Result is array of t values such that the tangent line from the curve |
| 365 | /// evaluated at that point goes through the argument point. |
| 366 | pub fn tangents_to_point(&self, p: Point) -> ArrayVec<f64, 4> { |
| 367 | let (a, b, c, d_orig) = self.parameters(); |
| 368 | let d = d_orig - p.to_vec2(); |
| 369 | // coefficients of x(t) \cross x'(t) |
| 370 | let c4 = b.cross(a); |
| 371 | let c3 = 2.0 * c.cross(a); |
| 372 | let c2 = c.cross(b) + 3.0 * d.cross(a); |
| 373 | let c1 = 2.0 * d.cross(b); |
| 374 | let c0 = d.cross(c); |
| 375 | solve_quartic(c0, c1, c2, c3, c4) |
| 376 | .iter() |
| 377 | .copied() |
| 378 | .filter(|t| *t >= 0.0 && *t <= 1.0) |
| 379 | .collect() |
| 380 | } |
| 381 | |
| 382 | /// Preprocess a cubic Bézier to ease numerical robustness. |
| 383 | /// |
| 384 | /// If the cubic Bézier segment has zero or near-zero derivatives, perturb |
| 385 | /// the control points to make it easier to process (especially offset and |
| 386 | /// stroke), avoiding numerical robustness problems. |
| 387 | pub(crate) fn regularize(&self, dimension: f64) -> CubicBez { |
| 388 | let mut c = *self; |
| 389 | // First step: if control point is too near the endpoint, nudge it away |
| 390 | // along the tangent. |
| 391 | let dim2 = dimension * dimension; |
| 392 | if c.p0.distance_squared(c.p1) < dim2 { |
| 393 | let d02 = c.p0.distance_squared(c.p2); |
| 394 | if d02 >= dim2 { |
| 395 | // TODO: moderate if this would move closer to p3 |
| 396 | c.p1 = c.p0.lerp(c.p2, (dim2 / d02).sqrt()); |
| 397 | } else { |
| 398 | c.p1 = c.p0.lerp(c.p3, 1.0 / 3.0); |
| 399 | c.p2 = c.p3.lerp(c.p0, 1.0 / 3.0); |
| 400 | return c; |
| 401 | } |
| 402 | } |
| 403 | if c.p3.distance_squared(c.p2) < dim2 { |
| 404 | let d13 = c.p1.distance_squared(c.p2); |
| 405 | if d13 >= dim2 { |
| 406 | // TODO: moderate if this would move closer to p0 |
| 407 | c.p2 = c.p3.lerp(c.p1, (dim2 / d13).sqrt()); |
| 408 | } else { |
| 409 | c.p1 = c.p0.lerp(c.p3, 1.0 / 3.0); |
| 410 | c.p2 = c.p3.lerp(c.p0, 1.0 / 3.0); |
| 411 | return c; |
| 412 | } |
| 413 | } |
| 414 | if let Some(cusp_type) = self.detect_cusp(dimension) { |
| 415 | let d01 = c.p1 - c.p0; |
| 416 | let d01h = d01.hypot(); |
| 417 | let d23 = c.p3 - c.p2; |
| 418 | let d23h = d23.hypot(); |
| 419 | match cusp_type { |
| 420 | CuspType::Loop => { |
| 421 | c.p1 += (dimension / d01h) * d01; |
| 422 | c.p2 -= (dimension / d23h) * d23; |
| 423 | } |
| 424 | CuspType::DoubleInflection => { |
| 425 | // Avoid making control distance smaller than dimension |
| 426 | if d01h > 2.0 * dimension { |
| 427 | c.p1 -= (dimension / d01h) * d01; |
| 428 | } |
| 429 | if d23h > 2.0 * dimension { |
| 430 | c.p2 += (dimension / d23h) * d23; |
| 431 | } |
| 432 | } |
| 433 | } |
| 434 | } |
| 435 | c |
| 436 | } |
| 437 | |
| 438 | /// Detect whether there is a cusp. |
| 439 | /// |
| 440 | /// Return a cusp classification if there is a cusp with curvature greater than |
| 441 | /// the reciprocal of the given dimension. |
| 442 | fn detect_cusp(&self, dimension: f64) -> Option<CuspType> { |
| 443 | let d01 = self.p1 - self.p0; |
| 444 | let d02 = self.p2 - self.p0; |
| 445 | let d03 = self.p3 - self.p0; |
| 446 | let d12 = self.p2 - self.p1; |
| 447 | let d23 = self.p3 - self.p2; |
| 448 | let det_012 = d01.cross(d02); |
| 449 | let det_123 = d12.cross(d23); |
| 450 | let det_013 = d01.cross(d03); |
| 451 | let det_023 = d02.cross(d03); |
| 452 | if det_012 * det_123 > 0.0 && det_012 * det_013 < 0.0 && det_012 * det_023 < 0.0 { |
| 453 | let q = self.deriv(); |
| 454 | // accuracy isn't used for quadratic nearest |
| 455 | let nearest = q.nearest(Point::ORIGIN, 1e-9); |
| 456 | // detect whether curvature at minimum derivative exceeds 1/dimension, |
| 457 | // without division. |
| 458 | let d = q.eval(nearest.t); |
| 459 | let d2 = q.deriv().eval(nearest.t); |
| 460 | let cross = d.to_vec2().cross(d2.to_vec2()); |
| 461 | if nearest.distance_sq.powi(3) <= (cross * dimension).powi(2) { |
| 462 | let a = 3. * det_012 + det_023 - 2. * det_013; |
| 463 | let b = -3. * det_012 + det_013; |
| 464 | let c = det_012; |
| 465 | let d = b * b - 4. * a * c; |
| 466 | if d > 0.0 { |
| 467 | return Some(CuspType::DoubleInflection); |
| 468 | } else { |
| 469 | return Some(CuspType::Loop); |
| 470 | } |
| 471 | } |
| 472 | } |
| 473 | None |
| 474 | } |
| 475 | } |
| 476 | |
| 477 | /// An iterator for cubic beziers. |
| 478 | pub struct CubicBezIter { |
| 479 | cubic: CubicBez, |
| 480 | ix: usize, |
| 481 | } |
| 482 | |
| 483 | impl Shape for CubicBez { |
| 484 | type PathElementsIter<'iter> = CubicBezIter; |
| 485 | |
| 486 | #[inline ] |
| 487 | fn path_elements(&self, _tolerance: f64) -> CubicBezIter { |
| 488 | CubicBezIter { |
| 489 | cubic: *self, |
| 490 | ix: 0, |
| 491 | } |
| 492 | } |
| 493 | |
| 494 | fn area(&self) -> f64 { |
| 495 | 0.0 |
| 496 | } |
| 497 | |
| 498 | #[inline ] |
| 499 | fn perimeter(&self, accuracy: f64) -> f64 { |
| 500 | self.arclen(accuracy) |
| 501 | } |
| 502 | |
| 503 | fn winding(&self, _pt: Point) -> i32 { |
| 504 | 0 |
| 505 | } |
| 506 | |
| 507 | #[inline ] |
| 508 | fn bounding_box(&self) -> Rect { |
| 509 | ParamCurveExtrema::bounding_box(self) |
| 510 | } |
| 511 | } |
| 512 | |
| 513 | impl Iterator for CubicBezIter { |
| 514 | type Item = PathEl; |
| 515 | |
| 516 | fn next(&mut self) -> Option<PathEl> { |
| 517 | self.ix += 1; |
| 518 | match self.ix { |
| 519 | 1 => Some(PathEl::MoveTo(self.cubic.p0)), |
| 520 | 2 => Some(PathEl::CurveTo(self.cubic.p1, self.cubic.p2, self.cubic.p3)), |
| 521 | _ => None, |
| 522 | } |
| 523 | } |
| 524 | } |
| 525 | |
| 526 | impl ParamCurve for CubicBez { |
| 527 | #[inline ] |
| 528 | fn eval(&self, t: f64) -> Point { |
| 529 | let mt = 1.0 - t; |
| 530 | let v = self.p0.to_vec2() * (mt * mt * mt) |
| 531 | + (self.p1.to_vec2() * (mt * mt * 3.0) |
| 532 | + (self.p2.to_vec2() * (mt * 3.0) + self.p3.to_vec2() * t) * t) |
| 533 | * t; |
| 534 | v.to_point() |
| 535 | } |
| 536 | |
| 537 | fn subsegment(&self, range: Range<f64>) -> CubicBez { |
| 538 | let (t0, t1) = (range.start, range.end); |
| 539 | let p0 = self.eval(t0); |
| 540 | let p3 = self.eval(t1); |
| 541 | let d = self.deriv(); |
| 542 | let scale = (t1 - t0) * (1.0 / 3.0); |
| 543 | let p1 = p0 + scale * d.eval(t0).to_vec2(); |
| 544 | let p2 = p3 - scale * d.eval(t1).to_vec2(); |
| 545 | CubicBez { p0, p1, p2, p3 } |
| 546 | } |
| 547 | |
| 548 | /// Subdivide into halves, using de Casteljau. |
| 549 | #[inline ] |
| 550 | fn subdivide(&self) -> (CubicBez, CubicBez) { |
| 551 | let pm = self.eval(0.5); |
| 552 | ( |
| 553 | CubicBez::new( |
| 554 | self.p0, |
| 555 | self.p0.midpoint(self.p1), |
| 556 | ((self.p0.to_vec2() + self.p1.to_vec2() * 2.0 + self.p2.to_vec2()) * 0.25) |
| 557 | .to_point(), |
| 558 | pm, |
| 559 | ), |
| 560 | CubicBez::new( |
| 561 | pm, |
| 562 | ((self.p1.to_vec2() + self.p2.to_vec2() * 2.0 + self.p3.to_vec2()) * 0.25) |
| 563 | .to_point(), |
| 564 | self.p2.midpoint(self.p3), |
| 565 | self.p3, |
| 566 | ), |
| 567 | ) |
| 568 | } |
| 569 | |
| 570 | #[inline ] |
| 571 | fn start(&self) -> Point { |
| 572 | self.p0 |
| 573 | } |
| 574 | |
| 575 | #[inline ] |
| 576 | fn end(&self) -> Point { |
| 577 | self.p3 |
| 578 | } |
| 579 | } |
| 580 | |
| 581 | impl ParamCurveDeriv for CubicBez { |
| 582 | type DerivResult = QuadBez; |
| 583 | |
| 584 | #[inline ] |
| 585 | fn deriv(&self) -> QuadBez { |
| 586 | QuadBez::new( |
| 587 | (3.0 * (self.p1 - self.p0)).to_point(), |
| 588 | (3.0 * (self.p2 - self.p1)).to_point(), |
| 589 | (3.0 * (self.p3 - self.p2)).to_point(), |
| 590 | ) |
| 591 | } |
| 592 | } |
| 593 | |
| 594 | fn arclen_quadrature_core(coeffs: &[(f64, f64)], dm: Vec2, dm1: Vec2, dm2: Vec2) -> f64 { |
| 595 | coeffsimpl Iterator |
| 596 | .iter() |
| 597 | .map(|&(wi: f64, xi: f64)| { |
| 598 | let d: Vec2 = dm + dm2 * (xi * xi); |
| 599 | let dpx: f64 = (d + dm1 * xi).hypot(); |
| 600 | let dmx: f64 = (d - dm1 * xi).hypot(); |
| 601 | (2.25f64.sqrt() * wi) * (dpx + dmx) |
| 602 | }) |
| 603 | .sum::<f64>() |
| 604 | } |
| 605 | |
| 606 | fn arclen_rec(c: &CubicBez, accuracy: f64, depth: usize) -> f64 { |
| 607 | let d03 = c.p3 - c.p0; |
| 608 | let d01 = c.p1 - c.p0; |
| 609 | let d12 = c.p2 - c.p1; |
| 610 | let d23 = c.p3 - c.p2; |
| 611 | let lp_lc = d01.hypot() + d12.hypot() + d23.hypot() - d03.hypot(); |
| 612 | let dd1 = d12 - d01; |
| 613 | let dd2 = d23 - d12; |
| 614 | // It might be faster to do direct multiplies, the data dependencies would be shorter. |
| 615 | // The following values don't have the factor of 3 for first deriv |
| 616 | let dm = 0.25 * (d01 + d23) + 0.5 * d12; // first derivative at midpoint |
| 617 | let dm1 = 0.5 * (dd2 + dd1); // second derivative at midpoint |
| 618 | let dm2 = 0.25 * (dd2 - dd1); // 0.5 * (third derivative at midpoint) |
| 619 | |
| 620 | let est = GAUSS_LEGENDRE_COEFFS_8 |
| 621 | .iter() |
| 622 | .map(|&(wi, xi)| { |
| 623 | wi * { |
| 624 | let d_norm2 = (dm + dm1 * xi + dm2 * (xi * xi)).hypot2(); |
| 625 | let dd_norm2 = (dm1 + dm2 * (2.0 * xi)).hypot2(); |
| 626 | dd_norm2 / d_norm2 |
| 627 | } |
| 628 | }) |
| 629 | .sum::<f64>(); |
| 630 | let est_gauss8_error = (est.powi(3) * 2.5e-6).min(3e-2) * lp_lc; |
| 631 | if est_gauss8_error < accuracy { |
| 632 | return arclen_quadrature_core(GAUSS_LEGENDRE_COEFFS_8_HALF, dm, dm1, dm2); |
| 633 | } |
| 634 | let est_gauss16_error = (est.powi(6) * 1.5e-11).min(9e-3) * lp_lc; |
| 635 | if est_gauss16_error < accuracy { |
| 636 | return arclen_quadrature_core(GAUSS_LEGENDRE_COEFFS_16_HALF, dm, dm1, dm2); |
| 637 | } |
| 638 | let est_gauss24_error = (est.powi(9) * 3.5e-16).min(3.5e-3) * lp_lc; |
| 639 | if est_gauss24_error < accuracy || depth >= 20 { |
| 640 | return arclen_quadrature_core(GAUSS_LEGENDRE_COEFFS_24_HALF, dm, dm1, dm2); |
| 641 | } |
| 642 | let (c0, c1) = c.subdivide(); |
| 643 | arclen_rec(&c0, accuracy * 0.5, depth + 1) + arclen_rec(&c1, accuracy * 0.5, depth + 1) |
| 644 | } |
| 645 | |
| 646 | impl ParamCurveArclen for CubicBez { |
| 647 | /// Arclength of a cubic Bézier segment. |
| 648 | /// |
| 649 | /// This is an adaptive subdivision approach using Legendre-Gauss quadrature |
| 650 | /// in the base case, and an error estimate to decide when to subdivide. |
| 651 | fn arclen(&self, accuracy: f64) -> f64 { |
| 652 | arclen_rec(self, accuracy, depth:0) |
| 653 | } |
| 654 | } |
| 655 | |
| 656 | impl ParamCurveArea for CubicBez { |
| 657 | #[inline ] |
| 658 | fn signed_area(&self) -> f64 { |
| 659 | (self.p0.x * (6.0 * self.p1.y + 3.0 * self.p2.y + self.p3.y) |
| 660 | + 3.0 |
| 661 | * (self.p1.x * (-2.0 * self.p0.y + self.p2.y + self.p3.y) |
| 662 | - self.p2.x * (self.p0.y + self.p1.y - 2.0 * self.p3.y)) |
| 663 | - self.p3.x * (self.p0.y + 3.0 * self.p1.y + 6.0 * self.p2.y)) |
| 664 | * (1.0 / 20.0) |
| 665 | } |
| 666 | } |
| 667 | |
| 668 | impl ParamCurveNearest for CubicBez { |
| 669 | /// Find the nearest point, using subdivision. |
| 670 | fn nearest(&self, p: Point, accuracy: f64) -> Nearest { |
| 671 | let mut best_r: Option = None; |
| 672 | let mut best_t: f64 = 0.0; |
| 673 | for (t0: f64, t1: f64, q: QuadBez) in self.to_quads(accuracy) { |
| 674 | let nearest: Nearest = q.nearest(p, accuracy); |
| 675 | if best_r |
| 676 | .map(|best_r| nearest.distance_sq < best_r) |
| 677 | .unwrap_or(default:true) |
| 678 | { |
| 679 | best_t = t0 + nearest.t * (t1 - t0); |
| 680 | best_r = Some(nearest.distance_sq); |
| 681 | } |
| 682 | } |
| 683 | Nearest { |
| 684 | t: best_t, |
| 685 | distance_sq: best_r.unwrap(), |
| 686 | } |
| 687 | } |
| 688 | } |
| 689 | |
| 690 | impl ParamCurveCurvature for CubicBez {} |
| 691 | |
| 692 | impl ParamCurveExtrema for CubicBez { |
| 693 | fn extrema(&self) -> ArrayVec<f64, MAX_EXTREMA> { |
| 694 | fn one_coord(result: &mut ArrayVec<f64, MAX_EXTREMA>, d0: f64, d1: f64, d2: f64) { |
| 695 | let a: f64 = d0 - 2.0 * d1 + d2; |
| 696 | let b: f64 = 2.0 * (d1 - d0); |
| 697 | let c: f64 = d0; |
| 698 | let roots: ArrayVec = solve_quadratic(c0:c, c1:b, c2:a); |
| 699 | for &t: f64 in &roots { |
| 700 | if t > 0.0 && t < 1.0 { |
| 701 | result.push(element:t); |
| 702 | } |
| 703 | } |
| 704 | } |
| 705 | let mut result: ArrayVec = ArrayVec::new(); |
| 706 | let d0: Vec2 = self.p1 - self.p0; |
| 707 | let d1: Vec2 = self.p2 - self.p1; |
| 708 | let d2: Vec2 = self.p3 - self.p2; |
| 709 | one_coord(&mut result, d0.x, d1.x, d2.x); |
| 710 | one_coord(&mut result, d0.y, d1.y, d2.y); |
| 711 | result.sort_by(|a: &f64, b: &f64| a.partial_cmp(b).unwrap()); |
| 712 | result |
| 713 | } |
| 714 | } |
| 715 | |
| 716 | impl Mul<CubicBez> for Affine { |
| 717 | type Output = CubicBez; |
| 718 | |
| 719 | #[inline ] |
| 720 | fn mul(self, c: CubicBez) -> CubicBez { |
| 721 | CubicBez { |
| 722 | p0: self * c.p0, |
| 723 | p1: self * c.p1, |
| 724 | p2: self * c.p2, |
| 725 | p3: self * c.p3, |
| 726 | } |
| 727 | } |
| 728 | } |
| 729 | |
| 730 | impl Iterator for ToQuads { |
| 731 | type Item = (f64, f64, QuadBez); |
| 732 | |
| 733 | fn next(&mut self) -> Option<(f64, f64, QuadBez)> { |
| 734 | if self.i == self.n { |
| 735 | return None; |
| 736 | } |
| 737 | let t0: f64 = self.i as f64 / self.n as f64; |
| 738 | let t1: f64 = (self.i + 1) as f64 / self.n as f64; |
| 739 | let seg: CubicBez = self.c.subsegment(range:t0..t1); |
| 740 | let p1x2: Vec2 = 3.0 * seg.p1.to_vec2() - seg.p0.to_vec2(); |
| 741 | let p2x2: Vec2 = 3.0 * seg.p2.to_vec2() - seg.p3.to_vec2(); |
| 742 | let result: QuadBez = QuadBez::new(seg.p0, ((p1x2 + p2x2) / 4.0).to_point(), p2:seg.p3); |
| 743 | self.i += 1; |
| 744 | Some((t0, t1, result)) |
| 745 | } |
| 746 | |
| 747 | fn size_hint(&self) -> (usize, Option<usize>) { |
| 748 | let remaining: usize = self.n - self.i; |
| 749 | (remaining, Some(remaining)) |
| 750 | } |
| 751 | } |
| 752 | |
| 753 | /// Convert multiple cubic Bézier curves to quadratic splines. |
| 754 | /// |
| 755 | /// Ensures that the resulting splines have the same number of control points. |
| 756 | /// |
| 757 | /// Rust port of cu2qu [cubic_approx_quadratic](https://github.com/fonttools/fonttools/blob/3b9a73ff8379ab49d3ce35aaaaf04b3a7d9d1655/Lib/fontTools/cu2qu/cu2qu.py#L322) |
| 758 | pub fn cubics_to_quadratic_splines(curves: &[CubicBez], accuracy: f64) -> Option<Vec<QuadSpline>> { |
| 759 | let mut result: Vec = Vec::new(); |
| 760 | let mut split_order: usize = 0; |
| 761 | |
| 762 | while split_order <= MAX_SPLINE_SPLIT { |
| 763 | split_order += 1; |
| 764 | result.clear(); |
| 765 | |
| 766 | for curve: &CubicBez in curves { |
| 767 | match curve.approx_spline_n(n:split_order, accuracy) { |
| 768 | Some(spline: QuadSpline) => result.push(spline), |
| 769 | None => break, |
| 770 | } |
| 771 | } |
| 772 | |
| 773 | if result.len() == curves.len() { |
| 774 | return Some(result); |
| 775 | } |
| 776 | } |
| 777 | None |
| 778 | } |
| 779 | #[cfg (test)] |
| 780 | mod tests { |
| 781 | use crate::{ |
| 782 | cubics_to_quadratic_splines, Affine, CubicBez, Nearest, ParamCurve, ParamCurveArclen, |
| 783 | ParamCurveArea, ParamCurveDeriv, ParamCurveExtrema, ParamCurveNearest, Point, QuadBez, |
| 784 | QuadSpline, |
| 785 | }; |
| 786 | |
| 787 | #[test ] |
| 788 | fn cubicbez_deriv() { |
| 789 | // y = x^2 |
| 790 | let c = CubicBez::new( |
| 791 | (0.0, 0.0), |
| 792 | (1.0 / 3.0, 0.0), |
| 793 | (2.0 / 3.0, 1.0 / 3.0), |
| 794 | (1.0, 1.0), |
| 795 | ); |
| 796 | let deriv = c.deriv(); |
| 797 | |
| 798 | let n = 10; |
| 799 | for i in 0..=n { |
| 800 | let t = (i as f64) * (n as f64).recip(); |
| 801 | let delta = 1e-6; |
| 802 | let p = c.eval(t); |
| 803 | let p1 = c.eval(t + delta); |
| 804 | let d_approx = (p1 - p) * delta.recip(); |
| 805 | let d = deriv.eval(t).to_vec2(); |
| 806 | assert!((d - d_approx).hypot() < delta * 2.0); |
| 807 | } |
| 808 | } |
| 809 | |
| 810 | #[test ] |
| 811 | fn cubicbez_arclen() { |
| 812 | // y = x^2 |
| 813 | let c = CubicBez::new( |
| 814 | (0.0, 0.0), |
| 815 | (1.0 / 3.0, 0.0), |
| 816 | (2.0 / 3.0, 1.0 / 3.0), |
| 817 | (1.0, 1.0), |
| 818 | ); |
| 819 | let true_arclen = 0.5 * 5.0f64.sqrt() + 0.25 * (2.0 + 5.0f64.sqrt()).ln(); |
| 820 | for i in 0..12 { |
| 821 | let accuracy = 0.1f64.powi(i); |
| 822 | let error = c.arclen(accuracy) - true_arclen; |
| 823 | assert!(error.abs() < accuracy); |
| 824 | } |
| 825 | } |
| 826 | |
| 827 | #[test ] |
| 828 | fn cubicbez_inv_arclen() { |
| 829 | // y = x^2 / 100 |
| 830 | let c = CubicBez::new( |
| 831 | (0.0, 0.0), |
| 832 | (100.0 / 3.0, 0.0), |
| 833 | (200.0 / 3.0, 100.0 / 3.0), |
| 834 | (100.0, 100.0), |
| 835 | ); |
| 836 | let true_arclen = 100.0 * (0.5 * 5.0f64.sqrt() + 0.25 * (2.0 + 5.0f64.sqrt()).ln()); |
| 837 | for i in 0..12 { |
| 838 | let accuracy = 0.1f64.powi(i); |
| 839 | let n = 10; |
| 840 | for j in 0..=n { |
| 841 | let arc = (j as f64) * ((n as f64).recip() * true_arclen); |
| 842 | let t = c.inv_arclen(arc, accuracy * 0.5); |
| 843 | let actual_arc = c.subsegment(0.0..t).arclen(accuracy * 0.5); |
| 844 | assert!( |
| 845 | (arc - actual_arc).abs() < accuracy, |
| 846 | "at accuracy {accuracy:e}, wanted {actual_arc} got {arc}" |
| 847 | ); |
| 848 | } |
| 849 | } |
| 850 | // corner case: user passes accuracy larger than total arc length |
| 851 | let accuracy = true_arclen * 1.1; |
| 852 | let arc = true_arclen * 0.5; |
| 853 | let t = c.inv_arclen(arc, accuracy); |
| 854 | let actual_arc = c.subsegment(0.0..t).arclen(accuracy); |
| 855 | assert!( |
| 856 | (arc - actual_arc).abs() < 2.0 * accuracy, |
| 857 | "at accuracy {accuracy:e}, want {actual_arc} got {arc}" |
| 858 | ); |
| 859 | } |
| 860 | |
| 861 | #[test ] |
| 862 | fn cubicbez_inv_arclen_accuracy() { |
| 863 | let c = CubicBez::new((0.2, 0.73), (0.35, 1.08), (0.85, 1.08), (1.0, 0.73)); |
| 864 | let true_t = c.inv_arclen(0.5, 1e-12); |
| 865 | for i in 1..12 { |
| 866 | let accuracy = (0.1f64).powi(i); |
| 867 | let approx_t = c.inv_arclen(0.5, accuracy); |
| 868 | assert!((approx_t - true_t).abs() <= accuracy); |
| 869 | } |
| 870 | } |
| 871 | |
| 872 | #[test ] |
| 873 | #[allow (clippy::float_cmp)] |
| 874 | fn cubicbez_signed_area_linear() { |
| 875 | // y = 1 - x |
| 876 | let c = CubicBez::new( |
| 877 | (1.0, 0.0), |
| 878 | (2.0 / 3.0, 1.0 / 3.0), |
| 879 | (1.0 / 3.0, 2.0 / 3.0), |
| 880 | (0.0, 1.0), |
| 881 | ); |
| 882 | let epsilon = 1e-12; |
| 883 | assert_eq!((Affine::rotate(0.5) * c).signed_area(), 0.5); |
| 884 | assert!(((Affine::rotate(0.5) * c).signed_area() - 0.5).abs() < epsilon); |
| 885 | assert!(((Affine::translate((0.0, 1.0)) * c).signed_area() - 1.0).abs() < epsilon); |
| 886 | assert!(((Affine::translate((1.0, 0.0)) * c).signed_area() - 1.0).abs() < epsilon); |
| 887 | } |
| 888 | |
| 889 | #[test ] |
| 890 | fn cubicbez_signed_area() { |
| 891 | // y = 1 - x^3 |
| 892 | let c = CubicBez::new((1.0, 0.0), (2.0 / 3.0, 1.0), (1.0 / 3.0, 1.0), (0.0, 1.0)); |
| 893 | let epsilon = 1e-12; |
| 894 | assert!((c.signed_area() - 0.75).abs() < epsilon); |
| 895 | assert!(((Affine::rotate(0.5) * c).signed_area() - 0.75).abs() < epsilon); |
| 896 | assert!(((Affine::translate((0.0, 1.0)) * c).signed_area() - 1.25).abs() < epsilon); |
| 897 | assert!(((Affine::translate((1.0, 0.0)) * c).signed_area() - 1.25).abs() < epsilon); |
| 898 | } |
| 899 | |
| 900 | #[test ] |
| 901 | fn cubicbez_nearest() { |
| 902 | fn verify(result: Nearest, expected: f64) { |
| 903 | assert!( |
| 904 | (result.t - expected).abs() < 1e-6, |
| 905 | "got {result:?} expected {expected}" |
| 906 | ); |
| 907 | } |
| 908 | // y = x^3 |
| 909 | let c = CubicBez::new((0.0, 0.0), (1.0 / 3.0, 0.0), (2.0 / 3.0, 0.0), (1.0, 1.0)); |
| 910 | verify(c.nearest((0.1, 0.001).into(), 1e-6), 0.1); |
| 911 | verify(c.nearest((0.2, 0.008).into(), 1e-6), 0.2); |
| 912 | verify(c.nearest((0.3, 0.027).into(), 1e-6), 0.3); |
| 913 | verify(c.nearest((0.4, 0.064).into(), 1e-6), 0.4); |
| 914 | verify(c.nearest((0.5, 0.125).into(), 1e-6), 0.5); |
| 915 | verify(c.nearest((0.6, 0.216).into(), 1e-6), 0.6); |
| 916 | verify(c.nearest((0.7, 0.343).into(), 1e-6), 0.7); |
| 917 | verify(c.nearest((0.8, 0.512).into(), 1e-6), 0.8); |
| 918 | verify(c.nearest((0.9, 0.729).into(), 1e-6), 0.9); |
| 919 | verify(c.nearest((1.0, 1.0).into(), 1e-6), 1.0); |
| 920 | verify(c.nearest((1.1, 1.1).into(), 1e-6), 1.0); |
| 921 | verify(c.nearest((-0.1, 0.0).into(), 1e-6), 0.0); |
| 922 | let a = Affine::rotate(0.5); |
| 923 | verify((a * c).nearest(a * Point::new(0.1, 0.001), 1e-6), 0.1); |
| 924 | } |
| 925 | |
| 926 | // ensure to_quads returns something given colinear points |
| 927 | #[test ] |
| 928 | fn degenerate_to_quads() { |
| 929 | let c = CubicBez::new((0., 9.), (6., 6.), (12., 3.0), (18., 0.0)); |
| 930 | let quads = c.to_quads(1e-6).collect::<Vec<_>>(); |
| 931 | assert_eq!(quads.len(), 1, "{:?}" , &quads); |
| 932 | } |
| 933 | |
| 934 | #[test ] |
| 935 | fn cubicbez_extrema() { |
| 936 | // y = x^2 |
| 937 | let q = CubicBez::new((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)); |
| 938 | let extrema = q.extrema(); |
| 939 | assert_eq!(extrema.len(), 1); |
| 940 | assert!((extrema[0] - 0.5).abs() < 1e-6); |
| 941 | |
| 942 | let q = CubicBez::new((0.4, 0.5), (0.0, 1.0), (1.0, 0.0), (0.5, 0.4)); |
| 943 | let extrema = q.extrema(); |
| 944 | assert_eq!(extrema.len(), 4); |
| 945 | } |
| 946 | |
| 947 | #[test ] |
| 948 | fn cubicbez_toquads() { |
| 949 | // y = x^3 |
| 950 | let c = CubicBez::new((0.0, 0.0), (1.0 / 3.0, 0.0), (2.0 / 3.0, 0.0), (1.0, 1.0)); |
| 951 | for i in 0..10 { |
| 952 | let accuracy = 0.1f64.powi(i); |
| 953 | let mut worst: f64 = 0.0; |
| 954 | for (t0, t1, q) in c.to_quads(accuracy) { |
| 955 | let epsilon = 1e-12; |
| 956 | assert!((q.start() - c.eval(t0)).hypot() < epsilon); |
| 957 | assert!((q.end() - c.eval(t1)).hypot() < epsilon); |
| 958 | let n = 4; |
| 959 | for j in 0..=n { |
| 960 | let t = (j as f64) * (n as f64).recip(); |
| 961 | let p = q.eval(t); |
| 962 | let err = (p.y - p.x.powi(3)).abs(); |
| 963 | worst = worst.max(err); |
| 964 | assert!(err < accuracy, "got {err} wanted {accuracy}" ); |
| 965 | } |
| 966 | } |
| 967 | } |
| 968 | } |
| 969 | |
| 970 | #[test ] |
| 971 | fn cubicbez_approx_spline() { |
| 972 | let c1 = CubicBez::new( |
| 973 | (550.0, 258.0), |
| 974 | (1044.0, 482.0), |
| 975 | (2029.0, 1841.0), |
| 976 | (1934.0, 1554.0), |
| 977 | ); |
| 978 | |
| 979 | let quad = c1.try_approx_quadratic(344.0); |
| 980 | let expected = QuadBez::new( |
| 981 | Point::new(550.0, 258.0), |
| 982 | Point::new(1673.665720592873, 767.5164401068898), |
| 983 | Point::new(1934.0, 1554.0), |
| 984 | ); |
| 985 | assert!(quad.is_some()); |
| 986 | assert_eq!(quad.unwrap(), expected); |
| 987 | |
| 988 | let quad = c1.try_approx_quadratic(343.0); |
| 989 | assert!(quad.is_none()); |
| 990 | |
| 991 | let spline = c1.approx_spline_n(2, 343.0); |
| 992 | assert!(spline.is_some()); |
| 993 | let spline = spline.unwrap(); |
| 994 | let expected = [ |
| 995 | Point::new(550.0, 258.0), |
| 996 | Point::new(920.5, 426.0), |
| 997 | Point::new(2005.25, 1769.25), |
| 998 | Point::new(1934.0, 1554.0), |
| 999 | ]; |
| 1000 | assert_eq!(spline.points().len(), expected.len()); |
| 1001 | for (got, &wanted) in spline.points().iter().zip(expected.iter()) { |
| 1002 | assert!(got.distance(wanted) < 5.0); |
| 1003 | } |
| 1004 | |
| 1005 | let spline = c1.approx_spline(5.0); |
| 1006 | let expected = [ |
| 1007 | Point::new(550.0, 258.0), |
| 1008 | Point::new(673.5, 314.0), |
| 1009 | Point::new(984.8777777777776, 584.2666666666667), |
| 1010 | Point::new(1312.6305555555557, 927.825), |
| 1011 | Point::new(1613.1194444444443, 1267.425), |
| 1012 | Point::new(1842.7055555555555, 1525.8166666666666), |
| 1013 | Point::new(1957.75, 1625.75), |
| 1014 | Point::new(1934.0, 1554.0), |
| 1015 | ]; |
| 1016 | assert!(spline.is_some()); |
| 1017 | let spline = spline.unwrap(); |
| 1018 | assert_eq!(spline.points().len(), expected.len()); |
| 1019 | for (got, &wanted) in spline.points().iter().zip(expected.iter()) { |
| 1020 | assert!(got.distance(wanted) < 5.0); |
| 1021 | } |
| 1022 | } |
| 1023 | |
| 1024 | #[test ] |
| 1025 | fn cubicbez_cubics_to_quadratic_splines() { |
| 1026 | let curves = vec![ |
| 1027 | CubicBez::new( |
| 1028 | (550.0, 258.0), |
| 1029 | (1044.0, 482.0), |
| 1030 | (2029.0, 1841.0), |
| 1031 | (1934.0, 1554.0), |
| 1032 | ), |
| 1033 | CubicBez::new( |
| 1034 | (859.0, 384.0), |
| 1035 | (1998.0, 116.0), |
| 1036 | (1596.0, 1772.0), |
| 1037 | (8.0, 1824.0), |
| 1038 | ), |
| 1039 | CubicBez::new( |
| 1040 | (1090.0, 937.0), |
| 1041 | (418.0, 1300.0), |
| 1042 | (125.0, 91.0), |
| 1043 | (104.0, 37.0), |
| 1044 | ), |
| 1045 | ]; |
| 1046 | let converted = cubics_to_quadratic_splines(&curves, 5.0); |
| 1047 | assert!(converted.is_some()); |
| 1048 | let converted = converted.unwrap(); |
| 1049 | assert_eq!(converted[0].points().len(), 8); |
| 1050 | assert_eq!(converted[1].points().len(), 8); |
| 1051 | assert_eq!(converted[2].points().len(), 8); |
| 1052 | assert!(converted[0].points()[1].distance(Point::new(673.5, 314.0)) < 0.0001); |
| 1053 | assert!( |
| 1054 | converted[0].points()[2].distance(Point::new(88639.0 / 90.0, 52584.0 / 90.0)) < 0.0001 |
| 1055 | ); |
| 1056 | } |
| 1057 | |
| 1058 | #[test ] |
| 1059 | fn cubicbez_approx_spline_div_exact() { |
| 1060 | // Ensure rounding behavior for division matches fonttools |
| 1061 | // cu2qu. |
| 1062 | // See <https://github.com/linebender/kurbo/issues/272> |
| 1063 | let cubic = CubicBez::new( |
| 1064 | Point::new(408.0, 321.0), |
| 1065 | Point::new(408.0, 452.0), |
| 1066 | Point::new(342.0, 560.0), |
| 1067 | Point::new(260.0, 560.0), |
| 1068 | ); |
| 1069 | let spline = cubic.approx_spline(1.0).unwrap(); |
| 1070 | assert_eq!( |
| 1071 | spline.points(), |
| 1072 | &[ |
| 1073 | Point::new(408.0, 321.0), |
| 1074 | // Previous behavior produced 386.49999999999994 for the |
| 1075 | // y coordinate leading to inconsistent rounding. |
| 1076 | Point::new(408.0, 386.5), |
| 1077 | Point::new(368.16666666666663, 495.0833333333333), |
| 1078 | Point::new(301.0, 560.0), |
| 1079 | Point::new(260.0, 560.0) |
| 1080 | ] |
| 1081 | ); |
| 1082 | } |
| 1083 | |
| 1084 | #[test ] |
| 1085 | fn cubicbez_inflections() { |
| 1086 | let c = CubicBez::new((0., 0.), (0.8, 1.), (0.2, 1.), (1., 0.)); |
| 1087 | let inflections = c.inflections(); |
| 1088 | assert_eq!(inflections.len(), 2); |
| 1089 | assert!((inflections[0] - 0.311018).abs() < 1e-6); |
| 1090 | assert!((inflections[1] - 0.688982).abs() < 1e-6); |
| 1091 | let c = CubicBez::new((0., 0.), (1., 1.), (2., -1.), (3., 0.)); |
| 1092 | let inflections = c.inflections(); |
| 1093 | assert_eq!(inflections.len(), 1); |
| 1094 | assert!((inflections[0] - 0.5).abs() < 1e-6); |
| 1095 | let c = CubicBez::new((0., 0.), (1., 1.), (2., 1.), (3., 0.)); |
| 1096 | let inflections = c.inflections(); |
| 1097 | assert_eq!(inflections.len(), 0); |
| 1098 | } |
| 1099 | |
| 1100 | #[test ] |
| 1101 | fn cubic_to_quadratic_matches_python() { |
| 1102 | // from https://github.com/googlefonts/fontmake-rs/issues/217 |
| 1103 | let cubic = CubicBez { |
| 1104 | p0: (796.0, 319.0).into(), |
| 1105 | p1: (727.0, 314.0).into(), |
| 1106 | p2: (242.0, 303.0).into(), |
| 1107 | p3: (106.0, 303.0).into(), |
| 1108 | }; |
| 1109 | |
| 1110 | // FontTools can approximate this curve successfully in 7 splits, we can too |
| 1111 | assert!(cubic.approx_spline_n(7, 1.0).is_some()); |
| 1112 | |
| 1113 | // FontTools can solve this with accuracy 0.001, we can too |
| 1114 | assert!(cubics_to_quadratic_splines(&[cubic], 0.001).is_some()); |
| 1115 | } |
| 1116 | |
| 1117 | #[test ] |
| 1118 | fn cubics_to_quadratic_splines_matches_python() { |
| 1119 | // https://github.com/linebender/kurbo/pull/273 |
| 1120 | let light = CubicBez::new((378., 608.), (378., 524.), (355., 455.), (266., 455.)); |
| 1121 | let regular = CubicBez::new((367., 607.), (367., 511.), (338., 472.), (243., 472.)); |
| 1122 | let bold = CubicBez::new( |
| 1123 | (372.425, 593.05), |
| 1124 | (372.425, 524.95), |
| 1125 | (355.05, 485.95), |
| 1126 | (274., 485.95), |
| 1127 | ); |
| 1128 | let qsplines = cubics_to_quadratic_splines(&[light, regular, bold], 1.0).unwrap(); |
| 1129 | assert_eq!( |
| 1130 | qsplines, |
| 1131 | [ |
| 1132 | QuadSpline::new(vec![ |
| 1133 | (378.0, 608.0).into(), |
| 1134 | (378.0, 566.0).into(), |
| 1135 | (359.0833333333333, 496.5).into(), |
| 1136 | (310.5, 455.0).into(), |
| 1137 | (266.0, 455.0).into(), |
| 1138 | ]), |
| 1139 | QuadSpline::new(vec![ |
| 1140 | (367.0, 607.0).into(), |
| 1141 | (367.0, 559.0).into(), |
| 1142 | // Previous behavior produced 496.5 for the y coordinate |
| 1143 | (344.5833333333333, 499.49999999999994).into(), |
| 1144 | (290.5, 472.0).into(), |
| 1145 | (243.0, 472.0).into(), |
| 1146 | ]), |
| 1147 | QuadSpline::new(vec![ |
| 1148 | (372.425, 593.05).into(), |
| 1149 | (372.425, 559.0).into(), |
| 1150 | (356.98333333333335, 511.125).into(), |
| 1151 | (314.525, 485.95).into(), |
| 1152 | (274.0, 485.95).into(), |
| 1153 | ]), |
| 1154 | ] |
| 1155 | ); |
| 1156 | } |
| 1157 | } |
| 1158 | |