mrv.maya.nt.set
Covered: 431 lines
Missed: 11 lines
Skipped 228 lines
Percent: 97 %
  2
""" Contains improved clases for set and partition editing  """
  3
__docformat__ = "restructuredtext"
  5
import base as nt
  6
import typ
  7
import maya.OpenMaya as api
  8
import maya.cmds as cmds
  9
import it
 10
import mrv.maya.undo as undo
 14
class ConstraintError( RuntimeError ):
 15
	"""Thrown if a partition does not allow objects to be added, and the addition 
 16
	was not forced, and failure was not ignored as well"""
 20
class ObjectSet:
 21
	""" Extended and more convenient object set interface dealing with Nodes ( and 
 22
	provides the original MFnSet interface as well
 23
	"""                                                               
 24
	__metaclass__ = typ.MetaClassCreatorNodes
 25
	kReplace, kAdd,kAddForce, kRemove = range( 4 )
 29
	def partitions( self ):
 30
		""":return: list of Nodes of partitions the entity is set is part of"""
 31
		return [ p.mwrappedNode() for p in self.partition.moutputs() ]
 34
	@undoable 	
 35
	def setPartition( self, partition, mode = 0 ):
 36
		"""Add, add exclusive or remove the given partition from our partition list
 38
		:param partition: Node, representing the partition, or a list of such
 39
		:param mode:
 40
			 * 0 = replace
 41
			 * 1 = add 
 42
			 * 2 = remove 
 43
		:return: self for chained operations
 44
		:note: use the supplied enumeration to specify the mode"""
 46
		prts = partition
 47
		if isinstance( partition, Partition ):
 48
			prts = [ partition ]
 50
		if mode == self.kReplace:
 51
			self.setPartition( self.partitions( ), self.kRemove )
 52
			mode = self.kAdd		# now the partitions have to be added
 56
		if mode == self.kRemove:
 57
			for part in prts:
 58
				self.partition.mdisconnectNode( part )
 59
			return self
 62
		if mode == self.kAdd:
 64
			for part in prts:
 65
				self.partition.mconnectToArray( part.st, exclusive_connection = True )
 67
			return self
 70
		raise AssertionError( "Invalid mode given: %i" % mode )
 76
	def _toMemberObj( self, member ):
 77
		"""Convert member to a valid member object ( MObject, DagPath or Plug )"""
 78
		memberobj = member
 80
		if isinstance( member, nt.DagNode ):
 81
			memberobj = member.dagPath()
 82
		elif isinstance( member, nt.DependNode ):
 83
			memberobj = member.object()
 85
		return memberobj
 88
	def _forceMembership( self, member, component, is_single_member, ignore_failure ):
 89
		"""Search all sets connected to our partitions 
 90
		for intersecting members and remove them.
 91
		Finally dd the members in question to us again
 93
		:param member: can be selection list or MObject, MDagPath, MPlug
 94
		:return: self if everything is fine"""
 95
		for partition in self.partitions():
 96
			for otherset in partition.sets():
 97
				if is_single_member:
 98
					otherset.removeMember( member, component = component )
 99
				else:
100
					otherset.removeMembers( otherset.intersection( member, sets_are_members = True ) )
107
		if isinstance( member, api.MSelectionList ):
108
			return self._addRemoveMembers( member, self.kAdd, ignore_failure )
109
		else:
110
			return self._addRemoveMember( member, component, self.kAdd, ignore_failure )
113
	def _checkMemberAddResult( self, member, component, mode, ignore_failure, is_single_member ):
114
		"""Check whether the given member has truly been added to our set
115
		and either force membership or raise and exception
117
		:param is_single_member: if True, member can safely be assumed to be a single member, 
118
			this speeds up operations as we do not have to use multi-member tests"""
119
		if mode in ( self.kAdd, self.kAddForce ):
121
			if mode == self.kAdd and ignore_failure:
122
				return self
125
			numMatches = 1
126
			if isinstance( member, api.MSelectionList ):
127
				numMatches = member.length()
130
			not_all_members_added = True
131
			if is_single_member:
132
				not_all_members_added = not self.isMember( member, component = component )
133
			else:
134
				not_all_members_added = self.intersection( member, sets_are_members = True ).length() != numMatches
136
			if not_all_members_added:
137
				if mode == self.kAddForce:
138
					return self._forceMembership( member, component, is_single_member, ignore_failure )
