Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Polygon collideswith() #224

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ other objects.

collidecircle: Checks if the polygon collides with the given circle.

collideswith: Checks if the polygon collides with the given object.

insert_vertex: Adds a vertex to the polygon.

remove_vertex: Removes a vertex from the polygon.
Expand Down
26 changes: 26 additions & 0 deletions docs/polygon.rst
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,32 @@ Polygon Methods

.. ## Polygon.collidecircle ##

.. method:: collideswith

| :sl:`test if a shape or point and the polygon collide`
| :sg:`collideswith(Line, only_edges=False) -> bool`
| :sg:`collideswith(Circle, only_edges=False) -> bool`
| :sg:`collideswith((x, y), only_edges=False) -> bool`
| :sg:`contains(Vector2, only_edges=False) -> bool`

The `collideswith` method tests whether a given shape or point collides (overlaps)
with this `Polygon` object. The function takes in a single argument, which can be a
`Line`, `Circle`, tuple or list containing the x and y coordinates of a point,
or a `Vector2` object. The function returns a boolean value of `True`
if there is any overlap between the shape or point and the `Circle` object, or
`False` if there is no overlap.

.. note::
It is important to note that the shape must be an actual shape object, such as
a `Line`, or `Circle` instance. It is not possible to pass a tuple
or list of coordinates representing the shape as an argument(except for a point),
because the type of shape represented by the coordinates cannot be determined.
For example, a tuple with the format (a, b, c, d) could represent either a `Line`
or a Rect object, and there is no way to determine which is which without explicitly
passing a `Line` or `Rect` object as an argument.

.. ## Polygon.collideswith ##

.. method:: as_segments

| :sl:`returns the line segments of the polygon`
Expand Down
1 change: 1 addition & 0 deletions geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ class Polygon:
def collidecircle(self, polygon: CircleValue, only_edges: bool = False) -> bool: ...
@overload
def collidecircle(self, *circle, only_edges: bool = False) -> bool: ...
def collideswith(self, other: _CanBeCollided, only_edges: bool = False) -> bool: ...
def insert_vertex(self, index: int, vertex: Coordinate) -> None: ...
def remove_vertex(self, index: int) -> None: ...
def pop_vertex(self, index: int) -> Coordinate: ...
Expand Down
52 changes: 52 additions & 0 deletions src_c/polygon.c
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,56 @@ pg_polygon_collidecircle(pgPolygonObject *self, PyObject *const *args,
pgCollision_CirclePolygon(&circle, &self->polygon, only_edges));
}

static int
_pg_polygon_collideswith(pgPolygonBase *poly, PyObject *other, int only_edges)
{
if (pgLine_Check(other)) {
return pgCollision_PolygonLine(poly, &pgLine_AsLine(other),
only_edges);
}
else if (pgCircle_Check(other)) {
return pgCollision_CirclePolygon(&pgCircle_AsCircle(other), poly,
only_edges);
}
else if (PySequence_Check(other)) {
double x, y;
if (!pg_TwoDoublesFromObj(other, &x, &y)) {
PyErr_SetString(
PyExc_TypeError,
"Invalid point argument, must be a sequence of 2 numbers");
return -1;
}
return pgCollision_PolygonPoint(poly, x, y);
}

PyErr_SetString(
PyExc_TypeError,
"Invalid shape argument, must be a CircleType, LineType, or a "
"sequence of 2 numbers");

return -1;
}

static PyObject *
pg_polygon_collideswith(pgPolygonObject *self, PyObject *const *args,
Py_ssize_t nargs)
{
if (!nargs || nargs > 2) {
return RAISE(PyExc_TypeError,
"collideswith requires 1 or 2 arguments");
}
int only_edges = 0;
if (nargs == 2) {
only_edges = PyObject_IsTrue(args[1]);
}

int result = _pg_polygon_collideswith(&self->polygon, args[0], only_edges);
if (result == -1) {
return NULL;
}
return PyBool_FromLong(result);
}

