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