1use nalgebra::SVector;
10use symtropy_math::Point;
11
12use crate::body::{BodyHandle, RigidBody};
13use crate::joints::{HingeJoint, MotorDrive};
14use crate::world::PhysicsWorld;
15
16#[derive(Clone, Debug)]
18pub struct LinkSpec {
19 pub mass: f64,
20 pub length: f64,
21 pub radius: f64,
22 pub plane_a: usize,
23 pub plane_b: usize,
24 pub angle_limits: Option<(f64, f64)>,
25 pub motor_max_force: Option<f64>,
26 pub motor_damping: Option<f64>,
27}
28
29impl Default for LinkSpec {
30 fn default() -> Self {
31 Self {
32 mass: 1.0,
33 length: 0.3,
34 radius: 0.03,
35 plane_a: 0,
36 plane_b: 2,
37 angle_limits: Some((-2.9, 2.9)),
38 motor_max_force: Some(50.0),
39 motor_damping: None,
40 }
41 }
42}
43
44#[derive(Debug)]
46pub struct ArticulatedChain {
47 pub base: BodyHandle,
48 pub links: Vec<BodyHandle>,
49 pub num_joints: usize,
50}
51
52impl ArticulatedChain {
53 pub fn read_joint_states(&self, world: &PhysicsWorld<3>) -> Vec<(f64, f64)> {
55 let mut states = Vec::with_capacity(self.num_joints);
56 let mut prev = self.base;
57 for &link in &self.links {
58 let (angle, vel) = match (world.body(prev), world.body(link)) {
59 (Some(a), Some(b)) => {
60 let d = b.transform.translation.0 - a.transform.translation.0;
61 (
62 d[2].atan2(d[0]),
63 b.angular_velocity.get(0, 2) - a.angular_velocity.get(0, 2),
64 )
65 }
66 _ => (0.0, 0.0),
67 };
68 states.push((angle, vel));
69 prev = link;
70 }
71 states
72 }
73
74 pub fn tip(&self) -> BodyHandle {
76 *self.links.last().unwrap_or(&self.base)
77 }
78}
79
80pub struct ChainBuilder {
82 base_pos: Point<3>,
83 links: Vec<LinkSpec>,
84}
85
86impl ChainBuilder {
87 pub fn new() -> Self {
88 Self {
89 base_pos: Point::origin(),
90 links: Vec::new(),
91 }
92 }
93
94 pub fn base_position(mut self, pos: Point<3>) -> Self {
95 self.base_pos = pos;
96 self
97 }
98
99 pub fn add_link(mut self, spec: LinkSpec) -> Self {
100 self.links.push(spec);
101 self
102 }
103
104 pub fn build(self, world: &mut PhysicsWorld<3>) -> ArticulatedChain {
106 let base = world.add_body(RigidBody::static_body(
108 BodyHandle(0), self.base_pos,
110 Box::new(symtropy_math::Sphere::new(Point::origin(), 0.05)),
111 ));
112
113 let mut links = Vec::with_capacity(self.links.len());
114 let mut prev = base;
115 let mut pos = self.base_pos.0;
116
117 for spec in &self.links {
118 pos[2] -= spec.length;
119 let link_pos = Point::new([pos[0], pos[1], pos[2]]);
120 let handle = world.add_body(RigidBody::dynamic_sphere(
121 BodyHandle(0),
122 link_pos,
123 spec.radius.max(0.01),
124 spec.mass,
125 ));
126
127 let anchor_a: SVector<f64, 3> = SVector::from([0.0, 0.0, -spec.length * 0.5]);
128 let anchor_b: SVector<f64, 3> = SVector::from([0.0, 0.0, spec.length * 0.5]);
129
130 let mut hinge = HingeJoint::with_anchors(
131 prev,
132 handle,
133 anchor_a,
134 anchor_b,
135 spec.plane_a,
136 spec.plane_b,
137 );
138 if let Some((min, max)) = spec.angle_limits {
139 hinge = hinge.with_limits(min, max);
140 }
141 if let Some(max_force) = spec.motor_max_force {
142 let mut motor = MotorDrive::new(0.0, max_force);
143 if let Some(d) = spec.motor_damping {
144 motor.kd = d;
145 }
146 hinge = hinge.with_motor(motor);
147 }
148
149 world.add_constraint(Box::new(hinge));
150 links.push(handle);
151 prev = handle;
152 }
153
154 ArticulatedChain {
155 base,
156 links,
157 num_joints: self.links.len(),
158 }
159 }
160}
161
162impl Default for ChainBuilder {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 fn world_with_gravity() -> PhysicsWorld<3> {
173 PhysicsWorld::new(SVector::from([0.0, 0.0, -9.81]))
174 }
175
176 #[test]
177 fn test_single_link_pendulum() {
178 let mut world = world_with_gravity();
179 let chain = ChainBuilder::new()
180 .base_position(Point::new([0.0, 0.0, 2.0]))
181 .add_link(LinkSpec {
182 mass: 1.0,
183 length: 0.5,
184 ..Default::default()
185 })
186 .build(&mut world);
187
188 assert_eq!(chain.num_joints, 1);
189 for _ in 0..500 {
191 world.step(0.002);
192 }
193 let tip = world.body(chain.tip()).unwrap();
194 let pos = &tip.transform.translation.0;
195 assert!(
196 pos[0].is_finite() && pos[2].is_finite(),
197 "Tip position should be finite: {pos:?}"
198 );
199 let dist_from_base = ((pos[0]).powi(2) + (pos[1]).powi(2) + (pos[2] - 2.0).powi(2)).sqrt();
201 assert!(
202 dist_from_base < 2.0,
203 "Tip should stay near base: dist={dist_from_base}"
204 );
205 }
206
207 #[test]
208 fn test_three_link_chain_finite() {
209 let mut world = world_with_gravity();
210 let chain = ChainBuilder::new()
211 .base_position(Point::new([0.0, 0.0, 3.0]))
212 .add_link(LinkSpec {
213 mass: 2.0,
214 length: 0.5,
215 ..Default::default()
216 })
217 .add_link(LinkSpec {
218 mass: 1.5,
219 length: 0.4,
220 ..Default::default()
221 })
222 .add_link(LinkSpec {
223 mass: 1.0,
224 length: 0.3,
225 ..Default::default()
226 })
227 .build(&mut world);
228
229 assert_eq!(chain.num_joints, 3);
230 for _ in 0..200 {
231 world.step(0.001);
232 }
233 for &h in &chain.links {
234 let p = &world.body(h).unwrap().transform.translation.0;
235 assert!(p[0].is_finite() && p[1].is_finite() && p[2].is_finite());
236 }
237 }
238
239 #[test]
240 fn test_joint_readback() {
241 let mut world = world_with_gravity();
242 let chain = ChainBuilder::new()
243 .base_position(Point::new([0.0, 0.0, 2.0]))
244 .add_link(LinkSpec::default())
245 .add_link(LinkSpec::default())
246 .build(&mut world);
247
248 let states = chain.read_joint_states(&world);
249 assert_eq!(states.len(), 2);
250 for (a, v) in &states {
251 assert!(a.is_finite());
252 assert!(v.is_finite());
253 }
254 }
255
256 #[test]
257 fn test_tip_is_last_link() {
258 let mut world = PhysicsWorld::new(SVector::from([0.0, 0.0, 0.0]));
259 let chain = ChainBuilder::new()
260 .add_link(LinkSpec::default())
261 .add_link(LinkSpec::default())
262 .build(&mut world);
263 assert_eq!(chain.tip(), chain.links[1]);
264 }
265}