# debug_draw.py
from ._box2d import ffi, lib
from .math import Vec2, Rot, Transform, AABB, Mat22
[docs]
class Color:
def __init__(self, r: int, g: int, b: int, a: int = 0xFF):
self.r, self.g, self.b, self.a = r, g, b, a
self.hexcolor = None
[docs]
@classmethod
def from_b2HexColor(cls, hex_color: int):
r = (hex_color >> 16) & 0xFF
g = (hex_color >> 8) & 0xFF
b = hex_color & 0xFF
a = 0xFF # Alpha not part of b2HexColor, default opaque
color = cls(r, g, b, a)
return color
@property
def b2HexColor(self) -> int:
hexcolor = (self.r << 16) | (self.g << 8) | self.b
return hexcolor
hex = b2HexColor
@property
def as_float(self):
"""Return the RGBA color as a tuple of floats scaled 0 to 1."""
return (self.r / 255, self.g / 255, self.b / 255, self.a / 255)
[docs]
def changed(self, r: int = None, g: int = None, b: int = None, a: int = None):
"""
Return a new Color instance with the specified components changed.
Args:
r: New red component (0-255). If None, retains current value.
g: New green component (0-255). If None, retains current value.
b: New blue component (0-255). If None, retains current value.
a: New alpha component (0-255). If None, retains current value.
Returns:
A new instance of Color with updated color components.
"""
new_r = r if r is not None else self.r
new_g = g if g is not None else self.g
new_b = b if b is not None else self.b
new_a = a if a is not None else self.a
new_color = Color(new_r, new_g, new_b, new_a)
return new_color
def __iter__(self):
return iter((self.r, self.g, self.b, self.a))
def __repr__(self) -> str:
return f"Color(r={self.r}, g={self.g}, b={self.b}, a={self.a})"
def __eq__(self, other):
"""Check equality with another Color instance.
Two Color objects are considered equal if their red, green, blue,
and alpha components are all equal.
"""
if not isinstance(other, Color):
return NotImplemented
return (
self.r == other.r
and self.g == other.g
and self.b == other.b
and self.a == other.a
)
def __hash__(self):
"""Return the hash based on the color's RGBA components."""
return hash((self.r, self.g, self.b, self.a))
# Define callback wrappers with cffi.callback and conversion logic
@ffi.callback("void(b2Vec2*, int, b2HexColor, void*)")
def draw_polygon(vertices, count, color, context):
instance = ffi.from_handle(context)
py_vertices = [Vec2.from_b2Vec2(vertices[i]) for i in range(count)]
instance.draw_polygon(py_vertices, Color.from_b2HexColor(color))
@ffi.callback("void(b2Transform, b2Vec2*, int, float, b2HexColor, void*)")
def draw_solid_polygon(transform, vertices, count, radius, color, context):
instance = ffi.from_handle(context)
py_transform = Transform.from_b2Transform(transform)
py_vertices = [Vec2.from_b2Vec2(vertices[i]) for i in range(count)]
instance.draw_solid_polygon(
py_transform, py_vertices, radius, Color.from_b2HexColor(color)
)
@ffi.callback("void(b2Vec2, float, b2HexColor, void*)")
def draw_circle(center, radius, color, context):
instance = ffi.from_handle(context)
py_center = Vec2.from_b2Vec2(center)
instance.draw_circle(py_center, radius, Color.from_b2HexColor(color))
@ffi.callback("void(b2Vec2, b2Vec2, b2HexColor, void*)")
def draw_segment(p1, p2, color, context):
instance = ffi.from_handle(context)
py_p1 = Vec2.from_b2Vec2(p1)
py_p2 = Vec2.from_b2Vec2(p2)
instance.draw_segment(py_p1, py_p2, Color.from_b2HexColor(color))
@ffi.callback("void(b2Vec2, float, b2HexColor, void*)")
def draw_point(p, size, color, context):
instance = ffi.from_handle(context)
py_p = Vec2.from_b2Vec2(p)
instance.draw_point(py_p, size, Color.from_b2HexColor(color))
@ffi.callback("void(b2Vec2, const char*, b2HexColor, void*)")
def draw_string(p, s, color, context):
instance = ffi.from_handle(context)
py_p = Vec2.from_b2Vec2(p)
py_str = ffi.string(s).decode("utf-8")
instance.draw_string(py_p, py_str, Color.from_b2HexColor(color))
@ffi.callback("void(b2Vec2, b2Vec2, float, b2HexColor, void*)")
def draw_solid_capsule(p1, p2, radius, color, context):
instance = ffi.from_handle(context)
py_p1 = Vec2.from_b2Vec2(p1)
py_p2 = Vec2.from_b2Vec2(p2)
instance.draw_solid_capsule(py_p1, py_p2, radius, Color.from_b2HexColor(color))
@ffi.callback("void(b2Transform, float, b2HexColor, void*)")
def draw_solid_circle(transform, radius, color, context):
instance = ffi.from_handle(context)
py_transform = Transform.from_b2Transform(transform)
instance.draw_solid_circle(py_transform, radius, Color.from_b2HexColor(color))
@ffi.callback("void(b2Transform, void*)")
def draw_transform(transform, context):
instance = ffi.from_handle(context)
py_transform = Transform.from_b2Transform(transform)
instance.draw_transform(py_transform)
[docs]
class DebugDraw:
"""Abstract base class for custom debug rendering of Box2D simulations.
Subclass this and override methods to implement debug visualization of:
- Shape outlines and solids
- Joints, AABBs, contact points
- Physics metrics like mass centers and impulses
Set boolean flags (draw_shapes, draw_aabbs etc.) to control which elements are rendered.
Uses Box2D's b2DebugDraw callbacks internally.
Example:
class MyDebugDraw(DebugDraw):
def _draw_polygon(self, vertices, color):
# Implement polygon drawing with your graphics API
"""
def __init__(self):
# Create a C b2DebugDraw instance
self._debug_draw = lib.b2DefaultDebugDraw()
# Assign context handle to retrieve instance in callbacks
self._debug_draw.context = ffi.new_handle(self)
# Assign decorated callbacks
self._debug_draw.DrawPolygon = draw_polygon
self._debug_draw.DrawSolidPolygon = draw_solid_polygon
self._debug_draw.DrawCircle = draw_circle
self._debug_draw.DrawSegment = draw_segment
self._debug_draw.DrawPoint = draw_point
self._debug_draw.DrawString = draw_string
self._debug_draw.DrawSolidCapsule = draw_solid_capsule
self._debug_draw.DrawSolidCircle = draw_solid_circle
self._debug_draw.DrawTransform = draw_transform
# Store a handle to this Python object for context
self._context_handle = ffi.new_handle(self)
self._debug_draw.context = self._context_handle
@property
def draw_shapes(self):
return bool(self._debug_draw.drawShapes)
@draw_shapes.setter
def draw_shapes(self, value: bool):
self._debug_draw.drawShapes = bool(value)
@property
def draw_aabbs(self):
return bool(self._debug_draw.drawAABBs)
@draw_aabbs.setter
def draw_aabbs(self, value: bool):
self._debug_draw.drawAABBs = bool(value)
@property
def draw_joints(self):
return bool(self._debug_draw.drawJoints)
@draw_joints.setter
def draw_joints(self, value: bool):
self._debug_draw.drawJoints = bool(value)
@property
def draw_contacts(self):
return bool(self._debug_draw.drawContacts)
@draw_contacts.setter
def draw_contacts(self, value: bool):
self._debug_draw.drawContacts = bool(value)
@property
def draw_contact_normals(self):
return bool(self._debug_draw.drawContactNormals)
@draw_contact_normals.setter
def draw_contact_normals(self, value: bool):
self._debug_draw.drawContactNormals = bool(value)
@property
def draw_contact_impulses(self):
return bool(self._debug_draw.drawContactImpulses)
@draw_contact_impulses.setter
def draw_contact_impulses(self, value: bool):
self._debug_draw.drawContactImpulses = bool(value)
@property
def draw_friction_impulses(self):
return bool(self._debug_draw.drawFrictionImpulses)
@draw_friction_impulses.setter
def draw_friction_impulses(self, value: bool):
self._debug_draw.drawFrictionImpulses = bool(value)
@property
def draw_mass(self):
return bool(self._debug_draw.drawMass)
@draw_mass.setter
def draw_mass(self, value: bool):
self._debug_draw.drawMass = bool(value)
@property
def draw_joint_extras(self):
return bool(self._debug_draw.drawJointExtras)
@draw_joint_extras.setter
def draw_joint_extras(self, value: bool):
self._debug_draw.drawJointExtras = bool(value)
# Internal callback handlers (override these in subclasses)
[docs]
def draw_polygon(self, vertices: list[Vec2], color: Color):
"""Draw wireframe polygon outlines (AABBs and shape outlines when draw_aabbs/shapes enabled).
Args:
vertices: Polygon vertex coordinates in Counter-Clockwise order
color: RGB color with alpha
"""
pass # Override in subclass
[docs]
def draw_solid_polygon(
self, transform: Transform, vertices: list[Vec2], radius: float, color: Color
):
"""Draw filled convex polygons with optional rounded corners (triggered by draw_shapes flag).
Args:
transform: Position and rotation of the polygon
vertices: Polygon vertices in CCW order
radius: Radius for rounded corners (0 for sharp edges)
color: Fill color with transparency
"""
pass
[docs]
def draw_circle(self, center: Vec2, radius: float, color: Color):
"""Callback for drawing circle outlines.
Args:
center: World position of circle center
radius: Radius in meters
color: RGB color of the outline
"""
pass
[docs]
def draw_segment(self, p1: Vec2, p2: Vec2, color: Color):
"""Draw line segments for joints/contact normals (requires draw_joints or draw_contact_normals).
Args:
p1: Starting point in world coordinates
p2: Ending point in world coordinates
color: Color of the line segment
"""
pass
[docs]
def draw_point(self, p: Vec2, size: float, color: Color):
"""Visualize contact points (draw_contacts) or mass centers (draw_mass).
Args:
position: World coordinates of the point
size: Diameter to render the point (screen pixels or meters)
color: RGB color of the point
Note:
Used for contact points when draw_contacts flag is True
"""
pass
[docs]
def draw_string(self, p: Vec2, s: str, color: Color):
"""Render debug text for impulse values (draw_contact_impulses/draw_friction_impulses).
Args:
p: World position where text should be anchored
s: Text string to display
color: Color of the text
Note:
Coordinate system depends on your renderer's text handling
"""
pass
[docs]
def draw_capsule(self, p1: Vec2, p2: Vec2, radius: float, color: Color):
"""Callback for drawing capsule outlines (line segment with radius).
Args:
p1: First endpoint of the capsule's centerline
p2: Second endpoint of the capsule's centerline
radius: Radius of the capsule (extends beyond endpoints)
color: Outline color
Note:
Used for character controllers or rounded collision shapes
"""
pass
[docs]
def draw_solid_capsule(self, p1: Vec2, p2: Vec2, radius: float, color: Color):
"""Draw filled capsule shapes (triggered by draw_shapes for capsule fixtures).
Args:
p1: First endpoint of the capsule's axis
p2: Second endpoint of the capsule's axis
radius: Radial thickness of the capsule
color: Fill color with transparency
Note:
Rendered as two half-circles connected by a rectangle
"""
pass
[docs]
def draw_solid_circle(self, transform: Transform, radius: float, color: Color):
"""Draw filled circles with orientation marker (used for circular fixtures when draw_shapes enabled).
Args:
transform: Center position and rotation (rotation affects orientation line)
radius: Circle radius in world units
color: Fill color with alpha channel
"""
pass