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.3.1 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.3.1 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.3.1 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.3.1 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.3.1 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 ++++++++++++++++++++++++++++----- messages.po | 5 ++++- 2 files changed, 32 insertions(+), 6 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, diff --git a/messages.po b/messages.po index 7b2ca815..376437e0 100644 --- a/messages.po +++ b/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2018-05-01 21:21-0400\n" +"POT-Creation-Date: 2018-06-01 20:34-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -44,6 +44,9 @@ msgstr "" msgid "Inset" msgstr "" +msgid "Expand" +msgstr "" + msgid "TRIM after" msgstr "" -- cgit v1.3.1 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 ++--- print/resources/inkstitch.js | 54 ++++++++++++++++++++++++++++++++-- print/resources/style.css | 22 ++++++++++++++ print/templates/operator_overview.html | 4 +++ print/templates/print_detail.html | 7 +++-- print/templates/print_overview.html | 12 +++++--- print/templates/ui.html | 8 ++--- 7 files changed, 99 insertions(+), 16 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 diff --git a/print/resources/inkstitch.js b/print/resources/inkstitch.js index e5065fe6..bb140705 100644 --- a/print/resources/inkstitch.js +++ b/print/resources/inkstitch.js @@ -7,6 +7,9 @@ $.postJSON = function(url, data, success=null) { }); }; +var realistic_rendering = {}; +var normal_rendering = {}; + function ping() { $.get("/ping") .done(function() { setTimeout(ping, 1000) }) @@ -142,6 +145,11 @@ $(function() { setSVGTransform($(this), $(this).find('svg').css('transform')); }); + // ignore mouse events on the buttons (Fill, 100%, Apply to All) + $('figure.inksimulation div').on('mousedown mouseup', function(e) { + e.stopPropagation(); + }); + /* Apply transforms to All */ $('button.svg-apply').click(function() { var transform = $(this).parent().siblings('svg').css('transform'); @@ -190,6 +198,12 @@ $(function() { } }); }); + + $.getJSON('/realistic', function(realistic_data) { + // realistic_rendering is global + realistic_rendering = realistic_data; + }); + // wait until page size is set (if they've specified one) and then scale SVGs to fit setTimeout(function() { scaleAllSvg() }, 500); }); @@ -288,8 +302,8 @@ $(function() { $('.modal').hide(); }); - //Checkbox - $(':checkbox').on('change initialize', function() { + // View selection checkboxes + $(':checkbox.view').on('change initialize', function() { var field_name = $(this).attr('data-field-name'); $('.' + field_name).toggle($(this).prop('checked')); @@ -299,6 +313,42 @@ $(function() { $.postJSON('/settings/' + field_name, {value: $(this).prop('checked')}); }); + // Realistic rendering checkboxes + $(':checkbox.realistic').on('change', function(e) { + console.log("realistic rendering checkbox"); + + var item = $(this).data('field-name'); + var figure = $(this).closest('figure'); + var svg = figure.find('svg'); + var transform = svg.css('transform'); + var checked = $(this).prop('checked'); + + console.log("" + item + " " + transform); + + // do this later to allow this event handler to return now, + // which will cause the checkbox to be checked or unchecked + // immediately even if SVG rendering takes awhile + setTimeout(function() { + if (checked) { + if (!(item in normal_rendering)) { + normal_rendering[item] = svg[0].outerHTML; + } + svg[0].outerHTML = realistic_rendering[item]; + } else { + svg[0].outerHTML = normal_rendering[item]; + } + // can't use the svg variable here because setting outerHTML created a new tag + figure.find('svg').css({transform: transform}); + }, 100); + + e.stopPropagation(); + return true; + }); + + $('button.svg-realistic').click(function(e){ + $(this).find('input').click(); + }); + // Logo $('#logo-picker').change(function(e) { var file = e.originalEvent.currentTarget.files[0]; diff --git a/print/resources/style.css b/print/resources/style.css index 58ec8714..716934a8 100644 --- a/print/resources/style.css +++ b/print/resources/style.css @@ -473,6 +473,28 @@ body { border: none; background: grey; color: white; + display: inline-block; + font-size: 16px; + font-family: "Barlow", sans-serif; + padding-left: 3px; + padding-right: 3px; + margin: 0px 1px 0px 1px; + } + + input.realistic { + position: absolute; + transform: scale(0.7); + margin-left: 2px; + } + + label.realistic { + margin-left: 20px; + } + + /* prevents Chrome from sending a double event for the checkbox + and the containing + diff --git a/print/templates/print_detail.html b/print/templates/print_detail.html index 714d33a2..e73fe918 100644 --- a/print/templates/print_detail.html +++ b/print/templates/print_detail.html @@ -22,9 +22,12 @@ + - - +
{% include 'color_swatch.html' %}
diff --git a/print/templates/print_overview.html b/print/templates/print_overview.html index efcf5b2e..b42ab7a9 100644 --- a/print/templates/print_overview.html +++ b/print/templates/print_overview.html @@ -32,14 +32,18 @@ + - - + +
{% for color_block in color_blocks %} {% include 'color_swatch.html' %} - {% endfor %} - + {% endfor %} +
{{ _('Client Signature') }}
diff --git a/print/templates/ui.html b/print/templates/ui.html index b09dc941..3b11f345 100644 --- a/print/templates/ui.html +++ b/print/templates/ui.html @@ -26,10 +26,10 @@
{{ _('Print Layouts') }} -

-

-

-

+

+

+

+

-- cgit v1.3.1 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.3.1 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.3.1 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 +++++----- print/resources/inkstitch.js | 59 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 59 insertions(+), 13 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 diff --git a/print/resources/inkstitch.js b/print/resources/inkstitch.js index bb140705..4a757d5f 100644 --- a/print/resources/inkstitch.js +++ b/print/resources/inkstitch.js @@ -8,6 +8,7 @@ $.postJSON = function(url, data, success=null) { }; var realistic_rendering = {}; +var realistic_cache = {}; var normal_rendering = {}; function ping() { @@ -199,10 +200,21 @@ $(function() { }); }); - $.getJSON('/realistic', function(realistic_data) { +// $.getJSON('/realistic', function(realistic_data) { // realistic_rendering is global - realistic_rendering = realistic_data; - }); + /* + $.each(realistic_data, function(name, xml) { + var image = new Image(); + console.log("doing " + name); + image.onload = function() { + console.log("setting " + name + " = " + image); + realistic_rendering[name] = image; + } + image.src = 'data:image/svg+xml,' + xml; + }) + */ +// realistic_rendering = realistic_data; +// }); // wait until page size is set (if they've specified one) and then scale SVGs to fit setTimeout(function() { scaleAllSvg() }, 500); @@ -325,6 +337,12 @@ $(function() { console.log("" + item + " " + transform); + function finalize(svg_content) { + svg[0].outerHTML = svg_content; + // can't use the svg variable here because setting outerHTML created a new tag + figure.find('svg').css({transform: transform}); + } + // do this later to allow this event handler to return now, // which will cause the checkbox to be checked or unchecked // immediately even if SVG rendering takes awhile @@ -333,12 +351,39 @@ $(function() { if (!(item in normal_rendering)) { normal_rendering[item] = svg[0].outerHTML; } - svg[0].outerHTML = realistic_rendering[item]; + + if (!(item in realistic_cache)) { + // pre-render the realistic SVG to a raster image to spare the poor browser + var image = document.createElement('img'); + image.onload = function() { + console.log("rendering!"); + var canvas = document.createElement('canvas'); + + // maybe make DPI configurable? for now, use 600 + canvas.width = image.width / 96 * 600; + canvas.height = image.height / 96 * 600; + + var ctx = canvas.getContext('2d'); + + // rendering slows down the browser enough that we can miss sending + // pings, so tell the server side to wait for us + $.get("/printing/start") + .done(function() { + ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height); + realistic_cache[item] = '' + + '' + + ''; + finalize(realistic_cache[item]); + $.get("/printing/end"); + }); + }; + image.src = '/realistic/' + item; + } else { + finalize(realistic_cache[item]); + } } else { - svg[0].outerHTML = normal_rendering[item]; + finalize(normal_rendering[item]); } - // can't use the svg variable here because setting outerHTML created a new tag - figure.find('svg').css({transform: transform}); }, 100); e.stopPropagation(); -- cgit v1.3.1 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.3.1 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.3.1 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.3.1 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.3.1 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 ++ messages.po | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) (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() diff --git a/messages.po b/messages.po index 376437e0..02d4aba6 100644 --- a/messages.po +++ b/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2018-06-01 20:34-0400\n" +"POT-Creation-Date: 2018-06-10 16:01-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -124,6 +124,10 @@ msgstr "" msgid "Inset amount (default: half of contour underlay inset)" msgstr "" +#, python-format +msgid "satin column: %(id)s: at least two subpaths required (%(num)d found)" +msgstr "" + msgid "" "One or more rails crosses itself, and this is not allowed. Please split " "into multiple satin columns." -- cgit v1.3.1 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.3.1 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 ++++++++++++++++++++--------- messages.po | 4 ++-- 3 files changed, 23 insertions(+), 12 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() diff --git a/messages.po b/messages.po index 02d4aba6..b872f864 100644 --- a/messages.po +++ b/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2018-06-10 16:01-0400\n" +"POT-Creation-Date: 2018-06-12 20:18-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -185,7 +185,7 @@ msgstr "" msgid "No embroiderable paths found in document." msgstr "" -msgid "Tip: use Path -> Object to Path to convert non-paths before embroidering." +msgid "Tip: use Path -> Object to Path to convert non-paths." msgstr "" msgid "" -- cgit v1.3.1 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.3.1 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.3.1 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.3.1 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.3.1 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.3.1 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 +- messages.po | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) (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): diff --git a/messages.po b/messages.po index 36af2e50..fb081eb1 100644 --- a/messages.po +++ b/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2018-06-15 21:44-0400\n" +"POT-Creation-Date: 2018-06-16 22:33-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -319,6 +319,9 @@ msgstr "" msgid "Embroidery Simulation" msgstr "" +msgid "No embroidery file formats selected." +msgstr "" + msgid "" "Unable to autofill. This most often happens because your shape is made " "up of multiple sections that aren't connected." -- cgit v1.3.1 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 ++ symbols/inkstitch.svg | 127 +++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 259 insertions(+), 22 deletions(-) create mode 100644 lib/commands.py create mode 100644 lib/svg/path.py create mode 100644 symbols/inkstitch.svg (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) diff --git a/symbols/inkstitch.svg b/symbols/inkstitch.svg new file mode 100644 index 00000000..3b8a78b2 --- /dev/null +++ b/symbols/inkstitch.svg @@ -0,0 +1,127 @@ + + + + + + + + Ink/Stitch Commands + + + Fill stitch starting point + + + + + Fill stitch ending point + + + + + + + + image/svg+xml + + + + + + + + + + -- cgit v1.3.1 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 ++++++++++++++++ messages.po | 6 +++++- 2 files changed, 21 insertions(+), 1 deletion(-) (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] diff --git a/messages.po b/messages.po index fb081eb1..e6bfed43 100644 --- a/messages.po +++ b/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2018-06-16 22:33-0400\n" +"POT-Creation-Date: 2018-06-22 22:19-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -47,6 +47,10 @@ msgstr "" msgid "Expand" msgstr "" +#, python-format +msgid "%(id)s has more than one command of type '%(command)s' linked to it" +msgstr "" + msgid "TRIM after" msgstr "" -- cgit v1.3.1 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.3.1 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.3.1 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.3.1 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.3.1 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.3.1 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.3.1 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 --- inx/inkstitch_install.inx | 17 +++++++ inx/inkstitch_palettes.inx | 17 ------- lib/extensions/__init__.py | 2 +- lib/extensions/install.py | 123 +++++++++++++++++++++++++++++++++++++++++++++ lib/extensions/palettes.py | 112 ----------------------------------------- messages.po | 26 +++++++--- 6 files changed, 160 insertions(+), 137 deletions(-) create mode 100644 inx/inkstitch_install.inx delete mode 100644 inx/inkstitch_palettes.inx create mode 100644 lib/extensions/install.py delete mode 100644 lib/extensions/palettes.py (limited to 'lib') diff --git a/inx/inkstitch_install.inx b/inx/inkstitch_install.inx new file mode 100644 index 00000000..7275e13a --- /dev/null +++ b/inx/inkstitch_install.inx @@ -0,0 +1,17 @@ + + + <_name>Install add-ons for Inkscape + org.inkstitch.install + inkstitch.py + inkex.py + install + + all + + + + + + diff --git a/inx/inkstitch_palettes.inx b/inx/inkstitch_palettes.inx deleted file mode 100644 index 5daa3196..00000000 --- a/inx/inkstitch_palettes.inx +++ /dev/null @@ -1,17 +0,0 @@ - - - <_name>Install thread manufacturer color palettes - org.inkstitch.palettes - inkstitch.py - inkex.py - palettes - - all - - - - - - 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() diff --git a/messages.po b/messages.po index e6bfed43..89ce2de5 100644 --- a/messages.po +++ b/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2018-06-22 22:19-0400\n" +"POT-Creation-Date: 2018-06-24 15:55-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -198,7 +198,19 @@ msgid "" "Seeing a 'no such option' message? Please restart Inkscape to fix." msgstr "" -msgid "Directory in which to install palettes:" +msgid "" +"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:" +msgstr "" + +msgid "thread manufacturer color palettes" +msgstr "" + +msgid "Ink/Stitch visual commands (Object -> Symbols...)" +msgstr "" + +msgid "Directory in which to install add-ons:" msgstr "" msgid "Choose another directory..." @@ -210,24 +222,24 @@ msgstr "" msgid "Cancel" msgstr "" -msgid "Choose Inkscape palettes directory" +msgid "Choose Inkscape directory" msgstr "" -msgid "Thread palette installation failed" +msgid "Inkscape add-on installation failed" msgstr "" msgid "Installation Failed" msgstr "" msgid "" -"Thread palette files have been installed. Please restart Inkscape to " -"load the new palettes." +"Inkscape add-on files have been installed. Please restart Inkscape to " +"load the new add-ons." msgstr "" msgid "Installation Completed" msgstr "" -msgid "Ink/Stitch Thread Palette Installer" +msgid "Ink/Stitch Add-ons Installer" msgstr "" msgid "These settings will be applied to 1 object." -- cgit v1.3.1 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 ++++---------------- messages.po | 8 +------- 2 files changed, 5 insertions(+), 23 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() diff --git a/messages.po b/messages.po index 89ce2de5..d6f156f2 100644 --- a/messages.po +++ b/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2018-06-24 15:55-0400\n" +"POT-Creation-Date: 2018-06-28 20:32-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -210,12 +210,6 @@ msgstr "" msgid "Ink/Stitch visual commands (Object -> Symbols...)" msgstr "" -msgid "Directory in which to install add-ons:" -msgstr "" - -msgid "Choose another directory..." -msgstr "" - msgid "Install" msgstr "" -- cgit v1.3.1 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 --- inx/inkstitch_flip.inx | 17 +++++++++++++++++ lib/extensions/__init__.py | 1 + lib/extensions/flip.py | 34 ++++++++++++++++++++++++++++++++++ messages.po | 5 ++++- 4 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 inx/inkstitch_flip.inx create mode 100644 lib/extensions/flip.py (limited to 'lib') diff --git a/inx/inkstitch_flip.inx b/inx/inkstitch_flip.inx new file mode 100644 index 00000000..f129b8d9 --- /dev/null +++ b/inx/inkstitch_flip.inx @@ -0,0 +1,17 @@ + + + <_name>Flip Satin Columns + org.inkstitch.flip_satins + inkstitch.py + inkex.py + flip + + all + + + + + + 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) diff --git a/messages.po b/messages.po index d6f156f2..72b60bb0 100644 --- a/messages.po +++ b/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2018-06-28 20:32-0400\n" +"POT-Creation-Date: 2018-06-30 13:02-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -198,6 +198,9 @@ msgid "" "Seeing a 'no such option' message? Please restart Inkscape to fix." msgstr "" +msgid "Please select one or more satin columns to flip." +msgstr "" + msgid "" "Ink/Stitch can install files (\"add-ons\") that make it easier to use " "Inkscape to create machine embroidery designs. These add-ons will be " -- cgit v1.3.1 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.3.1 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 +- messages.po | 20 +- print/resources/favicon.ico | Bin 0 -> 102290 bytes print/resources/inkstitch.js | 126 ++++++++++- print/resources/style.css | 351 ++++++++++++++++++++++------- print/templates/headline.html | 2 +- print/templates/index.html | 6 +- print/templates/operator_detailedview.html | 138 ++++++------ print/templates/operator_overview.html | 11 +- print/templates/print_detail.html | 11 +- print/templates/print_overview.html | 11 +- print/templates/ui.html | 76 ++++--- print/templates/ui_svg_action_buttons.html | 10 + 13 files changed, 522 insertions(+), 246 deletions(-) create mode 100644 print/resources/favicon.ico create mode 100644 print/templates/ui_svg_action_buttons.html (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") diff --git a/messages.po b/messages.po index 72b60bb0..a9851863 100644 --- a/messages.po +++ b/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2018-06-30 13:02-0400\n" +"POT-Creation-Date: 2018-07-10 19:54-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -441,15 +441,6 @@ msgstr "" msgid "Ctrl + Scroll to Zoom" msgstr "" -msgid "Scale" -msgstr "" - -msgid "Fit" -msgstr "" - -msgid "Apply to all" -msgstr "" - msgid "COLOR" msgstr "" @@ -507,3 +498,12 @@ msgstr "" msgid "No" msgstr "" +msgid "Scale" +msgstr "" + +msgid "Fit" +msgstr "" + +msgid "Apply to all" +msgstr "" + diff --git a/print/resources/favicon.ico b/print/resources/favicon.ico new file mode 100644 index 00000000..1b5455bd Binary files /dev/null and b/print/resources/favicon.ico differ diff --git a/print/resources/inkstitch.js b/print/resources/inkstitch.js index 6bcb183d..9f67bc90 100644 --- a/print/resources/inkstitch.js +++ b/print/resources/inkstitch.js @@ -17,6 +17,85 @@ function ping() { .fail(function() { $('#errors').attr('class', 'show') }); } +//function to chunk opd view into pieces + // source: https://stackoverflow.com/questions/3366529/wrap-every-3-divs-in-a-div +$.fn.chunk = function(size) { + var arr = []; + for (var i = 0; i < this.length; i += size) { + arr.push(this.slice(i, i + size)); + } + return this.pushStack(arr, "chunk", size); +} + +// build operator detailed view (opd) +function buildOpd(thumbnail_size = $('#operator-detailedview-thumbnail-size').val() ) { + + var thumbnail_size = parseInt(thumbnail_size); + var thumbnail_size_mm = thumbnail_size + 'mm'; + + var thumbnail_layout = (thumbnail_size >= 60) ? 'medium' : 'small'; + + // remove old settings + $( "div.page.operator-detailedview header" ).remove(); + $( "div.page.operator-detailedview footer" ).remove(); + $( "div.page.operator-detailedview .job-headline" ).remove(); + $('div.page.operator-detailedview .opd-color-block').parentsUntil('div.page.operator-detailedview').addBack().unwrap(); + $('.opd-color-block').removeClass('medium large'); + $('.opd-color-block').removeAttr('style'); + + // set thumbnail size + $('.operator-svg.operator-preview').css({ + 'width': thumbnail_size_mm, + 'height': thumbnail_size_mm, + 'max-width': thumbnail_size_mm + }); + + // calculate number of blocks per page + var num_blocks_per_page = 1; + if(thumbnail_layout == 'medium') { + $('.opd-color-block').addClass('medium'); + // set width to be able to calculate the height + $('.opd-color-block').css({ 'width': thumbnail_size_mm }); + // calculate max height -> source: https://stackoverflow.com/questions/6060992/element-with-the-max-height-from-a-set-of-elements + var color_box_height = Math.max.apply(null, $('.opd-color-block').map(function () { return $(this).height(); }).get()); + var container_height = $('#opd-info').height(); + var num_rows = Math.floor(container_height / color_box_height); + var num_columns = Math.floor(175 / thumbnail_size); + // if only two blocks fit into one row, use 50% of the space for each of them + if(num_columns == 2) { $('.opd-color-block').css({ 'width': 'calc(50% - 2mm)' }); } + // set equal height for all color blocks + $('.opd-color-block').css({ 'height': color_box_height }); + // set number of color blocks per page for medium thumbnails + num_blocks_per_page = num_columns * num_rows; + // use layout for large thumbnails if only 2 or less color blocks fit on one page + if(num_blocks_per_page <= 2) { + $('.opd-color-block').removeClass('medium').removeAttr('style').addClass('large'); + thumbnail_layout = 'large'; + // set number of color blocks per page for large thumbnails + num_blocks_per_page = 2; + } + } else { + // set number of color blocks per page for small thumbnails + num_blocks_per_page = Math.floor(220 / thumbnail_size); + } + // set number of color blocks per page to 1 if it defaults to zero + // this should never happen, but we want to avoid the browser to crash + num_blocks_per_page = (num_blocks_per_page <= 0) ? '1' : num_blocks_per_page; + + // adjust to new settings + var header = $('#opd-info header').prop('outerHTML'); + var footer = $('#opd-info footer').prop('outerHTML'); + var job_headline = $('#opd-info .job-headline').prop('outerHTML'); + var opd_visibility = ($('#operator-detailedview').is(':checked')) ? 'block' :'none'; + var paper_size = $('#printing-size').val(); + $('.opd-color-block').chunk(num_blocks_per_page).wrap('
'); + $('div.operator-detailedview').prepend(header); + $('.operator-job-info').prepend(job_headline); + $('div.operator-detailedview').append(footer); + // update page numbers + setPageNumbers(); +} + // set pagenumbers function setPageNumbers() { var totalPageNum = $('body').find('.page:visible').length; @@ -46,11 +125,13 @@ function scaleSVG(element, scale = 'fit') { element.find('.scale').text(label); } -// set preview svg scale to fit into its box if transform is not set +// set preview svg scale to fit into its box if display block and transform is not set function scaleAllSvg() { $('.page').each(function() { - if( $(this).find('.inksimulation svg').css('transform') == 'none') { - scaleSVG($(this).find('.inksimulation'), 'fit'); + if( $(this).css('display') == 'block' ) { + if( $(this).find('.inksimulation svg').css('transform') == 'none') { + scaleSVG($(this).find('.inksimulation'), 'fit'); + } } }); } @@ -74,8 +155,6 @@ function setSVGTransform(figure, transform) { $(function() { setTimeout(ping, 1000); - setPageNumbers(); - /* SCALING AND MOVING SVG */ /* Mousewheel scaling */ @@ -192,6 +271,9 @@ $(function() { item.attr('src', value); } else if (item.is('select')) { item.val(value).trigger('initialize'); + } else if (item.is('input[type=range]')) { + item.val(value).trigger('initialize'); + $('#display-thumbnail-size').html(value + 'mm'); } else if (item.is('figure.inksimulation')) { setSVGTransform(item, value); } else { @@ -200,8 +282,11 @@ $(function() { }); }); - // wait until page size is set (if they've specified one) and then scale SVGs to fit - setTimeout(function() { scaleAllSvg() }, 500); + // wait until page size is set (if they've specified one) and then scale SVGs to fit and build operator detailed view + setTimeout(function() { + scaleAllSvg(); + buildOpd(); + }, 500); }); $('[contenteditable="true"]').keypress(function(e) { @@ -258,12 +343,37 @@ $(function() { /* Settings */ + // Settings Tabs + $('#tabs button').click(function() { + var active_fieldset_position = $(this).index() +1; + $('#settings-ui #fieldsets-ui > fieldset').css({'display': 'none'}); + $('#settings-ui #fieldsets-ui > fieldset:nth-child('+active_fieldset_position+')').css({'display': 'block'}); + $('#tabs .tab.active').removeClass("active"); + $(this).addClass("active"); + }); + // Paper Size $('select#printing-size').on('change initialize', function(){ $('.page').toggleClass('a4', $(this).find(':selected').val() == 'a4'); }).on('change', function() { $.postJSON('/settings/paper-size', {value: $(this).find(':selected').val()}); }); + + // Operator detailed view: thumbnail size setting + $(document).on('input', '#operator-detailedview-thumbnail-size', function() { + var thumbnail_size_mm = $(this).val() + 'mm'; + $('#display-thumbnail-size').html( thumbnail_size_mm ); + }); + + // Operator detailed view: thumbnail size setting action + $('#operator-detailedview-thumbnail-size').change(function() { + // set thumbnail size + var thumbnail_size = $(this).val(); + // set page break positions + buildOpd(thumbnail_size); + + $.postJSON('/settings/operator-detailedview-thumbnail-size', {value: thumbnail_size}); + }); // Thread Palette $('select#thread-palette').change(function(){ @@ -303,6 +413,7 @@ $(function() { var field_name = $(this).attr('data-field-name'); $('.' + field_name).toggle($(this).prop('checked')); + scaleAllSvg(); setPageNumbers(); }).on('change', function() { var field_name = $(this).attr('data-field-name'); @@ -397,6 +508,7 @@ $(function() { settings["client-detailedview"] = $("[data-field-name='client-detailedview']").is(':checked'); settings["operator-overview"] = $("[data-field-name='operator-overview']").is(':checked'); settings["operator-detailedview"] = $("[data-field-name='operator-detailedview']").is(':checked'); + settings["operator-detailedview-thumbnail-size"] = $("[data-field-name='operator-detailedview-thumbnail-size']").val(); settings["paper-size"] = $('select#printing-size').find(':selected').val(); var logo = $("figure.brandlogo img").attr('src'); diff --git a/print/resources/style.css b/print/resources/style.css index 81b2ea7e..174b21dd 100644 --- a/print/resources/style.css +++ b/print/resources/style.css @@ -42,65 +42,6 @@ src: url(barlow-condensed-extra-bold.ttf) format('truetype'); } -@media screen { - .page { - margin-top: 20mm !important; - outline: 1px dotted grey; - } - - .header-field::before { - content: attr(data-label); - padding-right: 0.5em; - } - - [contenteditable=true]:empty::after{ - content: attr(data-placeholder); - color: rgb(200, 200, 200); - font-weight: normal; - } -} - -@media print { - body { - margin-bottom: 0 !important; - } - - .page { - page-break-after: always; - margin: 0 !important; - } - - figure.inksimulation div { - display: none; - } - - .ui { - display: none; - } - - #settings-ui { - display: none !important; - } - - #errors { - display: none !important; - } - - .header-field:not(:empty)::before { - content: attr(data-label); - padding-right: 0.5em; - } - - span.logo-instructions { - display: none; - } -} - -@page { - size: auto; /* auto is the initial value */ - margin: 0; -} - body { font-family: "Barlow", sans-serif; font-size: 8pt; @@ -131,7 +72,24 @@ body { height: 292mm; padding: 15mm; } + + .page.client-overview.a4 header, .page.operator-overview.a4 header { + height: 50mm; + flex-direction: row; + } + + .page.client-overview.a4 div.job-details, .page.operator-overview.a4 div.job-details { + width: 100%; + } + .page.client-overview.a4 .client-overview-main figure.inksimulation { + height: 150mm; + } + + .page.client-overview.a4 figure.brandlogo, .page.operator-overview.a4 figure.brandlogo { + margin: -6mm 2.5mm; + } + /* Settings */ .ui { @@ -230,9 +188,71 @@ body { border-radius: 5px; cursor: pointer; } + + #settings-ui #tabs { + margin-left: 20px; + z-index: 1; + } + + #settings-ui button { + background: transparent; + border: 1px solid #413232; + padding: 5px; + } + + #settings-ui button.active { + border-bottom: 1px solid white; + } + + #settings-ui #ui-design { + display: none; + } + + #settings-ui > #fieldsets-ui > fieldset > legend { + display: none; + } #settings-ui fieldset { + margin-top: -1px; margin-bottom: 1em; + border: 1px solid rgb(80,80,80); + z-index: 2; + } + + #settings-ui div { + position: relative; + } + + #settings-ui .select-container { + position: relative; + display: inline-block; + } + + #settings-ui select { + margin-left: 1em; + height: 35px; + padding: 0px 25px 0 5px; + font-size: 16px; + background-color: transparent; + border: 1px solid rgb(80,80,80); + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + } + + #settings-ui select::-ms-expand { + display: none; + } + + #settings-ui .select-container::after { + content: 'â–¾'; + position: absolute; + right: 0; + top: 0; + padding: 0 5px; + border-left: 1px solid rgb(80,80,80); + line-height: 35px; + pointer-events: none; } #modal-background { @@ -287,8 +307,8 @@ body { display: block; width: 100%; height: 100%; - line-height: 30mm; text-align: center; + position: relative; } figure.brandlogo img { @@ -308,14 +328,15 @@ body { } .logo-instructions { - white-space: nowrap; - - /* chrome ignores this :( + display: block; text-align: center; - */ - + font-weight: bold; font-size: 10px; - color: rgb(192, 192, 192); + color: rgb(117, 117, 117); + background: rgba(255,255,255, 0.8); + position: absolute; + bottom: 3mm; + cursor: pointer; } .operator-detailedview figure.brandlogo { @@ -482,13 +503,9 @@ body { } input.realistic { - position: absolute; transform: scale(0.7); - margin-left: 2px; - } - - label.realistic { - margin-left: 20px; + margin: 0; + vertical-align: bottom; } /* prevents Chrome from sending a double event for the checkbox @@ -599,26 +616,26 @@ body { height: 230mm; } - .operator-job-info { + .operator-detailedview.small .operator-job-info { display: table; width: 100%; } - .operator-job-info div { + .operator-detailedview.small .operator-job-info div { display: table-row; } - div.job-headline { + .operator-detailedview.small div.job-headline { display: table-header-group; font-size: 9pt; font-weight: bold; } - div.job-headline p { + .operator-detailedview.small div.job-headline p { height: 1em; } - .operator-job-info p { + .operator-detailedview.small .operator-job-info p { height: 15mm; max-width: 15mm; display: table-cell; @@ -628,11 +645,11 @@ body { border: 1px solid rgb(239,239,239); } - .operator-job-info span { + .operator-detailedview.small .operator-job-info span { display: block; } - .operator-job-info span.color-index { + .operator-detailedview.small .operator-job-info span.color-index { position: absolute; top: 0; left: 0; @@ -640,25 +657,126 @@ body { width: 10mm; } - .operator-svg.operator-colorswatch { - width: 15mm; + .operator-detailedview.small .operator-svg.operator-colorswatch { + width: 10mm; } - .operator-svg.operator-preview { - min-width: 15mm; - max-width: 20mm; + .operator-detailedview.small .operator-svg.operator-preview { + width: 15mm; height: 15mm; } - .operator-svg svg { + .operator-detailedview.small .operator-svg svg { position: absolute; top: 0; left: 0; - width: auto; - max-width: 30mm; + right: 0; + width: 100%; height: 100%; } + + /* opd medium/large thumbnails */ + + .operator-detailedview.medium div.job-headline, + .operator-detailedview.large div.job-headline { + display: none; + } + + .opd-color-block.medium, + .opd-color-block.large { + overflow: hidden; + position: relative; + border: 0.5mm solid rgb(239,239,239); + } + + .opd-color-block.medium .operator-colorswatch, + .opd-color-block.large .operator-colorswatch { + position: absolute; + top: 0; + padding: 0; + margin: 0; + width: 10mm; + height: 10mm; + border-right: 0.5mm solid rgb(239,239,239); + border-bottom: 0.5mm solid rgb(239,239,239); + } + + .opd-color-block.medium .operator-colorswatch svg, + .opd-color-block.medium .opd-summary p:first-child span, + .opd-color-block.medium .operator-preview svg, + .opd-color-block.large .operator-colorswatch svg, + .opd-color-block.large .opd-summary p:first-child span, + .opd-color-block.large .operator-preview svg { + height: 100%; + width: 100%; + } + + /* opd medium thumbnails */ + + .opd-color-block.medium { + display: inline-block; + float: left; + margin: 1mm; + } + + .opd-color-block.medium p { + margin: 0 auto; + } + + .opd-color-block.medium p:last-child { + margin-bottom: 1mm; + } + + .opd-color-block.medium .operator-preview { + margin: 1mm auto 0 auto; + } + + .opd-color-block.medium.opd-summary .operator-colorswatch { + display: none; + } + + .opd-color-block.medium span::before { + content: ' \00B7 '; + line-height: 0; + display: inline-block; + margin: 0 1mm; + vertical-align: super; + } + + .opd-color-block.medium span:first-child::before { + content: ''; + } + + /* opd large thumbnails */ + + .opd-color-block.large { + display: block; + margin: 5mm 0; + } + + .opd-color-block.large:first-child { + margin-top: 0; + } + .opd-color-block.large .operator-preview { + margin: 0; + } + + .opd-color-block.large .operator-preview { + float: left; + } + + .opd-color-block.large p { + text-align: left; + } + + .opd-color-block.large p:nth-child(3) { + margin-top: 5mm; + } + + .opd-color-block.large span { + display: block; + } /* Footer */ @@ -830,3 +948,62 @@ body { height: calc(100% / 5); width: calc(100% / 12); } + +@media screen { + .page { + margin-top: 20mm !important; + outline: 1px dotted grey; + } + + .header-field::before { + content: attr(data-label); + padding-right: 0.5em; + } + + [contenteditable=true]:empty::after{ + content: attr(data-placeholder); + color: rgb(200, 200, 200); + font-weight: normal; + } +} + +@media print { + body { + margin-bottom: 0 !important; + } + + .page { + page-break-after: always; + margin: 0 !important; + } + + figure.inksimulation div { + display: none; + } + + .ui { + display: none; + } + + #settings-ui { + display: none !important; + } + + #errors { + display: none !important; + } + + .header-field:not(:empty)::before { + content: attr(data-label); + padding-right: 0.5em; + } + + span.logo-instructions { + display: none; + } +} + +@page { + size: auto; /* auto is the initial value */ + margin: 0; +} diff --git a/print/templates/headline.html b/print/templates/headline.html index 7a7059b9..369c370e 100644 --- a/print/templates/headline.html +++ b/print/templates/headline.html @@ -2,8 +2,8 @@ - {{ _("Click to choose another logo") }}
diff --git a/print/templates/index.html b/print/templates/index.html index 0c9cedfd..c7fa5d74 100644 --- a/print/templates/index.html +++ b/print/templates/index.html @@ -5,6 +5,7 @@ + {% include 'ui.html' %} @@ -23,10 +24,7 @@
{% include 'operator_overview.html' %}
{# operator detailed view #} - {% for color_block_part in color_blocks | batch(12) %} - {% set outer_loop = loop %} -
{% include 'operator_detailedview.html' %}
- {% endfor %} + {% include 'operator_detailedview.html' %} diff --git a/print/templates/operator_detailedview.html b/print/templates/operator_detailedview.html index 0b948cb0..e1899db4 100644 --- a/print/templates/operator_detailedview.html +++ b/print/templates/operator_detailedview.html @@ -1,76 +1,76 @@ + + +
+

+ ## +

+

+ {{ svg_overview|safe }} +

+

+ {{ _('Unique Colors') }}: {{ job.num_colors }} + {{ _('Color Blocks') }}: {{ job.num_color_blocks }} +

+

+ {{ _('Design box size') }}: {{ "%0.1fmm X %0.1fmm" | format(*job.dimensions) }} + {{ _('Total thread used') }}: {{job.estimated_thread }} + {{ _('Total stitch count') }}: {{job.num_stitches }} +

+

+ {{ _('Total nr stops') }}: {{ job.num_stops }} + {{ _('Total nr trims') }}: {{ job.num_trims }} +

+

+ +

+
+ + + {% for color_block in color_blocks %} -
-
-
-

{# svg color #}

-

{# svg preview #}

-

{{ _('Color') }}

-

{{ _('Thread Consumption') }}

-

{{ _('Stops and Trims') }}

-

{{ _('Notes') }}

-
- {% if outer_loop.index == 1 %} -
-

- ## -

-

- {{ svg_overview|safe }} -

-

- {{ _('Unique Colors') }}: {{ job.num_colors }} - {{ _('Color Blocks') }}: {{ job.num_color_blocks }} -

-

- {{ _('Design box size') }}: {{ "%0.1fmm X %0.1fmm" | format(*job.dimensions) }} - {{ _('Total thread used') }}: {{job.estimated_thread }} - {{ _('Total stitch count') }}: {{job.num_stitches }} -

-

- {{ _('Total nr stops') }}: {{ job.num_stops }} - {{ _('Total nr trims') }}: {{ job.num_trims }} -

-

- -

-
- {% endif %} - {% for color_block in color_block_part %} -
-

- - - - #{{ loop.index + outer_loop.index0 * 12 }} - - -

-

- {{ color_block.svg_preview|safe }} -

-

- {{ color_block.color.name }} - {{ color_block.color.rgb }} - {{ color_block.color.manufacturer }} {{ "#" + color_block.color.number if color_block.color.number }} -

-

- {{ _('thread used') }}: - {{ _('# stitches') }}: {{ color_block.num_stitches }} -

-

- {{ _('# stops') }}: {{ color_block.num_stops }} - {{ _('# trims') }}: {{ color_block.num_trims }} -

-

- -

-
- {% endfor %} +
+

+ + + + #{{ loop.index }} + + +

+

+ {{ color_block.svg_preview|safe }} +

+

+ {{ color_block.color.name }} + {{ color_block.color.rgb }} + {{ color_block.color.manufacturer }} {{ "#" + color_block.color.number if color_block.color.number }} +

+

+ {{ _('thread used') }}: + {{ _('# stitches') }}: {{ color_block.num_stitches }} +

+

+ {{ _('# stops') }}: {{ color_block.num_stops }} + {{ _('# trims') }}: {{ color_block.num_trims }} +

+

+ +

-
+ {% endfor %} + - {% include 'footer.html' %} diff --git a/print/templates/operator_overview.html b/print/templates/operator_overview.html index 6fa46911..0c379316 100644 --- a/print/templates/operator_overview.html +++ b/print/templates/operator_overview.html @@ -27,16 +27,7 @@
{{ svg_overview|safe }} -
{{ _('Scale') }} %
-
- - - - -
+ {% include 'ui_svg_action_buttons.html' %}
{% include 'footer.html' %} diff --git a/print/templates/print_detail.html b/print/templates/print_detail.html index e73fe918..241ac88b 100644 --- a/print/templates/print_detail.html +++ b/print/templates/print_detail.html @@ -17,16 +17,7 @@
{{color_block.svg_preview|safe}} -
{{ _('Scale') }} %
-
- - - - -
+ {% include 'ui_svg_action_buttons.html' %}
{% include 'color_swatch.html' %} diff --git a/print/templates/print_overview.html b/print/templates/print_overview.html index b42ab7a9..ca8638ff 100644 --- a/print/templates/print_overview.html +++ b/print/templates/print_overview.html @@ -27,16 +27,7 @@
{{ svg_overview|safe }} -
{{ _('Scale') }} %
-
- - - - -
+ {% include 'ui_svg_action_buttons.html' %}
diff --git a/print/templates/ui.html b/print/templates/ui.html index 3b11f345..bc6c57b9 100644 --- a/print/templates/ui.html +++ b/print/templates/ui.html @@ -13,41 +13,51 @@

X

{{ _('Settings') }}

-
- {{ _('Page Setup') }} -
-

{{ _('Printing Size') }}: - + + + +

+
+
+
+ {{ _('Print Layouts') }} +

+

+

+

+

{{ _('Thumbnail size') }}: 15mm +

+
+ +
+
+
+ {{ _('Design') }} +

+

-
-
-
- {{ _('Print Layouts') }} -

-

-

-

-
- -
- -
- {{ _('Design') }} -

{{ _('Thread Palette') }}: - -

-
-
+ +
+
diff --git a/print/templates/operator_detailedview.html b/print/templates/operator_detailedview.html index e1899db4..e3c0d441 100644 --- a/print/templates/operator_detailedview.html +++ b/print/templates/operator_detailedview.html @@ -30,18 +30,16 @@ {{ _('Total stitch count') }}: {{job.num_stitches }}

- {{ _('Total nr stops') }}: {{ job.num_stops }} - {{ _('Total nr trims') }}: {{ job.num_trims }} + {{ _('Total stops') }}: {{ job.num_stops }} + {{ _('Total trims') }}: {{ job.num_trims }}

- {% for color_block in color_blocks %} -

@@ -64,13 +62,11 @@ {{ _('# stitches') }}: {{ color_block.num_stitches }}

- {{ _('# stops') }}: {{ color_block.num_stops }} - {{ _('# trims') }}: {{ color_block.num_trims }} + {{ _('trims') }}: {{ color_block.num_trims }} + {{ _('stop after?') }}: {{ _("yes") if color_block.stop_after else _("no") }}

{% endfor %} - - diff --git a/print/templates/operator_overview.html b/print/templates/operator_overview.html index 0c379316..048fd58e 100644 --- a/print/templates/operator_overview.html +++ b/print/templates/operator_overview.html @@ -5,8 +5,8 @@

{{ _('Unique Colors') }}:{{ job.num_colors }}

{{ _('Color Blocks') }}:{{ job.num_color_blocks }}

-

{{ _('Total nr stops') }}:{{ job.num_stops }}

-

{{ _('Total nr trims') }}:{{ job.num_trims }}

+

{{ _('Total stops') }}:{{ job.num_stops }}

+

{{ _('Total trims') }}:{{ job.num_trims }}

diff --git a/print/templates/print_overview.html b/print/templates/print_overview.html index ca8638ff..cfbb81d1 100644 --- a/print/templates/print_overview.html +++ b/print/templates/print_overview.html @@ -5,8 +5,8 @@

{{ _('Unique Colors') }}:{{ job.num_colors }}

{{ _('Color Blocks') }}:{{ job.num_color_blocks }}

-

{{ _('Total nr stops') }}:{{ job.num_stops }}

-

{{ _('Total nr trims') }}:{{ job.num_trims }}

+

{{ _('Total stops') }}:{{ job.num_stops }}

+

{{ _('Total trims') }}:{{ job.num_trims }}

-- cgit v1.3.1