kopia lustrzana https://github.com/inkstitch/inkstitch
826 wiersze
31 KiB
Python
826 wiersze
31 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.
|
|
|
|
# This file needs some more love before it'll pass type checking.
|
|
# mypy: ignore-errors=true
|
|
|
|
from collections import defaultdict
|
|
from itertools import chain
|
|
from math import cos, radians, sin
|
|
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
|
|
|
from networkx import is_empty
|
|
from shapely import get_point, line_merge, minimum_bounding_radius, segmentize
|
|
from shapely.affinity import rotate, scale, translate
|
|
from shapely.geometry import LineString, MultiLineString, Point, Polygon
|
|
from shapely.ops import nearest_points
|
|
|
|
from ..stitch_plan import Stitch, StitchGroup
|
|
from ..svg import PIXELS_PER_MM
|
|
from ..tartan.utils import (get_palette_width, get_tartan_settings,
|
|
get_tartan_stripes, sort_fills_and_strokes,
|
|
stripes_to_shapes)
|
|
from ..utils import cache, 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
|
|
from .linear_gradient_fill import remove_start_end_travel
|
|
from .running_stitch import bean_stitch
|
|
|
|
if TYPE_CHECKING:
|
|
from ..elements import FillStitch
|
|
|
|
|
|
def tartan_fill(fill: 'FillStitch', outline: Polygon, starting_point: Union[tuple, Stitch, None], ending_point: Union[tuple, Stitch, None]):
|
|
"""
|
|
Main method to fill the tartan element with tartan fill stitches
|
|
|
|
:param fill: FillStitch element
|
|
:param outline: the outline of the fill
|
|
:param starting_point: the starting point (or None)
|
|
:param ending_point: the ending point (or None)
|
|
:returns: stitch_groups forming the tartan pattern
|
|
"""
|
|
tartan_settings = get_tartan_settings(fill.node)
|
|
warp, weft = get_tartan_stripes(tartan_settings)
|
|
warp_width = get_palette_width(tartan_settings)
|
|
weft_width = get_palette_width(tartan_settings, 1)
|
|
|
|
offset = (abs(tartan_settings['offset_x']), abs(tartan_settings['offset_y']))
|
|
rotation = tartan_settings['rotate']
|
|
dimensions = _get_dimensions(fill, outline, offset, warp_width, weft_width)
|
|
rotation_center = _get_rotation_center(outline)
|
|
|
|
warp_shapes = stripes_to_shapes(
|
|
warp,
|
|
dimensions,
|
|
outline,
|
|
rotation,
|
|
rotation_center,
|
|
tartan_settings['symmetry'],
|
|
tartan_settings['scale'],
|
|
tartan_settings['min_stripe_width'],
|
|
False, # weft
|
|
False # do not cut polygons just yet
|
|
)
|
|
|
|
weft_shapes = stripes_to_shapes(
|
|
weft,
|
|
dimensions,
|
|
outline,
|
|
rotation,
|
|
rotation_center,
|
|
tartan_settings['symmetry'],
|
|
tartan_settings['scale'],
|
|
tartan_settings['min_stripe_width'],
|
|
True, # weft
|
|
False # do not cut polygons just yet
|
|
)
|
|
|
|
if fill.herringbone_width > 0:
|
|
lines = _generate_herringbone_lines(outline, fill, dimensions, rotation)
|
|
warp_lines, weft_lines = _split_herringbone_warp_weft(lines, fill.rows_per_thread, fill.running_stitch_length)
|
|
warp_color_lines = _get_herringbone_color_segments(warp_lines, warp_shapes, outline, rotation, fill.running_stitch_length)
|
|
weft_color_lines = _get_herringbone_color_segments(weft_lines, weft_shapes, outline, rotation, fill.running_stitch_length, True)
|
|
else:
|
|
lines = _generate_tartan_lines(outline, fill, dimensions, rotation)
|
|
warp_lines, weft_lines = _split_warp_weft(lines, fill.rows_per_thread)
|
|
warp_color_lines = _get_tartan_color_segments(warp_lines, warp_shapes, outline, rotation, fill.running_stitch_length)
|
|
weft_color_lines = _get_tartan_color_segments(weft_lines, weft_shapes, outline, rotation, fill.running_stitch_length, True)
|
|
if not lines:
|
|
return []
|
|
|
|
warp_color_runs = _get_color_runs(warp_shapes, fill.running_stitch_length)
|
|
weft_color_runs = _get_color_runs(weft_shapes, fill.max_stitch_length)
|
|
|
|
color_lines = defaultdict(list)
|
|
for color, lines in chain(warp_color_lines.items(), weft_color_lines.items()):
|
|
color_lines[color].extend(lines)
|
|
|
|
color_runs = defaultdict(list)
|
|
for color, lines in chain(warp_color_runs.items(), weft_color_runs.items()):
|
|
color_runs[color].extend(lines)
|
|
|
|
color_lines, color_runs = sort_fills_and_strokes(color_lines, color_runs)
|
|
|
|
stitch_groups = _get_fill_stitch_groups(fill, outline, color_lines, starting_point, ending_point)
|
|
if stitch_groups and not fill.stop_at_ending_point:
|
|
starting_point = stitch_groups[-1].stitches[-1]
|
|
stitch_groups += _get_run_stitch_groups(fill, outline, color_runs, starting_point, ending_point)
|
|
return stitch_groups
|
|
|
|
|
|
def _generate_herringbone_lines(
|
|
outline: Polygon,
|
|
fill: 'FillStitch',
|
|
dimensions: Tuple[float, float, float, float],
|
|
rotation: float,
|
|
) -> List[List[List[LineString]]]:
|
|
"""
|
|
Generates herringbone lines with staggered stitch positions
|
|
|
|
:param outline: the outline to fill with the herringbone lines
|
|
:param fill: the tartan fill element
|
|
:param dimensions: minx, miny, maxx, maxy
|
|
:param rotation: the rotation value
|
|
:returns: a tuple of two list with herringbone stripes [0] up segments / [1] down segments \
|
|
"""
|
|
rotation_center = _get_rotation_center(outline)
|
|
minx, miny, maxx, maxy = dimensions
|
|
|
|
herringbone_lines: list = [[], []]
|
|
odd = True
|
|
while minx < maxx:
|
|
odd = not odd
|
|
right = minx + fill.herringbone_width
|
|
if odd:
|
|
left_line = LineString([(minx, miny), (minx, maxy + fill.herringbone_width)])
|
|
else:
|
|
left_line = LineString([(minx, miny - fill.herringbone_width), (minx, maxy)])
|
|
|
|
if odd:
|
|
right_line = LineString([(right, miny - fill.herringbone_width), (right, maxy)])
|
|
else:
|
|
right_line = LineString([(right, miny), (right, maxy + fill.herringbone_width)])
|
|
|
|
left_line = segmentize(left_line, max_segment_length=fill.row_spacing)
|
|
right_line = segmentize(right_line, max_segment_length=fill.row_spacing)
|
|
|
|
lines = list(zip(left_line.coords, right_line.coords))
|
|
|
|
staggered_lines = []
|
|
for i, line in enumerate(lines):
|
|
linestring = LineString(line)
|
|
staggered_line = apply_stitches(linestring, fill.max_stitch_length, fill.staggers, fill.row_spacing, i)
|
|
# make sure we do not ommit the very first or very last point (it would confuse our sorting algorithm)
|
|
staggered_line = LineString([linestring.coords[0]] + list(staggered_line.coords) + [linestring.coords[-1]])
|
|
staggered_lines.append(staggered_line)
|
|
|
|
if odd:
|
|
herringbone_lines[0].append(list(rotate(MultiLineString(staggered_lines), rotation, rotation_center).geoms))
|
|
else:
|
|
herringbone_lines[1].append(list(rotate(MultiLineString(staggered_lines), rotation, rotation_center).geoms))
|
|
|
|
# add some little space extra to make things easier with line_merge later on
|
|
# (avoid spots with 4 line points)
|
|
minx += fill.herringbone_width + 0.005
|
|
|
|
return herringbone_lines
|
|
|
|
|
|
def _generate_tartan_lines(
|
|
outline: Polygon,
|
|
fill: 'FillStitch',
|
|
dimensions: Tuple[float, float, float, float],
|
|
rotation: float,
|
|
) -> List[LineString]:
|
|
"""
|
|
Generates tartan lines with staggered stitch positions
|
|
|
|
:param outline: the outline to fill with the herringbone lines
|
|
:param fill: the tartan fill element
|
|
:param dimensions: minx, miny, maxx, maxy
|
|
:param rotation: the rotation value
|
|
:returns: a list with the tartan lines
|
|
"""
|
|
rotation_center = _get_rotation_center(outline)
|
|
# default angle is 45°
|
|
rotation += fill.tartan_angle
|
|
minx, miny, maxx, maxy = dimensions
|
|
|
|
left_line = LineString([(minx, miny), (minx, maxy)])
|
|
left_line = rotate(left_line, rotation, rotation_center)
|
|
left_line = segmentize(left_line, max_segment_length=fill.row_spacing)
|
|
|
|
right_line = LineString([(maxx, miny), (maxx, maxy)])
|
|
right_line = rotate(right_line, rotation, rotation_center)
|
|
right_line = segmentize(right_line, max_segment_length=fill.row_spacing)
|
|
|
|
lines = list(zip(left_line.coords, right_line.coords))
|
|
|
|
staggered_lines = []
|
|
for i, line in enumerate(lines):
|
|
linestring = LineString(line)
|
|
staggered_line = apply_stitches(linestring, fill.max_stitch_length, fill.staggers, fill.row_spacing, i)
|
|
# make sure we do not ommit the very first or very last point (it would confuse our sorting algorithm)
|
|
staggered_line = LineString([linestring.coords[0]] + list(staggered_line.coords) + [linestring.coords[-1]])
|
|
staggered_lines.append(staggered_line)
|
|
return staggered_lines
|
|
|
|
|
|
def _split_herringbone_warp_weft(
|
|
lines: List[List[List[LineString]]],
|
|
rows_per_thread: int,
|
|
stitch_length: float
|
|
) -> tuple:
|
|
"""
|
|
Split the herringbone lines into warp lines and weft lines as defined by rows rows_per_thread
|
|
Merge weft lines for each block.
|
|
|
|
:param lines: lines to divide
|
|
:param rows_per_thread: length of line blocks
|
|
:param stitch_length: maximum stitch length for weft connector lines
|
|
:returns: [0] warp and [1] weft list of MultiLineString objects
|
|
"""
|
|
warp_lines: List[LineString] = []
|
|
weft_lines: List[LineString] = []
|
|
for i, line_blocks in enumerate(lines):
|
|
for line_block in line_blocks:
|
|
if i == 0:
|
|
warp, weft = _split_warp_weft(line_block, rows_per_thread)
|
|
else:
|
|
weft, warp = _split_warp_weft(line_block, rows_per_thread)
|
|
warp_lines.append(warp)
|
|
weft_lines.append(weft)
|
|
|
|
connected_weft = []
|
|
line2 = None
|
|
for multilinestring in weft_lines:
|
|
connected_line_block = []
|
|
geoms = list(multilinestring.geoms)
|
|
for line1, line2 in zip(geoms[:-1], geoms[1:]):
|
|
connected_line_block.append(line1)
|
|
connector_line = LineString([get_point(line1, -1), get_point(line2, 0)])
|
|
connector_line = segmentize(connector_line, max_segment_length=stitch_length)
|
|
connected_line_block.append(connector_line)
|
|
if line2:
|
|
connected_line_block.append(line2)
|
|
connected_weft.append(ensure_multi_line_string(line_merge(MultiLineString(connected_line_block))))
|
|
return warp_lines, connected_weft
|
|
|
|
|
|
def _split_warp_weft(lines: List[LineString], rows_per_thread: int) -> Tuple[List[LineString], List[LineString]]:
|
|
"""
|
|
Divide given lines in warp and weft, sort afterwards
|
|
|
|
:param lines: a list of LineString shapes
|
|
:param rows_per_thread: length of line blocks
|
|
:returns: tuple with sorted [0] warp and [1] weft LineString shapes
|
|
"""
|
|
warp_lines = []
|
|
weft_lines = []
|
|
for i in range(rows_per_thread):
|
|
warp_lines.extend(lines[i::rows_per_thread*2])
|
|
weft_lines.extend(lines[i+rows_per_thread::rows_per_thread*2])
|
|
return _sort_lines(warp_lines), _sort_lines(weft_lines)
|
|
|
|
|
|
def _sort_lines(lines: List[LineString]):
|
|
"""
|
|
Sort given list of LineString shapes by first coordinate
|
|
and reverse every second line
|
|
|
|
:param lines: a list of LineString shapes
|
|
:returns: sorted list of LineString shapes with alternating directions
|
|
"""
|
|
# sort lines
|
|
lines.sort(key=lambda line: line.coords[0])
|
|
# reverse every second line
|
|
lines = [line if i % 2 == 0 else line.reverse() for i, line in enumerate(lines)]
|
|
return MultiLineString(lines)
|
|
|
|
|
|
@cache
|
|
def _get_rotation_center(outline: Polygon) -> Point:
|
|
"""
|
|
Returns the rotation center used for any tartan pattern rotation
|
|
|
|
:param outline: the polygon shape to be filled with the pattern
|
|
:returns: the center point of the shape
|
|
"""
|
|
# somehow outline.centroid doesn't deliver the point we need
|
|
bounds = outline.bounds
|
|
return LineString([(bounds[0], bounds[1]), (bounds[2], bounds[3])]).centroid
|
|
|
|
|
|
@cache
|
|
def _get_dimensions(
|
|
fill: 'FillStitch',
|
|
outline: Polygon,
|
|
offset: Tuple[float, float],
|
|
warp_width: float,
|
|
weft_width: float
|
|
) -> Tuple[float, float, float, float]:
|
|
"""
|
|
Calculates the dimensions for the tartan pattern.
|
|
Make sure it is big enough for pattern rotations, etc.
|
|
|
|
:param fill: the FillStitch element
|
|
:param outline: the shape to be filled with a tartan pattern
|
|
:param offset: mm offset for x, y
|
|
:param warp_width: mm warp width
|
|
:param weft_width: mm weft width
|
|
:returns: a tuple with boundaries (minx, miny, maxx, maxy)
|
|
"""
|
|
# add space to allow rotation and herringbone patterns to cover the shape
|
|
centroid = _get_rotation_center(outline)
|
|
min_radius = minimum_bounding_radius(outline)
|
|
minx = centroid.x - min_radius
|
|
miny = centroid.y - min_radius
|
|
maxx = centroid.x + min_radius
|
|
maxy = centroid.y + min_radius
|
|
|
|
# add some extra space
|
|
extra_space = max(
|
|
warp_width * PIXELS_PER_MM,
|
|
weft_width * PIXELS_PER_MM,
|
|
2 * fill.row_spacing * fill.rows_per_thread
|
|
)
|
|
minx -= extra_space
|
|
maxx += extra_space
|
|
miny -= extra_space
|
|
maxy += extra_space
|
|
|
|
minx -= (offset[0] * PIXELS_PER_MM)
|
|
miny -= (offset[1] * PIXELS_PER_MM)
|
|
|
|
return minx, miny, maxx, maxy
|
|
|
|
|
|
def _get_herringbone_color_segments(
|
|
lines: List[MultiLineString],
|
|
polygons: defaultdict,
|
|
outline: Polygon,
|
|
rotation: float,
|
|
stitch_length: float,
|
|
weft: bool = False
|
|
) -> defaultdict:
|
|
"""
|
|
Generate herringbone line segments in given tartan direction grouped by color
|
|
|
|
:param lines: the line segments forming the pattern
|
|
:param polygons: color grouped polygon stripes
|
|
:param outline: the outline to be filled with the herringbone pattern
|
|
:param rotation: degrees used for rotation
|
|
:param stitch_length: maximum stitch length for weft connector lines
|
|
:param weft: wether to render as warp or weft
|
|
:returns: defaultdict with color grouped herringbone segments
|
|
"""
|
|
line_segments: defaultdict = defaultdict(list)
|
|
|
|
if not polygons:
|
|
return line_segments
|
|
|
|
lines = line_merge(lines)
|
|
for line_blocks in lines:
|
|
segments = _get_tartan_color_segments(line_blocks, polygons, outline, rotation, stitch_length, weft, True)
|
|
for color, segment in segments.items():
|
|
if weft:
|
|
line_segments[color].append(MultiLineString(segment))
|
|
else:
|
|
line_segments[color].extend(segment)
|
|
|
|
if not weft:
|
|
return line_segments
|
|
|
|
return _get_weft_herringbone_color_segments(outline, line_segments, polygons, stitch_length)
|
|
|
|
|
|
def _get_weft_herringbone_color_segments(
|
|
outline: Polygon,
|
|
line_segments: defaultdict,
|
|
polygons: defaultdict,
|
|
stitch_length: float,
|
|
) -> defaultdict:
|
|
"""
|
|
Makes sure weft herringbone lines connect correctly
|
|
|
|
Herringbone weft lines need to connect in horizontal direction (or whatever the current rotation is)
|
|
which is opposed to the herringbone stripe blocks \\\\ //// \\\\ //// \\\\ ////
|
|
|
|
:param outline: the outline to be filled with the herringbone pattern
|
|
:param line_segments: the line segments forming the pattern
|
|
:param polygons: color grouped polygon stripes
|
|
:param stitch_length: maximum stitch length
|
|
:returns: defaultdict with color grouped weft lines
|
|
"""
|
|
weft_lines = defaultdict(list)
|
|
for color, lines in line_segments.items():
|
|
color_lines: List[LineString] = []
|
|
for polygon in polygons[color][0]:
|
|
polygon = polygon.normalize()
|
|
polygon_coords = list(polygon.exterior.coords)
|
|
polygon_top = LineString(polygon_coords[0:2])
|
|
polygon_bottom = LineString(polygon_coords[2:4]).reverse()
|
|
if not any([polygon_top.intersects(outline), polygon_bottom.intersects(outline)]):
|
|
polygon_top = LineString(polygon_coords[1:3])
|
|
polygon_bottom = LineString(polygon_coords[3:5]).reverse()
|
|
|
|
polygon_multi_lines = lines
|
|
polygon_multi_lines.sort(key=lambda line: polygon_bottom.project(line.centroid))
|
|
polygon_lines = []
|
|
for multiline in polygon_multi_lines:
|
|
polygon_lines.extend(multiline.geoms)
|
|
polygon_lines = [line for line in polygon_lines if line.intersects(polygon)]
|
|
if not polygon_lines:
|
|
continue
|
|
color_lines.extend(polygon_lines)
|
|
|
|
if polygon_top.intersects(outline) or polygon_bottom.intersects(outline):
|
|
connectors = _get_weft_herringbone_connectors(polygon_lines, polygon_top, polygon_bottom, stitch_length)
|
|
if connectors:
|
|
color_lines.extend(connectors)
|
|
|
|
check_stop_flag()
|
|
|
|
# Users are likely to type in a herringbone width which is a multiple (or fraction) of the stripe width.
|
|
# They may end up unconnected after line_merge, so we need to shift the weft for a random small number
|
|
multi_lines = translate(ensure_multi_line_string(line_merge(MultiLineString(color_lines))), 0.00123, 0.00123)
|
|
multi_lines = ensure_multi_line_string(multi_lines.intersection(outline))
|
|
|
|
weft_lines[color].extend(list(multi_lines.geoms))
|
|
|
|
return weft_lines
|
|
|
|
|
|
def _get_weft_herringbone_connectors(
|
|
polygon_lines: List[LineString],
|
|
polygon_top: LineString,
|
|
polygon_bottom: LineString,
|
|
stitch_length: float
|
|
) -> List[LineString]:
|
|
"""
|
|
Generates lines to connect lines
|
|
|
|
:param polygon_lines: lines to connect
|
|
:param polygon_top: top line of the polygon
|
|
:param polygon_bottom: bottom line of the polygon
|
|
:param stitch_length: stitch length
|
|
:returns: a list of LineString connectors
|
|
"""
|
|
connectors: List[LineString] = []
|
|
previous_end = None
|
|
for line in reversed(polygon_lines):
|
|
start = get_point(line, 0)
|
|
end = get_point(line, -1)
|
|
if previous_end is None:
|
|
# adjust direction of polygon lines if necessary
|
|
if polygon_top.project(start, True) > 0.5:
|
|
polygon_top = polygon_top.reverse()
|
|
polygon_bottom = polygon_bottom.reverse()
|
|
start_distance = polygon_top.project(start)
|
|
end_distance = polygon_top.project(end)
|
|
if start_distance > end_distance:
|
|
start, end = end, start
|
|
previous_end = end
|
|
continue
|
|
|
|
# adjust line direction and add connectors
|
|
prev_polygon_line = min([polygon_top, polygon_bottom], key=lambda polygon_line: previous_end.distance(polygon_line))
|
|
current_polygon_line = min([polygon_top, polygon_bottom], key=lambda polygon_line: start.distance(polygon_line))
|
|
if prev_polygon_line != current_polygon_line:
|
|
start, end = end, start
|
|
if not previous_end == start:
|
|
connector = LineString([previous_end, start])
|
|
if prev_polygon_line == polygon_top:
|
|
connector = connector.offset_curve(-0.0001)
|
|
else:
|
|
connector = connector.offset_curve(0.0001)
|
|
connectors.append(LineString([previous_end, get_point(connector, 0)]))
|
|
connectors.append(segmentize(connector, max_segment_length=stitch_length))
|
|
connectors.append(LineString([get_point(connector, -1), start]))
|
|
previous_end = end
|
|
return connectors
|
|
|
|
|
|
def _get_tartan_color_segments(
|
|
lines: List[LineString],
|
|
polygons: defaultdict,
|
|
outline: Polygon,
|
|
rotation: float,
|
|
stitch_length: float,
|
|
weft: bool = False,
|
|
herringbone: bool = False
|
|
) -> defaultdict:
|
|
"""
|
|
Generate tartan line segments in given tartan direction grouped by color
|
|
|
|
:param lines: the lines to form the tartan pattern with
|
|
:param polygons: color grouped polygon stripes
|
|
:param outline: the outline to fill with the tartan pattern
|
|
:param rotation: rotation in degrees
|
|
:param stitch_length: maximum stitch length for weft connector lines
|
|
:param weft: wether to render as warp or weft
|
|
:param herringbone: wether herringbone or normal tartan patterns are rendered
|
|
:returns: a dictionary with color grouped line segments
|
|
"""
|
|
line_segments: defaultdict = defaultdict(list)
|
|
if not polygons:
|
|
return line_segments
|
|
for color, shapes in polygons.items():
|
|
polygons = shapes[0]
|
|
for polygon in polygons:
|
|
segments = _get_segment_lines(polygon, lines, outline, stitch_length, rotation, weft, herringbone)
|
|
if segments:
|
|
line_segments[color].extend(segments)
|
|
check_stop_flag()
|
|
return line_segments
|
|
|
|
|
|
def _get_color_runs(lines: defaultdict, stitch_length: float) -> defaultdict:
|
|
"""
|
|
Segmentize running stitch segments and return in a separate color grouped dictionary
|
|
|
|
:param lines: tartan shapes grouped by color
|
|
:param stitch_length: stitch length used to segmentize the lines
|
|
:returns: defaultdict with segmentized running stitches grouped by color
|
|
"""
|
|
runs: defaultdict = defaultdict(list)
|
|
if not lines:
|
|
return runs
|
|
for color, shapes in lines.items():
|
|
for run in shapes[1]:
|
|
runs[color].append(segmentize(run, max_segment_length=stitch_length))
|
|
return runs
|
|
|
|
|
|
def _get_segment_lines(
|
|
polygon: Polygon,
|
|
lines: MultiLineString,
|
|
outline: Polygon,
|
|
stitch_length: float,
|
|
rotation: float,
|
|
weft: bool,
|
|
herringbone: bool
|
|
) -> List[LineString]:
|
|
"""
|
|
Fill the given polygon with lines
|
|
Each line should start and end at the outline border
|
|
|
|
:param polygon: the polygon stripe to fill
|
|
:param lines: the lines that form the pattern
|
|
:param outline: the outline to fill with the tartan pattern
|
|
:param stitch_length: maximum stitch length for weft connector lines
|
|
:param rotation: rotation in degrees
|
|
:param weft: wether to render as warp or weft
|
|
:param herringbone: wether herringbone or normal tartan patterns are rendered
|
|
:returns: a list of LineString objects
|
|
"""
|
|
boundary = outline.boundary
|
|
segments = []
|
|
if not lines.intersects(polygon):
|
|
return []
|
|
segment_lines = list(ensure_multi_line_string(lines.intersection(polygon), 0.5).geoms)
|
|
if not segment_lines:
|
|
return []
|
|
previous_line = None
|
|
for line in segment_lines:
|
|
segments.append(line)
|
|
if not previous_line:
|
|
previous_line = line
|
|
continue
|
|
point1 = get_point(previous_line, -1)
|
|
point2 = get_point(line, 0)
|
|
if point1.equals(point2):
|
|
previous_line = line
|
|
continue
|
|
# add connector from point1 to point2 if none of them touches the outline
|
|
connector = _get_connector(point1, point2, boundary, stitch_length)
|
|
if connector:
|
|
segments.append(connector)
|
|
previous_line = line
|
|
|
|
if not segments:
|
|
return []
|
|
lines = line_merge(MultiLineString(segments))
|
|
|
|
if not (herringbone and weft):
|
|
lines = lines.intersection(outline)
|
|
|
|
if not herringbone:
|
|
lines = _connect_lines_to_outline(lines, outline, rotation, stitch_length, weft)
|
|
|
|
return list(ensure_multi_line_string(lines).geoms)
|
|
|
|
|
|
def _get_connector(
|
|
point1: Point,
|
|
point2: Point,
|
|
boundary: Union[MultiLineString, LineString],
|
|
stitch_length: float
|
|
) -> Optional[LineString]:
|
|
"""
|
|
Constructs a line between the two points when they are not near the boundary
|
|
|
|
:param point1: first point
|
|
:param point2: last point
|
|
:param boundary: the outline of the shape (including holes)
|
|
:param stitch_length: maximum stitch length to segmentize new line
|
|
:returns: a LineString between point1 and point1, None if one of them touches the boundary
|
|
"""
|
|
connector = None
|
|
if point1.distance(boundary) > 0.005 and point2.distance(boundary) > 0.005:
|
|
connector = segmentize(LineString([point1, point2]), max_segment_length=stitch_length)
|
|
return connector
|
|
|
|
|
|
def _connect_lines_to_outline(
|
|
lines: Union[MultiLineString, LineString],
|
|
outline: Polygon,
|
|
rotation: float,
|
|
stitch_length: float,
|
|
weft: bool
|
|
) -> Union[MultiLineString, LineString]:
|
|
"""
|
|
Connects end points within the shape with the outline
|
|
This should only be necessary if the tartan angle is nearly 0 or 90 degrees
|
|
|
|
:param lines: lines to connect to the outline (if necessary)
|
|
:param outline: the shape to be filled with a tartan pattern
|
|
:param rotation: the rotation value
|
|
:param stitch_length: maximum stitch length to segmentize new line
|
|
:param weft: wether to render as warp or weft
|
|
:returns: merged line(s) connected to the outline
|
|
"""
|
|
boundary = outline.boundary
|
|
lines = list(ensure_multi_line_string(lines).geoms)
|
|
outline_connectors = []
|
|
for line in lines:
|
|
start = get_point(line, 0)
|
|
end = get_point(line, -1)
|
|
if start.intersects(outline) and start.distance(boundary) > 0.05:
|
|
outline_connectors.append(_connect_point_to_outline(start, outline, rotation, stitch_length, weft))
|
|
if end.intersects(outline) and end.distance(boundary) > 0.05:
|
|
outline_connectors.append(_connect_point_to_outline(end, outline, rotation, stitch_length, weft))
|
|
lines.extend(outline_connectors)
|
|
lines = line_merge(MultiLineString(lines))
|
|
return lines
|
|
|
|
|
|
def _connect_point_to_outline(
|
|
point: Point,
|
|
outline: Polygon,
|
|
rotation: float,
|
|
stitch_length: float,
|
|
weft: bool
|
|
) -> Union[LineString, list]:
|
|
"""
|
|
Connect given point to the outline
|
|
|
|
:param outline: the shape to be filled with a tartan pattern
|
|
:param rotation: the rotation value
|
|
:param stitch_length: maximum stitch length to segmentize new line
|
|
:param weft: wether to render as warp or weft
|
|
:returns: a Linestring with the correct angle for the given tartan direction (between outline and point)
|
|
"""
|
|
scale_factor = point.hausdorff_distance(outline) * 2
|
|
directional_vector = _get_angled_line_from_point(point, rotation, scale_factor, weft)
|
|
directional_vector = outline.boundary.intersection(directional_vector)
|
|
if directional_vector.is_empty:
|
|
return []
|
|
return segmentize(LineString([point, nearest_points(directional_vector, point)[0]]), max_segment_length=stitch_length)
|
|
|
|
|
|
def _get_angled_line_from_point(point: Point, rotation: float, scale_factor: float, weft: bool) -> LineString:
|
|
"""
|
|
Generates an angled line for the given tartan direction
|
|
|
|
:param point: the starting point for the new line
|
|
:param rotation: the rotation value
|
|
:param scale_factor: defines the length of the line
|
|
:param weft: wether to render as warp or weft
|
|
:returns: a LineString
|
|
"""
|
|
if not weft:
|
|
rotation += 90
|
|
rotation = radians(rotation)
|
|
x = point.coords[0][0] + cos(rotation)
|
|
y = point.coords[0][1] + sin(rotation)
|
|
return scale(LineString([point, (x, y)]), scale_factor, scale_factor)
|
|
|
|
|
|
def _get_fill_stitch_groups(
|
|
fill: 'FillStitch',
|
|
shape: Polygon,
|
|
color_lines: defaultdict,
|
|
starting_point: Union[tuple, Stitch, None],
|
|
ending_point: Union[tuple, Stitch, None]
|
|
) -> List[StitchGroup]:
|
|
"""
|
|
Route fill stitches
|
|
|
|
:param fill: the FillStitch element
|
|
:param shape: the shape to be filled
|
|
:param color_lines: lines grouped by color
|
|
:param starting_point: the starting_point
|
|
:paramt ending_point: the ending_point
|
|
:returns: a list with StitchGroup objects
|
|
"""
|
|
stitch_groups: List[StitchGroup] = []
|
|
i = 0
|
|
for color, lines in color_lines.items():
|
|
if not fill.stop_at_ending_point:
|
|
i += 1
|
|
if stitch_groups:
|
|
starting_point = stitch_groups[-1].stitches[-1]
|
|
if starting_point is None:
|
|
starting_point = ensure_multi_line_string(shape.boundary).geoms[0].coords[1]
|
|
if ending_point is None:
|
|
ending_point = ensure_multi_line_string(shape.boundary).geoms[0].coords[1]
|
|
|
|
segments = [list(line.coords) for line in lines if len(line.coords) > 1]
|
|
if len(segments) == 0:
|
|
continue
|
|
stitch_group = _segments_to_stitch_group(fill, shape, segments, i, color, starting_point, ending_point)
|
|
if stitch_group is not None:
|
|
stitch_groups.append(stitch_group)
|
|
check_stop_flag()
|
|
return stitch_groups
|
|
|
|
|
|
def _get_run_stitch_groups(
|
|
fill: 'FillStitch',
|
|
shape: Polygon,
|
|
color_lines: defaultdict,
|
|
starting_point: Optional[Union[tuple, Stitch]],
|
|
ending_point: Optional[Union[tuple, Stitch]]
|
|
) -> List[StitchGroup]:
|
|
"""
|
|
Route running stitches
|
|
|
|
:param fill: the FillStitch element
|
|
:param shape: the shape to be filled
|
|
:param color_lines: lines grouped by color
|
|
:param starting_point: the starting point
|
|
:param ending_point: the ending point
|
|
:returns: a list with StitchGroup objects
|
|
"""
|
|
stitch_groups: List[StitchGroup] = []
|
|
for color, lines in color_lines.items():
|
|
if not fill.stop_at_ending_point and stitch_groups:
|
|
starting_point = stitch_groups[-1].stitches[-1]
|
|
# get segments and ignore lines smaller than 0.5 mm
|
|
segments = [list(line.coords) for line in lines if line.length > 0.5 * PIXELS_PER_MM]
|
|
if len(segments) == 0:
|
|
continue
|
|
stitch_group = _segments_to_stitch_group(fill, shape, segments, 0, color, starting_point, ending_point, True)
|
|
if stitch_group is not None:
|
|
stitch_groups.append(stitch_group)
|
|
check_stop_flag()
|
|
return stitch_groups
|
|
|
|
|
|
def _segments_to_stitch_group(
|
|
fill: 'FillStitch',
|
|
shape: Polygon,
|
|
segments: List[List[Tuple[float, float]]],
|
|
iteration: int,
|
|
color: str,
|
|
starting_point: Optional[Union[tuple, Stitch]],
|
|
ending_point: Optional[Union[tuple, Stitch]],
|
|
runs: bool = False
|
|
) -> Optional[StitchGroup]:
|
|
"""
|
|
Route segments and turn them into a stitch group
|
|
|
|
:param fill: the FillStitch element
|
|
:param shape: the shape to be filled
|
|
:param segments: a list with coordinate tuples
|
|
:param iteration: wether to remove start and end travel stitches from the stitch group
|
|
:param color: color information
|
|
:param starting_point: the starting point
|
|
:param ending_point: the ending point
|
|
:param runs: wether running_stitch options should be applied or not
|
|
:returns: a StitchGroup
|
|
"""
|
|
fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
|
|
if is_empty(fill_stitch_graph):
|
|
return None
|
|
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, False)
|
|
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
|
|
)
|
|
|
|
if iteration:
|
|
stitches = remove_start_end_travel(fill, stitches, color, iteration)
|
|
|
|
if runs:
|
|
stitches = bean_stitch(stitches, fill.bean_stitch_repeats, ['auto_fill_travel'])
|
|
|
|
stitch_group = StitchGroup(
|
|
color=color,
|
|
tags=("tartan_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
|
|
)
|
|
|
|
if runs:
|
|
stitch_group.add_tag("tartan_run")
|
|
|
|
return stitch_group
|