Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implment MDagModifier bindings #22

Merged
merged 15 commits into from
Jun 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ tmp/*
MFn.Types.inl
devkit.tgz
devkitBase

# IDE stuff
.vscode
2 changes: 1 addition & 1 deletion build_linux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ echo "(2) Finished in $compile_duration ms"
echo "(2) ----------------------------"
echo "(3) Cleaning.."

python ./scripts/mfn.py clean
python ./scripts/mfn.py clear

t3=$(date +%s.%N)
clean_duration=$(echo "(($t3 - $t2) * 1000)/1" | bc)
Expand Down
2 changes: 1 addition & 1 deletion build_win32.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Write-Host "(3) Finished in $link_duration ms"
Write-Host "(3) ----------------------------"
Write-Host "(4) Cleaning.."

& python .\scripts\mfn.py clean
& python .\scripts\mfn.py clear

$t4 = $stopwatch.ElapsedMilliseconds
$clean_duration = $t4 - $t3
Expand Down
7 changes: 3 additions & 4 deletions docker_build_linux.ps1
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
param (
[string]$maya_version = "2020"
[string]$maya_version = "2022"
)

docker run -ti --rm `
-v ${env:DEVKIT_LOCATION}:/devkit `
-v ${PWD}:/workspace `
-e DEVKIT_LOCATION=/devkit `
cmdc ./build_linux.sh $maya_version
cmdc:${maya_version} `
./build_linux.sh $maya_version
8 changes: 8 additions & 0 deletions docker_test_linux.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
param (
[string]$maya_version = "2022"
)

docker run -ti --rm `
-v ${PWD}:/workspace `
cmdc:${maya_version} `
mayapy -m nose -xv --exe ./tests
5 changes: 4 additions & 1 deletion src/MDGModifier.inl
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#ifndef DGMODIFIER_INL
#define DGMODIFIER_INL
#define _doc_DGModifier_addAttribute \
"Adds an operation to the modifier to add a new dynamic attribute to\n"\
"the given dependency node.\n"\
Expand Down Expand Up @@ -704,4 +706,5 @@ DGModifier
CHECK_STATUS(status);
}, py::arg("plugin"),
py::arg("attribute"),
_doc_DGModifier_unlinkExtensionAttributeFromPlugin);
_doc_DGModifier_unlinkExtensionAttributeFromPlugin);
#endif DGMODIFIER_INL
147 changes: 147 additions & 0 deletions src/MDagModifier.inl
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#include "MDGModifier.inl"

py::class_<MDagModifier, MDGModifier>(m, "DagModifier")
.def(py::init<>())

.def("createNode", [](MDagModifier & self, std::string type, MObject parent = MObject::kNullObj) -> MObject {
if (!parent.isNull())
{
validate::has_fn(
parent, MFn::kDagNode,
"Cannot createNode - 'parent' must be a 'kDagNode' object , not a '^1s' object."
);
}

MString type_name(type.c_str());
MStatus status;
MObject result = self.createNode(type_name, parent, &status);

if (status == MS::kInvalidParameter)
{
MString error_msg("Cannot create dependency node '^1s' - use DGModifier instead.");
error_msg.format(error_msg, type_name);
throw pybind11::type_error(error_msg.asChar());
} else if (result.isNull()) {
MString error_msg("Cannot create unknown node type '^1s'.");
error_msg.format(error_msg, type_name);
throw pybind11::type_error(error_msg.asChar());
}

CHECK_STATUS(status)

return result;
},
R"pbdoc(Adds an operation to the modifier to create a DAG node of the specified type.
If a parent DAG node is provided the new node will be parented under it.
If no parent is provided and the new DAG node is a transform type then it will be parented under the world.
In both of these cases, the method returns the new DAG node.

If no parent is provided and the new DAG node is not a transform type
then a transform node will be created and the child parented under that.
The new transform will be parented under the world \
and it is the transform node which will be returned by the method, not the child.

None of the newly created nodes will be added to the DAG until the modifier's doIt() method is called.)pbdoc")

.def("createNode", [](MDagModifier & self, MTypeId typeId, MObject parent = MObject::kNullObj) -> MObject {
if (!parent.isNull())
{
validate::has_fn(
parent, MFn::kDagNode,
"Cannot createNode - 'parent' must be a 'kDagNode' object , not a '^1s' object."
);
}

MString type_id_str = MString() + typeId.id();

MStatus status;
MObject result = self.createNode(typeId, parent, &status);

if (status == MS::kInvalidParameter)
{
MString error_msg("Cannot create dependency node with type ID '^1s'' - use DGModifier instead.");
error_msg.format(error_msg, type_id_str);
throw pybind11::type_error(error_msg.asChar());
} else if (result.isNull()) {
MString error_msg("Cannot create unknown node with type ID '^1s'.");
error_msg.format(error_msg, type_id_str);
throw pybind11::type_error(error_msg.asChar());
}

CHECK_STATUS(status)

return result;
},
R"pbdoc(Adds an operation to the modifier to create a DAG node of the specified type.

If a parent DAG node is provided the new node will be parented under it.
If no parent is provided and the new DAG node is a transform type then it will be parented under the world.
In both of these cases the method returns the new DAG node.

If no parent is provided and the new DAG node is not a transform type
then a transform node will be created and the child parented under that.
The new transform will be parented under the world \
and it is the transform node which will be returned by the method, not the child.

None of the newly created nodes will be added to the DAG until the modifier's doIt() method is called.)pbdoc")

.def("reparentNode", [](MDagModifier & self, MObject node, MObject newParent = MObject::kNullObj) {
validate::is_not_null(node, "Cannot reparent a null object.");

if (!node.hasFn(MFn::kDagNode))
{
MString error_msg("Cannot parent '^1s' to '^2s' - must specify a 'kDagNode' object , not a '^3s' object.");
error_msg.format(
error_msg,
MFnDependencyNode(node).name(),
newParent.isNull() ? "the world" : MFnDependencyNode(newParent).name(),
node.apiTypeStr()
);
throw pybind11::type_error(error_msg.asChar());
}
if (!newParent.isNull())
{
if (!newParent.hasFn(MFn::kDagNode))
{
MString error_msg("Cannot parent '^1s' to '^2s' - must specify a 'kDagNode' object , not a '^3s' object.");
error_msg.format(
error_msg,
MFnDependencyNode(node).name(),
newParent.isNull() ? "the world" : MFnDependencyNode(newParent).name(),
newParent.apiTypeStr()
);
throw pybind11::type_error(error_msg.asChar());
}

MFnDagNode fn(newParent);

if (fn.isChildOf(node))
{
MString error_msg("Cannot parent '^1s' to one of its children, '^2s'.");
error_msg.format(
error_msg,
MFnDagNode(node).partialPathName(),
MFnDagNode(newParent).partialPathName()
);
throw std::invalid_argument(error_msg.asChar());
}
}

if (node == newParent)
{
MString error_msg("Cannot parent '^1s' to itself.");
error_msg.format(
error_msg,
MFnDagNode(node).partialPathName()
);
throw std::invalid_argument(error_msg.asChar());
}

MStatus status = self.reparentNode(node, newParent);

CHECK_STATUS(status)
},
R"pbdoc(Adds an operation to the modifier to reparent a DAG node under a specified parent.

If no parent is provided then the DAG node will be reparented under the world, so long as it is a transform type.
If it is not a transform type then the doIt() will raise a RuntimeError.)pbdoc");
6 changes: 4 additions & 2 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
// Types
#include <maya/MAngle.h>
#include <maya/MColor.h>
#include <maya/MDagModifier.h>
#include <maya/MDagPath.h>
#include <maya/MDataBlock.h>
#include <maya/MDataHandle.h>
#include <maya/MDGModifier.h>
#include <maya/MDistance.h>
#include <maya/MDagPath.h>
#include <maya/MDGModifier.h>
#include <maya/MEulerRotation.h>
#include <maya/MFn.h>
#include <maya/MIntArray.h>
Expand Down Expand Up @@ -68,6 +69,7 @@ PYBIND11_MODULE(cmdc, m) {
#include "ForwardDeclarations.inl"

#include "Math.inl"
#include "MDagModifier.inl"
#include "MDagPath.inl"
#include "MDGModifier.inl"
#include "MFn.inl"
Expand Down
File renamed without changes.
140 changes: 140 additions & 0 deletions tests/test_MDagModifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import cmdc
import nose

from nose.plugins.skip import SkipTest

from maya import cmds
from maya.api import OpenMaya

from . import assert_equals, as_obj, as_plug, new_scene

def test_createNode():
return

node = cmds.createNode('transform', name='root')
node_obj = as_obj(node)
null_obj = cmdc.Object()
type_id = cmdc.FnDependencyNode(node_obj).typeId()

for doc, (value, parent) in (
['a valid type name', ('transform', null_obj)],
['a valid type name and parent', ('transform', node_obj)],
['a valid typeId', (cmdc.TypeId(type_id), null_obj)],
['a valid typeId and parent', (cmdc.TypeId(type_id), node_obj)],
):
test_createNode.__doc__ = """Test MDagModifier::createNode if called with {}.""".format(doc)

yield _createNode_pass, value, parent

not_a_dag = as_obj('time1')
not_a_node = as_plug('persp.message').attribute()
type_id = cmdc.FnDependencyNode(as_obj('time1')).typeId()

for doc, (value, parent) in (
['an invalid type name', ('foobar', null_obj)],
['a non-DAG type name', ('network', null_obj)],
['an invalid typeId', (cmdc.TypeId(0xdeadbeef), null_obj)],
['an non-DAG typeId', (cmdc.TypeId(type_id), null_obj)],
['an invalid parent (not a DAG node)', ('transform', not_a_dag)],
['an invalid parent (not a node)', ('transform', not_a_node)],
):
test_createNode.__doc__ = """Test MDagGModifier::createNode raises error if called with {}.""".format(doc)

yield _createNode_fail, value, parent


@nose.with_setup(teardown=new_scene)
def _createNode_fail(value, parent):
old_nodes = cmds.ls(long=True)

nose.tools.assert_raises(
TypeError, _createNode_pass, value, parent
)

new_nodes = cmds.ls(long=True)

assert len(old_nodes) == len(new_nodes), "DagModifier.createNode modified the scene graph."


@nose.with_setup(teardown=new_scene)
def _createNode_pass(value, parent):
old_nodes = cmds.ls(long=True)

mod = cmdc.DagModifier()
node = mod.createNode(value, parent)
mod.doIt()

new_nodes = cmds.ls(long=True)

add_nodes = set(new_nodes) - set(old_nodes)

assert not node.isNull(), "Created node is not valid."
assert len(add_nodes) == 1, "`ls` did not return new node."


def test_reparentNode():
node_a = cmds.createNode('transform')
node_b = cmds.createNode('transform')
node_c = cmds.createNode('transform', parent=node_a)
node_d = cmds.createNode('transform', parent=node_c)

node_obj_a = as_obj(node_a)
node_obj_b = as_obj(node_b)
node_obj_c = as_obj(node_c)
node_obj_d = as_obj(node_d)
null_obj = cmdc.Object()

for doc, (node, new_parent) in (
['a null object (parent to world)', (node_obj_c, null_obj)],
['a valid object', (node_obj_c, node_obj_b)],
):
test_reparentNode.__doc__ = """Test MDagModifier::reparentNode if called with {}.""".format(doc)

yield _reparentNode_pass, node, new_parent

not_a_dag = as_obj('time1')
not_a_node = as_plug('persp.message').attribute()

for exc, doc, (node, new_parent) in (
[TypeError, 'an invalid object (not a DAG node)', (node_obj_c, not_a_dag)],
[TypeError, 'an invalid object (not a node)', (node_obj_c, not_a_node)],
[ValueError, 'the same object', (node_obj_c, node_obj_c)],
[ValueError, 'a parent and one of its children', (node_obj_c, node_obj_d)],
):
test_reparentNode.__doc__ = """Test MDagModifier::reparentNode raises an error if called with {}.""".format(doc)

yield _reparentNode_fail, exc, node, new_parent


def _reparentNode_pass(node, new_parent):
fn_node = cmdc.FnDagNode(node)

old_parent = fn_node.parent(0)

mod = cmdc.DagModifier()
mod.reparentNode(node, new_parent)

mod.doIt()
parent = fn_node.parent(0)

if new_parent.isNull():
assert parent == fn_node.dagRoot(), "DagModifier.reparentNode doIt failed"
else:
assert parent == new_parent, "DagModifier.reparentNode doIt failed"

mod.undoIt()
parent = fn_node.parent(0)
assert parent == old_parent, "DagModifier.reparentNode undo failed"

# Parent the node to world before the next test.
mod = cmdc.DagModifier()
mod.reparentNode(node, old_parent)
mod.doIt()


def _reparentNode_fail(exception, node, new_parent):
nose.tools.assert_raises(
exception,
cmdc.DagModifier().reparentNode,
node, new_parent
)