From 54c58df50543fc3aae6dca2cec504acfaae55856 Mon Sep 17 00:00:00 2001 From: itzpr3d4t0r <103119829+itzpr3d4t0r@users.noreply.github.com> Date: Fri, 12 Jul 2024 23:05:48 +0200 Subject: [PATCH 1/7] circle intersect --- docs/circle.rst | 18 +++++++++++- geometry.pyi | 1 + src_c/circle.c | 22 +++++++++++++++ src_c/collisions.c | 46 ++++++++++++++++++++++++++++++ src_c/include/collisions.h | 3 ++ test/test_circle.py | 58 ++++++++++++++++++++++++++++++++++---- 6 files changed, 142 insertions(+), 6 deletions(-) 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() From 017dcf32f999f04bb07a6775fa4b30a0513b7b5b Mon Sep 17 00:00:00 2001 From: itzpr3d4t0r <103119829+itzpr3d4t0r@users.noreply.github.com> Date: Fri, 12 Jul 2024 23:13:35 +0200 Subject: [PATCH 2/7] fix test --- test/test_circle.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test_circle.py b/test/test_circle.py index ef7be32a..417c9cc6 100644 --- a/test/test_circle.py +++ b/test/test_circle.py @@ -1506,7 +1506,6 @@ def test_intersect_return_type(self): objects = [ Circle(10, 10, 4), - Rect(10, 10, 4, 4), ] for object in objects: @@ -1525,7 +1524,9 @@ def test_intersect(self): self.assertEqual(c.intersect(circle), []) # intersecting circle - self.assertEqual([(10.25, 6.007820144332172), (10.25, 13.992179855667828)], c.intersect(c4)) + self.assertEqual( + [(10.25, 6.007820144332172), (10.25, 13.992179855667828)], c.intersect(c4) + ) # touching self.assertEqual([(14.0, 10.0)], c.intersect(c5)) From 957c887897d89249a26594fe2521c2843dc892cd Mon Sep 17 00:00:00 2001 From: itzpr3d4t0r <103119829+itzpr3d4t0r@users.noreply.github.com> Date: Sat, 13 Jul 2024 00:01:26 +0200 Subject: [PATCH 3/7] slightly faster formula --- src_c/collisions.c | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src_c/collisions.c b/src_c/collisions.c index d7fc454b..27fdad39 100644 --- a/src_c/collisions.c +++ b/src_c/collisions.c @@ -593,36 +593,35 @@ 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) + 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; + } - double dx = x2 - x1; - double dy = y2 - y1; - double d = sqrt(dx * dx + dy * dy); - - if (d > r1 + r2 || d < fabs(r1 - r2)) { + if (d2 == 0 && A->r == B->r) { return 0; } - double a = (r1 * r1 - r2 * r2 + d * d) / (2 * d); - double h = sqrt(r1 * r1 - a * a); + 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 = x1 + a * (x2 - x1) / d; - double ym = y1 + a * (y2 - y1) / d; + double xm = A->x + a * (dx / d); + double ym = A->y + a * (dy / 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; + 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 (d == r1 + r2 || d == fabs(r1 - r2)) { + if (d2 == r_sum2 || d2 == r_diff2) { intersections[0] = xs1; intersections[1] = ys1; return 1; From bbf0a53c4f0c6b3d255236faeaea7039ce17bdfa Mon Sep 17 00:00:00 2001 From: itzpr3d4t0r <103119829+itzpr3d4t0r@users.noreply.github.com> Date: Sat, 13 Jul 2024 12:16:40 +0200 Subject: [PATCH 4/7] better docs, added a test --- docs/circle.rst | 14 +++++++------- docs/geometry.rst | 2 ++ test/test_circle.py | 6 +++++- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/circle.rst b/docs/circle.rst index 984d95c7..777dc47d 100644 --- a/docs/circle.rst +++ b/docs/circle.rst @@ -408,16 +408,16 @@ Circle Methods .. method:: intersect - | :sl:`returns the intersection points of the circle with another shape` - | :sg:`intersect(Circle) -> intersection_points` + | :sl:`finds intersections between the circle and another shape` + | :sg:`intersect(Circle) -> list[Point]` 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. + 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 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. + 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/test/test_circle.py b/test/test_circle.py index 417c9cc6..59a99886 100644 --- a/test/test_circle.py +++ b/test/test_circle.py @@ -1506,6 +1506,9 @@ def test_intersect_return_type(self): objects = [ Circle(10, 10, 4), + Circle(10, 10, 400), + Circle(10, 10, 1), + Circle(15, 10, 10), ] for object in objects: @@ -1517,10 +1520,11 @@ def test_intersect(self): 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]: + for circle in [c, c2, c3, c3_1]: self.assertEqual(c.intersect(circle), []) # intersecting circle From 34e3a8d0ab44e102c32bd5aa19567f8a8bdc277d Mon Sep 17 00:00:00 2001 From: itzpr3d4t0r <103119829+itzpr3d4t0r@users.noreply.github.com> Date: Sat, 13 Jul 2024 12:22:55 +0200 Subject: [PATCH 5/7] added a comment, fixed missing newline --- src_c/circle.c | 1 + src_c/collisions.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src_c/circle.c b/src_c/circle.c index c5e29945..c4cf9100 100644 --- a/src_c/circle.c +++ b/src_c/circle.c @@ -737,6 +737,7 @@ 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; diff --git a/src_c/collisions.c b/src_c/collisions.c index 27fdad39..ae1ad162 100644 --- a/src_c/collisions.c +++ b/src_c/collisions.c @@ -632,4 +632,4 @@ pgIntersection_CircleCircle(pgCircleBase *A, pgCircleBase *B, intersections[2] = xs2; intersections[3] = ys2; return 2; -} \ No newline at end of file +} From 609d52a02def1e63c4a4a963b2e36ed5665298a0 Mon Sep 17 00:00:00 2001 From: itzpr3d4t0r <103119829+itzpr3d4t0r@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:45:50 +0200 Subject: [PATCH 6/7] use more permissive comparisons --- src_c/collisions.c | 4 ++-- src_c/include/geometry.h | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src_c/collisions.c b/src_c/collisions.c index ae1ad162..30f7d57c 100644 --- a/src_c/collisions.c +++ b/src_c/collisions.c @@ -605,7 +605,7 @@ pgIntersection_CircleCircle(pgCircleBase *A, pgCircleBase *B, return 0; } - if (d2 == 0 && A->r == B->r) { + if (double_compare(d2, 0) && double_compare(A->r, B->r)) { return 0; } @@ -621,7 +621,7 @@ pgIntersection_CircleCircle(pgCircleBase *A, pgCircleBase *B, double xs2 = xm - h * (dy / d); double ys2 = ym + h * (dx / d); - if (d2 == r_sum2 || d2 == r_diff2) { + if (double_compare(d2, r_sum2) || double_compare(d2, r_diff2)) { intersections[0] = xs1; intersections[1] = ys1; return 1; 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 */ From 872a56aa44d7ad776ea40756bdaab64fd28e9df2 Mon Sep 17 00:00:00 2001 From: itzpr3d4t0r <103119829+itzpr3d4t0r@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:18:28 +0200 Subject: [PATCH 7/7] fix docs a bit --- docs/circle.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/circle.rst b/docs/circle.rst index 777dc47d..8cb93c39 100644 --- a/docs/circle.rst +++ b/docs/circle.rst @@ -409,9 +409,9 @@ Circle Methods .. method:: intersect | :sl:`finds intersections between the circle and another shape` - | :sg:`intersect(Circle) -> list[Point]` + | :sg:`intersect(Circle) -> list[Tuple[float, float]]` - Calculates and returns a list of intersection points between the circle and another shape. + 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.