Chapter 15. Draggers and Manipulators

Chapter Objectives

After reading this chapter, you'll be able to do the following:

This chapter describes how to use draggers and manipulators, which are special objects in the scene graph that have a user interface and respond to events. Manipulators, such as the handle box, trackball, and directional light manipulator, are nodes that employ draggers to enable the user to interact with them and edit them. For information on how draggers receive and respond to events, see Chapter 10.

What Is a Dragger?

A dragger is a node in the scene graph with specialized behavior that enables it to respond to user events. All Inventor draggers have a built-in user interface, and they insert geometry into the scene graph that is used for picking and user feedback. Figure 15-1 shows the class tree for dragger classes.

Types of Draggers

For all draggers subclassed from SoDragger, the user employs a click-drag-release motion with the mouse. Table 15-1 indicates the use of each dragger subclassed from SoDragger. For example, the drag-point dragger responds to dragging by translating in three dimensions.

Subclasses of SoDragger fall into two general categories: simple draggers and compound draggers. In general, simple draggers perform only one operation, such as a scale or a translation. Compound draggers perform several operations and are composed of multiple simple draggers. Simple draggers can be used in three ways:

  • You can connect the field of a simple dragger to other fields or to engines in the scene graph. This is a simple way to set up dependencies within the scene graph.

  • You can write callback functions that are performed when interaction starts or finishes, whenever the mouse moves, or when the value in the dragger's field changes.

  • You can use the simple draggers as building blocks to create more complex draggers.

Compound draggers are similar to simple draggers, except that they have more parts because they are comprised of two or more draggers. The SoTransformBoxDragger, for example, uses a scale dragger, three rotators, and six translators.

Figure 15-1. Dragger Classes


Table 15-1. Uses of Draggers

Dragger

Use

SoCenterballDragger

rotation, center

SoDirectionalLightDragger

rotation

SoDragPointDragger

translation

SoHandleBoxDragger

translation, scale

SoJackDragger

rotation, translation, uniform scale (three dimensions)

SoPointLightDragger

translation

SoRotateCylindricalDragger

rotation

SoRotateDiscDragger

rotation

SoRotateSphericalDragger

rotation

SoScale1Dragger

scale (one dimension)

SoScale2Dragger

scale (two dimensions)

SoScaleUniformDragger

uniform scale (three dimensions)

SoScale2UniformDragger

uniform scale (two dimensions)

SoSpotLightDragger

translation, rotation, cone angle

SoTabBoxDragger

scale, translation

SoTabPlaneDragger

scale (two dimensions), translation (two dimensions)

SoTrackballDragger

rotation, scale

SoTransformBoxDragger

rotation, translation, scale

SoTranslate1Dragger

translation (one dimension)

SoTranslate2Dragger

translation (two dimensions)


Manipulators versus Draggers

Manipulators are subclasses of other nodes (such as SoTransform or SoDirectionalLight) that employ draggers (as hidden children) to respond to user events and edit themselves. Figure 15-2 shows the portions of the class tree that contain manipulator classes. Each manipulator contains a dragger that responds directly to user events and in turn modifies the fields of the manipulator. A manipulator inserts geometry into the scene that provides feedback to the user; this geometry is provided by the manipulator's dragger. An SoHandleBoxManip, for example, inserts cubes and lines into the scene that allow the user to edit the scale and translate fields of an SoTransform node by moving the mouse in various ways (see Figure 15-3). This geometry is part of the SoHandleBoxDragger contained within the SoHandleBoxManip manipulator. An SoTrackballManip allows the user to edit the rotation field of an SoTransform node by inserting a sphere surrounded by three ribbons into the scene (see Figure 15-4). The user can then rotate or scale the object inside this trackball.

A dragger moves only itself when it responds to user events. A manipulator, on the other hand, moves itself and affects other objects in the scene graph because, as a subclass of SoTransform or SoLight, it functions as a transform or light node and modifies the traversal state. A dragger supplies geometry and a user interface for a manipulator. A manipulator uses the values it receives from the dragger and copies them into its own fields. When interaction finishes and the manipulator is removed from the scene graph, it copies its values into the transform or light node it was replacing.

Figure 15-2. Manipulator Classes


A manipulator replaces a node in the scene graph, substituting an editable version of that node for the original. When interaction finishes, the original (non-editable) node can be restored. Each manipulator contains a dragger that allows the user to edit its fields. Manipulators derived from SoTransform are as follows:

SoCenterBallManip

SoHandleBoxManip

SoJackManip

SoTabBoxManip

SoTrackballManip

SoTransformBoxManip

Other manipulators include the SoPointLightManip, derived from SoPointLight, and the SoDirectionalLightManip, derived from SoDirectionalLight.

