# Copyright 2004-2009 Nanorex, Inc. See LICENSE file for details. """ Move_Command.py The 'Command' part of the Move Mode (Move_Command and Move_GraphicsMode are the two split classes of the old modifyMode) It provides the command object for its GraphicsMode class. The Command class defines anything related to the 'command half' of the mode -- For example: - Anything related to its current Property Manager, its settings or state - The model operations the command does (unless those are so simple that the mouse event bindings in the _GM half can do them directly and the code is still clean, *and* no command-half subclass needs to override them). @version: $Id$ @copyright: 2004-2009 Nanorex, Inc. See LICENSE file for details. History: See Move_GraphicsMode.py TODO as of 2008-09-09: -refactor update ui related code. Example some methods call propMgr.updateMessage() etc. this needs to be in a central place... either in this callls or in PM._update_UI_do_updates() """ import foundation.env as env import math from commands.Move.MovePropertyManager import MovePropertyManager from commands.SelectChunks.SelectChunks_Command import SelectChunks_Command from command_support.GraphicsMode_API import GraphicsMode_interface from geometry.BoundingBox import BBox from utilities.Log import redmsg from geometry.VQT import V, Q from utilities.debug import print_compact_traceback from commands.Translate.TranslateChunks_GraphicsMode import TranslateChunks_GraphicsMode from commands.Rotate.RotateChunks_GraphicsMode import RotateChunks_GraphicsMode from ne1_ui.toolbars.Ui_MoveFlyout import MoveFlyout class Move_Command(SelectChunks_Command): """ """ _pointRequestCommand_pivotPoint = None GraphicsMode_class = TranslateChunks_GraphicsMode #The class constant PM_class defines the class for the Property Manager of #this command. See Command.py for more infor about this class constant PM_class = MovePropertyManager #Flyout Toolbar FlyoutToolbar_class = MoveFlyout commandName = 'MODIFY' featurename = "Move Chunks Mode" from utilities.constants import CL_EDIT_GENERIC command_level = CL_EDIT_GENERIC pw = None command_should_resume_prevMode = False command_has_its_own_PM = True flyoutToolbar = None #START command API methods ============================================= def command_entered(self): """ Overrides superclass method. @see: baseCommand.command_enter_PM() for documentation """ super(Move_Command, self).command_entered() self.propMgr.set_move_xyz(0, 0, 0) # Init X, Y, and Z to zero self.propMgr.set_move_delta_xyz(0,0,0) # Init DelX,DelY, DelZ to zero def command_enter_misc_actions(self): """ Overrides superclass method. @see: baseCommand.command_enter_misc_actions() for documentation """ pass def command_exit_misc_actions(self): """ Overrides superclass method. @see: baseCommand.command_exit_misc_actions() for documentation """ self.w.toolsMoveMoleculeAction.setChecked(False) self.w.rotateComponentsAction.setChecked(False) #END new command API methods ============================================== def _acceptLineModePoints(self, params): #bruce 080801, revises acceptParamsFromTemporaryMode """ Accept returned points from the Line_Command request command. """ ### REVIEW: can this be called with params == None, # and/or never called, if Line_Command is terminated early? # In current code, it must always be called, # and is always called regardless of how Line_Command exits # and probably in the old other case as well). # [bruce 080904 comment] (points,) = params del params #Usually points will contain 2 items. But if user abruptly terminates #the temporary mode, this might not be true. So move the chunk by offset #only when you have got 2 points! Ninad 2007-10-16 if len(points) == 2: startPoint = points[0] endPoint = points[1] offset = endPoint - startPoint movables = self.graphicsMode.getMovablesForLeftDragging() self.assy.translateSpecifiedMovables(offset, movables = movables) self.o.gl_update() self.propMgr.moveFromToButton.setChecked(False) return def _acceptRotateAboutPointResults(self, params): #bruce 080801, revises acceptParamsFromTemporaryMode """ Accept results (empty tuple) from the RotateAboutPoint request command. (This exists in order to perform a side effect, and since callRequestCommand requires a results callback.) """ ### REVIEW: can this be called with params == None # if RotateAboutPoint is terminated early? del params self.propMgr.rotateAboutPointButton.setChecked(False) return def _acceptPointRequestCommand_pivotPoint(self, params): self._pointRequestCommand_pivotPoint = params def EXPERIMENTAL_rotateAboutPointTemporaryCommand(self, isChecked = False): #@ATTENTION: THIS IS NOT USED AS OF NOV 28, 2008 SCHEDULED FOR REMOVAL """ @see: self.moveFromToTemporaryMode """ #@TODO: clean this up. This was written just after Rattlesnake rc2 #for FNANO presentation -- Ninad 2008-04-17 if self.commandSequencer._f_command_stack_is_locked: # This is normal when the command is exiting on its own # and changes the state of its action programmatically. # In this case, redundant exit causes bugs, so skip it. # It might be better to avoid sending the signal when # programmatically changing the action state. # See similar code and comment in ops_view.py. # [bruce 080905] return if isChecked: # invoke the RotateAboutPoint command self.propMgr.rotateStartCoordLineEdit.setEnabled(isChecked) msg = "Click inside the 3D workspace to define two points " \ "of a line. The selection will be rotated about the first point "\ "in the direction specified by that line" self.propMgr.updateMessage(msg) cs = self.commandSequencer # following was revised by bruce 080801 mouseClickLimit = 1 planeAxis = None planePoint = None cs.callRequestCommand( 'Point_RequestCommand', arguments = (mouseClickLimit, planeAxis, planePoint), # number of mouse click points to accept accept_results = self._acceptPointRequestCommand_pivotPoint ) ###Next step: define reference vector ##mouseClickLimit = 1 ##planeAxis = self.glpane.lineOfSight ##planePoint = self._pointRequestCommand_pivotPoint ##print "**** planePoint = ", planePoint ##cs.callRequestCommand( 'Point_RequestCommand', ##arguments = (mouseClickLimit, planeAxis, planePoint), # number of mouse click points to accept ##accept_results = self. _acceptRotateAboutPointResults ##) else: # exit the RotateAboutPoint command currentCommand = self.commandSequencer.currentCommand if currentCommand.commandName == "RotateAboutPoint": currentCommand.command_Cancel() self.propMgr.rotateStartCoordLineEdit.setEnabled(False) self.propMgr.updateMessage() def rotateAboutPointTemporaryCommand(self, isChecked = False): """ @see: self.moveFromToTemporaryMode """ #@TODO: clean this up. This was written just after Rattlesnake rc2 #for FNANO presentation -- Ninad 2008-04-17 if self.commandSequencer._f_command_stack_is_locked: # This is normal when the command is exiting on its own # and changes the state of its action programmatically. # In this case, redundant exit causes bugs, so skip it. # It might be better to avoid sending the signal when # programmatically changing the action state. # See similar code and comment in ops_view.py. # [bruce 080905] return if isChecked: # invoke the RotateAboutPoint command self.propMgr.rotateStartCoordLineEdit.setEnabled(isChecked) msg = "Define 3 points. The 1st point is the rotation point "\ "(i.e. the rotation axis perpendicular to the view). "\ "The 2nd point is the starting angle. "\ "The 3rd point is the ending angle." self.propMgr.updateMessage(msg) # following was revised by bruce 080801 self.commandSequencer.callRequestCommand( 'RotateAboutPoint', arguments = (3,), # number of mouse click points to accept accept_results = self._acceptRotateAboutPointResults ) else: # exit the RotateAboutPoint command currentCommand = self.commandSequencer.currentCommand if currentCommand.commandName == "RotateAboutPoint": currentCommand.command_Cancel() self.propMgr.rotateStartCoordLineEdit.setEnabled(False) self.propMgr.updateMessage() def moveFromToTemporaryMode(self, isChecked = False): """ Move the selected entities by the offset vector specified by the endpoints of a line. To use this feature, click on 'Move From/To button' in the PM and specify the two endpoints from GLPane. The program enters a temporary mode while you do that and then uses the data collected while in temporary mode (i.e. two endpoints) to move the selection. TODO: Note that the endpoints always assume GLPane depth. As of today, the temporary mode API knows nothing about the highlighting. Once it is implemented, we can then specify atom centers etc as reference points. See comments in Line_Command for further details. """ if isChecked: self.propMgr.startCoordLineEdit.setEnabled(isChecked) msg = "Define the offset vector by clicking two points. "\ "The 1st point is the 'from' point. "\ "The 2nd point is the 'to' point." self.propMgr.updateMessage(msg) # following was revised by bruce 080801 self.commandSequencer.callRequestCommand( 'Line_Command', arguments = (2,), # number of mouse click points to accept accept_results = self._acceptLineModePoints ) else: self.propMgr.startCoordLineEdit.setEnabled(False) self.propMgr.updateMessage() def rotateThetaPlus(self): """ Rotate the selected chunk(s) by theta (plus) """ button = self.propMgr.rotateAroundAxisButtonRow.checkedButton() if button: rotype = str(button.text()) else: env.history.message(redmsg("Rotate By Specified Angle:" "Please press the button "\ "corresponding to the axis of rotation")) return theta = self.propMgr.rotateThetaSpinBox.value() self.rotateTheta( rotype, theta) def rotateThetaMinus(self): """ Rotate the selected chunk(s) by theta (minus) """ button = self.propMgr.rotateAroundAxisButtonRow.checkedButton() if button: rotype = str(button.text()) else: env.history.message(redmsg("Rotate By Specified Angle:"\ " Please press the button "\ "corresponding to the axis of rotation")) return theta = self.propMgr.rotateThetaSpinBox.value() * -1.0 self.rotateTheta( rotype, theta) def rotateTheta(self, rotype, theta): """ Rotate the selected chunk(s) /jig(s) around the specified axis by theta (degrees) """ movables = self.graphicsMode.getMovablesForLeftDragging() if not movables: env.history.message(redmsg("No chunks or movable jigs selected.")) return if rotype == 'ROTATEX': ma = V(1,0,0) # X Axis elif rotype == 'ROTATEY': ma = V(0,1,0) # Y Axis elif rotype == 'ROTATEZ': ma = V(0,0,1) # Z Axis else: print 'modifyMody.rotateTheta: Error. rotype = ', rotype, ', which is undefined.' return # wware 20061214: I don't know where the need arose for this factor of 100, # but it's necessary to get correct angles. #ninad 070322: #Will's above comment was for "dy = 100.0 * (pi / 180.0) * theta # Convert to radians" #I agree with this. In fact if I enter angle of 1 degree, it multiplies it by 100! #May be it was necessary in Qt3 branch. I am modifying this formula #to remove this multiplication factor of 100 as its giving wrong results dy = (math.pi / 180.0) * theta # Convert to radians qrot = Q(ma,dy) # Quat for rotation delta. if self.propMgr.rotateAsUnitCB.isChecked(): # Rotate the selection as a unit. self.assy.rotateSpecifiedMovables(qrot, movables) else: for item in movables: try: item.rot(qrot) except AssertionError: print_compact_traceback("Selected movable doesn't have"\ "rot method?") self.o.gl_update() def transDeltaPlus(self): """ Add X, Y, and Z to the selected chunk(s) current position """ movables = self.graphicsMode.getMovablesForLeftDragging() if not movables: env.history.message(redmsg("No chunks or movable jigs selected.")) return offset = self.propMgr.get_move_delta_xyz() self.assy.translateSpecifiedMovables(offset, movables = movables) self.o.gl_update() def transDeltaMinus(self): """ Subtract X, Y, and Z from the selected chunk(s) current position """ movables = self.graphicsMode.getMovablesForLeftDragging() if not movables: env.history.message(redmsg("No chunks or movable jigs selected.")) return offset = self.propMgr.get_move_delta_xyz(Plus=False) self.assy.translateSpecifiedMovables(offset, movables = movables) self.o.gl_update() def moveAbsolute(self): """ Move selected chunk(s), jig(s) to absolute X, Y, and Z by computing the bbox center of everything as if they were one big chunk, then move everything as a unit. """ movables = self.graphicsMode.getMovablesForLeftDragging() if not movables: env.history.message(redmsg("No chunks or movable jigs selected.")) return ## Compute bbox for selected chunk(s). bbox = BBox() for m in movables: if hasattr(m, "bbox"): # Fixes bug 1990. Mark 060702. bbox.merge(m.bbox) pt1 = bbox.center() # pt1 = center point for bbox of selected chunk(s). pt2 = self.propMgr.get_move_xyz() # pt2 = X, Y, Z values from PM offset = pt2 - pt1 # Compute offset for translating the selection self.assy.translateSpecifiedMovables(offset, movables = movables) # Print history message about what happened. if len(movables) == 1: msg = "[%s] moved to [X: %.2f] [Y: %.2f] [Z: %.2f]" % (movables[0].name, pt2[0], pt2[1], pt2[2]) else: msg = "Selected chunks/jigs moved by offset [X: %.2f] [Y: %.2f] [Z: %.2f]" % (offset[0], offset[1], offset[2]) env.history.message(msg) self.o.gl_update() return def _create_GraphicsMode(self): GM_class = self.GraphicsMode_class assert issubclass(GM_class, GraphicsMode_interface) args = [self] kws = {} self.graphicsMode = GM_class(*args, **kws) self.translate_graphicsMode = TranslateChunks_GraphicsMode(*args, **kws) self.rotate_graphicsMode = RotateChunks_GraphicsMode(*args, **kws) def switchGraphicsModeTo(self, newGraphicsMode = 'TRANSLATE_CHUNKS'): """ Switch graphics mode of self to the one specified by the client. Changing graphics mode while remaining in the same command has certain advantages and it also bypasses some code related to entering a new command. @param newGraphicsMode: specifies the new graphics mode to switch to @type newGraphicsMode: string @see: B{MovePropertyManager.activate_translateGroupBox} """ #TODO: Make this a general API method if need arises - Ninad 2008-01-25 assert newGraphicsMode in ['TRANSLATE_CHUNKS', 'ROTATE_CHUNKS'] if newGraphicsMode == 'TRANSLATE_CHUNKS': if self.graphicsMode is self.translate_graphicsMode: return self.graphicsMode = self.translate_graphicsMode self.graphicsMode.Enter_GraphicsMode() self.glpane.update_after_new_graphicsMode() elif newGraphicsMode == 'ROTATE_CHUNKS': if self.graphicsMode is self.rotate_graphicsMode: return self.graphicsMode = self.rotate_graphicsMode self.graphicsMode.Enter_GraphicsMode() self.glpane.update_after_new_graphicsMode()