Subclassing PyMEL: Maya Transform

Categories: Maya, PyMEL, Python
Tags: No Tags
Comments: 20 Comments
Published on: March 21, 2013

Maya Controller

This is how you subclass PyMEL’s Transform Node to create custom shapes and node types that inherit all the methods of Maya’s Transformational Objects.

Subclassing PyNodes is one of the (so-called ‘experimental’) hidden gems of PyMEL. This feature allows you to take advantage of all of the factory generated methods on each Maya node type. You can then add any of your own methods to your custom class and have it become a super-node!

I’ve covered this briefly in the PyMEL Virtual Classes post, which if you haven’t read, please do, as I’ll be skipping all the basic methods illustrated there and focusing on the new implementation of the _createVirtual() method, which adds quite a bit of complexity.

All the nitty gritty details are documented and outlined in the customClasses.py example in the pymel package. There’s a lot of reading there and you have to interpret it a bit to actually make your own “creation” method which I’ve always wanted to do by simply inheriting the nodetypes.Transform class, which seemed like an easy first target.

So here’s our boilerplate declaration, NODETYPE, and list() method:

import maya.cmds
import pymel.core
import pymel.internal.factories

class Controller( pymel.core.nodetypes.Transform ):
    """
    Controller PyMEL class inheriting from Transform.
    Complete documentation on this process:
    https://github.com/LumaPictures/pymel/blob/master/examples/customClasses.py

    *Keyword Arguments:*
        * ``name/n`` name of object
        * ``shape_type/st`` circle, square, box, volumeBox
        * ``scale`` radius of shape
        * ``color`` white, black, and others

    *Examples:* ::

       Controller(n='test', color = 'black', shape_type = 'circle' )
    """

    NODE_TYPE = "pcsController"

    @classmethod
    def list( cls, *args, **kwargs ):
        """
        Returns all instances the node in the scene

        *Returns:*
            * ``nodes`` list of nodes in scene of type Controller

        *Examples:* ::

           Controller.list()

        """
        kwargs['type'] = cls.__melnode__
        return [ node for node in pymel.core.ls( *args, **kwargs ) if isinstance( node, cls )]

    @classmethod
    def _isVirtual( cls, obj, name ):
        """
        Tests to see if node is of NODE_TYPE
        PyMEL code should not be used inside the callback, only API and maya.cmds.

        *Returns:*
            * ``True/False`` if of NODE_TYPE

        """
        fn = pymel.core.api.MFnDependencyNode( obj )
        try:
            if fn.hasAttribute( 'node_type' ):
                plug = fn.findPlug( 'node_type' )
                if plug.asString() == cls.NODE_TYPE:
                    return True
                return False
        except:
            pass
        return False

All of this was covered in my previous post so let’s hop straight into the intricacies of the _preCreateVirtual() method:

    @classmethod
    def _preCreateVirtual( cls, shape_type = 'circle', scale = 0.5, color = 'white', **kwargs ):
        """ This class method is called prior to node creation and gives you a
            chance to modify the kwargs dictionary that is passed to the creation
            command.  If it returns two dictionaries, the second is used passed
            as the kwargs to the postCreate method

            this method must be a classmethod or staticmethod

        *Arguments:*
            * ``cls`` classmethod

        *Keyword Arguments:*
            * ``shape_type`` circle, square, box, volumeBox
            * ``scale`` radius of shape
            * ``color`` white, black, and others

        *Returns:*
            * ``kwargs`` all kwargs passed and default to _createVirtual
            * ``postKwargs`` scale & color kwarg passed to _postCreateVirtual

        *Examples:* ::

            Controller(n='test', color = 'black', shape_type = 'circle' )

        """
        # adding needed args to kwargs to pass to _createVirtual method

        # get name
        if 'n' in kwargs:
            name = kwargs.pop( 'n' )
        elif 'name' in kwargs:
            name = kwargs.get( 'name' )
        elif 'n' not in kwargs:
            # if no name is passed, then use the joint Id as the name.
            name = cls.NODE_TYPE

        kwargs['name'] = name

        # get shape_type
        if 'st' in kwargs:
            shape_type = kwargs.pop( 'st' )
        elif 'shape_type' in kwargs:
            shape_type = kwargs.get( 'shape_type' )

        kwargs['shape_type'] = shape_type

        # adding needed args to postKwargs to pass to _postCreateVirtual
        postKwargs = {}
        postKwargs['shape_type'] = shape_type
        postKwargs['scale'] = scale
        postKwargs['color'] = color

        # returns kwargs to _createVirtual, postKwargs to _postCreateVirtual
        return kwargs, postKwargs

Now this is where things start to get juicy. _preCreateVirtual is basically your __init__ method. This is where you set your default key word arguments (aka: kwargs). We do some clever short and long name searching of the kwargs to allow for convenient argument passing into this class just as many Maya scripters are used to.

Most importantly, however, we’re explicitly defining two dictionaries, kwargs, and postKwargs because _preCreateVirtual is where you figure out which arguments need to go to the “creation” method (_createVirtual) and which need to go to the “finalizing” method (_postCreateVirtual). Whatever this function returns as ‘kwargs’ will go to _createVirtual() and whatever this function returns as ‘postKwargs’ will go to _postCreateVirtual(). Makes sense, so far?

Here’s a sample line of code to instantiate the class that _preCreateVirtual() is going to parse:

controller = Controller(n='handle', shape_type = 'box', color = 'black')

Now the new stuff, the good stuff, the heart of the matter:

    @classmethod
    def _createVirtual( cls, **kwargs ):
        """ The create method can be used to override the 'default' node creation
            command;  it is given the kwargs given on node creation (possibly
            altered by the preCreate), and must return the string name of the
            created node. (...or any another type of object (such as an MObject),
            as long as the postCreate and class.__init__ support it.)

        *Arguments:*
            * ``cls`` classmethod

        *Keyword Arguments:*
            * ``kwargs`` from _preCreateVirtual

        *Returns:*
            * ``string`` name of node created
        """

        # turn kwargs into variables
        name = kwargs.get( 'name' )
        shape_type = kwargs.get( 'shape_type' )

        if shape_type == 'circle':
            # circle returns list of transform and makeNurbCircle, just grab transform
            return maya.cmds.circle( nr = [0, 1, 0], n = name )[0] #@UndefinedVariable

        elif shape_type == 'square':
            # curve returns string name
            return maya.cmds.curve( n = name, d = 1, p = [( 0.5, 0, -0.5 ), #@UndefinedVariable
                                                                 ( 0.5, 0, 0.5 ),
                                                                 ( -0.5, 0, 0.5 ),
                                                                 ( -0.5, 0, -0.5 ),
                                                                 ( 0.5, 0, -0.5 )],
                                                                 k = [0, 1, 2, 3, 4] )

        elif shape_type == 'box':
            # curve returns string name
            return maya.cmds.curve( n = name, d = 1, p = [( 0.5, 0.5, 0.5 ), ( 0.5, 0.5, -0.5 ), ( -0.5, 0.5, -0.5 ), #@UndefinedVariable
                                                                ( -0.5, 0.5, 0.5 ), ( 0.5, 0.5, 0.5 ), ( 0.5, -0.5, 0.5 ),
                                                                ( -0.5, -0.5, 0.5 ), ( -0.5, 0.5, 0.5 ), ( 0.5, 0.5, 0.5 ),
                                                                ( 0.5, 0.5, -0.5 ), ( 0.5, -0.5, -0.5 ), ( 0.5, -0.5, 0.5 ),
                                                                ( -0.5, -0.5, 0.5 ), ( -0.5, 0.5, 0.5 ), ( -0.5, 0.5, -0.5 ),
                                                                ( -0.5, -0.5, -0.5 ), ( -0.5, -0.5, 0.5 ), ( 0.5, -0.5, 0.5 ),
                                                                ( 0.5, -0.5, -0.5 ), ( -0.5, -0.5, -0.5 )] )

        elif shape_type == 'volumeBox':
            # sphere returns list of transform and makeNurbSphere, just grab transform
            return maya.cmds.sphere( n = name, s = 4, nsp = 3, d = 1 )[0] #@UndefinedVariable

