#[cfg(test)]
#[path = "analytic_instance_tests.rs"]
mod tests;
use crate::{CoreError, SourceRows};
use molgfx_math::{Aabb, Quat, Rgba8, Vec3};
use std::sync::Arc;
const EPSILON: f32 = 1.0e-6;
const PARALLEL_BOUNDS_THRESHOLD: usize = molgfx_math::parallel::BLOCK * 4;
#[repr(C)]
#[derive(Clone, Copy, PartialEq, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct AnalyticSphere {
pub center: [f32; 3],
pub radius: f32,
}
#[repr(C)]
#[derive(Clone, Copy, PartialEq, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct AnalyticCapsule {
pub start_radius: [f32; 4],
pub end_reserved: [f32; 4],
}
impl AnalyticCapsule {
pub fn new(start: Vec3, end: Vec3, radius: f32) -> Result<Self, CoreError> {
if !start.is_finite()
|| !end.is_finite()
|| !radius.is_finite()
|| radius <= 0.0
|| (end - start).length_squared() <= EPSILON * EPSILON
{
return Err(invalid(
"analytic capsule must be finite and non-degenerate",
));
}
Ok(Self {
start_radius: [start.x, start.y, start.z, radius],
end_reserved: [end.x, end.y, end.z, 0.0],
})
}
#[must_use]
pub fn start(self) -> Vec3 {
Vec3::new(
self.start_radius[0],
self.start_radius[1],
self.start_radius[2],
)
}
#[must_use]
pub fn end(self) -> Vec3 {
Vec3::new(
self.end_reserved[0],
self.end_reserved[1],
self.end_reserved[2],
)
}
#[must_use]
pub const fn radius(self) -> f32 {
self.start_radius[3]
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct AnalyticTemplate {
spheres: Arc<[AnalyticSphere]>,
capsules: Arc<[AnalyticCapsule]>,
source_rows: SourceRows,
bound_radius: f32,
}
impl AnalyticTemplate {
pub fn new(
spheres: Arc<[AnalyticSphere]>,
capsules: Arc<[AnalyticCapsule]>,
source_rows: SourceRows,
) -> Result<Self, CoreError> {
let part_count = spheres.len().saturating_add(capsules.len());
if part_count == 0 || part_count != source_rows.len() as usize {
return Err(invalid(
"analytic template parts must be non-empty and match source rows",
));
}
if spheres.iter().any(|sphere| {
!sphere.center.iter().all(|value| value.is_finite())
|| !sphere.radius.is_finite()
|| sphere.radius <= 0.0
}) || capsules.iter().any(|capsule| {
!capsule.start().is_finite()
|| !capsule.end().is_finite()
|| !capsule.radius().is_finite()
|| capsule.radius() <= 0.0
}) {
return Err(invalid(
"analytic template parts must be finite and positive",
));
}
let bound_radius = template_bound_radius(&spheres, &capsules);
Ok(Self {
spheres,
capsules,
source_rows,
bound_radius,
})
}
#[must_use]
pub const fn spheres(&self) -> &Arc<[AnalyticSphere]> {
&self.spheres
}
#[must_use]
pub const fn capsules(&self) -> &Arc<[AnalyticCapsule]> {
&self.capsules
}
#[must_use]
pub const fn source_rows(&self) -> &SourceRows {
&self.source_rows
}
#[must_use]
pub fn part_count(&self) -> usize {
self.spheres.len().saturating_add(self.capsules.len())
}
#[must_use]
pub const fn bound_radius(&self) -> f32 {
self.bound_radius
}
}
#[repr(C, align(16))]
#[derive(Clone, Copy, PartialEq, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct RigidInstance {
pub translation_scale: [f32; 4],
pub orientation: [f32; 4],
}
impl RigidInstance {
pub fn new(translation: Vec3, orientation: Quat, scale: f32) -> Result<Self, CoreError> {
let norm = orientation.length_squared();
if !translation.is_finite()
|| !orientation.is_finite()
|| !scale.is_finite()
|| scale <= 0.0
|| !norm.is_finite()
|| norm <= EPSILON
{
return Err(invalid(
"rigid instance transform must be finite and invertible",
));
}
Ok(Self {
translation_scale: [translation.x, translation.y, translation.z, scale],
orientation: orientation.normalize().to_array(),
})
}
#[must_use]
pub fn translation(self) -> Vec3 {
Vec3::new(
self.translation_scale[0],
self.translation_scale[1],
self.translation_scale[2],
)
}
#[must_use]
pub const fn scale(self) -> f32 {
self.translation_scale[3]
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct InstanceStyle {
pub color: Rgba8,
}
impl Default for InstanceStyle {
fn default() -> Self {
Self {
color: Rgba8::WHITE,
}
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct InstanceBatch {
template: Arc<AnalyticTemplate>,
transforms: Arc<[RigidInstance]>,
source_rows: SourceRows,
style: InstanceStyle,
bounds: Aabb,
visible: bool,
}
impl InstanceBatch {
pub fn new(
template: Arc<AnalyticTemplate>,
transforms: Arc<[RigidInstance]>,
source_rows: SourceRows,
) -> Result<Self, CoreError> {
if transforms.is_empty() || transforms.len() != source_rows.len() as usize {
return Err(invalid(
"instance transforms must be non-empty and match source rows",
));
}
let occurrences = transforms
.len()
.checked_mul(template.part_count())
.ok_or_else(|| invalid("instance-part occurrences exceed picking capacity"))?;
if occurrences > crate::EntityId::MAX_INDEX as usize + 1 {
return Err(invalid("instance-part occurrences exceed picking capacity"));
}
let bounds = instance_bounds(&transforms, template.bound_radius());
if bounds.is_empty() {
return Err(invalid("instance bounds must be finite"));
}
Ok(Self {
template,
transforms,
source_rows,
style: InstanceStyle::default(),
bounds,
visible: true,
})
}
#[must_use]
pub const fn template(&self) -> &Arc<AnalyticTemplate> {
&self.template
}
#[must_use]
pub const fn transforms(&self) -> &Arc<[RigidInstance]> {
&self.transforms
}
#[must_use]
pub const fn source_rows(&self) -> &SourceRows {
&self.source_rows
}
#[must_use]
pub const fn with_style(mut self, style: InstanceStyle) -> Self {
self.style = style;
self
}
#[must_use]
pub const fn style(&self) -> InstanceStyle {
self.style
}
#[must_use]
pub const fn bounds(&self) -> Aabb {
self.bounds
}
#[must_use]
pub const fn visible(&self) -> bool {
self.visible
}
pub(crate) fn set_visible(&mut self, visible: bool) -> bool {
if self.visible == visible {
return false;
}
self.visible = visible;
true
}
}
fn template_bound_radius(spheres: &[AnalyticSphere], capsules: &[AnalyticCapsule]) -> f32 {
let sphere_bound = spheres
.iter()
.map(|sphere| Vec3::from(sphere.center).length() + sphere.radius)
.fold(0.0f32, f32::max);
capsules.iter().fold(sphere_bound, |bound, capsule| {
bound.max(capsule.start().length().max(capsule.end().length()) + capsule.radius())
})
}
fn instance_bounds(transforms: &[RigidInstance], template_radius: f32) -> Aabb {
molgfx_math::parallel::reduce_blocks(
transforms,
PARALLEL_BOUNDS_THRESHOLD,
Aabb::EMPTY,
|block| {
let mut bound = Aabb::EMPTY;
for transform in block {
bound.extend_sphere(transform.translation(), template_radius * transform.scale());
}
bound
},
|left, right| left.union(&right),
)
}
const fn invalid(reason: &'static str) -> CoreError {
CoreError::InvalidBatch { reason }
}