diff --git a/buildconfig/stubs/pygame/rect.pyi b/buildconfig/stubs/pygame/rect.pyi index 72bf30b73f..e631a3cdf7 100644 --- a/buildconfig/stubs/pygame/rect.pyi +++ b/buildconfig/stubs/pygame/rect.pyi @@ -7,6 +7,7 @@ from typing import ( Union, overload, Optional, + Tuple, ) from pygame.typing import Point, RectLike, SequenceLike @@ -85,6 +86,10 @@ class _GenericRect(Collection[_N]): @center.setter def center(self, value: Point) -> None: ... @property + def relcenter(self) -> Tuple[_N, _N]: ... + @relcenter.setter + def relcenter(self, value: Point) -> None: ... + @property def centerx(self) -> _N: ... @centerx.setter def centerx(self, value: float) -> None: ... diff --git a/docs/reST/ref/rect.rst b/docs/reST/ref/rect.rst index c774eb2f36..97d9013d19 100644 --- a/docs/reST/ref/rect.rst +++ b/docs/reST/ref/rect.rst @@ -47,7 +47,7 @@ top, left, bottom, right topleft, bottomleft, topright, bottomright midtop, midleft, midbottom, midright - center, centerx, centery + center, relcenter, centerx, centery size, width, height w,h @@ -93,6 +93,25 @@ and ``__new__()`` is assumed to take no arguments. So these methods should be overridden if any extra attributes need to be copied. + .. versionadded:: 2.5.2 + ``relcenter`` added to Rect / FRect. This will return you a ``Point`` of + the center relative to the topleft of the Rect. Setting a ``Point`` to it will + modify the size of the rect to 2 times the ``Point`` used. Below you can find a + code example of how it should work : + + .. code-block:: python + + > my_rect = pygame.Rect(0, 0, 2, 2) + > my_rect.relcenter + > (1, 1) + > my_rect.relcenter = (128, 128) + > my_rect.relcenter, my_rect.size + > ((128, 128), (256, 256)) + + Beware of non integer relative centers ! Using a Rect instead of FRect will round down + the values of the returned ``Point``. + + .. method:: copy | :sl:`copy the rectangle` diff --git a/src_c/rect.c b/src_c/rect.c index f3e22f69d8..751bbe3145 100644 --- a/src_c/rect.c +++ b/src_c/rect.c @@ -133,6 +133,8 @@ four_floats_from_obj(PyObject *obj, float *val1, float *val2, float *val3, #define RectExport_setmidright pg_rect_setmidright #define RectExport_getcenter pg_rect_getcenter #define RectExport_setcenter pg_rect_setcenter +#define RectExport_getrelcenter pg_rect_getrelcenter +#define RectExport_setrelcenter pg_rect_setrelcenter #define RectExport_getsize pg_rect_getsize #define RectExport_setsize pg_rect_setsize #define RectImport_primitiveType int @@ -250,6 +252,8 @@ four_floats_from_obj(PyObject *obj, float *val1, float *val2, float *val3, #define RectExport_setmidright pg_frect_setmidright #define RectExport_getcenter pg_frect_getcenter #define RectExport_setcenter pg_frect_setcenter +#define RectExport_getrelcenter pg_frect_getrelcenter +#define RectExport_setrelcenter pg_frect_setrelcenter #define RectExport_getsize pg_frect_getsize #define RectExport_setsize pg_frect_setsize #define RectImport_primitiveType float @@ -684,6 +688,8 @@ static PyGetSetDef pg_frect_getsets[] = { {"size", (getter)pg_frect_getsize, (setter)pg_frect_setsize, NULL, NULL}, {"center", (getter)pg_frect_getcenter, (setter)pg_frect_setcenter, NULL, NULL}, + {"relcenter", (getter)pg_frect_getrelcenter, (setter)pg_frect_setrelcenter, + NULL, NULL}, {"__safe_for_unpickling__", (getter)pg_rect_getsafepickle, NULL, NULL, NULL}, @@ -726,6 +732,8 @@ static PyGetSetDef pg_rect_getsets[] = { {"size", (getter)pg_rect_getsize, (setter)pg_rect_setsize, NULL, NULL}, {"center", (getter)pg_rect_getcenter, (setter)pg_rect_setcenter, NULL, NULL}, + {"relcenter", (getter)pg_rect_getrelcenter, (setter)pg_rect_setrelcenter, + NULL, NULL}, {"__safe_for_unpickling__", (getter)pg_rect_getsafepickle, NULL, NULL, NULL}, diff --git a/src_c/rect_impl.h b/src_c/rect_impl.h index 27df1bc315..04f586fb04 100644 --- a/src_c/rect_impl.h +++ b/src_c/rect_impl.h @@ -284,6 +284,12 @@ #ifndef RectExport_setcenter #error RectExport_setcenter needs to be defined #endif +#ifndef RectExport_getrelcenter +#error RectExport_getrelcenter needs to be defined +#endif +#ifndef RectExport_setrelcenter +#error RectExport_setrelcenter needs to be defined +#endif #ifndef RectExport_getsize #error RectExport_getsize needs to be defined #endif @@ -604,6 +610,10 @@ RectExport_getcenter(RectObject *self, void *closure); static int RectExport_setcenter(RectObject *self, PyObject *value, void *closure); static PyObject * +RectExport_getrelcenter(RectObject *self, void *closure); +static int +RectExport_setrelcenter(RectObject *self, PyObject *value, void *closure); +static PyObject * RectExport_getsize(RectObject *self, void *closure); static int RectExport_setsize(RectObject *self, PyObject *value, void *closure); @@ -2808,6 +2818,33 @@ RectExport_getcenter(RectObject *self, void *closure) self->r.y + (self->r.h / 2)); } +/*center*/ +static PyObject * +RectExport_getrelcenter(RectObject *self, void *closure) +{ + return TupleFromTwoPrimitives(self->r.w / 2, self->r.h / 2); +} + +static int +RectExport_setrelcenter(RectObject *self, PyObject *value, void *closure) +{ + PrimitiveType val1, val2; + + if (NULL == value) { + /* Attribute deletion not supported. */ + PyErr_SetString(PyExc_AttributeError, "can't delete attribute"); + return -1; + } + + if (!twoPrimitivesFromObj(value, &val1, &val2)) { + PyErr_SetString(PyExc_TypeError, "invalid rect assignment"); + return -1; + } + self->r.w = val1 * 2; + self->r.h = val2 * 2; + return 0; +} + static int RectExport_setcenter(RectObject *self, PyObject *value, void *closure) { @@ -2962,6 +2999,8 @@ RectExport_iterator(RectObject *self) #undef RectExport_setmidright #undef RectExport_getcenter #undef RectExport_setcenter +#undef RectExport_getrelcenter +#undef RectExport_setrelcenter #undef RectExport_getsize #undef RectExport_setsize #undef RectExport_iterator diff --git a/test/rect_test.py b/test/rect_test.py index 0ad1af77c3..0e183dd084 100644 --- a/test/rect_test.py +++ b/test/rect_test.py @@ -512,6 +512,35 @@ def test_center__del(self): with self.assertRaises(AttributeError): del r.center + def test_relcenter(self): + """Changing the relcenter attribute changes the rect's size and + does not move the rect. + """ + r = Rect(1, 2, 75, 45) + new_relcenter = (697, 345) + old_topleft = (r.left, r.top) + expected_size = (697 * 2, 345 * 2) + + r.relcenter = new_relcenter + self.assertEqual(new_relcenter, r.relcenter) + self.assertEqual(old_topleft, r.topleft) + self.assertEqual(expected_size, r.size) + + def test_relcenter__invalid_value(self): + """Ensures the relcenter attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", 1, (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.relcenter = value + + def test_relcenter__del(self): + """Ensures the center attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.relcenter + def test_midleft(self): """Changing the midleft attribute moves the rect and does not change the rect's size @@ -790,7 +819,9 @@ def test_inflate__smaller(self): and shrinks dimensions by correct values.""" r = Rect(2, 4, 6, 8) r2 = r.inflate(-4, -6) + expected_new_relcenter = r.w // 2 - 2, r.h // 2 - 3 + self.assertEqual(expected_new_relcenter, r2.relcenter) self.assertEqual(r.center, r2.center) self.assertEqual(r.left + 2, r2.left) self.assertEqual(r.top + 3, r2.top) @@ -820,7 +851,9 @@ def test_inflate_ip__smaller(self): r = Rect(2, 4, 6, 8) r2 = Rect(r) r2.inflate_ip(-4, -6) + expected_new_relcenter = r.w // 2 - 2, r.h // 2 - 3 + self.assertEqual(expected_new_relcenter, r2.relcenter) self.assertEqual(r.center, r2.center) self.assertEqual(r.left + 2, r2.left) self.assertEqual(r.top + 3, r2.top) @@ -833,7 +866,9 @@ def test_scale_by__larger_single_argument(self): """The scale method scales around the center of the rectangle""" r = Rect(2, 4, 6, 8) r2 = r.scale_by(2) + expected_new_relcenter = r.size + self.assertEqual(expected_new_relcenter, r2.relcenter) self.assertEqual(r.center, r2.center) self.assertEqual(r.left - 3, r2.left) self.assertEqual(r.top - 4, r2.top) @@ -847,7 +882,9 @@ def test_scale_by__larger_single_argument_kwarg(self): keyword arguments 'x' and 'y'""" r = Rect(2, 4, 6, 8) r2 = r.scale_by(x=2) + expected_new_relcenter = r.size + self.assertEqual(expected_new_relcenter, r2.relcenter) self.assertEqual(r.center, r2.center) self.assertEqual(r.left - 3, r2.left) self.assertEqual(r.top - 4, r2.top) @@ -860,7 +897,9 @@ def test_scale_by__smaller_single_argument(self): """The scale method scales around the center of the rectangle""" r = Rect(2, 4, 8, 8) r2 = r.scale_by(0.5) + expected_new_relcenter = r.w // 4, r.h // 4 + self.assertEqual(expected_new_relcenter, r2.relcenter) self.assertEqual(r.center, r2.center) self.assertEqual(r.left + 2, r2.left) self.assertEqual(r.top + 2, r2.top) @@ -876,6 +915,9 @@ def test_scale_by__larger(self): # act r2 = r.scale_by(2, 4) # assert + expected_new_relcenter = r.w, r.h * 2 + + self.assertEqual(expected_new_relcenter, r2.relcenter) self.assertEqual(r.center, r2.center) self.assertEqual(r.left - 3, r2.left) self.assertEqual(r.centery - r.h * 4 / 2, r2.top) @@ -896,6 +938,9 @@ def test_scale_by__larger_kwargs_scale_by(self): r3 = r.scale_by((2, 4)) self.assertEqual(r2, r3) # assert + expected_new_relcenter = r.w, r.h * 2 + + self.assertEqual(expected_new_relcenter, r2.relcenter) self.assertEqual(r.center, r2.center) self.assertEqual(r.left - 3, r2.left) self.assertEqual(r.centery - r.h * 4 / 2, r2.top) @@ -914,6 +959,9 @@ def test_scale_by__larger_kwargs(self): # act r2 = r.scale_by(x=2, y=4) # assert + expected_new_relcenter = r.w, r.h * 2 + + self.assertEqual(expected_new_relcenter, r2.relcenter) self.assertEqual(r.center, r2.center) self.assertEqual(r.left - 3, r2.left) self.assertEqual(r.centery - r.h * 4 / 2, r2.top) @@ -929,6 +977,9 @@ def test_scale_by__smaller(self): # act r2 = r.scale_by(0.5, 0.25) # assert + expected_new_relcenter = r.w // 4, r.h // 8 + + self.assertEqual(expected_new_relcenter, r2.relcenter) self.assertEqual(r.center, r2.center) self.assertEqual(r.left + 2, r2.left) self.assertEqual(r.centery - r.h / 4 / 2, r2.top) @@ -946,28 +997,43 @@ def test_scale_by__subzero(self): r.scale_by(0.00001) rx1 = r.scale_by(10, 1) + expected_new_relcenter = r.w * 5, r.h // 2 + + self.assertEqual(expected_new_relcenter, rx1.relcenter) self.assertEqual(r.centerx - r.w * 10 / 2, rx1.x) self.assertEqual(r.y, rx1.y) self.assertEqual(r.w * 10, rx1.w) self.assertEqual(r.h, rx1.h) rx2 = r.scale_by(-10, 1) + expected_new_relcenter = r.w * 5, r.h // 2 + + self.assertEqual(expected_new_relcenter, rx2.relcenter) self.assertEqual(rx1.x, rx2.x) self.assertEqual(rx1.y, rx2.y) self.assertEqual(rx1.w, rx2.w) self.assertEqual(rx1.h, rx2.h) ry1 = r.scale_by(1, 10) + expected_new_relcenter = r.w // 2, r.h * 5 + + self.assertEqual(expected_new_relcenter, ry1.relcenter) self.assertEqual(r.x, ry1.x) self.assertEqual(r.centery - r.h * 10 / 2, ry1.y) self.assertEqual(r.w, ry1.w) self.assertEqual(r.h * 10, ry1.h) ry2 = r.scale_by(1, -10) + expected_new_relcenter = r.w // 2, r.h * 5 + + self.assertEqual(expected_new_relcenter, ry2.relcenter) self.assertEqual(ry1.x, ry2.x) self.assertEqual(ry1.y, ry2.y) self.assertEqual(ry1.w, ry2.w) self.assertEqual(ry1.h, ry2.h) r1 = r.scale_by(10) + expected_new_relcenter = r.w * 5, r.h * 5 + + self.assertEqual(expected_new_relcenter, r1.relcenter) self.assertEqual(r.centerx - r.w * 10 / 2, r1.x) self.assertEqual(r.centery - r.h * 10 / 2, r1.y) self.assertEqual(r.w * 10, r1.w) @@ -1026,7 +1092,9 @@ def test_scale_by_ip__larger(self): r = Rect(2, 4, 6, 8) r2 = Rect(r) r2.scale_by_ip(2) + expected_new_relcenter = r.w, r.h + self.assertEqual(expected_new_relcenter, r2.relcenter) self.assertEqual(r.center, r2.center) self.assertEqual(r.left - 3, r2.left) self.assertEqual(r.top - 4, r2.top) @@ -1040,7 +1108,9 @@ def test_scale_by_ip__smaller(self): r = Rect(2, 4, 8, 8) r2 = Rect(r) r2.scale_by_ip(0.5) + expected_new_relcenter = r.w // 4, r.h // 4 + self.assertEqual(expected_new_relcenter, r2.relcenter) self.assertEqual(r.center, r2.center) self.assertEqual(r.left + 2, r2.left) self.assertEqual(r.top + 2, r2.top) @@ -1064,6 +1134,9 @@ def test_scale_by_ip__kwargs(self): r2.scale_by_ip(x=2, y=4) # assert + expected_new_relcenter = r.w, r.h * 2 + + self.assertEqual(expected_new_relcenter, r2.relcenter) self.assertEqual(r.center, r2.center) self.assertEqual(r.left - 3, r2.left) self.assertEqual(r.centery - r.h * 4 / 2, r2.top) @@ -3003,7 +3076,9 @@ def testCalculatedAttributes(self): midx = r.left + r.width / 2 midy = r.top + r.height / 2 + expected_new_relcenter = r.w / 2, r.h / 2 + self.assertEqual(expected_new_relcenter, r.relcenter) self.assertEqual(midx, r.centerx) self.assertEqual(midy, r.centery) self.assertEqual((r.centerx, r.centery), r.center) @@ -3031,7 +3106,9 @@ def test_scale_by__larger_single_argument(self): """The scale method scales around the center of the rectangle""" r = FRect(2.1, 4, 6, 8.9) r2 = r.scale_by(2.3) + expected_new_relcenter = r.w * 1.15, r.h * 1.15 + self.assertSeqAlmostEqual5(expected_new_relcenter, r2.relcenter) self.assertSeqAlmostEqual5(r.center, r2.center) # ((w * scaling) - w) / 2 -> 3.9 self.assertAlmostEqual5(r.left - 3.9, r2.left) @@ -3046,7 +3123,9 @@ def test_scale_by__larger_single_argument_kwarg(self): keyword arguments 'x' and 'y'""" r = FRect(2.1, 4, 6, 8.9) r2 = r.scale_by(x=2.3) + expected_new_relcenter = r.w * 1.15, r.h * 1.15 + self.assertSeqAlmostEqual5(expected_new_relcenter, r2.relcenter) self.assertSeqAlmostEqual5(r.center, r2.center) # ((w * scaling) - w) / 2 -> 3.9 self.assertAlmostEqual5(r.left - 3.9, r2.left) @@ -3060,7 +3139,9 @@ def test_scale_by__smaller_single_argument(self): """The scale method scales around the center of the rectangle""" r = FRect(2.1, 4, 6, 8.9) r2 = r.scale_by(0.5) + expected_new_relcenter = r.w * 0.25, r.h * 0.25 + self.assertSeqAlmostEqual5(expected_new_relcenter, r2.relcenter) self.assertSeqAlmostEqual5(r.center, r2.center) self.assertAlmostEqual5(r.left + 1.5, r2.left) self.assertAlmostEqual5(r.top + 2.225, r2.top) @@ -3076,6 +3157,9 @@ def test_scale_by__larger(self): # act r2 = r.scale_by(2, 4) # assert + expected_new_relcenter = r.w, r.h * 2 + + self.assertSeqAlmostEqual5(expected_new_relcenter, r2.relcenter) self.assertSeqAlmostEqual5(r.center, r2.center) self.assertAlmostEqual5(r.left - 3, r2.left) self.assertAlmostEqual5(r.centery - r.h * 4 / 2, r2.top) @@ -3096,6 +3180,9 @@ def test_scale_by__larger_kwargs_scale_by(self): r3 = r.scale_by((2, 4)) self.assertEqual(r2, r3) # assert + expected_new_relcenter = r.w, r.h * 2 + + self.assertSeqAlmostEqual5(expected_new_relcenter, r2.relcenter) self.assertSeqAlmostEqual5(r.center, r2.center) self.assertAlmostEqual5(r.left - 3, r2.left) self.assertAlmostEqual5(r.centery - r.h * 4 / 2, r2.top) @@ -3114,6 +3201,9 @@ def test_scale_by__larger_kwargs(self): # act r2 = r.scale_by(x=2, y=4) # assert + expected_new_relcenter = r.w, r.h * 2 + + self.assertSeqAlmostEqual5(expected_new_relcenter, r2.relcenter) self.assertSeqAlmostEqual5(r.center, r2.center) self.assertAlmostEqual5(r.left - 3, r2.left) self.assertAlmostEqual5(r.centery - r.h * 4 / 2, r2.top) @@ -3129,6 +3219,9 @@ def test_scale_by__smaller(self): # act r2 = r.scale_by(0.5, 0.25) # assert + expected_new_relcenter = r.w * 0.25, r.h * 0.125 + + self.assertSeqAlmostEqual5(expected_new_relcenter, r2.relcenter) self.assertSeqAlmostEqual5(r.center, r2.center) self.assertAlmostEqual5(r.left + 1.5, r2.left) self.assertAlmostEqual5(r.centery - r.h / 4 / 2, r2.top) @@ -3146,28 +3239,45 @@ def test_scale_by__subzero(self): r.scale_by(0.00001) rx1 = r.scale_by(10, 1) + expected_new_relcenter = r.w * 5, r.h * 0.5 + + self.assertSeqAlmostEqual5(expected_new_relcenter, rx1.relcenter) self.assertAlmostEqual5(r.centerx - r.w * 10 / 2, rx1.x) self.assertAlmostEqual5(r.y, rx1.y) self.assertAlmostEqual5(r.w * 10, rx1.w) self.assertAlmostEqual5(r.h, rx1.h) + rx2 = r.scale_by(-10, 1) + expected_new_relcenter = r.w * 5, r.h * 0.5 + + self.assertSeqAlmostEqual5(expected_new_relcenter, rx2.relcenter) self.assertAlmostEqual5(rx1.x, rx2.x) self.assertAlmostEqual5(rx1.y, rx2.y) self.assertAlmostEqual5(rx1.w, rx2.w) self.assertAlmostEqual5(rx1.h, rx2.h) ry1 = r.scale_by(1, 10) + expected_new_relcenter = r.w * 0.5, r.h * 5 + + self.assertSeqAlmostEqual5(expected_new_relcenter, ry1.relcenter) self.assertAlmostEqual5(r.x, ry1.x) self.assertAlmostEqual5(r.centery - r.h * 10 / 2, ry1.y) self.assertAlmostEqual5(r.w, ry1.w) self.assertAlmostEqual5(r.h * 10, ry1.h) + ry2 = r.scale_by(1, -10) + expected_new_relcenter = r.w * 0.5, r.h * 5 + + self.assertSeqAlmostEqual5(expected_new_relcenter, ry2.relcenter) self.assertAlmostEqual5(ry1.x, ry2.x) self.assertAlmostEqual5(ry1.y, ry2.y) self.assertAlmostEqual5(ry1.w, ry2.w) self.assertAlmostEqual5(ry1.h, ry2.h) r1 = r.scale_by(10) + expected_new_relcenter = r.w * 5, r.h * 5 + + self.assertSeqAlmostEqual5(expected_new_relcenter, r1.relcenter) self.assertAlmostEqual5(r.centerx - r.w * 10 / 2, r1.x) self.assertAlmostEqual5(r.centery - r.h * 10 / 2, r1.y) self.assertAlmostEqual5(r.w * 10, r1.w) @@ -3226,7 +3336,9 @@ def test_scale_by_ip__larger(self): r = FRect(2.1, 4, 6, 8.9) r2 = FRect(r) r2.scale_by_ip(2.3) + expected_new_relcenter = r.w * 1.15, r.h * 1.15 + self.assertSeqAlmostEqual5(expected_new_relcenter, r2.relcenter) self.assertSeqAlmostEqual5(r.center, r2.center) # ((w * scaling) - w) / 2 -> 3.9 self.assertAlmostEqual5(r.left - 3.9, r2.left) @@ -3241,7 +3353,9 @@ def test_scale_by_ip__smaller(self): r = FRect(2.1, 4, 6, 8.9) r2 = FRect(r) r2.scale_by_ip(0.5) + expected_new_relcenter = r.w * 0.25, r.h * 0.25 + self.assertSeqAlmostEqual5(expected_new_relcenter, r2.relcenter) self.assertSeqAlmostEqual5(r.center, r2.center) self.assertAlmostEqual5(r.left + 1.5, r2.left) self.assertAlmostEqual5(r.top + 2.225, r2.top) @@ -3265,6 +3379,9 @@ def test_scale_by_ip__kwargs(self): r2.scale_by_ip(x=2, y=4) # assert + expected_new_relcenter = r.w, r.h * 2.0 + + self.assertSeqAlmostEqual5(expected_new_relcenter, r2.relcenter) self.assertEqual(r.center, r2.center) self.assertAlmostEqual5(r.left - 3, r2.left) self.assertAlmostEqual5(r.centery - r.h * 4 / 2, r2.top)