pub const NO_PARENT: i32 = -1;
pub(super) fn joint_index_map(joints: &[String]) -> std::collections::HashMap<&str, usize> {
joints.iter().enumerate().map(|(i, p)| (p.as_str(), i)).collect()
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Topology {
parents: Vec<i32>,
}
impl Topology {
pub fn new() -> Self {
Self { parents: Vec::new() }
}
pub fn from_joint_paths(paths: &[String]) -> Self {
let by_path = joint_index_map(paths);
let parents = paths
.iter()
.map(|p| {
p.rsplit_once('/')
.and_then(|(parent, _)| by_path.get(parent).copied().map(|i| i as i32))
.unwrap_or(NO_PARENT)
})
.collect();
Self { parents }
}
pub fn from_parents(parents: Vec<i32>) -> Self {
Self { parents }
}
pub fn parents(&self) -> &[i32] {
&self.parents
}
pub fn num_joints(&self) -> usize {
self.parents.len()
}
pub fn parent(&self, i: usize) -> i32 {
self.parents.get(i).copied().unwrap_or(NO_PARENT)
}
pub fn is_root(&self, i: usize) -> bool {
self.parent(i) == NO_PARENT
}
pub fn validate(&self) -> Result<(), TopologyError> {
for (i, &p) in self.parents.iter().enumerate() {
if p == NO_PARENT {
continue;
}
if p < 0 || (p as usize) >= self.parents.len() {
return Err(TopologyError::ParentOutOfRange { joint: i, parent: p });
}
if (p as usize) >= i {
return Err(TopologyError::ParentNotBeforeChild { joint: i, parent: p });
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum TopologyError {
#[error("joint {joint}: parent index {parent} is out of range")]
ParentOutOfRange { joint: usize, parent: i32 },
#[error("joint {joint}: parent index {parent} must be less than the joint's own index")]
ParentNotBeforeChild { joint: usize, parent: i32 },
}
#[cfg(test)]
mod tests {
use super::*;
fn paths(items: &[&str]) -> Vec<String> {
items.iter().map(|s| s.to_string()).collect()
}
#[test]
fn derives_parents_from_path_encoding() {
let t = Topology::from_joint_paths(&paths(&["Root", "Root/Hip", "Root/Hip/Knee", "Root/Hip/Other"]));
assert_eq!(t.parents(), &[NO_PARENT, 0, 1, 1]);
assert!(t.is_root(0));
assert!(!t.is_root(1));
assert_eq!(t.num_joints(), 4);
t.validate().unwrap();
}
#[test]
fn joints_with_missing_prefix_become_roots() {
let t = Topology::from_joint_paths(&paths(&["A", "Foo/Bar"]));
assert_eq!(t.parents(), &[NO_PARENT, NO_PARENT]);
}
#[test]
fn validate_rejects_forward_parent_reference() {
let t = Topology::from_parents(vec![1, NO_PARENT]);
assert!(matches!(
t.validate(),
Err(TopologyError::ParentNotBeforeChild { joint: 0, parent: 1 })
));
}
#[test]
fn validate_rejects_out_of_range_parent() {
let t = Topology::from_parents(vec![NO_PARENT, 7]);
assert!(matches!(
t.validate(),
Err(TopologyError::ParentOutOfRange { joint: 1, parent: 7 })
));
}
}