Implement returnDrawCoords JSON option in MinimalLib to return 2D drawing coordinates when SVG or canvas depictions are generated (#8815)

Co-authored-by: ptosco <paolo.tosco@novartis.com>
This commit is contained in:
Paolo Tosco
2025-09-29 09:36:06 +02:00
committed by GitHub
parent 56df4de046
commit 24b2039040
6 changed files with 209 additions and 7 deletions

View File

@@ -3980,6 +3980,75 @@ M END\n";
free(pkl);
}
void test_return_draw_coords() {
const char *mb =
"\n\
RDKit 2D\n\
\n\
3 3 0 0 0 0 0 0 0 0999 V2000\n\
0.0000 0.8930 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n\
0.7734 -0.4465 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n\
-0.7734 -0.4465 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n\
1 2 1 0\n\
2 3 1 0\n\
1 3 1 0\n\
M END\n";
char *pkl;
char *res;
size_t pkl_size;
size_t i;
unsigned int above_tol;
double reference[6];
double highlight[6];
printf("--------------------------\n");
printf(" test_return_draw_coords\n");
pkl = get_mol(mb, &pkl_size, "");
assert(pkl && pkl_size);
res = get_svg(
pkl, pkl_size,
"{\"width\":300,\"height\":200,\"padding\":0.2,\"returnDrawCoords\":true}");
assert(res);
assert(strstr(res, "\"drawCoords\":"));
assert(strstr(res, "\"svg\":"));
sscanf(res, "{\"drawCoords\":[[%lf,%lf],[%lf,%lf],[%lf,%lf]]", &reference[0],
&reference[1], &reference[2], &reference[3], &reference[4],
&reference[5]);
free(res);
res = get_svg(
pkl, pkl_size,
"{\"width\":300,\"height\":200,\"padding\":0.2,\"atoms\":[0],\"returnDrawCoords\":true}");
assert(res);
assert(strstr(res, "\"drawCoords\":"));
assert(strstr(res, "\"svg\":"));
sscanf(res, "{\"drawCoords\":[[%lf,%lf],[%lf,%lf],[%lf,%lf]]", &highlight[0],
&highlight[1], &highlight[2], &highlight[3], &highlight[4],
&highlight[5]);
above_tol = 0;
for (i = 0; !above_tol && i < 6; ++i) {
above_tol = (fabs(reference[i] - highlight[i]) > 0.1);
}
assert(above_tol);
free(res);
res = get_svg(
pkl, pkl_size,
"{\"width\":300,\"height\":200,\"padding\":0.2,\"atoms\":[0],\"returnDrawCoords\":true,\"drawingExtentsInclude\":{\"ALL\":true,\"HIGHLIGHTS\":false}}");
assert(res);
assert(strstr(res, "\"drawCoords\":"));
assert(strstr(res, "\"svg\":"));
sscanf(res, "{\"drawCoords\":[[%lf,%lf],[%lf,%lf],[%lf,%lf]]", &highlight[0],
&highlight[1], &highlight[2], &highlight[3], &highlight[4],
&highlight[5]);
above_tol = 0;
for (i = 0; !above_tol && i < 6; ++i) {
above_tol = (fabs(reference[i] - highlight[i]) > 0.1);
}
assert(!above_tol);
free(res);
free(pkl);
}
int main() {
enable_logging();
char *vers = version();
@@ -4021,5 +4090,6 @@ int main() {
test_get_mol_remove_hs();
test_png_metadata();
test_drawing_extents_include();
test_return_draw_coords();
return 0;
}

View File

@@ -498,6 +498,7 @@ std::string process_details(rj::Document &doc, const std::string &details,
GET_JSON_VALUE(doc, drawingDetails, forceCoords, Bool)
GET_JSON_VALUE(doc, drawingDetails, wavyBonds, Bool)
GET_JSON_VALUE(doc, drawingDetails, useMolBlockWedging, Bool)
GET_JSON_VALUE(doc, drawingDetails, returnDrawCoords, Bool)
return "";
}
@@ -756,7 +757,12 @@ class SVGDrawerFromDetails : public DrawerFromDetails {
std::string finalizeDrawing() {
CHECK_INVARIANT(d_drawer, "d_drawer must not be null");
d_drawer->finishDrawing();
return d_drawer->getDrawingText();
auto svg = d_drawer->getDrawingText();
return createDrawingResult(svg);
}
const char *getDrawingResultKey() {
static const char *SVG_KEY = "svg";
return SVG_KEY;
}
std::unique_ptr<MolDraw2DSVG> d_drawer;
};

View File

@@ -11,9 +11,14 @@
#include <GraphMol/MolDraw2D/MolDraw2DHelpers.h>
#include <GraphMol/MolDraw2D/MolDraw2DUtils.h>
#include <GraphMol/Chirality.h>
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <string>
#include <vector>
namespace rj = rapidjson;
namespace RDKit {
namespace MinimalLib {
@@ -31,6 +36,7 @@ struct DrawingDetails {
bool forceCoords = false;
bool wavyBonds = false;
bool useMolBlockWedging = false;
bool returnDrawCoords = false;
std::string legend;
std::vector<int> atomIds;
std::vector<int> bondIds;
@@ -125,6 +131,13 @@ class DrawerFromDetails {
molDrawingDetails.bondMultiMap, molDrawingDetails.radiiMap,
molDrawingDetails.lineWidthMultiplierMap);
}
if (molDrawingDetails.returnDrawCoords) {
d_drawCoords.reset(new std::vector<RDGeom::Point2D>());
d_drawCoords->reserve(molPtr->getNumAtoms());
for (size_t i = 0; i < molPtr->getNumAtoms(); ++i) {
d_drawCoords->push_back(drawer().getDrawCoords(i));
}
}
return finalizeDrawing();
}
std::string draw_rxn(const ChemicalReaction &rxn) {
@@ -162,14 +175,42 @@ class DrawerFromDetails {
MolDraw2DUtils::updateDrawerParamsFromJSON(drawer(), d_details);
}
}
std::string createDrawingResult(const std::string &res) {
if (!d_drawCoords) {
return res;
}
rj::Document doc;
doc.SetObject();
rj::Value rjDrawCoords(rj::kArrayType);
for (const auto &drawXY : *d_drawCoords) {
rj::Value rjXY(rj::kArrayType);
rjXY.PushBack(drawXY.x, doc.GetAllocator());
rjXY.PushBack(drawXY.y, doc.GetAllocator());
rjDrawCoords.PushBack(rjXY, doc.GetAllocator());
}
doc.AddMember("drawCoords", rjDrawCoords, doc.GetAllocator());
const auto drawingResultKey = getDrawingResultKey();
if (drawingResultKey) {
doc.AddMember(rj::StringRef(drawingResultKey),
rj::Value(res.c_str(), doc.GetAllocator()),
doc.GetAllocator());
}
rj::StringBuffer buffer;
rj::Writer<rj::StringBuffer> writer(buffer);
writer.SetMaxDecimalPlaces(5);
doc.Accept(writer);
return buffer.GetString();
}
private:
virtual MolDraw2D &drawer() const = 0;
virtual void initDrawer(const DrawingDetails &drawingDetails) = 0;
virtual std::string finalizeDrawing() = 0;
virtual const char *getDrawingResultKey() { return nullptr; };
int d_width;
int d_height;
std::string d_details;
std::unique_ptr<std::vector<RDGeom::Point2D>> d_drawCoords;
};
} // namespace MinimalLib

