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

visualization.mddocs/

Debug Visualization and Drawing

Pymunk provides comprehensive debug visualization capabilities for popular graphics libraries, enabling easy debugging and prototyping of physics simulations with visual feedback.

SpaceDebugDrawOptions - Base Class

The foundation for all debug drawing implementations, providing customizable rendering options and colors.

Class Definition

class SpaceDebugDrawOptions:
    """
    Base class for configuring debug drawing of physics spaces.
    
    Provides customizable colors, drawing flags, and transformation options.
    Subclassed by library-specific implementations (pygame, pyglet, matplotlib).
    """
    
    # Drawing flags (combine with bitwise OR)
    DRAW_SHAPES: int           # Draw collision shapes  
    DRAW_CONSTRAINTS: int      # Draw joints/constraints
    DRAW_COLLISION_POINTS: int # Draw collision contact points
    
    def __init__(self) -> None:
        """
        Create debug draw options with default settings.
        
        Default flags enable drawing of shapes, constraints, and collision points.
        """

Drawing Control Flags

# Control what elements are drawn
options.flags: int = (
    SpaceDebugDrawOptions.DRAW_SHAPES | 
    SpaceDebugDrawOptions.DRAW_CONSTRAINTS |
    SpaceDebugDrawOptions.DRAW_COLLISION_POINTS
)
"""
Bitmask controlling what elements to draw.
Combine flags with bitwise OR to enable multiple elements.

Examples:
    options.flags = SpaceDebugDrawOptions.DRAW_SHAPES  # Only shapes
    options.flags = SpaceDebugDrawOptions.DRAW_SHAPES | SpaceDebugDrawOptions.DRAW_CONSTRAINTS  # Shapes + constraints
    options.flags = 0  # Draw nothing
"""

Color Configuration

# Shape colors based on body type
options.shape_dynamic_color: SpaceDebugColor = SpaceDebugColor(52, 152, 219, 255)
"""Color for dynamic body shapes (blue)"""

options.shape_static_color: SpaceDebugColor = SpaceDebugColor(149, 165, 166, 255)  
"""Color for static body shapes (gray)"""

options.shape_kinematic_color: SpaceDebugColor = SpaceDebugColor(39, 174, 96, 255)
"""Color for kinematic body shapes (green)"""

options.shape_sleeping_color: SpaceDebugColor = SpaceDebugColor(114, 148, 168, 255)
"""Color for sleeping body shapes (darker blue)"""

# Element-specific colors
options.shape_outline_color: SpaceDebugColor = SpaceDebugColor(44, 62, 80, 255)
"""Color for shape outlines (dark blue-gray)"""

options.constraint_color: SpaceDebugColor = SpaceDebugColor(142, 68, 173, 255)
"""Color for constraints/joints (purple)"""

options.collision_point_color: SpaceDebugColor = SpaceDebugColor(231, 76, 60, 255)
"""Color for collision points (red)"""

Coordinate Transformation

options.transform: Transform = Transform.identity()
"""
Transformation applied to all drawing coordinates.

Use to implement camera/viewport transformations:
- Translation: Move view to follow objects
- Scaling: Zoom in/out 
- Rotation: Rotate view

Example:
    # Camera following player
    camera_pos = player.position
    options.transform = Transform.translation(-camera_pos.x, -camera_pos.y)
    
    # Zoom and center
    zoom = 2.0
    center = (400, 300)  # Screen center
    options.transform = (
        Transform.translation(*center) @
        Transform.scaling(zoom) @
        Transform.translation(-camera_pos.x, -camera_pos.y)
    )
"""

SpaceDebugColor - Color Utility

class SpaceDebugColor(NamedTuple):
    """
    RGBA color tuple for debug drawing.
    
    Values are typically 0-255 for integer colors or 0.0-1.0 for float colors.
    """
    
    r: float  # Red component
    g: float  # Green component  
    b: float  # Blue component
    a: float  # Alpha component (opacity)
    
    def as_int(self) -> tuple[int, int, int, int]:
        """
        Return color as integer tuple (0-255 range).
        
        Example:
            color = SpaceDebugColor(1.0, 0.5, 0.0, 1.0)
            rgba_int = color.as_int()  # (255, 128, 0, 255)
        """
    
    def as_float(self) -> tuple[float, float, float, float]:
        """
        Return color as float tuple (0.0-1.0 range).
        
        Example:
            color = SpaceDebugColor(255, 128, 0, 255)  
            rgba_float = color.as_float()  # (1.0, 0.5, 0.0, 1.0)
        """

