kopia lustrzana https://github.com/inkstitch/inkstitch
				
				
				
			
		
			
				
	
	
		
			351 wiersze
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			351 wiersze
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
# Authors: see git history
 | 
						|
#
 | 
						|
# Copyright (c) 2023 Authors
 | 
						|
# Licensed under the GNU GPL version 3.0 or later.  See the file LICENSE for details.
 | 
						|
 | 
						|
from math import ceil, floor, sqrt
 | 
						|
 | 
						|
import numpy as np
 | 
						|
from inkex import DirectedLineSegment, Transform
 | 
						|
from networkx import is_empty
 | 
						|
from shapely import segmentize
 | 
						|
from shapely.affinity import rotate
 | 
						|
from shapely.geometry import LineString, MultiLineString, Point, Polygon
 | 
						|
 | 
						|
from ..stitch_plan import StitchGroup
 | 
						|
from ..svg import get_node_transform
 | 
						|
from ..utils.geometry import ensure_multi_line_string
 | 
						|
from ..utils.threading import check_stop_flag
 | 
						|
from .auto_fill import (build_fill_stitch_graph, build_travel_graph,
 | 
						|
                        find_stitch_path, graph_make_valid)
 | 
						|
from .circular_fill import path_to_stitches
 | 
						|
from .guided_fill import apply_stitches
 | 
						|
 | 
						|
 | 
						|
def linear_gradient_fill(fill, shape, starting_point, ending_point):
 | 
						|
    lines, colors, stop_color_line_indices = _get_lines_and_colors(shape, fill)
 | 
						|
    color_lines, colors = _get_color_lines(lines, colors, stop_color_line_indices)
 | 
						|
    stitch_groups = _get_stitch_groups(fill, shape, colors, color_lines, starting_point, ending_point)
 | 
						|
    return stitch_groups
 | 
						|
 | 
						|
 | 
						|
def _get_lines_and_colors(shape, fill):
 | 
						|
    '''
 | 
						|
    Returns lines and color gradient information
 | 
						|
    lines:  a list of lines which cover the whole shape in a 90° angle to the gradient line
 | 
						|
    colors: a list of color values
 | 
						|
    stop_color_line_indices: line indices indicating where color changes are positioned at
 | 
						|
    '''
 | 
						|
    orig_bbox = shape.bounds
 | 
						|
 | 
						|
    # get angle, colors, as well as start and stop position of the gradient
 | 
						|
    angle, colors, offsets, gradient_start, gradient_end = _get_gradient_info(fill, orig_bbox)
 | 
						|
 | 
						|
    # get lines
 | 
						|
    lines, bottom_line = _get_lines(fill, shape, orig_bbox, angle)
 | 
						|
 | 
						|
    gradient_start_line_index = round(bottom_line.project(gradient_start) / fill.row_spacing)
 | 
						|
    if gradient_start_line_index == 0:
 | 
						|
        gradient_start_line_index = -round(LineString([gradient_start, gradient_end]).project(Point(bottom_line.coords[0])) / fill.row_spacing)
 | 
						|
    stop_color_line_indices = [gradient_start_line_index]
 | 
						|
    gradient_line = LineString([gradient_start, gradient_end])
 | 
						|
    for offset in offsets[1:]:
 | 
						|
        stop_color_line_indices.append(round((gradient_line.length * offset) / fill.row_spacing) + gradient_start_line_index)
 | 
						|
 | 
						|
    return lines, colors, stop_color_line_indices
 | 
						|
 | 
						|
 | 
						|
def _get_gradient_info(fill, bbox):
 | 
						|
    if fill.gradient is None:
 | 
						|
        # there is no linear gradient, let's simply space out one single color instead
 | 
						|
        angle = fill.angle
 | 
						|
        offsets = [0, 1]
 | 
						|
        colors = [fill.color, 'none']
 | 
						|
        gradient_start = (bbox[0], bbox[1])
 | 
						|
        gradient_end = (bbox[2], bbox[3])
 | 
						|
    else:
 | 
						|
        fill.gradient.apply_transform()
 | 
						|
        offsets = fill.gradient.stop_offsets
 | 
						|
        # get stop colors
 | 
						|
        # it would be easier if we just used fill.gradient.stop_styles to collect them
 | 
						|
        # but inkex/tinycss fails on stop color styles when it is not in the style attribute, but in it's own stop-color attribute
 | 
						|
        colors = []
 | 
						|
        for stop in fill.gradient.stops:
 | 
						|
            color = stop.get_computed_style('stop-color')
 | 
						|
            opacity = stop.get_computed_style('stop-opacity')
 | 
						|
            if float(opacity) > 0:
 | 
						|
                colors.append(color)
 | 
						|
            else:
 | 
						|
                colors.append('none')
 | 
						|
        gradient_start, gradient_end = gradient_start_end(fill.node, fill.gradient)
 | 
						|
        angle = gradient_angle(fill.node, fill.gradient)
 | 
						|
    return angle, colors, offsets, Point(list(gradient_start)), Point(list(gradient_end))
 | 
						|
 | 
						|
 | 
						|
