summaryrefslogtreecommitdiff
path: root/lib/stitches/StitchPattern.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/stitches/StitchPattern.py')
-rw-r--r--lib/stitches/StitchPattern.py266
1 files changed, 167 insertions, 99 deletions
diff --git a/lib/stitches/StitchPattern.py b/lib/stitches/StitchPattern.py
index d0a3f7aa..ba3e3031 100644
--- a/lib/stitches/StitchPattern.py
+++ b/lib/stitches/StitchPattern.py
@@ -1,6 +1,6 @@
from shapely.geometry.polygon import LinearRing, LineString
from shapely.geometry import Polygon, MultiLineString
-from shapely.ops import polygonize
+from shapely.ops import polygonize
from shapely.geometry import MultiPolygon
from anytree import AnyNode, PreOrderIter
from shapely.geometry.polygon import orient
@@ -10,68 +10,90 @@ from ..stitches import ConnectAndSamplePattern
from ..stitches import constants
-
-# Problem: When shapely offsets a LinearRing the start/end point might be handled wrongly since they are only treated as LineString.
-# (See e.g. https://i.stack.imgur.com/vVh56.png as a problematic example)
-# This method checks first whether the start/end point form a problematic edge with respect to the offset side. If it is not a problematic
-# edge we can use the normal offset_routine. Otherwise we need to perform two offsets:
-# -offset the ring
-# -offset the start/end point + its two neighbors left and right
-# Finally both offsets are merged together to get the correct offset of a LinearRing
def offset_linear_ring(ring, offset, side, resolution, join_style, mitre_limit):
+ """
+ Solves following problem: When shapely offsets a LinearRing the
+ start/end point might be handled wrongly since they
+ are only treated as LineString.
+ (See e.g. https://i.stack.imgur.com/vVh56.png as a problematic example)
+ This method checks first whether the start/end point form a problematic
+ edge with respect to the offset side. If it is not a problematic
+ edge we can use the normal offset_routine. Otherwise we need to
+ perform two offsets:
+ -offset the ring
+ -offset the start/end point + its two neighbors left and right
+ Finally both offsets are merged together to get the correct
+ offset of a LinearRing
+ """
+
coords = ring.coords[:]
- # check whether edge at index 0 is concave or convex. Only for concave edges we need to spend additional effort
+ # check whether edge at index 0 is concave or convex. Only for
+ # concave edges we need to spend additional effort
dx_seg1 = dy_seg1 = 0
if coords[0] != coords[-1]:
- dx_seg1 = coords[0][0]-coords[-1][0]
- dy_seg1 = coords[0][1]-coords[-1][1]
+ dx_seg1 = coords[0][0] - coords[-1][0]
+ dy_seg1 = coords[0][1] - coords[-1][1]
else:
- dx_seg1 = coords[0][0]-coords[-2][0]
- dy_seg1 = coords[0][1]-coords[-2][1]
- dx_seg2 = coords[1][0]-coords[0][0]
- dy_seg2 = coords[1][1]-coords[0][1]
+ dx_seg1 = coords[0][0] - coords[-2][0]
+ dy_seg1 = coords[0][1] - coords[-2][1]
+ dx_seg2 = coords[1][0] - coords[0][0]
+ dy_seg2 = coords[1][1] - coords[0][1]
# use cross product:
- crossvalue = dx_seg1*dy_seg2-dy_seg1*dx_seg2
+ crossvalue = dx_seg1 * dy_seg2 - dy_seg1 * dx_seg2
sidesign = 1
- if side == 'left':
+ if side == "left":
sidesign = -1
- # We do not need to take care of the joint n-0 since we offset along a concave edge:
- if sidesign*offset*crossvalue <= 0:
+ # We do not need to take care of the joint n-0 since we
+ # offset along a concave edge:
+ if sidesign * offset * crossvalue <= 0:
return ring.parallel_offset(offset, side, resolution, join_style, mitre_limit)
# We offset along a convex edge so we offset the joint n-0 separately:
if coords[0] != coords[-1]:
coords.append(coords[0])
offset_ring1 = ring.parallel_offset(
- offset, side, resolution, join_style, mitre_limit)
+ offset, side, resolution, join_style, mitre_limit
+ )
offset_ring2 = LineString((coords[-2], coords[0], coords[1])).parallel_offset(
- offset, side, resolution, join_style, mitre_limit)
+ offset, side, resolution, join_style, mitre_limit
+ )
# Next we need to merge the results:
- if offset_ring1.geom_type == 'LineString':
- return LinearRing(offset_ring2.coords[:]+offset_ring1.coords[1:-1])
+ if offset_ring1.geom_type == "LineString":
+ return LinearRing(offset_ring2.coords[:] + offset_ring1.coords[1:-1])
else:
- # We have more than one resulting LineString for offset of the geometry (ring) = offset_ring1.
- # Hence we need to find the LineString which belongs to the offset of element 0 in coords =offset_ring2
+ # We have more than one resulting LineString for offset of
+ # the geometry (ring) = offset_ring1.
+ # Hence we need to find the LineString which belongs to the
+ # offset of element 0 in coords =offset_ring2
# in order to add offset_ring2 geometry to it:
result_list = []
- thresh = constants.offset_factor_for_adjacent_geometry*abs(offset)
+ thresh = constants.offset_factor_for_adjacent_geometry * abs(offset)
for offsets in offset_ring1:
- if(abs(offsets.coords[0][0]-coords[0][0]) < thresh and abs(offsets.coords[0][1]-coords[0][1]) < thresh):
- result_list.append(LinearRing(
- offset_ring2.coords[:]+offsets.coords[1:-1]))
+ if (
+ abs(offsets.coords[0][0] - coords[0][0]) < thresh
+ and abs(offsets.coords[0][1] - coords[0][1]) < thresh
+ ):
+ result_list.append(
+ LinearRing(offset_ring2.coords[:] + offsets.coords[1:-1])
+ )
else:
result_list.append(LinearRing(offsets))
return MultiLineString(result_list)
-# Removes all geometries which do not form a "valid" LinearRing (meaning a ring which does not form a straight line)
def take_only_valid_linear_rings(rings):
- if(rings.geom_type == 'MultiLineString'):
+ """
+ Removes all geometries which do not form a "valid" LinearRing
+ (meaning a ring which does not form a straight line)
+ """
+ if rings.geom_type == "MultiLineString":
new_list = []
for ring in rings:
- if len(ring.coords) > 3 or (len(ring.coords) == 3 and ring.coords[0] != ring.coords[-1]):
+ if len(ring.coords) > 3 or (
+ len(ring.coords) == 3 and ring.coords[0] != ring.coords[-1]
+ ):
new_list.append(ring)
if len(new_list) == 1:
return LinearRing(new_list[0])
@@ -86,138 +108,184 @@ def take_only_valid_linear_rings(rings):
return rings
-#Since naturally holes have the opposite point ordering than non-holes we make
-#all lines within the tree "root" uniform (having all the same ordering direction)
def make_tree_uniform_ccw(root):
+ """
+ Since naturally holes have the opposite point ordering than non-holes we
+ make all lines within the tree "root" uniform (having all the same
+ ordering direction)
+ """
for node in PreOrderIter(root):
- if(node.id == 'hole'):
+ if node.id == "hole":
node.val.coords = list(node.val.coords)[::-1]
-#Used to define which stitching strategy shall be used
+# Used to define which stitching strategy shall be used
class StitchingStrategy(IntEnum):
CLOSEST_POINT = 0
INNER_TO_OUTER = 1
-# Takes a polygon (which can have holes) as input and creates offsetted versions until the polygon is filled with these smaller offsets.
-# These created geometries are afterwards connected to each other and resampled with a maximum stitch_distance.
-# The return value is a LineString which should cover the full polygon.
-#Input:
-#-poly: The shapely polygon which can have holes
-#-offset: The used offset for the curves
-#-join_style: Join style for the offset - can be round, mitered or bevel (https://shapely.readthedocs.io/en/stable/manual.html#shapely.geometry.JOIN_STYLE)
-#For examples look at https://shapely.readthedocs.io/en/stable/_images/parallel_offset.png
-#-stitch_distance maximum allowed stitch distance between two points
-#-offset_by_half: True if the points shall be interlaced
-#-strategy: According to StitchingStrategy you can select between different strategies for the connection between parent and childs
-#Output:
-#-List of point coordinate tuples
-#-Tag (origin) of each point to analyze why a point was placed at this position
-def offset_poly(poly, offset, join_style, stitch_distance, offset_by_half, strategy, starting_point):
+
+def offset_poly(
+ poly, offset, join_style, stitch_distance, offset_by_half, strategy, starting_point
+):
+ """
+ Takes a polygon (which can have holes) as input and creates offsetted
+ versions until the polygon is filled with these smaller offsets.
+ These created geometries are afterwards connected to each other and
+ resampled with a maximum stitch_distance.
+ The return value is a LineString which should cover the full polygon.
+ Input:
+ -poly: The shapely polygon which can have holes
+ -offset: The used offset for the curves
+ -join_style: Join style for the offset - can be round, mitered or bevel
+ (https://shapely.readthedocs.io/en/stable/manual.html#shapely.geometry.JOIN_STYLE)
+ For examples look at
+ https://shapely.readthedocs.io/en/stable/_images/parallel_offset.png
+ -stitch_distance maximum allowed stitch distance between two points
+ -offset_by_half: True if the points shall be interlaced
+ -strategy: According to StitchingStrategy enum class you can select between
+ different strategies for the connection between parent and childs
+ -starting_point: Defines the starting point for the stitching
+ Output:
+ -List of point coordinate tuples
+ -Tag (origin) of each point to analyze why a point was placed
+ at this position
+ """
ordered_poly = orient(poly, -1)
- ordered_poly = ordered_poly.simplify(
- constants.simplification_threshold, False)
- root = AnyNode(id="node", val=ordered_poly.exterior, already_rastered=False, transferred_point_priority_deque=DEPQ(
- iterable=None, maxlen=None))
+ ordered_poly = ordered_poly.simplify(constants.simplification_threshold, False)
+ root = AnyNode(
+ id="node",
+ val=ordered_poly.exterior,
+ already_rastered=False,
+ transferred_point_priority_deque=DEPQ(iterable=None, maxlen=None),
+ )
active_polys = [root]
active_holes = [[]]
for holes in ordered_poly.interiors:
- #print("hole: - is ccw: ", LinearRing(holes).is_ccw)
active_holes[0].append(
- AnyNode(id="hole", val=holes, already_rastered=False, transferred_point_priority_deque=DEPQ(
- iterable=None, maxlen=None)))
+ AnyNode(
+ id="hole",
+ val=holes,
+ already_rastered=False,
+ transferred_point_priority_deque=DEPQ(iterable=None, maxlen=None),
+ )
+ )
- # counter = 0
- while len(active_polys) > 0: # and counter < 20:
- # counter += 1
- # print("New iter")
+ while len(active_polys) > 0:
current_poly = active_polys.pop()
current_holes = active_holes.pop()
poly_inners = []
- # outer = current_poly.val.parallel_offset(offset,'left', 5, join_style, 10)
- outer = offset_linear_ring(current_poly.val, offset, 'left', 5, join_style, 10)
+ outer = offset_linear_ring(
+ current_poly.val,
+ offset,
+ "left",
+ resolution=5,
+ joint_style=join_style,
+ mitre_limit=10,
+ )
outer = outer.simplify(constants.simplification_threshold, False)
outer = take_only_valid_linear_rings(outer)
for j in range(len(current_holes)):
- # inner = closeLinearRing(current_holes[j].val,offset/2.0).parallel_offset(offset,'left', 5, join_style, 10)
inner = offset_linear_ring(
- current_holes[j].val, offset, 'left', 5, join_style, 10)
+ current_holes[j].val,
+ offset,
+ "left",
+ resolution=5,
+ joint_style=join_style,
+ mitre_limit=10,
+ )
inner = inner.simplify(constants.simplification_threshold, False)
inner = take_only_valid_linear_rings(inner)
if not inner.is_empty:
poly_inners.append(Polygon(inner))
if not outer.is_empty:
if len(poly_inners) == 0:
- if outer.geom_type == 'LineString':
+ if outer.geom_type == "LineString":
result = Polygon(outer)
else:
result = MultiPolygon(polygonize(outer))
else:
- if outer.geom_type == 'LineString':
- result = Polygon(outer).difference(
- MultiPolygon(poly_inners))
+ if outer.geom_type == "LineString":
+ result = Polygon(outer).difference(MultiPolygon(poly_inners))
else:
- result = MultiPolygon(outer).difference(
- MultiPolygon(poly_inners))
+ result = MultiPolygon(outer).difference(MultiPolygon(poly_inners))
- if not result.is_empty and result.area > offset*offset/10:
+ if not result.is_empty and result.area > offset * offset / 10:
result_list = []
- if result.geom_type == 'Polygon':
+ if result.geom_type == "Polygon":
result_list = [result]
else:
result_list = list(result)
- # print("New result_list: ", len(result_list))
+
for polygon in result_list:
polygon = orient(polygon, -1)
- if polygon.area < offset*offset/10:
+ if polygon.area < offset * offset / 10:
continue
- polygon = polygon.simplify(constants.simplification_threshold, False)
+ polygon = polygon.simplify(
+ constants.simplification_threshold, False
+ )
poly_coords = polygon.exterior
- # if polygon.exterior.is_ccw:
- # hole.coords = list(hole.coords)[::-1]
- #poly_coords = polygon.exterior.simplify(constants.simplification_threshold, False)
poly_coords = take_only_valid_linear_rings(poly_coords)
if poly_coords.is_empty:
continue
- #print("node: - is ccw: ", LinearRing(poly_coords).is_ccw)
- # if(LinearRing(poly_coords).is_ccw):
- # print("Fehler!")
- node = AnyNode(id="node", parent=current_poly,
- val=poly_coords, already_rastered=False, transferred_point_priority_deque=DEPQ(
- iterable=None, maxlen=None))
+
+ node = AnyNode(
+ id="node",
+ parent=current_poly,
+ val=poly_coords,
+ already_rastered=False,
+ transferred_point_priority_deque=DEPQ(
+ iterable=None, maxlen=None
+ ),
+ )
active_polys.append(node)
hole_node_list = []
for hole in polygon.interiors:
hole_node = AnyNode(
- id="hole", val=hole, already_rastered=False, transferred_point_priority_deque=DEPQ(
- iterable=None, maxlen=None))
+ id="hole",
+ val=hole,
+ already_rastered=False,
+ transferred_point_priority_deque=DEPQ(
+ iterable=None, maxlen=None
+ ),
+ )
for previous_hole in current_holes:
if Polygon(hole).contains(Polygon(previous_hole.val)):
previous_hole.parent = hole_node
hole_node_list.append(hole_node)
active_holes.append(hole_node_list)
- for previous_hole in current_holes: # if the previous holes are not contained in the new holes they have been merged with the outer polygon
- if previous_hole.parent == None:
+ for previous_hole in current_holes:
+ # If the previous holes are not
+ # contained in the new holes they
+ # have been merged with the
+ # outer polygon
+ if previous_hole.parent is None:
previous_hole.parent = current_poly
-
- #DebuggingMethods.drawPoly(root, 'r-')
+ # DebuggingMethods.drawPoly(root, 'r-')
make_tree_uniform_ccw(root)
# print(RenderTree(root))
if strategy == StitchingStrategy.CLOSEST_POINT:
- connected_line, connected_line_origin = ConnectAndSamplePattern.connect_raster_tree_nearest_neighbor(
- root, offset, stitch_distance, starting_point, offset_by_half)
+ (
+ connected_line,
+ connected_line_origin,
+ ) = ConnectAndSamplePattern.connect_raster_tree_nearest_neighbor(
+ root, offset, stitch_distance, starting_point, offset_by_half
+ )
elif strategy == StitchingStrategy.INNER_TO_OUTER:
- connected_line, connected_line_origin = ConnectAndSamplePattern.connect_raster_tree_from_inner_to_outer(
- root, offset, stitch_distance, starting_point, offset_by_half)
+ (
+ connected_line,
+ connected_line_origin,
+ ) = ConnectAndSamplePattern.connect_raster_tree_from_inner_to_outer(
+ root, offset, stitch_distance, starting_point, offset_by_half
+ )
else:
- print("Invalid strategy!")
- assert(0)
+ raise ValueError("Invalid stitching stratety!")
return connected_line, connected_line_origin