Remove some hacking.
[tabakrolletjie.git] / tabakrolletjie / rays.py
1 """ Light ray manipulation. Pew. Pew. Pew. Wommmm. """
2
3 import math
4
5 import pymunk
6 import pymunk.autogeometry
7 import pymunk.pygame_util
8
9 from .constants import SCREEN_SIZE
10 from .utils import debug_timer
11
12
13 def screen_rays(pos):
14     """ An iterable that returns ordered rays from pos to the edge of the
15         screen, starting with the edge point (0, 0) and continuing clockwise
16         in pymunk coordinates.
17     """
18     w, h = SCREEN_SIZE
19     left, right, bottom, top = 0, w, 0, h
20     step = 1
21     for y in range(0, h, step):
22         yield pymunk.Vec2d(left, y)
23     for x in range(0, w, step):
24         yield pymunk.Vec2d(x, top)
25     for y in range(top, -1, -step):
26         yield pymunk.Vec2d(right, y)
27     for x in range(right, -1, -step):
28         yield pymunk.Vec2d(x, bottom)
29
30
31 @debug_timer("rays.calculate_ray_polys")
32 def calculate_ray_polys(space, position, light_filter):
33     """ Calculate a set of convex RayPolys that cover all the areas that light
34         can reach from the given position, taking into account the obstacles
35         present in the space.
36     """
37     position = pymunk.Vec2d(position)
38     vertices = [position]
39     start, end = None, None
40     ray_polys = []
41     for ray in screen_rays(position):
42         info = space.segment_query_first(position, ray, 1, light_filter)
43         point = ray if info is None else info.point
44         vertices.append(point)
45         if len(vertices) == 2:
46             start = vertices[1]
47         elif len(vertices) > 3:
48             trial_poly = pymunk.Poly(None, vertices)
49             trial_poly.update(pymunk.Transform.identity())
50             query_prev = trial_poly.point_query(end)
51             query_pos = trial_poly.point_query(position)
52             if query_prev.distance < -0.01 or query_pos.distance < -0.01:
53                 ray_polys.append(RayPoly(
54                     start - position, end - position, vertices[:-1]))
55                 start = vertices[-1]
56                 vertices = [position, start]
57             else:
58                 vertices = trial_poly.get_vertices()
59         end = point
60     if len(vertices) > 2:
61         ray_polys.append(RayPoly(start, end, vertices))
62     return ray_polys
63
64
65 def to_pymunk_radians(deg):
66     """ Convert degrees in [0, 360] to radians in (-pi, pi].
67
68         Return None if degrees is None.
69     """
70     if deg is None:
71         return None
72     deg = deg * math.pi / 180.0
73     if deg > math.pi:
74         deg -= 2 * math.pi
75     return deg
76
77
78 class RayPolyManager(object):
79     def __init__(self, body, ray_filter):
80         self._body = body
81         self._ray_filter = ray_filter
82         self._rays = []
83         self._angle_limits = (None, None)
84         self._poly_cache = None
85         self._pygame_poly_cache = None
86
87     def generate_rays(self, space, position):
88         self._rays = calculate_ray_polys(space, position, self._ray_filter)
89         self._poly_cache = None
90         self._pygame_poly_cache = None
91
92     def set_angle_limits(self, angle_limits):
93         start, end = angle_limits
94         self._angle_limits = (
95             to_pymunk_radians(start), to_pymunk_radians(end))
96         self._poly_cache = None
97         self._pygame_poly_cache = None
98
99     def polys(self):
100         if self._poly_cache is None:
101             self._poly_cache = [
102                 rp.poly(self._body, self._ray_filter) for rp in self._rays
103                 if rp.within_limits(*self._angle_limits)
104             ]
105         return self._poly_cache
106
107     def pygame_polys(self, surface):
108         if self._pygame_poly_cache is None:
109             self._pygame_poly_cache = [
110                 [
111                     pymunk.pygame_util.to_pygame(v, surface)
112                     for v in poly.get_vertices()
113                 ]
114                 for poly in self.polys()
115             ]
116         return self._pygame_poly_cache
117
118
119 class RayPoly(object):
120     def __init__(self, start_vec, end_vec, vertices):
121         self.start = start_vec  # vector from position to first point
122         self.end = end_vec  # vector from position to last point
123         self.vertices = vertices
124
125     def poly(self, body, filter):
126         shape = pymunk.Poly(body, self.vertices)
127         shape.filter = filter
128         return shape
129
130     def within_limits(self, start_limit, end_limit):
131         if start_limit is None or end_limit is None:
132             return True
133
134         def between(n):
135             if start_limit < end_limit:
136                 return start_limit <= n <= end_limit
137             return (start_limit <= n) or (n <= end_limit)
138
139         start_within = between(self.start.angle)
140         end_within = between(self.end.angle)
141         if start_within or end_within:
142             return True
143         return False