Remove unused pos arg.
[tabakrolletjie.git] / tabakrolletjie / rays.py
1 """ Light ray manipulation. Pew. Pew. Pew. Wommmm. """
2
3 import math
4
5 import pygame.rect
6
7 import pymunk
8 import pymunk.autogeometry
9 import pymunk.pygame_util
10
11 from .constants import SCREEN_SIZE
12 from .utils import debug_timer
13
14
15 def screen_rays():
16     """ An iterable that returns ordered rays from pos to the edge of the
17         screen, starting with the edge point (0, 0) and continuing clockwise
18         in pymunk coordinates.
19     """
20     w, h = SCREEN_SIZE
21     left, right, bottom, top = 0, w, 0, h
22     step = 1
23     for y in range(0, h, step):
24         yield pymunk.Vec2d(left, y)
25     for x in range(0, w, step):
26         yield pymunk.Vec2d(x, top)
27     for y in range(top, -1, -step):
28         yield pymunk.Vec2d(right, y)
29     for x in range(right, -1, -step):
30         yield pymunk.Vec2d(x, bottom)
31
32
33 @debug_timer("rays.calculate_ray_polys")
34 def calculate_ray_polys(space, position, light_filter):
35     """ Calculate a set of convex RayPolys that cover all the areas that light
36         can reach from the given position, taking into account the obstacles
37         present in the space.
38     """
39     position = pymunk.Vec2d(position)
40     vertices = [position]
41     start, end = None, None
42     ray_polys = []
43     for ray in screen_rays():
44         info = space.segment_query_first(position, ray, 1, light_filter)
45         point = ray if info is None else info.point
46         vertices.append(point)
47         if len(vertices) == 2:
48             start = vertices[1]
49         elif len(vertices) > 3:
50             trial_poly = pymunk.Poly(None, vertices)
51             trial_poly.update(pymunk.Transform.identity())
52             query_prev = trial_poly.point_query(end)
53             query_pos = trial_poly.point_query(position)
54             if query_prev.distance < -0.01 or query_pos.distance < -0.01:
55                 ray_polys.append(RayPoly(position, vertices[:-1]))
56                 start = vertices[-1]
57                 vertices = [position, start]
58             else:
59                 vertices = trial_poly.get_vertices()
60         end = point
61     if len(vertices) > 2:
62         ray_polys.append(RayPoly(position, vertices))
63     return ray_polys
64
65
66 def to_pymunk_radians(deg):
67     """ Convert degrees in [0, 360] to radians in (-pi, pi].
68
69         Return None if degrees is None.
70     """
71     if deg is None:
72         return None
73     deg = deg * math.pi / 180.0
74     if deg > math.pi:
75         deg -= 2 * math.pi
76     return deg
77
78
79 class RayPolyManager(object):
80     def __init__(
81             self, body, position, ray_filter, radius_limits, direction,
82             spread):
83         self._body = body  # light's body
84         self._position = pymunk.Vec2d(position)  # light's position
85         self._ray_filter = ray_filter  # light filter
86         self._rays = []  # list of RayPolys
87         self._direction = None  # normal vector for direction
88         self._start = None  # normal vector in direction of start angle limit
89         self._end = None  # normal vector in direction of end angle limit
90         self._set_angle_limits(direction, spread)
91         if direction:
92             self.direction = direction  # Update direction
93         self._max_radius = None  # maximum radius in pixels
94         self._min_radius = None  # minimum radius in pixels
95         self._set_radius_limits(radius_limits)
96         self._old_poly_cache = None  # last polys added to the space
97         self._poly_cache = None  # list of pymunk.Polys for rays
98         self._space = None  # space the rays form part of
99
100     def set_space(self, space):
101         self._space = space
102         self._rays = calculate_ray_polys(
103             self._space, self._position, self._ray_filter)
104         self._poly_cache = None
105
106     def update_shapes(self):
107         if self._old_poly_cache:
108             self._space.remove(*self._old_poly_cache)
109         new_polys = self._old_poly_cache = self.polys()
110         self._space.add(*new_polys)
111
112     @property
113     def position(self):
114         return self._position
115
116     @property
117     def max_radius(self):
118         return self._max_radius
119
120     @max_radius.setter
121     def max_radius(self, value):
122         self._max_radius = value or 0.0
123
124     @property
125     def min_radius(self):
126         return self._min_radius
127
128     @min_radius.setter
129     def min_radius(self, value):
130         self._min_radius = value or 0.0
131
132     def reaches(self, position):
133         distance = self.position.get_distance(position)
134         return (self._min_radius <= distance <= self._max_radius)
135
136     def _set_radius_limits(self, radius_limits):
137         if radius_limits is None or not radius_limits[0]:
138             self._min_radius = 0
139         else:
140             self._min_radius = radius_limits[0]
141         if radius_limits is None or not radius_limits[1]:
142             self._max_radius = 50.0
143         else:
144             self._max_radius = radius_limits[1]
145
146     def rotatable(self):
147         return self._direction is not None
148
149     @property
150     def direction(self):
151         if self._direction is None:
152             return 0
153         return self._direction.angle_degrees
154
155     @direction.setter
156     def direction(self, degrees):
157         spread = self._direction.get_angle_between(self._start)
158         self._direction.angle_degrees = degrees
159         self._start = self._direction.rotated(spread)
160         self._end = self._direction.rotated(-spread)
161         self._poly_cache = None
162
163     @property
164     def spread(self):
165         if not self._direction:
166             return 2 * math.pi
167         return math.fabs(self._start.get_angle_between(self._end))
168
169     def _set_angle_limits(self, direction, spread):
170         if direction is None or spread is None:
171             self._direction = None
172             self._start = None
173             self._end = None
174         else:
175             self._direction = pymunk.Vec2d(1, 0)
176             self._start = self._direction.rotated_degrees(-spread/2.)
177             self._end = self._direction.rotated_degrees(spread/2.)
178         self._poly_cache = None
179
180     def polys(self):
181         if self._poly_cache is None:
182             self._poly_cache = poly_cache = []
183             for rp in self._rays:
184                 poly = rp.poly(self._start, self._end)
185                 if poly:
186                     poly.body = self._body
187                     poly.filter = self._ray_filter
188                     poly_cache.append(poly)
189         return self._poly_cache
190
191     def pygame_position(self, surface):
192         return pymunk.pygame_util.to_pygame(self._position, surface)
193
194     def pygame_rect(self, surface):
195         half_width = self.max_radius
196         rect_width = half_width * 2
197         rect_x, rect_y = pymunk.pygame_util.to_pygame(self._position, surface)
198         dest_rect = pygame.rect.Rect(rect_x, rect_y, rect_width, rect_width)
199         dest_rect.move_ip(-half_width, -half_width)
200         return dest_rect
201
202     def pygame_polys(self, surface):
203         return [
204             [pymunk.pygame_util.to_pygame(v, surface)
205              for v in poly.get_vertices()]
206             for poly in self.polys()
207         ]
208
209
210 class RayPoly(object):
211     def __init__(self, position, vertices):
212         self.position = position  # pointy end of the conical polygon
213         self.vertices = vertices  # all vertices in the polygon
214
215     def _between(self, v, start, end):
216         if start < end:
217             return start <= v <= end
218         return (start <= v) or (v <= end)
219
220     def poly(self, start, end):
221         trial = pymunk.Poly(None, self.vertices)
222         trial.update(pymunk.Transform.identity())
223
224         if start is None or end is None:
225             return trial  # no limits
226
227         start_info = trial.segment_query(
228             self.position + 1250 * start, self.position + 0.1 * start, 0)
229         end_info = trial.segment_query(
230             self.position + 1250 * end, self.position + 0.1 * end, 0)
231
232         vertices = self.vertices[:]
233         vertices = [
234             v for v in vertices
235             if self._between((v - self.position).angle, start.angle, end.angle)
236         ]
237         if start_info.shape is not None:
238             vertices.append(start_info.point)
239         if end_info.shape is not None:
240             vertices.append(end_info.point)
241         vertices.append(self.position)
242
243         poly = pymunk.Poly(None, vertices)
244         if len(poly.get_vertices()) < 3:
245             return None
246         return poly