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