diff options
Diffstat (limited to 'lib/elements')
| -rw-r--r-- | lib/elements/element.py | 65 | ||||
| -rw-r--r-- | lib/elements/fill_stitch.py | 28 | ||||
| -rw-r--r-- | lib/elements/satin_column.py | 280 | ||||
| -rw-r--r-- | lib/elements/stroke.py | 38 |
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: |
