Skip to content

Commit

Permalink
make PathPen curveTo/qCurveTo accept variable number of points
Browse files Browse the repository at this point in the history
When constructing a pathops.Path using the PathPen, we should handle variable number of point arguments to curveTo and qCurveTo, following fontTools' BasePen which is the blueprint of all segment-based pens.

https://github.com/fonttools/fonttools/blob/35856412485bce4a1e6c08cceb52c7a87c75c4ee/Lib/fontTools/pens/basePen.py#L274-L329

Also see unified-font-object/ufo-spec#211

and googlefonts/ufo2ft#468 (comment)
  • Loading branch information
anthrotype committed Nov 11, 2022
1 parent 703430e commit 81d4bb4
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 9 deletions.
1 change: 1 addition & 0 deletions src/python/pathops/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
PathOpsError,
UnsupportedVerbError,
OpenPathError,
NumberOfPointsError,
bits2float,
float2bits,
decompose_quadratic_segment,
Expand Down
2 changes: 1 addition & 1 deletion src/python/pathops/_pathops.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ cdef class PathPen:

cpdef lineTo(self, pt)

cpdef curveTo(self, pt1, pt2, pt3)
# def curveTo(self, *points)

# def qCurveTo(self, *points)

Expand Down
39 changes: 31 additions & 8 deletions src/python/pathops/_pathops.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ cdef class OpenPathError(PathOpsError):
pass


cdef class NumberOfPointsError(PathOpsError):
pass


# Helpers to convert to/from a float and its bit pattern

cdef inline int32_t _float2bits(float x):
Expand Down Expand Up @@ -891,16 +895,35 @@ cdef class PathPen:
cpdef lineTo(self, pt):
self.path.lineTo(pt[0], pt[1])

cpdef curveTo(self, pt1, pt2, pt3):
# support BasePen "super-beziers"? Nah.
self.path.cubicTo(
pt1[0], pt1[1],
pt2[0], pt2[1],
pt3[0], pt3[1])
def curveTo(self, *points):
num_offcurves = len(points) - 1
if num_offcurves == 2:
pt1, pt2, pt3 = points
self.path.cubicTo(
pt1[0], pt1[1],
pt2[0], pt2[1],
pt3[0], pt3[1])
elif num_offcurves == 1:
pt1, pt2 = points
self.path.quadTo(pt1[0], pt1[1], pt2[0], pt2[1])
elif num_offcurves == 0:
pt = points[0]
self.path.lineTo(pt[0], pt[1])
else:
# support BasePen "super-beziers"? Nah.
raise NumberOfPointsError(
"curveTo requires between 1 and 3 points; got %d" % len(points)
)

def qCurveTo(self, *points):
for pt1, pt2 in _decompose_quadratic_segment(points):
self._qCurveToOne(pt1, pt2)
num_offcurves = len(points) - 1
if num_offcurves > 0:
for pt1, pt2 in _decompose_quadratic_segment(points):
self._qCurveToOne(pt1, pt2)
elif num_offcurves == 0:
self.lineTo(points[0])
else:
raise NumberOfPointsError("qCurveTo requires at least 1 point; got 0")

cdef _qCurveToOne(self, pt1, pt2):
self.path.quadTo(pt1[0], pt1[1], pt2[0], pt2[1])
Expand Down
51 changes: 51 additions & 0 deletions tests/pathops_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
ArcSize,
Direction,
simplify,
NumberOfPointsError,
)

import pytest
Expand Down Expand Up @@ -106,6 +107,56 @@ def test_decompose_join_quadratic_segments(self):
('qCurveTo', ((1.0, 1.0), (2.0, 2.0), (3.0, 3.0))),
('closePath', ())]

def test_qCurveTo_varargs(self):
path = Path()
pen = path.getPen()
pen.moveTo((0, 0))
pen.qCurveTo((1, 1))
pen.closePath()

items = list(path)
assert len(items) == 3
# qcurve without offcurves is stored internally as a line
assert items[1][0] == PathVerb.LINE
assert items[1][1] == ((1.0, 1.0),)

assert list(path.segments) == [
('moveTo', ((0.0, 0.0),)),
('lineTo', ((1.0, 1.0),)),
('closePath', ()),
]

with pytest.raises(
NumberOfPointsError, match="qCurveTo requires at least 1 point; got 0"
):
pen.qCurveTo()

def test_curveTo_varargs(self):
path = Path()
pen = path.getPen()
pen.moveTo((0, 0))
pen.curveTo((1, 1), (2, 2), (3, 3)) # a cubic
pen.curveTo((4, 4), (5, 5)) # a quadratic
pen.curveTo((6, 6)) # a line
pen.closePath()

assert list(path.segments) == [
('moveTo', ((0.0, 0.0),)),
('curveTo', ((1.0, 1.0), (2.0, 2.0), (3.0, 3.0))),
('qCurveTo', ((4.0, 4.0), (5.0, 5.0))),
('lineTo', ((6.0, 6.0),)),
('closePath', ()),
]

with pytest.raises(
NumberOfPointsError, match="curveTo requires between 1 and 3 points; got 0"
):
pen.curveTo()
with pytest.raises(
NumberOfPointsError, match="curveTo requires between 1 and 3 points; got 4"
):
pen.curveTo((0, 0), (1, 1), (2, 2), (3, 3))

def test_last_implicit_lineTo(self):
# https://github.com/fonttools/skia-pathops/issues/6
path = Path()
Expand Down

0 comments on commit 81d4bb4

Please sign in to comment.