Remove Point class and only use built-ins

Using built-in types (mostly tuples) seems to cut the running time
overall by about one half. Spelling out linear interpolation in the
bezier_at functions actually cuts the running time by another 10%,
but I'm not sure if it's worth it given that this code looks a bit
nicer.
This commit is contained in:
jamesgk 2015-12-01 13:20:09 -08:00
parent bb79a8bf62
commit 561bed0ea0
2 changed files with 56 additions and 54 deletions

View File

@ -17,78 +17,82 @@ from math import hypot
from fontTools.misc import bezierTools from fontTools.misc import bezierTools
class Point: def vector(p1, p2):
"""An arithmetic-compatible 2D vector. """Return the vector from p1 to p2."""
We use this because arithmetic with RoboFab's RPoint is prohibitively slow. return p2[0] - p1[0], p2[1] - p1[1]
"""
def __init__(self, p):
self.p = map(float, p)
def __getitem__(self, key): def translate(p, v):
return self.p[key] """Translate a point by a vector."""
return p[0] + v[0], p[1] + v[1]
def __add__(self, other):
return Point([a + b for a, b in zip(self.p, other.p)])
def __sub__(self, other): def scale(v, n):
return Point([a - b for a, b in zip(self.p, other.p)]) """Scale a vector."""
return v[0] * n, v[1] * n
def __mul__(self, n):
return Point([a * n for a in self.p])
def dist(self, other): def dist(p1, p2):
"""Calculate the distance between two points.""" """Calculate the distance between two points."""
return hypot(self[0] - other[0], self[1] - other[1]) return hypot(p1[0] - p2[0], p1[1] - p2[1])
def dot(self, other):
"""Return the dot product of two points."""
return sum(a * b for a, b in zip(self.p, other.p))
def lerp(p1, p2, t): def dot(v1, v2):
"""Linearly interpolate between p1 and p2 at time t.""" """Return the dot product of two vectors."""
return p1 * (1 - t) + p2 * t return v1[0] * v2[0] + v1[1] * v2[1]
def lerp(a, b, t):
"""Linearly interpolate between scalars a and b at time t."""
return a * (1 - t) + b * t
def lerp_pt(p1, p2, t):
"""Linearly interpolate between points p1 and p2 at time t."""
(x1, y1), (x2, y2) = p1, p2
return lerp(x1, x2, t), lerp(y1, y2, t)
def quadratic_bezier_at(p, t): def quadratic_bezier_at(p, t):
"""Return the point on a quadratic bezier curve at time t.""" """Return the point on a quadratic bezier curve at time t."""
return Point([ (x1, y1), (x2, y2), (x3, y3) = p
lerp(lerp(p[0][0], p[1][0], t), lerp(p[1][0], p[2][0], t), t), return (
lerp(lerp(p[0][1], p[1][1], t), lerp(p[1][1], p[2][1], t), t)]) lerp(lerp(x1, x2, t), lerp(x2, x3, t), t),
lerp(lerp(y1, y2, t), lerp(y2, y3, t), t))
def cubic_bezier_at(p, t): def cubic_bezier_at(p, t):
"""Return the point on a cubic bezier curve at time t.""" """Return the point on a cubic bezier curve at time t."""
return Point([ (x1, y1), (x2, y2), (x3, y3), (x4, y4) = p
lerp(lerp(lerp(p[0][0], p[1][0], t), lerp(p[1][0], p[2][0], t), t), return (
lerp(lerp(p[1][0], p[2][0], t), lerp(p[2][0], p[3][0], t), t), t), lerp(lerp(lerp(x1, x2, t), lerp(x2, x3, t), t),
lerp(lerp(lerp(p[0][1], p[1][1], t), lerp(p[1][1], p[2][1], t), t), lerp(lerp(x2, x3, t), lerp(x3, x4, t), t), t),
lerp(lerp(p[1][1], p[2][1], t), lerp(p[2][1], p[3][1], t), t), t)]) lerp(lerp(lerp(y1, y2, t), lerp(y2, y3, t), t),
lerp(lerp(y2, y3, t), lerp(y3, y4, t), t), t))
def cubic_approx(p, t): def cubic_approx(p, t):
"""Approximate a cubic bezier curve with a quadratic one.""" """Approximate a cubic bezier curve with a quadratic one."""
p1 = lerp(p[0], p[1], 1.5) p1 = lerp_pt(p[0], p[1], 1.5)
p2 = lerp(p[3], p[2], 1.5) p2 = lerp_pt(p[3], p[2], 1.5)
return [p[0], lerp(p1, p2, t), p[3]] return p[0], lerp_pt(p1, p2, t), p[3]
def calc_intersect(p): def calc_intersect(p):
"""Calculate the intersection of ab and cd, given [a, b, c, d].""" """Calculate the intersection of ab and cd, given [a, b, c, d]."""
a, b, c, d = p a, b, c, d = p
ab = b - a ab = vector(a, b)
cd = d - c cd = vector(c, d)
p = Point([-ab[1], ab[0]]) p = -ab[1], ab[0]
try: try:
h = p.dot(a - c) / p.dot(cd) h = dot(p, vector(c, a)) / dot(p, cd)
except ZeroDivisionError: except ZeroDivisionError:
raise ValueError('Parallel vectors given to calc_intersect.') raise ValueError('Parallel vectors given to calc_intersect.')
return c + cd * h return translate(c, scale(cd, h))
def cubic_approx_spline(p, n): def cubic_approx_spline(p, n):
@ -107,9 +111,7 @@ def cubic_approx_spline(p, n):
spline = [p[0]] spline = [p[0]]
ts = [(float(i) / n) for i in range(1, n)] ts = [(float(i) / n) for i in range(1, n)]
segments = [ segments = bezierTools.splitCubicAtT(p[0], p[1], p[2], p[3], *ts)
map(Point, segment)
for segment in bezierTools.splitCubicAtT(p[0], p[1], p[2], p[3], *ts)]
for i in range(len(segments)): for i in range(len(segments)):
segment = cubic_approx(segments[i], float(i) / (n - 1)) segment = cubic_approx(segments[i], float(i) / (n - 1))
spline.append(segment[1]) spline.append(segment[1])
@ -128,11 +130,11 @@ def curve_spline_dist(bezier, spline):
segment = [ segment = [
spline[0] if i == 1 else segment[2], spline[0] if i == 1 else segment[2],
spline[i], spline[i],
spline[i + 1] if i == n else lerp(spline[i], spline[i + 1], 0.5)] spline[i + 1] if i == n else lerp_pt(spline[i], spline[i + 1], 0.5)]
for j in range(steps): for j in range(steps):
p1 = cubic_bezier_at(bezier, (float(j) / steps + i - 1) / n) p1 = cubic_bezier_at(bezier, (float(j) / steps + i - 1) / n)
p2 = quadratic_bezier_at(segment, float(j) / steps) p2 = quadratic_bezier_at(segment, float(j) / steps)
error = max(error, p1.dist(p2)) error = max(error, dist(p1, p2))
return error return error

View File

@ -25,7 +25,7 @@ the resulting splines are interpolation-compatible.
from robofab.objects.objectsRF import RSegment from robofab.objects.objectsRF import RSegment
from cu2qu import Point, curve_to_quadratic, curves_to_quadratic from cu2qu import curve_to_quadratic, curves_to_quadratic
_zip = zip _zip = zip
@ -115,10 +115,10 @@ def points_to_quadratic(p0, p1, p2, p3, max_err, max_n):
""" """
if hasattr(p0, 'x'): if hasattr(p0, 'x'):
curve = [Point([i.x, i.y]) for i in [p0, p1, p2, p3]] curve = [[float(i.x), float(i.y)] for i in [p0, p1, p2, p3]]
return curve_to_quadratic(curve, max_err, max_n) return curve_to_quadratic(curve, max_err, max_n)
curves = [[Point([i.x, i.y]) for i in p] for p in zip(p0, p1, p2, p3)] curves = [[[float(i.x), float(i.y)] for i in p] for p in zip(p0, p1, p2, p3)]
return curves_to_quadratic(curves, max_err, max_n) return curves_to_quadratic(curves, max_err, max_n)