1use bincode::Decode;
12use bincode::Encode;
13use cu_spatial_payloads::Transform3D;
14use cu29::prelude::*;
15use cu29::units::si::angle::radian;
16use cu29::units::si::f64::Angle;
17use cu29::units::si::f64::Length;
18use cu29::units::si::length::meter;
19use repose_collision::CuCollisionEnv;
20use repose_collision::CuRobotCollisionModel;
21use repose_kinematics::CuIkParams;
22use repose_kinematics::CuIkStatus;
23use repose_kinematics::CuJointPosition;
24use repose_kinematics::CuJointResolution;
25use repose_kinematics::CuKinematicChain;
26use serde::Deserialize;
27use serde::Serialize;
28
29#[derive(Debug, Clone, Copy, PartialEq)]
35pub struct CuCartesianWaypoint<const N: usize> {
36 pub positions: [CuJointPosition; N],
37 pub zone: Length,
39}
40
41impl<const N: usize> CuCartesianWaypoint<N> {
42 pub const fn new(positions: [CuJointPosition; N], zone: Length) -> Self {
44 Self { positions, zone }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq)]
50pub struct CuTcpZone {
51 pub waypoint: usize,
53 pub center: [Length; 3],
55 pub radius: Length,
56}
57
58#[derive(Debug, Clone, Copy)]
60pub struct CuCartesianPathPoint<const N: usize> {
61 pub positions: [CuJointPosition; N],
62 pub stop: bool,
64 pub zone: Option<CuTcpZone>,
66}
67impl<const N: usize> Default for CuCartesianPathPoint<N> {
68 fn default() -> Self {
69 Self {
70 positions: [CuJointPosition::default(); N],
71 stop: false,
72 zone: None,
73 }
74 }
75}
76
77#[derive(Debug, Clone)]
79pub struct CuCartesianBlendPath<const N: usize, const CAP: usize> {
80 pub points: [CuCartesianPathPoint<N>; CAP],
81 pub len: usize,
82}
83impl<const N: usize, const CAP: usize> Default for CuCartesianBlendPath<N, CAP> {
84 fn default() -> Self {
85 Self {
86 points: [CuCartesianPathPoint::default(); CAP],
87 len: 0,
88 }
89 }
90}
91impl<const N: usize, const CAP: usize> CuCartesianBlendPath<N, CAP> {
92 pub fn points(&self) -> &[CuCartesianPathPoint<N>] {
93 &self.points[..self.len]
94 }
95 pub fn clear(&mut self) {
96 self.len = 0;
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Encode, Decode, Serialize, Deserialize, Reflect)]
111#[serde(default, deny_unknown_fields)]
112pub struct CuCartesianBlendParams {
113 pub translation_step: Length,
115 pub rotation_step: Angle,
117 pub collision_step: CuJointResolution,
119 pub ik: CuIkParams,
120}
121impl Default for CuCartesianBlendParams {
122 fn default() -> Self {
123 Self {
124 translation_step: Length::new::<meter>(0.01),
125 rotation_step: Angle::new::<radian>(0.05),
126 collision_step: CuJointResolution::default(),
127 ik: CuIkParams::default(),
128 }
129 }
130}
131impl CuCartesianBlendParams {
132 pub fn validate(&self) -> CuResult<()> {
135 let translation = self.translation_step.get::<meter>();
136 let rotation = self.rotation_step.get::<radian>();
137 if !translation.is_finite()
138 || translation <= 0.0
139 || !rotation.is_finite()
140 || rotation <= 0.0
141 {
142 return Err(CuError::from(
143 "Cartesian blend steps must be positive and finite",
144 ));
145 }
146 self.collision_step.metric().validate()?;
147 self.ik.validate()
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, Reflect)]
153#[serde(rename_all = "snake_case")]
154pub enum CuCartesianBlendStatus {
155 Success,
156 InvalidInput,
157 InverseKinematicsFailed {
161 waypoint: usize,
162 corner: bool,
163 step: usize,
164 steps: usize,
165 },
166 Collision {
167 waypoint: usize,
168 },
169 ZoneExceeded {
170 waypoint: usize,
171 },
172 CapacityExceeded,
173}
174
175#[derive(Clone, Copy)]
176struct Pose {
177 p: [f64; 3],
178 q: [f64; 4],
179}
180impl Default for Pose {
181 fn default() -> Self {
182 Self {
183 p: [0.0; 3],
184 q: [1.0, 0.0, 0.0, 0.0],
185 }
186 }
187}
188impl Pose {
189 fn from_transform(t: Transform3D<f64>) -> Self {
190 let m = t.to_matrix();
191 let trace = m[0][0] + m[1][1] + m[2][2];
192 let q = if trace > 0.0 {
193 let s = libm::sqrt(trace + 1.0) * 2.0;
194 [
195 s / 4.0,
196 (m[2][1] - m[1][2]) / s,
197 (m[0][2] - m[2][0]) / s,
198 (m[1][0] - m[0][1]) / s,
199 ]
200 } else if m[0][0] > m[1][1] && m[0][0] > m[2][2] {
201 let s = libm::sqrt(1.0 + m[0][0] - m[1][1] - m[2][2]) * 2.0;
202 [
203 (m[2][1] - m[1][2]) / s,
204 s / 4.0,
205 (m[0][1] + m[1][0]) / s,
206 (m[0][2] + m[2][0]) / s,
207 ]
208 } else if m[1][1] > m[2][2] {
209 let s = libm::sqrt(1.0 + m[1][1] - m[0][0] - m[2][2]) * 2.0;
210 [
211 (m[0][2] - m[2][0]) / s,
212 (m[0][1] + m[1][0]) / s,
213 s / 4.0,
214 (m[1][2] + m[2][1]) / s,
215 ]
216 } else {
217 let s = libm::sqrt(1.0 + m[2][2] - m[0][0] - m[1][1]) * 2.0;
218 [
219 (m[1][0] - m[0][1]) / s,
220 (m[0][2] + m[2][0]) / s,
221 (m[1][2] + m[2][1]) / s,
222 s / 4.0,
223 ]
224 };
225 Self {
226 p: [m[0][3], m[1][3], m[2][3]],
227 q,
228 }
229 }
230 fn transform(self) -> Transform3D<f64> {
231 let [w, x, y, z] = self.q;
232 Transform3D::<f64>::from_matrix([
233 [
234 1.0 - 2.0 * (y * y + z * z),
235 2.0 * (x * y - z * w),
236 2.0 * (x * z + y * w),
237 self.p[0],
238 ],
239 [
240 2.0 * (x * y + z * w),
241 1.0 - 2.0 * (x * x + z * z),
242 2.0 * (y * z - x * w),
243 self.p[1],
244 ],
245 [
246 2.0 * (x * z - y * w),
247 2.0 * (y * z + x * w),
248 1.0 - 2.0 * (x * x + y * y),
249 self.p[2],
250 ],
251 [0.0, 0.0, 0.0, 1.0],
252 ])
253 }
254 fn lerp(self, other: Self, t: f64) -> Self {
255 Self {
256 p: core::array::from_fn(|i| self.p[i] + (other.p[i] - self.p[i]) * t),
257 q: slerp(self.q, other.q, t),
258 }
259 }
260}
261fn distance(a: [f64; 3], b: [f64; 3]) -> f64 {
262 let d = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
263 libm::sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2])
264}
265fn slerp(a: [f64; 4], mut b: [f64; 4], t: f64) -> [f64; 4] {
266 let mut dot = (0..4).map(|i| a[i] * b[i]).sum::<f64>();
267 if dot < 0.0 {
268 b = b.map(|v| -v);
269 dot = -dot;
270 }
271 let (wa, wb) = if dot > 0.9995 {
272 (1.0 - t, t)
273 } else {
274 let theta = libm::acos(dot.clamp(-1.0, 1.0));
275 let denom = libm::sin(theta);
276 (
277 libm::sin((1.0 - t) * theta) / denom,
278 libm::sin(t * theta) / denom,
279 )
280 };
281 let mut q = core::array::from_fn(|i| wa * a[i] + wb * b[i]);
282 let norm = libm::sqrt(q.iter().map(|v| v * v).sum::<f64>());
283 for v in &mut q {
284 *v /= norm;
285 }
286 q
287}
288fn angle(a: [f64; 4], b: [f64; 4]) -> f64 {
289 let dot = (0..4).map(|i| a[i] * b[i]).sum::<f64>();
290 2.0 * libm::acos(dot.abs().clamp(0.0, 1.0))
291}
292
293#[derive(Clone, Copy)]
295struct Progress {
296 waypoint: usize,
297 step: usize,
298 steps: usize,
299}
300
301struct Builder<'a, const N: usize, const M: usize, const CAP: usize> {
302 chain: &'a CuKinematicChain<N>,
303 model: &'a CuRobotCollisionModel<N, M>,
304 env: &'a CuCollisionEnv<'a>,
305 config: &'a CuCartesianBlendParams,
306 out: &'a mut CuCartesianBlendPath<N, CAP>,
307}
308impl<const N: usize, const M: usize, const CAP: usize> Builder<'_, N, M, CAP> {
309 fn append(
310 &mut self,
311 target: Pose,
312 exact: Option<[CuJointPosition; N]>,
313 stop: bool,
314 zone: Option<(usize, [f64; 3], f64)>,
315 progress: Progress,
316 ) -> CuResult<CuCartesianBlendStatus> {
317 let waypoint = progress.waypoint;
318 if self.out.len >= CAP {
319 return Ok(CuCartesianBlendStatus::CapacityExceeded);
320 }
321 let previous = self.out.points[self.out.len - 1].positions;
322 let mut q = previous;
323 if let Some(exact) = exact {
324 q = exact;
325 } else {
326 let status = self.chain.inverse_kinematics_dls(
327 &target.transform(),
328 &previous,
329 &self.config.ik,
330 &mut q,
331 )?;
332 if !matches!(status, CuIkStatus::Converged { .. }) {
333 return Ok(CuCartesianBlendStatus::InverseKinematicsFailed {
334 waypoint,
335 corner: zone.is_some(),
336 step: progress.step,
337 steps: progress.steps,
338 });
339 }
340 }
341 if !self
342 .model
343 .is_configuration_collision_free(self.chain, &q, self.env)?
344 || !self.model.is_segment_collision_free(
345 self.chain,
346 &previous,
347 &q,
348 self.config.collision_step,
349 self.env,
350 )?
351 {
352 return Ok(CuCartesianBlendStatus::Collision { waypoint });
353 }
354 if let Some((index, center, radius)) = zone {
355 let mut actual = Transform3D::<f64>::identity();
356 self.chain.forward_kinematics(&q, &mut actual)?;
357 if distance(Pose::from_transform(actual).p, center) > radius {
358 return Ok(CuCartesianBlendStatus::ZoneExceeded { waypoint: index });
359 }
360 }
361 if previous
364 .iter()
365 .zip(q.iter())
366 .all(|(a, b)| (a.si_value() - b.si_value()).abs() <= 1e-12)
367 {
368 if stop {
369 self.out.points[self.out.len - 1].stop = true;
370 }
371 return Ok(CuCartesianBlendStatus::Success);
372 }
373 self.out.points[self.out.len] = CuCartesianPathPoint {
374 positions: q,
375 stop,
376 zone: zone.map(|(waypoint, center, radius)| CuTcpZone {
377 waypoint,
378 center: center.map(Length::new::<meter>),
379 radius: Length::new::<meter>(radius),
380 }),
381 };
382 self.out.len += 1;
383 Ok(CuCartesianBlendStatus::Success)
384 }
385 fn steps(&self, dist: f64, rotation: f64) -> usize {
386 let count = libm::ceil(
387 (dist / self.config.translation_step.get::<meter>())
388 .max(rotation / self.config.rotation_step.get::<radian>()),
389 )
390 .max(1.0);
391 if !count.is_finite() || count > CAP as f64 {
392 CAP.saturating_add(1)
393 } else {
394 count as usize
395 }
396 }
397 fn line(
398 &mut self,
399 from: Pose,
400 to: Pose,
401 exact: Option<[CuJointPosition; N]>,
402 stop: bool,
403 waypoint: usize,
404 ) -> CuResult<CuCartesianBlendStatus> {
405 let steps = self.steps(distance(from.p, to.p), angle(from.q, to.q));
406 if steps > CAP.saturating_sub(self.out.len) {
407 return Ok(CuCartesianBlendStatus::CapacityExceeded);
408 }
409 for i in 1..=steps {
410 let final_point = i == steps;
411 let status = self.append(
412 from.lerp(to, i as f64 / steps as f64),
413 if final_point { exact } else { None },
414 stop && final_point,
415 None,
416 Progress {
417 waypoint,
418 step: i,
419 steps,
420 },
421 )?;
422 if status != CuCartesianBlendStatus::Success {
423 return Ok(status);
424 }
425 }
426 Ok(CuCartesianBlendStatus::Success)
427 }
428}
429
430pub fn blend_cartesian_path<const N: usize, const M: usize, const CAP: usize>(
446 chain: &CuKinematicChain<N>,
447 model: &CuRobotCollisionModel<N, M>,
448 env: &CuCollisionEnv<'_>,
449 waypoints: &[CuCartesianWaypoint<N>],
450 params: &CuCartesianBlendParams,
451 out: &mut CuCartesianBlendPath<N, CAP>,
452) -> CuResult<CuCartesianBlendStatus> {
453 out.clear();
454 if waypoints.len() < 2
455 || waypoints.len() > CAP
456 || waypoints
457 .first()
458 .is_none_or(|w| w.zone.get::<meter>() != 0.0)
459 || waypoints
460 .last()
461 .is_none_or(|w| w.zone.get::<meter>() != 0.0)
462 || params.validate().is_err()
463 {
464 return Ok(CuCartesianBlendStatus::InvalidInput);
465 }
466 let config = params;
467 let mut poses = [Pose::default(); CAP];
468 for i in 0..waypoints.len() {
469 let radius = waypoints[i].zone.get::<meter>();
470 if !radius.is_finite() || radius < 0.0 || !chain.is_within_limits(&waypoints[i].positions) {
471 return Ok(CuCartesianBlendStatus::InvalidInput);
472 }
473 let mut transform = Transform3D::<f64>::identity();
474 chain.forward_kinematics(&waypoints[i].positions, &mut transform)?;
475 poses[i] = Pose::from_transform(transform);
476 }
477 if !model.is_configuration_collision_free(chain, &waypoints[0].positions, env)? {
478 return Ok(CuCartesianBlendStatus::Collision { waypoint: 0 });
479 }
480 out.points[0] = CuCartesianPathPoint {
481 positions: waypoints[0].positions,
482 stop: true,
483 zone: None,
484 };
485 out.len = 1;
486 let result = (|| -> CuResult<CuCartesianBlendStatus> {
487 let mut builder = Builder {
488 chain,
489 model,
490 env,
491 config,
492 out,
493 };
494 let mut current = poses[0];
495 for i in 1..waypoints.len() - 1 {
496 let r = waypoints[i].zone.get::<meter>();
497 if r == 0.0 {
498 let s = builder.line(current, poses[i], Some(waypoints[i].positions), true, i)?;
499 if s != CuCartesianBlendStatus::Success {
500 return Ok(s);
501 }
502 current = poses[i];
503 continue;
504 }
505 let before = distance(poses[i - 1].p, poses[i].p);
506 let after = distance(poses[i].p, poses[i + 1].p);
507 if before <= 1e-12
508 || after <= 1e-12
509 || r <= 2.0 * config.ik.position_tolerance.get::<meter>()
510 {
511 return Ok(CuCartesianBlendStatus::InvalidInput);
512 }
513 let incoming =
514 core::array::from_fn::<_, 3, _>(|j| (poses[i].p[j] - poses[i - 1].p[j]) / before);
515 let outgoing =
516 core::array::from_fn::<_, 3, _>(|j| (poses[i + 1].p[j] - poses[i].p[j]) / after);
517 if (0..3).map(|j| incoming[j] * outgoing[j]).sum::<f64>() < -0.9999 {
518 return Ok(CuCartesianBlendStatus::InvalidInput);
519 }
520 let trim = r.min(before * 0.49).min(after * 0.49) * 0.9;
521 let entry = poses[i - 1].lerp(poses[i], 1.0 - trim / before);
522 let exit = poses[i].lerp(poses[i + 1], trim / after);
523 let status = builder.line(current, entry, None, false, i)?;
524 if status != CuCartesianBlendStatus::Success {
525 return Ok(status);
526 }
527 let steps = builder.steps(
528 2.0 * trim,
529 angle(entry.q, poses[i].q) + angle(poses[i].q, exit.q),
530 );
531 if steps > CAP.saturating_sub(builder.out.len) {
532 return Ok(CuCartesianBlendStatus::CapacityExceeded);
533 }
534 for k in 1..=steps {
535 let t = k as f64 / steps as f64;
536 let pose = entry.lerp(poses[i], t).lerp(poses[i].lerp(exit, t), t);
537 let progress = Progress {
538 waypoint: i,
539 step: k,
540 steps,
541 };
542 let status =
543 builder.append(pose, None, false, Some((i, poses[i].p, r)), progress)?;
544 if status != CuCartesianBlendStatus::Success {
545 return Ok(status);
546 }
547 }
548 current = exit;
549 }
550 let last = waypoints.len() - 1;
551 let status = builder.line(
552 current,
553 poses[last],
554 Some(waypoints[last].positions),
555 true,
556 last,
557 )?;
558 if status != CuCartesianBlendStatus::Success {
559 return Ok(status);
560 }
561 for (i, waypoint) in waypoints.iter().enumerate().take(last).skip(1) {
562 if waypoint.zone.get::<meter>() > 0.0
563 && !builder
564 .out
565 .points()
566 .iter()
567 .any(|p| p.zone.is_some_and(|z| z.waypoint == i))
568 {
569 return Ok(CuCartesianBlendStatus::InvalidInput);
570 }
571 }
572 Ok(CuCartesianBlendStatus::Success)
573 })();
574 if !matches!(result, Ok(CuCartesianBlendStatus::Success)) {
575 out.clear();
576 }
577 result
578}
579
580#[cfg(test)]
581mod tests {
582 use super::*;
583 use crate::test_support::obstacle;
584 use crate::test_support::q;
585 use crate::test_support::robot;
586 use crate::test_support::waypoints;
587
588 #[test]
589 fn zone_produces_a_corner_inside_tcp_radius() {
590 let (chain, model) = robot();
591 let points = [q(0.0, 0.0), q(0.3, 0.0), q(0.3, 0.3)];
592 let env = CuCollisionEnv::EMPTY;
593 let mut path = CuCartesianBlendPath::<3, 256>::default();
594 assert_eq!(
595 blend_cartesian_path(
596 &chain,
597 &model,
598 &env,
599 &waypoints(&points, [0.0, 0.1, 0.0]),
600 &CuCartesianBlendParams::default(),
601 &mut path
602 )
603 .unwrap(),
604 CuCartesianBlendStatus::Success
605 );
606 let mut in_zone = 0;
607 for p in path.points() {
608 if let Some(zone) = p.zone {
609 assert_eq!(zone.waypoint, 1);
610 assert_eq!(zone.radius, Length::new::<meter>(0.1));
611 in_zone += 1;
612 let mut pose = Transform3D::<f64>::identity();
613 chain.forward_kinematics(&p.positions, &mut pose).unwrap();
614 let m = pose.to_matrix();
615 let d = distance([m[0][3], m[1][3], m[2][3]], [0.3, 0.0, 0.0]);
616 assert!(d < 0.1 && d > 0.001);
617 assert!(!p.stop);
618 }
619 }
620 assert!(in_zone > 2);
621 assert!(!path.points().iter().any(|p| p.positions == points[1]));
622 assert!(path.points()[0].stop && path.points()[path.len - 1].stop);
623 }
624
625 #[test]
626 fn zero_zone_stops_exactly_and_collision_fails_closed() {
627 let (chain, model) = robot();
628 let points = [q(0.0, 0.0), q(0.3, 0.0), q(0.3, 0.3)];
629 let env = CuCollisionEnv::EMPTY;
630 let config = CuCartesianBlendParams::default();
631 let mut path = CuCartesianBlendPath::<3, 256>::default();
632 assert_eq!(
633 blend_cartesian_path(
634 &chain,
635 &model,
636 &env,
637 &waypoints(&points, [0.0; 3]),
638 &config,
639 &mut path
640 )
641 .unwrap(),
642 CuCartesianBlendStatus::Success
643 );
644 assert!(
645 path.points()
646 .iter()
647 .any(|p| p.stop && p.positions == points[1])
648 );
649 assert!(path.points().iter().all(|p| p.zone.is_none()));
650 let obstacles = [obstacle()];
651 let env = CuCollisionEnv::new(&obstacles);
652 assert!(matches!(
653 blend_cartesian_path(
654 &chain,
655 &model,
656 &env,
657 &waypoints(&points, [0.0, 0.1, 0.0]),
658 &config,
659 &mut path
660 )
661 .unwrap(),
662 CuCartesianBlendStatus::Collision { .. }
663 ));
664 assert_eq!(path.len, 0);
665 }
666
667 #[test]
668 fn ik_failure_reports_the_move_and_progress() {
669 let (chain, model) = robot();
670 let env = CuCollisionEnv::EMPTY;
671 let mut config = CuCartesianBlendParams {
674 translation_step: Length::new::<meter>(0.1),
675 ..CuCartesianBlendParams::default()
676 };
677 config.ik.max_iterations = 2;
678 let mut path = CuCartesianBlendPath::<3, 64>::default();
679 let status = blend_cartesian_path(
680 &chain,
681 &model,
682 &env,
683 &waypoints(&[q(0.0, 0.0), q(0.3, 0.0)], [0.0, 0.0]),
684 &config,
685 &mut path,
686 )
687 .unwrap();
688 assert_eq!(
689 status,
690 CuCartesianBlendStatus::InverseKinematicsFailed {
691 waypoint: 1,
692 corner: false,
693 step: 1,
694 steps: 3,
695 }
696 );
697 assert_eq!(path.len, 0);
698 }
699
700 #[test]
701 fn invalid_and_capacity_cases_leave_no_partial_path() {
702 let (chain, model) = robot();
703 let points = [q(0.0, 0.0), q(0.3, 0.0), q(0.3, 0.3)];
704 let env = CuCollisionEnv::EMPTY;
705 let config = CuCartesianBlendParams::default();
706 let mut path = CuCartesianBlendPath::<3, 8>::default();
707 assert_eq!(
708 blend_cartesian_path(
709 &chain,
710 &model,
711 &env,
712 &waypoints(&points, [0.0, 0.1, 0.0]),
713 &config,
714 &mut path
715 )
716 .unwrap(),
717 CuCartesianBlendStatus::CapacityExceeded
718 );
719 assert_eq!(path.len, 0);
720 assert_eq!(
721 blend_cartesian_path(
722 &chain,
723 &model,
724 &env,
725 &waypoints(&points, [0.0, -0.1, 0.0]),
726 &config,
727 &mut path
728 )
729 .unwrap(),
730 CuCartesianBlendStatus::InvalidInput
731 );
732 assert_eq!(path.len, 0);
733 }
734}