summaryrefslogtreecommitdiff
path: root/lib/stitches
diff options
context:
space:
mode:
Diffstat (limited to 'lib/stitches')
-rw-r--r--lib/stitches/__init__.py1
-rw-r--r--lib/stitches/auto_fill.py33
-rw-r--r--lib/stitches/circular_fill.py2
-rw-r--r--lib/stitches/fill.py18
-rw-r--r--lib/stitches/guided_fill.py6
-rw-r--r--lib/stitches/linear_gradient_fill.py2
-rw-r--r--lib/stitches/tartan_fill.py808
7 files changed, 847 insertions, 23 deletions
diff --git a/lib/stitches/__init__.py b/lib/stitches/__init__.py
index ba56a0ec..2164ef9e 100644
--- a/lib/stitches/__init__.py
+++ b/lib/stitches/__init__.py
@@ -9,6 +9,7 @@ from .fill import legacy_fill
from .guided_fill import guided_fill
from .linear_gradient_fill import linear_gradient_fill
from .meander_fill import meander_fill
+from .tartan_fill import tartan_fill
# Can't put this here because we get a circular import :(
# from .auto_satin import auto_satin
diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py
index ebb1fb6f..e90093ce 100644
--- a/lib/stitches/auto_fill.py
+++ b/lib/stitches/auto_fill.py
@@ -7,6 +7,7 @@
import math
from itertools import chain, groupby
+from typing import Iterator
import networkx
from shapely import geometry as shgeo
@@ -53,11 +54,15 @@ class PathEdge(object):
def __eq__(self, other):
return self._sorted_nodes == other._sorted_nodes and self.key == other.key
+ def __iter__(self) -> Iterator:
+ for i in range(2):
+ yield self[i]
+
def is_outline(self):
- return self.key in self.OUTLINE_KEYS
+ return self.key.startswith(self.OUTLINE_KEYS)
def is_segment(self):
- return self.key == self.SEGMENT_KEY
+ return self.key.startswith(self.SEGMENT_KEY)
@debug.time
@@ -87,7 +92,7 @@ def auto_fill(shape,
return fallback(shape, running_stitch_length, running_stitch_tolerance)
# ensure graph is eulerian
- fill_stitch_graph = graph_make_valid(fill_stitch_graph)
+ graph_make_valid(fill_stitch_graph)
travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath)
@@ -298,8 +303,24 @@ def add_edges_between_outline_nodes(graph, duplicate_every_other=False):
def graph_make_valid(graph):
if not networkx.is_eulerian(graph):
- return networkx.eulerize(graph)
- return graph
+ newgraph = networkx.eulerize(graph)
+ for start, end, key, data in newgraph.edges(keys=True, data=True):
+ if isinstance(key, int):
+ # make valid duplicated edges, we cannot use the very same key
+ # again, but the automatic naming will not apply to the autofill algorithm
+ graph_edges = graph[start][end]
+ if 'segment' in graph_edges.keys():
+ data = graph_edges['segment']
+ graph.add_edge(start, end, key=f'segment-{key}', **data)
+ elif 'outline' in graph_edges.keys():
+ data = graph_edges['outline']
+ graph.add_edge(start, end, key='outline-{key}', **data)
+ elif 'extra' in graph_edges.keys():
+ data = graph_edges['extra']
+ graph.add_edge(start, end, key='extra-{key}', **data)
+ elif 'initial' in graph_edges.keys():
+ data = graph_edges['initial']
+ graph.add_edge(start, end, key='initial-{key}', **data)
def fallback(shape, running_stitch_length, running_stitch_tolerance):
@@ -380,7 +401,7 @@ def weight_edges_by_length(graph, multiplier=1):
def get_segments(graph):
segments = []
for start, end, key, data in graph.edges(keys=True, data=True):
- if key == 'segment':
+ if key.startswith('segment'):
segments.append(data["geometry"])
return segments
diff --git a/lib/stitches/circular_fill.py b/lib/stitches/circular_fill.py
index ec133f99..28346dd9 100644
--- a/lib/stitches/circular_fill.py
+++ b/lib/stitches/circular_fill.py
@@ -77,7 +77,7 @@ def circular_fill(shape,
if is_empty(fill_stitch_graph):
return fallback(shape, running_stitch_length, running_stitch_tolerance)
- fill_stitch_graph = graph_make_valid(fill_stitch_graph)
+ graph_make_valid(fill_stitch_graph)
travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath)
path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point)
diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py
index 9e9ff790..c492a629 100644
--- a/lib/stitches/fill.py
+++ b/lib/stitches/fill.py
@@ -14,12 +14,15 @@ from ..utils import cache
from ..utils.threading import check_stop_flag
-def legacy_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, flip, staggers, skip_last):
+def legacy_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, flip, reverse, staggers, skip_last):
rows_of_segments = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing, flip)
groups_of_segments = pull_runs(rows_of_segments, shape, row_spacing)
- return [section_to_stitches(group, angle, row_spacing, max_stitch_length, staggers, skip_last)
- for group in groups_of_segments]
+ stitches = [section_to_stitches(group, angle, row_spacing, max_stitch_length, staggers, skip_last)
+ for group in groups_of_segments]
+ if reverse:
+ stitches = [segment[::-1] for segment in stitches[::-1]]
+ return stitches
@cache
@@ -223,14 +226,8 @@ def pull_runs(rows, shape, row_spacing):
# over to midway up the lower right leg. We want to stop there and
# start a new patch.
- # for row in rows:
- # print >> sys.stderr, len(row)
-
- # print >>sys.stderr, "\n".join(str(len(row)) for row in rows)
-
rows = list(rows)
runs = []
- count = 0
while (len(rows) > 0):
run = []
prev = None
@@ -248,10 +245,7 @@ def pull_runs(rows, shape, row_spacing):
rows[row_num] = rest
- # print >> sys.stderr, len(run)
runs.append(run)
rows = [r for r in rows if len(r) > 0]
- count += 1
-
return runs
diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py
index 6f650028..bc7a3ab2 100644
--- a/lib/stitches/guided_fill.py
+++ b/lib/stitches/guided_fill.py
@@ -43,7 +43,7 @@ def guided_fill(shape,
if is_empty(fill_stitch_graph):
return fallback(shape, guideline, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance,
num_staggers, skip_last, starting_point, ending_point, underpath)
- fill_stitch_graph = graph_make_valid(fill_stitch_graph)
+ graph_make_valid(fill_stitch_graph)
travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath)
path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point)
@@ -156,14 +156,14 @@ def take_only_line_strings(thing):
return shgeo.MultiLineString(line_strings)
-def apply_stitches(line, max_stitch_length, num_staggers, row_spacing, row_num, threshold=None):
+def apply_stitches(line, max_stitch_length, num_staggers, row_spacing, row_num, threshold=None) -> shgeo.LineString:
if num_staggers == 0:
num_staggers = 1 # sanity check to avoid division by zero.
start = ((row_num / num_staggers) % 1) * max_stitch_length
projections = np.arange(start, line.length, max_stitch_length)
points = np.array([line.interpolate(projection).coords[0] for projection in projections])
- if len(points) <= 2:
+ if len(points) < 2:
return line
stitched_line = shgeo.LineString(points)
diff --git a/lib/stitches/linear_gradient_fill.py b/lib/stitches/linear_gradient_fill.py
index c1f0fb46..e72161ca 100644
--- a/lib/stitches/linear_gradient_fill.py
+++ b/lib/stitches/linear_gradient_fill.py
@@ -268,7 +268,7 @@ def _get_stitch_groups(fill, shape, colors, color_lines, starting_point, ending_
if is_empty(fill_stitch_graph):
continue
- fill_stitch_graph = graph_make_valid(fill_stitch_graph)
+ 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)
diff --git a/lib/stitches/tartan_fill.py b/lib/stitches/tartan_fill.py
new file mode 100644
index 00000000..4d9f3b0f
--- /dev/null
+++ b/lib/stitches/tartan_fill.py
@@ -0,0 +1,808 @@
+# 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 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)
+ if stitch_groups:
+ 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,
+) -> List[StitchGroup]:
+ """
+ Route fill stitches
+
+ :param fill: the FillStitch element
+ :param shape: the shape to be filled
+ :param color_lines: lines grouped by color
+ :returns: a list with StitchGroup objects
+ """
+ stitch_groups: List[StitchGroup] = []
+ i = 0
+ for color, lines in color_lines.items():
+ i += 1
+ if stitch_groups:
+ starting_point = stitch_groups[-1].stitches[-1]
+ else:
+ starting_point = ensure_multi_line_string(shape.boundary).geoms[0].coords[0]
+ ending_point = ensure_multi_line_string(shape.boundary).geoms[0].coords[0]
+ segments = [list(line.coords) for line in lines if len(line.coords) > 1]
+ 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():
+ segments = [list(line.coords) for line in lines if len(line.coords) > 1]
+ stitch_group = _segments_to_stitch_group(fill, shape, segments, None, 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: Optional[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)
+ 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