CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-pymunk

Pymunk is a easy-to-use pythonic 2D physics library built on Munk2D

Pending
Overview
Eval results
Files

geometry.mddocs/

Geometry Processing and Generation

Pymunk provides advanced geometry processing capabilities for automatic shape generation, convex hull calculation, polygon simplification, and bitmap-to-geometry conversion using marching squares algorithms.

Autogeometry Module

The pymunk.autogeometry module contains functions for automatic geometry generation and processing, particularly useful for converting images or bitmap data into physics collision shapes.

Polyline Processing

Functions for processing and simplifying polylines (sequences of connected points).

def is_closed(polyline: list[tuple[float, float]]) -> bool:
    """
    Test if a polyline is closed (first vertex equals last vertex).
    
    Args:
        polyline: List of (x, y) coordinate tuples
        
    Returns:
        True if polyline forms a closed loop
        
    Example:
        open_line = [(0, 0), (10, 0), (10, 10)]
        closed_line = [(0, 0), (10, 0), (10, 10), (0, 0)]
        
        print(is_closed(open_line))   # False
        print(is_closed(closed_line)) # True
    """

def simplify_curves(polyline: list[tuple[float, float]], tolerance: float) -> list[Vec2d]:
    """
    Simplify a polyline using the Douglas-Peucker algorithm.
    
    Excellent for smooth or gently curved shapes, removes redundant vertices
    while preserving overall shape within tolerance.
    
    Args:
        polyline: List of (x, y) coordinate tuples
        tolerance: Maximum allowed deviation from original shape
        
    Returns:
        Simplified polyline as list of Vec2d points
        
    Example:
        # Smooth curve with many points
        curve = [(i, math.sin(i * 0.1) * 10) for i in range(100)]
        simplified = pymunk.autogeometry.simplify_curves(curve, tolerance=1.0)
        print(f"Reduced from {len(curve)} to {len(simplified)} points")
    """

def simplify_vertexes(polyline: list[tuple[float, float]], tolerance: float) -> list[Vec2d]:
    """
    Simplify a polyline by discarding "flat" vertices.
    
    Excellent for straight-edged or angular shapes, removes vertices that
    don't significantly change direction.
    
    Args:
        polyline: List of (x, y) coordinate tuples  
        tolerance: Maximum allowed angular deviation
        
    Returns:
        Simplified polyline with flat vertices removed
        
    Example:
        # Rectangle with extra points on edges
        rect_with_extras = [
            (0, 0), (5, 0), (10, 0),     # Extra point at (5,0)
            (10, 5), (10, 10),           # Extra point at (10,5) 
            (5, 10), (0, 10),            # Extra point at (5,10)
            (0, 5), (0, 0)               # Extra point at (0,5)
        ]
        simplified = pymunk.autogeometry.simplify_vertexes(rect_with_extras, 0.1)
        # Result: [(0,0), (10,0), (10,10), (0,10), (0,0)] - clean rectangle
    """

Convex Hull Generation

def to_convex_hull(polyline: list[tuple[float, float]], tolerance: float) -> list[Vec2d]:
    """
    Calculate the convex hull of a set of points.
    
    Returns the smallest convex polygon that contains all input points.
    Useful for creating simplified collision shapes from complex geometry.
    
    Args:
        polyline: List of (x, y) coordinate tuples
        tolerance: Precision tolerance for hull calculation
        
    Returns:
        Convex hull as closed polyline (first and last points are same)
        
    Example:
        # Complex concave shape  
        points = [
            (0, 0), (10, 0), (5, 5),     # Triangle with internal point
            (15, 2), (20, 0), (25, 5),   # More complex boundary
            (20, 10), (10, 8), (0, 10)
        ]
        
        hull = pymunk.autogeometry.to_convex_hull(points, 0.1)
        
        # Create physics shape from hull
        hull_body = pymunk.Body(body_type=pymunk.Body.STATIC)
        hull_shape = pymunk.Poly(hull_body, hull[:-1])  # Exclude duplicate last point
        space.add(hull_body, hull_shape)
    """

Convex Decomposition

