#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct MorphTargetExport {
pub name: String,
pub delta_positions: Vec<[f32; 3]>,
pub delta_normals: Vec<[f32; 3]>,
pub default_weight: f32,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct MorphExportConfig {
pub threshold: f32,
pub include_normals: bool,
pub normalize: bool,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct MorphExportBundle {
pub targets: Vec<MorphTargetExport>,
pub config: MorphExportConfig,
}
#[allow(dead_code)]
pub fn default_morph_export_config() -> MorphExportConfig {
MorphExportConfig {
threshold: 1e-6,
include_normals: true,
normalize: false,
}
}
#[allow(dead_code)]
pub fn new_morph_target_export(name: &str, vertex_count: usize) -> MorphTargetExport {
MorphTargetExport {
name: name.to_string(),
delta_positions: vec![[0.0; 3]; vertex_count],
delta_normals: vec![[0.0; 3]; vertex_count],
default_weight: 0.0,
}
}
#[allow(dead_code)]
pub fn morph_delta_positions(target: &MorphTargetExport) -> &[[f32; 3]] {
&target.delta_positions
}
#[allow(dead_code)]
pub fn morph_delta_normals(target: &MorphTargetExport) -> &[[f32; 3]] {
&target.delta_normals
}
#[allow(dead_code)]
pub fn morph_target_count(bundle: &MorphExportBundle) -> usize {
bundle.targets.len()
}
#[allow(dead_code)]
pub fn morph_target_name(bundle: &MorphExportBundle, index: usize) -> &str {
bundle
.targets
.get(index)
.map(|t| t.name.as_str())
.unwrap_or("")
}
#[allow(dead_code)]
pub fn pack_morph_bundle(
targets: &[MorphTargetExport],
config: &MorphExportConfig,
) -> MorphExportBundle {
let mut out_targets: Vec<MorphTargetExport> = Vec::with_capacity(targets.len());
for t in targets {
let mut target = t.clone();
if config.normalize {
normalize_morph_deltas_internal(&mut target.delta_positions);
}
if !config.include_normals {
target.delta_normals.clear();
}
if config.threshold > 0.0 {
filter_deltas_by_threshold(&mut target.delta_positions, config.threshold);
if !target.delta_normals.is_empty() {
filter_deltas_by_threshold(&mut target.delta_normals, config.threshold);
}
}
out_targets.push(target);
}
MorphExportBundle {
targets: out_targets,
config: config.clone(),
}
}
#[allow(dead_code)]
pub fn morph_bundle_to_json(bundle: &MorphExportBundle) -> String {
let mut s = String::from("{\n \"morph_targets\": [\n");
for (i, t) in bundle.targets.iter().enumerate() {
s.push_str(&format!(
" {{\"name\":\"{}\",\"vertex_count\":{},\"has_normals\":{},\"default_weight\":{:.6}}}",
t.name,
t.delta_positions.len(),
!t.delta_normals.is_empty(),
t.default_weight,
));
if i + 1 < bundle.targets.len() {
s.push(',');
}
s.push('\n');
}
s.push_str(" ],\n");
s.push_str(&format!(
" \"config\":{{\"threshold\":{:.8},\"include_normals\":{},\"normalize\":{}}}\n",
bundle.config.threshold, bundle.config.include_normals, bundle.config.normalize,
));
s.push('}');
s
}
#[allow(dead_code)]
pub fn morph_delta_magnitude(delta: [f32; 3]) -> f32 {
(delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2]).sqrt()
}
#[allow(dead_code)]
pub fn normalize_morph_deltas(target: &mut MorphTargetExport) {
normalize_morph_deltas_internal(&mut target.delta_positions);
}
#[allow(dead_code)]
pub fn filter_morph_by_threshold(target: &mut MorphTargetExport, threshold: f32) {
filter_deltas_by_threshold(&mut target.delta_positions, threshold);
if !target.delta_normals.is_empty() {
filter_deltas_by_threshold(&mut target.delta_normals, threshold);
}
}
#[allow(dead_code)]
pub fn morph_weight_range() -> (f32, f32) {
(0.0, 1.0)
}
#[allow(dead_code)]
pub fn morph_export_size_bytes(bundle: &MorphExportBundle) -> usize {
let mut total = 0usize;
for t in &bundle.targets {
total += t.delta_positions.len() * 3 * 4;
if !t.delta_normals.is_empty() {
total += t.delta_normals.len() * 3 * 4;
}
}
total
}
fn normalize_morph_deltas_internal(deltas: &mut [[f32; 3]]) {
let max_mag = deltas
.iter()
.map(|d| morph_delta_magnitude(*d))
.fold(0.0_f32, f32::max);
if max_mag < 1e-12 {
return;
}
let inv = 1.0 / max_mag;
for d in deltas.iter_mut() {
d[0] *= inv;
d[1] *= inv;
d[2] *= inv;
}
}
fn filter_deltas_by_threshold(deltas: &mut [[f32; 3]], threshold: f32) {
for d in deltas.iter_mut() {
if morph_delta_magnitude(*d) < threshold {
*d = [0.0; 3];
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_values() {
let cfg = default_morph_export_config();
assert!(cfg.include_normals);
assert!(!cfg.normalize);
assert!(cfg.threshold > 0.0);
}
#[test]
fn new_morph_target_has_correct_size() {
let t = new_morph_target_export("smile", 100);
assert_eq!(t.name, "smile");
assert_eq!(t.delta_positions.len(), 100);
assert_eq!(t.delta_normals.len(), 100);
assert!((t.default_weight - 0.0).abs() < 1e-6);
}
#[test]
fn morph_delta_positions_accessor() {
let t = new_morph_target_export("test", 5);
let dp = morph_delta_positions(&t);
assert_eq!(dp.len(), 5);
}
#[test]
fn morph_delta_normals_accessor() {
let t = new_morph_target_export("test", 5);
let dn = morph_delta_normals(&t);
assert_eq!(dn.len(), 5);
}
#[test]
fn morph_delta_magnitude_basic() {
assert!((morph_delta_magnitude([3.0, 4.0, 0.0]) - 5.0).abs() < 1e-6);
}
#[test]
fn morph_delta_magnitude_zero() {
assert!((morph_delta_magnitude([0.0, 0.0, 0.0])).abs() < 1e-6);
}
#[test]
fn pack_morph_bundle_preserves_count() {
let targets = vec![
new_morph_target_export("a", 10),
new_morph_target_export("b", 10),
];
let cfg = default_morph_export_config();
let bundle = pack_morph_bundle(&targets, &cfg);
assert_eq!(morph_target_count(&bundle), 2);
}
#[test]
fn pack_morph_bundle_strips_normals_when_disabled() {
let targets = vec![new_morph_target_export("a", 10)];
let mut cfg = default_morph_export_config();
cfg.include_normals = false;
let bundle = pack_morph_bundle(&targets, &cfg);
assert!(bundle.targets[0].delta_normals.is_empty());
}
#[test]
fn morph_target_name_valid_index() {
let targets = vec![new_morph_target_export("blink", 5)];
let cfg = default_morph_export_config();
let bundle = pack_morph_bundle(&targets, &cfg);
assert_eq!(morph_target_name(&bundle, 0), "blink");
}
#[test]
fn morph_target_name_invalid_index() {
let targets = vec![new_morph_target_export("blink", 5)];
let cfg = default_morph_export_config();
let bundle = pack_morph_bundle(&targets, &cfg);
assert_eq!(morph_target_name(&bundle, 99), "");
}
#[test]
fn normalize_morph_deltas_max_is_one() {
let mut t = new_morph_target_export("test", 3);
t.delta_positions[0] = [3.0, 0.0, 0.0];
t.delta_positions[1] = [0.0, 4.0, 0.0];
t.delta_positions[2] = [0.0, 0.0, 5.0];
normalize_morph_deltas(&mut t);
let max_mag = t
.delta_positions
.iter()
.map(|d| morph_delta_magnitude(*d))
.fold(0.0_f32, f32::max);
assert!((max_mag - 1.0).abs() < 1e-5);
}
#[test]
fn normalize_morph_deltas_all_zero_is_noop() {
let mut t = new_morph_target_export("test", 3);
normalize_morph_deltas(&mut t);
for d in &t.delta_positions {
assert_eq!(*d, [0.0; 3]);
}
}
#[test]
fn filter_morph_by_threshold_zeroes_small() {
let mut t = new_morph_target_export("test", 3);
t.delta_positions[0] = [0.001, 0.0, 0.0];
t.delta_positions[1] = [1.0, 0.0, 0.0];
t.delta_positions[2] = [0.0005, 0.0, 0.0];
filter_morph_by_threshold(&mut t, 0.01);
assert_eq!(t.delta_positions[0], [0.0; 3]);
assert!((t.delta_positions[1][0] - 1.0).abs() < 1e-6);
assert_eq!(t.delta_positions[2], [0.0; 3]);
}
#[test]
fn morph_weight_range_is_zero_to_one() {
let (lo, hi) = morph_weight_range();
assert!((lo - 0.0).abs() < 1e-6);
assert!((hi - 1.0).abs() < 1e-6);
}
#[test]
fn morph_export_size_bytes_calculation() {
let targets = vec![new_morph_target_export("a", 10)];
let cfg = default_morph_export_config();
let bundle = pack_morph_bundle(&targets, &cfg);
assert_eq!(morph_export_size_bytes(&bundle), 240);
}
#[test]
fn morph_export_size_bytes_no_normals() {
let targets = vec![new_morph_target_export("a", 10)];
let mut cfg = default_morph_export_config();
cfg.include_normals = false;
let bundle = pack_morph_bundle(&targets, &cfg);
assert_eq!(morph_export_size_bytes(&bundle), 120);
}
#[test]
fn morph_bundle_to_json_contains_name() {
let targets = vec![new_morph_target_export("jaw_open", 5)];
let cfg = default_morph_export_config();
let bundle = pack_morph_bundle(&targets, &cfg);
let json = morph_bundle_to_json(&bundle);
assert!(json.contains("jaw_open"));
assert!(json.contains("morph_targets"));
}
#[test]
fn morph_bundle_to_json_multiple_targets() {
let targets = vec![
new_morph_target_export("a", 5),
new_morph_target_export("b", 5),
];
let cfg = default_morph_export_config();
let bundle = pack_morph_bundle(&targets, &cfg);
let json = morph_bundle_to_json(&bundle);
assert!(json.contains("\"a\""));
assert!(json.contains("\"b\""));
}
}