1"""Traditional PyOpenGL interface to Togl 2 3This module provides access to the Tkinter Togl widget 4with a relatively "thick" wrapper API that creates a set 5of default "examination" operations. 6 7Note that many (most) Linux distributions have 8now split out the Tkinter bindings into a separate package, 9and that Togl must be separately downloaded (a script is 10provided in the source distribution which downloads and 11installs Togl 2.0 binaries for you). 12 13Because of the complexity and fragility of the installation, 14it is recommended that you use Pygame, wxPython, PyGtk, or 15PyQt for real-world projects, and GLUT or Pygame for simple 16demo/testing interfaces. 17 18The Togl project is located here: 19 20 http://togl.sourceforge.net/ 21""" 22# A class that creates an opengl widget. 23# Mike Hartshorn 24# Department of Chemistry 25# University of York, UK 26import os,sys, logging 27_log = logging.getLogger( 'OpenGL.Tk' ) 28from OpenGL.GL import * 29from OpenGL.GLU import * 30try: 31 from tkinter import _default_root 32 from tkinter import * 33 from tkinter import dialog 34except ImportError as err: 35 try: 36 from Tkinter import _default_root 37 from Tkinter import * 38 import Dialog as dialog 39 except ImportError as err: 40 _log.error( """Unable to import Tkinter, likely need to install a separate package (python-tk) to have Tkinter support. You likely also want to run the src/togl.py script in the PyOpenGL source distribution to install the Togl widget""" ) 41 raise 42import math 43 44def glTranslateScene(s, x, y, mousex, mousey): 45 glMatrixMode(GL_MODELVIEW) 46 mat = glGetDoublev(GL_MODELVIEW_MATRIX) 47 glLoadIdentity() 48 glTranslatef(s * (x - mousex), s * (mousey - y), 0.0) 49 glMultMatrixd(mat) 50 51 52def glRotateScene(s, xcenter, ycenter, zcenter, x, y, mousex, mousey): 53 glMatrixMode(GL_MODELVIEW) 54 mat = glGetDoublev(GL_MODELVIEW_MATRIX) 55 glLoadIdentity() 56 glTranslatef(xcenter, ycenter, zcenter) 57 glRotatef(s * (y - mousey), 1., 0., 0.) 58 glRotatef(s * (x - mousex), 0., 1., 0.) 59 glTranslatef(-xcenter, -ycenter, -zcenter) 60 glMultMatrixd(mat) 61 62 63def sub(x, y): 64 return list(map(lambda a, b: a-b, x, y)) 65 66 67def dot(x, y): 68 t = 0 69 for i in range(len(x)): 70 t = t + x[i]*y[i] 71 return t 72 73 74def glDistFromLine(x, p1, p2): 75 f = list(map(lambda x, y: x-y, p2, p1)) 76 g = list(map(lambda x, y: x-y, x, p1)) 77 return dot(g, g) - dot(f, g)**2/dot(f, f) 78 79 80# Keith Junius <junius@chem.rug.nl> provided many changes to Togl 81TOGL_NORMAL = 1 82TOGL_OVERLAY = 2 83 84def v3distsq(a,b): 85 d = ( a[0] - b[0], a[1] - b[1], a[2] - b[2] ) 86 return d[0]*d[0] + d[1]*d[1] + d[2]*d[2] 87 88# new version from Daniel Faken (Daniel_Faken@brown.edu) for static 89# loading comptability 90if _default_root is None: 91 _default_root = Tk() 92 93# Add this file's directory to Tcl's search path 94# This code is intended to work with the src/togl.py script 95# which will populate the directory with the appropriate 96# Binary Togl distribution. Note that Togl 2.0 and above 97# uses "stubs", so we don't care what Tk version we are using, 98# but that a different build is required for 64-bit Python. 99# Thus the directory structure is *not* the same as the 100# original PyOpenGL versions. 101if sys.maxsize > 2**32: 102 suffix = '-64' 103else: 104 suffix = '' 105try: 106 TOGL_DLL_PATH = os.path.join( 107 os.path.dirname(__file__), 108 'togl-'+ sys.platform + suffix, 109 ) 110except NameError as err: 111 # no __file__, likely running as an egg 112 TOGL_DLL_PATH = "" 113 114if not os.path.isdir( TOGL_DLL_PATH ): 115 _log.warning( 'Expected Tk Togl installation in %s', TOGL_DLL_PATH ) 116_log.info( 'Loading Togl from: %s', TOGL_DLL_PATH ) 117_default_root.tk.call('lappend', 'auto_path', TOGL_DLL_PATH) 118try: 119 _default_root.tk.call('package', 'require', 'Togl') 120 _default_root.tk.eval('load {} Togl') 121except TclError as err: 122 _log.error( """Failure loading Togl package: %s, on debian systems this is provided by `libtogl2`""", err ) 123 if _default_root: 124 _default_root.destroy() 125 raise 126 127# This code is needed to avoid faults on sys.exit() 128# [DAA, Jan 1998], updated by mcfletch 2009 129import atexit 130def cleanup(): 131 try: 132 import tkinter 133 except ImportError: 134 import Tkinter as tkinter 135 try: 136 if tkinter._default_root: tkinter._default_root.destroy() 137 except (TclError,AttributeError): 138 pass 139 tkinter._default_root = None 140atexit.register( cleanup ) 141 142class Togl(Widget): 143 """ 144 Togl Widget 145 Keith Junius 146 Department of Biophysical Chemistry 147 University of Groningen, The Netherlands 148 Very basic widget which provides access to Togl functions. 149 """ 150 def __init__(self, master=None, cnf={}, **kw): 151 Widget.__init__(self, master, 'togl', cnf, kw) 152 153 154 def render(self): 155 return 156 157 158 def swapbuffers(self): 159 self.tk.call(self._w, 'swapbuffers') 160 161 162 def makecurrent(self): 163 self.tk.call(self._w, 'makecurrent') 164 165 166 def alloccolor(self, red, green, blue): 167 return self.tk.getint(self.tk.call(self._w, 'alloccolor', red, green, blue)) 168 169 170 def freecolor(self, index): 171 self.tk.call(self._w, 'freecolor', index) 172 173 174 def setcolor(self, index, red, green, blue): 175 self.tk.call(self._w, 'setcolor', index, red, green, blue) 176 177 178 def loadbitmapfont(self, fontname): 179 return self.tk.getint(self.tk.call(self._w, 'loadbitmapfont', fontname)) 180 181 182 def unloadbitmapfont(self, fontbase): 183 self.tk.call(self._w, 'unloadbitmapfont', fontbase) 184 185 186 def uselayer(self, layer): 187 self.tk.call(self._w, 'uselayer', layer) 188 189 190 def showoverlay(self): 191 self.tk.call(self._w, 'showoverlay') 192 193 194 def hideoverlay(self): 195 self.tk.call(self._w, 'hideoverlay') 196 197 198 def existsoverlay(self): 199 return self.tk.getboolean(self.tk.call(self._w, 'existsoverlay')) 200 201 202 def getoverlaytransparentvalue(self): 203 return self.tk.getint(self.tk.call(self._w, 'getoverlaytransparentvalue')) 204 205 206 def ismappedoverlay(self): 207 return self.tk.getboolean(self.tk.call(self._w, 'ismappedoverlay')) 208 209 210 def alloccoloroverlay(self, red, green, blue): 211 return self.tk.getint(self.tk.call(self._w, 'alloccoloroverlay', red, green, blue)) 212 213 214 def freecoloroverlay(self, index): 215 self.tk.call(self._w, 'freecoloroverlay', index) 216 217 218 219class RawOpengl(Widget, Misc): 220 """Widget without any sophisticated bindings\ 221 by Tom Schwaller""" 222 223 224 def __init__(self, master=None, cnf={}, **kw): 225 Widget.__init__(self, master, 'togl', cnf, kw) 226 self.bind('<Map>', self.tkMap) 227 self.bind('<Expose>', self.tkExpose) 228 self.bind('<Configure>', self.tkExpose) 229 230 231 def tkRedraw(self, *dummy): 232 # This must be outside of a pushmatrix, since a resize event 233 # will call redraw recursively. 234 self.update_idletasks() 235 self.tk.call(self._w, 'makecurrent') 236 _mode = glGetDoublev(GL_MATRIX_MODE) 237 try: 238 glMatrixMode(GL_PROJECTION) 239 glPushMatrix() 240 try: 241 self.redraw() 242 glFlush() 243 finally: 244 glPopMatrix() 245 finally: 246 glMatrixMode(_mode) 247 self.tk.call(self._w, 'swapbuffers') 248 249 250 def tkMap(self, *dummy): 251 self.tkExpose() 252 253 254 def tkExpose(self, *dummy): 255 self.tkRedraw() 256 257 258 259 260class Opengl(RawOpengl): 261 """\ 262Tkinter bindings for an Opengl widget. 263Mike Hartshorn 264Department of Chemistry 265University of York, UK 266http://www.yorvic.york.ac.uk/~mjh/ 267""" 268 269 def __init__(self, master=None, cnf={}, **kw): 270 """\ 271 Create an opengl widget. 272 Arrange for redraws when the window is exposed or when 273 it changes size.""" 274 275 #Widget.__init__(self, master, 'togl', cnf, kw) 276 RawOpengl.__init__(*(self, master, cnf), **kw) 277 self.initialised = 0 278 279 # Current coordinates of the mouse. 280 self.xmouse = 0 281 self.ymouse = 0 282 283 # Where we are centering. 284 self.xcenter = 0.0 285 self.ycenter = 0.0 286 self.zcenter = 0.0 287 288 # The _back color 289 self.r_back = 1. 290 self.g_back = 0. 291 self.b_back = 1. 292 293 # Where the eye is 294 self.distance = 10.0 295 296 # Field of view in y direction 297 self.fovy = 30.0 298 299 # Position of clipping planes. 300 self.near = 0.1 301 self.far = 1000.0 302 303 # Is the widget allowed to autospin? 304 self.autospin_allowed = 0 305 306 # Is the widget currently autospinning? 307 self.autospin = 0 308 309 # Basic bindings for the virtual trackball 310 self.bind('<Map>', self.tkMap) 311 self.bind('<Expose>', self.tkExpose) 312 self.bind('<Configure>', self.tkExpose) 313 self.bind('<Shift-Button-1>', self.tkHandlePick) 314 #self.bind('<Button-1><ButtonRelease-1>', self.tkHandlePick) 315 self.bind('<Button-1>', self.tkRecordMouse) 316 self.bind('<B1-Motion>', self.tkTranslate) 317 self.bind('<Button-2>', self.StartRotate) 318 self.bind('<B2-Motion>', self.tkRotate) 319 self.bind('<ButtonRelease-2>', self.tkAutoSpin) 320 self.bind('<Button-3>', self.tkRecordMouse) 321 self.bind('<B3-Motion>', self.tkScale) 322 323 324 def help(self): 325 """Help for the widget.""" 326 327 d = dialog.Dialog(None, {'title': 'Viewer help', 328 'text': 'Button-1: Translate\n' 329 'Button-2: Rotate\n' 330 'Button-3: Zoom\n' 331 'Reset: Resets transformation to identity\n', 332 'bitmap': 'questhead', 333 'default': 0, 334 'strings': ('Done', 'Ok')}) 335 assert d 336 337 def activate(self): 338 """Cause this Opengl widget to be the current destination for drawing.""" 339 340 self.tk.call(self._w, 'makecurrent') 341 342 343 # This should almost certainly be part of some derived class. 344 # But I have put it here for convenience. 345 def basic_lighting(self): 346 """\ 347 Set up some basic lighting (single infinite light source). 348 349 Also switch on the depth buffer.""" 350 351 self.activate() 352 light_position = (1, 1, 1, 0) 353 glLightfv(GL_LIGHT0, GL_POSITION, light_position) 354 glEnable(GL_LIGHTING) 355 glEnable(GL_LIGHT0) 356 glDepthFunc(GL_LESS) 357 glEnable(GL_DEPTH_TEST) 358 glMatrixMode(GL_MODELVIEW) 359 glLoadIdentity() 360 361 def set_background(self, r, g, b): 362 """Change the background colour of the widget.""" 363 364 self.r_back = r 365 self.g_back = g 366 self.b_back = b 367 368 self.tkRedraw() 369 370 371 def set_centerpoint(self, x, y, z): 372 """Set the new center point for the model. 373 This is where we are looking.""" 374 375 self.xcenter = x 376 self.ycenter = y 377 self.zcenter = z 378 379 self.tkRedraw() 380 381 382 def set_eyepoint(self, distance): 383 """Set how far the eye is from the position we are looking.""" 384 385 self.distance = distance 386 self.tkRedraw() 387 388 389 def reset(self): 390 """Reset rotation matrix for this widget.""" 391 392 glMatrixMode(GL_MODELVIEW) 393 glLoadIdentity() 394 self.tkRedraw() 395 396 397 def tkHandlePick(self, event): 398 """Handle a pick on the scene.""" 399 400 if hasattr(self, 'pick'): 401 # here we need to use glu.UnProject 402 403 # Tk and X have their origin top left, 404 # while Opengl has its origin bottom left. 405 # So we need to subtract y from the window height to get 406 # the proper pick position for Opengl 407 408 realy = self.winfo_height() - event.y 409 410 p1 = gluUnProject(event.x, realy, 0.) 411 p2 = gluUnProject(event.x, realy, 1.) 412 413 if self.pick(self, p1, p2): 414 """If the pick method returns true we redraw the scene.""" 415 416 self.tkRedraw() 417 418 419 def tkRecordMouse(self, event): 420 """Record the current mouse position.""" 421 422 self.xmouse = event.x 423 self.ymouse = event.y 424 425 426 def StartRotate(self, event): 427 # Switch off any autospinning if it was happening 428 429 self.autospin = 0 430 self.tkRecordMouse(event) 431 432 433 def tkScale(self, event): 434 """Scale the scene. Achieved by moving the eye position. 435 436 Dragging up zooms in, while dragging down zooms out 437 """ 438 scale = 1 - 0.01 * (event.y - self.ymouse) 439 # do some sanity checks, scale no more than 440 # 1:1000 on any given click+drag 441 if scale < 0.001: 442 scale = 0.001 443 elif scale > 1000: 444 scale = 1000 445 self.distance = self.distance * scale 446 self.tkRedraw() 447 self.tkRecordMouse(event) 448 449 450 def do_AutoSpin(self): 451 self.activate() 452 453 glRotateScene(0.5, self.xcenter, self.ycenter, self.zcenter, self.yspin, self.xspin, 0, 0) 454 self.tkRedraw() 455 456 if self.autospin: 457 self.after(10, self.do_AutoSpin) 458 459 460 def tkAutoSpin(self, event): 461 """Perform autospin of scene.""" 462 463 self.after(4) 464 self.update_idletasks() 465 466 # This could be done with one call to pointerxy but I'm not sure 467 # it would any quicker as we would have to split up the resulting 468 # string and then conv 469 470 x = self.tk.getint(self.tk.call('winfo', 'pointerx', self._w)) 471 y = self.tk.getint(self.tk.call('winfo', 'pointery', self._w)) 472 473 if self.autospin_allowed: 474 if x != event.x_root and y != event.y_root: 475 self.autospin = 1 476 477 self.yspin = x - event.x_root 478 self.xspin = y - event.y_root 479 480 self.after(10, self.do_AutoSpin) 481 482 483 def tkRotate(self, event): 484 """Perform rotation of scene.""" 485 486 self.activate() 487 glRotateScene(0.5, self.xcenter, self.ycenter, self.zcenter, event.x, event.y, self.xmouse, self.ymouse) 488 self.tkRedraw() 489 self.tkRecordMouse(event) 490 491 492 def tkTranslate(self, event): 493 """Perform translation of scene.""" 494 495 self.activate() 496 497 # Scale mouse translations to object viewplane so object tracks with mouse 498 win_height = max( 1,self.winfo_height() ) 499 obj_c = ( self.xcenter, self.ycenter, self.zcenter ) 500 win = gluProject( obj_c[0], obj_c[1], obj_c[2]) 501 obj = gluUnProject( win[0], win[1] + 0.5 * win_height, win[2]) 502 dist = math.sqrt( v3distsq( obj, obj_c ) ) 503 scale = abs( dist / ( 0.5 * win_height ) ) 504 505 glTranslateScene(scale, event.x, event.y, self.xmouse, self.ymouse) 506 self.tkRedraw() 507 self.tkRecordMouse(event) 508 509 510 def tkRedraw(self, *dummy): 511 """Cause the opengl widget to redraw itself.""" 512 513 if not self.initialised: return 514 self.activate() 515 516 glPushMatrix() # Protect our matrix 517 self.update_idletasks() 518 self.activate() 519 w = self.winfo_width() 520 h = self.winfo_height() 521 glViewport(0, 0, w, h) 522 523 # Clear the background and depth buffer. 524 glClearColor(self.r_back, self.g_back, self.b_back, 0.) 525 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) 526 527 glMatrixMode(GL_PROJECTION) 528 glLoadIdentity() 529 gluPerspective(self.fovy, float(w)/float(h), self.near, self.far) 530 531 if 0: 532 # Now translate the scene origin away from the world origin 533 glMatrixMode(GL_MODELVIEW) 534 mat = glGetDoublev(GL_MODELVIEW_MATRIX) 535 glLoadIdentity() 536 glTranslatef(-self.xcenter, -self.ycenter, -(self.zcenter+self.distance)) 537 glMultMatrixd(mat) 538 else: 539 gluLookAt(self.xcenter, self.ycenter, self.zcenter + self.distance, 540 self.xcenter, self.ycenter, self.zcenter, 541 0., 1., 0.) 542 glMatrixMode(GL_MODELVIEW) 543 544 # Call objects redraw method. 545 self.redraw(self) 546 glFlush() # Tidy up 547 glPopMatrix() # Restore the matrix 548 549 self.tk.call(self._w, 'swapbuffers') 550 def redraw( self, *args, **named ): 551 """Prevent access errors if user doesn't set redraw fast enough""" 552 553 554 def tkMap(self, *dummy): 555 """Cause the opengl widget to redraw itself.""" 556 557 self.tkExpose() 558 559 560 def tkExpose(self, *dummy): 561 """Redraw the widget. 562 Make it active, update tk events, call redraw procedure and 563 swap the buffers. Note: swapbuffers is clever enough to 564 only swap double buffered visuals.""" 565 566 self.activate() 567 if not self.initialised: 568 self.basic_lighting() 569 self.initialised = 1 570 self.tkRedraw() 571 572 573 def tkPrint(self, file): 574 """Turn the current scene into PostScript via the feedback buffer.""" 575 576 self.activate() 577