1#![allow(
2 dead_code,
3 clippy::needless_borrows_for_generic_args,
4 clippy::new_without_default
5)]
6
7#[cfg(feature = "viz")]
15use gnuplot::{AxesCommon, Caption, Color, Figure, PointSymbol};
16
17use std::f64::consts::PI;
18
19const KP: f64 = 15.0;
21const DT: f64 = 0.01;
22
23const L1: f64 = 1.0;
25const L2: f64 = 1.0;
26
27pub struct TwoJointArm {
28 pub theta1: f64,
29 pub theta2: f64,
30 pub target_x: f64,
31 pub target_y: f64,
32 pub trajectory: Vec<(f64, f64, f64, f64)>, }
34
35impl TwoJointArm {
36 pub fn new() -> Self {
37 TwoJointArm {
38 theta1: 0.0,
39 theta2: 0.0,
40 target_x: 2.0,
41 target_y: 0.0,
42 trajectory: Vec::new(),
43 }
44 }
45
46 pub fn set_target(&mut self, x: f64, y: f64) {
47 self.target_x = x;
48 self.target_y = y;
49 }
50
51 pub fn solve_inverse_kinematics(&mut self, goal_threshold: f64) -> bool {
52 let mut iterations = 0;
53 const MAX_ITERATIONS: usize = 1000;
54
55 self.trajectory.clear();
56
57 while iterations < MAX_ITERATIONS {
58 let (end_x, end_y) = self.forward_kinematics();
59 self.trajectory
60 .push((self.theta1, self.theta2, end_x, end_y));
61
62 let distance_to_goal =
64 ((end_x - self.target_x).powi(2) + (end_y - self.target_y).powi(2)).sqrt();
65 if distance_to_goal < goal_threshold {
66 return true;
67 }
68
69 let (theta1_goal, theta2_goal) = match self.calculate_target_angles() {
71 Some(angles) => angles,
72 None => continue, };
74
75 self.theta1 += KP * self.angle_diff(theta1_goal, self.theta1) * DT;
77 self.theta2 += KP * self.angle_diff(theta2_goal, self.theta2) * DT;
78
79 iterations += 1;
80 }
81
82 false }
84
85 fn calculate_target_angles(&self) -> Option<(f64, f64)> {
86 let x = self.target_x;
87 let y = self.target_y;
88
89 let distance = (x.powi(2) + y.powi(2)).sqrt();
91 if distance > (L1 + L2) {
92 return None; }
94
95 let cos_theta2 = (x.powi(2) + y.powi(2) - L1.powi(2) - L2.powi(2)) / (2.0 * L1 * L2);
97
98 let cos_theta2 = cos_theta2.clamp(-1.0, 1.0);
100 let mut theta2_goal = cos_theta2.acos();
101
102 let tmp = (L2 * theta2_goal.sin()).atan2(L1 + L2 * theta2_goal.cos());
104 let mut theta1_goal = y.atan2(x) - tmp;
105
106 if theta1_goal < 0.0 {
108 theta2_goal = -theta2_goal;
109 let tmp = (L2 * theta2_goal.sin()).atan2(L1 + L2 * theta2_goal.cos());
110 theta1_goal = y.atan2(x) - tmp;
111 }
112
113 Some((theta1_goal, theta2_goal))
114 }
115
116 pub fn forward_kinematics(&self) -> (f64, f64) {
117 let elbow_x = L1 * self.theta1.cos();
118 let elbow_y = L1 * self.theta1.sin();
119
120 let end_x = elbow_x + L2 * (self.theta1 + self.theta2).cos();
121 let end_y = elbow_y + L2 * (self.theta1 + self.theta2).sin();
122
123 (end_x, end_y)
124 }
125
126 fn angle_diff(&self, theta1: f64, theta2: f64) -> f64 {
127 let mut diff = theta1 - theta2;
128 while diff > PI {
129 diff -= 2.0 * PI;
130 }
131 while diff < -PI {
132 diff += 2.0 * PI;
133 }
134 diff
135 }
136}
137
138#[cfg(feature = "viz")]
140impl TwoJointArm {
141 pub fn save_animation_frames(&self, target_name: &str) {
142 let output_dir = format!("img/arm_navigation/{}", target_name);
143 std::fs::create_dir_all(&output_dir).unwrap();
144
145 for (frame, &(theta1, theta2, end_x, end_y)) in self.trajectory.iter().enumerate() {
146 if frame % 5 == 0 {
147 self.save_frame(frame / 5, theta1, theta2, end_x, end_y, &output_dir);
149 }
150 }
151
152 println!("Animation frames saved in {}/frame_*.png", output_dir);
153 println!("Create video with: ffmpeg -r 10 -i {}/frame_%04d.png -c:v libx264 -pix_fmt yuv420p {}_animation.mp4", output_dir, target_name);
154 }
155
156 fn save_frame(
157 &self,
158 frame: usize,
159 theta1: f64,
160 _theta2: f64,
161 end_x: f64,
162 end_y: f64,
163 output_dir: &str,
164 ) {
165 let mut fg = Figure::new();
166 {
167 let axes = fg
168 .axes2d()
169 .set_title(
170 &format!(
171 "Two Joint Arm Control - Frame {} (Target: {:.2}, {:.2})",
172 frame, self.target_x, self.target_y
173 ),
174 &[],
175 )
176 .set_x_label("X [m]", &[])
177 .set_y_label("Y [m]", &[])
178 .set_x_range(gnuplot::Fix(-2.5), gnuplot::Fix(2.5))
179 .set_y_range(gnuplot::Fix(-2.5), gnuplot::Fix(2.5))
180 .set_aspect_ratio(gnuplot::Fix(1.0));
181
182 let shoulder = (0.0, 0.0);
184 let elbow = (L1 * theta1.cos(), L1 * theta1.sin());
185 let wrist = (end_x, end_y);
186
187 axes.lines(
189 &[shoulder.0, elbow.0],
190 &[shoulder.1, elbow.1],
191 &[Color("black")],
192 );
193 axes.lines(&[elbow.0, wrist.0], &[elbow.1, wrist.1], &[Color("black")]);
194
195 axes.points(
197 &[shoulder.0, elbow.0, wrist.0],
198 &[shoulder.1, elbow.1, wrist.1],
199 &[Color("red"), PointSymbol('O')],
200 );
201
202 axes.points(
204 &[self.target_x],
205 &[self.target_y],
206 &[Color("green"), PointSymbol('*')],
207 );
208
209 axes.lines(
211 &[wrist.0, self.target_x],
212 &[wrist.1, self.target_y],
213 &[Color("green")],
214 );
215 }
216
217 let output_path = format!("{}/frame_{:04}.png", output_dir, frame);
218 fg.set_terminal("pngcairo", &output_path);
219 fg.show().unwrap();
220 }
221
222 pub fn save_arm_plot(&self, output_path: &str) {
224 let (end_x, end_y) = self.forward_kinematics();
225
226 let mut fg = Figure::new();
227 {
228 let axes = fg
229 .axes2d()
230 .set_title("Two Joint Arm to Point Control", &[])
231 .set_x_label("X [m]", &[])
232 .set_y_label("Y [m]", &[])
233 .set_x_range(gnuplot::Fix(-2.5), gnuplot::Fix(2.5))
234 .set_y_range(gnuplot::Fix(-2.5), gnuplot::Fix(2.5))
235 .set_aspect_ratio(gnuplot::Fix(1.0));
236
237 let shoulder = (0.0, 0.0);
239 let elbow = (L1 * self.theta1.cos(), L1 * self.theta1.sin());
240 let wrist = (end_x, end_y);
241
242 axes.lines(
244 &[shoulder.0, elbow.0],
245 &[shoulder.1, elbow.1],
246 &[Color("black")],
247 );
248 axes.lines(&[elbow.0, wrist.0], &[elbow.1, wrist.1], &[Color("black")]);
249
250 axes.points(
252 &[shoulder.0, elbow.0, wrist.0],
253 &[shoulder.1, elbow.1, wrist.1],
254 &[Color("red"), PointSymbol('O')],
255 );
256
257 axes.points(
259 &[self.target_x],
260 &[self.target_y],
261 &[Color("green"), PointSymbol('*')],
262 );
263
264 axes.lines(
266 &[wrist.0, self.target_x],
267 &[wrist.1, self.target_y],
268 &[Color("green")],
269 );
270 }
271
272 fg.set_terminal("pngcairo", output_path);
273 fg.show().unwrap();
274 println!("Arm plot saved to: {}", output_path);
275 }
276
277 pub fn create_summary_plot(&self, target_name: &str) {
278 let mut fg = Figure::new();
279 {
280 let axes = fg
281 .axes2d()
282 .set_title("Two Joint Arm - Joint Angles vs Time", &[])
283 .set_x_label("Iteration", &[])
284 .set_y_label("Angle [rad]", &[]);
285
286 let iterations: Vec<f64> = (0..self.trajectory.len()).map(|i| i as f64).collect();
287 let theta1_data: Vec<f64> = self.trajectory.iter().map(|(t1, _, _, _)| *t1).collect();
288 let theta2_data: Vec<f64> = self.trajectory.iter().map(|(_, t2, _, _)| *t2).collect();
289
290 axes.lines(
291 &iterations,
292 &theta1_data,
293 &[Caption("Theta1 [rad]"), Color("blue")],
294 );
295 axes.lines(
296 &iterations,
297 &theta2_data,
298 &[Caption("Theta2 [rad]"), Color("red")],
299 );
300 }
301
302 let output_path = format!("img/arm_navigation/{}_summary.png", target_name);
303 std::fs::create_dir_all("img/arm_navigation").unwrap();
304 fg.set_terminal("pngcairo", &output_path);
305 fg.show().unwrap();
306 println!("Summary plot saved to: {}", output_path);
307 }
308
309 pub fn run_random_targets_demo(&mut self, num_targets: usize) {
310 use rand::Rng;
311 let mut rng = rand::rng();
312
313 println!("Starting Two Joint Arm random targets demo...");
314
315 for i in 0..num_targets {
316 let angle = rng.random::<f64>() * 2.0 * PI;
318 let radius = rng.random::<f64>() * (L1 + L2 - 0.1); let target_x = radius * angle.cos();
320 let target_y = radius * angle.sin();
321
322 self.set_target(target_x, target_y);
323
324 println!("Target {}: ({:.2}, {:.2})", i + 1, target_x, target_y);
325
326 if self.solve_inverse_kinematics(0.01) {
327 println!(" Reached target in {} iterations", self.trajectory.len());
328 let target_name = format!("target_{:02}", i + 1);
329 self.save_animation_frames(&target_name);
330 } else {
331 println!(" Failed to reach target");
332 }
333 }
334
335 self.create_summary_plot("random_demo");
336 println!("Two Joint Arm demo complete!");
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[test]
345 fn test_forward_kinematics_at_origin() {
346 let arm = TwoJointArm::new();
347 let (x, y) = arm.forward_kinematics();
348 assert!((x - 2.0).abs() < 1e-10); assert!(y.abs() < 1e-10);
350 }
351
352 #[test]
353 fn test_solve_ik_reaches_target() {
354 let mut arm = TwoJointArm::new();
355 arm.set_target(1.5, 0.5);
356 assert!(arm.solve_inverse_kinematics(0.01));
357
358 let (end_x, end_y) = arm.forward_kinematics();
359 let distance = ((end_x - 1.5).powi(2) + (end_y - 0.5).powi(2)).sqrt();
360 assert!(distance < 0.01);
361 }
362}