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