1 """ Light ray manipulation. Pew. Pew. Pew. Wommmm. """
8 import pymunk.autogeometry
9 import pymunk.pygame_util
11 from .utils import debug_timer
14 def screen_rays(pos, bounding_radius):
15 """ An iterable that returns ordered rays from pos to the edge of the
16 screen, starting with the edge point (0, 0) and continuing clockwise
17 in pymunk coordinates.
19 r = int(bounding_radius)
20 left, right = int(pos.x) - r, int(pos.x) + r
21 bottom, top = int(pos.y) - r, int(pos.y) + r
23 for y in range(bottom, top + 1, step):
24 yield pymunk.Vec2d(left, y)
25 for x in range(left, right + 1, step):
26 yield pymunk.Vec2d(x, top)
27 for y in range(top, bottom - 1, -step):
28 yield pymunk.Vec2d(right, y)
29 for x in range(right, left - 1, -step):
30 yield pymunk.Vec2d(x, bottom)
33 @debug_timer("rays.calculate_ray_polys")
34 def calculate_ray_polys(space, position, bounding_radius, 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, bounding_radius):
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,
82 spread, bounding_radius):
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._bounding_radius = None # absolute maximum radius
93 self.direction = direction # Update direction
94 self._max_radius = None # maximum radius in pixels
95 self._min_radius = None # minimum radius in pixels
96 self._set_radius_limits(radius_limits)
97 self._set_bounding_radius(bounding_radius)
98 self._old_poly_cache = None # last polys added to the space
99 self._poly_cache = None # list of pymunk.Polys for rays
100 self._space = None # space the rays form part of
102 def set_space(self, space):
104 self._rays = calculate_ray_polys(
105 self._space, self._position, self._bounding_radius,
107 self._poly_cache = None
109 def update_shapes(self):
110 if self._old_poly_cache:
111 self._space.remove(*self._old_poly_cache)
112 new_polys = self._old_poly_cache = self.polys()
113 self._space.add(*new_polys)
117 return self._position
120 def max_radius(self):
121 return self._max_radius
124 def max_radius(self, value):
125 self._max_radius = value or 0.0
128 def min_radius(self):
129 return self._min_radius
132 def min_radius(self, value):
133 self._min_radius = value or 0.0
136 """ Return the required information from the ray_manager """
137 if self._direction is None:
141 direction = self._direction.angle_degrees
142 spread = math.degrees(self.spread)
144 "radius_limits": (self._min_radius, self._max_radius),
145 "direction": direction,
149 def reaches(self, position):
150 distance = self.position.get_distance(position)
151 return (self._min_radius <= distance <= self._max_radius)
153 def _set_radius_limits(self, radius_limits):
154 if radius_limits is None or not radius_limits[0]:
157 self._min_radius = radius_limits[0]
158 if radius_limits is None or not radius_limits[1]:
159 self._max_radius = 50.0
161 self._max_radius = radius_limits[1]
163 def _set_bounding_radius(self, bounding_radius):
164 if bounding_radius is None:
165 bounding_radius = self._max_radius
166 self._bounding_radius = bounding_radius
169 return self._direction is not None
173 if self._direction is None:
175 return self._direction.angle_degrees
178 def direction(self, degrees):
179 spread = self._direction.get_angle_between(self._start)
180 self._direction.angle_degrees = degrees
181 self._start = self._direction.rotated(spread)
182 self._end = self._direction.rotated(-spread)
183 self._poly_cache = None
187 if not self._direction:
189 return math.fabs(self._start.get_angle_between(self._end))
191 def _set_angle_limits(self, direction, spread):
192 if direction is None or spread is None:
193 self._direction = None
197 self._direction = pymunk.Vec2d(1, 0)
198 self._start = self._direction.rotated_degrees(-spread/2.)
199 self._end = self._direction.rotated_degrees(spread/2.)
200 self._poly_cache = None
203 if self._poly_cache is None:
204 self._poly_cache = poly_cache = []
205 for rp in self._rays:
206 poly = rp.poly(self._start, self._end)
208 poly.body = self._body
209 poly.filter = self._ray_filter
210 poly_cache.append(poly)
211 return self._poly_cache
213 def pygame_position(self, surface):
214 return pymunk.pygame_util.to_pygame(self._position, surface)
216 def pygame_rect(self, surface):
217 half_width = self.max_radius
218 rect_width = half_width * 2
219 rect_x, rect_y = pymunk.pygame_util.to_pygame(self._position, surface)
220 dest_rect = pygame.rect.Rect(rect_x, rect_y, rect_width, rect_width)
221 dest_rect.move_ip(-half_width, -half_width)
224 def pygame_polys(self, surface):
226 [pymunk.pygame_util.to_pygame(v, surface)
227 for v in poly.get_vertices()]
228 for poly in self.polys()
232 class RayPoly(object):
233 def __init__(self, position, vertices):
234 self.position = position # pointy end of the conical polygon
235 self.vertices = vertices # all vertices in the polygon
237 def _between(self, v, start, end):
239 return start <= v <= end
240 return (start <= v) or (v <= end)
242 def poly(self, start, end):
243 trial = pymunk.Poly(None, self.vertices)
244 trial.update(pymunk.Transform.identity())
246 if start is None or end is None:
247 return trial # no limits
249 start_info = trial.segment_query(
250 self.position + 1250 * start, self.position + 0.1 * start, 0)
251 end_info = trial.segment_query(
252 self.position + 1250 * end, self.position + 0.1 * end, 0)
254 vertices = self.vertices[:]
257 if self._between((v - self.position).angle, start.angle, end.angle)
259 if start_info.shape is not None:
260 vertices.append(start_info.point)
261 if end_info.shape is not None:
262 vertices.append(end_info.point)
263 vertices.append(self.position)
265 poly = pymunk.Poly(None, vertices)
266 if len(poly.get_vertices()) < 3: