1pub const SCALE: f64 = 20.0;
7
8pub const BOND_PX: f64 = SCALE;
10
11pub const FONT_FRAC: f64 = 0.45;
16
17pub const FONT_STEM_EM: f64 = 0.0933;
19
20pub const FONT_STEM_EM_BOLD: f64 = 0.144;
24
25pub const STROKE_FRAC: f64 = 0.042; pub const OFFSET_FRAC: f64 = 0.15;
31
32pub const WEDGE_WIDTH_FRAC: f64 = 0.30;
34
35pub const PAD_FRAC: f64 = 1.0;
36pub const END_GAP_FRAC: f64 = 0.13;
37pub const CHAIN_END_GAP_FRAC: f64 = 0.0;
38pub const HASH_PER_BOND: i32 = 8;
39pub const HALO_FRAC: f64 = 2.0 * STROKE_FRAC;
40pub const LABEL_GAP_FRAC: f64 = 0.05;
41pub const SHADE_FRAC: f64 = 0.90;
42pub const MARK_FRAC: f64 = 1.0;
43pub const MARK_STROKE_FRAC: f64 = 0.1;
49pub const MARK_HALO_STROKE_FRAC: f64 = 0.2;
51pub const MARK_OPACITY: f64 = 0.7;
52pub const MARK_HALO_OPACITY: f64 = 0.45;
53pub const MARK_HALO_COLOR: &str = "#555";
59pub const SCRIPT_SCALE: f64 = 0.66;
61pub const STAR_FRAC: f64 = 2.0;
66
67pub const STROKE_PX: f64 = STROKE_FRAC * BOND_PX;
68pub const OFFSET_PX: f64 = OFFSET_FRAC * BOND_PX;
69pub const WEDGE_HALF_PX: f64 = WEDGE_WIDTH_FRAC * BOND_PX * 0.5;
70pub const FONT_PX: f64 = FONT_FRAC * BOND_PX;
71pub const PAD_PX: f64 = PAD_FRAC * BOND_PX;
72pub const END_GAP_PX: f64 = END_GAP_FRAC * BOND_PX;
73pub const CHAIN_END_GAP_PX: f64 = CHAIN_END_GAP_FRAC * BOND_PX;
74pub const HALO_STROKE: f64 = HALO_FRAC * BOND_PX;
75pub const LABEL_GAP_PX: f64 = LABEL_GAP_FRAC * BOND_PX;
76pub const HALO_GAP_PX: f64 = 0.5 * LABEL_GAP_PX;
78pub const HALO_OPACITY: f64 = 0.5;
79pub const MARK_STROKE_PX: f64 = MARK_STROKE_FRAC * BOND_PX;
80pub const MARK_HALO_STROKE_PX: f64 = MARK_HALO_STROKE_FRAC * BOND_PX;
81
82pub const WEIGHT_AT_ONE: f64 = 1.5;
88
89pub const WEIGHT_MIN: f64 = 1.0 / WEIGHT_AT_ONE;
91
92pub fn diagram_weight(weight: f64) -> f64 {
100 assert!(
101 weight.is_finite() && weight >= WEIGHT_MIN - 1e-12,
102 "mol weight must be finite and >= {WEIGHT_MIN}, got {weight}"
103 );
104 weight * WEIGHT_AT_ONE
105}
106
107pub fn label_weight_grow_px(weight: f64) -> f64 {
115 let ink = diagram_weight(weight);
116 0.5 * FONT_STEM_EM * FONT_PX * (ink - 1.0)
117}
118
119pub fn label_weight_standoff_px(weight: f64) -> f64 {
126 label_weight_grow_px(weight) + 0.5 * (stroke_px_for_weight(weight) - STROKE_PX)
127}
128
129pub fn stroke_px_for_weight(weight: f64) -> f64 {
134 stroke_px_from_stem(FONT_STEM_EM) * diagram_weight(weight)
135}
136
137pub fn stroke_frac_from_stem(stem_em: f64) -> f64 {
139 (stem_em * FONT_FRAC * 1000.0).round() / 1000.0
140}
141
142pub fn stroke_px_from_stem(stem_em: f64) -> f64 {
144 stroke_frac_from_stem(stem_em) * BOND_PX
145}
146
147pub fn halo_stroke_from_stroke(stroke_px: f64) -> f64 {
149 2.0 * stroke_px
150}
151
152pub fn halo_stroke_for_weight(weight: f64) -> f64 {
157 HALO_STROKE * diagram_weight(weight).sqrt()
158}
159
160pub fn halo_gap_for_weight(weight: f64) -> f64 {
165 HALO_GAP_PX * diagram_weight(weight).sqrt()
166}
167
168pub fn hash_count(length: f64) -> usize {
170 let n = (f64::from(HASH_PER_BOND) * length / BOND_PX).round() as i32;
171 n.clamp(5, 12) as usize
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn offset_matches_python_house_style() {
180 assert!((OFFSET_PX - 3.0).abs() < 1e-9);
181 assert!((STROKE_PX - 0.84).abs() < 1e-9);
182 assert!((FONT_PX - 9.0).abs() < 1e-9);
183 assert!((SHADE_FRAC - 0.90).abs() < 1e-9);
184 }
185
186 #[test]
187 fn stroke_tracks_font_stem() {
188 let expected = (FONT_STEM_EM * FONT_FRAC * 1000.0).round() / 1000.0;
189 assert!((STROKE_FRAC - expected).abs() < 1e-9);
190 assert!((stroke_px_from_stem(FONT_STEM_EM) - STROKE_PX).abs() < 1e-9);
191 }
192
193 #[test]
194 fn weight_scales_stroke_and_grow() {
195 assert!((diagram_weight(1.0) - WEIGHT_AT_ONE).abs() < 1e-9);
197 assert!((diagram_weight(WEIGHT_MIN) - 1.0).abs() < 1e-9);
198 assert!((stroke_px_for_weight(WEIGHT_MIN) - STROKE_PX).abs() < 1e-9);
199 assert!((stroke_px_for_weight(1.0) - WEIGHT_AT_ONE * STROKE_PX).abs() < 1e-9);
200 assert!((stroke_px_for_weight(2.0) - 2.0 * WEIGHT_AT_ONE * STROKE_PX).abs() < 1e-9);
201 assert!(label_weight_grow_px(WEIGHT_MIN).abs() < 1e-12);
202 let grow1 = label_weight_grow_px(1.0);
203 assert!((grow1 - 0.5 * FONT_STEM_EM * FONT_PX * (WEIGHT_AT_ONE - 1.0)).abs() < 1e-9);
204 assert!((halo_stroke_from_stroke(STROKE_PX) - 2.0 * STROKE_PX).abs() < 1e-9);
205 assert!((halo_stroke_for_weight(WEIGHT_MIN) - HALO_STROKE).abs() < 1e-9);
206 assert!((halo_stroke_for_weight(1.0) - HALO_STROKE * WEIGHT_AT_ONE.sqrt()).abs() < 1e-9);
207 let w_abs4 = 4.0 / WEIGHT_AT_ONE;
209 assert!((halo_stroke_for_weight(w_abs4) - 2.0 * HALO_STROKE).abs() < 1e-9);
210 assert!((halo_gap_for_weight(w_abs4) - 2.0 * HALO_GAP_PX).abs() < 1e-9);
211 assert!(halo_stroke_for_weight(2.0) < halo_stroke_from_stroke(stroke_px_for_weight(2.0)));
212 assert!(label_weight_standoff_px(WEIGHT_MIN).abs() < 1e-12);
213 let stand1 = label_weight_standoff_px(1.0);
214 assert!(stand1 > label_weight_grow_px(1.0));
215 assert!(
216 (stand1
217 - (label_weight_grow_px(1.0)
218 + 0.5 * (stroke_px_for_weight(1.0) - STROKE_PX)))
219 .abs()
220 < 1e-9
221 );
222 }
223
224 #[test]
225 #[should_panic(expected = "mol weight must be finite and >=")]
226 fn diagram_weight_rejects_below_min() {
227 let _ = diagram_weight(0.5);
228 }
229}