# Common colors
RED = SpaceDebugColor(255, 0, 0, 255)
GREEN = SpaceDebugColor(0, 255, 0, 255)  
BLUE = SpaceDebugColor(0, 0, 255, 255)
WHITE = SpaceDebugColor(255, 255, 255, 255)
BLACK = SpaceDebugColor(0, 0, 0, 255)
TRANSPARENT = SpaceDebugColor(0, 0, 0, 0)

Pygame Integration

High-performance debug drawing for Pygame applications with coordinate system handling.

Setup and Usage

import pygame
import pymunk
import pymunk.pygame_util

# Initialize Pygame  
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()

# Create physics space
space = pymunk.Space()
space.gravity = (0, -981)

# Create debug drawing options
draw_options = pymunk.pygame_util.DrawOptions(screen)

# Main loop
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
    
    # Clear screen
    screen.fill((255, 255, 255))  # White background
    
    # Step physics
    dt = clock.tick(60) / 1000.0
    space.step(dt)
    
    # Draw physics debug visualization
    space.debug_draw(draw_options)
    
    pygame.display.flip()

pygame.quit()

Coordinate System Configuration

import pymunk.pygame_util

# Pygame uses Y-down coordinates, physics often uses Y-up
# Configure coordinate system behavior:

pymunk.pygame_util.positive_y_is_up = False  # Default - Y increases downward
"""
Control Y-axis direction:
- False (default): Y increases downward (standard Pygame)
- True: Y increases upward (mathematical convention)

When True:    When False:
y ^           +------ > x  
  |           |
  +---> x     v y
"""

# If using Y-up physics, set gravity appropriately:
if pymunk.pygame_util.positive_y_is_up:
    space.gravity = (0, -981)  # Downward gravity
else:
    space.gravity = (0, 981)   # "Upward" gravity (actually downward in screen coords)

Coordinate Conversion Utilities

def get_mouse_pos(surface: pygame.Surface) -> Vec2d:
    """
    Get mouse position converted to physics coordinates.
    
    Handles coordinate system conversion based on positive_y_is_up setting.
    
    Example:
        mouse_pos = pymunk.pygame_util.get_mouse_pos(screen)
        hit = space.point_query_nearest(mouse_pos, 0, pymunk.ShapeFilter())
    """

def to_pygame(point: Vec2d, surface: pygame.Surface) -> tuple[int, int]:
    """
    Convert physics coordinates to Pygame screen coordinates.
    
    Args:
        point: Physics world coordinate
        surface: Pygame surface for height reference
        
    Returns:
        (x, y) tuple in Pygame coordinates
    """

def from_pygame(point: tuple[int, int], surface: pygame.Surface) -> Vec2d:
    """
    Convert Pygame screen coordinates to physics coordinates.
    
    Args:
        point: Pygame coordinate (x, y)
        surface: Pygame surface for height reference
        
    Returns:
        Vec2d in physics world coordinates
    """

Custom Colors

import pygame
import pymunk.pygame_util

# Create debug options
draw_options = pymunk.pygame_util.DrawOptions(screen)

# Customize colors
draw_options.shape_outline_color = pymunk.SpaceDebugColor(255, 0, 0, 255)  # Red outlines
draw_options.constraint_color = pymunk.SpaceDebugColor(0, 255, 0, 255)     # Green constraints
draw_options.collision_point_color = pymunk.SpaceDebugColor(255, 255, 0, 255)  # Yellow collision points

# Set individual shape colors
circle_shape.color = pygame.Color("pink")
box_shape.color = (0, 255, 255, 255)  # Cyan

# Control what gets drawn
draw_options.flags = pymunk.SpaceDebugDrawOptions.DRAW_SHAPES  # Only shapes, no constraints

Pyglet Integration

OpenGL-based debug drawing with batched rendering for performance.

Setup and Usage

import pyglet
import pymunk
import pymunk.pyglet_util

# Create window
window = pyglet.window.Window(800, 600, "Physics Debug")

# Create physics space
space = pymunk.Space()
space.gravity = (0, -981)

# Create debug drawing options
draw_options = pymunk.pyglet_util.DrawOptions()

@window.event
def on_draw():
    window.clear()
    
    # Draw physics debug visualization  
    space.debug_draw(draw_options)

def update(dt):
    space.step(dt)

# Schedule physics updates
pyglet.clock.schedule_interval(update, 1/60.0)

# Run application
pyglet.app.run()

Batched Drawing for Performance

import pyglet
import pymunk.pyglet_util

# Create custom batch for optimized rendering
batch = pyglet.graphics.Batch()

# Create debug options with custom batch
draw_options = pymunk.pyglet_util.DrawOptions(batch=batch)

@window.event
def on_draw():
    window.clear()
    
    # Draw physics (adds to batch, doesn't render immediately)
    space.debug_draw(draw_options) 
    
    # Render entire batch efficiently  
    batch.draw()

Context Manager Support

import pyglet
import pymunk.pyglet_util

draw_options = pymunk.pyglet_util.DrawOptions()

@window.event  
def on_draw():
    window.clear()
    
    # Use context manager for automatic OpenGL state management
    with draw_options:
        space.debug_draw(draw_options)

Custom Shape Colors

import pyglet
import pymunk.pyglet_util

# Set shape-specific colors
circle_shape.color = (255, 0, 0, 255)      # Red circle
box_shape.color = (0, 255, 0, 255)         # Green box
segment_shape.color = (0, 0, 255, 255)     # Blue segment

# Use Pyglet color utilities
import pyglet.image
circle_shape.color = pyglet.image.get_color("pink")

Matplotlib Integration

Publication-quality debug visualization for analysis and documentation.

Setup and Usage

import matplotlib.pyplot as plt
import pymunk  
import pymunk.matplotlib_util

# Create figure and axes
fig, ax = plt.subplots(figsize=(10, 8))

# Create physics space
space = pymunk.Space()
space.gravity = (0, -981)

# Add some physics objects...
# ... physics simulation code ...

# Create debug drawing options
draw_options = pymunk.matplotlib_util.DrawOptions(ax)

# Draw physics state
space.debug_draw(draw_options)

# Configure plot
ax.set_xlim(0, 800)
ax.set_ylim(0, 600)
ax.set_aspect('equal')
ax.grid(True, alpha=0.3)
ax.set_title('Physics Simulation Debug View')

plt.show()

Animation and Time Series

import matplotlib.pyplot as plt
import matplotlib.animation as animation
import pymunk
import pymunk.matplotlib_util

fig, ax = plt.subplots()
space = pymunk.Space()
draw_options = pymunk.matplotlib_util.DrawOptions(ax)

def animate(frame):
    ax.clear()
    
    # Step physics
    space.step(1/60.0)
    
    # Draw current state
    space.debug_draw(draw_options)
    
    # Configure axes
    ax.set_xlim(0, 800)
    ax.set_ylim(0, 600)
    ax.set_aspect('equal')
    ax.set_title(f'Frame {frame}')

# Create animation
anim = animation.FuncAnimation(fig, animate, interval=16, blit=False)
plt.show()

# Save as video
# anim.save('physics_simulation.mp4', writer='ffmpeg', fps=60)

Custom Shape Styling

import matplotlib.pyplot as plt
import pymunk.matplotlib_util

# Set shape colors using matplotlib color formats
circle_shape.color = (1.0, 0.0, 0.0, 1.0)     # Red (RGBA floats)
box_shape.color = 'blue'                       # Named color
segment_shape.color = '#00FF00'                # Hex color

# Create debug options with custom styling
draw_options = pymunk.matplotlib_util.DrawOptions(ax)
draw_options.shape_outline_color = pymunk.SpaceDebugColor(0, 0, 0, 255)  # Black outlines

Advanced Visualization Techniques

Camera and Viewport Control

import pymunk

class Camera:
    def __init__(self, position=(0, 0), zoom=1.0):
        self.position = pymunk.Vec2d(*position)
        self.zoom = zoom
    
    def follow_body(self, body, screen_size):
        """Make camera follow a body"""
        self.position = body.position
    
    def get_transform(self, screen_size):
        """Get transform matrix for debug drawing"""
        screen_center = pymunk.Vec2d(*screen_size) * 0.5
        
        return (
            pymunk.Transform.translation(*screen_center) @
            pymunk.Transform.scaling(self.zoom) @
            pymunk.Transform.translation(-self.position.x, -self.position.y)
        )

# Usage with any debug drawing system
camera = Camera()
camera.follow_body(player_body, (800, 600))

draw_options.transform = camera.get_transform((800, 600))
space.debug_draw(draw_options)

Selective Drawing

import pymunk

class SelectiveDrawOptions(pymunk.pygame_util.DrawOptions):
    """Custom draw options that only draw specific shapes"""
    
    def __init__(self, surface, shape_filter=None):
        super().__init__(surface)
        self.shape_filter = shape_filter or (lambda shape: True)
    
    def draw_shape(self, shape):
        """Override to implement selective drawing"""
        if self.shape_filter(shape):
            super().draw_shape(shape)

# Filter functions
def draw_only_dynamic(shape):
    return shape.body.body_type == pymunk.Body.DYNAMIC

def draw_only_circles(shape):
    return isinstance(shape, pymunk.Circle)

# Usage
draw_options = SelectiveDrawOptions(screen, draw_only_dynamic)
space.debug_draw(draw_options)

Performance Monitoring

