Merge branch 'master' of ctpug.org.za:tabakrolletjie
[tabakrolletjie.git] / tabakrolletjie / rays.py
index 1892edf4febe9e97dd9e26806afed426780c4e9c..f4c5ed16fcc69f1500aab6f6be3e0eeadb77e2a6 100644 (file)
@@ -1,6 +1,9 @@
 """ Light ray manipulation. Pew. Pew. Pew. Wommmm. """
 
+import math
+
 import pymunk
+import pymunk.autogeometry
 import pymunk.pygame_util
 
 from .constants import SCREEN_SIZE
@@ -26,25 +29,128 @@ def screen_rays(pos):
 
 
 @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, ray_filter):
+        self._body = body  # light's body
+        self._ray_filter = ray_filter  # light filter
+        self._rays = []  # list of RayPolys
+        self._start = None  # normal vector in direction of start angle limit
+        self._end = None  # normal vector in direction of end angle limit
+        self._poly_cache = None  # list of pymunk.Polys for rays
+
+    def generate_rays(self, space, position):
+        self._rays = calculate_ray_polys(space, position, self._ray_filter)
+        self._poly_cache = None
+
+    def set_angle_limits(self, angle_limits):
+        if angle_limits is 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._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_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 + 0.1 * start, self.position + 1250 * start, 0)
+        end_info = trial.segment_query(
+            self.position + 0.1 * end, self.position + 1250 * 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