From dc78c4852fdfa2257e1329a14a64fde4e24df343 Mon Sep 17 00:00:00 2001 From: itzpr3d4t0r <103119829+itzpr3d4t0r@users.noreply.github.com> Date: Sat, 22 Jul 2023 16:14:05 +0200 Subject: [PATCH] added line collidelist/collidelistall --- docs/geometry.rst | 4 ++ docs/line.rst | 46 ++++++++++++ geometry.pyi | 2 + src_c/line.c | 173 ++++++++++++++++++++++++++++++++++++++++++---- test/test_line.py | 166 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 376 insertions(+), 15 deletions(-) diff --git a/docs/geometry.rst b/docs/geometry.rst index 528f7bcb..0b45f110 100644 --- a/docs/geometry.rst +++ b/docs/geometry.rst @@ -130,6 +130,10 @@ other objects. collideswith: Checks if the line collides with the given object. + collidelist: Checks if the line collides with any of the given objects. + + collidelistall: Checks if the line collides with all of the given objects. + as_circle: Returns a circle which fully encloses the line. as_rect: Returns the smallest rectangle that contains the line. diff --git a/docs/line.rst b/docs/line.rst index ac0f0270..1a07f3e1 100644 --- a/docs/line.rst +++ b/docs/line.rst @@ -371,6 +371,52 @@ Line Methods .. ## Line.collideswith ## + .. method:: collidelist + + | :sl:`test if a list of objects collide with the line` + | :sg:`collidelist(colliders) -> int` + + The `collidelist` method tests whether a given list of shapes or points collides + (overlaps) with this `Line` object. The function takes in a single argument, which + must be a list of `Line`, `Circle`, `Rect`, `Polygon`, tuple or list containing the + x and y coordinates of a point, or `Vector2` objects. The function returns the index + of the first shape or point in the list that collides with the `Line` object, or + -1 if there is no collision. + + .. note:: + It is important to note that the shapes must be actual shape objects, such as + `Line`, `Circle`, `Polygon`, or `Rect` instances. 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. + + .. ## Line.collidelist ## + + .. method:: collidelistall + + | :sl:`test if all objects in a list collide with the line` + | :sg:`collidelistall(colliders) -> list` + + The `collidelistall` method tests whether a given list of shapes or points collides + (overlaps) with this `Line` object. The function takes in a single argument, which + must be a list of `Line`, `Circle`, `Rect`, `Polygon`, tuple or list containing the + x and y coordinates of a point, or `Vector2` objects. The function returns a list + containing the indices of all the shapes or points in the list that collide with + the `Line` object, or an empty list if there is no collision. + + .. note:: + It is important to note that the shapes must be actual shape objects, such as + `Line`, `Circle`, `Polygon`, or `Rect` instances. 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. + + .. ## Line.collidelistall ## + .. method:: as_circle diff --git a/geometry.pyi b/geometry.pyi index 02eca2d7..1902009c 100644 --- a/geometry.pyi +++ b/geometry.pyi @@ -112,6 +112,8 @@ class Line(Sequence[float]): @overload def collidecircle(self, x: float, y: float, r: float) -> bool: ... def collidepolygon(self, polygon: Polygon, only_edges: bool = False) -> bool: ... + def collidelist(self, colliders: Sequence[_CanBeCollided]) -> int: ... + def collidelistall(self, colliders: Sequence[_CanBeCollided]) -> List[int]: ... def as_circle(self) -> Circle: ... def as_rect(self) -> Rect: ... @overload diff --git a/src_c/line.c b/src_c/line.c index bdc6d0b5..e746d85f 100644 --- a/src_c/line.c +++ b/src_c/line.c @@ -393,41 +393,182 @@ pg_line_is_perpendicular(pgLineObject *self, PyObject *const *args, return PyBool_FromLong(dot == 0); } -static PyObject * -pg_line_collideswith(pgLineObject *self, PyObject *arg) +static int +_pg_line_collideswith(pgLineBase *sline, PyObject *arg) { - int result = 0; if (pgLine_Check(arg)) { - result = pgCollision_LineLine(&self->line, &pgLine_AsLine(arg)); + return pgCollision_LineLine(sline, &pgLine_AsLine(arg)); } else if (pgRect_Check(arg)) { - result = pgCollision_RectLine(&pgRect_AsRect(arg), &self->line); + return pgCollision_RectLine(&pgRect_AsRect(arg), sline); } else if (pgCircle_Check(arg)) { - result = pgCollision_LineCircle(&self->line, &pgCircle_AsCircle(arg)); + return pgCollision_LineCircle(sline, &pgCircle_AsCircle(arg)); } else if (pgPolygon_Check(arg)) { - result = - pgCollision_PolygonLine(&pgPolygon_AsPolygon(arg), &self->line, 0); + return pgCollision_PolygonLine(&pgPolygon_AsPolygon(arg), sline, 0); } else if (PySequence_Check(arg)) { double x, y; if (!pg_TwoDoublesFromObj(arg, &x, &y)) { - return RAISE( + PyErr_SetString( PyExc_TypeError, "Invalid point argument, must be a sequence of 2 numbers"); + return -1; } - result = pgCollision_LinePoint(&self->line, x, y); - } - else { - return RAISE(PyExc_TypeError, - "Invalid shape argument, must be a CircleType, RectType, " - "LineType, PolygonType or a sequence of 2 numbers"); + return pgCollision_LinePoint(sline, x, y); } + PyErr_SetString(PyExc_TypeError, + "Invalid shape argument, must be a CircleType, RectType, " + "LineType, PolygonType or a sequence of 2 numbers"); + return -1; +} + +static PyObject * +pg_line_collideswith(pgLineObject *self, PyObject *arg) +{ + int result = _pg_line_collideswith(&self->line, arg); + if (result == -1) { + return NULL; + } return PyBool_FromLong(result); } +static PyObject * +pg_line_collidelist(pgLineObject *self, PyObject *arg) +{ + Py_ssize_t i; + pgLineBase *sline = &self->line; + int colliding; + + if (!PySequence_Check(arg)) { + return RAISE(PyExc_TypeError, "Argument must be a sequence"); + } + + /* fast path */ + if (PySequence_FAST_CHECK(arg)) { + PyObject **items = PySequence_Fast_ITEMS(arg); + for (i = 0; i < PySequence_Fast_GET_SIZE(arg); i++) { + if ((colliding = _pg_line_collideswith(sline, items[i])) == -1) { + /*invalid shape*/ + return NULL; + } + + if (colliding) { + return PyLong_FromSsize_t(i); + } + } + return PyLong_FromLong(-1); + } + + /* general sequence path */ + for (i = 0; i < PySequence_Length(arg); i++) { + PyObject *obj = PySequence_GetItem(arg, i); + if (!obj) { + return NULL; + } + + colliding = _pg_line_collideswith(sline, obj); + Py_DECREF(obj); + + if (colliding == 1) { + return PyLong_FromSsize_t(i); + } + else if (colliding == -1) { + return NULL; + } + } + + return PyLong_FromLong(-1); +} + +static PyObject * +pg_line_collidelistall(pgLineObject *self, PyObject *arg) +{ + PyObject *ret, **items; + Py_ssize_t i; + pgLineBase *sline = &self->line; + int colliding; + + if (!PySequence_Check(arg)) { + return RAISE(PyExc_TypeError, "Argument must be a sequence"); + } + + ret = PyList_New(0); + if (!ret) { + return NULL; + } + + /* fast path */ + if (PySequence_FAST_CHECK(arg)) { + PyObject **items = PySequence_Fast_ITEMS(arg); + + for (i = 0; i < PySequence_Fast_GET_SIZE(arg); i++) { + if ((colliding = _pg_line_collideswith(sline, items[i])) == -1) { + /*invalid shape*/ + Py_DECREF(ret); + return NULL; + } + + if (!colliding) { + continue; + } + + PyObject *num = PyLong_FromSsize_t(i); + if (!num) { + Py_DECREF(ret); + return NULL; + } + + if (PyList_Append(ret, num)) { + Py_DECREF(num); + Py_DECREF(ret); + return NULL; + } + Py_DECREF(num); + } + + return ret; + } + + /* general sequence path */ + for (i = 0; i < PySequence_Length(arg); i++) { + PyObject *obj = PySequence_GetItem(arg, i); + if (!obj) { + Py_DECREF(ret); + return NULL; + } + + if ((colliding = _pg_line_collideswith(sline, obj)) == -1) { + /*invalid shape*/ + Py_DECREF(obj); + Py_DECREF(ret); + return NULL; + } + Py_DECREF(obj); + + if (!colliding) { + continue; + } + + PyObject *num = PyLong_FromSsize_t(i); + if (!num) { + Py_DECREF(ret); + return NULL; + } + + if (PyList_Append(ret, num)) { + Py_DECREF(num); + Py_DECREF(ret); + return NULL; + } + Py_DECREF(num); + } + + return ret; +} + static PyObject * pg_line_move(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs) { @@ -758,6 +899,8 @@ static struct PyMethodDef pg_line_methods[] = { {"collideswith", (PyCFunction)pg_line_collideswith, METH_O, NULL}, {"collidepolygon", (PyCFunction)pg_line_collidepolygon, METH_FASTCALL, NULL}, + {"collidelist", (PyCFunction)pg_line_collidelist, METH_O, NULL}, + {"collidelistall", (PyCFunction)pg_line_collidelistall, METH_O, NULL}, {"as_rect", (PyCFunction)pg_line_as_rect, METH_NOARGS, NULL}, {"update", (PyCFunction)pg_line_update, METH_FASTCALL, NULL}, {"move", (PyCFunction)pg_line_move, METH_FASTCALL, NULL}, diff --git a/test/test_line.py b/test/test_line.py index 4a24562f..e3fea4d2 100644 --- a/test/test_line.py +++ b/test/test_line.py @@ -1381,6 +1381,172 @@ def test_meth_as_segments_argvalue(self): with self.assertRaises(ValueError): l.as_segments(value) + def test_meth_collidelist_argtype(self): + """Tests if the function correctly handles incorrect types as parameters""" + + invalid_types = ( + True, + False, + None, + [ + 1, + ], + "1", + (1,), + 1, + 0, + -1, + 1.23, + (1, 2, 3), + Circle(10, 10, 4), + Line(10, 10, 4, 4), + Rect(10, 10, 4, 4), + Vector3(10, 10, 4), + Vector2(10, 10), + ) + + l = Line(0, 0, 1, 1) + + for value in invalid_types: + with self.assertRaises(TypeError): + l.collidelist(value) + + def test_meth_collidelist_argnum(self): + """Tests if the function correctly handles incorrect number of parameters""" + l = Line(0, 0, 1, 1) + + invalid_args = [ + (l, l), + (l, l, l), + (l, l, l, l), + ] + + with self.assertRaises(TypeError): + l.collidelist() + + for arg in invalid_args: + with self.assertRaises(TypeError): + l.collidelist(*arg) + + def test_meth_collidelist_return_type(self): + """Tests if the function returns the correct type""" + l = Line(0, 0, 1, 1) + + items = [ + l, + Line(10, 10, 1, 1), + Rect(0, 0, 1, 1), + Circle(0, 0, 1), + Polygon((-5, 0), (5, 0), (0, 5)), + (5, 5), + [5, 5], + Vector2(5, 5), + ] + + for item in items: + self.assertIsInstance(l.collidelist([item]), int) + + def test_meth_collidelist(self): + """Ensures that the collidelist method works correctly""" + l = Line(0, 0, 10, 10) + + circles = [Circle(0, 0, 1), Circle(10, 10, 1), Circle(5, 5, 1)] + rects = [Rect(100, 100, 1, 1), Rect(1345, 213, 1, 1), Rect(5, 5, 1, 1)] + lines = [Line(240, 570, 1245, 1313), Line(0, 0, 17, 43), Line(5, 5, 1, 1)] + polygons = [ + Polygon((-50, 0), (-25, 0), (-34, -33)), + Polygon((5, 5), (15, 5), (10, 10)), + Polygon((2.5, 2.5), (7.5, 2.5), (5, 5)), + ] + + expected = [0, 2, 1, 1] + + for objects, expected in zip([circles, rects, lines, polygons], expected): + self.assertEqual(l.collidelist(objects), expected) + + def test_meth_collidelistall_argtype(self): + """Tests if the function correctly handles incorrect types as parameters""" + + invalid_types = ( + True, + False, + None, + [ + 1, + ], + "1", + (1,), + 1, + 0, + -1, + 1.23, + (1, 2, 3), + Circle(10, 10, 4), + Line(10, 10, 4, 4), + Rect(10, 10, 4, 4), + Vector3(10, 10, 4), + Vector2(10, 10), + ) + + l = Line(0, 0, 1, 1) + + for value in invalid_types: + with self.assertRaises(TypeError): + l.collidelistall(value) + + def test_meth_collidelistall_argnum(self): + """Tests if the function correctly handles incorrect number of parameters""" + l = Line(0, 0, 1, 1) + + invalid_args = [ + (l, l), + (l, l, l), + (l, l, l, l), + ] + + with self.assertRaises(TypeError): + l.collidelistall() + + for arg in invalid_args: + with self.assertRaises(TypeError): + l.collidelistall(*arg) + + def test_meth_collidelistall_return_type(self): + """Tests if the function returns the correct type""" + l = Line(0, 0, 1, 1) + + items = [ + l, + Line(10, 10, 1, 1), + Rect(0, 0, 1, 1), + Circle(0, 0, 1), + Polygon((-5, 0), (5, 0), (0, 5)), + (5, 5), + [5, 5], + Vector2(5, 5), + ] + + for item in items: + self.assertIsInstance(l.collidelistall([item]), list) + + def test_meth_collidelistall(self): + """Ensures that the collidelistall method works correctly""" + l = Line(0, 0, 10, 10) + + circles = [Circle(0, 0, 1), Circle(10, 10, 1), Circle(5, 5, 1)] + rects = [Rect(100, 100, 1, 1), Rect(1345, 213, 1, 1), Rect(5, 5, 1, 1)] + lines = [Line(240, 570, 1245, 1313), Line(0, 0, 17, 43), Line(5, 5, 1, 1)] + polygons = [ + Polygon((-50, 0), (-25, 0), (-34, -33)), + Polygon((5, 5), (15, 5), (10, 10)), + Polygon((2.5, 2.5), (7.5, 2.5), (5, 5)), + ] + + expected = [[0, 1, 2], [2], [1], [1, 2]] + + for objects, expected in zip([circles, rects, lines, polygons], expected): + self.assertEqual(l.collidelistall(objects), expected) + if __name__ == "__main__": unittest.main()