#[allow(dead_code)]
pub struct PoseCondition {
pub joint_name: String,
pub min_angle: f32,
pub max_angle: f32,
}
#[allow(dead_code)]
pub struct PoseConstraintConfig {
pub blend_speed: f32,
pub require_all: bool,
}
#[allow(dead_code)]
pub struct PoseConstraint {
pub morph_name: String,
pub conditions: Vec<PoseCondition>,
pub blend_speed: f32,
pub require_all: bool,
}
#[allow(dead_code)]
pub fn default_pose_constraint_config() -> PoseConstraintConfig {
PoseConstraintConfig {
blend_speed: 1.0,
require_all: true,
}
}
#[allow(dead_code)]
pub fn new_pose_constraint(morph_name: &str, cfg: &PoseConstraintConfig) -> PoseConstraint {
PoseConstraint {
morph_name: morph_name.to_string(),
conditions: Vec::new(),
blend_speed: cfg.blend_speed,
require_all: cfg.require_all,
}
}
#[allow(dead_code)]
pub fn add_pose_condition(
constraint: &mut PoseConstraint,
joint: &str,
min_angle: f32,
max_angle: f32,
) {
constraint.conditions.push(PoseCondition {
joint_name: joint.to_string(),
min_angle,
max_angle,
});
}
#[allow(dead_code)]
pub fn pose_constraint_satisfied(
constraint: &PoseConstraint,
joint_angles: &[(&str, f32)],
) -> bool {
if constraint.conditions.is_empty() {
return true;
}
let cond_ok = |cond: &PoseCondition| {
joint_angles
.iter()
.find(|(name, _)| *name == cond.joint_name)
.is_some_and(|(_, angle)| *angle >= cond.min_angle && *angle <= cond.max_angle)
};
if constraint.require_all {
constraint.conditions.iter().all(cond_ok)
} else {
constraint.conditions.iter().any(cond_ok)
}
}
#[allow(dead_code)]
pub fn pose_constraint_weight(
constraint: &PoseConstraint,
joint_angles: &[(&str, f32)],
) -> f32 {
if constraint.conditions.is_empty() {
return 1.0;
}
let satisfied = constraint.conditions.iter().filter(|cond| {
joint_angles
.iter()
.find(|(name, _)| *name == cond.joint_name)
.is_some_and(|(_, angle)| *angle >= cond.min_angle && *angle <= cond.max_angle)
}).count();
satisfied as f32 / constraint.conditions.len() as f32
}
#[allow(dead_code)]
pub fn pose_constraint_morph_name(constraint: &PoseConstraint) -> &str {
&constraint.morph_name
}
#[allow(dead_code)]
pub fn pose_condition_count(constraint: &PoseConstraint) -> usize {
constraint.conditions.len()
}
#[allow(dead_code)]
pub fn reset_pose_constraint(constraint: &mut PoseConstraint) {
constraint.conditions.clear();
}
#[allow(dead_code)]
pub fn set_constraint_blend_speed(constraint: &mut PoseConstraint, speed: f32) {
constraint.blend_speed = speed.clamp(0.0, 1.0);
}
#[allow(dead_code)]
pub fn pose_constraint_is_active(
constraint: &PoseConstraint,
joint_angles: &[(&str, f32)],
) -> bool {
pose_constraint_weight(constraint, joint_angles) > 0.5
}
#[cfg(test)]
mod tests {
use super::*;
fn make_constraint() -> PoseConstraint {
let cfg = default_pose_constraint_config();
let mut c = new_pose_constraint("corrective_elbow", &cfg);
add_pose_condition(&mut c, "elbow_l", 45.0, 135.0);
c
}
#[test]
fn test_default_config() {
let cfg = default_pose_constraint_config();
assert_eq!(cfg.blend_speed, 1.0);
assert!(cfg.require_all);
}
#[test]
fn test_new_constraint_empty() {
let cfg = default_pose_constraint_config();
let c = new_pose_constraint("test_morph", &cfg);
assert_eq!(c.morph_name, "test_morph");
assert!(c.conditions.is_empty());
}
#[test]
fn test_add_condition() {
let mut c = make_constraint();
assert_eq!(pose_condition_count(&c), 1);
add_pose_condition(&mut c, "shoulder_l", 0.0, 90.0);
assert_eq!(pose_condition_count(&c), 2);
}
#[test]
fn test_satisfied_in_range() {
let c = make_constraint();
let angles = [("elbow_l", 90.0f32)];
assert!(pose_constraint_satisfied(&c, &angles));
}
#[test]
fn test_not_satisfied_out_of_range() {
let c = make_constraint();
let angles = [("elbow_l", 10.0f32)];
assert!(!pose_constraint_satisfied(&c, &angles));
}
#[test]
fn test_weight_partial() {
let cfg = default_pose_constraint_config();
let mut c = new_pose_constraint("m", &cfg);
add_pose_condition(&mut c, "j1", 0.0, 90.0);
add_pose_condition(&mut c, "j2", 0.0, 90.0);
let angles = [("j1", 45.0f32), ("j2", 120.0f32)];
let w = pose_constraint_weight(&c, &angles);
assert!((w - 0.5).abs() < 1e-6);
}
#[test]
fn test_morph_name() {
let c = make_constraint();
assert_eq!(pose_constraint_morph_name(&c), "corrective_elbow");
}
#[test]
fn test_reset_constraint() {
let mut c = make_constraint();
assert!(!c.conditions.is_empty());
reset_pose_constraint(&mut c);
assert!(c.conditions.is_empty());
}
#[test]
fn test_blend_speed_clamp() {
let cfg = default_pose_constraint_config();
let mut c = new_pose_constraint("m", &cfg);
set_constraint_blend_speed(&mut c, 2.0);
assert_eq!(c.blend_speed, 1.0);
set_constraint_blend_speed(&mut c, -1.0);
assert_eq!(c.blend_speed, 0.0);
}
#[test]
fn test_is_active() {
let c = make_constraint();
let in_range = [("elbow_l", 90.0f32)];
let out_range = [("elbow_l", 5.0f32)];
assert!(pose_constraint_is_active(&c, &in_range));
assert!(!pose_constraint_is_active(&c, &out_range));
}
#[test]
fn test_empty_conditions_always_satisfied() {
let cfg = default_pose_constraint_config();
let c = new_pose_constraint("m", &cfg);
let angles: [(&str, f32); 0] = [];
assert!(pose_constraint_satisfied(&c, &angles));
assert_eq!(pose_constraint_weight(&c, &angles), 1.0);
}
}