1#![forbid(unsafe_code)]
6
7pub mod layout;
8pub mod svg;
9
10use chematic_core::Molecule;
11
12pub use layout::{Layout, Point, compute_layout};
13pub use svg::render_svg;
14
15pub fn depict_svg(mol: &Molecule) -> String {
17 let layout = compute_layout(mol);
18 render_svg(mol, &layout)
19}
20
21#[cfg(test)]
26mod tests {
27 use super::*;
28 use crate::layout::BOND_LEN;
29
30 use chematic_smiles::parse;
31
32 fn mol(smiles: &str) -> Molecule {
34 parse(smiles).unwrap_or_else(|e| panic!("Failed to parse '{}': {:?}", smiles, e))
35 }
36
37 #[test]
42 fn test_layout_benzene_six_distinct_coords() {
43 let m = mol("c1ccccc1");
44 assert_eq!(m.atom_count(), 6);
45 let layout = compute_layout(&m);
46 assert_eq!(layout.coords.len(), 6);
47
48 for i in 0..6 {
50 for j in (i + 1)..6 {
51 let d = layout.coords[i].dist(&layout.coords[j]);
52 assert!(
53 d > BOND_LEN / 2.0,
54 "Atoms {} and {} are too close: {:.2} < {:.2}",
55 i,
56 j,
57 d,
58 BOND_LEN / 2.0
59 );
60 }
61 }
62 }
63
64 #[test]
68 fn test_layout_single_atom() {
69 let m = mol("[C]");
70 assert_eq!(m.atom_count(), 1);
71 let layout = compute_layout(&m);
72 assert_eq!(layout.coords.len(), 1);
73 let p = layout.coords[0];
75 assert!((p.x).abs() < 1.0 && (p.y).abs() < 1.0, "Single atom not near origin: {:?}", p);
76 }
77
78 #[test]
82 fn test_layout_ethane_bond_length() {
83 let m = mol("CC");
84 assert_eq!(m.atom_count(), 2);
85 let layout = compute_layout(&m);
86 assert_eq!(layout.coords.len(), 2);
87 let d = layout.coords[0].dist(&layout.coords[1]);
88 let tolerance = BOND_LEN * 0.01;
89 assert!(
90 (d - BOND_LEN).abs() < tolerance,
91 "Ethane bond distance {:.4} != BOND_LEN {:.4} (±{:.4})",
92 d,
93 BOND_LEN,
94 tolerance
95 );
96 }
97
98 #[test]
102 fn test_layout_naphthalene_ten_coords() {
103 let m = mol("c1ccc2ccccc2c1");
104 assert_eq!(m.atom_count(), 10);
105 let layout = compute_layout(&m);
106 assert_eq!(layout.coords.len(), 10);
107
108 for i in 0..10 {
110 for j in (i + 1)..10 {
111 let d = layout.coords[i].dist(&layout.coords[j]);
112 assert!(
113 d > BOND_LEN / 2.0,
114 "Naphthalene atoms {} and {} too close: {:.2}",
115 i,
116 j,
117 d
118 );
119 }
120 }
121
122 let (min_x, min_y, max_x, max_y) = layout.bounding_box();
124 assert!(max_x - min_x < 6.0 * BOND_LEN, "Naphthalene too wide");
125 assert!(max_y - min_y < 6.0 * BOND_LEN, "Naphthalene too tall");
126 }
127
128 #[test]
133 fn test_layout_disconnected_fragments_no_overlap() {
134 let m = mol("CC.CC");
135 assert_eq!(m.atom_count(), 4);
136 let layout = compute_layout(&m);
137
138 for i in 0..2 {
141 for j in 2..4 {
142 let d = layout.coords[i].dist(&layout.coords[j]);
143 assert!(
144 d >= BOND_LEN,
145 "Atoms from different fragments too close: atoms {} and {}, dist {:.2}",
146 i,
147 j,
148 d
149 );
150 }
151 }
152 }
153
154 #[test]
158 fn test_svg_benzene_no_text() {
159 let m = mol("c1ccccc1");
160 let layout = compute_layout(&m);
161 let svg = render_svg(&m, &layout);
162 assert!(svg.contains("<svg"), "SVG must start with <svg");
163 assert!(svg.contains("<line"), "SVG must have bond lines");
164 assert!(!svg.contains("<text"), "Benzene SVG must have no atom text labels");
165 }
166
167 #[test]
171 fn test_svg_pyridine_contains_nitrogen_label() {
172 let m = mol("c1ccncc1");
173 let layout = compute_layout(&m);
174 let svg = render_svg(&m, &layout);
175 assert!(svg.contains("<text"), "Pyridine SVG must have a text label");
176 assert!(svg.contains('N'), "Pyridine SVG must contain 'N' label");
177 }
178
179 #[test]
183 fn test_svg_aspirin_non_empty() {
184 let m = mol("CC(=O)Oc1ccccc1C(=O)O");
185 let layout = compute_layout(&m);
186 let svg = render_svg(&m, &layout);
187 assert!(!svg.is_empty(), "Aspirin SVG must be non-empty");
188 assert!(svg.contains("<line"), "Aspirin SVG must contain line elements");
189 }
190
191 #[test]
195 fn test_svg_double_bond_two_lines() {
196 let m = mol("C=C");
197 let layout = compute_layout(&m);
198 let svg = render_svg(&m, &layout);
199 let count = svg.matches("<line").count();
200 assert!(count >= 2, "C=C SVG should have >= 2 <line elements, got {}", count);
201 }
202
203 #[test]
207 fn test_depict_svg_caffeine_valid() {
208 let m = mol("Cn1cnc2c1c(=O)n(c(=O)n2C)C");
209 let svg = depict_svg(&m);
210 assert!(svg.starts_with("<svg"), "Caffeine SVG must start with <svg");
211 assert!(svg.ends_with("</svg>"), "Caffeine SVG must end with </svg>");
212 }
213
214 #[test]
218 fn test_depict_svg_water_contains_o() {
219 let m = mol("[OH2]");
220 let svg = depict_svg(&m);
221 assert!(svg.contains('O'), "Water SVG must contain 'O'");
222 }
223
224 #[test]
229 fn test_depict_svg_single_carbon_no_label() {
230 let m = mol("C");
231 let svg = depict_svg(&m);
232 assert!(svg.starts_with("<svg"), "Single C SVG must start with <svg");
233 assert!(svg.ends_with("</svg>"), "Single C SVG must end with </svg>");
234 assert!(!svg.contains("<text"), "Single C SVG should have no text label");
236 }
237}