import re import itertools import maya.OpenMaya as om import maya.cmds as cmds import cross3d from cross3d.abstract.abstractscenewrapper import AbstractSceneWrapper from cross3d import ExceptionRouter class MayaSceneWrapper( AbstractSceneWrapper ): #-------------------------------------------------------------------------------- # Making OpenMaya pythonic #-------------------------------------------------------------------------------- @classmethod def _asMOBject(cls, obj): ''' When passed a object name it will convert it to a MObject. If passed a MObject, it will return the MObject :param obj: A MObject or object name to return as a MObject ''' if isinstance(obj, basestring): sel = om.MSelectionList() sel.add(obj) obj = om.MObject() sel.getDependNode(0, obj) elif obj.__module__.startswith("pymel"): obj = obj.__apimfn__().object() return obj @classmethod def _getAttributeDataType(cls, data): """ Returns the OpenMaya.MFnData id for the given object. Returns OpenMaya.MFnData.kInvalid if the object type could not be identified. :param data: the object to get the data type of :return: int value for the dataType """ dataType = om.MFnData.kInvalid if isinstance(data, basestring): dataType = om.MFnData.kString elif isinstance(value, float): dataType = om.MFnData.kFloatArray elif isinstance(value, int): dataType = om.MFnData.kIntArray # TODO: add support for other types return dataType @classmethod def _createAttribute(cls, mObj, name, dataType=None, shortName=None, default=None): """ Create a attribute on the provided object. Returns the attribute name and shortName. You should provide dataType or default when calling this method so a valid dataType is selected. MayaSceneWrapper._normalizeAttributeName is called on name to ensure it is storing the attribute with a valid attribute name. If shortName is not provided, the name will have MayaSceneWrapper._normalizeAttributeShortName called on it. :param mObj: The OpenMaya.MObject to create the attribute on :param name: The name used to access the attribute :param dataType: The type of data to store in the attribute. Defaults to None. :param shortName: The shortName used by scripting. Defaults to None. :param default: The default value assigned to the attribute. Defaults to None. :return: (name, short name) Because the attribute name and shortName are normalized, this returns the actual names used for attribute names. """ name = cls._normalizeAttributeName(name) if dataType == None and default != None: dataType == cls._getAttributeDataType(default) if dataType == om.MFnData.kInvalid: # No vaid dataType was found, default to None so we can handle it diffrently dataType == None cross3d.logger.debug('Unable To determine the attribute type.\n{}'.format(str(default))) if dataType == None: # MCH 09/17/14 # TODO Evaluate if this is a valid default? dataType = om.MFnData.kAny with ExceptionRouter(): if shortName == None: shortName = cls._normalizeAttributeShortName(name, uniqueOnObj=mObj) depNode = om.MFnDependencyNode(mObj) sAttr = om.MFnTypedAttribute() if False: #if default: # TODO: Handle creating the default object attr = sAttr.create(name, shortName, dataType, default) else: attr = sAttr.create(name, shortName, dataType) # TODO MIKE: Problem with "|groundPlane_transform". try: depNode.addAttribute(attr) except RuntimeError: pass return name, shortName @classmethod def _getPlug(cls, mObj, name): """ For a given OpenMaya.MObject return the OpenMaya.MPlug object with that attribute name. If the property does not exist, raises "RuntimeError: (kInvalidParameter): Cannot find item of required type" :param mObj: The source MObject :param name: The name of the attribute to get from mObj. :return: A OpenMaya.MPlug object """ with ExceptionRouter(): depNode = om.MFnDependencyNode(mObj) attr = depNode.attribute(name) return om.MPlug(mObj, attr) @classmethod def _hasAttribute(cls, mObj, name): with ExceptionRouter(): depNode = om.MFnDependencyNode(mObj) return depNode.hasAttribute(name) @classmethod def _isDagNode(cls, mObj): """ Is this object in the DAG. """ return mObj.hasFn(om.MFn.kDagNode) @classmethod def _namespace(self, mObj): name = self._mObjName(mObj, False) return re.match(r'((?P<namespace>[^:]+):)?(?P<name>.+)', name).groupdict() @classmethod def _normalizeAttributeName(cls, name): """ Removes invalid characters for attribute names from the provided string. :param name: The string used as the name of a attribute. :return: string """ return re.sub(r'\W', '', name) @classmethod def _normalizeAttributeShortName(cls, name, uniqueOnObj=None): """ Creates a shortName for the provided attribute name by calling MayaSceneWrapper._normalizeAttributeName. It then adds the first character to any capital letters in the rest of the name. The name is then lowercased. If uniqueOnObj is provided with a object, it will ensure the returned attribute name is unique by attaching a 3 digit padded number to it. It will be the lowest available number. :param name: The string used to generate the short name. :param uniqueOnObj: Ensure the name is unque. Defaults to None. :return: string """ name = cls._normalizeAttributeName(name) if len(name): name = name[0] + re.sub(r'[a-z]', '', name[1:]) name = name.lower() if uniqueOnObj: # Ensure a unique object name by adding a value to the number. # TODO MIKE: Same issue with the "|groundPlane_transform". try: names = set(cmds.listAttr(cls._mObjName(uniqueOnObj), shortNames=True)) name = api.Scene._findUniqueName(name, names, incFormat='{name}{count}') except ValueError: pass return name @classmethod def _setAttribute(cls, mObj, name, value): """ Stores a pythonic value as a attribute on the provided object. This does not call MayaSceneWrapper._normalizeAttributeName, so make sure you have a valid attribute name. This does not create the attribute, so make sure it is created first. Note: Only string, float and int dataType's are currently supported. :param mObj: A OpenMaya.MObject to set the attribute value to :param name: The name of the attribute to store the value in :param value: The value to store in the attribute """ plug = cls._getPlug(mObj, name) if isinstance(value, basestring): plug.setString(value) elif isinstance(value, bool): plug.setBool(value) # elif isinstance(value, double): # plug.setDouble(value) elif isinstance(value, float): plug.setFloat(value) elif isinstance(value, int): plug.setInt(value) # elif isinstance(value, MAngle): # plug.setMAngle(value) # elif isinstance(value, MDataHandle): # plug.setMDataHandle(value) # elif isinstance(value, MDistance): # plug.setMDistance(value) # elif isinstance(value, MObject): # plug.setMObject(value) # elif isinstance(value, MPxData): # plug.setMPxData(value) # elif isinstance(value, MTime): # plug.setMTime(value) # elif isinstance(value, int): # plug.setNumElements(value) # elif isinstance(value, ShortInt): # plug.setShort(value) @classmethod def _getchildShapeNodes(cls, nativeObject): """ A Maya helper that returns a generator of all shape nodes for the provided transform node. Args: nativeObject (OpenMaya.MObject): The object to get the shape nodes of. """ if nativeObject.apiType() == om.MFn.kTransform: path = om.MDagPath.getAPathTo(nativeObject) numShapes = om.MScriptUtil() numShapes.createFromInt(0) numShapesPtr = numShapes.asUintPtr() path.numberOfShapesDirectlyBelow(numShapesPtr) for index in range(om.MScriptUtil(numShapesPtr).asUint()): p = om.MDagPath.getAPathTo(nativeObject) p.extendToShapeDirectlyBelow(index) yield p.node() @classmethod def _getShapeNode(cls, nativeObject): """ A Maya Helper that returns the first shape node of the provided transform node. If no shape node exists the nativeObject is returned. Args: nativeObject (OpenMaya.MObject): The MObject to get the first shape node from. Returns: OpenMaya.MObject: The first shape node of the transform or the passed in object. """ if nativeObject.apiType() == om.MFn.kTransform: path = om.MDagPath.getAPathTo(nativeObject) numShapes = om.MScriptUtil() numShapes.createFromInt(0) numShapesPtr = numShapes.asUintPtr() path.numberOfShapesDirectlyBelow(numShapesPtr) if om.MScriptUtil(numShapesPtr).asUint(): # TODO: Should this return the last shape, instead of the first? path.extendToShapeDirectlyBelow(0) return path.node() return nativeObject @classmethod def _getTransformNode(cls, nativeObject): """ A Maya Helper that returns the first transform node of the provided shape node. The nativeObject is returned if the nativeObject is a transform node. :param nativeObject: The OpenMaya.MObject to get the transform node of :return: OpenMaya.MObject """ with ExceptionRouter(): # If its not a dag object, there is no transform to return use the nativeObject if not cls._isDagNode(nativeObject): # The world node doesn't play well with the getting transform code. return nativeObject path = om.MDagPath.getAPathTo(nativeObject) newPointer = path.transform() if newPointer != nativeObject: return newPointer return nativeObject @classmethod def _mFnApiTypeMap(cls): """ Creates a dictionary mapping all apiType values to their apiTypeStr. A few values have duplicate keys so the names are inside a list. This method is intended to be used for api exploration only and should not be used in production code. Returns: dict: A dict mapping int values to a list of OpenMaya.MFn constant names. """ out = {} for name in dir(om.MFn): value = getattr(om.MFn, name) if name.startswith('k'): out.setdefault(value, []).append(name) return out @classmethod def _mObjName(cls, nativeObject, fullName=True): """ A Maya Helper that returns the name of a object, because OpenMaya can't expose the name in a single call. :param nativeObject: The OpenMaya.MObject to get the name of :param fullName: If True return the Name(Full Path), else return the displayName. Defaults to True :return: nativeObject's name as a string """ with ExceptionRouter(): if cls._isDagNode(nativeObject): dagPath = om.MDagPath.getAPathTo(nativeObject) if fullName: return dagPath.fullPathName() return dagPath.partialPathName().split("|")[-1] return om.MFnDependencyNode(nativeObject).name() #-------------------------------------------------------------------------------- # cross3d private methods #-------------------------------------------------------------------------------- @property def _nativePointer(self): """ If you are storing OpenMaya.MObject's long enough for them to become invalidated(opening a new scene for example), maya will crash and close because it is now accessing invalid memory. To be able to safely store MObject's for long term, we need to store them in a MObjectHandle object. """ return self._nativeHandle.object() @_nativePointer.setter def _nativePointer(self, nativePointer): if nativePointer is None: # We can't pass None to om.MObjectHandle, but we can pass a empty MObject nativePointer = om.MObject() self._nativeHandle = om.MObjectHandle(nativePointer) def _nativeProperty(self, key, default=None): """ \remarks return the value of the property defined by the inputed key \sa hasProperty, setProperty, _nativeProperty, AbstractScene._fromNativeValue \param key <str> \param default <variant> (auto-converted from the application's native value) \return <variant> """ name = self._mObjName(self._nativePointer, True) ret = cmds.getAttr("{name}.{key}".format(name=name, key=key)) return ret return default def _setNativeProperty(self, key, nativeValue): """ \remarks set the value of the property defined by the inputed key \sa hasProperty, property, setProperty, AbstractScene._toNativeValue \param key <str> \param value <variant> (pre-converted to the application's native value) \retrun <bool> success """ name = self._mObjName(self._nativePointer, True) attrId = "{name}.{key}".format(name=name, key=key) # MCH 10/07/14 HACK: This seems brittle as hell, and will probably will cause problems later # also, we will probably run into cases where we will need to pass kwargs to set the proper variable if isinstance(nativeValue, (list, tuple)): # This may not work in all cases, but based on the documentation, you have to pass a flat # args list to setattr. cmds.setAttr(attrId, *list(itertools.chain(*nativeValue))) # # for the test cases I've found so far, cmds.getAttr returns [(value, value, value)]. # # This can not be passed to setAttr, and must be stripped to a flat args list. # if isinstance(nativeValue[0], (list, tuple)): # value = nativeValue[0] # cmds.setAttr(attrId, *value) return cmds.setAttr(attrId, nativeValue) def propertyNames(self): """ Return a list of the property names linked to this instance :return: list of names """ name = self._mObjName(self._nativePointer, True) return cmds.listAttr(name, settable=True, output=True) #-------------------------------------------------------------------------------- # cross3d public methods #-------------------------------------------------------------------------------- def namespace(self): # I am not re-using the name method on purpose. name = self._mObjName(self._nativePointer, False) # Splitting the name to detect for name spaces. split = name.split(':')[0:] if len(split) > 1: return ':'.join(split[:-1]) return '' def setNamespace(self, namespace): # I am not re-using the name method on purpose. name = self._mObjName(self._nativePointer, False) displayName = name.split(':')[-1] if not namespace: cmds.rename(self.path(), self.displayName()) else: if not cmds.namespace(exists=namespace): cmds.namespace(add=namespace) cmds.rename(self.path(), ':'.join([namespace, displayName])) return True def displayName(self): """ Returns the display name for object. This does not include parent structure """ return self.name().split(':')[-1] def name(self): """ Return the full name of this object, including parent structure """ return self._mObjName(self._nativePointer, False) def path(self): return self._mObjName(self._nativePointer, True) def setDisplayName(self, name): """ Set the display name for this wrapper instance to the inputed name - if not reimplemented, then it will set the object's actual name to the inputed name """ cmds.rename(self.path(), ':'.join([self.namespace(), name])) def setUniqueId(self, uniqueId): """ Unique Id is read only and can not be set in Maya """ return False def uniqueId(self): """ Return the unique id for this controller instance """ return self._nativeHandle.hashCode() # register the symbol cross3d.registerSymbol('SceneWrapper', MayaSceneWrapper)