1use crate::env::CuCollisionEnv;
4use crate::model::CuCollisionReport;
5use crate::model::CuRobotCollisionModel;
6use crate::robots::CuCollisionModelSpec;
7use crate::scene::CuPlanningScene;
8use crate::scene::DEFAULT_MAX_ATTACHED;
9use crate::scene::DEFAULT_MAX_OBSTACLES;
10use crate::scene_exclusions::CuSceneExclusions;
11use crate::scene_exclusions::DEFAULT_MAX_EXCLUSIONS;
12use cu29::prelude::*;
13use repose_kinematics::CuChainSpec;
14use repose_kinematics::CuJointPosition;
15use repose_kinematics::CuJointPositionsPayload;
16use repose_kinematics::CuKinematicChain;
17use serde::Deserialize;
18use serde::Serialize;
19
20pub const DEFAULT_MAX_CONTACTS: usize = 16;
22
23#[derive(Debug, Clone, Serialize, Deserialize, Reflect)]
32#[reflect(no_field_bounds, from_reflect = false)]
33#[serde(deny_unknown_fields)]
35pub struct CuCollisionCheckParams<const N: usize, const M: usize = N> {
36 #[serde(default)]
38 #[reflect(ignore)]
39 pub chain: Option<CuChainSpec<N>>,
40 #[serde(default)]
42 #[reflect(ignore)]
43 pub collision_model: Option<CuCollisionModelSpec<M>>,
44}
45
46impl<const N: usize, const M: usize> Default for CuCollisionCheckParams<N, M> {
47 fn default() -> Self {
48 Self {
49 chain: None,
50 collision_model: None,
51 }
52 }
53}
54
55impl<const N: usize, const M: usize> CuCollisionCheckParams<N, M> {
56 pub fn resolve_chain(&self) -> CuResult<CuKinematicChain<N>> {
58 self.chain
59 .as_ref()
60 .ok_or_else(|| CuError::from("Missing required `chain` configuration"))?
61 .resolve()
62 }
63
64 pub fn resolve_collision_model(&self) -> CuResult<CuRobotCollisionModel<N, M>> {
66 self.collision_model
67 .as_ref()
68 .ok_or_else(|| CuError::from("Missing required `collision_model` configuration"))?
69 .resolve()
70 }
71
72 pub fn validate(&self) -> CuResult<()> {
74 self.resolve_chain()?;
75 self.resolve_collision_model()?;
76 Ok(())
77 }
78}
79
80#[derive(Reflect)]
84#[reflect(no_field_bounds, from_reflect = false)]
85pub struct CuCollisionCheckTask<
86 const N: usize,
87 const M: usize = N,
88 const MAX_OBS: usize = DEFAULT_MAX_OBSTACLES,
89 const MAX_ATTACHED: usize = DEFAULT_MAX_ATTACHED,
90 const MAX_CONTACTS: usize = DEFAULT_MAX_CONTACTS,
91 const MAX_EXCL: usize = DEFAULT_MAX_EXCLUSIONS,
92> {
93 #[reflect(ignore)]
94 chain: CuKinematicChain<N>,
95 #[reflect(ignore)]
96 model: CuRobotCollisionModel<N, M>,
97}
98
99impl<
100 const N: usize,
101 const M: usize,
102 const MAX_OBS: usize,
103 const MAX_ATTACHED: usize,
104 const MAX_CONTACTS: usize,
105 const MAX_EXCL: usize,
106> CuCollisionCheckTask<N, M, MAX_OBS, MAX_ATTACHED, MAX_CONTACTS, MAX_EXCL>
107{
108 pub fn from_params(
110 chain: CuKinematicChain<N>,
111 model: CuRobotCollisionModel<N, M>,
112 ) -> CuResult<Self> {
113 chain.validate()?;
114 Ok(Self { chain, model })
115 }
116
117 pub const fn chain(&self) -> &CuKinematicChain<N> {
119 &self.chain
120 }
121
122 pub const fn model(&self) -> &CuRobotCollisionModel<N, M> {
124 &self.model
125 }
126
127 pub fn check_configuration(
133 &self,
134 q: &[CuJointPosition; N],
135 scene: Option<&CuPlanningScene<MAX_OBS, MAX_ATTACHED>>,
136 exclusions: Option<&CuSceneExclusions<MAX_EXCL>>,
137 out_report: &mut CuCollisionReport<MAX_CONTACTS>,
138 ) -> CuResult<()> {
139 let env = match (scene, exclusions) {
140 (Some(scene), Some(exclusions)) => scene.collision_env().with_exclusions(exclusions),
141 (Some(scene), None) => scene.collision_env(),
142 (None, Some(exclusions)) if !exclusions.is_empty() => {
143 return Err(CuError::from(
144 "repose_collision: scene exclusions require a planning scene snapshot",
145 ));
146 }
147 (None, _) => CuCollisionEnv::EMPTY,
148 };
149 self.model
150 .check_configuration(&self.chain, q, &env, out_report)
151 }
152}
153
154impl<
155 const N: usize,
156 const M: usize,
157 const MAX_OBS: usize,
158 const MAX_ATTACHED: usize,
159 const MAX_CONTACTS: usize,
160 const MAX_EXCL: usize,
161> Freezable for CuCollisionCheckTask<N, M, MAX_OBS, MAX_ATTACHED, MAX_CONTACTS, MAX_EXCL>
162{
163}
164
165impl<
166 const N: usize,
167 const M: usize,
168 const MAX_OBS: usize,
169 const MAX_ATTACHED: usize,
170 const MAX_CONTACTS: usize,
171 const MAX_EXCL: usize,
172> CuTask for CuCollisionCheckTask<N, M, MAX_OBS, MAX_ATTACHED, MAX_CONTACTS, MAX_EXCL>
173{
174 type Resources<'r> = ();
175 type Input<'m> = input_msg!(
176 'm,
177 CuJointPositionsPayload<N>,
178 CuPlanningScene<MAX_OBS, MAX_ATTACHED>,
179 CuSceneExclusions<MAX_EXCL>
180 );
181 type Output<'m> = output_msg!(CuCollisionReport<MAX_CONTACTS>);
182
183 fn new(cfg: Option<&ComponentConfig>, _res: Self::Resources<'_>) -> CuResult<Self>
184 where
185 Self: Sized,
186 {
187 let params: CuCollisionCheckParams<N, M> = cfg
188 .map(ComponentConfig::deserialize_into::<CuCollisionCheckParams<N, M>>)
189 .transpose()?
190 .unwrap_or_default();
191 params.validate()?;
192 Self::from_params(params.resolve_chain()?, params.resolve_collision_model()?)
193 }
194
195 fn process<'i, 'o>(
196 &mut self,
197 _ctx: &CuContext,
198 input: &Self::Input<'i>,
199 output: &mut Self::Output<'o>,
200 ) -> CuResult<()> {
201 output.tov = if !matches!(input.0.tov, Tov::None) {
202 input.0.tov
203 } else if !matches!(input.1.tov, Tov::None) {
204 input.1.tov
205 } else {
206 input.2.tov
207 };
208
209 let Some(joints) = input.0.payload() else {
210 output.clear_payload();
211 return Ok(());
212 };
213
214 let mut report = CuCollisionReport::<MAX_CONTACTS>::default();
215 self.check_configuration(
216 &joints.positions,
217 input.1.payload(),
218 input.2.payload(),
219 &mut report,
220 )?;
221 output.set_payload(report);
222 Ok(())
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use cu29::units::si::length::meter;
230
231 const ONE_JOINT_CHAIN: &str = r#"{"joints": [{"joint_type": "revolute", "origin_translation": [0.0, 0.0, 0.0], "origin_rpy": [0.0, 0.0, 0.0], "axis": "pos_z", "limits": {"revolute": {"lower": -3.0, "upper": 3.0, "max_velocity": 1.0}}}]}"#;
232
233 fn config(body: &str) -> ComponentConfig {
234 ron::from_str(&alloc::format!("#![enable(unwrap_newtypes)]\n{{{body}}}")).unwrap()
235 }
236
237 #[test]
238 fn serial_collision_task_requires_matching_explicit_models() {
239 assert!(CuCollisionCheckTask::<1>::new(None, ()).is_err());
240 let chain_only = config(&alloc::format!(r#""chain": {ONE_JOINT_CHAIN}"#));
241 assert!(CuCollisionCheckTask::<1>::new(Some(&chain_only), ()).is_err());
242 let model = r#"{"shapes": [{"shape": {"sphere": {"center": [0.0, 0.0, 0.0], "radius": 0.1}}, "frame": 0}], "safety_margin": 0.01}"#;
243 let cfg = config(&alloc::format!(
244 r#""chain": {ONE_JOINT_CHAIN}, "collision_model": {model}"#
245 ));
246 let task = CuCollisionCheckTask::<1>::new(Some(&cfg), ()).unwrap();
247 assert_eq!(task.model().link_shapes().len(), 1);
248 assert_eq!(task.model().safety_margin().get::<meter>(), 0.01);
249 assert!(
250 cfg.deserialize_into::<CuCollisionCheckParams<1, 2>>()
251 .is_err()
252 );
253 let typo = config(&alloc::format!(
255 r#""chain": {ONE_JOINT_CHAIN}, "collision_model": {model}, "safety_margin_m": 0.02"#
256 ));
257 assert!(CuCollisionCheckTask::<1>::new(Some(&typo), ()).is_err());
258 }
259
260 #[test]
261 fn collision_task_accepts_presets() {
262 let cfg = config(r#""chain": "igus_rebel", "collision_model": "igus_rebel""#);
263 let task = CuCollisionCheckTask::<6>::new(Some(&cfg), ()).unwrap();
264 assert_eq!(task.model(), &crate::igus_rebel_collision_model());
265 assert!(CuCollisionCheckTask::<6, 4>::new(Some(&cfg), ()).is_err());
266 }
267}