pax_global_header00006660000000000000000000000064132231406220014505gustar00rootroot0000000000000052 comment=fef272dfb6b4f0db056733b9240ab4db99cf50a8 booleanOperations-0.8.0/000077500000000000000000000000001322314062200151755ustar00rootroot00000000000000booleanOperations-0.8.0/.gitignore000066400000000000000000000001451322314062200171650ustar00rootroot00000000000000*.py[cod] dist build local MANIFEST Lib/booleanOperations.egg-info .eggs .DS_Store .tox/ .cache/ booleanOperations-0.8.0/.travis.yml000066400000000000000000000046571322314062200173220ustar00rootroot00000000000000sudo: false language: python python: - 2.7 - 3.4 - 3.5 install: - pip install --upgrade pip setuptools wheel - pip install tox-travis # Travis by default only clones a 'shallow' repository with --depth=50. # When building the distribution packages, we use git to determine the # package version string (via setuptools_scm), hence we need to fetch # the whole repo, and not just the last 50 commits. - if [[ $TRAVIS_PYTHON_VERSION == 3.5 ]]; then git fetch --unshallow; fi script: tox before_deploy: - python setup.py sdist bdist_wheel deploy: # deploy to Github Releases on tags - provider: releases api_key: secure: H2rz0E/GjRrRmvf6EYWv7Fyu+y4qU+InGiYlQ6xoyrjmMFCTYiPNW0ysu8893JFILusfO48zzEix7HcumQXtJU16XYwqft9eiWIGzSlP1krusmpQ0yTnuy9wEQcd7fXLEQDfiVZpJxWpC1TtsuIdDqpSUFcHDg4hlvn1IIh+6HwRP6+M5Mprze087ydBCKkO4CvqR4olC7bkSPorRtqUsiUHlUxYFS+R75a8ghqVQbky7ew9/r90zljCBFwYTPEv9tQ7OxnAQiUaAYojceThsBrNP1lH83tOoqB2FkKr9I5dxi5gqNuUsJHh3QK0V59oQCxU+iVFYfrlIZHTBQ+2caxIeWm2k9RkMXXkbg1LyfpORt+5L+NcFNE4WT0DTlYrKnpW+jrak0LEWaI97+15uHahph3vOJgaZrTFvRQZUJTWCNcji2jGbZg+O3pS1Vtd1xEgJ1TcrkOYLYBMrVOywbuQL4sXofLxH3I5RwAenkgm5TwxtuiNxpUkqlEhzJ+GYwWtfb7/qtcLIK7Xn6cDdsQG6XYQMVt+HpCKgRLlrQC8si6l+jZBEtJ7o4IKeYbtybFCe5H3V0jnRFRyncPPdpBavqJ093Pr7fGLvmZuHl551WRtyPlBN4aMGsqvqUSjwr/TxsX5zZ1tQ4QTN2YTmCdeUO6P/fwTBf5p0F3QFo8= file_glob: true file: "dist/*" skip_cleanup: true on: repo: typemytype/booleanOperations tags: true all_branches: true python: 3.5 # deploy to PyPI on tags - provider: pypi server: https://upload.pypi.org/legacy/ on: repo: typemytype/booleanOperations tags: true all_branches: true python: 3.5 user: anthrotype password: secure: B6rFOTA8ol1kt210q7USDVOzLFWUyMIwdZuaUdejYmUb0CXK6W8kKj0H01yBCgjkhtew9YEQSB62jFiGUOJH9L8JjpJO2RG8cHIIblXQfQ5DXogmcEtsAFotQz0o3UO/SYJaMY917XsPCqw3MTgksd2NWwO7H3YCjaWntN4zZxyTWowGr2Vhbj9s8e+MGMtrZ3Is91AcoXNI7HPj2oVhAtPP5GGpceIP7JlOGdHGUFJKuwJLRKVjEURWXN6ZOQNoxuG6KdkpGtJYCFgRCW65e48WpQs8lts/ByyOMyRItPIPI+K/cPOaIvZ7IUhu9SueBgKcW2YVzQrPzD07N5Lhb4LaKhqgMwpy7oWntnpO0/Xes7bonAoDYKPz7O3O3zl57fy1sVhGXZZNoGUfwyFz1PPecONavRpDQYCMEOq8BcENT86mf0xbAvtmTvBgos8NknmZ35ckUkyrye8t058CwW0yhEO2Pr36XPKoEP9EAiq2YRvINPYnI1T3jTPUUwnc3IAv4zQvPUKWqs2/hEVy8FTCz/0hBgDv2cs4pRcofTzfXSczGMfyuNS3i4n3CHc4FEOYP9MuEWGDC7KF4MVNsBWbh5pkISwhIwNbW1UDept2k+Dmb9XrSoCt5aSvEPJVT3YTA4qTGSUQvhzj1SvXi3s2lh+1WbibEJqNfpdobGg= distributions: sdist bdist_wheel booleanOperations-0.8.0/LICENSE000066400000000000000000000020731322314062200162040ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2013 Frederik Berlaen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. booleanOperations-0.8.0/Lib/000077500000000000000000000000001322314062200157035ustar00rootroot00000000000000booleanOperations-0.8.0/Lib/booleanOperations/000077500000000000000000000000001322314062200213665ustar00rootroot00000000000000booleanOperations-0.8.0/Lib/booleanOperations/__init__.py000066400000000000000000000007471322314062200235070ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import from .booleanOperationManager import BooleanOperationManager from .exceptions import BooleanOperationsError from .version import __version__ # export BooleanOperationManager static methods union = BooleanOperationManager.union difference = BooleanOperationManager.difference intersection = BooleanOperationManager.intersection xor = BooleanOperationManager.xor getIntersections = BooleanOperationManager.getIntersections booleanOperations-0.8.0/Lib/booleanOperations/booleanGlyph.py000066400000000000000000000165311322314062200243710ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import import weakref from copy import deepcopy try: from robofab.pens.pointPen import AbstractPointPen from robofab.pens.adapterPens import PointToSegmentPen, SegmentToPointPen from robofab.pens.boundsPen import BoundsPen except: from ufoLib.pointPen import ( AbstractPointPen, PointToSegmentPen, SegmentToPointPen) from fontTools.pens.boundsPen import BoundsPen from fontTools.pens.areaPen import AreaPen from .booleanOperationManager import BooleanOperationManager manager = BooleanOperationManager() class BooleanGlyphDataPointPen(AbstractPointPen): def __init__(self, glyph): self._glyph = glyph self._points = [] self.copyContourData = True def _flushContour(self): points = self._points if len(points) == 1 and points[0][0] == "move": # it's an anchor segmentType, pt, smooth, name = points[0] self._glyph.anchors.append((pt, name)) elif self.copyContourData: # ignore double points on start and end firstPoint = points[0] if firstPoint[0] == "move": # remove trailing off curves in an open path while points[-1][0] is None: points.pop() lastPoint = points[-1] if firstPoint[0] is not None and lastPoint[0] is not None: if firstPoint[1] == lastPoint[1]: if firstPoint[0] in ("line", "move"): del points[0] else: raise AssertionError("Unhandled point type sequence") elif firstPoint[0] == "move": # auto close the path _, pt, smooth, name = firstPoint points[0] = "line", pt, smooth, name contour = self._glyph.contourClass() contour._points = points self._glyph.contours.append(contour) def beginPath(self, identifier=None): self._points = [] def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): self._points.append((segmentType, pt, smooth, name)) def endPath(self): self._flushContour() def addComponent(self, baseGlyphName, transformation): self._glyph.components.append((baseGlyphName, transformation)) class BooleanContour(object): """ Contour like object. """ def __init__(self): self._points = [] self._clockwise = None self._bounds = None def __len__(self): return len(self._points) # shallow contour API def draw(self, pen): pointPen = PointToSegmentPen(pen) self.drawPoints(pointPen) def drawPoints(self, pointPen): pointPen.beginPath() for segmentType, pt, smooth, name in self._points: pointPen.addPoint(pt=pt, segmentType=segmentType, smooth=smooth, name=name) pointPen.endPath() def _get_clockwise(self): if self._clockwise is None: pen = AreaPen() pen.endPath = pen.closePath self.draw(pen) self._clockwise = pen.value < 0 return self._clockwise clockwise = property(_get_clockwise) def _get_bounds(self): if self._bounds is None: pen = BoundsPen(None) self.draw(pen) self._bounds = pen.bounds return self._bounds bounds = property(_get_bounds) class BooleanGlyph(object): """ Glyph like object handling boolean operations. union: result = BooleanGlyph(glyph).union(BooleanGlyph(glyph2)) result = BooleanGlyph(glyph) | BooleanGlyph(glyph2) difference: result = BooleanGlyph(glyph).difference(BooleanGlyph(glyph2)) result = BooleanGlyph(glyph) % BooleanGlyph(glyph2) intersection: result = BooleanGlyph(glyph).intersection(BooleanGlyph(glyph2)) result = BooleanGlyph(glyph) & BooleanGlyph(glyph2) xor: result = BooleanGlyph(glyph).xor(BooleanGlyph(glyph2)) result = BooleanGlyph(glyph) ^ BooleanGlyph(glyph2) """ contourClass = BooleanContour def __init__(self, glyph=None, copyContourData=True): self.contours = [] self.components = [] self.anchors = [] self.name = None self.unicodes = None self.width = None self.lib = {} self.note = None if glyph: pen = self.getPointPen() pen.copyContourData = copyContourData glyph.drawPoints(pen) self.name = glyph.name self.unicodes = glyph.unicodes self.width = glyph.width self.lib = deepcopy(glyph.lib) self.note = glyph.note if not isinstance(glyph, self.__class__): self.getSourceGlyph = weakref.ref(glyph) def __repr__(self): return "" % self.name def __len__(self): return len(self.contours) def __getitem__(self, index): return self.contours[index] def getSourceGlyph(self): return None def getParent(self): source = self.getSourceGlyph() if source: return source.getParent() return None # shalllow glyph API def draw(self, pen): pointPen = PointToSegmentPen(pen) self.drawPoints(pointPen) def drawPoints(self, pointPen): for contour in self.contours: contour.drawPoints(pointPen) for baseName, transformation in self.components: pointPen.addComponent(baseName, transformation) for pt, name in self.anchors: pointPen.beginPath() pointPen.addPoint(pt=pt, segmentType="move", smooth=False, name=name) pointPen.endPath() def getPen(self): return SegmentToPointPen(self.getPointPen()) def getPointPen(self): return BooleanGlyphDataPointPen(self) # boolean operations def _booleanMath(self, operation, other): if not isinstance(other, self.__class__): other = self.__class__(other) destination = self.__class__(self, copyContourData=False) func = getattr(manager, operation) if operation == "union": contours = self.contours if other is not None: contours += other.contours func(contours, destination.getPointPen()) else: subjectContours = self.contours clipContours = other.contours func(subjectContours, clipContours, destination.getPointPen()) return destination def __or__(self, other): return self.union(other) __ror__ = __ior__ = __or__ def __mod__(self, other): return self.difference(other) __rmod__ = __imod__ = __mod__ def __and__(self, other): return self.intersection(other) __rand__ = __iand__ = __and__ def __xor__(self, other): return self.xor(other) __rxor__ = __ixor__ = __xor__ def union(self, other): return self._booleanMath("union", other) def difference(self, other): return self._booleanMath("difference", other) def intersection(self, other): return self._booleanMath("intersection", other) def xor(self, other): return self._booleanMath("xor", other) def removeOverlap(self): return self._booleanMath("union", None) booleanOperations-0.8.0/Lib/booleanOperations/booleanOperationManager.py000066400000000000000000000123731322314062200265410ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import from .flatten import InputContour, OutputContour from .exceptions import ( InvalidSubjectContourError, InvalidClippingContourError, ExecutionError) import pyclipper """ General Suggestions: - Contours should only be sent here if they actually overlap. This can be checked easily using contour bounds. - Only perform operations on closed contours. - contours must have an on curve point - some kind of a log """ _operationMap = { "union": pyclipper.CT_UNION, "intersection": pyclipper.CT_INTERSECTION, "difference": pyclipper.CT_DIFFERENCE, "xor": pyclipper.CT_XOR, } _fillTypeMap = { "evenOdd": pyclipper.PFT_EVENODD, "nonZero": pyclipper.PFT_NONZERO, # we keep the misspelling for compatibility with earlier versions "noneZero": pyclipper.PFT_NONZERO, } def clipExecute(subjectContours, clipContours, operation, subjectFillType="nonZero", clipFillType="nonZero"): pc = pyclipper.Pyclipper() for i, subjectContour in enumerate(subjectContours): # ignore paths with no area if pyclipper.Area(subjectContour): try: pc.AddPath(subjectContour, pyclipper.PT_SUBJECT) except pyclipper.ClipperException: raise InvalidSubjectContourError("contour %d is invalid for clipping" % i) for j, clipContour in enumerate(clipContours): # ignore paths with no area if pyclipper.Area(clipContour): try: pc.AddPath(clipContour, pyclipper.PT_CLIP) except pyclipper.ClipperException: raise InvalidClippingContourError("contour %d is invalid for clipping" % j) try: solution = pc.Execute(_operationMap[operation], _fillTypeMap[subjectFillType], _fillTypeMap[clipFillType]) except pyclipper.ClipperException as exc: raise ExecutionError(exc) return [[tuple(p) for p in path] for path in solution] def _performOperation(operation, subjectContours, clipContours, outPen): # prep the contours subjectInputContours = [InputContour(contour) for contour in subjectContours if contour and len(contour) > 1] clipInputContours = [InputContour(contour) for contour in clipContours if contour and len(contour) > 1] inputContours = subjectInputContours + clipInputContours resultContours = clipExecute([subjectInputContour.originalFlat for subjectInputContour in subjectInputContours], [clipInputContour.originalFlat for clipInputContour in clipInputContours], operation, subjectFillType="nonZero", clipFillType="nonZero") # convert to output contours outputContours = [OutputContour(contour) for contour in resultContours] # re-curve entire contour for inputContour in inputContours: for outputContour in outputContours: if outputContour.final: continue if outputContour.reCurveFromEntireInputContour(inputContour): # the input is expired if a match was made, # so stop passing it to the outputs break # re-curve segments for inputContour in inputContours: # skip contours that were comppletely used in the previous step if inputContour.used: continue # XXX this could be expensive if an input becomes completely used # it doesn't stop from being passed to the output for outputContour in outputContours: outputContour.reCurveFromInputContourSegments(inputContour) # curve fit for outputContour in outputContours: outputContour.reCurveSubSegments(inputContours) # output the results for outputContour in outputContours: outputContour.drawPoints(outPen) return outputContours class BooleanOperationManager(object): @staticmethod def union(contours, outPen): return _performOperation("union", contours, [], outPen) @staticmethod def difference(subjectContours, clipContours, outPen): return _performOperation("difference", subjectContours, clipContours, outPen) @staticmethod def intersection(subjectContours, clipContours, outPen): return _performOperation("intersection", subjectContours, clipContours, outPen) @staticmethod def xor(subjectContours, clipContours, outPen): return _performOperation("xor", subjectContours, clipContours, outPen) @staticmethod def getIntersections(contours): from .flatten import _scalePoints, inverseClipperScale # prep the contours inputContours = [InputContour(contour) for contour in contours if contour and len(contour) > 1] inputFlatPoints = set() for contour in inputContours: inputFlatPoints.update(contour.originalFlat) resultContours = clipExecute( [inputContour.originalFlat for inputContour in inputContours], [], "union", subjectFillType="nonZero", clipFillType="nonZero") resultFlatPoints = set() for contour in resultContours: resultFlatPoints.update(contour) intersections = resultFlatPoints - inputFlatPoints return _scalePoints(intersections, inverseClipperScale) booleanOperations-0.8.0/Lib/booleanOperations/exceptions.py000066400000000000000000000012421322314062200241200ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import class BooleanOperationsError(Exception): """Base BooleanOperations exception""" class InvalidContourError(BooleanOperationsError): """Raised when any input contour is invalid""" class InvalidSubjectContourError(InvalidContourError): """Raised when a 'subject' contour is not valid""" class InvalidClippingContourError(InvalidContourError): """Raised when a 'clipping' contour is not valid""" class OpenContourError(BooleanOperationsError): """Raised when any input contour is open""" class ExecutionError(BooleanOperationsError): """Raised when clipping execution fails""" booleanOperations-0.8.0/Lib/booleanOperations/flatten.py000066400000000000000000001452471322314062200234120ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import import math from fontTools.misc import bezierTools from fontTools.pens.basePen import decomposeQuadraticSegment import pyclipper from .exceptions import OpenContourError """ To Do: - the stuff listed below - need to know what kind of curves should be used for curve fit--curve or qcurve - false curves and duplicate points need to be filtered early on notes: - the flattened segments *must* be cyclical. if they aren't, matching is almost impossible. optimization ideas: - the flattening of the output segment in the full contour matching is probably expensive. - there should be a way to flag an input contour as entirely used so that it isn't tried and tried and tried for segment matches. - do a faster test when matching segments: when a end match is found, jump back input length and grab the output segment. test for match with the input. - cache input contour objects. matching these to incoming will be a little difficult because of point names and identifiers. alternatively, deal with those after the fact. - some tests on input before conversion to input objects could yield significant speedups. would need to check each contour for self intersection and each non-self-intersectingcontour for collision with other contours. and contours that don't have a hit could be skipped. this cound be done roughly with bounds. this should probably be done by extenal callers. - set a proper starting points of the output segments based on known points known points are: input oncurve points if nothing found intersection points (only use this is in the final curve fitting stage) test cases: - untouched contour: make clockwise and counter-clockwise tests of the same contour """ epsilon = 1e-8 # factors for transferring coordinates to and from Clipper clipperScale = 1 << 17 inverseClipperScale = 1.0 / clipperScale # approximateSegmentLength setting _approximateSegmentLength = 5.3 # ------------- # Input Objects # ------------- # Input class InputContour(object): def __init__(self, contour): # gather the point data pointPen = ContourPointDataPen() contour.drawPoints(pointPen) points = pointPen.getData() reversedPoints = _reversePoints(points) # gather segments self.segments = _convertPointsToSegments(points) # only calculate once all the flat points. # it seems to have some tiny difference and its a lot faster # if the flat points are calculated from the reversed input points. self.reversedSegments = _convertPointsToSegments(reversedPoints, willBeReversed=True) # simple reverse the flat points and store them in the reversedSegments index = 0 for segment in self.segments: otherSegment = self.reversedSegments[index] otherSegment.flat = segment.getReversedFlatPoints() index -= 1 # get the direction; returns True if counter-clockwise, False otherwise self.clockwise = not pyclipper.Orientation(points) # store the gathered data if self.clockwise: self.clockwiseSegments = self.segments self.counterClockwiseSegments = self.reversedSegments else: self.clockwiseSegments = self.reversedSegments self.counterClockwiseSegments = self.segments # flag indicating if the contour has been used self.used = False # ---------- # Attributes # ---------- # the original direction in flat segments def _get_originalFlat(self): if self.clockwise: return self.clockwiseFlat else: return self.counterClockwiseFlat originalFlat = property(_get_originalFlat) # the clockwise direction in flat segments def _get_clockwiseFlat(self): flat = [] segments = self.clockwiseSegments for segment in segments: flat.extend(segment.flat) return flat clockwiseFlat = property(_get_clockwiseFlat) # the counter-clockwise direction in flat segments def _get_counterClockwiseFlat(self): flat = [] segments = self.counterClockwiseSegments for segment in segments: flat.extend(segment.flat) return flat counterClockwiseFlat = property(_get_counterClockwiseFlat) def hasOnCurve(self): for inputSegment in self.segments: if not inputSegment.used and inputSegment.segmentType != "line": return True return False class InputSegment(object): # __slots__ = ["points", "previousOnCurve", "scaledPreviousOnCurve", "flat", "used"] def __init__(self, points=None, previousOnCurve=None, willBeReversed=False): if points is None: points = [] self.points = points self.previousOnCurve = previousOnCurve self.scaledPreviousOnCurve = _scaleSinglePoint(previousOnCurve, scale=clipperScale) self.used = False self.flat = [] # if the bcps are equal to the oncurves convert the segment to a line segment. # otherwise this causes an error when flattening. if self.segmentType == "curve": if previousOnCurve == points[0].coordinates and points[1].coordinates == points[-1].coordinates: oncurve = points[-1] oncurve.segmentType = "line" self.points = points = [oncurve] elif previousOnCurve[0] == points[0].coordinates[0] == points[1].coordinates[0] == points[-1].coordinates[0]: oncurve = points[-1] oncurve.segmentType = "line" self.points = points = [oncurve] elif previousOnCurve[1] == points[0].coordinates[1] == points[1].coordinates[1] == points[-1].coordinates[1]: oncurve = points[-1] oncurve.segmentType = "line" self.points = points = [oncurve] # its a reversed segment the flat points will be set later on in the InputContour if willBeReversed: return pointsToFlatten = [] if self.segmentType == "qcurve": assert len(points) >= 0 flat = [] currentOnCurve = previousOnCurve pointCoordinates = [point.coordinates for point in points] for pt1, pt2 in decomposeQuadraticSegment(pointCoordinates[1:]): pt0x, pt0y = currentOnCurve pt1x, pt1y = pt1 pt2x, pt2y = pt2 mid1x = pt0x + 0.66666666666666667 * (pt1x - pt0x) mid1y = pt0y + 0.66666666666666667 * (pt1y - pt0y) mid2x = pt2x + 0.66666666666666667 * (pt1x - pt2x) mid2y = pt2y + 0.66666666666666667 * (pt1y - pt2y) convertedQuadPointToFlatten = [currentOnCurve, (mid1x, mid1y), (mid2x, mid2y), pt2] flat.extend(_flattenSegment(convertedQuadPointToFlatten)) currentOnCurve = pt2 self.flat = flat # this shoudl be easy. # copy the quad to cubic from fontTools.pens.basePen elif self.segmentType == "curve": pointsToFlatten = [previousOnCurve] + [point.coordinates for point in points] else: assert len(points) == 1 self.flat = [point.coordinates for point in points] if pointsToFlatten: self.flat = _flattenSegment(pointsToFlatten) # if len(self.flat) == 1 and self.segmentType == "curve": # oncurve = self.points[-1] # oncurve.segmentType = "line" # self.points = [oncurve] self.flat = _scalePoints(self.flat, scale=clipperScale) self.flat = _checkFlatPoints(self.flat) self.used = False def _get_segmentType(self): return self.points[-1].segmentType segmentType = property(_get_segmentType) def getReversedFlatPoints(self): reversedFlatPoints = [self.scaledPreviousOnCurve] + self.flat[:-1] reversedFlatPoints.reverse() return reversedFlatPoints def split(self, tValues): """ Split the segment according the t values """ if self.segmentType == "curve": on1 = self.previousOnCurve off1 = self.points[0].coordinates off2 = self.points[1].coordinates on2 = self.points[2].coordinates return bezierTools.splitCubicAtT(on1, off1, off2, on2, *tValues) elif self.segmentType == "line": segments = [] x1, y1 = self.previousOnCurve x2, y2 = self.points[0].coordinates dx = x2 - x1 dy = y2 - y1 pp = x1, y1 for t in tValues: np = (x1+dx*t, y1+dy*t) segments.append([pp, np]) pp = np segments.append([pp, (x2, y2)]) return segments elif self.segmentType == "qcurve": raise NotImplementedError else: raise NotImplementedError def tValueForPoint(self, point): """ get a t values for a given point required: the point must be a point on the curve. in an overlap cause the point will be an intersection points wich is alwasy a point on the curve """ if self.segmentType == "curve": on1 = self.previousOnCurve off1 = self.points[0].coordinates off2 = self.points[1].coordinates on2 = self.points[2].coordinates return _tValueForPointOnCubicCurve(point, (on1, off1, off2, on2)) elif self.segmentType == "line": return _tValueForPointOnLine(point, (self.previousOnCurve, self.points[0].coordinates)) elif self.segmentType == "qcurve": raise NotImplementedError else: raise NotImplementedError class InputPoint(object): __slots__ = ["coordinates", "segmentType", "smooth", "name", "kwargs"] def __init__(self, coordinates, segmentType=None, smooth=False, name=None, kwargs=None): x, y = coordinates self.coordinates = coordinates self.segmentType = segmentType self.smooth = smooth self.name = name self.kwargs = kwargs def __getitem__(self, i): return self.coordinates[i] def copy(self): copy = self.__class__( coordinates=self.coordinates, segmentType=self.segmentType, smooth=self.smooth, name=self.name, kwargs=self.kwargs ) return copy def __str__(self): return "%s %s" % (self.segmentType, self.coordinates) def __repr__(self): return self.__str__() # ------------- # Input Support # ------------- class ContourPointDataPen: """ Point pen for gathering raw contour data. An instance of this pen may only be used for one contour. """ def __init__(self): self._points = None self._foundStartingPoint = False def getData(self): """ Return a list of normalized InputPoint objects for the contour drawn with this pen. """ # organize the points into segments # 1. make sure there is an on curve haveOnCurve = False for point in self._points: if point.segmentType is not None: haveOnCurve = True break # 2. move the off curves to front of the list if haveOnCurve: _prepPointsForSegments(self._points) # 3. ignore double points on start and end firstPoint = self._points[0] lastPoint = self._points[-1] if firstPoint.segmentType is not None and lastPoint.segmentType is not None: if firstPoint.coordinates == lastPoint.coordinates: if (firstPoint.segmentType in ["line", "move"]): del self._points[0] else: raise AssertionError("Unhandled point type sequence") # done return self._points def beginPath(self, **kwargs): # TODO store and pass on the contour identifier? assert self._points is None self._points = [] def endPath(self): pass def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): if segmentType == "move": raise OpenContourError("Unhandled open contour") if not self._foundStartingPoint and segmentType is not None: kwargs['startingPoint'] = self._foundStartingPoint = True data = InputPoint( coordinates=pt, segmentType=segmentType, smooth=smooth, name=name, kwargs=kwargs ) self._points.append(data) def addComponent(self, baseGlyphName, transformation): raise NotImplementedError def _prepPointsForSegments(points): """ Move any off curves at the end of the contour to the beginning of the contour. This makes segmentation easier. """ while 1: point = points[-1] if point.segmentType: break else: point = points.pop() points.insert(0, point) continue break def _copyPoints(points): """ Make a shallow copy of the points. """ copied = [point.copy() for point in points] return copied def _reversePoints(points): """ Reverse the points. This differs from the reversal point pen in RoboFab in that it doesn't worry about maintaing the start point position. That has no benefit within the context of this module. """ # copy the points points = _copyPoints(points) # find the first on curve type and recycle # it for the last on curve type firstOnCurve = None for index, point in enumerate(points): if point.segmentType is not None: firstOnCurve = index break lastSegmentType = points[firstOnCurve].segmentType # reverse the points points = reversed(points) # work through the reversed remaining points final = [] for point in points: segmentType = point.segmentType if segmentType is not None: point.segmentType = lastSegmentType lastSegmentType = segmentType final.append(point) # move any offcurves at the end of the points # to the start of the points _prepPointsForSegments(final) # done return final def _convertPointsToSegments(points, willBeReversed=False): """ Compile points into InputSegment objects. """ # get the last on curve previousOnCurve = None for point in reversed(points): if point.segmentType is not None: previousOnCurve = point.coordinates break assert previousOnCurve is not None # gather the segments offCurves = [] segments = [] for point in points: # off curve, hold. if point.segmentType is None: offCurves.append(point) else: segment = InputSegment( points=offCurves + [point], previousOnCurve=previousOnCurve, willBeReversed=willBeReversed ) segments.append(segment) offCurves = [] previousOnCurve = point.coordinates assert not offCurves return segments # -------------- # Output Objects # -------------- class OutputContour(object): def __init__(self, pointList): if pointList[0] == pointList[-1]: del pointList[-1] self.clockwise = not pyclipper.Orientation(pointList) self.segments = [ OutputSegment( segmentType="flat", points=[point] ) for point in pointList ] def _scalePoint(self, point): x, y = point x = x * inverseClipperScale if int(x) == x: x = int(x) y = y * inverseClipperScale if int(y) == y: y = int(y) return x, y # ---------- # Attributes # ---------- def _get_final(self): # XXX this could be optimized: # store a fixed value after teh contour is finalized # don't do the dymanic searching if that flag is set to True for segment in self.segments: if not segment.final: return False return True final = property(_get_final) # -------------------------- # Re-Curve and Curve Fitting # -------------------------- def reCurveFromEntireInputContour(self, inputContour): """ Match if entire input contour matches entire output contour, allowing for different start point. """ if self.clockwise: inputFlat = inputContour.clockwiseFlat else: inputFlat = inputContour.counterClockwiseFlat outputFlat = [] for segment in self.segments: # XXX this could be expensive assert segment.segmentType == "flat" outputFlat += segment.points # test lengths haveMatch = False if len(inputFlat) == len(outputFlat): if inputFlat == outputFlat: haveMatch = True else: inputStart = inputFlat[0] if inputStart in outputFlat: # there should be only one occurance of the point # but handle it just in case if outputFlat.count(inputStart) > 1: startIndexes = [index for index, point in enumerate(outputFlat) if point == inputStart] else: startIndexes = [outputFlat.index(inputStart)] # slice and dice to test possible orders for startIndex in startIndexes: test = outputFlat[startIndex:] + outputFlat[:startIndex] if inputFlat == test: haveMatch = True break if haveMatch: # clear out the flat points self.segments = [] # replace with the appropriate points from the input if self.clockwise: inputSegments = inputContour.clockwiseSegments else: inputSegments = inputContour.counterClockwiseSegments for inputSegment in inputSegments: self.segments.append( OutputSegment( segmentType=inputSegment.segmentType, points=[ OutputPoint( coordinates=point.coordinates, segmentType=point.segmentType, smooth=point.smooth, name=point.name, kwargs=point.kwargs ) for point in inputSegment.points ], final=True ) ) inputSegment.used = True # reset the direction of the final contour self.clockwise = inputContour.clockwise return True return False def reCurveFromInputContourSegments(self, inputContour): return # match individual segments if self.clockwise: inputSegments = inputContour.clockwiseSegments else: inputSegments = inputContour.counterClockwiseSegments for inputSegment in inputSegments: # skip used if inputSegment.used: continue # skip if the input contains more points than the entire output contour if len(inputSegment.flat) > len(self.segments): continue # skip if the input end is not in the contour inputSegmentLastPoint = inputSegment.flat[-1] outputFlat = [segment.points[-1] for segment in self.segments] if inputSegmentLastPoint not in outputFlat: continue # work through all output segments for outputSegmentIndex, outputSegment in enumerate(self.segments): # skip finalized if outputSegment.final: continue # skip if the output point doesn't match the input end if outputSegment.points[-1] != inputSegmentLastPoint: continue # make a set of ranges for slicing the output into a testable list of points inputLength = len(inputSegment.flat) outputRanges = [] outputSegmentIndex += 1 if outputSegmentIndex - inputLength < 0: r1 = (len(self.segments) + outputSegmentIndex - inputLength, len(self.segments)) outputRanges.append(r1) r2 = (0, outputSegmentIndex) outputRanges.append(r2) else: outputRanges.append((outputSegmentIndex - inputLength, outputSegmentIndex)) # gather the output segments testableOutputSegments = [] for start, end in outputRanges: testableOutputSegments += self.segments[start:end] # create a list of points test = [] for s in testableOutputSegments: # stop if a segment is final if s.final: test = None break test.append(s.points[-1]) if test == inputSegment.flat and inputSegment.segmentType != "line": # insert new segment newSegment = OutputSegment( segmentType=inputSegment.segmentType, points=[ OutputPoint( coordinates=point.coordinates, segmentType=point.segmentType, smooth=point.smooth, name=point.name, kwargs=point.kwargs ) for point in inputSegment.points ], final=True ) self.segments.insert(outputSegmentIndex, newSegment) # remove old segments # XXX this is sloppy for start, end in outputRanges: if start > outputSegmentIndex: start += 1 end += 1 del self.segments[start:end] # flag the original as used inputSegment.used = True break # ? match line start points (to prevent curve fit in shortened line) return False def reCurveSubSegmentsCheckInputContoursOnHasCurve(self, inputContours): # test is the remaining input contours contains only lineTo points # XXX could be cached return True # for inputContour in inputContours: # if inputContour.used: # continue # if inputContour.hasOnCurve(): # return True # return False def reCurveSubSegments(self, inputContours): if not self.segments: # its all done return # the inputContours has some curved segments # if not it all the segments will be converted at the end if self.reCurveSubSegmentsCheckInputContoursOnHasCurve(inputContours): # collect all flat points in a dict of unused inputContours # collect both clockwise segment and counterClockwise segments # it happens a lot that the directions turns around # the clockwise attribute can help but testing the directions is always needed # collect all oncurve points as well flatInputPointsSegmentDict = dict() reversedFlatInputPointsSegmentDict = dict() flatIntputOncurves = set() for inputContour in inputContours: if inputContour.used: continue if self.clockwise: inputSegments = inputContour.clockwiseSegments reversedSegments = inputContour.counterClockwiseSegments else: inputSegments = inputContour.counterClockwiseSegments reversedSegments = inputContour.clockwiseSegments for inputSegment in inputSegments: if inputSegment.used: continue for p in inputSegment.flat: flatInputPointsSegmentDict[p] = inputSegment flatIntputOncurves.add(inputSegment.scaledPreviousOnCurve) for inputSegment in reversedSegments: if inputSegment.used: continue for p in inputSegment.flat: reversedFlatInputPointsSegmentDict[p] = inputSegment flatIntputOncurves.add(inputSegment.scaledPreviousOnCurve) flatInputPoints = set(flatInputPointsSegmentDict.keys()) # reset the starting point to a known point. # not somewhere in the middle of a flatten point list firstSegment = self.segments[0] foundStartingPoint = True if firstSegment.segmentType == "flat": foundStartingPoint = False for index, segment in enumerate(self.segments): if segment.segmentType in ["line", "curve", "qcurve"]: foundStartingPoint = True break if foundStartingPoint: # if found re index the segments # if there is no known starting point found do it later based on the intersection points self.segments = self.segments[index:] + self.segments[:index] # collect all flat points in a intersect segment remainingSubSegment = OutputSegment(segmentType="intersect", points=[]) # store all segments in one big temp list newSegments = [] for index, segment in enumerate(self.segments): if segment.segmentType != "flat": # when the segment contains only one points its a line cause it is a single intersection point if len(remainingSubSegment.points) == 1: remainingSubSegment.segmentType = "line" remainingSubSegment.final = True remainingSubSegment.points = [ OutputPoint( coordinates=self._scalePoint(point), segmentType="line", smooth=point.smooth, name=point.name, kwargs=point.kwargs ) for point in remainingSubSegment.points ] newSegments.append(remainingSubSegment) remainingSubSegment = OutputSegment(segmentType="intersect", points=[]) newSegments.append(segment) continue remainingSubSegment.points.extend(segment.points) newSegments.append(remainingSubSegment) # loop over all segments for segment in newSegments: # handle only segments tagged as intersect if segment.segmentType != "intersect": continue # skip empty segments if not segment.points: continue # get al inputSegments, this is an unorderd list of all points no in the the flatInputPoints segmentPointsSet = set(segment.points) intersectionPoints = segmentPointsSet - flatInputPoints # merge both oncurves and intersectionPoints as known points possibleStartingPoints = flatIntputOncurves | intersectionPoints hasOncurvePoints = segmentPointsSet & flatIntputOncurves # if not starting point is found earlier do it here foundStartingPointIndex = None if not foundStartingPoint: for index, p in enumerate(segment.points): if p in flatIntputOncurves: foundStartingPointIndex = index break if foundStartingPointIndex is None: for index, p in enumerate(segment.points): if p in intersectionPoints: foundStartingPointIndex = index break segment.points = segment.points[foundStartingPointIndex:] + segment.points[:foundStartingPointIndex] # split list based on oncurvepoints and intersection points, aka possibleStartingPoints. segmentedFlatPoints = [[]] for p in segment.points: segmentedFlatPoints[-1].append(p) if p in possibleStartingPoints: segmentedFlatPoints.append([]) if not segmentedFlatPoints[-1]: segmentedFlatPoints.pop(-1) if len(segmentedFlatPoints) > 1 and len(segmentedFlatPoints[0]) == 1: # if last segment is a curve, the start point may be last point on the last segment. If so, merge them. # check if they both have the same inputSegment or reversedInputSegment fp = segmentedFlatPoints[0][0] lp = segmentedFlatPoints[-1][-1] mergeFirstSegments = False if fp in flatInputPoints and lp in flatInputPoints: firstInputSegment = flatInputPointsSegmentDict[fp] lastInputSegment = flatInputPointsSegmentDict[lp] reversedFirstInputSegment = reversedFlatInputPointsSegmentDict[fp] reversedLastInputSegment = reversedFlatInputPointsSegmentDict[lp] if (firstInputSegment.segmentType == reversedFirstInputSegment.segmentType == "curve") or (lastInputSegment.segmentType == reversedLastInputSegment.segmentType == "curve"): if firstInputSegment == lastInputSegment or reversedFirstInputSegment == reversedLastInputSegment: mergeFirstSegments = True # elif len(firstInputSegment.points) > 1 and len(lastInputSegment.points) > 1: elif fp == lastInputSegment.scaledPreviousOnCurve: mergeFirstSegments = True elif lp == firstInputSegment.scaledPreviousOnCurve: mergeFirstSegments = True elif fp == reversedLastInputSegment.scaledPreviousOnCurve: mergeFirstSegments = True elif lp == reversedFirstInputSegment.scaledPreviousOnCurve: mergeFirstSegments = True elif not hasOncurvePoints and _distance(fp, lp): # Merge last segment with first segment if the distance between the last point and the first # point is less than the step distance between the last two points. _approximateSegmentLength # can be significantly smaller than this step size. if len(segmentedFlatPoints[-1]) > 1: f1 = segmentedFlatPoints[-1][-2] f2 = segmentedFlatPoints[-1][-1] stepLen = _distance(f1, f2) else: stepLen = _approximateSegmentLength*clipperScale if _distance(fp, lp) <= stepLen: mergeFirstSegments = True if mergeFirstSegments: segmentedFlatPoints[0] = segmentedFlatPoints[-1] + segmentedFlatPoints[0] segmentedFlatPoints.pop(-1) mergeFirstSegments = False convertedSegments = [] previousIntersectionPoint = None if segmentedFlatPoints[-1][-1] in intersectionPoints: previousIntersectionPoint = self._scalePoint(segmentedFlatPoints[-1][-1]) elif segmentedFlatPoints[0][0] in intersectionPoints: previousIntersectionPoint = self._scalePoint(segmentedFlatPoints[0][0]) for flatSegment in segmentedFlatPoints: # search two points in the flat segment that is not an inputOncurve or intersection point # to get a proper direction of the flatSegment # based on these two points pick a inputSegment fp = ep = None for p in flatSegment: if p in possibleStartingPoints: continue elif fp is None: fp = p elif ep is None: ep = p else: break canDoFastLine = True if ep is None and ((fp is None) or (len(flatSegment) == 2)): # if fp is not None, then it is a flattened part of a curve, and should be used to derive the input segment. # It may be either the first or second point. # If fp is None, I use the original logic. if fp is None: fp = flatSegment[-1] # flat segment only contains two intersection points or one intersection point and one input oncurve point # this can be ignored cause this is a very small line # and will be converted to a simple line if self.clockwise: inputSegment = reversedFlatInputPointsSegmentDict.get(fp) else: inputSegment = flatInputPointsSegmentDict.get(fp) else: # get inputSegment based on the clockwise settings inputSegment = flatInputPointsSegmentDict[fp] if ep is not None and ep in inputSegment.flat: # if two points are found get indexes fi = inputSegment.flat.index(fp) ei = inputSegment.flat.index(ep) if fi > ei: # if the start index is bigger # get the reversed inputSegment inputSegment = reversedFlatInputPointsSegmentDict[fp] else: # if flat segment is short and has only one point not in intersections and input oncurves # test it against the reversed inputSegment reversedInputSegment = reversedFlatInputPointsSegmentDict[fp] if flatSegment[0] == reversedInputSegment.flat[0] and flatSegment[-1] == reversedInputSegment.flat[-1]: inputSegment = reversedInputSegment elif flatSegment[0] in intersectionPoints and flatSegment[-1] == reversedInputSegment.flat[-1]: inputSegment = reversedInputSegment elif flatSegment[-1] in intersectionPoints and flatSegment[0] == reversedInputSegment.flat[0]: inputSegment = reversedInputSegment canDoFastLine = False # if there is only one point in a flat segment # this is a single intersection points (two crossing lineTo's) if inputSegment.segmentType == "curve": canDoFastLine = False if (len(flatSegment) == 1 or inputSegment is None) and canDoFastLine: # p = flatSegment[0] for p in flatSegment: previousIntersectionPoint = self._scalePoint(p) pointInfo = dict() kwargs = dict() if p in flatInputPointsSegmentDict: lineSegment = flatInputPointsSegmentDict[p] segmentPoint = lineSegment.points[-1] pointInfo["smooth"] = segmentPoint.smooth pointInfo["name"] = segmentPoint.name kwargs.update(segmentPoint.kwargs) convertedSegments.append(OutputPoint(coordinates=previousIntersectionPoint, segmentType="line", kwargs=kwargs, **pointInfo)) continue tValues = None lastPointWithAttributes = None if flatSegment[0] == inputSegment.flat[0] and flatSegment[-1] != inputSegment.flat[-1]: # needed the first part of the segment # if previousIntersectionPoint is None: # previousIntersectionPoint = self._scalePoint(flatSegment[-1]) searchPoint = self._scalePoint(flatSegment[-1]) tValues = inputSegment.tValueForPoint(searchPoint) curveNeeded = 0 replacePointOnNewCurve = [(3, searchPoint)] previousIntersectionPoint = searchPoint elif flatSegment[-1] == inputSegment.flat[-1] and flatSegment[0] != inputSegment.flat[0]: # needed the end of the segment if previousIntersectionPoint is None: previousIntersectionPoint = self._scalePoint(flatSegment[0]) convertedSegments.append(OutputPoint( coordinates=previousIntersectionPoint, segmentType="line", )) tValues = inputSegment.tValueForPoint(previousIntersectionPoint) curveNeeded = -1 replacePointOnNewCurve = [(0, previousIntersectionPoint)] previousIntersectionPoint = None lastPointWithAttributes = inputSegment.points[-1] elif flatSegment[0] != inputSegment.flat[0] and flatSegment[-1] != inputSegment.flat[-1]: # needed the a middle part of the segment if previousIntersectionPoint is None: previousIntersectionPoint = self._scalePoint(flatSegment[0]) tValues = inputSegment.tValueForPoint(previousIntersectionPoint) searchPoint = self._scalePoint(flatSegment[-1]) tValues.extend(inputSegment.tValueForPoint(searchPoint)) curveNeeded = 1 replacePointOnNewCurve = [(0, previousIntersectionPoint), (3, searchPoint)] previousIntersectionPoint = searchPoint else: # take the whole segments as is newCurve = [ OutputPoint( coordinates=point.coordinates, segmentType=point.segmentType, smooth=point.smooth, name=point.name, kwargs=point.kwargs ) for point in inputSegment.points ] convertedSegments.extend(newCurve) previousIntersectionPoint = None # if we found some tvalue, split the curve and get the requested parts of the splitted curves if tValues: newCurve = inputSegment.split(tValues) newCurve = list(newCurve[curveNeeded]) for i, replace in replacePointOnNewCurve: newCurve[i] = replace newCurve = [OutputPoint(coordinates=p, segmentType=None) for p in newCurve[1:]] newCurve[-1].segmentType = inputSegment.segmentType if lastPointWithAttributes is not None: newCurve[-1].smooth = lastPointWithAttributes.smooth newCurve[-1].name = lastPointWithAttributes.name newCurve[-1].kwargs = lastPointWithAttributes.kwargs convertedSegments.extend(newCurve) # replace the the points with the converted segments segment.points = convertedSegments segment.segmentType = "reCurved" self.segments = newSegments # XXX convert all of the remaining segments to lines for segment in self.segments: if not segment.points: continue if segment.segmentType not in ["intersect", "flat"]: continue segment.segmentType = "line" segment.points = [ OutputPoint( coordinates=self._scalePoint(point), segmentType="line", # smooth=point.smooth, # name=point.name, # kwargs=point.kwargs ) for point in segment.points ] # ---- # Draw # ---- def drawPoints(self, pointPen): pointPen.beginPath() points = [] for segment in self.segments: points.extend(segment.points) hasOnCurve = False originalStartingPoints = [] for index, point in enumerate(points): if point.segmentType is not None: hasOnCurve = True if point.kwargs is not None and point.kwargs.get("startingPoint"): distanceFromOrigin = math.hypot(*point) originalStartingPoints.append((distanceFromOrigin, index)) if originalStartingPoints: # use the original starting point that is closest to the origin startingPointIndex = sorted(originalStartingPoints)[0][1] points = points[startingPointIndex:] + points[:startingPointIndex] elif hasOnCurve: while points[0].segmentType is None: p = points.pop(0) points.append(p) previousPointCoordinates = None for point in points: if previousPointCoordinates is not None and point.segmentType and tuple(point.coordinates) == previousPointCoordinates: continue kwargs = {} if point.kwargs is not None: kwargs = point.kwargs pointPen.addPoint( point.coordinates, segmentType=point.segmentType, smooth=point.smooth, name=point.name, **kwargs ) if point.segmentType: previousPointCoordinates = tuple(point.coordinates) else: previousPointCoordinates = None pointPen.endPath() class OutputSegment(object): __slots__ = ["segmentType", "points", "final"] def __init__(self, segmentType=None, points=None, final=False): self.segmentType = segmentType if points is None: points = [] self.points = points self.final = final class OutputPoint(InputPoint): pass # ---------- # Misc. Math # ---------- def _tValueForPointOnCubicCurve(point, cubicCurve, isHorizontal=0): """ Finds a t value on a curve from a point. The points must be originaly be a point on the curve. This will only back trace the t value, needed to split the curve in parts """ pt1, pt2, pt3, pt4 = cubicCurve a, b, c, d = bezierTools.calcCubicParameters(pt1, pt2, pt3, pt4) solutions = bezierTools.solveCubic(a[isHorizontal], b[isHorizontal], c[isHorizontal], d[isHorizontal] - point[isHorizontal]) solutions = [t for t in solutions if 0 <= t < 1] if not solutions and not isHorizontal: # can happen that a horizontal line doens intersect, try the vertical return _tValueForPointOnCubicCurve(point, (pt1, pt2, pt3, pt4), isHorizontal=1) if len(solutions) > 1: intersectionLenghts = {} for t in solutions: tp = _getCubicPoint(t, pt1, pt2, pt3, pt4) dist = _distance(tp, point) intersectionLenghts[dist] = t minDist = min(intersectionLenghts.keys()) solutions = [intersectionLenghts[minDist]] return solutions def _tValueForPointOnQuadCurve(point, pts, isHorizontal=0): quadSegments = decomposeQuadraticSegment(pts[1:]) previousOnCurve = pts[0] solutionsDict = dict() for index, (pt1, pt2) in enumerate(quadSegments): a, b, c = bezierTools.calcQuadraticParameters(previousOnCurve, pt1, pt2) subSolutions = bezierTools.solveQuadratic(a[isHorizontal], b[isHorizontal], c[isHorizontal] - point[isHorizontal]) subSolutions = [t for t in subSolutions if 0 <= t < 1] for t in subSolutions: solutionsDict[(t, index)] = _getQuadPoint(t, previousOnCurve, pt1, pt2) previousOnCurve = pt2 solutions = list(solutionsDict.keys()) if not solutions and not isHorizontal: return _tValueForPointOnQuadCurve(point, pts, isHorizontal=1) if len(solutions) > 1: intersectionLenghts = {} for t in solutions: tp = solutionsDict[t] dist = _distance(tp, point) intersectionLenghts[dist] = t minDist = min(intersectionLenghts.keys()) solutions = [intersectionLenghts[minDist]] return solutions def _tValueForPointOnLine(point, line): pt1, pt2 = line dist = _distance(pt1, point) totalDist = _distance(pt1, pt2) return [dist / totalDist] def _scalePoints(points, scale=1, convertToInteger=True): """ Scale points and optionally convert them to integers. """ if convertToInteger: points = [ (int(round(x * scale)), int(round(y * scale))) for (x, y) in points ] else: points = [(x * scale, y * scale) for (x, y) in points] return points def _scaleSinglePoint(point, scale=1, convertToInteger=True): """ Scale a single point """ x, y = point if convertToInteger: return int(round(x * scale)), int(round(y * scale)) else: return (x * scale, y * scale) def _intPoint(pt): return int(round(pt[0])), int(round(pt[1])) def _checkFlatPoints(points): _points = [] previousX = previousY = None for x, y in points: if x == previousX: continue elif y == previousY: continue if (x, y) not in _points: # is it possible that two flat point are on top of eachother??? _points.append((x, y)) previousX, previousY = x, y if _points[-1] != points[-1]: _points[-1] = points[-1] return _points """ The curve flattening code was forked and modified from RoboFab's FilterPen. That code was written by Erik van Blokland. """ def _flattenSegment(segment, approximateSegmentLength=_approximateSegmentLength): """ Flatten the curve segment int a list of points. The first and last points in the segment must be on curves. The returned list of points will not include the first on curve point. false curves (where the off curves are not any different from the on curves) must not be sent here. duplicate points must not be sent here. """ onCurve1, offCurve1, offCurve2, onCurve2 = segment if _pointOnLine(onCurve1, onCurve2, offCurve1) and _pointOnLine(onCurve1, onCurve2, offCurve2): return [onCurve2] est = _estimateCubicCurveLength(onCurve1, offCurve1, offCurve2, onCurve2) / approximateSegmentLength flat = [] minStep = 0.1564 step = 1.0 / est if step > .3: step = minStep t = step while t < 1: pt = _getCubicPoint(t, onCurve1, offCurve1, offCurve2, onCurve2) # ignore when point is in the same direction as the on - off curve line if not _pointOnLine(offCurve2, onCurve2, pt) and not _pointOnLine(onCurve1, offCurve1, pt): flat.append(pt) t += step flat.append(onCurve2) return flat def _distance(pt1, pt2): return math.sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2) def _pointOnLine(pt1, pt2, a): return abs(_distance(pt1, a) + _distance(a, pt2) - _distance(pt1, pt2)) < epsilon def _estimateCubicCurveLength(pt0, pt1, pt2, pt3, precision=10): """ Estimate the length of this curve by iterating through it and averaging the length of the flat bits. """ points = [] length = 0 step = 1.0 / precision factors = range(0, precision + 1) for i in factors: points.append(_getCubicPoint(i * step, pt0, pt1, pt2, pt3)) for i in range(len(points) - 1): pta = points[i] ptb = points[i + 1] length += _distance(pta, ptb) return length def _mid(pt1, pt2): """ (Point, Point) -> Point Return the point that lies in between the two input points. """ (x0, y0), (x1, y1) = pt1, pt2 return 0.5 * (x0 + x1), 0.5 * (y0 + y1) def _getCubicPoint(t, pt0, pt1, pt2, pt3): if t == 0: return pt0 if t == 1: return pt3 if t == 0.5: a = _mid(pt0, pt1) b = _mid(pt1, pt2) c = _mid(pt2, pt3) d = _mid(a, b) e = _mid(b, c) return _mid(d, e) else: cx = (pt1[0] - pt0[0]) * 3.0 cy = (pt1[1] - pt0[1]) * 3.0 bx = (pt2[0] - pt1[0]) * 3.0 - cx by = (pt2[1] - pt1[1]) * 3.0 - cy ax = pt3[0] - pt0[0] - cx - bx ay = pt3[1] - pt0[1] - cy - by t3 = t ** 3 t2 = t * t x = ax * t3 + bx * t2 + cx * t + pt0[0] y = ay * t3 + by * t2 + cy * t + pt0[1] return x, y def _getQuadPoint(t, pt0, pt1, pt2): if t == 0: return pt0 if t == 1: return pt2 else: cx = pt0[0] cy = pt0[1] bx = (pt1[0] - cx) * 2.0 by = (pt1[1] - cy) * 2.0 ax = pt2[0] - cx - bx ay = pt2[1] - cy - by x = ax * t**2 + bx * t + cx y = ay * t**2 + by * t + cy return x, y booleanOperations-0.8.0/Lib/booleanOperations/version.py000066400000000000000000000002111322314062200234170ustar00rootroot00000000000000try: __version__ = __import__('pkg_resources').require('booleanOperations')[0].version except Exception: __version__ = 'unknown' booleanOperations-0.8.0/MANIFEST.in000066400000000000000000000000531322314062200167310ustar00rootroot00000000000000exclude .travis.yml appveyor.yml .gitignorebooleanOperations-0.8.0/README.rst000066400000000000000000000116251322314062200166710ustar00rootroot00000000000000|Build Status| |PyPI| |Python Versions| BooleanOperations ================= Boolean operations on paths based on a super fast `polygon clipper library by Angus Johnson `__. You can download the latest version from PyPI: https://pypi.org/project/booleanOperations. Install ------- `Pip `__ is the recommended tool to install booleanOperations. To install the latest version: .. code:: sh pip install booleanOperations BooleanOperations depends on the following packages: - `pyclipper `__: Cython wrapper for the C++ Clipper library - `fonttools `__ - `ufoLib `__ All dependencies are available on PyPI, so they will be resolved automatically upon installing booleanOperations. BooleanOperationManager ----------------------- Containing a ``BooleanOperationManager`` handling all boolean operations on paths. Paths must be similar to ``defcon``, ``robofab`` contours. A manager draws the result in a ``pointPen``. .. code:: py from booleanOperations import BooleanOperationManager manager = BooleanOperationManager() BooleanOperationManager() ~~~~~~~~~~~~~~~~~~~~~~~~~ Create a ``BooleanOperationManager``. manager.union(contours, pointPen) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Performs a union on all ``contours`` and draw it in the ``pointPen``. (this is a what a remove overlaps does) manager.difference(contours, clipContours, pointPen) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Knock out the ``clipContours`` from the ``contours`` and draw it in the ``pointPen``. manager.intersection(contours, clipContours, pointPen) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Draw only the overlaps from the ``contours`` with the ``clipContours``\ and draw it in the ``pointPen``. manager.xor(contours, clipContours, pointPen) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Draw only the parts that not overlaps from the ``contours`` with the ``clipContours``\ and draw it in the ``pointPen``. manager.getIntersections(contours) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Returning all intersection for the given contours BooleanGlyph ------------ A glyph like object with boolean powers. .. code:: py from booleanOperations.booleanGlyph import BooleanGlyph booleanGlyph = BooleanGlyph(sourceGlyph) BooleanGlyph(sourceGlyph) ~~~~~~~~~~~~~~~~~~~~~~~~~ Create a ``BooleanGlyph`` object from ``sourceGlyph``. This is a very shallow glyph object with basic support. booleanGlyph.union(other) ^^^^^^^^^^^^^^^^^^^^^^^^^ Perform a **union** with the ``other``. Other must be a glyph or ``BooleanGlyph`` object. .. code:: py result = BooleanGlyph(glyph).union(BooleanGlyph(glyph2)) result = BooleanGlyph(glyph) | BooleanGlyph(glyph2) booleanGlyph.difference(other) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Perform a **difference** with the ``other``. Other must be a glyph or ``BooleanGlyph`` object. .. code:: py result = BooleanGlyph(glyph).difference(BooleanGlyph(glyph2)) result = BooleanGlyph(glyph) % BooleanGlyph(glyph2) booleanGlyph.intersection(other) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Perform a **intersection** with the ``other``. Other must be a glyph or ``BooleanGlyph`` object. .. code:: py result = BooleanGlyph(glyph).intersection(BooleanGlyph(glyph2)) result = BooleanGlyph(glyph) & BooleanGlyph(glyph2) booleanGlyph.xor(other) ^^^^^^^^^^^^^^^^^^^^^^^ Perform a **xor** with the ``other``. Other must be a glyph or ``BooleanGlyph`` object. .. code:: py result = BooleanGlyph(glyph).xor(BooleanGlyph(glyph2)) result = BooleanGlyph(glyph) ^ BooleanGlyph(glyph2) booleanGlyph.removeOverlap() ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Perform a **union** on it self. This will remove all overlapping contours and self intersecting contours. .. code:: py result = BooleanGlyph(glyph).removeOverlap() -------------- booleanGlyph.name ^^^^^^^^^^^^^^^^^ The **name** of the ``sourceGlyph``. booleanGlyph.unicodes ^^^^^^^^^^^^^^^^^^^^^ The **unicodes** of the ``sourceGlyph``. booleanGlyph.width ^^^^^^^^^^^^^^^^^^ The **width** of the ``sourceGlyph``. booleanGlyph.lib ^^^^^^^^^^^^^^^^ The **lib** of the ``sourceGlyph``. booleanGlyph.note ^^^^^^^^^^^^^^^^^ The **note** of the ``sourceGlyph``. booleanGlyph.contours ^^^^^^^^^^^^^^^^^^^^^ List the **contours** of the glyph. booleanGlyph.components ^^^^^^^^^^^^^^^^^^^^^^^ List the **components** of the glyph. booleanGlyph.anchors ^^^^^^^^^^^^^^^^^^^^ List the **anchors** of the glyph. .. |Build Status| image:: https://api.travis-ci.org/typemytype/booleanOperations.svg :target: https://travis-ci.org/typemytype/booleanOperations .. |PyPI| image:: https://img.shields.io/pypi/v/booleanOperations.svg :target: https://pypi.org/project/booleanOperations/ .. |Python Versions| image:: https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5%2C%203.6-blue.svg booleanOperations-0.8.0/appveyor.yml000066400000000000000000000032071322314062200175670ustar00rootroot00000000000000environment: matrix: - PYTHON: "C:\\Python27" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "32" TOXENV: "py27" TOXPYTHON: "C:\\Python27\\python.exe" - PYTHON: "C:\\Python34" PYTHON_VERSION: "3.4.x" PYTHON_ARCH: "32" TOXENV: "py34" TOXPYTHON: "C:\\Python34\\python.exe" - PYTHON: "C:\\Python35" PYTHON_VERSION: "3.5.0" PYTHON_ARCH: "32" TOXENV: "py35" TOXPYTHON: "C:\\Python35\\python.exe" - PYTHON: "C:\\Python27-x64" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "64" TOXENV: "py27" TOXPYTHON: "C:\\Python27-x64\\python.exe" - PYTHON: "C:\\Python34-x64" PYTHON_VERSION: "3.4.x" PYTHON_ARCH: "64" TOXENV: "py34" TOXPYTHON: "C:\\Python34-x64\\python.exe" - PYTHON: "C:\\Python35-x64" PYTHON_VERSION: "3.5.0" PYTHON_ARCH: "64" TOXENV: "py35" TOXPYTHON: "C:\\Python35-x64\\python.exe" init: - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" install: # prepend newly installed Python to the PATH - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" # check that we have the expected version and architecture for Python - "python --version" - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" # upgrade pip to avoid out-of-date warnings - "pip install --disable-pip-version-check --user --upgrade pip" # install/upgrade setuptools and wheel to build packages - "pip install --upgrade setuptools wheel" # install tox to run test suite in a virtual environment - "pip install -U tox" build: false test_script: - "tox" booleanOperations-0.8.0/requirements.txt000066400000000000000000000000601322314062200204550ustar00rootroot00000000000000pyclipper==1.0.5 fonttools==3.1.2 ufoLib==2.0.0 booleanOperations-0.8.0/setup.cfg000066400000000000000000000004631322314062200170210ustar00rootroot00000000000000[wheel] universal = 1 [sdist] formats = zip [aliases] test = pytest [tool:pytest] minversion = 3.0.2 testpaths = Lib/booleanOperations tests addopts = # run py.test in verbose mode -v # show extra test summary info -r a # run doctests in all .py modules --doctest-modules booleanOperations-0.8.0/setup.py000066400000000000000000000027621322314062200167160ustar00rootroot00000000000000from setuptools import setup, find_packages import sys needs_pytest = {'pytest', 'test'}.intersection(sys.argv) pytest_runner = ['pytest_runner'] if needs_pytest else [] needs_wheel = {'bdist_wheel'}.intersection(sys.argv) wheel = ['wheel'] if needs_wheel else [] with open('README.rst', 'r') as f: long_description = f.read() setup_params = dict( name="booleanOperations", use_scm_version=True, description="Boolean operations on paths.", long_description=long_description, author="Frederik Berlaen", author_email="frederik@typemytype.com", url="https://github.com/typemytype/booleanOperations", license="MIT", package_dir={"": "Lib"}, packages=find_packages("Lib"), setup_requires=[ "setuptools_scm>=1.11.1,!=1.13.1,!=1.14.0", ] + pytest_runner + wheel, tests_require=[ 'pytest>=3.0.2', ], install_requires=[ "pyclipper>=1.0.5", "fonttools>=3.1.2", "ufoLib>=2.0.0", ], classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Topic :: Multimedia :: Graphics :: Editors :: Vector-Based', 'Topic :: Software Development :: Libraries :: Python Modules', ], ) if __name__ == "__main__": setup(**setup_params) booleanOperations-0.8.0/tests/000077500000000000000000000000001322314062200163375ustar00rootroot00000000000000booleanOperations-0.8.0/tests/test_booleanOperations.py000066400000000000000000000002341322314062200234320ustar00rootroot00000000000000import booleanOperations def test_dummy_function(): assert True class TestDummyClass(object): def test_dummy_method(self): assert True booleanOperations-0.8.0/tox.ini000066400000000000000000000005761322314062200165200ustar00rootroot00000000000000[tox] envlist = py27, py35 [testenv] basepython = # we use TOXPYTHON env variable to specify the location of Appveyor Python py27: {env:TOXPYTHON:python2.7} py34: {env:TOXPYTHON:python3.4} py35: {env:TOXPYTHON:python3.5} deps = pytest -rrequirements.txt commands = # pass to pytest any extra positional arguments after `tox -- ...` pytest {posargs}