Add direction property to ray manager.
[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(pos):
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(position):
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         self._max_radius = None  # maximum radius in pixels
92         self._min_radius = None  # minimum radius in pixels
93         self._set_radius_limits(radius_limits)
94         self._old_poly_cache = None  # last polys added to the space
95         self._poly_cache = None  # list of pymunk.Polys for rays
96         self._space = None  # space the rays form part of
97
98     def set_space(self, space):
99         self._space = space
100         self._rays = calculate_ray_polys(
101             self._space, self._position, self._ray_filter)
102         self._poly_cache = None
103
104     def update_shapes(self):
105         if self._old_poly_cache:
106             self._space.remove(*self._old_poly_cache)
107         new_polys = self._old_poly_cache = self.polys()
108         self._space.add(*new_polys)
109
110     @property
111     def position(self):
112         return self._position
113
114     @property
115     def max_radius(self):
116         return self._max_radius
117
118     @max_radius.setter
119     def max_radius(self, value):
120         self._max_radius = value or 0.0
121
122     @property
123     def min_radius(self):
124         return self._min_radius
125
126     @min_radius.setter
127     def min_radius(self, value):
128         self._min_radius = value or 0.0
129
130     def reaches(self, position):
131         distance = self.position.get_distance(self.position)
132         return (self._min_radius <= distance <= self._max_radius)
133
134     def _set_radius_limits(self, radius_limits):
135         if radius_limits is None or not radius_limits[0]:
136             self._min_radius = 0
137         else:
138             self._min_radius = radius_limits[0]
139         if radius_limits is None or not radius_limits[1]:
140             self._max_radius = 50.0
141         else:
142             self._max_radius = radius_limits[1]
143
144     def rotatable(self):
145         return self._direction is not None
146
147     @property
148     def direction(self):
149         return self._direction.angle_degrees
150
151     @direction.setter
152     def direction(self, degrees):
153         spread = self._direction.get_angle_between(self._start)
154         self._direction.angle_degrees = degrees
155         self._start = self._direction.rotated(spread)
156         self._end = self._direction.rotated(-spread)
157         self._poly_cache = None
158
159     def _set_angle_limits(self, direction, spread):
160         if direction is None or spread is None:
161             self._direction = None
162             self._start = None
163             self._end = None
164         else:
165             self._direction = pymunk.Vec2d(1, 0)
166             self._start = self._direction.rotated_degrees(-spread/2.)
167             self._end = self._direction.rotated_degrees(spread/2.)
168         self._poly_cache = None
169
170     def polys(self):
171         if self._poly_cache is None:
172             self._poly_cache = poly_cache = []
173             for rp in self._rays:
174                 poly = rp.poly(self._start, self._end)
175                 if poly:
176                     poly.body = self._body
177                     poly.filter = self._ray_filter
178                     poly_cache.append(poly)
179         return self._poly_cache
180
181     def pygame_position(self, surface):
182         return pymunk.pygame_util.to_pygame(self._position, surface)
183
184     def pygame_rect(self, surface):
185         half_width = self.max_radius
186         rect_width = half_width * 2
187         rect_x, rect_y = pymunk.pygame_util.to_pygame(self._position, surface)
188         dest_rect = pygame.rect.Rect(rect_x, rect_y, rect_width, rect_width)
189         dest_rect.move_ip(-half_width, -half_width)
190         return dest_rect
191
192     def pygame_polys(self, surface):
193         return [
194             [pymunk.pygame_util.to_pygame(v, surface)
195              for v in poly.get_vertices()]
196             for poly in self.polys()
197         ]
198
199
200 class RayPoly(object):
201     def __init__(self, position, vertices):
202         self.position = position  # pointy end of the conical polygon
203         self.vertices = vertices  # all vertices in the polygon
204
205     def _between(self, v, start, end):
206         if start < end:
207             return start <= v <= end
208         return (start <= v) or (v <= end)
209
210     def poly(self, start, end):
211         trial = pymunk.Poly(None, self.vertices)
212         trial.update(pymunk.Transform.identity())
213
214         if start is None or end is None:
215             return trial  # no limits
216
217         start_info = trial.segment_query(
218             self.position + 1250 * start, self.position + 0.1 * start, 0)
219         end_info = trial.segment_query(
220             self.position + 1250 * end, self.position + 0.1 * end, 0)
221
222         vertices = self.vertices[:]
223         vertices = [
224             v for v in vertices
225             if self._between((v - self.position).angle, start.angle, end.angle)
226         ]
227         if start_info.shape is not None:
228             vertices.append(start_info.point)
229         if end_info.shape is not None:
230             vertices.append(end_info.point)
231         vertices.append(self.position)
232
233         poly = pymunk.Poly(None, vertices)
234         if len(poly.get_vertices()) < 3:
235             return None
236         return poly