Skip to content

Commit 49f317a

Browse files
author
Aditya
committed
Add collision detection algorithms
Implements AABB, circle, circle-AABB, point-in-rectangle, and point-in-circle collision detection with comprehensive doctests. Fixes #12569
1 parent 840ca00 commit 49f317a

File tree

1 file changed

+299
-0
lines changed

1 file changed

+299
-0
lines changed

physics/collision_detection.py

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
"""
2+
Collision detection algorithms for 2D geometric shapes.
3+
4+
Collision detection is a fundamental concept in computational geometry, physics
5+
simulations, and game development. It determines whether two or more geometric
6+
objects intersect or overlap in space.
7+
8+
This module implements several common 2D collision detection algorithms:
9+
- Axis-Aligned Bounding Box (AABB) collision detection
10+
- Circle-circle collision detection
11+
- Circle-AABB collision detection
12+
- Point-in-rectangle detection
13+
- Point-in-circle detection
14+
15+
Reference: https://en.wikipedia.org/wiki/Collision_detection
16+
Reference: https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection
17+
"""
18+
19+
from __future__ import annotations
20+
21+
from math import sqrt
22+
23+
24+
def is_aabb_collision(
25+
x1: float, y1: float, w1: float, h1: float,
26+
x2: float, y2: float, w2: float, h2: float,
27+
) -> bool:
28+
"""
29+
Check if two Axis-Aligned Bounding Boxes (AABBs) are colliding.
30+
31+
Each rectangle is defined by its top-left corner (x, y), width (w),
32+
and height (h).
33+
34+
>>> is_aabb_collision(0, 0, 10, 10, 5, 5, 10, 10)
35+
True
36+
>>> is_aabb_collision(0, 0, 10, 10, 20, 20, 10, 10)
37+
False
38+
>>> is_aabb_collision(0, 0, 10, 10, 10, 0, 10, 10)
39+
False
40+
>>> is_aabb_collision(0, 0, 5, 5, 3, 3, 5, 5)
41+
True
42+
>>> is_aabb_collision(-5, -5, 10, 10, 0, 0, 10, 10)
43+
True
44+
>>> is_aabb_collision(0, 0, -1, 10, 5, 5, 10, 10)
45+
Traceback (most recent call last):
46+
...
47+
ValueError: Width and height must be non-negative
48+
>>> is_aabb_collision(0, 0, 10, 10, 5, 5, -1, 10)
49+
Traceback (most recent call last):
50+
...
51+
ValueError: Width and height must be non-negative
52+
"""
53+
if w1 < 0 or h1 < 0 or w2 < 0 or h2 < 0:
54+
raise ValueError("Width and height must be non-negative")
55+
56+
return x1 < x2 + w2 and x1 + w1 > x2 and y1 < y2 + h2 and y1 + h1 > y2
57+
58+
59+
def is_circle_collision(
60+
cx1: float, cy1: float, r1: float,
61+
cx2: float, cy2: float, r2: float,
62+
) -> bool:
63+
"""
64+
Check if two circles are colliding.
65+
66+
Each circle is defined by its center (cx, cy) and radius (r).
67+
68+
>>> is_circle_collision(0, 0, 5, 8, 0, 5)
69+
True
70+
>>> is_circle_collision(0, 0, 5, 20, 20, 5)
71+
False
72+
>>> is_circle_collision(0, 0, 10, 5, 5, 10)
73+
True
74+
>>> is_circle_collision(0, 0, 1, 3, 0, 1)
75+
False
76+
>>> is_circle_collision(0, 0, 0, 0, 0, 0)
77+
False
78+
>>> is_circle_collision(0, 0, -1, 5, 5, 3)
79+
Traceback (most recent call last):
80+
...
81+
ValueError: Radius must be non-negative
82+
"""
83+
if r1 < 0 or r2 < 0:
84+
raise ValueError("Radius must be non-negative")
85+
86+
distance_squared = (cx2 - cx1) ** 2 + (cy2 - cy1) ** 2
87+
radius_sum = r1 + r2
88+
return distance_squared < radius_sum ** 2
89+
90+
91+
def is_circle_aabb_collision(
92+
cx: float, cy: float, r: float,
93+
rx: float, ry: float, rw: float, rh: float,
94+
) -> bool:
95+
"""
96+
Check if a circle and an Axis-Aligned Bounding Box (AABB) are colliding.
97+
98+
The circle is defined by its center (cx, cy) and radius (r).
99+
The rectangle is defined by its top-left corner (rx, ry), width (rw),
100+
and height (rh).
101+
102+
>>> is_circle_aabb_collision(5, 5, 3, 0, 0, 10, 10)
103+
True
104+
>>> is_circle_aabb_collision(20, 20, 3, 0, 0, 10, 10)
105+
False
106+
>>> is_circle_aabb_collision(12, 5, 3, 0, 0, 10, 10)
107+
True
108+
>>> is_circle_aabb_collision(0, 0, 1, 5, 5, 10, 10)
109+
False
110+
>>> is_circle_aabb_collision(5, 5, -1, 0, 0, 10, 10)
111+
Traceback (most recent call last):
112+
...
113+
ValueError: Radius must be non-negative
114+
>>> is_circle_aabb_collision(5, 5, 3, 0, 0, -1, 10)
115+
Traceback (most recent call last):
116+
...
117+
ValueError: Width and height must be non-negative
118+
"""
119+
if r < 0:
120+
raise ValueError("Radius must be non-negative")
121+
if rw < 0 or rh < 0:
122+
raise ValueError("Width and height must be non-negative")
123+
124+
closest_x = max(rx, min(cx, rx + rw))
125+
closest_y = max(ry, min(cy, ry + rh))
126+
127+
distance_squared = (cx - closest_x) ** 2 + (cy - closest_y) ** 2
128+
return distance_squared < r**2
129+
130+
131+
def is_point_in_rectangle(
132+
px: float, py: float,
133+
rx: float, ry: float, rw: float, rh: float,
134+
) -> bool:
135+
"""
136+
Check if a point is inside an Axis-Aligned Bounding Box (rectangle).
137+
138+
The point is defined by (px, py).
139+
The rectangle is defined by its top-left corner (rx, ry), width (rw),
140+
and height (rh).
141+
142+
>>> is_point_in_rectangle(5, 5, 0, 0, 10, 10)
143+
True
144+
>>> is_point_in_rectangle(15, 15, 0, 0, 10, 10)
145+
False
146+
>>> is_point_in_rectangle(0, 0, 0, 0, 10, 10)
147+
True
148+
>>> is_point_in_rectangle(10, 10, 0, 0, 10, 10)
149+
False
150+
>>> is_point_in_rectangle(-1, 5, 0, 0, 10, 10)
151+
False
152+
>>> is_point_in_rectangle(5, 5, 0, 0, -1, 10)
153+
Traceback (most recent call last):
154+
...
155+
ValueError: Width and height must be non-negative
156+
"""
157+
if rw < 0 or rh < 0:
158+
raise ValueError("Width and height must be non-negative")
159+
160+
return rx <= px < rx + rw and ry <= py < ry + rh
161+
162+
163+
def is_point_in_circle(
164+
px: float, py: float,
165+
cx: float, cy: float, r: float,
166+
) -> bool:
167+
"""
168+
Check if a point is inside a circle.
169+
170+
The point is defined by (px, py).
171+
The circle is defined by its center (cx, cy) and radius (r).
172+
173+
>>> is_point_in_circle(3, 4, 0, 0, 10)
174+
True
175+
>>> is_point_in_circle(10, 10, 0, 0, 5)
176+
False
177+
>>> is_point_in_circle(0, 0, 0, 0, 1)
178+
True
179+
>>> is_point_in_circle(5, 0, 0, 0, 5)
180+
False
181+
>>> is_point_in_circle(3, 4, 0, 0, -1)
182+
Traceback (most recent call last):
183+
...
184+
ValueError: Radius must be non-negative
185+
"""
186+
if r < 0:
187+
raise ValueError("Radius must be non-negative")
188+
189+
distance_squared = (px - cx) ** 2 + (py - cy) ** 2
190+
return distance_squared < r**2
191+
192+
193+
def detect_all_collisions(
194+
objects: list[dict],
195+
) -> list[tuple[int, int]]:
196+
"""
197+
Detect all pairwise collisions among a list of geometric objects.
198+
199+
Each object is a dictionary with a 'type' key ('circle' or 'rect') and
200+
the corresponding geometric parameters.
201+
202+
Circle: {'type': 'circle', 'cx': float, 'cy': float, 'r': float}
203+
Rectangle: {'type': 'rect', 'x': float, 'y': float, 'w': float, 'h': float}
204+
205+
Returns a list of tuples (i, j) where objects[i] and objects[j] collide.
206+
207+
>>> objects = [
208+
... {'type': 'circle', 'cx': 0, 'cy': 0, 'r': 5},
209+
... {'type': 'circle', 'cx': 3, 'cy': 0, 'r': 5},
210+
... {'type': 'circle', 'cx': 100, 'cy': 100, 'r': 1},
211+
... ]
212+
>>> detect_all_collisions(objects)
213+
[(0, 1)]
214+
>>> objects = [
215+
... {'type': 'rect', 'x': 0, 'y': 0, 'w': 10, 'h': 10},
216+
... {'type': 'rect', 'x': 5, 'y': 5, 'w': 10, 'h': 10},
217+
... {'type': 'circle', 'cx': 20, 'cy': 20, 'r': 3},
218+
... ]
219+
>>> detect_all_collisions(objects)
220+
[(0, 1)]
221+
>>> detect_all_collisions([])
222+
[]
223+
"""
224+
collisions: list[tuple[int, int]] = []
225+
for i in range(len(objects)):
226+
for j in range(i + 1, len(objects)):
227+
if _check_collision(objects[i], objects[j]):
228+
collisions.append((i, j))
229+
return collisions
230+
231+
232+
def _check_collision(obj1: dict, obj2: dict) -> bool:
233+
"""
234+
Check collision between two geometric objects.
235+
236+
>>> _check_collision(
237+
... {'type': 'circle', 'cx': 0, 'cy': 0, 'r': 5},
238+
... {'type': 'circle', 'cx': 3, 'cy': 0, 'r': 5},
239+
... )
240+
True
241+
>>> _check_collision(
242+
... {'type': 'rect', 'x': 0, 'y': 0, 'w': 10, 'h': 10},
243+
... {'type': 'rect', 'x': 20, 'y': 20, 'w': 5, 'h': 5},
244+
... )
245+
False
246+
"""
247+
type1, type2 = obj1["type"], obj2["type"]
248+
249+
if type1 == "circle" and type2 == "circle":
250+
return is_circle_collision(
251+
obj1["cx"], obj1["cy"], obj1["r"],
252+
obj2["cx"], obj2["cy"], obj2["r"],
253+
)
254+
255+
if type1 == "rect" and type2 == "rect":
256+
return is_aabb_collision(
257+
obj1["x"], obj1["y"], obj1["w"], obj1["h"],
258+
obj2["x"], obj2["y"], obj2["w"], obj2["h"],
259+
)
260+
261+
if type1 == "circle" and type2 == "rect":
262+
return is_circle_aabb_collision(
263+
obj1["cx"], obj1["cy"], obj1["r"],
264+
obj2["x"], obj2["y"], obj2["w"], obj2["h"],
265+
)
266+
267+
if type1 == "rect" and type2 == "circle":
268+
return is_circle_aabb_collision(
269+
obj2["cx"], obj2["cy"], obj2["r"],
270+
obj1["x"], obj1["y"], obj1["w"], obj1["h"],
271+
)
272+
273+
msg = f"Unknown object types: {type1}, {type2}"
274+
raise ValueError(msg)
275+
276+
277+
if __name__ == "__main__":
278+
import doctest
279+
280+
doctest.testmod()
281+
282+
print("AABB collision:", is_aabb_collision(0, 0, 10, 10, 5, 5, 10, 10))
283+
print("Circle collision:", is_circle_collision(0, 0, 5, 8, 0, 5))
284+
print("Point in rect:", is_point_in_rectangle(5, 5, 0, 0, 10, 10))
285+
print("Point in circle:", is_point_in_circle(3, 4, 0, 0, 10))
286+
print(
287+
"Circle-AABB collision:",
288+
is_circle_aabb_collision(5, 5, 3, 0, 0, 10, 10),
289+
)
290+
print(
291+
"Detect all:",
292+
detect_all_collisions(
293+
[
294+
{"type": "circle", "cx": 0, "cy": 0, "r": 5},
295+
{"type": "circle", "cx": 3, "cy": 0, "r": 5},
296+
{"type": "rect", "x": 100, "y": 100, "w": 10, "h": 10},
297+
]
298+
),
299+
)

0 commit comments

Comments
 (0)