Chapter 8. Advanced Programming Topics

The following sections provide code examples and descriptions of advanced functionality in a Molecular Inventor application:

Printing A Scene Graph

The following code example, from /usr/share/src/MolInventor/examples/miApp/scene.c++, uses the Open Inventor SoXtPrintDialog class to create an image that can be sent to a printer or saved in a file. The SoXtPrintDialog class presents a set of dialog boxes with which a user interacts to specify how the image should be created and where the image should be sent. It then renders the scene graph using the SoOffscreenRenderer class and sends the resultant image to the printer or the file.

The implementation is broken into three functions:

  • myPrint()—is invoked when the user selects the File > Print option. It creates an instance of the SoXtPrintDialog class, if not already created, and registers two callback functions: beforePrintCB() and afterPrintCB().

  • beforePrintCB()—is a callback function invoked by SoXtPrintDialog just before it renders the image. beforePrintCB() creates a new scene graph, called printRoot, unless it already exists, that contains a camera and a light, and it attaches the current scene graph, called root, to printRoot.

  • afterPrintCB()—is a callback function that removes the current scene graph, root, from the scene graph, printRoot, used for printing.

myPrint()

The following steps are performed by myPrint():

  1. Create an instance of the SoXtPrintDialog class, called printDialog, if one does not already exist. SoXtPrintDialog creates a set of dialog boxes that allow the user to specify how the image is rendered and whether the resultant image is sent to a printer or a file. It generates the image by rendering the scene graph into an offscreen render area that it manages.

  2. Set the title of printDialog.

  3. Register beforePrintCB() and afterPrintCB() as callback functions. beforePrintCB() is invoked just before the offscreen rendering. afterPrintCB() is invoked after the printing or file writing has completed.

  4. Get the size of the rendering area from the instance of SoXtExaminerViewer currently in use and set this as the size of rendering area to be used by the offscreen renderer managed by printDialog.

  5. Make printDialog visible to the user.

    Example 8-1. myPrint()


    void
    myPrint()
    {
        // If we haven't made the print dialog yet, do so now
        if (printDialog == NULL) {
            printDialog = new SoXtPrintDialog;
            printDialog->setTitle(“MolInventor Printing”);
            printDialog->setBeforePrintCallback(beforePrintCB, NULL);
            printDialog->setAfterPrintCallback(afterPrintCB, NULL);
        }
    
        // Get the size of the render area and set this as printDialog's
        // print size
        Widget widget = examinerViewer->getRenderAreaWidget();
        if (widget != NULL) {
            Arg args[2];
            int n = 0;
            SbVec2s sz;
            XtSetArg(args[n], XtNwidth, &sz[0]); n++;
            XtSetArg(args[n], XtNheight, &sz[1]); n++;
            XtGetValues(widget, args, n);
            printDialog->setPrintSize(sz);
        }
    
        // Show the dialog
        printDialog->show();
    }
    

beforePrintCB()

beforePrintCB() is registered with printDialog as the function to be invoked just before offscreen rendering is performed. This allows the application to perform any necessary actions prior to rendering.

Viewers, such as SoXtExaminerViewer, automatically supply a camera and light to scene graphs that do not contain them. However, the SoOffScreenRenderer class used by SoXtPrintDialog when rendering a scene does not add a light and camera. Therefore, these nodes must be added to the scene graph for the molecular display to be seen.

beforePrintCB() creates a new scene graph, called printRoot, that contains a camera, a group used to control a light, and the scene graph that has been used by the application to display the molecular structure in a viewer. This latter scene graph begins at root.

The following steps describe how to accomplish these tasks:

  1. If a root node for a scene graph used to render into the offscreen area has not previously been created, create one now. Use an instance of SoGroup and call it printRoot.

  2. Get the camera used by the current instance of the SoXtExaminerviewer class (examinerViewer) and use that as the camera for the printRoot scene graph.

  3. Create a new node, hlGroup, that is an instance of the SoGroup class. The children of this node are used to control the light.

  4. Create an instance of the SoRotation class, called headlightRot. headlightRot is used to place the light at the camera's position by setting to the orientation of the light to the orientation of the camera in the current instance of the SoXtExaminerviewer class (examinerViewer).

  5. Get the light used by examinerViewer, and add it, as the light source, to hlGroup.

  6. Add an instance of SoResetTransfrom as a child to hlGroup. This node resets the transformation perform by headlightRot so that the rest of the scene graph is not affected by headlightRot.

  7. Add the subgraph, hlGroup, as a child to the printRoot node.

    Figure 8-1. printRoot Scene Graph


  8. Supply the current orientation of examinerViewer's camera to headlightRot so the light is placed at the position of this camera. Recall that in step 2, the camera in the printRoot scene graph was set to that used by examinerViewer.

  9. Add the MI scene graph, root, as a child to the printRoot node.

  10. Set the scene graph, printRoot, that printDialog should use.

    Example 8-2. beforePrintCB


    void
    beforePrintCB(void *userData, SoXtPrintDialog *pd)
    
    {
        static SoRotation *headlightRot = NULL;
        if (printRoot == NULL) {
            printRoot = new SoGroup;
            printRoot->ref();
    
            // Use the examinerViewer's camera as the offscreen renderer's
            // camera
            printRoot->addChild(examinerViewer->getCamera());
    
            // Create a Group which holds a Rotation, a DirectionalLight
            // and a ResetTransform.  The DirectionalLight comes from the
            // examinerViewer.  This Group is identical to what is used in
            // the SoXtViewer code to set up the light for any viewers
            // derived from it.  Therefore, the offscreen renderer's light will
            // be in the same position as the examinerViewer's light.
            //
            // See /usr/share/src/Inventor/samples/viewers/MyViewer.c++
    
            SoGroup *hlGroup = new SoGroup;
            if (headlightRot != NULL) headlightRot->unref();
            headlightRot = new SoRotation;
            hlGroup->addChild(headlightRot);
            hlGroup->addChild(examinerViewer->getHeadlight());
            hlGroup->addChild(new SoResetTransform);
    
            printRoot->addChild(hlGroup);
        }
        headlightRot->rotation.setValue(
            examinerViewer->getCamera()->orientation.getValue());
    
        printRoot->addChild(root);
        printDialog->setSceneGraph(printRoot);
    }
    

afterPrint()

afterPrintCB() is registered with printDialog as the function to be invoked after the printing or writing to a file is done. This method allows the application to perform any necessary cleanup after SoXtPrintDialog has finished its operations. In the following example, afterPrintCB() just removes the root scene graph from the printRoot scene graph.

Example 8-3. afterPrint()


void
afterPrintCB(void *userData, SoXtPrintDialog *pd)
{
    printRoot->removeChild(root);
}

Using the X Clipboard

Open Inventor provides a class, SoXtClipboard, that handles the copying and pasting of information between applications. For more information about SoXtClipboard, see Chapter 16 in The Inventor Mentor.

The example in this section shows how the application, miApp, uses SoXtClipboard to copy a scene graph to the X clipboard. You can find this sample code in /usr/share/src/MolInventor/examples/miApp/scene.c++.

scene.c++ uses the following steps:

  1. Create an instance of SoXtClipboard if one does not already exist.

  2. Create a root node, newRoot, that is an instance of type SoSeparator. Reference this node so Open Inventor does not deallocate it.

  3. Add copies of the nodes in the existing scene graph as children to newRoot. If the user has previously selected the Edit > CopyChemUI option, add an instance of the ChemUI class after the new instance of ChemDisplayParam.

  4. Invoke SoXtClipboard's copy method to copy the new scene graph to the clipboard.

  5. Unreference newRoot so that Open Inventor deallocates it.

    Example 8-4. copyAll()


    void    
    copyAll(Time eventTime)
    {
        if (clipBoard == NULL) {
            clipBoard = new SoXtClipboard(SoXt::getTopLevelWidget());
        }
        // Make a new scene graph leaving out the ChemSelection node.
        SoSeparator *newRoot = new SoSeparator;
        newRoot->ref();
        ChemData *newChemData = (ChemData *)chemData->copy();
        newRoot->addChild(newChemData);
        ChemDisplayParam *newCDP = 
            (ChemDisplayParam *)chemDisplayParam->copy();
        newRoot->addChild(newCDP);
        if (copyChemUI) {
            ChemUI *newChemUI = new ChemUI;
            newRoot->addChild(newChemUI);
        }
        ChemRadii *newChemRadii = (ChemRadii *)chemRadii->copy();
        newRoot->addChild(newChemRadii);
        ChemColor *newChemColor = (ChemColor *)chemColor->copy();
        newRoot->addChild(newChemColor);
        ChemDisplay *newChemDisplay = (ChemDisplay *)chemDisplay->copy();
        newRoot->addChild(newChemDisplay);
        clipBoard->copy(newRoot, eventTime);
        newRoot->unref();
    }
    

