diff --git a/docs/geometry.rst b/docs/geometry.rst index 528f7bcb..e995a7bd 100644 --- a/docs/geometry.rst +++ b/docs/geometry.rst @@ -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. diff --git a/docs/polygon.rst b/docs/polygon.rst index bb37fd5a..eb607f05 100644 --- a/docs/polygon.rst +++ b/docs/polygon.rst @@ -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` diff --git a/geometry.pyi b/geometry.pyi index 02eca2d7..0066b7cd 100644 --- a/geometry.pyi +++ b/geometry.pyi @@ -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: ... diff --git a/src_c/polygon.c b/src_c/polygon.c index 1308ff68..5e96476e 100644 --- a/src_c/polygon.c +++ b/src_c/polygon.c @@ -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}, @@ -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}, diff --git a/test/test_polygon.py b/test/test_polygon.py index 398fea12..3a181ec3 100644 --- a/test/test_polygon.py +++ b/test/test_polygon.py @@ -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) @@ -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__":