Figure 15-3. Handle-Box Manipulator


Figure 15-4. Trackball Manipulator


Simple Draggers

A simple dragger moves in 3D space in response to click-drag-release mouse events. Its position in space is determined by its position in the scene graph. Each simple dragger has a field that reflects the current state of the dragger. For example, the SoRotateDiscDragger has a rotation field that indicates its current rotation value. This field can be connected to other fields in the scene graph or to the input field of an engine (see the following section). Callback functions can also be used with simple draggers, as described in “Callback Functions”.

Field Connections

A convenient way to use a dragger is to connect its fields to other fields or engines in the scene graph. For example, the SoTranslate1Dragger has a translation field that could be used in a variety of ways. Figure 15-5 shows how this field could be used to edit the radius of a cone node. Since the dragger's translation field is an SoSFVec3f, you need to use an SoDecomposeVec3f engine to extract the x value of the dragger's

translation. This x value is then fed into the bottomRadius field of the cone node. Now, whenever the dragger is translated, the radius of the cone changes.

Figure 15-5. Connecting a Dragger's Field to Another Field in the Scene Graph


Example 15-1 shows the code for connecting these fields and engines. Figure 15-6 shows an image created by this example.

Example 15-1. Using a Simple Dragger


   // Create myDragger with an initial translation of (1,0,0)
   SoTranslate1Dragger *myDragger = new SoTranslate1Dragger;
   root->addChild(myDragger);
   myDragger->translation.setValue(1,0,0);

   // Place an SoCone below myDragger
   SoTransform *myTransform = new SoTransform;
   SoCone *myCone = new SoCone;
   root->addChild(myTransform);
   root->addChild(myCone);
   myTransform->translation.setValue(0,3,0);

   // SoDecomposeVec3f engine extracts myDragger's x-component
   // The result is connected to myCone's bottomRadius.
   SoDecomposeVec3f *myEngine = new SoDecomposeVec3f;
   myEngine->vector.connectFrom(&myDragger->translation);
   myCone->bottomRadius.connectFrom(&myEngine->x);

Figure 15-6. Using a Dragger and Engine to Edit the Radius of a Cone


Callback Functions

Any dragger or manipulator can use callback functions to pass data back to the application. This callback mechanism can be used to augment the default functionality of the dragger or manipulator. Several lists of callback functions and associated data, of class SoCallbackList, are automatically created when a dragger is constructed. You can add functions to and remove functions from these lists and pass a pointer to the user callback data. Draggers use these lists of callback functions:

  • Start callbacks—called when manipulation starts

  • Motion callbacks—called after each mouse movement during manipulation

  • Value-changed callbacks—called when any of the dragger's fields change

  • Finish callbacks—called when manipulation finishes

The following methods add functions to and remove functions from these callback lists:

addStartCallback(functionName, userData)
removeStartCallback(functionName, userData)

addMotionCallback(functionName, userData)
removeMotionCallback(functionName, userData)

addValueChangedCallback(functionName, userData)
removeValueChangedCallback(functionName, userData)

addFinishCallback(functionName, userData)
removeFinishCallback(functionName, userData)

These methods are called on SoDragger. To call one of these methods on the manipulator, call getDragger() first, then call the callback list method.

For example, you could write a start callback function that would turn an object to wireframe during manipulation and a finish callback function that would turn it back to filled drawing style when manipulation finishes. You could write a value-changed callback to find out when the value being manipulated has changed, and then use the getValue() method to obtain the field's new value.

Using Multiple Draggers

Example 15-2 uses three translate1Draggers to change the x, y, and z components of a translation that affects some 3D text. Figure 15-7 shows two images created by this program.

Figure 15-7. A Slider Box That Uses Draggers and Engines to Move Text


Example 15-2. Using Multiple Draggers


//  Uses 3 translate1Draggers to change the x, y, and z 
//  components of a translation. A calculator engine assembles 
//  the components.
//  Arranges these draggers along edges of a box containing the
//  3D text to be moved.
//  The 3D text and the box are made with SoShapeKits

#include <Inventor/engines/SoCalculator.h>
#include <Inventor/nodekits/SoShapeKit.h>
#include <Inventor/nodes/SoCube.h>
#include <Inventor/nodes/SoSeparator.h>
#include <Inventor/nodes/SoText3.h>
#include <Inventor/nodes/SoTransform.h>

#include <Inventor/Xt/SoXt.h>
#include <Inventor/Xt/viewers/SoXtExaminerViewer.h>

#include <Inventor/draggers/SoTranslate1Dragger.h>

