zoom-pan-rotate widget for pyGTK

This pyGTK OpenGL widget lets you display any GL drawing, which you can zoom, rotate and pan with the mouse.  You can also determine what the user clicks on.


Spin the object around to look at it from a different angle by dragging with the left mouse-button.  Zoom in and out with the scroll-wheel or dragging the middle mouse button (or, on a two-button mouse, both buttons at the same time).  Pan using CTRL right-mouse-button dragging.

Selection ("picking") is also triggered by a left mouse button, but its up to the draw function to represent that graphically.

After a frustrating time developing this kind of code custom for each project, and always having some bug somewhere that I was living with, I found the excellent GLT ZPR C code on the internet.  And a quick translation to Python 2.5+ later, here it is!

Its kind of depressing that this simple utility widget isn't part of the framework.
   1 """ OpenGL Zoom Pan Rotate Widget for PyGTK
   2     subclass or otherwise assign a 'draw' function to an instance of this class
   3     The draw gets called back each time it should render
   4     The OpenGL context has been set up so you can just draw with naked gl* func
   5     Optionally, provide a 'pick' function to get callbacks when the user clicks
   6     on an object that has been named with glPushNames()
   7 
   8     translation of the excellent GLT ZPR (Zoom Pan Rotate) C code:
   9     http://www.nigels.com/glt/gltzpr/
  10     Released under LGPL: http://www.gnu.org/copyleft/lesser.html
  11     (c) William Edwards 2010
  12 """
  13 
  14 import pygtk; pygtk.require('2.0')
  15 import gtk, gtk.gdk as gdk, gtk.gtkgl as gtkgl, gtk.gdkgl as gdkgl
