diff --git a/.gitignore b/.gitignore index 5b6925eb4..16a1e66be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -.*.swp +*.swp +*.kate-swp *.pyc *.spec *.zip diff --git a/.travis.yml b/.travis.yml index 7f92b993e..bbcd22401 100644 --- a/.travis.yml +++ b/.travis.yml @@ -56,7 +56,7 @@ install: sudo apt-get install python-gi python-gi-cairo libgirepository1.0-dev # wxPython doen't publish linux wheels in pypi - wget https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-14.04/wxPython-4.0.0b2-cp27-cp27mu-linux_x86_64.whl + wget -q https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-14.04/wxPython-4.0.0b2-cp27-cp27mu-linux_x86_64.whl pip install wxPython-4.0.0b2-cp27-cp27mu-linux_x86_64.whl # We can't use the shapely wheel because it includes the geos diff --git a/Makefile b/Makefile index 43d6d9f16..a43792023 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -EXTENSIONS:=embroider embroider_params embroider_simulate embroider_update +EXTENSIONS:=embroider embroider_params embroider_simulate embroider_print # This gets the branch name or the name of the tag VERSION:=$(TRAVIS_BRANCH) @@ -8,7 +8,9 @@ ARCH:=$(shell uname -m) dist: distclean locales bin/build-dist $(EXTENSIONS) cp *.inx dist - mv locales dist/inkstitch/bin + mkdir -p dist/inkstitch/bin/locales + cp -a locales/* dist/inkstitch/bin/locales + cp -a print dist/inkstitch/bin/ if [ "$$BUILD" = "windows" ]; then \ cd dist; zip -r ../inkstitch-$(VERSION)-win32.zip *; \ else \ @@ -20,7 +22,7 @@ distclean: messages.po: rm -f messages.po - xgettext --no-location --add-comments embroider*.py inkstitch/__init__.py + pybabel extract -o messages.po -F babel.conf --no-location --add-comments l10n --add-comments L10n --add-comments L10N -s . .PHONY: messages.po .PHONY: locales diff --git a/babel.conf b/babel.conf new file mode 100644 index 000000000..f7df6cb39 --- /dev/null +++ b/babel.conf @@ -0,0 +1,7 @@ +[ignore: libembroidery.py] +[ignore: embroidermodder/**] + +[python: **.py] + +[jinja2: print/templates/*.html] + diff --git a/bin/build-dist b/bin/build-dist index 8fa4b59f9..c8a2da2a8 100755 --- a/bin/build-dist +++ b/bin/build-dist @@ -29,6 +29,9 @@ fi # This lets pyinstaller see inkex.py, etc. pyinstaller_args+="-p inkscape-0.92.2/share/extensions " +# output useful debugging info that helps us trace library dependency issues +pyinstaller_args+="--log-level DEBUG " + mkdir -p dist/inkstitch/bin for extension in "$@"; do if [ "$BUILD" = "windows" ]; then diff --git a/embroider.py b/embroider.py index 804782a94..8c5d135b1 100644 --- a/embroider.py +++ b/embroider.py @@ -14,1078 +14,18 @@ import sys import traceback sys.path.append("/usr/share/inkscape/extensions") import os -import subprocess -from copy import deepcopy -import time -from itertools import chain, izip, groupby -from collections import deque + import inkex -import simplepath -import simplestyle -import simpletransform -from bezmisc import bezierlength, beziertatlength, bezierpointatt -from cspsubdiv import cspsubdiv -import cubicsuperpath -import math -import lxml.etree as etree -import shapely.geometry as shgeo -import shapely.affinity as affinity -import shapely.ops -import networkx -from pprint import pformat - import inkstitch -from inkstitch import _, cache, dbg, param, EmbroideryElement, get_nodes, SVG_POLYLINE_TAG, SVG_GROUP_TAG, PIXELS_PER_MM, get_viewbox_transform -from inkstitch.stitches import running_stitch, auto_fill, legacy_fill -from inkstitch.utils import cut_path +from inkstitch import _, PIXELS_PER_MM +from inkstitch.extensions import InkstitchExtension +from inkstitch.stitch_plan import patches_to_stitch_plan +from inkstitch.svg import render_stitch_plan -class Fill(EmbroideryElement): - element_name = _("Fill") +class Embroider(InkstitchExtension): def __init__(self, *args, **kwargs): - super(Fill, self).__init__(*args, **kwargs) - - @property - @param('auto_fill', _('Manually routed fill stitching'), type='toggle', inverse=True, default=True) - def auto_fill(self): - return self.get_boolean_param('auto_fill', True) - - @property - @param('angle', _('Angle of lines of stitches'), unit='deg', type='float', default=0) - @cache - def angle(self): - return math.radians(self.get_float_param('angle', 0)) - - @property - def color(self): - return self.get_style("fill") - - @property - @param('flip', _('Flip fill (start right-to-left)'), type='boolean', default=False) - def flip(self): - return self.get_boolean_param("flip", False) - - @property - @param('row_spacing_mm', _('Spacing between rows'), unit='mm', type='float', default=0.25) - def row_spacing(self): - return max(self.get_float_param("row_spacing_mm", 0.25), 0.01) - - @property - def end_row_spacing(self): - return self.get_float_param("end_row_spacing_mm") - - @property - @param('max_stitch_length_mm', _('Maximum fill stitch length'), unit='mm', type='float', default=3.0) - def max_stitch_length(self): - return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.01) - - @property - @param('staggers', _('Stagger rows this many times before repeating'), type='int', default=4) - def staggers(self): - return self.get_int_param("staggers", 4) - - @property - @cache - def paths(self): - return self.flatten(self.parse_path()) - - @property - @cache - def shape(self): - poly_ary = [] - for sub_path in self.paths: - point_ary = [] - last_pt = None - for pt in sub_path: - if (last_pt is not None): - vp = (pt[0] - last_pt[0], pt[1] - last_pt[1]) - dp = math.sqrt(math.pow(vp[0], 2.0) + math.pow(vp[1], 2.0)) - # dbg.write("dp %s\n" % dp) - if (dp > 0.01): - # I think too-close points confuse shapely. - point_ary.append(pt) - last_pt = pt - else: - last_pt = pt - if point_ary: - poly_ary.append(point_ary) - - # shapely's idea of "holes" are to subtract everything in the second set - # from the first. So let's at least make sure the "first" thing is the - # biggest path. - # TODO: actually figure out which things are holes and which are shells - poly_ary.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) - - polygon = shgeo.MultiPolygon([(poly_ary[0], poly_ary[1:])]) - # print >> sys.stderr, "polygon valid:", polygon.is_valid - return polygon - - def to_patches(self, last_patch): - stitch_lists = legacy_fill(self.shape, - self.angle, - self.row_spacing, - self.end_row_spacing, - self.max_stitch_length, - self.flip, - self.staggers) - return [Patch(stitches=stitch_list, color=self.color) for stitch_list in stitch_lists] - - rows_of_segments = fill.intersect_region_with_grating(self.shape, self.angle, self.row_spacing, self.end_row_spacing, self.flip) - groups_of_segments = fill.pull_runs(rows_of_segments) - - return [fill.section_to_patch(group) for group in groups_of_segments] - - -class AutoFill(Fill): - element_name = _("Auto-Fill") - - @property - @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True) - def auto_fill(self): - return self.get_boolean_param('auto_fill', True) - - @property - @cache - def outline(self): - return self.shape.boundary[0] - - @property - @cache - def outline_length(self): - return self.outline.length - - @property - def flip(self): - return False - - @property - @param('running_stitch_length_mm', _('Running stitch length (traversal between sections)'), unit='mm', type='float', default=1.5) - def running_stitch_length(self): - return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) - - @property - @param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=False) - def fill_underlay(self): - return self.get_boolean_param("fill_underlay", default=False) - - @property - @param('fill_underlay_angle', _('Fill angle (default: fill angle + 90 deg)'), unit='deg', group=_('AutoFill Underlay'), type='float') - @cache - def fill_underlay_angle(self): - underlay_angle = self.get_float_param("fill_underlay_angle") - - if underlay_angle: - return math.radians(underlay_angle) - else: - return self.angle + math.pi / 2.0 - - @property - @param('fill_underlay_row_spacing_mm', _('Row spacing (default: 3x fill row spacing)'), unit='mm', group=_('AutoFill Underlay'), type='float') - @cache - def fill_underlay_row_spacing(self): - return self.get_float_param("fill_underlay_row_spacing_mm") or self.row_spacing * 3 - - @property - @param('fill_underlay_max_stitch_length_mm', _('Max stitch length'), unit='mm', group=_('AutoFill Underlay'), type='float') - @cache - def fill_underlay_max_stitch_length(self): - 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) - 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) - if not isinstance(shape, shgeo.MultiPolygon): - shape = shgeo.MultiPolygon([shape]) - return shape - else: - return self.shape - - def to_patches(self, last_patch): - stitches = [] - - if last_patch is None: - starting_point = None - else: - starting_point = last_patch.stitches[-1] - - if self.fill_underlay: - stitches.extend(auto_fill(self.underlay_shape, - self.fill_underlay_angle, - self.fill_underlay_row_spacing, - self.fill_underlay_row_spacing, - self.fill_underlay_max_stitch_length, - self.running_stitch_length, - self.staggers, - starting_point)) - starting_point = stitches[-1] - - stitches.extend(auto_fill(self.shape, - self.angle, - self.row_spacing, - self.end_row_spacing, - self.max_stitch_length, - self.running_stitch_length, - self.staggers, - starting_point)) - - return [Patch(stitches=stitches, color=self.color)] - - -class Stroke(EmbroideryElement): - element_name = "Stroke" - - @property - @param('satin_column', _('Satin stitch along paths'), type='toggle', inverse=True) - def satin_column(self): - return self.get_boolean_param("satin_column") - - @property - def color(self): - return self.get_style("stroke") - - @property - @cache - def width(self): - stroke_width = self.get_style("stroke-width") - - if stroke_width.endswith("px"): - stroke_width = stroke_width[:-2] - - return float(stroke_width) - - @property - def dashed(self): - return self.get_style("stroke-dasharray") is not None - - @property - @param('running_stitch_length_mm', _('Running stitch length'), unit='mm', type='float', default=1.5) - def running_stitch_length(self): - return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) - - @property - @param('zigzag_spacing_mm', _('Zig-zag spacing (peak-to-peak)'), unit='mm', type='float', default=0.4) - @cache - def zigzag_spacing(self): - return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01) - - @property - @param('repeats', _('Repeats'), type='int', default="1") - def repeats(self): - return self.get_int_param("repeats", 1) - - @property - def paths(self): - return self.flatten(self.parse_path()) - - def is_running_stitch(self): - # stroke width <= 0.5 pixels is deprecated in favor of dashed lines - return self.dashed or self.width <= 0.5 - - def stroke_points(self, emb_point_list, zigzag_spacing, stroke_width): - patch = Patch(color=self.color) - p0 = emb_point_list[0] - rho = 0.0 - side = 1 - last_segment_direction = None - - 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 segi in order: - p1 = emb_point_list[segi] - - # how far we have to go along segment - seg_len = (p1 - p0).length() - if (seg_len == 0): - continue - - # vector pointing along segment - along = (p1 - p0).unit() - - # vector pointing to edge of stroke width - perp = along.rotate_left() * (stroke_width * 0.5) - - 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) - - # 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 - - p0 = p1 - last_segment_direction = along - rho -= seg_len - - if (p0 - patch.stitches[-1]).length() > 0.1: - patch.add_stitch(p0) - - return patch - - def to_patches(self, last_patch): - patches = [] - - for path in self.paths: - path = [inkstitch.Point(x, y) for x, y in path] - if self.is_running_stitch(): - patch = self.stroke_points(path, self.running_stitch_length, stroke_width=0.0) - else: - patch = self.stroke_points(path, self.zigzag_spacing / 2.0, stroke_width=self.width) - - patches.append(patch) - - return patches - - -class SatinColumn(EmbroideryElement): - element_name = _("Satin Column") - - def __init__(self, *args, **kwargs): - super(SatinColumn, self).__init__(*args, **kwargs) - - @property - @param('satin_column', _('Custom satin column'), type='toggle') - def satin_column(self): - return self.get_boolean_param("satin_column") - - @property - def color(self): - return self.get_style("stroke") - - @property - @param('zigzag_spacing_mm', _('Zig-zag spacing (peak-to-peak)'), unit='mm', type='float', default=0.4) - def zigzag_spacing(self): - # peak-to-peak distance between zigzags - return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01) - - @property - @param('pull_compensation_mm', _('Pull compensation'), unit='mm', type='float') - def pull_compensation(self): - # In satin stitch, the stitches have a tendency to pull together and - # narrow the entire column. We can compensate for this by stitching - # wider than we desire the column to end up. - return self.get_float_param("pull_compensation_mm", 0) - - @property - @param('contour_underlay', _('Contour underlay'), type='toggle', group=_('Contour Underlay')) - def contour_underlay(self): - # "Contour underlay" is stitching just inside the rectangular shape - # of the satin column; that is, up one side and down the other. - return self.get_boolean_param("contour_underlay") - - @property - @param('contour_underlay_stitch_length_mm', _('Stitch length'), unit='mm', group=_('Contour Underlay'), type='float', default=1.5) - def contour_underlay_stitch_length(self): - return max(self.get_float_param("contour_underlay_stitch_length_mm", 1.5), 0.01) - - @property - @param('contour_underlay_inset_mm', _('Contour underlay inset amount'), unit='mm', group=_('Contour Underlay'), type='float', default=0.4) - def contour_underlay_inset(self): - # how far inside the edge of the column to stitch the underlay - return self.get_float_param("contour_underlay_inset_mm", 0.4) - - @property - @param('center_walk_underlay', _('Center-walk underlay'), type='toggle', group=_('Center-Walk Underlay')) - def center_walk_underlay(self): - # "Center walk underlay" is stitching down and back in the centerline - # between the two sides of the satin column. - return self.get_boolean_param("center_walk_underlay") - - @property - @param('center_walk_underlay_stitch_length_mm', _('Stitch length'), unit='mm', group=_('Center-Walk Underlay'), type='float', default=1.5) - def center_walk_underlay_stitch_length(self): - return max(self.get_float_param("center_walk_underlay_stitch_length_mm", 1.5), 0.01) - - @property - @param('zigzag_underlay', _('Zig-zag underlay'), type='toggle', group=_('Zig-zag Underlay')) - def zigzag_underlay(self): - return self.get_boolean_param("zigzag_underlay") - - @property - @param('zigzag_underlay_spacing_mm', _('Zig-Zag spacing (peak-to-peak)'), unit='mm', group=_('Zig-zag Underlay'), type='float', default=3) - def zigzag_underlay_spacing(self): - return max(self.get_float_param("zigzag_underlay_spacing_mm", 3), 0.01) - - @property - @param('zigzag_underlay_inset_mm', _('Inset amount (default: half of contour underlay inset)'), unit='mm', group=_('Zig-zag Underlay'), type='float') - def zigzag_underlay_inset(self): - # how far in from the edge of the satin the points in the zigzags - # should be - - # Default to half of the contour underlay inset. That is, if we're - # doing both contour underlay and zigzag underlay, make sure the - # points of the zigzag fall outside the contour underlay but inside - # the edges of the satin column. - return self.get_float_param("zigzag_underlay_inset_mm") or self.contour_underlay_inset / 2.0 - - @property - @cache - def csp(self): - return self.parse_path() - - @property - @cache - def flattened_beziers(self): - if len(self.csp) == 2: - return self.simple_flatten_beziers() - else: - return self.flatten_beziers_with_rungs() - - - def flatten_beziers_with_rungs(self): - input_paths = [self.flatten([path]) for path in self.csp] - input_paths = [shgeo.LineString(path[0]) for path in input_paths] - - paths = input_paths[:] - paths.sort(key=lambda path: path.length, reverse=True) - - # Imagine a satin column as a curvy ladder. - # The two long paths are the "rails" of the ladder. The remainder are - # the "rungs". - rails = paths[:2] - rungs = shgeo.MultiLineString(paths[2:]) - - # The rails should stay in the order they were in the original CSP. - # (this lets the user control where the satin starts and ends) - rails.sort(key=lambda rail: input_paths.index(rail)) - - result = [] - - for rail in rails: - if not rail.is_simple: - self.fatal(_("One or more rails crosses itself, and this is not allowed. Please split into multiple satin columns.")) - - # handle null intersections here? - linestrings = shapely.ops.split(rail, rungs) - - print >> dbg, "rails and rungs", [str(rail) for rail in rails], [str(rung) for rung in rungs] - if len(linestrings.geoms) < len(rungs.geoms) + 1: - self.fatal(_("satin column: One or more of the rungs doesn't intersect both rails.") + " " + _("Each rail should intersect both rungs once.")) - elif len(linestrings.geoms) > len(rungs.geoms) + 1: - self.fatal(_("satin column: One or more of the rungs intersects the rails more than once.") + " " + _("Each rail should intersect both rungs once.")) - - paths = [[inkstitch.Point(*coord) for coord in ls.coords] for ls in linestrings.geoms] - result.append(paths) - - return zip(*result) - - - def simple_flatten_beziers(self): - # Given a pair of paths made up of bezier segments, flatten - # each individual bezier segment into line segments that approximate - # the curves. Retain the divisions between beziers -- we'll use those - # later. - - paths = [] - - for path in self.csp: - # See the documentation in the parent class for parse_path() for a - # description of the format of the CSP. Each bezier is constructed - # using two neighboring 3-tuples in the list. - - flattened_path = [] - - # iterate over pairs of 3-tuples - for prev, current in zip(path[:-1], path[1:]): - flattened_segment = self.flatten([[prev, current]]) - flattened_segment = [inkstitch.Point(x, y) for x, y in flattened_segment[0]] - flattened_path.append(flattened_segment) - - paths.append(flattened_path) - - return zip(*paths) - - def validate_satin_column(self): - # The node should have exactly two paths with no fill. Each - # path should have the same number of points, meaning that they - # will both be made up of the same number of bezier curves. - - node_id = self.node.get("id") - - if self.get_style("fill") is not None: - self.fatal(_("satin column: object %s has a fill (but should not)") % node_id) - - if len(self.csp) == 2: - if len(self.csp[0]) != len(self.csp[1]): - self.fatal(_("satin column: object %(id)s has two paths with an unequal number of points (%(length1)d and %(length2)d)") % \ - dict(id=node_id, length1=len(self.csp[0]), length2=len(self.csp[1]))) - - def offset_points(self, pos1, pos2, offset_px): - # Expand or contract two points about their midpoint. This is - # useful for pull compensation and insetting underlay. - - distance = (pos1 - pos2).length() - - if distance < 0.0001: - # if they're the same point, we don't know which direction - # to offset in, so we have to just return the points - return pos1, pos2 - - # don't contract beyond the midpoint, or we'll start expanding - if offset_px < -distance / 2.0: - offset_px = -distance / 2.0 - - pos1 = pos1 + (pos1 - pos2).unit() * offset_px - pos2 = pos2 + (pos2 - pos1).unit() * offset_px - - return pos1, pos2 - - def walk(self, path, start_pos, start_index, distance): - # Move pixels along , which is a sequence of line - # segments defined by points. - - # is the index of the line segment in that - # we're currently on. is where along that line - # segment we are. Return a new position and index. - - # print >> dbg, "walk", start_pos, start_index, distance - - pos = start_pos - index = start_index - last_index = len(path) - 1 - distance_remaining = distance - - while True: - if index >= last_index: - return pos, index - - segment_end = path[index + 1] - segment = segment_end - pos - segment_length = segment.length() - - if segment_length > distance_remaining: - # our walk ends partway along this segment - return pos + segment.unit() * distance_remaining, index - else: - # our walk goes past the end of this segment, so advance - # one point - index += 1 - distance_remaining -= segment_length - pos = segment_end - - def walk_paths(self, spacing, offset): - # Take a bezier segment from each path in turn, and plot out an - # equal number of points on each bezier. Return the points plotted. - # The points will be contracted or expanded by offset using - # offset_points(). - - points = [[], []] - - def add_pair(pos1, pos2): - pos1, pos2 = self.offset_points(pos1, pos2, offset) - points[0].append(pos1) - points[1].append(pos2) - - # We may not be able to fit an even number of zigzags in each pair of - # beziers. We'll store the remaining bit of the beziers after handling - # each section. - remainder_path1 = [] - remainder_path2 = [] - - for segment1, segment2 in self.flattened_beziers: - subpath1 = remainder_path1 + segment1 - subpath2 = remainder_path2 + segment2 - - len1 = shgeo.LineString(subpath1).length - len2 = shgeo.LineString(subpath2).length - - # Base the number of stitches in each section on the _longest_ of - # the two beziers. Otherwise, things could get too sparse when one - # side is significantly longer (e.g. when going around a corner). - # The risk here is that we poke a hole in the fabric if we try to - # cram too many stitches on the short bezier. The user will need - # to avoid this through careful construction of paths. - # - # TODO: some commercial machine embroidery software compensates by - # pulling in some of the "inner" stitches toward the center a bit. - - # note, this rounds down using integer-division - num_points = max(len1, len2) / spacing - - spacing1 = len1 / num_points - spacing2 = len2 / num_points - - pos1 = subpath1[0] - index1 = 0 - - pos2 = subpath2[0] - index2 = 0 - - for i in xrange(int(num_points)): - add_pair(pos1, pos2) - - pos1, index1 = self.walk(subpath1, pos1, index1, spacing1) - pos2, index2 = self.walk(subpath2, pos2, index2, spacing2) - - if index1 < len(subpath1) - 1: - remainder_path1 = [pos1] + subpath1[index1 + 1:] - else: - remainder_path1 = [] - - if index2 < len(subpath2) - 1: - remainder_path2 = [pos2] + subpath2[index2 + 1:] - else: - remainder_path2 = [] - - # We're off by one in the algorithm above, so we need one more - # pair of points. We also want to add points at the very end to - # make sure we match the vectors on screen as best as possible. - # Try to avoid doing both if they're going to stack up too - # closely. - - end1 = remainder_path1[-1] - end2 = remainder_path2[-1] - - if (end1 - pos1).length() > 0.3 * spacing: - add_pair(pos1, pos2) - - add_pair(end1, end2) - - return points - - def do_contour_underlay(self): - # "contour walk" underlay: do stitches up one side and down the - # other. - forward, back = self.walk_paths(self.contour_underlay_stitch_length, - -self.contour_underlay_inset) - return Patch(color=self.color, stitches=(forward + list(reversed(back)))) - - def do_center_walk(self): - # Center walk underlay is just a running stitch down and back on the - # center line between the bezier curves. - - # Do it like contour underlay, but inset all the way to the center. - forward, back = self.walk_paths(self.center_walk_underlay_stitch_length, - -100000) - return Patch(color=self.color, stitches=(forward + list(reversed(back)))) - - def do_zigzag_underlay(self): - # zigzag underlay, usually done at a much lower density than the - # satin itself. It looks like this: - # - # \/\/\/\/\/\/\/\/\/\/| - # /\/\/\/\/\/\/\/\/\/\| - # - # In combination with the "contour walk" underlay, this is the - # "German underlay" described here: - # http://www.mrxstitch.com/underlay-what-lies-beneath-machine-embroidery/ - - patch = Patch(color=self.color) - - sides = self.walk_paths(self.zigzag_underlay_spacing / 2.0, - -self.zigzag_underlay_inset) - - # This organizes the points in each side in the order that they'll be - # visited. - sides = [sides[0][::2] + list(reversed(sides[0][1::2])), - sides[1][1::2] + list(reversed(sides[1][::2]))] - - # This fancy bit of iterable magic just repeatedly takes a point - # from each side in turn. - for point in chain.from_iterable(izip(*sides)): - patch.add_stitch(point) - - return patch - - def do_satin(self): - # satin: do a zigzag pattern, alternating between the paths. The - # zigzag looks like this to make the satin stitches look perpendicular - # to the column: - # - # /|/|/|/|/|/|/|/| - - # print >> dbg, "satin", self.zigzag_spacing, self.pull_compensation - - patch = Patch(color=self.color) - - sides = self.walk_paths(self.zigzag_spacing, self.pull_compensation) - - # Like in zigzag_underlay(): take a point from each side in turn. - for point in chain.from_iterable(izip(*sides)): - patch.add_stitch(point) - - return patch - - def to_patches(self, last_patch): - # Stitch a variable-width satin column, zig-zagging between two paths. - - # The algorithm will draw zigzags between each consecutive pair of - # beziers. The boundary points between beziers serve as "checkpoints", - # allowing the user to control how the zigzags flow around corners. - - # First, verify that we have valid paths. - self.validate_satin_column() - - patches = [] - - if self.center_walk_underlay: - patches.append(self.do_center_walk()) - - if self.contour_underlay: - patches.append(self.do_contour_underlay()) - - if self.zigzag_underlay: - # zigzag underlay comes after contour walk underlay, so that the - # zigzags sit on the contour walk underlay like rail ties on rails. - patches.append(self.do_zigzag_underlay()) - - patches.append(self.do_satin()) - - return patches - - -class Polyline(EmbroideryElement): - # Handle a element, which is treated as a set of points to - # stitch exactly. - # - # elements are pretty rare in SVG, from what I can tell. - # Anything you can do with a can also be done with a