main(int , char **argv)
{
   Widget myWindow = SoXt::init(argv[0]);
   if (myWindow == NULL) exit(1);

   SoSeparator *root = new SoSeparator;
   root->ref();

   // Create 3 translate1Draggers and place them in space.
   SoSeparator *xDragSep = new SoSeparator;
   SoSeparator *yDragSep = new SoSeparator;
   SoSeparator *zDragSep = new SoSeparator;
   root->addChild(xDragSep);
   root->addChild(yDragSep);
   root->addChild(zDragSep);
   // Separators will each hold a different transform
   SoTransform *xDragXf = new SoTransform;
   SoTransform *yDragXf = new SoTransform;
   SoTransform *zDragXf = new SoTransform;
   xDragXf->set("translation 0 -4 8");
   yDragXf->set("translation -8 0 8 rotation 0 0 1 1.57");
   zDragXf->set("translation -8 -4 0 rotation 0 1 0 -1.57");
   xDragSep->addChild(xDragXf);
   yDragSep->addChild(yDragXf);
   zDragSep->addChild(zDragXf);

   // Add the draggers under the separators, after transforms
   SoTranslate1Dragger *xDragger = new SoTranslate1Dragger;
   SoTranslate1Dragger *yDragger = new SoTranslate1Dragger;
   SoTranslate1Dragger *zDragger = new SoTranslate1Dragger;
   xDragSep->addChild(xDragger);
   yDragSep->addChild(yDragger);
   zDragSep->addChild(zDragger);

   // Create shape kit for the 3D text
   // The text says 'Slide Arrows To Move Me'
   SoShapeKit *textKit = new SoShapeKit;
   root->addChild(textKit);
   SoText3 *myText3 = new SoText3;
   textKit->setPart("shape", myText3);
   myText3->justification = SoText3::CENTER;
   myText3->string.set1Value(0,"Slide Arrows");
   myText3->string.set1Value(1,"To");
   myText3->string.set1Value(2,"Move Me");
   textKit->set("font { size 2}");
   textKit->set("material { diffuseColor 1 1 0}");

   // Create shape kit for surrounding box.
   // It's an unpickable cube, sized as (16,8,16)
   SoShapeKit *boxKit = new SoShapeKit;
   root->addChild(boxKit);
   boxKit->setPart("shape", new SoCube);
   boxKit->set("drawStyle { style LINES }");
   boxKit->set("pickStyle { style UNPICKABLE }");
   boxKit->set("material { emissiveColor 1 0 1 }");
   boxKit->set("shape { width 16 height 8 depth 16 }");

   // Create the calculator to make a translation
   // for the text. The x component of a translate1Dragger's 
   // translation field shows how far it moved in that 
   // direction. So our text's translation is:
   // (xDragTranslate[0],yDragTranslate[0],zDragTranslate[0])
   SoCalculator *myCalc = new SoCalculator;
   myCalc->ref();
   myCalc->A.connectFrom(&xDragger->translation);
   myCalc->B.connectFrom(&yDragger->translation);
   myCalc->C.connectFrom(&zDragger->translation);
   myCalc->expression = "oA = vec3f(A[0],B[0],C[0])";

   // Connect the the translation in textKit from myCalc
   SoTransform *textXf = (SoTransform *)
            textKit->getPart("transform",TRUE);
   textXf->translation.connectFrom(&myCalc->oA);

   SoXtExaminerViewer *myViewer = new
            SoXtExaminerViewer(myWindow);
   myViewer->setSceneGraph(root);
   myViewer->setTitle("Slider Box");
   myViewer->viewAll();
   myViewer->show();

   SoXt::show(myWindow);
   SoXt::mainLoop();
}

Manipulators

You can use manipulators in your application in various ways:

  • You can use the replaceNode() method to replace certain kinds of nodes in the scene graph with an editable version. When the user is finished manipulating the node, use the replaceManip() method to restore the original node to the scene graph.

  • You can write your own callback functions to use the field values of the manipulator. The callback functions described in “Callback Functions” can be used for any manipulator. (Recall that these functions belong to the dragger, so you need to call getDragger() before using them.)

You can also combine use of these two techniques. For example, you can use replaceNode() to replace an SoTransform with a manipulator. Then you can use a value-changed callback to notify the application when any of the manipulator's dragger fields changes, and the application can use this new value, if desired.

The following sections describe both of these techniques in more detail.

Replacing a Node with a Manipulator

To use any manipulator in an application, follow these basic steps:

  1. Construct the manipulator.

  2. Reference it if you plan on reusing it.

  3. Replace the node in the scene graph with the manipulator. Manipulators derived from SoTransform, such as the handle box and trackball, replace an SoTransform node. An SoDirectionalLight-
    Manip
    replaces an SoDirectionalLight node, an SoPointLightManip replaces an SoPointLight node, and so on.

Replacing a Node

The replaceNode() method takes a path as an argument:

replaceNode(SoPath *p)

