Tips and Tricks for using pymxs

Gizmos in pymxs

Accessing the gizmo of a modifier (for those modifiers that have a gizmo) can be tricky. Consider this code:

myMod = rt.UVWMap()
rt.addModifier(myTeapot, myMod)
myMod.gizmo.position = rt.Point3(20,20,20)

Although this works in MAXScript, in Python it produces the error: AttributeError: 'pymxs.MXSWrapperBase' object has no attribute 'gizmo'.

The reason is that the Python script does not force a scene graph evaluation the way that MAXScript does, so the gizmo is not created on the node by the time the script tries to access it. These situations can be worked around by forcing a viewport redraw, as in the following modified script:

from pymxs import runtime as rt

myTeapot = rt.teapot()
rt.convertTo(myTeapot, rt.editable_poly)
myMod = rt.UVWMap()
rt.addModifier(myTeapot, myMod)
rt.redrawViews() # needed to make the gizmo available
myMod.gizmo.position = rt.Point3(20,20,20)
print myMod.gizmo.position

Non-callable MAXScript Constructs

Although pymxs supports conversion between many Python and MAXScript types, and most "non-Pythonic" constructs (such as passing arguments by reference), you may find some MAXScript commands are simply impossible to replicate in Python. For example, using the "as" coercion syntax. In MAXScript you can do something like this:

selectedFaces = getFaceSelection selection[1] -- returns BitArray value
myArray = selectedFaces as Array

In Python this is not possible. Instead, we can create a function in MAXScript that does this coercion internally and returns the result. Global functions will be visible to the MAXScript system for the lifetime of the 3ds Max instance. You can do this in a separate MAXScript script, or within your Python script using MaxPlus.Core.EvalMAXScript(), for example:

MaxPlus.Core.EvalMAXScript("fn b2a b = (return b as Array)")
rt.b2a(selectedFaces)

Getting The Scene Root Node

MAXScript (and therefore pymxs) does not have an equivalent of MaxPlus.Core.GetRootNode(), but the functionality can be easily implemented. Here is an example that gets the root node and traverses its children:

from pymxs import runtime as rt

def getRootNode():
    rootScene = rt.rootScene
    worldSubAnim = rootScene[rt.Name('world')]
    return worldSubAnim.object
    
def outputNode(n, indent = ''):
    print indent, n.Name
    for c in n.Children:
        outputNode(c, indent + '--')
        
if __name__ == '__main__':
    outputNode(getRootNode())

Registering Python Callbacks

Some MAXScript callback registration functions can take a Python function as the callback argument. These include registerTimeCallback(), registerRedrawViewsCallback(), and nodeEventCallback(). Others take a string rather than a function as an argument, and thus cannot take a Python function. These functions include DialogMonitorOPS.RegisterNofication() and callbacks.addScript().

One work-around is to define a variable in the MAXScript runtime using pymxs.runtime.Execute(), and then assign the Python function to the variable. Here is a simple example to illustrate the idea:

import pymxs
rt = pymxs.runtime

def myCallback():
    print "Callback fired!"

# connect to the MXS runtime
rt.Execute ('pyCallback = undefined')
rt.pyCallback = myCallback

# register ourselves as a callback
rt.callbacks.addScript(rt.Name('nodeCreated'), 'pyCallback()')