Source code for box2d.joint

# joint.py

from box2d._box2d import lib, ffi
from abc import ABC, abstractmethod
from .math import Vec2


[docs] class Joint(ABC): """Base class for all physics joints connecting two rigid bodies. Manages the lifecycle and common properties of constraints between bodies, such as anchors and collision handling between connected bodies. """
[docs] def __init__(self, world, body_a, body_b, collide_connected=False): """Initialize a joint between two bodies. Args: world: The physics world where the joint exists body_a: First body to connect (must be movable/dynamic) body_b: Second body to connect (can be static or dynamic) collide_connected: Whether connected bodies should collide with each other """ pass
def _set_userdata(self): """Finalize joint creation in the physics simulation. Should be called after joint configuration is complete. Handles the internal connection between the joint definition and simulation. """ self._joint_handle = ffi.new_handle(self) lib.b2Joint_SetUserData(self._joint_id, self._joint_handle)
[docs] def destroy(self): """Destroy the joint and remove it from the world.""" if self._joint_id and lib.b2Joint_IsValid(self._joint_id): lib.b2DestroyJoint(self._joint_id) self._joint_id = None
@property def is_valid(self): """Check if the joint is currently active in the simulation. Returns: True if the joint still connects its bodies, False if it has been removed or destroyed """ return lib.b2Joint_IsValid(self._joint_id) @property def body_a(self): """Get the first body connected by this joint. Returns: Body: The dynamic body that initiated the joint connection """ return ffi.from_handle( lib.b2Body_GetUserData(lib.b2Joint_GetBodyA(self._joint_id)) ) @property def body_b(self): """Get the second body connected by this joint. Returns: Body: The partner body (can be static or dynamic) """ return ffi.from_handle( lib.b2Body_GetUserData(lib.b2Joint_GetBodyB(self._joint_id)) ) @property def anchor_a(self): """Local connection point on the first body. Returns: Vec2: Position where the joint attaches to body_a in its local coordinates """ vec = lib.b2Joint_GetLocalAnchorA(self._joint_id) return Vec2(vec.x, vec.y) @property def anchor_b(self): """Local connection point on the second body. Returns: Vec2: Position where the joint attaches to body_b in its local coordinates """ vec = lib.b2Joint_GetLocalAnchorB(self._joint_id) return Vec2(vec.x, vec.y) @property def constraint_force(self): """Current force exerted by the joint to maintain its constraint. Returns: Vec2: Constraint force vector in world coordinates """ vec = lib.b2Joint_GetConstraintForce(self._joint_id) return Vec2(vec.x, vec.y) @property def constraint_torque(self): """Current torque exerted by the joint to maintain rotation constraints. Returns: float: Constraint torque value """ return lib.b2Joint_GetConstraintTorque(self._joint_id) @property def collide_connected(self) -> bool: """Check if connected bodies can collide with each other. Returns: bool: True if connected bodies can collide, False otherwise """ return lib.b2Joint_GetCollideConnected(self._joint_id) @collide_connected.setter def collide_connected(self, collide: bool): """Control whether connected bodies can collide with each other. Args: collide: True to enable collisions between bodies, False to disable """ lib.b2Joint_SetCollideConnected(self._joint_id, collide)
[docs] def wake_bodies(self): """Ensure connected bodies are active and responsive to movement. Useful when restarting dragging after bodies entered sleep state. """ lib.b2Joint_WakeBodies(self._joint_id)
[docs] class MouseJoint(Joint): """Interactive joint for dragging bodies with mouse-like movement. Designed for smoothly pulling dynamic bodies to target positions, with spring-like behavior controls for realistic manipulation. """
[docs] def __init__( self, world, body, target, max_force=1000.0, damping_ratio=0.7, hertz=5.0 ): """Create a drag-and-move joint for interactive manipulation. Args: world: The physics world where the joint exists body: Dynamic body to be dragged target: Initial world position (x,y) to pull toward max_force: Maximum force allowed for movement damping_ratio: Spring damping hertz: Spring stiffness in Hz """ self._target = Vec2(*target) self._max_force = max_force self._damping_ratio = damping_ratio self._hertz = hertz self.world = world defn = lib.b2DefaultMouseJointDef() defn.bodyIdA = body._body_id defn.bodyIdB = body._body_id defn.target = self._target.b2Vec2[0] defn.maxForce = self._max_force defn.dampingRatio = self._damping_ratio defn.collideConnected = False defn.hertz = self._hertz self._def = defn self._joint_id = lib.b2CreateMouseJoint( self.world._world_id, ffi.addressof(self._def) ) self._set_userdata() self.wake_bodies()
@property def target(self): """Current target position to drag toward. Returns: Vec2: World coordinates of the drag target """ return Vec2.from_b2Vec2(lib.b2MouseJoint_GetTarget(self._joint_id)) @target.setter def target(self, value): """Update the position being dragged toward. Args: value (tuple/Vec2): New target position in world coordinates """ vec = ffi.new("b2Vec2*", {"x": value[0], "y": value[1]}) lib.b2MouseJoint_SetTarget(self._joint_id, vec[0]) @property def max_force(self): """Maximum pulling force available to move the body.""" return lib.b2MouseJoint_GetMaxForce(self._joint_id) @max_force.setter def max_force(self, value): """Adjust maximum pulling force.""" lib.b2MouseJoint_SetMaxForce(self._joint_id, float(value)) @property def damping_ratio(self): """Spring damping controlling movement smoothness.""" return lib.b2MouseJoint_GetSpringDampingRatio(self._joint_id) @damping_ratio.setter def damping_ratio(self, value): """Set how quickly movement stabilizes at target.""" lib.b2MouseJoint_SetSpringDampingRatio(self._joint_id, float(value))
class WeldJoint(Joint): """WeldJoint connects two bodies rigidly, fully constraining their relative translation and rotation while allowing for softness when spring parameters are configured. The weld joint "welds" two bodies together using provided local anchor points. These anchors are specified in each body's local coordinate system. Args: world: The physics world instance. body_a: The first body to be joined. body_b: The second body to be joined. local_anchor_a (tuple): Local coordinates (x, y) on body_a where the joint is attached. local_anchor_b (tuple): Local coordinates (x, y) on body_b where the joint is attached. collide_connected (bool, optional): If True, the connected bodies will collide. linear_hertz (float, optional): Linear spring stiffness in Hertz linear_damping_ratio (float, optional): Linear damping ratio angular_hertz (float, optional): Angular spring stiffness in Hertz angular_damping_ratio (float, optional): Angular damping ratio reference_angle (float, optional): The reference angle between the two bodies. """ def __init__( self, world, body_a, body_b, local_anchor_a, local_anchor_b, collide_connected=False, linear_hertz=None, linear_damping_ratio=None, angular_hertz=None, angular_damping_ratio=None, reference_angle=None, ): self._local_anchor_a = Vec2(*local_anchor_a) self._local_anchor_b = Vec2(*local_anchor_b) self._linear_hertz = linear_hertz self._linear_damping_ratio = linear_damping_ratio self._angular_hertz = angular_hertz self._angular_damping_ratio = angular_damping_ratio self._reference_angle = reference_angle self.world = world defn = lib.b2DefaultWeldJointDef() defn.bodyIdA = body_a._body_id defn.bodyIdB = body_b._body_id defn.collideConnected = collide_connected # Use the provided local anchor points directly. defn.localAnchorA = self._local_anchor_a.b2Vec2[0] defn.localAnchorB = self._local_anchor_b.b2Vec2[0] if self._reference_angle is not None: defn.referenceAngle = self._reference_angle # Set the spring/damping parameters to allow for soft welding. if self._linear_hertz is not None: defn.linearHertz = self._linear_hertz if self._linear_damping_ratio is not None: defn.linearDampingRatio = self._linear_damping_ratio if self._angular_hertz is not None: defn.angularHertz = self._angular_hertz if self._angular_damping_ratio is not None: defn.angularDampingRatio = self._angular_damping_ratio self._def = defn self._joint_id = lib.b2CreateWeldJoint( self.world._world_id, ffi.addressof(self._def) ) self._set_userdata() @property def linear_hertz(self): """The linear stiffness (in Hertz) of the weld joint spring.""" return lib.b2WeldJoint_GetLinearHertz(self._joint_id) @linear_hertz.setter def linear_hertz(self, value): lib.b2WeldJoint_SetLinearHertz(self._joint_id, float(value)) @property def linear_damping_ratio(self): """The linear damping ratio (non-dimensional) of the weld joint spring.""" return lib.b2WeldJoint_GetLinearDampingRatio(self._joint_id) @linear_damping_ratio.setter def linear_damping_ratio(self, value): lib.b2WeldJoint_SetLinearDampingRatio(self._joint_id, float(value)) @property def angular_hertz(self): """The angular stiffness (in Hertz) of the weld joint.""" return lib.b2WeldJoint_GetAngularHertz(self._joint_id) @angular_hertz.setter def angular_hertz(self, value): lib.b2WeldJoint_SetAngularHertz(self._joint_id, float(value)) @property def angular_damping_ratio(self): """The angular damping ratio (non-dimensional) of the weld joint.""" return lib.b2WeldJoint_GetAngularDampingRatio(self._joint_id) @angular_damping_ratio.setter def angular_damping_ratio(self, value): lib.b2WeldJoint_SetAngularDampingRatio(self._joint_id, float(value)) class RevoluteJoint(Joint): """ RevoluteJoint connects two bodies at a pair of anchor points, allowing for relative rotation about a shared axis. Optionally, joint limits and a motor can be enabled. """ def __init__( self, world, body_a, body_b, anchor_a, anchor_b, collide_connected=False, lower_angle=None, upper_angle=None, enable_limit=None, motor_speed=None, max_motor_torque=None, enable_motor=None, reference_angle=None, ): """ Initialize a revolute joint with separate local anchor points for each body. Args: world: The physics world instance. body_a: The first body to connect. body_b: The second body to connect. anchor_a (tuple): The local (x, y) coordinates on body_a for the joint. anchor_b (tuple): The local (x, y) coordinates on body_b for the joint. collide_connected (bool, optional): If True, connected bodies will collide. lower_angle (float, optional): Lower joint limit in radians. upper_angle (float, optional): Upper joint limit in radians. enable_limit (bool, optional): Whether to enable joint limits. motor_speed (float, optional): Desired motor speed in radians/sec. max_motor_torque (float, optional): Maximum motor torque in newton-meters. enable_motor (bool, optional): Whether to enable the joint motor. reference_angle (float, optional): Reference angle between the two bodies. """ self._localAnchorA = Vec2(*anchor_a) self._localAnchorB = Vec2(*anchor_b) self._lower_angle = lower_angle self._upper_angle = upper_angle self._enable_limit = enable_limit self._motor_speed = motor_speed self._max_motor_torque = max_motor_torque self._enable_motor = enable_motor self._reference_angle = reference_angle # Get a default revolute joint definition from Box2D. defn = lib.b2DefaultRevoluteJointDef() defn.bodyIdA = body_a._body_id defn.bodyIdB = body_b._body_id defn.collideConnected = collide_connected # Use the provided local anchors for each body. defn.localAnchorA = self._localAnchorA.b2Vec2[0] defn.localAnchorB = self._localAnchorB.b2Vec2[0] if self._reference_angle is not None: defn.referenceAngle = self._reference_angle # Configure joint limits. if self._lower_angle is not None: defn.lowerAngle = self._lower_angle if self._upper_angle is not None: defn.upperAngle = self._upper_angle if self._enable_limit is not None: defn.enableLimit = self._enable_limit # Configure motor parameters. if self._motor_speed is not None: defn.motorSpeed = self._motor_speed if self._max_motor_torque is not None: defn.maxMotorTorque = self._max_motor_torque if self._enable_motor is not None: defn.enableMotor = self._enable_motor self._def = defn self.world = world self._joint_id = lib.b2CreateRevoluteJoint( self.world._world_id, ffi.addressof(self._def) ) self._set_userdata() @property def angle(self): """Current joint angle in radians relative to the reference angle.""" return lib.b2RevoluteJoint_GetAngle(self._joint_id) @property def motor_speed(self): """Desired motor speed in radians per second.""" return lib.b2RevoluteJoint_GetMotorSpeed(self._joint_id) @motor_speed.setter def motor_speed(self, value): lib.b2RevoluteJoint_SetMotorSpeed(self._joint_id, float(value)) @property def max_motor_torque(self): """Maximum motor torque in newton-meters.""" return lib.b2RevoluteJoint_GetMaxMotorTorque(self._joint_id) @max_motor_torque.setter def max_motor_torque(self, value): lib.b2RevoluteJoint_SetMaxMotorTorque(self._joint_id, float(value)) @property def lower_limit(self): """The lower joint limit in radians.""" return lib.b2RevoluteJoint_GetLowerLimit(self._joint_id) @property def upper_limit(self): """The upper joint limit in radians.""" return lib.b2RevoluteJoint_GetUpperLimit(self._joint_id) def set_limits(self, lower, upper): """ Set the joint limits in radians. Args: lower (float): Lower limit angle. upper (float): Upper limit angle. """ lib.b2RevoluteJoint_SetLimits(self._joint_id, float(lower), float(upper)) class PrismaticJoint(Joint): """ A prismatic joint constrains two bodies to translate along a shared axis while preventing relative rotation. Also known as a slider joint. The prismatic joint is useful for things like: - Pistons and linear actuators - Moving platforms and elevators - Sliding doors and drawers - Any motion constrained to a line Features include: - Linear limits to restrict range of motion - Motor to drive relative motion - Spring to create soft constraints """ def __init__( self, world, body_a, body_b, anchor_a, anchor_b, axis, collide_connected=False, lower_limit=None, upper_limit=None, enable_limit=None, motor_speed=None, max_motor_force=None, enable_motor=None, reference_angle=None, enable_spring=None, hertz=None, damping_ratio=None, ): """Initialize a prismatic joint between two bodies. Args: world: The physics world instance body_a: First body to connect body_b: Second body to connect anchor_a (tuple): Local anchor point on body A (x,y) anchor_b (tuple): Local anchor point on body B (x,y) axis (tuple): The axis defining allowed translation (x,y) in body A's frame collide_connected (bool): Whether bodies can collide lower_limit (float): Lower translation limit upper_limit (float): Upper translation limit enable_limit (bool): Whether to enable joint limits motor_speed (float): Desired motor speed in meters/sec max_motor_force (float): Maximum motor force in N enable_motor (bool): Whether to enable the joint motor reference_angle (float): Reference angle between bodies enable_spring (bool): Enable spring behavior hertz (float): Spring oscillation frequency in Hz damping_ratio (float): Spring damping ratio """ self._local_anchor_a = Vec2(*anchor_a) self._local_anchor_b = Vec2(*anchor_b) self._local_axis_a = Vec2(*axis) self._lower_limit = lower_limit self._upper_limit = upper_limit self._enable_limit = enable_limit self._motor_speed = motor_speed self._max_motor_force = max_motor_force self._enable_motor = enable_motor self._reference_angle = reference_angle self._enable_spring = enable_spring self._hertz = hertz self._damping_ratio = damping_ratio self.world = world defn = lib.b2DefaultPrismaticJointDef() defn.bodyIdA = body_a._body_id defn.bodyIdB = body_b._body_id defn.collideConnected = collide_connected defn.localAnchorA = self._local_anchor_a.b2Vec2[0] defn.localAnchorB = self._local_anchor_b.b2Vec2[0] defn.localAxisA = self._local_axis_a.b2Vec2[0] if self._reference_angle is not None: defn.referenceAngle = self._reference_angle if self._enable_limit is not None: defn.enableLimit = self._enable_limit if self._lower_limit is not None: defn.lowerTranslation = self._lower_limit if self._upper_limit is not None: defn.upperTranslation = self._upper_limit if self._enable_motor is not None: defn.enableMotor = self._enable_motor if self._motor_speed is not None: defn.motorSpeed = self._motor_speed if self._max_motor_force is not None: defn.maxMotorForce = self._max_motor_force if self._enable_spring is not None: defn.enableSpring = self._enable_spring if self._hertz is not None: defn.hertz = self._hertz if self._damping_ratio is not None: defn.dampingRatio = self._damping_ratio self._def = defn self._joint_id = lib.b2CreatePrismaticJoint( self.world._world_id, ffi.addressof(self._def) ) self._set_userdata() @property def joint_translation(self): """Get the current joint translation.""" return lib.b2PrismaticJoint_GetJointTranslation(self._joint_id) @property def joint_speed(self): """Get the current joint linear speed.""" return lib.b2PrismaticJoint_GetJointSpeed(self._joint_id) @property def limit_enabled(self): """Check if the joint limit is enabled.""" return lib.b2PrismaticJoint_IsLimitEnabled(self._joint_id) @limit_enabled.setter def limit_enabled(self, enable): """Enable/disable the joint limit. Args: enable (bool): True to enable limits, False to disable """ lib.b2PrismaticJoint_EnableLimit(self._joint_id, enable) @property def lower_limit(self): """Get the lower joint limit.""" return lib.b2PrismaticJoint_GetLowerLimit(self._joint_id) @lower_limit.setter def lower_limit(self, lower): """Set the lower joint limit.""" upper_limit = self.upper_limit lib.b2PrismaticJoint_SetLimits(self._joint_id, float(lower), float(upper_limit)) @property def upper_limit(self): """Get the upper joint limit.""" return lib.b2PrismaticJoint_GetUpperLimit(self._joint_id) @upper_limit.setter def upper_limit(self, upper): """Set the upper joint limit.""" lower_limit = self.lower_limit lib.b2PrismaticJoint_SetLimits(self._joint_id, float(lower_limit), float(upper)) def set_limits(self, lower, upper): """Set the joint limits. Args: lower (float): Lower translation limit upper (float): Upper translation limit """ lib.b2PrismaticJoint_SetLimits(self._joint_id, float(lower), float(upper)) @property def motor_enabled(self): """Check if the joint motor is enabled.""" return lib.b2PrismaticJoint_IsMotorEnabled(self._joint_id) @motor_enabled.setter def motor_enabled(self, enable): """Enable/disable the joint motor. Args: enable (bool): True to enable motor, False to disable """ lib.b2PrismaticJoint_EnableMotor(self._joint_id, enable) @property def motor_speed(self): """Get the motor speed in meters per second.""" return lib.b2PrismaticJoint_GetMotorSpeed(self._joint_id) @motor_speed.setter def motor_speed(self, speed): """Set the motor speed. Args: speed (float): Desired speed in meters per second """ lib.b2PrismaticJoint_SetMotorSpeed(self._joint_id, float(speed)) @property def max_motor_force(self): """Get maximum motor force in Newtons.""" return lib.b2PrismaticJoint_GetMaxMotorForce(self._joint_id) @max_motor_force.setter def max_motor_force(self, force): """Set the maximum motor force. Args: force (float): Maximum force in Newtons """ lib.b2PrismaticJoint_SetMaxMotorForce(self._joint_id, float(force)) @property def motor_force(self): """Get the current motor force in Newtons.""" return lib.b2PrismaticJoint_GetMotorForce(self._joint_id) @property def spring_enabled(self): """Check if spring behavior is enabled.""" return lib.b2PrismaticJoint_IsSpringEnabled(self._joint_id) @spring_enabled.setter def spring_enabled(self, enable): """Enable/disable spring behavior. Args: enable (bool): True to enable spring, False to disable """ lib.b2PrismaticJoint_EnableSpring(self._joint_id, enable) @property def spring_hertz(self): """Get spring frequency in Hertz.""" return lib.b2PrismaticJoint_GetSpringHertz(self._joint_id) @spring_hertz.setter def spring_hertz(self, hertz): """Set spring oscillation frequency. Args: hertz (float): Frequency in Hertz (cycles/sec) """ lib.b2PrismaticJoint_SetSpringHertz(self._joint_id, float(hertz)) @property def spring_damping_ratio(self): """Get spring damping ratio.""" return lib.b2PrismaticJoint_GetSpringDampingRatio(self._joint_id) @spring_damping_ratio.setter def spring_damping_ratio(self, damping): """Set spring damping ratio. Args: damping (float): Damping ratio. """ lib.b2PrismaticJoint_SetSpringDampingRatio(self._joint_id, float(damping)) class WheelJoint(Joint): """A wheel joint constrains a point on one body to translate along an axis fixed on another body. This is similar to a prismatic joint but with rotation. A rotational motor can be added to drive the relative rotation. This joint is designed for vehicle suspensions. """ def __init__( self, world, body_a, body_b, anchor_a, anchor_b, axis, collide_connected=False, enable_limit=False, lower_translation=0.0, upper_translation=0.0, enable_motor=False, motor_speed=0.0, max_motor_torque=0.0, enable_spring=False, spring_hertz=0.0, spring_damping_ratio=0.0, ): """Initialize a wheel joint. Args: world: The physics world instance body_a: First body to connect body_b: Second body to connect anchor_a (tuple): Local anchor point on body A (x,y) anchor_b (tuple): Local anchor point on body B (x,y) axis (tuple): The axis defining translation in body A's frame (x,y) collide_connected (bool): Whether bodies can collide enable_limit (bool): Enable joint translation limits lower_translation (float): Lower translation limit upper_translation (float): Upper translation limit enable_motor (bool): Enable the joint motor motor_speed (float): Motor speed in radians/second max_motor_torque (float): Maximum motor torque in N-m enable_spring (bool): Enable spring behavior spring_hertz (float): Spring frequency in Hz spring_damping_ratio (float): Spring damping ratio """ self._local_anchor_a = Vec2(*anchor_a) self._local_anchor_b = Vec2(*anchor_b) self._local_axis_a = Vec2(*axis) self._enable_limit = enable_limit self._lower_translation = lower_translation self._upper_translation = upper_translation self._enable_motor = enable_motor self._motor_speed = motor_speed self._max_motor_torque = max_motor_torque self._enable_spring = enable_spring self._spring_hertz = spring_hertz self._spring_damping_ratio = spring_damping_ratio self.world = world defn = lib.b2DefaultWheelJointDef() defn.bodyIdA = body_a._body_id defn.bodyIdB = body_b._body_id defn.collideConnected = collide_connected defn.localAnchorA = self._local_anchor_a.b2Vec2[0] defn.localAnchorB = self._local_anchor_b.b2Vec2[0] defn.localAxisA = self._local_axis_a.b2Vec2[0] defn.enableLimit = self._enable_limit defn.lowerTranslation = self._lower_translation defn.upperTranslation = self._upper_translation defn.enableMotor = self._enable_motor defn.motorSpeed = self._motor_speed defn.maxMotorTorque = self._max_motor_torque defn.enableSpring = self._enable_spring defn.hertz = self._spring_hertz defn.dampingRatio = self._spring_damping_ratio self._def = defn self._joint_id = lib.b2CreateWheelJoint( self.world._world_id, ffi.addressof(self._def) ) self._set_userdata() @property def spring_enabled(self): """Check if spring behavior is enabled.""" return lib.b2WheelJoint_IsSpringEnabled(self._joint_id) @spring_enabled.setter def spring_enabled(self, enable): """Enable/disable spring behavior. Args: enable (bool): True to enable spring, False to disable """ lib.b2WheelJoint_EnableSpring(self._joint_id, enable) @property def spring_hertz(self): """Get spring frequency in Hertz.""" return lib.b2WheelJoint_GetSpringHertz(self._joint_id) @spring_hertz.setter def spring_hertz(self, hertz): """Set spring frequency. Args: hertz (float): Frequency in Hz """ lib.b2WheelJoint_SetSpringHertz(self._joint_id, float(hertz)) @property def spring_damping_ratio(self): """Get spring damping ratio (non-dimensional).""" return lib.b2WheelJoint_GetSpringDampingRatio(self._joint_id) @spring_damping_ratio.setter def spring_damping_ratio(self, damping): """Set spring damping ratio. Args: damping (float): Damping ratio (non-dimensional) """ lib.b2WheelJoint_SetSpringDampingRatio(self._joint_id, float(damping)) @property def limit_enabled(self): """Check if translation limits are enabled.""" return lib.b2WheelJoint_IsLimitEnabled(self._joint_id) @limit_enabled.setter def limit_enabled(self, enable): """Enable/disable translation limits. Args: enable (bool): True to enable limits, False to disable """ lib.b2WheelJoint_EnableLimit(self._joint_id, enable) @property def lower_limit(self): """Get lower translation limit.""" return lib.b2WheelJoint_GetLowerLimit(self._joint_id) @property def upper_limit(self): """Get upper translation limit.""" return lib.b2WheelJoint_GetUpperLimit(self._joint_id) def set_limits(self, lower, upper): """Set the translation limits. Args: lower (float): Lower translation limit upper (float): Upper translation limit """ lib.b2WheelJoint_SetLimits(self._joint_id, float(lower), float(upper)) @property def motor_enabled(self): """Check if joint motor is enabled.""" return lib.b2WheelJoint_IsMotorEnabled(self._joint_id) @motor_enabled.setter def motor_enabled(self, enable): """Enable/disable the joint motor. Args: enable (bool): True to enable motor, False to disable """ lib.b2WheelJoint_EnableMotor(self._joint_id, enable) @property def motor_speed(self): """Get motor speed in radians per second.""" return lib.b2WheelJoint_GetMotorSpeed(self._joint_id) @motor_speed.setter def motor_speed(self, speed): """Set motor speed. Args: speed (float): Speed in radians per second """ lib.b2WheelJoint_SetMotorSpeed(self._joint_id, float(speed)) @property def max_motor_torque(self): """Get maximum motor torque in N-m.""" return lib.b2WheelJoint_GetMaxMotorTorque(self._joint_id) @max_motor_torque.setter def max_motor_torque(self, torque): """Set maximum motor torque. Args: torque (float): Maximum torque in N-m """ lib.b2WheelJoint_SetMaxMotorTorque(self._joint_id, float(torque)) @property def motor_torque(self): """Get current motor torque in N-m.""" return lib.b2WheelJoint_GetMotorTorque(self._joint_id) class DistanceJoint(Joint): """A distance joint constrains two points on two bodies to maintain a constant distance. A distance joint connects two points on two bodies with a massless rod or a spring. The distance can be static (like a rod) or behave like a spring that can stretch. When spring is enabled, it can optionally have a motor to actively change its length. Features: - Optional spring behavior with configurable stiffness and damping - Optional length limits to restrict stretching - Optional motor to actively change the distance """ def __init__( self, world, body_a, body_b, anchor_a, anchor_b, collide_connected=False, length=None, min_length=None, max_length=None, enable_limit=False, enable_spring=False, hertz=None, damping_ratio=None, enable_motor=False, motor_speed=None, max_motor_force=None, ): """Initialize a distance joint between two bodies. Args: world: The physics world instance body_a: First body to connect body_b: Second body to connect anchor_a (tuple): Local anchor point on body A (x,y) anchor_b (tuple): Local anchor point on body B (x,y) collide_connected (bool): Whether bodies can collide length (float): Rest length. Calculated from anchors if None. min_length (float): Minimum allowed length when using limits max_length (float): Maximum allowed length when using limits enable_limit (bool): Whether to enable length limits enable_spring (bool): Enable spring behavior hertz (float): Spring oscillation frequency in Hz when enabled damping_ratio (float): Spring damping ratio [0,1] when enabled enable_motor (bool): Enable the joint motor motor_speed (float): Desired motor speed in meters/second max_motor_force (float): Maximum motor force in Newtons """ self._local_anchor_a = Vec2(*anchor_a) self._local_anchor_b = Vec2(*anchor_b) self._length = length self._min_length = min_length self._max_length = max_length self._enable_limit = enable_limit self._enable_spring = enable_spring self._hertz = hertz self._damping_ratio = damping_ratio self._enable_motor = enable_motor self._motor_speed = motor_speed self._max_motor_force = max_motor_force self.world = world defn = lib.b2DefaultDistanceJointDef() defn.bodyIdA = body_a._body_id defn.bodyIdB = body_b._body_id defn.collideConnected = collide_connected defn.localAnchorA = self._local_anchor_a.b2Vec2[0] defn.localAnchorB = self._local_anchor_b.b2Vec2[0] if self._length is not None: defn.length = self._length if self._min_length is not None: defn.minLength = self._min_length if self._max_length is not None: defn.maxLength = self._max_length defn.enableLimit = self._enable_limit defn.enableSpring = self._enable_spring if self._hertz is not None: defn.hertz = self._hertz if self._damping_ratio is not None: defn.dampingRatio = self._damping_ratio defn.enableMotor = self._enable_motor if self._motor_speed is not None: defn.motorSpeed = self._motor_speed if self._max_motor_force is not None: defn.maxMotorForce = self._max_motor_force self._def = defn self._joint_id = lib.b2CreateDistanceJoint( self.world._world_id, ffi.addressof(self._def) ) self._set_userdata() @property def length(self): """Get the rest length of the joint.""" return lib.b2DistanceJoint_GetLength(self._joint_id) @length.setter def length(self, value): """Set the rest length of the joint. Args: value (float): New rest length """ lib.b2DistanceJoint_SetLength(self._joint_id, float(value)) @property def current_length(self): """Get the current distance between anchor points.""" return lib.b2DistanceJoint_GetCurrentLength(self._joint_id) @property def spring_enabled(self): """Check if spring behavior is enabled.""" return lib.b2DistanceJoint_IsSpringEnabled(self._joint_id) @spring_enabled.setter def spring_enabled(self, enable): """Enable/disable spring behavior. Args: enable (bool): True to enable spring, False for rigid behavior """ lib.b2DistanceJoint_EnableSpring(self._joint_id, enable) @property def spring_hertz(self): """Get spring frequency in Hertz.""" return lib.b2DistanceJoint_GetSpringHertz(self._joint_id) @spring_hertz.setter def spring_hertz(self, hertz): """Set spring frequency in Hertz. Args: hertz (float): Oscillation frequency in Hz """ lib.b2DistanceJoint_SetSpringHertz(self._joint_id, float(hertz)) @property def spring_damping_ratio(self): """Get spring damping ratio.""" return lib.b2DistanceJoint_GetSpringDampingRatio(self._joint_id) @spring_damping_ratio.setter def spring_damping_ratio(self, damping): """Set spring damping ratio. Args: damping (float): Damping ratio [0,1] """ lib.b2DistanceJoint_SetSpringDampingRatio(self._joint_id, float(damping)) @property def limit_enabled(self): """Check if length limits are enabled.""" return lib.b2DistanceJoint_IsLimitEnabled(self._joint_id) @limit_enabled.setter def limit_enabled(self, enable): """Enable/disable length limits. Args: enable (bool): True to enable limits, False to disable """ lib.b2DistanceJoint_EnableLimit(self._joint_id, enable) @property def min_length(self): """Get the minimum allowed length.""" return lib.b2DistanceJoint_GetMinLength(self._joint_id) @min_length.setter def min_length(self, value): """Set the minimum allowed length. Args: value (float): New minimum length """ self.set_length_range(float(value), self.max_length) @property def max_length(self): """Get the maximum allowed length.""" return lib.b2DistanceJoint_GetMaxLength(self._joint_id) @max_length.setter def max_length(self, value): """Set the maximum allowed length. Args: value (float): New maximum length """ self.set_length_range(self.min_length, float(value)) def set_length_range(self, min_length, max_length): """Set the allowed length range. Args: min_length (float): Minimum allowed length max_length (float): Maximum allowed length """ lib.b2DistanceJoint_SetLengthRange( self._joint_id, float(min_length), float(max_length) ) @property def motor_enabled(self): """Check if the joint motor is enabled.""" return lib.b2DistanceJoint_IsMotorEnabled(self._joint_id) @motor_enabled.setter def motor_enabled(self, enable): """Enable/disable the joint motor. Args: enable (bool): True to enable motor, False to disable """ lib.b2DistanceJoint_EnableMotor(self._joint_id, enable) @property def motor_speed(self): """Get motor speed in meters per second.""" return lib.b2DistanceJoint_GetMotorSpeed(self._joint_id) @motor_speed.setter def motor_speed(self, speed): """Set motor speed. Args: speed (float): Desired speed in meters per second """ lib.b2DistanceJoint_SetMotorSpeed(self._joint_id, float(speed)) @property def max_motor_force(self): """Get maximum motor force in Newtons.""" return lib.b2DistanceJoint_GetMaxMotorForce(self._joint_id) @max_motor_force.setter def max_motor_force(self, force): """Set maximum motor force. Args: force (float): Maximum force in Newtons """ lib.b2DistanceJoint_SetMaxMotorForce(self._joint_id, float(force)) @property def motor_force(self): """Get current motor force in Newtons.""" return lib.b2DistanceJoint_GetMotorForce(self._joint_id) class MotorJoint(Joint): """A motor joint is used to control the relative motion between two bodies. The motor joint is used to drive the relative transform between two bodies. It takes a relative position and rotation and applies the forces and torques needed to achieve that relative transform over time. A typical usage is to control the movement of a dynamic body with respect to the ground. Features: - Control relative linear position between bodies - Control relative angular position between bodies - Maximum force and torque limits - Position correction factor for stability """ def __init__( self, world, body_a, body_b, linear_offset=None, angular_offset=None, max_force=None, max_torque=None, correction_factor=None, collide_connected=False, ): """Initialize a motor joint. Args: world: The physics world instance body_a: First body to connect body_b: Second body to connect linear_offset (tuple): Position of bodyB minus bodyA, in bodyA's frame angular_offset (float): Angle of bodyB minus bodyA in radians max_force (float): Maximum force in Newtons max_torque (float): Maximum torque in Newton-meters correction_factor (float): Position correction factor in range [0,1] collide_connected (bool): Whether connected bodies can collide """ self._linear_offset = linear_offset self._angular_offset = angular_offset self._max_force = max_force self._max_torque = max_torque self._correction_factor = correction_factor self.world = world defn = lib.b2DefaultMotorJointDef() defn.bodyIdA = body_a._body_id defn.bodyIdB = body_b._body_id defn.collideConnected = collide_connected if linear_offset is not None: defn.linearOffset = Vec2(*self._linear_offset).b2Vec2[0] if angular_offset is not None: defn.angularOffset = self._angular_offset if max_force is not None: defn.maxForce = self._max_force if max_torque is not None: defn.maxTorque = self._max_torque if correction_factor is not None: defn.correctionFactor = self._correction_factor self._def = defn self._joint_id = lib.b2CreateMotorJoint( self.world._world_id, ffi.addressof(self._def) ) self._set_userdata() @property def linear_offset(self): """Get the target linear offset in bodyA's frame.""" return Vec2.from_b2Vec2(lib.b2MotorJoint_GetLinearOffset(self._joint_id)) @linear_offset.setter def linear_offset(self, offset): """Set the target linear offset in bodyA's frame. Args: offset (tuple/Vec2): Target position of bodyB in bodyA frame """ vec = Vec2(*offset) lib.b2MotorJoint_SetLinearOffset(self._joint_id, vec.b2Vec2[0]) @property def angular_offset(self): """Get the target angular offset in radians.""" return lib.b2MotorJoint_GetAngularOffset(self._joint_id) @angular_offset.setter def angular_offset(self, angle): """Set the target angular offset. Args: angle (float): Target angle in radians """ lib.b2MotorJoint_SetAngularOffset(self._joint_id, float(angle)) @property def max_force(self): """Get maximum force in Newtons.""" return lib.b2MotorJoint_GetMaxForce(self._joint_id) @max_force.setter def max_force(self, force): """Set the maximum force in Newtons. Args: force (float): Maximum force value """ lib.b2MotorJoint_SetMaxForce(self._joint_id, float(force)) @property def max_torque(self): """Get maximum torque in Newton-meters.""" return lib.b2MotorJoint_GetMaxTorque(self._joint_id) @max_torque.setter def max_torque(self, torque): """Set the maximum torque in Newton-meters. Args: torque (float): Maximum torque value """ lib.b2MotorJoint_SetMaxTorque(self._joint_id, float(torque)) @property def correction_factor(self): """Get position correction factor [0,1].""" return lib.b2MotorJoint_GetCorrectionFactor(self._joint_id) @correction_factor.setter def correction_factor(self, factor): """Set the position correction factor. Args: factor (float): Correction factor in range [0,1] """ lib.b2MotorJoint_SetCorrectionFactor(self._joint_id, float(factor))