diff --git a/extensions/fablabchemnitz_chain_paths.py b/extensions/fablabchemnitz_chain_paths.py index e468e3fc..1bdbbe64 100644 --- a/extensions/fablabchemnitz_chain_paths.py +++ b/extensions/fablabchemnitz_chain_paths.py @@ -28,31 +28,13 @@ __version__ = '0.7' # Keep in sync with chain_paths.inx ca line 22 __author__ = 'Juergen Weigert ' __credits__ = ['Juergen Weigert', 'Veronika Irvine'] -import sys, os, shutil, time, logging, tempfile, math +import sys +import math import re - -#debug = True -debug = False - -# search path, so that inkscape libraries are found when we are standalone. -sys_platform = sys.platform.lower() -if sys_platform.startswith('win'): # windows - sys.path.append('C:\Program Files\Inkscape\share\extensions') -elif sys_platform.startswith('darwin'): # mac - sys.path.append('/Applications/Inkscape.app/Contents/Resources/extensions') -else: # linux - # if sys_platform.startswith('linux'): - sys.path.append('/usr/share/inkscape/extensions') - -# inkscape libraries import inkex - inkex.localization.localize() - from optparse import SUPPRESS_HELP - -def uutounit(self, nn, uu): - return self.svg.unittouu(str(nn)+uu) +debug = False class ChainPaths(inkex.Effect): @@ -86,7 +68,7 @@ class ChainPaths(inkex.Effect): - The document units are always irrelevant as everything in inkscape is expected to be in 90dpi pixel units """ - dialog_units = uutounit(self, 1.0, units) + dialog_units = self.svg.unittouu(str(1.0)+units) self.unit_factor = 1.0 / dialog_units return self.unit_factor @@ -237,15 +219,15 @@ class ChainPaths(inkex.Effect): if self.near_ends(end1, seg['end2']): # prepend seg to chain - self.set_segment_done(seg['id'], seg['n'], 'prepended to ' + id + ' ' + str(cur_idx)) + self.set_segment_done(seg['id'], seg['n'], 'prepended to ' + str(id) + ' ' + str(cur_idx)) chain = self.link_segments(seg['seg'], chain) end1 = [chain[0][1][0], chain[0][1][1]] segments_idx = 0 # this chain changed. re-visit all candidate continue if self.near_ends(end2, seg['end1']): - # append seg to chain - self.set_segment_done(seg['id'], seg['n'], 'appended to ' + id + ' ' + str(cur_idx)) + # append seg to chain + self.set_segment_done(seg['id'], seg['n'], 'appended to ' + str(id) + ' ' + str(cur_idx)) chain = self.link_segments(chain, seg['seg']) end2 = [chain[-1][1][0], chain[-1][1][1]] segments_idx = 0 # this chain changed. re-visit all candidate diff --git a/extensions/fablabchemnitz_pixel2svg.inx b/extensions/fablabchemnitz_pixel2svg.inx new file mode 100644 index 00000000..4af65d98 --- /dev/null +++ b/extensions/fablabchemnitz_pixel2svg.inx @@ -0,0 +1,45 @@ + + + Pixel2SVG + fablabchemnitz.de.pixel2svg + + + 5 + true + false + + + true + false + false + 256 + + + + Trace all colors. + Don't trace this color: + Only trace this color: + + FFFFFF + + + <_param name="instructions" type="description" xml:space="preserve">This extension is based on: +pixel2svg - converts pixel art to SVG - pixel by pixel. +Copyright 2011 Florian Berger +http://florian-berger.de/en/software/pixel2svg + + + + + image + + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz_pixel2svg.py b/extensions/fablabchemnitz_pixel2svg.py new file mode 100644 index 00000000..419a14d6 --- /dev/null +++ b/extensions/fablabchemnitz_pixel2svg.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +""" +Pixel2SVG - Convert the pixels of bitmap images to SVG rects + +Idea and original implementation as standalone script: +Copyright (C) 2011 Florian Berger +Homepage: + +Rewritten as Inkscape extension: +Copyright (C) 2012 ~suv + +'getFilePath()' is based on code from 'extractimages.py': +Copyright (C) 2005 Aaron Spike, aaron@ekips.org + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import os +import sys +import base64 +from io import StringIO, BytesIO +import urllib.parse +import urllib.request +import inkex +from PIL import Image +from lxml import etree + +inkex.localization.localize + +DEBUG = False + + +# int r = ( hexcolor >> 16 ) & 0xFF; +# int g = ( hexcolor >> 8 ) & 0xFF; +# int b = hexcolor & 0xFF; +# int hexcolor = (r << 16) + (g << 8) + b; + +def hex_to_int_color(v): + if (v[0] == '#'): + v = v[1:] + assert(len(v) == 6) + return int(v[:2], 16), int(v[2:4], 16), int(v[4:6], 16) + + +class Pixel2SVG(inkex.Effect): + def __init__(self): + inkex.Effect.__init__(self) + # pixel2svg options + self.arg_parser.add_argument("-s", "--squaresize", type=int, default="5", help="Width and height of vector squares in pixels") + self.arg_parser.add_argument("--transparency", type=inkex.Boolean, default=True, help="Convert transparency to 'fill-opacity'") + self.arg_parser.add_argument("--overlap", type=inkex.Boolean, default=False, help="Overlap vector squares by 1px") + self.arg_parser.add_argument("--offset_image", type=inkex.Boolean, default=True, help="Offset traced image") + self.arg_parser.add_argument("--delete_image", type=inkex.Boolean, default=False, help="Delete bitmap image") + self.arg_parser.add_argument("--maxsize", type=int, default="256", help="Max. image size (width or height)") + self.arg_parser.add_argument("--verbose", type=inkex.Boolean, default=False) + self.arg_parser.add_argument("--color_mode", default="all", help="Which colors to trace.") + self.arg_parser.add_argument("--color", default="FFFFFF", help="Special color") + self.arg_parser.add_argument("--tab") + + def getImagePath(self, node, xlink): + """ + Find image file, return path + """ + absref = node.get(inkex.addNS('absref', 'sodipodi')) + url = urlparse(xlink) + href = urllib.request.url2pathname + + path = '' + #path selection strategy: + # 1. href if absolute + # 2. realpath-ified href + # 3. absref, only if the above does not point to a file + if href is not None: + path = os.path.realpath(href) + if (not os.path.isfile(path)): + if absref is not None: + path = absref + + try: + path = unicode(path, "utf-8") + except TypeError: + path = path + + if (not os.path.isfile(path)): + inkex.errormsg(_( + "No xlink:href or sodipodi:absref attributes found, " + + "or they do not point to an existing file! Unable to find image file.")) + if path: + inkex.errormsg(_("Sorry we could not locate %s") % str(path)) + return False + + if (os.path.isfile(path)): + return path + + def getImageData(self, xlink): + """ + Read, decode and return data of embedded image + """ + comma = xlink.find(',') + data = '' + + if comma > 0: + data = base64.decodebytes(xlink[comma:].encode('UTF-8')) + else: + inkex.errormsg(_("Failed to read embedded image data.")) + + return data + + def getImage(self, node): + image_element=self.svg.find('.//{http://www.w3.org/2000/svg}image') + image_string=image_element.get('{http://www.w3.org/1999/xlink}href') + #find comma position + i=0 + while i<40: + if image_string[i]==',': + break + i=i+1 + return Image.open(BytesIO(base64.b64decode(image_string[i+1:len(image_string)]))) + + def drawFilledRect(self, parent, svgpx): + """ + Draw rect based on ((x, y), (width,height), ((r,g,b),a)), add to parent + """ + style = {} + pos = svgpx[0] + dim = svgpx[1] + rgb = svgpx[2][0] + alpha = svgpx[2][1] + + style['stroke'] = 'none' + + if len(rgb) == 3: + # fill: rgb tuple + style['fill'] = '#%02x%02x%02x' % (rgb[0], rgb[1], rgb[2]) + elif len(rgb) == 1: + # fill: color name, or 'none' + style['fill'] = rgb[0] + else: + # fill: 'Unset' (no fill defined) + pass + + if alpha < 255: + # only write 'fill-opacity' for non-default value + style['fill-opacity'] = '%s' % round(alpha/255.0, 8) + + rect_attribs = {'x': str(pos[0]), + 'y': str(pos[1]), + 'width': str(dim[0]), + 'height': str(dim[1]), + 'style': str(inkex.Style(style)), } + + rect = etree.SubElement(parent, inkex.addNS('rect', 'svg'), rect_attribs) + + return rect + + def vectorizeImage(self, node): + """ + Parse RGBA values of linked bitmap image, create a group and + draw the rectangles (SVG pixels) inside the new group + """ + image = self.getImage(node) + + if image: + # init, set limit (default: 256) + pixel2svg_max = self.options.maxsize + + if self.options.verbose: + inkex.debug("ID: %s" % node.get('id')) + inkex.debug("Image size:\t%dx%d" % image.size) + inkex.debug("Image format:\t%s" % image.format) + inkex.debug("Image mode:\t%s" % image.mode) + inkex.debug("Image info:\t%s" % image.info) + + if (image.mode == 'P' and 'transparency' in image.info): + inkex.debug( + "Note: paletted image with an alpha channel is handled badly with " + + "current PIL:\n" + + "") + elif not image.mode in ('RGBA', 'LA'): + inkex.debug("No alpha channel or transparency found") + + image = image.convert("RGBA") + (width, height) = image.size + + if width <= pixel2svg_max and height <= pixel2svg_max: + + # color trace modes + trace_color = [] + if self.options.color: + trace_color = hex_to_int_color(self.options.color) + + # get RGBA data + rgba_values = list(image.getdata()) + + # create group + nodeParent = node.getparent() + nodeIndex = nodeParent.index(node) + pixel2svg_group = etree.Element(inkex.addNS('g', 'svg')) + pixel2svg_group.set('id', "%s_pixel2svg" % node.get('id')) + nodeParent.insert(nodeIndex+1, pixel2svg_group) + + # move group beside original image + if self.options.offset_image: + pixel2svg_offset = width + else: + pixel2svg_offset = 0.0 + pixel2svg_translate = ('translate(%s, %s)' % + (float(node.get('x') or 0.0) + pixel2svg_offset, + node.get('y') or 0.0)) + pixel2svg_group.set('transform', pixel2svg_translate) + + # draw bbox rectangle at the bottom of group + pixel2svg_bbox_fill = ('none', ) + pixel2svg_bbox_alpha = 255 + pixel2svg_bbox = ((0, 0), + (width * self.options.squaresize, + height * self.options.squaresize), + (pixel2svg_bbox_fill, pixel2svg_bbox_alpha)) + self.drawFilledRect(pixel2svg_group, pixel2svg_bbox) + + # reverse list (performance), pop last one instead of first + rgba_values.reverse() + # loop through pixels (per row) + rowcount = 0 + while rowcount < height: + colcount = 0 + while colcount < width: + rgba_tuple = rgba_values.pop() + # Omit transparent pixels + if rgba_tuple[3] > 0: + # color options + do_trace = True + if (self.options.color_mode != "all"): + if (trace_color == rgba_tuple[:3]): + # colors match + if (self.options.color_mode == "other"): + do_trace = False + else: + # colors don't match + if (self.options.color_mode == "this"): + do_trace = False + if do_trace: + # position + svgpx_x = colcount * self.options.squaresize + svgpx_y = rowcount * self.options.squaresize + # dimension + overlap + svgpx_size = self.options.squaresize + self.options.overlap + # get color, ignore alpha + svgpx_rgb = rgba_tuple[:3] + svgpx_a = 255 + # transparency + if self.options.transparency: + svgpx_a = rgba_tuple[3] + svgpx = ((svgpx_x, svgpx_y), + (svgpx_size, svgpx_size), + (svgpx_rgb, svgpx_a) + ) + # draw square in group + self.drawFilledRect(pixel2svg_group, svgpx) + colcount = colcount + 1 + rowcount = rowcount + 1 + + # all done + if DEBUG: + inkex.debug("All rects drawn.") + + if self.options.delete_image: + nodeParent.remove(node) + + else: + # bail out with larger images + inkex.errormsg(_( + "Bailing out: this extension is not intended for large images.\n" + + "The current limit is %spx for either dimension of the bitmap image." + % pixel2svg_max)) + sys.exit(0) + + # clean-up? + if DEBUG: + inkex.debug("Done.") + + else: + inkex.errormsg(_("Bailing out: No supported image file or data found")) + sys.exit(1) + + def effect(self): + """ + Pixel2SVG - Convert the pixels of bitmap images to SVG rects + """ + found_image = False + if (self.options.ids): + for node in self.svg.selected.values(): + if node.tag == inkex.addNS('image', 'svg'): + found_image = True + self.vectorizeImage(node) + + if not found_image: + inkex.errormsg(_("Please select one or more bitmap image(s) for Pixel2SVG")) + sys.exit(0) + +if __name__ == '__main__': + Pixel2SVG().run() \ No newline at end of file diff --git a/extensions/fablabchemnitz_reorder_sequence.py b/extensions/fablabchemnitz_reorder_sequence.py new file mode 100644 index 00000000..4da2f7a6 --- /dev/null +++ b/extensions/fablabchemnitz_reorder_sequence.py @@ -0,0 +1,1240 @@ +#!/usr/bin/env python3 +# +# SVG Path Ordering Extension +# This extension uses a simple TSP algorithm to order the paths so as +# to reduce plotting time by plotting nearby paths consecutively. +# +# Copyright 2019, Windell H. Oskay, Evil Mad Science LLC +# www.evilmadscientist.com +# +# +# While written from scratch, this is a derivative in spirit of the work by +# Matthew Beckler and Daniel C. Newman for the EggBot project. +# +# The MIT License (MIT) +# +# Copyright (c) 2019 Windell H. Oskay, Evil Mad Scientist Laboratories +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import sys +import inkex +import gettext +import math +import plot_utils # https://github.com/evil-mad/plotink Requires version 0.15 +from lxml import etree +from inkex import Transform + +""" +TODOs: + +* Apparent difference in execution time for portrait vs landscape document orientation. + Seems to be related to the _change_ + +* Implement path functions + + +<_option value=0>Leave as is +<_option value=1>Reorder subpaths +<_option value=2>Break apart + + +self.OptionParser.add_option( "--path_handling",\ +action="store", type="int", dest="path_handling",\ +default=1,help="How compound paths are handled") + + +* Consider re-introducing GUI method for rendering: + +false + + +""" + +class ReorderEffect(inkex.Effect): + """ + Inkscape effect extension. + Re-order the objects in the SVG document for faster plotting. + Respect layers: Initialize a new dictionary of objects for each layer, and sort + objects within that layer only + Objects in root of document are treated as being on a _single_ layer, and will all + be sorted. + + """ + + def __init__( self ): + inkex.Effect.__init__( self ) + + self.arg_parser.add_argument("--reordering", type=int, default=1, help="How groups are handled") + + self.auto_rotate = False + + def effect(self): + # Main entry point of the program + + self.svg_width = 0 + self.svg_height = 0 + self.air_total_default = 0 + self.air_total_sorted = 0 + self.printPortrait = False + + # Rendering is available for debug purposes. It only previews + # pen-up movements that are reordered and typically does not + # include all possible movement. + + self.preview_rendering = False + self.layer_index = 0 # index for coloring layers + + self.svg = self.document.getroot() + + self.DocUnits = "in" # Default + self.DocUnits = self.svg.unit + + self.unit_scaling = 1 + + self.getDocProps() + + """ + Set up the document-wide transforms to handle SVG viewbox + """ + + matCurrent = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] + + viewbox = self.svg.get( 'viewBox' ) + + vb = self.svg.get('viewBox') + if vb: + p_a_r = self.svg.get('preserveAspectRatio') + sx,sy,ox,oy = plot_utils.vb_scale(vb, p_a_r, self.svg_width, self.svg_height) + else: + sx = 1.0 / float(plot_utils.PX_PER_INCH) # Handle case of no viewbox + sy = sx + ox = 0.0 + oy = 0.0 + + # Initial transform of document is based on viewbox, if present: + matCurrent = Transform('scale(' + str(sx) + ',' + str(sy) +') translate(' + str(ox) + ',' + str(oy) +')').matrix + # Set up x_last, y_last, which keep track of last known pen position + # The initial position is given by the expected initial pen position + + self.y_last = 0 + + if (self.printPortrait): + self.x_last = self.svg_width + else: + self.x_last = 0 + + parent_vis='visible' + + self.root_nodes = [] + + if self.preview_rendering: + # Remove old preview layers, if rendering is enabled + for node in self.svg: + if node.tag == inkex.addNS( 'g', 'svg' ) or node.tag == 'g': + if ( node.get( inkex.addNS( 'groupmode', 'inkscape' ) ) == 'layer' ): + LayerName = node.get( inkex.addNS( 'label', 'inkscape' ) ) + if LayerName == '% Preview': + self.svg.remove( node ) + + preview_transform = Transform( + 'translate({2:.6E},{3:.6E}) scale({0:.6E},{1:.6E})'.format( + 1.0/sx, 1.0/sy, -ox, -oy)).matrix + path_attrs = { 'transform': str(Transform(preview_transform))} + self.preview_layer = etree.Element(inkex.addNS('g', 'svg'), + path_attrs, nsmap=inkex.NSS) + + + self.preview_layer.set( inkex.addNS('groupmode', 'inkscape' ), 'layer' ) + self.preview_layer.set( inkex.addNS( 'label', 'inkscape' ), '% Preview' ) + self.svg.append( self.preview_layer ) + + + # Preview stroke width: 1/1000 of page width or height, whichever is smaller + if self.svg_width < self.svg_height: + width_du = self.svg_width / 1000.0 + else: + width_du = self.svg_height / 1000.0 + + """ + Stroke-width is a css style element, and cannot accept scientific notation. + + Thus, in cases with large scaling (i.e., high values of 1/sx, 1/sy) + resulting from the viewbox attribute of the SVG document, it may be necessary to use + a _very small_ stroke width, so that the stroke width displayed on the screen + has a reasonable width after being displayed greatly magnified thanks to the viewbox. + + Use log10(the number) to determine the scale, and thus the precision needed. + """ + + log_ten = math.log10(width_du) + if log_ten > 0: # For width_du > 1 + width_string = "{0:.3f}".format(width_du) + else: + prec = int(math.ceil(-log_ten) + 3) + width_string = "{0:.{1}f}".format(width_du, prec) + + self.p_style = {'stroke-width': width_string, 'fill': 'none', + 'stroke-linejoin': 'round', 'stroke-linecap': 'round'} + + self.svg = self.parse_svg(self.svg, matCurrent) + + + def parse_svg(self, input_node, mat_current=None, parent_vis='visible'): + """ + Input: An SVG node (usually) containing other nodes: + The SVG root, a layer, sublayer, or other group. + Output: The re-ordered node. The contents are reordered with the greedy + algorithm, except: + - Layers and sublayers are preserved. The contents of each are + re-ordered for faster plotting. + - Groups are either preserved, broken apart, or re-ordered within + the group, depending on the value of group_mode. + """ + + coord_dict = {} + # coord_dict maps a node ID to the following data: + # Is the node plottable, first coordinate pair, last coordinate pair. + # i.e., Node_id -> (Boolean: plottable, Xi, Yi, Xf, Yf) + + group_dict = {} + # group_dict maps a node ID for a group to the contents of that group. + # The contents may be a preserved nested group or a flat list, depending + # on the selected group handling mode. Example: + # group_dict = {'id_1': , + # 'id_2': + + nodes_to_delete = [] + + counter = 0 # TODO: Replace this with better unique ID system + + # Account for input_node's transform and any transforms above it: + if mat_current is None: + mat_current = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] + try: + matNew = Transform(mat_current) * Transform(input_node.get("transform")) + except AttributeError: + matNew = mat_current + + for node in input_node: + # Step through each object within the top-level input node + + try: + id = node.get( 'id' ) + except AttributeError: + id = self.uniqueId(None,True) + + # First check for object visibility: + skip_object = False + + # Check for "display:none" in the node's style attribute: + style = dict(inkex.Style.parse_str(node.get('style'))) + if 'display' in style.keys() and style['display'] == 'none': + skip_object = True # Plot neither this object nor its children + + # The node may have a display="none" attribute as well: + if node.get( 'display' ) == 'none': + skip_object = True # Plot neither this object nor its children + + # Visibility attributes control whether a given object will plot. + # Children of hidden (not visible) parents may be plotted if + # they assert visibility. + visibility = node.get( 'visibility', parent_vis ) + + if 'visibility' in style.keys(): + visibility = style['visibility'] # Style may override attribute. + + if visibility == 'inherit': + visibility = parent_vis + + if visibility != 'visible': + skip_object = True # Skip this object and its children + + # Next, check to see if this inner node is itself a group or layer: + if node.tag == inkex.addNS( 'g', 'svg' ) or node.tag == 'g': + + # Use the user-given option to decide what to do with subgroups: + subgroup_mode = self.options.reordering + +# Values of the parameter: +# subgroup_mode=="1": Preserve groups +# subgroup_mode=="2": Reorder within groups +# subgroup_mode=="3": Break apart groups + + if node.get(inkex.addNS('groupmode', 'inkscape')) == 'layer': + # The node is a layer or sub-layer, not a regular group. + # Parse it separately, and re-order its contents. + + subgroup_mode = 2 # Always sort within each layer. + self.layer_index += 1 + + layer_name = node.get( inkex.addNS( 'label', 'inkscape' ) ) + + if sys.version_info < (3,): # Yes this is ugly. More elegant suggestions welcome. :) + layer_name = layer_name.encode( 'ascii', 'ignore' ) #Drop non-ascii characters + else: + layer_name = str(layer_name) + layer_name.lstrip # Remove leading whitespace + + if layer_name: + if layer_name[0] == '%': # First character is '%'; This + skip_object = True # is a documentation layer; skip plotting. + self.layer_index -= 1 # Set this back to previous value. + + if skip_object: + # Do not re-order hidden groups or layers. + subgroup_mode = 1 # Preserve this group + + if subgroup_mode == 3: + # Break apart this non-layer subgroup and add it to + # the set of things to be re-ordered. + + nodes_to_delete.append(node) + nodes_inside_group = self.group2NodeDict(node) + + for a_node in nodes_inside_group: + try: + id = a_node.get( 'id' ) + except AttributeError: + id = self.uniqueId(None,True) + + # Use getFirstPoint and getLastPoint on each object: + start_plottable, first_point = self.getFirstPoint(a_node, matNew) + end_plottable, last_point = self.getLastPoint(a_node, matNew) + + coord_dict[id] = (start_plottable and end_plottable, + first_point[0], first_point[1], last_point[0], last_point[1] ) + # Entry in group_dict is this node + group_dict[id] = a_node + + elif subgroup_mode == 2: + # Reorder a layer or subgroup with a recursive call. + + node = self.parse_svg(node, matNew, visibility) + + # Capture the first and last x,y coordinates of the optimized node + start_plottable, first_point = self.group_first_pt(node, matNew) + end_plottable, last_point = self.group_last_pt(node, matNew) + + # Then add this optimized node to the coord_dict + coord_dict[id] = (start_plottable and end_plottable, + first_point[0], first_point[1], last_point[0], last_point[1] ) + # Entry in group_dict is this node + group_dict[id] = node + + else: # (subgroup_mode == 1) + # Preserve the group, but find its first and last point so + # that it can be re-ordered with respect to other items + + if skip_object: + start_plottable = False + end_plottable = False + first_point = [(-1.), (-1.)] + last_point = [(-1.), (-1.)] + else: + start_plottable, first_point = self.group_first_pt(node, matNew) + end_plottable, last_point = self.group_last_pt(node, matNew) + + coord_dict[id] = (start_plottable and end_plottable, + first_point[0], first_point[1], last_point[0], last_point[1] ) + # Entry in group_dict is this node + group_dict[id] = node + + else: # Handle objects that are not groups + if skip_object: + start_plottable = False + end_plottable = False + first_point = [(-1.), (-1.)] + last_point = [(-1.), (-1.)] + else: + start_plottable, first_point = self.getFirstPoint(node, matNew) + end_plottable, last_point = self.getLastPoint(node, matNew) + + coord_dict[id] = (start_plottable and end_plottable, + first_point[0], first_point[1], last_point[0], last_point[1] ) + group_dict[id] = node # Entry in group_dict is this node + + # Perform the re-ordering: + ordered_element_list = self.ReorderNodeList(coord_dict, group_dict) + + # Once a better order for the svg elements has been determined, + # All there is do to is to reintroduce the nodes to the parent in the correct order + for elt in ordered_element_list: + # Creates identical node at the correct location according to ordered_element_list + input_node.append(elt) + # Once program is finished parsing through + for element_to_remove in nodes_to_delete: + try: + input_node.remove(element_to_remove) + except ValueError: + inkex.errormsg(str(element_to_remove.get('id'))+" is not a member of " + str(input_node.get('id'))) + + return input_node + + + def break_apart_path(self, path): + """ + An SVG path may contain multiple distinct portions, that are normally separated + by pen-up movements. + + This function takes the path data string from an SVG path, parses it, and returns + a dictionary of independent path data strings, each of which represents a single + pen-down movement. It is equivalent to the Inkscape function Path > Break Apart + + Input: path data string, representing a single SVG path + Output: Dictionary of (separated) path data strings + + """ + MaxLength = len(path) + ix = 0 + move_to_location = [] + path_dictionary = {} + path_list = [] + path_number = 1 + + # Search for M or m location + while ix < MaxLength: + if(path[ix] == 'm' or path[ix] == 'M'): + move_to_location.append(ix) + ix = ix + 1 + # Iterate through every M or m location in our list of move to instructions + # Slice the path string according to path beginning and ends as indicated by the + # location of these instructions + + for counter, m in enumerate(move_to_location): + if (m == move_to_location[-1]): + # last entry + path_list.append(path[m:MaxLength].rstrip()) + else: + path_list.append(path[m:move_to_location[counter + 1]].rstrip()) + + for counter, current_path in enumerate(path_list): + + # Enumerate over every entry in the path looking for relative m commands + if current_path[0] == 'm' and counter > 0: + # If path contains relative m command, the best case is when the last command + # was a Z or z. In this case, all relative operations are performed relative to + # initial x, y coordinates of the previous path + + if path_list[counter -1][-1].upper() == 'Z': + current_path_x, current_path_y,index = self.getFirstPoint(current_path, matNew) + prev_path_x, prev_path_y,ignore = self.getFirstPoint(path_list[counter-1]) + adapted_x = current_path_x + prev_path_x + adapted_y = current_path_y + prev_path_y + # Now we can replace the path data with an Absolute Move to instruction + # HOWEVER, we need to adapt all the data until we reach a different command in the case of a repeating + path_list[counter] = "m "+str(adapted_x)+","+str(adapted_y) + ' ' +current_path[index:] + + # If there is no z or absolute commands, we need to parse the entire path + else: + + # scan path for absolute coordinates. If present, begin parsing from their index + # instead of the beginning + prev_path = path_list[counter-1] + prev_path_length = len(prev_path) + jx = 0 + x_val, y_val = 0,0 + # Check one char at a time + # until we have the moveTo Command + last_command = '' + is_absolute_command = False + repeated_command = False + # name of command + # how many parameters we need to skip + accepted_commands = { + 'M':0, + 'L':0, + 'H':0, + 'V':0, + 'C':4, + 'S':2, + 'Q':2, + 'T':0, + 'A':5 + } + + # If there is an absolute command which specifies a new initial point + # then we can save time by setting our index directly to its location in the path data + # See if an accepted_command is present in the path data. If it is present further in the + # string than any command found before, then set the pointer to that location + # if a command is not found, find() will return a -1. jx is initialized to 0, so if no matches + # are found, the program will parse from the beginning to the end of the path + + for keys in 'MLCSQTA': # TODO: Compare to last_point; see if we can clean up this part + if(prev_path.find(keys) > jx): + jx = prev_path.find(keys) + + while jx < prev_path_length: + + temp_x_val = '' + temp_y_val = '' + num_of_params_to_skip = 0 + + # SVG Path commands can be repeated + if (prev_path[jx].isdigit() and last_command): + repeated_command = True + else: + repeated_command = False + + if (prev_path[jx].isalpha() and prev_path[jx].upper() in accepted_commands) or repeated_command: + + if repeated_command: + #is_relative_command is saved from last iteration of the loop + current_command = last_command + else: + # If the character is accepted, we must parse until reach the x y coordinates + is_absolute_command = prev_path[jx].isupper() + current_command = prev_path[jx].upper() + + # Each command has a certain number of parameters we must pass before we reach the + # information we care about. We will parse until we know that we have reached them + + # Get to start of next number + # We will know we have reached a number if the current character is a +/- sign + # or current character is a digit + while jx < prev_path_length: + if(prev_path[jx] in '+-' or prev_path[jx].isdigit()): + break + jx = jx + 1 + + # We need to parse past the unused parameters in our command + # The number of parameters to parse past is dependent on the command and stored + # as the value of accepted_command + # Spaces and commas are used to deliniate paramters + while jx < prev_path_length and num_of_params_to_skip < accepted_commands[current_command]: + if(prev_path[jx].isspace() or prev_path[jx] == ','): + num_of_params_to_skip = num_of_params_to_skip + 1 + jx = jx + 1 + + # Now, we are in front of the x character + + if current_command.upper() == 'V': + temp_x_val = 0 + + if current_command.upper() == 'H': + temp_y_val = 0 + + # Parse until next character is a digit or +/- character + while jx < prev_path_length and current_command.upper() != 'V': + if(prev_path[jx] in '+-' or prev_path[jx].isdigit()): + break + jx = jx + 1 + + # Save each next character until we reach a space + while jx < prev_path_length and current_command.upper() != 'V' and not (prev_path[jx].isspace() or prev_path[jx] == ','): + temp_x_val = temp_x_val + prev_path[jx] + jx = jx + 1 + + # Then we know we have completely parsed the x character + + # Now we are in front of the y character + + # Parse until next character is a digit or +/- character + while jx < prev_path_length and current_command.upper() != 'H': + if(prev_path[jx] in '+-' or prev_path[jx].isdigit()): + break + jx = jx + 1 + + ## Save each next character until we reach a space + while jx < prev_path_length and current_command.upper() != 'H' and not (prev_path[jx].isspace() or prev_path[jx] == ','): + temp_y_val = temp_y_val + prev_path[jx] + jx = jx + 1 + + # Then we know we have completely parsed the y character + + if is_absolute_command: + + if current_command == 'H': + # Absolute commands create new x,y position + try: + x_val = float(temp_x_val) + except ValueError: + pass + elif current_command == 'V': + # Absolute commands create new x,y position + try: + y_val = float(temp_y_val) + except ValueError: + pass + else: + # Absolute commands create new x,y position + try: + x_val = float(temp_x_val) + y_val = float(temp_y_val) + except ValueError: + pass + else: + + if current_command == 'h': + # Absolute commands create new x,y position + try: + x_val = x_val + float(temp_x_val) + except ValueError: + pass + elif current_command == 'V': + # Absolute commands create new x,y position + try: + y_val = y_val + float(temp_y_val) + except ValueError: + pass + else: + # Absolute commands create new x,y position + try: + x_val = x_val + float(temp_x_val) + y_val = y_val + float(temp_y_val) + except ValueError: + pass + last_command = current_command + jx = jx + 1 + x,y,index = self.getFirstPoint(current_path,None) + path_list[counter] = "m "+str(x_val+x)+","+str(y_val+y) + ' ' + current_path[index:] + + for counter, path in enumerate(path_list): + path_dictionary['AxiDraw_Path'+ str(counter)] = path + + return path_dictionary + + + def getFirstPoint(self, node, matCurrent): + """ + Input: (non-group) node and parent transformation matrix + Output: Boolean value to indicate if the svg element is plottable and + two floats stored in a list representing the x and y coordinates we plot first + """ + + # first apply the current matrix transform to this node's transform + matNew = Transform(matCurrent) * Transform(Transform(node.get("transform")).matrix) + + point = [float(-1), float(-1)] + try: + if node.tag == inkex.addNS( 'path', 'svg' ): + + pathdata = node.get('d') + + point = plot_utils.pathdata_first_point(pathdata) + Transform(matNew).apply_to_point(point) + + return True, point + + if node.tag == inkex.addNS( 'rect', 'svg' ) or node.tag == 'rect': + + """ + The x,y coordinates for a rect are included in their specific attributes + If there is a transform, we need translate the x & y coordinates to their + correct location via Transform(matNew).apply_to_point(point). + """ + + point[0] = float( node.get( 'x' ) ) + point[1] = float( node.get( 'y' ) ) + + Transform(matNew).apply_to_point(point) + + return True, point + + if node.tag == inkex.addNS( 'line', 'svg' ) or node.tag == 'line': + """ + The x1 and y1 attributes are where we will start to draw + So, get them, apply the transform matrix, and return the point + """ + + point[0] = float( node.get( 'x1' ) ) + point[1] = float( node.get( 'y1' ) ) + + Transform(matNew).apply_to_point(point) + + return True, point + + + if node.tag == inkex.addNS( 'polyline', 'svg' ) or node.tag == 'polyline': + pl = node.get( 'points', '' ).strip() + + if pl == '': + return False, point + + pa = pl.replace(',',' ').split() # replace comma with space before splitting + + if not pa: + return False, point + pathLength = len( pa ) + if (pathLength < 4): # Minimum of x1,y1 x2,y2 required. + return False, point + + d = "M " + pa[0] + " " + pa[1] + i = 2 + while (i < (pathLength - 1 )): + d += " L " + pa[i] + " " + pa[i + 1] + i += 2 + + point = plot_utils.pathdata_first_point(d) + Transform(matNew).apply_to_point(point) + + return True, point + + if (node.tag == inkex.addNS( 'polygon', 'svg' ) or + node.tag == 'polygon'): + """ + We need to extract x1 and y1 from these: + + We accomplish this with Python string strip + and split methods. Then apply transforms + """ + # Strip() removes all whitespace from the start and end of p1 + pl = node.get( 'points', '' ).strip() + if (pl == ''): + # If pl is blank there has been an error, return False and -1,-1 to indicate a problem has occured + return False, point + # Split string by whitespace + pa = pl.split() + if not len( pa ): + # If pa is blank there has been an error, return False and -1,-1 to indicate a problem has occured + return False, point + # pa[0] = "x1,y1 + # split string via comma to get x1 and y1 individually + # then point = [x1,x2] + point = pa[0].split(",") + + point = [float(point[0]),float(point[1])] + + Transform(matNew).apply_to_point(point) + + return True, point + + if node.tag == inkex.addNS( 'ellipse', 'svg' ) or \ + node.tag == 'ellipse': + + cx = float( node.get( 'cx', '0' ) ) + cy = float( node.get( 'cy', '0' ) ) + rx = float( node.get( 'rx', '0' ) ) + + point[0] = cx - rx + point[1] = cy + + Transform(matNew).apply_to_point(point) + + return True, point + + if node.tag == inkex.addNS( 'circle', 'svg' ) or \ + node.tag == 'circle': + cx = float( node.get( 'cx', '0' ) ) + cy = float( node.get( 'cy', '0' ) ) + r = float( node.get( 'r', '0' ) ) + point[0] = cx - r + point[1] = cy + + Transform(matNew).apply_to_point(point) + + return True, point + + if node.tag == inkex.addNS('symbol', 'svg') or node.tag == 'symbol': + # A symbol is much like a group, except that + # it's an invisible object. + return False, point # Skip this element. + + if node.tag == inkex.addNS('use', 'svg') or node.tag == 'use': + + """ + A element refers to another SVG element via an xlink:href="#blah" + attribute. We will handle the element by doing an XPath search through + the document, looking for the element with the matching id="blah" + attribute. We then recursively process that element after applying + any necessary (x,y) translation. + + Notes: + 1. We ignore the height and g attributes as they do not apply to + path-like elements, and + 2. Even if the use element has visibility="hidden", SVG still calls + for processing the referenced element. The referenced element is + hidden only if its visibility is "inherit" or "hidden". + 3. We may be able to unlink clones using the code in pathmodifier.py + """ + + refid = node.get(inkex.addNS('href', 'xlink')) + + if refid is not None: + # [1:] to ignore leading '#' in reference + path = '//*[@id="{0}"]'.format(refid[1:]) + refnode = node.xpath(path) + if refnode is not None: + + x = float(node.get('x', '0')) + y = float(node.get('y', '0')) + + # Note: the transform has already been applied + if x != 0 or y != 0: + mat_new2 = Transform(matNew) * Transform('translate({0:f},{1:f})'.format(x, y)) + else: + mat_new2 = matNew + # Note that the referenced object may be a 'symbol`, + # which acts like a group, or it may be a simple + # object. + + if len(refnode) > 0: + plottable, the_point = self.group_first_pt(refnode[0], mat_new2) + else: + plottable, the_point = self.group_first_pt(refnode, mat_new2) + + return plottable, the_point + except: + pass + + # Svg Object is not a plottable element + # In this case, return False to indicate a non-plottable element + # and a default point + + return False, point + + def getLastPoint(self, node, matCurrent): + """ + Input: XML tree node and transformation matrix + Output: Boolean value to indicate if the svg element is plottable or not and + two floats stored in a list representing the x and y coordinates we plot last + """ + + # first apply the current matrix transform to this node's transform + matNew = Transform(matCurrent) * Transform(node.get("transform")) + + # If we return a negative value, we know that this function did not work + point = [float(-1), float(-1)] + try: + if node.tag == inkex.addNS( 'path', 'svg' ): + + path = node.get('d') + + point = plot_utils.pathdata_last_point(path) + Transform(matNew).apply_to_point(point) + + return True, point + + if node.tag == inkex.addNS( 'rect', 'svg' ) or node.tag == 'rect': + + """ + The x,y coordinates for a rect are included in their specific attributes + If there is a transform, we need translate the x & y coordinates to their + correct location via Transform(matNew).apply_to_point(point). + """ + + point[0] = float( node.get( 'x' ) ) + point[1] = float( node.get( 'y' ) ) + + Transform(matNew).apply_to_point(point) + + return True, point # Same start and end points + + if node.tag == inkex.addNS( 'line', 'svg' ) or node.tag == 'line': + + """ + The x2 and y2 attributes are where we will end our drawing + So, get them, apply the transform matrix, and return the point + """ + + point[0] = float( node.get( 'x2' ) ) + point[1] = float( node.get( 'y2' ) ) + + Transform(matNew).apply_to_point(point) + + return True, point + + if node.tag == inkex.addNS( 'polyline', 'svg' ) or node.tag == 'polyline': + + pl = node.get( 'points', '' ).strip() + + if pl == '': + return False, point + + pa = pl.replace(',',' ').split() + if not pa: + return False, point + pathLength = len( pa ) + if (pathLength < 4): # Minimum of x1,y1 x2,y2 required. + return False, point + + d = "M " + pa[0] + " " + pa[1] + i = 2 + while (i < (pathLength - 1 )): + d += " L " + pa[i] + " " + pa[i + 1] + i += 2 + + endpoint = plot_utils.pathdata_last_point(d) + Transform(matNew).apply_to_point(point) + + return True, endpoint + + if node.tag == inkex.addNS( 'polygon', 'svg' ) or node.tag == 'polygon': + """ + We need to extract x1 and y1 from these: + + We accomplish this with Python string strip + and split methods. Then apply transforms + """ + # Strip() removes all whitespace from the start and end of p1 + pl = node.get( 'points', '' ).strip() + if (pl == ''): + # If pl is blank there has been an error, return -1,-1 to indicate a problem has occured + return False, point + # Split string by whitespace + pa = pl.split() + if not len( pa ): + # If pl is blank there has been an error, return -1,-1 to indicate a problem has occured + return False, point + # pa[0] = "x1,y1 + # split string via comma to get x1 and y1 individually + # then point = [x1,x2] + point = pa[0].split(",") + + point = [float(point[0]),float(point[1])] + + Transform(matNew).apply_to_point(point) + + return True, point + + if node.tag == inkex.addNS( 'ellipse', 'svg' ) or node.tag == 'ellipse': + + cx = float( node.get( 'cx', '0' ) ) + cy = float( node.get( 'cy', '0' ) ) + rx = float( node.get( 'rx', '0' ) ) + + point[0] = cx - rx + point[1] = cy + + Transform(matNew).apply_to_point(point) + + return True, point + + if node.tag == inkex.addNS( 'circle', 'svg' ) or node.tag == 'circle': + cx = float( node.get( 'cx', '0' ) ) + cy = float( node.get( 'cy', '0' ) ) + r = float( node.get( 'r', '0' ) ) + point[0] = cx - r + point[1] = cy + + Transform(matNew).apply_to_point(point) + + return True, point + + if node.tag == inkex.addNS('symbol', 'svg') or node.tag == 'symbol': + # A symbol is much like a group, except that it should only be + # rendered when called within a "use" tag. + return False, point # Skip this element. + + if node.tag == inkex.addNS('use', 'svg') or node.tag == 'use': + + """ + A element refers to another SVG element via an xlink:href="#blah" + attribute. We will handle the element by doing an XPath search through + the document, looking for the element with the matching id="blah" + attribute. We then recursively process that element after applying + any necessary (x,y) translation. + + Notes: + 1. We ignore the height and g attributes as they do not apply to + path-like elements, and + 2. Even if the use element has visibility="hidden", SVG still calls + for processing the referenced element. The referenced element is + hidden only if its visibility is "inherit" or "hidden". + 3. We may be able to unlink clones using the code in pathmodifier.py + """ + + refid = node.get(inkex.addNS('href', 'xlink')) + if refid is not None: + # [1:] to ignore leading '#' in reference + path = '//*[@id="{0}"]'.format(refid[1:]) + refnode = node.xpath(path) + if refnode is not None: + x = float(node.get('x', '0')) + y = float(node.get('y', '0')) + # Note: the transform has already been applied + if x != 0 or y != 0: + mat_new2 = Transform(matNew)* Transform('translate({0:f},{1:f})'.format(x, y)) + else: + mat_new2 = matNew + if len(refnode) > 0: + plottable, the_point = self.group_last_pt(refnode[0], mat_new2) + else: + plottable, the_point = self.group_last_pt(refnode, mat_new2) + return plottable, the_point + except: + pass + + # Svg Object is not a plottable element; + # Return False and a default point + return False, point + + + def group_first_pt(self, group, matCurrent = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]): + """ + Input: A Node which we have found to be a group + Output: Boolean value to indicate if a point is plottable + float values for first x,y coordinates of svg element + """ + + if len(group) == 0: # Empty group -- The object may not be a group. + return self.getFirstPoint(group, matCurrent) + + success = False + point = [float(-1), float(-1)] + + # first apply the current matrix transform to this node's transform + matNew = Transform( matCurrent) * Transform(group.get("transform")) + + # Step through the group, we examine each element until we find a plottable object + for subnode in group: + # Check to see if the subnode we are looking at in this iteration of our for loop is a group + # If it is a group, we must recursively call this function to search for a plottable object + if subnode.tag == inkex.addNS( 'g', 'svg' ) or subnode.tag == 'g': + # Verify that the nested group has objects within it + # otherwise we will not parse it + if subnode is not None: + # Check if group contains plottable elements by recursively calling group_first_pt + # If group contains plottable subnode, then it will return that value and escape the loop + # Else function continues search for first plottable object + success, point = self.group_first_pt(subnode, matNew) + if success: + # Subnode inside nested group is plottable! + # Break from our loop so we can return the first point of this plottable subnode + break + else: + continue + else: + # Node is not a group + # Get its first (x,y) coordinates + # Also get a Boolean value to indicate if the subnode is plottable or not + # If subnode is not plottable, continue to next subnode in the group + success, point = self.getFirstPoint(subnode, matNew) + + if success: + # Subnode inside group is plottable! + # Break from our loop so we can return the first point of this plottable subnode + break + else: + continue + return success, point + + + def group_last_pt(self, group, matCurrent=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]): + """ + Input: A Node which we have found to be a group + Output: The last node within the group which can be plotted + """ + + if len(group) == 0: # Empty group -- Did someone send an object that isn't a group? + return self.getLastPoint(group, matCurrent) + + success = False + point = [float(-1),float(-1)] + + # first apply the current matrix transform to this node's transform + matNew = Transform(matCurrent) * Transform(group.get("transform")) + + # Step through the group, we examine each element until we find a plottable object + for subnode in reversed(group): + # Check to see if the subnode we are looking at in this iteration of our for loop is a group + # If it is a group, we must recursively call this function to search for a plottable object + if subnode.tag == inkex.addNS( 'g', 'svg' ) or subnode.tag == 'g': + # Verify that the nested group has objects within it + # otherwise we will not parse it + if subnode is not None: + # Check if group contains plottable elements by recursively calling group_last_pt + # If group contains plottable subnode, then it will return that value and escape the loop + # Else function continues search for last plottable object + success, point = self.group_last_pt(subnode, matNew) + if success: + # Subnode inside nested group is plottable! + # Break from our loop so we can return the first point of this plottable subnode + break + else: + continue + else: + # Node is not a group + # Get its first (x,y) coordinates + # Also get a Boolean value to indicate if the subnode is plottable or not + # If subnode is not plottable, continue to next subnode in the group + success, point = self.getLastPoint(subnode, matNew) + if success: + + # Subode inside nested group is plottable! + # Break from our loop so we can return the first point of this plottable subnode + break + else: + continue + return success, point + + + def group2NodeDict(self, group, mat_current=None): + + if mat_current is None: + mat_current = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] + + # first apply the current matrix transform to this node's transform + matNew = Transform(mat_current) * Transform(group.get("transform")) + + nodes_in_group = [] + + # Step through the group, we examine each element until we find a plottable object + for subnode in group: + # Check to see if the subnode we are looking at in this iteration of our for loop is a group + # If it is a group, we must recursively call this function to search for a plottable object + if subnode.tag == inkex.addNS( 'g', 'svg' ) or subnode.tag == 'g': + # Verify that the nested group has objects within it + # otherwise we will not parse it + if subnode is not None: + # Check if group contains plottable elements by recursively calling group_first_pt + # If group contains plottable subnode, then it will return that value and escape the loop + # Else function continues search for first plottable object + nodes_in_group.extend(self.group2NodeDict(subnode, matNew)) + else: + Transform(matNew) * Transform(subnode) + nodes_in_group.append(subnode) + return nodes_in_group + + + def ReorderNodeList(self, coord_dict, group_dict): + # Re-order the given set of SVG elements, using a simple "greedy" algorithm. + # The first object will be the element closest to the origin + # After this choice, the algorithm loops through all remaining elements looking for the element whose first x,y + # coordinates are closest to the the previous choice's last x,y coordinates + # This process continues until all elements have been sorted into ordered_element_list and removed from group_dict + + ordered_layer_element_list = [] + + # Continue until all elements have been re-ordered + while group_dict: + + nearest_dist = float('inf') + for key,node in group_dict.items(): + # Is this node non-plottable? + # If so, exit loop and append element to ordered_layer_element_list + if not coord_dict[key][0]: + # Object is not Plottable + nearest = node + nearest_id = key + continue + + # If we reach this point, node is plottable and needs to be considered in our algo + entry_x = coord_dict[key][1] # x-coordinate of first point of the path + entry_y = coord_dict[key][2] # y-coordinate of first point of the path + + exit_x = coord_dict[key][3] # x-coordinate of last point of the path + exit_y = coord_dict[key][4] # y-coordinate of last point of the path + + object_dist = (entry_x-self.x_last)*(entry_x-self.x_last) + (entry_y-self.y_last) * (entry_y-self.y_last) + # This is actually the distance squared; calculating it rather than the pythagorean distance + # saves a square root calculation. Right now, we only care about _which distance is less_ + # not the exact value of it, so this is a harmless shortcut. + # If this distance is smaller than the previous element's distance, then replace the previous + # element's entry with our current element's distance + if nearest_dist >= object_dist: + # We have found an element closer than the previous closest element + nearest = node + nearest_id = key + nearest_dist = object_dist + nearest_start_x = entry_x + nearest_start_y = entry_y + + # Now that the closest object has been determined, it is time to add it to the + # optimized list of closest objects + ordered_layer_element_list.append(nearest) + + # To determine the closest object in the next iteration of the loop, + # we must save the last x,y coor of this element + # If this element is plottable, then save the x,y coordinates + # If this element is non-plottable, then do not save the x,y coordinates + if coord_dict[nearest_id][0]: + + # Also, draw line indicating that we've found a new point. + if self.preview_rendering: + preview_path = [] # pen-up path data for preview + + preview_path.append("M{0:.3f} {1:.3f}".format( + self.x_last, self.y_last)) + preview_path.append("{0:.3f} {1:.3f}".format( + nearest_start_x, nearest_start_y)) + self.p_style.update({'stroke': self.color_index(self.layer_index)}) + path_attrs = { + 'style': str(inkex.Style(self.p_style)), + 'd': " ".join(preview_path)} + + etree.SubElement( self.preview_layer, + inkex.addNS( 'path', 'svg '), path_attrs, nsmap=inkex.NSS ) + + self.x_last = coord_dict[nearest_id][3] + self.y_last = coord_dict[nearest_id][4] + + # Remove this element from group_dict to indicate it has been optimized + del group_dict[nearest_id] + + # Once all elements have been removed from the group_dictionary + # Return the optimized list of svg elements in the layer + return ordered_layer_element_list + + + def color_index(self, index): + index = index % 9 + + if index == 0: + return "rgb(255, 0, 0))" + elif index == 1: + return "rgb(170, 85, 0))" + elif index == 2: + return "rgb(85, 170, 0))" + elif index == 3: + return "rgb(0, 255, 0))" + elif index == 4: + return "rgb(0, 170, 85))" + elif index == 5: + return "rgb(0, 85, 170))" + elif index == 6: + return "rgb(0, 0, 255))" + elif index == 7: + return "rgb(85, 0, 170))" + else: + return "rgb(170, 0, 85))" + + + def getDocProps(self): + """ + Get the document's height and width attributes from the tag. + Use a default value in case the property is not present or is + expressed in units of percentages. + """ + + self.svg_height = plot_utils.getLengthInches(self, 'height') + self.svg_width = plot_utils.getLengthInches(self, 'width') + + width_string = self.svg.get('width') + if width_string: + value, units = plot_utils.parseLengthWithUnits(width_string) + self.doc_units = units + + if self.auto_rotate and (self.svg_height > self.svg_width): + self.printPortrait = True + if self.svg_height is None or self.svg_width is None: + return False + else: + return True + + + def get_output(self): + # Return serialized copy of svg document output + result = etree.tostring(self.document) + return result.decode("utf-8") + +# Create effect instance and apply it. + +if __name__ == '__main__': + ReorderEffect().run() \ No newline at end of file diff --git a/extensions/fablabchemnitz_tool_covers.py b/extensions/fablabchemnitz_tool_covers.py new file mode 100644 index 00000000..b7876acb --- /dev/null +++ b/extensions/fablabchemnitz_tool_covers.py @@ -0,0 +1,495 @@ +#!/usr/bin/env python3 +# +# (c) 2020 Yoichi Tanibayashi +# +import inkex +from lxml import etree +import math +inkex.localization.localize + + +class Point(object): + def __init__(self, x, y): + self.x = x + self.y = y + + def distance(self, c): + return math.sqrt((c.x - self.x) ** 2 + (c.y - self.y) ** 2) + + def rotate(self, rad): + new_x = math.cos(rad) * self.x - math.sin(rad) * self.y + new_y = math.sin(rad) * self.x + math.cos(rad) * self.y + self.x = new_x + self.y = new_y + return self + + def mirror(self): + self.x = -self.x + return self + + +class Vpoint(Point): + ''' + (x, y)座標と方向(rad)を持つ点 + + rad: 方向(真上: 0, 右: math.pi / 2, ..) + ''' + def __init__(self, x, y, rad=0): + super(Vpoint, self).__init__(x, y) + self.rad = rad + + def rotate(self, rad): + super(Vpoint, self).rotate(rad) + self.rad += rad + return self + + def mirror(self): + super(Vpoint, self).mirror() + self.rad = -self.rad + return self + + +class SvgObj(object): + DEF_COLOR = '#00FF00' + DEF_STROKE_WIDTH = 0.2 + DEF_STROKE_DASHARRAY = 'none' + + def __init__(self, parent): + self.parent = parent + self.type = None + self.attr = {} + + def draw(self, color=DEF_COLOR, + stroke_width=DEF_STROKE_WIDTH, + stroke_dasharray=DEF_STROKE_DASHARRAY): + + self.attr['style'] = str(inkex.Style({ + 'stroke': str(color), + 'stroke-width': str(stroke_width), + 'stroke-dasharray': str(stroke_dasharray), + 'fill': 'none'})) + return etree.SubElement(self.parent, + inkex.addNS(self.type, 'svg'), + self.attr) + + +class SvgCircle(SvgObj): + DEF_COLOR = '#FF0000' + DEF_STROKE_WIDTH = 0.2 + DEF_STROKE_DASHARRAY = 'none' + + def __init__(self, parent, r): + super(SvgCircle, self).__init__(parent) + self.r = r + self.type = 'circle' + + def draw(self, point, + color=DEF_COLOR, + stroke_width=DEF_STROKE_WIDTH, + stroke_dasharray=DEF_STROKE_DASHARRAY): + self.attr['cx'] = str(point.x) + self.attr['cy'] = str(point.y) + self.attr['r'] = str(self.r) + + return super(SvgCircle, self).draw(color, + stroke_width, stroke_dasharray) + + +class SvgPath(SvgObj): + DEF_COLOR = '#0000FF' + DEF_STROKE_WIDTH = 0.2 + DEF_STROKE_DASHARRAY = 'none' + + def __init__(self, parent, points): + super(SvgPath, self).__init__(parent) + self.points = points + self.type = 'path' + + def create_svg_d(self, origin_vpoint, points): + ''' + to be override + + This is sample code. + ''' + svg_d = '' + for i, p in enumerate(points): + (x1, y1) = (p.x + origin_vpoint.x, p.y + origin_vpoint.y) + if i == 0: + svg_d = 'M %f,%f' % (x1, y1) + else: + svg_d += ' L %f,%f' % (x1, y1) + return svg_d + + def rotate(self, rad): + for p in self.points: + p.rotate(rad) + return self + + def mirror(self): + for p in self.points: + p.mirror() + return self + + def draw(self, origin, + color=DEF_COLOR, stroke_width=DEF_STROKE_WIDTH, + stroke_dasharray=DEF_STROKE_DASHARRAY): + + self.rotate(origin.rad) + + svg_d = self.create_svg_d(origin, self.points) + # inkex.errormsg('svg_d=%s' % svg_d) + # inkex.errormsg('svg_d=%s' % str(Path( svg_d ))) + + self.attr['d'] = svg_d + return super(SvgPath, self).draw(color, stroke_width, stroke_dasharray) + + +class SvgLine(SvgPath): + # exactly same as SvgPath + pass + + +class SvgPolygon(SvgPath): + def create_svg_d(self, origin, points): + svg_d = super(SvgPolygon, self).create_svg_d(origin, points) + svg_d += ' Z' + return svg_d + + +class SvgPart1Outline(SvgPolygon): + def __init__(self, parent, points, bw_bf): + super(SvgPart1Outline, self).__init__(parent, points) + self.bw_bf = bw_bf + + def create_svg_d(self, origin, points, bw_bf=1): + for i, p in enumerate(points): + (x1, y1) = (p.x + origin.x, p.y + origin.y) + if i == 0: + d = 'M %f,%f' % (x1, y1) + elif i == 7: + d += ' L %f,%f' % (x1, y1) + x2 = x1 + y2 = y1 + self.bw_bf + elif i == 8: + d += ' C %f,%f %f,%f %f,%f' % (x2, y2, x1, y2, x1, y1) + else: + d += ' L %f,%f' % (x1, y1) + + d += ' Z' + return d + + +class SvgNeedleHole(SvgPolygon): + def __init__(self, parent, w, h, tf): + ''' + w: width + h: height + tf: tilt factor + ''' + self.w = w + self.h = h + self.tf = tf + + self.gen_points(self.w, self.h, self.tf) + super(SvgNeedleHole, self).__init__(parent, self.points) + + def gen_points(self, w, h, tf): + self.points = [] + self.points.append(Point(-w / 2, h * tf)) + self.points.append(Point( w / 2, h * (1 - tf))) + self.points.append(Point( w / 2, -h * tf)) + self.points.append(Point(-w / 2, -h * (1 - tf))) + + +class Part1(object): + def __init__(self, parent, + w1, w2, h1, h2, bw, bl, bf, dia1, d1, d2, + needle_w, needle_h, needle_tf, needle_corner_rotation): + self.parent = parent + self.w1 = w1 + self.w2 = w2 + self.h1 = h1 + self.h2 = h2 + self.bw = bw + self.bl = bl + self.bf = bf + self.dia1 = dia1 + self.d1 = d1 + self.d2 = d2 + self.needle_w = needle_w + self.needle_h = needle_h + self.needle_tf = needle_tf + self.needle_corner_rotation = needle_corner_rotation + + # グループ作成 + attr = {inkex.addNS('label', 'inkscape'): 'Part1'} + self.g = etree.SubElement(self.parent, 'g', attr) + + # 図形作成 + self.points_outline = self.create_points_outline() + self.svg_outline = SvgPart1Outline(self.g, self.points_outline, + (self.bw * self.bf)) + self.svg_hole = SvgCircle(self.g, self.dia1 / 2) + + self.vpoints_needle = self.create_needle_vpoints() + self.svgs_needle_hole = [] + for v in self.vpoints_needle: + svg_nh = SvgNeedleHole(self.g, + self.needle_w, + self.needle_h, + self.needle_tf) + self.svgs_needle_hole.append((svg_nh, v)) + + def create_points_outline(self): + ''' + 外枠の座標を生成 + ''' + points = [] + (x0, y0) = (-(self.w2 / 2), 0) + + (x, y) = (x0, y0 + self.h1 + self.h2) + points.append(Point(x, y)) + + y = y0 + self.h1 + points.append(Point(x, y)) + + x = -(self.w1 / 2) + y = y0 + points.append(Point(x, y)) + + x = self.w1 / 2 + points.append(Point(x, y)) + + x = self.w2 / 2 + y += self.h1 + points.append(Point(x, y)) + + y += self.h2 + points.append(Point(x, y)) + + x = self.bw / 2 + points.append(Point(x, y)) + + y += self.bl - self.bw / 2 + points.append(Point(x, y)) + + x = -(self.bw / 2) + points.append(Point(x, y)) + + y = y0 + self.h1 + self.h2 + points.append(Point(x, y)) + + return points + + def create_needle_vpoints(self): + ''' + 針穴の点と方向を生成 + ''' + rad1 = math.atan((self.w2 - self.w1) / (2 * self.h1)) + rad1a = (math.pi - rad1) / 2 + a1 = self.d1 / math.tan(rad1a) + + rad2 = (math.pi / 2) - rad1 + rad2a = (math.pi - rad2) / 2 + a2 = self.d1 / math.tan(rad2a) + + # + # 頂点 + # + vpoints1 = [] + for i, p in enumerate(self.points_outline): + (nx, ny) = (p.x, p.y) + if i == 0: + nx += self.d1 + ny -= self.d1 * 1.5 + vpoints1.append(Vpoint(nx, ny, 0)) + if i == 1: + nx += self.d1 + ny += a1 + vpoints1.append(Vpoint(nx, ny, rad1)) + if i == 2: + nx += a2 + ny += self.d1 + vpoints1.append(Vpoint(nx, ny, math.pi / 2)) + if i == 3: + nx -= a2 + ny += self.d1 + vpoints1.append(Vpoint(nx, ny, (math.pi / 2) + rad2)) + if i == 4: + nx -= self.d1 + ny += a1 + vpoints1.append(Vpoint(nx, ny, math.pi)) + if i == 5: + nx -= self.d1 + ny -= self.d1 * 1.5 + vpoints1.append(Vpoint(nx, ny, math.pi)) + if i > 5: + break + + # 頂点を補完する点を生成 + vpoints2 = [] + for i in range(len(vpoints1)-1): + d = vpoints1[i].distance(vpoints1[i+1]) + n = int(abs(round(d / self.d2))) + for p in self.split_vpoints(vpoints1[i], vpoints1[i+1], n): + vpoints2.append(p) + + vpoints2.insert(0, vpoints1[0]) + return vpoints2 + + def split_vpoints(self, v1, v2, n): + ''' + v1, v2間をn個に分割して、リストを生成 + ''' + if n == 0: + return [v1] + (dx, dy) = ((v2.x - v1.x) / n, (v2.y - v1.y) / n) + + v = [] + for i in range(n): + v.append(Vpoint(v1.x + dx * (i + 1), + v1.y + dy * (i + 1), + v1.rad)) + if self.needle_corner_rotation: + v[-1].rad = (v1.rad + v2.rad) / 2 + return v + + def draw(self, origin): + origin_base = Vpoint(origin.x + self.w2 / 2, + origin.y, + origin.rad) + self.svg_outline.draw(origin_base, color='#0000FF') + + x = origin.x + self.w2 / 2 + y = origin.y + self.h1 + self.h2 + self.bl - self.bw / 2 + origin_hole = Point(x, y) + self.svg_hole.draw(origin_hole, color='#FF0000') + + for (svg_nh, p) in self.svgs_needle_hole: + origin_nh = Vpoint(origin.x + p.x + self.w2 / 2, + origin.y + p.y, + p.rad) + svg_nh.draw(origin_nh, color='#FF0000') + + +class Part2(object): + def __init__(self, parent, part1, dia2): + self.parent = parent + self.part1 = part1 + self.dia2 = dia2 + + # グループ作成 + attr = {inkex.addNS('label', 'inkscape'): 'Part2'} + self.g = etree.SubElement(self.parent, 'g', attr) + + # 外枠 + # ``Part1``の``points_outline``をミラーして、 + # 最初の6つのポイントを利用 + self.points_outline = [] + for i in range(6): + self.points_outline.append(self.part1.points_outline[i].mirror()) + + self.svg_outline = SvgPolygon(self.g, self.points_outline) + + # 留め具 + self.svg_hole = SvgCircle(self.g, self.dia2 / 2) + + # 針穴 + # ``Part1``の``vpoints_needle``をミラーして利用 + self.svgs_needle_hole = [] + for v in self.part1.vpoints_needle: + v.mirror() + # ``SvgNeedleHole``もミラーする + svg_nh = SvgNeedleHole(self.g, + self.part1.needle_w, + self.part1.needle_h, + self.part1.needle_tf) + svg_nh.mirror() + self.svgs_needle_hole.append((svg_nh, v)) + + def draw(self, origin): + origin_base = Vpoint(origin.x + self.part1.w2 / 2, + origin.y, origin.rad) + self.svg_outline.draw(origin_base, color='#0000FF') + + x = origin.x + self.part1.w2 / 2 + y = origin.y + self.part1.h1 + self.part1.h2 + y -= (self.svg_hole.r + self.part1.d1) + origin_hole = Vpoint(x, y, origin.rad) + self.svg_hole.draw(origin_hole, color='#FF0000') + + for (svg_nh, p) in self.svgs_needle_hole: + origin_nh = Vpoint(origin.x + p.x + self.part1.w2 / 2, + origin.y + p.y, + p.rad) + svg_nh.draw(origin_nh, color='#FF0000') + + +class PliersCover(inkex.Effect): + DEF_OFFSET_X = 20 + DEF_OFFSET_Y = 20 + + def __init__(self): + inkex.Effect.__init__(self) + self.arg_parser.add_argument("--tabs") + self.arg_parser.add_argument("--w1", type=float) + self.arg_parser.add_argument("--w2", type=float) + self.arg_parser.add_argument("--h1", type=float) + self.arg_parser.add_argument("--h2", type=float) + self.arg_parser.add_argument("--bw", type=float) + self.arg_parser.add_argument("--bl", type=float) + self.arg_parser.add_argument("--bf", type=float) + self.arg_parser.add_argument("--dia1", type=float) + self.arg_parser.add_argument("--dia2", type=float) + self.arg_parser.add_argument("--d1", type=float) + self.arg_parser.add_argument("--d2", type=float) + self.arg_parser.add_argument("--needle_w", type=float) + self.arg_parser.add_argument("--needle_h", type=float) + self.arg_parser.add_argument("--needle_tf", type=float) + self.arg_parser.add_argument("--needle_corner_rotation", type=inkex.Boolean, default=True) + + def effect(self): + # inkex.errormsg('view_center=%s' % str(self.view_center)) + # inkex.errormsg('selected=%s' % str(self.selected)) + + # parameters + opt = self.options + + # + # error check + # + if opt.w1 >= opt.w2: + msg = "Error: w1(%d) > w2(%d) !" % (opt.w1, opt.w2) + inkex.errormsg(msg) + return + + if opt.dia1 >= opt.bw: + msg = "Error: dia1(%d) >= bw(%d) !" % (opt.dia1, opt.bw) + inkex.errormsg(msg) + return + + # + # draw + # + origin_vpoint = Vpoint(self.DEF_OFFSET_X, self.DEF_OFFSET_Y) + + # グループ作成 + attr = {inkex.addNS('label', 'inkscape'): 'PliersCover'} + self.g = etree.SubElement(self.svg.get_current_layer(), 'g', attr) + + part1 = Part1(self.g, + opt.w1, opt.w2, opt.h1, opt.h2, + opt.bw, opt.bl, opt.bf, opt.dia1, + opt.d1, opt.d2, + opt.needle_w, opt.needle_h, opt.needle_tf, + opt.needle_corner_rotation) + part1.draw(origin_vpoint) + + origin_vpoint.x += opt.w2 + 10 + + part2 = Part2(self.g, part1, opt.dia2) + part2.draw(origin_vpoint) + + +if __name__ == '__main__': + PliersCover().run() \ No newline at end of file