The path is supplied by the application. For example, Figure 15-8 shows the path to a target SoTransform node. When a transform manipulator replaces this node, editing the manipulator will affect cube2 in the scene graph.

Manipulators subclassed from SoTransformManip use special nodes to maintain their shape (so that the trackball remains spherical, for example) and to ensure that they surround the shape objects they affect. These nodes are described in The Inventor Toolmaker.

Figure 15-8. Specifying the Path to the Target Node


Removing the Manipulator

To remove the manipulator from the scene graph:

  1. Use the replaceManip() method to restore the original node to the scene graph. In the example, the field values from the manipulator are copied into the transform node.

  2. Use unref() on the manipulator so that it will be deleted.

Because the manipulator methods replaceManip() and replaceNode() exchange the new node for the tail of the given path, you can reuse the path for subsequent calls to these methods.

For example, if we begin with:

myManip = new SoTrackballManip;
myPathToTransform = createPathtoTransform(pickPath);

Then we can call:

myManip->replaceNode(myPathToTransform);

to put the manipulator at the end of the path.

Later, we can call

myManip->replaceManip(myPathToTransform, new SoTransform);

to remove the manipulator and replace it with a transform.

Using the replaceNode() Method

Example 15-3 displays a cube, a sphere, and a lamp. The lamp is read from a file and inserted as the “contents” part of an SoWrapperKit. When the user picks the cube, a trackball replaces the transform node that affects the cube. When the user picks the sphere, a handle box replaces the transform node that affects the sphere. When the user picks the lamp, a transform box replaces the “transform” part of the wrapper kit containing the lamp. Figure 15-9 shows an image created by this program. This example shows the following techniques:

  • Using replaceNode() and replaceManip() to make certain nodes in the scene graph editable and to restore the original nodes when manipulation finishes

  • Using selection callbacks (see Chapter 10)

    Example 15-3. Using Manipulators to Transform Objects


    // Note that for illustration purposes, the
    // cube and SoWrapperKit already have transform nodes 
    // associated with them; the sphere does not. In all cases, 
    // the routine createTransformPath() is used to find the 
    // transform node that affects the picked object.
    
    #include <Inventor/SoDB.h>
    #include <Inventor/SoInput.h>
    #include <Inventor/manips/SoHandleBoxManip.h>
    #include <Inventor/manips/SoTrackballManip.h>
    #include <Inventor/manips/SoTransformBoxManip.h>
    #include <Inventor/nodekits/SoWrapperKit.h>
    #include <Inventor/nodes/SoCamera.h>
    #include <Inventor/nodes/SoCube.h>
    #include <Inventor/nodes/SoGroup.h>
    #include <Inventor/nodes/SoLight.h>
    

    Figure 15-9. Adding Manipulators to a Scene


    #include <Inventor/nodes/SoMaterial.h>
    #include <Inventor/nodes/SoSelection.h>
    #include <Inventor/nodes/SoSphere.h>
    #include <Inventor/nodes/SoTransform.h>
    
    #include <Inventor/Xt/SoXt.h>
    #include <Inventor/Xt/viewers/SoXtExaminerViewer.h>
    
    // function prototypes
    void selectionCallback(void *, SoPath *);
    void deselectionCallback(void *, SoPath *);
    void dragStartCallback(void *, SoDragger *);
    void dragFinishCallback(void *, SoDragger *);
    
    // global data
    SoSeparator *root;
    SoHandleBoxManip    *myHandleBox;
    SoTrackballManip    *myTrackball;
    SoTransformBoxManip *myTransformBox;
    SoPath *handleBoxPath    = NULL;
    SoPath *trackballPath    = NULL;
    SoPath *transformBoxPath = NULL;
    
    main(int, char **argv)
    {
       // Initialize Inventor and Xt
       Widget myWindow = SoXt::init(argv[0]);
       if (myWindow == NULL) exit(1);
    
       // Create and set up the selection node
       SoSelection *selectionRoot = new SoSelection;
       selectionRoot->ref();
       selectionRoot->
          addSelectionCallback(selectionCallback, NULL);
       selectionRoot->
          addDeselectionCallback(deselectionCallback, NULL);
    
       // Create the scene graph
       root = new SoSeparator;
       selectionRoot->addChild(root);
    
       // Read a file into contents of SoWrapperKit 
       // Translate it to the right.
       SoWrapperKit *myWrapperKit = new SoWrapperKit;
       root->addChild(myWrapperKit);
       SoInput myInput;
       if (!myInput.openFile("luxo.iv")) 
          return (1);
       SoSeparator *objectFromFile = SoDB::readAll(&myInput);
       if (objectFromFile == NULL) return (1);
       myWrapperKit->setPart("contents",objectFromFile);
       myWrapperKit->set("transform { translation 3 -1 0 }");
       SoMaterial *wrapperMat 
          = (SoMaterial *) myWrapperKit->getPart("material",TRUE);
       wrapperMat->diffuseColor.setValue(.8, .8, .8);
    
       // Create a cube with its own transform.
       SoSeparator *cubeRoot = new SoSeparator;
       SoTransform *cubeXform = new SoTransform;
       cubeXform->translation.setValue(-4, 0, 0);
       root->addChild(cubeRoot);
       cubeRoot->addChild(cubeXform);
    
       SoMaterial *cubeMat = new SoMaterial;
       cubeMat->diffuseColor.setValue(.8, .8, .8);
       cubeRoot->addChild(cubeMat);
       cubeRoot->addChild(new SoCube);
    
       // Add a sphere node without a transform
       // (one will be added when we attach the manipulator)
       SoSeparator *sphereRoot = new SoSeparator;
       SoMaterial *sphereMat = new SoMaterial;
       root->addChild(sphereRoot);
       sphereRoot->addChild(sphereMat);
       sphereRoot->addChild(new SoSphere);
       sphereMat->diffuseColor.setValue(.8, .8, .8);
    
       // Create the manipulators
       myHandleBox = new SoHandleBoxManip;
       myHandleBox->ref();
       myTrackball = new SoTrackballManip;
       myTrackball->ref();
       myTransformBox = new SoTransformBoxManip;
       myTransformBox->ref();
    
       // Get the draggers and add callbacks to them. Note
       // that you don't put callbacks on manipulators. You put
       // them on the draggers which handle events for them. 
       SoDragger *myDragger;
       myDragger = myTrackball->getDragger();
       myDragger->addStartCallback(dragStartCallback,cubeMat);
       myDragger->addFinishCallback(dragFinishCallback,cubeMat);
    
       myDragger = myHandleBox->getDragger();
       myDragger->addStartCallback(dragStartCallback,sphereMat);
       myDragger->addFinishCallback(dragFinishCallback,sphereMat);
    
       myDragger = myTransformBox->getDragger();
       myDragger->addStartCallback(dragStartCallback,wrapperMat);
       myDragger->addFinishCallback(dragFinishCallback,wrapperMat);
    
       SoXtExaminerViewer *myViewer 
          = new SoXtExaminerViewer(myWindow);
       myViewer->setSceneGraph(selectionRoot);
       myViewer->setTitle("Attaching Manipulators");
       myViewer->show();
       myViewer->viewAll();
    
       SoXt::show(myWindow);
       SoXt::mainLoop();
    }
    
    // Is this node of a type that is influenced by transforms?
    SbBool
    isTransformable(SoNode *myNode)
    {
       if (myNode->isOfType(SoGroup::getClassTypeId())
          || myNode->isOfType(SoShape::getClassTypeId())
          || myNode->isOfType(SoCamera::getClassTypeId())
          || myNode->isOfType(SoLight::getClassTypeId()))
          return TRUE;
       else 
          return FALSE;
    }
    
    //  Create a path to the transform node that affects the tail
    //  of the input path.  Three possible cases:
    //   [1] The path-tail is a node kit. Just ask the node kit for
    //       a path to the part called "transform"
    //   [2] The path-tail is NOT a group.  Search siblings of path
    //       tail from right to left until you find a transform. If
    //       none is found, or if another transformable object is 
    //       found (shape,group,light,or camera), then insert a 
    //       transform just to the left of the tail. This way, the 
    //       manipulator only affects the selected object.
    //   [3] The path-tail IS a group.  Search its children left to
    //       right until a transform is found. If a transformable
    //       node is found first, insert a transform just left of 
    //       that node.  This way the manip will affect all nodes
    //       in the group.
    SoPath *
    createTransformPath(SoPath *inputPath)
    {
       int pathLength = inputPath->getLength();
       if (pathLength < 2) // Won't be able to get parent of tail
          return NULL;
    
       SoNode *tail = inputPath->getTail();
    
       // CASE 1: The tail is a node kit.
       // Nodekits have built in policy for creating parts.
       // The kit copies inputPath, then extends it past the 
       // kit all the way down to the transform. It creates the
       // transform if necessary.
       if (tail->isOfType(SoBaseKit::getClassTypeId())) {
          SoBaseKit *kit = (SoBaseKit *) tail;
          return kit->createPathToPart("transform",TRUE,inputPath);
       }
    
       SoTransform *editXf = NULL;
       SoGroup     *parent;
       SbBool      existedBefore = FALSE;
    
       // CASE 2: The tail is not a group.
       SbBool isTailGroup;
       isTailGroup = tail->isOfType(SoGroup::getClassTypeId());
       if (!isTailGroup) {
          // 'parent' is node above tail. Search under parent right
          // to left for a transform. If we find a 'movable' node
          // insert a transform just left of tail.  
          parent = (SoGroup *) inputPath->getNode(pathLength - 2);
          int tailIndx = parent->findChild(tail);
    
          for (int i = tailIndx; (i >= 0) && (editXf == NULL);i--){
             SoNode *myNode = parent->getChild(i);
             if (myNode->isOfType(SoTransform::getClassTypeId()))
                editXf = (SoTransform *) myNode;
             else if (i != tailIndx && (isTransformable(myNode)))
                break;
          }
          if (editXf == NULL) {
             existedBefore = FALSE;
             editXf = new SoTransform;
             parent->insertChild(editXf, tailIndx);
          }
          else
             existedBefore = TRUE;
       }
       // CASE 3: The tail is a group.
       else {
          // Search the children from left to right for transform 
          // nodes. Stop the search if we come to a movable node
          // and insert a transform before it.
          parent = (SoGroup *) tail;
          for (int i = 0;
             (i < parent->getNumChildren()) && (editXf == NULL); 
             i++) {
             SoNode *myNode = parent->getChild(i);
             if (myNode->isOfType(SoTransform::getClassTypeId()))
                editXf = (SoTransform *) myNode;
             else if (isTransformable(myNode))
                break;
          }
          if (editXf == NULL) {
             existedBefore = FALSE;
             editXf = new SoTransform;
             parent->insertChild(editXf, i);
          }
          else 
             existedBefore = TRUE;
       }
    
       // Create 'pathToXform.' Copy inputPath, then make last
       // node be editXf.
       SoPath *pathToXform = NULL;
       pathToXform = inputPath->copy();
       pathToXform->ref();
       if (!isTailGroup) // pop off the last entry.
          pathToXform->pop();
       // add editXf to the end
       int xfIndex   = parent->findChild(editXf);
       pathToXform->append(xfIndex);
       pathToXform->unrefNoDelete();
    
       return(pathToXform);
    }
    
    // This routine is called when an object
    // gets selected. We determine which object
    // was selected, then call replaceNode()
    // to replace the object's transform with
    // a manipulator.
    void
    selectionCallback(
       void *, // user data is not used
       SoPath *selectionPath)
    {
       // Attach the manipulator.
       // Use the convenience routine to get a path to
       // the transform that affects the selected object.
       SoPath *xformPath = createTransformPath(selectionPath);
       if (xformPath == NULL) return;
       xformPath->ref();
    
       // Attach the handle box to the sphere,
       // the trackball to the cube
       // or the transformBox to the wrapperKit
       if (selectionPath->getTail()->isOfType(
            SoSphere::getClassTypeId())) {
          handleBoxPath = xformPath;
          myHandleBox->replaceNode(xformPath);
       }
       else if (selectionPath->getTail()->
            isOfType(SoCube::getClassTypeId())) {
          trackballPath = xformPath;
          myTrackball->replaceNode(xformPath);
       }
       else if (selectionPath->getTail()->
            isOfType(SoWrapperKit::getClassTypeId())) {
          transformBoxPath = xformPath;
          myTransformBox->replaceNode(xformPath);
       }
    }
    
    // This routine is called whenever an object gets
    // deselected. It detaches the manipulator from
    // the transform node, and removes it from the 
    // scene graph that will not be visible.
    void
    deselectionCallback(
       void *, // user data is not used
       SoPath *deselectionPath)
    {
       if (deselectionPath->getTail()->
            isOfType(SoSphere::getClassTypeId())) {
          myHandleBox->replaceManip(handleBoxPath,NULL);
          handleBoxPath->unref();
       }
       else if (deselectionPath->getTail()->
            isOfType(SoCube::getClassTypeId())) {
          myTrackball->replaceManip(trackballPath,NULL);
          trackballPath->unref();
       }
       else if (deselectionPath->getTail()->
            isOfType(SoWrapperKit::getClassTypeId())) {
          myTransformBox->replaceManip(transformBoxPath,NULL);
          transformBoxPath->unref();
       }
    }
    
    // This is called when a manipulator is
    // about to begin manipulation.
    void
    dragStartCallback(
       void *myMaterial, // user data
       SoDragger *)         // callback data not used
    {
       ((SoMaterial *) myMaterial)->diffuseColor=SbColor(1,.2,.2);
    }
    
    // This is called when a manipulator is
    // done manipulating.
    void
    dragFinishCallback(
       void *myMaterial, // user data
       SoDragger *)    // callback data not used
    {
       ((SoMaterial *) myMaterial)->diffuseColor=SbColor(.8,.8,.8);
    }
    

