diff --git a/docs/circle.rst b/docs/circle.rst index 1ef173e9..8cb93c39 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:`finds intersections between the circle and another shape` + | :sg:`intersect(Circle) -> list[Tuple[float, float]]` + + Returns a list of intersection points between the circle and another shape. + The other shape must be a `Circle` object. + If the circle does not intersect or has infinite intersections, an empty list is returned. + + .. note:: + The shape argument must be an instance of the `Circle` class. + Passing a tuple or list of coordinates representing the shape is not supported, + as the type of shape cannot be determined from coordinates alone. + + .. ## Circle.intersect ## \ No newline at end of file diff --git a/docs/geometry.rst b/docs/geometry.rst index 4f061284..75679eb0 100644 --- a/docs/geometry.rst +++ b/docs/geometry.rst @@ -70,6 +70,8 @@ performing transformations and checking for collisions with other objects. as_rect: Returns the smallest rectangle that contains the circle. + intersect: Finds intersections between the circle and another shape. + Additionally to these, the circle shape can also be used as a collider for the ``geometry.raycast`` function. Line 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..c4cf9100 100644 --- a/src_c/circle.c +++ b/src_c/circle.c @@ -732,6 +732,28 @@ pg_circle_collidelistall(pgCircleObject *self, PyObject *arg) return ret; } +static PyObject * +pg_circle_intersect(pgCircleObject *self, PyObject *arg) +{ + pgCircleBase *scirc = &self->circle; + + /* max number of intersections when supporting: Circle (2), */ + 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 +774,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..30f7d57c 100644 --- a/src_c/collisions.c +++ b/src_c/collisions.c @@ -588,3 +588,48 @@ pgRaycast_LineCircle(pgLineBase *line, pgCircleBase *circle, double max_t, return 1; } + +static int +pgIntersection_CircleCircle(pgCircleBase *A, pgCircleBase *B, + double *intersections) +{ + double dx = B->x - A->x; + double dy = B->y - A->y; + double d2 = dx * dx + dy * dy; + double r_sum = A->r + B->r; + double r_diff = A->r - B->r; + double r_sum2 = r_sum * r_sum; + double r_diff2 = r_diff * r_diff; + + if (d2 > r_sum2 || d2 < r_diff2) { + return 0; + } + + if (double_compare(d2, 0) && double_compare(A->r, B->r)) { + return 0; + } + + double d = sqrt(d2); + double a = (d2 + A->r * A->r - B->r * B->r) / (2 * d); + double h = sqrt(A->r * A->r - a * a); + + double xm = A->x + a * (dx / d); + double ym = A->y + a * (dy / d); + + double xs1 = xm + h * (dy / d); + double ys1 = ym - h * (dx / d); + double xs2 = xm - h * (dy / d); + double ys2 = ym + h * (dx / d); + + if (double_compare(d2, r_sum2) || double_compare(d2, r_diff2)) { + intersections[0] = xs1; + intersections[1] = ys1; + return 1; + } + + intersections[0] = xs1; + intersections[1] = ys1; + intersections[2] = xs2; + intersections[3] = ys2; + return 2; +} 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/src_c/include/geometry.h b/src_c/include/geometry.h index aac70cf1..1986e3ca 100644 --- a/src_c/include/geometry.h +++ b/src_c/include/geometry.h @@ -147,4 +147,12 @@ PG_FREEPOLY_COND(pgPolygonBase *poly, int was_sequence) } } +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)); +} + #endif /* ~_GEOMETRY_H */ diff --git a/test/test_circle.py b/test/test_circle.py index b05d198f..59a99886 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,61 @@ 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), + Circle(10, 10, 400), + Circle(10, 10, 1), + Circle(15, 10, 10), + ] + + 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) + c3_1 = Circle(10, 10, 400) + c4 = Circle(16, 10, 7) + c5 = Circle(18, 10, 4) + + for circle in [c, c2, c3, c3_1]: + 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()