1# -*- coding: utf-8 -*- 2"""fontTools.misc.bezierTools.py -- tools for working with Bezier path segments. 3""" 4 5from fontTools.misc.arrayTools import calcBounds, sectRect, rectArea 6from fontTools.misc.transform import Identity 7import math 8from collections import namedtuple 9 10Intersection = namedtuple("Intersection", ["pt", "t1", "t2"]) 11 12 13__all__ = [ 14 "approximateCubicArcLength", 15 "approximateCubicArcLengthC", 16 "approximateQuadraticArcLength", 17 "approximateQuadraticArcLengthC", 18 "calcCubicArcLength", 19 "calcCubicArcLengthC", 20 "calcQuadraticArcLength", 21 "calcQuadraticArcLengthC", 22 "calcCubicBounds", 23 "calcQuadraticBounds", 24 "splitLine", 25 "splitQuadratic", 26 "splitCubic", 27 "splitQuadraticAtT", 28 "splitCubicAtT", 29 "solveQuadratic", 30 "solveCubic", 31 "quadraticPointAtT", 32 "cubicPointAtT", 33 "linePointAtT", 34 "segmentPointAtT", 35 "lineLineIntersections", 36 "curveLineIntersections", 37 "curveCurveIntersections", 38 "segmentSegmentIntersections", 39] 40 41 42def calcCubicArcLength(pt1, pt2, pt3, pt4, tolerance=0.005): 43 """Calculates the arc length for a cubic Bezier segment. 44 45 Whereas :func:`approximateCubicArcLength` approximates the length, this 46 function calculates it by "measuring", recursively dividing the curve 47 until the divided segments are shorter than ``tolerance``. 48 49 Args: 50 pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples. 51 tolerance: Controls the precision of the calcuation. 52 53 Returns: 54 Arc length value. 55 """ 56 return calcCubicArcLengthC( 57 complex(*pt1), complex(*pt2), complex(*pt3), complex(*pt4), tolerance 58 ) 59 60 61def _split_cubic_into_two(p0, p1, p2, p3): 62 mid = (p0 + 3 * (p1 + p2) + p3) * 0.125 63 deriv3 = (p3 + p2 - p1 - p0) * 0.125 64 return ( 65 (p0, (p0 + p1) * 0.5, mid - deriv3, mid), 66 (mid, mid + deriv3, (p2 + p3) * 0.5, p3), 67 ) 68 69 70def _calcCubicArcLengthCRecurse(mult, p0, p1, p2, p3): 71 arch = abs(p0 - p3) 72 box = abs(p0 - p1) + abs(p1 - p2) + abs(p2 - p3) 73 if arch * mult >= box: 74 return (arch + box) * 0.5 75 else: 76 one, two = _split_cubic_into_two(p0, p1, p2, p3) 77 return _calcCubicArcLengthCRecurse(mult, *one) + _calcCubicArcLengthCRecurse( 78 mult, *two 79 ) 80 81 82def calcCubicArcLengthC(pt1, pt2, pt3, pt4, tolerance=0.005): 83 """Calculates the arc length for a cubic Bezier segment. 84 85 Args: 86 pt1,pt2,pt3,pt4: Control points of the Bezier as complex numbers. 87 tolerance: Controls the precision of the calcuation. 88 89 Returns: 90 Arc length value. 91 """ 92 mult = 1.0 + 1.5 * tolerance # The 1.5 is a empirical hack; no math 93 return _calcCubicArcLengthCRecurse(mult, pt1, pt2, pt3, pt4) 94 95 96epsilonDigits = 6 97epsilon = 1e-10 98 99 100def _dot(v1, v2): 101 return (v1 * v2.conjugate()).real 102 103 104def _intSecAtan(x): 105 # In : sympy.integrate(sp.sec(sp.atan(x))) 106 # Out: x*sqrt(x**2 + 1)/2 + asinh(x)/2 107 return x * math.sqrt(x ** 2 + 1) / 2 + math.asinh(x) / 2 108 109 110def calcQuadraticArcLength(pt1, pt2, pt3): 111 """Calculates the arc length for a quadratic Bezier segment. 112 113 Args: 114 pt1: Start point of the Bezier as 2D tuple. 115 pt2: Handle point of the Bezier as 2D tuple. 116 pt3: End point of the Bezier as 2D tuple. 117 118 Returns: 119 Arc length value. 120 121 Example:: 122 123 >>> calcQuadraticArcLength((0, 0), (0, 0), (0, 0)) # empty segment 124 0.0 125 >>> calcQuadraticArcLength((0, 0), (50, 0), (80, 0)) # collinear points 126 80.0 127 >>> calcQuadraticArcLength((0, 0), (0, 50), (0, 80)) # collinear points vertical 128 80.0 129 >>> calcQuadraticArcLength((0, 0), (50, 20), (100, 40)) # collinear points 130 107.70329614269008 131 >>> calcQuadraticArcLength((0, 0), (0, 100), (100, 0)) 132 154.02976155645263 133 >>> calcQuadraticArcLength((0, 0), (0, 50), (100, 0)) 134 120.21581243984076 135 >>> calcQuadraticArcLength((0, 0), (50, -10), (80, 50)) 136 102.53273816445825 137 >>> calcQuadraticArcLength((0, 0), (40, 0), (-40, 0)) # collinear points, control point outside 138 66.66666666666667 139 >>> calcQuadraticArcLength((0, 0), (40, 0), (0, 0)) # collinear points, looping back 140 40.0 141 """ 142 return calcQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3)) 143 144 145def calcQuadraticArcLengthC(pt1, pt2, pt3): 146 """Calculates the arc length for a quadratic Bezier segment. 147 148 Args: 149 pt1: Start point of the Bezier as a complex number. 150 pt2: Handle point of the Bezier as a complex number. 151 pt3: End point of the Bezier as a complex number. 152 153 Returns: 154 Arc length value. 155 """ 156 # Analytical solution to the length of a quadratic bezier. 157 # I'll explain how I arrived at this later. 158 d0 = pt2 - pt1 159 d1 = pt3 - pt2 160 d = d1 - d0 161 n = d * 1j 162 scale = abs(n) 163 if scale == 0.0: 164 return abs(pt3 - pt1) 165 origDist = _dot(n, d0) 166 if abs(origDist) < epsilon: 167 if _dot(d0, d1) >= 0: 168 return abs(pt3 - pt1) 169 a, b = abs(d0), abs(d1) 170 return (a * a + b * b) / (a + b) 171 x0 = _dot(d, d0) / origDist 172 x1 = _dot(d, d1) / origDist 173 Len = abs(2 * (_intSecAtan(x1) - _intSecAtan(x0)) * origDist / (scale * (x1 - x0))) 174 return Len 175 176 177def approximateQuadraticArcLength(pt1, pt2, pt3): 178 """Calculates the arc length for a quadratic Bezier segment. 179 180 Uses Gauss-Legendre quadrature for a branch-free approximation. 181 See :func:`calcQuadraticArcLength` for a slower but more accurate result. 182 183 Args: 184 pt1: Start point of the Bezier as 2D tuple. 185 pt2: Handle point of the Bezier as 2D tuple. 186 pt3: End point of the Bezier as 2D tuple. 187 188 Returns: 189 Approximate arc length value. 190 """ 191 return approximateQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3)) 192 193 194def approximateQuadraticArcLengthC(pt1, pt2, pt3): 195 """Calculates the arc length for a quadratic Bezier segment. 196 197 Uses Gauss-Legendre quadrature for a branch-free approximation. 198 See :func:`calcQuadraticArcLength` for a slower but more accurate result. 199 200 Args: 201 pt1: Start point of the Bezier as a complex number. 202 pt2: Handle point of the Bezier as a complex number. 203 pt3: End point of the Bezier as a complex number. 204 205 Returns: 206 Approximate arc length value. 207 """ 208 # This, essentially, approximates the length-of-derivative function 209 # to be integrated with the best-matching fifth-degree polynomial 210 # approximation of it. 211 # 212 # https://en.wikipedia.org/wiki/Gaussian_quadrature#Gauss.E2.80.93Legendre_quadrature 213 214 # abs(BezierCurveC[2].diff(t).subs({t:T})) for T in sorted(.5, .5±sqrt(3/5)/2), 215 # weighted 5/18, 8/18, 5/18 respectively. 216 v0 = abs( 217 -0.492943519233745 * pt1 + 0.430331482911935 * pt2 + 0.0626120363218102 * pt3 218 ) 219 v1 = abs(pt3 - pt1) * 0.4444444444444444 220 v2 = abs( 221 -0.0626120363218102 * pt1 - 0.430331482911935 * pt2 + 0.492943519233745 * pt3 222 ) 223 224 return v0 + v1 + v2 225 226 227def calcQuadraticBounds(pt1, pt2, pt3): 228 """Calculates the bounding rectangle for a quadratic Bezier segment. 229 230 Args: 231 pt1: Start point of the Bezier as a 2D tuple. 232 pt2: Handle point of the Bezier as a 2D tuple. 233 pt3: End point of the Bezier as a 2D tuple. 234 235 Returns: 236 A four-item tuple representing the bounding rectangle ``(xMin, yMin, xMax, yMax)``. 237 238 Example:: 239 240 >>> calcQuadraticBounds((0, 0), (50, 100), (100, 0)) 241 (0, 0, 100, 50.0) 242 >>> calcQuadraticBounds((0, 0), (100, 0), (100, 100)) 243 (0.0, 0.0, 100, 100) 244 """ 245 (ax, ay), (bx, by), (cx, cy) = calcQuadraticParameters(pt1, pt2, pt3) 246 ax2 = ax * 2.0 247 ay2 = ay * 2.0 248 roots = [] 249 if ax2 != 0: 250 roots.append(-bx / ax2) 251 if ay2 != 0: 252 roots.append(-by / ay2) 253 points = [ 254 (ax * t * t + bx * t + cx, ay * t * t + by * t + cy) 255 for t in roots 256 if 0 <= t < 1 257 ] + [pt1, pt3] 258 return calcBounds(points) 259 260 261def approximateCubicArcLength(pt1, pt2, pt3, pt4): 262 """Approximates the arc length for a cubic Bezier segment. 263 264 Uses Gauss-Lobatto quadrature with n=5 points to approximate arc length. 265 See :func:`calcCubicArcLength` for a slower but more accurate result. 266 267 Args: 268 pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples. 269 270 Returns: 271 Arc length value. 272 273 Example:: 274 275 >>> approximateCubicArcLength((0, 0), (25, 100), (75, 100), (100, 0)) 276 190.04332968932817 277 >>> approximateCubicArcLength((0, 0), (50, 0), (100, 50), (100, 100)) 278 154.8852074945903 279 >>> approximateCubicArcLength((0, 0), (50, 0), (100, 0), (150, 0)) # line; exact result should be 150. 280 149.99999999999991 281 >>> approximateCubicArcLength((0, 0), (50, 0), (100, 0), (-50, 0)) # cusp; exact result should be 150. 282 136.9267662156362 283 >>> approximateCubicArcLength((0, 0), (50, 0), (100, -50), (-50, 0)) # cusp 284 154.80848416537057 285 """ 286 return approximateCubicArcLengthC( 287 complex(*pt1), complex(*pt2), complex(*pt3), complex(*pt4) 288 ) 289 290 291def approximateCubicArcLengthC(pt1, pt2, pt3, pt4): 292 """Approximates the arc length for a cubic Bezier segment. 293 294 Args: 295 pt1,pt2,pt3,pt4: Control points of the Bezier as complex numbers. 296 297 Returns: 298 Arc length value. 299 """ 300 # This, essentially, approximates the length-of-derivative function 301 # to be integrated with the best-matching seventh-degree polynomial 302 # approximation of it. 303 # 304 # https://en.wikipedia.org/wiki/Gaussian_quadrature#Gauss.E2.80.93Lobatto_rules 305 306 # abs(BezierCurveC[3].diff(t).subs({t:T})) for T in sorted(0, .5±(3/7)**.5/2, .5, 1), 307 # weighted 1/20, 49/180, 32/90, 49/180, 1/20 respectively. 308 v0 = abs(pt2 - pt1) * 0.15 309 v1 = abs( 310 -0.558983582205757 * pt1 311 + 0.325650248872424 * pt2 312 + 0.208983582205757 * pt3 313 + 0.024349751127576 * pt4 314 ) 315 v2 = abs(pt4 - pt1 + pt3 - pt2) * 0.26666666666666666 316 v3 = abs( 317 -0.024349751127576 * pt1 318 - 0.208983582205757 * pt2 319 - 0.325650248872424 * pt3 320 + 0.558983582205757 * pt4 321 ) 322 v4 = abs(pt4 - pt3) * 0.15 323 324 return v0 + v1 + v2 + v3 + v4 325 326 327def calcCubicBounds(pt1, pt2, pt3, pt4): 328 """Calculates the bounding rectangle for a quadratic Bezier segment. 329 330 Args: 331 pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples. 332 333 Returns: 334 A four-item tuple representing the bounding rectangle ``(xMin, yMin, xMax, yMax)``. 335 336 Example:: 337 338 >>> calcCubicBounds((0, 0), (25, 100), (75, 100), (100, 0)) 339 (0, 0, 100, 75.0) 340 >>> calcCubicBounds((0, 0), (50, 0), (100, 50), (100, 100)) 341 (0.0, 0.0, 100, 100) 342 >>> print("%f %f %f %f" % calcCubicBounds((50, 0), (0, 100), (100, 100), (50, 0))) 343 35.566243 0.000000 64.433757 75.000000 344 """ 345 (ax, ay), (bx, by), (cx, cy), (dx, dy) = calcCubicParameters(pt1, pt2, pt3, pt4) 346 # calc first derivative 347 ax3 = ax * 3.0 348 ay3 = ay * 3.0 349 bx2 = bx * 2.0 350 by2 = by * 2.0 351 xRoots = [t for t in solveQuadratic(ax3, bx2, cx) if 0 <= t < 1] 352 yRoots = [t for t in solveQuadratic(ay3, by2, cy) if 0 <= t < 1] 353 roots = xRoots + yRoots 354 355 points = [ 356 ( 357 ax * t * t * t + bx * t * t + cx * t + dx, 358 ay * t * t * t + by * t * t + cy * t + dy, 359 ) 360 for t in roots 361 ] + [pt1, pt4] 362 return calcBounds(points) 363 364 365def splitLine(pt1, pt2, where, isHorizontal): 366 """Split a line at a given coordinate. 367 368 Args: 369 pt1: Start point of line as 2D tuple. 370 pt2: End point of line as 2D tuple. 371 where: Position at which to split the line. 372 isHorizontal: Direction of the ray splitting the line. If true, 373 ``where`` is interpreted as a Y coordinate; if false, then 374 ``where`` is interpreted as an X coordinate. 375 376 Returns: 377 A list of two line segments (each line segment being two 2D tuples) 378 if the line was successfully split, or a list containing the original 379 line. 380 381 Example:: 382 383 >>> printSegments(splitLine((0, 0), (100, 100), 50, True)) 384 ((0, 0), (50, 50)) 385 ((50, 50), (100, 100)) 386 >>> printSegments(splitLine((0, 0), (100, 100), 100, True)) 387 ((0, 0), (100, 100)) 388 >>> printSegments(splitLine((0, 0), (100, 100), 0, True)) 389 ((0, 0), (0, 0)) 390 ((0, 0), (100, 100)) 391 >>> printSegments(splitLine((0, 0), (100, 100), 0, False)) 392 ((0, 0), (0, 0)) 393 ((0, 0), (100, 100)) 394 >>> printSegments(splitLine((100, 0), (0, 0), 50, False)) 395 ((100, 0), (50, 0)) 396 ((50, 0), (0, 0)) 397 >>> printSegments(splitLine((0, 100), (0, 0), 50, True)) 398 ((0, 100), (0, 50)) 399 ((0, 50), (0, 0)) 400 """ 401 pt1x, pt1y = pt1 402 pt2x, pt2y = pt2 403 404 ax = pt2x - pt1x 405 ay = pt2y - pt1y 406 407 bx = pt1x 408 by = pt1y 409 410 a = (ax, ay)[isHorizontal] 411 412 if a == 0: 413 return [(pt1, pt2)] 414 t = (where - (bx, by)[isHorizontal]) / a 415 if 0 <= t < 1: 416 midPt = ax * t + bx, ay * t + by 417 return [(pt1, midPt), (midPt, pt2)] 418 else: 419 return [(pt1, pt2)] 420 421 422def splitQuadratic(pt1, pt2, pt3, where, isHorizontal): 423 """Split a quadratic Bezier curve at a given coordinate. 424 425 Args: 426 pt1,pt2,pt3: Control points of the Bezier as 2D tuples. 427 where: Position at which to split the curve. 428 isHorizontal: Direction of the ray splitting the curve. If true, 429 ``where`` is interpreted as a Y coordinate; if false, then 430 ``where`` is interpreted as an X coordinate. 431 432 Returns: 433 A list of two curve segments (each curve segment being three 2D tuples) 434 if the curve was successfully split, or a list containing the original 435 curve. 436 437 Example:: 438 439 >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 150, False)) 440 ((0, 0), (50, 100), (100, 0)) 441 >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 50, False)) 442 ((0, 0), (25, 50), (50, 50)) 443 ((50, 50), (75, 50), (100, 0)) 444 >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 25, False)) 445 ((0, 0), (12.5, 25), (25, 37.5)) 446 ((25, 37.5), (62.5, 75), (100, 0)) 447 >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 25, True)) 448 ((0, 0), (7.32233, 14.6447), (14.6447, 25)) 449 ((14.6447, 25), (50, 75), (85.3553, 25)) 450 ((85.3553, 25), (92.6777, 14.6447), (100, -7.10543e-15)) 451 >>> # XXX I'm not at all sure if the following behavior is desirable: 452 >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 50, True)) 453 ((0, 0), (25, 50), (50, 50)) 454 ((50, 50), (50, 50), (50, 50)) 455 ((50, 50), (75, 50), (100, 0)) 456 """ 457 a, b, c = calcQuadraticParameters(pt1, pt2, pt3) 458 solutions = solveQuadratic( 459 a[isHorizontal], b[isHorizontal], c[isHorizontal] - where 460 ) 461 solutions = sorted(t for t in solutions if 0 <= t < 1) 462 if not solutions: 463 return [(pt1, pt2, pt3)] 464 return _splitQuadraticAtT(a, b, c, *solutions) 465 466 467def splitCubic(pt1, pt2, pt3, pt4, where, isHorizontal): 468 """Split a cubic Bezier curve at a given coordinate. 469 470 Args: 471 pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples. 472 where: Position at which to split the curve. 473 isHorizontal: Direction of the ray splitting the curve. If true, 474 ``where`` is interpreted as a Y coordinate; if false, then 475 ``where`` is interpreted as an X coordinate. 476 477 Returns: 478 A list of two curve segments (each curve segment being four 2D tuples) 479 if the curve was successfully split, or a list containing the original 480 curve. 481 482 Example:: 483 484 >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 150, False)) 485 ((0, 0), (25, 100), (75, 100), (100, 0)) 486 >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 50, False)) 487 ((0, 0), (12.5, 50), (31.25, 75), (50, 75)) 488 ((50, 75), (68.75, 75), (87.5, 50), (100, 0)) 489 >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 25, True)) 490 ((0, 0), (2.29379, 9.17517), (4.79804, 17.5085), (7.47414, 25)) 491 ((7.47414, 25), (31.2886, 91.6667), (68.7114, 91.6667), (92.5259, 25)) 492 ((92.5259, 25), (95.202, 17.5085), (97.7062, 9.17517), (100, 1.77636e-15)) 493 """ 494 a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4) 495 solutions = solveCubic( 496 a[isHorizontal], b[isHorizontal], c[isHorizontal], d[isHorizontal] - where 497 ) 498 solutions = sorted(t for t in solutions if 0 <= t < 1) 499 if not solutions: 500 return [(pt1, pt2, pt3, pt4)] 501 return _splitCubicAtT(a, b, c, d, *solutions) 502 503 504def splitQuadraticAtT(pt1, pt2, pt3, *ts): 505 """Split a quadratic Bezier curve at one or more values of t. 506 507 Args: 508 pt1,pt2,pt3: Control points of the Bezier as 2D tuples. 509 *ts: Positions at which to split the curve. 510 511 Returns: 512 A list of curve segments (each curve segment being three 2D tuples). 513 514 Examples:: 515 516 >>> printSegments(splitQuadraticAtT((0, 0), (50, 100), (100, 0), 0.5)) 517 ((0, 0), (25, 50), (50, 50)) 518 ((50, 50), (75, 50), (100, 0)) 519 >>> printSegments(splitQuadraticAtT((0, 0), (50, 100), (100, 0), 0.5, 0.75)) 520 ((0, 0), (25, 50), (50, 50)) 521 ((50, 50), (62.5, 50), (75, 37.5)) 522 ((75, 37.5), (87.5, 25), (100, 0)) 523 """ 524 a, b, c = calcQuadraticParameters(pt1, pt2, pt3) 525 return _splitQuadraticAtT(a, b, c, *ts) 526 527 528def splitCubicAtT(pt1, pt2, pt3, pt4, *ts): 529 """Split a cubic Bezier curve at one or more values of t. 530 531 Args: 532 pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples. 533 *ts: Positions at which to split the curve. 534 535 Returns: 536 A list of curve segments (each curve segment being four 2D tuples). 537 538 Examples:: 539 540 >>> printSegments(splitCubicAtT((0, 0), (25, 100), (75, 100), (100, 0), 0.5)) 541 ((0, 0), (12.5, 50), (31.25, 75), (50, 75)) 542 ((50, 75), (68.75, 75), (87.5, 50), (100, 0)) 543 >>> printSegments(splitCubicAtT((0, 0), (25, 100), (75, 100), (100, 0), 0.5, 0.75)) 544 ((0, 0), (12.5, 50), (31.25, 75), (50, 75)) 545 ((50, 75), (59.375, 75), (68.75, 68.75), (77.3438, 56.25)) 546 ((77.3438, 56.25), (85.9375, 43.75), (93.75, 25), (100, 0)) 547 """ 548 a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4) 549 return _splitCubicAtT(a, b, c, d, *ts) 550 551 552def _splitQuadraticAtT(a, b, c, *ts): 553 ts = list(ts) 554 segments = [] 555 ts.insert(0, 0.0) 556 ts.append(1.0) 557 ax, ay = a 558 bx, by = b 559 cx, cy = c 560 for i in range(len(ts) - 1): 561 t1 = ts[i] 562 t2 = ts[i + 1] 563 delta = t2 - t1 564 # calc new a, b and c 565 delta_2 = delta * delta 566 a1x = ax * delta_2 567 a1y = ay * delta_2 568 b1x = (2 * ax * t1 + bx) * delta 569 b1y = (2 * ay * t1 + by) * delta 570 t1_2 = t1 * t1 571 c1x = ax * t1_2 + bx * t1 + cx 572 c1y = ay * t1_2 + by * t1 + cy 573 574 pt1, pt2, pt3 = calcQuadraticPoints((a1x, a1y), (b1x, b1y), (c1x, c1y)) 575 segments.append((pt1, pt2, pt3)) 576 return segments 577 578 579def _splitCubicAtT(a, b, c, d, *ts): 580 ts = list(ts) 581 ts.insert(0, 0.0) 582 ts.append(1.0) 583 segments = [] 584 ax, ay = a 585 bx, by = b 586 cx, cy = c 587 dx, dy = d 588 for i in range(len(ts) - 1): 589 t1 = ts[i] 590 t2 = ts[i + 1] 591 delta = t2 - t1 592 593 delta_2 = delta * delta 594 delta_3 = delta * delta_2 595 t1_2 = t1 * t1 596 t1_3 = t1 * t1_2 597 598 # calc new a, b, c and d 599 a1x = ax * delta_3 600 a1y = ay * delta_3 601 b1x = (3 * ax * t1 + bx) * delta_2 602 b1y = (3 * ay * t1 + by) * delta_2 603 c1x = (2 * bx * t1 + cx + 3 * ax * t1_2) * delta 604 c1y = (2 * by * t1 + cy + 3 * ay * t1_2) * delta 605 d1x = ax * t1_3 + bx * t1_2 + cx * t1 + dx 606 d1y = ay * t1_3 + by * t1_2 + cy * t1 + dy 607 pt1, pt2, pt3, pt4 = calcCubicPoints( 608 (a1x, a1y), (b1x, b1y), (c1x, c1y), (d1x, d1y) 609 ) 610 segments.append((pt1, pt2, pt3, pt4)) 611 return segments 612 613 614# 615# Equation solvers. 616# 617 618from math import sqrt, acos, cos, pi 619 620 621def solveQuadratic(a, b, c, sqrt=sqrt): 622 """Solve a quadratic equation. 623 624 Solves *a*x*x + b*x + c = 0* where a, b and c are real. 625 626 Args: 627 a: coefficient of *x²* 628 b: coefficient of *x* 629 c: constant term 630 631 Returns: 632 A list of roots. Note that the returned list is neither guaranteed to 633 be sorted nor to contain unique values! 634 """ 635 if abs(a) < epsilon: 636 if abs(b) < epsilon: 637 # We have a non-equation; therefore, we have no valid solution 638 roots = [] 639 else: 640 # We have a linear equation with 1 root. 641 roots = [-c / b] 642 else: 643 # We have a true quadratic equation. Apply the quadratic formula to find two roots. 644 DD = b * b - 4.0 * a * c 645 if DD >= 0.0: 646 rDD = sqrt(DD) 647 roots = [(-b + rDD) / 2.0 / a, (-b - rDD) / 2.0 / a] 648 else: 649 # complex roots, ignore 650 roots = [] 651 return roots 652 653 654def solveCubic(a, b, c, d): 655 """Solve a cubic equation. 656 657 Solves *a*x*x*x + b*x*x + c*x + d = 0* where a, b, c and d are real. 658 659 Args: 660 a: coefficient of *x³* 661 b: coefficient of *x²* 662 c: coefficient of *x* 663 d: constant term 664 665 Returns: 666 A list of roots. Note that the returned list is neither guaranteed to 667 be sorted nor to contain unique values! 668 669 Examples:: 670 671 >>> solveCubic(1, 1, -6, 0) 672 [-3.0, -0.0, 2.0] 673 >>> solveCubic(-10.0, -9.0, 48.0, -29.0) 674 [-2.9, 1.0, 1.0] 675 >>> solveCubic(-9.875, -9.0, 47.625, -28.75) 676 [-2.911392, 1.0, 1.0] 677 >>> solveCubic(1.0, -4.5, 6.75, -3.375) 678 [1.5, 1.5, 1.5] 679 >>> solveCubic(-12.0, 18.0, -9.0, 1.50023651123) 680 [0.5, 0.5, 0.5] 681 >>> solveCubic( 682 ... 9.0, 0.0, 0.0, -7.62939453125e-05 683 ... ) == [-0.0, -0.0, -0.0] 684 True 685 """ 686 # 687 # adapted from: 688 # CUBIC.C - Solve a cubic polynomial 689 # public domain by Ross Cottrell 690 # found at: http://www.strangecreations.com/library/snippets/Cubic.C 691 # 692 if abs(a) < epsilon: 693 # don't just test for zero; for very small values of 'a' solveCubic() 694 # returns unreliable results, so we fall back to quad. 695 return solveQuadratic(b, c, d) 696 a = float(a) 697 a1 = b / a 698 a2 = c / a 699 a3 = d / a 700 701 Q = (a1 * a1 - 3.0 * a2) / 9.0 702 R = (2.0 * a1 * a1 * a1 - 9.0 * a1 * a2 + 27.0 * a3) / 54.0 703 704 R2 = R * R 705 Q3 = Q * Q * Q 706 R2 = 0 if R2 < epsilon else R2 707 Q3 = 0 if abs(Q3) < epsilon else Q3 708 709 R2_Q3 = R2 - Q3 710 711 if R2 == 0.0 and Q3 == 0.0: 712 x = round(-a1 / 3.0, epsilonDigits) 713 return [x, x, x] 714 elif R2_Q3 <= epsilon * 0.5: 715 # The epsilon * .5 above ensures that Q3 is not zero. 716 theta = acos(max(min(R / sqrt(Q3), 1.0), -1.0)) 717 rQ2 = -2.0 * sqrt(Q) 718 a1_3 = a1 / 3.0 719 x0 = rQ2 * cos(theta / 3.0) - a1_3 720 x1 = rQ2 * cos((theta + 2.0 * pi) / 3.0) - a1_3 721 x2 = rQ2 * cos((theta + 4.0 * pi) / 3.0) - a1_3 722 x0, x1, x2 = sorted([x0, x1, x2]) 723 # Merge roots that are close-enough 724 if x1 - x0 < epsilon and x2 - x1 < epsilon: 725 x0 = x1 = x2 = round((x0 + x1 + x2) / 3.0, epsilonDigits) 726 elif x1 - x0 < epsilon: 727 x0 = x1 = round((x0 + x1) / 2.0, epsilonDigits) 728 x2 = round(x2, epsilonDigits) 729 elif x2 - x1 < epsilon: 730 x0 = round(x0, epsilonDigits) 731 x1 = x2 = round((x1 + x2) / 2.0, epsilonDigits) 732 else: 733 x0 = round(x0, epsilonDigits) 734 x1 = round(x1, epsilonDigits) 735 x2 = round(x2, epsilonDigits) 736 return [x0, x1, x2] 737 else: 738 x = pow(sqrt(R2_Q3) + abs(R), 1 / 3.0) 739 x = x + Q / x 740 if R >= 0.0: 741 x = -x 742 x = round(x - a1 / 3.0, epsilonDigits) 743 return [x] 744 745 746# 747# Conversion routines for points to parameters and vice versa 748# 749 750 751def calcQuadraticParameters(pt1, pt2, pt3): 752 x2, y2 = pt2 753 x3, y3 = pt3 754 cx, cy = pt1 755 bx = (x2 - cx) * 2.0 756 by = (y2 - cy) * 2.0 757 ax = x3 - cx - bx 758 ay = y3 - cy - by 759 return (ax, ay), (bx, by), (cx, cy) 760 761 762def calcCubicParameters(pt1, pt2, pt3, pt4): 763 x2, y2 = pt2 764 x3, y3 = pt3 765 x4, y4 = pt4 766 dx, dy = pt1 767 cx = (x2 - dx) * 3.0 768 cy = (y2 - dy) * 3.0 769 bx = (x3 - x2) * 3.0 - cx 770 by = (y3 - y2) * 3.0 - cy 771 ax = x4 - dx - cx - bx 772 ay = y4 - dy - cy - by 773 return (ax, ay), (bx, by), (cx, cy), (dx, dy) 774 775 776def calcQuadraticPoints(a, b, c): 777 ax, ay = a 778 bx, by = b 779 cx, cy = c 780 x1 = cx 781 y1 = cy 782 x2 = (bx * 0.5) + cx 783 y2 = (by * 0.5) + cy 784 x3 = ax + bx + cx 785 y3 = ay + by + cy 786 return (x1, y1), (x2, y2), (x3, y3) 787 788 789def calcCubicPoints(a, b, c, d): 790 ax, ay = a 791 bx, by = b 792 cx, cy = c 793 dx, dy = d 794 x1 = dx 795 y1 = dy 796 x2 = (cx / 3.0) + dx 797 y2 = (cy / 3.0) + dy 798 x3 = (bx + cx) / 3.0 + x2 799 y3 = (by + cy) / 3.0 + y2 800 x4 = ax + dx + cx + bx 801 y4 = ay + dy + cy + by 802 return (x1, y1), (x2, y2), (x3, y3), (x4, y4) 803 804 805# 806# Point at time 807# 808 809 810def linePointAtT(pt1, pt2, t): 811 """Finds the point at time `t` on a line. 812 813 Args: 814 pt1, pt2: Coordinates of the line as 2D tuples. 815 t: The time along the line. 816 817 Returns: 818 A 2D tuple with the coordinates of the point. 819 """ 820 return ((pt1[0] * (1 - t) + pt2[0] * t), (pt1[1] * (1 - t) + pt2[1] * t)) 821 822 823def quadraticPointAtT(pt1, pt2, pt3, t): 824 """Finds the point at time `t` on a quadratic curve. 825 826 Args: 827 pt1, pt2, pt3: Coordinates of the curve as 2D tuples. 828 t: The time along the curve. 829 830 Returns: 831 A 2D tuple with the coordinates of the point. 832 """ 833 x = (1 - t) * (1 - t) * pt1[0] + 2 * (1 - t) * t * pt2[0] + t * t * pt3[0] 834 y = (1 - t) * (1 - t) * pt1[1] + 2 * (1 - t) * t * pt2[1] + t * t * pt3[1] 835 return (x, y) 836 837 838def cubicPointAtT(pt1, pt2, pt3, pt4, t): 839 """Finds the point at time `t` on a cubic curve. 840 841 Args: 842 pt1, pt2, pt3, pt4: Coordinates of the curve as 2D tuples. 843 t: The time along the curve. 844 845 Returns: 846 A 2D tuple with the coordinates of the point. 847 """ 848 x = ( 849 (1 - t) * (1 - t) * (1 - t) * pt1[0] 850 + 3 * (1 - t) * (1 - t) * t * pt2[0] 851 + 3 * (1 - t) * t * t * pt3[0] 852 + t * t * t * pt4[0] 853 ) 854 y = ( 855 (1 - t) * (1 - t) * (1 - t) * pt1[1] 856 + 3 * (1 - t) * (1 - t) * t * pt2[1] 857 + 3 * (1 - t) * t * t * pt3[1] 858 + t * t * t * pt4[1] 859 ) 860 return (x, y) 861 862 863def segmentPointAtT(seg, t): 864 if len(seg) == 2: 865 return linePointAtT(*seg, t) 866 elif len(seg) == 3: 867 return quadraticPointAtT(*seg, t) 868 elif len(seg) == 4: 869 return cubicPointAtT(*seg, t) 870 raise ValueError("Unknown curve degree") 871 872 873# 874# Intersection finders 875# 876 877 878def _line_t_of_pt(s, e, pt): 879 sx, sy = s 880 ex, ey = e 881 px, py = pt 882 if not math.isclose(sx, ex): 883 return (px - sx) / (ex - sx) 884 if not math.isclose(sy, ey): 885 return (py - sy) / (ey - sy) 886 # Line is a point! 887 return -1 888 889 890def _both_points_are_on_same_side_of_origin(a, b, origin): 891 xDiff = (a[0] - origin[0]) * (b[0] - origin[0]) 892 yDiff = (a[1] - origin[1]) * (b[1] - origin[1]) 893 return not (xDiff <= 0.0 and yDiff <= 0.0) 894 895 896def lineLineIntersections(s1, e1, s2, e2): 897 """Finds intersections between two line segments. 898 899 Args: 900 s1, e1: Coordinates of the first line as 2D tuples. 901 s2, e2: Coordinates of the second line as 2D tuples. 902 903 Returns: 904 A list of ``Intersection`` objects, each object having ``pt``, ``t1`` 905 and ``t2`` attributes containing the intersection point, time on first 906 segment and time on second segment respectively. 907 908 Examples:: 909 910 >>> a = lineLineIntersections( (310,389), (453, 222), (289, 251), (447, 367)) 911 >>> len(a) 912 1 913 >>> intersection = a[0] 914 >>> intersection.pt 915 (374.44882952482897, 313.73458370177315) 916 >>> (intersection.t1, intersection.t2) 917 (0.45069111555824454, 0.5408153767394238) 918 """ 919 s1x, s1y = s1 920 e1x, e1y = e1 921 s2x, s2y = s2 922 e2x, e2y = e2 923 if ( 924 math.isclose(s2x, e2x) and math.isclose(s1x, e1x) and not math.isclose(s1x, s2x) 925 ): # Parallel vertical 926 return [] 927 if ( 928 math.isclose(s2y, e2y) and math.isclose(s1y, e1y) and not math.isclose(s1y, s2y) 929 ): # Parallel horizontal 930 return [] 931 if math.isclose(s2x, e2x) and math.isclose(s2y, e2y): # Line segment is tiny 932 return [] 933 if math.isclose(s1x, e1x) and math.isclose(s1y, e1y): # Line segment is tiny 934 return [] 935 if math.isclose(e1x, s1x): 936 x = s1x 937 slope34 = (e2y - s2y) / (e2x - s2x) 938 y = slope34 * (x - s2x) + s2y 939 pt = (x, y) 940 return [ 941 Intersection( 942 pt=pt, t1=_line_t_of_pt(s1, e1, pt), t2=_line_t_of_pt(s2, e2, pt) 943 ) 944 ] 945 if math.isclose(s2x, e2x): 946 x = s2x 947 slope12 = (e1y - s1y) / (e1x - s1x) 948 y = slope12 * (x - s1x) + s1y 949 pt = (x, y) 950 return [ 951 Intersection( 952 pt=pt, t1=_line_t_of_pt(s1, e1, pt), t2=_line_t_of_pt(s2, e2, pt) 953 ) 954 ] 955 956 slope12 = (e1y - s1y) / (e1x - s1x) 957 slope34 = (e2y - s2y) / (e2x - s2x) 958 if math.isclose(slope12, slope34): 959 return [] 960 x = (slope12 * s1x - s1y - slope34 * s2x + s2y) / (slope12 - slope34) 961 y = slope12 * (x - s1x) + s1y 962 pt = (x, y) 963 if _both_points_are_on_same_side_of_origin( 964 pt, e1, s1 965 ) and _both_points_are_on_same_side_of_origin(pt, s2, e2): 966 return [ 967 Intersection( 968 pt=pt, t1=_line_t_of_pt(s1, e1, pt), t2=_line_t_of_pt(s2, e2, pt) 969 ) 970 ] 971 return [] 972 973 974def _alignment_transformation(segment): 975 # Returns a transformation which aligns a segment horizontally at the 976 # origin. Apply this transformation to curves and root-find to find 977 # intersections with the segment. 978 start = segment[0] 979 end = segment[-1] 980 angle = math.atan2(end[1] - start[1], end[0] - start[0]) 981 return Identity.rotate(-angle).translate(-start[0], -start[1]) 982 983 984def _curve_line_intersections_t(curve, line): 985 aligned_curve = _alignment_transformation(line).transformPoints(curve) 986 if len(curve) == 3: 987 a, b, c = calcQuadraticParameters(*aligned_curve) 988 intersections = solveQuadratic(a[1], b[1], c[1]) 989 elif len(curve) == 4: 990 a, b, c, d = calcCubicParameters(*aligned_curve) 991 intersections = solveCubic(a[1], b[1], c[1], d[1]) 992 else: 993 raise ValueError("Unknown curve degree") 994 return sorted(i for i in intersections if 0.0 <= i <= 1) 995 996 997def curveLineIntersections(curve, line): 998 """Finds intersections between a curve and a line. 999 1000 Args: 1001 curve: List of coordinates of the curve segment as 2D tuples. 1002 line: List of coordinates of the line segment as 2D tuples. 1003 1004 Returns: 1005 A list of ``Intersection`` objects, each object having ``pt``, ``t1`` 1006 and ``t2`` attributes containing the intersection point, time on first 1007 segment and time on second segment respectively. 1008 1009 Examples:: 1010 >>> curve = [ (100, 240), (30, 60), (210, 230), (160, 30) ] 1011 >>> line = [ (25, 260), (230, 20) ] 1012 >>> intersections = curveLineIntersections(curve, line) 1013 >>> len(intersections) 1014 3 1015 >>> intersections[0].pt 1016 (84.90010344084885, 189.87306176459828) 1017 """ 1018 if len(curve) == 3: 1019 pointFinder = quadraticPointAtT 1020 elif len(curve) == 4: 1021 pointFinder = cubicPointAtT 1022 else: 1023 raise ValueError("Unknown curve degree") 1024 intersections = [] 1025 for t in _curve_line_intersections_t(curve, line): 1026 pt = pointFinder(*curve, t) 1027 intersections.append(Intersection(pt=pt, t1=t, t2=_line_t_of_pt(*line, pt))) 1028 return intersections 1029 1030 1031def _curve_bounds(c): 1032 if len(c) == 3: 1033 return calcQuadraticBounds(*c) 1034 elif len(c) == 4: 1035 return calcCubicBounds(*c) 1036 raise ValueError("Unknown curve degree") 1037 1038 1039def _split_segment_at_t(c, t): 1040 if len(c) == 2: 1041 s, e = c 1042 midpoint = linePointAtT(s, e, t) 1043 return [(s, midpoint), (midpoint, e)] 1044 if len(c) == 3: 1045 return splitQuadraticAtT(*c, t) 1046 elif len(c) == 4: 1047 return splitCubicAtT(*c, t) 1048 raise ValueError("Unknown curve degree") 1049 1050 1051def _curve_curve_intersections_t( 1052 curve1, curve2, precision=1e-3, range1=None, range2=None 1053): 1054 bounds1 = _curve_bounds(curve1) 1055 bounds2 = _curve_bounds(curve2) 1056 1057 if not range1: 1058 range1 = (0.0, 1.0) 1059 if not range2: 1060 range2 = (0.0, 1.0) 1061 1062 # If bounds don't intersect, go home 1063 intersects, _ = sectRect(bounds1, bounds2) 1064 if not intersects: 1065 return [] 1066 1067 def midpoint(r): 1068 return 0.5 * (r[0] + r[1]) 1069 1070 # If they do overlap but they're tiny, approximate 1071 if rectArea(bounds1) < precision and rectArea(bounds2) < precision: 1072 return [(midpoint(range1), midpoint(range2))] 1073 1074 c11, c12 = _split_segment_at_t(curve1, 0.5) 1075 c11_range = (range1[0], midpoint(range1)) 1076 c12_range = (midpoint(range1), range1[1]) 1077 1078 c21, c22 = _split_segment_at_t(curve2, 0.5) 1079 c21_range = (range2[0], midpoint(range2)) 1080 c22_range = (midpoint(range2), range2[1]) 1081 1082 found = [] 1083 found.extend( 1084 _curve_curve_intersections_t( 1085 c11, c21, precision, range1=c11_range, range2=c21_range 1086 ) 1087 ) 1088 found.extend( 1089 _curve_curve_intersections_t( 1090 c12, c21, precision, range1=c12_range, range2=c21_range 1091 ) 1092 ) 1093 found.extend( 1094 _curve_curve_intersections_t( 1095 c11, c22, precision, range1=c11_range, range2=c22_range 1096 ) 1097 ) 1098 found.extend( 1099 _curve_curve_intersections_t( 1100 c12, c22, precision, range1=c12_range, range2=c22_range 1101 ) 1102 ) 1103 1104 unique_key = lambda ts: (int(ts[0] / precision), int(ts[1] / precision)) 1105 seen = set() 1106 unique_values = [] 1107 1108 for ts in found: 1109 key = unique_key(ts) 1110 if key in seen: 1111 continue 1112 seen.add(key) 1113 unique_values.append(ts) 1114 1115 return unique_values 1116 1117 1118def curveCurveIntersections(curve1, curve2): 1119 """Finds intersections between a curve and a curve. 1120 1121 Args: 1122 curve1: List of coordinates of the first curve segment as 2D tuples. 1123 curve2: List of coordinates of the second curve segment as 2D tuples. 1124 1125 Returns: 1126 A list of ``Intersection`` objects, each object having ``pt``, ``t1`` 1127 and ``t2`` attributes containing the intersection point, time on first 1128 segment and time on second segment respectively. 1129 1130 Examples:: 1131 >>> curve1 = [ (10,100), (90,30), (40,140), (220,220) ] 1132 >>> curve2 = [ (5,150), (180,20), (80,250), (210,190) ] 1133 >>> intersections = curveCurveIntersections(curve1, curve2) 1134 >>> len(intersections) 1135 3 1136 >>> intersections[0].pt 1137 (81.7831487395506, 109.88904552375288) 1138 """ 1139 intersection_ts = _curve_curve_intersections_t(curve1, curve2) 1140 return [ 1141 Intersection(pt=segmentPointAtT(curve1, ts[0]), t1=ts[0], t2=ts[1]) 1142 for ts in intersection_ts 1143 ] 1144 1145 1146def segmentSegmentIntersections(seg1, seg2): 1147 """Finds intersections between two segments. 1148 1149 Args: 1150 seg1: List of coordinates of the first segment as 2D tuples. 1151 seg2: List of coordinates of the second segment as 2D tuples. 1152 1153 Returns: 1154 A list of ``Intersection`` objects, each object having ``pt``, ``t1`` 1155 and ``t2`` attributes containing the intersection point, time on first 1156 segment and time on second segment respectively. 1157 1158 Examples:: 1159 >>> curve1 = [ (10,100), (90,30), (40,140), (220,220) ] 1160 >>> curve2 = [ (5,150), (180,20), (80,250), (210,190) ] 1161 >>> intersections = segmentSegmentIntersections(curve1, curve2) 1162 >>> len(intersections) 1163 3 1164 >>> intersections[0].pt 1165 (81.7831487395506, 109.88904552375288) 1166 >>> curve3 = [ (100, 240), (30, 60), (210, 230), (160, 30) ] 1167 >>> line = [ (25, 260), (230, 20) ] 1168 >>> intersections = segmentSegmentIntersections(curve3, line) 1169 >>> len(intersections) 1170 3 1171 >>> intersections[0].pt 1172 (84.90010344084885, 189.87306176459828) 1173 1174 """ 1175 # Arrange by degree 1176 swapped = False 1177 if len(seg2) > len(seg1): 1178 seg2, seg1 = seg1, seg2 1179 swapped = True 1180 if len(seg1) > 2: 1181 if len(seg2) > 2: 1182 intersections = curveCurveIntersections(seg1, seg2) 1183 else: 1184 intersections = curveLineIntersections(seg1, seg2) 1185 elif len(seg1) == 2 and len(seg2) == 2: 1186 intersections = lineLineIntersections(*seg1, *seg2) 1187 else: 1188 raise ValueError("Couldn't work out which intersection function to use") 1189 if not swapped: 1190 return intersections 1191 return [Intersection(pt=i.pt, t1=i.t2, t2=i.t1) for i in intersections] 1192 1193 1194def _segmentrepr(obj): 1195 """ 1196 >>> _segmentrepr([1, [2, 3], [], [[2, [3, 4], [0.1, 2.2]]]]) 1197 '(1, (2, 3), (), ((2, (3, 4), (0.1, 2.2))))' 1198 """ 1199 try: 1200 it = iter(obj) 1201 except TypeError: 1202 return "%g" % obj 1203 else: 1204 return "(%s)" % ", ".join(_segmentrepr(x) for x in it) 1205 1206 1207def printSegments(segments): 1208 """Helper for the doctests, displaying each segment in a list of 1209 segments on a single line as a tuple. 1210 """ 1211 for segment in segments: 1212 print(_segmentrepr(segment)) 1213 1214 1215if __name__ == "__main__": 1216 import sys 1217 import doctest 1218 1219 sys.exit(doctest.testmod().failed) 1220