def convex_decomposition(polyline: list[tuple[float, float]], tolerance: float) -> list[list[Vec2d]]:
    """
    Decompose a complex polygon into multiple convex polygons.
    
    Breaks down concave shapes into simpler convex parts that can be
    used directly as Pymunk collision shapes.
    
    Args:
        polyline: List of (x, y) coordinate tuples forming the polygon
        tolerance: Approximation tolerance
        
    Returns:
        List of convex polygons, each as a list of Vec2d points
        
    Warning:
        Self-intersecting polygons may produce overly simplified results.
        
    Example:
        # L-shaped polygon (concave)
        l_shape = [
            (0, 0), (10, 0), (10, 5),
            (5, 5), (5, 10), (0, 10), (0, 0)
        ]
        
        convex_parts = pymunk.autogeometry.convex_decomposition(l_shape, 0.1)
        
        # Create physics shapes for each convex part
        body = pymunk.Body(body_type=pymunk.Body.STATIC) 
        for i, part in enumerate(convex_parts):
            shape = pymunk.Poly(body, part)
            shape.collision_type = i  # Different collision types if needed
            space.add(shape)
    """

Marching Squares - Bitmap to Geometry

Convert bitmap data (images, heightmaps, etc.) into collision geometry using marching squares algorithms.

def march_soft(
    bb: BB,
    x_samples: int, 
    y_samples: int,
    threshold: float,
    sample_func: Callable[[tuple[float, float]], float]
) -> PolylineSet:
    """
    Trace anti-aliased contours from bitmap data using marching squares.
    
    Creates smooth contours by interpolating between sample points.
    Excellent for organic shapes and smooth terrain generation.
    
    Args:
        bb: Bounding box of area to sample
        x_samples: Number of sample points horizontally
        y_samples: Number of sample points vertically  
        threshold: Value threshold for inside/outside determination
        sample_func: Function that returns sample value at (x, y) coordinate
        
    Returns:
        PolylineSet containing traced contour polylines
        
    Example:
        # Convert circular bitmap to geometry
        def circle_sample(point):
            x, y = point
            center = (50, 50)
            radius = 20
            distance = math.sqrt((x - center[0])**2 + (y - center[1])**2)
            return 1.0 if distance <= radius else 0.0
        
        bb = pymunk.BB(0, 0, 100, 100)
        polylines = pymunk.autogeometry.march_soft(
            bb, x_samples=50, y_samples=50, threshold=0.5, sample_func=circle_sample
        )
        
        # Create collision shapes from contours
        for polyline in polylines:
            if len(polyline) >= 3:  # Need at least 3 points for polygon
                body = space.static_body
                shape = pymunk.Poly(body, polyline)
                space.add(shape)
    """

def march_hard(
    bb: BB,
    x_samples: int,
    y_samples: int, 
    threshold: float,
    sample_func: Callable[[tuple[float, float]], float]
) -> PolylineSet:
    """
    Trace aliased (pixelated) contours from bitmap data.
    
    Creates hard-edged, pixelated contours following exact sample boundaries.
    Better for pixel art, tile-based games, or when exact pixel alignment is needed.
    
    Args:
        bb: Bounding box of area to sample
        x_samples: Number of sample points horizontally
        y_samples: Number of sample points vertically
        threshold: Value threshold for inside/outside determination  
        sample_func: Function that returns sample value at (x, y) coordinate
        
    Returns:
        PolylineSet containing traced contour polylines
        
    Example:
        # Convert pixel art to collision shapes
        pixel_art = [
            [0, 0, 0, 0, 0],
            [0, 1, 1, 1, 0], 
            [0, 1, 0, 1, 0],
            [0, 1, 1, 1, 0],
            [0, 0, 0, 0, 0]
        ]
        
        def pixel_sample(point):
            x, y = int(point[0]), int(point[1])
            if 0 <= x < 5 and 0 <= y < 5:
                return pixel_art[y][x]
            return 0
        
        bb = pymunk.BB(0, 0, 5, 5) 
        polylines = pymunk.autogeometry.march_hard(
            bb, x_samples=5, y_samples=5, threshold=0.5, sample_func=pixel_sample
        )
    """

