Skip to content

Add Circle move()/move_ip() #2561

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions buildconfig/stubs/pygame/geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ class Circle:
@overload
def __init__(self, circle: _CircleValue) -> None: ...
@overload
def move(self, x: float, y: float) -> Circle: ...
@overload
def move(self, move_by: Coordinate) -> Circle: ...
@overload
def move_ip(self, x: float, y: float) -> None: ...
@overload
def move_ip(self, move_by: Coordinate) -> None: ...
@overload
def collidepoint(self, x: float, y: float) -> bool: ...
@overload
def collidepoint(self, point: Coordinate) -> bool: ...
Expand Down
39 changes: 39 additions & 0 deletions docs/reST/ref/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,45 @@

.. ## Circle.collidecircle ##

.. method:: move

| :sl:`moves the circle by a given amount`
| :sg:`move((x, y)) -> Circle`
| :sg:`move(x, y) -> Circle`
| :sg:`move(Vector2) -> Circle`

The `move` method allows you to create a new `Circle` object that is moved by a given
offset from the original `Circle`. This is useful if you want to move a `Circle` without
modifying the original. The move method takes either a tuple of (x, y) coordinates,
two separate x and y coordinates, or a `Vector2` object as its argument, and returns
a new `Circle` object with the updated position.

.. note::
This method is equivalent(behaviour wise) to the following code:
::
Circle((circle.x + x, circle.y + y), circle.r)

.. ## Circle.move ##

.. method:: move_ip

| :sl:`moves the circle by a given amount, in place`
| :sg:`move_ip((x, y)) -> None`
| :sg:`move_ip(x, y) -> None`
| :sg:`move_ip(Vector2) -> None`

The `move_ip` method is similar to the move method, but it moves the `Circle` in place,
modifying the original `Circle` object. This method takes the same types of arguments
as move, and it always returns None.

.. note::
This method is equivalent(behaviour wise) to the following code:
::
circle.x += x
circle.y += y

.. ## Circle.move_ip ##

.. method:: colliderect

| :sl:`checks if a rectangle intersects the circle`
Expand Down
79 changes: 79 additions & 0 deletions src_c/circle.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@ _pg_circle_subtype_new(PyTypeObject *type, pgCircleBase *circle)
return (PyObject *)circle_obj;
}

static PyObject *
_pg_circle_subtype_new3(PyTypeObject *type, double x, double y, double r)
{
pgCircleObject *circle_obj =
(pgCircleObject *)pgCircle_Type.tp_new(type, NULL, NULL);

if (circle_obj) {
circle_obj->circle.x = x;
circle_obj->circle.y = y;
circle_obj->circle.r = r;
}
return (PyObject *)circle_obj;
}

static int
_pg_circle_set_radius(PyObject *value, pgCircleBase *circle)
{
Expand Down Expand Up @@ -261,6 +275,35 @@ pg_circle_str(pgCircleObject *self)
return pg_circle_repr(self);
}

static PyObject *
pg_circle_move(pgCircleObject *self, PyObject *const *args, Py_ssize_t nargs)
{
double Dx, Dy;

if (!pg_TwoDoublesFromFastcallArgs(args, nargs, &Dx, &Dy)) {
return RAISE(PyExc_TypeError, "move requires a pair of numbers");
}

return _pg_circle_subtype_new3(Py_TYPE(self), self->circle.x + Dx,
self->circle.y + Dy, self->circle.r);
}

static PyObject *
pg_circle_move_ip(pgCircleObject *self, PyObject *const *args,
Py_ssize_t nargs)
{
double Dx, Dy;

if (!pg_TwoDoublesFromFastcallArgs(args, nargs, &Dx, &Dy)) {
return RAISE(PyExc_TypeError, "move_ip requires a pair of numbers");
}

self->circle.x += Dx;
self->circle.y += Dy;

Py_RETURN_NONE;
}

