• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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