def _get_lines(fill, shape, bounding_box, angle):
 | 
						|
    '''
 | 
						|
    To generate the lines we rotate the bounding box to bring the angle in vertical position.
 | 
						|
    From bounds we create a Polygon which we then rotate back, so we receive a rotated bounding box
 | 
						|
    which aligns well to the stitch angle. Combining the points of the subdivided top and bottom line
 | 
						|
    will finally deliver to our stitch rows
 | 
						|
    '''
 | 
						|
 | 
						|
    # get the rotated bounding box for the shape
 | 
						|
    rotated_shape = rotate(shape, -angle, origin=(bounding_box[0], bounding_box[3]), use_radians=True)
 | 
						|
    bounds = rotated_shape.bounds
 | 
						|
 | 
						|
    # Generate a Polygon from the rotated bounding box which we then rotate back into original position
 | 
						|
    # extend bounding box for lines just a little to make sure we cover the whole area with lines
 | 
						|
    # this avoids rounding errors due to the rotation later on
 | 
						|
    rot_bbox = Polygon([
 | 
						|
        (bounds[0] - fill.max_stitch_length, bounds[1] - fill.row_spacing),
 | 
						|
        (bounds[2] + fill.max_stitch_length, bounds[1] - fill.row_spacing),
 | 
						|
        (bounds[2] + fill.max_stitch_length, bounds[3] + fill.row_spacing),
 | 
						|
        (bounds[0] - fill.max_stitch_length, bounds[3] + fill.row_spacing)
 | 
						|
    ])
 | 
						|
    # and rotate it back into original position
 | 
						|
    rot_bbox = list(rotate(rot_bbox, angle, origin=(bounding_box[0], bounding_box[3]), use_radians=True).exterior.coords)
 | 
						|
 | 
						|
    # segmentize top and bottom line to finally be ableto generate the stitch lines
 | 
						|
    top_line = LineString([rot_bbox[0], rot_bbox[1]])
 | 
						|
    top = segmentize(top_line, max_segment_length=fill.row_spacing)
 | 
						|
 | 
						|
    bottom_line = LineString([rot_bbox[3], rot_bbox[2]])
 | 
						|
    bottom = segmentize(bottom_line, max_segment_length=fill.row_spacing)
 | 
						|
 | 
						|
    lines = list(zip(top.coords, bottom.coords))
 | 
						|
 | 
						|
    # stagger stitched lines according to user settings
 | 
						|
    staggered_lines = []
 | 
						|
    for i, line in enumerate(lines):
 | 
						|
        staggered_line = apply_stitches(LineString(line), fill.max_stitch_length, fill.staggers, fill.row_spacing, i)
 | 
						|
        staggered_lines.append(staggered_line)
 | 
						|
    return staggered_lines, bottom_line
 | 
						|
 | 
						|
 | 
						|
