Chapter 9. Creating a Selection Highlight Style

This chapter explains how you can provide your own style of selection highlighting by deriving a new highlight class. The first part of the chapter offers an overview of the steps required to create a new highlight class. When necessary, additional sections explain key concepts in further detail. The chapter examples show how to create two new highlight classes:

Before reading this chapter, be sure to read Chapter 10 in The Inventor Mentor.

Built-in Highlighting

Inventor provides built-in selection highlighting with the SoXtRenderArea and with two classes derived from SoGLRenderAction: SoBoxHighlight-
RenderAction and SoLineHighlightRenderAction. After a scene graph is rendered, the highlight action renders the path for each object in the selection list. The highlight action is provided to the render area by the application, which calls the setGLRenderAction() method.

Overview

Although Inventor provides predefined highlight styles, at times you may want to highlight selected objects with a different style. You can provide your own highlight style in two ways: derive a new highlight class from SoGLRenderAction (described here), or make changes directly to the scene graph whenever the selection changes (using SoSelection callbacks, as described in Chapter 10 of The Inventor Mentor). If you're using manipulators, it's probably easiest to allow the manipulator to make changes to the scene graph through use of selection callbacks. In this case, the manipulator shows which objects are selected.

Creating a new highlight style by deriving a class from SoGLRenderAction is basically the same as deriving any new action class, as described in Chapter 4. You can use the action-class macros found in SoSubAction.h. The SO_ACTION_HEADER() macro declares type identifier and naming variables and methods that all node classes must support. The SO_ACTION_SOURCE() macro defines the static variables and methods declared in the SO_ACTION_HEADER() macro.

Creating a new highlight action class requires these steps:

  1. Select a name for the new highlight class. It will be derived from SoGLRenderAction or another highlight class.

  2. Define an initClass() method to initialize the type information. Use the SO_ACTION_INIT_CLASS() macro.

  3. Define a constructor. Use the SO_ACTION_CONSTRUCTOR() macro.

  4. Implement the virtual apply() method (see “The apply() Method”).

  5. Implement other methods, if desired, to specify the appearance of the highlight. These optional methods include setColor(), setLinePattern(), and setLineWidth().

The apply() Method

The apply() method of the highlight action renders the scene graph in such a way that selected objects stand out. You could use a two-pass approach, in which the scene is rendered first, and the additional highlights are rendered second. Or your highlight action might render only the selected objects, or render selected objects in a totally different manner (for example, as boxes or spheres). As for any action, the apply() method is an overloaded virtual function (it can take a node, a path, or a path list). The derived highlight class provides stubs for the path and path list forms of the apply() method. Because selection makes the most sense in the context of a scene graph (since the selection list is a list of paths), highlighting paths and path lists is not defined. The function stubs simply call the corresponding apply() method of SoGLRenderAction for paths and path lists.

It's also worth examining the apply() method of the sample classes in more detail, since it uses caching for improved efficiency. As shown in Example 9-2, the apply() method uses a search action to find the selection node and then caches this path. Paths from the root of the scene graph to the selected objects are constructed as follows:

  1. The apply() method first determines whether the cached path to the selection node is still valid. If the root of the new path is the root of the scene graph and the tail is the selection node, it considers the cached path to be valid (since paths are automatically updated when nodes are added or removed).

  2. Next, it checks to be sure that something is selected. The selection node keeps track of the paths to the selected objects. The path from the root to the selection node (in Figure 9-1, the path from Node A to Node B) is prepended to each of the paths maintained in the selection list.

    Figure 9-1. Highlight Path


    If the selection path ends in a node kit, additional work is performed to extend the path all the way down to the selected object. In Figure 9-1, the selection path is from Node B to Node D (a node kit). First, the last kit in the node kit path is obtained (here, Node D). Then, the node kit path is cast to a full path, and the tail of that path is obtained (Node E). Figure 9-1 shows the complete highlight path, which extends from the root of the scene graph to the selection node (Node A to Node B), and from the selection node to the selected object (Node B to Node E).

Creating a Highlight in the Overlay Planes

