pointInsidePen.py 6.2 KB

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