summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/api/page_specs.py5
-rw-r--r--lib/elements/element.py65
-rw-r--r--lib/elements/fill_stitch.py28
-rw-r--r--lib/elements/satin_column.py280
-rw-r--r--lib/elements/stroke.py38
-rw-r--r--lib/extensions/base.py97
-rw-r--r--lib/extensions/lettering.py7
-rw-r--r--lib/extensions/params.py12
-rw-r--r--lib/extensions/select_elements.py8
-rw-r--r--lib/extensions/stroke_to_lpe_satin.py93
-rw-r--r--lib/extensions/troubleshoot.py24
-rw-r--r--lib/gui/simulator.py5
-rw-r--r--lib/metadata.py85
-rw-r--r--lib/stitches/meander_fill.py26
-rw-r--r--lib/stitches/running_stitch.py3
-rw-r--r--lib/svg/tags.py3
-rw-r--r--lib/tiles.py56
-rw-r--r--lib/update.py126
-rw-r--r--lib/utils/geometry.py13
-rw-r--r--lib/utils/smoothing.py8
20 files changed, 618 insertions, 364 deletions
diff --git a/lib/api/page_specs.py b/lib/api/page_specs.py
index ff9f7cf6..f7cec8c6 100644
--- a/lib/api/page_specs.py
+++ b/lib/api/page_specs.py
@@ -7,6 +7,7 @@ from flask import Blueprint, g, jsonify
page_specs = Blueprint('page_specs', __name__)
+
@page_specs.route('')
def get_page_specs():
@@ -18,5 +19,5 @@ def get_page_specs():
"pagecolor": metadata.document[1].get('pagecolor'),
"deskcolor": metadata.document[1].get('inkscape:deskcolor')
}
-
- return jsonify(page_specs) \ No newline at end of file
+
+ return jsonify(page_specs)
diff --git a/lib/elements/element.py b/lib/elements/element.py
index d3690b4c..269cbdc2 100644
--- a/lib/elements/element.py
+++ b/lib/elements/element.py
@@ -58,63 +58,6 @@ def param(*args, **kwargs):
class EmbroideryElement(object):
def __init__(self, node):
self.node = node
- self._update_legacy_params()
-
- def _update_legacy_params(self): # noqa: C901
- # update legacy embroider_ attributes to namespaced attributes
- legacy_attribs = False
- for attrib in self.node.attrib:
- if attrib.startswith('embroider_'):
- self.replace_legacy_param(attrib)
- legacy_attribs = True
-
- # convert legacy tie setting
- legacy_tie = self.get_param('ties', None)
- if legacy_tie == "True":
- self.set_param('ties', 0)
- elif legacy_tie == "False":
- self.set_param('ties', 3)
-
- # convert legacy fill_method
- legacy_fill_method = self.get_int_param('fill_method', None)
- if legacy_fill_method == 0:
- self.set_param('fill_method', 'auto_fill')
- elif legacy_fill_method == 1:
- self.set_param('fill_method', 'contour_fill')
- elif legacy_fill_method == 2:
- self.set_param('fill_method', 'guided_fill')
- elif legacy_fill_method == 3:
- self.set_param('fill_method', 'legacy_fill')
-
- # legacy satin method
- if self.get_boolean_param('e_stitch', False) is True:
- self.remove_param('e_stitch')
- self.set_param('satin_method', 'e_stitch')
-
- # default setting for fill_underlay has changed
- if legacy_attribs and not self.get_param('fill_underlay', ""):
- self.set_param('fill_underlay', False)
-
- # convert legacy stroke_method
- if self.get_style("stroke"):
- # manual stitch
- legacy_manual_stitch = self.get_boolean_param('manual_stitch', False)
- if legacy_manual_stitch is True:
- self.remove_param('manual_stitch')
- self.set_param('stroke_method', 'manual_stitch')
- # stroke_method
- legacy_stroke_method = self.get_int_param('stroke_method', None)
- if legacy_stroke_method == 0:
- self.set_param('stroke_method', 'running_stitch')
- elif legacy_stroke_method == 1:
- self.set_param('stroke_method', 'ripple_stitch')
- if (not self.get_param('stroke_method', None) and
- self.get_param('satin_column', False) is False and
- not self.node.style('stroke-dasharray')):
- self.set_param('stroke_method', 'zigzag_stitch')
- # if the stroke method is a zigzag-stitch but we are receiving a dashed line, set it to running stitch
- if self.get_param('stroke_method', None) == 'zigzag_stitch' and self.node.style('stroke-dasharray'):
- self.set_param('stroke_method', 'running_stitch')
@property
def id(self):
@@ -131,14 +74,6 @@ class EmbroideryElement(object):
params.append(prop.fget.param)
return params
- def replace_legacy_param(self, param):
- # remove "embroider_" prefix
- new_param = param[10:]
- if new_param in INKSTITCH_ATTRIBS:
- value = self.node.get(param, "").strip()
- self.set_param(param[10:], value)
- del self.node.attrib[param]
-
@cache
def get_param(self, param, default):
value = self.node.get(INKSTITCH_ATTRIBS[param], "").strip()
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/elements/satin_column.py b/lib/elements/satin_column.py
index 887aec01..42e3362c 100644
--- a/lib/elements/satin_column.py
+++ b/lib/elements/satin_column.py
@@ -14,6 +14,7 @@ from shapely import affinity as shaffinity
from shapely import geometry as shgeo
from shapely.ops import nearest_points
+from ..debug import debug
from ..i18n import _
from ..stitch_plan import StitchGroup
from ..stitches import running_stitch
@@ -774,140 +775,143 @@ class SatinColumn(EmbroideryElement):
offset_a = offset_a * scale
offset_b = offset_b * scale
- out1 = pos1 + (pos1 - pos2).unit() * offset_a
- out2 = pos2 + (pos2 - pos1).unit() * offset_b
+ # convert offset to float before using because it may be a numpy.float64
+ out1 = pos1 + (pos1 - pos2).unit() * float(offset_a)
+ out2 = pos2 + (pos2 - pos1).unit() * float(offset_b)
return out1, out2
- def walk(self, path, start_pos, start_index, distance):
- # Move <distance> pixels along <path>, which is a sequence of line
- # segments defined by points.
-
- # <start_index> is the index of the line segment in <path> that
- # we're currently on. <start_pos> is where along that line
- # segment we are. Return a new position and index.
-
- # print >> dbg, "walk", start_pos, start_index, distance
-
- pos = start_pos
- index = start_index
- last_index = len(path) - 1
- distance_remaining = distance
-
- while True:
- if index >= last_index:
- return pos, index
-
- segment_end = path[index + 1]
- segment = segment_end - pos
- segment_length = segment.length()
-
- if segment_length > distance_remaining:
- # our walk ends partway along this segment
- return pos + segment.unit() * distance_remaining, index
- else:
- # our walk goes past the end of this segment, so advance
- # one point
- index += 1
- distance_remaining -= segment_length
- pos = segment_end
+ def _stitch_distance(self, pos0, pos1, previous_pos0, previous_pos1):
+ """Return the distance from one stitch to the next."""
+ previous_stitch = previous_pos1 - previous_pos0
+ if previous_stitch.length() < 0.01:
+ return shgeo.LineString((pos0, pos1)).distance(shgeo.Point(previous_pos0))
+ else:
+ # Measure the distance at a right angle to the previous stitch, at
+ # the start and end of the stitch, and pick the biggest. If we're
+ # going around a curve, the points on the inside of the curve will
+ # be much closer together, and we only care about the distance on
+ # the outside of the curve.
+ #
+ # In this example with two horizontal stitches, we want the vertical
+ # separation between them.
+ # _________
+ # \_______/
+ normal = previous_stitch.unit().rotate_left()
+ d0 = pos0 - previous_pos0
+ d1 = pos1 - previous_pos1
+ return max(abs(d0 * normal), abs(d1 * normal))
+
+ @debug.time
def plot_points_on_rails(self, spacing, offset_px=(0, 0), offset_proportional=(0, 0), use_random=False
) -> typing.List[typing.Tuple[Point, Point]]:
# Take a section from each rail in turn, and plot out an equal number
# of points on both rails. Return the points plotted. The points will
# be contracted or expanded by offset using self.offset_points().
- # pre-cache ramdomised parameters to avoid property calls in loop
- if use_random:
- seed = prng.join_args(self.random_seed, "satin-points")
- offset_proportional_min = np.array(offset_proportional) - self.random_width_decrease
- offset_range = (self.random_width_increase + self.random_width_decrease)
- spacing_sigma = spacing * self.random_zigzag_spacing
+ processor = SatinProcessor(self, offset_px, offset_proportional, use_random)
pairs = []
- to_travel = 0
- cycle = 0
-
- for section0, section1 in self.flattened_sections:
- # Take one section at a time, delineated by the rungs. For each
- # one, we want to try to travel proportionately on each rail as
- # we go between stitches. For example, for the letter O, the
- # outside rail is longer than the inside rail. We need to travel
- # further on the outside rail between each stitch than we do
- # on the inside rail.
-
- pos0 = section0[0]
- pos1 = section1[0]
-
- len0 = shgeo.LineString(section0).length
- len1 = shgeo.LineString(section1).length
-
- last_index0 = len(section0) - 1
- last_index1 = len(section1) - 1
-
- if len0 == 0:
- continue
-
- ratio = len1 / len0
-
- index0 = 0
- index1 = 0
-
- while index0 < last_index0 and index1 < last_index1:
- check_stop_flag()
-
- # Each iteration of this outer loop is one stitch. Keep going
- # until we fall off the end of the section.
-
- old_center = shgeo.Point(x / 2 for x in (pos0 + pos1))
-
- while to_travel > 0 and index0 < last_index0 and index1 < last_index1:
- # In this loop, we inch along each rail a tiny bit per
- # iteration. The goal is to travel the requested spacing
- # amount along the _centerline_ between the two rails.
- #
- # Why not just travel the requested amount along the rails
- # themselves? Imagine a letter V. The distance we travel
- # along the rails themselves is much longer than the distance
- # between the horizontal stitches themselves:
- #
- # \______/
- # \____/
- # \__/
- # \/
- #
- # For more complicated rail shapes, the distance between each
- # stitch will vary as the angles of the rails vary. The
- # easiest way to compensate for this is to just go a tiny bit
- # at a time and see how far we went.
-
- # Note that this is 0.05 pixels, which is around 0.01mm, way
- # smaller than the resolution of an embroidery machine.
- pos0, index0 = self.walk(section0, pos0, index0, 0.05)
- pos1, index1 = self.walk(section1, pos1, index1, 0.05 * ratio)
-
- new_center = shgeo.Point(x/2 for x in (pos0 + pos1))
- to_travel -= new_center.distance(old_center)
- old_center = new_center
-
- if to_travel <= 0:
- if use_random:
- roll = prng.uniform_floats(seed, cycle)
- offset_prop = offset_proportional_min + roll[0:2] * offset_range
- to_travel = spacing + ((roll[2] - 0.5) * 2 * spacing_sigma)
- else:
- offset_prop = offset_proportional
- to_travel = spacing
-
- a, b = self.offset_points(pos0, pos1, offset_px, offset_prop)
- pairs.append((a, b))
- cycle += 1
-
- if to_travel > 0:
- a, b = self.offset_points(pos0, pos1, offset_px, offset_prop)
- pairs.append((a, b))
+ for i, (section0, section1) in enumerate(self.flattened_sections):
+ check_stop_flag()
+
+ if i == 0:
+ old_pos0 = section0[0]
+ old_pos1 = section1[0]
+ pairs.append(processor.process_points(old_pos0, old_pos1))
+
+ path0 = shgeo.LineString(section0)
+ path1 = shgeo.LineString(section1)
+
+ # Base the number of stitches in each section on the _longer_ of
+ # the two sections. Otherwise, things could get too sparse when one
+ # side is significantly longer (e.g. when going around a corner).
+ num_points = max(path0.length, path1.length) / spacing
+
+ # Section stitch spacing and the cursor are expressed as a fraction
+ # of the total length of the path, because we use normalized=True
+ # below.
+ section_stitch_spacing = 1.0 / num_points
+
+ # current_spacing, however, is in pixels.
+ spacing_multiple = processor.get_stitch_spacing_multiple()
+ current_spacing = spacing * spacing_multiple
+
+ # In all sections after the first, we need to figure out how far to
+ # travel before placing the first stitch.
+ distance = self._stitch_distance(section0[0], section1[0], old_pos0, old_pos1)
+ to_travel = (1 - min(distance / spacing, 1.0)) * section_stitch_spacing * spacing_multiple
+ debug.log(f"num_points: {num_points}, section_stitch_spacing: {section_stitch_spacing}, distance: {distance}, to_travel: {to_travel}")
+
+ cursor = 0
+ iterations = 0
+ while cursor + to_travel <= 1:
+ iterations += 1
+ pos0 = Point.from_shapely_point(path0.interpolate(cursor + to_travel, normalized=True))
+ pos1 = Point.from_shapely_point(path1.interpolate(cursor + to_travel, normalized=True))
+
+ # If the rails are parallel, then our stitch spacing will be
+ # perfect. If the rails are coming together or spreading apart,
+ # then we'll have to travel much further along the rails to get
+ # the right stitch spacing. Imagine a satin like the letter V:
+ #
+ # \______/
+ # \____/
+ # \__/
+ # \/
+ #
+ # In this case the stitches will be way too close together.
+ # We'll compensate for that here.
+ #
+ # We'll measure how far this stitch is from the previous one.
+ # If we went one third as far as we were expecting to, then
+ # we'll need to try again, this time travelling 3x as far as we
+ # originally tried.
+ #
+ # This works great for the V, but what if things change
+ # mid-stitch?
+ #
+ # \ /
+ # \ /
+ # \ /
+ # ||
+ #
+ # In this case, we may way overshoot. We can also undershoot
+ # for similar reasons. To deal with that, we'll revise our
+ # guess a second time. Two tries seems to be the sweet spot.
+ #
+ # In any case, we'll only revise if our stitch spacing is off by
+ # more than 5%.
+ if iterations <= 2:
+ distance = self._stitch_distance(pos0, pos1, old_pos0, old_pos1)
+ if abs((current_spacing - distance) / current_spacing) > 0.05:
+ # We'll revise to_travel then go back to the start of
+ # the loop and try again.
+ to_travel = (current_spacing / distance) * to_travel
+ if iterations == 1:
+ # Don't overshoot the end of this section on the
+ # first try. If we've gone too far, we want to have
+ # a chance to correct.
+ to_travel = min(to_travel, 1 - cursor)
+ continue
+
+ cursor += to_travel
+ spacing_multiple = processor.get_stitch_spacing_multiple()
+ to_travel = section_stitch_spacing * spacing_multiple
+ current_spacing = spacing * spacing_multiple
+
+ old_pos0 = pos0
+ old_pos1 = pos1
+ pairs.append(processor.process_points(pos0, pos1))
+ iterations = 0
+
+ # Add one last stitch at the end unless our previous stitch is already
+ # really close to the end.
+ if pairs and section0 and section1:
+ if self._stitch_distance(section0[-1], section1[-1], old_pos0, old_pos1) > 0.1 * PIXELS_PER_MM:
+ pairs.append(processor.process_points(section0[-1], section1[-1]))
return pairs
@@ -1153,3 +1157,37 @@ class SatinColumn(EmbroideryElement):
return []
return [patch]
+
+
+class SatinProcessor:
+ def __init__(self, satin, offset_px, offset_proportional, use_random):
+ self.satin = satin
+ self.use_random = use_random
+ self.offset_px = offset_px
+ self.offset_proportional = offset_proportional
+ self.random_zigzag_spacing = satin.random_zigzag_spacing
+
+ if use_random:
+ self.seed = prng.join_args(satin.random_seed, "satin-points")
+ self.offset_proportional_min = np.array(offset_proportional) - satin.random_width_decrease
+ self.offset_range = (satin.random_width_increase + satin.random_width_decrease)
+ self.cycle = 0
+
+ def process_points(self, pos0, pos1):
+ if self.use_random:
+ roll = prng.uniform_floats(self.seed, self.cycle)
+ self.cycle += 1
+ offset_prop = self.offset_proportional_min + roll[0:2] * self.offset_range
+ else:
+ offset_prop = self.offset_proportional
+
+ a, b = self.satin.offset_points(pos0, pos1, self.offset_px, offset_prop)
+ return a, b
+
+ def get_stitch_spacing_multiple(self):
+ if self.use_random:
+ roll = prng.uniform_floats(self.seed, self.cycle)
+ self.cycle += 1
+ return 1.0 + ((roll[0] - 0.5) * 2) * self.random_zigzag_spacing
+ else:
+ return 1.0
diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py
index 258bf737..fd5983d5 100644
--- a/lib/elements/stroke.py
+++ b/lib/elements/stroke.py
@@ -3,6 +3,8 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+from math import ceil
+
import shapely.geometry
from inkex import Transform
@@ -104,7 +106,7 @@ class Stroke(EmbroideryElement):
@property
@param('running_stitch_length_mm',
_('Running stitch length'),
- tooltip=_('Length of stitches in running stitch mode.'),
+ tooltip=_('Length of stitches. Stitches can be shorter according to the stitch tolerance setting.'),
unit='mm',
type='float',
select_items=[('stroke_method', 'running_stitch'), ('stroke_method', 'ripple_stitch')],
@@ -115,7 +117,7 @@ class Stroke(EmbroideryElement):
@property
@param('running_stitch_tolerance_mm',
- _('Running stitch tolerance'),
+ _('Stitch tolerance'),
tooltip=_('All stitches must be within this distance from the path. ' +
'A lower tolerance means stitches will be closer together. ' +
'A higher tolerance means sharp corners may be rounded.'),
@@ -128,6 +130,20 @@ class Stroke(EmbroideryElement):
return max(self.get_float_param("running_stitch_tolerance_mm", 0.2), 0.01)
@property
+ @param('max_stitch_length_mm',
+ _('Max stitch length'),
+ tooltip=_('Split stitches longer than this.'),
+ unit='mm',
+ type='float',
+ select_items=[('stroke_method', 'manual_stitch')],
+ sort_index=4)
+ def max_stitch_length(self):
+ max_length = self.get_float_param("max_stitch_length_mm", None)
+ if not max_length or max_length <= 0:
+ return
+ return max_length
+
+ @property
@param('zigzag_spacing_mm',
_('Zig-zag spacing (peak-to-peak)'),
tooltip=_('Length of stitches in zig-zag mode.'),
@@ -396,6 +412,21 @@ class Stroke(EmbroideryElement):
return StitchGroup(self.color, repeated_stitches, lock_stitches=self.lock_stitches, force_lock_stitches=self.force_lock_stitches)
+ def apply_max_stitch_length(self, path):
+ # apply max distances
+ max_len_path = [path[0]]
+ for points in zip(path[:-1], path[1:]):
+ line = shapely.geometry.LineString(points)
+ dist = line.length
+ if dist > self.max_stitch_length:
+ num_subsections = ceil(dist / self.max_stitch_length)
+ additional_points = [Point(coord.x, coord.y)
+ for coord in [line.interpolate((i/num_subsections), normalized=True)
+ for i in range(1, num_subsections + 1)]]
+ max_len_path.extend(additional_points)
+ max_len_path.append(points[1])
+ return max_len_path
+
def ripple_stitch(self):
return StitchGroup(
color=self.color,
@@ -422,6 +453,9 @@ class Stroke(EmbroideryElement):
path = [Point(x, y) for x, y in path]
# manual stitch
if self.stroke_method == 'manual_stitch':
+ if self.max_stitch_length:
+ path = self.apply_max_stitch_length(path)
+
if self.force_lock_stitches:
lock_stitches = self.lock_stitches
else:
diff --git a/lib/extensions/base.py b/lib/extensions/base.py
index 7b3c6f1c..e381e2c1 100644
--- a/lib/extensions/base.py
+++ b/lib/extensions/base.py
@@ -3,112 +3,33 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
-import json
import os
-import re
-from collections.abc import MutableMapping
-from lxml import etree
+import inkex
from lxml.etree import Comment
from stringcase import snakecase
-import inkex
-
from ..commands import is_command, layer_commands
from ..elements import EmbroideryElement, nodes_to_elements
from ..elements.clone import is_clone
from ..i18n import _
from ..marker import has_marker
+from ..metadata import InkStitchMetadata
from ..svg import generate_unique_id
from ..svg.tags import (CONNECTOR_TYPE, EMBROIDERABLE_TAGS, INKSCAPE_GROUPMODE,
NOT_EMBROIDERABLE_TAGS, SVG_CLIPPATH_TAG, SVG_DEFS_TAG,
SVG_GROUP_TAG, SVG_MASK_TAG)
-from ..utils.settings import DEFAULT_METADATA, global_settings
-
-SVG_METADATA_TAG = inkex.addNS("metadata", "svg")
-
-
-def strip_namespace(tag):
- """Remove xml namespace from a tag name.
-
- >>> {http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}namedview
- <<< namedview
- """
-
- match = re.match(r'^\{[^}]+\}(.+)$', tag)
-
- if match:
- return match.group(1)
- else:
- return tag
-
-
-class InkStitchMetadata(MutableMapping):
- """Helper class to get and set inkstitch-specific metadata attributes.
-
- Operates on a document and acts like a dict. Setting an item adds or
- updates a metadata element in the document. Getting an item retrieves
- a metadata element's text contents or None if an element by that name
- doesn't exist.
- """
-
- def __init__(self, document):
- super().__init__()
- self.document = document
- self.metadata = document.metadata
-
- for setting in DEFAULT_METADATA:
- if self[setting] is None:
- self[setting] = global_settings[f'default_{setting}']
+from ..update import update_inkstitch_document
- # Because this class inherints from MutableMapping, all we have to do is
- # implement these five methods and we get a full dict-like interface.
- def __setitem__(self, name, value):
- item = self._find_item(name)
- item.text = json.dumps(value)
-
- def _find_item(self, name, create=True):
- tag = inkex.addNS(name, "inkstitch")
- item = self.metadata.find(tag)
- if item is None and create:
- item = etree.SubElement(self.metadata, tag)
-
- return item
-
- def __getitem__(self, name):
- item = self._find_item(name)
-
- try:
- return json.loads(item.text)
- except (ValueError, TypeError):
- return None
-
- def __delitem__(self, name):
- item = self._find_item(name, create=False)
-
- if item is not None:
- self.metadata.remove(item)
-
- def __iter__(self):
- for child in self.metadata:
- if child.prefix == "inkstitch":
- yield strip_namespace(child.tag)
-
- def __len__(self):
- i = 0
- for i, item in enumerate(self):
- pass
-
- return i + 1
-
- def __json__(self):
- return dict(self)
-
-
-class InkstitchExtension(inkex.Effect):
+class InkstitchExtension(inkex.EffectExtension):
"""Base class for Inkstitch extensions. Not intended for direct use."""
+ def load(self, *args, **kwargs):
+ document = super().load(*args, **kwargs)
+ update_inkstitch_document(document)
+ return document
+
@classmethod
def name(cls):
return snakecase(cls.__name__)
diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py
index a396765b..b304a6f9 100644
--- a/lib/extensions/lettering.py
+++ b/lib/extensions/lettering.py
@@ -31,9 +31,11 @@ class LetteringFrame(wx.Frame):
DEFAULT_FONT = "small_font"
def __init__(self, *args, **kwargs):
- # begin wxGlade: MyFrame.__init__
self.group = kwargs.pop('group')
self.cancel_hook = kwargs.pop('on_cancel', None)
+ self.metadata = kwargs.pop('metadata', [])
+
+ # begin wxGlade: MyFrame.__init__
wx.Frame.__init__(self, None, wx.ID_ANY,
_("Ink/Stitch Lettering")
)
@@ -492,8 +494,9 @@ class Lettering(CommandsExtension):
return group
def effect(self):
+ metadata = self.get_inkstitch_metadata()
app = wx.App()
- frame = LetteringFrame(group=self.get_or_create_group(), on_cancel=self.cancel)
+ frame = LetteringFrame(group=self.get_or_create_group(), on_cancel=self.cancel, metadata=metadata)
# position left, center
current_screen = wx.Display.GetFromPoint(wx.GetMousePosition())
diff --git a/lib/extensions/params.py b/lib/extensions/params.py
index bf01153b..fb13c223 100644
--- a/lib/extensions/params.py
+++ b/lib/extensions/params.py
@@ -478,9 +478,11 @@ class SettingsFrame(wx.Frame):
lc = wx.Locale()
lc.Init(wx.LANGUAGE_DEFAULT)
- # begin wxGlade: MyFrame.__init__
self.tabs_factory = kwargs.pop('tabs_factory', [])
self.cancel_hook = kwargs.pop('on_cancel', None)
+ self.metadata = kwargs.pop('metadata', [])
+
+ # begin wxGlade: MyFrame.__init__
wx.Frame.__init__(self, None, wx.ID_ANY,
_("Embroidery Params")
)
@@ -672,7 +674,8 @@ class Params(InkstitchExtension):
classes.append(FillStitch)
if element.get_style("stroke") is not None:
classes.append(Stroke)
- classes.append(SatinColumn)
+ if len(element.path) > 1:
+ classes.append(SatinColumn)
return classes
def get_nodes_by_class(self):
@@ -785,8 +788,11 @@ class Params(InkstitchExtension):
def effect(self):
try:
app = wx.App()
+ metadata = self.get_inkstitch_metadata()
frame = SettingsFrame(
- tabs_factory=self.create_tabs, on_cancel=self.cancel)
+ tabs_factory=self.create_tabs,
+ on_cancel=self.cancel,
+ metadata=metadata)
# position left, center
current_screen = wx.Display.GetFromPoint(wx.GetMousePosition())
diff --git a/lib/extensions/select_elements.py b/lib/extensions/select_elements.py
index 8fa9ca9d..896e04b0 100644
--- a/lib/extensions/select_elements.py
+++ b/lib/extensions/select_elements.py
@@ -21,6 +21,7 @@ class SelectElements(InkstitchExtension):
pars.add_argument("--info", type=str, dest="info")
pars.add_argument("--select-running-stitch", type=Boolean, dest="running", default=False)
+ pars.add_argument("--running-stitch-condition", type=str, dest="running_stitch_condition", default="all")
pars.add_argument("--select-ripples", type=Boolean, dest="ripples", default=False)
pars.add_argument("--select-zigzag", type=Boolean, dest="zigzag", default=False)
pars.add_argument("--select-manual", type=Boolean, dest="manual", default=False)
@@ -101,7 +102,7 @@ class SelectElements(InkstitchExtension):
def _select_stroke(self, element):
select = False
method = element.stroke_method
- if self.options.running and method == 'running_stitch':
+ if self.options.running and method == 'running_stitch' and self._running_condition(element):
select = True
if self.options.ripples and method == 'ripple_stitch':
select = True
@@ -130,6 +131,11 @@ class SelectElements(InkstitchExtension):
select = True
return select
+ def _running_condition(self, element):
+ element_id = element.node.get_id() or ''
+ conditions = {'all': True, 'autorun-top': element_id.startswith('autorun'), 'autorun-underpath': element_id.startswith('underpath')}
+ return conditions[self.options.running_stitch_condition]
+
def _select_fill_underlay(self, element):
underlay = {'all': True, 'no': not element.fill_underlay, 'yes': element.fill_underlay}
return underlay[self.options.fill_underlay]
diff --git a/lib/extensions/stroke_to_lpe_satin.py b/lib/extensions/stroke_to_lpe_satin.py
index 4052207a..d162539b 100644
--- a/lib/extensions/stroke_to_lpe_satin.py
+++ b/lib/extensions/stroke_to_lpe_satin.py
@@ -26,6 +26,7 @@ class StrokeToLpeSatin(InkstitchExtension):
self.arg_parser.add_argument("-l", "--length", type=float, default=15, dest="length")
self.arg_parser.add_argument("-t", "--stretched", type=inkex.Boolean, default=False, dest="stretched")
self.arg_parser.add_argument("-r", "--rungs", type=inkex.Boolean, default=False, dest="add_rungs")
+ self.arg_parser.add_argument("-s", "--path-specific", type=inkex.Boolean, default=True, dest="path_specific")
def effect(self):
if not self.svg.selection or not self.get_elements():
@@ -52,48 +53,79 @@ class StrokeToLpeSatin(InkstitchExtension):
pattern_path = pattern_obj.get_path(self.options.add_rungs, min_width, max_width, length, self.svg.unit)
pattern_node_type = pattern_obj.node_types
+ if not self.options.path_specific:
+ lpe = self._create_lpe_element(pattern, pattern_path, pattern_node_type)
+
+ for element in self.elements:
+ if self.options.path_specific:
+ lpe = self._create_lpe_element(pattern, pattern_path, pattern_node_type, element)
+ if isinstance(element, SatinColumn):
+ self._process_satin_column(element, lpe)
+ elif isinstance(element, Stroke):
+ self._process_stroke(element, lpe)
+
+ def _create_lpe_element(self, pattern, pattern_path, pattern_node_type, element=None):
+ # define id for the lpe path
+ if not element:
+ lpe_id = f'inkstitch-effect-{pattern}'
+ else:
+ lpe_id = f'inkstitch-effect-{pattern}-{element.id}'
+
+ # it is possible, that there is already a path effect with this id, if so, use it
+ previous_lpe = self.svg.getElementById(lpe_id)
+ if previous_lpe is not None:
+ return previous_lpe
+
# the lpe 'pattern along path' has two options to repeat the pattern, get user input
copy_type = 'repeated' if self.options.stretched is False else 'repeated_stretched'
+ lpe = inkex.PathEffect(attrib={'id': lpe_id,
+ 'effect': "skeletal",
+ 'is_visible': "true",
+ 'lpeversion': "1",
+ 'pattern': pattern_path,
+ 'copytype': copy_type,
+ 'prop_scale': "1",
+ 'scale_y_rel': "false",
+ 'spacing': "0",
+ 'normal_offset': "0",
+ 'tang_offset': "0",
+ 'prop_units': "false",
+ 'vertical_pattern': "false",
+ 'hide_knot': "false",
+ 'fuse_tolerance': "0.02",
+ 'pattern-nodetypes': pattern_node_type})
# add the path effect element to the defs section
- self.lpe = inkex.PathEffect(attrib={'id': f'inkstitch-effect-{pattern}',
- 'effect': "skeletal",
- 'is_visible': "true",
- 'lpeversion': "1",
- 'pattern': pattern_path,
- 'copytype': copy_type,
- 'prop_scale': "1",
- 'scale_y_rel': "false",
- 'spacing': "0",
- 'normal_offset': "0",
- 'tang_offset': "0",
- 'prop_units': "false",
- 'vertical_pattern': "false",
- 'hide_knot': "false",
- 'fuse_tolerance': "0.02",
- 'pattern-nodetypes': pattern_node_type})
- self.svg.defs.add(self.lpe)
+ self.svg.defs.add(lpe)
+ return lpe
- for element in self.elements:
- if isinstance(element, SatinColumn):
- self._process_satin_column(element)
- elif isinstance(element, Stroke):
- self._process_stroke(element)
+ def _process_stroke(self, element, lpe):
+ element = self._ensure_path_element(element, lpe)
- def _process_stroke(self, element):
previous_effects = element.node.get(PATH_EFFECT, None)
if not previous_effects:
- element.node.set(PATH_EFFECT, self.lpe.get_id(as_url=1))
+ element.node.set(PATH_EFFECT, lpe.get_id(as_url=1))
element.node.set(ORIGINAL_D, element.node.get('d', ''))
else:
- url = previous_effects + ';' + self.lpe.get_id(as_url=1)
+ url = previous_effects + ';' + lpe.get_id(as_url=1)
element.node.set(PATH_EFFECT, url)
element.node.pop('d')
element.set_param('satin_column', 'true')
element.node.style['stroke-width'] = self.svg.viewport_to_unit('0.756')
- def _process_satin_column(self, element):
+ def _ensure_path_element(self, element, lpe):
+ # elements other than paths (rectangle, circles, etc.) can be handled by inkscape for lpe
+ # but they are way easier to handle for us if we turn them into paths
+ if element.node.TAG == 'path':
+ return element
+
+ path_element = element.node.to_path_element()
+ parent = element.node.getparent()
+ parent.replace(element.node, path_element)
+ return Stroke(path_element)
+
+ def _process_satin_column(self, element, lpe):
current_effects = element.node.get(PATH_EFFECT, None)
# there are possibly multiple path effects, let's check if inkstitch-effect is among them
if not current_effects or 'inkstitch-effect' not in current_effects:
@@ -106,10 +138,11 @@ class StrokeToLpeSatin(InkstitchExtension):
inkstitch_effect = current_effects[inkstitch_effect_position][1:]
# get the path effect element
old_effect_element = self.svg.getElementById(inkstitch_effect)
- # remove the old inkstitch-effect
- old_effect_element.getparent().remove(old_effect_element)
- # update the path effect link
- current_effects[inkstitch_effect_position] = self.lpe.get_id(as_url=1)
+ # remove the old inkstitch-effect if it is path specific
+ if inkstitch_effect.endswith(element.id):
+ old_effect_element.getparent().remove(old_effect_element)
+ # update path effect link
+ current_effects[inkstitch_effect_position] = lpe.get_id(as_url=1)
element.node.set(PATH_EFFECT, ';'.join(current_effects))
element.node.pop('d')
diff --git a/lib/extensions/troubleshoot.py b/lib/extensions/troubleshoot.py
index f7d979e7..fdc7fa9e 100644
--- a/lib/extensions/troubleshoot.py
+++ b/lib/extensions/troubleshoot.py
@@ -7,21 +7,25 @@ import textwrap
import inkex
-from .base import InkstitchExtension
from ..commands import add_layer_commands
from ..elements.validation import (ObjectTypeWarning, ValidationError,
ValidationWarning)
from ..i18n import _
+from ..svg import PIXELS_PER_MM
from ..svg.path import get_correction_transform
-from ..svg.tags import (INKSCAPE_GROUPMODE, INKSCAPE_LABEL, SODIPODI_ROLE)
+from ..svg.tags import INKSCAPE_GROUPMODE, INKSCAPE_LABEL, SODIPODI_ROLE
+from .base import InkstitchExtension
class Troubleshoot(InkstitchExtension):
- def effect(self):
+ def __init__(self, *args, **kwargs):
+ InkstitchExtension.__init__(self, *args, **kwargs)
+ self.arg_parser.add_argument("-p", "--pointer-size", type=float, default=5, dest="pointer_size_mm")
+ self.arg_parser.add_argument("-f", "--font-size", type=float, default=2, dest="font_size_mm")
+ def effect(self):
self.create_troubleshoot_layer()
-
problem_types = {'error': set(), 'warning': set(), 'type_warning': set()}
if self.get_elements(True):
@@ -50,6 +54,8 @@ class Troubleshoot(InkstitchExtension):
def insert_pointer(self, problem):
correction_transform = get_correction_transform(self.troubleshoot_layer)
+ pointer_size = self.options.pointer_size_mm * PIXELS_PER_MM
+ font_size = self.options.font_size_mm * PIXELS_PER_MM
if isinstance(problem, ValidationWarning):
fill_color = "#ffdd00"
@@ -61,12 +67,14 @@ class Troubleshoot(InkstitchExtension):
fill_color = "#ff9900"
layer = self.type_warning_group
- pointer_style = "stroke:#000000;stroke-width:0.2;fill:%s;" % (fill_color)
- text_style = "fill:%s;stroke:#000000;stroke-width:0.2;font-size:8px;text-align:center;text-anchor:middle" % (fill_color)
+ pointer_style = f'stroke:#000000;stroke-width:0.1;fill:{ fill_color }'
+ text_style = f'fill:{ fill_color };stroke:#000000;stroke-width:0.1;font-size:{ font_size }px;text-align:center;text-anchor:middle'
+ pointer_path = f'm {problem.position.x},{problem.position.y} {pointer_size / 5},{pointer_size} ' \
+ f'h -{pointer_size / 2.5} l {pointer_size / 5},-{pointer_size}'
path = inkex.PathElement(attrib={
"id": self.uniqueId("inkstitch__invalid_pointer__"),
- "d": "m %s,%s 4,20 h -8 l 4,-20" % (problem.position.x, problem.position.y),
+ "d": pointer_path,
"style": pointer_style,
INKSCAPE_LABEL: _('Invalid Pointer'),
"transform": correction_transform
@@ -76,7 +84,7 @@ class Troubleshoot(InkstitchExtension):
text = inkex.TextElement(attrib={
INKSCAPE_LABEL: _('Description'),
"x": str(problem.position.x),
- "y": str(float(problem.position.y) + 30),
+ "y": str(float(problem.position.y) + pointer_size + font_size),
"transform": correction_transform,
"style": text_style
})
diff --git a/lib/gui/simulator.py b/lib/gui/simulator.py
index d9f51b48..2c1ccc2a 100644
--- a/lib/gui/simulator.py
+++ b/lib/gui/simulator.py
@@ -805,7 +805,10 @@ class SimulatorPreview(Thread):
return
if patches and not self.refresh_needed.is_set():
- stitch_plan = stitch_groups_to_stitch_plan(patches)
+ metadata = self.parent.metadata
+ collapse_len = metadata['collapse_len_mm']
+ min_stitch_len = metadata['min_stitch_len_mm']
+ stitch_plan = stitch_groups_to_stitch_plan(patches, collapse_len=collapse_len, min_stitch_len=min_stitch_len)
# GUI stuff needs to happen in the main thread, so we ask the main
# thread to call refresh_simulator().
diff --git a/lib/metadata.py b/lib/metadata.py
new file mode 100644
index 00000000..837fbf00
--- /dev/null
+++ b/lib/metadata.py
@@ -0,0 +1,85 @@
+import json
+import re
+from collections.abc import MutableMapping
+
+import inkex
+from lxml import etree
+
+from .utils.settings import DEFAULT_METADATA, global_settings
+
+
+def strip_namespace(tag):
+ """Remove xml namespace from a tag name.
+
+ >>> {http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}namedview
+ <<< namedview
+ """
+
+ match = re.match(r'^\{[^}]+\}(.+)$', tag)
+
+ if match:
+ return match.group(1)
+ else:
+ return tag
+
+
+class InkStitchMetadata(MutableMapping):
+ """Helper class to get and set inkstitch-specific metadata attributes.
+
+ Operates on a document and acts like a dict. Setting an item adds or
+ updates a metadata element in the document. Getting an item retrieves
+ a metadata element's text contents or None if an element by that name
+ doesn't exist.
+ """
+
+ def __init__(self, document):
+ super().__init__()
+ self.document = document
+ self.metadata = document.metadata
+
+ for setting in DEFAULT_METADATA:
+ if self[setting] is None:
+ self[setting] = global_settings[f'default_{setting}']
+
+ # Because this class inherints from MutableMapping, all we have to do is
+ # implement these five methods and we get a full dict-like interface.
+ def __setitem__(self, name, value):
+ item = self._find_item(name)
+ item.text = json.dumps(value)
+
+ def _find_item(self, name, create=True):
+ tag = inkex.addNS(name, "inkstitch")
+ item = self.metadata.find(tag)
+ if item is None and create:
+ item = etree.SubElement(self.metadata, tag)
+
+ return item
+
+ def __getitem__(self, name):
+ item = self._find_item(name)
+
+ try:
+ return json.loads(item.text)
+ except (ValueError, TypeError):
+ return None
+
+ def __delitem__(self, name):
+ item = self._find_item(name, create=False)
+
+ if item is not None:
+ self.metadata.remove(item)
+
+ def __iter__(self):
+ for child in self.metadata:
+ if child.prefix == "inkstitch":
+ yield strip_namespace(child.tag)
+
+ def __len__(self):
+ i = 0
+ for i, item in enumerate(self):
+ pass
+
+ return i + 1
+
+ def __json__(self):
+ return dict(self)
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..8ce0c8a2 100644
--- a/lib/svg/tags.py
+++ b/lib/svg/tags.py
@@ -27,6 +27,7 @@ SVG_IMAGE_TAG = inkex.addNS('image', 'svg')
SVG_CLIPPATH_TAG = inkex.addNS('clipPath', 'svg')
SVG_MASK_TAG = inkex.addNS('mask', 'svg')
+SVG_METADATA_TAG = inkex.addNS("metadata", "svg")
INKSCAPE_LABEL = inkex.addNS('label', 'inkscape')
INKSCAPE_GROUPMODE = inkex.addNS('groupmode', 'inkscape')
CONNECTION_START = inkex.addNS('connection-start', 'inkscape')
@@ -81,6 +82,7 @@ inkstitch_attribs = [
'reverse',
'meander_pattern',
'meander_scale_percent',
+ 'meander_angle',
'expand_mm',
'fill_underlay',
'fill_underlay_angle',
@@ -97,6 +99,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/update.py b/lib/update.py
new file mode 100644
index 00000000..cba6f671
--- /dev/null
+++ b/lib/update.py
@@ -0,0 +1,126 @@
+from inkex import errormsg
+
+from .i18n import _
+from .elements import EmbroideryElement
+from .metadata import InkStitchMetadata
+from .svg.tags import INKSTITCH_ATTRIBS
+
+INKSTITCH_SVG_VERSION = 1
+
+
+def update_inkstitch_document(svg):
+ document = svg.getroot()
+ # get the inkstitch svg version from the document
+ search_string = "//*[local-name()='inkstitch_svg_version']//text()"
+ file_version = document.findone(search_string)
+ try:
+ file_version = int(file_version)
+ except (TypeError, ValueError):
+ file_version = 0
+
+ if file_version == INKSTITCH_SVG_VERSION:
+ return
+
+ if file_version > INKSTITCH_SVG_VERSION:
+ errormsg(_("This document was created with a newer Version of Ink/Stitch. "
+ "It is possible that not everything works as expected.\n\n"
+ "Please update your Ink/Stitch version: https://inkstitch.org/docs/install/"))
+ # they may not want to be bothered with this info everytime they call an inkstitch extension
+ # let's udowngrade the file version number
+ _update_inkstitch_svg_version(svg)
+ else:
+ # this document is either a new document or it is outdated
+ # if we cannot find any inkstitch attribute in the document, we assume that this is a new document which doesn't need to be updated
+ search_string = "//*[namespace-uri()='http://inkstitch.org/namespace' or " \
+ "@*[namespace-uri()='http://inkstitch.org/namespace'] or " \
+ "@*[starts-with(name(), 'embroider_')]]"
+ inkstitch_element = document.findone(search_string)
+ if inkstitch_element is None:
+ _update_inkstitch_svg_version(svg)
+ return
+
+ # update elements
+ for element in document.iterdescendants():
+ # We are just checking for params and update them.
+ # No need to check for specific stitch types at this point
+ update_legacy_params(EmbroideryElement(element), file_version, INKSTITCH_SVG_VERSION)
+ _update_inkstitch_svg_version(svg)
+
+
+def _update_inkstitch_svg_version(svg):
+ # set inkstitch svg version
+ metadata = InkStitchMetadata(svg.getroot())
+ metadata['inkstitch_svg_version'] = INKSTITCH_SVG_VERSION
+
+
+def update_legacy_params(element, file_version, inkstitch_svg_version):
+ for version in range(file_version + 1, inkstitch_svg_version + 1):
+ _update_to(version, element)
+
+
+def _update_to(version, element):
+ if version == 1:
+ _update_to_one(element)
+
+
+def _update_to_one(element): # noqa: C901
+ # update legacy embroider_ attributes to namespaced attributes
+ legacy_attribs = False
+ for attrib in element.node.attrib:
+ if attrib.startswith('embroider_'):
+ _replace_legacy_embroider_param(element, attrib)
+ legacy_attribs = True
+
+ # convert legacy tie setting
+ legacy_tie = element.get_param('ties', None)
+ if legacy_tie == "True":
+ element.set_param('ties', 0)
+ elif legacy_tie == "False":
+ element.set_param('ties', 3)
+
+ # convert legacy fill_method
+ legacy_fill_method = element.get_int_param('fill_method', None)
+ if legacy_fill_method == 0:
+ element.set_param('fill_method', 'auto_fill')
+ elif legacy_fill_method == 1:
+ element.set_param('fill_method', 'contour_fill')
+ elif legacy_fill_method == 2:
+ element.set_param('fill_method', 'guided_fill')
+ elif legacy_fill_method == 3:
+ element.set_param('fill_method', 'legacy_fill')
+
+ # legacy satin method
+ if element.get_boolean_param('e_stitch', False) is True:
+ element.remove_param('e_stitch')
+ element.set_param('satin_method', 'e_stitch')
+
+ # default setting for fill_underlay has changed
+ if legacy_attribs and not element.get_param('fill_underlay', ""):
+ element.set_param('fill_underlay', False)
+
+ # convert legacy stroke_method
+ if element.get_style("stroke"):
+ # manual stitch
+ legacy_manual_stitch = element.get_boolean_param('manual_stitch', False)
+ if legacy_manual_stitch is True:
+ element.remove_param('manual_stitch')
+ element.set_param('stroke_method', 'manual_stitch')
+ # stroke_method
+ legacy_stroke_method = element.get_int_param('stroke_method', None)
+ if legacy_stroke_method == 0:
+ element.set_param('stroke_method', 'running_stitch')
+ elif legacy_stroke_method == 1:
+ element.set_param('stroke_method', 'ripple_stitch')
+ if (not element.get_param('stroke_method', None) and
+ element.get_param('satin_column', False) is False and
+ not element.node.style('stroke-dasharray')):
+ element.set_param('stroke_method', 'zigzag_stitch')
+
+
+def _replace_legacy_embroider_param(element, param):
+ # remove "embroider_" prefix
+ new_param = param[10:]
+ if new_param in INKSTITCH_ATTRIBS:
+ value = element.node.get(param, "").strip()
+ element.set_param(param[10:], value)
+ del element.node.attrib[param]
diff --git a/lib/utils/geometry.py b/lib/utils/geometry.py
index 8f34c467..7434ae27 100644
--- a/lib/utils/geometry.py
+++ b/lib/utils/geometry.py
@@ -4,7 +4,9 @@
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import math
+import typing
+import numpy
from shapely.geometry import LineString, LinearRing, MultiLineString, Polygon, MultiPolygon, MultiPoint, GeometryCollection
from shapely.geometry import Point as ShapelyPoint
@@ -148,9 +150,9 @@ def cut_path(points, length):
class Point:
- def __init__(self, x: float, y: float):
- self.x = x
- self.y = y
+ def __init__(self, x: typing.Union[float, numpy.float64], y: typing.Union[float, numpy.float64]):
+ self.x = float(x)
+ self.y = float(y)
@classmethod
def from_shapely_point(cls, point):
@@ -203,13 +205,14 @@ class Point:
return "%s(%s,%s)" % (type(self), self.x, self.y)
def length(self):
- return math.sqrt(math.pow(self.x, 2.0) + math.pow(self.y, 2.0))
+ return (self.x ** 2 + self.y ** 2) ** 0.5
def distance(self, other):
return (other - self).length()
def unit(self):
- return self.mul(1.0 / self.length())
+ length = self.length()
+ return self.__class__(self.x / length, self.y / length)
def angle(self):
return math.atan2(self.y, self.x)
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]