use crate::{MolHandle, WASM_MAX_ATOMS, WASM_MAX_INPUT_BYTES, escape_json_string};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn minimize_dreiding_json(mol: &MolHandle) -> String {
let coords = chematic_3d::generate_coords(&mol.inner);
let min_coords = chematic_3d::minimize_dreiding(&mol.inner, coords);
chematic_3d::write_pdb(&mol.inner, &min_coords)
}
#[wasm_bindgen]
pub fn whim_descriptors_json(mol: &MolHandle) -> String {
let coords = chematic_3d::generate_coords(&mol.inner);
let desc = chematic_3d::whim_descriptors(&mol.inner, &coords);
let parts: Vec<String> = desc.iter().map(|&v| v.to_string()).collect();
format!("[{}]", parts.join(","))
}
#[wasm_bindgen]
pub fn getaway_descriptors_json(mol: &MolHandle) -> String {
let coords = chematic_3d::generate_coords(&mol.inner);
let desc = chematic_3d::getaway_descriptors(&mol.inner, &coords);
let parts: Vec<String> = desc.iter().map(|&v| v.to_string()).collect();
format!("[{}]", parts.join(","))
}
#[wasm_bindgen]
pub fn whim_getaway_combined_json(mol: &MolHandle) -> String {
let coords = chematic_3d::generate_coords(&mol.inner);
let desc = chematic_3d::whim_getaway_combined(&mol.inner, &coords);
let parts: Vec<String> = desc.iter().map(|&v| v.to_string()).collect();
format!("[{}]", parts.join(","))
}
#[wasm_bindgen]
pub fn gasteiger_charges_json(mol: &MolHandle) -> String {
let q = chematic_chem::gasteiger_charges(&mol.inner);
let parts: Vec<String> = q.iter().map(|v| format!("{v:.4}")).collect();
format!("[{}]", parts.join(","))
}
#[wasm_bindgen]
pub fn mmff94_charges_json(mol: &MolHandle) -> String {
let q = chematic_chem::mmff94_charges(&mol.inner);
let parts: Vec<String> = q.iter().map(|v| format!("{v:.6}")).collect();
format!("[{}]", parts.join(","))
}
#[wasm_bindgen]
pub fn mmff94_charges_typed_json(mol: &MolHandle) -> String {
if mol.inner.atom_count() > WASM_MAX_ATOMS {
return format!(
r#"{{"error":"molecule too large (max {} atoms)"}}"#,
WASM_MAX_ATOMS
);
}
let q = chematic_chem::mmff94_charges_typed(&mol.inner);
let vals: Vec<String> = q.iter().map(|x| format!("{x:.6}")).collect();
format!(r#"{{"charges":[{}]}}"#, vals.join(","))
}
#[wasm_bindgen]
pub fn minimize_mmff94_json(mol: &MolHandle, max_iter: u32) -> String {
if mol.inner.atom_count() > WASM_MAX_ATOMS {
return format!(
r#"{{"error":"molecule too large (max {} atoms)"}}"#,
WASM_MAX_ATOMS
);
}
let conf = chematic_3d::generate_coords(&mol.inner);
let n = mol.inner.atom_count();
let mut coords: Vec<[f64; 3]> = (0..n)
.map(|i| {
let p = conf.get(chematic_core::AtomIdx(i as u32));
[p.x, p.y, p.z]
})
.collect();
match chematic_ff::minimize_mmff94_full(&mol.inner, &mut coords, max_iter as usize) {
Ok(r) => format!(
r#"{{"energy":{:.4},"rmsd":{:.4},"converged":{},"iterations":{}}}"#,
r.energy, r.rmsd, r.converged, r.iterations
),
Err(e) => format!(r#"{{"error":"{}"}}"#, e),
}
}
#[wasm_bindgen]
pub fn minimize_mmff94_lbfgs_json(mol: &MolHandle, max_iter: u32) -> String {
if mol.inner.atom_count() > WASM_MAX_ATOMS {
return format!(
r#"{{"error":"molecule too large (max {} atoms)"}}"#,
WASM_MAX_ATOMS
);
}
let conf = chematic_3d::generate_coords(&mol.inner);
let n = mol.inner.atom_count();
let mut coords: Vec<[f64; 3]> = (0..n)
.map(|i| {
let p = conf.get(chematic_core::AtomIdx(i as u32));
[p.x, p.y, p.z]
})
.collect();
match chematic_ff::minimize_mmff94_lbfgs(&mol.inner, &mut coords, max_iter as usize) {
Ok(r) => format!(
r#"{{"energy":{:.4},"rmsd":{:.4},"converged":{},"iterations":{}}}"#,
r.energy, r.rmsd, r.converged, r.iterations
),
Err(e) => format!(r#"{{"error":"{}"}}"#, e),
}
}
#[wasm_bindgen]
pub fn mmff94_energy_breakdown_json(mol: &MolHandle) -> String {
if mol.inner.atom_count() > WASM_MAX_ATOMS {
return format!(
r#"{{"error":"molecule too large (max {} atoms)"}}"#,
WASM_MAX_ATOMS
);
}
let conf = chematic_3d::generate_coords(&mol.inner);
let n = mol.inner.atom_count();
let coords: Vec<[f64; 3]> = (0..n)
.map(|i| {
let p = conf.get(chematic_core::AtomIdx(i as u32));
[p.x, p.y, p.z]
})
.collect();
match chematic_ff::mmff94_energy_breakdown(&mol.inner, &coords) {
Ok(bd) => format!(
r#"{{"bond":{:.4},"angle":{:.4},"torsion":{:.4},"vdw":{:.4},"elec":{:.4},"total":{:.4}}}"#,
bd.bond, bd.angle, bd.torsion, bd.vdw, bd.electrostatic, bd.total
),
Err(e) => format!(r#"{{"error":"{}"}}"#, e),
}
}
#[wasm_bindgen]
pub fn mmff94_partial_charges_json(mol: &MolHandle) -> String {
if mol.inner.atom_count() > WASM_MAX_ATOMS {
return format!(
r#"{{"error":"molecule too large (max {} atoms)"}}"#,
WASM_MAX_ATOMS
);
}
match chematic_ff::mmff94_charges_numeric(&mol.inner) {
Ok(q) => {
let vals: Vec<String> = q.iter().map(|x| format!("{x:.4}")).collect();
format!(r#"{{"charges":[{}]}}"#, vals.join(","))
}
Err(e) => format!(r#"{{"error":"{}"}}"#, e),
}
}
#[wasm_bindgen]
pub fn pharmacophore_fp_3d_summary(mol: &MolHandle) -> String {
let _coords = chematic_3d::generate_coords(&mol.inner);
let features = chematic_perception::detect_features(&mol.inner);
let mut counts = [0; 6];
for feat in features {
let idx = match feat.ftype {
chematic_perception::FeatureType::Donor => 0,
chematic_perception::FeatureType::Acceptor => 1,
chematic_perception::FeatureType::Aromatic => 2,
chematic_perception::FeatureType::Hydrophobic => 3,
chematic_perception::FeatureType::Positive => 4,
chematic_perception::FeatureType::Negative => 5,
};
counts[idx] += 1;
}
format!(
"{{\"Donor\":{},\"Acceptor\":{},\"Aromatic\":{},\"Hydrophobic\":{},\"Positive\":{},\"Negative\":{}}}",
counts[0], counts[1], counts[2], counts[3], counts[4], counts[5]
)
}
#[wasm_bindgen]
pub fn autocorr_3d_json(mol: &MolHandle) -> String {
let coords = chematic_3d::generate_coords(&mol.inner);
let ac = chematic_3d::autocorr_3d(&mol.inner, &coords);
format!(
"[{:.4},{:.4},{:.4},{:.4},{:.4},{:.4},{:.4},{:.4}]",
ac[0], ac[1], ac[2], ac[3], ac[4], ac[5], ac[6], ac[7]
)
}
#[wasm_bindgen]
pub fn generate_3d_coords_json(mol: &MolHandle) -> String {
let coords = chematic_3d::generate_and_minimize_dreiding(&mol.inner);
let parts: Vec<String> = coords
.points
.iter()
.map(|p| format!("[{:.4},{:.4},{:.4}]", p.x, p.y, p.z))
.collect();
format!("[{}]", parts.join(","))
}
#[wasm_bindgen]
pub fn generate_3d_etkdg_coords_json(mol: &MolHandle) -> String {
let coords = chematic_3d::generate_coords_etkdg(&mol.inner);
let parts: Vec<String> = coords
.points
.iter()
.map(|p| format!("[{:.4},{:.4},{:.4}]", p.x, p.y, p.z))
.collect();
format!("[{}]", parts.join(","))
}
#[wasm_bindgen]
pub fn conformer_ensemble_json(mol: &MolHandle, n: u32, rmsd_threshold: f64) -> String {
let smiles = chematic_smiles::canonical_smiles(&mol.inner);
let fresh = match chematic_smiles::parse(&smiles) {
Ok(m) => m,
Err(_) => return r#"{"conformers":[],"count":0}"#.to_string(),
};
let config = chematic_3d::ConformerConfig {
count: n as usize,
rmsd_threshold,
..chematic_3d::ConformerConfig::default()
};
match chematic_3d::generate_conformer_ensemble_with_config(fresh, &config) {
Ok(ensemble) => {
let conf_jsons: Vec<String> = (0..ensemble.conformer_count())
.filter_map(|i| ensemble.get_conformer(i))
.map(|c| {
let pts: Vec<String> = c
.points
.iter()
.map(|p| format!("[{:.4},{:.4},{:.4}]", p.x, p.y, p.z))
.collect();
format!("[{}]", pts.join(","))
})
.collect();
let count = conf_jsons.len();
format!(
r#"{{"conformers":[{}],"count":{count}}}"#,
conf_jsons.join(",")
)
}
Err(_) => r#"{"conformers":[],"count":0}"#.to_string(),
}
}
#[wasm_bindgen]
pub fn shape_descriptors_json(mol: &MolHandle) -> String {
let coords = chematic_3d::generate_coords(&mol.inner);
let m = &*mol.inner;
fn fmt(v: f64) -> String {
if v.is_finite() {
format!("{v:.4}")
} else {
"null".to_string()
}
}
format!(
"{{\"pmi1\":{},\"pmi2\":{},\"pmi3\":{},\"npr1\":{},\"npr2\":{},\
\"asphericity\":{},\"eccentricity\":{},\"radiusOfGyration\":{},\"planeOfBestFit\":{}}}",
fmt(chematic_3d::pmi1(m, &coords)),
fmt(chematic_3d::pmi2(m, &coords)),
fmt(chematic_3d::pmi3(m, &coords)),
fmt(chematic_3d::npr1(m, &coords)),
fmt(chematic_3d::npr2(m, &coords)),
fmt(chematic_3d::asphericity(m, &coords)),
fmt(chematic_3d::eccentricity(m, &coords)),
fmt(chematic_3d::radius_of_gyration(m, &coords)),
fmt(chematic_3d::plane_of_best_fit(m, &coords)),
)
}
#[wasm_bindgen]
pub struct ConformerHandle {
inner: chematic_3d::ConformerEnsemble,
}
#[wasm_bindgen]
impl ConformerHandle {
#[wasm_bindgen(constructor)]
pub fn new(smiles: &str) -> Result<ConformerHandle, JsValue> {
let mol = chematic_smiles::parse(smiles).map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(ConformerHandle {
inner: chematic_3d::ConformerEnsemble::new(mol),
})
}
pub fn conformer_count(&self) -> usize {
self.inner.conformer_count()
}
pub fn mol(&self) -> MolHandle {
let smi = chematic_smiles::canonical_smiles(self.inner.mol());
let mol = chematic_smiles::parse(&smi)
.unwrap_or_else(|_| chematic_core::MoleculeBuilder::new().build());
MolHandle {
inner: std::rc::Rc::new(mol),
}
}
pub fn add_generated_conformer(&mut self) -> usize {
let coords = chematic_3d::generate_coords(self.inner.mol());
self.inner.add_conformer(coords).unwrap_or(usize::MAX)
}
pub fn add_minimized_conformer(&mut self) -> usize {
let coords = chematic_3d::generate_coords(self.inner.mol());
let minimized = chematic_3d::minimize(self.inner.mol(), coords);
self.inner.add_conformer(minimized).unwrap_or(usize::MAX)
}
pub fn get_conformer_pdb(&self, idx: usize) -> Option<String> {
self.inner
.get_conformer(idx)
.map(|coords| chematic_3d::write_pdb(self.inner.mol(), coords))
}
pub fn conformer_rmsd(&self, a: usize, b: usize) -> f64 {
self.inner.conformer_rmsd(a, b).unwrap_or(f64::NAN)
}
pub fn conformer_rmsd_no_align(&self, a: usize, b: usize) -> f64 {
self.inner.conformer_rmsd_no_align(a, b).unwrap_or(f64::NAN)
}
pub fn remove_conformer(&mut self, idx: usize) -> bool {
self.inner.remove_conformer(idx).is_some()
}
pub fn cluster_conformers_json(&self, rms_threshold: f64) -> String {
let kept = self.inner.cluster_conformers_by_rms(rms_threshold);
let total = self.inner.conformer_count();
let removed = total.saturating_sub(kept.len());
let indices_str: Vec<String> = kept.iter().map(|i| i.to_string()).collect();
format!(
r#"{{"kept_indices":[{}],"removed_count":{}}}"#,
indices_str.join(","),
removed
)
}
}
#[wasm_bindgen]
pub fn depict_data_with_coords_json(mol: &MolHandle, coords_json: &str) -> String {
let coords: Vec<(f64, f64)> = {
let mut result = Vec::new();
let inner = coords_json.trim().trim_matches(|c| c == '[' || c == ']');
for pair in inner.split("],[") {
let clean = pair.trim().trim_matches(|c| c == '[' || c == ']');
let nums: Vec<f64> = clean
.split(',')
.filter_map(|s| s.trim().parse::<f64>().ok())
.collect();
if nums.len() >= 2 {
result.push((nums[0], nums[1]));
}
}
result
};
let data = chematic_depict::depict_data_with_coords(&mol.inner, &coords);
let atoms: Vec<String> = data
.atoms
.iter()
.map(|a| {
let label = match &a.label {
Some(s) => format!("\"{}\"", escape_json_string(s)),
None => "null".to_string(),
};
let charge_suffix = if a.charge != 0 {
format!(",\"charge\":{}", a.charge)
} else {
String::new()
};
let label_prefix = format!("{label},");
format!(
r#"{{"idx":{},"element":"{}","x":{:.4},"y":{:.4},"label":{}"color":"{}"{}}}"#,
a.idx.0,
a.element.symbol(),
a.pos.x,
a.pos.y,
label_prefix,
escape_json_string(&a.color),
charge_suffix,
)
})
.collect();
let bond_kind = |k: &chematic_depict::DepictBondKind| match k {
chematic_depict::DepictBondKind::Single => "Single",
chematic_depict::DepictBondKind::Double => "Double",
chematic_depict::DepictBondKind::Triple => "Triple",
chematic_depict::DepictBondKind::Aromatic => "Aromatic",
chematic_depict::DepictBondKind::Up => "Up",
chematic_depict::DepictBondKind::Down => "Down",
};
let bonds: Vec<String> = data
.bonds
.iter()
.map(|b| {
format!(
r#"{{"idx":{},"atom1":{},"atom2":{},"kind":"{}"}}"#,
b.idx.0,
b.atom1.0,
b.atom2.0,
bond_kind(&b.kind)
)
})
.collect();
format!(
r#"{{"atoms":[{}],"bonds":[{}]}}"#,
atoms.join(","),
bonds.join(",")
)
}
#[wasm_bindgen]
pub fn minimize_uff_json(smiles: &str, coords_json: &str, max_iter: u32) -> String {
let mol = match chematic_smiles::parse(smiles) {
Ok(m) => m,
Err(e) => return format!("{{\"error\":\"{e}\"}}"),
};
let coords_arr: Vec<[f64; 3]> = match serde_json::from_str(coords_json) {
Ok(v) => v,
Err(e) => return format!("{{\"error\":\"coords parse error: {e}\"}}"),
};
let types = chematic_ff::assign_uff_types(&mol);
let iters = if max_iter == 0 {
500
} else {
max_iter as usize
};
let result = chematic_ff::minimize_uff(&mol, &types, coords_arr, iters);
let coords_out: Vec<[f64; 3]> = result.coords;
let coords_str = coords_out
.iter()
.map(|c| format!("[{:.4},{:.4},{:.4}]", c[0], c[1], c[2]))
.collect::<Vec<_>>()
.join(",");
format!(
"{{\"coords\":[{coords_str}],\"energy\":{:.4},\"iterations\":{},\"converged\":{}}}",
result.energy, result.iterations, result.converged
)
}
#[wasm_bindgen]
pub fn get_bond_length_json(smiles: &str, a: u32, b: u32) -> f64 {
if smiles.len() > WASM_MAX_INPUT_BYTES {
return -1.0;
}
let mol = match chematic_smiles::parse(smiles) {
Ok(m) => m,
Err(_) => return -1.0,
};
let coords = chematic_3d::generate_coords(&mol);
let a_idx = chematic_core::AtomIdx(a);
let b_idx = chematic_core::AtomIdx(b);
if a >= mol.atom_count() as u32 || b >= mol.atom_count() as u32 {
return -1.0;
}
chematic_3d::get_bond_length(&coords, a_idx, b_idx)
}
#[wasm_bindgen]
pub fn get_dihedral_json(smiles: &str, a: u32, b: u32, c: u32, d: u32) -> JsValue {
if smiles.len() > WASM_MAX_INPUT_BYTES {
return JsValue::NULL;
}
let mol = match chematic_smiles::parse(smiles) {
Ok(m) => m,
Err(_) => return JsValue::NULL,
};
if a >= mol.atom_count() as u32
|| b >= mol.atom_count() as u32
|| c >= mol.atom_count() as u32
|| d >= mol.atom_count() as u32
{
return JsValue::NULL;
}
let coords = chematic_3d::generate_coords(&mol);
let a_idx = chematic_core::AtomIdx(a);
let b_idx = chematic_core::AtomIdx(b);
let c_idx = chematic_core::AtomIdx(c);
let d_idx = chematic_core::AtomIdx(d);
match chematic_3d::get_dihedral_deg(&coords, a_idx, b_idx, c_idx, d_idx) {
Some(angle) => JsValue::from_f64(angle),
None => JsValue::NULL,
}
}
#[wasm_bindgen]
pub fn set_dihedral_json(
smiles: &str,
a: u32,
b: u32,
c: u32,
d: u32,
angle_deg: f64,
) -> Result<String, String> {
if smiles.len() > WASM_MAX_INPUT_BYTES {
return Err("Input SMILES too long".to_string());
}
let mol = chematic_smiles::parse(smiles).map_err(|e| format!("Parse error: {}", e))?;
if a >= mol.atom_count() as u32
|| b >= mol.atom_count() as u32
|| c >= mol.atom_count() as u32
|| d >= mol.atom_count() as u32
{
return Err("Atom index out of range".to_string());
}
let coords = chematic_3d::generate_coords(&mol);
let a_idx = chematic_core::AtomIdx(a);
let b_idx = chematic_core::AtomIdx(b);
let c_idx = chematic_core::AtomIdx(c);
let d_idx = chematic_core::AtomIdx(d);
let angle_rad = angle_deg.to_radians();
let new_coords =
chematic_3d::set_dihedral(&coords, &mol, a_idx, b_idx, c_idx, d_idx, angle_rad);
Ok(chematic_3d::write_pdb(&mol, &new_coords))
}