1#[allow(dead_code)]
10#[derive(Debug, Clone)]
11pub struct MorphTargetExport {
12 pub name: String,
14 pub delta_positions: Vec<[f32; 3]>,
16 pub delta_normals: Vec<[f32; 3]>,
18 pub default_weight: f32,
20}
21
22#[allow(dead_code)]
24#[derive(Debug, Clone)]
25pub struct MorphExportConfig {
26 pub threshold: f32,
28 pub include_normals: bool,
30 pub normalize: bool,
32}
33
34#[allow(dead_code)]
36#[derive(Debug, Clone)]
37pub struct MorphExportBundle {
38 pub targets: Vec<MorphTargetExport>,
40 pub config: MorphExportConfig,
42}
43
44#[allow(dead_code)]
48pub fn default_morph_export_config() -> MorphExportConfig {
49 MorphExportConfig {
50 threshold: 1e-6,
51 include_normals: true,
52 normalize: false,
53 }
54}
55
56#[allow(dead_code)]
60pub fn new_morph_target_export(name: &str, vertex_count: usize) -> MorphTargetExport {
61 MorphTargetExport {
62 name: name.to_string(),
63 delta_positions: vec![[0.0; 3]; vertex_count],
64 delta_normals: vec![[0.0; 3]; vertex_count],
65 default_weight: 0.0,
66 }
67}
68
69#[allow(dead_code)]
73pub fn morph_delta_positions(target: &MorphTargetExport) -> &[[f32; 3]] {
74 &target.delta_positions
75}
76
77#[allow(dead_code)]
79pub fn morph_delta_normals(target: &MorphTargetExport) -> &[[f32; 3]] {
80 &target.delta_normals
81}
82
83#[allow(dead_code)]
85pub fn morph_target_count(bundle: &MorphExportBundle) -> usize {
86 bundle.targets.len()
87}
88
89#[allow(dead_code)]
91pub fn morph_target_name(bundle: &MorphExportBundle, index: usize) -> &str {
92 bundle
93 .targets
94 .get(index)
95 .map(|t| t.name.as_str())
96 .unwrap_or("")
97}
98
99#[allow(dead_code)]
103pub fn pack_morph_bundle(
104 targets: &[MorphTargetExport],
105 config: &MorphExportConfig,
106) -> MorphExportBundle {
107 let mut out_targets: Vec<MorphTargetExport> = Vec::with_capacity(targets.len());
108 for t in targets {
109 let mut target = t.clone();
110 if config.normalize {
111 normalize_morph_deltas_internal(&mut target.delta_positions);
112 }
113 if !config.include_normals {
114 target.delta_normals.clear();
115 }
116 if config.threshold > 0.0 {
117 filter_deltas_by_threshold(&mut target.delta_positions, config.threshold);
118 if !target.delta_normals.is_empty() {
119 filter_deltas_by_threshold(&mut target.delta_normals, config.threshold);
120 }
121 }
122 out_targets.push(target);
123 }
124 MorphExportBundle {
125 targets: out_targets,
126 config: config.clone(),
127 }
128}
129
130#[allow(dead_code)]
132pub fn morph_bundle_to_json(bundle: &MorphExportBundle) -> String {
133 let mut s = String::from("{\n \"morph_targets\": [\n");
134 for (i, t) in bundle.targets.iter().enumerate() {
135 s.push_str(&format!(
136 " {{\"name\":\"{}\",\"vertex_count\":{},\"has_normals\":{},\"default_weight\":{:.6}}}",
137 t.name,
138 t.delta_positions.len(),
139 !t.delta_normals.is_empty(),
140 t.default_weight,
141 ));
142 if i + 1 < bundle.targets.len() {
143 s.push(',');
144 }
145 s.push('\n');
146 }
147 s.push_str(" ],\n");
148 s.push_str(&format!(
149 " \"config\":{{\"threshold\":{:.8},\"include_normals\":{},\"normalize\":{}}}\n",
150 bundle.config.threshold, bundle.config.include_normals, bundle.config.normalize,
151 ));
152 s.push('}');
153 s
154}
155
156#[allow(dead_code)]
160pub fn morph_delta_magnitude(delta: [f32; 3]) -> f32 {
161 (delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2]).sqrt()
162}
163
164#[allow(dead_code)]
167pub fn normalize_morph_deltas(target: &mut MorphTargetExport) {
168 normalize_morph_deltas_internal(&mut target.delta_positions);
169}
170
171#[allow(dead_code)]
174pub fn filter_morph_by_threshold(target: &mut MorphTargetExport, threshold: f32) {
175 filter_deltas_by_threshold(&mut target.delta_positions, threshold);
176 if !target.delta_normals.is_empty() {
177 filter_deltas_by_threshold(&mut target.delta_normals, threshold);
178 }
179}
180
181#[allow(dead_code)]
183pub fn morph_weight_range() -> (f32, f32) {
184 (0.0, 1.0)
185}
186
187#[allow(dead_code)]
193pub fn morph_export_size_bytes(bundle: &MorphExportBundle) -> usize {
194 let mut total = 0usize;
195 for t in &bundle.targets {
196 total += t.delta_positions.len() * 3 * 4;
197 if !t.delta_normals.is_empty() {
198 total += t.delta_normals.len() * 3 * 4;
199 }
200 }
201 total
202}
203
204fn normalize_morph_deltas_internal(deltas: &mut [[f32; 3]]) {
207 let max_mag = deltas
208 .iter()
209 .map(|d| morph_delta_magnitude(*d))
210 .fold(0.0_f32, f32::max);
211 if max_mag < 1e-12 {
212 return;
213 }
214 let inv = 1.0 / max_mag;
215 for d in deltas.iter_mut() {
216 d[0] *= inv;
217 d[1] *= inv;
218 d[2] *= inv;
219 }
220}
221
222fn filter_deltas_by_threshold(deltas: &mut [[f32; 3]], threshold: f32) {
223 for d in deltas.iter_mut() {
224 if morph_delta_magnitude(*d) < threshold {
225 *d = [0.0; 3];
226 }
227 }
228}
229
230#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn default_config_values() {
238 let cfg = default_morph_export_config();
239 assert!(cfg.include_normals);
240 assert!(!cfg.normalize);
241 assert!(cfg.threshold > 0.0);
242 }
243
244 #[test]
245 fn new_morph_target_has_correct_size() {
246 let t = new_morph_target_export("smile", 100);
247 assert_eq!(t.name, "smile");
248 assert_eq!(t.delta_positions.len(), 100);
249 assert_eq!(t.delta_normals.len(), 100);
250 assert!((t.default_weight - 0.0).abs() < 1e-6);
251 }
252
253 #[test]
254 fn morph_delta_positions_accessor() {
255 let t = new_morph_target_export("test", 5);
256 let dp = morph_delta_positions(&t);
257 assert_eq!(dp.len(), 5);
258 }
259
260 #[test]
261 fn morph_delta_normals_accessor() {
262 let t = new_morph_target_export("test", 5);
263 let dn = morph_delta_normals(&t);
264 assert_eq!(dn.len(), 5);
265 }
266
267 #[test]
268 fn morph_delta_magnitude_basic() {
269 assert!((morph_delta_magnitude([3.0, 4.0, 0.0]) - 5.0).abs() < 1e-6);
270 }
271
272 #[test]
273 fn morph_delta_magnitude_zero() {
274 assert!((morph_delta_magnitude([0.0, 0.0, 0.0])).abs() < 1e-6);
275 }
276
277 #[test]
278 fn pack_morph_bundle_preserves_count() {
279 let targets = vec![
280 new_morph_target_export("a", 10),
281 new_morph_target_export("b", 10),
282 ];
283 let cfg = default_morph_export_config();
284 let bundle = pack_morph_bundle(&targets, &cfg);
285 assert_eq!(morph_target_count(&bundle), 2);
286 }
287
288 #[test]
289 fn pack_morph_bundle_strips_normals_when_disabled() {
290 let targets = vec![new_morph_target_export("a", 10)];
291 let mut cfg = default_morph_export_config();
292 cfg.include_normals = false;
293 let bundle = pack_morph_bundle(&targets, &cfg);
294 assert!(bundle.targets[0].delta_normals.is_empty());
295 }
296
297 #[test]
298 fn morph_target_name_valid_index() {
299 let targets = vec![new_morph_target_export("blink", 5)];
300 let cfg = default_morph_export_config();
301 let bundle = pack_morph_bundle(&targets, &cfg);
302 assert_eq!(morph_target_name(&bundle, 0), "blink");
303 }
304
305 #[test]
306 fn morph_target_name_invalid_index() {
307 let targets = vec![new_morph_target_export("blink", 5)];
308 let cfg = default_morph_export_config();
309 let bundle = pack_morph_bundle(&targets, &cfg);
310 assert_eq!(morph_target_name(&bundle, 99), "");
311 }
312
313 #[test]
314 fn normalize_morph_deltas_max_is_one() {
315 let mut t = new_morph_target_export("test", 3);
316 t.delta_positions[0] = [3.0, 0.0, 0.0];
317 t.delta_positions[1] = [0.0, 4.0, 0.0];
318 t.delta_positions[2] = [0.0, 0.0, 5.0];
319 normalize_morph_deltas(&mut t);
320 let max_mag = t
321 .delta_positions
322 .iter()
323 .map(|d| morph_delta_magnitude(*d))
324 .fold(0.0_f32, f32::max);
325 assert!((max_mag - 1.0).abs() < 1e-5);
326 }
327
328 #[test]
329 fn normalize_morph_deltas_all_zero_is_noop() {
330 let mut t = new_morph_target_export("test", 3);
331 normalize_morph_deltas(&mut t);
332 for d in &t.delta_positions {
333 assert_eq!(*d, [0.0; 3]);
334 }
335 }
336
337 #[test]
338 fn filter_morph_by_threshold_zeroes_small() {
339 let mut t = new_morph_target_export("test", 3);
340 t.delta_positions[0] = [0.001, 0.0, 0.0];
341 t.delta_positions[1] = [1.0, 0.0, 0.0];
342 t.delta_positions[2] = [0.0005, 0.0, 0.0];
343 filter_morph_by_threshold(&mut t, 0.01);
344 assert_eq!(t.delta_positions[0], [0.0; 3]);
345 assert!((t.delta_positions[1][0] - 1.0).abs() < 1e-6);
346 assert_eq!(t.delta_positions[2], [0.0; 3]);
347 }
348
349 #[test]
350 fn morph_weight_range_is_zero_to_one() {
351 let (lo, hi) = morph_weight_range();
352 assert!((lo - 0.0).abs() < 1e-6);
353 assert!((hi - 1.0).abs() < 1e-6);
354 }
355
356 #[test]
357 fn morph_export_size_bytes_calculation() {
358 let targets = vec![new_morph_target_export("a", 10)];
359 let cfg = default_morph_export_config();
360 let bundle = pack_morph_bundle(&targets, &cfg);
361 assert_eq!(morph_export_size_bytes(&bundle), 240);
363 }
364
365 #[test]
366 fn morph_export_size_bytes_no_normals() {
367 let targets = vec![new_morph_target_export("a", 10)];
368 let mut cfg = default_morph_export_config();
369 cfg.include_normals = false;
370 let bundle = pack_morph_bundle(&targets, &cfg);
371 assert_eq!(morph_export_size_bytes(&bundle), 120);
372 }
373
374 #[test]
375 fn morph_bundle_to_json_contains_name() {
376 let targets = vec![new_morph_target_export("jaw_open", 5)];
377 let cfg = default_morph_export_config();
378 let bundle = pack_morph_bundle(&targets, &cfg);
379 let json = morph_bundle_to_json(&bundle);
380 assert!(json.contains("jaw_open"));
381 assert!(json.contains("morph_targets"));
382 }
383
384 #[test]
385 fn morph_bundle_to_json_multiple_targets() {
386 let targets = vec![
387 new_morph_target_export("a", 5),
388 new_morph_target_export("b", 5),
389 ];
390 let cfg = default_morph_export_config();
391 let bundle = pack_morph_bundle(&targets, &cfg);
392 let json = morph_bundle_to_json(&bundle);
393 assert!(json.contains("\"a\""));
394 assert!(json.contains("\"b\""));
395 }
396}