Skip to content

Commit 46cc666

Browse files
authored
Merge pull request #194 from pygame-community/line_collidepolygon
Line `collidepolygon()` and Polygon `collideline()`
2 parents c440157 + 0bcfd10 commit 46cc666

File tree

11 files changed

+600
-3
lines changed

11 files changed

+600
-3
lines changed

docs/geometry.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ other objects.
126126

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

129+
collidepolygon: Checks if the line collides with the given polygon.
130+
129131
collideswith: Checks if the line collides with the given object.
130132

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

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

179+
collideline: Checks if the polygon collides with the given line.
180+
177181
collidecircle: Checks if the polygon collides with the given circle.
178182

179183
insert_vertex: Adds a vertex to the polygon.

docs/line.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,29 @@ Line Methods
320320

321321
.. ## Line.collidecircle ##
322322
323+
.. method:: collidepolygon
324+
325+
| :sl:`test if a line intersects with a polygon`
326+
| :sg:`collidepolygon(Polygon, only_edges=False) -> bool`
327+
| :sg:`collidepolygon((x1, y1), (x2, y2), ..., only_edges=False) -> bool`
328+
| :sg:`collidepolygon(x1, y1, x2, y2, ..., only_edges=False) -> bool`
329+
330+
Tests whether a given `Polygon` collides with the `Line`.
331+
It takes either a `Polygon` or Polygon-like object as an argument and it returns
332+
`True` if the polygon collides with the `Line`, `False` otherwise.
333+
334+
The optional `only_edges` argument can be set to `True` to only test whether the
335+
edges of the polygon intersect the `Line`. This means that a Line that is
336+
inscribed by the `Polygon` or completely outside of it will not be considered colliding.
337+
This can be useful for performance reasons if you only care about the edges of the
338+
polygon.
339+
340+
.. note::
341+
Keep in mind that the more vertices the polygon has, the more CPU time it will
342+
take to calculate the collision.
343+
344+
.. ## Line.collidepolygon ##
345+
323346
324347
.. method:: collideswith
325348

docs/polygon.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,23 @@ Polygon Methods
157157

158158
.. ## Polygon.collidepoint ##
159159
160+
.. method:: collideline
161+
162+
| :sl:`tests if a line intersects the polygon`
163+
| :sg:`collideline(Line, only_edges=False) -> bool`
164+
| :sg:`collideline((x1, y1), (x2, y2), only_edges=False) -> bool`
165+
| :sg:`collideline(x1, y1, x2, y2, only_edges=False) -> bool`
166+
167+
Tests whether a given `Line` collides with the `Polygon`.
168+
It takes either a `Line` or Line-like object as an argument and it returns `True`
169+
if the `Line` collides with the `Polygon`, `False` otherwise.
170+
171+
The optional `only_edges` argument can be set to `True` to only test whether the
172+
edges of the polygon intersect the `Line`. This means that a Line that is
173+
inscribed by the `Polygon` or completely outside of it will not be considered colliding.
174+
175+
.. ## Polygon.collideline ##
176+
160177
.. method:: collidecircle
161178

162179
| :sl:`tests if a circle is inside the polygon`
@@ -171,6 +188,7 @@ Polygon Methods
171188
The optional `only_edges` argument can be set to `True` to only test whether the
172189
edges of the polygon intersect the `Circle`. This means that a Polygon that is
173190
completely inscribed in, or circumscribed by the `Circle` will not be considered colliding.
191+
174192
This can be useful for performance reasons if you only care about the edges of the
175193
polygon.
176194

