Ejemplo n.º 1
0
 def from_points(cls, points):
     """Create a line segment from one or more collinear points.  The first
     point is assumed to be the anchor.  The order of the remaining points
     is unimportant, however they must all be collinear.  The furthest
     point from the anchor determines the line segment's vector.
     
     :param points: Iterable of at least 2 distinct points.
     """
     points = iter(points)
     try:
         start = end = planar.Vec2(*next(points))
     except StopIteration:
         raise ValueError("Expected iterable of 1 or more points")
     furthest = 0.0
     pt_vectors = []
     for p in points:
         p = planar.Vec2(*p)
         dist = (p - start).length2
         if dist > furthest:
             furthest = dist
             end = p
         pt_vectors.append(p)
     segment = _LinearGeometry.__new__(cls)
     if end != start:
         segment.vector = end - start
     else:
         # degenerate case
         segment.direction = (1, 0)
         segment.length = 0.0
     segment._anchor = start
     for p in pt_vectors:
         if not segment.contains_point(p):
             raise ValueError("All points provided must be collinear")
     return segment
Ejemplo n.º 2
0
    def from_shapes(cls, shapes):
        """Creating a bounding box that completely encloses all of the
        shapes provided.
        """
        shapes = iter(shapes)
        try:
            shape = next(shapes)
        except StopIteration:
            raise ValueError(
                ("BoundingBox.from_shapes(): requires at least one shape"))
        min_x, min_y = shape.bounding_box.min_point
        max_x, max_y = shape.bounding_box.max_point

        for shape in shapes:
            x, y = shape.bounding_box.min_point
            if x < min_x:
                min_x = x
            if y < min_y:
                min_y = y
            x, y = shape.bounding_box.max_point
            if x > max_x:
                max_x = x
            if y > max_y:
                max_y = y
        box = object.__new__(cls)
        box._min = planar.Vec2(min_x, min_y)
        box._max = planar.Vec2(max_x, max_y)
        return box
Ejemplo n.º 3
0
 def _pt_tangents(self, point):
     """Return the pair of tangent points for the given exterior point.
     This general algorithm works for all polygons in O(n) time.
     """
     px, py = point
     left_tan = right_tan = self[0]
     verts = iter(self)
     v0_x, v0_y = self[-2]
     v1_x, v1_y = self[-1]
     prev_turn = (v1_x - v0_x)*(py - v0_y) - (px - v0_x)*(v1_y - v0_y)
     v0_x = v1_x
     v0_y = v1_y
     for v1_x, v1_y in self:
         next_turn = (v1_x - v0_x)*(py - v0_y) - (px - v0_x)*(v1_y - v0_y)
         if prev_turn <= 0.0 and next_turn > 0.0:
             if ((v0_x - px)*(right_tan.y - py)
                 - (right_tan.x - px)*(v0_y - py) >= 0.0):
                 right_tan = planar.Vec2(v0_x, v0_y)
         elif prev_turn > 0.0 and next_turn <= 0.0:
             if ((v0_x - px)*(left_tan.y - py)
                 - (left_tan.x - px)*(v0_y - py) <= 0.0):
                 left_tan = planar.Vec2(v0_x, v0_y)
         v0_x = v1_x
         v0_y = v1_y
         prev_turn = next_turn
     return left_tan, right_tan
Ejemplo n.º 4
0
    def regular(cls, vertex_count, radius, center=(0, 0), angle=0):
        """Create a regular polygon with the specified number of vertices
        radius distance from the center point. Regular polygons are
        always convex.

        :param vertex_count: The number of vertices in the polygon.
            Must be >= 3.
        :type vertex_count: int
        :param radius: distance from vertices to center point.
        :type radius: float
        :param center: The center point of the polygon. If omitted,
            the polygon will be centered on the origin.
        :type center: Vec2
        :param angle: The starting angle for the vertices, in degrees.
        :type angle: float
        """
        cx, cy = center
        angle_step = 360.0 / vertex_count
        verts = []
        for i in range(vertex_count):
            x, y = cos_sin_deg(angle)
            verts.append((x * radius + cx, y * radius + cy))
            angle += angle_step
        poly = cls(vertices=verts, is_convex=True)
        poly._centroid = planar.Vec2(*center)
        poly._max_r = radius
        poly._max_r2 = radius * radius
        poly._min_r = min_r = ((poly[0] + poly[1]) * 0.5 - center).length
        poly._min_r2 = min_r * min_r
        poly._dupe_verts = False
        return poly