Using the Selection List

The following example shows how to use the selection list maintained by ChemSelection to copy the selected atoms and bonds to the X clipboard. Most of the steps are the same as presented in “Using the X Clipboard.” This example, however, shows how to create a ChemData node that contains only those atoms and bonds that are on the current selection list. You can find this sample code in /usr/share/src/MolInventor/examples/miApp/scene.c++.

The following steps describe the tasks accomplished in Example 8-5.

  1. Determine if anything was selected by the user by getting ChemSelection's selection list and checking its length.

    If there are no entries in the list (its length is zero), return from copySelected().

  2. If something was selected, make sure there is at least one atom among the selected items, otherwise return out of copySelected().

    It makes no sense to copy bonds if the atoms joined by those bonds are not also selected.

  3. Create a root node, newRoot, that is an instance of type SoSeparator. Reference this node so that Open Inventor does not deallocate it.

  4. Add copies of the nodes in the existing scene graph as children of newRoot. If the user has previously selected the Edit > CopyChemUI option, add an instance of the ChemUI class after the new instance of ChemDisplayParam.

  5. Set newChemDisplay to display all atoms, bonds, and labels.

  6. Create an instance of SoSearchAction to search the SoPath portions of the ChemPaths contained in the selection list for instances of any ChemBaseData-derived nodes.

  7. The SoSearchAction is set to find the last occurrence of a ChemBaseData-derived node in the ChemPath, that is, the data node closest to and on the left side of the ChemDisplay node in the scene graph. The last occurrence is the instance in the traversal state that supplies data to the ChemDisplay node.

    Figure 8-2. Finding the Last Data Node Before the ChemDisplay Node


  8. Loop over the ChemPaths stored in the selection list and apply the search action to the SoPath portion of the ChemPath. If a ChemBaseData-derived node is not found, print an error, otherwise, double check to make sure the node that was found is derived from ChemBaseData.

  9. For each atom in the ChemPath, obtain the information regarding that atom from the ChemBaseData-derived node that was found by the search action and store that information in the newly created instance of ChemData, newChemData. At the same time, maintain a list of all of the selected atoms in an instance of the SbIntList class, atomList.

    This instance is used to check the selected bonds to make sure that both atoms joined by the bond have been selected. Also, keep a running count of the number of atoms placed into newChemData.

  10. For each bond in the ChemPath, check to see that both of its atoms are contained in atomList. If they are, obtain the information regarding that bond from the ChemBaseData-derived node that was found by the search action and store that information in newChemData. Also, keep a running count of the number of bonds added to newChemData.

  11. Set the number of bonds and atoms in newChemData using the running counts of the atoms and bonds.

  12. Create an instance of SoXtClipboard if one does not already exist.

  13. Invoke SoXtClipboard's copy method to copy the new scene graph to the X clipboard.

  14. Unreference newRoot so that Open Inventor deallocates it.

    Example 8-5. Copying Selected Items


    void
    copySelected(Time eventTime)
    {
        // Check to see if there is anything selected
        const ChemPathList *cpl = chemSelection->getList();
        int32_t length = cpl->getLength();
        if (length == 0) return;  
        
        // Check to see if any atoms were selected.  If not, then can't
        // put anything on the clipboard.
        SbBool canContinue = FALSE;
        for (int32_t pathListLoop = length-1; pathListLoop >= 0; pathListLoop--) {
            ChemPath *cp = (*cpl)[pathListLoop];
            const MFVec2i &index = cp->getAtomIndex();
            if (index.getNum() > 0) {
                canContinue = TRUE;
                break;
            }
        }
        if (!canContinue) return;
     
        // Make a new scene graph
        SoSeparator *newRoot = new SoSeparator;
        newRoot->ref();
    
        ChemData *newChemData = new ChemData;
        newRoot->addChild(newChemData);
    
        ChemDisplayParam *newCDP = (ChemDisplayParam *)chemDisplayParam->copy();
        newRoot->addChild(newCDP);
    
        if (copyChemUI) {
            ChemUI *newChemUI = new ChemUI;
            newRoot->addChild(newChemUI);
        }
    
        ChemRadii *newChemRadii = (ChemRadii *)chemRadii->copy();
        newRoot->addChild(newChemRadii);
    
        ChemColor *newChemColor = (ChemColor *)chemColor->copy();
        newRoot->addChild(newChemColor);
    
        ChemDisplay *newChemDisplay = new ChemDisplay;
        newRoot->addChild(newChemDisplay);
        newChemDisplay->atomIndex.set1Value(0,
            SbVec2i(0, CHEM_DISPLAY_USE_REST_OF_ATOMS));
        newChemDisplay->bondIndex.set1Value(0,
            SbVec2i(0, CHEM_DISPLAY_USE_REST_OF_BONDS));
        newChemDisplay->atomLabelIndex.set1Value(0,
            SbVec2i(0, CHEM_DISPLAY_USE_REST_OF_ATOMS));
        newChemDisplay->bondLabelIndex.set1Value(0,
            SbVec2i(0, CHEM_DISPLAY_USE_REST_OF_BONDS));
    
        // Create a SoSearchAction which will be used to find the
        // ChemBaseData node in the ChemPath.
        SoSearchAction sa;
        sa.setType(ChemBaseData::getClassTypeId());
        sa.setInterest(SoSearchAction::LAST);
    
        // Put the selected data in the scene graph.
        // atomCount and bondCount keep track of the number of atoms and
        // bonds are in the selection list.  atomList is used in order to
        // assure that selected bonds have had both atoms selected.  If
        // both have not been selected, then the bond is not added to the
        // scene graph.
        int32_t atomCount = 0;
        int32_t bondCount = 0;
        SbIntList atomList;
    
        int32_t indexLoop;
        int32_t start, end;
        SoPath *tmpPath;
        for (pathListLoop = length-1; pathListLoop >= 0; pathListLoop--) {
            // Get a ChemPath from the ChemPathList and make a copy of it.
            ChemPath *cp = (*cpl)[pathListLoop];
            const SoPath *path = cp->getSoPath();
            tmpPath = path->copy();
            tmpPath->ref();
    
            // Search for the ChemBaseData node
            sa.apply(tmpPath);
    
            // If nothing found, then an error
            if (sa.getPath() == NULL) {
                printf(“ChemData not in path!!!\n”);
                tmpPath->unref();
                newRoot->unref();
                return;
            }
    
            // If found, double check that it is of type ChemBaseData
            if (!sa.getPath()->getTail()->isOfType(ChemBaseData::getClassTypeId())) {
                printf(“ChemData not found!!!\n”);
                tmpPath->unref();
                newRoot->unref();
                return;
            }
    
            // Add the atoms to the ChemData node.
            ChemBaseData *cd = (ChemBaseData *)sa.getPath()->getTail();
            const MFVec2i &atomIndex = cp->getAtomIndex();
            if (atomIndex.getNum() > 0) {
                atomList.truncate(0);
                int32_t numberOfAtoms = cd->numberOfAtoms.getValue();
                for (indexLoop = 0; indexLoop < atomIndex.getNum(); indexLoop++) {
                    atomIndex[indexLoop].getValue(start, end);
                    if (end == CHEM_DISPLAY_USE_REST_OF_ATOMS) {
                        end = numberOfAtoms;
                    }
                    else {
                        end += start;
                    }
                    for (int32_t atomLoop = start; atomLoop < end; atomLoop++) {
                        atomList.append(atomLoop);
                        newChemData->atomicNumber.set1Value(atomCount,
                            cd->getAtomicNumber(atomLoop));
                        newChemData->atomId.set1Value(atomCount,
                            cd->getAtomId(atomLoop));
                        newChemData->atomName.set1Value(atomCount,
                            cd->getAtomName(atomLoop));
                        newChemData->atomIndex.set1Value(atomCount,
                            cd->getAtomIndex(atomLoop));
                        newChemData->atomCoordinates.set1Value(atomCount,
                            cd->getAtomCoordinates(atomLoop));
                        atomCount++;
                    }
                }
    
                // Check to see if any of the selected bonds have had both
                // atoms selected and if so, add the bond
                int32_t from, to;
                int32_t newFrom, newTo;
                int32_t numberOfBonds = cd->numberOfBonds.getValue();
                const MFVec2i &bondIndex = cp->getBondIndex();
                for (indexLoop = 0; indexLoop < bondIndex.getNum(); indexLoop++) {
                    bondIndex[indexLoop].getValue(start, end);
                    if (end == CHEM_DISPLAY_USE_REST_OF_BONDS) {
                        end = numberOfBonds;
                    }
                    else {
                        end += start;
                    }
                    for (int32_t bondLoop = start; bondLoop < end; bondLoop++) {
                        from = cd->getBondFrom(bondLoop);
                        to   = cd->getBondTo(bondLoop);
                        if ((newFrom = atomList.find(from)) != -1 &&
                            (newTo = atomList.find(to)) != -1) {
                            newChemData->bondFrom.set1Value(bondCount, newFrom);
                            newChemData->bondTo.set1Value(bondCount, newTo);
                            newChemData->bondType.set1Value(bondCount,
                                cd->getBondType(bondLoop));
                            newChemData->bondIndex.set1Value(bondCount, bondCount);
                            bondCount++;
                        }
                    }
                }
            }
        }
    
        // Be sure to set the numberOfAtoms and the numberOfBonds in the
        // ChemData node.
        newChemData->numberOfAtoms.setValue(atomCount);
        newChemData->numberOfBonds.setValue(bondCount);
    
        // Put the scene graph on the clipboard
        if (clipBoard == NULL) {
            clipBoard = new SoXtClipboard(SoXt::getTopLevelWidget());
        }
        clipBoard->copy(newRoot, eventTime);
    
        newRoot->unref();
    }
    

Filling ChemData Fields Quickly

The ChemData node and the tables in ChemColor and ChemRadii store information regarding the chemical system in field-types derived from SoMField—the base class for multiple-valued fields. To store data in one of these fields, use the set1Value() method.

Each time the set1Value() method for a multiple-valued field is invoked, it makes sure the internal array used to store data is large enough for one more entry. If it is not, the method allocates a new array large enough to hold one more value and copies the contents of the old array into the new array. The reallocating and copying can become time-consuming for large chemical systems.

The following procedure describes a faster way of putting data into these fields.

  1. Invoke the setNum() method for a multiple-valued field. This invocation sets the size of the internal array maintained by the field to the number supplied as the argument to setNum(). You could use set1Value() at this point. It will not have to reallocate the array and will therefore be quicker. However, each invocation of set1Value() still causes it to check if there is enough room in the array.

  2. Alternatively, you can obtain a pointer to the multiple-valued field's internal array by using its startEditing() method. You can then directly store data in the array without using the set1Value() method. Care must be taken that the number of items added to the array is not greater than the number specified with the setNum() method. Also, once startEditing() has been invoked, it is illegal to invoke any other methods which edit the field, such as set1Value() or setValue(), until the finishEditing() method has been invoked.

  3. When all the data is stored in the array, invoke the finishEditing() method.

