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 pymunk
6 import pymunk.autogeometry
7 import pymunk.pygame_util
8
9 from .constants import SCREEN_SIZE
10 from .utils import debug_timer
11
12
13 def screen_rays(pos):
14     """ An iterable that returns ordered rays from pos to the edge of the
15         screen, starting with the edge point (0, 0) and continuing clockwise
16         in pymunk coordinates.
17     """
18     w, h = SCREEN_SIZE
19     left, right, bottom, top = 0, w, 0, h
20     step = 1
21     for y in range(0, h, step):
22         yield pymunk.Vec2d(left, y)
23     for x in range(0, w, step):
24         yield pymunk.Vec2d(x, top)
25     for y in range(top, -1, -step):
26         yield pymunk.Vec2d(right, y)
27     for x in range(right, -1, -step):
28         yield pymunk.Vec2d(x, bottom)
29
30
31 @debug_timer("rays.calculate_ray_polys")
32 def calculate_ray_polys(space, position, light_filter):
33     """ Calculate a set of convex RayPolys that cover all the areas that light
34         can reach from the given position, taking into account the obstacles
35         present in the space.
36     """
37     position = pymunk.Vec2d(position)
38     vertices = [position]
39     start, end = None, None
40     ray_polys = []
41     for ray in screen_rays(position):
42         info = space.segment_query_first(position, ray, 1, light_filter)
43         point = ray if info is None else info.point
44         vertices.append(point)
45         if len(vertices) == 2:
46             start = vertices[1]
47         elif len(vertices) > 3:
48             trial_poly = pymunk.Poly(None, vertices)
49             trial_poly.update(pymunk.Transform.identity())
50             query_prev = trial_poly.point_query(end)
51             query_pos = trial_poly.point_query(position)
52             if query_prev.distance < -0.01 or query_pos.distance < -0.01:
53                 ray_polys.append(RayPoly(position, vertices[:-1]))
54                 start = vertices[-1]
55                 vertices = [position, start]
56             else:
57                 vertices = trial_poly.get_vertices()
58         end = point
59     if len(vertices) > 2:
60         ray_polys.append(RayPoly(position, vertices))
61     return ray_polys
62
63
64 def to_pymunk_radians(deg):
65     """ Convert degrees in [0, 360] to radians in (-pi, pi].
66
67         Return None if degrees is None.
68     """
69     if deg is None:
70         return None
71     deg = deg * math.pi / 180.0
72     if deg > math.pi:
73         deg -= 2 * math.pi
74     return deg
75
76
77 class RayPolyManager(object):
78     def __init__(self, body, ray_filter):
79         self._body = body  # light's body
80         self._ray_filter = ray_filter  # light filter
81         self._rays = []  # list of RayPolys
82         self._start = None  # normal vector in direction of start angle limit
83         self._end = None  # normal vector in direction of end angle limit
84         self._poly_cache = None  # list of pymunk.Polys for rays
85
86     def generate_rays(self, space, position):
87         self._rays = calculate_ray_polys(space, position, self._ray_filter)
88         self._poly_cache = None
89
90     def set_angle_limits(self, angle_limits):
91         if angle_limits is None:
92             self._start = None
93             self._end = None
94         else:
95             self._start = pymunk.Vec2d(1, 0).rotated(
96                 to_pymunk_radians(angle_limits[0]))
97             self._end = pymunk.Vec2d(1, 0).rotated(
98                 to_pymunk_radians(angle_limits[1]))
99         self._poly_cache = None
100
101     def polys(self):
102         if self._poly_cache is None:
103             self._poly_cache = poly_cache = []
104             for rp in self._rays:
105                 poly = rp.poly(self._start, self._end)
106                 if poly:
107                     poly.body = self._body
108                     poly.filter = self._ray_filter
109                     poly_cache.append(poly)
110         return self._poly_cache
111
112     def pygame_polys(self, surface):
113         return [
114             [pymunk.pygame_util.to_pygame(v, surface)
115              for v in poly.get_vertices()]
116             for poly in self.polys()
117         ]
118
119
120 class RayPoly(object):
121     def __init__(self, position, vertices):
122         self.position = position  # pointy end of the conical polygon
123         self.vertices = vertices  # all vertices in the polygon
124
125     def _between(self, v, start, end):
126         if start < end:
127             return start <= v <= end
128         return (start <= v) or (v <= end)
129
130     def poly(self, start, end):
131         trial = pymunk.Poly(None, self.vertices)
132         trial.update(pymunk.Transform.identity())
133
134         if start is None or end is None:
135             return trial  # no limits
136
137         start_info = trial.segment_query(
138             self.position + 0.1 * start, self.position + 1250 * start, 1)
139         end_info = trial.segment_query(
140             self.position + 0.1 * end, self.position + 1250 * end, 1)
141
142         vertices = self.vertices[:]
143         if start_info:
144             vertices.append(start_info.point)
145         if end_info:
146             vertices.append(end_info.point)
147         vertices = [
148             v for v in vertices
149             if self._between((v - self.position).angle, start.angle, end.angle)
150         ]
151         vertices.append(self.position)
152
153         poly = pymunk.Poly(None, vertices)
154         if len(poly.get_vertices()) < 3:
155             return None
156         return poly