summaryrefslogtreecommitdiff
path: root/lib/elements
diff options
context:
space:
mode:
Diffstat (limited to 'lib/elements')
-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
4 files changed, 220 insertions, 191 deletions
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: