Serialize light state across day and night transitions
[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 serialize(self):
133         """ Return the required information from the ray_manager """
134         if self._direction is None:
135             direction = None
136             spread = None
137         else:
138             direction = self._direction.angle_degrees
139             spread = math.degrees(self.spread)
140         return {
141             "radius_limits": (self._min_radius, self._max_radius),
142             "direction": direction,
143             "spread": spread,
144         }
145
146     def reaches(self, position):
147         distance = self.position.get_distance(position)
148         return (self._min_radius <= distance <= self._max_radius)
149
150     def _set_radius_limits(self, radius_limits):
151         if radius_limits is None or not radius_limits[0]:
152             self._min_radius = 0
153         else:
154             self._min_radius = radius_limits[0]
155         if radius_limits is None or not radius_limits[1]:
156             self._max_radius = 50.0
157         else:
158             self._max_radius = radius_limits[1]
159
160     def rotatable(self):
161         return self._direction is not None
162
163     @property
164     def direction(self):
165         if self._direction is None:
166             return 0
167         return self._direction.angle_degrees
168
169     @direction.setter
170     def direction(self, degrees):
171         spread = self._direction.get_angle_between(self._start)
172         self._direction.angle_degrees = degrees
173         self._start = self._direction.rotated(spread)
174         self._end = self._direction.rotated(-spread)
175         self._poly_cache = None
176
177     @property
178     def spread(self):
179         if not self._direction:
180             return 2 * math.pi
181         return math.fabs(self._start.get_angle_between(self._end))
182
183     def _set_angle_limits(self, direction, spread):
184         if direction is None or spread is None:
185             self._direction = None
186             self._start = None
187             self._end = None
188         else:
189             self._direction = pymunk.Vec2d(1, 0)
190             self._start = self._direction.rotated_degrees(-spread/2.)
191             self._end = self._direction.rotated_degrees(spread/2.)
192         self._poly_cache = None
193
194     def polys(self):
195         if self._poly_cache is None:
196             self._poly_cache = poly_cache = []
197             for rp in self._rays:
198                 poly = rp.poly(self._start, self._end)
199                 if poly:
200                     poly.body = self._body
201                     poly.filter = self._ray_filter
202                     poly_cache.append(poly)
203         return self._poly_cache
204
205     def pygame_position(self, surface):
206         return pymunk.pygame_util.to_pygame(self._position, surface)
207
208     def pygame_rect(self, surface):
209         half_width = self.max_radius
210         rect_width = half_width * 2
211         rect_x, rect_y = pymunk.pygame_util.to_pygame(self._position, surface)
212         dest_rect = pygame.rect.Rect(rect_x, rect_y, rect_width, rect_width)
213         dest_rect.move_ip(-half_width, -half_width)
214         return dest_rect
215
216     def pygame_polys(self, surface):
217         return [
218             [pymunk.pygame_util.to_pygame(v, surface)
219              for v in poly.get_vertices()]
220             for poly in self.polys()
221         ]
222
223
224 class RayPoly(object):
225     def __init__(self, position, vertices):
226         self.position = position  # pointy end of the conical polygon
227         self.vertices = vertices  # all vertices in the polygon
228
229     def _between(self, v, start, end):
230         if start < end:
231             return start <= v <= end
232         return (start <= v) or (v <= end)
233
234     def poly(self, start, end):
235         trial = pymunk.Poly(None, self.vertices)
236         trial.update(pymunk.Transform.identity())
237
238         if start is None or end is None:
239             return trial  # no limits
240
241         start_info = trial.segment_query(
242             self.position + 1250 * start, self.position + 0.1 * start, 0)
243         end_info = trial.segment_query(
244             self.position + 1250 * end, self.position + 0.1 * end, 0)
245
246         vertices = self.vertices[:]
247         vertices = [
248             v for v in vertices
249             if self._between((v - self.position).angle, start.angle, end.angle)
250         ]
251         if start_info.shape is not None:
252             vertices.append(start_info.point)
253         if end_info.shape is not None:
254             vertices.append(end_info.point)
255         vertices.append(self.position)
256
257         poly = pymunk.Poly(None, vertices)
258         if len(poly.get_vertices()) < 3:
259             return None
260         return poly