Chapter 4. Creating an Action

In this chapter, you'll learn how to create a subclass of SoAction. Make sure you understand the material in Chapters 1 and 2 before reading this chapter.

The chapter examples show creating and using an action class called GetVolumeAction.

Overview

The file SoSubAction.h contains the macros for defining new action classes. The SO_ACTION_HEADER() macro declares type identifier and naming variables and methods that all action classes must support. The SO_ACTION_SOURCE() macro defines the static variables and methods declared in the SO_ACTION_HEADER() macro.

Creating a new action requires these steps:

  1. Select a name for the new action class and determine what class it is derived from.

  2. Define an initClass() method to initialize the runtime type information for the class (see “Initializing the Action Class”).

    a. Enable elements in the state that are used by nodes when the action
    is applied to them (see “Enabling Elements in the State”).

    b. Register a static method for each node class that supports this action
    (see “Registering Static Methods”).

  3. Write the constructor for the action (see “Defining the Constructor”).

  4. Write the destructor for the action (see “Defining the Destructor”).

  5. If necessary, override the beginTraversal() method to implement a different traversal behavior or to perform special initialization before traversal. The apply() methods all call beginTraversal() (see “Traversal Behavior”).

  6. Implement the methods that you registered in step 2b of this list (see “Implementing Static Methods”).


Tip: An easy way to create a new action is to derive it from the callback action. The callback action provides generic traversal of the scene graph and enables all standard elements. Note that deriving a new action class requires more work than simply registering callback functions with the callback action. In many cases, this latter approach will suffice.


Initializing the Action Class

All action classes must have a static method to initialize the class (just like node classes). In this method, typically called initClass(), the type identifier information for the class is set up. This method must be called for all action classes to set up the method list correctly before an instance of the action can be created. The required work of this method is done by the SO_ACTION_INIT_CLASS() macro.

Enabling Elements in the State

Your action may also need to enable certain elements in the state. For example, the SoRayPickAction enables the SoPickRayElement in its initClass() routine:

enableElement(SoPickRayElement::getClassTypeId());

Recall from Chapter 2 that you can also enable elements in node class initialization methods using the SO_ENABLE() macro. All elements enabled by a parent action class are automatically enabled in a derived class.

Registering Static Methods

In addition, you need to explicitly register a static method for each supported node class with the method list for your new action. At a minimum, register a method for SoNode, which can be inherited by other nodes. Use the SO_ACTION_ADD_METHOD() macro to register methods with the method list. For example:

SO_ACTION_ADD_METHOD(SoCube, cubeVolume);

See “Implementing Static Methods” for information on implementing new static methods.

Defining the Constructor

Use the SO_ACTION_CONSTRUCTOR() to perform the basic work for you. For example:

GetVolumeAction::GetVolumeAction()
{
   SO_ACTION_CONSTRUCTOR(GetVolumeAction);
}

Defining the Destructor

There is no macro for defining a destructor. Use the destructor to free up anything you created in the constructor or in any other methods for the class. For example:

SoWriteAction::~SoWriteAction()
{
   // Get rid of the SoOutput if we created it in the
   // constructor
   if (createdOutput)
      delete output;
}

Traversal Behavior

The apply() method on SoAction always calls beginTraversal() to begin traversal of the graph. The default behavior of beginTraversal() is simply to call SoAction::traverse(). If you need to initialize the action every time it is applied, you can implement a beginTraversal() method for your action class. For example, the SoGetBoundingBoxAction sets the bounding box to be empty each time before it begins traversing the scene graph. Afterwards, it calls SoAction::traverse(). Note that beginTraversal() is passed a node, which is either the root of the graph the action is being applied to or the head of a path. Regardless of whether the action is applied to a node, a path, or a path list, the traverse() method handles the traversal appropriately.

For certain classes, you may also want to perform certain operations after traversal, or you may want to change the traversal behavior itself. For example, the render action performs multiple passes for antialiasing.

Implementing Static Methods

Chapter 2 provides important background material on implementing static methods and on doAction(). If you are creating a new node class, the static methods are typically included in the new node class. If you are creating a new action class, the static methods are included in the new action class. In the end, the new class (whether node or action) registers these static methods with the action class.