PolylineSet Class

class PolylineSet:
    """
    Collection of polylines returned by marching squares algorithms.
    
    Provides sequence interface for easy iteration and processing.
    """
    
    def __init__(self) -> None:
        """Create empty polyline set."""
    
    def collect_segment(self, v0: tuple[float, float], v1: tuple[float, float]) -> None:
        """
        Add line segment to appropriate polyline.
        
        Automatically connects segments into continuous polylines.
        
        Args:
            v0: Start point of segment
            v1: End point of segment
        """
    
    # Sequence interface
    def __len__(self) -> int:
        """Return number of polylines in set."""
        
    def __getitem__(self, index: int) -> list[Vec2d]:
        """Get polyline at index."""
        
    def __iter__(self):
        """Iterate over polylines."""

Utility Module (Deprecated)

The pymunk.util module contains legacy geometry utilities. These functions are deprecated in favor of autogeometry but remain for compatibility.

Polygon Analysis

import pymunk.util

def is_clockwise(points: list[tuple[float, float]]) -> bool:
    """
    Test if points form a clockwise polygon.
    
    Args:
        points: List of (x, y) coordinate tuples
        
    Returns:
        True if points are in clockwise order
        
    Example:
        cw_points = [(0, 0), (10, 0), (10, 10), (0, 10)]      # Clockwise  
        ccw_points = [(0, 0), (0, 10), (10, 10), (10, 0)]     # Counter-clockwise
        
        print(pymunk.util.is_clockwise(cw_points))   # True
        print(pymunk.util.is_clockwise(ccw_points))  # False
    """

def is_convex(points: list[tuple[float, float]]) -> bool:
    """
    Test if polygon is convex.
    
    Args:
        points: List of at least 3 (x, y) coordinate tuples
        
    Returns:
        True if polygon is convex (no internal angles > 180°)
        
    Example:
        square = [(0, 0), (10, 0), (10, 10), (0, 10)]
        l_shape = [(0, 0), (10, 0), (10, 5), (5, 5), (5, 10), (0, 10)]
        
        print(pymunk.util.is_convex(square))   # True
        print(pymunk.util.is_convex(l_shape)) # False  
    """

def is_left(p0: tuple[float, float], p1: tuple[float, float], p2: tuple[float, float]) -> int:
    """
    Test if point p2 is left, on, or right of line from p0 to p1.
    
    Args:
        p0: First point defining the line
        p1: Second point defining the line
        p2: Point to test
        
    Returns:
        > 0 if p2 is left of the line
        = 0 if p2 is on the line  
        < 0 if p2 is right of the line
    """

Geometric Calculations

import pymunk.util

def calc_center(points: list[tuple[float, float]]) -> Vec2d:
    """
    Calculate centroid (geometric center) of polygon.
    
    Args:
        points: Polygon vertices
        
    Returns:
        Center point as Vec2d
    """

def calc_area(points: list[tuple[float, float]]) -> float:
    """
    Calculate signed area of polygon.
    
    Returns:
        Positive area for counter-clockwise polygons
        Negative area for clockwise polygons
    """

def calc_perimeter(points: list[tuple[float, float]]) -> float:
    """Calculate perimeter (total edge length) of polygon."""

def convex_hull(points: list[tuple[float, float]]) -> list[Vec2d]:
    """
    Calculate convex hull using Graham scan algorithm.
    
    Returns:
        Convex hull vertices in counter-clockwise order
    """

def triangulate(polygon: list[tuple[float, float]]) -> list[list[Vec2d]]:
    """
    Triangulate polygon into triangles.
    
    Args:
        polygon: Simple polygon vertices
        
    Returns:
        List of triangles, each as 3-vertex list
    """

Usage Examples

Terrain Generation from Heightmap

import pymunk
import pymunk.autogeometry
import math

