Custom Manipulator using MPxManipContainer in Python API 2.0 (Maya 2020)
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report
Hello,
I've been trying to implement a custom manipulator using the Python API for one of our internal tools, and I'm having some issues getting it to work as intended in the API 2.0 version. I started out creating it in API 1.0, since that's the version on which all the examples in the devkit seem to be made (for manipulators specifically), but switched to API 2.0 because I wanted to do custom drawing in Viewport 2.0.
Because the MPxManipContainer in API 1.0 only has the draw function, not the drawUI function, I can't seem to do any kind of custom drawing in Viewport 2.0 on the previous version, which is my main reason for switching to API 2.0. I also understand that I might get better results by using the C++ API instead of python, but if possible I want to stick to the python API for easy access to my other internal library functions, and because of my previous experience using python in general.
Before posting about my specific issue, I'll attach the information about my maya and OS versions below:
Autodesk Maya 2020.4
Microsoft Windows 10 Home, version 10.0.18363
To demonstrate my issues, I've created two manipulator plugin files, one made for API 1.0, and one for API 2.0, trying to setup the same behavior (basically a custom move manipulator, this is not my intended use, but the simplest way to reproduce the problems). Both of them seem to connect to the selected objects translate plug correctly, but only the API 1.0 version behaves correctly when interacted with.
API 1.0 manipulator (working as intended)
This first gif shows how the API 1.0 version behaves when bound to a cube. As you can see the position is updated when dragging, and the position is relative to the start position of the drag.
API 2.0 version (not working)
This second gif shows the API 2.0 version, not working the same. When dragging, the position is only updated after releasing the mouse, and the position seems to be relative to the original position of the manipulator when bound, and it's not updating after each drag.
The code is of course slightly different to account to the changes of the API versions, but overall the classes and function calls are the same, so I would definitely not expect such a big difference in behavior. Because there doesn't seem to be any manipulator examples for API 2.0 in the devkit, I can't really reference any existing code to figure out if there are additional functions that need to be called in that version.
I'm attaching the code of both plugins so anyone can give them a try locally, and hopefully the result is the same and not just an issue on my machine. Both plugins are also attached in a zip archive for convenience.
"""MAYA API 1.0 VERSION"""
import maya.OpenMaya as OpenMaya
import maya.OpenMayaUI as OpenMayaUI
import maya.OpenMayaMPx as OpenMayaMPx
import sys
MANIP_CMD_NAME = "customManipCtxCmd"
MANIP_NODE_NAME = "customManip"
MANIP_ID = OpenMaya.MTypeId(0x7fffe)
class CustomManip(OpenMayaMPx.MPxManipContainer):
def __init__(self):
super(CustomManip, self).__init__()
self.move_manip = None
def createChildren(self):
self.move_manip = self.addFreePointTriadManip("moveManip", "move")
def connectToDependNode(self, node):
node_fn = OpenMaya.MFnDependencyNode(node)
translate_plug = node_fn.findPlug("translate", False)
move_manip_fn = OpenMayaUI.MFnFreePointTriadManip(self.move_manip)
move_manip_fn.connectToPointPlug(translate_plug)
self.finishAddingManips()
super(CustomManip, self).connectToDependNode(node)
def custom_manip_creator():
return OpenMayaMPx.asMPxPtr(CustomManip())
def custom_manip_initialize():
OpenMayaMPx.MPxManipContainer.initialize()
class CustomManipContext(OpenMayaMPx.MPxSelectionContext):
def __init__(self):
super(CustomManipContext, self).__init__()
self.update_manipulators_callback_id = None
def toolOnSetup(self, event):
update_manipulators(self)
self.update_manipulators_callback_id = OpenMaya.MModelMessage.addCallback(
OpenMaya.MModelMessage.kActiveListModified, update_manipulators, self
)
def toolOffCleanup(self):
self.deleteManipulators()
try:
if self.update_manipulators_callback_id:
OpenMaya.MModelMessage.removeCallback(self.update_manipulators_callback_id)
except RuntimeError:
sys.stderr.write("{0}: Cleanup called before setup.\n".format(MANIP_CMD_NAME))
super(CustomManipContext, self).toolOffCleanup()
def update_manipulators(client_data):
"""
Slot called when active selection changes, to update manipulators
Args:
client_data (MPxSelectionContext):
"""
client_data.deleteManipulators()
selection_list = OpenMaya.MSelectionList()
OpenMaya.MGlobal.getActiveSelectionList(selection_list)
selection_iter = OpenMaya.MItSelectionList(selection_list, OpenMaya.MFn.kInvalid)
while not selection_iter.isDone():
depend_node = OpenMaya.MObject()
selection_iter.getDependNode(depend_node)
if depend_node.isNull() or not depend_node.hasFn(OpenMaya.MFn.kDependencyNode):
selection_iter.next()
continue
depend_node_fn = OpenMaya.MFnDependencyNode(depend_node)
translate_plug = depend_node_fn.findPlug("translate", False)
if translate_plug.isNull():
selection_iter.next()
continue
manipulator_object = OpenMaya.MObject()
manipulator = OpenMayaMPx.MPxManipContainer.newManipulator(MANIP_NODE_NAME, manipulator_object)
if manipulator is not None:
client_data.addManipulator(manipulator_object)
manipulator.connectToDependNode(depend_node)
selection_iter.next()
class CustomManipCtxCmd(OpenMayaMPx.MPxContextCommand):
def __init__(self):
super(CustomManipCtxCmd, self).__init__()
def makeObj(self):
return OpenMayaMPx.asMPxPtr(CustomManipContext())
def context_cmd_creator():
return OpenMayaMPx.asMPxPtr(CustomManipCtxCmd())
def initializePlugin(m_object):
m_plugin = OpenMayaMPx.MFnPlugin(m_object)
m_plugin.registerContextCommand(MANIP_CMD_NAME, context_cmd_creator)
m_plugin.registerNode(
MANIP_NODE_NAME,
MANIP_ID,
custom_manip_creator,
custom_manip_initialize,
OpenMayaMPx.MPxNode.kManipContainer
)
def uninitializePlugin(m_object):
m_plugin = OpenMayaMPx.MFnPlugin(m_object)
m_plugin.deregisterContextCommand(MANIP_CMD_NAME)
m_plugin.deregisterNode(MANIP_ID)"""MAYA API 2.0 VERSION"""
import maya.api.OpenMaya as OpenMaya
import maya.api.OpenMayaUI as OpenMayaUI
import sys
MANIP_CMD_NAME = "customManip2CtxCmd"
MANIP_NODE_NAME = "customManip2"
MANIP_ID = OpenMaya.MTypeId(0x7ffff)
# Define maya_useNewAPI to indicate using Maya Python API 2.0.
maya_useNewAPI = True
class CustomManip2(OpenMayaUI.MPxManipContainer):
def __init__(self):
super(CustomManip2, self).__init__()
self.move_manip = None
def createChildren(self):
self.move_manip = self.addFreePointTriadManip("moveManip", "move")
def connectToDependNode(self, node):
node_fn = OpenMaya.MFnDependencyNode(node)
translate_plug = node_fn.findPlug("translate", False)
move_manip_fn = OpenMayaUI.MFnFreePointTriadManip(self.move_manip)
move_manip_fn.connectToPointPlug(translate_plug)
self.finishAddingManips()
super(CustomManip2, self).connectToDependNode(node)
def custom_manip_creator():
return CustomManip2()
def custom_manip_initialize():
OpenMayaUI.MPxManipContainer.initialize()
class CustomManip2Context(OpenMayaUI.MPxSelectionContext):
def __init__(self):
super(CustomManip2Context, self).__init__()
self.update_manipulators_callback_id = None
def toolOnSetup(self, event):
update_manipulators(self)
self.update_manipulators_callback_id = OpenMaya.MEventMessage.addEventCallback(
"SelectionChanged", update_manipulators, self
)
def toolOffCleanup(self):
self.deleteManipulators()
try:
if self.update_manipulators_callback_id:
OpenMaya.MMessage.removeCallback(self.update_manipulators_callback_id)
except RuntimeError:
sys.stderr.write("{0}: Cleanup called before setup.\n".format(MANIP_CMD_NAME))
super(CustomManip2Context, self).toolOffCleanup()
def update_manipulators(client_data):
"""
Slot called when active selection changes, to update manipulators
Args:
client_data (MPxSelectionContext):
"""
client_data.deleteManipulators()
selection_list = OpenMaya.MGlobal.getActiveSelectionList(True) # type: OpenMaya.MSelectionList
for selection_index in range(selection_list.length()):
depend_node = selection_list.getDependNode(selection_index)
if depend_node.isNull() or not depend_node.hasFn(OpenMaya.MFn.kDependencyNode):
continue
depend_node_fn = OpenMaya.MFnDependencyNode(depend_node)
translate_plug = depend_node_fn.findPlug("translate", False)
if translate_plug.isNull:
continue
manipulator, manipulator_object = OpenMayaUI.MPxManipContainer.newManipulator(MANIP_NODE_NAME)
if manipulator is not None:
client_data.addManipulator(manipulator_object)
manipulator.connectToDependNode(depend_node)
class CustomManip2CtxCmd(OpenMayaUI.MPxContextCommand):
def __init__(self):
super(CustomManip2CtxCmd, self).__init__()
def makeObj(self):
return CustomManip2Context()
def context_cmd_creator():
return CustomManip2CtxCmd()
def initializePlugin(m_object):
m_plugin = OpenMaya.MFnPlugin(m_object)
m_plugin.registerContextCommand(MANIP_CMD_NAME, context_cmd_creator)
m_plugin.registerNode(
MANIP_NODE_NAME,
MANIP_ID,
custom_manip_creator,
custom_manip_initialize,
OpenMaya.MPxNode.kManipContainer
)
def uninitializePlugin(m_object):
m_plugin = OpenMaya.MFnPlugin(m_object)
m_plugin.deregisterContextCommand(MANIP_CMD_NAME)
m_plugin.deregisterNode(MANIP_ID)
Any help would be greatly appreciated, and don't hesitate to reach out for more information.
Kind regards,
-Albin