View File

@@ -28,7 +28,7 @@
rxn_callback("[C:1](=[O:2])-O.[N:3]>>[O:2]=[C:1]-[N:3]");
});
function form_to_details(details) {
var controls = ["addAtomIndices", "addBondIndices", "explicitMethyl", "addStereoAnnotation"];
var controls = ["addAtomIndices", "addBondIndices", "explicitMethyl", "addStereoAnnotation", "returnDrawCoords"];
for (i in controls) {
var control = controls[i];
details[control] = document.getElementById(control).checked
@@ -38,17 +38,33 @@
var control = texts[i];
details[control] = document.getElementById(control).value
}
if (document.getElementById("ignoreIndicesWhenCentering").checked) {
details.drawingExtentsInclude = { ALL: true, ANNOTATIONS: false };
details.padding = 0.1;
} else {
delete details.drawingExtentsInclude;
delete details.padding;
}
}
function draw_with_highlights(mol, details) {
form_to_details(details);
var tdetails = JSON.stringify(details)
var svg = mol.get_svg_with_highlights(tdetails);
if (svg == "") return;
var res = mol.get_svg_with_highlights(tdetails);
if (res == "") return;
if (details.returnDrawCoords) {
var {drawCoords, svg} = JSON.parse(res);
console.log(`SVG drawCoords: ${JSON.stringify(drawCoords)}`);
} else {
var svg = res;
}
var ob = document.getElementById("drawing");
ob.outerHTML = "<div id='drawing'>" + svg + "</div>";
var canvas = document.getElementById("rdkit-canvas");
mol.draw_to_canvas_with_highlights(canvas, tdetails);
res = mol.draw_to_canvas_with_highlights(canvas, tdetails);
if (details.returnDrawCoords) {
var {drawCoords} = JSON.parse(res);
console.log(`canvas drawCoords: ${JSON.stringify(drawCoords)}`);
}
}
function draw(mol) {
var details = {};
@@ -171,6 +187,11 @@
<br />
<input type="checkbox" id="explicitMethyl" name="explicitMethyl" onclick="option_changed(this);" />
<label for="explicitMethyl">explicitMethyl</label>
<input type="checkbox" id="ignoreIndicesWhenCentering" name="ignoreIndicesWhenCentering" onclick="option_changed(this);" />
<label for="ignoreIndicesWhenCentering">ignoreIndicesWhenCentering</label>
<br />
<input type="checkbox" id="returnDrawCoords" name="returnDrawCoords" onclick="option_changed(this);" />
<label for="returnDrawCoords">returnDrawCoords</label>
<br />
<input type="text" id="legend" onkeyup="option_changed(this);"><label for="legend">legend</label>
<input type="text" id="legendFontSize" onkeyup="option_changed(this);"><label

View File

@@ -59,7 +59,7 @@ class JSDrawerFromDetails : public MinimalLib::DrawerFromDetails {
drawingDetails.noFreetype));
updateDrawerParamsFromJSON();
}
std::string finalizeDrawing() { return ""; }
std::string finalizeDrawing() { return createDrawingResult(""); }
std::unique_ptr<MolDraw2DJS> d_drawer;
emscripten::val d_ctx;
};

