diff --git a/avogadro/qtplugins/CMakeLists.txt b/avogadro/qtplugins/CMakeLists.txt index 49d7c5223b..481930e876 100644 --- a/avogadro/qtplugins/CMakeLists.txt +++ b/avogadro/qtplugins/CMakeLists.txt @@ -97,6 +97,7 @@ endfunction() # Now to make the plugins. add_subdirectory(3dmol) add_subdirectory(applycolors) +add_subdirectory(aligntool) add_subdirectory(bondcentrictool) add_subdirectory(bonding) add_subdirectory(cartoons) diff --git a/avogadro/qtplugins/aligntool/CMakeLists.txt b/avogadro/qtplugins/aligntool/CMakeLists.txt new file mode 100644 index 0000000000..6b579404d9 --- /dev/null +++ b/avogadro/qtplugins/aligntool/CMakeLists.txt @@ -0,0 +1,22 @@ +set(aligntool_srcs + aligntool.cpp +) + +set(aligntool_uis +) + +set(aligntool_rcs + aligntool.qrc +) + +avogadro_plugin(AlignTool + "AlignTool" + ToolPlugin + aligntool.h + AlignTool + "${aligntool_srcs}" + "${aligntool_uis}" + "${aligntool_rcs}" +) + +target_link_libraries(AlignTool PRIVATE Avogadro::QtOpenGL) diff --git a/avogadro/qtplugins/aligntool/align.png b/avogadro/qtplugins/aligntool/align.png new file mode 100644 index 0000000000..5bd3838583 Binary files /dev/null and b/avogadro/qtplugins/aligntool/align.png differ diff --git a/avogadro/qtplugins/aligntool/align@2x.png b/avogadro/qtplugins/aligntool/align@2x.png new file mode 100644 index 0000000000..ab671cbb6b Binary files /dev/null and b/avogadro/qtplugins/aligntool/align@2x.png differ diff --git a/avogadro/qtplugins/aligntool/aligntool.cpp b/avogadro/qtplugins/aligntool/aligntool.cpp new file mode 100644 index 0000000000..c7109ba6a3 --- /dev/null +++ b/avogadro/qtplugins/aligntool/aligntool.cpp @@ -0,0 +1,318 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + This source code is released under the 3-Clause BSD License, (see "LICENSE"). +******************************************************************************/ + +#include "aligntool.h" + +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using Avogadro::Core::Elements; +using Avogadro::QtGui::Molecule; +using Avogadro::Rendering::GeometryNode; +using Avogadro::Rendering::Identifier; +using Avogadro::Rendering::TextLabel3D; +using Avogadro::Rendering::TextProperties; + +namespace Avogadro::QtPlugins { + +using QtGui::Molecule; +using QtGui::RWAtom; + +AlignTool::AlignTool(QObject* parent_) + : QtGui::ToolPlugin(parent_), m_activateAction(new QAction(this)), + m_molecule(nullptr), m_toolWidget(nullptr) +{ + m_activateAction->setText(tr("Align")); + m_activateAction->setIcon(QIcon(":/icons/align.png")); + m_activateAction->setToolTip( + tr("Align Molecules\n\n" + "Left Mouse: \tSelect up to two atoms.\n" + "\tThe first atom is centered at the origin.\n" + "\tThe second atom is aligned to the selected axis.\n" + "Right Mouse: \tReset alignment.\n" + "Double-Click: \tCenter the atom at the origin.")); +} + +AlignTool::~AlignTool() +{ + if (m_toolWidget) + m_toolWidget->deleteLater(); +} + +QWidget* AlignTool::toolWidget() const +{ + if (!m_toolWidget) { + m_toolWidget = new QWidget; + + QLabel* labelAxis = new QLabel(tr("Axis:"), m_toolWidget); + labelAxis->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + labelAxis->setMaximumHeight(15); + + // Combo box to select desired aixs to align to + QComboBox* comboAxis = new QComboBox(m_toolWidget); + m_axis = 0; + comboAxis->addItem("x"); + comboAxis->addItem("y"); + comboAxis->addItem("z"); + comboAxis->setCurrentIndex(m_axis); + + // Button to actually perform actions + QPushButton* buttonAlign = new QPushButton(m_toolWidget); + buttonAlign->setText(tr("Align")); + connect(buttonAlign, SIGNAL(clicked()), this, SLOT(align())); + + QGridLayout* gridLayout = new QGridLayout(); + gridLayout->addWidget(labelAxis, 0, 0, 1, 1, Qt::AlignRight); + QHBoxLayout* hLayout = new QHBoxLayout; + hLayout->addWidget(comboAxis); + hLayout->addStretch(1); + gridLayout->addLayout(hLayout, 0, 1); + + QHBoxLayout* hLayout3 = new QHBoxLayout(); + hLayout3->addStretch(1); + hLayout3->addWidget(buttonAlign); + hLayout3->addStretch(1); + QVBoxLayout* layout = new QVBoxLayout(); + layout->addLayout(gridLayout); + layout->addLayout(hLayout3); + layout->addStretch(1); + m_toolWidget->setLayout(layout); + + connect(comboAxis, SIGNAL(currentIndexChanged(int)), this, + SLOT(axisChanged(int))); + + connect(m_toolWidget, SIGNAL(destroyed()), this, + SLOT(toolWidgetDestroyed())); + } + + return m_toolWidget; +} + +void AlignTool::axisChanged(int axis) +{ + // Axis to use - x=0, y=1, z=2 + m_axis = axis; +} + +void AlignTool::alignChanged(int align) +{ + // Type of alignment - 0=everything, 1=molecule + m_alignType = align; +} + +void AlignTool::align() +{ + if (m_atoms.size() == 0) + return; + + if (m_atoms.size() >= 1) + shiftAtomToOrigin(m_atoms[0].index); + if (m_atoms.size() == 2) + alignAtomToAxis(m_atoms[1].index, m_axis); + + m_atoms.clear(); +} + +void AlignTool::shiftAtomToOrigin(Index atomIndex) +{ + // Shift the atom to the origin + Vector3 shift = m_molecule->atom(atomIndex).position3d(); + const Core::Array& coords = m_molecule->atomPositions3d(); + Core::Array newCoords(coords.size()); + for (Index i = 0; i < coords.size(); ++i) + newCoords[i] = coords[i] - shift; + + m_molecule->setAtomPositions3d(newCoords, tr("Align at Origin")); + m_molecule->emitChanged(QtGui::Molecule::Atoms); +} + +void AlignTool::alignAtomToAxis(Index atomIndex, int axis) +{ + // Align the atom to the specified axis + Vector3 align = m_molecule->atom(atomIndex).position3d(); + const Core::Array& coords = m_molecule->atomPositions3d(); + Core::Array newCoords(coords.size()); + + double alpha, beta, gamma; + alpha = beta = gamma = 0.0; + + Vector3 pos = m_molecule->atom(atomIndex).position3d(); + pos.normalize(); + Vector3 axisVector; + + if (axis == 0) // x-axis + axisVector = Vector3(1., 0., 0.); + else if (axis == 1) // y-axis + axisVector = Vector3(0., 1., 0.); + else if (axis == 2) // z-axis + axisVector = Vector3(0., 0., 1.); + + // Calculate the angle of the atom from the axis + double angle = acos(axisVector.dot(pos)); + + // Get the vector for the rotation + axisVector = axisVector.cross(pos); + axisVector.normalize(); + + // Now to rotate the fragment + for (Index i = 0; i < coords.size(); ++i) + newCoords[i] = Eigen::AngleAxisd(-angle, axisVector) * coords[i]; + + m_molecule->setAtomPositions3d(newCoords, tr("Align to Axis")); + m_molecule->emitChanged(QtGui::Molecule::Atoms); +} + +void AlignTool::toolWidgetDestroyed() +{ + m_toolWidget = nullptr; +} + +QUndoCommand* AlignTool::mousePressEvent(QMouseEvent* e) +{ + // If the click is released on an atom, add it to the list + if (e->button() != Qt::LeftButton || !m_renderer) + return nullptr; + + Identifier hit = m_renderer->hit(e->pos().x(), e->pos().y()); + + // Now add the atom on release. + if (hit.type == Rendering::AtomType) { + if (toggleAtom(hit)) + emit drawablesChanged(); + e->accept(); + } + + return nullptr; +} + +QUndoCommand* AlignTool::mouseDoubleClickEvent(QMouseEvent* e) +{ + // Reset the atom list + if (e->button() == Qt::LeftButton && !m_atoms.isEmpty()) { + m_atoms.clear(); + emit drawablesChanged(); + e->accept(); + } + return nullptr; +} + +bool AlignTool::toggleAtom(const Rendering::Identifier& atom) +{ + int ind = m_atoms.indexOf(atom); + if (ind >= 0) { + m_atoms.remove(ind); + return true; + } + + if (m_atoms.size() >= 2) + return false; + + m_atoms.push_back(atom); + return true; +} + +inline Vector3ub AlignTool::contrastingColor(const Vector3ub& rgb) const +{ + // If we're far 'enough' (+/-32) away from 128, just invert the component. + // If we're close to 128, inverting the color will end up too close to the + // input -- adjust the component before inverting. + const unsigned char minVal = 32; + const unsigned char maxVal = 223; + Vector3ub result; + for (size_t i = 0; i < 3; ++i) { + unsigned char input = rgb[i]; + if (input > 160 || input < 96) + result[i] = static_cast(255 - input); + else + result[i] = static_cast(255 - (input / 4)); + + // Clamp to 32-->223 to prevent pure black/white + result[i] = std::min(maxVal, std::max(minVal, result[i])); + } + + return result; +} + +void AlignTool::draw(Rendering::GroupNode& node) +{ + if (m_atoms.size() == 0) + return; + + auto* geo = new GeometryNode; + node.addChild(geo); + + // Add labels, extract positions + QVector positions(m_atoms.size(), Vector3()); + + TextProperties atomLabelProp; + atomLabelProp.setFontFamily(TextProperties::SansSerif); + atomLabelProp.setAlign(TextProperties::HCenter, TextProperties::VCenter); + + for (int i = 0; i < m_atoms.size(); ++i) { + Identifier& ident = m_atoms[i]; + Q_ASSERT(ident.type == Rendering::AtomType); + Q_ASSERT(ident.molecule != nullptr); + + auto atom = m_molecule->atom(ident.index); + Q_ASSERT(atom.isValid()); + unsigned char atomicNumber(atom.atomicNumber()); + positions[i] = atom.position3d(); + + // get the color of the atom + const unsigned char* color = Elements::color(atomicNumber); + atomLabelProp.setColorRgb(contrastingColor(Vector3ub(color)).data()); + + auto* label = new TextLabel3D; + label->setText(QString("#%1").arg(i + 1).toStdString()); + label->setTextProperties(atomLabelProp); + label->setAnchor(positions[i].cast()); + label->setRadius( + static_cast(Elements::radiusCovalent(atomicNumber)) + 0.1f); + geo->addDrawable(label); + } +} + +void AlignTool::registerCommands() +{ + emit registerCommand("centerAtom", tr("Center the atom at the origin.")); + emit registerCommand( + "alignAtom", + tr("Rotate the molecule to align the atom to the specified axis.")); +} + +bool AlignTool::handleCommand(const QString& command, + const QVariantMap& options) +{ + if (m_molecule == nullptr) + return false; // No molecule to handle the command. + + return true; +} +} // namespace Avogadro::QtPlugins diff --git a/avogadro/qtplugins/aligntool/aligntool.h b/avogadro/qtplugins/aligntool/aligntool.h new file mode 100644 index 0000000000..1fb13d517e --- /dev/null +++ b/avogadro/qtplugins/aligntool/aligntool.h @@ -0,0 +1,96 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + This source code is released under the 3-Clause BSD License, (see "LICENSE"). +******************************************************************************/ + +#ifndef AVOGADRO_QTPLUGINS_ALIGNTOOL_H +#define AVOGADRO_QTPLUGINS_ALIGNTOOL_H + +#include + +#include +#include + +namespace Avogadro { +namespace QtPlugins { + +/** + * @class AlignTool aligntool.h + * + * @brief The Align Tool class aligns molecules to a frame of reference. + * @author Geoffrey Hutchison + */ +class AlignTool : public QtGui::ToolPlugin +{ + Q_OBJECT +public: + explicit AlignTool(QObject* parent_ = nullptr); + ~AlignTool() override; + + QString name() const override { return tr("Align tool"); } + QString description() const override + { + return tr("Align molecules to a Cartesian axis"); + } + unsigned char priority() const override { return 90; } + QAction* activateAction() const override { return m_activateAction; } + QWidget* toolWidget() const override; + + void setMolecule(QtGui::Molecule* mol) override + { + if (mol) + m_molecule = mol->undoMolecule(); + } + + void setEditMolecule(QtGui::RWMolecule* mol) override { m_molecule = mol; } + + void setGLRenderer(Rendering::GLRenderer* renderer) override + { + m_renderer = renderer; + } + + QUndoCommand* mousePressEvent(QMouseEvent* e) override; + QUndoCommand* mouseDoubleClickEvent(QMouseEvent* e) override; + + void draw(Rendering::GroupNode& node) override; + + Vector3ub contrastingColor(const Vector3ub& rgb) const; + + void shiftAtomToOrigin(Index atomIndex); + void alignAtomToAxis(Index atomIndex, int axis); + + bool toggleAtom(const Rendering::Identifier& atom); + + bool handleCommand(const QString& command, + const QVariantMap& options) override; + + /** + * Called by the app to tell the tool to register commands. + * If the tool has commands, it should emit the registerCommand signals. + */ + void registerCommands() override; + +public Q_SLOTS: + void axisChanged(int axis); + void alignChanged(int align); + void align(); + +private: + QAction* m_activateAction; + QtGui::RWMolecule* m_molecule; + Rendering::GLRenderer* m_renderer; + QVector m_atoms; + + int m_axis; + int m_alignType; + + mutable QWidget* m_toolWidget; + +private Q_SLOTS: + void toolWidgetDestroyed(); +}; + +} // namespace QtPlugins +} // namespace Avogadro + +#endif // AVOGADRO_QTOPENGL_ALIGNTOOL_H diff --git a/avogadro/qtplugins/aligntool/aligntool.qrc b/avogadro/qtplugins/aligntool/aligntool.qrc new file mode 100644 index 0000000000..42a612d3b3 --- /dev/null +++ b/avogadro/qtplugins/aligntool/aligntool.qrc @@ -0,0 +1,6 @@ + + + align.png + align@2x.png + +