The following two examples show a class derived from SoGLRenderAction that renders the selected objects as 2D rectangles in the overlay planes. This example illustrates several techniques you will probably use when deriving any new highlight class:

  • Using function “stubs” to call the path and pathList forms of the apply() method of the parent class

  • Caching the path to the selection node

In addition, the OverlayHighlightRenderAction class performs some work that is class-specific. For efficiency, its constructor sets up a scene graph that contains a 2D rectangle. Later, the apply() method changes the values of the rectangle so that it appears in the correct position for the highlighted object. The updateBbox() method is specific to this highlight class. It projects the 3D bounding box for the selected object onto the screen and renders it as a 2D rectangle.

When the constructor creates an SoGLRenderAction, it passes in a dummy viewport region. The SoXtRenderArea will pass the real viewport region to to SoGLRenderAction before the render action's apply() method is called.

Example 9-1 shows the class declaration found in the include file OverlayHighlightRenderAction.h.

Example 9-1. OverlayHighlightRenderAction.h


#include <Inventor/actions/SoGLRenderAction.h>

class SoCamera;
class SoCoordinate3;
class SoOrthographicCamera;
class SoSeparator;
class OverlayHighlightRenderAction : public SoGLRenderAction {
     SO_ACTION_HEADER(OverlayHighlightRenderAction);
  public:
   // Constructor and destructor
   OverlayHighlightRenderAction();
   ~OverlayHighlightRenderAction();
   
   // Applies action to the graph rooted by a node,
   // only drawing selected objects.
   virtual void    apply(SoNode *node);

   // Applies action to the graph defined by a path or path
   // list.
   // These simply invoke the parent class apply() methods.
   // These do NOT highlight the path, whether selected or not.
   // They are implemented to keep the compiler happy.
   virtual void    apply(SoPath *path);
   virtual void    apply(const SoPathList &pathList,
                   SbBool obeysRules = FALSE);

   // This must be called before this class is used.
   static void initClass();
   
  protected:
   void             updateBbox(SoPath *p, SoCamera *c);
   
   // Local scene graph
   SoSeparator          *localRoot;
   SoOrthographicCamera *orthoCam;
   SoCoordinate3        *coords;
   
   // We will cache the path to the first selection node
   SoPath               *selPath;
};

Example 9-2 shows the class definition, found in the source file.

Example 9-2. OverlayHL.c++


#include <limits.h>
#include <math.h>
#include <Inventor/SbBox.h>
#include <Inventor/SoPath.h>
#include <Inventor/SoNodeKitPath.h>
#include <Inventor/actions/SoGetBoundingBoxAction.h>
#include <Inventor/actions/SoSearchAction.h>
#include <Inventor/nodekits/SoBaseKit.h>
#include <Inventor/nodes/SoColorIndex.h>
#include <Inventor/nodes/SoCoordinate3.h>
#include <Inventor/nodes/SoDrawStyle.h>
#include <Inventor/nodes/SoFaceSet.h>
#include <Inventor/nodes/SoLightModel.h>
#include <Inventor/nodes/SoOrthographicCamera.h>
#include <Inventor/nodes/SoPickStyle.h>
#include <Inventor/nodes/SoSelection.h>
#include <Inventor/nodes/SoSeparator.h>

#include "OverlayHighlightRenderAction.h"

SO_ACTION_SOURCE(OverlayHighlightRenderAction);

//    Initializes the OverlayHighlightRenderAction class.
void
OverlayHighlightRenderAction::initClass()
{
   SO_ACTION_INIT_CLASS(OverlayHighlightRenderAction,
                        SoGLRenderAction);
}