static PyObject *
pg_circle_update(pgCircleObject *self, PyObject *const *args, Py_ssize_t nargs)
{
Expand Down Expand Up @@ -350,6 +393,9 @@ static struct PyMethodDef pg_circle_methods[] = {
DOC_CIRCLE_COLLIDEPOINT},
{"collidecircle", (PyCFunction)pg_circle_collidecircle, METH_FASTCALL,
DOC_CIRCLE_COLLIDECIRCLE},
{"move", (PyCFunction)pg_circle_move, METH_FASTCALL, DOC_CIRCLE_MOVE},
{"move_ip", (PyCFunction)pg_circle_move_ip, METH_FASTCALL,
DOC_CIRCLE_MOVEIP},
{"colliderect", (PyCFunction)pg_circle_colliderect, METH_FASTCALL,
DOC_CIRCLE_COLLIDERECT},
{"update", (PyCFunction)pg_circle_update, METH_FASTCALL,
Expand Down Expand Up @@ -548,6 +594,38 @@ pg_circle_setdiameter(pgCircleObject *self, PyObject *value, void *closure)
return 0;
}

static int
double_compare(double a, double b)
{
/* Uses both a fixed epsilon and an adaptive epsilon */
const double e = 1e-6;
return fabs(a - b) < e || fabs(a - b) <= e * MAX(fabs(a), fabs(b));
}

static PyObject *
pg_circle_richcompare(PyObject *self, PyObject *other, int op)
{
pgCircleBase c1, c2;
int equal;

if (!pgCircle_FromObject(self, &c1) || !pgCircle_FromObject(other, &c2)) {
equal = 0;
}
else {
equal = double_compare(c1.x, c2.x) && double_compare(c1.y, c2.y) &&
double_compare(c1.r, c2.r);
}

switch (op) {
case Py_EQ:
return PyBool_FromLong(equal);
case Py_NE:
return PyBool_FromLong(!equal);
default:
Py_RETURN_NOTIMPLEMENTED;
}
}

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},
Expand Down Expand Up @@ -581,4 +659,5 @@ static PyTypeObject pgCircle_Type = {
.tp_getset = pg_circle_getsets,
.tp_init = (initproc)pg_circle_init,
.tp_new = pg_circle_new,
.tp_richcompare = pg_circle_richcompare,
};
2 changes: 2 additions & 0 deletions src_c/doc/geometry_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
#define DOC_CIRCLE_CIRCUMFERENCE "circumference -> float\ncircumference of the circle"
#define DOC_CIRCLE_COLLIDEPOINT "collidepoint((x, y)) -> bool\ncollidepoint(x, y) -> bool\ncollidepoint(Vector2) -> bool\ntest if a point is inside the circle"
#define DOC_CIRCLE_COLLIDECIRCLE "collidecircle(Circle) -> bool\ncollidecircle(x, y, radius) -> bool\ncollidecircle((x, y), radius) -> bool\ntest if two circles collide"
#define DOC_CIRCLE_MOVE "move((x, y)) -> Circle\nmove(x, y) -> Circle\nmove(Vector2) -> Circle\nmoves the circle by a given amount"
#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"
#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"
#define DOC_CIRCLE_UPDATE "update((x, y), radius) -> None\nupdate(x, y, radius) -> None\nupdates the circle position and radius"
#define DOC_CIRCLE_COPY "copy() -> Circle\nreturns a copy of the circle"
188 changes: 188 additions & 0 deletions test/geometry_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,194 @@ def test_selfupdate(self):
self.assertEqual(c.r, c_r)
self.assertEqual(c.r_sqr, c_r_sqr)

def test_circle_richcompare(self):
"""Ensures that the circle correctly compares itself to other circles"""
c = Circle(0, 0, 10)
c2 = Circle(0, 0, 10)
c3 = Circle(0, 0, 5)
c4 = Circle(0, 0, 20)

self.assertTrue(c == c2)
self.assertFalse(c != c2)

self.assertFalse(c == c3)
self.assertTrue(c != c3)

self.assertFalse(c == c4)
self.assertTrue(c != c4)

# self compare
self.assertTrue(c == c)
self.assertFalse(c != c)

# not implemented compare
with self.assertRaises(TypeError):
c > c2
with self.assertRaises(TypeError):
c < c2
with self.assertRaises(TypeError):
c >= c2
with self.assertRaises(TypeError):
c <= c2

# invalid types
invalid_types = (
None,
[],
"1",
(1,),
Vector2(1, 1),
1,
0.2324,
Rect(0, 0, 10, 10),
True,
)

