1use crate::{MolHandle, WASM_MAX_ATOMS, WASM_MAX_INPUT_BYTES, escape_json_string};
4use wasm_bindgen::prelude::*;
5
6#[wasm_bindgen]
17pub fn minimize_dreiding_json(mol: &MolHandle) -> String {
18 let coords = chematic_3d::generate_coords(&mol.inner);
19 let min_coords = chematic_3d::minimize_dreiding(&mol.inner, coords);
20 chematic_3d::write_pdb(&mol.inner, &min_coords)
21}
22
23#[wasm_bindgen]
27pub fn whim_descriptors_json(mol: &MolHandle) -> String {
28 let coords = chematic_3d::generate_coords(&mol.inner);
29 let desc = chematic_3d::whim_descriptors(&mol.inner, &coords);
30 let parts: Vec<String> = desc.iter().map(|&v| v.to_string()).collect();
31 format!("[{}]", parts.join(","))
32}
33
34#[wasm_bindgen]
44pub fn getaway_descriptors_json(mol: &MolHandle) -> String {
45 let coords = chematic_3d::generate_coords(&mol.inner);
46 let desc = chematic_3d::getaway_descriptors(&mol.inner, &coords);
47 let parts: Vec<String> = desc.iter().map(|&v| v.to_string()).collect();
48 format!("[{}]", parts.join(","))
49}
50
51#[wasm_bindgen]
56pub fn whim_getaway_combined_json(mol: &MolHandle) -> String {
57 let coords = chematic_3d::generate_coords(&mol.inner);
58 let desc = chematic_3d::whim_getaway_combined(&mol.inner, &coords);
59 let parts: Vec<String> = desc.iter().map(|&v| v.to_string()).collect();
60 format!("[{}]", parts.join(","))
61}
62
63#[wasm_bindgen]
65pub fn gasteiger_charges_json(mol: &MolHandle) -> String {
66 let q = chematic_chem::gasteiger_charges(&mol.inner);
67 let parts: Vec<String> = q.iter().map(|v| format!("{v:.4}")).collect();
68 format!("[{}]", parts.join(","))
69}
70
71#[wasm_bindgen]
77pub fn mmff94_charges_json(mol: &MolHandle) -> String {
78 let q = chematic_chem::mmff94_charges(&mol.inner);
79 let parts: Vec<String> = q.iter().map(|v| format!("{v:.6}")).collect();
80 format!("[{}]", parts.join(","))
81}
82
83#[wasm_bindgen]
88pub fn mmff94_charges_typed_json(mol: &MolHandle) -> String {
89 if mol.inner.atom_count() > WASM_MAX_ATOMS {
90 return format!(
91 r#"{{"error":"molecule too large (max {} atoms)"}}"#,
92 WASM_MAX_ATOMS
93 );
94 }
95 let q = chematic_chem::mmff94_charges_typed(&mol.inner);
96 let vals: Vec<String> = q.iter().map(|x| format!("{x:.6}")).collect();
97 format!(r#"{{"charges":[{}]}}"#, vals.join(","))
98}
99
100#[wasm_bindgen]
106pub fn minimize_mmff94_json(mol: &MolHandle, max_iter: u32) -> String {
107 if mol.inner.atom_count() > WASM_MAX_ATOMS {
108 return format!(
109 r#"{{"error":"molecule too large (max {} atoms)"}}"#,
110 WASM_MAX_ATOMS
111 );
112 }
113 let conf = chematic_3d::generate_coords(&mol.inner);
114 let n = mol.inner.atom_count();
115 let mut coords: Vec<[f64; 3]> = (0..n)
116 .map(|i| {
117 let p = conf.get(chematic_core::AtomIdx(i as u32));
118 [p.x, p.y, p.z]
119 })
120 .collect();
121 match chematic_ff::minimize_mmff94_full(&mol.inner, &mut coords, max_iter as usize) {
122 Ok(r) => format!(
123 r#"{{"energy":{:.4},"rmsd":{:.4},"converged":{},"iterations":{}}}"#,
124 r.energy, r.rmsd, r.converged, r.iterations
125 ),
126 Err(e) => format!(r#"{{"error":"{}"}}"#, e),
127 }
128}
129
130#[wasm_bindgen]
133pub fn minimize_mmff94_lbfgs_json(mol: &MolHandle, max_iter: u32) -> String {
134 if mol.inner.atom_count() > WASM_MAX_ATOMS {
135 return format!(
136 r#"{{"error":"molecule too large (max {} atoms)"}}"#,
137 WASM_MAX_ATOMS
138 );
139 }
140 let conf = chematic_3d::generate_coords(&mol.inner);
141 let n = mol.inner.atom_count();
142 let mut coords: Vec<[f64; 3]> = (0..n)
143 .map(|i| {
144 let p = conf.get(chematic_core::AtomIdx(i as u32));
145 [p.x, p.y, p.z]
146 })
147 .collect();
148 match chematic_ff::minimize_mmff94_lbfgs(&mol.inner, &mut coords, max_iter as usize) {
149 Ok(r) => format!(
150 r#"{{"energy":{:.4},"rmsd":{:.4},"converged":{},"iterations":{}}}"#,
151 r.energy, r.rmsd, r.converged, r.iterations
152 ),
153 Err(e) => format!(r#"{{"error":"{}"}}"#, e),
154 }
155}
156
157#[wasm_bindgen]
160pub fn mmff94_energy_breakdown_json(mol: &MolHandle) -> String {
161 if mol.inner.atom_count() > WASM_MAX_ATOMS {
162 return format!(
163 r#"{{"error":"molecule too large (max {} atoms)"}}"#,
164 WASM_MAX_ATOMS
165 );
166 }
167 let conf = chematic_3d::generate_coords(&mol.inner);
168 let n = mol.inner.atom_count();
169 let coords: Vec<[f64; 3]> = (0..n)
170 .map(|i| {
171 let p = conf.get(chematic_core::AtomIdx(i as u32));
172 [p.x, p.y, p.z]
173 })
174 .collect();
175 match chematic_ff::mmff94_energy_breakdown(&mol.inner, &coords) {
176 Ok(bd) => format!(
177 r#"{{"bond":{:.4},"angle":{:.4},"torsion":{:.4},"vdw":{:.4},"elec":{:.4},"total":{:.4}}}"#,
178 bd.bond, bd.angle, bd.torsion, bd.vdw, bd.electrostatic, bd.total
179 ),
180 Err(e) => format!(r#"{{"error":"{}"}}"#, e),
181 }
182}
183
184#[wasm_bindgen]
189pub fn mmff94_partial_charges_json(mol: &MolHandle) -> String {
190 if mol.inner.atom_count() > WASM_MAX_ATOMS {
191 return format!(
192 r#"{{"error":"molecule too large (max {} atoms)"}}"#,
193 WASM_MAX_ATOMS
194 );
195 }
196 match chematic_ff::mmff94_charges_numeric(&mol.inner) {
197 Ok(q) => {
198 let vals: Vec<String> = q.iter().map(|x| format!("{x:.4}")).collect();
199 format!(r#"{{"charges":[{}]}}"#, vals.join(","))
200 }
201 Err(e) => format!(r#"{{"error":"{}"}}"#, e),
202 }
203}
204
205#[wasm_bindgen]
208pub fn pharmacophore_fp_3d_summary(mol: &MolHandle) -> String {
209 let _coords = chematic_3d::generate_coords(&mol.inner);
210 let features = chematic_perception::detect_features(&mol.inner);
211 let mut counts = [0; 6];
212 for feat in features {
213 let idx = match feat.ftype {
214 chematic_perception::FeatureType::Donor => 0,
215 chematic_perception::FeatureType::Acceptor => 1,
216 chematic_perception::FeatureType::Aromatic => 2,
217 chematic_perception::FeatureType::Hydrophobic => 3,
218 chematic_perception::FeatureType::Positive => 4,
219 chematic_perception::FeatureType::Negative => 5,
220 };
221 counts[idx] += 1;
222 }
223 format!(
224 "{{\"Donor\":{},\"Acceptor\":{},\"Aromatic\":{},\"Hydrophobic\":{},\"Positive\":{},\"Negative\":{}}}",
225 counts[0], counts[1], counts[2], counts[3], counts[4], counts[5]
226 )
227}
228
229#[wasm_bindgen]
232pub fn autocorr_3d_json(mol: &MolHandle) -> String {
233 let coords = chematic_3d::generate_coords(&mol.inner);
234 let ac = chematic_3d::autocorr_3d(&mol.inner, &coords);
235 format!(
236 "[{:.4},{:.4},{:.4},{:.4},{:.4},{:.4},{:.4},{:.4}]",
237 ac[0], ac[1], ac[2], ac[3], ac[4], ac[5], ac[6], ac[7]
238 )
239}
240
241#[wasm_bindgen]
246pub fn generate_3d_coords_json(mol: &MolHandle) -> String {
247 let coords = chematic_3d::generate_and_minimize_dreiding(&mol.inner);
248 let parts: Vec<String> = coords
249 .points
250 .iter()
251 .map(|p| format!("[{:.4},{:.4},{:.4}]", p.x, p.y, p.z))
252 .collect();
253 format!("[{}]", parts.join(","))
254}
255
256#[wasm_bindgen]
258pub fn generate_3d_etkdg_coords_json(mol: &MolHandle) -> String {
259 let coords = chematic_3d::generate_coords_etkdg(&mol.inner);
260 let parts: Vec<String> = coords
261 .points
262 .iter()
263 .map(|p| format!("[{:.4},{:.4},{:.4}]", p.x, p.y, p.z))
264 .collect();
265 format!("[{}]", parts.join(","))
266}
267
268#[wasm_bindgen]
271pub fn conformer_ensemble_json(mol: &MolHandle, n: u32, rmsd_threshold: f64) -> String {
272 let smiles = chematic_smiles::canonical_smiles(&mol.inner);
273 let fresh = match chematic_smiles::parse(&smiles) {
274 Ok(m) => m,
275 Err(_) => return r#"{"conformers":[],"count":0}"#.to_string(),
276 };
277 let config = chematic_3d::ConformerConfig {
278 count: n as usize,
279 rmsd_threshold,
280 ..chematic_3d::ConformerConfig::default()
281 };
282 match chematic_3d::generate_conformer_ensemble_with_config(fresh, &config) {
283 Ok(ensemble) => {
284 let conf_jsons: Vec<String> = (0..ensemble.conformer_count())
285 .filter_map(|i| ensemble.get_conformer(i))
286 .map(|c| {
287 let pts: Vec<String> = c
288 .points
289 .iter()
290 .map(|p| format!("[{:.4},{:.4},{:.4}]", p.x, p.y, p.z))
291 .collect();
292 format!("[{}]", pts.join(","))
293 })
294 .collect();
295 let count = conf_jsons.len();
296 format!(
297 r#"{{"conformers":[{}],"count":{count}}}"#,
298 conf_jsons.join(",")
299 )
300 }
301 Err(_) => r#"{"conformers":[],"count":0}"#.to_string(),
302 }
303}
304
305#[wasm_bindgen]
311pub fn shape_descriptors_json(mol: &MolHandle) -> String {
312 let coords = chematic_3d::generate_coords(&mol.inner);
313 let m = &*mol.inner;
314
315 fn fmt(v: f64) -> String {
316 if v.is_finite() {
317 format!("{v:.4}")
318 } else {
319 "null".to_string()
320 }
321 }
322
323 format!(
324 "{{\"pmi1\":{},\"pmi2\":{},\"pmi3\":{},\"npr1\":{},\"npr2\":{},\
325\"asphericity\":{},\"eccentricity\":{},\"radiusOfGyration\":{},\"planeOfBestFit\":{}}}",
326 fmt(chematic_3d::pmi1(m, &coords)),
327 fmt(chematic_3d::pmi2(m, &coords)),
328 fmt(chematic_3d::pmi3(m, &coords)),
329 fmt(chematic_3d::npr1(m, &coords)),
330 fmt(chematic_3d::npr2(m, &coords)),
331 fmt(chematic_3d::asphericity(m, &coords)),
332 fmt(chematic_3d::eccentricity(m, &coords)),
333 fmt(chematic_3d::radius_of_gyration(m, &coords)),
334 fmt(chematic_3d::plane_of_best_fit(m, &coords)),
335 )
336}
337
338#[wasm_bindgen]
344pub struct ConformerHandle {
345 inner: chematic_3d::ConformerEnsemble,
346}
347
348#[wasm_bindgen]
349impl ConformerHandle {
350 #[wasm_bindgen(constructor)]
354 pub fn new(smiles: &str) -> Result<ConformerHandle, JsValue> {
355 let mol = chematic_smiles::parse(smiles).map_err(|e| JsValue::from_str(&e.to_string()))?;
356 Ok(ConformerHandle {
357 inner: chematic_3d::ConformerEnsemble::new(mol),
358 })
359 }
360
361 pub fn conformer_count(&self) -> usize {
363 self.inner.conformer_count()
364 }
365
366 pub fn mol(&self) -> MolHandle {
368 let smi = chematic_smiles::canonical_smiles(self.inner.mol());
369 let mol = chematic_smiles::parse(&smi)
370 .unwrap_or_else(|_| chematic_core::MoleculeBuilder::new().build());
371 MolHandle {
372 inner: std::rc::Rc::new(mol),
373 }
374 }
375
376 pub fn add_generated_conformer(&mut self) -> usize {
380 let coords = chematic_3d::generate_coords(self.inner.mol());
381 self.inner.add_conformer(coords).unwrap_or(usize::MAX)
382 }
383
384 pub fn add_minimized_conformer(&mut self) -> usize {
388 let coords = chematic_3d::generate_coords(self.inner.mol());
389 let minimized = chematic_3d::minimize(self.inner.mol(), coords);
390 self.inner.add_conformer(minimized).unwrap_or(usize::MAX)
391 }
392
393 pub fn get_conformer_pdb(&self, idx: usize) -> Option<String> {
395 self.inner
396 .get_conformer(idx)
397 .map(|coords| chematic_3d::write_pdb(self.inner.mol(), coords))
398 }
399
400 pub fn conformer_rmsd(&self, a: usize, b: usize) -> f64 {
404 self.inner.conformer_rmsd(a, b).unwrap_or(f64::NAN)
405 }
406
407 pub fn conformer_rmsd_no_align(&self, a: usize, b: usize) -> f64 {
411 self.inner.conformer_rmsd_no_align(a, b).unwrap_or(f64::NAN)
412 }
413
414 pub fn remove_conformer(&mut self, idx: usize) -> bool {
416 self.inner.remove_conformer(idx).is_some()
417 }
418
419 pub fn cluster_conformers_json(&self, rms_threshold: f64) -> String {
429 let kept = self.inner.cluster_conformers_by_rms(rms_threshold);
430 let total = self.inner.conformer_count();
431 let removed = total.saturating_sub(kept.len());
432 let indices_str: Vec<String> = kept.iter().map(|i| i.to_string()).collect();
433 format!(
434 r#"{{"kept_indices":[{}],"removed_count":{}}}"#,
435 indices_str.join(","),
436 removed
437 )
438 }
439}
440
441#[wasm_bindgen]
451pub fn depict_data_with_coords_json(mol: &MolHandle, coords_json: &str) -> String {
452 let coords: Vec<(f64, f64)> = {
454 let mut result = Vec::new();
455 let inner = coords_json.trim().trim_matches(|c| c == '[' || c == ']');
456 for pair in inner.split("],[") {
458 let clean = pair.trim().trim_matches(|c| c == '[' || c == ']');
459 let nums: Vec<f64> = clean
460 .split(',')
461 .filter_map(|s| s.trim().parse::<f64>().ok())
462 .collect();
463 if nums.len() >= 2 {
464 result.push((nums[0], nums[1]));
465 }
466 }
467 result
468 };
469
470 let data = chematic_depict::depict_data_with_coords(&mol.inner, &coords);
471
472 let atoms: Vec<String> = data
473 .atoms
474 .iter()
475 .map(|a| {
476 let label = match &a.label {
477 Some(s) => format!("\"{}\"", escape_json_string(s)),
478 None => "null".to_string(),
479 };
480 let charge_suffix = if a.charge != 0 {
481 format!(",\"charge\":{}", a.charge)
482 } else {
483 String::new()
484 };
485 let label_prefix = format!("{label},");
486 format!(
487 r#"{{"idx":{},"element":"{}","x":{:.4},"y":{:.4},"label":{}"color":"{}"{}}}"#,
488 a.idx.0,
489 a.element.symbol(),
490 a.pos.x,
491 a.pos.y,
492 label_prefix,
493 escape_json_string(&a.color),
494 charge_suffix,
495 )
496 })
497 .collect();
498
499 let bond_kind = |k: &chematic_depict::DepictBondKind| match k {
500 chematic_depict::DepictBondKind::Single => "Single",
501 chematic_depict::DepictBondKind::Double => "Double",
502 chematic_depict::DepictBondKind::Triple => "Triple",
503 chematic_depict::DepictBondKind::Aromatic => "Aromatic",
504 chematic_depict::DepictBondKind::Up => "Up",
505 chematic_depict::DepictBondKind::Down => "Down",
506 };
507
508 let bonds: Vec<String> = data
509 .bonds
510 .iter()
511 .map(|b| {
512 format!(
513 r#"{{"idx":{},"atom1":{},"atom2":{},"kind":"{}"}}"#,
514 b.idx.0,
515 b.atom1.0,
516 b.atom2.0,
517 bond_kind(&b.kind)
518 )
519 })
520 .collect();
521
522 format!(
523 r#"{{"atoms":[{}],"bonds":[{}]}}"#,
524 atoms.join(","),
525 bonds.join(",")
526 )
527}
528
529#[wasm_bindgen]
541pub fn minimize_uff_json(smiles: &str, coords_json: &str, max_iter: u32) -> String {
542 let mol = match chematic_smiles::parse(smiles) {
543 Ok(m) => m,
544 Err(e) => return format!("{{\"error\":\"{e}\"}}"),
545 };
546 let coords_arr: Vec<[f64; 3]> = match serde_json::from_str(coords_json) {
547 Ok(v) => v,
548 Err(e) => return format!("{{\"error\":\"coords parse error: {e}\"}}"),
549 };
550 let types = chematic_ff::assign_uff_types(&mol);
551 let iters = if max_iter == 0 {
552 500
553 } else {
554 max_iter as usize
555 };
556 let result = chematic_ff::minimize_uff(&mol, &types, coords_arr, iters);
557 let coords_out: Vec<[f64; 3]> = result.coords;
558 let coords_str = coords_out
559 .iter()
560 .map(|c| format!("[{:.4},{:.4},{:.4}]", c[0], c[1], c[2]))
561 .collect::<Vec<_>>()
562 .join(",");
563 format!(
564 "{{\"coords\":[{coords_str}],\"energy\":{:.4},\"iterations\":{},\"converged\":{}}}",
565 result.energy, result.iterations, result.converged
566 )
567}
568
569#[wasm_bindgen]
586pub fn get_bond_length_json(smiles: &str, a: u32, b: u32) -> f64 {
587 if smiles.len() > WASM_MAX_INPUT_BYTES {
588 return -1.0;
589 }
590 let mol = match chematic_smiles::parse(smiles) {
591 Ok(m) => m,
592 Err(_) => return -1.0,
593 };
594 let coords = chematic_3d::generate_coords(&mol);
595 let a_idx = chematic_core::AtomIdx(a);
596 let b_idx = chematic_core::AtomIdx(b);
597 if a >= mol.atom_count() as u32 || b >= mol.atom_count() as u32 {
598 return -1.0;
599 }
600 chematic_3d::get_bond_length(&coords, a_idx, b_idx)
601}
602
603#[wasm_bindgen]
615pub fn get_dihedral_json(smiles: &str, a: u32, b: u32, c: u32, d: u32) -> JsValue {
616 if smiles.len() > WASM_MAX_INPUT_BYTES {
617 return JsValue::NULL;
618 }
619 let mol = match chematic_smiles::parse(smiles) {
620 Ok(m) => m,
621 Err(_) => return JsValue::NULL,
622 };
623 if a >= mol.atom_count() as u32
624 || b >= mol.atom_count() as u32
625 || c >= mol.atom_count() as u32
626 || d >= mol.atom_count() as u32
627 {
628 return JsValue::NULL;
629 }
630 let coords = chematic_3d::generate_coords(&mol);
631 let a_idx = chematic_core::AtomIdx(a);
632 let b_idx = chematic_core::AtomIdx(b);
633 let c_idx = chematic_core::AtomIdx(c);
634 let d_idx = chematic_core::AtomIdx(d);
635 match chematic_3d::get_dihedral_deg(&coords, a_idx, b_idx, c_idx, d_idx) {
636 Some(angle) => JsValue::from_f64(angle),
637 None => JsValue::NULL,
638 }
639}
640
641#[wasm_bindgen]
655pub fn set_dihedral_json(
656 smiles: &str,
657 a: u32,
658 b: u32,
659 c: u32,
660 d: u32,
661 angle_deg: f64,
662) -> Result<String, String> {
663 if smiles.len() > WASM_MAX_INPUT_BYTES {
664 return Err("Input SMILES too long".to_string());
665 }
666 let mol = chematic_smiles::parse(smiles).map_err(|e| format!("Parse error: {}", e))?;
667 if a >= mol.atom_count() as u32
668 || b >= mol.atom_count() as u32
669 || c >= mol.atom_count() as u32
670 || d >= mol.atom_count() as u32
671 {
672 return Err("Atom index out of range".to_string());
673 }
674 let coords = chematic_3d::generate_coords(&mol);
675 let a_idx = chematic_core::AtomIdx(a);
676 let b_idx = chematic_core::AtomIdx(b);
677 let c_idx = chematic_core::AtomIdx(c);
678 let d_idx = chematic_core::AtomIdx(d);
679 let angle_rad = angle_deg.to_radians();
680 let new_coords =
681 chematic_3d::set_dihedral(&coords, &mol, a_idx, b_idx, c_idx, d_idx, angle_rad);
682 Ok(chematic_3d::write_pdb(&mol, &new_coords))
684}
685
686