Skip to content

Commit

Permalink
Added simple intersection tests with polygons using naive SAT impleme…
Browse files Browse the repository at this point in the history
…ntation
  • Loading branch information
taras-doba-ua committed May 1, 2015
1 parent 771aae8 commit afe42c4
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 12 deletions.
5 changes: 3 additions & 2 deletions gengine/collision/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .shapes import Circle, BoundingBox
from .shapes import Circle, BoundingBox, Polygon
from .intersection import intersects
from .containment import contains
from .quadtree import QuadTree
Expand All @@ -9,5 +9,6 @@
"BoundingBox",
"intersects",
"contains",
"QuadTree"
"QuadTree",
"Polygon"
]
24 changes: 22 additions & 2 deletions gengine/collision/containment.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from planar import Vec2

from .shapes import Circle, BoundingBox
from .shapes import Circle, BoundingBox, Polygon


def bbox_contains_circle(bbox, circle):
Expand Down Expand Up @@ -54,11 +54,31 @@ def bbox_contains_bbox(bbox, other):
)


def polygon_contains_bbox(polygon, bbox):
pol2 = bbox.to_polygon()
return polygon_contains_polygon(polygon, pol2)


def bbox_contains_polygon(bbox, polygon):
pol2 = bbox.to_polygon()
return polygon_contains_polygon(pol2, polygon)


def polygon_contains_polygon(polygon, other):
for point in other:
if not polygon.contains_point(point):
return False
return True


_registry = {
(Circle, BoundingBox): circle_contains_bbox,
(BoundingBox, Circle): bbox_contains_circle,
(Circle, Circle): circle_contains_circle,
(BoundingBox, BoundingBox): bbox_contains_bbox
(BoundingBox, BoundingBox): bbox_contains_bbox,
(Polygon, BoundingBox): polygon_contains_bbox,
(BoundingBox, Polygon): bbox_contains_polygon,
(Polygon, Polygon): polygon_contains_polygon,
}


Expand Down
96 changes: 94 additions & 2 deletions gengine/collision/intersection.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import functools
import itertools as it
from planar import Vec2

from .shapes import Circle, BoundingBox
from .shapes import Circle, BoundingBox, Polygon


def inverse(func):
Expand All @@ -11,6 +12,94 @@ def do(left, right, **kw):
return do


def moving_circle_to_circle(mcircle, other, border=False):
d = mcircle.seg.distance_to(other.center)
if border:
return d <= (other.radius + mcircle.radius)
else:
return d < (other.radius + mcircle.radius)


def moving_circle_to_bbox(mcircle, bbox):
p1, p2 = mcircle.seg.points()
r = mcircle.radius
n = mcircle.seg.normal
# We will check 3 parts - inital and ending positions and shaft between
c1 = Circle(p1, r)
c2 = Circle(p2, r)
pol = Polygon([
p1 + n * r,
p1 - n * r,
p2 - n * r,
p2 + n * r],
is_convex=True)
return (
intersects(c1, bbox) or
intersects(c2, bbox) or
intersects(pol, bbox))


def moving_circle_to_moving_circle(mcircle, other, border=False):
# Find Closest point of approach (CPA)
# http://geomalgorithms.com/a07-_distance.html
dv = mcircle.velocity - other.velocity
# If velocities are equal just assume parallel lines
if dv.is_null:
cpa_time = 0
else:
dv2 = dv.dot(dv)
w0 = mcircle.center - other.center
cpa_time = - w0.dot(dv) / dv2

if cpa_time <= 0:
# We had closes point earlier of directly at start
pass # FIXME
elif cpa_time > mcircle.dt and cpa_time > other.dt:
pass # FIXME
else:
c1 = mcircle.at(cpa_time)
c2 = other.at(cpa_time)
d = c1.center.distance_to(c2.center)
r_sum = c1.radius + c2.radius
if d > r_sum:
return False
if d < r_sum:
return True
return border


def polygon_to_bbox(polygon, bbox, border=False):
pol2 = bbox.to_polygon()
return polygon_to_polygon(polygon, pol2, border=border)


def polygon_to_polygon(polygon, other, border=False):
# FIXME: We can skip projecting the owner of edge, cause we can know
# exactly where is the maximum point (0, for this normal)

