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.)
The SoDragger( C++ | Java | .NET ) class, a node kit, constructs the nodes shown in Figure 8.1, “ Structure of SoDragger ”. 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.)
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( C++ | Java | .NET ) 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( C++ | Java | .NET ) 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:
Select a name for the new dragger class and decide what class it is derived from. (It will usually be derived from SoDragger( C++ | Java | .NET ).)
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 the section called “Creating the Field”).
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 the section called “Designing the Parts”).
For the parts in the catalog that determine the user interface:
a. Define a unique resource name.
b. Create a default geometry file (see the section called “Creating the Default Geometry File”).
c. Create the compiled-in geometry using the special utility program provided (see the section called “Creating the Compiled-in Default Geometry”).
Define an initClass() method to initialize the type information (see the section called “Initializing the Dragger Class”).
Define an exitClass() method to clean up the type information. In the exitClass() routine of your class, use the macro SO__KIT_EXIT_CLASS.
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 the section called “Creating the Projector”.
Define a constructor (see the section called “Constructor”).
Implement dragStart(), drag(), and dragFinish() methods. These routines perform the dragging (see the section called “Dragging Callback Functions”).
Implement the value-changed callback function. This function updates the dragger's field to reflect the dragger's current position.
Implement the field sensor callback function. This function causes the dragger to reposition itself whenever changes are made to its field.
Implement the setUpConnections() method, which attaches and detaches field sensors.
Define a destructor (see the section called “Destructor”).
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, “ Structure of TranslateRadialDragger ”.
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 } } }
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.
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:
Use the special utility program provided with Inventor, ivToIncludeFile , which translates an .iv file (such as the one shown in Example 8.1, “ translateRadialDragger.iv ”) 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.
In the include file, within the class definition of the dragger, declare the static variable geomBuffer:
static const char geomBuffer[];
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.
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().
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:
Use SO_KIT_CONSTRUCTOR() to set up the internal variables for the class.
Define the catalog entries for the new dragger (a node kit).
Put the default parts into the global dictionary.
Create the parts list and the parts that are created by default in this dragger using SO_KIT_INIT_INSTANCE().
Create the appropriate field for the dragger—in this case, translation.
Create the parts for the dragger.
Set the switches to inactive (if your dragger uses active/inactive pairs of parts).
Create the projector.
Add the dragger callback functions.
Add the value-changed callback function.
Put a sensor on the field.
Call the setUpConnections() method to attach the field sensors.
Steps 1 through 5 and step 6 are standard procedure for new node kits. Step 7 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.
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.
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
Every dragger has a field (or fields) reflecting its state. This dragger performs translations, so you create a translation field.
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.
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.
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 fromSbProjector( C++ | Java | .NET ) 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( C++ | Java | .NET ). 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();
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.
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, “ Maintaining Consistency Between the Field and the Motion Matrix ”). Use a value-changed callback function to update the translation field.
If the translation field changes, the motionMatrix must be changed so that the dragger moves to that new position (again, see Figure 8.3, “ Maintaining Consistency Between the Field and the Motion Matrix ”). (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.
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.
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( C++ | Java | .NET ). (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.
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( C++ | Java | .NET ). 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( C++ | Java | .NET ) (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, “ TranslateRadialDragger.c++ ” 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.
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( C++ | Java | .NET ) 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)); }
The last thing a dragger needs to do is reset the state of its geometry when dragging is completed. Example 8.3, “ TranslateRadialDragger.c++ ” includes the code for dragFinish().
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)); }
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.
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, “ TranslateRadialDragger.c++ ” shows setUpConnections() for the TranslateRadialDragger class.
Example 8.2, “ TranslateRadialDragger.h ” 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(); static void exitClass(); 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, “ TranslateRadialDragger.c++ ” 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"); } void TranslateRadialDragger::exitClass() { SO__KIT_EXIT_CLASS(TranslateRadialDragger); } 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); }