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 @@
pygame-ce documentation
#} {%- set basic = ['Color', 'display', 'draw', 'event', 'font', 'image', 'key', 'locals', 'mixer', 'mouse', 'music', 'pygame', 'Rect', 'Surface', 'time'] %} {%- set advanced = ['BufferProxy', 'freetype', 'gfxdraw', 'midi', 'PixelArray', 'pixelcopy', 'sndarray', 'surfarray', 'cursors', 'joystick', 'mask', 'math', 'sprite', 'transform'] %} -{%- set hidden = ['sdl2_video', 'sdl2_controller'] %} +{%- set hidden = ['sdl2_video', 'sdl2_controller', 'geometry'] %} {%- if pyg_sections %}

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("", x, y, r); + + Py_DECREF(x); + Py_DECREF(y); + Py_DECREF(r); + + return result; +} + +static PyObject * +pg_circle_str(pgCircleObject *self) +{ + return pg_circle_repr(self); +} + +static struct PyMethodDef pg_circle_methods[] = { + {"__copy__", (PyCFunction)pg_circle_copy, METH_NOARGS, DOC_CIRCLE_COPY}, + {"copy", (PyCFunction)pg_circle_copy, METH_NOARGS, DOC_CIRCLE_COPY}, + {NULL, NULL, 0, NULL}}; + +#define GETTER_SETTER(name) \ + static PyObject *pg_circle_get##name(pgCircleObject *self, void *closure) \ + { \ + return PyFloat_FromDouble(self->circle.name); \ + } \ + static int pg_circle_set##name(pgCircleObject *self, PyObject *value, \ + void *closure) \ + { \ + double val; \ + DEL_ATTR_NOT_SUPPORTED_CHECK_NO_NAME(value); \ + if (!pg_DoubleFromObj(value, &val)) { \ + PyErr_Format(PyExc_TypeError, "Expected a number, got '%s'", \ + Py_TYPE(value)->tp_name); \ + return -1; \ + } \ + self->circle.name = val; \ + return 0; \ + } + +GETTER_SETTER(x) +GETTER_SETTER(y) + +#undef GETTER_SETTER + +static PyObject * +pg_circle_getr(pgCircleObject *self, void *closure) +{ + return PyFloat_FromDouble(self->circle.r); +} + +static int +pg_circle_setr(pgCircleObject *self, PyObject *value, void *closure) +{ + double radius; + + DEL_ATTR_NOT_SUPPORTED_CHECK_NO_NAME(value); + + if (!pg_DoubleFromObj(value, &radius)) { + PyErr_Format(PyExc_TypeError, "Expected a number, got '%s'", + Py_TYPE(value)->tp_name); + return -1; + } + + if (radius <= 0) { + PyErr_SetString(PyExc_ValueError, "Radius must be positive"); + return -1; + } + + self->circle.r = radius; + + return 0; +} + +static PyGetSetDef pg_circle_getsets[] = { + {"x", (getter)pg_circle_getx, (setter)pg_circle_setx, DOC_CIRCLE_X, NULL}, + {"y", (getter)pg_circle_gety, (setter)pg_circle_sety, DOC_CIRCLE_Y, NULL}, + {"r", (getter)pg_circle_getr, (setter)pg_circle_setr, DOC_CIRCLE_R, NULL}, + {NULL, 0, NULL, NULL, NULL}}; + +static PyTypeObject pgCircle_Type = { + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "pygame.geometry.Circle", + .tp_basicsize = sizeof(pgCircleObject), + .tp_dealloc = (destructor)pg_circle_dealloc, + .tp_repr = (reprfunc)pg_circle_repr, + .tp_str = (reprfunc)pg_circle_str, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = DOC_CIRCLE, + .tp_weaklistoffset = offsetof(pgCircleObject, weakreflist), + .tp_methods = pg_circle_methods, + .tp_getset = pg_circle_getsets, + .tp_init = (initproc)pg_circle_init, + .tp_new = pg_circle_new, +}; diff --git a/src_c/doc/geometry_doc.h b/src_c/doc/geometry_doc.h new file mode 100644 index 0000000000..03cb419850 --- /dev/null +++ b/src_c/doc/geometry_doc.h @@ -0,0 +1,7 @@ +/* Auto generated file: with makeref.py . Docs go in docs/reST/ref/ . */ +#define DOC_GEOMETRY "pygame module for the Circle, Line, and Polygon objects" +#define DOC_CIRCLE "Circle((x, y), radius) -> Circle\nCircle(x, y, radius) -> Circle\npygame object for representing a circle" +#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_COPY "copy() -> Circle\nreturns a copy of the circle" diff --git a/src_c/geometry.c b/src_c/geometry.c new file mode 100644 index 0000000000..33b1fdb96e --- /dev/null +++ b/src_c/geometry.c @@ -0,0 +1,49 @@ +#include "geometry.h" +#include "circle.c" + +static PyMethodDef geometry_methods[] = {{NULL, NULL, 0, NULL}}; + +MODINIT_DEFINE(geometry) +{ + PyObject *module, *apiobj; + static void *c_api[PYGAMEAPI_GEOMETRY_NUMSLOTS]; + + static struct PyModuleDef _module = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "geometry", + .m_doc = "Module for the Line, Circle and Polygon objects\n", + .m_size = -1, + .m_methods = geometry_methods, + }; + + import_pygame_base(); + if (PyErr_Occurred()) { + return NULL; + } + + if (PyType_Ready(&pgCircle_Type) < 0) { + return NULL; + } + + module = PyModule_Create(&_module); + if (!module) { + return NULL; + } + + Py_INCREF(&pgCircle_Type); + if (PyModule_AddObject(module, "Circle", (PyObject *)&pgCircle_Type)) { + Py_DECREF(&pgCircle_Type); + Py_DECREF(module); + return NULL; + } + + c_api[0] = &pgCircle_Type; + apiobj = encapsulate_api(c_api, "geometry"); + if (PyModule_AddObject(module, PYGAMEAPI_LOCAL_ENTRY, apiobj)) { + Py_XDECREF(apiobj); + Py_DECREF(module); + return NULL; + } + + return module; +} diff --git a/src_c/geometry.h b/src_c/geometry.h new file mode 100644 index 0000000000..34d7ea0c4a --- /dev/null +++ b/src_c/geometry.h @@ -0,0 +1,28 @@ +#ifndef PYGAME_CE_GEOMETRY_H +#define PYGAME_CE_GEOMETRY_H + +#include "pygame.h" +#include "pgcompat.h" + +typedef struct { + double x, y, r; +} pgCircleBase; + +typedef struct { + PyObject_HEAD pgCircleBase circle; + PyObject *weakreflist; +} pgCircleObject; + +#define pgCircle_CAST(o) ((pgCircleObject *)(o)) +#define pgCircle_AsCircle(o) (pgCircle_CAST(o)->circle) +#define pgCircle_GETX(self) (pgCircle_CAST(self)->circle.x) +#define pgCircle_GETY(self) (pgCircle_CAST(self)->circle.y) +#define pgCircle_GETR(self) (pgCircle_CAST(self)->circle.r) +#define pgCircle_Check(o) ((o)->ob_type == &pgCircle_Type) + +static PyTypeObject pgCircle_Type; + +static int +pgCircle_FromObject(PyObject *obj, pgCircleBase *out); + +#endif // PYGAME_CE_GEOMETRY_H diff --git a/src_c/include/_pygame.h b/src_c/include/_pygame.h index 88d4fc0db0..a5ae05138c 100644 --- a/src_c/include/_pygame.h +++ b/src_c/include/_pygame.h @@ -116,6 +116,12 @@ typedef struct pg_bufferinfo_s { #define pg_TwoFloatsFromObj \ (*(int (*)(PyObject *, float *, float *))PYGAMEAPI_GET_SLOT(base, 7)) +#define pg_DoubleFromObj \ + (*(int (*)(PyObject *, double *))PYGAMEAPI_GET_SLOT(base, 24)) + +#define pg_TwoDoublesFromObj \ + (*(int (*)(PyObject *, double *, double *))PYGAMEAPI_GET_SLOT(base, 25)) + #define pg_UintFromObj \ (*(int (*)(PyObject *, Uint32 *))PYGAMEAPI_GET_SLOT(base, 8)) @@ -468,6 +474,12 @@ typedef struct pgColorObject pgColorObject; #define import_pygame_math() IMPORT_PYGAME_MODULE(math) #endif /* PYGAMEAPI_MATH_INTERNAL */ +#ifndef PYGAMEAPI_GEOMETRY_INTERNAL + +#define import_pygame_geometry() IMPORT_PYGAME_MODULE(geometry) + +#endif /* ~PYGAMEAPI_GEOMETRY_INTERNAL */ + /* * Window module */ @@ -502,6 +514,7 @@ PYGAMEAPI_DEFINE_SLOTS(pixelarray); PYGAMEAPI_DEFINE_SLOTS(color); PYGAMEAPI_DEFINE_SLOTS(math); PYGAMEAPI_DEFINE_SLOTS(_window); +PYGAMEAPI_DEFINE_SLOTS(geometry); #else /* ~PYGAME_H */ PYGAMEAPI_EXTERN_SLOTS(base); PYGAMEAPI_EXTERN_SLOTS(rect); @@ -515,6 +528,7 @@ PYGAMEAPI_EXTERN_SLOTS(pixelarray); PYGAMEAPI_EXTERN_SLOTS(color); PYGAMEAPI_EXTERN_SLOTS(math); PYGAMEAPI_EXTERN_SLOTS(_window); +PYGAMEAPI_EXTERN_SLOTS(geometry); #endif /* ~PYGAME_H */ diff --git a/src_c/static.c b/src_c/static.c index 03cc7c61da..602b52fa6e 100644 --- a/src_c/static.c +++ b/src_c/static.c @@ -22,6 +22,7 @@ #undef import_pygame_base #undef import_pygame_rect #undef import_pygame_surface +#undef import_pygame_geometry #undef import_pygame_color #undef import_pygame_bufferproxy #undef import_pygame_rwobject @@ -42,6 +43,11 @@ import_pygame_surface(void) { } +void +import_pygame_geometry(void) +{ +} + void import_pygame_color(void) { @@ -78,6 +84,8 @@ PyInit_version(void); PyMODINIT_FUNC PyInit_rect(void); PyMODINIT_FUNC +PyInit_geometry(void); +PyMODINIT_FUNC PyInit_surflock(void); PyMODINIT_FUNC PyInit_rwobject(void); @@ -269,6 +277,7 @@ PyInit_pygame_static() load_submodule("pygame", PyInit_key(), "key"); load_submodule("pygame", PyInit_rect(), "rect"); + load_submodule("pygame", PyInit_geometry(), "geometry"); load_submodule("pygame", PyInit_gfxdraw(), "gfxdraw"); load_submodule("pygame", PyInit_pg_time(), "time"); load_submodule("pygame", PyInit__freetype(), "_freetype"); @@ -381,6 +390,7 @@ PyInit_pygame_static() #include "time.c" #include "system.c" +#include "geometry.c" #include "_freetype.c" #include "freetype/ft_wrap.c" diff --git a/test/geometry_test.py b/test/geometry_test.py new file mode 100644 index 0000000000..39e95e0017 --- /dev/null +++ b/test/geometry_test.py @@ -0,0 +1,211 @@ +import unittest + +from pygame import Vector2 + +from pygame.geometry import Circle + + +class CircleTypeTest(unittest.TestCase): + def testConstruction_invalid_type(self): + """Checks whether passing wrong types to the constructor + raises the appropriate errors + """ + invalid_types = (None, [], "1", (1,), [1, 2, 3], Vector2(1, 1)) + + # Test x + for value in invalid_types: + with self.assertRaises(TypeError): + c = Circle(value, 0, 1) + # Test y + for value in invalid_types: + with self.assertRaises(TypeError): + c = Circle(0, value, 1) + # Test r + for value in invalid_types + (-1,): + with self.assertRaises(TypeError): + c = Circle(0, 0, value) + + def test2ndConstruction_invalid_type(self): + """Checks whether passing wrong types to the 2nd constructor + raises the appropriate errors + """ + invalid_types = (None, [], "1", (1,), [1, 2, 3], Vector2(1, 1)) + + # Test x + for value in invalid_types: + with self.assertRaises(TypeError): + c = Circle((value, 0), 1) + # Test y + for value in invalid_types: + with self.assertRaises(TypeError): + c = Circle((0, value), 1) + # Test r + for value in invalid_types + (-1,): + with self.assertRaises(TypeError): + c = Circle((0, 0), value) + + def testConstruction_invalid_arguments_number(self): + """Checks whether passing the wrong number of arguments to the constructor + raises the appropriate errors + """ + arguments = ( + (1,), # one non vec3 non circle arg + (1, 1, 1, 1), # four args + ) + + for arg_seq in arguments: + with self.assertRaises(TypeError): + c = Circle(*arg_seq) + + def testConstructionXYR_float(self): + c = Circle(1.0, 2.0, 3.0) + + self.assertEqual(1.0, c.x) + self.assertEqual(2.0, c.y) + self.assertEqual(3.0, c.r) + + def testConstructionTUP_XYR_float(self): + c = Circle((1.0, 2.0, 3.0)) + + self.assertEqual(1.0, c.x) + self.assertEqual(2.0, c.y) + self.assertEqual(3.0, c.r) + + def testConstructionXYR_int(self): + c = Circle(1, 2, 3) + + self.assertEqual(1.0, c.x) + self.assertEqual(2.0, c.y) + self.assertEqual(3.0, c.r) + + def testConstructionTUP_XYR_int(self): + c = Circle((1, 2, 3)) + + self.assertEqual(1.0, c.x) + self.assertEqual(2.0, c.y) + self.assertEqual(3.0, c.r) + + def test_x(self): + """Ensures changing the x attribute moves the circle and does not change + the circle's radius. + """ + expected_x = 10.0 + expected_y = 2.0 + expected_radius = 5.0 + c = Circle(1, expected_y, expected_radius) + + c.x = expected_x + + self.assertEqual(c.x, expected_x) + self.assertEqual(c.y, expected_y) + self.assertEqual(c.r, expected_radius) + + def test_x__invalid_value(self): + """Ensures the x attribute handles invalid values correctly.""" + c = Circle(0, 0, 1) + + for value in (None, [], "1", (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + c.x = value + + def test_x__del(self): + """Ensures the x attribute can't be deleted.""" + c = Circle(0, 0, 1) + + with self.assertRaises(AttributeError): + del c.x + + def test_y(self): + """Ensures changing the y attribute moves the circle and does not change + the circle's radius. + """ + expected_x = 10.0 + expected_y = 2.0 + expected_radius = 5.0 + c = Circle(expected_x, 1, expected_radius) + + c.y = expected_y + + self.assertEqual(c.x, expected_x) + self.assertEqual(c.y, expected_y) + self.assertEqual(c.r, expected_radius) + + def test_y__invalid_value(self): + """Ensures the y attribute handles invalid values correctly.""" + c = Circle(0, 0, 1) + + for value in (None, [], "1", (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + c.y = value + + def test_y__del(self): + """Ensures the y attribute can't be deleted.""" + c = Circle(0, 0, 1) + + with self.assertRaises(AttributeError): + del c.y + + def test_r(self): + """Ensures changing the r attribute changes the radius without moving the circle.""" + expected_x = 10.0 + expected_y = 2.0 + expected_radius = 5.0 + c = Circle(expected_x, expected_y, 1.0) + + c.r = expected_radius + + self.assertEqual(c.x, expected_x) + self.assertEqual(c.y, expected_y) + self.assertEqual(c.r, expected_radius) + + def test_r__invalid_value(self): + """Ensures the r attribute handles invalid values correctly.""" + c = Circle(0, 0, 1) + + for value in (None, [], "1", (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + c.r = value + + for value in (-10.3234, -1, 0, 0.0): + with self.assertRaises(ValueError): + c.r = value + + def test_r__del(self): + """Ensures the r attribute can't be deleted.""" + c = Circle(0, 0, 1) + + with self.assertRaises(AttributeError): + del c.r + + def test__str__(self): + """Checks whether the __str__ method works correctly.""" + c_str = "" + circle = Circle((10.3, 3.2), 4.3) + self.assertEqual(str(circle), c_str) + self.assertEqual(circle.__str__(), c_str) + + def test__repr__(self): + """Checks whether the __repr__ method works correctly.""" + c_repr = "" + circle = Circle((10.3, 3.2), 4.3) + self.assertEqual(repr(circle), c_repr) + self.assertEqual(circle.__repr__(), c_repr) + + def test_copy(self): + c = Circle(10, 10, 4) + # check 1 arg passed + with self.assertRaises(TypeError): + c.copy(10) + + # check copied circle has the same attribute values + c_2 = c.copy() + self.assertEqual(c.x, c_2.x) + self.assertEqual(c.y, c_2.y) + self.assertEqual(c.r, c_2.r) + + # check c2 is not c + self.assertIsNot(c_2, c) + + +if __name__ == "__main__": + unittest.main()