Files
rdkit/Code/GraphMol/MolDraw2D/MolDraw2DDetails.cpp
David Cosgrove f9b47d907f Add ACS1996 drawing style (#5425)
* Started on ACS 1996 drawing mode.
Significant change (not by itself, sadly) is that MolDrawOptions::lineWidth has changed from int to double to allow for ACS requirement of 0.6px bond widths.

* Wavy lines and dashed wedges.

* Better dashed wedges.

* Rounder wavy bonds, same as SVG.

* Added FreeSans font for ACS1996 mode.

* Add help functions to write enum classes to ostream.

* Dashed wedge separation now 2.5px between line edges rather than line middles.  Therefore wider gap between lines.

* Increase offset for wavy bond.  Get classes for atoms and bonds in wavy bond correct.

* For SMILES input, option to force wavy and crossed bonds for unspecified stereochem.

* Tidy debugging.

* Extra space round atom labels.

* Extra space between chars in freetype string.

* Reformats.

* Change double bond offset.

* Improve ring double bonds.

* Simple non-ring double bonds all working.

* Tidy.

* All double bonds seem good.

* Remove redundant function.
Move calcTripleBondLines into DrawMol for consistency with calcDoubleBondLines.
Use doubleBondOffset for wavy lines.

* Correct spacing between FT chars.

* Tidying.

* Use MolBlock wedging if there is any.  Needs to be made an option.

* If dashed wedge thick end has bonds of it, stop one dash short.

* Adjust solid wedge ends to line with attached bonds.

* Width of wedge ends now based on double bond separation.

* Change catch_tests.cpp

* Rounder waves in wavy lines.

* Dashed wedges same width even if one dash less..

* Embedded Roboto-Regular font in code.

* Fix docstrings.

* doubleBondTerminal swapped ends.
Deal with O2 - 2 terminal atoms of degree 1.

* Fix terminal double bonds.

* Slightly fatter truncated wedge bonds.

* Fix crash on complicated double bonds.

* Control more assert tests with DO_TEST_ASSERT.

* Fix 2 colour solid wedges.
Fix slanted wedge for morphine (test1_5).

* Change definittion of multipleBondOffset to fraction of mean bond length.

* Don't slant end of solid wedge to atom symbol.

* Fix wiggle separation.

* Fix 2-colour terminal double bonds.

* Fix colours on triple bonds.

* Don't attempt to draw non-existent points in triangle..

* Symmetric bond for P=O and like.

* Fix query bonds.

* Reformatting.

* Tidy up use of font.

* Add FreeSans font and license to $RDBASE/Data/Fonts.

* Draw unspecified stereo as unknown.

* Add check_file_hash.

* Tidying.

* Tidying.

* Start Python wrappers.

* Fix solid wedges for 3-connected atoms.

* Docstrings.

* Tidying.

* Alter width of bond highlights in ACS 1996 mode.

* Expose setACS1996Options and mean BondLength in Python.

* Expose drawMolACS1996Cairo in Python.
Docstrings.

* Extra padding between legend and picture in flexicanvas.

* Python tests.

* Tidy catch tests.

* Tidying.

* Fix catch tests.

* Fix no Freetype tests.

* Draw solid wedge more sensibly..

* Fix bond end at solid wedge.

* Tidy.

* Fix Python Cairo build issues.

* Fix wedge end shape for terminal double bonds.

* Hide the joins at the bond ends.

* Fix gcc pickiness.

* Extra test for no atom labels.

* Change where it looks for FreeSans.ttf for ACS1996 drawings.

* Same number of waves for wavy bonds in SVG and Cairo.

* Same number of waves for wavy bonds in SVG and Cairo.

* rename unspecifiedStereoIsUnknown to markUnspecifiedStereoAsUnknown and move to MolFileStereochem.h

* refactor use of iterators

* py docs update

* undo a bunch of bad formatting changes

* remove FreeSans

* get windows builds working

* Fix problem with Windows build.

* Changes in response to review.

* Align description of unspecifiedStereoIsUnknown in C++ to match Python.

* Still working on file open modes.

* Took out extraneous functions for drawing in ACS1996 mode, including the one that was breaking the windows build.

* Add RDKIT_MOLDRAW2D_EXPORT.

* Fix expected test results.

* Clarified warning.

* RDKIT_MOLDRAW2D_EXPORT missing.

* Windows!

* Update Code/GraphMol/MolDraw2D/rxn_test1.cpp

Co-authored-by: David Cosgrove <david@cozchemix.co.uk>
Co-authored-by: greg landrum <greg.landrum@gmail.com>
2022-07-21 18:11:33 +02:00

413 lines
13 KiB
C++

//
// Copyright (C) 2015-2020 Greg Landrum
//
// @@ All Rights Reserved @@
// This file is part of the RDKit.
// The contents are covered by the terms of the BSD license
// which is included in the file license.txt, found at the root
// of the RDKit source tree.
//
#include <GraphMol/MolDraw2D/MolDraw2DDetails.h>
#include <GraphMol/MolDraw2D/StringRect.h>
#include <GraphMol/Conformer.h>
#include <GraphMol/SubstanceGroup.h>
#include <cmath>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
// ****************************************************************************
namespace RDKit {
namespace MolDraw2D_detail {
// implementation from $RDBASE/rdkit/sping/pid.py
void arcPoints(const Point2D &cds1, const Point2D &cds2,
std::vector<Point2D> &res, float startAng, float extent) {
// Note: this implementation is simple and not particularly efficient.
float xScale = (cds2.x - cds1.x) / 2.0;
float yScale = (cds2.y - cds1.y) / 2.0;
if (xScale < 0) {
xScale *= -1;
}
if (yScale < 0) {
yScale *= -1;
}
float x = std::min(cds1.x, cds2.x) + xScale;
float y = std::min(cds1.y, cds2.y) + yScale;
int steps = std::max(static_cast<int>(extent * 2), 5);
float step = M_PI * extent / (180 * steps);
float angle = M_PI * startAng / 180;
for (int i = 0; i <= steps; ++i) {
Point2D point(x + xScale * cos(angle), y - yScale * sin(angle));
res.emplace_back(point);
angle += step;
}
}
void addStereoAnnotation(const ROMol &mol, bool includeRelativeCIP) {
const auto &sgs = mol.getStereoGroups();
std::vector<unsigned int> doneAts(mol.getNumAtoms(), 0);
unsigned int grpid = 1;
for (const auto &sg : sgs) {
for (const auto atom : sg.getAtoms()) {
if (doneAts[atom->getIdx()]) {
BOOST_LOG(rdWarningLog) << "Warning: atom " << atom->getIdx()
<< " is in more than one stereogroup. Only the "
"label from the first group will be used."
<< std::endl;
continue;
}
std::string lab;
std::string cip;
if (includeRelativeCIP ||
sg.getGroupType() == StereoGroupType::STEREO_ABSOLUTE) {
atom->getPropIfPresent(common_properties::_CIPCode, cip);
}
switch (sg.getGroupType()) {
case StereoGroupType::STEREO_ABSOLUTE:
lab = "abs";
break;
case StereoGroupType::STEREO_OR:
lab = (boost::format("or%d") % grpid).str();
break;
case StereoGroupType::STEREO_AND:
lab = (boost::format("and%d") % grpid).str();
break;
default:
break;
}
if (!lab.empty()) {
doneAts[atom->getIdx()] = 1;
if (!cip.empty()) {
lab += " (" + cip + ")";
}
atom->setProp(common_properties::atomNote, lab);
}
}
if (sg.getGroupType() != StereoGroupType::STEREO_ABSOLUTE) {
++grpid;
}
}
for (auto atom : mol.atoms()) {
std::string cip;
if (!doneAts[atom->getIdx()] &&
atom->getPropIfPresent(common_properties::_CIPCode, cip)) {
std::string lab = "(" + cip + ")";
atom->setProp(common_properties::atomNote, lab);
}
}
for (auto bond : mol.bonds()) {
std::string cip;
if (!bond->getPropIfPresent(common_properties::_CIPCode, cip)) {
if (bond->getStereo() == Bond::STEREOE) {
cip = "E";
} else if (bond->getStereo() == Bond::STEREOZ) {
cip = "Z";
}
}
if (!cip.empty()) {
std::string lab = "(" + cip + ")";
bond->setProp(common_properties::bondNote, lab);
}
}
}
namespace {
// note, this is approximate since we're just using it for drawing
bool lineSegmentsIntersect(const Point2D &s1, const Point2D &s2,
const Point2D &s3, const Point2D &s4) {
auto d1x = (s1.x - s2.x);
auto d1y = (s1.y - s2.y);
auto d2x = (s3.x - s4.x);
auto d2y = (s3.y - s4.y);
if (fabs(d1x) < 1e-4) {
// fudge factor, since this isn't super critical
d1x = 1e-4;
}
if (fabs(d2x) < 1e-4) {
// fudge factor, since this isn't super critical
d2x = 1e-4;
}
auto m1 = d1y / d1x;
auto m2 = d2y / d2x;
if (m1 == m2 || m1 == -m2) {
// parallel
return false;
}
auto b1 = (s1.x * s2.y - s2.x * s1.y) / d1x;
auto b2 = (s3.x * s4.y - s4.x * s3.y) / d2x;
auto intersectX = (b2 - b1) / (m1 - m2);
return ((intersectX < s1.x) ^ (intersectX < s2.x)) &&
((intersectX < s3.x) ^ (intersectX < s4.x));
}
} // namespace
std::vector<Point2D> getBracketPoints(
const Point2D &p1, const Point2D &p2, const Point2D &refPt,
const std::vector<std::pair<Point2D, Point2D>> &bondSegments,
double bracketFrac) {
std::vector<Point2D> res;
auto v = p2 - p1;
Point2D bracketDir{v.y, -v.x};
bracketDir *= bracketFrac;
// we'll default to use the refPt
auto refVect = p2 - refPt;
// but check if we intersect any of the bonds:
for (const auto &seg : bondSegments) {
if (lineSegmentsIntersect(p1, p2, seg.first, seg.second)) {
refVect = p2 - seg.first;
}
}
if (bracketDir.dotProduct(refVect) > 0) {
bracketDir *= -1;
}
auto p0 = p1 + bracketDir;
auto p3 = p2 + bracketDir;
return {p0, p1, p2, p3};
}
// there are a several empirically determined constants here.
std::vector<Point2D> handdrawnLine(Point2D cds1, Point2D cds2, double scale,
bool shiftBegin, bool shiftEnd,
unsigned nSteps, double deviation,
double endShift) {
// std::cout << " " << scale << " " << endShift / scale << std::endl;
while (endShift / scale > 0.02) {
endShift *= 0.75;
}
if (shiftBegin) {
cds1.x += (std::rand() % 10 >= 5 ? endShift : -endShift) / scale;
cds1.y += (std::rand() % 10 >= 5 ? endShift : -endShift) / scale;
}
if (shiftEnd) {
cds2.x += (std::rand() % 10 >= 5 ? endShift : -endShift) / scale;
cds2.y += (std::rand() % 10 >= 5 ? endShift : -endShift) / scale;
}
Point2D step = (cds2 - cds1) / nSteps;
// make sure we aren't adding loads of wiggles to short lines
while (step.length() < 0.2 && nSteps > 2) {
--nSteps;
step = (cds2 - cds1) / nSteps;
}
// make sure the wiggles aren't too big
while (deviation / step.length() > 0.15 || deviation * scale > 0.70) {
deviation *= 0.75;
}
Point2D perp{step.y, -step.x};
perp.normalize();
std::vector<Point2D> pts;
pts.push_back(cds1);
for (unsigned int i = 1; i < nSteps; ++i) {
auto tgt = cds1 + step * i;
tgt += perp * deviation * (std::rand() % 20 - 10) / 10.0;
pts.push_back(tgt);
}
pts.push_back(cds2);
return pts;
}
// ****************************************************************************
bool doesLineIntersect(const StringRect &rect, const Point2D &end1,
const Point2D &end2, double padding) {
Point2D tl, tr, bl, br;
rect.calcCorners(tl, tr, br, bl, padding);
if (doLinesIntersect(end2, end1, tl, tr, nullptr)) {
return true;
}
if (doLinesIntersect(end2, end1, tr, br, nullptr)) {
return true;
}
if (doLinesIntersect(end2, end1, br, bl, nullptr)) {
return true;
}
if (doLinesIntersect(end2, end1, bl, tl, nullptr)) {
return true;
}
return false;
}
// ****************************************************************************
bool doesTriangleIntersect(const StringRect &rect, const Point2D &pt1,
const Point2D &pt2, const Point2D &pt3,
double padding) {
// the quick test is for any of the triangle points inside the rectangle.
if (rect.isPointInside(pt1, padding) || rect.isPointInside(pt2, padding) ||
rect.isPointInside(pt3, padding)) {
return true;
}
// But if the rectangle is inside the triangle, that's not enough of a test.
Point2D tl, tr, br, bl;
rect.calcCorners(tl, tr, br, bl, padding);
if (isPointInTriangle(tl, pt1, pt2, pt3) ||
isPointInTriangle(tr, pt1, pt2, pt3) ||
isPointInTriangle(br, pt1, pt2, pt3) ||
isPointInTriangle(bl, pt1, pt2, pt3)) {
return true;
}
// and finally all the points in the rectangle can be outside the triangle,
// but the sides can cross it. And vice versa. So see if any of the sides
// intersect.
if (doLinesIntersect(tl, tr, pt1, pt2, nullptr) ||
doLinesIntersect(tl, tr, pt2, pt3, nullptr) ||
doLinesIntersect(tl, tr, pt3, pt1, nullptr) ||
doLinesIntersect(tr, br, pt1, pt2, nullptr) ||
doLinesIntersect(tr, br, pt2, pt3, nullptr) ||
doLinesIntersect(tr, br, pt3, pt1, nullptr) ||
doLinesIntersect(br, bl, pt1, pt2, nullptr) ||
doLinesIntersect(br, bl, pt2, pt3, nullptr) ||
doLinesIntersect(br, bl, pt3, pt1, nullptr) ||
doLinesIntersect(bl, tl, pt1, pt2, nullptr) ||
doLinesIntersect(bl, tl, pt2, pt3, nullptr) ||
doLinesIntersect(bl, tl, pt3, pt1, nullptr)) {
return true;
}
return false;
}
// ****************************************************************************
bool doesLineIntersectEllipse(const Point2D &centre, double xradius,
double yradius, double padding,
const Point2D &end1, const Point2D &end2) {
// using
// https://math.stackexchange.com/questions/76457/check-if-a-point-is-within-an-ellipse
// to see if either end1 or end2 are inside the ellipse.
double xr2 = (xradius + padding) * (xradius + padding);
double yr2 = (yradius + padding) * (yradius + padding);
double xdisc = (end1.x - centre.x) * (end1.x - centre.x) / xr2;
double ydisc = (end1.y - centre.y) * (end1.y - centre.y) / yr2;
if (xdisc + ydisc <= 1.0) {
return true;
}
xdisc = (end2.x - centre.x) * (end2.x - centre.x) / xr2;
ydisc = (end2.y - centre.y) * (end2.y - centre.y) / yr2;
return xdisc + ydisc <= 1.0;
}
// ****************************************************************************
bool doesLineIntersectArc(const Point2D &centre, double xradius, double yradius,
double start_ang, double stop_ang, double padding,
const Point2D &end1, const Point2D &end2) {
double xr = xradius + padding;
double yr = yradius + padding;
double xr2 = xr * xr;
double yr2 = yr * yr;
auto pointInArc = [&](const Point2D &p) -> bool {
double xdisc = p.x * p.x / xr2;
double ydisc = p.y * p.y / yr2;
// start_ang can be more than stop_ang, if, for example, the arc goes from
// 315 to 45.
if (xdisc + ydisc <= 1.0) {
// end1 is inside the whole ellipse. See if the angle it makes with the
// x axis lies between start_and and stop_ang.
double th = atan2(p.x, p.y) * 180.0 / M_PI;
if (th < 0.0) {
th += 360.0;
}
if (start_ang < stop_ang) {
// it's pretty straightforward
if (th >= start_ang && th <= stop_ang) {
return true;
}
} else {
// the arc crosses 0.
if (th >= start_ang && th <= 360.0) {
return true;
}
if (th >= 0.0 && th <= stop_ang) {
return true;
}
}
}
return false;
};
Point2D p1 = end1 - centre;
if (pointInArc(p1)) {
return true;
}
Point2D p2 = end2 - centre;
return pointInArc(p2);
}
// ****************************************************************************
bool doLinesIntersect(const Point2D &l1s, const Point2D &l1f,
const Point2D &l2s, const Point2D &l2f, Point2D *ip) {
// using spell from answer 2 of
// https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect
double s1_x = l1f.x - l1s.x;
double s1_y = l1f.y - l1s.y;
double s2_x = l2f.x - l2s.x;
double s2_y = l2f.y - l2s.y;
double d = (-s2_x * s1_y + s1_x * s2_y);
if (d == 0.0) {
// parallel lines.
return false;
}
double s, t;
s = (-s1_y * (l1s.x - l2s.x) + s1_x * (l1s.y - l2s.y)) / d;
t = (s2_x * (l1s.y - l2s.y) - s2_y * (l1s.x - l2s.x)) / d;
if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
if (ip) {
ip->x = l1s.x + t * s1_x;
ip->y = l1s.y + t * s1_y;
}
return true;
}
return false;
}
// ****************************************************************************
bool isPointInTriangle(const Point2D &pt, const Point2D &t1, const Point2D &t2,
const Point2D &t3) {
double d = ((t2.y - t3.y) * (t1.x - t3.x) + (t3.x - t2.x) * (t1.y - t3.y));
double a =
((t2.y - t3.y) * (pt.x - t3.x) + (t3.x - t2.x) * (pt.y - t3.y)) / d;
double b =
((t3.y - t1.y) * (pt.x - t3.x) + (t1.x - t3.x) * (pt.y - t3.y)) / d;
double c = 1 - a - b;
return 0 <= a && a <= 1 && 0 <= b && b <= 1 && 0 <= c && c <= 1;
}
std::vector<std::tuple<Point2D, Point2D, Point2D, Point2D>> getWavyLineSegments(
const Point2D &p1, const Point2D &p2, unsigned int nSegments,
double vertOffset) {
std::vector<std::tuple<Point2D, Point2D, Point2D, Point2D>> res;
PRECONDITION(nSegments > 1, "too few segments");
if (nSegments % 2) {
++nSegments; // we're going to assume an even number of segments
}
Point2D delta = (p2 - p1);
Point2D perp(delta.y, -delta.x);
perp.normalize();
perp *= vertOffset;
delta /= nSegments;
for (unsigned int i = 0; i < nSegments; ++i) {
Point2D startpt = p1 + delta * i;
Point2D segpt = startpt + delta;
Point2D cpt1 = startpt + perp * (i % 2 ? -1 : 1);
Point2D cpt2 = segpt + perp * (i % 2 ? -1 : 1);
res.emplace_back(startpt, cpt1, cpt2, segpt);
}
return res;
}
} // namespace MolDraw2D_detail
} // namespace RDKit