xref: /aosp_15_r20/external/fonttools/Lib/fontTools/pens/pointInsidePen.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
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