Chapter 8. Creating Draggers and Manipulators

This chapter shows how to create new draggers and manipulators. The first half of the chapter deals with creating new draggers, which are a special type of node kit with a user interface. The second half of the chapter describes creating new manipulators, which are nodes that employ draggers.

Before reading this chapter, you should first read and thoroughly understand Chapter 15 in The Inventor Mentor. Also, because draggers are node kits, you should be familiar with Chapter 14 in The Inventor Mentor and Chapter 7 in this book. If you want to create a new manipulator that uses existing draggers, you can skip directly to “Creating a Manipulator”.

The chapter examples show how to create four new classes:

The first two classes created in this chapter are derived from the SoDragger class. The first class, TranslateRadialDragger, shows how to create a simple dragger, which typically performs only one operation such as scaling or translating. The second class, RotTransDragger, shows how to combine simple draggers into a compound dragger that can perform more than one operation. The third class, RotTransManip, shows how to create a new manipulator that is a subclass of SoTransformManip. The last class, Coord3Manip, shows how to create a manipulator that is subclassed from SoCoordinate3 and uses an existing dragger, the SoDragPointDragger.

Creating a Simple Dragger

This section describes the design and implementation of a simple dragger. Simple draggers perform one operation, such as rotating, translating, or scaling, and have a fixed user interface. Although simple draggers can be useful by themselves, they are often combined to make a compound dragger or a manipulator, as described in later sections.

The TranslateRadialDragger allows the user to translate the dragger along a line. The direction of the line is determined by the center of the dragger and the point on the dragger that the user first hits. By default, the dragger geometry is a sphere. When the user begins manipulating, an arrow appears in the direction of motion. (Changing the default geometry for a dragger without writing a new dragger is discussed in Chapter 15 of The Inventor Mentor. With this technique, any user can replace the arrow with a line or a bolt of lightning, for example.)

Overview

The SoDragger class, a node kit, constructs the nodes shown in Figure 8-1. The motion matrix for a simple dragger is typically a translation or a rotation matrix. This matrix is updated by the dragger as it responds to mouse motion. Changes in the motion matrix cause the dragger geometry to move on the screen. In the case of the TranslateRadialDragger, the motion matrix is a translation matrix. When this matrix is updated, it moves the dragger along its line in response to the user's dragging the mouse.

Each dragger has a field that reflects its state. The TranslateRadialDragger performs translations, so it has a translation field. The value of this field must be in sync with the value of the motion matrix. When you construct the dragger, you set it up so that whenever the motion matrix is updated, the dragger's translation field is updated, and vice versa. To update the translation field when the dragger moves, you use a value-changed callback. (This is the typical case.) To update the motion matrix when the translation field changes, you attach a field sensor to the translation field. (This case is less typical.)

Figure 8-1. Structure of SoDragger


This discussion of draggers and manipulators uses the terms world space and local space. World space is the transformation space at the top of the scene graph. Local space (for the dragger) is the space after the motion matrix. This is typically where you perform calculations for positioning and moving the dragger. When draggers perform projections, they typically need to know the matrix to convert from local space to world space. The getLocalToWorldMatrix() method on SoDragger is a convenience method for transforming between these two spaces. Other convenience methods are

  • getWorldToLocalMatrix()

  • transformMatrixLocalToWorld()

  • transformMatrixWorldToLocal()

The geomSeparator node also plays an important part in the functioning
of the dragger. To facilitate caching, its children should not change during manipulation. (They can, however, change at the start or finish of user manipulation.)

Before you begin writing code for the new class, answer the following questions:

  • What should the dragger look like?

  • How should the dragger move?

  • How do you want to interpret the dragger's motion?

The file SoSubkit.h contains the macros for defining new node-kit classes. Since SoDragger is a node kit, you'll be using macros from this file. (Also, because you are deriving a class from a node kit class, you do not need to include this file.) The SO_KIT_HEADER() macro declares type identifier and naming variables and methods that all node kit classes must support. The SO_KIT_SOURCE() macro defines the static variables and methods declared in the SO_KIT_HEADER() macro. Creating a new dragger follows the same general pattern as creating any new node kit, but with some additional steps, as follows:

  1. Select a name for the new dragger class and decide what class it is derived from. (It will usually be derived from SoDragger.)

  2. Determine what kind of motion the dragger will perform and add the appropriate field (for a simple dragger) or fields (for a complex dragger). Examples of fields added to the dragger are translation, rotation, and scaleFactor (see “Creating the Field”).

  3. Design each part in the catalog. Use the SO_KIT_CATALOG_-
    ENTRY_HEADER() macro in the header file and SO_KIT_ADD_CATALOG_ENTRY() macro in the source file (see “Designing the Parts”).

  4. For the parts in the catalog that determine the user interface:

    a. Define a unique resource name.

    b. Create a default geometry file
    (see “Creating the Default Geometry File”).

    c. Create the compiled-in geometry using the special utility program
    provided (see “Creating the Compiled-in Default Geometry”).

  5. Define an initClass() method to initialize the type information (see “Initializing the Dragger Class”).

  6. Determine what kind of projector models your interpretation of the mouse motion. For example, if your dragger moves along a line, use a line projector. It if moves in a plane, use a plane projector. If it rotates about an axis, use a cylinder projector. If it moves along the surface of a sphere, use a sphere projector. (The projector is declared in the .h file, created in the constructor, and used in the drag methods.) See “Creating the Projector”.

  7. Define a constructor (see “Constructor”).

  8. Implement dragStart(), drag(), and dragFinish() methods. These routines perform the dragging (see “Dragging Callback Functions”).

  9. Implement the value-changed callback function. This function updates the dragger's field to reflect the dragger's current position.

  10. Implement the field sensor callback function. This function causes the dragger to reposition itself whenever changes are made to its field.

  11. Implement the setUpConnections() method, which attaches and detaches field sensors.

  12. Define a destructor (see “Destructor”).

Designing the Parts

Our TranslateRadialDragger has four public parts and three internal parts. The first two parts, the translator part and the translatorActive part, are the geometry the user actually interacts with. The second two parts, the feedback part and the feedbackActive part, are passive parts that indicate the direction of translation. In general, users cannot interact with feedback parts; often, these parts do not appear until after manipulation has begun. The internal parts are the switch nodes that switch between the active and inactive pairs of parts and the feedbackRotate part, which orients the feedback geometry.

The subclass TranslateRadialDragger adds additional nodes to the base class, as shown in Figure 8-2.

Creating the Default Geometry File

Once we've identified our parts, we need to design the geometry for them. The golden rule here is to start simple. For now, use easy primitive shapes for your part geometry; there will be plenty of time to tweak them later. Example 8-1 shows the default geometry file for the TranslateRadialDragger (see Chapter 11 in The Inventor Mentor for more information on the Inventor file format).

Example 8-1. translateRadialDragger.iv


#Inventor V2.0 ascii

# Geometry resource file for the TranslateRadialDragger

DEF translateRadialTranslator Separator {
   Material { diffuseColor .6 .6 .6 }
   DrawStyle { style LINES }
   Sphere { radius 1.732 }
}
DEF translateRadialTranslatorActive Separator {
   Material { diffuseColor .6 .6 0 }
   DrawStyle { style LINES }
   Sphere { radius 1.732 }
}

# Don't show anything for feedback during inactive state
DEF translateRadialFeedback Separator { }

DEF translateRadialFeedbackActive Separator {
   Material { diffuseColor .5  .9 .9 }
   # An arrow aligned with the x axis.

   RotationXYZ {
      axis Z
      angle 1.57079
   }
   Separator {

      #stick
      Cylinder { height 4.0 radius 0.05 }

      #left arrowhead
      Translation { translation 0 2.2 0 }
      Cone { height 0.4 bottomRadius 0.2 }

      #right arrowhead
      Translation { translation 0 -4.4 0 }
      RotationXYZ { axis Z angle 3.14159 }
      Cone { height 0.4 bottomRadius 0.2 }
   }
}

Figure 8-2. Structure of TranslateRadialDragger


In general, follow these conventions when designing the parts of your dragger:

  • Give the default geometry file the same name as the dragger. In this case, it is translateRadialDragger.iv.

  • In the node-kit catalog, give parts active names like “translator” and “rotator.” Don't name them “cube” and “arrow”; the user may change the cube geometry to be a sphere, or the arrow geometry to a be poodle.

  • Prefix the resource name (that is, the name used in the default geometry file and the global dictionary) with the base name of the dragger. (Lots of draggers could have a part named “rotator.”) Also, if the geometry is for the active portion of a part, suffix it with Active. For example, we have translateRadialTranslatorActive.

Creating the Compiled-in Default Geometry

It's a harsh, cruel world. The reality of end-user environments is that sometimes resource files, such as a dragger's default geometry file, are accidentally lost or deleted. If the dragger is to continue to operate in this situation, we need to compile in a copy of the default geometry with the dragger.

To compile the default geometry along with the dragger, follow these steps:

  1. Use the special utility program provided with Inventor, ivToIncludeFile, which translates an .iv file (such as the one shown in Example 8-1) into binary and then writes the binary version as an array of hexadecimal numbers. Once created, you can read this memory buffer directly into Inventor.

    For example, to create the geometry buffer for the translate radial dragger, the command is

    ivToIncludeFile TranslateRadialDragger::geomBuffer 
            < translateRadialDragger.iv 
            > TranslateRadialDraggerGeom.h
    

    This utility, provided with Inventor, creates a file called TranslateRadialDraggerGeom.h that looks like this:

    char TranslateRadialDragger::geomBuffer[]= { ... }
    

    The braces contain the hexadecimal version of the binary format for the .iv scene graph file. By convention, the name of the file containing the geometry is the name of the manipulator, followed by Geom.h.

  2. In the include file, within the class definition of the dragger, declare the static variable geomBuffer:

    static const char geomBuffer[];
    

  3. In the source file, include the compiled-in version:

    #include  "geom/TranslateRadialDraggerGeom.h"
    

After you've created the dragger, you can modify the resource file (TranslateRadialDragger.iv). See Chapter 15 in The Inventor Mentor for information on how to change the resource file without recompiling. When you are finished modifying the resource file, run the translation program and recompile the dragger; the geometry becomes a permanent part of your dragger.

Initializing the Dragger Class

Define the initClass() method using the SO_KIT_INIT_CLASS() macro. Note that for existing Inventor draggers, initClass() is called by

SoInteraction::init()

For any draggers or manipulators that you add to the system, you will need to call yourDragger::initClass() from the application after the call to SoInteraction::init(), or after anything that calls SoInteraction::init(), such as SoXt::init().

Constructor

Every simple dragger should declare a constructor that causes the default geometry for this dragger to be used. Let's briefly examine each step in the constructor:

a. Use SO_KIT_CONSTRUCTOR() to set up the internal variables for
the class.

b. Define the catalog entries for the new dragger (a node kit).

c. Put the default parts into the global dictionary.

d. Create the parts list and the parts that are created by default in this
dragger using SO_KIT_INIT_INSTANCE().

e. Create the appropriate field for the dragger—in this case,
translation.

f. Create the parts for the dragger.

g. Set the switches to inactive (if your dragger uses active/inactive pairs
of parts).

h. Create the projector.

i. Add the dragger callback functions.

j. Add the value-changed callback function.

k. Put a sensor on the field.

l. Call the setUpConnections() method to attach the field sensors.

