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

Add Circle.collidelist/collidelistall() #2880

Merged
merged 10 commits into from
Sep 29, 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
3 changes: 3 additions & 0 deletions buildconfig/stubs/pygame/geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ from typing import (
Callable,
Protocol,
Tuple,
Sequence,
List,
)

Expand Down Expand Up @@ -95,6 +96,8 @@ class Circle:
@overload
def colliderect(self, topleft: Coordinate, size: Coordinate, /) -> bool: ...
def collideswith(self, other: _CanBeCollided, /) -> bool: ...
def collidelist(self, colliders: Sequence[_CanBeCollided], /) -> int: ...
def collidelistall(self, colliders: Sequence[_CanBeCollided], /) -> List[int]: ...
def intersect(self, other: _CanBeIntersected, /) -> List[Tuple[float, float]]: ...
def contains(self, shape: _CanBeCollided) -> bool: ...
@overload
Expand Down
44 changes: 43 additions & 1 deletion docs/reST/ref/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@
x, y coordinates and width, height as its argument.

.. versionadded:: 2.4.0

.. ## Circle.colliderect ##

.. method:: collideswith
Expand All @@ -217,6 +217,48 @@

.. ## 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 `Circle`, `Rect`, `FRect`, or a point. 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::
The shapes must be actual shape objects, such as `Circle`, `Rect` or `FRect`
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 shape type can't be
determined from the coordinates alone.

.. versionadded:: 2.5.2

.. ## 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 `Circle`, `Rect`, `FRect`, or a point. 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::
The shapes must be actual shape objects, such as `Circle`, `Rect` or `FRect`
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 shape type can't be
determined from the coordinates alone.

.. versionadded:: 2.5.2

.. ## Circle.collidelistall ##

.. method:: contains

| :sl:`tests if a shape or point is inside the circle`
Expand Down
164 changes: 157 additions & 7 deletions src_c/circle.c
Original file line number Diff line number Diff line change
Expand Up @@ -312,11 +312,10 @@ pg_circle_rotate_ip(pgCircleObject *self, PyObject *const *args,
Py_RETURN_NONE;
}

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);
}
Expand All @@ -335,21 +334,168 @@ pg_circle_collideswith(pgCircleObject *self, PyObject *arg)
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 two numbers");
return -1;
}
result = pgCollision_CirclePoint(scirc, x, y);
}
else {
return RAISE(PyExc_TypeError,
"Invalid shape argument, must be a Circle, Rect / FRect, "
"Line, Polygon or a sequence of two numbers");
PyErr_SetString(
PyExc_TypeError,
"Invalid point argument, must be a sequence of 2 numbers");
return -1;
}

return result;
}

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);
}

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, "colliders argument must be a sequence");
}

/* fast path */
if (pgSequenceFast_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_ITEM(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;
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);
damusss marked this conversation as resolved.
Show resolved Hide resolved
if (!ret) {
return NULL;
}

/* fast path */
if (pgSequenceFast_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_ITEM(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 PyObject *
pg_circle_as_rect(pgCircleObject *self, PyObject *_null)
{
Expand Down Expand Up @@ -461,6 +607,10 @@ static struct PyMethodDef pg_circle_methods[] = {
DOC_CIRCLE_UPDATE},
{"collideswith", (PyCFunction)pg_circle_collideswith, METH_O,
DOC_CIRCLE_COLLIDESWITH},
{"collidelist", (PyCFunction)pg_circle_collidelist, METH_O,
DOC_CIRCLE_COLLIDELIST},
{"collidelistall", (PyCFunction)pg_circle_collidelistall, METH_O,
DOC_CIRCLE_COLLIDELISTALL},
{"as_rect", (PyCFunction)pg_circle_as_rect, METH_NOARGS,
DOC_CIRCLE_ASRECT},
{"as_frect", (PyCFunction)pg_circle_as_frect, METH_NOARGS,
Expand Down
2 changes: 2 additions & 0 deletions src_c/doc/geometry_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#define DOC_CIRCLE_COLLIDECIRCLE "collidecircle(circle, /) -> bool\ncollidecircle(x, y, radius, /) -> bool\ncollidecircle((x, y), radius, /) -> bool\ncollidecircle(vector2, radius, /) -> bool\ntests if a circle collides with this circle"
#define DOC_CIRCLE_COLLIDERECT "colliderect(rect, /) -> bool\ncolliderect((x, y, width, height), /) -> bool\ncolliderect(x, y, width, height, /) -> bool\ncolliderect((x, y), (width, height), /) -> bool\ncolliderect(vector2, (width, height), /) -> bool\ntests if a rectangle collides with this circle"
#define DOC_CIRCLE_COLLIDESWITH "collideswith(circle, /) -> bool\ncollideswith(rect, /) -> bool\ncollideswith((x, y), /) -> bool\ncollideswith(vector2, /) -> bool\ntests if a shape or point collides with this circle"
#define DOC_CIRCLE_COLLIDELIST "collidelist(colliders) -> int\ntest if a list of objects collide with the circle"
#define DOC_CIRCLE_COLLIDELISTALL "collidelistall(colliders) -> list\ntest if all objects in a list collide with the circle"
#define DOC_CIRCLE_CONTAINS "contains(circle, /) -> bool\ncontains(rect, /) -> bool\ncontains((x, y), /) -> bool\ncontains(vector2, /) -> bool\ntests if a shape or point is inside the circle"
#define DOC_CIRCLE_MOVE "move((x, y), /) -> Circle\nmove(x, y, /) -> Circle\nmove(vector2, /) -> Circle\nmoves the circle by a given amount"
#define DOC_CIRCLE_MOVEIP "move_ip((x, y), /) -> None\nmove_ip(x, y, /) -> None\nmove_ip(vector2, /) -> None\nmoves the circle by a given amount, in place"
Expand Down
96 changes: 96 additions & 0 deletions test/geometry_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,102 @@ def test_collideswith(self):
self.assertTrue(c.collideswith(p))
self.assertFalse(c.collideswith(p2))

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),
]

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)]
points = [(-10, -10), Vector2(1, 1), Vector2(10, -20), (10, 10)]
expected = [1, 2, 3]

for objects, expected in zip([circles, rects, points], 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),
(10, 10),
Vector2(9, 9),
]

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)]
points = [Vector2(-10, -10), (8, 8), (10, -20), Vector2(10, 10)]
expected = [[1, 2], [2], [1, 3]]

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

def test_update(self):
"""Ensures that updating the circle position
and dimension correctly updates position and dimension"""
Expand Down
Loading