diff --git a/docs/circle.rst b/docs/circle.rst index f3479313..865107f2 100644 --- a/docs/circle.rst +++ b/docs/circle.rst @@ -243,6 +243,29 @@ Circle Methods .. ## Circle.colliderect ## + .. method:: collidepolygon + + | :sl:`checks if a polygon intersects the circle` + | :sg:`collidepolygon(Polygon, only_edges=False) -> bool` + | :sg:`collidepolygon((x1, y1), (x2, y2), ..., only_edges=False) -> bool` + | :sg:`collidepolygon((x1, y1), (x2, y2), ..., only_edges=False) -> bool` + + Tests whether a given `Polygon` collides with the `Circle`. + It takes either a `Polygon` or Polygon-like object as an argument and it returns + `True` if the polygon collides with the `Circle`, `False` otherwise. + + The optional `only_edges` argument can be set to `True` to only test whether the + edges of the polygon intersect the `Circle`. This means that a Polygon that is + completely inscribed in, or circumscribed by the `Circle` will not be considered colliding. + This can be useful for performance reasons if you only care about the edges of the + polygon. + + .. note:: + Keep in mind that the more vertices the polygon has, the more CPU time it will + take to calculate the collision. + + .. ## Circle.collidepolygon ## + .. method:: collideswith | :sl:`test if a shape or point and the circle collide` diff --git a/docs/geometry.rst b/docs/geometry.rst index 1c16d6a3..9b0fbafa 100644 --- a/docs/geometry.rst +++ b/docs/geometry.rst @@ -54,6 +54,8 @@ performing transformations and checking for collisions with other objects. colliderect: Checks if the circle collides with the given rectangle. + collidepolygon: Checks if the circle collides with the given polygon. + collideswith: Checks if the circle collides with the given object. contains: Checks if the circle fully contains the given object. @@ -168,6 +170,8 @@ other objects. collidepoint: Checks if the polygon collides with the given point. + collidecircle: Checks if the polygon collides with the given circle. + insert_vertex: Adds a vertex to the polygon. remove_vertex: Removes a vertex from the polygon. diff --git a/docs/polygon.rst b/docs/polygon.rst index 4704a044..e5a84852 100644 --- a/docs/polygon.rst +++ b/docs/polygon.rst @@ -157,6 +157,29 @@ Polygon Methods .. ## Polygon.collidepoint ## + .. method:: collidecircle + + | :sl:`tests if a circle is inside the polygon` + | :sg:`collidecircle(Circle, only_edges=False) -> bool` + | :sg:`collidecircle((x, y), radius, only_edges=False) -> bool` + | :sg:`collidecircle(x, y, radius, only_edges=False) -> bool` + + Tests whether a given `Circle` collides with the `Polygon`. + It takes either a `Circle` or Circle-like object as an argument and it returns + `True` if the circle collides with the `Polygon`, `False` otherwise. + + The optional `only_edges` argument can be set to `True` to only test whether the + edges of the polygon intersect the `Circle`. This means that a Polygon that is + completely inscribed in, or circumscribed by the `Circle` will not be considered colliding. + This can be useful for performance reasons if you only care about the edges of the + polygon. + + .. note:: + Keep in mind that the more vertices the polygon has, the more CPU time it will + take to calculate the collision. + + .. ## Polygon.collidecircle ## + .. method:: as_segments | :sl:`returns the line segments of the polygon` diff --git a/examples/polygon_circle_collision.py b/examples/polygon_circle_collision.py new file mode 100644 index 00000000..7cae28a8 --- /dev/null +++ b/examples/polygon_circle_collision.py @@ -0,0 +1,74 @@ +import pygame +from pygame.draw import circle as draw_circle, polygon as draw_polygon +from geometry import regular_polygon, Circle + +pygame.init() + +WHITE = (255, 255, 255) +YELLOW = (255, 255, 0) +RED = (255, 0, 0) +SHAPE_THICKNESS = 3 +FPS = 60 +WIDTH, HEIGHT = 800, 800 +WIDTH2, HEIGHT2 = WIDTH // 2, HEIGHT // 2 +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +pygame.display.set_caption("Polygon-Circle Collision Visualization") +clock = pygame.time.Clock() + +font = pygame.font.SysFont("Arial", 25, bold=True) +colliding_text = font.render("Colliding", True, RED) +colliding_textr = colliding_text.get_rect(center=(WIDTH2, 50)) +not_colliding_text = font.render("Not colliding", True, WHITE) +not_colliding_textr = not_colliding_text.get_rect(center=(WIDTH2, 50)) + +modei_text = font.render("Right click to toggle collision mode", True, WHITE) +modei_textr = modei_text.get_rect(center=(WIDTH2, HEIGHT - 50)) + +mode0_txt = font.render("Current: EDGES ONLY", True, YELLOW) +mode0_txtr = mode0_txt.get_rect(center=(WIDTH2, HEIGHT - 25)) + +mode1_txt = font.render("Current: FULL", True, YELLOW) +mode1_txtr = mode1_txt.get_rect(center=(WIDTH2, HEIGHT - 25)) + +circle = Circle((WIDTH2, HEIGHT2), 80) +polygon = regular_polygon(10, (WIDTH2, HEIGHT2), 165) +only_edges = False +running = True + +while running: + circle.center = pygame.mouse.get_pos() + + colliding = circle.collidepolygon(polygon, only_edges) + # Alternative: + # colliding = polygon.collidecircle(circle, only_edges) + + color = RED if colliding else WHITE + + screen.fill((0, 0, 0)) + + draw_circle(screen, color, circle.center, circle.r, SHAPE_THICKNESS) + draw_polygon(screen, color, polygon.vertices, SHAPE_THICKNESS) + + screen.blit( + colliding_text if colliding else not_colliding_text, + colliding_textr if colliding else not_colliding_textr, + ) + + screen.blit(modei_text, modei_textr) + + screen.blit( + mode0_txt if only_edges else mode1_txt, mode0_txtr if only_edges else mode1_txtr + ) + + pygame.display.flip() + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + elif event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 3: + only_edges = not only_edges + + clock.tick(FPS) + +pygame.quit() diff --git a/geometry.pyi b/geometry.pyi index ef041bbf..84b655b1 100644 --- a/geometry.pyi +++ b/geometry.pyi @@ -16,7 +16,7 @@ from typing_extensions import Protocol from pygame.math import Vector2, Vector3 from pygame.rect import Rect -Coordinate = Union[Tuple[float, float], Sequence[float], Vector2, Tuple[int, int]] +Coordinate = Union[Sequence[float, float], Vector2, Sequence[int, int]] Shape = Union["Line", "Circle", "Rect", "Polygon"] @@ -191,6 +191,12 @@ class Circle: @overload def move_ip(self, move_by: Coordinate) -> None: ... def contains(self, shape: Shape) -> bool: ... + @overload + def collidepolygon( + self, polygon: Union[Polygon, Sequence[Coordinate]], only_edges: bool = False + ) -> bool: ... + @overload + def collidepolygon(self, *coords, only_edges: bool = False) -> bool: ... class Polygon: vertices: List[Coordinate] @@ -232,6 +238,10 @@ class Polygon: @overload def collidepoint(self, point: Coordinate) -> bool: ... def get_bounding_box(self) -> Rect: ... + @overload + def collidecircle(self, polygon: CircleValue, only_edges: bool = False) -> bool: ... + @overload + def collidecircle(self, *circle, only_edges: bool = False) -> bool: ... def is_convex() -> bool: ... def insert_vertex(self, index: int, vertex: Coordinate) -> None: ... def remove_vertex(self, index: int) -> None: ... diff --git a/src_c/circle.c b/src_c/circle.c index a356ef11..ea323597 100644 --- a/src_c/circle.c +++ b/src_c/circle.c @@ -110,7 +110,7 @@ pgCircle_FromObject(PyObject *obj, pgCircleBase *out) /* Path for other sequences or Types that count as sequences*/ PyObject *tmp = NULL; length = PySequence_Length(obj); - if (length == 3) { + if (length == 3 && !pgPolygon_Check(obj)) { /*These are to be substituted with better pg_DoubleFromSeqIndex() * implementations*/ tmp = PySequence_ITEM(obj, 0); @@ -468,6 +468,32 @@ pg_circle_contains(pgCircleObject *self, PyObject *arg) return PyBool_FromLong(result); } +static PyObject * +pg_circle_collidepolygon(pgCircleObject *self, PyObject *const *args, + Py_ssize_t nargs) +{ + int was_sequence, result, only_edges = 0; + pgPolygonBase poly; + + /* Check for the optional only_edges argument */ + if (PyBool_Check(args[nargs - 1])) { + only_edges = args[nargs - 1] == Py_True; + nargs--; + } + + if (!pgPolygon_FromObjectFastcall(args, nargs, &poly, &was_sequence)) { + return RAISE( + PyExc_TypeError, + "collidepolygon requires a PolygonType or PolygonLike object"); + } + + result = pgCollision_CirclePolygon(&self->circle, &poly, only_edges); + + PG_FREEPOLY_COND(&poly, was_sequence); + + return PyBool_FromLong(result); +} + static struct PyMethodDef pg_circle_methods[] = { {"collidecircle", (PyCFunction)pg_circle_collidecircle, METH_FASTCALL, NULL}, @@ -475,6 +501,8 @@ static struct PyMethodDef pg_circle_methods[] = { {"collidepoint", (PyCFunction)pg_circle_collidepoint, METH_FASTCALL, NULL}, {"colliderect", (PyCFunction)pg_circle_colliderect, METH_FASTCALL, NULL}, {"collideswith", (PyCFunction)pg_circle_collideswith, METH_O, NULL}, + {"collidepolygon", (PyCFunction)pg_circle_collidepolygon, METH_FASTCALL, + NULL}, {"as_rect", (PyCFunction)pg_circle_as_rect, METH_NOARGS, NULL}, {"update", (PyCFunction)pg_circle_update, METH_FASTCALL, NULL}, {"move", (PyCFunction)pg_circle_move, METH_FASTCALL, NULL}, diff --git a/src_c/collisions.c b/src_c/collisions.c index 8cc29ed3..9a79b2d4 100644 --- a/src_c/collisions.c +++ b/src_c/collisions.c @@ -321,6 +321,96 @@ pgCollision_PolygonPoint(pgPolygonBase *poly, double x, double y) return collision; } +static int +_pgCollision_PolygonPoint_opt(pgPolygonBase *poly, double x, double y) +{ + /* This is a faster version of pgCollision_PolygonPoint that assumes + * that the point passed is not on one of the polygon's vertices. */ + int collision = 0; + Py_ssize_t i, j; + + for (i = 0, j = poly->verts_num - 1; i < poly->verts_num; j = i++) { + double xi = poly->vertices[i * 2]; + double yi = poly->vertices[i * 2 + 1]; + double xj = poly->vertices[j * 2]; + double yj = poly->vertices[j * 2 + 1]; + + if (((yi > y) != (yj > y)) && + (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) { + collision = !collision; + } + } + + return collision; +} + +static int +pgCollision_CirclePolygon(pgCircleBase *circle, pgPolygonBase *poly, + int only_edges) +{ + Py_ssize_t i, j; + double cx = circle->x; + double cy = circle->y; + double cr = circle->r; + double cr_sqr = cr * cr; + + /* Check if the circle is colliding with any of the polygon's edges. */ + for (i = 0, j = poly->verts_num - 1; i < poly->verts_num; j = i++) { + double xi = poly->vertices[i * 2]; + double yi = poly->vertices[i * 2 + 1]; + double xj = poly->vertices[j * 2]; + double yj = poly->vertices[j * 2 + 1]; + + double dx = xj - xi; + double dy = yj - yi; + + double xi_m_cx = xi - cx; + double yi_m_cy = yi - cy; + + double at2 = 2 * (dx * dx + dy * dy); + double b = 2 * (dx * xi_m_cx + dy * yi_m_cy); + double c = xi_m_cx * xi_m_cx + yi_m_cy * yi_m_cy - cr_sqr; + + double bb4ac = b * b - 2 * at2 * c; + + if (bb4ac < 0) { + continue; + } + + double sqrt_bb4ac = sqrt(bb4ac); + double mu1 = (-b + sqrt_bb4ac) / at2; + double mu2 = (-b - sqrt_bb4ac) / at2; + + if ((0 <= mu1 && mu1 <= 1) || (0 <= mu2 && mu2 <= 1)) { + return 1; + } + } + + /* Circle is not colliding with any of the polygon's edges. If we only + * care for edge collision, return now. */ + if (only_edges) { + return 0; + } + + int center_inside = _pgCollision_PolygonPoint_opt(poly, cx, cy); + + if (center_inside) { + return 1; + } + + /* Check if any of the polygon's vertices are inside the circle */ + for (i = 0; i < poly->verts_num; i++) { + double dx = poly->vertices[i * 2] - cx; + double dy = poly->vertices[i * 2 + 1] - cy; + + if (dx * dx + dy * dy <= cr_sqr) { + return 1; + } + } + + return 0; +} + static int pgRaycast_LineLine(pgLineBase *A, pgLineBase *B, double max_t, double *T) { diff --git a/src_c/geometry.c b/src_c/geometry.c index bbbff9dc..b9afe25f 100644 --- a/src_c/geometry.c +++ b/src_c/geometry.c @@ -6,7 +6,7 @@ #include "simd_collisions_avx2.c" #endif /* ~__AVX2__ */ -#define PYGAMEAPI_GEOMETRY_NUMSLOTS 24 +#define PYGAMEAPI_GEOMETRY_NUMSLOTS 18 /* * origin, direction, max_dist @@ -548,30 +548,24 @@ MODINIT_DEFINE(geometry) } /* export the c api */ - c_api[0] = pgCollision_LineLine; - c_api[1] = pgIntersection_LineLine; - c_api[2] = pgCollision_LineCircle; - c_api[3] = pgCollision_CircleCircle; - c_api[4] = pgCollision_RectLine; - c_api[5] = pgCollision_RectCircle; - c_api[6] = &pgLine_Type; - c_api[7] = pgLine_New; - c_api[8] = pgLine_New4; - c_api[9] = pgLine_FromObject; - c_api[10] = pgLine_FromObjectFastcall; - c_api[11] = pgLine_Length; - c_api[12] = pgLine_LengthSquared; - c_api[13] = pgLine_At; - c_api[14] = &pgCircle_Type; - c_api[15] = pgCircle_New; - c_api[16] = pgCircle_New3; - c_api[17] = pgCircle_FromObject; - c_api[18] = &pgPolygon_Type; - c_api[19] = pgPolygon_New; - c_api[20] = pgPolygon_New2; - c_api[21] = pgPolygon_New4; - c_api[22] = pgPolygon_FromObject; - c_api[23] = pgPolygon_FromObjectFastcall; + c_api[0] = &pgLine_Type; + c_api[1] = pgLine_New; + c_api[2] = pgLine_New4; + c_api[3] = pgLine_FromObject; + c_api[4] = pgLine_FromObjectFastcall; + c_api[5] = pgLine_Length; + c_api[6] = pgLine_LengthSquared; + c_api[7] = pgLine_At; + c_api[8] = &pgCircle_Type; + c_api[9] = pgCircle_New; + c_api[10] = pgCircle_New3; + c_api[11] = pgCircle_FromObject; + c_api[12] = &pgPolygon_Type; + c_api[13] = pgPolygon_New; + c_api[14] = pgPolygon_New2; + c_api[15] = pgPolygon_New4; + c_api[16] = pgPolygon_FromObject; + c_api[17] = pgPolygon_FromObjectFastcall; apiobj = encapsulate_api(c_api, "geometry"); if (PyModule_AddObject(module, PYGAMEAPI_LOCAL_ENTRY, apiobj)) { diff --git a/src_c/include/collisions.h b/src_c/include/collisions.h index ff077279..20ff5c5f 100644 --- a/src_c/include/collisions.h +++ b/src_c/include/collisions.h @@ -43,4 +43,7 @@ pgRaycast_LineCircle(pgLineBase *, pgCircleBase *, double, double *); static int pgCollision_PolygonPoint(pgPolygonBase *, double, double); +static int +pgCollision_CirclePolygon(pgCircleBase *, pgPolygonBase *, int); + #endif /* ~_PG_COLLISIONS_H */ diff --git a/src_c/polygon.c b/src_c/polygon.c index e3a9a630..58afcead 100644 --- a/src_c/polygon.c +++ b/src_c/polygon.c @@ -1161,6 +1161,27 @@ pg_polygon_is_convex(pgPolygonObject *self, PyObject *_null) return PyBool_FromLong(_pg_polygon_is_convex_helper(&self->polygon)); } +static PyObject * +pg_polygon_collidecircle(pgPolygonObject *self, PyObject *const *args, + Py_ssize_t nargs) +{ + pgCircleBase circle; + int only_edges = 0; + + /* Check for the optional only_edges argument */ + if (PyBool_Check(args[nargs - 1])) { + only_edges = args[nargs - 1] == Py_True; + nargs--; + } + + if (!pgCircle_FromObjectFastcall(args, nargs, &circle)) { + return RAISE(PyExc_TypeError, "Invalid circle parameter"); + } + + return PyBool_FromLong( + pgCollision_CirclePolygon(&circle, &self->polygon, only_edges)); +} + static struct PyMethodDef pg_polygon_methods[] = { {"as_segments", (PyCFunction)pg_polygon_as_segments, METH_NOARGS, NULL}, {"move", (PyCFunction)pg_polygon_move, METH_FASTCALL, NULL}, @@ -1169,6 +1190,8 @@ static struct PyMethodDef pg_polygon_methods[] = { {"rotate_ip", (PyCFunction)pg_polygon_rotate_ip, METH_O, NULL}, {"collidepoint", (PyCFunction)pg_polygon_collidepoint, METH_FASTCALL, NULL}, + {"collidecircle", (PyCFunction)pg_polygon_collidecircle, METH_FASTCALL, + NULL}, {"get_bounding_box", (PyCFunction)pg_polygon_get_bounding_box, METH_NOARGS, NULL}, {"is_convex", (PyCFunction)pg_polygon_is_convex, METH_NOARGS, NULL}, diff --git a/test/test_circle.py b/test/test_circle.py index e796646f..2a6b5605 100644 --- a/test/test_circle.py +++ b/test/test_circle.py @@ -6,7 +6,7 @@ from pygame import Vector2, Vector3 from pygame import Rect -from geometry import Circle, Line, Polygon +from geometry import Circle, Line, Polygon, regular_polygon E_T = "Expected True, " E_F = "Expected False, " @@ -999,6 +999,185 @@ def test_contains_polygon(self): # intersecting polygon self.assertFalse(c.contains(p3)) + def test_collidepolygon_argtype(self): + """Tests if the function correctly handles incorrect types as parameters""" + + invalid_types = ( + True, + False, + None, + [], + "1", + (1,), + 1, + 0, + -1, + 1.23, + (1, 2, 3), + Circle(10, 10, 4), + Line(10, 10, 4, 4), + Rect(10, 10, 4, 4), + Vector3(10, 10, 4), + Vector2(10, 10), + ) + + c = Circle(10, 10, 4) + + for value in invalid_types: + with self.assertRaises(TypeError): + c.collidepolygon(value) + with self.assertRaises(TypeError): + c.collidepolygon(value, True) + with self.assertRaises(TypeError): + c.collidepolygon(value, False) + + def test_collidepolygon_argnum(self): + """Tests if the function correctly handles incorrect number of parameters""" + c = Circle(10, 10, 4) + + poly = Polygon((-5, 0), (5, 0), (0, 5)) + invalid_args = [ + (poly, poly), + (poly, poly, poly), + (poly, poly, poly, poly), + ] + + with self.assertRaises(TypeError): + c.collidepolygon() + + for arg in invalid_args: + with self.assertRaises(TypeError): + c.collidepolygon(*arg) + with self.assertRaises(TypeError): + c.collidepolygon(*arg, True) + with self.assertRaises(TypeError): + c.collidepolygon(*arg, False) + + def test_collidepolygon_return_type(self): + """Tests if the function returns the correct type""" + c = Circle(10, 10, 4) + + vertices = [(-5, 0), (5, 0), (0, 5)] + + items = [ + Polygon(vertices), + vertices, + tuple(vertices), + [list(v) for v in vertices], + ] + + for item in items: + self.assertIsInstance(c.collidepolygon(item), bool) + self.assertIsInstance(c.collidepolygon(item, True), bool) + self.assertIsInstance(c.collidepolygon(item, False), bool) + + self.assertIsInstance(c.collidepolygon(*vertices), bool) + self.assertIsInstance(c.collidepolygon(*vertices, True), bool) + self.assertIsInstance(c.collidepolygon(*vertices, False), bool) + + def test_collidepolygon(self): + """Ensures that the collidepolygon method correctly determines if a polygon + is colliding with the circle""" + epsilon = 0.00000000000001 + + c = Circle(0, 0, 15) + + p1 = Polygon([(-5, 0), (5, 0), (0, 5)]) + p2 = Polygon([(100, 150), (200, 225), (150, 200)]) + p3 = Polygon([(0, 0), (50, 50), (50, -50), (0, -50)]) + p4 = regular_polygon(4, c.center, 100) + p5 = regular_polygon(3, (c.x + c.r - 5, c.y), 5) + p6 = regular_polygon(3, (c.x + c.r - 5, c.y), 5 - epsilon) + + # circle contains polygon + self.assertTrue(c.collidepolygon(p1)) + self.assertTrue(c.collidepolygon(p1, False)) + + # non colliding + self.assertFalse(c.collidepolygon(p2)) + self.assertFalse(c.collidepolygon(p2, False)) + + # intersecting polygon + self.assertTrue(c.collidepolygon(p3)) + self.assertTrue(c.collidepolygon(p3, False)) + + # polygon contains circle + self.assertTrue(c.collidepolygon(p4)) + self.assertTrue(c.collidepolygon(p4, False)) + + # circle contains polygon, barely touching + self.assertTrue(c.collidepolygon(p5)) + self.assertTrue(c.collidepolygon(p5, False)) + + # circle contains polygon, barely not touching + self.assertTrue(c.collidepolygon(p6)) + self.assertTrue(c.collidepolygon(p6, False)) + + # --- Edge only --- + + # circle contains polygon + self.assertFalse(c.collidepolygon(p1, True)) + + # non colliding + self.assertFalse(c.collidepolygon(p2, True)) + + # intersecting polygon + self.assertTrue(c.collidepolygon(p3, True)) + + # polygon contains circle + self.assertFalse(c.collidepolygon(p4, True)) + + # circle contains polygon, barely touching + self.assertTrue(c.collidepolygon(p5, True)) + + # circle contains polygon, barely not touching + self.assertFalse(c.collidepolygon(p6, True)) + + def test_collidepolygon_invalid_only_edges_param(self): + """Tests if the function correctly handles incorrect types as only_edges parameter""" + c = Circle(10, 10, 4) + poly = Polygon((-5, 0), (5, 0), (0, 5)) + + invalid_types = ( + None, + [], + "1", + (1,), + 1, + 0, + -1, + 1.23, + (1, 2, 3), + Circle(10, 10, 4), + Line(10, 10, 4, 4), + Rect(10, 10, 4, 4), + Vector3(10, 10, 4), + Vector2(10, 10), + ) + + for value in invalid_types: + with self.assertRaises(TypeError): + c.collidepolygon(poly, value) + + def test_collidepolygon_no_invalidation(self): + """Ensures that the function doesn't modify the polygon or the circle""" + c = Circle(10, 10, 4) + poly = Polygon((-5, 0), (5, 0), (0, 5)) + + c_copy = c.copy() + poly_copy = poly.copy() + + c.collidepolygon(poly) + + self.assertEqual(c.x, c_copy.x) + self.assertEqual(c.y, c_copy.y) + self.assertEqual(c.r, c_copy.r) + + self.assertEqual(poly.vertices, poly_copy.vertices) + self.assertEqual(poly.verts_num, poly_copy.verts_num) + self.assertEqual(poly.c_x, poly_copy.c_x) + self.assertEqual(poly.c_y, poly_copy.c_y) + if __name__ == "__main__": unittest.main() diff --git a/test/test_polygon.py b/test/test_polygon.py index 62b90503..5e3fdd33 100644 --- a/test/test_polygon.py +++ b/test/test_polygon.py @@ -4,7 +4,7 @@ from pygame import Vector2, Vector3, Rect import geometry -from geometry import Polygon, Line +from geometry import Polygon, Circle, Line, regular_polygon import math @@ -1648,6 +1648,183 @@ def test_is_convex_meth(self): self.assertTrue(p1.is_convex()) self.assertFalse(p2.is_convex()) + def test_collidecircle_argtype(self): + """Tests if the function correctly handles incorrect types as parameters""" + + invalid_types = ( + True, + False, + None, + [], + "1", + (1,), + 1, + 0, + -1, + 1.23, + Polygon((0, 0), (0, 1), (1, 1), (1, 0)), + Line(10, 10, 4, 4), + Rect(10, 10, 4, 4), + Vector2(10, 10), + ) + + p = Polygon((0, 0), (0, 1), (1, 1), (1, 0)) + + for value in invalid_types: + with self.assertRaises(TypeError): + p.collidecircle(value) + with self.assertRaises(TypeError): + p.collidecircle(value, True) + with self.assertRaises(TypeError): + p.collidecircle(value, False) + + def test_collidecircle_argnum(self): + """Tests if the function correctly handles incorrect number of parameters""" + p = Polygon((0, 0), (0, 1), (1, 1), (1, 0)) + + circle = Circle(10, 10, 4) + invalid_args = [ + (circle, circle), + (circle, circle, circle), + (circle, circle, circle, circle), + (circle, circle, circle, circle, circle), + ] + + with self.assertRaises(TypeError): + p.collidecircle() + + for arg in invalid_args: + with self.assertRaises(TypeError): + p.collidecircle(*arg) + with self.assertRaises(TypeError): + p.collidecircle(*arg, True) + with self.assertRaises(TypeError): + p.collidecircle(*arg, False) + + def test_collidecircle_return_type(self): + """Tests if the function returns the correct type""" + p = Polygon((0, 0), (0, 1), (1, 1), (1, 0)) + + circle_val = [10, 10, 4] + + items = [ + Circle(circle_val), + circle_val, + tuple(circle_val), + ] + + for item in items: + self.assertIsInstance(p.collidecircle(item), bool) + self.assertIsInstance(p.collidecircle(item, True), bool) + self.assertIsInstance(p.collidecircle(item, False), bool) + + self.assertIsInstance(p.collidecircle(*circle_val), bool) + self.assertIsInstance(p.collidecircle(*circle_val, True), bool) + self.assertIsInstance(p.collidecircle(*circle_val, False), bool) + + def test_collidecircle(self): + """Ensures that the collidecircle method correctly determines if a polygon + is colliding with the circle""" + epsilon = 0.00000000000001 + + c = Circle(0, 0, 15) + + p1 = Polygon([(-5, 0), (5, 0), (0, 5)]) + p2 = Polygon([(100, 150), (200, 225), (150, 200)]) + p3 = Polygon([(0, 0), (50, 50), (50, -50), (0, -50)]) + p4 = regular_polygon(4, c.center, 100) + p5 = regular_polygon(3, (c.x + c.r - 5, c.y), 5) + p6 = regular_polygon(3, (c.x + c.r - 5, c.y), 5 - epsilon) + + # circle contains polygon + self.assertTrue(p1.collidecircle(c)) + self.assertTrue(p1.collidecircle(c), False) + + # non colliding + self.assertFalse(p2.collidecircle(c)) + self.assertFalse(p2.collidecircle(c), False) + + # intersecting polygon + self.assertTrue(p3.collidecircle(c)) + self.assertTrue(p3.collidecircle(c), False) + + # polygon contains circle + self.assertTrue(p4.collidecircle(c)) + self.assertTrue(p4.collidecircle(c), False) + + # circle contains polygon, barely touching + self.assertTrue(p5.collidecircle(c)) + self.assertTrue(p5.collidecircle(c), False) + + # circle contains polygon, barely not touching + self.assertTrue(p6.collidecircle(c)) + self.assertTrue(p6.collidecircle(c), False) + + # --- Edge only --- + + # circle contains polygon + self.assertFalse(p1.collidecircle(c, True)) + + # non colliding + self.assertFalse(p2.collidecircle(c, True)) + + # intersecting polygon + self.assertTrue(p3.collidecircle(c, True)) + + # polygon contains circle + self.assertFalse(p4.collidecircle(c, True)) + + # circle contains polygon, barely touching + self.assertTrue(p5.collidecircle(c, True)) + + # circle contains polygon, barely not touching + self.assertFalse(p6.collidecircle(c, True)) + + def test_collidepolygon_invalid_only_edges_param(self): + """Tests if the function correctly handles incorrect types as only_edges parameter""" + c = Circle(10, 10, 4) + poly = Polygon((-5, 0), (5, 0), (0, 5)) + + invalid_types = ( + None, + [], + "1", + (1,), + 1, + 0, + -1, + 1.23, + (1, 2, 3), + Circle(10, 10, 4), + Line(10, 10, 4, 4), + Rect(10, 10, 4, 4), + Vector3(10, 10, 4), + Vector2(10, 10), + ) + + for value in invalid_types: + with self.assertRaises(TypeError): + poly.collidecircle(c, value) + + def test_collidecircle_no_invalidation(self): + """Ensures that the function doesn't modify the polygon or the circle""" + c = Circle(10, 10, 4) + poly = Polygon((-5, 0), (5, 0), (0, 5)) + + c_copy = c.copy() + poly_copy = poly.copy() + + poly.collidecircle(c) + + self.assertEqual(c.x, c_copy.x) + self.assertEqual(c.y, c_copy.y) + self.assertEqual(c.r, c_copy.r) + + self.assertEqual(poly.vertices, poly_copy.vertices) + self.assertEqual(poly.verts_num, poly_copy.verts_num) + self.assertEqual(poly.c_x, poly_copy.c_x) + self.assertEqual(poly.c_y, poly_copy.c_y) + if __name__ == "__main__": unittest.main()