use crate::{
AddHsParams, Atom, AtomId, Bond, BondDirection, BondOrder, ChiralTag, Molecule, ValenceModel,
assign_valence,
};
use glam::DVec2;
use std::collections::HashSet;
use std::sync::{Arc, OnceLock};
const EMBEDDED_DRAW_FONT_BYTES: &[u8] = include_bytes!("../../assets/fonts/NotoSans-Regular.ttf");
const EMBEDDED_DRAW_FONT_FAMILY: &str = "Noto Sans";
fn embedded_draw_font_data() -> Arc<dyn AsRef<[u8]> + Send + Sync> {
static FONT_DATA: OnceLock<Arc<Vec<u8>>> = OnceLock::new();
FONT_DATA
.get_or_init(|| Arc::new(EMBEDDED_DRAW_FONT_BYTES.to_vec()))
.clone() as Arc<dyn AsRef<[u8]> + Send + Sync>
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SvgDrawError {
#[error("coordinate generation failed: {0}")]
CoordinateGeneration(String),
#[error("unsupported drawing path: {0}")]
Unsupported(String),
#[error(transparent)]
UnsupportedFeature(#[from] crate::UnsupportedFeatureError),
#[error("SVG parse failed: {0}")]
SvgParse(String),
#[error("PNG pixmap allocation failed for {width}x{height}")]
PixmapAllocation { width: u32, height: u32 },
#[error("PNG encoding failed: {0}")]
PngEncode(String),
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct PreparedDrawAtom {
pub index: usize,
pub atomic_number: u8,
pub x: f64,
pub y: f64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PreparedDrawBond {
pub index: usize,
pub begin_atom: usize,
pub end_atom: usize,
pub bond_order: BondOrder,
pub is_aromatic: bool,
pub direction: BondDirection,
pub rdkit_direction_name: String,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct PreparedDrawMolecule {
pub atoms: Vec<PreparedDrawAtom>,
pub bonds: Vec<PreparedDrawBond>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct DrawColour {
r: f64,
g: f64,
b: f64,
a: f64,
}
impl DrawColour {
const fn new(r: f64, g: f64, b: f64) -> Self {
Self { r, g, b, a: 1.0 }
}
const fn with_alpha(self, a: f64) -> Self {
Self { a, ..self }
}
}
#[derive(Debug, Clone)]
struct DrawOptions {
padding: f64,
multiple_bond_offset: f64,
bond_line_width: f64,
scale_bond_width: bool,
split_bonds: bool,
clear_background: bool,
background_colour: DrawColour,
query_colour: DrawColour,
flag_close_contacts_dist: i32,
annotation_font_scale: f64,
atom_note_colour: DrawColour,
bond_note_colour: DrawColour,
annotation_colour: DrawColour,
dummies_are_attachments: bool,
variable_attachment_colour: DrawColour,
variable_atom_radius: f64,
variable_bond_width_multiplier: f64,
include_annotations: bool,
add_stereo_annotation: bool,
include_radicals: bool,
circle_atoms: bool,
continuous_highlight: bool,
}
impl Default for DrawOptions {
fn default() -> Self {
Self {
padding: 0.05,
multiple_bond_offset: 0.15,
bond_line_width: 2.0,
scale_bond_width: false,
split_bonds: false,
clear_background: true,
background_colour: DrawColour::new(1.0, 1.0, 1.0),
query_colour: DrawColour::new(0.0, 0.0, 0.0),
flag_close_contacts_dist: 3,
annotation_font_scale: 0.8,
atom_note_colour: DrawColour::new(0.0, 0.0, 1.0),
bond_note_colour: DrawColour::new(1.0, 0.0, 0.0),
annotation_colour: DrawColour::new(0.0, 0.0, 0.0),
dummies_are_attachments: false,
variable_attachment_colour: DrawColour::new(0.5, 0.5, 0.5),
variable_atom_radius: 0.3,
variable_bond_width_multiplier: 2.0,
include_annotations: true,
add_stereo_annotation: false,
include_radicals: true,
circle_atoms: false,
continuous_highlight: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OrientType {
C,
N,
E,
S,
W,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TextAlignType {
Middle,
Start,
End,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TextDrawType {
Normal,
Subscript,
Superscript,
}
#[derive(Debug, Clone)]
struct StringRect {
ch: char,
draw_mode: TextDrawType,
trans: DVec2,
offset: DVec2,
g_centre: DVec2,
y_shift: f64,
width: f64,
height: f64,
rect_corr: f64,
}
impl StringRect {
fn calc_centre(&self) -> DVec2 {
let mut c = self.trans + self.g_centre - self.offset;
c.y -= self.y_shift;
c
}
fn calc_corners(&self, padding: f64) -> (DVec2, DVec2, DVec2, DVec2) {
let wb2 = padding + self.width / 2.0;
let hb2 = padding + self.height / 2.0;
let c = self.calc_centre();
(
DVec2::new(c.x - wb2, c.y - hb2),
DVec2::new(c.x + wb2, c.y - hb2),
DVec2::new(c.x + wb2, c.y + hb2),
DVec2::new(c.x - wb2, c.y + hb2),
)
}
fn is_point_inside(&self, pt: DVec2, padding: f64) -> bool {
let (mut tl, mut tr, mut br, mut bl) = self.calc_corners(padding);
if tl.y < bl.y {
std::mem::swap(&mut tl, &mut bl);
std::mem::swap(&mut tr, &mut br);
}
pt.x >= tl.x && pt.x <= br.x && pt.y >= br.y && pt.y <= tl.y
}
fn does_it_intersect(&self, other: &StringRect, padding: f64) -> bool {
let (mut ttl, mut ttr, mut tbr, mut tbl) = self.calc_corners(padding);
if ttl.y < tbl.y {
std::mem::swap(&mut ttl, &mut tbl);
std::mem::swap(&mut ttr, &mut tbr);
}
let (mut otl, mut otr, mut obr, mut obl) = other.calc_corners(padding);
if otl.y < obl.y {
std::mem::swap(&mut otl, &mut obl);
std::mem::swap(&mut otr, &mut obr);
}
if (otl.x >= ttl.x && otl.x <= ttr.x && otl.y >= tbl.y && otl.y <= ttl.y)
|| (otr.x >= ttl.x && otr.x <= ttr.x && otr.y >= tbl.y && otr.y <= ttl.y)
|| (obr.x >= ttl.x && obr.x <= ttr.x && obr.y >= tbl.y && obr.y <= ttl.y)
|| (obl.x >= ttl.x && obl.x <= ttr.x && obl.y >= tbl.y && obl.y <= ttl.y)
{
return true;
}
(ttl.x >= otl.x && ttl.x <= otr.x && ttl.y >= obl.y && ttl.y <= otl.y)
|| (ttr.x >= otl.x && ttr.x <= otr.x && ttr.y >= obl.y && ttr.y <= otl.y)
|| (tbr.x >= otl.x && tbr.x <= otr.x && tbr.y >= obl.y && tbr.y <= otl.y)
|| (tbl.x >= otl.x && tbl.x <= otr.x && tbl.y >= obl.y && tbl.y <= otl.y)
}
}
#[derive(Debug, Clone)]
struct AtomLabel {
symbol: String,
atom_idx: usize,
atomic_num: u8,
orient: OrientType,
cds: DVec2,
colour: DrawColour,
rects: Vec<StringRect>,
}
impl AtomLabel {
fn new(
symbol: impl Into<String>,
atom_idx: usize,
atomic_num: u8,
orient: OrientType,
cds: DVec2,
colour: DrawColour,
font_size: f64,
) -> Self {
let symbol = symbol.into();
let mut rects = get_string_rects(&symbol, orient, font_size);
adjust_colons(&symbol, &mut rects);
Self {
symbol,
atom_idx,
atomic_num,
orient,
cds,
colour,
rects,
}
}
fn recalculate_rects(&mut self, font_size: f64) {
self.rects = get_string_rects(&self.symbol, self.orient, font_size);
adjust_colons(&self.symbol, &mut self.rects);
}
fn find_extremes(&self, xmin: &mut f64, xmax: &mut f64, ymin: &mut f64, ymax: &mut f64) {
for rect in &self.rects {
let shifted = StringRect {
trans: rect.trans + self.cds,
..rect.clone()
};
let (tl, tr, br, bl) = shifted.calc_corners(0.0);
for pt in [tl, tr, br, bl] {
if pt.x < *xmin {
*xmin = pt.x;
}
if pt.x > *xmax {
*xmax = pt.x;
}
if pt.y < *ymin {
*ymin = pt.y;
}
if pt.y > *ymax {
*ymax = pt.y;
}
}
}
}
fn does_rect_clash(&self, rect: &StringRect, padding: f64) -> bool {
for alrect in &self.rects {
let mut shifted = alrect.clone();
shifted.trans += self.cds;
if shifted.does_it_intersect(rect, padding) {
return true;
}
}
false
}
}
fn adjust_colons(symbol: &str, rects: &mut [StringRect]) {
if symbol.is_empty() {
return;
}
let mut tmp_sym = symbol.to_string();
while let Some(lt_pos) = tmp_sym.find('<') {
let Some(gt_pos) = tmp_sym.find('>') else {
break;
};
tmp_sym = tmp_sym[..lt_pos].to_string() + &tmp_sym[gt_pos + 1..];
}
let Some(colon_pos) = tmp_sym.find(':') else {
return;
};
if colon_pos >= rects.len() {
return;
}
let left_height = if colon_pos > 0 {
rects[colon_pos - 1].height
} else {
0.0
};
let right_height = if colon_pos < symbol.len() - 1 && colon_pos + 1 < rects.len() {
rects[colon_pos + 1].height
} else {
0.0
};
rects[colon_pos].height = left_height.min(right_height);
}
type DashPattern = Vec<f64>;
#[derive(Debug, Clone)]
struct DrawLine {
begin: DVec2,
end: DVec2,
colour: DrawColour,
width: f64,
scale_width: bool,
dash_pattern: DashPattern,
atom1_idx: usize,
atom2_idx: usize,
bond_idx: usize,
}
impl DrawLine {
fn kind_is_simple(&self) -> bool {
self.dash_pattern.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WedgeKind {
Solid,
Dashed,
}
#[derive(Debug, Clone)]
struct DrawWedge {
points: Vec<DVec2>,
col1: DrawColour,
col2: DrawColour,
width: f64,
kind: WedgeKind,
one_less_dash: bool,
split_bonds: bool,
atom1_idx: usize,
atom2_idx: usize,
bond_idx: usize,
}
#[derive(Debug, Clone, Copy)]
enum DrawBondShapeRef {
Line(usize),
Wedge(usize),
Arrow(usize),
}
#[derive(Debug, Clone)]
struct DrawPolyline {
points: Vec<DVec2>,
colour: DrawColour,
width: f64,
scale_width: bool,
fill_polys: bool,
atom1_idx: Option<usize>,
atom2_idx: Option<usize>,
bond_idx: Option<usize>,
}
#[derive(Debug, Clone)]
struct DrawArrow {
begin: DVec2,
end: DVec2,
colour: DrawColour,
width: f64,
scale_width: bool,
frac: f64,
angle: f64,
atom1_idx: usize,
atom2_idx: usize,
bond_idx: usize,
}
#[derive(Debug, Clone)]
struct DrawRadical {
rect: StringRect,
orient: OrientType,
atom_idx: usize,
count: u8,
}
#[derive(Debug, Clone)]
struct DrawAnnotation {
text: String,
align: TextAlignType,
class_: String,
font_scale: f64,
pos: DVec2,
colour: DrawColour,
rects: Vec<StringRect>,
}
impl DrawAnnotation {
fn new(
text: String,
align: TextAlignType,
class_: String,
font_scale: f64,
pos: DVec2,
colour: DrawColour,
font_size: f64,
) -> Self {
let rects = get_string_rects(&text, OrientType::C, font_size * font_scale);
Self {
text,
align,
class_,
font_scale,
pos,
colour,
rects,
}
}
fn find_extremes(&self, xmin: &mut f64, xmax: &mut f64, ymin: &mut f64, ymax: &mut f64) {
for rect in &self.rects {
let shifted = StringRect {
trans: rect.trans + self.pos,
..rect.clone()
};
let (tl, tr, br, bl) = shifted.calc_corners(0.0);
*xmin = xmin.min(tr.x).min(bl.x);
*ymin = ymin.min(tr.y).min(bl.y);
*xmax = xmax.max(tr.x).max(bl.x);
*ymax = ymax.max(tr.y).max(bl.y);
}
}
}
fn atom_colour(atomic_num: i32) -> DrawColour {
match atomic_num {
0 => DrawColour::new(0.1, 0.1, 0.1),
1 | 6 => DrawColour::new(0.0, 0.0, 0.0),
7 => DrawColour::new(0.0, 0.0, 1.0),
8 => DrawColour::new(1.0, 0.0, 0.0),
9 => DrawColour::new(0.2, 0.8, 0.8),
15 => DrawColour::new(1.0, 0.5, 0.0),
16 => DrawColour::new(0.8, 0.8, 0.0),
17 => DrawColour::new(0.0, 0.802, 0.0),
35 => DrawColour::new(0.5, 0.3, 0.1),
53 => DrawColour::new(0.63, 0.12, 0.94),
201 => DrawColour::new(0.68, 0.85, 0.90),
_ => DrawColour::new(0.0, 0.0, 0.0),
}
}
fn draw_colour_to_svg(col: DrawColour) -> String {
let convert: &[u8] = b"0123456789ABCDEF";
let has_alpha = (1.0 - col.a).abs() > 1e-3;
let mut res = if has_alpha {
vec![b'#'; 9]
} else {
vec![b'#'; 7]
};
let mut i = 1usize;
let v = |c: f64| -> usize { (c * 255.0).clamp(0.0, 255.0) as usize };
let ri = v(col.r);
res[i] = convert[ri / 16];
i += 1;
res[i] = convert[ri % 16];
i += 1;
let gi = v(col.g);
res[i] = convert[gi / 16];
i += 1;
res[i] = convert[gi % 16];
i += 1;
let bi = v(col.b);
res[i] = convert[bi / 16];
i += 1;
res[i] = convert[bi % 16];
i += 1;
if has_alpha {
let ai = v(col.a);
res[i] = convert[ai / 16];
i += 1;
res[i] = convert[ai % 16];
}
String::from_utf8(res).unwrap()
}
fn format_double(value: f64) -> String {
format!("{value:.1}")
}
fn format_svg_font_size_px(value: f64) -> String {
format!("{}", value as u32)
}
fn xml_escape(text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn calc_perpendicular(begin: DVec2, end: DVec2) -> DVec2 {
let bvx = begin.x - end.x;
let bvy = begin.y - end.y;
let perp_x = -bvy;
let perp_y = bvx;
let len = perp_x.hypot(perp_y);
if len < 1e-8 {
return DVec2::new(0.0, 0.0);
}
DVec2::new(perp_x / len, perp_y / len)
}
fn calc_inner_perpendicular(cds1: DVec2, cds2: DVec2, cds3: DVec2) -> DVec2 {
let mut perp = calc_perpendicular(cds1, cds2);
let v1 = DVec2::new(cds1.x - cds2.x, cds1.y - cds2.y);
let v2 = DVec2::new(cds2.x - cds3.x, cds2.y - cds3.y);
let obv = v1 - v2;
if obv.dot(perp) < 0.0 {
perp *= -1.0;
}
perp
}
fn direction_vector(from: DVec2, to: DVec2) -> DVec2 {
(to - from).normalize_or_zero()
}
fn angle_to(v1: DVec2, v2: DVec2) -> f64 {
v1.angle_to(v2)
}
fn cross(a: DVec2, b: DVec2) -> f64 {
a.x * b.y - a.y * b.x
}
fn line_intersection(a1: DVec2, a2: DVec2, b1: DVec2, b2: DVec2) -> Option<DVec2> {
let s1_x = a2.x - a1.x;
let s1_y = a2.y - a1.y;
let s2_x = b2.x - b1.x;
let s2_y = b2.y - b1.y;
let d = -s2_x * s1_y + s1_x * s2_y;
if d == 0.0 {
return None;
}
let s = (-s1_y * (a1.x - b1.x) + s1_x * (a1.y - b1.y)) / d;
let t = (s2_x * (a1.y - b1.y) - s2_y * (a1.x - b1.x)) / d;
if (0.0..=1.0).contains(&s) && (0.0..=1.0).contains(&t) {
Some(DVec2::new(a1.x + t * s1_x, a1.y + t * s1_y))
} else {
None
}
}
fn debug_target_bond() -> Option<usize> {
std::env::var("COSMOL_DEBUG_BOND")
.ok()
.and_then(|v| v.parse::<usize>().ok())
}
fn debug_svg_bond_enabled(bond_idx: usize) -> bool {
debug_target_bond() == Some(bond_idx)
}
fn debug_svg_bond_log(message: impl std::fmt::Display) {
if debug_target_bond().is_some() {
eprintln!("{message}");
}
}
fn transform_point(point: DVec2, trans: DVec2, scale: DVec2, to_centre: DVec2) -> DVec2 {
let mut ret_pt = point;
ret_pt += trans;
ret_pt.x *= scale.x;
ret_pt.y *= scale.y;
ret_pt + to_centre
}
fn atom_degree(mol: &Molecule, atom_idx: usize) -> usize {
mol.bonds()
.iter()
.filter(|b| b.begin().index() == atom_idx || b.end().index() == atom_idx)
.count()
}
fn atom_neighbors(mol: &Molecule, atom_idx: usize) -> Vec<usize> {
let mut ns = Vec::new();
for b in mol.bonds() {
if b.begin().index() == atom_idx {
ns.push(b.end().index());
} else if b.end().index() == atom_idx {
ns.push(b.begin().index());
}
}
ns
}
fn are_bonds_trans(at1: DVec2, at2: DVec2, at3: DVec2, at4: DVec2) -> bool {
let v21 = at1 - at2;
let v34 = at4 - at3;
v21.dot(v34) < 0.0
}
fn are_bonds_parallel(at1: DVec2, at2: DVec2, at3: DVec2, at4: DVec2, tol: f64) -> bool {
let v21 = direction_vector(at1, at2);
let v34 = direction_vector(at4, at3);
(1.0 - v21.dot(v34).abs()).abs() < tol
}
fn other_neighbor(
mol: &Molecule,
first_atom_idx: usize,
second_atom_idx: usize,
nbor_num: usize,
) -> Option<usize> {
let mut nbour_count = 0usize;
for nbr in atom_neighbors(mol, first_atom_idx) {
if nbr != second_atom_idx {
if nbour_count == nbor_num {
return Some(nbr);
}
nbour_count += 1;
}
}
None
}
fn bond_between_atoms(mol: &Molecule, a1: usize, a2: usize) -> Option<&Bond> {
mol.bonds().iter().find(|b| {
(b.begin().index() == a1 && b.end().index() == a2)
|| (b.begin().index() == a2 && b.end().index() == a1)
})
}
fn get_wavy_line_segments(
p1: DVec2,
p2: DVec2,
n_segments: usize,
vert_offset: f64,
) -> Vec<(DVec2, DVec2, DVec2, DVec2)> {
let mut n = if n_segments < 2 { 2 } else { n_segments };
if n % 2 == 1 {
n += 1; }
let delta = (p2 - p1) / n as f64;
let mut perp = DVec2::new(p2.y - p1.y, -(p2.x - p1.x));
let perp_len = perp.length();
if perp_len > 1e-8 {
perp /= perp_len;
perp *= vert_offset;
}
let mut res = Vec::with_capacity(n);
for i in 0..n {
let startpt = p1 + delta * i as f64;
let segpt = startpt + delta;
let sign = if i % 2 == 0 { 1.0 } else { -1.0 };
let cpt1 = startpt + perp * sign;
let cpt2 = segpt + perp * sign;
res.push((startpt, cpt1, cpt2, segpt));
}
res
}
const RDKIT_CHAR_WIDTHS: [i32; 256] = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
278, 278, 355, 556, 556, 889, 667, 222, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556,
556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556, 1015, 667, 667, 722, 722, 667,
611, 778, 722, 278, 500, 667, 556, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667,
667, 611, 278, 278, 278, 469, 556, 222, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500,
222, 833, 556, 556, 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 333, 556, 556, 167, 556, 556, 556, 556, 191, 333, 556, 333, 333, 500, 500, 0, 556,
556, 556, 278, 0, 537, 350, 222, 333, 333, 556, 1000, 1000, 0, 611, 0, 333, 333, 333, 333, 333,
333, 333, 333, 0, 333, 333, 0, 333, 333, 333, 1000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 1000, 0, 370, 0, 0, 0, 0, 556, 778, 1000, 365, 0, 0, 0, 0, 0, 889, 0, 0, 0, 278, 0, 0,
222, 611, 944, 611, 0, 0, 834,
];
fn char_width(ch: char) -> i32 {
let code = ch as u32;
if code < 256 {
RDKIT_CHAR_WIDTHS[code as usize]
} else {
0
}
}
fn select_scale_factor(ch: char, draw_type: TextDrawType) -> f64 {
match draw_type {
TextDrawType::Normal => 1.0,
TextDrawType::Subscript => 0.66,
TextDrawType::Superscript => {
if ch == '+' || ch == '-' {
0.66
} else {
0.66
}
}
}
}
fn parse_draw_chars(text: &str) -> (Vec<char>, Vec<TextDrawType>) {
let mut chars = Vec::new();
let mut modes = Vec::new();
let mut i = 0;
let bytes = text.as_bytes();
let mut current_mode = TextDrawType::Normal;
while i < text.len() {
if bytes[i] == b'<' {
if text[i..].starts_with("<sup>") {
current_mode = TextDrawType::Superscript;
i += 5;
continue;
} else if text[i..].starts_with("</sup>") {
current_mode = TextDrawType::Normal;
i += 6;
continue;
} else if text[i..].starts_with("<sub>") {
current_mode = TextDrawType::Subscript;
i += 5;
continue;
} else if text[i..].starts_with("</sub>") {
current_mode = TextDrawType::Normal;
i += 6;
continue;
} else if text[i..].starts_with("<lit>") {
i += 5;
continue;
} else if text[i..].starts_with("</lit>") {
i += 6;
continue;
}
}
if bytes[i] == b'&' {
if text[i..].starts_with("&") {
chars.push('&');
modes.push(current_mode);
i += 5;
continue;
} else if text[i..].starts_with("<") {
chars.push('<');
modes.push(current_mode);
i += 4;
continue;
} else if text[i..].starts_with(">") {
chars.push('>');
modes.push(current_mode);
i += 4;
continue;
}
}
let ch = text[i..].chars().next().unwrap();
chars.push(ch);
modes.push(current_mode);
i += ch.len_utf8();
}
(chars, modes)
}
fn adjust_string_rects_for_super_subscript(draw_modes: &[TextDrawType], rects: &mut [StringRect]) {
let mut last_char: isize = -1;
for i in 0..draw_modes.len() {
match draw_modes[i] {
TextDrawType::Superscript => {
if last_char < 0 {
for (j, mode) in draw_modes.iter().enumerate().skip(i + 1) {
if *mode == TextDrawType::Normal {
last_char = j as isize;
break;
}
}
}
if last_char >= 0 {
rects[i].rect_corr = rects[last_char as usize].height;
rects[i].trans.y -= rects[i].rect_corr / 2.0;
}
}
TextDrawType::Subscript => {
if last_char >= 0 {
rects[i].rect_corr = -rects[last_char as usize].height;
rects[i].trans.y -= rects[i].rect_corr / 2.0;
}
}
TextDrawType::Normal => last_char = i as isize,
}
}
for i in 1..rects.len() {
if (draw_modes[i] == TextDrawType::Subscript
&& draw_modes[i - 1] == TextDrawType::Superscript)
|| (draw_modes[i - 1] == TextDrawType::Subscript
&& draw_modes[i] == TextDrawType::Superscript)
{
let move_back = rects[i].trans.x - rects[i - 1].trans.x;
for rect in rects.iter_mut().skip(i) {
rect.trans.x -= move_back;
}
}
}
}
fn get_string_rects_unsplit(text: &str, act_font_size: f64) -> Vec<StringRect> {
let (draw_chars, draw_modes) = parse_draw_chars(text);
let mut rects = Vec::with_capacity(draw_chars.len());
let mut running_x = 0.0;
let mut max_width: f64 = 0.0;
for &ch in &draw_chars {
max_width = max_width.max(char_width(ch) as f64);
}
if max_width <= 0.0 {
return rects;
}
for (idx, &ch) in draw_chars.iter().enumerate() {
let mode = draw_modes[idx];
let mut width = 0.6 * act_font_size * (char_width(ch) as f64) / max_width;
let mut height = if ch == '+' {
0.6 * act_font_size
} else if ch == '-' {
0.4 * act_font_size
} else {
0.8 * act_font_size
};
let cscale = select_scale_factor(ch, mode);
width *= cscale;
height *= cscale;
let mut offset = DVec2::new(width / 2.0, height / 2.0);
if ch == '+' || ch == '-' {
offset.y /= 2.0;
}
let g_centre = DVec2::new(width / 2.0, height / 2.0);
rects.push(StringRect {
ch,
draw_mode: mode,
trans: DVec2::new(running_x, 0.0),
offset,
g_centre,
y_shift: 0.0,
width,
height,
rect_corr: 0.0,
});
running_x += if mode != TextDrawType::Normal {
width * 1.05
} else {
width * 1.15
};
}
for rect in &mut rects {
rect.g_centre.y = act_font_size - rect.g_centre.y;
rect.offset.y = act_font_size / 2.0;
}
adjust_string_rects_for_super_subscript(&draw_modes, &mut rects);
rects
}
fn align_string(align: TextAlignType, draw_modes: &[TextDrawType], rects: &mut [StringRect]) {
if rects.is_empty() {
return;
}
let mut talign = align;
if talign == TextAlignType::Middle
&& draw_modes
.iter()
.filter(|mode| **mode == TextDrawType::Normal)
.count()
== 1
{
talign = TextAlignType::Start;
}
let mut align_trans = DVec2::ZERO;
let mut align_offset = DVec2::ZERO;
if talign == TextAlignType::Start || talign == TextAlignType::End {
let mut align_char = 0usize;
for (i, mode) in draw_modes.iter().enumerate() {
if *mode == TextDrawType::Normal {
align_char = i;
if talign == TextAlignType::Start {
break;
}
}
}
align_trans = rects[align_char].trans;
align_offset = rects[align_char].offset;
} else {
let mut x_min = f64::MAX;
let mut x_max = f64::MIN;
let mut num_norm = 0usize;
for (i, rect) in rects.iter().enumerate() {
if draw_modes[i] == TextDrawType::Normal {
let (_tl, tr, _br, bl) = rect.calc_corners(0.0);
x_min = x_min.min(bl.x).min(tr.x);
x_max = x_max.max(bl.x).max(tr.x);
align_offset += rect.offset;
num_norm += 1;
}
}
align_trans.x = (x_max - x_min) / 2.0;
align_trans.y = 0.0;
if num_norm > 0 {
align_offset /= num_norm as f64;
}
}
for rect in rects.iter_mut() {
rect.trans -= align_trans;
rect.offset = align_offset;
}
}
fn atom_label_to_pieces(label: &str, orient: OrientType) -> Vec<String> {
if let Some(mut lit_sym) = label.strip_prefix("<lit>") {
if let Some(idx) = lit_sym.find("</lit>") {
lit_sym = &lit_sym[..idx];
}
return vec![lit_sym.to_string()];
}
let bytes = label.as_bytes();
let mut label_pieces = Vec::new();
let mut next_piece = String::new();
let mut i = 0usize;
loop {
if i == bytes.len() {
if !next_piece.is_empty() {
label_pieces.push(next_piece);
}
break;
}
let split_here = (i + 2 <= bytes.len() && &label[i..i + 2] == "<s")
|| bytes[i] == b':'
|| bytes[i].is_ascii_uppercase();
if split_here && !next_piece.is_empty() {
label_pieces.push(std::mem::take(&mut next_piece));
}
next_piece.push(bytes[i] as char);
i += 1;
}
if label_pieces.len() < 2 {
return label_pieces;
}
if orient == OrientType::E || orient == OrientType::S {
if let Some(pos) = label_pieces
.iter()
.position(|piece| piece == "<sup>+</sup>" || piece == "<sup>-</sup>")
{
let charge = label_pieces.remove(pos);
label_pieces.push(charge);
}
}
let mut final_pieces = Vec::new();
let mut curr_piece = String::new();
let mut had_symbol = false;
for piece in label_pieces {
if piece.is_empty() {
continue;
}
if !piece.as_bytes()[0].is_ascii_uppercase() {
curr_piece.push_str(&piece);
} else if had_symbol {
final_pieces.push(std::mem::take(&mut curr_piece));
curr_piece = piece;
had_symbol = true;
} else {
curr_piece.push_str(&piece);
had_symbol = true;
}
}
if !curr_piece.is_empty() {
final_pieces.push(curr_piece);
}
final_pieces
}
fn get_string_rects(text: &str, orient: OrientType, font_size: f64) -> Vec<StringRect> {
let text_bits = atom_label_to_pieces(text, orient);
if orient == OrientType::W {
let new_label = text_bits.iter().rev().cloned().collect::<String>();
let mut rects = get_string_rects_unsplit(&new_label, font_size);
let draw_modes: Vec<TextDrawType> = rects.iter().map(|r| r.draw_mode).collect();
align_string(TextAlignType::End, &draw_modes, &mut rects);
return rects;
}
if orient == OrientType::E {
let new_label = text_bits.concat();
let mut rects = get_string_rects_unsplit(&new_label, font_size);
let draw_modes: Vec<TextDrawType> = rects.iter().map(|r| r.draw_mode).collect();
align_string(TextAlignType::Start, &draw_modes, &mut rects);
return rects;
}
let mut rects = Vec::new();
let mut running_y = 0.0;
let ta = TextAlignType::Middle;
for tb in text_bits {
let mut t_rects = get_string_rects_unsplit(&tb, font_size);
let t_draw_modes: Vec<TextDrawType> = t_rects.iter().map(|r| r.draw_mode).collect();
align_string(ta, &t_draw_modes, &mut t_rects);
let mut max_height = f64::MIN;
for rect in &mut t_rects {
max_height = max_height.max(rect.height);
rect.y_shift = running_y;
}
rects.extend(t_rects);
if orient == OrientType::N {
running_y -= 1.1 * max_height;
} else if orient == OrientType::S {
running_y += 1.1 * max_height;
}
}
rects
}
fn label_rects_intersect(
rects: &[StringRect],
cds: DVec2,
other: &StringRect,
padding: f64,
) -> bool {
for r in rects {
let shifted = StringRect {
trans: DVec2::new(r.trans.x + cds.x, r.trans.y + cds.y),
width: r.width,
height: r.height,
..r.clone()
};
if shifted.does_it_intersect(other, padding) {
return true;
}
}
false
}
fn do_labels_clash(label1: &AtomLabel, label2: &AtomLabel) -> bool {
for r1 in &label1.rects {
let s1 = StringRect {
trans: DVec2::new(r1.trans.x + label1.cds.x, r1.trans.y + label1.cds.y),
width: r1.width,
height: r1.height,
..r1.clone()
};
if (label1.atom_idx == 59 && label2.atom_idx == 55)
|| (label1.atom_idx == 55 && label2.atom_idx == 59)
{
let (tl, tr, br, bl) = s1.calc_corners(0.0);
eprintln!(
"COSMOL_RUST_LABEL_CLASH_RECT1 atom1={} atom2={} orient1={:?} orient2={:?} trans=({:.17},{:.17}) tl=({:.17},{:.17}) tr=({:.17},{:.17}) br=({:.17},{:.17}) bl=({:.17},{:.17})",
label1.atom_idx,
label2.atom_idx,
label1.orient,
label2.orient,
s1.trans.x,
s1.trans.y,
tl.x,
tl.y,
tr.x,
tr.y,
br.x,
br.y,
bl.x,
bl.y
);
}
let intersects = label2.does_rect_clash(&s1, 0.0);
if (label1.atom_idx == 59 && label2.atom_idx == 55)
|| (label1.atom_idx == 55 && label2.atom_idx == 59)
{
for r2 in &label2.rects {
let s2 = StringRect {
trans: DVec2::new(r2.trans.x + label2.cds.x, r2.trans.y + label2.cds.y),
width: r2.width,
height: r2.height,
..r2.clone()
};
let res = s2.does_it_intersect(&s1, 0.0);
let (tl, tr, br, bl) = s2.calc_corners(0.0);
eprintln!(
"COSMOL_RUST_LABEL_CLASH_RECT2 atom1={} atom2={} trans=({:.17},{:.17}) tl=({:.17},{:.17}) tr=({:.17},{:.17}) br=({:.17},{:.17}) bl=({:.17},{:.17}) res={}",
label1.atom_idx,
label2.atom_idx,
s2.trans.x,
s2.trans.y,
tl.x,
tl.y,
tr.x,
tr.y,
br.x,
br.y,
bl.x,
bl.y,
res
);
}
}
if intersects {
return true;
}
}
false
}
fn rect_clashes_with_line(rect: &StringRect, begin: DVec2, end: DVec2, padding: f64) -> bool {
let (tl, tr, br, bl) = rect.calc_corners(padding);
for (e1, e2) in [(tl, tr), (tr, br), (br, bl), (bl, tl)] {
if line_intersection(begin, end, e1, e2).is_some() {
return true;
}
}
false
}
fn point_in_triangle(pt: DVec2, t1: DVec2, t2: DVec2, t3: DVec2) -> bool {
let d = (t2.y - t3.y) * (t1.x - t3.x) + (t3.x - t2.x) * (t1.y - t3.y);
let a = ((t2.y - t3.y) * (pt.x - t3.x) + (t3.x - t2.x) * (pt.y - t3.y)) / d;
let b = ((t3.y - t1.y) * (pt.x - t3.x) + (t1.x - t3.x) * (pt.y - t3.y)) / d;
let c = 1.0 - a - b;
(0.0..=1.0).contains(&a) && (0.0..=1.0).contains(&b) && (0.0..=1.0).contains(&c)
}
fn rect_clashes_with_triangle(
rect: &StringRect,
t1: DVec2,
t2: DVec2,
t3: DVec2,
padding: f64,
) -> bool {
if rect.is_point_inside(t1, padding)
|| rect.is_point_inside(t2, padding)
|| rect.is_point_inside(t3, padding)
{
return true;
}
let (tl, tr, br, bl) = rect.calc_corners(padding);
if point_in_triangle(tl, t1, t2, t3)
|| point_in_triangle(tr, t1, t2, t3)
|| point_in_triangle(br, t1, t2, t3)
|| point_in_triangle(bl, t1, t2, t3)
{
return true;
}
[(tl, tr), (tr, br), (br, bl), (bl, tl)]
.into_iter()
.any(|(r1, r2)| {
line_intersection(r1, r2, t1, t2).is_some()
|| line_intersection(r1, r2, t2, t3).is_some()
|| line_intersection(r1, r2, t3, t1).is_some()
})
}
fn label_rects_clash_with_line(label: &AtomLabel, begin: DVec2, end: DVec2, padding: f64) -> bool {
for r in &label.rects {
let shifted = StringRect {
trans: DVec2::new(r.trans.x + label.cds.x, r.trans.y + label.cds.y),
width: r.width,
height: r.height,
..r.clone()
};
if rect_clashes_with_line(&shifted, begin, end, padding) {
return true;
}
}
false
}
fn label_rects_clash_with_wedge(label: &AtomLabel, wedge: &DrawWedge, padding: f64) -> bool {
for r in &label.rects {
let shifted = StringRect {
trans: DVec2::new(r.trans.x + label.cds.x, r.trans.y + label.cds.y),
width: r.width,
height: r.height,
..r.clone()
};
match wedge.kind {
WedgeKind::Solid => {
for tri in wedge.points.chunks_exact(3) {
if rect_clashes_with_triangle(&shifted, tri[0], tri[1], tri[2], padding) {
return true;
}
}
}
WedgeKind::Dashed => {
if wedge.points.len() >= 3
&& rect_clashes_with_triangle(
&shifted,
wedge.points[0],
wedge.points[1],
wedge.points[2],
padding,
)
{
return true;
}
}
}
}
false
}
fn does_label_clash_with_bonds(
label: &AtomLabel,
bonds: &[DrawLine],
wedges: &[DrawWedge],
join_paths: &[DrawPolyline],
) -> bool {
for line in bonds {
if label_rects_clash_with_line(label, line.begin, line.end, 0.0) {
return true;
}
}
for wedge in wedges {
if label_rects_clash_with_wedge(label, wedge, 0.0) {
return true;
}
}
for poly in join_paths {
for segment in poly.points.windows(2) {
if label_rects_clash_with_line(label, segment[0], segment[1], 0.0) {
return true;
}
}
}
false
}
fn trim_other_bond_vecs(other_bond_vecs: &mut Vec<DVec2>) {
if other_bond_vecs.len() < 3 {
return;
}
let mut first_vec = 0usize;
let mut second_vec = 1usize;
let mut largest_ang = -361.0f64;
for i in 0..other_bond_vecs.len() - 1 {
for j in i + 1..other_bond_vecs.len() {
let ang = angle_to(other_bond_vecs[i], other_bond_vecs[j]);
if ang > largest_ang {
first_vec = i;
second_vec = j;
largest_ang = ang;
}
}
}
let new_vecs = vec![other_bond_vecs[first_vec], other_bond_vecs[second_vec]];
*other_bond_vecs = new_vecs;
}
fn order_other_bond_vecs(points: &[DVec2], other_bond_vecs: &mut [DVec2]) {
if other_bond_vecs.len() < 2 || points.len() < 3 {
return;
}
let mid = (points[1] + points[2]) / 2.0;
let midp1 = direction_vector(mid, points[1]);
let dot1 = midp1.dot(other_bond_vecs[0]);
let dot2 = midp1.dot(other_bond_vecs[1]);
if dot1 < dot2 {
other_bond_vecs.swap(0, 1);
}
}
fn build_single_colour_wedge_triangles(
mut points: Vec<DVec2>,
other_bond_vecs: &[DVec2],
) -> Vec<DVec2> {
let point = points[0];
let end1 = points[1];
let end2 = points[2];
let mid_end = (end1 + end2) / 2.0;
let mut adjend1 = end1;
let mut adjend2 = end2;
points.clear();
if other_bond_vecs.is_empty() {
points.push(point);
points.push(adjend1);
points.push(adjend2);
} else if other_bond_vecs.len() == 1 {
let side1 = (end1 - point) * 2.0;
if let Some(ip) = line_intersection(
point,
point + side1,
mid_end - other_bond_vecs[0],
mid_end + other_bond_vecs[0],
) {
adjend1 = ip;
}
let side2 = (end2 - point) * 2.0;
if let Some(ip) = line_intersection(
point,
point + side2,
mid_end - other_bond_vecs[0],
mid_end + other_bond_vecs[0],
) {
adjend2 = ip;
}
points.push(point);
points.push(adjend1);
points.push(adjend2);
} else if other_bond_vecs.len() == 2 {
let side1 = (end1 - point) * 2.0;
if let Some(ip) = line_intersection(
point,
point + side1,
mid_end - other_bond_vecs[0],
mid_end + other_bond_vecs[0],
) {
adjend1 = ip;
}
points.push(point);
points.push(adjend1);
points.push(mid_end);
let side2 = (end2 - point) * 2.0;
if let Some(ip) = line_intersection(
point,
point + side2,
mid_end - other_bond_vecs[1],
mid_end + other_bond_vecs[1],
) {
adjend2 = ip;
}
points.push(point);
points.push(mid_end);
points.push(adjend2);
}
points
}
fn build_two_colour_wedge_triangles(
mut points: Vec<DVec2>,
other_bond_vecs: &[DVec2],
) -> Vec<DVec2> {
let point = points[0];
let end1 = points[1];
let end2 = points[2];
let mid_end = (end1 + end2) / 2.0;
let mut adjend1 = end1;
let mut adjend2 = end2;
points.clear();
let e1 = end1 - point;
let e2 = end2 - point;
let mid1 = point + e1 * 0.5;
let mid2 = point + e2 * 0.5;
points.push(point);
points.push(mid1);
points.push(mid2);
if other_bond_vecs.is_empty() {
points.push(mid1);
points.push(adjend2);
points.push(adjend1);
points.push(mid1);
points.push(mid2);
points.push(adjend2);
} else if other_bond_vecs.len() == 1 {
let side1 = (end1 - point) * 2.0;
if let Some(ip) = line_intersection(
point,
point + side1,
mid_end - other_bond_vecs[0],
mid_end + other_bond_vecs[0],
) {
adjend1 = ip;
}
let side2 = (end2 - point) * 2.0;
if let Some(ip) = line_intersection(
point,
point + side2,
mid_end - other_bond_vecs[0],
mid_end + other_bond_vecs[0],
) {
adjend2 = ip;
}
points.push(mid1);
points.push(adjend2);
points.push(adjend1);
points.push(mid1);
points.push(mid2);
points.push(adjend2);
} else if other_bond_vecs.len() == 2 {
let side1 = (end1 - point) * 2.0;
if let Some(ip) = line_intersection(
point,
point + side1,
mid_end - other_bond_vecs[0],
mid_end + other_bond_vecs[0],
) {
adjend1 = ip;
}
let side2 = (end2 - point) * 2.0;
if let Some(ip) = line_intersection(
point,
point + side2,
mid_end - other_bond_vecs[1],
mid_end + other_bond_vecs[1],
) {
adjend2 = ip;
}
points.push(mid1);
points.push(adjend1);
points.push(mid_end);
points.push(mid_end);
points.push(mid2);
points.push(mid1);
points.push(mid_end);
points.push(adjend2);
points.push(mid2);
}
points
}
fn get_bracket_points(
p1: DVec2,
p2: DVec2,
ref_pt: DVec2,
bond_segments: &[(DVec2, DVec2)],
) -> Vec<DVec2> {
let v = p2 - p1;
let mut bracket_dir = DVec2::new(v.y, -v.x);
let bracket_frac = 0.15; bracket_dir *= bracket_frac;
let mut ref_vect = p2 - ref_pt;
for (seg_begin, seg_end) in bond_segments {
if line_segments_intersect(p1, p2, *seg_begin, *seg_end) {
ref_vect = p2 - *seg_begin;
}
}
if bracket_dir.dot(ref_vect) > 0.0 {
bracket_dir *= -1.0;
}
let p0 = p1 + bracket_dir;
let p3 = p2 + bracket_dir;
vec![p0, p1, p2, p3]
}
fn line_segments_intersect(p1: DVec2, p2: DVec2, q1: DVec2, q2: DVec2) -> bool {
fn cross(a: DVec2, b: DVec2) -> f64 {
a.x * b.y - a.y * b.x
}
let r = p2 - p1;
let s = q2 - q1;
let r_cross_s = cross(r, s);
if r_cross_s.abs() < 1e-10 {
return false; }
let t = cross(q1 - p1, s) / r_cross_s;
let u = cross(q1 - p1, r) / r_cross_s;
t >= 0.0 && t <= 1.0 && u >= 0.0 && u <= 1.0
}
struct DrawMol {
width: f64,
height: f64,
draw_width: f64,
draw_height: f64,
mol_height: f64,
margin_padding: f64,
scale: f64,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
x_range: f64,
y_range: f64,
at_cds: Vec<DVec2>,
atom_labels: Vec<Option<AtomLabel>>,
atom_orients: Vec<OrientType>,
implicit_hs: Vec<i32>,
bonds: Vec<DrawLine>,
wedges: Vec<DrawWedge>,
arrows: Vec<DrawArrow>,
draw_items: Vec<DrawPolyline>,
join_paths: Vec<DrawPolyline>,
post_shapes: Vec<DrawPolyline>,
radicals: Vec<DrawRadical>,
raw_double_bonds: Vec<(usize, usize)>,
single_bond_lines: Vec<usize>,
bond_draw_order: Vec<DrawBondShapeRef>,
mean_bond_length: f64,
font_size: f64,
font_scale: f64,
options: DrawOptions,
annotations: Vec<DrawAnnotation>,
}
impl DrawMol {
fn include_extreme_point(
x_min: &mut f64,
x_max: &mut f64,
y_min: &mut f64,
y_max: &mut f64,
pt: DVec2,
) {
if pt.x < *x_min {
*x_min = pt.x;
}
if pt.x > *x_max {
*x_max = pt.x;
}
if pt.y < *y_min {
*y_min = pt.y;
}
if pt.y > *y_max {
*y_max = pt.y;
}
}
fn debug_svg_row_active(row: usize) -> bool {
std::env::var("COSMOLKIT_DEBUG_SVG_ROW")
.ok()
.and_then(|s| s.parse::<usize>().ok())
== Some(row)
}
fn get_colour(&self, mol: &Molecule, atom_idx: usize) -> DrawColour {
atom_colour(mol.atoms()[atom_idx].atomic_number() as i32)
}
fn get_bond_colours(&self, mol: &Molecule, bond: &Bond) -> (DrawColour, DrawColour) {
(
self.get_colour(mol, bond.begin().index()),
self.get_colour(mol, bond.end().index()),
)
}
fn from_molecule(
mol: &Molecule,
width: u32,
height: u32,
options: DrawOptions,
) -> Result<Self, SvgDrawError> {
let prepared_mol = prepare_molecule_for_svg_drawing(mol)?;
let at_cds: Vec<DVec2> = if let Some(coords) = prepared_mol.coordinates_2d() {
let mapped: Vec<DVec2> = coords.iter().map(|pt| DVec2::new(pt[0], -pt[1])).collect();
if Self::debug_svg_row_active(58) {
for (idx, pt) in mapped.iter().enumerate() {
eprintln!(
"COSMOL_AT_CDS idx={} pt=({:.17},{:.17}) bits=({:#018x},{:#018x})",
idx,
pt.x,
pt.y,
pt.x.to_bits(),
pt.y.to_bits()
);
}
}
mapped
} else {
return Err(SvgDrawError::CoordinateGeneration(
"prepareMolForDrawing did not materialize a 2D conformer".to_string(),
));
};
let valence = assign_valence(&prepared_mol, ValenceModel::RdkitLike)
.map_err(|e| SvgDrawError::Unsupported(format!("valence assignment failed: {e}")))?;
let base_at_cds = at_cds.clone();
let mut out = Self {
width: width as f64,
height: height as f64,
draw_width: width as f64,
draw_height: height as f64,
mol_height: height as f64,
margin_padding: options.padding,
scale: 1.0,
x_min: f64::MAX / 2.0,
x_max: f64::MIN / 2.0,
y_min: f64::MAX / 2.0,
y_max: f64::MIN / 2.0,
x_range: f64::MAX,
y_range: f64::MAX,
at_cds,
atom_labels: Vec::new(),
atom_orients: Vec::new(),
implicit_hs: valence.implicit_hydrogens,
bonds: Vec::new(),
wedges: Vec::new(),
arrows: Vec::new(),
draw_items: Vec::new(),
join_paths: Vec::new(),
post_shapes: Vec::new(),
radicals: Vec::new(),
raw_double_bonds: Vec::new(),
single_bond_lines: Vec::new(),
bond_draw_order: Vec::new(),
mean_bond_length: 0.0,
font_size: 0.6,
font_scale: 1.0,
options,
annotations: Vec::new(),
};
let rebuild_draw_objects =
|draw: &mut Self, rel_font_scale: f64| -> Result<(), SvgDrawError> {
draw.scale = 1.0;
draw.font_scale = rel_font_scale;
draw.x_min = f64::MAX / 2.0;
draw.x_max = f64::MIN / 2.0;
draw.y_min = f64::MAX / 2.0;
draw.y_max = f64::MIN / 2.0;
draw.x_range = f64::MAX;
draw.y_range = f64::MAX;
draw.at_cds = base_at_cds.clone();
draw.atom_labels.clear();
draw.atom_orients.clear();
draw.bonds.clear();
draw.wedges.clear();
draw.arrows.clear();
draw.draw_items.clear();
draw.join_paths.clear();
draw.post_shapes.clear();
draw.radicals.clear();
draw.raw_double_bonds.clear();
draw.single_bond_lines.clear();
draw.bond_draw_order.clear();
draw.mean_bond_length = 0.0;
draw.annotations.clear();
draw.extract_atom_symbols(&prepared_mol);
draw.extract_variable_bonds(&prepared_mol);
draw.extract_bonds(&prepared_mol)?;
draw.smooth_bond_joins(&prepared_mol);
draw.resolve_atom_symbol_clashes();
draw.extract_regions();
draw.extract_highlights(&prepared_mol);
draw.extract_attachments(&prepared_mol);
draw.extract_atom_notes(&prepared_mol);
if draw.options.add_stereo_annotation {
draw.extract_cip_codes(&prepared_mol);
}
draw.extract_stereo_groups(&prepared_mol);
draw.extract_bond_notes(&prepared_mol);
draw.extract_radicals(&prepared_mol);
draw.extract_sgroup_data(&prepared_mol);
draw.extract_brackets(&prepared_mol);
draw.extract_link_nodes(&prepared_mol);
Ok(())
};
rebuild_draw_objects(&mut out, 1.0)?;
out.calculate_scale();
let drawn_font_size = out.font_size * out.font_scale;
let clamped_font_scale = if drawn_font_size > 40.0 {
40.0 / out.font_size
} else if drawn_font_size < 6.0 {
6.0 / out.font_size
} else {
out.font_scale
};
if (clamped_font_scale - out.font_scale).abs() > 1.0e-12 {
let final_font_scale = clamped_font_scale;
let final_scale = out.scale;
rebuild_draw_objects(&mut out, final_font_scale / final_scale)?;
out.find_extremes();
out.refresh_ranges_from_extremes();
out.scale = final_scale;
out.font_scale = final_font_scale;
}
out.extract_mol_notes(&prepared_mol);
out.find_extremes();
out.refresh_ranges_from_extremes();
if Self::debug_svg_row_active(12) {
for idx in [0usize, 1usize] {
if let Some(label) = out.atom_labels.get(idx).and_then(|l| l.as_ref()) {
eprintln!(
"row12-pre-draw label{} cds=({}, {}) orient={:?} rects={:?}",
idx, label.cds.x, label.cds.y, label.orient, label.rects
);
}
eprintln!(
"row12-pre-draw at_cds{}=({}, {})",
idx, out.at_cds[idx].x, out.at_cds[idx].y
);
}
}
out.change_to_draw_coords();
out.extract_close_contacts();
if Self::debug_svg_row_active(12) {
for idx in [0usize, 1usize] {
if let Some(label) = out.atom_labels.get(idx).and_then(|l| l.as_ref()) {
eprintln!(
"row12-post-draw label{} cds=({}, {}) orient={:?} rects={:?}",
idx, label.cds.x, label.cds.y, label.orient, label.rects
);
}
eprintln!(
"row12-post-draw at_cds{}=({}, {})",
idx, out.at_cds[idx].x, out.at_cds[idx].y
);
}
}
Ok(out)
}
fn font_scale_factor(&self) -> f64 {
self.font_scale
}
fn radical_spot_radius_unscaled(&self) -> f64 {
0.2 * self.options.multiple_bond_offset * self.font_scale_factor()
}
fn extract_atom_symbols(&mut self, mol: &Molecule) {
self.atom_labels.clear();
self.atom_orients.clear();
let font_size = self.font_size * self.font_scale;
for atom in mol.atoms() {
let idx = atom.id().index();
let orient = self.get_atom_orientation(mol, idx);
self.atom_orients.push(orient);
let symbol = self.get_atom_symbol(mol, atom, orient);
if symbol.is_empty() {
self.atom_labels.push(None);
} else {
self.atom_labels.push(Some(AtomLabel::new(
symbol,
idx,
atom.atomic_number(),
orient,
self.at_cds[idx],
atom_colour(atom.atomic_number() as i32),
font_size,
)));
}
if Self::debug_svg_row_active(142) && matches!(idx, 55 | 59) {
match self.atom_labels.get(idx).and_then(|l| l.as_ref()) {
Some(label) => {
eprintln!(
"COSMOL_ROW142_LABEL_EXTRACT atom={} symbol={} cds=({:.17},{:.17}) orient={:?} rects={}",
idx,
label.symbol,
label.cds.x,
label.cds.y,
label.orient,
label.rects.len()
);
for (rect_idx, rect) in label.rects.iter().enumerate() {
eprintln!(
"COSMOL_ROW142_LABEL_RECT atom={} rect={} ch={} trans=({:.17},{:.17}) offset=({:.17},{:.17}) gc=({:.17},{:.17}) y_shift={:.17} width={:.17} height={:.17} rect_corr={:.17}",
idx,
rect_idx,
rect.ch,
rect.trans.x,
rect.trans.y,
rect.offset.x,
rect.offset.y,
rect.g_centre.x,
rect.g_centre.y,
rect.y_shift,
rect.width,
rect.height,
rect.rect_corr
);
}
}
None => eprintln!("COSMOL_ROW142_LABEL_EXTRACT atom={} none", idx),
}
}
}
}
fn get_atom_orientation(&self, mol: &Molecule, atom_idx: usize) -> OrientType {
const VERT_SLOPE: f64 = 2.7474774194546216;
let neighbours = atom_neighbors(mol, atom_idx);
let atom = &mol.atoms()[atom_idx];
let at1_cds = self.at_cds[atom_idx];
let mut nbr_sum = DVec2::ZERO;
for &nbr in &neighbours {
nbr_sum += self.at_cds[nbr] - at1_cds;
}
if Self::debug_svg_row_active(123) && matches!(atom_idx, 18 | 19) {
eprintln!(
"COSMOL_ATOM_ORIENT_INPUT atom={} degree={} at=({:.17},{:.17}) nbrs={:?} nbr_sum=({:.17},{:.17})",
atom_idx,
neighbours.len(),
at1_cds.x,
at1_cds.y,
neighbours,
nbr_sum.x,
nbr_sum.y
);
}
let mut orient = OrientType::C;
if !neighbours.is_empty() {
let mut islope = 1000.0;
if nbr_sum.x.abs() > 1.0e-4 {
islope = nbr_sum.y / nbr_sum.x;
}
if islope.abs() <= VERT_SLOPE {
orient = if nbr_sum.x > 0.0 {
OrientType::W
} else {
OrientType::E
};
} else {
orient = if nbr_sum.y > 0.0 {
OrientType::S
} else {
OrientType::N
};
}
if orient == OrientType::N || orient == OrientType::S {
if neighbours.len() == 1 {
if islope.abs() > VERT_SLOPE {
orient = OrientType::E;
} else if nbr_sum.x > 0.0 {
orient = OrientType::W;
} else {
orient = OrientType::E;
}
} else if neighbours.len() == 3 {
for &nbr in &neighbours {
let bond_vec = self.at_cds[nbr] - at1_cds;
if bond_vec.x.abs() < 1.0e-16 {
orient = if bond_vec.y > 0.0 {
OrientType::S
} else {
OrientType::N
};
break;
}
let ang = (bond_vec.y / bond_vec.x).atan().to_degrees();
if ang > 80.0 && ang < 100.0 && orient == OrientType::S {
orient = OrientType::S;
break;
} else if ang < -80.0 && ang > -100.0 && orient == OrientType::N {
orient = OrientType::N;
break;
}
}
}
}
} else {
orient = match atom.atomic_number() {
8 | 9 | 16 | 17 | 34 | 35 | 52 | 53 | 84 | 85 => OrientType::W,
_ => OrientType::E,
};
}
if Self::debug_svg_row_active(123) && matches!(atom_idx, 18 | 19) {
eprintln!("COSMOL_ATOM_ORIENT atom={} orient={:?}", atom_idx, orient);
}
orient
}
fn get_atom_symbol(&self, mol: &Molecule, atom: &Atom, _orient: OrientType) -> String {
let atomic_num = atom.atomic_number();
let symbol = element_symbol(atomic_num);
let isotope = atom
.isotope()
.map(|iso| format!("<sup>{iso}</sup>"))
.unwrap_or_default();
let map_num = atom.atom_map().map(|n| format!(":{n}")).unwrap_or_default();
let num_h = if atomic_num == 6 && atom_degree(mol, atom.id().index()) > 0 {
0
} else {
atom.explicit_hydrogens() as usize
+ self
.implicit_hs
.get(atom.id().index())
.copied()
.unwrap_or(0) as usize
};
let h = match num_h {
0 => String::new(),
1 => "H".to_string(),
n => format!("H<sub>{n}</sub>"),
};
let charge = if atom.formal_charge() == 0 {
String::new()
} else {
let magnitude = atom.formal_charge().unsigned_abs();
let sign = if atom.formal_charge() > 0 { "+" } else { "-" };
if magnitude > 1 {
format!("<sup>{magnitude}{sign}</sup>")
} else {
format!("<sup>{sign}</sup>")
}
};
if isotope.is_empty() && h.is_empty() && charge.is_empty() && map_num.is_empty() {
if is_linear_atom(mol, &self.at_cds, atom.id().index())
|| atomic_num != 6
|| atom_degree(mol, atom.id().index()) == 0
{
symbol.to_string()
} else {
String::new()
}
} else {
format!("{isotope}{symbol}{map_num}{charge}{h}")
}
}
fn calc_mean_bond_length(&mut self, mol: &Molecule) {
if mol.bonds().is_empty() || self.at_cds.len() < 2 {
self.mean_bond_length = 1.0;
return;
}
let mut total = 0.0;
let mut count = 0;
for bond in mol.bonds() {
let b = bond.begin().index();
let e = bond.end().index();
if b < self.at_cds.len() && e < self.at_cds.len() {
let dist = (self.at_cds[b] - self.at_cds[e]).length();
if dist > 1e-8 {
total += dist;
count += 1;
}
}
}
self.mean_bond_length = if count > 0 { total / count as f64 } else { 1.0 };
}
fn extract_bonds(&mut self, mol: &Molecule) -> Result<(), SvgDrawError> {
self.bonds.clear();
self.wedges.clear();
self.arrows.clear();
self.draw_items.clear();
self.join_paths.clear();
self.single_bond_lines.clear();
self.raw_double_bonds.clear();
self.bond_draw_order.clear();
self.calc_mean_bond_length(mol);
let double_bond_offset = self.options.multiple_bond_offset * self.mean_bond_length;
for bond in mol.bonds() {
match bond.order() {
BondOrder::Double | BondOrder::Aromatic => {
if bond.order() == BondOrder::Double {
self.raw_double_bonds
.push((bond.begin().index(), bond.end().index()));
}
self.make_double_bond_lines(mol, bond, double_bond_offset);
}
BondOrder::Single => {
if matches!(
bond.direction(),
BondDirection::BeginWedge | BondDirection::BeginDash
) {
self.make_wedged_bond(mol, bond)?;
} else {
let (begin, end) = self
.adjust_bond_ends_for_labels(bond.begin().index(), bond.end().index());
self.new_bond_line(mol, begin, end, bond);
}
}
BondOrder::Triple => {
let (begin, end) =
self.adjust_bond_ends_for_labels(bond.begin().index(), bond.end().index());
self.new_bond_line(mol, begin, end, bond);
self.make_triple_bond_lines(mol, bond, double_bond_offset);
}
BondOrder::Quadruple => {
let (begin, end) =
self.adjust_bond_ends_for_labels(bond.begin().index(), bond.end().index());
self.new_bond_line(mol, begin, end, bond);
}
BondOrder::Null | BondOrder::Hydrogen | BondOrder::Unspecified => {
self.make_bond_null_query_line(bond);
}
BondOrder::Dative
| BondOrder::DativeOne
| BondOrder::DativeLeft
| BondOrder::DativeRight => {
self.make_dative_bond(mol, bond, double_bond_offset);
}
_ => {
let (begin, end) =
self.adjust_bond_ends_for_labels(bond.begin().index(), bond.end().index());
self.new_bond_line(mol, begin, end, bond);
}
}
}
self.adjust_bonds_on_solid_wedge_ends(mol);
Ok(())
}
fn pick_bonds_to_wedge(&self, mol: &Molecule) -> HashSet<usize> {
let mut wedges = HashSet::new();
for atom in mol.atoms() {
let idx = atom.id().index();
let chiral = atom.chiral_tag();
if chiral == crate::ChiralTag::TetrahedralCw
|| chiral == crate::ChiralTag::TetrahedralCcw
{
let neighbours = atom_neighbors(mol, idx);
if let Some(perm) = atom.chiral_permutation() {
let perms: Vec<usize> =
(0..4).map(|i| ((perm >> (i * 2)) & 3) as usize).collect();
if let Some(&hi_neighbor_idx) = perms.first() {
if hi_neighbor_idx < neighbours.len() {
let hi_atom = neighbours[hi_neighbor_idx];
if let Some(bond) = bond_between_atoms(mol, idx, hi_atom) {
if bond.order() == BondOrder::Single {
wedges.insert(bond.id().index());
}
}
}
}
}
}
}
wedges
}
fn determine_bond_wedge_state(
&self,
mol: &Molecule,
bond: &Bond,
wedge_bonds: &HashSet<usize>,
) -> BondDirection {
let (chiral_atom, other_atom) = {
let b_idx = bond.begin().index();
let e_idx = bond.end().index();
let ba = mol.atoms()[b_idx].chiral_tag();
let ea = mol.atoms()[e_idx].chiral_tag();
if (ba == crate::ChiralTag::TetrahedralCw || ba == crate::ChiralTag::TetrahedralCcw)
&& wedge_bonds.contains(&bond.id().index())
{
(b_idx, e_idx)
} else if (ea == crate::ChiralTag::TetrahedralCw
|| ea == crate::ChiralTag::TetrahedralCcw)
&& wedge_bonds.contains(&bond.id().index())
{
(e_idx, b_idx)
} else {
return BondDirection::None;
}
};
let dir = self.at_cds[other_atom] - self.at_cds[chiral_atom];
let ang = f64::atan2(dir.y, dir.x);
if ang > -std::f64::consts::FRAC_PI_2 && ang <= std::f64::consts::FRAC_PI_2 {
BondDirection::EndUpRight
} else {
BondDirection::EndDownRight
}
}
fn make_double_bond_lines(&mut self, mol: &Molecule, bond: &Bond, offset: f64) {
let b = bond.begin().index();
let e = bond.end().index();
let cols = self.get_bond_colours(mol, bond);
let (end1, end2) = self.adjust_bond_ends_for_labels(b, e);
if debug_svg_bond_enabled(bond.id().index()) {
debug_svg_bond_log(format!(
"RUST make_double_bond_lines bond={} b={b} e={e} at_cds_b=({:.15},{:.15}) at_cds_e=({:.15},{:.15}) end1=({:.15},{:.15}) end2=({:.15},{:.15}) cols=({:?},{:?})",
bond.id().index(),
self.at_cds[b].x,
self.at_cds[b].y,
self.at_cds[e].x,
self.at_cds[e].y,
end1.x,
end1.y,
end2.x,
end2.y,
cols.0,
cols.1
));
}
let saved_b = self.at_cds[b];
let saved_e = self.at_cds[e];
self.at_cds[b] = end1;
self.at_cds[e] = end2;
let (l1s, l1f, l2s, l2f) = self.calc_double_bond_lines(mol, bond, offset);
if debug_svg_bond_enabled(bond.id().index()) {
debug_svg_bond_log(format!(
"RUST calc_double_bond_lines bond={} l1s=({:.15},{:.15}) l1f=({:.15},{:.15}) l2s=({:.15},{:.15}) l2f=({:.15},{:.15})",
bond.id().index(),
l1s.x,
l1s.y,
l1f.x,
l1f.y,
l2s.x,
l2s.y,
l2f.x,
l2f.y
));
}
self.new_bond_line_with_colours(l1s, l1f, cols.0, cols.1, bond);
let l1 = (l1s - l1f).length_squared();
let l2 = (l2s - l2f).length_squared();
if (atom_degree(mol, b) == 1 || atom_degree(mol, e) == 1)
&& cols.0 != cols.1
&& (l1 - l2).abs() > 0.01
{
let midlen = l1.sqrt() / 2.0;
let not_mid = if atom_degree(mol, b) == 1 {
let line_dir = direction_vector(l2s, l2f);
l2s + line_dir * midlen
} else {
let line_dir = direction_vector(l2f, l2s);
l2f + line_dir * midlen
};
if debug_svg_bond_enabled(bond.id().index()) {
debug_svg_bond_log(format!(
"RUST asym_split bond={} l1={:.15} l2={:.15} midlen={:.15} not_mid=({:.15},{:.15})",
bond.id().index(),
l1,
l2,
midlen,
not_mid.x,
not_mid.y
));
}
self.new_bond_line_with_colours(l2s, not_mid, cols.0, cols.0, bond);
self.new_bond_line_with_colours(not_mid, l2f, cols.1, cols.1, bond);
} else {
self.new_bond_line_with_colours(l2s, l2f, cols.0, cols.1, bond);
}
self.at_cds[b] = saved_b;
self.at_cds[e] = saved_e;
}
fn calc_double_bond_lines(
&mut self,
mol: &Molecule,
bond: &Bond,
offset: f64,
) -> (DVec2, DVec2, DVec2, DVec2) {
let at1 = bond.begin().index();
let at2 = bond.end().index();
if is_linear_atom(mol, &self.at_cds, at1)
|| is_linear_atom(mol, &self.at_cds, at2)
|| (atom_degree(mol, at1) == 1 && atom_degree(mol, at2) == 1)
{
let at1_cds = self.at_cds[at1];
let at2_cds = self.at_cds[at2];
let perp = calc_perpendicular(at1_cds, at2_cds) * offset * 0.5;
return (
at1_cds + perp,
at2_cds + perp,
at1_cds - perp,
at2_cds - perp,
);
}
if atom_degree(mol, at1) == 1 || atom_degree(mol, at2) == 1 {
return self.double_bond_terminal(mol, at1, at2, offset);
}
let at1_cds = self.at_cds[at1];
let at2_cds = self.at_cds[at2];
let mut l1s = at1_cds;
let mut l1f = at2_cds;
let (mut l2s, mut l2f) = if mol
.derived_cache()
.rings
.as_ref()
.is_some_and(|ri| ri.is_initialized() && ri.num_bond_rings(bond.id()) > 0)
{
self.bond_inside_ring(mol, bond, offset)
} else if self.atom_labels[at1].is_some() && self.atom_labels[at2].is_some() {
let (a, b, c, d) = self.double_bond_terminal(mol, at1, at2, offset);
l1s = a;
l1f = b;
(c, d)
} else {
self.bond_non_ring(mol, bond, offset)
};
if !are_bonds_parallel(l1s, l1f, l2f, l2s, 1.0e-4) {
std::mem::swap(&mut l1s, &mut l2s);
}
if matches!(bond.direction(), BondDirection::EitherDouble)
|| matches!(bond.stereo(), crate::BondStereo::Any)
{
std::mem::swap(&mut l1s, &mut l2s);
}
(l1s, l1f, l2s, l2f)
}
fn bond_inside_ring(&self, mol: &Molecule, bond: &Bond, offset: f64) -> (DVec2, DVec2) {
let Some(rings) = mol.derived_cache().rings.as_ref() else {
return self.bond_non_ring(mol, bond, offset);
};
let mut bond_in_rings = Vec::new();
for (i, ring_bonds) in rings.bond_rings().iter().enumerate() {
if ring_bonds.iter().any(|&bond_id| bond_id == bond.id()) {
bond_in_rings.push(i);
}
}
let other_ring_atom = |bond_atom: usize, ring_bonds: &[crate::BondId]| -> Option<usize> {
for bond2 in mol.bonds() {
if bond2.id() == bond.id() {
continue;
}
if !ring_bonds.iter().any(|&ring_bond| ring_bond == bond2.id()) {
continue;
}
if bond2.begin().index() == bond_atom {
return Some(bond2.end().index());
}
if bond2.end().index() == bond_atom {
return Some(bond2.begin().index());
}
}
None
};
let mut ring_to_use = None;
if bond_in_rings.len() > 1 {
for &ring_idx in &bond_in_rings {
let candidate = &rings.bond_rings()[ring_idx];
ring_to_use = Some(candidate.as_slice());
let ring_ok = candidate.iter().all(|bond_idx| {
mol.bonds()[bond_idx.index()].is_aromatic() == bond.is_aromatic()
});
if ring_ok {
break;
}
}
} else {
ring_to_use = bond_in_rings
.first()
.and_then(|&i| rings.bond_rings().get(i))
.map(Vec::as_slice);
}
let Some(ring_to_use) = ring_to_use else {
return self.bond_non_ring(mol, bond, offset);
};
let Some(third_atom) = other_ring_atom(bond.begin().index(), ring_to_use) else {
return self.bond_non_ring(mol, bond, offset);
};
let Some(fourth_atom) = other_ring_atom(bond.end().index(), ring_to_use) else {
return self.bond_non_ring(mol, bond, offset);
};
let beg_idx = bond.begin().index();
let end_idx = bond.end().index();
let is_trans = are_bonds_trans(
self.at_cds[third_atom],
self.at_cds[beg_idx],
self.at_cds[end_idx],
self.at_cds[fourth_atom],
);
if debug_svg_bond_enabled(bond.id().index()) {
debug_svg_bond_log(format!(
"RUST bond_inside_ring bond={} bond_in_rings={:?} ring_to_use_len={} third_atom={} fourth_atom={} is_trans={} beg_idx={} end_idx={}",
bond.id().index(),
bond_in_rings,
ring_to_use.len(),
third_atom,
fourth_atom,
is_trans,
beg_idx,
end_idx
));
}
if is_trans {
self.bond_non_ring(mol, bond, offset)
} else {
let result = (
self.double_bond_end(
&self.at_cds,
third_atom,
beg_idx,
end_idx,
offset,
self.atom_labels[beg_idx].is_none(),
),
self.double_bond_end(
&self.at_cds,
fourth_atom,
end_idx,
beg_idx,
offset,
self.atom_labels[end_idx].is_none(),
),
);
if debug_svg_bond_enabled(bond.id().index()) {
debug_svg_bond_log(format!(
"RUST bond_inside_ring result bond={} l2s=({:.15},{:.15}) l2f=({:.15},{:.15})",
bond.id().index(),
result.0.x,
result.0.y,
result.1.x,
result.1.y
));
}
result
}
}
fn bond_non_ring(&self, mol: &Molecule, bond: &Bond, offset: f64) -> (DVec2, DVec2) {
let beg_at = bond.begin().index();
let end_at = bond.end().index();
let beg_trunc = self.atom_labels[beg_at].is_none();
let end_trunc = self.atom_labels[end_at].is_none();
let non_colinear_nbor = |at1: usize, at2: usize| -> Option<usize> {
let mut third_atom = None;
for i in 1..atom_degree(mol, at1) {
third_atom = other_neighbor(mol, at1, at2, i);
if let Some(third) = third_atom {
if !are_bonds_parallel(
self.at_cds[at1],
self.at_cds[at2],
self.at_cds[at1],
self.at_cds[third],
1.0e-4,
) {
return Some(third);
}
}
}
if third_atom.is_none() {
third_atom = other_neighbor(mol, at1, at2, 1);
}
third_atom
};
match (atom_degree(mol, beg_at), atom_degree(mol, end_at)) {
(2, 2) => {
let Some(third_atom) = other_neighbor(mol, beg_at, end_at, 0) else {
return (self.at_cds[beg_at], self.at_cds[end_at]);
};
let Some(fourth_atom) = other_neighbor(mol, end_at, beg_at, 0) else {
return (self.at_cds[beg_at], self.at_cds[end_at]);
};
let l2s = self.double_bond_end(
&self.at_cds,
third_atom,
beg_at,
end_at,
offset,
beg_trunc,
);
let is_trans = are_bonds_trans(
self.at_cds[third_atom],
self.at_cds[beg_at],
self.at_cds[end_at],
self.at_cds[fourth_atom],
);
let l2f = if is_trans {
let perp = calc_inner_perpendicular(
self.at_cds[end_at],
self.at_cds[beg_at],
self.at_cds[third_atom],
);
self.at_cds[end_at] + perp * offset
} else {
self.double_bond_end(
&self.at_cds,
fourth_atom,
end_at,
beg_at,
offset,
end_trunc,
)
};
(l2s, l2f)
}
(2, d) if d > 2 => {
let Some(third_atom) = other_neighbor(mol, beg_at, end_at, 0) else {
return (self.at_cds[beg_at], self.at_cds[end_at]);
};
let Some(mut fourth_atom) = other_neighbor(mol, end_at, beg_at, 0) else {
return (self.at_cds[beg_at], self.at_cds[end_at]);
};
let l2s = self.double_bond_end(
&self.at_cds,
third_atom,
beg_at,
end_at,
offset,
beg_trunc,
);
let is_trans = are_bonds_trans(
self.at_cds[third_atom],
self.at_cds[beg_at],
self.at_cds[end_at],
self.at_cds[fourth_atom],
);
if is_trans {
if let Some(nbr) = non_colinear_nbor(end_at, beg_at) {
fourth_atom = nbr;
}
}
let l2f = self.double_bond_end(
&self.at_cds,
fourth_atom,
end_at,
beg_at,
offset,
end_trunc,
);
(l2s, l2f)
}
(d, 2) if d > 2 => {
let Some(mut third_atom) = other_neighbor(mol, beg_at, end_at, 0) else {
return (self.at_cds[beg_at], self.at_cds[end_at]);
};
let Some(fourth_atom) = other_neighbor(mol, end_at, beg_at, 0) else {
return (self.at_cds[beg_at], self.at_cds[end_at]);
};
let mut l2s = self.double_bond_end(
&self.at_cds,
third_atom,
beg_at,
end_at,
offset,
beg_trunc,
);
let is_trans = are_bonds_trans(
self.at_cds[third_atom],
self.at_cds[beg_at],
self.at_cds[end_at],
self.at_cds[fourth_atom],
);
if is_trans {
if let Some(nbr) = non_colinear_nbor(beg_at, end_at) {
third_atom = nbr;
l2s = self.double_bond_end(
&self.at_cds,
third_atom,
beg_at,
end_at,
offset,
end_trunc,
);
}
}
let l2f = self.double_bond_end(
&self.at_cds,
fourth_atom,
end_at,
beg_at,
offset,
end_trunc,
);
(l2s, l2f)
}
(d1, d2) if d1 > 2 && d2 > 2 => {
let Some(third_atom) = other_neighbor(mol, beg_at, end_at, 0) else {
return (self.at_cds[beg_at], self.at_cds[end_at]);
};
let mut l2s = self.double_bond_end(
&self.at_cds,
third_atom,
beg_at,
end_at,
offset,
beg_trunc,
);
let Some(mut fourth_atom) = other_neighbor(mol, end_at, beg_at, 0) else {
return (self.at_cds[beg_at], self.at_cds[end_at]);
};
let is_trans = are_bonds_trans(
self.at_cds[third_atom],
self.at_cds[beg_at],
self.at_cds[end_at],
self.at_cds[fourth_atom],
);
if is_trans {
if let Some(nbr) = non_colinear_nbor(end_at, beg_at) {
fourth_atom = nbr;
}
}
let l2f = self.double_bond_end(
&self.at_cds,
fourth_atom,
end_at,
beg_at,
offset,
end_trunc,
);
(l2s, l2f)
}
_ => (self.at_cds[beg_at], self.at_cds[end_at]),
}
}
fn double_bond_terminal(
&mut self,
mol: &Molecule,
mut at1: usize,
mut at2: usize,
mut offset: f64,
) -> (DVec2, DVec2, DVec2, DVec2) {
let orig_at1 = at1;
let orig_at2 = at2;
let mut swapped = false;
if atom_degree(mol, at1) > 1 && atom_degree(mol, at2) == 1 {
std::mem::swap(&mut at1, &mut at2);
swapped = true;
}
let at1_cds = self.at_cds[at1];
let at2_cds = self.at_cds[at2];
let (mut l1s, mut l1f, mut l2s, mut l2f);
if self.atom_labels[at2].is_some() {
offset /= 2.0;
let perp = calc_perpendicular(at1_cds, at2_cds) * offset;
l1s = at1_cds + perp;
l1f = at2_cds + perp;
l2s = at1_cds - perp;
l2f = at2_cds - perp;
} else if atom_degree(mol, at2) > 2 {
offset /= 2.0;
let perp = calc_perpendicular(at1_cds, at2_cds) * offset;
l1s = at1_cds + perp;
l1f = at2_cds + perp;
l2s = at1_cds - perp;
l2f = at2_cds - perp;
let bl = (l1s - l1f).length().max((l2s - l2f).length());
let l1 = direction_vector(l1s, l1f);
l1f = l1s + l1 * 2.0 * bl;
let l2 = direction_vector(l2s, l2f);
l2f = l2s + l2 * 2.0 * bl;
for nbr in atom_neighbors(mol, at2) {
let nbr_cds = self.at_cds[nbr];
if let Some(ip) = line_intersection(l1s, l1f, at2_cds, nbr_cds) {
l1f = ip;
}
if let Some(ip) = line_intersection(l2s, l2f, at2_cds, nbr_cds) {
l2f = ip;
}
}
} else {
l1s = at1_cds;
l1f = at2_cds;
let Some(third_atom) = other_neighbor(mol, at2, at1, 0) else {
return (l1s, l1f, at1_cds, at2_cds);
};
let perp = calc_inner_perpendicular(at1_cds, at2_cds, self.at_cds[third_atom]);
l2s = at1_cds + perp * offset;
l2f = self.double_bond_end(&self.at_cds, at1, at2, third_atom, offset, true);
if direction_vector(l1s, l1f)
.dot(direction_vector(l2s, l2f))
.abs()
< 0.9999
{
l2f = self.double_bond_end(&self.at_cds, at1, at2, third_atom, -offset, true);
}
if self.atom_labels[at1].is_some() {
if let Some(label) = self.atom_labels[at1].as_mut() {
if Self::debug_svg_row_active(142) && at1 == 59 {
eprintln!(
"COSMOL_ROW142_DBT_SHIFT_BEFORE orig=({},{}) used=({},{}) offset={:.17} perp=({:.17},{:.17}) cds=({:.17},{:.17})",
orig_at1,
orig_at2,
at1,
at2,
offset,
perp.x,
perp.y,
label.cds.x,
label.cds.y
);
}
label.cds += perp * offset * 0.5;
if Self::debug_svg_row_active(142) && at1 == 59 {
eprintln!(
"COSMOL_ROW142_DBT_SHIFT_AFTER atom=59 cds=({:.17},{:.17})",
label.cds.x, label.cds.y
);
}
}
}
}
if swapped {
std::mem::swap(&mut l1s, &mut l1f);
std::mem::swap(&mut l2s, &mut l2f);
}
(l1s, l1f, l2s, l2f)
}
fn double_bond_end(
&self,
at_cds: &[DVec2],
at1: usize,
at2: usize,
at3: usize,
offset: f64,
trunc: bool,
) -> DVec2 {
let v21 = direction_vector(at_cds[at2], at_cds[at1]);
let v23 = direction_vector(at_cds[at2], at_cds[at3]);
let mut v23perp = DVec2::new(-v23.y, v23.x).normalize_or_zero();
let mut bis = v21 + v23;
if bis.length_squared() < 1.0e-6 {
let result = at_cds[at2] - v23perp * offset;
if debug_target_bond().is_some() {
debug_svg_bond_log(format!(
"RUST double_bond_end colinear at1={at1} at2={at2} at3={at3} offset={offset:.15} trunc={trunc} result=({:.15},{:.15})",
result.x, result.y
));
}
return result;
}
bis = bis.normalize();
if v23perp.dot(bis) < 0.0 {
v23perp *= -1.0;
}
if trunc {
if let Some(ip) = line_intersection(
at_cds[at2],
at_cds[at2] + bis,
at_cds[at2] + v23perp * offset,
at_cds[at3] + v23perp * offset,
) {
if debug_target_bond().is_some() {
debug_svg_bond_log(format!(
"RUST double_bond_end trunc_hit at1={at1} at2={at2} at3={at3} offset={offset:.15} result=({:.15},{:.15})",
ip.x, ip.y
));
}
return ip;
}
}
let result = at_cds[at2] + v23perp * offset;
if debug_target_bond().is_some() {
debug_svg_bond_log(format!(
"RUST double_bond_end fallback at1={at1} at2={at2} at3={at3} offset={offset:.15} trunc={trunc} result=({:.15},{:.15})",
result.x, result.y
));
}
result
}
fn make_triple_bond_lines(&mut self, mol: &Molecule, bond: &Bond, offset: f64) {
let b = bond.begin().index();
let e = bond.end().index();
let (end1, end2) = self.adjust_bond_ends_for_labels(b, e);
let saved_b = self.at_cds[b];
let saved_e = self.at_cds[e];
self.at_cds[b] = end1;
self.at_cds[e] = end2;
let perp = calc_perpendicular(self.at_cds[b], self.at_cds[e]);
let line1_b = self.at_cds[b] + perp * offset;
let line1_e = self.at_cds[e] + perp * offset;
self.new_bond_line_from_points(mol, line1_b, line1_e, bond);
let line2_b = self.at_cds[b] - perp * offset;
let line2_e = self.at_cds[e] - perp * offset;
self.new_bond_line_from_points(mol, line2_b, line2_e, bond);
self.at_cds[b] = saved_b;
self.at_cds[e] = saved_e;
self.single_bond_lines.push(self.bonds.len() - 2);
self.single_bond_lines.push(self.bonds.len() - 1);
}
fn make_wedged_bond(&mut self, mol: &Molecule, bond: &Bond) -> Result<(), SvgDrawError> {
let b = bond.begin().index();
let e = bond.end().index();
let too_small = (self.at_cds[b] - self.at_cds[e]).length_squared() < 1e-8;
if too_small {
return Ok(());
}
let saved_mean_bond_length = self.mean_bond_length;
if self.atom_labels[b].is_some() || self.atom_labels[e].is_some() {
self.mean_bond_length *= 2.0;
}
let (end1, end2) = self.adjust_bond_ends_for_labels(b, e);
self.mean_bond_length = saved_mean_bond_length;
let perp = calc_perpendicular(self.at_cds[b], self.at_cds[e]);
let disp = perp * self.options.multiple_bond_offset * self.mean_bond_length / 2.0;
let t1 = end2 + disp;
let t2 = end2 - disp;
let points = vec![end1, t1, t2];
let line_width = if self.options.bond_line_width < 1.0 {
self.options.bond_line_width
} else {
self.options.bond_line_width / 2.0
};
let kind = if bond.direction() == BondDirection::BeginWedge {
WedgeKind::Solid
} else {
WedgeKind::Dashed
};
let (col1, col2) = self.get_bond_colours(mol, bond);
let one_less_dash = e < mol.atoms().len() && atom_degree(mol, e) > 1;
let points = if kind == WedgeKind::Solid {
let mut points = points;
let mut other_bond_vecs = self.find_other_bond_vecs(mol, e, b);
if other_bond_vecs.len() > 2 {
trim_other_bond_vecs(&mut other_bond_vecs);
}
if other_bond_vecs.len() == 2 {
order_other_bond_vecs(&points, &mut other_bond_vecs);
}
if col1 != col2 || self.options.split_bonds {
build_two_colour_wedge_triangles(points, &other_bond_vecs)
} else {
build_single_colour_wedge_triangles(points, &other_bond_vecs)
}
} else {
points
};
self.wedges.push(DrawWedge {
points,
col1,
col2,
width: line_width,
kind,
one_less_dash,
split_bonds: self.options.split_bonds,
atom1_idx: b,
atom2_idx: e,
bond_idx: bond.id().index(),
});
self.bond_draw_order
.push(DrawBondShapeRef::Wedge(self.wedges.len() - 1));
Ok(())
}
fn find_other_bond_vecs(
&self,
mol: &Molecule,
atom_idx: usize,
other_atom_idx: usize,
) -> Vec<DVec2> {
let mut result = Vec::new();
if atom_degree(mol, atom_idx) == 1 || self.atom_labels[atom_idx].is_some() {
return result;
}
for i in 1..atom_degree(mol, atom_idx) {
let Some(third_atom) = other_neighbor(mol, atom_idx, other_atom_idx, i - 1) else {
continue;
};
let Some(bond_idx) = mol.bonds().iter().position(|bond| {
let b = bond.begin().index();
let e = bond.end().index();
(b == atom_idx && e == third_atom) || (b == third_atom && e == atom_idx)
}) else {
continue;
};
let bond = &mol.bonds()[bond_idx];
if bond.order() == BondOrder::Triple {
continue;
}
if bond.order() == BondOrder::Double
&& atom_degree(mol, atom_idx) > 2
&& atom_degree(mol, third_atom) == 1
{
continue;
}
result.push(direction_vector(
self.at_cds[atom_idx],
self.at_cds[third_atom],
));
}
result
}
fn make_dative_bond(&mut self, mol: &Molecule, bond: &Bond, offset: f64) {
let b = bond.begin().index();
let e = bond.end().index();
let (end1, end2) = self.adjust_bond_ends_for_labels(b, e);
let mid = (end1 + end2) * 0.5;
let (col1, col2) = self.get_bond_colours(mol, bond);
let atid2 = if self.options.split_bonds { b } else { e };
self.new_bond_line_with_metadata(
end1,
mid,
col1,
col1,
b,
atid2,
bond.id().index(),
DashPattern::new(),
);
let len = (end2 - end1).length();
if len <= 1.0e-12 {
return;
}
let frac = 2.0 * offset / len;
self.arrows.push(DrawArrow {
begin: mid,
end: end2,
colour: col2,
width: self.options.bond_line_width,
scale_width: self.options.scale_bond_width,
frac,
angle: std::f64::consts::PI / 12.0,
atom1_idx: b,
atom2_idx: atid2,
bond_idx: bond.id().index(),
});
self.bond_draw_order
.push(DrawBondShapeRef::Arrow(self.arrows.len() - 1));
}
fn make_bond_null_query_line(&mut self, bond: &Bond) {
let b = bond.begin().index();
let e = bond.end().index();
let (begin, end) = self.adjust_bond_ends_for_labels(b, e);
let dash = vec![2.0, 2.0];
let colour = DrawColour::new(0.5, 0.5, 0.5);
self.bonds.push(DrawLine {
begin,
end,
colour,
width: self.options.bond_line_width,
scale_width: self.options.scale_bond_width,
dash_pattern: dash,
atom1_idx: b,
atom2_idx: e,
bond_idx: bond.id().index(),
});
self.bond_draw_order
.push(DrawBondShapeRef::Line(self.bonds.len() - 1));
}
fn new_bond_line(&mut self, mol: &Molecule, begin: DVec2, end: DVec2, bond: &Bond) {
self.new_bond_line_from_points(mol, begin, end, bond);
}
fn new_bond_line_from_points(&mut self, mol: &Molecule, begin: DVec2, end: DVec2, bond: &Bond) {
let (col1, col2) = self.get_bond_colours(mol, bond);
self.new_bond_line_with_colours(begin, end, col1, col2, bond);
}
fn new_bond_line_with_colours(
&mut self,
begin: DVec2,
end: DVec2,
col1: DrawColour,
col2: DrawColour,
bond: &Bond,
) {
self.new_bond_line_with_metadata(
begin,
end,
col1,
col2,
bond.begin().index(),
bond.end().index(),
bond.id().index(),
DashPattern::new(),
);
}
fn new_bond_line_with_metadata(
&mut self,
begin: DVec2,
end: DVec2,
col1: DrawColour,
col2: DrawColour,
atom1_idx: usize,
atom2_idx: usize,
bond_idx: usize,
dash_pattern: DashPattern,
) {
if debug_svg_bond_enabled(bond_idx) {
debug_svg_bond_log(format!(
"RUST new_bond_line_with_metadata bond={} begin=({:.15},{:.15}) end=({:.15},{:.15}) col1={:?} col2={:?} atom1_idx={} atom2_idx={} split_bonds={}",
bond_idx,
begin.x,
begin.y,
end.x,
end.y,
col1,
col2,
atom1_idx,
atom2_idx,
self.options.split_bonds
));
}
let line_width = self.options.bond_line_width;
if col1 == col2 && !self.options.split_bonds {
let idx = self.bonds.len();
self.bonds.push(DrawLine {
begin,
end,
colour: col1,
width: line_width,
scale_width: self.options.scale_bond_width,
dash_pattern: dash_pattern.clone(),
atom1_idx,
atom2_idx,
bond_idx,
});
if dash_pattern.is_empty() {
self.single_bond_lines.push(idx);
}
self.bond_draw_order.push(DrawBondShapeRef::Line(idx));
} else {
let mid = (begin + end) / 2.0;
if debug_svg_bond_enabled(bond_idx) {
debug_svg_bond_log(format!(
"RUST new_bond_line split bond={} mid=({:.15},{:.15}) first=({:.15},{:.15})->({:.15},{:.15}) second=({:.15},{:.15})->({:.15},{:.15})",
bond_idx,
mid.x,
mid.y,
begin.x,
begin.y,
mid.x,
mid.y,
mid.x,
mid.y,
end.x,
end.y
));
}
let first_atom2_idx = if self.options.split_bonds {
usize::MAX
} else {
atom2_idx
};
let idx1 = self.bonds.len();
self.bonds.push(DrawLine {
begin,
end: mid,
colour: col1,
width: line_width,
scale_width: self.options.scale_bond_width,
dash_pattern: dash_pattern.clone(),
atom1_idx,
atom2_idx: first_atom2_idx,
bond_idx,
});
if dash_pattern.is_empty() {
self.single_bond_lines.push(idx1);
}
self.bond_draw_order.push(DrawBondShapeRef::Line(idx1));
let second_atom1_idx = if self.options.split_bonds {
atom2_idx
} else {
atom1_idx
};
let idx2 = self.bonds.len();
self.bonds.push(DrawLine {
begin: mid,
end,
colour: col2,
width: line_width,
scale_width: self.options.scale_bond_width,
dash_pattern,
atom1_idx: second_atom1_idx,
atom2_idx,
bond_idx,
});
if self.bonds[idx2].dash_pattern.is_empty() {
self.single_bond_lines.push(idx2);
}
self.bond_draw_order.push(DrawBondShapeRef::Line(idx2));
}
}
fn adjust_bond_ends_for_labels(&self, beg_at_idx: usize, end_at_idx: usize) -> (DVec2, DVec2) {
fn adjust_bond_end_for_string(
end2: DVec2,
padding: f64,
rects: &[StringRect],
move_end: &mut DVec2,
) {
let label_pos = *move_end;
for (rect_idx, rect) in rects.iter().enumerate() {
let shifted = StringRect {
trans: rect.trans + label_pos,
..rect.clone()
};
let (tl, tr, br, bl) = shifted.calc_corners(padding);
if debug_target_bond().is_some() {
debug_svg_bond_log(format!(
"RUST adjustBondEndForString rect={rect_idx} move_end_before=({:.15},{:.15}) end2=({:.15},{:.15}) tl=({:.15},{:.15}) tr=({:.15},{:.15}) br=({:.15},{:.15}) bl=({:.15},{:.15})",
move_end.x,
move_end.y,
end2.x,
end2.y,
tl.x,
tl.y,
tr.x,
tr.y,
br.x,
br.y,
bl.x,
bl.y
));
}
for (p1, p2) in [(tl, tr), (tr, br), (br, bl), (bl, tl)] {
if let Some(ip) = line_intersection(*move_end, end2, p1, p2) {
if debug_target_bond().is_some() {
debug_svg_bond_log(format!(
"RUST adjustBondEndForString hit move_end=({:.15},{:.15}) -> ip=({:.15},{:.15}) edge=({:.15},{:.15})->({:.15},{:.15})",
move_end.x, move_end.y, ip.x, ip.y, p1.x, p1.y, p2.x, p2.y
));
}
*move_end = ip;
}
}
}
}
let mut beg_cds = self.at_cds[beg_at_idx];
let mut end_cds = self.at_cds[end_at_idx];
let padding = 0.033 * self.mean_bond_length;
let debug_target = debug_target_bond().is_some();
if debug_target {
debug_svg_bond_log(format!(
"RUST adjust_bond_ends_for_labels begin beg_at_idx={beg_at_idx} end_at_idx={end_at_idx} beg_cds=({:.15},{:.15}) end_cds=({:.15},{:.15}) padding={:.15} beg_label={} end_label={}",
beg_cds.x,
beg_cds.y,
end_cds.x,
end_cds.y,
padding,
self.atom_labels
.get(beg_at_idx)
.is_some_and(|l| l.is_some()),
self.atom_labels
.get(end_at_idx)
.is_some_and(|l| l.is_some())
));
}
if let Some(Some(label)) = self.atom_labels.get(beg_at_idx) {
adjust_bond_end_for_string(end_cds, padding, &label.rects, &mut beg_cds);
}
if let Some(Some(label)) = self.atom_labels.get(end_at_idx) {
adjust_bond_end_for_string(beg_cds, padding, &label.rects, &mut end_cds);
}
if debug_target {
debug_svg_bond_log(format!(
"RUST adjust_bond_ends_for_labels end beg_cds=({:.15},{:.15}) end_cds=({:.15},{:.15})",
beg_cds.x, beg_cds.y, end_cds.x, end_cds.y
));
}
(beg_cds, end_cds)
}
fn smooth_bond_joins(&mut self, mol: &Molecule) {
for atom in mol.atoms() {
let idx = atom.id().index();
if self.atom_labels.get(idx).and_then(|l| l.as_ref()).is_some() {
continue;
}
let degree = atom_degree(mol, idx);
let mut do_it = degree == 2;
if !do_it && degree == 3 {
for bond in mol.bonds() {
let nbr = if bond.begin().index() == idx {
Some(bond.end().index())
} else if bond.end().index() == idx {
Some(bond.begin().index())
} else {
None
};
if let Some(nbr) = nbr {
if (atom_degree(mol, nbr) == 1 && bond.order() == BondOrder::Double)
|| matches!(
bond.direction(),
BondDirection::BeginWedge | BondDirection::BeginDash
)
{
do_it = true;
break;
}
}
}
}
if !do_it {
continue;
}
let mut done = false;
for i in 0..self.single_bond_lines.len() {
let line1_idx = self.single_bond_lines[i];
let Some(line1) = self.bonds.get(line1_idx) else {
continue;
};
let p1 = line_endpoint_for_atom(line1, idx);
if p1.is_none() {
continue;
}
let p1 = p1.unwrap();
for j in 0..self.single_bond_lines.len() {
if i == j {
continue;
}
let line2_idx = self.single_bond_lines[j];
let Some(line2) = self.bonds.get(line2_idx) else {
continue;
};
let p2 = line_endpoint_for_atom(line2, idx);
if p2.is_none() {
continue;
}
let p2 = p2.unwrap();
if (line_point(line1, p1) - line_point(line2, p2)).length_squared() < 1.0e-6 {
let p12 = if p1 == 1 { 0 } else { 1 };
let p22 = if p2 == 1 { 0 } else { 1 };
let len = if line1.colour == line2.colour {
0.05
} else {
0.025
};
let dv1 = (line_point(line1, p1) - line_point(line1, p12)) * len;
let dv2 = (line_point(line1, p1) - line_point(line2, p22)) * len;
let join = line_point(line1, p1);
if Self::debug_svg_row_active(58) {
eprintln!(
"COSMOL_JOIN atom={} line1={} bond1={} a1=({}, {}) line2={} bond2={} a2=({}, {}) l1=({:.17},{:.17})->({:.17},{:.17}) l2=({:.17},{:.17})->({:.17},{:.17}) p0={:.17},{:.17} p1={:.17},{:.17} p2={:.17},{:.17} bits=({:#018x},{:#018x}) ({:#018x},{:#018x}) ({:#018x},{:#018x})",
idx,
line1_idx,
line1.bond_idx,
line1.atom1_idx,
line1.atom2_idx,
line2_idx,
line2.bond_idx,
line2.atom1_idx,
line2.atom2_idx,
line1.begin.x,
line1.begin.y,
line1.end.x,
line1.end.y,
line2.begin.x,
line2.begin.y,
line2.end.x,
line2.end.y,
(join - dv1).x,
(join - dv1).y,
join.x,
join.y,
(join - dv2).x,
(join - dv2).y,
(join - dv1).x.to_bits(),
(join - dv1).y.to_bits(),
join.x.to_bits(),
join.y.to_bits(),
(join - dv2).x.to_bits(),
(join - dv2).y.to_bits()
);
}
self.join_paths.push(DrawPolyline {
points: vec![join - dv1, join, join - dv2],
colour: line1.colour,
width: line1.width,
scale_width: line1.scale_width,
fill_polys: false,
atom1_idx: None,
atom2_idx: None,
bond_idx: None,
});
done = true;
break;
}
}
if done {
break;
}
}
}
}
fn orient_atom_label(&mut self, at_idx: usize) -> bool {
let Some(orig_label) = self.atom_labels.get(at_idx).and_then(|l| l.as_ref()) else {
return false;
};
if Self::debug_svg_row_active(142) && at_idx == 59 {
eprintln!(
"COSMOL_ROW142_ORIENT_START atom=59 symbol={} cds=({:.17},{:.17}) orient={:?} rects={}",
orig_label.symbol,
orig_label.cds.x,
orig_label.cds.y,
orig_label.orient,
orig_label.rects.len()
);
}
if orig_label.rects.len() == 2
&& orig_label
.rects
.get(1)
.is_some_and(|rect| rect.ch.is_ascii_lowercase())
{
return false;
}
let new_orients = [
[OrientType::S, OrientType::E, OrientType::W],
[OrientType::N, OrientType::W, OrientType::E],
[OrientType::E, OrientType::S, OrientType::N],
[OrientType::W, OrientType::N, OrientType::S],
];
let orig = orig_label.orient;
let pref = match orig {
OrientType::S => 1usize,
OrientType::W => 2usize,
OrientType::E => 3usize,
_ => 0usize,
};
let mut ok = false;
for orient1 in new_orients[pref] {
{
let label = self.atom_labels[at_idx].as_mut().expect("label present");
label.orient = orient1;
label.recalculate_rects(self.font_size * self.font_scale);
if Self::debug_svg_row_active(142) && at_idx == 59 {
eprintln!(
"COSMOL_ROW142_ORIENT_TRY atom=59 orient={:?} cds=({:.17},{:.17})",
label.orient, label.cds.x, label.cds.y
);
}
}
let label = self.atom_labels[at_idx].as_ref().expect("label present");
let mut clashes_labels = false;
if Self::debug_svg_row_active(142) && at_idx == 59 {
for (idx, other) in self.atom_labels.iter().enumerate() {
if idx == at_idx {
continue;
}
let Some(other_label) = other.as_ref() else {
continue;
};
if do_labels_clash(label, other_label) {
clashes_labels = true;
eprintln!(
"COSMOL_ROW142_ORIENT_LABEL_CLASH atom=59 orient={:?} other_atom={} other_orient={:?} self_rects={} other_rects={}",
label.orient,
other_label.atom_idx,
other_label.orient,
label.rects.len(),
other_label.rects.len()
);
break;
}
}
} else {
clashes_labels = self.atom_labels.iter().enumerate().any(|(idx, other)| {
idx != at_idx
&& other
.as_ref()
.is_some_and(|other_label| do_labels_clash(label, other_label))
});
}
let clashes_bonds =
does_label_clash_with_bonds(label, &self.bonds, &self.wedges, &self.join_paths);
if Self::debug_svg_row_active(142) && at_idx == 59 {
eprintln!(
"COSMOL_ROW142_ORIENT_RESULT atom=59 orient={:?} clashes_labels={} clashes_bonds={}",
label.orient, clashes_labels, clashes_bonds
);
}
if !clashes_labels && !clashes_bonds {
ok = true;
break;
}
}
if !ok {
let label = self.atom_labels[at_idx].as_mut().expect("label present");
label.orient = orig;
}
if Self::debug_svg_row_active(142) && at_idx == 59 {
if let Some(label) = self.atom_labels.get(at_idx).and_then(|l| l.as_ref()) {
eprintln!(
"COSMOL_ROW142_ORIENT_END atom=59 ok={} cds=({:.17},{:.17}) orient={:?}",
ok, label.cds.x, label.cds.y, label.orient
);
}
}
ok
}
fn resolve_atom_symbol_clashes(&mut self) {
for at_idx1 in 0..self.atom_labels.len() {
for at_idx2 in 0..self.atom_labels.len() {
if at_idx1 >= at_idx2 {
continue;
}
let Some(label1) = self.atom_labels[at_idx1].as_ref() else {
continue;
};
let Some(label2) = self.atom_labels[at_idx2].as_ref() else {
continue;
};
if !(label1.rects.len() > 1 || label2.rects.len() > 1) {
continue;
}
if do_labels_clash(label1, label2) {
if Self::debug_svg_row_active(142) && (at_idx1 == 59 || at_idx2 == 59) {
eprintln!(
"COSMOL_ROW142_CLASH atom1={} atom2={} rects1={} rects2={}",
at_idx1,
at_idx2,
label1.rects.len(),
label2.rects.len()
);
}
let mut idxs = [-1isize, -1isize];
if label1.rects.len() > 1 {
idxs[0] = at_idx1 as isize;
}
if label2.rects.len() > 1 {
idxs[1] = at_idx2 as isize;
}
if label1.rects.len() > label2.rects.len() {
idxs.swap(0, 1);
}
if !(idxs[0] != -1 && self.orient_atom_label(idxs[0] as usize)) && idxs[1] != -1
{
let _ = self.orient_atom_label(idxs[1] as usize);
}
}
}
}
}
fn extract_radicals(&mut self, mol: &Molecule) {
self.radicals.clear();
for atom in mol.atoms() {
let idx = atom.id().index();
if atom.radical_electrons() == 0 {
continue;
}
let (rect, orient) = self.calc_radical_rect(atom);
if Self::debug_svg_row_active(123) {
eprintln!(
"COSMOL_RADICAL_RECT atom={} count={} orient={:?} trans=({:.17},{:.17}) wh=({:.17},{:.17})",
idx,
atom.radical_electrons(),
orient,
rect.trans.x,
rect.trans.y,
rect.width,
rect.height
);
}
self.radicals.push(DrawRadical {
rect,
orient,
atom_idx: idx,
count: atom.radical_electrons(),
});
}
}
fn extract_cip_codes(&mut self, mol: &Molecule) {
let annotation_font_scale = 0.8; let atom_note_colour = DrawColour::new(0.0, 0.0, 1.0); let bond_note_colour = DrawColour::new(1.0, 0.0, 0.0); let font_size = self.font_size * self.font_scale;
for atom in mol.atoms() {
let idx = atom.id().index();
if let Some(cip) = atom.prop("_CIPCode") {
let cip = format!("({})", cip);
let mut annot = DrawAnnotation::new(
cip,
TextAlignType::Middle,
"CIP_Code".to_string(),
annotation_font_scale,
DVec2::ZERO,
atom_note_colour,
font_size,
);
self.calc_annotation_position_for_atom(mol, idx, &mut annot);
self.annotations.push(annot);
}
}
for bond in mol.bonds() {
let idx = bond.id().index();
let mut cip_code = bond.prop("_CIPCode").map(|s| s.to_string());
if cip_code.is_none() {
if let Some(stereo_prop) = bond.prop("_BondStereo") {
cip_code = match stereo_prop {
"E" | "STEREOE" => Some("E".to_string()),
"Z" | "STEREOZ" => Some("Z".to_string()),
_ => None,
};
}
}
if let Some(cip) = cip_code {
let cip = format!("({})", cip);
let mut annot = DrawAnnotation::new(
cip,
TextAlignType::Middle,
"CIP_Code".to_string(),
annotation_font_scale,
DVec2::ZERO,
bond_note_colour,
font_size,
);
self.calc_annotation_position_for_bond(mol, bond, &mut annot);
self.annotations.push(annot);
}
}
}
fn extract_atom_notes(&mut self, mol: &Molecule) {
let font_size = self.font_size * self.font_scale;
for atom in mol.atoms() {
if let Some(note) = atom.prop("atomNote") {
if !note.is_empty() {
let mut annot = DrawAnnotation::new(
note.to_string(),
TextAlignType::Middle,
"note".to_string(),
self.options.annotation_font_scale,
DVec2::ZERO,
self.options.atom_note_colour,
font_size,
);
self.calc_annotation_position_for_atom(mol, atom.id().index(), &mut annot);
self.annotations.push(annot);
}
}
}
}
fn extract_bond_notes(&mut self, mol: &Molecule) {
let font_size = self.font_size * self.font_scale;
for bond in mol.bonds() {
if let Some(note) = bond.prop("bondNote") {
if !note.is_empty() {
let mut annot = DrawAnnotation::new(
note.to_string(),
TextAlignType::Middle,
"note".to_string(),
self.options.annotation_font_scale,
DVec2::ZERO,
self.options.bond_note_colour,
font_size,
);
self.calc_annotation_position_for_bond(mol, bond, &mut annot);
self.annotations.push(annot);
}
}
}
}
fn extract_mol_notes(&mut self, mol: &Molecule) {
if !self.options.include_annotations {
return;
}
if let Some(note) = mol.prop("molNote") {
if note.is_empty() {
return;
}
let font_size = self.font_size * self.font_scale;
let mut annot = DrawAnnotation::new(
note.to_string(),
TextAlignType::Start,
"molnote".to_string(),
self.options.annotation_font_scale,
DVec2::ZERO,
self.options.annotation_colour,
font_size,
);
let text_width: f64 = annot.rects.iter().map(|r| r.width).sum::<f64>()
* self.font_size
* self.options.annotation_font_scale;
annot.pos = DVec2::new(
(self.draw_width - text_width) / 2.0,
self.draw_height * (1.0 - self.margin_padding),
);
for _i in 0..50 {
let clash = self.does_note_clash(&annot) != 0;
if !clash {
break;
}
annot.pos.y -= 5.0;
}
self.annotations.push(annot);
}
if let Some(note) = mol.prop("atomNote") {
if !note.is_empty() {
let mut annot = DrawAnnotation::new(
note.to_string(),
TextAlignType::Start,
"molnote".to_string(),
self.options.annotation_font_scale,
DVec2::ZERO,
self.options.annotation_colour,
self.font_size * self.font_scale,
);
let text_width: f64 = annot.rects.iter().map(|r| r.width).sum::<f64>()
* self.font_size
* self.font_scale
* self.options.annotation_font_scale;
annot.pos = DVec2::new(
(self.draw_width - text_width) / 2.0,
self.draw_height * (1.0 - self.margin_padding * 3.0),
);
for _i in 0..50 {
let clash = self.does_note_clash(&annot) != 0;
if !clash {
break;
}
annot.pos.y -= 5.0;
}
self.annotations.push(annot);
}
}
}
fn extract_stereo_groups(&mut self, mol: &Molecule) {
let _ = mol;
}
fn extract_highlights(&mut self, mol: &Molecule) {
let highlight_colour = DrawColour::new(1.0, 0.8, 0.2);
if self.options.continuous_highlight {
self.make_continuous_highlights(mol, highlight_colour);
return;
}
if self.options.circle_atoms {
self.make_atom_circle_highlights(mol, highlight_colour);
}
for bond in mol.bonds() {
let idx = bond.id().index();
let is_highlighted = bond
.prop("_highlight")
.is_some_and(|v| v == "1" || v == "true");
if !is_highlighted {
continue;
}
let b = bond.begin().index();
let e = bond.end().index();
self.join_paths.push(DrawPolyline {
points: vec![self.at_cds[b], self.at_cds[e]],
colour: highlight_colour,
width: self.options.bond_line_width * 3.0,
scale_width: false,
fill_polys: false,
atom1_idx: Some(b),
atom2_idx: Some(e),
bond_idx: Some(idx),
});
}
}
fn make_continuous_highlights(&mut self, mol: &Molecule, colour: DrawColour) {
for bond in mol.bonds() {
let idx = bond.id().index();
let is_highlighted = bond
.prop("_highlight")
.is_some_and(|v| v == "1" || v == "true");
if !is_highlighted {
continue;
}
let b = bond.begin().index();
let e = bond.end().index();
self.join_paths.push(DrawPolyline {
points: vec![self.at_cds[b], self.at_cds[e]],
colour,
width: self.options.bond_line_width * 4.0,
scale_width: false,
fill_polys: false,
atom1_idx: Some(b),
atom2_idx: Some(e),
bond_idx: Some(idx),
});
}
let mut seen_atoms = std::collections::HashSet::new();
for bond in mol.bonds() {
let idx = bond.id().index();
let is_highlighted = bond
.prop("_highlight")
.is_some_and(|v| v == "1" || v == "true");
if !is_highlighted {
continue;
}
for &aidx in &[bond.begin().index(), bond.end().index()] {
if seen_atoms.insert(aidx) {
let pos = self.at_cds[aidx];
let radius = 0.35;
let n_segments = 16;
let mut points = Vec::with_capacity(n_segments);
for i in 0..n_segments {
let ang = 2.0 * std::f64::consts::PI * i as f64 / n_segments as f64;
points.push(DVec2::new(
pos.x + radius * ang.cos(),
pos.y + radius * ang.sin(),
));
}
self.post_shapes.push(DrawPolyline {
points,
colour,
width: self.options.bond_line_width * 2.0,
scale_width: false,
fill_polys: false,
atom1_idx: Some(aidx),
atom2_idx: None,
bond_idx: None,
});
}
}
}
}
fn make_atom_circle_highlights(&mut self, mol: &Molecule, colour: DrawColour) {
for atom in mol.atoms() {
let idx = atom.id().index();
let is_highlighted = atom
.prop("_highlight")
.is_some_and(|v| v == "1" || v == "true");
if !is_highlighted {
continue;
}
let pos = self.at_cds[idx];
let radius = 0.5; let n_segments = 24;
let mut points = Vec::with_capacity(n_segments);
for i in 0..n_segments {
let ang = 2.0 * std::f64::consts::PI * i as f64 / n_segments as f64;
points.push(DVec2::new(
pos.x + radius * ang.cos(),
pos.y + radius * ang.sin(),
));
}
self.post_shapes.push(DrawPolyline {
points,
colour,
width: self.options.bond_line_width * 2.0,
scale_width: false,
fill_polys: false,
atom1_idx: Some(idx),
atom2_idx: None,
bond_idx: None,
});
}
}
fn extract_regions(&mut self) {
}
fn extract_attachments(&mut self, mol: &Molecule) {
if !self.options.dummies_are_attachments {
return;
}
for atom in mol.atoms() {
let idx = atom.id().index();
if atom.prop("atomLabel").is_some() {
continue;
}
if atom.atomic_number() == 0 && atom_degree(mol, idx) == 1 {
let at1_cds = self.at_cds[idx];
let nbrs = atom_neighbors(mol, idx);
if nbrs.is_empty() {
continue;
}
let nbr_idx = nbrs[0];
let at2_cds = self.at_cds[nbr_idx];
let perp = calc_perpendicular(at1_cds, at2_cds);
let p1 = DVec2::new(at1_cds.x - perp.x * 0.5, at1_cds.y - perp.y * 0.5);
let p2 = DVec2::new(at1_cds.x + perp.x * 0.5, at1_cds.y + perp.y * 0.5);
let colour = DrawColour::new(0.5, 0.5, 0.5);
self.join_paths.push(DrawPolyline {
points: vec![p1, p2],
colour,
width: self.options.bond_line_width,
scale_width: false,
fill_polys: false,
atom1_idx: Some(idx),
atom2_idx: Some(nbr_idx),
bond_idx: None,
});
}
}
}
fn extract_sgroup_data(&mut self, mol: &Molecule) {
if !self.options.include_annotations {
return;
}
let sgs = mol.substance_groups();
if sgs.is_empty() {
return;
}
let font_size = self.font_size * self.font_scale;
for sg in sgs {
let typ = sg.props().get("TYPE").cloned();
if typ.as_deref() == Some("DAT") {
let mut text = String::new();
let data_fields = sg.data_fields();
for df in data_fields {
text.push_str(df);
text.push('|');
}
if !text.is_empty() {
text.pop();
}
if text.is_empty() {
continue;
}
let atom_idx = if !sg.atoms().is_empty() {
Some(sg.atoms()[0].index())
} else {
None
};
let mut located = false;
let mut orig_loc = DVec2::ZERO;
if let Some(field_disp) = sg.props().get("FIELDDISP") {
if field_disp.len() >= 26 {
if let (Ok(xp), Ok(yp)) = (
field_disp[0..10].trim().parse::<f64>(),
field_disp[10..20].trim().parse::<f64>(),
) {
orig_loc = DVec2::new(xp, -yp);
if field_disp.as_bytes().get(25) == Some(&b'R') {
if let Some(ai) = atom_idx {
if xp.abs() > 1e-3 || yp.abs() > 1e-3 {
orig_loc.x += self.at_cds[ai].x;
orig_loc.y -= self.at_cds[ai].y;
located = true;
}
}
} else {
located = true;
}
}
}
}
let mut annot = DrawAnnotation::new(
text,
TextAlignType::Start,
"note".to_string(),
self.options.annotation_font_scale,
DVec2::ZERO,
self.options.annotation_colour,
font_size,
);
if located {
annot.pos = orig_loc;
} else if let Some(ai) = atom_idx {
self.calc_annotation_position_for_atom(mol, ai, &mut annot);
}
self.annotations.push(annot);
}
}
}
fn extract_variable_bonds(&mut self, mol: &Molecule) {
let n_atoms = mol.atoms().len();
for bond in mol.bonds() {
let endpts = bond.prop("_MolFileBondEndPts");
let attach = bond.prop("_MolFileBondAttach");
if let (Some(endpts), Some(_attach)) = (endpts, attach) {
let oat_strs: Vec<&str> = endpts.split_whitespace().collect();
let mut atoms_involved = vec![false; n_atoms];
for oat_str in &oat_strs {
if let Ok(mut oat) = oat_str.parse::<usize>() {
if oat == 0 || oat > n_atoms {
continue; }
oat -= 1; atoms_involved[oat] = true;
let center = self.at_cds[oat];
let radius = self.options.variable_atom_radius;
let n_segments = 16;
let mut points = Vec::with_capacity(n_segments);
for i in 0..n_segments {
let ang = 2.0 * std::f64::consts::PI * i as f64 / n_segments as f64;
points.push(DVec2::new(
center.x + radius * ang.cos(),
center.y + radius * ang.sin(),
));
}
self.join_paths.push(DrawPolyline {
points,
colour: self.options.variable_attachment_colour,
width: 1.0,
scale_width: false,
fill_polys: false,
atom1_idx: Some(oat),
atom2_idx: None,
bond_idx: None,
});
}
}
for b in mol.bonds() {
let ba = b.begin().index();
let ea = b.end().index();
if atoms_involved[ba] && atoms_involved[ea] {
self.join_paths.push(DrawPolyline {
points: vec![self.at_cds[ba], self.at_cds[ea]],
colour: self.options.variable_attachment_colour,
width: self.options.bond_line_width
* self.options.variable_bond_width_multiplier,
scale_width: false,
fill_polys: false,
atom1_idx: Some(ba),
atom2_idx: Some(ea),
bond_idx: Some(b.id().index()),
});
}
}
if mol.atoms()[bond.begin().index()].atomic_number() == 0 {
self.atom_labels[bond.begin().index()] = None;
}
}
}
}
fn extract_brackets(&mut self, mol: &Molecule) {
let sgs = mol.substance_groups();
if sgs.is_empty() {
return;
}
let font_size = self.font_size * self.font_scale;
for sg in sgs {
let display = match sg.display() {
Some(d) => d,
None => continue,
};
if display.brackets.is_empty() {
continue;
}
let mut ref_pt = DVec2::ZERO;
if !sg.atoms().is_empty() {
let mut x_min = f64::MAX / 2.0;
let mut y_min = f64::MAX / 2.0;
let mut x_max = f64::MIN / 2.0;
let mut y_max = f64::MIN / 2.0;
for brk in &display.brackets {
let p1 = DVec2::new(brk.p1[0], -brk.p1[1]);
let p2 = DVec2::new(brk.p2[0], -brk.p2[1]);
x_min = x_min.min(p1.x).min(p2.x);
y_min = y_min.min(p1.y).min(p2.y);
x_max = x_max.max(p1.x).max(p2.x);
y_max = y_max.max(p1.y).max(p2.y);
}
let mut num_in = 0;
for aidx in sg.atoms() {
let a_cds = self.at_cds[aidx.index()];
if a_cds.x >= x_min && a_cds.x <= x_max && a_cds.y >= y_min && a_cds.y <= y_max
{
ref_pt += a_cds;
num_in += 1;
}
}
if num_in > 0 {
ref_pt /= num_in as f64;
} else {
for aidx in sg.atoms() {
ref_pt += self.at_cds[aidx.index()];
}
ref_pt /= sg.atoms().len() as f64;
}
}
let sg_atom_indices: std::collections::HashSet<usize> =
sg.atoms().iter().map(|a| a.index()).collect();
let mut sg_bond_segments: Vec<(DVec2, DVec2)> = Vec::new();
for bnd_idx in sg.bonds() {
let bnd = &mol.bonds()[bnd_idx.index()];
let ba = bnd.begin().index();
let ea = bnd.end().index();
if sg_atom_indices.contains(&ba) && sg_atom_indices.contains(&ea) {
sg_bond_segments.push((self.at_cds[ba], self.at_cds[ea]));
} else if sg_atom_indices.contains(&ba) {
sg_bond_segments.push((self.at_cds[ba], self.at_cds[ea]));
} else if sg_atom_indices.contains(&ea) {
sg_bond_segments.push((self.at_cds[ea], self.at_cds[ba]));
}
}
let mut num_brackets = 0;
for brk in &display.brackets {
num_brackets += 1;
let p1 = DVec2::new(brk.p1[0], -brk.p1[1]);
let p2 = DVec2::new(brk.p2[0], -brk.p2[1]);
let points = get_bracket_points(p1, p2, ref_pt, &sg_bond_segments);
self.post_shapes.push(DrawPolyline {
points,
colour: DrawColour::new(0.0, 0.0, 0.0),
width: self.options.bond_line_width,
scale_width: false,
fill_polys: false,
atom1_idx: None,
atom2_idx: None,
bond_idx: None,
});
}
if self.options.include_annotations {
let last_shape_idx = self.post_shapes.len() - 1;
let brk_shp = &self.post_shapes[last_shape_idx];
let longline = brk_shp.points[1] - brk_shp.points[2];
let longline_len = longline.length();
let cos45 = 1.0 / (2.0_f64).sqrt();
let horizontal = if longline_len > 1e-10 {
(longline / longline_len).x.abs() > cos45
} else {
false
};
let mut label_brk = last_shape_idx;
for i in 1..num_brackets {
let brk_shp_i = &self.post_shapes[last_shape_idx - i];
if horizontal {
if brk_shp_i.points[2].y > self.post_shapes[label_brk].points[2].y {
label_brk = last_shape_idx - i;
}
} else {
if brk_shp_i.points[2].x > self.post_shapes[label_brk].points[2].x {
label_brk = last_shape_idx - i;
}
}
}
if let Some(connect) = sg.props().get("CONNECT") {
let brk_shp = &self.post_shapes[label_brk];
let mut bot_pt = brk_shp.points[2];
let mut brk_pt = brk_shp.points[3];
if (!horizontal && brk_shp.points[1].y < bot_pt.y)
|| (horizontal && brk_shp.points[1].x > bot_pt.x)
{
bot_pt = brk_shp.points[1];
brk_pt = brk_shp.points[0];
}
let mut da = DrawAnnotation::new(
connect.clone(),
TextAlignType::Middle,
"connect".to_string(),
self.options.annotation_font_scale,
bot_pt + (bot_pt - brk_pt),
DrawColour::new(0.0, 0.0, 0.0),
font_size,
);
if brk_pt.x < bot_pt.x {
da.align = TextAlignType::Start;
}
self.annotations.push(da);
}
let label = sg
.props()
.get("LABEL")
.or_else(|| sg.props().get("TYPE"))
.cloned();
if let Some(mut label) = label {
if label == "GEN" {
continue;
}
if sg.props().get("LABEL").is_none() {
label = label.to_lowercase();
}
let brk_shp = &self.post_shapes[label_brk];
let top_pt = brk_shp.points[1];
let brk_pt = brk_shp.points[0];
let final_top = if (!horizontal && brk_shp.points[2].y > top_pt.y)
|| (horizontal && brk_shp.points[2].x < top_pt.x)
{
brk_shp.points[2]
} else {
top_pt
};
let final_brk = if (!horizontal && brk_shp.points[2].y > top_pt.y)
|| (horizontal && brk_shp.points[2].x < top_pt.x)
{
brk_shp.points[3]
} else {
brk_pt
};
let mut da = DrawAnnotation::new(
label,
TextAlignType::Middle,
"connect".to_string(),
self.options.annotation_font_scale,
final_top + (final_top - final_brk),
DrawColour::new(0.0, 0.0, 0.0),
font_size,
);
if final_brk.x < final_top.x {
da.align = TextAlignType::Start;
}
self.annotations.push(da);
}
}
}
}
fn extract_link_nodes(&mut self, mol: &Molecule) {
if mol.prop("molFileLinkNodes").is_none() {
return;
}
let font_size = self.font_size * self.font_scale;
let crossing_frac = 0.333;
let length_frac = 0.333;
let mut label_pt = DVec2::new(-1000.0, -1000.0);
let mut label_perp = DVec2::ZERO;
for bond in mol.bonds() {
let endpts = bond.prop("_MolFileBondEndPts");
if endpts.is_none() {
continue;
}
let ba = bond.begin().index();
let ea = bond.end().index();
let start_loc = self.at_cds[ba];
let end_loc = self.at_cds[ea];
let vect = end_loc - start_loc;
let offset = vect * crossing_frac;
let crossing_pt = start_loc + offset;
let perp = DVec2::new(vect.y, -vect.x);
let perp = perp * length_frac;
let p1 = crossing_pt + perp / 2.0;
let p2 = crossing_pt - perp / 2.0;
let bond_segments: Vec<(DVec2, DVec2)> = Vec::new();
let points = get_bracket_points(p1, p2, start_loc, &bond_segments);
self.post_shapes.push(DrawPolyline {
points,
colour: DrawColour::new(0.0, 0.0, 0.0),
width: self.options.bond_line_width,
scale_width: false,
fill_polys: false,
atom1_idx: Some(ba),
atom2_idx: Some(ea),
bond_idx: Some(bond.id().index()),
});
if p1.x > label_pt.x {
label_pt = p1;
label_perp = crossing_pt - start_loc;
}
if p2.x > label_pt.x {
label_pt = p2;
label_perp = crossing_pt - start_loc;
}
}
if label_pt.x > -500.0 {
if self.options.include_annotations {
let perp_len = label_perp.length();
let perp = if perp_len > 1e-10 {
label_perp / perp_len * 0.2
} else {
DVec2::new(0.2, 0.0)
};
let da = DrawAnnotation::new(
"(1-1)".to_string(), TextAlignType::Start,
"linknode".to_string(),
self.options.annotation_font_scale,
label_pt + perp,
DrawColour::new(0.0, 0.0, 0.0),
font_size,
);
self.annotations.push(da);
}
}
}
fn extract_close_contacts(&mut self) {
if self.options.flag_close_contacts_dist < 0 {
return;
}
let tol = (self.options.flag_close_contacts_dist as f64)
* (self.options.flag_close_contacts_dist as f64);
let mut flagged = vec![false; self.at_cds.len()];
let trans = DVec2::new(-self.x_min, -self.y_min);
let scale = DVec2::new(self.scale, self.scale);
let scaled_ranges = DVec2::new(self.scale * self.x_range, self.scale * self.y_range);
let to_centre = DVec2::new(
(self.draw_width - scaled_ranges.x) / 2.0 + self.width * self.margin_padding,
(self.draw_height - scaled_ranges.y) / 2.0 + self.height * self.margin_padding,
);
for i in 0..self.at_cds.len() {
if flagged[i] {
continue;
}
let ci = transform_point(self.at_cds[i], trans, scale, to_centre);
for j in (i + 1)..self.at_cds.len() {
if flagged[j] {
continue;
}
let cj = transform_point(self.at_cds[j], trans, scale, to_centre);
let d = (cj - ci).length_squared();
if d <= tol {
flagged[i] = true;
flagged[j] = true;
break;
}
}
if flagged[i] {
let p1 = ci;
let p2 = p1;
let offset = DVec2::new(0.1 * self.scale, 0.1 * self.scale);
let p1 = p1 - offset;
let p2 = p2 + offset;
let points = vec![
p1,
DVec2::new(p1.x, p2.y),
p2,
DVec2::new(p2.x, p1.y),
p1, ];
self.post_shapes.push(DrawPolyline {
points,
colour: DrawColour::new(1.0, 0.0, 0.0),
width: self.options.bond_line_width,
scale_width: false,
fill_polys: false,
atom1_idx: Some(i),
atom2_idx: None,
bond_idx: None,
});
}
}
}
fn calc_annotation_position_for_atom(
&self,
mol: &Molecule,
atom_idx: usize,
annot: &mut DrawAnnotation,
) {
let start_ang = self.calc_note_start_angle(mol, atom_idx);
let at_cds = self.at_cds[atom_idx];
let rad_step = 0.25;
let mut least_worst_pos = at_cds;
let mut least_worst_score = 100;
for j in 1..4 {
let note_rad = j as f64 * rad_step;
if j == 1
&& self
.atom_labels
.get(atom_idx)
.and_then(|l| l.as_ref())
.is_some()
{
continue;
}
for i in 0..12 {
let ang = start_ang + i as f64 * 30.0_f64.to_radians();
annot.pos = DVec2::new(
at_cds.x + ang.cos() * note_rad,
at_cds.y + ang.sin() * note_rad,
);
let clash_score = self.does_note_clash(annot);
if clash_score == 0 {
return;
} else if clash_score < least_worst_score {
least_worst_score = clash_score;
least_worst_pos = annot.pos;
}
}
}
annot.pos = least_worst_pos;
}
fn calc_annotation_position_for_bond(
&self,
mol: &Molecule,
bond: &Bond,
annot: &mut DrawAnnotation,
) {
let b = bond.begin().index();
let e = bond.end().index();
let mut at1_cds = self.at_cds[b];
let mut at2_cds = self.at_cds[e];
if (at1_cds - at2_cds).length_squared() < 0.0001 {
at2_cds.x += 0.1;
at2_cds.y += 0.1;
}
let perp = calc_perpendicular(at1_cds, at2_cds);
let bond_vec = direction_vector(at1_cds, at2_cds);
let bond_len = (at1_cds - at2_cds).length();
let mid_offsets = [0.5, 0.33, 0.66, 0.25, 0.75];
let offset_step = self.options.multiple_bond_offset;
let mut least_worst_pos = (at1_cds + at2_cds) / 2.0;
let mut least_worst_score = 100;
for mo in &mid_offsets {
let mid = at1_cds + bond_vec * bond_len * mo;
for j in 1..6 {
if j == 1
&& matches!(
bond.order(),
BondOrder::Double | BondOrder::Triple | BondOrder::Aromatic
)
{
continue;
}
let offset = j as f64 * offset_step;
annot.pos = mid + perp * offset;
let cs = self.does_note_clash(annot);
if cs == 0 {
return;
}
if cs < least_worst_score {
least_worst_pos = annot.pos;
least_worst_score = cs;
}
annot.pos = mid - perp * offset;
let cs = self.does_note_clash(annot);
if cs == 0 {
return;
}
if cs < least_worst_score {
least_worst_pos = annot.pos;
least_worst_score = cs;
}
}
}
annot.pos = least_worst_pos;
}
fn calc_note_start_angle(&self, mol: &Molecule, atom_idx: usize) -> f64 {
let degree = atom_degree(mol, atom_idx);
if degree == 0 {
return std::f64::consts::FRAC_PI_2;
}
let at_cds = self.at_cds[atom_idx];
let mut bond_vecs: Vec<DVec2> = Vec::new();
for &nbr in &atom_neighbors(mol, atom_idx) {
let bond_vec = if (at_cds - self.at_cds[nbr]).length_squared() < 0.0001 {
DVec2::new(0.1, 0.1)
} else {
direction_vector(at_cds, self.at_cds[nbr])
};
bond_vecs.push(bond_vec.normalize());
}
let ret_vec = if bond_vecs.len() == 1 {
if self
.atom_labels
.get(atom_idx)
.and_then(|l| l.as_ref())
.is_none()
{
DVec2::new(bond_vecs[0].y, -bond_vecs[0].x)
} else {
-bond_vecs[0]
}
} else if bond_vecs.len() == 2 {
let mut rv = bond_vecs[0] + bond_vecs[1];
if rv.length_squared() > 1.0e-6 {
rv *= -1.0;
} else {
rv = DVec2::new(-bond_vecs[0].y, bond_vecs[0].x).normalize();
}
rv
} else {
let mut discrim = 4.0 * std::f64::consts::PI / bond_vecs.len() as f64;
let mut ret = bond_vecs[0] + bond_vecs[1];
'outer: for i in 0..bond_vecs.len() - 1 {
for j in (i + 1)..bond_vecs.len() {
let ang = bond_vecs[i].dot(bond_vecs[j]).acos();
if ang < discrim {
ret = bond_vecs[i] + bond_vecs[j];
ret = ret.normalize();
discrim = -1.0;
break 'outer;
}
}
}
if discrim > 0.0 {
ret *= -1.0;
}
ret
};
f64::atan2(ret_vec.y, ret_vec.x)
}
fn does_note_clash(&self, annot: &DrawAnnotation) -> i32 {
let padding = self.scale * 0.04;
for rect in &annot.rects {
let adjusted = StringRect {
trans: rect.trans + annot.pos,
..rect.clone()
};
let cs = self.does_rect_clash_with_score(&adjusted, padding);
if cs != 0 {
return cs;
}
}
0
}
fn does_rect_clash_with_score(&self, rect: &StringRect, padding: f64) -> i32 {
for &(at1, at2) in &self.raw_double_bonds {
if rect_clashes_with_line(rect, self.at_cds[at1], self.at_cds[at2], 0.0) {
return 1;
}
}
for label in self.atom_labels.iter().flatten() {
if label_rects_intersect(&label.rects, label.cds, rect, padding) {
return 2;
}
}
for annot in &self.annotations {
for ar in &annot.rects {
let adjusted = StringRect {
trans: ar.trans + annot.pos,
..ar.clone()
};
if rect.does_it_intersect(&adjusted, padding) {
return 3;
}
}
}
0
}
fn calc_radical_rect(&self, atom: &Atom) -> (StringRect, OrientType) {
let idx = atom.id().index();
let at_cds = self.at_cds[idx];
let spot_rad = self.radical_spot_radius_unscaled();
let orient = self.atom_orients.get(idx).copied().unwrap_or(OrientType::C);
let rad_size =
(4.0 * f64::from(atom.radical_electrons()) - 2.0) * spot_rad / self.font_scale_factor();
let (x_min, x_max, y_min, y_max) = if let Some(Some(label)) = self.atom_labels.get(idx) {
let mut x_min = f64::MAX;
let mut x_max = f64::MIN;
let mut y_min = f64::MAX;
let mut y_max = f64::MIN;
label.find_extremes(&mut x_min, &mut x_max, &mut y_min, &mut y_max);
(x_min, x_max, y_min, y_max)
} else {
(
at_cds.x - 3.0 * spot_rad,
at_cds.x + 3.0 * spot_rad,
at_cds.y - 3.0 * spot_rad,
at_cds.y + 3.0 * spot_rad,
)
};
for trial in std::iter::once(orient).chain(
[OrientType::N, OrientType::E, OrientType::S, OrientType::W]
.into_iter()
.filter(move |fallback| *fallback != orient),
) {
let rect = radical_rect_for_orientation(
trial, at_cds, x_min, x_max, y_min, y_max, spot_rad, rad_size,
);
if !self.does_rect_clash(&rect, 0.0) {
return (rect, trial);
}
}
(
radical_rect_for_orientation(
OrientType::N,
at_cds,
x_min,
x_max,
y_min,
y_max,
spot_rad,
rad_size,
),
OrientType::N,
)
}
fn does_rect_clash(&self, rect: &StringRect, padding: f64) -> bool {
for bond in &self.bonds {
if rect_clashes_with_line(rect, bond.begin, bond.end, padding) {
return true;
}
}
for wedge in &self.wedges {
match wedge.kind {
WedgeKind::Solid => {
for tri in wedge.points.chunks_exact(3) {
if rect_clashes_with_triangle(rect, tri[0], tri[1], tri[2], padding) {
return true;
}
}
}
WedgeKind::Dashed => {
if wedge.points.len() >= 3
&& rect_clashes_with_triangle(
rect,
wedge.points[0],
wedge.points[1],
wedge.points[2],
padding,
)
{
return true;
}
}
}
}
for label in self.atom_labels.iter().flatten() {
if label_rects_intersect(&label.rects, label.cds, rect, padding) {
return true;
}
}
false
}
fn calculate_scale(&mut self) {
self.find_extremes();
self.refresh_ranges_from_extremes();
self.draw_width = self.width * (1.0 - 2.0 * self.margin_padding);
self.draw_height = self.height * (1.0 - 2.0 * self.margin_padding);
self.mol_height = self.draw_height;
if Self::debug_svg_row_active(58) {
eprintln!(
"COSMOL_EXTREME x_min={:.17} x_max={:.17} y_min={:.17} y_max={:.17} x_range={:.17} y_range={:.17} scale={:.17} bits=({:#018x},{:#018x},{:#018x},{:#018x},{:#018x},{:#018x},{:#018x})",
self.x_min,
self.x_max,
self.y_min,
self.y_max,
self.x_range,
self.y_range,
self.scale,
self.x_min.to_bits(),
self.x_max.to_bits(),
self.y_min.to_bits(),
self.y_max.to_bits(),
self.x_range.to_bits(),
self.y_range.to_bits(),
self.scale.to_bits()
);
}
if Self::debug_svg_row_active(123) {
eprintln!(
"COSMOL_CALC_SCALE_PRE x_min={:.17} x_max={:.17} y_min={:.17} y_max={:.17} x_range={:.17} y_range={:.17} draw_width={:.17} mol_height={:.17} old_scale={:.17} font_scale={:.17}",
self.x_min,
self.x_max,
self.y_min,
self.y_max,
self.x_range,
self.y_range,
self.draw_width,
self.mol_height,
self.scale,
self.font_scale
);
}
let new_scale = (self.draw_width / self.x_range).min(self.mol_height / self.y_range);
let scale_mult = new_scale / self.scale;
self.scale *= scale_mult;
self.font_scale *= scale_mult;
if Self::debug_svg_row_active(123) {
eprintln!(
"COSMOL_CALC_SCALE_POST new_scale={:.17} scale={:.17} font_scale={:.17}",
new_scale, self.scale, self.font_scale
);
}
}
fn find_extremes(&mut self) {
self.x_min = f64::MAX;
self.x_max = f64::MIN;
self.y_min = f64::MAX;
self.y_max = f64::MIN;
for line in &self.bonds {
for pt in [line.begin, line.end] {
Self::include_extreme_point(
&mut self.x_min,
&mut self.x_max,
&mut self.y_min,
&mut self.y_max,
pt,
);
}
}
for wedge in &self.wedges {
for &pt in &wedge.points {
Self::include_extreme_point(
&mut self.x_min,
&mut self.x_max,
&mut self.y_min,
&mut self.y_max,
pt,
);
}
}
for poly in &self.join_paths {
for &pt in &poly.points {
Self::include_extreme_point(
&mut self.x_min,
&mut self.x_max,
&mut self.y_min,
&mut self.y_max,
pt,
);
}
}
for arrow in &self.arrows {
for pt in [arrow.begin, arrow.end] {
Self::include_extreme_point(
&mut self.x_min,
&mut self.x_max,
&mut self.y_min,
&mut self.y_max,
pt,
);
}
}
for shape in &self.post_shapes {
for &pt in &shape.points {
Self::include_extreme_point(
&mut self.x_min,
&mut self.x_max,
&mut self.y_min,
&mut self.y_max,
pt,
);
}
}
for annot in &self.annotations {
annot.find_extremes(
&mut self.x_min,
&mut self.x_max,
&mut self.y_min,
&mut self.y_max,
);
}
for shape in &self.draw_items {
for &pt in &shape.points {
Self::include_extreme_point(
&mut self.x_min,
&mut self.x_max,
&mut self.y_min,
&mut self.y_max,
pt,
);
}
}
for label in self.atom_labels.iter().flatten() {
label.find_extremes(
&mut self.x_min,
&mut self.x_max,
&mut self.y_min,
&mut self.y_max,
);
if Self::debug_svg_row_active(123) {
let mut lx_min = f64::MAX;
let mut lx_max = f64::MIN;
let mut ly_min = f64::MAX;
let mut ly_max = f64::MIN;
label.find_extremes(&mut lx_min, &mut lx_max, &mut ly_min, &mut ly_max);
eprintln!(
"COSMOL_LABEL_EXTREME atom={} symbol={} xmin={:.17} xmax={:.17} ymin={:.17} ymax={:.17}",
label.atom_idx, label.symbol, lx_min, lx_max, ly_min, ly_max
);
}
}
for radical in &self.radicals {
let cx = radical.rect.trans.x;
let cy = radical.rect.trans.y;
let hw = radical.rect.width / 2.0;
let hh = radical.rect.height / 2.0;
if Self::debug_svg_row_active(123) {
eprintln!(
"COSMOL_RADICAL_EXTREME atom={} count={} xmin={:.17} xmax={:.17} ymin={:.17} ymax={:.17}",
radical.atom_idx,
radical.count,
cx - hw,
cx + hw,
cy - hh,
cy + hh
);
}
for pt in [DVec2::new(cx - hw, cy - hh), DVec2::new(cx + hw, cy + hh)] {
Self::include_extreme_point(
&mut self.x_min,
&mut self.x_max,
&mut self.y_min,
&mut self.y_max,
pt,
);
}
}
if self.at_cds.is_empty() {
self.x_min = -1.0;
self.x_max = 1.0;
self.y_min = -1.0;
self.y_max = 1.0;
} else if self.x_min == f64::MAX {
for &pt in &self.at_cds {
Self::include_extreme_point(
&mut self.x_min,
&mut self.x_max,
&mut self.y_min,
&mut self.y_max,
pt,
);
}
}
}
fn refresh_ranges_from_extremes(&mut self) {
self.x_range = self.x_max - self.x_min;
self.y_range = self.y_max - self.y_min;
if self.x_range < 1.0e-4 {
self.x_range = 2.0;
self.x_min -= 1.0;
self.x_max += 1.0;
}
if self.y_range < 1.0e-4 {
self.y_range = 2.0;
self.y_min -= 1.0;
self.y_max += 1.0;
}
if Self::debug_svg_row_active(58) {
eprintln!(
"COSMOL_RANGE x_min={:.17} x_max={:.17} y_min={:.17} y_max={:.17} x_range={:.17} y_range={:.17} bits=({:#018x},{:#018x},{:#018x},{:#018x},{:#018x},{:#018x})",
self.x_min,
self.x_max,
self.y_min,
self.y_max,
self.x_range,
self.y_range,
self.x_min.to_bits(),
self.x_max.to_bits(),
self.y_min.to_bits(),
self.y_max.to_bits(),
self.x_range.to_bits(),
self.y_range.to_bits()
);
}
}
fn change_to_draw_coords(&mut self) {
let trans = DVec2::new(-self.x_min, -self.y_min);
let scale = DVec2::new(self.scale, self.scale);
let scaled_ranges = DVec2::new(self.scale * self.x_range, self.scale * self.y_range);
let to_centre = DVec2::new(
(self.draw_width - scaled_ranges.x) / 2.0 + self.width * self.margin_padding,
(self.draw_height - scaled_ranges.y) / 2.0 + self.height * self.margin_padding,
);
if Self::debug_svg_row_active(58) {
eprintln!(
"COSMOL_TRANSFORM trans={:.17},{:.17} scale={:.17},{:.17} to_centre={:.17},{:.17} x_range={:.17} y_range={:.17} bits=({:#018x},{:#018x}) ({:#018x},{:#018x}) ({:#018x},{:#018x})",
trans.x,
trans.y,
scale.x,
scale.y,
to_centre.x,
to_centre.y,
self.x_range,
self.y_range,
trans.x.to_bits(),
trans.y.to_bits(),
scale.x.to_bits(),
scale.y.to_bits(),
to_centre.x.to_bits(),
to_centre.y.to_bits()
);
}
for label in self.atom_labels.iter_mut().flatten() {
label.cds = transform_point(label.cds, trans, scale, to_centre);
label.recalculate_rects(self.font_size * self.font_scale);
}
for line in &mut self.bonds {
line.begin = transform_point(line.begin, trans, scale, to_centre);
line.end = transform_point(line.end, trans, scale, to_centre);
}
for wedge in &mut self.wedges {
for pt in &mut wedge.points {
*pt = transform_point(*pt, trans, scale, to_centre);
}
}
for poly in &mut self.join_paths {
for pt in &mut poly.points {
*pt = transform_point(*pt, trans, scale, to_centre);
}
}
if Self::debug_svg_row_active(58) {
for (idx, poly) in self.join_paths.iter().enumerate() {
eprintln!(
"COSMOL_JOIN_DRAW idx={} pts={} bits={}",
idx,
poly.points
.iter()
.map(|pt| format!("({:.17},{:.17})", pt.x, pt.y))
.collect::<Vec<_>>()
.join(" "),
poly.points
.iter()
.map(|pt| format!("({:#018x},{:#018x})", pt.x.to_bits(), pt.y.to_bits()))
.collect::<Vec<_>>()
.join(" ")
);
}
}
for arrow in &mut self.arrows {
arrow.begin = transform_point(arrow.begin, trans, scale, to_centre);
arrow.end = transform_point(arrow.end, trans, scale, to_centre);
}
for radical in &mut self.radicals {
let scaled_pt = transform_point(
DVec2::new(radical.rect.trans.x, radical.rect.trans.y),
trans,
scale,
to_centre,
);
radical.rect.trans = scaled_pt;
radical.rect.width *= self.font_scale;
radical.rect.height *= self.font_scale;
}
for annot in &mut self.annotations {
annot.pos = transform_point(annot.pos, trans, scale, to_centre);
annot.rects = get_string_rects(
&annot.text,
OrientType::C,
self.font_size * self.font_scale * annot.font_scale,
);
}
for poly in &mut self.post_shapes {
for pt in &mut poly.points {
*pt = transform_point(*pt, trans, scale, to_centre);
}
}
}
fn line_endpoint_idx(&self, line: &DrawLine, atom_idx: usize) -> Option<usize> {
if line.atom1_idx == atom_idx {
Some(0)
} else if line.atom2_idx == atom_idx {
Some(1)
} else {
None
}
}
fn adjust_bonds_on_solid_wedge_ends(&mut self, mol: &Molecule) {
for bond in mol.bonds() {
if bond.direction() != BondDirection::BeginWedge {
continue;
}
let end_idx = bond.end().index();
if atom_degree(mol, end_idx) != 2 || self.atom_labels[end_idx].is_some() {
continue;
}
let Some(third_atom) = other_neighbor(mol, end_idx, bond.begin().index(), 0) else {
continue;
};
let Some(adj_bond) = bond_between_atoms(mol, end_idx, third_atom) else {
continue;
};
if adj_bond.order() == BondOrder::Triple {
continue;
}
let b1 = direction_vector(self.at_cds[end_idx], self.at_cds[bond.begin().index()]);
let b2 = direction_vector(
self.at_cds[adj_bond.begin().index()],
self.at_cds[adj_bond.end().index()],
);
if (1.0 - b1.dot(b2)).abs() < 0.001 {
continue;
}
let Some(wedge) = self.wedges.iter().find(|w| w.bond_idx == bond.id().index()) else {
continue;
};
let end_cds = self.at_cds[end_idx];
let mut closest_line_idx = None;
let mut closest_dist = 1.0;
for (i, shape) in self.bonds.iter().enumerate() {
if shape.bond_idx != adj_bond.id().index() || !shape.dash_pattern.is_empty() {
continue;
}
let d0 = (shape.begin - end_cds).length_squared();
if d0 < closest_dist {
closest_dist = d0;
closest_line_idx = Some((i, 0usize));
}
let d1 = (shape.end - end_cds).length_squared();
if d1 < closest_dist {
closest_dist = d1;
closest_line_idx = Some((i, 1usize));
}
}
let Some((line_idx, line_end_idx)) = closest_line_idx else {
continue;
};
let (mut p1, mut p2) = match wedge.points.len() {
3 => (1usize, 2usize),
9 => (4usize, 5usize),
_ => continue,
};
if (self.at_cds[third_atom] - wedge.points[p1]).length_squared()
< (self.at_cds[third_atom] - wedge.points[p2]).length_squared()
{
std::mem::swap(&mut p1, &mut p2);
}
let bond_line = &mut self.bonds[line_idx];
if line_end_idx == 0 {
bond_line.begin = wedge.points[p1];
} else {
bond_line.end = wedge.points[p1];
}
}
}
}
fn line_endpoint_for_atom(line: &DrawLine, atom_idx: usize) -> Option<usize> {
if line.atom1_idx == atom_idx {
Some(0)
} else if line.atom2_idx == atom_idx {
Some(1)
} else {
None
}
}
fn line_point(line: &DrawLine, point_idx: usize) -> DVec2 {
match point_idx {
0 => line.begin,
_ => line.end,
}
}
fn is_linear_atom(mol: &Molecule, at_cds: &[DVec2], atom_idx: usize) -> bool {
if atom_degree(mol, atom_idx) != 2 {
return false;
}
let nbrs = atom_neighbors(mol, atom_idx);
if nbrs.len() != 2 {
return false;
}
let at1_cds = at_cds[atom_idx];
let mut bond_vecs = [DVec2::ZERO, DVec2::ZERO];
let mut bond_orders = [BondOrder::Unspecified, BondOrder::Unspecified];
for (i, &nbr) in nbrs.iter().enumerate() {
let mut bond_vec = at_cds[nbr] - at1_cds;
let len = bond_vec.length();
if len <= 1e-8 {
return false;
}
bond_vec /= len;
bond_vecs[i] = bond_vec;
bond_orders[i] = mol
.bonds()
.iter()
.find(|bond| {
let begin = bond.begin().index();
let end = bond.end().index();
(begin == atom_idx && end == nbr) || (begin == nbr && end == atom_idx)
})
.expect("adjacent atoms must be connected")
.order();
}
bond_orders[0] == bond_orders[1] && bond_vecs[0].dot(bond_vecs[1]) < -0.95
}
fn radical_rect_at(trans: DVec2, width: f64, height: f64) -> StringRect {
StringRect {
ch: '\0',
draw_mode: TextDrawType::Normal,
trans,
offset: DVec2::ZERO,
g_centre: DVec2::ZERO,
y_shift: 0.0,
width,
height,
rect_corr: 0.0,
}
}
fn radical_rect_for_orientation(
orient: OrientType,
at_cds: DVec2,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
spot_rad: f64,
rad_size: f64,
) -> StringRect {
let (tx, ty, width, height) = match orient {
OrientType::N => (
at_cds.x,
y_max + 0.5 * (spot_rad * 3.0),
rad_size,
spot_rad * 3.0,
),
OrientType::S => (
at_cds.x,
y_min - 0.5 * (spot_rad * 3.0),
rad_size,
spot_rad * 3.0,
),
OrientType::E => (x_max + 3.0 * spot_rad, at_cds.y, spot_rad * 1.5, rad_size),
OrientType::W => (x_min - 3.0 * spot_rad, at_cds.y, spot_rad * 1.5, rad_size),
OrientType::C => (
at_cds.x,
y_max + 0.5 * (spot_rad * 3.0),
rad_size,
spot_rad * 3.0,
),
};
radical_rect_at(DVec2::new(tx, ty), width, height)
}
fn element_symbol(atomic_num: u8) -> &'static str {
match atomic_num {
0 => "*",
1 => "H",
2 => "He",
3 => "Li",
4 => "Be",
5 => "B",
6 => "C",
7 => "N",
8 => "O",
9 => "F",
10 => "Ne",
11 => "Na",
12 => "Mg",
13 => "Al",
14 => "Si",
15 => "P",
16 => "S",
17 => "Cl",
18 => "Ar",
19 => "K",
20 => "Ca",
21 => "Sc",
22 => "Ti",
23 => "V",
24 => "Cr",
25 => "Mn",
26 => "Fe",
27 => "Co",
28 => "Ni",
29 => "Cu",
30 => "Zn",
31 => "Ga",
32 => "Ge",
33 => "As",
34 => "Se",
35 => "Br",
36 => "Kr",
37 => "Rb",
38 => "Sr",
39 => "Y",
40 => "Zr",
41 => "Nb",
42 => "Mo",
43 => "Tc",
44 => "Ru",
45 => "Rh",
46 => "Pd",
47 => "Ag",
48 => "Cd",
49 => "In",
50 => "Sn",
51 => "Sb",
52 => "Te",
53 => "I",
54 => "Xe",
55 => "Cs",
56 => "Ba",
57..=71 => "La",
72 => "Hf",
73 => "Ta",
74 => "W",
75 => "Re",
76 => "Os",
77 => "Ir",
78 => "Pt",
79 => "Au",
80 => "Hg",
81 => "Tl",
82 => "Pb",
83 => "Bi",
84 => "Po",
85 => "At",
86 => "Rn",
87 => "Fr",
88 => "Ra",
89..=103 => "Ac",
104 => "Rf",
105 => "Db",
106 => "Sg",
107 => "Bh",
108 => "Hs",
109 => "Mt",
110 => "Ds",
111 => "Rg",
112 => "Cn",
113 => "Nh",
114 => "Fl",
115 => "Mc",
116 => "Lv",
117 => "Ts",
118 => "Og",
_ => "?",
}
}
fn init_drawing(out: &mut String, width: u32, height: u32) {
out.push_str("<?xml version='1.0' encoding='iso-8859-1'?>\n");
out.push_str(concat!(
"<svg version='1.1' baseProfile='full'\n",
" xmlns='http://www.w3.org/2000/svg'\n",
" xmlns:rdkit='http://www.rdkit.org/xml'\n",
" xmlns:xlink='http://www.w3.org/1999/xlink'\n",
" xml:space='preserve'\n",
));
out.push_str(&format!(
"width='{}px' height='{}px' viewBox='0 0 {} {}'>\n",
width, height, width, height
));
out.push_str("<!-- END OF HEADER -->\n");
}
fn clear_drawing(out: &mut String, width: u32, height: u32, colour: DrawColour) {
let col = draw_colour_to_svg(colour);
out.push_str(&format!(
"<rect style='opacity:1.0;fill:{};stroke:none' width='{}.0' height='{}.0' x='0.0' y='0.0'> </rect>\n",
col, width, height
));
}
#[allow(unused_variables)]
fn init_text_drawer(out: &mut String) {
}
#[allow(unused_variables)]
fn add_molecule_metadata(out: &mut String, mol: &Molecule, width: u32, height: u32) {
let _ = (out, mol, width, height);
}
#[allow(unused_variables)]
fn tag_atoms(out: &mut String, atom_labels: &[Option<AtomLabel>]) {
}
#[allow(unused_variables)]
fn output_classes(out: &mut String, class_name: &str) {
}
fn draw_line_svg(out: &mut String, line: &DrawLine, scale: f64) {
let col = draw_colour_to_svg(line.colour);
let width = if line.scale_width {
line.width * scale
} else {
line.width
};
let dash_str = if line.dash_pattern.is_empty() {
String::new()
} else {
let parts: Vec<String> = line
.dash_pattern
.iter()
.map(|d| format!("{}", *d as i32))
.collect();
format!(";stroke-dasharray:{}", parts.join(","))
};
out.push_str("<path ");
out.push_str("class='");
out.push_str(&format!("bond-{}", line.bond_idx));
if line.atom1_idx != usize::MAX {
out.push_str(&format!(" atom-{}", line.atom1_idx));
}
if line.atom2_idx != usize::MAX && line.atom2_idx != line.atom1_idx {
out.push_str(&format!(" atom-{}", line.atom2_idx));
}
out.push_str("' ");
out.push_str(&format!(
"d='M {},{} L {},{}' ",
format_double(line.begin.x),
format_double(line.begin.y),
format_double(line.end.x),
format_double(line.end.y),
));
out.push_str(&format!(
"style='fill:none;fill-rule:evenodd;stroke:{};stroke-width:{}px;\
stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1{}' />\n",
col,
format_double(width),
dash_str
));
}
fn draw_wavy_line_svg(out: &mut String, begin: DVec2, end: DVec2, col: DrawColour, width: f64) {
let segments = get_wavy_line_segments(begin, end, 6, 0.15);
let col_str = draw_colour_to_svg(col);
if segments.is_empty() {
return;
}
let (first, _, _, _) = segments[0];
out.push_str(&format!(
"<path d='M {} {}",
format_double(first.x),
format_double(first.y)
));
for &(_, cpt1, cpt2, segpt) in &segments {
out.push_str(&format!(
" C {} {}, {} {}, {} {}",
format_double(cpt1.x),
format_double(cpt1.y),
format_double(cpt2.x),
format_double(cpt2.y),
format_double(segpt.x),
format_double(segpt.y),
));
}
out.push_str(&format!(
"' style='fill:none;stroke:{};stroke-width:{}px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1' />\n",
col_str, format_double(width),
));
}
fn draw_ellipse_svg(
out: &mut String,
centre: DVec2,
rx: f64,
ry: f64,
col: DrawColour,
width: f64,
) {
let col_str = draw_colour_to_svg(col);
out.push_str(&format!(
"<ellipse cx='{}' cy='{}' rx='{}' ry='{}' ",
format_double(centre.x),
format_double(centre.y),
format_double(rx),
format_double(ry),
));
out.push_str(&format!(
"style='fill:none;stroke:{};stroke-width:{}px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1' />\n",
col_str, format_double(width),
));
}
fn draw_wedge_svg(out: &mut String, wedge: &DrawWedge) {
match wedge.kind {
WedgeKind::Solid => {
draw_solid_wedge_polygon(out, wedge);
}
WedgeKind::Dashed => {
let col = draw_colour_to_svg(wedge.col1);
draw_dashed_wedge(out, wedge, col);
}
}
}
fn draw_solid_wedge_path(
out: &mut String,
wedge: &DrawWedge,
points: &[DVec2],
fill: DrawColour,
stroke: DrawColour,
) {
let fill_col = draw_colour_to_svg(fill);
let stroke_col = draw_colour_to_svg(stroke);
out.push_str("<path ");
out.push_str(&format!(
"class='bond-{} atom-{} atom-{}' ",
wedge.bond_idx, wedge.atom1_idx, wedge.atom2_idx
));
out.push_str(&format!(
"d='M {},{}",
format_double(points[0].x),
format_double(points[0].y),
));
for pt in &points[1..] {
out.push_str(&format!(
" L {},{}",
format_double(pt.x),
format_double(pt.y)
));
}
out.push_str(" Z' ");
out.push_str(&format!(
"style='fill:{};fill-rule:evenodd;fill-opacity:{};stroke:{};stroke-width:{}px;\
stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:{};' />\n",
fill_col,
fill.a,
stroke_col,
format_double(wedge.width / 2.0),
stroke.a
));
}
fn draw_solid_wedge_polygon(out: &mut String, wedge: &DrawWedge) {
if wedge.points.len() < 3 {
return;
}
if wedge.points.len() == 3 || wedge.points.len() == 9 {
draw_solid_wedge_path(out, wedge, &wedge.points[0..3], wedge.col1, wedge.col1);
}
if wedge.points.len() == 6 {
let quad = [
wedge.points[0],
wedge.points[1],
wedge.points[2],
wedge.points[5],
];
draw_solid_wedge_path(out, wedge, &quad, wedge.col1, wedge.col1);
} else if wedge.points.len() == 9 {
let quad = [
wedge.points[4],
wedge.points[5],
wedge.points[6],
wedge.points[7],
];
draw_solid_wedge_path(out, wedge, &quad, wedge.col2, wedge.col2);
}
}
fn draw_dashed_wedge(out: &mut String, wedge: &DrawWedge, col: String) {
let _ = col;
if wedge.points.len() != 3 {
return;
}
let at1_cds = wedge.points[0];
let end1_cds = wedge.points[1];
let end2_cds = wedge.points[2];
let midend = (end1_cds + end2_cds) * 0.5;
let mut e1 = direction_vector(at1_cds, end1_cds);
let mut e2 = direction_vector(at1_cds, end2_cds);
let mut dash_sep = 2.5 + wedge.width;
let central_len = (at1_cds - midend).length();
let mut n_dashes = (central_len / dash_sep).round() as usize;
let num_dashes_needed = if wedge.one_less_dash { 4 } else { 3 };
if n_dashes < num_dashes_needed {
n_dashes = num_dashes_needed;
}
if n_dashes == 0 {
let col = draw_colour_to_svg(wedge.col1);
out.push_str(&format!(
"<path class='bond-{} atom-{} atom-{}' d='M {},{} L {},{}' \
style='fill:none;fill-rule:evenodd;stroke:{};stroke-width:{}px;stroke-linecap:butt;\
stroke-linejoin:miter;stroke-opacity:{}' />\n",
wedge.bond_idx,
wedge.atom1_idx,
wedge.atom2_idx,
format_double(end1_cds.x),
format_double(end1_cds.y),
format_double(end2_cds.x),
format_double(end2_cds.y),
col,
format_double(wedge.width),
wedge.col1.a
));
return;
}
dash_sep = central_len / n_dashes as f64;
if wedge.one_less_dash {
let endlenb2 = (end1_cds - end2_cds).length() / 2.0;
let central_line = direction_vector(at1_cds, midend);
let central_perp = DVec2::new(-central_line.y, central_line.x);
let new_end1 = at1_cds + central_line * (central_len - dash_sep) + central_perp * endlenb2;
let new_end2 = at1_cds + central_line * (central_len - dash_sep) - central_perp * endlenb2;
e1 = direction_vector(at1_cds, new_end1);
e2 = direction_vector(at1_cds, new_end2);
}
dash_sep *= (end1_cds - at1_cds).length() / central_len;
let extra = if wedge.one_less_dash { 0 } else { 1 };
let col1 = draw_colour_to_svg(wedge.col1);
let col2 = draw_colour_to_svg(wedge.col2);
for i in 1..(n_dashes + extra) {
let e11 = at1_cds + e1 * i as f64 * dash_sep;
let e22 = at1_cds + e2 * i as f64 * dash_sep;
let stroke = if i > n_dashes / 2 { &col2 } else { &col1 };
out.push_str(&format!(
"<path class='bond-{} atom-{} atom-{}' d='M {},{} L {},{}' \
style='fill:none;fill-rule:evenodd;stroke:{};stroke-width:{}px;stroke-linecap:butt;\
stroke-linejoin:miter;stroke-opacity:{}' />\n",
wedge.bond_idx,
wedge.atom1_idx,
wedge.atom2_idx,
format_double(e11.x),
format_double(e11.y),
format_double(e22.x),
format_double(e22.y),
stroke,
format_double(wedge.width),
wedge.col1.a
));
}
}
fn draw_arrow_svg(out: &mut String, arrow: &DrawArrow, scale: f64) {
let col = draw_colour_to_svg(arrow.colour);
let width = if arrow.scale_width {
arrow.width * scale
} else {
arrow.width
};
let (arrow_end, arrow1, arrow2) =
calc_arrow_head(arrow.end, arrow.begin, arrow.frac, width, arrow.angle);
out.push_str("<path ");
out.push_str("class='");
out.push_str(&format!("bond-{}", arrow.bond_idx));
if arrow.atom1_idx != usize::MAX {
out.push_str(&format!(" atom-{}", arrow.atom1_idx));
}
if arrow.atom2_idx != usize::MAX && arrow.atom2_idx != arrow.atom1_idx {
out.push_str(&format!(" atom-{}", arrow.atom2_idx));
}
out.push_str("' ");
out.push_str(&format!(
"d='M {},{} L {},{}' ",
format_double(arrow.begin.x),
format_double(arrow.begin.y),
format_double(arrow_end.x),
format_double(arrow_end.y),
));
out.push_str(&format!(
"style='fill:none;fill-rule:evenodd;stroke:{};stroke-width:{}px;\
stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:{}' />\n",
col,
format_double(width),
arrow.colour.a
));
out.push_str("<path ");
out.push_str("class='");
out.push_str(&format!("bond-{}", arrow.bond_idx));
if arrow.atom1_idx != usize::MAX {
out.push_str(&format!(" atom-{}", arrow.atom1_idx));
}
if arrow.atom2_idx != usize::MAX && arrow.atom2_idx != arrow.atom1_idx {
out.push_str(&format!(" atom-{}", arrow.atom2_idx));
}
out.push_str("' ");
out.push_str(&format!(
"d='M {},{} L {},{} L {},{} Z' ",
format_double(arrow1.x),
format_double(arrow1.y),
format_double(arrow_end.x),
format_double(arrow_end.y),
format_double(arrow2.x),
format_double(arrow2.y),
));
out.push_str(&format!(
"style='fill:{};fill-rule:evenodd;fill-opacity:{};stroke:{};stroke-width:{}px;\
stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:{};' />\n",
col,
arrow.colour.a,
col,
format_double(width),
arrow.colour.a
));
}
fn calc_arrow_head(
mut arrow_end: DVec2,
arrow_begin: DVec2,
mut frac: f64,
line_width: f64,
angle: f64,
) -> (DVec2, DVec2, DVec2) {
if angle < 1.0e-6 {
return (arrow_end, arrow_end, arrow_end);
}
let adjuster = 0.5 * line_width / angle.sin();
let len = (arrow_begin - arrow_end).length();
if len > 1.0e-6 {
let adj_len = len - adjuster;
arrow_end.x = arrow_begin.x + (arrow_end.x - arrow_begin.x) * adj_len / len;
arrow_end.y = arrow_begin.y + (arrow_end.y - arrow_begin.y) * adj_len / len;
}
let delta = arrow_begin - arrow_end;
let cos_angle = angle.cos();
let sin_angle = angle.sin();
frac /= cos_angle;
let arrow1 = DVec2::new(
arrow_end.x + frac * (delta.x * cos_angle + delta.y * sin_angle),
arrow_end.y + frac * (delta.y * cos_angle - delta.x * sin_angle),
);
let arrow2 = DVec2::new(
arrow_end.x + frac * (delta.x * cos_angle - delta.y * sin_angle),
arrow_end.y + frac * (delta.y * cos_angle + delta.x * sin_angle),
);
(arrow_end, arrow1, arrow2)
}
fn draw_polyline_svg(out: &mut String, polyline: &DrawPolyline, scale: f64) {
if polyline.points.is_empty() {
return;
}
let col = draw_colour_to_svg(polyline.colour);
let width = if polyline.scale_width {
polyline.width * scale
} else {
polyline.width
};
out.push_str("<path ");
if let Some(bond_idx) = polyline.bond_idx {
out.push_str(&format!("class='bond-{}' ", bond_idx));
} else if let Some(atom_idx) = polyline.atom1_idx {
out.push_str(&format!("class='atom-{}' ", atom_idx));
}
out.push_str(&format!(
"d='M {},{}",
format_double(polyline.points[0].x),
format_double(polyline.points[0].y),
));
for pt in &polyline.points[1..] {
out.push_str(&format!(
" L {},{}",
format_double(pt.x),
format_double(pt.y)
));
}
if polyline.fill_polys {
out.push_str(&format!(
" Z' style='fill:{};fill-rule:evenodd;fill-opacity:{};",
col, polyline.colour.a
));
} else {
out.push_str("' style='fill:none;");
}
out.push_str(&format!(
"stroke:{};stroke-width:{}px;stroke-linecap:butt;stroke-linejoin:miter;\
stroke-miterlimit:10;stroke-opacity:{};' />\n",
col,
format_double(width),
polyline.colour.a,
));
}
fn draw_annotation_svg(out: &mut String, annot: &DrawAnnotation, base_font_size: f64) {
let col = draw_colour_to_svg(annot.colour);
let font_size = format_svg_font_size_px(base_font_size * annot.font_scale);
out.push_str(&format!("<!-- annotation class={} -->\n", annot.class_));
for rect in &annot.rects {
let x = format_double(rect.trans.x + annot.pos.x);
let y = format_double(rect.trans.y + annot.pos.y);
let ch_str = xml_escape(&rect.ch.to_string());
let dy = match rect.draw_mode {
TextDrawType::Superscript => "-0.35em",
TextDrawType::Subscript => "0.35em",
TextDrawType::Normal => "0",
};
let font_size_attr = match rect.draw_mode {
TextDrawType::Normal => format!("font-size='{}px'", font_size),
TextDrawType::Superscript | TextDrawType::Subscript => {
let sz = font_size.parse::<f64>().unwrap_or(0.0) * 0.7;
format!("font-size='{}px'", format_svg_font_size_px(sz))
}
};
out.push_str(&format!(
"<text x='{}' y='{}' text-anchor='middle' dominant-baseline='central' \
fill='{}' font-family='{}' {} dy='{}'>{}</text>\n",
x, y, col, EMBEDDED_DRAW_FONT_FAMILY, font_size_attr, dy, ch_str,
));
}
}
fn draw_atom_label_svg(out: &mut String, label: &AtomLabel, base_font_size: f64) {
let col = draw_colour_to_svg(label.colour);
for rect in &label.rects {
let x = format_double(label.cds.x + rect.trans.x - rect.offset.x);
let y = format_double(
label.cds.y - rect.trans.y + rect.offset.y - rect.rect_corr - rect.y_shift,
);
let ch_str = xml_escape(&rect.ch.to_string());
let font_size =
format_svg_font_size_px(base_font_size * select_scale_factor(rect.ch, rect.draw_mode));
out.push_str(&format!(
"<text x='{}' y='{}' class='atom-{}' style='font-size:{}px;font-style:normal;\
font-weight:normal;fill-opacity:1;stroke:none;font-family:sans-serif;\
text-anchor:start;fill:{}' >{}</text>\n",
x, y, label.atom_idx, font_size, col, ch_str,
));
}
}
fn draw_radical_svg(out: &mut String, radicals: &[DrawRadical], spot_rad: f64) {
for rad in radicals {
let cx = rad.rect.trans.x;
let cy = rad.rect.trans.y;
let width = match rad.orient {
OrientType::N | OrientType::S | OrientType::C => rad.rect.width,
OrientType::E | OrientType::W => rad.rect.height,
};
let dir = match rad.orient {
OrientType::N | OrientType::S | OrientType::C => 0,
OrientType::E | OrientType::W => 1,
};
match rad.count {
3 => {
let mut spot = DVec2::new(cx, cy);
if dir == 1 {
spot.y = cy - 0.6 * width + spot_rad;
} else {
spot.x = cx - 0.6 * width + spot_rad;
}
draw_radical_spot_svg(out, spot, spot_rad, rad.atom_idx);
let mut spot = DVec2::new(cx, cy);
if dir == 1 {
spot.y = cy + 0.6 * width - spot_rad;
} else {
spot.x = cx + 0.6 * width - spot_rad;
}
draw_radical_spot_svg(out, spot, spot_rad, rad.atom_idx);
draw_radical_spot_svg(out, DVec2::new(cx, cy), spot_rad, rad.atom_idx);
}
1 => {
draw_radical_spot_svg(out, DVec2::new(cx, cy), spot_rad, rad.atom_idx);
}
4 => {
let mut spot = DVec2::new(cx, cy);
if dir == 1 {
spot.y = cy + 6.0 * spot_rad;
} else {
spot.x = cx + 6.0 * spot_rad;
}
draw_radical_spot_svg(out, spot, spot_rad, rad.atom_idx);
let mut spot = DVec2::new(cx, cy);
if dir == 1 {
spot.y = cy - 6.0 * spot_rad;
} else {
spot.x = cx - 6.0 * spot_rad;
}
draw_radical_spot_svg(out, spot, spot_rad, rad.atom_idx);
let mut spot = DVec2::new(cx, cy);
if dir == 1 {
spot.y = cy + 2.0 * spot_rad;
} else {
spot.x = cx + 2.0 * spot_rad;
}
draw_radical_spot_svg(out, spot, spot_rad, rad.atom_idx);
let mut spot = DVec2::new(cx, cy);
if dir == 1 {
spot.y = cy - 2.0 * spot_rad;
} else {
spot.x = cx - 2.0 * spot_rad;
}
draw_radical_spot_svg(out, spot, spot_rad, rad.atom_idx);
}
2 => {
let mut spot = DVec2::new(cx, cy);
if dir == 1 {
spot.y = cy + 2.0 * spot_rad;
} else {
spot.x = cx + 2.0 * spot_rad;
}
draw_radical_spot_svg(out, spot, spot_rad, rad.atom_idx);
let mut spot = DVec2::new(cx, cy);
if dir == 1 {
spot.y = cy - 2.0 * spot_rad;
} else {
spot.x = cx - 2.0 * spot_rad;
}
draw_radical_spot_svg(out, spot, spot_rad, rad.atom_idx);
}
_ => {}
}
}
}
fn draw_radical_spot_svg(out: &mut String, centre: DVec2, radius: f64, atom_idx: usize) {
let mut pts: Vec<DVec2> = Vec::new();
let num_steps = 1 + ((360.0f64 - 0.0) / 5.0) as i32;
let ang_incr = ((360.0f64 - 0.0) / num_steps as f64) * std::f64::consts::PI / 180.0;
let start_ang = 0.0f64;
for i in 0..=num_steps {
let ang = start_ang + i as f64 * ang_incr;
let x = centre.x + radius * ang.cos();
let y = centre.y + radius * ang.sin();
pts.push(DVec2::new(x, y));
}
pts.push(centre);
let poly = DrawPolyline {
points: pts,
colour: DrawColour::new(0.0, 0.0, 0.0),
width: 0.0,
scale_width: false,
fill_polys: true,
atom1_idx: Some(atom_idx),
atom2_idx: None,
bond_idx: None,
};
draw_polyline_svg(out, &poly, 1.0);
}
fn svg_to_png(svg: &str) -> Result<Vec<u8>, SvgDrawError> {
let mut opt = usvg::Options::default();
opt.font_family = EMBEDDED_DRAW_FONT_FAMILY.to_owned();
let fontdb = opt.fontdb_mut();
fontdb.load_font_source(usvg::fontdb::Source::Binary(embedded_draw_font_data()));
fontdb.set_sans_serif_family(EMBEDDED_DRAW_FONT_FAMILY);
let tree =
usvg::Tree::from_str(svg, &opt).map_err(|err| SvgDrawError::SvgParse(err.to_string()))?;
let size = tree.size().to_int_size();
let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).ok_or(
SvgDrawError::PixmapAllocation {
width: size.width(),
height: size.height(),
},
)?;
resvg::render(&tree, tiny_skia::Transform::default(), &mut pixmap.as_mut());
pixmap
.encode_png()
.map_err(|err| SvgDrawError::PngEncode(err.to_string()))
}
pub fn mol_to_svg(molecule: &Molecule, width: u32, height: u32) -> Result<String, SvgDrawError> {
let draw_mol = DrawMol::from_molecule(molecule, width, height, DrawOptions::default())?;
let mut out = String::new();
init_drawing(&mut out, width, height);
let bg = draw_mol.options.background_colour;
if draw_mol.options.clear_background {
clear_drawing(&mut out, width, height, bg);
}
let scale = draw_mol.scale;
let base_font_size = draw_mol.font_size * draw_mol.font_scale;
for shape_ref in &draw_mol.bond_draw_order {
match *shape_ref {
DrawBondShapeRef::Line(idx) => {
if let Some(line) = draw_mol.bonds.get(idx) {
draw_line_svg(&mut out, line, scale);
}
}
DrawBondShapeRef::Wedge(idx) => {
if let Some(wedge) = draw_mol.wedges.get(idx) {
draw_wedge_svg(&mut out, wedge);
}
}
DrawBondShapeRef::Arrow(idx) => {
if let Some(arrow) = draw_mol.arrows.get(idx) {
draw_arrow_svg(&mut out, arrow, scale);
}
}
}
}
for join in &draw_mol.join_paths {
draw_polyline_svg(&mut out, join, scale);
}
for label in draw_mol.atom_labels.iter().flatten() {
draw_atom_label_svg(&mut out, label, base_font_size);
}
let spot_rad = 0.2 * draw_mol.options.multiple_bond_offset * draw_mol.font_scale;
draw_radical_svg(&mut out, &draw_mol.radicals, spot_rad);
for annot in &draw_mol.annotations {
draw_annotation_svg(&mut out, annot, base_font_size);
}
for shape in &draw_mol.post_shapes {
draw_polyline_svg(&mut out, shape, scale);
}
out.push_str("</svg>\n");
Ok(out)
}
pub fn mol_to_png(molecule: &Molecule, width: u32, height: u32) -> Result<Vec<u8>, SvgDrawError> {
let svg = mol_to_svg(molecule, width, height)?;
svg_to_png(&svg)
}
fn prepare_molecule_for_drawing(
molecule: &Molecule,
force_coords: bool,
) -> Result<Molecule, SvgDrawError> {
let kekulize = true;
let add_chiral_hs = true;
let wedge_bonds = true;
let wavy_bonds = false;
let mut prepared = molecule.clone();
if kekulize && prepared.num_bonds() > 0 {
prepared = prepared.with_kekulized_bonds(false).map_err(|e| {
SvgDrawError::Unsupported(format!(
"prepareMolForDrawing KekulizeIfPossible failed: {e}"
))
})?;
}
if add_chiral_hs {
let chiral_ats = prepared
.derived_cache()
.rings
.as_ref()
.filter(|ring_info| ring_info.is_initialized())
.map(|ring_info| {
prepared
.atoms()
.iter()
.filter(|atom| {
ring_info.num_atom_rings(atom.id()) > 1
&& matches!(
atom.chiral_tag(),
ChiralTag::TetrahedralCcw | ChiralTag::TetrahedralCw
)
})
.map(|atom| atom.id())
.collect::<Vec<AtomId>>()
})
.unwrap_or_default();
if !chiral_ats.is_empty() {
let add_coords = !force_coords
&& (!prepared.conformers_2d().is_empty() || !prepared.conformers_3d().is_empty());
prepared = prepared
.with_hydrogens_with_params(AddHsParams {
add_coords,
only_on_atoms: Some(chiral_ats),
..AddHsParams::default()
})
.map_err(|e| {
SvgDrawError::Unsupported(format!(
"prepareMolForDrawing addHs on chiral atoms failed: {e}"
))
})?;
}
}
if force_coords || (prepared.conformers_2d().is_empty() && prepared.conformers_3d().is_empty())
{
prepared = prepared
.with_2d_coordinates_with_params(crate::With2DCoordinatesParams {
canon_orient: true,
..crate::With2DCoordinatesParams::default()
})
.map_err(|e| {
SvgDrawError::CoordinateGeneration(format!(
"prepareMolForDrawing compute2DCoords failed: {e}"
))
})?;
}
if wedge_bonds {
crate::io::molblock::wedge_molecule_bonds_like_rdkit(&mut prepared, None).map_err(|e| {
SvgDrawError::Unsupported(format!("prepareMolForDrawing wedgeMolBonds failed: {e}"))
})?;
}
if wavy_bonds {
return Err(SvgDrawError::Unsupported(
"prepareMolForDrawing wavyBonds branch is not modeled".to_string(),
));
}
Ok(prepared)
}
fn prepare_molecule_for_svg_drawing(molecule: &Molecule) -> Result<Molecule, SvgDrawError> {
prepare_molecule_for_drawing(molecule, false)
}
pub(crate) fn prepare_mol_for_drawing_parity(
molecule: &Molecule,
) -> Result<PreparedDrawMolecule, SvgDrawError> {
let prepared = prepare_molecule_for_drawing(molecule, true)?;
let coords = prepared.coordinates_2d().ok_or_else(|| {
SvgDrawError::CoordinateGeneration(
"prepareMolForDrawing did not materialize a 2D conformer".to_string(),
)
})?;
let atoms = coords
.iter()
.enumerate()
.map(|(i, pt)| PreparedDrawAtom {
index: i,
atomic_number: prepared.atoms()[i].atomic_number(),
x: pt[0],
y: pt[1],
})
.collect();
let bonds = prepared
.bonds()
.iter()
.map(|bond| PreparedDrawBond {
index: bond.id().index(),
begin_atom: bond.begin().index(),
end_atom: bond.end().index(),
bond_order: bond.order(),
is_aromatic: bond.is_aromatic(),
direction: bond.direction(),
rdkit_direction_name: {
match bond.direction() {
BondDirection::None => "NONE",
BondDirection::BeginWedge => "BEGINWEDGE",
BondDirection::BeginDash => "BEGINDASH",
BondDirection::EndUpRight => "ENDUPRIGHT",
BondDirection::EndDownRight => "ENDDOWNRIGHT",
BondDirection::EitherDouble => "EITHERDOUBLE",
BondDirection::Unknown => "UNKNOWN",
}
}
.to_string(),
})
.collect();
Ok(PreparedDrawMolecule { atoms, bonds })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_element_symbol() {
assert_eq!(super::element_symbol(1), "H");
assert_eq!(super::element_symbol(6), "C");
assert_eq!(super::element_symbol(8), "O");
assert_eq!(super::element_symbol(0), "*");
assert_eq!(super::element_symbol(200), "?");
}
#[test]
fn test_draw_colour_to_svg() {
let black = DrawColour::new(0.0, 0.0, 0.0);
assert_eq!(draw_colour_to_svg(black), "#000000");
let white = DrawColour::new(1.0, 1.0, 1.0);
assert_eq!(draw_colour_to_svg(white), "#FFFFFF");
let red = DrawColour::new(1.0, 0.0, 0.0);
assert_eq!(draw_colour_to_svg(red), "#FF0000");
}
#[test]
fn test_parse_draw_chars() {
let (chars, modes) = parse_draw_chars("C");
assert_eq!(chars, vec!['C']);
assert_eq!(modes, vec![TextDrawType::Normal]);
let (chars, modes) = parse_draw_chars("<sup>3</sup>C");
assert_eq!(chars, vec!['3', 'C']);
assert_eq!(modes, vec![TextDrawType::Superscript, TextDrawType::Normal]);
}
#[test]
fn test_bond_string_rects() {
let rects = get_string_rects("C", OrientType::C, 0.6);
assert!(!rects.is_empty());
assert_eq!(rects[0].ch, 'C');
}
#[test]
fn test_format_double() {
assert_eq!(format_double(1.0), "1.0");
assert_eq!(format_double(1.5), "1.5");
}
#[test]
fn test_mol_to_svg_contains_expected_elements() {
use crate::atom::{AtomSpec, Element};
use crate::bond::BondSpec;
use crate::builder::MoleculeBuilder;
let mut builder = MoleculeBuilder::new();
let c = builder.add_atom(AtomSpec::new(Element::C));
let h1 = builder.add_atom(AtomSpec::new(Element::H));
let h2 = builder.add_atom(AtomSpec::new(Element::H));
let h3 = builder.add_atom(AtomSpec::new(Element::H));
let h4 = builder.add_atom(AtomSpec::new(Element::H));
builder
.add_bond(BondSpec::new(c, h1, BondOrder::Single))
.unwrap();
builder
.add_bond(BondSpec::new(c, h2, BondOrder::Single))
.unwrap();
builder
.add_bond(BondSpec::new(c, h3, BondOrder::Single))
.unwrap();
builder
.add_bond(BondSpec::new(c, h4, BondOrder::Single))
.unwrap();
let mol = builder.build().expect("build methane");
let svg = mol_to_svg(&mol, 300, 300).expect("svg rendering");
assert!(
svg.starts_with("<?xml"),
"SVG should start with XML declaration"
);
assert!(svg.contains("viewBox"), "SVG should have viewBox");
assert!(svg.contains("</svg>"), "SVG should have closing tag");
assert!(
svg.contains("<line") || svg.contains("<path"),
"SVG should contain bond geometry"
);
}
#[test]
fn svg_to_png_rasterizes_embedded_font_text() {
let svg = "<?xml version='1.0' encoding='iso-8859-1'?>\
<svg version='1.1' baseProfile='full' xmlns='http://www.w3.org/2000/svg' \
width='120px' height='80px' viewBox='0 0 120 80'>\
<rect width='120' height='80' fill='#FFFFFF'/>\
<text x='12' y='54' style='font-size:48px;font-style:normal;\
font-weight:normal;fill-opacity:1;stroke:none;font-family:sans-serif;\
text-anchor:start;fill:#000000'>O</text>\
</svg>";
let png = svg_to_png(&svg).expect("text png rasterization");
let pixmap = tiny_skia::Pixmap::decode_png(&png).expect("decode rendered png");
let text_pixels = pixmap
.pixels()
.iter()
.filter(|pixel| {
pixel.alpha() == 255
&& pixel.red() < 245
&& pixel.green() < 245
&& pixel.blue() < 245
})
.count();
assert!(
text_pixels > 0,
"embedded-font text should produce non-background pixels"
);
}
#[test]
fn draw_svg_generates_missing_2d_coords_via_registered_operation() {
use crate::atom::{AtomSpec, Element};
use crate::bond::BondSpec;
use crate::builder::MoleculeBuilder;
let mut builder = MoleculeBuilder::new();
let a0 = builder.add_atom(AtomSpec::new(Element::C));
let a1 = builder.add_atom(AtomSpec::new(Element::C));
builder
.add_bond(BondSpec::new(a0, a1, BondOrder::Single))
.unwrap();
let mol = builder.build().unwrap();
let svg = mol_to_svg(&mol, 300, 300).unwrap();
assert!(svg.contains("</svg>"));
assert!(svg.contains("<line") || svg.contains("<path"));
}
#[test]
fn debug_row142_string_rect_intersection_matches_current_runtime() {
let self_rect = StringRect {
ch: 'C',
draw_mode: TextDrawType::Normal,
trans: DVec2::new(5.11364365437029811, 2.85196889627227712),
offset: DVec2::ZERO,
g_centre: DVec2::new(0.01361495844875371, 0.05999999999999983),
y_shift: 0.0,
width: 0.27722991689750742,
height: 0.47999999999999954,
rect_corr: 0.0,
};
let other_rect = StringRect {
ch: 'C',
draw_mode: TextDrawType::Normal,
trans: DVec2::new(5.07639897095288184, 3.19438069942302949),
offset: DVec2::ZERO,
g_centre: DVec2::new(0.04836565096952878, 0.05999999999999983),
y_shift: 0.0,
width: 0.36000000000000032,
height: 0.47999999999999954,
rect_corr: 0.0,
};
let (mut ttl, mut ttr, mut tbr, mut tbl) = self_rect.calc_corners(0.0);
if ttl.y < tbl.y {
std::mem::swap(&mut ttl, &mut tbl);
std::mem::swap(&mut ttr, &mut tbr);
}
let (mut otl, mut otr, mut obr, mut obl) = other_rect.calc_corners(0.0);
if otl.y < obl.y {
std::mem::swap(&mut otl, &mut obl);
std::mem::swap(&mut otr, &mut obr);
}
let case1 = (otl.x >= ttl.x && otl.x <= ttr.x && otl.y >= tbl.y && otl.y <= ttl.y)
|| (otr.x >= ttl.x && otr.x <= ttr.x && otr.y >= tbl.y && otr.y <= ttl.y)
|| (obr.x >= ttl.x && obr.x <= ttr.x && obr.y >= tbl.y && obr.y <= ttl.y)
|| (obl.x >= ttl.x && obl.x <= ttr.x && obl.y >= tbl.y && obl.y <= ttl.y);
let case2 = (ttl.x >= otl.x && ttl.x <= otr.x && ttl.y >= obl.y && ttl.y <= otl.y)
|| (ttr.x >= otl.x && ttr.x <= otr.x && ttr.y >= obl.y && ttr.y <= otl.y)
|| (tbr.x >= otl.x && tbr.x <= otr.x && tbr.y >= obl.y && tbr.y <= otl.y)
|| (tbl.x >= otl.x && tbl.x <= otr.x && tbl.y >= obl.y && tbl.y <= otl.y);
eprintln!(
"COSMOL_ROW142_RECT_TEST ttl=({:.17},{:.17}) ttr=({:.17},{:.17}) tbr=({:.17},{:.17}) tbl=({:.17},{:.17}) otl=({:.17},{:.17}) otr=({:.17},{:.17}) obr=({:.17},{:.17}) obl=({:.17},{:.17}) case1={} case2={} res={}",
ttl.x,
ttl.y,
ttr.x,
ttr.y,
tbr.x,
tbr.y,
tbl.x,
tbl.y,
otl.x,
otl.y,
otr.x,
otr.y,
obr.x,
obr.y,
obl.x,
obl.y,
case1,
case2,
self_rect.does_it_intersect(&other_rect, 0.0)
);
assert!(self_rect.does_it_intersect(&other_rect, 0.0));
}
}