In this tutorial, we are going to create an editor such that a user can create paths in the viewport.
First, use the SAMSON Element Generator to create a new SAMSON Element called Path containing:
- An Editor class called Editor (full name: SEPathEditor). This class will make it possible for the user to add new nodes to the path.
- A Visual Model class called VisualModel (full name: SEPathVisualModel). This will be the main class used to represent a path in the viewport and the document.
- A Node class called NodeĀ (full name: SEPathNode). This class will be used to represent path nodes. The corresponding GUI class will be used to interact with the node.
Preliminaries
Let’s do a few things to set up our SAMSON Element.
First, since we want the user to create paths through the editor contained in our SAMSON Element, we are going to hide from SAMSON most other classes. Precisely, we need to remove from the descriptor of the SAMSON Element 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, remove lines from SEPathDescriptor.cpp such that you obtain:
1 2 3 4 5 6 |
SB_ELEMENT_CLASSES_BEGIN; SB_ELEMENT_CLASS(SEPathEditor); SB_ELEMENT_CLASS(SEPathNodeGUI); SB_ELEMENT_CLASSES_END; |
Then, modify the getDescription function of SEPathEditor.cpp to provide a user-friendly name for the editor:
1 2 3 4 5 6 7 |
QString SEPathEditor::getDescription() const { // SAMSON Element 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:
1 2 3 4 5 6 7 |
QString SEPathEditor::getToolTip() const { // SAMSON Element 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. 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 SEPathEditor.cpp:
1 2 3 4 5 6 7 |
SEPathEditor::SEPathEditor() { propertyWidget = 0; } SEPathEditor::~SEPathEditor() {} |
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 SEPathNode.hpp, we include SBVector3.hpp which contains the declarations of all physical vectors:
1 2 3 4 |
#pragma once #include "SBDDataGraphNode.hpp" #include "SBVector3.hpp" |
and in the end of the class declaration, we add this position and its accessors:
1 2 3 4 5 6 7 8 |
SBPosition3 getPosition() const; void setPosition(const SBPosition3& position); private: SBPosition3 position; }; |
and we define these accessors in SEPathNode.cpp:
1 2 3 4 5 6 7 8 9 10 11 |
SBPosition3 SEPathNode::getPosition() const { return position; } void SEPathNode::setPosition(const SBPosition3& position) { this->position = position; } |
Finally, we modify the declaration of the path node constructor in SEPathNode.hpp to accept a position:
1 |
SEPathNode(const SBPosition3& position); |
and we modify the constructor accordingly in SEPathNode.cpp:
1 2 3 4 5 |
SEPathNode::SEPathNode(const SBPosition3& position) { this->position = position; } |
Finally, we modify the constructor descriptor in SEPathNodeDescriptor.hpp:
1 2 3 4 5 |
SB_FACTORY_BEGIN; SB_CONSTRUCTOR_1(SEPathNode, const SBPosition3&); SB_FACTORY_END; |
Paths
We need paths to hold a list of path nodes, so we include SEPathNode.hpp at the beginning of SEPathVisualModel.hpp:
1 2 3 4 5 6 7 8 |
#pragma once #include "SBMVisualModel.hpp" #include "SBBaseEvent.hpp" #include "SBDocumentEvent.hpp" #include "SBStructuralEvent.hpp" #include "SEPathNode.hpp" |
and we add such a list at the end of the class declaration, as well as functions to manage it:
1 2 3 4 5 6 7 8 |
void addNode(SEPathNode* pathNode); void removeNode(SEPathNode* pathNode); private: SBPointerList<SEPathNode> pathNodeList; }; |
and we define these functions in SEPathVisualModel.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void SEPathVisualModel::addNode(SEPathNode* pathNode) { if (!pathNode) return; pathNodeList.addReferenceTarget(pathNode); } void SEPathVisualModel::removeNode(SEPathNode* pathNode) { if (!pathNode) return; 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
void SEPathVisualModel::display() { if (pathNodeList.empty()) return; unsigned int numberOfPathNodes = pathNodeList.size(); unsigned int numberOfPathLines = pathNodeList.size() - 1; // 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](); // fill in the arrays unsigned int currentPathNodeIndex = 0; SB_FOR(SEPathNode* currentPathNode, pathNodeList) { SBPosition3 position = currentPathNode->getPosition(); positionData[3 * currentPathNodeIndex + 0] = (float)position.v[0].getValue(); positionData[3 * currentPathNodeIndex + 1] = (float)position.v[1].getValue(); positionData[3 * currentPathNodeIndex + 2] = (float)position.v[2].getValue(); if (currentPathNodeIndex < numberOfPathLines) { indexData[2 * currentPathNodeIndex + 0] = currentPathNodeIndex; indexData[2 * currentPathNodeIndex + 1] = currentPathNodeIndex + 1; } pathNodeRadiusData[currentPathNodeIndex] = (float)SBQuantity::length(SBQuantity::angstrom(0.1)).getValue(); pathLineRadiusData[currentPathNodeIndex] = (float)SBQuantity::length(SBQuantity::angstrom(0.05)).getValue(); colorData[4 * currentPathNodeIndex + 0] = 1.0f; colorData[4 * currentPathNodeIndex + 1] = 1.0f; colorData[4 * currentPathNodeIndex + 2] = 0.0f; colorData[4 * currentPathNodeIndex + 3] = 1.0f; capData[currentPathNodeIndex] = 0; flagData[currentPathNodeIndex] = currentPathNode->getInheritedFlags() | getInheritedFlags(); currentPathNodeIndex++; } // display SAMSON::displaySpheres(numberOfPathNodes, positionData, pathNodeRadiusData, colorData, flagData); SAMSON::displayCylinders(numberOfPathLines, numberOfPathNodes, indexData, positionData, pathLineRadiusData, capData, colorData, flagData); // clean delete[] positionData; delete[] indexData; delete[] pathNodeRadiusData; delete[] pathLineRadiusData; delete[] capData; delete[] colorData; delete[] flagData; } |
Editor
The editor needs to hold a pointer to the path that’s currently being created, so we include SEPathVisualModel.hpp at the beginning of SEPathEditor.hpp:
1 2 3 4 5 6 7 8 9 10 11 |
#pragma once #include "SBGEditor.hpp" #include "SEPathEditorGUI.hpp" #include "SBBaseEvent.hpp" #include "SBDocumentEvent.hpp" #include "SBDynamicalEvent.hpp" #include "SBStructuralEvent.hpp" #include "SBAction.hpp" #include "SEPathVisualModel.hpp" |
and we store a SAMSON pointer to the path at the end of the editor class declaration:
1 2 3 4 5 |
private: SBPointer<SEPathVisualModel> path; }; |
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 SEPathEditor.cpp (i.e. when the user changes the active editor):
1 2 3 4 5 6 7 8 9 10 11 |
void SEPathEditor::beginEditing() { path = 0; } void SEPathEditor::endEditing() { path = 0; } |
and we also make it possible for a user to stop the current path by pressing the Escape key:
1 2 3 4 5 6 7 8 9 10 |
void SEPathEditor::keyPressEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Escape) { path = 0; 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 SEPathEditor.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
void SEPathEditor::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 == 0 || (path != 0 && path->isErased())) { path = new SEPathVisualModel(); path->create(); SAMSON::getActiveLayer()->addChild(path()); } // get a world position from the mouse position when the user clicked SBPosition3 position = SAMSON::getWorldPositionFromViewportPosition(event->x(), event->y()); // add a path node SEPathNode* pathNode = new SEPathNode(position); path->addNode(pathNode); SAMSON::requestViewportUpdate(); // refresh the viewport } |
Moving existing nodes
In order to make path nodes movable, we first need to make them selectable in the viewport. To achieve this, we modify the displayForSelection function in SEPathVisualModel.cpp to render node indices instead of colors:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
void SEPathVisualModel::displayForSelection() { if (pathNodeList.empty()) return; unsigned int numberOfPathNodes = pathNodeList.size(); // allocate arrays and initialize them to zeros float* positionData = new float[3 * numberOfPathNodes](); float* pathNodeRadiusData = new float[numberOfPathNodes](); unsigned int* nodeIndexData = new unsigned int[numberOfPathNodes](); // fill in the arrays unsigned int currentPathNodeIndex = 0; SB_FOR(SEPathNode* currentPathNode, pathNodeList) { SBPosition3 position = currentPathNode->getPosition(); positionData[3 * currentPathNodeIndex + 0] = (float)position.v[0].getValue(); positionData[3 * currentPathNodeIndex + 1] = (float)position.v[1].getValue(); positionData[3 * currentPathNodeIndex + 2] = (float)position.v[2].getValue(); pathNodeRadiusData[currentPathNodeIndex] = (float)SBQuantity::length(SBQuantity::angstrom(0.1)).getValue(); nodeIndexData[currentPathNodeIndex] = currentPathNode->getNodeIndex(); currentPathNodeIndex++; } // display SAMSON::displaySpheresSelection(numberOfPathNodes, positionData, pathNodeRadiusData, nodeIndexData); // clean delete[] positionData; delete[] pathNodeRadiusData; delete[] nodeIndexData; } |
Then, we declare a pointer to a selected node in SEPathEditor.hpp:
1 2 3 4 5 6 |
private: SBPointer<SEPathVisualModel> path; SBPointer<SEPathNode> selectedPathNode; }; |
and we modify the editor to clean the pointer’s value when necessary:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void SEPathEditor::beginEditing() { path = 0; selectedPathNode = 0; } void SEPathEditor::endEditing() { path = 0; selectedPathNode = 0; } |
and:
1 2 3 4 5 6 7 8 9 10 11 |
void SEPathEditor::keyPressEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Escape) { path = 0; selectedPathNode = 0; event->accept(); } } |
Then, we determine whether the user clicks on a path node during a mouse press event in SEPathEditor.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 |
void SEPathEditor::mousePressEvent(QMouseEvent* event) { // find whether the user clicked on a node path SBNode* node = SAMSON::getNode(event->x(), event->y(), (SBNode::GetClass() == std::string("SEPathNode")) && (SBNode::GetElementUUID() == SBUUID(SB_ELEMENT_UUID))); if (node) selectedPathNode = static_cast<SEPathNode*>(node); else selectedPathNode = 0; } |
Note that we only get nodes which have the same class name and the same element UUID, so we’re sure that we can cast to SEPathNode*.
Finally, if a path node is selected, we move it when the user moves the mouse:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
void SEPathEditor::mouseMoveEvent(QMouseEvent* event) { if (selectedPathNode != 0) { // get a world position from the mouse position when the user clicked SBPosition3 position = SAMSON::getWorldPositionFromViewportPosition(event->x(), event->y()); // 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:
1 2 3 4 5 6 7 8 9 10 |
void SEPathEditor::mouseReleaseEvent(QMouseEvent* event) { if (selectedPathNode != 0) { selectedPathNode = 0; return; } // ... |
Conclusion
We would need a few more things to make this SAMSON Element 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. This will be seen in an upcoming part.
If you have any questions or feedback, please use the SAMSON forum.