Steps a through e and step f are standard procedure for new node kits. Step g is the recommended convention if your dragger uses active/inactive pairs of parts, but it is not required. The others are required steps for draggers. Here are a few more details about some of these steps.

Defining New Dragger Catalog Entries

As for any node kit, use the SO_KIT_ADD_CATALOG_ENTRY() macro to create the catalog entries for the dragger. This example places the switch nodes and the feedbackRotate node below the geomSeparator part for improved caching behavior.

Putting Default Parts into the Global Dictionary

When the first instance of the dragger is created, the default scene graph for each part needs to be read and installed in the global dictionary. Use the readDefaultParts() method to read the geometry. It takes as arguments the name of the user-changeable resource .iv file, the compiled-in geomBuffer, and the size of the geomBuffer.

if (SO_KIT_IS_FIRST_INSTANCE())
	   readDefaultParts
          ("translateRadialDragger.iv",  	// default geom file
	    		       geomBuffer,                   // compiled-in defaults
	    		       sizeof(geomBuffer));          // size of buffer
	

Creating the Field

Every dragger has a field (or fields) reflecting its state. This dragger performs translations, so you create a translation field.

Creating the Parts

Creating the parts is again standard node-kit procedure. Use setPartAsDefault() to look up the part in the global dictionary and install the default geometry from the resource file:

setPartAsDefault("translator", "translateRadialTranslator");

The method setPartAsDefault() differs from setPart(). If setPartAsDefault() is used, then the given subgraph will not be written to file unless it is changed later. If setPart() were used, then every subgraph of every part would appear in the file, resulting in lengthy files. Also, when reading the dragger back into Inventor, the geometry written to file would always replace the default. Hence, writing a dragger to file would fix its look forever, even if the default were redesigned.

Setting the Switches

Although the draggers provided with the standard Inventor library use pairs of active and inactive parts, this is not a requirement for your new dragger. However, if you do use similar pairs of parts, the constructor should set the switches to the inactive part to start with:

SoSwitch *sw;
sw = SO_GET_ANY_PART(this, "translatorSwitch", SoSwitch);
setSwitchValue(sw, 0);
sw = SO_GET_ANY_PART(this, "feedbackSwitch", SoSwitch);
setSwitchValue(sw, 0);

The setSwitchValue() method is a convenience routine that checks that the given value is a change before setting it. It also checks whether the switch is NULL.

Creating the Projector

The most common way of turning 2D mouse input into direct 3D interaction is to project the 2D cursor position into a 3D line directed from the eye into the scene and intersect that line with some shape. The classes derived from SbProjector are designed to do just that, as well as to simplify the task of interpreting the result of the projection as a translation, rotation, and so on. For more information on SbProjectors, see the Open Inventor C++ Reference Manual.

In the case of the TranslateRadialDragger, we wish the user to be able to translate the dragger along a line. To turn the 2D mouse position into a translation in three dimensions along a line, we make use of an SbLineProjector. For now, we just construct an instance of the projector and save a pointer to it. Later, we need to update the projector with the current transform space, the current view volume, and so on. Then, every time the mouse moves, we find out what the mouse position projects to in three dimensions, and how far it moved from the previous position.

To create the projector, the constructor includes the following line:

    lineProj = new SbLineProjector();

Defining the Callback Functions

During construction, the dragger class must also register a set of callback functions that are invoked by the base class when some event or change in state occurs. For the TranslateRadialDragger (and most other simple draggers), we need to know these things:

  • When the user initiates dragging this happens when the primary mouse button is pressed and a hit occurs on the dragger geometry.

  • When the user drags the mouse-this happens when the mouse moves, provided dragging has already been initiated and the primary mouse button is still down.

  • When the user completes dragging-this happens when the primary mouse button is released, provided dragging was already in progress.

Adding a Value-Changed Callback

Whenever the motionMatrix changes (for example, as a result of interaction or copying), we must update the translation field to correctly reflect this new state (see Figure 8-3). Use a value-changed callback function to update the translation field.

Adding a Field Sensor

