avatar-anim 0.1.2

A parser for Second Life avatar animations files
Documentation
use crate::{AnimError, Result};
use glam::Vec3;
use std::collections::BTreeMap;
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::Path;
use xml::reader::{EventReader, XmlEvent};

#[derive(Clone, Debug, PartialEq)]
pub struct SkeletonBone {
    pub name: String,
    pub pos: Vec3,
    pub parent: Option<String>,
    pub attributes: BTreeMap<String, String>,
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct SkeletonDefinition {
    pub bones: Vec<SkeletonBone>,
}

impl SkeletonDefinition {
    pub fn from_xml_file<P: AsRef<Path>>(path: P) -> Result<Self> {
        let file = File::open(path).map_err(AnimError::Io)?;
        Self::from_xml_reader(BufReader::new(file))
    }

    pub fn from_xml_reader<R: Read>(reader: R) -> Result<Self> {
        let parser = EventReader::new(reader);
        let mut bones = Vec::new();
        let mut parent_stack = Vec::<Option<String>>::new();

        for event in parser {
            let event = event.map_err(|e| {
                AnimError::InvalidStructure(format!("Skeleton XML parse error: {e}"))
            })?;
            match event {
                XmlEvent::StartElement {
                    name, attributes, ..
                } if name.local_name == "bone" => {
                    let parent = parent_stack.iter().rev().find_map(Clone::clone);
                    let mut attrs = BTreeMap::new();
                    let mut bone_name = None;
                    let mut pos = None;
                    for attr in attributes {
                        let attr_name = attr.name.local_name;
                        if attr_name == "name" {
                            bone_name = Some(attr.value.clone());
                        } else if attr_name == "pos" || attr_name == "position" {
                            pos = Some(parse_vec3(&attr.value)?);
                        }
                        attrs.insert(attr_name, attr.value);
                    }

                    parent_stack.push(bone_name.clone());
                    if let Some(name) = bone_name
                        && let Some(pos) = pos
                    {
                        bones.push(SkeletonBone {
                            name,
                            pos,
                            parent,
                            attributes: attrs,
                        });
                    }
                }
                XmlEvent::EndElement { name } if name.local_name == "bone" => {
                    parent_stack.pop();
                }
                _ => {}
            }
        }

        Ok(Self { bones })
    }

    pub fn bone(&self, name: &str) -> Option<&SkeletonBone> {
        self.bones.iter().find(|bone| bone.name == name)
    }

    pub fn position(&self, name: &str) -> Option<Vec3> {
        self.bone(name).map(|bone| bone.pos)
    }

    pub fn bones_with_prefix(&self, prefix: &str) -> Vec<&SkeletonBone> {
        self.bones
            .iter()
            .filter(|bone| bone.name.starts_with(prefix))
            .collect()
    }

    pub fn bones_in_group(&self, group: &str) -> Vec<&SkeletonBone> {
        self.bones
            .iter()
            .filter(|bone| bone.attributes.get("group").is_some_and(|g| g == group))
            .collect()
    }
}

fn parse_vec3(value: &str) -> Result<Vec3> {
    let mut parts = value.split_whitespace();
    let x = parse_component(parts.next(), value)?;
    let y = parse_component(parts.next(), value)?;
    let z = parse_component(parts.next(), value)?;
    if parts.next().is_some() {
        return Err(AnimError::InvalidStructure(format!(
            "Expected 3-vector, got '{value}'"
        )));
    }
    Ok(Vec3::new(x, y, z))
}

fn parse_component(component: Option<&str>, original: &str) -> Result<f32> {
    component
        .ok_or_else(|| AnimError::InvalidStructure(format!("Expected 3-vector, got '{original}'")))?
        .parse::<f32>()
        .map_err(|_| {
            AnimError::InvalidStructure(format!("Invalid vector component in '{original}'"))
        })
}