, and - # much more. - # - # Notably, EmbroiderModder2 uses elements when converting from - # common machine embroidery file formats to SVG. Handling those here lets - # users use File -> Import to pull in existing designs they may have - # obtained, for example purchased fonts. - - @property - def points(self): - # example: "1,2 0,0 1.5,3 4,2" - - points = self.node.get('points') - points = points.split(" ") - points = [[float(coord) for coord in point.split(",")] for point in points] - - return points - - @property - def path(self): - # A polyline is a series of connected line segments described by their - # points. In order to make use of the existing logic for incorporating - # svg transforms that is in our superclass, we'll convert the polyline - # to a degenerate cubic superpath in which the bezier handles are on - # the segment endpoints. - - path = [[[point[:], point[:], point[:]] for point in self.points]] - - return path - - @property - @cache - def csp(self): - csp = self.parse_path() - - return csp - - @property - def color(self): - # EmbroiderModder2 likes to use the `stroke` property directly instead - # of CSS. - return self.get_style("stroke") or self.node.get("stroke") - - @property - def stitches(self): - # For a , we'll stitch the points exactly as they exist in - # the SVG, with no stitch spacing interpolation, flattening, etc. - - # See the comments in the parent class's parse_path method for a - # description of the CSP data structure. - - stitches = [point for handle_before, point, handle_after in self.csp[0]] - - return stitches - - def to_patches(self, last_patch): - patch = Patch(color=self.color) - - for stitch in self.stitches: - patch.add_stitch(inkstitch.Point(*stitch)) - - return [patch] - -def detect_classes(node): - if node.tag == SVG_POLYLINE_TAG: - return [Polyline] - else: - element = EmbroideryElement(node) - - if element.get_boolean_param("satin_column"): - return [SatinColumn] - else: - classes = [] - - if element.get_style("fill"): - if element.get_boolean_param("auto_fill", True): - classes.append(AutoFill) - else: - classes.append(Fill) - - if element.get_style("stroke"): - classes.append(Stroke) - - if element.get_boolean_param("stroke_first", False): - classes.reverse() - - return classes - - -class Patch: - def __init__(self, color=None, stitches=None, trim_after=False, stop_after=False): - self.color = color - self.stitches = stitches or [] - self.trim_after = trim_after - self.stop_after = stop_after - - def __add__(self, other): - if isinstance(other, Patch): - return Patch(self.color, self.stitches + other.stitches) - else: - raise TypeError("Patch can only be added to another Patch") - - def add_stitch(self, stitch): - self.stitches.append(stitch) - - def reverse(self): - return Patch(self.color, self.stitches[::-1]) - - -def process_stop_after(stitches): - # The user wants the machine to pause after this patch. This can - # be useful for applique and similar on multi-needle machines that - # normally would not stop between colors. - # - # On such machines, the user assigns needles to the colors in the - # design before starting stitching. C01, C02, etc are normal - # needles, but C00 is special. For a block of stitches assigned - # to C00, the machine will continue sewing with the last color it - # had and pause after it completes the C00 block. - # - # That means we need to introduce an artificial color change - # shortly before the current stitch so that the user can set that - # to C00. We'll go back 3 stitches and do that: - - if len(stitches) >= 3: - stitches[-3].stop = True - - # and also add a color change on this stitch, completing the C00 - # block: - - stitches[-1].stop = True - - # reference for the above: https://github.com/lexelby/inkstitch/pull/29#issuecomment-359175447 - - -def process_trim(stitches, next_stitch): - # DST (and maybe other formats?) has no actual TRIM instruction. - # Instead, 3 sequential JUMPs cause the machine to trim the thread. - # - # To support both DST and other formats, we'll add a TRIM and two - # JUMPs. The TRIM will be converted to a JUMP by libembroidery - # if saving to DST, resulting in the 3-jump sequence. - - delta = next_stitch - stitches[-1] - delta = delta * (1/4.0) - - pos = stitches[-1] - - for i in xrange(3): - pos += delta - stitches.append(inkstitch.Stitch(pos.x, pos.y, stitches[-1].color, jump=True)) - - # first one should be TRIM instead of JUMP - stitches[-3].jump = False - stitches[-3].trim = True - - -def add_tie(stitches, tie_path): - color = tie_path[0].color - - tie_path = cut_path(tie_path, 0.6) - tie_stitches = running_stitch(tie_path, 0.3) - tie_stitches = [inkstitch.Stitch(*stitch, color=color) for stitch in tie_stitches] - - stitches.extend(tie_stitches[1:]) - stitches.extend(list(reversed(tie_stitches))[1:]) - - -def add_tie_off(stitches): - if not stitches: - return - - add_tie(stitches, list(reversed(stitches))) - - -def add_tie_in(stitches, upcoming_stitches): - if not upcoming_stitches: - return - - add_tie(stitches, upcoming_stitches) - - -def add_ties(original_stitches): - """Add tie-off before and after trims, jumps, and color changes.""" - - # we're going to copy most stitches over, adding tie in/off as needed - stitches = [] - - need_tie_in = True - - for i, stitch in enumerate(original_stitches): - is_special = stitch.trim or stitch.jump or stitch.stop - - if is_special and not need_tie_in: - add_tie_off(stitches) - stitches.append(stitch) - need_tie_in = True - elif need_tie_in and not is_special: - stitches.append(stitch) - add_tie_in(stitches, original_stitches[i:]) - need_tie_in = False - else: - stitches.append(stitch) - - # add tie-off at the end if we ended on a normal stitch - if not is_special: - add_tie_off(stitches) - - # overwrite the stitch plan with our new one that contains ties - original_stitches[:] = stitches - - -def patches_to_stitches(patch_list, collapse_len_px=3.0): - stitches = [] - - last_stitch = None - last_color = None - need_trim = False - for patch in patch_list: - if not patch.stitches: - continue - - jump_stitch = True - for stitch in patch.stitches: - if last_stitch and last_color == patch.color: - l = (stitch - last_stitch).length() - if l <= 0.1: - # filter out duplicate successive stitches - jump_stitch = False - continue - - if jump_stitch: - # consider collapsing jump stitch, if it is pretty short - if l < collapse_len_px: - # dbg.write("... collapsed\n") - jump_stitch = False - - if stitches and last_color and last_color != patch.color: - # add a color change - stitches.append(inkstitch.Stitch(last_stitch.x, last_stitch.y, last_color, stop=True)) - - if need_trim: - process_trim(stitches, stitch) - need_trim = False - - if jump_stitch: - stitches.append(inkstitch.Stitch(stitch.x, stitch.y, patch.color, jump=True)) - - stitches.append(inkstitch.Stitch(stitch.x, stitch.y, patch.color, jump=False)) - - jump_stitch = False - last_stitch = stitch - last_color = patch.color - - if patch.trim_after: - need_trim = True - - if patch.stop_after: - process_stop_after(stitches) - - add_ties(stitches) - - return stitches - -def stitches_to_polylines(stitches): - polylines = [] - last_color = None - last_stitch = None - trimming = False - - for stitch in stitches: - if stitch.color != last_color or stitch.trim: - trimming = True - polylines.append([stitch.color, []]) - - if trimming and (stitch.jump or stitch.trim): - continue - - trimming = False - - polylines[-1][1].append(stitch.as_tuple()) - - last_color = stitch.color - last_stitch = stitch - - return polylines - -def emit_inkscape(parent, stitches): - transform = get_viewbox_transform(parent.getroottree().getroot()) - - # we need to correct for the viewbox - transform = simpletransform.invertTransform(transform) - transform = simpletransform.formatTransform(transform) - - for color, polyline in stitches_to_polylines(stitches): - # dbg.write('polyline: %s %s\n' % (color, repr(polyline))) - inkex.etree.SubElement(parent, - inkex.addNS('polyline', 'svg'), - {'style': simplestyle.formatStyle( - {'stroke': color if color is not None else '#000000', - 'stroke-width': "0.4", - 'fill': 'none'}), - 'points': " ".join(",".join(str(coord) for coord in point) for point in polyline), - 'transform': transform - }) - -def get_elements(effect): - elements = [] - nodes = get_nodes(effect) - - for node in nodes: - classes = detect_classes(node) - elements.extend(cls(node) for cls in classes) - - return elements - - -def elements_to_patches(elements): - patches = [] - for element in elements: - if patches: - last_patch = patches[-1] - else: - last_patch = None - - patches.extend(element.embroider(last_patch)) - - return patches - -class Embroider(inkex.Effect): - - def __init__(self, *args, **kwargs): - inkex.Effect.__init__(self) + InkstitchExtension.__init__(self) self.OptionParser.add_option("-c", "--collapse_len_mm", action="store", type="float", dest="collapse_length_mm", default=3.0, @@ -1147,43 +87,18 @@ class Embroider(inkex.Effect): return output_path - def hide_layers(self): - for g in self.document.getroot().findall(SVG_GROUP_TAG): - if g.get(inkex.addNS("groupmode", "inkscape")) == "layer": - g.set("style", "display:none") - def effect(self): - # Printing anything other than a valid SVG on stdout blows inkscape up. - old_stdout = sys.stdout - sys.stdout = sys.stderr - - self.patch_list = [] - - self.elements = get_elements(self) - - if not self.elements: - if self.selected: - 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.")) + if not self.get_elements(): return if self.options.hide_layers: - self.hide_layers() + self.hide_all_layers() - patches = elements_to_patches(self.elements) - stitches = patches_to_stitches(patches, self.options.collapse_length_mm * PIXELS_PER_MM) - inkstitch.write_embroidery_file(self.get_output_path(), stitches, self.document.getroot()) + patches = self.elements_to_patches(self.elements) + stitch_plan = patches_to_stitch_plan(patches, self.options.collapse_length_mm * PIXELS_PER_MM) + inkstitch.write_embroidery_file(self.get_output_path(), stitch_plan, self.document.getroot()) + render_stitch_plan(self.document.getroot(), stitch_plan) - new_layer = inkex.etree.SubElement(self.document.getroot(), SVG_GROUP_TAG, {}) - new_layer.set('id', self.uniqueId("embroidery")) - new_layer.set(inkex.addNS('label', 'inkscape'), _('Embroidery')) - new_layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer') - - emit_inkscape(new_layer, stitches) - - sys.stdout = old_stdout if __name__ == '__main__': sys.setrecursionlimit(100000) @@ -1192,8 +107,6 @@ if __name__ == '__main__': try: e.affect() except KeyboardInterrupt: - print >> dbg, "interrupted!" - - print >> dbg, traceback.format_exc() - - dbg.flush() + # for use at the command prompt for debugging + print >> sys.stderr, "interrupted!" + print >> sys.stderr, traceback.format_exc() diff --git a/embroider_params.py b/embroider_params.py index 73c6f479c..607dbb2a1 100644 --- a/embroider_params.py +++ b/embroider_params.py @@ -8,16 +8,17 @@ import traceback import time from threading import Thread, Event from copy import copy -from cStringIO import StringIO import wx from wx.lib.scrolledpanel import ScrolledPanel from collections import defaultdict -import inkex import inkstitch -from inkstitch import _, Param, EmbroideryElement, get_nodes -from embroider import Fill, AutoFill, Stroke, SatinColumn from functools import partial from itertools import groupby +from inkstitch import _ +from inkstitch.extensions import InkstitchExtension +from inkstitch.stitch_plan import patches_to_stitch_plan +from inkstitch.elements import EmbroideryElement, Fill, AutoFill, Stroke, SatinColumn +from inkstitch.utils import save_stderr, restore_stderr from embroider_simulate import EmbroiderySimulator @@ -412,9 +413,10 @@ class SettingsFrame(wx.Frame): wx.CallAfter(self.refresh_simulator, patches) def refresh_simulator(self, patches): + stitch_plan = patches_to_stitch_plan(patches) if self.simulate_window: self.simulate_window.stop() - self.simulate_window.load(patches=patches) + self.simulate_window.load(stitch_plan=stitch_plan) else: my_rect = self.GetRect() simulator_pos = my_rect.GetTopRight() @@ -428,7 +430,7 @@ class SettingsFrame(wx.Frame): self.simulate_window = EmbroiderySimulator(None, -1, _("Preview"), simulator_pos, size=(300, 300), - patches=patches, + stitch_plan=stitch_plan, on_close=self.simulate_window_closed, target_duration=5, max_width=max_width, @@ -631,10 +633,10 @@ class SettingsFrame(wx.Frame): self.Layout() # end wxGlade -class EmbroiderParams(inkex.Effect): +class EmbroiderParams(InkstitchExtension): def __init__(self, *args, **kwargs): self.cancelled = False - inkex.Effect.__init__(self, *args, **kwargs) + InkstitchExtension.__init__(self, *args, **kwargs) def embroidery_classes(self, node): element = EmbroideryElement(node) @@ -653,7 +655,7 @@ class EmbroiderParams(inkex.Effect): return classes def get_nodes_by_class(self): - nodes = get_nodes(self) + nodes = self.get_nodes() nodes_by_class = defaultdict(list) for z, node in enumerate(nodes): @@ -755,21 +757,6 @@ class EmbroiderParams(inkex.Effect): sys.exit(0) -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()) - os.dup2(null.fileno(), 2) - sys.stderr_backup = sys.stderr - sys.stderr = StringIO() - - -def restore_stderr(): - os.dup2(sys.stderr_dup, 2) - sys.stderr_backup.write(sys.stderr.getvalue()) - sys.stderr = sys.stderr_backup - - # end of class MyFrame if __name__ == "__main__": save_stderr() diff --git a/embroider_update.inx b/embroider_print.inx similarity index 79% rename from embroider_update.inx rename to embroider_print.inx index b700e605a..cbba82ccf 100644 --- a/embroider_update.inx +++ b/embroider_print.inx @@ -1,8 +1,8 @@ - <_name>Update embroidery param names - lexelby.embroider.update - embroider_update.py + <_name>Print + jonh.embroider.print + embroider_print.py inkex.py all @@ -11,6 +11,6 @@ diff --git a/embroider_print.py b/embroider_print.py new file mode 100644 index 000000000..96c3255d2 --- /dev/null +++ b/embroider_print.py @@ -0,0 +1,321 @@ +#!/usr/bin/python +# + +import sys +import traceback +import os +from threading import Thread +import socket +import errno +import time +import logging +import wx + +import inkex +import inkstitch +from inkstitch import _, PIXELS_PER_MM, SVG_GROUP_TAG +from inkstitch.extensions import InkstitchExtension +from inkstitch.stitch_plan import patches_to_stitch_plan +from inkstitch.svg import render_stitch_plan +from inkstitch.utils import save_stderr, restore_stderr + +from jinja2 import Environment, FileSystemLoader, select_autoescape +from datetime import date +import base64 + +from flask import Flask, request, Response, send_from_directory +import webbrowser +import requests + + +def datetimeformat(value, format='%Y/%m/%d'): + return value.strftime(format) + + +def open_url(url): + # Avoid spurious output from xdg-open. Any output on stdout will crash + # inkscape. + null = open(os.devnull, 'w') + old_stdout = os.dup(sys.stdout.fileno()) + os.dup2(null.fileno(), sys.stdout.fileno()) + + if getattr(sys, 'frozen', False): + + # PyInstaller sets LD_LIBRARY_PATH. We need to temporarily clear it + # to avoid confusing xdg-open, which webbrowser will run. + + # The following code is adapted from PyInstaller's documentation + # http://pyinstaller.readthedocs.io/en/stable/runtime-information.html + + old_environ = dict(os.environ) # make a copy of the environment + lp_key = 'LD_LIBRARY_PATH' # for Linux and *BSD. + lp_orig = os.environ.get(lp_key + '_ORIG') # pyinstaller >= 20160820 has this + if lp_orig is not None: + os.environ[lp_key] = lp_orig # restore the original, unmodified value + else: + os.environ.pop(lp_key, None) # last resort: remove the env var + + webbrowser.open(url) + + # restore the old environ + os.environ.clear() + os.environ.update(old_environ) + else: + webbrowser.open(url) + + # restore file descriptors + os.dup2(old_stdout, sys.stdout.fileno()) + os.close(old_stdout) + + +class PrintPreviewServer(Thread): + def __init__(self, *args, **kwargs): + self.html = kwargs.pop('html') + Thread.__init__(self, *args, **kwargs) + self.daemon = True + self.last_request_time = None + self.shutting_down = False + + self.__setup_app() + + def __set_resources_path(self): + if getattr(sys, 'frozen', False): + self.resources_path = os.path.join(sys._MEIPASS, 'print', 'resources') + else: + self.resources_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'print', 'resources') + + def __setup_app(self): + self.__set_resources_path() + self.app = Flask(__name__) + + @self.app.before_request + def request_started(): + self.last_request_time = time.time() + + @self.app.before_first_request + def start_watcher(): + self.watcher_thread = Thread(target=self.watch) + self.watcher_thread.daemon = True + self.watcher_thread.start() + + @self.app.route('/') + def index(): + return self.html + + @self.app.route('/shutdown', methods=['POST']) + def shutdown(): + self.shutting_down = True + request.environ.get('werkzeug.server.shutdown')() + return 'Server shutting down...' + + @self.app.route('/resources/', methods=['GET']) + def resources(resource): + return send_from_directory(self.resources_path, resource, cache_timeout=1) + + @self.app.route('/ping') + def ping(): + # Javascript is letting us know it's still there. This resets self.last_request_time. + return "pong" + + @self.app.route('/printing/start') + def printing_start(): + # temporarily turn off the watcher while the print dialog is up, + # because javascript will be frozen + self.last_request_time = None + return "OK" + + @self.app.route('/printing/end') + def printing_end(): + # nothing to do here -- request_started() will restart the watcher + return "OK" + + def stop(self): + # for whatever reason, shutting down only seems possible in + # the context of a flask request, so we'll just make one + requests.post("http://%s:%s/shutdown" % (self.host, self.port)) + + def watch(self): + try: + while True: + time.sleep(1) + if self.shutting_down: + break + + if self.last_request_time is not None and \ + (time.time() - self.last_request_time) > 3: + self.stop() + break + except: + # seems like sometimes this thread blows up during shutdown + pass + + def disable_logging(self): + logging.getLogger('werkzeug').setLevel(logging.ERROR) + + def run(self): + self.disable_logging() + + self.host = "127.0.0.1" + self.port = 5000 + + while True: + try: + self.app.run(self.host, self.port, threaded=True) + except socket.error, e: + if e.errno == errno.EADDRINUSE: + self.port += 1 + continue + else: + raise + else: + break + + +class PrintInfoFrame(wx.Frame): + def __init__(self, *args, **kwargs): + self.print_server = kwargs.pop("print_server") + wx.Frame.__init__(self, *args, **kwargs) + + panel = wx.Panel(self) + sizer = wx.BoxSizer(wx.VERTICAL) + + text = wx.StaticText(panel, label=_("A print preview has been opened in your web browser. This window will stay open in order to communicate with the JavaScript code running in your browser.\n\nThis window will close after you close the print preview in your browser, or you can close it manually if necessary.")) + font = wx.Font(14, wx.DEFAULT, wx.NORMAL, wx.NORMAL) + text.SetFont(font) + sizer.Add(text, proportion=1, flag=wx.ALL|wx.EXPAND, border=20) + + stop_button = wx.Button(panel, id=wx.ID_CLOSE) + stop_button.Bind(wx.EVT_BUTTON, self.close_button_clicked) + sizer.Add(stop_button, proportion=0, flag=wx.ALIGN_CENTER|wx.ALL, border=10) + + panel.SetSizer(sizer) + panel.Layout() + + self.timer = wx.PyTimer(self.__watcher) + self.timer.Start(250) + + def close_button_clicked(self, event): + self.print_server.stop() + + def __watcher(self): + if not self.print_server.is_alive(): + self.timer.Stop() + self.timer = None + self.Destroy() + + +class Print(InkstitchExtension): + def build_environment(self): + if getattr( sys, 'frozen', False ) : + template_dir = os.path.join(sys._MEIPASS, "print", "templates") + else: + template_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "print", "templates") + + env = Environment( + loader = FileSystemLoader(template_dir), + autoescape=select_autoescape(['html', 'xml']), + extensions=['jinja2.ext.i18n'] + ) + + env.filters['datetimeformat'] = datetimeformat + env.install_gettext_translations(inkstitch.translation) + + return env + + def strip_namespaces(self): + # namespace prefixes seem to trip up HTML, so get rid of them + for element in self.document.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 = {} + + if not self.get_elements(): + return + + self.hide_all_layers() + + patches = self.elements_to_patches(self.elements) + stitch_plan = patches_to_stitch_plan(patches) + render_stitch_plan(self.document.getroot(), stitch_plan) + + self.strip_namespaces() + + # 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']") + stitch_plan_layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") + + # First, delete all of the other layers. We don't need them and they'll + # just bulk up the SVG. + for layer in layers: + if layer is not stitch_plan_layer: + svg.remove(layer) + + overview_svg = inkex.etree.tostring(self.document) + + color_block_groups = stitch_plan_layer.getchildren() + + for i, group in enumerate(color_block_groups): + # clear the stitch plan layer + del stitch_plan_layer[:] + + # add in just this group + stitch_plan_layer.append(group) + + # save an SVG preview + stitch_plan.color_blocks[i].svg_preview = inkex.etree.tostring(self.document) + + env = self.build_environment() + template = env.get_template('index.html') + + html = template.render( + view = {'client_overview': False, 'client_detailedview': False, 'operator_overview': True, 'operator_detailedview': True}, + logo = {'src' : '', 'title' : 'LOGO'}, + date = date.today(), + client = "", + job = { + 'title': '', + 'num_colors': stitch_plan.num_colors, + 'num_color_blocks': len(stitch_plan), + 'num_stops': stitch_plan.num_stops, + 'num_trims': stitch_plan.num_trims, + 'dimensions': stitch_plan.dimensions_mm, + 'num_stitches': stitch_plan.num_stitches, + 'estimated_time': '', # TODO + 'estimated_thread': '', # TODO + }, + svg_overview = overview_svg, + svg_scale = '100%', + color_blocks = stitch_plan.color_blocks, + ) + + print_server = PrintPreviewServer(html=html) + print_server.start() + + time.sleep(1) + open_url("http://%s:%s/" % (print_server.host, print_server.port)) + + app = wx.App() + info_frame = PrintInfoFrame(None, title=_("Ink/Stitch Print"), size=(450, 350), print_server=print_server) + info_frame.Show() + app.MainLoop() + + # don't let inkex print the document out + sys.exit(0) + + +if __name__ == '__main__': + save_stderr() + effect = Print() + effect.affect() + restore_stderr() diff --git a/embroider_simulate.py b/embroider_simulate.py index d14072492..c7c3f6bde 100644 --- a/embroider_simulate.py +++ b/embroider_simulate.py @@ -5,16 +5,18 @@ import wx import inkex import simplestyle import colorsys +from itertools import izip import inkstitch +from inkstitch.extensions import InkstitchExtension from inkstitch import PIXELS_PER_MM -from embroider import _, patches_to_stitches, get_elements, elements_to_patches +from inkstitch.stitch_plan import patches_to_stitch_plan +from inkstitch.svg import color_block_to_point_lists class EmbroiderySimulator(wx.Frame): def __init__(self, *args, **kwargs): - stitch_file = kwargs.pop('stitch_file', None) - patches = kwargs.pop('patches', None) + stitch_plan = kwargs.pop('stitch_plan', None) self.on_close_hook = kwargs.pop('on_close', None) self.frame_period = kwargs.pop('frame_period', 80) self.stitches_per_frame = kwargs.pop('stitches_per_frame', 1) @@ -32,7 +34,7 @@ class EmbroiderySimulator(wx.Frame): self.panel = wx.Panel(self, wx.ID_ANY) self.panel.SetFocus() - self.load(stitch_file, patches) + self.load(stitch_plan) if self.target_duration: self.adjust_speed(self.target_duration) @@ -54,13 +56,10 @@ class EmbroiderySimulator(wx.Frame): self.Bind(wx.EVT_CLOSE, self.on_close) - def load(self, stitch_file=None, patches=None): - if stitch_file: - self.mirror = True - self.segments = self._parse_stitch_file(stitch_file) - elif patches: + def load(self, stitch_plan=None): + if stitch_plan: self.mirror = False - self.segments = self._patches_to_segments(patches) + self.segments = self._stitch_plan_to_segments(stitch_plan) else: return @@ -114,55 +113,21 @@ class EmbroiderySimulator(wx.Frame): return string def color_to_pen(self, color): - # python colorsys module uses floats from 0 to 1.0 - color = [value / 255.0 for value in color] - - hls = list(colorsys.rgb_to_hls(*color)) - - # Our background is white. If the color is too close to white, then - # it won't be visible. Capping lightness should make colors visible - # without changing them too much. - hls[1] = min(hls[1], 0.85) - - color = colorsys.hls_to_rgb(*hls) - - # convert back to values in the range of 0-255 - color = [value * 255 for value in color] - - return wx.Pen(color) - - def _patches_to_segments(self, patches): - stitches = patches_to_stitches(patches) + return wx.Pen(color.visible_on_white.rgb) + def _stitch_plan_to_segments(self, stitch_plan): segments = [] - last_pos = None - last_color = None - pen = None - trimming = False + for color_block in stitch_plan: + pen = self.color_to_pen(color_block.color) - for stitch in stitches: - if stitch.trim: - trimming = True - last_pos = None - continue - - if trimming: - if stitch.jump: + for point_list in color_block_to_point_lists(color_block): + # if there's only one point, there's nothing to do, so skip + if len(point_list) < 2: continue - else: - trimming = False - pos = (stitch.x, stitch.y) - - if stitch.color == last_color: - if last_pos: - segments.append(((last_pos, pos), pen)) - else: - pen = self.color_to_pen(simplestyle.parseColor(stitch.color)) - - last_pos = pos - last_color = stitch.color + for start, end in izip(point_list[:-1], point_list[1:]): + segments.append(((start, end), pen)) return segments @@ -292,18 +257,22 @@ class EmbroiderySimulator(wx.Frame): except IndexError: self.timer.Stop() -class SimulateEffect(inkex.Effect): +class SimulateEffect(InkstitchExtension): def __init__(self): - inkex.Effect.__init__(self) + InkstitchExtension.__init__(self) self.OptionParser.add_option("-P", "--path", action="store", type="string", dest="path", default=".", help="Directory in which to store output file") def effect(self): - patches = elements_to_patches(get_elements(self)) + if not self.get_elements(): + return + + patches = self.elements_to_patches(self.elements) + stitch_plan = patches_to_stitch_plan(patches) app = wx.App() - frame = EmbroiderySimulator(None, -1, _("Embroidery Simulation"), wx.DefaultPosition, size=(1000, 1000), patches=patches) + frame = EmbroiderySimulator(None, -1, _("Embroidery Simulation"), wx.DefaultPosition, size=(1000, 1000), stitch_plan=stitch_plan) app.SetTopWindow(frame) frame.Show() wx.CallAfter(frame.go) diff --git a/embroider_update.py b/embroider_update.py deleted file mode 100644 index b6821d548..000000000 --- a/embroider_update.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/python -# -# Update embroidery parameters stored in XML attributes from old to new -# format. - -import sys -sys.path.append("/usr/share/inkscape/extensions") -import os -import inkex -import simplestyle - -PIXELS_PER_MM = 10 - - -class EmbroiderParams(inkex.Effect): - - def __init__(self, *args, **kwargs): - inkex.Effect.__init__(self) - - self.mapping = {"zigzag_spacing": "zigzag_spacing_mm", - "row_spacing": "row_spacing_mm", - "pull_compensation": "pull_compensation_mm", - "max_stitch_length": "max_stitch_length_mm", - "satin_underlay": "contour_underlay", - "satin_underlay_inset": "contour_underlay_inset_mm", - "satin_zigzag_underlay_spacing": "zigzag_underlay_spacing_mm", - "satin_center_walk": "center_walk_underlay", - "stitch_length": "running_stitch_length_mm", - } - - def effect(self): - for node in self.document.getroot().iter(): - for old, new in self.mapping.iteritems(): - old = "embroider_%s" % old - new = "embroider_%s" % new - - value = node.attrib.pop(old, None) - if value: - if new.endswith('_mm') and value.strip(): - value = str(float(value) / PIXELS_PER_MM) - - node.set(new, value) - - if 'embroider_zigzag_underlay_spacing_mm' in node.attrib: - node.set('embroider_zigzag_underlay', 'yes') - - style = simplestyle.parseStyle(node.get('style')) - - if style.get('fill', 'none') != 'none' and \ - 'embroider_auto_fill' not in node.attrib: - node.set('embroider_auto_fill', 'no') - -if __name__ == '__main__': - e = EmbroiderParams() - e.affect() diff --git a/images/logos/README.md b/images/logos/README.md new file mode 100644 index 000000000..0ac9c52f9 --- /dev/null +++ b/images/logos/README.md @@ -0,0 +1 @@ +This directory contains the official Ink/Stitch logo and a companion branding guide. Note that these are plain SVGs and are not embroidery designs. diff --git a/images/logos/inkstitch_branding_quickguide.svg b/images/logos/inkstitch_branding_quickguide.svg new file mode 100644 index 000000000..8fb907374 --- /dev/null +++ b/images/logos/inkstitch_branding_quickguide.svg @@ -0,0 +1,705 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/logos/inkstitch_colour_logo.svg b/images/logos/inkstitch_colour_logo.svg new file mode 100644 index 000000000..d9a298cfe --- /dev/null +++ b/images/logos/inkstitch_colour_logo.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + diff --git a/images/logos/inkstitch_official_logo.svg b/images/logos/inkstitch_official_logo.svg new file mode 100644 index 000000000..8c0b5f9bb --- /dev/null +++ b/images/logos/inkstitch_official_logo.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/inkstitch/__init__.py b/inkstitch/__init__.py index ba120be01..e72a59b52 100644 --- a/inkstitch/__init__.py +++ b/inkstitch/__init__.py @@ -7,6 +7,9 @@ import gettext from copy import deepcopy import math import libembroidery +from inkstitch.utils import cache +from inkstitch.utils.geometry import Point + import inkex import simplepath import simplestyle @@ -17,11 +20,6 @@ import cubicsuperpath from shapely import geometry as shgeo -try: - from functools import lru_cache -except ImportError: - from backports.functools_lru_cache import lru_cache - # modern versions of Inkscape use 96 pixels per inch as per the CSS standard PIXELS_PER_MM = 96 / 25.4 @@ -29,16 +27,16 @@ 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') +INKSCAPE_LABEL = inkex.addNS('label', 'inkscape') +INKSCAPE_GROUPMODE = inkex.addNS('groupmode', 'inkscape') EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG) dbg = open(os.devnull, "w") +translation = None _ = lambda message: message -# simplify use of lru_cache decorator -def cache(*args, **kwargs): - return lru_cache(maxsize=None)(*args, **kwargs) def localize(): if getattr(sys, 'frozen', False): @@ -49,9 +47,9 @@ def localize(): locale_dir = os.path.join(locale_dir, 'locales') - translation = gettext.translation("inkstitch", locale_dir, fallback=True) + global translation, _ - global _ + translation = gettext.translation("inkstitch", locale_dir, fallback=True) _ = translation.gettext localize() @@ -155,276 +153,11 @@ def get_viewbox_transform(node): return transform -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 - self.description = description - self.unit = unit - self.values = values or [""] - self.type = type - self.group = group - self.inverse = inverse - self.default = default - self.tooltip = tooltip - self.sort_index = sort_index - - def __repr__(self): - return "Param(%s)" % vars(self) - - -# Decorate a member function or property with information about -# the embroidery parameter it corresponds to -def param(*args, **kwargs): - p = Param(*args, **kwargs) - - def decorator(func): - func.param = p - return func - - return decorator - - -class EmbroideryElement(object): - def __init__(self, node): - self.node = node - - @property - def id(self): - return self.node.get('id') - - @classmethod - def get_params(cls): - params = [] - for attr in dir(cls): - prop = getattr(cls, attr) - if isinstance(prop, property): - # The 'param' attribute is set by the 'param' decorator defined above. - if hasattr(prop.fget, 'param'): - params.append(prop.fget.param) - - return params - - @cache - def get_param(self, param, default): - value = self.node.get("embroider_" + param, "").strip() - - return value or default - - @cache - def get_boolean_param(self, param, default=None): - value = self.get_param(param, default) - - if isinstance(value, bool): - return value - else: - return value and (value.lower() in ('yes', 'y', 'true', 't', '1')) - - @cache - def get_float_param(self, param, default=None): - try: - value = float(self.get_param(param, default)) - except (TypeError, ValueError): - return default - - if param.endswith('_mm'): - value = value * PIXELS_PER_MM - - return value - - @cache - def get_int_param(self, param, default=None): - try: - value = int(self.get_param(param, default)) - except (TypeError, ValueError): - return default - - if param.endswith('_mm'): - value = int(value * PIXELS_PER_MM) - - return value - - def set_param(self, name, value): - self.node.set("embroider_%s" % name, str(value)) - - @cache - def get_style(self, style_name): - style = simplestyle.parseStyle(self.node.get("style")) - if (style_name not in style): - return None - value = style[style_name] - if value == 'none': - return None - return value - - @cache - def has_style(self, style_name): - style = simplestyle.parseStyle(self.node.get("style")) - return style_name in style - - @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. - # - # A "superpath" is a collection of paths that are all in one object. - # - # The "cubic" bit in "cubic superpath" is because the bezier curves - # inkscape uses involve cubic polynomials. - # - # Each path is a collection of tuples, each of the form: - # - # (control_before, point, control_after) - # - # A bezier curve segment is defined by an endpoint, a control point, - # a second control point, and a final endpoint. A path is a bunch of - # bezier curves strung together. One could represent a path as a set - # of four-tuples, but there would be redundancy because the ending - # point of one bezier is the starting point of the next. Instead, a - # path is a set of 3-tuples as shown above, and one must construct - # each bezier curve by taking the appropriate endpoints and control - # points. Bleh. It should be noted that a straight segment is - # represented by having the control point on each end equal to that - # end's point. - # - # 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) - - # apply the combined transform to this node's path - simpletransform.applyTransformToPath(transform, path) - - - return path - - def flatten(self, path): - """approximate a path containing beziers with a series of points""" - - path = deepcopy(path) - - cspsubdiv(path, 0.1) - - flattened = [] - - for comp in path: - vertices = [] - for ctl in comp: - vertices.append((ctl[1][0], ctl[1][1])) - flattened.append(vertices) - - return flattened - - @property - @param('trim_after', - _('TRIM after'), - tooltip=_('Trim thread after this object (for supported machines and file formats)'), - type='boolean', - default=False, - sort_index=1000) - def trim_after(self): - return self.get_boolean_param('trim_after', False) - - @property - @param('stop_after', - _('STOP after'), - tooltip=_('Add STOP instruction after this object (for supported machines and file formats)'), - type='boolean', - default=False, - sort_index=1000) - def stop_after(self): - return self.get_boolean_param('stop_after', False) - - def to_patches(self, last_patch): - raise NotImplementedError("%s must implement to_patches()" % self.__class__.__name__) - - def embroider(self, last_patch): - patches = self.to_patches(last_patch) - - if patches: - patches[-1].trim_after = self.trim_after - patches[-1].stop_after = self.stop_after - - return patches - - def fatal(self, message): - print >> sys.stderr, "error:", message - sys.exit(1) - - -class Point: - def __init__(self, x, y): - self.x = x - self.y = y - - def __add__(self, other): - return Point(self.x + other.x, self.y + other.y) - - def __sub__(self, other): - return Point(self.x - other.x, self.y - other.y) - - def mul(self, scalar): - return Point(self.x * scalar, self.y * scalar) - - def __mul__(self, other): - if isinstance(other, Point): - # dot product - return self.x * other.x + self.y * other.y - elif isinstance(other, (int, float)): - return Point(self.x * other, self.y * other) - else: - raise ValueError("cannot multiply Point by %s" % type(other)) - - def __rmul__(self, other): - if isinstance(other, (int, float)): - return self.__mul__(other) - else: - raise ValueError("cannot multiply Point by %s" % type(other)) - - def __repr__(self): - return "Point(%s,%s)" % (self.x, self.y) - - def length(self): - return math.sqrt(math.pow(self.x, 2.0) + math.pow(self.y, 2.0)) - - def unit(self): - return self.mul(1.0 / self.length()) - - def rotate_left(self): - return Point(-self.y, self.x) - - def rotate(self, angle): - return Point(self.x * math.cos(angle) - self.y * math.sin(angle), self.y * math.cos(angle) + self.x * math.sin(angle)) - - def as_int(self): - return Point(int(round(self.x)), int(round(self.y))) - - def as_tuple(self): - return (self.x, self.y) - - def __cmp__(self, other): - return cmp(self.as_tuple(), other.as_tuple()) - - def __getitem__(self, item): - return self.as_tuple()[item] - - def __len__(self): - return 2 +@cache +def get_stroke_scale(node): + doc_width, doc_height = get_doc_size(node) + viewbox = node.get('viewBox').strip().replace(',', ' ').split() + return doc_width / float(viewbox[2]) class Stitch(Point): @@ -440,51 +173,11 @@ class Stitch(Point): return "Stitch(%s, %s, %s, %s, %s, %s)" % (self.x, self.y, self.color, "JUMP" if self.jump else "", "TRIM" if self.trim else "", "STOP" if self.stop else "") -def descendants(node): - nodes = [] - element = EmbroideryElement(node) - - if element.has_style('display') and element.get_style('display') is None: - return [] - - if node.tag == SVG_DEFS_TAG: - return [] - - for child in node: - nodes.extend(descendants(child)) - - if node.tag in EMBROIDERABLE_TAGS: - nodes.append(node) - - return nodes - - -def get_nodes(effect): - """Get all XML nodes, or just those selected - - effect is an instance of a subclass of inkex.Effect. - """ - - if effect.selected: - nodes = [] - for node in effect.document.getroot().iter(): - if node.get("id") in effect.selected: - nodes.extend(descendants(node)) - else: - nodes = descendants(effect.document.getroot()) - - return nodes - - def make_thread(color): - # strip off the leading "#" - if color.startswith("#"): - color = color[1:] - thread = libembroidery.EmbThread() - thread.color = libembroidery.embColor_fromHexStr(color) + thread.color = libembroidery.embColor_make(*color.rgb) - thread.description = color + thread.description = color.name thread.catalogNumber = "" return thread @@ -572,19 +265,25 @@ def get_origin(svg): return default -def write_embroidery_file(file_path, stitches, svg): +def write_embroidery_file(file_path, stitch_plan, svg): origin = get_origin(svg) pattern = libembroidery.embPattern_create() - last_color = None - for stitch in stitches: - if stitch.color != last_color: - add_thread(pattern, make_thread(stitch.color)) - last_color = stitch.color + for color_block in stitch_plan: + add_thread(pattern, make_thread(color_block.color)) - flags = get_flags(stitch) - libembroidery.embPattern_addStitchAbs(pattern, stitch.x - origin.x, stitch.y - origin.y, flags, 1) + for stitch in color_block: + if stitch.stop: + # The user specified "STOP after". "STOP" is the same thing as + # a color change, and the user will assign a special color at + # the machine that tells it to pause after. We need to add + # another copy of the same color here so that the stitches after + # the STOP are still the same color. + add_thread(pattern, make_thread(color_block.color)) + + flags = get_flags(stitch) + libembroidery.embPattern_addStitchAbs(pattern, stitch.x - origin.x, stitch.y - origin.y, flags, 1) libembroidery.embPattern_addStitchAbs(pattern, stitch.x - origin.x, stitch.y - origin.y, libembroidery.END, 1) diff --git a/inkstitch/elements/__init__.py b/inkstitch/elements/__init__.py new file mode 100644 index 000000000..7e05e19c3 --- /dev/null +++ b/inkstitch/elements/__init__.py @@ -0,0 +1,6 @@ +from auto_fill import AutoFill +from fill import Fill +from stroke import Stroke +from satin_column import SatinColumn +from element import EmbroideryElement +from polyline import Polyline diff --git a/inkstitch/elements/auto_fill.py b/inkstitch/elements/auto_fill.py new file mode 100644 index 000000000..6eb1f10c7 --- /dev/null +++ b/inkstitch/elements/auto_fill.py @@ -0,0 +1,108 @@ +import math +from .. import _ +from .element import param, Patch +from ..utils import cache +from .fill import Fill +from shapely import geometry as shgeo +from ..stitches import auto_fill + + +class AutoFill(Fill): + element_name = _("Auto-Fill") + + @property + @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True) + def auto_fill(self): + return self.get_boolean_param('auto_fill', True) + + @property + @cache + def outline(self): + return self.shape.boundary[0] + + @property + @cache + def outline_length(self): + return self.outline.length + + @property + def flip(self): + return False + + @property + @param('running_stitch_length_mm', _('Running stitch length (traversal between sections)'), unit='mm', type='float', default=1.5) + def running_stitch_length(self): + return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) + + @property + @param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=False) + def fill_underlay(self): + return self.get_boolean_param("fill_underlay", default=False) + + @property + @param('fill_underlay_angle', _('Fill angle (default: fill angle + 90 deg)'), unit='deg', group=_('AutoFill Underlay'), type='float') + @cache + def fill_underlay_angle(self): + underlay_angle = self.get_float_param("fill_underlay_angle") + + if underlay_angle: + return math.radians(underlay_angle) + else: + return self.angle + math.pi / 2.0 + + @property + @param('fill_underlay_row_spacing_mm', _('Row spacing (default: 3x fill row spacing)'), unit='mm', group=_('AutoFill Underlay'), type='float') + @cache + def fill_underlay_row_spacing(self): + return self.get_float_param("fill_underlay_row_spacing_mm") or self.row_spacing * 3 + + @property + @param('fill_underlay_max_stitch_length_mm', _('Max stitch length'), unit='mm', group=_('AutoFill Underlay'), type='float') + @cache + def fill_underlay_max_stitch_length(self): + 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) + 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) + if not isinstance(shape, shgeo.MultiPolygon): + shape = shgeo.MultiPolygon([shape]) + return shape + else: + return self.shape + + def to_patches(self, last_patch): + stitches = [] + + if last_patch is None: + starting_point = None + else: + starting_point = last_patch.stitches[-1] + + if self.fill_underlay: + stitches.extend(auto_fill(self.underlay_shape, + self.fill_underlay_angle, + self.fill_underlay_row_spacing, + self.fill_underlay_row_spacing, + self.fill_underlay_max_stitch_length, + self.running_stitch_length, + self.staggers, + starting_point)) + starting_point = stitches[-1] + + stitches.extend(auto_fill(self.shape, + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.running_stitch_length, + self.staggers, + starting_point)) + + return [Patch(stitches=stitches, color=self.color)] diff --git a/inkstitch/elements/element.py b/inkstitch/elements/element.py new file mode 100644 index 000000000..7a029eacd --- /dev/null +++ b/inkstitch/elements/element.py @@ -0,0 +1,260 @@ +import sys +from copy import deepcopy + +from ..utils import cache +from shapely import geometry as shgeo +from .. import _, PIXELS_PER_MM, get_viewbox_transform, get_stroke_scale, convert_length + +# inkscape-provided utilities +import simpletransform +import simplestyle +import cubicsuperpath +from cspsubdiv import cspsubdiv + +class Patch: + """A raw collection of stitches with attached instructions.""" + + def __init__(self, color=None, stitches=None, trim_after=False, stop_after=False): + self.color = color + self.stitches = stitches or [] + self.trim_after = trim_after + self.stop_after = stop_after + + def __add__(self, other): + if isinstance(other, Patch): + return Patch(self.color, self.stitches + other.stitches) + else: + raise TypeError("Patch can only be added to another Patch") + + def add_stitch(self, stitch): + self.stitches.append(stitch) + + def reverse(self): + 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 + self.description = description + self.unit = unit + self.values = values or [""] + self.type = type + self.group = group + self.inverse = inverse + self.default = default + self.tooltip = tooltip + self.sort_index = sort_index + + def __repr__(self): + return "Param(%s)" % vars(self) + + +# Decorate a member function or property with information about +# the embroidery parameter it corresponds to +def param(*args, **kwargs): + p = Param(*args, **kwargs) + + def decorator(func): + func.param = p + return func + + return decorator + + +class EmbroideryElement(object): + def __init__(self, node): + self.node = node + + @property + def id(self): + return self.node.get('id') + + @classmethod + def get_params(cls): + params = [] + for attr in dir(cls): + prop = getattr(cls, attr) + if isinstance(prop, property): + # The 'param' attribute is set by the 'param' decorator defined above. + if hasattr(prop.fget, 'param'): + params.append(prop.fget.param) + + return params + + @cache + def get_param(self, param, default): + value = self.node.get("embroider_" + param, "").strip() + + return value or default + + @cache + def get_boolean_param(self, param, default=None): + value = self.get_param(param, default) + + if isinstance(value, bool): + return value + else: + return value and (value.lower() in ('yes', 'y', 'true', 't', '1')) + + @cache + def get_float_param(self, param, default=None): + try: + value = float(self.get_param(param, default)) + except (TypeError, ValueError): + value = default + + if value is None: + return value + + if param.endswith('_mm'): + value = value * PIXELS_PER_MM + + return value + + @cache + def get_int_param(self, param, default=None): + try: + value = int(self.get_param(param, default)) + except (TypeError, ValueError): + return default + + if param.endswith('_mm'): + value = int(value * PIXELS_PER_MM) + + return value + + def set_param(self, name, value): + self.node.set("embroider_%s" % name, str(value)) + + @cache + def get_style(self, style_name): + style = simplestyle.parseStyle(self.node.get("style")) + if (style_name not in style): + return None + value = style[style_name] + if value == 'none': + return None + return value + + @cache + def has_style(self, style_name): + style = simplestyle.parseStyle(self.node.get("style")) + return style_name in style + + @property + @cache + def stroke_width(self): + width = self.get_style("stroke-width") + + if width is None: + return 1.0 + + width = convert_length(width) + + return width * get_stroke_scale(self.node.getroottree().getroot()) + + @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. + # + # A "superpath" is a collection of paths that are all in one object. + # + # The "cubic" bit in "cubic superpath" is because the bezier curves + # inkscape uses involve cubic polynomials. + # + # Each path is a collection of tuples, each of the form: + # + # (control_before, point, control_after) + # + # A bezier curve segment is defined by an endpoint, a control point, + # a second control point, and a final endpoint. A path is a bunch of + # bezier curves strung together. One could represent a path as a set + # of four-tuples, but there would be redundancy because the ending + # point of one bezier is the starting point of the next. Instead, a + # path is a set of 3-tuples as shown above, and one must construct + # each bezier curve by taking the appropriate endpoints and control + # points. Bleh. It should be noted that a straight segment is + # represented by having the control point on each end equal to that + # end's point. + # + # 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) + + # apply the combined transform to this node's path + simpletransform.applyTransformToPath(transform, path) + + + return path + + def flatten(self, path): + """approximate a path containing beziers with a series of points""" + + path = deepcopy(path) + + cspsubdiv(path, 0.1) + + flattened = [] + + for comp in path: + vertices = [] + for ctl in comp: + vertices.append((ctl[1][0], ctl[1][1])) + flattened.append(vertices) + + return flattened + + @property + @param('trim_after', + _('TRIM after'), + tooltip=_('Trim thread after this object (for supported machines and file formats)'), + type='boolean', + default=False, + sort_index=1000) + def trim_after(self): + return self.get_boolean_param('trim_after', False) + + @property + @param('stop_after', + _('STOP after'), + tooltip=_('Add STOP instruction after this object (for supported machines and file formats)'), + type='boolean', + default=False, + sort_index=1000) + def stop_after(self): + return self.get_boolean_param('stop_after', False) + + def to_patches(self, last_patch): + raise NotImplementedError("%s must implement to_patches()" % self.__class__.__name__) + + def embroider(self, last_patch): + patches = self.to_patches(last_patch) + + if patches: + patches[-1].trim_after = self.trim_after + patches[-1].stop_after = self.stop_after + + return patches + + def fatal(self, message): + print >> sys.stderr, "error:", message + sys.exit(1) diff --git a/inkstitch/elements/fill.py b/inkstitch/elements/fill.py new file mode 100644 index 000000000..a74a897d0 --- /dev/null +++ b/inkstitch/elements/fill.py @@ -0,0 +1,97 @@ +from .. import _, PIXELS_PER_MM +from .element import param, EmbroideryElement, Patch +from ..utils import cache +from shapely import geometry as shgeo +import math +from ..stitches import running_stitch, auto_fill, legacy_fill + +class Fill(EmbroideryElement): + element_name = _("Fill") + + def __init__(self, *args, **kwargs): + super(Fill, self).__init__(*args, **kwargs) + + @property + @param('auto_fill', _('Manually routed fill stitching'), type='toggle', inverse=True, default=True) + def auto_fill(self): + return self.get_boolean_param('auto_fill', True) + + @property + @param('angle', _('Angle of lines of stitches'), unit='deg', type='float', default=0) + @cache + def angle(self): + return math.radians(self.get_float_param('angle', 0)) + + @property + def color(self): + return self.get_style("fill") + + @property + @param('flip', _('Flip fill (start right-to-left)'), type='boolean', default=False) + def flip(self): + return self.get_boolean_param("flip", False) + + @property + @param('row_spacing_mm', _('Spacing between rows'), unit='mm', type='float', default=0.25) + def row_spacing(self): + return max(self.get_float_param("row_spacing_mm", 0.25), 0.1 * PIXELS_PER_MM) + + @property + def end_row_spacing(self): + return self.get_float_param("end_row_spacing_mm") + + @property + @param('max_stitch_length_mm', _('Maximum fill stitch length'), unit='mm', type='float', default=3.0) + def max_stitch_length(self): + return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.1 * PIXELS_PER_MM) + + @property + @param('staggers', _('Stagger rows this many times before repeating'), type='int', default=4) + def staggers(self): + return self.get_int_param("staggers", 4) + + @property + @cache + def paths(self): + return self.flatten(self.parse_path()) + + @property + @cache + def shape(self): + poly_ary = [] + for sub_path in self.paths: + point_ary = [] + last_pt = None + for pt in sub_path: + if (last_pt is not None): + vp = (pt[0] - last_pt[0], pt[1] - last_pt[1]) + dp = math.sqrt(math.pow(vp[0], 2.0) + math.pow(vp[1], 2.0)) + # dbg.write("dp %s\n" % dp) + if (dp > 0.01): + # I think too-close points confuse shapely. + point_ary.append(pt) + last_pt = pt + else: + last_pt = pt + if point_ary: + poly_ary.append(point_ary) + + # shapely's idea of "holes" are to subtract everything in the second set + # from the first. So let's at least make sure the "first" thing is the + # biggest path. + # TODO: actually figure out which things are holes and which are shells + poly_ary.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) + + polygon = shgeo.MultiPolygon([(poly_ary[0], poly_ary[1:])]) + # print >> sys.stderr, "polygon valid:", polygon.is_valid + return polygon + + def to_patches(self, last_patch): + stitch_lists = legacy_fill(self.shape, + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.flip, + self.staggers) + return [Patch(stitches=stitch_list, color=self.color) for stitch_list in stitch_lists] diff --git a/inkstitch/elements/polyline.py b/inkstitch/elements/polyline.py new file mode 100644 index 000000000..6ded9fd14 --- /dev/null +++ b/inkstitch/elements/polyline.py @@ -0,0 +1,72 @@ +from .. import _, Point +from .element import param, EmbroideryElement, Patch +from ..utils import cache + + +class Polyline(EmbroideryElement): + # Handle a element, which is treated as a set of points to + # stitch exactly. + # + # elements are pretty rare in SVG, from what I can tell. + # Anything you can do with a can also be done with a

