diff --git a/buildconfig/stubs/pygame/geometry.pyi b/buildconfig/stubs/pygame/geometry.pyi index 4ffbfc742d..b4e719c9ba 100644 --- a/buildconfig/stubs/pygame/geometry.pyi +++ b/buildconfig/stubs/pygame/geometry.pyi @@ -9,6 +9,8 @@ from typing import ( from pygame import Rect, FRect from ._common import Coordinate, RectValue +from .rect import Rect, FRect +from .math import Vector2 _CanBeCircle = Union[Circle, Tuple[Coordinate, float], Sequence[float]] @@ -18,6 +20,7 @@ class _HasCirclettribute(Protocol): circle: Union[_CanBeCircle, Callable[[], _CanBeCircle]] _CircleValue = Union[_CanBeCircle, _HasCirclettribute] +_CanBeCollided = Union[Circle, Rect, FRect, Coordinate, Vector2] class Circle: @property @@ -90,6 +93,7 @@ class Circle: def colliderect(self, x: float, y: float, w: float, h: float, /) -> bool: ... @overload def colliderect(self, topleft: Coordinate, size: Coordinate, /) -> bool: ... + def collideswith(self, other: _CanBeCollided, /) -> bool: ... @overload def update(self, circle: _CircleValue, /) -> None: ... @overload diff --git a/docs/reST/ref/geometry.rst b/docs/reST/ref/geometry.rst index 533c66f31d..2499c20d40 100644 --- a/docs/reST/ref/geometry.rst +++ b/docs/reST/ref/geometry.rst @@ -262,6 +262,27 @@ .. ## Circle.colliderect ## + .. method:: collideswith + + | :sl:`check if a shape or point collides with the circle` + | :sg:`collideswith(circle, /) -> bool` + | :sg:`collideswith(rect, /) -> bool` + | :sg:`collideswith((x, y), /) -> bool` + | :sg:`collideswith(vector2, /) -> bool` + + The `collideswith` method checks if a shape or point overlaps with a `Circle` object. + It takes a single argument which can be a `Circle`, `Rect`, `FRect`, or a point. + It returns `True` if there's an overlap, and `False` otherwise. + + .. note:: + The shape argument must be an actual shape object (`Circle`, `Rect`, or `FRect`). + You can't pass a tuple or list of coordinates representing the shape (except for a point), + because the shape type can't be determined from the coordinates alone. + + .. versionadded:: 2.5.0 + + .. ## Circle.collideswith ## + .. method:: update | :sl:`updates the circle position and radius` diff --git a/src_c/circle.c b/src_c/circle.c index 3d84a3e33f..597cf295a4 100644 --- a/src_c/circle.c +++ b/src_c/circle.c @@ -388,6 +388,44 @@ pg_circle_colliderect(pgCircleObject *self, PyObject *const *args, return PyBool_FromLong(pgCollision_RectCircle(x, y, w, h, &self->circle)); } +static PyObject * +pg_circle_collideswith(pgCircleObject *self, PyObject *arg) +{ + int result = 0; + pgCircleBase *scirc = &self->circle; + if (pgCircle_Check(arg)) { + result = pgCollision_CircleCircle(&pgCircle_AsCircle(arg), scirc); + } + else if (pgRect_Check(arg)) { + SDL_Rect *argrect = &pgRect_AsRect(arg); + result = pgCollision_RectCircle((double)argrect->x, (double)argrect->y, + (double)argrect->w, (double)argrect->h, + scirc); + } + else if (pgFRect_Check(arg)) { + SDL_FRect *argrect = &pgFRect_AsRect(arg); + result = pgCollision_RectCircle((double)argrect->x, (double)argrect->y, + (double)argrect->w, (double)argrect->h, + scirc); + } + else if (PySequence_Check(arg)) { + double x, y; + if (!pg_TwoDoublesFromObj(arg, &x, &y)) { + return RAISE( + PyExc_TypeError, + "Invalid point argument, must be a sequence of two numbers"); + } + 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"); + } + + return PyBool_FromLong(result); +} + static PyObject * pg_circle_as_rect(pgCircleObject *self, PyObject *_null) { @@ -422,6 +460,8 @@ static struct PyMethodDef pg_circle_methods[] = { DOC_CIRCLE_COLLIDERECT}, {"update", (PyCFunction)pg_circle_update, METH_FASTCALL, DOC_CIRCLE_UPDATE}, + {"collideswith", (PyCFunction)pg_circle_collideswith, METH_O, + DOC_CIRCLE_COLLIDESWITH}, {"as_rect", (PyCFunction)pg_circle_as_rect, METH_NOARGS, DOC_CIRCLE_ASRECT}, {"as_frect", (PyCFunction)pg_circle_as_frect, METH_NOARGS, diff --git a/src_c/doc/geometry_doc.h b/src_c/doc/geometry_doc.h index bd8ffb53cb..690f69bd69 100644 --- a/src_c/doc/geometry_doc.h +++ b/src_c/doc/geometry_doc.h @@ -14,6 +14,7 @@ #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" #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\nchecks if a rectangle intersects the circle" +#define DOC_CIRCLE_COLLIDESWITH "collideswith(circle, /) -> bool\ncollideswith(rect, /) -> bool\ncollideswith((x, y), /) -> bool\ncollideswith(vector2, /) -> bool\ncheck if a shape or point collides with the circle" #define DOC_CIRCLE_UPDATE "update((x, y), radius, /) -> None\nupdate(x, y, radius, /) -> None\nupdates the circle position and radius" #define DOC_CIRCLE_ASRECT "as_rect() -> Rect\nreturns the smallest pygame.Rect object that contains the circle" #define DOC_CIRCLE_ASFRECT "as_frect() -> FRect\nreturns the smallest pygame.FRect object that contains the circle" diff --git a/test/geometry_test.py b/test/geometry_test.py index 1dbe9b4332..d3031fe504 100644 --- a/test/geometry_test.py +++ b/test/geometry_test.py @@ -616,6 +616,57 @@ def test_colliderect(self): self.assertTrue(c3.colliderect(0.4, 0.0, 1, 1), msgt) self.assertTrue(c3.colliderect((0.4, 0.0), (1, 1)), msgt) + def test_collideswith_argtype(self): + """tests if the function correctly handles incorrect types as parameters""" + invalid_types = (None, [], "1", (1,), Vector3(1, 1, 1), 1) + + c = Circle(10, 10, 4) + + for value in invalid_types: + with self.assertRaises(TypeError): + c.collideswith(value) + + def test_collideswith_argnum(self): + c = Circle(10, 10, 4) + args = [tuple(range(x)) for x in range(2, 4)] + + # no params + with self.assertRaises(TypeError): + c.collideswith() + + # too many params + for arg in args: + with self.assertRaises(TypeError): + c.collideswith(*arg) + + def test_collideswith(self): + """Ensures the collideswith function correctly registers collisions with circles, lines, rects and points""" + c = Circle(0, 0, 5) + + # circle + c2 = Circle(0, 10, 15) + c3 = Circle(100, 100, 1) + self.assertTrue(c.collideswith(c2)) + self.assertFalse(c.collideswith(c3)) + + # rect + r = Rect(0, 0, 10, 10) + r2 = Rect(50, 0, 10, 10) + self.assertTrue(c.collideswith(r)) + self.assertFalse(c.collideswith(r2)) + + # rect + r = FRect(0, 0, 10, 10) + r2 = FRect(50, 0, 10, 10) + self.assertTrue(c.collideswith(r)) + self.assertFalse(c.collideswith(r2)) + + # point + p = (0, 0) + p2 = (50, 0) + self.assertTrue(c.collideswith(p)) + self.assertFalse(c.collideswith(p2)) + def test_update(self): """Ensures that updating the circle position and dimension correctly updates position and dimension"""