use std::collections::BTreeMap;
use omgkit_core::MolBuilder;
use omgkit_depict::{
generate,
geom::{point_in_polygon, Point2},
label::Label,
render::{is_squeezed, label_at, scene, touches_glyphs, Primitive, Scene},
style::Style,
};
const WRITINGS: usize = 30;
const SEED_TRIES: usize = 8;
fn check_the_shuffler() {
for n in [2usize, 3, 5, 8, 13, 30, 100] {
let mut identical = 0usize;
for seed in 0..64u64 {
let p = shuffled(n, seed);
let mut sorted = p.clone();
sorted.sort_unstable();
assert!(
sorted.into_iter().eq(0..u32::try_from(n).expect("n 不大")),
"n={n} seed={seed}:搅出来的根本不是一个置换"
);
if p.iter().enumerate().all(|(i, x)| *x as usize == i) {
identical += 1;
}
}
let allowed = if n <= 3 { 40 } else { 2 };
assert!(
identical <= allowed,
"n={n}:64 个种子里有 {identical} 个搅出恒等置换 —— 这样的搅拌器验不了写法无关"
);
}
}
fn main() {
check_the_shuffler();
let path = std::env::args()
.nth(1)
.unwrap_or_else(|| "harness/corpus/large.smi".into());
let writings: usize = std::env::args().nth(2).map_or(WRITINGS, |x| {
x.parse().expect("第二个参数是写法数,要是个整数")
});
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("读不了 {path}:{e}"));
let dump = std::env::args().nth(3);
let mut prints: Vec<String> = Vec::new();
let mut fails: BTreeMap<&'static str, Vec<String>> = BTreeMap::new();
let mut n_ok = 0usize;
let mut n_skip = 0usize;
let mut checked: BTreeMap<&'static str, usize> = BTreeMap::new();
let mut quality: BTreeMap<&'static str, usize> = BTreeMap::new();
for (lineno, line) in text.lines().enumerate().map(|(i, l)| (i + 1, l)) {
let smi = line.split_whitespace().next().unwrap_or("");
if smi.is_empty() || smi.starts_with('#') {
continue;
}
let Some(m) = prep(smi) else {
n_skip += 1;
continue;
};
if m.num_atoms() < 2 {
n_skip += 1;
continue;
}
n_ok += 1;
for style in &Style::ALL {
let d = generate(&m, style);
let s = scene(&m, &d, style);
let grown = d.drawn(&m);
let tag = format!("{}:{lineno}:{smi}", style.name);
if dump.is_some() {
prints.push(format!(
"{lineno:07}:{}:{smi} {:016x}",
style.name.replace(' ', "_"),
scene_digest(&s)
));
}
let clean = d.degraded.is_empty() && d.unresolved.is_empty();
if !d.crossings.is_empty() {
*quality.entry("—— 其中有键交叉").or_default() += 1usize;
let terminal = d.crossings.iter().any(|(b1, b2)| {
[b1, b2].iter().any(|b| {
let bd = &grown.bonds()[**b as usize];
grown.degree(bd.begin) == 1 || grown.degree(bd.end) == 1
})
});
*quality
.entry(if d.degraded.is_empty() {
"—— 布局没退化的"
} else {
"—— 布局已退化的"
})
.or_default() += 1usize;
*quality
.entry(if terminal {
"—— 涉及端基键的"
} else {
"—— 两端都不是端基的"
})
.or_default() += 1usize;
}
if labels_really_overlap(&grown, &d, style) > 0 {
*quality.entry("—— 其中确有两个标签盒叠上").or_default() += 1usize;
}
if !d.degraded.is_empty()
&& d.crossings.is_empty()
&& labels_really_overlap(&grown, &d, style) == 0
&& no_atom_sits_on_another(&grown, &d).2.is_none()
{
*quality
.entry("—— 其中只是标着退化,没有别的毛病")
.or_default() += 1usize;
}
if squeezed_bonds(&grown, &d, style) > 0 {
*quality.entry("—— 有标签在键上塞不下").or_default() += 1usize;
}
if accidental_collinear(&grown, &d) > 0 {
*quality.entry("—— 有骨架原子被摆成 180°").or_default() += 1usize;
}
if cramped_substituents(&grown, &d) > 0 {
*quality.entry("—— 有取代基挤到另一根键上").or_default() += 1usize;
}
let (forced, avoidable) = ring_wedges(&grown, &d);
if forced > 0 {
*quality.entry("—— 有楔形只能画在环键上").or_default() += 1usize;
}
if avoidable > 0 {
*quality
.entry("—— 其中本可避开(抢共用键输了)")
.or_default() += 1usize;
}
if crowded_centres(&grown, &d) > 0 {
*quality.entry("—— 有立体中心的取代基挤在一侧").or_default() += 1usize;
}
for g in &d.degraded {
let omgkit_depict::rings::Degradation::BridgedRingSystem { template, .. } = g
else {
continue;
};
match template {
omgkit_depict::templates::Status::Hit => {}
omgkit_depict::templates::Status::NotInTable => {
*quality
.entry("—— 其中表里没有(骨架该补进 bridged.smi)")
.or_default() += 1usize;
}
omgkit_depict::templates::Status::NoFingerprint => {
*quality
.entry("—— 其中骨架指纹算不出来(要查那个分子本身)")
.or_default() += 1usize;
}
}
}
*quality
.entry(if !d.degraded.is_empty() {
"退化(桥环等)"
} else if !d.unresolved.is_empty() {
"有未解冲突"
} else if !d.crossings.is_empty() {
"有键交叉"
} else {
"干净"
})
.or_default() += 1usize;
for (name, hit, bad) in checks(&m, &grown, &d, &s, style, clean, writings) {
*checked.entry(name).or_default() += usize::from(hit);
if let Some(why) = bad {
fails
.entry(name)
.or_default()
.push(format!("{tag} —— {why}"));
}
}
}
}
if let Some(p) = &dump {
prints.sort();
prints.push(String::new()); std::fs::write(p, prints.join("\n")).unwrap_or_else(|e| panic!("写不了 {p}:{e}"));
println!("逐图指纹落在 {p}({} 张)\n", prints.len() - 1);
}
println!("语料 {path}:解析成功 {n_ok},跳过 {n_skip};每分子比 {writings} 种写法\n");
let tot: usize = quality
.iter()
.filter(|(k, _)| !k.starts_with('—'))
.map(|(_, v)| *v)
.sum();
println!("出图质量({tot} 个分子×规范):");
for (k, v) in &quality {
println!(" {k:<16} {v:>6} {:>5.1}%", 100.0 * *v as f64 / tot as f64);
}
println!();
println!("{:<14} {:>10} {:>8}", "性质", "查到", "违例");
let mut total = 0usize;
for (name, n) in &checked {
let bad = fails.get(name).map_or(0, Vec::len);
total += bad;
println!("{name:<14} {n:>10} {bad:>8}");
}
for (name, list) in &fails {
println!("\n=== {name} 的前几例 ===");
for x in list.iter().take(300) {
println!(" {x}");
}
if list.len() > 300 {
println!(" …… 另有 {} 例", list.len() - 300);
}
}
println!(
"\n{}",
if total == 0 {
"全部通过".to_string()
} else {
format!("共 {total} 处违例")
}
);
if total > 0 {
std::process::exit(1);
}
}
fn scene_digest(s: &Scene) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for item in fingerprint(s) {
for b in item.as_bytes() {
h ^= u64::from(*b);
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h ^= 0xff;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}
fn prep(smi: &str) -> Option<MolBuilder> {
let mut m = omgkit_io::smiles::parse(smi).ok()?;
omgkit_chem::pipeline::sanitize(&mut m).ok()?;
omgkit_io::stereo::perceive_bond_stereo(&mut m);
Some(m)
}
fn cramped_substituents(m: &MolBuilder, d: &omgkit_depict::Depiction) -> usize {
const CRAMPED: f64 = 15.0;
let mut n = 0usize;
for a in 0..u32::try_from(m.num_atoms()).expect("原子数超出 u32") {
let here = d.coords[a as usize];
let mut angs: Vec<f64> = m
.neighbors(a)
.map(|(nb, _)| {
(d.coords[nb as usize] - here)
.angle()
.to_degrees()
.rem_euclid(360.0)
})
.collect();
if angs.len() < 3 {
continue; }
angs.sort_by(|x, y| x.partial_cmp(y).expect("角度不会是 NaN"));
for k in 0..angs.len() {
let gap = (angs[(k + 1) % angs.len()] - angs[k]).rem_euclid(360.0);
if gap < CRAMPED {
n += 1;
}
}
}
n
}
fn labels_really_overlap(m: &MolBuilder, d: &omgkit_depict::Depiction, style: &Style) -> usize {
d.unresolved
.iter()
.filter(|(a, b)| {
let (Some(la), Some(lb)) = (
label_at(m, *a, style, &d.coords),
label_at(m, *b, style, &d.coords),
) else {
return false;
};
let dv = d.coords[*a as usize] - d.coords[*b as usize];
dv.x.abs() < la.half_w + lb.half_w && dv.y.abs() < la.half_h + lb.half_h
})
.count()
}
fn accidental_collinear(m: &MolBuilder, d: &omgkit_depict::Depiction) -> usize {
use omgkit_core::BondOrder;
use omgkit_depict::render::is_collinear;
(0..u32::try_from(m.num_atoms()).expect("原子数超出 u32"))
.filter(|a| {
if !is_collinear(m, *a, &d.coords) {
return false;
}
let mut doubles = 0usize;
let mut triple = false;
for (_, bi) in m.neighbors(*a) {
match m.bonds()[bi as usize].order {
BondOrder::Triple => triple = true,
BondOrder::Double => doubles += 1,
_ => {}
}
}
!(triple || doubles >= 2)
})
.count()
}
fn ring_wedges(m: &MolBuilder, d: &omgkit_depict::Depiction) -> (usize, usize) {
use omgkit_core::BondOrder;
let rings = omgkit_chem::sssr::ring_set(m);
let (mut forced, mut avoidable) = (0usize, 0usize);
for (bi, w) in d.wedges.iter().enumerate() {
let Some(narrow) = w.narrow() else { continue };
if !rings.iter().any(|r| r.bonds.contains(&(bi as u32))) {
continue;
}
let has_alt = m.neighbors(narrow).any(|(_, b)| {
m.bonds()[b as usize].order == BondOrder::Single
&& !rings.iter().any(|r| r.bonds.contains(&b))
});
if has_alt {
avoidable += 1;
} else {
forced += 1;
}
}
(forced, avoidable)
}
fn crowded_centres(m: &MolBuilder, d: &omgkit_depict::Depiction) -> usize {
(0..u32::try_from(m.num_atoms()).expect("原子数超出 u32"))
.filter(|a| {
if !m.neighbors(*a).any(|(_, bi)| {
d.wedges
.get(bi as usize)
.and_then(|w| w.narrow())
.is_some_and(|n| n == *a)
}) {
return false;
}
let c = d.coords[*a as usize];
let mut angs: Vec<f64> = m
.neighbors(*a)
.map(|(n, _)| {
let v = d.coords[n as usize] - c;
v.y.atan2(v.x).to_degrees().rem_euclid(360.0)
})
.collect();
if angs.len() != 3 {
return false;
}
angs.sort_by(|x, y| x.partial_cmp(y).expect("坐标非 NaN"));
let mut gap = 360.0 - (angs[2] - angs[0]);
for w in angs.windows(2) {
gap = f64::max(gap, w[1] - w[0]);
}
gap > 180.0
})
.count()
}
fn labels_of(m: &MolBuilder, d: &omgkit_depict::Depiction, style: &Style) -> Vec<Option<Label>> {
(0..u32::try_from(m.num_atoms()).expect("原子数超出 u32"))
.map(|a| label_at(m, a, style, &d.coords))
.collect()
}
fn squeezed(
m: &MolBuilder,
d: &omgkit_depict::Depiction,
style: &Style,
labels: &[Option<Label>],
) -> Vec<bool> {
m.bonds()
.iter()
.map(|b| {
is_squeezed(
d.coords[b.begin as usize],
d.coords[b.end as usize],
labels[b.begin as usize].as_ref(),
labels[b.end as usize].as_ref(),
style,
)
})
.collect()
}
fn squeezed_bonds(m: &MolBuilder, d: &omgkit_depict::Depiction, style: &Style) -> usize {
squeezed(m, d, style, &labels_of(m, d, style))
.iter()
.filter(|x| **x)
.count()
}
fn shape(c: &[Point2]) -> Vec<i64> {
let mut v: Vec<i64> = (0..c.len())
.flat_map(|i| ((i + 1)..c.len()).map(move |j| (i, j)))
.map(|(i, j)| (c[i].dist(c[j]) * 1e4).round() as i64)
.collect();
v.sort_unstable();
v
}
fn quantised(c: &[Point2]) -> Vec<(i64, i64)> {
let mut v: Vec<(i64, i64)> = c
.iter()
.map(|p| ((p.x * 1e4).round() as i64, (p.y * 1e4).round() as i64))
.collect();
v.sort_unstable();
v
}
fn fingerprint(s: &Scene) -> Vec<String> {
let q = |p: Point2| format!("{:.3},{:.3}", p.x, p.y);
let mut v: Vec<String> = s
.items
.iter()
.map(|it| match it {
Primitive::Line { from, to, .. } => {
let (x, y) = (q(*from), q(*to));
if x <= y {
format!("L {x} {y}")
} else {
format!("L {y} {x}")
}
}
Primitive::Wedge { from, to, .. } => format!("W {} {}", q(*from), q(*to)),
Primitive::Hash { from, to, .. } => format!("H {} {}", q(*from), q(*to)),
Primitive::Text { at, runs, .. } => format!("T {} {runs:?}", q(*at)),
Primitive::Ball { .. } | Primitive::Stick { .. } => {
unreachable!("二维审计收到了三维图元 —— 场景拿错了")
}
})
.collect();
v.sort();
v
}
type Check = (&'static str, bool, Option<String>);
fn checks(
orig: &MolBuilder,
drawn: &MolBuilder,
d: &omgkit_depict::Depiction,
s: &Scene,
style: &Style,
clean: bool,
writings: usize,
) -> Vec<Check> {
let mut v = vec![
ring_double_bonds(drawn, d, s, style, clean),
wedges_reach_canvas(d, s),
wedges_read_back(drawn, d),
bond_lengths_equal(drawn, d, clean),
inside_canvas(drawn, d, s, style),
lines_clear_of_labels(drawn, d, s, style),
no_atom_sits_on_another(drawn, d),
no_angle_is_pinched(drawn, d, clean),
];
v.extend(writing_independent(orig, d, s, style, clean, writings));
v
}
fn no_angle_is_pinched(m: &MolBuilder, d: &omgkit_depict::Depiction, clean: bool) -> Check {
if !clean {
return ("键角不过窄", false, None);
}
const FLOOR: f64 = 89.0;
for a in 0..u32::try_from(m.num_atoms()).expect("原子数超出 u32") {
let nbrs: Vec<u32> = m.neighbors(a).map(|(n, _)| n).collect();
if nbrs.len() < 2 || nbrs.len() > 3 {
continue;
}
let c = d.coords[a as usize];
for i in 0..nbrs.len() {
for j in (i + 1)..nbrs.len() {
if m.neighbors(nbrs[i]).any(|(n, _)| n == nbrs[j]) {
continue;
}
let u = (d.coords[nbrs[i] as usize] - c).normalized();
let v = (d.coords[nbrs[j] as usize] - c).normalized();
let deg = u.dot(v).clamp(-1.0, 1.0).acos().to_degrees();
if deg < FLOOR {
return (
"键角不过窄",
true,
Some(format!(
"原子 {a} 处 {}–{a}–{} 的夹角只有 {deg:.1}°",
nbrs[i], nbrs[j]
)),
);
}
}
}
}
("键角不过窄", true, None)
}
fn no_atom_sits_on_another(m: &MolBuilder, d: &omgkit_depict::Depiction) -> Check {
const TOL: f64 = 0.05; for i in 0..d.coords.len() {
for j in (i + 1)..d.coords.len() {
let dist = d.coords[i].dist(d.coords[j]);
if dist < TOL {
let reported = d
.unresolved
.iter()
.any(|(a, b)| (*a as usize, *b as usize) == (i, j));
return (
"原子不重合",
true,
Some(format!(
"原子 {i} 与 {j} 相距 {dist:.4} 个键长{}",
if reported {
"(已报未解冲突)"
} else {
"**而且没报出来**"
}
)),
);
}
}
}
let _ = m;
("原子不重合", true, None)
}
fn shuffled(n: usize, seed: u64) -> Vec<u32> {
let mut state = seed;
let mut next = move || -> u64 {
state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
};
let mut p: Vec<u32> = (0..u32::try_from(n).unwrap_or(u32::MAX)).collect();
for i in (1..n).rev() {
let j = usize::try_from(next() % (i as u64 + 1)).unwrap_or(0);
p.swap(i, j);
}
p
}
fn seed_of(s: &str) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in s.bytes() {
h ^= u64::from(b);
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}
fn writing_independent(
m: &MolBuilder,
d0: &omgkit_depict::Depiction,
s: &Scene,
style: &Style,
clean: bool,
writings: usize,
) -> [Check; 3] {
let want = fingerprint(s);
let n = m.num_atoms();
let canon = omgkit_io::canon::canonical_smiles(m).smiles;
let ranks0 = omgkit_io::canon::canonical_ranks(m);
let base = seed_of(&canon);
let (mut compared, mut skipped, mut unshuffled) = (0usize, 0usize, 0usize);
for k in 1..=writings {
let mut found: Option<(String, MolBuilder)> = None;
for t in 0..SEED_TRIES {
let seed = base
.wrapping_add((k as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15))
.wrapping_add((t as u64).wrapping_mul(0xD1B5_4A32_D192_ED03));
let w = omgkit_io::smiles::write_with_priority(m, &shuffled(n, seed));
let Some(m2) = prep(&w.smiles) else { continue };
if omgkit_io::canon::canonical_ranks(&m2) == ranks0 {
continue; }
found = Some((w.smiles, m2));
break;
}
let Some((smiles, m2)) = found else {
unshuffled += 1;
continue;
};
if canon != omgkit_io::canon::canonical_smiles(&m2).smiles {
skipped += 1;
continue;
}
compared += 1;
let d2 = generate(&m2, style);
let got = fingerprint(&scene(&m2, &d2, style));
if got != want {
let diff = want.iter().zip(&got).filter(|(a, b)| a != b).count();
let coords_same = quantised(&d0.coords) == quantised(&d2.coords);
let wedge_kinds = |d: &omgkit_depict::Depiction| {
let mut v: Vec<String> = d
.wedges
.iter()
.filter_map(|w| {
w.narrow()
.map(|_| format!("{w:?}").split('{').next().unwrap_or("").to_string())
})
.collect();
v.sort();
v
};
let layer = if !coords_same {
if shape(&d0.coords) == shape(&d2.coords) {
"形状相同,摆位不同"
} else {
"形状就变了"
}
} else if wedge_kinds(d0) != wedge_kinds(&d2) {
"坐标相同,楔形不同"
} else {
"坐标相同,键级或落点不同"
};
return [
(
"写法无关",
true,
Some(format!(
"[{layer}|{}] 写成 {smiles} 之后有 {diff}/{} 处图元不同",
if clean {
"布局干净"
} else {
"布局已退化"
},
want.len()
)),
),
("写法无关·比满", compared == writings, None),
("写法无关·没查成", false, None),
];
}
}
let broke_the_molecule = compared == 0 && skipped > 0;
[
(
"写法无关",
compared > 0,
broke_the_molecule.then(|| {
format!(
"{skipped} 次改写出来不是同一个分子(另有 {unshuffled} 次换不出新存储序)\
—— 改写器坏了,不是画错了"
)
}),
),
("写法无关·比满", compared == writings, None),
("写法无关·没查成", compared == 0, None),
]
}
fn ring_double_bonds(
m: &MolBuilder,
d: &omgkit_depict::Depiction,
s: &Scene,
style: &Style,
clean: bool,
) -> Check {
if !clean {
return ("环内双键", false, None);
}
let pts = canvas_pts(m, d, style);
let rings = omgkit_chem::sssr::ring_set(m);
let orders = omgkit_depict::render::drawn_orders(m);
let mut hit = false;
for (bi, b) in m.bonds().iter().enumerate() {
let bond_no = u32::try_from(bi).expect("键数超出 u32");
let mine: Vec<_> = rings
.iter()
.filter(|r| r.bonds.contains(&bond_no))
.collect();
if mine.is_empty() || orders[bi] != omgkit_core::BondOrder::Double {
continue;
}
let (pa, pb) = (pts[b.begin as usize], pts[b.end as usize]);
let len = pa.dist(pb);
if len < 1e-9 {
continue;
}
let mid = (pa + pb) * 0.5;
let axis = (pb - pa) * (1.0 / len);
let normal = Point2::new(-axis.y, axis.x);
let lines: Vec<(Point2, Point2)> = s
.items
.iter()
.filter_map(|it| match it {
Primitive::Line { from, to, .. } => Some((*from, *to)),
_ => None,
})
.filter(|(f, t)| {
[f, t].iter().all(|p| {
let v = **p - pa;
v.dot(normal).abs() < 0.30 * len
&& v.dot(axis) > -0.10 * len
&& v.dot(axis) < 1.10 * len
})
})
.collect();
if lines.len() != 2 {
continue; }
hit = true;
for (f, t) in lines {
let lm = (f + t) * 0.5;
if (lm - mid).dot(normal).abs() < 0.02 * len {
continue; }
let ok = mine.iter().any(|r| {
let poly: Vec<Point2> = r.atoms.iter().map(|a| pts[*a as usize]).collect();
point_in_polygon(lm, &poly)
});
if !ok {
return (
"环内双键",
true,
Some(format!("键 {bi}({}–{})有一条线画在环外", b.begin, b.end)),
);
}
}
}
("环内双键", hit, None)
}
fn wedges_reach_canvas(d: &omgkit_depict::Depiction, s: &Scene) -> Check {
let recorded = d.wedges.iter().filter(|w| w.narrow().is_some()).count();
let drawn = s
.items
.iter()
.filter(|it| matches!(it, Primitive::Wedge { .. } | Primitive::Hash { .. }))
.count();
if recorded != drawn {
return (
"楔形落地",
true,
Some(format!("记了 {recorded} 个,画出来 {drawn} 个")),
);
}
("楔形落地", recorded > 0, None)
}
fn wedges_read_back(m: &MolBuilder, d: &omgkit_depict::Depiction) -> Check {
let genuine = omgkit_io::stereo::genuine_tetrahedral(m);
let mut hit = false;
for (i, a) in m.atoms().iter().enumerate() {
let at = u32::try_from(i).expect("原子数超出 u32");
if !genuine[i]
|| !matches!(
a.chiral_tag,
omgkit_core::ChiralTag::Cw | omgkit_core::ChiralTag::Ccw
)
|| d.unwedged.contains(&at)
{
continue;
}
hit = true;
let got = omgkit_depict::stereo::read_chirality(m, &d.coords, &d.wedges, at);
if got != Some(a.chiral_tag) {
return (
"楔形可读",
true,
Some(format!(
"中心 {at} 画出来了,反读是 {got:?},该是 {:?}",
a.chiral_tag
)),
);
}
}
("楔形可读", hit, None)
}
fn bond_lengths_equal(m: &MolBuilder, d: &omgkit_depict::Depiction, clean: bool) -> Check {
if !clean || m.num_bonds() == 0 {
return ("键长全等", false, None);
}
let first = d.coords[m.bonds()[0].begin as usize].dist(d.coords[m.bonds()[0].end as usize]);
for (bi, b) in m.bonds().iter().enumerate() {
let l = d.coords[b.begin as usize].dist(d.coords[b.end as usize]);
if (l - first).abs() > 1e-6 {
return (
"键长全等",
true,
Some(format!("键 {bi} 长 {l:.4},第一根长 {first:.4}")),
);
}
}
("键长全等", true, None)
}
fn inside_canvas(m: &MolBuilder, d: &omgkit_depict::Depiction, s: &Scene, style: &Style) -> Check {
for it in &s.items {
let pts: Vec<(Point2, f64, f64)> = match it {
Primitive::Line { from, to, width } => {
vec![
(*from, *width / 2.0, *width / 2.0),
(*to, *width / 2.0, *width / 2.0),
]
}
Primitive::Wedge { from, to, wide } | Primitive::Hash { from, to, wide, .. } => {
vec![
(*from, *wide / 2.0, *wide / 2.0),
(*to, *wide / 2.0, *wide / 2.0),
]
}
Primitive::Text { .. } => continue, Primitive::Ball { .. } | Primitive::Stick { .. } => {
unreachable!("二维审计收到了三维图元 —— 场景拿错了")
}
};
for (p, rx, ry) in pts {
if p.x - rx < -0.01
|| p.x + rx > s.width + 0.01
|| p.y - ry < -0.01
|| p.y + ry > s.height
{
return (
"不出画布",
true,
Some(format!(
"图元在 ({:.2},{:.2})±({rx:.2},{ry:.2}),画布 {:.2}×{:.2}",
p.x, p.y, s.width, s.height
)),
);
}
}
}
let scale = style.bond_length_pt;
let pts = canvas_pts(m, d, style);
for a in 0..u32::try_from(m.num_atoms()).expect("原子数超出 u32") {
let Some(l) = label_at(m, a, style, &d.coords) else {
continue;
};
let c = pts[a as usize] + l.offset_canvas() * scale;
let (rx, ry) = (l.half_w * scale, l.half_h * scale);
if c.x - rx < -0.01 || c.x + rx > s.width + 0.01 || c.y - ry < -0.01 || c.y + ry > s.height
{
return (
"不出画布",
true,
Some(format!(
"标签 {} 在 ({:.2},{:.2})±({rx:.2},{ry:.2}),画布 {:.2}×{:.2}",
l.plain(),
c.x,
c.y,
s.width,
s.height
)),
);
}
}
("不出画布", true, None)
}
fn lines_clear_of_labels(
m: &MolBuilder,
d: &omgkit_depict::Depiction,
s: &Scene,
style: &Style,
) -> Check {
use omgkit_core::BondOrder;
use omgkit_depict::render::drawn_orders;
let scale = style.bond_length_pt;
let pts = canvas_pts(m, d, style);
let labels = labels_of(m, d, style);
let tight = squeezed(m, d, style, &labels);
let orders = drawn_orders(m);
let mut it = s.items.iter();
let mut compared = 0usize;
for (bi, b) in m.bonds().iter().enumerate() {
let n = match orders[bi] {
BondOrder::Double => 2,
BondOrder::Triple => 3,
_ => 1,
};
let mine: Vec<&Primitive> = (0..n).filter_map(|_| it.next()).collect();
if tight[bi] {
continue;
}
for a in [b.begin, b.end] {
let Some(l) = &labels[a as usize] else {
continue;
};
for p in mine.iter().filter_map(|x| match x {
Primitive::Line { from, to, .. } => Some([*from, *to]),
_ => None,
}) {
for p in p {
compared += 1;
if touches_glyphs(l, pts[a as usize], p, scale, style.line_width_pt) {
return (
"线端不压字",
true,
Some(format!(
"键 {bi} 的线端点 ({:.2},{:.2}) 压在原子 {a} 的标签 {} 的字上",
p.x,
p.y,
l.plain(),
)),
);
}
}
}
}
}
assert!(
it.all(|x| matches!(x, Primitive::Text { .. })),
"图元与键对不上号:按键消耗完之后剩下的不全是标签"
);
("线端不压字", compared > 0, None)
}
fn canvas_pts(m: &MolBuilder, d: &omgkit_depict::Depiction, style: &Style) -> Vec<Point2> {
let bnd = omgkit_depict::render::bounds(&d.coords, m, style);
d.coords
.iter()
.map(|p| omgkit_depict::render::to_canvas(*p, bnd, style.bond_length_pt))
.collect()
}