molgfx-render 0.2.0

The render graph, passes and the engine that drives a frame.
Documentation
use super::*;

fn resolved(result: Result<Aabb, RenderError>) -> Aabb {
    result.unwrap_or_else(|error| panic!("{error}"))
}

fn rendered_bounds(placed: &PlacedStructure) -> Aabb {
    placed
        .render_bvh()
        .unwrap_or_else(|error| panic!("{error}"))
        .bounds()
}
use molgfx_core::{Scene, StructureHandle, TrajectoryFrame, TrajectorySegment};
use std::sync::Arc;

fn trajectory_scene() -> (Scene, StructureHandle) {
    let source = "data_bounds\nloop_\n_atom_site.group_PDB\n_atom_site.id\n_atom_site.type_symbol\n_atom_site.label_atom_id\n_atom_site.label_comp_id\n_atom_site.label_asym_id\n_atom_site.label_seq_id\n_atom_site.Cartn_x\n_atom_site.Cartn_y\n_atom_site.Cartn_z\nATOM 1 C CA ALA A 1 0 0 0\nATOM 2 C CA GLY A 2 100 0 0\n";
    let parsed = molframe::read_bytes(
        source.as_bytes().to_vec(),
        Some("trajectory-bounds.cif"),
        &molframe::ReadOptions::new(),
    );
    let (structure, _) = match parsed {
        Ok(value) => value,
        Err(diagnostics) => panic!("trajectory bounds fixture parses: {diagnostics:?}"),
    };
    let mut scene = Scene::new();
    let handle = match scene.add_structure(&structure) {
        Ok(handle) => handle,
        Err(error) => panic!("trajectory bounds fixture is placed: {error}"),
    };
    (scene, handle)
}

fn trajectory_segment(start_x: f32, end_x: f32) -> TrajectorySegment {
    let start_positions: Arc<[[f32; 3]]> = Arc::from([[start_x, 0.0, 0.0], [100.0, 0.0, 0.0]]);
    let end_positions: Arc<[[f32; 3]]> = Arc::from([[end_x, 0.0, 0.0], [100.0, 0.0, 0.0]]);
    let start = match TrajectoryFrame::new(1, 0.0, start_positions, "bounds:start") {
        Ok(frame) => frame,
        Err(error) => panic!("start frame is valid: {error}"),
    };
    let end = match TrajectoryFrame::new(2, 1.0, end_positions, "bounds:end") {
        Ok(frame) => frame,
        Err(error) => panic!("end frame is valid: {error}"),
    };
    match TrajectorySegment::new(start, end, 0.25) {
        Ok(segment) => segment,
        Err(error) => panic!("trajectory segment is valid: {error}"),
    }
}

#[test]
fn a_sparse_surface_selection_uses_only_its_atom_bounds() {
    let source = "data_bounds\nloop_\n_atom_site.group_PDB\n_atom_site.id\n_atom_site.type_symbol\n_atom_site.label_atom_id\n_atom_site.label_comp_id\n_atom_site.label_asym_id\n_atom_site.label_seq_id\n_atom_site.Cartn_x\n_atom_site.Cartn_y\n_atom_site.Cartn_z\nATOM 1 C CA ALA A 1 0 0 0\nATOM 2 C CA GLY A 2 100 0 0\n";
    let parsed = molframe::read_bytes(
        source.as_bytes().to_vec(),
        Some("bounds.cif"),
        &molframe::ReadOptions::new(),
    );
    let (structure, _) = match parsed {
        Ok(value) => value,
        Err(diagnostics) => panic!("bounds fixture parses: {diagnostics:?}"),
    };
    let Some(placed) = PlacedStructure::new(&structure) else {
        panic!("bounds fixture has coordinates")
    };
    let selected = resolved(selected_atom_bounds(
        &placed,
        &AtomSelection::Sparse(vec![0]),
        1.0,
    ));
    assert!(selected.max.x < 10.0);
    assert!(rendered_bounds(&placed).max.x > 90.0);
}