For more information regarding setNum(), set1Value(), startEditing(), and finishEditing(), see the appropriate man page for the particular multiple-valued field. Also look at the description of the setValues() method as a way of copying the contents of an existing array into a multiple-valued field.

The following code is an excerpt from /usr/share/src/MolInventor/examples/helloMI/readFile.c++.

Example 8-6. Filling chemData Fields Quickly


    // Set the numberOfAtoms and numberOfBonds in chemData
    chemData->numberOfAtoms.setValue(numAtoms);
    chemData->numberOfBonds.setValue(numBonds);

    // For fast filling of the chemData fields, first set the size of the
    // ChemData fields.
    chemData->atomicNumber.setNum(numAtoms);
    chemData->atomId.setNum(numAtoms);
    chemData->atomName.setNum(numAtoms);
    chemData->atomIndex.setNum(numAtoms);
    chemData->atomCoordinates.setNum(numAtoms);

    chemData->bondFrom.setNum(numBonds);
    chemData->bondTo.setNum(numBonds);
    chemData->bondType.setNum(numBonds);
    chemData->bondIndex.setNum(numBonds);

    // Then “startEditing()” the fields
    short    *atomicNumber    = chemData->atomicNumber.startEditing();
    int32_t  *atomId          = chemData->atomId.startEditing();
    SbString *atomName        = chemData->atomName.startEditing();
    int32_t  *atomIndex       = chemData->atomIndex.startEditing();
    SbVec3f  *atomCoordinates = chemData->atomCoordinates.startEditing();
    
    int32_t  *bondFrom        = chemData->bondFrom.startEditing();
    int32_t  *bondTo          = chemData->bondTo.startEditing();
    int32_t  *bondType        = chemData->bondType.startEditing();
    int32_t  *bondIndex       = chemData->bondIndex.startEditing();

    // Read in the atom data
    // Need atomicNumber, atomIndex, atomId, atomName
    // and atomCoordinates
    
    short atnum;
    float xx, yy, zz;
    SbString atomString;
    
    for (i = 0; i < numAtoms; i++) {
        fgets(buf, 132, fp);
        parseAtomRecord(buf, xx, yy, zz, atomString);
        stringToAtnum(atomString.getString(), atnum);

        // If didn't use “startEditing()” then the following code
        // would be used:
        //
        // chemData->atomicNumber.set1Value(i, atnum);
        // chemData->atomIndex.set1Value(i, atnum);
        // chemData->atomId.set1Value(i, i+1);
        // chemData->atomName.set1Value(i, atomString);
        // chemData->atomCoordinates.set1Value(i, SbVec3f(xx,yy,zz));
        //
        // The reason this is slower is that the field size is
        // reallocated with each “set1Value()”.

        atomicNumber[i]    = atnum;
        atomIndex[i]       = atnum;
        atomId[i]          = i+1;
        atomName[i]        = atomString;
        atomCoordinates[i] = SbVec3f(xx, yy, zz);
    }

    // “finishEditing()” the atom fields

    chemData->atomicNumber.finishEditing();
    chemData->atomId.finishEditing();
    chemData->atomName.finishEditing();
    chemData->atomIndex.finishEditing();
    chemData->atomCoordinates.finishEditing();

    // Read in the bond data
    // Need bondFrom, bondTo, bondType, bondIndex

    int32_t from, to;
    short type;

    for (i = 0; i < numBonds; i++) {
        fgets(buf, 132, fp);
        parseBondRecord(buf, from, to, type);

        // If didn't use “startEditing()” then the following code
        // would be used:
        //
        // chemData->bondFrom.set1Value(i, from);
        // chemData->bondTo.set1Value(i, to);
        // chemData->bondType.set1Value(i, type);
        // chemData->bondIndex.set1Value(i, i);

        bondFrom[i]  = from;
        bondTo[i]    = to;
        bondType[i]  = type;
        bondIndex[i] = i;
    }

    // “finishEditing()” the bond fields

    chemData->bondFrom.finishEditing();
    chemData->bondTo.finishEditing();
    chemData->bondType.finishEditing();
    chemData->bondIndex.finishEditing();