141
				raise ConstraintError( "At least some members of %r could not be added to %r due to violation of exclusivity constraint" % (member,self) )
145
		return self
148
	def _addRemoveMember( self, member, component, mode, ignore_failure ):
149
		"""Add or remove the member with undo support
151
		:param mode: kRemove or kAdd"""
152
		memberobj = self._toMemberObj( member )
154
		op = undo.GenericOperation()
155
		mfninst = self._mfncls( self._apiobj )
156
		doitfunc = mfninst.addMember
157
		undoitfunc = mfninst.removeMember
160
		args = [ memberobj ]
161
		if isinstance( memberobj, api.MDagPath ):	# add component ( default None )
162
			args.append( component )
166
		if mode == ObjectSet.kRemove:
167
			tmp = undoitfunc
168
			undoitfunc = doitfunc
169
			doitfunc = tmp
172
		op.setDoitCmd( doitfunc, *args )
173
		op.setUndoitCmd( undoitfunc, *args )
174
		op.doIt( )
176
		return self._checkMemberAddResult( member, component, mode, ignore_failure, True )
178
	def _addRemoveMembers( self, members, mode, ignore_failure ):
179
		"""Add or remove the members to the set
181
		:param mode: kRemove or kAdd or kAddForce"""
182
		sellist = nt.toSelectionList( members )	# handles 'member is SelectionList' case !
184
		lsellist = sellist.length()
185
		if not lsellist:
186
			return self
190
		if lsellist == 1:
191
			return self._addRemoveMember( it.iterSelectionList( sellist, asNode = 0 ).next(), api.MObject(), mode, ignore_failure )
194
		mfninst = self._mfncls( self._apiobj )
195
		doitfunc = mfninst.addMembers
196
		undoitfunc = mfninst.removeMembers
199
		if mode == ObjectSet.kRemove:
200
			tmp = undoitfunc
201
			undoitfunc = doitfunc
202
			doitfunc = tmp
208
			sellist = self.intersection(sellist)
211
		op = undo.GenericOperation()	
212
		op.setDoitCmd( doitfunc, sellist )
213
		op.setUndoitCmd( undoitfunc, sellist )
214
		op.doIt()
216
		return self._checkMemberAddResult( sellist, None, mode, ignore_failure, False )
218
	@undoable
219
	def clear( self ):
220
		"""Clear the set so that it will be empty afterwards
222
		:return: self"""
223
		self.removeMembers( self.getMembers() )
224
		return self
226
	@undoable
227
	def addMember( self, member, component = api.MObject(), force = False, ignore_failure = False ):
228
		"""Add the item to the set
230
		:param member: Node, MObject, MDagPath or plug
231
		:param force: if True, member ship will be forced by removing the member in question 
232
			from the other set connected to our partitions
233
		:param ignore_failure: if True, a failed add due to partion constraints will result in an 
234
			exception, otherwise it will be silently ignored. Ignored if if force is True
235
		:param component: if member is a dagnode, you can specify a component instance 
236
			of type component instance ( Single|Double|TripleIndexComponent )
237
		:todo: handle components - currently its only possible when using selection lists
238
		:return: self """
239
		mode = self.kAdd
240
		if force:
241
			mode = self.kAddForce
242
		return self._addRemoveMember( member, component, mode, ignore_failure )		
244
	@undoable
245
	def add( self, member_or_members, *args, **kwargs ):
246
		"""Combined method which takes single or multiple members which are to be added
248
		:param member_or_members: one of the input types supported by `addMember` and
249
			`addMembers`
250
		:param kwargs: see `addMember`
251
		:note: this method is for convenience only and should not be used to 
252
			add massive amounts of items"""
253
		addfun = None
254
		if isinstance(member_or_members, (tuple, list, api.MSelectionList)):
255
			addfun = self.addMembers
256
		else:
257
			addfun = self.addMember
259
		return addfun(member_or_members, *args, **kwargs)
261
	@undoable
262
	def removeMember( self, member, component = api.MObject()  ):
263
		"""Remove the member from the set
265
		:param member: member of the list, for types see `addMember`"""
266
		return self._addRemoveMember( member, component, ObjectSet.kRemove, True )
268
	@undoable
269
	def discard( self, member_or_members, *args, **kwargs ):
