From 4c986117bfe1f2caa8280d4b0ddbeec69f41b18d Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 26 May 2018 21:26:40 -0400 Subject: first attempt at realistic rendering --- lib/extensions/print_pdf.py | 2 +- lib/svg/svg.py | 157 ++++++++++++++++++++++++++++++++++++++------ lib/threads/color.py | 15 +++++ lib/utils/geometry.py | 6 ++ 4 files changed, 159 insertions(+), 21 deletions(-) (limited to 'lib') diff --git a/lib/extensions/print_pdf.py b/lib/extensions/print_pdf.py index baeb7eba..43e53318 100644 --- a/lib/extensions/print_pdf.py +++ b/lib/extensions/print_pdf.py @@ -119,7 +119,7 @@ class PrintPreviewServer(Thread): def start_watcher(): self.watcher_thread = Thread(target=self.watch) self.watcher_thread.daemon = True - self.watcher_thread.start() + #self.watcher_thread.start() @self.app.route('/') def index(): diff --git a/lib/svg/svg.py b/lib/svg/svg.py index 852215f2..147fdc84 100644 --- a/lib/svg/svg.py +++ b/lib/svg/svg.py @@ -1,9 +1,124 @@ -import simpletransform, simplestyle, inkex +import math +import simpletransform, simplestyle, simplepath, inkex -from .units import get_viewbox_transform -from .tags import SVG_GROUP_TAG, INKSCAPE_LABEL, INKSCAPE_GROUPMODE, SVG_PATH_TAG +from .units import get_viewbox_transform, PIXELS_PER_MM +from .tags import SVG_GROUP_TAG, INKSCAPE_LABEL, INKSCAPE_GROUPMODE, SVG_PATH_TAG, SVG_DEFS_TAG from ..i18n import _ -from ..utils import cache +from ..utils import cache, Point + + +# The stitch vector path looks like this: +# _______ +# (_______) +# +# It's 0.4mm high, which is the approximate thickness of common machine embroidery threads. + +# 1.52 pixels = 0.4mm +stitch_height = 1.52 + +# This vector path starts at the origin and contains a placeholder (%s) for the stitch length. +stitch_path = "M0,0c0.386,0,0.417,0.378,0.428,0.759c0.012,0.382,-0.048,0.754,-0.428,0.759h-%sc-0.357,-0.003,-0.399,-0.376,-0.413,-0.759c-0.014,-0.382,0.067,-0.759,0.413,-0.759z" + +# This filter makes the above stitch path look like a real stitch with lighting. +realistic_filter = """ + + + + + + + + + + + + + + + + + + +""" + +def realistic_stitch(start, end): + """Generate a stitch vector path given a start and end point.""" + + end = Point(*end) + start = Point(*start) + + stitch_length = (end - start).length() + stitch_center = (end + start) / 2.0 + stitch_direction = (end - start) + stitch_angle = math.atan2(stitch_direction.y, stitch_direction.x) + + stitch_length = max(0, stitch_length - 0.2 * PIXELS_PER_MM) + + # create the path by filling in the length in the template + path = simplepath.parsePath(stitch_path % stitch_length) + + simplepath.scalePath(path, 1, 0.8) + + # rotate the path to match the stitch + rotation_center_x = -stitch_length / 2.0 + rotation_center_y = stitch_height / 2.0 + simplepath.rotatePath(path, stitch_angle, cx=rotation_center_x, cy=rotation_center_y) + + # move the path to the location of the stitch + simplepath.translatePath(path, stitch_center.x, stitch_center.y) + + return simplepath.formatPath(path) def color_block_to_point_lists(color_block): @@ -37,22 +152,21 @@ def color_block_to_paths(color_block, svg): # We could emit just a single path with one subpath per point list, but # emitting multiple paths makes it easier for the user to manipulate them. for point_list in color_block_to_point_lists(color_block): - color = color_block.color.visible_on_white.to_hex_str() - paths.append(inkex.etree.Element( - SVG_PATH_TAG, - {'style': simplestyle.formatStyle( - {'stroke': color, - 'stroke-width': "0.4", - 'fill': 'none'}), - 'd': "M" + " ".join(" ".join(str(coord) for coord in point) for point in point_list), - 'transform': get_correction_transform(svg), - 'embroider_manual_stitch': 'true', - 'embroider_trim_after': 'true', - })) - - # no need to trim at the end of a thread color - if paths: - paths[-1].attrib.pop('embroider_trim_after') + color = color_block.color.visible_on_white.darker.to_hex_str() + start = point_list[0] + for point in point_list[1:]: + paths.append(inkex.etree.Element( + SVG_PATH_TAG, + {'style': simplestyle.formatStyle( + { + 'fill': color, + 'stroke': 'none', + 'filter': 'url(#realistic-stitch-filter)' + }), + 'd': realistic_stitch(start, point), + 'transform': get_correction_transform(svg) + })) + start = point return paths @@ -78,4 +192,7 @@ def render_stitch_plan(svg, stitch_plan): INKSCAPE_LABEL: "color block %d" % (i + 1)}) group.extend(color_block_to_paths(color_block, svg)) + defs = svg.find(SVG_DEFS_TAG) + defs.append(inkex.etree.fromstring(realistic_filter)) + svg.append(layer) diff --git a/lib/threads/color.py b/lib/threads/color.py index af474127..fede2ecc 100644 --- a/lib/threads/color.py +++ b/lib/threads/color.py @@ -80,3 +80,18 @@ class ThreadColor(object): color = tuple(value * 255 for value in color) return ThreadColor(color, name=self.name, number=self.number, manufacturer=self.manufacturer) + + @property + def darker(self): + hls = list(colorsys.rgb_to_hls(*self.rgb_normalized)) + + # Capping lightness should make the color visible without changing it + # too much. + hls[1] *= 0.75 + + color = colorsys.hls_to_rgb(*hls) + + # convert back to values in the range of 0-255 + color = tuple(value * 255 for value in color) + + return ThreadColor(color, name=self.name, number=self.number, manufacturer=self.manufacturer) diff --git a/lib/utils/geometry.py b/lib/utils/geometry.py index 61b98bcb..7ff9b1cd 100644 --- a/lib/utils/geometry.py +++ b/lib/utils/geometry.py @@ -71,6 +71,12 @@ class Point: else: raise ValueError("cannot multiply Point by %s" % type(other)) + def __div__(self, other): + if isinstance(other, (int, float)): + return self * (1.0 / other) + else: + raise ValueErorr("cannot divide Point by %s" % type(other)) + def __repr__(self): return "Point(%s,%s)" % (self.x, self.y) -- cgit v1.2.3 From d7d2328b59e4a0f0f93830d636b74c2d92ab9d56 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sun, 27 May 2018 15:36:44 -0400 Subject: tinker with rendering --- lib/svg/svg.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) (limited to 'lib') diff --git a/lib/svg/svg.py b/lib/svg/svg.py index 147fdc84..8921ccec 100644 --- a/lib/svg/svg.py +++ b/lib/svg/svg.py @@ -11,23 +11,23 @@ from ..utils import cache, Point # _______ # (_______) # -# It's 0.4mm high, which is the approximate thickness of common machine embroidery threads. +# It's 0.32mm high, which is the approximate thickness of common machine embroidery threads. -# 1.52 pixels = 0.4mm -stitch_height = 1.52 +# 1.216 pixels = 0.32mm +stitch_height = 1.216 # This vector path starts at the origin and contains a placeholder (%s) for the stitch length. -stitch_path = "M0,0c0.386,0,0.417,0.378,0.428,0.759c0.012,0.382,-0.048,0.754,-0.428,0.759h-%sc-0.357,-0.003,-0.399,-0.376,-0.413,-0.759c-0.014,-0.382,0.067,-0.759,0.413,-0.759z" +stitch_path = "M0,0c0.386,0,0.417,0.302,0.428,0.607c0.012,0.306,-0.048,0.603,-0.428,0.607h-%sc-0.357,-0.002,-0.399,-0.3,-0.413,-0.607c-0.014,-0.305,0.067,-0.607,0.413,-0.607z" # This filter makes the above stitch path look like a real stitch with lighting. realistic_filter = """ + x="-0.1" + width="1.2" + y="-0.1" + height="1.2"> + surfaceScale="30"> @@ -108,15 +108,13 @@ def realistic_stitch(start, end): # create the path by filling in the length in the template path = simplepath.parsePath(stitch_path % stitch_length) - simplepath.scalePath(path, 1, 0.8) - # rotate the path to match the stitch rotation_center_x = -stitch_length / 2.0 rotation_center_y = stitch_height / 2.0 simplepath.rotatePath(path, stitch_angle, cx=rotation_center_x, cy=rotation_center_y) # move the path to the location of the stitch - simplepath.translatePath(path, stitch_center.x, stitch_center.y) + simplepath.translatePath(path, stitch_center.x - rotation_center_x, stitch_center.y - rotation_center_y) return simplepath.formatPath(path) -- cgit v1.2.3 From a9b834878b618a3aea6c343e71a2df3046bde3a7 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 29 May 2018 20:55:46 -0400 Subject: optimize path --- lib/svg/svg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/svg/svg.py b/lib/svg/svg.py index 8921ccec..655ddc58 100644 --- a/lib/svg/svg.py +++ b/lib/svg/svg.py @@ -17,7 +17,7 @@ from ..utils import cache, Point stitch_height = 1.216 # This vector path starts at the origin and contains a placeholder (%s) for the stitch length. -stitch_path = "M0,0c0.386,0,0.417,0.302,0.428,0.607c0.012,0.306,-0.048,0.603,-0.428,0.607h-%sc-0.357,-0.002,-0.399,-0.3,-0.413,-0.607c-0.014,-0.305,0.067,-0.607,0.413,-0.607z" +stitch_path = "M0,0c0.4,0,0.4,0.3,0.4,0.6c0,0.3,-0.1,0.6,-0.4,0.6v0.2,-0.2h-%sc-0.4,0,-0.4,-0.3,-0.4,-0.6c0,-0.3,0.1,-0.6,0.4,-0.6v-0.2,0.2z" # This filter makes the above stitch path look like a real stitch with lighting. realistic_filter = """ -- cgit v1.2.3 From 0348b03b91f64c43c5d0adba2f41d0ef21a82c9a Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 30 May 2018 20:10:11 -0400 Subject: add realistic boolean param to render_stitch_plan() --- lib/svg/realistic_rendering.py | 129 ++++++++++++++++++++++++++++++++ lib/svg/svg.py | 166 +++++++++++------------------------------ 2 files changed, 171 insertions(+), 124 deletions(-) create mode 100644 lib/svg/realistic_rendering.py (limited to 'lib') diff --git a/lib/svg/realistic_rendering.py b/lib/svg/realistic_rendering.py new file mode 100644 index 00000000..e31534da --- /dev/null +++ b/lib/svg/realistic_rendering.py @@ -0,0 +1,129 @@ +import simplepath +import math + +from .units import PIXELS_PER_MM +from ..utils import cache, Point + +# The stitch vector path looks like this: +# _______ +# (_______) +# +# It's 0.32mm high, which is the approximate thickness of common machine +# embroidery threads. + +# 1.216 pixels = 0.32mm +stitch_height = 1.216 + +# This vector path starts at the upper right corner of the stitch shape and +# proceeds counter-clockwise.and contains a placeholder (%s) for the stitch +# length. +# +# It contains two invisible "whiskers" of zero width that go above and below +# to ensure that the SVG renderer allocates a large enough canvas area when +# computing the gaussian blur steps. Otherwise, we'd have to expand the +# width and height attributes of the tag to add more buffer space. +# The width and height are specified in multiples of the bounding box +# size, It's the bounding box aligned with the global SVG canvas's axes, not +# the axes of the stitch itself. That means that having a big enough value +# to add enough padding on the long sides of the stitch would waste a ton +# of space on the short sides and significantly slow down rendering. +stitch_path = "M0,0c0.4,0,0.4,0.3,0.4,0.6c0,0.3,-0.1,0.6,-0.4,0.6v0.2,-0.2h-%sc-0.4,0,-0.4,-0.3,-0.4,-0.6c0,-0.3,0.1,-0.6,0.4,-0.6v-0.2,0.2z" + +# This filter makes the above stitch path look like a real stitch with lighting. +realistic_filter = """ + + + + + + + + + + + + + + + + + + +""" + +def realistic_stitch(start, end): + """Generate a stitch vector path given a start and end point.""" + + end = Point(*end) + start = Point(*start) + + stitch_length = (end - start).length() + stitch_center = (end + start) / 2.0 + stitch_direction = (end - start) + stitch_angle = math.atan2(stitch_direction.y, stitch_direction.x) + + stitch_length = max(0, stitch_length - 0.2 * PIXELS_PER_MM) + + # create the path by filling in the length in the template + path = simplepath.parsePath(stitch_path % stitch_length) + + # rotate the path to match the stitch + rotation_center_x = -stitch_length / 2.0 + rotation_center_y = stitch_height / 2.0 + simplepath.rotatePath(path, stitch_angle, cx=rotation_center_x, cy=rotation_center_y) + + # move the path to the location of the stitch + simplepath.translatePath(path, stitch_center.x - rotation_center_x, stitch_center.y - rotation_center_y) + + return simplepath.formatPath(path) diff --git a/lib/svg/svg.py b/lib/svg/svg.py index 655ddc58..5552abd8 100644 --- a/lib/svg/svg.py +++ b/lib/svg/svg.py @@ -1,122 +1,10 @@ -import math -import simpletransform, simplestyle, simplepath, inkex +import simpletransform, simplestyle, inkex -from .units import get_viewbox_transform, PIXELS_PER_MM +from .units import get_viewbox_transform from .tags import SVG_GROUP_TAG, INKSCAPE_LABEL, INKSCAPE_GROUPMODE, SVG_PATH_TAG, SVG_DEFS_TAG +from .realistic_rendering import realistic_stitch, realistic_filter from ..i18n import _ -from ..utils import cache, Point - - -# The stitch vector path looks like this: -# _______ -# (_______) -# -# It's 0.32mm high, which is the approximate thickness of common machine embroidery threads. - -# 1.216 pixels = 0.32mm -stitch_height = 1.216 - -# This vector path starts at the origin and contains a placeholder (%s) for the stitch length. -stitch_path = "M0,0c0.4,0,0.4,0.3,0.4,0.6c0,0.3,-0.1,0.6,-0.4,0.6v0.2,-0.2h-%sc-0.4,0,-0.4,-0.3,-0.4,-0.6c0,-0.3,0.1,-0.6,0.4,-0.6v-0.2,0.2z" - -# This filter makes the above stitch path look like a real stitch with lighting. -realistic_filter = """ - - - - - - - - - - - - - - - - - - -""" - -def realistic_stitch(start, end): - """Generate a stitch vector path given a start and end point.""" - - end = Point(*end) - start = Point(*start) - - stitch_length = (end - start).length() - stitch_center = (end + start) / 2.0 - stitch_direction = (end - start) - stitch_angle = math.atan2(stitch_direction.y, stitch_direction.x) - - stitch_length = max(0, stitch_length - 0.2 * PIXELS_PER_MM) - - # create the path by filling in the length in the template - path = simplepath.parsePath(stitch_path % stitch_length) - - # rotate the path to match the stitch - rotation_center_x = -stitch_length / 2.0 - rotation_center_y = stitch_height / 2.0 - simplepath.rotatePath(path, stitch_angle, cx=rotation_center_x, cy=rotation_center_y) - - # move the path to the location of the stitch - simplepath.translatePath(path, stitch_center.x - rotation_center_x, stitch_center.y - rotation_center_y) - - return simplepath.formatPath(path) +from ..utils import cache def color_block_to_point_lists(color_block): @@ -145,10 +33,9 @@ def get_correction_transform(svg): return transform -def color_block_to_paths(color_block, svg): +def color_block_to_realistic_stitches(color_block, svg): paths = [] - # We could emit just a single path with one subpath per point list, but - # emitting multiple paths makes it easier for the user to manipulate them. + for point_list in color_block_to_point_lists(color_block): color = color_block.color.visible_on_white.darker.to_hex_str() start = point_list[0] @@ -168,8 +55,31 @@ def color_block_to_paths(color_block, svg): return paths +def color_block_to_paths(color_block, svg): + paths = [] + # We could emit just a single path with one subpath per point list, but + # emitting multiple paths makes it easier for the user to manipulate them. + for point_list in color_block_to_point_lists(color_block): + color = color_block.color.visible_on_white.to_hex_str() + paths.append(inkex.etree.Element( + SVG_PATH_TAG, + {'style': simplestyle.formatStyle( + {'stroke': color, + 'stroke-width': "0.4", + 'fill': 'none'}), + 'd': "M" + " ".join(" ".join(str(coord) for coord in point) for point in point_list), + 'transform': get_correction_transform(svg), + 'embroider_manual_stitch': 'true', + 'embroider_trim_after': 'true', + })) + + # no need to trim at the end of a thread color + if paths: + paths[-1].attrib.pop('embroider_trim_after') + + return paths -def render_stitch_plan(svg, stitch_plan): +def render_stitch_plan(svg, stitch_plan, realistic=False): layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") if layer is None: layer = inkex.etree.Element(SVG_GROUP_TAG, @@ -188,9 +98,17 @@ def render_stitch_plan(svg, stitch_plan): SVG_GROUP_TAG, {'id': '__color_block_%d__' % i, INKSCAPE_LABEL: "color block %d" % (i + 1)}) - group.extend(color_block_to_paths(color_block, svg)) - - defs = svg.find(SVG_DEFS_TAG) - defs.append(inkex.etree.fromstring(realistic_filter)) + if realistic: + group.extend(color_block_to_realistic_stitches(color_block, svg)) + else: + group.extend(color_block_to_paths(color_block, svg)) svg.append(layer) + + if realistic: + defs = svg.find(SVG_DEFS_TAG) + + if defs is None: + defs = inkex.etree.SubElement(svg, SVG_DEFS_TAG) + + defs.append(inkex.etree.fromstring(realistic_filter)) -- cgit v1.2.3 From ae286b17ad450c530d1822208cce75db3bd3faf2 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 30 May 2018 22:25:43 -0400 Subject: refactor print_pdf and generate realistic previews --- lib/extensions/print_pdf.py | 86 ++++++++++++++++++++++++++++----------------- 1 file changed, 54 insertions(+), 32 deletions(-) (limited to 'lib') diff --git a/lib/extensions/print_pdf.py b/lib/extensions/print_pdf.py index 43e53318..91562c09 100644 --- a/lib/extensions/print_pdf.py +++ b/lib/extensions/print_pdf.py @@ -21,7 +21,7 @@ import requests from .base import InkstitchExtension from ..i18n import _, translation as inkstitch_translation from ..svg import PIXELS_PER_MM, render_stitch_plan -from ..svg.tags import SVG_GROUP_TAG +from ..svg.tags import SVG_GROUP_TAG, INKSCAPE_GROUPMODE from ..stitch_plan import patches_to_stitch_plan from ..threads import ThreadCatalog @@ -94,6 +94,8 @@ class PrintPreviewServer(Thread): self.html = kwargs.pop('html') self.metadata = kwargs.pop('metadata') self.stitch_plan = kwargs.pop('stitch_plan') + self.realistic_overview_svg = kwargs.pop('realistic_overview_svg') + self.realistic_color_block_svgs = kwargs.pop('realistic_color_block_svgs') Thread.__init__(self, *args, **kwargs) self.daemon = True self.last_request_time = None @@ -119,7 +121,7 @@ class PrintPreviewServer(Thread): def start_watcher(): self.watcher_thread = Thread(target=self.watch) self.watcher_thread.daemon = True - #self.watcher_thread.start() + self.watcher_thread.start() @self.app.route('/') def index(): @@ -202,6 +204,13 @@ class PrintPreviewServer(Thread): return jsonify(threads) + @self.app.route('/realistic', methods=['GET']) + def get_realistic(): + return jsonify({ + 'overview': self.realistic_overview_svg, + 'color_blocks': self.realistic_color_block_svgs + }) + def stop(self): # for whatever reason, shutting down only seems possible in # the context of a flask request, so we'll just make one @@ -295,38 +304,24 @@ class Print(InkstitchExtension): return env - def strip_namespaces(self): + def strip_namespaces(self, svg): # namespace prefixes seem to trip up HTML, so get rid of them - for element in self.document.iter(): + for element in svg.iter(): if element.tag[0]=='{': element.tag = element.tag[element.tag.index('}',1) + 1:] - def effect(self): - # It doesn't really make sense to print just a couple of selected - # objects. It's almost certain they meant to print the whole design. - # If they really wanted to print just a few objects, they could set - # the rest invisible temporarily. - self.selected = {} + def render_svgs(self, stitch_plan, realistic=False): + svg = deepcopy(self.document).getroot() + render_stitch_plan(svg, stitch_plan, realistic) - if not self.get_elements(): - return - - self.hide_all_layers() - - patches = self.elements_to_patches(self.elements) - stitch_plan = patches_to_stitch_plan(patches) - palette = ThreadCatalog().match_and_apply_palette(stitch_plan, self.get_inkstitch_metadata()['thread-palette']) - render_stitch_plan(self.document.getroot(), stitch_plan) - - self.strip_namespaces() + self.strip_namespaces(svg) # Now the stitch plan layer will contain a set of groups, each # corresponding to a color block. We'll create a set of SVG files # corresponding to each individual color block and a final one # for all color blocks together. - svg = self.document.getroot() - layers = svg.findall("./g[@{http://www.inkscape.org/namespaces/inkscape}groupmode='layer']") + layers = svg.findall("./g[@%s='layer']" % INKSCAPE_GROUPMODE) stitch_plan_layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") # First, delete all of the other layers. We don't need them and they'll @@ -335,9 +330,9 @@ class Print(InkstitchExtension): if layer is not stitch_plan_layer: svg.remove(layer) - overview_svg = inkex.etree.tostring(self.document) - + overview_svg = inkex.etree.tostring(svg) color_block_groups = stitch_plan_layer.getchildren() + color_block_svgs = [] for i, group in enumerate(color_block_groups): # clear the stitch plan layer @@ -347,12 +342,15 @@ class Print(InkstitchExtension): stitch_plan_layer.append(group) # save an SVG preview - stitch_plan.color_blocks[i].svg_preview = inkex.etree.tostring(self.document) + color_block_svgs.append(inkex.etree.tostring(svg)) + + return overview_svg, color_block_svgs + def render_html(self, stitch_plan, overview_svg, selected_palette): env = self.build_environment() template = env.get_template('index.html') - html = template.render( + return template.render( view = {'client_overview': False, 'client_detailedview': False, 'operator_overview': True, 'operator_detailedview': True}, logo = {'src' : '', 'title' : 'LOGO'}, date = date.today(), @@ -371,14 +369,38 @@ class Print(InkstitchExtension): svg_overview = overview_svg, color_blocks = stitch_plan.color_blocks, palettes = ThreadCatalog().palette_names(), - selected_palette = palette, + selected_palette = selected_palette, ) - # We've totally mucked with the SVG. Restore it so that we can save - # metadata into it. - self.document = deepcopy(self.original_document) + def effect(self): + # It doesn't really make sense to print just a couple of selected + # objects. It's almost certain they meant to print the whole design. + # If they really wanted to print just a few objects, they could set + # the rest invisible temporarily. + self.selected = {} + + if not self.get_elements(): + return + + patches = self.elements_to_patches(self.elements) + stitch_plan = patches_to_stitch_plan(patches) + palette = ThreadCatalog().match_and_apply_palette(stitch_plan, self.get_inkstitch_metadata()['thread-palette']) + + overview_svg, color_block_svgs = self.render_svgs(stitch_plan, realistic=False) + realistic_overview_svg, realistic_color_block_svgs = self.render_svgs(stitch_plan, realistic=True) + + for i, svg in enumerate(color_block_svgs): + stitch_plan.color_blocks[i].svg_preview = svg + + html = self.render_html(stitch_plan, overview_svg, palette) - print_server = PrintPreviewServer(html=html, metadata=self.get_inkstitch_metadata(), stitch_plan=stitch_plan) + print_server = PrintPreviewServer( + html=html, + metadata=self.get_inkstitch_metadata(), + stitch_plan=stitch_plan, + realistic_overview_svg=realistic_overview_svg, + realistic_color_block_svgs=realistic_color_block_svgs + ) print_server.start() time.sleep(1) -- cgit v1.2.3 From 2a4f3e8cdf23d34f6fcfe7dd6454824a928512fd Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 1 Jun 2018 20:34:27 -0400 Subject: add Expand param for fills --- lib/elements/auto_fill.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) (limited to 'lib') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index 08ae67f7..504bae2a 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -63,20 +63,43 @@ class AutoFill(Fill): return self.get_float_param("fill_underlay_max_stitch_length_mm") or self.max_stitch_length @property - @param('fill_underlay_inset_mm', _('Inset'), unit='mm', group=_('AutoFill Underlay'), type='float', default=0) + @param('fill_underlay_inset_mm', + _('Inset'), + tooltip='Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.', + unit='mm', + group=_('AutoFill Underlay'), + type='float', + default=0) def fill_underlay_inset(self): return self.get_float_param('fill_underlay_inset_mm', 0) @property - def underlay_shape(self): - if self.fill_underlay_inset: - shape = self.shape.buffer(-self.fill_underlay_inset) + @param('expand_mm', + _('Expand'), + tooltip='Expand the shape before fill stitching, to compensate for gaps between shapes.', + unit='mm', + type='float', + default=0) + def expand(self): + return self.get_float_param('expand_mm', 0) + + def shrink_or_grow_shape(self, amount): + if amount: + shape = self.shape.buffer(amount) if not isinstance(shape, shgeo.MultiPolygon): shape = shgeo.MultiPolygon([shape]) return shape else: return self.shape + @property + def underlay_shape(self): + return self.shrink_or_grow_shape(-self.fill_underlay_inset) + + @property + def fill_shape(self): + return self.shrink_or_grow_shape(self.expand) + def to_patches(self, last_patch): stitches = [] @@ -96,7 +119,7 @@ class AutoFill(Fill): starting_point)) starting_point = stitches[-1] - stitches.extend(auto_fill(self.shape, + stitches.extend(auto_fill(self.fill_shape, self.angle, self.row_spacing, self.end_row_spacing, -- cgit v1.2.3 From f10393989bdd2e7dd1056930ba060aab3870a592 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sun, 3 Jun 2018 23:24:26 -0400 Subject: realistic rendering checkboxes --- lib/extensions/print_pdf.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/extensions/print_pdf.py b/lib/extensions/print_pdf.py index 91562c09..3dcb2743 100644 --- a/lib/extensions/print_pdf.py +++ b/lib/extensions/print_pdf.py @@ -206,10 +206,10 @@ class PrintPreviewServer(Thread): @self.app.route('/realistic', methods=['GET']) def get_realistic(): - return jsonify({ - 'overview': self.realistic_overview_svg, - 'color_blocks': self.realistic_color_block_svgs - }) + realistic = { 'overview': self.realistic_overview_svg } + for i, svg in enumerate(self.realistic_color_block_svgs): + realistic["block%d" % i] = svg + return jsonify(realistic) def stop(self): # for whatever reason, shutting down only seems possible in -- cgit v1.2.3 From 692e033e71ed4655cef93be44c762bacf39caaee Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 4 Jun 2018 20:19:37 -0400 Subject: don't crash on empty subpaths --- lib/elements/element.py | 5 ++++- lib/elements/stroke.py | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/elements/element.py b/lib/elements/element.py index 300136dd..42f6c470 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -29,6 +29,10 @@ class Patch: else: raise TypeError("Patch can only be added to another Patch") + def __len__(self): + # This method allows `len(patch)` and `if patch: + return len(self.stitches) + def add_stitch(self, stitch): self.stitches.append(stitch) @@ -36,7 +40,6 @@ class Patch: return Patch(self.color, self.stitches[::-1]) - class Param(object): def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False, default=None, tooltip=None, sort_index=0): self.name = name diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index 48662b6d..097b36bc 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -97,6 +97,11 @@ class Stroke(EmbroideryElement): # TODO: use inkstitch.stitches.running_stitch patch = Patch(color=self.color) + + # can't stitch a single point + if len(emb_point_list) < 2: + return patch + p0 = emb_point_list[0] rho = 0.0 side = 1 @@ -156,6 +161,7 @@ class Stroke(EmbroideryElement): else: patch = self.stroke_points(path, self.zigzag_spacing / 2.0, stroke_width=self.stroke_width) - patches.append(patch) + if patch: + patches.append(patch) return patches -- cgit v1.2.3 From 0ff4f2a61bf295449ae0e7b9fdec75ff19c28771 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 4 Jun 2018 20:43:56 -0400 Subject: convert Stroke to use stitches.running_stitch --- lib/elements/stroke.py | 79 ++++++++++++++++++++++---------------------------- 1 file changed, 34 insertions(+), 45 deletions(-) (limited to 'lib') diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index 097b36bc..d3054132 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -3,7 +3,7 @@ import sys from .element import param, EmbroideryElement, Patch from ..i18n import _ from ..utils import cache, Point - +from ..stitches import running_stitch warned_about_legacy_running_stitch = False @@ -93,61 +93,50 @@ class Stroke(EmbroideryElement): else: return False - def stroke_points(self, emb_point_list, zigzag_spacing, stroke_width): - # TODO: use inkstitch.stitches.running_stitch + def simple_satin(self, path, zigzag_spacing, stroke_width): + "zig-zag along the path at the specified spacing and wdith" - patch = Patch(color=self.color) + # `self.zigzag_spacing` is the length for a zig and a zag + # together (a V shape). Start with running stitch at half + # that length: + patch = self.running_stitch(path, zigzag_spacing / 2.0) - # can't stitch a single point - if len(emb_point_list) < 2: - return patch + # Now move the points left and right. Consider each pair + # of points in turn, and move perpendicular to them, + # alternating left and right. - p0 = emb_point_list[0] - rho = 0.0 - side = 1 - last_segment_direction = None + offset = stroke_width / 2.0 - for repeat in xrange(self.repeats): - if repeat % 2 == 0: - order = range(1, len(emb_point_list)) - else: - order = range(-2, -len(emb_point_list) - 1, -1) + for i in xrange(len(patch) - 1): + start = patch.stitches[i] + end = patch.stitches[i + 1] + segment_direction = (end - start).unit() + zigzag_direction = segment_direction.rotate_left() - for segi in order: - p1 = emb_point_list[segi] + if i % 2 == 1: + zigzag_direction *= -1 - # how far we have to go along segment - seg_len = (p1 - p0).length() - if (seg_len == 0): - continue + patch.stitches[i] += zigzag_direction * offset - # vector pointing along segment - along = (p1 - p0).unit() + return patch - # vector pointing to edge of stroke width - perp = along.rotate_left() * (stroke_width * 0.5) + def running_stitch(self, path, stitch_length): + repeated_path = [] - if stroke_width == 0.0 and last_segment_direction is not None: - if abs(1.0 - along * last_segment_direction) > 0.5: - # if greater than 45 degree angle, stitch the corner - rho = zigzag_spacing - patch.add_stitch(p0) + # go back and forth along the path as specified by self.repeats + for i in xrange(self.repeats): + if i % 2 == 1: + # reverse every other pass + this_path = path[::-1] + else: + this_path = path[:] - # iteration variable: how far we are along segment - while (rho <= seg_len): - left_pt = p0 + along * rho + perp * side - patch.add_stitch(left_pt) - rho += zigzag_spacing - side = -side + repeated_path.extend(this_path) - p0 = p1 - last_segment_direction = along - rho -= seg_len + stitches = running_stitch(repeated_path, stitch_length) - if (p0 - patch.stitches[-1]).length() > 0.1: - patch.add_stitch(p0) + return Patch(self.color, stitches) - return patch def to_patches(self, last_patch): patches = [] @@ -157,9 +146,9 @@ class Stroke(EmbroideryElement): if self.manual_stitch_mode: patch = Patch(color=self.color, stitches=path, stitch_as_is=True) elif self.is_running_stitch(): - patch = self.stroke_points(path, self.running_stitch_length, stroke_width=0.0) + patch = self.running_stitch(path, self.running_stitch_length) else: - patch = self.stroke_points(path, self.zigzag_spacing / 2.0, stroke_width=self.stroke_width) + patch = self.simple_satin(path, self.zigzag_spacing, self.stroke_width) if patch: patches.append(patch) -- cgit v1.2.3 From fb273a6daa0654a48ca609eef470343733878146 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 8 Jun 2018 23:16:08 -0400 Subject: rasterize realistic SVGs at 600dpi --- lib/extensions/print_pdf.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'lib') diff --git a/lib/extensions/print_pdf.py b/lib/extensions/print_pdf.py index 3dcb2743..6e2eff58 100644 --- a/lib/extensions/print_pdf.py +++ b/lib/extensions/print_pdf.py @@ -204,12 +204,13 @@ class PrintPreviewServer(Thread): return jsonify(threads) - @self.app.route('/realistic', methods=['GET']) - def get_realistic(): - realistic = { 'overview': self.realistic_overview_svg } - for i, svg in enumerate(self.realistic_color_block_svgs): - realistic["block%d" % i] = svg - return jsonify(realistic) + @self.app.route('/realistic/block', methods=['GET']) + def get_realistic_block(index): + return Response(self.realistic_color_block_svgs[index], mimetype='image/svg+xml') + + @self.app.route('/realistic/overview', methods=['GET']) + def get_realistic_overview(): + return Response(self.realistic_overview_svg, mimetype='image/svg+xml') def stop(self): # for whatever reason, shutting down only seems possible in -- cgit v1.2.3 From ba9b50ab861087d094ee3f85324c65092c1b9f78 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 9 Jun 2018 21:23:21 -0400 Subject: fix latent bug in base.py --- lib/extensions/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/extensions/base.py b/lib/extensions/base.py index ff587ca5..52321cfc 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -63,10 +63,10 @@ class InkStitchMetadata(MutableMapping): else: item.getparent().remove(item) - def _find_item(self, name): + def _find_item(self, name, create=True): tag = inkex.addNS(name, "inkstitch") item = self.metadata.find(tag) - if item is None: + if item is None and create: item = inkex.etree.SubElement(self.metadata, tag) return item @@ -80,9 +80,9 @@ class InkStitchMetadata(MutableMapping): return None def __delitem__(self, name): - item = self._find_item(name) + item = self._find_item(name, create=False) - if item: + if item is not None: self.metadata.remove(item) def __iter__(self): -- cgit v1.2.3 From d06ff488f0977ab52dbf0169d85cc5d7a413e079 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sun, 10 Jun 2018 15:13:51 -0400 Subject: fix defaulting of stroke width to 1 --- lib/elements/element.py | 6 +++--- lib/elements/stroke.py | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/elements/element.py b/lib/elements/element.py index 42f6c470..39437c9f 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -135,10 +135,10 @@ class EmbroideryElement(object): self.node.set("embroider_%s" % name, str(value)) @cache - def get_style(self, style_name): + def get_style(self, style_name, default=None): style = simplestyle.parseStyle(self.node.get("style")) if (style_name not in style): - return None + return default value = style[style_name] if value == 'none': return None @@ -161,7 +161,7 @@ class EmbroideryElement(object): @property @cache def stroke_width(self): - width = self.get_style("stroke-width") + width = self.get_style("stroke-width", "1") if width is None: return 1.0 diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index d3054132..5239f978 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -57,10 +57,7 @@ class Stroke(EmbroideryElement): def is_running_stitch(self): # using stroke width <= 0.5 pixels to indicate running stitch is deprecated in favor of dashed lines - try: - stroke_width = float(self.get_style("stroke-width")) - except ValueError: - stroke_width = 1 + stroke_width = float(self.get_style("stroke-width", 1)) if self.dashed: return True -- cgit v1.2.3 From 406032c0f76a6caf1657d13d2b0956551fb7c726 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sun, 10 Jun 2018 15:31:10 -0400 Subject: handle SVG with no width/height --- lib/svg/units.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/svg/units.py b/lib/svg/units.py index 015da60e..126027bc 100644 --- a/lib/svg/units.py +++ b/lib/svg/units.py @@ -75,11 +75,24 @@ def convert_length(length): raise ValueError(_("Unknown unit: %s") % units) +@cache +def get_viewbox(svg): + return svg.get('viewBox').strip().replace(',', ' ').split() + @cache def get_doc_size(svg): - doc_width = convert_length(svg.get('width')) - doc_height = convert_length(svg.get('height')) + width = svg.get('width') + height = svg.get('height') + + if width is None or height is None: + # fall back to the dimensions from the viewBox + viewbox = get_viewbox(svg) + width = viewbox[2] + height = viewbox[3] + + doc_width = convert_length(width) + doc_height = convert_length(height) return doc_width, doc_height @@ -88,7 +101,7 @@ def get_viewbox_transform(node): # somewhat cribbed from inkscape-silhouette doc_width, doc_height = get_doc_size(node) - viewbox = node.get('viewBox').strip().replace(',', ' ').split() + viewbox = get_viewbox(node) dx = -float(viewbox[0]) dy = -float(viewbox[1]) -- cgit v1.2.3 From f79b3a7a95bba7d927cefd321d52eff819bb9180 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sun, 10 Jun 2018 15:43:17 -0400 Subject: default fill to black per SVG spec --- lib/elements/fill.py | 3 ++- lib/extensions/base.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/elements/fill.py b/lib/elements/fill.py index 52a42260..8d1d35f2 100644 --- a/lib/elements/fill.py +++ b/lib/elements/fill.py @@ -27,7 +27,8 @@ class Fill(EmbroideryElement): @property def color(self): - return self.get_style("fill") + # SVG spec says the default fill is black + return self.get_style("fill", "#000000") @property @param('flip', _('Flip fill (start right-to-left)'), type='boolean', default=False) diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 52321cfc..8edfe797 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -158,7 +158,7 @@ class InkstitchExtension(inkex.Effect): else: classes = [] - if element.get_style("fill"): + if element.get_style("fill", "black"): if element.get_boolean_param("auto_fill", True): classes.append(AutoFill) else: -- cgit v1.2.3 From 83efa9e02fc19a1f4bb0e1524aa601c48c5ca6ef Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sun, 10 Jun 2018 16:01:37 -0400 Subject: error if satin column set for path with single subpath --- lib/elements/satin_column.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'lib') diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index 3593db64..1d13c5e0 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -97,6 +97,8 @@ class SatinColumn(EmbroideryElement): def flattened_beziers(self): if len(self.csp) == 2: return self.simple_flatten_beziers() + elif len(self.csp) < 2: + self.fatal(_("satin column: %(id)s: at least two subpaths required (%(num)d found)") % dict(num=len(self.csp), id=self.node.get('id'))) else: return self.flatten_beziers_with_rungs() -- cgit v1.2.3 From d1042eb9dc0883c5949e046a713dfaa22a56951b Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sun, 10 Jun 2018 16:09:38 -0400 Subject: fix crash on 'use last settings' in Params --- lib/extensions/params.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'lib') diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 03a6f3cc..2e7d3f90 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -354,6 +354,9 @@ class SettingsFrame(wx.Frame): self.simulate_thread = None self.simulate_refresh_needed = Event() + # used when closing to avoid having the window reopen at the last second + self.disable_simulate_window = False + wx.CallLater(1000, self.update_simulator) self.presets_box = wx.StaticBox(self, wx.ID_ANY, label=_("Presets")) @@ -392,6 +395,9 @@ class SettingsFrame(wx.Frame): self.simulate_window.stop() self.simulate_window.clear() + if self.disable_simulate_window: + return + if not self.simulate_thread or not self.simulate_thread.is_alive(): self.simulate_thread = Thread(target=self.simulate_worker) self.simulate_thread.daemon = True @@ -586,6 +592,7 @@ class SettingsFrame(wx.Frame): self.close() def use_last(self, event): + self.disable_simulate_window = True self._load_preset("__LAST__") self.apply(event) -- cgit v1.2.3 From 350c292f8d0415fefefa83ce5ce84c2b5c17bd75 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 12 Jun 2018 20:18:55 -0400 Subject: show 'no embroiderable paths' error in Params too --- lib/extensions/base.py | 2 +- lib/extensions/params.py | 29 ++++++++++++++++++++--------- 2 files changed, 21 insertions(+), 10 deletions(-) (limited to 'lib') diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 8edfe797..4589132f 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -111,7 +111,7 @@ class InkstitchExtension(inkex.Effect): inkex.errormsg(_("No embroiderable paths selected.")) else: inkex.errormsg(_("No embroiderable paths found in document.")) - inkex.errormsg(_("Tip: use Path -> Object to Path to convert non-paths before embroidering.")) + inkex.errormsg(_("Tip: use Path -> Object to Path to convert non-paths.")) def descendants(self, node): nodes = [] diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 2e7d3f90..9d8de41b 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -639,6 +639,9 @@ class SettingsFrame(wx.Frame): self.Layout() # end wxGlade +class NoValidObjects(Exception): + pass + class Params(InkstitchExtension): def __init__(self, *args, **kwargs): self.cancelled = False @@ -696,6 +699,11 @@ class Params(InkstitchExtension): def create_tabs(self, parent): tabs = [] + nodes_by_class = self.get_nodes_by_class() + + if not nodes_by_class: + raise NoValidObjects() + for cls, nodes in self.get_nodes_by_class(): params = cls.get_params() @@ -752,12 +760,15 @@ class Params(InkstitchExtension): self.cancelled = True def effect(self): - app = wx.App() - frame = SettingsFrame(tabs_factory=self.create_tabs, on_cancel=self.cancel) - frame.Show() - app.MainLoop() - - if self.cancelled: - # This prevents the superclass from outputting the SVG, because we - # may have modified the DOM. - sys.exit(0) + try: + app = wx.App() + frame = SettingsFrame(tabs_factory=self.create_tabs, on_cancel=self.cancel) + frame.Show() + app.MainLoop() + + if self.cancelled: + # This prevents the superclass from outputting the SVG, because we + # may have modified the DOM. + sys.exit(0) + except NoValidObjects: + self.no_elements_error() -- cgit v1.2.3 From ede0e766d899d2f0aafd36915e5b599972f549c7 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 12 Jun 2018 21:28:02 -0400 Subject: add output extension --- lib/extensions/__init__.py | 1 + lib/extensions/output.py | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 lib/extensions/output.py (limited to 'lib') diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index ebdd2fc9..a4654d2c 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -4,3 +4,4 @@ from params import Params from print_pdf import Print from simulate import Simulate from input import Input +from output import Output diff --git a/lib/extensions/output.py b/lib/extensions/output.py new file mode 100644 index 00000000..72bbe37d --- /dev/null +++ b/lib/extensions/output.py @@ -0,0 +1,49 @@ +import sys +import traceback +import os +import inkex +import tempfile + +from .base import InkstitchExtension +from ..i18n import _ +from ..output import write_embroidery_file +from ..stitch_plan import patches_to_stitch_plan +from ..svg import render_stitch_plan, PIXELS_PER_MM + + +class Output(InkstitchExtension): + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self) + self.OptionParser.add_option("-c", "--collapse_len_mm", + action="store", type="float", + dest="collapse_length_mm", default=3.0, + help="max collapse length (mm)") + self.OptionParser.add_option("-f", "--format", + dest="file_extension", + help="file extension to output (example: DST)") + + def effect(self): + if not self.get_elements(): + return + + patches = self.elements_to_patches(self.elements) + stitch_plan = patches_to_stitch_plan(patches, self.options.collapse_length_mm * PIXELS_PER_MM) + + # libembroidery wants to write to an actual file rather than stdout + temp_file = tempfile.NamedTemporaryFile(suffix=".%s" % self.options.file_extension, delete=False) + + # in windows, failure to close here will keep the file locked + temp_file.close() + + write_embroidery_file(temp_file.name, stitch_plan, self.document.getroot()) + + # inkscape will read the file contents from stdout and copy + # to the destination file that the user chose + with open(temp_file.name) as output_file: + sys.stdout.write(output_file.read()) + + # clean up the temp file + os.remove(temp_file.name) + + # don't let inkex output the SVG! + sys.exit(0) -- cgit v1.2.3 From ea1135c1451ab2db54fd52fbf48a8eee9c5a43e0 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 12 Jun 2018 22:15:32 -0400 Subject: add ZIP batch export extension --- lib/extensions/__init__.py | 1 + lib/extensions/base.py | 9 +++++ lib/extensions/embroider.py | 3 +- lib/extensions/zip.py | 80 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 lib/extensions/zip.py (limited to 'lib') diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index a4654d2c..6d3e00d8 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -5,3 +5,4 @@ from print_pdf import Print from simulate import Simulate from input import Input from output import Output +from zip import Zip diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 4589132f..831b6dc6 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -200,6 +200,15 @@ class InkstitchExtension(inkex.Effect): def get_inkstitch_metadata(self): return InkStitchMetadata(self.document) + def get_base_file_name(self): + svg_filename = self.document.getroot().get(inkex.addNS('docname', 'sodipodi'), "embroidery.svg") + + if svg_filename.endswith('.svg'): + svg_filename = svg_filename[:-4] + + return svg_filename + + def parse(self): """Override inkex.Effect to add Ink/Stitch xml namespace""" diff --git a/lib/extensions/embroider.py b/lib/extensions/embroider.py index a213be64..1e994e27 100644 --- a/lib/extensions/embroider.py +++ b/lib/extensions/embroider.py @@ -44,8 +44,7 @@ class Embroider(InkstitchExtension): if self.options.output_file: output_path = os.path.join(self.options.path, self.options.output_file) else: - svg_filename = self.document.getroot().get(inkex.addNS('docname', 'sodipodi'), "embroidery.svg") - csv_filename = svg_filename.replace('.svg', '.%s' % self.options.output_format) + csv_filename = '%s.%s' % (self.get_base_file_name(), self.options.output_format) output_path = os.path.join(self.options.path, csv_filename) def add_suffix(path, suffix): diff --git a/lib/extensions/zip.py b/lib/extensions/zip.py new file mode 100644 index 00000000..4720ad1e --- /dev/null +++ b/lib/extensions/zip.py @@ -0,0 +1,80 @@ +import sys +import traceback +import os +import inkex +import tempfile +from zipfile import ZipFile +from libembroidery import * + +from .base import InkstitchExtension +from ..i18n import _ +from ..output import write_embroidery_file +from ..stitch_plan import patches_to_stitch_plan +from ..svg import render_stitch_plan, PIXELS_PER_MM + + +class Zip(InkstitchExtension): + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self) + self.OptionParser.add_option("-c", "--collapse_len_mm", + action="store", type="float", + dest="collapse_length_mm", default=3.0, + help="max collapse length (mm)") + + # it's kind of obnoxious that I have to do this... + self.formats = [] + formatList = embFormatList_create() + curFormat = formatList + while(curFormat): + # extension includes the dot, so we'll remove it + extension = embFormat_extension(curFormat)[1:] + description = embFormat_description(curFormat) + writer_state = embFormat_writerState(curFormat) + + if writer_state.strip() and embFormat_type(curFormat) != EMBFORMAT_OBJECTONLY: + self.OptionParser.add_option('--format-%s' % extension, type="inkbool", dest=extension) + self.formats.append(extension) + curFormat = curFormat.next + + def effect(self): + if not self.get_elements(): + return + + patches = self.elements_to_patches(self.elements) + stitch_plan = patches_to_stitch_plan(patches, self.options.collapse_length_mm * PIXELS_PER_MM) + + base_file_name = self.get_base_file_name() + path = tempfile.mkdtemp() + + files = [] + + 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()) + files.append(output_file) + + if not files: + self.errormsg(_("No embroidery file formats selected.")) + + temp_file = tempfile.NamedTemporaryFile(suffix=".zip", delete=False) + + # in windows, failure to close here will keep the file locked + temp_file.close() + + with ZipFile(temp_file.name, "w") as zip_file: + for file in files: + zip_file.write(file) + + # inkscape will read the file contents from stdout and copy + # to the destination file that the user chose + with open(temp_file.name) as output_file: + sys.stdout.write(output_file.read()) + + os.remove(temp_file.name) + for file in files: + os.remove(file) + os.rmdir(path) + + # don't let inkex output the SVG! + sys.exit(0) -- cgit v1.2.3 From f9a5e4c03a073d403222f0d5b7810cdaab90145a Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 13 Jun 2018 12:53:05 -0400 Subject: remove tmp directory from zip file paths --- lib/extensions/zip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/extensions/zip.py b/lib/extensions/zip.py index 4720ad1e..a7616536 100644 --- a/lib/extensions/zip.py +++ b/lib/extensions/zip.py @@ -64,7 +64,7 @@ class Zip(InkstitchExtension): with ZipFile(temp_file.name, "w") as zip_file: for file in files: - zip_file.write(file) + zip_file.write(file, os.path.basename(file)) # inkscape will read the file contents from stdout and copy # to the destination file that the user chose -- cgit v1.2.3 From 4c46c2eec1fb7cf9e85617030214bcb170b8b533 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 13 Jun 2018 20:10:22 -0400 Subject: fix zip file corruption --- lib/extensions/output.py | 4 +++- lib/extensions/zip.py | 10 +++++++++- lib/utils/io.py | 21 ++++++++++++++++++++- 3 files changed, 32 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/extensions/output.py b/lib/extensions/output.py index 72bbe37d..924c2d3a 100644 --- a/lib/extensions/output.py +++ b/lib/extensions/output.py @@ -35,12 +35,14 @@ class Output(InkstitchExtension): # in windows, failure to close here will keep the file locked temp_file.close() + # libembroidery likes to debug log things to stdout. No way to disable it. + save_stdout() write_embroidery_file(temp_file.name, stitch_plan, self.document.getroot()) # inkscape will read the file contents from stdout and copy # to the destination file that the user chose with open(temp_file.name) as output_file: - sys.stdout.write(output_file.read()) + sys.real_stdout.write(output_file.read()) # clean up the temp file os.remove(temp_file.name) diff --git a/lib/extensions/zip.py b/lib/extensions/zip.py index a7616536..ca12efdd 100644 --- a/lib/extensions/zip.py +++ b/lib/extensions/zip.py @@ -11,6 +11,7 @@ from ..i18n import _ from ..output import write_embroidery_file from ..stitch_plan import patches_to_stitch_plan from ..svg import render_stitch_plan, PIXELS_PER_MM +from ..utils.io import save_stdout class Zip(InkstitchExtension): @@ -48,12 +49,19 @@ class Zip(InkstitchExtension): files = [] + # libembroidery likes to debug log things to stdout. No way to disable it. + save_stdout() 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()) files.append(output_file) + # I'd love to do restore_stderr() here, but if I do, libembroidery's + # stuff still prints out and corrupts the zip! That's because it uses + # C's buffered stdout, so it hasn't actually written anything to the + # real standard output yet. + if not files: self.errormsg(_("No embroidery file formats selected.")) @@ -69,7 +77,7 @@ class Zip(InkstitchExtension): # inkscape will read the file contents from stdout and copy # to the destination file that the user chose with open(temp_file.name) as output_file: - sys.stdout.write(output_file.read()) + sys.real_stdout.write(output_file.read()) os.remove(temp_file.name) for file in files: diff --git a/lib/utils/io.py b/lib/utils/io.py index be1fdf24..44d48c2a 100644 --- a/lib/utils/io.py +++ b/lib/utils/io.py @@ -7,12 +7,31 @@ def save_stderr(): # GTK likes to spam stderr, which inkscape will show in a dialog. null = open(os.devnull, 'w') sys.stderr_dup = os.dup(sys.stderr.fileno()) + sys.real_stderr = os.fdopen(sys.stderr_dup, 'w') os.dup2(null.fileno(), 2) sys.stderr_backup = sys.stderr sys.stderr = StringIO() def restore_stderr(): + sys.real_stderr.close() os.dup2(sys.stderr_dup, 2) - sys.stderr_backup.write(sys.stderr.getvalue()) + sys.real_stderr.write(sys.stderr.getvalue()) sys.stderr = sys.stderr_backup + +# It's probably possible to generalize this code, but when I tried, +# the result was incredibly unreadable. +def save_stdout(): + null = open(os.devnull, 'w') + sys.stdout_dup = os.dup(sys.stdout.fileno()) + sys.real_stdout = os.fdopen(sys.stdout_dup, 'w') + os.dup2(null.fileno(), 1) + sys.stdout_backup = sys.stdout + sys.stdout = StringIO() + + +def restore_stdout(): + sys.real_stdout.close() + os.dup2(sys.stdout_dup, 1) + sys.real_stdout.write(sys.stdout.getvalue()) + sys.stdout = sys.stdout_backup -- cgit v1.2.3 From b674c192ee5ff7b3bbc48837379d1cea5f61b3bc Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 13 Jun 2018 20:45:51 -0400 Subject: fix issue with input plugin --- lib/extensions/input.py | 6 +++++- lib/utils/io.py | 8 ++------ 2 files changed, 7 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/extensions/input.py b/lib/extensions/input.py index 251859c5..21248dd9 100644 --- a/lib/extensions/input.py +++ b/lib/extensions/input.py @@ -14,6 +14,7 @@ from ..svg import PIXELS_PER_MM, render_stitch_plan from ..svg.tags import INKSCAPE_LABEL from ..i18n import _ from ..stitch_plan import StitchPlan +from ..utils.io import save_stdout class Input(object): @@ -25,6 +26,9 @@ class Input(object): def affect(self, args): + # libembroidery likes to dump a bunch of debugging stuff to stdout + save_stdout() + embroidery_file = args[0] pattern = embPattern_create() embPattern_read(pattern, embroidery_file) @@ -65,4 +69,4 @@ class Input(object): # Note: this is NOT the same as centering the design in the canvas! layer.set('transform', 'translate(%s,%s)' % (extents[0], extents[1])) - print etree.tostring(svg) + print >> sys.real_stdout, etree.tostring(svg) diff --git a/lib/utils/io.py b/lib/utils/io.py index 44d48c2a..e5a246f3 100644 --- a/lib/utils/io.py +++ b/lib/utils/io.py @@ -9,15 +9,13 @@ def save_stderr(): sys.stderr_dup = os.dup(sys.stderr.fileno()) sys.real_stderr = os.fdopen(sys.stderr_dup, 'w') os.dup2(null.fileno(), 2) - sys.stderr_backup = sys.stderr sys.stderr = StringIO() def restore_stderr(): - sys.real_stderr.close() os.dup2(sys.stderr_dup, 2) sys.real_stderr.write(sys.stderr.getvalue()) - sys.stderr = sys.stderr_backup + sys.stderr = sys.real_stderr # It's probably possible to generalize this code, but when I tried, # the result was incredibly unreadable. @@ -26,12 +24,10 @@ def save_stdout(): sys.stdout_dup = os.dup(sys.stdout.fileno()) sys.real_stdout = os.fdopen(sys.stdout_dup, 'w') os.dup2(null.fileno(), 1) - sys.stdout_backup = sys.stdout sys.stdout = StringIO() def restore_stdout(): - sys.real_stdout.close() os.dup2(sys.stdout_dup, 1) sys.real_stdout.write(sys.stdout.getvalue()) - sys.stdout = sys.stdout_backup + sys.stdout = sys.real_stdout -- cgit v1.2.3 From 0659bc294e943bcaa10f63966e667003623e6da4 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 16 Jun 2018 22:33:02 -0400 Subject: fix output regression --- lib/extensions/output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/extensions/output.py b/lib/extensions/output.py index 924c2d3a..f4b153e6 100644 --- a/lib/extensions/output.py +++ b/lib/extensions/output.py @@ -9,7 +9,7 @@ from ..i18n import _ from ..output import write_embroidery_file from ..stitch_plan import patches_to_stitch_plan from ..svg import render_stitch_plan, PIXELS_PER_MM - +from ..utils.io import save_stdout class Output(InkstitchExtension): def __init__(self, *args, **kwargs): -- cgit v1.2.3 From e29096ee138bd674e96a369a853d75eb7c919823 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Thu, 21 Jun 2018 15:41:06 -0400 Subject: add commands framework --- lib/commands.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/elements/element.py | 29 ++++++---------- lib/elements/stroke.py | 3 +- lib/extensions/base.py | 4 ++- lib/extensions/params.py | 3 +- lib/svg/__init__.py | 1 + lib/svg/path.py | 20 +++++++++++ lib/svg/tags.py | 5 +++ 8 files changed, 132 insertions(+), 22 deletions(-) create mode 100644 lib/commands.py create mode 100644 lib/svg/path.py (limited to 'lib') diff --git a/lib/commands.py b/lib/commands.py new file mode 100644 index 00000000..ec62d716 --- /dev/null +++ b/lib/commands.py @@ -0,0 +1,89 @@ +import inkex +import cubicsuperpath + +from .svg import apply_transforms +from .svg.tags import SVG_USE_TAG, SVG_SYMBOL_TAG, CONNECTION_START, CONNECTION_END, XLINK_HREF + + +class Command(object): + def __init__(self, connector): + self.connector = connector + self.svg = self.connector.getroottree().getroot() + + self.parse_command() + + def get_node_by_url(self, url): + # url will be #path12345. Find the object at the other end. + + if url is None: + raise ValueError("url is None") + + if not url.startswith('#'): + raise ValueError("invalid connection url: %s" % url) + + id = url[1:] + + try: + return self.svg.xpath(".//*[@id='%s']" % id)[0] + except (IndexError, AttributeError): + raise ValueError("could not find node by url %s" % id) + + def parse_connector_path(self): + path = cubicsuperpath.parsePath(self.connector.get('d')) + return apply_transforms(path, self.connector) + + def parse_command(self): + path = self.parse_connector_path() + + neighbors = [ + (self.get_node_by_url(self.connector.get(CONNECTION_START)), path[0][0][1]), + (self.get_node_by_url(self.connector.get(CONNECTION_END)), path[0][-1][1]) + ] + + if neighbors[0][0].tag != SVG_USE_TAG: + neighbors.reverse() + + if neighbors[0][0].tag != SVG_USE_TAG: + raise ValueError("connector does not point to a use tag") + + self.symbol = self.get_node_by_url(neighbors[0][0].get(XLINK_HREF)) + + if self.symbol.tag != SVG_SYMBOL_TAG: + raise ValueError("use points to non-symbol") + + self.command = self.symbol.get('id') + + if self.command.startswith('inkstitch_'): + self.command = self.command[10:] + else: + raise ValueError("symbol is not an Ink/Stitch command") + + self.target = neighbors[1][0] + self.target_point = neighbors[1][1] + + def __repr__(self): + return "Command('%s', %s)" % (self.command, self.target_point) + +def find_commands(node): + """Find the symbols this node is connected to and return them as Commands""" + + # find all paths that have this object as a connection + xpath = ".//*[@inkscape:connection-start='#%(id)s' or @inkscape:connection-end='#%(id)s']" % dict(id=node.get('id')) + connectors = node.getroottree().getroot().xpath(xpath, namespaces=inkex.NSS) + + # try to turn them into commands + commands = [] + for connector in connectors: + try: + commands.append(Command(connector)) + except ValueError: + import sys + import traceback + print >> sys.stderr, "not a Command:", connector.get('id'), traceback.format_exc() + # Parsing the connector failed, meaning it's not actually an Ink/Stitch command. + pass + + return commands + +def is_command(node): + return CONNECTION_START in node.attrib or CONNECTION_END in node.attrib diff --git a/lib/elements/element.py b/lib/elements/element.py index 39437c9f..465813d4 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -4,7 +4,8 @@ from shapely import geometry as shgeo from ..i18n import _ from ..utils import cache -from ..svg import PIXELS_PER_MM, get_viewbox_transform, convert_length, get_doc_size +from ..svg import PIXELS_PER_MM, convert_length, get_doc_size, apply_transforms +from ..commands import find_commands # inkscape-provided utilities import simpletransform @@ -171,10 +172,6 @@ class EmbroideryElement(object): @property def path(self): - return cubicsuperpath.parsePath(self.node.get("d")) - - @cache - def parse_path(self): # A CSP is a "cubic superpath". # # A "path" is a sequence of strung-together bezier curves. @@ -202,22 +199,16 @@ class EmbroideryElement(object): # In a path, each element in the 3-tuple is itself a tuple of (x, y). # Tuples all the way down. Hasn't anyone heard of using classes? - path = self.path - - # start with the identity transform - transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] - - # combine this node's transform with all parent groups' transforms - transform = simpletransform.composeParents(self.node, transform) - - # add in the transform implied by the viewBox - viewbox_transform = get_viewbox_transform(self.node.getroottree().getroot()) - transform = simpletransform.composeTransform(viewbox_transform, transform) + return cubicsuperpath.parsePath(self.node.get("d")) - # apply the combined transform to this node's path - simpletransform.applyTransformToPath(transform, path) + @cache + def parse_path(self): + return apply_transforms(self.path, self.node) - return path + @property + @cache + def commands(self): + return find_commands(self.node) def strip_control_points(self, subpath): return [point for control_before, point, control_after in subpath] diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index 5239f978..eca9e0ba 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -4,6 +4,7 @@ from .element import param, EmbroideryElement, Patch from ..i18n import _ from ..utils import cache, Point from ..stitches import running_stitch +from ..svg import parse_length_with_units warned_about_legacy_running_stitch = False @@ -57,7 +58,7 @@ class Stroke(EmbroideryElement): def is_running_stitch(self): # using stroke width <= 0.5 pixels to indicate running stitch is deprecated in favor of dashed lines - stroke_width = float(self.get_style("stroke-width", 1)) + stroke_width, units = parse_length_with_units(self.get_style("stroke-width", "1")) if self.dashed: return True diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 831b6dc6..78f75cf1 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -7,6 +7,7 @@ from collections import MutableMapping from ..svg.tags import * from ..elements import AutoFill, Fill, Stroke, SatinColumn, Polyline, EmbroideryElement from ..utils import cache +from ..commands import is_command SVG_METADATA_TAG = inkex.addNS("metadata", "svg") @@ -165,7 +166,8 @@ class InkstitchExtension(inkex.Effect): classes.append(Fill) if element.get_style("stroke"): - classes.append(Stroke) + if not is_command(element.node): + classes.append(Stroke) if element.get_boolean_param("stroke_first", False): classes.reverse() diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 9d8de41b..58fedd6b 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -19,6 +19,7 @@ from ..stitch_plan import patches_to_stitch_plan from ..elements import EmbroideryElement, Fill, AutoFill, Stroke, SatinColumn from ..utils import save_stderr, restore_stderr from ..simulator import EmbroiderySimulator +from ..commands import is_command def presets_path(): @@ -655,7 +656,7 @@ class Params(InkstitchExtension): classes.append(AutoFill) classes.append(Fill) - if element.get_style("stroke"): + if element.get_style("stroke") and not is_command(node): classes.append(Stroke) if element.get_style("stroke-dasharray") is None: diff --git a/lib/svg/__init__.py b/lib/svg/__init__.py index 1895bba4..50543b1b 100644 --- a/lib/svg/__init__.py +++ b/lib/svg/__init__.py @@ -1,2 +1,3 @@ from .svg import color_block_to_point_lists, render_stitch_plan from .units import * +from .path import apply_transforms diff --git a/lib/svg/path.py b/lib/svg/path.py new file mode 100644 index 00000000..a8012774 --- /dev/null +++ b/lib/svg/path.py @@ -0,0 +1,20 @@ +import simpletransform +import cubicsuperpath + +from .units import get_viewbox_transform + +def apply_transforms(path, node): + # start with the identity transform + transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] + + # combine this node's transform with all parent groups' transforms + transform = simpletransform.composeParents(node, transform) + + # add in the transform implied by the viewBox + viewbox_transform = get_viewbox_transform(node.getroottree().getroot()) + transform = simpletransform.composeTransform(viewbox_transform, transform) + + # apply the combined transform to this node's path + simpletransform.applyTransformToPath(transform, path) + + return path diff --git a/lib/svg/tags.py b/lib/svg/tags.py index fee59957..5488608c 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -5,8 +5,13 @@ SVG_PATH_TAG = inkex.addNS('path', 'svg') SVG_POLYLINE_TAG = inkex.addNS('polyline', 'svg') SVG_DEFS_TAG = inkex.addNS('defs', 'svg') SVG_GROUP_TAG = inkex.addNS('g', 'svg') +SVG_SYMBOL_TAG = inkex.addNS('symbol', 'svg') +SVG_USE_TAG = inkex.addNS('use', 'svg') 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') +XLINK_HREF = inkex.addNS('href', 'xlink') EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG) -- cgit v1.2.3 From 1f4bc62d960ab99c6911ab0a292d6ea52a309813 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 22 Jun 2018 22:19:57 -0400 Subject: add quick access methods for commands --- lib/elements/element.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'lib') diff --git a/lib/elements/element.py b/lib/elements/element.py index 465813d4..3c31f1b0 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -210,6 +210,22 @@ class EmbroideryElement(object): def commands(self): return find_commands(self.node) + @cache + def get_commands(self, command): + return [c for c in self.commands if c.command == command] + + @cache + def get_command(self, command): + commands = self.get_commands(command) + + if len(commands) == 1: + return commands[0] + elif len(commands) > 1: + raise ValueError(_("%(id)s has more than one command of type '%(command)s' linked to it") % + dict(id=self.node.get(id), command=command)) + else: + return None + def strip_control_points(self, subpath): return [point for control_before, point, control_after in subpath] -- cgit v1.2.3 From 0c527cc51e896f57d15c399c28c8c66c16d1cc59 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 22 Jun 2018 22:29:23 -0400 Subject: starting point specified by fill_start command --- lib/elements/auto_fill.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index 504bae2a..a2c63bd9 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -100,13 +100,21 @@ class AutoFill(Fill): def fill_shape(self): return self.shrink_or_grow_shape(self.expand) + def get_starting_point(self, last_patch): + # If there is a "fill_start" Command, then use that; otherwise pick + # the point closest to the end of the last patch. + + if self.get_command('fill_start'): + return self.get_command('fill_start').target_point + elif last_patch: + return last_patch.stitches[-1] + else: + return None + def to_patches(self, last_patch): stitches = [] - if last_patch is None: - starting_point = None - else: - starting_point = last_patch.stitches[-1] + starting_point = self.get_starting_point(last_patch) if self.fill_underlay: stitches.extend(auto_fill(self.underlay_shape, -- cgit v1.2.3 From abbda62835bfc99e49d0de1ccdffb6739dd2142e Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 22 Jun 2018 22:31:42 -0400 Subject: ending point speciifed by fill_end command --- lib/elements/auto_fill.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index a2c63bd9..59816878 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -111,10 +111,17 @@ class AutoFill(Fill): else: return None + def get_ending_point(self): + if self.get_command('fill_end'): + return self.get_command('fill_end').target_point + else: + return None + def to_patches(self, last_patch): stitches = [] starting_point = self.get_starting_point(last_patch) + ending_point = self.get_ending_point() if self.fill_underlay: stitches.extend(auto_fill(self.underlay_shape, @@ -134,6 +141,7 @@ class AutoFill(Fill): self.max_stitch_length, self.running_stitch_length, self.staggers, - starting_point)) + starting_point, + ending_point)) return [Patch(stitches=stitches, color=self.color)] -- cgit v1.2.3 From b7cb98d277bcad6a9b418fc11ee716bbc754e69d Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 23 Jun 2018 16:34:35 -0400 Subject: end on the ending point --- lib/stitches/auto_fill.py | 82 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 14 deletions(-) (limited to 'lib') diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 518a2812..c558fa1b 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -15,17 +15,17 @@ class MaxQueueLengthExceeded(Exception): pass -def auto_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, running_stitch_length, staggers, starting_point=None): +def auto_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, running_stitch_length, staggers, starting_point, ending_point=None): stitches = [] rows_of_segments = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing) segments = [segment for row in rows_of_segments for segment in row] graph = build_graph(shape, segments, angle, row_spacing) - path = find_stitch_path(graph, segments) + path = find_stitch_path(graph, segments, starting_point, ending_point) - if starting_point: - stitches.extend(connect_points(shape, starting_point, path[0][0], running_stitch_length)) +# if starting_point: +# stitches.extend(connect_points(shape, starting_point, path[0][0], running_stitch_length)) stitches.extend(path_to_stitches(graph, path, shape, angle, row_spacing, max_stitch_length, running_stitch_length, staggers)) @@ -272,7 +272,7 @@ def insert_loop(path, loop): start and end point. The points will be specified in order, such that they will look like this: - ((p1, p2), (p2, p3), (p3, p4) ... (pn, p1)) + ((p1, p2), (p2, p3), (p3, p4), ...) path will be modified in place. """ @@ -282,11 +282,55 @@ def insert_loop(path, loop): for i, (start, end) in enumerate(path): if start == loop_start: break + else: + # if we didn't find the start of the loop in the list at all, it must + # be the endpoint of the last segment + i += 1 path[i:i] = loop +def nearest_node_on_outline(graph, point, outline_index=0): + point = shapely.geometry.Point(*point) + outline_nodes = [node for node, data in graph.nodes(data=True) if data['index'] == outline_index] + nearest = min(outline_nodes, key=lambda node: shapely.geometry.Point(*node).distance(point)) + + return nearest + +def get_outline_nodes(graph, outline_index=0): + outline_nodes = [(node, data['projection']) \ + for node, data \ + in graph.nodes(data=True) \ + if data['index'] == outline_index] + outline_nodes.sort(key=lambda (node, projection): projection) + outline_nodes = [node for node, data in outline_nodes] + + return outline_nodes + +def find_initial_path(graph, starting_point, ending_point=None): + starting_node = nearest_node_on_outline(graph, starting_point) + + if ending_point is None: + # If they didn't give an ending point, pick either neighboring node + # along the outline -- doesn't matter which. This effectively means + # we end where we started. + neighbors = [n for n, keys in graph.adj[starting_node].iteritems() if 'outline' in keys] + return [(starting_node, neighbors[0])] + else: + ending_node = nearest_node_on_outline(graph, ending_point) + outline_nodes = get_outline_nodes(graph) + + # Multiply the outline_nodes list by 2 (duplicate it) because + # the ending_node may occur first. + outline_nodes *= 2 + start_index = outline_nodes.index(starting_node) + end_index = outline_nodes.index(ending_node, start_index) + path = outline_nodes[start_index:end_index + 1] + + # we have a series of sequential points, but we need to + # turn it into an edge list + return node_list_to_edge_list(path) -def find_stitch_path(graph, segments): +def find_stitch_path(graph, segments, starting_point=None, ending_point=None): """find a path that visits every grating segment exactly once Theoretically, we just need to find an Eulerian Path in the graph. @@ -313,14 +357,20 @@ def find_stitch_path(graph, segments): segments_visited = 0 nodes_visited = deque() - # start with a simple loop: down one segment and then back along the - # outer border to the starting point. - path = [segments[0], list(reversed(segments[0]))] + if starting_point is None: + starting_point = segments[0][0] + path = find_initial_path(graph, starting_point, ending_point) - graph.remove_edges_from(path) + # We're starting with a path and _not_ removing the edges of that path from + # the graph. This means we're implicitly adding those edges to the graph. + # That means that the starting and ending point (and only those two) will + # now have odd degree. That means that there must exist an Eulerian + # Path that starts and ends at those two nodes. - segments_visited += 1 - nodes_visited.extend(segments[0]) + nodes_visited.append(path[0][0]) + #print >> sys.stderr, "starting path:", path + + #return path while segments_visited < num_segments: result = find_loop(graph, nodes_visited) @@ -331,15 +381,19 @@ def find_stitch_path(graph, segments): loop, segments = result - #print >> dbg, "found loop:", loop + #print >> sys.stderr, "found loop:", loop #dbg.flush() segments_visited += segments - nodes_visited += [edge[0] for edge in loop] + nodes_visited.extend(edge[0] for edge in loop) graph.remove_edges_from(loop) insert_loop(path, loop) + #print >> sys.stderr, "loop made, nodes_visited:", nodes_visited + + #print >> sys.stderr, "path:", path + #if segments_visited >= 12: # break -- cgit v1.2.3 From e0a2b31ede8b412c83747fef168c3dda0d07087f Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 23 Jun 2018 21:41:00 -0400 Subject: fix collapse_sequential_outline_edges --- lib/stitches/auto_fill.py | 77 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 25 deletions(-) (limited to 'lib') diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index c558fa1b..42fd1ef5 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -2,7 +2,7 @@ import sys import shapely import networkx import math -from itertools import groupby +from itertools import groupby, izip from collections import deque from .fill import intersect_region_with_grating, row_num, stitch_row @@ -14,6 +14,29 @@ from ..utils.geometry import Point as InkstitchPoint class MaxQueueLengthExceeded(Exception): pass +class PathEdge(object): + OUTLINE_KEYS = ("outline", "extra", "initial") + SEGMENT_KEY = "segment" + + def __init__(self, nodes, key): + self.nodes = nodes + self._sorted_nodes = tuple(sorted(self.nodes)) + self.key = key + + def __getitem__(self, item): + return self.nodes[item] + + def __hash__(self): + return hash((self._sorted_nodes, self.key)) + + def __eq__(self, other): + return self._sorted_nodes == other._sorted_nodes and self.key == other.key + + def is_outline(self): + return self.key in self.OUTLINE_KEYS + + def is_segment(self): + return self.key == self.SEGMENT_KEY def auto_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, running_stitch_length, staggers, starting_point, ending_point=None): stitches = [] @@ -157,14 +180,20 @@ def node_list_to_edge_list(node_list): def bfs_for_loop(graph, starting_node, max_queue_length=2000): to_search = deque() - to_search.appendleft(([starting_node], set(), 0)) + to_search.append((None, set())) while to_search: if len(to_search) > max_queue_length: raise MaxQueueLengthExceeded() - path, visited_edges, visited_segments = to_search.pop() - ending_node = path[-1] + path, visited_edges = to_search.pop() + + if path is None: + # This is the very first time through the loop, so initialize. + path = [] + ending_node = starting_node + else: + ending_node = path[-1][-1] # get a list of neighbors paired with the key of the edge I can follow to get there neighbors = [ @@ -178,26 +207,21 @@ def bfs_for_loop(graph, starting_node, max_queue_length=2000): for next_node, key in neighbors: # skip if I've already followed this edge - edge = (tuple(sorted((ending_node, next_node))), key) + edge = PathEdge((ending_node, next_node), key) if edge in visited_edges: continue - new_path = path + [next_node] - - if key == "segment": - new_visited_segments = visited_segments + 1 - else: - new_visited_segments = visited_segments + new_path = path + [edge] if next_node == starting_node: # ignore trivial loops (down and back a doubled edge) if len(new_path) > 3: - return node_list_to_edge_list(new_path), new_visited_segments + return new_path new_visited_edges = visited_edges.copy() new_visited_edges.add(edge) - to_search.appendleft((new_path, new_visited_edges, new_visited_segments)) + to_search.appendleft((new_path, new_visited_edges)) def find_loop(graph, starting_nodes): @@ -314,7 +338,7 @@ def find_initial_path(graph, starting_point, ending_point=None): # along the outline -- doesn't matter which. This effectively means # we end where we started. neighbors = [n for n, keys in graph.adj[starting_node].iteritems() if 'outline' in keys] - return [(starting_node, neighbors[0])] + return [PathEdge((starting_node, neighbors[0]), "initial")] else: ending_node = nearest_node_on_outline(graph, ending_point) outline_nodes = get_outline_nodes(graph) @@ -324,11 +348,15 @@ def find_initial_path(graph, starting_point, ending_point=None): outline_nodes *= 2 start_index = outline_nodes.index(starting_node) end_index = outline_nodes.index(ending_node, start_index) - path = outline_nodes[start_index:end_index + 1] + nodes = outline_nodes[start_index:end_index + 1] # we have a series of sequential points, but we need to # turn it into an edge list - return node_list_to_edge_list(path) + path = [] + for start, end in izip(nodes[:-1], nodes[1:]): + path.append(PathEdge((start, end), "initial")) + + return path def find_stitch_path(graph, segments, starting_point=None, ending_point=None): """find a path that visits every grating segment exactly once @@ -368,23 +396,22 @@ def find_stitch_path(graph, segments, starting_point=None, ending_point=None): # Path that starts and ends at those two nodes. nodes_visited.append(path[0][0]) + #print >> sys.stderr, "nodes_visited", nodes_visited #print >> sys.stderr, "starting path:", path #return path while segments_visited < num_segments: - result = find_loop(graph, nodes_visited) + loop = find_loop(graph, nodes_visited) - if not result: + if not loop: print >> sys.stderr, _("Unexpected error while generating fill stitches. Please send your SVG file to lexelby@github.") break - loop, segments = result - #print >> sys.stderr, "found loop:", loop #dbg.flush() - segments_visited += segments + segments_visited += sum(1 for edge in loop if edge.is_segment()) nodes_visited.extend(edge[0] for edge in loop) graph.remove_edges_from(loop) @@ -417,10 +444,10 @@ def collapse_sequential_outline_edges(graph, path): new_path = [] for edge in path: - if graph.has_edge(*edge, key="segment"): + if edge.is_segment(): if start_of_run: # close off the last run - new_path.append((start_of_run, edge[0])) + new_path.append(PathEdge((start_of_run, edge[0]), "collapsed")) start_of_run = None new_path.append(edge) @@ -430,7 +457,7 @@ def collapse_sequential_outline_edges(graph, path): if start_of_run: # if we were still in a run, close it off - new_path.append((start_of_run, edge[1])) + new_path.append(PathEdge((start_of_run, edge[1]), "collapsed")) return new_path @@ -496,7 +523,7 @@ def path_to_stitches(graph, path, shape, angle, row_spacing, max_stitch_length, stitches = [] for edge in path: - if graph.has_edge(*edge, key="segment"): + if edge.is_segment(): stitch_row(stitches, edge[0], edge[1], angle, row_spacing, max_stitch_length, staggers) else: stitches.extend(connect_points(shape, edge[0], edge[1], running_stitch_length)) -- cgit v1.2.3 From 78efaf120f08883e00428d54c2035e63934566ba Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 23 Jun 2018 22:53:17 -0400 Subject: remove unnecessary travel back to start --- lib/stitches/auto_fill.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 42fd1ef5..c7c3cdec 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -47,8 +47,10 @@ def auto_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, run graph = build_graph(shape, segments, angle, row_spacing) path = find_stitch_path(graph, segments, starting_point, ending_point) -# if starting_point: -# stitches.extend(connect_points(shape, starting_point, path[0][0], running_stitch_length)) + if ending_point is None: + # The end of the path travels around the outline back to the start. + # This isn't necessary, so remove it. + trim_end(path) stitches.extend(path_to_stitches(graph, path, shape, angle, row_spacing, max_stitch_length, running_stitch_length, staggers)) @@ -516,6 +518,9 @@ def connect_points(shape, start, end, running_stitch_length): return stitches +def trim_end(path): + while path and path[-1].is_outline(): + path.pop() def path_to_stitches(graph, path, shape, angle, row_spacing, max_stitch_length, running_stitch_length, staggers): path = collapse_sequential_outline_edges(graph, path) -- cgit v1.2.3 From 3950be13160dca1117cdebac0ab41332ac744e40 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 23 Jun 2018 23:10:01 -0400 Subject: tidy comments --- lib/stitches/auto_fill.py | 79 ++++++++++++++--------------------------------- 1 file changed, 23 insertions(+), 56 deletions(-) (limited to 'lib') diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index c7c3cdec..6326ced2 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -47,11 +47,6 @@ def auto_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, run graph = build_graph(shape, segments, angle, row_spacing) path = find_stitch_path(graph, segments, starting_point, ending_point) - if ending_point is None: - # The end of the path travels around the outline back to the start. - # This isn't necessary, so remove it. - trim_end(path) - stitches.extend(path_to_stitches(graph, path, shape, angle, row_spacing, max_stitch_length, running_stitch_length, staggers)) return stitches @@ -159,8 +154,6 @@ def build_graph(shape, segments, angle, row_spacing): else: edge_set = 1 - #print >> sys.stderr, outline_index, "es", edge_set, "rn", row_num, inkstitch.Point(*nodes[0]) * self.north(angle), inkstitch.Point(*nodes[1]) * self.north(angle) - # add an edge between each successive node for i, (node1, node2) in enumerate(zip(nodes, nodes[1:] + [nodes[0]])): graph.add_edge(node1, node2, key="outline") @@ -242,14 +235,6 @@ def find_loop(graph, starting_nodes): somewhere else. """ - #loop = self.simple_loop(graph, starting_nodes[-2]) - - #if loop: - # print >> sys.stderr, "simple_loop success" - # starting_nodes.pop() - # starting_nodes.pop() - # return loop - loop = None retry = [] max_queue_length = 2000 @@ -257,7 +242,6 @@ def find_loop(graph, starting_nodes): while not loop: while not loop and starting_nodes: starting_node = starting_nodes.pop() - #print >> sys.stderr, "find loop from", starting_node try: # Note: if bfs_for_loop() returns None, no loop can be @@ -266,12 +250,7 @@ def find_loop(graph, starting_nodes): # case we discard that node and try the next. loop = bfs_for_loop(graph, starting_node, max_queue_length) - #if not loop: - #print >> dbg, "failed on", starting_node - #dbg.flush() except MaxQueueLengthExceeded: - #print >> dbg, "gave up on", starting_node - #dbg.flush() # We're giving up on this node for now. We could try # this node again later, so add it to the bottm of the # stack. @@ -337,8 +316,8 @@ def find_initial_path(graph, starting_point, ending_point=None): if ending_point is None: # If they didn't give an ending point, pick either neighboring node - # along the outline -- doesn't matter which. This effectively means - # we end where we started. + # along the outline -- doesn't matter which. We do this because + # the algorithm requires we start with _some_ path. neighbors = [n for n, keys in graph.adj[starting_node].iteritems() if 'outline' in keys] return [PathEdge((starting_node, neighbors[0]), "initial")] else: @@ -368,13 +347,14 @@ def find_stitch_path(graph, segments, starting_point=None, ending_point=None): The edges on the outline of the region are only there to help us get from one grating segment to the next. - We'll build a "cycle" (a path that ends where it starts) using - Hierholzer's algorithm. We'll stop once we've visited every grating - segment. + We'll build a Eulerian Path using Hierholzer's algorithm. A true + Eulerian Path would visit every single edge (including all the extras + we inserted in build_graph()),but we'll stop short once we've visited + every grating segment since that's all we really care about. Hierholzer's algorithm says to select an arbitrary starting node at each step. In order to produce a reasonable stitch path, we'll select - the vertex carefully such that we get back-and-forth traversal like + the starting node carefully such that we get back-and-forth traversal like mowing a lawn. To do this, we'll use a simple heuristic: try to start from nodes in @@ -389,19 +369,21 @@ def find_stitch_path(graph, segments, starting_point=None, ending_point=None): if starting_point is None: starting_point = segments[0][0] + path = find_initial_path(graph, starting_point, ending_point) - # We're starting with a path and _not_ removing the edges of that path from - # the graph. This means we're implicitly adding those edges to the graph. - # That means that the starting and ending point (and only those two) will - # now have odd degree. That means that there must exist an Eulerian - # Path that starts and ends at those two nodes. + # Our graph is Eulerian: every node has an even degree. An Eulerian graph + # must have an Eulerian Circuit which visits every edge and ends where it + # starts. + # + # However, we're starting with a path and _not_ removing the edges of that + # path from the graph. By doing this, we're implicitly adding those edges + # to the graph, after which the starting and ending point (and only those + # two) will now have odd degree. A graph that's Eulerian except for two + # nodes must have an Eulerian Path that starts and ends at those two nodes. + # That's how we force the starting and ending point. nodes_visited.append(path[0][0]) - #print >> sys.stderr, "nodes_visited", nodes_visited - #print >> sys.stderr, "starting path:", path - - #return path while segments_visited < num_segments: loop = find_loop(graph, nodes_visited) @@ -410,26 +392,17 @@ def find_stitch_path(graph, segments, starting_point=None, ending_point=None): print >> sys.stderr, _("Unexpected error while generating fill stitches. Please send your SVG file to lexelby@github.") break - #print >> sys.stderr, "found loop:", loop - #dbg.flush() - segments_visited += sum(1 for edge in loop if edge.is_segment()) nodes_visited.extend(edge[0] for edge in loop) graph.remove_edges_from(loop) insert_loop(path, loop) - #print >> sys.stderr, "loop made, nodes_visited:", nodes_visited - - #print >> sys.stderr, "path:", path - - #if segments_visited >= 12: - # break - - # Now we have a loop that covers every grating segment. It returns to - # where it started, which is unnecessary, so we'll snip the last bit off. - #while original_graph.has_edge(*path[-1], key="outline"): - # path.pop() + if ending_point is None: + # If they didn't specify an ending point, then the end of the path travels + # around the outline back to the start (see find_initial_path()). This + # isn't necessary, so remove it. + trim_end(path) return path @@ -499,9 +472,6 @@ def connect_points(shape, start, end, running_stitch_length): direction = math.copysign(1.0, distance) one_stitch = running_stitch_length * direction - #print >> dbg, "connect_points:", outline_index, start, end, distance, stitches, direction - #dbg.flush() - stitches = [InkstitchPoint(*outline.interpolate(pos).coords[0])] for i in xrange(num_stitches): @@ -513,9 +483,6 @@ def connect_points(shape, start, end, running_stitch_length): if (end - stitches[-1]).length() > 0.1 * PIXELS_PER_MM: stitches.append(end) - #print >> dbg, "end connect_points" - #dbg.flush() - return stitches def trim_end(path): -- cgit v1.2.3 From 0e4fab06c4bec76d90e7f18580b0272ce74af439 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sun, 24 Jun 2018 15:55:13 -0400 Subject: installer now installs symbols too --- lib/extensions/__init__.py | 2 +- lib/extensions/install.py | 123 +++++++++++++++++++++++++++++++++++++++++++++ lib/extensions/palettes.py | 112 ----------------------------------------- 3 files changed, 124 insertions(+), 113 deletions(-) create mode 100644 lib/extensions/install.py delete mode 100644 lib/extensions/palettes.py (limited to 'lib') diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index 6d3e00d8..b8951e12 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -1,5 +1,5 @@ from embroider import Embroider -from palettes import Palettes +from install import Install from params import Params from print_pdf import Print from simulate import Simulate diff --git a/lib/extensions/install.py b/lib/extensions/install.py new file mode 100644 index 00000000..5ce511e7 --- /dev/null +++ b/lib/extensions/install.py @@ -0,0 +1,123 @@ +# -*- coding: UTF-8 -*- + +import sys +import traceback +import os +from os.path import realpath, dirname +from glob import glob +from threading import Thread +import socket +import errno +import time +import logging +import wx +import inkex + +from ..utils import guess_inkscape_config_path + + +class InstallerFrame(wx.Frame): + def __init__(self, *args, **kwargs): + wx.Frame.__init__(self, *args, **kwargs) + + default_path = guess_inkscape_config_path() + + panel = wx.Panel(self) + sizer = wx.BoxSizer(wx.VERTICAL) + + text_sizer = wx.BoxSizer(wx.HORIZONTAL) + + text = _('Ink/Stitch can install files ("add-ons") that make it easier to use Inkscape to create machine embroidery designs. These add-ons will be installed:') + \ + "\n\n • " + _("thread manufacturer color palettes") + \ + "\n • " + _("Ink/Stitch visual commands (Object -> Symbols...)") + \ + "\n\n" + _("Directory in which to install add-ons:") + + static_text = wx.StaticText(panel, label=text) + font = wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.NORMAL) + static_text.SetFont(font) + text_sizer.Add(static_text, proportion=0, flag=wx.ALL|wx.EXPAND, border=10) + sizer.Add(text_sizer, proportion=3, flag=wx.ALL|wx.EXPAND, border=0) + + path_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.path_input = wx.TextCtrl(panel, wx.ID_ANY, value=default_path) + path_sizer.Add(self.path_input, proportion=3, flag=wx.RIGHT, border=20) + chooser_button = wx.Button(panel, wx.ID_OPEN, _('Choose another directory...')) + path_sizer.Add(chooser_button, proportion=1, flag=0) + sizer.Add(path_sizer, proportion=1, flag=wx.ALL|wx.ALIGN_BOTTOM, border=10) + + buttons_sizer = wx.BoxSizer(wx.HORIZONTAL) + install_button = wx.Button(panel, wx.ID_ANY, _("Install")) + install_button.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_TICK_MARK)) + buttons_sizer.Add(install_button, proportion=0, flag=wx.ALIGN_RIGHT|wx.ALL, border=5) + cancel_button = wx.Button(panel, wx.ID_CANCEL, _("Cancel")) + buttons_sizer.Add(cancel_button, proportion=0, flag=wx.ALIGN_RIGHT|wx.ALL, border=5) + sizer.Add(buttons_sizer, proportion=1, flag=wx.ALIGN_RIGHT|wx.ALIGN_BOTTOM) + + #outer_sizer = wx.BoxSizer(wx.HORIZONTAL) + #outer_sizer.Add(sizer, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL) + + panel.SetSizer(sizer) + panel.Layout() + + chooser_button.Bind(wx.EVT_BUTTON, self.chooser_button_clicked) + cancel_button.Bind(wx.EVT_BUTTON, self.cancel_button_clicked) + install_button.Bind(wx.EVT_BUTTON, self.install_button_clicked) + + def cancel_button_clicked(self, event): + self.Destroy() + + def chooser_button_clicked(self, event): + dialog = wx.DirDialog(self, _("Choose Inkscape directory")) + if dialog.ShowModal() != wx.ID_CANCEL: + self.path_input.SetValue(dialog.GetPath()) + + def install_button_clicked(self, event): + try: + self.install_addons('palettes') + self.install_addons('symbols') + except Exception, e: + wx.MessageDialog(self, + _('Inkscape add-on installation failed') + ': \n' + traceback.format_exc(), + _('Installation Failed'), + wx.OK).ShowModal() + else: + wx.MessageDialog(self, + _('Inkscape add-on files have been installed. Please restart Inkscape to load the new add-ons.'), + _('Installation Completed'), + wx.OK).ShowModal() + + self.Destroy() + + def install_addons(self, type): + path = os.path.join(self.path_input.GetValue(), type) + src_dir = self.get_bundled_dir(type) + self.copy_files(glob(os.path.join(src_dir, "*")), path) + + def get_bundled_dir(self, name): + if getattr(sys, 'frozen', None) is not None: + return realpath(os.path.join(sys._MEIPASS, '..', name)) + else: + return realpath(os.path.join(dirname(realpath(__file__)), '..', '..', name)) + + if (sys.platform == "win32"): + # If we try to just use shutil.copy it says the operation requires elevation. + def copy_files(self, files, dest): + import winutils + + winutils.copy(files, dest) + else: + def copy_files(self, files, dest): + import shutil + + if not os.path.exists(dest): + os.makedirs(dest) + + for palette_file in files: + shutil.copy(palette_file, dest) + +class Install(inkex.Effect): + def effect(self): + app = wx.App() + installer_frame = InstallerFrame(None, title=_("Ink/Stitch Add-ons Installer"), size=(550, 350)) + installer_frame.Show() + app.MainLoop() diff --git a/lib/extensions/palettes.py b/lib/extensions/palettes.py deleted file mode 100644 index f7a6c7a5..00000000 --- a/lib/extensions/palettes.py +++ /dev/null @@ -1,112 +0,0 @@ -import sys -import traceback -import os -from os.path import realpath, dirname -from glob import glob -from threading import Thread -import socket -import errno -import time -import logging -import wx -import inkex - -from ..utils import guess_inkscape_config_path - - -class InstallPalettesFrame(wx.Frame): - def __init__(self, *args, **kwargs): - wx.Frame.__init__(self, *args, **kwargs) - - default_path = os.path.join(guess_inkscape_config_path(), "palettes") - - panel = wx.Panel(self) - sizer = wx.BoxSizer(wx.VERTICAL) - - text = wx.StaticText(panel, label=_("Directory in which to install palettes:")) - font = wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.NORMAL) - text.SetFont(font) - sizer.Add(text, proportion=0, flag=wx.ALL|wx.EXPAND, border=10) - - path_sizer = wx.BoxSizer(wx.HORIZONTAL) - self.path_input = wx.TextCtrl(panel, wx.ID_ANY, value=default_path) - path_sizer.Add(self.path_input, proportion=3, flag=wx.RIGHT|wx.EXPAND, border=20) - chooser_button = wx.Button(panel, wx.ID_OPEN, _('Choose another directory...')) - path_sizer.Add(chooser_button, proportion=1, flag=wx.EXPAND) - sizer.Add(path_sizer, proportion=0, flag=wx.ALL|wx.EXPAND, border=10) - - buttons_sizer = wx.BoxSizer(wx.HORIZONTAL) - install_button = wx.Button(panel, wx.ID_ANY, _("Install")) - install_button.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_TICK_MARK)) - buttons_sizer.Add(install_button, proportion=0, flag=wx.ALIGN_RIGHT|wx.ALL, border=5) - cancel_button = wx.Button(panel, wx.ID_CANCEL, _("Cancel")) - buttons_sizer.Add(cancel_button, proportion=0, flag=wx.ALIGN_RIGHT|wx.ALL, border=5) - sizer.Add(buttons_sizer, proportion=0, flag=wx.ALIGN_RIGHT) - - outer_sizer = wx.BoxSizer(wx.HORIZONTAL) - outer_sizer.Add(sizer, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL) - - panel.SetSizer(outer_sizer) - panel.Layout() - - chooser_button.Bind(wx.EVT_BUTTON, self.chooser_button_clicked) - cancel_button.Bind(wx.EVT_BUTTON, self.cancel_button_clicked) - install_button.Bind(wx.EVT_BUTTON, self.install_button_clicked) - - def cancel_button_clicked(self, event): - self.Destroy() - - def chooser_button_clicked(self, event): - dialog = wx.DirDialog(self, _("Choose Inkscape palettes directory")) - if dialog.ShowModal() != wx.ID_CANCEL: - self.path_input.SetValue(dialog.GetPath()) - - def install_button_clicked(self, event): - try: - self.install_palettes() - except Exception, e: - wx.MessageDialog(self, - _('Thread palette installation failed') + ': \n' + traceback.format_exc(), - _('Installation Failed'), - wx.OK).ShowModal() - else: - wx.MessageDialog(self, - _('Thread palette files have been installed. Please restart Inkscape to load the new palettes.'), - _('Installation Completed'), - wx.OK).ShowModal() - - self.Destroy() - - def install_palettes(self): - path = self.path_input.GetValue() - palettes_dir = self.get_bundled_palettes_dir() - self.copy_files(glob(os.path.join(palettes_dir, "*")), path) - - def get_bundled_palettes_dir(self): - if getattr(sys, 'frozen', None) is not None: - return realpath(os.path.join(sys._MEIPASS, '..', 'palettes')) - else: - return os.path.join(dirname(realpath(__file__)), 'palettes') - - if (sys.platform == "win32"): - # If we try to just use shutil.copy it says the operation requires elevation. - def copy_files(self, files, dest): - import winutils - - winutils.copy(files, dest) - else: - def copy_files(self, files, dest): - import shutil - - if not os.path.exists(dest): - os.makedirs(dest) - - for palette_file in files: - shutil.copy(palette_file, dest) - -class Palettes(inkex.Effect): - def effect(self): - app = wx.App() - installer_frame = InstallPalettesFrame(None, title=_("Ink/Stitch Thread Palette Installer"), size=(450, 200)) - installer_frame.Show() - app.MainLoop() -- cgit v1.2.3 From de4ead1ad467997fa81a4459e194769dfab185e2 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Thu, 28 Jun 2018 20:32:09 -0400 Subject: remove directory picker from install extension --- lib/extensions/install.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) (limited to 'lib') diff --git a/lib/extensions/install.py b/lib/extensions/install.py index 5ce511e7..d55b96d0 100644 --- a/lib/extensions/install.py +++ b/lib/extensions/install.py @@ -20,7 +20,7 @@ class InstallerFrame(wx.Frame): def __init__(self, *args, **kwargs): wx.Frame.__init__(self, *args, **kwargs) - default_path = guess_inkscape_config_path() + self.path = guess_inkscape_config_path() panel = wx.Panel(self) sizer = wx.BoxSizer(wx.VERTICAL) @@ -29,8 +29,7 @@ class InstallerFrame(wx.Frame): text = _('Ink/Stitch can install files ("add-ons") that make it easier to use Inkscape to create machine embroidery designs. These add-ons will be installed:') + \ "\n\n • " + _("thread manufacturer color palettes") + \ - "\n • " + _("Ink/Stitch visual commands (Object -> Symbols...)") + \ - "\n\n" + _("Directory in which to install add-ons:") + "\n • " + _("Ink/Stitch visual commands (Object -> Symbols...)") static_text = wx.StaticText(panel, label=text) font = wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.NORMAL) @@ -38,13 +37,6 @@ class InstallerFrame(wx.Frame): text_sizer.Add(static_text, proportion=0, flag=wx.ALL|wx.EXPAND, border=10) sizer.Add(text_sizer, proportion=3, flag=wx.ALL|wx.EXPAND, border=0) - path_sizer = wx.BoxSizer(wx.HORIZONTAL) - self.path_input = wx.TextCtrl(panel, wx.ID_ANY, value=default_path) - path_sizer.Add(self.path_input, proportion=3, flag=wx.RIGHT, border=20) - chooser_button = wx.Button(panel, wx.ID_OPEN, _('Choose another directory...')) - path_sizer.Add(chooser_button, proportion=1, flag=0) - sizer.Add(path_sizer, proportion=1, flag=wx.ALL|wx.ALIGN_BOTTOM, border=10) - buttons_sizer = wx.BoxSizer(wx.HORIZONTAL) install_button = wx.Button(panel, wx.ID_ANY, _("Install")) install_button.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_TICK_MARK)) @@ -53,13 +45,9 @@ class InstallerFrame(wx.Frame): buttons_sizer.Add(cancel_button, proportion=0, flag=wx.ALIGN_RIGHT|wx.ALL, border=5) sizer.Add(buttons_sizer, proportion=1, flag=wx.ALIGN_RIGHT|wx.ALIGN_BOTTOM) - #outer_sizer = wx.BoxSizer(wx.HORIZONTAL) - #outer_sizer.Add(sizer, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL) - panel.SetSizer(sizer) panel.Layout() - chooser_button.Bind(wx.EVT_BUTTON, self.chooser_button_clicked) cancel_button.Bind(wx.EVT_BUTTON, self.cancel_button_clicked) install_button.Bind(wx.EVT_BUTTON, self.install_button_clicked) @@ -89,7 +77,7 @@ class InstallerFrame(wx.Frame): self.Destroy() def install_addons(self, type): - path = os.path.join(self.path_input.GetValue(), type) + path = os.path.join(self.path, type) src_dir = self.get_bundled_dir(type) self.copy_files(glob(os.path.join(src_dir, "*")), path) @@ -118,6 +106,6 @@ class InstallerFrame(wx.Frame): class Install(inkex.Effect): def effect(self): app = wx.App() - installer_frame = InstallerFrame(None, title=_("Ink/Stitch Add-ons Installer"), size=(550, 350)) + installer_frame = InstallerFrame(None, title=_("Ink/Stitch Add-ons Installer"), size=(550, 250)) installer_frame.Show() app.MainLoop() -- cgit v1.2.3 From 3299b7450f847e9ecb0df9f8b8a5cd3da755a33b Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 30 Jun 2018 13:02:33 -0400 Subject: add extension to swap satin column rails --- lib/extensions/__init__.py | 1 + lib/extensions/flip.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 lib/extensions/flip.py (limited to 'lib') diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index b8951e12..b11ba1a4 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -6,3 +6,4 @@ from simulate import Simulate from input import Input from output import Output from zip import Zip +from flip import Flip diff --git a/lib/extensions/flip.py b/lib/extensions/flip.py new file mode 100644 index 00000000..75d8fe17 --- /dev/null +++ b/lib/extensions/flip.py @@ -0,0 +1,34 @@ +import sys +import inkex +import cubicsuperpath + +from .base import InkstitchExtension +from ..i18n import _ +from ..elements import SatinColumn + +class Flip(InkstitchExtension): + def flip(self, satin): + csp = cubicsuperpath.parsePath(satin.node.get("d")) + + if len(csp) > 1: + # find the rails (the two longest paths) and swap them + indices = range(len(csp)) + indices.sort(key=lambda i: len(csp[i]), reverse=True) + + first = indices[0] + second = indices[1] + csp[first], csp[second] = csp[second], csp[first] + + satin.node.set("d", cubicsuperpath.formatPath(csp)) + + def effect(self): + if not self.get_elements(): + return + + if not self.selected: + inkex.errormsg(_("Please select one or more satin columns to flip.")) + return + + for element in self.elements: + if isinstance(element, SatinColumn): + self.flip(element) -- cgit v1.2.3 From ac84d7b0d4365b61b5a375b8c4497a81093cc734 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 7 Jul 2018 15:31:59 -0400 Subject: fix brain-o --- lib/extensions/flip.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/extensions/flip.py b/lib/extensions/flip.py index 75d8fe17..d8d78cb5 100644 --- a/lib/extensions/flip.py +++ b/lib/extensions/flip.py @@ -1,19 +1,25 @@ import sys import inkex import cubicsuperpath +from shapely import geometry as shgeo from .base import InkstitchExtension from ..i18n import _ from ..elements import SatinColumn class Flip(InkstitchExtension): + def subpath_to_linestring(self, subpath): + return shgeo.LineString() + def flip(self, satin): - csp = cubicsuperpath.parsePath(satin.node.get("d")) + csp = satin.path if len(csp) > 1: + flattened = satin.flatten(csp) + # find the rails (the two longest paths) and swap them indices = range(len(csp)) - indices.sort(key=lambda i: len(csp[i]), reverse=True) + indices.sort(key=lambda i: shgeo.LineString(flattened[i]).length, reverse=True) first = indices[0] second = indices[1] -- cgit v1.2.3 From d4c4f2c7cc947eea71d99ef6030ecb4401508835 Mon Sep 17 00:00:00 2001 From: Kaalleen <36401965+kaalleen@users.noreply.github.com> Date: Thu, 12 Jul 2018 21:16:22 +0200 Subject: operator detailedview dynamic thumbnail size (#221) --- lib/extensions/base.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'lib') diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 78f75cf1..d230f1b0 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -58,11 +58,7 @@ class InkStitchMetadata(MutableMapping): def __setitem__(self, name, value): item = self._find_item(name) - - if value: - item.text = json.dumps(value) - else: - item.getparent().remove(item) + item.text = json.dumps(value) def _find_item(self, name, create=True): tag = inkex.addNS(name, "inkstitch") -- cgit v1.2.3 From 61983b615b202bb95c21d7a5021af3373615e839 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 30 Jun 2018 14:16:56 -0400 Subject: add has_command() --- lib/elements/element.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'lib') diff --git a/lib/elements/element.py b/lib/elements/element.py index 3c31f1b0..f0b7ea6f 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -214,6 +214,10 @@ class EmbroideryElement(object): def get_commands(self, command): return [c for c in self.commands if c.command == command] + @cache + def has_command(self, command): + return len(self.get_commands(command)) > 0 + @cache def get_command(self, command): commands = self.get_commands(command) -- cgit v1.2.3 From 3893d13b52b2755ea134ec4d3a215ee807dbbc2e Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 30 Jun 2018 14:18:45 -0400 Subject: add support for trim/stop commands --- lib/elements/element.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/elements/element.py b/lib/elements/element.py index f0b7ea6f..1c67d123 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -268,8 +268,8 @@ class EmbroideryElement(object): patches = self.to_patches(last_patch) if patches: - patches[-1].trim_after = self.trim_after - patches[-1].stop_after = self.stop_after + patches[-1].trim_after = self.has_command("trim") or self.trim_after + patches[-1].stop_after = self.has_command("stop") or self.stop_after return patches -- cgit v1.2.3 From aa86dc56ad5cb9166ab1c9cda036d9521855ad29 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 30 Jun 2018 14:19:28 -0400 Subject: remove 'TRIM after' and 'STOP after' from Params dialog --- lib/elements/element.py | 12 ------------ 1 file changed, 12 deletions(-) (limited to 'lib') diff --git a/lib/elements/element.py b/lib/elements/element.py index 1c67d123..62e9745d 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -242,22 +242,10 @@ class EmbroideryElement(object): return [self.strip_control_points(subpath) for subpath in path] @property - @param('trim_after', - _('TRIM after'), - tooltip=_('Trim thread after this object (for supported machines and file formats)'), - type='boolean', - default=False, - sort_index=1000) def trim_after(self): return self.get_boolean_param('trim_after', False) @property - @param('stop_after', - _('STOP after'), - tooltip=_('Add STOP instruction after this object (for supported machines and file formats)'), - type='boolean', - default=False, - sort_index=1000) def stop_after(self): return self.get_boolean_param('stop_after', False) -- cgit v1.2.3 From a0659e2c2d7609294c1a5a70153b1aa647595e94 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 4 Jul 2018 21:15:49 -0400 Subject: remove leftover debugging code (oops) --- lib/commands.py | 3 --- 1 file changed, 3 deletions(-) (limited to 'lib') diff --git a/lib/commands.py b/lib/commands.py index ec62d716..02c13b25 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -77,9 +77,6 @@ def find_commands(node): try: commands.append(Command(connector)) except ValueError: - import sys - import traceback - print >> sys.stderr, "not a Command:", connector.get('id'), traceback.format_exc() # Parsing the connector failed, meaning it's not actually an Ink/Stitch command. pass -- cgit v1.2.3 From d090fa003830f117918fac201ca527d513507a70 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 4 Jul 2018 21:16:49 -0400 Subject: move get_bundled_dir to utils --- lib/extensions/install.py | 10 ++-------- lib/utils/__init__.py | 1 + lib/utils/paths.py | 10 ++++++++++ 3 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 lib/utils/paths.py (limited to 'lib') diff --git a/lib/extensions/install.py b/lib/extensions/install.py index d55b96d0..42a92113 100644 --- a/lib/extensions/install.py +++ b/lib/extensions/install.py @@ -13,7 +13,7 @@ import logging import wx import inkex -from ..utils import guess_inkscape_config_path +from ..utils import guess_inkscape_config_path, get_bundled_dir class InstallerFrame(wx.Frame): @@ -78,15 +78,9 @@ class InstallerFrame(wx.Frame): def install_addons(self, type): path = os.path.join(self.path, type) - src_dir = self.get_bundled_dir(type) + src_dir = get_bundled_dir(type) self.copy_files(glob(os.path.join(src_dir, "*")), path) - def get_bundled_dir(self, name): - if getattr(sys, 'frozen', None) is not None: - return realpath(os.path.join(sys._MEIPASS, '..', name)) - else: - return realpath(os.path.join(dirname(realpath(__file__)), '..', '..', name)) - if (sys.platform == "win32"): # If we try to just use shutil.copy it says the operation requires elevation. def copy_files(self, files, dest): diff --git a/lib/utils/__init__.py b/lib/utils/__init__.py index ff06d4a9..78d037f1 100644 --- a/lib/utils/__init__.py +++ b/lib/utils/__init__.py @@ -2,3 +2,4 @@ from geometry import * from cache import cache from io import * from inkscape import * +from paths import * diff --git a/lib/utils/paths.py b/lib/utils/paths.py new file mode 100644 index 00000000..863e8e69 --- /dev/null +++ b/lib/utils/paths.py @@ -0,0 +1,10 @@ +import sys +import os +from os.path import dirname, realpath + + +def get_bundled_dir(name): + if getattr(sys, 'frozen', None) is not None: + return realpath(os.path.join(sys._MEIPASS, "..", name)) + else: + return realpath(os.path.join(dirname(realpath(__file__)), '..', '..', name)) -- cgit v1.2.3 From 62ef2850a2f57d64d0e65fbfc055b85e3c940031 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 4 Jul 2018 21:17:20 -0400 Subject: generalize Element.shape and implement in all element types --- lib/elements/element.py | 4 ++++ lib/elements/polyline.py | 7 +++++++ lib/elements/satin_column.py | 11 +++++++++++ lib/elements/stroke.py | 7 +++++++ 4 files changed, 29 insertions(+) (limited to 'lib') diff --git a/lib/elements/element.py b/lib/elements/element.py index 62e9745d..ebca90a4 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -205,6 +205,10 @@ class EmbroideryElement(object): def parse_path(self): return apply_transforms(self.path, self.node) + @property + def shape(self): + raise NotImplementedError("INTERNAL ERROR: %s must implement shape()", self.__class__) + @property @cache def commands(self): diff --git a/lib/elements/polyline.py b/lib/elements/polyline.py index 5c474237..b9ffdc0b 100644 --- a/lib/elements/polyline.py +++ b/lib/elements/polyline.py @@ -1,3 +1,5 @@ +from shapely import geometry as shgeo + from .element import param, EmbroideryElement, Patch from ..i18n import _ from ..utils.geometry import Point @@ -27,6 +29,11 @@ class Polyline(EmbroideryElement): return points + @property + @cache + def shape(self): + return shgeo.LineString(self.points) + @property def path(self): # A polyline is a series of connected line segments described by their diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index 1d13c5e0..2ceb38de 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -87,6 +87,17 @@ class SatinColumn(EmbroideryElement): # the edges of the satin column. return self.get_float_param("zigzag_underlay_inset_mm") or self.contour_underlay_inset / 2.0 + @property + @cache + def shape(self): + # This isn't used for satins at all, but other parts of the code + # may need to know the general shape of a satin column. + + flattened = self.flatten(self.parse_path()) + line_strings = [shgeo.LineString(path) for path in flattened] + + return shgeo.MultiLineString(line_strings) + @property @cache def csp(self): diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index eca9e0ba..e8eb4783 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -1,4 +1,5 @@ import sys +import shapely.geometry from .element import param, EmbroideryElement, Patch from ..i18n import _ @@ -50,6 +51,12 @@ class Stroke(EmbroideryElement): else: return self.flatten(path) + @property + @cache + def shape(self): + line_strings = [shapely.geometry.LineString(path) for path in self.paths] + return shapely.geometry.MultiLineString(line_strings) + @property @param('manual_stitch', _('Manual stitch placement'), tooltip=_("Stitch every node in the path. Stitch length and zig-zag spacing are ignored."), type='boolean', default=False) def manual_stitch_mode(self): -- cgit v1.2.3 From 22102ee0e839b22668873c45756a3666d4d9cbff Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 4 Jul 2018 21:42:53 -0400 Subject: add CONNECTION_TYPE --- lib/svg/tags.py | 1 + 1 file changed, 1 insertion(+) (limited to 'lib') diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 5488608c..7eb87540 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -12,6 +12,7 @@ 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') EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG) -- cgit v1.2.3 From d9033be6fc4dcca4ee33f80a55f1ae4a7921be89 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 4 Jul 2018 21:44:08 -0400 Subject: refactor out logic to gather node's transforms --- lib/svg/__init__.py | 2 +- lib/svg/path.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) (limited to 'lib') diff --git a/lib/svg/__init__.py b/lib/svg/__init__.py index 50543b1b..8e846555 100644 --- a/lib/svg/__init__.py +++ b/lib/svg/__init__.py @@ -1,3 +1,3 @@ from .svg import color_block_to_point_lists, render_stitch_plan from .units import * -from .path import apply_transforms +from .path import apply_transforms, get_node_transform diff --git a/lib/svg/path.py b/lib/svg/path.py index a8012774..2d9c0ff3 100644 --- a/lib/svg/path.py +++ b/lib/svg/path.py @@ -4,6 +4,14 @@ import cubicsuperpath from .units import get_viewbox_transform def apply_transforms(path, node): + transform = get_node_transform(node) + + # apply the combined transform to this node's path + simpletransform.applyTransformToPath(transform, path) + + return path + +def get_node_transform(node): # start with the identity transform transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] @@ -14,7 +22,4 @@ def apply_transforms(path, node): viewbox_transform = get_viewbox_transform(node.getroottree().getroot()) transform = simpletransform.composeTransform(viewbox_transform, transform) - # apply the combined transform to this node's path - simpletransform.applyTransformToPath(transform, path) - - return path + return transform -- cgit v1.2.3 From 3de394e14b00ac2653084f534149db418bd6cebd Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 4 Jul 2018 21:45:21 -0400 Subject: add new extension to attach commands to objects --- lib/extensions/__init__.py | 1 + lib/extensions/commands.py | 128 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 lib/extensions/commands.py (limited to 'lib') diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index b11ba1a4..dfdc7a3e 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -7,3 +7,4 @@ from input import Input from output import Output from zip import Zip from flip import Flip +from commands import * diff --git a/lib/extensions/commands.py b/lib/extensions/commands.py new file mode 100644 index 00000000..26ced110 --- /dev/null +++ b/lib/extensions/commands.py @@ -0,0 +1,128 @@ +import os +import sys +import inkex +import simpletransform +import cubicsuperpath +from copy import deepcopy +from shapely import geometry as shgeo + +from .base import InkstitchExtension +from ..i18n import _ +from ..elements import SatinColumn +from ..utils import get_bundled_dir, cache +from ..svg.tags import SVG_DEFS_TAG, SVG_GROUP_TAG, SVG_USE_TAG, SVG_PATH_TAG, INKSCAPE_GROUPMODE, XLINK_HREF, CONNECTION_START, CONNECTION_END, CONNECTOR_TYPE +from ..svg import get_node_transform + + +class Commands(InkstitchExtension): + COMMANDS = ["fill_start", "fill_end", "stop", "trim"] + + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + for command in self.COMMANDS: + self.OptionParser.add_option("--%s" % command, type="inkbool") + + @property + def symbols_path(self): + return os.path.join(get_bundled_dir("symbols"), "inkstitch.svg") + + @property + @cache + def symbols_svg(self): + with open(self.symbols_path) as symbols_file: + return inkex.etree.parse(symbols_file) + + @property + @cache + def symbol_defs(self): + return self.symbols_svg.find(SVG_DEFS_TAG) + + @property + @cache + def defs(self): + return self.document.find(SVG_DEFS_TAG) + + def ensure_symbol(self, command): + path = "./*[@id='inkstitch_%s']" % command + if self.defs.find(path) is None: + self.defs.append(deepcopy(self.symbol_defs.find(path))) + + def get_correction_transform(self, node): + # if we want to place our new nodes in the same group as this node, + # then we'll need to factor in the effects of any transforms set on + # the parents of this node. + + # we can ignore the transform on the node itself since it won't apply + # to the objects we add + transform = get_node_transform(node.getparent()) + + # now invert it, so that we can position our objects in absolute + # coordinates + transform = simpletransform.invertTransform(transform) + + return simpletransform.formatTransform(transform) + + def add_connector(self, symbol, element): + # I'd like it if I could position the connector endpoint nicely but inkscape just + # moves it to the element's center immediately after the extension runs. + start_pos = (symbol.get('x'), symbol.get('y')) + end_pos = element.shape.centroid + + path = inkex.etree.Element(SVG_PATH_TAG, + { + "id": self.uniqueId("connector"), + "d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y), + "style": "stroke:#000000;stroke-width:1px;", + "transform": self.get_correction_transform(symbol), + CONNECTION_START: "#%s" % symbol.get('id'), + CONNECTION_END: "#%s" % element.node.get('id'), + CONNECTOR_TYPE: "polyline", + } + ) + + symbol.getparent().insert(symbol.getparent().index(symbol), path) + + def get_command_pos(self, element, index, total): + # Put command symbols 30 pixels out from the shape, spaced evenly around it. + outline = element.shape.buffer(30).exterior + return outline.interpolate(index / float(total), normalized=True) + + def add_command(self, element, commands): + for i, command in enumerate(commands): + pos = self.get_command_pos(element, i, len(commands)) + + symbol = inkex.etree.SubElement(element.node.getparent(), SVG_USE_TAG, + { + "id": self.uniqueId("use"), + XLINK_HREF: "#inkstitch_%s" % command, + "height": "100%", + "width": "100%", + "x": str(pos.x), + "y": str(pos.y), + "transform": self.get_correction_transform(element.node) + } + ) + + self.add_connector(symbol, element) + + def effect(self): + if not self.get_elements(): + return + + if not self.selected: + inkex.errormsg(_("Please select one or more objects to which to attach commands.")) + return + + self.svg = self.document.getroot() + + commands = [command for command in self.COMMANDS if getattr(self.options, command)] + + if not commands: + inkex.errormsg(_("Please choose one or more commands to attach.")) + return + + for command in commands: + self.ensure_symbol(command) + + for element in self.elements: + self.add_command(element, commands) -- cgit v1.2.3 From 1c5e4fbf73e673a12d67d27a1f0e88c2265c762f Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 4 Jul 2018 21:54:23 -0400 Subject: set fill to none for connectors --- lib/extensions/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/extensions/commands.py b/lib/extensions/commands.py index 26ced110..aeda2cc2 100644 --- a/lib/extensions/commands.py +++ b/lib/extensions/commands.py @@ -72,7 +72,7 @@ class Commands(InkstitchExtension): { "id": self.uniqueId("connector"), "d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y), - "style": "stroke:#000000;stroke-width:1px;", + "style": "stroke:#000000;stroke-width:1px;fill:none;", "transform": self.get_correction_transform(symbol), CONNECTION_START: "#%s" % symbol.get('id'), CONNECTION_END: "#%s" % element.node.get('id'), -- cgit v1.2.3 From 7ccc6aa72ce02ea335f129577c8984698a987d0e Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 4 Jul 2018 22:08:08 -0400 Subject: remove legacy params when attaching the equivalent command --- lib/extensions/commands.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/extensions/commands.py b/lib/extensions/commands.py index aeda2cc2..9c080b4e 100644 --- a/lib/extensions/commands.py +++ b/lib/extensions/commands.py @@ -87,8 +87,26 @@ class Commands(InkstitchExtension): outline = element.shape.buffer(30).exterior return outline.interpolate(index / float(total), normalized=True) + def remove_legacy_param(self, 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 + # thing. + # + # If we didn't delete these here, then things would get confusing. + # If the user were to delete a "trim" symbol added by this extension + # but the "embroider_trim_after" attribute is still set, then the + # trim would keep happening. + + attribute = "embroider_%s_after" % command + + if attribute in element.node.attrib: + del element.node.attrib[attribute] + def add_command(self, element, commands): for i, command in enumerate(commands): + self.remove_legacy_param(element, command) + pos = self.get_command_pos(element, i, len(commands)) symbol = inkex.etree.SubElement(element.node.getparent(), SVG_USE_TAG, @@ -125,4 +143,4 @@ class Commands(InkstitchExtension): self.ensure_symbol(command) for element in self.elements: - self.add_command(element, commands) + self.add_command(element, commands) -- cgit v1.2.3 From 1b63ac5bfa12dc4fc4162a4e7c8e06130e5e906c Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 4 Jul 2018 23:00:03 -0400 Subject: only process each node once --- lib/extensions/commands.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/extensions/commands.py b/lib/extensions/commands.py index 9c080b4e..4c9fd172 100644 --- a/lib/extensions/commands.py +++ b/lib/extensions/commands.py @@ -142,5 +142,11 @@ class Commands(InkstitchExtension): for command in commands: self.ensure_symbol(command) + # Each object (node) in the SVG may correspond to multiple Elements of different + # types (e.g. stroke + fill). We only want to process each one once. + seen_nodes = set() + for element in self.elements: - self.add_command(element, commands) + if element.node not in seen_nodes: + self.add_command(element, commands) + seen_nodes.add(element.node) -- cgit v1.2.3 From 73565ae691b409af9190a02485e51ed590510384 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 9 Jul 2018 20:59:46 -0400 Subject: fix stop count --- lib/stitch_plan/stitch_plan.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/stitch_plan/stitch_plan.py b/lib/stitch_plan/stitch_plan.py index 93bcd195..742916f0 100644 --- a/lib/stitch_plan/stitch_plan.py +++ b/lib/stitch_plan/stitch_plan.py @@ -183,10 +183,7 @@ class ColorBlock(object): def num_stops(self): """Number of pauses in this color block.""" - # Stops are encoded using two STOP stitches each. See the comment in - # stop.py for an explanation. - - return sum(1 for stitch in self if stitch.stop) / 2 + return sum(1 for stitch in self if stitch.stop) @property def num_trims(self): -- cgit v1.2.3 From 0c6288f7693a6aeb2d3dd15ad727d868de183b6a Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 10 Jul 2018 20:03:51 -0400 Subject: perturb the positions of commands a bit --- lib/extensions/commands.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/extensions/commands.py b/lib/extensions/commands.py index 4c9fd172..5767447c 100644 --- a/lib/extensions/commands.py +++ b/lib/extensions/commands.py @@ -4,6 +4,7 @@ import inkex import simpletransform import cubicsuperpath from copy import deepcopy +from random import random from shapely import geometry as shgeo from .base import InkstitchExtension @@ -84,8 +85,16 @@ class Commands(InkstitchExtension): def get_command_pos(self, element, index, total): # Put command symbols 30 pixels out from the shape, spaced evenly around it. + + # get a line running 30 pixels out from the shape outline = element.shape.buffer(30).exterior - return outline.interpolate(index / float(total), normalized=True) + + # pick this item's spot arond the outline and perturb it a bit to avoid + # stacking up commands if they run the extension multiple times + position = index / float(total) + position += random() * 0.1 + + return outline.interpolate(position, normalized=True) def remove_legacy_param(self, element, command): if command == "trim" or command == "stop": -- cgit v1.2.3 From b90d4c152e4c319e74b984207aa369b47af05074 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 10 Jul 2018 20:07:47 -0400 Subject: make connector 50% transparent --- lib/extensions/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/extensions/commands.py b/lib/extensions/commands.py index 5767447c..2f3006ff 100644 --- a/lib/extensions/commands.py +++ b/lib/extensions/commands.py @@ -73,7 +73,7 @@ class Commands(InkstitchExtension): { "id": self.uniqueId("connector"), "d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y), - "style": "stroke:#000000;stroke-width:1px;fill:none;", + "style": "stroke:#000000;stroke-width:1px;stroke-opacity:0.5;fill:none;", "transform": self.get_correction_transform(symbol), CONNECTION_START: "#%s" % symbol.get('id'), CONNECTION_END: "#%s" % element.node.get('id'), -- cgit v1.2.3 From 6caba7b839e9f4e90ab9f3ff1110c8759e30337d Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 10 Jul 2018 20:12:38 -0400 Subject: fix import --- lib/extensions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index dfdc7a3e..8b243176 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -7,4 +7,4 @@ from input import Input from output import Output from zip import Zip from flip import Flip -from commands import * +from commands import Commands -- cgit v1.2.3 From dbcbf7cff1c8e76a2715d939818124c33cb5fa1e Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sun, 15 Jul 2018 20:15:35 -0400 Subject: switch to pyembroidery for file generation --- lib/output.py | 65 +++++++++++++++++++++------------------------------ lib/threads/color.py | 9 +++++++ lib/utils/geometry.py | 3 +++ 3 files changed, 39 insertions(+), 38 deletions(-) (limited to 'lib') diff --git a/lib/output.py b/lib/output.py index 84128a25..491c190a 100644 --- a/lib/output.py +++ b/lib/output.py @@ -1,4 +1,4 @@ -import libembroidery +import pyembroidery import inkex import simpletransform import shapely.geometry as shgeo @@ -7,15 +7,6 @@ from .utils import Point from .svg import PIXELS_PER_MM, get_doc_size, get_viewbox_transform -def make_thread(color): - thread = libembroidery.EmbThread() - thread.color = libembroidery.embColor_make(*color.rgb) - - thread.description = color.name - thread.catalogNumber = "" - - return thread - def add_thread(pattern, thread): """Add a thread to a pattern and return the thread's index""" @@ -23,20 +14,17 @@ def add_thread(pattern, thread): return libembroidery.embThreadList_count(pattern.threadList) - 1 -def get_flags(stitch): - flags = 0 - +def get_command(stitch): if stitch.jump: - flags |= libembroidery.JUMP - - if stitch.trim: - flags |= libembroidery.TRIM - - if stitch.color_change: - flags |= libembroidery.STOP - - return flags - + return pyembroidery.JUMP + elif stitch.trim: + return pyembroidery.TRIM + elif stitch.color_change: + return pyembroidery.COLOR_CHANGE + elif stitch.stop: + return pyembroidery.STOP + else: + return pyembroidery.NEEDLE_AT def _string_to_floats(string): floats = string.split(',') @@ -102,27 +90,28 @@ def get_origin(svg): def write_embroidery_file(file_path, stitch_plan, svg): origin = get_origin(svg) - pattern = libembroidery.embPattern_create() + pattern = pyembroidery.EmbPattern() for color_block in stitch_plan: - add_thread(pattern, make_thread(color_block.color)) + pattern.add_thread(color_block.color.pyembroidery_thread) for stitch in color_block: - if stitch.stop: - # This is the start of the extra color block added by the - # "STOP after" handler (see stitch_plan/stop.py). Assign it - # the same color. - add_thread(pattern, make_thread(color_block.color)) + command = get_command(stitch) + pattern.add_stitch_absolute(command, stitch.x, stitch.y) - flags = get_flags(stitch) - libembroidery.embPattern_addStitchAbs(pattern, stitch.x - origin.x, stitch.y - origin.y, flags, 1) - - libembroidery.embPattern_addStitchAbs(pattern, stitch.x - origin.x, stitch.y - origin.y, libembroidery.END, 1) + pattern.add_stitch_absolute(pyembroidery.END, stitch.x, stitch.y) # convert from pixels to millimeters - libembroidery.embPattern_scale(pattern, 1/PIXELS_PER_MM) + # also multiply by 10 to get tenths of a millimeter as required by pyembroidery + scale = 10 / PIXELS_PER_MM + + settings = { + # correct for the origin + "translate": -origin, - # SVG and embroidery disagree on the direction of the Y axis - libembroidery.embPattern_flipVertical(pattern) + # convert from pixels to millimeters + # also multiply by 10 to get tenths of a millimeter as required by pyembroidery + "scale": (scale, scale) + } - libembroidery.embPattern_write(pattern, file_path) + pyembroidery.write(pattern, file_path, settings) diff --git a/lib/threads/color.py b/lib/threads/color.py index fede2ecc..d94f8825 100644 --- a/lib/threads/color.py +++ b/lib/threads/color.py @@ -38,6 +38,15 @@ class ThreadColor(object): def to_hex_str(self): return "#%s" % self.hex_digits + @property + def pyembroidery_thread(self): + return { + "name": self.name, + "id": self.number, + "manufacturer": self.manufacturer, + "rgb": int(self.hex_digits, 16), + } + @property def hex_digits(self): return "%02X%02X%02X" % self.rgb diff --git a/lib/utils/geometry.py b/lib/utils/geometry.py index 7ff9b1cd..d0cb96cf 100644 --- a/lib/utils/geometry.py +++ b/lib/utils/geometry.py @@ -65,6 +65,9 @@ class Point: else: raise ValueError("cannot multiply Point by %s" % type(other)) + def __neg__(self): + return self * -1 + def __rmul__(self, other): if isinstance(other, (int, float)): return self.__mul__(other) -- cgit v1.2.3 From 2cd4963d09ef78dd25a7401cc47a69474d7fa952 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sun, 15 Jul 2018 22:53:18 -0400 Subject: adjust stitch plan code for pyembroidery --- lib/output.py | 6 ++- lib/stitch_plan/stitch.py | 14 ++++++- lib/stitch_plan/stitch_plan.py | 94 +++++++++++++++++++++++++----------------- lib/stitch_plan/stop.py | 82 +++++++++++++++++++++++++----------- lib/stitch_plan/ties.py | 18 ++++---- lib/stitch_plan/trim.py | 23 ----------- 6 files changed, 140 insertions(+), 97 deletions(-) delete mode 100644 lib/stitch_plan/trim.py (limited to 'lib') diff --git a/lib/output.py b/lib/output.py index 491c190a..1c580f04 100644 --- a/lib/output.py +++ b/lib/output.py @@ -111,7 +111,11 @@ def write_embroidery_file(file_path, stitch_plan, svg): # convert from pixels to millimeters # also multiply by 10 to get tenths of a millimeter as required by pyembroidery - "scale": (scale, scale) + "scale": (scale, scale), + + # This forces a jump at the start of the design and after each trim, + # even if we're close enough not to need one. + "full_jump": True, } pyembroidery.write(pattern, file_path, settings) diff --git a/lib/stitch_plan/stitch.py b/lib/stitch_plan/stitch.py index 12642a60..5230efec 100644 --- a/lib/stitch_plan/stitch.py +++ b/lib/stitch_plan/stitch.py @@ -2,7 +2,7 @@ from ..utils.geometry import Point class Stitch(Point): - def __init__(self, x, y, color=None, jump=False, stop=False, trim=False, color_change=False, no_ties=False): + def __init__(self, x, y, color=None, jump=False, stop=False, trim=False, color_change=False, fake_color_change=False, no_ties=False): self.x = x self.y = y self.color = color @@ -10,10 +10,20 @@ class Stitch(Point): self.trim = trim self.stop = stop self.color_change = color_change + self.fake_color_change = fake_color_change self.no_ties = no_ties def __repr__(self): - return "Stitch(%s, %s, %s, %s, %s, %s, %s)" % (self.x, self.y, self.color, "JUMP" if self.jump else " ", "TRIM" if self.trim else " ", "STOP" if self.stop else " ", "NO TIES" if self.no_ties else " ") + return "Stitch(%s, %s, %s, %s, %s, %s, %s, %s%s)" % (self.x, + self.y, + self.color, + "JUMP" if self.jump else " ", + "TRIM" if self.trim else " ", + "STOP" if self.stop else " ", + "NO TIES" if self.no_ties else " ", + "FAKE " if self.fake_color_change else "", + "COLOR CHANGE" if self.color_change else " " + ) def copy(self): return Stitch(self.x, self.y, self.color, self.jump, self.stop, self.trim, self.color_change, self.no_ties) diff --git a/lib/stitch_plan/stitch_plan.py b/lib/stitch_plan/stitch_plan.py index 742916f0..1a466295 100644 --- a/lib/stitch_plan/stitch_plan.py +++ b/lib/stitch_plan/stitch_plan.py @@ -16,64 +16,40 @@ def patches_to_stitch_plan(patches, collapse_len=3.0 * PIXELS_PER_MM): """ stitch_plan = StitchPlan() - color_block = stitch_plan.new_color_block() - need_trim = False + if not patches: + return stitch_plan + + color_block = stitch_plan.new_color_block(color=patches[0].color) + for patch in patches: if not patch.stitches: continue - if not color_block.has_color(): - # set the color for the first color block - color_block.color = patch.color - - if color_block.color == patch.color: - if need_trim: - process_trim(color_block, patch.stitches[0]) - need_trim = False - - # add a jump stitch between patches if the distance is more - # than the collapse length - if color_block.last_stitch: - if (patch.stitches[0] - color_block.last_stitch).length() > collapse_len: - color_block.add_stitch(patch.stitches[0].x, patch.stitches[0].y, jump=True) - - else: + if color_block.color != patch.color or color_block.stop_after: # add a color change (only if we didn't just do a "STOP after") - if not color_block.last_stitch.color_change: - stitch = color_block.last_stitch.copy() - stitch.color_change = True - color_block.add_stitch(stitch) + if not color_block.stop_after: + color_block.add_stitch(color_change=True) - color_block = stitch_plan.new_color_block() - color_block.color = patch.color + color_block = stitch_plan.new_color_block(color=patch.color) color_block.filter_duplicate_stitches() color_block.add_stitches(patch.stitches, no_ties=patch.stitch_as_is) if patch.trim_after: - # a trim needs to be followed by a jump to the next stitch, so - # we'll process it when we start the next patch - need_trim = True + color_block.add_stitch(trim=True) if patch.stop_after: - process_stop(color_block) + process_stop(stitch_plan) + + # process_stop() may have split the block into two + color_block = stitch_plan.last_color_block - add_jumps(stitch_plan) add_ties(stitch_plan) return stitch_plan -def add_jumps(stitch_plan): - """Add a JUMP stitch at the start of each color block.""" - - for color_block in stitch_plan: - stitch = color_block.stitches[0].copy() - stitch.jump = True - color_block.stitches.insert(0, stitch) - - class StitchPlan(object): """Holds a set of color blocks, each containing stitches.""" @@ -85,6 +61,9 @@ class StitchPlan(object): self.color_blocks.append(color_block) return color_block + def add_color_block(self, color_block): + self.color_blocks.append(color_block) + def __iter__(self): return iter(self.color_blocks) @@ -99,6 +78,10 @@ class StitchPlan(object): """Number of unique colors in the stitch plan.""" return len({block.color for block in self}) + @property + def num_color_blocks(self): + return len(self.color_blocks) + @property def num_stops(self): return sum(block.num_stops for block in self) @@ -137,6 +120,13 @@ class StitchPlan(object): dimensions = self.dimensions return (dimensions[0] / PIXELS_PER_MM, dimensions[1] / PIXELS_PER_MM) + @property + def last_color_block(self): + if self.color_blocks: + return self.color_blocks[-1] + else: + return None + class ColorBlock(object): """Holds a set of stitches, all with the same thread color.""" @@ -148,6 +138,9 @@ class ColorBlock(object): def __iter__(self): return iter(self.stitches) + def __len__(self): + return len(self.stitches) + def __repr__(self): return "ColorBlock(%s, %s)" % (self.color, self.stitches) @@ -191,6 +184,13 @@ class ColorBlock(object): return sum(1 for stitch in self if stitch.trim) + @property + def stop_after(self): + if self.last_stitch is not None: + return self.last_stitch.stop + else: + return False + def filter_duplicate_stitches(self): if not self.stitches: return @@ -212,11 +212,21 @@ class ColorBlock(object): self.stitches = stitches def add_stitch(self, *args, **kwargs): + if not args: + # They're adding a command, e.g. `color_block.add_stitch(stop=True)``. + # Use the position from the last stitch. + if self.last_stitch: + args = (self.last_stitch.x, self.last_stitch.y) + else: + raise ValueError("internal error: can't add a command to an empty stitch block") + if isinstance(args[0], Stitch): self.stitches.append(args[0]) elif isinstance(args[0], Point): self.stitches.append(Stitch(args[0].x, args[0].y, *args[1:], **kwargs)) else: + if not args and self.last_stitch: + args = (self.last_stitch.x, self.last_stitch.y) self.stitches.append(Stitch(*args, **kwargs)) def add_stitches(self, stitches, *args, **kwargs): @@ -237,3 +247,11 @@ class ColorBlock(object): maxy = max(stitch.y for stitch in self) return minx, miny, maxx, maxy + + def split_at(self, index): + """Split this color block into two at the specified stitch index""" + + new_color_block = ColorBlock(self.color, self.stitches[index:]) + del self.stitches[index:] + + return new_color_block diff --git a/lib/stitch_plan/stop.py b/lib/stitch_plan/stop.py index 81dec1da..12a88d3a 100644 --- a/lib/stitch_plan/stop.py +++ b/lib/stitch_plan/stop.py @@ -1,43 +1,75 @@ -def process_stop(color_block): +from ..svg import PIXELS_PER_MM + +def process_stop(stitch_plan): """Handle the "stop after" checkbox. The user wants the machine to pause after this patch. This can be useful for applique and similar on multi-needle machines that normally would not stop between colors. - In machine embroidery files, there's no such thing as an actual - "STOP" instruction. All that exists is a "color change" command - (which libembroidery calls STOP just to be confusing). + In most machine embroidery file formats, there's no such thing as + an actual "STOP" instruction. All that exists is a "color change" + command. On multi-needle machines, the user assigns needles to the colors in - the design before starting stitching. C01, C02, etc are normal + the design before starting stitching. C01, C02, etc are the normal needles, but C00 is special. For a block of stitches assigned to C00, the machine will continue sewing with the last color it - had and pause after it completes the C00 block. + had and pause after it completes the C00 block. Machines that don't + call it C00 still have a similar concept. + + We'll add a STOP instruction at the end of this color block. + Unfortunately, we have a bit of a catch-22: the user needs to set + C00 (or equivalent) for the _start_ of this block to get the + machine to stop at the end of this block. That means it will use + the previous color, which isn't the right color at all! - That means we need to add an artificial color change instruction - shortly before the current stitch so that the user can set that color - block to C00. We'll go back 3 stitches and mark the start of the C00 - block: + For the first STOP in a given thread color, we'll need to + introduce an extra color change. The user can then set the correct + color for the first section and C00 for the second, resulting in + a stop where we want it. + + We'll try to find a logical place to split the color block, like + a TRIM or a really long stitch. Failing that, we'll just split + it in half. """ - if len(color_block.stitches) >= 3: - # make a copy of the stitch and set it as a color change - stitch = color_block.stitches[-3].copy() - stitch.color_change = True + if not stitch_plan.last_color_block or len(stitch_plan.last_color_block) < 2: + return + + last_stitch = stitch_plan.last_color_block.last_stitch + stitch_plan.last_color_block.add_stitch(last_stitch.x, last_stitch.y, stop=True) + + if len(stitch_plan) > 1: + # if this isn't the first stop in this color, then we're done + if stitch_plan.color_blocks[-2].stop_after and \ + stitch_plan.color_blocks[-2].color == stitch_plan.last_color_block.color: + return + + # We need to split this color block. Pick the last TRIM or + # the last long stitch (probably between distant patches). + + for i in xrange(len(stitch_plan.last_color_block) - 2, -1, -1): + stitch = stitch_plan.last_color_block.stitches[i] - # mark this stitch as a "stop" so that we can avoid - # adding tie stitches in ties.py - stitch.stop = True + if stitch.trim: + # ignore the trim right before the stop we just added + if i < len(stitch_plan.last_color_block) - 2: + # split after the trim + i = i + 1 + break - # insert it after the stitch - color_block.stitches.insert(-2, stitch) + if i > 0: + next_stitch = stitch_plan.last_color_block.stitches[i + 1] - # and also add a color change on this stitch, completing the C00 - # block: + if (stitch - next_stitch).length() > 20 * PIXELS_PER_MM: + break - stitch = color_block.stitches[-1].copy() - stitch.color_change = True - color_block.add_stitch(stitch) + if i == 0: + # Darn, we didn't find a TRIM or long stitch. Just chop the + # block in half. + i = len(stitch_plan.last_color_block) / 2 - # reference for the above: https://github.com/lexelby/inkstitch/pull/29#issuecomment-359175447 + new_color_block = stitch_plan.last_color_block.split_at(i) + stitch_plan.last_color_block.add_stitch(color_change=True, fake_color_change=True) + stitch_plan.add_color_block(new_color_block) diff --git a/lib/stitch_plan/ties.py b/lib/stitch_plan/ties.py index 6d07ac71..573469f5 100644 --- a/lib/stitch_plan/ties.py +++ b/lib/stitch_plan/ties.py @@ -30,15 +30,16 @@ def add_tie_in(stitches, upcoming_stitches): def add_ties(stitch_plan): """Add tie-off before and after trims, jumps, and color changes.""" + need_tie_in = True for color_block in stitch_plan: - need_tie_in = True new_stitches = [] for i, stitch in enumerate(color_block.stitches): - # Tie before and after TRIMs, JUMPs, and color changes, but ignore - # the fake color change introduced by a "STOP after" (see stop.py). - is_special = stitch.trim or stitch.jump or (stitch.color_change and not stitch.stop) + is_special = stitch.trim or stitch.jump or stitch.color_change or stitch.stop - if is_special and not need_tie_in: + # see stop.py for an explanation of the fake color change + is_fake = stitch.fake_color_change + + if is_special and not is_fake and not need_tie_in: add_tie_off(new_stitches) new_stitches.append(stitch) need_tie_in = True @@ -49,7 +50,8 @@ def add_ties(stitch_plan): else: new_stitches.append(stitch) - if not need_tie_in: - add_tie_off(new_stitches) - color_block.replace_stitches(new_stitches) + + if not need_tie_in: + # tie off at the end if we haven't already + add_tie_off(color_block.stitches) diff --git a/lib/stitch_plan/trim.py b/lib/stitch_plan/trim.py deleted file mode 100644 index f692a179..00000000 --- a/lib/stitch_plan/trim.py +++ /dev/null @@ -1,23 +0,0 @@ -def process_trim(color_block, next_stitch): - """Handle the "trim after" checkbox. - - DST (and maybe other formats?) has no actual TRIM instruction. - Instead, 3 sequential JUMPs cause the machine to trim the thread. - - To support both DST and other formats, we'll add a TRIM and two - JUMPs. The TRIM will be converted to a JUMP by libembroidery - if saving to DST, resulting in the 3-jump sequence. - """ - - delta = next_stitch - color_block.last_stitch - delta = delta * (1/4.0) - - pos = color_block.last_stitch - - for i in xrange(3): - pos += delta - color_block.add_stitch(pos.x, pos.y, jump=True) - - # first one should be TRIM instead of JUMP - color_block.stitches[-3].jump = False - color_block.stitches[-3].trim = True -- cgit v1.2.3 From 754bf54897e309fa21fa61bc7a626cde71a00f97 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sun, 15 Jul 2018 23:01:52 -0400 Subject: fix gap caused by splitting block --- lib/stitch_plan/stitch_plan.py | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'lib') diff --git a/lib/stitch_plan/stitch_plan.py b/lib/stitch_plan/stitch_plan.py index 1a466295..5d847ad2 100644 --- a/lib/stitch_plan/stitch_plan.py +++ b/lib/stitch_plan/stitch_plan.py @@ -1,3 +1,5 @@ +from copy import copy + from .stitch import Stitch from .stop import process_stop from .trim import process_trim @@ -254,4 +256,11 @@ class ColorBlock(object): new_color_block = ColorBlock(self.color, self.stitches[index:]) del self.stitches[index:] + # If we're splitting in the middle of a run of stitches, we don't + # want a gap to appear in the preview and the PDF printout, so + # add an extra stitch to bridge the gap. Technically this will + # result in a double needle penetration but it's no big deal. + if not self.last_stitch.trim: + self.add_stitch(copy(new_color_block.stitches[0])) + return new_color_block -- cgit v1.2.3 From b1912157579299212131b86f0b7267d7d91df047 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 16 Jul 2018 19:38:42 -0400 Subject: tidy up code --- lib/stitch_plan/stitch_plan.py | 9 --------- lib/stitch_plan/stop.py | 35 ++++++++++++++++++++++++----------- 2 files changed, 24 insertions(+), 20 deletions(-) (limited to 'lib') diff --git a/lib/stitch_plan/stitch_plan.py b/lib/stitch_plan/stitch_plan.py index 5d847ad2..1a466295 100644 --- a/lib/stitch_plan/stitch_plan.py +++ b/lib/stitch_plan/stitch_plan.py @@ -1,5 +1,3 @@ -from copy import copy - from .stitch import Stitch from .stop import process_stop from .trim import process_trim @@ -256,11 +254,4 @@ class ColorBlock(object): new_color_block = ColorBlock(self.color, self.stitches[index:]) del self.stitches[index:] - # If we're splitting in the middle of a run of stitches, we don't - # want a gap to appear in the preview and the PDF printout, so - # add an extra stitch to bridge the gap. Technically this will - # result in a double needle penetration but it's no big deal. - if not self.last_stitch.trim: - self.add_stitch(copy(new_color_block.stitches[0])) - return new_color_block diff --git a/lib/stitch_plan/stop.py b/lib/stitch_plan/stop.py index 12a88d3a..0ccaeaf8 100644 --- a/lib/stitch_plan/stop.py +++ b/lib/stitch_plan/stop.py @@ -1,5 +1,8 @@ +from copy import copy + from ..svg import PIXELS_PER_MM + def process_stop(stitch_plan): """Handle the "stop after" checkbox. @@ -34,33 +37,35 @@ def process_stop(stitch_plan): it in half. """ - if not stitch_plan.last_color_block or len(stitch_plan.last_color_block) < 2: + color_block = stitch_plan.last_color_block + + if not color_block or len(color_block) < 2: return - last_stitch = stitch_plan.last_color_block.last_stitch - stitch_plan.last_color_block.add_stitch(last_stitch.x, last_stitch.y, stop=True) + last_stitch = color_block.last_stitch + color_block.add_stitch(stop=True) if len(stitch_plan) > 1: # if this isn't the first stop in this color, then we're done if stitch_plan.color_blocks[-2].stop_after and \ - stitch_plan.color_blocks[-2].color == stitch_plan.last_color_block.color: + stitch_plan.color_blocks[-2].color == color_block.color: return # We need to split this color block. Pick the last TRIM or # the last long stitch (probably between distant patches). - for i in xrange(len(stitch_plan.last_color_block) - 2, -1, -1): - stitch = stitch_plan.last_color_block.stitches[i] + for i in xrange(len(color_block) - 2, -1, -1): + stitch = color_block.stitches[i] if stitch.trim: # ignore the trim right before the stop we just added - if i < len(stitch_plan.last_color_block) - 2: + if i < len(color_block) - 2: # split after the trim i = i + 1 break if i > 0: - next_stitch = stitch_plan.last_color_block.stitches[i + 1] + next_stitch = color_block.stitches[i + 1] if (stitch - next_stitch).length() > 20 * PIXELS_PER_MM: break @@ -68,8 +73,16 @@ def process_stop(stitch_plan): if i == 0: # Darn, we didn't find a TRIM or long stitch. Just chop the # block in half. - i = len(stitch_plan.last_color_block) / 2 + i = len(color_block) / 2 + + new_color_block = color_block.split_at(i) + + # If we're splitting in the middle of a run of stitches, we don't + # want a gap to appear in the preview and the PDF printout, so + # add an extra stitch to bridge the gap. Technically this will + # result in a double needle penetration but it's no big deal. + if not color_block.last_stitch.trim: + color_block.add_stitch(copy(new_color_block.stitches[0])) - new_color_block = stitch_plan.last_color_block.split_at(i) - stitch_plan.last_color_block.add_stitch(color_change=True, fake_color_change=True) + color_block.add_stitch(color_change=True, fake_color_change=True) stitch_plan.add_color_block(new_color_block) -- cgit v1.2.3 From 3cac91a1934de8d9a341f89e9f2fa2c4b7c41a29 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 17 Jul 2018 21:29:44 -0400 Subject: update input extension for pyembroidery --- lib/extensions/input.py | 39 ++++++++++----------------------------- lib/output.py | 7 ------- lib/threads/color.py | 8 +++++++- 3 files changed, 17 insertions(+), 37 deletions(-) (limited to 'lib') diff --git a/lib/extensions/input.py b/lib/extensions/input.py index 21248dd9..99bb70ab 100644 --- a/lib/extensions/input.py +++ b/lib/extensions/input.py @@ -8,48 +8,29 @@ import inkex if getattr(sys, 'frozen', None) is None: sys.path.append(realpath(path_join(dirname(__file__), '..', '..'))) -from libembroidery import * +import pyembroidery from ..svg import PIXELS_PER_MM, render_stitch_plan from ..svg.tags import INKSCAPE_LABEL from ..i18n import _ -from ..stitch_plan import StitchPlan +from ..stitch_plan import StitchPlan, ColorBlock from ..utils.io import save_stdout class Input(object): - def pattern_stitches(self, pattern): - stitch_pointer = pattern.stitchList - while stitch_pointer: - yield stitch_pointer.stitch - stitch_pointer = stitch_pointer.next - - def affect(self, args): - # libembroidery likes to dump a bunch of debugging stuff to stdout - save_stdout() - embroidery_file = args[0] - pattern = embPattern_create() - embPattern_read(pattern, embroidery_file) - embPattern_flipVertical(pattern) + pattern = pyembroidery.read(embroidery_file) stitch_plan = StitchPlan() color_block = None - current_color = None - - for stitch in self.pattern_stitches(pattern): - if stitch.color != current_color: - thread = embThreadList_getAt(pattern.threadList, stitch.color) - color = thread.color - color_block = stitch_plan.new_color_block((color.r, color.g, color.b)) - current_color = stitch.color - if not stitch.flags & END: - color_block.add_stitch(stitch.xx * PIXELS_PER_MM, stitch.yy * PIXELS_PER_MM, - jump=stitch.flags & JUMP, - color_change=stitch.flags & STOP, - trim=stitch.flags & TRIM) + for raw_stitches, thread in pattern.get_as_colorblocks(): + color_block = stitch_plan.new_color_block(thread) + for x, y, command in raw_stitches: + color_block.add_stitch(x * PIXELS_PER_MM / 10.0, y * PIXELS_PER_MM / 10.0, + jump=(command == pyembroidery.JUMP), + trim=(command == pyembroidery.TRIM)) extents = stitch_plan.extents svg = etree.Element("svg", nsmap=inkex.NSS, attrib= @@ -69,4 +50,4 @@ class Input(object): # Note: this is NOT the same as centering the design in the canvas! layer.set('transform', 'translate(%s,%s)' % (extents[0], extents[1])) - print >> sys.real_stdout, etree.tostring(svg) + print etree.tostring(svg) diff --git a/lib/output.py b/lib/output.py index 1c580f04..0d7f9918 100644 --- a/lib/output.py +++ b/lib/output.py @@ -7,13 +7,6 @@ from .utils import Point from .svg import PIXELS_PER_MM, get_doc_size, get_viewbox_transform -def add_thread(pattern, thread): - """Add a thread to a pattern and return the thread's index""" - - libembroidery.embPattern_addThread(pattern, thread) - - return libembroidery.embThreadList_count(pattern.threadList) - 1 - def get_command(stitch): if stitch.jump: return pyembroidery.JUMP diff --git a/lib/threads/color.py b/lib/threads/color.py index d94f8825..cc6c0c48 100644 --- a/lib/threads/color.py +++ b/lib/threads/color.py @@ -1,7 +1,7 @@ import simplestyle import re import colorsys - +from pyembroidery.EmbThread import EmbThread class ThreadColor(object): hex_str_re = re.compile('#([0-9a-z]{3}|[0-9a-z]{6})', re.I) @@ -9,6 +9,12 @@ class ThreadColor(object): def __init__(self, color, name=None, number=None, manufacturer=None): if color is None: self.rgb = (0, 0, 0) + elif isinstance(color, EmbThread): + self.name = color.description + self.number = color.catalog_number + self.manufacturer = color.brand + self.rgb = (color.get_red(), color.get_green(), color.get_blue()) + return elif isinstance(color, (list, tuple)): self.rgb = tuple(color) elif self.hex_str_re.match(color): -- cgit v1.2.3 From 40968365d4e8cbc271c654e008a31707add8a7e3 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 17 Jul 2018 21:34:08 -0400 Subject: update output extension for pyembroidery --- lib/extensions/output.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/extensions/output.py b/lib/extensions/output.py index f4b153e6..1dc8d19d 100644 --- a/lib/extensions/output.py +++ b/lib/extensions/output.py @@ -29,20 +29,17 @@ class Output(InkstitchExtension): patches = self.elements_to_patches(self.elements) stitch_plan = patches_to_stitch_plan(patches, self.options.collapse_length_mm * PIXELS_PER_MM) - # libembroidery wants to write to an actual file rather than stdout temp_file = tempfile.NamedTemporaryFile(suffix=".%s" % self.options.file_extension, delete=False) # in windows, failure to close here will keep the file locked temp_file.close() - # libembroidery likes to debug log things to stdout. No way to disable it. - save_stdout() write_embroidery_file(temp_file.name, stitch_plan, self.document.getroot()) # inkscape will read the file contents from stdout and copy # to the destination file that the user chose with open(temp_file.name) as output_file: - sys.real_stdout.write(output_file.read()) + sys.stdout.write(output_file.read()) # clean up the temp file os.remove(temp_file.name) -- cgit v1.2.3 From 017026e10c5b6a6ed2ee4324ceb9a7b3b6b2e359 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 17 Jul 2018 21:43:59 -0400 Subject: fix zip extension --- lib/extensions/zip.py | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) (limited to 'lib') diff --git a/lib/extensions/zip.py b/lib/extensions/zip.py index ca12efdd..02f29e8a 100644 --- a/lib/extensions/zip.py +++ b/lib/extensions/zip.py @@ -4,7 +4,7 @@ import os import inkex import tempfile from zipfile import ZipFile -from libembroidery import * +import pyembroidery from .base import InkstitchExtension from ..i18n import _ @@ -24,18 +24,11 @@ class Zip(InkstitchExtension): # it's kind of obnoxious that I have to do this... self.formats = [] - formatList = embFormatList_create() - curFormat = formatList - while(curFormat): - # extension includes the dot, so we'll remove it - extension = embFormat_extension(curFormat)[1:] - description = embFormat_description(curFormat) - writer_state = embFormat_writerState(curFormat) - - if writer_state.strip() and embFormat_type(curFormat) != EMBFORMAT_OBJECTONLY: + for format in pyembroidery.supported_formats(): + if 'writer' in format and format['category'] == 'embroidery': + extension = format['extension'] self.OptionParser.add_option('--format-%s' % extension, type="inkbool", dest=extension) self.formats.append(extension) - curFormat = curFormat.next def effect(self): if not self.get_elements(): @@ -49,19 +42,12 @@ class Zip(InkstitchExtension): files = [] - # libembroidery likes to debug log things to stdout. No way to disable it. - save_stdout() 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()) files.append(output_file) - # I'd love to do restore_stderr() here, but if I do, libembroidery's - # stuff still prints out and corrupts the zip! That's because it uses - # C's buffered stdout, so it hasn't actually written anything to the - # real standard output yet. - if not files: self.errormsg(_("No embroidery file formats selected.")) @@ -77,7 +63,7 @@ class Zip(InkstitchExtension): # inkscape will read the file contents from stdout and copy # to the destination file that the user chose with open(temp_file.name) as output_file: - sys.real_stdout.write(output_file.read()) + sys.stdout.write(output_file.read()) os.remove(temp_file.name) for file in files: -- cgit v1.2.3 From 89f1d45c30c1f0b540097122d23251bf1be8db8f Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 17 Jul 2018 21:45:45 -0400 Subject: clean up remaining libembroidery references --- lib/extensions/input.py | 5 ----- 1 file changed, 5 deletions(-) (limited to 'lib') diff --git a/lib/extensions/input.py b/lib/extensions/input.py index 99bb70ab..cb5ac452 100644 --- a/lib/extensions/input.py +++ b/lib/extensions/input.py @@ -3,11 +3,6 @@ from os.path import realpath, dirname, join as path_join import sys from inkex import etree import inkex - -# help python find libembroidery when running in a local repo clone -if getattr(sys, 'frozen', None) is None: - sys.path.append(realpath(path_join(dirname(__file__), '..', '..'))) - import pyembroidery from ..svg import PIXELS_PER_MM, render_stitch_plan -- cgit v1.2.3 From 5ce8df77a07b11b902792d299d4cb89b6951ccd8 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 20 Jul 2018 21:41:28 -0400 Subject: remove incorrect stop logic --- lib/stitch_plan/stitch_plan.py | 47 +++++++++++----------- lib/stitch_plan/stop.py | 88 ------------------------------------------ 2 files changed, 24 insertions(+), 111 deletions(-) delete mode 100644 lib/stitch_plan/stop.py (limited to 'lib') diff --git a/lib/stitch_plan/stitch_plan.py b/lib/stitch_plan/stitch_plan.py index 1a466295..0fa87d71 100644 --- a/lib/stitch_plan/stitch_plan.py +++ b/lib/stitch_plan/stitch_plan.py @@ -1,6 +1,4 @@ from .stitch import Stitch -from .stop import process_stop -from .trim import process_trim from .ties import add_ties from ..svg import PIXELS_PER_MM from ..utils.geometry import Point @@ -26,26 +24,29 @@ def patches_to_stitch_plan(patches, collapse_len=3.0 * PIXELS_PER_MM): if not patch.stitches: continue - if color_block.color != patch.color or color_block.stop_after: - # add a color change (only if we didn't just do a "STOP after") - if not color_block.stop_after: + if color_block.color != patch.color: + if len(color_block) == 0: + # We just processed a stop, which created a new color block. + # We'll just claim this new block as ours: + color_block.color = patch.color + else: + # end the previous block with a color change color_block.add_stitch(color_change=True) - color_block = stitch_plan.new_color_block(color=patch.color) + # make a new block of our color + color_block = stitch_plan.new_color_block(color=patch.color) - color_block.filter_duplicate_stitches() color_block.add_stitches(patch.stitches, no_ties=patch.stitch_as_is) if patch.trim_after: color_block.add_stitch(trim=True) if patch.stop_after: - process_stop(stitch_plan) - - # process_stop() may have split the block into two - color_block = stitch_plan.last_color_block + color_block.add_stitch(stop=True) + color_block = stitch_plan.new_color_block(color_block.color) - add_ties(stitch_plan) + stitch_plan.filter_duplicate_stitches() + stitch_plan.add_ties() return stitch_plan @@ -64,6 +65,14 @@ class StitchPlan(object): def add_color_block(self, color_block): self.color_blocks.append(color_block) + def filter_duplicate_stitches(self): + for color_block in self: + color_block.filter_duplicate_stitches() + + def add_ties(self): + # see ties.py + add_ties(self) + def __iter__(self): return iter(self.color_blocks) @@ -198,12 +207,12 @@ class ColorBlock(object): stitches = [self.stitches[0]] for stitch in self.stitches[1:]: - if stitches[-1].jump or stitch.stop or stitch.trim: - # Don't consider jumps, stops, or trims as candidates for filtering + if stitches[-1].jump or stitch.stop or stitch.trim or stitch.color_change: + # Don't consider jumps, stops, color changes, or trims as candidates for filtering pass else: l = (stitch - stitches[-1]).length() - if l <= 0.1: + if l <= 0.1 * PIXELS_PER_MM: # duplicate stitch, skip this one continue @@ -247,11 +256,3 @@ class ColorBlock(object): maxy = max(stitch.y for stitch in self) return minx, miny, maxx, maxy - - def split_at(self, index): - """Split this color block into two at the specified stitch index""" - - new_color_block = ColorBlock(self.color, self.stitches[index:]) - del self.stitches[index:] - - return new_color_block diff --git a/lib/stitch_plan/stop.py b/lib/stitch_plan/stop.py deleted file mode 100644 index 0ccaeaf8..00000000 --- a/lib/stitch_plan/stop.py +++ /dev/null @@ -1,88 +0,0 @@ -from copy import copy - -from ..svg import PIXELS_PER_MM - - -def process_stop(stitch_plan): - """Handle the "stop after" checkbox. - - The user wants the machine to pause after this patch. This can - be useful for applique and similar on multi-needle machines that - normally would not stop between colors. - - In most machine embroidery file formats, there's no such thing as - an actual "STOP" instruction. All that exists is a "color change" - command. - - On multi-needle machines, the user assigns needles to the colors in - the design before starting stitching. C01, C02, etc are the normal - needles, but C00 is special. For a block of stitches assigned - to C00, the machine will continue sewing with the last color it - had and pause after it completes the C00 block. Machines that don't - call it C00 still have a similar concept. - - We'll add a STOP instruction at the end of this color block. - Unfortunately, we have a bit of a catch-22: the user needs to set - C00 (or equivalent) for the _start_ of this block to get the - machine to stop at the end of this block. That means it will use - the previous color, which isn't the right color at all! - - For the first STOP in a given thread color, we'll need to - introduce an extra color change. The user can then set the correct - color for the first section and C00 for the second, resulting in - a stop where we want it. - - We'll try to find a logical place to split the color block, like - a TRIM or a really long stitch. Failing that, we'll just split - it in half. - """ - - color_block = stitch_plan.last_color_block - - if not color_block or len(color_block) < 2: - return - - last_stitch = color_block.last_stitch - color_block.add_stitch(stop=True) - - if len(stitch_plan) > 1: - # if this isn't the first stop in this color, then we're done - if stitch_plan.color_blocks[-2].stop_after and \ - stitch_plan.color_blocks[-2].color == color_block.color: - return - - # We need to split this color block. Pick the last TRIM or - # the last long stitch (probably between distant patches). - - for i in xrange(len(color_block) - 2, -1, -1): - stitch = color_block.stitches[i] - - if stitch.trim: - # ignore the trim right before the stop we just added - if i < len(color_block) - 2: - # split after the trim - i = i + 1 - break - - if i > 0: - next_stitch = color_block.stitches[i + 1] - - if (stitch - next_stitch).length() > 20 * PIXELS_PER_MM: - break - - if i == 0: - # Darn, we didn't find a TRIM or long stitch. Just chop the - # block in half. - i = len(color_block) / 2 - - new_color_block = color_block.split_at(i) - - # If we're splitting in the middle of a run of stitches, we don't - # want a gap to appear in the preview and the PDF printout, so - # add an extra stitch to bridge the gap. Technically this will - # result in a double needle penetration but it's no big deal. - if not color_block.last_stitch.trim: - color_block.add_stitch(copy(new_color_block.stitches[0])) - - color_block.add_stitch(color_change=True, fake_color_change=True) - stitch_plan.add_color_block(new_color_block) -- cgit v1.2.3 From e0cecd6fa444e3b7ea2391ce4f2953ae9fd5f3fa Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 23 Jul 2018 20:15:49 -0400 Subject: fix a couple crashes --- lib/stitch_plan/stitch_plan.py | 4 ++++ lib/svg/svg.py | 3 +++ 2 files changed, 7 insertions(+) (limited to 'lib') diff --git a/lib/stitch_plan/stitch_plan.py b/lib/stitch_plan/stitch_plan.py index 0fa87d71..a7cd60e8 100644 --- a/lib/stitch_plan/stitch_plan.py +++ b/lib/stitch_plan/stitch_plan.py @@ -45,6 +45,10 @@ def patches_to_stitch_plan(patches, collapse_len=3.0 * PIXELS_PER_MM): color_block.add_stitch(stop=True) color_block = stitch_plan.new_color_block(color_block.color) + if len(color_block) == 0: + # last block ended in a stop, so now we have an empty block + del stitch_plan.color_blocks[-1] + stitch_plan.filter_duplicate_stitches() stitch_plan.add_ties() diff --git a/lib/svg/svg.py b/lib/svg/svg.py index 5552abd8..48b1343a 100644 --- a/lib/svg/svg.py +++ b/lib/svg/svg.py @@ -37,6 +37,9 @@ def color_block_to_realistic_stitches(color_block, svg): paths = [] for point_list in color_block_to_point_lists(color_block): + if not point_list: + continue + color = color_block.color.visible_on_white.darker.to_hex_str() start = point_list[0] for point in point_list[1:]: -- cgit v1.2.3 From 1bd7aa110a1b30a6c44f4d792b3c817e10234c07 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 23 Jul 2018 20:22:53 -0400 Subject: change '# stops' in block to be 'stop after?' --- lib/stitch_plan/stitch_plan.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/stitch_plan/stitch_plan.py b/lib/stitch_plan/stitch_plan.py index a7cd60e8..682ea09f 100644 --- a/lib/stitch_plan/stitch_plan.py +++ b/lib/stitch_plan/stitch_plan.py @@ -97,7 +97,7 @@ class StitchPlan(object): @property def num_stops(self): - return sum(block.num_stops for block in self) + return sum(1 for block in self if block.stop_after) @property def num_trims(self): @@ -185,12 +185,6 @@ class ColorBlock(object): """Number of stitches in this color block.""" return len(self.stitches) - @property - def num_stops(self): - """Number of pauses in this color block.""" - - return sum(1 for stitch in self if stitch.stop) - @property def num_trims(self): """Number of trims in this color block.""" -- cgit v1.2.3