16 from OpenGL.GL import * 17 from OpenGL.GLU import * 18 from OpenGL.GLUT import * 19 import math
20 from numpy import matrix
21 22 class GLZPR(gtkgl.DrawingArea): 23 def __init__(self,w=640,h=480): 24 try: 25 glconfig = gdkgl.Config(mode = (gdkgl.MODE_RGB|gdkgl.MODE_DOUBLE|gdkgl.MODE_DEPTH)) 26 except gtk.gdkgl.NoMatches: 27 glconfig = gdkgl.Config(mode = (gdkgl.MODE_RGB|gdkgl.MODE_DEPTH)) 28 gtkgl.DrawingArea.__init__(self,glconfig) 29 self.set_size_request(w,h) 30 self.connect_after("realize",self._init) 31 self.connect("configure_event",self._reshape) 32 self.connect("expose_event",self._draw) 33 self.connect("button_press_event",self._mouseButton) 34 self.connect("button_release_event",self._mouseButton) 35 self.connect("motion_notify_event",self._mouseMotion) 36 self.connect("scroll_event",self._mouseScroll) 37 self.set_events(self.get_events()| 38 gdk.BUTTON_PRESS_MASK|gdk.BUTTON_RELEASE_MASK| 39 gdk.POINTER_MOTION_MASK|gdk.POINTER_MOTION_HINT_MASK) 40 self._zNear, self._zFar = -10.0, 10.0 41 self._zprReferencePoint = [0.,0.,0.,0.]
42 self._mouseX = self._mouseY = 0 43 self._dragPosX = self._dragPosY = self._dragPosZ = 0.
44 self._mouseRotate = self._mouseZoom = self._mousePan = False 45
46 class _Context: 47 def __init__(self,widget): 48 self._widget = widget
49 self._count = 0 50 self._modelview = self._projection = None 51 self._persist = False 52 def __enter__(self): 53 assert(self._count == 0) 54 self.ctx = gtkgl.widget_get_gl_context(self._widget) 55 self.surface = gtkgl.widget_get_gl_drawable(self._widget) 56 self._begin = self.surface.gl_begin(self.ctx) 57 if self._begin: 58 self._count += 1 59 if self._projection is not None: 60 glMatrixMode(GL_PROJECTION) 61 glLoadMatrixd(self._projection) 62 if self._modelview is not None: 63 glMatrixMode(GL_MODELVIEW) 64 glLoadMatrixd(self._modelview) 65 return self 66 return 67 def __exit__(self,exc_type,exc_value,exc_traceback): 68 if self._begin: 69 self._count -= 1 70 if self._persist and (exc_type is None): 71 self._modelview = glGetDoublev(GL_MODELVIEW_MATRIX) 72 self._projection = glGetDoublev(GL_PROJECTION_MATRIX) 73 self.surface.gl_end() 74 del self.ctx
75 del self.surface
76 self._persist = False 77 if exc_type is not None: 78 import traceback
79 traceback.print_exception(exc_type,exc_value,exc_traceback) 80 return True # suppress 81
82 def open_context(self,persist_matrix_changes = False): 83 if not hasattr(self,"_context"): 84 self._context = self._Context(self) 85 assert(self._context._count == 0) 86 self._context._persist = persist_matrix_changes
87 return self._context
88
89 def get_open_context(self): 90 if hasattr(self,"_context") and (self._context._count > 0): 91 return self._context
92
93 def _init(self,widget): 94 assert(widget == self) 95 self.init() ### optionally overriden by subclasses 96 return True 97
98 def reset(self): 99 with self.open_context(True): 100 glMatrixMode(GL_MODELVIEW) 101 glLoadIdentity() 102
103 def init(self): 104 glLightfv(GL_LIGHT0,GL_AMBIENT, (0.,0.,0.,1.)) 105 glLightfv(GL_LIGHT0,GL_DIFFUSE, (1.,1.,1.,1.)) 106 glLightfv(GL_LIGHT0,GL_SPECULAR,(1.,1.,1.,1.)) 107 glLightfv(GL_LIGHT0,GL_POSITION,(1.,1.,1.,0.)) 108 glMaterialfv(GL_FRONT,GL_AMBIENT, (.7,.7,.7,1.)) 109 glMaterialfv(GL_FRONT,GL_DIFFUSE, (.8,.8,.8,1.)) 110 glMaterialfv(GL_FRONT,GL_SPECULAR,(1.,1.,1.,1.)) 111 glMaterialfv(GL_FRONT,GL_SHININESS,100.0) 112 glEnable(GL_LIGHTING) 113 glEnable(GL_LIGHT0) 114 glDepthFunc(GL_LESS) 115 glEnable(GL_DEPTH_TEST) 116 glEnable(GL_NORMALIZE) 117 glEnable(GL_COLOR_MATERIAL) 118
119 def _reshape(self,widget,event): 120 assert(self == widget)
121 with self.open_context(True): 122 x, y, width, height = self.get_allocation() 123 glViewport(0,0,width,height);
124 self._top = 1.0 125 self._bottom = -1.0 126 self._left = -float(width)/float(height) 127 self._right = -self._left
128 glMatrixMode(GL_PROJECTION) 129 glLoadIdentity() 130 glOrtho(self._left,self._right,self._bottom,self._top,self._zNear,self._zFar) 131 if hasattr(self,"reshape"): 132 self.reshape(event,x,y,width,height) ### optionally implemented by subclasses 133 return True 134
135 def _mouseMotion(self,widget,event): 136 assert(widget==self) 137 if event.is_hint: 138 x, y, state = event.window.get_pointer() 139 else: 140 x = event.x
141 y = event.y
142 state = event.state
143 dx = x - self._mouseX
144 dy = y - self._mouseY
145 if (dx==0 and dy==0): return 146 self._mouseX, self._mouseY = x, y
147 with self.open_context(True): 148 changed = False 149 if self._mouseZoom: 150 s = math.exp(float(dy)*0.01) 151 self._apply(glScalef,s,s,s) 152 changed = True 153 elif self._mouseRotate: 154 ax, ay, az = dy, dx, 0.
155 viewport = glGetIntegerv(GL_VIEWPORT) 156 angle = math.sqrt(ax**2+ay**2+az**2)/float(viewport[2]+1)*180.0 157 inv = matrix(glGetDoublev(GL_MODELVIEW_MATRIX)).I
158 bx = inv[0,0]*ax + inv[1,0]*ay + inv[2,0]*az
159 by = inv[0,1]*ax + inv[1,1]*ay + inv[2,1]*az
160 bz = inv[0,2]*ax + inv[1,2]*ay + inv[2,2]*az
161 self._apply(glRotatef,angle,bx,by,bz) 162 changed = True 163 elif self._mousePan: 164 px, py, pz = self._pos(x,y);
165 modelview = glGetDoublev(GL_MODELVIEW_MATRIX) 166 glLoadIdentity() 167 glTranslatef(px-self._dragPosX,py-self._dragPosY,pz-self._dragPosZ) 168 glMultMatrixd(modelview) 169 self._dragPosX = px
170 self._dragPosY = py
171 self._dragPosZ = pz
172 changed = True 173 if changed: 174 self.queue_draw() 175
176 def _apply(self,func,*args): 177 glTranslatef(*self._zprReferencePoint[0:3]) 178 func(*args) 179 glTranslatef(*map(lambda x:-x,self._zprReferencePoint[0:3])) 180 181 def _mouseScroll(self,widget,event): 182 assert(self == widget)
183 s = 4. if (event.direction == gdk.SCROLL_UP) else -4.
184 s = math.exp(s*0.01) 185 with self.open_context(True): 186 self._apply(glScalef,s,s,s) 187 self.queue_draw() 188
189 @classmethod 190 def event_masked(cls,event,mask): 191 return (event.state & mask) == mask
192
193 @classmethod 194 def _button_check(cls,event,button,mask): 195 # this shouldn't be so crazy complicated 196 if event.button == button: 197 return (event.type == gdk.BUTTON_PRESS)
198 return cls.event_masked(event,mask)
199
200 @classmethod 201 def get_left_button_down(cls,event): 202 return cls._button_check(event,1,gdk.BUTTON1_MASK) 203
204 @classmethod 205 def get_middle_button_down(cls,event): 206 return cls._button_check(event,2,gdk.BUTTON2_MASK) 207 208 @classmethod 209 def get_right_button_down(cls,event): 210 return cls._button_check(event,3,gdk.BUTTON3_MASK) 211 212 def _mouseButton(self,widget,event): 213 left = self.get_left_button_down(event) 214 middle = self.get_middle_button_down(event) 215 right = self.get_right_button_down(event) 216 self._mouseRotate = left and not (middle or right) 217 self._mouseZoom = middle or (left and right) 218 self._mousePan = right and self.event_masked(event,gdk.CONTROL_MASK) 219 x = self._mouseX = event.x
220 y = self._mouseY = event.y
221 self._dragPosX, self._dragPosY, self._dragPosZ = self._pos(x,y) 222 if (left and not self.event_masked(event,gdk.CONTROL_MASK)) and \
223 hasattr(self,"pick"): 224 with self.open_context(): 225 nearest, hits = \
226 self._pick(x,self.get_allocation().height-1-y,3,3,event) 227 self.pick(event,nearest,hits) # None if nothing hit 228 self.queue_draw() 229
230 def pick(self,event,nearest,hits): 231 print "picked",nearest
232 for hit in hits: 233 print hit.near, hit.far, hit.names
234
235 def _pos(self,x,y): 236 """ 237 Use the ortho projection and viewport information 238 to map from mouse co-ordinates back into world 239 co-ordinates 240 """
241 viewport = glGetIntegerv(GL_VIEWPORT) 242 px = float(x-viewport[0])/float(viewport[2]) 243 py = float(y-viewport[1])/float(viewport[3])
244 px = self._left + px*(self._right-self._left) 245 py = self._top + py*(self._bottom-self._top) 246 pz = self._zNear
247 return (px,py,pz) 248
249 def _pick(self,x,y,dx,dy,event): 250 buf = glSelectBuffer(256) 251 glRenderMode(GL_SELECT) 252 glInitNames() 253 glMatrixMode(GL_PROJECTION) 254 glPushMatrix() # remember projection matrix 255 viewport = glGetIntegerv(GL_VIEWPORT) 256 projection = glGetDoublev(GL_PROJECTION_MATRIX) 257 glLoadIdentity() 258 gluPickMatrix(x,y,dx,dy,viewport) 259 glMultMatrixd(projection)
260 glMatrixMode(GL_MODELVIEW) 261 glPushMatrix() 262 self.draw(event) 263 glPopMatrix() 264 hits = glRenderMode(GL_RENDER) 265 nearest = []
266 minZ = None 267 for hit in hits: 268 if (len(hit.names) > 0) and \
269 ((minZ is None) or (hit.near < minZ)): 270 minZ = hit.near
271 nearest = hit.names
272 glMatrixMode(GL_PROJECTION) 273 glPopMatrix() # restore projection matrix 274 glMatrixMode(GL_MODELVIEW) 275 return (nearest, hits) 276 277 def _draw(self,widget,event): 278 assert(self == widget)
279 with self.open_context() as ctx: 280 glMatrixMode(GL_MODELVIEW) 281 self.draw(event) ### implemented by subclasses 282 if ctx.surface.is_double_buffered(): 283 ctx.surface.swap_buffers() 284 else: 285 glFlush() 286 return True 287 288 def _demo_draw(event): 289 glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) 290 glScalef(0.25,0.25,0.25) 291 glPushMatrix() # No name for grey sphere 292 glColor3f(0.3,0.3,0.3) 293 glutSolidSphere(0.7, 20, 20) 294 glPopMatrix() 295 glPushMatrix()
296 glPushName(1) # Red cone is 3 297 glColor3f(1,0,0) 298 glRotatef(90,0,1,0) 299 glutSolidCone(0.6, 4.0, 20, 20) 300 glPopName() 301 glPopMatrix() 302 glPushMatrix()
303 glPushName(2) # Green cone is 2 304 glColor3f(0,1,0) 305 glRotatef(-90,1,0,0) 306 glutSolidCone(0.6, 4.0, 20, 20) 307 glPopName() 308 glPopMatrix() 309 glPushMatrix()
310 glColor3f(0,0,1) # Blue cone is 3 311 glPushName(3) 312 glutSolidCone(0.6, 4.0, 20, 20) 313 glPopName() 314 glPopMatrix() 315 316 317 if __name__ == '__main__': 318 import sys
319 glutInit(sys.argv) 320 gtk.gdk.threads_init() 321 window = gtk.Window(gtk.WINDOW_TOPLEVEL) 322 window.set_title("Zoom Pan Rotate") 323 window.set_size_request(640,480) 324 window.connect("destroy",lambda event: gtk.main_quit()) 325 vbox = gtk.VBox(False, 0) 326 window.add(vbox) 327 zpr = GLZPR() 328 zpr.draw = _demo_draw
329 vbox.pack_start(zpr,True,True) 330 window.show_all() 331 gtk.main() 332
Comments