# SAT or Separating Axis Theorem.
# If we have a plane, on which projections of shapes do not intersect, than
# objects don't intersect
_seen = set([])
for edge in it.chain(polygon.iter_edges(), other.iter_edges()):
# Ignore projections on the same normal vectors.
# Useful for enhancing paralelogram tests
edge_normal = edge.normal
if edge_normal in _seen:
continue
_seen.add(edge.normal)
# We project on 1d plane so only 1 coordinate
min_x, max_x = polygon.project(edge_normal)
other_min, other_max = other.project(edge_normal)
if border:
if min_x > other_max or max_x < other_min:
return False
else:
if min_x >= other_max or max_x <= other_min:
return False
return True


def circle_to_bbox(circle, bbox, border=False):
c_center = circle.center
c_radius = circle.radius
Expand Down Expand Up @@ -106,7 +195,10 @@ def bbox_to_bbox(bbox, other, border=False):
(Circle, BoundingBox): circle_to_bbox,
(BoundingBox, Circle): inverse(circle_to_bbox),
(Circle, Circle): circle_to_circle,
(BoundingBox, BoundingBox): bbox_to_bbox
(BoundingBox, BoundingBox): bbox_to_bbox,
(Polygon, BoundingBox): polygon_to_bbox,
(BoundingBox, Polygon): inverse(polygon_to_bbox),
(Polygon, Polygon): polygon_to_polygon
}


Expand Down
34 changes: 31 additions & 3 deletions gengine/collision/moving_shapes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .shapes import Shape
from .shapes import Shape, LineSegment
from gengine.util import lazy_property


class MovingShape(Shape):
Expand All @@ -7,5 +8,32 @@ class MovingShape(Shape):
pass


class MovingCircle(Shape):
pass
class MovingCircle(MovingShape):

def __init__(self, seg, radius, timestamp, dt):
self.seg = seg
self.radius = radius
self.timestamp = timestamp
self.dt = dt

@classmethod
def from_velocity(cls, initial_circle, velocity, timestamp, dt):
initial = initial_circle.center
radius = initial_circle.radius
seg = LineSegment.from_points(initial, velocity * dt)
return cls(seg, radius, timestamp, dt)

@lazy_property
def bounding_box(self):
return self.seg.bounding_box.inflate(self.radius*2)

def contains_point(self, point):
d = self.seg.distance_to(point)
return d <= self.radius

def __repr__(self):
"""Precise string representation."""
return "MovingCircle(%s, %s, %s, %s, %s)" % (
self.initial, self.ending, self.radius, self.timestamp, self.dt)

__str__ = __repr__
42 changes: 41 additions & 1 deletion gengine/collision/shapes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import abc
from gengine.utils import lazy_property
from planar import BoundingBox as OriginalBBox, Vec2
from planar.line import LineSegment as OriginalLineSegment
from planar.polygon import Polygon as OriginalPolygon # Segfault on C impl =(.


__all__ = [
"Shape",
"LineSegment",
"BoundingBox",
"Circle"
]


class Shape:
Expand All @@ -24,7 +34,13 @@ def contains_point(self, point):


class BoundingBox(OriginalBBox, Shape):
pass

def to_polygon(self):
min_bbox, max_bbox = self.min_point, self.max_point
return Polygon([
min_bbox, (min_bbox.x, max_bbox.y),
max_bbox, (max_bbox.x, min_bbox.y)],
is_convex=True)


class Circle(Shape):
Expand All @@ -50,3 +66,27 @@ def __repr__(self):
self.center.x, self.center.y, self.radius)

__str__ = __repr__


class LineSegment(OriginalLineSegment, Shape):

@lazy_property
def bounding_box(self):
return BoundingBox(self.points)


class Polygon(OriginalPolygon, Shape):

def iter_edges(self):
for i in range(len(self)):
yield LineSegment.from_points([self[i], self[i - 1]])

def project(self, vector):
min_point = max_point = vector.dot(self[0])
for vertice in self:
x = vector.dot(vertice)
if x < min_point:
min_point = x
if x > max_point:
max_point = x
return min_point, max_point
79 changes: 77 additions & 2 deletions tests/collision/test_intersections.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from unittest import TestCase, skip

from planar import Vec2
from gengine.collision import Circle, BoundingBox, intersects, contains
from gengine.collision import Circle, BoundingBox, intersects, contains, \
Polygon


