Intersection and point-at-time functions from beziers.py
This commit is contained in:
parent
8e42f693a7
commit
01957a9b94
@ -2,9 +2,13 @@
|
||||
"""fontTools.misc.bezierTools.py -- tools for working with Bezier path segments.
|
||||
"""
|
||||
|
||||
from fontTools.misc.arrayTools import calcBounds
|
||||
from fontTools.misc.arrayTools import calcBounds, sectRect, rectArea
|
||||
from fontTools.misc.transform import Offset, Identity
|
||||
from fontTools.misc.py23 import *
|
||||
import math
|
||||
from collections import namedtuple
|
||||
|
||||
Intersection = namedtuple("Intersection", ["pt", "t1", "t2"])
|
||||
|
||||
|
||||
__all__ = [
|
||||
@ -25,6 +29,14 @@ __all__ = [
|
||||
"splitCubicAtT",
|
||||
"solveQuadratic",
|
||||
"solveCubic",
|
||||
"quadraticPointAtT",
|
||||
"cubicPointAtT",
|
||||
"linePointAtT",
|
||||
"segmentPointAtT",
|
||||
"lineLineIntersections",
|
||||
"curveLineIntersections",
|
||||
"curveCurveIntersections",
|
||||
"segmentSegmentIntersections",
|
||||
]
|
||||
|
||||
|
||||
@ -753,10 +765,376 @@ def calcCubicPoints(a, b, c, d):
|
||||
return (x1, y1), (x2, y2), (x3, y3), (x4, y4)
|
||||
|
||||
|
||||
#
|
||||
# Point at time
|
||||
#
|
||||
|
||||
|
||||
def linePointAtT(pt1, pt2, t):
|
||||
"""Finds the point at time `t` on a line.
|
||||
|
||||
Args:
|
||||
pt1, pt2: Coordinates of the line as 2D tuples.
|
||||
t: The time along the line.
|
||||
|
||||
Returns:
|
||||
A 2D tuple with the coordinates of the point.
|
||||
"""
|
||||
return ((pt1[0] * (1 - t) + pt2[0] * t), (pt1[1] * (1 - t) + pt2[1] * t))
|
||||
|
||||
|
||||
def quadraticPointAtT(pt1, pt2, pt3, t):
|
||||
"""Finds the point at time `t` on a quadratic curve.
|
||||
|
||||
Args:
|
||||
pt1, pt2, pt3: Coordinates of the curve as 2D tuples.
|
||||
t: The time along the curve.
|
||||
|
||||
Returns:
|
||||
A 2D tuple with the coordinates of the point.
|
||||
"""
|
||||
x = (1 - t) * (1 - t) * pt1[0] + 2 * (1 - t) * t * pt2[0] + t * t * pt3[0]
|
||||
y = (1 - t) * (1 - t) * pt1[1] + 2 * (1 - t) * t * pt2[1] + t * t * pt3[1]
|
||||
return (x, y)
|
||||
|
||||
|
||||
def cubicPointAtT(pt1, pt2, pt3, pt4, t):
|
||||
"""Finds the point at time `t` on a cubic curve.
|
||||
|
||||
Args:
|
||||
pt1, pt2, pt3, pt4: Coordinates of the curve as 2D tuples.
|
||||
t: The time along the curve.
|
||||
|
||||
Returns:
|
||||
A 2D tuple with the coordinates of the point.
|
||||
"""
|
||||
x = (
|
||||
(1 - t) * (1 - t) * (1 - t) * pt1[0]
|
||||
+ 3 * (1 - t) * (1 - t) * t * pt2[0]
|
||||
+ 3 * (1 - t) * t * t * pt3[0]
|
||||
+ t * t * t * pt4[0]
|
||||
)
|
||||
y = (
|
||||
(1 - t) * (1 - t) * (1 - t) * pt1[1]
|
||||
+ 3 * (1 - t) * (1 - t) * t * pt2[1]
|
||||
+ 3 * (1 - t) * t * t * pt3[1]
|
||||
+ t * t * t * pt4[1]
|
||||
)
|
||||
return (x, y)
|
||||
|
||||
|
||||
def segmentPointAtT(seg, t):
|
||||
if len(seg) == 2:
|
||||
return linePointAtT(*seg, t)
|
||||
elif len(seg) == 3:
|
||||
return quadraticPointAtT(*seg, t)
|
||||
elif len(seg) == 4:
|
||||
return cubicPointAtT(*seg, t)
|
||||
raise ValueError("Unknown curve degree")
|
||||
|
||||
|
||||
#
|
||||
# Intersection finders
|
||||
#
|
||||
|
||||
|
||||
def _line_t_of_pt(s, e, pt):
|
||||
sx, sy = s
|
||||
ex, ey = e
|
||||
px, py = pt
|
||||
if not math.isclose(sx, ex):
|
||||
return (px - sx) / (ex - sx)
|
||||
if not math.isclose(sy, ey):
|
||||
return (py - sy) / (ey - sy)
|
||||
# Line is a point!
|
||||
return -1
|
||||
|
||||
|
||||
def _both_points_are_on_same_side_of_origin(a, b, c):
|
||||
xDiff = (a[0] - c[0]) * (b[0] - c[0])
|
||||
yDiff = (a[1] - c[1]) * (b[1] - c[1])
|
||||
return not (xDiff <= 0.0 and yDiff <= 0.0)
|
||||
|
||||
|
||||
def lineLineIntersections(s1, e1, s2, e2):
|
||||
"""Finds intersections between two line segments.
|
||||
|
||||
Args:
|
||||
s1, e1: Coordinates of the first line as 2D tuples.
|
||||
s2, e2: Coordinates of the second line as 2D tuples.
|
||||
|
||||
Returns:
|
||||
A list of ``Intersection`` objects, each object having ``pt``, ``t1``
|
||||
and ``t2`` attributes containing the intersection point, time on first
|
||||
segment and time on second segment respectively.
|
||||
|
||||
Examples::
|
||||
|
||||
>>> a = lineLineIntersections( (310,389), (453, 222), (289, 251), (447, 367))
|
||||
>>> len(a)
|
||||
1
|
||||
>>> intersection = a[0]
|
||||
>>> intersection.pt
|
||||
(374.44882952482897, 313.73458370177315)
|
||||
>>> (intersection.t1, intersection.t2)
|
||||
(0.45069111555824454, 0.5408153767394238)
|
||||
"""
|
||||
ax, ay = s1
|
||||
bx, by = e1
|
||||
cx, cy = s2
|
||||
dx, dy = e2
|
||||
if math.isclose(cx, dx) and math.isclose(ax, bx):
|
||||
return []
|
||||
if math.isclose(cy, dy) and math.isclose(ay, by):
|
||||
return []
|
||||
if math.isclose(cx, dx) and math.isclose(cy, dy):
|
||||
return []
|
||||
if math.isclose(ax, bx) and math.isclose(ay, by):
|
||||
return []
|
||||
if math.isclose(bx, ax):
|
||||
x = ax
|
||||
slope34 = (dy - xy) / (dx - cx)
|
||||
y = slope34 * (x - cx) + cy
|
||||
pt = (x, y)
|
||||
return [
|
||||
Intersection(
|
||||
pt=pt, t1=_line_t_of_pt(s1, e1, pt), t2=_line_t_of_pt(s2, e2, pt)
|
||||
)
|
||||
]
|
||||
if math.isclose(cx, dx):
|
||||
x = cx
|
||||
slope12 = (by - ay) / (bx - ax)
|
||||
y = slope12 * (x - ax) + ay
|
||||
pt = (x, y)
|
||||
return [
|
||||
Intersection(
|
||||
pt=pt, t1=_line_t_of_pt(s1, e1, pt), t2=_line_t_of_pt(s2, e2, pt)
|
||||
)
|
||||
]
|
||||
|
||||
slope12 = (by - ay) / (bx - ax)
|
||||
slope34 = (dy - cy) / (dx - cx)
|
||||
if math.isclose(slope12, slope34):
|
||||
return []
|
||||
x = (slope12 * ax - ay - slope34 * cx + cy) / (slope12 - slope34)
|
||||
y = slope12 * (x - ax) + ay
|
||||
pt = (x, y)
|
||||
if _both_points_are_on_same_side_of_origin(
|
||||
pt, e1, s1
|
||||
) and _both_points_are_on_same_side_of_origin(pt, s2, e2):
|
||||
return [
|
||||
Intersection(
|
||||
pt=pt, t1=_line_t_of_pt(s1, e1, pt), t2=_line_t_of_pt(s2, e2, pt)
|
||||
)
|
||||
]
|
||||
return []
|
||||
|
||||
|
||||
def _alignment_transformation(segment):
|
||||
start = segment[0]
|
||||
end = segment[-1]
|
||||
m = Offset(-start[0], -start[1])
|
||||
endpt = m.transformPoint(end)
|
||||
angle = math.atan2(endpt[1], endpt[0])
|
||||
return m.reverseTransform(Identity.rotate(-angle))
|
||||
|
||||
|
||||
def _curve_line_intersections_t(curve, line):
|
||||
aligned_curve = _alignment_transformation(line).transformPoints(curve)
|
||||
if len(curve) == 3:
|
||||
a, b, c = calcCubicParameters(*aligned_curve)
|
||||
intersections = solveQuadratic(a[0], b[0], c[0])
|
||||
intersections.extend(solveQuadratic(a[1], b[1], c[1]))
|
||||
elif len(curve) == 4:
|
||||
a, b, c, d = calcCubicParameters(*aligned_curve)
|
||||
intersections = solveCubic(a[0], b[0], c[0], d[0])
|
||||
intersections.extend(solveCubic(a[1], b[1], c[1], d[1]))
|
||||
else:
|
||||
raise ValueError("Unknown curve degree")
|
||||
return sorted([i for i in intersections if (0.0 <= i <= 1)])
|
||||
|
||||
|
||||
def curveLineIntersections(curve, line):
|
||||
"""Finds intersections between a curve and a line.
|
||||
|
||||
Args:
|
||||
curve: List of coordinates of the curve segment as 2D tuples.
|
||||
line: List of coordinates of the line segment as 2D tuples.
|
||||
|
||||
Returns:
|
||||
A list of ``Intersection`` objects, each object having ``pt``, ``t1``
|
||||
and ``t2`` attributes containing the intersection point, time on first
|
||||
segment and time on second segment respectively.
|
||||
|
||||
Examples::
|
||||
>>> curve = [ (100, 240), (30, 60), (210, 230), (160, 30) ]
|
||||
>>> line = [ (25, 260), (230, 20) ]
|
||||
>>> intersections = curveLineIntersections(curve, line)
|
||||
>>> len(intersections)
|
||||
3
|
||||
>>> intersections[0].pt
|
||||
(84.90010344084885, 189.87306176459828)
|
||||
"""
|
||||
if len(curve) == 3:
|
||||
pointFinder = quadraticPointAtT
|
||||
elif len(curve) == 4:
|
||||
pointFinder = cubicPointAtT
|
||||
else:
|
||||
raise ValueError("Unknown curve degree")
|
||||
intersections = []
|
||||
for t in _curve_line_intersections_t(curve, line):
|
||||
pt = pointFinder(*curve, t)
|
||||
intersections.append(Intersection(pt=pt, t1=t, t2=_line_t_of_pt(*line, pt)))
|
||||
return intersections
|
||||
|
||||
|
||||
def _curve_bounds(c):
|
||||
if len(c) == 3:
|
||||
return calcQuadraticBounds(*c)
|
||||
elif len(c) == 4:
|
||||
return calcCubicBounds(*c)
|
||||
|
||||
|
||||
def _split_curve_at_t(c, t):
|
||||
if len(c) == 3:
|
||||
return splitQuadraticAtT(*c, t)
|
||||
elif len(c) == 4:
|
||||
return splitCubicAtT(*c, t)
|
||||
|
||||
|
||||
def _curve_curve_intersections_t(
|
||||
curve1, curve2, precision=1e-3, range1=None, range2=None
|
||||
):
|
||||
bounds1 = _curve_bounds(curve1)
|
||||
bounds2 = _curve_bounds(curve2)
|
||||
|
||||
if not range1:
|
||||
range1 = (0.0, 1.0)
|
||||
if not range2:
|
||||
range2 = (0.0, 1.0)
|
||||
|
||||
# If bounds don't intersect, go home
|
||||
intersects, _ = sectRect(bounds1, bounds2)
|
||||
if not intersects:
|
||||
return []
|
||||
|
||||
def midpoint(r):
|
||||
return 0.5 * (r[0] + r[1])
|
||||
|
||||
# If they do overlap but they're tiny, approximate
|
||||
if rectArea(bounds1) < precision and rectArea(bounds2) < precision:
|
||||
return [(midpoint(range1), midpoint(range2))]
|
||||
|
||||
c11, c12 = _split_curve_at_t(curve1, 0.5)
|
||||
c11_range = (range1[0], midpoint(range1))
|
||||
c12_range = (midpoint(range1), range1[1])
|
||||
|
||||
c21, c22 = _split_curve_at_t(curve2, 0.5)
|
||||
c21_range = (range2[0], midpoint(range2))
|
||||
c22_range = (midpoint(range2), range2[1])
|
||||
|
||||
found = []
|
||||
found.extend(
|
||||
_curve_curve_intersections_t(
|
||||
c11, c21, precision, range1=c11_range, range2=c21_range
|
||||
)
|
||||
)
|
||||
found.extend(
|
||||
_curve_curve_intersections_t(
|
||||
c12, c21, precision, range1=c12_range, range2=c21_range
|
||||
)
|
||||
)
|
||||
found.extend(
|
||||
_curve_curve_intersections_t(
|
||||
c11, c22, precision, range1=c11_range, range2=c22_range
|
||||
)
|
||||
)
|
||||
found.extend(
|
||||
_curve_curve_intersections_t(
|
||||
c12, c22, precision, range1=c12_range, range2=c22_range
|
||||
)
|
||||
)
|
||||
unique_key = lambda ts: int(ts[0] / precision)
|
||||
seen = set()
|
||||
return [
|
||||
seen.add(unique_key(ts)) or ts for ts in found if unique_key(ts) not in seen
|
||||
]
|
||||
|
||||
|
||||
def curveCurveIntersections(curve1, curve2):
|
||||
"""Finds intersections between a curve and a curve.
|
||||
|
||||
Args:
|
||||
curve1: List of coordinates of the first curve segment as 2D tuples.
|
||||
curve2: List of coordinates of the second curve segment as 2D tuples.
|
||||
|
||||
Returns:
|
||||
A list of ``Intersection`` objects, each object having ``pt``, ``t1``
|
||||
and ``t2`` attributes containing the intersection point, time on first
|
||||
segment and time on second segment respectively.
|
||||
|
||||
Examples::
|
||||
>>> curve1 = [ (10,100), (90,30), (40,140), (220,220) ]
|
||||
>>> curve2 = [ (5,150), (180,20), (80,250), (210,190) ]
|
||||
>>> intersections = curveCurveIntersections(curve1, curve2)
|
||||
>>> len(intersections)
|
||||
3
|
||||
>>> intersections[0].pt
|
||||
(81.7831487395506, 109.88904552375288)
|
||||
"""
|
||||
intersection_ts = _curve_curve_intersections_t(curve1, curve2)
|
||||
return [
|
||||
Intersection(pt=segmentPointAtT(curve1, ts[0]), t1=ts[0], t2=ts[1])
|
||||
for ts in intersection_ts
|
||||
]
|
||||
|
||||
|
||||
def segmentSegmentIntersections(seg1, seg2):
|
||||
"""Finds intersections between two segments.
|
||||
|
||||
Args:
|
||||
seg1: List of coordinates of the first segment as 2D tuples.
|
||||
seg2: List of coordinates of the second segment as 2D tuples.
|
||||
|
||||
Returns:
|
||||
A list of ``Intersection`` objects, each object having ``pt``, ``t1``
|
||||
and ``t2`` attributes containing the intersection point, time on first
|
||||
segment and time on second segment respectively.
|
||||
|
||||
Examples::
|
||||
>>> curve1 = [ (10,100), (90,30), (40,140), (220,220) ]
|
||||
>>> curve2 = [ (5,150), (180,20), (80,250), (210,190) ]
|
||||
>>> intersections = segmentSegmentIntersections(curve1, curve2)
|
||||
>>> len(intersections)
|
||||
3
|
||||
>>> intersections[0].pt
|
||||
(81.7831487395506, 109.88904552375288)
|
||||
>>> curve3 = [ (100, 240), (30, 60), (210, 230), (160, 30) ]
|
||||
>>> line = [ (25, 260), (230, 20) ]
|
||||
>>> intersections = segmentSegmentIntersections(curve3, line)
|
||||
>>> len(intersections)
|
||||
3
|
||||
>>> intersections[0].pt
|
||||
(84.90010344084885, 189.87306176459828)
|
||||
|
||||
"""
|
||||
# Arrange by degree
|
||||
if len(seg2) > len(seg1):
|
||||
seg2, seg1 = seg1, seg2
|
||||
if len(seg1) > 2:
|
||||
if len(seg2) > 2:
|
||||
return curveCurveIntersections(seg1, seg2)
|
||||
else:
|
||||
return curveLineIntersections(seg1, seg2)
|
||||
elif len(seg1) == 2 and len(seg2) == 2:
|
||||
return lineLineIntersections(seg1, seg2)
|
||||
raise ValueError("Couldn't work out which intersection function to use")
|
||||
|
||||
|
||||
def _segmentrepr(obj):
|
||||
"""
|
||||
>>> _segmentrepr([1, [2, 3], [], [[2, [3, 4], [0.1, 2.2]]]])
|
||||
'(1, (2, 3), (), ((2, (3, 4), (0.1, 2.2))))'
|
||||
>>> _segmentrepr([1, [2, 3], [], [[2, [3, 4], [0.1, 2.2]]]])
|
||||
'(1, (2, 3), (), ((2, (3, 4), (0.1, 2.2))))'
|
||||
"""
|
||||
try:
|
||||
it = iter(obj)
|
||||
|
Loading…
x
Reference in New Issue
Block a user