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

magnetic_scattrelease-4.2.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since f1d48e9 was 959eb01, checked in by ajj, 8 years ago

normalising line endings

  • Property mode set to 100644
File size: 19.9 KB
RevLine 
[959eb01]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.GetVirtualSizeTuple()
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.