"""This module defines the basic functionality for collision, as well
as the base classes that compose an abstract interface to the library
developers choose to use.
Both :class:`Space` and :class:`Geom` (parent class of :class:`Ray`,
:class:`Trimesh`, :class:`Box`, :class:`Sphere`, :class:`Plane`, etc)
wrap the corresponding "native" object that the adapted library uses,
assigned to private attribute ``_inner_object``. To access (not set) it,
these classes have public property ``inner_object``.
This module also contains the auxiliary classes :class:`RayContactData` and
:class:`NearCallbackArgs`.
The following are common abbreviations present both in code and documentation:
* geom: geometry object
* trimesh: triangular mesh
"""
from abc import ABCMeta, abstractmethod
import logging
from ... import exceptions as exc
from ...lib.pydispatch import dispatcher
from . import signals
logger = logging.getLogger(__name__)
#==============================================================================
# Environment
#==============================================================================
[docs]class Engine(object):
"""Collision engine abstract base class.
"""
__metaclass__ = ABCMeta
@classmethod
[docs] def calc_collision(cls, geom1, geom2):
"""Calculate information of the collision between these geoms.
Check if ``geom1`` and ``geom2`` actually collide and
create a list of contact data objects if they do.
:param geom1:
:type geom1: type of :attr:`Geom.inner_object`
:param geom2:
:type geom2: type of :attr:`Geom.inner_object`
:return: contacts information
:rtype: list of contact data objects
"""
# Raising an exception efectively makes this definition be that of
# an abstract method (i.e. calling it directly raises an exception),
# except that it not requires the subclass to implement it if it is
# not used. We would like to use @classmethod AND @abstractmethod,
# but until Python 3.3 that doesn't work correctly.
# http://docs.python.org/3/library/abc.html
raise NotImplementedError()
@classmethod
[docs] def near_callback(cls, args, geom1, geom2):
"""Handle possible collision between ``geom1`` and ``geom2``.
The responsible for determining if there is an actual collision
is :meth:`calc_collision`, which will return a list of contact
data objects.
That information is passed to either
:meth:`process_collision_contacts` or
:meth:`process_ray_collision_contacts`, depending on whether
``geom1`` or ``geom2`` is a ray or not. It's an unhandled
case that both geoms were rays.
This function is usually the callback function for
:meth:`Space.collide`, although it will probably be handed over to
the inner object of a :class:`Space` subclass.
:param args: data structure wrapping the objects necessary to
process the collision
:type args: :class:`NearCallbackArgs`
:param geom1:
:type geom1: type of :attr:`Geom.inner_object`
:param geom2:
:type geom2: type of :attr:`Geom.inner_object`
"""
# Don't continue if the geoms are connected and
# that case should be ignored.
if args.ignore_connected and cls.are_geoms_connected(geom1, geom2):
return
# Ray's special case.
# Collision between a ray and another geom must be handled in a special
# way because no contact joints are to be generated, there is no
# "real" collision.
# 1. Check if any of both geoms is a ray.
# 2. If both are, that's an error.
# 3. If they are not, all is fine.
# 4. If one is a ray, assign it to a special variable so the special
# function is called with parameters in the correct order.
ray_geom = None
other_geom = None
if cls.is_ray(geom1):
if cls.is_ray(geom2):
logger.error("Weird, ODE says two rays may collide. "
"That case is not handled.") # TODO: better msg
return
ray_geom = geom1
other_geom = geom2
elif cls.is_ray(geom2):
ray_geom = geom2
other_geom = geom1
# calculate collision contacts data
contacts = cls.calc_collision(geom1, geom2)
if ray_geom is None:
cls.process_collision_contacts(args, geom1, geom2, contacts)
else:
cls.process_ray_collision_contacts(ray_geom, other_geom, contacts)
@classmethod
@classmethod
@classmethod
[docs] def are_geoms_connected(cls, geom1, geom2):
"""Return whether ``geom1``'s body is connected to ``geom2``'s body.
The *connection* is checked as whether geoms bodies are connected
through a joint or not.
:param geom1:
:type geom1: type of :attr:`Geom.inner_object`
:param geom2:
:type geom2: type of :attr:`Geom.inner_object`
:return: True if geoms' bodies are connected; False otherwise
:rtype: bool
"""
# Like :meth:`calc_collision`, this is an @abtractclassmethod too.
raise NotImplementedError()
@classmethod
[docs] def is_ray(cls, geom):
"""Return whether ``geom`` is a ray-like object or not.
:param geom:
:type geom: type of :attr:`Geom.inner_object`
:return: True if ``geom`` is an instance of the class representing a
ray in the adapted library
:rtype: bool
"""
# Like :meth:`calc_collision`, this is an @abtractclassmethod too.
raise NotImplementedError()
[docs]class Space(object):
"""Collision space abstract base class.
This class wraps the corresponding "native" object the
adapted-to library (e.g. ODE) uses, assigned to
:attr:`_inner_object`.
Subclasses must implement these methods:
* :meth:`__init__`
* :meth:`collide`
"""
__metaclass__ = ABCMeta
@abstractmethod
def __init__(self):
self._inner_object = None
@property
def inner_object(self):
return self._inner_object
@abstractmethod
[docs] def collide(self, args, callback):
pass
#==============================================================================
# Parents
#==============================================================================
[docs]class Geom(object):
"""Geometry object encapsulation.
This class wraps the corresponding "native" object the
adapted-to library (e.g. ODE) uses, assigned to
:attr:`_inner_object`.
Subclasses must implement these methods:
* :meth:`__init__`
* :meth:`attach_body`
* :meth:`get_position`, :meth:`set_position`
* :meth:`get_rotation`, :meth:`set_rotation`
"""
__metaclass__ = ABCMeta
@abstractmethod
def __init__(self):
self._inner_object = None
self._attached_body = None
@abstractmethod
[docs] def attach_body(self, body):
self._attached_body = body
#==========================================================================
# Getters and setters
#==========================================================================
@property
def inner_object(self):
return self._inner_object
[docs] def get_attached_body(self):
return self._attached_body
@abstractmethod
[docs] def get_position(self):
"""Get the position of the geom.
:return: position
:rtype: 3-sequence of floats
"""
pass
@abstractmethod
[docs] def get_rotation(self):
"""Get the orientation of the geom.
:return: rotation matrix
:rtype: 9-sequence of floats
"""
pass
@abstractmethod
[docs] def set_position(self, pos):
"""Set the position of the geom.
:param pos: position
:type pos: 3-sequence of floats
"""
pass
@abstractmethod
[docs] def set_rotation(self, rot):
"""Set the orientation of the geom.
:param rot: rotation matrix
:type rot: 9-sequence of floats
"""
pass
[docs]class BasicShape(Geom):
"""Abstract class from whom every solid object's shape derive"""
__metaclass__ = ABCMeta
#==============================================================================
# Other shapes
#==============================================================================
[docs]class Plane(BasicShape):
"""Plane, different from a box"""
__metaclass__ = ABCMeta
@abstractmethod
def __init__(self, space, normal, dist):
super(Plane, self).__init__()
[docs]class Ray(Geom):
"""
Ray aligned along the Z-axis by default.
"A ray is different from all the other geom classes in that it does not
represent a solid object. It is an infinitely thin line that starts from
the geom's position and extends in the direction of the geom's local
Z-axis." (ODE Wiki Manual)
"""
__metaclass__ = ABCMeta
@abstractmethod
def __init__(self, space, length):
super(Ray, self).__init__()
self._last_contact = None
self._closer_contact = None
@abstractmethod
[docs] def get_length(self):
pass
@abstractmethod
[docs] def set_length(self, length):
pass
[docs]class Trimesh(Geom):
"""A triangular mesh i.e. a surface composed of triangular faces.
.. note::
Note that a trimesh need not be closed. For example, it could be
used to model the ground surface.
Its geometry is defined by two attributes: :attr:`vertices` and
:attr:`faces`, both list of 3-tuple numbers. However, each tuple
in :attr:`vertices` designates a 3D point in space whereas each tuple
in :attr:`faces` is a group of indices referencing points in
:attr:`vertices`.
.. warning::
The order of vertices indices for each face **does** matter.
Example::
vertices = [(0, 0.0, 0), (0, 0.0, 1), (0, 0.0, 2), (0, 0.0, 3),
(1, 0.0, 0), (1, 0.0, 1), (1, 0.0, 2), (1, 0.0, 3)]
faces = [(0, 1, 4), (1, 5, 4), (1, 6, 5),
(1, 2, 6), (2, 3, 6), (3, 7, 6)]
The, the first face is defined by points:
``(0, 0.0, 0), (0, 0.0, 1), (1, 0.0, 0)``.
With that order, the normal to the face is ``(0, 1.0, 0)`` i.e. the Y axis.
The rationale to determining the *inwards* and *outwards* directions
follows the well-known "right hand rule".
"""
@abstractmethod
def __init__(self, space, vertices, faces):
super(Trimesh, self).__init__()
@staticmethod
[docs] def swap_faces_indices(faces):
"""Faces had to change their indices to work with ODE. With the initial
get_faces, the normal to the triangle defined by the 3 vertices pointed
(following the right-hand rule) downwards. Swapping the third with the
first index, now the triangle normal pointed upwards."""
new_faces = []
for face in faces:
new_faces.append((face[2], face[1], face[0]))
return new_faces
[docs]class HeightfieldTrimesh(Trimesh):
def __init__(self, space, size_x, size_z, vertices):
faces = self.calc_faces(size_x, size_z)
super(HeightfieldTrimesh, self).__init__(space, vertices, faces)
@staticmethod
[docs] def calc_faces(size_x, size_z):
"""Return the faces for a horizontal grid of ``size_x`` by ``size_z``
cells.
Faces are triangular, so each is composed by 3 vertices. Consequently,
each returned face is a length-3 sequence of the vertex indices.
:param size_x: number of cells along the X axis
:type size_x: positive int
:param size_z: number of cells along the Z axis
:type size_z: positive int
:return: faces for a heightfield trimesh based in a horizontal grid of
``size_x`` by ``size_z`` cells
:rtype: list of 3-tuple of ints
>>> HeightfieldTrimesh.calc_faces(2, 4)
[(0, 1, 4), (1, 5, 4), (1, 6, 5), (1, 2, 6), (2, 3, 6), (3, 7, 6)]
"""
# index of each square is calculated because it is needed to define faces
indices = []
for x in range(size_x):
indices_x = []
for z in range(size_z):
indices_x.insert(z, size_z * x + z)
indices.insert(x, indices_x)
# faces = [(1a,1b,1c), (2a,2b,2c), ...]
faces = []
for x in range(size_x - 1):
for z in range(size_z - 1):
zero = indices[x][z] # top-left corner
one = indices[x][z + 1] # bottom-left
two = indices[x + 1][z] # top-right
three = indices[x + 1][z + 1] # bottom-right
# there are two face types for each square
# contiguous squares must have different face types
face_type = zero
if size_z % 2 == 0:
face_type += 1
# there are 2 faces per square
if face_type % 2 == 0:
face1 = (zero, three, two)
face2 = (zero, one, three)
else:
face1 = (zero, one, two)
face2 = (one, three, two)
faces.append(face1)
faces.append(face2)
return faces
[docs]class ConstantHeightfieldTrimesh(HeightfieldTrimesh):
"""A trimesh that is a heightfield at constant level.
.. note::
More than anything, this geom is for demonstration purposes,
because it could be easily replaced with a :class:`Plane`.
"""
def __init__(self, space, size_x, size_z, height):
"""Constructor.
:param space:
:type space: :class:`Space`
:param size_x: number of cells along the X axis
:type size_x: positive int
:param size_z: number of cells along the Z axis
:type size_z: positive int
:param height:
:type height: float
"""
vertices = self.calc_vertices(size_x, size_z, height)
super(ConstantHeightfieldTrimesh, self).__init__(
space, size_x, size_z, vertices)
@staticmethod
[docs] def calc_vertices(size_x, size_z, height=0.0):
"""Return the vertices of a horizontal grid of ``size_x`` by ``size_z``
cells at a certain ``height``.
:param size_x: number of cells along the X axis
:type size_x: positive int
:param size_z: number of cells along the Z axis
:type size_z: positive int
:param height:
:type height: float
>>> ConstantHeightfieldTrimesh.calc_vertices(2, 4)
[(0, 0.0, 0), (0, 0.0, 1), (0, 0.0, 2), ..., (1, 0.0, 3)]
"""
verts = []
for x in range(size_x):
for z in range(size_z):
verts.append((x, height, z))
return verts
#==============================================================================
# Basic Shapes
#==============================================================================
[docs]class Box(BasicShape):
"""Box shape, aligned along the X, Y and Z axii by default"""
__metaclass__ = ABCMeta
@abstractmethod
def __init__(self, space, size):
super(Box, self).__init__()
[docs]class Sphere(BasicShape):
"""Spherical shape"""
__metaclass__ = ABCMeta
@abstractmethod
def __init__(self, space, radius):
super(Sphere, self).__init__()
[docs]class Capsule(BasicShape):
"""Capsule shape, aligned along the Z-axis by default"""
__metaclass__ = ABCMeta
@abstractmethod
def __init__(self, space, length, radius):
super(Capsule, self).__init__()
[docs]class Cylinder(BasicShape):
"""Cylinder shape, aligned along the Z-axis by default"""
__metaclass__ = ABCMeta
@abstractmethod
def __init__(self, space, length, radius):
super(Cylinder, self).__init__()
[docs]class Cone(BasicShape):
"""Cone"""
__metaclass__ = ABCMeta
@abstractmethod
def __init__(self):
super(Cone, self).__init__()
#==============================================================================
# aux classes
#==============================================================================
[docs]class NearCallbackArgs(object):
"""Data structure to save the args passed to
:meth:`Engine.near_callback`.
All attributes are read-only (set at initialization).
"""
def __init__(self, world=None, contact_group=None, ignore_connected=True):
"""Constructor.
:param world:
:type world: :class:`.physics.base.World`
:param contact_group:
:type contact_group: :class:`ContactGroup`
:param ignore_connected: whether to ignore collisions of geoms
whose bodies are connected, or not
:type ignore_connected: bool
"""
self._world = world
self._contact_group = contact_group
self._ignore_connected = ignore_connected
@property
def world(self):
return self._world
@property
def contact_group(self):
return self._contact_group
@property
def ignore_connected(self):
return self._ignore_connected