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