270
		"""Removes a single member or multiple members from the set
272
		:param member_or_members: any of the types supported by `removeMember`
273
			or `removeMembers`
274
		:param kwargs: see `removeMember`"""
275
		rmfun = None
276
		if isinstance(member_or_members, (tuple, list, api.MSelectionList)):
277
			rmfun = self.removeMembers
278
		else:
279
			rmfun = self.removeMember
281
		return rmfun(member_or_members, *args, **kwargs)
283
	@undoable
284
	def addMembers( self, nodes, force = False, ignore_failure = False ):
285
		"""Add items from iterable or selection list as members to this set
287
		:param nodes: MSelectionList or list of Nodes and Plugs
288
		:param force: see `addMember`
289
		:param ignore_failure: see `addMember`
290
		:return: self """
291
		mode = self.kAdd
292
		if force:
293
			mode = self.kAddForce
294
		return self._addRemoveMembers( nodes, mode, ignore_failure )
296
	@undoable
297
	def removeMembers( self, nodes ):
298
		"""Remove items from iterable or selection list from this set
300
		:param nodes: see `addMembers`
301
		:return: self """
302
		return self._addRemoveMembers( nodes, ObjectSet.kRemove, True )
304
	@undoable
305
	def setMembers( self, nodes, mode, **kwargs ):
306
		"""Adjust set membership for nodes
308
		:param nodes: items to handle, supports everything that `addMembers` does
309
		:param kwargs: arguments passed to `addMembers` or `removeMembers`"""
310
		if mode == self.kReplace:
311
			self.clear()
312
			mode = self.kAdd
315
		if mode == self.kAdd:
316
			return self.addMembers( nodes, **kwargs )
319
		return self.removeMembers( nodes, **kwargs )
326
	def getMembers( self, flatten = False ):
327
		""":return: MSelectionList with members of this set
328
		:param flatten: if True, members that are objectSets themselves will be resolved to their 
329
			respective members
330
		:note: the members are ordinary api objects that still need to be wrapped
331
		:note: use iterMembers to iterate the members as wrapped Nodes"""
332
		sellist = api.MSelectionList()
333
		self._mfncls( self._apiobj ).getMembers( sellist, flatten )
334
		return sellist
336
	def iterMembers( self, *args, **kwargs ):
337
		"""Iterate members of this set
339
		:note: All keywords of iterMembers are supported
340
		:note: if 'handlePlugs' is False, the iteration using a filter type will be faster
341
		:note: handleComponents will allow component iteration - see the iterator documentation"""
342
		return it.iterSelectionList( self.getMembers( ), *args, **kwargs ) 
344
	def isMember( self, obj, component = api.MObject() ):
345
		""":return: True if obj is a member of this set
346
		:param component: is given, the component must be fully part of the set 
347
			for the object ( dagNode ) to be considered part of the set
348
		:note: all keywords of `it.iterSelectionList` are supported
349
		:note: ismember does not appear to be working properly with component assignments.
350
			It returns true for components that are not actually in the givne shading group"""
351
		if not component.isNull():
352
			return self._mfncls( self._apiobj ).isMember( self._toMemberObj( obj ), component )
353
		return self._mfncls( self._apiobj ).isMember( self._toMemberObj( obj ) )
358
	members = getMembers
363
	class _TmpSet( object ):
364
		"""Temporary set that will delete itself once its python destructor is called"""
365
		__slots__ = "setobj"
366
		def __init__( self, sellist ):
367
			dgmod = api.MDGModifier( )
368
			self.setobj = dgmod.createNode( "objectSet" )
369
			dgmod.doIt( )
371
			mfnset = api.MFnSet( self.setobj )
372
			mfnset.addMembers( sellist )
374
		def __del__( self ):
375
			"""Delete our own set upon deletion"""
378
			mfnset = api.MFnSet( self.setobj )
379
			mfnset.clear()
380
			del( mfnset )
381
			dgmod = api.MDGModifier()
382
			dgmod.deleteNode( self.setobj )
383
			dgmod.doIt()
386
	@classmethod
387
	def _toValidSetOpInput( cls, objects, sets_are_members = False ):
388
		"""Method creating valid input for the union/intersection or difference methods
390
		:note: it may return a temporary set that will delete itself once the wrapper object
391
			is being destroyed
392
		:param sets_are_members: see `union`
393
		:note: set """
394
		if isinstance( objects, (tuple, list) ):
396
			if not objects:		# emty list, return empty mobject array
