Skip to content

Instantly share code, notes, and snippets.

@villares
Last active April 28, 2025 22:58
Show Gist options
  • Save villares/124e59f5c089376d28d6380a78b797e6 to your computer and use it in GitHub Desktop.
Save villares/124e59f5c089376d28d6380a78b797e6 to your computer and use it in GitHub Desktop.
Exemplos de Pymunk (imported mode é com o menu py5 ativado no Thonny, e module mode não precisa do plug-in)
import pymunk
CT_DEFAULT = 0
CT_SPECIAL = 1
CT_WALL = 2
MINIMUM_DIST = 50
current_poly = [] # (x, y) tuples while mouse dragging
wall_start = None
drawing_dict = {}
save_filename = 'data.pickle'
def setup():
global space
size(600, 600)
space = pymunk.Space()
space.gravity = 0, 900
space.add_collision_handler(
CT_SPECIAL, CT_DEFAULT
).pre_solve = color_collision
add_wall(100, 500, 500, 500)
def draw(): # py5's main loop
background(100, 0, 200) # R, G, B
for item, (draw_func, kwargs) in list(drawing_dict.items()):
draw_func(item, **kwargs)
if item.body.position.y > height + 50:
space.remove(item)
del drawing_dict[item]
# many new balls
if is_key_pressed and key_code == SHIFT:
add_ball(mouse_x + random(-1, 1),
mouse_y, f=0)
# wall preview
fill_and_stroke('red', 'red', 3)
if wall_start:
line(*wall_start, mouse_x, mouse_y)
if current_poly: # draws poly preview while dragging mouse
with begin_closed_shape():
vertices(current_poly)
# advance the simulation
space.step(1 / 60)
def color_collision(arbiter, space, data):
s1, s2 = arbiter.shapes
s2.collision_type=CT_SPECIAL
s1.collision_type=CT_DEFAULT
drawing_dict[s2][1]['f'] = int(color(255, 0, 0))
drawing_dict[s1][1]['f'] = 0
return True
def add_static_ball(
x, y, diameter=20, f=0, collision_type=CT_DEFAULT
):
radius = diameter / 2
shp = pymunk.Circle(space.static_body, radius, (x, y))
shp.friction = 0.99
shp.collision_type=collision_type
space.add(shp)
drawing_dict[shp] = (
draw_circle,
{'s': None, 'f': f}
)
def add_ball(
x, y, diameter=20, f=255, collision_type=CT_WALL
):
radius = diameter / 2
body = pymunk.Body(radius ** 2 / 10, 100)
body.position = x, y
shp = pymunk.Circle(body, radius, (0, 0))
shp.friction = 0.99
shp.collision_type=collision_type
space.add(body, shp)
drawing_dict[shp] = (
draw_circle,
{'s': None, 'f': f}
)
def add_wall(xa, ya, xb, yb):
shp = pymunk.Segment(space.static_body,
(xa, ya), (xb, yb),
radius=1.5)
shp.collision_type = CT_WALL
shp.friction = 100.99
space.add(shp)
drawing_dict[shp] = (
draw_static_segment,
{'s': 128}
)
def add_poly(poly):
triangs = triangulate(poly)
# this is bad, I should use shply!
(xa, ya), (xb, yb) = min_max(poly)
cx, cy = (xa + xb) / 2, (ya + yb) / 2
polys = []
total_mass = total_moi = 0
for tri in triangs:
poly = [(x - cx, y - cy) for x, y in tri]
mass = poly_area(poly) * 0.1
total_mass += mass
moi = pymunk.moment_for_poly(mass, poly)
if not np.isnan(moi):
total_moi += moi
polys.append(poly)
body = pymunk.Body(total_mass, total_moi)
body.position = cx, cy
shps = []
for poly in polys:
shp = pymunk.Poly(body, poly)
shp.friction = 0.2
shps.append(shp)
drawing_dict[shp] = (
draw_poly,
{'s': None, 'f': 255}
)
space.add(body, *shps) # Note critical * operator expands .add(b, s0, s1, s2...)
def fill_and_stroke(f=255, s=0, weight=None):
stroke_weight(weight or 1)
if s is not None:
stroke(s)
else:
no_stroke()
if f is not None:
fill(f)
else:
no_fill()
def draw_circle(shp, f=0, s=None):
fill_and_stroke(f, s)
circle(shp.body.position.x + shp.offset.x,
shp.body.position.y + shp.offset.y,
shp.radius * 2)
def draw_static_segment(shp, f=None, s=0, weight=3):
fill_and_stroke(f, s, weight)
line(shp.a.x, shp.a.y,
shp.b.x, shp.b.y)
def draw_poly(shp,f=255, s=None, weight=None):
fill_and_stroke(f, s, weight)
with push_matrix():
translate(shp.body.position.x, shp.body.position.y)
rotate(shp.body.angle)
pts = shp.get_vertices()
with begin_closed_shape():
vertices(pts)
def mouse_clicked():
if is_key_pressed and key_code == CONTROL:
f = color(255, 255, 0)
ct = CT_SPECIAL
else:
f = 0
ct = CT_DEFAULT
add_static_ball(mouse_x + random(-1, 1),
mouse_y,
f=255,
collision_type=ct)
def mouse_dragged():
global wall_start
mx, my = mouse_x, mouse_y
if is_key_pressed and key_code == CONTROL:
if (not current_poly or
dist(current_poly[-1][0],
current_poly[-1][1],
mx, my) >= MINIMUM_DIST):
current_poly.append((mx, my))
elif not wall_start:
wall_start = (mx, my)
def min_max(pts):
coords = tuple(zip(*pts))
return tuple(map(min, coords)), tuple(map(max, coords))
def triangulate(original_shape):
"""
Based on https://gist.github.com/Shaptic/6297978
"""
# Use orientation of the top-left-most vertex.
shp = list(original_shape[:])
left, starting_index = shp[0], 0
for i, v in enumerate(shp):
if v[0] < left[0] or (v[0] == left[0] and v[1] < left[1]):
left = v
starting_index = i
orientation = ccw(get_ear(shp, starting_index))
triangs = []
while len(shp) >= 3:
reflex_vertices = []
eartip = -1
for i, v in enumerate(shp): # For each vertex in the shp
if eartip >= 0:
break
triang = get_ear(shp, i) # A triang from vertex to adjacents.
# If polygon's orientation doesn't match that of the triang,
# it's definitely a reflex and not an ear.
if ccw(triang) != orientation:
reflex_vertices.append(v)
continue # Test reflex vertices first.
for rv in reflex_vertices:
if rv in triang:
continue # If we are testing ourselves, skip.
elif in_poly(rv, triang):
break # If any reflex vertex in triang, not ear.
else: # No reflexes, so we test all past current vertex.
for past_current_v in shp[i + 2:]:
if past_current_v in triang:
continue
elif in_poly(past_current_v, triang):
break # No vertices in the triang, we are an ear.
else:
eartip = i
if eartip == -1:
break
triangs.append(get_ear(shp, eartip))
del shp[eartip]
return triangs
def ccw(tri):
"""Tell if triang is counterclockwise."""
assert len(tri) == 3, 'must be a triang'
a, b, c = tri
return (b[0] - a[0]) * (c[1] - a[1]) > (b[1] - a[1]) * (c[0] - a[0])
def in_poly(p, points):
# ray-casting algorithm based on
# https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html
inside = False
if len(points[0]) == 3:
for i, (xi, yi, _) in enumerate(points):
xj, yj, _= points[i - 1]
if ((yi > p[1]) != (yj > p[1])) and (p[0] < (xj - xi) * (p[1] - yi)
/ (yj - yi) + xi):
inside = not inside # an intersection was found
return inside
else:
for i, (xi, yi) in enumerate(points):
xj, yj = points[i - 1]
if ((yi > p[1]) != (yj > p[1])) and (p[0] < (xj - xi) * (p[1] - yi)
/ (yj - yi) + xi):
inside = not inside # an intersection was found
return inside
def get_ear(shp, ear):
return (shp[ear - 1], shp[ear], shp[(ear+1) % len(shp)])
def poly_area(pts):
area = 0.0
for (ax, ay), (bx, by) in zip(pts, pts[1:] + [pts[0]]):
area += ax * by
area -= bx * ay
return abs(area) / 2.0
def mouse_released():
global wall_start
if len(current_poly) >= 3:
add_poly(current_poly)
elif (wall_start and
dist(*wall_start,
mouse_x, mouse_y) > 5):
inicio_x, inicio_y = wall_start
add_wall(inicio_x, inicio_y,
mouse_x, mouse_y)
wall_start = None
current_poly.clear()
def key_pressed():
global wall_start, space, drawing_dict
# tecla DELETE apaga walls
if wall_start and key in (DELETE, ESC):
intercept_escape()
wall_start = None
elif key == DELETE:
for shp in reversed(drawing_dict.keys()):
if isinstance(shp, pymunk.Segment):
space.remove(shp)
del drawing_dict[shp]
break
# "c" limpa balls
elif key in ('c', 'C'):
for shp in space.shapes:
if isinstance(shp, pymunk.Circle):
space.remove(shp)
del drawing_dict[shp]
# "s" salve simulation state
elif key == 's':
save_pickle((space, drawing_dict), 'data.pickle')
print(f'Saved {save_filename}.')
# "l" load from previous saved state
elif key == 'l':
if Path(save_filename).is_file():
space, drawing_dict = load_pickle(save_filename)
print(f'Loaded {save_filename}.')
print(f'shps in simulation: {len(space.shapes)}')
import py5
import pymunk
CT_DEFAULT = 0
CT_SPECIAL = 1
CT_WALL = 2
MINIMUM_DIST = 50
current_poly = [] # (x, y) tuples while mouse dragging
wall_start = None
drawing_dict = {}
save_filename = 'data.pickle'
def setup():
global space
py5.size(600, 600)
space = pymunk.Space()
space.gravity = 0, 900
space.add_collision_handler(
CT_SPECIAL, CT_DEFAULT
).pre_solve = color_collision
add_wall(100, 500, 500, 500)
def draw(): # py5's main loop
py5.background(100, 0, 200) # R, G, B
for item, (draw_func, kwargs) in list(drawing_dict.items()):
draw_func(item, **kwargs)
if item.body.position.y > py5.height + 50:
space.remove(item)
del drawing_dict[item]
# many new balls
if py5.is_key_pressed and py5.key_code == py5.SHIFT:
add_ball(py5.mouse_x + py5.random(-1, 1),
py5.mouse_y, fill=0)
# wall preview
fill_and_stroke('red', 'red', 3)
if wall_start:
py5.line(*wall_start, py5.mouse_x, py5.mouse_y)
if current_poly: # draws poly preview while dragging mouse
with py5.begin_closed_shape():
py5.vertices(current_poly)
# advance the simulation
space.step(1 / 60)
def color_collision(arbiter, space, data):
s1, s2 = arbiter.shapes
s2.collision_type=CT_SPECIAL
s1.collision_type=CT_DEFAULT
drawing_dict[s2][1]['fill'] = int(py5.color(255, 0, 0))
drawing_dict[s1][1]['fill'] = 0
return True
def add_static_ball(
x, y, diameter=20, fill=0, collision_type=CT_DEFAULT
):
radius = diameter / 2
shape = pymunk.Circle(space.static_body, radius, (x, y))
shape.friction = 0.99
shape.collision_type=collision_type
space.add(shape)
drawing_dict[shape] = (
draw_circle,
{'stroke': None, 'fill': fill}
)
def add_ball(
x, y, diameter=20, fill=255, collision_type=CT_WALL
):
radius = diameter / 2
body = pymunk.Body(radius ** 2 / 10, 100)
body.position = x, y
shape = pymunk.Circle(body, radius, (0, 0))
shape.friction = 0.99
shape.collision_type=collision_type
space.add(body, shape)
drawing_dict[shape] = (
draw_circle,
{'stroke': None, 'fill': fill}
)
def add_wall(xa, ya, xb, yb):
shape = pymunk.Segment(space.static_body,
(xa, ya), (xb, yb),
radius=1.5)
shape.collision_type = CT_WALL
shape.friction = 100.99
space.add(shape)
drawing_dict[shape] = (
draw_static_segment,
{'stroke': 128}
)
def add_poly(poly):
triangles = triangulate(poly)
# this is bad, I should use shapely!
(xa, ya), (xb, yb) = min_max(poly)
cx, cy = (xa + xb) / 2, (ya + yb) / 2
polys = []
total_mass = total_moi = 0
for tri in triangles:
poly = [(x - cx, y - cy) for x, y in tri]
mass = poly_area(poly) * 0.1
total_mass += mass
moi = pymunk.moment_for_poly(mass, poly)
if not py5.np.isnan(moi):
total_moi += moi
polys.append(poly)
body = pymunk.Body(total_mass, total_moi)
body.position = cx, cy
shapes = []
for poly in polys:
shp = pymunk.Poly(body, poly)
shp.friction = 0.2
shapes.append(shp)
drawing_dict[shp] = (
draw_poly,
{'stroke': None, 'fill': 255}
)
space.add(body, *shapes) # Note critical * operator expands .add(b, s0, s1, s2...)
def fill_and_stroke(fill=255, stroke=0, weight=None):
py5.stroke_weight(weight or 1)
if stroke is not None:
py5.stroke(stroke)
else:
py5.no_stroke()
if fill is not None:
py5.fill(fill)
else:
py5.no_fill()
def draw_circle(shape, fill=0, stroke=None):
fill_and_stroke(fill, stroke)
py5.circle(shape.body.position.x + shape.offset.x,
shape.body.position.y + shape.offset.y,
shape.radius * 2)
def draw_static_segment(shape, fill=None, stroke=0, weight=3):
fill_and_stroke(fill, stroke, weight)
py5.line(shape.a.x, shape.a.y,
shape.b.x, shape.b.y)
def draw_poly(shape,fill=255, stroke=None, weight=None):
fill_and_stroke(fill, stroke, weight)
with py5.push_matrix():
py5.translate(shape.body.position.x, shape.body.position.y)
py5.rotate(shape.body.angle)
pts = shape.get_vertices()
with py5.begin_closed_shape():
py5.vertices(pts)
def mouse_clicked():
if py5.is_key_pressed and py5.key_code == py5.CONTROL:
f = py5.color(255, 255, 0)
ct = CT_SPECIAL
else:
f = 0
ct = CT_DEFAULT
add_static_ball(py5.mouse_x + py5.random(-1, 1),
py5.mouse_y,
fill=255,
collision_type=ct)
def mouse_dragged():
global wall_start
mx, my = py5.mouse_x, py5.mouse_y
if py5.is_key_pressed and py5.key_code == py5.CONTROL:
if (not current_poly or
py5.dist(current_poly[-1][0],
current_poly[-1][1],
mx, my) >= MINIMUM_DIST):
current_poly.append((mx, my))
elif not wall_start:
wall_start = (mx, my)
def min_max(pts):
coords = tuple(zip(*pts))
return tuple(map(min, coords)), tuple(map(max, coords))
def triangulate(original_shape):
"""
Based on https://gist.github.com/Shaptic/6297978
"""
# Use orientation of the top-left-most vertex.
shape = list(original_shape[:])
left, starting_index = shape[0], 0
for i, v in enumerate(shape):
if v[0] < left[0] or (v[0] == left[0] and v[1] < left[1]):
left = v
starting_index = i
orientation = ccw(get_ear(shape, starting_index))
triangles = []
while len(shape) >= 3:
reflex_vertices = []
eartip = -1
for i, v in enumerate(shape): # For each vertex in the shape
if eartip >= 0:
break
triangle = get_ear(shape, i) # A triangle from vertex to adjacents.
# If polygon's orientation doesn't match that of the triangle,
# it's definitely a reflex and not an ear.
if ccw(triangle) != orientation:
reflex_vertices.append(v)
continue # Test reflex vertices first.
for rv in reflex_vertices:
if rv in triangle:
continue # If we are testing ourselves, skip.
elif in_poly(rv, triangle):
break # If any reflex vertex in triangle, not ear.
else: # No reflexes, so we test all past current vertex.
for past_current_v in shape[i + 2:]:
if past_current_v in triangle:
continue
elif in_poly(past_current_v, triangle):
break # No vertices in the triangle, we are an ear.
else:
eartip = i
if eartip == -1:
break
triangles.append(get_ear(shape, eartip))
del shape[eartip]
return triangles
def ccw(tri):
"""Tell if triangle is counterclockwise."""
assert len(tri) == 3, 'must be a triangle'
a, b, c = tri
return (b[0] - a[0]) * (c[1] - a[1]) > (b[1] - a[1]) * (c[0] - a[0])
def in_poly(p, points):
# ray-casting algorithm based on
# https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html
inside = False
if len(points[0]) == 3:
for i, (xi, yi, _) in enumerate(points):
xj, yj, _= points[i - 1]
if ((yi > p[1]) != (yj > p[1])) and (p[0] < (xj - xi) * (p[1] - yi)
/ (yj - yi) + xi):
inside = not inside # an intersection was found
return inside
else:
for i, (xi, yi) in enumerate(points):
xj, yj = points[i - 1]
if ((yi > p[1]) != (yj > p[1])) and (p[0] < (xj - xi) * (p[1] - yi)
/ (yj - yi) + xi):
inside = not inside # an intersection was found
return inside
def get_ear(shape, ear):
return (shape[ear - 1], shape[ear], shape[(ear+1) % len(shape)])
def poly_area(pts):
area = 0.0
for (ax, ay), (bx, by) in zip(pts, pts[1:] + [pts[0]]):
area += ax * by
area -= bx * ay
return abs(area) / 2.0
def mouse_released():
global wall_start
if len(current_poly) >= 3:
add_poly(current_poly)
elif (wall_start and
py5.dist(*wall_start,
py5.mouse_x, py5.mouse_y) > 5):
inicio_x, inicio_y = wall_start
add_wall(inicio_x, inicio_y,
py5.mouse_x, py5.mouse_y)
wall_start = None
current_poly.clear()
def key_pressed():
global wall_start, space, drawing_dict
# tecla DELETE apaga walls
if wall_start and py5.key in (py5.DELETE, py5.ESC):
py5.intercept_escape()
wall_start = None
elif py5.key == py5.DELETE:
for shape in reversed(drawing_dict.keys()):
if isinstance(shape, pymunk.Segment):
space.remove(shape)
del drawing_dict[shape]
break
# "c" limpa balls
elif py5.key in ('c', 'C'):
for shape in space.shapes:
if isinstance(shape, pymunk.Circle):
space.remove(shape)
del drawing_dict[shape]
# "s" salve simulation state
elif py5.key == 's':
py5.save_pickle((space, drawing_dict), 'data.pickle')
print(f'Saved {save_filename}.')
# "l" load from previous saved state
elif py5.key == 'l':
if py5.Path(save_filename).is_file():
space, drawing_dict = py5.load_pickle(save_filename)
print(f'Loaded {save_filename}.')
print(f'shapes in simulation: {len(space.shapes)}')
py5.run_sketch(block=False)
import pymunk
drawing_dict = {} # o que tem que desenhar
def setup():
global space
size(600, 600)
space = pymunk.Space()
space.gravity = 0, 900
# parede xa, ya, xb, yb
add_wall(100, 500, 500, 500)
add_ball(100, 100, f=color(0, 0, 200))
add_static_ball(200, 200, f=255)
def draw(): # py5's main loop
background(100, 0, 200) # R, G, B
for item, (draw_func, kwargs) in list(drawing_dict.items()):
draw_func(item, **kwargs)
if item.body.position.y > height + 50:
space.remove(item)
del drawing_dict[item]
# acrescenta loucamente bolinhas
if is_key_pressed and key_code == SHIFT:
add_ball(mouse_x + random(-1, 1),
mouse_y, f=0)
# advance the simulation
space.step(1 / 60)
def add_ball(
x, y, diameter=20, f=255
):
radius = diameter / 2
body = pymunk.Body(radius ** 2 / 10, 100)
body.position = x, y
shp = pymunk.Circle(body, radius, (0, 0))
shp.friction = 0.99
space.add(body, shp)
drawing_dict[shp] = (
draw_circle,
{'s': None, 'f': f}
)
def add_static_ball(
x, y, diameter=20, f=0
):
radius = diameter / 2
shp = pymunk.Circle(space.static_body, radius, (x, y))
shp.friction = 0.99
space.add(shp)
drawing_dict[shp] = (
draw_circle,
{'s': None, 'f': f}
)
def add_wall(xa, ya, xb, yb):
shp = pymunk.Segment(space.static_body,
(xa, ya), (xb, yb),
radius=1.5)
shp.friction = 100.99
space.add(shp)
drawing_dict[shp] = (
draw_static_segment,
{'s': 128}
)
def fill_and_stroke(f=255, s=0, weight=None):
stroke_weight(weight or 1)
if s is not None:
stroke(s)
else:
no_stroke()
if f is not None:
fill(f)
else:
no_fill()
def draw_circle(shp, f=0, s=None):
fill_and_stroke(f, s)
circle(shp.body.position.x + shp.offset.x,
shp.body.position.y + shp.offset.y,
shp.radius * 2)
def draw_static_segment(shp, f=None, s=0, weight=3):
fill_and_stroke(f, s, weight)
line(shp.a.x, shp.a.y,
shp.b.x, shp.b.y)
def mouse_clicked():
if is_key_pressed and key_code == CONTROL:
add_static_ball(mouse_x + random(-1, 1),
mouse_y,
f=255)
else:
add_ball(mouse_x + random(-1, 1),
mouse_y,
diameter=30,
f=color(0, 255, 0))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment