kopia lustrzana https://github.com/inkstitch/inkstitch
refactor, tidy, and C901 fixes
rodzic
aeeaf72338
commit
330c6be787
|
@ -15,10 +15,9 @@ from shapely.validation import explain_validity
|
|||
from ..i18n import _
|
||||
from ..marker import get_marker_elements
|
||||
from ..stitch_plan import StitchGroup
|
||||
from ..stitches import tangential_fill_stitch_line_creator, auto_fill, legacy_fill, guided_fill
|
||||
from ..stitches import tangential_fill, auto_fill, legacy_fill, guided_fill
|
||||
from ..svg import PIXELS_PER_MM
|
||||
from ..svg.tags import INKSCAPE_LABEL
|
||||
from ..utils import Point as InkstitchPoint
|
||||
from ..utils import cache, version
|
||||
from .element import EmbroideryElement, param
|
||||
from .validation import ValidationError, ValidationWarning
|
||||
|
@ -571,26 +570,40 @@ class FillStitch(EmbroideryElement):
|
|||
return [stitch_group]
|
||||
|
||||
def do_tangential_fill(self, last_patch, starting_point):
|
||||
stitch_groups = []
|
||||
polygons = self.fill_shape.geoms
|
||||
if not starting_point:
|
||||
starting_point = (0, 0)
|
||||
for poly in polygons:
|
||||
connected_line = tangential_fill_stitch_line_creator.tangential_fill(
|
||||
poly,
|
||||
self.tangential_strategy,
|
||||
self.row_spacing,
|
||||
self.max_stitch_length,
|
||||
self.join_style + 1,
|
||||
self.clockwise,
|
||||
shgeo.Point(starting_point),
|
||||
self.avoid_self_crossing,
|
||||
)
|
||||
path = [InkstitchPoint(*p) for p in connected_line]
|
||||
starting_point = shgeo.Point(starting_point)
|
||||
|
||||
stitch_groups = []
|
||||
for polygon in self.fill_shape.geoms:
|
||||
tree = tangential_fill.offset_polygon(polygon, self.row_spacing, self.join_style + 1, self.clockwise)
|
||||
|
||||
stitches = []
|
||||
if self.tangential_strategy == 0:
|
||||
stitches = tangential_fill.inner_to_outer(
|
||||
tree,
|
||||
self.row_spacing,
|
||||
self.max_stitch_length,
|
||||
starting_point,
|
||||
self.avoid_self_crossing
|
||||
)
|
||||
elif self.tangential_strategy == 1:
|
||||
stitches = tangential_fill.single_spiral(
|
||||
tree,
|
||||
self.max_stitch_length,
|
||||
starting_point
|
||||
)
|
||||
elif self.tangential_strategy == 2:
|
||||
stitches = tangential_fill.double_spiral(
|
||||
tree,
|
||||
self.max_stitch_length,
|
||||
starting_point
|
||||
)
|
||||
|
||||
stitch_group = StitchGroup(
|
||||
color=self.color,
|
||||
tags=("auto_fill", "auto_fill_top"),
|
||||
stitches=path)
|
||||
stitches=stitches)
|
||||
stitch_groups.append(stitch_group)
|
||||
|
||||
return stitch_groups
|
||||
|
@ -611,13 +624,11 @@ class FillStitch(EmbroideryElement):
|
|||
self.angle,
|
||||
self.row_spacing,
|
||||
self.max_stitch_length,
|
||||
min(self.min_stitch_length, self.max_stitch_length),
|
||||
self.running_stitch_length,
|
||||
self.skip_last,
|
||||
starting_point,
|
||||
ending_point,
|
||||
self.underpath,
|
||||
self.interlaced))
|
||||
self.underpath))
|
||||
return [stitch_group]
|
||||
|
||||
@cache
|
||||
|
|
|
@ -11,19 +11,16 @@ from ..stitch_plan import Stitch
|
|||
from ..utils.geometry import Point as InkstitchPoint, reverse_line_string
|
||||
|
||||
|
||||
@debug.time
|
||||
def guided_fill(shape,
|
||||
guideline,
|
||||
angle,
|
||||
row_spacing,
|
||||
max_stitch_length,
|
||||
min_stitch_length,
|
||||
running_stitch_length,
|
||||
skip_last,
|
||||
starting_point,
|
||||
ending_point=None,
|
||||
underpath=True,
|
||||
offset_by_half=True):
|
||||
underpath=True):
|
||||
try:
|
||||
segments = intersect_region_with_grating_guideline(shape, guideline, row_spacing)
|
||||
fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
|
||||
|
@ -36,15 +33,12 @@ def guided_fill(shape,
|
|||
|
||||
travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath)
|
||||
path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point)
|
||||
result = path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing,
|
||||
max_stitch_length, min_stitch_length, running_stitch_length, skip_last, offset_by_half)
|
||||
result = path_to_stitches(path, travel_graph, fill_stitch_graph, max_stitch_length, running_stitch_length, skip_last)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@debug.time
|
||||
def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, max_stitch_length, min_stitch_length,
|
||||
running_stitch_length, skip_last, offset_by_half):
|
||||
def path_to_stitches(path, travel_graph, fill_stitch_graph, stitch_length, running_stitch_length, skip_last):
|
||||
path = collapse_sequential_outline_edges(path)
|
||||
|
||||
stitches = []
|
||||
|
@ -62,7 +56,7 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing,
|
|||
path_geometry = reverse_line_string(path_geometry)
|
||||
|
||||
point_list = [Stitch(*point) for point in path_geometry.coords]
|
||||
new_stitches = running_stitch(point_list, max_stitch_length)
|
||||
new_stitches = running_stitch(point_list, stitch_length)
|
||||
|
||||
# need to tag stitches
|
||||
|
||||
|
@ -124,7 +118,7 @@ def repair_non_simple_lines(line):
|
|||
counter += 1
|
||||
if repaired.geom_type != 'LineString':
|
||||
raise ValueError(
|
||||
_("Guide line (or offsetted instance) is self crossing!"))
|
||||
_("Guide line (or offset copy) is self crossing!"))
|
||||
else:
|
||||
return repaired
|
||||
|
||||
|
|
|
@ -0,0 +1,538 @@
|
|||
from collections import namedtuple
|
||||
from itertools import chain
|
||||
|
||||
import networkx as nx
|
||||
import numpy as np
|
||||
import trimesh
|
||||
from shapely.geometry import GeometryCollection, MultiPolygon, Polygon, LineString, MultiLineString, Point
|
||||
from shapely.geometry.polygon import orient
|
||||
from shapely.ops import nearest_points
|
||||
from shapely.ops import polygonize
|
||||
|
||||
from .running_stitch import running_stitch
|
||||
from ..i18n import _
|
||||
from ..stitch_plan import Stitch
|
||||
from ..stitches import constants
|
||||
from ..utils import DotDict
|
||||
from ..utils.geometry import cut, reverse_line_string, roll_linear_ring
|
||||
from ..utils.geometry import ensure_geometry_collection, ensure_multi_polygon
|
||||
|
||||
|
||||
class Tree(nx.DiGraph):
|
||||
# This lets us do tree.nodes['somenode'].parent instead of the default
|
||||
# tree.nodes['somenode']['parent'].
|
||||
node_attr_dict_factory = DotDict
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.__node_num = 0
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def generate_node_name(self):
|
||||
node = self.__node_num
|
||||
self.__node_num += 1
|
||||
|
||||
return node
|
||||
|
||||
|
||||
nearest_neighbor_tuple = namedtuple(
|
||||
"nearest_neighbor_tuple",
|
||||
[
|
||||
"nearest_point_parent",
|
||||
"nearest_point_child",
|
||||
"proj_distance_parent",
|
||||
"child_node",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
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 = ensure_multi_polygon(result)
|
||||
|
||||
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):
|
||||
"""
|
||||
Removes all geometries which do not form a "valid" LinearRing.
|
||||
|
||||
A "valid" ring is one that does not form a straight line.
|
||||
"""
|
||||
|
||||
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):
|
||||
# Unfortunately for us, Inkscape SVGs have an inverted Y coordinate.
|
||||
# Normally we don't have to care about that, but in this very specific
|
||||
# case, the meaning of is_ccw is flipped. It actually tests whether
|
||||
# a ring is clockwise. That makes this logic super-confusing.
|
||||
if ring.is_ccw != clockwise:
|
||||
return reverse_line_string(ring)
|
||||
else:
|
||||
return ring
|
||||
|
||||
|
||||
def _orient_tree(tree, clockwise=True):
|
||||
"""
|
||||
Orient all linear rings in the tree.
|
||||
|
||||
Since naturally holes have the opposite point ordering than non-holes we
|
||||
make all lines within the tree uniform (having all the same ordering
|
||||
direction)
|
||||
"""
|
||||
|
||||
for node in tree.nodes.values():
|
||||
node.val = _orient_linear_ring(node.val, clockwise)
|
||||
|
||||
|
||||
def offset_polygon(polygon, offset, join_style, clockwise):
|
||||
"""
|
||||
Convert a polygon to a tree of isocontours.
|
||||
|
||||
An isocontour is an offset version of the polygon's boundary. For example,
|
||||
the isocontours of a circle are a set of concentric circles inside the
|
||||
circle.
|
||||
|
||||
This function takes a polygon (which may have holes) as input and creates
|
||||
isocontours until the polygon is filled completely. The isocontours are
|
||||
returned as a Tree, with a parent-child relationship indicating that the
|
||||
parent isocontour contains the child isocontour.
|
||||
|
||||
Arguments:
|
||||
polygon - The shapely Polygon which may have holes
|
||||
offset - The spacing between isocontours
|
||||
join_style - Join style used when offsetting the Polygon border to create
|
||||
isocontours. Can be round, mitered or bevel, as defined by
|
||||
shapely:
|
||||
https://shapely.readthedocs.io/en/stable/manual.html#shapely.geometry.JOIN_STYLE
|
||||
clockwise - If True, isocontour points are in clockwise order; if False, counter-clockwise.
|
||||
|
||||
Return Value:
|
||||
Tree - see above
|
||||
"""
|
||||
|
||||
ordered_polygon = orient(polygon, -1)
|
||||
tree = Tree()
|
||||
tree.add_node('root', type='node', parent=None, val=ordered_polygon.exterior)
|
||||
active_polygons = ['root']
|
||||
active_holes = [[]]
|
||||
|
||||
for hole in ordered_polygon.interiors:
|
||||
hole_node = tree.generate_node_name()
|
||||
tree.add_node(hole_node, type="hole", val=hole)
|
||||
active_holes[0].append(hole_node)
|
||||
|
||||
while len(active_polygons) > 0:
|
||||
current_poly = active_polygons.pop()
|
||||
current_holes = active_holes.pop()
|
||||
|
||||
outer, inners = _offset_polygon_and_holes(tree, current_poly, current_holes, offset, join_style)
|
||||
polygons = _match_polygons_and_holes(outer, inners)
|
||||
|
||||
for polygon in polygons.geoms:
|
||||
new_polygon, new_holes = _convert_polygon_to_nodes(tree, polygon, parent_polygon=current_poly, child_holes=current_holes)
|
||||
|
||||
if new_polygon is not None:
|
||||
active_polygons.append(new_polygon)
|
||||
active_holes.append(new_holes)
|
||||
|
||||
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 not tree.nodes[previous_hole].parent:
|
||||
tree.nodes[previous_hole].parent = current_poly
|
||||
tree.add_edge(current_poly, previous_hole)
|
||||
|
||||
_orient_tree(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.geoms))
|
||||
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 = tree.generate_node_name()
|
||||
tree.add_node(node, type='node', parent=parent_polygon, val=exterior)
|
||||
tree.add_edge(parent_polygon, node)
|
||||
|
||||
hole_nodes = []
|
||||
for hole in polygon.interiors:
|
||||
hole_node = tree.generate_node_name()
|
||||
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_nodes.append(hole_node)
|
||||
|
||||
return node, hole_nodes
|
||||
|
||||
|
||||
def _get_nearest_points_closer_than_thresh(travel_line, next_line, threshold):
|
||||
"""
|
||||
Find the first point along travel_line that is within threshold of next_line.
|
||||
|
||||
Input:
|
||||
travel_line - The "parent" line for which the distance should
|
||||
be minimized to enter next_line
|
||||
next_line - contains the next_line which need to be entered
|
||||
threshold - The distance between travel_line and next_line needs
|
||||
to below threshold to be a valid point for entering
|
||||
|
||||
Return value:
|
||||
tuple or None
|
||||
- the tuple structure is:
|
||||
(point in travel_line, point in next_line)
|
||||
- None is returned if there is no point that satisfies the threshold.
|
||||
"""
|
||||
|
||||
# We'll buffer next_line and find the intersection with travel_line.
|
||||
# Then we'll return the very first point in the intersection,
|
||||
# matched with a corresponding point on next_line. Fortunately for
|
||||
# us, intersection of a Polygon with a LineString yields pieces of
|
||||
# the LineString in the same order as the input LineString.
|
||||
threshold_area = next_line.buffer(threshold)
|
||||
portion_within_threshold = travel_line.intersection(threshold_area)
|
||||
|
||||
if portion_within_threshold.is_empty:
|
||||
return None
|
||||
else:
|
||||
if isinstance(portion_within_threshold, MultiLineString):
|
||||
portion_within_threshold = portion_within_threshold.geoms[0]
|
||||
|
||||
parent_point = Point(portion_within_threshold.coords[0])
|
||||
return nearest_points(parent_point, next_line)
|
||||
|
||||
|
||||
def _create_nearest_points_list(
|
||||
travel_line, tree, children, threshold, threshold_hard):
|
||||
"""Determine the best place to enter each of parent's children
|
||||
|
||||
Arguments:
|
||||
travel_line - The "parent" line for which the distance should
|
||||
be minimized to enter each child
|
||||
children - children of travel_line that need to be entered
|
||||
threshold - The distance between travel_line and a child should
|
||||
to be below threshold to be a valid point for entering
|
||||
threshold_hard - As a last resort, we can accept an entry point
|
||||
that is this far way
|
||||
|
||||
Return value:
|
||||
list of nearest_neighbor_tuple - indicating where to enter each
|
||||
respective child
|
||||
"""
|
||||
|
||||
children_nearest_points = []
|
||||
|
||||
for child in children:
|
||||
result = _get_nearest_points_closer_than_thresh(travel_line, tree.nodes[child].val, threshold)
|
||||
if result is None:
|
||||
# where holes meet outer borders a distance
|
||||
# up to 2 * used offset can arise
|
||||
result = _get_nearest_points_closer_than_thresh(travel_line, tree.nodes[child].val, threshold_hard)
|
||||
|
||||
proj = travel_line.project(result[0])
|
||||
children_nearest_points.append(
|
||||
nearest_neighbor_tuple(
|
||||
nearest_point_parent=result[0],
|
||||
nearest_point_child=result[1],
|
||||
proj_distance_parent=proj,
|
||||
child_node=child,
|
||||
)
|
||||
)
|
||||
|
||||
return children_nearest_points
|
||||
|
||||
|
||||
def _find_path_inner_to_outer(tree, node, offset, starting_point,
|
||||
avoid_self_crossing, forward=True):
|
||||
"""Find a stitch path for this ring and its children.
|
||||
|
||||
Strategy: A connection from parent to child is made as fast as possible to
|
||||
reach the innermost child as fast as possible in order to stitch afterwards
|
||||
from inner to outer.
|
||||
|
||||
This function calls itself recursively to find a stitch path for each child
|
||||
(and its children).
|
||||
|
||||
Arguments:
|
||||
tree - a Tree of isocontours (as returned by offset_polygon)
|
||||
offset - offset that was passed to offset_polygon
|
||||
starting_point - starting point for stitching
|
||||
avoid_self_crossing - if True, tries to generate a path that does not
|
||||
cross itself.
|
||||
forward - if True, this ring will be stitched in its natural direction
|
||||
(used internally by avoid_self_crossing)
|
||||
|
||||
Return value:
|
||||
LineString -- the stitching path
|
||||
"""
|
||||
|
||||
current_node = tree.nodes[node]
|
||||
current_ring = current_node.val
|
||||
|
||||
if not forward and avoid_self_crossing:
|
||||
current_ring = reverse_line_string(current_ring)
|
||||
|
||||
# reorder the coordinates of this ring so that it starts with
|
||||
# a point nearest the starting_point
|
||||
start_distance = current_ring.project(starting_point)
|
||||
current_ring = roll_linear_ring(current_ring, start_distance)
|
||||
current_node.val = current_ring
|
||||
|
||||
# Find where along this ring to connect to each child.
|
||||
nearest_points_list = _create_nearest_points_list(
|
||||
current_ring,
|
||||
tree,
|
||||
tree[node],
|
||||
constants.offset_factor_for_adjacent_geometry * offset,
|
||||
2.05 * offset
|
||||
)
|
||||
nearest_points_list.sort(key=lambda tup: tup.proj_distance_parent)
|
||||
|
||||
result_coords = []
|
||||
if not nearest_points_list:
|
||||
# We have no children, so we're at the center of a spiral. Reversing
|
||||
# the ring gives a nicer visual appearance.
|
||||
# current_ring = reverse_line_string(current_ring)
|
||||
pass
|
||||
else:
|
||||
# This is a recursive algorithm. We'll stitch along this ring, pausing
|
||||
# to jump to each child ring in turn and sew it before continuing on
|
||||
# this ring. We'll end back where we started.
|
||||
|
||||
result_coords.append(current_ring.coords[0])
|
||||
distance_so_far = 0
|
||||
for child_connection in nearest_points_list:
|
||||
# Cut this ring into pieces before and after where this child will connect.
|
||||
before, after = cut(current_ring, child_connection.proj_distance_parent - distance_so_far)
|
||||
distance_so_far += child_connection.proj_distance_parent
|
||||
|
||||
# Stitch the part leading up to this child.
|
||||
if before is not None:
|
||||
result_coords.extend(before.coords)
|
||||
|
||||
# Stitch this child. The child will start and end in the same
|
||||
# place, which should be close to our current location.
|
||||
child_path = _find_path_inner_to_outer(
|
||||
tree,
|
||||
child_connection.child_node,
|
||||
offset,
|
||||
child_connection.nearest_point_child,
|
||||
avoid_self_crossing,
|
||||
not forward
|
||||
)
|
||||
result_coords.extend(child_path.coords)
|
||||
|
||||
# Skip ahead a little bit on this ring before resuming. This
|
||||
# gives a nice spiral pattern, where we spiral out from the
|
||||
# innermost child.
|
||||
if after is not None:
|
||||
skip, after = cut(after, offset)
|
||||
distance_so_far += offset
|
||||
|
||||
current_ring = after
|
||||
|
||||
if current_ring is not None:
|
||||
# skip a little at the end so we don't end exactly where we started.
|
||||
remaining_length = current_ring.length
|
||||
if remaining_length > offset:
|
||||
current_ring, skip = cut(current_ring, current_ring.length - offset)
|
||||
|
||||
result_coords.extend(current_ring.coords)
|
||||
|
||||
return LineString(result_coords)
|
||||
|
||||
|
||||
def inner_to_outer(tree, offset, stitch_length, starting_point, avoid_self_crossing):
|
||||
"""Fill a shape with spirals, from innermost to outermost."""
|
||||
|
||||
stitch_path = _find_path_inner_to_outer(tree, 'root', offset, starting_point, avoid_self_crossing)
|
||||
points = [Stitch(*point) for point in stitch_path.coords]
|
||||
stitches = running_stitch(points, stitch_length)
|
||||
|
||||
return stitches
|
||||
|
||||
|
||||
def _reorder_linear_ring(ring, start):
|
||||
distances = ring - start
|
||||
start_index = np.argmin(np.linalg.norm(distances, axis=1))
|
||||
return np.roll(ring, -start_index, axis=0)
|
||||
|
||||
|
||||
def _interpolate_linear_rings(ring1, ring2, max_stitch_length, start=None):
|
||||
"""
|
||||
Interpolate between two LinearRings
|
||||
|
||||
Creates a path from start_point on ring1 and around the rings, ending at a
|
||||
nearby point on ring2. The path will smoothly transition from ring1 to
|
||||
ring2 as it travels around the rings.
|
||||
|
||||
Inspired by interpolate() from https://github.com/mikedh/pocketing/blob/master/pocketing/polygons.py
|
||||
|
||||
Arguments:
|
||||
ring1 -- LinearRing start point will lie on
|
||||
ring2 -- LinearRing end point will lie on
|
||||
max_stitch_length -- maximum stitch length (used to calculate resampling accuracy)
|
||||
start -- Point on ring1 to start at, as a tuple
|
||||
|
||||
Return value: Path interpolated between two LinearRings, as a LineString.
|
||||
"""
|
||||
|
||||
# Resample the two LinearRings so that they are the same number of points
|
||||
# long. Then take the corresponding points in each ring and interpolate
|
||||
# between them, gradually going more toward ring2.
|
||||
#
|
||||
# This is a little less accurate than the method in interpolate(), but several
|
||||
# orders of magnitude faster because we're not building and querying a KDTree.
|
||||
|
||||
num_points = int(20 * ring1.length / max_stitch_length)
|
||||
ring1_resampled = trimesh.path.traversal.resample_path(np.array(ring1.coords), count=num_points)
|
||||
ring2_resampled = trimesh.path.traversal.resample_path(np.array(ring2.coords), count=num_points)
|
||||
|
||||
if start is not None:
|
||||
ring1_resampled = _reorder_linear_ring(ring1_resampled, start)
|
||||
ring2_resampled = _reorder_linear_ring(ring2_resampled, start)
|
||||
|
||||
weights = np.linspace(0.0, 1.0, num_points).reshape((-1, 1))
|
||||
points = (ring1_resampled * (1.0 - weights)) + (ring2_resampled * weights)
|
||||
result = LineString(points)
|
||||
|
||||
# TODO: remove when rastering is cheaper
|
||||
return result.simplify(constants.simplification_threshold, False)
|
||||
|
||||
|
||||
def _check_and_prepare_tree_for_valid_spiral(tree):
|
||||
"""Check whether spiral fill is possible, and tweak if necessary.
|
||||
|
||||
Takes a tree consisting of isocontours. If a parent has more than one child
|
||||
we cannot create a spiral. However, to make the routine more robust, we
|
||||
allow more than one child if only one of the children has own children. The
|
||||
other children are removed in this routine then. If the routine returns true,
|
||||
the tree will have been cleaned up from unwanted children.
|
||||
|
||||
If even with these weaker constraints, a spiral is not possible, False is
|
||||
returned.
|
||||
"""
|
||||
|
||||
def process_node(node):
|
||||
children = set(tree[node])
|
||||
|
||||
if len(children) == 0:
|
||||
return True
|
||||
elif len(children) == 1:
|
||||
child = children.pop()
|
||||
return process_node(child)
|
||||
else:
|
||||
children_with_children = {child for child in children if tree[child]}
|
||||
if len(children_with_children) > 1:
|
||||
# Node has multiple children with children, so a perfect spiral is not possible.
|
||||
# This False value will be returned all the way up the stack.
|
||||
return False
|
||||
elif len(children_with_children) == 1:
|
||||
children_without_children = children - children_with_children
|
||||
child = children_with_children.pop()
|
||||
tree.remove_nodes_from(children_without_children)
|
||||
return process_node(child)
|
||||
else:
|
||||
# None of the children has its own children, so we'll just take the longest.
|
||||
longest = max(children, key=lambda child: tree[child]['val'].length)
|
||||
shorter_children = children - {longest}
|
||||
tree.remove_nodes_from(shorter_children)
|
||||
return process_node(longest)
|
||||
|
||||
return process_node('root')
|
||||
|
||||
|
||||
def single_spiral(tree, stitch_length, starting_point):
|
||||
"""Fill a shape with a single spiral going from outside to center."""
|
||||
return _spiral_fill(tree, stitch_length, starting_point, _make_spiral)
|
||||
|
||||
|
||||
def double_spiral(tree, stitch_length, starting_point):
|
||||
"""Fill a shape with a double spiral going from outside to center and back to outside. """
|
||||
return _spiral_fill(tree, stitch_length, starting_point, _make_fermat_spiral)
|
||||
|
||||
|
||||
def _spiral_fill(tree, stitch_length, close_point, spiral_maker):
|
||||
if not _check_and_prepare_tree_for_valid_spiral(tree):
|
||||
raise ValueError(_("Shape cannot be filled with single or double spiral!"))
|
||||
|
||||
starting_point = close_point.coords[0]
|
||||
rings = [tree.nodes[node].val for node in nx.dfs_preorder_nodes(tree, 'root')]
|
||||
path = spiral_maker(rings, stitch_length, starting_point)
|
||||
path = [Stitch(*stitch) for stitch in path]
|
||||
|
||||
return running_stitch(path, stitch_length)
|
||||
|
||||
|
||||
def _make_fermat_spiral(rings, stitch_length, starting_point):
|
||||
forward = _make_spiral(rings[::2], stitch_length, starting_point)
|
||||
back = _make_spiral(rings[1::2], stitch_length, starting_point)
|
||||
back.reverse()
|
||||
|
||||
return chain(forward, back)
|
||||
|
||||
|
||||
def _make_spiral(rings, stitch_length, starting_point):
|
||||
path = []
|
||||
|
||||
for ring1, ring2 in zip(rings[:-1], rings[1:]):
|
||||
spiral_part = _interpolate_linear_rings(ring1, ring2, stitch_length, starting_point)
|
||||
path.extend(spiral_part.coords)
|
||||
|
||||
return path
|
|
@ -1,279 +0,0 @@
|
|||
from enum import IntEnum
|
||||
|
||||
import networkx as nx
|
||||
from shapely.geometry import Polygon, MultiPolygon, GeometryCollection
|
||||
from shapely.geometry.polygon import orient
|
||||
from shapely.ops import polygonize
|
||||
|
||||
from .running_stitch import running_stitch
|
||||
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, ensure_geometry_collection, ensure_multi_polygon
|
||||
|
||||
|
||||
class Tree(nx.DiGraph):
|
||||
# This lets us do tree.nodes['somenode'].parent instead of the default
|
||||
# tree.nodes['somenode']['parent'].
|
||||
node_attr_dict_factory = DotDict
|
||||
|
||||
|
||||
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 = ensure_multi_polygon(result)
|
||||
|
||||
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):
|
||||
"""
|
||||
Removes all geometries which do not form a "valid" LinearRing
|
||||
(meaning a ring which does not form a straight line)
|
||||
"""
|
||||
|
||||
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):
|
||||
# Unfortunately for us, Inkscape SVGs have an inverted Y coordinate.
|
||||
# Normally we don't have to care about that, but in this very specific
|
||||
# case, the meaning of is_ccw is flipped. It actually tests whether
|
||||
# a ring is clockwise. That makes this logic super-confusing.
|
||||
if ring.is_ccw != clockwise:
|
||||
return reverse_line_string(ring)
|
||||
else:
|
||||
return ring
|
||||
|
||||
|
||||
def make_tree_uniform(tree, clockwise=True):
|
||||
"""
|
||||
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 tree.nodes.values():
|
||||
node.val = orient_linear_ring(node.val, clockwise)
|
||||
|
||||
|
||||
# Used to define which stitching strategy shall be used
|
||||
class StitchingStrategy(IntEnum):
|
||||
INNER_TO_OUTER = 0
|
||||
SINGLE_SPIRAL = 1
|
||||
DOUBLE_SPIRAL = 2
|
||||
|
||||
|
||||
def check_and_prepare_tree_for_valid_spiral(tree):
|
||||
"""
|
||||
Takes a tree consisting of offsetted curves. If a parent has more than one child we
|
||||
cannot create a spiral. However, to make the routine more robust, we allow more than
|
||||
one child if only one of the childs has own childs. The other childs are removed in this
|
||||
routine then. If the routine returns true, the tree will have been cleaned up from unwanted
|
||||
childs. If the routine returns false even under the mentioned weaker conditions the
|
||||
tree cannot be connected by one spiral.
|
||||
"""
|
||||
|
||||
def process_node(node):
|
||||
children = set(tree[node])
|
||||
|
||||
if len(children) == 0:
|
||||
return True
|
||||
elif len(children) == 1:
|
||||
child = children.pop()
|
||||
return process_node(child)
|
||||
else:
|
||||
children_with_children = {child for child in children if tree[child]}
|
||||
if len(children_with_children) > 1:
|
||||
# Node has multiple children with children, so a perfect spiral is not possible.
|
||||
# This False value will be returned all the way up the stack.
|
||||
return False
|
||||
elif len(children_with_children) == 1:
|
||||
children_without_children = children - children_with_children
|
||||
child = children_with_children.pop()
|
||||
tree.remove_nodes_from(children_without_children)
|
||||
return process_node(child)
|
||||
else:
|
||||
# None of the children has its own children, so we'll just take the longest.
|
||||
longest = max(children, key=lambda child: tree[child]['val'].length)
|
||||
shorter_children = children - {longest}
|
||||
tree.remove_nodes_from(shorter_children)
|
||||
return process_node(longest)
|
||||
|
||||
return process_node('root')
|
||||
|
||||
|
||||
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.
|
||||
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
|
||||
-min_stitch_distance stitches within a row shall be at least min_stitch_distance apart. Stitches connecting
|
||||
offsetted paths might be shorter.
|
||||
-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. In
|
||||
addition it offers the option "SPIRAL" which creates a real spiral towards inner.
|
||||
In contrast to the other two options, "SPIRAL" does not end at the starting point
|
||||
but at the innermost point
|
||||
-starting_point: Defines the starting point for the stitching
|
||||
-avoid_self_crossing: don't let the path cross itself when using the Inner to Outer strategy
|
||||
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)
|
||||
tree = Tree()
|
||||
tree.add_node('root', type='node', parent=None, val=ordered_poly.exterior)
|
||||
active_polys = ['root']
|
||||
active_holes = [[]]
|
||||
|
||||
# We don't care about the names of the nodes, we just need them to be unique.
|
||||
node_num = 0
|
||||
|
||||
for hole in ordered_poly.interiors:
|
||||
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()
|
||||
outer, inners = offset_polygon_and_holes(tree, current_poly, current_holes, offset, join_style)
|
||||
|
||||
if not outer.is_empty:
|
||||
polygons = match_polygons_and_holes(outer, inners)
|
||||
|
||||
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)
|
||||
|
||||
if new_polygon:
|
||||
active_polys.append(new_polygon)
|
||||
active_holes.append(new_holes)
|
||||
|
||||
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 not tree.nodes[previous_hole].parent:
|
||||
tree.nodes[previous_hole].parent = current_poly
|
||||
tree.add_edge(current_poly, previous_hole)
|
||||
|
||||
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', offset, stitch_distance, starting_point, avoid_self_crossing)
|
||||
path = [Stitch(*point) for point in connected_line.coords]
|
||||
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 = 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 = 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
|
|
@ -1,339 +0,0 @@
|
|||
from collections import namedtuple
|
||||
from itertools import chain
|
||||
import networkx as nx
|
||||
import numpy as np
|
||||
import trimesh
|
||||
from shapely.geometry import Point, LineString, LinearRing, MultiLineString
|
||||
from shapely.ops import nearest_points
|
||||
|
||||
from .running_stitch import running_stitch
|
||||
|
||||
from ..debug import debug
|
||||
from ..stitches import constants
|
||||
from ..stitch_plan import Stitch
|
||||
from ..utils.geometry import cut, roll_linear_ring, reverse_line_string
|
||||
|
||||
nearest_neighbor_tuple = namedtuple(
|
||||
"nearest_neighbor_tuple",
|
||||
[
|
||||
"nearest_point_parent",
|
||||
"nearest_point_child",
|
||||
"proj_distance_parent",
|
||||
"child_node",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def get_nearest_points_closer_than_thresh(travel_line, next_line, threshold):
|
||||
"""
|
||||
Find the first point along travel_line that is within threshold of next_line.
|
||||
|
||||
Input:
|
||||
-travel_line: The "parent" line for which the distance should
|
||||
be minimized to enter next_line
|
||||
-next_line: contains the next_line which need to be entered
|
||||
-threshold: The distance between travel_line and next_line needs
|
||||
to below threshold to be a valid point for entering
|
||||
|
||||
Output:
|
||||
-tuple or None
|
||||
- the tuple structure is:
|
||||
(nearest point in travel_line, nearest point in next_line)
|
||||
- None is returned if there is no point that satisfies the threshold.
|
||||
"""
|
||||
|
||||
# We'll buffer next_line and find the intersection with travel_line.
|
||||
# Then we'll return the very first point in the intersection,
|
||||
# matched with a corresponding point on next_line. Fortunately for
|
||||
# us, intersection of a Polygon with a LineString yields pieces of
|
||||
# the LineString in the same order as the input LineString.
|
||||
threshold_area = next_line.buffer(threshold)
|
||||
portion_within_threshold = travel_line.intersection(threshold_area)
|
||||
|
||||
if portion_within_threshold.is_empty:
|
||||
return None
|
||||
else:
|
||||
if isinstance(portion_within_threshold, MultiLineString):
|
||||
portion_within_threshold = portion_within_threshold.geoms[0]
|
||||
|
||||
parent_point = Point(portion_within_threshold.coords[0])
|
||||
return nearest_points(parent_point, next_line)
|
||||
|
||||
|
||||
def create_nearest_points_list(
|
||||
travel_line, tree, children_list, threshold, threshold_hard):
|
||||
"""
|
||||
Takes a line and calculates the nearest distance along this line to
|
||||
enter the childs in children_list
|
||||
The method calculates the distances along the line and along the
|
||||
reversed line to find the best direction which minimizes the overall
|
||||
distance for all childs.
|
||||
Input:
|
||||
-travel_line: The "parent" line for which the distance should
|
||||
be minimized to enter the childs
|
||||
-children_list: contains the childs of travel_line which need to be entered
|
||||
-threshold: The distance between travel_line and a child needs to be
|
||||
below threshold to be a valid point for entering
|
||||
-preferred_direction: Put a bias on the desired travel direction along
|
||||
travel_line. If equals zero no bias is applied.
|
||||
preferred_direction=1 means we prefer the direction of travel_line;
|
||||
preferred_direction=-1 means we prefer the opposite direction.
|
||||
Output:
|
||||
-stitching direction for travel_line
|
||||
-list of tuples (one tuple per child). The tuple structure is:
|
||||
((nearest point in travel_line, nearest point in child),
|
||||
distance along travel_line, belonging child)
|
||||
"""
|
||||
|
||||
children_nearest_points = []
|
||||
|
||||
for child in children_list:
|
||||
result = get_nearest_points_closer_than_thresh(travel_line, tree.nodes[child].val, threshold)
|
||||
if result is None:
|
||||
# where holes meet outer borders a distance
|
||||
# up to 2 * used offset can arise
|
||||
result = get_nearest_points_closer_than_thresh(travel_line, tree.nodes[child].val, threshold_hard)
|
||||
|
||||
proj = travel_line.project(result[0])
|
||||
children_nearest_points.append(
|
||||
nearest_neighbor_tuple(
|
||||
nearest_point_parent=result[0],
|
||||
nearest_point_child=result[1],
|
||||
proj_distance_parent=proj,
|
||||
child_node=child,
|
||||
)
|
||||
)
|
||||
|
||||
return children_nearest_points
|
||||
|
||||
|
||||
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
|
||||
reach the innermost child as fast as possible in order to stitch afterwards
|
||||
from inner to outer.
|
||||
Input:
|
||||
-tree: contains the offsetted curves in a hierachical organized
|
||||
data structure.
|
||||
-used_offset: used offset when the offsetted curves were generated
|
||||
-stitch_distance: maximum allowed distance between two points
|
||||
after sampling
|
||||
-min_stitch_distance stitches within a row shall be at least min_stitch_distance apart. Stitches connecting
|
||||
offsetted paths might be shorter.
|
||||
-close_point: defines the beginning point for stitching
|
||||
(stitching starts always from the undisplaced curve)
|
||||
-offset_by_half: If true the resulting points are interlaced otherwise not.
|
||||
Returnvalues:
|
||||
-All offsetted curves connected to one line and sampled with points obeying
|
||||
stitch_distance and offset_by_half
|
||||
-Tag (origin) of each point to analyze why a point was placed
|
||||
at this position
|
||||
"""
|
||||
|
||||
current_node = tree.nodes[node]
|
||||
current_ring = current_node.val
|
||||
|
||||
if not forward and avoid_self_crossing:
|
||||
current_ring = reverse_line_string(current_ring)
|
||||
|
||||
# reorder the coordinates of this ring so that it starts with
|
||||
# a point nearest the starting_point
|
||||
start_distance = current_ring.project(starting_point)
|
||||
current_ring = roll_linear_ring(current_ring, start_distance)
|
||||
current_node.val = current_ring
|
||||
|
||||
# Find where along this ring to connect to each child.
|
||||
nearest_points_list = create_nearest_points_list(
|
||||
current_ring,
|
||||
tree,
|
||||
tree[node],
|
||||
constants.offset_factor_for_adjacent_geometry * offset,
|
||||
2.05 * offset
|
||||
)
|
||||
nearest_points_list.sort(key=lambda tup: tup.proj_distance_parent)
|
||||
|
||||
result_coords = []
|
||||
if not nearest_points_list:
|
||||
# We have no children, so we're at the center of a spiral. Reversing
|
||||
# the ring gives a nicer visual appearance.
|
||||
# current_ring = reverse_line_string(current_ring)
|
||||
pass
|
||||
else:
|
||||
# This is a recursive algorithm. We'll stitch along this ring, pausing
|
||||
# to jump to each child ring in turn and sew it before continuing on
|
||||
# this ring. We'll end back where we started.
|
||||
|
||||
result_coords.append(current_ring.coords[0])
|
||||
distance_so_far = 0
|
||||
for child_connection in nearest_points_list:
|
||||
# Cut this ring into pieces before and after where this child will connect.
|
||||
before, after = cut(current_ring, child_connection.proj_distance_parent - distance_so_far)
|
||||
distance_so_far += child_connection.proj_distance_parent
|
||||
|
||||
# Stitch the part leading up to this child.
|
||||
if before is not None:
|
||||
result_coords.extend(before.coords)
|
||||
|
||||
# Stitch this child. The child will start and end in the same
|
||||
# place, which should be close to our current location.
|
||||
child_path = connect_raster_tree_from_inner_to_outer(
|
||||
tree,
|
||||
child_connection.child_node,
|
||||
offset,
|
||||
stitch_distance,
|
||||
child_connection.nearest_point_child,
|
||||
avoid_self_crossing,
|
||||
not forward
|
||||
)
|
||||
result_coords.extend(child_path.coords)
|
||||
|
||||
# Skip ahead a little bit on this ring before resuming. This
|
||||
# gives a nice spiral pattern, where we spiral out from the
|
||||
# innermost child.
|
||||
if after is not None:
|
||||
skip, after = cut(after, offset)
|
||||
distance_so_far += offset
|
||||
|
||||
current_ring = after
|
||||
|
||||
if current_ring is not None:
|
||||
# skip a little at the end so we don't end exactly where we started.
|
||||
remaining_length = current_ring.length
|
||||
if remaining_length > offset:
|
||||
current_ring, skip = cut(current_ring, current_ring.length - offset)
|
||||
|
||||
result_coords.extend(current_ring.coords)
|
||||
|
||||
return LineString(result_coords)
|
||||
|
||||
|
||||
def reorder_linear_ring(ring, start):
|
||||
distances = ring - start
|
||||
start_index = np.argmin(np.linalg.norm(distances, axis=1))
|
||||
return np.roll(ring, -start_index, axis=0)
|
||||
|
||||
|
||||
def interpolate_linear_rings(ring1, ring2, max_stitch_length, start=None):
|
||||
"""
|
||||
Interpolate between two LinearRings
|
||||
|
||||
Creates a path from start_point on ring1 and around the rings, ending at a
|
||||
nearby point on ring2. The path will smoothly transition from ring1 to
|
||||
ring2 as it travels around the rings.
|
||||
|
||||
Inspired by interpolate() from https://github.com/mikedh/pocketing/blob/master/pocketing/polygons.py
|
||||
|
||||
Arguments:
|
||||
ring1 -- LinearRing start point will lie on
|
||||
ring2 -- LinearRing end point will lie on
|
||||
max_stitch_length -- maximum stitch length (used to calculate resampling accuracy)
|
||||
start -- Point on ring1 to start at, as a tuple
|
||||
|
||||
Return value: Path interpolated between two LinearRings, as a LineString.
|
||||
"""
|
||||
|
||||
# Resample the two LinearRings so that they are the same number of points
|
||||
# long. Then take the corresponding points in each ring and interpolate
|
||||
# between them, gradually going more toward ring2.
|
||||
#
|
||||
# This is a little less accurate than the method in interpolate(), but several
|
||||
# orders of magnitude faster because we're not building and querying a KDTree.
|
||||
|
||||
num_points = int(20 * ring1.length / max_stitch_length)
|
||||
ring1_resampled = trimesh.path.traversal.resample_path(np.array(ring1.coords), count=num_points)
|
||||
ring2_resampled = trimesh.path.traversal.resample_path(np.array(ring2.coords), count=num_points)
|
||||
|
||||
if start is not None:
|
||||
ring1_resampled = reorder_linear_ring(ring1_resampled, start)
|
||||
ring2_resampled = reorder_linear_ring(ring2_resampled, start)
|
||||
|
||||
weights = np.linspace(0.0, 1.0, num_points).reshape((-1, 1))
|
||||
points = (ring1_resampled * (1.0 - weights)) + (ring2_resampled * weights)
|
||||
result = LineString(points)
|
||||
|
||||
# TODO: remove when rastering is cheaper
|
||||
return result.simplify(constants.simplification_threshold, False)
|
||||
|
||||
|
||||
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
|
||||
Input:
|
||||
-tree: contains the offsetted curves in a hierarchical organized
|
||||
data structure.
|
||||
-used_offset: used offset when the offsetted curves were generated
|
||||
-stitch_distance: maximum allowed distance between two points
|
||||
after sampling
|
||||
-min_stitch_distance stitches within a row shall be at least min_stitch_distance apart. Stitches connecting
|
||||
offsetted paths might be shorter.
|
||||
-close_point: defines the beginning point for stitching
|
||||
(stitching starts always from the undisplaced curve)
|
||||
-offset_by_half: If true the resulting points are interlaced otherwise not.
|
||||
Return values:
|
||||
-All offsetted curves connected to one spiral and sampled with
|
||||
points obeying stitch_distance and offset_by_half
|
||||
-Tag (origin) of each point to analyze why a point was
|
||||
placed at this position
|
||||
"""
|
||||
|
||||
starting_point = close_point.coords[0]
|
||||
|
||||
rings = [tree.nodes[node].val for node in nx.dfs_preorder_nodes(tree, 'root')]
|
||||
|
||||
path = make_spiral(rings, stitch_distance, starting_point)
|
||||
path = [Stitch(*stitch) for stitch in path]
|
||||
|
||||
return running_stitch(path, stitch_distance)
|
||||
|
||||
|
||||
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
|
||||
Input:
|
||||
-tree: contains the offsetted curves in a hierarchical organized
|
||||
data structure.
|
||||
-used_offset: used offset when the offsetted curves were generated
|
||||
-stitch_distance: maximum allowed distance between two points
|
||||
after sampling
|
||||
-min_stitch_distance stitches within a row shall be at least min_stitch_distance apart. Stitches connecting
|
||||
offsetted paths might be shorter.
|
||||
-close_point: defines the beginning point for stitching
|
||||
(stitching starts always from the undisplaced curve)
|
||||
-offset_by_half: If true the resulting points are interlaced otherwise not.
|
||||
Return values:
|
||||
-All offsetted curves connected to one spiral and sampled with
|
||||
points obeying stitch_distance and offset_by_half
|
||||
-Tag (origin) of each point to analyze why a point was
|
||||
placed at this position
|
||||
"""
|
||||
|
||||
starting_point = close_point.coords[0]
|
||||
|
||||
rings = [tree.nodes[node].val for node in nx.dfs_preorder_nodes(tree, 'root')]
|
||||
|
||||
path = make_fermat_spiral(rings, stitch_distance, starting_point)
|
||||
path = [Stitch(*stitch) for stitch in path]
|
||||
|
||||
return running_stitch(path, stitch_distance)
|
||||
|
||||
|
||||
def make_fermat_spiral(rings, stitch_distance, starting_point):
|
||||
forward = make_spiral(rings[::2], stitch_distance, starting_point)
|
||||
back = make_spiral(rings[1::2], stitch_distance, starting_point)
|
||||
back.reverse()
|
||||
|
||||
return chain(forward, back)
|
||||
|
||||
|
||||
def make_spiral(rings, stitch_distance, starting_point):
|
||||
path = []
|
||||
|
||||
for ring1, ring2 in zip(rings[:-1], rings[1:]):
|
||||
spiral_part = interpolate_linear_rings(ring1, ring2, stitch_distance, starting_point)
|
||||
path.extend(spiral_part.coords)
|
||||
|
||||
return path
|
|
@ -19,7 +19,6 @@ stringcase
|
|||
tinycss2
|
||||
flask
|
||||
fonttools
|
||||
depq
|
||||
trimesh
|
||||
scipy==1.7.3
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue