use core::f64::consts::PI;
use std::collections::VecDeque;
use chematic_core::{AtomIdx, BondOrder, Molecule};
use chematic_perception::find_sssr;
use crate::coords::{Coords3D, Point3};
fn ideal_bond_len(mol: &Molecule, a: AtomIdx, b: AtomIdx) -> f64 {
let ea = mol.atom(a).element;
let eb = mol.atom(b).element;
let order = mol
.bond_between(a, b)
.map(|(_, bond)| bond.order)
.unwrap_or(BondOrder::Single);
let (lo, hi) = if ea.atomic_number() <= eb.atomic_number() {
(ea.atomic_number(), eb.atomic_number())
} else {
(eb.atomic_number(), ea.atomic_number())
};
match (lo, hi, order) {
(6, 6, BondOrder::Single) | (6, 6, BondOrder::Up) | (6, 6, BondOrder::Down) => 1.54,
(6, 6, BondOrder::Double) => 1.34,
(6, 6, BondOrder::Triple) => 1.20,
(6, 6, BondOrder::Aromatic) => 1.40,
(6, 7, BondOrder::Single) | (6, 7, BondOrder::Up) | (6, 7, BondOrder::Down) => 1.47,
(6, 7, BondOrder::Double) => 1.27,
(6, 7, BondOrder::Triple) => 1.16,
(6, 7, BondOrder::Aromatic) => 1.34,
(6, 8, BondOrder::Single) | (6, 8, BondOrder::Up) | (6, 8, BondOrder::Down) => 1.43,
(6, 8, BondOrder::Double) => 1.22,
(6, 8, BondOrder::Aromatic) => 1.36,
(6, 16, _) => 1.82,
(6, 9, _) => 1.35,
(6, 17, _) => 1.77,
(6, 35, _) => 1.94,
(6, 53, _) => 2.14,
(1, 6, _) => 1.09,
(1, 7, _) => 1.01,
(1, 8, _) => 0.96,
_ => 1.54,
}
}
fn ideal_angle(mol: &Molecule, center: AtomIdx) -> f64 {
let mut has_triple = false;
let mut has_double_or_arom = false;
for (nb, bidx) in mol.neighbors(center) {
let _ = nb;
let order = mol.bond(bidx).order;
match order {
BondOrder::Triple => has_triple = true,
BondOrder::Double | BondOrder::Aromatic => has_double_or_arom = true,
_ => {}
}
}
if has_triple {
PI } else if has_double_or_arom {
PI * 2.0 / 3.0 } else {
PI * 109.5_f64.to_radians() / PI }
}
fn connected_components(mol: &Molecule) -> Vec<Vec<AtomIdx>> {
let n = mol.atom_count();
let mut visited = vec![false; n];
let mut components: Vec<Vec<AtomIdx>> = Vec::new();
for start in 0..n {
if visited[start] {
continue;
}
let mut component: Vec<AtomIdx> = Vec::new();
let mut queue: VecDeque<AtomIdx> = VecDeque::new();
let start_idx = AtomIdx(start as u32);
visited[start] = true;
queue.push_back(start_idx);
while let Some(current) = queue.pop_front() {
component.push(current);
for (nb, _) in mol.neighbors(current) {
if !visited[nb.0 as usize] {
visited[nb.0 as usize] = true;
queue.push_back(nb);
}
}
}
components.push(component);
}
components
}
pub fn generate_coords(mol: &Molecule) -> Coords3D {
let n = mol.atom_count();
let mut coords = Coords3D::new_zeroed(n);
if n == 0 {
return coords;
}
if n == 1 {
coords.set(AtomIdx(0), Point3::zero());
return coords;
}
let ring_set = find_sssr(mol);
let components = connected_components(mol);
let mut x_offset = 0.0_f64;
for component in &components {
let placed = place_component(mol, component, &ring_set, x_offset, &mut coords);
let max_x = placed
.iter()
.map(|&idx| coords.get(idx).x)
.fold(f64::NEG_INFINITY, f64::max);
x_offset = max_x + 5.0;
}
coords
}
fn place_component<'a>(
mol: &Molecule,
component: &'a [AtomIdx],
ring_set: &chematic_perception::RingSet,
x_offset: f64,
coords: &mut Coords3D,
) -> &'a [AtomIdx] {
if component.is_empty() {
return component;
}
let total = mol.atom_count();
let mut placed = vec![false; total];
place_rings(mol, component, ring_set, x_offset, coords, &mut placed);
let root = component[0];
if !placed[root.0 as usize] {
coords.set(root, Point3::new(x_offset, 0.0, 0.0));
placed[root.0 as usize] = true;
}
dfs_place(mol, root, &mut placed, coords);
component
}
fn place_rings(
mol: &Molecule,
component: &[AtomIdx],
ring_set: &chematic_perception::RingSet,
x_offset: f64,
coords: &mut Coords3D,
placed: &mut Vec<bool>,
) {
let component_set: std::collections::HashSet<AtomIdx> =
component.iter().copied().collect();
let mut ring_cx = x_offset;
let mut ring_cy = 0.0_f64;
let mut first_ring = true;
for ring in ring_set.rings() {
if !ring.iter().all(|a| component_set.contains(a)) {
continue;
}
let ring_size = ring.len();
if ring_size == 0 {
continue;
}
let bond_len = {
let a0 = ring[0];
let a1 = ring[1 % ring_size];
ideal_bond_len(mol, a0, a1)
};
let r = bond_len / (2.0 * (PI / ring_size as f64).sin());
if first_ring {
ring_cx = x_offset + r;
ring_cy = 0.0;
first_ring = false;
} else {
let already_placed: Vec<AtomIdx> = ring
.iter()
.copied()
.filter(|a| placed[a.0 as usize])
.collect();
if already_placed.len() >= 2 {
let p0 = coords.get(already_placed[0]);
let p1 = coords.get(already_placed[1]);
ring_cx = (p0.x + p1.x) / 2.0;
ring_cy = (p0.y + p1.y) / 2.0 + r;
} else if already_placed.len() == 1 {
let p0 = coords.get(already_placed[0]);
ring_cx = p0.x + r;
ring_cy = p0.y;
}
}
for (k, &atom_idx) in ring.iter().enumerate() {
if placed[atom_idx.0 as usize] {
continue; }
let angle = 2.0 * PI * k as f64 / ring_size as f64;
let x = ring_cx + r * angle.cos();
let y = ring_cy + r * angle.sin();
coords.set(atom_idx, Point3::new(x, y, 0.0));
placed[atom_idx.0 as usize] = true;
}
}
}
fn dfs_place(mol: &Molecule, current: AtomIdx, placed: &mut Vec<bool>, coords: &mut Coords3D) {
let pos_current = coords.get(current);
let mut placed_neighbors: Vec<AtomIdx> = Vec::new();
let mut unplaced_neighbors: Vec<AtomIdx> = Vec::new();
for (nb, _) in mol.neighbors(current) {
if placed[nb.0 as usize] {
placed_neighbors.push(nb);
} else {
unplaced_neighbors.push(nb);
}
}
if unplaced_neighbors.is_empty() {
return;
}
let incoming_dir: Point3 = if let Some(&parent) = placed_neighbors.first() {
let p = coords.get(parent);
pos_current.sub(&p).normalize()
} else {
Point3::new(1.0, 0.0, 0.0)
};
let perp = perpendicular_to(incoming_dir);
let angle = ideal_angle(mol, current);
for (i, &nb) in unplaced_neighbors.iter().enumerate() {
let bond_len = ideal_bond_len(mol, current, nb);
let bend_angle = PI - angle; let dir_bent = rotate_around_axis(incoming_dir, perp, bend_angle);
let dihedral = (i as f64) * (2.0 * PI / 3.0); let dir_final = rotate_around_axis(dir_bent, incoming_dir, dihedral);
let new_pos = pos_current.add(&dir_final.scale(bond_len));
coords.set(nb, new_pos);
placed[nb.0 as usize] = true;
dfs_place(mol, nb, placed, coords);
}
}
fn perpendicular_to(v: Point3) -> Point3 {
let candidate = if v.x.abs() < 0.9 {
Point3::new(1.0, 0.0, 0.0)
} else {
Point3::new(0.0, 1.0, 0.0)
};
let proj = v.scale(v.dot(&candidate));
candidate.sub(&proj).normalize()
}
fn rotate_around_axis(v: Point3, axis: Point3, theta: f64) -> Point3 {
let cos_t = theta.cos();
let sin_t = theta.sin();
let dot = axis.dot(&v);
let term1 = v.scale(cos_t);
let term2 = axis.cross(&v).scale(sin_t);
let term3 = axis.scale(dot * (1.0 - cos_t));
term1.add(&term2).add(&term3)
}