#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum WeightPaintFormat {
Json,
Csv,
Binary,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct WeightPaintConfig {
pub normalize: bool,
pub clamp_to_01: bool,
pub format: WeightPaintFormat,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct VertexWeightEntry {
pub vertex_idx: u32,
pub bone_name: String,
pub weight: f32,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct WeightPaintData {
pub entries: Vec<VertexWeightEntry>,
pub vertex_count: usize,
pub bone_count: usize,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct WeightPaintExportResult {
pub data_string: String,
pub entry_count: usize,
}
#[allow(dead_code)]
pub fn default_weight_paint_config() -> WeightPaintConfig {
WeightPaintConfig {
normalize: true,
clamp_to_01: true,
format: WeightPaintFormat::Json,
}
}
#[allow(dead_code)]
pub fn new_weight_paint_data(vertex_count: usize) -> WeightPaintData {
WeightPaintData {
entries: Vec::new(),
vertex_count,
bone_count: 0,
}
}
#[allow(dead_code)]
pub fn add_weight_entry(data: &mut WeightPaintData, entry: VertexWeightEntry) {
let is_new_bone = !data
.entries
.iter()
.any(|e| e.bone_name == entry.bone_name);
if is_new_bone {
data.bone_count += 1;
}
data.entries.push(entry);
}
#[allow(dead_code)]
pub fn new_weight_entry(vertex: u32, bone: &str, weight: f32) -> VertexWeightEntry {
VertexWeightEntry {
vertex_idx: vertex,
bone_name: bone.to_string(),
weight,
}
}
#[allow(dead_code)]
pub fn export_weight_paint(
data: &WeightPaintData,
cfg: &WeightPaintConfig,
) -> WeightPaintExportResult {
let mut working = data.clone();
if cfg.clamp_to_01 {
for e in &mut working.entries {
e.weight = e.weight.clamp(0.0, 1.0);
}
}
if cfg.normalize {
normalize_vertex_weights(&mut working);
}
let data_string = match cfg.format {
WeightPaintFormat::Json => {
let entries_json: Vec<String> = working
.entries
.iter()
.map(|e| {
format!(
r#"{{"vertex_idx":{},"bone_name":"{}","weight":{:.6}}}"#,
e.vertex_idx, e.bone_name, e.weight
)
})
.collect();
format!(
r#"{{"vertex_count":{},"bone_count":{},"entries":[{}]}}"#,
working.vertex_count,
working.bone_count,
entries_json.join(",")
)
}
WeightPaintFormat::Csv => {
let mut lines = vec!["vertex_idx,bone_name,weight".to_string()];
for e in &working.entries {
lines.push(format!("{},{},{:.6}", e.vertex_idx, e.bone_name, e.weight));
}
lines.join("\n")
}
WeightPaintFormat::Binary => {
let mut bytes: Vec<u8> = Vec::with_capacity(working.entries.len() * 8);
for e in &working.entries {
bytes.extend_from_slice(&e.vertex_idx.to_le_bytes());
bytes.extend_from_slice(&e.weight.to_le_bytes());
}
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
};
WeightPaintExportResult {
entry_count: working.entries.len(),
data_string,
}
}
#[allow(dead_code)]
pub fn normalize_vertex_weights(data: &mut WeightPaintData) {
let mut vtx_indices: Vec<u32> = data.entries.iter().map(|e| e.vertex_idx).collect();
vtx_indices.sort_unstable();
vtx_indices.dedup();
for vtx in vtx_indices {
let sum: f32 = data
.entries
.iter()
.filter(|e| e.vertex_idx == vtx)
.map(|e| e.weight)
.sum();
if sum > 0.0 {
for e in data.entries.iter_mut().filter(|e| e.vertex_idx == vtx) {
e.weight /= sum;
}
}
}
}
#[allow(dead_code)]
pub fn weights_for_vertex(
data: &WeightPaintData,
vertex: u32,
) -> Vec<&VertexWeightEntry> {
data.entries
.iter()
.filter(|e| e.vertex_idx == vertex)
.collect()
}
#[allow(dead_code)]
pub fn weight_format_name(cfg: &WeightPaintConfig) -> &'static str {
match cfg.format {
WeightPaintFormat::Json => "JSON",
WeightPaintFormat::Csv => "CSV",
WeightPaintFormat::Binary => "Binary",
}
}
#[allow(dead_code)]
pub fn weight_paint_result_to_json(r: &WeightPaintExportResult) -> String {
format!(
r#"{{"entry_count":{},"data_string_len":{}}}"#,
r.entry_count,
r.data_string.len()
)
}
#[allow(dead_code)]
pub fn validate_weights(data: &WeightPaintData) -> bool {
data.entries.iter().all(|e| {
e.weight >= 0.0
&& e.weight <= 1.0
&& (e.vertex_idx as usize) < data.vertex_count
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_normalize_and_clamp() {
let cfg = default_weight_paint_config();
assert!(cfg.normalize);
assert!(cfg.clamp_to_01);
assert_eq!(cfg.format, WeightPaintFormat::Json);
}
#[test]
fn add_weight_entry_updates_bone_count() {
let mut d = new_weight_paint_data(4);
add_weight_entry(&mut d, new_weight_entry(0, "hip", 0.6));
add_weight_entry(&mut d, new_weight_entry(0, "spine", 0.4));
add_weight_entry(&mut d, new_weight_entry(1, "hip", 0.9)); assert_eq!(d.bone_count, 2);
assert_eq!(d.entries.len(), 3);
}
#[test]
fn normalize_vertex_weights_sums_to_one() {
let mut d = new_weight_paint_data(2);
add_weight_entry(&mut d, new_weight_entry(0, "A", 3.0));
add_weight_entry(&mut d, new_weight_entry(0, "B", 1.0));
normalize_vertex_weights(&mut d);
let sum: f32 = weights_for_vertex(&d, 0).iter().map(|e| e.weight).sum();
assert!((sum - 1.0).abs() < 1e-5);
}
#[test]
fn export_json_format_contains_vertex_count() {
let mut d = new_weight_paint_data(10);
add_weight_entry(&mut d, new_weight_entry(0, "bone0", 1.0));
let cfg = WeightPaintConfig {
normalize: false,
clamp_to_01: false,
format: WeightPaintFormat::Json,
};
let result = export_weight_paint(&d, &cfg);
assert!(result.data_string.contains("\"vertex_count\":10"));
assert_eq!(result.entry_count, 1);
}
#[test]
fn export_csv_format_has_header() {
let d = new_weight_paint_data(5);
let cfg = WeightPaintConfig {
normalize: false,
clamp_to_01: false,
format: WeightPaintFormat::Csv,
};
let result = export_weight_paint(&d, &cfg);
assert!(result.data_string.starts_with("vertex_idx,bone_name,weight"));
}
#[test]
fn validate_weights_valid() {
let mut d = new_weight_paint_data(3);
add_weight_entry(&mut d, new_weight_entry(0, "bone", 0.5));
add_weight_entry(&mut d, new_weight_entry(1, "bone", 1.0));
assert!(validate_weights(&d));
}
#[test]
fn validate_weights_out_of_range_vertex() {
let mut d = new_weight_paint_data(2);
add_weight_entry(&mut d, new_weight_entry(99, "bone", 0.5));
assert!(!validate_weights(&d));
}
#[test]
fn weight_format_name_returns_correct_strings() {
let mut cfg = default_weight_paint_config();
cfg.format = WeightPaintFormat::Json;
assert_eq!(weight_format_name(&cfg), "JSON");
cfg.format = WeightPaintFormat::Csv;
assert_eq!(weight_format_name(&cfg), "CSV");
cfg.format = WeightPaintFormat::Binary;
assert_eq!(weight_format_name(&cfg), "Binary");
}
#[test]
fn weight_paint_result_to_json_contains_entry_count() {
let r = WeightPaintExportResult {
data_string: "hello".to_string(),
entry_count: 42,
};
let j = weight_paint_result_to_json(&r);
assert!(j.contains("\"entry_count\":42"));
}
#[test]
fn export_binary_format_non_empty_when_entries_present() {
let mut d = new_weight_paint_data(1);
add_weight_entry(&mut d, new_weight_entry(0, "root", 1.0));
let cfg = WeightPaintConfig {
normalize: false,
clamp_to_01: false,
format: WeightPaintFormat::Binary,
};
let result = export_weight_paint(&d, &cfg);
assert!(!result.data_string.is_empty());
}
}