def _get_color_lines(lines, colors, stop_color_line_indices):
 | 
						|
    '''
 | 
						|
    To define which line will be stitched in which color, we will loop through the color sections
 | 
						|
    defined by the stop positions of the gradient (stop_color_line_indices).
 | 
						|
    Each section will then be subdivided into smaller sections using the square root of the total line number
 | 
						|
    of the whole section. Lines left over from this operation will be added step by step to the smaller sub-sections.
 | 
						|
    Since we do this symmetrically we may end one line short, which we an add at the end.
 | 
						|
 | 
						|
    Now we define the line colors of the first half of our color section, we will later mirror this on the second half.
 | 
						|
    Therefor we use one additional line of color2 in each sub-section and position them as evenly as possible between the color1 lines.
 | 
						|
    Doing this we take care, that the number of consecutive lines of color1 is always decreasing.
 | 
						|
 | 
						|
    For example let's take a 12 lines sub-section, with 5 lines of color2.
 | 
						|
    12 / 5 = 2.4
 | 
						|
    12 % 5 = 2
 | 
						|
    This results into the following pattern:
 | 
						|
    xx|xx|x|x|x| (while x = color1 and | = color2).
 | 
						|
    Note that the first two parts have an additional line (as defined by the modulo operation)
 | 
						|
 | 
						|
    Method returns
 | 
						|
    color_lines: A dictionary with lines grouped by color
 | 
						|
    colors:      An updated list of color values.
 | 
						|
                 Colors which are positioned outside the shape will be removed.
 | 
						|
    '''
 | 
						|
 | 
						|
    # create dictionary with a key for each color
 | 
						|
    color_lines = {}
 | 
						|
    for color in colors:
 | 
						|
        color_lines[color] = []
 | 
						|
 | 
						|
    prev_color = colors[0]
 | 
						|
    prev = None
 | 
						|
    for line_index, color in zip(stop_color_line_indices, colors):
 | 
						|
        if prev is None:
 | 
						|
            if line_index > 0:
 | 
						|
                color_lines[color].extend(lines[0:line_index + 1])
 | 
						|
            prev = line_index
 | 
						|
            prev_color = color
 | 
						|
            continue
 | 
						|
        if prev < 0 and line_index < 0:
 | 
						|
            prev = line_index
 | 
						|
            prev_color = color
 | 
						|
            continue
 | 
						|
 | 
						|
        prev += 1
 | 
						|
        line_index += 1
 | 
						|
        total_lines = line_index - prev
 | 
						|
        sections = floor(sqrt(total_lines))
 | 
						|
 | 
						|
        color1 = []
 | 
						|
        color2 = []
 | 
						|
 | 
						|
        c2_count = 0
 | 
						|
        c1_count = 0
 | 
						|
        max_count = 1000
 | 
						|
        current_line = 0
 | 
						|
 | 
						|
        line_count_diff = floor((total_lines - sections**2) / 2)
 | 
						|
 | 
						|
        stop = False
 | 
						|
        for i in range(sections):
 | 
						|
            if stop:
 | 
						|
                break
 | 
						|
 | 
						|
            c2_count += 1
 | 
						|
            c1_count = sections - c2_count
 | 
						|
            rest = c1_count % c2_count
 | 
						|
            c1_count = ceil(c1_count / c2_count)
 | 
						|
 | 
						|
            current_line, line_count_diff, color1, color2, max_count, stop = _add_lines(
 | 
						|
                current_line,
 | 
						|
                total_lines,
 | 
						|
                line_count_diff,
 | 
						|
                color1,
 | 
						|
                color2,
 | 
						|
                stop,
 | 
						|
                rest,
 | 
						|
                c1_count,
 | 
						|
                c2_count,
 | 
						|
                max_count
 | 
						|
            )
 | 
						|
 | 
						|
        # mirror the first half of the color section to receive the full section
 | 
						|
        second_half = color2[-1] * 2 + 1
 | 
						|
 | 
						|
        color1 = np.array(color1)
 | 
						|
        color2 = np.array(color2)
 | 
						|
 | 
						|
        c1 = np.append(color1, second_half - color2)
 | 
						|
        color2 = np.append(color2, second_half - color1)
 | 
						|
        color1 = c1
 | 
						|
 | 
						|
        # until now we only cared about the length of the section
 | 
						|
        # now we need to move it to the correct position
 | 
						|
        color1 += prev
 | 
						|
        color2 += prev
 | 
						|
 | 
						|
        # add lines to their color key in the dictionary
 | 
						|
        # as sections can start before or after the actual shape we need to make sure,
 | 
						|
        # that we only try to add existing lines
 | 
						|
        color_lines[prev_color].extend([lines[x] for x in color1 if 0 < x < len(lines)])
 | 
						|
        color_lines[color].extend([lines[x] for x in color2 if 0 < x < len(lines)])
 | 
						|
 | 
						|
        prev = np.max(color2)
 | 
						|
        prev_color = color
 | 
						|
 | 
						|
        check_stop_flag()
 | 
						|
 | 
						|
    # add left over lines to last color
 | 
						|
    color_lines[color].extend(lines[prev+1:])
 | 
						|
 | 
						|
    # remove transparent colors (we just want a gap)
 | 
						|
    color_lines.pop('none', None)
 | 
						|
 | 
						|
    # remove empty line lists and update colors
 | 
						|
    color_lines = {color: lines for color, lines in color_lines.items() if lines}
 | 
						|
    colors = list(color_lines.keys())
 | 
						|
 | 
						|
    return color_lines, colors
 | 
						|
 | 
						|
 | 
						|
