Writing Queryable Commands for Modo

In modo 601, three scripting languages are supported; Python, Perl and Lua. These are implemented as simple wrappers for the internal commands. So for example, if you wish to do something simple, like read all of the point positions on a mesh and output them to a file or to the debug output, you have to do something like this in Python:

	#	Tell modo to query the vertices on the active layer.
	vertices = lx.eval("query layerservice verts ? all")
	
	#	Calculate how many vertices we have in the vertices tuple.
	vertex_count = len(vertices)

	#	Loop through the vertices.
	for vertex in range(0, vertex_count):

		#	Grab the position of the current vertex.
		vertex_pos = lx.eval('query layerservice vert.wpos ? %s' % vertex)
	
		#	Break the vector out into three variables.
		vertex_pos_x = vertex_pos[0]
		vertex_pos_y = vertex_pos[1]
		vertex_pos_z = vertex_pos[2]

So, as you can see from the above snippet, we are simply wrapping modo commands in lx.eval(""), the command is sent to modo and modo returns a value.

The benefits of this, are that you can sit with a copy of modo open, run through the steps that you want your script to automate and then simply copy and paste the commands that modo fires. The downside of this, is that it's slow, especially when working with lots of data, such as vertices. The other drawback of this system, is that you only have access to the functionality that is exposed in modo, through a command. So if you wanted to read the input to a specific relationship graph for example, you wouldn't be able to do it, because there is no command inside of modo that provides that functionality.

Both of these issues can be partially solved, by implementing a custom command in modo, using the C++ API. We can add a command to read all of the point positions on a particular mesh in one command, speeding up the script above by a large degree (rather than having to loop through the points from python), or we can add extra functionality, such as a command that allows you to query an internal value. By writing these as commands that are exposed to scripting, we can easily reuse them time and again to improve both speed and functionality of multiple scripts.

In this blog post, I'll show you the framework for creating a queryable command. I'll be basing it on the latest SDK (54144) and of course, modo 601.


Overview

Ok, so before we begin with the actual code for this command, we first need to work out exactly what it's going to do and how it's going to do it.