Ejemplo n.º 5
0
 def point_right(self, point):
     """Return True if the specified point is in the space
     to the right of, but not behind the ray.
     """
     to_point = planar.Vec2(*point) - self._anchor
     return (self._direction.dot(to_point) > -planar.EPSILON
             and self._normal.dot(to_point) >= planar.EPSILON)
Ejemplo n.º 6
0
    def centroid(self):
        """The geometric center point of the polygon. This point only exists 
        for simple polygons. For non-simple polygons it is ``None``. Note
        in concave polygons, this point may lie outside of the polygon itself.

        If the centroid is unknown, it is calculated from the vertices and
        cached. If the polygon is known to be simple, this takes O(n) time. If
        not, then the simple polygon check is also performed, which has an
        expected complexity of O(n log n).
        """
        if self._centroid is _unknown:
            if self.is_simple:
                # Compute the centroid using by summing the centroids
                # of triangles made from each edge with vertex[0] weighted
                # (positively or negatively) by each triangle's area
                a = self[0]
                b = self[1]
                total_area = 0.0
                centroid = planar.Vec2(0, 0)
                for i in range(2, len(self)):
                    c = self[i]
                    area = ((b[0] - a[0]) * (c[1] - a[1]) 
                        - (c[0] - a[0]) * (b[1] - a[1]))
                    centroid += (a + b + c) * area
                    total_area += area
                    b = c
                self._centroid = centroid / (3.0 * total_area)
            else:
                self._centroid = None
        return self._centroid
Ejemplo n.º 7
0
    def __mul__(self, other):
        """Apply the transform using matrix multiplication, creating a
        resulting object of the same type.  A transform may be applied to
        another transform, a vector, vector array, or shape.

        :param other: The object to transform.
        :type other: Affine, :class:`~planar.Vec2`, 
            :class:`~planar.Vec2Array`, :class:`~planar.Shape`
        :rtype: Same as ``other``
        """
        sa, sb, sc, sd, se, sf, _, _, _ = self
        if isinstance(other, Affine):
            oa, ob, oc, od, oe, of, _, _, _ = other
            return tuple.__new__(Affine, 
                (sa*oa + sb*od, sa*ob + sb*oe, sa*oc + sb*of + sc,
                 sd*oa + se*od, sd*ob + se*oe, sd*oc + se*of + sf,
                 0.0, 0.0, 1.0))
        elif hasattr(other, 'from_points'):
            # Point/vector array
            Point = planar.Point
            points = getattr(other, 'points', other)
            try:
                return other.from_points(
                    Point(px*sa + py*sd + sc, px*sb + py*se + sf)
                    for px, py in points)
            except TypeError:
                return NotImplemented
        else:
            try:
                vx, vy = other
            except Exception:
                return NotImplemented
            return planar.Vec2(vx*sa + vy*sd + sc, vx*sb + vy*se + sf)
Ejemplo n.º 8
0
 def _init_min_max(self, points):
     points = iter(points)
     try:
         min_x, min_y = max_x, max_y = next(points)
     except StopIteration:
         raise ValueError("BoundingBox() requires at least one point")
     for x, y in points:
         if x < min_x:
             min_x = x * 1.0
         elif x > max_x:
             max_x = x * 1.0
         if y < min_y:
             min_y = y * 1.0
         elif y > max_y:
             max_y = y * 1.0
     self._min = planar.Vec2(min_x, min_y)
     self._max = planar.Vec2(max_x, max_y)
Ejemplo n.º 9
0
 def point_behind(self, point):
     """Return True if the specified point is behind the anchor point with
     respect to the direction of the ray.  In other words, the angle
     between the ray direction and the vector pointing from the ray's
     anchor to the given point is greater than 90 degrees.
     """
     to_point = planar.Vec2(*point) - self._anchor
     return self.direction.dot(to_point) <= -planar.EPSILON
Ejemplo n.º 10
0
 def point_right(self, point):
     """Return True if the specified point is in the space
     to the right of, but not behind the line segment.
     """
     to_point = planar.Vec2(*point) - self._anchor
     along = self._direction.dot(to_point)
     return (self.length + planar.EPSILON > along > -planar.EPSILON
             and self._normal.dot(to_point) >= planar.EPSILON)
Ejemplo n.º 11
0
 def vector(self, value):
     vector = planar.Vec2(*value)
     length = vector.length
     if length:
         self.direction = vector
     else:
         self.direction = (1, 0)
     self.length = vector.length
