diff options
| -rw-r--r-- | README.md | 24 | ||||
| -rw-r--r-- | embroider.py | 755 | ||||
| -rw-r--r-- | embroider_params.py | 1 | ||||
| -rw-r--r-- | embroider_simulate.inx | 17 | ||||
| -rw-r--r-- | embroider_simulate.py | 195 | ||||
| -rw-r--r-- | images/autofill_hole_example.svg | 77 | ||||
| -rw-r--r-- | images/autofill_with_holes.png | bin | 7327 -> 0 bytes | |||
| -rw-r--r-- | images/patches/README.md | 2 | ||||
| -rw-r--r-- | images/patches/orca_patch.jpg | bin | 0 -> 1667404 bytes | |||
| -rw-r--r-- | images/patches/super_hero_patch.jpg | bin | 0 -> 1737976 bytes | |||
| -rw-r--r-- | images/satin_column_rungs_example.svg | 107 | ||||
| -rw-r--r-- | requirements.txt | 6 |
12 files changed, 944 insertions, 240 deletions
@@ -98,30 +98,10 @@ Another issue is that Inkscape has a memory leak related to extensions. The mor ### AutoFill -AutoFill is the default method for generating fill stitching. To use it, create a closed path in Inskcape and add a fill color. +AutoFill is the default method for generating fill stitching. To use it, create a closed path in Inskcape and add a fill color. This algorithm works for complex shapes with or without holes. -inkscape-embroidery will break the shape up into sections that it can embroider at once using back-and-forth rows of stitches. It then adds straight-stitching between sections until it's filled in the entire design. The staggered pattern of stitches is continued seamlessly between sections, so the end result doesn't appear to have any breaks. When moving from one section to the next, it generates running stitching along the outside edge of the shape. +inkscape-embroidery will break the shape up into sections that it can embroider at once using back-and-forth rows of stitches. It then adds straight-stitching between sections until it's filled in the entire design. The staggered pattern of stitches is continued seamlessly between sections, so the end result doesn't appear to have any breaks. When moving from one section to the next, it generates running stitching along the border of the shape. -This algorithm works great for simple shapes, convex or concave. However, it doesn't work for shapes with holes, because the stitching could get "stuck" on the edge of a hole and be unable to reach any remaining section. For this reason, AutoFill rejects regions with holes in them. - -So what do you do if your shape does have holes? You have two choices: use manually-routed fill (described below), or break the shape up into one or more shapes without holes. - -Here's an example of converting a region with a hole into a region without: - - -An SVG version is available in `images/autofill_hole_example.svg` for you to test out. - -Note the thin line drawn from the hole to the edge. In fact, this is a very thin strip missing from the shape -- thinner than the spacing between the rows of stitches. This allows the autofill system to travel into and out of the center of the shape if necessary to get from section to section. - -Note that I've drawn the gap at exactly the same angle as the fill. When the autofill system sees that it is traveling in the same direction as the fill, **it places stitches correctly to match the fill pattern**. This means that the gap will be virtually undetectable because the travel stitches will be hidden in the fill. This may double- or triple-up one of the fill rows, but it's really hard to tell unless you look very closely. - -To easily create a gap like this, follow these steps: - -1. Draw a line with the pencil tool that is parallel to your fill angle. For an angle that's a multiple of 45 degrees, hold control while drawing the line to snap the angle. -1. Make sure your line goes from outside the fill area to inside the hole. -1. Set the stroke width to 0.1 pixels. -1. Path -> Stroke to Path -1. Select the new path and your fill region together and do Path -> Difference (control-minus). #### AutoFill parameters diff --git a/embroider.py b/embroider.py index c207c670..f9c5d147 100644 --- a/embroider.py +++ b/embroider.py @@ -24,7 +24,8 @@ import os import subprocess from copy import deepcopy import time -from itertools import chain, izip +from itertools import chain, izip, groupby +from collections import deque import inkex import simplepath import simplestyle @@ -37,6 +38,7 @@ 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 PyEmb @@ -46,9 +48,12 @@ dbg = open("/tmp/embroider-debug.txt", "w") PyEmb.dbg = dbg 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') +EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG) + class Param(object): def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False, default=None): @@ -157,6 +162,11 @@ class EmbroideryElement(object): 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". @@ -186,9 +196,7 @@ class EmbroideryElement(object): # In a path, each element in the 3-tuple is itself a tuple of (x, y). # Tuples all the way down. Hasn't anyone heard of using classes? - path = cubicsuperpath.parsePath(self.node.get("d")) - - # print >> sys.stderr, pformat(path) + path = self.path # start with the identity transform transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] @@ -288,7 +296,8 @@ class Fill(EmbroideryElement): last_pt = pt else: last_pt = pt - poly_ary.append(point_ary) + 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 @@ -309,8 +318,11 @@ class Fill(EmbroideryElement): def north(self, angle): return self.east(angle).rotate(math.pi / 2) + def row_num(self, point, angle, row_spacing): + return round((point * self.north(angle)) / row_spacing) + def adjust_stagger(self, stitch, angle, row_spacing, max_stitch_length): - row_num = round((stitch * self.north(angle)) / row_spacing) + row_num = self.row_num(stitch, angle, row_spacing) row_stagger = row_num % self.staggers stagger_offset = (float(row_stagger) / self.staggers) * max_stitch_length offset = ((stitch * self.east(angle)) - stagger_offset) % max_stitch_length @@ -448,6 +460,55 @@ class Fill(EmbroideryElement): return runs + def stitch_row(self, patch, beg, end, angle, row_spacing, max_stitch_length): + # We want our stitches to look like this: + # + # ---*-----------*----------- + # ------*-----------*-------- + # ---------*-----------*----- + # ------------*-----------*-- + # ---*-----------*----------- + # + # Each successive row of stitches will be staggered, with + # num_staggers rows before the pattern repeats. A value of + # 4 gives a nice fill while hiding the needle holes. The + # first row is offset 0%, the second 25%, the third 50%, and + # the fourth 75%. + # + # Actually, instead of just starting at an offset of 0, we + # can calculate a row's offset relative to the origin. This + # way if we have two abutting fill regions, they'll perfectly + # tile with each other. That's important because we often get + # abutting fill regions from pull_runs(). + + + beg = PyEmb.Point(*beg) + end = PyEmb.Point(*end) + + row_direction = (end - beg).unit() + segment_length = (end - beg).length() + + # only stitch the first point if it's a reasonable distance away from the + # last stitch + if not patch.stitches or (beg - patch.stitches[-1]).length() > 0.5 * self.options.pixels_per_mm: + patch.add_stitch(beg) + + first_stitch = self.adjust_stagger(beg, angle, row_spacing, max_stitch_length) + + # we might have chosen our first stitch just outside this row, so move back in + if (first_stitch - beg) * row_direction < 0: + first_stitch += row_direction * max_stitch_length + + offset = (first_stitch - beg).length() + + while offset < segment_length: + patch.add_stitch(beg + offset * row_direction) + offset += max_stitch_length + + if (end - patch.stitches[-1]).length() > 0.1 * self.options.pixels_per_mm: + patch.add_stitch(end) + + def section_to_patch(self, group_of_segments, angle=None, row_spacing=None, max_stitch_length=None): if max_stitch_length is None: max_stitch_length = self.max_stitch_length @@ -466,58 +527,13 @@ class Fill(EmbroideryElement): last_end = None for segment in group_of_segments: - # We want our stitches to look like this: - # - # ---*-----------*----------- - # ------*-----------*-------- - # ---------*-----------*----- - # ------------*-----------*-- - # ---*-----------*----------- - # - # Each successive row of stitches will be staggered, with - # num_staggers rows before the pattern repeats. A value of - # 4 gives a nice fill while hiding the needle holes. The - # first row is offset 0%, the second 25%, the third 50%, and - # the fourth 75%. - # - # Actually, instead of just starting at an offset of 0, we - # can calculate a row's offset relative to the origin. This - # way if we have two abutting fill regions, they'll perfectly - # tile with each other. That's important because we often get - # abutting fill regions from pull_runs(). - (beg, end) = segment if (swap): (beg, end) = (end, beg) - beg = PyEmb.Point(*beg) - end = PyEmb.Point(*end) - - row_direction = (end - beg).unit() - segment_length = (end - beg).length() + self.stitch_row(patch, beg, end, angle, row_spacing, max_stitch_length) - # only stitch the first point if it's a reasonable distance away from the - # last stitch - if last_end is None or (beg - last_end).length() > 0.5 * self.options.pixels_per_mm: - patch.add_stitch(beg) - - first_stitch = self.adjust_stagger(beg, angle, row_spacing, max_stitch_length) - - # we might have chosen our first stitch just outside this row, so move back in - if (first_stitch - beg) * row_direction < 0: - first_stitch += row_direction * max_stitch_length - - offset = (first_stitch - beg).length() - - while offset < segment_length: - patch.add_stitch(beg + offset * row_direction) - offset += max_stitch_length - - if (end - patch.stitches[-1]).length() > 0.1 * self.options.pixels_per_mm: - patch.add_stitch(end) - - last_end = end swap = not swap return patch @@ -529,6 +545,9 @@ class Fill(EmbroideryElement): return [self.section_to_patch(group) for group in groups_of_segments] +class MaxQueueLengthExceeded(Exception): + pass + class AutoFill(Fill): @property @param('auto_fill', 'Automatically routed fill stitching', type='toggle', default=True) @@ -580,27 +599,358 @@ class AutoFill(Fill): @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) + return self.get_float_param("fill_underlay_max_stitch_length_mm") or self.max_stitch_length - def validate(self): - if len(self.shape.boundary) > 1: - self.fatal("auto-fill: object %s cannot be auto-filled because it has one or more holes. Please disable auto-fill for this object or break it into separate objects without holes." % self.node.get('id')) + def which_outline(self, coords): + """return the index of the outline on which the point resides - def is_same_run(self, segment1, segment2): - if shgeo.Point(segment1[0]).distance(shgeo.Point(segment2[0])) > self.max_stitch_length: - return False + Index 0 is the outer boundary of the fill region. 1+ are the + outlines of the holes. + """ - if shgeo.Point(segment1[1]).distance(shgeo.Point(segment2[1])) > self.max_stitch_length: - return False + # I'd use an intersection check, but floating point errors make it + # fail sometimes. + + point = shgeo.Point(*coords) + outlines = list(enumerate(self.shape.boundary)) + closest = min(outlines, key=lambda (index, outline): outline.distance(point)) + + return closest[0] + + def project(self, coords, outline_index): + """project the point onto the specified outline + + This returns the distance along the outline at which the point resides. + """ + + return self.shape.boundary.project(shgeo.Point(*coords)) + + def build_graph(self, segments, angle, row_spacing): + """build a graph representation of the grating segments + + This function builds a specialized graph (as in graph theory) that will + help us determine a stitching path. The idea comes from this paper: + + http://www.sciencedirect.com/science/article/pii/S0925772100000158 + + The goal is to build a graph that we know must have an Eulerian Path. + An Eulerian Path is a path from edge to edge in the graph that visits + every edge exactly once and ends at the node it started at. Algorithms + exist to build such a path, and we'll use Hierholzer's algorithm. + + A graph must have an Eulerian Path if every node in the graph has an + even number of edges touching it. Our goal here is to build a graph + that will have this property. + + Based on the paper linked above, we'll build the graph as follows: + + * nodes are the endpoints of the grating segments, where they meet + with the outer outline of the region the outlines of the interior + holes in the region. + * edges are: + * each section of the outer and inner outlines of the region, + between nodes + * double every other edge in the outer and inner hole outlines + + Doubling up on some of the edges seems as if it will just mean we have + to stitch those spots twice. This may be true, but it also ensures + that every node has 4 edges touching it, ensuring that a valid stitch + path must exist. + """ + + graph = networkx.MultiGraph() + + # First, add the grating segments as edges. We'll use the coordinates + # of the endpoints as nodes, which networkx will add automatically. + for segment in segments: + # networkx allows us to label nodes with arbitrary data. We'll + # mark this one as a grating segment. + graph.add_edge(*segment, key="segment") + + for node in graph.nodes(): + outline_index = self.which_outline(node) + outline_projection = self.project(node, outline_index) + + # Tag each node with its index and projection. + graph.add_node(node, index=outline_index, projection=outline_projection) + + nodes = graph.nodes(data=True) + nodes.sort(key=lambda node: (node[1]['index'], node[1]['projection'])) + + for outline_index, nodes in groupby(nodes, key=lambda node: node[1]['index']): + nodes = [ node for node, data in nodes ] + + # heuristic: change the order I visit the nodes in the outline if necessary. + # If the start and endpoints are in the same row, I can't tell which row + # I should treat it as being in. + while True: + row0 = self.row_num(PyEmb.Point(*nodes[0]), angle, row_spacing) + row1 = self.row_num(PyEmb.Point(*nodes[1]), angle, row_spacing) + + if row0 == row1: + nodes = nodes[1:] + [nodes[0]] + else: + break + + # heuristic: it's useful to try to keep the duplicated edges in the same rows. + # this prevents the BFS from having to search a ton of edges. + row_num = min(row0, row1) + if row_num % 2 == 0: + edge_set = 0 + else: + edge_set = 1 + + #print >> sys.stderr, outline_index, "es", edge_set, "rn", row_num, PyEmb.Point(*nodes[0]) * self.north(angle), PyEmb.Point(*nodes[1]) * self.north(angle) + + # add an edge between each successive node + for i, (node1, node2) in enumerate(zip(nodes, nodes[1:] + [nodes[0]])): + graph.add_edge(node1, node2, key="outline") + + # duplicate edges contained in every other row (exactly half + # will be duplicated) + row_num = min(self.row_num(PyEmb.Point(*node1), angle, row_spacing), + self.row_num(PyEmb.Point(*node2), angle, row_spacing)) + + # duplicate every other edge around this outline + if i % 2 == edge_set: + graph.add_edge(node1, node2, key="extra") + + + if not networkx.is_eulerian(graph): + raise Exception("something went wrong: graph is not eulerian") + + return graph + + def node_list_to_edge_list(self, node_list): + return zip(node_list[:-1], node_list[1:]) + + def bfs_for_loop(self, graph, starting_node, max_queue_length=2000): + to_search = deque() + to_search.appendleft(([starting_node], set(), 0)) + + while to_search: + if len(to_search) > max_queue_length: + raise MaxQueueLengthExceeded() + + path, visited_edges, visited_segments = to_search.pop() + ending_node = path[-1] + + # get a list of neighbors paired with the key of the edge I can follow to get there + neighbors = [ + (node, key) + for node, adj in graph.adj[ending_node].iteritems() + for key in adj + ] + + # heuristic: try grating segments first + neighbors.sort(key=lambda (dest, key): key == "segment", reverse=True) + + for next_node, key in neighbors: + # skip if I've already followed this edge + edge = (tuple(sorted((ending_node, next_node))), key) + if edge in visited_edges: + continue + + new_path = path + [next_node] + + if key == "segment": + new_visited_segments = visited_segments + 1 + else: + new_visited_segments = visited_segments + + if next_node == starting_node: + # ignore trivial loops (down and back a doubled edge) + if len(new_path) > 3: + return self.node_list_to_edge_list(new_path), new_visited_segments + + new_visited_edges = visited_edges.copy() + new_visited_edges.add(edge) + + to_search.appendleft((new_path, new_visited_edges, new_visited_segments)) + + def find_loop(self, graph, starting_nodes): + """find a loop in the graph that is connected to the existing path + + Start at a candidate node and search through edges to find a path + back to that node. We'll use a breadth-first search (BFS) in order to + find the shortest available loop. + + In most cases, the BFS should not need to search far to find a loop. + The queue should stay relatively short. + + An added heuristic will be used: if the BFS queue's length becomes + too long, we'll abort and try a different starting point. Due to + the way we've set up the graph, there's bound to be a better choice + somewhere else. + """ + + #loop = self.simple_loop(graph, starting_nodes[-2]) + + #if loop: + # print >> sys.stderr, "simple_loop success" + # starting_nodes.pop() + # starting_nodes.pop() + # return loop + + loop = None + retry = [] + max_queue_length = 2000 + + while not loop: + while not loop and starting_nodes: + starting_node = starting_nodes.pop() + #print >> sys.stderr, "find loop from", starting_node + + try: + # Note: if bfs_for_loop() returns None, no loop can be + # constructed from the starting_node (because the + # necessary edges have already been consumed). In that + # case we discard that node and try the next. + loop = self.bfs_for_loop(graph, starting_node, max_queue_length) + + if not loop: + print >> dbg, "failed on", starting_node + dbg.flush() + except MaxQueueLengthExceeded: + print >> dbg, "gave up on", starting_node + dbg.flush() + # We're giving up on this node for now. We could try + # this node again later, so add it to the bottm of the + # stack. + retry.append(starting_node) + + # Darn, couldn't find a loop. Try harder. + starting_nodes.extendleft(retry) + max_queue_length *= 2 + + starting_nodes.extendleft(retry) + return loop + + def insert_loop(self, path, loop): + """insert a sub-loop into an existing path + + The path will be a series of edges describing a path through the graph + that ends where it starts. The loop will be similar, and its starting + point will be somewhere along the path. + + Insert the loop into the path, resulting in a longer path. + + Both the path and the loop will be a list of edges specified as a + start and end point. The points will be specified in order, such + that they will look like this: + + ((p1, p2), (p2, p3), (p3, p4) ... (pn, p1)) + + path will be modified in place. + """ - return True + loop_start = loop[0][0] - def perimeter_distance(self, p1, p2): - # how far around the perimeter (and in what direction) do I need to go + for i, (start, end) in enumerate(path): + if start == loop_start: + break + + path[i:i] = loop + + def find_stitch_path(self, graph, segments): + """find a path that visits every grating segment exactly once + + Theoretically, we just need to find an Eulerian Path in the graph. + However, we don't actually care whether every single edge is visited. + The edges on the outline of the region are only there to help us get + from one grating segment to the next. + + We'll build a "cycle" (a path that ends where it starts) using + Hierholzer's algorithm. We'll stop once we've visited every grating + segment. + + Hierholzer's algorithm says to select an arbitrary starting node at + each step. In order to produce a reasonable stitch path, we'll select + the vertex carefully such that we get back-and-forth traversal like + mowing a lawn. + + To do this, we'll use a simple heuristic: try to start from nodes in + the order of most-recently-visited first. + """ + + original_graph = graph + graph = graph.copy() + num_segments = len(segments) + segments_visited = 0 + nodes_visited = deque() + + # start with a simple loop: down one segment and then back along the + # outer border to the starting point. + path = [segments[0], list(reversed(segments[0]))] + + graph.remove_edges_from(path) + + segments_visited += 1 + nodes_visited.extend(segments[0]) + + while segments_visited < num_segments: + result = self.find_loop(graph, nodes_visited) + + if not result: + print >> sys.stderr, "Unexpected error filling region. Please send your SVG to lexelby@github." + break + + loop, segments = result + + print >> dbg, "found loop:", loop + dbg.flush() + + segments_visited += segments + nodes_visited += [edge[0] for edge in loop] + graph.remove_edges_from(loop) + + self.insert_loop(path, loop) + + #if segments_visited >= 12: + # break + + # Now we have a loop that covers every grating segment. It returns to + # where it started, which is unnecessary, so we'll snip the last bit off. + #while original_graph.has_edge(*path[-1], key="outline"): + # path.pop() + + return path + + def collapse_sequential_outline_edges(self, graph, path): + """collapse sequential edges that fall on the same outline + + When the path follows multiple edges along the outline of the region, + replace those edges with the starting and ending points. We'll use + these to stitch along the outline later on. + """ + + start_of_run = None + new_path = [] + + for edge in path: + if graph.has_edge(*edge, key="segment"): + if start_of_run: + # close off the last run + new_path.append((start_of_run, edge[0])) + start_of_run = None + + new_path.append(edge) + else: + if not start_of_run: + start_of_run = edge[0] + + if start_of_run: + # if we were still in a run, close it off + new_path.append((start_of_run, edge[1])) + + return new_path + + def outline_distance(self, outline, p1, p2): + # how far around the outline (and in what direction) do I need to go # to get from p1 to p2? - p1_projection = self.outline.project(shgeo.Point(p1)) - p2_projection = self.outline.project(shgeo.Point(p2)) + p1_projection = outline.project(shgeo.Point(p1)) + p2_projection = outline.project(shgeo.Point(p2)) distance = p2_projection - p1_projection @@ -618,91 +968,95 @@ class AutoFill(Fill): else: return distance - def connect_points(self, p1, p2): - patch = Patch(color=self.color) + def connect_points(self, patch, start, end): + outline_index = self.which_outline(start) + outline = self.shape.boundary[outline_index] - pos = self.outline.project(shgeo.Point(p1)) - distance = self.perimeter_distance(p1, p2) + pos = outline.project(shgeo.Point(start)) + distance = self.outline_distance(outline, start, end) stitches = abs(int(distance / self.running_stitch_length)) direction = math.copysign(1.0, distance) one_stitch = self.running_stitch_length * direction + print >> dbg, "connect_points:", start, end, distance, stitches, direction + + patch.add_stitch(PyEmb.Point(*start)) + for i in xrange(stitches): pos = (pos + one_stitch) % self.outline_length - stitch = PyEmb.Point(*self.outline.interpolate(pos).coords[0]) + patch.add_stitch(PyEmb.Point(*outline.interpolate(pos).coords[0])) - # if we're moving along the fill direction, adjust the stitch to - # match the fill so it blends in - if patch.stitches: - if abs((stitch - patch.stitches[-1]) * self.north(self.angle)) < 0.01: - new_stitch = self.adjust_stagger(stitch, self.angle, self.row_spacing, self.max_stitch_length) + end = PyEmb.Point(*end) + if (end - patch.stitches[-1]).length() > 0.1 * self.options.pixels_per_mm: + patch.add_stitch(end) - # don't push the point past the end of this section of the outline - if self.outline.distance(shgeo.Point(new_stitch)) <= 0.01: - stitch = new_stitch + def path_to_patch(self, graph, path, angle, row_spacing, max_stitch_length): + path = self.collapse_sequential_outline_edges(graph, path) - patch.add_stitch(stitch) + patch = Patch(color=self.color) + #patch.add_stitch(PyEmb.Point(*path[0][0])) - return patch + #for edge in path: + # patch.add_stitch(PyEmb.Point(*edge[1])) - def get_corner_points(self, section): - return section[0][0], section[0][-1], section[-1][0], section[-1][-1] + for edge in path: + if graph.has_edge(*edge, key="segment"): + self.stitch_row(patch, edge[0], edge[1], angle, row_spacing, max_stitch_length) + else: + self.connect_points(patch, *edge) + + return patch - def nearest_corner(self, section, point): - return min(self.get_corner_points(section), - key=lambda corner: abs(self.perimeter_distance(point, corner))) + def visualize_graph(self, graph): + patches = [] - def find_nearest_section(self, sections, point): - sections_with_nearest_corner = [(i, self.nearest_corner(section, point)) - for i, section in enumerate(sections)] - return min(sections_with_nearest_corner, - key=lambda(section, corner): abs(self.perimeter_distance(point, corner))) + graph = graph.copy() - def section_from_corner(self, section, start_corner, angle, row_spacing, max_stitch_length): - if start_corner not in section[0]: - section = list(reversed(section)) + for start, end, key in graph.edges_iter(keys=True): + if key == "extra": + patch = Patch(color="#FF0000") + patch.add_stitch(PyEmb.Point(*start)) + patch.add_stitch(PyEmb.Point(*end)) + patches.append(patch) - if section[0][0] != start_corner: - section = [list(reversed(row)) for row in section] + return patches - return self.section_to_patch(section, angle, row_spacing, max_stitch_length) def do_auto_fill(self, angle, row_spacing, max_stitch_length, starting_point=None): + patches = [] + rows_of_segments = self.intersect_region_with_grating(angle, row_spacing) - sections = self.pull_runs(rows_of_segments) + segments = [segment for row in rows_of_segments for segment in row] - patches = [] - last_stitch = starting_point - while sections: - if last_stitch: - section_index, start_corner = self.find_nearest_section(sections, last_stitch) - patches.append(self.connect_points(last_stitch, start_corner)) - patches.append(self.section_from_corner(sections.pop(section_index), start_corner, angle, row_spacing, max_stitch_length)) - else: - patches.append(self.section_to_patch(sections.pop(0), angle, row_spacing, max_stitch_length)) + graph = self.build_graph(segments, angle, row_spacing) + path = self.find_stitch_path(graph, segments) - last_stitch = patches[-1].stitches[-1] + if starting_point: + patch = Patch(self.color) + self.connect_points(patch, starting_point, path[0][0]) + patches.append(patch) + + patches.append(self.path_to_patch(graph, path, angle, row_spacing, max_stitch_length)) return patches - def to_patches(self, last_patch): - print >> dbg, "autofill" - self.validate() + def to_patches(self, last_patch): patches = [] if last_patch is None: - last_stitch = None + starting_point = None else: - last_stitch = last_patch.stitches[-1] + nearest_point = self.outline.interpolate(self.outline.project(shgeo.Point(last_patch.stitches[-1]))) + starting_point = PyEmb.Point(*nearest_point.coords[0]) if self.fill_underlay: - patches.extend(self.do_auto_fill(self.fill_underlay_angle, self.fill_underlay_row_spacing, self.fill_underlay_max_stitch_length, last_stitch)) - last_stitch = patches[-1].stitches[-1] + patches.extend(self.do_auto_fill(self.fill_underlay_angle, self.fill_underlay_row_spacing, self.fill_underlay_max_stitch_length, starting_point)) + starting_point = patches[-1].stitches[-1] - patches.extend(self.do_auto_fill(self.angle, self.row_spacing, self.max_stitch_length, last_stitch)) + patches.extend(self.do_auto_fill(self.angle, self.row_spacing, self.max_stitch_length, starting_point)) return patches @@ -909,6 +1263,49 @@ class SatinColumn(EmbroideryElement): @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 = input_paths[:2] + rungs = shgeo.MultiLineString(input_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) + + if len(linestrings.geoms) < len(rungs.geoms) + 1: + print >> dbg, [str(rail) for rail in rails], [str(rung) for rung in rungs] + self.fatal("Expected %d linestrings, got %d" % (len(rungs.geoms) + 1, len(linestrings.geoms))) + + paths = [[PyEmb.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 @@ -940,14 +1337,12 @@ class SatinColumn(EmbroideryElement): node_id = self.node.get("id") - if len(self.csp) != 2: - self.fatal("satin column: object %s invalid: expected exactly two sub-paths, but there are %s" % (node_id, len(self.csp))) - 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[0]) != len(self.csp[1]): - self.fatal("satin column: object %s has two paths with an unequal number of points (%s and %s)" % (node_id, len(self.csp[0]), len(self.csp[1]))) + if len(self.csp) == 2: + if len(self.csp[0]) != len(self.csp[1]): + self.fatal("satin column: object %s has two paths with an unequal number of points (%s and %s)" % (node_id, len(self.csp[0]), len(self.csp[1]))) def offset_points(self, pos1, pos2, offset_px): # Expand or contract two points about their midpoint. This is @@ -1173,27 +1568,98 @@ class SatinColumn(EmbroideryElement): return patches -def detect_classes(node): - element = EmbroideryElement(node) +class Polyline(EmbroideryElement): + # Handle a <polyline> element, which is treated as a set of points to + # stitch exactly. + # + # <polyline> elements are pretty rare in SVG, from what I can tell. + # Anything you can do with a <polyline> can also be done with a <p>, and + # much more. + # + # Notably, EmbroiderModder2 uses <polyline> 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 - if element.get_boolean_param("satin_column"): - return [SatinColumn] + @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 <polyline>, 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(PyEmb.Point(*stitch)) + + return [patch] + +def detect_classes(node): + if node.tag == SVG_POLYLINE_TAG: + return [Polyline] else: - classes = [] + element = EmbroideryElement(node) - if element.get_style("fill"): - if element.get_boolean_param("auto_fill", True): - classes.append(AutoFill) - else: - classes.append(Fill) + 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_style("stroke"): + classes.append(Stroke) - if element.get_boolean_param("stroke_first", False): - classes.reverse() + if element.get_boolean_param("stroke_first", False): + classes.reverse() - return classes + return classes def descendants(node): @@ -1209,7 +1675,7 @@ def descendants(node): for child in node: nodes.extend(descendants(child)) - if node.tag == SVG_PATH_TAG: + if node.tag in EMBROIDERABLE_TAGS: nodes.append(node) return nodes @@ -1340,6 +1806,10 @@ class Embroider(inkex.Effect): action="store", type="string", dest="path", default=".", help="Directory in which to store output file") + self.OptionParser.add_option("-F", "--output-file", + action="store", type="string", + dest="output_file", default=".", + help="Output filename.") self.OptionParser.add_option("-b", "--max-backups", action="store", type="int", dest="max_backups", default=5, @@ -1351,7 +1821,7 @@ class Embroider(inkex.Effect): self.patches = [] def handle_node(self, node): - print >> dbg, "handling node", node.get('id'), node.get('tag') + print >> dbg, "handling node", node.get('id'), node.tag nodes = descendants(node) for node in nodes: classes = detect_classes(node) @@ -1359,9 +1829,12 @@ class Embroider(inkex.Effect): self.elements.extend(cls(node, self.options) for cls in classes) def get_output_path(self): - svg_filename = self.document.getroot().get(inkex.addNS('docname', 'sodipodi')) - csv_filename = svg_filename.replace('.svg', '.csv') - output_path = os.path.join(self.options.path, csv_filename) + if self.options.output_file: + output_path = os.path.join(self.options.path, self.options.output_file) + else: + svg_filename = self.document.getroot().get(inkex.addNS('docname', 'sodipodi'), "embroidery.svg") + csv_filename = svg_filename.replace('.svg', '.csv') + output_path = os.path.join(self.options.path, csv_filename) def add_suffix(path, suffix): if suffix > 0: diff --git a/embroider_params.py b/embroider_params.py index b1b76669..5fb88b85 100644 --- a/embroider_params.py +++ b/embroider_params.py @@ -371,6 +371,7 @@ class SettingsFrame(wx.Frame): tabs = [current_tab] if current_tab.paired_tab: tabs.append(current_tab.paired_tab) + tabs.extend(current_tab.paired_tab.dependent_tabs) tabs.extend(current_tab.dependent_tabs) for tab in tabs: diff --git a/embroider_simulate.inx b/embroider_simulate.inx new file mode 100644 index 00000000..9c38ec97 --- /dev/null +++ b/embroider_simulate.inx @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension"> + <_name>Simulate</_name> + <id>jonh.embroider.simulate</id> + <dependency type="executable" location="extensions">embroider_simulate.py</dependency> + <dependency type="executable" location="extensions">inkex.py</dependency> + <param name="path" type="string" _gui-text="Directory where embroidery output is stored"></param> + <effect> + <object-type>all</object-type> + <effects-menu> + <submenu _name="Embroidery"/> + </effects-menu> + </effect> + <script> + <command reldir="extensions" interpreter="python">embroider_simulate.py</command> + </script> +</inkscape-extension> diff --git a/embroider_simulate.py b/embroider_simulate.py new file mode 100644 index 00000000..2629c812 --- /dev/null +++ b/embroider_simulate.py @@ -0,0 +1,195 @@ +import sys +import os +import numpy +import wx +import inkex + +class EmbroiderySimulator(wx.Frame): + def __init__(self, *args, **kwargs): + stitch_file = kwargs.pop('stitch_file') + self.frame_period = kwargs.pop('frame_period', 80) + self.stitches_per_frame = kwargs.pop('stitches_per_frame', 1) + + wx.Frame.__init__(self, *args, **kwargs) + self.panel = wx.Panel(self, wx.ID_ANY) + self.panel.SetFocus() + + self.stitches = self._parse_stitches(stitch_file) + self.width, self.height = self.get_dimensions() + + self.buffer = wx.EmptyBitmap(self.width, self.height) + self.dc = wx.MemoryDC() + self.dc.SelectObject(self.buffer) + self.canvas = wx.GraphicsContext.Create(self.dc) + + self.clear() + + self.Bind(wx.EVT_SIZE, self.on_size) + self.panel.Bind(wx.EVT_PAINT, self.on_paint) + self.panel.Bind(wx.EVT_KEY_DOWN, self.on_key_down) + + self.last_pos = None + + def on_key_down(self, event): + keycode = event.GetKeyCode() + + if keycode == ord("+") or keycode == ord("=") or keycode == wx.WXK_UP: + if self.frame_period == 1: + self.stitches_per_frame *= 2 + else: + self.frame_period = self.frame_period / 2 + elif keycode == ord("-") or keycode == ord("_") or keycode == wx.WXK_DOWN: + if self.stitches_per_frame == 1: + self.frame_period *= 2 + else: + self.stitches_per_frame /= 2 + elif keycode == ord("Q"): + self.Close() + elif keycode == ord('P'): + if self.timer.IsRunning(): + self.timer.Stop() + else: + self.timer.Start(self.frame_period) + + self.frame_period = max(1, self.frame_period) + self.stitches_per_frame = max(self.stitches_per_frame, 1) + + if self.timer.IsRunning(): + self.timer.Stop() + self.timer.Start(self.frame_period) + + def _strip_quotes(self, string): + if string.startswith('"') and string.endswith('"'): + string = string[1:-1] + + return string + + def _parse_stitches(self, stitch_file_path): + # "$","1","229","229","229","(null)","(null)" + # "*","JUMP","1.595898","48.731899" + # "*","STITCH","1.595898","48.731899" + + stitches = [] + + pos = (0, 0) + color = wx.Brush('black') + cut = True + + with open(stitch_file_path) as stitch_file: + for line in stitch_file: + fields = line.strip().split(",") + fields = [self._strip_quotes(field) for field in fields] + + symbol, command = fields[:2] + + if symbol == "$": + red, green, blue = fields[2:5] + color = wx.Pen((int(red), int(green), int(blue))) + elif symbol == "*": + if command == "COLOR": + # change color + # The next command should be a JUMP, and we'll need to skip stitching. + cut = True + elif command == "JUMP" or command == "STITCH": + # JUMP just means a long stitch, really. + + x, y = fields[2:] + new_pos = (int(float(x) * 10), int(float(y) * 10)) + + if not cut: + stitches.append(((pos, new_pos), color)) + + cut = False + pos = new_pos + + return stitches + + def get_dimensions(self): + width = 0 + height = 0 + + for stitch in self.stitches: + (start_x, start_y), (end_x, end_y) = stitch[0] + + width = max(width, start_x, end_x) + height = max(height, start_y, end_y) + + return width, height + + def go(self): + self.current_stitch = 0 + self.timer = wx.PyTimer(self.draw_one_stitch) + self.timer.Start(self.frame_period) + + def clear(self): + self.dc.SetBackground(wx.Brush('white')) + self.dc.Clear() + + def on_size(self, e): + # ensure that the whole canvas is visible + window_width, window_height = self.GetSize() + client_width, client_height = self.GetClientSize() + + decorations_width = window_width - client_width + decorations_height = window_height - client_height + + self.SetSize((self.width + decorations_width, self.height + decorations_height)) + + e.Skip() + + def on_paint(self, e): + dc = wx.PaintDC(self.panel) + dc.DrawBitmap(self.buffer, 0, 0) + + if self.last_pos: + dc.DrawLine(self.last_pos[0] - 10, self.last_pos[1], self.last_pos[0] + 10, self.last_pos[1]) + dc.DrawLine(self.last_pos[0], self.last_pos[1] - 10, self.last_pos[0], self.last_pos[1] + 10) + + def redraw(self): + dc = wx.ClientDC(self) + dc.DrawBitmap(self.buffer, 0, 0) + + def draw_one_stitch(self): + for i in xrange(self.stitches_per_frame): + try: + ((x1, y1), (x2, y2)), color = self.stitches[self.current_stitch] + y1 = self.height - y1 + y2 = self.height - y2 + + self.canvas.SetPen(color) + self.canvas.DrawLines(((x1, y1), (x2, y2))) + self.Refresh() + + self.current_stitch += 1 + self.last_pos = (x2, y2) + except IndexError: + self.timer.Stop() + +class SimulateEffect(inkex.Effect): + def __init__(self): + inkex.Effect.__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): + app = wx.App() + frame = EmbroiderySimulator(None, -1, "Embroidery Simulation", wx.DefaultPosition, size=(1000, 1000), stitch_file=self.get_stitch_file()) + app.SetTopWindow(frame) + frame.Show() + wx.CallAfter(frame.go) + app.MainLoop() + + def get_stitch_file(self): + svg_filename = self.document.getroot().get(inkex.addNS('docname', 'sodipodi')) + csv_filename = svg_filename.replace('.svg', '.csv') + stitch_file = os.path.join(self.options.path, csv_filename) + + return stitch_file + + +if __name__ == "__main__": + effect = SimulateEffect() + effect.affect() + sys.exit(0) diff --git a/images/autofill_hole_example.svg b/images/autofill_hole_example.svg deleted file mode 100644 index 336daf67..00000000 --- a/images/autofill_hole_example.svg +++ /dev/null @@ -1,77 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="1000" - height="1000" - viewBox="0 0 999.99999 999.99999" - id="svg8375" - version="1.1" - inkscape:version="0.91+devel r" - sodipodi:docname="fill_hole_test.svg"> - <sodipodi:namedview - showguides="false" - inkscape:snap-global="false" - id="base" - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1.0" - inkscape:pageopacity="0.0" - inkscape:pageshadow="2" - inkscape:zoom="0.7" - inkscape:cx="468.45776" - inkscape:cy="536.24327" - inkscape:document-units="px" - inkscape:current-layer="svg8375" - showgrid="false" - units="px" - inkscape:window-width="1366" - inkscape:window-height="705" - inkscape:window-x="-4" - inkscape:window-y="-4" - inkscape:window-maximized="1" /> - <defs - id="defs8377" /> - <metadata - id="metadata8380"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> - </cc:Work> - </rdf:RDF> - </metadata> - <g - transform="translate(0,-52.362271)" - id="layer1" - inkscape:groupmode="layer" - inkscape:label="Layer 1"> - <path - embroider_flip="false" - embroider_angle="0" - sodipodi:nodetypes="ccccccscccccccsccccccccccc" - inkscape:connector-curvature="0" - d="m 414.90565,693.75697 c -6.5866,20.30142 15.21664,49.37053 44.57587,52.0303 45.44418,8.51926 23.68134,10.31142 70.19909,15.5613 27.00355,7.96974 63.40857,-0.89056 70.47442,-31.65165 19.06371,-19.4564 6.3559,-23.21007 -15.77354,-50.43359 -25.27364,-19.92334 4.88868,39.08324 -25.52808,35.42564 -12.65191,-0.24114 -48.93148,7.24395 -61.76545,7.50181 -8.17255,0.1642 -14.77902,-58.59734 -37.38799,-55.83935 -22.60896,2.75798 -34.41447,-3.39812 -44.61575,27.25253 l -140.24492,-0.008 c 3.63842,-35.17072 14.75399,-69.76994 14.85157,-87.53594 l 3.12805,-32.91829 c 4.41043,-22.01287 21.67084,-9.66993 42.99708,-7.86081 30.64108,-4.8033 61.35018,-6.93231 92.35657,-5.95846 23.17821,0.32264 46.99947,-1.47767 70.41305,-0.50455 16.9103,0.70283 33.60794,2.85238 49.69706,8.29335 34.7607,13.40607 72.10758,1.7815 107.61777,9.46979 21.71457,8.56118 49.58446,13.3113 63.84245,33.12969 9.64206,38.43081 -23.11024,71.4676 -23.23456,109.20592 3.79524,28.37642 -2.66541,56.15236 -6.20726,84.14589 6.91104,29.48759 -11.37684,58.7607 -44.77434,50.89406 -82.68536,-13.30843 -149.52668,-65.52565 -233.27987,-62.68255 -33.26856,3.94593 -78.41763,30.92651 -106.60905,16.90432 -36.17407,-4.34864 -36.41922,-41.07773 -29.93402,-68.73791 -2.75654,-15.37135 -2.60739,-30.5701 -1.04307,-45.6915 z" - style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - id="path4418-8" - embroider_auto_fill="True" - embroider_row_spacing_mm="0.25" - embroider_running_stitch_length_mm="1.2" - embroider_staggers="4" - embroider_max_stitch_length_mm="2.5" - embroider_fill_underlay_max_stitch_length_mm="" - embroider_fill_underlay_angle="" - embroider_fill_underlay_row_spacing_mm="" - embroider_fill_underlay="True" /> - </g> -</svg> diff --git a/images/autofill_with_holes.png b/images/autofill_with_holes.png Binary files differdeleted file mode 100644 index 3ec4d0cf..00000000 --- a/images/autofill_with_holes.png +++ /dev/null diff --git a/images/patches/README.md b/images/patches/README.md index aa3bd99e..27ed3849 100644 --- a/images/patches/README.md +++ b/images/patches/README.md @@ -1,3 +1,5 @@ + +    diff --git a/images/patches/orca_patch.jpg b/images/patches/orca_patch.jpg Binary files differnew file mode 100644 index 00000000..ab08a88a --- /dev/null +++ b/images/patches/orca_patch.jpg diff --git a/images/patches/super_hero_patch.jpg b/images/patches/super_hero_patch.jpg Binary files differnew file mode 100644 index 00000000..992fbec0 --- /dev/null +++ b/images/patches/super_hero_patch.jpg diff --git a/images/satin_column_rungs_example.svg b/images/satin_column_rungs_example.svg new file mode 100644 index 00000000..0f629eca --- /dev/null +++ b/images/satin_column_rungs_example.svg @@ -0,0 +1,107 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + sodipodi:docname="satin_column_rungs_example.svg" + inkscape:version="0.92.1 unknown" + version="1.1" + id="svg8375" + viewBox="0 0 999.99999 999.99999" + height="1000" + width="1000"> + <sodipodi:namedview + inkscape:window-maximized="1" + inkscape:window-y="-4" + inkscape:window-x="-4" + inkscape:window-height="705" + inkscape:window-width="1366" + units="px" + showgrid="false" + inkscape:current-layer="embroidery" + inkscape:document-units="px" + inkscape:cy="658.03477" + inkscape:cx="498.16245" + inkscape:zoom="0.98994949" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + borderopacity="1.0" + bordercolor="#666666" + pagecolor="#ffffff" + id="base" + inkscape:snap-global="false" /> + <defs + id="defs8377" /> + <metadata + id="metadata8380"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + style="display:inline" + transform="translate(0,-52.362271)" + id="layer1" + inkscape:groupmode="layer" + inkscape:label="Layer 1"> + <path + embroider_satin_column="True" + inkscape:connector-curvature="0" + d="M 655.589,419.28516 H 812.06055 V 623.13281 H 511.23828 V 419.28516 H 655.589 m 1.01015,20 h 135.4614 V 603.13281 H 531.23828 V 439.28516 h 125.36087" + style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ff0000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path68100" + sodipodi:nodetypes="cccccccccccc" + embroider_zigzag_spacing_mm="" /> + <path + embroider_satin_column="True" + inkscape:connector-curvature="0" + d="M 275.589,419.28516 H 432.06055 V 623.13281 H 131.23828 V 419.28516 H 275.589 m 1.01015,20 h 135.4614 V 603.13281 H 151.23828 V 439.28516 h 125.36087 m 107.7792,-29.69724 v 40.4061 m 3.47879,23.43968 h 65.71429 m -62.14286,97.85714 h 62.5 m -81.07143,19.28572 v 46.07142 M 177.14286,595.21941 v 43.92857 m -2.85714,-65 h -58.92858 m 54.28572,-107.5 h -51.42857 m 63.57143,-18.92857 v -39.64285" + style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ff0000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path68100-6" + sodipodi:nodetypes="cccccccccccccc" + embroider_zigzag_spacing_mm="" /> + <path + embroider_satin_column="True" + inkscape:connector-curvature="0" + d="M 655.589,139.28516 H 812.06055 V 343.13281 H 511.23828 V 139.28516 H 655.589 m 1.01015,20 h 135.4614 V 323.13281 H 531.23828 V 159.28516 h 125.36087" + style="display:inline;opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#5100ff;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path68100-7" + sodipodi:nodetypes="cccccccccccc" + embroider_zigzag_spacing_mm="" /> + <path + embroider_satin_column="True" + inkscape:connector-curvature="0" + d="M 275.589,139.28516 H 432.06055 V 343.13281 H 131.23828 V 139.28516 H 275.589 m 1.01015,20 h 135.4614 V 323.13281 H 151.23828 V 159.28516 h 125.36087 m 107.7792,-29.69724 v 40.4061 m 3.47879,23.43968 h 65.71429 m -62.14286,97.85714 h 62.5 m -81.07143,19.28572 v 46.07142 M 177.14286,315.21941 v 43.92857 m -2.85714,-65 h -58.92858 m 54.28572,-107.5 h -51.42857 m 63.57143,-18.92857 v -39.64285" + style="display:inline;opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#5100ff;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path68100-6-5" + sodipodi:nodetypes="cccccccccccccc" + embroider_zigzag_spacing_mm="" /> + </g> + <g + inkscape:groupmode="layer" + inkscape:label="Embroidery" + id="embroidery"> + <path + id="path69070" + style="fill:none;stroke:#0000ff;stroke-width:0.40000001" + d="m 655.589,366.92289 1.01015,20 1.78985,-20 0.63418,20 2.16582,-20 0.25821,20 2.54179,-20 -0.11776,20 2.91776,-20 -0.49373,20 3.29373,-20 -0.86969,20 3.66969,-20 -1.24566,20 4.04566,-20 -1.62163,20 4.42163,-20 -1.9976,20 4.7976,-20 -2.37357,20 5.17357,-20 -2.74954,20 5.54954,-20 -3.12551,20 5.92551,-20 -3.50148,20 6.30148,-20 -3.87744,20 6.67744,-20 -4.25341,20 7.05341,-20 -4.62938,20 7.42938,-20 -5.00535,20 7.80535,-20 -5.38132,20 8.18132,-20 -5.75729,20 8.55729,-20 -6.13326,20 8.93326,-20 -6.50923,20 9.30923,-20 -6.88519,20 9.68519,-20 -7.26116,20 10.06116,-20 -7.63713,20 10.43713,-20 -8.0131,20 10.8131,-20 -8.38907,20 11.18907,-20 -8.76504,20 11.56504,-20 -9.14101,20 11.94101,-20 -9.51698,20 12.31698,-20 -9.89294,20 12.69294,-20 -10.26891,20 13.06891,-20 -10.64488,20 13.44488,-20 -11.02085,20 13.82085,-20 -11.39682,20 14.19682,-20 -11.77279,20 14.57279,-20 -12.14876,20 14.94876,-20 -12.52473,20 15.32473,-20 -12.9007,20 15.7007,-20 -13.27666,20 16.07666,-20 -13.65263,20 16.45263,-20 -14.0286,20 16.8286,-20 -14.40457,20 17.20457,-20 -14.78054,20 17.58054,-20 -15.15651,20 17.95651,-20 -15.53248,20 18.33248,-20 -15.90845,20 18.70845,-20 -16.28441,20 19.08441,-20 -16.66038,20 19.46038,-20 -17.03635,20 19.83635,-20 -17.41232,20 20.21232,-20 -17.78829,20 20.58829,-20 -18.16426,20 20.96426,-20 -18.54023,20 21.34023,-20 -18.9162,20 21.7162,-20 -19.29216,20 22.09216,-20 -19.66813,20 22.13968,-19.67155 -20,19.78451 20,-16.98451 -20,19.23716 20,-16.43716 -20,18.68981 20,-15.88981 -20,18.14246 20,-15.34246 -20,17.59511 20,-14.79511 -20,17.04775 20,-14.24775 -20,16.5004 20,-13.7004 -20,15.95305 20,-13.15305 -20,15.4057 20,-12.6057 -20,14.85835 20,-12.05835 -20,14.31099 20,-11.51099 -20,13.76364 20,-10.96364 -20,13.21629 20,-10.41629 -20,12.66894 20,-9.86894 -20,12.12159 20,-9.32159 -20,11.57423 20,-8.77423 -20,11.02688 20,-8.22688 -20,10.47953 20,-7.67953 -20,9.93218 20,-7.13218 -20,9.38483 20,-6.58483 -20,8.83747 20,-6.03747 -20,8.29012 20,-5.49012 -20,7.74277 20,-4.94277 -20,7.19542 20,-4.39542 -20,6.64807 20,-3.84807 -20,6.10071 20,-3.30071 -20,5.55336 20,-2.75336 -20,5.00601 20,-2.20601 -20,4.45866 20,-1.65866 -20,3.91131 20,-1.11131 -20,3.36395 20,-0.56395 -20,2.8166 20,-0.0166 -20,2.26925 20,0.53075 -20,1.7219 20,1.0781 -20,1.17455 20,1.62545 -20,0.62719 20,2.17281 -20,0.0798 20,2.72016 -20,-0.46751 20,3.26751 -20,-1.01486 20,3.81486 -20,-1.56221 20,4.36221 -20,-2.10957 20,4.90957 -20,-2.65692 20,5.45692 -20,-3.20427 20,6.00427 -20,-3.75162 20,6.55162 -20,-4.29897 20,7.09897 -20,-4.84633 20,7.64633 -20,-5.39368 20,8.19368 -20,-5.94103 20,8.74103 -20,-6.48838 20,9.28838 -20,-7.03573 20,9.83573 -20,-7.58309 20,10.38309 -20,-8.13044 20,10.93044 -20,-8.67779 20,11.47779 -20,-9.22514 20,12.02514 -20,-9.77249 20,12.57249 -20,-10.31985 20,13.11985 -20,-10.8672 20,13.6672 -20,-11.41455 20,14.21455 -20,-11.9619 20,14.7619 -20,-12.50925 20,15.30925 -20,-13.05661 20,15.85661 -20,-13.60396 20,16.40396 -20,-14.15131 20,16.95131 -20,-14.69866 20,17.49866 -20,-15.24601 20,18.04601 -20,-15.79337 20,18.59337 -20,-16.34072 20,19.14072 -20,-16.88807 20,19.68807 -20,-17.43542 20,20.23542 -20,-17.98277 20,20.78277 -20,-18.53013 20,21.33013 -20,-19.07748 20,21.87748 -20,-19.62483 19.1192,21.54403 -20.00175,-20 17.20175,20 -19.62833,-20 16.82833,20 -19.2549,-20 16.4549,20 -18.88148,-20 16.08148,20 -18.50806,-20 15.70806,20 -18.13464,-20 15.33464,20 -17.76121,-20 14.96121,20 -17.38779,-20 14.58779,20 -17.01437,-20 14.21437,20 -16.64095,-20 13.84095,20 -16.26752,-20 13.46752,20 -15.8941,-20 13.0941,20 -15.52068,-20 12.72068,20 -15.14726,-20 12.34726,20 -14.77383,-20 11.97383,20 -14.40041,-20 11.60041,20 -14.02699,-20 11.22699,20 -13.65357,-20 10.85357,20 -13.28014,-20 10.48014,20 -12.90672,-20 10.10672,20 -12.5333,-20 9.7333,20 -12.15988,-20 9.35988,20 -11.78645,-20 8.98645,20 -11.41303,-20 8.61303,20 -11.03961,-20 8.23961,20 -10.66619,-20 7.86619,20 -10.29276,-20 7.49276,20 -9.91934,-20 7.11934,20 -9.54592,-20 6.74592,20 -9.1725,-20 6.3725,20 -8.79907,-20 5.99907,20 -8.42565,-20 5.62565,20 -8.05223,-20 5.25223,20 -7.67881,-20 4.87881,20 -7.30538,-20 4.50538,20 -6.93196,-20 4.13196,20 -6.55854,-20 3.75854,20 -6.18512,-20 3.38512,20 -5.81169,-20 3.01169,20 -5.43827,-20 2.63827,20 -5.06485,-20 2.26485,20 -4.69143,-20 1.89143,20 -4.318,-20 1.518,20 -3.94458,-20 1.14458,20 -3.57116,-20 0.77116,20 -3.19774,-20 0.39774,20 -2.82431,-20 0.0243,20 -2.45089,-20 -0.34911,20 -2.07747,-20 -0.72253,20 -1.70405,-20 -1.09595,20 -1.33062,-20 -1.46938,20 -0.9572,-20 -1.8428,20 -0.58378,-20 -2.21622,20 -0.21036,-20 -2.58964,20 0.16307,-20 -2.96307,20 0.53649,-20 -3.33649,20 0.90991,-20 -3.70991,20 1.28333,-20 -4.08333,20 1.65676,-20 -4.45676,20 2.03018,-20 -4.83018,20 2.4036,-20 -5.2036,20 2.77702,-20 -5.57702,20 3.15045,-20 -5.95045,20 3.52387,-20 -6.32387,20 3.89729,-20 -6.69729,20 4.27071,-20 -7.07071,20 4.64414,-20 -7.44414,20 5.01756,-20 -7.81756,20 5.39098,-20 -8.19098,20 5.7644,-20 -8.5644,20 6.13783,-20 -8.93783,20 6.51125,-20 -9.31125,20 6.88467,-20 -9.68467,20 7.25809,-20 -10.05809,20 7.63152,-20 -10.43152,20 8.00494,-20 -10.80494,20 8.37836,-20 -11.17836,20 8.75178,-20 -11.55178,20 9.12521,-20 -11.92521,20 9.49863,-20 -12.29863,20 9.87205,-20 -12.67205,20 10.24547,-20 -13.04547,20 10.6189,-20 -13.4189,20 10.99232,-20 -13.79232,20 11.36574,-20 -14.16574,20 11.73916,-20 -14.53916,20 12.11258,-20 -14.91258,20 12.48601,-20 -15.28601,20 12.85943,-20 -15.65943,20 13.23285,-20 -16.03285,20 13.60627,-20 -16.40627,20 13.9797,-20 -16.7797,20 14.35312,-20 -17.15312,20 14.72654,-20 -17.52654,20 15.09996,-20 -17.89996,20 15.47339,-20 -18.27339,20 15.84681,-20 -18.64681,20 16.22023,-20 -19.02023,20 16.59365,-20 -19.39365,20 16.96708,-20 -19.76708,20 17.3405,-20 -20.1405,20 17.71392,-20 -20.51392,20 18.08734,-20 -20.88734,20 18.46077,-20 -21.26077,20 18.83419,-20 -21.63419,20 19.20761,-20 -22.00761,20 19.58103,-20 -22.38103,20 19.95446,-20 -20.29593,17.54147 20,-19.49641 -20,16.69641 20,-18.94727 -20,16.14727 20,-18.39814 -20,15.59814 20,-17.849 -20,15.049 20,-17.29987 -20,14.49987 20,-16.75073 -20,13.95073 20,-16.20159 -20,13.40159 20,-15.65246 -20,12.85246 20,-15.10332 -20,12.30332 20,-14.55419 -20,11.75419 20,-14.00505 -20,11.20505 20,-13.45592 -20,10.65592 20,-12.90678 -20,10.10678 20,-12.35765 -20,9.55765 20,-11.80851 -20,9.00851 20,-11.25937 -20,8.45937 20,-10.71024 -20,7.91024 20,-10.1611 -20,7.3611 20,-9.61197 -20,6.81197 20,-9.06283 -20,6.26283 20,-8.5137 -20,5.7137 20,-7.96456 -20,5.16456 20,-7.41543 -20,4.61543 20,-6.86629 -20,4.06629 20,-6.31715 -20,3.51715 20,-5.76802 -20,2.96802 20,-5.21888 -20,2.41888 20,-4.66975 -20,1.86975 20,-4.12061 -20,1.32061 20,-3.57148 -20,0.77148 20,-3.02234 -20,0.22234 20,-2.47321 -20,-0.32679 20,-1.92407 -20,-0.87593 20,-1.37493 -20,-1.42507 20,-0.8258 -20,-1.9742 20,-0.27666 -20,-2.52334 20,0.27247 -20,-3.07247 20,0.82161 -20,-3.62161 20,1.37074 -20,-4.17074 20,1.91988 -20,-4.71988 20,2.46902 -20,-5.26902 20,3.01815 -20,-5.81815 20,3.56729 -20,-6.36729 20,4.11642 -20,-6.91642 20,4.66556 -20,-7.46556 20,5.21469 -20,-8.01469 20,5.76383 -20,-8.56383 20,6.31296 -20,-9.11296 20,6.8621 -20,-9.6621 20,7.41124 -20,-10.21124 20,7.96037 -20,-10.76037 20,8.50951 -20,-11.30951 20,9.05864 -20,-11.85864 20,9.60778 -20,-12.40778 20,10.15691 -20,-12.95691 20,10.70605 -20,-13.50605 20,11.25518 -20,-14.05518 20,11.80432 -20,-14.60432 20,12.35346 -20,-15.15346 20,12.90259 -20,-15.70259 20,13.45173 -20,-16.25173 20,14.00086 -20,-16.80086 20,14.55 -20,-17.35 20,15.09913 -20,-17.89913 20,15.64827 -20,-18.44827 20,16.1974 -20,-18.9974 20,16.74654 -20,-19.54654 20,17.29568 -20,-20.09568 20,17.84481 -20,-20.64481 20,18.39395 -20,-21.19395 20,18.94308 -20,-21.74308 20,19.49222 -19.78912,-22.08134 20.13625,20 -17.33625,-20 19.76471,20 -16.96471,-20 19.39317,20 -16.59317,-20 19.02164,20 -16.22164,-20 18.6501,20 -15.8501,-20 18.27857,20 -15.47857,-20 17.90703,20 -15.10703,-20 17.5355,20 -14.7355,-20 17.16396,20 -14.36396,-20 16.79243,20 -13.99243,-20 16.42089,20 -13.62089,-20 16.04936,20 -13.24936,-20 15.67782,20 -12.87782,-20 15.30628,20 -12.50628,-20 14.93475,20 -12.13475,-20 14.56321,20 -11.76321,-20 14.19168,20 -11.39168,-20 13.82014,20 -11.02014,-20 13.44861,20 -10.64861,-20 13.07707,20 -10.27707,-20 12.70554,20 -9.90554,-20 12.334,20 -9.534,-20 11.96247,20 -9.16247,-20 11.59093,20 -8.79093,-20 11.2194,20 -8.4194,-20 10.84786,20 -8.04786,-20 10.47632,20 -7.67632,-20 10.10479,20 -7.30479,-20 9.73325,20 -6.93325,-20 9.36172,20 -6.56172,-20 8.99018,20 -6.19018,-20 8.61865,20 -5.81865,-20 8.24711,20 -5.44711,-20 7.87558,20 -5.07558,-20 7.50404,20 -4.70404,-20 7.13251,20 -4.33251,-20 6.76097,20 -3.96097,-20 6.38943,20 -3.58943,-20 6.0179,20 -3.2179,-20 5.64636,20 -2.84636,-20 5.27483,20 -2.47483,-20 4.90329,20 -2.10329,-20 4.53176,20 -1.73176,-20 4.16022,20 -1.36022,-20 3.78869,20 -0.98869,-20 3.41715,20 -0.61715,-20 3.04562,20 -0.24562,-20 2.67408,20 0.12592,-20 2.30255,20 0.49745,-20 1.93101,20 0.86899,-20 1.55947,20 1.24053,-20 1.18794,20 0.1519,-20 1.01015,20" + inkscape:connector-curvature="0" /> + <path + id="path69074" + style="fill:none;stroke:#0000ff;stroke-width:0.40000001" + d="m 275.589,366.92289 1.01015,20 1.78985,-20 0.98415,20 1.81585,-20 0.95815,20 1.84185,-20 0.93215,20 1.86785,-20 0.90615,20 1.89385,-20 0.88015,20 1.91985,-20 0.85416,20 1.94584,-20 0.82816,20 1.97184,-20 0.80216,20 1.99784,-20 0.77616,20 2.02384,-20 0.75016,20 2.04984,-20 0.72416,20 2.07584,-20 0.69816,20 2.10184,-20 0.67216,20 2.12784,-20 0.64616,20 2.15384,-20 0.62016,20 2.17984,-20 0.59417,20 2.20583,-20 0.56817,20 2.23183,-20 0.54217,20 2.25783,-20 0.51617,20 2.28383,-20 0.49017,20 2.30983,-20 0.46417,20 2.33583,-20 0.43817,20 2.36183,-20 0.41217,20 2.38783,-20 0.38617,20 2.41383,-20 0.36017,20 2.43983,-20 0.33417,20 2.46583,-20 0.30818,20 2.49182,-20 0.28218,20 2.51782,-20 0.25618,20 2.54382,-20 0.23018,20 2.56982,-20 0.20418,20 2.59582,-20 0.17818,20 2.62182,-20 0.15218,20 2.64782,-20 0.12618,20 2.67382,-20 0.10018,20 2.69982,-20 0.0742,20 2.72582,-20 0.0482,20 2.75181,-20 0.0222,20 2.77781,-20 -1.05306,20 3.85306,-20 -2.1283,20 4.9283,-20 -3.20355,20 6.00351,-20 -4.27879,20 7.07879,-20 -5.35404,20 8.15404,-20 -6.42928,20 9.22928,-20 -7.50453,20 10.30453,-20 -8.57977,20 11.37977,-20 -9.65502,20 12.45502,-20 -10.73026,20 13.53026,-20 -11.80551,20 14.60551,-20 -12.88075,20 15.68075,-20 -13.956,20 16.756,-20 -15.03124,20 17.83124,-20 -16.10649,20 18.90649,-20 -17.18173,20 19.98173,-20 -18.25698,20 20.72853,-19.67155 -20,20.66778 20,-17.86778 -20,19.59253 20,-16.79253 -20,18.51729 20,-15.71729 -20,17.44204 20,-14.64204 -20,16.3668 20,-13.5668 -20,15.29155 20,-12.49155 -20,14.21631 20,-11.41631 -20,13.14106 20,-10.34106 -20,12.06582 20,-9.26582 -20,10.99057 20,-8.19057 -20,9.91533 20,-7.11533 -20,8.84008 20,-6.04008 -20,7.76484 20,-4.96484 -20,6.68959 20,-3.88959 -20,5.61435 20,-2.81435 -20,4.5391 20,-1.7391 -20,3.46386 20,-0.66386 -20,2.38861 20,0.41139 -20,1.31337 20,1.48663 -20,0.23812 20,2.56188 -20,0.23135 20,2.56865 -20,0.22458 20,2.57542 -20,0.21781 20,2.58219 -20,0.21104 20,2.58896 -20,0.20427 20,2.59573 -20,0.1975 20,2.6025 -20,0.19073 20,2.60927 -20,0.18396 20,2.61604 -20,0.17719 20,2.62281 -20,0.17042 20,2.62958 -20,0.16365 20,2.63635 -20,0.15688 20,2.64312 -20,0.15011 20,2.64989 -20,0.14334 20,2.65666 -20,0.13657 20,2.66343 -20,0.1298 20,2.6702 -20,0.12302 20,2.67698 -20,0.11625 20,2.68375 -20,0.10948 20,2.69052 -20,0.10271 20,2.69729 -20,0.0959 20,2.70406 -20,0.0892 20,2.71083 -20,0.0824 20,2.7176 -20,0.0756 20,2.72437 -20,0.0689 20,2.73114 -20,0.0621 20,2.73791 -20,0.0553 20,2.74468 -20,0.0486 20,2.75145 -20,0.0418 20,2.75822 -20,0.035 20,2.76499 -20,0.0282 20,2.77176 -20,0.0215 20,2.77853 -20,0.0147 20,2.7853 -20,0.008 20,2.79208 -20,10e-4 20,2.79885 -20,-1.00316 20,3.80316 -20,-2.00746 20,4.80746 -20,-3.01177 20,5.81177 -20,-4.01608 20,6.81608 -20,-5.02039 20,7.82039 -20,-6.0247 20,8.8247 -20,-7.02901 20,9.82901 -20,-8.03332 20,10.83332 -20,-9.03763 20,11.83763 -20,-10.04194 20,12.84194 -20,-11.04625 20,13.84625 -20,-12.05056 20,14.85056 -20,-13.05487 20,15.85487 -20,-14.05918 20,16.85918 -20,-15.06349 20,17.86349 -20,-16.0678 20,18.8678 -20,-17.07211 20,19.87211 -20.00438,-18.0808 19.12358,20 -20.91927,-20 18.11927,20 -19.91496,-20 17.11496,20 -18.91065,-20 16.11065,20 -17.90634,-20 15.10634,20 -16.90203,-20 14.10203,20 -15.89772,-20 13.09772,20 -14.89341,-20 12.09341,20 -13.8891,-20 11.0891,20 -12.8848,-20 10.0848,20 -11.88049,-20 9.08049,20 -10.87618,-20 8.07618,20 -9.87187,-20 7.07187,20 -8.86756,-20 6.06756,20 -7.86325,-20 5.06325,20 -6.85894,-20 4.05894,20 -5.85463,-20 3.05463,20 -4.85032,-20 2.05032,20 -3.84601,-20 1.04601,20 -2.8417,-20 0.0417,20 -1.83739,-20 -0.96261,20 -0.83308,-20 -1.96692,20 -0.8213,-20 -1.9787,20 -0.80952,-20 -1.99048,20 -0.79774,-20 -2.00226,20 -0.78596,-20 -2.01404,20 -0.77419,-20 -2.02581,20 -0.76241,-20 -2.03759,20 -0.75063,-20 -2.04937,20 -0.73885,-20 -2.06115,20 -0.72707,-20 -2.07293,20 -0.71529,-20 -2.08471,20 -0.70351,-20 -2.09649,20 -0.69174,-20 -2.10826,20 -0.67996,-20 -2.12004,20 -0.66818,-20 -2.13182,20 -0.6564,-20 -2.1436,20 -0.64462,-20 -2.15538,20 -0.63284,-20 -2.16716,20 -0.62106,-20 -2.17894,20 -0.60928,-20 -2.19072,20 -0.59751,-20 -2.20249,20 -0.58573,-20 -2.21427,20 -0.57395,-20 -2.22605,20 -0.56217,-20 -2.23783,20 -0.55039,-20 -2.24961,20 -0.53861,-20 -2.26139,20 -0.52683,-20 -2.27317,20 -0.51505,-20 -2.28495,20 -0.50328,-20 -2.29672,20 -0.4915,-20 -2.3085,20 -0.47972,-20 -2.32028,20 -0.46794,-20 -2.33206,20 -0.45616,-20 -2.34384,20 -0.44438,-20 -2.35562,20 -0.4326,-20 -2.3674,20 -0.42083,-20 -2.37917,20 -0.40905,-20 -2.39095,20 -0.39727,-20 -2.40273,20 -0.38549,-20 -2.41451,20 -0.37371,-20 -2.42629,20 -0.36193,-20 -2.43807,20 -0.35015,-20 -2.44985,20 -0.33837,-20 -2.46163,20 -0.3266,-20 -2.4734,20 -0.31482,-20 -2.48518,20 -0.30304,-20 -2.49696,20 -0.29126,-20 -2.50874,20 -0.27948,-20 -2.52052,20 -0.2677,-20 -2.5323,20 -0.25592,-20 -2.54408,20 -0.24414,-20 -2.55586,20 -0.23237,-20 -2.56763,20 -0.22059,-20 -2.57941,20 -0.20881,-20 -2.59119,20 -0.19703,-20 -2.60297,20 -0.18525,-20 -2.61475,20 -0.17347,-20 -2.62653,20 -0.16169,-20 -2.63831,20 -0.14992,-20 -2.65008,20 -0.13814,-20 -2.66186,20 -0.12636,-20 -2.67364,20 -0.11458,-20 -2.68542,20 -0.1028,-20 -2.6972,20 -0.091,-20 -2.70898,20 -0.0792,-20 -2.72076,20 -0.0675,-20 -2.73254,20 -0.0557,-20 -2.74431,20 -0.0439,-20 -2.75609,20 -0.0321,-20 -2.76787,20 -0.0204,-20 -2.77965,20 -0.009,-20 -2.79143,20 1.14719,-20 -3.94719,20 2.30296,-20 -5.10296,20 3.45872,-20 -6.25872,20 4.61449,-20 -7.41449,20 5.77025,-20 -8.57025,20 6.92602,-20 -9.72602,20 8.08178,-20 -10.88178,20 9.23755,-20 -12.03755,20 10.39331,-20 -13.19331,20 11.54908,-20 -14.34908,20 12.70484,-20 -15.50484,20 13.86061,-20 -16.66061,20 15.01637,-20 -17.81637,20 16.17213,-20 -18.97213,20 17.3279,-20 -20.1279,20 18.48366,-20 -21.28366,20 19.65853,-20.0191 -20,17.56057 20,-19.20481 -20,16.40481 20,-18.04904 -20,15.24904 20,-16.89328 -20,14.09328 20,-15.73751 -20,12.93751 20,-14.58175 -20,11.78175 20,-13.42598 -20,10.62598 20,-12.27022 -20,9.47022 20,-11.11446 -20,8.31446 20,-9.95869 -20,7.15869 20,-8.80293 -20,6.00293 20,-7.64716 -20,4.84716 20,-6.4914 -20,3.6914 20,-5.33563 -20,2.53563 20,-4.17987 -20,1.37987 20,-3.0241 -20,0.2241 20,-1.86834 -20,-0.93166 20,-0.71257 -20,-2.08743 20,-0.69431 -20,-2.10569 20,-0.67604 -20,-2.12396 20,-0.65777 -20,-2.14223 20,-0.63951 -20,-2.16049 20,-0.62124 -20,-2.17876 20,-0.60297 -20,-2.19703 20,-0.58471 -20,-2.21529 20,-0.56644 -20,-2.23356 20,-0.54817 -20,-2.25183 20,-0.52991 -20,-2.27009 20,-0.51164 -20,-2.28836 20,-0.49337 -20,-2.30663 20,-0.47511 -20,-2.32489 20,-0.45684 -20,-2.34316 20,-0.43857 -20,-2.36143 20,-0.42031 -20,-2.37969 20,-0.40204 -20,-2.39796 20,-0.38377 -20,-2.41623 20,-0.36551 -20,-2.43449 20,-0.34724 -20,-2.45276 20,-0.32897 -20,-2.47103 20,-0.31071 -20,-2.48929 20,-0.29244 -20,-2.50756 20,-0.27417 -20,-2.52583 20,-0.25591 -20,-2.54409 20,-0.23764 -20,-2.56236 20,-0.21938 -20,-2.58062 20,-0.20111 -20,-2.59889 20,-0.18284 -20,-2.61716 20,-0.16458 -20,-2.63542 20,-0.14631 -20,-2.65369 20,-0.12804 -20,-2.67196 20,-0.10978 -20,-2.69022 20,-0.0915 -20,-2.70849 20,-0.0732 -20,-2.72676 20,-0.055 -20,-2.74502 20,-0.0367 -20,-2.76329 20,-0.0184 -20,-2.78156 20,-1.8e-4 -20,-2.79982 20,1.14343 -20,-3.94343 20,2.28703 -20,-5.08703 20,3.43063 -20,-6.23063 20,4.57423 -20,-7.37423 20,5.71784 -20,-8.51784 20,6.86144 -20,-9.66144 20,8.00504 -20,-10.80504 20,9.14864 -20,-11.94864 20,10.29225 -20,-13.09225 20,11.43585 -20,-14.23585 20,12.57945 -20,-15.37945 20,13.72305 -20,-16.52306 20,14.86666 -20,-17.66666 20,16.01027 -20,-18.81027 20,17.15387 -20,-19.95387 20,18.29747 -19.78912,-20.88658 20.55893,20 -17.75893,-20 19.41533,20 -16.61533,-20 18.27173,20 -15.47173,-20 17.12813,20 -14.32813,-20 15.98452,20 -13.18452,-20 14.84092,20 -12.04092,-20 13.69732,20 -10.89732,-20 12.55372,20 -9.75372,-20 11.41012,20 -8.61012,-20 10.26651,20 -7.46651,-20 9.12291,20 -6.32291,-20 7.97931,20 -5.17931,-20 6.83571,20 -4.03571,-20 5.6921,20 -2.8921,-20 4.5485,20 -1.7485,-20 3.4049,20 -0.6049,-20 2.2613,20 0.5387,-20 1.11769,20 1.68231,-20 1.11457,20 1.68543,-20 1.11146,20 1.68854,-20 1.10834,20 1.69166,-20 1.10522,20 1.69478,-20 1.1021,20 1.6979,-20 1.09898,20 1.70102,-20 1.09586,20 1.70414,-20 1.09274,20 1.70726,-20 1.08962,20 1.71038,-20 1.0865,20 1.7135,-20 1.08338,20 1.71662,-20 1.08027,20 1.71973,-20 1.07715,20 1.72285,-20 1.07403,20 1.72597,-20 1.07091,20 1.72909,-20 1.06779,20 1.73221,-20 1.06467,20 1.73533,-20 1.06155,20 1.73845,-20 1.05843,20 1.74157,-20 1.05531,20 1.74469,-20 1.05219,20 1.74781,-20 1.04908,20 1.75092,-20 1.04596,20 1.75404,-20 1.04284,20 1.75716,-20 1.03972,20 1.76028,-20 1.0366,20 1.7634,-20 1.03348,20 1.76652,-20 1.03036,20 1.76964,-20 1.02724,20 1.77276,-20 1.02412,20 1.77588,-20 1.021,20 1.779,-20 1.01788,20 1.78212,-20 1.01477,20 1.78523,-20 1.01165,20 0.32819,-20 1.01015,20" + inkscape:connector-curvature="0" /> + </g> +</svg> diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..c7132aa2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +backports.functools_lru_cache +wxPython +networkx +shapely +lxml +appdirs |
