Web Analytics Made Easy - Statcounter
Skip to content

Programming a path generator#

Example

In this tutorial, we are going to create an editor such that a user can create paths in the viewport.

First, use the SAMSON Extension Generator to create a new SAMSON Extension called PathGenerator containing:

  • An Editor class called Editor (full name: SEPathGeneratorEditor) - this class will make it possible for the user to add new nodes to the path.
  • A Visual Model class called VisualModel (full name: SEPathGeneratorVisualModel) - this will be the main class used to represent a path in the viewport and the document.
  • A Node class called Node (full name: SEPathGeneratorNode) - this class will be used to represent path nodes and the corresponding GUI class will be used to interact with the node.

Extension generator: choose classes

Sample code#

The source code for an Extension created in this tutorial can be found at SAMSON-Developer-Tutorials.

Preliminaries#

Let's do a few things to set up our SAMSON Extension.

First, since we want the user to create paths through the editor contained in our SAMSON Extension, we are going to hide from SAMSON most other classes (e.g., this won't allow the use to add this visual model). Precisely, we need to remove from the descriptor of the SAMSON Extension all the classes that we do not want to expose, and only keep the ones that will be directly useful to the user, i.e. the editor itself and the node GUI. Thus, comment lines in SEPathGeneratorDescriptor.cpp such that you obtain:

SB_ELEMENT_CLASSES_BEGIN;

    SB_ELEMENT_CLASS(SEPathGeneratorEditor);
    //SB_ELEMENT_CLASS(SEPathGeneratorVisualModel);
    //SB_ELEMENT_CLASS(SEPathGeneratorVisualModelProperties);
    //SB_ELEMENT_CLASS(SEPathGeneratorNode);
    SB_ELEMENT_CLASS(SEPathGeneratorNodeGUI);
    //SB_ELEMENT_CLASS(SEPathGeneratorNodeProperties);

SB_ELEMENT_CLASSES_END;

Then, modify the getName(), getDescription() functions of SEPathGeneratorEditor to provide a user-friendly name for the editor:

QString SEPathGeneratorEditor::getDescription() const { 

    // SAMSON Extension generator pro tip: modify this function to return a user-friendly string that will be displayed in menus

    return QObject::tr("Path creator"); 

}

And don't forget the tooltip:

QString SEPathGeneratorEditor::getToolTip() const { 

    // SAMSON Extension generator pro tip: modify this function to have your editor display a tool tip in the SAMSON GUI when the mouse hovers the editor's icon

    return QObject::tr("Click in the viewport to create a path and add nodes to it.<br>Press Escape to end the path."); 

}

Since we do not need a GUI for the editor itself, we remove it from the constructor and the destructor of the editor in SEPathGeneratorEditor.cpp:

SEPathGeneratorEditor::SEPathGeneratorEditor() {

    propertyWidget = nullptr;

}

SEPathGeneratorEditor::~SEPathGeneratorEditor() {}

Path nodes#

In this tutorial, a path will be composed of path nodes connected by straight lines.

We need each path node to store its position so, at the beginning of SEPathGeneratorNode.hpp, we include SBVector3.hpp which contains the declarations of all physical vectors:

#include "SBVector3.hpp"

and in the end of the class declaration, we add this position and its accessors:

    SBPosition3 getPosition() const;
    void setPosition(const SBPosition3& position);

private:

    SBPosition3 position;

and we define these accessors in SEPathGeneratorNode.cpp:

SBPosition3 SEPathGeneratorNode::getPosition() const {

    return position;

}

void SEPathGeneratorNode::setPosition(const SBPosition3& position) {

    this->position = position;

}

Finally, we modify the declaration of the path node constructor of SEPathGeneratorNode to accept a position:

    SEPathGeneratorNode(const SBPosition3& position);

and we modify the constructor accordingly in SEPathGeneratorNode.cpp:

SEPathGeneratorNode::SEPathGeneratorNode(const SBPosition3& position) {

    this->position = position;

}

Finally, we modify the constructor descriptor in SEPathGeneratorNodeDescriptor.hpp:

SB_FACTORY_BEGIN;

    SB_CONSTRUCTOR_1(SEPathGeneratorNode, const SBPosition3&);

SB_FACTORY_END;

Paths#

We need paths to hold a list of path nodes, so we include SEPathGeneratorNode.hpp at the beginning of SEPathGeneratorVisualModel.hpp:

#include "SEPathGeneratorNode.hpp"

and we add such a list at the end of the class declaration, as well as functions to manage it:

    void addNode(SEPathGeneratorNode* pathNode);
    void removeNode(SEPathGeneratorNode* pathNode);

private:

    SBPointerList<SEPathGeneratorNode> pathNodeList;

and we define these functions in SEPathGeneratorVisualModel.cpp:

void SEPathGeneratorVisualModel::addNode(SEPathGeneratorNode* pathNode) {

    if (pathNode)
        pathNodeList.addReferenceTarget(pathNode);

}

void SEPathGeneratorVisualModel::removeNode(SEPathGeneratorNode* pathNode) {

    if (pathNode)
        pathNodeList.removeReferenceTarget(pathNode);

}

Note that we use SAMSON pointers to manage this list, as we should whenever we want to hold references to persistent SAMSON objects.

Finally, we need to display the path based on the list of path nodes, so we modify the display function:

void SEPathGeneratorVisualModel::display(SBNode::RenderingPass renderingPass) {

    // SAMSON Extension generator pro tip: this function is called by SAMSON during the main rendering loop. This is the main function of your visual model. 
    // Implement this function to display things in SAMSON, for example thanks to the utility functions provided by SAMSON (e.g. displaySpheres, displayTriangles, etc.)

    if (pathNodeList.empty()) return;

    const unsigned int numberOfPathNodes = pathNodeList.size();
    const unsigned int numberOfPathLines = numberOfPathNodes - 1;

    const unsigned int inheritedFlags = getInheritedFlags();
    const float nodeRadius = SBQuantity::length(SBQuantity::angstrom(0.1)).getValue();
    const float lineRadius = 0.5 * nodeRadius;

    // allocate arrays and initialize them to zeros

    float* positionData = new float[3 * numberOfPathNodes]();
    float* pathNodeRadiusData = new float[numberOfPathNodes]();
    float* pathLineRadiusData = new float[numberOfPathNodes]();
    float* colorData = new float[4 * numberOfPathNodes]();
    unsigned int* capData = new unsigned int[numberOfPathNodes]();
    unsigned int* flagData = new unsigned int[numberOfPathNodes]();
    unsigned int* indexData = new unsigned int[2 * numberOfPathLines]();
    unsigned int* nodeIndexData = new unsigned int[numberOfPathNodes]();

    // fill in the arrays

    unsigned int i = 0;

    SB_FOR(SEPathGeneratorNode * currentPathNode, pathNodeList) {

       const SBPosition3& position = currentPathNode->getPosition();
        positionData[3 * i + 0] = (float)position.v[0].getValue();
        positionData[3 * i + 1] = (float)position.v[1].getValue();
        positionData[3 * i + 2] = (float)position.v[2].getValue();
        nodeIndexData[i] = currentPathNode->getNodeIndex();

        if (i < numberOfPathLines) {

            indexData[2 * i + 0] = i;
            indexData[2 * i + 1] = i + 1;

        }

        pathNodeRadiusData[i] = nodeRadius;
        pathLineRadiusData[i] = lineRadius;
        colorData[4 * i + 0] = 1.0f;
        colorData[4 * i + 1] = 1.0f;
        colorData[4 * i + 2] = 0.0f;
        colorData[4 * i + 3] = 1.0f;
        capData[i] = 0;
        flagData[i] = currentPathNode->getInheritedFlags() | inheritedFlags;

        i++;

    }

    // display

    if (renderingPass == SBNode::RenderingPass::OpaqueGeometry) {

        SAMSON::displaySpheres(numberOfPathNodes, positionData, pathNodeRadiusData, colorData, flagData);
        SAMSON::displayCylinders(numberOfPathLines, numberOfPathNodes, indexData, positionData, pathLineRadiusData, capData, colorData, flagData);

    }
    else if (renderingPass == SBNode::RenderingPass::ShadowingGeometry) {

        // display for shadows

        SAMSON::displaySpheres(numberOfPathNodes, positionData, pathNodeRadiusData, colorData, flagData, true);
        SAMSON::displayCylinders(numberOfPathLines, numberOfPathNodes, indexData, positionData, pathLineRadiusData, capData, colorData, flagData, true);

    }
    else if (renderingPass == SBNode::RenderingPass::SelectableGeometry) {

        // display in the selectable mode in order to make path nodes movable

        SAMSON::displaySpheresSelection(numberOfPathNodes, positionData, pathNodeRadiusData, nodeIndexData);

    }

    // clean

    delete[] positionData;
    delete[] indexData;
    delete[] nodeIndexData;
    delete[] pathNodeRadiusData;
    delete[] pathLineRadiusData;
    delete[] capData;
    delete[] colorData;
    delete[] flagData;

}

Note

In order to make path nodes movable, we need to make them selectable in the viewport. To achieve this, we implement the SBNode::RenderingPass::SelectableGeometry pass in the display function of SEPathGeneratorVisualModel in which we supply node indices to the displaySpheresSelection function.

Editor#

The editor needs to hold a pointer to the path that is currently being created, so we include SEPathGeneratorVisualModel.hpp at the beginning of SEPathGeneratorEditor.hpp:

#include "SEPathGeneratorVisualModel.hpp"

In the SEPathGeneratorEditor class declaration, add a SAMSON pointer to the path and a pointer to the selected node in order to be able to move the path nodes:

private:

    SBPointer<SEPathGeneratorVisualModel>    path;
    SBPointer<SEPathGeneratorNode>           selectedPathNode;

This path pointer will be non-zero whenever we are creating or editing a path, and will be zero otherwise. So we need to make sure that we reset this pointer to zero whenever we begin and end editing in SEPathGeneratorEditor.cpp (i.e. when the user changes the active editor):

void SEPathGeneratorEditor::beginEditing() {

    path = nullptr;
    selectedPathNode = nullptr;

}

void SEPathGeneratorEditor::endEditing() {

    path = nullptr;
    selectedPathNode = nullptr;

}

and we also make it possible for a user to stop the current path by pressing the Escape key:

void SEPathGeneratorEditor::keyPressEvent(QKeyEvent* event) {

    if (event->key() == Qt::Key_Escape) {

        path = nullptr;
        selectedPathNode = nullptr;
        event->accept();

    }

}

We now want to create path nodes when we release the mouse button in the viewport. To achieve this, we modify the mouseReleaseEvent function in SEPathGeneratorEditor.cpp:

void SEPathGeneratorEditor::mouseReleaseEvent(QMouseEvent* event) {

    // create the path if necessary, i.e. if we don't have a valid pointer or
    // if our pointer points to an erased path

    if (path == nullptr || (path != nullptr && path->isErased())) {

        path = new SEPathGeneratorVisualModel();
        path->create();
        SAMSON::getActiveDocument()->addChild(path());

    }

    // get a world position from the mouse position when the user clicked

    SBPosition3 position = SAMSON::getWorldPositionFromViewportPosition(event->pos());

    // add a path node

    SEPathGeneratorNode* pathNode = new SEPathGeneratorNode(position);
    path->addNode(pathNode);

    SAMSON::requestViewportUpdate(); // refresh the viewport

}

To make the nodes movable, we determine whether the user clicks on a path node during a mouse press event in SEPathGeneratorEditor.cpp:

void SEPathGeneratorEditor::mousePressEvent(QMouseEvent* event) {

    // find whether the user clicked on a node path

    SBNode* node = SAMSON::getNode(event->pos(), 
        (SBNode::GetClass() == std::string("SEPathGeneratorNode")) &&
        (SBNode::GetElementUUID() == SBUUID(SB_ELEMENT_UUID)));

    if (node) selectedPathNode = static_cast<SEPathGeneratorNode*>(node);
    else selectedPathNode = nullptr;

}

Note that we only get nodes which have the same class name and the same extension UUID, so we're sure that we can cast to SEPathGeneratorNode*.

Finally, if a path node is selected, we move it when the user moves the mouse:

void SEPathGeneratorEditor::mouseMoveEvent(QMouseEvent* event) {

    if (selectedPathNode != nullptr) {

        // get a world position from the mouse position when the user clicked

        SBPosition3 position = SAMSON::getWorldPositionFromViewportPosition(event->pos());

        // move the path node

        selectedPathNode->setPosition(position);

        // refresh the viewport

        SAMSON::requestViewportUpdate();

    }

}

and we unselect the node when the user releases the mouse button by modifying the mouseReleaseEvent function:

void SEPathGeneratorEditor::mouseReleaseEvent(QMouseEvent* event) {

    if (selectedPathNode != nullptr) {

        selectedPathNode = nullptr;
        return;

    }

    // ...

}

Conclusion#

We would need a few more things to make this SAMSON Extension more user-friendly: make actions undoable, being able to erase nodes, provide feedback when we click in the viewport to show the user where a node is about to be created, etc.

If you have any questions or feedback, please use the SAMSON Connect Forum.