3
Allows convenient access and handling of references in an object oriented manner
5
__docformat__ = "restructuredtext"
7
from mrv.path import make_path
8
from mrv.util import And
9
from mrv.exc import MRVError
10
from mrv.maya.ns import Namespace, _isRootOf
11
from mrv.maya.util import noneToList
12
from mrv.interface import iDagItem
14
import maya.cmds as cmds
15
import maya.OpenMaya as api
16
from itertools import ifilter
18
__all__ = ("createReference", "listReferences", "FileReference", "FileReferenceError")
21
class FileReferenceError(MRVError):
29
def createReference(*args, **kwargs):
30
"""create a new reference, see `FileReference.create` for more information"""
31
return FileReference.create(*args, **kwargs)
33
def listReferences(*args, **kwargs):
34
"""List intermediate references of in the scene, see `FileReference.ls` for
36
return FileReference.ls(*args, **kwargs)
41
class FileReference(iDagItem):
42
"""Represents a Maya file reference
44
:note: do not cache these instances but get a fresh one when you have to work with it
45
:note: as FileReference is also a iDagItem, all the respective methods, especially for
46
parent/child iteration and query can be used as well"""
47
editTypes = [ 'setAttr','addAttr','deleteAttr','connectAttr','disconnectAttr','parent']
48
_sep = '/' # iDagItem configuration
49
__slots__ = '_refnode'
52
def _splitCopyNumber(cls, path):
53
""":return: (path, copynumber), copynumber is at least 0 """
54
lbraceindex = path.rfind('{')
57
# END handle no brace found
59
return (path[:lbraceindex], int(path[lbraceindex+1:-1]))
62
def __init__(self, filepath = None, refnode = None):
64
self._refnode = str(refnode)
66
self._refnode = cmds.referenceQuery(filepath, rfn=1)
68
raise ValueError("Specify either filepath or refnode to initialize the instance from")
71
def __eq__(self, other):
72
"""Special treatment for other filereferences"""
73
# need equal copy numbers as well as equal paths - the refnode encapsulates all this
74
if isinstance(other, FileReference):
75
return self._refnode == other._refnode
77
return self.path() == other
79
def __ne__(self, other):
80
return not self.__eq__(other)
83
return hash(self.path(copynumber=1))
86
return str(self.path())
89
return "FileReference(%s)" % str(self.path(copynumber=1))
91
#} END object overrides
93
#{ Reference Adjustments
95
def create(cls, filepath, namespace=None, load = True, **kwargs):
96
"""Create a reference with the given namespace
98
:param filepath: path describing the reference file location
99
:param namespace: if None, a unique namespace will be generated for you
100
The namespace will contain all referenced objects.
101
:param load: if True, the reference will be created in loaded state, other
102
wise its loading is deferred
103
:param kwargs: passed to file command
104
:raise ValueError: if the namespace does already exist
105
:raise RuntimeError: if the reference could not be created"""
106
filepath = make_path(cls._splitCopyNumber(filepath)[0])
109
if not i: return base
110
return "%s%i" % (base,i)
113
if not ns: # assure unique namespace
114
nsbasename = filepath.stripext().basename()
115
ns = Namespace.findUnique(nsbasename, incrementFunc = nsfunc)
117
ns = Namespace(ns) # assure we have a namespace object
119
ns = ns.relativeTo(Namespace(Namespace.rootpath))
121
raise ValueError("Namespace %s for %s does already exist" % (ns,filepath))
123
# assure we keep the current namespace
124
prevns = Namespace.current()
126
# removing duplicate **kwargs
127
kwargs.pop('ns', None)
128
kwargs.pop('reference', kwargs.pop('r', None))
129
kwargs.pop('deferReference', kwargs.pop('dr', None))
131
createdRefpath = cmds.file(filepath, ns=str(ns),r=1,dr=not load, **kwargs)
134
# END assure we keep the namespace
136
return FileReference(createdRefpath)
139
def remove(self, **kwargs):
140
""" Remove the given reference from the scene
142
:note: assures that no namespaces of that reference are left, remaining objects
143
will be moved into the root namespace. This way the namespaces will not be left as waste.
144
This fails if there are referenced objects in the subnamespace - we currently
145
ignore that issue as the main reference removal worked at that point.
146
:note: kwargs passed to namespace.delete """
147
ns = self.namespace()
148
cmds.file(self.path(copynumber=1), rr=1)
155
def replace(self, filepath):
156
"""Replace this reference with filepath
158
:param filepath: the path to the file to replace this reference with
159
Reference instances will be handled as well.
161
filepath = (isinstance(filepath, type(self)) and filepath.path()) or filepath
162
filepath = self._splitCopyNumber(filepath)[0]
163
cmds.file(filepath, lr=self._refnode)
167
def importRef(self, depth=0):
168
"""Import the reference until the given depth is reached
171
- x<1: import all references and subreferences
172
- x: import until level x is reached, 0 imports just self such that
173
all its children are on the same level as self was before import
174
:return: list of FileReference objects that are now in the root namespace - this
175
list could be empty if all subreferences are fully imported"""
176
def importRecursive(reference, curdepth, maxdepth):
178
reference.setLoaded(True)
179
children = reference.children()
180
cmds.file(reference.path(copynumber=1), importReference=1)
182
if curdepth == maxdepth:
186
for childref in children:
187
outsubrefs.extend(importRecursive(childref, curdepth+1, maxdepth))
190
# END importRecursive
192
return importRecursive(self, 0, depth)
194
# } END reference adjustments
199
def fromPaths(cls, paths, **kwargs):
200
"""Find the reference for each path in paths. If you provide the path X
201
2 times, but you only have one reference to X, the return value will be
202
[FileReference(X), None] as there are less references than provided paths.
204
:param paths: a list of paths or references whose references in the scene
205
should be returned. In case a reference is found, its plain path will be
207
:param kwargs: all supported by `ls` to yield the base set of references
208
we will use to match the paths with. Additionally, you may specify:
211
if True, default False, the extension will be ignored
212
during the search, only the actual base name will matter.
213
This way, an MA file will be matched with an MB file.
214
The references returned will still have their extension original extension.
216
:return: list(FileReference|None, ...)
217
if a filereference was found for given occurrence of Path, it will be returned
218
at index of the current path in the input paths, otherwise it is None.
219
:note: zip(paths, result) to get a corresponding tuple list associating each input path
220
with the located reference"""
221
if not isinstance(paths, (list,tuple)) or hasattr(paths, 'next'):
222
raise TypeError("paths must be tuple, was %s" % type(paths))
224
ignore_ext = kwargs.pop("ignore_extension", False)
225
refs = cls.ls(**kwargs)
227
# build dict for fast lookup
228
# It will keep each reference
230
pathscp = [(isinstance(p, cls) and p.path()) or make_path(p) for p in paths]
234
conv = lambda f: f.expandvars().splitext()[0]
235
# END ignore extension converter
237
def countTuple(filepath, lut):
238
count = lut.get(filepath, 0)
239
lut[filepath] = count + 1
240
return (filepath , count)
245
lut[countTuple(conv(ref.path()), clut)] = ref # keys have no ext
246
# END for each ref to put into lut
249
for i,path in enumerate(pathscp):
250
pathscp[i] = countTuple(conv(path), clut)
251
# END for each path to prepare
255
ref_or_none = lut.get(path, None)
256
outlist.append(ref_or_none)
257
# no need to delete the keys as they have to be unique anyway
258
# END for each path to find
262
def ls(cls, rootReference = "", predicate = lambda x: True):
263
"""list all references in the scene or under the given root
265
:param rootReference: if not empty, the references below it will be returned.
266
Otherwise all scene references will be listed.
267
May be string, Path or FileReference
268
:param predicate: method returning true for each valid file reference object that
269
should be part of the return value.
270
:return: list of `FileReference` s objects"""
271
if isinstance(rootReference, cls):
272
rootReference = rootReference.path(copynumber=1)
273
# END handle non-string type
275
for reffile in cmds.file(str(rootReference), q=1, r=1):
276
refinst = FileReference(filepath = reffile)
277
if predicate(refinst):
279
# END for each reference file
283
def lsDeep(cls, predicate = lambda x: True, **kwargs):
284
"""Return all references recursively
286
:param kwargs: support for arguments as in `ls`, hence you can use the
287
rootReference flag to restrict the set of returned FileReferences."""
288
kwargs['predicate'] = predicate
289
refs = cls.ls(**kwargs)
292
out.extend(ref.childrenDeep(order=cls.kOrder_BreadthFirst, predicate=predicate))
298
def iterNodes(self, *args, **kwargs):
299
"""Creates iterator over nodes in this reference
301
:param args: MFn.kType filter ids to be used to pre-filter all nodes.
302
If you know what you are looking for, setting this can greatly improve
304
:param kwargs: additional kwargs will be passed to either `iterDagNodes`
305
or `iterDgNodes` (dag = False). The following additional kwargs may
309
if True, default True, return wrapped Nodes, if False MDagPaths
310
or MObjects will be returned
313
if True, default False, return dag nodes only. Otherwise return dependency nodes
314
as well. Enables assemblies and assembilesInReference.
317
if True, return only dagNodes with no parent. Needs dag and
318
is mutually exclusive with assembilesInReference.
320
* assembliesInReference:
321
if True, return only dag nodes that have no
322
parent in their own reference. They may have a parent not coming from their
323
reference though. This flag has a big negative performance impact and requires dag.
326
if function returns True for Node|MObject|MDagPath n, n will be yielded.
327
Defaults to return True for all.
328
:raise ValueError: if incompatible arguments have been given"""
331
rns = self.namespace()
332
rnsrela = rns.toRelative()+':'
333
asNode = kwargs.get('asNode', True)
334
predicate = kwargs.get('predicate', lambda n: True)
335
kwargs['asNode'] = False # we will do it
337
dag = kwargs.pop('dag', False)
338
assemblies = kwargs.pop('assemblies', False)
339
assembliesInReference = kwargs.pop('assembliesInReference', False)
342
if (assemblies or assembliesInReference) and not dag:
343
raise ValueError("Cannot list assemblies of any kind if dag is not specified")
345
if assemblies and assembliesInReference:
346
raise ValueError("assemblies and assembilesInReference are mutually exclusive")
348
# CONSTRUCT PREDICATE
352
# cache functions for 10% more performance
353
mfndag = api.MFnDagNode()
354
mfndagSetObject = mfndag.setObject
355
mfndagParentNamespace = mfndag.parentNamespace
356
MDagPath = api.MDagPath
357
mdppop = MDagPath.pop
358
mdplen = MDagPath.length
362
if not _isRootOf(rnsrela, mfndagParentNamespace()):
364
# END first namespace check
366
# assemblies have no parents
373
elif assembliesInReference:
377
# check whether parent is in a different namespace
379
if _isRootOf(rnsrela, mfndagParentNamespace()):
381
# END check parent rns
383
# END handle assemblies
388
iter_type = nt.it.iterDagNodes
390
mfndep = api.MFnDependencyNode()
391
mfndepSetObject = mfndep.setObject
392
mfndepParentNamespace = mfndep.parentNamespace
396
if not _isRootOf(rnsrela, mfndepParentNamespace()):
398
# END first namespace check
403
iter_type = nt.it.iterDgNodes
404
# END handle dag/dg mode predicate
406
kwargs['predicate'] = pred
408
# have to iterate it manually in order to get the toNode conversion right
409
NodeFromObj = nt.NodeFromObj
410
for n in iter_type(*args, **kwargs):
415
# END for each node in iteartion
420
def cleanup(self, unresolvedEdits = True, editTypes = editTypes):
421
"""remove unresolved edits or all edits on this reference
423
:param unresolvedEdits: if True, only dangling connections will be removed,
424
if False, all reference edits will be removed - the reference will be unloaded for beforehand.
425
The loading state of the reference will stay unchanged after the operation.
426
:param editTypes: list of edit types to remove during cleanup
428
wasloaded = self.isLoaded()
429
if not unresolvedEdits:
430
self.setLoaded(False)
432
for etype in editTypes:
433
cmds.file(cr=self._refnode, editCommand=etype)
435
if not unresolvedEdits:
436
self.setLoaded(wasloaded)
441
def setLocked(self, state):
442
"""Set the reference to be locked or unlocked
444
:param state: if True, the reference is locked , if False its unlocked and
447
if self.isLocked() == state:
451
wasloaded = self.isLoaded()
452
self.setLoaded(False)
455
cmds.setAttr(self._refnode+".locked", state)
457
# reset the loading state
458
self.setLoaded(wasloaded)
463
def setLoaded(self, state):
464
"""set the reference loaded or unloaded
466
:param state: True = unload reference, True = load reference
469
if state == self.isLoaded(): # already desired state
473
cmds.file(loadReference=self._refnode)
475
cmds.file(unloadReference=self._refnode)
480
def setNamespace(self, namespace):
481
"""set the reference to use the given namespace
483
:param namespace: Namespace instance or name of the short namespace
484
:raise RuntimeError: if namespace already exists or if reference is not root
486
shortname = namespace
487
if isinstance(namespace, Namespace):
488
shortname = namespace.basename()
491
cmds.file(self.path(copynumber=1), e=1, ns=shortname)
498
""":return: the parent reference of this instance or None if we are root"""
499
parentrfn = cmds.referenceQuery(self._refnode, rfn=1, p=1)
502
return FileReference(refnode = parentrfn)
504
def children(self , predicate = lambda x: True):
505
""" :return: all intermediate child references of this instance """
506
return self.ls(rootReference = self, predicate = predicate)
510
""":return: True if our file reference exists in maya"""
512
self.path(copynumber=1)
519
""":return: True if reference is locked"""
520
return cmds.getAttr(self._refnode + ".locked")
523
""":return: True if the reference is loaded"""
524
return cmds.file(rfn=self._refnode, q=1, dr=1) == False
526
def copynumber(self):
527
""":return: the references copy number - starting at 0 for the first reference
528
:note: we do not cache the copy number as mayas internal numbering can change on
529
when references change - the only stable thing is the reference node name"""
530
return self._splitCopyNumber(self.path(copynumber=1))[1]
533
""":return: namespace object of the full namespace holding all objects in this reference"""
534
fullpath = self.path(copynumber=1)
535
refspace = cmds.file(fullpath, q=1, ns=1)
536
parentspace = cmds.file(fullpath, q=1, pns=1)[0] # returns lists, although its always just one string
539
# END handle parent namespace
540
return Namespace(":" + parentspace + refspace)
542
def path(self, copynumber=False, unresolved = False):
543
""":return: Path object with the path containing the reference's data
544
:param copynumber: If True, the returned path will include the copy number.
545
As it will be a path object, it might not be fully usable in that state
546
:param unresolved: see `ls`
547
:note: we always query it from maya as our numbers change if some other
548
reference is being removed and cannot be trusted"""
549
path_str = cmds.referenceQuery(self._refnode, f=1, un=unresolved)
551
path_str = self._splitCopyNumber(path_str)[0]
552
# END handle copy number
553
return make_path(path_str)
555
def referenceNode(self):
556
""":return: wrapped reference node managing this reference"""
557
import mrv.maya.nt as nt
558
return nt.NodeFromStr(self._refnode)