First, for SoNode, you will usually want to register the nullAction() method (defined on SoAction). This method will be used for any node classes that do not have a specific method registered for them.

For the node classes that actually do something for your action, you need to implement a static method. You'll probably choose one of two approaches:

  • Implement a new method for the class to handle the method in its unique way.

  • Implement a simple method that calls the doAction() method for that class if you want it to perform the action in its typical manner (for example, group nodes traverse their children in a specified order, property nodes affect elements in the state, and so on).

In our example class, the GetVolumeAction registers the nullAction() method for SoNode. This method, defined on SoAction, will be inherited by nodes that do not implement their own GetVolumeAction:

SO_ACTION_ADD_METHOD(SoNode, nullAction);

GetVolumeAction implements specific methods for the SoCube and SoSphere classes. For purposes of example, only the cube and sphere classes implement the GetVolumeAction. If you were actually creating and using such an action, you would probably want to be able to obtain the volume of any shape.

For GetVolumeAction, the group and relevant property classes (such as the coordinate, transformation, and complexity classes) all call doAction(), since they perform their “typical” action behavior:

void
GetVolumeAction::callDoAction(SoAction *action, SoNode *node)
{
   node->doAction(action);
}

Creating a New Action

The example presented here defines GetVolumeAction, an action class that computes the volume of shapes. Each shape adds its volume (in world space) to the volume collected so far.

Since the standard shape classes know nothing about GetVolumeAction, we have to register the appropriate methods for them with the method list. This is illustrated in the example by adding methods for the SoCube and SoSphere classes to compute volume.

When you create a new action, you have to decide which elements to enable. You must go through the list of elements and decide which ones have a bearing on the action being performed. Since the GetVolumeAction is concerned solely with geometry and coordinate spaces, we enable only elements that are relevant to these properties. We ignore material, textures, and other appearance properties. Note that we also enable SoSwitchElement, since it is vital to correct traversal of graphs with switch nodes; all actions doing traversal should enable this element.

Example 4-1 shows the header file for the GetVolumeAction class.

Example 4-1. GetVolumeAction.h


#include <Inventor/actions/SoSubAction.h>

class GetVolumeAction : public SoAction {

   SO_ACTION_HEADER(GetVolumeAction);

 public:
   // Initializes this action class for use with scene graphs
   static void    initClass();

   // Constructor and destructor
   GetVolumeAction();
   virtual ~GetVolumeAction();

   // Returns computed volume after action is applied
   float          getVolume() const { return volume; }

 protected:
   // Initiates action on graph
   virtual void   beginTraversal(SoNode *node);

 private:
   float          volume;      // Computed volume

   // These are the methods that are used to apply the action
   // to various node classes. The third method is registered
   // for all relevant non-shape nodes. The calling sequence for
   // these methods is that used for all methods in the global
   // action table.
   static void    cubeVolume(SoAction *, SoNode *);
   static void    sphereVolume(SoAction *, SoNode *);
   static void    callDoAction(SoAction *, SoNode *);

   // This adds the given object-space volume to the total. It
   // first converts the volume to world space, using the
   // current model matrix.
   void           addVolume(float objectSpaceArea);
};

Example 4-2 shows the source code for the GetVolumeAction class.

Example 4-2. GetVolumeAction.c++


#include <Inventor/elements/SoComplexityElement.h>
#include <Inventor/elements/SoComplexityTypeElement.h>
#include <Inventor/elements/SoCoordinateElement.h>
#include <Inventor/elements/SoElements.h>
#include <Inventor/elements/SoFontNameElement.h>
#include <Inventor/elements/SoFontSizeElement.h>
#include <Inventor/elements/SoModelMatrixElement.h>
#include <Inventor/elements/SoProfileCoordinateElement.h>
#include <Inventor/elements/SoProfileElement.h>
#include <Inventor/elements/SoSwitchElement.h>
#include <Inventor/elements/SoUnitsElement.h>
#include <Inventor/elements/SoViewVolumeElement.h>
#include <Inventor/elements/SoViewingMatrixElement.h>
#include <Inventor/elements/SoViewportRegionElement.h>
#include <Inventor/nodes/SoCamera.h>
#include <Inventor/nodes/SoComplexity.h>
#include <Inventor/nodes/SoCoordinate3.h>
#include <Inventor/nodes/SoCoordinate4.h>
#include <Inventor/nodes/SoCube.h>
#include <Inventor/nodes/SoFont.h>
#include <Inventor/nodes/SoGroup.h>
#include <Inventor/nodes/SoProfile.h>
#include <Inventor/nodes/SoProfileCoordinate2.h>
#include <Inventor/nodes/SoProfileCoordinate3.h>
#include <Inventor/nodes/SoSphere.h>
#include <Inventor/nodes/SoTransformation.h>
#include "GetVolumeAction.h"

