use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SkeletonVisualization {
pub bones: Vec<BoneSegment>,
pub joints: Vec<JointViz>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BoneSegment {
pub start: [f32; 3],
pub end: [f32; 3],
pub name: String,
pub mass: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct JointViz {
pub position: [f32; 3],
pub joint_type: String,
pub dof: u8,
}
impl SkeletonVisualization {
#[must_use]
pub fn from_skeleton(
skeleton: &crate::skeleton::Skeleton,
transforms: &crate::kinematics::WorldTransforms,
) -> Self {
let mut bones = Vec::with_capacity(skeleton.bones.len());
for bone in &skeleton.bones {
let end_pos = transforms
.position(bone.id)
.map(|v| [v.x, v.y, v.z])
.unwrap_or([0.0; 3]);
let start_pos = bone
.parent
.and_then(|pid| transforms.position(pid))
.map(|v| [v.x, v.y, v.z])
.unwrap_or(end_pos);
bones.push(BoneSegment {
start: start_pos,
end: end_pos,
name: bone.name.clone(),
mass: bone.mass,
});
}
Self {
bones,
joints: Vec::new(), }
}
pub fn add_joints(
&mut self,
joints: &[crate::joint::Joint],
transforms: &crate::kinematics::WorldTransforms,
) {
for joint in joints {
let position = transforms
.position(joint.child_bone)
.map(|v| [v.x, v.y, v.z])
.unwrap_or([0.0; 3]);
self.joints.push(JointViz {
position,
joint_type: format!("{:?}", joint.joint_type),
dof: joint.joint_type.degrees_of_freedom(),
});
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MuscleOverlay {
pub muscles: Vec<MuscleViz>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MuscleViz {
pub origin: [f32; 3],
pub insertion: [f32; 3],
pub activation: f32,
pub group: String,
pub max_force: f32,
}
impl MuscleOverlay {
#[must_use]
pub fn from_muscles(
muscles: &[crate::muscle::Muscle],
transforms: &crate::kinematics::WorldTransforms,
) -> Self {
let vizs: Vec<MuscleViz> = muscles
.iter()
.map(|m| {
let origin = transforms
.position(m.origin_bone)
.map(|v| [v.x, v.y, v.z])
.unwrap_or([0.0; 3]);
let insertion = transforms
.position(m.insertion_bone)
.map(|v| [v.x, v.y, v.z])
.unwrap_or([0.0; 3]);
MuscleViz {
origin,
insertion,
activation: m.activation,
group: format!("{:?}", m.group),
max_force: m.max_force_n,
}
})
.collect();
Self { muscles: vizs }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GaitCycleVisualization {
pub gait_type: String,
pub duration: f32,
pub stride_length: f32,
pub speed: f32,
pub limb_tracks: Vec<LimbTrack>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LimbTrack {
pub limb_index: usize,
pub phase_offset: f32,
pub duty_factor: f32,
}
impl GaitCycleVisualization {
#[must_use]
pub fn from_gait(gait: &crate::gait::Gait) -> Self {
let limb_tracks: Vec<LimbTrack> = gait
.cycle
.limb_phase_offsets
.iter()
.enumerate()
.map(|(i, &offset)| LimbTrack {
limb_index: i,
phase_offset: offset,
duty_factor: gait.cycle.duty_factor,
})
.collect();
Self {
gait_type: format!("{:?}", gait.gait_type),
duration: gait.cycle.cycle_duration_s,
stride_length: gait.cycle.stride_length_m,
speed: gait.speed(),
limb_tracks,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BodyPlanVisualization {
pub plan_type: String,
pub limb_count: u8,
pub can_fly: bool,
pub can_swim: bool,
pub joint_count: u16,
}
impl BodyPlanVisualization {
#[must_use]
pub fn from_body_plan(plan: crate::preset::BodyPlan) -> Self {
Self {
plan_type: format!("{plan:?}"),
limb_count: plan.limb_count(),
can_fly: plan.can_fly(),
can_swim: plan.can_swim(),
joint_count: plan.typical_joint_count(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn body_plan_biped() {
let viz = BodyPlanVisualization::from_body_plan(crate::preset::BodyPlan::Bipedal);
assert_eq!(viz.limb_count, 2);
assert!(!viz.can_fly);
assert!(viz.plan_type.contains("Bipedal"));
}
#[test]
fn body_plan_avian() {
let viz = BodyPlanVisualization::from_body_plan(crate::preset::BodyPlan::Avian);
assert!(viz.can_fly);
}
#[test]
fn body_plan_aquatic() {
let viz = BodyPlanVisualization::from_body_plan(crate::preset::BodyPlan::Aquatic);
assert!(viz.can_swim);
}
#[test]
fn gait_cycle_human_walk() {
let gait = crate::gait::Gait::human_walk();
let viz = GaitCycleVisualization::from_gait(&gait);
assert_eq!(viz.gait_type, "Walk");
assert!(viz.duration > 0.0);
assert!(viz.speed > 0.0);
assert!(!viz.limb_tracks.is_empty());
}
#[test]
fn gait_cycle_quadruped_trot() {
let gait = crate::gait::Gait::quadruped_trot();
let viz = GaitCycleVisualization::from_gait(&gait);
assert_eq!(viz.gait_type, "Trot");
assert_eq!(viz.limb_tracks.len(), 4);
}
#[test]
fn muscle_overlay_manual() {
let overlay = MuscleOverlay {
muscles: vec![MuscleViz {
origin: [0.0, 1.0, 0.0],
insertion: [0.0, 0.5, 0.0],
activation: 0.7,
group: "Flexor".into(),
max_force: 500.0,
}],
};
assert_eq!(overlay.muscles.len(), 1);
assert!((overlay.muscles[0].activation - 0.7).abs() < 0.01);
}
#[test]
fn skeleton_viz_manual() {
let viz = SkeletonVisualization {
bones: vec![BoneSegment {
start: [0.0, 0.0, 0.0],
end: [0.0, 0.5, 0.0],
name: "femur".into(),
mass: 2.0,
}],
joints: vec![JointViz {
position: [0.0, 0.5, 0.0],
joint_type: "Hinge".into(),
dof: 1,
}],
};
assert_eq!(viz.bones.len(), 1);
assert_eq!(viz.joints.len(), 1);
}
#[test]
fn body_plan_serializes() {
let viz = BodyPlanVisualization::from_body_plan(crate::preset::BodyPlan::Hexapod);
let json = serde_json::to_string(&viz);
assert!(json.is_ok());
}
}