Scene Management
Viso’s structural state lives in molex::Assembly, which is owned by
your application — not by viso. Viso is a pure consumer: you push
the latest Arc<Assembly> to the engine via
[VisoEngine::set_assembly], and the engine drains the snapshot on
the next sync tick and rederives its render state.
There is no “group” abstraction in viso — every entity lives directly
in the Assembly and is identified by an opaque EntityId.
Pushing Assembly Snapshots
┌──────────────────────┐ ┌─────────────────┐
│ your application │ Arc<Assembly> │ VisoEngine │
│ │ ─engine.set_assembly──► │ │
│ molex::Assembly │ │ pending slot │
│ (mutated freely) │ │ → Scene+derived│
└──────────────────────┘ └─────────────────┘
- Your application mutates its
molex::Assembly(using molex’s APIs) and callsengine.set_assembly(Arc::new(self.assembly.clone())). VisoEngine::update(dt)drains the pending snapshot; if its generation differs from the last applied one, the engine rederives its per-entity state and submits a full-rebuild request to the background mesh processor.
That’s the entire structural ingest contract for library users. There is no viso-defined channel, publisher, or consumer in the public API.
Mutating the Scene
You mutate molex::Assembly through molex’s own APIs and re-publish
to viso after each batch of changes:
#![allow(unused)]
fn main() {
use std::sync::Arc;
use molex::Assembly;
use viso::{Transition, VisoEngine};
// 1. Mutate the assembly however you like.
let mut assembly = /* your owned Assembly */;
assembly.add_entity(new_entity);
assembly.update_positions(eid, &new_coords);
// ... add/remove/update as needed ...
// 2. Push the new snapshot. Cheap: Arc<Assembly> is shared
// by reference.
engine.set_assembly(Arc::new(assembly.clone()));
// 3. (Optional) For entities whose positions changed, queue a
// per-entity transition so the next sync animates instead of
// snapping. Without this, the engine snaps to the new state.
engine.set_entity_behavior(entity_id, Transition::smooth());
}
The next engine.update(dt) drains the pending snapshot, rederives
the scene, and submits a full-rebuild to the background mesh
processor. Mesh generation happens off-thread, so the main thread is
not blocked.
Note. If you’re embedding viso as a library, ignore
VisoAppentirely.VisoAppis the standalone-app helper that viso uses to be its own host when run viacargo run -p viso(or theviewer/gui/webfeatures). Library consumers own their ownAssemblyand don’t need or want the convenience wrapper.
Engine-Side Annotations
Some per-entity state is purely a viso concern (it doesn’t belong on
the molecular structure itself). Those live on
EntityAnnotations, mutated through engine methods:
#![allow(unused)]
fn main() {
// Animation behavior overrides (keyed by EntityId, not raw u32).
let eid = engine.entity_id(raw_id).expect("known entity");
engine.set_entity_behavior(eid, Transition::collapse_expand(/* ... */));
engine.clear_entity_behavior(eid);
// Per-entity appearance overrides (drawing mode, color scheme,
// helix/sheet style, surface kind, palette, etc.).
let mut overrides = DisplayOverrides::default();
overrides.color_scheme = Some(ColorScheme::SecondaryStructure);
engine.set_entity_appearance(eid, overrides);
engine.clear_entity_appearance(eid);
}
set_entity_appearance diffs against the previous overrides and
dispatches only the invalidations that matter — a surface_kind
change triggers surface regeneration, a color_scheme change triggers
color recomputation, and so on.
Looking Up Entities
The engine exposes a small read-only surface for looking entities up:
#![allow(unused)]
fn main() {
// Translate a raw u32 (from IPC, TOML, CLI) to an opaque EntityId.
let eid: Option<EntityId> = engine.entity_id(raw_id);
// Walk the current Assembly snapshot directly.
for entity in engine.assembly().entities() {
println!("{:?}: {}", entity.id(), entity.molecule_type());
}
// Total entity count.
let n = engine.entity_count();
}
entity_id is the canonical “boundary translator” — wire formats
carry raw u32 ids; viso-internal APIs use EntityId. Translate
once at the boundary and pass EntityId through.
Focus
Focus determines what the camera follows and what the user is
“working on”. It cycles through entities with Tab:
#![allow(unused)]
fn main() {
pub enum Focus {
Session, // All visible entities (default)
Entity(EntityId), // A specific entity
}
}
#![allow(unused)]
fn main() {
// Cycle: Session → Entity₁ → … → EntityN → Session
engine.execute(VisoCommand::CycleFocus);
// Focus a specific entity by raw id.
engine.execute(VisoCommand::FocusEntity { id });
// Reset to session-wide focus.
engine.execute(VisoCommand::ResetFocus);
// Read current focus state.
let focus: Focus = engine.focus();
let focused_entity: Option<EntityId> = engine.focused_entity();
}
What Happens During Sync
When a new Assembly snapshot arrives:
- Rederive per-entity state. For each entity, the engine builds render-ready derived data (backbone chains, sidechain topology, SS types, residue color metadata).
- Submit a
FullRebuild. The background processor receives aVec<FullRebuildEntity>plus the active display, color, and geometry options. Per-entity mesh caching means only entities whosemesh_versionchanged are regenerated. - Triple-buffer the result. When the processor finishes, the
resulting
PreparedRebuildis written to a triple buffer. - Apply on the next frame.
engine.update(dt)callsapply_pending_scene, which uploads the GPU buffers in a memcpy and rebuilds picking bind groups.
The main thread never blocks. If the new meshes aren’t ready by the next frame, the previous frame’s data continues to render until they are.