Customizing a Dragger (Advanced)

This section describes how to modify the appearance of a dragger. This customization is performed either by changing the default geometry for a part or by changing the part after an instance of a dragger has been built. Although the look and feel of a dragger can be changed or removed in this manner, no new functionality can be added.

Using the SoTrackballDragger as an example, this section describes the parts of a dragger and how they combine to make the whole dragger. It explains how the geometry for each part can be changed or removed, and how that can affect the functionality of the dragger. Example 15-4 illustrates how parts of an SoTranslate1Dragger can be changed after it has been built.

Parts of a Dragger

Every dragger is a node kit that is constructed out of parts. A part is simply a piece of the dragger that has some task associated with it. Often, two parts act as a pair. One part is displayed when it is in use (or active), and the other is displayed when that part is not in use (or inactive). For example, for the

trackball's inactive “XRotator” part, a white stripe is displayed, and for its active “XRotatorActive” part, a yellow stripe is displayed.

Each dragger has a resource file associated with it that contains an Inventor scene graph describing the default geometry for each part. By creating a new resource file, you can override the default and give the part a new shape or new properties such as color or drawing style. In the resource file, scene graphs are labeled with their unique resource names.

Many classes of draggers use the same part names. For example, the trackball, rotate-disc, rotate-cylindrical, and rotate-spherical draggers each have a part named “rotator.” Since the default parts are stored in the global dictionary, each part in each class must have a unique resource name. In all cases, the class name (without the “So” or “Dragger”) is prepended to the part name. Table 15-2 shows how the resource names and part names relate.

