Computer Graphics Workshop '96 Lecture Notes | 1/19/96 |
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:
;; create viewer (define viewer (new-SoXtExaminerViewer)) (-> viewer 'show) ;; set up scene graph with selection (define root (new-SoSelection)) (-> root 'ref) (-> root 'addChild (new-SoSphere)) (define transform (new-SoTransform)) (-> root 'addChild transform) (-> (-> transform 'translation) 'setValue 3 0 0) (-> root 'addChild (new-SoCone)) ;; set up selection callback (define callback-info (new-SchemeSoCBInfo)) (-> callback-info 'ref) (-> (-> callback-info 'callbackName) 'setValue "selection-callback") (-> root 'addSelectionCallback (get-scheme-selection-path-cb) (void-cast callback-info)) ;; selection callback function (define (selection-callback data path) (let ((selected-object (-> path 'getTail))) (display "Object ") (display selected-object) (display " was selected.") (newline))) (-> viewer 'setSceneGraph root) (-> 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 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.
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 functionality. All Inventor nodes store 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 (-> path 'getTail))) (if (= 1 (-> 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 ))))Several possibilities come to mind for how to use this: for example, you could have some text on the screen saying "new game", and each time that text is clicked it could reset the state of the game.
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:
(-> (-> my-selection 'policy) 'setValue SoSelection::SINGLE)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 viewer (new-SoXtExaminerViewer)) (-> viewer 'show) (define root (new-SoSeparator)) (-> root 'ref) (define event-callback (new-SoEventCallback)) (-> root 'addChild event-callback) (define mat (new-SoMaterial)) (-> root 'addChild mat) (-> root 'addChild (new-SoSphere)) (-> viewer 'setSceneGraph root) (-> viewer 'setViewing 0) ;; select the arrow icon in the viewer ;; add a callback (defined below) for key presses. ;; Note that the callback (of type SoEventCallbackCB *) is acquired by ;; the call to (get-scheme-event-callback-cb), and the data for the ;; callback is, as usual, a SchemeSoCBInfo (cast to a void *). ;; We use the material node as the user data field in this example. (define callback-info (new-SchemeSoCBInfo)) (-> callback-info 'ref) (-> (-> callback-info 'callbackName) 'setValue "key-event-cb") (-> (-> callback-info 'affectsNode) 'setValue mat) (-> event-callback 'addEventCallback (SoKeyboardEvent::getClassTypeId) (get-scheme-event-callback-cb) (void-cast callback-info)) (define (key-event-cb user-data event-node) (let ((material-node (SoMaterial-cast user-data)) (event (-> event-node 'getEvent))) (if (= 1 (SO_KEY_PRESS_EVENT event ANY)) (begin ;; turn the material node red. (-> (-> material-node 'diffuseColor) 'setValue 0.8 0.2 0.2)) (begin ;; return the material node to white on key release events. (-> (-> material-node 'diffuseColor) 'setValue 0.8 0.8 0.8))) ;; tell the callback node that we have handled the event. (-> event-node 'setHandled)))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 Scheme versions of the Inventor Mentor examples. 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.
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. Ideally, you would probably like the kind of high-level functionality (i.e. 3-D picking) which SoSelection provides. However, you can get the mouse's 2D position within the window by using the getPosition or getNormalizedPosition methods of SoEvent; you might then calculate a trajectory based on that point and send an object along it (as in the firing of shells in Missile Command).
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).
$Id: index.html,v 1.2 1996/01/19 18:45:12 kbrussel Exp $