//  Constructor
OverlayHighlightRenderAction::OverlayHighlightRenderAction()
      : SoGLRenderAction(SbVec2s(1, 1)) // pass a dummy
                                        // viewport region
{
   SO_ACTION_CONSTRUCTOR(OverlayHighlightRenderAction);

   selPath = NULL;
   
   // Set up the local rendering graph
   localRoot = new SoSeparator;
   localRoot->ref();
   
   SoPickStyle *pickStyle = new SoPickStyle;
   pickStyle->style = SoPickStyle::UNPICKABLE;
   localRoot->addChild(pickStyle);
   
   // Set up camera to look at 0 <= x,y <= 1
   orthoCam = new SoOrthographicCamera;
   orthoCam->position.setValue(.5, .5, 1.);
   orthoCam->height = 1.0;
   localRoot->addChild(orthoCam);
   
   SoLightModel *lmodel = new SoLightModel;
   lmodel->model = SoLightModel::BASE_COLOR;
   localRoot->addChild(lmodel);

   SoColorIndex *color = new SoColorIndex;
   color->index = 1;
   localRoot->addChild(color);

   SoDrawStyle *drawStyle = new SoDrawStyle;
   drawStyle->style = SoDrawStyle::LINES;
   drawStyle->lineWidth = 3;
   drawStyle->linePattern = 0xffff;
   localRoot->addChild(drawStyle);
   
   coords = new SoCoordinate3;
   coords->point.setNum(0);
   localRoot->addChild(coords);
   
   SoFaceSet *fset = new SoFaceSet;
   fset->numVertices = 4;
   localRoot->addChild(fset);
}    

//  Destructor

OverlayHighlightRenderAction::~OverlayHighlightRenderAction()
{
   localRoot->unref();
   if (selPath != NULL)
      selPath->unref();
}    

// Update the bbox to surround the projected bounding box of
// the path.
// Use: protected
void
OverlayHighlightRenderAction::updateBbox(SoPath *p, SoCamera
                                       *camera)
{
   coords->point.deleteValues(0); // clear them all out
   
   if (camera == NULL) return;
   
   // Compute the 3d bounding box of the passed path
   SoGetBoundingBoxAction bba(getViewportRegion());    
   bba.apply(p); 
   SbVec3f min, max;
   bba.getBoundingBox().getBounds(min, max); 
   
   // Project points to (0 <= x,y,z <= 1) screen coordinates
   SbViewVolume vv = camera->getViewVolume();
   SbVec3f screenPoint[8];
   vv.projectToScreen(SbVec3f(min[0], min[1], min[2]),
                     screenPoint[0]);
   vv.projectToScreen(SbVec3f(min[0], min[1], max[2]),
                     screenPoint[1]);
   vv.projectToScreen(SbVec3f(min[0], max[1], min[2]),
                     screenPoint[2]);
   vv.projectToScreen(SbVec3f(min[0], max[1], max[2]),
                     screenPoint[3]);
   vv.projectToScreen(SbVec3f(max[0], min[1], min[2]),
                     screenPoint[4]);
   vv.projectToScreen(SbVec3f(max[0], min[1], max[2]),
                     screenPoint[5]);
   vv.projectToScreen(SbVec3f(max[0], max[1], min[2]),
                     screenPoint[6]);
   vv.projectToScreen(SbVec3f(max[0], max[1], max[2]),
                     screenPoint[7]);
   
   // Find the encompassing 2d box (0 <= x,y <= 1)
   SbBox2f bbox2;
   for (int i = 0; i < 8; i++)
      bbox2.extendBy(SbVec2f(screenPoint[i][0],
                    screenPoint[i][1]));
   
   if (! bbox2.isEmpty()) {
      float xmin, ymin, xmax, ymax;
      bbox2.getBounds(xmin, ymin, xmax, ymax);
                       
      // Set up the coordinate node
      coords->point.set1Value(0,  xmin, ymin, 0);
      coords->point.set1Value(1,  xmax, ymin, 0);
      coords->point.set1Value(2,  xmax, ymax, 0);
      coords->point.set1Value(3,  xmin, ymax, 0);
   }
}

