use nalgebra::SVector;
use symtropy_math::Point;
use crate::body::{BodyHandle, RigidBody};
use crate::joints::{HingeJoint, MotorDrive};
use crate::world::PhysicsWorld;
#[derive(Clone, Debug)]
pub struct LinkSpec {
pub mass: f64,
pub length: f64,
pub radius: f64,
pub plane_a: usize,
pub plane_b: usize,
pub angle_limits: Option<(f64, f64)>,
pub motor_max_force: Option<f64>,
pub motor_damping: Option<f64>,
}
impl Default for LinkSpec {
fn default() -> Self {
Self {
mass: 1.0,
length: 0.3,
radius: 0.03,
plane_a: 0,
plane_b: 2,
angle_limits: Some((-2.9, 2.9)),
motor_max_force: Some(50.0),
motor_damping: None,
}
}
}
#[derive(Debug)]
pub struct ArticulatedChain {
pub base: BodyHandle,
pub links: Vec<BodyHandle>,
pub num_joints: usize,
}
impl ArticulatedChain {
pub fn read_joint_states(&self, world: &PhysicsWorld<3>) -> Vec<(f64, f64)> {
let mut states = Vec::with_capacity(self.num_joints);
let mut prev = self.base;
for &link in &self.links {
let (angle, vel) = match (world.body(prev), world.body(link)) {
(Some(a), Some(b)) => {
let d = b.transform.translation.0 - a.transform.translation.0;
(
d[2].atan2(d[0]),
b.angular_velocity.get(0, 2) - a.angular_velocity.get(0, 2),
)
}
_ => (0.0, 0.0),
};
states.push((angle, vel));
prev = link;
}
states
}
pub fn tip(&self) -> BodyHandle {
*self.links.last().unwrap_or(&self.base)
}
}
pub struct ChainBuilder {
base_pos: Point<3>,
links: Vec<LinkSpec>,
}
impl ChainBuilder {
pub fn new() -> Self {
Self {
base_pos: Point::origin(),
links: Vec::new(),
}
}
pub fn base_position(mut self, pos: Point<3>) -> Self {
self.base_pos = pos;
self
}
pub fn add_link(mut self, spec: LinkSpec) -> Self {
self.links.push(spec);
self
}
pub fn build(self, world: &mut PhysicsWorld<3>) -> ArticulatedChain {
let base = world.add_body(RigidBody::static_body(
BodyHandle(0), self.base_pos,
Box::new(symtropy_math::Sphere::new(Point::origin(), 0.05)),
));
let mut links = Vec::with_capacity(self.links.len());
let mut prev = base;
let mut pos = self.base_pos.0;
for spec in &self.links {
pos[2] -= spec.length;
let link_pos = Point::new([pos[0], pos[1], pos[2]]);
let handle = world.add_body(RigidBody::dynamic_sphere(
BodyHandle(0),
link_pos,
spec.radius.max(0.01),
spec.mass,
));
let anchor_a: SVector<f64, 3> = SVector::from([0.0, 0.0, -spec.length * 0.5]);
let anchor_b: SVector<f64, 3> = SVector::from([0.0, 0.0, spec.length * 0.5]);
let mut hinge = HingeJoint::with_anchors(
prev,
handle,
anchor_a,
anchor_b,
spec.plane_a,
spec.plane_b,
);
if let Some((min, max)) = spec.angle_limits {
hinge = hinge.with_limits(min, max);
}
if let Some(max_force) = spec.motor_max_force {
let mut motor = MotorDrive::new(0.0, max_force);
if let Some(d) = spec.motor_damping {
motor.kd = d;
}
hinge = hinge.with_motor(motor);
}
world.add_constraint(Box::new(hinge));
links.push(handle);
prev = handle;
}
ArticulatedChain {
base,
links,
num_joints: self.links.len(),
}
}
}
impl Default for ChainBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn world_with_gravity() -> PhysicsWorld<3> {
PhysicsWorld::new(SVector::from([0.0, 0.0, -9.81]))
}
#[test]
fn test_single_link_pendulum() {
let mut world = world_with_gravity();
let chain = ChainBuilder::new()
.base_position(Point::new([0.0, 0.0, 2.0]))
.add_link(LinkSpec {
mass: 1.0,
length: 0.5,
..Default::default()
})
.build(&mut world);
assert_eq!(chain.num_joints, 1);
for _ in 0..500 {
world.step(0.002);
}
let tip = world.body(chain.tip()).unwrap();
let pos = &tip.transform.translation.0;
assert!(
pos[0].is_finite() && pos[2].is_finite(),
"Tip position should be finite: {pos:?}"
);
let dist_from_base = ((pos[0]).powi(2) + (pos[1]).powi(2) + (pos[2] - 2.0).powi(2)).sqrt();
assert!(
dist_from_base < 2.0,
"Tip should stay near base: dist={dist_from_base}"
);
}
#[test]
fn test_three_link_chain_finite() {
let mut world = world_with_gravity();
let chain = ChainBuilder::new()
.base_position(Point::new([0.0, 0.0, 3.0]))
.add_link(LinkSpec {
mass: 2.0,
length: 0.5,
..Default::default()
})
.add_link(LinkSpec {
mass: 1.5,
length: 0.4,
..Default::default()
})
.add_link(LinkSpec {
mass: 1.0,
length: 0.3,
..Default::default()
})
.build(&mut world);
assert_eq!(chain.num_joints, 3);
for _ in 0..200 {
world.step(0.001);
}
for &h in &chain.links {
let p = &world.body(h).unwrap().transform.translation.0;
assert!(p[0].is_finite() && p[1].is_finite() && p[2].is_finite());
}
}
#[test]
fn test_joint_readback() {
let mut world = world_with_gravity();
let chain = ChainBuilder::new()
.base_position(Point::new([0.0, 0.0, 2.0]))
.add_link(LinkSpec::default())
.add_link(LinkSpec::default())
.build(&mut world);
let states = chain.read_joint_states(&world);
assert_eq!(states.len(), 2);
for (a, v) in &states {
assert!(a.is_finite());
assert!(v.is_finite());
}
}
#[test]
fn test_tip_is_last_link() {
let mut world = PhysicsWorld::new(SVector::from([0.0, 0.0, 0.0]));
let chain = ChainBuilder::new()
.add_link(LinkSpec::default())
.add_link(LinkSpec::default())
.build(&mut world);
assert_eq!(chain.tip(), chain.links[1]);
}
}