inkstitch/lib/stitches/tangential_fill_stitch_patt...

340 wiersze
14 KiB
Python

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