diff options
Diffstat (limited to 'lib/extensions')
| -rw-r--r-- | lib/extensions/__init__.py | 9 | ||||
| -rw-r--r-- | lib/extensions/break_apart.py | 68 | ||||
| -rw-r--r-- | lib/extensions/convert_to_satin.py | 60 | ||||
| -rw-r--r-- | lib/extensions/import_threadlist.py | 102 | ||||
| -rw-r--r-- | lib/extensions/remove_embroidery_settings.py | 8 | ||||
| -rw-r--r-- | lib/extensions/stitch_plan_preview.py | 24 | ||||
| -rw-r--r-- | lib/extensions/zip.py | 56 |
7 files changed, 297 insertions, 30 deletions
diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index 38276a64..6f9ef5c4 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -1,9 +1,11 @@ from auto_satin import AutoSatin +from break_apart import BreakApart from convert_to_satin import ConvertToSatin from cut_satin import CutSatin from embroider import Embroider from flip import Flip from global_commands import GlobalCommands +from import_threadlist import ImportThreadlist from input import Input from install import Install from layer_commands import LayerCommands @@ -15,10 +17,11 @@ from params import Params from print_pdf import Print from remove_embroidery_settings import RemoveEmbroiderySettings from simulate import Simulate +from stitch_plan_preview import StitchPlanPreview from zip import Zip - __all__ = extensions = [Embroider, + StitchPlanPreview, Install, Params, Print, @@ -35,4 +38,6 @@ __all__ = extensions = [Embroider, AutoSatin, Lettering, Troubleshoot, - RemoveEmbroiderySettings] + RemoveEmbroiderySettings, + BreakApart, + ImportThreadlist] diff --git a/lib/extensions/break_apart.py b/lib/extensions/break_apart.py new file mode 100644 index 00000000..625ace55 --- /dev/null +++ b/lib/extensions/break_apart.py @@ -0,0 +1,68 @@ +from copy import deepcopy + +from shapely.geometry import Polygon + +import inkex + +from ..elements import AutoFill, Fill +from ..i18n import _ +from ..svg import get_correction_transform +from .base import InkstitchExtension + + +class BreakApart(InkstitchExtension): + def effect(self): # noqa: C901 + if not self.get_elements(): + return + + if not self.selected: + inkex.errormsg(_("Please select one or more fill areas to break apart.")) + return + + for element in self.elements: + if not isinstance(element, AutoFill) and not isinstance(element, Fill): + continue + if len(element.paths) <= 1: + continue + + polygons = [] + multipolygons = [] + holes = [] + + for path in element.paths: + polygons.append(Polygon(path)) + + # sort paths by size and convert to polygons + polygons.sort(key=lambda polygon: polygon.area, reverse=True) + + for shape in polygons: + if shape in holes: + continue + polygon_list = [shape] + + for other in polygons: + if shape != other and shape.contains(other) and other not in holes: + # check if "other" is inside a hole, before we add it to the list + if any(p.contains(other) for p in polygon_list[1:]): + continue + polygon_list.append(other) + holes.append(other) + multipolygons.append(polygon_list) + self.element_to_nodes(multipolygons, element) + + def element_to_nodes(self, multipolygons, element): + for polygons in multipolygons: + el = deepcopy(element) + d = "" + for polygon in polygons: + # copy element and replace path + el.node.set('id', self.uniqueId(element.node.get('id') + "_")) + d += "M" + for x, y in polygon.exterior.coords: + d += "%s,%s " % (x, y) + d += " " + d += "Z" + el.node.set('d', d) + el.node.set('transform', get_correction_transform(element.node)) + element.node.getparent().insert(0, el.node) + element.node.getparent().remove(element.node) diff --git a/lib/extensions/convert_to_satin.py b/lib/extensions/convert_to_satin.py index 1227b207..e2b287dd 100644 --- a/lib/extensions/convert_to_satin.py +++ b/lib/extensions/convert_to_satin.py @@ -1,16 +1,16 @@ -from copy import deepcopy -from itertools import chain, groupby import math +from itertools import chain, groupby -import inkex -from numpy import diff, sign, setdiff1d import numpy +from numpy import diff, setdiff1d, sign from shapely import geometry as shgeo +import inkex + from ..elements import Stroke from ..i18n import _ -from ..svg import get_correction_transform, PIXELS_PER_MM -from ..svg.tags import SVG_PATH_TAG +from ..svg import PIXELS_PER_MM, get_correction_transform +from ..svg.tags import INKSTITCH_ATTRIBS, SVG_PATH_TAG from ..utils import Point from .base import InkstitchExtension @@ -49,23 +49,31 @@ class ConvertToSatin(InkstitchExtension): # ignore paths with just one point -- they're not visible to the user anyway continue - self.fix_loop(path) + for satin in self.convert_path_to_satins(path, element.stroke_width, style_args, correction_transform, path_style): + parent.insert(index, satin) + index += 1 - try: - rails, rungs = self.path_to_satin(path, element.stroke_width, style_args) - except SelfIntersectionError: - inkex.errormsg( - _("Cannot convert %s to a satin column because it intersects itself. Try breaking it up into multiple paths.") % - element.node.get('id')) + parent.remove(element.node) - # revert any changes we've made - self.document = deepcopy(self.original_document) + def convert_path_to_satins(self, path, stroke_width, style_args, correction_transform, path_style, depth=0): + try: + rails, rungs = self.path_to_satin(path, stroke_width, style_args) + yield self.satin_to_svg_node(rails, rungs, correction_transform, path_style) + except SelfIntersectionError: + # The path intersects itself. Split it in two and try doing the halves + # individually. - return + if depth >= 20: + # At this point we're slicing the path way too small and still + # getting nowhere. Just give up on this section of the path. + return - parent.insert(index, self.satin_to_svg_node(rails, rungs, correction_transform, path_style)) + half = int(len(path) / 2.0) + halves = [path[:half + 1], path[half:]] - parent.remove(element.node) + for path in halves: + for satin in self.convert_path_to_satins(path, stroke_width, style_args, correction_transform, path_style, depth=depth + 1): + yield satin def fix_loop(self, path): if path[0] == path[-1]: @@ -107,13 +115,16 @@ class ConvertToSatin(InkstitchExtension): return args def path_to_satin(self, path, stroke_width, style_args): + if Point(*path[0]).distance(Point(*path[-1])) < 1: + raise SelfIntersectionError() + path = shgeo.LineString(path) left_rail = path.parallel_offset(stroke_width / 2.0, 'left', **style_args) right_rail = path.parallel_offset(stroke_width / 2.0, 'right', **style_args) if not isinstance(left_rail, shgeo.LineString) or \ - not isinstance(right_rail, shgeo.LineString): + not isinstance(right_rail, shgeo.LineString): # If the parallel offsets come out as anything but a LineString, that means the # path intersects itself, when taking its stroke width into consideration. See # the last example for parallel_offset() in the Shapely documentation: @@ -159,6 +170,13 @@ class ConvertToSatin(InkstitchExtension): # The dot product of two vectors is |v1| * |v2| * cos(angle). # These are unit vectors, so their magnitudes are 1. cos_angle_between = prev_direction * direction + + # Clamp to the valid range for a cosine. The above _should_ + # already be in this range, but floating point inaccuracy can + # push it outside the range causing math.acos to throw + # ValueError ("math domain error"). + cos_angle_between = max(-1.0, min(1.0, cos_angle_between)) + angle = abs(math.degrees(math.acos(cos_angle_between))) # Use the square of the angle, measured in degrees. @@ -248,7 +266,7 @@ class ConvertToSatin(InkstitchExtension): # arbitrarily rejects the rung if there was one less than 2 # millimeters before this one. if last_rung_center is not None and \ - (rung_center - last_rung_center).length() < 2 * PIXELS_PER_MM: + (rung_center - last_rung_center).length() < 2 * PIXELS_PER_MM: continue else: last_rung_center = rung_center @@ -292,6 +310,6 @@ class ConvertToSatin(InkstitchExtension): "style": path_style, "transform": correction_transform, "d": d, - "embroider_satin_column": "true", + INKSTITCH_ATTRIBS['satin_column']: "true", } ) diff --git a/lib/extensions/import_threadlist.py b/lib/extensions/import_threadlist.py new file mode 100644 index 00000000..d31c0d69 --- /dev/null +++ b/lib/extensions/import_threadlist.py @@ -0,0 +1,102 @@ +import os +import re +import sys + +import inkex + +from ..i18n import _ +from ..threads import ThreadCatalog +from .base import InkstitchExtension + + +class ImportThreadlist(InkstitchExtension): + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.OptionParser.add_option("-f", "--filepath", type="str", default="", dest="filepath") + self.OptionParser.add_option("-m", "--method", type="int", default=1, dest="method") + self.OptionParser.add_option("-t", "--palette", type="str", default=None, dest="palette") + + def effect(self): + # Remove selection, we want all the elements in the document + self.selected = {} + + if not self.get_elements(): + return + + path = self.options.filepath + if not os.path.exists(path): + print >> sys.stderr, _("File not found.") + sys.exit(1) + + method = self.options.method + if method == 1: + colors = self.parse_inkstitch_threadlist(path) + else: + colors = self.parse_threadlist_by_catalog_number(path) + + if all(c is None for c in colors): + print >>sys.stderr, _("Couldn't find any matching colors in the file.") + if method == 1: + print >>sys.stderr, _('Please try to import as "other threadlist" and specify a color palette below.') + else: + print >>sys.stderr, _("Please chose an other color palette for your design.") + sys.exit(1) + + # Iterate through the color blocks to apply colors + element_color = "" + i = -1 + for element in self.elements: + if element.color != element_color: + element_color = element.color + i += 1 + + # No more colors in the list, stop here + if i == len(colors): + break + + style = element.node.get('style').replace("%s" % element_color, "%s" % colors[i]) + element.node.set('style', style) + + def parse_inkstitch_threadlist(self, path): + colors = [] + with open(path) as threadlist: + for line in threadlist: + if line[0].isdigit(): + m = re.search(r"\((#[0-9A-Fa-f]{6})\)", line) + if m: + colors.append(m.group(1)) + else: + # Color not found + colors.append(None) + return colors + + def parse_threadlist_by_catalog_number(self, path): + palette_name = self.options.palette + palette = ThreadCatalog().get_palette_by_name(palette_name) + + colors = [] + palette_numbers = [] + palette_colors = [] + + for color in palette: + palette_numbers.append(color.number) + palette_colors.append('#%s' % color.hex_digits.lower()) + with open(path) as threadlist: + for line in threadlist: + if line[0].isdigit(): + # some threadlists may add a # in front of the catalof number + # let's remove it from the entire string before splitting it up + thread = line.replace('#', '').split() + catalog_number = set(thread[1:]).intersection(palette_numbers) + if catalog_number: + color_index = palette_numbers.index(next(iter(catalog_number))) + colors.append(palette_colors[color_index]) + else: + # No color found + colors.append(None) + return colors + + def find_elements(self, xpath): + svg = self.document.getroot() + elements = svg.xpath(xpath, namespaces=inkex.NSS) + return elements diff --git a/lib/extensions/remove_embroidery_settings.py b/lib/extensions/remove_embroidery_settings.py index d87a216a..d39c7e94 100644 --- a/lib/extensions/remove_embroidery_settings.py +++ b/lib/extensions/remove_embroidery_settings.py @@ -30,11 +30,11 @@ class RemoveEmbroiderySettings(InkstitchExtension): if not self.selected: xpath = ".//svg:path" elements = self.find_elements(xpath) - self.remove_embroider_attributes(elements) + self.remove_inkstitch_attributes(elements) else: for node in self.selected: elements = self.get_selected_elements(node) - self.remove_embroider_attributes(elements) + self.remove_inkstitch_attributes(elements) def remove_commands(self): if not self.selected: @@ -83,8 +83,8 @@ class RemoveEmbroiderySettings(InkstitchExtension): def remove_element(self, element): element.getparent().remove(element) - def remove_embroider_attributes(self, elements): + def remove_inkstitch_attributes(self, elements): for element in elements: for attrib in element.attrib: - if attrib.startswith('embroider_'): + if attrib.startswith(inkex.NSS['inkstitch'], 1): del element.attrib[attrib] diff --git a/lib/extensions/stitch_plan_preview.py b/lib/extensions/stitch_plan_preview.py new file mode 100644 index 00000000..b89e24a7 --- /dev/null +++ b/lib/extensions/stitch_plan_preview.py @@ -0,0 +1,24 @@ +from ..stitch_plan import patches_to_stitch_plan +from ..svg import render_stitch_plan +from .base import InkstitchExtension + + +class StitchPlanPreview(InkstitchExtension): + + def effect(self): + # delete old stitch plan + svg = self.document.getroot() + layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") + if layer is not None: + del layer[:] + + # create new stitch plan + if not self.get_elements(): + return + patches = self.elements_to_patches(self.elements) + stitch_plan = patches_to_stitch_plan(patches) + render_stitch_plan(svg, stitch_plan) + + # translate stitch plan to the right side of the canvas + layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") + layer.set('transform', 'translate(%s)' % svg.get('viewBox', '0 0 800 0').split(' ')[2]) diff --git a/lib/extensions/zip.py b/lib/extensions/zip.py index 2376f79a..aebff331 100644 --- a/lib/extensions/zip.py +++ b/lib/extensions/zip.py @@ -1,14 +1,17 @@ -import sys import os +import sys import tempfile from zipfile import ZipFile + +import inkex import pyembroidery -from .base import InkstitchExtension from ..i18n import _ from ..output import write_embroidery_file from ..stitch_plan import patches_to_stitch_plan from ..svg import PIXELS_PER_MM +from .base import InkstitchExtension +from ..threads import ThreadCatalog class Zip(InkstitchExtension): @@ -26,6 +29,10 @@ class Zip(InkstitchExtension): extension = format['extension'] self.OptionParser.add_option('--format-%s' % extension, type="inkbool", dest=extension) self.formats.append(extension) + self.OptionParser.add_option('--format-svg', type="inkbool", dest='svg') + self.OptionParser.add_option('--format-threadlist', type="inkbool", dest='threadlist') + self.formats.append('svg') + self.formats.append('threadlist') def effect(self): if not self.get_elements(): @@ -42,7 +49,17 @@ class Zip(InkstitchExtension): for format in self.formats: if getattr(self.options, format): output_file = os.path.join(path, "%s.%s" % (base_file_name, format)) - write_embroidery_file(output_file, stitch_plan, self.document.getroot()) + if format == 'svg': + output = open(output_file, 'w') + output.write(inkex.etree.tostring(self.document.getroot())) + output.close() + if format == 'threadlist': + output_file = os.path.join(path, "%s_%s.txt" % (base_file_name, _("threadlist"))) + output = open(output_file, 'w') + output.write(self.get_threadlist(stitch_plan, base_file_name)) + output.close() + else: + write_embroidery_file(output_file, stitch_plan, self.document.getroot()) files.append(output_file) if not files: @@ -69,3 +86,36 @@ class Zip(InkstitchExtension): # don't let inkex output the SVG! sys.exit(0) + + def get_threadlist(self, stitch_plan, design_name): + ThreadCatalog().match_and_apply_palette(stitch_plan, self.get_inkstitch_metadata()['thread-palette']) + thread_used = [] + + thread_output = "%s\n" % _("Design Details") + thread_output += "==============\n\n" + + thread_output += "%s: %s\n" % (_("Title"), design_name) + thread_output += "%s (mm): %.2f x %.2f\n" % (_("Size"), stitch_plan.dimensions_mm[0], stitch_plan.dimensions_mm[1]) + thread_output += "%s: %s\n" % (_("Stitches"), stitch_plan.num_stitches) + thread_output += "%s: %s\n\n" % (_("Colors"), stitch_plan.num_colors) + + thread_output += "%s\n" % _("Thread Order") + thread_output += "============\n\n" + + for i, color_block in enumerate(stitch_plan): + thread = color_block.color + + thread_output += str(i + 1) + " " + string = "%s #%s - %s (#%s)" % (thread.name, thread.number, thread.manufacturer, thread.hex_digits.lower()) + thread_output += string + "\n" + + thread_used.append(string) + + thread_output += "\n" + thread_output += _("Thread Used") + "\n" + thread_output += "============" + "\n\n" + + for thread in set(thread_used): + thread_output += thread + "\n" + + return "%s" % thread_output |
