diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/api/page_specs.py | 5 | ||||
| -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 | ||||
| -rw-r--r-- | lib/extensions/base.py | 97 | ||||
| -rw-r--r-- | lib/extensions/lettering.py | 7 | ||||
| -rw-r--r-- | lib/extensions/params.py | 12 | ||||
| -rw-r--r-- | lib/extensions/select_elements.py | 8 | ||||
| -rw-r--r-- | lib/extensions/stroke_to_lpe_satin.py | 93 | ||||
| -rw-r--r-- | lib/extensions/troubleshoot.py | 24 | ||||
| -rw-r--r-- | lib/gui/simulator.py | 5 | ||||
| -rw-r--r-- | lib/metadata.py | 85 | ||||
| -rw-r--r-- | lib/stitches/meander_fill.py | 26 | ||||
| -rw-r--r-- | lib/stitches/running_stitch.py | 3 | ||||
| -rw-r--r-- | lib/svg/tags.py | 3 | ||||
| -rw-r--r-- | lib/tiles.py | 56 | ||||
| -rw-r--r-- | lib/update.py | 126 | ||||
| -rw-r--r-- | lib/utils/geometry.py | 13 | ||||
| -rw-r--r-- | lib/utils/smoothing.py | 8 |
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] |
