From b30b511ee7554e45ce88b1ffd136cde856412cfd Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Sun, 27 Sep 2020 16:01:29 +0100 Subject: [PATCH 1/3] declare keep_starting_points as 'bint' type --- src/python/pathops/_pathops.pxd | 8 ++++---- src/python/pathops/_pathops.pyx | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/python/pathops/_pathops.pxd b/src/python/pathops/_pathops.pxd index 78aabfc..ad593bd 100644 --- a/src/python/pathops/_pathops.pxd +++ b/src/python/pathops/_pathops.pxd @@ -158,7 +158,7 @@ cdef class Path: cpdef reverse(self) - cpdef simplify(self, bint fix_winding=*, keep_starting_points=*) + cpdef simplify(self, bint fix_winding=*, bint keep_starting_points=*) cpdef convertConicsToQuads(self, float tolerance=*) @@ -307,12 +307,12 @@ cpdef Path op( Path one, Path two, SkPathOp operator, - fix_winding=*, - keep_starting_points=*, + bint fix_winding=*, + bint keep_starting_points=*, ) -cpdef Path simplify(Path path, fix_winding=*, keep_starting_points=*) +cpdef Path simplify(Path path, bint fix_winding=*, bint keep_starting_points=*) cdef class OpBuilder: diff --git a/src/python/pathops/_pathops.pyx b/src/python/pathops/_pathops.pyx index 74bf0df..7eb3fd8 100644 --- a/src/python/pathops/_pathops.pyx +++ b/src/python/pathops/_pathops.pyx @@ -345,7 +345,7 @@ cdef class Path: skpath.addPath(contour.path) self.path = skpath - cpdef simplify(self, bint fix_winding=True, keep_starting_points=True): + cpdef simplify(self, bint fix_winding=True, bint keep_starting_points=True): cdef list first_points if keep_starting_points: first_points = self.firstPoints @@ -626,10 +626,10 @@ cdef class Path: >>> affine = (2, 0, 0, 2, 0, 0) >>> p2 = p1.transform(*affine) >>> list(p2.segments) == [ - ('moveTo', ((2.0, 4.0),)), - ('lineTo', ((6.0, 8.0),)), - ('endPath', ()), - ] + ... ('moveTo', ((2.0, 4.0),)), + ... ('lineTo', ((6.0, 8.0),)), + ... ('endPath', ()), + ... ] True """ cdef SkMatrix matrix = SkMatrix.MakeAll( @@ -1374,8 +1374,8 @@ cpdef Path op( Path one, Path two, SkPathOp operator, - fix_winding=True, - keep_starting_points=True + bint fix_winding=True, + bint keep_starting_points=True ): cdef list first_points if keep_starting_points: @@ -1390,7 +1390,7 @@ cpdef Path op( return result -cpdef Path simplify(Path path, fix_winding=True, keep_starting_points=True): +cpdef Path simplify(Path path, bint fix_winding=True, bint keep_starting_points=True): cdef list first_points if keep_starting_points: first_points = path.firstPoints @@ -1406,7 +1406,7 @@ cpdef Path simplify(Path path, fix_winding=True, keep_starting_points=True): cdef class OpBuilder: - def __init__(self, bint fix_winding=True, keep_starting_points=True): + def __init__(self, bint fix_winding=True, bint keep_starting_points=True): self.fix_winding = fix_winding self.keep_starting_points = keep_starting_points self.first_points = [] From 41a136e92f87068ed0011d3819cec66dac95a9fb Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Sun, 27 Sep 2020 17:09:07 +0100 Subject: [PATCH 2/3] Add clockwise=False option to control outermost contour direction Fixes https://github.com/fonttools/skia-pathops/issues/30 --- src/python/pathops/_pathops.pxd | 18 +++++++++++--- src/python/pathops/_pathops.pyx | 41 ++++++++++++++++++++++---------- src/python/pathops/operations.py | 21 +++++++++++++--- 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/src/python/pathops/_pathops.pxd b/src/python/pathops/_pathops.pxd index ad593bd..b61daac 100644 --- a/src/python/pathops/_pathops.pxd +++ b/src/python/pathops/_pathops.pxd @@ -158,7 +158,12 @@ cdef class Path: cpdef reverse(self) - cpdef simplify(self, bint fix_winding=*, bint keep_starting_points=*) + cpdef simplify( + self, + bint fix_winding=*, + bint keep_starting_points=*, + bint clockwise=*, + ) cpdef convertConicsToQuads(self, float tolerance=*) @@ -279,7 +284,7 @@ cdef int path_is_inside(const SkPath& self, const SkPath& other) except -1 cpdef int restore_starting_points(Path path, list points) except -1 -cpdef bint winding_from_even_odd(Path path, bint truetype=*) except False +cpdef bint winding_from_even_odd(Path path, bint clockwise=*) except False cdef list _decompose_quadratic_segment(tuple points) @@ -309,10 +314,16 @@ cpdef Path op( SkPathOp operator, bint fix_winding=*, bint keep_starting_points=*, + bint clockwise=*, ) -cpdef Path simplify(Path path, bint fix_winding=*, bint keep_starting_points=*) +cpdef Path simplify( + Path path, + bint fix_winding=*, + bint keep_starting_points=*, + bint clockwise=*, +) cdef class OpBuilder: @@ -321,6 +332,7 @@ cdef class OpBuilder: cdef bint fix_winding cdef bint keep_starting_points cdef list first_points + cdef bint clockwise cpdef add(self, Path path, SkPathOp operator) diff --git a/src/python/pathops/_pathops.pyx b/src/python/pathops/_pathops.pyx index 7eb3fd8..fb65cca 100644 --- a/src/python/pathops/_pathops.pyx +++ b/src/python/pathops/_pathops.pyx @@ -345,14 +345,19 @@ cdef class Path: skpath.addPath(contour.path) self.path = skpath - cpdef simplify(self, bint fix_winding=True, bint keep_starting_points=True): + cpdef simplify( + self, + bint fix_winding=True, + bint keep_starting_points=True, + bint clockwise=False, + ): cdef list first_points if keep_starting_points: first_points = self.firstPoints if not Simplify(self.path, &self.path): raise PathOpsError("simplify operation did not succeed") if fix_winding: - winding_from_even_odd(self) + winding_from_even_odd(self, clockwise) if keep_starting_points: restore_starting_points(self, first_points) @@ -1116,23 +1121,23 @@ DEF DEBUG_WINDING = False @cython.wraparound(False) @cython.boundscheck(False) -cpdef bint winding_from_even_odd(Path path, bint truetype=False) except False: +cpdef bint winding_from_even_odd(Path path, bint clockwise=False) except False: """ Take a simplified path (without overlaps) and set the contours directions according to the non-zero winding fill type. The outermost contours are set to counter-clockwise direction, unless - 'truetype' is True. + 'clockwise' is True. """ # TODO re-enable this once the new feature is stabilized in upstream skia # https://github.com/fonttools/skia-pathops/issues/10 # if AsWinding(path.path, &path.path): - # if path.clockwise ^ truetype: + # if path.clockwise ^ clockwise: # path.reverse() # return True # # # in the unlikely event the built-in method fails, try our naive approach cdef int i, j - cdef bint inverse = not truetype + cdef bint inverse = not clockwise cdef bint is_clockwise, is_even cdef Path contour, other @@ -1375,7 +1380,8 @@ cpdef Path op( Path two, SkPathOp operator, bint fix_winding=True, - bint keep_starting_points=True + bint keep_starting_points=True, + bint clockwise=False, ): cdef list first_points if keep_starting_points: @@ -1384,13 +1390,18 @@ cpdef Path op( if not Op(one.path, two.path, operator, &result.path): raise PathOpsError("operation did not succeed") if fix_winding: - winding_from_even_odd(result) + winding_from_even_odd(result, clockwise) if keep_starting_points: restore_starting_points(result, first_points) return result -cpdef Path simplify(Path path, bint fix_winding=True, bint keep_starting_points=True): +cpdef Path simplify( + Path path, + bint fix_winding=True, + bint keep_starting_points=True, + bint clockwise=False, +): cdef list first_points if keep_starting_points: first_points = path.firstPoints @@ -1398,7 +1409,7 @@ cpdef Path simplify(Path path, bint fix_winding=True, bint keep_starting_points= if not Simplify(path.path, &result.path): raise PathOpsError("operation did not succeed") if fix_winding: - winding_from_even_odd(result) + winding_from_even_odd(result, clockwise) if keep_starting_points: restore_starting_points(result, first_points) return result @@ -1406,10 +1417,16 @@ cpdef Path simplify(Path path, bint fix_winding=True, bint keep_starting_points= cdef class OpBuilder: - def __init__(self, bint fix_winding=True, bint keep_starting_points=True): + def __init__( + self, + bint fix_winding=True, + bint keep_starting_points=True, + bint clockwise=False, + ): self.fix_winding = fix_winding self.keep_starting_points = keep_starting_points self.first_points = [] + self.clockwise = clockwise cpdef add(self, Path path, SkPathOp operator): self.builder.add(path.path, operator) @@ -1421,7 +1438,7 @@ cdef class OpBuilder: if not self.builder.resolve(&result.path): raise PathOpsError("operation did not succeed") if self.fix_winding: - winding_from_even_odd(result) + winding_from_even_odd(result, self.clockwise) if self.keep_starting_points: restore_starting_points(result, self.first_points) return result diff --git a/src/python/pathops/operations.py b/src/python/pathops/operations.py index 283657c..49ef0dc 100644 --- a/src/python/pathops/operations.py +++ b/src/python/pathops/operations.py @@ -19,13 +19,20 @@ def _draw(contours): return path -def union(contours, outpen, fix_winding=True, keep_starting_points=True): +def union( + contours, + outpen, + fix_winding=True, + keep_starting_points=True, + clockwise=False, +): if not contours: return path = _draw(contours) path.simplify( fix_winding=fix_winding, - keep_starting_points=keep_starting_points + keep_starting_points=keep_starting_points, + clockwise=clockwise, ) path.draw(outpen) @@ -37,10 +44,18 @@ def _do( outpen, fix_winding=True, keep_starting_points=True, + clockwise=False, ): one = _draw(subject_contours) two = _draw(clip_contours) - result = op(one, two, operator, fix_winding, keep_starting_points) + result = op( + one, + two, + operator, + fix_winding=fix_winding, + keep_starting_points=keep_starting_points, + clockwise=clockwise, + ) result.draw(outpen) From cbc63dd6071f078c17fb6c512199070ba7da7a8e Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Sun, 27 Sep 2020 17:09:45 +0100 Subject: [PATCH 3/3] add tests for simplify() with/without clockwise option --- tests/pathops_test.py | 60 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/pathops_test.py b/tests/pathops_test.py index 6c84709..0e0b0ad 100644 --- a/tests/pathops_test.py +++ b/tests/pathops_test.py @@ -10,6 +10,7 @@ float2bits, ArcSize, Direction, + simplify, ) import pytest @@ -790,3 +791,62 @@ def test_path_operation(message, operations, expected): round_pts.append(tuple(round(c, 2) for c in pt)) rounded.append((verb, tuple(round_pts))) assert tuple(rounded) == expected, message + + + +@pytest.fixture +def overlapping_path(): + path = Path() + path.moveTo(0, 0) + path.lineTo(10, 0) + path.lineTo(10, 10) + path.lineTo(0, 10) + path.close() + path.moveTo(5, 5) + path.lineTo(15, 5) + path.lineTo(15, 15) + path.lineTo(5, 15) + path.close() + return path + + +def test_simplify(overlapping_path): + result = simplify(overlapping_path) + + assert overlapping_path != result + assert list(result) == [ + (PathVerb.MOVE, ((0, 0),)), + (PathVerb.LINE, ((10, 0),)), + (PathVerb.LINE, ((10, 5),)), + (PathVerb.LINE, ((15, 5),)), + (PathVerb.LINE, ((15, 15),)), + (PathVerb.LINE, ((5, 15),)), + (PathVerb.LINE, ((5, 10),)), + (PathVerb.LINE, ((0, 10),)), + (PathVerb.CLOSE, ()), + ] + + overlapping_path.simplify() + + assert overlapping_path == result + + +def test_simplify_clockwise(overlapping_path): + result = simplify(overlapping_path, clockwise=True) + + assert overlapping_path != result + assert list(result) == [ + (PathVerb.MOVE, ((0, 0),)), + (PathVerb.LINE, ((0, 10),)), + (PathVerb.LINE, ((5, 10),)), + (PathVerb.LINE, ((5, 15),)), + (PathVerb.LINE, ((15, 15),)), + (PathVerb.LINE, ((15, 5),)), + (PathVerb.LINE, ((10, 5),)), + (PathVerb.LINE, ((10, 0),)), + (PathVerb.CLOSE, ()), + ] + + overlapping_path.simplify(clockwise=True) + + assert overlapping_path == result