summaryrefslogtreecommitdiff
path: root/lib/extensions
diff options
context:
space:
mode:
Diffstat (limited to 'lib/extensions')
-rw-r--r--lib/extensions/__init__.py9
-rw-r--r--lib/extensions/break_apart.py68
-rw-r--r--lib/extensions/convert_to_satin.py60
-rw-r--r--lib/extensions/import_threadlist.py102
-rw-r--r--lib/extensions/remove_embroidery_settings.py8
-rw-r--r--lib/extensions/stitch_plan_preview.py24
-rw-r--r--lib/extensions/zip.py56
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