More pymunk explorations #763
Replies: 3 comments 16 replies
|
@hx2A I have tried moving the classes to another file for importing and something weird is going on... now I'm feeling very stupid. I'll see if I can prepare a minimal example to reproduce the problem. EDIT: Removed outdated code ... see comments bellow for a more recent version. |
|
So, I'm still working on the importable Today I struggled to add two constraints/joints, From https://github.com/villares/sketch-a-day/blob/main/2026/sketch_2026_04_11/ : # This is a py5 "module mode" sketch
# learn about py5 modes at https://py5coding.org
"""
Press " " (space bar) to create many balls
Press "w" to drag the mouse and create a static segment (wall)
Press "b" to drag the mouse and create a box
Press "k" to drag the mouse and create a kinematic box
Press "p" to drag the mouse and create a polygon
Press <delete> or <backspace> to remove objects under the mouse
Press "c" to remove dynamic objects
You can drag kinematic objects (and nudge a bit the dynamic ones)
You can use the mouse wheel to rotate some objects
"""
import py5
import shapely
from pymunk_helpers import Simulation, SObj, Constraint
from pymunk_helpers import Segment, Ball, Box, Poly, PivotJoint, PinJoint
ongoing_creation = None
def setup():
py5.size(600, 600)
global sim
sim = Simulation(gravity=(0,0))
#
Segment(50, 550, 550, 550, static=True)
Segment(50, 50, 550, 50, static=True)
Segment(50, 50, 50, 550, fill_color=128, kinematic=True)
Segment(550, 50, 550, 550, fill_color=128, kinematic=True)
ba = Ball(100, 100, 50)
bb = Ball(200, 50, 100)
PinJoint(ba, bb, anchor_a=(100, 120))
sa = Segment(300, 100, 400, 100, fill_color=255, radius=20)
sb = Segment(400, 100, 500, 200, fill_color=255, radius=20)
PivotJoint(sa, sb, (400, 100))
def draw():
py5.background(200)
sim.draw_and_update(step=1/60) # draw and clean up
# create many balls
if py5.is_key_pressed and py5.key == " ":
d = py5.random(10, 50)
c = py5.color(d * 5, 0, 255 - d * 5)
Ball(py5.mouse_x + py5.random(-1, 1), py5.mouse_y, d, c)
# preview & object creation
py5.no_fill()
py5.stroke_weight(2)
py5.stroke(255)
x, y = py5.mouse_x, py5.mouse_y
match ongoing_creation:
case pts, "p":
# poly creation ongoing!
with py5.begin_shape():
py5.vertices(pts)
py5.vertex(x, y)
if (len(pts) == 0) or py5.dist(x, y, *pts[-1]) > 20:
pts.append((x, y))
case sx, sy, "w":
with py5.push_style(), py5.begin_shape():
py5.line(sx, sy, x, y)
case sx, sy, "b" | "k":
py5.rect_mode(py5.CORNERS)
py5.rect(sx, sy, x, y)
def key_pressed():
if py5.key in (py5.BACKSPACE, py5.DELETE):
for obj in sim:
if obj.under_mouse():
obj.remove_from_sim()
elif py5.key == "c":
for obj in sim:
if isinstance(obj, Constraint) or obj.body.body_type not in (sim.STATIC, sim.KINEMATIC):
obj.remove_from_sim()
elif py5.key == 's':
py5.save_frame('###.png')
def mouse_pressed():
global ongoing_creation
if py5.key == "k" and py5.is_key_pressed:
ongoing_creation = (py5.mouse_x, py5.mouse_y, "k")
elif py5.key == "b" and py5.is_key_pressed:
ongoing_creation = (py5.mouse_x, py5.mouse_y, "c")
elif py5.key == "W" and py5.is_key_pressed:
ongoing_creation = (py5.mouse_x, py5.mouse_y, "W")
elif py5.key == "w" and py5.is_key_pressed:
ongoing_creation = (py5.mouse_x, py5.mouse_y, "w")
elif py5.key == "p" and py5.is_key_pressed:
ongoing_creation = ([], "p")
def mouse_released():
global ongoing_creation
mx, my = py5.mouse_x, py5.mouse_y
match ongoing_creation:
case sx, sy, "w":
sim.add_kinematic_segment(sx, sy, mx, my)
case sx, sy, "W":
sim.add_segment(sx, sy, mx, my, radius=20)
case sx, sy, box_type:
w, h = abs(sx - mx), abs(sy - my)
x, y = (sx + mx) / 2, (sy + my) / 2
Box(x, y, w, h, kinematic=(box_type == "k"))
case pts, "p":
shapely_poly = shapely.Polygon(pts) if len(pts) >= 3 else shapely.Polygon()
if shapely_poly.area > 100 and shapely_poly.is_simple:
Poly(shapely_poly, fill_color=py5.color(255))
ongoing_creation = None
def mouse_dragged():
dx, dy = py5.mouse_x - py5.pmouse_x, py5.mouse_y - py5.pmouse_y
if not py5.is_key_pressed:
for obj in sim:
if obj.under_mouse():
if obj.type == sim.KINEMATIC:
obj.translate(dx, dy)
if obj.type == sim.DYNAMIC:
obj.vel_update(dx, dy)
def mouse_wheel(e):
for obj in sim:
if obj.under_mouse() and isinstance(obj, SObj):
obj.rotate(py5.radians(e.get_count()))
py5.run_sketch(block=False) |
|
Today I updated the
class Poly(SObj):
"""
A simulated polygonal, Kinematic or Dynamic, custom object.
It can be created from a sequence of vertices, or shapely [Multi]Polygons.
"""
def __init__(self, geometry:
Sequence[Sequence] | shapely.Polygon | shapely.MultiPolygon,
fill_color=None,
kinematic=False,
simulation=None):
super().__init__(simulation)
if isinstance(geometry, shapely.Polygon):
cx, cy = geometry.centroid.x, geometry.centroid.y
geometry = shapely.affinity.translate(geometry, -cx, -cy)
polygons = (geometry,)
elif isinstance(geometry, shapely.MultiPolygon):
cx, cy = geometry.centroid.x, geometry.centroid.y
geometry = shapely.affinity.translate(geometry, -cx, -cy)
polygons = tuple(geometry.geoms)
else:
geometry = shapely.Polygon(geometry)
cx, cy = geometry.centroid.x, geometry.centroid.y
geometry = shapely.affinity.translate(geometry, -cx, -cy)
polygons = (geometry,)
self.fill_color = fill_color or py5.color(255)
self.geometry = geometry
self.py5shape = py5.convert_shape(geometry)
self.py5shape.disable_style()
self.shapes = []
triangles = [] # the translated to origin triangles
for poly in polygons:
vs, faces = triangulate_polygon(poly)
triangles.extend((tuple(vs[a]), tuple(vs[b]), tuple(vs[c]))
for a, b, c in faces)
if kinematic:
self.body = pymunk.Body(body_type=pymunk.Body.KINEMATIC)
else:
mass = geometry.area * self.simulation.mass_scale
moment = sum(
pymunk.moment_for_poly(mass / len(triangles), tri, (0, 0))
for tri in triangles
)
self.body = pymunk.Body(mass, moment)
for tri in triangles:
shape = pymunk.Poly(self.body, tri)
shape.friction = self.friction
shape.elasticity = self.elasticity
self.shapes.append(shape)
self.body.position = (cx, cy)
self.register_object()
def draw(self):
s = self.simulation.current_sketch
s.fill(self.fill_color)
s.no_stroke()
if self.py5shape is None:
self.py5shape = py5.convert_shape(self.geometry)
with s.push_matrix():
s.translate(self.body.position.x, self.body.position.y)
s.rotate(self.body.angle)
s.shape(self.py5shape)
# trigger removal of falling objects... might be improved
if self.body.position.y > s.height + 200:
self.remove_from_sim() |


Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
I think that the first time I saw pymunk was thanks to some post by @tabreturn. So much fun!
From time to time I try to put my students to use it, but there is a lot of complexity to deal with. Along the years I have always tought of ways of making it simpler to draw the simulations, but nothing really worked very well. Talking to @ramalho, he suggested a "wrapper class" approach, and I now think I have something of a proof of concept!
I even tried my hand at a tiny little bit of meta-programming to help add factory methods ... that stuff might be"hidden" if this becomes a library. Maybe using
match/caseon the main sketch part could be a bit excessive for beginners, I'm not sure. This is work in progress.This depends on pymunk, shapely, trimesh and trimesh's "optional dependency" mapbox_earcut.
If
python -m pip install pymunk trimesh[easy]doesn't work, then trypython -m pip install trimesh shapely mapbox_earcut.All reactions