2D Joints¶
a pendulum chain built from PinJoint2D.
▶ Run in browserTags: 2d physics joints constraints
A joint constrains two physics bodies relative to each other, and the physics world keeps it
satisfied on every fixed step: you declare the constraint and never correct a position by hand.
PinJoint2D(body_a=..., body_b=..., anchor=...) pins two bodies together at a world-space point.
They cannot separate there but rotate freely about it, so a pin to a STATIC body makes a
pendulum and a line of pins makes a rope.
Here a line of DYNAMIC balls hangs from a STATIC anchor, each pinned to the one above it at
that body’s centre. The bodies and joints sit under a PhysicsRoot2D, whose gravity points
down the screen (+Y). The anchor is captured into each body’s own frame when the joint enters
the tree, so the pivot travels and turns with the bodies it joins. The balls’ collision_mask
is 0, so they never collide as circles and only the pins hold them together.
Click or tap to kick the bottom ball away from the pointer and set the chain swinging.
An iterative solver lets a loaded chain stretch a little. world_2d.position_iterations is how
many position passes each step spends on the joints: press I to cycle it through 3, 8 and 32 and
watch the “worst link” figure, which is how far the most stretched link is from its rest length.
More passes pull the chain tighter for a step that costs more. The pymunk backend has no
position pass, ignores the setting and hangs the chain close to its rest length anyway. Press R
to hang the chain back up (both keys are buttons on screen too); a reset clears each ball’s
rotation and spin too, because a pin’s pivot is held in its body’s frame.
Source¶
1# /// simvx
2# tags = ["2d", "physics", "joints", "constraints"]
3# web = { width = 800, height = 600 }
4# ///
5"""2D Joints: a pendulum chain built from PinJoint2D.
6
7A joint constrains two physics bodies relative to each other, and the physics world keeps it
8satisfied on every fixed step: you declare the constraint and never correct a position by hand.
9`PinJoint2D(body_a=..., body_b=..., anchor=...)` pins two bodies together at a world-space point.
10They cannot separate there but rotate freely about it, so a pin to a `STATIC` body makes a
11pendulum and a line of pins makes a rope.
12
13Here a line of `DYNAMIC` balls hangs from a `STATIC` anchor, each pinned to the one above it at
14that body's centre. The bodies and joints sit under a `PhysicsRoot2D`, whose `gravity` points
15down the screen (+Y). The anchor is captured into each body's own frame when the joint enters
16the tree, so the pivot travels and turns with the bodies it joins. The balls' `collision_mask`
17is 0, so they never collide as circles and only the pins hold them together.
18
19Click or tap to kick the bottom ball away from the pointer and set the chain swinging.
20
21An iterative solver lets a loaded chain stretch a little. `world_2d.position_iterations` is how
22many position passes each step spends on the joints: press I to cycle it through 3, 8 and 32 and
23watch the "worst link" figure, which is how far the most stretched link is from its rest length.
24More passes pull the chain tighter for a step that costs more. The pymunk backend has no
25position pass, ignores the setting and hangs the chain close to its rest length anyway. Press R
26to hang the chain back up (both keys are buttons on screen too); a reset clears each ball's
27rotation and spin too, because a pin's pivot is held in its body's frame.
28"""
29
30from itertools import pairwise
31
32from simvx.core import (
33 Anchor,
34 BodyMode,
35 Button,
36 CanvasLayer,
37 CircleShape2D,
38 HBoxContainer,
39 Input,
40 Key,
41 Label,
42 Node2D,
43 PhysicsBody2D,
44 PhysicsRoot2D,
45 PinJoint2D,
46 Pointer,
47 Vec2,
48 on_input,
49)
50from simvx.graphics import App
51
52LINKS = 6
53SPACING = 50.0
54RADIUS = 10.0
55KICK_SPEED = 600.0
56POSITION_PASSES = (3, 8, 32)
57
58
59class PendulumChain(Node2D):
60 dynamic = True
61 input_context = {"kick": [Pointer.PRIMARY], "reset": [Key.R], "passes": [Key.I]}
62
63 def on_ready(self):
64 self.world = self.add_child(PhysicsRoot2D(name="World", gravity=(0.0, 1200.0)))
65 top = Vec2(self.viewport_size.x / 2, 100)
66 self.anchor = self.world.add_child(
67 PhysicsBody2D(name="Anchor", mode=BodyMode.STATIC, position=top, shape=CircleShape2D(8.0))
68 )
69 self.balls = []
70 above = self.anchor
71 for i in range(LINKS):
72 ball = self.world.add_child(
73 PhysicsBody2D(
74 name=f"Ball{i}",
75 mode=BodyMode.DYNAMIC,
76 position=top + Vec2(0, SPACING * (i + 1)),
77 collision_mask=0,
78 shape=CircleShape2D(RADIUS),
79 )
80 )
81 self.world.add_child(PinJoint2D(body_a=above, body_b=ball, anchor=above.position))
82 self.balls.append(ball)
83 above = ball
84 self.starts = [ball.position.copy() for ball in self.balls]
85 self.status = self.add_child(CanvasLayer()).add_child(Label(anchor=Anchor.TOP_LEFT, inset=12))
86 self._add_help()
87
88 @on_input("kick")
89 def kick(self, event):
90 tip = self.balls[-1]
91 tip.velocity = (tip.world_position - Input.mouse_position).normalized() * KICK_SPEED
92
93 @on_input("reset")
94 def reset(self, event=None):
95 for ball, start in zip(self.balls, self.starts, strict=True):
96 ball.position = start
97 ball.velocity = Vec2()
98 ball.rotation = 0.0
99 ball.spin = 0.0
100
101 @on_input("passes")
102 def cycle_passes(self, event=None):
103 world = self.world.world_2d
104 world.position_iterations = POSITION_PASSES[
105 (POSITION_PASSES.index(world.position_iterations) + 1) % len(POSITION_PASSES)
106 ]
107
108 def points(self):
109 return [self.anchor.world_position] + [ball.world_position for ball in self.balls]
110
111 def on_update(self, dt):
112 points = self.points()
113 worst = max(abs((b - a).length() - SPACING) for a, b in pairwise(points))
114 self.status.text = (
115 f"Click or tap to kick the chain\n"
116 f"position_iterations {self.world.world_2d.position_iterations} "
117 f"worst link {worst / SPACING:.1%} long"
118 )
119
120 def on_draw(self, renderer):
121 points = self.points()
122 for a, b in pairwise(points):
123 renderer.line(a, b, stroke=(0.6, 0.6, 0.7, 1.0))
124 renderer.circle(points[0], 8, fill=(1.0, 0.3, 0.3, 1.0))
125 for i, point in enumerate(points[1:]):
126 fade = 1 - i / (LINKS - 1)
127 renderer.circle(point, RADIUS, fill=(0.3 + 0.5 * fade, 0.6 + 0.3 * fade, 1.0, 1.0))
128
129 # Example help overlay: not part of the technique shown.
130 def _add_help(self):
131 buttons = [Button("R: reset", on_press=self.reset), Button("I: position passes", on_press=self.cycle_passes)]
132 self.status.parent.add_child(HBoxContainer(children=buttons, anchor=Anchor.BOTTOM_LEFT, inset=12))
133
134
135if __name__ == "__main__":
136 App(title="2D Joints", width=800, height=600).run(PendulumChain())