Remove the promise of ducks
[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 .utils import debug_timer
12
13
14 def screen_rays(pos, bounding_radius):
15     """ An iterable that returns ordered rays from pos to the edge of the
16         screen, starting with the edge point (0, 0) and continuing clockwise
17         in pymunk coordinates.
18     """
19     r = int(bounding_radius)
20     left, right = int(pos.x) - r, int(pos.x) + r
21     bottom, top = int(pos.y) - r, int(pos.y) + r
22     step = 1
23     for y in range(bottom, top + 1, step):
24         yield pymunk.Vec2d(left, y)
25     for x in range(left, right + 1, step):
26         yield pymunk.Vec2d(x, top)
27     for y in range(top, bottom - 1, -step):
28         yield pymunk.Vec2d(right, y)
29     for x in range(right, left - 1, -step):
30         yield pymunk.Vec2d(x, bottom)
31
32
33 @debug_timer("rays.calculate_ray_polys")
34 def calculate_ray_polys(space, position, bounding_radius, 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(position, bounding_radius):
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, bounding_radius):
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         self._bounding_radius = None   # absolute maximum radius
92         if direction:
93             self.direction = direction  # Update direction
94         self._max_radius = None  # maximum radius in pixels
95         self._min_radius = None  # minimum radius in pixels
96         self._set_radius_limits(radius_limits)
97         self._set_bounding_radius(bounding_radius)
98         self._old_poly_cache = None  # last polys added to the space
99         self._poly_cache = None  # list of pymunk.Polys for rays
100         self._space = None  # space the rays form part of
101
102     def set_space(self, space):
103         self._space = space
104         self._rays = calculate_ray_polys(
105             self._space, self._position, self._bounding_radius,
106             self._ray_filter)
107         self._poly_cache = None
108
109     def update_shapes(self):
110         if self._old_poly_cache:
111             self._space.remove(*self._old_poly_cache)
112             for poly in self._old_poly_cache:
113                 poly.body = None
114         new_polys = self._old_poly_cache = self.polys()
115         self._space.add(*new_polys)
116
117     @property
118     def position(self):
119         return self._position
120
121     @property
122     def max_radius(self):
123         return self._max_radius
124
125     @max_radius.setter
126     def max_radius(self, value):
127         self._max_radius = value or 0.0
128
129     @property
130     def min_radius(self):
131         return self._min_radius
132
133     @min_radius.setter
134     def min_radius(self, value):
135         self._min_radius = value or 0.0
136
137     def serialize(self):
138         """ Return the required information from the ray_manager """
139         if self._direction is None:
140             direction = None
141             spread = None
142         else:
143             direction = self._direction.angle_degrees
144             spread = math.degrees(self.spread)
145         return {
146             "radius_limits": (self._min_radius, self._max_radius),
147             "direction": direction,
148             "spread": spread,
149         }
150
151     def reaches(self, position):
152         distance = self.position.get_distance(position)
153         return (self._min_radius <= distance <= self._max_radius)
154
155     def _set_radius_limits(self, radius_limits):
156         if radius_limits is None or not radius_limits[0]:
157             self._min_radius = 0
158         else:
159             self._min_radius = radius_limits[0]
160         if radius_limits is None or not radius_limits[1]:
161             self._max_radius = 50.0
162         else:
163             self._max_radius = radius_limits[1]
164
165     def _set_bounding_radius(self, bounding_radius):
166         if bounding_radius is None:
167             bounding_radius = self._max_radius
168         self._bounding_radius = bounding_radius
169
170     def rotatable(self):
171         return self._direction is not None
172
173     @property
174     def direction(self):
175         if self._direction is None:
176             return 0
177         return self._direction.angle_degrees
178
179     @direction.setter
180     def direction(self, degrees):
181         spread = self._direction.get_angle_between(self._start)
182         self._direction.angle_degrees = degrees
183         self._start = self._direction.rotated(spread)
184         self._end = self._direction.rotated(-spread)
185         self._poly_cache = None
186
187     @property
188     def spread(self):
189         if not self._direction:
190             return 2 * math.pi
191         return math.fabs(self._start.get_angle_between(self._end))
192
193     def _set_angle_limits(self, direction, spread):
194         if direction is None or spread is None:
195             self._direction = None
196             self._start = None
197             self._end = None
198         else:
199             self._direction = pymunk.Vec2d(1, 0)
200             self._start = self._direction.rotated_degrees(-spread/2.)
201             self._end = self._direction.rotated_degrees(spread/2.)
202         self._poly_cache = None
203
204     def polys(self):
205         if self._poly_cache is None:
206             self._poly_cache = poly_cache = []
207             for rp in self._rays:
208                 poly = rp.poly(self._start, self._end)
209                 if poly:
210                     poly.body = self._body
211                     poly.filter = self._ray_filter
212                     poly_cache.append(poly)
213         return self._poly_cache
214
215     def pygame_position(self, surface):
216         return pymunk.pygame_util.to_pygame(self._position, surface)
217
218     def pygame_rect(self, surface):
219         half_width = self.max_radius
220         rect_width = half_width * 2
221         rect_x, rect_y = pymunk.pygame_util.to_pygame(self._position, surface)
222         dest_rect = pygame.rect.Rect(rect_x, rect_y, rect_width, rect_width)
223         dest_rect.move_ip(-half_width, -half_width)
224         return dest_rect
225
226     def pygame_polys(self, surface):
227         return [
228             [pymunk.pygame_util.to_pygame(v, surface)
229              for v in poly.get_vertices()]
230             for poly in self.polys()
231         ]
232
233
234 class RayPoly(object):
235     def __init__(self, position, vertices):
236         self.position = position  # pointy end of the conical polygon
237         self.vertices = vertices  # all vertices in the polygon
238
239     def _between(self, v, start, end):
240         if start < end:
241             return start <= v <= end
242         return (start <= v) or (v <= end)
243
244     def poly(self, start, end):
245         trial = pymunk.Poly(None, self.vertices)
246         trial.update(pymunk.Transform.identity())
247
248         if start is None or end is None:
249             return trial  # no limits
250
251         start_info = trial.segment_query(
252             self.position + 1250 * start, self.position + 0.1 * start, 0)
253         end_info = trial.segment_query(
254             self.position + 1250 * end, self.position + 0.1 * end, 0)
255
256         vertices = self.vertices[:]
257         vertices = [
258             v for v in vertices
259             if self._between((v - self.position).angle, start.angle, end.angle)
260         ]
261         if start_info.shape is not None:
262             vertices.append(start_info.point)
263         if end_info.shape is not None:
264             vertices.append(end_info.point)
265         vertices.append(self.position)
266
267         poly = pymunk.Poly(None, vertices)
268         if len(poly.get_vertices()) < 3:
269             return None
270         return poly