"""
This module defines high-level shape objects for Box2D.
Each shape’s constructor now accepts a body and a shape definition.
Each shape (except Chain) subclasses a common base that implements common properties.
Each shape also has a classmethod “create” that matches the signature from before.
Chain is now implemented as a separate class.
"""
from ._box2d import lib, ffi
from abc import ABC
from .math import Vec2, Transform, VectorLike
from .shape_def import (
CircleDef,
CapsuleDef,
SegmentDef,
PolygonDef,
BoxDef,
ChainDef,
)
[docs]
class Shape(ABC):
"""
Base class for all non-chain shapes.
Its constructor now accepts only a body and a shape definition.
It provides common properties like density, friction, restitution, and a helper _finalize.
"""
def __init__(self, body, shapedef):
self._body = body
def _finalize(self):
"""
Finalize shape creation by setting the user data pointer.
"""
self._handle = ffi.new_handle(self)
lib.b2Shape_SetUserData(self._shape_id, self._handle)
@property
def density(self):
"""Get the mass density of the shape."""
return lib.b2Shape_GetDensity(self._shape_id)
@density.setter
def density(self, value):
"""Set the mass density of the shape (and update the body mass)."""
lib.b2Shape_SetDensity(self._shape_id, float(value), True)
@property
def friction(self):
"""Get the friction coefficient of the shape."""
return lib.b2Shape_GetFriction(self._shape_id)
@friction.setter
def friction(self, value):
"""Set the friction coefficient of the shape."""
lib.b2Shape_SetFriction(self._shape_id, float(value))
@property
def restitution(self):
"""Get the restitution (bounciness) of the shape."""
return lib.b2Shape_GetRestitution(self._shape_id)
@restitution.setter
def restitution(self, value):
"""Set the restitution (bounciness) of the shape."""
lib.b2Shape_SetRestitution(self._shape_id, float(value))
@property
def is_sensor(self):
"""Check if this shape is a sensor."""
return lib.b2Shape_IsSensor(self._shape_id)
@property
def body(self):
"""Return the body to which this shape is attached."""
return self._body
[docs]
class Circle(Shape):
"""
A circle shape that can be attached to a body.
"""
[docs]
def __init__(self, body, shapedef):
"""
Initialize a Circle shape from a body and a CircleDef instance.
"""
super().__init__(body, shapedef)
self._shape_id = lib.b2CreateCircleShape(
body._body_id, ffi.addressof(shapedef.shapedef), shapedef.circle
)
self._finalize()
[docs]
@classmethod
def create(
cls,
body,
radius,
center=(0, 0),
density=None,
friction=None,
restitution=None,
is_sensor=None,
collision_filter=None,
custom_color=None,
):
"""
Create and attach a circle shape to a body.
The parameters are the same as the previous initializer.
"""
shapedef = CircleDef(
radius,
center,
density,
friction,
restitution,
is_sensor,
collision_filter,
custom_color,
)
return cls(body, shapedef)
[docs]
class Capsule(Shape):
"""
A capsule shape that can be attached to a body.
"""
[docs]
def __init__(self, body, shapedef):
"""
Initialize a Capsule shape from a body and a CapsuleDef instance.
"""
super().__init__(body, shapedef)
self._shape_id = lib.b2CreateCapsuleShape(
body._body_id, ffi.addressof(shapedef.shapedef), shapedef.capsule
)
self._finalize()
[docs]
@classmethod
def create(
cls,
body,
point1,
point2,
radius,
density=None,
friction=None,
restitution=None,
is_sensor=None,
collision_filter=None,
custom_color=None,
):
"""
Create and attach a capsule shape to a body.
The parameters are the same as the previous initializer.
"""
shapedef = CapsuleDef(
point1,
point2,
radius,
density,
friction,
restitution,
is_sensor,
collision_filter,
custom_color=None,
)
return cls(body, shapedef)
[docs]
class Segment(Shape):
"""
A line segment shape that can be attached to a body.
"""
[docs]
def __init__(self, body, shapedef):
"""
Initialize a Segment shape from a body and a SegmentDef instance.
"""
super().__init__(body, shapedef)
self._shape_id = lib.b2CreateSegmentShape(
body._body_id, ffi.addressof(shapedef.shapedef), shapedef.segment
)
self._finalize()
[docs]
@classmethod
def create(
cls,
body,
point1,
point2,
density=None,
friction=None,
restitution=None,
is_sensor=None,
collision_filter=None,
custom_color=None,
):
"""
Create and attach a segment shape to a body.
The parameters are the same as the previous initializer.
"""
shapedef = SegmentDef(
point1,
point2,
density,
friction,
restitution,
is_sensor,
collision_filter,
custom_color,
)
return cls(body, shapedef)
[docs]
class Polygon(Shape):
"""
A convex polygon shape that can be attached to a body.
"""
[docs]
def __init__(self, body, shapedef):
"""
Initialize a Polygon shape from a body and a PolygonDef instance.
"""
super().__init__(body, shapedef)
# Note: shapedef.polygon is the geometry computed (via b2MakePolygon)
self._shape_id = lib.b2CreatePolygonShape(
body._body_id,
ffi.addressof(shapedef.shapedef),
ffi.addressof(shapedef.polygon),
)
self._finalize()
[docs]
@classmethod
def create(
cls,
body,
vertices,
radius=0.0,
density=None,
friction=None,
restitution=None,
is_sensor=None,
collision_filter=None,
custom_color=None,
):
"""
Create and attach a polygon shape to a body.
The parameters are the same as the previous initializer.
"""
shapedef = PolygonDef(
vertices,
radius,
density,
friction,
restitution,
is_sensor,
collision_filter,
custom_color,
)
return cls(body, shapedef)
[docs]
class Box(Polygon):
"""
A box (rectangle) shape that can be attached to a body.
Inherits from Polygon since a box is a special case of a convex polygon.
"""
[docs]
@classmethod
def create(
cls,
body,
width,
height,
radius=0.0,
offset=(0, 0),
angle=0.0,
density=None,
friction=None,
restitution=None,
is_sensor=None,
collision_filter=None,
custom_color=None,
):
"""
Create and attach a box shape to a body.
The parameters are the same as the previous initializer.
"""
shapedef = BoxDef(
width,
height,
offset,
radius,
angle,
density,
friction,
restitution,
is_sensor,
collision_filter,
custom_color,
)
return cls(body, shapedef)
class ChainSegment(Shape):
"""
A segment of a chain shape.
Chain segments are created automatically by the Chain class and represent
individual segments within a chain. Each segment has a reference to its parent chain.
"""
def __init__(self, b2chainsegment, chain: "Chain"):
"""
Initialize a chain segment with its parent chain and endpoints.
Parameters:
- b2chainsegment: The b2ChainSegment b2ShapeId
- chain: The parent Chain object
"""
self._shape_id = b2chainsegment
self.parent_chain = chain
super().__init__(chain.body, None)
self._finalize()
[docs]
class Chain:
"""
A chain shape that can be attached to a body.
Chain shapes are not a subclass of Shape and do not have the common shape methods
because they are typically used for static boundaries and have no density/sensor properties.
"""
def __init__(self, body, shapedef):
self._body = body
self._shape_id = lib.b2CreateChain(
body._body_id, ffi.addressof(shapedef.shapedef)
)
segment_count = lib.b2Chain_GetSegmentCount(self._shape_id)
segments = ffi.new("b2ShapeId[]", segment_count)
lib.b2Chain_GetSegments(self._shape_id, segments, segment_count)
self.segments = [ChainSegment(segments[i], self) for i in range(segment_count)]
[docs]
@classmethod
def create(
cls,
body,
vertices,
loop=False,
friction=None,
restitution=None,
collision_filter=None,
custom_color=None,
):
"""
Create and attach a chain shape to a body.
"""
shapedef = ChainDef(
vertices, loop, friction, restitution, collision_filter, custom_color
)
return cls(body, shapedef)
@property
def body(self):
"""Return the body this chain is attached to."""
return self._body