#![allow(dead_code)]
#[derive(Debug, Clone)]
pub struct MorphExportLodConfig {
pub lod_ratio: f32,
pub delta_threshold: f32,
pub max_entries: usize,
}
#[derive(Debug, Clone)]
pub struct LodMorphEntry {
pub name: String,
pub original_vertex_count: usize,
pub lod_deltas: Vec<[f32; 3]>,
}
#[derive(Debug, Clone)]
pub struct MorphExportLodResult {
pub entries: Vec<LodMorphEntry>,
pub lod_ratio: f32,
pub total_original_vertices: usize,
pub total_lod_vertices: usize,
}
#[derive(Debug, Clone)]
pub struct MorphExportLod {
config: MorphExportLodConfig,
entries: Vec<LodMorphEntry>,
}
#[allow(dead_code)]
pub fn default_morph_export_lod_config() -> MorphExportLodConfig {
MorphExportLodConfig {
lod_ratio: 0.5,
delta_threshold: 1e-4,
max_entries: 128,
}
}
#[allow(dead_code)]
pub fn new_morph_export_lod(config: MorphExportLodConfig) -> MorphExportLod {
MorphExportLod {
config,
entries: Vec::new(),
}
}
#[allow(dead_code)]
pub fn mel_add_entry(
mel: &mut MorphExportLod,
name: &str,
deltas: &[[f32; 3]],
) {
if mel.entries.len() >= mel.config.max_entries {
return;
}
let original_vertex_count = deltas.len();
mel.entries.push(LodMorphEntry {
name: name.to_string(),
original_vertex_count,
lod_deltas: deltas.to_vec(),
});
}
#[allow(dead_code)]
pub fn mel_export_lod(mel: &MorphExportLod) -> MorphExportLodResult {
let ratio = mel.config.lod_ratio.clamp(0.0, 1.0);
let threshold = mel.config.delta_threshold;
let mut result_entries = Vec::with_capacity(mel.entries.len());
let mut total_orig = 0usize;
let mut total_lod = 0usize;
for entry in &mel.entries {
let n = entry.original_vertex_count;
total_orig += n;
let stride = if ratio <= 0.0 {
usize::MAX
} else {
(1.0 / ratio).round() as usize
};
let stride = stride.max(1);
let lod_deltas: Vec<[f32; 3]> = entry
.lod_deltas
.iter()
.enumerate()
.filter(|(i, d)| {
*i % stride == 0
&& (d[0].abs() > threshold || d[1].abs() > threshold || d[2].abs() > threshold)
})
.map(|(_, d)| *d)
.collect();
total_lod += lod_deltas.len();
result_entries.push(LodMorphEntry {
name: entry.name.clone(),
original_vertex_count: n,
lod_deltas,
});
}
MorphExportLodResult {
entries: result_entries,
lod_ratio: ratio,
total_original_vertices: total_orig,
total_lod_vertices: total_lod,
}
}
#[allow(dead_code)]
pub fn mel_entry_count(mel: &MorphExportLod) -> usize {
mel.entries.len()
}
#[allow(dead_code)]
pub fn mel_lod_vertex_count(result: &MorphExportLodResult, index: usize) -> usize {
result.entries.get(index).map(|e| e.lod_deltas.len()).unwrap_or(0)
}
#[allow(dead_code)]
pub fn mel_compression_ratio(result: &MorphExportLodResult) -> f32 {
if result.total_original_vertices == 0 {
return 0.0;
}
result.total_lod_vertices as f32 / result.total_original_vertices as f32
}
#[allow(dead_code)]
pub fn mel_to_json(mel: &MorphExportLod) -> String {
format!(
"{{\"entry_count\":{},\"lod_ratio\":{},\"delta_threshold\":{}}}",
mel.entries.len(),
mel.config.lod_ratio,
mel.config.delta_threshold
)
}
#[allow(dead_code)]
pub fn mel_write_to_file(result: &MorphExportLodResult, path: &str) -> std::io::Result<usize> {
let content = format!(
"{{\"lod_ratio\":{},\"total_original\":{},\"total_lod\":{},\"entry_count\":{}}}",
result.lod_ratio,
result.total_original_vertices,
result.total_lod_vertices,
result.entries.len()
);
std::fs::write(path, &content)?;
Ok(content.len())
}
#[allow(dead_code)]
pub fn mel_clear(mel: &mut MorphExportLod) {
mel.entries.clear();
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_deltas(n: usize) -> Vec<[f32; 3]> {
(0..n).map(|i| [i as f32 * 0.1, 0.0, 0.0]).collect()
}
#[test]
fn test_add_entry_and_count() {
let mut mel = new_morph_export_lod(default_morph_export_lod_config());
mel_add_entry(&mut mel, "smile", &sample_deltas(100));
assert_eq!(mel_entry_count(&mel), 1);
}
#[test]
fn test_export_reduces_vertices() {
let cfg = MorphExportLodConfig {
lod_ratio: 0.5,
delta_threshold: 0.0,
max_entries: 10,
};
let mut mel = new_morph_export_lod(cfg);
mel_add_entry(&mut mel, "morph", &sample_deltas(10));
let result = mel_export_lod(&mel);
assert!(result.total_lod_vertices <= result.total_original_vertices);
}
#[test]
fn test_compression_ratio_range() {
let mut mel = new_morph_export_lod(default_morph_export_lod_config());
mel_add_entry(&mut mel, "m", &sample_deltas(20));
let result = mel_export_lod(&mel);
let r = mel_compression_ratio(&result);
assert!((0.0..=1.0).contains(&r));
}
#[test]
fn test_empty_result_compression_ratio() {
let mel = new_morph_export_lod(default_morph_export_lod_config());
let result = mel_export_lod(&mel);
assert_eq!(mel_compression_ratio(&result), 0.0);
}
#[test]
fn test_clear_removes_entries() {
let mut mel = new_morph_export_lod(default_morph_export_lod_config());
mel_add_entry(&mut mel, "a", &sample_deltas(10));
mel_clear(&mut mel);
assert_eq!(mel_entry_count(&mel), 0);
}
#[test]
fn test_to_json_contains_lod_ratio() {
let mel = new_morph_export_lod(default_morph_export_lod_config());
let json = mel_to_json(&mel);
assert!(json.contains("lod_ratio"));
}
#[test]
fn test_max_entries_enforced() {
let cfg = MorphExportLodConfig {
lod_ratio: 0.5,
delta_threshold: 0.0,
max_entries: 2,
};
let mut mel = new_morph_export_lod(cfg);
mel_add_entry(&mut mel, "a", &sample_deltas(5));
mel_add_entry(&mut mel, "b", &sample_deltas(5));
mel_add_entry(&mut mel, "c", &sample_deltas(5)); assert_eq!(mel_entry_count(&mel), 2);
}
#[test]
fn test_lod_vertex_count_oob_returns_zero() {
let mel = new_morph_export_lod(default_morph_export_lod_config());
let result = mel_export_lod(&mel);
assert_eq!(mel_lod_vertex_count(&result, 99), 0);
}
#[test]
fn test_threshold_filters_small_deltas() {
let cfg = MorphExportLodConfig {
lod_ratio: 1.0,
delta_threshold: 1.0,
max_entries: 10,
};
let mut mel = new_morph_export_lod(cfg);
mel_add_entry(&mut mel, "m", &sample_deltas(10));
let result = mel_export_lod(&mel);
assert_eq!(result.total_lod_vertices, 0);
}
}