2D Joints¶
a swinging pendulum chain built from PinJoint2D constraints.
▶ Run in browserTags: 2d
A line of dynamic balls hangs from a fixed anchor at the top of the screen. Each ball is pinned to the one above it, so the whole chain swings and settles under gravity like a real rope of beads. You are watching the physics solver hold those pin constraints together every fixed step.
What it demonstrates¶
PhysicsBody2D(DYNAMIC): a dynamic body that gravity and forces act on.
PhysicsBody2D(STATIC): an immovable body used as the fixed anchor at the top.
PinJoint2D: pins two bodies at a single world point; they cannot separate there but rotate freely about it (chain them to build a rope).
The Physics2DWorld solves every joint automatically: you only declare the constraints, you never integrate or correct positions by hand.
Click-to-interact: a left click kicks the bottom ball away from the cursor so you can set the whole chain swinging.
Controls¶
LMB Kick the bottom ball away from the mouse R Reset the chain to its hanging start position ESC Quit
Run: uv run python examples/features/2d/joints.py
Source¶
1"""2D Joints: a swinging pendulum chain built from PinJoint2D constraints.
2
3A line of dynamic balls hangs from a fixed anchor at the top of the screen.
4Each ball is pinned to the one above it, so the whole chain swings and settles
5under gravity like a real rope of beads. You are watching the physics solver
6hold those pin constraints together every fixed step.
7
8## What it demonstrates
9 - PhysicsBody2D(DYNAMIC): a dynamic body that gravity and forces act on.
10 - PhysicsBody2D(STATIC): an immovable body used as the fixed anchor at the top.
11 - PinJoint2D: pins two bodies at a single world point; they cannot separate
12 there but rotate freely about it (chain them to build a rope).
13 - The Physics2DWorld solves every joint automatically: you only declare the
14 constraints, you never integrate or correct positions by hand.
15 - Click-to-interact: a left click kicks the bottom ball away from the cursor
16 so you can set the whole chain swinging.
17
18## Controls
19 LMB Kick the bottom ball away from the mouse
20 R Reset the chain to its hanging start position
21 ESC Quit
22
23Run: uv run python examples/features/2d/joints.py
24"""
25
26from simvx.core import (
27 BodyMode,
28 CircleShape2D,
29 CollisionShape2D,
30 Input,
31 InputMap,
32 Key,
33 MouseButton,
34 Node2D,
35 PhysicsBody2D,
36 PhysicsRoot2D,
37 PinJoint2D,
38 Vec2,
39)
40from simvx.graphics import App
41
42WIDTH, HEIGHT = 800, 600
43CHAIN_LENGTH = 6
44LINK_SPACING = 50.0
45BALL_RADIUS = 10.0
46ANCHOR = Vec2(WIDTH / 2, 100)
47# Y-down world (gravity = +Y) so the chain hangs downward on screen.
48GRAVITY = Vec2(0.0, 1200.0)
49
50
51class PendulumChain(Node2D):
52 """A chain of PhysicsBody2D nodes connected by PinJoint2D constraints."""
53
54 dynamic = True # the chain swings every frame (physics body positions)
55
56 def on_ready(self):
57 InputMap.add_action("apply_force", [MouseButton.LEFT])
58 InputMap.add_action("reset_chain", [Key.R])
59 InputMap.add_action("quit", [Key.ESCAPE])
60
61 self._root = self.add_child(PhysicsRoot2D(name="World", gravity=GRAVITY))
62
63 # Fixed anchor (static body). Disjoint collision masks across the chain
64 # so the beads never collide as circles: only the pins act.
65 self._anchor = PhysicsBody2D(
66 name="Anchor", mode=BodyMode.STATIC, position=Vec2(ANCHOR.x, ANCHOR.y),
67 collision_layer=0x1, collision_mask=0x0,
68 )
69 self._anchor.add_child(CollisionShape2D(shape=CircleShape2D(8.0)))
70 self._root.add_child(self._anchor)
71
72 # Chain of dynamic bodies, pinned to the one above at the upper pivot.
73 self._balls: list[PhysicsBody2D] = []
74 self._start_positions: list[Vec2] = []
75 prev = self._anchor
76 prev_pos = Vec2(ANCHOR.x, ANCHOR.y)
77 for i in range(CHAIN_LENGTH):
78 pos = Vec2(ANCHOR.x, ANCHOR.y + LINK_SPACING * (i + 1))
79 body = PhysicsBody2D(
80 name=f"Ball{i}", mode=BodyMode.DYNAMIC, mass=1.0, position=pos,
81 collision_layer=0x1, collision_mask=0x0,
82 )
83 body.add_child(CollisionShape2D(shape=CircleShape2D(BALL_RADIUS)))
84 self._root.add_child(body)
85 self._balls.append(body)
86 self._start_positions.append(pos)
87 # Pin this body to the previous one at the previous body's pivot.
88 self._root.add_child(PinJoint2D(body_a=prev, body_b=body, anchor=prev_pos))
89 prev = body
90 prev_pos = pos
91
92 def _reset(self):
93 for body, pos in zip(self._balls, self._start_positions, strict=True):
94 body.position = Vec2(pos.x, pos.y)
95 body.velocity = Vec2()
96
97 def on_update(self, dt: float):
98 if Input.is_action_just_pressed("quit"):
99 self.app.quit()
100 return
101 if Input.is_action_just_pressed("reset_chain"):
102 self._reset()
103 return
104 if Input.is_action_just_pressed("apply_force"):
105 # Kick the bottom ball away from the mouse (set its velocity directly).
106 p = self._balls[-1].world_position
107 mouse = Input.mouse_position
108 dx, dy = p.x - mouse.x, p.y - mouse.y
109 dist = max((dx * dx + dy * dy) ** 0.5, 1.0)
110 speed = 600.0
111 self._balls[-1].velocity = Vec2(dx / dist * speed, dy / dist * speed)
112
113 def on_draw(self, renderer):
114 # Lines between links.
115 link_colour = (0.6, 0.6, 0.7, 1.0)
116 anchor_pos = self._anchor.world_position
117 if self._balls:
118 renderer.draw_line(anchor_pos, self._balls[0].world_position, colour=link_colour)
119 for i in range(len(self._balls) - 1):
120 renderer.draw_line(self._balls[i].world_position, self._balls[i + 1].world_position, colour=link_colour)
121
122 # Anchor.
123 renderer.draw_circle(anchor_pos, 8, colour=(1.0, 0.3, 0.3, 1.0), filled=True)
124
125 # Balls (gradient).
126 for i, body in enumerate(self._balls):
127 t = i / max(1, CHAIN_LENGTH - 1)
128 colour = (0.3 + 0.5 * (1 - t), 0.6 + 0.3 * (1 - t), 1.0, 1.0)
129 renderer.draw_circle(body.world_position, BALL_RADIUS, colour=colour, filled=True)
130
131 # HUD.
132 renderer.draw_text("Pendulum Chain -- PinJoint2D Demo", (10, 10), colour=(1.0, 1.0, 1.0), scale=2)
133 renderer.draw_text("LMB: kick | R: reset | ESC: quit", (10, 50), colour=(0.71, 0.71, 0.71))
134
135
136if __name__ == "__main__":
137 App(title="2D Joints -- Pendulum Chain", width=WIDTH, height=HEIGHT).run(PendulumChain())