#[derive(Debug, Clone, PartialEq)]
pub struct Style {
pub name: &'static str,
pub bond_length_pt: f64,
pub line_width_pt: f64,
pub bold_width_pt: f64,
pub margin_width_pt: f64,
pub hash_spacing_pt: f64,
pub bond_spacing_pct: f64,
pub chain_angle_deg: f64,
pub atom_label_pt: f64,
pub caption_pt: f64,
pub font_family: &'static str,
}
impl Style {
pub const ACS_1996: Style = Style {
name: "ACS Document 1996",
bond_length_pt: 14.4,
line_width_pt: 0.6,
bold_width_pt: 2.0,
margin_width_pt: 1.6,
hash_spacing_pt: 2.5,
bond_spacing_pct: 18.0,
chain_angle_deg: 120.0,
atom_label_pt: 10.0,
caption_pt: 10.0,
font_family: "Arial, Helvetica, sans-serif",
};
pub const CHEMDRAW_DEFAULT: Style = Style {
name: "ChemDraw New Document",
bond_length_pt: 30.0,
line_width_pt: 1.0,
bold_width_pt: 2.0,
margin_width_pt: 2.0,
hash_spacing_pt: 2.7,
bond_spacing_pct: 12.0,
chain_angle_deg: 120.0,
atom_label_pt: 10.0,
caption_pt: 12.0,
font_family: "Arial, Helvetica, sans-serif",
};
pub const ALL: [Style; 2] = [Style::ACS_1996, Style::CHEMDRAW_DEFAULT];
#[must_use]
pub fn label_size(&self) -> f64 {
self.atom_label_pt / self.bond_length_pt
}
#[must_use]
pub fn margin(&self) -> f64 {
self.margin_width_pt / self.bond_length_pt
}
#[must_use]
pub fn bond_spacing(&self) -> f64 {
self.bond_spacing_pct / 100.0
}
#[must_use]
pub fn layout_fingerprint(&self) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for v in [self.label_size(), self.chain_angle_deg] {
for b in v.to_bits().to_le_bytes() {
h ^= u64::from(b);
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
}
h
}
}
impl Default for Style {
fn default() -> Self {
Style::ACS_1996
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_fingerprint_really_is_fnv_1a() {
let mut h: u64 = 14_695_981_039_346_656_037; for b in b"foobar" {
h ^= u64::from(*b);
h = h.wrapping_mul(1_099_511_628_211); }
assert_eq!(
h, 0x8594_4171_f739_67e8,
"这对常数不是 FNV-1a 的 —— 官方测试向量 foobar 对不上"
);
let expect = |s: &Style| -> u64 {
let mut h: u64 = 14_695_981_039_346_656_037;
for v in [s.label_size(), s.chain_angle_deg] {
for b in v.to_bits().to_le_bytes() {
h ^= u64::from(b);
h = h.wrapping_mul(1_099_511_628_211);
}
}
h
};
let mut checked = 0usize;
for s in &Style::ALL {
assert_eq!(
s.layout_fingerprint(),
expect(s),
"{} 的指纹不是 FNV-1a —— 常数写错了",
s.name
);
checked += 1;
}
assert!(checked > 0, "一套规范都没查,这条判据在空过");
}
type Tweak = (&'static str, fn(&mut Style));
#[test]
fn the_fingerprint_covers_exactly_what_moves_the_layout() {
let sensitive = ["CCC(CC)C(O)(C(CC)CC)C(O)=O", "BrC(Br)CS(=O)(=O)C1=CC=CC=C1"];
let base = Style::ACS_1996;
let mols: Vec<_> = sensitive.iter().map(|s| crate::tests_prep(s)).collect();
let coords: Vec<Vec<crate::geom::Point2>> = mols
.iter()
.map(|m| crate::generate(m, &base).coords)
.collect();
let cases: [Tweak; 11] = [
("bond_length_pt", |s| s.bond_length_pt /= 1.4),
("atom_label_pt", |s| s.atom_label_pt *= 1.4),
("chain_angle_deg", |s| s.chain_angle_deg = 108.0),
("line_width_pt", |s| s.line_width_pt *= 1.4),
("bold_width_pt", |s| s.bold_width_pt *= 1.4),
("margin_width_pt", |s| s.margin_width_pt *= 1.4),
("hash_spacing_pt", |s| s.hash_spacing_pt *= 1.4),
("bond_spacing_pct", |s| s.bond_spacing_pct *= 1.4),
("caption_pt", |s| s.caption_pt *= 1.4),
("font_family", |s| s.font_family = "serif"),
("name", |s| s.name = "另一套规范"),
];
let Style {
name: _,
bond_length_pt: _,
line_width_pt: _,
bold_width_pt: _,
margin_width_pt: _,
hash_spacing_pt: _,
bond_spacing_pct: _,
chain_angle_deg: _,
atom_label_pt: _,
caption_pt: _,
font_family: _,
} = &base;
let (mut moved_any, mut still_any) = (false, false);
for (field, tweak) in cases {
let mut st = base.clone();
tweak(&mut st);
assert_ne!(st, base, "{field}:扰动没生效,这一档在空过");
let fp_changed = st.layout_fingerprint() != base.layout_fingerprint();
let moved = mols.iter().zip(&coords).any(|(m, c)| {
crate::generate(m, &st)
.coords
.iter()
.zip(c)
.any(|(u, v)| u.dist(*v) > 1e-9)
});
assert_eq!(
fp_changed, moved,
"{field}:指纹变={fp_changed} 而坐标变={moved} —— 指纹的覆盖面对不上"
);
moved_any |= moved;
still_any |= !moved;
}
assert!(
moved_any,
"没有一个字段挪动过布局 —— 分子挑得不敏感,这条判据在空过"
);
assert!(still_any, "没有一个字段是只进渲染的 —— 那也不对");
}
#[test]
fn the_two_styles_really_do_differ_in_what_counts_as_a_clash() {
let acs = Style::ACS_1996;
let cd = Style::CHEMDRAW_DEFAULT;
assert_eq!(
acs.atom_label_pt, cd.atom_label_pt,
"两套的字号本来就该一样"
);
assert!(
acs.label_size() > cd.label_size() * 1.9,
"标签占键长的比例应当差近一倍:ACS {:.2} vs 默认 {:.2}",
acs.label_size(),
cd.label_size()
);
let m = crate::tests_prep("BrC(Br)CS(=O)(=O)C1=CC=CC=C1");
let (a, b) = (crate::generate(&m, &acs), crate::generate(&m, &cd));
assert!(
a.coords
.iter()
.zip(&b.coords)
.any(|(u, v)| u.dist(*v) > 1e-9),
"两套规范画出来的坐标一模一样 —— 那把 Style 传进布局就是白传"
);
}
#[test]
fn the_fingerprint_tracks_layout_and_ignores_rendering() {
let base = Style::ACS_1996;
let mut render_only = base.clone();
render_only.line_width_pt = 99.0;
render_only.hash_spacing_pt = 42.0;
render_only.caption_pt = 7.0;
render_only.font_family = "Comic Sans";
assert_eq!(
base.layout_fingerprint(),
render_only.layout_fingerprint(),
"只改渲染项不该让已有坐标失效"
);
let mut layout_changed = base.clone();
layout_changed.bond_length_pt = 30.0;
assert_ne!(
base.layout_fingerprint(),
layout_changed.layout_fingerprint(),
"改了键长(进而改了碰撞阈值)却拿到同一个指纹,错配就查不出来了"
);
assert_ne!(
Style::ACS_1996.layout_fingerprint(),
Style::CHEMDRAW_DEFAULT.layout_fingerprint(),
"两套内置规范的布局指纹必须不同"
);
}
#[test]
fn the_numbers_match_the_chemdraw_manual() {
let a = Style::ACS_1996;
assert_eq!(
(a.bond_length_pt, a.line_width_pt, a.bold_width_pt),
(14.4, 0.6, 2.0)
);
assert_eq!((a.margin_width_pt, a.hash_spacing_pt), (1.6, 2.5));
assert_eq!(
(a.bond_spacing_pct, a.chain_angle_deg, a.atom_label_pt),
(18.0, 120.0, 10.0)
);
let c = Style::CHEMDRAW_DEFAULT;
assert_eq!(
(c.bond_length_pt, c.line_width_pt, c.bold_width_pt),
(30.0, 1.0, 2.0)
);
assert_eq!((c.margin_width_pt, c.hash_spacing_pt), (2.0, 2.7));
assert_eq!(
(c.bond_spacing_pct, c.chain_angle_deg, c.atom_label_pt),
(12.0, 120.0, 10.0)
);
}
}