Maya API How-To #22

Back · Previous · Next Maya

How do I generate construction history on a mesh node?

The first thing you'll want to do is to determine whether or not the mesh in question has construction history already. If history exists, you need only disconnect the plugs between the history node and the mesh, and insert your node.

If there is no history, then you need to generate an intermediate node upstream of the mesh, and use its output to feed your node.

Has History
Node has construction history
Has History + Node
Break connections and insert node

No History
No construction history
Intermediate
Create intermediate and insert node
// **************************************************************************
//  AddConstructionHistory
// **************************************************************************
/// Adds construction history to the specified mesh, if required.
///
/// If no construction history exists, then a copy of the specifed mesh is
/// created using MFnMesh, and this new mesh is set to be an intermediate
/// node. If history already exists for the mesh, no new nodes are created.
///
/// The plugs returned represent those between which you will insert any
/// construction.
///
///   history.outMesh ->  ( your node )  -> mesh.inMesh
///
/// \note   It is assumed that you wish to insert a node immediately upstream
///         of the specified mesh. If you provide a valid (MDGModifier*) then
///         the plugs returned by this function are _disconnected_, even if
///         they were connected at the point when this was called.
///
/// \param  mesh: The mesh to which you wish to add construction history.
/// \param  outMeshPlug: The plug from which you will connect your
///                      construction node; e.g.
///                         outMeshPlug.outMesh -> yourNode.inMesh
/// \param  inMeshPlug: The plug on the mesh node to which you will connect
///                     your construction node; e.g.:
///                         yourNode.outMesh -> mesh.inMesh
/// \param  intermediate: The MObject for the intermediate node (a copy of
///                       the specified mesh) that was created to introduce
///                       construction history. This will be
///                       MObject::kNullObj if the mesh already has history.
/// \param  dg: Pointer to a MDGModifier. If non-NULL, and history already
///             exists for the mesh, this holds the undo for the disconnect
///             of the attributes from the history node to the mesh node.
// **************************************************************************
MStatus AddConstructionHistory( const MDagPath& mesh,
                                MPlug& outMeshPlug,
                                MPlug& inMeshPlug,
                                MObject& intermediate,
                                MDGModifier* dg )
{
    MStatus status;

    intermediate = MObject::kNullObj;

    MFnDependencyNode fnDependNode( mesh.node() );

    // Assign the '.inMesh' plug from the mesh node.
    inMeshPlug = fnDependNode.findPlug( "inMesh" );

    // If the mesh has history, use it for the '.outMesh' plug.
    MPlugArray cc;
    inMeshPlug.connectedTo( cc, true /* asDst */, false );
    if ( cc.length() > 0 )
    {
        // Assign the '.outMesh' plug from the node upstream of the mesh.
        outMeshPlug = cc[0];

        // Disconnect the plugs.
        if ( NULL != dg )
        {
            dg->disconnect( outMeshPlug, inMeshPlug );
            status = dg->doIt();
        }
    }
    else
    {
        // Need to generate construction history on the mesh
        MDagPath transform( mesh );
        transform.pop();

        MFnMesh fnMesh;
        intermediate = fnMesh.copy ( mesh.node(), transform.node(), &status );
        if ( status != MS::kSuccess ) { status.perror( "FAILED to copy mesh node" ); return status; }

        // Set the copy to be an intermediate object
        fnDependNode.setObject( intermediate );
        MPlug intermediatePlug = fnDependNode.findPlug( "intermediateObject" );
        intermediatePlug.setValue( true );

        // Assign the '.outMesh' plug from the intermediate node.
        outMeshPlug = fnDependNode.findPlug( "outMesh" );
    }

    return status;
}

Note: This will not properly associate any per-face shading assignments on the mesh if the intermediate node is created. If you need per-face shading assignments you must collapse this new construction history and then assign the shading groups. (More on this in an upcoming How-To…)

Undo

MDGModifier does not offer an interface for duplicating a node, so we use MFnMesh::copy() instead.

(OK, I suppose you could try MDGModifier::commandToExecute( "duplicate" ), but then you have to go through hoops to determine the name of the node that was created as the duplicate, and you'll likely end up with latent shader connections (and whatever else), whereas MFnMesh::copy() gives us a clean shape with no baggage.)

Bottom line: Because this doesn't use MDGModifier to construct the node, you cannot rely on MDGModifier to track its undo. The work done in your undoIt() method then has two tasks:

  1. Undo the MDGModifier tasks.

  2. Delete the intermediate mesh node (if it was created)

For my undo I typically keep an array or container of (MDGModifier*) objects for undo. A class using a similar system would look something like this:

class MyCommand : public MPxCommand
{
    private:

        // The stack of undo to perform.
        std::vector<MDGModifier*> mUndo;

        // The intermediate mesh.
        MDagPath mIntermediateMesh;
        bool mHasMeshUndo;
};

Here's the undoIt() method which undoes a modifier stack and deletes an intermediate mesh. Because the intermediate mesh may not have been created (and thus won't need to be undone) a (bool) is used to indicate when the insertion of construction history created the node.

MStatus MyCommand::undoIt( void )
{
    MStatus status;

    std::vector<MDGModifier*>::reverse_iterator riter;
    for ( riter = mUndo.rbegin(); riter != mUndo.rend(); ++riter )
    {
        (*riter)->undoIt();
        delete (*riter);
    }
    // We're not reusing the undo for redo.
    mUndo.clear();

    if ( mHasMeshUndo )
    {
        MGlobal::deleteNode( mIntermediateMesh );
        mHasMeshUndo = false;
    }

    return status;
}

Redo

Another repercussion of using MFnMesh::copy() is that you cannot rely on MDGModifer for your redo. Each time the command is undone and redone, the DAG path (and associated MObject) for the intermediate mesh node will be different. Any cached MDGModifier::connect() calls referring to the intermediate mesh will fail when you attempt to redo the modifier, because the node it needs will not exist. For cases like this I simply resort to using MPxCommand::redoIt() to perform all the work, rebuilding the undo stack each time.


Related How-To's