For example, the SoTrackballDragger has twelve parts. Table 15-2 lists the resource and part names of eight of these parts (for brevity, the “userRotator” and “userAxis” parts are omitted). When you interact with a trackball dragger, you are actually interacting with its parts. For example, if the mouse goes down over the trackball's “XRotator” part, a rotation about the x-axis is initiated.

Table 15-2. Selected Parts of the SoTrackballDragger

Resource Names

Part Names

Task

trackballRotator

trackball RotatorActive

rotator
rotatorActive

Free rotation

trackballXRotator

trackballXRotatorActive

XRotator
XRotatorActive

Rotation about x-axis

trackballYRotator

trackballYRotatorActive

YRotator
YRotatorActive

Rotation about y-axis

trackballZRotator

trackballZRotatorActive

ZRotator
ZRotatorActive

Rotation about z-axis


Changing a Part after Building the Dragger

To change the part of a dragger that has already been built, use the setPart() or setPartAsPath() method provided by SoInteractionKit. The setPart() method takes the root of a scene graph as a parameter, while setPartAsPath() accepts an SoPath.

For example, to change the “rotator” part of myDragger:

myDragger->setPart("rotator", myNewRotatorSceneGraph);

To change the “rotator” part of a dragger within a manipulator:

myManip->getDragger()->setPart("rotator",
                             myNewRotatorSceneGraph);

