From c12d293c5f79399daa44eb19a6f7e666011009cc Mon Sep 17 00:00:00 2001 From: bilhox <69472620+bilhox@users.noreply.github.com> Date: Sun, 1 Sep 2024 00:07:20 +0200 Subject: [PATCH 1/2] rel_center --- buildconfig/stubs/pygame/rect.pyi | 4 + docs/reST/ref/rect.rst | 21 +++++- src_c/rect.c | 8 ++ src_c/rect_impl.h | 39 ++++++++++ test/rect_test.py | 117 ++++++++++++++++++++++++++++++ 5 files changed, 188 insertions(+), 1 deletion(-) diff --git a/buildconfig/stubs/pygame/rect.pyi b/buildconfig/stubs/pygame/rect.pyi index 9899c84270..91efee8e1b 100644 --- a/buildconfig/stubs/pygame/rect.pyi +++ b/buildconfig/stubs/pygame/rect.pyi @@ -93,6 +93,10 @@ class _GenericRect(Collection[_N]): @center.setter def center(self, value: Coordinate) -> None: ... @property + def rel_center(self) -> Tuple[_N, _N]: ... + @rel_center.setter + def rel_center(self, value: Coordinate) -> 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 a936683a10..82738524ac 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, rel_center, centerx, centery size, width, height w,h @@ -92,6 +92,25 @@ However, the subclass's ``__init__()`` method is not called, 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 + ``rel_center`` added to Rect / FRect. This will return you ``Coordinate`` of + the center relative to the topleft of the Rect. Setting a ``Coordinate`` to it will + modify the size of the rect to 2 times the ``Coordinate`` 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.rel_center + > (1, 1) + > my_rect.rel_center = (128, 128) + > my_rect.rel_center, 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 ``Coordinate``. + .. method:: copy diff --git a/src_c/rect.c b/src_c/rect.c index f3e22f69d8..29fe347e2c 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}, + {"rel_center", (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}, + {"rel_center", (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 c918b0db8c..d4332cca75 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); @@ -2804,6 +2814,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) { @@ -2958,6 +2995,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 dc64cc9e11..a0f7561081 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_rel_center(self): + """Changing the rel_center attribute changes the rect's size and + does not move the rect. + """ + r = Rect(1, 2, 75, 45) + new_rel_center = (697, 345) + old_topleft = (r.left, r.top) + expected_size = (697 * 2, 345 * 2) + + r.rel_center = new_rel_center + self.assertEqual(new_rel_center, r.rel_center) + self.assertEqual(old_topleft, r.topleft) + self.assertEqual(expected_size, r.size) + + def test_rel_center__invalid_value(self): + """Ensures the rel_center 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.rel_center = value + + def test_rel_center__del(self): + """Ensures the center attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.rel_center + def test_midleft(self): """Changing the midleft attribute moves the rect and does not change the rect's size @@ -788,7 +817,9 @@ def test_inflate__smaller(self): """The inflate method inflates around the center of the rectangle""" r = Rect(2, 4, 6, 8) r2 = r.inflate(-4, -6) + expected_new_rel_center = r.w // 2 - 2, r.h // 2 - 3 + self.assertEqual(expected_new_rel_center, r2.rel_center) self.assertEqual(r.center, r2.center) self.assertEqual(r.left + 2, r2.left) self.assertEqual(r.top + 3, r2.top) @@ -816,7 +847,9 @@ def test_inflate_ip__smaller(self): r = Rect(2, 4, 6, 8) r2 = Rect(r) r2.inflate_ip(-4, -6) + expected_new_rel_center = r.w // 2 - 2, r.h // 2 - 3 + self.assertEqual(expected_new_rel_center, r2.rel_center) self.assertEqual(r.center, r2.center) self.assertEqual(r.left + 2, r2.left) self.assertEqual(r.top + 3, r2.top) @@ -829,7 +862,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_rel_center = r.size + self.assertEqual(expected_new_rel_center, r2.rel_center) self.assertEqual(r.center, r2.center) self.assertEqual(r.left - 3, r2.left) self.assertEqual(r.top - 4, r2.top) @@ -843,7 +878,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_rel_center = r.size + self.assertEqual(expected_new_rel_center, r2.rel_center) self.assertEqual(r.center, r2.center) self.assertEqual(r.left - 3, r2.left) self.assertEqual(r.top - 4, r2.top) @@ -856,7 +893,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_rel_center = r.w // 4, r.h // 4 + self.assertEqual(expected_new_rel_center, r2.rel_center) self.assertEqual(r.center, r2.center) self.assertEqual(r.left + 2, r2.left) self.assertEqual(r.top + 2, r2.top) @@ -872,6 +911,9 @@ def test_scale_by__larger(self): # act r2 = r.scale_by(2, 4) # assert + expected_new_rel_center = r.w, r.h * 2 + + self.assertEqual(expected_new_rel_center, r2.rel_center) self.assertEqual(r.center, r2.center) self.assertEqual(r.left - 3, r2.left) self.assertEqual(r.centery - r.h * 4 / 2, r2.top) @@ -890,6 +932,9 @@ def test_scale_by__larger_kwargs_scale_by(self): # act r2 = r.scale_by(scale_by=(2, 4)) # assert + expected_new_rel_center = r.w, r.h * 2 + + self.assertEqual(expected_new_rel_center, r2.rel_center) self.assertEqual(r.center, r2.center) self.assertEqual(r.left - 3, r2.left) self.assertEqual(r.centery - r.h * 4 / 2, r2.top) @@ -908,6 +953,9 @@ def test_scale_by__larger_kwargs(self): # act r2 = r.scale_by(x=2, y=4) # assert + expected_new_rel_center = r.w, r.h * 2 + + self.assertEqual(expected_new_rel_center, r2.rel_center) self.assertEqual(r.center, r2.center) self.assertEqual(r.left - 3, r2.left) self.assertEqual(r.centery - r.h * 4 / 2, r2.top) @@ -923,6 +971,9 @@ def test_scale_by__smaller(self): # act r2 = r.scale_by(0.5, 0.25) # assert + expected_new_rel_center = r.w // 4, r.h // 8 + + self.assertEqual(expected_new_rel_center, r2.rel_center) self.assertEqual(r.center, r2.center) self.assertEqual(r.left + 2, r2.left) self.assertEqual(r.centery - r.h / 4 / 2, r2.top) @@ -940,28 +991,43 @@ def test_scale_by__subzero(self): r.scale_by(0.00001) rx1 = r.scale_by(10, 1) + expected_new_rel_center = r.w * 5, r.h // 2 + + self.assertEqual(expected_new_rel_center, rx1.rel_center) 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_rel_center = r.w * 5, r.h // 2 + + self.assertEqual(expected_new_rel_center, rx2.rel_center) 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_rel_center = r.w // 2, r.h * 5 + + self.assertEqual(expected_new_rel_center, ry1.rel_center) 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_rel_center = r.w // 2, r.h * 5 + + self.assertEqual(expected_new_rel_center, ry2.rel_center) 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_rel_center = r.w * 5, r.h * 5 + + self.assertEqual(expected_new_rel_center, r1.rel_center) 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) @@ -1020,7 +1086,9 @@ def test_scale_by_ip__larger(self): r = Rect(2, 4, 6, 8) r2 = Rect(r) r2.scale_by_ip(2) + expected_new_rel_center = r.w, r.h + self.assertEqual(expected_new_rel_center, r2.rel_center) self.assertEqual(r.center, r2.center) self.assertEqual(r.left - 3, r2.left) self.assertEqual(r.top - 4, r2.top) @@ -1034,7 +1102,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_rel_center = r.w // 4, r.h // 4 + self.assertEqual(expected_new_rel_center, r2.rel_center) self.assertEqual(r.center, r2.center) self.assertEqual(r.left + 2, r2.left) self.assertEqual(r.top + 2, r2.top) @@ -1058,6 +1128,9 @@ def test_scale_by_ip__kwargs(self): r2.scale_by_ip(x=2, y=4) # assert + expected_new_rel_center = r.w, r.h * 2 + + self.assertEqual(expected_new_rel_center, r2.rel_center) self.assertEqual(r.center, r2.center) self.assertEqual(r.left - 3, r2.left) self.assertEqual(r.centery - r.h * 4 / 2, r2.top) @@ -2997,7 +3070,9 @@ def testCalculatedAttributes(self): midx = r.left + r.width / 2 midy = r.top + r.height / 2 + expected_new_rel_center = r.w / 2, r.h / 2 + self.assertEqual(expected_new_rel_center, r.rel_center) self.assertEqual(midx, r.centerx) self.assertEqual(midy, r.centery) self.assertEqual((r.centerx, r.centery), r.center) @@ -3025,7 +3100,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_rel_center = r.w * 1.15, r.h * 1.15 + self.assertSeqAlmostEqual5(expected_new_rel_center, r2.rel_center) self.assertSeqAlmostEqual5(r.center, r2.center) # ((w * scaling) - w) / 2 -> 3.9 self.assertAlmostEqual5(r.left - 3.9, r2.left) @@ -3040,7 +3117,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_rel_center = r.w * 1.15, r.h * 1.15 + self.assertSeqAlmostEqual5(expected_new_rel_center, r2.rel_center) self.assertSeqAlmostEqual5(r.center, r2.center) # ((w * scaling) - w) / 2 -> 3.9 self.assertAlmostEqual5(r.left - 3.9, r2.left) @@ -3054,7 +3133,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_rel_center = r.w * 0.25, r.h * 0.25 + self.assertSeqAlmostEqual5(expected_new_rel_center, r2.rel_center) self.assertSeqAlmostEqual5(r.center, r2.center) self.assertAlmostEqual5(r.left + 1.5, r2.left) self.assertAlmostEqual5(r.top + 2.225, r2.top) @@ -3070,6 +3151,9 @@ def test_scale_by__larger(self): # act r2 = r.scale_by(2, 4) # assert + expected_new_rel_center = r.w, r.h * 2 + + self.assertSeqAlmostEqual5(expected_new_rel_center, r2.rel_center) self.assertSeqAlmostEqual5(r.center, r2.center) self.assertAlmostEqual5(r.left - 3, r2.left) self.assertAlmostEqual5(r.centery - r.h * 4 / 2, r2.top) @@ -3088,6 +3172,9 @@ def test_scale_by__larger_kwargs_scale_by(self): # act r2 = r.scale_by(scale_by=(2, 4)) # assert + expected_new_rel_center = r.w, r.h * 2 + + self.assertSeqAlmostEqual5(expected_new_rel_center, r2.rel_center) self.assertSeqAlmostEqual5(r.center, r2.center) self.assertAlmostEqual5(r.left - 3, r2.left) self.assertAlmostEqual5(r.centery - r.h * 4 / 2, r2.top) @@ -3106,6 +3193,9 @@ def test_scale_by__larger_kwargs(self): # act r2 = r.scale_by(x=2, y=4) # assert + expected_new_rel_center = r.w, r.h * 2 + + self.assertSeqAlmostEqual5(expected_new_rel_center, r2.rel_center) self.assertSeqAlmostEqual5(r.center, r2.center) self.assertAlmostEqual5(r.left - 3, r2.left) self.assertAlmostEqual5(r.centery - r.h * 4 / 2, r2.top) @@ -3121,6 +3211,9 @@ def test_scale_by__smaller(self): # act r2 = r.scale_by(0.5, 0.25) # assert + expected_new_rel_center = r.w * 0.25, r.h * 0.125 + + self.assertSeqAlmostEqual5(expected_new_rel_center, r2.rel_center) self.assertSeqAlmostEqual5(r.center, r2.center) self.assertAlmostEqual5(r.left + 1.5, r2.left) self.assertAlmostEqual5(r.centery - r.h / 4 / 2, r2.top) @@ -3138,28 +3231,45 @@ def test_scale_by__subzero(self): r.scale_by(0.00001) rx1 = r.scale_by(10, 1) + expected_new_rel_center = r.w * 5, r.h * 0.5 + + self.assertSeqAlmostEqual5(expected_new_rel_center, rx1.rel_center) 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_rel_center = r.w * 5, r.h * 0.5 + + self.assertSeqAlmostEqual5(expected_new_rel_center, rx2.rel_center) 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_rel_center = r.w * 0.5, r.h * 5 + + self.assertSeqAlmostEqual5(expected_new_rel_center, ry1.rel_center) 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_rel_center = r.w * 0.5, r.h * 5 + + self.assertSeqAlmostEqual5(expected_new_rel_center, ry2.rel_center) 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_rel_center = r.w * 5, r.h * 5 + + self.assertSeqAlmostEqual5(expected_new_rel_center, r1.rel_center) 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) @@ -3218,7 +3328,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_rel_center = r.w * 1.15, r.h * 1.15 + self.assertSeqAlmostEqual5(expected_new_rel_center, r2.rel_center) self.assertSeqAlmostEqual5(r.center, r2.center) # ((w * scaling) - w) / 2 -> 3.9 self.assertAlmostEqual5(r.left - 3.9, r2.left) @@ -3233,7 +3345,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_rel_center = r.w * 0.25, r.h * 0.25 + self.assertSeqAlmostEqual5(expected_new_rel_center, r2.rel_center) self.assertSeqAlmostEqual5(r.center, r2.center) self.assertAlmostEqual5(r.left + 1.5, r2.left) self.assertAlmostEqual5(r.top + 2.225, r2.top) @@ -3257,6 +3371,9 @@ def test_scale_by_ip__kwargs(self): r2.scale_by_ip(x=2, y=4) # assert + expected_new_rel_center = r.w, r.h * 2.0 + + self.assertSeqAlmostEqual5(expected_new_rel_center, r2.rel_center) self.assertEqual(r.center, r2.center) self.assertAlmostEqual5(r.left - 3, r2.left) self.assertAlmostEqual5(r.centery - r.h * 4 / 2, r2.top) From cc12f81f61fc773f37c64b7497f55a6516132f45 Mon Sep 17 00:00:00 2001 From: bilhox <69472620+bilhox@users.noreply.github.com> Date: Sun, 1 Sep 2024 00:16:59 +0200 Subject: [PATCH 2/2] fixed tyo --- docs/reST/ref/rect.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reST/ref/rect.rst b/docs/reST/ref/rect.rst index 82738524ac..fe8ea6b7c7 100644 --- a/docs/reST/ref/rect.rst +++ b/docs/reST/ref/rect.rst @@ -94,7 +94,7 @@ overridden if any extra attributes need to be copied. .. versionadded:: 2.5.2 - ``rel_center`` added to Rect / FRect. This will return you ``Coordinate`` of + ``rel_center`` added to Rect / FRect. This will return you a ``Coordinate`` of the center relative to the topleft of the Rect. Setting a ``Coordinate`` to it will modify the size of the rect to 2 times the ``Coordinate`` used. Below you can find a code example of how it should work :