4.7. 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 SoCube SoCube and SoSphere SoSphere 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 SoSwitchElement 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();
   static void    exitClass();

   // 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.cxx

#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);
}

void
GetVolumeAction::exitClass()
{
  SO_ACTION_EXIT_CLASS(GetVolumeAction);
}

// 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;
}