From 5f687b0d29a459dc62219da067dadce617356cd7 Mon Sep 17 00:00:00 2001 From: legendofa <34162874+legendofa@users.noreply.github.com> Date: Fri, 14 Nov 2025 19:00:36 +0900 Subject: [PATCH] Additional `PinSlotJoint` 2D implementation with DOF: 1 translation + 1 rotation (#775) * Additional groove joint 2D implementation with 1 translation + 1 rotation * Conditional import for 2d feature flag * Groove joint 2d tests in testbed examples * Renamed GrooveJoint to PinSlotJoint * cargo fmt * Cross-reference to groove joint in Godot engine --- examples2d/all_examples2.rs | 2 + examples2d/pin_slot_joint2.rs | 86 +++++++++ src/dynamics/joint/generic_joint.rs | 2 + src/dynamics/joint/mod.rs | 2 + src/dynamics/joint/pin_slot_joint.rs | 276 +++++++++++++++++++++++++++ 5 files changed, 368 insertions(+) create mode 100644 examples2d/pin_slot_joint2.rs create mode 100644 src/dynamics/joint/pin_slot_joint.rs diff --git a/examples2d/all_examples2.rs b/examples2d/all_examples2.rs index 58c1a8e..a6629cb 100644 --- a/examples2d/all_examples2.rs +++ b/examples2d/all_examples2.rs @@ -26,6 +26,7 @@ mod joint_motor_position2; mod joints2; mod locked_rotations2; mod one_way_platforms2; +mod pin_slot_joint2; mod platform2; mod polyline2; mod pyramid2; @@ -66,6 +67,7 @@ pub fn main() { ("One-way platforms", one_way_platforms2::init_world), ("Platform", platform2::init_world), ("Polyline", polyline2::init_world), + ("Pin Slot Joint", pin_slot_joint2::init_world), ("Pyramid", pyramid2::init_world), ("Restitution", restitution2::init_world), ("Rope Joints", rope_joints2::init_world), diff --git a/examples2d/pin_slot_joint2.rs b/examples2d/pin_slot_joint2.rs new file mode 100644 index 0000000..1c8100b --- /dev/null +++ b/examples2d/pin_slot_joint2.rs @@ -0,0 +1,86 @@ +use rapier2d::prelude::*; +use rapier_testbed2d::Testbed; + +pub fn init_world(testbed: &mut Testbed) { + /* + * World + */ + let mut bodies = RigidBodySet::new(); + let mut colliders = ColliderSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); + + /* + * Ground + */ + let ground_size = 3.0; + let ground_height = 0.1; + + let rigid_body_floor = RigidBodyBuilder::fixed().translation(vector![0.0, -ground_height]); + let floor_handle = bodies.insert(rigid_body_floor); + let floor_collider = ColliderBuilder::cuboid(ground_size, ground_height); + colliders.insert_with_parent(floor_collider, floor_handle, &mut bodies); + + /* + * Character we will control manually. + */ + let rigid_body_character = + RigidBodyBuilder::kinematic_position_based().translation(vector![0.0, 0.3]); + let character_handle = bodies.insert(rigid_body_character); + let character_collider = ColliderBuilder::cuboid(0.15, 0.3); + colliders.insert_with_parent(character_collider, character_handle, &mut bodies); + + /* + * Tethered cube. + */ + let rad = 0.4; + + let rigid_body_cube = + RigidBodyBuilder::new(RigidBodyType::Dynamic).translation(vector![1.0, 1.0]); + let cube_handle = bodies.insert(rigid_body_cube); + let cube_collider = ColliderBuilder::cuboid(rad, rad); + colliders.insert_with_parent(cube_collider, cube_handle, &mut bodies); + + /* + * Rotation axis indicator ball. + */ + let rigid_body_ball = + RigidBodyBuilder::new(RigidBodyType::Dynamic).translation(vector![1.0, 1.0]); + let ball_handle = bodies.insert(rigid_body_ball); + let ball_collider = ColliderBuilder::ball(0.1); + colliders.insert_with_parent(ball_collider, ball_handle, &mut bodies); + + /* + * Fixed joint between rotation axis indicator and cube. + */ + let fixed_joint = FixedJointBuilder::new() + .local_anchor1(point![0.0, 0.0]) + .local_anchor2(point![0.0, -0.4]) + .build(); + impulse_joints.insert(cube_handle, ball_handle, fixed_joint, true); + + /* + * Pin slot joint between cube and ground. + */ + let axis: nalgebra::Unit< + nalgebra::Matrix< + f32, + nalgebra::Const<2>, + nalgebra::Const<1>, + nalgebra::ArrayStorage, + >, + > = UnitVector::new_normalize(vector![1.0, 1.0]); + let pin_slot_joint = PinSlotJointBuilder::new(axis) + .local_anchor1(point![2.0, 2.0]) + .local_anchor2(point![0.0, 0.4]) + .limits([-1.0, f32::INFINITY]) // Set the limits for the pin slot joint + .build(); + impulse_joints.insert(character_handle, cube_handle, pin_slot_joint, true); + + /* + * Set up the testbed. + */ + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); + testbed.set_character_body(character_handle); + testbed.look_at(point![0.0, 1.0], 100.0); +} diff --git a/src/dynamics/joint/generic_joint.rs b/src/dynamics/joint/generic_joint.rs index df03b9c..03493dd 100644 --- a/src/dynamics/joint/generic_joint.rs +++ b/src/dynamics/joint/generic_joint.rs @@ -67,6 +67,8 @@ bitflags::bitflags! { const LOCKED_REVOLUTE_AXES = Self::LIN_X.bits() | Self::LIN_Y.bits(); /// The set of degrees of freedom locked by a prismatic joint. const LOCKED_PRISMATIC_AXES = Self::LIN_Y.bits() | Self::ANG_X.bits(); + /// The set of degrees of freedom locked by a pin slot joint. + const LOCKED_PIN_SLOT_AXES = Self::LIN_Y.bits(); /// The set of degrees of freedom locked by a fixed joint. const LOCKED_FIXED_AXES = Self::LIN_X.bits() | Self::LIN_Y.bits() | Self::ANG_X.bits(); /// The set of degrees of freedom left free by a revolute joint. diff --git a/src/dynamics/joint/mod.rs b/src/dynamics/joint/mod.rs index 423d4c2..8002505 100644 --- a/src/dynamics/joint/mod.rs +++ b/src/dynamics/joint/mod.rs @@ -3,6 +3,7 @@ pub use self::generic_joint::*; pub use self::impulse_joint::*; pub use self::motor_model::MotorModel; pub use self::multibody_joint::*; +pub use self::pin_slot_joint::*; pub use self::prismatic_joint::*; pub use self::revolute_joint::*; pub use self::rope_joint::*; @@ -16,6 +17,7 @@ mod generic_joint; mod impulse_joint; mod motor_model; mod multibody_joint; +mod pin_slot_joint; mod prismatic_joint; mod revolute_joint; mod rope_joint; diff --git a/src/dynamics/joint/pin_slot_joint.rs b/src/dynamics/joint/pin_slot_joint.rs new file mode 100644 index 0000000..3980a7a --- /dev/null +++ b/src/dynamics/joint/pin_slot_joint.rs @@ -0,0 +1,276 @@ +#[cfg(feature = "dim2")] +use crate::dynamics::joint::{GenericJointBuilder, JointAxesMask}; + +use crate::dynamics::joint::GenericJoint; +use crate::dynamics::{JointAxis, MotorModel}; +use crate::math::{Point, Real, UnitVector}; + +use super::{JointLimits, JointMotor}; + +#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] +#[derive(Copy, Clone, Debug, PartialEq)] +#[repr(transparent)] +/// A pin slot joint, locks all relative motion between two bodies except for translation along the joint’s principal axis and relative rotations. +/// This joint is also known as a [groove joint in Godot](https://docs.godotengine.org/en/stable/classes/class_groovejoint2d.html). +pub struct PinSlotJoint { + /// The underlying joint data. + pub data: GenericJoint, +} + +impl PinSlotJoint { + /// Creates a new pin slot joint allowing only relative translations along the specified axis and relative rotations. + /// + /// This axis is expressed in the local-space of both rigid-bodies. + #[cfg(feature = "dim2")] + pub fn new(axis: UnitVector) -> Self { + let data = GenericJointBuilder::new(JointAxesMask::LOCKED_PIN_SLOT_AXES) + .local_axis1(axis) + .local_axis2(axis) + .build(); + Self { data } + } + + /// The underlying generic joint. + pub fn data(&self) -> &GenericJoint { + &self.data + } + + /// Are contacts between the attached rigid-bodies enabled? + pub fn contacts_enabled(&self) -> bool { + self.data.contacts_enabled + } + + /// Sets whether contacts between the attached rigid-bodies are enabled. + pub fn set_contacts_enabled(&mut self, enabled: bool) -> &mut Self { + self.data.set_contacts_enabled(enabled); + self + } + + /// The joint’s anchor, expressed in the local-space of the first rigid-body. + #[must_use] + pub fn local_anchor1(&self) -> Point { + self.data.local_anchor1() + } + + /// Sets the joint’s anchor, expressed in the local-space of the first rigid-body. + pub fn set_local_anchor1(&mut self, anchor1: Point) -> &mut Self { + self.data.set_local_anchor1(anchor1); + self + } + + /// The joint’s anchor, expressed in the local-space of the second rigid-body. + #[must_use] + pub fn local_anchor2(&self) -> Point { + self.data.local_anchor2() + } + + /// Sets the joint’s anchor, expressed in the local-space of the second rigid-body. + pub fn set_local_anchor2(&mut self, anchor2: Point) -> &mut Self { + self.data.set_local_anchor2(anchor2); + self + } + + /// The principal axis of the joint, expressed in the local-space of the first rigid-body. + #[must_use] + pub fn local_axis1(&self) -> UnitVector { + self.data.local_axis1() + } + + /// Sets the principal axis of the joint, expressed in the local-space of the first rigid-body. + pub fn set_local_axis1(&mut self, axis1: UnitVector) -> &mut Self { + self.data.set_local_axis1(axis1); + self + } + + /// The principal axis of the joint, expressed in the local-space of the second rigid-body. + #[must_use] + pub fn local_axis2(&self) -> UnitVector { + self.data.local_axis2() + } + + /// Sets the principal axis of the joint, expressed in the local-space of the second rigid-body. + pub fn set_local_axis2(&mut self, axis2: UnitVector) -> &mut Self { + self.data.set_local_axis2(axis2); + self + } + + /// The motor affecting the joint’s translational degree of freedom. + #[must_use] + pub fn motor(&self) -> Option<&JointMotor> { + self.data.motor(JointAxis::LinX) + } + + /// Set the spring-like model used by the motor to reach the desired target velocity and position. + pub fn set_motor_model(&mut self, model: MotorModel) -> &mut Self { + self.data.set_motor_model(JointAxis::LinX, model); + self + } + + /// Sets the target velocity this motor needs to reach. + pub fn set_motor_velocity(&mut self, target_vel: Real, factor: Real) -> &mut Self { + self.data + .set_motor_velocity(JointAxis::LinX, target_vel, factor); + self + } + + /// Sets the target angle this motor needs to reach. + pub fn set_motor_position( + &mut self, + target_pos: Real, + stiffness: Real, + damping: Real, + ) -> &mut Self { + self.data + .set_motor_position(JointAxis::LinX, target_pos, stiffness, damping); + self + } + + /// Configure both the target angle and target velocity of the motor. + pub fn set_motor( + &mut self, + target_pos: Real, + target_vel: Real, + stiffness: Real, + damping: Real, + ) -> &mut Self { + self.data + .set_motor(JointAxis::LinX, target_pos, target_vel, stiffness, damping); + self + } + + /// Sets the maximum force the motor can deliver. + pub fn set_motor_max_force(&mut self, max_force: Real) -> &mut Self { + self.data.set_motor_max_force(JointAxis::LinX, max_force); + self + } + + /// The limit distance attached bodies can translate along the joint’s principal axis. + #[must_use] + pub fn limits(&self) -> Option<&JointLimits> { + self.data.limits(JointAxis::LinX) + } + + /// Sets the `[min,max]` limit distances attached bodies can translate along the joint’s principal axis. + pub fn set_limits(&mut self, limits: [Real; 2]) -> &mut Self { + self.data.set_limits(JointAxis::LinX, limits); + self + } +} + +impl From for GenericJoint { + fn from(val: PinSlotJoint) -> GenericJoint { + val.data + } +} + +/// Create pin slot joints using the builder pattern. +/// +/// A pin slot joint locks all relative motion except for translations along the joint’s principal axis and relative rotations. +#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct PinSlotJointBuilder(pub PinSlotJoint); + +impl PinSlotJointBuilder { + /// Creates a new builder for pin slot joints. + /// + /// This axis is expressed in the local-space of both rigid-bodies. + #[cfg(feature = "dim2")] + pub fn new(axis: UnitVector) -> Self { + Self(PinSlotJoint::new(axis)) + } + + /// Sets whether contacts between the attached rigid-bodies are enabled. + #[must_use] + pub fn contacts_enabled(mut self, enabled: bool) -> Self { + self.0.set_contacts_enabled(enabled); + self + } + + /// Sets the joint’s anchor, expressed in the local-space of the first rigid-body. + #[must_use] + pub fn local_anchor1(mut self, anchor1: Point) -> Self { + self.0.set_local_anchor1(anchor1); + self + } + + /// Sets the joint’s anchor, expressed in the local-space of the second rigid-body. + #[must_use] + pub fn local_anchor2(mut self, anchor2: Point) -> Self { + self.0.set_local_anchor2(anchor2); + self + } + + /// Sets the principal axis of the joint, expressed in the local-space of the first rigid-body. + #[must_use] + pub fn local_axis1(mut self, axis1: UnitVector) -> Self { + self.0.set_local_axis1(axis1); + self + } + + /// Sets the principal axis of the joint, expressed in the local-space of the second rigid-body. + #[must_use] + pub fn local_axis2(mut self, axis2: UnitVector) -> Self { + self.0.set_local_axis2(axis2); + self + } + + /// Set the spring-like model used by the motor to reach the desired target velocity and position. + #[must_use] + pub fn motor_model(mut self, model: MotorModel) -> Self { + self.0.set_motor_model(model); + self + } + + /// Sets the target velocity this motor needs to reach. + #[must_use] + pub fn motor_velocity(mut self, target_vel: Real, factor: Real) -> Self { + self.0.set_motor_velocity(target_vel, factor); + self + } + + /// Sets the target angle this motor needs to reach. + #[must_use] + pub fn motor_position(mut self, target_pos: Real, stiffness: Real, damping: Real) -> Self { + self.0.set_motor_position(target_pos, stiffness, damping); + self + } + + /// Configure both the target angle and target velocity of the motor. + #[must_use] + pub fn set_motor( + mut self, + target_pos: Real, + target_vel: Real, + stiffness: Real, + damping: Real, + ) -> Self { + self.0.set_motor(target_pos, target_vel, stiffness, damping); + self + } + + /// Sets the maximum force the motor can deliver. + #[must_use] + pub fn motor_max_force(mut self, max_force: Real) -> Self { + self.0.set_motor_max_force(max_force); + self + } + + /// Sets the `[min,max]` limit distances attached bodies can translate along the joint’s principal axis. + #[must_use] + pub fn limits(mut self, limits: [Real; 2]) -> Self { + self.0.set_limits(limits); + self + } + + /// Builds the pin slot joint. + #[must_use] + pub fn build(self) -> PinSlotJoint { + self.0 + } +} + +impl From for GenericJoint { + fn from(val: PinSlotJointBuilder) -> GenericJoint { + val.0.into() + } +}