#![allow(clippy::module_name_repetitions)]
mod cxsmiles;
mod depict_spec;
mod ffi;
mod layout;
mod svg;
pub use cxsmiles::{cx_atom_labels, smiles_base};
pub use depict_spec::{depict, DepictSpec, MolNode, MolSpec, ShadeSpec};
pub use layout::{layout_with_rdkit, sanitize_dummy_molblock, source_to_molblock};
pub use svg::scene_to_svg;
pub use xpict_core::scene::{AtomIn, BondIn, MoleculeIn, Scene};
pub use xpict_core::{depict_molecule, SCALE};
use xpict_core::scene::AtomIn as CoreAtom;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("mol() requires a non-empty SMILES or molfile")]
EmptySource,
#[error("RDKit parse failed: {0}")]
Parse(String),
#[error("RDKit layout failed: {0}")]
Layout(String),
#[error("{0}")]
Message(String),
}
#[derive(Debug, Clone)]
pub struct Mol {
pub source: String,
pub frame_molblock: Option<String>,
}
impl Mol {
pub fn from_source(smiles_or_molfile: impl AsRef<str>) -> Result<Self, Error> {
let text = smiles_or_molfile.as_ref().trim();
if text.is_empty() {
return Err(Error::EmptySource);
}
Ok(Self {
source: text.to_string(),
frame_molblock: None,
})
}
pub fn render(&mut self, opts: MolRenderOptions) -> Result<Rendered, Error> {
crate::render(self, opts)
}
pub fn ensure_frame(&mut self) -> Result<&str, Error> {
if self.frame_molblock.is_none() {
let (_, mb) = layout_with_rdkit(&self.source, None, None)?;
self.frame_molblock = Some(mb);
}
Ok(self.frame_molblock.as_deref().unwrap_or(""))
}
}
#[derive(Debug, Clone)]
pub struct Rendered {
pub width: f64,
pub height: f64,
pub scene: Scene,
pub molecule: MoleculeIn,
pub source: String,
pub frame_molblock: String,
pub coords: Vec<SvgAtom>,
pub svg_coords: Vec<SvgAtom>,
pub bonds: Vec<SvgBond>,
}
impl Rendered {
pub fn to_svg(&self) -> String {
scene_to_svg(&self.scene)
}
pub fn frame(&self) -> &str {
&self.frame_molblock
}
pub fn render_aligned(
&self,
other: &mut Mol,
mut opts: MolRenderOptions,
) -> Result<Rendered, Error> {
opts.align_to = Some(self.frame_molblock.clone());
render(other, opts)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SvgAtom {
pub index: i32,
pub element: String,
pub x: f64,
pub y: f64,
pub label: Option<String>,
pub charge: i32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SvgBond {
pub index: i32,
pub begin: i32,
pub end: i32,
pub order: f64,
pub stereo: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct MolRenderOptions {
pub id: Option<String>,
pub color: Option<String>,
pub atom_shade: Option<Vec<f64>>,
pub bond_shade: Option<Vec<f64>>,
pub mark_atoms: Option<Vec<i32>>,
pub mark_bonds: Option<Vec<(i32, i32)>>,
pub star_labels: Option<Vec<Option<String>>>,
pub bold_labels: Option<bool>,
pub align_to: Option<String>,
}
fn is_star(a: &CoreAtom) -> bool {
if a.element.as_deref() == Some("*") {
return true;
}
a.z == Some(0)
}
fn apply_star_labels(mut molecule: MoleculeIn, labels: &[Option<String>]) -> MoleculeIn {
if labels.is_empty() {
return molecule;
}
let stars: Vec<usize> = molecule
.atoms
.iter()
.enumerate()
.filter(|(_, a)| is_star(a))
.map(|(i, _)| i)
.collect();
if stars.is_empty() {
return molecule;
}
for (k, raw) in labels.iter().enumerate() {
if k >= stars.len() {
break;
}
let label = match raw {
None => "*".to_string(),
Some(s) if s.trim().is_empty() => "*".to_string(),
Some(s) => s.trim().to_string(),
};
molecule.atoms[stars[k]].label = Some(label);
}
molecule
}
fn apply_cx_by_index(mut molecule: MoleculeIn, source: &str) -> MoleculeIn {
let aliases = cx_atom_labels(source);
if aliases.is_empty() {
return molecule;
}
for a in &mut molecule.atoms {
let idx = a.index as usize;
if let Some(Some(label)) = aliases.get(idx) {
let label = label.trim();
if !label.is_empty() {
a.label = Some(label.to_string());
}
}
}
molecule
}
fn apply_opts(mut molecule: MoleculeIn, opts: &MolRenderOptions, source: &str) -> MoleculeIn {
if let Some(ref id) = opts.id {
molecule.id = Some(id.clone());
}
if let Some(ref color) = opts.color {
molecule.color = Some(color.clone());
}
if let Some(ref shade) = opts.atom_shade {
molecule.atom_shade = Some(shade.clone());
}
if let Some(ref shade) = opts.bond_shade {
molecule.bond_shade = Some(shade.clone());
}
if let Some(ref marks) = opts.mark_atoms {
molecule.mark_atoms = marks.clone();
}
if let Some(ref marks) = opts.mark_bonds {
molecule.mark_bonds = marks.clone();
}
if let Some(bold) = opts.bold_labels {
molecule.bold_labels = bold;
}
if let Some(ref labels) = opts.star_labels {
molecule = apply_star_labels(molecule, labels);
} else {
molecule = apply_cx_by_index(molecule, source);
}
molecule
}
fn to_coord_list(atoms: &[CoreAtom]) -> Vec<SvgAtom> {
atoms
.iter()
.map(|a| SvgAtom {
index: a.index,
element: a.symbol().to_string(),
x: a.x,
y: a.y,
label: a.label.clone(),
charge: a.charge,
})
.collect()
}
pub fn mol(smiles_or_molfile: impl AsRef<str>) -> Result<Mol, Error> {
Mol::from_source(smiles_or_molfile)
}
pub fn render(input: &mut Mol, opts: MolRenderOptions) -> Result<Rendered, Error> {
let template = opts.align_to.as_deref().filter(|s| !s.is_empty());
let (laid, pose_mb) =
layout_with_rdkit(&input.source, template, opts.id.clone())?;
if input.frame_molblock.is_none() {
input.frame_molblock = Some(pose_mb.clone());
}
let molecule = apply_opts(laid, &opts, &input.source);
let scene = depict_molecule(&molecule);
let coords = to_coord_list(&molecule.atoms);
let bonds: Vec<SvgBond> = molecule
.bonds
.iter()
.map(|b| SvgBond {
index: b.index,
begin: b.begin,
end: b.end,
order: b.order,
stereo: b.stereo.clone(),
})
.collect();
Ok(Rendered {
width: scene.width,
height: scene.height,
scene,
molecule,
source: input.source.clone(),
frame_molblock: pose_mb,
svg_coords: coords.clone(),
coords,
bonds,
})
}
pub fn to_svg(scene: &Scene) -> String {
scene_to_svg(scene)
}