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

Circle collidelist() / collidelistall() #219

Merged
merged 4 commits into from
May 27, 2024
Merged
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
46 changes: 46 additions & 0 deletions docs/circle.rst
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,52 @@ Circle Methods

.. ## Circle.collideswith ##

.. method:: collidelist

| :sl:`test if a list of objects collide with the circle`
| :sg:`collidelist(colliders) -> int`

The `collidelist` method tests whether a given list of shapes or points collides
(overlaps) with this `Circle` 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 `Circle` 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.

.. ## Circle.collidelist ##

.. method:: collidelistall

| :sl:`test if all objects in a list collide with the circle`
| :sg:`collidelistall(colliders) -> list`

The `collidelistall` method tests whether a given list of shapes or points collides
(overlaps) with this `Circle` 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 `Circle` 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.

.. ## Circle.collidelistall ##

.. method:: contains

| :sl:`test if a shape or point is inside the circle`
Expand Down
4 changes: 4 additions & 0 deletions docs/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ performing transformations and checking for collisions with other objects.

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

collidelist: Checks if the circle collides with any of the given objects.

collidelistall: Checks if the circle collides with all of the given objects.

contains: Checks if the circle fully contains the given object.

rotate: Rotates the circle by the given amount.
Expand Down
2 changes: 2 additions & 0 deletions geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ class Circle:
@overload
def colliderect(self, x: int, y: int, w: int, h: int) -> bool: ...
def collideswith(self, other: _CanBeCollided) -> bool: ...
def collidelist(self, colliders: Sequence[_CanBeCollided]) -> int: ...
def collidelistall(self, colliders: Sequence[_CanBeCollided]) -> List[int]: ...
def __copy__(self) -> Circle: ...

copy = __copy__
Expand Down
173 changes: 158 additions & 15 deletions src_c/circle.c
Original file line number Diff line number Diff line change
Expand Up @@ -320,37 +320,44 @@ pg_circle_colliderect(pgCircleObject *self, PyObject *const *args,
return PyBool_FromLong(pgCollision_RectCircle(&temp, &self->circle));
}

