Repository: fonttools/fonttools

Issue:
Curve-curve intersection edge cases
These two "line-like" cubics have an intersection point at (0,0.5), but `curveCurveIntersections` does not find it:

```python
from fontTools.misc.bezierTools import lineLineIntersections, curveCurveIntersections

seg1 = [(0.0, 0.0), (0.0, 0.25), (0.0, 0.75), (0.0, 1.0)]
seg2 = [(0.0, 0.5), (0.25, 0.5), (0.75, 0.5), (1.0, 0.5)]

print(lineLineIntersections(seg1[0], seg1[-1], seg2[0], seg2[-1]))
# [Intersection(pt=(0.0, 0.5), t1=0.5, t2=0.0)]

print(curveCurveIntersections(seg1, seg2))
# []
```


Discussion:
In this case, we are failing at the first test (bounding box intersection between the two curves), so we go home early and don't do any subdivision test:

```python
seg1 = [(0.0, 0.0), (0.0, 0.25), (0.0, 0.75), (0.0, 1.0)]
seg2 = [(0.0, 0.5), (0.25, 0.5), (0.75, 0.5), (1.0, 0.5)]

bounds1 = _curve_bounds(seg1)
bounds2 = _curve_bounds(seg2)

print(bounds1, bounds2)
# (0.0, 0.0, 0.0, 1.0) (0.0, 0.5, 1.0, 0.5)

print(sectRect(bounds1, bounds2))
# (False, (0, 0, 0, 0))
```
good catch! would it be enough to special-case this edge case when both curves's bounds are flattened to perperdicular lines?
What I would suggest is that if a curve is sufficiently line-like (for some definition...), it gets turned into a line, and then either `curveLineIntersections` or `lineLineIntersections` handles it. This'll be both faster and more correct.
maybe it's our `sectRect` that's at fault.

In the classic "Graphics Gems" IV book, the chapter about curve intersections, they have the code below in the routine that checks two beziers' bounds intersection (in particular, see the comment ` // Not >= : need boundary case`)

https://web.archive.org/web/20130613230901/http://users.soe.ucsc.edu/~pang/160/f98/Gems/GemsIV/curve_isect/Bezier.cc

```c
    // Test bounding box of b against bounding box of a
    if( ( minax > maxbx ) || ( minay > maxby )  // Not >= : need boundary case
	|| ( minbx > maxax ) || ( minby > maxay ) )
	return 0; // they don't intersect
    else
	return 1; // they intersect
```
Possibly true. But if you *can* get away with treating curves as lines, the maths gets a lot simpler and less liable to go wrong, so we should do that.

Resolving patch:
diff --git a/Lib/fontTools/misc/bezierTools.py b/Lib/fontTools/misc/bezierTools.py
--- a/Lib/fontTools/misc/bezierTools.py
+++ b/Lib/fontTools/misc/bezierTools.py
@@ -1370,6 +1370,11 @@ def midpoint(r):
     return unique_values
 
 
+def _is_linelike(segment):
+    maybeline = _alignment_transformation(segment).transformPoints(segment)
+    return all(math.isclose(p[1], 0.0) for p in maybeline)
+
+
 def curveCurveIntersections(curve1, curve2):
     """Finds intersections between a curve and a curve.
 
@@ -1391,6 +1396,17 @@ def curveCurveIntersections(curve1, curve2):
         >>> intersections[0].pt
         (81.7831487395506, 109.88904552375288)
     """
+    if _is_linelike(curve1):
+        line1 = curve1[0], curve1[-1]
+        if _is_linelike(curve2):
+            line2 = curve2[0], curve2[-1]
+            return lineLineIntersections(*line1, *line2)
+        else:
+            return curveLineIntersections(curve2, line1)
+    elif _is_linelike(curve2):
+        line2 = curve2[0], curve2[-1]
+        return curveLineIntersections(curve1, line2)
+
     intersection_ts = _curve_curve_intersections_t(curve1, curve2)
     return [
         Intersection(pt=segmentPointAtT(curve1, ts[0]), t1=ts[0], t2=ts[1])


Test patch:
diff --git a/Tests/misc/bezierTools_test.py b/Tests/misc/bezierTools_test.py
--- a/Tests/misc/bezierTools_test.py
+++ b/Tests/misc/bezierTools_test.py
@@ -4,6 +4,7 @@
     calcQuadraticArcLength,
     calcCubicBounds,
     curveLineIntersections,
+    curveCurveIntersections,
     segmentPointAtT,
     splitLine,
     splitQuadratic,
@@ -189,3 +190,10 @@ def test_calcQuadraticArcLength():
     assert calcQuadraticArcLength(
         (210, 333), (289, 333), (326.5, 290.5)
     ) == pytest.approx(127.9225)
+
+
+def test_intersections_linelike():
+    seg1 = [(0.0, 0.0), (0.0, 0.25), (0.0, 0.75), (0.0, 1.0)]
+    seg2 = [(0.0, 0.5), (0.25, 0.5), (0.75, 0.5), (1.0, 0.5)]
+    pt = curveCurveIntersections(seg1, seg2)[0][0]
+    assert pt == (0.0, 0.5)