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