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