3
Contains some basic classes that are required to run the nodes system
5
All classes defined here can replace classes in the node type hierarachy if the name
6
matches. This allows to create hand-implemented types.
8
__docformat__ = "restructuredtext"
10
from typ import nodeTypeToMfnClsMap, nodeTypeTree, MetaClassCreatorNodes, _addCustomType
11
from mrv.util import uncapitalize, capitalize, pythonIndex, Call
12
from mrv.interface import iDuplicatable, iDagItem
13
from mrv.maya.util import StandinClass
14
import maya.OpenMaya as api
15
import maya.cmds as cmds
16
import mrv.maya.ns as nsm
17
import mrv.maya.undo as undo
18
import mrv.maya.env as env
19
from new import instancemethod
20
from util import in_double3_out_vector, undoable_in_double3_as_vector
22
log = logging.getLogger("mrv.maya.nt.base")
24
# direct import to safe api. lookup
25
from maya.OpenMaya import MFnDagNode, MDagPath, MObject, MObjectHandle
27
from itertools import chain
30
_nodesdict = None # will be set during maya.nt initialization
33
__all__ = ("nodeTypeToNodeTypeCls", "isAbsolutePath", "toApiobj", "toApiobjOrDagPath",
34
"toSelectionList", "toComponentSelectionList", "toSelectionListFromNames",
35
"fromSelectionList", "toNodesFromNames", "findByName", "objExists",
36
"delete", "selection", "activeSelectionList", "iterSelection", "select",
37
"createNode", "SetFilter", "Node", "NodeFromObj", "NodeFromStr",
38
"DependNode", "Entity", "DagNode", "Attribute", "UnitAttribute", "TypedAttribute",
39
"NumericAttribute", "MessageAttribute", "MatrixAttribute", "LightDataAttribute",
40
"GenericAttribute", "EnumAttribute", "CompoundAttribute", "Data", "VectorArrayData",
41
"UInt64ArrayData", "StringData", "StringArrayData", "SphereData", "PointArrayData",
42
"PluginData", "NumericData", "NObjectData", "MatrixData", "IntArrayData",
43
"GeometryData", "SubdData", "NurbsSurfaceData", "NurbsCurveData", "MeshData",
44
"LatticeData", "DynSweptGeometryData", "DoubleArrayData", "ComponentListData",
45
"ArrayAttrsData", "Component", "SingleIndexedComponent", "DoubleIndexedComponent",
46
"TripleIndexedComponent", "MDagPathUtil", "Reference", "Transform", "Shape")
49
############################
51
##########################
52
# to prevent always creating instances of the same class per call
53
_nameToApiSelList = api.MSelectionList()
54
_mfndep = api.MFnDependencyNode()
55
_mfndag = api.MFnDagNode()
58
_mfndep_setobject = _mfndep.setObject
59
_mfndag_setObject = _mfndag.setObject
60
_mfndep_typename = _mfndep.typeName
61
_mfndag_typename = _mfndag.typeName
62
_mfndep_name = _mfndep.name
64
_api_mdagpath_node = MDagPath.node
65
_apitype_to_name = dict() # [int] - > type name string
67
_plugin_type_ids = ( api.MFn.kPluginDeformerNode,
68
api.MFn.kPluginDependNode,
69
api.MFn.kPluginEmitterNode,
70
api.MFn.kPluginFieldNode,
71
api.MFn.kPluginHwShaderNode,
72
api.MFn.kPluginIkSolver,
73
api.MFn.kPluginImagePlaneNode,
74
api.MFn.kPluginLocatorNode,
75
api.MFn.kPluginManipContainer,
76
api.MFn.kPluginObjectSet,
77
api.MFn.kPluginParticleAttributeMapperNode,
79
api.MFn.kPluginSpringNode,
80
api.MFn.kPluginTransformNode)
82
_plugin_type_ids_lut = set(_plugin_type_ids)
84
_plugin_type_to_node_type_name = dict(zip((_plugin_type_ids), ("UnknownPluginDeformerNode",
85
"UnknownPluginDependNode",
86
"UnknownPluginEmitterNode",
87
"UnknownPluginFieldNode",
88
"UnknownPluginHwShaderNode",
89
"UnknownPluginIkSolver",
90
"UnknownPluginImagePlaneNode",
91
"UnknownPluginLocatorNode",
92
"UnknownPluginManipContainer",
93
"UnknownPluginObjectSet",
94
"UnknownPluginParticleAttributeMapperNode",
96
"UnknownPluginSpringNode",
97
"UnknownPluginTransformNode")))
101
############################
103
##########################
107
def nodeTypeToNodeTypeCls(nodeTypeName, apiobj):
108
""" Convert the given node type (str) to the respective python node type class
110
:param nodeTypeName: the type name you which to have the actual class for
111
:param apiobj: source api object, its apiType is used as fallback in case we
112
don't know the node"""
114
nodeTypeCls = _nodesdict[capitalize(nodeTypeName)]
116
# assume its a plugin node - in that case the parent will be nicely defined
117
# and helps us to figure out that its a default dummy
118
parentclsname = _plugin_type_to_node_type_name.get(apiobj.apiType(), (isinstance(apiobj, MDagPath) and 'UnknownDag') or 'Unknown')
119
_addCustomType(_nodesdict, parentclsname, nodeTypeName)
120
nodeTypeCls = _nodesdict[capitalize(nodeTypeName)]
121
# END exception handling
123
if isinstance(nodeTypeCls, StandinClass):
124
nodeTypeCls = nodeTypeCls.createCls()
129
def _makeAbsolutePath(nodename):
130
# if dag paths are passed in, we do nothing as a dag object is obviously meant.
131
# Otherwise prepend a '|' to make it a dag object - the calling method will deal
132
# with it accordingly
133
if nodename.count('|') == 0:
134
return '|' + nodename
137
def isAbsolutePath(nodename):
138
return nodename.startswith('|')
140
def toDagPath(apiobj):
141
"""Find ONE valid dag path to the given dag api object"""
143
MFnDagNode(apiobj).getPath(dagpath)
146
def toApiobj(nodename):
147
""" Convert the given nodename to the respective MObject
149
:note: uses unique names only, and will fail if a non-unique path is given, which is
150
as selection lists do not work properly with partial names !
151
:note: even dag objects will end up as MObject
152
:note: code repeats partly in toApiobjOrDagPath as its supposed to be as fast
153
as possible - this method gets called quite a few times in benchmarks"""
154
_nameToApiSelList.clear()
156
nodename = _makeAbsolutePath(nodename)
158
objnamelist = [nodename]
159
if nodename.startswith("|") and nodename.count('|') == 1:
160
objnamelist.append(nodename[1:])
162
for name in objnamelist:
164
_nameToApiSelList.add(name)
169
_nameToApiSelList.getDependNode(0, obj)
171
# if we requested a dg node, but got a dag node, fail
172
if name.count('|') == 0 and obj.hasFn(api.MFn.kDagNode):
173
log.warn("Skipped %s as a dependency node was expected, but got a dag node" % name)
175
# END dag/dg inconsistency handling
178
# END if no exception on selectionList.add
179
# END for each test-object
182
def toApiobjOrDagPath(nodename):
183
"""Convert the given nodename to the respective MObject or MDagPath
185
:note: we treat "nodename" and "\|nodename" as the same objects as they occupy the
186
same namespace - one time a dep node is meant, the other time a dag node.
187
If querying a dag node, the dep node with the same name is not found, although it is in
188
the same freaking namespace ! IMHO this is a big bug !"""
189
_nameToApiSelList.clear()
191
nodename = _makeAbsolutePath(nodename)
193
objnamelist = [nodename]
194
if nodename.startswith("|") and nodename.count('|') == 1: # check dep node too ! ("|nodename", but "nodename" could exist too, occupying the "|nodename" name !
195
objnamelist.append(nodename[1:])
197
for name in objnamelist:
199
_nameToApiSelList.add(name)
205
_nameToApiSelList.getDagPath(0 , dag)
209
_nameToApiSelList.getDependNode(0, obj)
211
# if we requested a dg node, but got a dag node, fail
212
if name.count('|') == 0 and obj.hasFn(api.MFn.kDagNode):
213
log.warn("Skipped %s as a dependency node was expected, but got a dag node" % name)
215
# END dag/dg inconsistency handling
218
# END if no exception on selectionList.add
219
# END for each object name
222
def toSelectionList(nodeList, mergeWithExisting = False):
223
"""Convert an iterable filled with Nodes to a selection list
225
:param nodeList: iterable filled with dg and dag nodes as well as plugs, dagpaths or mobjects or strings
226
:param mergeWithExisting: if true, the selection list will not allow dupliacates , but adding objects
227
also takes (much) longer, depending on the size of the list
228
:return: selection list filled with objects from node list"""
229
if isinstance(nodeList, api.MSelectionList): # sanity check
232
sellist = api.MSelectionList()
233
for node in nodeList:
234
if isinstance(node, DagNode):
235
sellist.add(node.dagPath(), MObject(), mergeWithExisting)
236
elif isinstance(node, DependNode):
237
sellist.add(node.object(), mergeWithExisting)
238
else: # probably plug or something else like an mobject or dagpath
239
# cannot properly apply our flag here without intensive checking
240
# TODO: probably put in the instance checks !
242
# END for each item in input array
245
def toComponentSelectionList(nodeCompList, mergeWithExisting = False):
246
"""As above, but only works on DagNodes having components - the components
247
can be a nullObject though to add the whole object after all.
249
:param nodeCompList: list of tuple(DagNode, Component), Component can be
250
filled component or null MObject"""
251
if isinstance(nodeCompList, api.MSelectionList): # sanity check
254
sellist = api.MSelectionList()
255
for node, component in nodeCompList:
256
sellist.add(node.dagPath(), component, mergeWithExisting)
260
def toSelectionListFromNames(nodenames):
261
"""Convert the given iterable of nodenames to a selection list
263
:return: MSelectionList, use `iterSelectionList` to retrieve the objects"""
264
sellist = api.MSelectionList()
265
for name in nodenames:
270
def fromSelectionList(sellist, handlePlugs=1, **kwargs):
271
""":return: list of Nodes and MPlugs stored in the given selection list
272
:param kwargs: passed to selectionListIterator"""
274
kwargs['handlePlugs'] = handlePlugs
275
return list(sellist.mtoIter(**kwargs))
277
def toNodesFromNames(nodenames, **kwargs):
278
""":return: list of wrapped nodes from the given list of node names
279
:note: this function is supposed to be faster for multiple nodes compared to
280
just creating a Node directly as we optimize the process due to the intermediate
281
selection list getting the api objects for the given names
282
:param kwargs: passed to `fromSelectionList`"""
283
return fromSelectionList(toSelectionListFromNames(nodenames), **kwargs)
285
def findByName(name , **kwargs):
287
:return: list of node matching name, whereas simple regex using ``*`` can be used
288
to describe a pattern
289
:param name: string like pcube, or pcube*, or ``pcube*|*Shape``
290
:param kwargs: passed to `fromSelectionList`"""
291
sellist = api.MSelectionList()
292
api.MGlobal.getSelectionListByName(name, sellist)
294
return fromSelectionList(sellist, **kwargs)
302
def objExists(objectname):
303
""":return: True if given object exists, false otherwise
304
:param objectname: we always use absolute paths to have a unique name
305
:note: perfer this method over mel as the API is used directly as we have some special
306
handling to assure we get the right nodes"""
307
return toApiobj(objectname) is not None
311
def delete(*args, **kwargs):
312
"""Delete the given nodes
314
:param args: Node instances, MObjects, MDagPaths or strings to delete
317
if True, default False, will do alot of pre-work to actually
318
make the deletion work properly using the UI, thus we sort dag nodes
319
by dag path token length to delete top level ones first and individually,
320
to finally delete all dependency nodes in a bunch
322
Using this flag will be slower, but yields much better results if deleting complex
323
dag and dependency trees with locked attributes, conversion nodes, transforms and shapes
324
:note: in general , no matter which options have been chosen , api deletion does not work well
325
as the used algorithm is totally different and inferior to the mel implementaiton
326
:note: will not raise in case of an error, but print a notification message
327
:note: all deletions will be stored on one undo operation"""
328
presort = kwargs.get("presort", False)
330
# presort - this allows objects high up in the hierarchy to be deleted first
331
# Otherwise we might have trouble deleting the ones lower in the hierarchy
332
# We are basically reimplementing the MEL command 'delete' which does the
333
# same thing internally I assume
334
nodes = toSelectionList(args).mtoList()
339
if isinstance(node, DagNode):
340
dagnodes.append(node)
342
depnodes.append(node)
343
# END for each node in nodes for categorizing
346
dagnodes.sort(key = lambda n: len(str(n).split('|')), reverse = True)
348
# use all of these in order
349
nodes = chain(dagnodes, depnodes)
352
# NOTE: objects really want to be deleted individually - otherwise
353
# maya might just crash for some reason !!
355
if not node.isValid():
361
log.error("Deletion of %s failed" % node)
362
# END exception handling
363
# END for each node to delete
365
def selection(filterType=api.MFn.kInvalid, **kwargs):
366
""":return: list of Nodes from the current selection
367
:param filterType: The type of nodes to return exclusively. Defaults to
369
:param kwargs: passed to `fromSelectionList`"""
370
kwargs['filterType'] = filterType
371
return fromSelectionList(activeSelectionList(), **kwargs)
373
def activeSelectionList():
374
""":return: MSelectionList of the current selection list"""
375
sellist = api.MSelectionList()
376
api.MGlobal.getActiveSelectionList(sellist)
379
def iterSelection(filterType=api.MFn.kInvalid, **kwargs):
380
""":return: iterator over current scene selection
381
:param filterType: MFn type specifying the node type to iterate upon. Defaults
383
:param kwargs: passed to `it.iterSelectionList`
384
:note: This iterator will always return Nodes"""
385
kwargs['asNode'] = 1 # remove our overridden warg
386
kwargs['filterType'] = filterType
387
return activeSelectionList().mtoIter(**kwargs)
389
def select(*nodesOrSelectionList , **kwargs):
390
"""Select the given list of wrapped nodes or selection list in maya
392
:param nodesOrSelectionList: single selection list or multiple wrapped nodes
395
* listAdjustment: default api.MGlobal.kReplaceList
396
:note: as this is a convenience function that is not required by the api itself,
397
but for interactive sessions, it will be undoable
398
:note: Components are only supported if a selection list is given
399
:note: This method is implicitly undoable"""
403
for item in nodesOrSelectionList:
404
if isinstance(item, basestring):
405
nodenames.append(item)
408
# END handel item type
411
if len(other) == 1 and isinstance(other[0], api.MSelectionList):
414
sellist = toSelectionList(other)
417
sellistnames = toSelectionListFromNames(nodenames)
418
sellist.merge(sellistnames)
420
adjustment = kwargs.get("listAdjustment", api.MGlobal.kReplaceList)
421
api.MGlobal.selectCommand(sellist , adjustment)
425
def createNode(nodename, nodetype, autocreateNamespace=True, renameOnClash = True,
426
forceNewLeaf=True , maxShapesPerTransform = 0):
427
"""Create a new node of nodetype with given nodename
429
:param nodename: like ``mynode``or ``namespace:mynode`` or ``|parent|mynode`` or
430
``|ns1:parent|ns1:ns2:parent|ns3:mynode``. The name may contain any amount of parents
432
:note: For reasons of safety, dag nodes must use absolute paths like ``|parent|child`` -
433
otherwise names might be ambiguous ! This method will assume absolute paths !
434
:param nodetype: a nodetype known to maya to be created accordingly
435
:param autocreateNamespace: if True, namespaces given in the nodename will be created
437
:param renameOnClash: if True, nameclashes will automatcially be resolved by creating a unique
438
name - this only happens if a dependency node has the same name as a dag node
439
:param forceNewLeaf: if True, nodes will be created anyway if a node with the same name
440
already exists - this will recreate the leaf portion of the given paths. Implies renameOnClash
441
If False, you will receive an already existing node if the name and type matches.
442
:param maxShapesPerTransform: only used when renameOnClash is True, defining the number of
443
shapes you may have below a transform. If the number would be exeeded by the creation of
444
a shape below a given transform, a new auto-renamed transform will be created automatically.
445
This transform is garantueed to be new and will be used as new parent for the shape.
446
:raise RuntimeError: If nodename contains namespaces or parents that may not be created
447
:raise NameError: If name of desired node clashes as existing node has different type
448
:note: As this method is checking a lot and tries to be smart, its relatively slow (creates ~1200 nodes / s)
449
:return: the newly create Node"""
450
if nodename in ('|', ''):
451
raise RuntimeError("Cannot create '|' or ''")
453
subpaths = nodename.split('|')
457
lenSubpaths = len(subpaths)
460
# SANITY CHECK ! Must use absolute dag paths
461
if nodename[0] != '|':
462
nodename = "|" + nodename # update with pipe
463
subpaths.insert(0, '')
465
# END special handling
467
added_operation = False
468
is_transform_type = nodetype == 'transform'
470
if not is_transform_type and nodeTypeTree.has_node(nodetype):
471
parents = list(nodeTypeTree.parent_iter(nodetype))
472
is_transform_type = 'transform' in parents
473
is_shape = 'shape' in parents
474
# END do more intense inheritance query
476
do_existence_checks = True
479
for i in xrange(start_index, lenSubpaths): # first token always pipe, need absolute paths
480
nodepartialname = '|'.join(subpaths[0 : i+1]) # full path to the node so far
481
is_last_iteration = i == lenSubpaths - 1
484
######################
485
# if it doesn't exist the first time, we can save all other checks as we
486
# will start creating it from now on
487
if do_existence_checks:
488
nodeapiobj = toApiobj(nodepartialname)
489
if nodeapiobj is not None:
490
# could be that the node already existed, but with an incorrect type
491
if is_last_iteration: # in the last iteration
493
parentnode = createdNode = nodeapiobj
494
_mfndep_setobject(createdNode)
495
existing_node_type = uncapitalize(_mfndep_typename())
496
nodetypecmp = uncapitalize(nodetype)
497
if nodetypecmp != existing_node_type:
498
# allow more specialized types, but not less specialized ones
499
if nodetypecmp not in nodeTypeTree.parent_iter(existing_node_type):
500
msg = "node %s did already exist, its type %s is incompatible with the requested type %s" % (nodepartialname, existing_node_type, nodetype)
502
# END nodetypes different
505
# END force new leaf handling
507
# just go ahead, but create a new node
508
renameOnClash = True # allow clashes and rename
509
# END leaf path handling
511
# remember what we have done so far and continue
512
parentnode = createdNode = nodeapiobj
515
do_existence_checks = False
516
# END node item exists handling
517
# END do existence checks
521
# it does not exist, check the namespace
522
dagtoken = '|'.join(subpaths[i : i+1])
524
if autocreateNamespace:
525
nsm.createNamespace(":".join(dagtoken.split(":")[0:-1])) # will resolve to root namespace at least
527
# see whether we have to create a transform or the actual nodetype
528
actualtype = "transform"
529
if is_last_iteration:
530
actualtype = nodetype
532
# create the node - either with or without parent
533
# The actual node needs to be created with a matching modifier, dag nodes
534
# with the DagMofier, dg nodes with the dg modifier
535
# The user currently has to specify a proper path.
536
if parentnode or actualtype == "transform" or (is_last_iteration and is_transform_type):
538
dagmod = api.MDagModifier()
542
if parentnode: # use parent
543
newapiobj = dagmod.createNode(actualtype, parentnode) # create with parent
545
newapiobj = dagmod.createNode(actualtype) # create
547
dagmod.renameNode(newapiobj, dagtoken) # rename
549
parentnode = createdNode = newapiobj # update parent
552
dgmod = api.MDGModifier()
554
# create dg node - really have to check for clashes afterwards
555
# It may also be that the user passed in a name which didn't
556
# show that we want a dag node - hence we have to check for failure
560
newapiobj = dgmod.createNode(actualtype) # create
563
dagmod = api.MDagModifier()
565
# even though it could be a transform derived type which can be
566
# created right away without an explicit parent, we don't know that,
567
# if we would know, we wouldn't be here.
568
# In case its not transform derived, a parent node would be created
569
# automatically. Problem is that this node is returned instead of
570
# the node we requested (logic bug if you ask me !), and we don't
571
# want to return a node type the caller didn't order.
572
# This is why we explicitly create a transform, to have the parent
573
# under our control. This will put even a transform derived type
574
# under an extra transform, which in that case would not be required.
575
# But here we are, and it cannot be helped.
577
trans = dagmod.createNode("transform")
578
newapiobj = dagmod.createNode(actualtype, trans)
580
newapiobj = dagmod.createNode(actualtype)
582
# END handle dag node
583
mod.renameNode(newapiobj, dagtoken) # rename
584
createdNode = newapiobj
585
# END (partial) node creation
587
# CLASHING CHECK (and name update) !
588
# PROBLEM: if a dep node with name of dagtoken already exists, it will
589
# rename the newly created (sub) node although it is not the same !
590
_mfndep_setobject(newapiobj)
591
actualname = _mfndep_name()
592
if actualname != dagtoken:
593
# Is it a renamed node because because a dep node of the same name existed ?
594
# Could be that a child of the same name existed too
595
if not renameOnClash:
596
msg = "named %s did already exist - cannot create a dag node with same name due to maya limitation" % nodepartialname
599
# update the tokens and use the new path
600
subpaths[i] = actualname
601
nodepartialname = '|'.join(subpaths[0 : i+1])
602
# END dag token renamed
603
# END for each partial path
605
# add the modifiers to the undo stack
606
op = undo.GenericOperationStack()
607
if dgmod is not None:
608
op.addCmd(dgmod.doIt, dgmod.undoIt)
609
if dagmod is not None:
610
op.addCmd(dagmod.doIt, dagmod.undoIt)
613
if createdNode is None:
614
raise RuntimeError("Failed to create %s (%s)" % (nodename, nodetype))
616
return NodeFromObj(createdNode)
621
def _checkedInstanceCreationDagPathSupport(mobject_or_mdagpath, clsToBeCreated, basecls):
622
"""Same purpose and attribtues as `_checkedInstanceCreation`, but supports
623
dagPaths as input as well"""
624
apiobj = mobject_or_mdagpath
626
if isinstance(mobject_or_mdagpath, MDagPath):
627
dagpath = mobject_or_mdagpath
628
# END if we have a dag path
630
clsinstance = _checkedInstanceCreation(mobject_or_mdagpath, _lookup_type(mobject_or_mdagpath), clsToBeCreated, basecls)
631
if isinstance(clsinstance, DagNode):
632
_setupDagNodeDelayedMethods(clsinstance, apiobj, dagpath)
636
def _checkedInstanceCreation(apiobj, typeName, clsToBeCreated, basecls):
637
"""Utiliy method creating a new class instance according to additional type information
638
Its used by __new__ constructors to finalize class creation
640
:param apiobj: the MObject or MDagPath of object to wrap
641
:param typeName: the name of the node type to be created
642
:param clsToBeCreated: the cls object as passed in to __new__
643
:param basecls: the class of the caller containing the __new__ method
644
:return: create clsinstance if the proper type (according to nodeTypeTree)"""
645
# get the node type class for the api type object
647
nodeTypeCls = nodeTypeToNodeTypeCls(typeName, apiobj)
650
# if an explicit type was requested, assure we are at least compatible with
651
# the given cls type - our node type is supposed to be the most specialized one
652
# cls is either of the same type as ours, or is a superclass.
653
# It is also okay if the user provided a class which is a subclass of the most
654
# suitable class we know, which acts like a virtal specialization
655
if clsToBeCreated is not basecls and clsToBeCreated is not nodeTypeCls:
656
vclass_attr = '__mrv_virtual_subtype__'
657
# If the class is a virtual subtype and indeed a subclass of our best known type,
659
if not issubclass(nodeTypeCls, clsToBeCreated) and \
660
not (hasattr(clsToBeCreated, vclass_attr) and issubclass(clsToBeCreated, nodeTypeCls)):
661
raise TypeError("Explicit class %r must be %r or a superclass of it. Consider setting the %s attribute to indicate you are a virtual subtype." % (clsToBeCreated, nodeTypeCls, vclass_attr))
663
nodeTypeCls = clsToBeCreated # respect the wish of the client
664
# END if explicit class given
667
# At this point, we only support type as we expect ourselves to be lowlevel
668
clsinstance = object.__new__(nodeTypeCls)
670
object.__setattr__(clsinstance, '_apiobj', apiobj)
673
def _setupDagNodeDelayedMethods(dagnode, mobject, mdagpath):
674
"""Setup the given dagnode with the instance methods it needs to handle the gven
675
mobject OR mdagpath accordingly, one of them may be None"""
676
instcls = type(dagnode)
678
# next time the MDagPath is accessed, we retrieve it from the MObject
679
object.__setattr__(dagnode, 'dagPath', instancemethod(instcls._dagPath_delayed, dagnode, instcls))
680
object.__setattr__(dagnode, 'object', instancemethod(instcls._object_cached, dagnode, instcls))
682
# MObject has to be retrieved on demand
683
# this is the default
684
object.__setattr__(dagnode, '_apidagpath', mdagpath)
685
# END handle missing MDagPath or MObject
688
def _createInstByPredicate(apiobj, cls, basecls, predicate):
689
"""Allows to wrap objects around MObjects where the actual compatabilty
690
cannot be determined by some nodetypename, but by the function set itself.
691
Thus it uses the nodeTypeToMfnClsMap to get mfn cls for testing
693
:param cls: the class to be created
694
:param basecls: the class where __new__ has actually been called
695
:param predicate: returns true if the given nodetypename is valid, and its mfn
696
should be taken for tests
697
:return: new class instance, or None if no mfn matched the apiobject"""
698
# try which node type fits
699
# All attribute instances end with attribute
700
# NOTE: the capital case 'A' assure we do not get this base class as option - this would
701
# be bad as it is compatible with all classes
702
attrtypekeys = [a for a in nodeTypeToMfnClsMap.keys() if predicate(a)]
704
for attrtype in attrtypekeys:
705
attrmfncls = nodeTypeToMfnClsMap[attrtype]
707
mfn = attrmfncls(apiobj)
711
newinst = _checkedInstanceCreation(apiobj, attrtype, cls, basecls) # lookup in node tree
713
# END for each known attr type
717
def _getUniqueName(dagpath):
718
"""Create a unique name based on the given dagpath by appending numbers"""
720
newpath = str(dagpath)
721
while cmds.objExists(newpath):
722
newpath = "%s%i" % (dagpath, copynumber)
724
# END while dagpath does exist
727
############################
729
##########################
733
class SetFilter(tuple):
734
"""Utility Class returning True or False on call, latter one if
735
the passed object does not match the filter"""
736
def __new__(cls, apitype, exactTypeFlag, deformerSet):
737
return tuple.__new__(cls, (apitype, exactTypeFlag, deformerSet))
739
def __call__(self, apiobj):
740
""":return: True if given api object matches our specifications """
741
if self[2]: # deformer sets
742
setnode = NodeFromObj(apiobj)
743
for elmplug in setnode.usedBy: # find connected deformer
744
iplug = elmplug.minput()
748
if iplug.node().hasFn(api.MFn.kGeometryFilt):
750
# END for each connected plug in usedBy array
752
return False # no deformer found
753
# deformer set handling
755
if self[1]: # exact type
756
return apiobj.apiType() == self[0]
759
return apiobj.hasFn(self[0])
767
_api_type_tuple = (MObject, MDagPath)
770
"""Common base for all maya nodes, providing access to the maya internal object
772
Use this class to directly create a maya node of the required type"""
773
__metaclass__ = MetaClassCreatorNodes
775
def __new__ (cls, *args, **kwargs):
776
"""return the proper class for the given object
778
:param args: arg[0] is the node to be wrapped
780
* string: wrap the API object with the respective name
785
If args is empty, a new node of the given type will be created within
786
maya. Shapes will automatically receive a parent transform.
787
kwargs will be passed to `createNode` in that case.
788
:note: This multi-purpose constructor is not perfectly optimized for speed,
789
consider using `NodeFromObj` instead"""
792
if not issubclass(cls, DependNode): # cls can be DependNode as well
793
raise TypeError("Can only create types being subclasses of Node, not %r" % cls)
794
# END handle invalid class
796
typename = uncapitalize(cls.__name__)
798
if issubclass(cls, Shape): # cls can be DagNode as well
799
instname = "%s|%sShape" % (instname, instname)
800
# END handle dag objects
802
return createNode(instname, typename, **kwargs)
803
# END handle creation mode
806
mobject_or_mdagpath = None
809
if isinstance(objorname, _api_type_tuple):
810
mobject_or_mdagpath = objorname
811
elif isinstance(objorname, basestring):
812
if objorname.find('.') != -1:
813
raise ValueError("%s cannot be handled - create a node, then access its attribute like Node('name').attr" % objorname)
814
mobject_or_mdagpath = toApiobjOrDagPath(objorname)
815
elif isinstance(objorname, MObjectHandle):
816
mobject_or_mdagpath = objorname.object()
818
raise TypeError("Objects of type %s cannot be handled" % type(objorname))
821
skip_checks = (len(args) > 1 and args[1]) or False
822
if (not skip_checks and (mobject_or_mdagpath is None
823
or (isinstance(mobject_or_mdagpath, MDagPath) and not mobject_or_mdagpath.isValid())
824
or (isinstance(mobject_or_mdagpath, MObject) and mobject_or_mdagpath.isNull()))):
825
raise ValueError("object does not exist: %s" % objorname)
826
# END evil validity checking
829
return _checkedInstanceCreationDagPathSupport(mobject_or_mdagpath, cls, Node)
832
#{ Overridden Methods
833
def __eq__(self, other):
834
"""compare the nodes according to their api object.
835
Valid inputs are other Node, MObject or MDagPath instances"""
837
if not isinstance(other, Node):
838
otherapiobj = NodeFromObj(other).object()
840
otherapiobj = other.object()
843
return self.object() == otherapiobj # does not appear to work as expected ...
845
def __ne__(self, other):
846
return not Node.__eq__(self, other)
850
:return: our name as hash - as python keeps a pool, each name will
851
correspond to the exact object.
852
:note: using asHashable of openMayaMPx did not work as it returns addresses
853
to instances - this does not work for MObjects though
854
:note: in maya2009 and newer, MObjectHandle.hashCode provides the information
856
return hash(str(self))
858
if hasattr(api.MObjectHandle, 'hashCode'):
859
def __hash_2009__(self):
860
""":return: hash of our object using MObjectHandle functionlity"""
861
return MObjectHandle(self.object()).hashCode()
863
__hash__ = __hash_2009__
864
__hash__.__name__ = '__hash__'
865
# END overwrite previous hash with faster version
867
#} END overridden methods
872
:return: the highest qualified api object of the actual superclass,
873
usually either MObject or MDagPath"""
874
raise NotImplementedError("To be implemented in subclass")
877
def getMFnClasses(cls):
879
:return: list of all function set classes this node supports, most derived
880
function set comes first"""
881
return [mrocls._mfncls for mrocls in cls.mro() if '_mfncls' in mrocls.__dict__]
884
""":return: the MFn Type id of the wrapped object"""
885
return self.apiObject().apiType()
887
def hasFn(self, mfntype):
888
""":return: True if our object supports the given function set type"""
889
return self.apiObject().hasFn(mfntype)
893
def _lookup_type(mobject_or_mdagpath):
894
""":return: node type name of the given MObject or MDagPath
895
:note: if we have a plugin type, we must use the 'slow' way
896
as the type is the same for all plugin nodes"""
897
apitype = mobject_or_mdagpath.apiType()
899
if apitype in _plugin_type_ids_lut:
901
# END force byName type check for plugin types
902
return _apitype_to_name[apitype]
904
# cache miss - fill in the type
905
if isinstance(mobject_or_mdagpath, MDagPath):
906
_mfndag_setObject(mobject_or_mdagpath)
907
typename =_mfndag_typename()
909
_mfndep_setobject(mobject_or_mdagpath)
910
typename = _mfndep_typename()
911
# END handle input type
912
_apitype_to_name[mobject_or_mdagpath.apiType()] = typename
915
# END handle cache miss
918
class NodeFromObj(object):
919
"""Virtual Constructor, producing nodes as the `Node` does, but it will only
920
accept MObjects or dagpaths which are expected to be valid.
921
As no additional checking is performed, it might be more unsafe to use, but
922
will be faster as it does not perform any runtime checks
924
It duplicates code from `_checkedInstanceCreation` and `_checkedInstanceCreationDagPathSupport`
925
to squeeze out the last tiny bit of performance as it can make quite a few more
926
assumptions and reduces method calls.
928
:note: Do not derive from this class, derive from `Node` instead
929
:note: We will always create the node type as determined by the type hierarchy"""
930
def __new__ (cls, mobject_or_mdagpath):
931
apiobj = mobject_or_mdagpath
933
if isinstance(mobject_or_mdagpath, MDagPath):
934
dagpath = mobject_or_mdagpath
935
# END if we have a dag path
937
clsinstance = object.__new__(nodeTypeToNodeTypeCls(_lookup_type(mobject_or_mdagpath), apiobj))
939
# apiobj is None, or MObject, or MDagPath, but will be set to the proper type
941
object.__setattr__(clsinstance, '_apiobj', apiobj)
943
# DagNode created from a MObject ?
944
if isinstance(clsinstance, DagNode):
945
_setupDagNodeDelayedMethods(clsinstance, apiobj, dagpath)
946
# END handel DagObjects
948
# for some reason, we have to call init ourselves in that case, probably
949
# since we are not afficliated with the actual instance we returned which
950
# makes a little bit of sense.
951
clsinstance.__init__(mobject_or_mdagpath)
955
class NodeFromStr(object):
956
"""Virtual constructor similar to `NodeFromObj`, but it will only accept strings
957
to produce a wrapped node as fast as possible. Therefore, the error checking is
959
def __new__ (cls, node_string):
960
return NodeFromObj(toApiobjOrDagPath(node_string))
963
class DependNode(Node, iDuplicatable): # parent just for epydoc -
964
""" Implements access to dependency nodes"""
966
#{ Overridden Methods
967
def __getattr__(self, attr):
968
"""Interpret attributes not in our dict as attributes on the wrapped node,
969
create a plug for it and add it to our class dict, effectively caching the attribute"""
970
base = super(DependNode, self)
972
plug = self.findPlug(attr)
973
except RuntimeError: # perhaps a base class can handle it
975
return base.__getattribute__(attr)
976
except AttributeError:
977
raise AttributeError("Attribute '%s' does not exist on '%s', neither as function not as attribute" % (attr, self.name()))
978
# END try to get attribute by base class
979
# END find plug exception handling
981
# NOTE: Don't cache the plug on the instance, it might be too dangerous
982
# in conjunction with changes to the DAG
984
# and assure our class knows about it so in future the plug will be retrieved
985
# right away, before having a function lookup miss
987
setattr(type(self), attr, property(lambda self: self.findPlug(attr)))
992
""":return: name of this object"""
996
""":return: class call syntax"""
998
return '%s("%s")' % (self.__class__.__name__, DependNode.__str__(self))
999
#} END overridden methods
1005
def duplicate(self, name = None, *args, **kwargs):
1006
"""Duplicate our node and return a wrapped version to it
1008
:param name: if given, the newly created node will use the given name
1010
* renameOnClash: if Trrue, default True, clashes are prevented by renaming the new node
1011
* autocreateNamespace: if True, default True, namespaces will be created if mentioned in the name
1012
:note: the copyTo method may not have not-undoable side-effects to be a proper
1014
:note: undo could be implemented for dg nodes - but for reasons of consistency, its disabled here -
1015
who knows how much it will crap out after a while as duplicate is not undoable (mel command) -
1016
it never really worked to undo a mel command from within python, executed using a dgmodifier - unfortunately
1017
it does not return any result making it hard to find the newly duplicated object !"""
1018
# returns name of duplicated node
1019
duplnode = NodeFromStr(cmds.duplicate(str(self))[0])
1023
# find a good name based on our own one - the default name is just not nice
1025
name = _getUniqueName(self)
1028
raise ValueError("Names for dependency nodes my not contain pipes: %s" % name)
1032
rkwargs['renameOnClash'] = kwargs.pop('renameOnClash', True)
1033
rkwargs['autocreateNamespace'] = kwargs.pop('autocreateNamespace', True)
1034
duplnode = duplnode.rename(name, **rkwargs)
1036
# call our base class to copy additional information
1037
self.copyTo(duplnode, *args, **kwargs)
1040
#) END iDuplicatable
1042
#{ preset type filters
1043
fSetsObject = SetFilter(api.MFn.kSet, True, 0) # object fSets only
1044
fSets = SetFilter(api.MFn.kSet, False, 0) # all set types
1049
def _getSetPlug(self):
1050
""":return: message plug - for non dag nodes, this will be connected """
1053
def connectedSets(self, setFilter = fSets):
1054
""":return: list of object set compatible Nodes having self as member
1055
:param setFilter: tuple(apiType, use_exact_type) - the combination of the
1056
desired api type and the exact type flag allow precise control whether you which
1057
to get only renderable shading engines, only objectfSets (tuple[1] = True),
1058
or all objects supporting the given object type.
1059
Its preset to only return shading engines
1060
:note: the returned sets order is defined by the order connections to instObjGroups
1061
:note: only sets will be returned that have the whole object as member, thus you will not
1062
see sets having component assignments like per-compoent shader assignments or deformer sets """
1064
# have to parse the connections to fSets manually, finding fSets matching the required
1065
# type and returning them
1067
iogplug = self._getSetPlug()
1069
for dplug in iogplug.moutputs():
1070
setapiobj = dplug.node()
1072
if not setFilter(setapiobj):
1074
outlist.append(NodeFromObj(MObject(setapiobj)))
1075
# END for each connected set
1079
# alias - connectedSets derives from the MayaAPI, but could be shorter
1080
sets = connectedSets
1082
def isMemberOf(self, setnode, component = MObject()):
1083
""":return: True if self is part of setnode
1084
:note: method is undoable
1085
:see: `sets.ObjectSet`"""
1086
return setnode.isMember(self, component = component)
1088
def addTo(self, setnode, component = MObject(), **kwargs):
1089
"""Add ourselves to the given set
1091
:note: method is undoable
1092
:see: `sets.ObjectSet`"""
1093
return setnode.addMember(self, component = component, **kwargs)
1095
def removeFrom(self, setnode, component = MObject()):
1096
"""remove ourselves to the given set
1098
:note: method is undoable
1099
:see: `sets.ObjectSet`"""
1100
return setnode.removeMember(self, component = component)
1102
#} END sets handling
1107
def rename(self, newname, autocreateNamespace=True, renameOnClash = True):
1108
"""Rename this node to newname
1110
:param newname: new name of the node
1111
:param autocreateNamespace: if true, namespaces given in newpath will be created automatically, otherwise
1112
a RuntimeException will be thrown if a required namespace does not exist
1113
:param renameOnClash: if true, clashing names will automatically be resolved by adjusting the name
1114
:return: renamed node which is the node itself
1115
:note: for safety reasons, this node is dagnode aware and uses a dag modifier for them !"""
1117
raise NameError("new node names may not contain '|' as in %s" % newname)
1119
# is it the same name ?
1120
if newname == api.MFnDependencyNode(self.object()).name():
1124
if not renameOnClash:
1127
if isinstance(self, DagNode): # dagnode: check existing children under parent
1128
parent = self.parent()
1130
testforobject = parent.fullChildName(newname) # append our name to the path
1131
if objExists(testforobject):
1132
raise RuntimeError("Object %s did already exist - renameOnClash could have resolved this issue" % testforobject)
1133
# END if we have a parent
1135
exists = objExists(newname) # depnode: check if object exists
1138
raise RuntimeError("Node named %s did already exist, failed to rename %s" % (newname, self))
1139
# END not renameOnClash handling
1142
ns = ":".join(newname.split(":")[:-1])
1143
if not nsm.existsNamespace(ns) and not autocreateNamespace:
1144
raise RuntimeError("Cannot rename %s to %s as namespace %s does not exist" % (self, newname, ns))
1145
ns = nsm.createNamespace(ns) # assure its there
1148
# NOTE: this stupid method will also rename shapes !!!
1149
# you cannot prevent it, so we have to store the names and rename it lateron !!
1150
shapenames = shapes = None # HACK: this is dagnodes only (only put here for convenience, should be in DagNode)
1154
if isinstance(self, DagNode):
1155
mod = undo.DagModifier()
1156
shapes = self.shapes()
1157
shapenames = [s.basename() for s in shapes ]
1159
mod = undo.DGModifier()
1160
mod.renameNode(self.object(), newname)
1163
# RENAME SHAPES BACK !
1164
#######################
1165
# Yeah, of course the rename method renames shapes although this has never been
1166
# requested ... its so stupid ...
1168
for shape,shapeorigname in zip(shapes, shapenames): # could use izip, but this is not about memory here
1169
mod.renameNode(shape.object(), shapeorigname)
1170
# END for each shape to rename
1171
# END handle renamed shapes
1181
:note: if the undo queue is enabled, the object becomes invalid, but stays alive until it
1183
:note: if you want to delete many nodes, its more efficient to delete them
1184
using the global `delete` method"""
1185
mod = undo.DGModifier()
1186
mod.deleteNode(self.object())
1189
def _addRemoveAttr(self, attr, add):
1190
"""DoIt function adding or removing attributes with undo"""
1191
mfninst = self._mfncls(self.object())
1192
doitfunc = mfninst.addAttribute
1195
doitfunc = mfninst.removeAttribute
1199
def addAttribute(self, attr):
1200
"""Add the given attribute to the node as local dynamic attribute
1202
:param attr: MObject of attribute or Attribute instance as retrieved from
1204
:return: plug to the newly added attribute
1205
:note: This method is explicitly not undoable as attributes are being deleted
1206
in memory right in the moment they are being removed, thus they cannot
1207
reside on the undo queue"""
1208
# return it if it already exists
1209
attrname = api.MFnAttribute(attr).name()
1211
return self.findPlug(attrname, False)
1212
except RuntimeError:
1215
self._addRemoveAttr(attr, True)
1216
return self.findPlug(api.MFnAttribute(attr).name())
1218
def removeAttribute(self, attr):
1219
"""Remove the given attribute from the node
1221
:param attr: see `addAttribute`"""
1222
# don't do anyting if it does not exist
1223
attrname = api.MFnAttribute(attr).name()
1225
self.findPlug(attrname, False)
1226
except RuntimeError:
1227
# it does not exist, that's what was requested
1230
self._addRemoveAttr(attr, False)
1233
def setNamespace(self, newns, **kwargs):
1235
:return: self after being moved to the given namespace. This will effectively
1237
:param newns: Namespace instance to put this Node into
1238
:param kwargs: to be passed to `rename`"""
1239
namespace, objname = nsm.Namespace.splitNamespace(self.basename())
1240
return self.rename(newns + objname, **kwargs)
1245
def setLocked(self, state):
1246
"""Lock or unloack this node
1248
:param state: if True, the node is locked. Locked nodes cannot be deleted,
1249
renamed or reparented
1250
:note: you can query the lock state with `isLocked`"""
1251
curstate = self.isLocked()
1252
# also works for dag nodes !
1253
depfn = api.MFnDependencyNode(self.object())
1255
op = undo.GenericOperation()
1256
op.setDoitCmd(depfn.setLocked, state)
1257
op.setUndoitCmd(depfn.setLocked, curstate)
1261
#{ Connections and Attributes
1263
def connections(self):
1264
""":return: MPlugArray of connected plugs"""
1265
cons = api.MPlugArray()
1266
mfn = DependNode._mfncls(self.object()).getConnections(cons)
1269
def dependencyInfo(self, attribute, by=True):
1271
:return: list of attributes that given attribute affects or that the given attribute
1273
if the attribute turns dirty.
1274
:param attribute: attribute instance or attribute name
1275
:param by: if false, affected attributes will be returned, otherwise the attributes affecting this one
1276
:note: see also `MPlug.affectedByPlugs`
1277
:note: USING MEL: as api command and mObject array always crashed on me ... don't know :("""
1278
if not isinstance(attribute, basestring):
1279
attribute = attribute.name()
1281
attrs = cmds.affects(attribute , str(self), by=by)
1282
return [self.attribute(an) for an in attrs]
1284
#} END connections and attribtues
1288
""":return: True if the object exists in the scene
1289
:note: objects on the undo queue are NOT valid, but alive"""
1290
return MObjectHandle(self.object()).isValid()
1293
""":return: True if the object exists in memory
1294
:note: objects on the undo queue are alive, but NOT valid"""
1295
return MObjectHandle(self.object()).isAlive()
1301
""":return: the MObject attached to this Node"""
1304
apiObject = object # overridden from Node
1306
def referenceFile(self):
1308
:return: name (str) of file this node is coming from - it could contain
1309
a copy number as {x}
1310
:note: will raise if the node is not referenced, use isReferenced to figure
1312
# apparently, we have to use MEL here :(
1313
return cmds.referenceQuery(str(self) , f=1)
1316
""":return: name of this instance
1317
:note: it is mainly for compatability with dagNodes which need this method
1318
in order to return the name of their leaf node"""
1324
if env.appVersion() < 2012:
1325
class Entity(DependNode):
1326
"""Common base for dagnodes and paritions
1328
:note: parent is set by metacls. Parent differs between maya2011 and newer versions,
1329
hence we cannot provide it here for use by the documnetation system"""
1331
class ContainerBase(DependNode):
1334
class Entity(ContainerBase):
1336
#END handle maya version differences
1339
class DagNode(Entity, iDagItem): # parent just for epydoc
1340
""" Implements access to DAG nodes"""
1343
kNextPos = MFnDagNode.kNextPos
1345
def __eq__(self, other):
1346
"""Compare MDagPaths directly
1347
Valid inputs are Node, DagNode, MObject and MDagPath instances."""
1348
if not isinstance(other, Node):
1349
other = NodeFromObj(other)
1350
if isinstance(other, DagNode):
1351
return self.dagPath() == other.dagPath()
1352
return self.object() == other.object()
1354
def __ne__(self, other):
1355
return not DagNode.__eq__(self, other)
1357
def __getitem__(self, index):
1359
:return: if index >= 0: Node(child) at index
1361
* if index < 0: Node parent at -(index+1)(if walking up the hierarchy)
1362
* If index is string, use DependNodes implementation
1364
:note: returned child can be transform or shape, use `shapes` or
1365
`childTransforms` if you need a quickfilter """
1367
return self.child(index)
1369
for i,parent in enumerate(self.iterParents()):
1372
# END for each parent
1373
raise IndexError("Parent with index %i did not exist for %r" % (index, self))
1376
def _getSetPlug(self):
1378
:return: the iogplug properly initialized for self
1379
Dag Nodes have the iog plug as they support instancing """
1380
return self.iog.elementByLogicalIndex(self.instanceNumber())
1384
def _setWorldspaceTransform(self, parentnode):
1385
"""Set ourselve's transformation matrix to our absolute worldspace transformation,
1386
possibly relative to the optional parentnode
1388
:param parentnode: if not None, it is assumed to be the future parent of the node,
1389
our transformation will be set such that we retain our worldspace position if parented below
1391
if not isinstance(self, Transform):
1394
nwm = self.wm.elementByLogicalIndex(self.instanceNumber()).masData().transformation().asMatrix()
1396
# compenstate for new parents transformation ?
1397
if parentnode is not None:
1398
# use world - inverse matrix
1399
parentInverseMatrix = parentnode.wim.elementByLogicalIndex(parentnode.instanceNumber()).masData().transformation().asMatrix()
1400
nwm = nwm * parentInverseMatrix
1401
# END if there is a new parent
1403
self.set(api.MTransformationMatrix(nwm))
1408
def reparent(self, parentnode, renameOnClash=True, raiseOnInstance=True, keepWorldSpace = False):
1409
""" Change the parent of all nodes (also instances) to be located below parentnode
1411
:param parentnode: Node instance of transform under which this node should be parented to
1412
if None, node will be reparented under the root (which only works for transforms)
1413
:param renameOnClash: resolve nameclashes by automatically renaming the node to make it unique
1414
:param raiseOnInstance: if True, this method will raise if you try to reparent an instanced object.
1415
If false, instanced objects will be merged into the newly created path under parentnode, effectively
1416
eliminating all other paths , keeping the newly created one
1417
:param keepWorldSpace: if True and node to be reparented is a transform, the world space position
1418
will be kept by adjusting the transformation accordingly.
1419
**WARNNG**: Currently we reset pivots when doing so
1421
:return : copy of self pointing to the new dag path self
1423
:note: will remove all instance of this object and leave this object at only one path -
1424
if this is not what you want, use the addChild method instead as it can properly handle this case
1426
:note: this method handles namespaces properly """
1427
if raiseOnInstance and self.instanceCount(False) > 1:
1428
raise RuntimeError("%r is instanced - reparent operation would destroy direct instances" % self)
1430
if not renameOnClash and parentnode and self != parentnode:
1431
# check existing children of parent and raise if same name exists
1432
# I think this check must be string based though as we are talking about
1433
# a possbly different api object with the same name - probably api will be faster
1434
testforobject = parentnode.fullChildName(self.basename()) # append our name to the path
1435
if objExists(testforobject):
1436
raise RuntimeError("Object %s did already exist" % testforobject)
1437
# END rename on clash handling
1439
# keep existing transformation ? Set the transformation accordingly beforehand
1441
# transform check done in method
1442
self._setWorldspaceTransform(parentnode)
1443
# END if keep worldspace
1446
# As stupid dagmodifier cannot handle instances right (as it works on MObjects
1447
mod = None # create it once we are sure the operation takes place
1449
if parentnode == self:
1450
raise RuntimeError("Cannot parent object %s under itself" % self)
1452
mod = undo.DagModifier()
1453
mod.reparentNode(self.object(), parentnode.object())
1456
if isinstance(self, Shape):
1457
raise RuntimeError("Shape %s cannot be parented under root '|' but needs a transform" % self)
1458
mod = undo.DagModifier()
1459
mod.reparentNode(self.object())
1460
# END handle parent node
1465
# find it in parentnodes children
1467
for child in parentnode.children():
1468
if DependNode.__eq__(self, child):
1470
else: # return updated version of ourselves
1471
return NodeFromObj(self.object())
1472
# END post-handle parent Node
1475
raise AssertionError("Could not find self in children after reparenting")
1478
def unparent(self, **kwargs):
1479
"""As `reparent`, but will unparent this transform under the scene root"""
1480
return self.reparent(None, **kwargs)
1483
def addInstancedChild(self, childNode, position=MFnDagNode.kNextPos):
1484
"""Add childnode as instanced child to this node
1486
:note: for more information, see `addChild`
1487
:note: its a shortcut to addChild allowing to clearly indicate what is happening"""
1488
return self.addChild(childNode, position = position, keepExistingParent=True)
1491
def removeChild(self, childNode, allowZeroParents = False):
1492
"""remove the given childNode (being a child of this node) from our child list, effectively
1493
parenting it under world !
1495
:param childNode: Node to unparent - if it is not one of our children, no change takes place
1496
:param allowZeroParents: if True, it is possible to leave a node unparented, thus no valid
1497
dag paths leads to it. If False, transforms will just be reparented under the world
1498
:return: copy of childnode pointing to the first valid dag path we find.
1499
:note: to prevent the child (if transform) to dangle in unknown space if the last instance
1500
is to be removed, it will instead be reparented to world.
1501
:note: removing shapes from their last parent will result in an error"""
1502
# reparent if we have a last-instance of something
1503
if not allowZeroParents:
1504
if childNode.instanceCount(False) == 1:
1505
if isinstance(childNode, Transform):
1506
return childNode.reparent(None)
1508
# must be shape - raise
1509
# TODO: could create new transform node which is pretty close to the maya default behaviour
1510
raise RuntimeError("Shapenodes cannot be unparented if no parent transform would be left")
1511
# END if instance count == 1
1512
# END if not allowZeroParents
1514
op = undo.GenericOperation()
1515
dagfn = api.MFnDagNode(self.dagPath())
1517
# The method will not fail if the child cannot be found in child list
1520
op.setDoitCmd(dagfn.removeChild, childNode.object())
1521
op.setUndoitCmd(self.addChild, childNode, keepExistingParent=True) # TODO: add child to position it had
1524
return NodeFromObj(childNode.object()) # will attach A new dag path respectively - it will just pick the first one it gets
1528
def addChild(self, childNode, position=MFnDagNode.kNextPos, keepExistingParent=False,
1529
renameOnClash=True, keepWorldSpace = False):
1530
"""Add the given childNode as child to this Node. Allows instancing !
1532
:param childNode: Node you wish to add
1533
:param position: the index to which to add the new child, kNextPos will add it as last child.
1534
It supports python style negative indices
1535
:param keepExistingParent: if True, the childNode will be instanced as it will
1536
have its previous parent(s) and this one, if False, the previous parent will be removed
1537
from the child's parent list
1538
:param renameOnClash: resolve nameclashes by automatically renaming the node to make it unique
1539
:param keepWorldSpace: see `reparent`, only effective if the node is not instanced
1540
:return: childNode whose path is pointing to the new child location
1541
:raise ValueError: if keepWorldSpace is requested with directly instanced nodes
1542
:note: as maya internally handles add/remove child as instancing operation, even though
1543
keepExistingParent is False, it will mess up things and for a short period of time in fact
1544
have two n + 1 instances, right before one is unlinked, This still fills a slot or something, and
1545
isInstanced will be true, although the pathcount is 1.
1546
Long story short: if the item to be added to us is not instanced, we use reparent instead. It
1547
will not harm in direct instances, so its save to use.
1548
:note: if the instance count of the item is 1 and keepExistingParent is False, the position
1549
argument is being ignored"""
1550
# should we use reparent to get around an instance bug ?
1551
is_direct_instance = childNode.instanceCount(0) > 1
1552
if not keepExistingParent and not is_direct_instance: # direct only
1553
return childNode.reparent(self, renameOnClash=renameOnClash, raiseOnInstance=False,
1554
keepWorldSpace = keepWorldSpace)
1555
# END reparent if not-instanced
1557
# CHILD ALREADY THERE ?
1558
#########################
1559
# We do not raise if the user already has what he wants
1560
# check if child is already part of our children
1562
# lets speed things up - getting children is expensive
1563
if isinstance(childNode, Transform):
1564
children = self.childTransforms()
1566
children = self.shapes()
1569
for exChild in children:
1570
if DependNode.__eq__(childNode, exChild):
1571
return exChild # exchild has proper dagpath
1572
del(children) # release memory
1574
if not renameOnClash:
1575
# check existing children of parent and raise if same name exists
1576
# I think this check must be string based though as we are talking about
1577
# a possbly different api object with the same name - probably api will be faster
1578
testforobject = self.fullChildName(childNode.basename()) # append our name to the path
1579
if objExists(testforobject):
1580
raise RuntimeError("Object %s did already exist below %r" % (testforobject , self))
1581
# END rename on clash handling
1586
op = undo.GenericOperationStack()
1589
if pos != self.kNextPos:
1590
pos = pythonIndex(pos, self.childCount())
1592
dagfn = api.MFnDagNode(self.dagPath())
1593
docmd = Call(dagfn.addChild, childNode.object(), pos, True)
1594
undocmd = Call(self.removeChild, childNode)
1596
op.addCmd(docmd, undocmd)
1599
# EXISTING PARENT HANDLING
1600
############################
1601
# if we do not keep parents, we also have to re-add it to the original parent
1602
# therefore wer create a dummy do with a real undo
1604
parentTransform = None
1605
if not keepExistingParent:
1606
# remove from childNode from its current parent (could be world !)
1607
parentTransform = childNode.parent()
1608
validParent = parentTransform
1610
# get the world, but initialize the function set with an mobject !
1611
# works only in the world case !
1612
worldobj = api.MFnDagNode(childNode.dagPath()).parent(0)
1613
validParent = DagNode(worldobj)
1614
# END if no valid parent
1616
parentDagFn = api.MFnDagNode(validParent.dagPath())
1617
childNodeObject = childNode.object()
1619
docmd = Call(parentDagFn.removeChild, childNodeObject)
1620
# TODO: find current position of item at parent restore it exactly
1621
undocmdCall = Call(parentDagFn.addChild, childNodeObject, MFnDagNode.kNextPos, True) # call ourselves
1623
# special case to add items back to world
1624
op.addCmd(docmd, undocmdCall)
1625
# END if not keep existing parent
1629
# UPDATE THE DAG PATH OF CHILDNODE
1630
################################
1631
# find dag path at the used index
1633
if pos == self.kNextPos:
1634
dagIndex = self.childCount() - 1 # last entry as child got added
1635
newChildNode = NodeFromObj(MDagPathUtil.childPathAtIndex(self.dagPath(), dagIndex))
1637
# update undo cmd to use the newly created child with the respective dag path
1638
undocmd.args = [newChildNode]
1640
# ALTER CMD FOR WORLD SPECIAL CASE ?
1641
######################################
1642
# alter undo to readd childNode to world using MEL ? - need final name for
1643
# this, which is why we delay so much
1644
if not keepExistingParent and not parentTransform and undocmdCall is not None: # have call and child is under world
1645
undocmdCall.func = cmds.parent
1646
undocmdCall.args = [str(newChildNode)]
1647
undocmdCall.kwargs = { "add":1, "world":1 }
1652
def addParent(self, parentnode, **kwargs):
1653
"""Adds ourselves as instance to the given parentnode at position
1655
:param kwargs: see `addChild`
1656
:return: self with updated dag path"""
1657
kwargs.pop("keepExistingParent", None)
1658
return parentnode.addChild(self, keepExistingParent = True, **kwargs)
1661
def setParent(self, parentnode, **kwargs):
1662
"""Change the parent of self to parentnode being placed at position
1664
:param kwargs: see `addChild`
1665
:return: self with updated dag path"""
1666
kwargs.pop("keepExistingParent", None) # knock off our changed attr
1667
return parentnode.addChild(self, keepExistingParent = False, **kwargs)
1670
def removeParent(self, parentnode ):
1671
"""Remove ourselves from given parentnode
1674
return parentnode.removeChild(self)
1677
#} END DAG modification
1681
"""Delete this node - this special version must be
1683
:note: if the undo queue is enabled, the object becomes invalid, but stays alive until it
1685
:note: if you want to delete many nodes, its more efficient to delete them
1686
using the global `delete` method"""
1687
mod = undo.DagModifier()
1688
mod.deleteNode(self.object())
1694
def duplicate(self, newpath='', autocreateNamespace=True, renameOnClash=True,
1695
newTransform = False, **kwargs):
1696
"""Duplciate the given node to newpath
1698
:param newpath: result depends on its format:
1700
* '' - empty string, creates a unique name based on the actual node name by appending a copy number
1701
to it, if newTransform is True, the newly created shape/transform will keep its name, but receives a new parent
1702
* 'newname' - relative path, the node will be duplicated not changing its current parent if newTransform is False
1703
* ``|parent|newname`` - absolute path, the node will be duplicated and reparented under the given path
1704
if newTransform is True, a new transform name will be created based on your name by appending a unique copy number
1706
:param autocreateNamespace: if true, namespaces given in newpath will be created automatically, otherwise
1707
a RuntimeException will be thrown if a required namespace does not exist
1708
:param renameOnClash: if true, clashing names will automatically be resolved by adjusting the name
1709
:param newTransform: if True, a new transform will be created based on the name of the parent transform
1710
of this shape node, appending a unique copy number to it.
1711
Only has an effect for shape nodes
1712
:return: newly create Node
1713
:note: duplicate performance could be improved by checking more before doing work that does not
1714
really change the scene, but adds undo operations
1715
:note: inbetween parents are always required as needed
1716
:todo: add example for each version of newpath
1717
:note: instancing can be realized using the `addChild` function
1718
:note: If meshes have tweaks applied, the duplicate will not have these tweaks and the meshes will look
1720
Using MEL works in that case ... (they fixed it there obviously) , but creates invalid objects
1721
:todo: Undo implementation - every undoable operation must in fact be based on strings to really work, all
1722
this is far too much - dagNode.duplicate must be undoable by itself
1723
:todo: duplicate should be completely reimplemented to support all mel options and actually work with
1724
meshes and tweaks - the underlying api duplication would still be used of course, as well as
1725
connections (to sets) and so on ... """
1726
selfIsShape = isinstance(self, Shape)
1729
# create a valid absolute name to have less special cases later on
1730
# if there is no name given, create a name
1731
if not newpath: # "" or None
1733
newpath = "%s|%s" % (_getUniqueName(self.transform()), self.basename())
1735
newpath = _getUniqueName(self)
1736
# END newTransform if there is no new path given
1737
elif newTransform and selfIsShape:
1738
newpath = "%s|%s" % (_getUniqueName(self.transform()), newpath.split('|')[-1])
1739
elif '|' not in newpath:
1740
myparent = self.parent()
1742
if myparent is not None:
1743
parentname = str(myparent)
1744
newpath = "%s|%s" % (parentname, newpath)
1745
# END path name handling
1747
# Instance Parent Check
1748
dagtokens = newpath.split('|')
1753
# need at least transform and shapename if path is absolute
1754
numtokens = 3 # like "|parent|shape" -> ['','parent', 'shape']
1755
shouldbe = '|transformname|shapename'
1757
numtokens = 2 # like "|parent" -> ['','parent']
1758
shouldbe = '|transformname'
1760
if '|' in newpath and (newpath == '|' or len(dagtokens) < numtokens):
1761
raise NameError("Duplicate paths should be at least %s, was %s" % (shouldbe, newpath))
1762
# END not instance path checking
1766
#####################
1767
if '|' in newpath and objExists(newpath):
1768
exnode = NodeFromStr(newpath)
1769
if not isinstance(exnode, self.__class__):
1770
raise RuntimeError("Existing object at path %s was of type %s, should be %s"
1771
% (newpath, exnode.__class__.__name__, self.__class__.__name__))
1772
return exnode# return already existing one as it has a compatible type
1773
# END target exists check
1777
# DUPLICATE IT WITHOUT UNDO
1778
############################
1779
# it will always duplicate the transform and return it
1780
# in case of instances, its the only way we have to get it below an own parent
1781
# bake all names into strings for undo and redo
1782
duplicate_node_parent = NodeFromObj(api.MFnDagNode(self.dagPath()).duplicate(False, False)) # get the duplicate
1785
# RENAME DUPLICATE CHILDREN
1786
###########################
1788
childsourceparent = self.transform() # works if we are a transform as well
1789
self_shape_duplicated = None # store Node of duplicates that corresponds to us (only if self is shape)
1791
srcchildren = childsourceparent.childrenDeep()
1792
destchildren = duplicate_node_parent.childrenDeep()
1795
if len(srcchildren) != len(destchildren):
1796
# Happens if we have duplicated a shape, whose transform hat several shapes
1797
# To find usually, there should be only one shape which is our duplicated shape
1798
if len(destchildren) != 1:
1799
raise AssertionError("Expected %s to have exactly one child, but it had %i" % (duplicate_node_parent, len(destchildren)))
1800
self_shape_duplicated = destchildren[0].rename(self.basename())
1802
# this is the only part where we have a one-one relationship between the original children
1803
# and their copies - store the id the current basename once we encounter it
1804
selfbasename = self.basename()
1805
for i,targetchild in enumerate(destchildren):
1806
srcchildbasename = srcchildren[i].basename()
1807
targetchild.rename(srcchildbasename)
1808
# HACK: we should only check the intermediate children, but actually conisder them deep
1809
# trying to reduce risk of problem by only setting duplicate_shape_index once
1810
if not self_shape_duplicated and selfbasename == srcchildbasename:
1811
self_shape_duplicated = targetchild
1812
# END for each child to rename
1818
# create requested parents of our duplicate
1819
parenttokens = dagtokens[:-1]
1820
leafobjectname = dagtokens[-1] # the basename of the dagpath
1821
duplparentname = None
1822
if selfIsShape and newTransform:
1823
parenttokens = dagtokens[:-2] # the parent of the duplicate node parent transform
1824
duplparentname = dagtokens[-2]
1825
# END shape and new transform handling
1828
if parenttokens: # could be [''] too if newpath = '|newpath'
1829
parentnodepath = '|'.join(parenttokens)
1830
parentnode = childsourceparent # in case we have a relative name
1832
# happens on input like "|name",
1833
# handle case that we are duplicating a transform and end up with a name
1834
# that already exists - createNode will return the existing one, and error if
1835
# the type does not match
1836
# We have to keep the duplicate as it contains duplicated values that are not
1837
# present in a generic newly created transform node
1839
if cmds.objExists(parentnodepath):
1840
parentnode = NodeFromStr(parentnodepath)
1841
elif parentnodepath != '':
1842
parentnode = createNode(parentnodepath, "transform",
1843
renameOnClash=renameOnClash,
1844
autocreateNamespace=autocreateNamespace)
1845
# END create parent handling
1848
# reparent our own duplicated node - this is always a transform at this
1850
if parentnode is not None:
1851
if selfIsShape and not newTransform:
1852
# duplicate_shape_parent is not needed, reparent shape to our valid parent
1853
# name and remove the intermediate parent
1854
self_shape_duplicated = self_shape_duplicated.reparent(parentnode, renameOnClash = renameOnClash)
1855
if str(duplicate_node_parent) not in str(parentnode):
1856
duplicate_node_parent.delete()
1857
duplicate_node_parent = None
1858
# END if we may delete the duplicate node parent
1861
# we are a transform and will reparent under our destined parent node
1862
duplicate_node_parent = duplicate_node_parent.reparent(parentnode, renameOnClash=renameOnClash)
1863
# END if there is a new parent node
1864
# END PARENT HANDLING
1866
# if we are a shape duplication, we have to rename the duplicated parent node as well
1867
# since maya's duplication routine really does a lot to change my names :)
1868
if duplparentname and duplicate_node_parent is not None:
1869
duplicate_node_parent = duplicate_node_parent.rename(duplparentname, renameOnClash=renameOnClash)
1870
# END dupl parent rename
1873
######################
1874
final_node = rename_target = duplicate_node_parent # item that is to be renamed to the final name later
1876
# rename target must be the child matching our name
1877
if selfIsShape: # want shape, have transform
1878
final_node = rename_target = self_shape_duplicated
1882
# rename the target to match the leaf of the path
1883
# we currently do not check whether the name is already set
1884
# - the rename method does that for us
1885
final_node = rename_target.rename(leafobjectname, autocreateNamespace = autocreateNamespace,
1886
renameOnClash=renameOnClash)
1888
# call our base class to copy additional information
1889
self.copyTo(final_node, **kwargs)
1895
#{ DAG Status Information
1896
def _checkHierarchyVal(self, plugName, cmpval):
1898
:return: cmpval if the plug value of one of the parents equals cmpval
1899
as well as the current entity"""
1900
if getattr(self, plugName).asInt() == cmpval:
1903
for parent in self.iterParents():
1904
if getattr(parent, plugName).asInt() == cmpval:
1909
def _getDisplayOverrideValue(self, plugName):
1911
:return: the given effective display override value or None if display
1912
overrides are disabled"""
1913
if self.do.mchildByName('ove').asInt():
1914
return getattr(self.do, plugName).asInt()
1916
for parent in self.iterParents():
1917
if parent.do.mchildByName('ove').asInt():
1918
return parent.do.mchildByName(plugName).asInt()
1922
def isVisible(self):
1924
:return: True if this node is visible - its visible if itself and all parents are
1926
return self._checkHierarchyVal('v', False)
1928
def isTemplate(self):
1930
:return: True if this node is templated - this is the case if itself or one of its
1931
parents are templated """
1932
return self._checkHierarchyVal('tmp', True)
1934
def displayOverrideValue(self, plugName):
1936
:return: the override display value actually identified by plugName affecting
1937
the given object (that should be a leaf node for the result you see in the viewport.
1938
The display type in effect is always the last one set in the hierarchy
1939
returns None display overrides are disabled"""
1940
return self._getDisplayOverrideValue(plugName)
1941
#} END dag status information
1944
""":return: True if the object exists in the scene
1945
:note: Handles DAG objects correctly that can be instanced, in which case
1946
the MObject may be valid , but the respective dag path is not.
1947
Additionally, if the object is not parented below any object, everything appears
1948
to be valid, but the path name is empty """
1949
return self.dagPath().isValid() and self.dagPath().fullPathName() != '' and DependNode.isValid(self)
1952
""":return: fully qualified (long) name of this dag node"""
1953
return self.fullPathName()
1955
# override dependnode implementation with the original one
1956
basename = iDagItem.basename
1961
def parentAtIndex(self, index):
1962
""":return: Node of the parent at the given index - non-instanced nodes only have one parent
1963
:note: if a node is instanced, it can have `parentCount` parents
1964
:todo: Update dagpath afterwards ! Use dagpaths instead !"""
1965
sutil = api.MScriptUtil()
1966
sutil.createFromInt(index)
1967
uint = sutil.asUint()
1969
return NodeFromObj(api.MFnDagNode(self.dagPath()).parent(uint))
1971
def transform(self):
1972
""":return: Node to lowest transform in the path attached to our node
1973
:note: for shapes this is the parent, for transforms the transform itself"""
1974
# this should be faster than asking maya for the path and converting
1976
if isinstance(self, Transform):
1978
return NodeFromObj(self.dagPath().transform())
1981
""":return: Maya node of the parent of this instance or None if this is the root"""
1982
# implement raw not using a wrapped path
1983
copy = MDagPath(self.dagPath())
1985
if copy.length() == 0: # ignore world !
1987
return NodeFromObj(copy)
1989
def children(self, predicate = lambda x: True, asNode=True):
1990
""":return: all child nodes below this dag node if predicate returns True for passed Node
1991
:param asNode: if True, you will receive the children as wrapped Nodes, otherwise you
1994
ownpath = self.dagPath()
1995
for i in range(ownpath.childCount()):
1996
copy = MDagPath(ownpath)
1997
copy.push(MDagPath.child(ownpath, i))
2000
copy = NodeFromObj(copy)
2002
if not predicate(copy):
2006
# END for each child
2009
def childrenByType(self, nodeType, predicate = lambda x: True):
2010
""":return: all childnodes below this one matching the given nodeType and the predicate
2011
:param nodeType: class of the nodeTyoe, like nt.Transform"""
2012
return [p for p in self.children() if isinstance(p, nodeType) and predicate(p)]
2014
def shapes(self, predicate = lambda x: True):
2015
""":return: all our Shape nodes
2016
:note: you could use getChildren with a predicate, but this method is more
2017
efficient as it uses dagpath functions to filter shapes"""
2018
shapeNodes = map(NodeFromObj, MDagPathUtil.shapes(self.dagPath())) # could use getChildrenByType, but this is faster
2019
return [s for s in shapeNodes if predicate(s)]
2021
def childTransforms(self, predicate = lambda x: True):
2022
""":return: list of all transform nodes below this one """
2023
transformNodes = map(NodeFromObj, MDagPathUtil.transforms(self.dagPath())) # could use getChildrenByType, but this is faster
2024
return [t for t in transformNodes if predicate(t)]
2026
def instanceNumber(self):
2027
""":return: our instance number
2028
:note: 0 does not indicate that this object is not instanced - use getInstanceCount instead"""
2029
return self.dagPath().instanceNumber()
2031
def instance(self, instanceNumber):
2032
""":return: Node to the instance identified by instanceNumber
2033
:param instanceNumber: range(0, self.instanceCount()-1)"""
2034
# secure it - could crash if its not an instanced node
2035
if self.instanceCount(False) == 1:
2037
raise AssertionError("instanceNumber for non-instanced nodes must be 0, was %i" % instanceNumber)
2040
allpaths = api.MDagPathArray()
2041
self.getAllPaths(allpaths)
2042
# copy the path as it will be invalidated once the array goes out of scope !
2043
return NodeFromObj(MDagPath(allpaths[instanceNumber]))
2045
def hasChild(self, node):
2046
""":return: True if node is a child of self"""
2047
return api.MFnDagNode(self.dagPath()).hasChild(node.object())
2049
def child(self, index):
2050
""":return: child of self at index
2051
:note: this method fixes the MFnDagNode.child method - it returns an MObject,
2052
which doesnt work well with instanced nodes - a dag path is required, which is what
2053
we use to aquire the object"""
2054
copy = MDagPath(self.dagPath())
2055
copy.push(MDagPath.child(self.dagPath(), index))
2056
return NodeFromObj(copy)
2061
def _dagPath_delayed(self):
2062
"""Handles the retrieval of a dagpath from an MObject if it is not known
2064
self._apidagpath = MDagPath()
2065
_mfndag_setObject(self._apiobj)
2066
_mfndag.getPath(self._apidagpath)
2068
object.__setattr__(self, 'dagPath', instancemethod(cls._dagPath_cached, self, cls))
2069
return self._apidagpath
2071
def _dagPath_cached(self):
2072
""":return: MDagPath attached to this node from a cached location"""
2073
return self._apidagpath
2075
def _object_cached(self):
2076
""":return: MObject associated with the path of this instance from a cached location"""
2079
def _object_delayed(self):
2080
""":return: MObject as retrieved from the MDagPath of our Node"""
2081
self._apiobj = self._apidagpath.node() # expensive call
2083
object.__setattr__(self, 'object', instancemethod(cls._object_cached, self, cls))
2086
# delayed mobject retrieval is the default for DagNodes as they are created from
2087
# MDagPaths most of the time
2088
object = _object_delayed
2092
:return: the original DagPath attached to this Node - it's not wrapped
2094
:note: If you plan to alter it, make sure you copy it using the
2095
MDagPath(node.dagPath()) construct !"""
2096
return self._apidagpath
2098
def apiObject(self):
2099
""":return: our dag path as this is our api object - the object defining this node best"""
2100
return self.dagPath()
2104
def iterInstances(self, excludeSelf = False):
2105
"""Get iterator over all (direct and indirect)instances of this node
2107
:param excludeSelf: if True, self will not be returned, if False, it will be in
2109
:note: Iterating instances is more efficient than querying all instances individually using
2111
:todo: add flag to allow iteration of indirect instances as well """
2112
# prevents crashes if this method is called within a dag instance added callback
2113
if self.instanceCount(True) == 1:
2120
ownNumber = self.instanceNumber()
2122
allpaths = api.MDagPathArray()
2123
self.getAllPaths(allpaths)
2125
# paths are ordered by instance number
2126
for i in range(allpaths.length()):
2127
# index is NOT instance number ! If transforms are instanced, children increase instance number
2128
dagpath = allpaths[i]
2129
if dagpath.instanceNumber() != ownNumber:
2130
yield NodeFromObj(MDagPath(dagpath))
2131
# END for each instance
2135
#} END base (classes)
2139
def _new_mixin(cls, *args, **kwargs):
2140
"""Constructor for MObject derived types which only differ in a few parameters.
2141
Requires _base_cls_ and _mfn_suffix_ to be set on the respective class
2143
return an attribute class of the respective type for given MObject
2145
:param args: arg[0] is attribute's MObject to be wrapped.
2146
:note: Custom constructors are not possible as __init__ is automatically called
2147
afterwards - MObject does not support anything but no args or another MObject."""
2148
# may fail as we didn't check of len(args), but its okay, lets safe the if statement
2149
# here ! Python will bark nicely anyway
2151
if cls != cls._base_cls_:
2152
# the user knows which type he wants, created it directly
2153
newinst = object.__new__(cls, mobject)
2154
# NOTE: Although this class is implemented not to need the _apiobj anymore
2155
# as we ARE an MObject, we are learning from the issue in Component
2156
# and just keep another reference to it, to be on the safe side
2157
# DEL_ME_AND_CRASH ############################
2158
newinst._apiobj = newinst ########
2159
#################################
2162
newinst = _createInstByPredicate(mobject, cls, cls, lambda x: x.endswith(cls._mfn_suffix_))
2165
raise ValueError("%s with apitype %r could not be wrapped into any function set" % (cls._mfn_suffix_, mobject.apiTypeStr()))
2169
# assure proper name, just in case
2170
_new_mixin.__name__ = '__new__'
2172
class Attribute(MObject):
2173
"""Represents an attribute in general - this is the base class
2174
Use this general class to create attribute wraps - it will return
2175
a class of the respective type """
2177
__metaclass__ = MetaClassCreatorNodes
2179
_mfn_suffix_ = 'Attribute'
2181
__new__ = _new_mixin
2184
def create(cls, full_name, brief_name, *args, **kwargs):
2185
""":return: A new Attribute
2186
:param full_name: the long name of the attribute
2187
:param brief_name: the brief name of the attribute
2188
:note: all args and kwargs are passed to the respective function set instance
2189
:note: specialize this method in derived types if required"""
2190
if cls == Attribute:
2191
raise TypeError("Cannot create plain Attributes, choose a subclass of Attribute instead")
2192
# END handle invalid type
2194
# keep the class around to be sure we don't die on the way due to decremented
2196
mfninst = cls._mfncls()
2197
attr = mfninst.create(full_name, brief_name, *args, **kwargs)
2198
return cls(attr) # this copies the MObject and we are safe
2201
Attribute._base_cls_ = Attribute
2203
class UnitAttribute(Attribute):
2207
class TypedAttribute(Attribute):
2211
class NumericAttribute(Attribute):
2213
def _create_using(cls, method_name, *args):
2214
mfninst = cls._mfncls()
2215
attr = getattr(mfninst, method_name)(*args)
2219
def createColor(cls, full_name, brief_name):
2220
""":return: An attribute representing a RGB color
2221
:param full_name: see `create`
2222
:param brief_name: see `create`"""
2223
return cls._create_using('createColor', full_name, brief_name)
2226
def createPoint(cls, full_name, brief_name):
2227
""":return: An attribute representing a point with XYZ coordinates
2228
:param full_name: see `create`
2229
:param brief_name: see `create`"""
2230
return cls._create_using('createPoint', full_name, brief_name)
2233
class MessageAttribute(Attribute):
2237
class MatrixAttribute(Attribute):
2241
class LightDataAttribute(Attribute):
2245
class GenericAttribute(Attribute):
2249
class EnumAttribute(Attribute):
2253
class CompoundAttribute(Attribute):
2262
"""Represents an data in general - this is the base class
2263
Use this general class to create data wrap objects - it will return a class of the respective type """
2265
__metaclass__ = MetaClassCreatorNodes
2267
_mfn_suffix_ = 'Data'
2269
__new__ = _new_mixin
2272
def create(cls, *args, **kwargs):
2273
""":return: A new instance of data wrapped in the desired Data type
2274
:note: specialize this method in derived types !"""
2276
raise TypeError("Cannot create 'plain' data, choose a subclass of Data instead")
2277
# END handle invalid type
2279
# keep the instance alive until we have wrapped the MObject which essentiall
2280
# creates a copy and increments its maya ref count.
2281
mfninst = cls._mfncls()
2282
data = mfninst.create(*args, **kwargs)
2285
Data._base_cls_ = Data
2288
class VectorArrayData(Data):
2292
class UInt64ArrayData(Data):
2296
class StringData(Data):
2300
class StringArrayData(Data):
2304
class SphereData(Data):
2308
class PointArrayData(Data):
2312
class PluginData(Data):
2313
"""Wraps plugin data as received by a plug. If plugin's registered their data
2314
types and tracking dictionaries using the `registerPluginDataTrackingDict`,
2315
the original self pointer can easily be retrieved using this classes interface"""
2319
""":return: python data wrapped by this plugin data object
2320
:note: the python data should be made such that it can be changed using
2321
the reference we return - otherwise it will be read-only as it is just a copy !
2322
:note: the data retrieved by this method cannot be used in plug.msetMObject(data) as it
2323
is ordinary python data, not an mobject
2324
:raise RuntimeError: if the data object's id is unknown to this class"""
2325
import maya.OpenMayaMPx as mpx # delayed import as it takes plenty of time
2327
mfn = self._mfncls(self._apiobj)
2328
datatype = mfn.typeId()
2330
trackingdict = sys._dataTypeIdToTrackingDictMap[datatype.id()]
2332
raise RuntimeError("Datatype %r is not registered to python as plugin data" % datatype)
2334
# retrieve the data pointer
2335
dataptrkey = mpx.asHashable(mfn.data())
2337
return trackingdict[dataptrkey]
2339
raise RuntimeError("Could not find data associated with plugin data pointer at %r" % dataptrkey)
2340
# END exception handling tracking dict
2341
# END exception handling dict access
2344
class NumericData(Data):
2348
class NObjectData(Data):
2353
""":note: maya 2011 and newer"""
2357
class MatrixData(Data):
2361
class IntArrayData(Data):
2365
class GeometryData(Data):
2366
"""Wraps geometry data providing additional convenience methods"""
2368
def uniqueObjectId(self):
2369
""":return: an object id that is guaranteed to be unique
2370
:note: use it with addObjectGroup to create a new unique group"""
2371
# find a unique object group id
2373
for ogid in range(self.objectGroupCount()):
2374
exog = self.objectGroup(ogid)
2375
while exog == objgrpid:
2377
# END for each existing object group
2381
class SubdData(GeometryData):
2385
class NurbsSurfaceData(GeometryData):
2389
class NurbsCurveData(GeometryData):
2393
class MeshData(GeometryData):
2397
class LatticeData(GeometryData):
2401
class DynSweptGeometryData(Data):
2405
class DoubleArrayData(Data):
2409
class ComponentListData(Data):
2410
"""Improves the default wrap by adding some required methods to deal with
2413
def __getitem__(self, index):
2414
""":return: the item at the given index"""
2415
return self._mfncls(self)[index]
2418
""":return: number of components stored in this data"""
2419
return self.length()
2421
def __contains__(self, component):
2422
""":return: True if the given component is contained in this data"""
2423
return self.has(component)
2426
class ArrayAttrsData(Data):
2434
class Component(MObject):
2435
"""Represents a shape component - its derivates can be used to handle component lists
2436
to be used in object sets and shading engines """
2437
__metaclass__ = MetaClassCreatorNodes
2438
_mfnType = None # to be set in the subclass component
2440
_mfn_suffix_ = "Component"
2442
__new__ = _new_mixin
2445
def create(cls, component_type):
2446
""":return: A new component instance carrying data of the given component type
2447
:param component_type: MFn:: component type to be created.
2448
:note: It is important that you call this function on the Component Class of
2449
a compatible type, or a RuntimeError will occour"""
2450
if cls == Component:
2451
raise TypeError("The base compnent type cannot be instantiated")
2452
# END handle invalid type
2454
cdata = cls._mfncls().create(component_type)
2458
def getMFnType(cls):
2459
""":return: mfn type of this class
2460
:note: the type returned is *not* the type of the shape component"""
2463
def addElements(self, *args):
2464
"""Operates exactly as described in the MFn...IndexComponent documentation,
2465
but returns self to allow combined calls and on-the-fly component generation
2468
self._mfncls(self).addElements(*args)
2471
def addElement(self, *args):
2472
"""see `addElements`
2475
:note: do not use this function as it will be really slow when handling many
2476
items, use addElements instead"""
2477
self._mfncls(self).addElement(*args)
2480
Component._base_cls_ = Component
2482
class SingleIndexedComponent(Component):
2483
"""precreated class for ease-of-use"""
2484
_mfnType = api.MFn.kSingleIndexedComponent
2486
def getElements(self):
2487
""":return: MIntArray containing the indices this component represents"""
2489
api.MFnSingleIndexedComponent(self).getElements(u)
2493
elements = getElements
2495
class DoubleIndexedComponent(Component): # derived just for epydoc
2496
"""Fixes some functions that would not work usually """
2497
_mfnType = api.MFn.kDoubleIndexedComponent
2499
def getElements(self):
2501
:return: (uIntArray, vIntArray) tuple containing arrays with the u and v
2502
indices this component represents"""
2505
api.MFnDoubleIndexedComponent(self).getElements(u, v)
2509
elements = getElements
2512
class TripleIndexedComponent(Component):
2513
"""precreated class for ease-of-use"""
2514
_mfnType = api.MFn.kTripleIndexedComponent
2516
def getElements(self):
2518
:return: (uIntArray, vIntArray, wIntArray) tuple containing arrays with
2519
the u, v and w indices this component represents"""
2523
api.MFnDoubleIndexedComponent(self).getElements(u, v, w)
2527
elements = getElements
2533
class MDagPathUtil(object):
2534
"""Performs operations on MDagPaths which are hard or inconvenient to do otherwise
2536
:note: We do NOT patch the actual api type as this would make it unusable to be passed in
2537
as reference/pointer type unless its being created by maya itself."""
2542
def parentPath(cls, path):
2544
:return: MDagPath to the parent of path or None if path is in the scene
2546
copy = MDagPath(path)
2548
if copy.length() == 0: # ignore world !
2553
def numShapes(cls, path):
2554
""":return: return the number of shapes below path"""
2555
sutil = api.MScriptUtil()
2556
uintptr = sutil.asUintPtr()
2557
sutil.setUint(uintptr , 0)
2559
path.numberOfShapesDirectlyBelow(uintptr)
2561
return sutil.uint(uintptr)
2564
def childPathAtIndex(cls, path, index):
2565
""":return: MDagPath pointing to this path's child at the given index"""
2566
copy = MDagPath(path)
2567
copy.push(path.child(index))
2571
def childPaths(cls, path, predicate = lambda x: True):
2572
""":return: list of child MDagPaths which have path as parent
2573
:param predicate: returns True for each path which should be included in the result."""
2575
for i in xrange(path.childCount()):
2576
childpath = cls.childPathAtIndex(path, i)
2577
if predicate(childpath):
2578
outPaths.append(childpath)
2585
def pop(cls, path, num):
2586
"""Pop the given number of items off the end of the path
2588
:return: path itself"""
2593
def extendToChild(cls, path, num):
2594
"""Extend path to the given child number - can be shape or transform
2596
:return: path itself"""
2597
path.extendToShapeDirectlyBelow(num)
2601
def childPathsByFn(cls, path, fn, predicate = lambda x: True):
2602
"""Get all children below path supporting the given MFn.type
2604
:return: MDagPaths to all matched paths below this path
2605
:param fn: member of MFn
2606
:param predicate: returns True for each path which should be included in the result."""
2607
isMatch = lambda p: p.hasFn(fn)
2608
return [p for p in cls.childPaths(path, predicate = isMatch) if predicate(p)]
2611
def shapes(cls, path, predicate = lambda x: True):
2612
""":return: MDagPaths to all shapes below path
2613
:param predicate: returns True for each path which should be included in the result.
2614
:note: have to explicitly assure we do not get transforms that are compatible to the shape function
2615
set for some reason - this is just odd and shouldn't be, but it happens if a transform has an instanced
2616
shape for example, perhaps even if it is not instanced"""
2617
return [shape for shape in cls.childPathsByFn(path, api.MFn.kShape, predicate=predicate) if shape.apiType() != api.MFn.kTransform]
2620
def transforms(cls, path, predicate = lambda x: True):
2621
""":return: MDagPaths to all transforms below path
2622
:param predicate: returns True to include path in result"""
2623
return cls.childPathsByFn(path, api.MFn.kTransform, predicate=predicate)
2624
#} END edit in place
2631
class Reference(DependNode):
2632
"""Implements additional utilities to work with references"""
2634
def fileReference(self):
2636
:return: `FileReference` instance initialized with the reference we
2638
import mrv.maya.ref as refmod
2639
return refmod.FileReference(refnode=self)
2642
class Transform(DagNode): # derived just for epydoc
2643
"""Precreated class to allow isinstance checking against their types and
2644
to add undo support to MFnTransform functions, as well as for usability
2646
:note: bases determined by metaclass
2647
:note: to have undoable set* functions , get the (improved) transformation matrix
2648
make your changes to it and use the `set` method """
2650
#{ MFnTransform Overrides
2653
def set(self, transformation):
2654
"""Set the transformation of this Transform node"""
2655
curtransformation = self.transformation()
2656
setter = self._api_set
2657
op = undo.GenericOperation()
2658
op.setDoitCmd(setter, transformation)
2659
op.setUndoitCmd(setter, curtransformation)
2662
#} END mfntransform overrides
2665
#{ Convenience Overrides
2667
""":return: MVector containing the scale of the transform"""
2668
return in_double3_out_vector(self._api_getScale)
2671
""":return: MVector containing the shear of the transform"""
2672
return in_double3_out_vector(self._api_getShear)
2675
def setScale(self, vec_scale):
2676
"""Set the scale of the transform with undo support from a single vector"""
2677
return undoable_in_double3_as_vector(self._api_setScale, self.getScale(), vec_scale)
2680
def setShear(self, vec_shear):
2681
"""Set the shear value of the transform with undo support from single vector"""
2682
return undoable_in_double3_as_vector(self._api_setShear, self.getShear(), vec_shear)
2685
def shearBy(self, vec_value):
2686
"""Add the given vector to the transform's shear"""
2687
return undoable_in_double3_as_vector(self._api_shearBy, self.getShear(), vec_value)
2690
def scaleBy(self, vec_value):
2691
"""Add the given vector to the transform's scale"""
2692
return undoable_in_double3_as_vector(self._api_scaleBy, self.getScale(), vec_value)
2694
#} END convenience overrides
2697
class Shape(DagNode): # base for epydoc !
2698
"""Interface providing common methods to all geometry shapes as they can be shaded.
2699
They usually support per object and per component shader assignments
2701
:note: as shadingEngines are derived from objectSet, this class deliberatly uses
2702
them interchangably when it comes to set handling.
2703
:note: for convenience, this class implements the shader related methods
2705
:note: bases determined by metaclass"""
2708
#{ preset type filters
2709
fSetsRenderable = SetFilter(api.MFn.kShadingEngine, False, 0) # shading engines only
2710
fSetsDeformer = SetFilter(api.MFn.kSet, True , 1) # deformer sets only
2715
def _parseSetConnections(self, allow_compoents):
2716
"""Manually parses the set connections from self
2718
:return: tuple(MObjectArray(setapiobj), MObjectArray(compapiobj)) if allow_compoents, otherwise
2719
just a list(setapiobj)"""
2720
sets = api.MObjectArray()
2721
iogplug = self._getSetPlug() # from DagNode , usually iog plug
2723
# this will never fail - logcical index creates the plug as needed
2724
# and drops it if it is no longer required
2726
components = api.MObjectArray()
2728
# take full assignments as well - make it work as the connectedSets api method
2729
for dplug in iogplug.moutputs():
2730
sets.append(dplug.node())
2731
components.append(MObject())
2732
# END full objecft assignments
2734
for compplug in iogplug.mchildByName('objectGroups'):
2735
for setplug in compplug.moutputs():
2736
sets.append(setplug.node()) # connected set
2738
# get the component from the data
2739
compdata = compplug.mchildByName('objectGrpCompList').masData()
2740
if compdata.length() == 1: # this is what we can handle
2741
components.append(compdata[0]) # the component itself
2743
raise AssertionError("more than one compoents in list")
2744
# END assure we have components in data
2745
# END for each set connected to component
2746
# END for each component group
2748
return (sets, components)
2750
for dplug in iogplug.moutputs():
2751
sets.append(dplug.node())
2753
# END for each object grouop connection in iog
2756
def componentAssignments(self, setFilter = fSetsRenderable, use_api = True, asComponent = True):
2758
:return: list of tuples(ObjectSetNode, Component_or_MObject) defininmg shader
2759
assignments on per component basis.
2761
If a shader is assigned to the whole object, the component would be a null object, otherwise
2762
it is an instance of a wrapped IndexedComponent class
2763
:note: The returned Component will be an MObject(kNullObject) only in case the component is
2764
not set. Hence you should check whether it isNull() before actually using it.
2765
:param setFilter: see `connectedSets`
2766
:param use_api: if True, api methods will be used if possible which is usually faster.
2767
If False, a custom non-api implementation will be used instead.
2768
This can be required if the apiImplementation is not reliable which happens in
2769
few cases of 'weird' component assignments
2770
:param asComponent: If True, the components will be wrapped into the matching MRV compontent type
2771
to provide a nicer interface. This might slightly slow down the process, but this is usually
2773
:note: the sets order will be the order of connections of the respective component list
2774
attributes at instObjGroups.objectGroups
2775
:note: currently only meshes and subdees support per component assignment, whereas only
2776
meshes can have per component shader assignments
2777
:note: SubDivision Components cannot be supported as the component type kSubdivCVComponent
2778
cannot be wrapped into any component function set - reevaluate that with new maya versions !
2779
:note: deformer set component assignments are only returned for instance 0 ! They apply to all
2780
output meshes though"""
2781
# SUBDEE SPECIAL CASE
2782
#########################
2783
# cannot handle components for subdees - return them empty
2784
if self._apiobj.apiType() == api.MFn.kSubdiv:
2785
log.warn("components are not supported for Subdivision surfaces due to m8.5 api limitation")
2786
sets = self.connectedSets(setFilter = setFilter)
2787
return [(setnode, MObject()) for setnode in sets]
2788
# END subdee handling
2790
sets = components = None
2794
# QUERY SETS AND COMPONENTS
2795
# for non-meshes, we have to parse the components manually
2796
if not use_api or not self._apiobj.hasFn(api.MFn.kMesh) or not self.isValidMesh():
2797
# check full membership
2798
sets,components = self._parseSetConnections(True)
2799
# END non-mesh handling
2801
# MESH - use the function set
2802
# take all fSets by default, we do the filtering
2803
sets = api.MObjectArray()
2804
components = api.MObjectArray()
2805
self.getConnectedSetsAndMembers(self.instanceNumber(), sets, components, False)
2806
# END sets/components query
2809
# wrap the sets and components
2811
for setobj,compobj in zip(sets, components):
2812
if not setFilter(setobj):
2815
setobj = NodeFromObj(MObject(setobj)) # copy obj to get memory to python
2816
if not compobj.isNull() and asComponent:
2817
compobj = Component(compobj) # this copies the object as well
2819
compobj = MObject(compobj) # make it ours
2820
# END handle component type
2822
outlist.append((setobj, compobj))
2823
# END for each set/component pair
2826
#} END set interface