Custom Manipulator using MPxManipContainer in Python API 2.0 (Maya 2020)

Custom Manipulator using MPxManipContainer in Python API 2.0 (Maya 2020)

albin.nocode
Contributor Contributor
2,755 Views
15 Replies
Message 1 of 16

Custom Manipulator using MPxManipContainer in Python API 2.0 (Maya 2020)

albin.nocode
Contributor
Contributor

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)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)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

2,756 Views
15 Replies
Replies (15)
Message 2 of 16

albin.nocode
Contributor
Contributor

Forgot to add, to activate the proper context for each plugin, call the following code, and then just select an object with a translate plug:

 

import maya.cmds as cmds

context = cmds.customManipCtxCmd()
context2 = cmds.customManip2CtxCmd()

# API 1.0 context
cmds.setToolTo(context)

# API 2.0 context
cmds.setToolTo(context2)
0 Likes
Message 3 of 16

143110913
Enthusiast
Enthusiast

Hi! 

 

I just ran into the same issue, 

 

did you find a solution? 

0 Likes
Message 4 of 16

albin.nocode
Contributor
Contributor

@143110913, unfortunately I never did find a solution, and I have since abandoned my plans to create a manipulator for that specific feature, instead relying on a pyside UI to achieve a similar behavior.

I'm sorry I can't offer any better workarounds at this time!

0 Likes
Message 5 of 16

143110913
Enthusiast
Enthusiast

Hi @albin.nocode,

 

If it helps with anything, the error is not present on the C++ api. I just did a test with the moveManip example on the sdk and had no issues.

 

Seems the issue is only with the Maya Python api 2.0

0 Likes
Message 6 of 16

albin.nocode
Contributor
Contributor
Thanks @143110913, it's good to know there's a workaround in case someone else needs it!
0 Likes
Message 7 of 16

gaia_dilorenzo01
Community Visitor
Community Visitor

Did anyone figure it out, the problem seems to be still present

Message 8 of 16

albin.nocode
Contributor
Contributor

@gaia_dilorenzo01, sorry to say I have not been able to resolve this issue on my end, and have not heard from anyone at autodesk either. I don't know if there's anyway to escalate the issue either beyond writing more in this post, so feels difficult to know where to turn.

Sorry I can't be of more help!

Message 9 of 16

FirespriteNate
Advocate
Advocate

This is still broken in Maya 2024 😫

0 Likes
Message 10 of 16

brentmc
Autodesk
Autodesk

Hi,

There is a swiss army manip plugin example in Maya that has both API 1.0 and 2.0 versions which might be helpful?

https://help.autodesk.com/view/MAYAUL/2024/ENU/?guid=MAYA_API_REF_py_ref_python_2api2_2py2_swiss_arm...

Brent McPherson
Principle Engineer
0 Likes
Message 11 of 16

FirespriteNate
Advocate
Advocate

Have you tried running the API2.0 SwissArmyKnife Manipulator example code?

It's completely broken, and exhibits exactly the same dreadful behaviour/performance as albin.nocode highlighted in his original post.

Half of the manips don't even let you click/interact with them properly using the LEFT mouse button, the ones that do won't update in real-time as you drag, and then when you release the mouse button, they jump to some completely random wrong position that was nowhere like where you dragged them to.

At least, that's what I get on my setup in Maya 2022. 

I tried to write a much simpler version using only two manips. I wrote it first using API1.0 and it worked fine. I then converted this same code to API2.0 and the interaction completely broke, same as above.

Something seems to be horribly broken with MPxManipulatorContainer in API2.0.

0 Likes
Message 12 of 16

brentmc
Autodesk
Autodesk

No, I haven't tried running that example but I will forward this info to the relevant team.

Thanks.

Brent McPherson
Principle Engineer
Message 13 of 16

albin.nocode
Contributor
Contributor

@brentmc Thank you for looking into it and getting the issue to the relevant team! Looking forward to an update later.

Message 14 of 16

brentmc
Autodesk
Autodesk

Hi,

 

Issue has been logged but there is no update and the workaround is to use API 1.0.

MAYA-130680: Python 2.0 API Bug: py2SwissArmyManip is broken

Brent McPherson
Principle Engineer
Message 15 of 16

albin.nocode
Contributor
Contributor
Thanks, that's perfectly fine, just happy the issue is tracked as a ticket now 🙂
0 Likes
Message 16 of 16

golubevcg
Explorer
Explorer

Hello everyone!

It seems like I found the same issue but in Python API 1.0.

For example, moveManip.cpp and moveManip.py. So, if using the Python version of this example, if you override the doDrag method (you can do nothing there; just override it and call the same function from the parent class), it will behave the same way described here - the same bug. Drag does not work interactively, and the manipulator position is updated after you release the mouse. An interesting thing is that this issue appears only in Python. I compiled the same plugin within C++, and over there, it works correctly. I hope it will help 🙂