use std::collections::HashMap;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct VertexGroup {
pub name: String,
pub indices: Vec<u32>,
pub weights: Vec<f32>,
}
impl VertexGroup {
pub fn new(name: impl Into<String>, mut indices: Vec<u32>) -> Self {
indices.sort_unstable();
indices.dedup();
Self {
name: name.into(),
indices,
weights: Vec::new(),
}
}
pub fn with_weights(name: impl Into<String>, mut indices: Vec<u32>, weights: Vec<f32>) -> Self {
let mut pairs: Vec<(u32, f32)> = indices
.iter()
.copied()
.zip(weights.iter().copied())
.collect();
pairs.sort_unstable_by_key(|(i, _)| *i);
pairs.dedup_by_key(|(i, _)| *i);
indices = pairs.iter().map(|(i, _)| *i).collect();
let weights = pairs.iter().map(|(_, w)| *w).collect();
Self {
name: name.into(),
indices,
weights,
}
}
pub fn len(&self) -> usize {
self.indices.len()
}
pub fn is_empty(&self) -> bool {
self.indices.is_empty()
}
pub fn weight_of(&self, vid: u32) -> f32 {
match self.indices.binary_search(&vid) {
Ok(pos) => {
if self.weights.is_empty() {
1.0
} else {
self.weights[pos]
}
}
Err(_) => 1.0,
}
}
pub fn contains(&self, vid: u32) -> bool {
self.indices.binary_search(&vid).is_ok()
}
pub fn union(&self, other: &VertexGroup) -> VertexGroup {
let mut merged = self.indices.clone();
merged.extend_from_slice(&other.indices);
merged.sort_unstable();
merged.dedup();
VertexGroup {
name: self.name.clone(),
indices: merged,
weights: Vec::new(),
}
}
pub fn intersect(&self, other: &VertexGroup) -> VertexGroup {
let indices: Vec<u32> = self
.indices
.iter()
.copied()
.filter(|v| other.contains(*v))
.collect();
VertexGroup {
name: self.name.clone(),
indices,
weights: Vec::new(),
}
}
pub fn difference(&self, other: &VertexGroup) -> VertexGroup {
let indices: Vec<u32> = self
.indices
.iter()
.copied()
.filter(|v| !other.contains(*v))
.collect();
VertexGroup {
name: self.name.clone(),
indices,
weights: Vec::new(),
}
}
}
pub struct VertexGroupMap {
groups: HashMap<String, VertexGroup>,
}
impl VertexGroupMap {
pub fn new() -> Self {
Self {
groups: HashMap::new(),
}
}
pub fn add(&mut self, group: VertexGroup) {
self.groups.insert(group.name.to_lowercase(), group);
}
pub fn get(&self, name: &str) -> Option<&VertexGroup> {
self.groups.get(&name.to_lowercase())
}
pub fn remove(&mut self, name: &str) -> Option<VertexGroup> {
self.groups.remove(&name.to_lowercase())
}
pub fn names(&self) -> Vec<&str> {
self.groups.values().map(|g| g.name.as_str()).collect()
}
pub fn len(&self) -> usize {
self.groups.len()
}
pub fn is_empty(&self) -> bool {
self.groups.is_empty()
}
pub fn from_y_bands(positions: &[[f32; 3]], min_y: f32, total_height: f32) -> Self {
let bands: &[(&str, f32, f32)] = &[
("feet", 0.00, 0.08),
("lower_legs", 0.08, 0.25),
("upper_legs", 0.25, 0.48),
("torso", 0.48, 0.72),
("shoulders", 0.72, 0.82),
("neck", 0.82, 0.90),
("head", 0.90, 1.01), ];
let mut band_verts: Vec<Vec<u32>> = vec![Vec::new(); bands.len()];
for (vid, pos) in positions.iter().enumerate() {
let vy = pos[1];
let t = if total_height > 0.0 {
(vy - min_y) / total_height
} else {
0.0
};
for (bi, &(_, lo, hi)) in bands.iter().enumerate() {
if t >= lo && t < hi {
band_verts[bi].push(vid as u32);
break;
}
}
}
let mut map = Self::new();
for (bi, &(name, _, _)) in bands.iter().enumerate() {
let group = VertexGroup::new(name, band_verts[bi].clone());
map.add(group);
}
map
}
pub fn to_json(&self) -> serde_json::Value {
let obj: serde_json::Map<String, serde_json::Value> = self
.groups
.iter()
.map(|(key, group)| {
(
key.clone(),
serde_json::to_value(group).unwrap_or(serde_json::Value::Null),
)
})
.collect();
serde_json::Value::Object(obj)
}
}
impl Default for VertexGroupMap {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn group_contains_vertex() {
let group = VertexGroup::new("test", vec![5, 10, 20]);
assert!(group.contains(5));
assert!(!group.contains(99));
}
#[test]
fn group_union_deduplicates() {
let a = VertexGroup::new("a", vec![0, 1, 2]);
let b = VertexGroup::new("b", vec![1, 2, 3]);
let u = a.union(&b);
assert_eq!(u.indices, vec![0, 1, 2, 3]);
}
#[test]
fn group_intersect() {
let a = VertexGroup::new("a", vec![0, 1, 2]);
let b = VertexGroup::new("b", vec![1, 2, 3]);
let i = a.intersect(&b);
assert_eq!(i.indices, vec![1, 2]);
}
#[test]
fn group_difference() {
let a = VertexGroup::new("a", vec![0, 1, 2]);
let b = VertexGroup::new("b", vec![1, 2]);
let d = a.difference(&b);
assert_eq!(d.indices, vec![0]);
}
#[test]
fn map_get_case_insensitive() {
let mut map = VertexGroupMap::new();
map.add(VertexGroup::new("Head", vec![0, 1]));
assert!(map.get("head").is_some());
assert!(map.get("HEAD").is_some());
}
#[test]
fn from_y_bands_creates_seven_groups() {
let positions: Vec<[f32; 3]> = vec![
[0.0, 0.04, 0.0], [0.0, 0.16, 0.0], [0.0, 0.36, 0.0], [0.0, 0.60, 0.0], [0.0, 0.77, 0.0], [0.0, 0.86, 0.0], [0.0, 0.95, 0.0], ];
let map = VertexGroupMap::from_y_bands(&positions, 0.0, 1.0);
assert_eq!(map.len(), 7);
}
#[test]
fn from_y_bands_head_at_top() {
let positions: Vec<[f32; 3]> = vec![
[0.0, 0.0, 0.0], [0.0, 1.8, 0.0], ];
let map = VertexGroupMap::from_y_bands(&positions, 0.0, 2.0);
let head = map.get("head").expect("head group missing");
assert!(head.contains(1), "vertex 1 should be in head group");
}
#[test]
fn weight_of_returns_one_for_simple_group() {
let group = VertexGroup::new("test", vec![3, 7, 11]);
assert!((group.weight_of(3) - 1.0).abs() < 1e-6);
assert!((group.weight_of(7) - 1.0).abs() < 1e-6);
assert!((group.weight_of(99) - 1.0).abs() < 1e-6);
}
#[test]
fn to_json_has_keys() {
let mut map = VertexGroupMap::new();
map.add(VertexGroup::new("head", vec![0]));
map.add(VertexGroup::new("feet", vec![1]));
let json = map.to_json();
assert!(json.is_object());
let obj = json.as_object().expect("should succeed");
assert!(obj.contains_key("head"));
assert!(obj.contains_key("feet"));
}
}