//  beginTraversal - render highlights for our selection node.
void
OverlayHighlightRenderAction::apply(SoNode *renderRoot)
{
   // Do not render the scene - only render the highlights
   
   // Is our cached path still valid?
   if ((selPath == NULL) ||
      (selPath->getHead() != renderRoot) ||
      (! selPath->getTail()->isOfType
                            (SoSelection::getClassTypeId()))) {

      // Find the selection node under the render root
      SoSearchAction sa;
      sa.setFind(SoSearchAction::TYPE);
      sa.setInterest(SoSearchAction::FIRST);
      sa.setType(SoSelection::getClassTypeId());
      sa.apply(renderRoot);
      
      // Cache this path
      if (selPath != NULL)
         selPath->unref();
      selPath = sa.getPath();
      if (selPath != NULL) {
         selPath = selPath->copy();
         selPath->ref();
      }
   }
   
   if (selPath != NULL) {
      // Make sure something is selected.
      SoSelection *sel = (SoSelection *) selPath->getTail();
      if (sel->getNumSelected() == 0) return;
      
      // Keep the length from the root to the selection
      // as an optimization so we can reuse this data.
      int reusablePathLength = selPath->getLength();

      // For each selection path, create a new path rooted 
      // under our localRoot.
      for (int j = 0; j < sel->getNumSelected(); j++) {
         // Continue the path down to the selected object.
         // No need to deal with p[0] since that is the sel
         // node.
         SoPath *p = sel->getPath(j);            
         SoNode *pathTail = p->getTail();

         if ( pathTail->isOfType(SoBaseKit::getClassTypeId())) {
            // Find the last nodekit on the path.
            SoNode *kitTail = ((SoNodeKitPath *)p)->getTail();

            // Extend the selectionPath until it reaches this
            // last kit.
            SoFullPath *fp = (SoFullPath *) p;
            int k = 0;
            do {
               selPath->append(fp->getIndex(++k));
            } 
            while ( fp->getNode(k) != kitTail );
         }
         else {
            for (int k = 1; k < p->getLength(); k++)
               selPath->append(p->getIndex(k));
         }
   
         // Find the camera used to render the selected object.
         SoNode *camera;
         SoSearchAction sa;
         sa.setFind(SoSearchAction::TYPE);
         sa.setInterest(SoSearchAction::LAST);
         sa.setType(SoCamera::getClassTypeId());
         sa.apply(selPath);
         camera = (sa.getPath() == NULL ? NULL :
                                  sa.getPath()->getTail());
         
         // Get the bounding box of the object and update the
         // local highlight graph.
         updateBbox(selPath, (SoCamera *)camera);
         
         // Make sure the box has some size.
         if (coords->point.getNum() == 0) {
           #ifdef DEBUG
            SoDebugError::post
                  ("OverlayHighlightRenderAction::apply",
                   "selected object has no bounding box - cannot
                   render a highlight");
           #endif              
         }
         else {
            // Render the highlight.
            SoGLRenderAction::apply(localRoot);
         }
               
         // Restore selPath for reuse.
         selPath->truncate(reusablePathLength);
      }
   }
}    
// Function stubs: we do not highlight paths and pathLists.

void
OverlayHighlightRenderAction::apply(SoPath *path)
{ SoGLRenderAction::apply(path); }

void
OverlayHighlightRenderAction::apply(const SoPathList &pathList,
                                    SbBool obeysRules)
{ SoGLRenderAction::apply(pathList, obeysRules); }

Example 9-3 shows a main program that uses this new highlight class. The highlight is drawn in the overlay planes, and the scene itself is drawn in the normal planes by a different render action.

Example 9-3. Main Program Using OverlayHighlightRenderAction


#include <X11/StringDefs.h>
#include <X11/Intrinsic.h>

#include <Inventor/SoDB.h>
#include <Inventor/SoInput.h>
#include <Inventor/Xt/SoXt.h>
#include <Inventor/Xt/viewers/SoXtExaminerViewer.h>
#include <Inventor/nodes/SoSelection.h>

#include "OverlayHighlightRenderAction.h"

void
main(int , char *argv[])
{
   // Initialization
   Widget mainWindow = SoXt::init(argv[0]);
   OverlayHighlightRenderAction::initClass();
   
   // Open the data file
   SoInput in;   
   char *datafile = "monitor.iv";
   if (! in.openFile(datafile)) {
     fprintf(stderr, "Cannot open %s for reading.\n", datafile);
     return;
   }

   // Read the input file
   SoNode *n;
   SoSeparator *sep = new SoSeparator;
   while ((SoDB::read(&in, n) != FALSE) && (n != NULL))
     sep->addChild(n);
   
   // Create a selection root to show off our new highlight.
   SoSelection *sel = new SoSelection;
   sel->addChild(sep);

   // Create a viewer.
   SoXtExaminerViewer *viewer = new
                                SoXtExaminerViewer(mainWindow);
   viewer->setSceneGraph(sel);
   
   // Set the overlay scene graph same as normal. For viewers,
   // we have to cast to render area graph.
   viewer->setOverlaySceneGraph(viewer->
                               SoXtRenderArea::getSceneGraph());
   
   viewer->setTitle("Overlay highlight");
   viewer->redrawOverlayOnSelectionChange(sel);
   viewer->setOverlayGLRenderAction(
                              new OverlayHighlightRenderAction);

   // Set up the overlay color map
   SbColor red(1, 0, 0);
   viewer->setOverlayColorMap(1, 1, &red);
   
   viewer->show(); 
   SoXt::show(mainWindow);
   SoXt::mainLoop();
}

Rendering Only Selected Objects

Examples 9-3 and 9-4 show a new class that renders only the selected objects. Rather than drawing a surrogate object as in Examples 9-1 and 9-2, these examples draw the selected objects themselves.

Example 9-4. ShowSelectionRA.h


#include <Inventor/actions/SoGLRenderAction.h>

class SoPath;

class ShowSelectionRenderAction : public SoGLRenderAction {
   SO_ACTION_HEADER(ShowSelectionRenderAction);
  public:
   ShowSelectionRenderAction();
   virtual ~ShowSelectionRenderAction();

   // Applies action to the graph rooted by a node,
   // only drawing selected objects.
   virtual void    apply(SoNode *node);
   
   // Applies action to the graph defined by a path or path
   // list.
   // These simply invoke the parent class apply() methods.
   // These do NOT highlight the path, whether selected or not.
   // They are implemented to keep the compiler happy.
   virtual void    apply(SoPath *path);
   virtual void    apply(const SoPathList &pathList,
                         SbBool obeysRules = FALSE);
   static void initClass();
   
  protected:
   // We will cache the path to the first selection node.
   SoPath           *selPath;
};

Example 9-5. ShowSelectionRA.c++


#include <Inventor/SoPath.h>
#include <Inventor/actions/SoSearchAction.h>
#include <Inventor/nodes/SoBaseColor.h>
#include <Inventor/nodes/SoDrawStyle.h>
#include <Inventor/nodes/SoLightModel.h>
#include <Inventor/nodes/SoNode.h>
#include <Inventor/nodes/SoSelection.h>
#include <Inventor/nodes/SoSeparator.h>
#include <Inventor/nodes/SoTexture2.h>

#include "ShowSelectionRA.h"

SO_ACTION_SOURCE(ShowSelectionRenderAction);

// Initializes the ShowSelectionRenderAction class.
void
ShowSelectionRenderAction::initClass()
{
   SO_ACTION_INIT_CLASS(ShowSelectionRenderAction,
                        SoGLRenderAction);
}

//  Constructor

ShowSelectionRenderAction::ShowSelectionRenderAction()
      : SoGLRenderAction(SbVec2s(1, 1)) 
                        // pass a dummy viewport region
{
   selPath = NULL;
}    

//  Destructor

ShowSelectionRenderAction::~ShowSelectionRenderAction()
{
   if (selPath != NULL)
     selPath->unref();
}    

//  Render the passed scene by searching for the first
//  selection node, then rendering only the selected objects.

void
ShowSelectionRenderAction::apply(SoNode *node)
{
   node->ref();
   
   // Do we have to search for the selection node?
   // Only if our cached path is NULL, 
   // or the action is being applied to a different scene,
   // or the tail of our existing path is no longer a selection
   // node (for instance if that node was removed from the
   // scene).
   if ((selPath == NULL) ||
      (selPath->getHead() != node) ||
      (! selPath->getTail()->isOfType(
           SoSelection::getClassTypeId()))) {
   
     // Find the first selection node under the passed root.
     SoSearchAction sa;
     sa.setFind(SoSearchAction::TYPE);
     sa.setInterest(SoSearchAction::FIRST);
     sa.setType(SoSelection::getClassTypeId());
     sa.apply(node);
   
     // Cache this new path.
     if (selPath != NULL)
       selPath->unref();
     selPath = sa.getPath();
     if (selPath != NULL) {
       selPath = selPath->copy();
       selPath->ref();
     }
   }
   
   // Render the selected paths!
   if (selPath != NULL) {       
     SoSelection *sel = (SoSelection *) selPath->getTail();
     if (sel->getNumSelected() > 0) {
       // Keep the length from the root to the selection
       // as an optimization so we can reuse this data
       int reusablePathLength = selPath->getLength();

       // For each selection path, we need the full path from
       // the passed root to render, else we may not have a
       // camera.
       for (int j = 0; j < sel->getNumSelected(); j++) {
         // Continue the path down to the selected object.
         // No need to deal with p[0] since that is the sel
         // node.
         SoPath *p = sel->getPath(j);
         for (int k = 1; k < p->getLength(); k++)
            selPath->append(p->getIndex(k));

         // Render the selected shape.
         SoGLRenderAction::apply(selPath);
      
         // Restore selPath for reuse.
         selPath->truncate(reusablePathLength);
       }
     }
   }
   
   node->unref();
}    

// Function stubs: we do not highlight paths and pathLists.

void
ShowSelectionRenderAction::apply(SoPath *path)
{ SoGLRenderAction::apply(path); }

void
ShowSelectionRenderAction::apply(const SoPathList &pathList, SbBool obeysRules)
{ SoGLRenderAction::apply(pathList, obeysRules); }

Example 9-6 shows a main program that uses this new highlight class. It creates two viewers that share the same selection node. One viewer uses the GLRenderAction, while the other uses the ShowSelectionRenderAction. Selection changes in one viewer are reflected in the other viewer as well.

Example 9-6. Main Program for ShowSelectionRenderAction


#include <X11/StringDefs.h>
#include <X11/Intrinsic.h>

#include <Inventor/SoDB.h>
#include <Inventor/SoInput.h>
#include <Inventor/Xt/SoXt.h>
#include <Inventor/Xt/viewers/SoXtExaminerViewer.h>
#include <Inventor/nodes/SoSelection.h>

#include "ShowSelectionRA.h"

void
main(int , char *argv[])
{
   // Initialization
   Widget mainWindow = SoXt::init(argv[0]);
   ShowSelectionRenderAction::initClass();
   
   // Open the data file
   SoInput in;   
   char *datafile = "monitor.iv";
   if (! in.openFile(datafile)) {
     fprintf(stderr, "Cannot open %s for reading.\n", datafile);
     return;
   }

   // Read the input file
   SoNode *n;
   SoSeparator *sep = new SoSeparator;
   while ((SoDB::read(&in, n) != FALSE) && (n != NULL))
     sep->addChild(n);
   
   // Create a selection root to show off our new highlight.
   SoSelection *sel = new SoSelection;
   sel->addChild(sep);

   // Create two viewers, one to show the scene, the other
   // to show the selected objects.
   SoXtExaminerViewer *viewer1 = 
                             new SoXtExaminerViewer(mainWindow);
   viewer1->setSceneGraph(sel);
   viewer1->setTitle("Scene");

   SoXtExaminerViewer *viewer2 = new SoXtExaminerViewer();
   viewer2->setSceneGraph(sel);
   viewer2->setGLRenderAction(new ShowSelectionRenderAction());    
   viewer2->redrawOnSelectionChange(sel);
   viewer2->setDecoration(FALSE);
   viewer2->setTitle("Selection");

   viewer1->show();
   viewer2->show();
   
   SoXt::show(mainWindow);
   SoXt::mainLoop();
}