summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/elements/fill_stitch.py28
-rw-r--r--lib/stitches/meander_fill.py26
-rw-r--r--lib/stitches/running_stitch.py3
-rw-r--r--lib/svg/tags.py2
-rw-r--r--lib/tiles.py56
-rw-r--r--lib/utils/smoothing.py8
6 files changed, 87 insertions, 36 deletions
diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py
index 980103a4..8f22278b 100644
--- a/lib/elements/fill_stitch.py
+++ b/lib/elements/fill_stitch.py
@@ -186,8 +186,20 @@ class FillStitch(EmbroideryElement):
return self.get_param('meander_pattern', min(tiles.all_tiles()).id)
@property
+ @param('meander_angle',
+ _('Meander pattern angle'),
+ type='float', unit="degrees",
+ default=0,
+ select_items=[('fill_method', 'meander_fill')],
+ sort_index=4)
+ def meander_angle(self):
+ return math.radians(self.get_float_param('meander_angle', 0))
+
+ @property
@param('meander_scale_percent',
_('Meander pattern scale'),
+ tooltip=_("Percentage to stretch or compress the meander pattern. You can scale horizontally " +
+ "and vertically individually by giving two percentages separated by a space. "),
type='float', unit="%",
default=100,
select_items=[('fill_method', 'meander_fill')],
@@ -554,6 +566,16 @@ class FillStitch(EmbroideryElement):
return self.get_float_param('expand_mm', 0)
@property
+ @param('clip', _('Clip path'),
+ tooltip=_('Constrain stitching to the shape. Useful when smoothing and expand are used.'),
+ type='boolean',
+ default=False,
+ select_items=[('fill_method', 'meander_fill')],
+ sort_index=6)
+ def clip(self):
+ return self.get_boolean_param('clip', False)
+
+ @property
@param('underpath',
_('Underpath'),
tooltip=_('Travel inside the shape when moving from section to section. Underpath '
@@ -648,7 +670,7 @@ class FillStitch(EmbroideryElement):
elif self.fill_method == 'guided_fill':
stitch_groups.extend(self.do_guided_fill(fill_shape, previous_stitch_group, start, end))
elif self.fill_method == 'meander_fill':
- stitch_groups.extend(self.do_meander_fill(fill_shape, i, start, end))
+ stitch_groups.extend(self.do_meander_fill(fill_shape, shape, i, start, end))
elif self.fill_method == 'circular_fill':
stitch_groups.extend(self.do_circular_fill(fill_shape, previous_stitch_group, start, end))
except ExitThread:
@@ -792,11 +814,11 @@ class FillStitch(EmbroideryElement):
))
return [stitch_group]
- def do_meander_fill(self, shape, i, starting_point, ending_point):
+ def do_meander_fill(self, shape, original_shape, i, starting_point, ending_point):
stitch_group = StitchGroup(
color=self.color,
tags=("meander_fill", "meander_fill_top"),
- stitches=meander_fill(self, shape, i, starting_point, ending_point))
+ stitches=meander_fill(self, shape, original_shape, i, starting_point, ending_point))
return [stitch_group]
@cache
diff --git a/lib/stitches/meander_fill.py b/lib/stitches/meander_fill.py
index 0a59da72..08ff4999 100644
--- a/lib/stitches/meander_fill.py
+++ b/lib/stitches/meander_fill.py
@@ -2,7 +2,7 @@ from itertools import combinations
import networkx as nx
from inkex import errormsg
-from shapely.geometry import MultiPoint, Point
+from shapely.geometry import LineString, MultiPoint, Point
from shapely.ops import nearest_points
from .. import tiles
@@ -18,7 +18,7 @@ from ..utils.threading import check_stop_flag
from .running_stitch import running_stitch
-def meander_fill(fill, shape, shape_index, starting_point, ending_point):
+def meander_fill(fill, shape, original_shape, shape_index, starting_point, ending_point):
debug.log(f"meander pattern: {fill.meander_pattern}")
tile = get_tile(fill.meander_pattern)
if not tile:
@@ -27,7 +27,7 @@ def meander_fill(fill, shape, shape_index, starting_point, ending_point):
debug.log(f"tile name: {tile.name}")
debug.log_line_strings(lambda: ensure_geometry_collection(shape.boundary).geoms, 'Meander shape')
- graph = tile.to_graph(shape, fill.meander_scale)
+ graph = tile.to_graph(shape, fill.meander_scale, fill.meander_angle)
if not graph:
label = fill.node.label or fill.node.get_id()
@@ -40,7 +40,7 @@ def meander_fill(fill, shape, shape_index, starting_point, ending_point):
start, end = find_starting_and_ending_nodes(graph, shape, starting_point, ending_point)
rng = iter_uniform_floats(fill.random_seed, 'meander-fill', shape_index)
- return post_process(generate_meander_path(graph, start, end, rng), shape, fill)
+ return post_process(generate_meander_path(graph, start, end, rng), shape, original_shape, fill)
def get_tile(tile_id):
@@ -126,10 +126,16 @@ def generate_meander_path(graph, start, end, rng):
check_stop_flag()
edge1, edge2 = poprandom(edge_pairs, rng)
- edges_to_consider.extend(replace_edge_pair(meander_path, edge1, edge2, graph, graph_nodes))
- break
+ new_edges = replace_edge_pair(meander_path, edge1, edge2, graph, graph_nodes)
+ if new_edges:
+ edges_to_consider.extend(new_edges)
+ break
+
+ debug.log_graph(graph, "remaining graph", "#FF0000")
+ points = path_to_points(meander_path)
+ debug.log_line_string(LineString(points), "meander path", "#00FF00")
- return path_to_points(meander_path)
+ return points
def replace_edge(path, edge, graph, graph_nodes):
@@ -169,14 +175,16 @@ def replace_edge_pair(path, edge1, edge2, graph, graph_nodes):
@debug.time
-def post_process(points, shape, fill):
+def post_process(points, shape, original_shape, fill):
debug.log(f"smoothness: {fill.smoothness}")
# debug.log_line_string(LineString(points), "pre-smoothed", "#FF0000")
smoothed_points = smooth_path(points, fill.smoothness)
smoothed_points = [InkStitchPoint.from_tuple(point) for point in smoothed_points]
stitches = running_stitch(smoothed_points, fill.running_stitch_length, fill.running_stitch_tolerance)
- stitches = clamp_path_to_polygon(stitches, shape)
+
+ if fill.clip:
+ stitches = clamp_path_to_polygon(stitches, original_shape)
return stitches
diff --git a/lib/stitches/running_stitch.py b/lib/stitches/running_stitch.py
index 1dbfcaaf..46f3a3e9 100644
--- a/lib/stitches/running_stitch.py
+++ b/lib/stitches/running_stitch.py
@@ -10,6 +10,8 @@ from copy import copy
import numpy as np
from shapely import geometry as shgeo
+
+from ..debug import debug
from ..utils import prng
from ..utils.geometry import Point
from ..utils.threading import check_stop_flag
@@ -246,6 +248,7 @@ def path_to_curves(points: typing.List[Point], min_len: float):
return curves
+@debug.time
def running_stitch(points, stitch_length, tolerance):
# Turn a continuous path into a running stitch.
stitches = [points[0]]
diff --git a/lib/svg/tags.py b/lib/svg/tags.py
index 9b5a78fb..bbef6ebb 100644
--- a/lib/svg/tags.py
+++ b/lib/svg/tags.py
@@ -81,6 +81,7 @@ inkstitch_attribs = [
'reverse',
'meander_pattern',
'meander_scale_percent',
+ 'meander_angle',
'expand_mm',
'fill_underlay',
'fill_underlay_angle',
@@ -97,6 +98,7 @@ inkstitch_attribs = [
'underpath',
'flip',
'expand_mm',
+ 'clip',
# stroke
'stroke_method',
'bean_stitch_repeats',
diff --git a/lib/tiles.py b/lib/tiles.py
index 1b418905..0bf92abc 100644
--- a/lib/tiles.py
+++ b/lib/tiles.py
@@ -5,7 +5,7 @@ import inkex
import json
import lxml
import networkx as nx
-from shapely.geometry import LineString
+from shapely.geometry import LineString, MultiLineString
from shapely.prepared import prep
from .debug import debug
@@ -59,8 +59,9 @@ class Tile:
def _load_paths(self, tile_svg):
path_elements = tile_svg.findall('.//svg:path', namespaces=inkex.NSS)
- self.tile = self._path_elements_to_line_strings(path_elements)
- # self.center, ignore, ignore = self._get_center_and_dimensions(self.tile)
+ tile = self._path_elements_to_line_strings(path_elements)
+ center, ignore, ignore = self._get_center_and_dimensions(MultiLineString(tile))
+ self.tile = [(start - center, end - center) for start, end in tile]
def _load_dimensions(self, tile_svg):
svg_element = tile_svg.getroot()
@@ -110,20 +111,20 @@ class Tile:
return translated_tile
- def _scale(self, x_scale, y_scale):
- scaled_shift0 = self.shift0.scale(x_scale, y_scale)
- scaled_shift1 = self.shift1.scale(x_scale, y_scale)
+ def _scale_and_rotate(self, x_scale, y_scale, angle):
+ transformed_shift0 = self.shift0.scale(x_scale, y_scale).rotate(angle)
+ transformed_shift1 = self.shift1.scale(x_scale, y_scale).rotate(angle)
- scaled_tile = []
+ transformed_tile = []
for start, end in self.tile:
- start = start.scale(x_scale, y_scale)
- end = end.scale(x_scale, y_scale)
- scaled_tile.append((start, end))
+ start = start.scale(x_scale, y_scale).rotate(angle)
+ end = end.scale(x_scale, y_scale).rotate(angle)
+ transformed_tile.append((start, end))
- return scaled_shift0, scaled_shift1, scaled_tile
+ return transformed_shift0, transformed_shift1, transformed_tile
@debug.time
- def to_graph(self, shape, scale):
+ def to_graph(self, shape, scale, angle):
"""Apply this tile to a shape, repeating as necessary.
Return value:
@@ -133,25 +134,37 @@ class Tile:
"""
self._load()
x_scale, y_scale = scale
- shift0, shift1, tile = self._scale(x_scale, y_scale)
+ shift0, shift1, tile = self._scale_and_rotate(x_scale, y_scale, angle)
shape_center, shape_width, shape_height = self._get_center_and_dimensions(shape)
- shape_diagonal = Point(shape_width, shape_height).length()
prepared_shape = prep(shape)
- return self._generate_graph(prepared_shape, shape_center, shape_diagonal, shift0, shift1, tile)
+ return self._generate_graph(prepared_shape, shape_center, shape_width, shape_height, shift0, shift1, tile)
- def _generate_graph(self, shape, shape_center, shape_diagonal, shift0, shift1, tile):
+ @debug.time
+ def _generate_graph(self, shape, shape_center, shape_width, shape_height, shift0, shift1, tile):
graph = nx.Graph()
- tiles0 = ceil(shape_diagonal / shift0.length()) + 2
- tiles1 = ceil(shape_diagonal / shift1.length()) + 2
- for repeat0 in range(floor(-tiles0 / 2), ceil(tiles0 / 2)):
- for repeat1 in range(floor(-tiles1 / 2), ceil(tiles1 / 2)):
+
+ shape_diagonal = Point(shape_width, shape_height).length()
+ num_tiles = ceil(shape_diagonal / min(shift0.length(), shift1.length()))
+ debug.log(f"num_tiles: {num_tiles}")
+
+ tile_diagonal = (shift0 + shift1).length()
+ x_cutoff = shape_width / 2 + tile_diagonal
+ y_cutoff = shape_height / 2 + tile_diagonal
+
+ for repeat0 in range(-num_tiles, num_tiles):
+ for repeat1 in range(-num_tiles, num_tiles):
check_stop_flag()
offset0 = repeat0 * shift0
offset1 = repeat1 * shift1
- this_tile = self._translate_tile(tile, offset0 + offset1 + shape_center)
+ offset = offset0 + offset1
+
+ if abs(offset.x) > x_cutoff or abs(offset.y) > y_cutoff:
+ continue
+
+ this_tile = self._translate_tile(tile, offset + shape_center)
for line in this_tile:
line_string = LineString(line)
if shape.contains(line_string):
@@ -161,6 +174,7 @@ class Tile:
return graph
+ @debug.time
def _remove_dead_ends(self, graph):
graph.remove_edges_from(nx.selfloop_edges(graph))
while True:
diff --git a/lib/utils/smoothing.py b/lib/utils/smoothing.py
index 9d43a9f1..1bb250c5 100644
--- a/lib/utils/smoothing.py
+++ b/lib/utils/smoothing.py
@@ -70,7 +70,8 @@ def smooth_path(path, smoothness=1.0):
# .T transposes the array (for some reason splprep expects
# [[x1, x2, ...], [y1, y2, ...]]
- tck, fp, ier, msg = splprep(coords.T, s=s, k=3, nest=-1, full_output=1)
+ with debug.time_this("splprep"):
+ tck, fp, ier, msg = splprep(coords.T, s=s, k=3, nest=-1, full_output=1)
if ier > 0:
debug.log(f"error {ier} smoothing path: {msg}")
return path
@@ -78,7 +79,8 @@ def smooth_path(path, smoothness=1.0):
# Evaluate the spline curve at many points along its length to produce the
# smoothed point list. 2 * num_points seems to be a good number, but it
# does produce a lot of points.
- smoothed_x_values, smoothed_y_values = splev(np.linspace(0, 1, int(num_points * 2)), tck[0])
- coords = np.array([smoothed_x_values, smoothed_y_values]).T
+ with debug.time_this("splev"):
+ smoothed_x_values, smoothed_y_values = splev(np.linspace(0, 1, int(num_points * 2)), tck[0])
+ coords = np.array([smoothed_x_values, smoothed_y_values]).T
return [Point(x, y) for x, y in coords]