SO_ACTION_SOURCE(GetVolumeAction);

// Initializes the GetVolumeAction class. This is a one-time
// thing that is done after database initialization and before
// any instance of this class is constructed.

void
GetVolumeAction::initClass()
{
   // Initialize the runtime type variables
   SO_ACTION_INIT_CLASS(GetVolumeAction, SoAction);

   // Enable elements that are involved in volume computation.
   // Most of these deal with geometrix properties
   // (coordinates, profiles) or transformations (model matrix,
   // units). Some are needed for certain groups (switches,
   // level-of-detail) to function correctly.
   SO_ENABLE(GetVolumeAction, SoModelMatrixElement);
   SO_ENABLE(GetVolumeAction, SoComplexityElement);
   SO_ENABLE(GetVolumeAction, SoComplexityTypeElement);
   SO_ENABLE(GetVolumeAction, SoCoordinateElement);
   SO_ENABLE(GetVolumeAction, SoFontNameElement);
   SO_ENABLE(GetVolumeAction, SoFontSizeElement);
   SO_ENABLE(GetVolumeAction, SoProfileCoordinateElement);
   SO_ENABLE(GetVolumeAction, SoProfileElement);
   SO_ENABLE(GetVolumeAction, SoSwitchElement);
   SO_ENABLE(GetVolumeAction, SoUnitsElement);
   SO_ENABLE(GetVolumeAction, SoViewVolumeElement);
   SO_ENABLE(GetVolumeAction, SoViewingMatrixElement);
   SO_ENABLE(GetVolumeAction, SoViewportRegionElement);

   // Now we need to register methods to implement this action
   // for various node classes. We have created implementations
   // for two specific shape nodes, SoCube and SoSphere, so we
   // can register specific methods for those two classes. We
   // also want to make sure that group classes traverse their
   // children correctly for this action, so we will use a
   // method that calls doAction() to handle groups. Finally,
   // we need to make sure that relevant property nodes set up
   // the state correctly; we can use the same method that
   // calls doAction() for these classes, as well. We will use
   // the SO_ACTION_ADD_METHOD() macro to make this easier.

   // This registers a method to call for SoNode, so it will be
   // used for any node class that does not have a more
   // specific method registered for it. This makes sure that
   // there is always a method to call for any node. The
   // "nullAction" method is defined on SoAction for use in
   // cases like this.
   SO_ACTION_ADD_METHOD(SoNode, nullAction);

   // These register methods for the two shapes that can
   // really handle the action
   SO_ACTION_ADD_METHOD(SoCube, cubeVolume);
   SO_ACTION_ADD_METHOD(SoSphere, sphereVolume);

   // Register the method that calls doAction() for all group
   // classes and for relevant properties (transformations,
   // coordinates, profiles, and so on).
   SO_ACTION_ADD_METHOD(SoCamera,             callDoAction);
   SO_ACTION_ADD_METHOD(SoComplexity,         callDoAction);
   SO_ACTION_ADD_METHOD(SoCoordinate3,        callDoAction);
   SO_ACTION_ADD_METHOD(SoCoordinate4,        callDoAction);
   SO_ACTION_ADD_METHOD(SoFont,               callDoAction);
   SO_ACTION_ADD_METHOD(SoGroup,              callDoAction);
   SO_ACTION_ADD_METHOD(SoProfile,            callDoAction);
   SO_ACTION_ADD_METHOD(SoProfileCoordinate2, callDoAction);
   SO_ACTION_ADD_METHOD(SoProfileCoordinate3, callDoAction);
   SO_ACTION_ADD_METHOD(SoTransformation,     callDoAction);
}

// Constructor

