summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/debug.py9
-rw-r--r--lib/elements/fill_stitch.py23
-rw-r--r--lib/extensions/params.py21
-rw-r--r--lib/stitches/meander_fill.py72
-rw-r--r--lib/svg/tags.py2
-rw-r--r--lib/tiles.py113
-rw-r--r--lib/utils/geometry.py12
-rw-r--r--lib/utils/list.py14
-rw-r--r--lib/utils/string.py7
9 files changed, 196 insertions, 77 deletions
diff --git a/lib/debug.py b/lib/debug.py
index 0d6af104..94d32cea 100644
--- a/lib/debug.py
+++ b/lib/debug.py
@@ -234,6 +234,15 @@ class Debug(object):
}))
@check_enabled
+ def log_point(self, point, name="point", color=None):
+ self.log_svg_element(etree.Element("circle", {
+ "cx": str(point.x),
+ "cy": str(point.y),
+ "r": "1",
+ "style": str(inkex.Style({"fill": "#000000"})),
+ }))
+
+ @check_enabled
def log_graph(self, graph, name="Graph", color=None):
d = ""
diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py
index fbaab0c2..790eec5c 100644
--- a/lib/elements/fill_stitch.py
+++ b/lib/elements/fill_stitch.py
@@ -5,6 +5,7 @@
import logging
import math
+import numpy as np
import re
import sys
import traceback
@@ -142,7 +143,7 @@ class FillStitch(EmbroideryElement):
@property
@param('smoothness_mm', _('Smoothness'),
tooltip=_(
- 'Smooth the stitch path. Smoothness limits how far the smoothed stitch path ' +
+ 'Smooth the stitch path. Smoothness limits approximately how far the smoothed stitch path ' +
'is allowed to deviate from the original path. Hint: a lower stitchc tolerance may be needed too.'
),
type='integer',
@@ -159,12 +160,22 @@ class FillStitch(EmbroideryElement):
return self.get_boolean_param('clockwise', True)
@property
- @param('meander_pattern', _('Meander Pattern'), type='dropdown', default=0,
+ @param('meander_pattern', _('Meander Pattern'), type='select', default=0,
options=[tile.name for tile in tiles.all_tiles()], select_items=[('fill_method', 4)], sort_index=3)
def meander_pattern(self):
return self.get_param('meander_pattern', None)
@property
+ @param('meander_scale_percent', _('Meander pattern scale'), type='float', unit="%", default=100, select_items=[('fill_method', 4)], sort_index=4)
+ def meander_scale(self):
+ return np.maximum(self.get_split_float_param('meander_scale_percent', (100, 100)), (10, 10)) / 100
+
+ @property
+ @param('meander_padding_mm', _('Meander padding'), type='float', unit="mm", default=0, select_items=[('fill_method', 4)], sort_index=5)
+ def meander_padding(self):
+ return self.get_float_param('meander_padding_mm', 0)
+
+ @property
@param('angle',
_('Angle of lines of stitches'),
tooltip=_('The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'),
@@ -593,7 +604,7 @@ class FillStitch(EmbroideryElement):
stitch_groups.extend(underlay_stitch_groups)
fill_shapes = self.fill_shape(shape)
- for fill_shape in fill_shapes.geoms:
+ for i, fill_shape in enumerate(fill_shapes.geoms):
if self.fill_method == 0:
stitch_groups.extend(self.do_auto_fill(fill_shape, previous_stitch_group, start, end))
if self.fill_method == 1:
@@ -601,7 +612,7 @@ class FillStitch(EmbroideryElement):
elif self.fill_method == 2:
stitch_groups.extend(self.do_guided_fill(fill_shape, previous_stitch_group, start, end))
elif self.fill_method == 4:
- stitch_groups.extend(self.do_meander_fill(fill_shape, start, end))
+ stitch_groups.extend(self.do_meander_fill(fill_shape, i, start, end))
except ExitThread:
raise
except Exception:
@@ -733,11 +744,11 @@ class FillStitch(EmbroideryElement):
))
return [stitch_group]
- def do_meander_fill(self, shape, starting_point, ending_point):
+ def do_meander_fill(self, shape, i, starting_point, ending_point):
stitch_group = StitchGroup(
color=self.color,
tags=("meander_fill", "meander_fill_top"),
- stitches=meander_fill(self, shape, starting_point, ending_point))
+ stitches=meander_fill(self, shape, i, starting_point, ending_point))
return [stitch_group]
@cache
diff --git a/lib/extensions/params.py b/lib/extensions/params.py
index 1262ceb6..a34aeeae 100644
--- a/lib/extensions/params.py
+++ b/lib/extensions/params.py
@@ -190,8 +190,13 @@ class ParamsTab(ScrolledPanel):
try:
values[name] = input.GetValue()
except AttributeError:
- # dropdown
- values[name] = input.GetSelection()
+ param = self.dict_of_choices[name]['param']
+ if param.type == 'dropdown':
+ # dropdown
+ values[name] = input.GetSelection()
+ elif param.type == 'select':
+ selection = input.GetSelection()
+ values[name] = param.options[selection]
return values
@@ -368,9 +373,17 @@ class ParamsTab(ScrolledPanel):
input.SetValue(param.values[0])
input.Bind(wx.EVT_CHECKBOX, self.changed)
- elif param.type == 'dropdown':
+ elif param.type in ('dropdown', 'select'):
input = wx.Choice(self, wx.ID_ANY, choices=param.options)
- input.SetSelection(int(param.values[0]))
+
+ if param.type == 'dropdown':
+ input.SetSelection(int(param.values[0]))
+ else:
+ try:
+ input.SetSelection(param.options.index(param.values[0]))
+ except ValueError:
+ input.SetSelection(param.default)
+
input.Bind(wx.EVT_CHOICE, self.changed)
input.Bind(wx.EVT_CHOICE, self.update_choice_state)
self.dict_of_choices[param.name] = {
diff --git a/lib/stitches/meander_fill.py b/lib/stitches/meander_fill.py
index 2ac3cd03..cb6e3f4e 100644
--- a/lib/stitches/meander_fill.py
+++ b/lib/stitches/meander_fill.py
@@ -1,21 +1,33 @@
+import networkx as nx
from shapely.geometry import MultiPoint, Point
from shapely.ops import nearest_points
-import networkx as nx
+from .running_stitch import running_stitch
from .. import tiles
from ..debug import debug
+from ..stitch_plan import Stitch
+from ..utils import smooth_path
+from ..utils.geometry import Point as InkStitchPoint
from ..utils.list import poprandom
+from ..utils.prng import iter_uniform_floats
-def meander_fill(fill, shape, starting_point, ending_point):
+def meander_fill(fill, shape, shape_index, starting_point, ending_point):
+ debug.log(f"meander pattern: {fill.meander_pattern}")
tile = get_tile(fill.meander_pattern)
if not tile:
return []
- graph = tile.to_graph(shape)
- start, end = find_starting_and_ending_nodes(graph, starting_point, ending_point)
+ debug.log(f"tile name: {tile.name}")
- return generate_meander_path(graph, start, end)
+ # debug.log_line_strings(ensure_geometry_collection(shape.boundary).geoms, 'Meander shape')
+ graph = tile.to_graph(shape, fill.meander_scale, fill.meander_padding)
+ # debug.log_graph(graph, 'Meander graph')
+ # debug.log(f"graph connected? {nx.is_connected(graph)}")
+ 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), fill)
def get_tile(tile_name):
@@ -27,7 +39,16 @@ def get_tile(tile_name):
return None
-def find_starting_and_ending_nodes(graph, starting_point, ending_point):
+def find_starting_and_ending_nodes(graph, shape, starting_point, ending_point):
+ if starting_point is None:
+ starting_point = shape.exterior.coords[0]
+ starting_point = Point(starting_point)
+
+ if ending_point is None:
+ ending_point = starting_point
+ else:
+ ending_point = Point(ending_point)
+
all_points = MultiPoint(list(graph))
starting_node = nearest_points(starting_point, all_points)[1].coords[0]
@@ -49,7 +70,8 @@ def find_initial_path(graph, start, end):
return nx.shortest_path(graph, start, end)
-def generate_meander_path(graph, start, end):
+@debug.time
+def generate_meander_path(graph, start, end, rng):
path = find_initial_path(graph, start, end)
path_edges = list(zip(path[:-1], path[1:]))
graph.remove_edges_from(path_edges)
@@ -59,15 +81,16 @@ def generate_meander_path(graph, start, end):
meander_path = path_edges
while edges_to_consider:
while edges_to_consider:
- edge = poprandom(edges_to_consider)
+ edge = poprandom(edges_to_consider, rng)
edges_to_consider.extend(replace_edge(meander_path, edge, graph, graph_nodes))
- edge_pairs = list(zip(path[:-1], path[1:]))
+ edge_pairs = list(zip(meander_path[:-1], meander_path[1:]))
while edge_pairs:
- edge1, edge2 = poprandom(edge_pairs)
+ edge1, edge2 = poprandom(edge_pairs, rng)
edges_to_consider.extend(replace_edge_pair(meander_path, edge1, edge2, graph, graph_nodes))
+ break
- return meander_path
+ return path_to_points(meander_path)
def replace_edge(path, edge, graph, graph_nodes):
@@ -81,8 +104,9 @@ def replace_edge(path, edge, graph, graph_nodes):
i = path.index(edge)
path[i:i + 1] = new_path
graph.remove_edges_from(new_path)
+ # do I need to remove the last one too?
graph_nodes.difference_update(start for start, end in new_path)
- debug.log(f"found new path of length {len(new_path)} at position {i}")
+ # debug.log(f"found new path of length {len(new_path)} at position {i}")
return new_path
@@ -98,7 +122,29 @@ def replace_edge_pair(path, edge1, edge2, graph, graph_nodes):
i = path.index(edge1)
path[i:i + 2] = new_path
graph.remove_edges_from(new_path)
+ # do I need to remove the last one too?
graph_nodes.difference_update(start for start, end in new_path)
- debug.log(f"found new pair path of length {len(new_path)} at position {i}")
+ # debug.log(f"found new pair path of length {len(new_path)} at position {i}")
return new_path
+
+
+@debug.time
+def post_process(points, 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 = [Stitch(point) for point in stitches]
+
+ return stitches
+
+
+def path_to_points(path):
+ points = [start for start, end in path]
+ if path:
+ points.append(path[-1][1])
+
+ return points
diff --git a/lib/svg/tags.py b/lib/svg/tags.py
index 4979b58a..32744d1b 100644
--- a/lib/svg/tags.py
+++ b/lib/svg/tags.py
@@ -70,6 +70,8 @@ inkstitch_attribs = [
'clockwise',
'reverse',
'meander_pattern',
+ 'meander_scale_percent',
+ 'meander_padding_mm',
'expand_mm',
'fill_underlay',
'fill_underlay_angle',
diff --git a/lib/tiles.py b/lib/tiles.py
index e9f0305a..2bef7a19 100644
--- a/lib/tiles.py
+++ b/lib/tiles.py
@@ -1,15 +1,15 @@
-import inkex
+import os
from math import ceil, floor
+
+import inkex
+import lxml
from networkx import Graph
-import os
from shapely.geometry import LineString
from shapely.prepared import prep
from .debug import debug
from .svg import apply_transforms
-from .svg.tags import SODIPODI_NAMEDVIEW
-from .utils import cache, get_bundled_dir, guess_inkscape_config_path, Point
-from random import random
+from .utils import Point, cache, get_bundled_dir, guess_inkscape_config_path
class Tile:
@@ -33,11 +33,7 @@ class Tile:
__str__ = __repr__
def _get_name(self, tile_svg, tile_path):
- name = tile_svg.get(SODIPODI_NAMEDVIEW)
- if name:
- return name
- else:
- return os.path.splitext(os.path.basename(tile_path))[0]
+ return os.path.splitext(os.path.basename(tile_path)[0])
def _load(self):
self._load_paths(self.tile_svg)
@@ -46,39 +42,35 @@ class Tile:
self._load_parallelogram(self.tile_svg)
def _load_paths(self, tile_svg):
- if self.tile is None:
- 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)
+ 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)
def _load_dimensions(self, tile_svg):
- if self.width is None:
- svg_element = tile_svg.getroot()
- self.width = svg_element.viewport_width
- self.height = svg_element.viewport_height
+ svg_element = tile_svg.getroot()
+ self.width = svg_element.viewport_width
+ self.height = svg_element.viewport_height
def _load_buffer_size(self, tile_svg):
- if self.buffer_size is None:
- circle_elements = tile_svg.findall('.//svg:circle', namespaces=inkex.NSS)
- if circle_elements:
- self.buffer_size = circle_elements[0].radius
- else:
- self.buffer_size = 0
+ circle_elements = tile_svg.findall('.//svg:circle', namespaces=inkex.NSS)
+ if circle_elements:
+ self.buffer_size = circle_elements[0].radius
+ else:
+ self.buffer_size = 0
def _load_parallelogram(self, tile_svg):
- if self.shift0 is None:
- parallelogram_elements = tile_svg.findall(".//svg:*[@class='para']", namespaces=inkex.NSS)
- if parallelogram_elements:
- path_element = parallelogram_elements[0]
- path = apply_transforms(path_element.get_path(), path_element)
- subpaths = path.to_superpath()
- subpath = subpaths[0]
- points = [Point.from_tuple(p[1]) for p in subpath]
- self.shift0 = points[1] - points[0]
- self.shift1 = points[2] - points[1]
- else:
- self.shift0 = Point(self.width, 0)
- self.shift1 = Point(0, self.height)
+ parallelogram_elements = tile_svg.findall(".//svg:*[@class='para']", namespaces=inkex.NSS)
+ if parallelogram_elements:
+ path_element = parallelogram_elements[0]
+ path = apply_transforms(path_element.get_path(), path_element)
+ subpaths = path.to_superpath()
+ subpath = subpaths[0]
+ points = [Point.from_tuple(p[1]) for p in subpath]
+ self.shift0 = points[1] - points[0]
+ self.shift1 = points[2] - points[1]
+ else:
+ self.shift0 = Point(self.width, 0)
+ self.shift1 = Point(0, self.height)
def _path_elements_to_line_strings(self, path_elements):
lines = []
@@ -109,8 +101,19 @@ class Tile:
return translated_tile
+ def _scale(self, x_scale, y_scale):
+ self.shift0 = self.shift0.scale(x_scale, y_scale)
+ self.shift1 = self.shift1.scale(x_scale, y_scale)
+
+ scaled_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))
+ self.tile = scaled_tile
+
@debug.time
- def to_graph(self, shape, only_inside=True, pad=True):
+ def to_graph(self, shape, scale, buffer=None):
"""Apply this tile to a shape, repeating as necessary.
Return value:
@@ -119,27 +122,36 @@ class Tile:
representation of this edge.
"""
self._load()
+ x_scale, y_scale = scale
+ self._scale(x_scale, y_scale)
shape_center, shape_width, shape_height = self._get_center_and_dimensions(shape)
- shape_diagonal = (shape_width ** 2 + shape_height ** 2) ** 0.5
- graph = Graph()
+ shape_diagonal = Point(shape_width, shape_height).length()
+
+ if not buffer:
+ average_scale = (x_scale + y_scale) / 2
+ buffer = self.buffer_size * average_scale
- if pad:
- shape = shape.buffer(-self.buffer_size)
+ contracted_shape = shape.buffer(-buffer)
+ prepared_shape = prep(contracted_shape)
- prepared_shape = prep(shape)
+ # debug.log_line_string(contracted_shape.exterior, "contracted shape")
+ return self._generate_graph(prepared_shape, shape_center, shape_diagonal)
+
+ def _generate_graph(self, shape, shape_center, shape_diagonal):
+ graph = Graph()
tiles0 = ceil(shape_diagonal / self.shift0.length()) + 2
tiles1 = ceil(shape_diagonal / self.shift1.length()) + 2
for repeat0 in range(floor(-tiles0 / 2), ceil(tiles0 / 2)):
for repeat1 in range(floor(-tiles1 / 2), ceil(tiles1 / 2)):
- shift0 = repeat0 * self.shift0 + shape_center
- shift1 = repeat1 * self.shift1 + shape_center
- this_tile = self._translate_tile(shift0 + shift1)
+ shift0 = repeat0 * self.shift0
+ shift1 = repeat1 * self.shift1
+ this_tile = self._translate_tile(shift0 + shift1 + shape_center)
for line in this_tile:
line_string = LineString(line)
- if not only_inside or prepared_shape.contains(line_string):
- graph.add_edge(line[0], line[1], line_string=line_string, weight=random() + 0.1)
+ if shape.contains(line_string):
+ graph.add_edge(line[0], line[1])
return graph
@@ -155,7 +167,10 @@ def all_tiles():
for tile_dir in all_tile_paths():
try:
for tile_file in sorted(os.listdir(tile_dir)):
- tiles.append(Tile(os.path.join(tile_dir, tile_file)))
+ try:
+ tiles.append(Tile(os.path.join(tile_dir, tile_file)))
+ except (OSError, lxml.etree.XMLSyntaxError):
+ pass
except FileNotFoundError:
pass
diff --git a/lib/utils/geometry.py b/lib/utils/geometry.py
index 2903bc56..366a433f 100644
--- a/lib/utils/geometry.py
+++ b/lib/utils/geometry.py
@@ -166,7 +166,7 @@ def _remove_duplicate_coordinates(coords_array):
return coords_array[keepers]
-def smooth_path(path, smoothness=100.0):
+def smooth_path(path, smoothness=1.0):
"""Smooth a path of coordinates.
Arguments:
@@ -178,6 +178,11 @@ def smooth_path(path, smoothness=100.0):
A list of Points.
"""
+ if smoothness == 0:
+ # s of exactly zero seems to indicate a default level of smoothing
+ # in splprep, so we'll just exit instead.
+ return path
+
# splprep blows up on duplicated consecutive points with "Invalid inputs"
coords = _remove_duplicate_coordinates(np.array(path))
num_points = len(coords)
@@ -188,7 +193,7 @@ def smooth_path(path, smoothness=100.0):
# the smoothed path and the original path is equal to the smoothness.
# In practical terms, if smoothness is 1mm, then the smoothed path can be
# up to 1mm away from the original path.
- s = num_points * smoothness ** 2
+ s = num_points * (smoothness ** 2)
# .T transposes the array (for some reason splprep expects
# [[x1, x2, ...], [y1, y2, ...]]
@@ -280,6 +285,9 @@ class Point:
def rotate(self, angle):
return self.__class__(self.x * math.cos(angle) - self.y * math.sin(angle), self.y * math.cos(angle) + self.x * math.sin(angle))
+ def scale(self, x_scale, y_scale):
+ return self.__class__(self.x * x_scale, self.y * y_scale)
+
def as_int(self):
return self.__class__(int(round(self.x)), int(round(self.y)))
diff --git a/lib/utils/list.py b/lib/utils/list.py
index 2bfe2cd7..efa3969e 100644
--- a/lib/utils/list.py
+++ b/lib/utils/list.py
@@ -1,8 +1,16 @@
-from random import randrange
+import random
-def poprandom(sequence):
- index = randrange(len(sequence))
+def _uniform_rng():
+ while True:
+ yield random.uniform(0, 1)
+
+
+_rng = _uniform_rng()
+
+
+def poprandom(sequence, rng=_rng):
+ index = int(round(next(rng) * (len(sequence) - 1)))
item = sequence[index]
# It's O(1) to pop the last item, and O(n) to pop any other item. So we'll
diff --git a/lib/utils/string.py b/lib/utils/string.py
index cb852ce3..e9204076 100644
--- a/lib/utils/string.py
+++ b/lib/utils/string.py
@@ -8,3 +8,10 @@ def string_to_floats(string, delimiter=","):
floats = string.split(delimiter)
return [float(num) for num in floats]
+
+
+def remove_suffix(string, suffix):
+ if string.endswith(suffix):
+ return string[:-len(suffix)]
+ else:
+ return string