You can also provide setPartAsPath() with the path to a particular instance of an object in the scene graph. The dragger then uses that object for the part. For example, if you have an arrow used as a weather vane mounted on a post, you could provide the path to the arrow and rotate the arrow itself. (Note the difference here between specifying a node and specifying a path. If you specify the arrow node using setPart(), a new instance of that node is created and two copies of the same geometry appear on the screen. If you specify the path to the arrow using setPartAsPath(), the dragger actually uses the existing arrow node and waits for the user to press the mouse on the same weather vane that is sitting on the barn.)

myRotateManip->getDragger()->setPartAsPath("rotator",
                           pathToMyWeatherVaneArrow);

Example 15-4 shows how to change the geometry of the draggers in Example 15-2. The “translator” and “translatorActive” parts are now cubes instead of arrows. The setPart() method is used to replace the default parts with the new scene graphs specified here. Figure 15-10 shows the new dragger geometry.

Figure 15-10. Changing the Dragger Parts to Cubes


Example 15-4. Changing Parts after Building a Dragger


// Create myTranslator and myTranslatorActive.
// These are custom geometry for the draggers.
SoSeparator *myTranslator = new SoSeparator;
SoSeparator *myTranslatorActive = new SoSeparator;
myTranslator->ref();
myTranslatorActive->ref();
   
// Materials for the dragger in regular and active states
SoMaterial *myMtl = new SoMaterial;
SoMaterial *myActiveMtl = new SoMaterial;
myMtl->diffuseColor.setValue(1,1,1);
myActiveMtl->diffuseColor.setValue(1,1,0);
myTranslator->addChild(myMtl);
myTranslatorActive->addChild(myActiveMtl);
   
// Same shape for both versions.
SoCube *myCube = new SoCube;
myCube->set("width 3 height .4 depth .4");
myTranslator->addChild(myCube);
myTranslatorActive->addChild(myCube);

