""" This software was developed by the University of Tennessee as part of the Distributed Data Analysis of Neutron Scattering Experiments (DANSE) project funded by the US National Science Foundation. See the license text in license.txt copyright 2009, University of Tennessee """ import wx import math try: import OpenGL except: import sys, os sys.path.insert(1,os.path.dirname(sys.executable)) import OpenGL.GL try: from wx import glcanvas haveGLCanvas = True except ImportError: haveGLCanvas = False try: # The Python OpenGL package can be found at # http://PyOpenGL.sourceforge.net/ from OpenGL.GL import * from OpenGL.GLU import * from OpenGL.GLUT import * haveOpenGL = True except ImportError: haveOpenGL = False # Color set DEFAULT_COLOR = [1.0, 1.0, 0.0, .2] COLOR_RED = [1.0, 0.0, 0.0, .2] COLOR_GREEN = [0.0, 1.0, 0.0, .2] COLOR_BLUE = [0.0, 0.0, 1.0, .2] COLOR_YELLOW = [1.0, 1.0, 0.0, .2] COLOR_HIGHLIGHT = [.2, .2, .8, .5] # List of shapes SHAPE_LIST = [] import ShapeParameters class SimPanel(wx.Panel): """ 3D viewer to support real-space simulation. """ window_name = "3dview" window_caption = "3D viewer" def __init__(self, parent, id = -1, plots = None, standalone=False, **kwargs): wx.Panel.__init__(self, parent, id = id, **kwargs) self.parent = parent #Add a sizer mainSizer = wx.BoxSizer(wx.VERTICAL) sliderSizer = wx.BoxSizer(wx.HORIZONTAL) self.canvas = CanvasBase(self) self.canvas.SetMinSize((200,2100)) #Add the model to it's sizer mainSizer.Add(self.canvas, 1, wx.EXPAND) #Add the Subsizers to mainsizer mainSizer.Add(sliderSizer, 0, wx.EXPAND) self.SetSizer(mainSizer) self.Bind(wx.EVT_CONTEXT_MENU, self._on_context_menu) def _on_context_menu(self, event): """ Default context menu for a plot panel """ # Slicer plot popup menu id = wx.NewId() slicerpop = wx.Menu() slicerpop.Append(id, '&Reset 3D View') wx.EVT_MENU(self, id, self.canvas.resetView) pos = event.GetPosition() pos = self.ScreenToClient(pos) self.PopupMenu(slicerpop, pos) class CanvasBase(glcanvas.GLCanvas): """ 3D canvas to display OpenGL 3D shapes """ window_name = "Simulation" window_caption = "Simulation" def __init__(self, parent): glcanvas.GLCanvas.__init__(self, parent, -1) self.init = False # initial mouse position self.lastx = self.x = 30 self.lasty = self.y = 30 self.tr_lastx = self.tr_x = 30 self.tr_lasty = self.tr_y = 30 self.size = None self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown) self.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseDown) self.Bind(wx.EVT_LEFT_UP, self.OnMouseUp) self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) self.Bind(wx.EVT_MOTION, self.OnMouseMotion) self.Bind(wx.EVT_MOUSEWHEEL, self._onMouseWheel) self.initialized = False self.shapes = [] self.parent = parent # Reference vectors self.x_vec = [1,0,0] self.y_vec = [0,1,0] self.mouse_down = False self.scale = 1.0 self.zoom = 1.0 self.translation = [0,0,0] # Bind to Edit events self.parent.Bind(ShapeParameters.EVT_EDIT_SHAPE, self._onEditShape) def resetView(self, evt=None): """ Resets zooming, translation and rotation. """ self.zoom = 1.0 self.scale = 1.0 self.x_vec = [1,0,0] self.y_vec = [0,1,0] glLoadIdentity() self.Refresh() def _onEditShape(self, evt): evt.Skip() evt.shape.highlight(True) for item in self.shapes: if not item == evt.shape: item.highlight(False) self.Refresh(False) def OnEraseBackground(self, event): pass # Do nothing, to avoid flashing on MSW. def OnSize(self, event): size = self.size = self.GetClientSize() if self.GetContext(): glViewport(0, 0, size.width, size.height) event.Skip() def OnPaint(self, event): size = self.GetClientSize() side = size.width if size.height10 or math.fabs(self.lasty-y)>10: self.lastx, self.lasty = self.x, self.y if math.fabs(self.lastx-x)>math.fabs(self.lasty-y): self.x = x else: self.y = y #self.x, self.y = evt.GetPosition() self.Refresh(False) elif evt.Dragging() and evt.RightIsDown(): self.lastx, self.lasty = self.x, self.y self.tr_lastx, self.tr_lasty = self.tr_x, self.tr_y self.tr_x, self.tr_y = evt.GetPosition() self.Refresh(False) def InitGL( self ): glutInitDisplayMode (GLUT_DOUBLE | GLUT_RGBA) #glShadeModel(GL_FLAT); glMatrixMode(GL_PROJECTION) glLight(GL_LIGHT0, GL_AMBIENT, [.2, .2, .2, 0]) # Object color #glLight(GL_LIGHT0, GL_DIFFUSE, COLOR_BLUE) glLight(GL_LIGHT0, GL_POSITION, [1.0, 1.0, -1.0, 0.0]) glLightModelfv(GL_LIGHT_MODEL_AMBIENT, [1, 1, 1, 0]) glEnable(GL_LIGHTING) glEnable(GL_LIGHT0) glEnable(GL_BLEND) glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable ( GL_ALPHA_TEST ) ; glAlphaFunc ( GL_GREATER, 0 ) ; glPixelStorei(GL_PACK_ALIGNMENT, 1) glPixelStorei(GL_UNPACK_ALIGNMENT, 1) glEnable(GL_NORMALIZE) glDepthFunc(GL_LESS) glEnable(GL_DEPTH_TEST) glDepthMask(GL_TRUE); glClearColor (1.0, 1.0, 1.0, 0.0); #glClear (GL_COLOR_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) gluPerspective(0, 1.0, 0.1, 60.0); glMatrixMode(GL_MODELVIEW) def getMaxSize(self): max_size = 0.0 # Ready to draw shapes for item in self.shapes: item.draw() l = item.get_length() if l>max_size: max_size = l return max_size def OnDraw(self): # clear color and depth buffers glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) # use a fresh transformation matrix # get the max object size to re-scale max_size = 1.0 # Ready to draw shapes for item in self.shapes: item.draw() l = item.get_length() if l>max_size: max_size = l max_size *= 1.05 scale = self.scale/max_size glScale(scale, scale, scale) self.scale = max_size self.drawAxes() if self.mouse_down: if math.fabs((self.x - self.lastx))>math.fabs((self.y - self.lasty)): angle = 10.0*(self.x - self.lastx) / math.fabs(self.x - self.lastx) glRotate(angle, self.y_vec[0], self.y_vec[1], self.y_vec[2]); self._rot_y(angle) elif math.fabs(self.y - self.lasty)>0: angle = 10.0*(self.y - self.lasty) / math.fabs(self.y - self.lasty) glRotate(angle, self.x_vec[0], self.x_vec[1], self.x_vec[2]); self._rot_x(angle) self.lasty = self.y self.lastx = self.x w, h = self.GetVirtualSize() # Translate in the x-y plane vx = self.x_vec[0] * 2.0*float(self.tr_x - self.tr_lastx)/w \ + self.y_vec[0] * 2.0*float(self.tr_lasty - self.tr_y)/h vy = self.x_vec[1] * 2.0*float(self.tr_x - self.tr_lastx)/w \ + self.y_vec[1] * 2.0*float(self.tr_lasty - self.tr_y)/h vz = self.x_vec[2] * 2.0*float(self.tr_x - self.tr_lastx)/w \ + self.y_vec[2] * 2.0*float(self.tr_lasty - self.tr_y)/h glTranslate(self.scale*vx/self.zoom, self.scale*vy/self.zoom, self.scale*vz/self.zoom) # push into visible buffer self.SwapBuffers() def _matrix_mult(self, v, axis, angle): c = math.cos(angle) s = math.sin(angle) x = axis[0] y = axis[1] z = axis[2] vx = v[0]*(x*x*(1-c)+c) + v[1]*(x*y*(1-c)-z*s) + v[2]*(x*z*(1-c)+y*s) vy = v[0]*(y*x*(1-c)+z*s) + v[1]*(y*y*(1-c)+c) + v[2]*(y*z*(1-c)-x*s) vz = v[0]*(x*z*(1-c)-y*s) + v[1]*(y*z*(1-c)+x*s) + v[2]*(z*z*(1-c)+c) return [vx, vy, vz] def _rot_y(self, theta): """ Rotate the view by theta around the y-axis """ angle = theta/180.0*math.pi axis = self.y_vec self.x_vec = self._matrix_mult(self.x_vec, self.y_vec, -angle) def _rot_x(self, theta): """ Rotate the view by theta around the x-axis """ angle = theta/180.0*math.pi self.y_vec = self._matrix_mult(self.y_vec, self.x_vec, -angle) def addShape(self, shape, name=None): """ Add a shape to the list of displayed shapes @param shape: BaseShape object @param name: name given to the shape """ if not name==None: shape.name = name shape.params['order']=len(self.shapes) self.shapes.append(shape) self.Refresh(False) def delShape(self, name): """ Delete a shape by name @param name: name of the shape to be deleted """ for i in range(len(self.shapes)): if self.shapes[i].name == name: del self.shapes[i] break self.Refresh(False) def getMaxVolume(self): """ Returns the maximum volume of the combination of all shapes. The maximum volume is the simple sum of the volumes of all the shapes. @return: sum of the volumes of all the shapes [float] """ sum = 0 for item in self.shapes: sum += item.get_volume() return sum def drawAxes(self): """ Draw 3D axes """ pos = self.scale * 0.7 # Z-axis is red zaxis= Arrow(x=pos, y=-pos, z=0, r_cyl=self.scale*0.005, r_cone=self.scale*0.01, l_cyl=self.scale*0.1, l_cone=self.scale*0.05) zaxis.color = COLOR_RED zaxis.draw() # Y-axis is yellow yaxis= Arrow(x=pos, y=-pos, z=0, r_cyl=self.scale*0.005, r_cone=self.scale*0.01, l_cyl=self.scale*0.1, l_cone=self.scale*0.05) yaxis.color = COLOR_YELLOW yaxis.rotate(-90,0,0) yaxis.draw() # X-axis is green xaxis= Arrow(x=pos, y=-pos, z=0, r_cyl=self.scale*0.005, r_cone=self.scale*0.01, l_cyl=self.scale*0.1, l_cone=self.scale*0.05) xaxis.color = COLOR_GREEN xaxis.rotate(0,-90,0) xaxis.draw() glLight(GL_LIGHT0, GL_DIFFUSE, DEFAULT_COLOR) class BaseShape: """ Basic shape functionality """ def __init__(self, x=0, y=0, z=0): self.name = '' ## Position self.x = x self.y = y self.z = z ## Orientation self.theta_x = 0 self.theta_y = 0 self.theta_z = 0 # Params self.params = {} self.params['contrast'] = 1.0 self.params['order'] = 0 self.details = {} self.details['contrast'] = 'A-2' self.details['order'] = ' ' self.highlighted = False self.color = DEFAULT_COLOR def get_volume(self): return 0 def get_length(self): return 1.0 def highlight(self, value=False): self.highlighted = value def rotate(self, alpha, beta, gamma): """ Set the rotation angles of the shape @param alpha: angle around the x-axis [degrees] @param beta: angle around the y-axis [degrees] @param gamma: angle around the z-axis [degrees] """ self.theta_x = alpha self.theta_y = beta self.theta_z = gamma def _rotate(self): """ Perform the OpenGL rotation Note that the rotation order is reversed. We do Y, X, Z do be compatible with simulation... """ glRotated(self.theta_z, 0, 0, 1) glRotated(self.theta_x, 1, 0, 0) glRotated(self.theta_y, 0, 1, 0) def _pre_draw(self): if self.highlighted: glLight(GL_LIGHT0, GL_DIFFUSE, COLOR_HIGHLIGHT) else: glLight(GL_LIGHT0, GL_DIFFUSE, self.color) class Arrow(BaseShape): """ Arrow shape used to show the three axes """ 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): BaseShape.__init__(self, x, y, z) self.r_cyl = r_cyl self.r_cone = r_cone self.l_cyl = l_cyl self.l_cone = l_cone def draw(self): self._pre_draw() glPushMatrix() glTranslate(self.x, self.y, self.z) # Perform rotation glRotate(self.theta_x, 1, 0, 0) glRotate(self.theta_y, 0, 1, 0) glRotate(self.theta_z, 0, 0, 1) # Draw axis cylinder qobj = gluNewQuadric(); gluCylinder(qobj, self.r_cyl, self.r_cyl, self.l_cyl, 15, 5); glTranslate(0, 0, self.z+self.l_cyl) # Draw cone of the arrow glutSolidCone(self.r_cone, self.l_cone, 30, 5) # Translate back to original position glTranslate(-self.x, -self.y, -self.z) glPopMatrix() class Cone(BaseShape): """ Conical shape """ def __init__(self, x=0, y=0, z=0, radius=0.5, height=1): BaseShape.__init__(self, x, y, z) self.radius = radius self.height = height def draw(self): glPushMatrix() glTranslate(self.x, self.y, self.z) glutSolidCone(self.radius, self.height, 30, 5) self._rotate() glTranslate(-self.x, -self.y, -self.z) glPopMatrix() class Sphere(BaseShape): """ Spherical shape """ def __init__(self, x=0, y=0, z=0, radius=10.0): BaseShape.__init__(self, x, y, z) self.name = 'sphere' self.params['radius'] = radius self.details['radius'] = 'A' def get_volume(self): return 4.0/3.0*math.pi*self.params['radius']*self.params['radius']*self.params['radius'] def get_length(self): return 2.0*self.params['radius'] def draw(self): self._pre_draw() glPushMatrix() glTranslate(self.x, self.y, self.z) #glutSolidSphere(self.params['radius'], 30, 30) qobj = gluNewQuadric(); #gluQuadricDrawStyle(qobj,GLU_SILHOUETTE) gluSphere(qobj,self.params['radius'], 30, 30) glTranslate(-self.x, -self.y, -self.z) glPopMatrix() glLight(GL_LIGHT0, GL_DIFFUSE, DEFAULT_COLOR) def accept(self, visitor): return visitor.fromSphere(self) def accept_update(self, visitor): return visitor.update_sphere(self) class Cylinder(BaseShape): """ Cylinder shape, by default the cylinder is oriented along the z-axis. The reference point of the cylinder is the center of the bottom circle. """ def __init__(self, x=0, y=0, z=0, radius=10.0, length=100.0): BaseShape.__init__(self, x, y, z) self.name = 'cylinder' self.params['radius'] = radius self.params['length'] = length self.details['radius'] = 'A' self.details['length'] = 'A' def get_volume(self): return math.pi*self.params['radius']*self.params['radius']*self.params['length'] def get_length(self): if self.params['length']>2.0*self.params['radius']: return self.params['length'] else: return 2.0*self.params['radius'] def draw(self): self._pre_draw() glPushMatrix() glTranslate(self.x, self.y, self.z) self._rotate() qobj = gluNewQuadric(); # gluCylinder(qobj, r_base, r_top, L, div around z, div along z) gluCylinder(qobj, self.params['radius'], self.params['radius'], self.params['length'], 15, 5); glTranslate(-self.x, -self.y, -self.z) glPopMatrix() glLight(GL_LIGHT0, GL_DIFFUSE, DEFAULT_COLOR) def accept(self, visitor): return visitor.fromCylinder(self) def accept_update(self, visitor): return visitor.update_cylinder(self) # Fill the shape list SHAPE_LIST.append(dict(name='Sphere', id=wx.NewId(), cl=Sphere)) SHAPE_LIST.append(dict(name='Cylinder', id=wx.NewId(), cl=Cylinder)) def getShapes(): """ Returns a list of all available shapes """ return SHAPE_LIST def getShapeClass(id): """ Returns a child class of BaseShape corresponding to the supplied shape ID. @param id: shape ID number """ def f(s): return s['id']==id if SHAPE_LIST is not None: shape = filter(f, SHAPE_LIST) return shape[0]['cl'] return None def getShapeClassByName(name): """ Returns a child class of BaseShape corresponding to the supplied shape name. @param name: shape name """ def f(s): return s['name']==name if SHAPE_LIST is not None: shape = filter(f, SHAPE_LIST) return shape[0]['cl'] return None