diff --git a/buildconfig/stubs/pygame/geometry.pyi b/buildconfig/stubs/pygame/geometry.pyi index 848c6a47b8..c7a8e43606 100644 --- a/buildconfig/stubs/pygame/geometry.pyi +++ b/buildconfig/stubs/pygame/geometry.pyi @@ -3,6 +3,9 @@ from typing import ( overload, ) +from pygame._common import Coordinate + + class Circle: x: float y: float @@ -16,5 +19,9 @@ class Circle: def __init__(self, circle: Circle) -> None: ... @overload def __init__(self, obj_with_circle_attr) -> None: ... + @overload + def collidepoint(self, x: float, y: float) -> bool: ... + @overload + def collidepoint(self, point: Coordinate) -> bool: ... def __copy__(self) -> Circle: ... copy = __copy__ diff --git a/docs/reST/ref/geometry.rst b/docs/reST/ref/geometry.rst index bc93befffc..b832bae71d 100644 --- a/docs/reST/ref/geometry.rst +++ b/docs/reST/ref/geometry.rst @@ -89,6 +89,20 @@ ---- + .. method:: collidepoint + + | :sl:`test if a point is inside the circle` + | :sg:`collidepoint((x, y)) -> bool` + | :sg:`collidepoint(x, y) -> bool` + | :sg:`collidepoint(Vector2) -> bool` + + The `collidepoint` method tests whether a given point is inside the `Circle` + (including the edge of the `Circle`). It takes a tuple of (x, y) coordinates, two + separate x and y coordinates, or a `Vector2` object as its argument, and returns + `True` if the point is inside the `Circle`, `False` otherwise. + + .. ## Circle.collidepoint ## + .. method:: copy | :sl:`returns a copy of the circle` diff --git a/src_c/_pygame.h b/src_c/_pygame.h index 7a2f069ef9..6a2238f005 100644 --- a/src_c/_pygame.h +++ b/src_c/_pygame.h @@ -445,7 +445,7 @@ typedef enum { #define PYGAMEAPI_PIXELARRAY_NUMSLOTS 2 #define PYGAMEAPI_COLOR_NUMSLOTS 5 #define PYGAMEAPI_MATH_NUMSLOTS 2 -#define PYGAMEAPI_BASE_NUMSLOTS 26 +#define PYGAMEAPI_BASE_NUMSLOTS 27 #define PYGAMEAPI_EVENT_NUMSLOTS 8 #define PYGAMEAPI_WINDOW_NUMSLOTS 1 #define PYGAMEAPI_GEOMETRY_NUMSLOTS 1 diff --git a/src_c/base.c b/src_c/base.c index 36903b8656..5cfb988ded 100644 --- a/src_c/base.c +++ b/src_c/base.c @@ -660,6 +660,20 @@ pg_TwoDoublesFromObj(PyObject *obj, double *val1, double *val2) return 1; } +static inline int +pg_TwoDoublesFromFastcallArgs(PyObject *const *args, Py_ssize_t nargs, + double *val1, double *val2) +{ + if (nargs == 1 && pg_TwoDoublesFromObj(args[0], val1, val2)) { + return 1; + } + else if (nargs == 2 && pg_DoubleFromObj(args[0], val1) && + pg_DoubleFromObj(args[1], val2)) { + return 1; + } + return 0; +} + static int pg_UintFromObj(PyObject *obj, Uint32 *val) { @@ -2258,8 +2272,9 @@ MODINIT_DEFINE(base) c_api[23] = pg_EnvShouldBlendAlphaSDL2; c_api[24] = pg_DoubleFromObj; c_api[25] = pg_TwoDoublesFromObj; + c_api[26] = pg_TwoDoublesFromFastcallArgs; -#define FILLED_SLOTS 26 +#define FILLED_SLOTS 27 #if PYGAMEAPI_BASE_NUMSLOTS != FILLED_SLOTS #error export slot count mismatch diff --git a/src_c/circle.c b/src_c/circle.c index 4109195b25..7310c24ca0 100644 --- a/src_c/circle.c +++ b/src_c/circle.c @@ -236,7 +236,24 @@ pg_circle_str(pgCircleObject *self) return pg_circle_repr(self); } +static PyObject * +pg_circle_collidepoint(pgCircleObject *self, PyObject *const *args, + Py_ssize_t nargs) +{ + double px, py; + + if (!pg_TwoDoublesFromFastcallArgs(args, nargs, &px, &py)) { + return RAISE( + PyExc_TypeError, + "Circle.collidepoint requires a point or PointLike object"); + } + + return PyBool_FromLong(pgCollision_CirclePoint(&self->circle, px, py)); +} + static struct PyMethodDef pg_circle_methods[] = { + {"collidepoint", (PyCFunction)pg_circle_collidepoint, METH_FASTCALL, + DOC_CIRCLE_COLLIDEPOINT}, {"__copy__", (PyCFunction)pg_circle_copy, METH_NOARGS, DOC_CIRCLE_COPY}, {"copy", (PyCFunction)pg_circle_copy, METH_NOARGS, DOC_CIRCLE_COPY}, {NULL, NULL, 0, NULL}}; diff --git a/src_c/collisions.c b/src_c/collisions.c new file mode 100644 index 0000000000..d47c16eb67 --- /dev/null +++ b/src_c/collisions.c @@ -0,0 +1,9 @@ +#include "collisions.h" + +static int +pgCollision_CirclePoint(pgCircleBase *circle, double Cx, double Cy) +{ + double dx = circle->x - Cx; + double dy = circle->y - Cy; + return dx * dx + dy * dy <= circle->r * circle->r; +} diff --git a/src_c/collisions.h b/src_c/collisions.h new file mode 100644 index 0000000000..f482c70d35 --- /dev/null +++ b/src_c/collisions.h @@ -0,0 +1,9 @@ +#ifndef _PG_COLLISIONS_H +#define _PG_COLLISIONS_H + +#include "geometry.h" + +static int +pgCollision_CirclePoint(pgCircleBase *circle, double, double); + +#endif /* ~_PG_COLLISIONS_H */ diff --git a/src_c/doc/geometry_doc.h b/src_c/doc/geometry_doc.h index 03cb419850..f4754e9f35 100644 --- a/src_c/doc/geometry_doc.h +++ b/src_c/doc/geometry_doc.h @@ -4,4 +4,5 @@ #define DOC_CIRCLE_X "x -> float\ncenter x coordinate of the circle" #define DOC_CIRCLE_Y "y -> float\ncenter y coordinate of the circle" #define DOC_CIRCLE_R "r -> float\nradius of the circle" +#define DOC_CIRCLE_COLLIDEPOINT "collidepoint((x, y)) -> bool\ncollidepoint(x, y) -> bool\ncollidepoint(Vector2) -> bool\ntest if a point is inside the circle" #define DOC_CIRCLE_COPY "copy() -> Circle\nreturns a copy of the circle" diff --git a/src_c/geometry.c b/src_c/geometry.c index 33b1fdb96e..54207074ab 100644 --- a/src_c/geometry.c +++ b/src_c/geometry.c @@ -1,4 +1,5 @@ #include "geometry.h" +#include "collisions.c" #include "circle.c" static PyMethodDef geometry_methods[] = {{NULL, NULL, 0, NULL}}; diff --git a/src_c/include/_pygame.h b/src_c/include/_pygame.h index a5ae05138c..468bcaf56a 100644 --- a/src_c/include/_pygame.h +++ b/src_c/include/_pygame.h @@ -122,6 +122,10 @@ typedef struct pg_bufferinfo_s { #define pg_TwoDoublesFromObj \ (*(int (*)(PyObject *, double *, double *))PYGAMEAPI_GET_SLOT(base, 25)) +#define pg_TwoDoublesFromFastcallArgs \ + (*(int (*)(PyObject *const *, Py_ssize_t, double *, \ + double *))PYGAMEAPI_GET_SLOT(base, 26)) + #define pg_UintFromObj \ (*(int (*)(PyObject *, Uint32 *))PYGAMEAPI_GET_SLOT(base, 8)) diff --git a/test/geometry_test.py b/test/geometry_test.py index 39e95e0017..003d62d997 100644 --- a/test/geometry_test.py +++ b/test/geometry_test.py @@ -1,6 +1,6 @@ import unittest -from pygame import Vector2 +from pygame import Vector2, Vector3 from pygame.geometry import Circle @@ -206,6 +206,59 @@ def test_copy(self): # check c2 is not c self.assertIsNot(c_2, c) + def test_collidepoint_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.collidepoint(value) + + def test_collidepoint_argnum(self): + c = Circle(10, 10, 4) + args = [tuple(range(x)) for x in range(3, 13)] + + # no params + with self.assertRaises(TypeError): + c.collidepoint() + + # too many params + for arg in args: + with self.assertRaises(TypeError): + c.collidepoint(*arg) + + def test_collidepoint(self): + c = Circle(0, 0, 5) + + p1 = (3, 3) + p2 = (10, 10) + p3 = Vector2(3, 3) + p4 = Vector2(10, 10) + + # colliding single + self.assertTrue(c.collidepoint(p1), "Expected True, point should collide here") + self.assertTrue(c.collidepoint(p3), "Expected True, point should collide here") + + # not colliding single + self.assertFalse( + c.collidepoint(p2), "Expected False, point should not collide here" + ) + self.assertFalse( + c.collidepoint(p4), "Expected False, point should not collide here" + ) + + # colliding 2 args + self.assertTrue( + c.collidepoint(3, 3), "Expected True, point should collide here" + ) + + # not colliding 2 args + self.assertFalse( + c.collidepoint(10, 10), "Expected False, point should not collide here" + ) + if __name__ == "__main__": unittest.main()