Python API 2.0 Reference
python/api2/py2LassoTool.py
1 from __future__ import division
2 ########################################################################
3 #
4 # DESCRIPTION:
5 #
6 # Produces the MEL command "lassoToolContext".
7 #
8 # This plug-in demonstrates "lasso selection" in a user defined context.
9 # It is supported in Viewport 2.0 and the Legacy Viewport.
10 # Selection is done through the API (MGlobal).
11 #
12 ########################################################################
13 #
14 # First make sure that lassoTool.py is in your MAYA_PLUG_IN_PATH. Then,
15 # to use the tool, execute the following in the script editor:
16 #
17 # cmds.loadPlugin("lassoTool.py")
18 # ctx = cmds.lassoToolContext()
19 # cmds.setToolTo(ctx)
20 #
21 # With Maya in component mode and the tool active, lasso select components
22 # in the viewport. Holding the shift key down will toggle the selection
23 # of the enclosed components.
24 #
25 ########################################################################
26 
27 from builtins import next
28 from builtins import range
29 import logging
30 import math
31 import sys
32 from maya.api import OpenMaya, OpenMayaRender, OpenMayaUI
33 from maya import OpenMayaRender as OpenMayaRenderV1
34 
35 logger = logging.getLogger('lassoTool')
36 
37 # tell Maya that we want to use Python API 2.0
38 maya_useNewAPI = True
39 
40 # command
41 class LassoToolContextCmd (OpenMayaUI.MPxContextCommand):
42  """
43  This context command class creates instances of the LassoToolContext.
44  """
45  kPluginCmdName = "py2LassoToolContext"
46 
47  def __init__(self):
49 
50  @classmethod
51  def creator(cls):
52  """
53  This factory method creates an instance of the LassoToolContextCmd class.
54  """
55  return cls()
56 
57  def makeObj(self):
58  """
59  This factory method creates an instance of the LassoToolContext class.
60  """
61  return LassoToolContext()
62 
63 class LassoToolContext(OpenMayaUI.MPxContext):
64  """
65  This context class extends a bounding box as the user drags the cursor during a selection
66  opeartion.
67  """
68 
69  help_string = "Drag mouse to select points by encircling"
70 
71  cursor_width = 16
72  cursor_height = 16
73  cursor_x_hot = 1
74  cursor_y_hot = 16
75  cursor_bits = bytearray( [
76  0x00, 0x00, 0x00, 0x00, 0xe0, 0x07, 0x10, 0x08, 0x10, 0x10, 0x08, 0x10,
77  0x08, 0x10, 0x08, 0x10, 0x08, 0x08, 0x08, 0x08, 0x14, 0x08, 0x14, 0x04,
78  0x08, 0x07, 0xf4, 0x00, 0x02, 0x00, 0x01, 0x00] )
79  cursor_mask_bits = bytearray( [
80  0x00, 0x00, 0x00, 0x00, 0xe0, 0x07, 0x10, 0x08, 0x10, 0x10, 0x08, 0x10,
81  0x08, 0x10, 0x08, 0x10, 0x08, 0x08, 0x08, 0x08, 0x14, 0x08, 0x14, 0x04,
82  0x08, 0x07, 0xf4, 0x00, 0x02, 0x00, 0x01, 0x00] )
83 
84  @classmethod
85  def creator(cls):
86  """
87  Create and return an instance of the LassoToolContext class.
88  """
89  return LassoToolContext(cls)
90 
91  def __init__(self):
92  """
93  Initialize the context member variables.
94  """
96  self.lasso = []
97  self.sorted_lasso = []
98  self.lasso_cursor = OpenMayaUI.MCursor(width=self.cursor_width,
99  height=self.cursor_height,
100  hotSpotX=self.cursor_x_hot,
101  hotSpotY=self.cursor_y_hot,
102  bits=self.cursor_bits,
103  mask=self.cursor_mask_bits)
104 
105  self.first_draw = False
106  self.list_adjustment = 0
107  self.view = None
108  self.setTitleString('Lasso Tool')
109 
110  # Set the initial state of the cursor.
111  self.setCursor(self.lasso_cursor)
112 
113  # Tell the context which XPM to use.
114  self.setImage('lassoTool.xpm', OpenMayaUI.MPxContext.kImage1)
115 
116  def stringClassName(self):
117  """
118  Return the class name string.
119  """
120  return 'Lasso Tool'
121 
122  def toolOnSetup( self, event ):
123  """
124  Perform any setup operations when the tool is created. In this case,
125  set the help string.
126  """
127  self.setHelpString( LassoToolContext.help_string )
128 
129  def do_press_common( self, event ):
130  """
131  Perfom the press operations common to both VP2.0 and the Legacy Viewport.
132  """
133 
134  # Figure out which modifier keys were pressed, and set up the
135  # listAdjustment parameter to reflect what to do with the selected points.
136  if event.isModifierShift() or event.isModifierControl():
137  if event.isModifierShift():
138  if event.isModifierControl():
139  # both shift and control pressed, merge new selections
140  self.list_adjustment = OpenMaya.MGlobal.kAddToList
141  else:
142  # shift only, xor new selections with previous ones
143  self.list_adjustment = OpenMaya.MGlobal.kXORWithList
144 
145  elif event.isModifierControl():
146  # control only, remove new selections from the previous list
147  self.list_adjustment = OpenMaya.MGlobal.kRemoveFromList
148  else:
149  self.list_adjustment = OpenMaya.MGlobal.kReplaceList
150 
151  start = event.position
152  self.lasso.append([start[0], start[1]])
153  self.min = [start[0], start[1]]
154  self.max = [start[0], start[1]]
155  self.first_draw = True
156  self.view = OpenMayaUI.M3dView.active3dView()
157 
158  def do_release_common( self, event ):
159  """
160  Perfom the release operations common to both VP2.0 and the Legacy Viewport.
161  """
162 
163  # Close the lasso by appending the starting point, and sort all the points on it.
164  self.append_lasso(self.lasso[0])
165  self.sorted_lasso = sorted(self.lasso, key=lambda x: (x[1], x[0]))
166 
167  # Save the state of the current selections. The "selectFromSceen"
168  # below will alter the active list, and we have to be able to put
169  # it back.
171 
172  # As a first approximation to the lasso, select all components with
173  # the bounding box that just contains the lasso.
174  OpenMaya.MGlobal.selectFromScreen( self.min[0], self.min[1], self.max[0], self.max[1],
175  OpenMaya.MGlobal.kReplaceList )
176 
177  # Get the list of selected items from within the bounding box
178  # and create a iterator for them.
179  bounding_box_list = OpenMaya.MGlobal.getActiveSelectionList()
180 
181  # Restore the active selection list to what it was before we
182  # the "selectFromScreen"
183  OpenMaya.MGlobal.setActiveSelectionList(incoming_list, OpenMaya.MGlobal.kReplaceList)
184 
185  # Iterate over the objects within the bounding box, extract the
186  # ones that are within the lasso, and add those to new_list.
187  iter = OpenMaya.MItSelectionList(bounding_box_list)
188  new_list = OpenMaya.MSelectionList()
189  new_list.clear()
190 
191  found_entire_objects = False
192  found_components = False
193 
194  while not iter.isDone():
195 
196  dag_path = iter.getDagPath()
197  component = OpenMaya.MObject.kNullObj
198  if iter.hasComponents():
199  sel = iter.getComponent()
200  component = sel[1]
201 
202  if component.isNull():
203  # not a component
204  found_entire_objects = True
205  next(iter)
206  continue
207 
208  found_components = True
209 
210  if component.apiType() == OpenMaya.MFn.kCurveCVComponent:
211  curve_cv_iter = OpenMaya.MItCurveCV( dag_path, component)
212  while not curve_cv_iter.isDone():
213  try:
214  point = curve_cv_iter.position(OpenMaya.MSpace.kWorld)
215  pt = self.view.worldToView(point)
216  if self.point_in_lasso(pt):
217  single_component = curve_cv_iter.currentItem()
218  new_list.add ((dag_path, single_component))
219  except:
220  OpenMaya.MGlobal.displayError("Could not get position")
221  next(curve_cv_iter)
222 
223  elif component.apiType() == OpenMaya.MFn.kSurfaceCVComponent:
224  surf_cv_iter = OpenMaya.MItSurfaceCV(dag_path, component, true)
225  while not surf_cv_iter.isDone():
226  try:
227  point = surf_cv_iter.position(OpenMaya.MSpace.kWorld)
228  pt = self.view.worldToView(point)
229  if self.point_in_lasso(pt):
230  single_component = surf_cv_iter.currentItem()
231  new_list.add((dag_path, single_component))
232  except:
233  OpenMaya.MGlobal.displayError("Could not get position")
234  next(surf_cv_iter)
235 
236  elif component.apiType() == OpenMaya.MFn.kMeshVertComponent:
237  vertex_iter = OpenMaya.MItMeshVertex(dag_path, component)
238  while not vertex_iter.isDone():
239  try:
240  point = vertex_iter.position(OpenMaya.MSpace.kWorld)
241  pt = self.view.worldToView(point)
242  if self.point_in_lasso(pt):
243  single_component = vertex_iter.currentItem()
244  new_list.add((dag_path, single_component))
245  except:
246  OpenMaya.MGlobal.displayError("Could not get position")
247  next(vertex_iter)
248 
249  elif component.apiType() == OpenMaya.MFn.kMeshEdgeComponent:
250  edge_iter = OpenMaya.MItMeshEdge(dag_path, component)
251  while not edge_iter.isDone():
252  try:
253  point = edge_iter.center(OpenMaya.MSpace.kWorld)
254  pt = view.worldToView(point)
255  if self.point_in_lasso(pt):
256  single_component = edge_iter.currentItem()
257  new_list.add ((dag_path, single_component))
258  except:
259  OpenMaya.MGlobal.displayError("Could not get position")
260  next(edge_iter)
261 
262  elif component.apiType() == OpenMaya.MFn.kMeshPolygonComponent:
263  polygon_iter = OpenMaya.MItMeshPolygon(dag_path, component)
264  while not polygon_iter.isDone():
265  try:
266  point = polygon_iter.center(OpenMaya.MSpace.kWorld)
267  pt = view.worldToView(point)
268  if self.point_in_lasso(pt):
269  single_component = polygon_iter.currentItem();
270  new_list.add ((dag_path, single_component))
271  except:
272  OpenMaya.MGlobal.displayError("Could not get position")
273  next(polygon_iter)
274 
275  next(iter)
276 
277  # Warn user if they are trying to select objects rather than components.
278  if found_entire_objects and (not found_components):
279  OpenMaya.MGlobal.displayWarning("lassoTool can only select components, not entire objects.")
280 
281  # Update the selection list as indicated by the modifier keys.
282  OpenMaya.MGlobal.selectCommand(new_list, self.list_adjustment)
283 
284  # Free the memory that held the lasso points.
285  self.lasso = []
286  self.sorted_lasso = []
287 
288  def draw_lasso_gl( self ):
289  """
290  Draw the lasso using OpenGL. This method is used by the Legacy Viewport.
291  """
292  self.view.beginGL()
293 
294  # Drawing in VP1 views will be done using V1 Python APIs:
295  gl_renderer = OpenMayaRenderV1.MHardwareRenderer.theRenderer()
296  gl_ft = gl_renderer.glFunctionTable()
297  gl_ft.glBegin( OpenMayaRenderV1.MGL_LINE_LOOP )
298  for i in range(len(self.lasso)):
299  gl_ft.glVertex2i( self.lasso[i][0], self.lasso[i][1] );
300  gl_ft.glEnd()
301 
302  self.view.endGL()
303 
304  def append_lasso( self, pt ):
305  """
306  Append the given point to the points defining the lasso.
307  """
308  x = pt[0]
309  y = pt[1]
310  [ix, iy] = self.lasso[-1]
311  ydif = int(math.fabs( y - iy ))
312  if ( ydif == 0 ):
313  return
314 
315  # Keep track of smallest rectangular area of the screen that
316  # completely contains the lasso.
317  if ( self.min[0] > x ):
318  self.min[0] = x
319  if ( self.max[0] < x ):
320  self.max[0] = x
321  if ( self.min[1] > y ):
322  self.min[1] = y
323  if ( self.max[1] < y ):
324  self.max[1] = y
325 
326  if ((y - iy ) < 0):
327  yinc = -1
328  else:
329  yinc = 1
330 
331  xinc = (x - ix)/ydif
332  fx = ix + xinc
333  cy = iy + yinc
334  for i in range(ydif):
335  self.lasso.append([int(fx), int(cy)])
336  fx = fx + xinc
337  cy = cy + yinc
338 
339  def point_in_lasso( self, pt ):
340  """
341  Check the given point to see if it's inside the loop defined by the lasso.
342  """
343  for i in range(len(self.lasso)):
344  if (self.sorted_lasso[i][1] == pt[1]):
345  while ((self.sorted_lasso[i][1] == pt[1]) and (self.sorted_lasso[i][0] < pt[0])):
346  i = i + 1
347  if (self.sorted_lasso[i][1] != pt[1]):
348  return False
349  sides = 0
350  i = i + 1
351  while (self.sorted_lasso[i][1] == pt[1]):
352  i = i + 1
353  sides = sides + 1
354 
355  if (sides % 2):
356  return False
357  else:
358  return True
359  return False
360 
361  def doPressLegacy( self, event ):
362  """
363  Handle the mouse press event in the Legacy Viewport.
364  """
365  self.do_press_common(event)
366  self.first_draw = False
367 
368  def doDragLegacy( self, event ):
369  """
370  Handle the mouse drag event in the Legacy Viewport. Add to the growing lasso.
371  """
372  self.view.beginXorDrawing(True, True, 1.0, OpenMayaUI.M3dView.kStippleDashed)
373  if not self.first_draw:
374  # Redraw the old lasso to clear it.
375  self.draw_lasso_gl()
376  else:
377  self.first_draw = False
378 
379  # Get the current position and append it to the lasso.
380  current_pos = event.position
381  self.append_lasso( current_pos )
382 
383  # Draw the new lasso.
384  self.draw_lasso_gl()
385  self.view.endXorDrawing()
386 
387  def doReleaseLegacy( self, event ):
388  """
389  Handle the mouse release event in the Legacy Viewport.
390  """
391  # Selects objects within the lass.
392  if self.first_draw:
393  # Redraw the lasso to clear it.
394  self.view.beginXorDrawing(True, True, 1.0, OpenMayaUI.M3dView.kStippleDashed);
395  self.draw_lasso_gl()
396  self.view.endXorDrawing()
397 
398  self.do_release_common( event );
399 
400  def doPress( self, event, drawMgr, context ):
401  """
402  Handle the mouse press event in VP2.0.
403  """
404  self.do_press_common(event)
405 
406  def doRelease( self, event, drawMgr, context ):
407  """
408  Handle the release press event in VP2.0.
409  """
410  self.do_release_common(event)
411 
412  def doDrag( self, event, draw_mgr, context ):
413  """
414  Handle the mouse drag event in VP2.0.
415  """
416  # Get the current position and append it to the lasso.
417  current_pos = event.position
418  self.append_lasso( current_pos )
419 
420  # Draw the lasso.
421  draw_mgr.beginDrawable()
422  draw_mgr.setColor( OpenMaya.MColor((1.0, 1.0, 0.0)) )
423  for i in range(len(self.lasso)-1):
424  draw_mgr.line2d( OpenMaya.MPoint( (self.lasso[i][0], self.lasso[i][1])), \
425  OpenMaya.MPoint((self.lasso[i+1][0], self.lasso[i+1][1])) )
426 
427  # Draw a final segment from the last lasso point to the first to close the loop.
428  draw_mgr.line2d( OpenMaya.MPoint( (self.lasso[-1][0], self.lasso[-1][1])), \
429  OpenMaya.MPoint((self.lasso[0][0], self.lasso[0][1])) )
430  draw_mgr.endDrawable()
431 
432  def doEnterRegion( self, event ):
433  """
434  Handle the enter region event. This method is called from both VP2.0 and the Legacy Viewport.
435  """
436  self.setHelpString( LassoToolContext.help_string )
437 
438 # Initialize the script plug-in
439 def initializePlugin(plugin):
440  pluginFn = OpenMaya.MFnPlugin(plugin)
441 
442  try:
443  pluginFn.registerContextCommand( LassoToolContextCmd.kPluginCmdName, LassoToolContextCmd.creator)
444  except:
445  logger.error("Failed to register context command: %s\n" % LassoToolContextCmd.kPluginCmdName)
446  raise
447 
448 
449 # Uninitialize the script plug-in
450 def uninitializePlugin(plugin):
451  pluginFn = OpenMaya.MFnPlugin(plugin)
452  try:
453  pluginFn.deregisterContextCommand(LassoToolContextCmd.kPluginCmdName)
454  except:
455  logger.error("Failed to unregister command: %s\n" % LassoToolContextCmd.kPluginCmdName)
456  raise
457 
458 #-
459 # ==========================================================================
460 # Copyright (C) 2016 Autodesk, Inc. and/or its licensors. All
461 # rights reserved.
462 #
463 # The coded instructions, statements, computer programs, and/or related
464 # material (collectively the "Data") in these files contain unpublished
465 # information proprietary to Autodesk, Inc. ("Autodesk") and/or its
466 # licensors, which is protected by U.S. and Canadian federal copyright
467 # law and by international treaties.
468 #
469 # The Data is provided for use exclusively by You. You have the right
470 # to use, modify, and incorporate this Data into other products for
471 # purposes authorized by the Autodesk software license agreement,
472 # without fee.
473 #
474 # The copyright notices in the Software and this entire statement,
475 # including the above license grant, this restriction and the
476 # following disclaimer, must be included in all copies of the
477 # Software, in whole or in part, and all derivative works of
478 # the Software, unless such copies or derivative works are solely
479 # in the form of machine-executable object code generated by a
480 # source language processor.
481 #
482 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
483 # AUTODESK DOES NOT MAKE AND HEREBY DISCLAIMS ANY EXPRESS OR IMPLIED
484 # WARRANTIES INCLUDING, BUT NOT LIMITED TO, THE WARRANTIES OF
485 # NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR
486 # PURPOSE, OR ARISING FROM A COURSE OF DEALING, USAGE, OR
487 # TRADE PRACTICE. IN NO EVENT WILL AUTODESK AND/OR ITS LICENSORS
488 # BE LIABLE FOR ANY LOST REVENUES, DATA, OR PROFITS, OR SPECIAL,
489 # DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES, EVEN IF AUTODESK
490 # AND/OR ITS LICENSORS HAS BEEN ADVISED OF THE POSSIBILITY
491 # OR PROBABILITY OF SUCH DAMAGES.
492 #
493 # ==========================================================================
494 #+