summaryrefslogtreecommitdiff
path: root/misc/pylib/robofab/misc
diff options
context:
space:
mode:
Diffstat (limited to 'misc/pylib/robofab/misc')
-rwxr-xr-xmisc/pylib/robofab/misc/__init__.py13
-rw-r--r--misc/pylib/robofab/misc/arrayTools.pyx160
-rw-r--r--misc/pylib/robofab/misc/bezierTools.py416
-rw-r--r--misc/pylib/robofab/misc/speedTestCase.py99
-rw-r--r--misc/pylib/robofab/misc/test.py119
5 files changed, 807 insertions, 0 deletions
diff --git a/misc/pylib/robofab/misc/__init__.py b/misc/pylib/robofab/misc/__init__.py
new file mode 100755
index 000000000..5ed66a4e4
--- /dev/null
+++ b/misc/pylib/robofab/misc/__init__.py
@@ -0,0 +1,13 @@
+"""
+
+ arrayTools and bezierTools, originally from fontTools and using Numpy,
+ now in a pure python implementation. This should ease the Numpy dependency
+ for normal UFO input/output and basic scripting tasks.
+
+ comparison test and speedtest provided.
+
+"""
+
+
+
+
diff --git a/misc/pylib/robofab/misc/arrayTools.pyx b/misc/pylib/robofab/misc/arrayTools.pyx
new file mode 100644
index 000000000..95884aa20
--- /dev/null
+++ b/misc/pylib/robofab/misc/arrayTools.pyx
@@ -0,0 +1,160 @@
+#
+# Various array and rectangle tools, but mostly rectangles, hence the
+# name of this module (not).
+#
+
+"""
+Rewritten to elimate the numpy dependency
+"""
+
+import math
+
+def calcBounds(array):
+ """Return the bounding rectangle of a 2D points array as a tuple:
+ (xMin, yMin, xMax, yMax)
+ """
+ if len(array) == 0:
+ return 0, 0, 0, 0
+ xs = [x for x, y in array]
+ ys = [y for x, y in array]
+ return min(xs), min(ys), max(xs), max(ys)
+
+def updateBounds(bounds, pt, min=min, max=max):
+ """Return the bounding recangle of rectangle bounds and point (x, y)."""
+ xMin, yMin, xMax, yMax = bounds
+ x, y = pt
+ return min(xMin, x), min(yMin, y), max(xMax, x), max(yMax, y)
+
+def pointInRect(pt, rect):
+ """Return True when point (x, y) is inside rect."""
+ xMin, yMin, xMax, yMax = rect
+ return (xMin <= pt[0] <= xMax) and (yMin <= pt[1] <= yMax)
+
+def pointsInRect(array, rect):
+ """Find out which points or array are inside rect.
+ Returns an array with a boolean for each point.
+ """
+ if len(array) < 1:
+ return []
+ xMin, yMin, xMax, yMax = rect
+ return [(xMin <= x <= xMax) and (yMin <= y <= yMax) for x, y in array]
+
+def vectorLength(vector):
+ """Return the length of the given vector."""
+ x, y = vector
+ return math.sqrt(x**2 + y**2)
+
+def asInt16(array):
+ """Round and cast to 16 bit integer."""
+ return [int(math.floor(i+0.5)) for i in array]
+
+
+def normRect(box):
+ """Normalize the rectangle so that the following holds:
+ xMin <= xMax and yMin <= yMax
+ """
+ return min(box[0], box[2]), min(box[1], box[3]), max(box[0], box[2]), max(box[1], box[3])
+
+def scaleRect(box, x, y):
+ """Scale the rectangle by x, y."""
+ return box[0] * x, box[1] * y, box[2] * x, box[3] * y
+
+def offsetRect(box, dx, dy):
+ """Offset the rectangle by dx, dy."""
+ return box[0]+dx, box[1]+dy, box[2]+dx, box[3]+dy
+
+def insetRect(box, dx, dy):
+ """Inset the rectangle by dx, dy on all sides."""
+ return box[0]+dx, box[1]+dy, box[2]-dx, box[3]-dy
+
+def sectRect(box1, box2):
+ """Return a boolean and a rectangle. If the input rectangles intersect, return
+ True and the intersecting rectangle. Return False and (0, 0, 0, 0) if the input
+ rectangles don't intersect.
+ """
+ xMin, yMin, xMax, yMax = (max(box1[0], box2[0]), max(box1[1], box2[1]),
+ min(box1[2], box2[2]), min(box1[3], box2[3]))
+ if xMin >= xMax or yMin >= yMax:
+ return 0, (0, 0, 0, 0)
+ return 1, (xMin, yMin, xMax, yMax)
+
+def unionRect(box1, box2):
+ """Return the smallest rectangle in which both input rectangles are fully
+ enclosed. In other words, return the total bounding rectangle of both input
+ rectangles.
+ """
+ return (max(box1[0], box2[0]), max(box1[1], box2[1]),
+ min(box1[2], box2[2]), min(box1[3], box2[3]))
+
+def rectCenter(box):
+ """Return the center of the rectangle as an (x, y) coordinate."""
+ return (box[0]+box[2])/2, (box[1]+box[3])/2
+
+def intRect(box):
+ """Return the rectangle, rounded off to integer values, but guaranteeing that
+ the resulting rectangle is NOT smaller than the original.
+ """
+ xMin, yMin, xMax, yMax = box
+ xMin = int(math.floor(xMin))
+ yMin = int(math.floor(yMin))
+ xMax = int(math.ceil(xMax))
+ yMax = int(math.ceil(yMax))
+ return (xMin, yMin, xMax, yMax)
+
+
+def _test():
+ """
+ >>> import math
+ >>> calcBounds([(0, 40), (0, 100), (50, 50), (80, 10)])
+ (0, 10, 80, 100)
+ >>> updateBounds((0, 0, 0, 0), (100, 100))
+ (0, 0, 100, 100)
+ >>> pointInRect((50, 50), (0, 0, 100, 100))
+ True
+ >>> pointInRect((0, 0), (0, 0, 100, 100))
+ True
+ >>> pointInRect((100, 100), (0, 0, 100, 100))
+ True
+ >>> not pointInRect((101, 100), (0, 0, 100, 100))
+ True
+ >>> list(pointsInRect([(50, 50), (0, 0), (100, 100), (101, 100)], (0, 0, 100, 100)))
+ [True, True, True, False]
+ >>> vectorLength((3, 4))
+ 5.0
+ >>> vectorLength((1, 1)) == math.sqrt(2)
+ True
+ >>> list(asInt16([0, 0.1, 0.5, 0.9]))
+ [0, 0, 1, 1]
+ >>> normRect((0, 10, 100, 200))
+ (0, 10, 100, 200)
+ >>> normRect((100, 200, 0, 10))
+ (0, 10, 100, 200)
+ >>> scaleRect((10, 20, 50, 150), 1.5, 2)
+ (15.0, 40, 75.0, 300)
+ >>> offsetRect((10, 20, 30, 40), 5, 6)
+ (15, 26, 35, 46)
+ >>> insetRect((10, 20, 50, 60), 5, 10)
+ (15, 30, 45, 50)
+ >>> insetRect((10, 20, 50, 60), -5, -10)
+ (5, 10, 55, 70)
+ >>> intersects, rect = sectRect((0, 10, 20, 30), (0, 40, 20, 50))
+ >>> not intersects
+ True
+ >>> intersects, rect = sectRect((0, 10, 20, 30), (5, 20, 35, 50))
+ >>> intersects
+ 1
+ >>> rect
+ (5, 20, 20, 30)
+ >>> unionRect((0, 10, 20, 30), (0, 40, 20, 50))
+ (0, 10, 20, 50)
+ >>> rectCenter((0, 0, 100, 200))
+ (50, 100)
+ >>> rectCenter((0, 0, 100, 199.0))
+ (50, 99.5)
+ >>> intRect((0.9, 2.9, 3.1, 4.1))
+ (0, 2, 4, 5)
+ """
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
diff --git a/misc/pylib/robofab/misc/bezierTools.py b/misc/pylib/robofab/misc/bezierTools.py
new file mode 100644
index 000000000..9872060b1
--- /dev/null
+++ b/misc/pylib/robofab/misc/bezierTools.py
@@ -0,0 +1,416 @@
+"""fontTools.misc.bezierTools.py -- tools for working with bezier path segments.
+Rewritten to elimate the numpy dependency
+"""
+
+
+__all__ = [
+ "calcQuadraticBounds",
+ "calcCubicBounds",
+ "splitLine",
+ "splitQuadratic",
+ "splitCubic",
+ "splitQuadraticAtT",
+ "splitCubicAtT",
+ "solveQuadratic",
+ "solveCubic",
+]
+
+from robofab.misc.arrayTools import calcBounds
+
+epsilon = 1e-12
+
+
+def calcQuadraticBounds(pt1, pt2, pt3):
+ """Return the bounding rectangle for a qudratic bezier segment.
+ pt1 and pt3 are the "anchor" points, pt2 is the "handle".
+
+ >>> calcQuadraticBounds((0, 0), (50, 100), (100, 0))
+ (0, 0, 100, 50.0)
+ >>> calcQuadraticBounds((0, 0), (100, 0), (100, 100))
+ (0.0, 0.0, 100, 100)
+ """
+ (ax, ay), (bx, by), (cx, cy) = calcQuadraticParameters(pt1, pt2, pt3)
+ ax2 = ax*2.0
+ ay2 = ay*2.0
+ roots = []
+ if ax2 != 0:
+ roots.append(-bx/ax2)
+ if ay2 != 0:
+ roots.append(-by/ay2)
+ points = [(ax*t*t + bx*t + cx, ay*t*t + by*t + cy) for t in roots if 0 <= t < 1] + [pt1, pt3]
+ return calcBounds(points)
+
+
+def calcCubicBounds(pt1, pt2, pt3, pt4):
+ """Return the bounding rectangle for a cubic bezier segment.
+ pt1 and pt4 are the "anchor" points, pt2 and pt3 are the "handles".
+
+ >>> calcCubicBounds((0, 0), (25, 100), (75, 100), (100, 0))
+ (0, 0, 100, 75.0)
+ >>> calcCubicBounds((0, 0), (50, 0), (100, 50), (100, 100))
+ (0.0, 0.0, 100, 100)
+ >>> calcCubicBounds((50, 0), (0, 100), (100, 100), (50, 0))
+ (35.566243270259356, 0, 64.43375672974068, 75.0)
+ """
+ (ax, ay), (bx, by), (cx, cy), (dx, dy) = calcCubicParameters(pt1, pt2, pt3, pt4)
+ # calc first derivative
+ ax3 = ax * 3.0
+ ay3 = ay * 3.0
+ bx2 = bx * 2.0
+ by2 = by * 2.0
+ xRoots = [t for t in solveQuadratic(ax3, bx2, cx) if 0 <= t < 1]
+ yRoots = [t for t in solveQuadratic(ay3, by2, cy) if 0 <= t < 1]
+ roots = xRoots + yRoots
+
+ points = [(ax*t*t*t + bx*t*t + cx * t + dx, ay*t*t*t + by*t*t + cy * t + dy) for t in roots] + [pt1, pt4]
+ return calcBounds(points)
+
+
+def splitLine(pt1, pt2, where, isHorizontal):
+ """Split the line between pt1 and pt2 at position 'where', which
+ is an x coordinate if isHorizontal is False, a y coordinate if
+ isHorizontal is True. Return a list of two line segments if the
+ line was successfully split, or a list containing the original
+ line.
+
+ >>> printSegments(splitLine((0, 0), (100, 200), 50, False))
+ ((0, 0), (50.0, 100.0))
+ ((50.0, 100.0), (100, 200))
+ >>> printSegments(splitLine((0, 0), (100, 200), 50, True))
+ ((0, 0), (25.0, 50.0))
+ ((25.0, 50.0), (100, 200))
+ >>> printSegments(splitLine((0, 0), (100, 100), 50, True))
+ ((0, 0), (50.0, 50.0))
+ ((50.0, 50.0), (100, 100))
+ >>> printSegments(splitLine((0, 0), (100, 100), 100, True))
+ ((0, 0), (100, 100))
+ >>> printSegments(splitLine((0, 0), (100, 100), 0, True))
+ ((0, 0), (0.0, 0.0))
+ ((0.0, 0.0), (100, 100))
+ >>> printSegments(splitLine((0, 0), (100, 100), 0, False))
+ ((0, 0), (0.0, 0.0))
+ ((0.0, 0.0), (100, 100))
+ """
+ pt1x, pt1y = pt1
+ pt2x, pt2y = pt2
+
+ ax = (pt2x - pt1x)
+ ay = (pt2y - pt1y)
+
+ bx = pt1x
+ by = pt1y
+
+ ax1 = (ax, ay)[isHorizontal]
+
+ if ax1 == 0:
+ return [(pt1, pt2)]
+
+ t = float(where - (bx, by)[isHorizontal]) / ax1
+ if 0 <= t < 1:
+ midPt = ax * t + bx, ay * t + by
+ return [(pt1, midPt), (midPt, pt2)]
+ else:
+ return [(pt1, pt2)]
+
+
+def splitQuadratic(pt1, pt2, pt3, where, isHorizontal):
+ """Split the quadratic curve between pt1, pt2 and pt3 at position 'where',
+ which is an x coordinate if isHorizontal is False, a y coordinate if
+ isHorizontal is True. Return a list of curve segments.
+
+ >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 150, False))
+ ((0, 0), (50, 100), (100, 0))
+ >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 50, False))
+ ((0.0, 0.0), (25.0, 50.0), (50.0, 50.0))
+ ((50.0, 50.0), (75.0, 50.0), (100.0, 0.0))
+ >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 25, False))
+ ((0.0, 0.0), (12.5, 25.0), (25.0, 37.5))
+ ((25.0, 37.5), (62.5, 75.0), (100.0, 0.0))
+ >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 25, True))
+ ((0.0, 0.0), (7.32233047034, 14.6446609407), (14.6446609407, 25.0))
+ ((14.6446609407, 25.0), (50.0, 75.0), (85.3553390593, 25.0))
+ ((85.3553390593, 25.0), (92.6776695297, 14.6446609407), (100.0, -7.1054273576e-15))
+ >>> # XXX I'm not at all sure if the following behavior is desirable:
+ >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 50, True))
+ ((0.0, 0.0), (25.0, 50.0), (50.0, 50.0))
+ ((50.0, 50.0), (50.0, 50.0), (50.0, 50.0))
+ ((50.0, 50.0), (75.0, 50.0), (100.0, 0.0))
+ """
+ a, b, c = calcQuadraticParameters(pt1, pt2, pt3)
+ solutions = solveQuadratic(a[isHorizontal], b[isHorizontal],
+ c[isHorizontal] - where)
+ solutions = [t for t in solutions if 0 <= t < 1]
+ solutions.sort()
+ if not solutions:
+ return [(pt1, pt2, pt3)]
+ return _splitQuadraticAtT(a, b, c, *solutions)
+
+
+def splitCubic(pt1, pt2, pt3, pt4, where, isHorizontal):
+ """Split the cubic curve between pt1, pt2, pt3 and pt4 at position 'where',
+ which is an x coordinate if isHorizontal is False, a y coordinate if
+ isHorizontal is True. Return a list of curve segments.
+
+ >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 150, False))
+ ((0, 0), (25, 100), (75, 100), (100, 0))
+ >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 50, False))
+ ((0.0, 0.0), (12.5, 50.0), (31.25, 75.0), (50.0, 75.0))
+ ((50.0, 75.0), (68.75, 75.0), (87.5, 50.0), (100.0, 0.0))
+ >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 25, True))
+ ((0.0, 0.0), (2.2937927384, 9.17517095361), (4.79804488188, 17.5085042869), (7.47413641001, 25.0))
+ ((7.47413641001, 25.0), (31.2886200204, 91.6666666667), (68.7113799796, 91.6666666667), (92.52586359, 25.0))
+ ((92.52586359, 25.0), (95.2019551181, 17.5085042869), (97.7062072616, 9.17517095361), (100.0, 1.7763568394e-15))
+ """
+ a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4)
+ solutions = solveCubic(a[isHorizontal], b[isHorizontal], c[isHorizontal],
+ d[isHorizontal] - where)
+ solutions = [t for t in solutions if 0 <= t < 1]
+ solutions.sort()
+ if not solutions:
+ return [(pt1, pt2, pt3, pt4)]
+ return _splitCubicAtT(a, b, c, d, *solutions)
+
+
+def splitQuadraticAtT(pt1, pt2, pt3, *ts):
+ """Split the quadratic curve between pt1, pt2 and pt3 at one or more
+ values of t. Return a list of curve segments.
+
+ >>> printSegments(splitQuadraticAtT((0, 0), (50, 100), (100, 0), 0.5))
+ ((0.0, 0.0), (25.0, 50.0), (50.0, 50.0))
+ ((50.0, 50.0), (75.0, 50.0), (100.0, 0.0))
+ >>> printSegments(splitQuadraticAtT((0, 0), (50, 100), (100, 0), 0.5, 0.75))
+ ((0.0, 0.0), (25.0, 50.0), (50.0, 50.0))
+ ((50.0, 50.0), (62.5, 50.0), (75.0, 37.5))
+ ((75.0, 37.5), (87.5, 25.0), (100.0, 0.0))
+ """
+ a, b, c = calcQuadraticParameters(pt1, pt2, pt3)
+ return _splitQuadraticAtT(a, b, c, *ts)
+
+
+def splitCubicAtT(pt1, pt2, pt3, pt4, *ts):
+ """Split the cubic curve between pt1, pt2, pt3 and pt4 at one or more
+ values of t. Return a list of curve segments.
+
+ >>> printSegments(splitCubicAtT((0, 0), (25, 100), (75, 100), (100, 0), 0.5))
+ ((0.0, 0.0), (12.5, 50.0), (31.25, 75.0), (50.0, 75.0))
+ ((50.0, 75.0), (68.75, 75.0), (87.5, 50.0), (100.0, 0.0))
+ >>> printSegments(splitCubicAtT((0, 0), (25, 100), (75, 100), (100, 0), 0.5, 0.75))
+ ((0.0, 0.0), (12.5, 50.0), (31.25, 75.0), (50.0, 75.0))
+ ((50.0, 75.0), (59.375, 75.0), (68.75, 68.75), (77.34375, 56.25))
+ ((77.34375, 56.25), (85.9375, 43.75), (93.75, 25.0), (100.0, 0.0))
+ """
+ a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4)
+ return _splitCubicAtT(a, b, c, d, *ts)
+
+
+def _splitQuadraticAtT(a, b, c, *ts):
+ ts = list(ts)
+ segments = []
+ ts.insert(0, 0.0)
+ ts.append(1.0)
+ ax, ay = a
+ bx, by = b
+ cx, cy = c
+ for i in range(len(ts) - 1):
+ t1 = ts[i]
+ t2 = ts[i+1]
+ delta = (t2 - t1)
+ # calc new a, b and c
+ a1x = ax * delta**2
+ a1y = ay * delta**2
+ b1x = (2*ax*t1 + bx) * delta
+ b1y = (2*ay*t1 + by) * delta
+ c1x = ax*t1**2 + bx*t1 + cx
+ c1y = ay*t1**2 + by*t1 + cy
+
+ pt1, pt2, pt3 = calcQuadraticPoints((a1x, a1y), (b1x, b1y), (c1x, c1y))
+ segments.append((pt1, pt2, pt3))
+ return segments
+
+
+def _splitCubicAtT(a, b, c, d, *ts):
+ ts = list(ts)
+ ts.insert(0, 0.0)
+ ts.append(1.0)
+ segments = []
+ ax, ay = a
+ bx, by = b
+ cx, cy = c
+ dx, dy = d
+ for i in range(len(ts) - 1):
+ t1 = ts[i]
+ t2 = ts[i+1]
+ delta = (t2 - t1)
+ # calc new a, b, c and d
+ a1x = ax * delta**3
+ a1y = ay * delta**3
+ b1x = (3*ax*t1 + bx) * delta**2
+ b1y = (3*ay*t1 + by) * delta**2
+ c1x = (2*bx*t1 + cx + 3*ax*t1**2) * delta
+ c1y = (2*by*t1 + cy + 3*ay*t1**2) * delta
+ d1x = ax*t1**3 + bx*t1**2 + cx*t1 + dx
+ d1y = ay*t1**3 + by*t1**2 + cy*t1 + dy
+ pt1, pt2, pt3, pt4 = calcCubicPoints((a1x, a1y), (b1x, b1y), (c1x, c1y), (d1x, d1y))
+ segments.append((pt1, pt2, pt3, pt4))
+ return segments
+
+
+#
+# Equation solvers.
+#
+
+from math import sqrt, acos, cos, pi
+
+
+def solveQuadratic(a, b, c,
+ sqrt=sqrt):
+ """Solve a quadratic equation where a, b and c are real.
+ a*x*x + b*x + c = 0
+ This function returns a list of roots. Note that the returned list
+ is neither guaranteed to be sorted nor to contain unique values!
+ """
+ if abs(a) < epsilon:
+ if abs(b) < epsilon:
+ # We have a non-equation; therefore, we have no valid solution
+ roots = []
+ else:
+ # We have a linear equation with 1 root.
+ roots = [-c/b]
+ else:
+ # We have a true quadratic equation. Apply the quadratic formula to find two roots.
+ DD = b*b - 4.0*a*c
+ if DD >= 0.0:
+ rDD = sqrt(DD)
+ roots = [(-b+rDD)/2.0/a, (-b-rDD)/2.0/a]
+ else:
+ # complex roots, ignore
+ roots = []
+ return roots
+
+
+def solveCubic(a, b, c, d,
+ abs=abs, pow=pow, sqrt=sqrt, cos=cos, acos=acos, pi=pi):
+ """Solve a cubic equation where a, b, c and d are real.
+ a*x*x*x + b*x*x + c*x + d = 0
+ This function returns a list of roots. Note that the returned list
+ is neither guaranteed to be sorted nor to contain unique values!
+ """
+ #
+ # adapted from:
+ # CUBIC.C - Solve a cubic polynomial
+ # public domain by Ross Cottrell
+ # found at: http://www.strangecreations.com/library/snippets/Cubic.C
+ #
+ if abs(a) < epsilon:
+ # don't just test for zero; for very small values of 'a' solveCubic()
+ # returns unreliable results, so we fall back to quad.
+ return solveQuadratic(b, c, d)
+ a = float(a)
+ a1 = b/a
+ a2 = c/a
+ a3 = d/a
+
+ Q = (a1*a1 - 3.0*a2)/9.0
+ R = (2.0*a1*a1*a1 - 9.0*a1*a2 + 27.0*a3)/54.0
+ R2_Q3 = R*R - Q*Q*Q
+
+ if R2_Q3 < 0:
+ theta = acos(R/sqrt(Q*Q*Q))
+ rQ2 = -2.0*sqrt(Q)
+ x0 = rQ2*cos(theta/3.0) - a1/3.0
+ x1 = rQ2*cos((theta+2.0*pi)/3.0) - a1/3.0
+ x2 = rQ2*cos((theta+4.0*pi)/3.0) - a1/3.0
+ return [x0, x1, x2]
+ else:
+ if Q == 0 and R == 0:
+ x = 0
+ else:
+ x = pow(sqrt(R2_Q3)+abs(R), 1/3.0)
+ x = x + Q/x
+ if R >= 0.0:
+ x = -x
+ x = x - a1/3.0
+ return [x]
+
+
+#
+# Conversion routines for points to parameters and vice versa
+#
+
+def calcQuadraticParameters(pt1, pt2, pt3):
+ x2, y2 = pt2
+ x3, y3 = pt3
+ cx, cy = pt1
+ bx = (x2 - cx) * 2.0
+ by = (y2 - cy) * 2.0
+ ax = x3 - cx - bx
+ ay = y3 - cy - by
+ return (ax, ay), (bx, by), (cx, cy)
+
+
+def calcCubicParameters(pt1, pt2, pt3, pt4):
+ x2, y2 = pt2
+ x3, y3 = pt3
+ x4, y4 = pt4
+ dx, dy = pt1
+ cx = (x2 -dx) * 3.0
+ cy = (y2 -dy) * 3.0
+ bx = (x3 - x2) * 3.0 - cx
+ by = (y3 - y2) * 3.0 - cy
+ ax = x4 - dx - cx - bx
+ ay = y4 - dy - cy - by
+ return (ax, ay), (bx, by), (cx, cy), (dx, dy)
+
+
+def calcQuadraticPoints(a, b, c):
+ ax, ay = a
+ bx, by = b
+ cx, cy = c
+ x1 = cx
+ y1 = cy
+ x2 = (bx * 0.5) + cx
+ y2 = (by * 0.5) + cy
+ x3 = ax + bx + cx
+ y3 = ay + by + cy
+ return (x1, y1), (x2, y2), (x3, y3)
+
+
+def calcCubicPoints(a, b, c, d):
+ ax, ay = a
+ bx, by = b
+ cx, cy = c
+ dx, dy = d
+ x1 = dx
+ y1 = dy
+ x2 = (cx / 3.0) + dx
+ y2 = (cy / 3.0) + dy
+ x3 = (bx + cx) / 3.0 + x2
+ y3 = (by + cy) / 3.0 + y2
+ x4 = ax + dx + cx + bx
+ y4 = ay + dy + cy + by
+ return (x1, y1), (x2, y2), (x3, y3), (x4, y4)
+
+
+def _segmentrepr(obj):
+ """
+ >>> _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)
+ except TypeError:
+ return str(obj)
+ else:
+ return "(%s)" % ", ".join([_segmentrepr(x) for x in it])
+
+
+def printSegments(segments):
+ """Helper for the doctests, displaying each segment in a list of
+ segments on a single line as a tuple.
+ """
+ for segment in segments:
+ print _segmentrepr(segment)
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
diff --git a/misc/pylib/robofab/misc/speedTestCase.py b/misc/pylib/robofab/misc/speedTestCase.py
new file mode 100644
index 000000000..e5003ac41
--- /dev/null
+++ b/misc/pylib/robofab/misc/speedTestCase.py
@@ -0,0 +1,99 @@
+"""
+
+ Speed comparison between the fontTools numpy based arrayTools and bezierTools,
+ and the pure python implementation in robofab.path.arrayTools and robofab.path.bezierTools
+
+"""
+
+import time
+
+from fontTools.misc import arrayTools
+from fontTools.misc import bezierTools
+
+import numpy
+
+import robofab.misc.arrayTools as noNumpyArrayTools
+import robofab.misc.bezierTools as noNumpyBezierTools
+
+################
+
+pt1 = (100, 100)
+pt2 = (200, 20)
+pt3 = (30, 580)
+pt4 = (153, 654)
+rect = [20, 20, 100, 100]
+
+## loops
+c = 10000
+
+print "(loop %s)"%c
+
+
+print "with numpy:"
+print "calcQuadraticParameters\t\t",
+n = time.time()
+for i in range(c):
+ bezierTools.calcQuadraticParameters(pt1, pt2, pt3)
+print time.time() - n
+
+print "calcBounds\t\t\t",
+n = time.time()
+for i in range(c):
+ arrayTools.calcBounds([pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3])
+print time.time() - n
+
+print "pointsInRect\t\t\t",
+n = time.time()
+for i in range(c):
+ arrayTools.pointsInRect([pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3, pt4], rect)
+print time.time() - n
+
+print "calcQuadraticBounds\t\t",
+n = time.time()
+for i in range(c):
+ bezierTools.calcQuadraticBounds(pt1, pt2, pt3)
+print time.time() - n
+
+print "calcCubicBounds\t\t\t",
+n = time.time()
+for i in range(c):
+ bezierTools.calcCubicBounds(pt1, pt2, pt3, pt4)
+print time.time() - n
+
+print
+##############
+
+print "no-numpy"
+print "calcQuadraticParameters\t\t",
+n = time.time()
+for i in range(c):
+ noNumpyBezierTools.calcQuadraticParameters(pt1, pt2, pt3)
+print time.time() - n
+
+print "calcBounds\t\t\t",
+n = time.time()
+for i in range(c):
+ noNumpyArrayTools.calcBounds([pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3])
+print time.time() - n
+
+print "pointsInRect\t\t\t",
+n = time.time()
+for i in range(c):
+ noNumpyArrayTools.pointsInRect([pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3, pt4], rect)
+print time.time() - n
+
+print "calcQuadraticBounds\t\t",
+n = time.time()
+for i in range(c):
+ noNumpyBezierTools.calcQuadraticBounds(pt1, pt2, pt3)
+print time.time() - n
+
+print "calcCubicBounds\t\t\t",
+n = time.time()
+for i in range(c):
+ noNumpyBezierTools.calcCubicBounds(pt1, pt2, pt3, pt4)
+print time.time() - n
+
+
+
+
diff --git a/misc/pylib/robofab/misc/test.py b/misc/pylib/robofab/misc/test.py
new file mode 100644
index 000000000..99a795c48
--- /dev/null
+++ b/misc/pylib/robofab/misc/test.py
@@ -0,0 +1,119 @@
+"""
+doc test requires fontTools to compare and defon to make the test font.
+"""
+
+import random
+from fontTools.pens.basePen import BasePen
+
+from fontTools.misc import arrayTools
+from fontTools.misc import bezierTools
+
+import robofab.misc.arrayTools as noNumpyArrayTools
+import robofab.misc.bezierTools as noNumpyBezierTools
+
+
+def drawMoveTo(pen, maxBox):
+ pen.moveTo((maxBox*random.random(), maxBox*random.random()))
+
+def drawLineTo(pen, maxBox):
+ pen.lineTo((maxBox*random.random(), maxBox*random.random()))
+
+def drawCurveTo(pen, maxBox):
+ pen.curveTo((maxBox*random.random(), maxBox*random.random()),
+ (maxBox*random.random(), maxBox*random.random()),
+ (maxBox*random.random(), maxBox*random.random()))
+
+def drawClosePath(pen):
+ pen.closePath()
+
+def createRandomFont():
+ from defcon import Font
+
+ maxGlyphs = 1000
+ maxContours = 10
+ maxSegments = 10
+ maxBox = 700
+ drawCallbacks = [drawLineTo, drawCurveTo]
+ f = Font()
+ for i in range(maxGlyphs):
+ name = "%s" %i
+ f.newGlyph(name)
+ g = f[name]
+ g.width = maxBox
+ pen = g.getPen()
+ for c in range(maxContours):
+ drawMoveTo(pen, maxBox)
+ for s in range(maxSegments):
+ random.choice(drawCallbacks)(pen, maxBox)
+ drawClosePath(pen)
+ return f
+
+class BoundsPen(BasePen):
+
+ def __init__(self, glyphSet, at, bt):
+ BasePen.__init__(self, glyphSet)
+ self.bounds = None
+ self._start = None
+ self._arrayTools = at
+ self._bezierTools = bt
+
+ def _moveTo(self, pt):
+ self._start = pt
+
+ def _addMoveTo(self):
+ if self._start is None:
+ return
+ bounds = self.bounds
+ if bounds:
+ self.bounds = self._arrayTools.updateBounds(bounds, self._start)
+ else:
+ x, y = self._start
+ self.bounds = (x, y, x, y)
+ self._start = None
+
+ def _lineTo(self, pt):
+ self._addMoveTo()
+ self.bounds = self._arrayTools.updateBounds(self.bounds, pt)
+
+ def _curveToOne(self, bcp1, bcp2, pt):
+ self._addMoveTo()
+ bounds = self.bounds
+ bounds = self._arrayTools.updateBounds(bounds, pt)
+ if not self._arrayTools.pointInRect(bcp1, bounds) or not self._arrayTools.pointInRect(bcp2, bounds):
+ bounds = self._arrayTools.unionRect(bounds, self._bezierTools.calcCubicBounds(
+ self._getCurrentPoint(), bcp1, bcp2, pt))
+ self.bounds = bounds
+
+ def _qCurveToOne(self, bcp, pt):
+ self._addMoveTo()
+ bounds = self.bounds
+ bounds = self._arrayTools.updateBounds(bounds, pt)
+ if not self._arrayTools.pointInRect(bcp, bounds):
+ bounds = self._arrayToolsunionRect(bounds, self._bezierTools.calcQuadraticBounds(
+ self._getCurrentPoint(), bcp, pt))
+ self.bounds = bounds
+
+
+
+def _testFont(font):
+ succes = True
+ for glyph in font:
+ fontToolsBoundsPen = BoundsPen(font, arrayTools, bezierTools)
+ glyph.draw(fontToolsBoundsPen)
+ noNumpyBoundsPen = BoundsPen(font, noNumpyArrayTools, noNumpyBezierTools)
+ glyph.draw(noNumpyBoundsPen)
+ if fontToolsBoundsPen.bounds != noNumpyBoundsPen.bounds:
+ succes = False
+ return succes
+
+
+def testCompareAgainstFontTools():
+ """
+ >>> font = createRandomFont()
+ >>> _testFont(font)
+ True
+ """
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod() \ No newline at end of file