diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/commands.py | 52 | ||||
| -rw-r--r-- | lib/elements/auto_fill.py | 4 | ||||
| -rw-r--r-- | lib/elements/element.py | 72 | ||||
| -rw-r--r-- | lib/elements/fill.py | 3 | ||||
| -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 | ||||
| -rwxr-xr-x | lib/inx/extensions.py | 7 | ||||
| -rw-r--r-- | lib/inx/utils.py | 17 | ||||
| -rw-r--r-- | lib/stitches/auto_fill.py | 62 | ||||
| -rw-r--r-- | lib/stitches/auto_satin.py | 29 | ||||
| -rw-r--r-- | lib/svg/rendering.py | 8 | ||||
| -rw-r--r-- | lib/svg/tags.py | 59 | ||||
| -rw-r--r-- | lib/svg/units.py | 7 | ||||
| -rw-r--r-- | lib/threads/catalog.py | 36 | ||||
| -rw-r--r-- | lib/threads/palette.py | 17 |
20 files changed, 582 insertions, 118 deletions
diff --git a/lib/commands.py b/lib/commands.py index 908c6e53..b92d79cf 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -1,54 +1,57 @@ -from copy import deepcopy import os -from random import random import sys +from copy import deepcopy +from random import random + +from shapely import geometry as shgeo import cubicsuperpath import inkex -from shapely import geometry as shgeo import simpletransform -from .i18n import _, N_ -from .svg import apply_transforms, get_node_transform, get_correction_transform, get_document, generate_unique_id -from .svg.tags import SVG_DEFS_TAG, SVG_GROUP_TAG, SVG_PATH_TAG, SVG_USE_TAG, SVG_SYMBOL_TAG, \ - CONNECTION_START, CONNECTION_END, CONNECTOR_TYPE, XLINK_HREF, INKSCAPE_LABEL -from .utils import cache, get_bundled_dir, Point - +from .i18n import N_, _ +from .svg import (apply_transforms, generate_unique_id, + get_correction_transform, get_document, get_node_transform) +from .svg.tags import (CONNECTION_END, CONNECTION_START, CONNECTOR_TYPE, + INKSCAPE_LABEL, INKSTITCH_ATTRIBS, SVG_DEFS_TAG, + SVG_GROUP_TAG, SVG_PATH_TAG, SVG_SYMBOL_TAG, + SVG_USE_TAG, XLINK_HREF) +from .utils import Point, cache, get_bundled_dir COMMANDS = { # L10N command attached to an object - N_("fill_start"): N_("Fill stitch starting position"), + "fill_start": N_("Fill stitch starting position"), # L10N command attached to an object - N_("fill_end"): N_("Fill stitch ending position"), + "fill_end": N_("Fill stitch ending position"), # L10N command attached to an object - N_("satin_start"): N_("Auto-route satin stitch starting position"), + "satin_start": N_("Auto-route satin stitch starting position"), # L10N command attached to an object - N_("satin_end"): N_("Auto-route satin stitch ending position"), + "satin_end": N_("Auto-route satin stitch ending position"), # L10N command attached to an object - N_("stop"): N_("Stop (pause machine) after sewing this object"), + "stop": N_("Stop (pause machine) after sewing this object"), # L10N command attached to an object - N_("trim"): N_("Trim thread after sewing this object"), + "trim": N_("Trim thread after sewing this object"), # L10N command attached to an object - N_("ignore_object"): N_("Ignore this object (do not stitch)"), + "ignore_object": N_("Ignore this object (do not stitch)"), # L10N command attached to an object - N_("satin_cut_point"): N_("Satin cut point (use with Cut Satin Column)"), + "satin_cut_point": N_("Satin cut point (use with Cut Satin Column)"), # L10N command that affects a layer - N_("ignore_layer"): N_("Ignore layer (do not stitch any objects in this layer)"), + "ignore_layer": N_("Ignore layer (do not stitch any objects in this layer)"), # L10N command that affects entire document - N_("origin"): N_("Origin for exported embroidery files"), + "origin": N_("Origin for exported embroidery files"), # L10N command that affects entire document - N_("stop_position"): N_("Jump destination for Stop commands (a.k.a. \"Frame Out position\")."), + "stop_position": N_("Jump destination for Stop commands (a.k.a. \"Frame Out position\")."), } OBJECT_COMMANDS = ["fill_start", "fill_end", "satin_start", "satin_end", "stop", "trim", "ignore_object", "satin_cut_point"] @@ -346,7 +349,7 @@ def get_command_pos(element, index, total): def remove_legacy_param(element, command): if command == "trim" or command == "stop": # If they had the old "TRIM after" or "STOP after" attributes set, - # automatically delete them. THe new commands will do the same + # automatically delete them. The new commands will do the same # thing. # # If we didn't delete these here, then things would get confusing. @@ -359,6 +362,13 @@ def remove_legacy_param(element, command): if attribute in element.node.attrib: del element.node.attrib[attribute] + # Attributes have changed to be namespaced. + # Let's check for them as well, they might have automatically changed. + attribute = INKSTITCH_ATTRIBS["%s_after" % command] + + if attribute in element.node.attrib: + del element.node.attrib[attribute] + def add_commands(element, commands): document = get_document(element.node) diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index 04da3288..b574c8bf 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -53,9 +53,9 @@ class AutoFill(Fill): return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) @property - @param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=False) + @param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=True) def fill_underlay(self): - return self.get_boolean_param("fill_underlay", default=False) + return self.get_boolean_param("fill_underlay", default=True) @property @param('fill_underlay_angle', diff --git a/lib/elements/element.py b/lib/elements/element.py index dd6c9063..83e3978f 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -1,14 +1,14 @@ -from copy import deepcopy import sys +from copy import deepcopy -from cspsubdiv import cspsubdiv import cubicsuperpath -import simplestyle +import tinycss2 +from cspsubdiv import cspsubdiv from ..commands import find_commands from ..i18n import _ from ..svg import PIXELS_PER_MM, apply_transforms, convert_length, get_doc_size -from ..svg.tags import INKSCAPE_LABEL +from ..svg.tags import INKSCAPE_LABEL, INKSTITCH_ATTRIBS from ..utils import cache @@ -72,6 +72,16 @@ class EmbroideryElement(object): def __init__(self, node): self.node = node + legacy_attribs = False + for attrib in self.node.attrib: + if attrib.startswith('embroider_'): + # update embroider_ attributes to namespaced attributes + self.replace_legacy_param(attrib) + legacy_attribs = True + if legacy_attribs and not self.get_param('fill_underlay', ""): + # defaut setting for fill_underlay has changed + self.set_param('fill_underlay', False) + @property def id(self): return self.node.get('id') @@ -85,13 +95,16 @@ class EmbroideryElement(object): # The 'param' attribute is set by the 'param' decorator defined above. if hasattr(prop.fget, 'param'): params.append(prop.fget.param) - return params + def replace_legacy_param(self, param): + 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("embroider_" + param, "").strip() - + value = self.node.get(INKSTITCH_ATTRIBS[param], "").strip() return value or default @cache @@ -131,22 +144,28 @@ class EmbroideryElement(object): return value def set_param(self, name, value): - self.node.set("embroider_%s" % name, str(value)) + param = INKSTITCH_ATTRIBS[name] + self.node.set(param, str(value)) + @property @cache + def style(self): + declarations = tinycss2.parse_declaration_list(self.node.get("style", "")) + style = {declaration.lower_name: declaration.value[0].serialize() for declaration in declarations} + + return style + def get_style(self, style_name, default=None): - style = simplestyle.parseStyle(self.node.get("style")) - if (style_name not in style): - return default - value = style[style_name] - if value == 'none': - return None - return value + style = self.style.get(style_name) + # Style not found, let's see if it is set as a separate attribute + if style is None: + style = self.node.get(style_name, default) + if style == 'none': + style = None + return style - @cache def has_style(self, style_name): - style = simplestyle.parseStyle(self.node.get("style")) - return style_name in style + return style_name in self.style @property @cache @@ -160,7 +179,7 @@ class EmbroideryElement(object): @property @cache def stroke_width(self): - width = self.get_style("stroke-width", "1") + width = self.get_style("stroke-width", None) if width is None: return 1.0 @@ -169,6 +188,17 @@ class EmbroideryElement(object): return width * self.stroke_scale @property + @param('ties', + _('Ties'), + tooltip=_('Add ties. Manual stitch will not add ties.'), + type='boolean', + default=True, + sort_index=4) + @cache + def ties(self): + return self.get_boolean_param("ties", True) + + @property def path(self): # A CSP is a "cubic superpath". # @@ -269,6 +299,10 @@ class EmbroideryElement(object): patches = self.to_patches(last_patch) + if not self.ties: + for patch in patches: + patch.stitch_as_is = True + if patches: patches[-1].trim_after = self.has_command("trim") or self.trim_after patches[-1].stop_after = self.has_command("stop") or self.stop_after diff --git a/lib/elements/fill.py b/lib/elements/fill.py index 7157cc46..a5b8bfc3 100644 --- a/lib/elements/fill.py +++ b/lib/elements/fill.py @@ -19,8 +19,7 @@ class UnconnectedError(ValidationError): "Ink/Stitch doesn't know what order to stitch them in. Please break this " "object up into separate shapes.") steps_to_solve = [ - _('* Path > Break apart (Shift+Ctrl+K)'), - _('* (Optional) Recombine shapes with holes (Ctrl+K).') + _('* Extensions > Ink/Stitch > Fill Tools > Break Apart and Retain Holes.') ] 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 diff --git a/lib/inx/extensions.py b/lib/inx/extensions.py index d1a0c7f3..030e8aa6 100755 --- a/lib/inx/extensions.py +++ b/lib/inx/extensions.py @@ -4,6 +4,7 @@ from .utils import build_environment, write_inx_file from .outputs import pyembroidery_output_formats from ..extensions import extensions, Input, Output from ..commands import LAYER_COMMANDS, OBJECT_COMMANDS, GLOBAL_COMMANDS, COMMANDS +from ..threads import ThreadCatalog def layer_commands(): @@ -27,6 +28,11 @@ def pyembroidery_debug_formats(): yield format['extension'], format['description'] +def threadcatalog(): + threadcatalog = ThreadCatalog().palette_names() + return threadcatalog + + def generate_extension_inx_files(): env = build_environment() @@ -38,6 +44,7 @@ def generate_extension_inx_files(): template = env.get_template('%s.inx' % name) write_inx_file(name, template.render(formats=pyembroidery_output_formats(), debug_formats=pyembroidery_debug_formats(), + threadcatalog=threadcatalog(), layer_commands=layer_commands(), object_commands=object_commands(), global_commands=global_commands())) diff --git a/lib/inx/utils.py b/lib/inx/utils.py index 1dc96829..a7c98a60 100644 --- a/lib/inx/utils.py +++ b/lib/inx/utils.py @@ -1,11 +1,12 @@ import errno -import os import gettext +import os +import sys from os.path import dirname -from jinja2 import Environment, FileSystemLoader -from ..i18n import translation as default_translation, locale_dir, N_ +from jinja2 import Environment, FileSystemLoader +from ..i18n import N_, locale_dir, translation as default_translation _top_path = dirname(dirname(dirname(os.path.realpath(__file__)))) inx_path = os.path.join(_top_path, "inx") @@ -25,6 +26,16 @@ def build_environment(): env.install_gettext_translations(current_translation) env.globals["locale"] = current_locale + if "BUILD" in os.environ: + # building a ZIP release, with inkstitch packaged as a binary + if sys.platform == "win32": + env.globals["command_tag"] = '<command reldir="extensions">inkstitch/bin/inkstitch.exe</command>' + else: + env.globals["command_tag"] = '<command reldir="extensions">inkstitch/bin/inkstitch</command>' + else: + # user is running inkstitch.py directly as a developer + env.globals["command_tag"] = '<command reldir="extensions" interpreter="python">inkstitch.py</command>' + return env diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 5833b779..dbbef136 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -1,18 +1,18 @@ # -*- coding: UTF-8 -*- -from itertools import groupby, chain import math +from itertools import chain, groupby import networkx from shapely import geometry as shgeo from shapely.ops import snap from shapely.strtree import STRtree +from .fill import intersect_region_with_grating, stitch_row +from .running_stitch import running_stitch from ..debug import debug from ..svg import PIXELS_PER_MM from ..utils.geometry import Point as InkstitchPoint, line_string_to_point_list -from .fill import intersect_region_with_grating, stitch_row -from .running_stitch import running_stitch class PathEdge(object): @@ -52,8 +52,7 @@ def auto_fill(shape, starting_point, ending_point=None, underpath=True): - - fill_stitch_graph = build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing) + fill_stitch_graph = build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing, starting_point, ending_point) if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length): return fallback(shape, running_stitch_length) @@ -95,7 +94,7 @@ def project(shape, coords, outline_index): @debug.time -def build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing): +def build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing, starting_point=None, ending_point=None): """build a graph representation of the grating segments This function builds a specialized graph (as in graph theory) that will @@ -146,11 +145,39 @@ def build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing): tag_nodes_with_outline_and_projection(graph, shape, graph.nodes()) add_edges_between_outline_nodes(graph, duplicate_every_other=True) + if starting_point: + insert_node(graph, shape, starting_point) + + if ending_point: + insert_node(graph, shape, ending_point) + debug.log_graph(graph, "graph") return graph +def insert_node(graph, shape, point): + """Add node to graph, splitting one of the outline edges""" + + point = tuple(point) + outline = which_outline(shape, point) + projection = project(shape, point, outline) + projected_point = list(shape.boundary)[outline].interpolate(projection) + node = (projected_point.x, projected_point.y) + + edges = [] + for start, end, key, data in graph.edges(keys=True, data=True): + if key == "outline": + edges.append(((start, end), data)) + + edge, data = min(edges, key=lambda (edge, data): shgeo.LineString(edge).distance(projected_point)) + + graph.remove_edge(*edge, key="outline") + graph.add_edge(edge[0], node, key="outline", **data) + graph.add_edge(node, edge[1], key="outline", **data) + tag_nodes_with_outline_and_projection(graph, shape, nodes=[node]) + + def tag_nodes_with_outline_and_projection(graph, shape, nodes): for node in nodes: outline_index = which_outline(shape, node) @@ -159,6 +186,27 @@ def tag_nodes_with_outline_and_projection(graph, shape, nodes): graph.add_node(node, outline=outline_index, projection=outline_projection) +def add_boundary_travel_nodes(graph, shape): + for outline_index, outline in enumerate(shape.boundary): + prev = None + for point in outline.coords: + point = shgeo.Point(point) + if prev is not None: + # Subdivide long straight line segments. Otherwise we may not + # have a node near the user's chosen starting or ending point + length = point.distance(prev) + segment = shgeo.LineString((prev, point)) + if length > 1: + # Just plot a point every pixel, that should be plenty of + # resolution. A pixel is around a quarter of a millimeter. + for i in xrange(1, int(length)): + subpoint = segment.interpolate(i) + graph.add_node((subpoint.x, subpoint.y), projection=outline.project(subpoint), outline=outline_index) + + graph.add_node((point.x, point.y), projection=outline.project(point), outline=outline_index) + prev = point + + def add_edges_between_outline_nodes(graph, duplicate_every_other=False): """Add edges around the outlines of the graph, connecting sequential nodes. @@ -240,6 +288,8 @@ def build_travel_graph(fill_stitch_graph, shape, fill_stitch_angle, underpath): # This will ensure that a path traveling inside the shape can reach its # target on the outline, which will be one of the points added above. tag_nodes_with_outline_and_projection(graph, shape, boundary_points) + else: + add_boundary_travel_nodes(graph, shape) add_edges_between_outline_nodes(graph) diff --git a/lib/stitches/auto_satin.py b/lib/stitches/auto_satin.py index 4ce356ce..9edff53c 100644 --- a/lib/stitches/auto_satin.py +++ b/lib/stitches/auto_satin.py @@ -1,20 +1,23 @@ -from itertools import chain, izip import math +from itertools import chain, izip -import cubicsuperpath -import inkex +import networkx as nx from shapely import geometry as shgeo from shapely.geometry import Point as ShapelyPoint -import simplestyle -import networkx as nx +import cubicsuperpath +import inkex +import simplestyle from ..commands import add_commands -from ..elements import Stroke, SatinColumn +from ..elements import SatinColumn, Stroke from ..i18n import _ -from ..svg import PIXELS_PER_MM, line_strings_to_csp, get_correction_transform, generate_unique_id -from ..svg.tags import SVG_PATH_TAG, SVG_GROUP_TAG, INKSCAPE_LABEL -from ..utils import Point as InkstitchPoint, cut, cache +from ..svg import (PIXELS_PER_MM, generate_unique_id, get_correction_transform, + line_strings_to_csp) +from ..svg.tags import (INKSCAPE_LABEL, INKSTITCH_ATTRIBS, SVG_GROUP_TAG, + SVG_PATH_TAG) +from ..utils import Point as InkstitchPoint +from ..utils import cache, cut class SatinSegment(object): @@ -209,9 +212,9 @@ class RunningStitch(object): self.original_element = original_element self.style = original_element.node.get('style', '') self.running_stitch_length = \ - original_element.node.get('embroider_running_stitch_length_mm', '') or \ - original_element.node.get('embroider_center_walk_underlay_stitch_length_mm', '') or \ - original_element.node.get('embroider_contour_underlay_stitch_length_mm', '') + original_element.node.get(INKSTITCH_ATTRIBS['running_stitch_length_mm'], '') or \ + original_element.node.get(INKSTITCH_ATTRIBS['center_walk_underlay_stitch_length_mm'], '') or \ + original_element.node.get(INKSTITCH_ATTRIBS['contour_underlay_stitch_length_mm'], '') def to_element(self): node = inkex.etree.Element(SVG_PATH_TAG) @@ -222,7 +225,7 @@ class RunningStitch(object): style['stroke-dasharray'] = "0.5,0.5" style = simplestyle.formatStyle(style) node.set("style", style) - node.set("embroider_running_stitch_length_mm", self.running_stitch_length) + node.set(INKSTITCH_ATTRIBS['running_stitch_length_mm'], self.running_stitch_length) stroke = Stroke(node) diff --git a/lib/svg/rendering.py b/lib/svg/rendering.py index 2711f12a..5860ceef 100644 --- a/lib/svg/rendering.py +++ b/lib/svg/rendering.py @@ -5,10 +5,11 @@ import simplepath import simplestyle import simpletransform -from .tags import INKSCAPE_GROUPMODE, INKSCAPE_LABEL, SVG_DEFS_TAG, SVG_GROUP_TAG, SVG_PATH_TAG -from .units import PIXELS_PER_MM, get_viewbox_transform from ..i18n import _ from ..utils import Point, cache +from .tags import (INKSCAPE_GROUPMODE, INKSCAPE_LABEL, INKSTITCH_ATTRIBS, + SVG_DEFS_TAG, SVG_GROUP_TAG, SVG_PATH_TAG) +from .units import PIXELS_PER_MM, get_viewbox_transform # The stitch vector path looks like this: # _______ @@ -198,6 +199,7 @@ def color_block_to_paths(color_block, svg, destination, visual_commands): add_commands(Stroke(destination[-1]), ["trim"]) color = color_block.color.visible_on_white.to_hex_str() + path = inkex.etree.Element(SVG_PATH_TAG, { 'style': simplestyle.formatStyle({ 'stroke': color, @@ -206,7 +208,7 @@ def color_block_to_paths(color_block, svg, destination, visual_commands): }), 'd': "M" + " ".join(" ".join(str(coord) for coord in point) for point in point_list), 'transform': get_correction_transform(svg), - 'embroider_manual_stitch': 'true' + INKSTITCH_ATTRIBS['manual_stitch']: 'true' }) destination.append(path) diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 55af113a..3e444513 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -1,5 +1,6 @@ import inkex + # This is used below and added to the document in ../extensions/base.py. inkex.NSS['inkstitch'] = 'http://inkstitch.org/namespace' @@ -13,15 +14,71 @@ SVG_GROUP_TAG = inkex.addNS('g', 'svg') SVG_SYMBOL_TAG = inkex.addNS('symbol', 'svg') SVG_USE_TAG = inkex.addNS('use', 'svg') +EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG) + INKSCAPE_LABEL = inkex.addNS('label', 'inkscape') INKSCAPE_GROUPMODE = inkex.addNS('groupmode', 'inkscape') CONNECTION_START = inkex.addNS('connection-start', 'inkscape') CONNECTION_END = inkex.addNS('connection-end', 'inkscape') CONNECTOR_TYPE = inkex.addNS('connector-type', 'inkscape') + XLINK_HREF = inkex.addNS('href', 'xlink') + SODIPODI_NAMEDVIEW = inkex.addNS('namedview', 'sodipodi') SODIPODI_GUIDE = inkex.addNS('guide', 'sodipodi') SODIPODI_ROLE = inkex.addNS('role', 'sodipodi') + INKSTITCH_LETTERING = inkex.addNS('lettering', 'inkstitch') -EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG) +INKSTITCH_ATTRIBS = {} +# Fill +inkstitch_attribs = [ + 'ties', + 'trim_after', + 'stop_after', + # fill + 'angle', + 'auto_fill', + 'expand_mm', + 'fill_underlay', + 'fill_underlay_angle', + 'fill_underlay_inset_mm', + 'fill_underlay_max_stitch_length_mm', + 'fill_underlay_row_spacing_mm', + 'fill_underlay_skip_last', + 'max_stitch_length_mm', + 'row_spacing_mm', + 'end_row_spacing_mm', + 'skip_last', + 'staggers', + 'underlay_underpath', + 'underpath', + 'flip', + 'expand_mm', + # stroke + 'manual_stitch', + 'bean_stitch_repeats', + 'repeats', + 'running_stitch_length_mm', + # satin column + 'satin_column', + 'satin_column', + 'running_stitch_length_mm', + 'center_walk_underlay', + 'center_walk_underlay_stitch_length_mm', + 'contour_underlay', + 'contour_underlay_stitch_length_mm', + 'contour_underlay_inset_mm', + 'zigzag_underlay', + 'zigzag_spacing_mm', + 'zigzag_underlay_inset_mm', + 'zigzag_underlay_spacing_mm', + 'e_stitch', + 'pull_compensation_mm', + 'stroke_first', + # Legacy + 'embroider_trim_after', + 'embroider_stop_after' + ] +for attrib in inkstitch_attribs: + INKSTITCH_ATTRIBS[attrib] = inkex.addNS(attrib, 'inkstitch') diff --git a/lib/svg/units.py b/lib/svg/units.py index 739dcbb4..319e018b 100644 --- a/lib/svg/units.py +++ b/lib/svg/units.py @@ -3,7 +3,6 @@ import simpletransform from ..i18n import _ from ..utils import cache - # modern versions of Inkscape use 96 pixels per inch as per the CSS standard PIXELS_PER_MM = 96 / 25.4 @@ -127,6 +126,12 @@ def get_viewbox_transform(node): try: sx = doc_width / float(viewbox[2]) sy = doc_height / float(viewbox[3]) + + # preserve aspect ratio + aspect_ratio = node.get('preserveAspectRatio', 'xMidYMid meet') + if aspect_ratio != 'none': + sx = sy = max(sx, sy) if 'slice' in aspect_ratio else min(sx, sy) + scale_transform = simpletransform.parseTransform("scale(%f, %f)" % (sx, sy)) transform = simpletransform.composeTransform(transform, scale_transform) except ZeroDivisionError: diff --git a/lib/threads/catalog.py b/lib/threads/catalog.py index ece2f8ac..aba2696d 100644 --- a/lib/threads/catalog.py +++ b/lib/threads/catalog.py @@ -1,9 +1,10 @@ import os -from os.path import dirname, realpath import sys -from glob import glob from collections import Sequence +from glob import glob +from os.path import dirname, realpath +from ..utils import guess_inkscape_config_path from .palette import ThreadPalette @@ -12,19 +13,32 @@ class _ThreadCatalog(Sequence): def __init__(self): self.palettes = [] - self.load_palettes(self.get_palettes_path()) + self.load_palettes(self.get_palettes_paths()) + + def get_palettes_paths(self): + """Creates a list containing the path of two directories: + 1. Palette directory of Inkscape + 2. Palette directory of inkstitch + """ + path = [os.path.join(guess_inkscape_config_path(), 'palettes')] - def get_palettes_path(self): if getattr(sys, 'frozen', None) is not None: - path = os.path.join(sys._MEIPASS, "..") + inkstitch_path = os.path.join(sys._MEIPASS, "..") else: - path = dirname(dirname(dirname(realpath(__file__)))) + inkstitch_path = dirname(dirname(dirname(realpath(__file__)))) - return os.path.join(path, 'palettes') + path.append(os.path.join(inkstitch_path, 'palettes')) - def load_palettes(self, path): - for palette_file in glob(os.path.join(path, '*.gpl')): - self.palettes.append(ThreadPalette(palette_file)) + return path + + def load_palettes(self, paths): + palettes = [] + for path in paths: + for palette_file in glob(os.path.join(path, 'InkStitch*.gpl')): + palette_basename = os.path.basename(palette_file) + if palette_basename not in palettes: + self.palettes.append(ThreadPalette(palette_file)) + palettes.append(palette_basename) def palette_names(self): return list(sorted(palette.name for palette in self)) @@ -59,6 +73,8 @@ class _ThreadCatalog(Sequence): chosen if more tha 80% of the thread colors in the stitch plan are exact matches for threads in the palette. """ + if not self.palettes: + return None threads = [color_block.color for color_block in stitch_plan] palettes_and_matches = [(palette, self._num_exact_color_matches(palette, threads)) diff --git a/lib/threads/palette.py b/lib/threads/palette.py index 654c43e5..d685e5bb 100644 --- a/lib/threads/palette.py +++ b/lib/threads/palette.py @@ -48,13 +48,16 @@ class ThreadPalette(Set): palette.readline() for line in palette: - fields = line.split("\t", 3) - thread_color = [int(field) for field in fields[:3]] - thread_name, thread_number = fields[3].strip().rsplit(" ", 1) - thread_name = thread_name.strip() - - thread = ThreadColor(thread_color, thread_name, thread_number, manufacturer=self.name) - self.threads[thread] = convert_color(sRGBColor(*thread_color, is_upscaled=True), LabColor) + try: + fields = line.split(None, 3) + thread_color = [int(field) for field in fields[:3]] + thread_name, thread_number = fields[3].strip().rsplit(" ", 1) + thread_name = thread_name.strip() + + thread = ThreadColor(thread_color, thread_name, thread_number, manufacturer=self.name) + self.threads[thread] = convert_color(sRGBColor(*thread_color, is_upscaled=True), LabColor) + except ValueError: + continue def __contains__(self, thread): return thread in self.threads |