So, we're going to write a command that will read all of the inputs to a particular graph for the current selected item. It will use the following syntax: "graph.reverse ? <graphname>". It will be a query only command, so this command won't allow you to rewire graphs, only query their current state. The command will return a list of item ids for input items. If the command fails (graph name doesn't exist, no item selected...etc), the command will return a failed error code. The data returned has to be generic so that it can be understood in different ways by different scripting languages, for example, Python will read the results as a tuple, Perl will read the results as an array...etc.

So it'll allow us to write something like the following in Python:

	#	Assuming we have a mesh item selected.
	influences = lx.eval("graph.reverse ? deformers")

So, let's get started...


The Basics

I'm going to assume that if you're reading this, you probably know a bit about the modo SDK already and you know the basic structure of plugins and commands. If however, you're new to developing plugins in modo, then I suggest you read the Getting Started section on the modo SDK wiki. This command example may be a bit too much for a first experiment with the SDK.


The Command

Ok, with all of that out of the way, it's time to get into the meat of the plugin and start writing our command.

First, we want to include the required header files, needed to access the functions we want to use in our plugin.

#include	"lx_plugin.hpp"
#include	"lx_item.hpp"
#include	"lx_value.hpp"
#include	"lx_select.hpp"
#include	"lx_seltypes.hpp"
#include	"lxu_command.hpp"

lx_plugin.hpp provides the basics for the SDK, we need to include this file in every plugin we write. lx_item.hpp provides the functions for working with items and reading relationship graphs. lx_value.hpp provides some functions for setting the value we return from our command. lx_select.hpp and lx_seltypes.hpp provide some functions and defines for working with selections. Finally, lxu_command.hpp provides some nice wrapper functions that make writing our command much easier.

Next, we want to define the name of the command and the name of the arguments our commands will use. We could write these directly into our functions as strings and ints, but to make things easier to modify, we should define them in a single manageable place.

#define		CMD_NAME		"graph.reverse"

#define		ARG_GRAPH		"graph"
#define		ARG_ITEM		"items"

#define		ARGi_ITEM		0
#define		ARGi_GRAPH		1

The CMD_NAME is the name of the command we will enter into modo or a script to run our command. The two strings are simply the names of our two command arguments and the two integers simply define "shortcuts" for the argument order.

Next up, we want to define a class and functions for our command. We're only going to implement the very basics of what's needed here. For the full overview of everything that can be implemented with commands, please check out the SDK wiki.

If this was a command that performed an action, we'd want to implement cmd_Execute(), but as we are querying and returning a value, we want to implement cmd_Query() instead.

class cmd_graphReverse : public CLxBasicCommand
{
	public:
		CLxUser_CommandService			cmd_srv;
		CLxUser_SelectionService		sel_srv;
		CLxUser_Scene					scene_loc;
		CLxUser_Item					item_loc, input_item_loc;
		CLxUser_ItemGraph				graph_loc;
		
		cmd_graphReverse				();

		int			basic_CmdFlags		()											LXx_OVERRIDE;
		bool		basic_Enable		(CLxUser_Message &msg)						LXx_OVERRIDE;
		LxResult	cmd_Query			(unsigned int index, ILxUnknownID vaQuery)	LXx_OVERRIDE;
};

Now we have to actually implement our functions. So we'll begin with the constructor.

cmd_graphReverse::cmd_graphReverse ()
{
	dyna_Add(ARG_ITEM, LXsTYPE_STRING);
	dyna_Add(ARG_GRAPH, LXsTYPE_STRING);
	
	basic_SetFlags(ARGi_ITEM, LXfCMDARG_QUERY);
}

In the constructor, we define the arguments that will be used on our command. First, we use dyna_Add to add the item and graph arguments with a string type. Then we set the properties of the two arguments, the first is the value returned from the query. An argument that has no property defined is assumed to be a required variable for the command to run.

The basic_CmdFlags function is a pure virtual function, so we need to implement it. But, we don't want to actually do anything here, so we'll just return 0.

int cmd_graphReverse::basic_CmdFlags ()
{
	return 0;
}

Next, we define whether the command is enabled or not. We could do all kinds of checks here, but for simplicity sake, we'll just check if at least one item is selected.

bool cmd_graphReverse::basic_Enable (CLxUser_Message &msg)
{
	if(sel_srv.Count(LXiSEL_ITEM) > 0)
	{
		return true;
	}
	else
	{
		return false;
	}
}

This uses the selection service to get the number of selected items. Ideally, we would also return a message to the user here if the command is disabled, to inform them why the command is disabled, but that's beyond the scope of this article.

With all of that out of the way, we're finally ready to start implementing the actual functionality of the command. I've broken this function down into smaller sections so I can explain what's happening in a smaller "bite-size" pieces.

LxResult cmd_graphReverse::cmd_Query (unsigned int index, ILxUnknownID vaQuery)
{
	CLxUser_ValueArray		val_array(vaQuery);

	if(!dyna_IsSet(ARGi_GRAPH))
	{
		return LXe_FAILED;
	}

	std::string graphName_arg;
	attr_GetString(ARGi_GRAPH, graphName_arg);

To begin with, we create a localized ValueArray, this is what will actually store the results of our query. We can pass multiple values to this to return multiple values from our command. We also check that our "graph" argument is set, if the user hasn't provided a value for that argument, we tell modo the command failed. If the "graph" argument is set, we read it and store it in a string.

Next we want to get the item to read the relationship graph from. To keep things simple in this example, we'll only work on the current selected item.

	unsigned selection_count = sel_srv.Count(LXiSEL_ITEM);
	
	CLxUser_ItemPacketTranslation	pkt_item;
	pkt_item.autoInit();
	
	if(selection_count == 0)
	{
		return LXe_FAILED;
	}

	void *pkt = sel_srv.ByIndex(LXiSEL_ITEM, 0);

	if(pkt_item.test())
	{
		pkt_item.GetItem(pkt, item_loc);
	}

	if(!item_loc.test())
	{
		return LXe_FAILED;
	}

We begin by checking the item count, if the item count is 0, then we tell modo the command failed. In reality, the command should never be enabled if the item count is 0, but it doesn't do any damage just to double check. Once we are sure we have at least one item selected, we read the first item and use the ItemPacketTranslation to get an item from the selection. Finally, we double check that we have a localized item to work with.

Now we have the graph name and item to work with, we simply need to read the input graph and get some information about the connected items.

	scene_loc.from (item_loc);
	if(!scene_loc.GetGraph (graphName_arg.c_str(), graph_loc))
	{
		return LXe_FAILED;
	}
	unsigned n = graph_loc.Reverse(item_loc);

	for (unsigned i=0; i<n; i++)
	{
		graph_loc.Reverse(item_loc, i, input_item_loc);

		if(!input_item_loc.test())
		{
			continue;
		}
		
		std::string item_ident = input_item_loc.GetIdentity();
		val_array.AddString(item_ident.c_str());
	}

	return LXe_OK;
}

First up, we get the current scene item from our selected item. We then use this to set the graph we want to read from. If the graph doesn't exist, we tell modo that the command failed. If the graph exists, we read the number of inputs to that graph on the current item. Once we have the input count, we loop through the connected items and get their id. We then write the id out the ValueArray as a returned value from the command. Finally, we return LXe_OK to tell modo that if we made it to the end of the cmd_Query function, our command fired successfully.

The bulk of our command is now implemented, we simply need to export the server to modo, so that it registers as a new command when you import the plugin.

void initialize()
{
	CLxGenericPolymorph	*srv;

	srv = new CLxPolymorph							 <cmd_graphReverse>;
	srv->AddInterface	(new CLxIfc_Command			<cmd_graphReverse>);
	srv->AddInterface	(new CLxIfc_Attributes		<cmd_graphReverse>);
	srv->AddInterface	(new CLxIfc_AttributesUI	<cmd_graphReverse>);
	lx::AddServer		 (CMD_NAME, srv);
}

void cleanup()
{

}

And that's it! If we build this and import the plugin into modo, when you run the following command, modo will output the names of any connected influences or deformers, linked to the current item. You can obviously change the graph name from deformers to another graph type, to read different relationship graphs.

	#	Assuming we have a mesh item selected.
	influences = lx.eval("graph.reverse ? deformers")

The Code

Listed below is the full source code for this article.

/*

	cmd_graphReverse.cpp

	Implements a simple queryable command that returns
	the input item to a particular graph.

	The command syntax is:
	graph.reverse ? <graphName>

*/

/*
	Include the header files required for implementing our command.
*/

#include	"lx_plugin.hpp"
#include	"lx_item.hpp"
#include	"lx_value.hpp"
#include	"lx_select.hpp"
#include	"lx_seltypes.hpp"
#include	"lxu_command.hpp"

/*
	Define the server and argument names for the command plugin.
*/

#define		CMD_NAME		"graph.reverse"

#define		ARG_GRAPH		"graph"
#define		ARG_ITEM		"items"

#define		ARGi_ITEM		0
#define		ARGi_GRAPH		1

/*
	Define the functions for our command.
*/

class cmd_graphReverse : public CLxBasicCommand
{
	public:
		CLxUser_CommandService			cmd_srv;
		CLxUser_SelectionService		sel_srv;
		CLxUser_Scene					scene_loc;
		CLxUser_Item					item_loc, input_item_loc;
		CLxUser_ItemGraph				graph_loc;
		
		cmd_graphReverse				();

		int			basic_CmdFlags		()											LXx_OVERRIDE;
		bool		basic_Enable		(CLxUser_Message &msg)						LXx_OVERRIDE;
		LxResult	cmd_Query			(unsigned int index, ILxUnknownID vaQuery)	LXx_OVERRIDE;
};

/*
	Implement the functions for our command.
*/

cmd_graphReverse::cmd_graphReverse ()
{
	/*
		This is where we define the arguments that our
		command will use.
	*/
	dyna_Add(ARG_ITEM, LXsTYPE_STRING);
	dyna_Add(ARG_GRAPH, LXsTYPE_STRING);
	
	basic_SetFlags(ARGi_ITEM, LXfCMDARG_QUERY);
}

int cmd_graphReverse::basic_CmdFlags ()
{
	/*
		This is a pure virtual function, so we need to
		implement it. Even though, we are simply going
		to return 0.
	*/
	return 0;
}

bool cmd_graphReverse::basic_Enable (CLxUser_Message &msg)
{
	/*
		Here we simply check if an item is selected, if
		there is an item, we return true to enable the
		command. If not, we return false.
	*/
	if(sel_srv.Count(LXiSEL_ITEM) > 0)
	{
		return true;
	}
	else
	{
		return false;
	}
}

LxResult cmd_graphReverse::cmd_Query (unsigned int index, ILxUnknownID vaQuery)
{
	/*
		Here our command actually does it's work. We read
		the input graph that is defined in our graph argument
		and we return the value to modo as a value array.
	*/

	CLxUser_ValueArray		val_array(vaQuery);

	//	Check that the graph name argument is set.
	if(!dyna_IsSet(ARGi_GRAPH))
	{
		return LXe_FAILED;
	}

	//	Read the graph name argument.
	std::string graphName_arg;
	attr_GetString(ARGi_GRAPH, graphName_arg);

	//	Get the selected item.
	unsigned selection_count = sel_srv.Count(LXiSEL_ITEM);
	
	CLxUser_ItemPacketTranslation	pkt_item;
	pkt_item.autoInit();
	
	if(selection_count == 0)
	{
		//	There are no current items selected.
		return LXe_FAILED;
	}

	//	Work on the first selected item only.
	void *pkt = sel_srv.ByIndex(LXiSEL_ITEM, 0);

	if(pkt_item.test())
	{
		pkt_item.GetItem(pkt, item_loc);
	}

	//	Check that we have managed to localize the first item.
	if(!item_loc.test())
	{
		return LXe_FAILED;
	}
	
	//	Read the input graph from the current item.
	scene_loc.from (item_loc);
	if(!scene_loc.GetGraph (graphName_arg.c_str(), graph_loc))
	{
		return LXe_FAILED;
	}
	unsigned n = graph_loc.Reverse(item_loc);

	/*
		Loop through the inputs and read their identity.
	*/
	for (unsigned i=0; i<n; i++)
	{
		graph_loc.Reverse(item_loc, i, input_item_loc);

		if(!input_item_loc.test())
		{
			continue;
		}
		
		/*
			Read the item identity for the current input item and
			add it to the value array. The value array is the return
			value from the command.
		*/
		std::string item_ident = input_item_loc.GetIdentity();
		val_array.AddString(item_ident.c_str());
	}

	return LXe_OK;
}

void initialize()
{
	CLxGenericPolymorph	*srv;

	srv = new CLxPolymorph							 <cmd_graphReverse>;
	srv->AddInterface	(new CLxIfc_Command			<cmd_graphReverse>);
	srv->AddInterface	(new CLxIfc_Attributes		<cmd_graphReverse>);
	srv->AddInterface	(new CLxIfc_AttributesUI	<cmd_graphReverse>);
	lx::AddServer		 (CMD_NAME, srv);
}

void cleanup()
{

}