Computer Graphics Workshop '97 Lecture Notes | 1/17/97 |
An SoSelection node is a subclass of SoSeparator; that is, it acts as a group node with Separator's additions of the push/pop of the rendering state. However, it implements several very powerful functions, primary that it automatically implements mouse-based selection of objects below it in the scene graph. Several callbacks can be set up along the selection process to give the user full control over which objects get selected and what happens when certain objets are selected.
The format for a callback activated upon selection/deselection of an object is as follows:
When an object is selected or deselected, the appropriate callback will be called with the user's specified data as the first argument, and the path to the appropriate node as the second argument. A path is an object which represents a path from the root node of the scene graph to any other node in the graph; essentially, it is a sequence of nodes, and it can be operated upon as a stack using pushes and pops. We can use the getTail method of SoPath to find the selected or deselected object.
Let's create a very simple example to try this out:
;; set up scene graph with selection (define root (new-SoSelection)) (send root 'ref) (send root 'addChild (new-SoSphere)) (define transform (new-SoTransform)) (send root 'addChild transform) (send (send transform 'translation) 'setValue 3 0 0) (send root 'addChild (new-SoCone)) ;; selection callback function (define (selection-callback data path) (let ((selected-object (send path 'getTail))) (display "Object ") (display selected-object) (display " was selected.") (newline))) ;; set up selection callback (send root 'addSelectionCallback (get-scheme-selection-path-cb) (void-cast (callback-info selection-callback))) ;; create viewer (define viewer (examiner root)) (send viewer 'setViewing 0)Now we can display the "identity" of the object which was selected; but note that we still have no programmatic knowledge about which object was picked. The data type returned by SoPath's getTail method is an SoNode; both SoSphere and SoCone are subclasses of SoNode, and it seems as though we can not tell the difference between them.
There are (at least) two solutions to this problem. One is to assign each object a name using SoBase's setName method. This name is stored inside the object, and can be retrieved later using the getName method. By checking the name of the object at the tail of the path, we could tell the difference between the cone and the sphere. (Note that we would need to use the operator == method of SbName to perform comparisons between names.)
However, because we are not trying to distinguish between two objects of the same type (i.e. two cones), there is a better way to achieve this goal. All Inventor nodes store run-time information internally about their data type; that is, a cone "knows" that it is a cone, even if it is represented as one of its parent classes (i.e. SoNode). In addition, all Inventor classes have a unique type identifier, which can be accessed by the static getClassTypeId function. For example:
(SoSphere::getClassTypeId)returns the type identifier for the SoSphere class. The isOfType method of SoBase takes as argument a type specifier and returns 1 if the object is of this data type, and 0 otherwise. So we could rewrite the above callback function as follows:
(define (selection-callback data path) (let ((selected-object (send path 'getTail))) (if (= 1 (send selected-object 'isOfType (SoSphere::getClassTypeId))) (let ((the-sphere (SoSphere-cast selected-object))) (display "Picked a sphere.") (newline) ;; additional operations with sphere ) (let ((the-cone (SoCone-cast selected-object))) (display "Picked a cone.") (newline) ;; additional operations with cone ))))This picking capability could be used, for example, to reset the state of a game every time some text saying "New Game" was clicked.
There are three policies for selection of objects; the chosen one is indicated in SoSelection's policy field. The default, SHIFT, policy selects the object under the mouse when the left button is clicked (deselecting all previously selected objects), deselects all objects when the mouse is clicked when over the background, and adds the object under the mouse to the selection list when an object is selected with the shift button depressed. The TOGGLE policy is equivalent to the SHIFT policy with the shift key depressed; the SINGLE policy is equivalent to the SHIFT policy without the shift key depressed. The selection node's policy can be changed in the following way:
(send (send my-selection 'policy) 'setValue SoSelection::SINGLE)The Inventor Mentor examples 10.1.addEventCB, 10.5.SelectionCB, 10.6.PickFilterTopLevel, and 10.7.PickFilterManip show more complex uses of selection. NOTE that the definition of the TOGGLE policy in Scheme is SoSelection::ENUM_TOGGLE, because of a conflict with the toggle method of the SoSelection class. C++ does not have this conflict.
An SoEventCallback node is added to the scene graph like any other node, but has a method which allows callbacks for specific event types to be set up; when one of these events occurs in the viewer where this scene graph is being displayed, it is handled by the EventCallback node.
From looking at the SEE ALSO section at the bottom of the manual page for SoEvent, we can see the different types of events for which we can set up callbacks:
We will ignore the Motion3Event and SpaceballButtonEvent types because we do not have 3-D input devices available.
To set up a callback for a key press, for example, we would do the following. Note the very similar structure for setting up a callback as in the sensor and selection examples.
(define root (new-SoSeparator)) (send root 'ref) (define event-callback (new-SoEventCallback)) (send root 'addChild event-callback) (define mat (new-SoMaterial)) (send root 'addChild mat) (send root 'addChild (new-SoSphere)) (define viewer (examiner root)) (send viewer 'setViewing 0) ;; select the arrow icon in the viewer ;; Callback for key presses. ;; Note that we use the material node as the user data here. (define (key-event-cb user-data event-node) (let ((material-node (SoMaterial-cast user-data)) (event (send event-node 'getEvent))) (if (= 1 (SO_KEY_PRESS_EVENT event ANY)) (begin ;; turn the material node red on key press events. (send (send material-node 'diffuseColor) 'setValue 0.8 0.2 0.2)) (begin ;; return the material node to white on key release events. (send (send material-node 'diffuseColor) 'setValue 0.8 0.8 0.8))) ;; tell the callback node that we have handled the event. (send event-node 'setHandled))) ;; Set up the above callback (send event-callback 'addEventCallback (SoKeyboardEvent::getClassTypeId) (get-scheme-event-callback-cb) (void-cast (callback-info key-event-cb mat)))Now whenever we press a key, the sphere turns red. When we release the key, the sphere returns to white. We could have tested the event for specific key presses and performed different functions for different keys.
One thing about the above code bears explaining: the use of the SO_KEY_PRESS_EVENT macro. From the manual page for SoKeyboardEvent:
There are similar macros for SoMouseButtonEvent:
These macros are usually defined by the C preprocessor and save keystrokes when testing whether an event is a key or mouse button press or release event. They have been redefined in Scheme to have similar syntax to the C form; an example of the syntax is in the code above. There are more examples in the Inventor Mentor examples listed above. Note that these functions return an SbBool, so in Scheme you must test the result of the macro to see whether it is 0 or 1.
The following example (expanded from the above one) combines both keyboard and mouse button events. The three mouse buttons now control which shape shows up in the viewer.
(define root (new-SoSeparator)) (send root 'ref) (define event-callback (new-SoEventCallback)) (send root 'addChild event-callback) (define mat (new-SoMaterial)) (send root 'addChild mat) (define switch (new-SoSwitch)) (send root 'addChild switch) (send (send switch 'whichChild) 'setValue 0) (send switch 'addChild (new-SoSphere)) (send switch 'addChild (new-SoCone)) (send switch 'addChild (new-SoCube)) (define viewer (examiner root)) (send viewer 'setViewing 0) ;; select the arrow icon in the viewer (send viewer 'setPopupMenuEnabled 0) ;; disable the popup menu to allow ;; right mouse button events to ;; come through ;; Callback for key presses. ;; Note that we use the material node as the user data here. (define (key-event-cb user-data event-node) (let ((material-node (SoMaterial-cast user-data)) (event (send event-node 'getEvent))) (if (= 1 (SO_KEY_PRESS_EVENT event ANY)) (begin ;; turn the material node red on key press events. (send (send material-node 'diffuseColor) 'setValue 0.8 0.2 0.2)) (begin ;; return the material node to white on key release events. (send (send material-node 'diffuseColor) 'setValue 0.8 0.8 0.8))) ;; tell the callback node that we have handled the event. (send event-node 'setHandled))) ;; Set up the above callback (send event-callback 'addEventCallback (SoKeyboardEvent::getClassTypeId) (get-scheme-event-callback-cb) (void-cast (callback-info key-event-cb mat))) ;; Callback for mouse button presses. ;; Note that we use the switch node as the user data here. (define (mouse-button-cb user-data event-node) (let ((switch-node (SoSwitch-cast user-data)) (event (send event-node 'getEvent))) (cond ((= 1 (SO_MOUSE_PRESS_EVENT event BUTTON1)) (send (send switch 'whichChild) 'setValue 0)) ((= 1 (SO_MOUSE_PRESS_EVENT event BUTTON2)) (send (send switch 'whichChild) 'setValue 1)) ((= 1 (SO_MOUSE_PRESS_EVENT event BUTTON3)) (send (send switch 'whichChild) 'setValue 2))) (send event-node 'setHandled))) ;; Set up the above callback (send event-callback 'addEventCallback (SoMouseButtonEvent::getClassTypeId) (get-scheme-event-callback-cb) (void-cast (callback-info mouse-button-cb switch)))Keyboard events are of obvious utility; if you want to have keyboard-driven interactivity with your program, it is straightforward to implement it. Mouse button events are probably trickier to implement usefully. Instead, you'll probably want the kind of high-level functionality (i.e. 3-D picking) which SoSelection provides.
Location2Events are generated whenever the mouse reaches a new position in the window; they are not generated continuously with the mouse's current position. You might use a Location2Event to update the mouse's current position in the window, and an idle callback to perform some manipulation based on that position (like moving the camera). The fifth problem set will show you how to build a new viewer using Location2Events to track the motion of the mouse.
$Id: index.html,v 1.6 1997/01/15 19:02:50 kbrussel Exp $