397
				return api.MObjectArray( )
399
			if not sets_are_members and isinstance( objects[ 0 ], ObjectSet ):
400
				objarray = api.MObjectArray( )
401
				for setNode in objects: 
402
					objarray.append( setNode._apiobj )
403
				return objarray
404
			else:
406
				sellist = nt.toSelectionList( objects )
407
				return cls._TmpSet( sellist )
411
		singleobj = objects
412
		if isinstance( singleobj, api.MSelectionList ):	# Selection List ?
413
			return cls._TmpSet( singleobj )
415
		if not sets_are_members and isinstance( singleobj, ObjectSet ):				# Single Object Set ?
416
			return singleobj.object()
418
		if isinstance( singleobj, cls._TmpSet ):										# single set object 
419
			return singleobj.setobj
421
		if isinstance( singleobj, api.MObject ) and singleobj.hasFn( api.MFn.kSet ):	# MObject object set ?
422
			return singleobj
425
		if isinstance( singleobj, api.MObjectArray ):
426
			return singleobj
429
		return cls._toValidSetOpInput( ( singleobj, ), sets_are_members = sets_are_members ) # will create a tmpset then
431
		raise TypeError( "Type InputObjects for set operation ( %r ) was not recognized" % objects )
434
	def _applySetOp( self, objects, opid, **kwargs ):
435
		"""Apply the set operation with the given id"""
438
		obj = fobj = self._toValidSetOpInput( objects, **kwargs )
439
		outlist = api.MSelectionList()
440
		if isinstance( obj, self._TmpSet ):
441
			fobj = obj.setobj	# need to keep reference to _TmpSet until it was used
443
		mfnset = self._mfncls( self._apiobj )
444
		if opid == "union":
445
			mfnset.getUnion( fobj, outlist )
446
		elif opid == "intersection":
447
			mfnset.getIntersection( fobj, outlist )
448
		else:
449
			raise AssertionError( "Invalid Set Operation: %s" % opid )
451
		return outlist
453
	@classmethod
454
	def tmpSet( cls, objects, sets_are_members = False ):
455
		"""
456
		:return: temporary set that will delete itself once it's reference count
457
			reaches 0. Use rval.setobj to access the actual set, as the returned object is 
458
			just a hanlde to it. The handle is a valid input to the set functions as well
459
		:param objects: see `union`
460
		:param sets_are_members: see `union`
461
		:note: useful if you want to use the set member union, intersection or substraction 
462
			methods efficiently on many sets in a row - these internally operate on a set, thus 
463
			it is faster to use them with another set from the beginning to prevent creation of intermediate 
464
			sets"""
465
		return cls._toValidSetOpInput( objects, sets_are_members = sets_are_members )
467
	def getUnion( self, objects, sets_are_members = False  ):
468
		"""Create a union of the given items with the members of this set
470
		:param objects: an ObjectSet, an MObject of an object set, a list of ObjectSets 
471
			or a list of wrapped Objects or an MSelectionList or a single wrapped object . 
472
			If you have objects in a list as well as sets
473
			themselves, objects must come first as the operation will fail otherwise.
474
		:param sets_are_members: if True, objects can contain sets, but they should not be treated 
475
			as sets to apply the set operation with, they should simply be members of this set, and 
476
			thus need to be wrapped into a tmp set as well
477
		:return: MSelectionList of all objects of self and objects """
478
		return self._applySetOp( objects, "union", sets_are_members = sets_are_members )
480
	def getIntersection( self, objects, sets_are_members = False  ):
481
		"""As `union`, but returns the intersection ( items in common ) of this 
482
		set with objects
484
		:param objects: see `union`
485
		:param sets_are_members: see `union`
486
		:return: MSelectionList of objects being in self and in objects"""
487
		return self._applySetOp( objects, "intersection", sets_are_members = sets_are_members )
489
	def getDifference( self, objects, sets_are_members = False  ):
490
		"""return the result of ``self minus objects``, thus objects will be substracted from our obejcts
492
		:param objects: see `union`
493
		:param sets_are_members: see `union`
494
		:return: MSelectionList containing objects of self not being in objects list"""
496
		intersections = list()
497
		obj = fobj = self._toValidSetOpInput( objects, sets_are_members = sets_are_members )
498
		outlist = api.MSelectionList()
499
		if isinstance( obj, self._TmpSet ):
