Skip to content

Line collidepolygon() and Polygon collideline() #194

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 15 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
4 changes: 4 additions & 0 deletions docs/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ other objects.

colliderect: Checks if the line collides with the given rectangle.

collidepolygon: Checks if the line collides with the given polygon.

collideswith: Checks if the line collides with the given object.

as_circle: Returns a circle which fully encloses the line.
Expand Down Expand Up @@ -174,6 +176,8 @@ other objects.

collidepoint: Checks if the polygon collides with the given point.

collideline: Checks if the polygon collides with the given line.

collidecircle: Checks if the polygon collides with the given circle.

insert_vertex: Adds a vertex to the polygon.
Expand Down
23 changes: 23 additions & 0 deletions docs/line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,29 @@ Line Methods

.. ## Line.collidecircle ##

.. method:: collidepolygon

| :sl:`test if a line intersects with a polygon`
| :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 `Line`.
It takes either a `Polygon` or Polygon-like object as an argument and it returns
`True` if the polygon collides with the `Line`, `False` otherwise.

The optional `only_edges` argument can be set to `True` to only test whether the
edges of the polygon intersect the `Line`. This means that a Line that is
inscribed by the `Polygon` or completely outside of it 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.

.. ## Line.collidepolygon ##


.. method:: collideswith

Expand Down
18 changes: 18 additions & 0 deletions docs/polygon.rst
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,23 @@ Polygon Methods

.. ## Polygon.collidepoint ##

.. method:: collideline

| :sl:`tests if a line intersects the polygon`
| :sg:`collideline(Line, only_edges=False) -> bool`
| :sg:`collideline((x1, y1), (x2, y2), only_edges=False) -> bool`
| :sg:`collideline(x1, y1, x2, y2, only_edges=False) -> bool`

Tests whether a given `Line` collides with the `Polygon`.
It takes either a `Line` or Line-like object as an argument and it returns `True`
if the `Line` 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 `Line`. This means that a Line that is
inscribed by the `Polygon` or completely outside of it will not be considered colliding.

.. ## Polygon.collideline ##

.. method:: collidecircle

| :sl:`tests if a circle is inside the polygon`
Expand All @@ -171,6 +188,7 @@ Polygon Methods
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.

Expand Down
108 changes: 108 additions & 0 deletions examples/polygon_line_collision.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from math import radians, sin, cos

import pygame
from pygame.draw import line as draw_line, polygon as draw_polygon
from geometry import Line, regular_polygon


# using this because we're missing line.rotate(), rotates a line around its midpoint
def rotate_line(line, d_ang):
angle = radians(d_ang)
ca, sa = cos(angle), sin(angle)
xm, ym = line.midpoint
ax, ay = line.a
bx, by = line.b
ax -= xm
ay -= ym
bx -= xm
by -= ym
new_ax, new_ay = xm + ax * ca - ay * sa, ym + ax * sa + ay * ca
new_bx, new_by = xm + bx * ca - by * sa, ym + bx * sa + by * ca
line.a, line.b = (new_ax, new_ay), (new_bx, new_by)


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-Line 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))

modei2_text = font.render("Left click to rotate", True, WHITE)
modei2_textr = modei2_text.get_rect(center=(WIDTH2, HEIGHT - 80))

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))

polygon = regular_polygon(6, (WIDTH2, HEIGHT2), 180)
line = Line((0, 0), (135, 135))
only_edges = False
running = True
color = WHITE

rotating = False

while running:
delta = (clock.tick(FPS) / 1000) * 60

line.midpoint = pygame.mouse.get_pos()

colliding = line.collidepolygon(polygon, only_edges)
# Alternative:
# colliding = polygon.collideline(line, only_edges)

if rotating:
rotate_line(line, 2)

color = RED if colliding else WHITE

screen.fill((10, 10, 10))

draw_polygon(screen, color, polygon.vertices, SHAPE_THICKNESS)
draw_line(screen, color, line.a, line.b, SHAPE_THICKNESS)

