""" Light ray manipulation. Pew. Pew. Pew. Wommmm. """
+import math
+
+import pygame.rect
+
import pymunk
+import pymunk.autogeometry
import pymunk.pygame_util
from .constants import SCREEN_SIZE
@debug_timer("rays.calculate_ray_polys")
-def calculate_ray_polys(space, body, position, light_filter):
+def calculate_ray_polys(space, position, light_filter):
+ """ Calculate a set of convex RayPolys that cover all the areas that light
+ can reach from the given position, taking into account the obstacles
+ present in the space.
+ """
position = pymunk.Vec2d(position)
vertices = [position]
+ start, end = None, None
ray_polys = []
for ray in screen_rays(position):
info = space.segment_query_first(position, ray, 1, light_filter)
point = ray if info is None else info.point
vertices.append(point)
- if len(vertices) > 3:
+ if len(vertices) == 2:
+ start = vertices[1]
+ elif len(vertices) > 3:
trial_poly = pymunk.Poly(None, vertices)
trial_poly.update(pymunk.Transform.identity())
- query_prev = trial_poly.point_query(vertices[-2])
+ query_prev = trial_poly.point_query(end)
query_pos = trial_poly.point_query(position)
if query_prev.distance < -0.01 or query_pos.distance < -0.01:
- new_poly = pymunk.Poly(body, vertices[:-1])
- vertices = [position, vertices[-1]]
- ray_polys.append(new_poly)
+ ray_polys.append(RayPoly(position, vertices[:-1]))
+ start = vertices[-1]
+ vertices = [position, start]
else:
- vertices = trial_poly.get_vertices() + [point]
+ vertices = trial_poly.get_vertices()
+ end = point
if len(vertices) > 2:
- ray_polys.append(pymunk.Poly(body, vertices))
+ ray_polys.append(RayPoly(position, vertices))
return ray_polys
+
+
+def to_pymunk_radians(deg):
+ """ Convert degrees in [0, 360] to radians in (-pi, pi].
+
+ Return None if degrees is None.
+ """
+ if deg is None:
+ return None
+ deg = deg * math.pi / 180.0
+ if deg > math.pi:
+ deg -= 2 * math.pi
+ return deg
+
+
+class RayPolyManager(object):
+ def __init__(
+ self, body, position, ray_filter, radius_limits, direction,
+ spread):
+ self._body = body # light's body
+ self._position = pymunk.Vec2d(position) # light's position
+ self._ray_filter = ray_filter # light filter
+ self._rays = [] # list of RayPolys
+ self._direction = None # normal vector for direction
+ self._start = None # normal vector in direction of start angle limit
+ self._end = None # normal vector in direction of end angle limit
+ self._set_angle_limits(direction, spread)
+ if direction:
+ self.direction = direction # Update direction
+ self._max_radius = None # maximum radius in pixels
+ self._min_radius = None # minimum radius in pixels
+ self._set_radius_limits(radius_limits)
+ self._old_poly_cache = None # last polys added to the space
+ self._poly_cache = None # list of pymunk.Polys for rays
+ self._space = None # space the rays form part of
+
+ def set_space(self, space):
+ self._space = space
+ self._rays = calculate_ray_polys(
+ self._space, self._position, self._ray_filter)
+ self._poly_cache = None
+
+ def update_shapes(self):
+ if self._old_poly_cache:
+ self._space.remove(*self._old_poly_cache)
+ new_polys = self._old_poly_cache = self.polys()
+ self._space.add(*new_polys)
+
+ @property
+ def position(self):
+ return self._position
+
+ @property
+ def max_radius(self):
+ return self._max_radius
+
+ @max_radius.setter
+ def max_radius(self, value):
+ self._max_radius = value or 0.0
+
+ @property
+ def min_radius(self):
+ return self._min_radius
+
+ @min_radius.setter
+ def min_radius(self, value):
+ self._min_radius = value or 0.0
+
+ def reaches(self, position):
+ distance = self.position.get_distance(position)
+ return (self._min_radius <= distance <= self._max_radius)
+
+ def _set_radius_limits(self, radius_limits):
+ if radius_limits is None or not radius_limits[0]:
+ self._min_radius = 0
+ else:
+ self._min_radius = radius_limits[0]
+ if radius_limits is None or not radius_limits[1]:
+ self._max_radius = 50.0
+ else:
+ self._max_radius = radius_limits[1]
+
+ def rotatable(self):
+ return self._direction is not None
+
+ @property
+ def direction(self):
+ if self._direction is None:
+ return 0
+ return self._direction.angle_degrees
+
+ @direction.setter
+ def direction(self, degrees):
+ spread = self._direction.get_angle_between(self._start)
+ self._direction.angle_degrees = degrees
+ self._start = self._direction.rotated(spread)
+ self._end = self._direction.rotated(-spread)
+ self._poly_cache = None
+
+ @property
+ def spread(self):
+ if not self._direction:
+ return 2 * math.pi
+ return math.fabs(self._start.get_angle_between(self._end))
+
+ def _set_angle_limits(self, direction, spread):
+ if direction is None or spread is None:
+ self._direction = None
+ self._start = None
+ self._end = None
+ else:
+ self._direction = pymunk.Vec2d(1, 0)
+ self._start = self._direction.rotated_degrees(-spread/2.)
+ self._end = self._direction.rotated_degrees(spread/2.)
+ self._poly_cache = None
+
+ def polys(self):
+ if self._poly_cache is None:
+ self._poly_cache = poly_cache = []
+ for rp in self._rays:
+ poly = rp.poly(self._start, self._end)
+ if poly:
+ poly.body = self._body
+ poly.filter = self._ray_filter
+ poly_cache.append(poly)
+ return self._poly_cache
+
+ def pygame_position(self, surface):
+ return pymunk.pygame_util.to_pygame(self._position, surface)
+
+ def pygame_rect(self, surface):
+ half_width = self.max_radius
+ rect_width = half_width * 2
+ rect_x, rect_y = pymunk.pygame_util.to_pygame(self._position, surface)
+ dest_rect = pygame.rect.Rect(rect_x, rect_y, rect_width, rect_width)
+ dest_rect.move_ip(-half_width, -half_width)
+ return dest_rect
+
+ def pygame_polys(self, surface):
+ return [
+ [pymunk.pygame_util.to_pygame(v, surface)
+ for v in poly.get_vertices()]
+ for poly in self.polys()
+ ]
+
+
+class RayPoly(object):
+ def __init__(self, position, vertices):
+ self.position = position # pointy end of the conical polygon
+ self.vertices = vertices # all vertices in the polygon
+
+ def _between(self, v, start, end):
+ if start < end:
+ return start <= v <= end
+ return (start <= v) or (v <= end)
+
+ def poly(self, start, end):
+ trial = pymunk.Poly(None, self.vertices)
+ trial.update(pymunk.Transform.identity())
+
+ if start is None or end is None:
+ return trial # no limits
+
+ start_info = trial.segment_query(
+ self.position + 1250 * start, self.position + 0.1 * start, 0)
+ end_info = trial.segment_query(
+ self.position + 1250 * end, self.position + 0.1 * end, 0)
+
+ vertices = self.vertices[:]
+ vertices = [
+ v for v in vertices
+ if self._between((v - self.position).angle, start.angle, end.angle)
+ ]
+ if start_info.shape is not None:
+ vertices.append(start_info.point)
+ if end_info.shape is not None:
+ vertices.append(end_info.point)
+ vertices.append(self.position)
+
+ poly = pymunk.Poly(None, vertices)
+ if len(poly.get_vertices()) < 3:
+ return None
+ return poly