_createVirtual is what you probably have never seen before so let’s dig in line be line. First we extract our variables from kwargs one at a time on lines 20 & 21. Then we simply use maya.cmds to create our shapes. The important thing is to return the strings of the objects we made which is the default return type of the maya.cmds for these particular shapes so this turned out pretty straightforward. Of course you can add as many different shape types as you want in here. Even draw your own curves and combine’em together however you wish.

Don’t get too crazy in here. I think you have to stick to maya.cmds and I suspect you cannot use PyMEL in here because that just sounds too meta. Save your extra work for the next method.

Now for the last of the internal virtual business, _post:

    @classmethod
    def _postCreateVirtual( cls, new_node, **kwargs ):
        """ This is called after creation, pymel/cmds allowed.
            This method is called after creating the new node, and gives you a
            chance to modify it.  The method is passed the PyNode of the newly
            created node, and the second dictionary returned by the preCreate, if
            it returned two items. You can use PyMEL code here, but you should
            avoid creating any new nodes.

            this method must be a classmethod or staticmethod

        *Arguments:*
            * ``cls`` classmethod
            * ``new_node`` string of new node created

        *Keyword Arguments:*
            * ``kwargs`` postKwargs received from _preCreateVirtual

        """

        # new_node is string from _createVirtual,  have to re-cast
        new_node = pymel.core.PyNode( new_node )

        new_node.addAttr( 'node_type', dt = 'string' )
        new_node.node_type.set( cls.NODE_TYPE )

        # freeze transform
        pymel.core.makeIdentity( new_node, a = True, t = True, r = True, s = True )

        # turn kwargs into variables
        shape_type = kwargs.get( 'shape_type' )
        scale = kwargs.get( 'scale' )
        color = kwargs.get( 'color' )

        # set scale
        new_node.scale.set( scale, scale, scale )

        # set color
        new_node.overrideEnabled.set( True )
        if color == 'white':
            new_node.overrideColor.set( 16 )
        elif color == 'black':
            new_node.overrideColor.set( 1 )

        # adjust cvs for volumeBox
        if shape_type == 'volumeBox':
            pymel.core.move( 0.5, '{0}.cv[1:2][1]'.format( new_node ), ws = True, x = True )
            pymel.core.move( 0.5, '{0}.cv[1:2][1]'.format( new_node ), ws = True, z = True )

            pymel.core.move( -0.5, '{0}.cv[1:2][2]'.format( new_node ), ws = True, x = True )
            pymel.core.move( 0.5, '{0}.cv[1:2][2]'.format( new_node ), ws = True, z = True )

            pymel.core.move( 0.5, '{0}.cv[1][0]'.format( new_node ), '{0}.cv[1][4]'.format( new_node ), '{0}.cv[2][0]'.format( new_node ), '{0}.cv[2][4]'.format( new_node ), ws = True, x = True )
            pymel.core.move( -0.5, '{0}.cv[1][0]'.format( new_node ), '{0}.cv[1][4]'.format( new_node ), '{0}.cv[2][0]'.format( new_node ), '{0}.cv[2][4]'.format( new_node ), ws = True, z = True )

            pymel.core.move( -0.5, '{0}.cv[1:2][3]'.format( new_node ), ws = True, x = True )
            pymel.core.move( -0.5, '{0}.cv[1:2][3]'.format( new_node ), ws = True, z = True )

            pymel.core.move( 0.5, '{0}.cv[2:3][0:4]'.format( new_node ), ws = True, y = True )
            pymel.core.move( -0.5, '{0}.cv[0:1][0:4]'.format( new_node ), ws = True, y = True )

!Careful!, the docs in the customClasses say and demonstrate that this method gets passed the “PyNode of the newly created node”. However, we’ve overridden the _createVirtual method and returned a string in our case because silly maya.cmds only deals in strings (weak!). So note that we had to re-cast to a PyNode on line 22, first thing, if we wanted to have any of the PyNode goodness in this method. We then add our custom attr, freeze-xform that sucker, and extract our kwargs we worked so hard to pass to this method via the postKwargs dictionary in the _preCreateVirtual() method. With our extracted kwargs we can set our scale and color and finally tweak our box shape so it looks nice.