screen.blit(
colliding_text if colliding else not_colliding_text,
colliding_textr if colliding else not_colliding_textr,
)

screen.blit(modei2_text, modei2_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 == 1:
rotating = True
elif event.button == 3:
only_edges = not only_edges
elif event.type == pygame.MOUSEBUTTONUP:
if event.button == 1:
rotating = False
pygame.quit()
2 changes: 2 additions & 0 deletions geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class Line(Sequence[float]):
def collidecircle(self, circle: CircleValue) -> bool: ...
@overload
def collidecircle(self, x: float, y: float, r: float) -> bool: ...
def collidepolygon(self, polygon: Polygon, only_edges: bool = False) -> bool: ...
def as_circle(self) -> Circle: ...
def as_rect(self) -> Rect: ...
@overload
Expand Down Expand Up @@ -243,6 +244,7 @@ class Polygon:
def collidepoint(self, x: float, y: float) -> bool: ...
@overload
def collidepoint(self, point: Coordinate) -> bool: ...
def collideline(self, line: LineValue, only_edges: bool = False) -> bool: ...
def get_bounding_box(self) -> Rect: ...
def is_convex(self) -> bool: ...
@overload
Expand Down
62 changes: 62 additions & 0 deletions src_c/collisions.c
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,68 @@ pgCollision_PolygonPoint(pgPolygonBase *poly, double x, double y)
return collision;
}

static inline int
pgCollision_PolygonPoints(pgPolygonBase *poly, double x1, double y1, double x2,
double y2)
{
int collision1 = 0, collision2 = 0;
Py_ssize_t i, j;

double *vertices = poly->vertices;

for (i = 0, j = poly->verts_num - 1; i < poly->verts_num; j = i++) {
double xi = vertices[i * 2];
double yi = vertices[i * 2 + 1];
double xj = vertices[j * 2];
double yj = vertices[j * 2 + 1];

double xj_xi = xj - xi;
double yj_yi = yj - yi;

if (((yi > y1) != (yj > y1)) &&
(x1 < xj_xi * (y1 - yi) / yj_yi + xi)) {
collision1 = !collision1;
}

if (((yi > y2) != (yj > y2)) &&
(x2 < xj_xi * (y2 - yi) / yj_yi + xi)) {
collision2 = !collision2;
}
}

return collision1 || collision2;
}

static inline int
_pgCollision_line_polyedges(pgLineBase *line, pgPolygonBase *poly)
{
Py_ssize_t i, j;
double *vertices = poly->vertices;

for (i = 0, j = poly->verts_num - 1; i < poly->verts_num; j = i++) {
if (pgCollision_LineLine(
line, &(pgLineBase){vertices[j * 2], vertices[j * 2 + 1],
vertices[i * 2], vertices[i * 2 + 1]})) {
return 1;
}
}

return 0;
}

static inline int
pgCollision_PolygonLine(pgPolygonBase *poly, pgLineBase *line, int only_edges)
{
int collision = _pgCollision_line_polyedges(line, poly);

if (collision || only_edges) {
return collision;
}

return pgCollision_PolygonPoints(poly, line->x1, line->y1, line->x2,
line->y2);
}

