├── LICENSE ├── README.md └── meta ├── __init__.py ├── config.py ├── core.py ├── examples ├── __init__.py ├── actor.py ├── rig.py └── skeleton.py └── manager.py /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, ArenaNet LLC 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of ArenaNet LLC nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Metanode 2 | A Python package for storing and operating on data in Maya. The Metanode is a class that wraps a Maya network node in a 3 | scene. The class offers a standardized interface for querying and editing data on its network node. The network node 4 | stores information about the scene without the need for a extra file. 5 | 6 | ## Getting Started 7 | To create your own Metanode create a class that inherits from Metanode in core.py. Overwrite the attr class method with 8 | a dictionary of the attributes you wish to create on the network node. The key should be the attribute name with values 9 | that are the arguments for adding the attribute. 10 | 11 | Once you have your new Metanode class generate it in a Maya scene. 12 | 13 | ``` 14 | my_meta = module.MetanodeSubClass.create('Name') 15 | ``` 16 | 17 | After a Metanode is created you can re-wrap it by initializing the class with the network node as a PyMel object. 18 | 19 | ``` 20 | network_node = pymel.core.PyNode('Name') 21 | module.MetanodeSubClass(network_node) 22 | ``` 23 | 24 | ## Authors 25 | 26 | * **Kyle Mistlin-Rude** 27 | * **Andrew Christophersen** 28 | * **Adam Perin** 29 | -------------------------------------------------------------------------------- /meta/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['core', 'manager', 'examples'] 2 | 3 | 4 | import core 5 | import manager 6 | import examples 7 | -------------------------------------------------------------------------------- /meta/config.py: -------------------------------------------------------------------------------- 1 | """Data for managing our Metanodes""" 2 | 3 | # Metanode manager node remapping and deletion 4 | META_TO_CHECK = [] 5 | META_TO_REMOVE = [] 6 | 7 | # Relink {'old.path': 'new.path'} 8 | META_TO_RELINK = {} 9 | 10 | # Global Metanode strings 11 | NODE_TYPE = 'network' 12 | META_TYPE = 'metaType' 13 | META_VERSION = 'metaVersion' 14 | LINEAL_VERSION = 'linealVersion' 15 | -------------------------------------------------------------------------------- /meta/core.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base classes and functions for creating new Metanode classes. 3 | """ 4 | 5 | from collections import OrderedDict 6 | import inspect 7 | import json 8 | 9 | import maya.api.OpenMaya as om2 10 | import pymel.core as pm 11 | 12 | from meta.config import * 13 | 14 | 15 | class Register(type): 16 | """ 17 | Meta type for tracking all Metanode classes in the import path. 18 | """ 19 | __meta_types__ = {} 20 | 21 | def __init__(cls, *args, **kwargs): 22 | super(Register, cls).__init__(*args, **kwargs) 23 | fully_qualified = cls.__module__ + '.' + cls.__name__ 24 | cls.__class__.__meta_types__[fully_qualified] = cls 25 | cls.meta_type = fully_qualified 26 | 27 | 28 | class Metanode(object): 29 | """ 30 | Base Metanode class. All Metanodes should inherit from this class. 31 | """ 32 | __metaclass__ = Register 33 | meta_version = 1 34 | events = dict() 35 | callbacks = dict() 36 | 37 | def __init__(self, node): 38 | """ 39 | Wrap a PyMel node with the Metanode class. 40 | """ 41 | if not hasattr(node, META_TYPE): 42 | raise Exception("{0} isn't a Metanode".format(node)) 43 | 44 | meta_type = node.attr(META_TYPE).get() 45 | if meta_type != self.meta_type and meta_type not in META_TO_RELINK.keys(): 46 | if meta_type not in Register.__meta_types__.keys(): 47 | raise Exception('{0} has an invalid meta type of {1}'.format(node, meta_type)) 48 | 49 | raise Exception('{0} is not of meta type {1}. It appears to be of type {2}'.format( 50 | node, 51 | self.meta_type, 52 | meta_type)) 53 | 54 | self.node = node 55 | self.uuid = get_object_uuid(node) 56 | self.attr_user_event = '{0}_attrChanged'.format(self.uuid) 57 | self.name_user_event = '{0}_nameChanged'.format(self.uuid) 58 | 59 | def __repr__(self): 60 | return '{0}.{1}({2!r})'.format(self.__class__.__module__, self.__class__.__name__, self.name) 61 | 62 | def __eq__(self, other): 63 | if hasattr(other, 'name'): 64 | return self.name == other.name 65 | return False 66 | 67 | def __ne__(self, other): 68 | return not self.__eq__(other) 69 | 70 | @classmethod 71 | def create(cls, name): 72 | """ 73 | Create a new Metanode. 74 | 75 | :param string name: The name for the created node. 76 | :return: Metanode class wrapping the newly created node. 77 | """ 78 | network_node = pm.createNode(NODE_TYPE) 79 | network_node.rename(name) 80 | 81 | for coreAttrName, coreAttrArgs in cls.attr_core().iteritems(): 82 | value = coreAttrArgs.pop('value') 83 | network_node.addAttr(coreAttrName, **coreAttrArgs) 84 | network_node.attr(coreAttrName).set(value) 85 | network_node.attr(coreAttrName).setLocked(True) 86 | 87 | for coreAttrName, coreAttrArgs in cls.attr_class().iteritems(): 88 | network_node.addAttr(coreAttrName, **coreAttrArgs) 89 | 90 | return cls(network_node) 91 | 92 | @classmethod 93 | def attr_core(cls): 94 | """:return OrderedDict: The core attributes all Metanodes have.""" 95 | return OrderedDict([ 96 | (META_TYPE, {'dt': 'string', 'k': False, 'value': cls.meta_type}), 97 | (META_VERSION, {'at': 'short', 'value': cls.meta_version}), 98 | (LINEAL_VERSION, {'at': 'short', 'value': cls.calculate_lineal_version()})]) 99 | 100 | @classmethod 101 | def attr_class(cls): 102 | """ 103 | Set of attributes this Metaclass adds to its network node. 104 | 105 | :return dict: key is attribute name and value is attribute settings. 106 | """ 107 | return {} 108 | 109 | def attr_dynamic(self): 110 | """ 111 | Set of attributes that need to be serialized but were not available during creation. 112 | 113 | :return dict: key is attribute name and value is attribute settings. 114 | """ 115 | return {} 116 | 117 | @classmethod 118 | def calculate_lineal_version(cls): 119 | """:return int: Step through resolution order to find total meta_version""" 120 | lineage = inspect.getmro(cls) 121 | version = 0 122 | for inherited_class in lineage: 123 | try: 124 | version += inherited_class.meta_version 125 | except AttributeError: 126 | pass 127 | return version 128 | 129 | @classmethod 130 | def scene_metanodes(cls): 131 | """ 132 | :return list: Metanodes of cls type in open scene. 133 | """ 134 | metas = [node for node in pm.ls(type=NODE_TYPE) if node.hasAttr(META_TYPE)] 135 | class_type = [cls(node) for node in metas if node.attr(META_TYPE).get() == cls.meta_type] 136 | return class_type 137 | 138 | def is_orphaned(self): 139 | """ 140 | Override this in derived classes to define when a node is orphaned or stranded in 141 | the scene and is safe to be cleaned up 142 | """ 143 | return False 144 | 145 | def _get_attr_data(self, attr_name): 146 | """ 147 | Query the attribute dictionaries for an attribute's creation arguments. 148 | 149 | :return dict: dictionary of the attribute's creation kwargs. 150 | """ 151 | attr_data = self.attr_class().get(attr_name) 152 | if attr_data is None: 153 | attr_data = self.attr_dynamic().get(attr_name) 154 | if attr_data is None: 155 | raise AttributeError( 156 | "'{0}' is not a registered attribute on a Metanode of type {1}".format(attr_name, self.meta_type)) 157 | return attr_data 158 | 159 | def get(self, attr_name): 160 | """ 161 | Get the value of the given attribute. Attribute name must be registered in one of the attr dictionaries. 162 | Currently supports attributes of type message, string, bool, float, int, enum 163 | 164 | :param string attr_name: Name of attribute to get 165 | :return: List or single value representing attribute value 166 | """ 167 | result = None 168 | # Get attribute data 169 | attr_data = self._get_attr_data(attr_name) 170 | # If multi: (return list) 171 | if attr_data.get('multi', False): 172 | # Data Type: MESSAGE 173 | if attr_data.get('at') == 'message': 174 | # Get connections 175 | result = pm.listConnections(self.node.attr(attr_name), d=False, s=True) 176 | # Data Type: STRING/BOOL/FLOAT/INT/ENUM 177 | else: 178 | # Get value 179 | result = list(self.node.attr(attr_name).get()) 180 | # If not multi: (return single value) 181 | else: 182 | # Data Type: MESSAGE 183 | if attr_data.get('at') == 'message': 184 | # Get connections 185 | node = pm.listConnections(self.node.attr(attr_name), d=False, s=True) 186 | if node: 187 | result = node[0] 188 | # Data Type: STRING/BOOL/FLOAT/INT/ENUM 189 | else: 190 | # Get value 191 | result = self.node.attr(attr_name).get() 192 | 193 | if attr_data.get('dt') == 'string': 194 | # Empty strings come back as None instead of '' 195 | if result is None: 196 | result = '' 197 | elif attr_data.get('dt') == 'stringArray': 198 | # Empty stringArrays come back as an empty list 199 | if result is None: 200 | result = [] 201 | return result 202 | 203 | def set(self, attr_name, value): 204 | """ 205 | Set the value of the given attribute. Attribute name must be registered in one of the attr dictionaries. 206 | Currently supports attributes of type message, string, bool, float, int, enum 207 | 208 | :param attr_name: Name of attribute to edit 209 | :param value: List or single value representing value of attribute to set 210 | """ 211 | # Get attribute data 212 | attr_data = self._get_attr_data(attr_name) 213 | # If multi: (value should be list) 214 | if attr_data.get('multi', False): 215 | if not isinstance(value, (list, tuple)): 216 | raise ValueError( 217 | "'{0}' is a multi attribute and must be set with a list or tuple of data".format(attr_name)) 218 | # Data Type: MESSAGE 219 | if attr_data.get('at') == 'message': 220 | for attr_element in self.node.attr(attr_name): 221 | pm.removeMultiInstance(attr_element, b=True) 222 | # Value should be list of PyNodes, we connect node.message to slot 223 | for index, item in enumerate(value): 224 | pm.connectAttr(item.message, self.node.attr(attr_name)[index]) 225 | # Data Type: STRING/BOOL/FLOAT/INT/ENUM 226 | else: 227 | for attr_element in self.node.attr(attr_name): 228 | pm.removeMultiInstance(attr_element, b=True) 229 | for index, item in enumerate(value): 230 | self.node.attr(attr_name)[index].set(item) 231 | # If not multi: 232 | else: 233 | # Data Type: MESSAGE 234 | if attr_data.get('at') == 'message': 235 | # Value should be a PyNode, we connect node.message to slot 236 | if value is not None: 237 | pm.connectAttr(value.message, self.node.attr(attr_name)) 238 | # If value is None, disconnect the current value 239 | else: 240 | pm.disconnectAttr(self.node.attr(attr_name), inputs=True) 241 | # Data Type: STRING/BOOL/FLOAT/INT/ENUM 242 | else: 243 | self.node.attr(attr_name).set(value) 244 | 245 | def update(self, *args, **kwargs): 246 | """ 247 | Update a metanode to the most recent version. 248 | 249 | :return: New Metanode, Dict mapping attributes from the old node that could not be found on the new one, 250 | along with their values and connections 251 | """ 252 | missing_attributes = {} 253 | could_not_set = [] 254 | name = self.name 255 | new_metanode = None 256 | try: 257 | # Rename this node. 258 | self.node.rename('updating__{0}'.format(name)) 259 | # Create new node with old name. 260 | new_metanode = self.__class__.create(name, *args, **kwargs) 261 | # For each user defined attr: 262 | attr_list = self.node.listAttr(userDefined=True, multi=True) + [self.node.message] 263 | for attr in attr_list: 264 | if attr.type() != 'message': 265 | data = attr.get() 266 | else: 267 | data = None 268 | 269 | # Source connections are those incoming to the attr, destination are outgoing 270 | source = attr.listConnections(plugs=True, source=True, destination=False) 271 | destination = attr.listConnections(plugs=True, source=False, destination=True) 272 | connections = source, destination 273 | 274 | attr_name = attr.name(includeNode=False) 275 | # if attribute does not exist on new node 276 | if not new_metanode.node.hasAttr(attr_name): 277 | # add to missingAttributes 278 | missing_attributes[attr_name] = data, connections 279 | # Sometimes network nodes connected to other network nodes just disappear when those 280 | # nodes are deleted. Disconnect them first to avoid that 281 | for sAttr in source: 282 | pm.disconnectAttr(sAttr, attr) 283 | for dAttr in destination: 284 | pm.disconnectAttr(attr, dAttr) 285 | else: 286 | # Copy data values from old node to new. 287 | if data is not None: 288 | try: 289 | if new_metanode.node.attr(attr_name).isLocked(): 290 | if not attr_name == META_VERSION and not attr_name == LINEAL_VERSION: 291 | pm.setAttr(attr_name, lock=False) 292 | new_metanode.set(attr_name, data) 293 | pm.setAttr(attr_name, lock=True) 294 | else: 295 | new_metanode.set(attr_name, data) 296 | except RuntimeError: 297 | could_not_set.append((attr_name, data)) 298 | # reconnect connections from old node to new 299 | for sAttr in source: 300 | pm.disconnectAttr(sAttr, attr) 301 | pm.connectAttr(sAttr, new_metanode.node.attr(attr_name)) 302 | for dAttr in destination: 303 | pm.disconnectAttr(attr, dAttr) 304 | pm.connectAttr(new_metanode.node.attr(attr_name), dAttr) 305 | pm.delete(self.node) 306 | except Exception as exc: 307 | print exc 308 | print exc.message 309 | 310 | # If something went wrong part way through the update roll back to the original node state 311 | if new_metanode is not None: 312 | pm.delete(new_metanode.node) 313 | self.node.rename(name) 314 | raise 315 | 316 | return new_metanode, missing_attributes, could_not_set 317 | 318 | def serialize_attr(self, attr_name): 319 | """ 320 | Returns a serialized format for the given attribute 321 | This behavior can be customized for some or all attributes by inherited classes 322 | The default return dictionary looks like this: 323 | {'name': 'fooAttr', 324 | 'type': 'message', 325 | 'value': 'barNode'} 326 | 327 | :param string attr_name: The attribute to retrieve data for 328 | :return: dict of attribute information 329 | """ 330 | attr_data = self._get_attr_data(attr_name) 331 | data_type = attr_data.get('at') or attr_data.get('dt') 332 | value = self.get(attr_name) 333 | if data_type == 'message': 334 | if value is not None: 335 | # Message attr values are PyNodes, query name for serialization 336 | if attr_data.get('multi', False): 337 | value = [item.name() for item in value] 338 | else: 339 | value = value.name() 340 | 341 | return {'name': attr_name, 'type': data_type, 'value': value} 342 | 343 | def deserialize_attr(self, data): 344 | """ 345 | Sets an attribute using a given serialized dict of data (generated by serialize_attr) 346 | This behavior can be customized for some or all attributes by inherited classes 347 | 348 | :param data: The dict of data to be used to set attribute(s) 349 | """ 350 | if not data: 351 | return 352 | 353 | value = data.get('value') 354 | if data.get('type') == 'message': 355 | if value is not None: 356 | # Message attribute values should be PyNodes, but are serialized as the node name 357 | # If multi attribute, value will be a list 358 | if isinstance(value, (list, tuple)): 359 | new_value = [] 360 | for i, v in enumerate(value): 361 | try: 362 | new_value.append(pm.PyNode(v)) 363 | except pm.MayaNodeError: 364 | # Node does not exist, attribute cannot be set 365 | pm.warning( 366 | "Element {0} of multi attribute '{1}' cannot be set, node '{2}' does not exist".format( 367 | i, data.get('name'), v)) 368 | value = new_value 369 | else: 370 | try: 371 | value = pm.PyNode(value) 372 | except pm.MayaNodeError: 373 | # Node does not exist, attribute cannot be set 374 | pm.warning("Attribute '{0}' cannot be set, node '{1}' does not exist".format( 375 | data.get('name'), value)) 376 | return 377 | # This call will fail and raise AttributeError if attrName is not registered in __attr__ or __dynamic_attr__ 378 | self.set(data.get('name'), value) 379 | 380 | def serialize(self, json_format=True): 381 | """ 382 | Create a serialized representation of this node. 383 | 384 | :param bool json_format: formats serialized data as json 385 | :return: Serialized representation of this node 386 | """ 387 | result = {'name': self.name, 'meta_type': self.meta_type, 'version': (self.node_version, self.node_lineal)} 388 | 389 | attributes = [] 390 | for attrName in self.attr_class(): 391 | serialized = self.serialize_attr(attrName) 392 | if serialized: 393 | attributes.append(serialized) 394 | result['attr'] = attributes 395 | 396 | dynamic_attr = [] 397 | for attrName in self.attr_dynamic(): 398 | serialized = self.serialize_attr(attrName) 399 | if serialized: 400 | dynamic_attr.append(serialized) 401 | result['dynamic_attr'] = dynamic_attr 402 | 403 | return json.dumps(result) if json_format else result 404 | 405 | def created_event(self): 406 | ''' 407 | Create user events for attribute changes and node renames. 408 | ''' 409 | sel_list = om2.MSelectionList() 410 | sel_list.add(self.node.name()) 411 | m_obj = sel_list.getDependNode(0) 412 | if self.uuid not in self.events: 413 | # Attribute 414 | attribute_callback = om2.MNodeMessage.addAttributeChangedCallback(m_obj, self._attribute_changed) 415 | om2.MUserEventMessage.registerUserEvent(self.attr_user_event) 416 | # Name 417 | name_callback = om2.MNodeMessage.addNameChangedCallback(m_obj, self._name_changed) 418 | om2.MUserEventMessage.registerUserEvent(self.name_user_event) 419 | 420 | self.events[self.uuid] = {self.attr_user_event, self.name_user_event} 421 | self.callbacks[self.uuid] = {attribute_callback, name_callback} 422 | 423 | def deleted_event(self): 424 | ''' 425 | Delete all callbacks of associated node. 426 | ''' 427 | for callback in self.callbacks[self.uuid]: 428 | om2.MMessage.removeCallback(callback) 429 | self.callbacks.pop(self.uuid, None) 430 | for event in self.events[self.uuid]: 431 | om2.MUserEventMessage.deregisterUserEvent(event) 432 | self.events.pop(self.uuid, None) 433 | 434 | def _attribute_changed(self, attribute_message, plug_dst, plug_scr, *args): 435 | # Message attribute edits 436 | if attribute_message & om2.MNodeMessage.kOtherPlugSet: 437 | input_uuid = om2.MFnDependencyNode(plug_scr.node()).uuid() 438 | if plug_dst.isElement: 439 | plug_dst = plug_dst.array() 440 | if attribute_message & om2.MNodeMessage.kConnectionMade: 441 | om2.MUserEventMessage.postUserEvent(self.attr_user_event, 442 | (self.uuid, plug_dst.partialName(), input_uuid, True)) 443 | elif attribute_message & om2.MNodeMessage.kConnectionBroken: 444 | om2.MUserEventMessage.postUserEvent(self.attr_user_event, 445 | (self.uuid, plug_dst.partialName(), input_uuid, False)) 446 | # Data attribute edits 447 | elif attribute_message & om2.MNodeMessage.kAttributeSet: 448 | dst_attribute = plug_dst.attribute() 449 | dst_type = dst_attribute.apiType() 450 | if plug_dst.isElement: 451 | attr_name = plug_dst.array().partialName() 452 | index = plug_dst.logicalIndex() 453 | else: 454 | attr_name = plug_dst.partialName() 455 | index = None 456 | if dst_type == om2.MFn.kTypedAttribute: 457 | attr_type = om2.MFnTypedAttribute(dst_attribute).attrType() 458 | if attr_type == om2.MFnData.kString: 459 | om2.MUserEventMessage.postUserEvent(self.attr_user_event, 460 | (self.uuid, attr_name, plug_dst.asString(), index)) 461 | elif attr_type == om2.MFnData.kStringArray: 462 | data_object = plug_dst.asMDataHandle().data() 463 | string_array = om2.MFnStringArrayData(data_object) 464 | om2.MUserEventMessage.postUserEvent(self.attr_user_event, 465 | (self.uuid, attr_name, string_array.array(), index)) 466 | elif dst_type == om2.MFn.kNumericAttribute: 467 | dstUnitType = om2.MFnNumericAttribute(dst_attribute).numericType() 468 | if dstUnitType == om2.MFnNumericData.kBoolean: 469 | om2.MUserEventMessage.postUserEvent(self.attr_user_event, 470 | (self.uuid, attr_name, plug_dst.asBool(), index)) 471 | elif dstUnitType in {om2.MFnNumericData.kFloat, om2.MFnNumericData.kDouble, om2.MFnNumericData.kAddr}: 472 | om2.MUserEventMessage.postUserEvent(self.attr_user_event, 473 | (self.uuid, attr_name, plug_dst.asDouble(), index)) 474 | elif dstUnitType in {om2.MFnNumericData.kShort, om2.MFnNumericData.kInt, 475 | om2.MFnNumericData.kLong, om2.MFnNumericData.kByte}: 476 | om2.MUserEventMessage.postUserEvent(self.attr_user_event, 477 | (self.uuid, attr_name, plug_dst.asShort(), index)) 478 | 479 | def _name_changed(self, *args): 480 | om2.MUserEventMessage.postUserEvent(self.name_user_event, (self.uuid, args[1], self.name)) 481 | 482 | def subscribe_attr(self, func): 483 | ''' 484 | Subscribe function to attribute changes. The data will come back as a tuple when changes are made. 485 | Message attribute changes will be; 486 | (meta_uuid(unicode), attribute_name(unicode), connected_node_uuid(unicode), connectionState(bool)) 487 | while data attributes will be; 488 | (meta_uuid(unicode), attribute_name(unicode), new_value(attribute type), index(int or None)). 489 | 490 | :param func: function to call when attributes updated. 491 | :return long: Index reference to callback. 492 | ''' 493 | index = om2.MUserEventMessage.addUserEventCallback(self.attr_user_event, func) 494 | self.callbacks[self.uuid].add(index) 495 | return self.uuid, index 496 | 497 | def subscribe_name(self, func): 498 | ''' 499 | Subscribe function to node name changes. The data will be; 500 | (meta_uuid(unicode), old_name(unicode), new_name(unicode)) 501 | 502 | :param func: function to call when name changes 503 | :return long: index reference to callback that can be used with unsubscribe call. 504 | ''' 505 | index = om2.MUserEventMessage.addUserEventCallback(self.name_user_event, func) 506 | self.callbacks[self.uuid].add(index) 507 | return self.uuid, index 508 | 509 | @classmethod 510 | def unsubscribe(cls, callback_data): 511 | ''' 512 | Remove callback for subscribed event. 513 | 514 | :param tuple callback_data: UUID and Index identifier for callback to remove. 515 | ''' 516 | uuid, index = callback_data 517 | if uuid in cls.callbacks: 518 | cls.callbacks[uuid].remove(index) 519 | om2.MMessage.removeCallback(index) 520 | 521 | @property 522 | def name(self): 523 | """:return string: Name of the network node.""" 524 | return self.node.name() 525 | 526 | @property 527 | def node_version(self): 528 | """:return int: Node's version value.""" 529 | if self.node.hasAttr(META_VERSION): 530 | return self.node.attr(META_VERSION).get() 531 | return -1 532 | 533 | @property 534 | def node_lineal(self): 535 | """:return int: Node's linealVersion value.""" 536 | if self.node.hasAttr(LINEAL_VERSION): 537 | return self.node.attr(LINEAL_VERSION).get() 538 | return -1 539 | 540 | @classmethod 541 | def changelog(cls): 542 | """ 543 | Implement this in derived classes to return a description of changes to the 544 | Metanode when incrementing the version. 545 | 546 | :return: Dict mapping versions to change descriptions 547 | """ 548 | return {0: 'Creation of Metanode class.'} 549 | 550 | 551 | class SingletonMetanode(Metanode): 552 | """ 553 | The base class for singleton Metanodes. Classes inherit from this if they wish to be 554 | the only instance of a particular metanode in the scene. 555 | """ 556 | meta_version = 1 557 | 558 | @classmethod 559 | def instance(cls): 560 | """ 561 | Controls access to the metanode type by returning one common instance of the node 562 | """ 563 | nodes = cls.scene_metanodes() 564 | if nodes: 565 | if pm.objExists(cls.__name__): 566 | metanode = cls(pm.PyNode(cls.__name__)) 567 | else: 568 | metanode = nodes[0] 569 | else: 570 | metanode = cls.create(cls.__name__) 571 | return metanode 572 | 573 | @classmethod 574 | def create(cls, _): 575 | pm.warning("{0} is a subclass of Singleton. Call '.instance()' to get this class.".format(cls.__name__)) 576 | 577 | 578 | def get_metanode(node, *args, **kwargs): 579 | """ 580 | By passing a network node with a meta type attribute, a Metanode instance will 581 | be returned of the appropriate meta type. 582 | 583 | :param pm.PyNode() node: a Maya network node with a meta type attribute 584 | :return: A subclass of Metanode of the type set on the network node. 585 | """ 586 | if isinstance(node, basestring): 587 | node = pm.PyNode(node) 588 | if not pm.hasAttr(node, META_TYPE): 589 | raise Exception("{0} isn't a Metanode".format(node)) 590 | meta_type = node.attr(META_TYPE).get() 591 | if meta_type not in Register.__meta_types__.keys(): 592 | raise Exception("{0} has an invalid meta type of {1}".format(node, meta_type)) 593 | 594 | return Register.__meta_types__[meta_type](node, *args, **kwargs) 595 | 596 | 597 | def get_scene_metanodes(): 598 | """ 599 | Get Dictionary of registered meta types with a list of the associated metanodes that exist in the scene. 600 | 601 | :return: Dictionary with every registered meta type as key with scene metanodes in lists. 602 | """ 603 | class_dictionary = Register.__meta_types__ 604 | meta_dictionary = dict([(registerType, []) for registerType in class_dictionary]) 605 | for node in pm.ls(type=NODE_TYPE): 606 | if pm.hasAttr(node, META_TYPE): 607 | meta_type = node.attr(META_TYPE).get() 608 | if meta_type in meta_dictionary: 609 | metanode = class_dictionary[meta_type](node) 610 | meta_dictionary[meta_type].append(metanode) 611 | return meta_dictionary 612 | 613 | 614 | def deserialize_metanode(data, node=None, json_format=True, verify_version=True, *args, **kwargs): 615 | """ 616 | Deserialize the given serialized data into a Metanode. 617 | 618 | :param data: Serialized node data. 619 | :param PyNode node: The serialized data will be applied to the given network node. If None, 620 | a new node will be created. Note that if a node is given, its name and attribute values 621 | may be altered. 622 | :param bool json_format: If true the data will be loaded from JSON. 623 | :param bool verify_version: Check if data version matches current Metanode. 624 | :return: Deserialized Metanode 625 | """ 626 | if json_format: 627 | data = json.loads(data) 628 | 629 | meta_type = data['meta_type'] 630 | # Get the appropriate metanode class for the serialized data, and let that class handle the 631 | # deserialization process 632 | metanode_class = Register.__meta_types__.get(meta_type) 633 | 634 | if metanode_class is None: 635 | raise ValueError("Given serialized data specifies an unregistered meta type of {0}".format(meta_type)) 636 | 637 | # If for a singleton metanode, ignore the given network node and deserialize onto the singleton class instance 638 | if issubclass(metanode_class, SingletonMetanode): 639 | metanode = metanode_class.instance() 640 | # Regular metanodes need to either use the given node or create a new one 641 | else: 642 | node_name = data['name'] 643 | # If node is not none, do not create new node, just load data onto existing node 644 | # Note: For this to work for every Metanode class, each class' `create` function must 645 | # be able to be called with only `name` as an argument. 646 | if node is None: 647 | metanode = metanode_class.create(node_name, *args, **kwargs) 648 | else: 649 | # Ensure network node is of same meta types as metaclass, version up to date, etc 650 | node.rename(node_name) 651 | metanode = metanode_class(node) 652 | 653 | if verify_version: 654 | version, lineal_version = data['version'] 655 | if metanode.node_version != version or metanode.node_lineal != lineal_version: 656 | pm.warning("Serialized data's version is inconsistent with current version of metanode class {0}".format( 657 | metanode_class.__name__)) 658 | 659 | for attr_data in data['attr']: 660 | metanode.deserialize_attr(attr_data) 661 | 662 | if data['dynamic_attr']: 663 | for attr_name, attr_args in metanode.attr_dynamic.iteritems(): 664 | metanode.node.addAttr(attr_name, **attr_args) 665 | for dynamic_data in data['dynamic_attr']: 666 | metanode.deserialize_attr(dynamic_data) 667 | 668 | return metanode 669 | 670 | 671 | def get_object_uuid(node): 672 | """Get PyNode UUID value as string.""" 673 | sel_list = om2.MSelectionList() 674 | sel_list.add(node.name()) 675 | m_obj = sel_list.getDependNode(0) 676 | return om2.MFnDependencyNode(m_obj).uuid().asString() 677 | -------------------------------------------------------------------------------- /meta/examples/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['actor', 'rig', 'skeleton'] 2 | 3 | import actor 4 | import rig 5 | import skeleton 6 | -------------------------------------------------------------------------------- /meta/examples/actor.py: -------------------------------------------------------------------------------- 1 | import pymel.core as pm 2 | 3 | import meta.core 4 | import meta.examples.skeleton 5 | 6 | attr_skeleton = 'skeleton' 7 | attr_export_meshes = 'exportMeshes' 8 | attr_export_collision = 'exportCollision' 9 | attr_export_cloth = 'exportCloth' 10 | attr_active_actor = 'activeActor' 11 | 12 | 13 | class Actor(meta.core.Metanode): 14 | ''' 15 | A Metanode for saving skeletons. 16 | ''' 17 | meta_version = 1 18 | 19 | @classmethod 20 | def attr_class(cls): 21 | return {attr_skeleton: {'at': 'message'}, 22 | attr_export_meshes: {'at': 'message', 'multi': True}, 23 | attr_export_collision: {'at': 'message', 'multi': True}, 24 | attr_export_cloth: {'at': 'message', 'multi': True}} 25 | 26 | @property 27 | def skeleton(self): 28 | return self.get(attr_skeleton) 29 | 30 | @skeleton.setter 31 | def skeleton(self, skeleton): 32 | if skeleton.__class__ is meta.examples.skeleton.Skeleton: 33 | self.set(attr_skeleton, skeleton.node) 34 | 35 | 36 | class ActiveActor(meta.core.SingletonMetanode): 37 | ''' 38 | Metanode Singleton for tracking the currently active Actor Metanode. 39 | ''' 40 | meta_version = 1 41 | 42 | @classmethod 43 | def __metanodeattributes__(cls): 44 | return {attr_active_actor: {'at': 'message'}} 45 | 46 | @property 47 | def active_actor(self): 48 | return self.get(attr_active_actor) 49 | 50 | @active_actor.setter 51 | def active_actor(self, actor): 52 | self.set(actor.node, attr_active_actor) 53 | 54 | 55 | def get_active_actor(): 56 | ''' 57 | :return: Actor 58 | ''' 59 | return ActiveActor.instance().active_actor 60 | -------------------------------------------------------------------------------- /meta/examples/rig.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | import pymel.core as pm 4 | 5 | import meta.core 6 | 7 | attr_rig_components = 'rigComponents' 8 | attr_rig = 'rig' 9 | attr_rig_group = 'rigGroup' 10 | attr_built = 'isBuild' 11 | attr_socket = 'socket' 12 | attr_component_group = 'componentGroup' 13 | attr_controls = 'controls' 14 | attr_bind = 'bindJoints' 15 | attr_start_joint = 'startJoint' 16 | attr_end_joint = 'endJoint' 17 | 18 | 19 | class Rig(meta.core.Metanode): 20 | meta_version = 1 21 | 22 | @classmethod 23 | def attr_class(cls): 24 | return {attr_rig_components: {'at': 'message', 'multi': True}, 25 | attr_rig_group: {'at': 'message'}} 26 | 27 | def add_component(self, new_component): 28 | ''' 29 | Add a component to the rig component. 30 | 31 | :param new_component: a rig component that will be a part of the rig 32 | ''' 33 | components = self.components 34 | components.append(new_component) 35 | component_nodes = [component.node for component in components] 36 | component_nodes = list(set(component_nodes)) 37 | self.set(attr_rig_components, component_nodes) 38 | 39 | new_component.rig = self 40 | 41 | def build_rig(self): 42 | rig_group = pm.group(em=True, n=self.name + '_rig_group') 43 | lock_transforms(rig_group) 44 | hide_transforms(rig_group) 45 | self.set(attr_rig_group, rig_group) 46 | 47 | for component in self.components: 48 | component.build() 49 | 50 | def demolish_rig(self): 51 | for component in self.components: 52 | component.demolish() 53 | pm.delete(self.get(attr_rig_group)) 54 | 55 | @property 56 | def components(self): 57 | nodes = self.get(attr_rig_components) 58 | components = list() 59 | for node in nodes: 60 | components.append(meta.core.get_metanode(node)) 61 | return components 62 | 63 | 64 | class Component(meta.core.Metanode): 65 | meta_version = 1 66 | 67 | attr_common = { 68 | attr_rig: {'at': 'message'}, 69 | attr_built: {'at': 'bool', 'dv': False}, 70 | attr_socket: {'at': 'message'}, 71 | attr_component_group: {'at': 'message'}, 72 | attr_controls: {'at': 'message', 'multi': True}, 73 | attr_bind: {'at': 'message', 'multi': True}} 74 | 75 | attr_build = OrderedDict([]) 76 | 77 | @classmethod 78 | def attr_class(cls): 79 | attrs = OrderedDict([]) 80 | attrs.update(cls.attr_common) 81 | attrs.update(cls.attr_build) 82 | return attrs 83 | 84 | def build(self): 85 | ''' 86 | Call to build individual component which requires the component not be built. 87 | ''' 88 | if not self.built and self.valid(): 89 | if self.bind_joints: 90 | pm.cutKey(self.bind_joints) 91 | self._create_rig() 92 | self.built = True 93 | 94 | def _create_rig(self): 95 | ''' 96 | Component subclasses should implement this method. This handles the 97 | creation of the component in Maya which is highly specific to each component. 98 | 99 | The general outline of this method: 100 | -create group for component in Maya 101 | -build the controls and parts to get the specific rig component behaviour 102 | -lock and hide extraneous attributes from the channel box 103 | -connect built pieces to the network node for easy access through the metanode 104 | -call super to set the isBuilt attribute to True and generate animation control shapes 105 | ''' 106 | pass 107 | 108 | def demolish(self): 109 | ''' 110 | Tear down control. First stores the control shapes, then deletes the group 111 | holding the rig controls and unused control materials. Finally, sets the isBuilt 112 | flag to False 113 | ''' 114 | if self.built: 115 | pm.delete(self.component_group) 116 | self.built = False 117 | 118 | def reset_controls(self): 119 | ''' 120 | Zero out all controls, such that they are all reset to the position they 121 | were during rig creation, and all bind joints are in skeleton zeropose. 122 | ''' 123 | for control in self.controls: 124 | for attr in ('{0}{1}'.format(at, ax) for at in 'tr' for ax in 'xyz'): 125 | control.attr(attr).set(0) 126 | for attr in 'sx sy sz'.split(): 127 | control.attr(attr).set(1) 128 | 129 | def valid(self): 130 | return True 131 | 132 | @property 133 | def rig(self): 134 | return Rig(self.get(attr_rig)) 135 | 136 | @rig.setter 137 | def rig(self, rig_meta): 138 | self.set(attr_rig, rig_meta.node) 139 | 140 | @property 141 | def bind_joints(self): 142 | return self.get(attr_bind) 143 | 144 | @bind_joints.setter 145 | def bind_joints(self, joints): 146 | self.set(attr_bind, joints) 147 | 148 | @property 149 | def controls(self): 150 | return self.get(attr_bind) 151 | 152 | @controls.setter 153 | def controls(self, joints): 154 | self.set(attr_bind, joints) 155 | 156 | @property 157 | def component_group(self): 158 | return self.get(attr_component_group) 159 | 160 | @component_group.setter 161 | def component_group(self, joints): 162 | self.set(attr_component_group, joints) 163 | 164 | @property 165 | def built(self): 166 | return self.get(attr_built) 167 | 168 | @built.setter 169 | def built(self, state): 170 | self.set(attr_built, state) 171 | 172 | 173 | class FK(Component): 174 | meta_version = 1 175 | 176 | attr_build = OrderedDict([ 177 | (attr_start_joint, {'at': 'message'}), 178 | (attr_end_joint, {'at': 'message'})]) 179 | 180 | def _create_rig(self): 181 | start = self.get(attr_start_joint) 182 | end = self.get(attr_end_joint) 183 | socket = self.get(attr_socket) 184 | 185 | component_group = pm.group(em=True, n=self.name + '_component') 186 | if socket: 187 | copy_transforms(socket, component_group) 188 | pm.pointConstraint(socket, component_group, mo=False) 189 | pm.orientConstraint(socket, component_group, mo=False) 190 | pm.parent(component_group, self.rig.get(attr_rig_group)) 191 | lock_transforms(component_group) 192 | hide_transforms(component_group) 193 | 194 | if start == end: 195 | bind_joints = [start] 196 | else: 197 | if end not in start.listRelatives(c=True, ad=True): 198 | raise Exception('{0} not a descendant of {1}'.format(end.name(), start.name())) 199 | 200 | bind_joints = list() 201 | bind_joints.append(end) 202 | node = end 203 | while not node == start: 204 | node = node.getParent() 205 | if pm.objectType(node) == 'joint': 206 | bind_joints.append(node) 207 | bind_joints = bind_joints[::-1] 208 | 209 | controls = [] 210 | for joint in bind_joints: 211 | name = joint.name() 212 | name = name.split('|')[-1] 213 | name = name + '_FK_CTRL' 214 | dup_joint = pm.duplicate(joint, parentOnly=True)[0] 215 | dup_joint.rename(name) 216 | controls.append(dup_joint) 217 | pm.parent(controls, w=1) 218 | for ind, joint in enumerate(controls[1:]): 219 | pm.parent(joint, controls[ind]) 220 | 221 | zero_groups = [] 222 | for joints in zip(controls, bind_joints): 223 | pm.pointConstraint(*joints, mo=False, w=1.0) 224 | orient = pm.orientConstraint(*joints, mo=False, w=1.0) 225 | # Set orient constraint to Shortest interpType 226 | orient.interpType.set(2) 227 | 228 | for control in controls: 229 | existing_parent = control.listRelatives(p=True) 230 | zero_group = pm.group(em=True, n="{0}_Zero".format(control.name().replace('_CTRL', ''))) 231 | copy_transforms(control, zero_group) 232 | 233 | pm.parent(control, zero_group) 234 | if pm.objectType(control) == 'joint': 235 | control.rotate.set((0, 0, 0)) 236 | control.attr('jointOrientX').set(0) 237 | control.attr('jointOrientY').set(0) 238 | control.attr('jointOrientZ').set(0) 239 | 240 | for parentNode in existing_parent: 241 | pm.parent(zero_group, parentNode) 242 | zero_groups.append(zero_group) 243 | 244 | control_shape = pm.circle(c=[0, 0, 0], nr=[0, 1, 0], sw=360, r=1, d=3, ut=0, tol=0, s=8, ch=1)[0] 245 | copy_transforms(control, control_shape) 246 | control_shape.rotateBy((0, 0, 90)) 247 | pm.delete(control_shape, constructionHistory=True) 248 | shape = pm.listRelatives(control_shape, children=True, shapes=True) 249 | pm.parent(shape, control, shape=True, add=True) 250 | pm.delete(control_shape) 251 | 252 | pm.parent(zero_groups[0], component_group) 253 | 254 | for node in zero_groups: 255 | lock_transforms(node) 256 | hide_transforms(node) 257 | 258 | self.bind_joints = bind_joints 259 | self.controls = controls 260 | self.component_group = component_group 261 | 262 | def valid(self): 263 | if self.get(attr_start_joint) and self.get(attr_end_joint): 264 | return True 265 | else: 266 | return False 267 | 268 | 269 | def copy_transforms(source, target): 270 | ''' 271 | Copy position and rotation of source to target. 272 | 273 | :param source: Object to copy transformation 274 | :param target: Object to apply transformation 275 | ''' 276 | translation = pm.xform(source, t=True, ws=True, q=True) 277 | pm.xform(target, t=translation, ws=True) 278 | 279 | rotation = pm.xform(source, ro=True, ws=True, q=True) 280 | rot_order = target.getRotationOrder() 281 | pm.xform(target, ro=rotation, ws=True, roo=source.getRotationOrder()) 282 | target.setRotationOrder(rot_order, True) 283 | 284 | 285 | def lock_transforms(transform, translate='xyz', rotate='xyz', scale='xyz'): 286 | ''' 287 | Lock transforms attributes. 288 | 289 | :param pm.PyNode transform: Node to lock transform attributes 290 | :param string translate: a string of translate axes to lock 291 | :param string rotate: a string of rotate axes to lock 292 | :param string scale: a string of scale axes to lock 293 | :return: dictionary of axes that were locked 294 | ''' 295 | locked_attrs = {'translate': '', 'rotate': '', 'scale': ''} 296 | 297 | for axis in translate: 298 | if not transform.attr('t' + axis).isLocked(): 299 | transform.attr('t' + axis).setLocked(True) 300 | locked_attrs['translate'] = locked_attrs['translate'] + axis 301 | for axis in rotate: 302 | if not transform.attr('r' + axis).isLocked(): 303 | transform.attr('r' + axis).setLocked(True) 304 | locked_attrs['rotate'] = locked_attrs['rotate'] + axis 305 | for axis in scale: 306 | if not transform.attr('s' + axis).isLocked(): 307 | transform.attr('s' + axis).setLocked(True) 308 | locked_attrs['scale'] = locked_attrs['scale'] + axis 309 | 310 | return locked_attrs 311 | 312 | 313 | def hide_transforms(transform, translate='xyz', rotate='xyz', scale='xyz'): 314 | ''' 315 | Hide transforms attributes in channel box. 316 | 317 | :param pm.PyNode transform: Node to hide transform attributes 318 | :param string translate: a string of translate axes to hide 319 | :param string rotate: a string of rotate axes to hide 320 | :param string scale: a string of scale axes to hide 321 | :return: dictionary of axes that were hide 322 | ''' 323 | for axis in translate: 324 | transform.attr('t' + axis).setKeyable(False) 325 | transform.attr('t' + axis).showInChannelBox(False) 326 | for axis in rotate: 327 | transform.attr('r' + axis).setKeyable(False) 328 | transform.attr('r' + axis).showInChannelBox(False) 329 | for axis in scale: 330 | transform.attr('s' + axis).setKeyable(False) 331 | transform.attr('s' + axis).showInChannelBox(False) 332 | -------------------------------------------------------------------------------- /meta/examples/skeleton.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pymel.core as pm 4 | 5 | import meta.core 6 | 7 | attr_bind_pose = 'bindPose' 8 | attr_zero_pose = 'zeroPose' 9 | attr_root = 'root' 10 | attr_no_bind = 'noBindJoints' 11 | attr_no_export = 'noExportJoints' 12 | 13 | 14 | class Skeleton(meta.core.Metanode): 15 | ''' 16 | A Metanode for saving skeletons. 17 | ''' 18 | metaversion = 1 19 | 20 | @classmethod 21 | def attr_class(cls): 22 | return {attr_bind_pose: {'dt': 'string'}, 23 | attr_zero_pose: {'dt': 'string'}, 24 | attr_root: {'at': 'message'}, 25 | attr_no_bind: {'at': 'message', 'multi': True}, 26 | attr_no_export: {'at': 'message', 'multi': True}} 27 | 28 | def store_bind_pose(self): 29 | ''' 30 | Captures the current skeleton pose as the bind pose on the network node 31 | ''' 32 | pose = json.dumps(get_pose(self.root)) 33 | self.node.attr(attr_bind_pose).set(pose) 34 | 35 | def store_zero_pose(self): 36 | ''' 37 | Captures the current skeleton pose as the zero pose on the network node 38 | ''' 39 | pose = json.dumps(get_pose(self.root)) 40 | self.node.attr(attr_zero_pose).set(pose) 41 | 42 | def apply_bind_pose(self): 43 | ''' 44 | Sets skeleton to pose saved in bind pose attribute. 45 | ''' 46 | pose = json.loads(self.get(attr_bind_pose)) 47 | if pose: 48 | set_pose(pose) 49 | 50 | def apply_zero_pose(self): 51 | ''' 52 | Sets skeleton to pose saved in zero pose attribute. 53 | ''' 54 | pose = json.loads(self.get(attr_zero_pose)) 55 | if pose: 56 | set_pose(pose) 57 | 58 | def append_no_bind(self, joints): 59 | ''' 60 | Connects the helper joints to the network node. 61 | 62 | :param list joints: a list of helper joints to connect to the network node 63 | ''' 64 | self.set(set(self.get(attr_no_bind) + joints), attr_no_bind) 65 | 66 | def append_no_export(self, joints): 67 | ''' 68 | Connects the helper joints to the network node. 69 | 70 | :param list joints: a list of helper joints to connect to the network node 71 | ''' 72 | self.set(set(self.get(attr_no_export) + joints), attr_no_export) 73 | 74 | def remove_no_bind(self, joints): 75 | ''' 76 | Remove joints from the list of noBindJoints currently connected to the network node 77 | 78 | :param list joints: a list of helper joints to disconnect from the network node 79 | ''' 80 | current = self.get(attr_no_bind) 81 | for joint in joints: 82 | if joint in current: 83 | current.remove(joint) 84 | self.set(joints, attr_no_bind) 85 | 86 | def remove_no_export(self, joints): 87 | ''' 88 | Remove joints from the list of noExportJoints currently connected to the network node 89 | 90 | :param list joints: a list of noExportJoints to disconnect from the network node 91 | ''' 92 | current = self.get(attr_no_export) 93 | for joint in joints: 94 | if joint in current: 95 | current.remove(joint) 96 | self.set(joints, attr_no_export) 97 | 98 | @property 99 | def root(self): 100 | ''' 101 | :return: The root of the skeleton. 102 | ''' 103 | return self.get(attr_root) 104 | 105 | @root.setter 106 | def root(self, root_joint): 107 | ''' 108 | Connect the root attribute on the network node to the specified root joint 109 | 110 | :param PyNode root_joint: the joint to set as the root of the skeleton 111 | ''' 112 | self.set(attr_root, root_joint) 113 | 114 | 115 | def get_pose(root): 116 | data = [] 117 | stack = [root] 118 | while stack: 119 | jnt = stack.pop() 120 | translate = pm.xform(jnt, q=True, translation=True, ws=True) 121 | rotate = pm.xform(jnt, q=True, rotation=True, ws=True) 122 | data.append((jnt.name(), translate, rotate)) 123 | stack.extend(pm.listRelatives(jnt, type='joint')) 124 | return data 125 | 126 | 127 | def set_pose(data): 128 | for jntName, t, r in data: 129 | if pm.uniqueObjExists(jntName): 130 | jnt = pm.PyNode(jntName) 131 | elif pm.uniqueObjExists(jntName[1:]): 132 | jnt = pm.PyNode(jntName[1:]) 133 | else: 134 | print pm.warning('No joint found for {0}'.format(jntName)) 135 | continue 136 | pm.xform(jnt, translation=t, ws=True) 137 | pm.xform(jnt, rotation=r, ws=True) 138 | -------------------------------------------------------------------------------- /meta/manager.py: -------------------------------------------------------------------------------- 1 | """Manager class for storing and updating Metanodes.""" 2 | 3 | import maya.api.OpenMaya as om2 4 | import pymel.core as pm 5 | 6 | from meta.config import * 7 | import meta.core 8 | 9 | 10 | class MetanodeManager(object): 11 | """ 12 | Manager class for storing state, and managing updates with recognized Metanodes in a scene. 13 | """ 14 | meta_dict = dict() 15 | network_nodes, relink, singleton, orphaned, update, deprecated = [], [], [], [], [], [] 16 | created_m_objs, callbacks = [], set() 17 | create_event, destroy_event = 'metanode_created', 'metanode_destroyed' 18 | om2.MUserEventMessage.registerUserEvent(create_event) 19 | om2.MUserEventMessage.registerUserEvent(destroy_event) 20 | 21 | def __init__(self): 22 | self.update_network_nodes() 23 | 24 | @classmethod 25 | def node_created_callback(cls, m_obj, _): 26 | """ 27 | Catches all network nodes that are created. Defer evaluates network node with metanode_created_callback 28 | so that network nodes have time to be inited as metas. 29 | 30 | :param m_obj: MObject for created network node. 31 | :param _: Extra argument passed from create event. 32 | :return: 33 | """ 34 | cls.created_m_objs.append(m_obj) 35 | pm.evalDeferred('meta.manager.MetanodeManager._check_created_node()') 36 | 37 | @classmethod 38 | def _check_created_node(cls): 39 | """ 40 | Catches all network nodes that are meta types. If they aren't in the meta dictionary they will be added. 41 | Should only apply to copied meta nodes and imported meta nodes as they dont go through the normal meta node 42 | create function. Always runs deferred, therefore this will not reliably catch metas from a batch process. 43 | If this is needed look at using update_meta_dictionary from your batch (scene load/new will also run 44 | update_meta_dictionary). 45 | """ 46 | m_obj = cls.created_m_objs.pop(0) 47 | m_objs_uuid = om2.MFnDependencyNode(m_obj).uuid() 48 | if m_objs_uuid.valid(): 49 | uuid = m_objs_uuid.asString() 50 | nodes = pm.ls(uuid) 51 | if nodes: 52 | if pm.hasAttr(nodes[0], META_TYPE): 53 | if nodes[0].attr(META_TYPE).get() in meta.core.Register.__meta_types__.keys(): 54 | metanode_class = meta.core.Register.__meta_types__[nodes[0].attr(META_TYPE).get()] 55 | if all(metanode.uuid != uuid for metanode in cls.meta_dict.get(metanode_class.meta_type, [])): 56 | if metanode_class.meta_type not in cls.meta_dict: 57 | cls.meta_dict[metanode_class.meta_type] = [] 58 | new_meta = metanode_class(nodes[0]) 59 | cls.meta_dict[metanode_class.meta_type].append(new_meta) 60 | om2.MUserEventMessage.postUserEvent(cls.create_event, (metanode_class.meta_type, new_meta)) 61 | new_meta.created_event() 62 | 63 | @classmethod 64 | def node_deleted_callback(cls, m_obj, _): 65 | """ 66 | Delete events created for any Metanode that is deleted. 67 | 68 | :param m_obj: MObject for deleted network node. 69 | :param _: Extra argument passed from delete event. 70 | """ 71 | uuid = om2.MFnDependencyNode(m_obj).uuid().asString() 72 | for key, value in cls.meta_dict.iteritems(): 73 | for metanode in value: 74 | if metanode.uuid == uuid: 75 | value.remove(metanode) 76 | om2.MUserEventMessage.postUserEvent(cls.destroy_event, (metanode.meta_type, metanode)) 77 | metanode.deleted_event() 78 | break 79 | 80 | @classmethod 81 | def subscribe_create(cls, func): 82 | index = om2.MUserEventMessage.addUserEventCallback(cls.create_event, func) 83 | cls.callbacks.add(index) 84 | return index 85 | 86 | @classmethod 87 | def subscribe_destroy(cls, func): 88 | index = om2.MUserEventMessage.addUserEventCallback(cls.destroy_event, func) 89 | cls.callbacks.add(index) 90 | return index 91 | 92 | @classmethod 93 | def unsubscribe(cls, callback): 94 | cls.callbacks.remove(callback) 95 | om2.MMessage.removeCallback(callback) 96 | 97 | @classmethod 98 | def update_meta_dictionary(cls): 99 | """ 100 | Updates meta dictionary with any meta nodes found in scene, not yet in the dictionary. Runs when scene loads. 101 | Should only need to be run when normal metanodeCreatedCallback cant catch a new meta node. 102 | This can happen when differed events aren't processed such as in a batch file open with an import 103 | of meta nodes from another file. 104 | """ 105 | update_meta_dictionary = meta.core.get_scene_metanodes() 106 | for update_meta_type, updateMetaList in update_meta_dictionary.iteritems(): 107 | cls.meta_dict.setdefault(update_meta_type, []) 108 | for updateMeta in updateMetaList: 109 | if all(updateMeta.uuid != metanode.uuid for metanode in cls.meta_dict[update_meta_type]): 110 | cls.meta_dict[update_meta_type].append(updateMeta) 111 | updateMeta.created_event() 112 | 113 | @classmethod 114 | def update_network_nodes(cls): 115 | cls.network_nodes = [node for node in pm.ls(type=NODE_TYPE) if pm.hasAttr(node, META_TYPE)] 116 | 117 | @classmethod 118 | def get_invalid_nodes(cls): 119 | """ 120 | Check all lists for nodes to fix. 121 | """ 122 | return cls.relink + cls.singleton + cls.orphaned + cls.update + cls.deprecated 123 | 124 | def validate_metanodes(self): 125 | """ 126 | Query metaDictionary and networkNodes for nodes to fix. This only gathers the nodes without fixing them. 127 | """ 128 | self.get_relink() 129 | self.get_extra_singletons() 130 | self.get_orphaned() 131 | self.get_nodes_to_update() 132 | self.get_deprecated() 133 | 134 | def recursive_metanode_fix(self): 135 | """ 136 | Call fixMetanodes then validateMetanodes until all issues are caught. 137 | """ 138 | msg = '' 139 | while self.get_invalid_nodes(): 140 | msg += self.fix_metanodes() 141 | self.validate_metanodes() 142 | return msg 143 | 144 | def fix_metanodes(self): 145 | """ 146 | Execute all fix functions on gathered Metanodes. 147 | """ 148 | msg = '' 149 | if self.relink: 150 | msg += self.update_relink() 151 | if self.singleton: 152 | msg += self.delete_extra_singletons() 153 | if self.orphaned: 154 | msg += self.delete_orphaned() 155 | if self.update: 156 | msg += self.update_metanodes() 157 | if self.deprecated: 158 | msg += self.delete_deprecated_meta_types() 159 | return msg 160 | 161 | @classmethod 162 | def _delete_metas(cls, metanodes, message_base): 163 | """ 164 | Delete passed Metanodes and return message. 165 | 166 | :param list metanodes: list of meta classes 167 | :param string message_base: Message about why the Metanode was deleted 168 | :return string: Description of Metanodes deleted. 169 | """ 170 | message = '' 171 | for metanode in reversed(metanodes): 172 | metanodes.remove(metanode) 173 | cls.network_nodes.remove(metanode.node) 174 | message += '{0}: {1}\n'.format(message_base, metanode.name) 175 | pm.lockNode(metanode.node, lock=False) 176 | pm.disconnectAttr(metanode.node) 177 | pm.delete(metanode.node) 178 | return message 179 | 180 | @classmethod 181 | def _delete_nodes(cls, nodes, message_base): 182 | """ 183 | Delete passed nodes and return message. 184 | 185 | :param list nodes: list of network nodes 186 | :param string message_base: Message about why the node was deleted 187 | :return string: Description of nodes deleted. 188 | """ 189 | message = '' 190 | for node in reversed(nodes): 191 | cls.network_nodes.remove(node) 192 | message += '{0}: {1}\n'.format(message_base, node.name()) 193 | pm.lockNode(node, lock=False) 194 | pm.disconnectAttr(node) 195 | pm.delete(node) 196 | nodes.remove(node) 197 | return message 198 | 199 | # RELINK 200 | @classmethod 201 | def get_relink(cls): 202 | """ 203 | Check network nodes for meta types to relink. 204 | """ 205 | relink_dict = META_TO_RELINK 206 | for oldType, newType in relink_dict.iteritems(): 207 | cls.relink = [node for node in cls.network_nodes if node.attr(META_TYPE).get() == oldType] 208 | 209 | @classmethod 210 | def update_relink(cls): 211 | """ 212 | Iterate through cls.relink to relink meta types that have been moved or renamed. 213 | 214 | :return: String of relinked meta 215 | """ 216 | relink_message = '' 217 | relink_dict = META_TO_RELINK 218 | for item in list(cls.relink): 219 | relink_message += 'Relinked outdated Metanode: {0}\n'.format(item.name()) 220 | item.attr(META_TYPE).unlock() 221 | item.attr(META_TYPE).set(relink_dict[item.attr(META_TYPE).get()]) 222 | item.attr(META_TYPE).lock() 223 | cls.relink.remove(item) 224 | return relink_message 225 | 226 | # Extra SINGLETON 227 | @classmethod 228 | def get_extra_singletons(cls): 229 | """ 230 | Collect extra singleton nodes that are not the recognized instance. 231 | """ 232 | cls.singleton = [] 233 | class_dictionary = meta.core.Register.__meta_types__ 234 | for meta_type in cls.meta_dict: 235 | if issubclass(class_dictionary[meta_type], meta.core.SingletonMetanode): 236 | if len(cls.meta_dict[meta_type]) > 1: 237 | instance_meta = class_dictionary[meta_type].instance() 238 | for singleton in cls.meta_dict[meta_type]: 239 | if singleton.node != instance_meta.node: 240 | cls.singleton.append(singleton) 241 | 242 | @classmethod 243 | def delete_extra_singletons(cls): 244 | """ 245 | Delete any extra singleton nodes. 246 | 247 | :return string: message about what nodes were deleted 248 | """ 249 | return cls._delete_metas(cls.singleton, 'Deleted duplicate singleton Metanode') 250 | 251 | # ORPHANED 252 | @classmethod 253 | def get_orphaned(cls): 254 | """ 255 | Collect metanodes in the metaDictionary that are orphaned. 256 | """ 257 | cls.orphaned = [] 258 | for meta_type in cls.meta_dict: 259 | for metaNode in cls.meta_dict[meta_type]: 260 | if metaNode.is_orphaned(): 261 | cls.orphaned.append(metaNode) 262 | 263 | @classmethod 264 | def delete_orphaned(cls): 265 | """ 266 | Delete all nodes in the cls.orphaned list. 267 | 268 | :return string: message about what Metanodes were deleted 269 | """ 270 | return cls._delete_metas(cls.orphaned, 'Deleted orphaned Metanode') 271 | 272 | # UPDATE 273 | @classmethod 274 | def get_nodes_to_update(cls, force=False): 275 | """ 276 | Find meta nodes with meta types in META_TO_CHECK that should be updated. 277 | 278 | :param force: force all Metanodes to be added 279 | :return: list of meta nodes to update 280 | """ 281 | cls.update = [] 282 | for meta_type in META_TO_CHECK: 283 | if not len(cls.meta_dict.get(meta_type, [])): 284 | continue 285 | for metanode in cls.meta_dict[meta_type]: 286 | if metanode.node.attr(META_TYPE).get() != meta_type: 287 | cls.update.append(metanode) 288 | elif metanode.linealVersion() < metanode.calculateLinealVersion() or force: 289 | cls.update.append(metanode) 290 | 291 | @classmethod 292 | def update_metanodes(cls): 293 | """ 294 | Call .update() on all metanodes in cls.update and return update messages. 295 | """ 296 | update_message = '' 297 | for metanode in reversed(cls.update): 298 | cls.update.remove(metanode) 299 | if pm.objExists(metanode.node): 300 | cls.network_nodes.remove(metanode.node) 301 | new, missing, could_not_set = metanode.update() 302 | if new: 303 | cls.network_nodes.append(new.node) 304 | update_message += 'Updating Metanode: {0}\n'.format(new) 305 | if missing: 306 | update_message += 'New Metanode lacks previous attributes: {0}'.format(missing) 307 | if could_not_set: 308 | update_message += 'Could not set attributes: {0}'.format(could_not_set) 309 | 310 | return update_message 311 | 312 | # DEPRECATED 313 | @classmethod 314 | def get_deprecated(cls): 315 | """ 316 | Find meta nodes with meta types in META_TO_REMOVE that should be deleted and add them to cls.deprecated 317 | """ 318 | deprecated_types = META_TO_REMOVE 319 | cls.deprecated = [node for node in cls.network_nodes if node.attr(META_TYPE).get() in deprecated_types] 320 | 321 | @classmethod 322 | def delete_deprecated_meta_types(cls): 323 | """ 324 | Delete nodes in cls.deprecated and return a message of all deleted nodes. 325 | """ 326 | return cls._delete_nodes(cls.deprecated, 'Deleted deprecated Metanode') 327 | 328 | 329 | def metanode_refresh(): 330 | """Call on scene start to update the MetanodeManager and check for invalid Metanodes.""" 331 | manager = MetanodeManager() 332 | manager.update_meta_dictionary() 333 | manager.validate_metanodes() 334 | if manager.get_invalid_nodes(): 335 | manager.recursive_metanode_fix() 336 | 337 | 338 | metanode_refresh() 339 | 340 | # Catch imported nodes 341 | postImportCallback = om2.MSceneMessage.addCallback(om2.MSceneMessage.kAfterImport, metanode_refresh) 342 | 343 | # Callbacks fired when network nodes are created and deleted to catch metanodes 344 | networkNodeCreatedCallback = om2.MDGMessage.addNodeAddedCallback(MetanodeManager.node_created_callback, 'network') 345 | networkNodeDeletedCallback = om2.MDGMessage.addNodeRemovedCallback(MetanodeManager.node_deleted_callback, 'network') 346 | --------------------------------------------------------------------------------