Skip to content

Commit

Permalink
Merge pull request #22 from yantor3d/rporter/MDAGModifier
Browse files Browse the repository at this point in the history
Implment MDagModifier bindings
  • Loading branch information
mottosso committed Jun 13, 2021
2 parents b97a148 + d37b487 commit b0a4cdb
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 9 deletions.
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
)

0 comments on commit b0a4cdb

Please sign in to comment.