import time
import pymunk

class ProfiledDrawOptions(pymunk.pygame_util.DrawOptions):
    """Draw options with performance monitoring"""
    
    def __init__(self, surface):
        super().__init__(surface)
        self.draw_time = 0
        self.shape_count = 0
    
    def draw_shape(self, shape):
        start_time = time.perf_counter()
        super().draw_shape(shape)
        self.draw_time += time.perf_counter() - start_time
        self.shape_count += 1
    
    def reset_stats(self):
        self.draw_time = 0
        self.shape_count = 0

# Usage  
draw_options = ProfiledDrawOptions(screen)
space.debug_draw(draw_options)

print(f"Drew {draw_options.shape_count} shapes in {draw_options.draw_time:.3f}s")
draw_options.reset_stats()

Integration Examples

Complete Pygame Example

import pygame
import pymunk
import pymunk.pygame_util
import math

# Initialize Pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Physics Debug Visualization")
clock = pygame.time.Clock()

# Create physics world
space = pymunk.Space()
space.gravity = (0, 981)  # Downward gravity for Y-down coordinate system

# Create ground
ground_body = space.static_body
ground = pymunk.Segment(ground_body, (0, 550), (800, 550), 5)
ground.friction = 0.7
space.add(ground)

# Create falling objects
def create_ball(x, y):
    mass = 10
    radius = 20
    moment = pymunk.moment_for_circle(mass, 0, radius)
    body = pymunk.Body(mass, moment)
    body.position = x, y
    shape = pymunk.Circle(body, radius)
    shape.friction = 0.7
    shape.elasticity = 0.8
    shape.color = pygame.Color("red")  # Custom color
    space.add(body, shape)

def create_box(x, y):
    mass = 15
    size = (40, 40)
    moment = pymunk.moment_for_box(mass, size)
    body = pymunk.Body(mass, moment)
    body.position = x, y
    shape = pymunk.Poly.create_box(body, size)
    shape.friction = 0.7
    shape.elasticity = 0.3
    shape.color = pygame.Color("blue")
    space.add(body, shape)

# Create debug drawing options
draw_options = pymunk.pygame_util.DrawOptions(screen)
draw_options.flags = (
    pymunk.SpaceDebugDrawOptions.DRAW_SHAPES |
    pymunk.SpaceDebugDrawOptions.DRAW_CONSTRAINTS
)  # Don't draw collision points for cleaner view

# Main game loop
running = True
while running:
    dt = clock.tick(60) / 1000.0
    
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_pos = pygame.mouse.get_pos()
            if event.button == 1:  # Left click - ball
                create_ball(*mouse_pos)
            elif event.button == 3:  # Right click - box  
                create_box(*mouse_pos)
    
    # Step physics
    space.step(dt)
    
    # Clear screen
    screen.fill((255, 255, 255))
    
    # Draw physics debug visualization
    space.debug_draw(draw_options)
    
    # Draw UI
    font = pygame.font.Font(None, 36)
    text = font.render("Left click: Ball, Right click: Box", True, (0, 0, 0))
    screen.blit(text, (10, 10))
    
    pygame.display.flip()

pygame.quit()

Multi-View Debugging

import pygame  
import pymunk
import pymunk.pygame_util

# Create multiple views for different debug information
screen = pygame.display.set_mode((1200, 600))

# Create subsurfaces for different views
main_view = screen.subsurface((0, 0, 800, 600))
detail_view = screen.subsurface((800, 0, 400, 300))
stats_view = screen.subsurface((800, 300, 400, 300))

# Create different draw options for each view
main_draw = pymunk.pygame_util.DrawOptions(main_view)
main_draw.flags = pymunk.SpaceDebugDrawOptions.DRAW_SHAPES

detail_draw = pymunk.pygame_util.DrawOptions(detail_view)  
detail_draw.flags = (
    pymunk.SpaceDebugDrawOptions.DRAW_SHAPES |
    pymunk.SpaceDebugDrawOptions.DRAW_CONSTRAINTS |
    pymunk.SpaceDebugDrawOptions.DRAW_COLLISION_POINTS
)

# Different zoom levels
main_draw.transform = pymunk.Transform.identity()
detail_draw.transform = pymunk.Transform.scaling(2.0)  # 2x zoom

# In main loop:
main_view.fill((255, 255, 255))
detail_view.fill((240, 240, 240))

space.debug_draw(main_draw)
space.debug_draw(detail_draw)

# Draw stats on stats_view
# ... statistics rendering code ...

Pymunk's visualization utilities provide powerful debugging capabilities across multiple graphics libraries, enabling rapid prototyping, debugging, and analysis of physics simulations with minimal setup and maximum flexibility.

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