def generate_terrain_from_heightmap(heightmap, world_width, world_height):
    """Generate physics terrain from 2D heightmap array"""
    
    height_samples = len(heightmap)
    width_samples = len(heightmap[0]) if heightmap else 0
    
    def sample_height(point):
        x, y = point
        # Normalize to heightmap coordinates
        hx = int((x / world_width) * width_samples)
        hy = int((y / world_height) * height_samples)
        
        # Clamp to valid range
        hx = max(0, min(width_samples - 1, hx))
        hy = max(0, min(height_samples - 1, hy))
        
        return heightmap[hy][hx]
    
    # Define terrain bounding box
    bb = pymunk.BB(0, 0, world_width, world_height)
    
    # Generate contour lines at different heights
    terrain_shapes = []
    for height_level in [0.2, 0.4, 0.6, 0.8]:
        polylines = pymunk.autogeometry.march_soft(
            bb, x_samples=50, y_samples=50, 
            threshold=height_level, sample_func=sample_height
        )
        
        # Convert polylines to collision shapes
        for polyline in polylines:
            if len(polyline) >= 3:
                # Simplify for performance
                simplified = pymunk.autogeometry.simplify_curves(polyline, tolerance=2.0)
                if len(simplified) >= 3:
                    body = space.static_body
                    shape = pymunk.Poly(body, simplified)
                    shape.friction = 0.7
                    terrain_shapes.append(shape)
    
    return terrain_shapes

