Programming new editors: Atom pusher

In this tutorial, you will learn how to program an Editor that pushes atoms in the viewport. Our editor will be a sphere, that when active pushes atoms.

See also
Editors

The source code for an Element created in this tutorial can be found at https://github.com/1A-OneAngstrom/SAMSON-Developer-Tutorials.

Generating an Element

We will start by creating a new SAMSON Element called AtomPusher thanks to the SAMSON Element generator (please, refer to the SAMSON Element generator tutorial for a reminder on how to use it):

The SAMSON Element generator generates an editor class (derived from the SBGEditor class) and a property widget class for the editor.

Setting the description of the Element

Open the header file of the editor descriptor (file: SEAtomPusherEditorDescriptor.hpp) and modify the class description:

SB_CLASS_DESCRIPTION("DevTutorial: Atom pusher");

Modify the getName function for both the editor class (file: SEAtomPusherEditor.cpp) and the editor GUI class (file: SEAtomPusherEditorGUI.cpp) to have a user-friendly description of the editor inside SAMSON:

return "DevTutorial: Atom pusher";

Add the getDescription function in the editor class (file: SEAtomPusherEditor.hpp)

virtual QString getDescription() const; ///< Returns the menu item text

And implement it as follows to have a user-friendly description of the editor in the menu:

QString SEAtomPusherEditor::getDescription() const {
return QObject::tr("DevTutorial: Atom pusher");
}

Modify the getToolTip function in the editor class (file: SEAtomPusherEditor.cpp) to have a user-friendly description of what the editor does:

QString SEAtomPusherEditor::getToolTip() const {
return QObject::tr("Pushes atoms when pressed");
}

Now, try building the SAMSON Element and when you launch SAMSON you should be able to find it in the App menu or in the toolbar. For now, it has no interface and does nothing, but we will now add functionality to it.

Interface of an editor

The SAMSON Element generator provides a GUI class (a property widget) for an editor which can be invoked by double-clicking on the editor's icon in the toolbar.

If your editor does not require GUI, you can disable it by simply commenting the creation and destruction of the property widget in the constructor and in the destructor of the editor class, respectively.

Displaying an editor

Since our editor is a sphere which can be in two states (active or not), we add the following variables in the editor class (file: SEAtomPusherEditor.hpp):

class SEAtomPusherEditor : public SBGEditor {
// ...
public:
SBPosition3 spherePosition; // position of the sphere
SBQuantity::length sphereRadius; // radius of the sphere
bool sphereIsActive; // true of sphere is active
};

And we initialize them in the constructor of the editor class:

SEAtomPusherEditor::SEAtomPusherEditor() {
// ...
spherePosition = SBPosition3();
sphereRadius = SBQuantity::angstrom(1.0);
sphereIsActive = false;
}

Let's now create a visual representation of our editor, the sphere. For that we implement the display function in the editor class (file: SEAtomPusherEditor.cpp):

void SEAtomPusherEditor::display() {
float positionData[3];
float radiusData[1];
float colorData[4];
unsigned int flagData[1];
positionData[0] = spherePosition[0].getValue();
positionData[1] = spherePosition[1].getValue();
positionData[2] = spherePosition[2].getValue();
radiusData[0] = sphereRadius.getValue();
if (sphereIsActive){
// a white sphere
colorData[0] = 1.0f;
colorData[1] = 1.0f;
colorData[2] = 1.0f;
colorData[3] = 1.0f;
}
else {
// a green sphere
colorData[0] = 0.0f;
colorData[1] = 1.0f;
colorData[2] = 0.0f;
colorData[3] = 1.0f;
}
flagData[0] = 0;
SAMSON::displaySpheres(1, positionData, radiusData, colorData, flagData);
}

Here, we fill in arrays for position of the sphere, its radius, and color, which we send to the SAMSON::displaySpheres function that will render the sphere in SAMSON's viewport.

If you want your editor to cast shadows, you can implement the displayForShadow function as well by just invoking the display function in it (see Casting shadows for more information):

void SEAtomPusherEditor::displayForShadow() {
display();
}

In the display function we used the sphere's position, to know this position we need to implement the mouseMoveEvent function in the editor class (file: SEAtomPusherEditorGUI.cpp):

void SEAtomPusherEditor::mouseMoveEvent(QMouseEvent* event) {
spherePosition = SAMSON::getWorldPositionFromViewportPosition(event->pos());
}

