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