examples/polygon_line_collision.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from math import radians, sin, cos
2+
3+
import pygame
4+
from pygame.draw import line as draw_line, polygon as draw_polygon
5+
from geometry import Line, regular_polygon
6+
7+
8+
# using this because we're missing line.rotate(), rotates a line around its midpoint
9+
def rotate_line(line, d_ang):
10+
angle = radians(d_ang)
11+
ca, sa = cos(angle), sin(angle)
12+
xm, ym = line.midpoint
13+
ax, ay = line.a
14+
bx, by = line.b
15+
ax -= xm
16+
ay -= ym
17+
bx -= xm
18+
by -= ym
19+
new_ax, new_ay = xm + ax * ca - ay * sa, ym + ax * sa + ay * ca
20+
new_bx, new_by = xm + bx * ca - by * sa, ym + bx * sa + by * ca
21+
line.a, line.b = (new_ax, new_ay), (new_bx, new_by)
22+
23+
24+
pygame.init()
25+
26+
WHITE = (255, 255, 255)
27+
YELLOW = (255, 255, 0)
28+
RED = (255, 0, 0)
29+
SHAPE_THICKNESS = 3
30+
FPS = 60
31+
WIDTH, HEIGHT = 800, 800
32+
WIDTH2, HEIGHT2 = WIDTH // 2, HEIGHT // 2
33+
34+
screen = pygame.display.set_mode((WIDTH, HEIGHT))
35+
pygame.display.set_caption("Polygon-Line Collision Visualization")
36+
clock = pygame.time.Clock()
37+
38+
font = pygame.font.SysFont("Arial", 25, bold=True)
39+
colliding_text = font.render("Colliding", True, RED)
40+
colliding_textr = colliding_text.get_rect(center=(WIDTH2, 50))
41+
not_colliding_text = font.render("Not colliding", True, WHITE)
42+
not_colliding_textr = not_colliding_text.get_rect(center=(WIDTH2, 50))
43+
44+
modei_text = font.render("Right click to toggle collision mode", True, WHITE)
45+
modei_textr = modei_text.get_rect(center=(WIDTH2, HEIGHT - 50))
46+
47+
modei2_text = font.render("Left click to rotate", True, WHITE)
48+
modei2_textr = modei2_text.get_rect(center=(WIDTH2, HEIGHT - 80))
49+
50+
mode0_txt = font.render("Current: EDGES ONLY", True, YELLOW)
51+
mode0_txtr = mode0_txt.get_rect(center=(WIDTH2, HEIGHT - 25))
52+
53+
mode1_txt = font.render("Current: FULL", True, YELLOW)
54+
mode1_txtr = mode1_txt.get_rect(center=(WIDTH2, HEIGHT - 25))
55+
56+
polygon = regular_polygon(6, (WIDTH2, HEIGHT2), 180)
57+
line = Line((0, 0), (135, 135))
58+
only_edges = False
59+
running = True
60+
color = WHITE
61+
62+
rotating = False
63+
64+
while running:
65+
delta = (clock.tick(FPS) / 1000) * 60
66+
67+
line.midpoint = pygame.mouse.get_pos()
68+
69+
colliding = line.collidepolygon(polygon, only_edges)
70+
# Alternative:
71+
# colliding = polygon.collideline(line, only_edges)
72+
73+
if rotating:
74+
rotate_line(line, 2)
75+
76+
color = RED if colliding else WHITE
77+
78+
screen.fill((10, 10, 10))
79+
80+
draw_polygon(screen, color, polygon.vertices, SHAPE_THICKNESS)
81+
draw_line(screen, color, line.a, line.b, SHAPE_THICKNESS)
82+
83+
screen.blit(
84+
colliding_text if colliding else not_colliding_text,
85+
colliding_textr if colliding else not_colliding_textr,
86+
)
87+
88+
screen.blit(modei2_text, modei2_textr)
89+
screen.blit(modei_text, modei_textr)
90+
91+
screen.blit(
92+
mode0_txt if only_edges else mode1_txt, mode0_txtr if only_edges else mode1_txtr
93+
)
94+
95+
pygame.display.flip()
96+
97+
for event in pygame.event.get():
98+
if event.type == pygame.QUIT:
99+
running = False
100+
elif event.type == pygame.MOUSEBUTTONDOWN:
101+
if event.button == 1:
102+
rotating = True
103+
elif event.button == 3:
104+
only_edges = not only_edges
105+
elif event.type == pygame.MOUSEBUTTONUP:
106+
if event.button == 1:
107+
rotating = False
108+
pygame.quit()

geometry.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ class Line(Sequence[float]):
111111
def collidecircle(self, circle: CircleValue) -> bool: ...
112112
@overload
113113
def collidecircle(self, x: float, y: float, r: float) -> bool: ...
114+
def collidepolygon(self, polygon: Polygon, only_edges: bool = False) -> bool: ...
114115
def as_circle(self) -> Circle: ...
115116
def as_rect(self) -> Rect: ...
116117
@overload
@@ -243,6 +244,7 @@ class Polygon:
243244
def collidepoint(self, x: float, y: float) -> bool: ...
244245
@overload
245246
def collidepoint(self, point: Coordinate) -> bool: ...
247+
def collideline(self, line: LineValue, only_edges: bool = False) -> bool: ...
246248
def get_bounding_box(self) -> Rect: ...
247249
def is_convex(self) -> bool: ...
248250
@overload

