molgfx-render 0.3.2

The render graph, passes and the engine that drives a frame.
Documentation
//! Deterministic analytic measurement-guide generation.

use super::label_types::{LabelGpu, line_record};
use molgfx_core::{AnnotationAnchor, Measurement, MeasurementKind};
use molgfx_math::{Rgba8, Vec3};

const GUIDE_SEGMENTS: u16 = 20;

#[derive(Clone, Copy)]
struct ArcStyle {
    color: Rgba8,
    entity: u32,
    structure: u32,
}

pub(super) fn measurement_guides(
    output: &mut Vec<LabelGpu>,
    value: &Measurement,
    entity: u32,
    structure: u32,
) -> Vec3 {
    let anchors = value.anchors();
    let color = Rgba8::opaque(120, 211, 225);
    match value.kind() {
        MeasurementKind::Distance => {
            output.push(line_record(
                anchors[0].position(),
                anchors[1].position(),
                1.4,
                color,
                entity,
                structure,
            ));
            anchors[0].position().lerp(anchors[1].position(), 0.5)
        }
        MeasurementKind::Angle => angle_guides(output, anchors, color, entity, structure),
        MeasurementKind::Dihedral => dihedral_guides(output, anchors, color, entity, structure),
    }
}

fn angle_guides(
    output: &mut Vec<LabelGpu>,
    anchors: &[AnnotationAnchor],
    color: Rgba8,
    entity: u32,
    structure: u32,
) -> Vec3 {
    let center = anchors[1].position();
    let left = anchors[0].position() - center;
    let right = anchors[2].position() - center;
    let radius = left.length().min(right.length()) * 0.34;
    let from = left.normalize_or_zero();
    let to = right.normalize_or_zero();
    let axis = from.cross(to).normalize_or_zero();
    let angle = from.dot(to).clamp(-1.0, 1.0).acos();
    arc(
        output,
        center,
        from,
        axis,
        angle,
        radius,
        ArcStyle {
            color,
            entity,
            structure,
        },
    )
}

fn dihedral_guides(
    output: &mut Vec<LabelGpu>,
    anchors: &[AnnotationAnchor],
    color: Rgba8,
    entity: u32,
    structure: u32,
) -> Vec3 {
    let [a, b, c, d] = [
        anchors[0].position(),
        anchors[1].position(),
        anchors[2].position(),
        anchors[3].position(),
    ];
    for (start, end) in [(a, b), (b, c), (c, d)] {
        output.push(line_record(start, end, 1.0, color, entity, structure));
    }
    let axis = (c - b).normalize_or_zero();
    let from = (a - b).reject_from_normalized(axis).normalize_or_zero();
    let to = (d - c).reject_from_normalized(axis).normalize_or_zero();
    let angle = axis.dot(from.cross(to)).atan2(from.dot(to));
    let center = b.lerp(c, 0.5);
    let radius = (a.distance(b).min(d.distance(c)) * 0.32).max(0.2);
    arc(
        output,
        center,
        from,
        axis,
        angle,
        radius,
        ArcStyle {
            color,
            entity,
            structure,
        },
    )
}

fn arc(
    output: &mut Vec<LabelGpu>,
    center: Vec3,
    from: Vec3,
    axis: Vec3,
    angle: f32,
    radius: f32,
    style: ArcStyle,
) -> Vec3 {
    let mut previous = center + from * radius;
    let mut midpoint = previous;
    for step in 1..=GUIDE_SEGMENTS {
        let fraction = f32::from(step) / f32::from(GUIDE_SEGMENTS);
        let current = center + rotate_axis(from, axis, angle * fraction) * radius;
        output.push(line_record(
            previous,
            current,
            1.3,
            style.color,
            style.entity,
            style.structure,
        ));
        if step == GUIDE_SEGMENTS / 2 {
            midpoint = current;
        }
        previous = current;
    }
    midpoint
}

fn rotate_axis(vector: Vec3, axis: Vec3, angle: f32) -> Vec3 {
    if axis.length_squared() < 0.5 {
        return vector;
    }
    let cosine = angle.cos();
    vector * cosine + axis.cross(vector) * angle.sin() + axis * axis.dot(vector) * (1.0 - cosine)
}