1 """ Light ray manipulation. Pew. Pew. Pew. Wommmm. """
8 import pymunk.autogeometry
9 import pymunk.pygame_util
11 from .constants import SCREEN_SIZE
12 from .utils import debug_timer
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.
21 left, right, bottom, top = 0, w, 0, h
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)
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
39 position = pymunk.Vec2d(position)
41 start, end = None, None
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:
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]))
57 vertices = [position, start]
59 vertices = trial_poly.get_vertices()
62 ray_polys.append(RayPoly(position, vertices))
66 def to_pymunk_radians(deg):
67 """ Convert degrees in [0, 360] to radians in (-pi, pi].
69 Return None if degrees is None.
73 deg = deg * math.pi / 180.0
79 class RayPolyManager(object):
81 self, body, position, ray_filter, radius_limits, direction,
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
97 def set_space(self, space):
99 self._rays = calculate_ray_polys(
100 self._space, self._position, self._ray_filter)
101 self._poly_cache = None
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)
111 return self._position
114 def max_radius(self):
115 return self._max_radius
118 def max_radius(self, value):
119 self._max_radius = value or 0.0
122 def min_radius(self):
123 return self._min_radius
126 def min_radius(self, value):
127 self._min_radius = value or 0.0
129 def reaches(self, position):
130 distance = self.position.get_distance(self.position)
131 return (self._min_radius <= distance <= self._max_radius)
133 def _set_radius_limits(self, radius_limits):
134 if radius_limits is None or not radius_limits[0]:
137 self._min_radius = radius_limits[0]
138 if radius_limits is None or not radius_limits[1]:
139 self._max_radius = 50.0
141 self._max_radius = radius_limits[1]
144 return self._start is not None
146 def rotate_degrees(self, degrees):
147 self._start.rotate_degrees(degrees)
148 self._end.rotate_degrees(degrees)
149 self._poly_cache = None
151 def _set_angle_limits(self, direction, spread):
152 if direction is None or spread is None:
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
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)
167 poly.body = self._body
168 poly.filter = self._ray_filter
169 poly_cache.append(poly)
170 return self._poly_cache
172 def pygame_position(self, surface):
173 return pymunk.pygame_util.to_pygame(self._position, surface)
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)
183 def pygame_polys(self, surface):
185 [pymunk.pygame_util.to_pygame(v, surface)
186 for v in poly.get_vertices()]
187 for poly in self.polys()
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
196 def _between(self, v, start, end):
198 return start <= v <= end
199 return (start <= v) or (v <= end)
201 def poly(self, start, end):
202 trial = pymunk.Poly(None, self.vertices)
203 trial.update(pymunk.Transform.identity())
205 if start is None or end is None:
206 return trial # no limits
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)
213 vertices = self.vertices[:]
216 if self._between((v - self.position).angle, start.angle, end.angle)
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)
224 poly = pymunk.Poly(None, vertices)
225 if len(poly.get_vertices()) < 3: