pub const MISSING: i32 = -1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AnimMapper {
indices: Vec<i32>,
source_len: usize,
identity: bool,
full: bool,
}
impl AnimMapper {
pub fn new(source: &[String], target: &[String]) -> Self {
use std::collections::HashMap;
let by_name: HashMap<&str, i32> = source.iter().enumerate().map(|(i, n)| (n.as_str(), i as i32)).collect();
let indices: Vec<i32> = target
.iter()
.map(|n| by_name.get(n.as_str()).copied().unwrap_or(MISSING))
.collect();
let full = indices.iter().all(|&i| i != MISSING);
let identity = source.len() == target.len() && indices.iter().enumerate().all(|(i, &j)| j == i as i32);
Self {
indices,
source_len: source.len(),
identity,
full,
}
}
pub fn indices(&self) -> &[i32] {
&self.indices
}
pub fn source_len(&self) -> usize {
self.source_len
}
pub fn target_len(&self) -> usize {
self.indices.len()
}
pub fn source_index(&self, target: usize) -> Option<usize> {
match self.indices.get(target).copied()? {
MISSING => None,
i => Some(i as usize),
}
}
pub fn is_identity(&self) -> bool {
self.identity
}
pub fn is_full(&self) -> bool {
self.full
}
pub fn is_sparse(&self) -> bool {
!self.full
}
pub fn remap<T: Clone>(&self, source: &[T], default: T) -> Vec<T> {
assert_eq!(
source.len(),
self.source_len,
"AnimMapper::remap: source length {} != mapper source length {}",
source.len(),
self.source_len
);
if self.identity {
return source.to_vec();
}
self.indices
.iter()
.map(|&i| {
if i == MISSING {
default.clone()
} else {
source[i as usize].clone()
}
})
.collect()
}
pub fn remap_with_stride<T: Copy>(&self, source: &[T], stride: usize, default: T) -> Vec<T> {
let expected = self.source_len * stride;
assert_eq!(
source.len(),
expected,
"AnimMapper::remap_with_stride: source length {} != source_len {} * stride {} = {}",
source.len(),
self.source_len,
stride,
expected
);
let mut out = Vec::with_capacity(self.indices.len() * stride);
for &i in &self.indices {
if i == MISSING {
for _ in 0..stride {
out.push(default);
}
} else {
let start = (i as usize) * stride;
out.extend_from_slice(&source[start..start + stride]);
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn s(items: &[&str]) -> Vec<String> {
items.iter().map(|x| x.to_string()).collect()
}
#[test]
fn identity_when_orders_match() {
let m = AnimMapper::new(&s(&["A", "B", "C"]), &s(&["A", "B", "C"]));
assert!(m.is_identity());
assert!(m.is_full());
assert_eq!(m.remap(&[10, 20, 30], 0), vec![10, 20, 30]);
}
#[test]
fn reorders_when_target_permutes() {
let m = AnimMapper::new(&s(&["A", "B", "C"]), &s(&["C", "A", "B"]));
assert!(!m.is_identity());
assert!(m.is_full());
assert_eq!(m.remap(&[1.0, 2.0, 3.0], 0.0), vec![3.0, 1.0, 2.0]);
}
#[test]
fn fills_missing_with_default() {
let m = AnimMapper::new(&s(&["A"]), &s(&["A", "B"]));
assert!(m.is_sparse());
assert_eq!(m.indices(), &[0, MISSING]);
assert_eq!(m.remap(&[42], 0), vec![42, 0]);
}
#[test]
#[should_panic(expected = "source length")]
fn remap_rejects_wrong_source_length() {
let m = AnimMapper::new(&s(&["A", "B"]), &s(&["A", "B"]));
let _ = m.remap(&[1], 0);
}
#[test]
#[should_panic(expected = "source length")]
fn remap_with_stride_rejects_wrong_source_length() {
let m = AnimMapper::new(&s(&["A", "B"]), &s(&["A"]));
let _ = m.remap_with_stride(&[0.0_f32; 7], 4, 0.0);
}
#[test]
fn remap_with_stride_handles_flat_matrices() {
let src = [
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, ];
let m = AnimMapper::new(&s(&["A", "B"]), &s(&["B", "missing", "A"]));
let out = m.remap_with_stride(&src, 4, 0.0);
assert_eq!(out, vec![5.0, 6.0, 7.0, 8.0, 0.0, 0.0, 0.0, 0.0, 1.0, 2.0, 3.0, 4.0]);
}
}