static PyObject *
pg_circle_collideswith(pgCircleObject *self, PyObject *arg)
static PG_FORCEINLINE int
_pg_circle_collideswith(pgCircleBase *scirc, PyObject *arg)
{
int result = 0;
pgCircleBase *scirc = &self->circle;
if (pgCircle_Check(arg)) {
result = pgCollision_CircleCircle(&pgCircle_AsCircle(arg), scirc);
return pgCollision_CircleCircle(&pgCircle_AsCircle(arg), scirc);
}
else if (pgRect_Check(arg)) {
result = pgCollision_RectCircle(&pgRect_AsRect(arg), scirc);
return pgCollision_RectCircle(&pgRect_AsRect(arg), scirc);
}
else if (pgLine_Check(arg)) {
result = pgCollision_LineCircle(&pgLine_AsLine(arg), scirc);
return pgCollision_LineCircle(&pgLine_AsLine(arg), scirc);
}
else if (pgPolygon_Check(arg)) {
result =
pgCollision_CirclePolygon(scirc, &pgPolygon_AsPolygon(arg), 0);
return pgCollision_CirclePolygon(scirc, &pgPolygon_AsPolygon(arg), 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_CirclePoint(scirc, x, y);
return pgCollision_CirclePoint(scirc, x, y);
}
else {
return RAISE(PyExc_TypeError,
"Invalid shape argument, must be a CircleType, RectType, "
"LineType, PolygonType or a sequence of 2 numbers");

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_circle_collideswith(pgCircleObject *self, PyObject *arg)
{
int result = _pg_circle_collideswith(&self->circle, arg);
if (result == -1) {
return NULL;
}

return PyBool_FromLong(result);
Expand Down Expand Up @@ -591,6 +598,140 @@ pg_circle_rotate_ip(pgCircleObject *self, PyObject *const *args,
Py_RETURN_NONE;
}

static PyObject *
pg_circle_collidelist(pgCircleObject *self, PyObject *arg)
{
Py_ssize_t i;
pgCircleBase *scirc = &self->circle;
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_circle_collideswith(scirc, 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;
}

if ((colliding = _pg_circle_collideswith(scirc, obj)) == -1) {
/*invalid shape*/
Py_DECREF(obj);
return NULL;
}
Py_DECREF(obj);

if (colliding) {
return PyLong_FromSsize_t(i);
}
}

return PyLong_FromLong(-1);
}

static PyObject *
pg_circle_collidelistall(pgCircleObject *self, PyObject *arg)
{
PyObject *ret, **items;
Py_ssize_t i;
pgCircleBase *scirc = &self->circle;
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_circle_collideswith(scirc, 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_circle_collideswith(scirc, obj)) == -1) {
/*invalid shape*/
Py_DECREF(ret);
Py_DECREF(obj);
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 struct PyMethodDef pg_circle_methods[] = {
{"collidecircle", (PyCFunction)pg_circle_collidecircle, METH_FASTCALL,
NULL},
Expand All @@ -600,6 +741,8 @@ static struct PyMethodDef pg_circle_methods[] = {
{"collideswith", (PyCFunction)pg_circle_collideswith, METH_O, NULL},
{"collidepolygon", (PyCFunction)pg_circle_collidepolygon, METH_FASTCALL,
NULL},
{"collidelist", (PyCFunction)pg_circle_collidelist, METH_O, NULL},
{"collidelistall", (PyCFunction)pg_circle_collidelistall, METH_O, NULL},
{"as_rect", (PyCFunction)pg_circle_as_rect, METH_NOARGS, NULL},
{"update", (PyCFunction)pg_circle_update, METH_FASTCALL, NULL},
{"move", (PyCFunction)pg_circle_move, METH_FASTCALL, NULL},
Expand Down
108 changes: 108 additions & 0 deletions test/test_circle.py
Original file line number Diff line number Diff line change
Expand Up @@ -1374,6 +1374,114 @@ def assert_approx_equal(circle1, circle2, eps=1e-12):
c.rotate_ip(angle, center)
assert_approx_equal(c, rotate_circle(c, angle, center))

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

invalid_types = (None, "1", (1,), 1, (1, 2, 3), True, False)

c = Circle(10, 10, 4)

for value in invalid_types:
with self.assertRaises(TypeError):
c.collidelist(value)

def test_collidelist_argnum(self):
"""Tests if the function correctly handles incorrect number of parameters"""
c = Circle(10, 10, 4)

circles = [(Circle(10, 10, 4), Circle(10, 10, 4))]

with self.assertRaises(TypeError):
c.collidelist()

with self.assertRaises(TypeError):
c.collidelist(circles, 1)

def test_collidelist_return_type(self):
"""Tests if the function returns the correct type"""
c = Circle(10, 10, 4)

objects = [
Circle(10, 10, 4),
Rect(10, 10, 4, 4),
Line(10, 10, 4, 4),
Polygon([(10, 10), (34, 10), (4, 43)]),
]

for object in objects:
self.assertIsInstance(c.collidelist([object]), int)

def test_collidelist(self):
"""Ensures that the collidelist method works correctly"""
c = Circle(10, 10, 4)

circles = [Circle(1000, 1000, 2), Circle(5, 10, 5), Circle(16, 10, 7)]
rects = [Rect(1000, 1000, 4, 4), Rect(1000, 200, 5, 5), Rect(5, 10, 7, 3)]
lines = [Line(10, 10, 4, 4), Line(100, 100, 553, 553), Line(136, 110, 324, 337)]
polygons = [
Polygon([(100, 100), (34, 10), (4, 43)]),
Polygon([(20, 10), (34, 10), (4, 43)]),
Polygon([(10, 10), (34, 10), (4, 43)]),
]
expected = [1, 2, 0, 2]

for objects, expected in zip([circles, rects, lines, polygons], expected):
self.assertEqual(c.collidelist(objects), expected)

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

invalid_types = (None, "1", (1,), 1, (1, 2, 3), True, False)

c = Circle(10, 10, 4)

for value in invalid_types:
with self.assertRaises(TypeError):
c.collidelistall(value)

def test_collidelistall_argnum(self):
"""Tests if the function correctly handles incorrect number of parameters"""
c = Circle(10, 10, 4)

circles = [(Circle(10, 10, 4), Circle(10, 10, 4))]

with self.assertRaises(TypeError):
c.collidelistall()

with self.assertRaises(TypeError):
c.collidelistall(circles, 1)

def test_collidelistall_return_type(self):
"""Tests if the function returns the correct type"""
c = Circle(10, 10, 4)

objects = [
Circle(10, 10, 4),
Rect(10, 10, 4, 4),
Line(10, 10, 4, 4),
Polygon([(10, 10), (34, 10), (4, 43)]),
]

for object in objects:
self.assertIsInstance(c.collidelistall([object]), list)

def test_collidelistall(self):
"""Ensures that the collidelistall method works correctly"""
c = Circle(10, 10, 4)

circles = [Circle(1000, 1000, 2), Circle(5, 10, 5), Circle(16, 10, 7)]
rects = [Rect(1000, 1000, 4, 4), Rect(1000, 200, 5, 5), Rect(5, 10, 7, 3)]
lines = [Line(10, 10, 4, 4), Line(0, 0, 553, 553), Line(5, 5, 10, 11)]
polygons = [
Polygon([(100, 100), (34, 10), (4, 43)]),
Polygon([(20, 10), (34, 10), (4, 43)]),
Polygon([(10, 10), (34, 10), (4, 43)]),
]
expected = [[1, 2], [2], [0, 1, 2], [2]]

for objects, expected in zip([circles, rects, lines, polygons], expected):
self.assertEqual(c.collidelistall(objects), expected)


if __name__ == "__main__":
unittest.main()
Loading