1use crate::dynamics::free_joint_dofs;
2use crate::dynamics::ImpulseJoint;
3use crate::dynamics::ImpulseJointDisabled;
4use crate::dynamics::ImpulseJointImpulses;
5use crate::dynamics::KinematicMultibodyJoint;
6use crate::dynamics::MultibodyJoint;
7use crate::dynamics::MultibodyJointArmature;
8use crate::dynamics::MultibodyJointCoupling;
9use crate::dynamics::MultibodyJointCouplings;
10use crate::dynamics::MultibodyJointDamping;
11use crate::dynamics::MultibodyJointFriction;
12use crate::dynamics::MultibodyJointSprings;
13use crate::dynamics::MultibodyJointState;
14use crate::dynamics::MultibodySelfContactsDisabled;
15use crate::dynamics::RapierImpulseJointHandle;
16use crate::dynamics::RapierMultibodyJointHandle;
17use crate::plugin::context::systemparams::RAPIER_CONTEXT_EXPECT_ERROR;
18use crate::plugin::context::RapierContextEntityLink;
19use crate::plugin::context::RapierContextJoints;
20use crate::plugin::context::RapierRigidBodySet;
21use bevy::platform::collections::HashSet;
22use bevy::prelude::*;
23
24use super::RapierContextLinkResolver;
25use rapier::dynamics::{
26 GenericJoint as RapierGenericJoint, ImpulseJointHandle, ImpulseJointSet, JointEnabled,
27 Multibody, MultibodyDofCoupling, MultibodyJointHandle, MultibodyJointSet, RigidBodyHandle,
28 RigidBodySet,
29};
30use rapier::math::SPATIAL_DIM;
31
32fn impulse_joint_target_body(
35 rigidbody_set: &RapierRigidBodySet,
36 child_of_query: &Query<&ChildOf>,
37 entity: Entity,
38) -> Option<RigidBodyHandle> {
39 let mut body_entity = entity;
40 loop {
41 if let Some(handle) = rigidbody_set.entity2body.get(&body_entity) {
42 return Some(*handle);
43 }
44 body_entity = child_of_query.get(body_entity).ok()?.parent();
45 }
46}
47
48fn impulse_joint_data(entity: Entity, joint: &ImpulseJoint, disabled: bool) -> RapierGenericJoint {
50 let mut data = joint.data.as_ref().into_rapier();
51 data.user_data = entity.to_bits() as u128;
52 if disabled {
53 data.set_enabled(false);
54 }
55 data
56}
57
58fn multibody_joint_data(entity: Entity, joint: &MultibodyJoint) -> RapierGenericJoint {
60 let mut data = joint.data.as_ref().into_rapier();
61 data.user_data = entity.to_bits() as u128;
62 data
63}
64
65fn insert_multibody_joint(
67 joints: &mut MultibodyJointSet,
68 body1: RigidBodyHandle,
69 body2: RigidBodyHandle,
70 data: RapierGenericJoint,
71 kinematic: bool,
72) -> Option<MultibodyJointHandle> {
73 if kinematic {
74 joints.insert_kinematic(body1, body2, data, true)
75 } else {
76 joints.insert(body1, body2, data, true)
77 }
78}
79
80fn write_multibody_joint_properties(
83 mb: &mut Multibody,
84 link_id: usize,
85 kinematic: bool,
86 damping: Option<&MultibodyJointDamping>,
87 friction: Option<&MultibodyJointFriction>,
88 armature: Option<&MultibodyJointArmature>,
89 springs: Option<&MultibodyJointSprings>,
90) {
91 let Some(link) = mb.link(link_id) else {
92 return;
93 };
94 let offset = link.assembly_id();
95 let locked_axes = link.joint().data.locked_axes;
96 let damping = damping.copied().unwrap_or_default();
97 let friction = friction.copied().unwrap_or_default();
98 let armature = armature.copied().unwrap_or_default();
99 let springs = springs.copied().unwrap_or_default();
100
101 for (dof, axis) in free_joint_dofs(locked_axes) {
102 mb.damping_mut()[offset + dof] = damping.0[axis];
103 mb.frictions_mut()[offset + dof] = friction.0[axis];
104 mb.armature_mut()[offset + dof] = armature.0[axis];
105 }
106
107 let Some(link) = mb.link_mut(link_id) else {
108 return;
109 };
110 link.joint.kinematic = kinematic;
111 for axis in 0..SPATIAL_DIM {
112 if locked_axes.bits() & (1 << axis) == 0 {
113 link.joint
114 .set_spring(axis, springs.stiffness[axis], springs.rest[axis]);
115 } else {
116 link.joint.set_spring(axis, 0.0, 0.0);
117 }
118 }
119}
120
121fn rapier_dof_coupling(
126 joints: &MultibodyJointSet,
127 rigidbody_set: &RapierRigidBodySet,
128 entity: Entity,
129 coupling: &MultibodyJointCoupling,
130) -> Option<MultibodyDofCoupling> {
131 let link_id = |entity: &Entity| {
132 let body = rigidbody_set.entity2body.get(entity)?;
133 joints.rigid_body_link(*body)
134 };
135 let link1 = link_id(&coupling.source)?;
136 let link2 = link_id(&entity)?;
137 if link1.multibody != link2.multibody {
138 return None;
139 }
140
141 let mb = joints.get_multibody(link1.multibody)?;
142 let dof = |link: usize, axis: usize| {
143 free_joint_dofs(mb.link(link)?.joint().data.locked_axes)
144 .find(|(_, free_axis)| *free_axis == axis)
145 .map(|(dof, _)| dof)
146 };
147 let (axis1, axis2) = (coupling.source_axis as usize, coupling.axis as usize);
148 Some(MultibodyDofCoupling {
149 link1: link1.id,
150 dof1: dof(link1.id, axis1)?,
151 axis1,
152 link2: link2.id,
153 dof2: dof(link2.id, axis2)?,
154 axis2,
155 coeff: coupling.coeff,
156 offset: coupling.offset,
157 })
158}
159
160fn sync_dof_couplings(
163 joints: &mut MultibodyJointSet,
164 rigidbody_set: &RapierRigidBodySet,
165 entity: Entity,
166 handle: MultibodyJointHandle,
167 couplings: Option<&MultibodyJointCouplings>,
168) {
169 let declared: Vec<_> = couplings
170 .iter()
171 .flat_map(|couplings| couplings.0.iter())
172 .filter_map(|c| rapier_dof_coupling(joints, rigidbody_set, entity, c))
173 .collect();
174 let Some((mb, link_id)) = joints.get_mut(handle) else {
175 return;
176 };
177 mb.retain_dof_couplings(|c| {
179 c.link2 != link_id || declared.iter().any(|d| same_dof_coupling(c, d))
180 });
181 for coupling in declared {
182 if !mb
183 .couplings()
184 .iter()
185 .any(|c| same_dof_coupling(c, &coupling))
186 {
187 mb.add_dof_coupling(coupling);
188 }
189 }
190}
191
192pub(crate) fn wake_up_multibody(mb: &Multibody, bodies: &mut RigidBodySet) {
194 for link in mb.links() {
195 if let Some(body) = bodies.get_mut(link.rigid_body_handle()) {
196 body.wake_up(true);
197 }
198 }
199}
200
201fn same_dof_coupling(a: &MultibodyDofCoupling, b: &MultibodyDofCoupling) -> bool {
203 (a.link1, a.dof1, a.axis1, a.link2, a.dof2, a.axis2)
204 == (b.link1, b.dof1, b.axis1, b.link2, b.dof2, b.axis2)
205 && a.coeff == b.coeff
206 && a.offset == b.offset
207}
208
209pub(crate) fn set_impulse_joint_enabled(
214 joints: &mut ImpulseJointSet,
215 bodies: &RigidBodySet,
216 handle: ImpulseJointHandle,
217 enabled: bool,
218) {
219 let Some(joint) = joints.get_mut(handle, true) else {
220 return;
221 };
222 let body_enabled = |h| bodies.get(h).is_none_or(|rb| rb.is_enabled());
223 joint.data.enabled = if !enabled {
224 JointEnabled::Disabled
225 } else if joint.data.enabled != JointEnabled::Disabled {
226 joint.data.enabled
227 } else if body_enabled(joint.body1()) && body_enabled(joint.body2()) {
228 JointEnabled::Enabled
229 } else {
230 JointEnabled::DisabledByAttachedBody
231 };
232}
233
234pub fn init_joints(
236 mut commands: Commands,
237 mut context_access: Query<(&RapierRigidBodySet, &mut RapierContextJoints)>,
238 context_links: RapierContextLinkResolver,
239 impulse_joints: Query<
240 (
241 Entity,
242 Option<&RapierContextEntityLink>,
243 &ImpulseJoint,
244 Has<ImpulseJointDisabled>,
245 ),
246 Without<RapierImpulseJointHandle>,
247 >,
248 multibody_joints: Query<
249 (
250 Entity,
251 Option<&RapierContextEntityLink>,
252 &MultibodyJoint,
253 Has<KinematicMultibodyJoint>,
254 ),
255 Without<RapierMultibodyJointHandle>,
256 >,
257 child_of_query: Query<&ChildOf>,
258) {
259 for (entity, entity_context_link, joint, disabled) in impulse_joints.iter() {
260 let context_entity = context_links.resolve(entity, entity_context_link, &mut commands);
262 let Some(context_entity) = context_entity else {
263 continue;
264 };
265
266 let Ok(rigidbody_set_joints) = context_access.get_mut(context_entity) else {
267 log::error!("Could not find entity {context_entity} with rapier context while initializing {entity}");
268 continue;
269 };
270 let rigidbody_set = rigidbody_set_joints.0;
271 let target = impulse_joint_target_body(rigidbody_set, &child_of_query, entity);
272 let joints = rigidbody_set_joints.1.into_inner();
273
274 if let (Some(target), Some(source)) = (target, rigidbody_set.entity2body.get(&joint.parent))
275 {
276 let handle = joints.impulse_joints.insert(
277 *source,
278 target,
279 impulse_joint_data(entity, joint, disabled),
280 true,
281 );
282 commands
283 .entity(entity)
284 .insert(RapierImpulseJointHandle(handle));
285 joints.entity2impulse_joint.insert(entity, handle);
286 }
287 }
288
289 for (entity, entity_context_link, joint, kinematic) in multibody_joints.iter() {
290 let context_entity = context_links.resolve(entity, entity_context_link, &mut commands);
292 let Some(context_entity) = context_entity else {
293 continue;
294 };
295
296 let Ok(context_joints) = context_access.get_mut(context_entity) else {
297 log::error!("Could not find entity {context_entity} with rapier context while initializing {entity}");
298 continue;
299 };
300 let context = context_joints.0;
301 let target = context.entity2body.get(&entity);
302 let joints = context_joints.1.into_inner();
303
304 if let (Some(target), Some(source)) = (target, context.entity2body.get(&joint.parent)) {
305 if let Some(handle) = insert_multibody_joint(
306 &mut joints.multibody_joints,
307 *source,
308 *target,
309 multibody_joint_data(entity, joint),
310 kinematic,
311 ) {
312 commands
313 .entity(entity)
314 .insert(RapierMultibodyJointHandle(handle));
315 joints.entity2multibody_joint.insert(entity, handle);
316 } else {
317 log::error!("Failed to create multibody joint: loop detected.")
318 }
319 }
320 }
321}
322
323#[allow(clippy::type_complexity)]
330pub fn apply_joint_user_changes(
331 mut commands: Commands,
332 mut context: Query<(&RapierRigidBodySet, &mut RapierContextJoints)>,
333 changed_impulse_joints: Query<
334 (
335 Entity,
336 &RapierContextEntityLink,
337 &RapierImpulseJointHandle,
338 &ImpulseJoint,
339 Has<ImpulseJointDisabled>,
340 ),
341 Or<(
342 Changed<ImpulseJoint>,
343 Changed<ImpulseJointDisabled>,
344 Changed<ChildOf>,
345 )>,
346 >,
347 changed_multibody_joints: Query<
348 (
349 Entity,
350 &RapierContextEntityLink,
351 &RapierMultibodyJointHandle,
352 &MultibodyJoint,
353 Has<KinematicMultibodyJoint>,
354 ),
355 Changed<MultibodyJoint>,
356 >,
357 child_of_query: Query<&ChildOf>,
358) {
359 for (entity, link, handle, changed_joint, disabled) in changed_impulse_joints.iter() {
360 let (rigidbody_set, mut context) =
361 context.get_mut(link.0).expect(RAPIER_CONTEXT_EXPECT_ERROR);
362 let joints = &mut context.impulse_joints;
363
364 let body1 = rigidbody_set
365 .entity2body
366 .get(&changed_joint.parent)
367 .copied();
368 let body2 = impulse_joint_target_body(rigidbody_set, &child_of_query, entity);
369 let (Some(body1), Some(body2), true) = (body1, body2, joints.contains(handle.0)) else {
370 joints.remove(handle.0, true);
372 context.entity2impulse_joint.remove(&entity);
373 commands.entity(entity).remove::<RapierImpulseJointHandle>();
374 continue;
375 };
376
377 let Some(joint) = joints.set_bodies(handle.0, body1, body2, true) else {
378 continue;
379 };
380
381 let old_data = joint.data;
384 let mut new_data = impulse_joint_data(entity, changed_joint, false);
385 new_data.enabled = old_data.enabled;
386 for (new_limit, old_limit) in new_data.limits.iter_mut().zip(old_data.limits.iter()) {
387 new_limit.impulse = old_limit.impulse;
388 }
389 for (new_motor, old_motor) in new_data.motors.iter_mut().zip(old_data.motors.iter()) {
390 new_motor.impulse = old_motor.impulse;
391 }
392 joint.data = new_data;
393
394 let enabled = changed_joint.data.as_ref().is_enabled() && !disabled;
395 set_impulse_joint_enabled(joints, &rigidbody_set.bodies, handle.0, enabled);
396 }
397
398 for (entity, link, handle, changed_joint, kinematic) in changed_multibody_joints.iter() {
399 let (rigidbody_set, mut context) =
400 context.get_mut(link.0).expect(RAPIER_CONTEXT_EXPECT_ERROR);
401 let context = &mut *context;
402 let data = multibody_joint_data(entity, changed_joint);
403 let new_parent = rigidbody_set
404 .entity2body
405 .get(&changed_joint.parent)
406 .copied();
407
408 let Some((mb, link_id)) = context.multibody_joints.get_mut(handle.0) else {
409 continue;
410 };
411 let Some(mb_link) = mb.link(link_id) else {
412 continue;
413 };
414 let body = mb_link.rigid_body_handle();
415 let old_parent = mb_link
416 .parent_id()
417 .and_then(|id| mb.link(id))
418 .map(|parent| parent.rigid_body_handle());
419
420 if old_parent.is_some() && old_parent == new_parent {
421 if let Some(mb_link) = mb.link_mut(link_id) {
423 mb_link.joint.data = data;
424 }
425 continue;
426 }
427
428 context.multibody_joints.remove(handle.0, true);
430 let new_handle = new_parent.and_then(|parent| {
431 insert_multibody_joint(&mut context.multibody_joints, parent, body, data, kinematic)
432 });
433 if let Some(new_handle) = new_handle {
434 context.entity2multibody_joint.insert(entity, new_handle);
435 commands
437 .entity(entity)
438 .insert(RapierMultibodyJointHandle(new_handle));
439 } else {
440 if new_parent.is_some() {
441 log::error!("Failed to re-attach multibody joint: loop detected.");
442 }
443 context.entity2multibody_joint.remove(&entity);
445 commands
446 .entity(entity)
447 .remove::<RapierMultibodyJointHandle>();
448 }
449 }
450}
451
452type MultibodyJointPropertiesComponents = (
454 &'static RapierContextEntityLink,
455 &'static RapierMultibodyJointHandle,
456 Has<KinematicMultibodyJoint>,
457 Option<&'static MultibodyJointDamping>,
458 Option<&'static MultibodyJointFriction>,
459 Option<&'static MultibodyJointArmature>,
460 Option<&'static MultibodyJointSprings>,
461 Option<&'static MultibodyJointCouplings>,
462);
463
464type ChangedMultibodyJointProperties = Or<(
466 Added<RapierMultibodyJointHandle>,
467 Changed<MultibodyJoint>,
468 Changed<KinematicMultibodyJoint>,
469 Changed<MultibodyJointDamping>,
470 Changed<MultibodyJointFriction>,
471 Changed<MultibodyJointArmature>,
472 Changed<MultibodyJointSprings>,
473 Changed<MultibodyJointCouplings>,
474)>;
475
476#[allow(clippy::type_complexity)]
486pub fn apply_multibody_joint_properties(
487 mut context: Query<(&mut RapierRigidBodySet, &mut RapierContextJoints)>,
488 changed_joints: Query<Entity, (With<MultibodyJoint>, ChangedMultibodyJointProperties)>,
489 changed_couplings: Query<Entity, Changed<MultibodyJointCouplings>>,
490 changed_topology: Query<
491 &RapierContextEntityLink,
492 (With<MultibodyJoint>, Changed<RapierMultibodyJointHandle>),
493 >,
494 joints: Query<MultibodyJointPropertiesComponents, With<MultibodyJoint>>,
495 all_couplings: Query<(
496 Entity,
497 &RapierContextEntityLink,
498 &RapierMultibodyJointHandle,
499 &MultibodyJointCouplings,
500 )>,
501 self_contacts_disabled: Query<(
502 Entity,
503 &RapierContextEntityLink,
504 Ref<MultibodySelfContactsDisabled>,
505 )>,
506 (
507 mut removed_kinematic,
508 mut removed_damping,
509 mut removed_friction,
510 mut removed_armature,
511 mut removed_springs,
512 mut removed_couplings,
513 ): (
514 RemovedComponents<KinematicMultibodyJoint>,
515 RemovedComponents<MultibodyJointDamping>,
516 RemovedComponents<MultibodyJointFriction>,
517 RemovedComponents<MultibodyJointArmature>,
518 RemovedComponents<MultibodyJointSprings>,
519 RemovedComponents<MultibodyJointCouplings>,
520 ),
521 mut removed_self_contacts_disabled: RemovedComponents<MultibodySelfContactsDisabled>,
522) {
523 let mut couplings_modified: HashSet<Entity> = changed_couplings.iter().collect();
524 couplings_modified.extend(removed_couplings.read());
525 let mut modified: HashSet<Entity> = changed_joints.iter().collect();
526 modified.extend(couplings_modified.iter().copied());
527 modified.extend(removed_kinematic.read());
528 modified.extend(removed_damping.read());
529 modified.extend(removed_friction.read());
530 modified.extend(removed_armature.read());
531 modified.extend(removed_springs.read());
532 let topology_changed: HashSet<Entity> = changed_topology.iter().map(|link| link.0).collect();
534
535 for entity in modified {
536 let Ok((link, handle, kinematic, damping, friction, armature, springs, couplings)) =
537 joints.get(entity)
538 else {
539 continue;
540 };
541 let Ok((mut rigidbody_set, mut context)) = context.get_mut(link.0) else {
542 continue;
543 };
544 let (rigidbody_set, context) = (&mut *rigidbody_set, &mut *context);
545
546 if couplings_modified.contains(&entity) {
547 sync_dof_couplings(
548 &mut context.multibody_joints,
549 rigidbody_set,
550 entity,
551 handle.0,
552 couplings,
553 );
554 }
555
556 if let Some((mb, link_id)) = context.multibody_joints.get_mut(handle.0) {
557 write_multibody_joint_properties(
558 mb, link_id, kinematic, damping, friction, armature, springs,
559 );
560 wake_up_multibody(mb, &mut rigidbody_set.bodies);
561 }
562 }
563
564 for (entity, link, handle, couplings) in all_couplings.iter() {
567 if !topology_changed.contains(&link.0) {
568 continue;
569 }
570 let Ok((rigidbody_set, mut context)) = context.get_mut(link.0) else {
571 continue;
572 };
573 sync_dof_couplings(
574 &mut context.multibody_joints,
575 &rigidbody_set,
576 entity,
577 handle.0,
578 Some(couplings),
579 );
580 }
581
582 let mut any_marker_removed = false;
585 for entity in removed_self_contacts_disabled.read() {
586 if !self_contacts_disabled.contains(entity) {
587 set_multibody_self_contacts(&mut context, entity, true);
588 any_marker_removed = true;
589 }
590 }
591 for (entity, link, marker) in self_contacts_disabled.iter() {
594 if any_marker_removed || marker.is_changed() || topology_changed.contains(&link.0) {
595 set_multibody_self_contacts(&mut context, entity, false);
596 }
597 }
598}
599
600pub fn writeback_impulse_joint_impulses(
603 context: Query<&RapierContextJoints>,
604 mut impulse_joints: Query<(
605 &RapierContextEntityLink,
606 &RapierImpulseJointHandle,
607 &mut ImpulseJointImpulses,
608 )>,
609) {
610 for (link, handle, mut impulses) in impulse_joints.iter_mut() {
611 let Some(joint) = context
612 .get(link.0)
613 .ok()
614 .and_then(|joints| joints.impulse_joints.get(handle.0))
615 else {
616 continue;
617 };
618
619 let new_impulses = if joint.data.is_enabled() {
621 ImpulseJointImpulses::from_rapier(joint)
622 } else {
623 ImpulseJointImpulses::default()
624 };
625 impulses.set_if_neq(new_impulses);
627 }
628}
629
630fn set_multibody_self_contacts(
632 contexts: &mut Query<(&mut RapierRigidBodySet, &mut RapierContextJoints)>,
633 entity: Entity,
634 enabled: bool,
635) {
636 for (rigidbody_set, mut context) in contexts.iter_mut() {
637 let Some(body) = rigidbody_set.entity2body.get(&entity) else {
638 continue;
639 };
640 let Some(link) = context.multibody_joints.rigid_body_link(*body).copied() else {
641 return;
642 };
643 let needs_change = context
644 .multibody_joints
645 .get_multibody(link.multibody)
646 .is_some_and(|mb| mb.self_contacts_enabled() != enabled);
647 if needs_change {
648 if let Some(mb) = context.multibody_joints.get_multibody_mut(link.multibody) {
649 mb.set_self_contacts_enabled(enabled);
650 }
651 }
652 return;
653 }
654}
655
656pub fn writeback_multibody_joint_states(
659 context: Query<&RapierContextJoints>,
660 mut multibody_joints: Query<(
661 &RapierContextEntityLink,
662 &RapierMultibodyJointHandle,
663 &mut MultibodyJointState,
664 )>,
665) {
666 for (link, handle, mut state) in multibody_joints.iter_mut() {
667 let Some(new_state) = context
668 .get(link.0)
669 .ok()
670 .and_then(|joints| joints.multibody_joint_state_from_handle(handle.0))
671 else {
672 continue;
673 };
674 state.set_if_neq(new_state);
676 }
677}
678
679#[cfg(test)]
680mod tests {
681 use super::*;
682 use crate::prelude::*;
683 use bevy::time::{TimePlugin, TimeUpdateStrategy};
684
685 fn test_app() -> App {
686 let mut app = App::new();
687 app.add_plugins((
688 TransformPlugin,
689 TimePlugin,
690 RapierPhysicsPlugin::<NoUserData>::default(),
691 ))
692 .insert_resource(TimeUpdateStrategy::ManualDuration(
693 std::time::Duration::from_secs_f32(1.0 / 60.0),
694 ));
695 app.finish();
696 app.update();
697 app
698 }
699
700 fn run(app: &mut App, frames: usize) {
701 for _ in 0..frames {
702 app.update();
703 }
704 }
705
706 fn spawn_body(app: &mut App, body: RigidBody, x: f32, y: f32) -> Entity {
707 app.world_mut()
708 .spawn((Transform::from_xyz(x, y, 0.0), body, Collider::ball(0.1)))
709 .id()
710 }
711
712 fn translation(app: &App, entity: Entity) -> Vec3 {
713 app.world().get::<Transform>(entity).unwrap().translation
714 }
715
716 fn with_joints<R>(
717 app: &mut App,
718 f: impl FnOnce(&RapierRigidBodySet, &RapierContextJoints) -> R,
719 ) -> R {
720 let world = app.world_mut();
721 let (bodies, joints) = world
722 .query::<(&RapierRigidBodySet, &RapierContextJoints)>()
723 .single(world)
724 .unwrap();
725 f(bodies, joints)
726 }
727
728 fn raw_joint<R>(
729 app: &mut App,
730 entity: Entity,
731 f: impl FnOnce(&RapierRigidBodySet, &rapier::dynamics::ImpulseJoint) -> R,
732 ) -> R {
733 with_joints(app, |bodies, joints| {
734 let handle = joints.entity2impulse_joint()[&entity];
735 f(bodies, joints.impulse_joints.get(handle).unwrap())
736 })
737 }
738
739 #[cfg(feature = "dim2")]
740 #[test]
741 fn pin_slot_joint_constrains_motion() {
742 let mut app = test_app();
743 let ground = spawn_body(&mut app, RigidBody::Fixed, 0.0, 0.0);
744 let body = spawn_body(&mut app, RigidBody::Dynamic, 0.0, 0.0);
745 app.world_mut().entity_mut(body).insert((
746 Velocity {
747 linear: Vect::new(1.0, 0.0),
748 angular: 2.0,
749 },
750 ImpulseJoint::new(
751 ground,
752 PinSlotJointBuilder::new(Vect::X).contacts_enabled(false),
753 ),
754 ));
755 run(&mut app, 60);
756
757 let pos = translation(&app, body);
758 assert!(pos.x > 0.5, "the body should slide along the slot: {pos}");
759 assert!(pos.y.abs() < 1.0e-2, "the body should not fall: {pos}");
760 let angvel = app.world().get::<Velocity>(body).unwrap().angular;
761 assert!(angvel.abs() > 1.0, "rotations should be free: {angvel}");
762
763 let joint = app.world().get::<ImpulseJoint>(body).unwrap();
764 assert!(matches!(joint.data, TypedJoint::PinSlotJoint(_)));
765 assert!(joint.data.as_ref().as_pin_slot().is_some());
766 assert!(joint.data.as_ref().as_prismatic().is_none());
767 }
768
769 #[test]
770 fn disabled_impulse_joint_lets_bodies_separate() {
771 let mut app = test_app();
772 let ground = spawn_body(&mut app, RigidBody::Fixed, 0.0, 0.0);
773 let body = spawn_body(&mut app, RigidBody::Dynamic, 0.0, 0.0);
774 let joint = FixedJointBuilder::new().contacts_enabled(false);
775 app.world_mut()
776 .entity_mut(body)
777 .insert((ImpulseJoint::new(ground, joint), ImpulseJointDisabled));
778 run(&mut app, 30);
779
780 assert!(translation(&app, body).y < -0.5, "the body should fall");
781 raw_joint(&mut app, body, |_, j| assert!(!j.data.is_enabled()));
782 with_joints(&mut app, |bodies, joints| {
783 assert_eq!(joints.attached_impulse_joints(bodies, ground).count(), 1);
784 assert_eq!(
785 joints
786 .attached_enabled_impulse_joints(bodies, ground)
787 .count(),
788 0
789 );
790 });
791 assert_eq!(
792 *app.world().get::<ImpulseJointImpulses>(body).unwrap(),
793 ImpulseJointImpulses::default()
794 );
795
796 app.world_mut()
797 .entity_mut(body)
798 .remove::<ImpulseJointDisabled>();
799 run(&mut app, 120);
800
801 raw_joint(&mut app, body, |_, j| assert!(j.data.is_enabled()));
802 let pos = translation(&app, body);
803 assert!(
804 pos.length() < 0.1,
805 "the joint should pull the body back: {pos}"
806 );
807
808 app.world_mut()
809 .entity_mut(body)
810 .insert(ImpulseJointDisabled);
811 run(&mut app, 30);
812 raw_joint(&mut app, body, |_, j| assert!(!j.data.is_enabled()));
813 assert!(
814 translation(&app, body).y < -0.5,
815 "the body should fall again"
816 );
817 }
818
819 #[test]
820 fn disabled_impulse_joint_splits_islands() {
821 let mut app = test_app();
822 let body1 = spawn_body(&mut app, RigidBody::Dynamic, 0.0, 0.0);
823 let body2 = spawn_body(&mut app, RigidBody::Dynamic, 2.0, 0.0);
824 app.world_mut()
825 .entity_mut(body1)
826 .insert((GravityScale(0.0), Sleeping::default()));
827 app.world_mut().entity_mut(body2).insert((
828 GravityScale(0.0),
829 Sleeping::default(),
830 ImpulseJoint::new(body1, FixedJointBuilder::new().local_anchor1(Vect::X * 2.0)),
831 ));
832 run(&mut app, 2);
833
834 app.world_mut()
836 .entity_mut(body2)
837 .insert((ImpulseJointDisabled, Velocity::linear(Vect::X)));
838 run(&mut app, 240);
839 let sleeping = |app: &App, entity| app.world().get::<Sleeping>(entity).unwrap().sleeping;
840 assert!(sleeping(&app, body1));
841 assert!(!sleeping(&app, body2));
842
843 app.world_mut()
845 .entity_mut(body2)
846 .remove::<ImpulseJointDisabled>();
847 run(&mut app, 2);
848 assert!(!sleeping(&app, body1));
849 }
850
851 #[test]
852 fn impulses_are_written_back() {
853 let mut app = test_app();
854 let ground = spawn_body(&mut app, RigidBody::Fixed, 0.0, 0.0);
855 let body = spawn_body(&mut app, RigidBody::Dynamic, 1.0, 0.0);
856 app.world_mut().entity_mut(body).insert((
857 AdditionalMassProperties::Mass(10.0),
858 ImpulseJoint::new(
859 ground,
860 FixedJointBuilder::new()
861 .local_anchor1(Vect::X)
862 .contacts_enabled(false),
863 ),
864 ));
865 run(&mut app, 10);
866
867 let impulses = *app.world().get::<ImpulseJointImpulses>(body).unwrap();
868 assert!(
869 impulses.linear.length() > 1.0e-2,
870 "the joint should hold the body against gravity: {impulses:?}"
871 );
872 let from_context = with_joints(&mut app, |_, joints| {
873 joints.impulse_joint_impulses(body).unwrap()
874 });
875 assert_eq!(impulses, from_context);
876 }
877
878 #[test]
879 fn reparenting_moves_the_joint() {
880 let mut app = test_app();
881 let ground1 = spawn_body(&mut app, RigidBody::Fixed, 0.0, 0.0);
882 let ground2 = spawn_body(&mut app, RigidBody::Fixed, 5.0, 0.0);
883 let body = spawn_body(&mut app, RigidBody::Dynamic, 0.0, -1.0);
884 app.world_mut().entity_mut(body).insert(ImpulseJoint::new(
885 ground1,
886 FixedJointBuilder::new().local_anchor1(-Vect::Y),
887 ));
888 run(&mut app, 2);
889
890 let handle = app.world().get::<RapierImpulseJointHandle>(body).unwrap().0;
891 with_joints(&mut app, |bodies, joints| {
892 let between: Vec<_> = joints
893 .impulse_joints_between(bodies, ground1, body)
894 .collect();
895 assert_eq!(between, vec![body]);
896 let between: Vec<_> = joints
897 .impulse_joints_between(bodies, body, ground1)
898 .collect();
899 assert_eq!(between, vec![body]);
900 assert_eq!(
901 joints.impulse_joints_between(bodies, ground2, body).count(),
902 0
903 );
904 assert_eq!(joints.impulse_joint_entity(handle), Some(body));
905 });
906
907 app.world_mut()
908 .get_mut::<ImpulseJoint>(body)
909 .unwrap()
910 .parent = ground2;
911 run(&mut app, 120);
912
913 assert_eq!(
914 app.world().get::<RapierImpulseJointHandle>(body).unwrap().0,
915 handle
916 );
917 with_joints(&mut app, |bodies, joints| {
918 assert_eq!(
919 joints.impulse_joints_between(bodies, ground1, body).count(),
920 0
921 );
922 let between: Vec<_> = joints
923 .impulse_joints_between(bodies, ground2, body)
924 .collect();
925 assert_eq!(between, vec![body]);
926 let raw = joints.impulse_joints.get(handle).unwrap();
927 assert_eq!(raw.body1(), bodies.entity2body()[&ground2]);
928 });
929 let pos = translation(&app, body);
930 assert!((pos.x - 5.0).abs() < 0.1, "the body should follow: {pos}");
931
932 let pending = app
934 .world_mut()
935 .spawn(Transform::from_xyz(-5.0, 0.0, 0.0))
936 .id();
937 app.world_mut()
938 .get_mut::<ImpulseJoint>(body)
939 .unwrap()
940 .parent = pending;
941 run(&mut app, 2);
942 assert!(app.world().get::<RapierImpulseJointHandle>(body).is_none());
943 with_joints(&mut app, |_, joints| {
944 assert!(joints.impulse_joints.is_empty())
945 });
946
947 app.world_mut().entity_mut(pending).insert(RigidBody::Fixed);
948 run(&mut app, 2);
949 raw_joint(&mut app, body, |bodies, raw| {
950 assert_eq!(raw.body1(), bodies.entity2body()[&pending]);
951 });
952 }
953
954 #[test]
955 fn softness_round_trip() {
956 let softness = SpringCoefficients::new(10.0, 0.5);
957 let mut joint = PrismaticJointBuilder::new(Vect::X)
958 .softness(softness)
959 .build();
960 assert_eq!(joint.softness(), softness);
961 let other = SpringCoefficients::new(20.0, 1.0);
962 joint.set_softness(other);
963 assert_eq!(joint.softness(), other);
964 assert_eq!(joint.data.softness(), other);
965 assert_eq!(
966 GenericJointBuilder::new(JointAxesMask::LOCKED_FIXED_AXES)
967 .softness(softness)
968 .build()
969 .softness(),
970 softness
971 );
972
973 let mut app = test_app();
974 let ground = spawn_body(&mut app, RigidBody::Fixed, 0.0, 0.0);
975 let body = spawn_body(&mut app, RigidBody::Dynamic, 0.0, 0.0);
976 app.world_mut()
977 .entity_mut(body)
978 .insert(ImpulseJoint::new(ground, joint));
979 run(&mut app, 1);
980 raw_joint(&mut app, body, |_, j| assert_eq!(j.data.softness, other));
981
982 app.world_mut()
983 .get_mut::<ImpulseJoint>(body)
984 .unwrap()
985 .data
986 .as_mut()
987 .set_softness(softness);
988 run(&mut app, 1);
989 raw_joint(&mut app, body, |_, j| assert_eq!(j.data.softness, softness));
990 }
991
992 #[cfg(feature = "dim3")]
993 #[test]
994 fn spherical_joint_frames() {
995 let basis1 = Quat::from_rotation_z(0.5);
996 let basis2 = Quat::from_rotation_x(-0.3);
997 let joint = SphericalJointBuilder::new()
998 .local_anchor1(Vect::X)
999 .local_basis1(basis1)
1000 .local_basis2(basis2)
1001 .contacts_enabled(false)
1002 .build();
1003 assert!(joint.local_basis1().abs_diff_eq(basis1, 1.0e-6));
1004 assert!(joint.local_basis2().abs_diff_eq(basis2, 1.0e-6));
1005 assert_eq!(joint.local_frame1().translation, Vect::X);
1006 assert!(!joint.contacts_enabled());
1007
1008 let mut other = SphericalJoint::new();
1009 other.set_local_frame1(joint.local_frame1());
1010 assert_eq!(other.local_frame1(), joint.local_frame1());
1011 assert_eq!(other.local_anchor1(), Vect::X);
1012 }
1013
1014 #[test]
1015 fn changing_a_joint_wakes_up_its_bodies() {
1016 let mut app = test_app();
1017 let body1 = spawn_body(&mut app, RigidBody::Dynamic, 0.0, 0.0);
1018 let body2 = spawn_body(&mut app, RigidBody::Dynamic, 2.0, 0.0);
1019 app.world_mut()
1020 .entity_mut(body1)
1021 .insert((GravityScale(0.0), Sleeping::default()));
1022 app.world_mut().entity_mut(body2).insert((
1023 GravityScale(0.0),
1024 Sleeping::default(),
1025 ImpulseJoint::new(body1, FixedJointBuilder::new().local_anchor1(Vect::X * 2.0)),
1026 ));
1027 run(&mut app, 120);
1028 assert!(app.world().get::<Sleeping>(body1).unwrap().sleeping);
1029 assert!(app.world().get::<Sleeping>(body2).unwrap().sleeping);
1030
1031 app.world_mut()
1032 .get_mut::<ImpulseJoint>(body2)
1033 .unwrap()
1034 .data
1035 .as_mut()
1036 .set_contacts_enabled(false);
1037 app.update();
1038
1039 with_joints(&mut app, |bodies, _| {
1040 for entity in [body1, body2] {
1041 let rb = &bodies.bodies[bodies.entity2body()[&entity]];
1042 assert!(!rb.is_sleeping());
1043 }
1044 });
1045 }
1046
1047 fn with_joints_mut<R>(
1048 app: &mut App,
1049 f: impl FnOnce(&mut RapierRigidBodySet, &mut RapierContextJoints) -> R,
1050 ) -> R {
1051 let world = app.world_mut();
1052 let (mut bodies, mut joints) = world
1053 .query::<(&mut RapierRigidBodySet, &mut RapierContextJoints)>()
1054 .single_mut(world)
1055 .unwrap();
1056 f(&mut bodies, &mut joints)
1057 }
1058
1059 fn vect(x: f32, y: f32) -> Vect {
1060 #[cfg(feature = "dim2")]
1061 return Vect::new(x, y);
1062 #[cfg(feature = "dim3")]
1063 return Vect::new(x, y, 0.0);
1064 }
1065
1066 const ROT_AXIS: JointAxis = JointAxis::AngX;
1068
1069 fn revolute(anchor1: Vect) -> TypedJoint {
1072 #[cfg(feature = "dim2")]
1073 let builder = RevoluteJointBuilder::new();
1074 #[cfg(feature = "dim3")]
1075 let builder = RevoluteJointBuilder::new(Vect::Z);
1076 builder
1077 .local_anchor1(anchor1)
1078 .local_anchor2(-Vect::X)
1079 .build()
1080 .into()
1081 }
1082
1083 fn prismatic(anchor1: Vect) -> TypedJoint {
1085 PrismaticJointBuilder::new(Vect::X)
1086 .local_anchor1(anchor1)
1087 .build()
1088 .into()
1089 }
1090
1091 fn spawn_link(app: &mut App, parent: Entity, pos: Vect, joint: TypedJoint) -> Entity {
1093 let entity = spawn_body(app, RigidBody::Dynamic, pos.x, pos.y);
1094 app.world_mut().entity_mut(entity).insert((
1095 ColliderMassProperties::Mass(1.0),
1096 MultibodyJoint::new(parent, joint),
1097 ));
1098 entity
1099 }
1100
1101 fn joint_state(app: &App, entity: Entity) -> MultibodyJointState {
1102 *app.world().get::<MultibodyJointState>(entity).unwrap()
1103 }
1104
1105 #[test]
1106 fn kinematic_multibody_joint_ignores_gravity() {
1107 let mut app = test_app();
1108 let ground = spawn_body(&mut app, RigidBody::Fixed, 0.0, 0.0);
1109 let arm = spawn_link(&mut app, ground, Vect::X, revolute(Vect::ZERO));
1110 app.world_mut()
1111 .entity_mut(arm)
1112 .insert(KinematicMultibodyJoint);
1113 run(&mut app, 60);
1114
1115 let pos = translation(&app, arm);
1116 assert!(
1117 (pos.x - 1.0).abs() < 1.0e-3 && pos.y.abs() < 1.0e-3,
1118 "the kinematic arm should not fall: {pos}"
1119 );
1120 with_joints(&mut app, |bodies, joints| {
1121 let (mb, link_id) = joints.multibody(bodies, arm).unwrap();
1122 assert!(mb.link(link_id).unwrap().joint().kinematic);
1123 });
1124
1125 with_joints_mut(&mut app, |bodies, joints| {
1127 joints.multibody_joint_velocity_mut(bodies, arm).unwrap()[0] = 1.0;
1128 });
1129 run(&mut app, 30);
1130 let angle = joint_state(&app, arm).coords[ROT_AXIS as usize];
1131 assert!((angle - 0.5).abs() < 0.05, "the arm should rotate: {angle}");
1132
1133 app.world_mut()
1134 .entity_mut(arm)
1135 .remove::<KinematicMultibodyJoint>();
1136 with_joints_mut(&mut app, |bodies, joints| {
1137 joints.multibody_joint_velocity_mut(bodies, arm).unwrap()[0] = 0.0;
1138 });
1139 run(&mut app, 30);
1140 with_joints(&mut app, |bodies, joints| {
1141 let (mb, link_id) = joints.multibody(bodies, arm).unwrap();
1142 assert!(!mb.link(link_id).unwrap().joint().kinematic);
1143 });
1144 let angle = joint_state(&app, arm).coords[ROT_AXIS as usize];
1145 assert!(angle < 0.3, "the arm should fall once dynamic: {angle}");
1146 }
1147
1148 #[test]
1149 fn multibody_joint_friction_and_damping_slow_down_the_joint() {
1150 let mut app = test_app();
1151 let mut links = vec![];
1152 for (i, y) in [0.0, 5.0, 10.0].into_iter().enumerate() {
1153 let ground = spawn_body(&mut app, RigidBody::Fixed, 0.0, y);
1154 let link = spawn_link(&mut app, ground, vect(0.0, y), prismatic(Vect::ZERO));
1155 match i {
1156 1 => {
1157 let friction = MultibodyJointFriction::default().with(JointAxis::LinX, 1.0);
1158 app.world_mut().entity_mut(link).insert(friction);
1159 }
1160 2 => {
1161 let damping = MultibodyJointDamping::default().with(JointAxis::LinX, 1.0);
1162 app.world_mut().entity_mut(link).insert(damping);
1163 }
1164 _ => {}
1165 }
1166 links.push(link);
1167 }
1168 run(&mut app, 1);
1169 with_joints_mut(&mut app, |bodies, joints| {
1170 for link in &links {
1171 joints.multibody_joint_velocity_mut(bodies, *link).unwrap()[0] = 2.0;
1172 }
1173 });
1174 run(&mut app, 30);
1175
1176 let [free, friction, damping] =
1177 std::array::from_fn(|i| joint_state(&app, links[i]).velocities[0]);
1178 assert!(free > 1.95, "the free joint should keep moving: {free}");
1179 assert!(
1180 friction < 1.8 && friction > 0.0,
1181 "friction should slow down the joint: {friction}"
1182 );
1183 assert!(
1184 damping < 1.8 && damping > 0.0,
1185 "damping should slow down the joint: {damping}"
1186 );
1187
1188 app.world_mut()
1190 .entity_mut(links[1])
1191 .remove::<MultibodyJointFriction>();
1192 run(&mut app, 1);
1193 with_joints(&mut app, |bodies, joints| {
1194 let (mb, _) = joints.multibody(bodies, links[1]).unwrap();
1195 let dofs = joints.multibody_link_dofs(bodies, links[1]).unwrap();
1196 assert_eq!(dofs.len(), 1);
1197 assert_eq!(mb.frictions()[dofs.start], 0.0);
1198 });
1199 }
1200
1201 #[test]
1202 fn multibody_joint_spring_pulls_towards_rest() {
1203 let mut app = test_app();
1204 let ground = spawn_body(&mut app, RigidBody::Fixed, 0.0, 0.0);
1205 let link = spawn_link(&mut app, ground, Vect::ZERO, prismatic(Vect::ZERO));
1206 app.world_mut().entity_mut(link).insert((
1207 MultibodyJointSprings::default().with(JointAxis::LinX, 50.0, 1.0),
1208 MultibodyJointDamping::splat(5.0),
1209 ));
1210 run(&mut app, 300);
1211
1212 let coord = joint_state(&app, link).coords[0];
1213 assert!(
1214 (coord - 1.0).abs() < 0.05,
1215 "the spring should reach its rest position: {coord}"
1216 );
1217 assert!((translation(&app, link).x - 1.0).abs() < 0.05);
1218 with_joints(&mut app, |bodies, joints| {
1219 let (mb, link_id) = joints.multibody(bodies, link).unwrap();
1220 let joint = mb.link(link_id).unwrap().joint();
1221 assert_eq!(joint.spring(0), (50.0, 1.0));
1222 });
1223
1224 app.world_mut()
1225 .entity_mut(link)
1226 .remove::<MultibodyJointSprings>();
1227 run(&mut app, 1);
1228 with_joints(&mut app, |bodies, joints| {
1229 let (mb, link_id) = joints.multibody(bodies, link).unwrap();
1230 assert_eq!(mb.link(link_id).unwrap().joint().spring(0), (0.0, 0.0));
1231 });
1232 }
1233
1234 #[test]
1235 fn multibody_inverse_kinematics_reaches_target() {
1236 let mut app = test_app();
1237 let ground = spawn_body(&mut app, RigidBody::Fixed, 0.0, 0.0);
1238 let link1 = spawn_link(&mut app, ground, Vect::X, revolute(Vect::ZERO));
1239 let link2 = spawn_link(&mut app, link1, Vect::X * 2.0, revolute(Vect::ZERO));
1240 run(&mut app, 1);
1241
1242 let target = Transform::from_xyz(1.0, 1.0, 0.0);
1243 let options = InverseKinematicsOption {
1244 constrained_axes: JointAxesMask::LIN_X | JointAxesMask::LIN_Y,
1245 max_iters: 50,
1246 ..Default::default()
1247 };
1248 let distance = |bodies: &RapierRigidBodySet| {
1249 let body = &bodies.bodies[bodies.entity2body()[&link2]];
1250 let pos = body.translation();
1251 (vect(pos.x, pos.y) - vect(1.0, 1.0)).length()
1252 };
1253
1254 with_joints_mut(&mut app, |bodies, joints| {
1255 let before = distance(bodies);
1256 let mut displacements = vec![];
1257 assert!(joints.multibody_inverse_kinematics(
1258 bodies,
1259 link2,
1260 target,
1261 &options,
1262 |_| true,
1263 &mut displacements,
1264 ));
1265 assert_eq!(displacements.len(), 2);
1266 let mut reused = vec![10.0, -3.0, 7.0];
1268 assert!(joints.multibody_inverse_kinematics(
1269 bodies,
1270 link2,
1271 target,
1272 &options,
1273 |_| true,
1274 &mut reused,
1275 ));
1276 assert_eq!(reused, displacements);
1277 assert!(!joints.multibody_apply_displacements(bodies, link2, &[0.0]));
1278 assert!(joints.multibody_apply_displacements(bodies, link2, &displacements));
1279 assert!(joints.multibody_forward_kinematics(bodies, link2, false));
1280 let after = distance(bodies);
1281 assert!(
1282 after < before && after < 0.05,
1283 "IK should move the end effector to the target: {before} -> {after}"
1284 );
1285
1286 joints.multibody_inverse_kinematics(
1288 bodies,
1289 link2,
1290 Transform::from_xyz(-1.0, 1.0, 0.0),
1291 &options,
1292 |_| false,
1293 &mut displacements,
1294 );
1295 assert!(displacements.iter().all(|d| *d == 0.0));
1296 assert!(joints.multibody_body_jacobian(bodies, link2).is_some());
1297 });
1298 }
1299
1300 #[test]
1301 fn multibody_joint_state_is_written_back() {
1302 let mut app = test_app();
1303 let ground = spawn_body(&mut app, RigidBody::Fixed, 0.0, 0.0);
1304 let arm = spawn_link(&mut app, ground, Vect::X, revolute(Vect::ZERO));
1305 run(&mut app, 20);
1306
1307 let state = joint_state(&app, arm);
1308 let angle = state.coords[ROT_AXIS as usize];
1309 assert!(angle < -0.1, "the arm should swing down: {state:?}");
1310 assert!(state.velocities[ROT_AXIS as usize] < 0.0);
1311 assert_eq!(state.coords[0], 0.0);
1312
1313 let rotation = app.world().get::<Transform>(arm).unwrap().rotation;
1314 let (_, _, body_angle) = rotation.to_euler(EulerRot::XYZ);
1315 assert!(
1316 (body_angle - angle).abs() < 1.0e-3,
1317 "{body_angle} vs {angle}"
1318 );
1319 #[cfg(feature = "dim2")]
1320 assert!((state.rotation - angle).abs() < 1.0e-3);
1321 #[cfg(feature = "dim3")]
1322 assert!(state
1323 .rotation
1324 .abs_diff_eq(Quat::from_rotation_x(angle), 1.0e-3));
1325
1326 let from_context = with_joints(&mut app, |_, joints| {
1327 joints.multibody_joint_state(arm).unwrap()
1328 });
1329 assert_eq!(state, from_context);
1330 with_joints(&mut app, |bodies, joints| {
1331 let velocities = joints.multibody_generalized_velocity(bodies, arm).unwrap();
1332 assert_eq!(velocities, &[state.velocities[ROT_AXIS as usize]]);
1333 let raw = joints
1334 .multibody_joints
1335 .get(joints.entity2multibody_joint()[&arm]);
1336 let (mb, link_id) = raw.unwrap();
1337 assert_eq!(
1338 RapierContextJoints::entity_from_multibody_joint(mb.link(link_id).unwrap().joint()),
1339 arm
1340 );
1341 });
1342 }
1343
1344 #[test]
1345 fn multibody_topology_helpers() {
1346 let mut app = test_app();
1347 let ground = spawn_body(&mut app, RigidBody::Fixed, 0.0, 0.0);
1348 let other_ground = spawn_body(&mut app, RigidBody::Fixed, 0.0, 5.0);
1349 let b1 = spawn_link(&mut app, ground, Vect::X, revolute(Vect::ZERO));
1350 let b2 = spawn_link(&mut app, b1, Vect::X * 2.0, revolute(Vect::ZERO));
1351 let b3 = spawn_link(&mut app, b1, vect(2.0, -1.0), revolute(-Vect::Y));
1352 run(&mut app, 1);
1353
1354 with_joints(&mut app, |bodies, joints| {
1355 for entity in [ground, b1, b2, b3] {
1356 assert_eq!(joints.multibody_root(bodies, entity), Some(ground));
1357 }
1358 assert_eq!(joints.multibody_root(bodies, other_ground), None);
1359
1360 let links: Vec<_> = joints.multibody_links(bodies, b2).collect();
1361 assert_eq!(links.len(), 4);
1362 assert_eq!(links[0], ground);
1363 for entity in [b1, b2, b3] {
1364 assert!(links.contains(&entity));
1365 }
1366
1367 assert_eq!(joints.multibody_parent_link(bodies, ground), None);
1368 assert_eq!(joints.multibody_parent_link(bodies, b1), Some(ground));
1369 assert_eq!(joints.multibody_parent_link(bodies, b2), Some(b1));
1370 assert_eq!(joints.multibody_parent_link(bodies, b3), Some(b1));
1371
1372 assert_eq!(joints.multibody_joint_between(bodies, b1, b2), Some(b2));
1373 assert_eq!(joints.multibody_joint_between(bodies, b2, b1), Some(b2));
1374 assert_eq!(joints.multibody_joint_between(bodies, ground, b2), None);
1375
1376 let mut attached: Vec<_> = joints.attached_multibody_joints(bodies, b1).collect();
1377 attached.sort();
1378 let mut expected = vec![b1, b2, b3];
1379 expected.sort();
1380 assert_eq!(attached, expected);
1381 });
1382
1383 app.world_mut()
1385 .get_mut::<MultibodyJoint>(b1)
1386 .unwrap()
1387 .parent = other_ground;
1388 run(&mut app, 1);
1389 with_joints(&mut app, |bodies, joints| {
1390 assert_eq!(joints.multibody_parent_link(bodies, b1), Some(other_ground));
1391 assert_eq!(joints.multibody_root(bodies, b2), Some(other_ground));
1392 assert_eq!(joints.multibody_root(bodies, ground), None);
1393 });
1394
1395 let joint_entities: Vec<_> = with_joints(&mut app, |bodies, joints| {
1397 joints.multibody_links(bodies, b1).skip(1).collect()
1398 });
1399 for entity in joint_entities {
1400 app.world_mut()
1401 .entity_mut(entity)
1402 .remove::<MultibodyJoint>();
1403 }
1404 run(&mut app, 1);
1405 with_joints(&mut app, |bodies, joints| {
1406 assert_eq!(joints.multibody_joints.multibodies().count(), 0);
1407 assert_eq!(joints.multibody_root(bodies, b1), None);
1408 });
1409 }
1410
1411 #[test]
1412 fn multibody_dof_couplings() {
1413 let mut app = test_app();
1414 let ground = spawn_body(&mut app, RigidBody::Fixed, 0.0, 0.0);
1415 let b1 = spawn_link(&mut app, ground, Vect::Y, prismatic(Vect::Y));
1416 let b2 = spawn_link(&mut app, ground, -Vect::Y, prismatic(-Vect::Y));
1417 app.world_mut()
1418 .entity_mut(b2)
1419 .insert(MultibodyJointCouplings(vec![MultibodyJointCoupling::new(
1420 JointAxis::LinX,
1421 b1,
1422 JointAxis::LinX,
1423 -1.0,
1424 0.0,
1425 )]));
1426 run(&mut app, 1);
1427 with_joints_mut(&mut app, |bodies, joints| {
1428 joints.multibody_joint_velocity_mut(bodies, b1).unwrap()[0] = 1.0;
1429 });
1430 run(&mut app, 30);
1431
1432 let x1 = joint_state(&app, b1).coords[0];
1433 let x2 = joint_state(&app, b2).coords[0];
1434 assert!(x1 > 0.1, "the first joint should move: {x1}");
1435 assert!(
1436 (x1 + x2).abs() < 1.0e-2,
1437 "the joints should be coupled: {x1} {x2}"
1438 );
1439 let num_couplings = |app: &mut App| {
1440 with_joints(app, |bodies, joints| {
1441 joints.multibody(bodies, b1).unwrap().0.couplings().len()
1442 })
1443 };
1444 assert_eq!(num_couplings(&mut app), 1);
1445
1446 app.world_mut()
1448 .entity_mut(b2)
1449 .remove::<MultibodyJointCouplings>();
1450 run(&mut app, 1);
1451 assert_eq!(num_couplings(&mut app), 0);
1452 let new_x2 = joint_state(&app, b2).coords[0];
1453 assert!((new_x2 - x2).abs() < 0.1, "{x2} -> {new_x2}");
1454 assert_eq!(
1455 with_joints(&mut app, |bodies, joints| joints.multibody_root(bodies, b2)),
1456 Some(ground)
1457 );
1458 }
1459
1460 #[test]
1461 fn multibody_couplings_and_self_contacts_follow_topology_changes() {
1462 let mut app = test_app();
1463 let ground = spawn_body(&mut app, RigidBody::Fixed, 0.0, 0.0);
1464 let b1 = spawn_link(&mut app, ground, Vect::Y, prismatic(Vect::Y));
1465 let b2 = spawn_body(&mut app, RigidBody::Dynamic, 0.0, -1.0);
1467 let coupling = |coeff| {
1468 MultibodyJointCouplings(vec![MultibodyJointCoupling::new(
1469 JointAxis::LinX,
1470 b1,
1471 JointAxis::LinX,
1472 coeff,
1473 0.0,
1474 )])
1475 };
1476 app.world_mut().entity_mut(b2).insert((
1477 ColliderMassProperties::Mass(1.0),
1478 MultibodySelfContactsDisabled,
1479 coupling(-1.0),
1480 ));
1481 run(&mut app, 1);
1482
1483 let read = |app: &mut App| {
1484 with_joints(app, |bodies, joints| {
1485 let (mb, _) = joints.multibody(bodies, b1).unwrap();
1486 let coeffs: Vec<_> = mb.couplings().iter().map(|c| c.coeff).collect();
1487 (coeffs, mb.self_contacts_enabled())
1488 })
1489 };
1490 assert_eq!(read(&mut app), (vec![], true));
1491
1492 app.world_mut()
1493 .entity_mut(b2)
1494 .insert(MultibodyJoint::new(ground, prismatic(-Vect::Y)));
1495 run(&mut app, 1);
1496 assert_eq!(read(&mut app), (vec![-1.0], false));
1497
1498 app.world_mut().entity_mut(b2).insert(coupling(2.0));
1500 run(&mut app, 1);
1501 assert_eq!(read(&mut app), (vec![2.0], false));
1502
1503 let b3 = spawn_link(&mut app, b1, Vect::Y * 2.0, prismatic(Vect::Y));
1505 run(&mut app, 1);
1506 assert_eq!(read(&mut app), (vec![2.0], false));
1507 assert_eq!(
1508 with_joints(&mut app, |bodies, joints| joints.multibody_root(bodies, b3)),
1509 Some(ground)
1510 );
1511
1512 app.world_mut()
1514 .get_mut::<MultibodyJoint>(b1)
1515 .unwrap()
1516 .parent = b2;
1517 run(&mut app, 1);
1518 assert_eq!(read(&mut app), (vec![2.0], false));
1519 with_joints(&mut app, |bodies, joints| {
1520 let parent = |entity| {
1521 let (mb, link_id) = joints.multibody(bodies, entity).unwrap();
1522 let parent_id = mb.link(link_id).unwrap().parent_id().unwrap();
1523 mb.link(parent_id).unwrap().rigid_body_handle()
1524 };
1525 assert_eq!(parent(b1), bodies.entity2body()[&b2]);
1526 });
1527 }
1528
1529 #[test]
1530 fn multibody_self_contacts_armature_and_damping() {
1531 let mut app = test_app();
1532 let ground = spawn_body(&mut app, RigidBody::Fixed, 0.0, 0.0);
1533 let arm = spawn_link(&mut app, ground, Vect::X, revolute(Vect::ZERO));
1534 app.world_mut()
1535 .entity_mut(ground)
1536 .insert(MultibodySelfContactsDisabled);
1537 app.world_mut().entity_mut(arm).insert((
1538 MultibodyJointArmature::splat(0.5),
1539 MultibodyJointDamping::splat(2.0),
1540 ));
1541 run(&mut app, 1);
1542
1543 let read = |app: &mut App| {
1544 with_joints(app, |bodies, joints| {
1545 let (mb, _) = joints.multibody(bodies, arm).unwrap();
1546 let dof = joints.multibody_link_dofs(bodies, arm).unwrap().start;
1547 (
1548 mb.self_contacts_enabled(),
1549 mb.armature()[dof],
1550 mb.damping()[dof],
1551 )
1552 })
1553 };
1554 assert_eq!(read(&mut app), (false, 0.5, 2.0));
1555
1556 app.world_mut()
1557 .entity_mut(ground)
1558 .remove::<MultibodySelfContactsDisabled>();
1559 app.world_mut()
1560 .entity_mut(arm)
1561 .remove::<(MultibodyJointArmature, MultibodyJointDamping)>();
1562 run(&mut app, 1);
1563 let default_damping = MultibodyJointDamping::default().0[ROT_AXIS as usize];
1564 assert_eq!(read(&mut app), (true, 0.0, default_damping));
1565 }
1566
1567 #[test]
1568 fn multibody_joint_properties_apply_to_late_joints() {
1569 let mut app = test_app();
1570 let ground = app
1571 .world_mut()
1572 .spawn(Transform::from_xyz(0.0, 0.0, 0.0))
1573 .id();
1574 let link = spawn_link(&mut app, ground, Vect::ZERO, prismatic(Vect::ZERO));
1575 app.world_mut().entity_mut(link).insert((
1576 KinematicMultibodyJoint,
1577 MultibodyJointSprings::default().with(JointAxis::LinX, 10.0, 2.0),
1578 ));
1579 run(&mut app, 2);
1580 assert!(app
1581 .world()
1582 .get::<RapierMultibodyJointHandle>(link)
1583 .is_none());
1584
1585 app.world_mut().entity_mut(ground).insert(RigidBody::Fixed);
1587 run(&mut app, 1);
1588 with_joints(&mut app, |bodies, joints| {
1589 let (mb, link_id) = joints.multibody(bodies, link).unwrap();
1590 let joint = mb.link(link_id).unwrap().joint();
1591 assert!(joint.kinematic);
1592 assert_eq!(joint.spring(0), (10.0, 2.0));
1593 });
1594 }
1595}