We are done with the heavy lifting, now we get to expand on our node by adding any other cool methods we want. Here’s a sample:

def set_color( self, color, shape_color = True ):
        """
        setter method that allows for setting the color on transform or shape node

        *Arguments:*
            * ``color`` name of color, 'white' or 'black'

        *Keyword Arguments:*
            * ``shape_color`` True to set color on shape node

        *Returns:*
            * ``True/False`` if successful

        *Examples:* ::

            my_controller = Controller(n='test', color = 'black', shape_type = 'circle' )
            my_controller.set_color( 'white', shape_color = True )
        """

        # test for shape
        if not self.getShape():
            return False

        if shape_color:
            # on the Shape
            self.getShape().overrideEnabled.set( True )
            if color == 'white':
                self.getShape().overrideColor.set( 16 )
            if color == 'black':
                self.getShape().overrideColor.set( 1 )
        else:
            # on the Transform
            try:
                self.overrideEnabled.set( True )
            except TypeError:
                print "No .overrideEnabled attr on {0}. Try setting shape_color = True.".foramt( type( self ) )
            if color == 'white':
                self.overrideColor.set( 16 )
            if color == 'black':
                self.overrideColor.set( 1 )

        return True

    # add custom helper methods to the Controller object here
#     def custom_method(self, attr):
#         pass

This is a handy setter method to change the color after it’s already been made. Here’s the sample line:

controller.set_color('black')

Just think, though. What kind of cool methods could you add to a Transform node? Granted it already has a shit-ton on there because that’s what PyMEL does best, attach any command you’d probably ever want to use on a Transform to it as a method but there must be others, right? Heck, you could start to animate it. Imagine:

controller.animate_me(performance ='random')

or maybe a custom duplication method, like:

controller.duplicate(mirror=True)

where it duplicates itself on the other side of the rig. The possibilities are endless! and they’re attached right there on the object itself instead of some random command somewhere. Hope you’re seeing the power of OOP, PyMEL, and sub-classing.

Finish up by registering which we’ve discussed before:

# register virtual classes
pymel.internal.factories.registerVirtualClass( Controller, nameRequired = False )

!Caution!: This works well on my Win7 machine but I found bugs attempting this in 2013×64 on OSX. It’s been submitted and may not be an issue for you, but if you get an error about an illegal attribute ‘__apicls__’, ping me and I’ll show you how to fix the bug in the PyMEL internal.factories. 😉

Now all you have to do is put all that together and see if you can get it to run in your Maya session. If so, then you’re good to go, well on your way to knowing more about Maya node creation than most and the world is your oyster! Now get to it. Start making some killer nodes and share the wealth.

Special thanks to Nicholas Silveira for shape drawing and ideas putting this node together.

Post to Twitter

