1 /******************************************************************************
2 
3   This source file is part of the Avogadro project.
4 
5   Copyright 2013 Kitware, Inc.
6 
7   Adapted from Avogadro 1.x with the following authors' permission:
8   Copyright (C) 2007 by Shahzad Ali
9   Copyright (C) 2007 by Ross Braithwaite
10   Copyright (C) 2007 by James Bunt
11   Copyright (C) 2007,2008 by Marcus D. Hanwell
12   Copyright (C) 2006,2007 by Benoit Jacob
13 
14   This source code is released under the New BSD License, (the "License").
15 
16   Unless required by applicable law or agreed to in writing, software
17   distributed under the License is distributed on an "AS IS" BASIS,
18   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19   See the License for the specific language governing permissions and
20   limitations under the License.
21 
22 ******************************************************************************/
23 
24 #include "bondcentrictool.h"
25 
26 #include <avogadro/qtopengl/glwidget.h>
27 
28 #include <avogadro/rendering/geometrynode.h>
29 #include <avogadro/rendering/glrenderer.h>
30 #include <avogadro/rendering/groupnode.h>
31 #include <avogadro/rendering/linestripgeometry.h>
32 #include <avogadro/rendering/meshgeometry.h>
33 #include <avogadro/rendering/textlabel3d.h>
34 #include <avogadro/rendering/textproperties.h>
35 
36 #include <avogadro/core/array.h>
37 #include <avogadro/core/atom.h>
38 #include <avogadro/core/elements.h>
39 #include <avogadro/core/vector.h>
40 #include <avogadro/qtgui/molecule.h>
41 #include <avogadro/qtgui/rwmolecule.h>
42 
43 #include <QtGui/QIcon>
44 #include <QtGui/QMouseEvent>
45 #include <QtWidgets/QAction>
46 
47 #include <Eigen/Geometry>
48 
49 #include <cmath>
50 
51 #ifndef M_PI
52 #define M_PI 3.14159265358979323846
53 #endif
54 
55 namespace Avogadro {
56 namespace QtPlugins {
57 
58 using Core::Array;
59 using Core::Elements;
60 using QtGui::Molecule;
61 using QtGui::RWAtom;
62 using QtGui::RWBond;
63 using QtGui::RWMolecule;
64 using Rendering::GeometryNode;
65 using Rendering::GroupNode;
66 using Rendering::Identifier;
67 using Rendering::LineStripGeometry;
68 using Rendering::MeshGeometry;
69 
70 namespace {
71 const std::string degreeString("°");
72 /// @todo Add wide character support to text renderer.
73 const std::string angstromString("Å");
74 
75 // Lookup for coloring bond angles:
getColor(size_t i)76 const Vector3ub& getColor(size_t i)
77 {
78   static std::vector<Vector3ub> colors;
79   if (colors.empty()) {
80     colors.push_back(Vector3ub(255, 64, 32));
81     colors.push_back(Vector3ub(64, 255, 32));
82     colors.push_back(Vector3ub(32, 64, 255));
83     colors.push_back(Vector3ub(255, 255, 32));
84     colors.push_back(Vector3ub(255, 32, 255));
85     colors.push_back(Vector3ub(32, 255, 255));
86     colors.push_back(Vector3ub(255, 128, 0));
87     colors.push_back(Vector3ub(128, 255, 0));
88     colors.push_back(Vector3ub(0, 255, 128));
89     colors.push_back(Vector3ub(0, 128, 255));
90     colors.push_back(Vector3ub(255, 0, 128));
91     colors.push_back(Vector3ub(128, 0, 255));
92   }
93 
94   return colors[i % colors.size()];
95 }
96 
97 // Returns unsigned, smallest angle between v1 and v2
vectorAngleDegrees(const Vector3f & v1,const Vector3f & v2)98 inline float vectorAngleDegrees(const Vector3f& v1, const Vector3f& v2)
99 {
100   const float crossProductNorm(v1.cross(v2).norm());
101   const float dotProduct(v1.dot(v2));
102   return std::atan2(crossProductNorm, dotProduct) * RAD_TO_DEG_F;
103 }
104 
105 // Returns signed, smallest angle between v1 and v2. Sign is determined from a
106 // right hand rule around axis.
vectorAngleDegrees(const Vector3f & v1,const Vector3f & v2,const Vector3f & axis)107 inline float vectorAngleDegrees(const Vector3f& v1, const Vector3f& v2,
108                                 const Vector3f& axis)
109 {
110   const Vector3f crossProduct(v1.cross(v2));
111   const float crossProductNorm(crossProduct.norm());
112   const float dotProduct(v1.dot(v2));
113   const float signDet(crossProduct.dot(axis));
114   const float angle(std::atan2(crossProductNorm, dotProduct) * RAD_TO_DEG_F);
115   return signDet > 0.f ? angle : -angle;
116 }
117 
118 // Convenience quad drawable:
119 class Quad : public MeshGeometry
120 {
121 public:
Quad()122   Quad() {}
~Quad()123   ~Quad() override {}
124 
125   /**
126    * @brief setQuad Set the four corners of the quad.
127    */
128   void setQuad(const Vector3f& topLeft, const Vector3f& topRight,
129                const Vector3f& bottomLeft, const Vector3f& bottomRight);
130 };
131 
setQuad(const Vector3f & topLeft,const Vector3f & topRight,const Vector3f & bottomLeft,const Vector3f & bottomRight)132 void Quad::setQuad(const Vector3f& topLeft, const Vector3f& topRight,
133                    const Vector3f& bottomLeft, const Vector3f& bottomRight)
134 {
135   const Vector3f bottom = bottomRight - bottomLeft;
136   const Vector3f left = topLeft - bottomLeft;
137   const Vector3f normal = bottom.cross(left).normalized();
138   Array<Vector3f> norms(4, normal);
139 
140   Array<Vector3f> verts(4);
141   verts[0] = topLeft;
142   verts[1] = topRight;
143   verts[2] = bottomLeft;
144   verts[3] = bottomRight;
145 
146   Array<unsigned int> indices(6);
147   indices[0] = 0;
148   indices[1] = 1;
149   indices[2] = 2;
150   indices[3] = 2;
151   indices[4] = 1;
152   indices[5] = 3;
153 
154   clear();
155   addVertices(verts, norms);
156   addTriangles(indices);
157 }
158 
159 // Convenience arc sector drawable:
160 class ArcSector : public MeshGeometry
161 {
162 public:
ArcSector()163   ArcSector() {}
~ArcSector()164   ~ArcSector() override {}
165 
166   /**
167    * Define the sector.
168    * @param origin Center of the circle from which the arc is cut.
169    * @param startEdge A vector defining an leading edge of the sector. The
170    * direction is used to fix the sector's rotation about the origin, and the
171    * length defines the radius of the sector.
172    * @param normal The normal direction to the plane of the sector.
173    * @param degreesCCW The extent of the sector, measured counter-clockwise from
174    * startEdge in degrees.
175    * @param resolutionDeg The radial width of each triangle used in the sector
176    * approximation in degrees. This will be adjusted to fit an integral number
177    * of triangles in the sector. Smaller triangles (better approximations) are
178    * chosen if adjustment is needed.
179    */
180   void setArcSector(const Vector3f& origin, const Vector3f& startEdge,
181                     const Vector3f& normal, float degreesCCW,
182                     float resolutionDeg);
183 };
184 
setArcSector(const Vector3f & origin,const Vector3f & startEdge,const Vector3f & normal,float degreesCCW,float resolutionDeg)185 void ArcSector::setArcSector(const Vector3f& origin, const Vector3f& startEdge,
186                              const Vector3f& normal, float degreesCCW,
187                              float resolutionDeg)
188 {
189   // Prepare rotation, calculate sizes
190   const unsigned int numTriangles =
191     static_cast<unsigned int>(std::fabs(std::ceil(degreesCCW / resolutionDeg)));
192   const size_t numVerts = static_cast<size_t>(numTriangles + 2);
193   const float stepAngleRads =
194     (degreesCCW / static_cast<float>(numTriangles)) * DEG_TO_RAD_F;
195   const Eigen::AngleAxisf rot(stepAngleRads, normal);
196 
197   // Generate normal array
198   Array<Vector3f> norms(numVerts, normal);
199 
200   // Generate vertices
201   Array<Vector3f> verts(numVerts);
202   Array<Vector3f>::iterator vertsInserter(verts.begin());
203   Array<Vector3f>::iterator vertsEnd(verts.end());
204   Vector3f radial = startEdge;
205   *(vertsInserter++) = origin;
206   *(vertsInserter++) = origin + radial;
207   while (vertsInserter != vertsEnd)
208     *(vertsInserter++) = origin + (radial = rot * radial);
209 
210   // Generate indices
211   Array<unsigned int> indices(numTriangles * 3);
212   Array<unsigned int>::iterator indexInserter(indices.begin());
213   Array<unsigned int>::iterator indexEnd(indices.end());
214   for (unsigned int i = 1; indexInserter != indexEnd; ++i) {
215     *(indexInserter++) = 0;
216     *(indexInserter++) = i;
217     *(indexInserter++) = i + 1;
218   }
219 
220   clear();
221   addVertices(verts, norms);
222   addTriangles(indices);
223 }
224 
225 // Convenience quad outline drawable:
226 class QuadOutline : public LineStripGeometry
227 {
228 public:
QuadOutline()229   QuadOutline() {}
~QuadOutline()230   ~QuadOutline() override {}
231 
232   /**
233    * @brief setQuad Set the four corners of the quad.
234    */
235   void setQuad(const Vector3f& topLeft, const Vector3f& topRight,
236                const Vector3f& bottomLeft, const Vector3f& bottomRight,
237                float lineWidth);
238 };
239 
setQuad(const Vector3f & topLeft,const Vector3f & topRight,const Vector3f & bottomLeft,const Vector3f & bottomRight,float lineWidth)240 void QuadOutline::setQuad(const Vector3f& topLeft, const Vector3f& topRight,
241                           const Vector3f& bottomLeft,
242                           const Vector3f& bottomRight, float lineWidth)
243 {
244   Array<Vector3f> verts(5);
245   verts[0] = topLeft;
246   verts[1] = topRight;
247   verts[2] = bottomRight;
248   verts[3] = bottomLeft;
249   verts[4] = topLeft;
250 
251   clear();
252   addLineStrip(verts, lineWidth);
253 }
254 
255 // Convenience arc drawable:
256 class ArcStrip : public LineStripGeometry
257 {
258 public:
ArcStrip()259   ArcStrip() {}
~ArcStrip()260   ~ArcStrip() override {}
261 
262   /**
263    * Define the arc.
264    * @param origin Center of the circle from which the arc is cut.
265    * @param start A vector pointing from the origin to the start of the arc.
266    * @param normal The normal direction to the plane of the circle.
267    * @param degreesCCW The extent of the arc, measured counter-clockwise from
268    * start in degrees.
269    * @param resolutionDeg The radial width of each segment used in the arc
270    * approximation, in degrees. This will be adjusted to fit an integral number
271    * of segments into the arc. Smaller segments (better approximations) are
272    * chosen if adjustment is needed.
273    * @param lineWidth The width of the line.
274    */
275   void setArc(const Vector3f& origin, const Vector3f& start,
276               const Vector3f& normal, float degreesCCW, float resolutionDeg,
277               float lineWidth);
278 };
279 
setArc(const Vector3f & origin,const Vector3f & start,const Vector3f & normal,float degreesCCW,float resolutionDeg,float lineWidth)280 void ArcStrip::setArc(const Vector3f& origin, const Vector3f& start,
281                       const Vector3f& normal, float degreesCCW,
282                       float resolutionDeg, float lineWidth)
283 {
284   // Prepare rotation, calculate sizes
285   const unsigned int resolution =
286     static_cast<unsigned int>(std::fabs(std::ceil(degreesCCW / resolutionDeg)));
287   const size_t numVerts = static_cast<size_t>(resolution + 1);
288   const float stepAngleRads =
289     (degreesCCW / static_cast<float>(resolution)) * DEG_TO_RAD_F;
290   const Eigen::AngleAxisf rot(stepAngleRads, normal);
291 
292   // Generate vertices
293   Array<Vector3f> verts(numVerts);
294   Array<Vector3f>::iterator vertsInserter(verts.begin());
295   Array<Vector3f>::iterator vertsEnd(verts.end());
296   Vector3f radial = start;
297   *(vertsInserter++) = origin + radial;
298   while (vertsInserter != vertsEnd)
299     *(vertsInserter++) = origin + (radial = rot * radial);
300 
301   clear();
302   addLineStrip(verts, lineWidth);
303 }
304 
305 } // end anon namespace
306 
BondCentricTool(QObject * parent_)307 BondCentricTool::BondCentricTool(QObject* parent_)
308   : QtGui::ToolPlugin(parent_)
309   , m_activateAction(new QAction(this))
310   , m_molecule(nullptr)
311   , m_renderer(nullptr)
312   , m_moveState(IgnoreMove)
313   , m_planeSnapIncr(10.f)
314   , m_snapPlaneToBonds(true)
315 {
316   m_activateAction->setText(tr("Bond-centric manipulation"));
317   m_activateAction->setIcon(QIcon(":/icons/bondcentrictool.png"));
318 }
319 
~BondCentricTool()320 BondCentricTool::~BondCentricTool() {}
321 
toolWidget() const322 QWidget* BondCentricTool::toolWidget() const
323 {
324   return nullptr;
325 }
326 
setMolecule(QtGui::Molecule * mol)327 void BondCentricTool::setMolecule(QtGui::Molecule* mol)
328 {
329   if (mol && mol->undoMolecule() != m_molecule) {
330     m_molecule = mol->undoMolecule();
331     reset();
332   }
333 }
334 
setEditMolecule(QtGui::RWMolecule * mol)335 void BondCentricTool::setEditMolecule(QtGui::RWMolecule* mol)
336 {
337   if (m_molecule != mol) {
338     m_molecule = mol;
339     reset();
340   }
341 }
342 
setGLWidget(QtOpenGL::GLWidget *)343 void BondCentricTool::setGLWidget(QtOpenGL::GLWidget*) {}
344 
setGLRenderer(Rendering::GLRenderer * ren)345 void BondCentricTool::setGLRenderer(Rendering::GLRenderer* ren)
346 {
347   m_renderer = ren;
348 }
349 
mousePressEvent(QMouseEvent * e)350 QUndoCommand* BondCentricTool::mousePressEvent(QMouseEvent* e)
351 {
352   // Don't start a new operation if we're already working:
353   if (m_moveState != IgnoreMove)
354     return nullptr;
355 
356   Rendering::Identifier ident = m_renderer->hit(e->pos().x(), e->pos().y());
357 
358   // If no hits, return. Also ensure that the hit molecule is the one we expect.
359   const Core::Molecule* mol = &m_molecule->molecule();
360   if (!ident.isValid() || ident.molecule != mol)
361     return nullptr;
362 
363   // If the hit is a left click on a bond, make it the selected bond and map
364   // mouse movements to the bond plane rotation.
365   if (ident.type == Rendering::BondType && e->button() == Qt::LeftButton)
366     return initRotatePlane(e, ident);
367 
368   // Return if selectedBond is not valid or the hit is not on a bond:
369   if (!m_selectedBond.isValid() || ident.type != Rendering::AtomType)
370     return nullptr;
371 
372   // Test if the atom is in the selected bond, or one bond removed.
373   RWAtom clickedAtom = m_molecule->atom(ident.index);
374   RWBond selectedBond = m_selectedBond.bond();
375   bool atomIsInBond = bondContainsAtom(selectedBond, clickedAtom);
376   bool atomIsNearBond = false;
377   RWAtom anchorAtom;
378   if (!atomIsInBond) {
379     Array<RWBond> bonds = m_molecule->bonds(clickedAtom);
380     for (Array<RWBond>::const_iterator it = bonds.begin(), itEnd = bonds.end();
381          it != itEnd; ++it) {
382       RWAtom atom = otherBondedAtom(*it, clickedAtom);
383       if (bondContainsAtom(selectedBond, atom)) {
384         anchorAtom = atom;
385         atomIsNearBond = true;
386         break;
387       }
388     }
389   }
390 
391   if (!atomIsInBond && !atomIsNearBond)
392     return nullptr;
393 
394   if (m_molecule) {
395     m_molecule->setInteractive(true);
396   }
397 
398   // If the hit is a left click on an atom in the selected bond, prepare to
399   // rotate the clicked bond around the other atom in the bond.
400   if (atomIsInBond && e->button() == Qt::LeftButton)
401     return initRotateBondedAtom(e, clickedAtom);
402 
403   // If the hit is a right click on an atom in the selected bond, prepare to
404   // change the bond length.
405   if (atomIsInBond && e->button() == Qt::RightButton)
406     return initAdjustBondLength(e, clickedAtom);
407 
408   // Is the hit a left click on an atom bonded to an atom in selectedBond?
409   if (atomIsNearBond &&
410       (e->button() == Qt::LeftButton || e->button() == Qt::RightButton)) {
411     return initRotateNeighborAtom(e, clickedAtom, anchorAtom);
412   }
413 
414   return nullptr;
415 }
416 
mouseDoubleClickEvent(QMouseEvent * e)417 QUndoCommand* BondCentricTool::mouseDoubleClickEvent(QMouseEvent* e)
418 {
419   if (m_selectedBond.isValid() && e->button() == Qt::LeftButton) {
420     reset();
421     emit drawablesChanged();
422   }
423   return nullptr;
424 }
425 
mouseMoveEvent(QMouseEvent * e)426 QUndoCommand* BondCentricTool::mouseMoveEvent(QMouseEvent* e)
427 {
428   if (m_moveState == IgnoreMove)
429     return nullptr;
430 
431   QUndoCommand* result = nullptr;
432 
433   switch (m_moveState) {
434     case RotatePlane:
435       result = rotatePlane(e);
436       break;
437     case RotateBondedAtom:
438       result = rotateBondedAtom(e);
439       break;
440     case AdjustBondLength:
441       result = adjustBondLength(e);
442       break;
443     case RotateNeighborAtom:
444       result = rotateNeighborAtom(e);
445       break;
446     default:
447       break;
448   }
449 
450   return result;
451 }
452 
mouseReleaseEvent(QMouseEvent *)453 QUndoCommand* BondCentricTool::mouseReleaseEvent(QMouseEvent*)
454 {
455   if (m_moveState != IgnoreMove) {
456     reset(KeepBond);
457     emit drawablesChanged();
458 
459     if (m_molecule) {
460       m_molecule->setInteractive(false); // allow an undo now
461     }
462   }
463 
464   return nullptr;
465 }
466 
draw(Rendering::GroupNode & node)467 void BondCentricTool::draw(Rendering::GroupNode& node)
468 {
469   RWBond selectedBond = m_selectedBond.bond();
470 
471   if (!selectedBond.isValid())
472     return;
473 
474   GeometryNode* geo = new GeometryNode;
475   node.addChild(geo);
476 
477   switch (m_moveState) {
478     default:
479     case IgnoreMove:
480     case RotatePlane:
481       drawBondQuad(*geo, selectedBond);
482       drawAtomBondAngles(*geo, selectedBond.atom1(), selectedBond);
483       drawAtomBondAngles(*geo, selectedBond.atom2(), selectedBond);
484       break;
485 
486     case RotateBondedAtom: {
487       drawBondQuad(*geo, selectedBond);
488 
489       RWAtom otherAtom = otherBondedAtom(selectedBond, m_clickedAtom.atom());
490       if (otherAtom.isValid()) {
491         drawAtomBondAngles(*geo, otherAtom, selectedBond);
492       }
493 
494       break;
495     }
496 
497     case AdjustBondLength:
498       drawBondQuad(*geo, selectedBond);
499       drawBondLengthLabel(*geo, selectedBond);
500       break;
501 
502     case RotateNeighborAtom: {
503       RWAtom clickedAtom = m_clickedAtom.atom();
504       RWAtom anchorAtom = m_anchorAtom.atom();
505       RWBond otherBond = m_molecule->bond(clickedAtom, anchorAtom);
506       if (otherBond.isValid())
507         drawBondAngle(*geo, selectedBond, otherBond);
508       break;
509     }
510   }
511 }
512 
reset(BondCentricTool::ResetBondBehavior bond)513 void BondCentricTool::reset(BondCentricTool::ResetBondBehavior bond)
514 {
515   if (bond == ResetBond)
516     m_selectedBond.reset();
517 
518   m_clickedAtom.reset();
519   m_anchorAtom.reset();
520   m_moveState = IgnoreMove;
521   m_clickedPoint = QPoint();
522 }
523 
initializeBondVectors()524 void BondCentricTool::initializeBondVectors()
525 {
526   RWBond bond = m_selectedBond.bond();
527   if (bond.isValid()) {
528     m_bondVector = (bond.atom2().position3d().cast<float>() -
529                     bond.atom1().position3d().cast<float>())
530                      .normalized();
531     m_planeNormalMouse = m_bondVector.unitOrthogonal();
532   }
533 }
534 
updateBondVector()535 void BondCentricTool::updateBondVector()
536 {
537   RWBond bond = m_selectedBond.bond();
538   if (bond.isValid()) {
539     m_bondVector = (bond.atom2().position3d().cast<float>() -
540                     bond.atom1().position3d().cast<float>())
541                      .normalized();
542   }
543 }
544 
initRotatePlane(QMouseEvent * e,const Rendering::Identifier & ident)545 QUndoCommand* BondCentricTool::initRotatePlane(
546   QMouseEvent* e, const Rendering::Identifier& ident)
547 {
548   RWBond selectedBond = m_molecule->bond(ident.index);
549   // Get unique id:
550   Index bondUniqueId = m_molecule->bondUniqueId(selectedBond);
551   if (bondUniqueId == MaxIndex)
552     return nullptr; // Something went horribly wrong.
553 
554   // Reset the bond vector/plane normal if the bond changed
555   if (bondUniqueId != m_selectedBond.uniqueIdentifier()) {
556     m_selectedBond =
557       QtGui::RWMolecule::PersistentBondType(m_molecule, bondUniqueId);
558     initializeBondVectors();
559   }
560   updatePlaneSnapAngles();
561   updateSnappedPlaneNormal();
562   if (!m_selectedBond.isValid())
563     return nullptr;
564   e->accept();
565   m_moveState = RotatePlane;
566   m_clickedPoint = e->pos();
567   m_lastDragPoint = e->pos();
568   emit drawablesChanged();
569   return nullptr;
570 }
571 
initRotateBondedAtom(QMouseEvent * e,const QtGui::RWAtom & clickedAtom)572 QUndoCommand* BondCentricTool::initRotateBondedAtom(
573   QMouseEvent* e, const QtGui::RWAtom& clickedAtom)
574 {
575   m_clickedAtom = RWMolecule::PersistentAtomType(clickedAtom);
576   if (!m_clickedAtom.isValid())
577     return nullptr;
578   e->accept();
579   m_moveState = RotateBondedAtom;
580   m_clickedPoint = e->pos();
581   m_lastDragPoint = e->pos();
582   resetFragment();
583   emit drawablesChanged();
584   return nullptr;
585 }
586 
initAdjustBondLength(QMouseEvent * e,const QtGui::RWAtom & clickedAtom)587 QUndoCommand* BondCentricTool::initAdjustBondLength(
588   QMouseEvent* e, const QtGui::RWAtom& clickedAtom)
589 {
590   m_clickedAtom = RWMolecule::PersistentAtomType(clickedAtom);
591   if (!m_clickedAtom.isValid())
592     return nullptr;
593   e->accept();
594   m_moveState = AdjustBondLength;
595   m_clickedPoint = e->pos();
596   m_lastDragPoint = e->pos();
597   resetFragment();
598   emit drawablesChanged();
599   return nullptr;
600 }
601 
initRotateNeighborAtom(QMouseEvent * e,const QtGui::RWAtom & clickedAtom,const QtGui::RWAtom & anchorAtom)602 QUndoCommand* BondCentricTool::initRotateNeighborAtom(
603   QMouseEvent* e, const QtGui::RWAtom& clickedAtom,
604   const QtGui::RWAtom& anchorAtom)
605 {
606   m_clickedAtom = RWMolecule::PersistentAtomType(clickedAtom);
607   m_anchorAtom = RWMolecule::PersistentAtomType(anchorAtom);
608   if (!m_clickedAtom.isValid() || !m_anchorAtom.isValid())
609     return nullptr;
610   e->accept();
611   m_moveState = RotateNeighborAtom;
612   m_clickedPoint = e->pos();
613   m_lastDragPoint = e->pos();
614   resetFragment();
615   emit drawablesChanged();
616   return nullptr;
617 }
618 
rotatePlane(QMouseEvent * e)619 QUndoCommand* BondCentricTool::rotatePlane(QMouseEvent* e)
620 {
621   // The bond should be valid.
622   const RWBond selectedBond = m_selectedBond.bond();
623   if (!selectedBond.isValid())
624     return nullptr;
625 
626   const QPoint deltaDrag = e->pos() - m_lastDragPoint;
627   const Rendering::Camera& camera(m_renderer->camera());
628 
629   // Atomic position in world coordinates
630   const Vector3 beginPos(selectedBond.atom1().position3d());
631   const Vector3 endPos(selectedBond.atom2().position3d());
632 
633   // Various quantities in window coordinates.
634   const Vector3f beginWin(camera.project(beginPos.cast<float>()));
635   const Vector3f endWin(camera.project(endPos.cast<float>()));
636   Vector3f bondVecWin(endWin - beginWin);
637   bondVecWin.z() = 0.f;
638   // Points into the viewing volume from camera:
639   const Vector3f zAxisWin(0.f, 0.f, 1.f);
640   // In plane of screen, orthogonal to bond:
641   const Vector3f orthoWin(zAxisWin.cross(bondVecWin).normalized());
642   const Vector3f dragWin(static_cast<float>(deltaDrag.x()),
643                          static_cast<float>(deltaDrag.y()), 0.f);
644 
645   // Compute the rotation. Not quite sure what's going on here, this is just
646   // ported from Avogadro 1. It doesn't seem right that rotation would be in
647   // degrees (it's the result of a dot product) and I think the fact that the
648   // DEG_TO_RAD conversion results in a useful angle is just a happy
649   // coincidence. But it works quite well.
650   const float rotation = dragWin.dot(orthoWin) / orthoWin.norm();
651   const Eigen::AngleAxisf rotator(rotation * DEG_TO_RAD_F, m_bondVector);
652 
653   // Rotate
654   m_planeNormalMouse = rotator * m_planeNormalMouse;
655   updateSnappedPlaneNormal();
656   emit drawablesChanged();
657 
658   m_lastDragPoint = e->pos();
659   return nullptr;
660 }
661 
rotateBondedAtom(QMouseEvent * e)662 QUndoCommand* BondCentricTool::rotateBondedAtom(QMouseEvent* e)
663 {
664   // Ensure that the mouse has moved a reasonable amount:
665   if ((m_lastDragPoint - e->pos()).manhattanLength() < 2)
666     return nullptr;
667 
668   RWBond bond = m_selectedBond.bond();
669   RWAtom clickedAtom = m_clickedAtom.atom();
670   RWAtom centerAtom = otherBondedAtom(bond, clickedAtom);
671 
672   // Sanity check:
673   if (!bond.isValid() || !clickedAtom.isValid() || !centerAtom.isValid())
674     return nullptr;
675 
676   // Compute the transformation:
677   //   - Rotation axis is m_planeNormal
678   //   - Rotation angle is:
679   //       - magnitude is angle between initial click and current pos around
680   //         center atom (performed in 2D).
681   //       - sign is based on whether m_planeNormal is pointing into/out of the
682   //         screen.
683   const Rendering::Camera& camera(m_renderer->camera());
684 
685   // Get the window coordinates of the relevant points
686   const Vector3f centerPos(centerAtom.position3d().cast<float>());
687   const Vector3f centerWin(camera.project(centerPos));
688   const Vector2f centerWin2(centerWin.head<2>());
689   const Vector2f lastDragWin(
690     static_cast<float>(m_lastDragPoint.x()),
691     static_cast<float>(camera.height() - m_lastDragPoint.y()));
692   const Vector2f dragWin(static_cast<float>(e->pos().x()),
693                          static_cast<float>(camera.height() - e->pos().y()));
694 
695   // Compute the angle between last drag and current drag positions
696   const Vector2f lastDragWinVec((lastDragWin - centerWin2).normalized());
697   const Vector2f dragWinVec((dragWin - centerWin2).normalized());
698   const float crossProductNorm(lastDragWinVec.x() * dragWinVec.y() -
699                                lastDragWinVec.y() * dragWinVec.x());
700   const float dotProduct(lastDragWinVec.dot(dragWinVec));
701   const float angle(std::atan2(crossProductNorm, dotProduct));
702 
703   // Figure out if the sign needs to be reversed:
704   const Vector3f centerPlusNormal(centerPos + m_planeNormal);
705   const Vector3f centerPlusNormalWin(camera.project(centerPlusNormal));
706   bool reverseSign = (centerPlusNormalWin.z() - centerWin.z()) >= 0;
707 
708   // Build transform
709   m_transform.setIdentity();
710   m_transform.translate(centerPos);
711   m_transform.rotate(
712     Eigen::AngleAxisf(reverseSign ? -angle : angle, m_planeNormal));
713   m_transform.translate(-centerPos);
714 
715   // Build the fragment if needed:
716   if (m_fragment.empty())
717     buildFragment(bond, clickedAtom);
718 
719   // Perform transformation
720   transformFragment();
721   updateBondVector();
722   m_molecule->emitChanged(Molecule::Modified | Molecule::Atoms);
723   emit drawablesChanged();
724 
725   m_lastDragPoint = e->pos();
726   return nullptr;
727 }
728 
adjustBondLength(QMouseEvent * e)729 QUndoCommand* BondCentricTool::adjustBondLength(QMouseEvent* e)
730 {
731   // Ensure that the mouse has moved a reasonable amount:
732   if ((m_lastDragPoint - e->pos()).manhattanLength() < 2)
733     return nullptr;
734 
735   RWBond selectedBond = m_selectedBond.bond();
736   RWAtom clickedAtom = m_clickedAtom.atom();
737 
738   // Sanity check:
739   if (!selectedBond.isValid() || !clickedAtom.isValid())
740     return nullptr;
741 
742   const Rendering::Camera& camera(m_renderer->camera());
743   RWAtom otherAtom = otherBondedAtom(selectedBond, clickedAtom);
744 
745   const Vector2f curPosWin(static_cast<float>(e->pos().x()),
746                            static_cast<float>(e->pos().y()));
747   const Vector2f lastPosWin(static_cast<float>(m_lastDragPoint.x()),
748                             static_cast<float>(m_lastDragPoint.y()));
749 
750   const Vector3f bond(clickedAtom.position3d().cast<float>() -
751                       otherAtom.position3d().cast<float>());
752   const Vector3f mouse(camera.unProject(curPosWin) -
753                        camera.unProject(lastPosWin));
754 
755   const Vector3f displacement((mouse.dot(bond) / bond.squaredNorm()) * bond);
756 
757   // Build transform
758   m_transform.setIdentity();
759   m_transform.translate(displacement);
760 
761   // Build the fragment if needed:
762   if (m_fragment.empty())
763     buildFragment(selectedBond, clickedAtom);
764 
765   // Perform transformation
766   transformFragment();
767   m_molecule->emitChanged(QtGui::Molecule::Modified | QtGui::Molecule::Atoms);
768   emit drawablesChanged();
769 
770   m_lastDragPoint = e->pos();
771   return nullptr;
772 }
773 
rotateNeighborAtom(QMouseEvent * e)774 QUndoCommand* BondCentricTool::rotateNeighborAtom(QMouseEvent* e)
775 {
776   // Ensure that the mouse has moved a reasonable amount:
777   if ((m_lastDragPoint - e->pos()).manhattanLength() < 2)
778     return nullptr;
779 
780   RWBond selectedBond = m_selectedBond.bond();
781   // Atom that was clicked
782   RWAtom clickedAtom = m_clickedAtom.atom();
783   // Atom in selected bond also attached to clickedAtom
784   RWAtom anchorAtom = m_anchorAtom.atom();
785   // The "other" atom in selected bond
786   RWAtom otherAtom = otherBondedAtom(selectedBond, anchorAtom);
787 
788   // Sanity check:
789   if (!selectedBond.isValid() || !anchorAtom.isValid() ||
790       !otherAtom.isValid() || !clickedAtom.isValid()) {
791     return nullptr;
792   }
793 
794   const Rendering::Camera& camera(m_renderer->camera());
795 
796   // Compute the angle between last drag and current drag positions
797   const Vector3f center(anchorAtom.position3d().cast<float>());
798   const Vector3f centerProj(camera.project(center));
799   const Vector2f centerWin(centerProj.head<2>());
800   const Vector2f curWin(static_cast<float>(e->pos().x()),
801                         static_cast<float>(camera.height() - e->pos().y()));
802   const Vector2f lastWin(
803     static_cast<float>(m_lastDragPoint.x()),
804     static_cast<float>(camera.height() - m_lastDragPoint.y()));
805   const Vector2f curVecWin((curWin - centerWin).normalized());
806   const Vector2f lastVecWin((lastWin - centerWin).normalized());
807   const float crossProductNorm(lastVecWin.x() * curVecWin.y() -
808                                lastVecWin.y() * curVecWin.x());
809   const float dotProduct(lastVecWin.dot(curVecWin));
810   const float angle(std::atan2(crossProductNorm, dotProduct));
811 
812   // Figure out if the sign needs to be reversed:
813   const Vector3f other(otherAtom.position3d().cast<float>());
814   const Vector3f otherProj(camera.project(other));
815   const bool reverseSign = otherProj.z() <= centerProj.z();
816 
817   // Axis of rotation
818   const Vector3f axis((center - other).normalized());
819 
820   // Build transform
821   m_transform.setIdentity();
822   m_transform.translate(center);
823   m_transform.rotate(Eigen::AngleAxisf(reverseSign ? -angle : angle, axis));
824   m_transform.translate(-center);
825 
826   // Build the fragment if needed:
827   if (m_fragment.empty())
828     buildFragment(selectedBond, anchorAtom);
829 
830   // Perform transformation
831   transformFragment();
832   updateBondVector();
833   m_molecule->emitChanged(QtGui::Molecule::Modified | QtGui::Molecule::Atoms);
834   emit drawablesChanged();
835 
836   m_lastDragPoint = e->pos();
837 
838   return nullptr;
839 }
840 
drawBondQuad(Rendering::GeometryNode & node,const RWBond & bond) const841 void BondCentricTool::drawBondQuad(Rendering::GeometryNode& node,
842                                    const RWBond& bond) const
843 {
844   const Vector3f atom1Pos(bond.atom1().position3d().cast<float>());
845   const Vector3f atom2Pos(bond.atom2().position3d().cast<float>());
846   Vector3f offset(m_bondVector.cross(m_planeNormal));
847 
848   const Vector3f v1(atom1Pos + offset);
849   const Vector3f v2(atom2Pos + offset);
850   const Vector3f v3(atom1Pos - offset);
851   const Vector3f v4(atom2Pos - offset);
852 
853   Quad* quad = new Quad;
854   node.addDrawable(quad);
855   quad->setColor(Vector3ub(63, 127, 255));
856   quad->setOpacity(127);
857   quad->setRenderPass(Rendering::TranslucentPass);
858   quad->setQuad(v1, v2, v3, v4);
859 
860   QuadOutline* quadOutline = new QuadOutline;
861   node.addDrawable(quadOutline);
862   quadOutline->setColor(Vector3ub(63, 127, 255));
863   quadOutline->setRenderPass(Rendering::OpaquePass);
864   quadOutline->setQuad(v1, v2, v3, v4, 1.f);
865 
866   // If the plane is rotating, show a hint for the unsnapped plane.
867   if (m_moveState == RotatePlane) {
868     Vector3f moffset(m_bondVector.cross(m_planeNormalMouse));
869 
870     const Vector3f mv1(atom1Pos + moffset);
871     const Vector3f mv2(atom2Pos + moffset);
872     const Vector3f mv3(atom1Pos - moffset);
873     const Vector3f mv4(atom2Pos - moffset);
874 
875     QuadOutline* mouseQuadOutline = new QuadOutline;
876     node.addDrawable(mouseQuadOutline);
877     mouseQuadOutline->setColor(Vector3ub(255, 255, 255));
878     mouseQuadOutline->setOpacity(127);
879     mouseQuadOutline->setRenderPass(Rendering::TranslucentPass);
880     mouseQuadOutline->setQuad(mv1, mv2, mv3, mv4, 1.f);
881   }
882 }
883 
drawBondAngle(Rendering::GeometryNode & node,const QtGui::RWBond & selectedBond,const QtGui::RWBond & movingBond) const884 void BondCentricTool::drawBondAngle(Rendering::GeometryNode& node,
885                                     const QtGui::RWBond& selectedBond,
886                                     const QtGui::RWBond& movingBond) const
887 {
888   // Draw the selected bond quad as usual
889   drawBondQuad(node, selectedBond);
890 
891   // Determine the atom shared between the bonds (atom1).
892   RWAtom atom1;
893   RWAtom atom2;
894   if (selectedBond.atom1() == movingBond.atom1() ||
895       selectedBond.atom2() == movingBond.atom1()) {
896     atom1 = movingBond.atom1();
897     atom2 = movingBond.atom2();
898   } else if (selectedBond.atom1() == movingBond.atom2() ||
899              selectedBond.atom2() == movingBond.atom2()) {
900     atom1 = movingBond.atom2();
901     atom2 = movingBond.atom1();
902   }
903 
904   if (!atom1.isValid())
905     return;
906 
907   // Add another quad in the plane normal to
908   // m_bondVector.cross(movingBondVector)
909   const Vector3f a1(atom1.position3d().cast<float>());
910   const Vector3f a2(atom2.position3d().cast<float>());
911   const Vector3f movingBondVector(a2 - a1);
912   const Vector3f movingBondUnitVector(movingBondVector.normalized());
913   // calculate a vector in the plane spanned by movingBondVector and
914   // m_bondVector that is orthogonal to m_bondVector, then project
915   // movingBondVector onto it. This is used to calculate the 'new' a2.
916   const Vector3f movingBondNormal(m_bondVector.cross(movingBondUnitVector));
917   const Vector3f newA2Direction(movingBondNormal.cross(m_bondVector));
918   const Vector3f movingBondVectorProj(movingBondVector.dot(newA2Direction) *
919                                       newA2Direction);
920   const Vector3f newA2(a1 + movingBondVectorProj);
921   const Vector3f& movingBondOffset(m_bondVector);
922   const Vector3f v1(a1 + movingBondOffset);
923   const Vector3f v2(newA2 + movingBondOffset);
924   const Vector3f v3(a1 - movingBondOffset);
925   const Vector3f v4(newA2 - movingBondOffset);
926 
927   Quad* quad = new Quad;
928   node.addDrawable(quad);
929   quad->setColor(Vector3ub(63, 127, 255));
930   quad->setOpacity(127);
931   quad->setRenderPass(Rendering::TranslucentPass);
932   quad->setQuad(v1, v2, v3, v4);
933 
934   QuadOutline* quadOutline = new QuadOutline;
935   node.addDrawable(quadOutline);
936   quadOutline->setColor(Vector3ub(63, 127, 255));
937   quadOutline->setRenderPass(Rendering::OpaquePass);
938   quadOutline->setQuad(v1, v2, v3, v4, 1.f);
939 
940   // Add an arc and label to show a bit more info:
941   const Vector3f selectedBondOffset(m_planeNormal.cross(m_bondVector));
942   const float radius(movingBondVector.norm() * 0.75f);
943   Vector3f startEdge(newA2Direction * radius);
944   Vector3f normal(m_bondVector);
945   float angle = vectorAngleDegrees(startEdge, selectedBondOffset, normal);
946   float displayAngle = std::fabs(angle);
947 
948   ArcSector* sect = new ArcSector;
949   node.addDrawable(sect);
950   sect->setColor(Vector3ub(255, 127, 63));
951   sect->setOpacity(127);
952   sect->setRenderPass(Rendering::TranslucentPass);
953   sect->setArcSector(a1, startEdge, normal, angle, 5.f);
954 
955   ArcStrip* arc = new ArcStrip;
956   node.addDrawable(arc);
957   arc->setColor(Vector3ub(255, 127, 63));
958   arc->setRenderPass(Rendering::OpaquePass);
959   arc->setArc(a1, startEdge, normal, angle, 5.f, 1.f);
960 
961   const Vector3f& textPos(a1);
962 
963   Rendering::TextLabel3D* label = new Rendering::TextLabel3D;
964   label->setText(QString::number(displayAngle, 'f', 1).toStdString() +
965                  degreeString);
966   label->setRenderPass(Rendering::Overlay3DPass);
967   label->setAnchor(textPos);
968   node.addDrawable(label);
969 
970   Rendering::TextProperties tprop;
971   tprop.setAlign(Rendering::TextProperties::HCenter,
972                  Rendering::TextProperties::VCenter);
973   tprop.setFontFamily(Rendering::TextProperties::SansSerif);
974   tprop.setColorRgb(255, 200, 64);
975   label->setTextProperties(tprop);
976 }
977 
drawBondLengthLabel(Rendering::GeometryNode & node,const QtGui::RWBond & bond)978 void BondCentricTool::drawBondLengthLabel(Rendering::GeometryNode& node,
979                                           const QtGui::RWBond& bond)
980 {
981   const Vector3f startPos(bond.atom1().position3d().cast<float>());
982   const Vector3f endPos(bond.atom2().position3d().cast<float>());
983   const Vector3f bondCenter((startPos + endPos) * 0.5f);
984   const Vector3f bondVector(endPos - startPos);
985 
986   Rendering::TextLabel3D* label = new Rendering::TextLabel3D;
987   label->setText(QString::number(bondVector.norm(), 'f', 2).toStdString() +
988                  angstromString);
989   label->setRenderPass(Rendering::Overlay3DPass);
990   label->setAnchor(bondCenter);
991   node.addDrawable(label);
992 
993   Rendering::TextProperties tprop;
994   tprop.setAlign(Rendering::TextProperties::HCenter,
995                  Rendering::TextProperties::VCenter);
996   tprop.setFontFamily(Rendering::TextProperties::SansSerif);
997   tprop.setColorRgb(255, 200, 64);
998   label->setTextProperties(tprop);
999 }
1000 
drawAtomBondAngles(Rendering::GeometryNode & node,const RWAtom & atom,const RWBond & anchorBond)1001 void BondCentricTool::drawAtomBondAngles(Rendering::GeometryNode& node,
1002                                          const RWAtom& atom,
1003                                          const RWBond& anchorBond)
1004 {
1005   const Array<RWBond> bonds = m_molecule->bonds(atom);
1006   Array<RWBond>::const_iterator bondIter(bonds.begin());
1007   Array<RWBond>::const_iterator bondEnd(bonds.end());
1008   size_t count = 0;
1009   while (bondIter != bondEnd) {
1010     if (*bondIter != anchorBond)
1011       drawAtomBondAngle(node, atom, anchorBond, *bondIter, getColor(count++));
1012     ++bondIter;
1013   }
1014 }
1015 
drawAtomBondAngle(Rendering::GeometryNode & node,const QtGui::RWAtom & atom,const QtGui::RWBond & anchorBond,const QtGui::RWBond & otherBond,const Vector3ub & color)1016 void BondCentricTool::drawAtomBondAngle(Rendering::GeometryNode& node,
1017                                         const QtGui::RWAtom& atom,
1018                                         const QtGui::RWBond& anchorBond,
1019                                         const QtGui::RWBond& otherBond,
1020                                         const Vector3ub& color)
1021 {
1022   const RWAtom otherAtom = otherBondedAtom(otherBond, atom);
1023   const RWAtom otherAnchorAtom = otherBondedAtom(anchorBond, atom);
1024 
1025   const Vector3f atomPos(atom.position3d().cast<float>());
1026   const Vector3f otherAtomPos(otherAtom.position3d().cast<float>());
1027   const Vector3f otherAnchorAtomPos(otherAnchorAtom.position3d().cast<float>());
1028 
1029   const Vector3f otherVector(otherAtomPos - atomPos);
1030   const Vector3f anchorVector(otherAnchorAtomPos - atomPos);
1031   const Vector3f anchorUnitVector(anchorVector.normalized());
1032 
1033   const float radius(otherVector.norm() * 0.75f);
1034   const Vector3f& origin(atomPos);
1035   const Vector3f start(anchorUnitVector * radius);
1036   const Vector3f axis(anchorVector.cross(otherVector).normalized());
1037   const float angle = vectorAngleDegrees(otherVector, anchorVector);
1038   const Vector3f& labelPos(otherAtomPos);
1039 
1040   ArcSector* sect = new ArcSector;
1041   node.addDrawable(sect);
1042   sect->setColor(color);
1043   sect->setOpacity(127);
1044   sect->setRenderPass(Rendering::TranslucentPass);
1045   sect->setArcSector(origin, start, axis, angle, 5.f);
1046 
1047   ArcStrip* arc = new ArcStrip;
1048   node.addDrawable(arc);
1049   arc->setColor(color);
1050   arc->setRenderPass(Rendering::OpaquePass);
1051   arc->setArc(origin, start, axis, angle, 5.f, 1.f);
1052 
1053   Rendering::TextLabel3D* label = new Rendering::TextLabel3D;
1054   label->setText(QString::number(angle, 'f', 1).toStdString() + degreeString);
1055   label->setRenderPass(Rendering::Overlay3DPass);
1056   label->setAnchor(labelPos);
1057   node.addDrawable(label);
1058 
1059   Rendering::TextProperties tprop;
1060   tprop.setAlign(Rendering::TextProperties::HCenter,
1061                  Rendering::TextProperties::VCenter);
1062   tprop.setFontFamily(Rendering::TextProperties::SansSerif);
1063   tprop.setColorRgb(color);
1064   label->setTextProperties(tprop);
1065 }
1066 
bondContainsAtom(const QtGui::RWBond & bond,const QtGui::RWAtom & atom) const1067 inline bool BondCentricTool::bondContainsAtom(const QtGui::RWBond& bond,
1068                                               const QtGui::RWAtom& atom) const
1069 {
1070   return atom == bond.atom1() || atom == bond.atom2();
1071 }
1072 
otherBondedAtom(const QtGui::RWBond & bond,const QtGui::RWAtom & atom) const1073 inline QtGui::RWAtom BondCentricTool::otherBondedAtom(
1074   const QtGui::RWBond& bond, const QtGui::RWAtom& atom) const
1075 {
1076   return bond.atom1() == atom ? bond.atom2() : bond.atom1();
1077 }
1078 
transformFragment() const1079 inline void BondCentricTool::transformFragment() const
1080 {
1081   // Convert the internal float matrix to use the same precision as the atomic
1082   // coordinates.
1083   Eigen::Transform<Real, 3, Eigen::Affine> transform(m_transform.cast<Real>());
1084   for (std::vector<int>::const_iterator it = m_fragment.begin(),
1085                                         itEnd = m_fragment.end();
1086        it != itEnd; ++it) {
1087     RWAtom atom = m_molecule->atomByUniqueId(*it);
1088     if (atom.isValid()) {
1089       Vector3 pos = atom.position3d();
1090       pos = transform * pos;
1091       atom.setPosition3d(pos);
1092     }
1093   }
1094 }
1095 
updatePlaneSnapAngles()1096 void BondCentricTool::updatePlaneSnapAngles()
1097 {
1098   m_planeSnapRef = m_bondVector.unitOrthogonal();
1099   m_planeSnapAngles.clear();
1100 
1101   // Add bond angles if requested:
1102   RWBond selectedBond = m_selectedBond.bond();
1103   if (m_snapPlaneToBonds && selectedBond.isValid()) {
1104     const RWAtom atom1 = selectedBond.atom1();
1105     const RWAtom atom2 = selectedBond.atom2();
1106     for (int i = 0; i < 2; ++i) {
1107       const RWAtom& atom = i == 0 ? atom1 : atom2;
1108       const Vector3f atomPos(atom.position3d().cast<float>());
1109       const Array<RWBond> bonds = m_molecule->bonds(atom);
1110       for (std::vector<RWBond>::const_iterator it = bonds.begin(),
1111                                                itEnd = bonds.end();
1112            it != itEnd; ++it) {
1113         if (*it != selectedBond) {
1114           const RWAtom otherAtom(otherBondedAtom(*it, atom));
1115           const Vector3f otherAtomPos(otherAtom.position3d().cast<float>());
1116           const Vector3f otherBondVector(otherAtomPos - atomPos);
1117           // Project otherBondVector into the plane normal to m_bondVector
1118           // (e.g. the rejection of otherBondVector onto m_bondVector)
1119           const Vector3f rej(
1120             otherBondVector -
1121             (otherBondVector.dot(m_bondVector) * m_bondVector));
1122           float angle(vectorAngleDegrees(m_planeSnapRef, rej, m_bondVector));
1123           m_planeSnapAngles.insert(angle);
1124           angle += 180.f;
1125           if (angle > 180.f)
1126             angle -= 360.f;
1127           m_planeSnapAngles.insert(angle);
1128         }
1129       }
1130     }
1131   }
1132 
1133   // Add default increments only if they are more than 5 degrees away
1134   // from a bond angle.
1135   const float minDist(5.f);
1136   for (float angle = -180.f; angle < 180.f; angle += m_planeSnapIncr) {
1137     std::set<float>::const_iterator upper(m_planeSnapAngles.lower_bound(angle));
1138     if (upper != m_planeSnapAngles.end()) {
1139       if (*upper - minDist < angle)
1140         continue;
1141       if (upper != m_planeSnapAngles.begin()) {
1142         std::set<float>::const_iterator lower(upper);
1143         std::advance(lower, -1);
1144         if (*lower + minDist > angle)
1145           continue;
1146       }
1147       m_planeSnapAngles.insert(angle);
1148     }
1149   }
1150 }
1151 
1152 // There may be some weirdness around +/-180 since we don't check for
1153 // wrapping, but it should be fine for this use case.
updateSnappedPlaneNormal()1154 void BondCentricTool::updateSnappedPlaneNormal()
1155 {
1156   const Vector3f mousePlaneVector(m_planeNormalMouse.cross(m_bondVector));
1157   const float angle(
1158     vectorAngleDegrees(m_planeSnapRef, mousePlaneVector, m_bondVector));
1159   float snappedAngle(angle);
1160   std::set<float>::const_iterator upper(m_planeSnapAngles.lower_bound(angle));
1161   if (upper != m_planeSnapAngles.end()) {
1162     if (upper != m_planeSnapAngles.begin()) {
1163       std::set<float>::const_iterator lower(upper);
1164       std::advance(lower, -1);
1165       float upperDist = std::fabs(angle - *upper);
1166       float lowerDist = std::fabs(angle - *lower);
1167       snappedAngle = upperDist < lowerDist ? *upper : *lower;
1168     } else {
1169       snappedAngle = *upper;
1170     }
1171   }
1172 
1173   if (angle == snappedAngle) {
1174     // If the angle didn't change, keep on keepin' on:
1175     m_planeNormal = m_planeNormalMouse;
1176   } else {
1177     // Otherwise, update the vector.
1178     const Vector3f planeVector =
1179       Eigen::AngleAxisf(snappedAngle * DEG_TO_RAD_F, m_bondVector) *
1180       m_planeSnapRef;
1181     m_planeNormal = planeVector.cross(m_bondVector);
1182   }
1183 }
1184 
fragmentHasAtom(int uid) const1185 inline bool BondCentricTool::fragmentHasAtom(int uid) const
1186 {
1187   return std::find(m_fragment.begin(), m_fragment.end(), uid) !=
1188          m_fragment.end();
1189 }
1190 
buildFragment(const QtGui::RWBond & bond,const QtGui::RWAtom & startAtom)1191 void BondCentricTool::buildFragment(const QtGui::RWBond& bond,
1192                                     const QtGui::RWAtom& startAtom)
1193 {
1194   m_fragment.clear();
1195   if (!buildFragmentRecurse(bond, startAtom, startAtom)) {
1196     // If this returns false, then a cycle has been found. Only move startAtom
1197     // in this case.
1198     m_fragment.clear();
1199   }
1200   m_fragment.push_back(m_molecule->atomUniqueId(startAtom));
1201 }
1202 
buildFragmentRecurse(const QtGui::RWBond & bond,const QtGui::RWAtom & startAtom,const QtGui::RWAtom & currentAtom)1203 bool BondCentricTool::buildFragmentRecurse(const QtGui::RWBond& bond,
1204                                            const QtGui::RWAtom& startAtom,
1205                                            const QtGui::RWAtom& currentAtom)
1206 {
1207   Array<RWBond> bonds = m_molecule->bonds(currentAtom);
1208   typedef std::vector<RWBond>::const_iterator BondIter;
1209   for (BondIter it = bonds.begin(), itEnd = bonds.end(); it != itEnd; ++it) {
1210     if (*it != bond) { // Skip the current bond
1211       RWAtom nextAtom = otherBondedAtom(*it, currentAtom);
1212       if (nextAtom != startAtom) {
1213         // Skip atoms that have already been added. This prevents infinite
1214         // recursion on cycles in the fragments
1215         int uid = m_molecule->atomUniqueId(nextAtom);
1216         if (!fragmentHasAtom(uid)) {
1217           m_fragment.push_back(uid);
1218           if (!buildFragmentRecurse(*it, startAtom, nextAtom))
1219             return false;
1220         }
1221       } else {
1222         // If we've reached startAtom, then we've found a cycle that indicates
1223         // no moveable fragment exists.
1224         return false;
1225       } // nextAtom != startAtom else
1226     }   // *it != bond
1227   }     // foreach bond
1228   return true;
1229 }
1230 
1231 } // namespace QtPlugins
1232 } // namespace Avogadro
1233