// Now, customize the draggers with the pieces we created.
xDragger->setPart("translator",myTranslator);
xDragger->setPart("translatorActive",myTranslatorActive);
yDragger->setPart("translator",myTranslator);
yDragger->setPart("translatorActive",myTranslatorActive);
zDragger->setPart("translator",myTranslator);
zDragger->setPart("translatorActive",myTranslatorActive);

Changing the Default Geometry for a Part

Every class of dragger has a resource file associated with it that contains Inventor scene graphs defining default geometry for that class. The default geometry for a given class is also compiled in, so that if the dragger resource files are lost, the dragger will still operate.

Where a Dragger Looks for Defaults

When a dragger is constructed, it checks whether a resource file for overriding the defaults has been created. When reading from this file (if found), if the dragger encounters a second definition of a particular geometry, the new geometry replaces any previously defined geometry of the same name.

Inventor will look for files only if the environment variable SO_DRAGGER_DIR has been set. If it has, Inventor will look in that directory.

In all cases, a given dragger class will read only files of its same name: the SoTranslate1Dragger class reads only resource files named translate1Dragger.iv, the SoTrackballDragger class reads only files named trackballDragger.iv.

Changing the default geometry of a dragger part is a simple matter of creating a new file that contains a scene graph defining the new resource. When encountered, this new definition overrides the default definitions of that resource. The next two sections show how to do this using the SoTrackballDragger as a foundation.

How to Edit Your File

To change the default geometry of a part, you merely redefine the scene graph for the geometry used by the part. When creating new resource files, it is necessary to define only the geometry that you wish to change. Other geometry will use the default values.

Keep in mind that you should never edit the resource files in Inventor/resources. If you want your application to use alternate resources, put your files in a convenient place and set the SO_DRAGGER_DIR environment variable to point there.

As an example, let's replace the trackballRotator resource of the trackball with a cube. (For more information on the Inventor file format, see Chapter 11.) Looking at the default geometry file for the trackball, we see that the trackballRotator resource is defined by this scene graph:

# default geometry for SoTrackballDragger's "rotator" part (inactive)
DEF trackballRotator Separator {
   DrawStyle { style INVISIBLE }
   Sphere {}
}
# default geometry for SoTrackballDragger's "rotatorActive" part
DEF trackballRotatorActive Separator {
   DrawStyle { style INVISIBLE }
   Sphere {}
}

Note that, in the case of the trackball, the resources specify that the rotator and rotatorActive parts have the same geometry, an invisible sphere. Although this is common, some draggers may have completely different geometry for when they are inactive and active (and most manipulators have more complicated scene graphs than just a sphere).

To change the trackballRotator and trackballRotatorActive resources from an invisible sphere to a visible cube, you simply replace the sphere with a cube in both scene graphs:

# default geometry for the SoTrackballDragger's "rotator" part
DEF trackballRotator Separator {
   BaseColor {
      rgb 1. 1. 1.        #white
   }
   Cube {}
}
# default geometry for the SoTrackballDragger's "rotatorActive" part
DEF trackballRotatorActive Separator {
   BaseColor {
      rgb .5 .5  0.        #yellow
   }
   Cube {}
}

Using this mechanism, you can not only change the geometry of a given part, but also remove the functionality of that part entirely. For example, to disable the trackball's "rotator" part but still leave the cube visible, you can make the cube unpickable:

# default geometry for the SoTrackballDragger's "rotator" part
DEF trackballRotator Separator {
   BaseColor {
      rgb 1. 1. 1.        #white
   }
   PickStyle {
      style UNPICKABLE
   }
   Cube {}
}
# default geometry for the SoTrackballDragger's "rotatorActive" part
DEF trackballRotatorActive Separator {
   BaseColor {
      rgb .5 .5  0.        #yellow
   }
   PickStyle {
      style UNPICKABLE
   }
   Cube {}
}

To remove the trackball's rotator part altogether, leaving a trackball that can only rotate about its x, y, and z axes, you could redefine its geometry to be an empty scene graph:

# default geometry for SoTrackballDragger's "rotator" part
DEF trackballRotator Separator {
}
# default geometry for SoTrackballDragger's "rotatorActive" part
DEF trackballRotatorActive Separator {
}

You can also read the geometry from a file instead of defining it inline:

DEF trackballRotator Separator {
   File { name "myCustomRotator.iv" }
}
DEF trackballRotatorActive Separator {
   File { name "myCustomRotatorActive.iv" }
}


Note: Never set a dragger part to NULL. Internal methods require a node to be present, even if it's simply an empty separator as shown in the previous example. (Often, the dragger parts are the children of a switch node. Changing a node to NULL could result in an incorrect ordering of the switch node's children.)