static struct PyMethodDef pg_polygon_methods[] = {
{"as_segments", (PyCFunction)pg_polygon_as_segments, METH_NOARGS, NULL},
{"move", (PyCFunction)pg_polygon_move, METH_FASTCALL, NULL},
Expand All @@ -1313,6 +1363,8 @@ static struct PyMethodDef pg_polygon_methods[] = {
{"collideline", (PyCFunction)pg_polygon_collideline, METH_FASTCALL, NULL},
{"collidecircle", (PyCFunction)pg_polygon_collidecircle, METH_FASTCALL,
NULL},
{"collideswith", (PyCFunction)pg_polygon_collideswith, METH_FASTCALL,
NULL},
{"as_rect", (PyCFunction)pg_polygon_as_rect, METH_NOARGS, NULL},
{"is_convex", (PyCFunction)pg_polygon_is_convex, METH_NOARGS, NULL},
{"__copy__", (PyCFunction)pg_polygon_copy, METH_NOARGS, NULL},
Expand Down
197 changes: 184 additions & 13 deletions test/test_polygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -2135,9 +2135,8 @@ def test_collideline_invalid_only_edges_param(self):
with self.assertRaises(TypeError):
poly.collideline(l, value)

def test_collidepolygon(self):
"""Ensures that the collidepolygon method correctly determines if a Polygon
is colliding with the Line"""
def test_collideline(self):
"""Ensures that the collideline method works correctly"""

l = Line(0, 0, 10, 10)
p1 = regular_polygon(4, l.center, 100)
Expand All @@ -2147,36 +2146,208 @@ def test_collidepolygon(self):
p5 = Polygon((0, 0), (0, 10), (-5, 10), (-5, 0))

# line inside polygon
self.assertTrue(l.collidepolygon(p1))
self.assertTrue(p1.collideline(l))

# line outside polygon
self.assertFalse(l.collidepolygon(p2))
self.assertFalse(p2.collideline(l))

# line intersects polygon edge
self.assertTrue(l.collidepolygon(p3))
self.assertTrue(p3.collideline(l))

# line intersects polygon vertex
self.assertTrue(l.collidepolygon(p4))
self.assertTrue(p4.collideline(l))

# line touches polygon vertex
self.assertTrue(l.collidepolygon(p5))
self.assertTrue(p5.collideline(l))

# --- Edge only ---

# line inside polygon
self.assertFalse(l.collidepolygon(p1, True))
self.assertFalse(p1.collideline(l, True))

# line outside polygon
self.assertFalse(l.collidepolygon(p2, True))
self.assertFalse(p2.collideline(l, True))

# line intersects polygon edge
self.assertTrue(l.collidepolygon(p3, True))
self.assertTrue(p3.collideline(l, True))

# line intersects polygon vertex
self.assertTrue(l.collidepolygon(p4, True))
self.assertTrue(p4.collideline(l, True))

# line touches polygon vertex
self.assertTrue(l.collidepolygon(p5, True))
self.assertTrue(p5.collideline(l, True))

def test_collideswith_argtype(self):
"""Tests if the function correctly handles incorrect types as parameters"""

invalid_types = (
True,
False,
None,
[],
"1",
(1,),
1,
0,
-1,
1.23,
(1, 2, 3),
Vector3(10, 10, 4),
)

p = Polygon((0, 0), (0, 1), (1, 1), (1, 0))

for value in invalid_types:
with self.assertRaises(TypeError):
p.collideswith(value)
with self.assertRaises(TypeError):
p.collideswith(value, True)
with self.assertRaises(TypeError):
p.collideswith(value, False)

def test_collideswith_argnum(self):
"""Tests if the function correctly handles incorrect number of parameters"""
p = Polygon((-5, 0), (5, 0), (0, 5))
invalid_args = [
(p, p),
(p, p, p),
(p, p, p, p),
]

with self.assertRaises(TypeError):
p.collideswith()

for arg in invalid_args:
with self.assertRaises(TypeError):
p.collideswith(*arg)
with self.assertRaises(TypeError):
p.collideswith(*arg, True)
with self.assertRaises(TypeError):
p.collideswith(*arg, False)

def test_collideswith_return_type(self):
"""Tests if the function returns the correct type"""
p = Polygon((-5, 0), (5, 0), (0, 5))

objects = [
Line(0, 0, 1, 1),
Circle(10, 10, 4),
Vector2(10, 10),
(10, 10),
[10, 10],
]

for obj in objects:
self.assertIsInstance(p.collideswith(obj), bool)
self.assertIsInstance(p.collideswith(obj, True), bool)
self.assertIsInstance(p.collideswith(obj, False), bool)

def assert_PolygonEquals(self, expected: Polygon, actual: Polygon) -> None:
self.assertEqual(expected.vertices, actual.vertices)
self.assertEqual(expected.verts_num, actual.verts_num)
self.assertEqual(expected.centerx, actual.centerx)
self.assertEqual(expected.centery, actual.centery)

def test_collideswith(self):
"""Ensures that the collidepolygon method correctly determines if a Polygon
is colliding with the given object"""
epsilon = 1e-14
p = Polygon((0, 0), (0, 1), (1, 1), (1, 0))

# --- Circle ---
circles = [
(Circle(0.5, 0.5, 0.3), True), # inside
(Circle(0.5, 0.5, 0.5), True), # outside
(Circle(100, 100, 10), False), # not colliding far away
(Circle(1.5, 0.5, 0.5), True), # perfectly touching
(
Circle(1.5, 0.5, 0.49999999999999 - epsilon),
False,
), # barely not touching
(Circle(1.5, 0.5, 0.49999999999999 + epsilon), True), # barely touching
]

for circle, expected in circles:
# check for no invalidation
p_copy = p.copy()
circle_copy = circle.copy()
p.collideswith(circle)
self.assert_PolygonEquals(p_copy, p)

self.assertEqual(circle.x, circle_copy.x)
self.assertEqual(circle.y, circle_copy.y)
self.assertEqual(circle.r, circle_copy.r)

# check collision works as expected
self.assertEqual(expected, p.collideswith(circle))

self.assertFalse(p.collideswith(circles[0][0], True))
self.assertFalse(p.collideswith(Circle(50, 50, 150), True))

# --- Line ---

lines = [
(Line(0.1, 0.1, 0.9, 0.9), True), # inside
(Line(-1, -1, 2, 2), True), # outside intersecting
(Line(0, 0, 1, 0), True), # parallel touching
(Line(100, 100, 500, 100), False), # not colliding
(Line(2, 0.5, 1, 0.5), True), # perfectly touching
(Line(2, 0.5, 1.0 + epsilon, 0.5), False), # barely not touching
(Line(1.0 - epsilon, 0.5, 2, 0.5), True), # barely touching
]

for line, expected in lines:
# check for no invalidation
p_copy = p.copy()
line_copy = line.copy()
p.collideswith(line)
self.assert_PolygonEquals(p_copy, p)

self.assertEqual(line.a, line_copy.a)
self.assertEqual(line.b, line_copy.b)

# check collision works as expected
self.assertEqual(expected, p.collideswith(line))

self.assertFalse(p.collideswith(lines[0][0], True))

# --- Vector2, tuple and list ---

vectors = [
(Vector2(0.5, 0.5), True), # inside
(Vector2(100, 100), False), # outside
(Vector2(1 + epsilon, 0.5), False), # barely not touching
(Vector2(1 - epsilon, 0.5), True), # barely touching
]

points = [(tuple(v), expected) for v, expected in vectors]
points.extend((list(v), expected) for v, expected in vectors)

for vec, expected in vectors:
# check for no invalidation
p_copy = p.copy()
v_copy = vec.copy()
p.collideswith(vec)

self.assert_PolygonEquals(p_copy, p)

self.assertEqual(vec.x, v_copy.x)
self.assertEqual(vec.y, v_copy.y)

# check collision works as expected
self.assertEqual(expected, p.collideswith(vec))

for point, expected in points:
# check for no invalidation
p_copy = p.copy()
p.collideswith(point)

self.assert_PolygonEquals(p_copy, p)

# check collision works as expected
self.assertEqual(expected, p.collideswith(point))

# to be expanded with more objects when implemented


if __name__ == "__main__":
Expand Down