# Example heightmap (mountain shape)
size = 20
heightmap = []
for y in range(size):
    row = []
    for x in range(size):
        # Create mountain shape
        center_x, center_y = size // 2, size // 2
        distance = math.sqrt((x - center_x)**2 + (y - center_y)**2)
        height = max(0, 1.0 - (distance / (size // 2)))
        row.append(height)
    heightmap.append(row)

# Generate terrain
terrain_shapes = generate_terrain_from_heightmap(heightmap, 800, 600)
space.add(*terrain_shapes)

Image-Based Collision Generation

import pymunk
import pymunk.autogeometry
from PIL import Image  # Requires Pillow: pip install pillow

def create_collision_from_image(image_path, scale=1.0, threshold=128):
    """Create collision shapes from image alpha channel or brightness"""
    
    # Load image
    image = Image.open(image_path).convert('RGBA')
    width, height = image.size
    pixels = image.load()
    
    def sample_image(point):
        x, y = int(point[0] / scale), int(point[1] / scale)
        
        # Clamp coordinates
        x = max(0, min(width - 1, x))  
        y = max(0, min(height - 1, y))
        
        # Sample alpha channel (or brightness for non-alpha images)
        r, g, b, a = pixels[x, y]
        return 1.0 if a > threshold else 0.0  # Use alpha for transparency
    
    # Define sampling area
    bb = pymunk.BB(0, 0, width * scale, height * scale)
    
    # Generate contours
    polylines = pymunk.autogeometry.march_soft(
        bb, x_samples=width, y_samples=height,
        threshold=0.5, sample_func=sample_image
    )
    
    # Convert to physics shapes
    shapes = []
    for polyline in polylines:
        if len(polyline) >= 3:
            # Simplify contour
            simplified = pymunk.autogeometry.simplify_curves(polyline, tolerance=1.0)
            
            if len(simplified) >= 3:
                body = space.static_body
                shape = pymunk.Poly(body, simplified)
                shape.friction = 0.7
                shapes.append(shape)
    
    return shapes

# Usage
collision_shapes = create_collision_from_image("sprite.png", scale=2.0)
space.add(*collision_shapes)

Complex Shape Decomposition

import pymunk
import pymunk.autogeometry

def create_complex_shape(vertices, body):
    """Create physics shapes for complex (possibly concave) polygon"""
    
    # Check if shape is already convex
    if pymunk.util.is_convex(vertices):
        # Simple case - create single polygon
        shape = pymunk.Poly(body, vertices)
        return [shape]
    else:
        # Complex case - decompose into convex parts
        convex_parts = pymunk.autogeometry.convex_decomposition(vertices, tolerance=1.0)
        
        shapes = []
        for i, part in enumerate(convex_parts):
            if len(part) >= 3:
                shape = pymunk.Poly(body, part)
                shape.collision_type = i  # Different collision types for each part
                shapes.append(shape)
        
        return shapes

# Example: L-shaped building
l_building = [
    (0, 0), (100, 0), (100, 60),     # Main rectangle
    (60, 60), (60, 100), (0, 100),   # Extension  
    (0, 0)                           # Close polygon
]

building_body = pymunk.Body(body_type=pymunk.Body.STATIC)
building_shapes = create_complex_shape(l_building, building_body)

space.add(building_body, *building_shapes)

Procedural Level Generation

import pymunk
import pymunk.autogeometry
import random
import math

def generate_cave_system(width, height, complexity=0.1):
    """Generate cave system using noise-based sampling"""
    
    def cave_sample(point):
        x, y = point
        
        # Multiple octaves of noise for complexity
        noise = 0
        amplitude = 1
        frequency = complexity
        
        for octave in range(4):
            # Simple noise approximation (replace with proper noise library)
            nx = x * frequency + octave * 100
            ny = y * frequency + octave * 100
            octave_noise = (math.sin(nx * 0.02) + math.cos(ny * 0.03)) * 0.5
            
            noise += octave_noise * amplitude
            amplitude *= 0.5
            frequency *= 2
        
        # Add some randomness
        noise += (random.random() - 0.5) * 0.2
        
        # Threshold for cave walls
        return 1.0 if noise > 0.1 else 0.0
    
    # Generate cave geometry
    bb = pymunk.BB(0, 0, width, height)
    polylines = pymunk.autogeometry.march_hard(
        bb, x_samples=width//10, y_samples=height//10,
        threshold=0.5, sample_func=cave_sample
    )
    
    # Create collision shapes
    cave_shapes = []
    for polyline in polylines:
        # Simplify cave walls
        simplified = pymunk.autogeometry.simplify_vertexes(polyline, tolerance=5.0)
        
        if len(simplified) >= 3:
            body = space.static_body
            shape = pymunk.Poly(body, simplified)
            shape.friction = 0.8
            shape.collision_type = 1  # Cave walls
            cave_shapes.append(shape)
    
    return cave_shapes

# Generate procedural cave level
cave_walls = generate_cave_system(800, 600, complexity=0.05)
space.add(*cave_walls)

Polygon Optimization Pipeline

import pymunk
import pymunk.autogeometry

def optimize_polygon_for_physics(vertices, target_vertex_count=8):
    """Optimize polygon for physics simulation performance"""
    
    # Step 1: Remove flat vertices 
    simplified_vertices = pymunk.autogeometry.simplify_vertexes(vertices, tolerance=1.0)
    
    # Step 2: If still too complex, use curve simplification
    if len(simplified_vertices) > target_vertex_count:
        simplified_vertices = pymunk.autogeometry.simplify_curves(
            simplified_vertices, tolerance=2.0
        )
    
    # Step 3: If still too complex, use convex hull
    if len(simplified_vertices) > target_vertex_count:
        simplified_vertices = pymunk.autogeometry.to_convex_hull(
            simplified_vertices, tolerance=1.0
        )
    
    return simplified_vertices

# Example: Optimize complex shape for physics
complex_shape = [(i, math.sin(i * 0.1) * 20) for i in range(100)]  # 100 vertices
optimized_shape = optimize_polygon_for_physics(complex_shape, target_vertex_count=6)

print(f"Reduced from {len(complex_shape)} to {len(optimized_shape)} vertices")

# Create optimized physics shape
body = pymunk.Body(10, pymunk.moment_for_poly(10, optimized_shape))
shape = pymunk.Poly(body, optimized_shape)
space.add(body, shape)

Pymunk's geometry processing capabilities provide powerful tools for converting complex artwork, images, and procedural data into efficient physics collision shapes, enabling sophisticated level generation and automatic collision boundary creation for games and simulations.

Install with Tessl CLI

npx tessl i tessl/pypi-pymunk

docs

bodies-shapes.md

constraints.md

geometry.md

index.md

physics-world.md

utilities.md

visualization.md

tile.json