Chapter Objectives
After reading this chapter, you'll be able to do the following:
Connect draggers to fields or engines in the scene graph
Explain the difference between a dragger and a manipulator
Write callback functions that are performed when interaction starts or finishes, when the mouse moves, or when the value in a dragger's field changes
Use manipulators in your application to allow the user to edit nodes in the scene graph directly
Customize the appearance of a dragger
This chapter describes how to use draggers and manipulators, which are special objects in the scene graph that have a user interface and respond to events. Manipulators, such as the handle box, trackball, and directional light manipulator, are nodes that employ draggers to enable the user to interact with them and edit them. For information on how draggers receive and respond to events, see Chapter 10.
A dragger is a node in the scene graph with specialized behavior that enables it to respond to user events. All Inventor draggers have a built-in user interface, and they insert geometry into the scene graph that is used for picking and user feedback. Figure 15-1 shows the class tree for dragger classes.
For all draggers subclassed from SoDragger, the user employs a click-drag-release motion with the mouse. Table 15-1 indicates the use of each dragger subclassed from SoDragger. For example, the drag-point dragger responds to dragging by translating in three dimensions.
Subclasses of SoDragger fall into two general categories: simple draggers and compound draggers. In general, simple draggers perform only one operation, such as a scale or a translation. Compound draggers perform several operations and are composed of multiple simple draggers. Simple draggers can be used in three ways:
You can connect the field of a simple dragger to other fields or to engines in the scene graph. This is a simple way to set up dependencies within the scene graph.
You can write callback functions that are performed when interaction starts or finishes, whenever the mouse moves, or when the value in the dragger's field changes.
You can use the simple draggers as building blocks to create more complex draggers.
Compound draggers are similar to simple draggers, except that they have more parts because they are comprised of two or more draggers. The SoTransformBoxDragger, for example, uses a scale dragger, three rotators, and six translators.
Dragger | Use |
---|---|
SoCenterballDragger | rotation, center |
SoDirectionalLightDragger | rotation |
SoDragPointDragger | translation |
SoHandleBoxDragger | translation, scale |
SoJackDragger | rotation, translation, uniform scale (three dimensions) |
SoPointLightDragger | translation |
SoRotateCylindricalDragger | rotation |
SoRotateDiscDragger | rotation |
SoRotateSphericalDragger | rotation |
SoScale1Dragger | scale (one dimension) |
SoScale2Dragger | scale (two dimensions) |
SoScaleUniformDragger | uniform scale (three dimensions) |
SoScale2UniformDragger | uniform scale (two dimensions) |
SoSpotLightDragger | translation, rotation, cone angle |
SoTabBoxDragger | scale, translation |
SoTabPlaneDragger | scale (two dimensions), translation (two dimensions) |
SoTrackballDragger | rotation, scale |
SoTransformBoxDragger | rotation, translation, scale |
SoTranslate1Dragger | translation (one dimension) |
SoTranslate2Dragger | translation (two dimensions) |
Manipulators are subclasses of other nodes (such as SoTransform or SoDirectionalLight) that employ draggers (as hidden children) to respond to user events and edit themselves. Figure 15-2 shows the portions of the class tree that contain manipulator classes. Each manipulator contains a dragger that responds directly to user events and in turn modifies the fields of the manipulator. A manipulator inserts geometry into the scene that provides feedback to the user; this geometry is provided by the manipulator's dragger. An SoHandleBoxManip, for example, inserts cubes and lines into the scene that allow the user to edit the scale and translate fields of an SoTransform node by moving the mouse in various ways (see Figure 15-3). This geometry is part of the SoHandleBoxDragger contained within the SoHandleBoxManip manipulator. An SoTrackballManip allows the user to edit the rotation field of an SoTransform node by inserting a sphere surrounded by three ribbons into the scene (see Figure 15-4). The user can then rotate or scale the object inside this trackball.
A dragger moves only itself when it responds to user events. A manipulator, on the other hand, moves itself and affects other objects in the scene graph because, as a subclass of SoTransform or SoLight, it functions as a transform or light node and modifies the traversal state. A dragger supplies geometry and a user interface for a manipulator. A manipulator uses the values it receives from the dragger and copies them into its own fields. When interaction finishes and the manipulator is removed from the scene graph, it copies its values into the transform or light node it was replacing.
A manipulator replaces a node in the scene graph, substituting an editable version of that node for the original. When interaction finishes, the original (non-editable) node can be restored. Each manipulator contains a dragger that allows the user to edit its fields. Manipulators derived from SoTransform are as follows:
SoCenterBallManip
SoHandleBoxManip
SoJackManip
SoTabBoxManip
SoTrackballManip
SoTransformBoxManip
Other manipulators include the SoPointLightManip, derived from SoPointLight, and the SoDirectionalLightManip, derived from SoDirectionalLight.
A simple dragger moves in 3D space in response to click-drag-release mouse events. Its position in space is determined by its position in the scene graph. Each simple dragger has a field that reflects the current state of the dragger. For example, the SoRotateDiscDragger has a rotation field that indicates its current rotation value. This field can be connected to other fields in the scene graph or to the input field of an engine (see the following section). Callback functions can also be used with simple draggers, as described in “Callback Functions”.
A convenient way to use a dragger is to connect its fields to other fields or engines in the scene graph. For example, the SoTranslate1Dragger has a translation field that could be used in a variety of ways. Figure 15-5 shows how this field could be used to edit the radius of a cone node. Since the dragger's translation field is an SoSFVec3f, you need to use an SoDecomposeVec3f engine to extract the x value of the dragger's
translation. This x value is then fed into the bottomRadius field of the cone node. Now, whenever the dragger is translated, the radius of the cone changes.
Example 15-1 shows the code for connecting these fields and engines. Figure 15-6 shows an image created by this example.
// Create myDragger with an initial translation of (1,0,0) SoTranslate1Dragger *myDragger = new SoTranslate1Dragger; root->addChild(myDragger); myDragger->translation.setValue(1,0,0); // Place an SoCone below myDragger SoTransform *myTransform = new SoTransform; SoCone *myCone = new SoCone; root->addChild(myTransform); root->addChild(myCone); myTransform->translation.setValue(0,3,0); // SoDecomposeVec3f engine extracts myDragger's x-component // The result is connected to myCone's bottomRadius. SoDecomposeVec3f *myEngine = new SoDecomposeVec3f; myEngine->vector.connectFrom(&myDragger->translation); myCone->bottomRadius.connectFrom(&myEngine->x); |
Any dragger or manipulator can use callback functions to pass data back to the application. This callback mechanism can be used to augment the default functionality of the dragger or manipulator. Several lists of callback functions and associated data, of class SoCallbackList, are automatically created when a dragger is constructed. You can add functions to and remove functions from these lists and pass a pointer to the user callback data. Draggers use these lists of callback functions:
The following methods add functions to and remove functions from these callback lists:
addStartCallback(functionName, userData)
removeStartCallback(functionName, userData)
addMotionCallback(functionName, userData)
removeMotionCallback(functionName, userData)
addValueChangedCallback(functionName, userData)
removeValueChangedCallback(functionName, userData)
addFinishCallback(functionName, userData)
removeFinishCallback(functionName, userData)
These methods are called on SoDragger. To call one of these methods on the manipulator, call getDragger() first, then call the callback list method.
For example, you could write a start callback function that would turn an object to wireframe during manipulation and a finish callback function that would turn it back to filled drawing style when manipulation finishes. You could write a value-changed callback to find out when the value being manipulated has changed, and then use the getValue() method to obtain the field's new value.
Example 15-2 uses three translate1Draggers to change the x, y, and z components of a translation that affects some 3D text. Figure 15-7 shows two images created by this program.
// Uses 3 translate1Draggers to change the x, y, and z // components of a translation. A calculator engine assembles // the components. // Arranges these draggers along edges of a box containing the // 3D text to be moved. // The 3D text and the box are made with SoShapeKits #include <Inventor/engines/SoCalculator.h> #include <Inventor/nodekits/SoShapeKit.h> #include <Inventor/nodes/SoCube.h> #include <Inventor/nodes/SoSeparator.h> #include <Inventor/nodes/SoText3.h> #include <Inventor/nodes/SoTransform.h> #include <Inventor/Xt/SoXt.h> #include <Inventor/Xt/viewers/SoXtExaminerViewer.h> #include <Inventor/draggers/SoTranslate1Dragger.h> main(int , char **argv) { Widget myWindow = SoXt::init(argv[0]); if (myWindow == NULL) exit(1); SoSeparator *root = new SoSeparator; root->ref(); // Create 3 translate1Draggers and place them in space. SoSeparator *xDragSep = new SoSeparator; SoSeparator *yDragSep = new SoSeparator; SoSeparator *zDragSep = new SoSeparator; root->addChild(xDragSep); root->addChild(yDragSep); root->addChild(zDragSep); // Separators will each hold a different transform SoTransform *xDragXf = new SoTransform; SoTransform *yDragXf = new SoTransform; SoTransform *zDragXf = new SoTransform; xDragXf->set("translation 0 -4 8"); yDragXf->set("translation -8 0 8 rotation 0 0 1 1.57"); zDragXf->set("translation -8 -4 0 rotation 0 1 0 -1.57"); xDragSep->addChild(xDragXf); yDragSep->addChild(yDragXf); zDragSep->addChild(zDragXf); // Add the draggers under the separators, after transforms SoTranslate1Dragger *xDragger = new SoTranslate1Dragger; SoTranslate1Dragger *yDragger = new SoTranslate1Dragger; SoTranslate1Dragger *zDragger = new SoTranslate1Dragger; xDragSep->addChild(xDragger); yDragSep->addChild(yDragger); zDragSep->addChild(zDragger); // Create shape kit for the 3D text // The text says 'Slide Arrows To Move Me' SoShapeKit *textKit = new SoShapeKit; root->addChild(textKit); SoText3 *myText3 = new SoText3; textKit->setPart("shape", myText3); myText3->justification = SoText3::CENTER; myText3->string.set1Value(0,"Slide Arrows"); myText3->string.set1Value(1,"To"); myText3->string.set1Value(2,"Move Me"); textKit->set("font { size 2}"); textKit->set("material { diffuseColor 1 1 0}"); // Create shape kit for surrounding box. // It's an unpickable cube, sized as (16,8,16) SoShapeKit *boxKit = new SoShapeKit; root->addChild(boxKit); boxKit->setPart("shape", new SoCube); boxKit->set("drawStyle { style LINES }"); boxKit->set("pickStyle { style UNPICKABLE }"); boxKit->set("material { emissiveColor 1 0 1 }"); boxKit->set("shape { width 16 height 8 depth 16 }"); // Create the calculator to make a translation // for the text. The x component of a translate1Dragger's // translation field shows how far it moved in that // direction. So our text's translation is: // (xDragTranslate[0],yDragTranslate[0],zDragTranslate[0]) SoCalculator *myCalc = new SoCalculator; myCalc->ref(); myCalc->A.connectFrom(&xDragger->translation); myCalc->B.connectFrom(&yDragger->translation); myCalc->C.connectFrom(&zDragger->translation); myCalc->expression = "oA = vec3f(A[0],B[0],C[0])"; // Connect the the translation in textKit from myCalc SoTransform *textXf = (SoTransform *) textKit->getPart("transform",TRUE); textXf->translation.connectFrom(&myCalc->oA); SoXtExaminerViewer *myViewer = new SoXtExaminerViewer(myWindow); myViewer->setSceneGraph(root); myViewer->setTitle("Slider Box"); myViewer->viewAll(); myViewer->show(); SoXt::show(myWindow); SoXt::mainLoop(); } |
You can use manipulators in your application in various ways:
You can use the replaceNode() method to replace certain kinds of nodes in the scene graph with an editable version. When the user is finished manipulating the node, use the replaceManip() method to restore the original node to the scene graph.
You can write your own callback functions to use the field values of the manipulator. The callback functions described in “Callback Functions” can be used for any manipulator. (Recall that these functions belong to the dragger, so you need to call getDragger() before using them.)
You can also combine use of these two techniques. For example, you can use replaceNode() to replace an SoTransform with a manipulator. Then you can use a value-changed callback to notify the application when any of the manipulator's dragger fields changes, and the application can use this new value, if desired.
The following sections describe both of these techniques in more detail.
To use any manipulator in an application, follow these basic steps:
Construct the manipulator.
Reference it if you plan on reusing it.
Replace the node in the scene graph with the manipulator. Manipulators derived from SoTransform, such as the handle box and trackball, replace an SoTransform node. An SoDirectionalLight-
Manip replaces an SoDirectionalLight node, an SoPointLightManip replaces an SoPointLight node, and so on.
The replaceNode() method takes a path as an argument:
replaceNode(SoPath *p)
|
The path is supplied by the application. For example, Figure 15-8 shows the path to a target SoTransform node. When a transform manipulator replaces this node, editing the manipulator will affect cube2 in the scene graph.
Manipulators subclassed from SoTransformManip use special nodes to maintain their shape (so that the trackball remains spherical, for example) and to ensure that they surround the shape objects they affect. These nodes are described in The Inventor Toolmaker.
To remove the manipulator from the scene graph:
Because the manipulator methods replaceManip() and replaceNode() exchange the new node for the tail of the given path, you can reuse the path for subsequent calls to these methods.
For example, if we begin with:
myManip = new SoTrackballManip; myPathToTransform = createPathtoTransform(pickPath); |
Then we can call:
myManip->replaceNode(myPathToTransform); |
to put the manipulator at the end of the path.
Later, we can call
myManip->replaceManip(myPathToTransform, new SoTransform); |
to remove the manipulator and replace it with a transform.
Example 15-3 displays a cube, a sphere, and a lamp. The lamp is read from a file and inserted as the “contents” part of an SoWrapperKit. When the user picks the cube, a trackball replaces the transform node that affects the cube. When the user picks the sphere, a handle box replaces the transform node that affects the sphere. When the user picks the lamp, a transform box replaces the “transform” part of the wrapper kit containing the lamp. Figure 15-9 shows an image created by this program. This example shows the following techniques:
Using replaceNode() and replaceManip() to make certain nodes in the scene graph editable and to restore the original nodes when manipulation finishes
Using selection callbacks (see Chapter 10)
// Note that for illustration purposes, the // cube and SoWrapperKit already have transform nodes // associated with them; the sphere does not. In all cases, // the routine createTransformPath() is used to find the // transform node that affects the picked object. #include <Inventor/SoDB.h> #include <Inventor/SoInput.h> #include <Inventor/manips/SoHandleBoxManip.h> #include <Inventor/manips/SoTrackballManip.h> #include <Inventor/manips/SoTransformBoxManip.h> #include <Inventor/nodekits/SoWrapperKit.h> #include <Inventor/nodes/SoCamera.h> #include <Inventor/nodes/SoCube.h> #include <Inventor/nodes/SoGroup.h> #include <Inventor/nodes/SoLight.h> |
#include <Inventor/nodes/SoMaterial.h> #include <Inventor/nodes/SoSelection.h> #include <Inventor/nodes/SoSphere.h> #include <Inventor/nodes/SoTransform.h> #include <Inventor/Xt/SoXt.h> #include <Inventor/Xt/viewers/SoXtExaminerViewer.h> // function prototypes void selectionCallback(void *, SoPath *); void deselectionCallback(void *, SoPath *); void dragStartCallback(void *, SoDragger *); void dragFinishCallback(void *, SoDragger *); // global data SoSeparator *root; SoHandleBoxManip *myHandleBox; SoTrackballManip *myTrackball; SoTransformBoxManip *myTransformBox; SoPath *handleBoxPath = NULL; SoPath *trackballPath = NULL; SoPath *transformBoxPath = NULL; main(int, char **argv) { // Initialize Inventor and Xt Widget myWindow = SoXt::init(argv[0]); if (myWindow == NULL) exit(1); // Create and set up the selection node SoSelection *selectionRoot = new SoSelection; selectionRoot->ref(); selectionRoot-> addSelectionCallback(selectionCallback, NULL); selectionRoot-> addDeselectionCallback(deselectionCallback, NULL); // Create the scene graph root = new SoSeparator; selectionRoot->addChild(root); // Read a file into contents of SoWrapperKit // Translate it to the right. SoWrapperKit *myWrapperKit = new SoWrapperKit; root->addChild(myWrapperKit); SoInput myInput; if (!myInput.openFile("luxo.iv")) return (1); SoSeparator *objectFromFile = SoDB::readAll(&myInput); if (objectFromFile == NULL) return (1); myWrapperKit->setPart("contents",objectFromFile); myWrapperKit->set("transform { translation 3 -1 0 }"); SoMaterial *wrapperMat = (SoMaterial *) myWrapperKit->getPart("material",TRUE); wrapperMat->diffuseColor.setValue(.8, .8, .8); // Create a cube with its own transform. SoSeparator *cubeRoot = new SoSeparator; SoTransform *cubeXform = new SoTransform; cubeXform->translation.setValue(-4, 0, 0); root->addChild(cubeRoot); cubeRoot->addChild(cubeXform); SoMaterial *cubeMat = new SoMaterial; cubeMat->diffuseColor.setValue(.8, .8, .8); cubeRoot->addChild(cubeMat); cubeRoot->addChild(new SoCube); // Add a sphere node without a transform // (one will be added when we attach the manipulator) SoSeparator *sphereRoot = new SoSeparator; SoMaterial *sphereMat = new SoMaterial; root->addChild(sphereRoot); sphereRoot->addChild(sphereMat); sphereRoot->addChild(new SoSphere); sphereMat->diffuseColor.setValue(.8, .8, .8); // Create the manipulators myHandleBox = new SoHandleBoxManip; myHandleBox->ref(); myTrackball = new SoTrackballManip; myTrackball->ref(); myTransformBox = new SoTransformBoxManip; myTransformBox->ref(); // Get the draggers and add callbacks to them. Note // that you don't put callbacks on manipulators. You put // them on the draggers which handle events for them. SoDragger *myDragger; myDragger = myTrackball->getDragger(); myDragger->addStartCallback(dragStartCallback,cubeMat); myDragger->addFinishCallback(dragFinishCallback,cubeMat); myDragger = myHandleBox->getDragger(); myDragger->addStartCallback(dragStartCallback,sphereMat); myDragger->addFinishCallback(dragFinishCallback,sphereMat); myDragger = myTransformBox->getDragger(); myDragger->addStartCallback(dragStartCallback,wrapperMat); myDragger->addFinishCallback(dragFinishCallback,wrapperMat); SoXtExaminerViewer *myViewer = new SoXtExaminerViewer(myWindow); myViewer->setSceneGraph(selectionRoot); myViewer->setTitle("Attaching Manipulators"); myViewer->show(); myViewer->viewAll(); SoXt::show(myWindow); SoXt::mainLoop(); } // Is this node of a type that is influenced by transforms? SbBool isTransformable(SoNode *myNode) { if (myNode->isOfType(SoGroup::getClassTypeId()) || myNode->isOfType(SoShape::getClassTypeId()) || myNode->isOfType(SoCamera::getClassTypeId()) || myNode->isOfType(SoLight::getClassTypeId())) return TRUE; else return FALSE; } // Create a path to the transform node that affects the tail // of the input path. Three possible cases: // [1] The path-tail is a node kit. Just ask the node kit for // a path to the part called "transform" // [2] The path-tail is NOT a group. Search siblings of path // tail from right to left until you find a transform. If // none is found, or if another transformable object is // found (shape,group,light,or camera), then insert a // transform just to the left of the tail. This way, the // manipulator only affects the selected object. // [3] The path-tail IS a group. Search its children left to // right until a transform is found. If a transformable // node is found first, insert a transform just left of // that node. This way the manip will affect all nodes // in the group. SoPath * createTransformPath(SoPath *inputPath) { int pathLength = inputPath->getLength(); if (pathLength < 2) // Won't be able to get parent of tail return NULL; SoNode *tail = inputPath->getTail(); // CASE 1: The tail is a node kit. // Nodekits have built in policy for creating parts. // The kit copies inputPath, then extends it past the // kit all the way down to the transform. It creates the // transform if necessary. if (tail->isOfType(SoBaseKit::getClassTypeId())) { SoBaseKit *kit = (SoBaseKit *) tail; return kit->createPathToPart("transform",TRUE,inputPath); } SoTransform *editXf = NULL; SoGroup *parent; SbBool existedBefore = FALSE; // CASE 2: The tail is not a group. SbBool isTailGroup; isTailGroup = tail->isOfType(SoGroup::getClassTypeId()); if (!isTailGroup) { // 'parent' is node above tail. Search under parent right // to left for a transform. If we find a 'movable' node // insert a transform just left of tail. parent = (SoGroup *) inputPath->getNode(pathLength - 2); int tailIndx = parent->findChild(tail); for (int i = tailIndx; (i >= 0) && (editXf == NULL);i--){ SoNode *myNode = parent->getChild(i); if (myNode->isOfType(SoTransform::getClassTypeId())) editXf = (SoTransform *) myNode; else if (i != tailIndx && (isTransformable(myNode))) break; } if (editXf == NULL) { existedBefore = FALSE; editXf = new SoTransform; parent->insertChild(editXf, tailIndx); } else existedBefore = TRUE; } // CASE 3: The tail is a group. else { // Search the children from left to right for transform // nodes. Stop the search if we come to a movable node // and insert a transform before it. parent = (SoGroup *) tail; for (int i = 0; (i < parent->getNumChildren()) && (editXf == NULL); i++) { SoNode *myNode = parent->getChild(i); if (myNode->isOfType(SoTransform::getClassTypeId())) editXf = (SoTransform *) myNode; else if (isTransformable(myNode)) break; } if (editXf == NULL) { existedBefore = FALSE; editXf = new SoTransform; parent->insertChild(editXf, i); } else existedBefore = TRUE; } // Create 'pathToXform.' Copy inputPath, then make last // node be editXf. SoPath *pathToXform = NULL; pathToXform = inputPath->copy(); pathToXform->ref(); if (!isTailGroup) // pop off the last entry. pathToXform->pop(); // add editXf to the end int xfIndex = parent->findChild(editXf); pathToXform->append(xfIndex); pathToXform->unrefNoDelete(); return(pathToXform); } // This routine is called when an object // gets selected. We determine which object // was selected, then call replaceNode() // to replace the object's transform with // a manipulator. void selectionCallback( void *, // user data is not used SoPath *selectionPath) { // Attach the manipulator. // Use the convenience routine to get a path to // the transform that affects the selected object. SoPath *xformPath = createTransformPath(selectionPath); if (xformPath == NULL) return; xformPath->ref(); // Attach the handle box to the sphere, // the trackball to the cube // or the transformBox to the wrapperKit if (selectionPath->getTail()->isOfType( SoSphere::getClassTypeId())) { handleBoxPath = xformPath; myHandleBox->replaceNode(xformPath); } else if (selectionPath->getTail()-> isOfType(SoCube::getClassTypeId())) { trackballPath = xformPath; myTrackball->replaceNode(xformPath); } else if (selectionPath->getTail()-> isOfType(SoWrapperKit::getClassTypeId())) { transformBoxPath = xformPath; myTransformBox->replaceNode(xformPath); } } // This routine is called whenever an object gets // deselected. It detaches the manipulator from // the transform node, and removes it from the // scene graph that will not be visible. void deselectionCallback( void *, // user data is not used SoPath *deselectionPath) { if (deselectionPath->getTail()-> isOfType(SoSphere::getClassTypeId())) { myHandleBox->replaceManip(handleBoxPath,NULL); handleBoxPath->unref(); } else if (deselectionPath->getTail()-> isOfType(SoCube::getClassTypeId())) { myTrackball->replaceManip(trackballPath,NULL); trackballPath->unref(); } else if (deselectionPath->getTail()-> isOfType(SoWrapperKit::getClassTypeId())) { myTransformBox->replaceManip(transformBoxPath,NULL); transformBoxPath->unref(); } } // This is called when a manipulator is // about to begin manipulation. void dragStartCallback( void *myMaterial, // user data SoDragger *) // callback data not used { ((SoMaterial *) myMaterial)->diffuseColor=SbColor(1,.2,.2); } // This is called when a manipulator is // done manipulating. void dragFinishCallback( void *myMaterial, // user data SoDragger *) // callback data not used { ((SoMaterial *) myMaterial)->diffuseColor=SbColor(.8,.8,.8); } |
This section describes how to modify the appearance of a dragger. This customization is performed either by changing the default geometry for a part or by changing the part after an instance of a dragger has been built. Although the look and feel of a dragger can be changed or removed in this manner, no new functionality can be added.
Using the SoTrackballDragger as an example, this section describes the parts of a dragger and how they combine to make the whole dragger. It explains how the geometry for each part can be changed or removed, and how that can affect the functionality of the dragger. Example 15-4 illustrates how parts of an SoTranslate1Dragger can be changed after it has been built.
Every dragger is a node kit that is constructed out of parts. A part is simply a piece of the dragger that has some task associated with it. Often, two parts act as a pair. One part is displayed when it is in use (or active), and the other is displayed when that part is not in use (or inactive). For example, for the
trackball's inactive “XRotator” part, a white stripe is displayed, and for its active “XRotatorActive” part, a yellow stripe is displayed.
Each dragger has a resource file associated with it that contains an Inventor scene graph describing the default geometry for each part. By creating a new resource file, you can override the default and give the part a new shape or new properties such as color or drawing style. In the resource file, scene graphs are labeled with their unique resource names.
Many classes of draggers use the same part names. For example, the trackball, rotate-disc, rotate-cylindrical, and rotate-spherical draggers each have a part named “rotator.” Since the default parts are stored in the global dictionary, each part in each class must have a unique resource name. In all cases, the class name (without the “So” or “Dragger”) is prepended to the part name. Table 15-2 shows how the resource names and part names relate.
For example, the SoTrackballDragger has twelve parts. Table 15-2 lists the resource and part names of eight of these parts (for brevity, the “userRotator” and “userAxis” parts are omitted). When you interact with a trackball dragger, you are actually interacting with its parts. For example, if the mouse goes down over the trackball's “XRotator” part, a rotation about the x-axis is initiated.
Table 15-2. Selected Parts of the SoTrackballDragger
Resource Names | Part Names | Task |
---|---|---|
trackballRotator trackball RotatorActive | rotator | Free rotation |
trackballXRotator trackballXRotatorActive | XRotator | Rotation about x-axis |
trackballYRotator trackballYRotatorActive | YRotator | Rotation about y-axis |
trackballZRotator trackballZRotatorActive | ZRotator | Rotation about z-axis |
To change the part of a dragger that has already been built, use the setPart() or setPartAsPath() method provided by SoInteractionKit. The setPart() method takes the root of a scene graph as a parameter, while setPartAsPath() accepts an SoPath.
For example, to change the “rotator” part of myDragger:
myDragger->setPart("rotator", myNewRotatorSceneGraph); |
To change the “rotator” part of a dragger within a manipulator:
myManip->getDragger()->setPart("rotator", myNewRotatorSceneGraph); |
You can also provide setPartAsPath() with the path to a particular instance of an object in the scene graph. The dragger then uses that object for the part. For example, if you have an arrow used as a weather vane mounted on a post, you could provide the path to the arrow and rotate the arrow itself. (Note the difference here between specifying a node and specifying a path. If you specify the arrow node using setPart(), a new instance of that node is created and two copies of the same geometry appear on the screen. If you specify the path to the arrow using setPartAsPath(), the dragger actually uses the existing arrow node and waits for the user to press the mouse on the same weather vane that is sitting on the barn.)
myRotateManip->getDragger()->setPartAsPath("rotator", pathToMyWeatherVaneArrow); |
Example 15-4 shows how to change the geometry of the draggers in Example 15-2. The “translator” and “translatorActive” parts are now cubes instead of arrows. The setPart() method is used to replace the default parts with the new scene graphs specified here. Figure 15-10 shows the new dragger geometry.
// Create myTranslator and myTranslatorActive. // These are custom geometry for the draggers. SoSeparator *myTranslator = new SoSeparator; SoSeparator *myTranslatorActive = new SoSeparator; myTranslator->ref(); myTranslatorActive->ref(); // Materials for the dragger in regular and active states SoMaterial *myMtl = new SoMaterial; SoMaterial *myActiveMtl = new SoMaterial; myMtl->diffuseColor.setValue(1,1,1); myActiveMtl->diffuseColor.setValue(1,1,0); myTranslator->addChild(myMtl); myTranslatorActive->addChild(myActiveMtl); // Same shape for both versions. SoCube *myCube = new SoCube; myCube->set("width 3 height .4 depth .4"); myTranslator->addChild(myCube); myTranslatorActive->addChild(myCube); // Now, customize the draggers with the pieces we created. xDragger->setPart("translator",myTranslator); xDragger->setPart("translatorActive",myTranslatorActive); yDragger->setPart("translator",myTranslator); yDragger->setPart("translatorActive",myTranslatorActive); zDragger->setPart("translator",myTranslator); zDragger->setPart("translatorActive",myTranslatorActive); |
Every class of dragger has a resource file associated with it that contains Inventor scene graphs defining default geometry for that class. The default geometry for a given class is also compiled in, so that if the dragger resource files are lost, the dragger will still operate.
When a dragger is constructed, it checks whether a resource file for overriding the defaults has been created. When reading from this file (if found), if the dragger encounters a second definition of a particular geometry, the new geometry replaces any previously defined geometry of the same name.
Inventor will look for files only if the environment variable SO_DRAGGER_DIR has been set. If it has, Inventor will look in that directory.
In all cases, a given dragger class will read only files of its same name: the SoTranslate1Dragger class reads only resource files named translate1Dragger.iv, the SoTrackballDragger class reads only files named trackballDragger.iv.
Changing the default geometry of a dragger part is a simple matter of creating a new file that contains a scene graph defining the new resource. When encountered, this new definition overrides the default definitions of that resource. The next two sections show how to do this using the SoTrackballDragger as a foundation.
To change the default geometry of a part, you merely redefine the scene graph for the geometry used by the part. When creating new resource files, it is necessary to define only the geometry that you wish to change. Other geometry will use the default values.
Keep in mind that you should never edit the resource files in Inventor/resources. If you want your application to use alternate resources, put your files in a convenient place and set the SO_DRAGGER_DIR environment variable to point there.
As an example, let's replace the trackballRotator resource of the trackball with a cube. (For more information on the Inventor file format, see Chapter 11.) Looking at the default geometry file for the trackball, we see that the trackballRotator resource is defined by this scene graph:
# default geometry for SoTrackballDragger's "rotator" part (inactive) DEF trackballRotator Separator { DrawStyle { style INVISIBLE } Sphere {} } # default geometry for SoTrackballDragger's "rotatorActive" part DEF trackballRotatorActive Separator { DrawStyle { style INVISIBLE } Sphere {} } |
Note that, in the case of the trackball, the resources specify that the rotator and rotatorActive parts have the same geometry, an invisible sphere. Although this is common, some draggers may have completely different geometry for when they are inactive and active (and most manipulators have more complicated scene graphs than just a sphere).
To change the trackballRotator and trackballRotatorActive resources from an invisible sphere to a visible cube, you simply replace the sphere with a cube in both scene graphs:
# default geometry for the SoTrackballDragger's "rotator" part DEF trackballRotator Separator { BaseColor { rgb 1. 1. 1. #white } Cube {} } # default geometry for the SoTrackballDragger's "rotatorActive" part DEF trackballRotatorActive Separator { BaseColor { rgb .5 .5 0. #yellow } Cube {} } |
Using this mechanism, you can not only change the geometry of a given part, but also remove the functionality of that part entirely. For example, to disable the trackball's "rotator" part but still leave the cube visible, you can make the cube unpickable:
# default geometry for the SoTrackballDragger's "rotator" part DEF trackballRotator Separator { BaseColor { rgb 1. 1. 1. #white } PickStyle { style UNPICKABLE } Cube {} } # default geometry for the SoTrackballDragger's "rotatorActive" part DEF trackballRotatorActive Separator { BaseColor { rgb .5 .5 0. #yellow } PickStyle { style UNPICKABLE } Cube {} } |
To remove the trackball's rotator part altogether, leaving a trackball that can only rotate about its x, y, and z axes, you could redefine its geometry to be an empty scene graph:
# default geometry for SoTrackballDragger's "rotator" part DEF trackballRotator Separator { } # default geometry for SoTrackballDragger's "rotatorActive" part DEF trackballRotatorActive Separator { } |
You can also read the geometry from a file instead of defining it inline:
DEF trackballRotator Separator { File { name "myCustomRotator.iv" } } DEF trackballRotatorActive Separator { File { name "myCustomRotatorActive.iv" } } |
Note: Never set a dragger part to NULL. Internal methods require a node to be present, even if it's simply an empty separator as shown in the previous example. (Often, the dragger parts are the children of a switch node. Changing a node to NULL could result in an incorrect ordering of the switch node's children.) |