diff --git a/docs/circle.rst b/docs/circle.rst index 1ef173e9..984d95c7 100644 --- a/docs/circle.rst +++ b/docs/circle.rst @@ -404,4 +404,20 @@ Circle Methods as the original `Circle` object. The function takes no arguments and returns the new `Circle` object. - .. ## Circle.copy ## \ No newline at end of file + .. ## Circle.copy ## + + .. method:: intersect + + | :sl:`returns the intersection points of the circle with another shape` + | :sg:`intersect(Circle) -> intersection_points` + + Calculates and returns a list of intersection points between the circle and another shape. + The other shape can either be a `Circle` object. + If the two objects do not intersect, an empty list is returned. + + .. note:: + The shape argument must be an actual shape object (Circle). + You can't pass a tuple or list of coordinates representing the shape, + because the shape type can't be determined from the coordinates alone. + + .. ## Circle.intersect ## \ No newline at end of file diff --git a/geometry.pyi b/geometry.pyi index fdd9fa48..2961962a 100644 --- a/geometry.pyi +++ b/geometry.pyi @@ -208,6 +208,7 @@ class Circle: def rotate_ip( self, angle: float, rotation_point: Coordinate = Circle.center ) -> None: ... + def intersect(self, other: Circle) -> List[Tuple[float, float]]: ... class Polygon: vertices: List[Coordinate] diff --git a/src_c/circle.c b/src_c/circle.c index 8b9934e6..c5e29945 100644 --- a/src_c/circle.c +++ b/src_c/circle.c @@ -732,6 +732,27 @@ pg_circle_collidelistall(pgCircleObject *self, PyObject *arg) return ret; } +static PyObject * +pg_circle_intersect(pgCircleObject *self, PyObject *arg) +{ + pgCircleBase *scirc = &self->circle; + + double intersections[4]; + int num = 0; + + if (pgCircle_Check(arg)) { + pgCircleBase *other = &pgCircle_AsCircle(arg); + num = pgIntersection_CircleCircle(scirc, other, intersections); + } + else { + PyErr_Format(PyExc_TypeError, "Argument must be a CircleType, got %s", + Py_TYPE(arg)->tp_name); + return NULL; + } + + return pg_PointList_FromArrayDouble(intersections, num * 2); +} + static struct PyMethodDef pg_circle_methods[] = { {"collidecircle", (PyCFunction)pg_circle_collidecircle, METH_FASTCALL, NULL}, @@ -752,6 +773,7 @@ static struct PyMethodDef pg_circle_methods[] = { {"copy", (PyCFunction)pg_circle_copy, METH_NOARGS, NULL}, {"rotate", (PyCFunction)pg_circle_rotate, METH_FASTCALL, NULL}, {"rotate_ip", (PyCFunction)pg_circle_rotate_ip, METH_FASTCALL, NULL}, + {"intersect", (PyCFunction)pg_circle_intersect, METH_O, NULL}, {NULL, NULL, 0, NULL}}; /* numeric functions */ diff --git a/src_c/collisions.c b/src_c/collisions.c index 6be4ff11..d7fc454b 100644 --- a/src_c/collisions.c +++ b/src_c/collisions.c @@ -588,3 +588,49 @@ pgRaycast_LineCircle(pgLineBase *line, pgCircleBase *circle, double max_t, return 1; } + +static int +pgIntersection_CircleCircle(pgCircleBase *A, pgCircleBase *B, + double *intersections) +{ + double x1 = A->x; + double y1 = A->y; + double r1 = A->r; + double x2 = B->x; + double y2 = B->y; + double r2 = B->r; + + if (x1 == x2 && y1 == y2 && r1 == r2) + return 0; + + double dx = x2 - x1; + double dy = y2 - y1; + double d = sqrt(dx * dx + dy * dy); + + if (d > r1 + r2 || d < fabs(r1 - r2)) { + return 0; + } + + double a = (r1 * r1 - r2 * r2 + d * d) / (2 * d); + double h = sqrt(r1 * r1 - a * a); + + double xm = x1 + a * (x2 - x1) / d; + double ym = y1 + a * (y2 - y1) / d; + + double xs1 = xm + h * (y2 - y1) / d; + double ys1 = ym - h * (x2 - x1) / d; + double xs2 = xm - h * (y2 - y1) / d; + double ys2 = ym + h * (x2 - x1) / d; + + if (d == r1 + r2 || d == fabs(r1 - r2)) { + intersections[0] = xs1; + intersections[1] = ys1; + return 1; + } + + intersections[0] = xs1; + intersections[1] = ys1; + intersections[2] = xs2; + intersections[3] = ys2; + return 2; +} \ No newline at end of file diff --git a/src_c/include/collisions.h b/src_c/include/collisions.h index b3ffe1ce..44cf133a 100644 --- a/src_c/include/collisions.h +++ b/src_c/include/collisions.h @@ -48,5 +48,8 @@ pgCollision_PolygonLine(pgPolygonBase *, pgLineBase *, int); static int pgCollision_CirclePolygon(pgCircleBase *, pgPolygonBase *, int); +static int +pgIntersection_CircleCircle(pgCircleBase *A, pgCircleBase *B, + double *intersections); #endif /* ~_PG_COLLISIONS_H */ diff --git a/test/test_circle.py b/test/test_circle.py index b05d198f..ef7be32a 100644 --- a/test/test_circle.py +++ b/test/test_circle.py @@ -1,12 +1,10 @@ -import unittest - import math +import unittest from math import sqrt -from pygame import Vector2, Vector3 -from pygame import Rect - from geometry import Circle, Line, Polygon, regular_polygon +from pygame import Rect +from pygame import Vector2, Vector3 E_T = "Expected True, " E_F = "Expected False, " @@ -1482,6 +1480,56 @@ def test_collidelistall(self): for objects, expected in zip([circles, rects, lines, polygons], expected): self.assertEqual(c.collidelistall(objects), expected) + def test_intersect_argtype(self): + """Tests if the function correctly handles incorrect types as parameters""" + + invalid_types = (None, "1", (1,), 1, (1, 2, 3), True, False) + + c = Circle(10, 10, 4) + + for value in invalid_types: + with self.assertRaises(TypeError): + c.intersect(value) + + def test_intersect_argnum(self): + """Tests if the function correctly handles incorrect number of parameters""" + c = Circle(10, 10, 4) + + circles = [(Circle(10, 10, 4) for _ in range(100))] + for size in range(len(circles)): + with self.assertRaises(TypeError): + c.intersect(*circles[:size]) + + def test_intersect_return_type(self): + """Tests if the function returns the correct type""" + c = Circle(10, 10, 4) + + objects = [ + Circle(10, 10, 4), + Rect(10, 10, 4, 4), + ] + + for object in objects: + self.assertIsInstance(c.intersect(object), list) + + def test_intersect(self): + + # Circle + c = Circle(10, 10, 4) + c2 = Circle(10, 10, 2) + c3 = Circle(100, 100, 1) + c4 = Circle(16, 10, 7) + c5 = Circle(18, 10, 4) + + for circle in [c, c2, c3]: + self.assertEqual(c.intersect(circle), []) + + # intersecting circle + self.assertEqual([(10.25, 6.007820144332172), (10.25, 13.992179855667828)], c.intersect(c4)) + + # touching + self.assertEqual([(14.0, 10.0)], c.intersect(c5)) + if __name__ == "__main__": unittest.main()