Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Dynamic Structure Updates

Viso is designed for live manipulation — structures can be updated mid-session by computational backends (Rosetta energy minimization, ML structure prediction) or user actions (mutations, drag operations).

All structural mutations happen on your molex::Assembly. After each batch of changes, push the new snapshot to the engine via engine.set_assembly(Arc::new(assembly.clone())). The engine itself is read-only with respect to structural state.

Per-Entity Coordinate Updates

To stream new atom positions for an existing entity, mutate the relevant entity’s coordinates on your Assembly (using molex’s update_protein_entities codec helper, or assembly.update_positions(eid, &coords) for direct position updates), then re-publish:

#![allow(unused)]
fn main() {
use std::sync::Arc;
use molex::ops::codec::update_protein_entities;

// Apply caller-provided Coords through molex's shared codec so the
// path matches the byte-format pipeline.
let mut entities = vec![assembly.entity(eid).unwrap().clone()];
update_protein_entities(&mut entities, &new_coords);
if let Some(updated) = entities.into_iter().next() {
    assembly.remove_entity(eid);
    assembly.add_entity(updated);
}

engine.set_assembly(Arc::new(assembly.clone()));
}

To make the next sync animate (instead of snapping), queue a per-entity behavior override before re-publishing:

#![allow(unused)]
fn main() {
engine.set_entity_behavior(entity_id, Transition::smooth());
engine.set_assembly(Arc::new(assembly.clone()));
}

The engine queues the transition for the affected entity on its next sync, regardless of whether the override was set before or after the set_assembly call (transitions are picked up in update).

Per-Entity Behavior Overrides

Override the default transition for a specific entity. Once set, every subsequent sync involving that entity uses the override (until cleared):

#![allow(unused)]
fn main() {
let eid = engine.entity_id(raw_id).expect("known entity");

engine.set_entity_behavior(eid, Transition::collapse_expand(
    Duration::from_millis(200),
    Duration::from_millis(300),
));

// Subsequent re-publishes that touch this entity will use
// collapse_expand instead of the default snap.
engine.set_assembly(Arc::new(assembly.clone()));

// Revert to default:
engine.clear_entity_behavior(eid);
}

Transitions

Every update can specify a Transition controlling the visual animation:

#![allow(unused)]
fn main() {
// Instant snap (no animation; used internally for initial loads
// and trajectory frames).
Transition::snap()

// Standard smooth interpolation (300ms cubic-hermite ease-out).
Transition::smooth()

// Two-phase: sidechains collapse to CA, then expand. For mutations.
Transition::collapse_expand(
    Duration::from_millis(300),
    Duration::from_millis(300),
)

// Two-phase: backbone moves first with sidechains hidden, then
// sidechains expand into place.
Transition::backbone_then_expand(
    Duration::from_millis(400),
    Duration::from_millis(600),
)

// Builder flags
Transition::collapse_expand(
    Duration::from_millis(200),
    Duration::from_millis(300),
)
    .allowing_size_change()
    .suppressing_initial_sidechains()
}

See Animation System for details on the data-driven phase model.

Preemption

If a new update arrives while an animation is playing, the current visual position becomes the new animation’s start state and the timer resets. This provides responsive feedback during rapid update cycles (e.g. Rosetta wiggle).

Constraint Visualization (Bands and Pulls)

Bands and pulls are not commands — they are stored constraint specs that the engine resolves to world-space positions every frame so they auto-track animated atoms.

Bands

A BandInfo references atoms structurally rather than by world-space position:

#![allow(unused)]
fn main() {
use viso::{AtomRef, BandInfo, BandTarget, BandType};

let band = BandInfo {
    anchor_a: AtomRef { residue: 42, atom_name: "CA".into() },
    anchor_b: BandTarget::Atom(AtomRef {
        residue: 87,
        atom_name: "CA".into(),
    }),
    strength: 1.0,
    target_length: 3.5,
    band_type: Some(BandType::Disulfide),
    is_pull: false,
    is_push: false,
    is_disabled: false,
    from_script: false,
};
}

BandTarget::Position(Vec3) anchors one end to a fixed world-space point (used for “space pulls”). band_type set to None lets the engine auto-detect the type from target_length.

Visual properties:

  • Radius scales with strength (0.1 to 0.4 Å)
  • Color depends on band_type: default (purple), backbone (yellow-orange), disulfide (yellow-green), H-bond (cyan)
  • Disabled bands are gray
  • Script-authored bands (from_script: true) render dimmer

Pulls

A PullInfo is a single active drag constraint. The atom is referenced structurally; the target is given in screen-space (physical pixels) and unprojected at the atom’s depth each frame so the drag stays parallel to the camera plane:

#![allow(unused)]
fn main() {
use viso::{AtomRef, PullInfo};

let pull = PullInfo {
    atom: AtomRef { residue: 42, atom_name: "CA".into() },
    screen_target: (mouse_x, mouse_y),
};
}

Pulls render as a purple cylinder from the atom to the target with a cone arrow head at the target end.

Update band and pull specs through the engine. Both methods replace the previous specs and re-resolve immediately:

#![allow(unused)]
fn main() {
engine.update_bands(vec![band1, band2]);
engine.update_pull(Some(pull));
engine.update_pull(None); // clear when drag ends
}

The engine resolves stored specs to world-space positions every frame, so bands and pulls track animated atoms automatically.