use crate::RibbonVertex;
use molgfx_core::{AtomSelection, EntityId, EntityKind};
use molgfx_math::{Rgba8, Vec3};
#[cfg(test)]
#[path = "paper_chain_tests.rs"]
mod tests;
const PYRANOSE: [&str; 6] = ["O5", "C1", "C2", "C3", "C4", "C5"];
const FURANOSE: [&str; 5] = ["O4", "C1", "C2", "C3", "C4"];
const RIBOSE: [&str; 5] = ["O4'", "C1'", "C2'", "C3'", "C4'"];
const PYRIMIDINE: [&str; 6] = ["N1", "C2", "N3", "C4", "C5", "C6"];
const PURINE_SIX: [&str; 6] = ["N1", "C2", "N3", "C4", "C5", "C6"];
const PURINE_FIVE: [&str; 5] = ["C4", "C5", "N7", "C8", "N9"];
pub fn append_paper_chain(
structure: &molframe::Structure,
selection: &AtomSelection,
height: f32,
opacity: u8,
vertices: &mut Vec<RibbonVertex>,
indices: &mut Vec<u32>,
) -> Result<(), crate::PackingError> {
for residue in structure.residues() {
if residue.atom("O5").is_some() {
append_named_ring(
residue, &PYRANOSE, selection, height, opacity, vertices, indices,
)?;
} else if residue.atom("O4").is_some() && residue.atom("C1").is_some() {
append_named_ring(
residue, &FURANOSE, selection, height, opacity, vertices, indices,
)?;
}
append_named_ring(
residue, &RIBOSE, selection, height, opacity, vertices, indices,
)?;
if residue.atom("N9").is_some() {
append_named_ring(
residue,
&PURINE_SIX,
selection,
height,
opacity,
vertices,
indices,
)?;
append_named_ring(
residue,
&PURINE_FIVE,
selection,
height,
opacity,
vertices,
indices,
)?;
} else if residue.atom("N1").is_some() {
append_named_ring(
residue,
&PYRIMIDINE,
selection,
height,
opacity,
vertices,
indices,
)?;
}
}
Ok(())
}
fn append_named_ring(
residue: molframe::ResidueRef<'_>,
names: &[&str],
selection: &AtomSelection,
height: f32,
opacity: u8,
vertices: &mut Vec<RibbonVertex>,
indices: &mut Vec<u32>,
) -> Result<(), crate::PackingError> {
let mut ring = Vec::with_capacity(names.len());
let mut entity = None;
for name in names {
let Some(atom) = residue.atom(name) else {
return Ok(());
};
if !selection.contains(atom.index().get()) {
return Ok(());
}
let Some(position) = atom.position() else {
return Ok(());
};
ring.push(Vec3::from(position));
let entity_id = EntityId::pack(EntityKind::Atom, u64::from(atom.index().get()))?;
entity.get_or_insert(entity_id.0);
}
let Some(entity) = entity else {
return Ok(());
};
emit_plate(&ring, height, opacity, entity, vertices, indices);
Ok(())
}
fn emit_plate(
ring: &[Vec3],
height: f32,
opacity: u8,
entity: u32,
vertices: &mut Vec<RibbonVertex>,
indices: &mut Vec<u32>,
) {
if ring.len() < 3 {
return;
}
let count = u16::try_from(ring.len()).map_or(f32::from(u16::MAX), f32::from);
let centroid = ring.iter().copied().sum::<Vec3>() / count.max(1.0);
let Some(normal) = ring_normal(ring) else {
return;
};
let offset = normal * height.abs().max(0.02);
let color = pucker_color(pucker_amplitude(ring, centroid, normal), opacity);
for edge in 0..ring.len() {
let next = (edge + 1) % ring.len();
let (top, top_next) = (ring[edge] + offset, ring[next] + offset);
let (bottom, bottom_next) = (ring[edge] - offset, ring[next] - offset);
push_face(
[centroid + offset, top, top_next],
normal,
entity,
color,
vertices,
indices,
);
push_face(
[centroid - offset, bottom_next, bottom],
-normal,
entity,
color,
vertices,
indices,
);
push_triangle(top, bottom, bottom_next, entity, color, vertices, indices);
push_triangle(top, bottom_next, top_next, entity, color, vertices, indices);
}
}
fn ring_normal(ring: &[Vec3]) -> Option<Vec3> {
let mut normal = Vec3::ZERO;
for (index, current) in ring.iter().enumerate() {
normal += current.cross(ring[(index + 1) % ring.len()]);
}
normal.try_normalize()
}
fn pucker_amplitude(ring: &[Vec3], centroid: Vec3, normal: Vec3) -> f32 {
ring.iter()
.map(|point| ((*point - centroid).dot(normal)).powi(2))
.sum::<f32>()
.sqrt()
}
const PUCKER_STOPS: [(u16, [u8; 3]); 6] = [
(0, [220, 62, 55]),
(19, [240, 178, 48]),
(48, [74, 176, 96]),
(78, [54, 190, 194]),
(102, [65, 105, 225]),
(130, [184, 76, 190]),
];
fn pucker_color(amplitude: f32, opacity: u8) -> Rgba8 {
let scaled = centi_angstrom(amplitude);
let mut color = PUCKER_STOPS[PUCKER_STOPS.len() - 1].1;
for window in PUCKER_STOPS.windows(2) {
let [(low, start), (high, end)] = [window[0], window[1]];
if scaled >= high {
continue;
}
color = [
mix_channel(start[0], end[0], scaled - low, high - low),
mix_channel(start[1], end[1], scaled - low, high - low),
mix_channel(start[2], end[2], scaled - low, high - low),
];
break;
}
Rgba8::new(color[0], color[1], color[2], opacity)
}
fn mix_channel(start: u8, end: u8, amount: u16, span: u16) -> u8 {
let span = u32::from(span.max(1));
let amount = u32::from(amount).min(span);
let low = u32::from(start.min(end));
let step = (u32::from(start.max(end)) - low) * amount / span;
let value = if start <= end {
low + step
} else {
u32::from(start) - step
};
u8::try_from(value)
.into_iter()
.fold(u8::MAX, |_, byte| byte)
}
fn centi_angstrom(amplitude: f32) -> u16 {
molgfx_math::truncate_u16(amplitude * 100.0)
}
fn push_triangle(
a: Vec3,
b: Vec3,
c: Vec3,
entity_id: u32,
color: Rgba8,
vertices: &mut Vec<RibbonVertex>,
indices: &mut Vec<u32>,
) {
let Some(normal) = (b - a).cross(c - a).try_normalize() else {
return;
};
push_face([a, b, c], normal, entity_id, color, vertices, indices);
}
fn push_face(
corners: [Vec3; 3],
normal: Vec3,
entity_id: u32,
color: Rgba8,
vertices: &mut Vec<RibbonVertex>,
indices: &mut Vec<u32>,
) {
let Ok(base) = u32::try_from(vertices.len()) else {
return;
};
for position in corners {
vertices.push(RibbonVertex {
position: position.to_array(),
entity_id,
normal: normal.to_array(),
color,
});
}
indices.extend([base, base.saturating_add(1), base.saturating_add(2)]);
}