1#![allow(dead_code)]
5
6use crate::params::ParamState;
7use std::collections::HashMap;
8
9pub type MorphMap = HashMap<String, f32>;
11
12pub struct SkinDeformPattern {
18 pub name: String,
19 pub region: String,
21 pub driver_params: Vec<String>,
23 pub base_weights: MorphMap,
25 pub max_weights: MorphMap,
27}
28
29impl SkinDeformPattern {
30 pub fn evaluate(&self, t: f32) -> MorphMap {
32 let t = t.clamp(0.0, 1.0);
33 let mut out: MorphMap = self.base_weights.clone();
34 for (k, v_max) in &self.max_weights {
35 let v_base = self.base_weights.get(k).copied().unwrap_or(0.0);
36 out.insert(k.clone(), v_base + (v_max - v_base) * t);
37 }
38 out
39 }
40
41 pub fn name(&self) -> &str {
42 &self.name
43 }
44
45 pub fn region(&self) -> &str {
46 &self.region
47 }
48}
49
50pub struct SkinDeformSystem {
56 patterns: Vec<SkinDeformPattern>,
57}
58
59impl SkinDeformSystem {
60 pub fn new() -> Self {
61 SkinDeformSystem {
62 patterns: Vec::new(),
63 }
64 }
65
66 pub fn add_pattern(&mut self, p: SkinDeformPattern) {
67 self.patterns.push(p);
68 }
69
70 pub fn evaluate_all(&self, params: &ParamState) -> MorphMap {
73 let mut out: MorphMap = HashMap::new();
74 for pat in &self.patterns {
75 let t = if pat.driver_params.is_empty() {
76 0.0_f32
77 } else {
78 let sum: f32 = pat
79 .driver_params
80 .iter()
81 .map(|k| driver_value(params, k))
82 .sum();
83 (sum / pat.driver_params.len() as f32).clamp(0.0, 1.0)
84 };
85 for (k, w) in pat.evaluate(t) {
86 let entry = out.entry(k).or_insert(0.0);
87 *entry = (*entry + w).clamp(0.0, 1.0);
88 }
89 }
90 out
91 }
92
93 pub fn pattern_count(&self) -> usize {
94 self.patterns.len()
95 }
96
97 pub fn find_pattern(&self, name: &str) -> Option<&SkinDeformPattern> {
98 self.patterns.iter().find(|p| p.name == name)
99 }
100}
101
102impl Default for SkinDeformSystem {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108fn driver_value(params: &ParamState, key: &str) -> f32 {
113 match key {
114 "muscle" => params.muscle,
115 "weight" => params.weight,
116 "age" => params.age,
117 _ => params.extra.get(key).copied().unwrap_or(0.0),
118 }
119}
120
121pub fn wrinkle_weights(bend_angle: f32, max_angle: f32, morph_prefix: &str) -> MorphMap {
130 let mut map = MorphMap::new();
131 if max_angle <= 0.0 {
132 return map;
133 }
134 let t = (bend_angle / max_angle).clamp(0.0, 1.0);
135 let inner = t * t;
137 let outer = t * (1.0 - t * 0.5);
138 map.insert(format!("{morph_prefix}_inner"), inner);
139 map.insert(format!("{morph_prefix}_outer"), outer);
140 map.insert(format!("{morph_prefix}_crease"), t);
141 map
142}
143
144#[allow(clippy::too_many_arguments)]
148pub fn bulge_weights(muscle_activation: f32, region: &str) -> MorphMap {
149 let mut map = MorphMap::new();
150 let t = muscle_activation.clamp(0.0, 1.0);
151 let bulge = (t * 1.333).clamp(0.0, 1.0);
153 let stretch = t * 0.4;
154 map.insert(format!("{region}_bulge"), bulge);
155 map.insert(format!("{region}_stretch"), stretch);
156 map.insert(format!("{region}_vein"), (t - 0.6).max(0.0) * 2.5);
157 map
158}
159
160pub fn sag_weights(bmi: f32, age: f32, gravity_axis: u8) -> MorphMap {
165 let mut map = MorphMap::new();
166 let b = bmi.clamp(0.0, 1.0);
167 let a = age.clamp(0.0, 1.0);
168 let sag = b * 0.6 + a * 0.4;
170 let axis_tag = match gravity_axis {
171 0 => "down",
172 1 => "side",
173 _ => "prone",
174 };
175 map.insert(format!("belly_sag_{axis_tag}"), sag);
176 map.insert(format!("chest_sag_{axis_tag}"), sag * 0.7);
177 map.insert(
178 format!("jowl_sag_{axis_tag}"),
179 (a * 0.5 + b * 0.3).clamp(0.0, 1.0),
180 );
181 map.insert(format!("buttock_sag_{axis_tag}"), sag * 0.8);
182 map
183}
184
185pub fn blend_skin_maps(a: &MorphMap, b: &MorphMap, t: f32) -> MorphMap {
187 let t = t.clamp(0.0, 1.0);
188 let mut out = MorphMap::new();
189 for (k, va) in a {
191 let vb = b.get(k).copied().unwrap_or(0.0);
192 out.insert(k.clone(), va + (vb - va) * t);
193 }
194 for (k, vb) in b {
196 if !a.contains_key(k) {
197 out.insert(k.clone(), vb * t);
198 }
199 }
200 out
201}
202
203pub fn clamp_skin_map(map: &MorphMap, lo: f32, hi: f32) -> MorphMap {
205 map.iter()
206 .map(|(k, v)| (k.clone(), v.clamp(lo, hi)))
207 .collect()
208}
209
210fn make_pattern(
215 name: &str,
216 region: &str,
217 drivers: &[&str],
218 base: &[(&str, f32)],
219 max: &[(&str, f32)],
220) -> SkinDeformPattern {
221 SkinDeformPattern {
222 name: name.to_string(),
223 region: region.to_string(),
224 driver_params: drivers.iter().map(|s| s.to_string()).collect(),
225 base_weights: base.iter().map(|(k, v)| (k.to_string(), *v)).collect(),
226 max_weights: max.iter().map(|(k, v)| (k.to_string(), *v)).collect(),
227 }
228}
229
230pub fn default_skin_system() -> SkinDeformSystem {
233 let mut sys = SkinDeformSystem::new();
234
235 sys.add_pattern(make_pattern(
237 "elbow_bend",
238 "forearm",
239 &["elbow_flex"],
240 &[("elbow_wrinkle_inner", 0.0), ("elbow_wrinkle_outer", 0.0)],
241 &[("elbow_wrinkle_inner", 1.0), ("elbow_wrinkle_outer", 0.6)],
242 ));
243
244 sys.add_pattern(make_pattern(
246 "knee_bend",
247 "lower_leg",
248 &["knee_flex"],
249 &[("knee_wrinkle_inner", 0.0), ("knee_wrinkle_outer", 0.0)],
250 &[("knee_wrinkle_inner", 1.0), ("knee_wrinkle_outer", 0.5)],
251 ));
252
253 sys.add_pattern(make_pattern(
255 "cheek_squash",
256 "cheek",
257 &["cheek_squash"],
258 &[("cheek_bulge", 0.0), ("nasolabial_fold", 0.0)],
259 &[("cheek_bulge", 0.9), ("nasolabial_fold", 0.7)],
260 ));
261
262 sys.add_pattern(make_pattern(
264 "belly_sag",
265 "belly",
266 &["weight", "age"],
267 &[("belly_sag_down", 0.0), ("belly_overhang", 0.0)],
268 &[("belly_sag_down", 1.0), ("belly_overhang", 0.8)],
269 ));
270
271 sys.add_pattern(make_pattern(
273 "bicep_bulge",
274 "upper_arm",
275 &["muscle"],
276 &[("bicep_bulge", 0.0), ("bicep_vein", 0.0)],
277 &[("bicep_bulge", 1.0), ("bicep_vein", 0.6)],
278 ));
279
280 sys.add_pattern(make_pattern(
282 "shoulder_wrinkle",
283 "shoulder",
284 &["shoulder_raise"],
285 &[("shoulder_wrinkle_top", 0.0), ("deltoid_crease", 0.0)],
286 &[("shoulder_wrinkle_top", 0.8), ("deltoid_crease", 0.5)],
287 ));
288
289 sys.add_pattern(make_pattern(
291 "neck_wrinkle",
292 "neck",
293 &["age"],
294 &[("neck_wrinkle_h", 0.0), ("neck_wrinkle_v", 0.0)],
295 &[("neck_wrinkle_h", 0.9), ("neck_wrinkle_v", 0.5)],
296 ));
297
298 sys.add_pattern(make_pattern(
300 "brow_compression",
301 "forehead",
302 &["brow_compress"],
303 &[("glabellar_crease", 0.0), ("brow_furrow", 0.0)],
304 &[("glabellar_crease", 1.0), ("brow_furrow", 0.8)],
305 ));
306
307 sys
308}
309
310#[cfg(test)]
315mod tests {
316 use super::*;
317
318 fn make_param(muscle: f32, weight: f32, age: f32) -> ParamState {
319 ParamState::new(0.5, weight, muscle, age)
320 }
321
322 #[test]
325 fn evaluate_at_zero_returns_base() {
326 let pat = make_pattern("test", "arm", &[], &[("a", 0.2)], &[("a", 0.8)]);
327 let result = pat.evaluate(0.0);
328 let v = *result.get("a").expect("should succeed");
329 assert!((v - 0.2).abs() < 1e-5, "expected 0.2, got {v}");
330 }
331
332 #[test]
333 fn evaluate_at_one_returns_max() {
334 let pat = make_pattern("test", "arm", &[], &[("a", 0.2)], &[("a", 0.8)]);
335 let result = pat.evaluate(1.0);
336 let v = *result.get("a").expect("should succeed");
337 assert!((v - 0.8).abs() < 1e-5, "expected 0.8, got {v}");
338 }
339
340 #[test]
341 fn evaluate_at_half_is_midpoint() {
342 let pat = make_pattern("test", "leg", &[], &[("x", 0.0)], &[("x", 1.0)]);
343 let result = pat.evaluate(0.5);
344 let v = *result.get("x").expect("should succeed");
345 assert!((v - 0.5).abs() < 1e-5);
346 }
347
348 #[test]
349 fn evaluate_clamps_t_above_one() {
350 let pat = make_pattern("test", "leg", &[], &[("x", 0.0)], &[("x", 1.0)]);
351 let result = pat.evaluate(2.0);
352 let v = *result.get("x").expect("should succeed");
353 assert!((v - 1.0).abs() < 1e-5, "should clamp to 1.0");
354 }
355
356 #[test]
357 fn evaluate_clamps_t_below_zero() {
358 let pat = make_pattern("test", "leg", &[], &[("x", 0.0)], &[("x", 1.0)]);
359 let result = pat.evaluate(-1.0);
360 let v = *result.get("x").expect("should succeed");
361 assert!((v - 0.0).abs() < 1e-5, "should clamp to 0.0");
362 }
363
364 #[test]
365 fn evaluate_includes_base_only_keys() {
366 let pat = make_pattern("test", "face", &[], &[("base_only", 0.3)], &[]);
367 let result = pat.evaluate(0.5);
368 let v = *result.get("base_only").expect("should succeed");
370 assert!((v - 0.3).abs() < 1e-5);
371 }
372
373 #[test]
376 fn system_starts_empty() {
377 let sys = SkinDeformSystem::new();
378 assert_eq!(sys.pattern_count(), 0);
379 }
380
381 #[test]
382 fn add_pattern_increments_count() {
383 let mut sys = SkinDeformSystem::new();
384 let pat = make_pattern("p1", "arm", &[], &[], &[]);
385 sys.add_pattern(pat);
386 assert_eq!(sys.pattern_count(), 1);
387 }
388
389 #[test]
390 fn find_pattern_returns_none_for_missing() {
391 let sys = SkinDeformSystem::new();
392 assert!(sys.find_pattern("ghost").is_none());
393 }
394
395 #[test]
396 fn find_pattern_returns_correct_region() {
397 let mut sys = SkinDeformSystem::new();
398 sys.add_pattern(make_pattern("elbow", "forearm", &[], &[], &[]));
399 let found = sys.find_pattern("elbow").expect("should succeed");
400 assert_eq!(found.region(), "forearm");
401 }
402
403 #[test]
404 fn evaluate_all_zero_drivers_yields_base_weights() {
405 let mut sys = SkinDeformSystem::new();
406 sys.add_pattern(make_pattern(
407 "neck",
408 "neck",
409 &["age"],
410 &[("neck_crease", 0.1)],
411 &[("neck_crease", 0.9)],
412 ));
413 let params = make_param(0.0, 0.0, 0.0); let result = sys.evaluate_all(¶ms);
415 let v = *result.get("neck_crease").expect("should succeed");
416 assert!((v - 0.1).abs() < 1e-5, "expected base=0.1, got {v}");
417 }
418
419 #[test]
420 fn evaluate_all_full_muscle_gives_max_bicep() {
421 let mut sys = SkinDeformSystem::new();
422 sys.add_pattern(make_pattern(
423 "bicep",
424 "upper_arm",
425 &["muscle"],
426 &[("bicep_bulge", 0.0)],
427 &[("bicep_bulge", 1.0)],
428 ));
429 let params = make_param(1.0, 0.5, 0.5);
430 let result = sys.evaluate_all(¶ms);
431 let v = *result.get("bicep_bulge").expect("should succeed");
432 assert!((v - 1.0).abs() < 1e-5);
433 }
434
435 #[test]
438 fn wrinkle_weights_zero_bend_is_zero() {
439 let w = wrinkle_weights(0.0, 180.0, "elbow_wrinkle");
440 let inner = *w.get("elbow_wrinkle_inner").expect("should succeed");
441 assert!(inner.abs() < 1e-6);
442 }
443
444 #[test]
445 fn wrinkle_weights_full_bend_inner_is_one() {
446 let w = wrinkle_weights(180.0, 180.0, "elbow_wrinkle");
447 let inner = *w.get("elbow_wrinkle_inner").expect("should succeed");
448 assert!((inner - 1.0).abs() < 1e-5);
449 }
450
451 #[test]
452 fn wrinkle_weights_max_angle_zero_returns_empty() {
453 let w = wrinkle_weights(90.0, 0.0, "x");
454 assert!(w.is_empty());
455 }
456
457 #[test]
460 fn bulge_weights_zero_activation() {
461 let w = bulge_weights(0.0, "bicep");
462 let b = *w.get("bicep_bulge").expect("should succeed");
463 assert!(b.abs() < 1e-6);
464 }
465
466 #[test]
467 fn bulge_weights_full_activation_clamps_to_one() {
468 let w = bulge_weights(1.0, "bicep");
469 let b = *w.get("bicep_bulge").expect("should succeed");
470 assert!(b <= 1.0 + 1e-6);
471 }
472
473 #[test]
476 fn sag_weights_zero_params_minimal_sag() {
477 let w = sag_weights(0.0, 0.0, 0);
478 let sag = *w.get("belly_sag_down").expect("should succeed");
479 assert!(sag.abs() < 1e-6);
480 }
481
482 #[test]
483 fn sag_weights_keys_contain_axis_tag() {
484 let w = sag_weights(0.5, 0.5, 1);
485 assert!(w.contains_key("belly_sag_side"));
486 assert!(w.contains_key("chest_sag_side"));
487 }
488
489 #[test]
492 fn blend_skin_maps_at_zero_returns_a() {
493 let a: MorphMap = [("k".to_string(), 0.2)].into();
494 let b: MorphMap = [("k".to_string(), 0.8)].into();
495 let r = blend_skin_maps(&a, &b, 0.0);
496 assert!((r["k"] - 0.2).abs() < 1e-5);
497 }
498
499 #[test]
500 fn blend_skin_maps_at_one_returns_b() {
501 let a: MorphMap = [("k".to_string(), 0.2)].into();
502 let b: MorphMap = [("k".to_string(), 0.8)].into();
503 let r = blend_skin_maps(&a, &b, 1.0);
504 assert!((r["k"] - 0.8).abs() < 1e-5);
505 }
506
507 #[test]
508 fn blend_skin_maps_b_only_key_scales_by_t() {
509 let a: MorphMap = HashMap::new();
510 let b: MorphMap = [("only_b".to_string(), 1.0)].into();
511 let r = blend_skin_maps(&a, &b, 0.5);
512 assert!((r["only_b"] - 0.5).abs() < 1e-5);
513 }
514
515 #[test]
518 fn clamp_skin_map_clamps_above_hi() {
519 let m: MorphMap = [("a".to_string(), 2.0)].into();
520 let c = clamp_skin_map(&m, 0.0, 1.0);
521 assert!((c["a"] - 1.0).abs() < 1e-5);
522 }
523
524 #[test]
525 fn clamp_skin_map_clamps_below_lo() {
526 let m: MorphMap = [("a".to_string(), -1.0)].into();
527 let c = clamp_skin_map(&m, 0.0, 1.0);
528 assert!((c["a"] - 0.0).abs() < 1e-5);
529 }
530
531 #[test]
534 fn default_skin_system_has_eight_patterns() {
535 let sys = default_skin_system();
536 assert_eq!(sys.pattern_count(), 8);
537 }
538
539 #[test]
540 fn default_skin_system_contains_belly_sag() {
541 let sys = default_skin_system();
542 assert!(sys.find_pattern("belly_sag").is_some());
543 }
544
545 #[test]
546 fn default_skin_system_evaluate_all_no_panic() {
547 let sys = default_skin_system();
548 let params = ParamState::default();
549 let result = sys.evaluate_all(¶ms);
550 for v in result.values() {
552 assert!(*v >= 0.0 && *v <= 1.0, "weight out of range: {v}");
553 }
554 }
555}