Skip to content

Commit ea33a4f

Browse files
itzpr3d4t0rEmc2356andrewhong04ScriptLineStudiosavaxar
authored
Add Circle move()/move_ip() (#2561)
* Added Circle.move()/Circle.move_ip() Co-authored-by: Emc2356 <63981925+emc2356@users.noreply.github.com> Co-authored-by: NovialRiptide <35881688+novialriptide@users.noreply.github.com> Co-authored-by: ScriptLineStudios <scriptlinestudios@protonmail.com> Co-authored-by: Avaxar <44055981+avaxar@users.noreply.github.com> Co-authored-by: maqa41 <amehebbet41@gmail.com> * improve double comparisons * small simplification * readd missing tests * format * properly handle not implemented comparisons and more tests * more tests --------- Co-authored-by: Emc2356 <63981925+emc2356@users.noreply.github.com> Co-authored-by: NovialRiptide <35881688+novialriptide@users.noreply.github.com> Co-authored-by: ScriptLineStudios <scriptlinestudios@protonmail.com> Co-authored-by: Avaxar <44055981+avaxar@users.noreply.github.com> Co-authored-by: maqa41 <amehebbet41@gmail.com>
1 parent f42f761 commit ea33a4f

File tree

5 files changed

+316
-0
lines changed

5 files changed

+316
-0
lines changed

buildconfig/stubs/pygame/geometry.pyi

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ class Circle:
6666
@overload
6767
def __init__(self, circle: _CircleValue) -> None: ...
6868
@overload
69+
def move(self, x: float, y: float) -> Circle: ...
70+
@overload
71+
def move(self, move_by: Coordinate) -> Circle: ...
72+
@overload
73+
def move_ip(self, x: float, y: float) -> None: ...
74+
@overload
75+
def move_ip(self, move_by: Coordinate) -> None: ...
76+
@overload
6977
def collidepoint(self, x: float, y: float) -> bool: ...
7078
@overload
7179
def collidepoint(self, point: Coordinate) -> bool: ...

docs/reST/ref/geometry.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,45 @@
176176

177177
.. ## Circle.collidecircle ##
178178
179+
.. method:: move
180+
181+
| :sl:`moves the circle by a given amount`
182+
| :sg:`move((x, y)) -> Circle`
183+
| :sg:`move(x, y) -> Circle`
184+
| :sg:`move(Vector2) -> Circle`
185+
186+
The `move` method allows you to create a new `Circle` object that is moved by a given
187+
offset from the original `Circle`. This is useful if you want to move a `Circle` without
188+
modifying the original. The move method takes either a tuple of (x, y) coordinates,
189+
two separate x and y coordinates, or a `Vector2` object as its argument, and returns
190+
a new `Circle` object with the updated position.
191+
192+
.. note::
193+
This method is equivalent(behaviour wise) to the following code:
194+
::
195+
Circle((circle.x + x, circle.y + y), circle.r)
196+
197+
.. ## Circle.move ##
198+
199+
.. method:: move_ip
200+
201+
| :sl:`moves the circle by a given amount, in place`
202+
| :sg:`move_ip((x, y)) -> None`
203+
| :sg:`move_ip(x, y) -> None`
204+
| :sg:`move_ip(Vector2) -> None`
205+
206+
The `move_ip` method is similar to the move method, but it moves the `Circle` in place,
207+
modifying the original `Circle` object. This method takes the same types of arguments
208+
as move, and it always returns None.
209+
210+
.. note::
211+
This method is equivalent(behaviour wise) to the following code:
212+
::
213+
circle.x += x
214+
circle.y += y
215+
216+
.. ## Circle.move_ip ##
217+
179218
.. method:: colliderect
180219

181220
| :sl:`checks if a rectangle intersects the circle`

src_c/circle.c

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@ _pg_circle_subtype_new(PyTypeObject *type, pgCircleBase *circle)
1313
return (PyObject *)circle_obj;
1414
}
1515

16+
static PyObject *
17+
_pg_circle_subtype_new3(PyTypeObject *type, double x, double y, double r)
18+
{
19+
pgCircleObject *circle_obj =
20+
(pgCircleObject *)pgCircle_Type.tp_new(type, NULL, NULL);
21+
22+
if (circle_obj) {
23+
circle_obj->circle.x = x;
24+
circle_obj->circle.y = y;
25+
circle_obj->circle.r = r;
26+
}
27+
return (PyObject *)circle_obj;
28+
}
29+
1630
static int
1731
_pg_circle_set_radius(PyObject *value, pgCircleBase *circle)
1832
{
@@ -261,6 +275,35 @@ pg_circle_str(pgCircleObject *self)
261275
return pg_circle_repr(self);
262276
}
263277

278+
static PyObject *
279+
pg_circle_move(pgCircleObject *self, PyObject *const *args, Py_ssize_t nargs)
280+
{
281+
double Dx, Dy;
282+
283+
if (!pg_TwoDoublesFromFastcallArgs(args, nargs, &Dx, &Dy)) {
284+
return RAISE(PyExc_TypeError, "move requires a pair of numbers");
285+
}
286+
287+
return _pg_circle_subtype_new3(Py_TYPE(self), self->circle.x + Dx,
288+
self->circle.y + Dy, self->circle.r);
289+
}
290+
291+
static PyObject *
292+
pg_circle_move_ip(pgCircleObject *self, PyObject *const *args,
293+
Py_ssize_t nargs)
294+
{
295+
double Dx, Dy;
296+
297+
if (!pg_TwoDoublesFromFastcallArgs(args, nargs, &Dx, &Dy)) {
298+
return RAISE(PyExc_TypeError, "move_ip requires a pair of numbers");
299+
}
300+
301+
self->circle.x += Dx;
302+
self->circle.y += Dy;
303+
304+
Py_RETURN_NONE;
305+
}
306+
264307
static PyObject *
265308
pg_circle_update(pgCircleObject *self, PyObject *const *args, Py_ssize_t nargs)
266309
{
@@ -350,6 +393,9 @@ static struct PyMethodDef pg_circle_methods[] = {
350393
DOC_CIRCLE_COLLIDEPOINT},
351394
{"collidecircle", (PyCFunction)pg_circle_collidecircle, METH_FASTCALL,
352395
DOC_CIRCLE_COLLIDECIRCLE},
396+
{"move", (PyCFunction)pg_circle_move, METH_FASTCALL, DOC_CIRCLE_MOVE},
397+
{"move_ip", (PyCFunction)pg_circle_move_ip, METH_FASTCALL,
398+
DOC_CIRCLE_MOVEIP},
353399
{"colliderect", (PyCFunction)pg_circle_colliderect, METH_FASTCALL,
354400
DOC_CIRCLE_COLLIDERECT},
355401
{"update", (PyCFunction)pg_circle_update, METH_FASTCALL,
@@ -548,6 +594,38 @@ pg_circle_setdiameter(pgCircleObject *self, PyObject *value, void *closure)
548594
return 0;
549595
}
550596

597+
static int
598+
double_compare(double a, double b)
599+
{
600+
/* Uses both a fixed epsilon and an adaptive epsilon */
601+
const double e = 1e-6;
602+
return fabs(a - b) < e || fabs(a - b) <= e * MAX(fabs(a), fabs(b));
603+
}
604+
605+
static PyObject *
606+
pg_circle_richcompare(PyObject *self, PyObject *other, int op)
607+
{
608+
pgCircleBase c1, c2;
609+
int equal;
610+
611+
if (!pgCircle_FromObject(self, &c1) || !pgCircle_FromObject(other, &c2)) {
612+
equal = 0;
613+
}
614+
else {
615+
equal = double_compare(c1.x, c2.x) && double_compare(c1.y, c2.y) &&
616+
double_compare(c1.r, c2.r);
617+
}
618+
619+
switch (op) {
620+
case Py_EQ:
621+
return PyBool_FromLong(equal);
622+
case Py_NE:
623+
return PyBool_FromLong(!equal);
624+
default:
625+
Py_RETURN_NOTIMPLEMENTED;
626+
}
627+
}
628+
551629
static PyGetSetDef pg_circle_getsets[] = {
552630
{"x", (getter)pg_circle_getx, (setter)pg_circle_setx, DOC_CIRCLE_X, NULL},
553631
{"y", (getter)pg_circle_gety, (setter)pg_circle_sety, DOC_CIRCLE_Y, NULL},
@@ -581,4 +659,5 @@ static PyTypeObject pgCircle_Type = {
581659
.tp_getset = pg_circle_getsets,
582660
.tp_init = (initproc)pg_circle_init,
583661
.tp_new = pg_circle_new,
662+
.tp_richcompare = pg_circle_richcompare,
584663
};

src_c/doc/geometry_doc.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#define DOC_CIRCLE_CIRCUMFERENCE "circumference -> float\ncircumference of the circle"
1212
#define DOC_CIRCLE_COLLIDEPOINT "collidepoint((x, y)) -> bool\ncollidepoint(x, y) -> bool\ncollidepoint(Vector2) -> bool\ntest if a point is inside the circle"
1313
#define DOC_CIRCLE_COLLIDECIRCLE "collidecircle(Circle) -> bool\ncollidecircle(x, y, radius) -> bool\ncollidecircle((x, y), radius) -> bool\ntest if two circles collide"
14+
#define DOC_CIRCLE_MOVE "move((x, y)) -> Circle\nmove(x, y) -> Circle\nmove(Vector2) -> Circle\nmoves the circle by a given amount"
15+
#define DOC_CIRCLE_MOVEIP "move_ip((x, y)) -> None\nmove_ip(x, y) -> None\nmove_ip(Vector2) -> None\nmoves the circle by a given amount, in place"
1416
#define DOC_CIRCLE_COLLIDERECT "colliderect(Rect) -> bool\ncolliderect((x, y, width, height)) -> bool\ncolliderect(x, y, width, height) -> bool\ncolliderect((x, y), (width, height)) -> bool\nchecks if a rectangle intersects the circle"
1517
#define DOC_CIRCLE_UPDATE "update((x, y), radius) -> None\nupdate(x, y, radius) -> None\nupdates the circle position and radius"
1618
#define DOC_CIRCLE_COPY "copy() -> Circle\nreturns a copy of the circle"

test/geometry_test.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,194 @@ def test_selfupdate(self):
696696
self.assertEqual(c.r, c_r)
697697
self.assertEqual(c.r_sqr, c_r_sqr)
698698

699+
def test_circle_richcompare(self):
700+
"""Ensures that the circle correctly compares itself to other circles"""
701+
c = Circle(0, 0, 10)
702+
c2 = Circle(0, 0, 10)
703+
c3 = Circle(0, 0, 5)
704+
c4 = Circle(0, 0, 20)
705+
706+
self.assertTrue(c == c2)
707+
self.assertFalse(c != c2)
708+
709+
self.assertFalse(c == c3)
710+
self.assertTrue(c != c3)
711+
712+
self.assertFalse(c == c4)
713+
self.assertTrue(c != c4)
714+
715+
# self compare
716+
self.assertTrue(c == c)
717+
self.assertFalse(c != c)
718+
719+
# not implemented compare
720+
with self.assertRaises(TypeError):
721+
c > c2
722+
with self.assertRaises(TypeError):
723+
c < c2
724+
with self.assertRaises(TypeError):
725+
c >= c2
726+
with self.assertRaises(TypeError):
727+
c <= c2
728+
729+
# invalid types
730+
invalid_types = (
731+
None,
732+
[],
733+
"1",
734+
(1,),
735+
Vector2(1, 1),
736+
1,
737+
0.2324,
738+
Rect(0, 0, 10, 10),
739+
True,
740+
)
741+
742+
for value in invalid_types:
743+
self.assertFalse(c == value)
744+
self.assertTrue(c != value)
745+
with self.assertRaises(TypeError):
746+
c > value
747+
with self.assertRaises(TypeError):
748+
c < value
749+
with self.assertRaises(TypeError):
750+
c >= value
751+
with self.assertRaises(TypeError):
752+
c <= value
753+
754+
def test_move_invalid_args(self):
755+
"""tests if the function correctly handles incorrect types as parameters"""
756+
invalid_types = (None, [], "1", (1,), Vector3(1, 1, 3), Circle(3, 3, 1))
757+
758+
c = Circle(10, 10, 4)
759+
760+
for value in invalid_types:
761+
with self.assertRaises(TypeError):
762+
c.move(value)
763+
764+
def test_move_argnum(self):
765+
c = Circle(10, 10, 4)
766+
767+
invalid_args = [(1, 1, 1), (1, 1, 1, 1)]
768+
769+
for arg in invalid_args:
770+
with self.assertRaises(TypeError):
771+
c.move(*arg)
772+
773+
def test_move_return_type(self):
774+
c = Circle(10, 10, 4)
775+
776+
class CircleSub(Circle):
777+
pass
778+
779+
cs = CircleSub(10, 10, 4)
780+
781+
self.assertIsInstance(c.move(1, 1), Circle)
782+
self.assertIsInstance(cs.move(1, 1), CircleSub)
783+
784+
def test_move(self):
785+
"""Ensures that moving the circle position correctly updates position"""
786+
c = Circle(0, 0, 3)
787+
788+
new_c = c.move(5, 5)
789+
790+
self.assertEqual(new_c.x, 5.0)
791+
self.assertEqual(new_c.y, 5.0)
792+
self.assertEqual(new_c.r, 3.0)
793+
self.assertEqual(new_c.r_sqr, 9.0)
794+
795+
new_c = new_c.move(-5, -10)
796+
797+
self.assertEqual(new_c.x, 0.0)
798+
self.assertEqual(new_c.y, -5.0)
799+
800+
def test_move_inplace(self):
801+
"""Ensures that moving the circle position by 0, 0 doesn't move the circle"""
802+
c = Circle(1, 1, 3)
803+
804+
c.move(0, 0)
805+
806+
self.assertEqual(c.x, 1.0)
807+
self.assertEqual(c.y, 1.0)
808+
self.assertEqual(c.r, 3.0)
809+
self.assertEqual(c.r_sqr, 9.0)
810+
811+
def test_move_equality(self):
812+
"""Ensures that moving the circle by 0, 0 will
813+
return a circle that's equal to the original"""
814+
c = Circle(1, 1, 3)
815+
816+
new_c = c.move(0, 0)
817+
818+
self.assertEqual(new_c, c)
819+
820+
def test_move_ip_invalid_args(self):
821+
"""tests if the function correctly handles incorrect types as parameters"""
822+
invalid_types = (None, [], "1", (1,), Vector3(1, 1, 3), Circle(3, 3, 1))
823+
824+
c = Circle(10, 10, 4)
825+
826+
for value in invalid_types:
827+
with self.assertRaises(TypeError):
828+
c.move_ip(value)
829+
830+
def test_move_ip_argnum(self):
831+
"""tests if the function correctly handles incorrect number of args"""
832+
c = Circle(10, 10, 4)
833+
834+
invalid_args = [(1, 1, 1), (1, 1, 1, 1)]
835+
836+
for arg in invalid_args:
837+
with self.assertRaises(TypeError):
838+
c.move_ip(*arg)
839+
840+
def test_move_ip(self):
841+
"""Ensures that moving the circle position correctly updates position"""
842+
c = Circle(0, 0, 3)
843+
844+
c.move_ip(5, 5)
845+
846+
self.assertEqual(c.x, 5.0)
847+
self.assertEqual(c.y, 5.0)
848+
self.assertEqual(c.r, 3.0)
849+
self.assertEqual(c.r_sqr, 9.0)
850+
851+
c.move_ip(-5, -10)
852+
self.assertEqual(c.x, 0.0)
853+
self.assertEqual(c.y, -5.0)
854+
855+
def test_move_ip_inplace(self):
856+
"""Ensures that moving the circle position by 0, 0 doesn't move the circle"""
857+
c = Circle(1, 1, 3)
858+
859+
c.move_ip(0, 0)
860+
861+
self.assertEqual(c.x, 1.0)
862+
self.assertEqual(c.y, 1.0)
863+
self.assertEqual(c.r, 3.0)
864+
self.assertEqual(c.r_sqr, 9.0)
865+
866+
def test_move_ip_equality(self):
867+
"""Ensures that moving the circle by 0, 0 will
868+
return a circle that's equal to the original"""
869+
c = Circle(1, 1, 3)
870+
871+
c.move_ip(0, 0)
872+
873+
self.assertEqual(c, Circle(1, 1, 3))
874+
875+
def test_move_ip_return_type(self):
876+
"""Ensures that the move_ip method returns None"""
877+
c = Circle(10, 10, 4)
878+
879+
class CircleSub(Circle):
880+
pass
881+
882+
cs = CircleSub(10, 10, 4)
883+
884+
self.assertEqual(type(c.move_ip(1, 1)), type(None))
885+
self.assertEqual(type(cs.move_ip(1, 1)), type(None))
886+
699887

700888
if __name__ == "__main__":
701889
unittest.main()

0 commit comments

Comments
 (0)