//! Physics-driven collision-volume offsets and positional record semantics. use crate::Error; use libremetaverse_types::compat::{Object, Vector3}; use std::collections::HashMap; use std::hash::{Hash, Hasher}; const PHYSICS_DRIVEN_PARAMS: [i32; 6] = [1200, 1201, 1204, 1205, 1206, 1207]; #[derive(Clone, Debug, PartialEq)] pub struct PhysicsVolumeMorphsVolumeMorph { bone_name: String, pos_delta: Vector3, } impl PhysicsVolumeMorphsVolumeMorph { pub fn new(bone_name: String, pos_delta: Vector3) -> Result { Ok(Self { bone_name, pos_delta, }) } pub fn deconstruct( &mut self, bone_name: &mut String, pos_delta: &mut Vector3, ) -> Result<(), Error> { *bone_name = self.bone_name.clone(); *pos_delta = self.pos_delta; Ok(()) } #[must_use] pub fn equals_with_volume_morph(&self, other: Self) -> bool { *self == other } #[must_use] pub fn equals_with_object(&self, object: Object) -> bool { object.downcast_ref::() == Some(self) } #[must_use] pub fn get_hash_code(&self) -> i32 { let mut hasher = std::collections::hash_map::DefaultHasher::new(); self.bone_name.hash(&mut hasher); for value in self.pos_delta.0 { value.to_bits().hash(&mut hasher); } hasher.finish() as i32 } #[must_use] #[allow(clippy::inherent_to_string_shadow_display)] pub fn to_string(&self) -> String { format!( "VolumeMorph {{ BoneName = {}, PosDelta = <{}, {}, {}> }}", self.bone_name, self.pos_delta.0[0], self.pos_delta.0[1], self.pos_delta.0[2] ) } #[must_use] pub fn eq(left: Self, right: Self) -> bool { left == right } #[must_use] pub fn ne(left: Self, right: Self) -> bool { left != right } #[must_use] pub fn bone_name(&self) -> String { self.bone_name.clone() } pub fn set_bone_name(&mut self, value: String) { self.bone_name = value; } #[must_use] pub fn pos_delta(&self) -> Vector3 { self.pos_delta } pub fn set_pos_delta(&mut self, value: Vector3) { self.pos_delta = value; } } pub struct PhysicsVolumeMorphs; impl PhysicsVolumeMorphs { pub fn compute_bone_offsets( driven_weights: HashMap, vp_weights: HashMap, ) -> Result, Error> { // Preserved for signature parity; the pinned implementation also uses // only the normalized driven output and the visual-param definition. let _ = vp_weights; let mut offsets: HashMap = HashMap::new(); for (param_id, normalized) in driven_weights { if !PHYSICS_DRIVEN_PARAMS.contains(¶m_id) { continue; } let Some(parameter) = crate::visual_catalog::param_by_id(param_id) else { continue; }; let Some(morphs) = parameter.volume_morphs.as_ref() else { continue; }; let local_value = parameter.min_value + (parameter.max_value - parameter.min_value) * normalized; for morph in morphs { let delta = Vector3([ morph.position_delta.x * local_value, morph.position_delta.y * local_value, morph.position_delta.z * local_value, ]); offsets .entry(morph.bone_name.clone()) .and_modify(|current| { for index in 0..3 { current.0[index] += delta.0[index]; } }) .or_insert(delta); } } Ok(offsets) } } #[cfg(test)] mod tests { use super::*; #[test] fn record_properties_equality_and_deconstruction_round_trip() { let delta = Vector3([0.0, 0.0, -0.01]); let mut morph = PhysicsVolumeMorphsVolumeMorph::new("LEFT_PEC".to_owned(), delta).unwrap(); let copy = morph.clone(); assert!(morph.equals_with_volume_morph(copy.clone())); assert!(morph.equals_with_object(Object::opaque(copy))); let mut name = String::new(); let mut output = Vector3::default(); morph.deconstruct(&mut name, &mut output).unwrap(); assert_eq!(name, "LEFT_PEC"); assert_eq!(output, delta); } #[test] fn driven_weight_uses_pinned_visual_param_range_and_morphs() { let offsets = PhysicsVolumeMorphs::compute_bone_offsets(HashMap::from([(1200, 1.0)]), HashMap::new()) .unwrap(); assert_eq!(offsets["LEFT_PEC"], Vector3([0.0, 0.0, -0.03])); assert_eq!(offsets["RIGHT_PEC"], Vector3([0.0, 0.0, -0.03])); } }