View File

@@ -4163,6 +4163,69 @@ function test_get_coords() {
}
}
function test_return_draw_coords() {
var mb = `
RDKit 2D
3 3 0 0 0 0 0 0 0 0999 V2000
0.0000 0.8930 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
0.7734 -0.4465 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
-0.7734 -0.4465 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
1 2 1 0
2 3 1 0
1 3 1 0
M END`;
var reference;
var highlight;
var aboveTol;
var mol = RDKitModule.get_mol(mb);
assert(mol);
res = mol.get_svg_with_highlights(JSON.stringify({
width: 300,
height: 200,
padding: 0.2,
returnDrawCoords: true
}));
assert(res);
res = JSON.parse(res);
assert(res.drawCoords);
assert(res.svg);
reference = res.drawCoords.flat();
res = mol.get_svg_with_highlights(JSON.stringify({
width: 300,
height: 200,
padding: 0.2,
atoms: [0],
returnDrawCoords: true
}));
assert(res);
res = JSON.parse(res);
assert(res.drawCoords);
assert(res.svg);
highlight = res.drawCoords.flat();
aboveTol = reference.some((ref, i) => (Math.abs(ref - highlight[i]) > 0.1));
assert(aboveTol);
res = mol.get_svg_with_highlights(JSON.stringify({
width: 300,
height: 200,
padding: 0.2,
atoms: [0],
returnDrawCoords: true,
drawingExtentsInclude: {
ALL: true,
HIGHLIGHTS: false
}
}));
assert(res);
res = JSON.parse(res);
assert(res.drawCoords);
assert(res.svg);
highlight = res.drawCoords.flat();
aboveTol = reference.some((ref, i) => (Math.abs(ref - highlight[i]) > 0.1));
assert(!aboveTol);
mol.delete();
}
initRDKitModule().then(function(instance) {
var done = {};
const waitAllTestsFinished = () => {
@@ -4260,6 +4323,7 @@ initRDKitModule().then(function(instance) {
test_png_metadata();
test_combine_with();
test_get_coords();
test_return_draw_coords();
waitAllTestsFinished().then(() =>
console.log("Tests finished successfully")