# Copyright 2004-2008 Nanorex, Inc. See LICENSE file for details. """ ThumbView.py - a simpler OpenGL widget, similar to GLPane @author: Huaicai @version: $Id$ @copyright: 2004-2008 Nanorex, Inc. See LICENSE file for details. """ import math from Numeric import dot from OpenGL.GL import GL_NORMALIZE from OpenGL.GL import GL_SMOOTH from OpenGL.GL import glShadeModel from OpenGL.GL import GL_DEPTH_TEST from OpenGL.GL import glEnable from OpenGL.GL import GL_CULL_FACE from OpenGL.GL import GL_MODELVIEW from OpenGL.GL import glMatrixMode from OpenGL.GL import glLoadIdentity from OpenGL.GL import glViewport from OpenGL.GL import GL_VIEWPORT from OpenGL.GL import glGetIntegerv from OpenGL.GL import glOrtho from OpenGL.GL import glFrustum from OpenGL.GL import glClearColor from OpenGL.GL import GL_COLOR_BUFFER_BIT from OpenGL.GL import GL_DEPTH_BUFFER_BIT from OpenGL.GL import glClear from OpenGL.GL import glTranslatef from OpenGL.GL import glRotatef from OpenGL.GL import GL_STENCIL_INDEX from OpenGL.GL import glReadPixelsi from OpenGL.GL import GL_DEPTH_COMPONENT from OpenGL.GL import glReadPixelsf from OpenGL.GL import glPushMatrix from OpenGL.GL import glSelectBuffer from OpenGL.GL import GL_SELECT from OpenGL.GL import glRenderMode from OpenGL.GL import glInitNames from OpenGL.GL import GL_CLIP_PLANE0 from OpenGL.GL import glClipPlane from OpenGL.GL import GL_RENDER from OpenGL.GL import glFlush from OpenGL.GL import GL_STENCIL_BUFFER_BIT from OpenGL.GL import GL_FALSE from OpenGL.GL import GL_ALWAYS from OpenGL.GL import glStencilFunc from OpenGL.GL import GL_REPLACE from OpenGL.GL import GL_TRUE from OpenGL.GL import glDepthMask from OpenGL.GL import GL_KEEP from OpenGL.GL import glStencilOp from OpenGL.GL import GL_STENCIL_TEST from OpenGL.GL import glDisable from OpenGL.GL import GL_PROJECTION from OpenGL.GL import glPopMatrix from OpenGL.GL import glDepthFunc from OpenGL.GL import GL_LEQUAL from OpenGL.GLU import gluPickMatrix, gluUnProject from PyQt4.Qt import Qt from geometry.VQT import V, Q, A from graphics.drawing.drawers import drawFullWindow from graphics.drawing.gl_lighting import _default_lights from graphics.drawing.gl_lighting import setup_standard_lights from graphics.drawing.setup_draw import setup_drawer from model.assembly import Assembly import foundation.env as env from utilities import debug_flags from utilities.debug import print_compact_traceback from utilities.constants import diTrueCPK from utilities.constants import gray from utilities.constants import bluesky, eveningsky, bg_seagreen, bgEVENING_SKY, bgSEAGREEN from utilities.constants import GL_FAR_Z from utilities.prefs_constants import bondpointHighlightColor_prefs_key from utilities.prefs_constants import backgroundGradient_prefs_key from utilities.prefs_constants import backgroundColor_prefs_key from foundation.Group import Group from model.chem import Atom from model.elements import Singlet from model.chunk import Chunk from operations.pastables import find_hotspot_for_pasting from graphics.widgets.GLPane_minimal import GLPane_minimal from platform_dependent.PlatformDependent import fix_event_helper class ThumbView(GLPane_minimal): """ A simple version of OpenGL widget, which can be used to show a simple thumb view of models when loading models or color changing. General rules for multiple QGLWidget uses: make sure the rendering context is current. Remember makeCurrent() will be called implicitly before any initializeGL, resizeGL, paintGL virtual functions call. Ideally, this class should coordinate with class GLPane in some ways. """ # Note: classes GLPane and ThumbView share lots of code, # which ought to be merged into their common superclass GLPane_minimal # [bruce 070914 comment; since then some of it has been merged, some # still needs to be] shareWidget = None #bruce 051212 always_draw_hotspot = False #bruce 060627 # default values of subclass-specific constants permit_draw_bond_letters = False #bruce 071023, overrides superclass def __init__(self, parent, name, shareWidget): """ Constructs an instance of a Thumbview. """ useStencilBuffer = False GLPane_minimal.__init__(self, parent, shareWidget, useStencilBuffer) self.glselectBufferSize = 500 # different value from that in GLPane_minimal # [I don't know whether this matters -- bruce 071003 comment] self.elementMode = None self.initialised = False #@@@Add the QGLWidget to the parentwidget's grid layout. This is done #here for improving the loading speed. Needs further optimization and #a better place to put this code if possible. -- Ninad 20070827 try: parent.gridLayout.addWidget(self, 0, 0, 1, 1) except: print_compact_traceback("bug: Preview Pane's parent widget doesn't" \ " have a layout. Preview Pane not added to the layout.") pass self.picking = False self.selectedObj = None #This enables the mouse bareMotion() event self.setMouseTracking(True) # clipping planes, as percentage of distance from the eye self.near = 0.66 self.far = 2.0 # start in perspective mode self.ortho = False #True self.scale # make sure superclass set this [bruce 080219] # default color and gradient values. self.backgroundColor = env.prefs[backgroundColor_prefs_key] self.backgroundGradient = env.prefs[ backgroundGradient_prefs_key ] def drawModel(self): """ This is an abstract method for drawing self's "model". [subclasses which need to draw anything must override this method] """ pass def drawSelected(self, obj): """ Draw the selected object. [subclasses can override this method] """ pass def _get_assy(self): #bruce 080220 """ Return the assy which contains all objects we are drawing (or None if this is not possible). [subclasses must override this method] """ return None def __get_assy(self): #bruce 080220 # this glue method is a necessary kluge so that the following property # for self.assy will use a subclass's overridden version of _get_assy return self._get_assy() assy = property(__get_assy) #bruce 080220, for per-assy glname dict def _setup_lighting(self): # as of bruce 060415, this is mostly duplicated between GLPane (has comments) and ThumbView ###@@@ """ [private method] Set up lighting in the model. [Called from both initializeGL and paintGL.] """ glEnable(GL_NORMALIZE) glMatrixMode(GL_PROJECTION) glLoadIdentity() glMatrixMode(GL_MODELVIEW) glLoadIdentity() #bruce 060415 moved following from ThumbView.initializeGL to this split-out method... #bruce 051212 revised lighting code to share prefs and common code with GLPane # (to fix bug 1200 and mitigate bugs 475 and 1158; # fully fixing those would require updating lighting in all ThumbView widgets # whenever lighting prefs change, including making .update calls on them, # and is not planned for near future since it's easy enough to close & reopen them) try: lights = self.shareWidget._lights #bruce 060415 shareWidget --> self.shareWidget; presumably always failed before that ####@@@@ will this fix some bugs about common lighting prefs?? except: lights = _default_lights setup_standard_lights( lights) return def initializeGL(self): self._setup_lighting() glShadeModel(GL_SMOOTH) glEnable(GL_DEPTH_TEST) glEnable(GL_CULL_FACE) glMatrixMode(GL_MODELVIEW) glLoadIdentity() if not self.isSharing(): ## setup_drawer() self._setup_display_lists() # defined in GLPane_minimal. [bruce 071030] return def resetView(self): """ Reset the view. Subclass can override this method with different , so call this version in the overridden version. """ self.pov = V(0.0, 0.0, 0.0) self.quat = Q(1, 0, 0, 0) def setBackgroundColor(self, color, gradient): """ Set the background to 'color' or 'gradient' (Sky Blue). """ self.backgroundColor = color # Ninad and I discussed this and decided that the background should always be set to skyblue. # This issue has to do with Build mode's water surface introducing inconsistencies # with the thumbview background color whenever Build mode's bg color is solid. # Change to "if 0:" to have the thubview background match the current mode background. # This fixes bug 1229. Mark 060116 if 1: self.backgroundGradient = env.prefs[ backgroundGradient_prefs_key ] else: self.backgroundGradient = gradient def resizeGL(self, width, height): """ Called by QtGL when the drawing window is resized. """ self.width = width self.height = height glViewport(0, 0, self.width, self.height) self.trackball.rescale(width, height) if not self.initialised: self.initialised = True def _setup_projection(self, glselect = False): #bruce 050608 split this out; 050615 revised docstring """ Set up standard projection matrix contents using aspect, vdist, and some attributes of self. @warning: leaves matrixmode as GL_PROJECTION. Optional arg glselect should be False (default) or a 4-tuple (to prepare for GL_SELECT picking). """ glMatrixMode(GL_PROJECTION) glLoadIdentity() scale = self.scale #bruce 050608 used this to clarify following code near, far = self.near, self.far #bruce 080219 moved these from one of two callers into here, # to fix bug when insert from partlib is first operation in NE1 self.aspect = (self.width + 0.0) / (self.height + 0.0) self.vdist = 6.0 * scale if glselect: x, y, w, h = glselect gluPickMatrix( x, y, w, h, glGetIntegerv( GL_VIEWPORT ) #k is this arg needed? it might be the default... ) if self.ortho: glOrtho( - scale * self.aspect, scale * self.aspect, - scale, scale, self.vdist * near, self.vdist * far ) else: glFrustum( - scale * near * self.aspect, scale * near * self.aspect, - scale * near , scale * near, self.vdist * near , self.vdist * far) return def paintGL(self): """ Called by QtGL when redrawing is needed. For every redraw, color & depth butter are cleared, view projection are reset, view location & orientation are also reset. """ if not self.initialised: return self._call_whatever_waits_for_gl_context_current() #bruce 071103 glDepthFunc( GL_LEQUAL) self.setDepthRange_setup_from_debug_pref() self.setDepthRange_Normal() from utilities.debug_prefs import debug_pref, Choice_boolean_True, Choice_boolean_False if debug_pref("always setup_lighting?", Choice_boolean_False): #bruce 060415 added debug_pref("always setup_lighting?"), in GLPane and ThumbView [KEEP DFLTS THE SAME!!]; # see comments in GLPane self._setup_lighting() #bruce 060415 added this call self.backgroundColor = env.prefs[backgroundColor_prefs_key] c = self.backgroundColor glClearColor(c[0], c[1], c[2], 0.0) del c glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) self.backgroundGradient = env.prefs[ backgroundGradient_prefs_key ] if self.backgroundGradient: glMatrixMode(GL_PROJECTION) glLoadIdentity() glMatrixMode(GL_MODELVIEW) glLoadIdentity() # Setting to blue sky (default), but might change to something else _bgGradient = bluesky if self.backgroundGradient == bgEVENING_SKY: _bgGradient = eveningsky if self.backgroundGradient == bgSEAGREEN: _bgGradient = bg_seagreen drawFullWindow(_bgGradient)# gradient color ## self.aspect = (self.width + 0.0) / (self.height + 0.0) ## self.vdist = 6.0 * self.scale self._setup_projection() glMatrixMode(GL_MODELVIEW) glLoadIdentity() glTranslatef(0.0, 0.0, -self.vdist) q = self.quat glRotatef(q.angle * 180.0 / math.pi, q.x, q.y, q.z) glTranslatef(self.pov[0], self.pov[1], self.pov[2]) if self.model_is_valid(): #bruce 080117 [testing this at start of paintGL to skip most of it]: # precaution (perhaps a bugfix); see similar code in GLPane. # # update, bruce 080220: this may have caused a bug by making this # not draw a blank-background graphics area when it has no model. # E.g. the partlib has no model when first entered. # Fixing this by only not drawing the model itself in that case [UNTESTED]. # (The GLPane always has a model, so has no similar issue.) # I'm not moving the coordinate transforms into this if statement, # since I don't know if they might be depended on by non-paintGL # drawing (e.g. for highlighting, called selection in some method # names and comments). [bruce 080220] self.drawModel() def __getattr__(self, name): # in class ThumbView if name == 'lineOfSight': return self.quat.unrot(V(0, 0, -1)) elif name == 'right': return self.quat.unrot(V(1, 0, 0)) elif name == 'left': return self.quat.unrot(V(-1, 0, 0)) elif name == 'up': return self.quat.unrot(V(0, 1, 0)) elif name == 'down': return self.quat.unrot(V(0, -1, 0)) elif name == 'out': return self.quat.unrot(V(0, 0, 1)) else: raise AttributeError, 'ThumbView has no "%s"' % name #bruce 060209 revised text def mousePressEvent(self, event): """ Dispatches mouse press events depending on shift and control key state. """ ## Huaicai 2/25/05. This is to fix item 2 of bug 400: make this rendering context ## as current, otherwise, the first event will get wrong coordinates self.makeCurrent() buttons, modifiers = event.buttons(), event.modifiers() #print "Button pressed: ", but if 1: #bruce 060328 kluge fix of undo part of bug 1775 (overkill, but should be ok) (part 1 of 2) import foundation.undo_manager as undo_manager main_assy = env.mainwindow().assy self.__begin_retval = undo_manager.external_begin_cmd_checkpoint(main_assy, cmdname = "(mmkit)") if buttons & Qt.LeftButton: if modifiers & Qt.ShiftModifier: pass # self.graphicsMode.leftShiftDown(event) elif modifiers & Qt.ControlModifier: pass # self.graphicsMode.leftCntlDown(event) else: self.leftDown(event) if buttons & Qt.MidButton: if modifiers & Qt.ShiftModifier: pass # self.graphicsMode.middleShiftDown(event) elif modifiers & Qt.ControlModifier: pass # self.graphicsMode.middleCntlDown(event) else: self.middleDown(event) if buttons & Qt.RightButton: if modifiers & Qt.ShiftModifier: pass # self.graphicsMode.rightShiftDown(event) elif modifiers & Qt.ControlModifier: pass # self.graphicsMode.rightCntlDown(event) else: pass # self.rightDown(event) __begin_retval = None def mouseReleaseEvent(self, event): """ Only used to detect the end of a freehand selection curve. """ buttons, modifiers = event.buttons(), event.modifiers() #print "Button released: ", but if buttons & Qt.LeftButton: if modifiers & Qt.ShiftModifier: pass # self.leftShiftUp(event) elif modifiers & Qt.ControlModifier: pass # self.leftCntlUp(event) else: self.leftUp(event) if buttons & Qt.MidButton: if modifiers & Qt.ShiftModifier: pass # self.graphicsMode.middleShiftUp(event) elif modifiers & Qt.ControlModifier: pass # self.graphicsMode.middleCntlUp(event) else: self.middleUp(event) if buttons & Qt.RightButton: if modifiers & Qt.ShiftModifier: pass # self.rightShiftUp(event) elif modifiers & Qt.ControlModifier: pass # self.rightCntlUp(event) else: pass # self.rightUp(event) if 1: #bruce 060328 kluge fix of undo part of bug 1775 (part 2 of 2) import foundation.undo_manager as undo_manager main_assy = env.mainwindow().assy undo_manager.external_end_cmd_checkpoint(main_assy, self.__begin_retval) return def mouseMoveEvent(self, event): """ Dispatches mouse motion events depending on shift and control key state. """ ##self.debug_event(event, 'mouseMoveEvent') buttons, modifiers = event.buttons(), event.modifiers() if buttons & Qt.LeftButton: if modifiers & Qt.ShiftModifier: pass # self.leftShiftDrag(event) elif modifiers & Qt.ControlModifier: pass # self.leftCntlDrag(event) else: pass # self.leftDrag(event) elif buttons & Qt.MidButton: if modifiers & Qt.ShiftModifier: pass # self.middleShiftDrag(event) elif modifiers & Qt.ControlModifier: pass # self.middleCntlDrag(event) else: self.middleDrag(event) elif buttons & Qt.RightButton: if modifiers & Qt.ShiftModifier: pass # self.rightShiftDrag(event) elif modifiers & Qt.ControlModifier: pass # self.rightCntlDrag(event) else: pass # self.rightDrag(event) else: #Huaicai: To fix bugs related to multiple rendering contexts existed in our application. # See comments in mousePressEvent() for more detail. self.makeCurrent() self.bareMotion(event) def wheelEvent(self, event): buttons, modifiers = event.buttons(), event.modifiers() # The following copies some code from basicMode.Wheel, but not yet the call of rescale_around_point, # since that is not implemented in this class; it ought to be made a method of a new common superclass # of this class and GLPane (and there are quite a few methods of GLPane about which that can be said, # some redundantly implemented here and some not). # [bruce 060829 comment] # # update [bruce 070402 comment]: # sharing that code would now be a bit more complicated (but is still desirable), # since GLPane.rescale_around_point is now best called by basicMode.rescale_around_point_re_user_prefs. # The real lesson is that even ThumbViews ought to use some kind of "edit mode" (like full-fledged modes, # even if some aspects of them would not be used), to handle mouse bindings. But this is likely to be # nontrivial since full-fledged modes might have extra behavior that's inappropriate but hard to # turn off. So if we decide to make ThumbView zoom compatible with that of the main graphics area, # the easiest quick way is just to copy and modify rescale_around_point_re_user_prefs and basicMode.Wheel # into this class. dScale = 1.0/1200.0 if modifiers & Qt.ShiftModifier: dScale *= 0.5 if modifiers & Qt.ControlModifier: dScale *= 2.0 self.scale *= 1.0 + dScale * event.delta() ##: The scale variable needs to set a limit, otherwise, it will set self.near = self.far = 0.0 ## because of machine precision, which will cause OpenGL Error. Huaicai 10/18/04 self.updateGL() return def bareMotion(self, event): wX = event.pos().x() wY = self.height - event.pos().y() if self.selectedObj is not None: stencilbit = glReadPixelsi(wX, wY, 1, 1, GL_STENCIL_INDEX)[0][0] if stencilbit: # If it's the same highlighting object, no display change needed. return self.updateGL() self.selectedObj = self.select(wX, wY) self.highlightSelected(self.selectedObj) return False # russ 080527 def leftDown(self, event): pass def leftUp(self, event): pass def middleDown(self, event): pos = event.pos() self.trackball.start(pos.x(), pos.y()) self.picking = True return def middleDrag(self, event): if self.picking: pos = event.pos() q = self.trackball.update(pos.x(), pos.y()) self.quat += q self.updateGL() return def middleUp(self, event): self.picking = False return def select(self, wX, wY): """ Use the OpenGL picking/selection to select any object. Return the selected object, otherwise, return None. Restore projection and model/view matrices before returning. """ ####@@@@ WARNING: The original code for this, in GLPane, has been duplicated and slightly modified # in at least three other places (search for glRenderMode to find them). This is bad; common code # should be used. Furthermore, I suspect it's sometimes needlessly called more than once per frame; # that should be fixed too. [bruce 060721 comment] wZ = glReadPixelsf(wX, wY, 1, 1, GL_DEPTH_COMPONENT) gz = wZ[0][0] if gz >= GL_FAR_Z: ##Empty space was clicked return None pxyz = A(gluUnProject(wX, wY, gz)) pn = self.out pxyz -= 0.0002*pn # Note: if this runs before the model is drawn, this can have an # exception "OverflowError: math range error", presumably because # appropriate state for gluUnProject was not set up. That doesn't # normally happen but can happen due to bugs (no known open bugs # of that kind). # Sometimes our drawing area can become "stuck at gray", # and when that happens, the same exception can occur from this line. # Could it be that too many accidental mousewheel scrolls occurred # and made the scale unreasonable? (To mitigate, we should prevent # those from doing anything unless we have a valid model, and also # reset that scale when loading a new model (latter is probably # already done, but I didn't check).) [bruce 080220 comment] dp = - dot(pxyz, pn) #Save projection matrix before it's changed. glMatrixMode(GL_PROJECTION) glPushMatrix() current_glselect = (wX, wY, 1, 1) self._setup_projection(glselect = current_glselect) glSelectBuffer(self.glselectBufferSize) glRenderMode(GL_SELECT) glInitNames() glMatrixMode(GL_MODELVIEW) # Save model view matrix before it's changed. glPushMatrix() try: glClipPlane(GL_CLIP_PLANE0, (pn[0], pn[1], pn[2], dp)) glEnable(GL_CLIP_PLANE0) self.drawModel() glDisable(GL_CLIP_PLANE0) except: print_compact_traceback("exception in mode.Draw() during GL_SELECT; ignored; restoring modelview matrix: ") glPopMatrix() glRenderMode(GL_RENDER) return None else: # Restore model/view matrix glPopMatrix() #Restore project matrix and set matrix mode to Model/View glMatrixMode(GL_PROJECTION) glPopMatrix() glMatrixMode(GL_MODELVIEW) glFlush() hit_records = list(glRenderMode(GL_RENDER)) if debug_flags.atom_debug and 0: print "%d hits" % len(hit_records) for (near, far, names) in hit_records: # see example code, renderpass.py if debug_flags.atom_debug and 0: print "hit record: near, far, names:", near, far, names # e.g. hit record: near, far, names: 1439181696 1453030144 (1638426L,) # which proves that near/far are too far apart to give actual depth, # in spite of the 1-pixel drawing window (presumably they're vertices # taken from unclipped primitives, not clipped ones). if names: name = names[-1] assy = self.assy obj = assy and assy.object_for_glselect_name(name) #k should always return an obj return obj return None # from ThumbView.select def highlightSelected(self, obj): # TODO: merge with GLPane (from which this was copied and modified) """ Highlight the selected object . In the mean time, we do stencil test to update stencil buffer, so it can be used to quickly test if pick is still on the same as last test. """ if not obj: return if not isinstance(obj, Atom) or (obj.element is not Singlet): return self._preHighlight() self.drawSelected(obj) self._endHighlight() glFlush() self.swapBuffers() return def _preHighlight(self): ### TODO: rename; move into GLPane_minimal, use in GLPane.py """ Change OpenGL settings to prepare for highlighting. """ self.makeCurrent() glClear(GL_STENCIL_BUFFER_BIT) glDepthMask(GL_FALSE) # turn off depth writing (but not depth test) #glDisable(GL_DEPTH_TEST) glStencilFunc(GL_ALWAYS, 1, 1) glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE) glEnable(GL_STENCIL_TEST) self.setDepthRange_Highlighting() glMatrixMode(GL_MODELVIEW) return def _endHighlight(self): """ Restore OpenGL settings changed by _preHighlight to standard values. """ glDepthMask(GL_TRUE) #glEnable(GL_DEPTH_TEST) glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP) glDisable(GL_STENCIL_TEST) self.setDepthRange_Normal() glMatrixMode(GL_MODELVIEW) return def saveLastView(self): #bruce 060627 for compatibility with GLPane (for sake of assy.update_parts) pass def forget_part(self, part): #bruce 060627 for compatibility with GLPane (for sake of chunk.kill) pass pass # end of class ThumbView # == class ElementView(ThumbView): """ Element graphical display class. """ # note: as of 080403 this is only used by elementColors.py and elementSelector.py def __init__(self, parent, name, shareWidget = None): ThumbView.__init__(self, parent, name, shareWidget) self.scale = 2.0 #5.0 ## the possible largest rvdw of all elements self.pos = V(0.0, 0.0, 0.0) self.mol = None ## Dummy attributes. A kludge, just try to make other code ## think it looks like a glpane object. self.displayMode = 0 self.selatom = None def resetView(self, scale = 2.0): """ Reset current view. """ ThumbView.resetView(self) self.scale = scale def drawModel(self): """ The method for element drawing. """ if self.mol: self.mol.draw(self, None) def model_is_valid(self): #bruce 080117 """ whether our model is currently valid for drawing [overrides GLPane_minimal method] """ return self.mol and self.mol.assy.assy_valid def _get_assy(self): """ [overrides ThumbView method] """ return self.mol and self.mol.assy def refreshDisplay(self, elm, dispMode = diTrueCPK): """ Display the new element or the same element but new display mode. """ self.makeCurrent() self.mol = self.constructModel(elm, self.pos, dispMode) self.updateGL() def updateColorDisplay(self, elm, dispMode = diTrueCPK): """ Display the new element or the same element but new display mode. """ self.makeCurrent() self.mol = self.constructModel(elm, self.pos, dispMode) self.updateGL() def constructModel(self, elm, pos, dispMode): """ This is to try to repeat what 'oneUnbonded()' function does, but hope to remove some stuff not needed here. The main purpose is to build the geometry model for element display. @param elm: An object of class Elem @param elm: L{Elem} @param dispMode: the display mode of the atom @type dispMode: int @return: the Chunk which contains the geometry model. @rtype: L{Chunk} """ assy = Assembly(None, run_updaters = False) assy.set_glpane(self) # sets .o and .glpane mol = Chunk(assy, 'dummy') atm = Atom(elm.symbol, pos, mol) atm.display = dispMode ## bruce 050510 comment: this is approximately how you should change the atom type (e.g. to sp2) for this new atom: ## atm.set_atomtype_but_dont_revise_singlets('sp2') ## see also atm.element.atomtypes -> a list of available atomtype objects for that element ## (which can be passed to set_atomtype_but_dont_revise_singlets) atm.make_bondpoints_when_no_bonds() return mol def drawSelected(self, obj): """ Override the parent version. Specific drawing code for the object. """ if isinstance(obj, Atom) and (obj.element is Singlet): obj.draw_in_abs_coords(self, env.prefs[bondpointHighlightColor_prefs_key]) pass # end of class ElementView class MMKitView(ThumbView): """ Currently used as the GLWidget for the graphical display and manipulation for element/clipboard/part. Initial attempt was to subclass this for each of above type models, but find trouble to dynamically change the GLWidget when changing tab page. """ # note: as of 080403 this is constructed only by PM_PreviewGroupBox.py # and required (assert isinstance(self.elementViewer, MMKitView)) by # PM_Clipboard.py, PM_MolecularModelingKit.py, and PM_PartLib.py. # I think that means it is used for MMKit atoms, PasteFromClipboard_Command clipboard, # and PartLib parts. [bruce 080403 comment] always_draw_hotspot = True #bruce 060627 to help with bug 2028 # (replaces a horribe kluge in old code which broke a fix to that bug) def __init__(self, parent, name, shareWidget = None): ThumbView.__init__(self, parent, name, shareWidget) self.scale = 2.0 self.pos = V(0.0, 0.0, 0.0) self.model = None ## Dummy attributes. A kludge, just try to make other code ## think it looks like a glpane object. self.displayMode = 0 self.selatom = None self.hotspotAtom = None #The current hotspot singlet for the part self.lastHotspotChunk = None # The previous chunk of the hotspot for the part hybrid_type_name = None elementMode = True #Used to differentiate elment page versus clipboard/part page def drawModel(self): """ The method for element drawing. """ if self.model: if isinstance(self.model, Chunk) or \ isinstance(self.model, Group): self.model.draw(self, None) else: ## Assembly self.model.draw(self) return def model_is_valid(self): #bruce 080117, revised 080220 """ whether our model is currently valid for drawing [overrides GLPane_minimal method] """ assy = self.assy if assy is not None: return assy.assy_valid return False def _get_assy(self): #bruce 080220 """ [overrides ThumbView method] """ if self.model: if isinstance(self.model, Chunk) or \ isinstance(self.model, Group): assy = self.model.assy # Note: assy can be None, if the dna updater kills a bare Ax, # since its chunk is then killed and its .assy set to None. # But the following is too verbose to be useful; also it # seemingly happens even for Ss3 (why??). # Todo: check if it's due to Ax (ok to not print it here) # or to something we didn't expect (needs print so as not # to risk hiding other bugs). ## if res is None: ## print "dna updater fyi: in model_is_valid: " \ ## "%r.model %r has .assy None" % \ ## (self, self.model) return assy else: ## self.model is an Assembly return self.model return None def refreshDisplay(self, elm, dispMode = diTrueCPK): """ Display the new element or the same element but new display mode. """ self.makeCurrent() self.model = self.constructModel(elm, self.pos, dispMode) self.updateGL() def changeHybridType(self, name): self.hybrid_type_name = name def resetView(self): """ Reset current view. """ ThumbView.resetView(self) self.scale = 2.0 def drawSelected(self, obj): """ Override the parent version. Specific drawing code for the object. """ if isinstance(obj, Atom) and (obj.element is Singlet): obj.draw_in_abs_coords(self, env.prefs[bondpointHighlightColor_prefs_key]) return def constructModel(self, elm, pos, dispMode): """ This is to try to repeat what 'oneUnbonded()' function does, but hope to remove some stuff not needed here. The main purpose is to build the geometry model for element display. @param elm: An object of class Elem @param elm: L{Elem} @param dispMode: the display mode of the atom @type dispMode: int @return: the Chunk which contains the geometry model. @rtype: L{Chunk} """ assy = Assembly(None, run_updaters = False) assy.set_glpane(self) # sets .o and .glpane mol = Chunk(assy, 'dummy') atm = Atom(elm.symbol, pos, mol) atm.display = dispMode ## bruce 050510 comment: this is approximately how you should change the atom type (e.g. to sp2) for this new atom: if self.hybrid_type_name: atm.set_atomtype_but_dont_revise_singlets(self.hybrid_type_name) ## see also atm.element.atomtypes -> a list of available atomtype objects for that element ## (which can be passed to set_atomtype_but_dont_revise_singlets) atm.make_bondpoints_when_no_bonds() self.elementMode = True return mol def leftDown(self, event): """ When in clipboard mode, set hotspot if a Singlet is highlighted. """ if self.elementMode: return obj = self.selectedObj if isinstance(obj, Atom) and (obj.element is Singlet): mol = obj.molecule if not mol is self.lastHotspotChunk: if self.lastHotspotChunk: # Unset previous hotspot [bruce 060629 fix bug 1974 -- only if in same part] if mol.part is self.lastHotspotChunk.part and mol.part is not None: # Old and new hotspot chunks are in same part. Unset old hotspot, # so as to encourage there to be only one per Part. # This should happen when you try to make more than one hotspot in one # library part or clipboard item, using the MMKit to make both. # It might make more sense for more general code in Part to prevent # more than one hotspot per part... but we have never decided whether # that would be a good feature. (I have long suspected that hotspots # should be replaced by some sort of jig, to give more control....) # I don't know if this case can ever happen as of now, since multichunk # clipboard items aren't shown in MMKit -- whether it can happen now # depends on whether any multichunk library parts have bondpoints on # more than one chunk. [bruce 060629] if env.debug() and self.lastHotspotChunk.hotspot: #bruce 060629 re bug 1974 print "debug: unsetting hotspot of %r (was %r)" % \ (self.lastHotspotChunk, self.lastHotspotChunk.hotspot) self.lastHotspotChunk.set_hotspot(None) else: # Don't unset hotspot in this case (doing so was causing bug 1974). if env.debug() and self.lastHotspotChunk.hotspot: print "debug: NOT unsetting hotspot of %r" % (self.lastHotspotChunk, ) pass self.lastHotspotChunk = mol # [as of 060629, the only purpose of this is to permit the above code to unset it in some cases] mol.set_hotspot(obj) if 1: #bruce 060328 fix gl_update part of bug 1775 (the code looks like that was a bug forever, don't know for sure) main_glpane = env.mainwindow().glpane if mol.part is main_glpane.part: main_glpane.gl_update() self.hotspotAtom = obj self.updateGL() return def gl_update(self): #bruce 070502 bugfix (can be called when ESPImage jigs appear in a partlib part) self.updateGL() #k guess at correct/safe thing to do return def gl_update_highlight(self): #bruce 070626 precaution (not sure if any code will call this) self.gl_update() return def gl_update_for_glselect(self): #bruce 070626 precaution (not sure if any code will call this) self.gl_update() return def updateModel(self, newObj): """ Set new chunk or Assembly for display. """ self.model = newObj #Reset hotspot related stuff for a new Assembly if isinstance(newObj, Assembly): self._find_and_set_hotSpotAtom_in_new_model(newObj) self._fitInWindow() self.elementMode = False self.updateGL() def _find_and_set_hotSpotAtom_in_new_model(self, newModel): """ If the model being viewed in the thumbView window already has a hotspot, set it as self.hotSpotAtom (which then will be used by client code) @see: self.updateModel @Note that , in self.leftDown, we can actually temporarily change the the hotspot for the partlib model. But then if you view another part and go back to this model, the hotspot will be reset to the one that already exists in the model (or None if one doesn't exist) """ assert isinstance(newModel, Assembly) chunkList = [] def func(node): if isinstance(node, Chunk): chunkList.append(node) newModel.part.topnode.apply2all(func) ok = False if len(chunkList) == 1: ok, hotspot_or_whynot = find_hotspot_for_pasting(chunkList[0]) elif len(chunkList) > 1: for chunk in chunkList: ok, hotspot_or_whynot = find_hotspot_for_pasting(chunk) if ok: break if ok: self.hotspotAtom = hotspot_or_whynot self.lastHotspotChunk = self.hotspotAtom.molecule else: self.hotspotAtom = None self.lastHotspotChunk = None return def setDisplay(self, mode): self.displayMode = mode return def _fitInWindow(self): if not self.model: return self.quat = Q(1, 0, 0, 0) if isinstance(self.model, Chunk): self.model._recompute_bbox() bbox = self.model.bbox else: ## Assembly part = self.model.part bbox = part.bbox self.scale = bbox.scale() if isinstance(self.width, int): width = self.width else: width = float(self.width()) if isinstance(self.height, int): height = self.height else: height = float(self.height()) aspect = width / height ##aspect = float(self.width) / self.height if aspect < 1.0: self.scale /= aspect center = bbox.center() self.pov = V(-center[0], -center[1], -center[2]) pass # end of class MMKitView # end