Here, we set the shpere's position from the cursor position in the viewport.

Now, try building the Element and when you launch SAMSON you should be able to find it in the Edit > Editor menu or in the editor toolbar. For now, it has no editing functionality, but you should be able to see the editor's sphere in the SAMSON's viewport once select the editor and move the mouse in the viewport.

Pushing atoms

SAMSON redirects Qt events to the active editor (see the Editors section for more information). The editor class provides a number of possibilities to handle the GUI interactions:

  • mousePressEvent - handles mouse press event;
  • mouseReleaseEvent - handles mouse release event;
  • mouseMoveEvent - handles mouse move event;
  • mouseDoubleClickEvent - handles mouse double click event;
  • wheelEvent - handles wheel event;
  • keyPressEvent - handles key press event;
  • keyReleaseEvent - handles key release event.

Since our sphere will be pushing atoms only when it is active. We assume that our editor is active when the mouse button is pressed. For that, we implement mousePressEvent and mouseReleaseEvent functions as follows:

void SEAtomPusherEditor::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::MouseButton::LeftButton) {
sphereIsActive = true;
}
}
void SEAtomPusherEditor::mouseReleaseEvent(QMouseEvent* event) {
if (event->button() == Qt::MouseButton::LeftButton) {
sphereIsActive = false;
}
}

Add a function in the editor class (file: SEAtomPusherEditor.hpp) to push atoms

void pushAtoms();

And implement it as follows:

void SEAtomPusherEditor::pushAtoms() {
// check if the sphere is in active state
if (!sphereIsActive) return;
// get an indexer of all atoms in the active document
SBNodeIndexer nodeIndexer;
SB_FOR(SBNode * node, nodeIndexer){
SBPointer<SBAtom> atom = static_cast<SBAtom*>(node);
if (!atom.isValid()) continue;
// get the atom's position
SBPosition3 atomPosition = atom->getPosition();
// compute the vector from the sphere's centers to the atom's center
SBPosition3 vectorFromSphereCenter = atomPosition - spherePosition;
// set the min distance between centers of the sphere and the atom by summing their radii
SBQuantity::length minDistance = sphereRadius + SAMSON::getAtomRadius();
// check if the sphere touches the atom
if (vectorFromSphereCenter.norm() < minDistance){
// push the atom in the direction from the sphere
vectorFromSphereCenter = vectorFromSphereCenter * (minDistance / vectorFromSphereCenter.norm());
atom->setPosition(spherePosition + vectorFromSphereCenter);
}
}
}

First, we check whether the sphere is active (the sphere pushes atoms only in the active state). We get a node indexer of all atoms in the active documents. Then we get the position of an atom and check whether it is touched by the sphere, if so we push it in the direction from the sphere.

Before, we implemented the mouseMoveEvent function in the editor class to update the sphere's position when the cursor moves. Let's now re-implement this function to handle the mouse movement for pushing atoms:

void SEAtomPusherEditor::mouseMoveEvent(QMouseEvent* event) {
SBPosition3 nodePosition;
SBNode* pickedNode = SAMSON::getNode(event->pos(), nodePosition);
if (pickedNode == NULL)
spherePosition = SAMSON::getWorldPositionFromViewportPosition(event->pos());
else
spherePosition = SAMSON::getWorldPositionFromViewportPosition(event->pos(), nodePosition);
pushAtoms();
}

First, we update the sphere's position and then me invoke the pushAtoms function.

We also make it possible to change the sphere's radius through the wheel event by implementing the according function in the editor class:

void SEAtomPusherEditor::wheelEvent(QWheelEvent* event) {
int angle = event->delta();
sphereRadius = sphereRadius * pow(1.002, angle);
// check for the minimal size of the sphere
if (sphereRadius < SBQuantity::angstrom(0.1)) sphereRadius = SBQuantity::angstrom(0.1);
pushAtoms();
}

Note, that by invoking the pushAtoms function in the end we also take into account that the sphere might be at the same time in the active state pushing atoms.

Now, you have a working editor for pushing atoms. Build the Element, launch SAMSON, load or create any molecule, and choose the developed editor either from the editor's toolbar or from the Edit > Editors menu. Try moving the editor around, activating it, pushing atoms, and changing the sphere's radius.

atompusher-editor.gif
An atom pusher editor