1#![allow(dead_code)]
5
6use std::collections::HashMap;
7use std::f32::consts::FRAC_PI_2;
8
9pub enum BulgeDirection {
11 VertexNormal,
13 Fixed([f32; 3]),
15 RadialFrom([f32; 3]),
17}
18
19pub struct Muscle {
21 pub name: String,
22 pub joint_name: String,
24 pub max_flex_angle: f32,
26 pub bulge_amplitude: f32,
28 pub influences: Vec<(u32, f32)>,
30 pub bulge_direction: BulgeDirection,
32}
33
34impl Muscle {
35 pub fn new(name: impl Into<String>, joint_name: impl Into<String>) -> Self {
36 Self {
37 name: name.into(),
38 joint_name: joint_name.into(),
39 max_flex_angle: FRAC_PI_2,
40 bulge_amplitude: 0.02,
41 influences: Vec::new(),
42 bulge_direction: BulgeDirection::VertexNormal,
43 }
44 }
45
46 pub fn with_influences(mut self, influences: Vec<(u32, f32)>) -> Self {
47 self.influences = influences;
48 self
49 }
50
51 pub fn with_amplitude(mut self, amp: f32) -> Self {
52 self.bulge_amplitude = amp;
53 self
54 }
55
56 pub fn with_max_flex(mut self, angle: f32) -> Self {
57 self.max_flex_angle = angle;
58 self
59 }
60
61 pub fn bulge_weight(&self, joint_angle: f32) -> f32 {
63 let t = if self.max_flex_angle == 0.0 {
64 0.0
65 } else {
66 (joint_angle / self.max_flex_angle).clamp(0.0, 1.0)
67 };
68 t * t * (3.0 - 2.0 * t)
70 }
71
72 pub fn compute_displacements(
74 &self,
75 joint_angle: f32,
76 positions: &[[f32; 3]],
77 normals: &[[f32; 3]],
78 ) -> Vec<(u32, [f32; 3])> {
79 let bw = self.bulge_weight(joint_angle);
80 let mut result = Vec::with_capacity(self.influences.len());
81
82 for &(vid, weight) in &self.influences {
83 let w = bw * weight;
84 let idx = vid as usize;
85
86 let dir = match &self.bulge_direction {
87 BulgeDirection::VertexNormal => {
88 if idx < normals.len() {
89 normalize(normals[idx])
90 } else {
91 [0.0, 1.0, 0.0]
92 }
93 }
94 BulgeDirection::Fixed(d) => normalize(*d),
95 BulgeDirection::RadialFrom(center) => {
96 if idx < positions.len() {
97 let p = positions[idx];
98 let v = [p[0] - center[0], p[1] - center[1], p[2] - center[2]];
99 normalize(v)
100 } else {
101 [0.0, 1.0, 0.0]
102 }
103 }
104 };
105
106 let disp = [
107 dir[0] * w * self.bulge_amplitude,
108 dir[1] * w * self.bulge_amplitude,
109 dir[2] * w * self.bulge_amplitude,
110 ];
111 result.push((vid, disp));
112 }
113
114 result
115 }
116}
117
118fn normalize(v: [f32; 3]) -> [f32; 3] {
120 let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
121 if len < 1e-10 {
122 [0.0, 1.0, 0.0]
123 } else {
124 [v[0] / len, v[1] / len, v[2] / len]
125 }
126}
127
128pub struct MuscleSimulator {
130 pub muscles: Vec<Muscle>,
131}
132
133impl MuscleSimulator {
134 pub fn new() -> Self {
135 Self {
136 muscles: Vec::new(),
137 }
138 }
139
140 pub fn add_muscle(&mut self, muscle: Muscle) {
141 self.muscles.push(muscle);
142 }
143
144 pub fn muscle_count(&self) -> usize {
145 self.muscles.len()
146 }
147
148 pub fn muscles_for_joint(&self, joint: &str) -> Vec<&Muscle> {
149 self.muscles
150 .iter()
151 .filter(|m| m.joint_name == joint)
152 .collect()
153 }
154
155 pub fn apply(
157 &self,
158 positions: &[[f32; 3]],
159 normals: &[[f32; 3]],
160 joint_angles: &HashMap<String, f32>,
161 ) -> Vec<[f32; 3]> {
162 let mut result = positions.to_vec();
163
164 for muscle in &self.muscles {
165 let angle = match joint_angles.get(&muscle.joint_name) {
166 Some(&a) => a,
167 None => continue,
168 };
169
170 let displacements = muscle.compute_displacements(angle, positions, normals);
171 for (vid, disp) in displacements {
172 let idx = vid as usize;
173 if idx < result.len() {
174 result[idx][0] += disp[0];
175 result[idx][1] += disp[1];
176 result[idx][2] += disp[2];
177 }
178 }
179 }
180
181 result
182 }
183}
184
185impl Default for MuscleSimulator {
186 fn default() -> Self {
187 Self::new()
188 }
189}
190
191pub fn bicep_muscle(joint_name: impl Into<String>) -> Muscle {
193 Muscle {
194 name: "bicep".to_string(),
195 joint_name: joint_name.into(),
196 max_flex_angle: FRAC_PI_2,
197 bulge_amplitude: 0.02,
198 influences: Vec::new(),
199 bulge_direction: BulgeDirection::VertexNormal,
200 }
201}
202
203pub fn quadricep_muscle(joint_name: impl Into<String>) -> Muscle {
205 Muscle {
206 name: "quadricep".to_string(),
207 joint_name: joint_name.into(),
208 max_flex_angle: FRAC_PI_2,
209 bulge_amplitude: 0.025,
210 influences: Vec::new(),
211 bulge_direction: BulgeDirection::VertexNormal,
212 }
213}
214
215pub fn calf_muscle(joint_name: impl Into<String>) -> Muscle {
217 Muscle {
218 name: "calf".to_string(),
219 joint_name: joint_name.into(),
220 max_flex_angle: FRAC_PI_2,
221 bulge_amplitude: 0.018,
222 influences: Vec::new(),
223 bulge_direction: BulgeDirection::VertexNormal,
224 }
225}
226
227pub fn muscle_from_region(
229 name: impl Into<String>,
230 joint_name: impl Into<String>,
231 positions: &[[f32; 3]],
232 center: [f32; 3],
233 radius: f32,
234 amplitude: f32,
235) -> Muscle {
236 let mut influences = Vec::new();
237
238 for (i, pos) in positions.iter().enumerate() {
239 let dx = pos[0] - center[0];
240 let dy = pos[1] - center[1];
241 let dz = pos[2] - center[2];
242 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
243
244 if dist <= radius {
245 let weight = if radius > 0.0 {
247 (1.0 - dist / radius).clamp(0.0, 1.0)
248 } else {
249 1.0
250 };
251 influences.push((i as u32, weight));
252 }
253 }
254
255 Muscle {
256 name: name.into(),
257 joint_name: joint_name.into(),
258 max_flex_angle: FRAC_PI_2,
259 bulge_amplitude: amplitude,
260 influences,
261 bulge_direction: BulgeDirection::RadialFrom(center),
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use std::f32::consts::{FRAC_PI_2, PI};
269
270 #[test]
271 fn test_muscle_new() {
272 let m = Muscle::new("bicep", "elbow");
273 assert_eq!(m.name, "bicep");
274 assert_eq!(m.joint_name, "elbow");
275 assert!((m.max_flex_angle - FRAC_PI_2).abs() < 1e-6);
276 assert!((m.bulge_amplitude - 0.02).abs() < 1e-6);
277 assert!(m.influences.is_empty());
278 }
279
280 #[test]
281 fn test_bulge_weight_zero_angle() {
282 let m = Muscle::new("m", "j");
283 let w = m.bulge_weight(0.0);
284 assert!(
285 w.abs() < 1e-6,
286 "bulge weight at 0 angle should be 0, got {w}"
287 );
288 }
289
290 #[test]
291 fn test_bulge_weight_max_angle() {
292 let m = Muscle::new("m", "j");
293 let w = m.bulge_weight(FRAC_PI_2);
294 assert!(
295 (w - 1.0).abs() < 1e-6,
296 "bulge weight at max_flex_angle should be 1, got {w}"
297 );
298 }
299
300 #[test]
301 fn test_bulge_weight_half() {
302 let m = Muscle::new("m", "j");
303 let w = m.bulge_weight(FRAC_PI_2 / 2.0);
305 assert!(
307 (w - 0.5).abs() < 1e-5,
308 "bulge weight at half max angle should be 0.5, got {w}"
309 );
310 }
311
312 #[test]
313 fn test_bulge_weight_clamped() {
314 let m = Muscle::new("m", "j");
315 let w_over = m.bulge_weight(PI * 10.0);
317 assert!(
318 (w_over - 1.0).abs() < 1e-6,
319 "bulge weight above max should clamp to 1, got {w_over}"
320 );
321 let w_under = m.bulge_weight(-1.0);
323 assert!(
324 w_under.abs() < 1e-6,
325 "bulge weight below 0 should clamp to 0, got {w_under}"
326 );
327 }
328
329 #[test]
330 fn test_compute_displacements_vertex_normal() {
331 let mut m = Muscle::new("m", "j");
332 m.influences = vec![(0, 1.0)];
333 m.bulge_amplitude = 1.0;
334 m.bulge_direction = BulgeDirection::VertexNormal;
335
336 let positions = vec![[0.0_f32, 0.0, 0.0]];
337 let normals = vec![[0.0_f32, 1.0, 0.0]];
338
339 let disps = m.compute_displacements(FRAC_PI_2, &positions, &normals);
341 assert_eq!(disps.len(), 1);
342 let (vid, d) = disps[0];
343 assert_eq!(vid, 0);
344 assert!((d[0]).abs() < 1e-6);
345 assert!(
346 (d[1] - 1.0).abs() < 1e-6,
347 "y displacement should be 1.0, got {}",
348 d[1]
349 );
350 assert!((d[2]).abs() < 1e-6);
351 }
352
353 #[test]
354 fn test_compute_displacements_fixed() {
355 let mut m = Muscle::new("m", "j");
356 m.influences = vec![(0, 1.0)];
357 m.bulge_amplitude = 1.0;
358 m.bulge_direction = BulgeDirection::Fixed([1.0, 0.0, 0.0]);
359
360 let positions = vec![[0.0_f32, 0.0, 0.0]];
361 let normals = vec![[0.0_f32, 1.0, 0.0]];
362
363 let disps = m.compute_displacements(FRAC_PI_2, &positions, &normals);
364 assert_eq!(disps.len(), 1);
365 let (vid, d) = disps[0];
366 assert_eq!(vid, 0);
367 assert!(
369 (d[0] - 1.0).abs() < 1e-6,
370 "x displacement should be 1.0, got {}",
371 d[0]
372 );
373 assert!((d[1]).abs() < 1e-6);
374 assert!((d[2]).abs() < 1e-6);
375 }
376
377 #[test]
378 fn test_compute_displacements_radial() {
379 let mut m = Muscle::new("m", "j");
380 m.influences = vec![(0, 1.0)];
381 m.bulge_amplitude = 1.0;
382 m.bulge_direction = BulgeDirection::RadialFrom([0.0, 0.0, 0.0]);
383
384 let positions = vec![[1.0_f32, 0.0, 0.0]];
386 let normals = vec![[0.0_f32, 1.0, 0.0]];
387
388 let disps = m.compute_displacements(FRAC_PI_2, &positions, &normals);
389 assert_eq!(disps.len(), 1);
390 let (vid, d) = disps[0];
391 assert_eq!(vid, 0);
392 assert!(
393 (d[0] - 1.0).abs() < 1e-6,
394 "x displacement should be 1.0, got {}",
395 d[0]
396 );
397 assert!((d[1]).abs() < 1e-6);
398 assert!((d[2]).abs() < 1e-6);
399 }
400
401 #[test]
402 fn test_simulator_add_muscle() {
403 let mut sim = MuscleSimulator::new();
404 assert_eq!(sim.muscle_count(), 0);
405 sim.add_muscle(Muscle::new("bicep", "elbow"));
406 sim.add_muscle(Muscle::new("tricep", "elbow"));
407 assert_eq!(sim.muscle_count(), 2);
408 }
409
410 #[test]
411 fn test_simulator_muscles_for_joint() {
412 let mut sim = MuscleSimulator::new();
413 sim.add_muscle(Muscle::new("bicep", "elbow"));
414 sim.add_muscle(Muscle::new("tricep", "elbow"));
415 sim.add_muscle(Muscle::new("quad", "knee"));
416
417 let elbow_muscles = sim.muscles_for_joint("elbow");
418 assert_eq!(elbow_muscles.len(), 2);
419
420 let knee_muscles = sim.muscles_for_joint("knee");
421 assert_eq!(knee_muscles.len(), 1);
422
423 let hip_muscles = sim.muscles_for_joint("hip");
424 assert!(hip_muscles.is_empty());
425 }
426
427 #[test]
428 fn test_simulator_apply() {
429 let mut sim = MuscleSimulator::new();
430 let mut m = Muscle::new("bicep", "elbow");
431 m.influences = vec![(0, 1.0)];
432 m.bulge_amplitude = 1.0;
433 m.bulge_direction = BulgeDirection::VertexNormal;
434 sim.add_muscle(m);
435
436 let positions = vec![[0.0_f32, 0.0, 0.0], [1.0, 0.0, 0.0]];
437 let normals = vec![[0.0_f32, 1.0, 0.0], [0.0, 1.0, 0.0]];
438 let mut joint_angles = HashMap::new();
439 joint_angles.insert("elbow".to_string(), FRAC_PI_2);
440
441 let result = sim.apply(&positions, &normals, &joint_angles);
442 assert_eq!(result.len(), 2);
443 assert!(
445 (result[0][1] - 1.0).abs() < 1e-5,
446 "vertex 0 y should be ~1.0, got {}",
447 result[0][1]
448 );
449 assert!((result[1][0] - 1.0).abs() < 1e-6);
451 assert!((result[1][1]).abs() < 1e-6);
452 }
453
454 #[test]
455 fn test_muscle_from_region() {
456 let positions: Vec<[f32; 3]> = (0..5).map(|i| [i as f32 * 0.1, 0.0, 0.0]).collect();
458 let center = [0.2_f32, 0.0, 0.0];
459 let radius = 0.15;
460
461 let m = muscle_from_region("test_muscle", "hip", &positions, center, radius, 0.05);
462 assert_eq!(m.name, "test_muscle");
463 assert_eq!(m.joint_name, "hip");
464 assert!((m.bulge_amplitude - 0.05).abs() < 1e-6);
465
466 assert!(!m.influences.is_empty());
469 let center_inf = m.influences.iter().find(|&&(vi, _)| vi == 2);
471 assert!(center_inf.is_some());
472 let (_, w) = center_inf.expect("should succeed");
473 assert!(
474 (*w - 1.0).abs() < 1e-5,
475 "center vertex weight should be 1.0, got {w}"
476 );
477 }
478
479 #[test]
480 fn test_preset_muscles() {
481 let bicep = bicep_muscle("elbow_L");
482 assert_eq!(bicep.name, "bicep");
483 assert_eq!(bicep.joint_name, "elbow_L");
484 assert!((bicep.max_flex_angle - FRAC_PI_2).abs() < 1e-6);
485 assert!((bicep.bulge_amplitude - 0.02).abs() < 1e-6);
486
487 let quad = quadricep_muscle("knee_R");
488 assert_eq!(quad.name, "quadricep");
489 assert_eq!(quad.joint_name, "knee_R");
490 assert!((quad.bulge_amplitude - 0.025).abs() < 1e-6);
491
492 let calf = calf_muscle("ankle_L");
493 assert_eq!(calf.name, "calf");
494 assert_eq!(calf.joint_name, "ankle_L");
495 assert!((calf.bulge_amplitude - 0.018).abs() < 1e-6);
496 }
497}