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