Python API 2.0 Reference
python/api2/py2SquareScaleManipContext.py
1 ########################################################################
2 #
3 # DESCRIPTION:
4 #
5 # This example is based on the squareScaleManip example but uses
6 # a context and context command. If the plug-in context is active,
7 # selecting geometry will show the manipulator. Only the right and
8 # left sides of the square currently modify the geometry if moved.
9 #
10 ########################################################################
11 #
12 # First make sure that py2SquareScaleManipContext.py is in your
13 # MAYA_PLUG_IN_PATH. Then, to use the tool, execute the following in the
14 # script editor:
15 #
16 # from maya import cmds
17 # cmds.file(new=1,force=1)
18 # cmds.polySphere()
19 # cmds.loadPlugin("py2SquareScaleManipContext.py")
20 #
21 # ctx = cmds.py2SquareScaleManipContext(nop=1)
22 # cmds.setToolTo(ctx)
23 # cmds.py2SquareScaleManipContext(ctx,q=1,nop=1)
24 #
25 # Once the tool is active, click on the sphere. Move the right and left
26 # edges of the square to modify the selected object's scale.
27 #
28 # The query should return "Doing absolutely nothing"
29 #
30 ########################################################################
31 
32 from __future__ import division
33 from builtins import object
34 import logging
35 import math
36 import sys
37 from maya.api import OpenMaya, OpenMayaUI, OpenMayaRender
38 from maya import OpenMayaRender as OpenMayaRenderV1
39 
40 logger = logging.getLogger('py2SquareScaleManipContext')
41 
42 # tell Maya that we want to use Python API 2.0
43 maya_useNewAPI = True
44 
45 #
46 # Utility classes
47 #
48 class PlaneMath(object):
49  """
50  This utility class represents a mathematical plane and performs intersection
51  tests with a line.
52  """
53  def __init__(self):
54  """
55  Initialze the member variables of the class.
56  """
57  self.a = 0.0
58  self.b = 0.0
59  self.c = 0.0
60  self.d = 0.0
61 
62  def set_plane( self, point_on_plane, normal_to_plane ):
63  """
64  Define the plane by supplying a point on the plane and the plane's normal.
65  """
66  _normal_to_plane = OpenMaya.MVector(normal_to_plane)
67  _normal_to_plane.normalize()
68 
69  # Calculate a,b,c,d based on input
70  self.a = _normal_to_plane.x
71  self.b = _normal_to_plane.y
72  self.c = _normal_to_plane.z
73  self.d = -(self.a*point_on_plane.x + self.b*point_on_plane.y + self.c*point_on_plane.z)
74 
75  def intersect( self, line ):
76  """
77  Intersect the plane with the given line. Return the intersection point if an intersection
78  occurs. Otherwise, raise an exception.
79  """
80  denominator = self.a*line[1].x + self.b*line[1].y + self.c*line[1].z
81 
82  # Verify that the vector and the plane are not parallel.
83  if (denominator < .00001):
84  raise Exception
85 
86  t = -(self.d + self.a*line[0].x + self.b*line[0].y + self.c*line[0].z) / denominator
87 
88  # Calculate the intersection point.
89  return line[0] + t * line[1]
90 
91 class LineMath(object):
92  """
93  This utility class represents a mathematical line and returns the closest point
94  on the line to a given point.
95  """
96  def __init__(self):
97  """
98  Initialze the member variables of the class.
99  """
100  self.point = OpenMaya.MPoint()
101  self.direction = OpenMaya.MVector()
102 
103  def set_line( self, line_point, line_direction ):
104  """
105  Define the line by supplying a point on the line and the line's direction.
106  """
107  self.point = OpenMaya.MPoint(line_point)
108  self.direction = OpenMaya.MVector(line_direction)
109  self.direction.normalize()
110 
111  def closest_point( self, to_point ):
112  """
113  Determine and return the point on the line which is closest to the given point.
114  """
115  t = self.direction * ( to_point - self.point )
116  return self.point + ( self.direction * t )
117 
118 class SquareGeometry(object):
119  """
120  This utility class defines methods for returning the four corner points of a unit square
121  in the X-Y plane.
122  """
123  @staticmethod
124  def top_left():
125  """
126  Return the top left corner of the square.
127  """
128  return OpenMaya.MPoint(-0.5, 0.5, 0.0)
129  @staticmethod
130  def top_right():
131  """
132  Return the top right corner of the square.
133  """
134  return OpenMaya.MPoint( 0.5, 0.5, 0.0)
135  @staticmethod
136  def bottom_left():
137  """
138  Return the bottom left corner of the square.
139  """
140  return OpenMaya.MPoint(-0.5, -0.5, 0.0)
141  @staticmethod
142  def bottom_right():
143  """
144  Return the bottom right corner of the square.
145  """
146  return OpenMaya.MPoint( 0.5, -0.5, 0.0)
147 
148 #
149 # SquareScaleManipulator
150 #
151 
152 class SquareScaleManipulator (OpenMayaUI.MPxManipulatorNode):
153  """
154  This is the subclassed manipulator node. It scales the selected objects
155  in the X direction based on dragging movements by the user.
156  """
157  kNodeName = 'py2SquareScaleContextManipulator'
158  kTypeId = OpenMaya.MTypeId( 0x00081162 )
159 
160  def __init__(self):
161  """
162  Initialize the manipulator member variables.
163  """
165 
166  # Setup the plane with a point on the plane along with a normal
167  self.point_on_plane = SquareGeometry.top_left()
168 
169  # Set plug indicies to a default
170  self.top_index = -1
171  self.right_index = -1
172  self.bottom_index = -1
173  self.left_index = -1
174  self.top_name = -1
175  self.right_name = -1
176  self.bottom_name = -1
177  self.left_name = -1
178 
179  # initialize rotate/translate to a good default
180  self.rotate_x = 0.0
181  self.rotate_y = 0.0
182  self.rotate_z = 0.0
183  self.translate_x = 0.0
184  self.translate_y = 0.0
185  self.translate_z = 0.0
186 
187  # Normal = cross product of two vectors on the plane
188  v1 = OpenMaya.MVector(SquareGeometry.top_left()) - OpenMaya.MVector(SquareGeometry.top_right())
189  v2 = OpenMaya.MVector(SquareGeometry.top_right()) - OpenMaya.MVector(SquareGeometry.bottom_right())
190  self.normal_to_plane = v1 ^ v2
191 
192  # Necessary to normalize
193  self.normal_to_plane.normalize()
194  self.plane = PlaneMath()
195  self.plane.set_plane( self.point_on_plane, self.normal_to_plane )
196 
197 
198  @classmethod
199  def creator(cls):
200  return cls()
201 
202  @classmethod
203  def initialize(cls):
204  pass
205 
206  # virtual
207  def postConstructor(self):
208  self.top_index = self.addDoubleValue( 'topValue', 0 )
209  self.right_index = self.addDoubleValue( 'rightValue', 0 )
210  self.bottom_index = self.addDoubleValue( 'bottomValue', 0 )
211  self.left_index = self.addDoubleValue( 'leftValue', 0 )
212 
213  gl_pickable_item = self.glFirstHandle()
214  self.top_name = gl_pickable_item
215  self.bottom_name = gl_pickable_item + 1
216  self.right_name = gl_pickable_item + 2
217  self.left_name = gl_pickable_item + 3
218 
219  # virtual
220  def connectToDependNode(self, depend_node):
221  """
222  Connect the manipulator to the given dependency node.
223  """
224 
225  # Make sure we have a scaleX plug and connect the
226  # plug to the rightIndex we created in the postConstructor
227  scale_x_plug = None
228  nodeFn = OpenMaya.MFnDependencyNode(depend_node)
229 
230  try:
231  scale_x_plug = nodeFn.findPlug('scaleX', True)
232  except:
233  logger.info(" Could not find scaleX plug!")
234  return
235 
236  plug_index = 0
237  try:
238  plug_index = self.connectPlugToValue(scale_x_plug, self.right_index)
239  except:
240  logger.info(" Could not connectPlugToValue!")
241  return
242 
243  self.finishAddingManips()
244  return OpenMayaUI.MPxManipulatorNode.connectToDependNode(self, depend_node)
245 
246  def pre_draw(self):
247  """
248  Update the region dragged by the mouse.
249  """
250 
251  # Populate the point arrays which are in local space
252  tl = SquareGeometry.top_left()
253  tr = SquareGeometry.top_right()
254  bl = SquareGeometry.bottom_left()
255  br = SquareGeometry.bottom_right()
256 
257  # Depending on what's active, we modify the
258  # end points with mouse deltas in local space
259  active = self.glActiveName()
260  if active:
261  if ( active == self.top_name ):
262  tl += self.mouse_point_gl_name
263  tr += self.mouse_point_gl_name
264  if ( active == self.bottom_name ):
265  bl += self.mouse_point_gl_name
266  br += self.mouse_point_gl_name
267  if ( active == self.right_name ):
268  tr += self.mouse_point_gl_name
269  br += self.mouse_point_gl_name
270  if ( active == self.left_name ):
271  tl += self.mouse_point_gl_name
272  bl += self.mouse_point_gl_name
273 
274  return [tl, tr, bl, br]
275 
276  # virtual
277  def draw(self, view, path, style, status):
278  """
279  Draw the manupulator in a legacy viewport.
280  """
281 
282  # drawing in VP1 views will be done using V1 Python APIs:
283  gl_renderer = OpenMayaRenderV1.MHardwareRenderer.theRenderer()
284  gl_ft = gl_renderer.glFunctionTable()
285 
286  [tl, tr, bl, br] = self.pre_draw()
287 
288  # Begin the drawing
289  view.beginGL()
290 
291  # Push the matrix and set the translate/rotate. Perform
292  # operations in reverse order
293  gl_ft.glMatrixMode( OpenMayaRenderV1.MGL_MODELVIEW )
294  gl_ft.glPushMatrix()
295  gl_ft.glTranslatef( self.translate_x, self.translate_y, self.translate_z )
296  gl_ft.glRotatef( math.degrees(self.rotate_z), 0.0, 0.0, 1.0 )
297  gl_ft.glRotatef( math.degrees(self.rotate_y), 0.0, 1.0, 0.0 )
298  gl_ft.glRotatef( math.degrees(self.rotate_x), 1.0, 0.0, 0.0 )
299 
300  # Top
301  # Place before you draw the manipulator component that can be pickable.
302  self.colorAndName( view, self.top_name, False, self.mainColor() )
303  gl_ft.glBegin( OpenMayaRenderV1.MGL_LINES )
304  gl_ft.glVertex3f( tl.x, tl.y, tl.z )
305  gl_ft.glVertex3f( tr.x, tr.y, tr.z )
306  gl_ft.glEnd()
307 
308  # Right
309  self.colorAndName( view, self.right_name, True, self.mainColor() )
310  gl_ft.glBegin( OpenMayaRenderV1.MGL_LINES )
311  gl_ft.glVertex3f( tr.x, tr.y, tr.z )
312  gl_ft.glVertex3f( br.x, br.y, br.z )
313  gl_ft.glEnd()
314 
315  # Bottom
316  self.colorAndName( view, self.bottom_name, False, self.mainColor() )
317  gl_ft.glBegin( OpenMayaRenderV1.MGL_LINES )
318  gl_ft.glVertex3f( br.x, br.y, br.z )
319  gl_ft.glVertex3f( bl.x, bl.y, bl.z )
320  gl_ft.glEnd()
321 
322  # Left
323  self.colorAndName( view, self.left_name, True, self.mainColor() )
324  gl_ft.glBegin( OpenMayaRenderV1.MGL_LINES )
325  gl_ft.glVertex3f( bl.x, bl.y, bl.z )
326  gl_ft.glVertex3f( tl.x, tl.y, tl.z )
327  gl_ft.glEnd()
328 
329  # Pop matrix
330  gl_ft.glPopMatrix()
331 
332  # End the drawing
333  view.endGL()
334 
335  # virtual
336  def preDrawUI(self, view):
337  """
338  Cache the viewport for use in VP 2.0 drawing.
339  """
340  pass
341 
342  # virtual
343  def drawUI(self, draw_manager, frame_context):
344  """
345  Draw the manupulator in a VP 2.0 viewport.
346  """
347 
348  [tl, tr, bl, br] = self.pre_draw()
349 
351  xform.rotateByComponents([math.degrees(self.rotate_x), \
352  math.degrees(self.rotate_y), \
353  math.degrees(self.rotate_z), \
354  OpenMaya.MTransformationMatrix.kZYX], \
355  OpenMaya.MSpace.kWorld)
356 
357  mat = xform.asMatrix()
358  tl *= mat
359  tr *= mat
360  bl *= mat
361  br *= mat
362 
363  # Top
364  draw_manager.beginDrawable(OpenMayaRender.MUIDrawManager.kNonSelectable, self.top_name)
365  self.setHandleColor(draw_manager, self.top_name, self.dimmedColor())
366  draw_manager.line(tl, tr)
367  draw_manager.endDrawable()
368 
369  # Right
370  draw_manager.beginDrawable(OpenMayaRender.MUIDrawManager.kSelectable, self.right_name)
371  self.setHandleColor(draw_manager, self.right_name, self.mainColor())
372  draw_manager.line(tr, br)
373  draw_manager.endDrawable()
374 
375  # Bottom
376  draw_manager.beginDrawable(OpenMayaRender.MUIDrawManager.kNonSelectable, self.bottom_name)
377  self.setHandleColor(draw_manager, self.bottom_name, self.dimmedColor())
378  draw_manager.line(br, bl)
379  draw_manager.endDrawable()
380 
381  # Left
382  draw_manager.beginDrawable(OpenMayaRender.MUIDrawManager.kSelectable, self.left_name)
383  self.setHandleColor(draw_manager, self.left_name, self.mainColor())
384  draw_manager.line(bl, tl)
385  draw_manager.endDrawable()
386 
387  # virtual
388  def doPress( self, view ):
389  """
390  Handle the mouse press event in a VP2.0 viewport.
391  """
392  # Reset the mousePoint information on a new press.
393  self.mouse_point_gl_name = OpenMaya.MPoint.kOrigin
394  self.update_drag_information()
395 
396  # virtual
397  def doDrag( self, view ):
398  """
399  Handle the mouse drag event in a VP2.0 viewport.
400  """
401  self.update_drag_information()
402 
403  # virtual
404  def doRelease( self, view ):
405  """
406  Handle the mouse release event in a VP2.0 viewport.
407  """
408  pass
409 
410  def set_draw_transform_info( self, rotation, translation ):
411  """
412  Store the given rotation and translation.
413  """
414  self.rotate_x = rotation[0]
415  self.rotate_y = rotation[1]
416  self.rotate_z = rotation[2]
417  self.translate_x = translation[0]
418  self.translate_y = translation[1]
419  self.translate_z = translation[2]
420 
421  def update_drag_information( self ):
422  """
423  Update the mouse's intersection location with the manipulator
424  """
425  # Find the mouse point in local space
426  self.local_mouse = self.mouseRay()
427 
428  # Find the intersection of the mouse point with the manip plane
429  mouse_intersection_with_manip_plane = self.plane.intersect( self.local_mouse )
430 
431  self.mouse_point_gl_name = mouse_intersection_with_manip_plane
432 
433  active = self.glActiveName()
434  if active:
435  start = OpenMaya.MPoint([0, 0, 0])
436  end = OpenMaya.MPoint([0, 0, 0])
437  if ( active == self.top_name ):
438  start = OpenMaya.MPoint(-0.5, 0.5, 0.0)
439  end = OpenMaya.MPoint( 0.5, 0.5, 0.0)
440  if ( active == self.bottom_name ):
441  start = OpenMaya.MPoint(-0.5, -0.5, 0.0)
442  end = OpenMaya.MPoint( 0.5, -0.5, 0.0)
443  if ( active == self.right_name ):
444  start = OpenMaya.MPoint( 0.5, 0.5, 0.0)
445  end = OpenMaya.MPoint( 0.5, -0.5, 0.0)
446  if ( active == self.left_name ):
447  start = OpenMaya.MPoint(-0.5, 0.5, 0.0)
448  end = OpenMaya.MPoint(-0.5, -0.5, 0.0)
449 
450  if ( active ):
451  # Find a vector on the plane
452  a = OpenMaya.MPoint( start.x, start.y, start.z )
453  b = OpenMaya.MPoint( end.x, end.y, end.z )
454  vab = a - b
455 
456  # Define line with a point and a vector on the plane
457  line = LineMath()
458  line.set_line( start, vab )
459 
460  # Find the closest point so that we can get the
461  # delta change of the mouse in local space
462  cpt = line.closest_point( self.mouse_point_gl_name )
463  self.mouse_point_gl_name -= cpt
464 
465  min_change_value = min( self.mouse_point_gl_name.x, self.mouse_point_gl_name.y, self.mouse_point_gl_name.z )
466  max_change_value = max( self.mouse_point_gl_name.x, self.mouse_point_gl_name.y, self.mouse_point_gl_name.z )
467  if ( active == self.right_name ):
468  self.setDoubleValue( self.right_index, max_change_value )
469  if ( active == self.left_name ):
470  self.setDoubleValue( self.right_index, min_change_value )
471 
472 
473 # command
474 class SquareScaleManipContextCmd (OpenMayaUI.MPxContextCommand):
475  """
476  This command class is used to create instances of the SquareScaleManipContext class.
477  """
478  kPluginCmdName = "py2SquareScaleManipContext"
479 
480  kNopFlag = "-nop"
481  kNopLongFlag = "-noOperation"
482 
483  def __init__(self):
485 
486  @staticmethod
487  def creator():
488  return SquareScaleManipContextCmd()
489 
490  def doQueryFlags(self):
491  theParser = self.parser()
492  if( theParser.isFlagSet(SquareScaleManipContextCmd.kNopFlag) ):
493  print("Doing absolutely nothing")
494  return
495 
496  def makeObj(self):
497  """
498  Create and return an instance of the SquareScaleManipContext class.
499  """
500  return SquareScaleManipContext()
501 
502  def appendSyntax(self):
503  theSyntax = self.syntax()
504  theSyntax.addFlag(SquareScaleManipContextCmd.kNopFlag, SquareScaleManipContextCmd.kNopLongFlag)
505 
506 
507 class SquareScaleManipContext(OpenMayaUI.MPxSelectionContext):
508  """
509  This context handles all mouse interaction in the viewport when activated.
510  When activated, it creates and manages an instance of the SquareScaleManuplator
511  class on the selected objects.
512  """
513 
514  kContextName = 'SquareScaleManipContext'
515 
516  @classmethod
517  def creator(cls):
518  return cls()
519 
520  def __init__(self):
521  """
522  Initialize the members of the SquareScaleManipContext class.
523  """
525  self.setTitleString('Plug-in manipulator: ' + SquareScaleManipContext.kContextName)
526  self.manipulator_class_ptr = None
527  self.first_object_selected = None
528  self.active_list_modified_msg_id = -1
529 
530  # virtual
531  def toolOnSetup(self, event):
532  """
533  Set the help string and selection list callback.
534  """
535  self.setHelpString('Move the object using the manipulator')
536 
537  SquareScaleManipContext.update_manipulators_cb(self)
538  try:
539  self.active_list_modified_msg_id = OpenMaya.MModelMessage.addCallback( \
540  OpenMaya.MModelMessage.kActiveListModified, \
541  SquareScaleManipContext.update_manipulators_cb, self)
542  except:
543  OpenMaya.MGlobal.displayError("SquareScaleManipContext.toolOnSetup(): Model addCallback failed")
544 
545  # Removes the callback
546  # virtual
547  def toolOffCleanup(self):
548  """
549  Unregister the selection list callback.
550  """
551  try:
552  OpenMaya.MModelMessage.removeCallback(self.active_list_modified_msg_id)
553  self.active_list_modified_msg_id = -1
554  except:
555  OpenMaya.MGlobal.displayError("SquareScaleManipContext.toolOffCleanup(): Model remove callback failed")
556 
558 
559  # virtual
560  def namesOfAttributes(self, attribute_names):
561  """
562  Return the names of the attributes of the selected objects this context will be modifying.
563  """
564  attribute_names.append('scaleX')
565 
566  # virtual
567  def setInitialState(self):
568  """
569  Set the initial transform of the manipulator.
570  """
571  xform = OpenMaya.MFnTransform( self.first_object_selected )
572  xformMatrix = xform.transformation()
573  translation = xformMatrix.translation( OpenMaya.MSpace.kWorld )
574  rotation = xformMatrix.rotation(False)
575 
576  self.manipulator_class_ptr.set_draw_transform_info( rotation, translation )
577 
578  # Ensure that valid geometry is selected
579  def valid_geometry_selected(self):
580  """
581  Check to make sure the selected objects have transforms.
582  """
583  list = None
584  iter = None
585  try:
587  iter = OpenMaya.MItSelectionList(list)
588  except:
589  logger.info(" Could not get active selection")
590  return False
591 
592  if (not list) or (list.length() == 0):
593  return False
594 
595  while not iter.isDone():
596  depend_node = iter.getDependNode()
597  if (depend_node.isNull() or (not depend_node.hasFn(OpenMaya.MFn.kTransform))):
598  OpenMaya.MGlobal.displayWarning('Object in selection list is not right type of node')
599  return False
600  next(iter)
601  return True
602 
603  def update_manipulators_cb(ctx):
604  """
605  Callback that creates the manipulator if valid geometry is selected. Also removes
606  the manipulator if no geometry is selected. Handles connecting the manipulator to
607  multiply selected nodes.
608  """
609  try:
610  ctx.deleteManipulators()
611  except:
612  logger.info(" No manipulators to delete")
613 
614  try:
615  if not ctx.valid_geometry_selected():
616  return
617 
618  # Clear info
619  ctx.manipulator_class_ptr = None
620  ctx.first_object_selected = OpenMaya.MObject.kNullObj
621 
622  (manipulator, manip_object) = SquareScaleManipulator.newManipulator('py2SquareScaleContextManipulator')
623 
624  if manipulator:
625  # Save state
626  ctx.manipulator_class_ptr = manipulator
627 
628  # Add the manipulator
629  ctx.addManipulator(manip_object)
630 
632  iter = OpenMaya.MItSelectionList(list)
633 
634  while not iter.isDone():
635  depend_node = iter.getDependNode()
636  depend_node_fn = OpenMaya.MFnDependencyNode(depend_node)
637 
638  # Connect the manipulator to the object in the selection list.
639  if (not manipulator.connectToDependNode(depend_node)):
640  OpenMaya.MGlobal.displayWarning('Error connecting manipulator to object %s' % depend_node_fn.name())
641  next(iter)
642  continue
643 
644  if ( ctx.first_object_selected == OpenMaya.MObject.kNullObj ):
645  ctx.first_object_selected = depend_node
646  next(iter)
647 
648  # Allow the manipulator to set initial state
649  ctx.setInitialState()
650 
651  except:
652  OpenMaya.MGlobal.displayError('Failed to create new manipulator')
653  return
654 
655  update_manipulators_cb = staticmethod(update_manipulators_cb)
656 
657 
658 # Initialize the script plug-in
659 def initializePlugin(plugin):
660  pluginFn = OpenMaya.MFnPlugin(plugin)
661 
662  try:
663  pluginFn.registerContextCommand( SquareScaleManipContextCmd.kPluginCmdName, SquareScaleManipContextCmd.creator)
664  except:
665  sys.stderr.write("Failed to register context command: %s\n" % SquareScaleManipContextCmd.kPluginCmdName)
666  raise
667 
668  try:
669  pluginFn.registerNode( SquareScaleManipulator.kNodeName, SquareScaleManipulator.kTypeId, \
670  SquareScaleManipulator.creator, SquareScaleManipulator.initialize, \
671  OpenMaya.MPxNode.kManipulatorNode)
672  except:
673  sys.stderr.write("Failed to register node: %s\n" % SquareScaleManipulator.kNodeName)
674  raise
675 
676 
677 # Uninitialize the script plug-in
678 def uninitializePlugin(plugin):
679  pluginFn = OpenMaya.MFnPlugin(plugin)
680  try:
681  pluginFn.deregisterContextCommand(SquareScaleManipContextCmd.kPluginCmdName)
682  except:
683  sys.stderr.write(
684  "Failed to unregister command: %s\n" % SquareScaleManipContextCmd.kPluginCmdName)
685  raise
686 
687  try:
688  pluginFn.deregisterNode(SquareScaleManipulator.kTypeId)
689  except:
690  sys.stderr.write(
691  "Failed to unregister node: %s\n" % SquareScaleManipulator.kNodeName)
692  raise
693 
694 # =======================================================================
695 # Copyright 2020 Autodesk, Inc. All rights reserved.
696 #
697 # This computer source code and related instructions and comments are the
698 # unpublished confidential and proprietary information of Autodesk, Inc.
699 # and are protected under applicable copyright and trade secret law. They
700 # may not be disclosed to, copied or used by any third party without the
701 # prior written consent of Autodesk, Inc.
702 # =======================================================================