Chapter 5. Creating an Element

If you want to create nodes that have effects not covered by the set of standard Inventor elements, you may need to create a new element class. Be sure you are familiar with the information in Chapters 1, 2, and 4 before you read this chapter.

The first part of this chapter offers an overview of the steps required to create a new element class. When necessary, additional sections explain key concepts in further detail and list relevant macros. The chapter example shows creating a new element, the temperature element, which is derived from SoFloatElement.

Overview

The file SoSubElement.h contains the macros for defining new element classes. The SO_ELEMENT_HEADER() macro declares type identifier and naming variables and methods that all element classes must support. The SO_ELEMENT_SOURCE() macro defines the static variables and methods declared in the SO_ELEMENT_HEADER() macro. Other macros useful in creating new element classes are mentioned in the following sections.

Creating a new element requires these steps:

  1. Select a name for the new element class and determine what class it is derived from (see “Deriving a Class from an Existing Element”).

  2. Implement an initClass() method to initialize the type information (see “The initClass() Method”).

  3. Implement a destructor. Elements don't have constructors (see “Destructor”).

  4. Implement an init() method (see “The init() Method”).

  5. Implement set() and get() methods to modify and access current values (see “The set() and get() Methods”).

  6. Implement a print() method for debugging purposes (see “The print() Method”).

  7. Depending on what your class is derived from and the nature of your element, you may need to implement the following methods:

    a. matches() and copyMatchInfo()
    (see “Additional Methods”).

    b. push() and pop() (see “Pushing and Popping Elements”).

    c. setElt() (see “Additional Methods”).

Deriving a Class from an Existing Element

Elements are used for storing information in the Inventor traversal state. Writing a new element goes hand-in-hand with writing a new node, since nodes set and get element values. As explained in Chapter 1, elements provide the mechanism for caching in Inventor. They also provide the pushing and popping facility used by separators. Later sections in this chapter supply details on both topics.

Before an element class can be used, it must be initialized using one of the macros provided (see “Creating a New Element”). The element must also be enabled in the method list of each action for which it is required. (Nodes and actions enable the elements in an action's state.)

As shown in Figure 5-1, most elements are derived from one of four abstract base classes: SoFloatElement, SoLongElement, SoReplacedElement, or SoAccumulatedElement. Most elements fall into the SoReplacedElement category. The diffuse-color and specular-color elements are examples of elements that fall into this category, because their value replaces the previous value (and it is not a simple floating-point or integer value). A few elements, such as transformations and profiles, accumulate values.

Figure 5-1. Element Class Tree (Part 1 of 2)


Figure 5-2. Element Class Tree (Part 2 of 2)


In addition, some elements have a derived element class that is used to cause the side effects required for OpenGL. An example is SoDrawStyleElement, from which SoGLDrawStyleElement is derived. “Special Behavior for Derived Elements” discusses the use of these element class pairs.

The first question to ask when creating a new element class is: What class can I derive my new element from? The answer depends partly on the kind of information stored in the element. If it can be stored as a single floating-point value (such as complexity or font size), derive your new class from SoFloatElement. If it can be stored as an integer (such as drawing style, an enumerated value), derive your new class from SoLongElement. You will probably be able to inherit many of the base class methods in these cases.

If your new element contains other types of values, you may be able to derive it from SoReplacedElement. If the current value of the element accumulates with previous values, you can derive it from SoAccumulated-
Element
. Later sections in this chapter describe in detail how these classes differ from each other.

The initClass() Method

The initClass() method sets up runtime type checking for the class. Use SO_ELEMENT_INIT_CLASS() in the initialization method for non-abstract classes. Use SO_ELEMENT_INIT_ABSTRACT_CLASS() in the initialization method for abstract classes.

Destructor

The destructor for the element is of the standard format:

SoModelMatrixElement::~SoModelMatrixElement()

If your element requires special cleanup—for example, because it allocates memory space that needs to be freed—the destructor should also perform that work.

At this point, you might be thinking, “What about the element constructor? Don't I need to write one?” Actually, you don't. The constructor for an element is built into the header, and you do not have access to it. There are only two times when an element instance is created:

  • When the first instance of an element is created and put on the stack by the state

  • When a new element instance is pushed onto the stack

In the first case, the virtual init() method is used after creating the first instance of the element. In the second case, the virtual push() method is used after creating each subsequent instance of the element. You can override the virtual init() and push() methods if your element class requires its own default value or other special behavior.

The init() Method

The init() method is called to initialize the very first instance of the element class used in a state instance and to set up its default values. The state creates the element and calls init() on it. Since all other instances are created by pushing, this method is called only once for each state instance.

The set() and get() Methods

All elements also need set() and get() methods that are used by the nodes to modify and access current values. There may also be a static method that returns the default value(s) for the element.

The print() Method

The print() method prints out an element's values, primarily for debugging purposes. This method, called by SoState::print(), is useful in tracking down problems during graph traversal.

Additional Methods

You may need to implement additional methods for certain element classes. Detailed information on matches() and copyMatchInfo() is presented in “Special Considerations for Caching”. The setElt() method is discussed in “Special Behavior for Derived Elements”. The push() and pop() methods are discussed in “Pushing and Popping Elements”.

matches() 

called to determine whether an element matches another element. This method is used to compare states for cache validity. copyMatchInfo() is used to create a copy of an element to be added to a cache.

setElt() 

a virtual method that is called by an element's static set() method in some cases. For details, read “Special Behavior for Derived Elements”.

push() 

used in a few cases to set up a new element instance that is a copy of the one just below it in the stack. Elements that contain two or more pieces of data and elements that implement side effects (often for OpenGL rendering) need to implement a push() method. The default push() method does nothing, since most elements contain only one piece of data and the value is typically set immediately after a push. Subclasses of SoAccumulatedElement must define their own push() method to copy values from the next instance in the stack, since the new instance accumulates values on top of the old ones. Use the SoElement method getNextInStack().

pop()  

the counterpart to push(). The default method does nothing. You need to implement a pop() method if your element has side effects. For example, when the SoGLDrawStyleElement pops, it restores the previous value by sending it to OpenGL. Other examples are the GL matrix elements, which call glPopMatrix() when they pop (see “Pushing and Popping Elements”).

Special Considerations for Caching

Elements do all of the work for caching in Inventor. If you are creating a node or an action that uses existing elements, you don't need to worry about caching. However, if you are creating a new element, you need to provide the necessary information so that caching continues to work. (You might want to reread “Caching” in Chapter 1 before continuing with this section.)

A cache is essentially a faster representation of the data you're trying to generate or compute in Inventor. What the cache contains depends on the action. A GL render cache contains an OpenGL display list. A bounding-box cache contains the bounding box itself. Using the cache speeds up subsequent traversal of the scene graph. In addition, the cache contains a list of the elements outside the cache that the information inside the cache depends on. For efficiency, the cache does not make an exact copy of each element it depends on. Instead, it copies only enough information to determine later on whether the cache is still valid.

The form of this information depends on the element. For some elements, such as those derived from SoFloatElement or SoLongElement, the value of the element is compared. For others, the node ID of the node that set the element's value is compared. For elements such as those derived from SoAccumulatedElement, the list of node IDs for all nodes that affect the instance of the element is compared. The copyMatchInfo() method should copy just enough information so that matches() can work properly.

Inventor uses an element's copyMatchInfo() method to copy the comparison data for the element into the cache. Then, to determine whether a cache is valid, Inventor compares each element in the cache with the corresponding element in the state by calling the element's matches() method.

You can inherit the matches() and copyMatchInfo() methods from the base classes, or for efficiency, you can write routines that are tailored to your new element class. The following sections explain in more detail how those routines work for the base classes.

SoFloatElement and SoLongElement

In many cases, you need to store only the value of the element in the cache. It doesn't matter which node has set the value. If the value of the element in the cache matches the value of the element in the state, the cache is still valid. The matches() and copyMatchInfo() methods for the SoFloatElement and SoLongElement classes function in this way.

SoReplacedElement

SoReplacedElement redefines the getElement() method of SoElement to store the node ID of the node that is about to set the value in the element. (Recall that a node's ID is unique and changes whenever the node changes.) SoReplacedElement also redefines matches() to compare the node IDs and return TRUE if the node IDs of the two elements match, as follows:

SbBool
SoReplacedElement::matches(const SoElement *elt) const
{
    return (nodeId == ((const SoReplacedElement *)
            elt)->nodeId);
}

An example of such an element is the coordinate element, which stores multiple coordinate values. Rather than compare all the values, this element simply compares the node IDs. The copyMatchInfo() method for this class copies only the node ID, since that is the information used to determine if the cache matches the state:

SoElement *
SoReplacedElement::copyMatchInfo() const
{
    SoReplacedElement *result =
        (SoReplacedElement *)getTypeId().createInstance();

    result->nodeId = nodeId;

    return result;
}

SoAccumulatedElement

Elements that accumulate values need to keep a list of node IDs for all nodes that affect an instance of the element. To determine whether a cache is valid for an accumulated element, the node ID list for the element in the cache is compared with the node ID list for the element in the state.

The SoAccumulatedElement class redefines the matches() method to compare lists of node IDs. If they plan to use the standard matches() method, subclasses of SoAccumulatedElement need to use the following methods provided by this parent class to keep the list of node IDs up to date:

clearNodeIds() 

clears the list of node IDs

addNodeId() 

adds the ID of the given node to the current list

setNodeId() 

sets the node ID list to only the node ID of the given node

Special Behavior for Derived Elements

In Inventor, elements that do not have a derived class have only a static set() method. Elements with derived classes for their GL versions often have both a static set() method and a setElt() method. Using this mechanism, nodes can deal with the generic base class for elements without dealing individually with each subclass. For example, a node's doAction() method can simply call SoDrawStyleElement::set rather than calling SoGLDrawStyleElement::set. If you are deriving an element from an existing element class, or if you are creating a new hierarchy of element classes, the setElt() mechanism described here may prove useful.

Suppose you are writing a new renderer and need some elements to perform additional work. In this case, you can derive an element from the existing element. You write a virtual setElt() method for your new derived class that sets the element to its correct value and performs the desired side effects. The rest is C++ magic, as follows:

  1. The static set() method of the base class calls its virtual setElt() method.

  2. If there is no derived class, the setElt() method of the base class is called. If there is a derived class, the setElt() method of the derived class is called to set the element value.

In Open Inventor, this mechanism is used for the GL rendering version of certain elements. Let's take a closer look at the plain vanilla and GL versions of the draw-style element to illustrate this process. As noted in “Deriving a Class from an Existing Element”, the GL version is used to perform side effects required for GL rendering. The base class, SoDrawStyleElement, provides a static set() method that calls a virtual setElt() on the modifiable instance of the element:

// Sets draw style in element accessed from state.
void
SoDrawStyleElement::set(SoState *state, SoNode *node, Style
                        style)
{
   SoDrawStyleElement *elt;

   // Get an instance we can change (pushing if necessary)
   elt = (SoDrawStyleElement *)
             getElement(state, classStackIndex,node);

   if (elt != NULL)
      elt->setElt(style);      // virtual setElt()--see below
}

The virtual setElt() is defined in the base class as follows:

SoLongElement::setElt( long value )
{
   data = value;
}

The derived GL class, SoGLDrawStyleElement, redefines the virtual setElt() method to do two things:

  • It sets the value in the Inventor instance of the element.

  • It makes the proper calls to OpenGL (glPolygonMode()) to reflect the current state.

The code for the setElt() method in SoGLDrawStyleElement is as follows:

// Sets draw style in element.
void
SoGLDrawStyleElement::setElt(long value)
if (data != value) {
   data = value;
   send();       // The send() method calls glPolygonMode().
}

At the same time, SoGLDrawStyleElement inherits the static set() method from the parent class, SoDrawStyleElement. Note that you can derive other classes from SoDrawStyleElement in a similar way to support other rendering actions.

Pushing and Popping Elements

You need to implement a push() method for your new element if the pushed copy of the element uses the previous element in some way. For example, accumulated elements such as the SoProfileElement need the previous element because they append values to the previous values. Their push() method copies the values from the previous element in the stack into the new element on the stack so that the current value can be appended to it. You also need to implement a push() method if your element requires some side effect when its value is set. For example, some matrix elements call glPushMatrix() inside their push() routine. You do not need to implement a push() method for most replaced elements (unless they have side effects), since their new value wipes out the previous value.

The push() method is called when you call set() on an element for the first time after the state is pushed (for example, underneath a separator). You may need to implement a corresponding pop() method whenever you implement a push() method for an element.

If your pop() method has side effects, those side effects need to be included in a cache for the separator that performs the pop; otherwise, the separator cannot restore the state properly. Include this line in an element's pop() method to tell all open caches they need to depend on this element when validating a cache:

capture(state);

For example, suppose the scene graph contains a draw-style node with a value of FILLED. A separator node to the right of this draw-style node contains a draw-style node with a value of LINES and a cube. Assume caching is turned on at the separator node. The separator's cache contains instructions for setting the draw-style to LINES and drawing the cube. When the state is restored, the draw-style element's pop() method causes the draw-style to be restored to its previous value (FILLED). If that previous value changes, this separator's cache is no longer valid.

Creating a New Element

This example creates an element called TemperatureElement that holds the current temperature of shapes during traversal. This element is derived from the SoFloatElement class.

Example 5-1 shows the header file for the TemperatureElement.

Example 5-1. TemperatureElement.h


#include <Inventor/elements/SoFloatElement.h>

class TemperatureElement : public SoFloatElement {

   SO_ELEMENT_HEADER(TemperatureElement);

 public:
   // Initializes the TemperatureElement class
   static void    initClass();

   // Initializes element
   virtual void   init(SoState *state);

   // Sets the current temperature in the state to the given
   // temperature (in degrees Fahrenheit)
   static void    set(SoState *state, SoNode *node, float temp);

   // Returns the current temperature from the state
   static float   get(SoState *state);

   // Returns the default temperature
   static float   getDefault()         { return 98.6; }

 private:
   virtual ~TemperatureElement();
};

Example 5-2 shows the source code for the TemperatureElement.

Example 5-2. TemperatureElement.c++


#include "TemperatureElement.h"

SO_ELEMENT_SOURCE(TemperatureElement);

// Initializes the TemperatureElement class.

void
TemperatureElement::initClass()
{
   SO_ELEMENT_INIT_CLASS(TemperatureElement, SoFloatElement);
}

// Destructor

TemperatureElement::~TemperatureElement()
{
}

// Initializes the first instance used in an action's state.

void
TemperatureElement::init(SoState *)
{
   data = getDefault();
}

// Sets the current temperature in the state.

void
TemperatureElement::set(SoState *state, SoNode *node,
                  float temp)
{
   // Use the corresponding method on SoFloatElement to set the
   // value in the top instance in the state
   SoFloatElement::set(classStackIndex, state, node, temp);
}

// Returns the current temperature from the state.

float
TemperatureElement::get(SoState *state)
{
   // Use the corresponding method on SoFloatElement to get the
   // value from the top instance in the state
   return SoFloatElement::get(classStackIndex, state);
}