def _add_lines(current_line, total_lines, line_count_diff, color1, color2, stop, rest, c1_count, c2_count, max_count):
 | 
						|
    for j in range(c2_count):
 | 
						|
        if stop:
 | 
						|
            break
 | 
						|
 | 
						|
        if rest == 0 or j < rest:
 | 
						|
            count = c1_count
 | 
						|
        else:
 | 
						|
            count = c1_count - 1
 | 
						|
        if line_count_diff > 0:
 | 
						|
            count += 1
 | 
						|
            line_count_diff -= 1
 | 
						|
        count = min(count, max_count)
 | 
						|
 | 
						|
        for k in range(count):
 | 
						|
            color1.append(current_line)
 | 
						|
            current_line += 1
 | 
						|
            if total_lines / 2 <= current_line + 1:
 | 
						|
                stop = True
 | 
						|
                break
 | 
						|
        color2.append(current_line)
 | 
						|
        current_line += 1
 | 
						|
    max_count = count
 | 
						|
    return current_line, line_count_diff, color1, color2, max_count, stop
 | 
						|
 | 
						|
 | 
						|
def _get_stitch_groups(fill, shape, colors, color_lines, starting_point, ending_point):
 | 
						|
    stitch_groups = []
 | 
						|
    for i, color in enumerate(colors):
 | 
						|
        lines = color_lines[color]
 | 
						|
 | 
						|
        multiline = ensure_multi_line_string(MultiLineString(lines).intersection(shape))
 | 
						|
        if multiline.is_empty:
 | 
						|
            continue
 | 
						|
 | 
						|
        segments = [list(line.coords) for line in multiline.geoms if len(line.coords) > 1]
 | 
						|
        fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
 | 
						|
 | 
						|
        if is_empty(fill_stitch_graph):
 | 
						|
            continue
 | 
						|
        graph_make_valid(fill_stitch_graph)
 | 
						|
 | 
						|
        travel_graph = build_travel_graph(fill_stitch_graph, shape, fill.angle, False)
 | 
						|
        path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point)
 | 
						|
        stitches = path_to_stitches(
 | 
						|
            shape,
 | 
						|
            path,
 | 
						|
            travel_graph,
 | 
						|
            fill_stitch_graph,
 | 
						|
            fill.running_stitch_length,
 | 
						|
            fill.running_stitch_tolerance,
 | 
						|
            fill.skip_last,
 | 
						|
            False  # no underpath
 | 
						|
        )
 | 
						|
 | 
						|
        stitches = remove_start_end_travel(fill, stitches, colors, i)
 | 
						|
 | 
						|
        stitch_groups.append(StitchGroup(
 | 
						|
            color=color,
 | 
						|
            tags=("linear_gradient_fill", "auto_fill_top"),
 | 
						|
            stitches=stitches,
 | 
						|
            force_lock_stitches=fill.force_lock_stitches,
 | 
						|
            lock_stitches=fill.lock_stitches,
 | 
						|
            trim_after=fill.has_command("trim") or fill.trim_after
 | 
						|
        ))
 | 
						|
 | 
						|
    return stitch_groups
 | 
						|
 | 
						|
 | 
						|
def remove_start_end_travel(fill, stitches, colors, color_section):
 | 
						|
    # We can savely remove travel stitches at start since we are changing color all the time
 | 
						|
    # but we do care for the first starting point, it is important when they use an underlay of the same color
 | 
						|
    remove_before = 0
 | 
						|
    if color_section > 0 or not fill.fill_underlay:
 | 
						|
        for stitch in range(len(stitches)-1):
 | 
						|
            if 'auto_fill_travel' not in stitches[stitch].tags:
 | 
						|
                remove_before = stitch
 | 
						|
                break
 | 
						|
        stitches = stitches[remove_before:]
 | 
						|
    remove_after = len(stitches) - 1
 | 
						|
    # We also remove travel stitches at the end. It is optional to the user if the last color block travels
 | 
						|
    # to the defined ending point
 | 
						|
    if color_section < len(colors) - 2 or not fill.stop_at_ending_point:
 | 
						|
        for stitch in range(remove_after, 0, -1):
 | 
						|
            if 'auto_fill_travel' not in stitches[stitch].tags:
 | 
						|
                remove_after = stitch + 1
 | 
						|
                break
 | 
						|
        stitches = stitches[:remove_after]
 | 
						|
    return stitches
 | 
						|
 | 
						|
 | 
						|
def gradient_start_end(node, gradient):
 | 
						|
    transform = Transform(get_node_transform(node))
 | 
						|
    gradient_start = transform.apply_to_point((float(gradient.x1()), float(gradient.y1())))
 | 
						|
    gradient_end = transform.apply_to_point((float(gradient.x2()), float(gradient.y2())))
 | 
						|
    return gradient_start, gradient_end
 | 
						|
 | 
						|
 | 
						|
def gradient_angle(node, gradient):
 | 
						|
    if gradient is None:
 | 
						|
        return
 | 
						|
    gradient_start, gradient_end = gradient_start_end(node, gradient)
 | 
						|
    gradient_line = DirectedLineSegment(gradient_start, gradient_end)
 | 
						|
    return gradient_line.angle
 |