Generate skeleton attention and genepool catalogs (#49)
This commit is contained in:
580
crates/libremetaverse/src/skeleton.rs
Normal file
580
crates/libremetaverse/src/skeleton.rs
Normal file
@@ -0,0 +1,580 @@
|
||||
//! Native skeleton records, traversal helpers, and custom XML loading.
|
||||
|
||||
#![allow(clippy::missing_errors_doc)] // Public signatures are fixed by the compatibility catalog.
|
||||
#![allow(clippy::must_use_candidate)] // Getters mirror C# properties.
|
||||
#![allow(clippy::needless_pass_by_value)] // Owned strings are fixed by the compatibility API.
|
||||
#![allow(clippy::unnecessary_wraps)] // Constructors mirror fallible mapped signatures.
|
||||
|
||||
use roxmltree::{Document, Node};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct JointBase {
|
||||
name: String,
|
||||
pos: Vec<f32>,
|
||||
rot: Vec<f32>,
|
||||
scale: Vec<f32>,
|
||||
group: String,
|
||||
support: String,
|
||||
end: Vec<f32>,
|
||||
reposition: bool,
|
||||
}
|
||||
|
||||
impl JointBase {
|
||||
pub fn new() -> Result<Self, crate::Error> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
pub fn support_category(&self) -> crate::rendering::JointSupportCategory {
|
||||
if self.support.eq_ignore_ascii_case("extended") {
|
||||
crate::rendering::JointSupportCategory::Extended
|
||||
} else {
|
||||
crate::rendering::JointSupportCategory::Base
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end(&self) -> Vec<f32> {
|
||||
self.end.clone()
|
||||
}
|
||||
|
||||
pub fn set_end(&mut self, value: Vec<f32>) {
|
||||
self.end = value;
|
||||
}
|
||||
|
||||
pub fn group(&self) -> String {
|
||||
self.group.clone()
|
||||
}
|
||||
|
||||
pub fn set_group(&mut self, value: String) {
|
||||
self.group = value;
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
pub fn set_name(&mut self, value: String) {
|
||||
self.name = value;
|
||||
}
|
||||
|
||||
pub fn pos(&self) -> Vec<f32> {
|
||||
self.pos.clone()
|
||||
}
|
||||
|
||||
pub fn set_pos(&mut self, value: Vec<f32>) {
|
||||
self.pos = value;
|
||||
}
|
||||
|
||||
pub fn reposition(&self) -> bool {
|
||||
self.reposition
|
||||
}
|
||||
|
||||
pub fn set_reposition(&mut self, value: bool) {
|
||||
self.reposition = value;
|
||||
}
|
||||
|
||||
pub fn rot(&self) -> Vec<f32> {
|
||||
self.rot.clone()
|
||||
}
|
||||
|
||||
pub fn set_rot(&mut self, value: Vec<f32>) {
|
||||
self.rot = value;
|
||||
}
|
||||
|
||||
pub fn scale(&self) -> Vec<f32> {
|
||||
self.scale.clone()
|
||||
}
|
||||
|
||||
pub fn set_scale(&mut self, value: Vec<f32>) {
|
||||
self.scale = value;
|
||||
}
|
||||
|
||||
pub fn support(&self) -> String {
|
||||
self.support.clone()
|
||||
}
|
||||
|
||||
pub fn set_support(&mut self, value: String) {
|
||||
self.support = value;
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn from_parts(
|
||||
name: String,
|
||||
pos: Vec<f32>,
|
||||
rot: Vec<f32>,
|
||||
scale: Vec<f32>,
|
||||
group: String,
|
||||
support: String,
|
||||
end: Vec<f32>,
|
||||
reposition: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
pos,
|
||||
rot,
|
||||
scale,
|
||||
group,
|
||||
support,
|
||||
end,
|
||||
reposition,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct CollisionVolume {
|
||||
pub base: JointBase,
|
||||
}
|
||||
|
||||
impl CollisionVolume {
|
||||
pub fn new() -> Result<Self, crate::Error> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
pub(crate) fn from_base(base: JointBase) -> Self {
|
||||
Self { base }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Joint {
|
||||
pub base: JointBase,
|
||||
collision_volume: Vec<CollisionVolume>,
|
||||
bone: Option<Vec<Joint>>,
|
||||
pivot: Vec<f32>,
|
||||
aliases: String,
|
||||
connected: bool,
|
||||
}
|
||||
|
||||
impl Default for Joint {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base: JointBase::default(),
|
||||
collision_volume: Vec::new(),
|
||||
bone: Some(Vec::new()),
|
||||
pivot: Vec::new(),
|
||||
aliases: String::new(),
|
||||
connected: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Joint {
|
||||
pub fn new() -> Result<Self, crate::Error> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
pub fn get_aliases_list(&self) -> Result<Vec<String>, crate::Error> {
|
||||
Ok(self
|
||||
.aliases
|
||||
.split_ascii_whitespace()
|
||||
.map(str::to_owned)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn aliases(&self) -> String {
|
||||
self.aliases.clone()
|
||||
}
|
||||
|
||||
pub fn set_aliases(&mut self, value: String) {
|
||||
self.aliases = value;
|
||||
}
|
||||
|
||||
pub fn bone(&self) -> Option<Vec<Self>> {
|
||||
self.bone.clone()
|
||||
}
|
||||
|
||||
pub fn set_bone(&mut self, value: Option<Vec<Self>>) {
|
||||
self.bone = value;
|
||||
}
|
||||
|
||||
pub fn collision_volume(&self) -> Vec<CollisionVolume> {
|
||||
self.collision_volume.clone()
|
||||
}
|
||||
|
||||
pub fn set_collision_volume(&mut self, value: Vec<CollisionVolume>) {
|
||||
self.collision_volume = value;
|
||||
}
|
||||
|
||||
pub fn connected(&self) -> bool {
|
||||
self.connected
|
||||
}
|
||||
|
||||
pub fn set_connected(&mut self, value: bool) {
|
||||
self.connected = value;
|
||||
}
|
||||
|
||||
pub fn pivot(&self) -> Vec<f32> {
|
||||
self.pivot.clone()
|
||||
}
|
||||
|
||||
pub fn set_pivot(&mut self, value: Vec<f32>) {
|
||||
self.pivot = value;
|
||||
}
|
||||
|
||||
pub(crate) fn from_parts(
|
||||
base: JointBase,
|
||||
collision_volume: Vec<CollisionVolume>,
|
||||
bone: Vec<Self>,
|
||||
pivot: Vec<f32>,
|
||||
aliases: String,
|
||||
connected: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
base,
|
||||
collision_volume,
|
||||
bone: Some(bone),
|
||||
pivot,
|
||||
aliases,
|
||||
connected,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct LindenSkeleton {
|
||||
bone: Joint,
|
||||
version: f32,
|
||||
version_specified: bool,
|
||||
num_bones: String,
|
||||
num_collision_volumes: String,
|
||||
}
|
||||
|
||||
impl Default for LindenSkeleton {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bone: Joint::default(),
|
||||
version: 0.0,
|
||||
version_specified: false,
|
||||
num_bones: String::new(),
|
||||
num_collision_volumes: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LindenSkeleton {
|
||||
pub fn new() -> Result<Self, crate::Error> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
pub fn build_expanded_joint_list(
|
||||
&self,
|
||||
joints_filter: Box<dyn Iterator<Item = String>>,
|
||||
) -> Result<Vec<String>, crate::Error> {
|
||||
let filter = joints_filter.collect::<Vec<_>>();
|
||||
let mut expanded = Vec::new();
|
||||
if let Some(children) = &self.bone.bone {
|
||||
for child in children {
|
||||
expand_joint(child, &self.bone.base.name, &mut expanded, &filter);
|
||||
}
|
||||
}
|
||||
Ok(expanded)
|
||||
}
|
||||
|
||||
pub fn build_joint_dictionary(&self) -> Result<HashMap<String, Arc<Joint>>, crate::Error> {
|
||||
let mut dictionary = HashMap::new();
|
||||
for joint in collect_joints(&self.bone) {
|
||||
if !joint.base.name.is_empty() {
|
||||
dictionary.insert(joint.base.name.clone(), Arc::clone(&joint));
|
||||
}
|
||||
for alias in joint.aliases.split_ascii_whitespace() {
|
||||
if !alias.is_empty() {
|
||||
dictionary.insert(alias.to_owned(), Arc::clone(&joint));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(dictionary)
|
||||
}
|
||||
|
||||
pub fn get_all_joints(&self) -> Result<Box<dyn Iterator<Item = Arc<Joint>>>, crate::Error> {
|
||||
Ok(Box::new(collect_joints(&self.bone).into_iter()))
|
||||
}
|
||||
|
||||
pub fn get_bone(&self, name_or_alias: String) -> Result<Option<Arc<Joint>>, crate::Error> {
|
||||
if name_or_alias.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(self.build_joint_dictionary()?.remove(&name_or_alias))
|
||||
}
|
||||
|
||||
pub fn get_default() -> Result<Self, crate::Error> {
|
||||
Ok(crate::skeleton_catalog::default_skeleton())
|
||||
}
|
||||
|
||||
pub fn load_with_method() -> Result<Self, crate::Error> {
|
||||
Self::get_default()
|
||||
}
|
||||
|
||||
pub fn load_with_string(file_name: Option<String>) -> Result<Self, crate::Error> {
|
||||
let Some(file_name) = file_name else {
|
||||
return Self::get_default();
|
||||
};
|
||||
let text = std::fs::read_to_string(file_name).map_err(|_| crate::Error::Argument)?;
|
||||
parse_skeleton_xml(&text)
|
||||
}
|
||||
|
||||
pub fn bone(&self) -> Joint {
|
||||
self.bone.clone()
|
||||
}
|
||||
|
||||
pub fn set_bone(&mut self, value: Joint) {
|
||||
self.bone = value;
|
||||
}
|
||||
|
||||
pub fn num_bones(&self) -> String {
|
||||
self.num_bones.clone()
|
||||
}
|
||||
|
||||
pub fn set_num_bones(&mut self, value: String) {
|
||||
self.num_bones = value;
|
||||
}
|
||||
|
||||
pub fn num_collision_volumes(&self) -> String {
|
||||
self.num_collision_volumes.clone()
|
||||
}
|
||||
|
||||
pub fn set_num_collision_volumes(&mut self, value: String) {
|
||||
self.num_collision_volumes = value;
|
||||
}
|
||||
|
||||
pub fn version(&self) -> f32 {
|
||||
self.version
|
||||
}
|
||||
|
||||
pub fn set_version(&mut self, value: f32) {
|
||||
self.version = value;
|
||||
}
|
||||
|
||||
pub fn version_specified(&self) -> bool {
|
||||
self.version_specified
|
||||
}
|
||||
|
||||
pub fn set_version_specified(&mut self, value: bool) {
|
||||
self.version_specified = value;
|
||||
}
|
||||
|
||||
pub(crate) fn from_parts(
|
||||
bone: Joint,
|
||||
version: f32,
|
||||
version_specified: bool,
|
||||
num_bones: String,
|
||||
num_collision_volumes: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
bone,
|
||||
version,
|
||||
version_specified,
|
||||
num_bones,
|
||||
num_collision_volumes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_joints(root: &Joint) -> Vec<Arc<Joint>> {
|
||||
fn visit(joint: &Joint, output: &mut Vec<Arc<Joint>>) {
|
||||
output.push(Arc::new(joint.clone()));
|
||||
if let Some(children) = &joint.bone {
|
||||
for child in children {
|
||||
visit(child, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut output = Vec::new();
|
||||
visit(root, &mut output);
|
||||
output
|
||||
}
|
||||
|
||||
fn expand_joint(
|
||||
current: &Joint,
|
||||
effective_parent: &str,
|
||||
expanded: &mut Vec<String>,
|
||||
filter: &[String],
|
||||
) {
|
||||
let mut next_effective_parent = effective_parent;
|
||||
if filter.iter().any(|name| name == ¤t.base.name) {
|
||||
let parent_matches_last = expanded.last().is_some_and(|last| last == effective_parent);
|
||||
if !parent_matches_last && filter.iter().any(|name| name == effective_parent) {
|
||||
expanded.push(effective_parent.to_owned());
|
||||
}
|
||||
expanded.push(current.base.name.clone());
|
||||
next_effective_parent = ¤t.base.name;
|
||||
}
|
||||
if let Some(children) = ¤t.bone {
|
||||
for child in children {
|
||||
expand_joint(child, next_effective_parent, expanded, filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_error(position: usize) -> crate::Error {
|
||||
crate::Error::Parse {
|
||||
position,
|
||||
context: "avatar skeleton XML",
|
||||
}
|
||||
}
|
||||
|
||||
fn node_position(node: Node<'_, '_>) -> usize {
|
||||
node.range().start
|
||||
}
|
||||
|
||||
fn required_attribute<'a>(node: Node<'a, '_>, name: &str) -> Result<&'a str, crate::Error> {
|
||||
node.attribute(name)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| parse_error(node_position(node)))
|
||||
}
|
||||
|
||||
fn vector_attribute(node: Node<'_, '_>, name: &str) -> Result<Vec<f32>, crate::Error> {
|
||||
let Some(value) = node.attribute(name) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let values = value
|
||||
.split_ascii_whitespace()
|
||||
.map(str::parse::<f32>)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| parse_error(node_position(node)))?;
|
||||
if values.len() != 3 || values.iter().any(|value| !value.is_finite()) {
|
||||
return Err(parse_error(node_position(node)));
|
||||
}
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
fn boolean_attribute(node: Node<'_, '_>, name: &str) -> Result<bool, crate::Error> {
|
||||
match node.attribute(name) {
|
||||
None => Ok(false),
|
||||
Some(value) if value.eq_ignore_ascii_case("true") => Ok(true),
|
||||
Some(value) if value.eq_ignore_ascii_case("false") => Ok(false),
|
||||
Some(_) => Err(parse_error(node_position(node))),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_base(node: Node<'_, '_>) -> Result<JointBase, crate::Error> {
|
||||
let support = node.attribute("support").unwrap_or_default();
|
||||
if !support.is_empty()
|
||||
&& !support.eq_ignore_ascii_case("base")
|
||||
&& !support.eq_ignore_ascii_case("extended")
|
||||
{
|
||||
return Err(parse_error(node_position(node)));
|
||||
}
|
||||
Ok(JointBase::from_parts(
|
||||
required_attribute(node, "name")?.to_owned(),
|
||||
vector_attribute(node, "pos")?,
|
||||
vector_attribute(node, "rot")?,
|
||||
vector_attribute(node, "scale")?,
|
||||
node.attribute("group").unwrap_or_default().to_owned(),
|
||||
support.to_owned(),
|
||||
vector_attribute(node, "end")?,
|
||||
boolean_attribute(node, "reposition")?,
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_joint(node: Node<'_, '_>, names: &mut HashSet<String>) -> Result<Joint, crate::Error> {
|
||||
if !node.has_tag_name("bone") {
|
||||
return Err(parse_error(node_position(node)));
|
||||
}
|
||||
let base = parse_base(node)?;
|
||||
if !names.insert(base.name.clone()) {
|
||||
return Err(parse_error(node_position(node)));
|
||||
}
|
||||
let aliases = node.attribute("aliases").unwrap_or_default().to_owned();
|
||||
for alias in aliases.split_ascii_whitespace() {
|
||||
if !names.insert(alias.to_owned()) {
|
||||
return Err(parse_error(node_position(node)));
|
||||
}
|
||||
}
|
||||
let mut collision_volumes = Vec::new();
|
||||
let mut bones = Vec::new();
|
||||
for child in node.children().filter(Node::is_element) {
|
||||
if child.has_tag_name("collision_volume") {
|
||||
if child.children().any(|node| node.is_element()) {
|
||||
return Err(parse_error(node_position(child)));
|
||||
}
|
||||
collision_volumes.push(CollisionVolume::from_base(parse_base(child)?));
|
||||
} else if child.has_tag_name("bone") {
|
||||
bones.push(parse_joint(child, names)?);
|
||||
} else {
|
||||
return Err(parse_error(node_position(child)));
|
||||
}
|
||||
}
|
||||
Ok(Joint::from_parts(
|
||||
base,
|
||||
collision_volumes,
|
||||
bones,
|
||||
vector_attribute(node, "pivot")?,
|
||||
aliases,
|
||||
boolean_attribute(node, "connected")?,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_skeleton_xml(text: &str) -> Result<LindenSkeleton, crate::Error> {
|
||||
let document = Document::parse(text).map_err(|_| parse_error(0))?;
|
||||
let root = document.root_element();
|
||||
if !root.has_tag_name("linden_skeleton") {
|
||||
return Err(parse_error(node_position(root)));
|
||||
}
|
||||
let version_text = required_attribute(root, "version")?;
|
||||
let version = version_text
|
||||
.parse::<f32>()
|
||||
.map_err(|_| parse_error(node_position(root)))?;
|
||||
if !version.is_finite() {
|
||||
return Err(parse_error(node_position(root)));
|
||||
}
|
||||
let num_bones = required_attribute(root, "num_bones")?.to_owned();
|
||||
let expected_bones = num_bones
|
||||
.parse::<usize>()
|
||||
.map_err(|_| parse_error(node_position(root)))?;
|
||||
let num_collision_volumes = required_attribute(root, "num_collision_volumes")?.to_owned();
|
||||
let expected_collision_volumes = num_collision_volumes
|
||||
.parse::<usize>()
|
||||
.map_err(|_| parse_error(node_position(root)))?;
|
||||
let root_bones = root.children().filter(Node::is_element).collect::<Vec<_>>();
|
||||
if root_bones.len() != 1 || !root_bones[0].has_tag_name("bone") {
|
||||
return Err(parse_error(node_position(root)));
|
||||
}
|
||||
let mut names = HashSet::new();
|
||||
let bone = parse_joint(root_bones[0], &mut names)?;
|
||||
let (actual_bones, actual_collision_volumes) = skeleton_counts(&bone);
|
||||
if actual_bones != expected_bones || actual_collision_volumes != expected_collision_volumes {
|
||||
return Err(parse_error(node_position(root)));
|
||||
}
|
||||
Ok(LindenSkeleton::from_parts(
|
||||
bone,
|
||||
version,
|
||||
true,
|
||||
num_bones,
|
||||
num_collision_volumes,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn skeleton_counts(root: &Joint) -> (usize, usize) {
|
||||
let mut bones = 1;
|
||||
let mut collision_volumes = root.collision_volume.len();
|
||||
if let Some(children) = &root.bone {
|
||||
for child in children {
|
||||
let (child_bones, child_collision_volumes) = skeleton_counts(child);
|
||||
bones += child_bones;
|
||||
collision_volumes += child_collision_volumes;
|
||||
}
|
||||
}
|
||||
(bones, collision_volumes)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn runtime_parser_rejects_count_mismatches_and_unknown_children() {
|
||||
let wrong_count = "<linden_skeleton version=\"1\" num_bones=\"2\" num_collision_volumes=\"0\"><bone name=\"root\"/></linden_skeleton>";
|
||||
assert!(matches!(
|
||||
parse_skeleton_xml(wrong_count),
|
||||
Err(crate::Error::Parse { .. })
|
||||
));
|
||||
let unknown = "<linden_skeleton version=\"1\" num_bones=\"1\" num_collision_volumes=\"0\"><bone name=\"root\"><unknown/></bone></linden_skeleton>";
|
||||
assert!(matches!(
|
||||
parse_skeleton_xml(unknown),
|
||||
Err(crate::Error::Parse { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user