2D Joints¶
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())