#[test]
fn selected_bounds_include_the_rendered_radius_scale() {
    let source = "data_bounds\nloop_\n_atom_site.group_PDB\n_atom_site.id\n_atom_site.type_symbol\n_atom_site.label_atom_id\n_atom_site.label_comp_id\n_atom_site.label_asym_id\n_atom_site.label_seq_id\n_atom_site.Cartn_x\n_atom_site.Cartn_y\n_atom_site.Cartn_z\nATOM 1 C CA ALA A 1 0 0 0\n";
    let parsed = molframe::read_bytes(
        source.as_bytes().to_vec(),
        Some("scaled-bounds.cif"),
        &molframe::ReadOptions::new(),
    );
    let (structure, _) = match parsed {
        Ok(value) => value,
        Err(diagnostics) => panic!("scaled bounds fixture parses: {diagnostics:?}"),
    };
    let Some(placed) = PlacedStructure::new(&structure) else {
        panic!("scaled bounds fixture has coordinates")
    };
    let radius = placed.atoms.radius().values()[0];

    let selected = resolved(selected_atom_bounds(&placed, &AtomSelection::All, 3.0));

    assert!((selected.max.x - radius * 3.0).abs() < f32::EPSILON);
    assert!((selected.min.x + radius * 3.0).abs() < f32::EPSILON);
}

#[test]
fn trajectory_bounds_include_both_resident_frame_endpoints() {
    let (mut scene, handle) = trajectory_scene();
    if let Err(error) = scene.set_trajectory_segment(handle, trajectory_segment(-20.0, 30.0)) {
        panic!("trajectory segment is installed: {error}");
    }
    let Some(placed) = scene.structure(handle) else {
        panic!("placed structure remains resident")
    };
    let radius = placed.atoms.radius().values()[0] * 2.0;

    let selected = resolved(selected_atom_bounds(
        placed,
        &AtomSelection::Sparse(vec![0]),
        2.0,
    ));

    assert!((selected.min.x - (-20.0 - radius)).abs() < f32::EPSILON);
    assert!((selected.max.x - (30.0 + radius)).abs() < f32::EPSILON);
    assert!(selected.max.x < 100.0);
}

#[test]
fn a_time_only_trajectory_update_reuses_the_surface_bounds_state() {
    let (mut scene, handle) = trajectory_scene();
    if let Err(error) = scene.set_trajectory_segment(handle, trajectory_segment(-20.0, 30.0)) {
        panic!("trajectory segment is installed: {error}");
    }
    let selection = scene.add_selection(AtomSelection::All);
    let representation = Representation::new(
        RepresentationTarget::Selection(selection),
        RepresentationKind::Surface,
    );
    let mut cache = SelectionBoundsCache::new();
    let Some(placed) = scene.structure(handle) else {
        panic!("placed structure remains resident")
    };
    let revision = placed.trajectory_revision();
    let before_state = SelectionBoundsState::new(placed, &representation);
    let before_bounds = resolved(cache.resolve(placed, &representation, &AtomSelection::All));

    if let Err(error) = scene.set_trajectory_time(handle, 0.75) {
        panic!("trajectory time advances: {error}");
    }
    let Some(placed) = scene.structure(handle) else {
        panic!("placed structure remains resident")
    };
    let after_state = SelectionBoundsState::new(placed, &representation);
    let after_bounds = resolved(cache.resolve(placed, &representation, &AtomSelection::All));

    assert!(placed.trajectory_revision() > revision);
    assert_eq!(after_state, before_state);
    assert_eq!(after_bounds, before_bounds);
}

#[test]
fn visual_displacement_expands_cached_culling_bounds() {
    let (mut scene, handle) = trajectory_scene();
    let selection = AtomSelection::All;
    let mut builder = molgfx_core::VisualProgramBuilder::new();
    let offset = match builder.vector([2.0, 0.0, 0.0]) {
        Ok(value) => value,
        Err(error) => panic!("offset is finite: {error}"),
    };
    if let Err(error) = builder.set_position_offset(offset, 2.0) {
        panic!("offset bound is valid: {error}");
    }
    let program = match builder.finish() {
        Ok(value) => value,
        Err(error) => panic!("program is valid: {error}"),
    };
    let mut representation = Representation::new(
        RepresentationTarget::Selection(scene.add_selection(selection.clone())),
        RepresentationKind::Spacefill,
    );
    representation.visual = Some(molgfx_core::VisualStyle::new(program));
    let Some(placed) = scene.structure(handle) else {
        panic!("structure remains resident")
    };
    let base = rendered_bounds(placed);
    let expanded =
        resolved(SelectionBoundsCache::new().resolve(placed, &representation, &selection));
    assert_eq!(expanded.min, base.min - Vec3::splat(2.0));
    assert_eq!(expanded.max, base.max + Vec3::splat(2.0));
}