, and + # much more. + # + # Notably, EmbroiderModder2 uses elements when converting from + # common machine embroidery file formats to SVG. Handling those here lets + # users use File -> Import to pull in existing designs they may have + # obtained, for example purchased fonts. + + @property + def points(self): + # example: "1,2 0,0 1.5,3 4,2" + + points = self.node.get('points') + points = points.split(" ") + points = [[float(coord) for coord in point.split(",")] for point in points] + + return points + + @property + def path(self): + # A polyline is a series of connected line segments described by their + # points. In order to make use of the existing logic for incorporating + # svg transforms that is in our superclass, we'll convert the polyline + # to a degenerate cubic superpath in which the bezier handles are on + # the segment endpoints. + + path = [[[point[:], point[:], point[:]] for point in self.points]] + + return path + + @property + @cache + def csp(self): + csp = self.parse_path() + + return csp + + @property + def color(self): + # EmbroiderModder2 likes to use the `stroke` property directly instead + # of CSS. + return self.get_style("stroke") or self.node.get("stroke") + + @property + def stitches(self): + # For a , we'll stitch the points exactly as they exist in + # the SVG, with no stitch spacing interpolation, flattening, etc. + + # See the comments in the parent class's parse_path method for a + # description of the CSP data structure. + + stitches = [point for handle_before, point, handle_after in self.csp[0]] + + return stitches + + def to_patches(self, last_patch): + patch = Patch(color=self.color) + + for stitch in self.stitches: + patch.add_stitch(Point(*stitch)) + + return [patch] diff --git a/inkstitch/elements/satin_column.py b/inkstitch/elements/satin_column.py new file mode 100644 index 000000000..d22f5145a --- /dev/null +++ b/inkstitch/elements/satin_column.py @@ -0,0 +1,403 @@ +from itertools import chain, izip + +from .. import _, Point +from .element import param, EmbroideryElement, Patch +from ..utils import cache +from shapely import geometry as shgeo, ops as shops + + +class SatinColumn(EmbroideryElement): + element_name = _("Satin Column") + + def __init__(self, *args, **kwargs): + super(SatinColumn, self).__init__(*args, **kwargs) + + @property + @param('satin_column', _('Custom satin column'), type='toggle') + def satin_column(self): + return self.get_boolean_param("satin_column") + + @property + def color(self): + return self.get_style("stroke") + + @property + @param('zigzag_spacing_mm', _('Zig-zag spacing (peak-to-peak)'), unit='mm', type='float', default=0.4) + def zigzag_spacing(self): + # peak-to-peak distance between zigzags + return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01) + + @property + @param('pull_compensation_mm', _('Pull compensation'), unit='mm', type='float') + def pull_compensation(self): + # In satin stitch, the stitches have a tendency to pull together and + # narrow the entire column. We can compensate for this by stitching + # wider than we desire the column to end up. + return self.get_float_param("pull_compensation_mm", 0) + + @property + @param('contour_underlay', _('Contour underlay'), type='toggle', group=_('Contour Underlay')) + def contour_underlay(self): + # "Contour underlay" is stitching just inside the rectangular shape + # of the satin column; that is, up one side and down the other. + return self.get_boolean_param("contour_underlay") + + @property + @param('contour_underlay_stitch_length_mm', _('Stitch length'), unit='mm', group=_('Contour Underlay'), type='float', default=1.5) + def contour_underlay_stitch_length(self): + return max(self.get_float_param("contour_underlay_stitch_length_mm", 1.5), 0.01) + + @property + @param('contour_underlay_inset_mm', _('Contour underlay inset amount'), unit='mm', group=_('Contour Underlay'), type='float', default=0.4) + def contour_underlay_inset(self): + # how far inside the edge of the column to stitch the underlay + return self.get_float_param("contour_underlay_inset_mm", 0.4) + + @property + @param('center_walk_underlay', _('Center-walk underlay'), type='toggle', group=_('Center-Walk Underlay')) + def center_walk_underlay(self): + # "Center walk underlay" is stitching down and back in the centerline + # between the two sides of the satin column. + return self.get_boolean_param("center_walk_underlay") + + @property + @param('center_walk_underlay_stitch_length_mm', _('Stitch length'), unit='mm', group=_('Center-Walk Underlay'), type='float', default=1.5) + def center_walk_underlay_stitch_length(self): + return max(self.get_float_param("center_walk_underlay_stitch_length_mm", 1.5), 0.01) + + @property + @param('zigzag_underlay', _('Zig-zag underlay'), type='toggle', group=_('Zig-zag Underlay')) + def zigzag_underlay(self): + return self.get_boolean_param("zigzag_underlay") + + @property + @param('zigzag_underlay_spacing_mm', _('Zig-Zag spacing (peak-to-peak)'), unit='mm', group=_('Zig-zag Underlay'), type='float', default=3) + def zigzag_underlay_spacing(self): + return max(self.get_float_param("zigzag_underlay_spacing_mm", 3), 0.01) + + @property + @param('zigzag_underlay_inset_mm', _('Inset amount (default: half of contour underlay inset)'), unit='mm', group=_('Zig-zag Underlay'), type='float') + def zigzag_underlay_inset(self): + # how far in from the edge of the satin the points in the zigzags + # should be + + # Default to half of the contour underlay inset. That is, if we're + # doing both contour underlay and zigzag underlay, make sure the + # points of the zigzag fall outside the contour underlay but inside + # the edges of the satin column. + return self.get_float_param("zigzag_underlay_inset_mm") or self.contour_underlay_inset / 2.0 + + @property + @cache + def csp(self): + return self.parse_path() + + @property + @cache + def flattened_beziers(self): + if len(self.csp) == 2: + return self.simple_flatten_beziers() + else: + return self.flatten_beziers_with_rungs() + + + def flatten_beziers_with_rungs(self): + input_paths = [self.flatten([path]) for path in self.csp] + input_paths = [shgeo.LineString(path[0]) for path in input_paths] + + paths = input_paths[:] + paths.sort(key=lambda path: path.length, reverse=True) + + # Imagine a satin column as a curvy ladder. + # The two long paths are the "rails" of the ladder. The remainder are + # the "rungs". + rails = paths[:2] + rungs = shgeo.MultiLineString(paths[2:]) + + # The rails should stay in the order they were in the original CSP. + # (this lets the user control where the satin starts and ends) + rails.sort(key=lambda rail: input_paths.index(rail)) + + result = [] + + for rail in rails: + if not rail.is_simple: + self.fatal(_("One or more rails crosses itself, and this is not allowed. Please split into multiple satin columns.")) + + # handle null intersections here? + linestrings = shops.split(rail, rungs) + + #print >> dbg, "rails and rungs", [str(rail) for rail in rails], [str(rung) for rung in rungs] + if len(linestrings.geoms) < len(rungs.geoms) + 1: + self.fatal(_("satin column: One or more of the rungs doesn't intersect both rails.") + " " + _("Each rail should intersect both rungs once.")) + elif len(linestrings.geoms) > len(rungs.geoms) + 1: + self.fatal(_("satin column: One or more of the rungs intersects the rails more than once.") + " " + _("Each rail should intersect both rungs once.")) + + paths = [[Point(*coord) for coord in ls.coords] for ls in linestrings.geoms] + result.append(paths) + + return zip(*result) + + + def simple_flatten_beziers(self): + # Given a pair of paths made up of bezier segments, flatten + # each individual bezier segment into line segments that approximate + # the curves. Retain the divisions between beziers -- we'll use those + # later. + + paths = [] + + for path in self.csp: + # See the documentation in the parent class for parse_path() for a + # description of the format of the CSP. Each bezier is constructed + # using two neighboring 3-tuples in the list. + + flattened_path = [] + + # iterate over pairs of 3-tuples + for prev, current in zip(path[:-1], path[1:]): + flattened_segment = self.flatten([[prev, current]]) + flattened_segment = [Point(x, y) for x, y in flattened_segment[0]] + flattened_path.append(flattened_segment) + + paths.append(flattened_path) + + return zip(*paths) + + def validate_satin_column(self): + # The node should have exactly two paths with no fill. Each + # path should have the same number of points, meaning that they + # will both be made up of the same number of bezier curves. + + node_id = self.node.get("id") + + if self.get_style("fill") is not None: + self.fatal(_("satin column: object %s has a fill (but should not)") % node_id) + + if len(self.csp) == 2: + if len(self.csp[0]) != len(self.csp[1]): + self.fatal(_("satin column: object %(id)s has two paths with an unequal number of points (%(length1)d and %(length2)d)") % \ + dict(id=node_id, length1=len(self.csp[0]), length2=len(self.csp[1]))) + + def offset_points(self, pos1, pos2, offset_px): + # Expand or contract two points about their midpoint. This is + # useful for pull compensation and insetting underlay. + + distance = (pos1 - pos2).length() + + if distance < 0.0001: + # if they're the same point, we don't know which direction + # to offset in, so we have to just return the points + return pos1, pos2 + + # don't contract beyond the midpoint, or we'll start expanding + if offset_px < -distance / 2.0: + offset_px = -distance / 2.0 + + pos1 = pos1 + (pos1 - pos2).unit() * offset_px + pos2 = pos2 + (pos2 - pos1).unit() * offset_px + + return pos1, pos2 + + def walk(self, path, start_pos, start_index, distance): + # Move pixels along , which is a sequence of line + # segments defined by points. + + # is the index of the line segment in that + # we're currently on. is where along that line + # segment we are. Return a new position and index. + + # print >> dbg, "walk", start_pos, start_index, distance + + pos = start_pos + index = start_index + last_index = len(path) - 1 + distance_remaining = distance + + while True: + if index >= last_index: + return pos, index + + segment_end = path[index + 1] + segment = segment_end - pos + segment_length = segment.length() + + if segment_length > distance_remaining: + # our walk ends partway along this segment + return pos + segment.unit() * distance_remaining, index + else: + # our walk goes past the end of this segment, so advance + # one point + index += 1 + distance_remaining -= segment_length + pos = segment_end + + def walk_paths(self, spacing, offset): + # Take a bezier segment from each path in turn, and plot out an + # equal number of points on each bezier. Return the points plotted. + # The points will be contracted or expanded by offset using + # offset_points(). + + points = [[], []] + + def add_pair(pos1, pos2): + pos1, pos2 = self.offset_points(pos1, pos2, offset) + points[0].append(pos1) + points[1].append(pos2) + + # We may not be able to fit an even number of zigzags in each pair of + # beziers. We'll store the remaining bit of the beziers after handling + # each section. + remainder_path1 = [] + remainder_path2 = [] + + for segment1, segment2 in self.flattened_beziers: + subpath1 = remainder_path1 + segment1 + subpath2 = remainder_path2 + segment2 + + len1 = shgeo.LineString(subpath1).length + len2 = shgeo.LineString(subpath2).length + + # Base the number of stitches in each section on the _longest_ of + # the two beziers. Otherwise, things could get too sparse when one + # side is significantly longer (e.g. when going around a corner). + # The risk here is that we poke a hole in the fabric if we try to + # cram too many stitches on the short bezier. The user will need + # to avoid this through careful construction of paths. + # + # TODO: some commercial machine embroidery software compensates by + # pulling in some of the "inner" stitches toward the center a bit. + + # note, this rounds down using integer-division + num_points = max(len1, len2) / spacing + + spacing1 = len1 / num_points + spacing2 = len2 / num_points + + pos1 = subpath1[0] + index1 = 0 + + pos2 = subpath2[0] + index2 = 0 + + for i in xrange(int(num_points)): + add_pair(pos1, pos2) + + pos1, index1 = self.walk(subpath1, pos1, index1, spacing1) + pos2, index2 = self.walk(subpath2, pos2, index2, spacing2) + + if index1 < len(subpath1) - 1: + remainder_path1 = [pos1] + subpath1[index1 + 1:] + else: + remainder_path1 = [] + + if index2 < len(subpath2) - 1: + remainder_path2 = [pos2] + subpath2[index2 + 1:] + else: + remainder_path2 = [] + + # We're off by one in the algorithm above, so we need one more + # pair of points. We also want to add points at the very end to + # make sure we match the vectors on screen as best as possible. + # Try to avoid doing both if they're going to stack up too + # closely. + + end1 = remainder_path1[-1] + end2 = remainder_path2[-1] + + if (end1 - pos1).length() > 0.3 * spacing: + add_pair(pos1, pos2) + + add_pair(end1, end2) + + return points + + def do_contour_underlay(self): + # "contour walk" underlay: do stitches up one side and down the + # other. + forward, back = self.walk_paths(self.contour_underlay_stitch_length, + -self.contour_underlay_inset) + return Patch(color=self.color, stitches=(forward + list(reversed(back)))) + + def do_center_walk(self): + # Center walk underlay is just a running stitch down and back on the + # center line between the bezier curves. + + # Do it like contour underlay, but inset all the way to the center. + forward, back = self.walk_paths(self.center_walk_underlay_stitch_length, + -100000) + return Patch(color=self.color, stitches=(forward + list(reversed(back)))) + + def do_zigzag_underlay(self): + # zigzag underlay, usually done at a much lower density than the + # satin itself. It looks like this: + # + # \/\/\/\/\/\/\/\/\/\/| + # /\/\/\/\/\/\/\/\/\/\| + # + # In combination with the "contour walk" underlay, this is the + # "German underlay" described here: + # http://www.mrxstitch.com/underlay-what-lies-beneath-machine-embroidery/ + + patch = Patch(color=self.color) + + sides = self.walk_paths(self.zigzag_underlay_spacing / 2.0, + -self.zigzag_underlay_inset) + + # This organizes the points in each side in the order that they'll be + # visited. + sides = [sides[0][::2] + list(reversed(sides[0][1::2])), + sides[1][1::2] + list(reversed(sides[1][::2]))] + + # This fancy bit of iterable magic just repeatedly takes a point + # from each side in turn. + for point in chain.from_iterable(izip(*sides)): + patch.add_stitch(point) + + return patch + + def do_satin(self): + # satin: do a zigzag pattern, alternating between the paths. The + # zigzag looks like this to make the satin stitches look perpendicular + # to the column: + # + # /|/|/|/|/|/|/|/| + + # print >> dbg, "satin", self.zigzag_spacing, self.pull_compensation + + patch = Patch(color=self.color) + + sides = self.walk_paths(self.zigzag_spacing, self.pull_compensation) + + # Like in zigzag_underlay(): take a point from each side in turn. + for point in chain.from_iterable(izip(*sides)): + patch.add_stitch(point) + + return patch + + def to_patches(self, last_patch): + # Stitch a variable-width satin column, zig-zagging between two paths. + + # The algorithm will draw zigzags between each consecutive pair of + # beziers. The boundary points between beziers serve as "checkpoints", + # allowing the user to control how the zigzags flow around corners. + + # First, verify that we have valid paths. + self.validate_satin_column() + + patches = [] + + if self.center_walk_underlay: + patches.append(self.do_center_walk()) + + if self.contour_underlay: + patches.append(self.do_contour_underlay()) + + if self.zigzag_underlay: + # zigzag underlay comes after contour walk underlay, so that the + # zigzags sit on the contour walk underlay like rail ties on rails. + patches.append(self.do_zigzag_underlay()) + + patches.append(self.do_satin()) + + return patches diff --git a/inkstitch/elements/stroke.py b/inkstitch/elements/stroke.py new file mode 100644 index 000000000..b0e7d23fb --- /dev/null +++ b/inkstitch/elements/stroke.py @@ -0,0 +1,109 @@ +from .. import _, Point +from .element import param, EmbroideryElement, Patch +from ..utils import cache + + +class Stroke(EmbroideryElement): + element_name = "Stroke" + + @property + @param('satin_column', _('Satin stitch along paths'), type='toggle', inverse=True) + def satin_column(self): + return self.get_boolean_param("satin_column") + + @property + def color(self): + return self.get_style("stroke") + + @property + def dashed(self): + return self.get_style("stroke-dasharray") is not None + + @property + @param('running_stitch_length_mm', _('Running stitch length'), unit='mm', type='float', default=1.5) + def running_stitch_length(self): + return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) + + @property + @param('zigzag_spacing_mm', _('Zig-zag spacing (peak-to-peak)'), unit='mm', type='float', default=0.4) + @cache + def zigzag_spacing(self): + return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01) + + @property + @param('repeats', _('Repeats'), type='int', default="1") + def repeats(self): + return self.get_int_param("repeats", 1) + + @property + def paths(self): + return self.flatten(self.parse_path()) + + def is_running_stitch(self): + # stroke width <= 0.5 pixels is deprecated in favor of dashed lines + return self.dashed or self.stroke_width <= 0.5 + + def stroke_points(self, emb_point_list, zigzag_spacing, stroke_width): + # TODO: use inkstitch.stitches.running_stitch + + patch = Patch(color=self.color) + p0 = emb_point_list[0] + rho = 0.0 + side = 1 + last_segment_direction = None + + 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 segi in order: + p1 = emb_point_list[segi] + + # how far we have to go along segment + seg_len = (p1 - p0).length() + if (seg_len == 0): + continue + + # vector pointing along segment + along = (p1 - p0).unit() + + # vector pointing to edge of stroke width + perp = along.rotate_left() * (stroke_width * 0.5) + + 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) + + # 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 + + p0 = p1 + last_segment_direction = along + rho -= seg_len + + if (p0 - patch.stitches[-1]).length() > 0.1: + patch.add_stitch(p0) + + return patch + + def to_patches(self, last_patch): + patches = [] + + for path in self.paths: + path = [Point(x, y) for x, y in path] + if self.is_running_stitch(): + patch = self.stroke_points(path, self.running_stitch_length, stroke_width=0.0) + else: + patch = self.stroke_points(path, self.zigzag_spacing / 2.0, stroke_width=self.stroke_width) + + patches.append(patch) + + return patches diff --git a/inkstitch/extensions.py b/inkstitch/extensions.py new file mode 100644 index 000000000..7795abb8f --- /dev/null +++ b/inkstitch/extensions.py @@ -0,0 +1,103 @@ +import inkex +from .elements import AutoFill, Fill, Stroke, SatinColumn, Polyline, EmbroideryElement +from . import SVG_POLYLINE_TAG, SVG_GROUP_TAG, SVG_DEFS_TAG, INKSCAPE_GROUPMODE, EMBROIDERABLE_TAGS, PIXELS_PER_MM + + +class InkstitchExtension(inkex.Effect): + """Base class for Inkstitch extensions. Not intended for direct use.""" + + def hide_all_layers(self): + for g in self.document.getroot().findall(SVG_GROUP_TAG): + if g.get(INKSCAPE_GROUPMODE) == "layer": + g.set("style", "display:none") + + def no_elements_error(self): + if self.selected: + 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.")) + + def descendants(self, node): + nodes = [] + element = EmbroideryElement(node) + + if element.has_style('display') and element.get_style('display') is None: + return [] + + if node.tag == SVG_DEFS_TAG: + return [] + + for child in node: + nodes.extend(self.descendants(child)) + + if node.tag in EMBROIDERABLE_TAGS: + nodes.append(node) + + return nodes + + def get_nodes(self): + """Get all XML nodes, or just those selected + + effect is an instance of a subclass of inkex.Effect. + """ + + if self.selected: + nodes = [] + for node in self.document.getroot().iter(): + if node.get("id") in self.selected: + nodes.extend(self.descendants(node)) + else: + nodes = self.descendants(self.document.getroot()) + + return nodes + + def detect_classes(self, node): + if node.tag == SVG_POLYLINE_TAG: + return [Polyline] + else: + element = EmbroideryElement(node) + + if element.get_boolean_param("satin_column"): + return [SatinColumn] + else: + classes = [] + + if element.get_style("fill"): + if element.get_boolean_param("auto_fill", True): + classes.append(AutoFill) + else: + classes.append(Fill) + + if element.get_style("stroke"): + classes.append(Stroke) + + if element.get_boolean_param("stroke_first", False): + classes.reverse() + + return classes + + + def get_elements(self): + self.elements = [] + for node in self.get_nodes(): + classes = self.detect_classes(node) + self.elements.extend(cls(node) for cls in classes) + + if self.elements: + return True + else: + self.no_elements_error() + return False + + def elements_to_patches(self, elements): + patches = [] + for element in elements: + if patches: + last_patch = patches[-1] + else: + last_patch = None + + patches.extend(element.embroider(last_patch)) + + return patches diff --git a/inkstitch/stitch_plan/__init__.py b/inkstitch/stitch_plan/__init__.py new file mode 100644 index 000000000..6c1f418aa --- /dev/null +++ b/inkstitch/stitch_plan/__init__.py @@ -0,0 +1 @@ +from stitch_plan import patches_to_stitch_plan, StitchPlan, ColorBlock diff --git a/inkstitch/stitch_plan/stitch_plan.py b/inkstitch/stitch_plan/stitch_plan.py new file mode 100644 index 000000000..3a7b8f181 --- /dev/null +++ b/inkstitch/stitch_plan/stitch_plan.py @@ -0,0 +1,187 @@ +from .. import Stitch, PIXELS_PER_MM +from ..utils.geometry import Point +from .stop import process_stop +from .trim import process_trim +from .ties import add_ties +from ..threads import ThreadColor + + +def patches_to_stitch_plan(patches, collapse_len=3.0 * PIXELS_PER_MM): + """Convert a collection of inkstitch.element.Patch objects to a StitchPlan. + + * applies instructions embedded in the Patch such as trim_after and stop_after + * adds tie-ins and tie-offs + * adds jump-stitches between patches if necessary + """ + + stitch_plan = StitchPlan() + color_block = stitch_plan.new_color_block() + + need_trim = False + for patch in patches: + if not patch.stitches: + continue + + if need_trim: + process_trim(color_block, patch.stitches[0]) + need_trim = False + + if not color_block.has_color(): + # set the color for the first color block + color_block.color = patch.color + + if color_block.color == patch.color: + # add a jump stitch between patches if the distance is more + # than the collapse length + if color_block.last_stitch: + if (patch.stitches[0] - color_block.last_stitch).length() > collapse_len: + color_block.add_stitch(patch.stitches[0].x, patch.stitches[0].y, jump=True) + else: + # add a color change + color_block.add_stitch(color_block.last_stitch.x, color_block.last_stitch.y, stop=True) + color_block = stitch_plan.new_color_block() + color_block.color = patch.color + + color_block.filter_duplicate_stitches() + color_block.add_stitches(patch.stitches) + + if patch.trim_after: + # a trim needs to be followed by a jump to the next stitch, so + # we'll process it when we start the next patch + need_trim = True + + if patch.stop_after: + process_stop(color_block) + + add_ties(stitch_plan) + + return stitch_plan + + +class StitchPlan(object): + """Holds a set of color blocks, each containing stitches.""" + + def __init__(self): + self.color_blocks = [] + + def new_color_block(self, *args, **kwargs): + color_block = ColorBlock(*args, **kwargs) + self.color_blocks.append(color_block) + return color_block + + def __iter__(self): + return iter(self.color_blocks) + + def __len__(self): + return len(self.color_blocks) + + @property + def num_colors(self): + """Number of unique colors in the stitch plan.""" + return len({block.color for block in self}) + + @property + def num_stops(self): + return sum(block.num_stops for block in self) + + @property + def num_trims(self): + return sum(block.num_trims for block in self) + + @property + def num_stitches(self): + return sum(block.num_stitches for block in self) + + @property + def dimensions_mm(self): + # TODO: implement this. Should do a bounding box calculation and + # convert to millimeters. + return "" + + +class ColorBlock(object): + """Holds a set of stitches, all with the same thread color.""" + + def __init__(self, color=None, stitches=None): + self.color = color + self.stitches = stitches or [] + + def __iter__(self): + return iter(self.stitches) + + def has_color(self): + return self._color is not None + + @property + def color(self): + return self._color + + @color.setter + def color(self, value): + if isinstance(value, ThreadColor): + self._color = value + elif value is None: + self._color = None + else: + self._color = ThreadColor(value) + + @property + def last_stitch(self): + if self.stitches: + return self.stitches[-1] + else: + return None + + @property + def num_stitches(self): + """Number of stitches in this color block.""" + return len(self.stitches) + + @property + def num_stops(self): + """Number of pauses in this color block.""" + + # Stops are encoded using two STOP stitches each. See the comment in + # stop.py for an explanation. + + return sum(1 for stitch in self if stitch.stop) / 2 + + @property + def num_trims(self): + """Number of trims in this color block.""" + + return sum(1 for stitch in self if stitch.trim) + + def filter_duplicate_stitches(self): + if not self.stitches: + return + + stitches = [self.stitches[0]] + + for stitch in self.stitches[1:]: + l = (stitch - stitches[-1]).length() + if l <= 0.1: + # duplicate stitch, skip this one + continue + + stitches.append(stitch) + + self.stitches = stitches + + def add_stitch(self, *args, **kwargs): + if isinstance(args[0], Stitch): + self.stitches.append(args[0]) + elif isinstance(args[0], Point): + self.stitches.append(Stitch(args[0].x, args[0].y)) + else: + self.stitches.append(Stitch(*args, **kwargs)) + + def add_stitches(self, stitches): + for stitch in stitches: + if isinstance(stitch, (Stitch, Point)): + self.add_stitch(stitch) + else: + self.add_stitch(*stitch) + + def replace_stitches(self, stitches): + self.stitches = stitches diff --git a/inkstitch/stitch_plan/stop.py b/inkstitch/stitch_plan/stop.py new file mode 100644 index 000000000..c5e9f7e4e --- /dev/null +++ b/inkstitch/stitch_plan/stop.py @@ -0,0 +1,27 @@ +def process_stop(color_block): + """Handle the "stop after" checkbox. + + The user wants the machine to pause after this patch. This can + be useful for applique and similar on multi-needle machines that + normally would not stop between colors. + + On such machines, the user assigns needles to the colors in the + design before starting stitching. C01, C02, etc are normal + needles, but C00 is special. For a block of stitches assigned + to C00, the machine will continue sewing with the last color it + had and pause after it completes the C00 block. + + That means we need to introduce an artificial color change + shortly before the current stitch so that the user can set that + to C00. We'll go back 3 stitches and do that: + """ + + if len(color_block.stitches) >= 3: + color_block.stitches[-3].stop = True + + # and also add a color change on this stitch, completing the C00 + # block: + + color_block.stitches[-1].stop = True + + # reference for the above: https://github.com/lexelby/inkstitch/pull/29#issuecomment-359175447 diff --git a/inkstitch/stitch_plan/ties.py b/inkstitch/stitch_plan/ties.py new file mode 100644 index 000000000..9c688e6ba --- /dev/null +++ b/inkstitch/stitch_plan/ties.py @@ -0,0 +1,46 @@ +from ..utils import cut_path +from ..stitches import running_stitch +from .. import Stitch +from copy import deepcopy + +def add_tie(stitches, tie_path): + tie_path = cut_path(tie_path, 0.6) + tie_stitches = running_stitch(tie_path, 0.3) + tie_stitches = [Stitch(stitch.x, stitch.y) for stitch in tie_stitches] + + stitches.extend(deepcopy(tie_stitches[1:])) + stitches.extend(deepcopy(list(reversed(tie_stitches))[1:])) + + +def add_tie_off(stitches): + add_tie(stitches, list(reversed(stitches))) + + +def add_tie_in(stitches, upcoming_stitches): + add_tie(stitches, upcoming_stitches) + + +def add_ties(stitch_plan): + """Add tie-off before and after trims, jumps, and color changes.""" + + for color_block in stitch_plan: + need_tie_in = True + new_stitches = [] + for i, stitch in enumerate(color_block.stitches): + is_special = stitch.trim or stitch.jump or stitch.stop + + if is_special and not need_tie_in: + add_tie_off(new_stitches) + new_stitches.append(stitch) + need_tie_in = True + elif need_tie_in and not is_special: + new_stitches.append(stitch) + add_tie_in(new_stitches, upcoming_stitches=color_block.stitches[i:]) + need_tie_in = False + else: + new_stitches.append(stitch) + + if not need_tie_in: + add_tie_off(new_stitches) + + color_block.replace_stitches(new_stitches) diff --git a/inkstitch/stitch_plan/trim.py b/inkstitch/stitch_plan/trim.py new file mode 100644 index 000000000..f692a179e --- /dev/null +++ b/inkstitch/stitch_plan/trim.py @@ -0,0 +1,23 @@ +def process_trim(color_block, next_stitch): + """Handle the "trim after" checkbox. + + DST (and maybe other formats?) has no actual TRIM instruction. + Instead, 3 sequential JUMPs cause the machine to trim the thread. + + To support both DST and other formats, we'll add a TRIM and two + JUMPs. The TRIM will be converted to a JUMP by libembroidery + if saving to DST, resulting in the 3-jump sequence. + """ + + delta = next_stitch - color_block.last_stitch + delta = delta * (1/4.0) + + pos = color_block.last_stitch + + for i in xrange(3): + pos += delta + color_block.add_stitch(pos.x, pos.y, jump=True) + + # first one should be TRIM instead of JUMP + color_block.stitches[-3].jump = False + color_block.stitches[-3].trim = True diff --git a/inkstitch/stitches/auto_fill.py b/inkstitch/stitches/auto_fill.py index cf765bc20..7f2659095 100644 --- a/inkstitch/stitches/auto_fill.py +++ b/inkstitch/stitches/auto_fill.py @@ -416,7 +416,7 @@ def connect_points(shape, start, end, running_stitch_length): #print >> dbg, "connect_points:", outline_index, start, end, distance, stitches, direction #dbg.flush() - stitches = [InkstitchPoint(*start)] + stitches = [InkstitchPoint(*outline.interpolate(pos).coords[0])] for i in xrange(num_stitches): pos = (pos + one_stitch) % outline.length diff --git a/inkstitch/stitches/fill.py b/inkstitch/stitches/fill.py index 52c951724..1b7377b0c 100644 --- a/inkstitch/stitches/fill.py +++ b/inkstitch/stitches/fill.py @@ -1,4 +1,5 @@ -from .. import Point as InkstitchPoint, cache, PIXELS_PER_MM +from .. import PIXELS_PER_MM +from ..utils import cache, Point as InkstitchPoint import shapely import math import sys diff --git a/inkstitch/stitches/running_stitch.py b/inkstitch/stitches/running_stitch.py index 3b1663dd1..81124339d 100644 --- a/inkstitch/stitches/running_stitch.py +++ b/inkstitch/stitches/running_stitch.py @@ -29,6 +29,10 @@ def running_stitch(points, stitch_length): for segment_end in points[1:]: segment = segment_end - segment_start segment_length = segment.length() + + if segment_length == 0: + continue + segment_direction = segment.unit() # corner detection diff --git a/inkstitch/svg.py b/inkstitch/svg.py new file mode 100644 index 000000000..d9258f196 --- /dev/null +++ b/inkstitch/svg.py @@ -0,0 +1,68 @@ +import simpletransform, simplestyle, inkex +from . import _, get_viewbox_transform, cache, SVG_GROUP_TAG, INKSCAPE_LABEL, INKSCAPE_GROUPMODE, SVG_POLYLINE_TAG + +def color_block_to_point_lists(color_block): + point_lists = [[]] + + for stitch in color_block: + if stitch.trim: + if point_lists[-1]: + point_lists.append([]) + continue + + if not stitch.jump and not stitch.stop: + point_lists[-1].append(stitch.as_tuple()) + + return point_lists + + +@cache +def get_correction_transform(svg): + transform = get_viewbox_transform(svg) + + # we need to correct for the viewbox + transform = simpletransform.invertTransform(transform) + transform = simpletransform.formatTransform(transform) + + return transform + + +def color_block_to_polylines(color_block, svg): + polylines = [] + for point_list in color_block_to_point_lists(color_block): + color = color_block.color.visible_on_white.to_hex_str() + polylines.append(inkex.etree.Element( + SVG_POLYLINE_TAG, + {'style': simplestyle.formatStyle( + {'stroke': color, + 'stroke-width': "0.4", + 'fill': 'none'}), + 'points': " ".join(",".join(str(coord) for coord in point) for point in point_list), + 'transform': get_correction_transform(svg) + })) + + return polylines + + +def render_stitch_plan(svg, stitch_plan): + layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") + if layer is None: + layer = inkex.etree.Element(SVG_GROUP_TAG, + {'id': '__inkstitch_stitch_plan__', + INKSCAPE_LABEL: _('Stitch Plan'), + INKSCAPE_GROUPMODE: 'layer'}) + else: + # delete old stitch plan + del layer[:] + + # make sure the layer is visible + layer.set('style', 'display:inline') + + for i, color_block in enumerate(stitch_plan): + group = inkex.etree.SubElement(layer, + SVG_GROUP_TAG, + {'id': '__color_block_%d__' % i, + INKSCAPE_LABEL: "color block %d" % (i + 1)}) + group.extend(color_block_to_polylines(color_block, svg)) + + svg.append(layer) diff --git a/inkstitch/threads/__init__.py b/inkstitch/threads/__init__.py new file mode 100644 index 000000000..3ba5ec15c --- /dev/null +++ b/inkstitch/threads/__init__.py @@ -0,0 +1 @@ +from color import ThreadColor diff --git a/inkstitch/threads/color.py b/inkstitch/threads/color.py new file mode 100644 index 000000000..ad3dc051a --- /dev/null +++ b/inkstitch/threads/color.py @@ -0,0 +1,80 @@ +import simplestyle +import re +import colorsys + +class ThreadColor(object): + hex_str_re = re.compile('#([0-9a-z]{3}|[0-9a-z]{6})', re.I) + + def __init__(self, color, name=None, description=None): + if color is None: + self.rgb = (0, 0, 0) + elif isinstance(color, (list, tuple)): + self.rgb = tuple(color) + elif self.hex_str_re.match(color): + self.rgb = simplestyle.parseColor(color) + else: + raise ValueError("Invalid color: " + repr(color)) + + self.name = name + self.description = description + + def __eq__(self, other): + if isinstance(other, ThreadColor): + return self.rgb == other.rgb + else: + return self == ThreadColor(other) + + def __hash__(self): + return hash(self.rgb) + + def __ne__(self, other): + return not(self == other) + + def __repr__(self): + return "ThreadColor" + repr(self.rgb) + + def to_hex_str(self): + return "#%s" % self.hex_digits + + @property + def hex_digits(self): + return "%02X%02X%02X" % self.rgb + + @property + def rgb_normalized(self): + return tuple(channel / 255.0 for channel in self.rgb) + + @property + def font_color(self): + """Pick a color that will allow text to show up on a swatch in the printout.""" + hls = colorsys.rgb_to_hls(*self.rgb_normalized) + + # We'll use white text unless the swatch color is too light. + if hls[1] > 0.7: + return (1, 1, 1) + else: + return (254, 254, 254) + + @property + def visible_on_white(self): + """A ThreadColor similar to this one but visible on white. + + If the thread color is white, we don't want to try to draw white in the + simulation view or print white in the print-out. Choose a color that's + as close as possible to the actual thread color but is still at least + somewhat visible on a white background. + """ + + hls = list(colorsys.rgb_to_hls(*self.rgb_normalized)) + + # Capping lightness should make the color visible without changing it + # too much. + if hls[1] > 0.85: + hls[1] = 0.85 + + 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, description=self.description) diff --git a/inkstitch/utils/__init__.py b/inkstitch/utils/__init__.py index f0d5783bf..94a95658c 100644 --- a/inkstitch/utils/__init__.py +++ b/inkstitch/utils/__init__.py @@ -1 +1,3 @@ from geometry import * +from cache import cache +from io import * diff --git a/inkstitch/utils/cache.py b/inkstitch/utils/cache.py new file mode 100644 index 000000000..38fe8f2c5 --- /dev/null +++ b/inkstitch/utils/cache.py @@ -0,0 +1,8 @@ +try: + from functools import lru_cache +except ImportError: + from backports.functools_lru_cache import lru_cache + +# simplify use of lru_cache decorator +def cache(*args, **kwargs): + return lru_cache(maxsize=None)(*args, **kwargs) diff --git a/inkstitch/utils/geometry.py b/inkstitch/utils/geometry.py index 8b6225549..61b98bcbb 100644 --- a/inkstitch/utils/geometry.py +++ b/inkstitch/utils/geometry.py @@ -1,5 +1,6 @@ -from .. import Point as InkstitchPoint from shapely.geometry import LineString, Point as ShapelyPoint +import math + def cut(line, distance): """ Cuts a LineString in two at a distance from its starting point. @@ -7,9 +8,10 @@ def cut(line, distance): This is an example in the Shapely documentation. """ if distance <= 0.0 or distance >= line.length: - return [LineString(line)] + return [LineString(line), None] coords = list(line.coords) for i, p in enumerate(coords): + # TODO: I think this doesn't work if the path doubles back on itself pd = line.project(ShapelyPoint(p)) if pd == distance: return [ @@ -37,5 +39,64 @@ def cut_path(points, length): path = LineString(points) subpath, rest = cut(path, length) - return [InkstitchPoint(*point) for point in subpath.coords] + return [Point(*point) for point in subpath.coords] + +class Point: + def __init__(self, x, y): + self.x = x + self.y = y + + def __add__(self, other): + return Point(self.x + other.x, self.y + other.y) + + def __sub__(self, other): + return Point(self.x - other.x, self.y - other.y) + + def mul(self, scalar): + return Point(self.x * scalar, self.y * scalar) + + def __mul__(self, other): + if isinstance(other, Point): + # dot product + return self.x * other.x + self.y * other.y + elif isinstance(other, (int, float)): + return Point(self.x * other, self.y * other) + else: + raise ValueError("cannot multiply Point by %s" % type(other)) + + def __rmul__(self, other): + if isinstance(other, (int, float)): + return self.__mul__(other) + else: + raise ValueError("cannot multiply Point by %s" % type(other)) + + def __repr__(self): + return "Point(%s,%s)" % (self.x, self.y) + + def length(self): + return math.sqrt(math.pow(self.x, 2.0) + math.pow(self.y, 2.0)) + + def unit(self): + return self.mul(1.0 / self.length()) + + def rotate_left(self): + return Point(-self.y, self.x) + + def rotate(self, angle): + return Point(self.x * math.cos(angle) - self.y * math.sin(angle), self.y * math.cos(angle) + self.x * math.sin(angle)) + + def as_int(self): + return Point(int(round(self.x)), int(round(self.y))) + + def as_tuple(self): + return (self.x, self.y) + + def __cmp__(self, other): + return cmp(self.as_tuple(), other.as_tuple()) + + def __getitem__(self, item): + return self.as_tuple()[item] + + def __len__(self): + return 2 diff --git a/inkstitch/utils/io.py b/inkstitch/utils/io.py new file mode 100644 index 000000000..e87b98812 --- /dev/null +++ b/inkstitch/utils/io.py @@ -0,0 +1,17 @@ +import os +import sys +from cStringIO import StringIO + +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()) + os.dup2(null.fileno(), 2) + sys.stderr_backup = sys.stderr + sys.stderr = StringIO() + + +def restore_stderr(): + os.dup2(sys.stderr_dup, 2) + sys.stderr_backup.write(sys.stderr.getvalue()) + sys.stderr = sys.stderr_backup diff --git a/messages.po b/messages.po index a2461b39d..92630bb06 100644 --- a/messages.po +++ b/messages.po @@ -1,145 +1,21 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. +# Translations template for PROJECT. +# Copyright (C) 2018 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2018. # #, fuzzy msgid "" msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-02-28 20:27-0500\n" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2018-03-30 20:26-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" -"Language: \n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=CHARSET\n" +"Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" - -msgid "Fill" -msgstr "" - -msgid "Manually routed fill stitching" -msgstr "" - -msgid "Angle of lines of stitches" -msgstr "" - -msgid "Flip fill (start right-to-left)" -msgstr "" - -msgid "Spacing between rows" -msgstr "" - -msgid "Maximum fill stitch length" -msgstr "" - -msgid "Stagger rows this many times before repeating" -msgstr "" - -msgid "Auto-Fill" -msgstr "" - -msgid "Automatically routed fill stitching" -msgstr "" - -msgid "Running stitch length (traversal between sections)" -msgstr "" - -msgid "Underlay" -msgstr "" - -msgid "AutoFill Underlay" -msgstr "" - -msgid "Fill angle (default: fill angle + 90 deg)" -msgstr "" - -msgid "Row spacing (default: 3x fill row spacing)" -msgstr "" - -msgid "Max stitch length" -msgstr "" - -msgid "Inset" -msgstr "" - -msgid "Satin stitch along paths" -msgstr "" - -msgid "Running stitch length" -msgstr "" - -msgid "Zig-zag spacing (peak-to-peak)" -msgstr "" - -msgid "Repeats" -msgstr "" - -msgid "Satin Column" -msgstr "" - -msgid "Custom satin column" -msgstr "" - -msgid "Pull compensation" -msgstr "" - -msgid "Contour underlay" -msgstr "" - -msgid "Contour Underlay" -msgstr "" - -msgid "Stitch length" -msgstr "" - -msgid "Contour underlay inset amount" -msgstr "" - -msgid "Center-walk underlay" -msgstr "" - -msgid "Center-Walk Underlay" -msgstr "" - -msgid "Zig-zag underlay" -msgstr "" - -msgid "Zig-zag Underlay" -msgstr "" - -msgid "Zig-Zag spacing (peak-to-peak)" -msgstr "" - -msgid "Inset amount (default: half of contour underlay inset)" -msgstr "" - -msgid "" -"One or more rails crosses itself, and this is not allowed. Please split " -"into multiple satin columns." -msgstr "" - -msgid "satin column: One or more of the rungs doesn't intersect both rails." -msgstr "" - -msgid "Each rail should intersect both rungs once." -msgstr "" - -msgid "" -"satin column: One or more of the rungs intersects the rails more than once." -msgstr "" - -#, python-format -msgid "satin column: object %s has a fill (but should not)" -msgstr "" - -#, python-format -msgid "" -"satin column: object %(id)s has two paths with an unequal number of points " -"(%(length1)d and %(length2)d)" -msgstr "" +"Generated-By: Babel 2.5.3\n" msgid "" "\n" @@ -147,19 +23,6 @@ msgid "" "Seeing a 'no such option' message? Please restart Inkscape to fix." msgstr "" -msgid "No embroiderable paths selected." -msgstr "" - -msgid "No embroiderable paths found in document." -msgstr "" - -msgid "" -"Tip: use Path -> Object to Path to convert non-paths before embroidering." -msgstr "" - -msgid "Embroidery" -msgstr "" - msgid "These settings will be applied to 1 object." msgstr "" @@ -168,8 +31,8 @@ msgid "These settings will be applied to %d objects." msgstr "" msgid "" -"Some settings had different values across objects. Select a value from the " -"dropdown or enter a new one." +"Some settings had different values across objects. Select a value from " +"the dropdown or enter a new one." msgstr "" #, python-format @@ -231,7 +94,20 @@ msgstr "" #, python-format msgid "" -"Preset \"%s\" already exists. Please use another name or press \"Overwrite\"" +"Preset \"%s\" already exists. Please use another name or press " +"\"Overwrite\"" +msgstr "" + +msgid "" +"A print preview has been opened in your web browser. This window will " +"stay open in order to communicate with the JavaScript code running in " +"your browser.\n" +"\n" +"This window will close after you close the print preview in your browser," +" or you can close it manually if necessary." +msgstr "" + +msgid "Ink/Stitch Print" msgstr "" msgid "Embroidery Simulation" @@ -245,6 +121,45 @@ msgstr "" msgid "Unknown unit: %s" msgstr "" +msgid "No embroiderable paths selected." +msgstr "" + +msgid "No embroiderable paths found in document." +msgstr "" + +msgid "Tip: use Path -> Object to Path to convert non-paths before embroidering." +msgstr "" + +msgid "Stitch Plan" +msgstr "" + +msgid "Auto-Fill" +msgstr "" + +msgid "Automatically routed fill stitching" +msgstr "" + +msgid "Running stitch length (traversal between sections)" +msgstr "" + +msgid "Underlay" +msgstr "" + +msgid "AutoFill Underlay" +msgstr "" + +msgid "Fill angle (default: fill angle + 90 deg)" +msgstr "" + +msgid "Row spacing (default: 3x fill row spacing)" +msgstr "" + +msgid "Max stitch length" +msgstr "" + +msgid "Inset" +msgstr "" + msgid "TRIM after" msgstr "" @@ -258,3 +173,215 @@ msgid "" "Add STOP instruction after this object (for supported machines and file " "formats)" msgstr "" + +msgid "Fill" +msgstr "" + +msgid "Manually routed fill stitching" +msgstr "" + +msgid "Angle of lines of stitches" +msgstr "" + +msgid "Flip fill (start right-to-left)" +msgstr "" + +msgid "Spacing between rows" +msgstr "" + +msgid "Maximum fill stitch length" +msgstr "" + +msgid "Stagger rows this many times before repeating" +msgstr "" + +msgid "Satin Column" +msgstr "" + +msgid "Custom satin column" +msgstr "" + +msgid "Zig-zag spacing (peak-to-peak)" +msgstr "" + +msgid "Pull compensation" +msgstr "" + +msgid "Contour underlay" +msgstr "" + +msgid "Contour Underlay" +msgstr "" + +msgid "Stitch length" +msgstr "" + +msgid "Contour underlay inset amount" +msgstr "" + +msgid "Center-walk underlay" +msgstr "" + +msgid "Center-Walk Underlay" +msgstr "" + +msgid "Zig-zag underlay" +msgstr "" + +msgid "Zig-zag Underlay" +msgstr "" + +msgid "Zig-Zag spacing (peak-to-peak)" +msgstr "" + +msgid "Inset amount (default: half of contour underlay inset)" +msgstr "" + +msgid "" +"One or more rails crosses itself, and this is not allowed. Please split " +"into multiple satin columns." +msgstr "" + +msgid "satin column: One or more of the rungs doesn't intersect both rails." +msgstr "" + +msgid "Each rail should intersect both rungs once." +msgstr "" + +msgid "" +"satin column: One or more of the rungs intersects the rails more than " +"once." +msgstr "" + +#, python-format +msgid "satin column: object %s has a fill (but should not)" +msgstr "" + +#, python-format +msgid "" +"satin column: object %(id)s has two paths with an unequal number of " +"points (%(length1)d and %(length2)d)" +msgstr "" + +msgid "Satin stitch along paths" +msgstr "" + +msgid "Running stitch length" +msgstr "" + +msgid "Repeats" +msgstr "" + +msgid "" +"Unable to autofill. This most often happens because your shape is made " +"up of multiple sections that aren't connected." +msgstr "" + +msgid "" +"Unexpected error while generating fill stitches. Please send your SVG " +"file to lexelby@github." +msgstr "" + +msgid "Color" +msgstr "" + +msgid "rgb" +msgstr "" + +msgid "thread used" +msgstr "" + +msgid "# stitches" +msgstr "" + +msgid "# stops" +msgstr "" + +msgid "# trims" +msgstr "" + +msgid "Page" +msgstr "" + +msgid "Enter job title..." +msgstr "" + +msgid "CLIENT" +msgstr "" + +msgid "Enter client name..." +msgstr "" + +msgid "PURCHASE ORDER #:" +msgstr "" + +msgid "Enter purchase order number..." +msgstr "" + +#, python-format +msgid "%Y.%m.%d" +msgstr "" + +msgid "Thread Consumption" +msgstr "" + +msgid "Stops and Trims" +msgstr "" + +msgid "Estimated Time" +msgstr "" + +msgid "Unique Colors" +msgstr "" + +msgid "Color Blocks" +msgstr "" + +msgid "Design box size" +msgstr "" + +msgid "Total thread used" +msgstr "" + +msgid "Total stitch count" +msgstr "" + +msgid "Total nr stops" +msgstr "" + +msgid "Total nr trims" +msgstr "" + +msgid "Job estimated time" +msgstr "" + +msgid "Scale" +msgstr "" + +msgid "COLOR" +msgstr "" + +msgid "Estimated time" +msgstr "" + +msgid "Client Signature" +msgstr "" + +msgid "Ink/Stitch Print Preview" +msgstr "" + +msgid "Print" +msgstr "" + +msgid "Settings" +msgstr "" + +msgid "Close" +msgstr "" + +msgid "⚠ lost connection to Ink/Stitch" +msgstr "" + +msgid "Printing Size" +msgstr "" + diff --git a/print/resources/barlow-bold.ttf b/print/resources/barlow-bold.ttf new file mode 100644 index 000000000..af2724042 Binary files /dev/null and b/print/resources/barlow-bold.ttf differ diff --git a/print/resources/barlow-condensed-bold.ttf b/print/resources/barlow-condensed-bold.ttf new file mode 100644 index 000000000..cb5e617c6 Binary files /dev/null and b/print/resources/barlow-condensed-bold.ttf differ diff --git a/print/resources/barlow-condensed-extra-bold.ttf b/print/resources/barlow-condensed-extra-bold.ttf new file mode 100644 index 000000000..d9352f09b Binary files /dev/null and b/print/resources/barlow-condensed-extra-bold.ttf differ diff --git a/print/resources/barlow-extra-bold.ttf b/print/resources/barlow-extra-bold.ttf new file mode 100644 index 000000000..45456d4b7 Binary files /dev/null and b/print/resources/barlow-extra-bold.ttf differ diff --git a/print/resources/barlow.ttf b/print/resources/barlow.ttf new file mode 100644 index 000000000..7885fbf88 Binary files /dev/null and b/print/resources/barlow.ttf differ diff --git a/print/resources/barlow_condensed.ttf b/print/resources/barlow_condensed.ttf new file mode 100644 index 000000000..7885fbf88 Binary files /dev/null and b/print/resources/barlow_condensed.ttf differ diff --git a/print/resources/inkstitch-logo.svg b/print/resources/inkstitch-logo.svg new file mode 100644 index 000000000..d9a298cfe --- /dev/null +++ b/print/resources/inkstitch-logo.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + diff --git a/print/resources/inkstitch.js b/print/resources/inkstitch.js new file mode 100644 index 000000000..454c9ae27 --- /dev/null +++ b/print/resources/inkstitch.js @@ -0,0 +1,111 @@ +function ping() { + $.get("/ping") + .done(function() { setTimeout(ping, 1000) }) + .fail(function() { $('#errors').attr('class', 'show') }); +} + +// set pagenumbers +function setPageNumbers() { + var totalPageNum = $('body').find('.page:visible').length; + $('span.total-page-num').text(totalPageNum); + $( '.page:visible span.page-num' ).each(function( index ) { + $(this).text(index + 1); + }); +} + +// set preview svg scale to fit into its box +function scaleInksimulation() { + $('.inksimulation').each(function() { + var scale = Math.min( + $(this).width() / $(this).find('svg').width(), + $(this).height() / $(this).find('svg').height() + ); + + // center the SVG + transform = "translate(-50%, -50%)"; + + if(scale <= 1) { + transform += " scale(" + scale + ")"; + label = parseInt(scale*100) + '%'; + } else { + label = "100%"; + } + + $(this).find('svg').css({ transform: transform }); + $(this).find('figcaption span').text(label); + }); +} + +$(function() { + setTimeout(ping, 1000); + setPageNumbers(); + scaleInksimulation(); + + /* Contendeditable Fields */ + + // When we focus out from a contenteditable field, we want to + // set the same content to all fields with the same classname + document.querySelectorAll('[contenteditable="true"]').forEach(function(elem) { + elem.addEventListener('focusout', function() { + var content = $(this).html(); + var field_name = $(this).attr('data-field-name'); + $('[data-field-name="' + field_name + '"]').html(content); + }); + }); + + $('[contenteditable="true"]').keypress(function(e) { + if (e.which == 13) { + // pressing enter defocuses the element + this.blur(); + + // also suppress the enter keystroke to avoid adding a new line + return false; + } else { + return true; + } + }); + + + /* Settings Bar */ + + $('button.close').click(function() { + $.post('/shutdown', {}) + .done(function(data) { + window.close(); + }); + }); + + $('button.print').click(function() { + // printing halts all javascript activity, so we need to tell the backend + // not to shut down until we're done. + $.get("/printing/start") + .done(function() { + window.print(); + $.get("/printing/end"); + }); + }); + + $('button.settings').click(function(){ + $('#settings-ui').show(); + }); + + $('#close-settings').click(function(){ + $('#settings-ui').hide(); + }); + + /* Settings */ + + // Paper Size + $('select#printing-size').change(function(){ + $('.page').toggleClass('a4'); + }); + + //Checkbox + $(':checkbox').change(function() { + $('.' + this.id).toggle(); + setPageNumbers(); + scaleInksimulation(); + }); + +}); + diff --git a/print/resources/jquery-3.3.1.min.js b/print/resources/jquery-3.3.1.min.js new file mode 100644 index 000000000..4d9b3a258 --- /dev/null +++ b/print/resources/jquery-3.3.1.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w(" + + + + + {% include 'ui.html' %} + +{# client overview #} +