If the translation field changes, the motionMatrix must be changed so that the dragger moves to that new position (again, see Figure 8-3). (The dragger's field could change if it is set by the user, by a field-to-field connection, by an engine, or by a value from file.) Put a sensor on the translation field to detect and communicate these changes.

Destructor

As with all classes, it is necessary to free any memory that was allocated during construction. Since draggers are a type of node, they are not explicitly deleted; instead, they are automatically deleted when their reference count falls to 0. When this happens, the child nodes of the dragger also have their reference counts decremented and are deleted automatically.

The only things that were created with new in the constructor were the projector and the sensor. We explicitly delete the projector and the sensor.

Figure 8-3. Maintaining Consistency Between the Field and the Motion Matrix


Dragging Callback Functions

The source file needs to implement the routines that are called as a result of user interaction: dragStart(), drag(), and dragFinish(). The functions are actually called by small static callback functions (usually called callback stubs). Those callback stubs are the functions that we registered in the constructor with the addCallback methods on SoDragger. (Recall that we need to use callback stubs because there is no notion of the implicit this pointer when the callback is invoked.)

Now, we'll look at the functions that do the real work.

Begin Dragging

When the user presses the primary mouse button and causes a hit on the dragger, dragging begins. When this happens, several things must be done by the dragger to prepare for the ensuing dragging by the user:

a. Find out where on the dragger the hit occurred using the
getLocalStartingPoint() method on SoDragger. This is often used to
determine a direction of manipulation, or to save an initial point that
is used as a marker for successive movements.

b. Set up the projector being used. Projectors need to be initialized,
typically with some parameters to set their size and position, a
transformation space to work in, and the view volume to use for
projecting the mouse position into three dimensions. The projector
geometry is usually determined by the dragger based on the initial
hit position. The transformation space and the view volume are
obtained from methods on SoDragger (using
getLocalToWorldMatrix() and getViewVolume()).

c. Set up any feedback geometry that may exist.

d. Set any appropriate switches to display parts as active.

The TranslateRadialDragger::dragStart() method accomplishes each of these things. The references to local space imply the transformation space that the parts of the manipulator exist in. Example 8-3 includes the code for dragStart().

In the case of the TranslateRadialDragger, a special method, orientFeedbackGeometry(), is called by dragStart() to align the feedback geometry (an arrow) with the direction of translation. This special method is specific to the sample class and is not a requirement for draggers in general.

Continue Dragging

Once dragging has begun, and for as long as the primary mouse button is held down, successive movements of the mouse cause the motion callbacks to be invoked. When motion is detected, the dragger typically does these things:

a. Update the view volume used by any projectors. This is necessary
because it is possible for the camera's view volume to change
between renderings, either because of viewport cropping or if some
external force, such as a viewer, is editing the camera.

b. Update the projector's workspace matrix.

c. Using the current mouse position, project to a new position on the
projector. Then using the new position, and perhaps some
previously saved positions, determine what kind of motion (scale,
rotate, translate) the dragger will perform, in local space.

d. Turn this motion into a matrix and append that matrix to the motion
matrix. This results in movement of the dragger.

Here is the code for TranslateRadialDragger::drag(). Recall that it is called by TranslateRadialDragger::motionCB(), which in turn is invoked by SoDragger whenever the mouse moves during dragging.

void
TranslateRadialDragger::drag()
{
   // Things can change between renderings. To be safe, update 
   // the projector with the current values.
   lineProj->setViewVolume(getViewVolume());    
   lineProj->setWorkingSpace(getLocalToWorldMatrix());

   // Find the new intersection on the projector.
   SbVec3f newHitPt 
     = lineProj->project(getNormalizedLocaterPosition()); 

   // Get initial point expressed in our current local space.
   SbVec3f startHitPt = getLocalStartingPoint();

   // Motion in local space is difference between old and
   // new positions.
   SbVec3f motion = newHitPt - startHitPt;

   // Append this to the startMotionMatrix, which was saved
   // automatically at the beginning of the drag, to find 
   // the current motion matrix.
   setMotionMatrix(
     appendTranslation(getStartMotionMatrix(), motion));
}

Finish Dragging

The last thing a dragger needs to do is reset the state of its geometry when dragging is completed. Example 8-3 includes the code for dragFinish().

Value-Changed Callback

The value-changed callback function updates the dragger's translation field when the motion matrix changes. Here is the code for the TranslateRadialDragger's value-changed callback function.

void
TranslateRadialDragger::valueChangedCB(void *, 
   SoDragger *inDragger)
{
   TranslateRadialDragger *myself 
     = (TranslateRadialDragger *) inDragger;

   // Get translation by decomposing motionMatrix.
   SbMatrix motMat = myself->getMotionMatrix();
   SbVec3f trans, scale;
   SbRotation rot, scaleOrient;
   motMat.getTransform(trans, rot, scale, scaleOrient);

   // Set "translation", disconnecting sensor while doing so.
   myself->fieldSensor->detach();
   if (myself->translation.getValue() != trans)
     myself->translation = trans;
   myself->fieldSensor->attach(&(myself->translation));
}

Field Sensor

If the dragger's translation field changes, the motion matrix must be updated to reflect that change. The callback function for the field sensor performs this task, as follows:

// If the "translation" field is set from outside, update
// motionMatrix accordingly.
void
TranslateRadialDragger::fieldSensorCB(void *inDragger, 
                                      SoSensor *)
{
   TranslateRadialDragger *myself 
     = (TranslateRadialDragger *) inDragger;

   SbMatrix motMat = myself->getMotionMatrix();
   myself->workFieldsIntoTransform(motMat);

   myself->setMotionMatrix(motMat);
}

Note that workFieldsIntoTransform() is a special method that changes only the parts of the matrix for which the dragger has fields. In this case, the translation in the matrix changes, but any rotation or scale in the matrix remains undisturbed.

The setUpConnections() Method

When a dragger is read from file or when a copy of a dragger is made, you do not want any of the dragger's sensors to fire. Implement a setUpConnections() method to detach and attach the sensors so that draggers can be copied and read correctly. The SoDragger::copy() and SoDragger::readInstance() methods call setUpConnections(FALSE) at the beginning and setUpConnections(TRUE) at the end. The constructor also calls setUpConnections(). Example 8-3 shows setUpConnections() for the TranslateRadialDragger class.

Example 8-2 shows the include file for the TranslateRadialDragger class.

Example 8-2. TranslateRadialDragger.h


//  Resource names and part names for this dragger are:
//      Resource Name:                        Part Name:
//      translateRadialTranslator             translator
//      translateRadialTranslatorActive       translatorActive
//      translateRadialFeedback               feedback
//      translateRadialFeedbackActive         feedbackActive

#include <Inventor/draggers/SoDragger.h>
#include <Inventor/fields/SoSFVec3f.h>
#include <Inventor/sensors/SoFieldSensor.h>

class SbLineProjector;

class TranslateRadialDragger : public SoDragger {
   SO_KIT_HEADER(TranslateRadialDragger);

   // Catalog entries for new parts added by this class.
   SO_KIT_CATALOG_ENTRY_HEADER(translatorSwitch);
   SO_KIT_CATALOG_ENTRY_HEADER(translator);
   SO_KIT_CATALOG_ENTRY_HEADER(translatorActive);
   SO_KIT_CATALOG_ENTRY_HEADER(feedbackRotate);
   SO_KIT_CATALOG_ENTRY_HEADER(feedbackSwitch);
   SO_KIT_CATALOG_ENTRY_HEADER(feedback);
   SO_KIT_CATALOG_ENTRY_HEADER(feedbackActive);

  public:
  
   // Constructor
   TranslateRadialDragger();

   // Field that will always contain the dragger's position.
   SoSFVec3f translation;

   // Initialize the class. This should be called once
   // after SoInteraction::init().
   static void initClass();

  protected:

   void orientFeedbackGeometry(const SbVec3f &localDir);

   // Projector used for calculating motion along a line.
   SbLineProjector *lineProj;

   // Static callback functions invoked by SoDragger when the 
   // mouse button goes down over this dragger, when the
   // mouse drags, and when the button is released.
   static void startCB(void *, SoDragger *);
   static void motionCB(void *, SoDragger *);
   static void finishCB(void *, SoDragger *);

   // These functions, invoked by the static callback 
   // functions, do all the work of moving the dragger.
   void dragStart();
   void drag();
   void dragFinish();

   // This sensor watches for changes to the translation field.
   SoFieldSensor *fieldSensor;
   static void fieldSensorCB(void *, SoSensor *);

   // This callback updates the translation field when 
   // the dragger is moved.
   static void valueChangedCB(void *, SoDragger *);

   // This will detach/attach the fieldSensor.
   // It is called at the end of the constructor (to attach).
   // and at the start/end of SoBaseKit::readInstance()
   // and on the new copy at the start/end of SoBaseKit::copy().
   // Returns the state of the node when this was called.
   virtual SbBool setUpConnections(SbBool onOff, 
                                   SbBool doItAlways = FALSE);

  private:

   static const char geomBuffer[];

   // Destructor.
   ~TranslateRadialDragger();
};    

Example 8-3 shows the source code for the TranslateRadialDragger class.

Example 8-3. TranslateRadialDragger.c++


#include <Inventor/nodes/SoRotation.h>
#include <Inventor/nodes/SoSeparator.h>
#include <Inventor/nodes/SoSwitch.h>
#include <Inventor/projectors/SbLineProjector.h>
#include <Inventor/sensors/SoFieldSensor.h>
#include <Inventor/SoPath.h>

// Include file for our new class.
#include "TranslateRadialDragger.h"

// This file contains the variable 
// TranslateRadialDragger::geomBuffer, which describes 
// the default geometry resources for this dragger.
#include "TranslateRadialDraggerGeom.h"

SO_KIT_SOURCE(TranslateRadialDragger);

//  Initializes the type ID for this dragger node. This
//  should be called once after SoInteraction::init().
void
TranslateRadialDragger::initClass()
{
   SO_KIT_INIT_CLASS(TranslateRadialDragger, SoDragger,
                     "Dragger");
}

TranslateRadialDragger::TranslateRadialDragger()
{
   SO_KIT_CONSTRUCTOR(TranslateRadialDragger);

   // Put this under geomSeparator so it draws efficiently.
   SO_KIT_ADD_CATALOG_ENTRY(translatorSwitch, SoSwitch, TRUE,
                            geomSeparator, , FALSE);
   SO_KIT_ADD_CATALOG_ENTRY(translator, SoSeparator, TRUE,
                            translatorSwitch, , TRUE);
   SO_KIT_ADD_CATALOG_ENTRY(translatorActive,SoSeparator, TRUE,
                            translatorSwitch, , TRUE);
   SO_KIT_ADD_CATALOG_ENTRY(feedbackRotate, SoRotation, TRUE,
                            geomSeparator, , TRUE);
   SO_KIT_ADD_CATALOG_ENTRY(feedbackSwitch, SoSwitch, TRUE,
                            geomSeparator, , FALSE);
   SO_KIT_ADD_CATALOG_ENTRY(feedback, SoSeparator, TRUE,
                            feedbackSwitch, , TRUE);
   SO_KIT_ADD_CATALOG_ENTRY(feedbackActive, SoSeparator, TRUE,
                            feedbackSwitch, , TRUE);

   // Read geometry resources. Only do this the first time we 
   // construct one.  'geomBuffer' contains our compiled in 
   // defaults. The user can override these by specifying new 
   // scene graphs in the file:
   // $(SO_DRAGGER_DIR)/translateRadialDragger.iv
   if (SO_KIT_IS_FIRST_INSTANCE())
     readDefaultParts("translateRadialDragger.iv", geomBuffer,
                      sizeof(geomBuffer));
      
   // Field that always shows current position of the dragger.
   SO_KIT_ADD_FIELD(translation, (0.0, 0.0, 0.0));

   // Creates the parts list for this node kit.
   SO_KIT_INIT_INSTANCE();

   // Create the parts of the dragger. This dragger has five 
   // parts that we need to create: "translator", 
   // "translatorActive", "feedback," and "feedbackActive" will 
   // be created using the resource mechanism. They are looked 
   // up in the global dictionary.
   // "rotator," used to position the feedback so it points in 
   // the direction selected by the user, will just be a plain 
   // old SoRotation node.
   // We call 'setPartAsDefault' because we are installing 
   // default geometries from the resource files. By calling
   // 'setPartAsDefault' instead of 'setPart', we ensure that 
   // these parts will not write to file unless they are 
   // changed later. 
   setPartAsDefault("translator", 
                    "translateRadialTranslator");
   setPartAsDefault("translatorActive", 
                    "translateRadialTranslatorActive");
   setPartAsDefault("feedback", 
                    "translateRadialFeedback");
   setPartAsDefault("feedbackActive", 
                    "translateRadialFeedbackActive");

   // Set the switch parts to 0 to display the inactive parts.
   // The parts "translatorSwitch" and "feedbackSwitch"
   // are not public parts (i.e., when making the catalog, the 
   // isPublic flag was set FALSE, so users cannot access them).
   // To retrieve the parts we must use the SO_GET_ANY_PART 
   // macro which calls the protected method getAnyPart().
   SoSwitch *sw;
   sw = SO_GET_ANY_PART(this, "translatorSwitch", SoSwitch);
   setSwitchValue(sw, 0);
   sw = SO_GET_ANY_PART(this, "feedbackSwitch", SoSwitch);
   setSwitchValue(sw, 0);

   // This dragger does motion along a line,
   // so we create a line projector.
   lineProj = new SbLineProjector();

   // Add the callback functions that will be called when
   // the user clicks, drags, and releases.
   addStartCallback(&TranslateRadialDragger::startCB);
   addMotionCallback(&TranslateRadialDragger::motionCB);
   addFinishCallback(&TranslateRadialDragger::finishCB);

   // Updates the translation field when the dragger moves.
   addValueChangedCallback(
                   &TranslateRadialDragger::valueChangedCB);
   // Updates the motionMatrix (and thus moves the dragger 
   // through space) to a new location whenever the translation
   // field is changed from the outside.
   fieldSensor = new SoFieldSensor(
                  &TranslateRadialDragger::fieldSensorCB, this);
   fieldSensor->setPriority(0);

   setUpConnections(TRUE, TRUE);
}

TranslateRadialDragger::~TranslateRadialDragger()
{
   // Delete what we created in the constructor.
   delete lineProj;
   if (fieldSensor!=NULL)
     delete fieldSensor;
}

SbBool
TranslateRadialDragger::setUpConnections(SbBool onOff, 
                                         SbBool doItAlways)
{
   if (!doItAlways && connectionsSetUp == onOff)
     return onOff;

   if (onOff) {
     // We connect AFTER base class.
     SoDragger::setUpConnections(onOff, doItAlways);

     // Call the sensor CB to make things up-to-date.
     fieldSensorCB(this, NULL);

     // Connect the field sensor.
     if (fieldSensor->getAttachedField() != &translation)
       fieldSensor->attach(&translation);
   }
   else {
     // We disconnect BEFORE base class.

     // Disconnect the field sensor.
     if (fieldSensor->getAttachedField()!=NULL)
        fieldSensor->detach();

     SoDragger::setUpConnections(onOff, doItAlways);
   }
   return !(connectionsSetUp = onOff);
}

//  Static callback functions called by SoDragger when the 
//  mouse goes down (over this dragger), drags, and releases.
void
TranslateRadialDragger::startCB(void *, SoDragger *dragger)
{
   TranslateRadialDragger *myself = 
            (TranslateRadialDragger *) dragger;
   myself->dragStart();
}
void
TranslateRadialDragger::motionCB(void *, SoDragger *dragger)
{
   TranslateRadialDragger *myself =
            (TranslateRadialDragger *) dragger;
   myself->drag();
}
void
TranslateRadialDragger::finishCB(void *, SoDragger *dragger)
{
   TranslateRadialDragger *myself =
            (TranslateRadialDragger *) dragger;
   myself->dragFinish();
}

//  Called when user clicks down on this dragger. Sets up the 
//  projector and switches parts to their "active" versions.
void
TranslateRadialDragger::dragStart()
{
   // Display the 'active' parts...
   SoSwitch *sw;
   sw = SO_GET_ANY_PART(this, "translatorSwitch", SoSwitch);
   setSwitchValue(sw, 1);
   sw = SO_GET_ANY_PART(this, "feedbackSwitch", SoSwitch);
   setSwitchValue(sw, 1);

   // Establish the projector line.
   // The direction of translation goes from the center of the
   // dragger toward the point that was hit, in local space. 
   // For the center, use (0,0,0).
   SbVec3f startLocalHitPt = getLocalStartingPoint();
   lineProj->setLine(SbLine(SbVec3f(0,0,0), startLocalHitPt));
   
   // Orient the feedback geometry.
   orientFeedbackGeometry(startLocalHitPt);
}

//  Sets the feedbackRotation node so that the feedback
//  geometry will be aligned with the direction of motion in
//  local space.
void
TranslateRadialDragger::orientFeedbackGeometry( 
                               const SbVec3f &localDir)
{
   // By default, feedback geometry aligns with the x axis.
   // Rotate so that it points in the given direction.
   SbRotation rotXToDir = SbRotation(SbVec3f(1,0,0), localDir);

   // Give this rotation to the "feedbackRotate" part.
   SoRotation *myPart = SO_GET_ANY_PART(this, "feedbackRotate", 
                                        SoRotation);
   myPart->rotation.setValue(rotXToDir);
}
   
//  Called when the mouse translates during dragging. Moves
//  the dragger based on the mouse motion.
void
TranslateRadialDragger::drag()
{
   // Things can change between renderings. To be safe, update 
   // the projector with the current values.
   lineProj->setViewVolume(getViewVolume());    
   lineProj->setWorkingSpace(getLocalToWorldMatrix());

   // Find the new intersection on the projector.
   SbVec3f newHitPt =
            lineProj->project(getNormalizedLocaterPosition()); 

   // Get initial point expressed in our current local space.
   SbVec3f startHitPt = getLocalStartingPoint();

   // Motion in local space is difference between old and
   // new positions.
   SbVec3f motion = newHitPt - startHitPt;

   // Append this to the startMotionMatrix, which was saved
   // automatically at the beginning of the drag, to find 
   // the current motion matrix.
   setMotionMatrix(
     appendTranslation(getStartMotionMatrix(), motion));
}

//  Called when mouse button is released and drag is completed.
void
TranslateRadialDragger::dragFinish()
{
   // Display inactive versions of parts...
   SoSwitch *sw;
   sw = SO_GET_ANY_PART(this, "translatorSwitch", SoSwitch);
   setSwitchValue(sw, 0);
   sw = SO_GET_ANY_PART(this, "feedbackSwitch", SoSwitch);
   setSwitchValue(sw, 0);

   // Get rid of the "feedbackRotate" part.  We don't need
   // it since we aren't showing the feedback any more.
   setAnyPart("feedbackRotate", NULL);
}    

// Called when the motionMatrix changes. Sets the 'translation'
// field based on the new motionMatrix.
void
TranslateRadialDragger::valueChangedCB(void *, 
                                       SoDragger *inDragger)
{
   TranslateRadialDragger *myself =
            (TranslateRadialDragger *) inDragger;

   // Get translation by decomposing motionMatrix.
   SbMatrix motMat = myself->getMotionMatrix();
   SbVec3f trans, scale;
   SbRotation rot, scaleOrient;
   motMat.getTransform(trans, rot, scale, scaleOrient);

   // Set "translation", disconnecting sensor while doing so.
   myself->fieldSensor->detach();
   if (myself->translation.getValue() != trans)
     myself->translation = trans;
   myself->fieldSensor->attach(&myself->translation);
}

// If the "translation" field is set from outside, update
// motionMatrix accordingly.
void
TranslateRadialDragger::fieldSensorCB(void *inDragger, 
                                      SoSensor *)
{
   TranslateRadialDragger *myself =
            (TranslateRadialDragger *) inDragger;

   SbMatrix motMat = myself->getMotionMatrix();
   myself->workFieldsIntoTransform(motMat);

   myself->setMotionMatrix(motMat);
}

Creating a Compound Dragger

In this section, you will learn how simple draggers can be combined to make a more complex compound dragger. Compound draggers can typically perform several different operations, such as scaling and translating. The SoCenterBallDragger and SoTransformBoxDragger are examples of compound draggers.

Unlike simple draggers, which can perform only one operation, compound draggers can typically do many things. The operation the user performs is determined by the part of the compound dragger that is first clicked upon.

The steps involved in creating a compound dragger are best illustrated through example. This section shows how to create a RotTransDragger, which—as its name implies—allows both rotations and translations. The rotation parts of this compound dragger are made from a set of three SoRotateCylindricalDraggers, one for each axis we wish to allow the user to rotate about. The translation part of the dragger is an instance of the TranslateRadialDragger that we created in the previous section.

Two nodes are introduced in this section: the SoAntiSquish node and the SoSurroundScale node. These nodes are often useful in compound draggers and in manipulators. The compound dragger created in this section uses an SoAntiSquish node. Its catalog also contains an SoSurroundScale node, which is not used by default. The RotTransManip manipulator created at the end of this chapter uses a RotTransDragger and turns on the SoSurroundScale part. This enables the manipulator to have its dragger geometry surround the other objects that will move along with it.

SoAntiSquish Node

The SoAntiSquish node makes scaling uniform so that draggers and manipulators retain their shape even if the current transformation contains a nonuniform scale. When an action is applied to this node, it decomposes the current transformation matrix into a rotation, a translation, and a scale. If the scale is nonuniform, it replaces the current transformation matrix with a new one that uses the same rotation and translation, but a uniform scale.

This node has one field, sizing, which controls how to make the scale uniform. Possible values for this field are as follows:

AVERAGE_DIMENSION 


averages the x, y, and z scale value (default).

BIGGEST_DIMENSION 


replaces all three scale values with the largest scale value.

SMALLEST_DIMENSION 


replaces all three scale values with the smallest scale value.

LONGEST_DIAGONAL 


constructs a unit cube at the origin and transforms it by the current transformation matrix. To do this, it finds the longest diagonal of that transformed cube. Then, it sets the uniform scale to be half the length of this diagonal. (If the cube is being sheared, the LONGEST_DIAGONAL gives you the greatest distance from any point in the cube to the origin. With shearing, this distance could be greater than BIGGEST_DIMENSION.)

SoSurroundScale Node

The SoSurroundScale node is used to cause a dragger or manipulator to surround certain objects in the scene. This node is typically used when you create an SoTransformManip from a dragger. Although this part is included in the catalog for the RotTransDragger, it is not actually constructed by the dragger and is left as NULL.

This node examines what you want it to surround in the scene graph and determines how large the objects are. It then adds a scale and a translation to the current transformation matrix so that it surrounds those objects. For a manipulator, these are usually the objects affected by the movement of the manipulator.

Figure 8-4 shows a scene graph containing a transform manipulator and a cube. The dragger within the manipulator includes a surround-scale node. The top separator node is the container node. In addition to the manipulator, it contains a cube, which the manipulator surrounds. The transform manipulator is the reset node. The manipulator surrounds everything below the container node and to the right of the reset node.

The SoSurroundScale node has two fields:

numNodesUpToContainer  


number of nodes up the current path to the node that contains the objects to surround

numNodesUpToReset  


number of nodes up the chain to the reset node (that is, the node where the bounding box is emptied)

In Figure 8-4, numNodesUpToContainer equals 4 and numNodesUpToReset equals 3. (Note that numNodesUpToReset must be smaller than numNodesUpToContainer, or there will be no reset node.) The result is that the geometry of the dragger now surrounds the cube.

Defining the Parts

Several operations must be completed when you are designing the parts of a compound dragger:

  • Define the desired operations of the dragger—for this example, the operations are a rotation about each of the three axes, as well as arbitrary translation.

  • Choose the simple dragger to suit each operation—for rotations about a fixed axis, you can use the SoRotateCylindricalDragger. (You could also use the SoRotateDiscDragger, which rotates about a fixed axis; however, its behavior is not well suited for this application, since it does not allow rotation when looking edge-on. It is better for dial-type draggers.) For the arbitrary translation part, you can use the TranslateRadialDragger created in the previous section.

  • Get the resource names and correlate them to part names—for example, the SoRotateCylindricalDragger is the XRotator part in the RotTransDragger. The compound dragger needs to create its own resource name for each part of each simple dragger. By convention, Inventor concatenates the part name as follows: compound dragger name/simple dragger part name/name of the part within the simple dragger. In the RotTransDragger, the three rotators share the same resources.

  • Determine the geometry of the parts—by default, the simple draggers use their default part geometry. Typically, what gives compound draggers their flair is the way they redefine this default geometry in an interesting way. This redefinition is done in the Inventor file for the dragger. In the RotTransDragger, the default cylinder of each SoRotateCylindricalDragger is replaced by a thin ring (actually a thin cylinder with its top and bottom removed). The RotTransDragger does not display any feedback for the SoRotateCylindricalDraggers because the richer geometry of the compound dragger provides sufficient visual information for the user. (The feedback parts are set to be an empty separator.)

    Figure 8-4. Using an SoSurroundScale Node in a Transform Manipulator


Once all of these things have been determined, you can create a default geometry file for the compound dragger. The following geometry file defines the geometry for all parts in the translator and rotators that make up the RotTransDragger. It uses the same naming conventions used in the previous section describing the simple dragger. At this point, you also need to create a default include file for the compiled-in geometry. As described in the previous section, run the ivToIncludeFile utility to translate the .iv file into an array of hexadecimal numbers.

Example 8-4 shows the default geometry file for RotTransDragger.

Example 8-4. rotTransDragger.iv


#Inventor V2.0 ascii

# Geometry resource file for the RotTransDragger

# Geometry for the rotating parts.
DEF rotTransRotatorRotator Separator {
    Material { 
       diffuseColor  .05 .18 .125
       emissiveColor .05 .18 .125
    }
    DrawStyle { lineWidth 2 }
    Cylinder {
	       radius 1.85
       	height .15
	       parts SIDES
    }
}

DEF rotTransRotatorRotatorActive Separator {
    Material { 
       	diffuseColor  .05 .2025 .18
	       emissiveColor .05 .2025 .18
    }
    DrawStyle { lineWidth 2 }
    Cylinder {
	       radius 1.85
	       height .15
	       parts SIDES
    }
}

# Do not display the axis feedback used
# by the cylinder manips.
DEF rotTransRotatorFeedback Separator { }
DEF rotTransRotatorFeedbackActive Separator { }

DEF rotTransTranslatorTranslator Separator {
    Material { diffuseColor .6 .6 .6 }
    DrawStyle { style LINES }
    Sphere { radius 1.732 }
}

DEF rotTransTranslatorTranslatorActive Separator {
    Material { diffuseColor .6 .6 0 }
    DrawStyle { style LINES }
    Sphere { radius 1.732 }
}

# Don't show anything for feedback during inactive state.
DEF rotTransTranslatorFeedback Separator { }

DEF rotTransTranslatorFeedbackActive Separator {
    Material { diffuseColor .5  .9 .9 }
    # An arrow aligned with the x-axis.
    RotationXYZ {
	       axis Z
	       angle 1.57079
    }
    Separator {

       	#stick
       	Cylinder { height 4.0 radius 0.05 }

       	#left arrowhead
	       Translation { translation 0 2.2 0 }
	       Cone { height 0.4 bottomRadius 0.2 }

       	#right arrowhead
       	Translation { translation 0 -4.4 0 }
       	RotationXYZ { axis Z angle 3.14159 }
	       Cone { height 0.4 bottomRadius 0.2 }
   }
}

Figure 8-5 shows the scene graph for the RotTransDragger class. Example 8-5 shows the header file for this class.

Example 8-5. RotTransDragger.h


//  Geometry resources and part names for this dragger:

//  Resource Names:                     Part Names:
// rotTransTranslatorTranslator      translator.translator
// rotTransTranslatorTranslatorActive
//                                   translator.translatorActive
// rotTransTranslatorFeedback        translator.feedback
// rotTransTranslatorFeedbackActive  translator.feedbackActive

// rotTransRotatorRotator             XRotator.rotator
// rotTransRotatorRotatorActive       XRotator.rotatorActive
// rotTransRotatorFeedback            XRotator.feedback
// rotTransRotatorFeedbackActive      XRotator.feedbackActive
// (and similarly for parts of the YRotator and ZRotator)

#include <Inventor/sensors/SoFieldSensor.h>
#include <Inventor/draggers/SoDragger.h>
#include <Inventor/fields/SoSFVec3f.h>
#include <Inventor/fields/SoSFRotation.h>

class TranslateRadialDragger;
class SoRotateCylindricalDragger;

class RotTransDragger : public SoDragger
{
   SO_KIT_HEADER(RotTransDragger);

   // Makes the dragger surround other objects
   SO_KIT_CATALOG_ENTRY_HEADER(surroundScale);

Figure 8-5. Structure of the RotTransDragger


// Keeps the dragger evenly sized in all 3 dimensions
   SO_KIT_CATALOG_ENTRY_HEADER(antiSquish);

   // The translating dragger...
   SO_KIT_CATALOG_ENTRY_HEADER(translator);

   // The X and Z rotators need to be turned so as to orient 
   // correctly. So create a separator part and put an 
   // SoRotation node and the dragger underneath.
   SO_KIT_CATALOG_ENTRY_HEADER(XRotatorSep);
   SO_KIT_CATALOG_ENTRY_HEADER(XRotatorRot);
   SO_KIT_CATALOG_ENTRY_HEADER(XRotator);

   SO_KIT_CATALOG_ENTRY_HEADER(YRotator);

   SO_KIT_CATALOG_ENTRY_HEADER(ZRotatorSep);
   SO_KIT_CATALOG_ENTRY_HEADER(ZRotatorRot);
   SO_KIT_CATALOG_ENTRY_HEADER(ZRotator);

  public:
  
   // Constructor
   RotTransDragger();

   // These fields reflect state of the dragger at all times.
   SoSFRotation rotation;
   SoSFVec3f   translation;

   // This should be called once after SoInteraction::init().
   static void initClass();

  protected:

   // These sensors ensure that the motionMatrix is updated 
   // when the fields are changed from outside.
   SoFieldSensor *rotFieldSensor;
   SoFieldSensor *translFieldSensor;
   static void fieldSensorCB(void *, SoSensor *);

   // This function is invoked by the child draggers when they 
   // change their value.
   static void valueChangedCB(void *, SoDragger *);

   // Called at the beginning and end of each dragging motion.
   // Tells the "surroundScale" part to recalculate.
   static void invalidateSurroundScaleCB(void *, SoDragger *);

   // This will detach/attach the fieldSensor.
   // It is called at the end of the constructor (to attach).
   // and at the start/end of SoBaseKit::readInstance()
   // and on the new copy at the start/end of SoBaseKit::copy()
   // Returns the state of the node when this was called.
   virtual SbBool setUpConnections( SbBool onOff, 
                        SbBool doItAlways = FALSE);

   // This allows us to specify that certain parts do not
   // write out. We'll use this on the antiSquish and
   // surroundScale parts.
   virtual void setDefaultOnNonWritingFields();

  private:

   static const char geomBuffer[];

   // Destructor.
   ~RotTransDragger();
};   

Initializing the Compound Dragger

The complete source file for the RotTransDragger is shown in Example 8-6. The process of initializing the compound dragger is identical to that of the simple dragger (see “Initializing the Dragger Class”).

Constructor

Many of the steps for constructing the compound dragger are the same as those for a simple dragger. This section describes the similarities and differences. The basic steps for constructing a dragger, described first in “Constructor”, are as follows:

a. Use SO_KIT_CONSTRUCTOR() to set up the internal variables for
the class.

b. Define the catalog entries for the new dragger.

c. Put the default parts into the global dictionary.

d. Create the parts list and the parts that are created by default in this
dragger using SO_KIT_INIT_INSTANCE().

e. Create the special-interest field or fields for the dragger.

f. Create the parts for the dragger.

g. Set the switches to inactive (if your dragger uses active/inactive pairs
of parts).

h. Create the projector.

i. Add the dragger callback functions.

j. Add the value-changed callback function.

k. Put a sensor on the special-interest field (or fields).

l. Call the setUpConnections() method to attach the field sensors.

Steps a through e

Steps a through e are similar for simple and compound draggers.

Step a, using SO_KIT_CONSTRUCTOR(), is the same for simple and compound draggers.

Step b is to define the catalog entries for the dragger, which is actually a node kit. Use the SO_KIT_ADD_CATALOG_ENTRY() macro to define the catalog entries. Arrange the simple draggers relative to each other, as shown earlier in Figure 8-5. Each simple dragger has its own separator, used for caching. The simple draggers can use the cache at the topSeparator node if none of the draggers is changing. In compound draggers such as this one, the geomSeparator, provided by the base SoDragger class, is not used.

Step c is to put the default parts into the global dictionary:

if (SO_KIT_IS_FIRST_INSTANCE())
   readDefaultParts("rotTransDragger.iv", geomBuffer, 
                     sizeof(geomBuffer));

Step d is to create the parts list and the default parts using the macro SO_KIT_INIT_INSTANCE().

Step e is to create the special-interest fields for this compound dragger: a rotation field and a translation field:

SO_KIT_ADD_FIELD(rotation, (0.0, 0.0, 0.0, 1.0));
SO_KIT_ADD_FIELD(translation, (0.0, 0.0. 0.0);

Step f: Creating the Parts

Step f is to create the parts for the node kit. This step is more involved for a compound dragger than for a simple dragger. At this point, you need to do the following things:

  1. Construct the antisquish node. For the RotTransDragger, the sizing field of the SoAntiSquish node is set to BIGGEST_DIMENSION. As a result, the largest of the three scale values is used as the uniform scale value.

  2. Create the simple draggers.

  3. Create the rotation nodes in the XRotatorRot and ZRotatorRot parts. The rotation node in the XRotatorRot part aligns the cylindrical rotate dragger with the x-axis (in the default position, it rotates about the y-axis). The rotation node in the ZRotatorRot part aligns the cylindrical rotate dragger with the z-axis.

Steps g through i

Steps g through i are all performed by the child draggers. The parent dragger class does not define these functions.

Step j: Value-Changed Callback Function

Step j is to update the rotation and translation fields in the dragger when the motion matrix changes (see “Value-Changed Callback”, where this step was performed for the simple dragger).

The code for the RotTransDragger is

addValueChangedCallback(&RotTransDragger::valueChangedCB);

Step k: Field Sensors

Conversely, you need to update the motion matrix when the translation or rotation field changes. This dragger places a sensor on the translation field and another sensor on the rotation field. Both sensors use the same callback function, fieldSensorCB, defined later in the source file.

// Updates motionMatrix when either field changes.
rotFieldSensor = new SoFieldSensor(
                     &RotTransDragger::fieldSensorCB, this);
rotFieldSensor->setPriority(0);
translFieldSensor = new SoFieldSensor(
                        &RotTransDragger::fieldSensorCB,this);
translFieldSensor->setPriority(0);

setUpConnections(TRUE, TRUE);

Step l: Setting Up Connections

The setUpConnections() method is used to connect and disconnect the dragger's field connections, callback functions, and sensors. This method performs the following operations:

  1. Calls the base class setUpConnections() method.

  2. Sets up the geometry of its child draggers.

    For each child dragger in the compound dragger, the setUpConnections() method calls getAnyPart() to build and return the dragger.

    Then it calls setPartAsDefault() after looking up the replacement parts in the global dictionary.

  3. Adds the start and finish callback functions.

    The SoSurroundScale node does its bounding-box calculations when it is built and when its invalidate() method is called. For efficiency, the recalculation is performed only at the beginning and end of a drag. In beween, the dragger continues to draw at the same size. Register the invalidateSurroundScaleCB() callback function for each simple dragger.

  4. Registers the child draggers.

    It is worth describing in more detail what happens when you call the registerChildDragger() method for each simple dragger in the compound dragger. This method binds the child and parent draggers together to function as a unit in two main ways. First, it causes the parent dragger's callback functions to be called after any of the child dragger's callback functions are called. Second, it causes all child draggers to move as a unit: whenever the user clicks and drags on one child dragger, the other draggers move in unison with the first dragger.

    When you call registerChildDragger(), the following things happen automatically. A value-changed callback function is added to monitor the motion in the child dragger. When the child dragger moves, the callback transforms that motion into the compound dragger's space, applying it to the compound dragger as a whole. It then zeros out the child dragger's motion so that the child is not moved relative to the whole.

    If you create a new dragger and for some reason you don't want the pieces of the compound dragger to move as a whole, you can use the registerChildDraggerCallbacksOnly() method, which doesn't transfer the child dragger's motion to the parent dragger. (The spotlight dragger is an example of a dragger that uses this method. When the cone widens, the rest of the dragger remains unchanged.)

  5. Attaches the field sensors (as for simple draggers).

Destructor

Since the child draggers are nodes, they are destroyed automatically when the parent node is destroyed. There are no projectors to destroy as with the simple dragger, since no projectors were created here. The destructor merely needs to delete the sensor.

Callback Functions

The RotTransDragger uses three callback functions, described above:

invalidateSurroundScaleCB()  


called when dragging starts and finishes

valueChangedCB()  


called when the motion matrix changes

fieldSensorCB() 

called when the special-interest field or fields change

The source file for the compound dragger must define each of these callback functions.

Example 8-6 shows the source code for RotTransDragger.c++:

Example 8-6. RotTransDragger.c++


#include <Inventor/nodes/SoAntiSquish.h>
#include <Inventor/nodes/SoRotation.h>
#include <Inventor/nodes/SoSeparator.h>
#include <Inventor/nodes/SoSurroundScale.h>
#include <Inventor/nodes/SoTransform.h>
#include <Inventor/sensors/SoFieldSensor.h>

// Include files for child dragger classes.
#include <Inventor/draggers/SoRotateCylindricalDragger.h>
#include "TranslateRadialDragger.h"

// Include file for our new class.
#include "RotTransDragger.h"

// This file contains RotTransDragger::geomBuffer, which 
// describes the default geometry resources for this class.
#include "RotTransDraggerGeom.h"

SO_KIT_SOURCE(RotTransDragger);


//  Initializes the type ID for this dragger node. This
//  should be called once after SoInteraction::init().
void
RotTransDragger::initClass()
{
   SO_KIT_INIT_CLASS(RotTransDragger, SoDragger, "Dragger");    
}

RotTransDragger::RotTransDragger()
{
   SO_KIT_CONSTRUCTOR(RotTransDragger);

   // Don't create "surroundScale" by default. It's only put 
   // to use if this dragger is used within a manipulator.
   SO_KIT_ADD_CATALOG_ENTRY(surroundScale, SoSurroundScale, TRUE,
                            topSeparator, geomSeparator, TRUE);
   // Create an anti-squish node by default.
   SO_KIT_ADD_CATALOG_ENTRY(antiSquish, SoAntiSquish, FALSE,
                            topSeparator, geomSeparator, TRUE);
   SO_KIT_ADD_CATALOG_ENTRY(translator, TranslateRadialDragger,
                            TRUE, topSeparator, geomSeparator,
                            TRUE);
   SO_KIT_ADD_CATALOG_ENTRY(XRotatorSep, SoSeparator, FALSE,
                            topSeparator, geomSeparator, FALSE);
   SO_KIT_ADD_CATALOG_ENTRY(XRotatorRot, SoRotation, TRUE,
                            XRotatorSep, , FALSE);
   SO_KIT_ADD_CATALOG_ENTRY(XRotator,SoRotateCylindricalDragger,
                            TRUE, XRotatorSep, ,TRUE);

   SO_KIT_ADD_CATALOG_ENTRY(YRotator, SoRotateCylindricalDragger,
                            TRUE, topSeparator, geomSeparator, TRUE);

   SO_KIT_ADD_CATALOG_ENTRY(ZRotatorSep, SoSeparator, FALSE,
                            topSeparator, geomSeparator, FALSE);
   SO_KIT_ADD_CATALOG_ENTRY(ZRotatorRot, SoRotation, TRUE,
                            ZRotatorSep, ,FALSE);
   SO_KIT_ADD_CATALOG_ENTRY(ZRotator, SoRotateCylindricalDragger,
                            TRUE, ZRotatorSep, ,TRUE);

   // Read geometry resources. Only do this the first time we
   // construct one. 'geomBuffer' contains our compiled in
   // defaults. The user can override these by specifying new
   // scene graphs in the file:
   // $(SO_DRAGGER_DIR)/rotTransDragger.iv"
   if (SO_KIT_IS_FIRST_INSTANCE())
     readDefaultParts("rotTransDragger.iv", geomBuffer,
                       sizeof(geomBuffer));

   // Fields that always show current state of the dragger.
   SO_KIT_ADD_FIELD(rotation, (0.0, 0.0, 0.0, 1.0));
   SO_KIT_ADD_FIELD(translation, (0.0, 0.0, 0.0));

   // Creates parts list and default parts for this nodekit.
   SO_KIT_INIT_INSTANCE();

   // Make the anti-squish node surround the biggest dimension
   SoAntiSquish *myAntiSquish =
            SO_GET_ANY_PART(this, "antiSquish", SoAntiSquish);
   myAntiSquish->sizing = SoAntiSquish::BIGGEST_DIMENSION;

   // Create the simple draggers that comprise this dragger.
   // This dragger has four simple pieces:  
   //    1 TranslateRadialDragger
   //    3 RotateCylindricalDraggers
   // In the constructor, we just call SO_GET_ANY_PART to
   // build each dragger.
   // Within the setUpConnections() method, we will
   // take care of giving these draggers new geometry and 
   // establishing their callbacks.

   // Create the translator dragger.    
   SoDragger *tDragger = SO_GET_ANY_PART(this, "translator", 
                         TranslateRadialDragger);

   // Create the XRotator dragger.    
   SoDragger *XDragger = SO_GET_ANY_PART(this, "XRotator", 
                         SoRotateCylindricalDragger);

   // Create the YRotator dragger.    
   SoDragger *YDragger = SO_GET_ANY_PART(this, "YRotator", 
                         SoRotateCylindricalDragger);

   // Create the ZRotator dragger.    
   SoDragger *ZDragger = SO_GET_ANY_PART(this, "ZRotator", 
                         SoRotateCylindricalDragger);

   // Set rotations in "XRotatorRot" and "ZRotatorRot" parts.
   // These parts will orient the draggers from their default 
   // (rotating about Y) to the desired configurations.
   // By calling 'setAnyPartAsDefault' instead of 'setAnyPart'
   // we ensure that they will not be written out, unless
   // they are changed later on.
   SoRotation *XRot = new SoRotation;
   XRot->rotation.setValue(
     SbRotation(SbVec3f(0,1,0), SbVec3f(1,0,0)));
   setAnyPartAsDefault("XRotatorRot", XRot);

   SoRotation *ZRot = new SoRotation;
   ZRot->rotation.setValue(
     SbRotation(SbVec3f(0,1,0), SbVec3f(0,0,1)));
   setAnyPartAsDefault("ZRotatorRot", ZRot);

   // Updates the fields when motionMatrix changes 
   addValueChangedCallback(&RotTransDragger::valueChangedCB);

   // Updates motionMatrix when either field changes.
   rotFieldSensor = new SoFieldSensor(
                        &RotTransDragger::fieldSensorCB, this);
   rotFieldSensor->setPriority(0);
   translFieldSensor = new SoFieldSensor(
                           &RotTransDragger::fieldSensorCB,this);
   translFieldSensor->setPriority(0);

   setUpConnections(TRUE, TRUE);
}

RotTransDragger::~RotTransDragger()
{
   if (rotFieldSensor!=NULL)
     delete rotFieldSensor;
   if (translFieldSensor!=NULL)
     delete translFieldSensor;
}

SbBool
RotTransDragger::setUpConnections(SbBool onOff, SbBool doItAlways)
{
   if (!doItAlways && connectionsSetUp == onOff)
     return onOff;

   if (onOff) {
     // We connect AFTER base class.
     SoDragger::setUpConnections(onOff, doItAlways);

     // For each of the simple draggers that compries this:
     // [a]Call setPart after looking up our replacement parts 
     //    in the global dictionary.
     // [b]Add the invalidateSurroundScaleCB as a start and end
     //    callback. When using a surroundScale node, these 
     //    trigger it to recalculate a bounding box at the 
     //    beginning and end of dragging.
     // [c]Register the dragger as a 'childDragger' of this 
     //    one. This has the following effects: 
     //    [1] This dragger's callbacks will be invoked 
     //        following the child manip's callbacks.  
     //    [2] When the child is dragged, the child's motion 
     //        will be transferred into motion of the entire 
     //        dragger.
      SoDragger *tD =
               (SoDragger *) getAnyPart("translator", FALSE);
      // [a] Set up the parts in the child dragger...
      tD->setPartAsDefault("translator",
                           "rotTransTranslatorTranslator");
      tD->setPartAsDefault("translatorActive",
                           "rotTransTranslatorTranslatorActive");
      tD->setPartAsDefault("feedback",
                           "rotTransTranslatorFeedback");
      tD->setPartAsDefault("feedbackActive",
                           "rotTransTranslatorFeedbackActive");
      // [b] and [c] Add the callbacks and register the child
      tD->addStartCallback(
               &RotTransDragger::invalidateSurroundScaleCB, this);
      tD->addFinishCallback(
               &RotTransDragger::invalidateSurroundScaleCB, this);
      registerChildDragger(tD);

      SoDragger *XD =
               (SoDragger *) getAnyPart("XRotator", FALSE);
      // [a] Set up the parts in the child dragger...
      XD->setPartAsDefault("rotator",
                           "rotTransRotatorRotator");
      XD->setPartAsDefault("rotatorActive",
                           "rotTransRotatorRotatorActive");
      XD->setPartAsDefault("feedback",
                           "rotTransRotatorFeedback");
      XD->setPartAsDefault("feedbackActive",
                           "rotTransRotatorFeedbackActive");
      // [b] and [c] Add the callbacks and register the child
      XD->addStartCallback(
               &RotTransDragger::invalidateSurroundScaleCB, this);
      XD->addFinishCallback(
               &RotTransDragger::invalidateSurroundScaleCB, this);
      registerChildDragger(XD);

      SoDragger *YD =
               (SoDragger *) getAnyPart("YRotator", FALSE);
      // [a] Set up the parts in the child dragger...
      YD->setPartAsDefault("rotator",
                           "rotTransRotatorRotator");
      YD->setPartAsDefault("rotatorActive",
                           "rotTransRotatorRotatorActive");
      YD->setPartAsDefault("feedback",
                           "rotTransRotatorFeedback");
      YD->setPartAsDefault("feedbackActive",
                           "rotTransRotatorFeedbackActive");
      // [b] and [c] Add the callbacks and register the child
      YD->addStartCallback(
               &RotTransDragger::invalidateSurroundScaleCB, this);
      YD->addFinishCallback(
               &RotTransDragger::invalidateSurroundScaleCB, this);
      registerChildDragger(YD);

      SoDragger *ZD =
               (SoDragger *) getAnyPart("ZRotator", FALSE);
      // [a] Set up the parts in the child dragger...
      ZD->setPartAsDefault("rotator",
                           "rotTransRotatorRotator");
      ZD->setPartAsDefault("rotatorActive",
                           "rotTransRotatorRotatorActive");
      ZD->setPartAsDefault("feedback",
                           "rotTransRotatorFeedback");
      ZD->setPartAsDefault("feedbackActive",
                           "rotTransRotatorFeedbackActive");
      // [b] and [c] Add the callbacks and register the child
      ZD->addStartCallback(
               &RotTransDragger::invalidateSurroundScaleCB, this);
      ZD->addFinishCallback(
               &RotTransDragger::invalidateSurroundScaleCB, this);
      registerChildDragger(ZD);


     // Call the sensor CB to make things up-to-date.
     fieldSensorCB(this, NULL);

     // Connect the field sensors
     if (translFieldSensor->getAttachedField() != &translation)
        translFieldSensor->attach(&translation);
     if (rotFieldSensor->getAttachedField() != &rotation)
        rotFieldSensor->attach(&rotation);
   }
   else {
     // We disconnect BEFORE base class.

     // Remove the callbacks from the child draggers,
     // and unregister them as children.
      SoDragger *tD =
               (SoDragger *) getAnyPart("translator", FALSE);
      tD->removeStartCallback(
               &RotTransDragger::invalidateSurroundScaleCB, this);
      tD->removeFinishCallback(
               &RotTransDragger::invalidateSurroundScaleCB, this);
      unregisterChildDragger(tD);

      SoDragger *XD =
               (SoDragger *) getAnyPart("XRotator", FALSE);
      XD->removeStartCallback(
               &RotTransDragger::invalidateSurroundScaleCB, this);
      XD->removeFinishCallback(
               &RotTransDragger::invalidateSurroundScaleCB, this);
      unregisterChildDragger(XD);

      SoDragger *YD =
               (SoDragger *) getAnyPart("YRotator", FALSE);
      YD->removeStartCallback(
               &RotTransDragger::invalidateSurroundScaleCB, this);
      YD->removeFinishCallback(
               &RotTransDragger::invalidateSurroundScaleCB, this);
      unregisterChildDragger(YD);

      SoDragger *ZD =
               (SoDragger *) getAnyPart("ZRotator", FALSE);
      ZD->removeStartCallback(
               &RotTransDragger::invalidateSurroundScaleCB, this);
      ZD->removeFinishCallback(
               &RotTransDragger::invalidateSurroundScaleCB, this);
      unregisterChildDragger(ZD);

     // Disconnect the field sensors.
     if (translFieldSensor->getAttachedField()!=NULL)
        translFieldSensor->detach();
     if (rotFieldSensor->getAttachedField()!=NULL)
        rotFieldSensor->detach();

     SoDragger::setUpConnections(onOff, doItAlways);
   }

   return !(connectionsSetUp = onOff);
}

// Called when the motionMatrix changes. Sets the "translation"
// and "rotation" fields based on the new motionMatrix
void
RotTransDragger::valueChangedCB(void *, SoDragger *inDragger)
{
   RotTransDragger *myself = (RotTransDragger *) inDragger;

   // Factor the motionMatrix into its parts
   SbMatrix motMat = myself->getMotionMatrix();
   SbVec3f   trans, scale;
   SbRotation rot, scaleOrient;
   motMat.getTransform(trans, rot, scale, scaleOrient);

   // Set the fields. Disconnect the sensors while doing so.
   myself->rotFieldSensor->detach();
   myself->translFieldSensor->detach();
   if (myself->rotation.getValue() != rot)
     myself->rotation = rot;
   if (myself->translation.getValue() != trans)
     myself->translation = trans;
   myself->rotFieldSensor->attach(&myself->rotation);
   myself->translFieldSensor->attach(&myself->translation);
}

// If the "translation" or "rotation" field changes, changes
// the motionMatrix accordingly.
void
RotTransDragger::fieldSensorCB(void *inDragger, SoSensor *)
{
   RotTransDragger *myself = (RotTransDragger *) inDragger;

   SbMatrix motMat = myself->getMotionMatrix();
   myself->workFieldsIntoTransform(motMat);

   myself->setMotionMatrix(motMat);
}

// When any child dragger starts or ends a drag, tell the
// "surroundScale" part (if it exists) to invalidate its 
// current bounding box value.
void 
RotTransDragger::invalidateSurroundScaleCB(void *parent, SoDragger *)
{
   RotTransDragger *myParentDragger = (RotTransDragger *) parent;

   // Invalidate the surroundScale, if it exists.
   SoSurroundScale *mySS = SO_CHECK_PART(
            myParentDragger, "surroundScale", SoSurroundScale);
   if (mySS != NULL)
      mySS->invalidate();
}

void
RotTransDragger::setDefaultOnNonWritingFields()
{
   // The nodes pointed to by these part-fields may 
   // change after construction, but we
   // don't want to write them out.
   surroundScale.setDefault(TRUE);
   antiSquish.setDefault(TRUE);

   SoDragger::setDefaultOnNonWritingFields();
}

Creating a Manipulator

A manipulator is derived from another class of node, such as an SoTransform, SoLight, or SoCamera. It employs a dragger to edit the fields of that node and adds the dragger to the node as a hidden child. For example, the SoPointLightManip is a subclass of SoPointLight. It adds an SoPointLightDragger as a hidden child of the point light. When this manipulator is rendered, it draws a point-light dragger and, because it is a subclass of SoPointLight, it also executes the light commands. When an SoPointLightManip is used, moving the dragger causes the light in the scene to move because the manipulator is responsible for maintaining the consistency between its own fields and the fields of its dragger.

Creating a new manipulator involves two kinds of work:

  1. First, you need to create a base class for the new kind of manipulator, if it does not already exist. The Inventor library provides four base classes for manipulators: SoTransformManip, SoPointLightManip, SoSpotLightManip, and SoDirectionalLightManip. This step requires more work than the following step.

  2. Next, you need to derive a new manipulator from this base class to employ the new dragger. This step is easy.

Examples 8-7 and 8-8 show the code to create RotTransManip, a subclass of SoTransformManip, so you do not need to create the base manipulator class (that is, you can skip step 1). You may be asking why anyone would want to derive a new manipulator from one of the existing base classes. The reason is that your new manipulator can use a different dragger, which creates a different look and feel for the user interface. For example, the handle box and trackball manipulators, which you're already familiar with, both edit the fields of an SoTransform. But their user interfaces look and operate differently from each other.

All you need to do in such cases is to create a new class of node and set the dragger in the constructor (step 2). If you are creating a manipulator derived from SoTransformManip, you also need to create the surround-scale part, since transform manipulators typically surround the things they affect (see “Creating a Compound Dragger”).

Deriving a Class from SoTransformManip

Examples 8-7 and 8-8 show the RotTransManip class, which employs a RotTransDragger to edit an SoTransform node.

Example 8-7. RotTransManip.h


#include <Inventor/manips/SoTransformManip.h>

class RotTransManip : public SoTransformManip
{
   SO_NODE_HEADER(RotTransManip);

  public:
   // Constructor
   RotTransManip();

   // Initialize the class. This should be called once
   // after SoInteraction::init(),
   // TranslateRadialDragger::init().
   // and RotTransDragger::init().
   static void initClass();

  private:
   // Destructor
   ~RotTransManip();
};    

Example 8-8. RotTransManip.c++


#include <Inventor/nodes/SoSurroundScale.h>

SO_NODE_SOURCE(RotTransManip);

//  Initialize the type ID for this manipulator node. This
//  should be called once after SoInteraction::init(),
//  TranslateRadialDragger::initClass()
//  and RotTransDragger::initClass()
void
RotTransManip::initClass()
{
   SO_NODE_INIT_CLASS(RotTransManip, SoTransformManip,
                      "TransformManip");
}

RotTransManip::RotTransManip()
{
   SO_NODE_CONSTRUCTOR(RotTransManip);

   // Create a new dragger and call setDragger(), 
   // a method inherited from SoTransformManip.
   RotTransDragger *myDrag = new RotTransDragger;
   setDragger(myDrag);

   // We want this manipulator to surround the objects it
   // affects when we put it in a scene. So create the
   // surroundScale node.
   SoSurroundScale *mySS = (SoSurroundScale *) 
     myDrag->getPart("surroundScale",TRUE);
   mySS->numNodesUpToContainer = 4;
   mySS->numNodesUpToReset = 3;
}

RotTransManip::~RotTransManip()
{
}

Overview

Examples 8-9 and 8-10 create the Coord3Manip class, which requires more work than the RotTransManip. Use the SO_NODE_HEADER() and SO_NODE_SOURCE() macros found in SoSubNode.h. Follow the same basic steps required for any node class, plus some additional ones, as follows:

  1. Select a name for the new manipulator class and determine what class it is derived from. The new manipulator will be a subclass of the kind of node you want to provide a user interface for. For example, Coord3Manip is a subclass of SoCoordinate3 because it provides a user interface for editing the SoCoordinate3 node.

  2. Define an initClass() method to initialize type information for the manipulator. Use the SO_NODE_INIT_CLASS() macro.

  3. Define a constructor for the manipulator. The constructor defines and names any new fields required by the manipulator using the SO_NODE_ADD_FIELD() macro. It also creates the field sensor (see step 7) and uses the setDragger() method to add the correct dragger as a child of this manipulator.

  4. Define a destructor for the manipulator. If the manipulator created a field sensor in the constructor, it will need to delete it in the destructor.

  5. Write the routines for replacing the “regular” node in the scene graph with the editable manipulator node and for putting it back (replaceNode() and replaceManip()).

  6. Write the value-changed callback on the dragger to update the manipulator's field if the dragger's location changes.

  7. Write the field sensor callback on the manipulator to update the dragger if the field in the manipulator changes.

  8. Implement a setDragger() method to allow the dragger to be replaced by another dragger.

  9. Implement a copy() method. The copy() method first copies the manipulator node and its field data. Then it copies the manipulator's children.

  10. Implement the actions supported by the manipulator. See “Implementing Actions for the Manipulator”.

Implementing Actions for the Manipulator

Example 8-10 provides a typical model for how most manipulators implement actions. The doAction() method for this new manipulator is similar to that for a group: it simply traverses the manipulator's children.

For most other actions, the manipulator first traverses its children (including the dragger) using doAction() and then calls the method on the base class. See the callback(), GLRender(), handleEvent(), and pick() methods in Example 8-10.

Exceptions are the getMatrix() and getBoundingBox() methods. The getMatrix() method does not use doAction() because it doesn't need to traverse all the children. It performs the same tests as SoGroup::getMatrix() (see Chapter 2).

The getBoundingBox() method first traverses the children, but it does some extra work to determine the center of the group. Then it traverses the base class (in the example, “this” is SoCoordinate3).

Deriving a Class from SoCoordinate3

Example 8-9 shows the header file for the Coord3Manip class. Example 8-10 shows the source code for this new class. Although these methods are quite lengthy, they are almost identical for all manipulator base classes. The only difference is the class name used. Also, in the replaceNode() and replaceManip() methods, the appropriate fields must be transferred from the old node to the new one. In this case, the field, point, is the only one copied.

The replaceNode() method must take care of the case where the child is owned by a node kit as well as the case where the child is simply a member of the group. To replace a node that is a part within a node kit, you must set the part by name:

pointFieldSensor->detach();
point = oldPart->point;
Coordinate3Manip::fieldSensorCB(this, NULL);
pointFieldSensor->attach(&point);

lastKit->setPart(partName, this);

To replace a node that is not contained in a node kit, you simply call replaceChild():

pointFieldSensor->detach();
point = ((SoCoordinate3 *)myFullPTail)->point;
Coordinate3Manip::fieldSensorCB(this, NULL);
pointFieldSensor->attach(&point);

((SoGroup *)parent)->replaceChild(myFullPTail, this);

Similarly, replaceManip() must deal separately with nodes that are owned by node kits and nodes that are not.

Example 8-9. Coord3Manip.h


#include <Inventor/draggers/SoDragger.h>
#include <Inventor/fields/SoSFLong.h>
#include <Inventor/nodes/SoCoordinate3.h>
#include <Inventor/sensors/SoFieldSensor.h>

class Coordinate3Manip : public SoCoordinate3
{
   SO_NODE_HEADER(Coordinate3Manip);

  public:
   // Constructor
   Coordinate3Manip();

   // The index of the 'point' field that will be edited 
   // by our child-dragger.
   SoSFLong editIndex;

   // Returns the dragger node being employed by this manip.
   SoDragger *getDragger();

   virtual SoNode *copy(SbBool copyConnections = FALSE) const;

   // For replacing a regular SoCoordinate3 node with this
   // manipulator.
   SbBool replaceNode(SoPath *p);

   // For replacing this manipulator with a regular 
   // SoCoordinate3 node.
   SbBool replaceManip(SoPath *p, SoCoordinate3 *newOne) const;

   // These functions implement all actions for this manip.
   // They first traverse the children, then use the 
   // SoCoordinate3 version of the actions. They traverse first 
   // so that the SoCoordinate3 will affect objects which 
   // follow it in the tree, but not the dragger-child.
   virtual void doAction(SoAction *action);
   virtual void callback(SoCallbackAction *action);
   virtual void GLRender(SoGLRenderAction *action);
   virtual void getBoundingBox(SoGetBoundingBoxAction *action);
   virtual void getMatrix(SoGetMatrixAction *action);
   virtual void handleEvent(SoHandleEventAction *action);
   virtual void pick(SoPickAction *action);
   virtual void search(SoSearchAction *action);

   // call this after SoInteraction::init();
   static void initClass();

   virtual SoChildList *getChildren() const;

  protected:

   // When the dragger moves, this interprets the translation 
   // field of the dragger and sets the point field of this 
   // node accordingly.
   static void valueChangedCB(void *,SoDragger *);

   // When the point field of this node changes, moves the
   // child-dragger to a new location, if necessary.
   SoFieldSensor *pointFieldSensor;
   static void fieldSensorCB(void *, SoSensor *);

   // Establishes the given dragger as the new child-dragger
   void setDragger(SoDragger *newDragger);

   // The hidden children.
   SoChildList *children;

  private:

   // Destructor
   ~Coordinate3Manip();

   int getNumChildren() const 
{ return (children->getLength()); }
};    


Example 8-10. Coord3Manip.c++


#include <Inventor/actions/SoCallbackAction.h>
#include <Inventor/actions/SoGLRenderAction.h>
#include <Inventor/actions/SoGetBoundingBoxAction.h>
#include <Inventor/actions/SoGetMatrixAction.h>
#include <Inventor/actions/SoHandleEventAction.h>
#include <Inventor/actions/SoPickAction.h>
#include <Inventor/actions/SoSearchAction.h>
#include <Inventor/draggers/SoDragPointDragger.h>
#include <Inventor/errors/SoDebugError.h>
#include <Inventor/fields/SoSFLong.h>
#include <Inventor/nodes/SoGroup.h>

// Include file for our new class
#include "Coordinate3Manip.h"

SO_NODE_SOURCE(Coordinate3Manip);

//  Initializes the type ID for this manipulator node. This
//  should be called once after SoInteraction::init().
void
Coordinate3Manip::initClass()
{
   SO_NODE_INIT_CLASS(Coordinate3Manip, SoCoordinate3,
                      "Coordinate3");
}


Coordinate3Manip::Coordinate3Manip()
{
   children = new SoChildList(this);

   SO_NODE_CONSTRUCTOR(Coordinate3Manip);

   // Create the new 'editIndex' field
   SO_NODE_ADD_FIELD(editIndex, (0));

   // Create the field sensor
   pointFieldSensor = new SoFieldSensor(
            &Coordinate3Manip::fieldSensorCB, this);
   pointFieldSensor->setPriority(0);
   pointFieldSensor->attach(&point);

   // Create a new SoDragPointDragger and use
   // it for our child-dragger.
   setDragger(new SoDragPointDragger);
}

Coordinate3Manip::~Coordinate3Manip()
{
   // Important to do this because dragger may have callbacks
   // to this node.
   setDragger(NULL);

   if (pointFieldSensor!=NULL)
     delete pointFieldSensor;
   delete children;
}

// Sets the dragger to be the given node...
// Adds it as a child and adds a valueChangedCallback  
// on the child to tell this node when the dragger moves.
void
Coordinate3Manip::setDragger(SoDragger *newDragger)
{
   SoDragger *oldDragger = getDragger();
   if (oldDragger) {
     oldDragger->removeValueChangedCallback(
              &Coordinate3Manip::valueChangedCB,this);
     children->remove(0);
   }
      
   if (newDragger!=NULL) {
     if (children->getLength() > 0)
       children->set(0, newDragger);
     else
       children->append(newDragger);
     // Call the fieldSensorCB to transfer our values 
     // into the new dragger.
     Coordinate3Manip::fieldSensorCB(this, NULL);
     newDragger->addValueChangedCallback(
              &Coordinate3Manip::valueChangedCB,this);
   }
}

// Returns value of the current dragger.
SoDragger *
Coordinate3Manip::getDragger()
{
   if (children->getLength() > 0) {
     SoNode *n = (*children)[0];
     if (n->isOfType(SoDragger::getClassTypeId()))
       return ((SoDragger *) n);
     else {
#ifdef DEBUG
       SoDebugError::post("Coordinate3Manip::getDragger",
                          "Child is not a dragger!");
#endif
     }
   }
   return NULL;
}

// Description:
//    Replaces the tail of the path with this manipulator.
//
//    [1] Tail of fullpath must be correct type, or we return.
//    [2] If path has a nodekit, we try to use setPart() to 
//        insert manip. otherwise:
//    [3] Path must be long enough, or we return without 
//    [4] replacing.Second to last node must be a group, or we //        return without replacing.
//    [5] Copy values from node we are replacing into this manip
//    [6] Replace this manip as the child.
//    [7] Do not ref or unref anything. Assume that the user 
//        knows what he's doing.
//    [8] Do not fiddle with either node's field connections. 
//        Assume that the user knows what he's doing.
//
SbBool
Coordinate3Manip::replaceNode(SoPath *inPath)
{
   SoFullPath *myFullPath = (SoFullPath *) inPath;

   SoNode     *myFullPTail = myFullPath->getTail();
   if (!myFullPTail->isOfType (Coordinate3Manip::getClassTypeId())) {
#ifdef DEBUG
     SoDebugError::post("Coordinate3Manip::replaceNode", 
                        "End of path is not a Coordinate3Manip");
#endif
     return FALSE;
   }

   SoNode *pTail = inPath->getTail();
   if (pTail->isOfType(SoBaseKit::getClassTypeId())) {

     // Okay, we've got a nodekit here! Let's do this right...
     // If myFullPTail is a part in the kit, then we've got to 
     // follow protocol and let the kit set the part itself.
     SoBaseKit *lastKit = 
                (SoBaseKit*)((SoNodeKitPath*)inPath)->getTail();
     SbString partName = lastKit->getPartString(inPath);
     if (partName != "") {
       SoCoordinate3 *oldPart =
                (SoCoordinate3 *) lastKit->getPart(partName, TRUE); 
       if (oldPart != NULL) {

         // Detach the sensor while copying the values.
         pointFieldSensor->detach();
         point = oldPart->point;
         Coordinate3Manip::fieldSensorCB(this, NULL);
         pointFieldSensor->attach(&point);

         lastKit->setPart(partName, this);
         return TRUE;
       }
       else {
         // Although the part's there, we couldn't get at it.
         // Some kind of problem going on
         return FALSE;
       }
     }
     // If it's not a part, that means it's contained within a 
     // subgraph underneath a part. For example, it's within 
     // the 'contents' separator of an SoWrapperKit. In that 
     // case, the nodekit doesn't care and we just continue on
     // through...
   }

   if (myFullPath->getLength() < 2) {
#ifdef DEBUG
     SoDebugError::post("Coordinate3Manip::replaceNode",
                       "Path is too short!");
#endif
     return FALSE;
   }

   SoNode      *parent = myFullPath->getNodeFromTail(1);
   if (!parent->isOfType( SoGroup::getClassTypeId() )) {
#ifdef DEBUG
     SoDebugError::post("Coordinate3Manip::replaceNode",
                        "Parent node is not a group.!");
#endif
     return FALSE;
   }

   ref();

   // Detach the sensor while copying the values.
   pointFieldSensor->detach();
   point = ((SoCoordinate3 *) myFullPTail)->point;
   Coordinate3Manip::fieldSensorCB(this, NULL);
   pointFieldSensor->attach(&point);

   ((SoGroup *) parent)->replaceChild(myFullPTail, this);

   unrefNoDelete();
   return TRUE;
}

// Replaces tail of path (which should be this manipulator)
// with the given SoCoordinate3 node.
//
//    [1] Tail of fullpath must be this node, or we return.
//    [2] If path has a nodekit, we try to use setPart() to 
//        insert new node. otherwise:
//    [3] Path must be long enough, or we return without 
//    [4] replacing. Second to last node must be a group, or we
//        return without replacing.
//    [5] Copy values from node we are replacing into this manip
//    [6] Replace this manip as the child.
//    [7] Do not ref or unref anything. Assume that the user 
//        knows what he's doing.
//    [8] Do not fiddle with either node's field connections. 
//        Assume that the user knows what he's doing.
//
SbBool
Coordinate3Manip::replaceManip(SoPath *path, SoCoordinate3 
                               *newOne) const
{
   SoFullPath *myFullPath = (SoFullPath *) path;

   SoNode     *myFullPTail = myFullPath->getTail();
   if (myFullPTail != this) {
#ifdef DEBUG
     SoDebugError::post("Coordinate3Manip::replaceManip",
                        "Child to replace is not this manip!");
#endif
     return FALSE;
   }

   SoNode *pTail = path->getTail();
   if (pTail->isOfType(SoBaseKit::getClassTypeId())) {

     // Okay, we've got a nodekit here! Let's do this right...
     // If myFullPTail is a part in the kit, then we've got to 
     // follow protocol and let the kit set the part itself.
     SoBaseKit *lastKit = (SoBaseKit *) 
                         ((SoNodeKitPath*) path)->getTail();
     SbString partName = lastKit->getPartString(path);
     if (partName != "") {
   
       if (newOne != NULL)
         newOne->point = point;
     
         lastKit->setPart(partName, newOne);
         return TRUE;
     }
     // If it's not a part, that means it's contained within a
     // subgraph underneath a part. For example, it's within the 
     // 'contents' separator of an SoWrapperKit. In that case, 
     // the node kit doesn't care and we just continue on
     // through...
   }

   if (myFullPath->getLength() < 2) {
#ifdef DEBUG
     SoDebugError::post("Coordinate3Manip::replaceManip", 
                        "Path is too short!");
#endif
     return FALSE;
   }
   SoNode      *parent = myFullPath->getNodeFromTail(1);
   if (! parent->isOfType(SoGroup::getClassTypeId())) {
#ifdef DEBUG
     SoDebugError::post("Coordinate3Manip::replaceManip",
                        "Parent node is not a group.!");
#endif
     return FALSE;
   }

   if (newOne == NULL)
     newOne = new SoCoordinate3;
   newOne->ref();
   newOne->point = point;
   ((SoGroup *) parent)->replaceChild((Coordinate3Manip *) this,
                                      newOne);
   newOne->unrefNoDelete();

   return TRUE;
}
//    Creates and returns an exact copy...
SoNode *
Coordinate3Manip::copy(SbBool copyConnections) const
{
   // Create a copy of the node and fieldData
   Coordinate3Manip *newManip = (Coordinate3Manip *) 
     SoCoordinate3::copy(copyConnections);

   // Copy the children
   for (int i = 0; i < children->getLength(); i++)
     newManip->children->append(
       (*children)[i]->copy(copyConnections));

   return newManip;
}

//    Returns the child list...
SoChildList *
Coordinate3Manip::getChildren() const
{
   return children;
}

void 
Coordinate3Manip::doAction(SoAction *action)
{
   int         numIndices;
   const int   *indices;

   if (action->getPathCode(numIndices, indices) 
     == SoAction::IN_PATH)
     children->traverse(action, 0, indices[numIndices - 1]);
   else
     children->traverse(action);
}

// These functions implement all actions for Coordinate3Manip.
void
Coordinate3Manip::getMatrix(SoGetMatrixAction *action)
{
   int         numIndices;
   const int   *indices;

   switch (action->getPathCode(numIndices, indices)) {
     case SoAction::NO_PATH:
       break;
     case SoAction::IN_PATH:
       children->traverse(action, 0,indices[numIndices - 1]);
       break;
     case SoAction::BELOW_PATH:
       break;
     case SoAction::OFF_PATH:
       children->traverse(action);
       break;
   }
}

void 
Coordinate3Manip::callback(SoCallbackAction *action)
{ 
   Coordinate3Manip::doAction(action);
   SoCoordinate3::callback(action);
}

void 
Coordinate3Manip::getBoundingBox(
   SoGetBoundingBoxAction *action)
{ 
   SbVec3f     totalCenter(0,0,0);
   int         numCenters = 0;
   int         numIndices;
   const int   *indices;
   int         lastChild;

   if (action->getPathCode(numIndices, indices) 
     == SoAction::IN_PATH)
     lastChild = indices[numIndices - 1];
   else
     lastChild = getNumChildren() - 1;

   // Traverse the children
   for (int i = 0; i <= lastChild; i++) {
     children->traverse(action, i, i);
     if (action->isCenterSet()) {
       totalCenter += action->getCenter();
       numCenters++;
       action->resetCenter();
     }
   }

   // Traverse this as an SoCoordinate3
   SoCoordinate3::getBoundingBox(action);
   if (action->isCenterSet()) {
     totalCenter += action->getCenter();
     numCenters++;
     action->resetCenter();
   }

   // Now, set the center to be the average:
   if (numCenters != 0)
     action->setCenter(totalCenter / numCenters, FALSE);
}

void 
Coordinate3Manip::GLRender(SoGLRenderAction *action)
{
   Coordinate3Manip::doAction(action); 
   SoCoordinate3::GLRender(action);
}

void 
Coordinate3Manip::handleEvent(SoHandleEventAction *action)
{ 
   Coordinate3Manip::doAction(action); 
   SoCoordinate3::handleEvent(action);
}

void 
Coordinate3Manip::pick(SoPickAction *action)
{ 
  Coordinate3Manip::doAction(action); 
  SoCoordinate3::pick(action);
}

void
Coordinate3Manip::valueChangedCB(void *inManip, 
   SoDragger *inDragger)
{
   Coordinate3Manip *manip = (Coordinate3Manip *) inManip;

   SbMatrix motMat = inDragger->getMotionMatrix();
   SbVec3f location = motMat[3];

   // Disconnect the field sensor
   manip->pointFieldSensor->detach();

   int ind = (int) manip->editIndex.getValue();

   // Set value of the point if it's different.
   if (ind < manip->point.getNum()) {
     if (manip->point[ind] != location)
       manip->point.set1Value(ind,location);
   }

   // Reconnect the field sensors
   manip->pointFieldSensor->attach(&manip->point);
}

void
Coordinate3Manip::fieldSensorCB(void *inManip, SoSensor *)
{
   Coordinate3Manip *manip = (Coordinate3Manip *) inManip;

   int ind = manip->editIndex.getValue();

   // Set value of the point if it's different.
   if (ind < manip->point.getNum()) {

     SoDragger *dragger = manip->getDragger();

     if (dragger!=NULL) {
       SbVec3f location = manip->point[ind];
       SbMatrix newMat = dragger->getMotionMatrix();
       newMat[3][0] = location[0];
       newMat[3][1] = location[1];
       newMat[3][2] = location[2];

       dragger->setMotionMatrix(newMat);
     }
   }
}