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