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
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
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
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
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)
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
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)
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)
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
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)
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
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
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
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
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
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
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))
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
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
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))
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
def __init__(self, point, direction): self.direction = direction self.offset = planar.Vec2(*point).dot(self.normal)
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()
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
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
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)
def anchor(self, value): self._anchor = planar.Vec2(*value)
def __init__(self, anchor, vector): self.vector = vector self._anchor = planar.Vec2(*anchor)
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()
def end(self, value): end = planar.Vec2(*value) self.vector = end - self._anchor