{% include 'print_overview.html' %}
+ +{# client detailedview #} + {% set printview = 'detailedview' %} + {% for color_block in color_blocks %} + {% set outer_loop = loop %} +
{% include 'print_detail.html' %}
+ {% endfor %} + +{# operator overview #} +
{% include 'operator_overview.html' %}
+ +{# operator detailed view #} + {% for color_block_part in color_blocks | batch(13) %} + {% set outer_loop = loop %} +
{% include 'operator_detailedview.html' %}
+ {% endfor %} + + + diff --git a/print/templates/operator_detailedview.html b/print/templates/operator_detailedview.html new file mode 100644 index 000000000..633b9286d --- /dev/null +++ b/print/templates/operator_detailedview.html @@ -0,0 +1,77 @@ +
+ {% include 'headline.html' %} +
+ +
+
+
+

{# svg color #}

+

{# svg preview #}

+

{{ _('Color') }}

+

{{ _('Thread Consumption') }}

+

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

+

{{ _('Estimated Time') }}

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

+ ## +

+

+ {{ svg_overview|safe }} +

+

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

+

+ {{ _('Design box size') }}: {{ 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 }} +

+

+ {{ job.estimated_time }} +

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

+ + + + #{{ loop.index + outer_loop.index0 * 13 }} + + +

+

+ {{ color_block.svg_preview|safe }} +

+

+ {{ color_block.color.thread_name }} + {{ color_block.color.rgb }} +

+

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

+

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

+

+ {{ color_block.estimatedtime }} +

+
+ {% endfor %} +
+
+ + {% include 'footer.html' %} + + diff --git a/print/templates/operator_overview.html b/print/templates/operator_overview.html new file mode 100644 index 000000000..25048ab76 --- /dev/null +++ b/print/templates/operator_overview.html @@ -0,0 +1,33 @@ +
+ {% include 'headline.html' %} +
+
+
+

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

+

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

+

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

+

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

+
+
+
+
+

{{ _('Design box size') }}:{{ job.dimensions }}

+

{{ _('Total stitch count') }}:{{job.num_stitches }}

+

{{ _('Total thread used') }}:{{job.estimated_thread }}

+
+
+
+
+

{{ _('Job estimated time') }}:

+

{{ job.estimated_time }}

+
+
+
+
+
+
+ {{ svg_overview|safe }} +
{{ _('Scale') }} {{ svg_scale }}
+
+
+ {% include 'footer.html' %} diff --git a/print/templates/print_detail.html b/print/templates/print_detail.html new file mode 100644 index 000000000..18a70bbaf --- /dev/null +++ b/print/templates/print_detail.html @@ -0,0 +1,28 @@ +
+ {% include 'headline.html' %} +
+
+
+

{{ _('COLOR') }}:{{ color_block.color.thread_name }}

+
+
+
+
+

{{ _('Estimated time') }}:

+

{{ color_block.estimatedtime }}

+
+
+
+
+
+
+ {{color_block.svg_preview|safe}} +
{{ _('Scale') }} {{ svg_scale }}
+
+ +
+ {% include 'color_swatch.html' %} +
+
+ + {% include 'footer.html' %} diff --git a/print/templates/print_overview.html b/print/templates/print_overview.html new file mode 100644 index 000000000..f5632cebc --- /dev/null +++ b/print/templates/print_overview.html @@ -0,0 +1,42 @@ +
+ {% include 'headline.html' %} +
+
+
+

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

+

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

+

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

+

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

+
+
+
+
+

{{ _('Design box size') }}:{{ job.dimensions }}

+

{{ _('Total stitch count') }}:{{job.num_stitches }}

+

{{ _('Total thread used') }}:{{job.estimated_thread }}

+
+
+
+
+

{{ _('Job estimated time') }}:

+

{{ job.estimated_time }}

+
+
+
+
+
+
+ {{ svg_overview|safe }} +
{{ _('Scale') }} {{ svg_scale }}
+
+ +
+ {% for color_block in color_blocks %} + {% include 'color_swatch.html' %} + {% endfor %} + +
+
{{ _('Client Signature') }}
+
+ + {% include 'footer.html' %} diff --git a/print/templates/ui.html b/print/templates/ui.html new file mode 100644 index 000000000..078f1a4ca --- /dev/null +++ b/print/templates/ui.html @@ -0,0 +1,33 @@ +
+

{{ _('Ink/Stitch Print Preview') }}

+
+ + + +
+
+ {{ _('⚠ lost connection to Ink/Stitch') }} +
+
+ +
+

X

+

{{ _('Settings') }}

+
+

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

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

+

+

+

+
+
+
diff --git a/requirements.txt b/requirements.txt index c029a9ed5..70baadc6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,6 @@ shapely lxml appdirs numpy +jinja2 +flask +requests