A quick recipe for testing a hit on an area.
Once you've drawn something and before you cr.fill() or cr.stroke(), you can record the path to a list of points for later use. This recipe includes an algorithm to tell whether a point is within that path's area or not. It prints to the console, so it's not exactly bling, but it's pretty nifty for all that. Go see http://local.wasp.uwa.edu.au/~pbourke/geometry/insidepoly/ for the theory behind the voodoo :)
Newsflash
Jeff Muizelaar informed me that Cairo has a built-in function to do this anyway. See code listing 2.
#! /usr/bin/env python
## hittest Copyright (C) 2007 Donn.C.Ingle
##
## Contact: donn.ingle@gmail.com - I hope this email lasts.
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## ( at your option ) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
##
import pygtk
pygtk.require('2.0')
import gtk, gobject, cairo
from gtk import gdk
# Create a GTK+ widget on which we will draw using Cairo
class Screen(gtk.DrawingArea):
# Draw in response to an expose-event
__gsignals__ = { "expose-event": "override" }
def __init__(self):
super(Screen,self).__init__()
# gtk.Widget signals
self.connect("button_press_event", self.button_press)
self.connect("button_release_event", self.button_release)
self.connect("motion_notify_event", self.motion_notify)
# More GTK voodoo : unmask events
self.add_events(gdk.BUTTON_PRESS_MASK |
gdk.BUTTON_RELEASE_MASK |
gdk.POINTER_MOTION_MASK)
# Handle the expose-event by drawing
def do_expose_event(self, event):
# Create the cairo context
cr = self.window.cairo_create()
self.hitpath = None #Is set later
# Restrict Cairo to the exposed area; avoid extra work
cr.rectangle(event.area.x, event.area.y,
event.area.width, event.area.height)
cr.clip()
self.draw(cr, *self.window.get_size())
def makeHitPath(self,cairopath):
## Make a simpler list of tuples
## Internally, a cairo path looks like this:
## (0, (10.0, 10.0))
## (1, (60.0, 10.0))
## (1, (60.0, 60.0))
## (1, (35.0, 60.0))
## (1, (35.0, 35.0))
## (1, (10.0, 35.0))
## (1, (10.0, 60.0))
## (1, (-40.0, 60.0))
## (3, ()) #want to ignore this one
## (0, (10.0, 10.0))
self.hitpath = []
for sub in cairopath:
if sub[1]: #kick out the close path () empty tuple
self.hitpath.append(sub[1]) #list of tuples
def draw(self, cr, width, height):
# Fill the background with gray
cr.set_source_rgb(0.5, 0.5, 0.5)
cr.rectangle(0, 0, width, height)
cr.fill()
def hitTest(self,*p):
## Code lifted from http://local.wasp.uwa.edu.au/~pbourke/geometry/insidepoly/
## converted to Python. I won't pretend I grok it at all, just glad it works!
## Not sure how well it works yet, it might have edge flaws.
px = p[0]
py = p[1]
counter = i = xinters = 0
p1 = p2 = ()
p1 = self.hitpath[0]
N = len(self.hitpath)
# Mathemagic loop-de-loop
for i in range(0,N):
p2 = self.hitpath[i % N]
if py > min( p1[1] , p2[1] ):
if py <= max( p1[1], p2[1] ):
if px <= max( p1[0], p2[0] ):
if p1[1] != p2[1]:
xinters = ( py - p1[1] ) * ( p2[0] - p1[0] ) / ( p2[1] - p1[1] ) + p1[0]
if p1[0] == p2[0] or px <= xinters: counter += 1
p1 = p2
if counter % 2 == 0:
return "outside"
return "inside"
def button_press(self,widget,event):
pass
def button_release(self,widget,event):
pass
def motion_notify(self,widget,event):
pass
# GTK mumbo-jumbo to show the widget in a window and quit when it's closed
def run(Widget):
window = gtk.Window()
window.connect("delete-event", gtk.main_quit)
widget = Widget()
widget.show()
window.add(widget)
window.present()
gtk.main()
class Shapes(Screen):
#Override the press event
def button_press(self,widget,event):
print self.hitTest(event.x, event.y)
def draw(self, cr, width, height):
x = y = 10
sx = sy = 50
cr.move_to(x,y)
cr.line_to(x+sx,y)
cr.line_to(x+sx,y+sy)
cr.line_to(x+(sx/2),y+sy)
cr.line_to(x+(sx/2),y+(sy/2))
cr.line_to(x,y+(sy/2))
cr.line_to(x,y+sy)
cr.line_to(x-sx,y+sy)
cr.close_path()
cr.set_source_rgb(1,0,0)
self.makeHitPath(cr.copy_path_flat()) #record the path to use as a hit area.
cr.fill() #consumes the path, so get it before the fill
run(Shapes)
Code Listing 2 : It's already done.
...snip
class Shapes(Screen):
#Override the press event
def button_press(self,widget,event):
## Gues what? Cairo had it built-in all along :)
## You just need to keep a ref to the context.
## I'm not sure if re-"drawing" the entire path, just so you can
## test it for a hit is faster than the other version of this
## script that uses a manual python-speed algorithm.
self.cr.append_path(self.hitpath) # re-gen the path
result = self.cr.in_fill(event.x, event.y) # Test it. Sweet.
print result
def draw(self, cr, width, height):
x = y = 10
sx = sy = 50
cr.move_to(x,y)
cr.line_to(x+sx,y)
cr.line_to(x+sx,y+sy)
cr.line_to(x+(sx/2),y+sy)
cr.line_to(x+(sx/2),y+(sy/2))
cr.line_to(x,y+(sy/2))
cr.line_to(x,y+sy)
cr.line_to(x-sx,y+sy)
cr.close_path()
cr.set_source_rgb(1,0,0)
self.hitpath = cr.copy_path_flat() #record the path to use as a hit area.
cr.fill() #consumes the path, so get it before the fill
run(Shapes)