500
			fobj = obj.setobj	# need to keep reference to _TmpSet until it was used
504
		if not hasattr( fobj, '__iter__' ):
505
			fobj = [ fobj ]
507
		for item in fobj:
508
			intersections.append( self.intersection( item ) )
511
		for its in intersections:
512
			self.removeMembers( its )
514
		difference = self.getMembers()
517
		for its in intersections:
518
			self.addMembers( its )
520
		return difference
522
	def iterUnion( self, setOrSetsOrObjects, **kwargs ):
523
		"""As union, but returns an iterator
525
		:param kwargs: passed to it.iterSelectionList"""
526
		return it.iterSelectionList( self.union( setOrSetsOrObjects ), **kwargs )
528
	def iterIntersection( self, setOrSetsOrObjects, **kwargs ):
529
		"""As intersection, but returns an iterator
531
		:param kwargs: passed to it.iterSelectionList"""
532
		return it.iterSelectionList( self.intersection( setOrSetsOrObjects ), **kwargs )
534
	def iterDifference( self, setOrSetsOrObjects, **kwargs ):
535
		"""As difference, but returns an iterator
537
		:param kwargs: passed to it.iterSelectionList"""
538
		return it.iterSelectionList( self.difference( setOrSetsOrObjects ), **kwargs )
543
	union = getUnion
544
	intersection = getIntersection
545
	difference = getDifference
548
	__or__ = union
549
	__add__ = union
550
	__sub__ = difference
551
	__and__ = intersection
555
	def __len__(self):
556
		"""
557
		:warn: This method is possibly slow as it will retrieve all members 
558
			just to get the size of the set. Don't use it directly if you like performance""" 
559
		return len(self.getMembers())
561
	def __iter__(self):
562
		return self.getMembers().mtoIter()
564
	def __contains__( self, obj ):
565
		""":return: True if the given obj is member of this set"""
566
		return self.isMember( obj )
571
class ShadingEngine:
572
	"""Provides specialized methods able to deal better with shaders
573
	than the default implementation.
575
	:todo: Force exclusivity must be a little more elaborate - this could be overwritten
576
		and reimplemented to take care of the details"""
578
	__metaclass__ = typ.MetaClassCreatorNodes
582
class Partition:
583
	"""Deal with common set <-> partition interactions"""
584
	__metaclass__ = typ.MetaClassCreatorNodes
588
	@undoable
589
	def _addRemoveMember( self, objectset, mode ):
590
		sets = objectset
591
		if isinstance( objectset, ObjectSet ):
592
			sets = [ objectset ]
594
		for oset in sets:
595
			oset.setPartition( self, mode )
598
		return self
601
	def addMember( self, objectset ):
602
		"""Add the given objectset or list of sets to the partition
604
		:param objectset: one or multiple object sets 
605
		:return: self allowing chained calls"""
606
		return self._addRemoveMember( objectset, ObjectSet.kAdd )
608
	add = addMember
610
	def removeMember( self, objectset ):
611
		"""Remove the given objectset from the partition
613
		:param objectset: one or multiple object sets 
614
		:return: self allowing chained calls"""
615
		return self._addRemoveMember( objectset, ObjectSet.kRemove )
617
	discard = removeMember
619
	def replaceMember( self, objectset ):
620
		"""Replace existing objectsets with the given one(s)
622
		:param objectset: one or multiple object sets 
623
		:return: self allowing chained calls)"""
624
		return self._addRemoveMember( objectset, ObjectSet.kReplace )
626
	@undoable
627
	def clear( self ):
628
		"""remove all members from this partition
630
		:return: self"""
631
		for m in self.getMembers():                           
632
			self.removeMember( m )
634
		return self
636
	def getMembers( self ):
637
		""":return: sets being member of this partition
638
		:note: have to filter the members as there might be non-set connections 
639
			in referenced environments"""
640
		out = list()
641
		for plug in self.st.minputs():
642
			node = plug.mwrappedNode()
643
			if not node.hasFn( api.MFn.kSet ):
644
				continue
645
			out.append( node )
647
		return out
653
	addSets = addMember
654
	removeSets = removeMember
655
	replaceSets = replaceMember
656
	sets = getMembers
657
	members = getMembers
661
	def __len__(self):
662
		return len(self.st.minputs())
664
	def __iter__(self):
665
		for s in self.getMembers():
666
			yield s