20 Comments
  1. alexis says:

    Powerful stuff !

    Thanks for sharing 🙂

  2. Awesome man , really outstanding work! keep it up!

  3. Thanks for the detailed tutorial Jason. It was very helpful and I built a bunch of scripts using virtual classes. However in maya 2014 I’m getting a new error from factories.registerVirtualClass.
    invalid attribute name(s) __apicls__: special attributes are not allowed on virtual nodes

    Attempting to import your vclasssample results in this error as well.
    Do you know why this is and how to fix it?

    David

    • jason says:

      Yeah, PyMEL 1.0.5 has a bug in it for virtual classes (as does 1.0.4 (maya2013) on OSX side).

      You need to hack a line in the factories.py script. Find the declaration of the variable ‘validSpecialAttrs’ and add ‘__apicls__’ do the list.

      • Mitch says:

        Argh, this bug is annoying. It stops me from using this feature if it means that any scripts I publicly release will require the user to go in and hack a bit of the code.

        • jason says:

          This is why you want to have your own distribution of pymel-1.0.5 on the network pythonpath that all of your users are using. The only trick is to make sure you add the path to this pymel VERY early on in the boot process. Basically you have to add it in the Maya.env or userSetup.mel/.py

          • Mitch says:

            Thanks for the tutorials by the way, I never even knew pymel had this awesome feature until I saw it on your site.

      • looksoon says:

        I have done this as you said, but maya 2014 for win is hung, why?

  4. Tyler Hurd says:

    Looks amazing! Can’t wait to try this. Thanks for the walk throughs.

  5. Tom says:

    Great tutorial. It’s got me a long way towards getting a virtual class set up. I’m having some problems with using PyMel methods on my new virtual class though. Please see the below file:

    https://github.com/kotchin/th_autorig2/blob/master/skeleton.py

    line 100 calls the getTranslation() method of my virtual class extending pm.Joint. This results in the following error:

    TypeError: in method ‘new_MFnDependencyNode’, argument 1 of type ‘MObject &’ #

    the .getAttr() methods works ok however, as shown on line 99.

    Do I need to initialise my virtual class somehow to register the PyMel methods?

    Could you shed any light on this please?

    • jason says:

      You are initializing properly with the last line.

      I instantiated a RigJoint() no problem:


      myJoint = RigJoint()
      myJoint.orient()

      and orient method works.The split method:


      myJoint.split(1)

      unparents and then errors with:


      # Error: TypeError: file /builds/[...]/OpenMaya.py line 3414: in method 'new_MFnDependencyNode', argument 1 of type 'MObject &' #

      Its easiest to debug running it through a debugger hooked up to Maya. I’d recommend setting that up.

      • Tom says:

        Thanks for your swift reply.
        This seemed to be a bug in the PyMel version that ships with Maya 2014 OSX (and perhaps others)
        Cloning PyMel from GitHub solves this problem

        Thanks again

  6. Lee says:

    Hey Jason,
    Thanks for these posts. Couple of questions if possible?

    Any reason you use isinstance for _isVirtual method over comparing type? The latter allows inheriting custom classes without subclasses giving a positive.

    Any reason against overriding pymel’s own classes? ie inheriting animLayer and returning True on _isVirtual for extending functionality?

    Cheers!

  7. Lee says:

    whoops, my bad – I meant for the .list method.
    customClasses.py answers the replacement – but missing special attributes (__melcmd__, __melcmd_isinfo__, __melcmdname__) when attempting to replace with same class name.
    Just coming across the many inconsistencies animLayers has even within pymel, otherwise everything working as intended.
    Thanks again.

  8. David says:

    Thanks so much, is very useful!

  9. Lee says:

    Heya Jason. Unsure if you’ve found a usage, but when designing a ‘singleton’ dg pynode;

    precreate:
    list existing nodes, pass to both create and post
    createVirtual:
    create if no nodes passed, otherwise, pass first node name to post
    postCreate:
    continue postCreate if no nodes passed from preCreate

    Does this sound like the approach to take? And/or can you think of any drawbacks to this?
    I’m aware there are plenty of alternatives (attribute on time1, optionVar etc) for what I currently have planned, but this is more of an experiment than anything.
    It’s also working as expected atm.

    Thanks.

    • jason says:

      Lee,

      I’ve never made singletons before. But maybe these overridden methods when subclassing pymel is a good way to do it. If it’s working for ya, well done. What’s your use-case?

  10. Neil says:

    Hey Jason,
    This is awesome stuff, thanks for posting! (Apologies if this is already common knowledge!) One thing that I stumbled upon was that you don’t need to use the virtual node class to instantiate a new ‘super node.’ You can just add the class sting attribute to an object that already exists in the scene.(Which is what postCreateVirtual does anyway) For example:

    for jnt in pymel.selected():

    print ‘Before adding class attribute: {}’.format(type(jnt))
    jnt.addAttr(‘_class’, dt=’string’)
    jnt._class.set(‘_JointNode’)

    new = pymel.PyNode(jnt) # Re-Instanciate the PyNode
    print ‘After getting the new PyNode: {}’.format(type(new))
    Before adding class attribute:
    After getting the new PyNode:

    Cheers!

Leave a Reply

Your email address will not be published. Required fields are marked *

*

Welcome , today is Wednesday, May 31, 2023