GetVolumeAction::GetVolumeAction()
{
   SO_ACTION_CONSTRUCTOR(GetVolumeAction);
}

// Destructor. Does nothing.

GetVolumeAction::~GetVolumeAction()
{
}

// Initiates action on a graph. This is called when the action
// is applied to a node, a path, or a path list. It gives us a
// chance to initialize things before beginning traversal.

void
GetVolumeAction::beginTraversal(SoNode *node)
{
   // Initialize volume to 0
   volume = 0.0;

   // Begin traversal at the given root node.
   traverse(node);
}

// This method implements the action for an SoCube node.

void
GetVolumeAction::cubeVolume(SoAction *action, SoNode *node)
{
   // The action is really an instance of GetVolumeAction
   GetVolumeAction *volumeAct = (GetVolumeAction *) action;

   // And the node pointer is really a cube:
   const SoCube    *cube = (const SoCube *) node;

   // Find the dimensions of the cube
   float width    = (cube->width.isIgnored()  ? 2.0 :
                     cube->width.getValue());
   float height   = (cube->height.isIgnored() ? 2.0 :
                     cube->height.getValue());
   float depth    = (cube->depth.isIgnored()  ? 2.0 :
                     cube->depth.getValue());

   // ...and the volume
   float cubeVol = width * height * depth;

   // Add the volume to the accumulated volume in the action
   volumeAct->addVolume(cubeVol);
}

// This method implements the action for an SoSphere node.

void
GetVolumeAction::sphereVolume(SoAction *action, SoNode *node)
{
   // The action is really an instance of GetVolumeAction
   GetVolumeAction *volumeAct = (GetVolumeAction *) action;

   // And the node pointer is really a sphere:
   const SoSphere  *sphere = (const SoSphere *) node;

   // Find the radius of the sphere
   float radius = (sphere->radius.isIgnored() ? 1.0 :
                   sphere->radius.getValue());

   // Compute the volume using our favorite formula that we all
   // remember from our math classes, right?
   float sphereVol = 4./3. * M_PI * radius * radius * radius;

   // Add the volume to the accumulated volume in the action
   volumeAct->addVolume(sphereVol);
}

// This method implements the action for all of the relevant
// non-shape node classes.

void
GetVolumeAction::callDoAction(SoAction *action, SoNode *node)
{
   node->doAction(action);
}

// This adds the given object-space volume to the total, first
// converting it to world space using the current model matrix.

void
GetVolumeAction::addVolume(float objectSpaceVolume)
{
   // Find the current modeling matrix
   const SbMatrix &modelMatrix =
     SoModelMatrixElement::get(state);

   // The determinant of the upper-left 3x3 of this matrix is
   // the conversion factor we need to go from object-space
   // volume to world space. Pretty cool, indeed.
   float objectToWorldFactor = modelMatrix.det3();

   // Add in the converted volume to our current volume
   volume += objectToWorldFactor * objectSpaceVolume;
}

Using New Action Classes

Like nodes, action classes must be initialized before any instances can be constructed. Also note that before you can add a method to the method list, you must initialize both the action class and the node class involved.

Example 4-3 reads in a scene graph from the file volume.iv and applies the GetVolumeAction action to it, printing the resulting volume.

Example 4-3. PrintVolume.c++


#include <Inventor/SoDB.h>
#include <Inventor/SoInput.h>
#include <Inventor/SoInteraction.h>
#include <Inventor/nodes/SoSeparator.h>

// Header file for new action class
#include "GetVolumeAction.h"

main()
{
   // Initialize Inventor
   SoInteraction::init();

   // Initialize the new action class
   GetVolumeAction::initClass();

   // Open the file and read the scene
   SoInput      myInput;
   SoSeparator  *root;
   if (! myInput.openFile("volume.iv")) {
     fprintf(stderr, "Can't open \"volume.iv\" for reading\n");
     return 1;
   }
   root = SoDB::readAll(&myInput);
   if (root == NULL) {
     printf("Couldn't read scene from \"volume.iv\"\n");
     return 2;
   }
   root->ref();

   // Compute the volume: apply a GetVolumeAction to the root
   GetVolumeAction va;
   va.apply(root);

   // Print the result
   printf("Total volume = %g\n", va.getVolume());

   return 0;
}