pull/1548/head
Lex Neva 2022-05-03 16:58:55 -04:00 zatwierdzone przez Kaalleen
rodzic 5a69fa3e9c
commit aeeaf72338
5 zmienionych plików z 151 dodań i 167 usunięć

Wyświetl plik

@ -576,19 +576,17 @@ class FillStitch(EmbroideryElement):
if not starting_point:
starting_point = (0, 0)
for poly in polygons:
connectedLine, _ = tangential_fill_stitch_line_creator.offset_poly(
connected_line = tangential_fill_stitch_line_creator.tangential_fill(
poly,
-self.row_spacing,
self.join_style + 1,
self.max_stitch_length,
min(self.min_stitch_length, self.max_stitch_length),
self.interlaced,
self.tangential_strategy,
self.row_spacing,
self.max_stitch_length,
self.join_style + 1,
self.clockwise,
shgeo.Point(starting_point),
self.avoid_self_crossing,
self.clockwise
)
path = [InkstitchPoint(*p) for p in connectedLine]
path = [InkstitchPoint(*p) for p in connected_line]
stitch_group = StitchGroup(
color=self.color,
tags=("auto_fill", "auto_fill_top"),

Wyświetl plik

@ -16,8 +16,7 @@ from shapely.strtree import STRtree
from ..debug import debug
from ..stitch_plan import Stitch
from ..svg import PIXELS_PER_MM
from ..utils.geometry import Point as InkstitchPoint
from ..utils.geometry import line_string_to_point_list
from ..utils.geometry import Point as InkstitchPoint, line_string_to_point_list, ensure_multi_line_string
from .fill import intersect_region_with_grating, stitch_row
from .running_stitch import running_stitch
@ -396,15 +395,6 @@ def travel_grating(shape, angle, row_spacing):
return shgeo.MultiLineString(list(segments))
def ensure_multi_line_string(thing):
"""Given either a MultiLineString or a single LineString, return a MultiLineString"""
if isinstance(thing, shgeo.LineString):
return shgeo.MultiLineString([thing])
else:
return thing
def build_travel_edges(shape, fill_angle):
r"""Given a graph, compute the interior travel edges.

Wyświetl plik

@ -1,10 +1,7 @@
from enum import IntEnum
import networkx as nx
from depq import DEPQ
from shapely.geometry import MultiLineString, Polygon
from shapely.geometry import MultiPolygon
from shapely.geometry.polygon import LinearRing
from shapely.geometry import Polygon, MultiPolygon, GeometryCollection
from shapely.geometry.polygon import orient
from shapely.ops import polygonize
@ -13,7 +10,7 @@ from ..stitch_plan import Stitch
from ..stitches import constants
from ..stitches import tangential_fill_stitch_pattern_creator
from ..utils import DotDict
from ..utils.geometry import reverse_line_string
from ..utils.geometry import reverse_line_string, ensure_geometry_collection, ensure_multi_polygon
class Tree(nx.DiGraph):
@ -23,15 +20,13 @@ class Tree(nx.DiGraph):
def offset_linear_ring(ring, offset, resolution, join_style, mitre_limit):
result = Polygon(ring).buffer(offset, resolution, cap_style=2, join_style=join_style, mitre_limit=mitre_limit, single_sided=True)
result = Polygon(ring).buffer(-offset, resolution, cap_style=2, join_style=join_style, mitre_limit=mitre_limit, single_sided=True)
result = ensure_multi_polygon(result)
if result.geom_type == 'Polygon':
return result.exterior
else:
result_list = []
for poly in result.geoms:
result_list.append(poly.exterior)
return MultiLineString(result_list)
rings = GeometryCollection([poly.exterior for poly in result.geoms])
rings = rings.simplify(constants.simplification_threshold, False)
return take_only_valid_linear_rings(rings)
def take_only_valid_linear_rings(rings):
@ -39,24 +34,14 @@ def take_only_valid_linear_rings(rings):
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.geoms:
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])
else:
return MultiLineString(new_list)
elif rings.geom_type == "LineString" or rings.geom_type == "LinearRing":
if len(rings.coords) <= 2:
return LinearRing()
elif len(rings.coords) == 3 and rings.coords[0] == rings.coords[-1]:
return LinearRing()
else:
return rings
else:
return LinearRing()
valid_rings = []
for ring in ensure_geometry_collection(rings).geoms:
if len(ring.coords) > 3 or (len(ring.coords) == 3 and ring.coords[0] != ring.coords[-1]):
valid_rings.append(ring)
return GeometryCollection(valid_rings)
def orient_linear_ring(ring, clockwise=True):
@ -127,8 +112,7 @@ def check_and_prepare_tree_for_valid_spiral(tree):
return process_node('root')
def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance, offset_by_half, strategy, starting_point, # noqa: C901
avoid_self_crossing, clockwise):
def offset_poly(poly, offset, join_style, clockwise):
"""
Takes a polygon (which can have holes) as input and creates offsetted
versions until the polygon is filled with these smaller offsets.
@ -159,21 +143,9 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance,
at this position
"""
if strategy in (StitchingStrategy.SINGLE_SPIRAL, StitchingStrategy.DOUBLE_SPIRAL) and len(poly.interiors) > 1:
raise ValueError(
"Single spiral geometry must not have more than one hole!")
ordered_poly = orient(poly, -1)
ordered_poly = ordered_poly.simplify(
constants.simplification_threshold, False)
tree = Tree()
tree.add_node('root',
type='node',
parent=None,
val=ordered_poly.exterior,
already_rastered=False,
transferred_point_priority_deque=DEPQ(iterable=None, maxlen=None),
)
tree.add_node('root', type='node', parent=None, val=ordered_poly.exterior)
active_polys = ['root']
active_holes = [[]]
@ -181,103 +153,26 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance,
node_num = 0
for hole in ordered_poly.interiors:
tree.add_node(node_num,
type="hole",
val=hole,
already_rastered=False,
transferred_point_priority_deque=DEPQ(iterable=None, maxlen=None),
)
tree.add_node(node_num, type="hole", val=hole)
active_holes[0].append(node_num)
node_num += 1
while len(active_polys) > 0:
current_poly = active_polys.pop()
current_holes = active_holes.pop()
poly_inners = []
outer, inners = offset_polygon_and_holes(tree, current_poly, current_holes, offset, join_style)
outer = offset_linear_ring(
tree.nodes[current_poly].val,
offset,
resolution=5,
join_style=join_style,
mitre_limit=10,
)
outer = outer.simplify(constants.simplification_threshold, False)
outer = take_only_valid_linear_rings(outer)
for hole in current_holes:
inner = offset_linear_ring(
tree.nodes[hole].val,
-offset, # take negative offset for holes
resolution=5,
join_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" or outer.geom_type == "LinearRing":
result = Polygon(outer)
else:
result = MultiPolygon(polygonize(outer))
else:
if outer.geom_type == "LineString" or outer.geom_type == "LinearRing":
result = Polygon(outer).difference(
MultiPolygon(poly_inners))
else:
result = MultiPolygon(polygonize(outer)).difference(
MultiPolygon(poly_inners))
polygons = match_polygons_and_holes(outer, inners)
if not result.is_empty and result.area > offset * offset / 10:
if result.geom_type == "Polygon":
result_list = [result]
else:
result_list = list(result.geoms)
if not polygons.is_empty:
for polygon in polygons.geoms:
new_polygon, new_holes = convert_polygon_to_nodes(tree, polygon, parent_polygon=current_poly, child_holes=current_holes)
for polygon in result_list:
polygon = orient(polygon, -1)
if new_polygon:
active_polys.append(new_polygon)
active_holes.append(new_holes)
if polygon.area < offset * offset / 10:
continue
polygon = polygon.simplify(
constants.simplification_threshold, False
)
poly_coords = polygon.exterior
poly_coords = take_only_valid_linear_rings(poly_coords)
if poly_coords.is_empty:
continue
node = node_num
node_num += 1
tree.add_node(node,
type='node',
parent=current_poly,
val=poly_coords,
already_rastered=False,
transferred_point_priority_deque=DEPQ(iterable=None, maxlen=None),
)
tree.add_edge(current_poly, node)
active_polys.append(node)
hole_node_list = []
for hole in polygon.interiors:
hole_node = node_num
node_num += 1
tree.add_node(hole_node,
type="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(tree.nodes[previous_hole].val)):
tree.nodes[previous_hole].parent = hole_node
tree.add_edge(hole_node, previous_hole)
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
@ -289,22 +184,96 @@ def offset_poly(poly, offset, join_style, stitch_distance, min_stitch_distance,
make_tree_uniform(tree, clockwise)
return tree
def offset_polygon_and_holes(tree, poly, holes, offset, join_style):
outer = offset_linear_ring(
tree.nodes[poly].val,
offset,
resolution=5,
join_style=join_style,
mitre_limit=10,
)
inners = []
for hole in holes:
inner = offset_linear_ring(
tree.nodes[hole].val,
-offset, # take negative offset for holes
resolution=5,
join_style=join_style,
mitre_limit=10,
)
if not inner.is_empty:
inners.append(Polygon(inner.geoms[0]))
return outer, inners
def match_polygons_and_holes(outer, inners):
result = MultiPolygon(polygonize(outer))
if len(inners) > 0:
result = ensure_geometry_collection(result.difference(MultiPolygon(inners)))
return result
def convert_polygon_to_nodes(tree, polygon, parent_polygon, child_holes):
polygon = orient(polygon, -1)
if polygon.area < 0.1:
return None, None
polygon = polygon.simplify(constants.simplification_threshold, False)
valid_rings = take_only_valid_linear_rings(polygon.exterior)
try:
exterior = valid_rings.geoms[0]
except IndexError:
return None, None
node = id(polygon) # just needs to be unique
tree.add_node(node, type='node', parent=parent_polygon, val=exterior)
tree.add_edge(parent_polygon, node)
hole_node_list = []
for hole in polygon.interiors:
hole_node = id(hole)
tree.add_node(hole_node, type="hole", val=hole)
for previous_hole in child_holes:
if Polygon(hole).contains(Polygon(tree.nodes[previous_hole].val)):
tree.nodes[previous_hole].parent = hole_node
tree.add_edge(hole_node, previous_hole)
hole_node_list.append(hole_node)
return node, hole_node_list
def tangential_fill(poly, strategy, offset, stitch_distance, join_style, clockwise, starting_point, avoid_self_crossing):
if strategy in (StitchingStrategy.SINGLE_SPIRAL, StitchingStrategy.DOUBLE_SPIRAL) and len(poly.interiors) > 1:
raise ValueError(
"Single spiral geometry must not have more than one hole!")
tree = offset_poly(poly, offset, join_style, clockwise)
if strategy == StitchingStrategy.INNER_TO_OUTER:
connected_line = tangential_fill_stitch_pattern_creator.connect_raster_tree_from_inner_to_outer(
tree, 'root', abs(offset), stitch_distance, min_stitch_distance, starting_point, offset_by_half, avoid_self_crossing)
tree, 'root', offset, stitch_distance, starting_point, avoid_self_crossing)
path = [Stitch(*point) for point in connected_line.coords]
return running_stitch(path, stitch_distance), "whatever"
return running_stitch(path, stitch_distance)
elif strategy == StitchingStrategy.SINGLE_SPIRAL:
if not check_and_prepare_tree_for_valid_spiral(tree):
raise ValueError("Geometry cannot be filled with one spiral!")
(connected_line, connected_line_origin) = tangential_fill_stitch_pattern_creator.connect_raster_tree_single_spiral(
tree, offset, stitch_distance, min_stitch_distance, starting_point, offset_by_half)
connected_line = tangential_fill_stitch_pattern_creator.connect_raster_tree_single_spiral(
tree, offset, stitch_distance, starting_point)
elif strategy == StitchingStrategy.DOUBLE_SPIRAL:
if not check_and_prepare_tree_for_valid_spiral(tree):
raise ValueError("Geometry cannot be filled with a double spiral!")
(connected_line, connected_line_origin) = tangential_fill_stitch_pattern_creator.connect_raster_tree_double_spiral(
tree, offset, stitch_distance, min_stitch_distance, starting_point, offset_by_half)
connected_line = tangential_fill_stitch_pattern_creator.connect_raster_tree_double_spiral(
tree, offset, stitch_distance, starting_point)
else:
raise ValueError("Invalid stitching stratety!")
return connected_line, connected_line_origin
return connected_line

Wyświetl plik

@ -107,8 +107,8 @@ def create_nearest_points_list(
return children_nearest_points
def connect_raster_tree_from_inner_to_outer(tree, node, offset, stitch_distance, min_stitch_distance, starting_point,
offset_by_half, avoid_self_crossing, forward=True):
def connect_raster_tree_from_inner_to_outer(tree, node, offset, stitch_distance, starting_point,
avoid_self_crossing, forward=True):
"""
Takes the offset curves organized as a tree, connects and samples them.
Strategy: A connection from parent to child is made as fast as possible to
@ -183,9 +183,7 @@ def connect_raster_tree_from_inner_to_outer(tree, node, offset, stitch_distance,
child_connection.child_node,
offset,
stitch_distance,
min_stitch_distance,
child_connection.nearest_point_child,
offset_by_half,
avoid_self_crossing,
not forward
)
@ -259,7 +257,7 @@ def interpolate_linear_rings(ring1, ring2, max_stitch_length, start=None):
return result.simplify(constants.simplification_threshold, False)
def connect_raster_tree_single_spiral(tree, used_offset, stitch_distance, min_stitch_distance, close_point, offset_by_half): # noqa: C901
def connect_raster_tree_single_spiral(tree, used_offset, stitch_distance, close_point): # noqa: C901
"""
Takes the offsetted curves organized as tree, connects and samples them as a spiral.
It expects that each node in the tree has max. one child
@ -288,10 +286,10 @@ def connect_raster_tree_single_spiral(tree, used_offset, stitch_distance, min_st
path = make_spiral(rings, stitch_distance, starting_point)
path = [Stitch(*stitch) for stitch in path]
return running_stitch(path, stitch_distance), None
return running_stitch(path, stitch_distance)
def connect_raster_tree_double_spiral(tree, used_offset, stitch_distance, min_stitch_distance, close_point, offset_by_half): # noqa: C901
def connect_raster_tree_double_spiral(tree, used_offset, stitch_distance, close_point): # noqa: C901
"""
Takes the offsetted curves organized as tree, connects and samples them as a spiral.
It expects that each node in the tree has max. one child
@ -320,7 +318,7 @@ def connect_raster_tree_double_spiral(tree, used_offset, stitch_distance, min_st
path = make_fermat_spiral(rings, stitch_distance, starting_point)
path = [Stitch(*stitch) for stitch in path]
return running_stitch(path, stitch_distance), None
return running_stitch(path, stitch_distance)
def make_fermat_spiral(rings, stitch_distance, starting_point):

Wyświetl plik

@ -5,7 +5,7 @@
import math
from shapely.geometry import LineString, LinearRing
from shapely.geometry import LineString, LinearRing, MultiLineString, Polygon, MultiPolygon, GeometryCollection
from shapely.geometry import Point as ShapelyPoint
@ -66,6 +66,35 @@ def reverse_line_string(line_string):
return LineString(line_string.coords[::-1])
def ensure_multi_line_string(thing):
"""Given either a MultiLineString or a single LineString, return a MultiLineString"""
if isinstance(thing, LineString):
return MultiLineString([thing])
else:
return thing
def ensure_geometry_collection(thing):
"""Given either some kind of geometry or a GeometryCollection, return a GeometryCollection"""
if isinstance(thing, (MultiLineString, MultiPolygon)):
return GeometryCollection(thing.geoms)
elif isinstance(thing, GeometryCollection):
return thing
else:
return GeometryCollection([thing])
def ensure_multi_polygon(thing):
"""Given either a MultiPolygon or a single Polygon, return a MultiPolygon"""
if isinstance(thing, Polygon):
return MultiPolygon([thing])
else:
return thing
def cut_path(points, length):
"""Return a subsection of at the start of the path that is length units long.