static int
_pgCollision_PolygonPoint_opt(pgPolygonBase *poly, double x, double y)
{
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,7 +43,10 @@ pgRaycast_LineCircle(pgLineBase *, pgCircleBase *, double, double *);
static int
pgCollision_PolygonPoint(pgPolygonBase *, double, double);

static int
pgCollision_PolygonLine(pgPolygonBase *, pgLineBase *, int);
static int
pgCollision_CirclePolygon(pgCircleBase *, pgPolygonBase *, int);


#endif /* ~_PG_COLLISIONS_H */
33 changes: 32 additions & 1 deletion src_c/line.c
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ pgLine_FromObject(PyObject *obj, pgLineBase *out)
}
if (PySequence_Check(obj)) {
length = PySequence_Length(obj);
if (length == 4) {
if (length == 4 && !pgPolygon_Check(obj)) {
PyObject *tmp;
tmp = PySequence_GetItem(obj, 0);
if (!pg_DoubleFromObj(tmp, &(out->x1))) {
Expand Down Expand Up @@ -169,6 +169,9 @@ pgLine_FromObject(PyObject *obj, pgLineBase *out)
Py_DECREF(sub);
return IS_LINE_VALID(out);
}
else {
return 0;
}
}
if (PyObject_HasAttrString(obj, "line")) {
PyObject *lineattr;
Expand Down Expand Up @@ -692,6 +695,32 @@ pg_line_scale_ip(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs) {
Py_RETURN_NONE;
}

static PyObject *
pg_line_collidepolygon(pgLineObject *self, PyObject *const *args,
Py_ssize_t nargs)
{
pgPolygonBase poly;
int was_sequence, result, 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 (!pgPolygon_FromObjectFastcall(args, nargs, &poly, &was_sequence)) {
return RAISE(
PyExc_TypeError,
"collidepolygon requires a Polygon or PolygonLike object");
}

result = pgCollision_PolygonLine(&poly, &self->line, only_edges);

PG_FREEPOLY_COND(&poly, was_sequence);

return PyBool_FromLong(result);
}

static PyObject *
pg_line_as_circle(pgLineObject *self, PyObject *_null)
{
Expand All @@ -718,6 +747,8 @@ static struct PyMethodDef pg_line_methods[] = {
{"collidecircle", (PyCFunction)pg_line_collidecircle, METH_FASTCALL, NULL},
{"colliderect", (PyCFunction)pg_line_colliderect, METH_FASTCALL, NULL},
{"collideswith", (PyCFunction)pg_line_collideswith, METH_O, NULL},
{"collidepolygon", (PyCFunction)pg_line_collidepolygon, METH_FASTCALL,
NULL},
{"as_rect", (PyCFunction)pg_line_as_rect, METH_NOARGS, NULL},
{"update", (PyCFunction)pg_line_update, METH_FASTCALL, NULL},
{"move", (PyCFunction)pg_line_move, METH_FASTCALL, NULL},
Expand Down
23 changes: 22 additions & 1 deletion src_c/polygon.c
Original file line number Diff line number Diff line change
Expand Up @@ -1190,7 +1190,6 @@ pg_polygon_is_convex(pgPolygonObject *self, PyObject *_null)
{
return PyBool_FromLong(_pg_polygon_is_convex_helper(&self->polygon));
}

static int
_pg_polygon_scale_helper(pgPolygonBase *poly, double factor)
{
Expand Down Expand Up @@ -1258,6 +1257,27 @@ pg_polygon_scale_ip(pgPolygonObject *self, PyObject *arg)
Py_RETURN_NONE;
}

static PyObject *
pg_polygon_collideline(pgPolygonObject *self, PyObject *const *args,
Py_ssize_t nargs)
{
pgLineBase line;
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 (!pgLine_FromObjectFastcall(args, nargs, &line)) {
return RAISE(PyExc_TypeError, "Invalid line parameter");
}

return PyBool_FromLong(
pgCollision_PolygonLine(&self->polygon, &line, only_edges));
}

static PyObject *
pg_polygon_collidecircle(pgPolygonObject *self, PyObject *const *args,
Py_ssize_t nargs)
Expand Down Expand Up @@ -1287,6 +1307,7 @@ static struct PyMethodDef pg_polygon_methods[] = {
{"rotate_ip", (PyCFunction)pg_polygon_rotate_ip, METH_FASTCALL, NULL},
{"collidepoint", (PyCFunction)pg_polygon_collidepoint, METH_FASTCALL,
NULL},
{"collideline", (PyCFunction)pg_polygon_collideline, METH_FASTCALL, NULL},
{"collidecircle", (PyCFunction)pg_polygon_collidecircle, METH_FASTCALL,
NULL},
{"get_bounding_box", (PyCFunction)pg_polygon_get_bounding_box, METH_NOARGS,
Expand Down
Loading