mirror of
https://github.com/rdkit/rdkit.git
synced 2026-06-03 21:44:30 +08:00
Fix arrowhead8500 (#8504)
* Fix the arrow heads. * Tidy. * Looser tolerance on test. --------- Co-authored-by: David Cosgrove <david@cozchemix.co.uk>
This commit is contained in:
@@ -99,13 +99,20 @@ DrawShapeArrow::DrawShapeArrow(const std::vector<Point2D> &points,
|
||||
frac_(frac),
|
||||
angle_(angle) {
|
||||
PRECONDITION(points_.size() == 2, "arrow bad points size");
|
||||
|
||||
// the two ends of the arrowhead are used for collision detection so we
|
||||
// may as well store them.
|
||||
// The two ends of the arrowhead are used for collision detection so we
|
||||
// may as well store them. They won't be used for drawing. lineWidth_ is
|
||||
// in pixels rather than drawing coords, so keep another set of coords that
|
||||
// are the original ends and arrowhead ends, with a lineWidth_ of 0, used
|
||||
// for findExtremes.
|
||||
origPts_[0] = points_[0];
|
||||
origPts_[1] = points_[1];
|
||||
Point2D ab(points_[1]), p1, p2;
|
||||
MolDraw2D_detail::calcArrowHead(ab, p1, p2, points_[0], fill_, frac_, angle_);
|
||||
points_.push_back(p1);
|
||||
points_.push_back(p2);
|
||||
MolDraw2D_detail::calcArrowHead(ab, p1, p2, points_[0], frac_, lineWidth_,
|
||||
angle_);
|
||||
ab = origPts_[1];
|
||||
MolDraw2D_detail::calcArrowHead(ab, p1, p2, points_[1], frac_, 0.0, angle_);
|
||||
origPts_[2] = p1;
|
||||
origPts_[3] = p2;
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
@@ -120,6 +127,42 @@ void DrawShapeArrow::myDraw(MolDraw2D &drawer) const {
|
||||
true);
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
void DrawShapeArrow::findExtremes(double &xmin, double &xmax, double &ymin,
|
||||
double &ymax) const {
|
||||
// do it for origPts_ rather than points_. The difference is that the
|
||||
// arrow ends haven't been adjusted for the lineWidth_. It's pretty
|
||||
// inconceivable that this will matter for these purposes as the chances
|
||||
// of a dative bond going to an atom without a symbol on the edge of the
|
||||
// drawing seem slim. findExtremes is only used for finding the extremes
|
||||
// of the whole drawing so as to set the scale.
|
||||
for (int i = 0; i < 4; i++) {
|
||||
const Point2D &p = origPts_[i];
|
||||
xmin = std::min(xmin, p.x);
|
||||
xmax = std::max(xmax, p.x);
|
||||
ymin = std::min(ymin, p.y);
|
||||
ymax = std::max(ymax, p.y);
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
void DrawShapeArrow::scale(const Point2D &scale_factor) {
|
||||
DrawShape::scale(scale_factor);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Point2D &p = origPts_[i];
|
||||
p.x *= scale_factor.x;
|
||||
p.y *= scale_factor.y;
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
void DrawShapeArrow::move(const Point2D &trans) {
|
||||
DrawShape::move(trans);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
origPts_[i] += trans;
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
bool DrawShapeArrow::doesRectClash(const StringRect &rect,
|
||||
double padding) const {
|
||||
|
||||
@@ -73,10 +73,15 @@ class DrawShapeArrow : public DrawShape {
|
||||
DrawShapeArrow &operator=(const DrawShapeArrow &) = delete;
|
||||
DrawShapeArrow &operator=(DrawShapeArrow &&) = delete;
|
||||
void myDraw(MolDraw2D &drawer) const override;
|
||||
void findExtremes(double &xmin, double &xmax, double &ymin,
|
||||
double &ymax) const override;
|
||||
void scale(const Point2D &scale_factor) override;
|
||||
void move(const Point2D &trans) override;
|
||||
bool doesRectClash(const StringRect &rect, double padding) const override;
|
||||
|
||||
double frac_;
|
||||
double angle_;
|
||||
Point2D origPts_[4]{{0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}};
|
||||
};
|
||||
|
||||
class DrawShapeEllipse : public DrawShape {
|
||||
|
||||
@@ -465,24 +465,21 @@ void MolDraw2D::drawWavyLine(const Point2D &cds1, const Point2D &cds2,
|
||||
void MolDraw2D::drawArrow(const Point2D &arrowBegin, const Point2D &arrowEnd,
|
||||
bool asPolygon, double frac, double angle,
|
||||
const DrawColour &col, bool rawCoords) {
|
||||
PRECONDITION(angle > 1.0e-6,
|
||||
"Arrow angle must be positive and greater than 0.0.");
|
||||
Point2D ae(arrowEnd), p1, p2;
|
||||
MolDraw2D_detail::calcArrowHead(ae, p1, p2, arrowBegin, asPolygon, frac,
|
||||
angle);
|
||||
MolDraw2D_detail::calcArrowHead(ae, p1, p2, arrowBegin, frac,
|
||||
getDrawLineWidth(), angle);
|
||||
|
||||
drawLine(arrowBegin, ae, col, col, rawCoords);
|
||||
if (!asPolygon) {
|
||||
drawLine(ae, p1, col, col, rawCoords);
|
||||
drawLine(ae, p2, col, col, rawCoords);
|
||||
} else {
|
||||
std::vector<Point2D> pts = {p1, ae, p2};
|
||||
bool fps = fillPolys();
|
||||
auto dc = colour();
|
||||
setFillPolys(true);
|
||||
setColour(col);
|
||||
drawPolygon(pts, rawCoords);
|
||||
setFillPolys(fps);
|
||||
setColour(dc);
|
||||
}
|
||||
std::vector<Point2D> pts = {p1, ae, p2};
|
||||
bool fps = fillPolys();
|
||||
auto dc = colour();
|
||||
setFillPolys(asPolygon);
|
||||
setColour(col);
|
||||
drawPolygon(pts, rawCoords);
|
||||
setFillPolys(fps);
|
||||
setColour(dc);
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
|
||||
@@ -271,7 +271,9 @@ class RDKIT_MOLDRAW2D_EXPORT MolDraw2D {
|
||||
const DrawColour &col1, const DrawColour &col2,
|
||||
unsigned int nSegments = 16,
|
||||
double vertOffset = 0.05, bool rawCoords = false);
|
||||
//! Draw an arrow with either lines or a filled head (when asPolygon is true)
|
||||
//! Draw an arrow with either lines or a filled head (when asPolygon is true).
|
||||
//! Angle is the half-angle at the point, so the default gives total angle
|
||||
//! of 60 degrees at the arrow head.
|
||||
virtual void drawArrow(const Point2D &cds1, const Point2D &cds2,
|
||||
bool asPolygon = false, double frac = 0.05,
|
||||
double angle = M_PI / 6,
|
||||
|
||||
@@ -167,7 +167,7 @@ void MolDraw2DCairo::drawPolygon(const std::vector<Point2D> &cds,
|
||||
cairo_line_join_t olinejoin = cairo_get_line_join(dp_cr);
|
||||
|
||||
cairo_set_line_cap(dp_cr, CAIRO_LINE_CAP_BUTT);
|
||||
cairo_set_line_join(dp_cr, CAIRO_LINE_JOIN_BEVEL);
|
||||
cairo_set_line_join(dp_cr, CAIRO_LINE_JOIN_MITER);
|
||||
setDashes(dp_cr, dash());
|
||||
cairo_set_line_width(dp_cr, width);
|
||||
|
||||
|
||||
@@ -342,17 +342,25 @@ std::vector<std::tuple<Point2D, Point2D, Point2D, Point2D>> getWavyLineSegments(
|
||||
RDKIT_MOLDRAW2D_EXPORT void calcArrowHead(Point2D &arrowEnd, Point2D &arrow1,
|
||||
Point2D &arrow2,
|
||||
const Point2D &arrowBegin,
|
||||
bool asPolygon, double frac,
|
||||
double frac, double lineWidth,
|
||||
double angle) {
|
||||
if (angle < 1.0e-6) {
|
||||
return;
|
||||
}
|
||||
// Allow for the mitre.
|
||||
double adjuster = 0.5 * lineWidth / sin(angle);
|
||||
double len = (arrowBegin - arrowEnd).length();
|
||||
if (len > 1.0e-6) {
|
||||
double adjLen = len - adjuster;
|
||||
arrowEnd.x = arrowBegin.x + (arrowEnd.x - arrowBegin.x) * adjLen / len;
|
||||
arrowEnd.y = arrowBegin.y + (arrowEnd.y - arrowBegin.y) * adjLen / len;
|
||||
}
|
||||
|
||||
auto delta = arrowBegin - arrowEnd;
|
||||
double cos_angle = std::cos(angle), sin_angle = std::sin(angle);
|
||||
// to have the arrowhead a consistent fraction of the line length, we need
|
||||
// the hypotenuse
|
||||
// To have the arrowhead a consistent fraction of the line length, we need
|
||||
// the hypotenuse.
|
||||
frac /= cos_angle;
|
||||
if (asPolygon) {
|
||||
// allow for the mitring, using an empirically derived guess.
|
||||
arrowEnd += delta * 0.1;
|
||||
}
|
||||
arrow1 = arrowEnd;
|
||||
arrow1.x += frac * (delta.x * cos_angle + delta.y * sin_angle);
|
||||
arrow1.y += frac * (delta.y * cos_angle - delta.x * sin_angle);
|
||||
|
||||
@@ -152,7 +152,7 @@ std::vector<std::tuple<Point2D, Point2D, Point2D, Point2D>> getWavyLineSegments(
|
||||
RDKIT_MOLDRAW2D_EXPORT void calcArrowHead(Point2D &arrowEnd, Point2D &arrow1,
|
||||
Point2D &arrow2,
|
||||
const Point2D &arrowBegin,
|
||||
bool asPolygon, double frac,
|
||||
double frac, double lineWidth,
|
||||
double angle);
|
||||
|
||||
// adjust p2 so that the line from p1 to p2 stops where it intersects
|
||||
|
||||
@@ -276,10 +276,11 @@ void MolDraw2DSVG::drawPolygon(const std::vector<Point2D> &cds,
|
||||
d_os << "' style='fill:none;";
|
||||
}
|
||||
|
||||
d_os << "stroke:" << col
|
||||
<< ";stroke-width:" << MolDraw2D_detail::formatDouble(width)
|
||||
<< "px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:"
|
||||
<< colour().a << ";" << dashString << "'";
|
||||
d_os
|
||||
<< "stroke:" << col
|
||||
<< ";stroke-width:" << MolDraw2D_detail::formatDouble(width)
|
||||
<< "px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:"
|
||||
<< colour().a << ";" << dashString << "'";
|
||||
d_os << " />\n";
|
||||
}
|
||||
|
||||
|
||||
@@ -66,13 +66,13 @@ const std::map<std::string, std::hash_result_t> SVG_HASHES = {
|
||||
{"contourMol_5.svg", 3792684719U},
|
||||
{"contourMol_6.svg", 1743220181U},
|
||||
{"contourMol_7.svg", 2221193310U},
|
||||
{"testDativeBonds_1.svg", 3550231997U},
|
||||
{"testDativeBonds_2.svg", 2510476717U},
|
||||
{"testDativeBonds_3.svg", 1742381275U},
|
||||
{"testDativeBonds_2a.svg", 3936523099U},
|
||||
{"testDativeBonds_2b.svg", 1652957675U},
|
||||
{"testDativeBonds_2c.svg", 630355005U},
|
||||
{"testDativeBonds_2d.svg", 2346072497U},
|
||||
{"testDativeBonds_1.svg", 1984402893U},
|
||||
{"testDativeBonds_2.svg", 1091476418U},
|
||||
{"testDativeBonds_3.svg", 1602774141U},
|
||||
{"testDativeBonds_2a.svg", 2463158006U},
|
||||
{"testDativeBonds_2b.svg", 3095643972U},
|
||||
{"testDativeBonds_2c.svg", 1745138239U},
|
||||
{"testDativeBonds_2d.svg", 3279423301U},
|
||||
{"testZeroOrderBonds_1.svg", 3733430366U},
|
||||
{"testFoundations_1.svg", 2350247048U},
|
||||
{"testFoundations_2.svg", 15997352U},
|
||||
@@ -355,7 +355,7 @@ const std::map<std::string, std::hash_result_t> SVG_HASHES = {
|
||||
{"testAtomAbbreviationsClash.svg", 1847939197U},
|
||||
{"testBlackAtomsUnderHighlight.svg", 3916069581U},
|
||||
{"testSmallReactionCanvas.svg", 2238356155U},
|
||||
{"testReactionProductSmoothCorners.svg", 774582418U},
|
||||
{"testReactionProductSmoothCorners.svg", 882352670U},
|
||||
{"testOptionalAtomListBrackets_1.svg", 4239908626U},
|
||||
{"testOptionalAtomListBrackets_2.svg", 3881772214U},
|
||||
{"testOptionalAtomListBrackets_3.svg", 2945415850U},
|
||||
@@ -368,6 +368,7 @@ const std::map<std::string, std::hash_result_t> SVG_HASHES = {
|
||||
{"testAtomAndBondLabels_4.svg", 229616498U},
|
||||
{"testStandardColoursHighlightedAtoms_1.svg", 4265528904U},
|
||||
{"testStandardColoursHighlightedAtoms_2.svg", 2285000572U},
|
||||
{"testArrowheads.svg", 3318006834U},
|
||||
};
|
||||
|
||||
// These PNG hashes aren't completely reliable due to floating point cruft,
|
||||
@@ -6577,13 +6578,14 @@ M END
|
||||
|
||||
auto h1s1 = (ends1[0] - ends1[1]).length();
|
||||
auto h2s1 = (ends2[0] - ends2[1]).length();
|
||||
// there's still a small difference in size of arrow head because the
|
||||
// allowance for mitring is done as a fraction of the overall arrow
|
||||
// length.
|
||||
CHECK_THAT(h1s1, Catch::Matchers::WithinAbs(h2s1, 0.1));
|
||||
// There's still a small difference in size of arrowhead because
|
||||
// it is done as a fraction of the overall arrow length and the dative
|
||||
// bonds in this ferrocene are of markedly different lengths. The
|
||||
// lengths are in pixels, so the differences aren't huge.
|
||||
CHECK_THAT(h1s1, Catch::Matchers::WithinAbs(h2s1, 0.75));
|
||||
auto h1s2 = (ends1[0] - ends1[2]).length();
|
||||
auto h2s2 = (ends2[0] - ends2[2]).length();
|
||||
CHECK_THAT(h1s2, Catch::Matchers::WithinAbs(h2s2, 0.1));
|
||||
CHECK_THAT(h1s2, Catch::Matchers::WithinAbs(h2s2, 0.75));
|
||||
|
||||
check_file_hash(nameBase + ".svg");
|
||||
}
|
||||
@@ -10375,7 +10377,8 @@ TEST_CASE("Github8209 - Reaction products not having bond corners smoothed") {
|
||||
size_t nOccurrences =
|
||||
std::distance(std::sregex_token_iterator(text.begin(), text.end(), path),
|
||||
std::sregex_token_iterator());
|
||||
CHECK(nOccurrences == 10);
|
||||
// There's one for 5 corners on each benzene, and now one on the arrowhead.
|
||||
CHECK(nOccurrences == 11);
|
||||
check_file_hash("testReactionProductSmoothCorners.svg");
|
||||
}
|
||||
|
||||
@@ -10698,4 +10701,54 @@ TEST_CASE("drawString() uses drawColour") {
|
||||
CHECK(text.find("#000000' >Z</text>") != std::string::npos);
|
||||
// The blue color:
|
||||
CHECK(text.find("#4C4CFF' >X</text>") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("Solid arrowhead in wrong place (Github 8500)") {
|
||||
MolDraw2DSVG drawer(300, 300);
|
||||
drawer.setColour(DrawColour(1.0, 0.0, 0.0));
|
||||
drawer.drawLine(Point2D{50.0, 50.0}, Point2D{250.0, 50.0}, true);
|
||||
drawer.drawLine(Point2D{250.0, 50.0}, Point2D{250.0, 250.0}, true);
|
||||
drawer.drawLine(Point2D{250.0, 250.0}, Point2D{50.0, 250.0}, true);
|
||||
drawer.drawLine(Point2D{50.0, 50.0}, Point2D{50.0, 250.0}, true);
|
||||
drawer.setColour(DrawColour(0.0, 1.0, 0.0));
|
||||
drawer.setLineWidth(8);
|
||||
drawer.drawArrow(Point2D{150.0, 150.0}, Point2D{250.0, 250.0}, true, 0.05,
|
||||
M_PI / 4, DrawColour(0.0, 1.0, 0.0), true);
|
||||
drawer.drawArrow(Point2D{150.0, 150.0}, Point2D{50.0, 50.0}, false, 0.05,
|
||||
M_PI / 4, DrawColour(0.0, 1.0, 0.0), true);
|
||||
drawer.setLineWidth(2);
|
||||
drawer.drawArrow(Point2D{150.0, 150.0}, Point2D{250.0, 50.0}, true, 0.05,
|
||||
M_PI / 4, DrawColour(0.0, 1.0, 0.0), true);
|
||||
drawer.drawArrow(Point2D{150.0, 150.0}, Point2D{50.0, 250.0}, false, 0.05,
|
||||
M_PI / 4, DrawColour(0.0, 1.0, 0.0), true);
|
||||
drawer.finishDrawing();
|
||||
|
||||
auto text = drawer.getDrawingText();
|
||||
std::ofstream outs("testArrowheads.svg");
|
||||
outs << text;
|
||||
outs.close();
|
||||
// Checking that the line ends are where they were in an SVG that looked
|
||||
// right on visual inspection.
|
||||
const static std::regex heads(
|
||||
"<path d='M (\\d+\\.\\d+),(\\d+\\.\\d+) L (\\d+\\.\\d+),(\\d+\\.\\d+) L (\\d+\\.\\d+),(\\d+\\.\\d+).*00FF00");
|
||||
std::ptrdiff_t const match_count_heads(
|
||||
std::distance(std::sregex_iterator(text.begin(), text.end(), heads),
|
||||
std::sregex_iterator()));
|
||||
CHECK(match_count_heads == 4);
|
||||
auto match_begin = std::sregex_iterator(text.begin(), text.end(), heads);
|
||||
std::smatch match = *match_begin;
|
||||
std::vector<std::pair<double, double>> expVals{
|
||||
{246.0, 246.0},
|
||||
{54.0, 54.0},
|
||||
{249.0, 51.0},
|
||||
{51.0, 249.0},
|
||||
};
|
||||
for (int i = 0; i < 4; ++i, ++match_begin) {
|
||||
std::smatch match = *match_begin;
|
||||
CHECK_THAT(std::stod(match[3]),
|
||||
Catch::Matchers::WithinAbs(expVals[i].first, 1.0e-4));
|
||||
CHECK_THAT(std::stod(match[4]),
|
||||
Catch::Matchers::WithinAbs(expVals[i].second, 1.0e-4));
|
||||
}
|
||||
check_file_hash("testArrowheads.svg");
|
||||
}
|
||||
Reference in New Issue
Block a user