1*e1fe3e4aSElliott Hughes"""fontTools.pens.pointInsidePen -- Pen implementing "point inside" testing 2*e1fe3e4aSElliott Hughesfor shapes. 3*e1fe3e4aSElliott Hughes""" 4*e1fe3e4aSElliott Hughes 5*e1fe3e4aSElliott Hughesfrom fontTools.pens.basePen import BasePen 6*e1fe3e4aSElliott Hughesfrom fontTools.misc.bezierTools import solveQuadratic, solveCubic 7*e1fe3e4aSElliott Hughes 8*e1fe3e4aSElliott Hughes 9*e1fe3e4aSElliott Hughes__all__ = ["PointInsidePen"] 10*e1fe3e4aSElliott Hughes 11*e1fe3e4aSElliott Hughes 12*e1fe3e4aSElliott Hughesclass PointInsidePen(BasePen): 13*e1fe3e4aSElliott Hughes """This pen implements "point inside" testing: to test whether 14*e1fe3e4aSElliott Hughes a given point lies inside the shape (black) or outside (white). 15*e1fe3e4aSElliott Hughes Instances of this class can be recycled, as long as the 16*e1fe3e4aSElliott Hughes setTestPoint() method is used to set the new point to test. 17*e1fe3e4aSElliott Hughes 18*e1fe3e4aSElliott Hughes Typical usage: 19*e1fe3e4aSElliott Hughes 20*e1fe3e4aSElliott Hughes pen = PointInsidePen(glyphSet, (100, 200)) 21*e1fe3e4aSElliott Hughes outline.draw(pen) 22*e1fe3e4aSElliott Hughes isInside = pen.getResult() 23*e1fe3e4aSElliott Hughes 24*e1fe3e4aSElliott Hughes Both the even-odd algorithm and the non-zero-winding-rule 25*e1fe3e4aSElliott Hughes algorithm are implemented. The latter is the default, specify 26*e1fe3e4aSElliott Hughes True for the evenOdd argument of __init__ or setTestPoint 27*e1fe3e4aSElliott Hughes to use the even-odd algorithm. 28*e1fe3e4aSElliott Hughes """ 29*e1fe3e4aSElliott Hughes 30*e1fe3e4aSElliott Hughes # This class implements the classical "shoot a ray from the test point 31*e1fe3e4aSElliott Hughes # to infinity and count how many times it intersects the outline" (as well 32*e1fe3e4aSElliott Hughes # as the non-zero variant, where the counter is incremented if the outline 33*e1fe3e4aSElliott Hughes # intersects the ray in one direction and decremented if it intersects in 34*e1fe3e4aSElliott Hughes # the other direction). 35*e1fe3e4aSElliott Hughes # I found an amazingly clear explanation of the subtleties involved in 36*e1fe3e4aSElliott Hughes # implementing this correctly for polygons here: 37*e1fe3e4aSElliott Hughes # http://graphics.cs.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html 38*e1fe3e4aSElliott Hughes # I extended the principles outlined on that page to curves. 39*e1fe3e4aSElliott Hughes 40*e1fe3e4aSElliott Hughes def __init__(self, glyphSet, testPoint, evenOdd=False): 41*e1fe3e4aSElliott Hughes BasePen.__init__(self, glyphSet) 42*e1fe3e4aSElliott Hughes self.setTestPoint(testPoint, evenOdd) 43*e1fe3e4aSElliott Hughes 44*e1fe3e4aSElliott Hughes def setTestPoint(self, testPoint, evenOdd=False): 45*e1fe3e4aSElliott Hughes """Set the point to test. Call this _before_ the outline gets drawn.""" 46*e1fe3e4aSElliott Hughes self.testPoint = testPoint 47*e1fe3e4aSElliott Hughes self.evenOdd = evenOdd 48*e1fe3e4aSElliott Hughes self.firstPoint = None 49*e1fe3e4aSElliott Hughes self.intersectionCount = 0 50*e1fe3e4aSElliott Hughes 51*e1fe3e4aSElliott Hughes def getWinding(self): 52*e1fe3e4aSElliott Hughes if self.firstPoint is not None: 53*e1fe3e4aSElliott Hughes # always make sure the sub paths are closed; the algorithm only works 54*e1fe3e4aSElliott Hughes # for closed paths. 55*e1fe3e4aSElliott Hughes self.closePath() 56*e1fe3e4aSElliott Hughes return self.intersectionCount 57*e1fe3e4aSElliott Hughes 58*e1fe3e4aSElliott Hughes def getResult(self): 59*e1fe3e4aSElliott Hughes """After the shape has been drawn, getResult() returns True if the test 60*e1fe3e4aSElliott Hughes point lies within the (black) shape, and False if it doesn't. 61*e1fe3e4aSElliott Hughes """ 62*e1fe3e4aSElliott Hughes winding = self.getWinding() 63*e1fe3e4aSElliott Hughes if self.evenOdd: 64*e1fe3e4aSElliott Hughes result = winding % 2 65*e1fe3e4aSElliott Hughes else: # non-zero 66*e1fe3e4aSElliott Hughes result = self.intersectionCount != 0 67*e1fe3e4aSElliott Hughes return not not result 68*e1fe3e4aSElliott Hughes 69*e1fe3e4aSElliott Hughes def _addIntersection(self, goingUp): 70*e1fe3e4aSElliott Hughes if self.evenOdd or goingUp: 71*e1fe3e4aSElliott Hughes self.intersectionCount += 1 72*e1fe3e4aSElliott Hughes else: 73*e1fe3e4aSElliott Hughes self.intersectionCount -= 1 74*e1fe3e4aSElliott Hughes 75*e1fe3e4aSElliott Hughes def _moveTo(self, point): 76*e1fe3e4aSElliott Hughes if self.firstPoint is not None: 77*e1fe3e4aSElliott Hughes # always make sure the sub paths are closed; the algorithm only works 78*e1fe3e4aSElliott Hughes # for closed paths. 79*e1fe3e4aSElliott Hughes self.closePath() 80*e1fe3e4aSElliott Hughes self.firstPoint = point 81*e1fe3e4aSElliott Hughes 82*e1fe3e4aSElliott Hughes def _lineTo(self, point): 83*e1fe3e4aSElliott Hughes x, y = self.testPoint 84*e1fe3e4aSElliott Hughes x1, y1 = self._getCurrentPoint() 85*e1fe3e4aSElliott Hughes x2, y2 = point 86*e1fe3e4aSElliott Hughes 87*e1fe3e4aSElliott Hughes if x1 < x and x2 < x: 88*e1fe3e4aSElliott Hughes return 89*e1fe3e4aSElliott Hughes if y1 < y and y2 < y: 90*e1fe3e4aSElliott Hughes return 91*e1fe3e4aSElliott Hughes if y1 >= y and y2 >= y: 92*e1fe3e4aSElliott Hughes return 93*e1fe3e4aSElliott Hughes 94*e1fe3e4aSElliott Hughes dx = x2 - x1 95*e1fe3e4aSElliott Hughes dy = y2 - y1 96*e1fe3e4aSElliott Hughes t = (y - y1) / dy 97*e1fe3e4aSElliott Hughes ix = dx * t + x1 98*e1fe3e4aSElliott Hughes if ix < x: 99*e1fe3e4aSElliott Hughes return 100*e1fe3e4aSElliott Hughes self._addIntersection(y2 > y1) 101*e1fe3e4aSElliott Hughes 102*e1fe3e4aSElliott Hughes def _curveToOne(self, bcp1, bcp2, point): 103*e1fe3e4aSElliott Hughes x, y = self.testPoint 104*e1fe3e4aSElliott Hughes x1, y1 = self._getCurrentPoint() 105*e1fe3e4aSElliott Hughes x2, y2 = bcp1 106*e1fe3e4aSElliott Hughes x3, y3 = bcp2 107*e1fe3e4aSElliott Hughes x4, y4 = point 108*e1fe3e4aSElliott Hughes 109*e1fe3e4aSElliott Hughes if x1 < x and x2 < x and x3 < x and x4 < x: 110*e1fe3e4aSElliott Hughes return 111*e1fe3e4aSElliott Hughes if y1 < y and y2 < y and y3 < y and y4 < y: 112*e1fe3e4aSElliott Hughes return 113*e1fe3e4aSElliott Hughes if y1 >= y and y2 >= y and y3 >= y and y4 >= y: 114*e1fe3e4aSElliott Hughes return 115*e1fe3e4aSElliott Hughes 116*e1fe3e4aSElliott Hughes dy = y1 117*e1fe3e4aSElliott Hughes cy = (y2 - dy) * 3.0 118*e1fe3e4aSElliott Hughes by = (y3 - y2) * 3.0 - cy 119*e1fe3e4aSElliott Hughes ay = y4 - dy - cy - by 120*e1fe3e4aSElliott Hughes solutions = sorted(solveCubic(ay, by, cy, dy - y)) 121*e1fe3e4aSElliott Hughes solutions = [t for t in solutions if -0.0 <= t <= 1.0] 122*e1fe3e4aSElliott Hughes if not solutions: 123*e1fe3e4aSElliott Hughes return 124*e1fe3e4aSElliott Hughes 125*e1fe3e4aSElliott Hughes dx = x1 126*e1fe3e4aSElliott Hughes cx = (x2 - dx) * 3.0 127*e1fe3e4aSElliott Hughes bx = (x3 - x2) * 3.0 - cx 128*e1fe3e4aSElliott Hughes ax = x4 - dx - cx - bx 129*e1fe3e4aSElliott Hughes 130*e1fe3e4aSElliott Hughes above = y1 >= y 131*e1fe3e4aSElliott Hughes lastT = None 132*e1fe3e4aSElliott Hughes for t in solutions: 133*e1fe3e4aSElliott Hughes if t == lastT: 134*e1fe3e4aSElliott Hughes continue 135*e1fe3e4aSElliott Hughes lastT = t 136*e1fe3e4aSElliott Hughes t2 = t * t 137*e1fe3e4aSElliott Hughes t3 = t2 * t 138*e1fe3e4aSElliott Hughes 139*e1fe3e4aSElliott Hughes direction = 3 * ay * t2 + 2 * by * t + cy 140*e1fe3e4aSElliott Hughes incomingGoingUp = outgoingGoingUp = direction > 0.0 141*e1fe3e4aSElliott Hughes if direction == 0.0: 142*e1fe3e4aSElliott Hughes direction = 6 * ay * t + 2 * by 143*e1fe3e4aSElliott Hughes outgoingGoingUp = direction > 0.0 144*e1fe3e4aSElliott Hughes incomingGoingUp = not outgoingGoingUp 145*e1fe3e4aSElliott Hughes if direction == 0.0: 146*e1fe3e4aSElliott Hughes direction = ay 147*e1fe3e4aSElliott Hughes incomingGoingUp = outgoingGoingUp = direction > 0.0 148*e1fe3e4aSElliott Hughes 149*e1fe3e4aSElliott Hughes xt = ax * t3 + bx * t2 + cx * t + dx 150*e1fe3e4aSElliott Hughes if xt < x: 151*e1fe3e4aSElliott Hughes continue 152*e1fe3e4aSElliott Hughes 153*e1fe3e4aSElliott Hughes if t in (0.0, -0.0): 154*e1fe3e4aSElliott Hughes if not outgoingGoingUp: 155*e1fe3e4aSElliott Hughes self._addIntersection(outgoingGoingUp) 156*e1fe3e4aSElliott Hughes elif t == 1.0: 157*e1fe3e4aSElliott Hughes if incomingGoingUp: 158*e1fe3e4aSElliott Hughes self._addIntersection(incomingGoingUp) 159*e1fe3e4aSElliott Hughes else: 160*e1fe3e4aSElliott Hughes if incomingGoingUp == outgoingGoingUp: 161*e1fe3e4aSElliott Hughes self._addIntersection(outgoingGoingUp) 162*e1fe3e4aSElliott Hughes # else: 163*e1fe3e4aSElliott Hughes # we're not really intersecting, merely touching 164*e1fe3e4aSElliott Hughes 165*e1fe3e4aSElliott Hughes def _qCurveToOne_unfinished(self, bcp, point): 166*e1fe3e4aSElliott Hughes # XXX need to finish this, for now doing it through a cubic 167*e1fe3e4aSElliott Hughes # (BasePen implements _qCurveTo in terms of a cubic) will 168*e1fe3e4aSElliott Hughes # have to do. 169*e1fe3e4aSElliott Hughes x, y = self.testPoint 170*e1fe3e4aSElliott Hughes x1, y1 = self._getCurrentPoint() 171*e1fe3e4aSElliott Hughes x2, y2 = bcp 172*e1fe3e4aSElliott Hughes x3, y3 = point 173*e1fe3e4aSElliott Hughes c = y1 174*e1fe3e4aSElliott Hughes b = (y2 - c) * 2.0 175*e1fe3e4aSElliott Hughes a = y3 - c - b 176*e1fe3e4aSElliott Hughes solutions = sorted(solveQuadratic(a, b, c - y)) 177*e1fe3e4aSElliott Hughes solutions = [ 178*e1fe3e4aSElliott Hughes t for t in solutions if ZERO_MINUS_EPSILON <= t <= ONE_PLUS_EPSILON 179*e1fe3e4aSElliott Hughes ] 180*e1fe3e4aSElliott Hughes if not solutions: 181*e1fe3e4aSElliott Hughes return 182*e1fe3e4aSElliott Hughes # XXX 183*e1fe3e4aSElliott Hughes 184*e1fe3e4aSElliott Hughes def _closePath(self): 185*e1fe3e4aSElliott Hughes if self._getCurrentPoint() != self.firstPoint: 186*e1fe3e4aSElliott Hughes self.lineTo(self.firstPoint) 187*e1fe3e4aSElliott Hughes self.firstPoint = None 188*e1fe3e4aSElliott Hughes 189*e1fe3e4aSElliott Hughes def _endPath(self): 190*e1fe3e4aSElliott Hughes """Insideness is not defined for open contours.""" 191*e1fe3e4aSElliott Hughes raise NotImplementedError 192