Ejemplo n.º 12
0
 def distance_to(self, point):
     """Return the distance between the given point and the ray."""
     to_point = planar.Vec2(*point) - self._anchor
     if self.direction.dot(to_point) >= 0.0:
         # Point "beside" ray
         return abs(to_point.dot(self._normal))
     else:
         # Point "behind" ray
         return to_point.length
Ejemplo n.º 13
0
    def reflect(self, point):
        """Reflect a point across the line.

        :param point: The point to reflect.
        :type point: Vec2
        """
        point = planar.Vec2(*point)
        offset_distance = point.dot(self._normal) - self.offset
        return point - 2.0 * self._normal * offset_distance
Ejemplo n.º 14
0
    def distance_to(self, point):
        """Return the signed distance from the line to the specified point.
        The sign indicates which half-plane contains the point. If the
        distance is negative, the point is in the "left" half plane with
        respect to the line, if it is positive, the point is in the "right"
        half plane.

        :param point: The point to measure the distance to.
        :type point: Vec2
        """
        point = planar.Vec2(*point)
        return point.dot(self._normal) - self.offset
Ejemplo n.º 15
0
 def from_points(cls, points):
     """Create a line from two or more collinear points.  The direction of
     the line is derived from the first two distinct points, the order of
     the remaining points is unimportant.
     
     :param points: Iterable of at least 2 distinct points.
     """
     points = iter(points)
     try:
         start = end = planar.Vec2(*next(points))
         while end == start:
             end = planar.Vec2(*next(points))
     except StopIteration:
         raise ValueError("Expected iterable of 2 or more distinct points")
     line = _LinearGeometry.__new__(cls)
     line.direction = end - start
     line.offset = start.dot(line.normal)
     for p in points:
         if not line.contains_point(p):
             raise ValueError("All points provided must be collinear")
     return line
Ejemplo n.º 16
0
 def from_points(cls, points):
     """Create a ray from two or more collinear points.  The direction of
     the ray is derived from the first two distinct points, with the first
     point assumed to be the anchor. The order of the remaining points is
     unimportant, however they must all be on the ray.
     
     :param points: Iterable of at least 2 distinct points.
     """
     points = iter(points)
     try:
         start = end = planar.Vec2(*next(points))
         while end == start:
             end = planar.Vec2(*next(points))
     except StopIteration:
         raise ValueError("Expected iterable of 2 or more distinct points")
     ray = _LinearGeometry.__new__(cls)
     ray.direction = end - start
     ray.anchor = start
     for p in points:
         if not ray.contains_point(p):
             raise ValueError("All points provided must be collinear")
     return ray
Ejemplo n.º 17
0
 def distance_to(self, point):
     """Return the distance between the given point and the line segment."""
     point = planar.Vec2(*point)
     to_point = point - self._anchor
     along = self.direction.dot(to_point)
     if along < 0.0:
         # Point "behind"
         return to_point.length
     if along > self.length:
         # Point "ahead"
         return (point - self.end).length
     else:
         # Point "beside"
         return abs(to_point.dot(self._normal))
Ejemplo n.º 18
0
    def star(cls, peak_count, radius1, radius2, center=(0, 0), angle=0):
        """Create a radial pointed star polygon with the specified number
        of peaks.

        :param peak_count: The number of peaks. The resulting polygon will
            have twice this number of vertices. Must be >= 2.
        :type peak_count: int
        :param radius1: The peak or valley vertex radius. A vertex
            is aligned on ``angle`` with this radius.
        :type radius1: float
        :param radius2: The alternating vertex radius.
        :type radius2: float
        :param center: The center point of the polygon. If omitted,
            the polygon will be centered on the origin.
        :type center: Vec2
        :param angle: The starting angle for the vertices, in degrees.
        :type angle: float
        """
        if peak_count < 2:
            raise ValueError(
                "star polygon must have a minimum of 2 peaks")
        cx, cy = center
        angle_step = 180.0 / peak_count
        verts = []
        for i in range(peak_count):
            x, y = cos_sin_deg(angle)
            verts.append((x * radius1 + cx, y * radius1 + cy))
            angle += angle_step
            x, y = cos_sin_deg(angle)
            verts.append((x * radius2 + cx, y * radius2 + cy))
            angle += angle_step
        is_simple = (radius1 > 0.0) == (radius2 > 0.0)
        poly = cls(verts, is_convex=(radius1 == radius2), 
            is_simple=is_simple or None)
        if is_simple:
            poly._centroid = planar.Vec2(*center)
        poly._max_r = max_r = max(abs(radius1), abs(radius2))
        poly._max_r2 = max_r * max_r
        if (radius1 >= 0.0) == (radius2 >= 0.0):
            if not poly.is_convex:
                poly._min_r = min_r = min(abs(radius1), abs(radius2))
                poly._min_r2 = min_r * min_r
            else:
                poly._min_r = min_r = (
                    (poly[0] + poly[1]) * 0.5 - center).length
                poly._min_r2 = min_r * min_r
        if radius1 > 0.0 and radius2 > 0.0:
            poly._dupe_verts = False
        return poly
Ejemplo n.º 19
0
    def project(self, point):
        """Compute the projection of a point onto the ray. This
        is the closest point on the ray to the specified point.

        :param point: The point to project.
        :type point: Vec2
        """
        to_point = planar.Vec2(*point) - self._anchor
        parallel = self.direction.project(to_point)
        if parallel.dot(self.direction) > -planar.EPSILON:
            # Point "beside" ray
            return parallel + self._anchor
        else:
            # Point "behind" ray
            return self._anchor
Ejemplo n.º 20
0
    def inflate(self, amount):
        """Return a new box resized from this one. The new
        box has its size changed by the specified amount,
        but remains centered on the same point.

        :param amount: The quantity to add to the width and
            height of the box. A scalar value changes
            both the width and height equally. A vector
            will change the width and height independently.
            Negative values reduce the size accordingly.
        :type amount: float or :class:`~planar.Vec2`
        """
        try:
            dx, dy = amount
        except (TypeError, ValueError):
            dx = dy = amount * 1.0
        dv = planar.Vec2(dx, dy) / 2.0
        return self.from_points((self._min - dv, self._max + dv))
Ejemplo n.º 21
0
    def project(self, point):
        """Compute the projection of a point onto the line segment. This
        is the closest point on the segment to the specified point.

        :param point: The point to project.
        :type point: Vec2
        """
        to_point = planar.Vec2(*point) - self._anchor
        parallel = self.direction.project(to_point)
        along = parallel.dot(self.direction)
        if along <= -planar.EPSILON:
            # Point "behind"
            return self._anchor
        elif along >= self.length + planar.EPSILON:
            # Point "ahead"
            return self.end
        else:
            # Point "beside"
            return parallel + self._anchor
Ejemplo n.º 22
0
 def __init__(self, point, direction):
     self.direction = direction
     self.offset = planar.Vec2(*point).dot(self.normal)
Ejemplo n.º 23
0
 def normal(self, value):
     normal = planar.Vec2(*value).normalized()
     if normal.is_null:
         raise ValueError("Line normal vector must not be null")
     self._normal = normal
     self._direction = normal.perpendicular()
Ejemplo n.º 24
0
 def point_behind(self, point):
     """Return True if the specified point is behind the anchor point with
     respect to the direction of the line segment.
     """
     to_point = planar.Vec2(*point) - self._anchor
     return self.direction.dot(to_point) <= -planar.EPSILON
Ejemplo n.º 25
0
 def point_ahead(self, point):
     """Return True if the specified point is ahead of the endpoint
     of the line segment with respect to its direction.
     """
     to_point = planar.Vec2(*point) - self._anchor
     return self.direction.dot(to_point) >= self.length + planar.EPSILON
Ejemplo n.º 26
0
 def column_vectors(self):
     """The values of the transform as three 2D column vectors"""
     a, b, c, d, e, f, _, _, _ = self
     return planar.Vec2(a, d), planar.Vec2(b, e), planar.Vec2(c, f)
Ejemplo n.º 27
0
 def anchor(self, value):
     self._anchor = planar.Vec2(*value)
Ejemplo n.º 28
0
 def __init__(self, anchor, vector):
     self.vector = vector
     self._anchor = planar.Vec2(*anchor)
Ejemplo n.º 29
0
 def direction(self, value):
     direction = planar.Vec2(*value).normalized()
     if direction.is_null:
         raise ValueError("Line direction vector must not be null")
     self._direction = direction
     self._normal = -direction.perpendicular()
Ejemplo n.º 30
0
 def end(self, value):
     end = planar.Vec2(*value)
     self.vector = end - self._anchor