src_c/collisions.c

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,68 @@ pgCollision_PolygonPoint(pgPolygonBase *poly, double x, double y)
321321
return collision;
322322
}
323323

324+
static inline int
325+
pgCollision_PolygonPoints(pgPolygonBase *poly, double x1, double y1, double x2,
326+
double y2)
327+
{
328+
int collision1 = 0, collision2 = 0;
329+
Py_ssize_t i, j;
330+
331+
double *vertices = poly->vertices;
332+
333+
for (i = 0, j = poly->verts_num - 1; i < poly->verts_num; j = i++) {
334+
double xi = vertices[i * 2];
335+
double yi = vertices[i * 2 + 1];
336+
double xj = vertices[j * 2];
337+
double yj = vertices[j * 2 + 1];
338+
339+
double xj_xi = xj - xi;
340+
double yj_yi = yj - yi;
341+
342+
if (((yi > y1) != (yj > y1)) &&
343+
(x1 < xj_xi * (y1 - yi) / yj_yi + xi)) {
344+
collision1 = !collision1;
345+
}
346+
347+
if (((yi > y2) != (yj > y2)) &&
348+
(x2 < xj_xi * (y2 - yi) / yj_yi + xi)) {
349+
collision2 = !collision2;
350+
}
351+
}
352+
353+
return collision1 || collision2;
354+
}
355+
356+
static inline int
357+
_pgCollision_line_polyedges(pgLineBase *line, pgPolygonBase *poly)
358+
{
359+
Py_ssize_t i, j;
360+
double *vertices = poly->vertices;
361+
362+
for (i = 0, j = poly->verts_num - 1; i < poly->verts_num; j = i++) {
363+
if (pgCollision_LineLine(
364+
line, &(pgLineBase){vertices[j * 2], vertices[j * 2 + 1],
365+
vertices[i * 2], vertices[i * 2 + 1]})) {
366+
return 1;
367+
}
368+
}
369+
370+
return 0;
371+
}
372+
373+
static inline int
374+
pgCollision_PolygonLine(pgPolygonBase *poly, pgLineBase *line, int only_edges)
375+
{
376+
int collision = _pgCollision_line_polyedges(line, poly);
377+
378+
if (collision || only_edges) {
379+
return collision;
380+
}
381+
382+
return pgCollision_PolygonPoints(poly, line->x1, line->y1, line->x2,
383+
line->y2);
384+
}
385+
324386
static int
325387
_pgCollision_PolygonPoint_opt(pgPolygonBase *poly, double x, double y)
326388
{

src_c/include/collisions.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ pgRaycast_LineCircle(pgLineBase *, pgCircleBase *, double, double *);
4343
static int
4444
pgCollision_PolygonPoint(pgPolygonBase *, double, double);
4545

46+
static int
47+
pgCollision_PolygonLine(pgPolygonBase *, pgLineBase *, int);
4648
static int
4749
pgCollision_CirclePolygon(pgCircleBase *, pgPolygonBase *, int);
4850

51+
4952
#endif /* ~_PG_COLLISIONS_H */

src_c/line.c

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ pgLine_FromObject(PyObject *obj, pgLineBase *out)
116116
}
117117
if (PySequence_Check(obj)) {
118118
length = PySequence_Length(obj);
119-
if (length == 4) {
119+
if (length == 4 && !pgPolygon_Check(obj)) {
120120
PyObject *tmp;
121121
tmp = PySequence_GetItem(obj, 0);
122122
if (!pg_DoubleFromObj(tmp, &(out->x1))) {
@@ -169,6 +169,9 @@ pgLine_FromObject(PyObject *obj, pgLineBase *out)
169169
Py_DECREF(sub);
170170
return IS_LINE_VALID(out);
171171
}
172+
else {
173+
return 0;
174+
}
172175
}
173176
if (PyObject_HasAttrString(obj, "line")) {
174177
PyObject *lineattr;
@@ -692,6 +695,32 @@ pg_line_scale_ip(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs) {
692695
Py_RETURN_NONE;
693696
}
694697

698+
static PyObject *
699+
pg_line_collidepolygon(pgLineObject *self, PyObject *const *args,
700+
Py_ssize_t nargs)
701+
{
702+
pgPolygonBase poly;
703+
int was_sequence, result, only_edges = 0;
704+
705+
/* Check for the optional only_edges argument */
706+
if (PyBool_Check(args[nargs - 1])) {
707+
only_edges = args[nargs - 1] == Py_True;
708+
nargs--;
709+
}
710+
711+
if (!pgPolygon_FromObjectFastcall(args, nargs, &poly, &was_sequence)) {
712+
return RAISE(
713+
PyExc_TypeError,
714+
"collidepolygon requires a Polygon or PolygonLike object");
715+
}
716+
717+
result = pgCollision_PolygonLine(&poly, &self->line, only_edges);
718+
719+
PG_FREEPOLY_COND(&poly, was_sequence);
720+
721+
return PyBool_FromLong(result);
722+
}
723+
695724
static PyObject *
696725
pg_line_as_circle(pgLineObject *self, PyObject *_null)
697726
{
@@ -718,6 +747,8 @@ static struct PyMethodDef pg_line_methods[] = {
718747
{"collidecircle", (PyCFunction)pg_line_collidecircle, METH_FASTCALL, NULL},
719748
{"colliderect", (PyCFunction)pg_line_colliderect, METH_FASTCALL, NULL},
720749
{"collideswith", (PyCFunction)pg_line_collideswith, METH_O, NULL},
750+
{"collidepolygon", (PyCFunction)pg_line_collidepolygon, METH_FASTCALL,
751+
NULL},
721752
{"as_rect", (PyCFunction)pg_line_as_rect, METH_NOARGS, NULL},
722753
{"update", (PyCFunction)pg_line_update, METH_FASTCALL, NULL},
723754
{"move", (PyCFunction)pg_line_move, METH_FASTCALL, NULL},

src_c/polygon.c

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1190,7 +1190,6 @@ pg_polygon_is_convex(pgPolygonObject *self, PyObject *_null)
11901190
{
11911191
return PyBool_FromLong(_pg_polygon_is_convex_helper(&self->polygon));
11921192
}
1193-
11941193
static int
11951194
_pg_polygon_scale_helper(pgPolygonBase *poly, double factor)
11961195
{
@@ -1258,6 +1257,27 @@ pg_polygon_scale_ip(pgPolygonObject *self, PyObject *arg)
12581257
Py_RETURN_NONE;
12591258
}
12601259

1260+
static PyObject *
1261+
pg_polygon_collideline(pgPolygonObject *self, PyObject *const *args,
1262+
Py_ssize_t nargs)
1263+
{
1264+
pgLineBase line;
1265+
int only_edges = 0;
1266+
1267+
/* Check for the optional only_edges argument */
1268+
if (PyBool_Check(args[nargs - 1])) {
1269+
only_edges = args[nargs - 1] == Py_True;
1270+
nargs--;
1271+
}
1272+
1273+
if (!pgLine_FromObjectFastcall(args, nargs, &line)) {
1274+
return RAISE(PyExc_TypeError, "Invalid line parameter");
1275+
}
1276+
1277+
return PyBool_FromLong(
1278+
pgCollision_PolygonLine(&self->polygon, &line, only_edges));
1279+
}
1280+
12611281
static PyObject *
12621282
pg_polygon_collidecircle(pgPolygonObject *self, PyObject *const *args,
12631283
Py_ssize_t nargs)
@@ -1287,6 +1307,7 @@ static struct PyMethodDef pg_polygon_methods[] = {
12871307
{"rotate_ip", (PyCFunction)pg_polygon_rotate_ip, METH_FASTCALL, NULL},
12881308
{"collidepoint", (PyCFunction)pg_polygon_collidepoint, METH_FASTCALL,
12891309
NULL},
1310+
{"collideline", (PyCFunction)pg_polygon_collideline, METH_FASTCALL, NULL},
12901311
{"collidecircle", (PyCFunction)pg_polygon_collidecircle, METH_FASTCALL,
12911312
NULL},
12921313
{"get_bounding_box", (PyCFunction)pg_polygon_get_bounding_box, METH_NOARGS,

0 commit comments

Comments
 (0)