2D Joints

Play Demo

Demonstrates:

  • RigidBody2D physics bodies with automatic gravity

  • StaticBody2D as a fixed anchor point

  • PinJoint2D connecting bodies with Baumgarte-stabilized distance constraints

  • PhysicsServer handles all constraint solving automatically

  • Click-to-apply-force interaction on the bottom ball

Controls: LMB - Apply impulse to the bottom ball R - Reset the chain ESC - Quit

Run: uv run python packages/graphics/examples/2d_joints.py

Source Code

  1"""2D Joints -- Pendulum chain with PinJoint2D constraints.
  2
  3Demonstrates:
  4  - RigidBody2D physics bodies with automatic gravity
  5  - StaticBody2D as a fixed anchor point
  6  - PinJoint2D connecting bodies with Baumgarte-stabilized distance constraints
  7  - PhysicsServer handles all constraint solving automatically
  8  - Click-to-apply-force interaction on the bottom ball
  9
 10Controls:
 11  LMB   - Apply impulse to the bottom ball
 12  R     - Reset the chain
 13  ESC   - Quit
 14
 15Run: uv run python packages/graphics/examples/2d_joints.py
 16"""
 17
 18
 19from simvx.core import Input, InputMap, Key, MouseButton, Node2D, PinJoint2D, RigidBody2D, StaticBody2D, Vec2
 20from simvx.graphics import App
 21
 22WIDTH, HEIGHT = 800, 600
 23CHAIN_LENGTH = 6
 24LINK_SPACING = 50.0
 25BALL_RADIUS = 10.0
 26ANCHOR = Vec2(WIDTH / 2, 100)
 27
 28
 29class PendulumChain(Node2D):
 30    """A chain of RigidBody2D nodes connected by PinJoint2D constraints."""
 31
 32    def ready(self):
 33        InputMap.add_action("apply_force", [MouseButton.LEFT])
 34        InputMap.add_action("reset_chain", [Key.R])
 35        InputMap.add_action("quit", [Key.ESCAPE])
 36
 37        # Fixed anchor (static body)
 38        self._anchor = self.add_child(StaticBody2D(name="Anchor", position=Vec2(ANCHOR.x, ANCHOR.y)))
 39
 40        # Chain of dynamic bodies
 41        self._balls: list[RigidBody2D] = []
 42        for i in range(CHAIN_LENGTH):
 43            body = self.add_child(RigidBody2D(
 44                name=f"Ball{i}",
 45                position=Vec2(ANCHOR.x + (i + 1) * 5, ANCHOR.y + LINK_SPACING * (i + 1)),
 46                mass=1.0, linear_damp=0.3,
 47            ))
 48            self._balls.append(body)
 49
 50        # Connect anchor to first ball, then chain the rest
 51        prev = self._anchor
 52        for body in self._balls:
 53            self.add_child(PinJoint2D(body_a=prev, body_b=body, distance=LINK_SPACING, stiffness=1.0, damping=0.5))
 54            prev = body
 55
 56    def _reset(self):
 57        self._anchor.position = Vec2(ANCHOR.x, ANCHOR.y)
 58        for i, body in enumerate(self._balls):
 59            body.position = Vec2(ANCHOR.x + (i + 1) * 3, ANCHOR.y + LINK_SPACING * (i + 1))
 60            body.linear_velocity = Vec2()
 61
 62    def process(self, dt: float):
 63        if Input.is_action_just_pressed("quit"):
 64            self.app.quit()
 65            return
 66        if Input.is_action_just_pressed("reset_chain"):
 67            self._reset()
 68            return
 69        if Input.is_action_just_pressed("apply_force"):
 70            # Kick bottom ball away from mouse position
 71            p = self._balls[-1].position
 72            mouse = Input.mouse_position
 73            dx, dy = p.x - mouse.x, p.y - mouse.y
 74            dist = max((dx * dx + dy * dy) ** 0.5, 1.0)
 75            strength = 400.0
 76            self._balls[-1].apply_impulse(Vec2(dx / dist * strength, dy / dist * strength))
 77
 78    def draw(self, renderer):
 79        # Lines between links
 80        link_colour = (0.6, 0.6, 0.7, 1.0)
 81        anchor_pos = self._anchor.position
 82        if self._balls:
 83            renderer.draw_line(anchor_pos, self._balls[0].position, colour=link_colour)
 84        for i in range(len(self._balls) - 1):
 85            renderer.draw_line(self._balls[i].position, self._balls[i + 1].position, colour=link_colour)
 86
 87        # Anchor
 88        renderer.draw_circle(anchor_pos, 8, colour=(1.0, 0.3, 0.3, 1.0), filled=True)
 89
 90        # Balls (gradient)
 91        for i, body in enumerate(self._balls):
 92            t = i / max(1, CHAIN_LENGTH - 1)
 93            colour = (0.3 + 0.5 * (1 - t), 0.6 + 0.3 * (1 - t), 1.0, 1.0)
 94            renderer.draw_circle(body.position, BALL_RADIUS, colour=colour, filled=True)
 95
 96        # HUD
 97        renderer.draw_text("Pendulum Chain -- PinJoint2D Demo", (10, 10), colour=(1.0, 1.0, 1.0), scale=2)
 98        renderer.draw_text("LMB: apply force | R: reset | ESC: quit", (10, 35), colour=(0.71, 0.71, 0.71))
 99
100
101if __name__ == "__main__":
102    App(title="2D Joints -- Pendulum Chain", width=WIDTH, height=HEIGHT).run(PendulumChain())