Skip to content

Circle collidepolygon() and Polygon collidecircle() #189

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 18 commits into from
Jul 6, 2023
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
23 changes: 23 additions & 0 deletions docs/circle.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
4 changes: 4 additions & 0 deletions docs/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions docs/polygon.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
74 changes: 74 additions & 0 deletions examples/polygon_circle_collision.py
Original file line number Diff line number Diff line change
@@ -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()
12 changes: 11 additions & 1 deletion geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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: ...
Expand Down
30 changes: 29 additions & 1 deletion src_c/circle.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -468,13 +468,41 @@ 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},
{"collideline", (PyCFunction)pg_circle_collideline, METH_FASTCALL, NULL},
{"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},
Expand Down
90 changes: 90 additions & 0 deletions src_c/collisions.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
44 changes: 19 additions & 25 deletions src_c/geometry.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)) {
Expand Down
3 changes: 3 additions & 0 deletions src_c/include/collisions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Loading