kopia lustrzana https://github.com/inkstitch/inkstitch
				
				
				
			
		
			
				
	
	
		
			314 wiersze
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			314 wiersze
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
import math
 | 
						|
from collections import namedtuple
 | 
						|
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",
 | 
						|
    ],
 | 
						|
)
 | 
						|
 | 
						|
 | 
						|
@debug.time
 | 
						|
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
 | 
						|
 | 
						|
 | 
						|
@debug.time
 | 
						|
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):
 | 
						|
    """
 | 
						|
    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,
 | 
						|
                min_stitch_distance,
 | 
						|
                child_connection.nearest_point_child,
 | 
						|
                offset_by_half,
 | 
						|
                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 orient_linear_ring(ring):
 | 
						|
    if not ring.is_ccw:
 | 
						|
        return LinearRing(reversed(ring.coords))
 | 
						|
    else:
 | 
						|
        return ring
 | 
						|
 | 
						|
 | 
						|
def reorder_linear_ring(ring, start):
 | 
						|
    # TODO: actually use start?
 | 
						|
    start_index = np.argmin(np.linalg.norm(ring, 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.
 | 
						|
    """
 | 
						|
 | 
						|
    ring1 = orient_linear_ring(ring1)
 | 
						|
    ring2 = orient_linear_ring(ring2)
 | 
						|
 | 
						|
    # 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_spiral(tree, used_offset, stitch_distance, min_stitch_distance, close_point, offset_by_half):  # 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
 | 
						|
    """
 | 
						|
 | 
						|
    if not tree['root']:  # if node has no children
 | 
						|
        stitches = [Stitch(*point) for point in tree.nodes['root'].val.coords]
 | 
						|
        return running_stitch(stitches, stitch_distance)
 | 
						|
 | 
						|
    starting_point = close_point.coords[0]
 | 
						|
    path = []
 | 
						|
    for node in nx.dfs_preorder_nodes(tree, 'root'):
 | 
						|
        if tree[node]:
 | 
						|
            ring1 = tree.nodes[node].val
 | 
						|
            child = list(tree.successors(node))[0]
 | 
						|
            ring2 = tree.nodes[child].val
 | 
						|
 | 
						|
            spiral_part = interpolate_linear_rings(ring1, ring2, stitch_distance, starting_point)
 | 
						|
            path.extend(spiral_part.coords)
 | 
						|
 | 
						|
    path = [Stitch(*stitch) for stitch in path]
 | 
						|
 | 
						|
    return running_stitch(path, stitch_distance), None
 |