Source code for ladybug_geometry_polyskel.offset

# coding=utf-8
"""Functions for offsetting a polygon using the straight skeleton."""
from __future__ import division
import math

from ladybug_geometry.geometry2d import Point2D, Ray2D, Polygon2D

from .polygraph import skeleton_as_directed_graph
from .polysplit import perimeter_core_subpolygons, _split_perimeter_subpolygons, \
    _exterior_cycles_as_polygons


[docs]def offset_polygon(polygon, distance, tolerance=1e-5): """Offset a Polygon2D by a given distance inwards or outwards. Assuming that the input is not self-intersecting, the resulting shapes should have all self-intersections resolved through the straight skeleton. However, if the distance is large enough, the result may be an empty list. Args: polygon: A Polygon2D to be offset inwards or outwards by a given distance. distance: The distance inwards that the polygon will be offset. Positive values will always be offset inwards while negative ones will be offset outwards. tolerance: Tolerance for point equivalence. (Default: 1e-5). """ # determine the methods to use based on the offset direction if -tolerance <= distance <= tolerance: return [polygon] elif distance < 0: # we are offsetting outwards offset_poly = polygon.offset(distance, check_intersection=False) if not offset_poly.is_self_intersecting: return [offset_poly] bound_rect = _bound_rect_for_ext_skeleton(polygon, distance) dg = skeleton_as_directed_graph(bound_rect, [polygon], tolerance) root_key = dg.hole_root_keys[0] _, _perimeter_sub_dg = _split_perimeter_subpolygons( dg, abs(distance), root_key, tolerance, None) offset_polygons = _exterior_cycles_as_polygons(_perimeter_sub_dg, tolerance) else: # we are offsetting inwards _, offset_polygons = perimeter_core_subpolygons( polygon, distance, tolerance=tolerance, flat_core=True) return offset_polygons
[docs]def offset_perimeter_polygons(polygon, distance, tolerance=1e-5): """Get a list of Polygon2Ds along the perimeter of an input polygon. These can be used to split the input polygons into core/perimeter shapes. Assuming that the input is not self-intersecting, the resulting shapes should have all self-intersections resolved through the straight skeleton. Args: polygon: A Polygon2D to be offset inwards or outwards by a given distance. distance: The distance inwards that the polygon will be offset. Positive values will always be offset inwards while negative ones will be offset outwards. tolerance: Tolerance for point equivalence. (Default: 1e-5). """ # determine the methods to use based on the offset direction if -tolerance <= distance <= tolerance: return [] elif distance < 0: # we are offsetting outwards offset_poly = polygon.offset(distance, check_intersection=False) if not offset_poly.is_self_intersecting: perimeter_polygons = [] for i, (in_pt, out_pt) in enumerate(zip(polygon, offset_poly)): pts = (polygon[i - 1], offset_poly[i - 1], out_pt, in_pt) perimeter_polygons.append(Polygon2D(pts)) return perimeter_polygons bound_rect = _bound_rect_for_ext_skeleton(polygon, distance) dg = skeleton_as_directed_graph(bound_rect, [polygon], tolerance) root_key = dg.hole_root_keys[0] perimeter_polygons, _ = _split_perimeter_subpolygons( dg, abs(distance), root_key, tolerance, None) else: # we are offsetting inwards dg = skeleton_as_directed_graph(polygon, None, tolerance) root_key = dg.outer_root_key perimeter_polygons, _ = _split_perimeter_subpolygons( dg, distance, root_key, tolerance, None) return perimeter_polygons
[docs]def offset_skeleton(polygon, distance, tolerance=1e-5): """Get a list of LineSegment2D for the straight used to offset a polygon. Args: polygon: A Polygon2D to be offset inwards or outwards by a given distance. distance: The distance inwards that the polygon will be offset. Positive values will always be offset inwards while negative ones will be offset outwards. tolerance: Tolerance for point equivalence. (Default: 1e-5). """ if -tolerance <= distance <= tolerance: return [] elif distance < 0: # we are offsetting outwards bound_rect = _bound_rect_for_ext_skeleton(polygon, distance) dg = skeleton_as_directed_graph(bound_rect, [polygon], tolerance) else: # we are offsetting inwards dg = skeleton_as_directed_graph(polygon, None, tolerance) return dg.connection_segments
def _bound_rect_for_ext_skeleton(polygon, distance): """Get a bounding rectangle to be used for offsetting a polygon outwards. Args: polygon: A Polygon2D that is being offset outwards for which a bounding rectangle is needed. distance: The distance outwards that the polygon will be offset. This value should always be negative. Returns: A Polygon2D for the bounding rectangle around the shape. """ # determine how far from the polygon the bounding rectangle will be drawn min_pt, max_pt = polygon.min, polygon.max poly_dim = min((max_pt.x - min_pt.x, max_pt.y - min_pt.y)) offset_factor = 50 if abs(distance) < poly_dim / 10 else 10 offset = abs(distance) * offset_factor # create a starting bounding rectangle from the min, max, and distance min_pt = Point2D(min_pt.x - offset, min_pt.y - offset) max_pt = Point2D(max_pt.x + offset, max_pt.y + offset) min_x_max_y, max_x_min_y = Point2D(min_pt.x, max_pt.y), Point2D(max_pt.x, min_pt.y) rect_pts = (min_pt, max_x_min_y, max_pt, min_x_max_y) rect_poly = Polygon2D(rect_pts) # find a corner point to be used as the base for the rectangle pts_dists, pts_info = [], [] for corner_i, conrner_pt in enumerate(rect_poly): for pt_i, pt in enumerate(polygon.vertices): pts_dists.append(conrner_pt.distance_to_point(pt)) pts_info.append((pt_i, corner_i)) sort_info = [inf for _, inf in sorted(zip(pts_dists, pts_info), key=lambda pair: pair[0])] poly_pt_i, corner_pt_i = sort_info[0] # shoot a ray from the polygon point to the rectangle poly_pt = polygon.vertices[poly_pt_i] v1 = polygon.vertices[poly_pt_i - 1] - poly_pt end_i = poly_pt_i + 1 if poly_pt_i != len(polygon) - 1 else 0 v2 = polygon.vertices[end_i] - poly_pt if not polygon.is_clockwise: ang = v1.angle_clockwise(v2) / 2 if ang == 0: ang = math.pi / 2 m_vec = v1.rotate(-ang).normalize() m_dist = -distance / math.sin(ang) else: ang = v1.angle_counterclockwise(v2) / 2 if ang == 0: ang = math.pi / 2 m_vec = v1.rotate(ang).normalize() m_dist = distance / math.sin(ang) m_vec = m_vec * m_dist corner_ray = Ray2D(poly_pt, m_vec) ray_int = rect_poly.intersect_line_ray(corner_ray) # if the ray intersection is too far from the corner, just use the start rectangle corner_pt = rect_pts[corner_pt_i] safe_distance = abs(distance) * (offset_factor - 3) if len(ray_int) == 0 or ray_int[0].distance_to_point(corner_pt) > safe_distance: return rect_poly # otherwise, adjust the bounding rectangle to give us a nicer skeleton bpt = ray_int[0] base, height = max_pt.x - min_pt.x, max_pt.y - min_pt.y if corner_pt_i == 0: # lower left corner rect_pts = ( bpt, Point2D(bpt.x + base, bpt.y), Point2D(bpt.x + base, bpt.y + height), Point2D(bpt.x, bpt.y + height)) elif corner_pt_i == 1: # lower right corner rect_pts = ( Point2D(bpt.x - base, bpt.y), bpt, Point2D(bpt.x, bpt.y + height), Point2D(bpt.x - base, bpt.y + height)) elif corner_pt_i == 2: # upper right corner rect_pts = ( Point2D(bpt.x - base, bpt.y - height), Point2D(bpt.x, bpt.y - height), bpt, Point2D(bpt.x - base, bpt.y)) else: # upper left corner rect_pts = ( Point2D(bpt.x, bpt.y - height), Point2D(bpt.x + base, bpt.y - height), Point2D(bpt.x + base, bpt.y), bpt) return Polygon2D(rect_pts)