From d52126a3c3c305ce0d15af5115323b5841cb6326 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Wed, 18 Dec 2024 14:50:44 -0500 Subject: [PATCH] Add support for atom and bond labels in the property tables Also render support for bond length and custom labels Signed-off-by: Geoff Hutchison --- avogadro/core/bond.h | 35 ++++++- avogadro/qtgui/rwmolecule.cpp | 15 +++ avogadro/qtgui/rwmolecule.h | 37 ++++++++ avogadro/qtgui/rwmolecule_undo.h | 20 ++++ avogadro/qtplugins/label/label.cpp | 94 ++++++++++++++++++- avogadro/qtplugins/label/label.h | 3 + .../propertytables/propertymodel.cpp | 28 ++++-- .../qtplugins/propertytables/propertymodel.h | 4 +- 8 files changed, 222 insertions(+), 14 deletions(-) diff --git a/avogadro/core/bond.h b/avogadro/core/bond.h index a6832209d4..e35921f61a 100644 --- a/avogadro/core/bond.h +++ b/avogadro/core/bond.h @@ -110,6 +110,16 @@ class BondTemplate unsigned char order() const; /** @} */ + /** + * The length of the bond or 0.0 if the bond is invalid. + */ + Real length() const; + + /** + * A label for the bond (if any) + */ + std::string label() const; + private: MoleculeType* m_molecule = nullptr; Index m_index = MaxIndex; @@ -197,8 +207,8 @@ typename BondTemplate::AtomType BondTemplate::atom2() } template -typename BondTemplate::AtomType BondTemplate::getOtherAtom(Index index) - const +typename BondTemplate::AtomType +BondTemplate::getOtherAtom(Index index) const { if (atom1().index() == index) return atom2(); @@ -207,9 +217,9 @@ typename BondTemplate::AtomType BondTemplate::getOtherAt } template -typename BondTemplate::AtomType BondTemplate::getOtherAtom( - BondTemplate::AtomType atom -) const +typename BondTemplate::AtomType +BondTemplate::getOtherAtom( + BondTemplate::AtomType atom) const { return getOtherAtom(atom.index()); } @@ -226,6 +236,21 @@ unsigned char BondTemplate::order() const return m_molecule->bondOrders()[m_index]; } +template +Real BondTemplate::length() const +{ + if (!isValid()) + return 0.0; + + return (atom1().position3d() - atom2().position3d()).norm(); +} + +template +std::string BondTemplate::label() const +{ + return m_molecule->bondLabel(m_index); +} + } // namespace Avogadro::Core #endif // AVOGADRO_CORE_BOND_H diff --git a/avogadro/qtgui/rwmolecule.cpp b/avogadro/qtgui/rwmolecule.cpp index 10333bde3a..a400b1fc3e 100644 --- a/avogadro/qtgui/rwmolecule.cpp +++ b/avogadro/qtgui/rwmolecule.cpp @@ -173,12 +173,27 @@ bool RWMolecule::setAtomPositions3d(const Core::Array& pos, bool RWMolecule::setAtomLabel(Index atomId, const std::string& label, const QString& undoText) { + if (atomId >= atomCount()) + return false; + auto* comm = new ModifyAtomLabelCommand(*this, atomId, label); comm->setText(undoText); m_undoStack.push(comm); return true; } +bool RWMolecule::setBondLabel(Index bondId, const std::string& label, + const QString& undoText) +{ + if (bondId >= bondCount()) + return false; + + auto* comm = new ModifyBondLabelCommand(*this, bondId, label); + comm->setText(undoText); + m_undoStack.push(comm); + return true; +} + bool RWMolecule::setAtomPosition3d(Index atomId, const Vector3& pos, const QString& undoText) { diff --git a/avogadro/qtgui/rwmolecule.h b/avogadro/qtgui/rwmolecule.h index f47eb302b9..06eb4bfe03 100644 --- a/avogadro/qtgui/rwmolecule.h +++ b/avogadro/qtgui/rwmolecule.h @@ -223,10 +223,42 @@ class AVOGADROQTGUI_EXPORT RWMolecule : public QObject bool setAtomPosition3d(Index atomId, const Vector3& pos, const QString& undoText = tr("Change Atom Position")); + /** + * Get the label on an atom. + * @param atomId The index of the atom. + * @return The label of the atom indexed at @a atomId, or an empty string if + * @a atomId is invalid or has no label. + */ std::string atomLabel(Index atomId) const; + + /** + * Set the label of a single atom. + * @param atomId The index of the atom to modify. + * @param label The new label. + * @param undoText The undo text to be displayed for undo commands. + * @return True on success, false otherwise. + */ bool setAtomLabel(Index atomId, const std::string& label, const QString& undoText = tr("Change Atom Label")); + /** + * Get the label on a bond + * @param bondId The index of the bond. + * @return The label of the bond indexed at @a bondId, or an empty string if + * @a bondId is invalid or has no label. + */ + std::string bondLabel(Index bondId) const; + + /** + * Set the label of a single bond. + * @param bondId The index of the bond to modify. + * @param label The new label. + * @param undoText The undo text to be displayed for undo commands. + * @return True on success, false otherwise. + */ + bool setBondLabel(Index bondId, const std::string& label, + const QString& undoText = tr("Change Bond Label")); + /** * Set whether the specified atom is selected or not. */ @@ -740,6 +772,11 @@ inline std::string RWMolecule::atomLabel(Index atomId) const return m_molecule.atomLabel(atomId); } +inline std::string RWMolecule::bondLabel(Index bondId) const +{ + return m_molecule.bondLabel(bondId); +} + inline Core::AtomHybridization RWMolecule::hybridization(Index atomId) const { return m_molecule.hybridization(atomId); diff --git a/avogadro/qtgui/rwmolecule_undo.h b/avogadro/qtgui/rwmolecule_undo.h index 142c0cb36b..b07c577a58 100644 --- a/avogadro/qtgui/rwmolecule_undo.h +++ b/avogadro/qtgui/rwmolecule_undo.h @@ -686,6 +686,26 @@ class ModifyAtomLabelCommand : public RWMolecule::UndoCommand }; } // namespace +namespace { +class ModifyBondLabelCommand : public RWMolecule::UndoCommand +{ + Index m_bondId; + std::string m_newLabel; + std::string m_oldLabel; + +public: + ModifyBondLabelCommand(RWMolecule& m, Index bondId, const std::string& label) + : UndoCommand(m), m_bondId(bondId), m_newLabel(label) + { + m_oldLabel = m_mol.molecule().bondLabel(m_bondId); + } + + void redo() override { m_mol.molecule().setBondLabel(m_bondId, m_newLabel); } + + void undo() override { m_mol.molecule().setBondLabel(m_bondId, m_oldLabel); } +}; +} // namespace + namespace { class ModifySelectionCommand : public MergeUndoCommand { diff --git a/avogadro/qtplugins/label/label.cpp b/avogadro/qtplugins/label/label.cpp index 0ee38f9f51..57f59a7341 100644 --- a/avogadro/qtplugins/label/label.cpp +++ b/avogadro/qtplugins/label/label.cpp @@ -5,6 +5,7 @@ #include "label.h" +#include #include #include @@ -71,10 +72,12 @@ struct LayerLabel : Core::LayerData Custom = 4, Ordinal = 8, UniqueID = 16, - PartialCharge = 32 + PartialCharge = 32, + Length = 64 // for bonds obviously }; unsigned short atomOptions; unsigned short residueOptions; + unsigned short bondOptions; QWidget* widget; float radiusScalar; @@ -88,6 +91,8 @@ struct LayerLabel : Core::LayerData settings.value("label/atomoptions", LabelOptions::Name).toInt(); residueOptions = settings.value("label/residueoptions", LabelOptions::None).toInt(); + bondOptions = + settings.value("label/bondoptions", LabelOptions::None).toInt(); radiusScalar = settings.value("label/radiusscalar", 0.5).toDouble(); auto q_color = @@ -115,7 +120,8 @@ struct LayerLabel : Core::LayerData { std::stringstream output; output << atomOptions << " " << residueOptions << " " << radiusScalar << " " - << (int)color[0] << " " << (int)color[1] << " " << (int)color[2]; + << (int)color[0] << " " << (int)color[1] << " " << (int)color[2] + << " " << bondOptions; return output.str(); } @@ -135,6 +141,9 @@ struct LayerLabel : Core::LayerData color[1] = std::stoi(aux); ss >> aux; color[2] = std::stoi(aux); + ss >> aux; + if (!aux.empty()) + bondOptions = std::stoi(aux); // backwards compatibility } void setupWidget(Label* slot) @@ -193,6 +202,20 @@ struct LayerLabel : Core::LayerData form->addRow(QObject::tr("Atom Label:"), atom); + // bond label + auto* bond = new QComboBox; + bond->setObjectName("bond"); + + // set up the various bond options + bond->addItem(QObject::tr("None"), int(LabelOptions::None)); + bond->addItem(QObject::tr("Length"), int(LabelOptions::Length)); + bond->addItem(QObject::tr("Index"), int(LabelOptions::Index)); + bond->addItem(QObject::tr("Custom"), int(LabelOptions::Custom)); + + QObject::connect(bond, SIGNAL(currentIndexChanged(int)), slot, + SLOT(bondLabelType(int))); + form->addRow(QObject::tr("Bond Label:"), bond); + auto* residue = new QComboBox; residue->setObjectName("residue"); for (char i = 0x00; i < std::pow(2, 2); ++i) { @@ -249,6 +272,9 @@ void Label::process(const QtGui::Molecule& molecule, Rendering::GroupNode& node) if (interface->atomOptions) { processAtom(molecule, node, layer); } + if (interface->bondOptions) { + processBond(molecule, node, layer); + } } } @@ -389,6 +415,60 @@ void Label::processAtom(const Core::Molecule& molecule, } } +void Label::processBond(const Core::Molecule& molecule, + Rendering::GroupNode& node, size_t layer) +{ + auto* geometry = new GeometryNode; + node.addChild(geometry); + std::map bondCount; + for (Index i = 0; i < molecule.bondCount(); ++i) { + Core::Bond bond = molecule.bond(i); + + // check if the bond is enabled in this layer + if (!m_layerManager.bondEnabled(bond.atom1().index(), + bond.atom2().index())) { + continue; + } + + // get the options for this bond + Core::Atom atom1 = bond.atom1(); + Core::Atom atom2 = bond.atom2(); + auto* interface1 = m_layerManager.getSetting( + m_layerManager.getLayerID(atom1.index())); + auto* interface2 = m_layerManager.getSetting( + m_layerManager.getLayerID(atom2.index())); + + // get the union of the options + char options = interface1->bondOptions | interface2->bondOptions; + Vector3ub color = interface1->color; + unsigned char atomicNumber1 = atom1.atomicNumber(); + unsigned char atomicNumber2 = atom2.atomicNumber(); + float radiusVDW1 = static_cast(Elements::radiusVDW(atomicNumber1)); + float radiusVDW2 = static_cast(Elements::radiusVDW(atomicNumber2)); + float radius = (radiusVDW1 + radiusVDW2) * interface1->radiusScalar / 2.0f; + + std::stringstream text; + // hopefully not all at once + if (options & LayerLabel::LabelOptions::Index) { + text << bond.index(); + } + if (options & LayerLabel::LabelOptions::Custom) { + text << bond.label(); + } + if (options & LayerLabel::LabelOptions::Length) { + text << std::fixed << std::setprecision(2) << bond.length(); + } + + // position will be between the two atoms + Vector3f pos = + (atom1.position3d().cast() + atom2.position3d().cast()) / + 2.0f; + + TextLabel3D* bondLabel = createLabel(text.str(), pos, radius, color); + geometry->addDrawable(bondLabel); + } +} + void Label::setColor(const QColor& color) { auto* interface = m_layerManager.getSetting(); @@ -413,6 +493,16 @@ void Label::atomLabelType(int index) emit drawablesChanged(); } +void Label::bondLabelType(int index) +{ + auto* interface = m_layerManager.getSetting(); + interface->bondOptions = char(setupWidget() + ->findChildren("bond")[0] + ->itemData(index) + .toInt()); + emit drawablesChanged(); +} + void Label::residueLabelType(int index) { auto* interface = m_layerManager.getSetting(); diff --git a/avogadro/qtplugins/label/label.h b/avogadro/qtplugins/label/label.h index a273da4e7d..68577fd002 100644 --- a/avogadro/qtplugins/label/label.h +++ b/avogadro/qtplugins/label/label.h @@ -41,6 +41,7 @@ class Label : public QtGui::ScenePlugin } public slots: void atomLabelType(int index); + void bondLabelType(int index); void residueLabelType(int index); void setRadiusScalar(double radius); void setColor(const QColor& color); @@ -48,6 +49,8 @@ public slots: private: void processAtom(const Core::Molecule& molecule, Rendering::GroupNode& node, size_t layer); + void processBond(const Core::Molecule& molecule, Rendering::GroupNode& node, + size_t layer); void processResidue(const Core::Molecule& molecule, Rendering::GroupNode& node, size_t layer); diff --git a/avogadro/qtplugins/propertytables/propertymodel.cpp b/avogadro/qtplugins/propertytables/propertymodel.cpp index d26298f29e..e19ea0dccf 100644 --- a/avogadro/qtplugins/propertytables/propertymodel.cpp +++ b/avogadro/qtplugins/propertytables/propertymodel.cpp @@ -37,10 +37,10 @@ using std::vector; using SecondaryStructure = Avogadro::Core::Residue::SecondaryStructure; -// element, valence, formal charge, partial charge, x, y, z, color -const int AtomColumns = 8; -// type, atom 1, atom 2, bond order, length -const int BondColumns = 5; +// element, valence, formal charge, partial charge, x, y, z, label, color +const int AtomColumns = 9; +// type, atom 1, atom 2, bond order, length, label +const int BondColumns = 6; // type, atom 1, atom 2, atom 3, angle const int AngleColumns = 5; // type, atom 1, atom 2, atom 3, atom 4, dihedral @@ -313,6 +313,8 @@ QVariant PropertyModel::data(const QModelIndex& index, int role) const case AtomDataZ: return QString("%L1").arg(m_molecule->atomPosition3d(row).z(), 0, 'f', 4); + case AtomDataLabel: + return m_molecule->atomLabel(row).c_str(); case AtomDataColor: default: return QVariant(); // nothing to show @@ -341,6 +343,8 @@ QVariant PropertyModel::data(const QModelIndex& index, int role) const return QVariant::fromValue(atom2.index() + 1); case BondDataOrder: return bond.order(); + case BondDataLabel: + return m_molecule->bondLabel(row).c_str(); default: // length, rounded to 4 decimals return QString("%L1 Å").arg( distance(atom1.position3d(), atom2.position3d()), 0, 'f', 3); @@ -509,6 +513,8 @@ QVariant PropertyModel::headerData(int section, Qt::Orientation orientation, return tr("Y (Å)"); case AtomDataZ: return tr("Z (Å)"); + case AtomDataLabel: + return tr("Label"); case AtomDataColor: return tr("Color"); } @@ -527,6 +533,8 @@ QVariant PropertyModel::headerData(int section, Qt::Orientation orientation, return tr("End Atom"); case BondDataOrder: return tr("Bond Order"); + case BondDataLabel: + return tr("Label"); default: // A bond length return tr("Length (Å)", "in Angstrom"); } @@ -620,11 +628,13 @@ Qt::ItemFlags PropertyModel::flags(const QModelIndex& index) const if (m_type == AtomType) { if (index.column() == AtomDataElement || index.column() == AtomDataFormalCharge || index.column() == AtomDataX || - index.column() == AtomDataY || index.column() == AtomDataZ) + index.column() == AtomDataY || index.column() == AtomDataZ || + index.column() == AtomDataLabel) return editable; // TODO: Color } else if (m_type == BondType) { - if (index.column() == BondDataOrder || index.column() == BondDataLength) + if (index.column() == BondDataOrder || index.column() == BondDataLength || + index.column() == BondDataLabel) return editable; } else if (m_type == ResidueType) { // TODO: Color @@ -693,6 +703,9 @@ bool PropertyModel::setData(const QModelIndex& index, const QVariant& value, case AtomDataZ: v[2] = value.toDouble(); break; + case AtomDataLabel: + undoMolecule->setAtomLabel(index.row(), value.toString().toStdString()); + break; default: return false; } @@ -710,6 +723,9 @@ bool PropertyModel::setData(const QModelIndex& index, const QVariant& value, case BondDataLength: setBondLength(index.row(), value.toDouble()); break; + case BondDataLabel: + undoMolecule->setBondLabel(index.row(), value.toString().toStdString()); + break; default: return false; } diff --git a/avogadro/qtplugins/propertytables/propertymodel.h b/avogadro/qtplugins/propertytables/propertymodel.h index 2092fdc941..7733a13684 100644 --- a/avogadro/qtplugins/propertytables/propertymodel.h +++ b/avogadro/qtplugins/propertytables/propertymodel.h @@ -112,6 +112,7 @@ public slots: AtomDataX, AtomDataY, AtomDataZ, + AtomDataLabel, AtomDataColor, AtomDataCharge, AtomDataCustom, @@ -124,7 +125,8 @@ public slots: BondDataAtom1, BondDataAtom2, BondDataOrder, - BondDataLength + BondDataLength, + BondDataLabel }; // Angle Data