Merge branch 'master' of ctpug.org.za:tabakrolletjie
[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     @property
160     def spread(self):
161         if not self._direction:
162             return 2 * math.pi
163         return math.fabs(self._start.get_angle_between(self._end))
164
165     def _set_angle_limits(self, direction, spread):
166         if direction is None or spread is None:
167             self._direction = None
168             self._start = None
169             self._end = None
170         else:
171             self._direction = pymunk.Vec2d(1, 0)
172             self._start = self._direction.rotated_degrees(-spread/2.)
173             self._end = self._direction.rotated_degrees(spread/2.)
174         self._poly_cache = None
175
176     def polys(self):
177         if self._poly_cache is None:
178             self._poly_cache = poly_cache = []
179             for rp in self._rays:
180                 poly = rp.poly(self._start, self._end)
181                 if poly:
182                     poly.body = self._body
183                     poly.filter = self._ray_filter
184                     poly_cache.append(poly)
185         return self._poly_cache
186
187     def pygame_position(self, surface):
188         return pymunk.pygame_util.to_pygame(self._position, surface)
189
190     def pygame_rect(self, surface):
191         half_width = self.max_radius
192         rect_width = half_width * 2
193         rect_x, rect_y = pymunk.pygame_util.to_pygame(self._position, surface)
194         dest_rect = pygame.rect.Rect(rect_x, rect_y, rect_width, rect_width)
195         dest_rect.move_ip(-half_width, -half_width)
196         return dest_rect
197
198     def pygame_polys(self, surface):
199         return [
200             [pymunk.pygame_util.to_pygame(v, surface)
201              for v in poly.get_vertices()]
202             for poly in self.polys()
203         ]
204
205
206 class RayPoly(object):
207     def __init__(self, position, vertices):
208         self.position = position  # pointy end of the conical polygon
209         self.vertices = vertices  # all vertices in the polygon
210
211     def _between(self, v, start, end):
212         if start < end:
213             return start <= v <= end
214         return (start <= v) or (v <= end)
215
216     def poly(self, start, end):
217         trial = pymunk.Poly(None, self.vertices)
218         trial.update(pymunk.Transform.identity())
219
220         if start is None or end is None:
221             return trial  # no limits
222
223         start_info = trial.segment_query(
224             self.position + 1250 * start, self.position + 0.1 * start, 0)
225         end_info = trial.segment_query(
226             self.position + 1250 * end, self.position + 0.1 * end, 0)
227
228         vertices = self.vertices[:]
229         vertices = [
230             v for v in vertices
231             if self._between((v - self.position).angle, start.angle, end.angle)
232         ]
233         if start_info.shape is not None:
234             vertices.append(start_info.point)
235         if end_info.shape is not None:
236             vertices.append(end_info.point)
237         vertices.append(self.position)
238
239         poly = pymunk.Poly(None, vertices)
240         if len(poly.get_vertices()) < 3:
241             return None
242         return poly