for value in invalid_types:
self.assertFalse(c == value)
self.assertTrue(c != value)
with self.assertRaises(TypeError):
c > value
with self.assertRaises(TypeError):
c < value
with self.assertRaises(TypeError):
c >= value
with self.assertRaises(TypeError):
c <= value

def test_move_invalid_args(self):
"""tests if the function correctly handles incorrect types as parameters"""
invalid_types = (None, [], "1", (1,), Vector3(1, 1, 3), Circle(3, 3, 1))

c = Circle(10, 10, 4)

for value in invalid_types:
with self.assertRaises(TypeError):
c.move(value)

def test_move_argnum(self):
c = Circle(10, 10, 4)

invalid_args = [(1, 1, 1), (1, 1, 1, 1)]

for arg in invalid_args:
with self.assertRaises(TypeError):
c.move(*arg)

def test_move_return_type(self):
c = Circle(10, 10, 4)

class CircleSub(Circle):
pass

cs = CircleSub(10, 10, 4)

self.assertIsInstance(c.move(1, 1), Circle)
self.assertIsInstance(cs.move(1, 1), CircleSub)

def test_move(self):
"""Ensures that moving the circle position correctly updates position"""
c = Circle(0, 0, 3)

new_c = c.move(5, 5)

self.assertEqual(new_c.x, 5.0)
self.assertEqual(new_c.y, 5.0)
self.assertEqual(new_c.r, 3.0)
self.assertEqual(new_c.r_sqr, 9.0)

new_c = new_c.move(-5, -10)

self.assertEqual(new_c.x, 0.0)
self.assertEqual(new_c.y, -5.0)

def test_move_inplace(self):
"""Ensures that moving the circle position by 0, 0 doesn't move the circle"""
c = Circle(1, 1, 3)

c.move(0, 0)

self.assertEqual(c.x, 1.0)
self.assertEqual(c.y, 1.0)
self.assertEqual(c.r, 3.0)
self.assertEqual(c.r_sqr, 9.0)

def test_move_equality(self):
"""Ensures that moving the circle by 0, 0 will
return a circle that's equal to the original"""
c = Circle(1, 1, 3)

new_c = c.move(0, 0)

self.assertEqual(new_c, c)

def test_move_ip_invalid_args(self):
"""tests if the function correctly handles incorrect types as parameters"""
invalid_types = (None, [], "1", (1,), Vector3(1, 1, 3), Circle(3, 3, 1))

c = Circle(10, 10, 4)

for value in invalid_types:
with self.assertRaises(TypeError):
c.move_ip(value)

def test_move_ip_argnum(self):
"""tests if the function correctly handles incorrect number of args"""
c = Circle(10, 10, 4)

invalid_args = [(1, 1, 1), (1, 1, 1, 1)]

for arg in invalid_args:
with self.assertRaises(TypeError):
c.move_ip(*arg)

def test_move_ip(self):
"""Ensures that moving the circle position correctly updates position"""
c = Circle(0, 0, 3)

c.move_ip(5, 5)

self.assertEqual(c.x, 5.0)
self.assertEqual(c.y, 5.0)
self.assertEqual(c.r, 3.0)
self.assertEqual(c.r_sqr, 9.0)

c.move_ip(-5, -10)
self.assertEqual(c.x, 0.0)
self.assertEqual(c.y, -5.0)

def test_move_ip_inplace(self):
"""Ensures that moving the circle position by 0, 0 doesn't move the circle"""
c = Circle(1, 1, 3)

c.move_ip(0, 0)

self.assertEqual(c.x, 1.0)
self.assertEqual(c.y, 1.0)
self.assertEqual(c.r, 3.0)
self.assertEqual(c.r_sqr, 9.0)

def test_move_ip_equality(self):
"""Ensures that moving the circle by 0, 0 will
return a circle that's equal to the original"""
c = Circle(1, 1, 3)

c.move_ip(0, 0)

self.assertEqual(c, Circle(1, 1, 3))

def test_move_ip_return_type(self):
"""Ensures that the move_ip method returns None"""
c = Circle(10, 10, 4)

class CircleSub(Circle):
pass

cs = CircleSub(10, 10, 4)

self.assertEqual(type(c.move_ip(1, 1)), type(None))
self.assertEqual(type(cs.move_ip(1, 1)), type(None))


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