Visitors in Action

From The Foundry MODO SDK wiki
Jump to: navigation, search

Visitors are a general mechanism allowing clients (plug-ins) to traverse the system's (modo's) internal state. The client essentially defines a single action function to be performed over and over as the system walks some internal database. At each step the client can query the system about the current state of the enumeration, and abort the enumeration if it wants to. Visitors can also be used to cause a client action to happen in a specific state.

Although this is related to the Visitor pattern, it's a very minimal implementation. Although the simplicity makes it easy to reuse for multiple purposes, it does take a little practice to understand how to use it.

Enumeration Methods

Interfaces provide enumeration methods to allow a client to traverse the contents of some container or database, especially when access by count and index are inefficient or otherwise undesirable. A typical example are the mesh accessors. These objects are allocated specifically for investigating the features of mesh elements: points, polygons, edges and maps. Methods on the object allow the client to select elements by index or ID and then to query the object for the element's properties.

In addition there are also enumeration methods. These take a mark mode, a visitor, and a monitor.

         LXxMETHOD(  LxResult,
 Enumerate) (
         LXtObjectID              self,
         LXtMarkMode              mode,
         LXtObjectID              visitor,
         LXtObjectID              monitor);

The mark mode identifies a subset of elements to traverse, and the monitor gives feedback about the overall progress. The visitor object is used to allow the client to act on each element during the traversal. Even when Count/ByIndex methods are available, an enumeration call will generally be more efficient and should be preferred, all things being equal.

Visitor Evaluation

The Visitor::Evaluate() method is called for each element, and the visitor is expected to request attributes of the current element that it wants to query. Let's say we want to make a list of point positions in a mesh. The general outline of this process would be to:

  • allocate a point accessor
  • create a visitor
  • enumerate the points:
    • for each point:
      • query point position
      • add to list

After enumeration we would have a list of point positions. This may seem more complex than enumerating points by index -- and it is -- but there are good reasons for learning this technique. Under certain circumstances it can be quite a bit more efficient; you should test your implementation because it may not be true in all cases. The real reason is that some things can only be accessed using visitors.

Abstract Visitor

Fortunately there are some wrappers that make this process relatively easy. Many of the user classes define alternate methods that take abstract visitors. For example all the mesh accessors have alternate methods defined like this:

         LxResult
 Enum (
         CLxImpl_AbstractVisitor *visitor,
         LXtMarkMode              mode = LXiMARK_ANY,
         ILxUnknownID             mon  = 0)

These C++ methods take CLxImpl_AbstractVisitor instead of generic objects along with other conveniences like optional arguments. The abstract visitor class requires a simple C++ method without any COM-related rigamarole. Here's all the code required to build a point position list using the user method.

 class CPointList : public CLxImpl_AbstractVisitor
 {
     public:
 	CLxUser_Point		 point;
 	std::vector<LXtFVector>	 list;
 
 		LxResult
 	Evaluate ()
 	{
 		LXtFVector	 pos;
 
 		point.Pos (pos);
 		list.push_back (pos);
		return LXe_OK;
 	}
 };
 
 	static void
 GetPointList (
 	CLxUser_Mesh		&mesh,
 	CPointList		&plist)
 {
 	plist.point.fromMesh (mesh);
 	plist.point.Enum (plist);
 }

There are several important things to notice about this example. First the visitor is just a C++ class deriving from CLxImpl_AbstractVisitor. The CLxImpl_ prefix normally means that the class requires a COM wrapper, but in this case that's just an incorrect use of the naming conventions. In fact the Evaluate() method derived from the superclass is sufficient.

Second, the point accessor is part of the member data of the visitor. This allows the visitor to query the state of the accessor during evaluation, which is critical for reading out the state of the current element as the mesh is traversed.

Finally, Evaluate() returns LXe_OK. If it returned anything else the enumeration would have aborted and the Enum() method would have returned that result rather than OK.

One Visitor

There may be interfaces that don't have nice user methods. What if something really requires a real visitor object and has no C++ alternate? That's also easy -- we can use the CLxInst_OneVisitor class to create a singleton. For example the previous code can be rewritten to use the raw Enumerate() method rather than the Enum() user wrapper.

 class CPointList
 {
     public:
 	CLxUser_Point		 point;
 	std::vector<LXtFVector>	 list;
 
 		LxResult
 	Evaluate ()
 	{
 		LXtFVector	 pos;
 
 		point.Pos (pos);
 		list.push_back (pos);
		return LXe_OK;
 	}
 };
 
 	static std::vector<LXtFVector>
 GetPointList (
 	CLxUser_Mesh		&mesh)
 {
 	CLxInst_OneVisitor<CPointList> one;
 
 	one.point.fromMesh (mesh);
 	one.point.Enumerate (plist, LXiMARK_ANY, 0);
 	return one.loc.list;
 }

What's basically happening here is that the One Visitor template class is doing the hard work of creating a single, one-use COM object for the purpose of enumeration. Admittedly some of the other tradeoffs are different too; it's a different way of doing effectively the same thing.

Spawned Visitor

... TBD ...