1use crate::env::CuCollisionEnv;
4use crate::scene_exclusions::CuSceneExclusionPartner as Partner;
5use crate::scene_exclusions::validate_exclusions;
6use crate::shapes::CuCollisionShape;
7use crate::shapes::CuLinkShape;
8use crate::shapes::shape_signed_distance;
9use crate::shapes::transform_shape;
10use CuContactKind::EnvironmentCollision;
11use CuContactKind::SelfCollision;
12use bincode::Decode;
13use bincode::Encode;
14use cu_spatial_payloads::Transform3D;
15use cu29::prelude::*;
16use cu29::units::si::f64::Length as Length64;
17use cu29::units::si::length::meter;
18use repose_kinematics::CuJointPosition;
19use repose_kinematics::CuJointResolution;
20use repose_kinematics::CuKinematicChain;
21use serde::Deserialize;
22use serde::Serialize;
23
24#[derive(
26 Debug, Clone, Copy, Default, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, Reflect,
27)]
28#[serde(rename_all = "snake_case")]
29pub enum CuContactKind {
30 #[default]
31 SelfCollision,
32 EnvironmentCollision,
33}
34
35#[derive(
37 Default, Debug, Clone, Copy, PartialEq, Encode, Decode, Serialize, Deserialize, Reflect,
38)]
39pub struct CuCollisionContact {
40 pub kind: CuContactKind,
41 pub link_index: u16,
43 pub partner_index: u16,
45 pub signed_distance: Length64,
47}
48
49#[derive(Debug, Clone, PartialEq, Encode, Decode, Serialize, Deserialize, Reflect)]
51#[reflect(no_field_bounds, from_reflect = false)]
52pub struct CuCollisionReport<const MAX_CONTACTS: usize> {
53 #[serde(with = "crate::serde_arrays::contact_array")]
54 contacts: [CuCollisionContact; MAX_CONTACTS],
55 len: u32,
57 total_contacts: u32,
59 min_signed_distance: Length64,
61}
62
63impl<const MAX_CONTACTS: usize> Default for CuCollisionReport<MAX_CONTACTS> {
64 fn default() -> Self {
65 Self {
66 contacts: [CuCollisionContact::default(); MAX_CONTACTS],
67 len: 0,
68 total_contacts: 0,
69 min_signed_distance: Length64::new::<meter>(f64::INFINITY),
70 }
71 }
72}
73
74impl<const MAX_CONTACTS: usize> CuCollisionReport<MAX_CONTACTS> {
75 pub fn clear(&mut self) {
77 self.len = 0;
78 self.total_contacts = 0;
79 self.min_signed_distance = Length64::new::<meter>(f64::INFINITY);
80 }
81
82 pub const fn is_collision_free(&self) -> bool {
84 self.total_contacts == 0
85 }
86
87 pub fn as_slice(&self) -> &[CuCollisionContact] {
89 let count = (self.len as usize).min(MAX_CONTACTS);
90 &self.contacts[..count]
91 }
92
93 pub const fn total_contacts(&self) -> usize {
95 self.total_contacts as usize
96 }
97
98 pub const fn min_signed_distance(&self) -> Length64 {
101 self.min_signed_distance
102 }
103
104 fn push_contact(&mut self, contact: CuCollisionContact) {
105 self.total_contacts = self.total_contacts.saturating_add(1);
106 let idx = self.len as usize;
107 if idx < MAX_CONTACTS {
108 self.contacts[idx] = contact;
109 self.len += 1;
110 }
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
120#[serde(default, deny_unknown_fields)]
121pub struct CuCollisionModelParams {
122 pub safety_margin: Length64,
124 pub skip_adjacent_links: bool,
128}
129
130impl Default for CuCollisionModelParams {
131 fn default() -> Self {
132 Self {
133 safety_margin: Length64::new::<meter>(0.0),
134 skip_adjacent_links: true,
135 }
136 }
137}
138
139impl CuCollisionModelParams {
140 pub fn validate(&self) -> CuResult<()> {
142 validate_margin(self.safety_margin)
143 }
144}
145
146fn validate_margin(margin: Length64) -> CuResult<()> {
147 let m = margin.get::<meter>();
148 if m.is_finite() && m >= 0.0 {
149 Ok(())
150 } else {
151 Err(CuError::from("repose_collision: invalid safety margin"))
152 }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Reflect)]
160#[reflect(no_field_bounds, from_reflect = false)]
161pub struct CuRobotCollisionModel<const N: usize, const M: usize = N> {
162 shapes: [CuLinkShape; M],
163 safety_margin: Length64,
164 check_pair: [[bool; M]; M],
165 len: usize,
166}
167
168impl<const N: usize, const M: usize> Encode for CuRobotCollisionModel<N, M> {
170 fn encode<E: bincode::enc::Encoder>(
171 &self,
172 encoder: &mut E,
173 ) -> Result<(), bincode::error::EncodeError> {
174 Encode::encode(&self.shapes, encoder)?;
175 Encode::encode(&self.safety_margin, encoder)?;
176 Encode::encode(&self.check_pair, encoder)?;
177 Encode::encode(&self.len, encoder)
178 }
179}
180impl<C, const N: usize, const M: usize> Decode<C> for CuRobotCollisionModel<N, M> {
181 fn decode<D: bincode::de::Decoder<Context = C>>(
182 decoder: &mut D,
183 ) -> Result<Self, bincode::error::DecodeError> {
184 let model = Self {
185 shapes: Decode::decode(decoder)?,
186 safety_margin: Decode::decode(decoder)?,
187 check_pair: Decode::decode(decoder)?,
188 len: Decode::decode(decoder)?,
189 };
190 if model.len > M || model.link_shapes().iter().any(|s| s.frame >= Some(N)) {
191 return Err(bincode::error::DecodeError::Other(
192 "Invalid collision model snapshot",
193 ));
194 }
195 Ok(model)
196 }
197}
198
199impl<'de, C, const N: usize, const M: usize> bincode::BorrowDecode<'de, C>
200 for CuRobotCollisionModel<N, M>
201{
202 fn borrow_decode<D: bincode::de::BorrowDecoder<'de, Context = C>>(
203 decoder: &mut D,
204 ) -> Result<Self, bincode::error::DecodeError> {
205 Decode::decode(decoder)
206 }
207}
208
209impl<const N: usize, const M: usize> CuRobotCollisionModel<N, M> {
210 pub fn new(shapes: &[CuLinkShape], params: CuCollisionModelParams) -> CuResult<Self> {
219 params.validate()?;
220 if shapes.len() > M || M > u16::MAX as usize {
221 return Err(CuError::from(
222 "repose_collision: collision model shape count exceeds capacity",
223 ));
224 }
225 if shapes.iter().any(|s| s.frame >= Some(N)) {
226 return Err(CuError::from(
227 "repose_collision: collision model frame index out of range",
228 ));
229 }
230 for s in shapes {
231 s.shape.validate()?;
232 }
233 Ok(Self::assemble(shapes, params))
234 }
235
236 pub(crate) fn assemble(shapes: &[CuLinkShape], params: CuCollisionModelParams) -> Self {
238 let mut model = Self {
239 shapes: [CuLinkShape::default(); M],
240 safety_margin: params.safety_margin,
241 check_pair: [[false; M]; M],
242 len: shapes.len(),
243 };
244 model.shapes[..shapes.len()].copy_from_slice(shapes);
245 for i in 0..shapes.len() {
246 for j in i + 1..shapes.len() {
247 let (a, b) = (shapes[i].frame, shapes[j].frame);
248 let adjacent = matches!((a, b), (Some(a), Some(b)) if a.abs_diff(b) == 1);
250 let enabled = a != b && !(params.skip_adjacent_links && adjacent);
251 model.check_pair[i][j] = enabled;
252 model.check_pair[j][i] = enabled;
253 }
254 }
255 model
256 }
257
258 pub const fn shape_count(&self) -> usize {
260 self.len
261 }
262
263 pub fn link_shapes(&self) -> &[CuLinkShape] {
265 &self.shapes[..self.len]
266 }
267
268 pub const fn safety_margin(&self) -> Length64 {
270 self.safety_margin
271 }
272
273 pub fn set_safety_margin(&mut self, safety_margin: Length64) -> CuResult<()> {
275 validate_margin(safety_margin)?;
276 self.safety_margin = safety_margin;
277 Ok(())
278 }
279
280 pub fn set_pair_enabled(
282 &mut self,
283 link_a: usize,
284 link_b: usize,
285 enabled: bool,
286 ) -> CuResult<()> {
287 if link_a >= self.len || link_b >= self.len {
288 return Err(CuError::from("repose_collision: link index out of bounds"));
289 }
290 self.check_pair[link_a][link_b] = enabled;
291 self.check_pair[link_b][link_a] = enabled;
292 Ok(())
293 }
294
295 pub const fn is_pair_enabled(&self, link_a: usize, link_b: usize) -> bool {
297 if link_a >= self.len || link_b >= self.len {
298 return false;
299 }
300 self.check_pair[link_a][link_b]
301 }
302
303 pub fn world_link_shapes(
306 &self,
307 link_frames: &[Transform3D<f64>; N],
308 out_shapes: &mut [CuCollisionShape; M],
309 ) {
310 for (link, out) in self.link_shapes().iter().zip(out_shapes.iter_mut()) {
311 *out = world_shape(link_frames, link.frame, &link.shape);
312 }
313 }
314
315 pub fn check_frames<const MAX_CONTACTS: usize>(
322 &self,
323 link_frames: &[Transform3D<f64>; N],
324 env: &CuCollisionEnv<'_>,
325 out_report: &mut CuCollisionReport<MAX_CONTACTS>,
326 ) -> CuResult<()> {
327 self.validate_env(env)?;
328 out_report.clear();
329 let margin_m = self.safety_margin.get::<meter>();
330 let mut min_dist_m = f64::INFINITY;
331 self.visit_pairs(link_frames, env, |contact| {
332 let d_m = contact.signed_distance.get::<meter>();
333 min_dist_m = min_dist_m.min(d_m);
334 if d_m < margin_m {
335 out_report.push_contact(contact);
336 }
337 true
338 });
339 out_report.min_signed_distance = Length64::new::<meter>(min_dist_m);
340 Ok(())
341 }
342
343 pub fn check_configuration<const MAX_CONTACTS: usize>(
345 &self,
346 chain: &CuKinematicChain<N>,
347 q: &[CuJointPosition; N],
348 env: &CuCollisionEnv<'_>,
349 out_report: &mut CuCollisionReport<MAX_CONTACTS>,
350 ) -> CuResult<()> {
351 let mut frames = [Transform3D::<f64>::identity(); N];
352 let mut ee = Transform3D::<f64>::identity();
353 chain.forward_frames(q, &mut frames, &mut ee)?;
354 self.check_frames(&frames, env, out_report)
355 }
356
357 pub fn is_configuration_collision_free(
360 &self,
361 chain: &CuKinematicChain<N>,
362 q: &[CuJointPosition; N],
363 env: &CuCollisionEnv<'_>,
364 ) -> CuResult<bool> {
365 self.validate_env(env)?;
366 if !chain.is_within_limits(q) {
367 return Ok(false);
368 }
369 let mut frames = [Transform3D::<f64>::identity(); N];
370 let mut ee = Transform3D::<f64>::identity();
371 chain.forward_frames(q, &mut frames, &mut ee)?;
372 let margin_m = self.safety_margin.get::<meter>();
373 let mut free = true;
374 self.visit_pairs(&frames, env, |contact| {
375 free = contact.signed_distance.get::<meter>() >= margin_m;
376 free
377 });
378 Ok(free)
379 }
380
381 pub fn is_segment_collision_free(
385 &self,
386 chain: &CuKinematicChain<N>,
387 q_start: &[CuJointPosition; N],
388 q_end: &[CuJointPosition; N],
389 max_step: impl Into<CuJointResolution>,
390 env: &CuCollisionEnv<'_>,
391 ) -> CuResult<bool> {
392 let metric = max_step.into().metric();
393 metric.validate()?;
394 let mut max_delta = 0.0_f64;
395 for j in 0..N {
396 chain.joints()[j].validate_coordinate(q_start[j])?;
397 chain.joints()[j].validate_coordinate(q_end[j])?;
398 max_delta =
399 max_delta.max((metric.normalized(q_end[j]) - metric.normalized(q_start[j])).abs());
400 }
401 if max_delta.is_nan() || max_delta > 1_000_000.0 {
403 return Err(CuError::from("Collision segment requires too many samples"));
404 }
405 let steps = (libm::ceil(max_delta) as usize).max(1);
406 for s in 0..=steps {
407 let alpha = (s as f64) / (steps as f64);
408 let mut q_interp = [CuJointPosition::default(); N];
409 for k in 0..N {
410 q_interp[k] = q_start[k].interpolate(q_end[k], alpha)?;
411 }
412 if !self.is_configuration_collision_free(chain, &q_interp, env)? {
413 return Ok(false);
414 }
415 }
416 Ok(true)
417 }
418
419 fn validate_env(&self, env: &CuCollisionEnv<'_>) -> CuResult<()> {
421 if env.obstacles().len() > u16::MAX as usize
422 || self.len + env.attached().len() > u16::MAX as usize
423 {
424 return Err(CuError::from(
425 "repose_collision: scene query shape count out of range",
426 ));
427 }
428 if env.attached().iter().any(|a| a.frame >= Some(N)) {
429 return Err(CuError::from(
430 "repose_collision: attached shape frame out of range",
431 ));
432 }
433 validate_exclusions(env.exclusions(), env, self.len)
434 }
435
436 fn visit_pairs(
439 &self,
440 link_frames: &[Transform3D<f64>; N],
441 env: &CuCollisionEnv<'_>,
442 mut visit: impl FnMut(CuCollisionContact) -> bool,
443 ) {
444 let mut robot = [CuCollisionShape::default(); M];
445 self.world_link_shapes(link_frames, &mut robot);
446 let robot = &robot[..self.len];
447 let obstacles = env.obstacles();
448 let attached = env.attached();
449 let robot_count = robot.len();
450 let mut emit =
451 |kind, link: usize, partner: usize, a: &CuCollisionShape, b: &CuCollisionShape| {
452 visit(CuCollisionContact {
453 kind,
454 link_index: link as u16,
455 partner_index: partner as u16,
456 signed_distance: shape_signed_distance(a, b),
457 })
458 };
459
460 for i in 0..robot_count {
461 for j in i + 1..robot_count {
462 if self.check_pair[i][j] && !emit(SelfCollision, i, j, &robot[i], &robot[j]) {
463 return;
464 }
465 }
466 }
467 for (i, r) in robot.iter().enumerate() {
468 for (o, obstacle) in obstacles.iter().enumerate() {
469 if !emit(EnvironmentCollision, i, o, r, obstacle) {
470 return;
471 }
472 }
473 }
474 for (i, att) in attached.iter().enumerate() {
475 let w = world_shape(link_frames, att.frame, &att.shape);
476 let link = robot_count + i;
477 for (j, other) in attached.iter().enumerate().skip(i + 1) {
478 if att.frame == other.frame
480 || env.is_excluded(
481 att.id,
482 Partner::Attached {
483 attached_id: other.id,
484 },
485 )
486 {
487 continue;
488 }
489 let wo = world_shape(link_frames, other.frame, &other.shape);
490 if !emit(SelfCollision, link, robot_count + j, &w, &wo) {
491 return;
492 }
493 }
494 }
495 for (i, att) in attached.iter().enumerate() {
496 let w = world_shape(link_frames, att.frame, &att.shape);
497 let link = robot_count + i;
498 for (r, shape) in robot.iter().enumerate() {
499 let partner = Partner::RobotShape {
500 shape_index: r as u16,
501 };
502 if !env.is_excluded(att.id, partner) && !emit(SelfCollision, link, r, &w, shape) {
503 return;
504 }
505 }
506 }
507 for (i, att) in attached.iter().enumerate() {
508 let w = world_shape(link_frames, att.frame, &att.shape);
509 let link = robot_count + i;
510 for (o, obstacle) in obstacles.iter().enumerate() {
511 let excluded = env.obstacle_id(o).is_some_and(|obstacle_id| {
512 env.is_excluded(att.id, Partner::Obstacle { obstacle_id })
513 });
514 if !excluded && !emit(EnvironmentCollision, link, o, &w, obstacle) {
515 return;
516 }
517 }
518 }
519 }
520}
521
522fn world_shape<const N: usize>(
524 link_frames: &[Transform3D<f64>; N],
525 frame: Option<usize>,
526 shape: &CuCollisionShape,
527) -> CuCollisionShape {
528 match frame {
529 Some(f) => {
530 let mut out = *shape;
531 transform_shape(&link_frames[f], shape, &mut out);
532 out
533 }
534 None => *shape,
535 }
536}