Pages

Tuesday, March 11, 2014

Descriptors and pythonic Maya properties

Updated 4/11/2015: fixing the embedded code that was busted by Blogger.
I’m still working on the followup to Rescuing Maya GUI From Itself, but while I was at it this StackOverflow question made me realize that the same trick works for pyMel-style property access to things like position or rotation. If you’re a member of the anti-pyMel brigade you might find this a useful trick for things like pCube1.translation = (0,10,0). Personally I use pyMel most of the time, but this is a good supplement or alternative for haterz or for special circumstance where pymel is too heavy.

The goal is to be able to write something like

from xform import Xform
example = Xform('pCube1')
print example.translation
# [0,0,0]
example.rotation = (0,40, 0)

The process s about as simple as it can get thanks to the magic of descriptors. This example spotlights one advantage of descriptors over getter/setter property functions: by inheriting the two classes (BBoxProperty and WorldXformProperty) I can get 4 distinct behaviors (world and local, read-write and read-only) with very little code and no if-checks.

'''
xform.py

Exposes the xform class: a simple way to set maya position, rotation and similar properties with point notation.

(c) 2014 Steve Theodore.  Distributed under the MIT License (http://opensource.org/licenses/MIT)
TLDR: Use, change and share, please retain this copyright notice.
'''

import maya.cmds as cmds

class XformProperty( object ):
    CMD = cmds.xform
    '''
    Descriptor that allows for get-set access of transform properties
    '''
    def __init__( self, flag ):
        self.Flag = flag
        self._q_args = {'q':True, flag:True}
        self._e_args = {flag: 0}


    def __get__( self, obj, objtype ):
        return self.CMD( obj, **self._q_args )

    def __set__( self, obj, value ):
        self._e_args[self.Flag] = value
        self.CMD( obj, **self._e_args )


class WorldXformProperty( XformProperty ):
    '''
    Get-set property in world space
    '''
    def __init__( self, flag ):
        self.Flag = flag
        self._q_args = {'q':True, flag:True, 'ws':True}
        self._e_args = {flag: 0, 'ws':True}

class BBoxProperty ( XformProperty ):
    '''
    Read only property for bounding boxes
    '''
    def __set__( self, obj, value ):
        raise RuntimeError ( "bounding box is a read-only property!" )

class WorldBBoxProperty ( WorldXformProperty, BBoxProperty ):
    '''
    Read only property for bounding boxes
    '''
    pass


class Xform( object ):
    '''
    Thin wrapper providing point-notation access to transform attributes

       example = Xform('pCube1')
       # |pCube1
       example.translation 
       # [0,0,0]
       example.translation = [0,10,0]

    For most purposes Xforms are just Maya unicode object names.  Note this does
    NOT track name changes automatically. You can, however, use 'rename':
       example = Xform('pCube1')
       example.rename('fred')
       print example.Object
       # |fred

    '''

    def __init__( self, obj ):
        self.Object = cmds.ls( obj, l=True )[0]

    def __repr__( self ):
        return unicode( self.Object )  # so that the command will work on the string name of the object

    # property descriptors  These are descriptors so they live at the class level,
    # not inside __init__!

    translation = XformProperty( 'translation' )
    rotation = XformProperty( 'rotation' )
    scale = XformProperty( 'scale' )
    pivots = XformProperty( 'pivots' )

    world_translation = WorldXformProperty( 'translation' )
    world_rotation = WorldXformProperty( 'rotation' )
    world_pivots = WorldXformProperty( 'pivots' )
    # maya does not allow 'world scale' - it's dependent on the parent scale

    # always local
    scaleTranslation = XformProperty( 'scaleTranslation' )
    rotateTranslation = XformProperty( 'rotateTranslation' )

    boundingBox = BBoxProperty( 'boundingBox' )
    world_boundingBox = WorldBBoxProperty( 'boundingBox' )


    def rename( self, new_name ):
        self.Object = cmds.ls( cmds.rename( self.Object, new_name ), l=True )[0]

    @classmethod
    def ls( cls, *args, **kwargs ):
        '''
        Returns a list of Xforms, using the same arguments and flags as the default ls command
        '''
        try:
            nodes = cmds.ls( *cmds.ls( *args, **kwargs ), type='transform' )
            return map ( Xform, nodes )
        except:
            return []

You may note the absence of a __metaclass__. In this case, with only a single class, a meta would be an unnecessary complication. Meanwhile the code for MayaGUI itself is up on GitHub. Comments and/or contributions welcome!

No comments:

Post a Comment