Use bounding foxes.
[tabakrolletjie.git] / tabakrolletjie / rays.py
index 32ae1a749bbb86c539e8c46e95709eb0292bd0d0..7d880d0aaa5ea417e38cb08fcfe167c5732c42f2 100644 (file)
@@ -2,34 +2,36 @@
 
 import math
 
+import pygame.rect
+
 import pymunk
 import pymunk.autogeometry
 import pymunk.pygame_util
 
-from .constants import SCREEN_SIZE
 from .utils import debug_timer
 
 
-def screen_rays(pos):
+def screen_rays(pos, bounding_radius):
     """ An iterable that returns ordered rays from pos to the edge of the
         screen, starting with the edge point (0, 0) and continuing clockwise
         in pymunk coordinates.
     """
-    w, h = SCREEN_SIZE
-    left, right, bottom, top = 0, w, 0, h
+    r = int(bounding_radius)
+    left, right = int(pos.x) - r, int(pos.x) + r
+    bottom, top = int(pos.y) + r, int(pos.y) - r
     step = 1
-    for y in range(0, h, step):
+    for y in range(top, bottom + 1, step):
         yield pymunk.Vec2d(left, y)
-    for x in range(0, w, step):
+    for x in range(left, right + 1, step):
         yield pymunk.Vec2d(x, top)
-    for y in range(top, -1, -step):
+    for y in range(bottom, top - 1, -step):
         yield pymunk.Vec2d(right, y)
-    for x in range(right, -1, -step):
+    for x in range(right, left - 1, -step):
         yield pymunk.Vec2d(x, bottom)
 
 
 @debug_timer("rays.calculate_ray_polys")
-def calculate_ray_polys(space, position, light_filter):
+def calculate_ray_polys(space, position, bounding_radius, 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.
@@ -38,7 +40,7 @@ def calculate_ray_polys(space, position, light_filter):
     vertices = [position]
     start, end = None, None
     ray_polys = []
-    for ray in screen_rays(position):
+    for ray in screen_rays(position, bounding_radius):
         info = space.segment_query_first(position, ray, 1, light_filter)
         point = ray if info is None else info.point
         vertices.append(point)
@@ -75,27 +77,126 @@ def to_pymunk_radians(deg):
 
 
 class RayPolyManager(object):
-    def __init__(self, body, ray_filter):
+    def __init__(
+            self, body, position, ray_filter, radius_limits, direction,
+            spread, bounding_radius):
         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)
+        self._bounding_radius = None   # absolute maximum radius
+        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._set_bounding_radius(bounding_radius)
+        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 generate_rays(self, space, position):
-        self._rays = calculate_ray_polys(space, position, self._ray_filter)
+    def set_space(self, space):
+        self._space = space
+        self._rays = calculate_ray_polys(
+            self._space, self._position, self._bounding_radius,
+            self._ray_filter)
         self._poly_cache = None
 
-    def set_angle_limits(self, angle_limits):
-        if angle_limits is 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 serialize(self):
+        """ Return the required information from the ray_manager """
+        if self._direction is None:
+            direction = None
+            spread = None
+        else:
+            direction = self._direction.angle_degrees
+            spread = math.degrees(self.spread)
+        return {
+            "radius_limits": (self._min_radius, self._max_radius),
+            "direction": direction,
+            "spread": spread,
+        }
+
+    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 _set_bounding_radius(self, bounding_radius):
+        if bounding_radius is None:
+            bounding_radius = self._max_radius
+        self._bounding_radius = bounding_radius
+
+    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._start = pymunk.Vec2d(1, 0).rotated(
-                to_pymunk_radians(angle_limits[0]))
-            self._end = pymunk.Vec2d(1, 0).rotated(
-                to_pymunk_radians(angle_limits[1]))
+            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):
@@ -109,6 +210,17 @@ class RayPolyManager(object):
                     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)