14.2. Data Sensors

There are three types of data sensors:

An SoFieldSensor( C++ | Java | .NET ) is notified whenever data in a particular field changes. An SoNodeSensor( C++ | Java | .NET ) is notified when data changes within a certain node, when data changes within any of the child nodes underneath that node, or when the graph topology changes under the node. An SoPathSensor( C++ | Java | .NET )is notified whenever data within any of the nodes in a certain path changes, or when nodes are added to or deleted from that path. A node is considered to be in the path if traversing the path would cause the node to be traversed.

[Tip]

Tip: Setting the value of a field to the same value it had before (for example, field.setValue(field.getValue())) is considered a change. Calling the touch() method of a field or node is also considered a change.

A render area attaches a node sensor to the root of the scene graph so that it can detect when you make any changes to the scene. It then automatically renders the scene again.

Data sensors are also useful if you want to monitor changes in part of a scene and communicate them to another element in the scene. For example, suppose you have a material in the scene graph with an editor attached to it. If the material changes, the editor needs to change the values of its sliders to reflect the new material. An SoNodeSensor( C++ | Java | .NET ) supplies this feedback to the material editor.

[Tip]

Tip: Field-to-field connections are another way of keeping different parts of the scene graph in sync. See Chapter 15, Engines.

The following sequence describes the necessary steps for setting up a data sensor:

  1. Construct the sensor.

  2. Set the callback function (see the next section).

  3. Set the priority of the sensor (see the section called “Priorities”).

  4. Attach the sensor to a field, node, or path.

  5. When you are finished with the sensor, delete it.

Callback functions, as their name suggests, allow Inventor to call back to the application when some predefined event occurs. A callback function usually takes a single argument of type void* that can be used to pass extra user-defined data to the function. Callback functions used by sensors also have a second argument of type SoSensor*. This argument is useful if the same callback function is used by more than one sensor. The argument is filled with a pointer to the sensor that caused the callback.

In C++, a sensor callback function can be declared as a static member function of a class. In this case, because static functions have no concept of this, you need to explicitly pass an instance of the class you want to modify as user data:


C++
colorSensor->setData(this);
  

.NET
          
        

Nonstatic C++ member functions are not suitable for use as callback functions.

Classes derived from SoDelayQueueSensor( C++ | Java | .NET ) use priorities to maintain sorting in the delay queue. The following methods are used to set and obtain the priority of a given sensor:

A sensor with a priority of 0 has the highest priority. It triggers as soon as the change to the scene graph is complete. If two sensors have the same priority, there is no guarantee about which sensor will trigger first.

The SoXtRenderArea( C++ ) has a redraw data sensor with a default priority of 10000. You can schedule other sensors before or after the redraw by choosing appropriate priorities.

For example, to set the priority of a sensor so that it is triggered right before redraw:


C++
SoNodeSensor   *s;
SoRenderArea   *renderArea;
s->setPriority(renderArea->getRedrawPriority() - 1);
    

.NET
SoNodeSensor s = new SoNodeSensor();
SoWinRenderArea renderArea = new SoWinRenderArea();
s.SetPriority(renderArea.GetRedrawPriority() - 1);
  

Java
SoNodeSensor s = new SoNodeSensor();
SwRenderArea renderArea = new SwRenderArea();
s.setPriority(renderArea.getRedrawPriority() - 1);
  

When data in the sensor's field, node, or path changes, the following things happen:

Example 14.1, “ Attaching a Field Sensor shows attaching a field sensor to the position field of a viewer's camera. A callback function reports each new camera position.

Example 14.1.  Attaching a Field Sensor


C++
#include <Inventor/SoDB.h>
#include <Inventor/Xt/SoXt.h>
#include <Inventor/Xt/viewers/SoXtExaminerViewer.h>
#include <Inventor/nodes/SoCamera.h>
#include <Inventor/nodes/SoSeparator.h>
#include <Inventor/sensors/SoFieldSensor.h>

