source: sasview/src/sas/sasgui/perspectives/simulation/SimCanvas.py @ 34f23c8

ticket-1249
Last change on this file since 34f23c8 was 34f23c8, checked in by Paul Kienzle <pkienzle@…>, 3 months ago

py3/wx4 compatibility changes for gui. Refs #1249

  • Property mode set to 100644
File size: 19.1 KB
Line 
1"""
2This software was developed by the University of Tennessee as part of the
3Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
4project funded by the US National Science Foundation.
5
6See the license text in license.txt
7
8copyright 2009, University of Tennessee
9"""
10import wx
11import math
12
13try:
14    import OpenGL
15except:
16    import sys, os
17    sys.path.insert(1,os.path.dirname(sys.executable))
18
19import OpenGL.GL
20try:
21    from wx import glcanvas
22    haveGLCanvas = True
23except ImportError:
24    haveGLCanvas = False
25try:
26    # The Python OpenGL package can be found at
27    # http://PyOpenGL.sourceforge.net/
28    from OpenGL.GL import *
29    from OpenGL.GLU import *
30    from OpenGL.GLUT import *
31    haveOpenGL = True
32except ImportError:
33    haveOpenGL = False
34
35# Color set
36DEFAULT_COLOR = [1.0, 1.0, 0.0, .2]
37COLOR_RED     = [1.0, 0.0, 0.0, .2]
38COLOR_GREEN   = [0.0, 1.0, 0.0, .2]
39COLOR_BLUE    = [0.0, 0.0, 1.0, .2]
40COLOR_YELLOW  = [1.0, 1.0, 0.0, .2]
41COLOR_HIGHLIGHT = [.2, .2, .8, .5]
42
43# List of shapes
44SHAPE_LIST = []
45
46import ShapeParameters
47
48class SimPanel(wx.Panel):
49    """
50        3D viewer to support real-space simulation.
51    """
52    window_name = "3dview"
53    window_caption = "3D viewer"
54
55    def __init__(self, parent, id = -1, plots = None, standalone=False, **kwargs):
56        wx.Panel.__init__(self, parent, id = id, **kwargs)
57        self.parent = parent
58
59        #Add a sizer
60        mainSizer = wx.BoxSizer(wx.VERTICAL)
61        sliderSizer = wx.BoxSizer(wx.HORIZONTAL)
62
63        self.canvas    = CanvasBase(self)
64        self.canvas.SetMinSize((200,2100))
65
66        #Add the model to it's sizer
67        mainSizer.Add(self.canvas, 1, wx.EXPAND)
68
69        #Add the Subsizers to mainsizer
70        mainSizer.Add(sliderSizer, 0, wx.EXPAND)
71
72        self.SetSizer(mainSizer)
73        self.Bind(wx.EVT_CONTEXT_MENU, self._on_context_menu)
74
75
76    def _on_context_menu(self, event):
77        """
78            Default context menu for a plot panel
79        """
80        # Slicer plot popup menu
81        id = wx.NewId()
82        slicerpop = wx.Menu()
83        slicerpop.Append(id, '&Reset 3D View')
84        wx.EVT_MENU(self, id, self.canvas.resetView)
85
86        pos = event.GetPosition()
87        pos = self.ScreenToClient(pos)
88        self.PopupMenu(slicerpop, pos)
89
90class CanvasBase(glcanvas.GLCanvas):
91    """
92        3D canvas to display OpenGL 3D shapes
93    """
94    window_name = "Simulation"
95    window_caption = "Simulation"
96
97    def __init__(self, parent):
98        glcanvas.GLCanvas.__init__(self, parent, -1)
99        self.init = False
100        # initial mouse position
101        self.lastx = self.x = 30
102        self.lasty = self.y = 30
103        self.tr_lastx = self.tr_x = 30
104        self.tr_lasty = self.tr_y = 30
105        self.size = None
106        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
107        self.Bind(wx.EVT_SIZE, self.OnSize)
108        self.Bind(wx.EVT_PAINT, self.OnPaint)
109        self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown)
110        self.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseDown)
111        self.Bind(wx.EVT_LEFT_UP, self.OnMouseUp)
112        self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp)
113        self.Bind(wx.EVT_MOTION, self.OnMouseMotion)
114        self.Bind(wx.EVT_MOUSEWHEEL, self._onMouseWheel)
115
116        self.initialized = False
117        self.shapes = []
118        self.parent = parent
119
120        # Reference vectors
121        self.x_vec = [1,0,0]
122        self.y_vec = [0,1,0]
123
124        self.mouse_down = False
125        self.scale = 1.0
126        self.zoom  = 1.0
127        self.translation = [0,0,0]
128
129        # Bind to Edit events
130        self.parent.Bind(ShapeParameters.EVT_EDIT_SHAPE, self._onEditShape)
131
132    def resetView(self, evt=None):
133        """
134            Resets zooming, translation and rotation.
135        """
136        self.zoom = 1.0
137        self.scale = 1.0
138        self.x_vec = [1,0,0]
139        self.y_vec = [0,1,0]
140        glLoadIdentity()
141        self.Refresh()
142
143    def _onEditShape(self, evt):
144        evt.Skip()
145        evt.shape.highlight(True)
146        for item in self.shapes:
147            if not item == evt.shape:
148                item.highlight(False)
149        self.Refresh(False)
150
151    def OnEraseBackground(self, event):
152        pass # Do nothing, to avoid flashing on MSW.
153
154    def OnSize(self, event):
155        size = self.size = self.GetClientSize()
156        if self.GetContext():
157            glViewport(0, 0, size.width, size.height)
158        event.Skip()
159
160    def OnPaint(self, event):
161        size = self.GetClientSize()
162        side = size.width
163        if size.height<size.width:
164            side = size.height
165
166        if self.GetContext():
167            glViewport(0, 0, side, side)
168            self.SetMinSize((side,side))
169
170        dc = wx.PaintDC(self)
171        self.SetCurrent()
172        if not self.init:
173            self.InitGL()
174            self.init = True
175        self.OnDraw()
176        event.Skip()
177
178    def _onMouseWheel(self, evt):
179        # Initialize mouse position so we don't have unwanted rotation
180        self.x, self.y = self.lastx, self.lasty = evt.GetPosition()
181        self.tr_x, self.tr_y = self.tr_lastx, self.tr_lasty = evt.GetPosition()
182
183        scale = 1.15
184        if evt.GetWheelRotation()<0:
185            scale = 1.0/scale
186
187        self.zoom *= scale
188
189        glScale(scale, scale, scale)
190        self.Refresh(False)
191
192    def OnMouseDown(self, evt):
193        self.SetFocus()
194        self.CaptureMouse()
195        self.x, self.y = self.lastx, self.lasty = evt.GetPosition()
196        self.tr_x, self.tr_y = self.tr_lastx, self.tr_lasty = self.x, self.y
197        self.mouse_down = True
198
199    def OnMouseUp(self, evt):
200        self.ReleaseMouse()
201        self.mouse_down = False
202
203    def OnRightUp(self, evt):
204        self.OnMouseUp(evt)
205        if self.x==self.tr_lastx and self.y==self.tr_lasty:
206            self._on_context_menu(evt)
207
208    def _on_context_menu(self, event):
209        """
210            Default context menu for a plot panel
211        """
212        id = wx.NewId()
213        slicerpop = wx.Menu()
214        slicerpop.Append(id, '&Reset 3D View')
215        wx.EVT_MENU(self, id, self.resetView)
216
217        pos = event.GetPosition()
218        #pos = self.ScreenToClient(pos)
219        self.PopupMenu(slicerpop, pos)
220
221    def OnMouseMotion(self, evt):
222        if evt.Dragging() and evt.LeftIsDown():
223            self.tr_lastx, self.tr_lasty = self.tr_x, self.tr_y
224            x, y = evt.GetPosition()
225
226            # Min distance to do anything
227            if math.fabs(self.lastx-x)>10 or math.fabs(self.lasty-y)>10:
228
229                self.lastx, self.lasty = self.x, self.y
230
231
232                if math.fabs(self.lastx-x)>math.fabs(self.lasty-y):
233                    self.x = x
234                else:
235                    self.y = y
236
237                #self.x, self.y = evt.GetPosition()
238                self.Refresh(False)
239
240        elif evt.Dragging() and evt.RightIsDown():
241            self.lastx, self.lasty = self.x, self.y
242            self.tr_lastx, self.tr_lasty = self.tr_x, self.tr_y
243            self.tr_x, self.tr_y = evt.GetPosition()
244            self.Refresh(False)
245
246    def InitGL( self ):
247        glutInitDisplayMode (GLUT_DOUBLE | GLUT_RGBA)
248        #glShadeModel(GL_FLAT);
249
250        glMatrixMode(GL_PROJECTION)
251
252        glLight(GL_LIGHT0, GL_AMBIENT, [.2, .2, .2, 0])
253
254        # Object color
255        #glLight(GL_LIGHT0, GL_DIFFUSE, COLOR_BLUE)
256
257        glLight(GL_LIGHT0, GL_POSITION, [1.0, 1.0, -1.0, 0.0])
258
259
260        glLightModelfv(GL_LIGHT_MODEL_AMBIENT, [1, 1, 1, 0])
261        glEnable(GL_LIGHTING)
262        glEnable(GL_LIGHT0)
263        glEnable(GL_BLEND)
264        glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
265
266        glEnable ( GL_ALPHA_TEST ) ;
267        glAlphaFunc ( GL_GREATER, 0 ) ;
268        glPixelStorei(GL_PACK_ALIGNMENT, 1)
269        glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
270
271        glEnable(GL_NORMALIZE)
272        glDepthFunc(GL_LESS)
273        glEnable(GL_DEPTH_TEST)
274        glDepthMask(GL_TRUE);
275        glClearColor (1.0, 1.0, 1.0, 0.0);
276        #glClear (GL_COLOR_BUFFER_BIT);
277        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
278
279
280        gluPerspective(0, 1.0, 0.1, 60.0);
281
282        glMatrixMode(GL_MODELVIEW)
283
284    def getMaxSize(self):
285        max_size = 0.0
286        # Ready to draw shapes
287        for item in self.shapes:
288            item.draw()
289            l = item.get_length()
290            if l>max_size:
291                max_size = l
292        return max_size
293
294    def OnDraw(self):
295        # clear color and depth buffers
296        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
297        # use a fresh transformation matrix
298
299        # get the max object size to re-scale
300        max_size = 1.0
301        # Ready to draw shapes
302        for item in self.shapes:
303            item.draw()
304            l = item.get_length()
305            if l>max_size:
306                max_size = l
307
308        max_size *= 1.05
309        scale = self.scale/max_size
310        glScale(scale, scale, scale)
311        self.scale = max_size
312
313        self.drawAxes()
314
315        if self.mouse_down:
316            if math.fabs((self.x - self.lastx))>math.fabs((self.y - self.lasty)):
317                angle = 10.0*(self.x - self.lastx) / math.fabs(self.x - self.lastx)
318                glRotate(angle, self.y_vec[0], self.y_vec[1], self.y_vec[2]);
319                self._rot_y(angle)
320            elif math.fabs(self.y - self.lasty)>0:
321                angle = 10.0*(self.y - self.lasty) / math.fabs(self.y - self.lasty)
322                glRotate(angle, self.x_vec[0], self.x_vec[1], self.x_vec[2]);
323                self._rot_x(angle)
324
325            self.lasty = self.y
326            self.lastx = self.x
327
328            w, h = self.GetVirtualSize()
329
330            # Translate in the x-y plane
331            vx = self.x_vec[0] * 2.0*float(self.tr_x - self.tr_lastx)/w \
332               + self.y_vec[0] * 2.0*float(self.tr_lasty - self.tr_y)/h
333            vy = self.x_vec[1] * 2.0*float(self.tr_x - self.tr_lastx)/w \
334               + self.y_vec[1] * 2.0*float(self.tr_lasty - self.tr_y)/h
335            vz = self.x_vec[2] * 2.0*float(self.tr_x - self.tr_lastx)/w \
336               + self.y_vec[2] * 2.0*float(self.tr_lasty - self.tr_y)/h
337
338            glTranslate(self.scale*vx/self.zoom, self.scale*vy/self.zoom, self.scale*vz/self.zoom)
339
340        # push into visible buffer
341        self.SwapBuffers()
342
343    def _matrix_mult(self, v, axis, angle):
344        c = math.cos(angle)
345        s = math.sin(angle)
346        x = axis[0]
347        y = axis[1]
348        z = axis[2]
349        vx = v[0]*(x*x*(1-c)+c)   + v[1]*(x*y*(1-c)-z*s) + v[2]*(x*z*(1-c)+y*s)
350        vy = v[0]*(y*x*(1-c)+z*s) + v[1]*(y*y*(1-c)+c)   + v[2]*(y*z*(1-c)-x*s)
351        vz = v[0]*(x*z*(1-c)-y*s) + v[1]*(y*z*(1-c)+x*s) + v[2]*(z*z*(1-c)+c)
352        return [vx, vy, vz]
353
354    def _rot_y(self, theta):
355        """
356            Rotate the view by theta around the y-axis
357        """
358        angle = theta/180.0*math.pi
359        axis = self.y_vec
360        self.x_vec = self._matrix_mult(self.x_vec, self.y_vec, -angle)
361
362    def _rot_x(self, theta):
363        """
364            Rotate the view by theta around the x-axis
365        """
366        angle = theta/180.0*math.pi
367        self.y_vec = self._matrix_mult(self.y_vec, self.x_vec, -angle)
368
369
370    def addShape(self, shape, name=None):
371        """
372            Add a shape to the list of displayed shapes
373            @param shape: BaseShape object
374            @param name: name given to the shape
375        """
376        if not name==None:
377            shape.name = name
378        shape.params['order']=len(self.shapes)
379        self.shapes.append(shape)
380        self.Refresh(False)
381
382    def delShape(self, name):
383        """
384            Delete a shape by name
385            @param name: name of the shape to be deleted
386        """
387        for i in range(len(self.shapes)):
388            if self.shapes[i].name == name:
389                del self.shapes[i]
390                break
391        self.Refresh(False)
392
393    def getMaxVolume(self):
394        """
395            Returns the maximum volume of the combination of all shapes.
396            The maximum volume is the simple sum of the volumes of all the shapes.
397            @return: sum of the volumes of all the shapes [float]
398        """
399        sum = 0
400        for item in self.shapes:
401            sum += item.get_volume()
402        return sum
403
404    def drawAxes(self):
405        """
406            Draw 3D axes
407        """
408        pos = self.scale * 0.7
409
410        # Z-axis is red
411        zaxis= Arrow(x=pos, y=-pos, z=0, r_cyl=self.scale*0.005, r_cone=self.scale*0.01,
412                     l_cyl=self.scale*0.1, l_cone=self.scale*0.05)
413        zaxis.color = COLOR_RED
414        zaxis.draw()
415
416        # Y-axis is yellow
417        yaxis= Arrow(x=pos, y=-pos, z=0, r_cyl=self.scale*0.005, r_cone=self.scale*0.01,
418                     l_cyl=self.scale*0.1, l_cone=self.scale*0.05)
419        yaxis.color = COLOR_YELLOW
420        yaxis.rotate(-90,0,0)
421        yaxis.draw()
422
423        # X-axis is green
424        xaxis= Arrow(x=pos, y=-pos, z=0, r_cyl=self.scale*0.005, r_cone=self.scale*0.01,
425                     l_cyl=self.scale*0.1, l_cone=self.scale*0.05)
426        xaxis.color = COLOR_GREEN
427        xaxis.rotate(0,-90,0)
428        xaxis.draw()
429
430        glLight(GL_LIGHT0, GL_DIFFUSE, DEFAULT_COLOR)
431
432
433class BaseShape:
434    """
435        Basic shape functionality
436    """
437    def __init__(self, x=0, y=0, z=0):
438        self.name = ''
439        ## Position
440        self.x = x
441        self.y = y
442        self.z = z
443        ## Orientation
444        self.theta_x = 0
445        self.theta_y = 0
446        self.theta_z = 0
447
448        # Params
449        self.params = {}
450        self.params['contrast']  = 1.0
451        self.params['order']     = 0
452        self.details = {}
453        self.details['contrast'] = 'A-2'
454        self.details['order']    = ' '
455
456        self.highlighted = False
457        self.color = DEFAULT_COLOR
458
459    def get_volume(self):
460        return 0
461
462    def get_length(self):
463        return 1.0
464
465    def highlight(self, value=False):
466        self.highlighted = value
467
468    def rotate(self, alpha, beta, gamma):
469        """
470            Set the rotation angles of the shape
471            @param alpha: angle around the x-axis [degrees]
472            @param beta: angle around the y-axis [degrees]
473            @param gamma: angle around the z-axis [degrees]
474        """
475        self.theta_x = alpha
476        self.theta_y = beta
477        self.theta_z = gamma
478
479    def _rotate(self):
480        """
481            Perform the OpenGL rotation
482
483            Note that the rotation order is reversed.
484            We do Y, X, Z do be compatible with simulation...
485        """
486
487        glRotated(self.theta_z, 0, 0, 1)
488        glRotated(self.theta_x, 1, 0, 0)
489        glRotated(self.theta_y, 0, 1, 0)
490
491    def _pre_draw(self):
492        if self.highlighted:
493            glLight(GL_LIGHT0, GL_DIFFUSE, COLOR_HIGHLIGHT)
494        else:
495            glLight(GL_LIGHT0, GL_DIFFUSE, self.color)
496
497
498class Arrow(BaseShape):
499    """
500        Arrow shape used to show the three axes
501    """
502    def __init__(self, x=0, y=0, z=0, r_cyl=0.01, r_cone=0.02, l_cyl=0.3, l_cone=0.1):
503        BaseShape.__init__(self, x, y, z)
504        self.r_cyl = r_cyl
505        self.r_cone = r_cone
506        self.l_cyl = l_cyl
507        self.l_cone = l_cone
508
509    def draw(self):
510        self._pre_draw()
511        glPushMatrix()
512        glTranslate(self.x, self.y, self.z)
513
514        # Perform rotation
515        glRotate(self.theta_x, 1, 0, 0)
516        glRotate(self.theta_y, 0, 1, 0)
517        glRotate(self.theta_z, 0, 0, 1)
518
519        # Draw axis cylinder
520        qobj = gluNewQuadric();
521        gluCylinder(qobj, self.r_cyl, self.r_cyl, self.l_cyl, 15, 5);
522
523        glTranslate(0, 0, self.z+self.l_cyl)
524
525        # Draw cone of the arrow
526        glutSolidCone(self.r_cone, self.l_cone, 30, 5)
527
528        # Translate back to original position
529        glTranslate(-self.x, -self.y, -self.z)
530        glPopMatrix()
531
532class Cone(BaseShape):
533    """
534        Conical shape
535    """
536    def __init__(self, x=0, y=0, z=0, radius=0.5, height=1):
537        BaseShape.__init__(self, x, y, z)
538        self.radius = radius
539        self.height = height
540
541    def draw(self):
542        glPushMatrix()
543        glTranslate(self.x, self.y, self.z)
544        glutSolidCone(self.radius, self.height, 30, 5)
545        self._rotate()
546        glTranslate(-self.x, -self.y, -self.z)
547        glPopMatrix()
548
549class Sphere(BaseShape):
550    """
551        Spherical shape
552    """
553    def __init__(self, x=0, y=0, z=0, radius=10.0):
554        BaseShape.__init__(self, x, y, z)
555        self.name = 'sphere'
556        self.params['radius'] = radius
557        self.details['radius'] = 'A'
558
559    def get_volume(self):
560        return 4.0/3.0*math.pi*self.params['radius']*self.params['radius']*self.params['radius']
561
562    def get_length(self):
563        return 2.0*self.params['radius']
564
565    def draw(self):
566        self._pre_draw()
567        glPushMatrix()
568        glTranslate(self.x, self.y, self.z)
569        #glutSolidSphere(self.params['radius'], 30, 30)
570        qobj = gluNewQuadric();
571        #gluQuadricDrawStyle(qobj,GLU_SILHOUETTE)
572        gluSphere(qobj,self.params['radius'], 30, 30)
573        glTranslate(-self.x, -self.y, -self.z)
574        glPopMatrix()
575        glLight(GL_LIGHT0, GL_DIFFUSE, DEFAULT_COLOR)
576
577    def accept(self, visitor):
578        return visitor.fromSphere(self)
579
580    def accept_update(self, visitor):
581        return visitor.update_sphere(self)
582
583class Cylinder(BaseShape):
584    """
585        Cylinder shape, by default the cylinder is oriented along
586        the z-axis.
587
588        The reference point of the cylinder is the center of the
589        bottom circle.
590    """
591    def __init__(self, x=0, y=0, z=0, radius=10.0, length=100.0):
592        BaseShape.__init__(self, x, y, z)
593        self.name = 'cylinder'
594        self.params['radius'] = radius
595        self.params['length'] = length
596        self.details['radius'] = 'A'
597        self.details['length'] = 'A'
598
599    def get_volume(self):
600        return math.pi*self.params['radius']*self.params['radius']*self.params['length']
601
602    def get_length(self):
603        if self.params['length']>2.0*self.params['radius']:
604            return self.params['length']
605        else:
606            return 2.0*self.params['radius']
607
608    def draw(self):
609        self._pre_draw()
610        glPushMatrix()
611
612        glTranslate(self.x, self.y, self.z)
613        self._rotate()
614        qobj = gluNewQuadric();
615        # gluCylinder(qobj, r_base, r_top, L, div around z, div along z)
616        gluCylinder(qobj, self.params['radius'], self.params['radius'], self.params['length'], 15, 5);
617
618        glTranslate(-self.x, -self.y, -self.z)
619        glPopMatrix()
620        glLight(GL_LIGHT0, GL_DIFFUSE, DEFAULT_COLOR)
621
622    def accept(self, visitor):
623        return visitor.fromCylinder(self)
624
625    def accept_update(self, visitor):
626        return visitor.update_cylinder(self)
627
628# Fill the shape list
629SHAPE_LIST.append(dict(name='Sphere',   id=wx.NewId(), cl=Sphere))
630SHAPE_LIST.append(dict(name='Cylinder', id=wx.NewId(), cl=Cylinder))
631
632def getShapes():
633    """
634        Returns a list of all available shapes
635    """
636    return SHAPE_LIST
637
638def getShapeClass(id):
639    """
640        Returns a child class of BaseShape corresponding
641        to the supplied shape ID.
642        @param id: shape ID number
643    """
644    def f(s): return s['id']==id
645    if SHAPE_LIST is not None:
646        shape = filter(f, SHAPE_LIST)
647        return shape[0]['cl']
648    return None
649
650def getShapeClassByName(name):
651    """
652        Returns a child class of BaseShape corresponding
653        to the supplied shape name.
654        @param name: shape name
655    """
656    def f(s): return s['name']==name
657    if SHAPE_LIST is not None:
658        shape = filter(f, SHAPE_LIST)
659        return shape[0]['cl']
660    return None
Note: See TracBrowser for help on using the repository browser.