class TestBBoxToCircle(TestCase):
Expand Down Expand Up @@ -47,7 +48,7 @@ def test_corner_intersection(self):
self.assertFalse(intersects(c, r))
self.assertTrue(intersects(c, r, border=True))

@skip("contains_point does not work on 0-size polygon")
@skip("contains_point does not work on 0-size bbox")
def test_circle_in_bbox_border(self):
# contains have no need for `border` attributes. It always includes
# border points.
Expand Down Expand Up @@ -136,3 +137,77 @@ def test_border_intersection(self):
self.assertTrue(intersects(c1, c2, border=True))
self.assertFalse(contains(c1, c2))
self.assertFalse(contains(c2, c1))


class TestPolygonBBox(TestCase):

def test_no_intersection(self):
bbox = BoundingBox([Vec2(0, 0), Vec2(2, 2)])
pol = Polygon([Vec2(5, 5), Vec2(3, 4), Vec2(7, 5)])
self.assertFalse(intersects(bbox, pol))
self.assertFalse(contains(bbox, pol))
self.assertFalse(contains(pol, bbox))

def test_pol_in_bbox(self):
bbox = BoundingBox([Vec2(0, 0), Vec2(5, 5)])
pol = Polygon([Vec2(3, 4), Vec2(2, 1), Vec2(3, 2)])
self.assertTrue(intersects(bbox, pol))
self.assertTrue(contains(bbox, pol))
self.assertFalse(contains(pol, bbox))

def test_bbox_in_pol(self):
bbox = BoundingBox([Vec2(0, 0), Vec2(2, 2)])
pol = Polygon([
Vec2(-1, -1), Vec2(-1, 5), Vec2(5, 5), Vec2(7, 4), Vec2(3, -1)])
self.assertTrue(intersects(bbox, pol))
self.assertTrue(contains(pol, bbox))
self.assertFalse(contains(bbox, pol))

def test_intersection(self):
bbox = BoundingBox([Vec2(0, 0), Vec2(2, 2)])
pol = Polygon([
Vec2(1, 1), Vec2(3, 1), Vec2(1, 3)])
self.assertTrue(intersects(bbox, pol))
self.assertFalse(contains(pol, bbox))
self.assertFalse(contains(bbox, pol))

def test_border_intersection(self):
bbox = BoundingBox([Vec2(0, 0), Vec2(2, 2)])
pol = Polygon([
Vec2(0, 0), Vec2(1, 0), Vec2(1, -2)])
self.assertFalse(intersects(bbox, pol))
self.assertTrue(intersects(bbox, pol, border=True))
self.assertFalse(contains(pol, bbox))
self.assertFalse(contains(bbox, pol))


class TestPolygonPolygon(TestCase):

def test_no_intersection(self):
pol1 = Polygon([Vec2(0, 0), Vec2(0, 2), Vec2(2, 2)])
pol2 = Polygon([Vec2(5, 5), Vec2(3, 4), Vec2(7, 5)])
self.assertFalse(intersects(pol1, pol2))
self.assertFalse(contains(pol1, pol2))
self.assertFalse(contains(pol1, pol2))

def test_contains(self):
pol1 = Polygon([Vec2(0, 0), Vec2(0, 10), Vec2(7, 8), Vec2(4, 0)])
pol2 = Polygon([Vec2(2, 2), Vec2(1, 2), Vec2(2, 1)])
self.assertTrue(intersects(pol1, pol2))
self.assertTrue(contains(pol1, pol2))
self.assertFalse(contains(pol2, pol1))

def test_intersection(self):
pol1 = Polygon([Vec2(0, 0), Vec2(0, 2), Vec2(2, 2)])
pol2 = Polygon([Vec2(2, 2), Vec2(2, 1), Vec2(1, 2)])
self.assertTrue(intersects(pol1, pol2))
self.assertFalse(contains(pol1, pol2))
self.assertFalse(contains(pol1, pol2))

def test_border_intersection(self):
pol1 = Polygon([Vec2(0, 0), Vec2(0, 2), Vec2(2, 2)])
pol2 = Polygon([Vec2(2, 2), Vec2(1, 1), Vec2(2, 0)])
self.assertFalse(intersects(pol1, pol2))
self.assertTrue(intersects(pol1, pol2, border=True))
self.assertFalse(contains(pol1, pol2))
self.assertFalse(contains(pol1, pol2))

0 comments on commit afe42c4

Please sign in to comment.