// Callback that reports whenever the viewer's position changes.
static void
cameraChangedCB(void *data, SoSensor *)
{
   SoCamera *viewerCamera = (SoCamera *)data;

   SbVec3f cameraPosition = viewerCamera->position.getValue();
   printf("Camera position: (%g,%g,%g)\n",
            cameraPosition[0], cameraPosition[1],
            cameraPosition[2]); 
}

main(int argc, char **argv)
{
   if (argc != 2) {
      fprintf(stderr, "Usage: %s filename.iv\n", argv[0]);
      exit(1);
   }

   Widget myWindow = SoXt::init(argv[0]);
   if (myWindow == NULL) exit(1);

   SoInput inputFile;
   if (inputFile.openFile(argv[1]) == FALSE) {
      fprintf(stderr, "Could not open file %s\n", argv[1]);
      exit(1);
   }
   
   SoSeparator *root = SoDB::readAll(&inputFile);
   root->ref();

   SoXtExaminerViewer *myViewer =
            new SoXtExaminerViewer(myWindow);
   myViewer->setSceneGraph(root);
   myViewer->setTitle("Camera Sensor");
   myViewer->show();

   // Get the camera from the viewer, and attach a 
   // field sensor to its position field:
   SoCamera *camera = myViewer->getCamera();
   SoFieldSensor *mySensor = 
            new SoFieldSensor(cameraChangedCB, camera);
   mySensor->attach(&camera->position);

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

.NET
using System;
using System.Windows.Forms;

using OIV.Inventor.Nodes;
using OIV.Inventor.Win.Viewers;
using OIV.Inventor;
using OIV.Inventor.Sensors;

namespace _12_1_FieldSensor
{
  public partial class MainForm : Form
  {
    SoWinExaminerViewer myViewer;
    SoFieldSensor mySensor;
    SoCamera camera;

    public MainForm()
    {
      InitializeComponent();
      CreateSample();
    }

    // Callback that reports whenever the viewer's position changes.
    void cameraChangedCB(SoSensor sensor)
    {
      SbVec3f cameraPosition = camera.position.Value;
      Console.WriteLine("Camera position: " + cameraPosition[0] + " " + cameraPosition[1] + " " + cameraPosition[2]);
    }

    public void CreateSample()
    {
      String defaultFileName =
        "../../../../../data/jackInTheBox.iv";

      String fileName = defaultFileName;

      SoInput inputFile = new SoInput();
      if (inputFile.OpenFile(fileName) == false)
      {
        Console.WriteLine("Could not open file : " + fileName);
      }
      else
      {
        SoWWWInline.SetReadAsSoFile(true);

        SoSeparator root = SoDB.ReadAll(inputFile);

        myViewer = new SoWinExaminerViewer(this, "", true,
            SoWinFullViewer.BuildFlags.BUILD_ALL, SoWinViewer.Types.BROWSER);
        myViewer.SetSceneGraph(root);
        myViewer.SetTitle("Camera Sensor");

        // Get the camera from the viewer, and attach a 
        // field sensor to its position field:
        camera = myViewer.GetCamera();
        mySensor = new SoFieldSensor();
        mySensor.Action = new SoSensor.SensorCB(cameraChangedCB);
        mySensor.Attach(camera.position);
      }
    }
  }
}

Java
import tools.*;

import com.openinventor.inventor.*;
import com.openinventor.inventor.nodes.*;
import com.openinventor.inventor.awt.*;
import com.openinventor.inventor.sensors.*;

import javax.swing.*;
import java.awt.*;

public class Main extends DemoInventor
{

  private String        m_filename;
  private JTextArea     m_txtArea;
  private SoFieldSensor m_fieldSensor;

  public Main()
  {
    m_txtArea = new JTextArea();
    m_filename = "../../../../data/models/jackInTheBox.iv";
  }

  public static void main(String[] args)
  {
    Main applet = new Main();
    DemoInventor.isAnApplet = false;

    if (args.length > 0)
      applet.m_filename = args[0];

    applet.start();
    demoMain(applet, "Camera Sensor");
  }

  public void start()
  {
    super.start();

    SoInput inputFile = new SoInput();
    if (inputFile.openFile(m_prefix + m_filename) == false)
    {
      System.err.println("Could not open file " + m_filename);
      System.exit(1);
    }

    SoSeparator root = SoDB.readAll(inputFile);

    SwSimpleViewer myViewer = new SwSimpleViewer();
    myViewer.setSceneGraph(root);

    // Get the camera from the viewer, and attach a
    // field sensor to its position field:
    SoCamera camera = myViewer.getArea().getCamera();
    m_fieldSensor = new SoFieldSensor(new CameraChangedRunnable(camera));
    m_fieldSensor.attach(camera.position);

    m_txtArea.setEditable(false);
    JScrollPane jsp = new JScrollPane(m_txtArea);
    jsp.setPreferredSize(new Dimension(400, 400));

    setLayout(new BorderLayout());
    add(myViewer, BorderLayout.CENTER);
    add(jsp, BorderLayout.EAST);
  }

  class CameraChangedRunnable implements Runnable
  {

    SoCamera m_cam;

    public CameraChangedRunnable(SoCamera cam)
    {
      m_cam = cam;
    }

    public void run()
    {
      SbVec3f cameraPosition = m_cam.position.getValue();
      m_txtArea.append("Camera position: (" + cameraPosition.getX() + "," +
                       cameraPosition.getY() + "," + cameraPosition.getZ() + ")\n");
    }
  }
}
  

You can use one of the following methods to obtain the field, node, or path that initiated the notification of any data sensor:

These methods work only for immediate (priority 0) sensors.

The trigger path is the chain of nodes from the last node notified down to the node that initiated notification. To obtain the trigger path, you must first use setTriggerPathFlag() to set the trigger-path flag to TRUE since it's expensive to save the path information. You must make this call before the sensor is notified. Otherwise, information on the trigger path is not saved and getTriggerPath() always returns NULL. (By default, this flag is set to FALSE.) The trigger field and trigger node are always available. Note that getTriggerField() returns NULL if the change was not to a field (for example, addChild() or touch() was called).

Example 14.2, “ Using the Trigger Node and Field shows using getTriggerNode() and getTriggerField() in a sensor callback function that prints a message whenever changes are made to the scene graph.

Example 14.2.  Using the Trigger Node and Field


C++
#include <Inventor/SoDB.h>
#include <Inventor/nodes/SoCube.h>
#include <Inventor/nodes/SoSeparator.h>
#include <Inventor/nodes/SoSphere.h>
#include <Inventor/sensors/SoNodeSensor.h>

// Sensor callback function:
static void
rootChangedCB(void *, SoSensor *s)
{
   // We know the sensor is really a data sensor:
   SoDataSensor *mySensor = (SoDataSensor *)s;
    
   SoNode *changedNode = mySensor->getTriggerNode();
   SoField *changedField = mySensor->getTriggerField();
    
   printf("The node named '%s' changed\n",
            changedNode->getName().getString());

   if (changedField != NULL) {
      SbName fieldName;
      changedNode->getFieldName(changedField, fieldName);
      printf(" (field %s)\n", fieldName.getString());
   } 
   else 
      printf(" (no fields changed)\n");
}

main(int, char **)
{
   SoDB::init();

   SoSeparator *root = new SoSeparator;
   root->ref();
   root->setName("Root");

   SoCube *myCube = new SoCube;
   root->addChild(myCube);
   myCube->setName("MyCube");

   SoSphere *mySphere = new SoSphere;
   root->addChild(mySphere);
   mySphere->setName("MySphere");

   SoNodeSensor *mySensor = new SoNodeSensor;

   mySensor->setPriority(0);
   mySensor->setFunction(rootChangedCB);
   mySensor->attach(root);

   // Now, make a few changes to the scene graph; the sensor's
   // callback function will be called immediately after each
   // change.
   myCube->width = 1.0;
   myCube->height = 2.0;
   mySphere->radius = 3.0;
   root->removeChild(mySphere);
}
    

.NET
using System;
using System.Windows.Forms;

using OIV.Inventor.Nodes;
using OIV.Inventor.Sensors;
using OIV.Inventor.Fields;

namespace _12_2_NodeSensor
{
  public partial class MainForm : Form
  {
    SoNodeSensor mySensor;

    public MainForm()
    {
      InitializeComponent();
      CreateSample();
    }

    // Sensor callback function:
    void rootChangedCB(SoSensor sensor)
    {
      // We know the sensor is really a data sensor:
      SoDataSensor mySensor = (SoDataSensor)sensor;

      SoNode changedNode = mySensor.GetTriggerNode();
      SoField changedField = mySensor.GetTriggerField();

      Console.WriteLine("The node named '" + changedNode.GetName() + "' changed.");

      if (changedField != null)
      {
        String fieldName;
        changedNode.GetFieldName(changedField, out fieldName);
        Console.WriteLine(" (field " + fieldName + " )");
      }
      else
      {
        Console.WriteLine(" (no fields changed)");
      }
    }

    public void CreateSample()
    {
      SoSeparator root = new SoSeparator();
      root.SetName("Root");
      SoCube myCube = new SoCube();
      root.AddChild(myCube);
      myCube.SetName("MyCube");

      SoSphere mySphere = new SoSphere();
      root.AddChild(mySphere);
      mySphere.SetName("MySphere");

      mySensor = new SoNodeSensor();
      mySensor.SetPriority(0);
      mySensor.Action = new SoSensor.SensorCB(rootChangedCB);
      mySensor.Attach(root);

      // Now, make a few changes to the scene graph; the sensor's
      // callback function will be called immediately after each
      // change.
      myCube.width.Value = 1.0f;
      myCube.height.Value = 2.0f;
      mySphere.radius.Value = 3.0f;
      root.RemoveChild(mySphere);
    }
  }
}
  

Java
import tools.*;

import com.openinventor.inventor.sensors.*;
import com.openinventor.inventor.nodes.*;
import com.openinventor.inventor.fields.*;

import javax.swing.*;
import java.awt.*;

public class Main extends DemoInventor
{

  private JTextArea    m_txtArea = new JTextArea();
  private SoNodeSensor m_sensor;

  public static void main(String[] args)
  {
    Main applet = new Main();
    DemoInventor.isAnApplet = false;
    applet.start();
    demoMain(applet, "Node Sensor");
  }

  public void start()
  {
    super.start();

    SoCube myCube = new SoCube();
    myCube.setName("MyCube");

    SoSphere mySphere = new SoSphere();
    mySphere.setName("MySphere");

    SoSeparator root = new SoSeparator();
    root.setName("Root");
    {
      root.addChild(myCube);
      root.addChild(mySphere);
    }

    m_sensor = new SoNodeSensor(new RootChangedRunnable());
    m_sensor.setPriority(0);
    m_sensor.attach(root);

    // Now, make a few changes to the scene graph; the sensor's
    // callback function will be called immediately after each
    // change.
    myCube.width.setValue(1.0f);
    myCube.height.setValue(2.0f);
    mySphere.radius.setValue(3.0f);
    root.removeChild(mySphere);

    m_txtArea.setEditable(false);

    setLayout(new BorderLayout());
    add(m_txtArea, BorderLayout.CENTER);
  }

  class RootChangedRunnable implements Runnable
  {
    public void run()
    {
      SoNode changedNode = m_sensor.getTriggerNode();
      SoField changedField = m_sensor.getTriggerField();

      m_txtArea.append("The node named '" + changedNode.getName() + "' changed\n");

      if (changedField != null)
        m_txtArea.append(" (field " + changedNode.getFieldName(changedField) + ")\n\n");
      else
        m_txtArea.append(" (no fields changed)\n");
    }
  }
}