diff --git a/buildconfig/Setup.Android.SDL2.in b/buildconfig/Setup.Android.SDL2.in index d1ad524358..db6f9fb046 100644 --- a/buildconfig/Setup.Android.SDL2.in +++ b/buildconfig/Setup.Android.SDL2.in @@ -65,3 +65,4 @@ math src_c/math.c $(SDL) $(DEBUG) pixelcopy src_c/pixelcopy.c $(SDL) $(DEBUG) newbuffer src_c/newbuffer.c $(SDL) $(DEBUG) _window src_c/window.c $(SDL) $(DEBUG) +geometry src_c/geometry.c $(SDL) $(DEBUG) diff --git a/buildconfig/Setup.Emscripten.SDL2.in b/buildconfig/Setup.Emscripten.SDL2.in index 7e3fd896da..3712b5d0b1 100644 --- a/buildconfig/Setup.Emscripten.SDL2.in +++ b/buildconfig/Setup.Emscripten.SDL2.in @@ -62,6 +62,7 @@ rect src_c/void.c rwobject src_c/void.c system src_c/void.c _window src_c/void.c +geometry src_c/void.c #_sdl2.controller src_c/_sdl2/controller.c $(SDL) $(DEBUG) -Isrc_c _sdl2.controller_old src_c/void.c diff --git a/buildconfig/Setup.SDL2.in b/buildconfig/Setup.SDL2.in index 011fb7a916..876aa1e8e1 100644 --- a/buildconfig/Setup.SDL2.in +++ b/buildconfig/Setup.SDL2.in @@ -75,4 +75,5 @@ math src_c/math.c $(SDL) $(DEBUG) pixelcopy src_c/pixelcopy.c $(SDL) $(DEBUG) newbuffer src_c/newbuffer.c $(SDL) $(DEBUG) system src_c/system.c $(SDL) $(DEBUG) +geometry src_c/geometry.c $(SDL) $(DEBUG) _window src_c/window.c $(SDL) $(DEBUG) diff --git a/buildconfig/stubs/gen_stubs.py b/buildconfig/stubs/gen_stubs.py index 8a0247f2ba..b2b04393c0 100644 --- a/buildconfig/stubs/gen_stubs.py +++ b/buildconfig/stubs/gen_stubs.py @@ -50,6 +50,7 @@ "sysfont", "_debug", "system", + "geometry", ] # pygame classes that are autoimported into main namespace are kept in this dict @@ -68,6 +69,7 @@ "mixer": ["Channel"], "time": ["Clock"], "joystick": ["Joystick"], + "geometry": ["Circle"], } # pygame modules from which __init__.py does the equivalent of diff --git a/buildconfig/stubs/pygame/geometry.pyi b/buildconfig/stubs/pygame/geometry.pyi new file mode 100644 index 0000000000..848c6a47b8 --- /dev/null +++ b/buildconfig/stubs/pygame/geometry.pyi @@ -0,0 +1,20 @@ +from typing import ( + Sequence, + overload, +) + +class Circle: + x: float + y: float + r: float + + @overload + def __init__(self, x: float, y: float, r: float) -> None: ... + @overload + def __init__(self, pos: Sequence[float], r: float) -> None: ... + @overload + def __init__(self, circle: Circle) -> None: ... + @overload + def __init__(self, obj_with_circle_attr) -> None: ... + def __copy__(self) -> Circle: ... + copy = __copy__ diff --git a/docs/reST/ext/boilerplate.py b/docs/reST/ext/boilerplate.py index bd92029f7b..d4aeae34c9 100644 --- a/docs/reST/ext/boilerplate.py +++ b/docs/reST/ext/boilerplate.py @@ -337,6 +337,7 @@ def lowercase_name(d): "music", "pygame", "Rect", + "geometry", "Surface", "sprite", "time", diff --git a/docs/reST/ref/geometry.rst b/docs/reST/ref/geometry.rst new file mode 100644 index 0000000000..bc93befffc --- /dev/null +++ b/docs/reST/ref/geometry.rst @@ -0,0 +1,103 @@ +.. include:: common.txt + +:mod:`pygame.geometry` +====================== + +.. module:: pygame.geometry + :synopsis: pygame geometry module + + .. warning:: + **Experimental Module** + + **This module is a work in progress. Refrain from relying on any features provided by + this module, as they are subject to change or removal without prior notice.** + + | :sl:`pygame module for the Circle, Line, and Polygon objects` + +.. currentmodule:: pygame + +.. class:: Circle + + | :sl:`pygame object for representing a circle` + | :sg:`Circle((x, y), radius) -> Circle` + | :sg:`Circle(x, y, radius) -> Circle` + + The `Circle` class provides many useful methods for collision / transform and intersection. + A `Circle` can be created from a combination of a pair of coordinates that represent + the center of the circle and a radius. Circles can also be created from python objects that + are already a `Circle` or have an attribute named "circle". + + Specifically, to construct a circle you can pass the x, y, and radius values as separate + arguments or inside a sequence(list or tuple). + + Functions that require a `Circle` argument may also accept these values as Circles: + + :: + + ((x, y), radius) + (x, y, radius) + + It is important to note that you cannot create degenerate circles, which are circles with + a radius of 0 or less. If you try to create such a circle, the `Circle` object will not be + created and an error will be raised. This is because a circle with a radius of 0 or + less is not a valid geometric object. + + The `Circle` class has both virtual and non-virtual attributes. Non-virtual attributes + are attributes that are stored in the `Circle` object itself. Virtual attributes are the + result of calculations that utilize the Circle's non-virtual attributes. + + Here is the list of all the attributes and methods of the Circle class: + + **Circle Attributes** + + ---- + + .. attribute:: x + + | :sl:`center x coordinate of the circle` + | :sg:`x -> float` + + The `x` coordinate of the center of the circle. It can be reassigned to move the circle. + Reassigning the `x` attribute will move the circle to the new `x` coordinate. + The `y` and `r` attributes will not be affected. + + .. ## Circle.x ## + + .. attribute:: y + + | :sl:`center y coordinate of the circle` + | :sg:`y -> float` + + The `y` coordinate of the center of the circle. It can be reassigned to move the circle. + Reassigning the `y` attribute will move the circle to the new `y` coordinate. + The `x` and `r` attributes will not be affected. + + .. ## Circle.y ## + + .. attribute:: r + + | :sl:`radius of the circle` + | :sg:`r -> float` + + It is not possible to set the radius to a negative value. It can be reassigned. + If reassigned it will only change the radius of the circle. + The circle will not be moved from its original position. + + .. ## Circle.r ## + + **Circle Methods** + + ---- + + .. method:: copy + + | :sl:`returns a copy of the circle` + | :sg:`copy() -> Circle` + + The `copy` method returns a new `Circle` object having the same position and radius + as the original `Circle` object. The function takes no arguments and returns the + new `Circle` object. + + .. ## Circle.copy ## + + .. ## pygame.Circle ## \ No newline at end of file diff --git a/docs/reST/themes/classic/elements.html b/docs/reST/themes/classic/elements.html index d4b2748ad8..30c154338f 100644 --- a/docs/reST/themes/classic/elements.html +++ b/docs/reST/themes/classic/elements.html @@ -40,7 +40,7 @@
Most useful stuff:
{% set sep = joiner(" | \n") %}
diff --git a/src_c/_pygame.h b/src_c/_pygame.h
index 0774b294e1..7a2f069ef9 100644
--- a/src_c/_pygame.h
+++ b/src_c/_pygame.h
@@ -445,8 +445,9 @@ typedef enum {
#define PYGAMEAPI_PIXELARRAY_NUMSLOTS 2
#define PYGAMEAPI_COLOR_NUMSLOTS 5
#define PYGAMEAPI_MATH_NUMSLOTS 2
-#define PYGAMEAPI_BASE_NUMSLOTS 24
+#define PYGAMEAPI_BASE_NUMSLOTS 26
#define PYGAMEAPI_EVENT_NUMSLOTS 8
#define PYGAMEAPI_WINDOW_NUMSLOTS 1
+#define PYGAMEAPI_GEOMETRY_NUMSLOTS 1
#endif /* _PYGAME_INTERNAL_H */
diff --git a/src_c/base.c b/src_c/base.c
index e33b5d27e4..36903b8656 100644
--- a/src_c/base.c
+++ b/src_c/base.c
@@ -575,6 +575,91 @@ pg_TwoFloatsFromObj(PyObject *obj, float *val1, float *val2)
return 1;
}
+static inline int
+pg_DoubleFromObj(PyObject *obj, double *val)
+{
+ if (PyFloat_Check(obj)) {
+ *val = PyFloat_AS_DOUBLE(obj);
+ return 1;
+ }
+
+ *val = (double)PyLong_AsLong(obj);
+ if (PyErr_Occurred()) {
+ PyErr_Clear();
+ return 0;
+ }
+
+ return 1;
+}
+
+/*Assumes obj is a Sequence, internal or conscious use only*/
+static inline int
+_pg_DoubleFromObjIndex(PyObject *obj, int index, double *val)
+{
+ int result = 0;
+
+ PyObject *item = PySequence_ITEM(obj, index);
+ if (!item) {
+ PyErr_Clear();
+ return 0;
+ }
+ result = pg_DoubleFromObj(item, val);
+ Py_DECREF(item);
+
+ return result;
+}
+
+static inline int
+pg_TwoDoublesFromObj(PyObject *obj, double *val1, double *val2)
+{
+ Py_ssize_t length;
+ /*Faster path for tuples and lists*/
+ if (pgSequenceFast_Check(obj)) {
+ length = PySequence_Fast_GET_SIZE(obj);
+ PyObject **f_arr = PySequence_Fast_ITEMS(obj);
+ if (length == 2) {
+ if (!pg_DoubleFromObj(f_arr[0], val1) ||
+ !pg_DoubleFromObj(f_arr[1], val2)) {
+ return 0;
+ }
+ }
+ else if (length == 1) {
+ /* Handle case of ((x, y), ) 'nested sequence' */
+ return pg_TwoDoublesFromObj(f_arr[0], val1, val2);
+ }
+ else {
+ return 0;
+ }
+ }
+ else if (PySequence_Check(obj)) {
+ length = PySequence_Length(obj);
+ if (length == 2) {
+ if (!_pg_DoubleFromObjIndex(obj, 0, val1)) {
+ return 0;
+ }
+ if (!_pg_DoubleFromObjIndex(obj, 1, val2)) {
+ return 0;
+ }
+ }
+ else if (length == 1 && !PyUnicode_Check(obj)) {
+ /* Handle case of ((x, y), ) 'nested sequence' */
+ PyObject *tmp = PySequence_ITEM(obj, 0);
+ int ret = pg_TwoDoublesFromObj(tmp, val1, val2);
+ Py_DECREF(tmp);
+ return ret;
+ }
+ else {
+ PyErr_Clear();
+ return 0;
+ }
+ }
+ else {
+ return 0;
+ }
+
+ return 1;
+}
+
static int
pg_UintFromObj(PyObject *obj, Uint32 *val)
{
@@ -2171,8 +2256,10 @@ MODINIT_DEFINE(base)
c_api[21] = pg_GetDefaultWindowSurface;
c_api[22] = pg_SetDefaultWindowSurface;
c_api[23] = pg_EnvShouldBlendAlphaSDL2;
+ c_api[24] = pg_DoubleFromObj;
+ c_api[25] = pg_TwoDoublesFromObj;
-#define FILLED_SLOTS 24
+#define FILLED_SLOTS 26
#if PYGAMEAPI_BASE_NUMSLOTS != FILLED_SLOTS
#error export slot count mismatch
diff --git a/src_c/circle.c b/src_c/circle.c
new file mode 100644
index 0000000000..4109195b25
--- /dev/null
+++ b/src_c/circle.c
@@ -0,0 +1,316 @@
+#include "geometry.h"
+#include "doc/geometry_doc.h"
+
+static PyObject *
+_pg_circle_subtype_new(PyTypeObject *type, pgCircleBase *circle)
+{
+ pgCircleObject *circle_obj =
+ (pgCircleObject *)pgCircle_Type.tp_new(type, NULL, NULL);
+
+ if (circle_obj) {
+ circle_obj->circle = *circle;
+ }
+ return (PyObject *)circle_obj;
+}
+
+static int
+_pg_circle_set_radius(PyObject *value, pgCircleBase *circle)
+{
+ double radius = 0;
+ if (!pg_DoubleFromObj(value, &radius) || radius <= 0) {
+ return 0;
+ }
+ circle->r = radius;
+
+ return 1;
+}
+
+static int
+pgCircle_FromObject(PyObject *obj, pgCircleBase *out)
+{
+ Py_ssize_t length;
+
+ if (pgCircle_Check(obj)) {
+ *out = pgCircle_AsCircle(obj);
+ return 1;
+ }
+
+ /* Paths for sequences */
+ if (pgSequenceFast_Check(obj)) {
+ PyObject **f_arr = PySequence_Fast_ITEMS(obj);
+ length = PySequence_Fast_GET_SIZE(obj);
+
+ if (length == 3) {
+ if (!pg_DoubleFromObj(f_arr[0], &out->x) ||
+ !pg_DoubleFromObj(f_arr[1], &out->y) ||
+ !_pg_circle_set_radius(f_arr[2], out)) {
+ return 0;
+ }
+ return 1;
+ }
+ else if (length == 1) {
+ if (!pgCircle_FromObject(f_arr[0], out)) {
+ return 0;
+ }
+ return 1;
+ }
+ else if (length == 2) {
+ if (!pg_TwoDoublesFromObj(f_arr[0], &out->x, &out->y) ||
+ !_pg_circle_set_radius(f_arr[1], out)) {
+ return 0;
+ }
+ return 1;
+ }
+ else {
+ /* Sequences of size other than 3 or 1 are not supported
+ (don't wanna support infinite sequence nesting anymore)*/
+ return 0;
+ }
+ }
+ else if (PySequence_Check(obj)) {
+ PyObject *tmp = NULL;
+ length = PySequence_Length(obj);
+ if (length == 3) {
+ /*These are to be substituted with better pg_DoubleFromSeqIndex()
+ * implementations*/
+ tmp = PySequence_ITEM(obj, 0);
+ if (!pg_DoubleFromObj(tmp, &out->x)) {
+ Py_DECREF(tmp);
+ return 0;
+ }
+ Py_DECREF(tmp);
+
+ tmp = PySequence_ITEM(obj, 1);
+ if (!pg_DoubleFromObj(tmp, &out->y)) {
+ Py_DECREF(tmp);
+ return 0;
+ }
+ Py_DECREF(tmp);
+
+ tmp = PySequence_ITEM(obj, 2);
+ if (!_pg_circle_set_radius(tmp, out)) {
+ Py_DECREF(tmp);
+ return 0;
+ }
+ Py_DECREF(tmp);
+
+ return 1;
+ }
+ else if (length == 2) {
+ tmp = PySequence_ITEM(obj, 0);
+ if (!pg_TwoDoublesFromObj(tmp, &out->x, &out->y)) {
+ Py_DECREF(tmp);
+ return 0;
+ }
+ Py_DECREF(tmp);
+
+ tmp = PySequence_ITEM(obj, 1);
+ if (!_pg_circle_set_radius(tmp, out)) {
+ Py_DECREF(tmp);
+ return 0;
+ }
+ Py_DECREF(tmp);
+
+ return 1;
+ }
+ else if (length == 1) {
+ tmp = PySequence_ITEM(obj, 0);
+ if (PyUnicode_Check(obj) || !pgCircle_FromObject(tmp, out)) {
+ Py_DECREF(tmp);
+ return 0;
+ }
+ Py_DECREF(tmp);
+ return 1;
+ }
+ else {
+ /* Sequences of size other than 3 or 1 are not supported
+ (don't wanna support infinite sequence nesting anymore)*/
+ return 0;
+ }
+ }
+
+ /* Path for objects that have a circle attribute */
+ PyObject *circleattr;
+ if (!(circleattr = PyObject_GetAttrString(obj, "circle"))) {
+ PyErr_Clear();
+ return 0;
+ }
+
+ if (PyCallable_Check(circleattr)) /*call if it's a method*/
+ {
+ PyObject *circleresult = PyObject_CallObject(circleattr, NULL);
+ Py_DECREF(circleattr);
+ if (!circleresult) {
+ PyErr_Clear();
+ return 0;
+ }
+ circleattr = circleresult;
+ }
+
+ if (!pgCircle_FromObject(circleattr, out)) {
+ PyErr_Clear();
+ Py_DECREF(circleattr);
+ return 0;
+ }
+
+ Py_DECREF(circleattr);
+
+ return 1;
+}
+
+static PyObject *
+pg_circle_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+ pgCircleObject *self = (pgCircleObject *)type->tp_alloc(type, 0);
+
+ if (self) {
+ self->circle.x = self->circle.y = 0;
+ self->circle.r = 1;
+ self->weakreflist = NULL;
+ }
+ return (PyObject *)self;
+}
+
+static int
+pg_circle_init(pgCircleObject *self, PyObject *args, PyObject *kwds)
+{
+ if (!pgCircle_FromObject(args, &self->circle)) {
+ PyErr_SetString(
+ PyExc_TypeError,
+ "Arguments must be a Circle, a sequence of length 3 or 2, or an "
+ "object with an attribute called 'circle'");
+ return -1;
+ }
+ return 0;
+}
+
+static void
+pg_circle_dealloc(pgCircleObject *self)
+{
+ if (self->weakreflist) {
+ PyObject_ClearWeakRefs((PyObject *)self);
+ }
+
+ Py_TYPE(self)->tp_free((PyObject *)self);
+}
+
+static PyObject *
+pg_circle_copy(pgCircleObject *self, PyObject *_null)
+{
+ return _pg_circle_subtype_new(Py_TYPE(self), &self->circle);
+}
+
+static PyObject *
+pg_circle_repr(pgCircleObject *self)
+{
+ PyObject *x, *y, *r;
+
+ x = PyFloat_FromDouble(self->circle.x);
+ if (!x) {
+ return NULL;
+ }
+ y = PyFloat_FromDouble(self->circle.y);
+ if (!y) {
+ Py_DECREF(x);
+ return NULL;
+ }
+ r = PyFloat_FromDouble(self->circle.r);
+ if (!r) {
+ Py_DECREF(x);
+ Py_DECREF(y);
+ return NULL;
+ }
+
+ PyObject *result = PyUnicode_FromFormat("