Use bounding foxes.
[tabakrolletjie.git] / tabakrolletjie / rays.py
index e187f3d3c532633540f41b330f68be6f15b38867..7d880d0aaa5ea417e38cb08fcfe167c5732c42f2 100644 (file)
@@ -2,34 +2,36 @@
 
 import math
 
 
 import math
 
+import pygame.rect
+
 import pymunk
 import pymunk.autogeometry
 import pymunk.pygame_util
 
 import pymunk
 import pymunk.autogeometry
 import pymunk.pygame_util
 
-from .constants import SCREEN_SIZE
 from .utils import debug_timer
 
 
 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.
     """
     """ 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
     step = 1
-    for y in range(0, h, step):
+    for y in range(top, bottom + 1, step):
         yield pymunk.Vec2d(left, y)
         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)
         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)
         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")
         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.
     """ 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 = []
     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)
         info = space.segment_query_first(position, ray, 1, light_filter)
         point = ray if info is None else info.point
         vertices.append(point)
@@ -50,15 +52,14 @@ def calculate_ray_polys(space, position, light_filter):
             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:
             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:
-                ray_polys.append(RayPoly(
-                    start - position, end - position, vertices[:-1]))
+                ray_polys.append(RayPoly(position, vertices[:-1]))
                 start = vertices[-1]
                 vertices = [position, start]
             else:
                 vertices = trial_poly.get_vertices()
         end = point
     if len(vertices) > 2:
                 start = vertices[-1]
                 vertices = [position, start]
             else:
                 vertices = trial_poly.get_vertices()
         end = point
     if len(vertices) > 2:
-        ray_polys.append(RayPoly(start, end, vertices))
+        ray_polys.append(RayPoly(position, vertices))
     return ray_polys
 
 
     return ray_polys
 
 
@@ -76,68 +77,192 @@ def to_pymunk_radians(deg):
 
 
 class RayPolyManager(object):
 
 
 class RayPolyManager(object):
-    def __init__(self, body, ray_filter):
-        self._body = body
-        self._ray_filter = ray_filter
-        self._rays = []
-        self._angle_limits = (None, None)
+    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 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
         self._poly_cache = None
-        self._pygame_poly_cache = None
 
 
-    def generate_rays(self, space, position):
-        self._rays = calculate_ray_polys(space, position, self._ray_filter)
+    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
         self._poly_cache = None
-        self._pygame_poly_cache = None
 
 
-    def set_angle_limits(self, angle_limits):
-        start, end = angle_limits
-        self._angle_limits = (
-            to_pymunk_radians(start), to_pymunk_radians(end))
+    @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
         self._poly_cache = None
-        self._pygame_poly_cache = None
 
     def polys(self):
         if self._poly_cache is None:
 
     def polys(self):
         if self._poly_cache is None:
-            self._poly_cache = [
-                rp.poly(self._body, self._ray_filter) for rp in self._rays
-                if rp.within_limits(*self._angle_limits)
-            ]
+            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
 
         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):
     def pygame_polys(self, surface):
-        if self._pygame_poly_cache is None:
-            self._pygame_poly_cache = [
-                [
-                    pymunk.pygame_util.to_pygame(v, surface)
-                    for v in poly.get_vertices()
-                ]
-                for poly in self.polys()
-            ]
-        return self._pygame_poly_cache
+        return [
+            [pymunk.pygame_util.to_pygame(v, surface)
+             for v in poly.get_vertices()]
+            for poly in self.polys()
+        ]
 
 
 class RayPoly(object):
 
 
 class RayPoly(object):
-    def __init__(self, start_vec, end_vec, vertices):
-        self.start = start_vec  # vector from position to first point
-        self.end = end_vec  # vector from position to last point
-        self.vertices = vertices
-
-    def poly(self, body, filter):
-        shape = pymunk.Poly(body, self.vertices)
-        shape.filter = filter
-        return shape
-
-    def within_limits(self, start_limit, end_limit):
-        if start_limit is None or end_limit is None:
-            return True
-
-        def between(n):
-            if start_limit < end_limit:
-                return start_limit <= n <= end_limit
-            return (start_limit <= n) or (n <= end_limit)
-
-        start_within = between(self.start.angle)
-        end_within = between(self.end.angle)
-        if start_within or end_within:
-            return True
-        return False
+    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